From eca8dd93c5e01a02431c55ebe1cfbb8ad5c444d4 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 29 May 2024 20:54:49 +0200 Subject: [PATCH 0001/1445] Bump version to 2024.7.0dev0 (#118399) --- .github/workflows/ci.yaml | 2 +- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 6cb8f8deec4..8c1b11e13ff 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -36,7 +36,7 @@ env: CACHE_VERSION: 8 UV_CACHE_VERSION: 1 MYPY_CACHE_VERSION: 8 - HA_SHORT_VERSION: "2024.6" + HA_SHORT_VERSION: "2024.7" DEFAULT_PYTHON: "3.12" ALL_PYTHON_VERSIONS: "['3.12']" # 10.3 is the oldest supported version diff --git a/homeassistant/const.py b/homeassistant/const.py index f5f5b35691c..da059d4230d 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -23,7 +23,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 -MINOR_VERSION: Final = 6 +MINOR_VERSION: Final = 7 PATCH_VERSION: Final = "0.dev0" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" diff --git a/pyproject.toml b/pyproject.toml index bd9e801de8c..9484420adb9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.6.0.dev0" +version = "2024.7.0.dev0" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 8cc15e82df21b390d8f68d3de72e8e1014382353 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Wed, 29 May 2024 21:09:50 +0200 Subject: [PATCH 0002/1445] Fix light discovery for Matter dimmable plugin unit (#118404) --- homeassistant/components/matter/light.py | 1 + .../fixtures/nodes/dimmable-plugin-unit.json | 502 ++++++++++++++++++ tests/components/matter/test_light.py | 1 + 3 files changed, 504 insertions(+) create mode 100644 tests/components/matter/fixtures/nodes/dimmable-plugin-unit.json diff --git a/homeassistant/components/matter/light.py b/homeassistant/components/matter/light.py index acd85884875..89400c98989 100644 --- a/homeassistant/components/matter/light.py +++ b/homeassistant/components/matter/light.py @@ -435,6 +435,7 @@ DISCOVERY_SCHEMAS = [ device_type=( device_types.ColorTemperatureLight, device_types.DimmableLight, + device_types.DimmablePlugInUnit, device_types.ExtendedColorLight, device_types.OnOffLight, ), diff --git a/tests/components/matter/fixtures/nodes/dimmable-plugin-unit.json b/tests/components/matter/fixtures/nodes/dimmable-plugin-unit.json new file mode 100644 index 00000000000..5b1e1cfaba6 --- /dev/null +++ b/tests/components/matter/fixtures/nodes/dimmable-plugin-unit.json @@ -0,0 +1,502 @@ +{ + "node_id": 36, + "date_commissioned": "2024-05-18T13:06:23.766788", + "last_interview": "2024-05-18T13:06:23.766793", + "interview_version": 6, + "available": true, + "is_bridge": false, + "attributes": { + "0/29/0": [ + { + "0": 22, + "1": 1 + } + ], + "0/29/1": [29, 31, 40, 42, 43, 44, 48, 49, 50, 51, 52, 54, 60, 62, 63], + "0/29/2": [41], + "0/29/3": [1], + "0/29/65532": 0, + "0/29/65533": 1, + "0/29/65528": [], + "0/29/65529": [], + "0/29/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533], + "0/31/0": [ + { + "1": 5, + "2": 2, + "3": [112233], + "4": null, + "254": 2 + } + ], + "0/31/1": [], + "0/31/2": 4, + "0/31/3": 3, + "0/31/4": 3, + "0/31/65532": 0, + "0/31/65533": 1, + "0/31/65528": [], + "0/31/65529": [], + "0/31/65531": [0, 1, 2, 3, 4, 65528, 65529, 65531, 65532, 65533], + "0/40/0": 1, + "0/40/1": "Matter", + "0/40/2": 4251, + "0/40/3": "Dimmable Plugin Unit", + "0/40/4": 4098, + "0/40/5": "", + "0/40/6": "", + "0/40/7": 1, + "0/40/8": "1.0", + "0/40/9": 131365, + "0/40/10": "2.1.25", + "0/40/15": "1000_0030_D228", + "0/40/18": "E2B4285EEDD3A387", + "0/40/19": { + "0": 3, + "1": 3 + }, + "0/40/65532": 0, + "0/40/65533": 1, + "0/40/65528": [], + "0/40/65529": [], + "0/40/65531": [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 15, 18, 19, 65528, 65529, 65531, 65532, + 65533 + ], + "0/42/0": [], + "0/42/1": true, + "0/42/2": 1, + "0/42/3": null, + "0/42/65532": 0, + "0/42/65533": 1, + "0/42/65528": [], + "0/42/65529": [0], + "0/42/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533], + "0/43/0": "en-US", + "0/43/1": [ + "en-US", + "de-DE", + "fr-FR", + "en-GB", + "es-ES", + "zh-CN", + "it-IT", + "ja-JP" + ], + "0/43/65532": 0, + "0/43/65533": 1, + "0/43/65528": [], + "0/43/65529": [], + "0/43/65531": [0, 1, 65528, 65529, 65531, 65532, 65533], + "0/44/0": 0, + "0/44/1": 0, + "0/44/2": [0, 1, 2, 3, 4, 5, 6, 8, 9, 10, 11, 7], + "0/44/65532": 0, + "0/44/65533": 1, + "0/44/65528": [], + "0/44/65529": [], + "0/44/65531": [0, 1, 2, 65528, 65529, 65531, 65532, 65533], + "0/48/0": 0, + "0/48/1": { + "0": 60, + "1": 900 + }, + "0/48/2": 0, + "0/48/3": 0, + "0/48/4": true, + "0/48/65532": 0, + "0/48/65533": 1, + "0/48/65528": [1, 3, 5], + "0/48/65529": [0, 2, 4], + "0/48/65531": [0, 1, 2, 3, 4, 65528, 65529, 65531, 65532, 65533], + "0/49/0": 1, + "0/49/1": [ + { + "0": "", + "1": true + } + ], + "0/49/2": 10, + "0/49/3": 20, + "0/49/4": true, + "0/49/5": 0, + "0/49/6": "", + "0/49/7": null, + "0/49/65532": 1, + "0/49/65533": 1, + "0/49/65528": [1, 5, 7], + "0/49/65529": [0, 2, 4, 6, 8], + "0/49/65531": [0, 1, 2, 3, 4, 5, 6, 7, 65528, 65529, 65531, 65532, 65533], + "0/50/65532": 0, + "0/50/65533": 1, + "0/50/65528": [1], + "0/50/65529": [0], + "0/50/65531": [65528, 65529, 65531, 65532, 65533], + "0/51/0": [ + { + "0": "r0", + "1": true, + "2": null, + "3": null, + "4": "AAemN9h0", + "5": ["wKhr7Q=="], + "6": ["/oAAAAAAAAACB6b//jfYdA=="], + "7": 1 + } + ], + "0/51/1": 2, + "0/51/2": 86407, + "0/51/3": 24, + "0/51/4": 0, + "0/51/5": [], + "0/51/6": [], + "0/51/7": [], + "0/51/8": false, + "0/51/65532": 0, + "0/51/65533": 1, + "0/51/65528": [], + "0/51/65529": [0], + "0/51/65531": [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 65528, 65529, 65531, 65532, 65533 + ], + "0/52/0": [ + { + "0": 26, + "1": "Logging~", + "2": 16224, + "3": 4056, + "4": 16352 + }, + { + "0": 26, + "1": "Logging", + "2": 16224, + "3": 4056, + "4": 16352 + }, + { + "0": 34, + "1": "cnR3X3JlY5c=", + "2": 5560, + "3": 862, + "4": 5856 + }, + { + "0": 36, + "1": "rtw_intz", + "2": 832, + "3": 200, + "4": 992 + }, + { + "0": 14, + "1": "interacZ", + "2": 4784, + "3": 1090, + "4": 5088 + }, + { + "0": 37, + "1": "cmd_thr", + "2": 3880, + "3": 718, + "4": 4064 + }, + { + "0": 4, + "1": "LOGUART\u0010", + "2": 3896, + "3": 974, + "4": 4064 + }, + { + "0": 3, + "1": "log_ser\n", + "2": 4968, + "3": 1242, + "4": 5088 + }, + { + "0": 35, + "1": "rtw_xmi\u0014", + "2": 840, + "3": 168, + "4": 992 + }, + { + "0": 49, + "1": "mesh_pr", + "2": 680, + "3": 42, + "4": 992 + }, + { + "0": 47, + "1": "BLE_app", + "2": 4864, + "3": 1112, + "4": 5088 + }, + { + "0": 44, + "1": "trace_t", + "2": 280, + "3": 68, + "4": 480 + }, + { + "0": 45, + "1": "UpperSt", + "2": 2904, + "3": 620, + "4": 3040 + }, + { + "0": 46, + "1": "HCI I/F", + "2": 1800, + "3": 356, + "4": 2016 + }, + { + "0": 8, + "1": "Tmr Svc", + "2": 3940, + "3": 933, + "4": 4076 + }, + { + "0": 38, + "1": "lev_snt", + "2": 3960, + "3": 930, + "4": 4064 + }, + { + "0": 27, + "1": "iot_thr", + "2": 7968, + "3": 1984, + "4": 8160 + }, + { + "0": 28, + "1": "iot_thr", + "2": 7968, + "3": 1984, + "4": 8160 + }, + { + "0": 2, + "1": "lev_hea", + "2": 3824, + "3": 831, + "4": 4064 + }, + { + "0": 23, + "1": "Wifi_Co", + "2": 7872, + "3": 1879, + "4": 8160 + }, + { + "0": 40, + "1": "lev_ota", + "2": 7896, + "3": 1442, + "4": 8160 + }, + { + "0": 39, + "1": "Schedul", + "2": 1696, + "3": 404, + "4": 2016 + }, + { + "0": 29, + "1": "AWS_MQT", + "2": 7832, + "3": 1824, + "4": 8160 + }, + { + "0": 41, + "1": "lev_net", + "2": 7768, + "3": 1788, + "4": 8160 + }, + { + "0": 18, + "1": "Lev_Tim", + "2": 3976, + "3": 948, + "4": 4064 + }, + { + "0": 1, + "1": "WATCHDO", + "2": 888, + "3": 212, + "4": 992 + }, + { + "0": 9, + "1": "TCP_IP", + "2": 3808, + "3": 644, + "4": 3968 + }, + { + "0": 50, + "1": "Bluetoo", + "2": 8000, + "3": 1990, + "4": 8160 + }, + { + "0": 20, + "1": "SHADOW_", + "2": 3736, + "3": 924, + "4": 4064 + }, + { + "0": 17, + "1": "NV_PROP", + "2": 1824, + "3": 446, + "4": 2016 + }, + { + "0": 16, + "1": "DIM_TAS", + "2": 1920, + "3": 460, + "4": 2016 + }, + { + "0": 19, + "1": "Lev_But", + "2": 3872, + "3": 956, + "4": 4064 + }, + { + "0": 7, + "1": "IDLE", + "2": 1944, + "3": 478, + "4": 2040 + }, + { + "0": 51, + "1": "CHIP", + "2": 6840, + "3": 1126, + "4": 8160 + } + ], + "0/52/1": 62880, + "0/52/2": 249440, + "0/52/3": 259456, + "0/52/65532": 1, + "0/52/65533": 1, + "0/52/65528": [], + "0/52/65529": [0], + "0/52/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533], + "0/54/0": "", + "0/54/1": 4, + "0/54/2": 3, + "0/54/3": 11, + "0/54/4": -66, + "0/54/65532": 0, + "0/54/65533": 1, + "0/54/65528": [], + "0/54/65529": [], + "0/54/65531": [0, 1, 2, 3, 4, 65528, 65529, 65531, 65532, 65533], + "0/60/0": 0, + "0/60/1": null, + "0/60/2": null, + "0/60/65532": 0, + "0/60/65533": 1, + "0/60/65528": [], + "0/60/65529": [0, 1, 2], + "0/60/65531": [0, 1, 2, 65528, 65529, 65531, 65532, 65533], + "0/62/2": 5, + "0/62/3": 2, + "0/62/5": 2, + "0/62/65532": 0, + "0/62/65533": 1, + "0/62/65528": [1, 3, 5, 8], + "0/62/65529": [0, 2, 4, 6, 7, 9, 10, 11], + "0/62/65531": [0, 1, 2, 3, 4, 5, 65528, 65529, 65531, 65532, 65533], + "0/63/0": [], + "0/63/1": [], + "0/63/2": 3, + "0/63/3": 3, + "0/63/65532": 0, + "0/63/65533": 1, + "0/63/65528": [2, 5], + "0/63/65529": [0, 1, 3, 4], + "0/63/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533], + "1/3/0": 0, + "1/3/1": 2, + "1/3/65532": 0, + "1/3/65533": 4, + "1/3/65528": [], + "1/3/65529": [0, 64], + "1/3/65531": [0, 1, 65528, 65529, 65531, 65532, 65533], + "1/4/0": 128, + "1/4/65532": 1, + "1/4/65533": 4, + "1/4/65528": [0, 1, 2, 3], + "1/4/65529": [0, 1, 2, 3, 4, 5], + "1/4/65531": [0, 65528, 65529, 65531, 65532, 65533], + "1/6/0": true, + "1/6/16384": true, + "1/6/16385": 0, + "1/6/16386": 0, + "1/6/16387": null, + "1/6/65532": 1, + "1/6/65533": 4, + "1/6/65528": [], + "1/6/65529": [0, 1, 2, 64, 65, 66], + "1/6/65531": [ + 0, 16384, 16385, 16386, 16387, 65528, 65529, 65531, 65532, 65533 + ], + "1/8/0": 254, + "1/8/1": 0, + "1/8/2": 1, + "1/8/3": 254, + "1/8/15": 0, + "1/8/16": 10, + "1/8/17": null, + "1/8/20": 50, + "1/8/16384": null, + "1/8/65532": 3, + "1/8/65533": 5, + "1/8/65528": [], + "1/8/65529": [0, 1, 2, 3, 4, 5, 6, 7], + "1/8/65531": [ + 0, 1, 2, 3, 15, 16, 17, 20, 16384, 65528, 65529, 65531, 65532, 65533 + ], + "1/29/0": [ + { + "0": 267, + "1": 1 + } + ], + "1/29/1": [3, 4, 6, 8, 29], + "1/29/2": [], + "1/29/3": [], + "1/29/65532": 0, + "1/29/65533": 1, + "1/29/65528": [], + "1/29/65529": [], + "1/29/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533] + }, + "attribute_subscriptions": [] +} diff --git a/tests/components/matter/test_light.py b/tests/components/matter/test_light.py index 775790701d1..2589e041b3b 100644 --- a/tests/components/matter/test_light.py +++ b/tests/components/matter/test_light.py @@ -116,6 +116,7 @@ async def test_light_turn_on_off( ("extended-color-light", "light.mock_extended_color_light"), ("color-temperature-light", "light.mock_color_temperature_light"), ("dimmable-light", "light.mock_dimmable_light"), + ("dimmable-plugin-unit", "light.dimmable_plugin_unit"), ], ) async def test_dimmable_light( From a0443ac328cd87443139b219a4d5a8c1e211ff76 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Wed, 29 May 2024 21:12:47 +0200 Subject: [PATCH 0003/1445] Add translation strings for Matter Fan presets (#118401) --- homeassistant/components/matter/icons.json | 21 ++++++++++++++++++++ homeassistant/components/matter/strings.json | 16 +++++++++++++++ 2 files changed, 37 insertions(+) create mode 100644 homeassistant/components/matter/icons.json diff --git a/homeassistant/components/matter/icons.json b/homeassistant/components/matter/icons.json new file mode 100644 index 00000000000..94da41931de --- /dev/null +++ b/homeassistant/components/matter/icons.json @@ -0,0 +1,21 @@ +{ + "entity": { + "fan": { + "fan": { + "state_attributes": { + "preset_mode": { + "default": "mdi:fan", + "state": { + "low": "mdi:fan-speed-1", + "medium": "mdi:fan-speed-2", + "high": "mdi:fan-speed-3", + "auto": "mdi:fan-auto", + "natural_wind": "mdi:tailwind", + "sleep_wind": "mdi:sleep" + } + } + } + } + } + } +} diff --git a/homeassistant/components/matter/strings.json b/homeassistant/components/matter/strings.json index c68b38bbb8c..c6c2d779255 100644 --- a/homeassistant/components/matter/strings.json +++ b/homeassistant/components/matter/strings.json @@ -62,6 +62,22 @@ } } }, + "fan": { + "fan": { + "state_attributes": { + "preset_mode": { + "state": { + "low": "Low", + "medium": "Medium", + "high": "High", + "auto": "Auto", + "natural_wind": "Natural wind", + "sleep_wind": "Sleep wind" + } + } + } + } + }, "sensor": { "flow": { "name": "Flow" From 43ceb1c6c8644fcbb771472d99b78e4967099ec0 Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Wed, 29 May 2024 14:18:46 -0500 Subject: [PATCH 0004/1445] Handle case where timer device id exists but is not registered (delayed command) (#118410) Handle case where device id exists but is not registered --- homeassistant/components/intent/timers.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/intent/timers.py b/homeassistant/components/intent/timers.py index 1dc6b279a61..cddfce55b9f 100644 --- a/homeassistant/components/intent/timers.py +++ b/homeassistant/components/intent/timers.py @@ -229,7 +229,9 @@ class TimerManager: if (not conversation_command) and (device_id is None): raise ValueError("Conversation command must be set if no device id") - if (device_id is not None) and (not self.is_timer_device(device_id)): + if (not conversation_command) and ( + (device_id is None) or (not self.is_timer_device(device_id)) + ): raise TimersNotSupportedError(device_id) total_seconds = 0 @@ -276,7 +278,7 @@ class TimerManager: name=f"Timer {timer_id}", ) - if timer.device_id is not None: + if timer.device_id in self.handlers: self.handlers[timer.device_id](TimerEventType.STARTED, timer) _LOGGER.debug( "Timer started: id=%s, name=%s, hours=%s, minutes=%s, seconds=%s, device_id=%s", From 9e9e1f75f245a89f5160f89a3eea087ef44d8ef1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 29 May 2024 09:55:12 -1000 Subject: [PATCH 0005/1445] Fix google_mail doing blocking I/O in the event loop (#118421) fixes #118411 --- homeassistant/components/google_tasks/api.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/google_tasks/api.py b/homeassistant/components/google_tasks/api.py index ed70f2f6f44..22e5e80229a 100644 --- a/homeassistant/components/google_tasks/api.py +++ b/homeassistant/components/google_tasks/api.py @@ -1,5 +1,6 @@ """API for Google Tasks bound to Home Assistant OAuth.""" +from functools import partial import json import logging from typing import Any @@ -52,7 +53,9 @@ class AsyncConfigEntryAuth: async def _get_service(self) -> Resource: """Get current resource.""" token = await self.async_get_access_token() - return build("tasks", "v1", credentials=Credentials(token=token)) + return await self._hass.async_add_executor_job( + partial(build, "tasks", "v1", credentials=Credentials(token=token)) + ) async def list_task_lists(self) -> list[dict[str, Any]]: """Get all TaskList resources.""" From 5fae2bd7c5ef6d2243ec691ae68ccf893661d323 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 29 May 2024 09:55:22 -1000 Subject: [PATCH 0006/1445] Fix google_tasks doing blocking I/O in the event loop (#118418) fixes #118407 From 1743d1700d3b13e3e67b063656b02b223b8316a4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 29 May 2024 09:55:34 -1000 Subject: [PATCH 0007/1445] Ensure paho.mqtt.client is imported in the executor (#118412) fixes #118405 --- homeassistant/components/mqtt/__init__.py | 2 +- homeassistant/components/mqtt/client.py | 11 ++++++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index f501e7fa89c..ea520e88366 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -244,7 +244,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: websocket_api.async_register_command(hass, websocket_subscribe) websocket_api.async_register_command(hass, websocket_mqtt_info) hass.data[DATA_MQTT] = mqtt_data = MqttData(config=mqtt_yaml, client=client) - client.start(mqtt_data) + await client.async_start(mqtt_data) # Restore saved subscriptions if mqtt_data.subscriptions_to_restore: diff --git a/homeassistant/components/mqtt/client.py b/homeassistant/components/mqtt/client.py index 70e6f573266..0871a0419e5 100644 --- a/homeassistant/components/mqtt/client.py +++ b/homeassistant/components/mqtt/client.py @@ -39,9 +39,11 @@ from homeassistant.core import ( ) from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.importlib import async_import_module from homeassistant.helpers.start import async_at_started from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass +from homeassistant.setup import SetupPhases, async_pause_setup from homeassistant.util.async_ import create_eager_task from homeassistant.util.collection import chunked_or_all from homeassistant.util.logging import catch_log_exception, log_exception @@ -491,13 +493,13 @@ class MQTT: """Handle HA stop.""" await self.async_disconnect() - def start( + async def async_start( self, mqtt_data: MqttData, ) -> None: """Start Home Assistant MQTT client.""" self._mqtt_data = mqtt_data - self.init_client() + await self.async_init_client() @property def subscriptions(self) -> list[Subscription]: @@ -528,8 +530,11 @@ class MQTT: mqttc.on_socket_open = self._async_on_socket_open mqttc.on_socket_register_write = self._async_on_socket_register_write - def init_client(self) -> None: + async def async_init_client(self) -> None: """Initialize paho client.""" + with async_pause_setup(self.hass, SetupPhases.WAIT_IMPORT_PACKAGES): + await async_import_module(self.hass, "paho.mqtt.client") + mqttc = MqttClientSetup(self.conf).client # on_socket_unregister_write and _async_on_socket_close # are only ever called in the event loop From f93a3127f22ba450edcf28877198157027cba110 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 29 May 2024 10:07:56 -1000 Subject: [PATCH 0008/1445] Fix workday doing blocking I/O in the event loop (#118422) --- .../components/workday/binary_sensor.py | 53 ++++++++++--------- 1 file changed, 29 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/workday/binary_sensor.py b/homeassistant/components/workday/binary_sensor.py index 1963359bf0a..205f500746e 100644 --- a/homeassistant/components/workday/binary_sensor.py +++ b/homeassistant/components/workday/binary_sensor.py @@ -68,6 +68,32 @@ def validate_dates(holiday_list: list[str]) -> list[str]: return calc_holidays +def _get_obj_holidays( + country: str | None, province: str | None, year: int, language: str | None +) -> HolidayBase: + """Get the object for the requested country and year.""" + if not country: + return HolidayBase() + + obj_holidays: HolidayBase = country_holidays( + country, + subdiv=province, + years=year, + language=language, + ) + if (supported_languages := obj_holidays.supported_languages) and language == "en": + for lang in supported_languages: + if lang.startswith("en"): + obj_holidays = country_holidays( + country, + subdiv=province, + years=year, + language=lang, + ) + LOGGER.debug("Changing language from %s to %s", language, lang) + return obj_holidays + + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: @@ -83,29 +109,9 @@ async def async_setup_entry( language: str | None = entry.options.get(CONF_LANGUAGE) year: int = (dt_util.now() + timedelta(days=days_offset)).year - - if country: - obj_holidays: HolidayBase = country_holidays( - country, - subdiv=province, - years=year, - language=language, - ) - if ( - supported_languages := obj_holidays.supported_languages - ) and language == "en": - for lang in supported_languages: - if lang.startswith("en"): - obj_holidays = country_holidays( - country, - subdiv=province, - years=year, - language=lang, - ) - LOGGER.debug("Changing language from %s to %s", language, lang) - else: - obj_holidays = HolidayBase() - + obj_holidays: HolidayBase = await hass.async_add_executor_job( + _get_obj_holidays, country, province, year, language + ) calc_add_holidays: list[str] = validate_dates(add_holidays) calc_remove_holidays: list[str] = validate_dates(remove_holidays) @@ -198,7 +204,6 @@ async def async_setup_entry( entry.entry_id, ) ], - True, ) From a670169325fbd5faeb15d38316293bc5c274361b Mon Sep 17 00:00:00 2001 From: swcloudgenie <45437888+swcloudgenie@users.noreply.github.com> Date: Wed, 29 May 2024 15:13:28 -0500 Subject: [PATCH 0009/1445] New official genie garage integration (#117020) * new official genie garage integration * move api constants into api module * move scan interval constant to cover.py --- .coveragerc | 6 + CODEOWNERS | 4 +- .../components/aladdin_connect/__init__.py | 63 ++-- .../components/aladdin_connect/api.py | 31 ++ .../application_credentials.py | 14 + .../components/aladdin_connect/config_flow.py | 147 ++------- .../components/aladdin_connect/const.py | 22 +- .../components/aladdin_connect/cover.py | 102 +++--- .../components/aladdin_connect/diagnostics.py | 28 -- .../components/aladdin_connect/manifest.json | 7 +- .../components/aladdin_connect/model.py | 22 +- .../components/aladdin_connect/sensor.py | 46 +-- .../components/aladdin_connect/strings.json | 42 +-- .../generated/application_credentials.py | 1 + requirements_all.txt | 6 +- requirements_test_all.txt | 6 +- tests/components/aladdin_connect/__init__.py | 2 +- tests/components/aladdin_connect/conftest.py | 48 --- .../snapshots/test_diagnostics.ambr | 20 -- .../aladdin_connect/test_config_flow.py | 312 ++++-------------- .../components/aladdin_connect/test_cover.py | 228 ------------- .../aladdin_connect/test_diagnostics.py | 41 --- tests/components/aladdin_connect/test_init.py | 258 --------------- .../components/aladdin_connect/test_model.py | 19 -- .../components/aladdin_connect/test_sensor.py | 165 --------- 25 files changed, 286 insertions(+), 1354 deletions(-) create mode 100644 homeassistant/components/aladdin_connect/api.py create mode 100644 homeassistant/components/aladdin_connect/application_credentials.py delete mode 100644 homeassistant/components/aladdin_connect/diagnostics.py delete mode 100644 tests/components/aladdin_connect/conftest.py delete mode 100644 tests/components/aladdin_connect/snapshots/test_diagnostics.ambr delete mode 100644 tests/components/aladdin_connect/test_cover.py delete mode 100644 tests/components/aladdin_connect/test_diagnostics.py delete mode 100644 tests/components/aladdin_connect/test_init.py delete mode 100644 tests/components/aladdin_connect/test_model.py delete mode 100644 tests/components/aladdin_connect/test_sensor.py diff --git a/.coveragerc b/.coveragerc index 4e78ea6a3e4..7594d2d2d98 100644 --- a/.coveragerc +++ b/.coveragerc @@ -58,6 +58,12 @@ omit = homeassistant/components/airvisual/sensor.py homeassistant/components/airvisual_pro/__init__.py homeassistant/components/airvisual_pro/sensor.py + homeassistant/components/aladdin_connect/__init__.py + homeassistant/components/aladdin_connect/api.py + homeassistant/components/aladdin_connect/application_credentials.py + homeassistant/components/aladdin_connect/cover.py + homeassistant/components/aladdin_connect/model.py + homeassistant/components/aladdin_connect/sensor.py homeassistant/components/alarmdecoder/__init__.py homeassistant/components/alarmdecoder/alarm_control_panel.py homeassistant/components/alarmdecoder/binary_sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index ddd1e424397..32f885f6015 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -80,8 +80,8 @@ build.json @home-assistant/supervisor /tests/components/airzone/ @Noltari /homeassistant/components/airzone_cloud/ @Noltari /tests/components/airzone_cloud/ @Noltari -/homeassistant/components/aladdin_connect/ @mkmer -/tests/components/aladdin_connect/ @mkmer +/homeassistant/components/aladdin_connect/ @swcloudgenie +/tests/components/aladdin_connect/ @swcloudgenie /homeassistant/components/alarm_control_panel/ @home-assistant/core /tests/components/alarm_control_panel/ @home-assistant/core /homeassistant/components/alert/ @home-assistant/core @frenck diff --git a/homeassistant/components/aladdin_connect/__init__.py b/homeassistant/components/aladdin_connect/__init__.py index 84710c3f74e..55c4345beb3 100644 --- a/homeassistant/components/aladdin_connect/__init__.py +++ b/homeassistant/components/aladdin_connect/__init__.py @@ -1,40 +1,33 @@ -"""The aladdin_connect component.""" +"""The Aladdin Connect Genie integration.""" -import logging -from typing import Final - -from AIOAladdinConnect import AladdinConnectClient -import AIOAladdinConnect.session_manager as Aladdin -from aiohttp import ClientError +from __future__ import annotations from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform +from homeassistant.const import Platform from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady -from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers import aiohttp_client, config_entry_oauth2_flow -from .const import CLIENT_ID, DOMAIN +from . import api +from .const import CONFIG_FLOW_MINOR_VERSION, CONFIG_FLOW_VERSION -_LOGGER: Final = logging.getLogger(__name__) - -PLATFORMS: list[Platform] = [Platform.COVER, Platform.SENSOR] +PLATFORMS: list[Platform] = [Platform.COVER] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: - """Set up platform from a ConfigEntry.""" - username = entry.data[CONF_USERNAME] - password = entry.data[CONF_PASSWORD] - acc = AladdinConnectClient( - username, password, async_get_clientsession(hass), CLIENT_ID + """Set up Aladdin Connect Genie from a config entry.""" + implementation = ( + await config_entry_oauth2_flow.async_get_config_entry_implementation( + hass, entry + ) + ) + + session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation) + + # If using an aiohttp-based API lib + entry.runtime_data = api.AsyncConfigEntryAuth( + aiohttp_client.async_get_clientsession(hass), session ) - try: - await acc.login() - except (ClientError, TimeoutError, Aladdin.ConnectionError) as ex: - raise ConfigEntryNotReady("Can not connect to host") from ex - except Aladdin.InvalidPasswordError as ex: - raise ConfigEntryAuthFailed("Incorrect Password") from ex - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = acc await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True @@ -42,7 +35,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): - hass.data[DOMAIN].pop(entry.entry_id) + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - return unload_ok + +async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: + """Migrate old config.""" + if config_entry.version < CONFIG_FLOW_VERSION: + config_entry.async_start_reauth(hass) + new_data = {**config_entry.data} + hass.config_entries.async_update_entry( + config_entry, + data=new_data, + version=CONFIG_FLOW_VERSION, + minor_version=CONFIG_FLOW_MINOR_VERSION, + ) + + return True diff --git a/homeassistant/components/aladdin_connect/api.py b/homeassistant/components/aladdin_connect/api.py new file mode 100644 index 00000000000..8100cd1e4d8 --- /dev/null +++ b/homeassistant/components/aladdin_connect/api.py @@ -0,0 +1,31 @@ +"""API for Aladdin Connect Genie bound to Home Assistant OAuth.""" + +from aiohttp import ClientSession +from genie_partner_sdk.auth import Auth + +from homeassistant.helpers import config_entry_oauth2_flow + +API_URL = "https://twdvzuefzh.execute-api.us-east-2.amazonaws.com/v1" +API_KEY = "k6QaiQmcTm2zfaNns5L1Z8duBtJmhDOW8JawlCC3" + + +class AsyncConfigEntryAuth(Auth): # type: ignore[misc] + """Provide Aladdin Connect Genie authentication tied to an OAuth2 based config entry.""" + + def __init__( + self, + websession: ClientSession, + oauth_session: config_entry_oauth2_flow.OAuth2Session, + ) -> None: + """Initialize Aladdin Connect Genie auth.""" + super().__init__( + websession, API_URL, oauth_session.token["access_token"], API_KEY + ) + self._oauth_session = oauth_session + + async def async_get_access_token(self) -> str: + """Return a valid access token.""" + if not self._oauth_session.valid_token: + await self._oauth_session.async_ensure_token_valid() + + return str(self._oauth_session.token["access_token"]) diff --git a/homeassistant/components/aladdin_connect/application_credentials.py b/homeassistant/components/aladdin_connect/application_credentials.py new file mode 100644 index 00000000000..e8e959f1fa3 --- /dev/null +++ b/homeassistant/components/aladdin_connect/application_credentials.py @@ -0,0 +1,14 @@ +"""application_credentials platform the Aladdin Connect Genie integration.""" + +from homeassistant.components.application_credentials import AuthorizationServer +from homeassistant.core import HomeAssistant + +from .const import OAUTH2_AUTHORIZE, OAUTH2_TOKEN + + +async def async_get_authorization_server(hass: HomeAssistant) -> AuthorizationServer: + """Return authorization server.""" + return AuthorizationServer( + authorize_url=OAUTH2_AUTHORIZE, + token_url=OAUTH2_TOKEN, + ) diff --git a/homeassistant/components/aladdin_connect/config_flow.py b/homeassistant/components/aladdin_connect/config_flow.py index e960138853a..aa42574a005 100644 --- a/homeassistant/components/aladdin_connect/config_flow.py +++ b/homeassistant/components/aladdin_connect/config_flow.py @@ -1,137 +1,58 @@ -"""Config flow for Aladdin Connect cover integration.""" - -from __future__ import annotations +"""Config flow for Aladdin Connect Genie.""" from collections.abc import Mapping +import logging from typing import Any -from AIOAladdinConnect import AladdinConnectClient -import AIOAladdinConnect.session_manager as Aladdin -from aiohttp.client_exceptions import ClientError import voluptuous as vol -from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from homeassistant.core import HomeAssistant -from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.config_entries import ConfigEntry, ConfigFlowResult +from homeassistant.helpers import config_entry_oauth2_flow -from .const import CLIENT_ID, DOMAIN - -STEP_USER_DATA_SCHEMA = vol.Schema( - { - vol.Required(CONF_USERNAME): str, - vol.Required(CONF_PASSWORD): str, - } -) - -REAUTH_SCHEMA = vol.Schema({vol.Required(CONF_PASSWORD): str}) +from .const import CONFIG_FLOW_MINOR_VERSION, CONFIG_FLOW_VERSION, DOMAIN -async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> None: - """Validate the user input allows us to connect. +class OAuth2FlowHandler( + config_entry_oauth2_flow.AbstractOAuth2FlowHandler, domain=DOMAIN +): + """Config flow to handle Aladdin Connect Genie OAuth2 authentication.""" - Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user. - """ - acc = AladdinConnectClient( - data[CONF_USERNAME], - data[CONF_PASSWORD], - async_get_clientsession(hass), - CLIENT_ID, - ) - try: - await acc.login() - except (ClientError, TimeoutError, Aladdin.ConnectionError): - raise + DOMAIN = DOMAIN + VERSION = CONFIG_FLOW_VERSION + MINOR_VERSION = CONFIG_FLOW_MINOR_VERSION - except Aladdin.InvalidPasswordError as ex: - raise InvalidAuth from ex - - -class AladdinConnectConfigFlow(ConfigFlow, domain=DOMAIN): - """Handle a config flow for Aladdin Connect.""" - - VERSION = 1 - entry: ConfigEntry | None + reauth_entry: ConfigEntry | None = None async def async_step_reauth( - self, entry_data: Mapping[str, Any] + self, user_input: Mapping[str, Any] ) -> ConfigFlowResult: - """Handle re-authentication with Aladdin Connect.""" - - self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) + """Perform reauth upon API auth error or upgrade from v1 to v2.""" + self.reauth_entry = self.hass.config_entries.async_get_entry( + self.context["entry_id"] + ) return await self.async_step_reauth_confirm() async def async_step_reauth_confirm( - self, user_input: dict[str, Any] | None = None + self, user_input: Mapping[str, Any] | None = None ) -> ConfigFlowResult: - """Confirm re-authentication with Aladdin Connect.""" - errors: dict[str, str] = {} - - if user_input: - assert self.entry is not None - password = user_input[CONF_PASSWORD] - data = { - CONF_USERNAME: self.entry.data[CONF_USERNAME], - CONF_PASSWORD: password, - } - - try: - await validate_input(self.hass, data) - - except InvalidAuth: - errors["base"] = "invalid_auth" - - except (ClientError, TimeoutError, Aladdin.ConnectionError): - errors["base"] = "cannot_connect" - - else: - self.hass.config_entries.async_update_entry( - self.entry, - data={ - **self.entry.data, - CONF_PASSWORD: password, - }, - ) - await self.hass.config_entries.async_reload(self.entry.entry_id) - return self.async_abort(reason="reauth_successful") - - return self.async_show_form( - step_id="reauth_confirm", - data_schema=REAUTH_SCHEMA, - errors=errors, - ) - - async def async_step_user( - self, user_input: dict[str, Any] | None = None - ) -> ConfigFlowResult: - """Handle the initial step.""" + """Dialog that informs the user that reauth is required.""" if user_input is None: return self.async_show_form( - step_id="user", data_schema=STEP_USER_DATA_SCHEMA + step_id="reauth_confirm", + data_schema=vol.Schema({}), ) + return await self.async_step_user() - errors = {} - - try: - await validate_input(self.hass, user_input) - except InvalidAuth: - errors["base"] = "invalid_auth" - - except (ClientError, TimeoutError, Aladdin.ConnectionError): - errors["base"] = "cannot_connect" - - else: - await self.async_set_unique_id( - user_input["username"].lower(), raise_on_progress=False + async def async_oauth_create_entry(self, data: dict) -> ConfigFlowResult: + """Create an oauth config entry or update existing entry for reauth.""" + if self.reauth_entry: + return self.async_update_reload_and_abort( + self.reauth_entry, + data=data, ) - self._abort_if_unique_id_configured() - return self.async_create_entry(title="Aladdin Connect", data=user_input) + return await super().async_oauth_create_entry(data) - return self.async_show_form( - step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors - ) - - -class InvalidAuth(HomeAssistantError): - """Error to indicate there is invalid auth.""" + @property + def logger(self) -> logging.Logger: + """Return logger.""" + return logging.getLogger(__name__) diff --git a/homeassistant/components/aladdin_connect/const.py b/homeassistant/components/aladdin_connect/const.py index bf77c032d1b..5312826469e 100644 --- a/homeassistant/components/aladdin_connect/const.py +++ b/homeassistant/components/aladdin_connect/const.py @@ -1,22 +1,14 @@ -"""Platform for the Aladdin Connect cover component.""" - -from __future__ import annotations +"""Constants for the Aladdin Connect Genie integration.""" from typing import Final from homeassistant.components.cover import CoverEntityFeature -from homeassistant.const import STATE_CLOSED, STATE_CLOSING, STATE_OPEN, STATE_OPENING - -NOTIFICATION_ID: Final = "aladdin_notification" -NOTIFICATION_TITLE: Final = "Aladdin Connect Cover Setup" - -STATES_MAP: Final[dict[str, str]] = { - "open": STATE_OPEN, - "opening": STATE_OPENING, - "closed": STATE_CLOSED, - "closing": STATE_CLOSING, -} DOMAIN = "aladdin_connect" +CONFIG_FLOW_VERSION = 2 +CONFIG_FLOW_MINOR_VERSION = 1 + +OAUTH2_AUTHORIZE = "https://app.aladdinconnect.com/login.html" +OAUTH2_TOKEN = "https://twdvzuefzh.execute-api.us-east-2.amazonaws.com/v1/oauth2/token" + SUPPORTED_FEATURES: Final = CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE -CLIENT_ID = "1000" diff --git a/homeassistant/components/aladdin_connect/cover.py b/homeassistant/components/aladdin_connect/cover.py index 61c8df92eaf..cf31b06cbcd 100644 --- a/homeassistant/components/aladdin_connect/cover.py +++ b/homeassistant/components/aladdin_connect/cover.py @@ -1,25 +1,23 @@ -"""Platform for the Aladdin Connect cover component.""" - -from __future__ import annotations +"""Cover Entity for Genie Garage Door.""" from datetime import timedelta from typing import Any -from AIOAladdinConnect import AladdinConnectClient, session_manager +from genie_partner_sdk.client import AladdinConnectClient from homeassistant.components.cover import CoverDeviceClass, CoverEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import STATE_CLOSED, STATE_CLOSING, STATE_OPENING from homeassistant.core import HomeAssistant -from homeassistant.exceptions import HomeAssistantError, PlatformNotReady +from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.device_registry as dr from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN, STATES_MAP, SUPPORTED_FEATURES -from .model import DoorDevice +from . import api +from .const import DOMAIN, SUPPORTED_FEATURES +from .model import GarageDoor -SCAN_INTERVAL = timedelta(seconds=300) +SCAN_INTERVAL = timedelta(seconds=15) async def async_setup_entry( @@ -28,25 +26,33 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Aladdin Connect platform.""" - acc: AladdinConnectClient = hass.data[DOMAIN][config_entry.entry_id] + session: api.AsyncConfigEntryAuth = config_entry.runtime_data + acc = AladdinConnectClient(session) doors = await acc.get_doors() if doors is None: raise PlatformNotReady("Error from Aladdin Connect getting doors") + device_registry = dr.async_get(hass) + doors_to_add = [] + for door in doors: + existing = device_registry.async_get(door.unique_id) + if existing is None: + doors_to_add.append(door) + async_add_entities( - (AladdinDevice(acc, door, config_entry) for door in doors), + (AladdinDevice(acc, door, config_entry) for door in doors_to_add), ) remove_stale_devices(hass, config_entry, doors) def remove_stale_devices( - hass: HomeAssistant, config_entry: ConfigEntry, devices: list[dict] + hass: HomeAssistant, config_entry: ConfigEntry, devices: list[GarageDoor] ) -> None: """Remove stale devices from device registry.""" device_registry = dr.async_get(hass) device_entries = dr.async_entries_for_config_entry( device_registry, config_entry.entry_id ) - all_device_ids = {f"{door['device_id']}-{door['door_number']}" for door in devices} + all_device_ids = {door.unique_id for door in devices} for device_entry in device_entries: device_id: str | None = None @@ -74,74 +80,52 @@ class AladdinDevice(CoverEntity): _attr_name = None def __init__( - self, acc: AladdinConnectClient, device: DoorDevice, entry: ConfigEntry + self, acc: AladdinConnectClient, device: GarageDoor, entry: ConfigEntry ) -> None: """Initialize the Aladdin Connect cover.""" self._acc = acc - self._device_id = device["device_id"] - self._number = device["door_number"] - self._serial = device["serial"] + self._device_id = device.device_id + self._number = device.door_number self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, f"{self._device_id}-{self._number}")}, - name=device["name"], + identifiers={(DOMAIN, device.unique_id)}, + name=device.name, manufacturer="Overhead Door", - model=device["model"], ) - self._attr_unique_id = f"{self._device_id}-{self._number}" - - async def async_added_to_hass(self) -> None: - """Connect Aladdin Connect to the cloud.""" - - self._acc.register_callback( - self.async_write_ha_state, self._serial, self._number - ) - await self._acc.get_doors(self._serial) - - async def async_will_remove_from_hass(self) -> None: - """Close Aladdin Connect before removing.""" - self._acc.unregister_callback(self._serial, self._number) - await self._acc.close() - - async def async_close_cover(self, **kwargs: Any) -> None: - """Issue close command to cover.""" - if not await self._acc.close_door(self._device_id, self._number): - raise HomeAssistantError("Aladdin Connect API failed to close the cover") + self._attr_unique_id = device.unique_id async def async_open_cover(self, **kwargs: Any) -> None: """Issue open command to cover.""" - if not await self._acc.open_door(self._device_id, self._number): - raise HomeAssistantError("Aladdin Connect API failed to open the cover") + await self._acc.open_door(self._device_id, self._number) + + async def async_close_cover(self, **kwargs: Any) -> None: + """Issue close command to cover.""" + await self._acc.close_door(self._device_id, self._number) async def async_update(self) -> None: """Update status of cover.""" - try: - await self._acc.get_doors(self._serial) - self._attr_available = True - - except (session_manager.ConnectionError, session_manager.InvalidPasswordError): - self._attr_available = False + await self._acc.update_door(self._device_id, self._number) @property def is_closed(self) -> bool | None: """Update is closed attribute.""" - value = STATES_MAP.get(self._acc.get_door_status(self._device_id, self._number)) + value = self._acc.get_door_status(self._device_id, self._number) if value is None: return None - return value == STATE_CLOSED + return bool(value == "closed") @property - def is_closing(self) -> bool: + def is_closing(self) -> bool | None: """Update is closing attribute.""" - return ( - STATES_MAP.get(self._acc.get_door_status(self._device_id, self._number)) - == STATE_CLOSING - ) + value = self._acc.get_door_status(self._device_id, self._number) + if value is None: + return None + return bool(value == "closing") @property - def is_opening(self) -> bool: + def is_opening(self) -> bool | None: """Update is opening attribute.""" - return ( - STATES_MAP.get(self._acc.get_door_status(self._device_id, self._number)) - == STATE_OPENING - ) + value = self._acc.get_door_status(self._device_id, self._number) + if value is None: + return None + return bool(value == "opening") diff --git a/homeassistant/components/aladdin_connect/diagnostics.py b/homeassistant/components/aladdin_connect/diagnostics.py deleted file mode 100644 index 67a31079f14..00000000000 --- a/homeassistant/components/aladdin_connect/diagnostics.py +++ /dev/null @@ -1,28 +0,0 @@ -"""Diagnostics support for Aladdin Connect.""" - -from __future__ import annotations - -from typing import Any - -from AIOAladdinConnect import AladdinConnectClient - -from homeassistant.components.diagnostics import async_redact_data -from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant - -from .const import DOMAIN - -TO_REDACT = {"serial", "device_id"} - - -async def async_get_config_entry_diagnostics( - hass: HomeAssistant, - config_entry: ConfigEntry, -) -> dict[str, Any]: - """Return diagnostics for a config entry.""" - - acc: AladdinConnectClient = hass.data[DOMAIN][config_entry.entry_id] - - return { - "doors": async_redact_data(acc.doors, TO_REDACT), - } diff --git a/homeassistant/components/aladdin_connect/manifest.json b/homeassistant/components/aladdin_connect/manifest.json index 344c77dcb73..69b38399cce 100644 --- a/homeassistant/components/aladdin_connect/manifest.json +++ b/homeassistant/components/aladdin_connect/manifest.json @@ -1,11 +1,10 @@ { "domain": "aladdin_connect", "name": "Aladdin Connect", - "codeowners": ["@mkmer"], + "codeowners": ["@swcloudgenie"], "config_flow": true, + "dependencies": ["application_credentials"], "documentation": "https://www.home-assistant.io/integrations/aladdin_connect", "iot_class": "cloud_polling", - "loggers": ["aladdin_connect"], - "quality_scale": "platinum", - "requirements": ["AIOAladdinConnect==0.1.58"] + "requirements": ["genie-partner-sdk==1.0.2"] } diff --git a/homeassistant/components/aladdin_connect/model.py b/homeassistant/components/aladdin_connect/model.py index 73e445f2f3b..db08cb7b8b8 100644 --- a/homeassistant/components/aladdin_connect/model.py +++ b/homeassistant/components/aladdin_connect/model.py @@ -5,12 +5,26 @@ from __future__ import annotations from typing import TypedDict -class DoorDevice(TypedDict): - """Aladdin door device.""" +class GarageDoorData(TypedDict): + """Aladdin door data.""" device_id: str door_number: int name: str status: str - serial: str - model: str + link_status: str + battery_level: int + + +class GarageDoor: + """Aladdin Garage Door Entity.""" + + def __init__(self, data: GarageDoorData) -> None: + """Create `GarageDoor` from dictionary of data.""" + self.device_id = data["device_id"] + self.door_number = data["door_number"] + self.unique_id = f"{self.device_id}-{self.door_number}" + self.name = data["name"] + self.status = data["status"] + self.link_status = data["link_status"] + self.battery_level = data["battery_level"] diff --git a/homeassistant/components/aladdin_connect/sensor.py b/homeassistant/components/aladdin_connect/sensor.py index 22aa9c6faf0..231928656a8 100644 --- a/homeassistant/components/aladdin_connect/sensor.py +++ b/homeassistant/components/aladdin_connect/sensor.py @@ -6,7 +6,7 @@ from collections.abc import Callable from dataclasses import dataclass from typing import cast -from AIOAladdinConnect import AladdinConnectClient +from genie_partner_sdk.client import AladdinConnectClient from homeassistant.components.sensor import ( SensorDeviceClass, @@ -15,13 +15,14 @@ from homeassistant.components.sensor import ( SensorStateClass, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import PERCENTAGE, SIGNAL_STRENGTH_DECIBELS +from homeassistant.const import PERCENTAGE from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback +from . import api from .const import DOMAIN -from .model import DoorDevice +from .model import GarageDoor @dataclass(frozen=True, kw_only=True) @@ -40,24 +41,6 @@ SENSORS: tuple[AccSensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, value_fn=AladdinConnectClient.get_battery_status, ), - AccSensorEntityDescription( - key="rssi", - translation_key="wifi_strength", - device_class=SensorDeviceClass.SIGNAL_STRENGTH, - entity_registry_enabled_default=False, - native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS, - state_class=SensorStateClass.MEASUREMENT, - value_fn=AladdinConnectClient.get_rssi_status, - ), - AccSensorEntityDescription( - key="ble_strength", - translation_key="ble_strength", - device_class=SensorDeviceClass.SIGNAL_STRENGTH, - entity_registry_enabled_default=False, - native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS, - state_class=SensorStateClass.MEASUREMENT, - value_fn=AladdinConnectClient.get_ble_strength, - ), ) @@ -66,7 +49,8 @@ async def async_setup_entry( ) -> None: """Set up Aladdin Connect sensor devices.""" - acc: AladdinConnectClient = hass.data[DOMAIN][entry.entry_id] + session: api.AsyncConfigEntryAuth = hass.data[DOMAIN][entry.entry_id] + acc = AladdinConnectClient(session) entities = [] doors = await acc.get_doors() @@ -88,26 +72,20 @@ class AladdinConnectSensor(SensorEntity): def __init__( self, acc: AladdinConnectClient, - device: DoorDevice, + device: GarageDoor, description: AccSensorEntityDescription, ) -> None: """Initialize a sensor for an Aladdin Connect device.""" - self._device_id = device["device_id"] - self._number = device["door_number"] + self._device_id = device.device_id + self._number = device.door_number self._acc = acc self.entity_description = description - self._attr_unique_id = f"{self._device_id}-{self._number}-{description.key}" + self._attr_unique_id = f"{device.unique_id}-{description.key}" self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, f"{self._device_id}-{self._number}")}, - name=device["name"], + identifiers={(DOMAIN, device.unique_id)}, + name=device.name, manufacturer="Overhead Door", - model=device["model"], ) - if device["model"] == "01" and description.key in ( - "battery_level", - "ble_strength", - ): - self._attr_entity_registry_enabled_default = True @property def native_value(self) -> float | None: diff --git a/homeassistant/components/aladdin_connect/strings.json b/homeassistant/components/aladdin_connect/strings.json index bfe932b039c..48f9b299a1d 100644 --- a/homeassistant/components/aladdin_connect/strings.json +++ b/homeassistant/components/aladdin_connect/strings.json @@ -1,39 +1,29 @@ { "config": { "step": { - "user": { - "data": { - "username": "[%key:common::config_flow::data::username%]", - "password": "[%key:common::config_flow::data::password%]" - } + "pick_implementation": { + "title": "[%key:common::config_flow::title::oauth2_pick_implementation%]" }, "reauth_confirm": { "title": "[%key:common::config_flow::title::reauth%]", - "description": "The Aladdin Connect integration needs to re-authenticate your account", - "data": { - "password": "[%key:common::config_flow::data::password%]" - } + "description": "Aladdin Connect needs to re-authenticate your account" } }, - - "error": { - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", - "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" - }, - "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", + "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", + "oauth_error": "[%key:common::config_flow::abort::oauth2_error%]", + "oauth_failed": "[%key:common::config_flow::abort::oauth2_failed%]", + "oauth_timeout": "[%key:common::config_flow::abort::oauth2_timeout%]", + "oauth_unauthorized": "[%key:common::config_flow::abort::oauth2_unauthorized%]", + "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", + "authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]", + "no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]", + "user_rejected_authorize": "[%key:common::config_flow::abort::oauth2_user_rejected_authorize%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" - } - }, - "entity": { - "sensor": { - "wifi_strength": { - "name": "Wi-Fi RSSI" - }, - "ble_strength": { - "name": "BLE Strength" - } + }, + "create_entry": { + "default": "[%key:common::config_flow::create_entry::authenticated%]" } } } diff --git a/homeassistant/generated/application_credentials.py b/homeassistant/generated/application_credentials.py index c576f242e30..bc6b29e4c23 100644 --- a/homeassistant/generated/application_credentials.py +++ b/homeassistant/generated/application_credentials.py @@ -4,6 +4,7 @@ To update, run python3 -m script.hassfest """ APPLICATION_CREDENTIALS = [ + "aladdin_connect", "electric_kiwi", "fitbit", "geocaching", diff --git a/requirements_all.txt b/requirements_all.txt index 3d297241539..c7ee7ae5623 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -6,9 +6,6 @@ # homeassistant.components.aemet AEMET-OpenData==0.5.1 -# homeassistant.components.aladdin_connect -AIOAladdinConnect==0.1.58 - # homeassistant.components.honeywell AIOSomecomfort==0.0.25 @@ -923,6 +920,9 @@ gassist-text==0.0.11 # homeassistant.components.google gcal-sync==6.0.4 +# homeassistant.components.aladdin_connect +genie-partner-sdk==1.0.2 + # homeassistant.components.geniushub geniushub-client==0.7.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index faeb0bdfcdb..ccc1ae213ca 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -6,9 +6,6 @@ # homeassistant.components.aemet AEMET-OpenData==0.5.1 -# homeassistant.components.aladdin_connect -AIOAladdinConnect==0.1.58 - # homeassistant.components.honeywell AIOSomecomfort==0.0.25 @@ -758,6 +755,9 @@ gassist-text==0.0.11 # homeassistant.components.google gcal-sync==6.0.4 +# homeassistant.components.aladdin_connect +genie-partner-sdk==1.0.2 + # homeassistant.components.geocaching geocachingapi==0.2.1 diff --git a/tests/components/aladdin_connect/__init__.py b/tests/components/aladdin_connect/__init__.py index 6e108ed88df..aa5957dc392 100644 --- a/tests/components/aladdin_connect/__init__.py +++ b/tests/components/aladdin_connect/__init__.py @@ -1 +1 @@ -"""The tests for Aladdin Connect platforms.""" +"""Tests for the Aladdin Connect Garage Door integration.""" diff --git a/tests/components/aladdin_connect/conftest.py b/tests/components/aladdin_connect/conftest.py deleted file mode 100644 index 979c30bdcea..00000000000 --- a/tests/components/aladdin_connect/conftest.py +++ /dev/null @@ -1,48 +0,0 @@ -"""Fixtures for the Aladdin Connect integration tests.""" - -from unittest import mock -from unittest.mock import AsyncMock - -import pytest - -DEVICE_CONFIG_OPEN = { - "device_id": 533255, - "door_number": 1, - "name": "home", - "status": "open", - "link_status": "Connected", - "serial": "12345", - "model": "02", - "rssi": -67, - "ble_strength": 0, - "vendor": "GENIE", - "battery_level": 0, -} - - -@pytest.fixture(name="mock_aladdinconnect_api") -def fixture_mock_aladdinconnect_api(): - """Set up aladdin connect API fixture.""" - with mock.patch( - "homeassistant.components.aladdin_connect.AladdinConnectClient" - ) as mock_opener: - mock_opener.login = AsyncMock(return_value=True) - mock_opener.close = AsyncMock(return_value=True) - - mock_opener.async_get_door_status = AsyncMock(return_value="open") - mock_opener.get_door_status.return_value = "open" - mock_opener.async_get_door_link_status = AsyncMock(return_value="connected") - mock_opener.get_door_link_status.return_value = "connected" - mock_opener.async_get_battery_status = AsyncMock(return_value="99") - mock_opener.get_battery_status.return_value = "99" - mock_opener.async_get_rssi_status = AsyncMock(return_value="-55") - mock_opener.get_rssi_status.return_value = "-55" - mock_opener.async_get_ble_strength = AsyncMock(return_value="-45") - mock_opener.get_ble_strength.return_value = "-45" - mock_opener.get_doors = AsyncMock(return_value=[DEVICE_CONFIG_OPEN]) - mock_opener.doors = [DEVICE_CONFIG_OPEN] - mock_opener.register_callback = mock.Mock(return_value=True) - mock_opener.open_door = AsyncMock(return_value=True) - mock_opener.close_door = AsyncMock(return_value=True) - - return mock_opener diff --git a/tests/components/aladdin_connect/snapshots/test_diagnostics.ambr b/tests/components/aladdin_connect/snapshots/test_diagnostics.ambr deleted file mode 100644 index 8f96567a49f..00000000000 --- a/tests/components/aladdin_connect/snapshots/test_diagnostics.ambr +++ /dev/null @@ -1,20 +0,0 @@ -# serializer version: 1 -# name: test_entry_diagnostics - dict({ - 'doors': list([ - dict({ - 'battery_level': 0, - 'ble_strength': 0, - 'device_id': '**REDACTED**', - 'door_number': 1, - 'link_status': 'Connected', - 'model': '02', - 'name': 'home', - 'rssi': -67, - 'serial': '**REDACTED**', - 'status': 'open', - 'vendor': 'GENIE', - }), - ]), - }) -# --- diff --git a/tests/components/aladdin_connect/test_config_flow.py b/tests/components/aladdin_connect/test_config_flow.py index 65b8b24a59d..d460d62625b 100644 --- a/tests/components/aladdin_connect/test_config_flow.py +++ b/tests/components/aladdin_connect/test_config_flow.py @@ -1,278 +1,82 @@ -"""Test the Aladdin Connect config flow.""" +"""Test the Aladdin Connect Garage Door config flow.""" -from unittest.mock import MagicMock, patch +from unittest.mock import patch -from AIOAladdinConnect.session_manager import InvalidPasswordError -from aiohttp.client_exceptions import ClientConnectionError +import pytest from homeassistant import config_entries -from homeassistant.components.aladdin_connect.const import DOMAIN -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.components.aladdin_connect.const import ( + DOMAIN, + OAUTH2_AUTHORIZE, + OAUTH2_TOKEN, +) +from homeassistant.components.application_credentials import ( + ClientCredential, + async_import_client_credential, +) from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers import config_entry_oauth2_flow +from homeassistant.setup import async_setup_component -from tests.common import MockConfigEntry +CLIENT_ID = "1234" +CLIENT_SECRET = "5678" -async def test_form(hass: HomeAssistant, mock_aladdinconnect_api: MagicMock) -> None: - """Test we get the form.""" - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - assert result["type"] is FlowResultType.FORM - assert result["errors"] is None - - with ( - patch( - "homeassistant.components.aladdin_connect.config_flow.AladdinConnectClient", - return_value=mock_aladdinconnect_api, - ), - patch( - "homeassistant.components.aladdin_connect.async_setup_entry", - return_value=True, - ) as mock_setup_entry, - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - CONF_USERNAME: "test-username", - CONF_PASSWORD: "test-password", - }, - ) - await hass.async_block_till_done() - - assert result2["type"] is FlowResultType.CREATE_ENTRY - assert result2["title"] == "Aladdin Connect" - assert result2["data"] == { - CONF_USERNAME: "test-username", - CONF_PASSWORD: "test-password", - } - - assert len(mock_setup_entry.mock_calls) == 1 - - -async def test_form_failed_auth( - hass: HomeAssistant, mock_aladdinconnect_api: MagicMock -) -> None: - """Test we handle failed authentication error.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - mock_aladdinconnect_api.login.return_value = False - mock_aladdinconnect_api.login.side_effect = InvalidPasswordError - with patch( - "homeassistant.components.aladdin_connect.config_flow.AladdinConnectClient", - return_value=mock_aladdinconnect_api, - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - CONF_USERNAME: "test-username", - CONF_PASSWORD: "test-password", - }, - ) - - assert result2["type"] is FlowResultType.FORM - assert result2["errors"] == {"base": "invalid_auth"} - - -async def test_form_connection_timeout( - hass: HomeAssistant, mock_aladdinconnect_api: MagicMock -) -> None: - """Test we handle http timeout error.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - mock_aladdinconnect_api.login.side_effect = ClientConnectionError - with patch( - "homeassistant.components.aladdin_connect.config_flow.AladdinConnectClient", - return_value=mock_aladdinconnect_api, - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - CONF_USERNAME: "test-username", - CONF_PASSWORD: "test-password", - }, - ) - - assert result2["type"] is FlowResultType.FORM - assert result2["errors"] == {"base": "cannot_connect"} - - -async def test_form_already_configured( - hass: HomeAssistant, mock_aladdinconnect_api: MagicMock -) -> None: - """Test we handle already configured error.""" - mock_entry = MockConfigEntry( - domain=DOMAIN, - data={CONF_USERNAME: "test-username", CONF_PASSWORD: "test-password"}, - unique_id="test-username", - ) - mock_entry.add_to_hass(hass) - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == config_entries.SOURCE_USER - - with patch( - "homeassistant.components.aladdin_connect.config_flow.AladdinConnectClient", - return_value=mock_aladdinconnect_api, - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - CONF_USERNAME: "test-username", - CONF_PASSWORD: "test-password", - }, - ) - await hass.async_block_till_done() - - assert result2["type"] is FlowResultType.ABORT - assert result2["reason"] == "already_configured" - - -async def test_reauth_flow( - hass: HomeAssistant, mock_aladdinconnect_api: MagicMock -) -> None: - """Test a successful reauth flow.""" - - mock_entry = MockConfigEntry( - domain=DOMAIN, - data={"username": "test-username", "password": "test-password"}, - unique_id="test-username", - ) - mock_entry.add_to_hass(hass) - - result = await hass.config_entries.flow.async_init( +@pytest.fixture +async def setup_credentials(hass: HomeAssistant) -> None: + """Fixture to setup credentials.""" + assert await async_setup_component(hass, "application_credentials", {}) + await async_import_client_credential( + hass, DOMAIN, - context={ - "source": config_entries.SOURCE_REAUTH, - "unique_id": mock_entry.unique_id, - "entry_id": mock_entry.entry_id, - }, - data={"username": "test-username", "password": "new-password"}, + ClientCredential(CLIENT_ID, CLIENT_SECRET), ) - assert result["step_id"] == "reauth_confirm" - assert result["type"] is FlowResultType.FORM - assert result["errors"] == {} - with ( - patch( - "homeassistant.components.aladdin_connect.async_setup_entry", - return_value=True, - ), - patch( - "homeassistant.components.aladdin_connect.config_flow.AladdinConnectClient", - return_value=mock_aladdinconnect_api, - ), - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - {CONF_PASSWORD: "new-password"}, - ) - await hass.async_block_till_done() - - assert result2["type"] is FlowResultType.ABORT - assert result2["reason"] == "reauth_successful" - assert mock_entry.data == { - CONF_USERNAME: "test-username", - CONF_PASSWORD: "new-password", - } - - -async def test_reauth_flow_auth_error( - hass: HomeAssistant, mock_aladdinconnect_api: MagicMock +async def test_full_flow( + hass: HomeAssistant, + hass_client_no_auth, + aioclient_mock, + current_request_with_host, + setup_credentials, ) -> None: - """Test an authorization error reauth flow.""" - - mock_entry = MockConfigEntry( - domain=DOMAIN, - data={"username": "test-username", "password": "test-password"}, - unique_id="test-username", - ) - mock_entry.add_to_hass(hass) - + """Check full flow.""" result = await hass.config_entries.flow.async_init( - DOMAIN, - context={ - "source": config_entries.SOURCE_REAUTH, - "unique_id": mock_entry.unique_id, - "entry_id": mock_entry.entry_id, + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + state = config_entry_oauth2_flow._encode_jwt( # noqa: SLF001 + hass, + { + "flow_id": result["flow_id"], + "redirect_uri": "https://example.com/auth/external/callback", }, - data={"username": "test-username", "password": "new-password"}, ) - assert result["step_id"] == "reauth_confirm" - assert result["type"] is FlowResultType.FORM - assert result["errors"] == {} - mock_aladdinconnect_api.login.return_value = False - mock_aladdinconnect_api.login.side_effect = InvalidPasswordError - with ( - patch( - "homeassistant.components.aladdin_connect.config_flow.AladdinConnectClient", - return_value=mock_aladdinconnect_api, - ), - patch( - "homeassistant.components.aladdin_connect.cover.async_setup_entry", - return_value=True, - ), - patch( - "homeassistant.components.aladdin_connect.cover.async_setup_entry", - return_value=True, - ), - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - {CONF_PASSWORD: "new-password"}, - ) - await hass.async_block_till_done() - - assert result2["type"] is FlowResultType.FORM - assert result2["errors"] == {"base": "invalid_auth"} - - -async def test_reauth_flow_connnection_error( - hass: HomeAssistant, mock_aladdinconnect_api: MagicMock -) -> None: - """Test a connection error reauth flow.""" - - mock_entry = MockConfigEntry( - domain=DOMAIN, - data={"username": "test-username", "password": "test-password"}, - unique_id="test-username", + assert result["url"] == ( + f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}" + "&redirect_uri=https://example.com/auth/external/callback" + f"&state={state}" ) - mock_entry.add_to_hass(hass) - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={ - "source": config_entries.SOURCE_REAUTH, - "unique_id": mock_entry.unique_id, - "entry_id": mock_entry.entry_id, + client = await hass_client_no_auth() + resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") + assert resp.status == 200 + assert resp.headers["content-type"] == "text/html; charset=utf-8" + + aioclient_mock.post( + OAUTH2_TOKEN, + json={ + "refresh_token": "mock-refresh-token", + "access_token": "mock-access-token", + "type": "Bearer", + "expires_in": 60, }, - data={"username": "test-username", "password": "new-password"}, ) - assert result["step_id"] == "reauth_confirm" - assert result["type"] is FlowResultType.FORM - assert result["errors"] == {} - mock_aladdinconnect_api.login.side_effect = ClientConnectionError - with patch( - "homeassistant.components.aladdin_connect.config_flow.AladdinConnectClient", - return_value=mock_aladdinconnect_api, - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - {CONF_PASSWORD: "new-password"}, - ) - await hass.async_block_till_done() + "homeassistant.components.aladdin_connect.async_setup_entry", return_value=True + ) as mock_setup: + await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result2["type"] is FlowResultType.FORM - assert result2["errors"] == {"base": "cannot_connect"} + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + assert len(mock_setup.mock_calls) == 1 diff --git a/tests/components/aladdin_connect/test_cover.py b/tests/components/aladdin_connect/test_cover.py deleted file mode 100644 index 082ade75ab9..00000000000 --- a/tests/components/aladdin_connect/test_cover.py +++ /dev/null @@ -1,228 +0,0 @@ -"""Test the Aladdin Connect Cover.""" - -from unittest.mock import AsyncMock, MagicMock, patch - -from AIOAladdinConnect import session_manager -import pytest - -from homeassistant.components.aladdin_connect.const import DOMAIN -from homeassistant.components.aladdin_connect.cover import SCAN_INTERVAL -from homeassistant.components.cover import DOMAIN as COVER_DOMAIN -from homeassistant.config_entries import ConfigEntryState -from homeassistant.const import ( - ATTR_ENTITY_ID, - SERVICE_CLOSE_COVER, - SERVICE_OPEN_COVER, - STATE_CLOSED, - STATE_CLOSING, - STATE_OPEN, - STATE_OPENING, - STATE_UNAVAILABLE, - STATE_UNKNOWN, -) -from homeassistant.core import HomeAssistant -from homeassistant.exceptions import HomeAssistantError -from homeassistant.setup import async_setup_component -from homeassistant.util.dt import utcnow - -from tests.common import MockConfigEntry, async_fire_time_changed - -YAML_CONFIG = {"username": "test-user", "password": "test-password"} - -DEVICE_CONFIG_OPEN = { - "device_id": 533255, - "door_number": 1, - "name": "home", - "status": "open", - "link_status": "Connected", - "serial": "12345", -} - -DEVICE_CONFIG_OPENING = { - "device_id": 533255, - "door_number": 1, - "name": "home", - "status": "opening", - "link_status": "Connected", - "serial": "12345", -} - -DEVICE_CONFIG_CLOSED = { - "device_id": 533255, - "door_number": 1, - "name": "home", - "status": "closed", - "link_status": "Connected", - "serial": "12345", -} - -DEVICE_CONFIG_CLOSING = { - "device_id": 533255, - "door_number": 1, - "name": "home", - "status": "closing", - "link_status": "Connected", - "serial": "12345", -} - -DEVICE_CONFIG_DISCONNECTED = { - "device_id": 533255, - "door_number": 1, - "name": "home", - "status": "open", - "link_status": "Disconnected", - "serial": "12345", -} - -DEVICE_CONFIG_BAD = { - "device_id": 533255, - "door_number": 1, - "name": "home", - "status": "open", -} -DEVICE_CONFIG_BAD_NO_DOOR = { - "device_id": 533255, - "door_number": 2, - "name": "home", - "status": "open", - "link_status": "Disconnected", -} - - -async def test_cover_operation( - hass: HomeAssistant, - mock_aladdinconnect_api: MagicMock, -) -> None: - """Test Cover Operation states (open,close,opening,closing) cover.""" - config_entry = MockConfigEntry( - domain=DOMAIN, - data=YAML_CONFIG, - unique_id="test-id", - ) - config_entry.add_to_hass(hass) - - assert await async_setup_component(hass, "homeassistant", {}) - await hass.async_block_till_done() - - mock_aladdinconnect_api.async_get_door_status = AsyncMock(return_value=STATE_OPEN) - mock_aladdinconnect_api.get_door_status.return_value = STATE_OPEN - - with patch( - "homeassistant.components.aladdin_connect.AladdinConnectClient", - return_value=mock_aladdinconnect_api, - ): - await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - - assert config_entry.state is ConfigEntryState.LOADED - assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - assert COVER_DOMAIN in hass.config.components - - await hass.services.async_call( - COVER_DOMAIN, - SERVICE_OPEN_COVER, - {ATTR_ENTITY_ID: "cover.home"}, - blocking=True, - ) - assert hass.states.get("cover.home").state == STATE_OPEN - - mock_aladdinconnect_api.open_door.return_value = False - with pytest.raises(HomeAssistantError): - await hass.services.async_call( - COVER_DOMAIN, - SERVICE_OPEN_COVER, - {ATTR_ENTITY_ID: "cover.home"}, - blocking=True, - ) - - mock_aladdinconnect_api.open_door.return_value = True - - mock_aladdinconnect_api.async_get_door_status = AsyncMock(return_value=STATE_CLOSED) - mock_aladdinconnect_api.get_door_status.return_value = STATE_CLOSED - - await hass.services.async_call( - COVER_DOMAIN, - SERVICE_CLOSE_COVER, - {ATTR_ENTITY_ID: "cover.home"}, - blocking=True, - ) - async_fire_time_changed( - hass, - utcnow() + SCAN_INTERVAL, - ) - await hass.async_block_till_done() - - assert hass.states.get("cover.home").state == STATE_CLOSED - - mock_aladdinconnect_api.close_door.return_value = False - with pytest.raises(HomeAssistantError): - await hass.services.async_call( - COVER_DOMAIN, - SERVICE_CLOSE_COVER, - {ATTR_ENTITY_ID: "cover.home"}, - blocking=True, - ) - - mock_aladdinconnect_api.close_door.return_value = True - - mock_aladdinconnect_api.async_get_door_status = AsyncMock( - return_value=STATE_CLOSING - ) - mock_aladdinconnect_api.get_door_status.return_value = STATE_CLOSING - - async_fire_time_changed( - hass, - utcnow() + SCAN_INTERVAL, - ) - await hass.async_block_till_done() - assert hass.states.get("cover.home").state == STATE_CLOSING - - mock_aladdinconnect_api.async_get_door_status = AsyncMock( - return_value=STATE_OPENING - ) - mock_aladdinconnect_api.get_door_status.return_value = STATE_OPENING - - async_fire_time_changed( - hass, - utcnow() + SCAN_INTERVAL, - ) - await hass.async_block_till_done() - assert hass.states.get("cover.home").state == STATE_OPENING - - mock_aladdinconnect_api.async_get_door_status = AsyncMock(return_value=None) - mock_aladdinconnect_api.get_door_status.return_value = None - - await hass.services.async_call( - COVER_DOMAIN, - SERVICE_CLOSE_COVER, - {ATTR_ENTITY_ID: "cover.home"}, - blocking=True, - ) - async_fire_time_changed( - hass, - utcnow() + SCAN_INTERVAL, - ) - await hass.async_block_till_done() - - assert hass.states.get("cover.home").state == STATE_UNKNOWN - - mock_aladdinconnect_api.get_doors.side_effect = session_manager.ConnectionError - - async_fire_time_changed( - hass, - utcnow() + SCAN_INTERVAL, - ) - await hass.async_block_till_done() - - assert hass.states.get("cover.home").state == STATE_UNAVAILABLE - - mock_aladdinconnect_api.get_doors.side_effect = session_manager.InvalidPasswordError - mock_aladdinconnect_api.login.return_value = False - mock_aladdinconnect_api.login.side_effect = session_manager.InvalidPasswordError - - async_fire_time_changed( - hass, - utcnow() + SCAN_INTERVAL, - ) - await hass.async_block_till_done() - assert hass.states.get("cover.home").state == STATE_UNAVAILABLE diff --git a/tests/components/aladdin_connect/test_diagnostics.py b/tests/components/aladdin_connect/test_diagnostics.py deleted file mode 100644 index 48741c77cd1..00000000000 --- a/tests/components/aladdin_connect/test_diagnostics.py +++ /dev/null @@ -1,41 +0,0 @@ -"""Test AccuWeather diagnostics.""" - -from unittest.mock import MagicMock, patch - -from syrupy import SnapshotAssertion - -from homeassistant.components.aladdin_connect.const import DOMAIN -from homeassistant.core import HomeAssistant - -from tests.common import MockConfigEntry -from tests.components.diagnostics import get_diagnostics_for_config_entry -from tests.typing import ClientSessionGenerator - -YAML_CONFIG = {"username": "test-user", "password": "test-password"} - - -async def test_entry_diagnostics( - hass: HomeAssistant, - hass_client: ClientSessionGenerator, - snapshot: SnapshotAssertion, - mock_aladdinconnect_api: MagicMock, -) -> None: - """Test config entry diagnostics.""" - - config_entry = MockConfigEntry( - domain=DOMAIN, - data=YAML_CONFIG, - unique_id="test-id", - ) - config_entry.add_to_hass(hass) - - with patch( - "homeassistant.components.aladdin_connect.AladdinConnectClient", - return_value=mock_aladdinconnect_api, - ): - await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - - result = await get_diagnostics_for_config_entry(hass, hass_client, config_entry) - - assert result == snapshot diff --git a/tests/components/aladdin_connect/test_init.py b/tests/components/aladdin_connect/test_init.py deleted file mode 100644 index bcc32101437..00000000000 --- a/tests/components/aladdin_connect/test_init.py +++ /dev/null @@ -1,258 +0,0 @@ -"""Test for Aladdin Connect init logic.""" - -from unittest.mock import MagicMock, patch - -from AIOAladdinConnect.session_manager import InvalidPasswordError -from aiohttp import ClientConnectionError - -from homeassistant.components.aladdin_connect.const import DOMAIN -from homeassistant.config_entries import ConfigEntryState -from homeassistant.core import HomeAssistant -from homeassistant.helpers import device_registry as dr - -from .conftest import DEVICE_CONFIG_OPEN - -from tests.common import AsyncMock, MockConfigEntry - -CONFIG = {"username": "test-user", "password": "test-password"} -ID = "533255-1" - - -async def test_setup_get_doors_errors(hass: HomeAssistant) -> None: - """Test component setup Get Doors Errors.""" - config_entry = MockConfigEntry( - domain=DOMAIN, - data=CONFIG, - unique_id="test-id", - ) - config_entry.add_to_hass(hass) - with ( - patch( - "homeassistant.components.aladdin_connect.cover.AladdinConnectClient.login", - return_value=True, - ), - patch( - "homeassistant.components.aladdin_connect.cover.AladdinConnectClient.get_doors", - return_value=None, - ), - ): - assert await hass.config_entries.async_setup(config_entry.entry_id) is True - await hass.async_block_till_done() - assert len(hass.states.async_all()) == 0 - - -async def test_setup_login_error( - hass: HomeAssistant, mock_aladdinconnect_api: MagicMock -) -> None: - """Test component setup Login Errors.""" - config_entry = MockConfigEntry( - domain=DOMAIN, - data=CONFIG, - unique_id=ID, - ) - config_entry.add_to_hass(hass) - mock_aladdinconnect_api.login.return_value = False - mock_aladdinconnect_api.login.side_effect = InvalidPasswordError - with patch( - "homeassistant.components.aladdin_connect.cover.AladdinConnectClient", - return_value=mock_aladdinconnect_api, - ): - assert await hass.config_entries.async_setup(config_entry.entry_id) is False - - -async def test_setup_connection_error( - hass: HomeAssistant, mock_aladdinconnect_api: MagicMock -) -> None: - """Test component setup Login Errors.""" - config_entry = MockConfigEntry( - domain=DOMAIN, - data=CONFIG, - unique_id=ID, - ) - config_entry.add_to_hass(hass) - mock_aladdinconnect_api.login.side_effect = ClientConnectionError - with patch( - "homeassistant.components.aladdin_connect.AladdinConnectClient", - return_value=mock_aladdinconnect_api, - ): - assert await hass.config_entries.async_setup(config_entry.entry_id) is False - - -async def test_setup_component_no_error(hass: HomeAssistant) -> None: - """Test component setup No Error.""" - config_entry = MockConfigEntry( - domain=DOMAIN, - data=CONFIG, - unique_id=ID, - ) - config_entry.add_to_hass(hass) - with patch( - "homeassistant.components.aladdin_connect.cover.AladdinConnectClient.login", - return_value=True, - ): - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - - assert config_entry.state is ConfigEntryState.LOADED - assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - - -async def test_entry_password_fail( - hass: HomeAssistant, mock_aladdinconnect_api: MagicMock -) -> None: - """Test password fail during entry.""" - entry = MockConfigEntry( - domain=DOMAIN, - data={"username": "test-user", "password": "test-password"}, - ) - entry.add_to_hass(hass) - mock_aladdinconnect_api.login = AsyncMock(return_value=False) - mock_aladdinconnect_api.login.side_effect = InvalidPasswordError - with patch( - "homeassistant.components.aladdin_connect.AladdinConnectClient", - return_value=mock_aladdinconnect_api, - ): - await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - assert entry.state is ConfigEntryState.SETUP_ERROR - - -async def test_load_and_unload( - hass: HomeAssistant, mock_aladdinconnect_api: MagicMock -) -> None: - """Test loading and unloading Aladdin Connect entry.""" - config_entry = MockConfigEntry( - domain=DOMAIN, - data=CONFIG, - unique_id=ID, - ) - config_entry.add_to_hass(hass) - - with patch( - "homeassistant.components.aladdin_connect.AladdinConnectClient", - return_value=mock_aladdinconnect_api, - ): - await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - - assert config_entry.state is ConfigEntryState.LOADED - assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - - assert await hass.config_entries.async_unload(config_entry.entry_id) - await hass.async_block_till_done() - assert config_entry.state is ConfigEntryState.NOT_LOADED - - -async def test_stale_device_removal( - hass: HomeAssistant, - device_registry: dr.DeviceRegistry, - mock_aladdinconnect_api: MagicMock, -) -> None: - """Test component setup missing door device is removed.""" - DEVICE_CONFIG_DOOR_2 = { - "device_id": 533255, - "door_number": 2, - "name": "home 2", - "status": "open", - "link_status": "Connected", - "serial": "12346", - "model": "02", - } - config_entry = MockConfigEntry( - domain=DOMAIN, - data=CONFIG, - unique_id=ID, - ) - config_entry.add_to_hass(hass) - mock_aladdinconnect_api.get_doors = AsyncMock( - return_value=[DEVICE_CONFIG_OPEN, DEVICE_CONFIG_DOOR_2] - ) - config_entry_other = MockConfigEntry( - domain="OtherDomain", - data=CONFIG, - unique_id="unique_id", - ) - config_entry_other.add_to_hass(hass) - - device_entry_other = device_registry.async_get_or_create( - config_entry_id=config_entry_other.entry_id, - identifiers={("OtherDomain", "533255-2")}, - ) - device_registry.async_update_device( - device_entry_other.id, - add_config_entry_id=config_entry.entry_id, - merge_identifiers={(DOMAIN, "533255-2")}, - ) - - with patch( - "homeassistant.components.aladdin_connect.AladdinConnectClient", - return_value=mock_aladdinconnect_api, - ): - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - - assert config_entry.state is ConfigEntryState.LOADED - assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - - device_entries = dr.async_entries_for_config_entry( - device_registry, config_entry.entry_id - ) - - assert len(device_entries) == 2 - assert any((DOMAIN, "533255-1") in device.identifiers for device in device_entries) - assert any((DOMAIN, "533255-2") in device.identifiers for device in device_entries) - assert any( - ("OtherDomain", "533255-2") in device.identifiers for device in device_entries - ) - - device_entries_other = dr.async_entries_for_config_entry( - device_registry, config_entry_other.entry_id - ) - assert len(device_entries_other) == 1 - assert any( - (DOMAIN, "533255-2") in device.identifiers for device in device_entries_other - ) - assert any( - ("OtherDomain", "533255-2") in device.identifiers - for device in device_entries_other - ) - - assert await hass.config_entries.async_unload(config_entry.entry_id) - await hass.async_block_till_done() - assert config_entry.state is ConfigEntryState.NOT_LOADED - - mock_aladdinconnect_api.get_doors = AsyncMock(return_value=[DEVICE_CONFIG_OPEN]) - with patch( - "homeassistant.components.aladdin_connect.AladdinConnectClient", - return_value=mock_aladdinconnect_api, - ): - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - - assert config_entry.state is ConfigEntryState.LOADED - assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - - device_entries = dr.async_entries_for_config_entry( - device_registry, config_entry.entry_id - ) - assert len(device_entries) == 1 - assert any((DOMAIN, "533255-1") in device.identifiers for device in device_entries) - assert not any( - (DOMAIN, "533255-2") in device.identifiers for device in device_entries - ) - assert not any( - ("OtherDomain", "533255-2") in device.identifiers for device in device_entries - ) - - device_entries_other = dr.async_entries_for_config_entry( - device_registry, config_entry_other.entry_id - ) - - assert len(device_entries_other) == 1 - assert any( - ("OtherDomain", "533255-2") in device.identifiers - for device in device_entries_other - ) - assert any( - (DOMAIN, "533255-2") in device.identifiers for device in device_entries_other - ) diff --git a/tests/components/aladdin_connect/test_model.py b/tests/components/aladdin_connect/test_model.py deleted file mode 100644 index 84b1c9ae40a..00000000000 --- a/tests/components/aladdin_connect/test_model.py +++ /dev/null @@ -1,19 +0,0 @@ -"""Test the Aladdin Connect model class.""" - -from homeassistant.components.aladdin_connect.model import DoorDevice -from homeassistant.core import HomeAssistant - - -async def test_model(hass: HomeAssistant) -> None: - """Test model for Aladdin Connect Model.""" - test_values = { - "device_id": "1", - "door_number": "2", - "name": "my door", - "status": "good", - } - result2 = DoorDevice(test_values) - assert result2["device_id"] == "1" - assert result2["door_number"] == "2" - assert result2["name"] == "my door" - assert result2["status"] == "good" diff --git a/tests/components/aladdin_connect/test_sensor.py b/tests/components/aladdin_connect/test_sensor.py deleted file mode 100644 index 9c229e2ac5e..00000000000 --- a/tests/components/aladdin_connect/test_sensor.py +++ /dev/null @@ -1,165 +0,0 @@ -"""Test the Aladdin Connect Sensors.""" - -from datetime import timedelta -from unittest.mock import AsyncMock, MagicMock, patch - -from homeassistant.components.aladdin_connect.const import DOMAIN -from homeassistant.components.aladdin_connect.cover import SCAN_INTERVAL -from homeassistant.core import HomeAssistant -from homeassistant.helpers import entity_registry as er -from homeassistant.util.dt import utcnow - -from tests.common import MockConfigEntry, async_fire_time_changed - -DEVICE_CONFIG_MODEL_01 = { - "device_id": 533255, - "door_number": 1, - "name": "home", - "status": "closed", - "link_status": "Connected", - "serial": "12345", - "model": "01", -} - - -CONFIG = {"username": "test-user", "password": "test-password"} -RELOAD_AFTER_UPDATE_DELAY = timedelta(seconds=31) - - -async def test_sensors( - hass: HomeAssistant, - mock_aladdinconnect_api: MagicMock, - entity_registry: er.EntityRegistry, -) -> None: - """Test Sensors for AladdinConnect.""" - config_entry = MockConfigEntry( - domain=DOMAIN, - data=CONFIG, - unique_id="test-id", - ) - config_entry.add_to_hass(hass) - - await hass.async_block_till_done() - - with patch( - "homeassistant.components.aladdin_connect.AladdinConnectClient", - return_value=mock_aladdinconnect_api, - ): - await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - - entry = entity_registry.async_get("sensor.home_battery") - assert entry - assert entry.disabled - assert entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION - update_entry = entity_registry.async_update_entity( - entry.entity_id, disabled_by=None - ) - await hass.async_block_till_done() - assert update_entry != entry - assert update_entry.disabled is False - state = hass.states.get("sensor.home_battery") - assert state is None - - async_fire_time_changed( - hass, - utcnow() + SCAN_INTERVAL, - ) - await hass.async_block_till_done() - state = hass.states.get("sensor.home_battery") - assert state - - entry = entity_registry.async_get("sensor.home_wi_fi_rssi") - await hass.async_block_till_done() - assert entry - assert entry.disabled - assert entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION - update_entry = entity_registry.async_update_entity( - entry.entity_id, disabled_by=None - ) - await hass.async_block_till_done() - assert update_entry != entry - assert update_entry.disabled is False - state = hass.states.get("sensor.home_wi_fi_rssi") - assert state is None - - update_entry = entity_registry.async_update_entity( - entry.entity_id, disabled_by=None - ) - await hass.async_block_till_done() - async_fire_time_changed( - hass, - utcnow() + SCAN_INTERVAL, - ) - await hass.async_block_till_done() - - state = hass.states.get("sensor.home_wi_fi_rssi") - assert state - - -async def test_sensors_model_01( - hass: HomeAssistant, - mock_aladdinconnect_api: MagicMock, - entity_registry: er.EntityRegistry, -) -> None: - """Test Sensors for AladdinConnect.""" - config_entry = MockConfigEntry( - domain=DOMAIN, - data=CONFIG, - unique_id="test-id", - ) - config_entry.add_to_hass(hass) - - await hass.async_block_till_done() - - with patch( - "homeassistant.components.aladdin_connect.AladdinConnectClient", - return_value=mock_aladdinconnect_api, - ): - mock_aladdinconnect_api.get_doors = AsyncMock( - return_value=[DEVICE_CONFIG_MODEL_01] - ) - await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - - entry = entity_registry.async_get("sensor.home_battery") - assert entry - assert entry.disabled is False - assert entry.disabled_by is None - state = hass.states.get("sensor.home_battery") - assert state - - entry = entity_registry.async_get("sensor.home_wi_fi_rssi") - await hass.async_block_till_done() - assert entry - assert entry.disabled - assert entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION - update_entry = entity_registry.async_update_entity( - entry.entity_id, disabled_by=None - ) - await hass.async_block_till_done() - assert update_entry != entry - assert update_entry.disabled is False - state = hass.states.get("sensor.home_wi_fi_rssi") - assert state is None - - update_entry = entity_registry.async_update_entity( - entry.entity_id, disabled_by=None - ) - await hass.async_block_till_done() - async_fire_time_changed( - hass, - utcnow() + SCAN_INTERVAL, - ) - await hass.async_block_till_done() - - state = hass.states.get("sensor.home_wi_fi_rssi") - assert state - - entry = entity_registry.async_get("sensor.home_ble_strength") - await hass.async_block_till_done() - assert entry - assert entry.disabled is False - assert entry.disabled_by is None - state = hass.states.get("sensor.home_ble_strength") - assert state From ab9581c61705c22f499846f698fcef281cb83125 Mon Sep 17 00:00:00 2001 From: Evgeny <940893+freekode@users.noreply.github.com> Date: Wed, 29 May 2024 23:12:24 +0200 Subject: [PATCH 0010/1445] Fix OpenWeatherMap migration (#118428) --- homeassistant/components/openweathermap/__init__.py | 7 ++++--- homeassistant/components/openweathermap/const.py | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/openweathermap/__init__.py b/homeassistant/components/openweathermap/__init__.py index 4d6cae86f39..7b21ae89b96 100644 --- a/homeassistant/components/openweathermap/__init__.py +++ b/homeassistant/components/openweathermap/__init__.py @@ -72,14 +72,15 @@ async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Migrate old entry.""" config_entries = hass.config_entries data = entry.data + options = entry.options version = entry.version _LOGGER.debug("Migrating OpenWeatherMap entry from version %s", version) - if version < 3: - new_data = {**data, CONF_MODE: OWM_MODE_V25} + if version < 4: + new_data = {**data, **options, CONF_MODE: OWM_MODE_V25} config_entries.async_update_entry( - entry, data=new_data, version=CONFIG_FLOW_VERSION + entry, data=new_data, options={}, version=CONFIG_FLOW_VERSION ) _LOGGER.info("Migration to version %s successful", CONFIG_FLOW_VERSION) diff --git a/homeassistant/components/openweathermap/const.py b/homeassistant/components/openweathermap/const.py index 1e5bfff4697..c074640ebc7 100644 --- a/homeassistant/components/openweathermap/const.py +++ b/homeassistant/components/openweathermap/const.py @@ -25,7 +25,7 @@ DEFAULT_NAME = "OpenWeatherMap" DEFAULT_LANGUAGE = "en" ATTRIBUTION = "Data provided by OpenWeatherMap" MANUFACTURER = "OpenWeather" -CONFIG_FLOW_VERSION = 3 +CONFIG_FLOW_VERSION = 4 ATTR_API_PRECIPITATION = "precipitation" ATTR_API_PRECIPITATION_KIND = "precipitation_kind" ATTR_API_DATETIME = "datetime" From f957ba09de6cc4313498c1d035d71bec4e39a927 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 29 May 2024 11:37:24 -1000 Subject: [PATCH 0011/1445] Fix blocking I/O in the event loop in meteo_france (#118429) --- homeassistant/components/meteo_france/weather.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/meteo_france/weather.py b/homeassistant/components/meteo_france/weather.py index 9edc557aafc..943d30fccfd 100644 --- a/homeassistant/components/meteo_france/weather.py +++ b/homeassistant/components/meteo_france/weather.py @@ -200,7 +200,7 @@ class MeteoFranceWeather( break forecast_data.append( { - ATTR_FORECAST_TIME: self.coordinator.data.timestamp_to_locale_time( + ATTR_FORECAST_TIME: dt_util.utc_from_timestamp( forecast["dt"] ).isoformat(), ATTR_FORECAST_CONDITION: format_condition( From e50defa7f5caa1c20e8cfffcd840b80e120a2058 Mon Sep 17 00:00:00 2001 From: tronikos Date: Wed, 29 May 2024 14:37:36 -0700 Subject: [PATCH 0012/1445] Bump opower to 0.4.6 (#118434) --- homeassistant/components/opower/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/opower/manifest.json b/homeassistant/components/opower/manifest.json index 91e4fbc960c..7e16bacdfda 100644 --- a/homeassistant/components/opower/manifest.json +++ b/homeassistant/components/opower/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/opower", "iot_class": "cloud_polling", "loggers": ["opower"], - "requirements": ["opower==0.4.4"] + "requirements": ["opower==0.4.6"] } diff --git a/requirements_all.txt b/requirements_all.txt index c7ee7ae5623..d60aeb4892e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1501,7 +1501,7 @@ openwrt-luci-rpc==1.1.17 openwrt-ubus-rpc==0.0.2 # homeassistant.components.opower -opower==0.4.4 +opower==0.4.6 # homeassistant.components.oralb oralb-ble==0.17.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ccc1ae213ca..7321bb6429b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1201,7 +1201,7 @@ openhomedevice==2.2.0 openwebifpy==4.2.4 # homeassistant.components.opower -opower==0.4.4 +opower==0.4.6 # homeassistant.components.oralb oralb-ble==0.17.6 From ef79842c2f5a5d7120dd22f53f57b24450b5ab3f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 29 May 2024 12:55:53 -1000 Subject: [PATCH 0013/1445] Fix google_mail doing blocking i/o in the event loop (take 2) (#118441) --- homeassistant/components/google_mail/__init__.py | 2 +- homeassistant/components/google_mail/api.py | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/google_mail/__init__.py b/homeassistant/components/google_mail/__init__.py index 1ac963b430a..441ecd3841f 100644 --- a/homeassistant/components/google_mail/__init__.py +++ b/homeassistant/components/google_mail/__init__.py @@ -32,7 +32,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Google Mail from a config entry.""" implementation = await async_get_config_entry_implementation(hass, entry) session = OAuth2Session(hass, entry, implementation) - auth = AsyncConfigEntryAuth(session) + auth = AsyncConfigEntryAuth(hass, session) await auth.check_and_refresh_token() hass.data[DOMAIN][entry.entry_id] = auth diff --git a/homeassistant/components/google_mail/api.py b/homeassistant/components/google_mail/api.py index e824e4b3ddd..485d640a04d 100644 --- a/homeassistant/components/google_mail/api.py +++ b/homeassistant/components/google_mail/api.py @@ -1,5 +1,7 @@ """API for Google Mail bound to Home Assistant OAuth.""" +from functools import partial + from aiohttp.client_exceptions import ClientError, ClientResponseError from google.auth.exceptions import RefreshError from google.oauth2.credentials import Credentials @@ -7,6 +9,7 @@ from googleapiclient.discovery import Resource, build from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_ACCESS_TOKEN +from homeassistant.core import HomeAssistant from homeassistant.exceptions import ( ConfigEntryAuthFailed, ConfigEntryNotReady, @@ -20,9 +23,11 @@ class AsyncConfigEntryAuth: def __init__( self, + hass: HomeAssistant, oauth2_session: config_entry_oauth2_flow.OAuth2Session, ) -> None: """Initialize Google Mail Auth.""" + self._hass = hass self.oauth_session = oauth2_session @property @@ -58,4 +63,6 @@ class AsyncConfigEntryAuth: async def get_resource(self) -> Resource: """Get current resource.""" credentials = Credentials(await self.check_and_refresh_token()) - return build("gmail", "v1", credentials=credentials) + return await self._hass.async_add_executor_job( + partial(build, "gmail", "v1", credentials=credentials) + ) From 639f6c640c46d01ca4496cd8056900aa2dec26dd Mon Sep 17 00:00:00 2001 From: tronikos Date: Wed, 29 May 2024 18:44:33 -0700 Subject: [PATCH 0014/1445] Improve LLM prompt (#118443) * Improve LLM prompt * test * improvements * improvements --- homeassistant/helpers/llm.py | 4 +++- tests/helpers/test_llm.py | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/homeassistant/helpers/llm.py b/homeassistant/helpers/llm.py index 5a39bfaa726..d1ce3047e78 100644 --- a/homeassistant/helpers/llm.py +++ b/homeassistant/helpers/llm.py @@ -250,7 +250,9 @@ class AssistAPI(API): prompt = [ ( "Call the intent tools to control Home Assistant. " - "When controlling an area, prefer passing area name and domain." + "When controlling a device, prefer passing just its name and its domain " + "(what comes before the dot in its entity id). " + "When controlling an area, prefer passing just area name and a single domain." ) ] area: ar.AreaEntry | None = None diff --git a/tests/helpers/test_llm.py b/tests/helpers/test_llm.py index a59b4767196..672b6a6642b 100644 --- a/tests/helpers/test_llm.py +++ b/tests/helpers/test_llm.py @@ -423,7 +423,9 @@ async def test_assist_api_prompt( ) first_part_prompt = ( "Call the intent tools to control Home Assistant. " - "When controlling an area, prefer passing area name and domain." + "When controlling a device, prefer passing just its name and its domain " + "(what comes before the dot in its entity id). " + "When controlling an area, prefer passing just area name and a single domain." ) no_timer_prompt = "This device does not support timers." From 4893faa67178b4e015d83e37041ab73266dab6b9 Mon Sep 17 00:00:00 2001 From: tronikos Date: Wed, 29 May 2024 23:37:45 -0700 Subject: [PATCH 0015/1445] Instruct LLM to not pass a list to the domain (#118451) --- homeassistant/helpers/llm.py | 3 ++- tests/helpers/test_llm.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/helpers/llm.py b/homeassistant/helpers/llm.py index d1ce3047e78..535e2af4d04 100644 --- a/homeassistant/helpers/llm.py +++ b/homeassistant/helpers/llm.py @@ -250,9 +250,10 @@ class AssistAPI(API): prompt = [ ( "Call the intent tools to control Home Assistant. " + "Do not pass the domain to the intent tools as a list. " "When controlling a device, prefer passing just its name and its domain " "(what comes before the dot in its entity id). " - "When controlling an area, prefer passing just area name and a single domain." + "When controlling an area, prefer passing just area name and domain." ) ] area: ar.AreaEntry | None = None diff --git a/tests/helpers/test_llm.py b/tests/helpers/test_llm.py index 672b6a6642b..63c1214dd6d 100644 --- a/tests/helpers/test_llm.py +++ b/tests/helpers/test_llm.py @@ -423,9 +423,10 @@ async def test_assist_api_prompt( ) first_part_prompt = ( "Call the intent tools to control Home Assistant. " + "Do not pass the domain to the intent tools as a list. " "When controlling a device, prefer passing just its name and its domain " "(what comes before the dot in its entity id). " - "When controlling an area, prefer passing just area name and a single domain." + "When controlling an area, prefer passing just area name and domain." ) no_timer_prompt = "This device does not support timers." From 092cdcfe91611a368eb305dd2b64fc5101cdbeaa Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 30 May 2024 08:46:18 +0200 Subject: [PATCH 0016/1445] Improve type hints in tests (a-h) (#118379) --- tests/components/camera/test_init.py | 6 ++--- .../test_auth_provider_homeassistant.py | 10 ++++--- tests/components/config/test_automation.py | 6 ++--- tests/components/config/test_core.py | 10 +++++-- tests/components/counter/test_init.py | 3 ++- tests/components/dynalite/test_panel.py | 14 +++++++--- tests/components/energy/test_sensor.py | 3 ++- tests/components/esphome/test_dashboard.py | 27 ++++++++++++------- tests/components/frontend/test_init.py | 8 +++--- .../google_assistant/test_google_assistant.py | 2 +- tests/components/hassio/test_http.py | 3 ++- .../homeassistant_alerts/test_init.py | 6 ++--- .../components/huawei_lte/test_config_flow.py | 2 +- 13 files changed, 66 insertions(+), 34 deletions(-) diff --git a/tests/components/camera/test_init.py b/tests/components/camera/test_init.py index dffc7e5aa53..0520908f210 100644 --- a/tests/components/camera/test_init.py +++ b/tests/components/camera/test_init.py @@ -849,12 +849,12 @@ async def test_rtsp_to_web_rtc_offer( async def test_unsupported_rtsp_to_web_rtc_stream_type( - hass, - hass_ws_client, + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, mock_camera, mock_hls_stream_source, # Not an RTSP stream source mock_rtsp_to_web_rtc, -): +) -> None: """Test rtsp-to-webrtc is not registered for non-RTSP streams.""" client = await hass_ws_client(hass) await client.send_json( diff --git a/tests/components/config/test_auth_provider_homeassistant.py b/tests/components/config/test_auth_provider_homeassistant.py index d2631cd7a7c..5c5661376e2 100644 --- a/tests/components/config/test_auth_provider_homeassistant.py +++ b/tests/components/config/test_auth_provider_homeassistant.py @@ -13,19 +13,23 @@ from tests.typing import WebSocketGenerator @pytest.fixture(autouse=True) -async def setup_config(hass, local_auth): +async def setup_config( + hass: HomeAssistant, local_auth: prov_ha.HassAuthProvider +) -> None: """Fixture that sets up the auth provider .""" auth_ha.async_setup(hass) @pytest.fixture -async def auth_provider(local_auth): +async def auth_provider( + local_auth: prov_ha.HassAuthProvider, +) -> prov_ha.HassAuthProvider: """Hass auth provider.""" return local_auth @pytest.fixture -async def owner_access_token(hass, hass_owner_user): +async def owner_access_token(hass: HomeAssistant, hass_owner_user: MockUser) -> str: """Access token for owner user.""" refresh_token = await hass.auth.async_create_refresh_token( hass_owner_user, CLIENT_ID diff --git a/tests/components/config/test_automation.py b/tests/components/config/test_automation.py index b17face10d9..9d9ee5d5649 100644 --- a/tests/components/config/test_automation.py +++ b/tests/components/config/test_automation.py @@ -25,10 +25,10 @@ def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None: @pytest.fixture async def setup_automation( - hass, + hass: HomeAssistant, automation_config, - stub_blueprint_populate, -): + stub_blueprint_populate: None, +) -> None: """Set up automation integration.""" assert await async_setup_component( hass, "automation", {"automation": automation_config} diff --git a/tests/components/config/test_core.py b/tests/components/config/test_core.py index da8a60ca6fd..29cbdd9b83e 100644 --- a/tests/components/config/test_core.py +++ b/tests/components/config/test_core.py @@ -19,11 +19,17 @@ from homeassistant.util import dt as dt_util, location from homeassistant.util.unit_system import US_CUSTOMARY_SYSTEM from tests.common import MockUser -from tests.typing import ClientSessionGenerator, WebSocketGenerator +from tests.typing import ( + ClientSessionGenerator, + MockHAClientWebSocket, + WebSocketGenerator, +) @pytest.fixture -async def client(hass, hass_ws_client): +async def client( + hass: HomeAssistant, hass_ws_client: WebSocketGenerator +) -> MockHAClientWebSocket: """Fixture that can interact with the config manager API.""" with patch.object(config, "SECTIONS", [core]): assert await async_setup_component(hass, "config", {}) diff --git a/tests/components/counter/test_init.py b/tests/components/counter/test_init.py index 342c22baf24..ef2caf2eab1 100644 --- a/tests/components/counter/test_init.py +++ b/tests/components/counter/test_init.py @@ -1,6 +1,7 @@ """The tests for the counter component.""" import logging +from typing import Any import pytest @@ -37,7 +38,7 @@ _LOGGER = logging.getLogger(__name__) @pytest.fixture -def storage_setup(hass, hass_storage): +def storage_setup(hass: HomeAssistant, hass_storage: dict[str, Any]): """Storage setup.""" async def _storage(items=None, config=None): diff --git a/tests/components/dynalite/test_panel.py b/tests/components/dynalite/test_panel.py index a1cd9749eb5..97752142f0c 100644 --- a/tests/components/dynalite/test_panel.py +++ b/tests/components/dynalite/test_panel.py @@ -5,11 +5,15 @@ from unittest.mock import patch from homeassistant.components import dynalite from homeassistant.components.cover import DEVICE_CLASSES from homeassistant.const import CONF_PORT +from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry +from tests.typing import WebSocketGenerator -async def test_get_config(hass, hass_ws_client): +async def test_get_config( + hass: HomeAssistant, hass_ws_client: WebSocketGenerator +) -> None: """Get the config via websocket.""" host = "1.2.3.4" port = 765 @@ -49,7 +53,9 @@ async def test_get_config(hass, hass_ws_client): } -async def test_save_config(hass, hass_ws_client): +async def test_save_config( + hass: HomeAssistant, hass_ws_client: WebSocketGenerator +) -> None: """Save the config via websocket.""" host1 = "1.2.3.4" port1 = 765 @@ -103,7 +109,9 @@ async def test_save_config(hass, hass_ws_client): assert modified_entry.data[CONF_PORT] == port3 -async def test_save_config_invalid_entry(hass, hass_ws_client): +async def test_save_config_invalid_entry( + hass: HomeAssistant, hass_ws_client: WebSocketGenerator +) -> None: """Try to update nonexistent entry.""" host1 = "1.2.3.4" port1 = 765 diff --git a/tests/components/energy/test_sensor.py b/tests/components/energy/test_sensor.py index 192cf6abea4..4128a80c587 100644 --- a/tests/components/energy/test_sensor.py +++ b/tests/components/energy/test_sensor.py @@ -7,6 +7,7 @@ from typing import Any import pytest from homeassistant.components.energy import data +from homeassistant.components.recorder.core import Recorder from homeassistant.components.recorder.util import session_scope from homeassistant.components.sensor import ( ATTR_LAST_RESET, @@ -35,7 +36,7 @@ TEST_TIME_ADVANCE_INTERVAL = timedelta(milliseconds=10) @pytest.fixture -async def setup_integration(recorder_mock): +async def setup_integration(recorder_mock: Recorder): """Set up the integration.""" async def setup_integration(hass): diff --git a/tests/components/esphome/test_dashboard.py b/tests/components/esphome/test_dashboard.py index dbf092bb9fc..1b0303a8a48 100644 --- a/tests/components/esphome/test_dashboard.py +++ b/tests/components/esphome/test_dashboard.py @@ -1,5 +1,6 @@ """Test ESPHome dashboard features.""" +from typing import Any from unittest.mock import patch from aioesphomeapi import DeviceInfo, InvalidAuthAPIError @@ -15,7 +16,7 @@ from tests.common import MockConfigEntry async def test_dashboard_storage( - hass: HomeAssistant, init_integration, mock_dashboard, hass_storage + hass: HomeAssistant, init_integration, mock_dashboard, hass_storage: dict[str, Any] ) -> None: """Test dashboard storage.""" assert hass_storage[dashboard.STORAGE_KEY]["data"] == { @@ -28,8 +29,10 @@ async def test_dashboard_storage( async def test_restore_dashboard_storage( - hass: HomeAssistant, mock_config_entry: MockConfigEntry, hass_storage -) -> MockConfigEntry: + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + hass_storage: dict[str, Any], +) -> None: """Restore dashboard url and slug from storage.""" hass_storage[dashboard.STORAGE_KEY] = { "version": dashboard.STORAGE_VERSION, @@ -46,8 +49,10 @@ async def test_restore_dashboard_storage( async def test_restore_dashboard_storage_end_to_end( - hass: HomeAssistant, mock_config_entry: MockConfigEntry, hass_storage -) -> MockConfigEntry: + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + hass_storage: dict[str, Any], +) -> None: """Restore dashboard url and slug from storage.""" hass_storage[dashboard.STORAGE_KEY] = { "version": dashboard.STORAGE_VERSION, @@ -65,8 +70,10 @@ async def test_restore_dashboard_storage_end_to_end( async def test_setup_dashboard_fails( - hass: HomeAssistant, mock_config_entry: MockConfigEntry, hass_storage -) -> MockConfigEntry: + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + hass_storage: dict[str, Any], +) -> None: """Test that nothing is stored on failed dashboard setup when there was no dashboard before.""" with patch.object( coordinator.ESPHomeDashboardAPI, "get_devices", side_effect=TimeoutError @@ -83,8 +90,10 @@ async def test_setup_dashboard_fails( async def test_setup_dashboard_fails_when_already_setup( - hass: HomeAssistant, mock_config_entry: MockConfigEntry, hass_storage -) -> MockConfigEntry: + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + hass_storage: dict[str, Any], +) -> None: """Test failed dashboard setup still reloads entries if one existed before.""" with patch.object( coordinator.ESPHomeDashboardAPI, "get_devices" diff --git a/tests/components/frontend/test_init.py b/tests/components/frontend/test_init.py index ddfe2b80b1d..57ee04da47f 100644 --- a/tests/components/frontend/test_init.py +++ b/tests/components/frontend/test_init.py @@ -109,14 +109,16 @@ async def mock_http_client( @pytest.fixture async def themes_ws_client( - hass: HomeAssistant, hass_ws_client: ClientSessionGenerator, frontend_themes -) -> TestClient: + hass: HomeAssistant, hass_ws_client: WebSocketGenerator, frontend_themes +) -> MockHAClientWebSocket: """Start the Home Assistant HTTP component.""" return await hass_ws_client(hass) @pytest.fixture -async def ws_client(hass, hass_ws_client, frontend): +async def ws_client( + hass: HomeAssistant, hass_ws_client: WebSocketGenerator, frontend +) -> MockHAClientWebSocket: """Start the Home Assistant HTTP component.""" return await hass_ws_client(hass) diff --git a/tests/components/google_assistant/test_google_assistant.py b/tests/components/google_assistant/test_google_assistant.py index 015818d132d..ea30f89e0ef 100644 --- a/tests/components/google_assistant/test_google_assistant.py +++ b/tests/components/google_assistant/test_google_assistant.py @@ -36,7 +36,7 @@ ACCESS_TOKEN = "superdoublesecret" @pytest.fixture -def auth_header(hass_access_token): +def auth_header(hass_access_token: str) -> dict[str, str]: """Generate an HTTP header with bearer token authorization.""" return {AUTHORIZATION: f"Bearer {hass_access_token}"} diff --git a/tests/components/hassio/test_http.py b/tests/components/hassio/test_http.py index 55d4d8b0365..a5ffb4f0d83 100644 --- a/tests/components/hassio/test_http.py +++ b/tests/components/hassio/test_http.py @@ -6,6 +6,7 @@ from unittest.mock import patch from aiohttp import StreamReader import pytest +from tests.common import MockUser from tests.test_util.aiohttp import AiohttpClientMocker @@ -19,7 +20,7 @@ def mock_not_onboarded(): @pytest.fixture -def hassio_user_client(hassio_client, hass_admin_user): +def hassio_user_client(hassio_client, hass_admin_user: MockUser): """Return a Hass.io HTTP client tied to a non-admin user.""" hass_admin_user.groups = [] return hassio_client diff --git a/tests/components/homeassistant_alerts/test_init.py b/tests/components/homeassistant_alerts/test_init.py index c1974bdf886..444db019c7c 100644 --- a/tests/components/homeassistant_alerts/test_init.py +++ b/tests/components/homeassistant_alerts/test_init.py @@ -580,13 +580,13 @@ async def test_no_alerts( ) async def test_alerts_change( hass: HomeAssistant, - hass_ws_client, + hass_ws_client: WebSocketGenerator, aioclient_mock: AiohttpClientMocker, ha_version: str, fixture_1: str, - expected_alerts_1: list[tuple(str, str)], + expected_alerts_1: list[tuple[str, str]], fixture_2: str, - expected_alerts_2: list[tuple(str, str)], + expected_alerts_2: list[tuple[str, str]], ) -> None: """Test creating issues based on alerts.""" fixture_1_content = load_fixture(fixture_1, "homeassistant_alerts") diff --git a/tests/components/huawei_lte/test_config_flow.py b/tests/components/huawei_lte/test_config_flow.py index 200796c87e7..329f06795d2 100644 --- a/tests/components/huawei_lte/test_config_flow.py +++ b/tests/components/huawei_lte/test_config_flow.py @@ -134,7 +134,7 @@ async def test_connection_errors( @pytest.fixture -def login_requests_mock(requests_mock): +def login_requests_mock(requests_mock: requests_mock.Mocker) -> requests_mock.Mocker: """Set up a requests_mock with base mocks for login tests.""" https_url = urlunparse( urlparse(FIXTURE_USER_INPUT[CONF_URL])._replace(scheme="https") From 242ee0464281d75a9b5439c58309f4973a17d4e3 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 30 May 2024 08:47:08 +0200 Subject: [PATCH 0017/1445] Improve type hints in tests (i-p) (#118380) --- tests/components/input_boolean/test_init.py | 3 ++- tests/components/input_button/test_init.py | 3 ++- tests/components/input_datetime/test_init.py | 3 ++- tests/components/input_number/test_init.py | 3 ++- tests/components/input_select/test_init.py | 3 ++- tests/components/input_text/test_init.py | 3 ++- tests/components/knx/conftest.py | 3 ++- tests/components/logbook/test_init.py | 2 +- tests/components/lovelace/test_dashboard.py | 16 ++++++++++++---- tests/components/lovelace/test_resources.py | 16 ++++++++++++---- tests/components/matrix/conftest.py | 3 ++- tests/components/maxcube/conftest.py | 11 ++++++++++- tests/components/mobile_app/test_notify.py | 4 +++- tests/components/network/test_init.py | 6 +++++- tests/components/otbr/test_websocket_api.py | 6 ++++-- .../components/owntracks/test_device_tracker.py | 7 +++++-- tests/components/person/conftest.py | 8 +++++++- tests/components/plex/conftest.py | 5 +++-- 18 files changed, 78 insertions(+), 27 deletions(-) diff --git a/tests/components/input_boolean/test_init.py b/tests/components/input_boolean/test_init.py index 2a616691e62..b2e99836477 100644 --- a/tests/components/input_boolean/test_init.py +++ b/tests/components/input_boolean/test_init.py @@ -1,6 +1,7 @@ """The tests for the input_boolean component.""" import logging +from typing import Any from unittest.mock import patch import pytest @@ -30,7 +31,7 @@ _LOGGER = logging.getLogger(__name__) @pytest.fixture -def storage_setup(hass, hass_storage): +def storage_setup(hass: HomeAssistant, hass_storage: dict[str, Any]): """Storage setup.""" async def _storage(items=None, config=None): diff --git a/tests/components/input_button/test_init.py b/tests/components/input_button/test_init.py index 568d0076318..e59d0543751 100644 --- a/tests/components/input_button/test_init.py +++ b/tests/components/input_button/test_init.py @@ -1,6 +1,7 @@ """The tests for the input_test component.""" import logging +from typing import Any from unittest.mock import patch import pytest @@ -27,7 +28,7 @@ _LOGGER = logging.getLogger(__name__) @pytest.fixture -def storage_setup(hass, hass_storage): +def storage_setup(hass: HomeAssistant, hass_storage: dict[str, Any]): """Storage setup.""" async def _storage(items=None, config=None): diff --git a/tests/components/input_datetime/test_init.py b/tests/components/input_datetime/test_init.py index 5d8ea90b8a6..fdbb9a7803f 100644 --- a/tests/components/input_datetime/test_init.py +++ b/tests/components/input_datetime/test_init.py @@ -1,6 +1,7 @@ """Tests for the Input slider component.""" import datetime +from typing import Any from unittest.mock import patch import pytest @@ -45,7 +46,7 @@ INITIAL_DATETIME = f"{INITIAL_DATE} {INITIAL_TIME}" @pytest.fixture -def storage_setup(hass, hass_storage): +def storage_setup(hass: HomeAssistant, hass_storage: dict[str, Any]): """Storage setup.""" async def _storage(items=None, config=None): diff --git a/tests/components/input_number/test_init.py b/tests/components/input_number/test_init.py index 62b95fe16b3..73e41f347ce 100644 --- a/tests/components/input_number/test_init.py +++ b/tests/components/input_number/test_init.py @@ -1,5 +1,6 @@ """The tests for the Input number component.""" +from typing import Any from unittest.mock import patch import pytest @@ -29,7 +30,7 @@ from tests.typing import WebSocketGenerator @pytest.fixture -def storage_setup(hass, hass_storage): +def storage_setup(hass: HomeAssistant, hass_storage: dict[str, Any]): """Storage setup.""" async def _storage(items=None, config=None): diff --git a/tests/components/input_select/test_init.py b/tests/components/input_select/test_init.py index 431f8b7d078..153d8ed848d 100644 --- a/tests/components/input_select/test_init.py +++ b/tests/components/input_select/test_init.py @@ -1,5 +1,6 @@ """The tests for the Input select component.""" +from typing import Any from unittest.mock import patch import pytest @@ -36,7 +37,7 @@ from tests.typing import WebSocketGenerator @pytest.fixture -def storage_setup(hass, hass_storage): +def storage_setup(hass: HomeAssistant, hass_storage: dict[str, Any]): """Storage setup.""" async def _storage(items=None, config=None, minor_version=STORAGE_VERSION_MINOR): diff --git a/tests/components/input_text/test_init.py b/tests/components/input_text/test_init.py index d98ee4f7668..3cae98b6dfe 100644 --- a/tests/components/input_text/test_init.py +++ b/tests/components/input_text/test_init.py @@ -1,5 +1,6 @@ """The tests for the Input text component.""" +from typing import Any from unittest.mock import patch import pytest @@ -36,7 +37,7 @@ TEST_VAL_MAX = 22 @pytest.fixture -def storage_setup(hass, hass_storage): +def storage_setup(hass: HomeAssistant, hass_storage: dict[str, Any]): """Storage setup.""" async def _storage(items=None, config=None): diff --git a/tests/components/knx/conftest.py b/tests/components/knx/conftest.py index a580fc9eb2c..864a160ac1a 100644 --- a/tests/components/knx/conftest.py +++ b/tests/components/knx/conftest.py @@ -4,6 +4,7 @@ from __future__ import annotations import asyncio import json +from typing import Any from unittest.mock import DEFAULT, AsyncMock, Mock, patch import pytest @@ -273,7 +274,7 @@ async def knx(request, hass, mock_config_entry: MockConfigEntry): @pytest.fixture -def load_knxproj(hass_storage): +def load_knxproj(hass_storage: dict[str, Any]) -> None: """Mock KNX project data.""" hass_storage[KNX_PROJECT_STORAGE_KEY] = { "version": 1, diff --git a/tests/components/logbook/test_init.py b/tests/components/logbook/test_init.py index 0ba96a8ca6a..3534192a43e 100644 --- a/tests/components/logbook/test_init.py +++ b/tests/components/logbook/test_init.py @@ -61,7 +61,7 @@ EMPTY_CONFIG = logbook.CONFIG_SCHEMA({logbook.DOMAIN: {}}) @pytest.fixture -async def hass_(recorder_mock, hass): +async def hass_(recorder_mock: Recorder, hass: HomeAssistant) -> HomeAssistant: """Set up things to be run when tests are started.""" assert await async_setup_component(hass, logbook.DOMAIN, EMPTY_CONFIG) return hass diff --git a/tests/components/lovelace/test_dashboard.py b/tests/components/lovelace/test_dashboard.py index 3353b2eea51..affa5e1479f 100644 --- a/tests/components/lovelace/test_dashboard.py +++ b/tests/components/lovelace/test_dashboard.py @@ -30,7 +30,9 @@ def mock_onboarding_done() -> Generator[MagicMock, None, None]: async def test_lovelace_from_storage( - hass: HomeAssistant, hass_ws_client, hass_storage: dict[str, Any] + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + hass_storage: dict[str, Any], ) -> None: """Test we load lovelace config from storage.""" assert await async_setup_component(hass, "lovelace", {}) @@ -83,7 +85,9 @@ async def test_lovelace_from_storage( async def test_lovelace_from_storage_save_before_load( - hass: HomeAssistant, hass_ws_client, hass_storage: dict[str, Any] + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + hass_storage: dict[str, Any], ) -> None: """Test we can load lovelace config from storage.""" assert await async_setup_component(hass, "lovelace", {}) @@ -101,7 +105,9 @@ async def test_lovelace_from_storage_save_before_load( async def test_lovelace_from_storage_delete( - hass: HomeAssistant, hass_ws_client, hass_storage: dict[str, Any] + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + hass_storage: dict[str, Any], ) -> None: """Test we delete lovelace config from storage.""" assert await async_setup_component(hass, "lovelace", {}) @@ -352,7 +358,9 @@ async def test_wrong_key_dashboard_from_yaml(hass: HomeAssistant) -> None: async def test_storage_dashboards( - hass: HomeAssistant, hass_ws_client, hass_storage: dict[str, Any] + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + hass_storage: dict[str, Any], ) -> None: """Test we load lovelace config from storage.""" assert await async_setup_component(hass, "lovelace", {}) diff --git a/tests/components/lovelace/test_resources.py b/tests/components/lovelace/test_resources.py index 7591960b589..d2008ce5d41 100644 --- a/tests/components/lovelace/test_resources.py +++ b/tests/components/lovelace/test_resources.py @@ -56,7 +56,9 @@ async def test_yaml_resources_backwards( async def test_storage_resources( - hass: HomeAssistant, hass_ws_client, hass_storage: dict[str, Any] + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + hass_storage: dict[str, Any], ) -> None: """Test defining resources in storage config.""" resource_config = [{**item, "id": uuid.uuid4().hex} for item in RESOURCE_EXAMPLES] @@ -77,7 +79,9 @@ async def test_storage_resources( async def test_storage_resources_import( - hass: HomeAssistant, hass_ws_client, hass_storage: dict[str, Any] + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + hass_storage: dict[str, Any], ) -> None: """Test importing resources from storage config.""" assert await async_setup_component(hass, "lovelace", {}) @@ -165,7 +169,9 @@ async def test_storage_resources_import( async def test_storage_resources_import_invalid( - hass: HomeAssistant, hass_ws_client, hass_storage: dict[str, Any] + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + hass_storage: dict[str, Any], ) -> None: """Test importing resources from storage config.""" assert await async_setup_component(hass, "lovelace", {}) @@ -189,7 +195,9 @@ async def test_storage_resources_import_invalid( async def test_storage_resources_safe_mode( - hass: HomeAssistant, hass_ws_client, hass_storage: dict[str, Any] + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + hass_storage: dict[str, Any], ) -> None: """Test defining resources in storage config.""" diff --git a/tests/components/matrix/conftest.py b/tests/components/matrix/conftest.py index 18227914df4..f65deea8dad 100644 --- a/tests/components/matrix/conftest.py +++ b/tests/components/matrix/conftest.py @@ -2,6 +2,7 @@ from __future__ import annotations +from pathlib import Path import re import tempfile from unittest.mock import patch @@ -304,7 +305,7 @@ def command_events(hass: HomeAssistant): @pytest.fixture -def image_path(tmp_path): +def image_path(tmp_path: Path): """Provide the Path to a mock image.""" image = Image.new("RGBA", size=(50, 50), color=(256, 0, 0)) image_file = tempfile.NamedTemporaryFile(dir=tmp_path) diff --git a/tests/components/maxcube/conftest.py b/tests/components/maxcube/conftest.py index 82a852a5201..88e40edfdd0 100644 --- a/tests/components/maxcube/conftest.py +++ b/tests/components/maxcube/conftest.py @@ -10,6 +10,8 @@ from maxcube.windowshutter import MaxWindowShutter import pytest from homeassistant.components.maxcube import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.helpers.typing import ConfigType from homeassistant.setup import async_setup_component from homeassistant.util.dt import now @@ -99,7 +101,14 @@ def hass_config(): @pytest.fixture -async def cube(hass, hass_config, room, thermostat, wallthermostat, windowshutter): +async def cube( + hass: HomeAssistant, + hass_config: ConfigType, + room, + thermostat, + wallthermostat, + windowshutter, +): """Build and setup a cube mock with a single room and some devices.""" with patch("homeassistant.components.maxcube.MaxCube") as mock: cube = mock.return_value diff --git a/tests/components/mobile_app/test_notify.py b/tests/components/mobile_app/test_notify.py index 53a51938fed..57f7933b00f 100644 --- a/tests/components/mobile_app/test_notify.py +++ b/tests/components/mobile_app/test_notify.py @@ -110,7 +110,9 @@ async def setup_push_receiver( @pytest.fixture -async def setup_websocket_channel_only_push(hass, hass_admin_user): +async def setup_websocket_channel_only_push( + hass: HomeAssistant, hass_admin_user: MockUser +) -> None: """Set up local push.""" entry = MockConfigEntry( data={ diff --git a/tests/components/network/test_init.py b/tests/components/network/test_init.py index b02692e5086..e57b3242e8c 100644 --- a/tests/components/network/test_init.py +++ b/tests/components/network/test_init.py @@ -20,6 +20,8 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import async_setup_component +from tests.typing import WebSocketGenerator + _NO_LOOPBACK_IPADDR = "192.168.1.5" _LOOPBACK_IPADDR = "127.0.0.1" @@ -409,7 +411,9 @@ async def test_interfaces_configured_from_storage( async def test_interfaces_configured_from_storage_websocket_update( - hass: HomeAssistant, hass_ws_client, hass_storage: dict[str, Any] + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + hass_storage: dict[str, Any], ) -> None: """Test settings from storage can be updated via websocket api.""" hass_storage[STORAGE_KEY] = { diff --git a/tests/components/otbr/test_websocket_api.py b/tests/components/otbr/test_websocket_api.py index c8ac839f629..df55d38d3b7 100644 --- a/tests/components/otbr/test_websocket_api.py +++ b/tests/components/otbr/test_websocket_api.py @@ -18,11 +18,13 @@ from . import ( ) from tests.test_util.aiohttp import AiohttpClientMocker -from tests.typing import WebSocketGenerator +from tests.typing import MockHAClientWebSocket, WebSocketGenerator @pytest.fixture -async def websocket_client(hass, hass_ws_client): +async def websocket_client( + hass: HomeAssistant, hass_ws_client: WebSocketGenerator +) -> MockHAClientWebSocket: """Create a websocket client.""" return await hass_ws_client(hass) diff --git a/tests/components/owntracks/test_device_tracker.py b/tests/components/owntracks/test_device_tracker.py index a36d03e973c..80e76a5e7b4 100644 --- a/tests/components/owntracks/test_device_tracker.py +++ b/tests/components/owntracks/test_device_tracker.py @@ -6,12 +6,13 @@ from unittest.mock import patch import pytest from homeassistant.components import owntracks +from homeassistant.components.device_tracker.legacy import Device from homeassistant.const import STATE_NOT_HOME from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry, async_fire_mqtt_message -from tests.typing import ClientSessionGenerator +from tests.typing import ClientSessionGenerator, MqttMockHAClient USER = "greg" DEVICE = "phone" @@ -284,7 +285,9 @@ BAD_JSON_SUFFIX = "** and it ends here ^^" @pytest.fixture -def setup_comp(hass, mock_device_tracker_conf, mqtt_mock): +def setup_comp( + hass, mock_device_tracker_conf: list[Device], mqtt_mock: MqttMockHAClient +): """Initialize components.""" hass.loop.run_until_complete(async_setup_component(hass, "device_tracker", {})) diff --git a/tests/components/person/conftest.py b/tests/components/person/conftest.py index 7f06b854c5c..ecec42b003d 100644 --- a/tests/components/person/conftest.py +++ b/tests/components/person/conftest.py @@ -1,14 +1,18 @@ """The tests for the person component.""" import logging +from typing import Any import pytest from homeassistant.components import person from homeassistant.components.person import DOMAIN +from homeassistant.core import HomeAssistant from homeassistant.helpers import collection from homeassistant.setup import async_setup_component +from tests.common import MockUser + DEVICE_TRACKER = "device_tracker.test_tracker" DEVICE_TRACKER_2 = "device_tracker.test_tracker_2" @@ -27,7 +31,9 @@ def storage_collection(hass): @pytest.fixture -def storage_setup(hass, hass_storage, hass_admin_user): +def storage_setup( + hass: HomeAssistant, hass_storage: dict[str, Any], hass_admin_user: MockUser +) -> None: """Storage setup.""" hass_storage[DOMAIN] = { "key": DOMAIN, diff --git a/tests/components/plex/conftest.py b/tests/components/plex/conftest.py index d00b8eb944b..480573216bc 100644 --- a/tests/components/plex/conftest.py +++ b/tests/components/plex/conftest.py @@ -4,6 +4,7 @@ from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest +import requests_mock from homeassistant.components.plex.const import DOMAIN, PLEX_SERVER_CONFIG, SERVERS from homeassistant.const import CONF_URL @@ -436,7 +437,7 @@ def mock_websocket(): @pytest.fixture def mock_plex_calls( entry, - requests_mock, + requests_mock: requests_mock.Mocker, children_20, children_30, children_200, @@ -550,7 +551,7 @@ def setup_plex_server( livetv_sessions, mock_websocket, mock_plex_calls, - requests_mock, + requests_mock: requests_mock.Mocker, empty_payload, session_default, session_live_tv, From 1317837986e34d6b7463ac02bc857adc8e9ab98f Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 30 May 2024 08:48:02 +0200 Subject: [PATCH 0018/1445] Improve type hints in tests (q-z) (#118381) --- tests/components/recorder/test_init.py | 2 +- tests/components/recorder/test_util.py | 6 +++--- .../components/repairs/test_websocket_api.py | 8 +++++-- tests/components/schedule/test_init.py | 15 ++++++------- tests/components/tag/test_event.py | 6 ++++-- tests/components/tag/test_init.py | 3 ++- tests/components/tag/test_trigger.py | 4 +++- tests/components/timer/test_init.py | 3 ++- tests/components/tod/test_binary_sensor.py | 12 ++++++++--- tests/components/trace/test_websocket_api.py | 21 +++++++++++-------- tests/components/unifiprotect/test_migrate.py | 11 ++++++---- tests/components/upcloud/test_config_flow.py | 4 +++- tests/components/zha/conftest.py | 2 +- tests/components/zha/test_select.py | 3 ++- tests/components/zha/test_sensor.py | 3 ++- tests/components/zha/test_websocket_api.py | 8 ++++++- tests/components/zone/test_init.py | 3 ++- 17 files changed, 74 insertions(+), 40 deletions(-) diff --git a/tests/components/recorder/test_init.py b/tests/components/recorder/test_init.py index 207f74bc01c..fb43799b4a3 100644 --- a/tests/components/recorder/test_init.py +++ b/tests/components/recorder/test_init.py @@ -1262,7 +1262,7 @@ async def test_auto_purge_disabled( async def test_auto_statistics( hass: HomeAssistant, setup_recorder: None, - freezer, + freezer: FrozenDateTimeFactory, ) -> None: """Test periodic statistics scheduling.""" timezone = "Europe/Copenhagen" diff --git a/tests/components/recorder/test_util.py b/tests/components/recorder/test_util.py index f9682fac3a6..974e401264e 100644 --- a/tests/components/recorder/test_util.py +++ b/tests/components/recorder/test_util.py @@ -721,7 +721,7 @@ async def test_no_issue_for_mariadb_with_MDEV_25020( async def test_basic_sanity_check( - hass: HomeAssistant, setup_recorder: None, recorder_db_url + hass: HomeAssistant, setup_recorder: None, recorder_db_url: str ) -> None: """Test the basic sanity checks with a missing table.""" if recorder_db_url.startswith(("mysql://", "postgresql://")): @@ -742,7 +742,7 @@ async def test_combined_checks( hass: HomeAssistant, setup_recorder: None, caplog: pytest.LogCaptureFixture, - recorder_db_url, + recorder_db_url: str, ) -> None: """Run Checks on the open database.""" if recorder_db_url.startswith(("mysql://", "postgresql://")): @@ -831,7 +831,7 @@ async def test_end_incomplete_runs( async def test_periodic_db_cleanups( - hass: HomeAssistant, setup_recorder: None, recorder_db_url + hass: HomeAssistant, setup_recorder: None, recorder_db_url: str ) -> None: """Test periodic db cleanups.""" if recorder_db_url.startswith(("mysql://", "postgresql://")): diff --git a/tests/components/repairs/test_websocket_api.py b/tests/components/repairs/test_websocket_api.py index 846b25ae8c2..60d0364b985 100644 --- a/tests/components/repairs/test_websocket_api.py +++ b/tests/components/repairs/test_websocket_api.py @@ -432,7 +432,9 @@ async def test_step_unauth( @pytest.mark.freeze_time("2022-07-19 07:53:05") async def test_list_issues( - hass: HomeAssistant, hass_storage: dict[str, Any], hass_ws_client + hass: HomeAssistant, + hass_storage: dict[str, Any], + hass_ws_client: WebSocketGenerator, ) -> None: """Test we can list issues.""" @@ -581,7 +583,9 @@ async def test_fix_issue_aborted( @pytest.mark.freeze_time("2022-07-19 07:53:05") -async def test_get_issue_data(hass: HomeAssistant, hass_ws_client) -> None: +async def test_get_issue_data( + hass: HomeAssistant, hass_ws_client: WebSocketGenerator +) -> None: """Test we can get issue data.""" assert await async_setup_component(hass, DOMAIN, {}) diff --git a/tests/components/schedule/test_init.py b/tests/components/schedule/test_init.py index a7e8449c845..c43b2500ccb 100644 --- a/tests/components/schedule/test_init.py +++ b/tests/components/schedule/test_init.py @@ -6,6 +6,7 @@ from collections.abc import Callable, Coroutine from typing import Any from unittest.mock import patch +from freezegun.api import FrozenDateTimeFactory import pytest from homeassistant.components.schedule import STORAGE_VERSION, STORAGE_VERSION_MINOR @@ -181,7 +182,7 @@ async def test_events_one_day( hass: HomeAssistant, schedule_setup: Callable[..., Coroutine[Any, Any, bool]], caplog: pytest.LogCaptureFixture, - freezer, + freezer: FrozenDateTimeFactory, ) -> None: """Test events only during one day of the week.""" freezer.move_to("2022-08-30 13:20:00-07:00") @@ -225,7 +226,7 @@ async def test_adjacent_cross_midnight( hass: HomeAssistant, schedule_setup: Callable[..., Coroutine[Any, Any, bool]], caplog: pytest.LogCaptureFixture, - freezer, + freezer: FrozenDateTimeFactory, ) -> None: """Test adjacent events don't toggle on->off->on.""" freezer.move_to("2022-08-30 13:20:00-07:00") @@ -286,7 +287,7 @@ async def test_adjacent_within_day( hass: HomeAssistant, schedule_setup: Callable[..., Coroutine[Any, Any, bool]], caplog: pytest.LogCaptureFixture, - freezer, + freezer: FrozenDateTimeFactory, ) -> None: """Test adjacent events don't toggle on->off->on.""" freezer.move_to("2022-08-30 13:20:00-07:00") @@ -349,7 +350,7 @@ async def test_non_adjacent_within_day( hass: HomeAssistant, schedule_setup: Callable[..., Coroutine[Any, Any, bool]], caplog: pytest.LogCaptureFixture, - freezer, + freezer: FrozenDateTimeFactory, ) -> None: """Test adjacent events don't toggle on->off->on.""" freezer.move_to("2022-08-30 13:20:00-07:00") @@ -429,7 +430,7 @@ async def test_to_midnight( schedule_setup: Callable[..., Coroutine[Any, Any, bool]], caplog: pytest.LogCaptureFixture, schedule: list[dict[str, str]], - freezer, + freezer: FrozenDateTimeFactory, ) -> None: """Test time range allow to 24:00.""" freezer.move_to("2022-08-30 13:20:00-07:00") @@ -516,7 +517,7 @@ async def test_load( async def test_schedule_updates( hass: HomeAssistant, schedule_setup: Callable[..., Coroutine[Any, Any, bool]], - freezer, + freezer: FrozenDateTimeFactory, ) -> None: """Test the schedule updates when time changes.""" freezer.move_to("2022-08-10 20:10:00-07:00") @@ -678,7 +679,7 @@ async def test_ws_create( hass_ws_client: WebSocketGenerator, entity_registry: er.EntityRegistry, schedule_setup: Callable[..., Coroutine[Any, Any, bool]], - freezer, + freezer: FrozenDateTimeFactory, to: str, next_event: str, saved_to: str, diff --git a/tests/components/tag/test_event.py b/tests/components/tag/test_event.py index d3dc7f73058..e0a10455d1e 100644 --- a/tests/components/tag/test_event.py +++ b/tests/components/tag/test_event.py @@ -1,5 +1,7 @@ """Tests for the tag component.""" +from typing import Any + from freezegun.api import FrozenDateTimeFactory import pytest @@ -18,7 +20,7 @@ from tests.typing import WebSocketGenerator @pytest.fixture def storage_setup_named_tag( hass: HomeAssistant, - hass_storage, + hass_storage: dict[str, Any], ): """Storage setup for test case of named tags.""" @@ -76,7 +78,7 @@ async def test_named_tag_scanned_event( @pytest.fixture -def storage_setup_unnamed_tag(hass: HomeAssistant, hass_storage): +def storage_setup_unnamed_tag(hass: HomeAssistant, hass_storage: dict[str, Any]): """Storage setup for test case of unnamed tags.""" async def _storage(items=None): diff --git a/tests/components/tag/test_init.py b/tests/components/tag/test_init.py index 914719c8c1a..4767cc40fdf 100644 --- a/tests/components/tag/test_init.py +++ b/tests/components/tag/test_init.py @@ -1,6 +1,7 @@ """Tests for the tag component.""" import logging +from typing import Any from freezegun.api import FrozenDateTimeFactory import pytest @@ -20,7 +21,7 @@ from tests.typing import WebSocketGenerator @pytest.fixture -def storage_setup(hass: HomeAssistant, hass_storage): +def storage_setup(hass: HomeAssistant, hass_storage: dict[str, Any]): """Storage setup.""" async def _storage(items=None): diff --git a/tests/components/tag/test_trigger.py b/tests/components/tag/test_trigger.py index 613b5585670..60d45abb7b9 100644 --- a/tests/components/tag/test_trigger.py +++ b/tests/components/tag/test_trigger.py @@ -1,5 +1,7 @@ """Tests for tag triggers.""" +from typing import Any + import pytest from homeassistant.components import automation @@ -18,7 +20,7 @@ def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None: @pytest.fixture -def tag_setup(hass: HomeAssistant, hass_storage): +def tag_setup(hass: HomeAssistant, hass_storage: dict[str, Any]): """Tag setup.""" async def _storage(items=None): diff --git a/tests/components/timer/test_init.py b/tests/components/timer/test_init.py index 0ac3eea3b8c..854ba10fe9f 100644 --- a/tests/components/timer/test_init.py +++ b/tests/components/timer/test_init.py @@ -2,6 +2,7 @@ from datetime import timedelta import logging +from typing import Any from unittest.mock import patch import pytest @@ -59,7 +60,7 @@ _LOGGER = logging.getLogger(__name__) @pytest.fixture -def storage_setup(hass: HomeAssistant, hass_storage): +def storage_setup(hass: HomeAssistant, hass_storage: dict[str, Any]): """Storage setup.""" async def _storage(items=None, config=None): diff --git a/tests/components/tod/test_binary_sensor.py b/tests/components/tod/test_binary_sensor.py index c3e13c089c5..c4b28b527cb 100644 --- a/tests/components/tod/test_binary_sensor.py +++ b/tests/components/tod/test_binary_sensor.py @@ -658,7 +658,9 @@ async def test_dst1( assert state.state == STATE_OFF -async def test_dst2(hass, freezer, hass_tz_info): +async def test_dst2( + hass: HomeAssistant, freezer: FrozenDateTimeFactory, hass_tz_info +) -> None: """Test DST when there's a time switch in the East.""" hass.config.time_zone = "CET" dt_util.set_default_time_zone(dt_util.get_time_zone("CET")) @@ -684,7 +686,9 @@ async def test_dst2(hass, freezer, hass_tz_info): assert state.state == STATE_OFF -async def test_dst3(hass, freezer, hass_tz_info): +async def test_dst3( + hass: HomeAssistant, freezer: FrozenDateTimeFactory, hass_tz_info +) -> None: """Test DST when there's a time switch forward in the West.""" hass.config.time_zone = "US/Pacific" dt_util.set_default_time_zone(dt_util.get_time_zone("US/Pacific")) @@ -712,7 +716,9 @@ async def test_dst3(hass, freezer, hass_tz_info): assert state.state == STATE_OFF -async def test_dst4(hass, freezer, hass_tz_info): +async def test_dst4( + hass: HomeAssistant, freezer: FrozenDateTimeFactory, hass_tz_info +) -> None: """Test DST when there's a time switch backward in the West.""" hass.config.time_zone = "US/Pacific" dt_util.set_default_time_zone(dt_util.get_time_zone("US/Pacific")) diff --git a/tests/components/trace/test_websocket_api.py b/tests/components/trace/test_websocket_api.py index f2cfb6a109f..91e651ba6e3 100644 --- a/tests/components/trace/test_websocket_api.py +++ b/tests/components/trace/test_websocket_api.py @@ -122,7 +122,7 @@ async def _assert_contexts(client, next_id, contexts, domain=None, item_id=None) async def test_get_trace( hass: HomeAssistant, hass_storage: dict[str, Any], - hass_ws_client, + hass_ws_client: WebSocketGenerator, domain, prefix, extra_trace_keys, @@ -425,7 +425,10 @@ async def test_get_trace( @pytest.mark.parametrize("domain", ["automation", "script"]) async def test_restore_traces( - hass: HomeAssistant, hass_storage: dict[str, Any], hass_ws_client, domain + hass: HomeAssistant, + hass_storage: dict[str, Any], + hass_ws_client: WebSocketGenerator, + domain: str, ) -> None: """Test restored traces.""" hass.set_state(CoreState.not_running) @@ -595,9 +598,9 @@ async def test_trace_overflow( async def test_restore_traces_overflow( hass: HomeAssistant, hass_storage: dict[str, Any], - hass_ws_client, - domain, - num_restored_moon_traces, + hass_ws_client: WebSocketGenerator, + domain: str, + num_restored_moon_traces: int, ) -> None: """Test restored traces are evicted first.""" hass.set_state(CoreState.not_running) @@ -675,10 +678,10 @@ async def test_restore_traces_overflow( async def test_restore_traces_late_overflow( hass: HomeAssistant, hass_storage: dict[str, Any], - hass_ws_client, - domain, - num_restored_moon_traces, - restored_run_id, + hass_ws_client: WebSocketGenerator, + domain: str, + num_restored_moon_traces: int, + restored_run_id: str, ) -> None: """Test restored traces are evicted first.""" hass.set_state(CoreState.not_running) diff --git a/tests/components/unifiprotect/test_migrate.py b/tests/components/unifiprotect/test_migrate.py index a48925d9c67..8fdf113f9db 100644 --- a/tests/components/unifiprotect/test_migrate.py +++ b/tests/components/unifiprotect/test_migrate.py @@ -23,8 +23,11 @@ from tests.typing import WebSocketGenerator async def test_deprecated_entity( - hass: HomeAssistant, ufp: MockUFPFixture, hass_ws_client, doorbell: Camera -): + hass: HomeAssistant, + ufp: MockUFPFixture, + hass_ws_client: WebSocketGenerator, + doorbell: Camera, +) -> None: """Test Deprecate entity repair does not exist by default (new installs).""" await init_entry(hass, ufp, [doorbell]) @@ -47,9 +50,9 @@ async def test_deprecated_entity_no_automations( hass: HomeAssistant, entity_registry: er.EntityRegistry, ufp: MockUFPFixture, - hass_ws_client, + hass_ws_client: WebSocketGenerator, doorbell: Camera, -): +) -> None: """Test Deprecate entity repair exists for existing installs.""" entity_registry.async_get_or_create( Platform.SWITCH, diff --git a/tests/components/upcloud/test_config_flow.py b/tests/components/upcloud/test_config_flow.py index 4ce87bf38ab..51ee8875ec3 100644 --- a/tests/components/upcloud/test_config_flow.py +++ b/tests/components/upcloud/test_config_flow.py @@ -110,7 +110,9 @@ async def test_options(hass: HomeAssistant) -> None: ) -async def test_already_configured(hass: HomeAssistant, requests_mock) -> None: +async def test_already_configured( + hass: HomeAssistant, requests_mock: requests_mock.Mocker +) -> None: """Test duplicate entry aborts and updates data.""" config_entry = MockConfigEntry( diff --git a/tests/components/zha/conftest.py b/tests/components/zha/conftest.py index 54440a0f75b..d9f335769ec 100644 --- a/tests/components/zha/conftest.py +++ b/tests/components/zha/conftest.py @@ -519,7 +519,7 @@ def network_backup() -> zigpy.backups.NetworkBackup: @pytest.fixture -def core_rs(hass_storage): +def core_rs(hass_storage: dict[str, Any]): """Core.restore_state fixture.""" def _storage(entity_id, state, attributes={}): diff --git a/tests/components/zha/test_select.py b/tests/components/zha/test_select.py index b08e077c11d..70f58ee4e6d 100644 --- a/tests/components/zha/test_select.py +++ b/tests/components/zha/test_select.py @@ -1,5 +1,6 @@ """Test ZHA select entities.""" +from typing import Any from unittest.mock import call, patch import pytest @@ -90,7 +91,7 @@ async def light(hass, zigpy_device_mock): @pytest.fixture -def core_rs(hass_storage): +def core_rs(hass_storage: dict[str, Any]): """Core.restore_state fixture.""" def _storage(entity_id, state): diff --git a/tests/components/zha/test_sensor.py b/tests/components/zha/test_sensor.py index 59da8332b27..8a9c59c587c 100644 --- a/tests/components/zha/test_sensor.py +++ b/tests/components/zha/test_sensor.py @@ -2,6 +2,7 @@ from datetime import timedelta import math +from typing import Any from unittest.mock import MagicMock, patch import pytest @@ -646,7 +647,7 @@ def hass_ms(hass: HomeAssistant): @pytest.fixture -def core_rs(hass_storage): +def core_rs(hass_storage: dict[str, Any]): """Core.restore_state fixture.""" def _storage(entity_id, uom, state): diff --git a/tests/components/zha/test_websocket_api.py b/tests/components/zha/test_websocket_api.py index 927da4ed2c0..85d849958a4 100644 --- a/tests/components/zha/test_websocket_api.py +++ b/tests/components/zha/test_websocket_api.py @@ -64,6 +64,7 @@ from .conftest import ( from .data import BASE_CUSTOM_CONFIGURATION, CONFIG_WITH_ALARM_OPTIONS from tests.common import MockConfigEntry, MockUser +from tests.typing import MockHAClientWebSocket, WebSocketGenerator IEEE_SWITCH_DEVICE = "01:2d:6f:00:0a:90:69:e7" IEEE_GROUPABLE_DEVICE = "01:2d:6f:00:0a:90:69:e8" @@ -151,7 +152,12 @@ async def device_groupable(hass, zigpy_device_mock, zha_device_joined): @pytest.fixture -async def zha_client(hass, hass_ws_client, device_switch, device_groupable): +async def zha_client( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + device_switch, + device_groupable, +) -> MockHAClientWebSocket: """Get ZHA WebSocket client.""" # load the ZHA API diff --git a/tests/components/zone/test_init.py b/tests/components/zone/test_init.py index fcd0c39a4f5..434ec9ccd2f 100644 --- a/tests/components/zone/test_init.py +++ b/tests/components/zone/test_init.py @@ -1,5 +1,6 @@ """Test zone component.""" +from typing import Any from unittest.mock import patch import pytest @@ -24,7 +25,7 @@ from tests.typing import WebSocketGenerator @pytest.fixture -def storage_setup(hass, hass_storage): +def storage_setup(hass: HomeAssistant, hass_storage: dict[str, Any]): """Storage setup.""" async def _storage(items=None, config=None): From baaf16e9b3f2b753f874980610f447b549d3b9c5 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 30 May 2024 08:53:42 +0200 Subject: [PATCH 0019/1445] Adjust type hint for request_mock.Mocker in pylint plugin (#118458) --- pylint/plugins/hass_enforce_type_hints.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index 99e3a4769ae..2077b865377 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -146,7 +146,7 @@ _TEST_FIXTURES: dict[str, list[str] | str] = { "mqtt_mock_entry": "MqttMockHAClientGenerator", "recorder_db_url": "str", "recorder_mock": "Recorder", - "requests_mock": "requests_mock.Mocker", + "requests_mock": "Mocker", "snapshot": "SnapshotAssertion", "socket_enabled": "None", "stub_blueprint_populate": "None", From 9221eeb2f7662e0795df27c2545edf643022bed0 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 30 May 2024 08:54:56 +0200 Subject: [PATCH 0020/1445] Add check for usefixtures decorator in pylint plugin (#118456) --- pylint/plugins/hass_enforce_type_hints.py | 12 +++++++ tests/pylint/test_enforce_type_hints.py | 39 ++++++++++++++++++++--- 2 files changed, 47 insertions(+), 4 deletions(-) diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index 2077b865377..6d3b68cbeb6 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -3113,6 +3113,12 @@ class HassTypeHintChecker(BaseChecker): "hass-return-type", "Used when method return type is incorrect", ), + "W7433": ( + "Argument %s is of type %s and could be move to " + "`@pytest.mark.usefixtures` decorator in %s", + "hass-consider-usefixtures-decorator", + "Used when an argument type is None and could be a fixture", + ), } options = ( ( @@ -3308,6 +3314,12 @@ class HassTypeHintChecker(BaseChecker): # Check that all positional arguments are correctly annotated. for arg_name, expected_type in _TEST_FIXTURES.items(): arg_node, annotation = _get_named_annotation(node, arg_name) + if arg_node and expected_type == "None": + self.add_message( + "hass-consider-usefixtures-decorator", + node=arg_node, + args=(arg_name, expected_type, node.name), + ) if arg_node and not _is_valid_type(expected_type, annotation): self.add_message( "hass-argument-type", diff --git a/tests/pylint/test_enforce_type_hints.py b/tests/pylint/test_enforce_type_hints.py index ad3b7d62be9..64dd472827e 100644 --- a/tests/pylint/test_enforce_type_hints.py +++ b/tests/pylint/test_enforce_type_hints.py @@ -1152,16 +1152,20 @@ def test_pytest_function( def test_pytest_invalid_function( linter: UnittestLinter, type_hint_checker: BaseChecker ) -> None: - """Ensure invalid hints are rejected for async_get_service.""" - func_node, hass_node, caplog_node = astroid.extract_node( - """ + """Ensure invalid hints are rejected for a test function.""" + func_node, hass_node, caplog_node, first_none_node, second_none_node = ( + astroid.extract_node( + """ async def test_sample( #@ hass: Something, #@ caplog: SomethingElse, #@ + current_request_with_host, #@ + enable_custom_integrations: None, #@ ) -> Anything: pass """, - "tests.components.pylint_test.notify", + "tests.components.pylint_test.notify", + ) ) type_hint_checker.visit_module(func_node.parent) @@ -1194,6 +1198,33 @@ def test_pytest_invalid_function( end_line=4, end_col_offset=25, ), + pylint.testutils.MessageTest( + msg_id="hass-consider-usefixtures-decorator", + node=first_none_node, + args=("current_request_with_host", "None", "test_sample"), + line=5, + col_offset=4, + end_line=5, + end_col_offset=29, + ), + pylint.testutils.MessageTest( + msg_id="hass-argument-type", + node=first_none_node, + args=("current_request_with_host", "None", "test_sample"), + line=5, + col_offset=4, + end_line=5, + end_col_offset=29, + ), + pylint.testutils.MessageTest( + msg_id="hass-consider-usefixtures-decorator", + node=second_none_node, + args=("enable_custom_integrations", "None", "test_sample"), + line=6, + col_offset=4, + end_line=6, + end_col_offset=36, + ), ): type_hint_checker.visit_asyncfunctiondef(func_node) From c6e0e93680b89c5f06c69f7ba25db97e917b9414 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 30 May 2024 09:37:01 +0200 Subject: [PATCH 0021/1445] Cleanup mock_get_source_ip from tests (#118459) --- tests/components/default_config/test_init.py | 2 +- tests/components/dlna_dmr/conftest.py | 5 -- .../emulated_roku/test_config_flow.py | 6 +-- tests/components/emulated_roku/test_init.py | 6 +-- tests/components/fritz/test_config_flow.py | 26 +++------- tests/components/homekit/test_config_flow.py | 51 ++++++------------- tests/components/homekit/test_homekit.py | 2 +- tests/components/homekit/test_init.py | 4 +- tests/components/homekit/test_util.py | 4 +- tests/components/lifx/conftest.py | 5 -- tests/components/local_ip/test_config_flow.py | 4 +- tests/components/local_ip/test_init.py | 2 +- .../motion_blinds/test_config_flow.py | 2 +- .../nmap_tracker/test_config_flow.py | 12 ++--- tests/components/qnap/conftest.py | 2 +- tests/components/reolink/conftest.py | 6 +-- tests/components/samsungtv/conftest.py | 5 -- tests/components/sonos/conftest.py | 6 --- tests/components/ssdp/test_init.py | 15 ------ tests/components/tplink/conftest.py | 5 -- tests/components/upnp/conftest.py | 1 - tests/components/upnp/test_config_flow.py | 9 ---- tests/components/upnp/test_init.py | 11 ++-- .../yamaha_musiccast/test_config_flow.py | 18 +++---- tests/components/yeelight/conftest.py | 7 --- tests/components/zeroconf/conftest.py | 8 --- 26 files changed, 53 insertions(+), 171 deletions(-) diff --git a/tests/components/default_config/test_init.py b/tests/components/default_config/test_init.py index 222b2b14673..9f8467af9db 100644 --- a/tests/components/default_config/test_init.py +++ b/tests/components/default_config/test_init.py @@ -34,7 +34,7 @@ def recorder_url_mock(): async def test_setup( - hass: HomeAssistant, mock_zeroconf: None, mock_get_source_ip, mock_bluetooth: None + hass: HomeAssistant, mock_zeroconf: None, mock_bluetooth: None ) -> None: """Test setup.""" recorder_helper.async_initialize_recorder(hass) diff --git a/tests/components/dlna_dmr/conftest.py b/tests/components/dlna_dmr/conftest.py index 59b1af546f2..0d88009f58e 100644 --- a/tests/components/dlna_dmr/conftest.py +++ b/tests/components/dlna_dmr/conftest.py @@ -158,8 +158,3 @@ def async_get_local_ip_mock() -> Iterable[Mock]: ) as func: func.return_value = AddressFamily.AF_INET, LOCAL_IP yield func - - -@pytest.fixture(autouse=True) -def dlna_dmr_mock_get_source_ip(mock_get_source_ip): - """Mock network util's async_get_source_ip.""" diff --git a/tests/components/emulated_roku/test_config_flow.py b/tests/components/emulated_roku/test_config_flow.py index 45cb83b4fea..0b0efb83967 100644 --- a/tests/components/emulated_roku/test_config_flow.py +++ b/tests/components/emulated_roku/test_config_flow.py @@ -8,7 +8,7 @@ from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry -async def test_flow_works(hass: HomeAssistant, mock_get_source_ip) -> None: +async def test_flow_works(hass: HomeAssistant) -> None: """Test that config flow works.""" result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, @@ -21,9 +21,7 @@ async def test_flow_works(hass: HomeAssistant, mock_get_source_ip) -> None: assert result["data"] == {"name": "Emulated Roku Test", "listen_port": 8060} -async def test_flow_already_registered_entry( - hass: HomeAssistant, mock_get_source_ip -) -> None: +async def test_flow_already_registered_entry(hass: HomeAssistant) -> None: """Test that config flow doesn't allow existing names.""" MockConfigEntry( domain="emulated_roku", data={"name": "Emulated Roku Test", "listen_port": 8062} diff --git a/tests/components/emulated_roku/test_init.py b/tests/components/emulated_roku/test_init.py index 00316c66425..cf2a415f19c 100644 --- a/tests/components/emulated_roku/test_init.py +++ b/tests/components/emulated_roku/test_init.py @@ -7,7 +7,7 @@ from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component -async def test_config_required_fields(hass: HomeAssistant, mock_get_source_ip) -> None: +async def test_config_required_fields(hass: HomeAssistant) -> None: """Test that configuration is successful with required fields.""" with ( patch.object(emulated_roku, "configured_servers", return_value=[]), @@ -35,9 +35,7 @@ async def test_config_required_fields(hass: HomeAssistant, mock_get_source_ip) - ) -async def test_config_already_registered_not_configured( - hass: HomeAssistant, mock_get_source_ip -) -> None: +async def test_config_already_registered_not_configured(hass: HomeAssistant) -> None: """Test that an already registered name causes the entry to be ignored.""" with ( patch( diff --git a/tests/components/fritz/test_config_flow.py b/tests/components/fritz/test_config_flow.py index f13575cf507..a54acbb0ac0 100644 --- a/tests/components/fritz/test_config_flow.py +++ b/tests/components/fritz/test_config_flow.py @@ -93,7 +93,6 @@ from tests.common import MockConfigEntry async def test_user( hass: HomeAssistant, fc_class_mock, - mock_get_source_ip, show_advanced_options: bool, user_input: dict, expected_config: dict, @@ -156,7 +155,6 @@ async def test_user( async def test_user_already_configured( hass: HomeAssistant, fc_class_mock, - mock_get_source_ip, show_advanced_options: bool, user_input, ) -> None: @@ -218,7 +216,6 @@ async def test_user_already_configured( ) async def test_exception_security( hass: HomeAssistant, - mock_get_source_ip, error, show_advanced_options: bool, user_input, @@ -251,7 +248,6 @@ async def test_exception_security( ) async def test_exception_connection( hass: HomeAssistant, - mock_get_source_ip, show_advanced_options: bool, user_input, ) -> None: @@ -282,7 +278,7 @@ async def test_exception_connection( [(True, MOCK_USER_INPUT_ADVANCED), (False, MOCK_USER_INPUT_SIMPLE)], ) async def test_exception_unknown( - hass: HomeAssistant, mock_get_source_ip, show_advanced_options: bool, user_input + hass: HomeAssistant, show_advanced_options: bool, user_input ) -> None: """Test starting a flow by user with an unknown exception.""" @@ -309,7 +305,6 @@ async def test_exception_unknown( async def test_reauth_successful( hass: HomeAssistant, fc_class_mock, - mock_get_source_ip, ) -> None: """Test starting a reauthentication flow.""" @@ -374,7 +369,6 @@ async def test_reauth_successful( async def test_reauth_not_successful( hass: HomeAssistant, fc_class_mock, - mock_get_source_ip, side_effect, error, ) -> None: @@ -442,7 +436,6 @@ async def test_reauth_not_successful( async def test_reconfigure_successful( hass: HomeAssistant, fc_class_mock, - mock_get_source_ip, show_advanced_options: bool, user_input: dict, expected_config: dict, @@ -508,7 +501,6 @@ async def test_reconfigure_successful( async def test_reconfigure_not_successful( hass: HomeAssistant, fc_class_mock, - mock_get_source_ip, ) -> None: """Test starting a reconfigure flow but no connection found.""" @@ -579,9 +571,7 @@ async def test_reconfigure_not_successful( } -async def test_ssdp_already_configured( - hass: HomeAssistant, fc_class_mock, mock_get_source_ip -) -> None: +async def test_ssdp_already_configured(hass: HomeAssistant, fc_class_mock) -> None: """Test starting a flow from discovery with an already configured device.""" mock_config = MockConfigEntry( @@ -608,9 +598,7 @@ async def test_ssdp_already_configured( assert result["reason"] == "already_configured" -async def test_ssdp_already_configured_host( - hass: HomeAssistant, fc_class_mock, mock_get_source_ip -) -> None: +async def test_ssdp_already_configured_host(hass: HomeAssistant, fc_class_mock) -> None: """Test starting a flow from discovery with an already configured host.""" mock_config = MockConfigEntry( @@ -638,7 +626,7 @@ async def test_ssdp_already_configured_host( async def test_ssdp_already_configured_host_uuid( - hass: HomeAssistant, fc_class_mock, mock_get_source_ip + hass: HomeAssistant, fc_class_mock ) -> None: """Test starting a flow from discovery with an already configured uuid.""" @@ -667,7 +655,7 @@ async def test_ssdp_already_configured_host_uuid( async def test_ssdp_already_in_progress_host( - hass: HomeAssistant, fc_class_mock, mock_get_source_ip + hass: HomeAssistant, fc_class_mock ) -> None: """Test starting a flow from discovery twice.""" with patch( @@ -690,7 +678,7 @@ async def test_ssdp_already_in_progress_host( assert result["reason"] == "already_in_progress" -async def test_ssdp(hass: HomeAssistant, fc_class_mock, mock_get_source_ip) -> None: +async def test_ssdp(hass: HomeAssistant, fc_class_mock) -> None: """Test starting a flow from discovery.""" with ( patch( @@ -732,7 +720,7 @@ async def test_ssdp(hass: HomeAssistant, fc_class_mock, mock_get_source_ip) -> N assert mock_setup_entry.called -async def test_ssdp_exception(hass: HomeAssistant, mock_get_source_ip) -> None: +async def test_ssdp_exception(hass: HomeAssistant) -> None: """Test starting a flow from discovery but no device found.""" with patch( "homeassistant.components.fritz.config_flow.FritzConnection", diff --git a/tests/components/homekit/test_config_flow.py b/tests/components/homekit/test_config_flow.py index ff47abab833..23f15bb344a 100644 --- a/tests/components/homekit/test_config_flow.py +++ b/tests/components/homekit/test_config_flow.py @@ -45,7 +45,7 @@ def _mock_config_entry_with_options_populated(): ) -async def test_setup_in_bridge_mode(hass: HomeAssistant, mock_get_source_ip) -> None: +async def test_setup_in_bridge_mode(hass: HomeAssistant) -> None: """Test we can setup a new instance in bridge mode.""" result = await hass.config_entries.flow.async_init( @@ -99,9 +99,7 @@ async def test_setup_in_bridge_mode(hass: HomeAssistant, mock_get_source_ip) -> assert len(mock_setup_entry.mock_calls) == 1 -async def test_setup_in_bridge_mode_name_taken( - hass: HomeAssistant, mock_get_source_ip -) -> None: +async def test_setup_in_bridge_mode_name_taken(hass: HomeAssistant) -> None: """Test we can setup a new instance in bridge mode when the name is taken.""" entry = MockConfigEntry( @@ -163,7 +161,7 @@ async def test_setup_in_bridge_mode_name_taken( async def test_setup_creates_entries_for_accessory_mode_devices( - hass: HomeAssistant, mock_get_source_ip + hass: HomeAssistant, ) -> None: """Test we can setup a new instance and we create entries for accessory mode devices.""" hass.states.async_set("camera.one", "on") @@ -257,7 +255,7 @@ async def test_setup_creates_entries_for_accessory_mode_devices( assert len(mock_setup_entry.mock_calls) == 7 -async def test_import(hass: HomeAssistant, mock_get_source_ip) -> None: +async def test_import(hass: HomeAssistant) -> None: """Test we can import instance.""" ignored_entry = MockConfigEntry(domain=DOMAIN, data={}, source=SOURCE_IGNORE) @@ -302,9 +300,7 @@ async def test_import(hass: HomeAssistant, mock_get_source_ip) -> None: assert len(mock_setup_entry.mock_calls) == 2 -async def test_options_flow_exclude_mode_advanced( - hass: HomeAssistant, mock_get_source_ip -) -> None: +async def test_options_flow_exclude_mode_advanced(hass: HomeAssistant) -> None: """Test config flow options in exclude mode with advanced options.""" config_entry = _mock_config_entry_with_options_populated() @@ -357,9 +353,7 @@ async def test_options_flow_exclude_mode_advanced( } -async def test_options_flow_exclude_mode_basic( - hass: HomeAssistant, mock_get_source_ip -) -> None: +async def test_options_flow_exclude_mode_basic(hass: HomeAssistant) -> None: """Test config flow options in exclude mode.""" config_entry = _mock_config_entry_with_options_populated() @@ -417,7 +411,6 @@ async def test_options_flow_devices( demo_cleanup, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - mock_get_source_ip, mock_async_zeroconf: None, ) -> None: """Test devices can be bridged.""" @@ -510,7 +503,7 @@ async def test_options_flow_devices( @patch(f"{PATH_HOMEKIT}.async_port_is_available", return_value=True) async def test_options_flow_devices_preserved_when_advanced_off( - port_mock, hass: HomeAssistant, mock_get_source_ip, mock_async_zeroconf: None + port_mock, hass: HomeAssistant, mock_async_zeroconf: None ) -> None: """Test devices are preserved if they were added in advanced mode but it was turned off.""" config_entry = MockConfigEntry( @@ -586,7 +579,7 @@ async def test_options_flow_devices_preserved_when_advanced_off( async def test_options_flow_include_mode_with_non_existant_entity( - hass: HomeAssistant, mock_get_source_ip + hass: HomeAssistant, ) -> None: """Test config flow options in include mode with a non-existent entity.""" config_entry = MockConfigEntry( @@ -646,7 +639,7 @@ async def test_options_flow_include_mode_with_non_existant_entity( async def test_options_flow_exclude_mode_with_non_existant_entity( - hass: HomeAssistant, mock_get_source_ip + hass: HomeAssistant, ) -> None: """Test config flow options in exclude mode with a non-existent entity.""" config_entry = MockConfigEntry( @@ -706,9 +699,7 @@ async def test_options_flow_exclude_mode_with_non_existant_entity( await hass.config_entries.async_unload(config_entry.entry_id) -async def test_options_flow_include_mode_basic( - hass: HomeAssistant, mock_get_source_ip -) -> None: +async def test_options_flow_include_mode_basic(hass: HomeAssistant) -> None: """Test config flow options in include mode.""" config_entry = _mock_config_entry_with_options_populated() @@ -754,9 +745,7 @@ async def test_options_flow_include_mode_basic( await hass.config_entries.async_unload(config_entry.entry_id) -async def test_options_flow_exclude_mode_with_cameras( - hass: HomeAssistant, mock_get_source_ip -) -> None: +async def test_options_flow_exclude_mode_with_cameras(hass: HomeAssistant) -> None: """Test config flow options in exclude mode with cameras.""" config_entry = _mock_config_entry_with_options_populated() @@ -863,9 +852,7 @@ async def test_options_flow_exclude_mode_with_cameras( await hass.config_entries.async_unload(config_entry.entry_id) -async def test_options_flow_include_mode_with_cameras( - hass: HomeAssistant, mock_get_source_ip -) -> None: +async def test_options_flow_include_mode_with_cameras(hass: HomeAssistant) -> None: """Test config flow options in include mode with cameras.""" config_entry = _mock_config_entry_with_options_populated() @@ -999,9 +986,7 @@ async def test_options_flow_include_mode_with_cameras( await hass.config_entries.async_unload(config_entry.entry_id) -async def test_options_flow_with_camera_audio( - hass: HomeAssistant, mock_get_source_ip -) -> None: +async def test_options_flow_with_camera_audio(hass: HomeAssistant) -> None: """Test config flow options with cameras that support audio.""" config_entry = _mock_config_entry_with_options_populated() @@ -1135,9 +1120,7 @@ async def test_options_flow_with_camera_audio( await hass.config_entries.async_unload(config_entry.entry_id) -async def test_options_flow_blocked_when_from_yaml( - hass: HomeAssistant, mock_get_source_ip -) -> None: +async def test_options_flow_blocked_when_from_yaml(hass: HomeAssistant) -> None: """Test config flow options.""" config_entry = MockConfigEntry( @@ -1181,7 +1164,6 @@ async def test_options_flow_blocked_when_from_yaml( async def test_options_flow_include_mode_basic_accessory( port_mock, hass: HomeAssistant, - mock_get_source_ip, hk_driver, mock_async_zeroconf: None, ) -> None: @@ -1283,7 +1265,7 @@ async def test_options_flow_include_mode_basic_accessory( async def test_converting_bridge_to_accessory_mode( - hass: HomeAssistant, hk_driver, mock_get_source_ip + hass: HomeAssistant, hk_driver ) -> None: """Test we can convert a bridge to accessory mode.""" @@ -1408,7 +1390,6 @@ def _get_schema_default(schema, key_name): async def test_options_flow_exclude_mode_skips_category_entities( port_mock, hass: HomeAssistant, - mock_get_source_ip, hk_driver, mock_async_zeroconf: None, entity_registry: er.EntityRegistry, @@ -1513,7 +1494,6 @@ async def test_options_flow_exclude_mode_skips_category_entities( async def test_options_flow_exclude_mode_skips_hidden_entities( port_mock, hass: HomeAssistant, - mock_get_source_ip, hk_driver, mock_async_zeroconf: None, entity_registry: er.EntityRegistry, @@ -1598,7 +1578,6 @@ async def test_options_flow_exclude_mode_skips_hidden_entities( async def test_options_flow_include_mode_allows_hidden_entities( port_mock, hass: HomeAssistant, - mock_get_source_ip, hk_driver, mock_async_zeroconf: None, entity_registry: er.EntityRegistry, diff --git a/tests/components/homekit/test_homekit.py b/tests/components/homekit/test_homekit.py index e0f0786f15d..77931bb74f4 100644 --- a/tests/components/homekit/test_homekit.py +++ b/tests/components/homekit/test_homekit.py @@ -111,7 +111,7 @@ def always_patch_driver(hk_driver): @pytest.fixture(autouse=True) -def patch_source_ip(mock_get_source_ip): +def patch_source_ip(): """Patch homeassistant and pyhap functions for getting local address.""" with patch("pyhap.util.get_local_address", return_value="10.10.10.10"): yield diff --git a/tests/components/homekit/test_init.py b/tests/components/homekit/test_init.py index 7e924be1637..2b251c7858d 100644 --- a/tests/components/homekit/test_init.py +++ b/tests/components/homekit/test_init.py @@ -26,9 +26,7 @@ from tests.common import MockConfigEntry from tests.components.logbook.common import MockRow, mock_humanify -async def test_humanify_homekit_changed_event( - hass: HomeAssistant, hk_driver, mock_get_source_ip -) -> None: +async def test_humanify_homekit_changed_event(hass: HomeAssistant, hk_driver) -> None: """Test humanifying HomeKit changed event.""" hass.config.components.add("recorder") with patch("homeassistant.components.homekit.HomeKit") as mock_homekit: diff --git a/tests/components/homekit/test_util.py b/tests/components/homekit/test_util.py index a7b9dae416e..24999242dc1 100644 --- a/tests/components/homekit/test_util.py +++ b/tests/components/homekit/test_util.py @@ -241,9 +241,7 @@ def test_density_to_air_quality() -> None: assert density_to_air_quality(200) == 5 -async def test_async_show_setup_msg( - hass: HomeAssistant, hk_driver, mock_get_source_ip -) -> None: +async def test_async_show_setup_msg(hass: HomeAssistant, hk_driver) -> None: """Test show setup message as persistence notification.""" pincode = b"123-45-678" diff --git a/tests/components/lifx/conftest.py b/tests/components/lifx/conftest.py index c126ca20ecd..093f2309e53 100644 --- a/tests/components/lifx/conftest.py +++ b/tests/components/lifx/conftest.py @@ -41,11 +41,6 @@ def mock_effect_conductor(): yield mock_conductor -@pytest.fixture(autouse=True) -def lifx_mock_get_source_ip(mock_get_source_ip): - """Mock network util's async_get_source_ip.""" - - @pytest.fixture(autouse=True) def lifx_no_wait_for_timeouts(): """Avoid waiting for timeouts in tests.""" diff --git a/tests/components/local_ip/test_config_flow.py b/tests/components/local_ip/test_config_flow.py index 554163bbc1c..3f9233f5b97 100644 --- a/tests/components/local_ip/test_config_flow.py +++ b/tests/components/local_ip/test_config_flow.py @@ -10,7 +10,7 @@ from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry -async def test_config_flow(hass: HomeAssistant, mock_get_source_ip) -> None: +async def test_config_flow(hass: HomeAssistant) -> None: """Test we can finish a config flow.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} @@ -25,7 +25,7 @@ async def test_config_flow(hass: HomeAssistant, mock_get_source_ip) -> None: assert state -async def test_already_setup(hass: HomeAssistant, mock_get_source_ip) -> None: +async def test_already_setup(hass: HomeAssistant) -> None: """Test we abort if already setup.""" MockConfigEntry( domain=DOMAIN, diff --git a/tests/components/local_ip/test_init.py b/tests/components/local_ip/test_init.py index cc4f4dd4968..51e0628a417 100644 --- a/tests/components/local_ip/test_init.py +++ b/tests/components/local_ip/test_init.py @@ -10,7 +10,7 @@ from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry -async def test_basic_setup(hass: HomeAssistant, mock_get_source_ip) -> None: +async def test_basic_setup(hass: HomeAssistant) -> None: """Test component setup creates entry from config.""" entry = MockConfigEntry(domain=DOMAIN, data={}) entry.add_to_hass(hass) diff --git a/tests/components/motion_blinds/test_config_flow.py b/tests/components/motion_blinds/test_config_flow.py index 4168c3a1f63..77171b06ad6 100644 --- a/tests/components/motion_blinds/test_config_flow.py +++ b/tests/components/motion_blinds/test_config_flow.py @@ -72,7 +72,7 @@ TEST_INTERFACES = [ @pytest.fixture(name="motion_blinds_connect", autouse=True) -def motion_blinds_connect_fixture(mock_get_source_ip): +def motion_blinds_connect_fixture(): """Mock Motionblinds connection and entry setup.""" with ( patch( diff --git a/tests/components/nmap_tracker/test_config_flow.py b/tests/components/nmap_tracker/test_config_flow.py index 2e12c53a759..5c0548c4158 100644 --- a/tests/components/nmap_tracker/test_config_flow.py +++ b/tests/components/nmap_tracker/test_config_flow.py @@ -25,7 +25,7 @@ from tests.common import MockConfigEntry @pytest.mark.parametrize( "hosts", ["1.1.1.1", "192.168.1.0/24", "192.168.1.0/24,192.168.2.0/24"] ) -async def test_form(hass: HomeAssistant, hosts: str, mock_get_source_ip) -> None: +async def test_form(hass: HomeAssistant, hosts: str) -> None: """Test we get the form.""" result = await hass.config_entries.flow.async_init( @@ -64,7 +64,7 @@ async def test_form(hass: HomeAssistant, hosts: str, mock_get_source_ip) -> None assert len(mock_setup_entry.mock_calls) == 1 -async def test_form_range(hass: HomeAssistant, mock_get_source_ip) -> None: +async def test_form_range(hass: HomeAssistant) -> None: """Test we get the form and can take an ip range.""" result = await hass.config_entries.flow.async_init( @@ -100,7 +100,7 @@ async def test_form_range(hass: HomeAssistant, mock_get_source_ip) -> None: assert len(mock_setup_entry.mock_calls) == 1 -async def test_form_invalid_hosts(hass: HomeAssistant, mock_get_source_ip) -> None: +async def test_form_invalid_hosts(hass: HomeAssistant) -> None: """Test invalid hosts passed in.""" result = await hass.config_entries.flow.async_init( @@ -124,7 +124,7 @@ async def test_form_invalid_hosts(hass: HomeAssistant, mock_get_source_ip) -> No assert result2["errors"] == {CONF_HOSTS: "invalid_hosts"} -async def test_form_already_configured(hass: HomeAssistant, mock_get_source_ip) -> None: +async def test_form_already_configured(hass: HomeAssistant) -> None: """Test duplicate host list.""" config_entry = MockConfigEntry( @@ -159,7 +159,7 @@ async def test_form_already_configured(hass: HomeAssistant, mock_get_source_ip) assert result2["reason"] == "already_configured" -async def test_form_invalid_excludes(hass: HomeAssistant, mock_get_source_ip) -> None: +async def test_form_invalid_excludes(hass: HomeAssistant) -> None: """Test invalid excludes passed in.""" result = await hass.config_entries.flow.async_init( @@ -183,7 +183,7 @@ async def test_form_invalid_excludes(hass: HomeAssistant, mock_get_source_ip) -> assert result2["errors"] == {CONF_EXCLUDE: "invalid_hosts"} -async def test_options_flow(hass: HomeAssistant, mock_get_source_ip) -> None: +async def test_options_flow(hass: HomeAssistant) -> None: """Test we can edit options.""" config_entry = MockConfigEntry( diff --git a/tests/components/qnap/conftest.py b/tests/components/qnap/conftest.py index 5c6d5eb65fc..512ebc35159 100644 --- a/tests/components/qnap/conftest.py +++ b/tests/components/qnap/conftest.py @@ -24,7 +24,7 @@ def mock_setup_entry() -> Generator[AsyncMock, None, None]: @pytest.fixture -def qnap_connect(mock_get_source_ip: None) -> Generator[MagicMock, None, None]: +def qnap_connect() -> Generator[MagicMock, None, None]: """Mock qnap connection.""" with patch( "homeassistant.components.qnap.config_flow.QNAPStats", autospec=True diff --git a/tests/components/reolink/conftest.py b/tests/components/reolink/conftest.py index 5fd52b97b6b..ba4e9615e8c 100644 --- a/tests/components/reolink/conftest.py +++ b/tests/components/reolink/conftest.py @@ -47,9 +47,7 @@ def mock_setup_entry() -> Generator[AsyncMock, None, None]: @pytest.fixture -def reolink_connect_class( - mock_get_source_ip: None, -) -> Generator[MagicMock, None, None]: +def reolink_connect_class() -> Generator[MagicMock, None, None]: """Mock reolink connection and return both the host_mock and host_mock_class.""" with ( patch( @@ -112,7 +110,7 @@ def reolink_connect( @pytest.fixture -def reolink_platforms(mock_get_source_ip: None) -> Generator[None, None, None]: +def reolink_platforms() -> Generator[None, None, None]: """Mock reolink entry setup.""" with patch("homeassistant.components.reolink.PLATFORMS", return_value=[]): yield diff --git a/tests/components/samsungtv/conftest.py b/tests/components/samsungtv/conftest.py index 8bef7317918..c7ac8785cbe 100644 --- a/tests/components/samsungtv/conftest.py +++ b/tests/components/samsungtv/conftest.py @@ -55,11 +55,6 @@ async def silent_ssdp_scanner(hass): yield -@pytest.fixture(autouse=True) -def samsungtv_mock_get_source_ip(mock_get_source_ip): - """Mock network util's async_get_source_ip.""" - - @pytest.fixture(autouse=True) def samsungtv_mock_async_get_local_ip(): """Mock upnp util's async_get_local_ip.""" diff --git a/tests/components/sonos/conftest.py b/tests/components/sonos/conftest.py index 657813b303f..bfece59ff9c 100644 --- a/tests/components/sonos/conftest.py +++ b/tests/components/sonos/conftest.py @@ -627,12 +627,6 @@ def tv_event_fixture(soco): return SonosMockEvent(soco, soco.avTransport, variables) -@pytest.fixture(autouse=True) -def mock_get_source_ip(mock_get_source_ip): - """Mock network util's async_get_source_ip in all sonos tests.""" - return mock_get_source_ip - - @pytest.fixture(name="zgs_discovery", scope="package") def zgs_discovery_fixture(): """Load ZoneGroupState discovery payload and return it.""" diff --git a/tests/components/ssdp/test_init.py b/tests/components/ssdp/test_init.py index 5131388c4e3..d10496500d2 100644 --- a/tests/components/ssdp/test_init.py +++ b/tests/components/ssdp/test_init.py @@ -42,7 +42,6 @@ async def init_ssdp_component(hass: HomeAssistant) -> SsdpListener: "homeassistant.components.ssdp.async_get_ssdp", return_value={"mock-domain": [{"st": "mock-st"}]}, ) -@pytest.mark.usefixtures("mock_get_source_ip") async def test_ssdp_flow_dispatched_on_st( mock_get_ssdp, hass: HomeAssistant, caplog: pytest.LogCaptureFixture, mock_flow_init ) -> None: @@ -85,7 +84,6 @@ async def test_ssdp_flow_dispatched_on_st( "homeassistant.components.ssdp.async_get_ssdp", return_value={"mock-domain": [{"manufacturerURL": "mock-url"}]}, ) -@pytest.mark.usefixtures("mock_get_source_ip") async def test_ssdp_flow_dispatched_on_manufacturer_url( mock_get_ssdp, hass: HomeAssistant, caplog: pytest.LogCaptureFixture, mock_flow_init ) -> None: @@ -125,7 +123,6 @@ async def test_ssdp_flow_dispatched_on_manufacturer_url( assert "Failed to fetch ssdp data" not in caplog.text -@pytest.mark.usefixtures("mock_get_source_ip") @patch( "homeassistant.components.ssdp.async_get_ssdp", return_value={"mock-domain": [{"manufacturer": "Paulus"}]}, @@ -170,7 +167,6 @@ async def test_scan_match_upnp_devicedesc_manufacturer( } -@pytest.mark.usefixtures("mock_get_source_ip") @patch( "homeassistant.components.ssdp.async_get_ssdp", return_value={"mock-domain": [{"deviceType": "Paulus"}]}, @@ -216,7 +212,6 @@ async def test_scan_match_upnp_devicedesc_devicetype( } -@pytest.mark.usefixtures("mock_get_source_ip") @patch( "homeassistant.components.ssdp.async_get_ssdp", return_value={ @@ -260,7 +255,6 @@ async def test_scan_not_all_present( assert not mock_flow_init.mock_calls -@pytest.mark.usefixtures("mock_get_source_ip") @patch( "homeassistant.components.ssdp.async_get_ssdp", return_value={ @@ -307,7 +301,6 @@ async def test_scan_not_all_match( assert not mock_flow_init.mock_calls -@pytest.mark.usefixtures("mock_get_source_ip") @patch( "homeassistant.components.ssdp.async_get_ssdp", return_value={"mock-domain": [{"deviceType": "Paulus"}]}, @@ -383,7 +376,6 @@ async def test_flow_start_only_alive( ) -@pytest.mark.usefixtures("mock_get_source_ip") @patch( "homeassistant.components.ssdp.async_get_ssdp", return_value={}, @@ -441,7 +433,6 @@ async def test_discovery_from_advertisement_sets_ssdp_st( "homeassistant.components.ssdp.async_build_source_set", return_value={IPv4Address("192.168.1.1")}, ) -@pytest.mark.usefixtures("mock_get_source_ip") async def test_start_stop_scanner(mock_source_set, hass: HomeAssistant) -> None: """Test we start and stop the scanner.""" ssdp_listener = await init_ssdp_component(hass) @@ -463,7 +454,6 @@ async def test_start_stop_scanner(mock_source_set, hass: HomeAssistant) -> None: assert ssdp_listener.async_stop.call_count == 1 -@pytest.mark.usefixtures("mock_get_source_ip") @pytest.mark.no_fail_on_log_exception @patch("homeassistant.components.ssdp.async_get_ssdp", return_value={}) async def test_scan_with_registered_callback( @@ -559,7 +549,6 @@ async def test_scan_with_registered_callback( assert async_integration_callback_from_cache.call_count == 1 -@pytest.mark.usefixtures("mock_get_source_ip") @patch( "homeassistant.components.ssdp.async_get_ssdp", return_value={"mock-domain": [{"st": "mock-st"}]}, @@ -688,7 +677,6 @@ _ADAPTERS_WITH_MANUAL_CONFIG = [ ] -@pytest.mark.usefixtures("mock_get_source_ip") @patch( "homeassistant.components.ssdp.async_get_ssdp", return_value={ @@ -714,7 +702,6 @@ async def test_async_detect_interfaces_setting_empty_route( assert sources == {("2001:db8::", 0, 0, 1), ("192.168.1.5", 0)} -@pytest.mark.usefixtures("mock_get_source_ip") @patch( "homeassistant.components.ssdp.async_get_ssdp", return_value={ @@ -764,7 +751,6 @@ async def test_bind_failure_skips_adapter( assert sources == {("192.168.1.5", 0)} # Note no UpnpServer for IPv6 address. -@pytest.mark.usefixtures("mock_get_source_ip") @patch( "homeassistant.components.ssdp.async_get_ssdp", return_value={ @@ -800,7 +786,6 @@ async def test_ipv4_does_additional_search_for_sonos( assert ssdp_listener.async_search.call_args[1] == {} -@pytest.mark.usefixtures("mock_get_source_ip") @patch( "homeassistant.components.ssdp.async_get_ssdp", return_value={"mock-domain": [{"deviceType": "Paulus"}]}, diff --git a/tests/components/tplink/conftest.py b/tests/components/tplink/conftest.py index 7e7e6961b91..4576f97ed83 100644 --- a/tests/components/tplink/conftest.py +++ b/tests/components/tplink/conftest.py @@ -84,11 +84,6 @@ def entity_reg_fixture(hass): return mock_registry(hass) -@pytest.fixture(autouse=True) -def tplink_mock_get_source_ip(mock_get_source_ip): - """Mock network util's async_get_source_ip.""" - - @pytest.fixture def mock_setup_entry() -> Generator[AsyncMock, None, None]: """Override async_setup_entry.""" diff --git a/tests/components/upnp/conftest.py b/tests/components/upnp/conftest.py index 0959e8e31da..00e8db124f0 100644 --- a/tests/components/upnp/conftest.py +++ b/tests/components/upnp/conftest.py @@ -228,7 +228,6 @@ async def ssdp_no_discovery(): @pytest.fixture async def mock_config_entry( hass: HomeAssistant, - mock_get_source_ip, ssdp_instant_discovery, mock_igd_device: IgdDevice, mock_mac_address_from_host, diff --git a/tests/components/upnp/test_config_flow.py b/tests/components/upnp/test_config_flow.py index a4598346a51..b8a08d3f592 100644 --- a/tests/components/upnp/test_config_flow.py +++ b/tests/components/upnp/test_config_flow.py @@ -38,7 +38,6 @@ from tests.common import MockConfigEntry @pytest.mark.usefixtures( "ssdp_instant_discovery", "mock_setup_entry", - "mock_get_source_ip", "mock_mac_address_from_host", ) async def test_flow_ssdp(hass: HomeAssistant) -> None: @@ -72,7 +71,6 @@ async def test_flow_ssdp(hass: HomeAssistant) -> None: @pytest.mark.usefixtures( "ssdp_instant_discovery", "mock_setup_entry", - "mock_get_source_ip", "mock_mac_address_from_host", ) async def test_flow_ssdp_ignore(hass: HomeAssistant) -> None: @@ -104,7 +102,6 @@ async def test_flow_ssdp_ignore(hass: HomeAssistant) -> None: } -@pytest.mark.usefixtures("mock_get_source_ip") async def test_flow_ssdp_incomplete_discovery(hass: HomeAssistant) -> None: """Test config flow: incomplete discovery through ssdp.""" # Discovered via step ssdp. @@ -126,7 +123,6 @@ async def test_flow_ssdp_incomplete_discovery(hass: HomeAssistant) -> None: assert result["reason"] == "incomplete_discovery" -@pytest.mark.usefixtures("mock_get_source_ip") async def test_flow_ssdp_non_igd_device(hass: HomeAssistant) -> None: """Test config flow: incomplete discovery through ssdp.""" # Discovered via step ssdp. @@ -151,7 +147,6 @@ async def test_flow_ssdp_non_igd_device(hass: HomeAssistant) -> None: @pytest.mark.usefixtures( "ssdp_instant_discovery", "mock_setup_entry", - "mock_get_source_ip", "mock_no_mac_address_from_host", ) async def test_flow_ssdp_no_mac_address(hass: HomeAssistant) -> None: @@ -249,7 +244,6 @@ async def test_flow_ssdp_discovery_changed_udn_match_host(hass: HomeAssistant) - @pytest.mark.usefixtures( "ssdp_instant_discovery", "mock_setup_entry", - "mock_get_source_ip", ) async def test_flow_ssdp_discovery_changed_udn_but_st_differs( hass: HomeAssistant, @@ -403,7 +397,6 @@ async def test_flow_ssdp_discovery_changed_udn_ignored_entry( @pytest.mark.usefixtures( "ssdp_instant_discovery", "mock_setup_entry", - "mock_get_source_ip", "mock_mac_address_from_host", ) async def test_flow_user(hass: HomeAssistant) -> None: @@ -435,7 +428,6 @@ async def test_flow_user(hass: HomeAssistant) -> None: @pytest.mark.usefixtures( "ssdp_no_discovery", "mock_setup_entry", - "mock_get_source_ip", "mock_mac_address_from_host", ) async def test_flow_user_no_discovery(hass: HomeAssistant) -> None: @@ -450,7 +442,6 @@ async def test_flow_user_no_discovery(hass: HomeAssistant) -> None: @pytest.mark.usefixtures( "ssdp_instant_discovery", "mock_setup_entry", - "mock_get_source_ip", "mock_mac_address_from_host", ) async def test_flow_ssdp_with_mismatched_udn(hass: HomeAssistant) -> None: diff --git a/tests/components/upnp/test_init.py b/tests/components/upnp/test_init.py index eab279b479e..4b5e375f8e0 100644 --- a/tests/components/upnp/test_init.py +++ b/tests/components/upnp/test_init.py @@ -30,9 +30,7 @@ from .conftest import ( from tests.common import MockConfigEntry -@pytest.mark.usefixtures( - "ssdp_instant_discovery", "mock_get_source_ip", "mock_mac_address_from_host" -) +@pytest.mark.usefixtures("ssdp_instant_discovery", "mock_mac_address_from_host") async def test_async_setup_entry_default(hass: HomeAssistant) -> None: """Test async_setup_entry.""" entry = MockConfigEntry( @@ -52,9 +50,7 @@ async def test_async_setup_entry_default(hass: HomeAssistant) -> None: assert await hass.config_entries.async_setup(entry.entry_id) is True -@pytest.mark.usefixtures( - "ssdp_instant_discovery", "mock_get_source_ip", "mock_no_mac_address_from_host" -) +@pytest.mark.usefixtures("ssdp_instant_discovery", "mock_no_mac_address_from_host") async def test_async_setup_entry_default_no_mac_address(hass: HomeAssistant) -> None: """Test async_setup_entry.""" entry = MockConfigEntry( @@ -76,7 +72,6 @@ async def test_async_setup_entry_default_no_mac_address(hass: HomeAssistant) -> @pytest.mark.usefixtures( "ssdp_instant_discovery_multi_location", - "mock_get_source_ip", "mock_mac_address_from_host", ) async def test_async_setup_entry_multi_location( @@ -106,7 +101,7 @@ async def test_async_setup_entry_multi_location( mock_async_create_device.assert_called_once_with(TEST_LOCATION) -@pytest.mark.usefixtures("mock_get_source_ip", "mock_mac_address_from_host") +@pytest.mark.usefixtures("mock_mac_address_from_host") async def test_async_setup_udn_mismatch( hass: HomeAssistant, mock_async_create_device: AsyncMock ) -> None: diff --git a/tests/components/yamaha_musiccast/test_config_flow.py b/tests/components/yamaha_musiccast/test_config_flow.py index 1c51b315a5a..321e7250e5a 100644 --- a/tests/components/yamaha_musiccast/test_config_flow.py +++ b/tests/components/yamaha_musiccast/test_config_flow.py @@ -129,7 +129,7 @@ def mock_empty_discovery_information(): async def test_user_input_device_not_found( - hass: HomeAssistant, mock_get_device_info_mc_exception, mock_get_source_ip + hass: HomeAssistant, mock_get_device_info_mc_exception ) -> None: """Test when user specifies a non-existing device.""" result = await hass.config_entries.flow.async_init( @@ -147,7 +147,7 @@ async def test_user_input_device_not_found( async def test_user_input_non_yamaha_device_found( - hass: HomeAssistant, mock_get_device_info_invalid, mock_get_source_ip + hass: HomeAssistant, mock_get_device_info_invalid ) -> None: """Test when user specifies an existing device, which does not provide the musiccast API.""" result = await hass.config_entries.flow.async_init( @@ -165,7 +165,7 @@ async def test_user_input_non_yamaha_device_found( async def test_user_input_device_already_existing( - hass: HomeAssistant, mock_get_device_info_valid, mock_get_source_ip + hass: HomeAssistant, mock_get_device_info_valid ) -> None: """Test when user specifies an existing device.""" mock_entry = MockConfigEntry( @@ -189,7 +189,7 @@ async def test_user_input_device_already_existing( async def test_user_input_unknown_error( - hass: HomeAssistant, mock_get_device_info_exception, mock_get_source_ip + hass: HomeAssistant, mock_get_device_info_exception ) -> None: """Test when user specifies an existing device, which does not provide the musiccast API.""" result = await hass.config_entries.flow.async_init( @@ -210,7 +210,6 @@ async def test_user_input_device_found( hass: HomeAssistant, mock_get_device_info_valid, mock_valid_discovery_information, - mock_get_source_ip, ) -> None: """Test when user specifies an existing device.""" result = await hass.config_entries.flow.async_init( @@ -236,7 +235,6 @@ async def test_user_input_device_found_no_ssdp( hass: HomeAssistant, mock_get_device_info_valid, mock_empty_discovery_information, - mock_get_source_ip, ) -> None: """Test when user specifies an existing device, which no discovery data are present for.""" result = await hass.config_entries.flow.async_init( @@ -261,9 +259,7 @@ async def test_user_input_device_found_no_ssdp( # SSDP Flows -async def test_ssdp_discovery_failed( - hass: HomeAssistant, mock_ssdp_no_yamaha, mock_get_source_ip -) -> None: +async def test_ssdp_discovery_failed(hass: HomeAssistant, mock_ssdp_no_yamaha) -> None: """Test when an SSDP discovered device is not a musiccast device.""" result = await hass.config_entries.flow.async_init( DOMAIN, @@ -284,7 +280,7 @@ async def test_ssdp_discovery_failed( async def test_ssdp_discovery_successful_add_device( - hass: HomeAssistant, mock_ssdp_yamaha, mock_get_source_ip + hass: HomeAssistant, mock_ssdp_yamaha ) -> None: """Test when the SSDP discovered device is a musiccast device and the user confirms it.""" result = await hass.config_entries.flow.async_init( @@ -320,7 +316,7 @@ async def test_ssdp_discovery_successful_add_device( async def test_ssdp_discovery_existing_device_update( - hass: HomeAssistant, mock_ssdp_yamaha, mock_get_source_ip + hass: HomeAssistant, mock_ssdp_yamaha ) -> None: """Test when the SSDP discovered device is a musiccast device, but it already exists with another IP.""" mock_entry = MockConfigEntry( diff --git a/tests/components/yeelight/conftest.py b/tests/components/yeelight/conftest.py index e4ce0afc9bf..46a0ebb1bd5 100644 --- a/tests/components/yeelight/conftest.py +++ b/tests/components/yeelight/conftest.py @@ -1,10 +1,3 @@ """yeelight conftest.""" -import pytest - from tests.components.light.conftest import mock_light_profiles # noqa: F401 - - -@pytest.fixture(autouse=True) -def yeelight_mock_get_source_ip(mock_get_source_ip): - """Mock network util's async_get_source_ip.""" diff --git a/tests/components/zeroconf/conftest.py b/tests/components/zeroconf/conftest.py index d52f8234922..d702ef482d6 100644 --- a/tests/components/zeroconf/conftest.py +++ b/tests/components/zeroconf/conftest.py @@ -1,9 +1 @@ """Tests for the Zeroconf component.""" - -import pytest - - -@pytest.fixture(autouse=True) -def zc_mock_get_source_ip(mock_get_source_ip): - """Enable the mock_get_source_ip fixture for all zeroconf tests.""" - return mock_get_source_ip From 06251d403a0ab4194931267b3a7729a889ca6571 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 30 May 2024 10:41:32 +0200 Subject: [PATCH 0022/1445] Fix special case in pylint type hint plugin (#118454) * Fix special case in pylint type hint plugin * Simplify * Simplify * Simplify * Apply Co-authored-by: Marc Mueller <30130371+cdce8p@users.noreply.github.com> --------- Co-authored-by: Marc Mueller <30130371+cdce8p@users.noreply.github.com> --- pylint/plugins/hass_enforce_type_hints.py | 6 +++++- tests/pylint/test_enforce_type_hints.py | 5 ++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index 6d3b68cbeb6..0fc522f46c2 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -69,7 +69,7 @@ class ClassTypeHintMatch: matches: list[TypeHintMatch] -_INNER_MATCH = r"((?:[\w\| ]+)|(?:\.{3})|(?:\w+\[.+\]))" +_INNER_MATCH = r"((?:[\w\| ]+)|(?:\.{3})|(?:\w+\[.+\])|(?:\[\]))" _TYPE_HINT_MATCHERS: dict[str, re.Pattern[str]] = { # a_or_b matches items such as "DiscoveryInfoType | None" # or "dict | list | None" @@ -2914,6 +2914,10 @@ def _is_valid_type( if expected_type == "...": return isinstance(node, nodes.Const) and node.value == Ellipsis + # Special case for an empty list, such as Callable[[], TestServer] + if expected_type == "[]": + return isinstance(node, nodes.List) and not node.elts + # Special case for `xxx | yyy` if match := _TYPE_HINT_MATCHERS["a_or_b"].match(expected_type): return ( diff --git a/tests/pylint/test_enforce_type_hints.py b/tests/pylint/test_enforce_type_hints.py index 64dd472827e..0153214c267 100644 --- a/tests/pylint/test_enforce_type_hints.py +++ b/tests/pylint/test_enforce_type_hints.py @@ -54,6 +54,7 @@ def test_regex_get_module_platform( ("list[dict[str, str]]", 1, ("list", "dict[str, str]")), ("list[dict[str, Any]]", 1, ("list", "dict[str, Any]")), ("tuple[bytes | None, str | None]", 2, ("tuple", "bytes | None", "str | None")), + ("Callable[[], TestServer]", 2, ("Callable", "[]", "TestServer")), ], ) def test_regex_x_of_y_i( @@ -1130,12 +1131,14 @@ def test_notify_get_service( def test_pytest_function( linter: UnittestLinter, type_hint_checker: BaseChecker ) -> None: - """Ensure valid hints are accepted for async_get_service.""" + """Ensure valid hints are accepted for a test function.""" func_node = astroid.extract_node( """ async def test_sample( #@ hass: HomeAssistant, caplog: pytest.LogCaptureFixture, + aiohttp_server: Callable[[], TestServer], + unused_tcp_port_factory: Callable[[], int], ) -> None: pass """, From 9bd1c408bd622394678b12648770e964730c9c74 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Thu, 30 May 2024 11:00:36 +0200 Subject: [PATCH 0023/1445] Raise `ConfigEntryNotReady` when there is no `_id` in the Tractive data (#118467) Co-authored-by: Maciej Bieniek <478555+bieniu@users.noreply.github.com> --- homeassistant/components/tractive/__init__.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/homeassistant/components/tractive/__init__.py b/homeassistant/components/tractive/__init__.py index 6c053411329..468f11979e8 100644 --- a/homeassistant/components/tractive/__init__.py +++ b/homeassistant/components/tractive/__init__.py @@ -148,6 +148,13 @@ async def _generate_trackables( tracker.details(), tracker.hw_info(), tracker.pos_report() ) + if not tracker_details.get("_id"): + _LOGGER.info( + "Tractive API returns incomplete data for tracker %s", + trackable["device_id"], + ) + raise ConfigEntryNotReady + return Trackables(tracker, trackable, tracker_details, hw_info, pos_report) From c0ccc869542c844600788599accdbbd96897925f Mon Sep 17 00:00:00 2001 From: ashionky <35916938+ashionky@users.noreply.github.com> Date: Thu, 30 May 2024 17:03:18 +0800 Subject: [PATCH 0024/1445] Bump refoss to v1.2.1 (#118450) --- homeassistant/components/refoss/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/refoss/manifest.json b/homeassistant/components/refoss/manifest.json index 8e5b3864bcc..8b9b2d8cf11 100644 --- a/homeassistant/components/refoss/manifest.json +++ b/homeassistant/components/refoss/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/refoss", "iot_class": "local_polling", - "requirements": ["refoss-ha==1.2.0"] + "requirements": ["refoss-ha==1.2.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index d60aeb4892e..bd10fc67a1e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2442,7 +2442,7 @@ rapt-ble==0.1.2 raspyrfm-client==1.2.8 # homeassistant.components.refoss -refoss-ha==1.2.0 +refoss-ha==1.2.1 # homeassistant.components.rainmachine regenmaschine==2024.03.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7321bb6429b..2f7cdf5556d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1900,7 +1900,7 @@ radiotherm==2.1.0 rapt-ble==0.1.2 # homeassistant.components.refoss -refoss-ha==1.2.0 +refoss-ha==1.2.1 # homeassistant.components.rainmachine regenmaschine==2024.03.0 From ac979e9105711a93732e15f47ae0365dbae245ec Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Thu, 30 May 2024 11:40:05 +0200 Subject: [PATCH 0025/1445] Bump deebot-client to 7.3.0 (#118462) --- homeassistant/components/ecovacs/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/ecovacs/conftest.py | 13 ++++++++----- 4 files changed, 11 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/ecovacs/manifest.json b/homeassistant/components/ecovacs/manifest.json index de4181b21b6..66dd07cf431 100644 --- a/homeassistant/components/ecovacs/manifest.json +++ b/homeassistant/components/ecovacs/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/ecovacs", "iot_class": "cloud_push", "loggers": ["sleekxmppfs", "sucks", "deebot_client"], - "requirements": ["py-sucks==0.9.10", "deebot-client==7.2.0"] + "requirements": ["py-sucks==0.9.10", "deebot-client==7.3.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index bd10fc67a1e..3326edf4c9d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -703,7 +703,7 @@ debugpy==1.8.1 # decora==0.6 # homeassistant.components.ecovacs -deebot-client==7.2.0 +deebot-client==7.3.0 # homeassistant.components.ihc # homeassistant.components.namecheapdns diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2f7cdf5556d..8c419fafcc6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -581,7 +581,7 @@ dbus-fast==2.21.3 debugpy==1.8.1 # homeassistant.components.ecovacs -deebot-client==7.2.0 +deebot-client==7.3.0 # homeassistant.components.ihc # homeassistant.components.namecheapdns diff --git a/tests/components/ecovacs/conftest.py b/tests/components/ecovacs/conftest.py index d4333f65dc4..f227b6092fd 100644 --- a/tests/components/ecovacs/conftest.py +++ b/tests/components/ecovacs/conftest.py @@ -1,10 +1,11 @@ """Common fixtures for the Ecovacs tests.""" -from collections.abc import Generator +from collections.abc import AsyncGenerator, Generator from typing import Any from unittest.mock import AsyncMock, Mock, patch from deebot_client import const +from deebot_client.command import DeviceCommandResult from deebot_client.device import Device from deebot_client.exceptions import ApiError from deebot_client.models import Credentials @@ -98,7 +99,7 @@ def mock_authenticator_authenticate(mock_authenticator: Mock) -> AsyncMock: @pytest.fixture -def mock_mqtt_client(mock_authenticator: Mock) -> Mock: +def mock_mqtt_client(mock_authenticator: Mock) -> Generator[Mock, None, None]: """Mock the MQTT client.""" with ( patch( @@ -117,10 +118,12 @@ def mock_mqtt_client(mock_authenticator: Mock) -> Mock: @pytest.fixture -def mock_device_execute() -> AsyncMock: +def mock_device_execute() -> Generator[AsyncMock, None, None]: """Mock the device execute function.""" with patch.object( - Device, "_execute_command", return_value=True + Device, + "_execute_command", + return_value=DeviceCommandResult(device_reached=True), ) as mock_device_execute: yield mock_device_execute @@ -139,7 +142,7 @@ async def init_integration( mock_mqtt_client: Mock, mock_device_execute: AsyncMock, platforms: Platform | list[Platform], -) -> MockConfigEntry: +) -> AsyncGenerator[MockConfigEntry, None]: """Set up the Ecovacs integration for testing.""" if not isinstance(platforms, list): platforms = [platforms] From 46aa3ca97c6cccb6cc2a5e820cef56af0217e4e8 Mon Sep 17 00:00:00 2001 From: David Bonnes Date: Thu, 30 May 2024 11:13:45 +0100 Subject: [PATCH 0026/1445] Move evohome constants to separate module (#118471) * move constants to const.py * make module docstring tweaks * move schemas back to init --- homeassistant/components/evohome/__init__.py | 77 ++++++++++--------- homeassistant/components/evohome/climate.py | 20 ++--- homeassistant/components/evohome/const.py | 74 +++++++++++++----- .../components/evohome/water_heater.py | 2 +- 4 files changed, 103 insertions(+), 70 deletions(-) diff --git a/homeassistant/components/evohome/__init__.py b/homeassistant/components/evohome/__init__.py index 2a664986b74..33f7e3200e1 100644 --- a/homeassistant/components/evohome/__init__.py +++ b/homeassistant/components/evohome/__init__.py @@ -1,6 +1,7 @@ -"""Support for (EMEA/EU-based) Honeywell TCC climate systems. +"""Support for (EMEA/EU-based) Honeywell TCC systems. -Such systems include evohome, Round Thermostat, and others. +Such systems provide heating/cooling and DHW and include Evohome, Round Thermostat, and +others. """ from __future__ import annotations @@ -10,7 +11,7 @@ from datetime import datetime, timedelta from http import HTTPStatus import logging import re -from typing import Any +from typing import Any, Final import evohomeasync as ev1 from evohomeasync.schema import SZ_ID, SZ_SESSION_ID, SZ_TEMP @@ -58,21 +59,31 @@ from homeassistant.helpers.storage import Store from homeassistant.helpers.typing import ConfigType import homeassistant.util.dt as dt_util -from .const import DOMAIN, GWS, STORAGE_KEY, STORAGE_VER, TCS, UTC_OFFSET +from .const import ( + ACCESS_TOKEN, + ACCESS_TOKEN_EXPIRES, + ATTR_DURATION_DAYS, + ATTR_DURATION_HOURS, + ATTR_DURATION_UNTIL, + ATTR_SYSTEM_MODE, + ATTR_ZONE_TEMP, + CONF_LOCATION_IDX, + DOMAIN, + GWS, + REFRESH_TOKEN, + SCAN_INTERVAL_DEFAULT, + SCAN_INTERVAL_MINIMUM, + STORAGE_KEY, + STORAGE_VER, + TCS, + USER_DATA, + UTC_OFFSET, + EvoService, +) _LOGGER = logging.getLogger(__name__) -ACCESS_TOKEN = "access_token" -ACCESS_TOKEN_EXPIRES = "access_token_expires" -REFRESH_TOKEN = "refresh_token" -USER_DATA = "user_data" - -CONF_LOCATION_IDX = "location_idx" - -SCAN_INTERVAL_DEFAULT = timedelta(seconds=300) -SCAN_INTERVAL_MINIMUM = timedelta(seconds=60) - -CONFIG_SCHEMA = vol.Schema( +CONFIG_SCHEMA: Final = vol.Schema( { DOMAIN: vol.Schema( { @@ -88,22 +99,12 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) -ATTR_SYSTEM_MODE = "mode" -ATTR_DURATION_DAYS = "period" -ATTR_DURATION_HOURS = "duration" +# system mode schemas are built dynamically when the services are regiatered -ATTR_ZONE_TEMP = "setpoint" -ATTR_DURATION_UNTIL = "duration" - -SVC_REFRESH_SYSTEM = "refresh_system" -SVC_SET_SYSTEM_MODE = "set_system_mode" -SVC_RESET_SYSTEM = "reset_system" -SVC_SET_ZONE_OVERRIDE = "set_zone_override" -SVC_RESET_ZONE_OVERRIDE = "clear_zone_override" - - -RESET_ZONE_OVERRIDE_SCHEMA = vol.Schema({vol.Required(ATTR_ENTITY_ID): cv.entity_id}) -SET_ZONE_OVERRIDE_SCHEMA = vol.Schema( +RESET_ZONE_OVERRIDE_SCHEMA: Final = vol.Schema( + {vol.Required(ATTR_ENTITY_ID): cv.entity_id} +) +SET_ZONE_OVERRIDE_SCHEMA: Final = vol.Schema( { vol.Required(ATTR_ENTITY_ID): cv.entity_id, vol.Required(ATTR_ZONE_TEMP): vol.All( @@ -114,7 +115,6 @@ SET_ZONE_OVERRIDE_SCHEMA = vol.Schema( ), } ) -# system mode schemas are built dynamically, below def _dt_local_to_aware(dt_naive: datetime) -> datetime: @@ -358,14 +358,14 @@ def setup_service_functions(hass: HomeAssistant, broker: EvoBroker) -> None: async_dispatcher_send(hass, DOMAIN, payload) - hass.services.async_register(DOMAIN, SVC_REFRESH_SYSTEM, force_refresh) + hass.services.async_register(DOMAIN, EvoService.REFRESH_SYSTEM, force_refresh) # Enumerate which operating modes are supported by this system modes = broker.config[SZ_ALLOWED_SYSTEM_MODES] # Not all systems support "AutoWithReset": register this handler only if required if [m[SZ_SYSTEM_MODE] for m in modes if m[SZ_SYSTEM_MODE] == SZ_AUTO_WITH_RESET]: - hass.services.async_register(DOMAIN, SVC_RESET_SYSTEM, set_system_mode) + hass.services.async_register(DOMAIN, EvoService.RESET_SYSTEM, set_system_mode) system_mode_schemas = [] modes = [m for m in modes if m[SZ_SYSTEM_MODE] != SZ_AUTO_WITH_RESET] @@ -409,7 +409,7 @@ def setup_service_functions(hass: HomeAssistant, broker: EvoBroker) -> None: if system_mode_schemas: hass.services.async_register( DOMAIN, - SVC_SET_SYSTEM_MODE, + EvoService.SET_SYSTEM_MODE, set_system_mode, schema=vol.Schema(vol.Any(*system_mode_schemas)), ) @@ -417,13 +417,13 @@ def setup_service_functions(hass: HomeAssistant, broker: EvoBroker) -> None: # The zone modes are consistent across all systems and use the same schema hass.services.async_register( DOMAIN, - SVC_RESET_ZONE_OVERRIDE, + EvoService.RESET_ZONE_OVERRIDE, set_zone_override, schema=RESET_ZONE_OVERRIDE_SCHEMA, ) hass.services.async_register( DOMAIN, - SVC_SET_ZONE_OVERRIDE, + EvoService.SET_ZONE_OVERRIDE, set_zone_override, schema=SET_ZONE_OVERRIDE_SCHEMA, ) @@ -612,7 +612,10 @@ class EvoDevice(Entity): return if payload["unique_id"] != self._attr_unique_id: return - if payload["service"] in (SVC_SET_ZONE_OVERRIDE, SVC_RESET_ZONE_OVERRIDE): + if payload["service"] in ( + EvoService.SET_ZONE_OVERRIDE, + EvoService.RESET_ZONE_OVERRIDE, + ): await self.async_zone_svc_request(payload["service"], payload["data"]) return await self.async_tcs_svc_request(payload["service"], payload["data"]) diff --git a/homeassistant/components/evohome/climate.py b/homeassistant/components/evohome/climate.py index 2d462b5c525..8b3e8a46e2c 100644 --- a/homeassistant/components/evohome/climate.py +++ b/homeassistant/components/evohome/climate.py @@ -1,4 +1,4 @@ -"""Support for Climate devices of (EMEA/EU-based) Honeywell TCC systems.""" +"""Support for Climate entities of the Evohome integration.""" from __future__ import annotations @@ -37,19 +37,14 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType import homeassistant.util.dt as dt_util -from . import ( +from . import EvoChild, EvoDevice +from .const import ( ATTR_DURATION_DAYS, ATTR_DURATION_HOURS, ATTR_DURATION_UNTIL, ATTR_SYSTEM_MODE, ATTR_ZONE_TEMP, CONF_LOCATION_IDX, - SVC_RESET_ZONE_OVERRIDE, - SVC_SET_SYSTEM_MODE, - EvoChild, - EvoDevice, -) -from .const import ( DOMAIN, EVO_AUTO, EVO_AUTOECO, @@ -61,6 +56,7 @@ from .const import ( EVO_PERMOVER, EVO_RESET, EVO_TEMPOVER, + EvoService, ) if TYPE_CHECKING: @@ -200,11 +196,11 @@ class EvoZone(EvoChild, EvoClimateEntity): async def async_zone_svc_request(self, service: str, data: dict[str, Any]) -> None: """Process a service request (setpoint override) for a zone.""" - if service == SVC_RESET_ZONE_OVERRIDE: + if service == EvoService.RESET_ZONE_OVERRIDE: await self._evo_broker.call_client_api(self._evo_device.reset_mode()) return - # otherwise it is SVC_SET_ZONE_OVERRIDE + # otherwise it is EvoService.SET_ZONE_OVERRIDE temperature = max(min(data[ATTR_ZONE_TEMP], self.max_temp), self.min_temp) if ATTR_DURATION_UNTIL in data: @@ -386,9 +382,9 @@ class EvoController(EvoClimateEntity): Data validation is not required, it will have been done upstream. """ - if service == SVC_SET_SYSTEM_MODE: + if service == EvoService.SET_SYSTEM_MODE: mode = data[ATTR_SYSTEM_MODE] - else: # otherwise it is SVC_RESET_SYSTEM + else: # otherwise it is EvoService.RESET_SYSTEM mode = EVO_RESET if ATTR_DURATION_DAYS in data: diff --git a/homeassistant/components/evohome/const.py b/homeassistant/components/evohome/const.py index 1347c1f797c..15949bc3c37 100644 --- a/homeassistant/components/evohome/const.py +++ b/homeassistant/components/evohome/const.py @@ -1,26 +1,60 @@ -"""Support for (EMEA/EU-based) Honeywell TCC climate systems.""" +"""The constants of the Evohome integration.""" -DOMAIN = "evohome" +from __future__ import annotations -STORAGE_VER = 1 -STORAGE_KEY = DOMAIN +from datetime import timedelta +from enum import StrEnum, unique +from typing import Final -# The Parent's (i.e. TCS, Controller's) operating mode is one of: -EVO_RESET = "AutoWithReset" -EVO_AUTO = "Auto" -EVO_AUTOECO = "AutoWithEco" -EVO_AWAY = "Away" -EVO_DAYOFF = "DayOff" -EVO_CUSTOM = "Custom" -EVO_HEATOFF = "HeatingOff" +DOMAIN: Final = "evohome" -# The Children's operating mode is one of: -EVO_FOLLOW = "FollowSchedule" # the operating mode is 'inherited' from the TCS -EVO_TEMPOVER = "TemporaryOverride" -EVO_PERMOVER = "PermanentOverride" +STORAGE_VER: Final = 1 +STORAGE_KEY: Final = DOMAIN -# These are used only to help prevent E501 (line too long) violations -GWS = "gateways" -TCS = "temperatureControlSystems" +# The Parent's (i.e. TCS, Controller) operating mode is one of: +EVO_RESET: Final = "AutoWithReset" +EVO_AUTO: Final = "Auto" +EVO_AUTOECO: Final = "AutoWithEco" +EVO_AWAY: Final = "Away" +EVO_DAYOFF: Final = "DayOff" +EVO_CUSTOM: Final = "Custom" +EVO_HEATOFF: Final = "HeatingOff" -UTC_OFFSET = "currentOffsetMinutes" +# The Children's (i.e. Dhw, Zone) operating mode is one of: +EVO_FOLLOW: Final = "FollowSchedule" # the operating mode is 'inherited' from the TCS +EVO_TEMPOVER: Final = "TemporaryOverride" +EVO_PERMOVER: Final = "PermanentOverride" + +# These two are used only to help prevent E501 (line too long) violations +GWS: Final = "gateways" +TCS: Final = "temperatureControlSystems" + +UTC_OFFSET: Final = "currentOffsetMinutes" + +CONF_LOCATION_IDX: Final = "location_idx" + +ACCESS_TOKEN: Final = "access_token" +ACCESS_TOKEN_EXPIRES: Final = "access_token_expires" +REFRESH_TOKEN: Final = "refresh_token" +USER_DATA: Final = "user_data" + +SCAN_INTERVAL_DEFAULT: Final = timedelta(seconds=300) +SCAN_INTERVAL_MINIMUM: Final = timedelta(seconds=60) + +ATTR_SYSTEM_MODE: Final = "mode" +ATTR_DURATION_DAYS: Final = "period" +ATTR_DURATION_HOURS: Final = "duration" + +ATTR_ZONE_TEMP: Final = "setpoint" +ATTR_DURATION_UNTIL: Final = "duration" + + +@unique +class EvoService(StrEnum): + """The Evohome services.""" + + REFRESH_SYSTEM: Final = "refresh_system" + SET_SYSTEM_MODE: Final = "set_system_mode" + RESET_SYSTEM: Final = "reset_system" + SET_ZONE_OVERRIDE: Final = "set_zone_override" + RESET_ZONE_OVERRIDE: Final = "clear_zone_override" diff --git a/homeassistant/components/evohome/water_heater.py b/homeassistant/components/evohome/water_heater.py index 26be4b47a36..66ba7f46a70 100644 --- a/homeassistant/components/evohome/water_heater.py +++ b/homeassistant/components/evohome/water_heater.py @@ -1,4 +1,4 @@ -"""Support for WaterHeater devices of (EMEA/EU) Honeywell TCC systems.""" +"""Support for WaterHeater entities of the Evohome integration.""" from __future__ import annotations From fc5d58effdccbcac23d2bb02d189b9ab9f98b7ef Mon Sep 17 00:00:00 2001 From: Alexey Guseynov Date: Thu, 30 May 2024 11:20:02 +0100 Subject: [PATCH 0027/1445] Add Total Volatile Organic Compounds (tVOC) matter discovery schema (#116963) --- homeassistant/components/matter/sensor.py | 13 +++++ tests/components/matter/test_sensor.py | 58 +++++++++++++++++++++++ 2 files changed, 71 insertions(+) diff --git a/homeassistant/components/matter/sensor.py b/homeassistant/components/matter/sensor.py index ff5848ef54e..4e2644a1ff7 100644 --- a/homeassistant/components/matter/sensor.py +++ b/homeassistant/components/matter/sensor.py @@ -219,6 +219,19 @@ DISCOVERY_SCHEMAS = [ clusters.CarbonDioxideConcentrationMeasurement.Attributes.MeasuredValue, ), ), + MatterDiscoverySchema( + platform=Platform.SENSOR, + entity_description=MatterSensorEntityDescription( + key="TotalVolatileOrganicCompoundsSensor", + native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, + device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS_PARTS, + state_class=SensorStateClass.MEASUREMENT, + ), + entity_class=MatterSensor, + required_attributes=( + clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement.Attributes.MeasuredValue, + ), + ), MatterDiscoverySchema( platform=Platform.SENSOR, entity_description=MatterSensorEntityDescription( diff --git a/tests/components/matter/test_sensor.py b/tests/components/matter/test_sensor.py index 4ee6180ad77..42b13e24c9e 100644 --- a/tests/components/matter/test_sensor.py +++ b/tests/components/matter/test_sensor.py @@ -84,6 +84,16 @@ async def air_quality_sensor_node_fixture( ) +@pytest.fixture(name="air_purifier_node") +async def air_purifier_node_fixture( + hass: HomeAssistant, matter_client: MagicMock +) -> MatterNode: + """Fixture for an air purifier node.""" + return await setup_integration_with_node_fixture( + hass, "air-purifier", matter_client + ) + + # This tests needs to be adjusted to remove lingering tasks @pytest.mark.parametrize("expected_lingering_tasks", [True]) async def test_sensor_null_value( @@ -333,3 +343,51 @@ async def test_air_quality_sensor( state = hass.states.get("sensor.lightfi_aq1_air_quality_sensor_pm10") assert state assert state.state == "50.0" + + +# This tests needs to be adjusted to remove lingering tasks +@pytest.mark.parametrize("expected_lingering_tasks", [True]) +async def test_air_purifier_sensor( + hass: HomeAssistant, + matter_client: MagicMock, + air_purifier_node: MatterNode, +) -> None: + """Test Air quality sensors are creayted for air purifier device.""" + # Carbon Dioxide + state = hass.states.get("sensor.air_purifier_carbon_dioxide") + assert state + assert state.state == "2.0" + + # PM1 + state = hass.states.get("sensor.air_purifier_pm1") + assert state + assert state.state == "2.0" + + # PM2.5 + state = hass.states.get("sensor.air_purifier_pm2_5") + assert state + assert state.state == "2.0" + + # PM10 + state = hass.states.get("sensor.air_purifier_pm10") + assert state + assert state.state == "2.0" + + # Temperature + state = hass.states.get("sensor.air_purifier_temperature") + assert state + assert state.state == "20.0" + + # Humidity + state = hass.states.get("sensor.air_purifier_humidity") + assert state + assert state.state == "50.0" + + # VOCS + state = hass.states.get("sensor.air_purifier_vocs") + assert state + assert state.state == "2.0" + assert state.attributes["state_class"] == "measurement" + assert state.attributes["unit_of_measurement"] == "ppm" + assert state.attributes["device_class"] == "volatile_organic_compounds_parts" + assert state.attributes["friendly_name"] == "Air Purifier VOCs" From cf51179009b6c9c0c4662fa7c692f4b720c23cfb Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Thu, 30 May 2024 12:45:11 +0200 Subject: [PATCH 0028/1445] Add tests for Tractive integration (#118470) Co-authored-by: Maciej Bieniek <478555+bieniu@users.noreply.github.com> --- .coveragerc | 6 - tests/components/tractive/__init__.py | 17 + tests/components/tractive/conftest.py | 87 ++- .../tractive/fixtures/trackable_object.json | 5 +- .../tractive/fixtures/tracker_details.json | 38 ++ .../tractive/fixtures/tracker_hw_info.json | 11 + .../tractive/fixtures/tracker_pos_report.json | 16 + .../snapshots/test_binary_sensor.ambr | 95 ++++ .../snapshots/test_device_tracker.ambr | 103 ++++ .../tractive/snapshots/test_diagnostics.ambr | 3 +- .../tractive/snapshots/test_sensor.ambr | 524 ++++++++++++++++++ .../tractive/snapshots/test_switch.ambr | 277 +++++++++ .../components/tractive/test_binary_sensor.py | 29 + .../tractive/test_device_tracker.py | 61 ++ tests/components/tractive/test_diagnostics.py | 11 +- tests/components/tractive/test_init.py | 163 ++++++ tests/components/tractive/test_sensor.py | 30 + tests/components/tractive/test_switch.py | 228 ++++++++ 18 files changed, 1686 insertions(+), 18 deletions(-) create mode 100644 tests/components/tractive/fixtures/tracker_details.json create mode 100644 tests/components/tractive/fixtures/tracker_hw_info.json create mode 100644 tests/components/tractive/fixtures/tracker_pos_report.json create mode 100644 tests/components/tractive/snapshots/test_binary_sensor.ambr create mode 100644 tests/components/tractive/snapshots/test_device_tracker.ambr create mode 100644 tests/components/tractive/snapshots/test_sensor.ambr create mode 100644 tests/components/tractive/snapshots/test_switch.ambr create mode 100644 tests/components/tractive/test_binary_sensor.py create mode 100644 tests/components/tractive/test_device_tracker.py create mode 100644 tests/components/tractive/test_init.py create mode 100644 tests/components/tractive/test_sensor.py create mode 100644 tests/components/tractive/test_switch.py diff --git a/.coveragerc b/.coveragerc index 7594d2d2d98..0ef8c5dfe29 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1470,12 +1470,6 @@ omit = homeassistant/components/traccar_server/entity.py homeassistant/components/traccar_server/helpers.py homeassistant/components/traccar_server/sensor.py - homeassistant/components/tractive/__init__.py - homeassistant/components/tractive/binary_sensor.py - homeassistant/components/tractive/device_tracker.py - homeassistant/components/tractive/entity.py - homeassistant/components/tractive/sensor.py - homeassistant/components/tractive/switch.py homeassistant/components/tradfri/__init__.py homeassistant/components/tradfri/base_class.py homeassistant/components/tradfri/coordinator.py diff --git a/tests/components/tractive/__init__.py b/tests/components/tractive/__init__.py index dcde4b87436..48254a80f37 100644 --- a/tests/components/tractive/__init__.py +++ b/tests/components/tractive/__init__.py @@ -1 +1,18 @@ """Tests for the tractive integration.""" + +from unittest.mock import patch + +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +async def init_integration( + hass: HomeAssistant, entry: MockConfigEntry +) -> MockConfigEntry: + """Set up the Tractive integration in Home Assistant.""" + entry.add_to_hass(hass) + + with patch("homeassistant.components.tractive.TractiveClient._listen"): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() diff --git a/tests/components/tractive/conftest.py b/tests/components/tractive/conftest.py index 2137919ce98..5492f58b2ba 100644 --- a/tests/components/tractive/conftest.py +++ b/tests/components/tractive/conftest.py @@ -1,14 +1,16 @@ """Common fixtures for the Tractive tests.""" from collections.abc import Generator +from typing import Any from unittest.mock import AsyncMock, Mock, patch from aiotractive.trackable_object import TrackableObject from aiotractive.tracker import Tracker import pytest -from homeassistant.components.tractive.const import DOMAIN +from homeassistant.components.tractive.const import DOMAIN, SERVER_UNAVAILABLE from homeassistant.const import CONF_EMAIL, CONF_PASSWORD +from homeassistant.helpers.dispatcher import async_dispatcher_send from tests.common import MockConfigEntry, load_json_object_fixture @@ -17,7 +19,72 @@ from tests.common import MockConfigEntry, load_json_object_fixture def mock_tractive_client() -> Generator[AsyncMock, None, None]: """Mock a Tractive client.""" - trackable_object = load_json_object_fixture("tractive/trackable_object.json") + def send_hardware_event( + entry: MockConfigEntry, event: dict[str, Any] | None = None + ): + """Send hardware event.""" + if event is None: + event = { + "tracker_id": "device_id_123", + "hardware": {"battery_level": 88}, + "tracker_state": "operational", + "charging_state": "CHARGING", + } + entry.runtime_data.client._send_hardware_update(event) + + def send_wellness_event( + entry: MockConfigEntry, event: dict[str, Any] | None = None + ): + """Send wellness event.""" + if event is None: + event = { + "pet_id": "pet_id_123", + "sleep": {"minutes_day_sleep": 100, "minutes_night_sleep": 300}, + "wellness": {"activity_label": "ok", "sleep_label": "good"}, + "activity": { + "calories": 999, + "minutes_goal": 200, + "minutes_active": 150, + "minutes_rest": 122, + }, + } + entry.runtime_data.client._send_wellness_update(event) + + def send_position_event( + entry: MockConfigEntry, event: dict[str, Any] | None = None + ): + """Send position event.""" + if event is None: + event = { + "tracker_id": "device_id_123", + "position": { + "latlong": [22.333, 44.555], + "accuracy": 99, + "sensor_used": "GPS", + }, + } + entry.runtime_data.client._send_position_update(event) + + def send_switch_event(entry: MockConfigEntry, event: dict[str, Any] | None = None): + """Send switch event.""" + if event is None: + event = { + "tracker_id": "device_id_123", + "buzzer_control": {"active": True}, + "led_control": {"active": False}, + "live_tracking": {"active": True}, + } + entry.runtime_data.client._send_switch_update(event) + + def send_server_unavailable_event(hass): + """Send server unavailable event.""" + async_dispatcher_send(hass, f"{SERVER_UNAVAILABLE}-12345") + + trackable_object = load_json_object_fixture("trackable_object.json", DOMAIN) + tracker_details = load_json_object_fixture("tracker_details.json", DOMAIN) + tracker_hw_info = load_json_object_fixture("tracker_hw_info.json", DOMAIN) + tracker_pos_report = load_json_object_fixture("tracker_pos_report.json", DOMAIN) + with ( patch( "homeassistant.components.tractive.aiotractive.Tractive", autospec=True @@ -33,7 +100,21 @@ def mock_tractive_client() -> Generator[AsyncMock, None, None]: details=AsyncMock(return_value=trackable_object), ), ] - client.tracker.return_value = Mock(spec=Tracker) + client.tracker.return_value = AsyncMock( + spec=Tracker, + details=AsyncMock(return_value=tracker_details), + hw_info=AsyncMock(return_value=tracker_hw_info), + pos_report=AsyncMock(return_value=tracker_pos_report), + set_live_tracking_active=AsyncMock(return_value={"pending": True}), + set_buzzer_active=AsyncMock(return_value={"pending": True}), + set_led_active=AsyncMock(return_value={"pending": True}), + ) + + client.send_hardware_event = send_hardware_event + client.send_wellness_event = send_wellness_event + client.send_position_event = send_position_event + client.send_switch_event = send_switch_event + client.send_server_unavailable_event = send_server_unavailable_event yield client diff --git a/tests/components/tractive/fixtures/trackable_object.json b/tests/components/tractive/fixtures/trackable_object.json index 066cc613a80..a33dd314bff 100644 --- a/tests/components/tractive/fixtures/trackable_object.json +++ b/tests/components/tractive/fixtures/trackable_object.json @@ -1,7 +1,8 @@ { - "device_id": "54321", + "device_id": "device_id_123", + "_id": "pet_id_123", "details": { - "_id": "xyz123", + "_id": "pet_id_123", "_version": "123abc", "name": "Test Pet", "pet_type": "DOG", diff --git a/tests/components/tractive/fixtures/tracker_details.json b/tests/components/tractive/fixtures/tracker_details.json new file mode 100644 index 00000000000..0acde4b991a --- /dev/null +++ b/tests/components/tractive/fixtures/tracker_details.json @@ -0,0 +1,38 @@ +{ + "_id": "device_id_123", + "_version": "abcd-123-efgh-456", + "hw_id": "device_id_123", + "model_number": "TG4422", + "hw_edition": "BLUE-WHITE", + "bluetooth_mac": null, + "geofence_sensitivity": "HIGH", + "battery_save_mode": null, + "read_only": false, + "demo": false, + "self_test_available": false, + "capabilities": [ + "LT", + "BUZZER", + "LT_BLE", + "LED_BLE", + "BUZZER_BLE", + "HW_REPORTS_BLE", + "WIFI_SCAN_REPORTS_BLE", + "LED", + "ACTIVITY_TRACKING", + "WIFI_ZONE", + "SLEEP_TRACKING" + ], + "supported_geofence_types": ["CIRCLE", "RECTANGLE", "POLYGON"], + "fw_version": "123.456", + "state": "OPERATIONAL", + "state_reason": "POWER_SAVING", + "charging_state": "NOT_CHARGING", + "battery_state": "FULL", + "power_saving_zone_id": "abcdef12345", + "prioritized_zone_id": "098765", + "prioritized_zone_type": "POWER_SAVING", + "prioritized_zone_last_seen_at": 1716106551, + "prioritized_zone_entered_at": 1716105066, + "_type": "tracker" +} diff --git a/tests/components/tractive/fixtures/tracker_hw_info.json b/tests/components/tractive/fixtures/tracker_hw_info.json new file mode 100644 index 00000000000..1f2929b328a --- /dev/null +++ b/tests/components/tractive/fixtures/tracker_hw_info.json @@ -0,0 +1,11 @@ +{ + "time": 1716105966, + "battery_level": 96, + "clip_mounted_state": null, + "_id": "device_id_123", + "_type": "device_hw_report", + "_version": "e87646946", + "report_id": "098123", + "power_saving_zone_id": "abcdef12345", + "hw_status": null +} diff --git a/tests/components/tractive/fixtures/tracker_pos_report.json b/tests/components/tractive/fixtures/tracker_pos_report.json new file mode 100644 index 00000000000..2fafd960ee8 --- /dev/null +++ b/tests/components/tractive/fixtures/tracker_pos_report.json @@ -0,0 +1,16 @@ +{ + "time": 1716106551, + "time_rcvd": 1716106561, + "pos_status": null, + "latlong": [33.222222, 44.555555], + "speed": null, + "pos_uncertainty": 30, + "_id": "device_id_123", + "_type": "device_pos_report", + "_version": "b7422b930", + "altitude": 85, + "report_id": "098123", + "sensor_used": "KNOWN_WIFI", + "nearby_user_id": null, + "power_saving_zone_id": "abcdef12345" +} diff --git a/tests/components/tractive/snapshots/test_binary_sensor.ambr b/tests/components/tractive/snapshots/test_binary_sensor.ambr new file mode 100644 index 00000000000..c6d50fb0fbb --- /dev/null +++ b/tests/components/tractive/snapshots/test_binary_sensor.ambr @@ -0,0 +1,95 @@ +# serializer version: 1 +# name: test_binary_sensor[binary_sensor.test_pet_tracker_battery_charging-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.test_pet_tracker_battery_charging', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Tracker battery charging', + 'platform': 'tractive', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'tracker_battery_charging', + 'unique_id': 'pet_id_123_battery_charging', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[binary_sensor.test_pet_tracker_battery_charging-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery_charging', + 'friendly_name': 'Test Pet Tracker battery charging', + }), + 'context': , + 'entity_id': 'binary_sensor.test_pet_tracker_battery_charging', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[binary_sensor.test_pet_tracker_battery_charging-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.test_pet_tracker_battery_charging', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Tracker battery charging', + 'platform': 'tractive', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'tracker_battery_charging', + 'unique_id': 'pet_id_123_battery_charging', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[binary_sensor.test_pet_tracker_battery_charging-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery_charging', + 'friendly_name': 'Test Pet Tracker battery charging', + }), + 'context': , + 'entity_id': 'binary_sensor.test_pet_tracker_battery_charging', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- diff --git a/tests/components/tractive/snapshots/test_device_tracker.ambr b/tests/components/tractive/snapshots/test_device_tracker.ambr new file mode 100644 index 00000000000..3a145a48b5a --- /dev/null +++ b/tests/components/tractive/snapshots/test_device_tracker.ambr @@ -0,0 +1,103 @@ +# serializer version: 1 +# name: test_device_tracker[device_tracker.test_pet_tracker-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'device_tracker', + 'entity_category': , + 'entity_id': 'device_tracker.test_pet_tracker', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Tracker', + 'platform': 'tractive', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'tracker', + 'unique_id': 'pet_id_123', + 'unit_of_measurement': None, + }) +# --- +# name: test_device_tracker[device_tracker.test_pet_tracker-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'battery_level': 88, + 'friendly_name': 'Test Pet Tracker', + 'gps_accuracy': 99, + 'latitude': 22.333, + 'longitude': 44.555, + 'source_type': , + }), + 'context': , + 'entity_id': 'device_tracker.test_pet_tracker', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'not_home', + }) +# --- +# name: test_sensor[device_tracker.test_pet_tracker-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'device_tracker', + 'entity_category': , + 'entity_id': 'device_tracker.test_pet_tracker', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Tracker', + 'platform': 'tractive', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'tracker', + 'unique_id': 'pet_id_123', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[device_tracker.test_pet_tracker-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'battery_level': 88, + 'friendly_name': 'Test Pet Tracker', + 'gps_accuracy': 99, + 'latitude': 22.333, + 'longitude': 44.555, + 'source_type': , + }), + 'context': , + 'entity_id': 'device_tracker.test_pet_tracker', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'not_home', + }) +# --- diff --git a/tests/components/tractive/snapshots/test_diagnostics.ambr b/tests/components/tractive/snapshots/test_diagnostics.ambr index 11bf7bae2a3..a66247749b7 100644 --- a/tests/components/tractive/snapshots/test_diagnostics.ambr +++ b/tests/components/tractive/snapshots/test_diagnostics.ambr @@ -21,6 +21,7 @@ }), 'trackables': list([ dict({ + '_id': '**REDACTED**', 'details': dict({ '_id': '**REDACTED**', '_type': 'pet_detail', @@ -64,7 +65,7 @@ 'weight': 23700, 'weight_is_default': None, }), - 'device_id': '54321', + 'device_id': 'device_id_123', }), ]), }) diff --git a/tests/components/tractive/snapshots/test_sensor.ambr b/tests/components/tractive/snapshots/test_sensor.ambr new file mode 100644 index 00000000000..f1ed397450e --- /dev/null +++ b/tests/components/tractive/snapshots/test_sensor.ambr @@ -0,0 +1,524 @@ +# serializer version: 1 +# name: test_sensor[sensor.test_pet_activity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'good', + 'low', + 'ok', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.test_pet_activity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Activity', + 'platform': 'tractive', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'activity', + 'unique_id': 'pet_id_123_activity_label', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[sensor.test_pet_activity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Test Pet Activity', + 'options': list([ + 'good', + 'low', + 'ok', + ]), + }), + 'context': , + 'entity_id': 'sensor.test_pet_activity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'ok', + }) +# --- +# name: test_sensor[sensor.test_pet_activity_time-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.test_pet_activity_time', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Activity time', + 'platform': 'tractive', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'activity_time', + 'unique_id': 'pet_id_123_minutes_active', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.test_pet_activity_time-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Pet Activity time', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.test_pet_activity_time', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '150', + }) +# --- +# name: test_sensor[sensor.test_pet_calories_burned-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.test_pet_calories_burned', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Calories burned', + 'platform': 'tractive', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'calories', + 'unique_id': 'pet_id_123_calories', + 'unit_of_measurement': 'kcal', + }) +# --- +# name: test_sensor[sensor.test_pet_calories_burned-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Pet Calories burned', + 'state_class': , + 'unit_of_measurement': 'kcal', + }), + 'context': , + 'entity_id': 'sensor.test_pet_calories_burned', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '999', + }) +# --- +# name: test_sensor[sensor.test_pet_daily_goal-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.test_pet_daily_goal', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Daily goal', + 'platform': 'tractive', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'daily_goal', + 'unique_id': 'pet_id_123_daily_goal', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.test_pet_daily_goal-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Pet Daily goal', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.test_pet_daily_goal', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '200', + }) +# --- +# name: test_sensor[sensor.test_pet_day_sleep-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.test_pet_day_sleep', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Day sleep', + 'platform': 'tractive', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'minutes_day_sleep', + 'unique_id': 'pet_id_123_minutes_day_sleep', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.test_pet_day_sleep-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Pet Day sleep', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.test_pet_day_sleep', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '100', + }) +# --- +# name: test_sensor[sensor.test_pet_night_sleep-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.test_pet_night_sleep', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Night sleep', + 'platform': 'tractive', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'minutes_night_sleep', + 'unique_id': 'pet_id_123_minutes_night_sleep', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.test_pet_night_sleep-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Pet Night sleep', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.test_pet_night_sleep', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '300', + }) +# --- +# name: test_sensor[sensor.test_pet_rest_time-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.test_pet_rest_time', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Rest time', + 'platform': 'tractive', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'rest_time', + 'unique_id': 'pet_id_123_minutes_rest', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.test_pet_rest_time-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Pet Rest time', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.test_pet_rest_time', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '122', + }) +# --- +# name: test_sensor[sensor.test_pet_sleep-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'good', + 'low', + 'ok', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.test_pet_sleep', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Sleep', + 'platform': 'tractive', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'sleep', + 'unique_id': 'pet_id_123_sleep_label', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[sensor.test_pet_sleep-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Test Pet Sleep', + 'options': list([ + 'good', + 'low', + 'ok', + ]), + }), + 'context': , + 'entity_id': 'sensor.test_pet_sleep', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'good', + }) +# --- +# name: test_sensor[sensor.test_pet_tracker_battery-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.test_pet_tracker_battery', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Tracker battery', + 'platform': 'tractive', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'tracker_battery_level', + 'unique_id': 'pet_id_123_battery_level', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensor[sensor.test_pet_tracker_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Test Pet Tracker battery', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.test_pet_tracker_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '88', + }) +# --- +# name: test_sensor[sensor.test_pet_tracker_state-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'inaccurate_position', + 'not_reporting', + 'operational', + 'system_shutdown_user', + 'system_startup', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.test_pet_tracker_state', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Tracker state', + 'platform': 'tractive', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'tracker_state', + 'unique_id': 'pet_id_123_tracker_state', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[sensor.test_pet_tracker_state-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Test Pet Tracker state', + 'options': list([ + 'inaccurate_position', + 'not_reporting', + 'operational', + 'system_shutdown_user', + 'system_startup', + ]), + }), + 'context': , + 'entity_id': 'sensor.test_pet_tracker_state', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'operational', + }) +# --- diff --git a/tests/components/tractive/snapshots/test_switch.ambr b/tests/components/tractive/snapshots/test_switch.ambr new file mode 100644 index 00000000000..ea9ea9d9e48 --- /dev/null +++ b/tests/components/tractive/snapshots/test_switch.ambr @@ -0,0 +1,277 @@ +# serializer version: 1 +# name: test_sensor[switch.test_pet_live_tracking-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.test_pet_live_tracking', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Live tracking', + 'platform': 'tractive', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'live_tracking', + 'unique_id': 'pet_id_123_live_tracking', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.test_pet_live_tracking-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Pet Live tracking', + }), + 'context': , + 'entity_id': 'switch.test_pet_live_tracking', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.test_pet_tracker_buzzer-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.test_pet_tracker_buzzer', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Tracker buzzer', + 'platform': 'tractive', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'tracker_buzzer', + 'unique_id': 'pet_id_123_buzzer', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.test_pet_tracker_buzzer-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Pet Tracker buzzer', + }), + 'context': , + 'entity_id': 'switch.test_pet_tracker_buzzer', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.test_pet_tracker_led-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.test_pet_tracker_led', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Tracker LED', + 'platform': 'tractive', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'tracker_led', + 'unique_id': 'pet_id_123_led', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.test_pet_tracker_led-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Pet Tracker LED', + }), + 'context': , + 'entity_id': 'switch.test_pet_tracker_led', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_switch[switch.test_pet_live_tracking-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.test_pet_live_tracking', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Live tracking', + 'platform': 'tractive', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'live_tracking', + 'unique_id': 'pet_id_123_live_tracking', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.test_pet_live_tracking-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Pet Live tracking', + }), + 'context': , + 'entity_id': 'switch.test_pet_live_tracking', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.test_pet_tracker_buzzer-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.test_pet_tracker_buzzer', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Tracker buzzer', + 'platform': 'tractive', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'tracker_buzzer', + 'unique_id': 'pet_id_123_buzzer', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.test_pet_tracker_buzzer-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Pet Tracker buzzer', + }), + 'context': , + 'entity_id': 'switch.test_pet_tracker_buzzer', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.test_pet_tracker_led-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.test_pet_tracker_led', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Tracker LED', + 'platform': 'tractive', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'tracker_led', + 'unique_id': 'pet_id_123_led', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.test_pet_tracker_led-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Pet Tracker LED', + }), + 'context': , + 'entity_id': 'switch.test_pet_tracker_led', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- diff --git a/tests/components/tractive/test_binary_sensor.py b/tests/components/tractive/test_binary_sensor.py new file mode 100644 index 00000000000..cd7ffbc3da3 --- /dev/null +++ b/tests/components/tractive/test_binary_sensor.py @@ -0,0 +1,29 @@ +"""Test the Tractive binary sensor platform.""" + +from unittest.mock import AsyncMock, patch + +from syrupy import SnapshotAssertion + +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import init_integration + +from tests.common import MockConfigEntry, snapshot_platform + + +async def test_binary_sensor( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, + mock_tractive_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test states of the binary sensor.""" + with patch("homeassistant.components.tractive.PLATFORMS", [Platform.BINARY_SENSOR]): + await init_integration(hass, mock_config_entry) + + mock_tractive_client.send_hardware_event(mock_config_entry) + await hass.async_block_till_done() + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) diff --git a/tests/components/tractive/test_device_tracker.py b/tests/components/tractive/test_device_tracker.py new file mode 100644 index 00000000000..ff78173ef7b --- /dev/null +++ b/tests/components/tractive/test_device_tracker.py @@ -0,0 +1,61 @@ +"""Test the Tractive device tracker platform.""" + +from unittest.mock import AsyncMock, patch + +from syrupy import SnapshotAssertion + +from homeassistant.components.device_tracker import SourceType +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import init_integration + +from tests.common import MockConfigEntry, snapshot_platform + + +async def test_device_tracker( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, + mock_tractive_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test states of the device_tracker.""" + with patch( + "homeassistant.components.tractive.PLATFORMS", [Platform.DEVICE_TRACKER] + ): + await init_integration(hass, mock_config_entry) + + mock_tractive_client.send_position_event(mock_config_entry) + mock_tractive_client.send_hardware_event(mock_config_entry) + await hass.async_block_till_done() + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + +async def test_source_type_phone( + hass: HomeAssistant, + mock_tractive_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test the device tracker with source type phone.""" + await init_integration(hass, mock_config_entry) + + mock_tractive_client.send_position_event( + mock_config_entry, + { + "tracker_id": "device_id_123", + "position": { + "latlong": [22.333, 44.555], + "accuracy": 99, + "sensor_used": "PHONE", + }, + }, + ) + mock_tractive_client.send_hardware_event(mock_config_entry) + await hass.async_block_till_done() + + assert ( + hass.states.get("device_tracker.test_pet_tracker").attributes["source_type"] + is SourceType.BLUETOOTH + ) diff --git a/tests/components/tractive/test_diagnostics.py b/tests/components/tractive/test_diagnostics.py index acf4a3ed151..cc4fcdeba15 100644 --- a/tests/components/tractive/test_diagnostics.py +++ b/tests/components/tractive/test_diagnostics.py @@ -1,12 +1,12 @@ """Test the Tractive diagnostics.""" -from unittest.mock import AsyncMock, patch +from unittest.mock import AsyncMock from syrupy import SnapshotAssertion -from homeassistant.components.tractive.const import DOMAIN from homeassistant.core import HomeAssistant -from homeassistant.setup import async_setup_component + +from . import init_integration from tests.common import MockConfigEntry from tests.components.diagnostics import get_diagnostics_for_config_entry @@ -21,9 +21,8 @@ async def test_entry_diagnostics( mock_config_entry: MockConfigEntry, ) -> None: """Test config entry diagnostics.""" - mock_config_entry.add_to_hass(hass) - with patch("homeassistant.components.tractive.PLATFORMS", []): - assert await async_setup_component(hass, DOMAIN, {}) + await init_integration(hass, mock_config_entry) + result = await get_diagnostics_for_config_entry( hass, hass_client, mock_config_entry ) diff --git a/tests/components/tractive/test_init.py b/tests/components/tractive/test_init.py new file mode 100644 index 00000000000..3387232b231 --- /dev/null +++ b/tests/components/tractive/test_init.py @@ -0,0 +1,163 @@ +"""Test init of Tractive integration.""" + +from unittest.mock import AsyncMock, patch + +from aiotractive.exceptions import TractiveError, UnauthorizedError +import pytest + +from homeassistant.components.tractive.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import EVENT_HOMEASSISTANT_STOP, STATE_UNAVAILABLE +from homeassistant.core import HomeAssistant + +from . import init_integration + +from tests.common import MockConfigEntry + + +async def test_setup_entry( + hass: HomeAssistant, + mock_tractive_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test a successful setup entry.""" + await init_integration(hass, mock_config_entry) + + assert mock_config_entry.state is ConfigEntryState.LOADED + + +async def test_unload_entry( + hass: HomeAssistant, + mock_tractive_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test successful unload of entry.""" + await init_integration(hass, mock_config_entry) + + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + assert mock_config_entry.state is ConfigEntryState.LOADED + + with patch("homeassistant.components.tractive.TractiveClient.unsubscribe"): + assert await hass.config_entries.async_unload(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert mock_config_entry.state is ConfigEntryState.NOT_LOADED + assert not hass.data.get(DOMAIN) + + +@pytest.mark.parametrize( + ("method", "exc", "entry_state"), + [ + ("authenticate", UnauthorizedError, ConfigEntryState.SETUP_ERROR), + ("authenticate", TractiveError, ConfigEntryState.SETUP_RETRY), + ("trackable_objects", TractiveError, ConfigEntryState.SETUP_RETRY), + ], +) +async def test_setup_failed( + hass: HomeAssistant, + mock_tractive_client: AsyncMock, + mock_config_entry: MockConfigEntry, + method: str, + exc: Exception, + entry_state: ConfigEntryState, +) -> None: + """Test for setup failure.""" + getattr(mock_tractive_client, method).side_effect = exc + + await init_integration(hass, mock_config_entry) + + assert mock_config_entry.state is entry_state + + +async def test_config_not_ready( + hass: HomeAssistant, + mock_tractive_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test for setup failure if the tracker_details doesn't contain '_id'.""" + mock_tractive_client.tracker.return_value.details.return_value.pop("_id") + + await init_integration(hass, mock_config_entry) + + assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY + + +async def test_trackable_without_details( + hass: HomeAssistant, + mock_tractive_client: AsyncMock, + mock_config_entry: MockConfigEntry, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test a successful setup entry.""" + mock_tractive_client.trackable_objects.return_value[0].details.return_value = { + "device_id": "xyz098" + } + + await init_integration(hass, mock_config_entry) + + assert ( + "Tracker xyz098 has no details and will be skipped. This happens for shared trackers" + in caplog.text + ) + assert mock_config_entry.state is ConfigEntryState.LOADED + + +async def test_trackable_without_device_id( + hass: HomeAssistant, + mock_tractive_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test a successful setup entry.""" + mock_tractive_client.trackable_objects.return_value[0].details.return_value = { + "device_id": None + } + + await init_integration(hass, mock_config_entry) + + assert mock_config_entry.state is ConfigEntryState.LOADED + + +async def test_unsubscribe_on_ha_stop( + hass: HomeAssistant, + mock_tractive_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test unsuscribe when HA stops.""" + await init_integration(hass, mock_config_entry) + + with patch( + "homeassistant.components.tractive.TractiveClient.unsubscribe" + ) as mock_unsuscribe: + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + + assert mock_unsuscribe.called + + +async def test_server_unavailable( + hass: HomeAssistant, + mock_tractive_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test states of the sensor.""" + entity_id = "sensor.test_pet_tracker_battery" + + await init_integration(hass, mock_config_entry) + + # send event to make the entity available + mock_tractive_client.send_hardware_event(mock_config_entry) + await hass.async_block_till_done() + + assert hass.states.get(entity_id).state != STATE_UNAVAILABLE + + # send server unavailable event, the entity should be unavailable + mock_tractive_client.send_server_unavailable_event(hass) + await hass.async_block_till_done() + + assert hass.states.get(entity_id).state == STATE_UNAVAILABLE + + # send event to make the entity available once again + mock_tractive_client.send_hardware_event(mock_config_entry) + await hass.async_block_till_done() + + assert hass.states.get(entity_id).state != STATE_UNAVAILABLE diff --git a/tests/components/tractive/test_sensor.py b/tests/components/tractive/test_sensor.py new file mode 100644 index 00000000000..b53cc3c4d64 --- /dev/null +++ b/tests/components/tractive/test_sensor.py @@ -0,0 +1,30 @@ +"""Test the Tractive sensor platform.""" + +from unittest.mock import AsyncMock, patch + +from syrupy import SnapshotAssertion + +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import init_integration + +from tests.common import MockConfigEntry, snapshot_platform + + +async def test_sensor( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, + mock_tractive_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test states of the sensor.""" + with patch("homeassistant.components.tractive.PLATFORMS", [Platform.SENSOR]): + await init_integration(hass, mock_config_entry) + + mock_tractive_client.send_hardware_event(mock_config_entry) + mock_tractive_client.send_wellness_event(mock_config_entry) + await hass.async_block_till_done() + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) diff --git a/tests/components/tractive/test_switch.py b/tests/components/tractive/test_switch.py new file mode 100644 index 00000000000..cc7ce6cf81f --- /dev/null +++ b/tests/components/tractive/test_switch.py @@ -0,0 +1,228 @@ +"""Test the Tractive switch platform.""" + +from unittest.mock import AsyncMock, patch + +from aiotractive.exceptions import TractiveError +from syrupy import SnapshotAssertion + +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +from homeassistant.const import ( + ATTR_ENTITY_ID, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_OFF, + STATE_ON, + Platform, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import init_integration + +from tests.common import MockConfigEntry, snapshot_platform + + +async def test_switch( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, + mock_tractive_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test states of the switch.""" + with patch("homeassistant.components.tractive.PLATFORMS", [Platform.SWITCH]): + await init_integration(hass, mock_config_entry) + + mock_tractive_client.send_switch_event(mock_config_entry) + await hass.async_block_till_done() + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + +async def test_switch_on( + hass: HomeAssistant, + mock_tractive_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test the switch can be turned on.""" + entity_id = "switch.test_pet_tracker_led" + + await init_integration(hass, mock_config_entry) + + mock_tractive_client.send_switch_event(mock_config_entry) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state + assert state.state == STATE_OFF + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + mock_tractive_client.send_switch_event( + mock_config_entry, + {"tracker_id": "device_id_123", "led_control": {"active": True}}, + ) + await hass.async_block_till_done() + + assert mock_tractive_client.tracker.return_value.set_led_active.call_count == 1 + assert ( + mock_tractive_client.tracker.return_value.set_led_active.call_args[0][0] is True + ) + + state = hass.states.get(entity_id) + assert state + assert state.state == STATE_ON + + +async def test_switch_off( + hass: HomeAssistant, + mock_tractive_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test the switch can be turned off.""" + entity_id = "switch.test_pet_tracker_buzzer" + + await init_integration(hass, mock_config_entry) + + mock_tractive_client.send_switch_event(mock_config_entry) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state + assert state.state == STATE_ON + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + mock_tractive_client.send_switch_event( + mock_config_entry, + {"tracker_id": "device_id_123", "buzzer_control": {"active": False}}, + ) + await hass.async_block_till_done() + + assert mock_tractive_client.tracker.return_value.set_buzzer_active.call_count == 1 + assert ( + mock_tractive_client.tracker.return_value.set_buzzer_active.call_args[0][0] + is False + ) + + state = hass.states.get(entity_id) + assert state + assert state.state == STATE_OFF + + +async def test_live_tracking_switch( + hass: HomeAssistant, + mock_tractive_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test the live_tracking switch.""" + entity_id = "switch.test_pet_live_tracking" + + await init_integration(hass, mock_config_entry) + + mock_tractive_client.send_switch_event(mock_config_entry) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state + assert state.state == STATE_ON + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + mock_tractive_client.send_switch_event( + mock_config_entry, + {"tracker_id": "device_id_123", "live_tracking": {"active": False}}, + ) + await hass.async_block_till_done() + + assert ( + mock_tractive_client.tracker.return_value.set_live_tracking_active.call_count + == 1 + ) + assert ( + mock_tractive_client.tracker.return_value.set_live_tracking_active.call_args[0][ + 0 + ] + is False + ) + + state = hass.states.get(entity_id) + assert state + assert state.state == STATE_OFF + + +async def test_switch_on_with_exception( + hass: HomeAssistant, + mock_tractive_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test the switch turn on with exception.""" + entity_id = "switch.test_pet_tracker_led" + + await init_integration(hass, mock_config_entry) + + mock_tractive_client.send_switch_event(mock_config_entry) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state + assert state.state == STATE_OFF + + mock_tractive_client.tracker.return_value.set_led_active.side_effect = TractiveError + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state + assert state.state == STATE_OFF + + +async def test_switch_off_with_exception( + hass: HomeAssistant, + mock_tractive_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test the switch turn off with exception.""" + entity_id = "switch.test_pet_tracker_buzzer" + + await init_integration(hass, mock_config_entry) + + mock_tractive_client.send_switch_event(mock_config_entry) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state + assert state.state == STATE_ON + + mock_tractive_client.tracker.return_value.set_buzzer_active.side_effect = ( + TractiveError + ) + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state + assert state.state == STATE_ON From c387698c6f0159dd6f9803d5ad210948c50478ed Mon Sep 17 00:00:00 2001 From: lunmay <28674102+lunmay@users.noreply.github.com> Date: Thu, 30 May 2024 13:24:58 +0200 Subject: [PATCH 0029/1445] Typo fix in media_extractor (#118473) --- homeassistant/components/media_extractor/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/media_extractor/strings.json b/homeassistant/components/media_extractor/strings.json index 4c3743b5c12..125aa08337a 100644 --- a/homeassistant/components/media_extractor/strings.json +++ b/homeassistant/components/media_extractor/strings.json @@ -23,7 +23,7 @@ }, "extract_media_url": { "name": "Get Media URL", - "description": "Extract media url from a service.", + "description": "Extract media URL from a service.", "fields": { "url": { "name": "Media URL", From e3f6d4cfbf3c8d3121788f3cf8a0d2c2e9407937 Mon Sep 17 00:00:00 2001 From: Tsvi Mostovicz Date: Thu, 30 May 2024 14:59:38 +0300 Subject: [PATCH 0030/1445] Use const instead of literal string in HVV integration (#118479) Use const instead of literal string --- homeassistant/components/hvv_departures/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/hvv_departures/sensor.py b/homeassistant/components/hvv_departures/sensor.py index 89260b921ea..6ad61295d04 100644 --- a/homeassistant/components/hvv_departures/sensor.py +++ b/homeassistant/components/hvv_departures/sensor.py @@ -9,7 +9,7 @@ from pygti.exceptions import InvalidAuth from homeassistant.components.sensor import SensorDeviceClass, SensorEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_ID +from homeassistant.const import ATTR_ID, CONF_OFFSET from homeassistant.core import HomeAssistant from homeassistant.helpers import aiohttp_client from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo @@ -92,7 +92,7 @@ class HVVDepartureSensor(SensorEntity): async def async_update(self, **kwargs: Any) -> None: """Update the sensor.""" departure_time = utcnow() + timedelta( - minutes=self.config_entry.options.get("offset", 0) + minutes=self.config_entry.options.get(CONF_OFFSET, 0) ) departure_time_tz_berlin = departure_time.astimezone(BERLIN_TIME_ZONE) From 7f49077ec67a87860733afff8f903f16dc5b96ab Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Thu, 30 May 2024 14:20:02 +0200 Subject: [PATCH 0031/1445] Set enity_category to config for airgradient select entities (#118477) --- homeassistant/components/airgradient/select.py | 3 +++ tests/components/airgradient/snapshots/test_select.ambr | 6 +++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/airgradient/select.py b/homeassistant/components/airgradient/select.py index 8dc13fe0eba..41b5a48c686 100644 --- a/homeassistant/components/airgradient/select.py +++ b/homeassistant/components/airgradient/select.py @@ -8,6 +8,7 @@ from airgradient.models import ConfigurationControl, TemperatureUnit from homeassistant.components.select import SelectEntity, SelectEntityDescription from homeassistant.config_entries import ConfigEntry +from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant from homeassistant.exceptions import ServiceValidationError from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -30,6 +31,7 @@ CONFIG_CONTROL_ENTITY = AirGradientSelectEntityDescription( key="configuration_control", translation_key="configuration_control", options=[x.value for x in ConfigurationControl], + entity_category=EntityCategory.CONFIG, value_fn=lambda config: config.configuration_control, set_value_fn=lambda client, value: client.set_configuration_control( ConfigurationControl(value) @@ -41,6 +43,7 @@ PROTECTED_SELECT_TYPES: tuple[AirGradientSelectEntityDescription, ...] = ( key="display_temperature_unit", translation_key="display_temperature_unit", options=[x.value for x in TemperatureUnit], + entity_category=EntityCategory.CONFIG, value_fn=lambda config: config.temperature_unit, set_value_fn=lambda client, value: client.set_temperature_unit( TemperatureUnit(value) diff --git a/tests/components/airgradient/snapshots/test_select.ambr b/tests/components/airgradient/snapshots/test_select.ambr index e32b57758c1..986e3c6ebb8 100644 --- a/tests/components/airgradient/snapshots/test_select.ambr +++ b/tests/components/airgradient/snapshots/test_select.ambr @@ -16,7 +16,7 @@ 'device_id': , 'disabled_by': None, 'domain': 'select', - 'entity_category': None, + 'entity_category': , 'entity_id': 'select.airgradient_configuration_source', 'has_entity_name': True, 'hidden_by': None, @@ -72,7 +72,7 @@ 'device_id': , 'disabled_by': None, 'domain': 'select', - 'entity_category': None, + 'entity_category': , 'entity_id': 'select.airgradient_display_temperature_unit', 'has_entity_name': True, 'hidden_by': None, @@ -128,7 +128,7 @@ 'device_id': , 'disabled_by': None, 'domain': 'select', - 'entity_category': None, + 'entity_category': , 'entity_id': 'select.airgradient_configuration_source', 'has_entity_name': True, 'hidden_by': None, From 12f2bcc3a49bc4b060f0e59407e6ee9161dcaaf2 Mon Sep 17 00:00:00 2001 From: Markus Jacobsen Date: Thu, 30 May 2024 14:31:38 +0200 Subject: [PATCH 0032/1445] Bang & Olufsen sort supported media_player features alphabetically (#118476) Sort supported media_player features alphabetically --- .../components/bang_olufsen/media_player.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/bang_olufsen/media_player.py b/homeassistant/components/bang_olufsen/media_player.py index 2ad23e3683b..725afab88b9 100644 --- a/homeassistant/components/bang_olufsen/media_player.py +++ b/homeassistant/components/bang_olufsen/media_player.py @@ -67,19 +67,19 @@ from .entity import BangOlufsenEntity _LOGGER = logging.getLogger(__name__) BANG_OLUFSEN_FEATURES = ( - MediaPlayerEntityFeature.PAUSE - | MediaPlayerEntityFeature.SEEK - | MediaPlayerEntityFeature.VOLUME_SET - | MediaPlayerEntityFeature.VOLUME_MUTE - | MediaPlayerEntityFeature.PREVIOUS_TRACK + MediaPlayerEntityFeature.BROWSE_MEDIA + | MediaPlayerEntityFeature.CLEAR_PLAYLIST | MediaPlayerEntityFeature.NEXT_TRACK + | MediaPlayerEntityFeature.PAUSE + | MediaPlayerEntityFeature.PLAY | MediaPlayerEntityFeature.PLAY_MEDIA + | MediaPlayerEntityFeature.PREVIOUS_TRACK + | MediaPlayerEntityFeature.SEEK | MediaPlayerEntityFeature.SELECT_SOURCE | MediaPlayerEntityFeature.STOP - | MediaPlayerEntityFeature.CLEAR_PLAYLIST - | MediaPlayerEntityFeature.PLAY - | MediaPlayerEntityFeature.BROWSE_MEDIA | MediaPlayerEntityFeature.TURN_OFF + | MediaPlayerEntityFeature.VOLUME_MUTE + | MediaPlayerEntityFeature.VOLUME_SET ) From 4b95ea864ffb099e328eb4602a4d7457a97fa789 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 30 May 2024 15:46:08 +0200 Subject: [PATCH 0033/1445] Fix a typo in hassfest (#118482) --- script/hassfest/icons.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/hassfest/icons.py b/script/hassfest/icons.py index b7ba2fbb402..e7451dfd498 100644 --- a/script/hassfest/icons.py +++ b/script/hassfest/icons.py @@ -48,7 +48,7 @@ def ensure_not_same_as_default(value: dict) -> dict: def icon_schema(integration_type: str, no_entity_platform: bool) -> vol.Schema: - """Create a icon schema.""" + """Create an icon schema.""" state_validator = cv.schema_with_slug_keys( icon_value_validator, From 2cc38b426aa1af41b8c15a2ab156a07ef5b5aeac Mon Sep 17 00:00:00 2001 From: Oleg Kurapov Date: Thu, 30 May 2024 16:29:50 +0200 Subject: [PATCH 0034/1445] Add XML support to RESTful binary sensor (#110062) * Add XML support to RESTful binary sensor * Add test for binary sensor with XML input data * Address mypy validation results by handling None returns * Use proper incorrect XML instead of blank * Change failure condition to match the behavior of the library method * Change error handling for bad XML to expect ExpatError * Parametrize bad XML test to catch both empty and invalid XML * Move exception handling out of the shared method --------- Co-authored-by: Erik Montnemery --- .../components/rest/binary_sensor.py | 18 +++-- homeassistant/components/rest/data.py | 11 +-- homeassistant/components/rest/sensor.py | 9 ++- tests/components/rest/test_binary_sensor.py | 71 +++++++++++++++++++ tests/components/rest/test_sensor.py | 16 ++++- 5 files changed, 107 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/rest/binary_sensor.py b/homeassistant/components/rest/binary_sensor.py index 0568203a91c..5aafd727178 100644 --- a/homeassistant/components/rest/binary_sensor.py +++ b/homeassistant/components/rest/binary_sensor.py @@ -4,6 +4,7 @@ from __future__ import annotations import logging import ssl +from xml.parsers.expat import ExpatError import voluptuous as vol @@ -149,24 +150,31 @@ class RestBinarySensor(ManualTriggerEntity, RestEntity, BinarySensorEntity): self._attr_is_on = False return - response = self.rest.data + try: + response = self.rest.data_without_xml() + except ExpatError as err: + self._attr_is_on = False + _LOGGER.warning( + "REST xml result could not be parsed and converted to JSON: %s", err + ) + return raw_value = response - if self._value_template is not None: + if response is not None and self._value_template is not None: response = self._value_template.async_render_with_possible_json_value( - self.rest.data, False + response, False ) try: - self._attr_is_on = bool(int(response)) + self._attr_is_on = bool(int(str(response))) except ValueError: self._attr_is_on = { "true": True, "on": True, "open": True, "yes": True, - }.get(response.lower(), False) + }.get(str(response).lower(), False) self._process_manual_data(raw_value) self.async_write_ha_state() diff --git a/homeassistant/components/rest/data.py b/homeassistant/components/rest/data.py index 4c9667e7651..e198202ae57 100644 --- a/homeassistant/components/rest/data.py +++ b/homeassistant/components/rest/data.py @@ -4,7 +4,6 @@ from __future__ import annotations import logging import ssl -from xml.parsers.expat import ExpatError import httpx import xmltodict @@ -79,14 +78,8 @@ class RestData: and (content_type := headers.get("content-type")) and content_type.startswith(XML_MIME_TYPES) ): - try: - value = json_dumps(xmltodict.parse(value)) - except ExpatError: - _LOGGER.warning( - "REST xml result could not be parsed and converted to JSON" - ) - else: - _LOGGER.debug("JSON converted from XML: %s", value) + value = json_dumps(xmltodict.parse(value)) + _LOGGER.debug("JSON converted from XML: %s", value) return value async def async_update(self, log_errors: bool = True) -> None: diff --git a/homeassistant/components/rest/sensor.py b/homeassistant/components/rest/sensor.py index 199ab3721c3..810d286d147 100644 --- a/homeassistant/components/rest/sensor.py +++ b/homeassistant/components/rest/sensor.py @@ -5,6 +5,7 @@ from __future__ import annotations import logging import ssl from typing import Any +from xml.parsers.expat import ExpatError import voluptuous as vol @@ -159,7 +160,13 @@ class RestSensor(ManualTriggerSensorEntity, RestEntity): def _update_from_rest_data(self) -> None: """Update state from the rest data.""" - value = self.rest.data_without_xml() + try: + value = self.rest.data_without_xml() + except ExpatError as err: + _LOGGER.warning( + "REST xml result could not be parsed and converted to JSON: %s", err + ) + value = self.rest.data if self._json_attrs: self._attr_extra_state_attributes = parse_json_attributes( diff --git a/tests/components/rest/test_binary_sensor.py b/tests/components/rest/test_binary_sensor.py index 39e6a7aea0d..65ec6bf5c05 100644 --- a/tests/components/rest/test_binary_sensor.py +++ b/tests/components/rest/test_binary_sensor.py @@ -362,6 +362,77 @@ async def test_setup_get_on(hass: HomeAssistant) -> None: assert state.state == STATE_ON +@respx.mock +async def test_setup_get_xml(hass: HomeAssistant) -> None: + """Test setup with valid xml configuration.""" + respx.get("http://localhost").respond( + status_code=HTTPStatus.OK, + headers={"content-type": "text/xml"}, + content="1", + ) + assert await async_setup_component( + hass, + BINARY_SENSOR_DOMAIN, + { + BINARY_SENSOR_DOMAIN: { + "platform": DOMAIN, + "resource": "http://localhost", + "method": "GET", + "value_template": "{{ value_json.dog }}", + "name": "foo", + "verify_ssl": "true", + "timeout": 30, + } + }, + ) + await hass.async_block_till_done() + assert len(hass.states.async_all(BINARY_SENSOR_DOMAIN)) == 1 + + state = hass.states.get("binary_sensor.foo") + assert state.state == STATE_ON + + +@respx.mock +@pytest.mark.parametrize( + ("content"), + [ + (""), + (""), + ], +) +async def test_setup_get_bad_xml( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture, content: str +) -> None: + """Test attributes get extracted from a XML result with bad xml.""" + + respx.get("http://localhost").respond( + status_code=HTTPStatus.OK, + headers={"content-type": "text/xml"}, + content=content, + ) + assert await async_setup_component( + hass, + BINARY_SENSOR_DOMAIN, + { + BINARY_SENSOR_DOMAIN: { + "platform": DOMAIN, + "resource": "http://localhost", + "method": "GET", + "value_template": "{{ value_json.toplevel.master_value }}", + "name": "foo", + "verify_ssl": "true", + "timeout": 30, + } + }, + ) + await hass.async_block_till_done() + assert len(hass.states.async_all(BINARY_SENSOR_DOMAIN)) == 1 + state = hass.states.get("binary_sensor.foo") + + assert state.state == STATE_OFF + assert "REST xml result could not be parsed" in caplog.text + + @respx.mock async def test_setup_with_exception(hass: HomeAssistant) -> None: """Test setup with exception.""" diff --git a/tests/components/rest/test_sensor.py b/tests/components/rest/test_sensor.py index 9af1ac9273e..2e02063b215 100644 --- a/tests/components/rest/test_sensor.py +++ b/tests/components/rest/test_sensor.py @@ -868,15 +868,25 @@ async def test_update_with_application_xml_convert_json_attrs_with_jsonattr_temp @respx.mock +@pytest.mark.parametrize( + ("content", "error_message"), + [ + ("", "Empty reply"), + ("", "Erroneous JSON"), + ], +) async def test_update_with_xml_convert_bad_xml( - hass: HomeAssistant, caplog: pytest.LogCaptureFixture + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, + content: str, + error_message: str, ) -> None: """Test attributes get extracted from a XML result with bad xml.""" respx.get("http://localhost").respond( status_code=HTTPStatus.OK, headers={"content-type": "text/xml"}, - content="", + content=content, ) assert await async_setup_component( hass, @@ -901,7 +911,7 @@ async def test_update_with_xml_convert_bad_xml( assert state.state == STATE_UNKNOWN assert "REST xml result could not be parsed" in caplog.text - assert "Empty reply" in caplog.text + assert error_message in caplog.text @respx.mock From 2ca407760895d03860e6d775cbe888123dd4e638 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Thu, 30 May 2024 16:39:04 +0200 Subject: [PATCH 0035/1445] Mark Matter climate dry/fan mode support on Panasonic AC (#118485) --- homeassistant/components/matter/climate.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/matter/climate.py b/homeassistant/components/matter/climate.py index 69a961ebf90..2050a9eb185 100644 --- a/homeassistant/components/matter/climate.py +++ b/homeassistant/components/matter/climate.py @@ -59,6 +59,7 @@ SUPPORT_DRY_MODE_DEVICES: set[tuple[int, int]] = { # The Matter spec is missing a feature flag if the device supports a dry mode. # In the list below specify tuples of (vendorid, productid) of devices that # support dry mode. + (0x0001, 0x0108), (0x1209, 0x8007), } @@ -66,6 +67,7 @@ SUPPORT_FAN_MODE_DEVICES: set[tuple[int, int]] = { # The Matter spec is missing a feature flag if the device supports a fan-only mode. # In the list below specify tuples of (vendorid, productid) of devices that # support fan-only mode. + (0x0001, 0x0108), (0x1209, 0x8007), } From 56e4fa86b0d998d1052934e0c359fa686a226bc8 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 30 May 2024 16:55:49 +0200 Subject: [PATCH 0036/1445] Update frontend to 20240530.0 (#118489) --- 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 d1177058706..c84a54d2642 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==20240529.0"] + "requirements": ["home-assistant-frontend==20240530.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 8b7b7cee138..5f823188423 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -32,7 +32,7 @@ habluetooth==3.1.1 hass-nabucasa==0.81.0 hassil==1.7.1 home-assistant-bluetooth==1.12.0 -home-assistant-frontend==20240529.0 +home-assistant-frontend==20240530.0 home-assistant-intents==2024.5.28 httpx==0.27.0 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 3326edf4c9d..c733f8f4786 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1087,7 +1087,7 @@ hole==0.8.0 holidays==0.49 # homeassistant.components.frontend -home-assistant-frontend==20240529.0 +home-assistant-frontend==20240530.0 # homeassistant.components.conversation home-assistant-intents==2024.5.28 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8c419fafcc6..4a7b30a9942 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -889,7 +889,7 @@ hole==0.8.0 holidays==0.49 # homeassistant.components.frontend -home-assistant-frontend==20240529.0 +home-assistant-frontend==20240530.0 # homeassistant.components.conversation home-assistant-intents==2024.5.28 From a95c074ab89c84cb4c9292c07ba6f43aef6a68a5 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Thu, 30 May 2024 16:59:45 +0200 Subject: [PATCH 0037/1445] Extend Matter sensor discovery schemas for Air Purifier / Air Quality devices (#118483) Co-authored-by: Franck Nijhof --- homeassistant/components/matter/sensor.py | 93 ++++++++++++++++++++ homeassistant/components/matter/strings.json | 18 ++++ tests/components/matter/test_sensor.py | 59 +++++++++++++ 3 files changed, 170 insertions(+) diff --git a/homeassistant/components/matter/sensor.py b/homeassistant/components/matter/sensor.py index 4e2644a1ff7..d91d4d33471 100644 --- a/homeassistant/components/matter/sensor.py +++ b/homeassistant/components/matter/sensor.py @@ -37,6 +37,17 @@ from .entity import MatterEntity, MatterEntityDescription from .helpers import get_matter from .models import MatterDiscoverySchema +AIR_QUALITY_MAP = { + clusters.AirQuality.Enums.AirQualityEnum.kExtremelyPoor: "extremely_poor", + clusters.AirQuality.Enums.AirQualityEnum.kVeryPoor: "very_poor", + clusters.AirQuality.Enums.AirQualityEnum.kPoor: "poor", + clusters.AirQuality.Enums.AirQualityEnum.kFair: "fair", + clusters.AirQuality.Enums.AirQualityEnum.kGood: "good", + clusters.AirQuality.Enums.AirQualityEnum.kModerate: "moderate", + clusters.AirQuality.Enums.AirQualityEnum.kUnknown: "unknown", + clusters.AirQuality.Enums.AirQualityEnum.kUnknownEnumValue: "unknown", +} + async def async_setup_entry( hass: HomeAssistant, @@ -271,4 +282,86 @@ DISCOVERY_SCHEMAS = [ clusters.Pm10ConcentrationMeasurement.Attributes.MeasuredValue, ), ), + MatterDiscoverySchema( + platform=Platform.SENSOR, + entity_description=MatterSensorEntityDescription( + key="AirQuality", + translation_key="air_quality", + device_class=SensorDeviceClass.ENUM, + state_class=None, + # convert to set first to remove the duplicate unknown value + options=list(set(AIR_QUALITY_MAP.values())), + measurement_to_ha=lambda x: AIR_QUALITY_MAP[x], + icon="mdi:air-filter", + ), + entity_class=MatterSensor, + required_attributes=(clusters.AirQuality.Attributes.AirQuality,), + ), + MatterDiscoverySchema( + platform=Platform.SENSOR, + entity_description=MatterSensorEntityDescription( + key="CarbonMonoxideSensor", + native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, + device_class=SensorDeviceClass.CO, + state_class=SensorStateClass.MEASUREMENT, + ), + entity_class=MatterSensor, + required_attributes=( + clusters.CarbonMonoxideConcentrationMeasurement.Attributes.MeasuredValue, + ), + ), + MatterDiscoverySchema( + platform=Platform.SENSOR, + entity_description=MatterSensorEntityDescription( + key="NitrogenDioxideSensor", + native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, + device_class=SensorDeviceClass.NITROGEN_DIOXIDE, + state_class=SensorStateClass.MEASUREMENT, + ), + entity_class=MatterSensor, + required_attributes=( + clusters.NitrogenDioxideConcentrationMeasurement.Attributes.MeasuredValue, + ), + ), + MatterDiscoverySchema( + platform=Platform.SENSOR, + entity_description=MatterSensorEntityDescription( + key="OzoneConcentrationSensor", + native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, + device_class=SensorDeviceClass.OZONE, + state_class=SensorStateClass.MEASUREMENT, + ), + entity_class=MatterSensor, + required_attributes=( + clusters.OzoneConcentrationMeasurement.Attributes.MeasuredValue, + ), + ), + MatterDiscoverySchema( + platform=Platform.SENSOR, + entity_description=MatterSensorEntityDescription( + key="HepaFilterCondition", + native_unit_of_measurement=PERCENTAGE, + device_class=None, + state_class=SensorStateClass.MEASUREMENT, + translation_key="hepa_filter_condition", + icon="mdi:filter-check", + ), + entity_class=MatterSensor, + required_attributes=(clusters.HepaFilterMonitoring.Attributes.Condition,), + ), + MatterDiscoverySchema( + platform=Platform.SENSOR, + entity_description=MatterSensorEntityDescription( + key="ActivatedCarbonFilterCondition", + native_unit_of_measurement=PERCENTAGE, + device_class=None, + state_class=SensorStateClass.MEASUREMENT, + translation_key="activated_carbon_filter_condition", + icon="mdi:filter-check", + ), + entity_class=MatterSensor, + required_attributes=( + clusters.ActivatedCarbonFilterMonitoring.Attributes.Condition, + ), + ), ] diff --git a/homeassistant/components/matter/strings.json b/homeassistant/components/matter/strings.json index c6c2d779255..a3f26a5865a 100644 --- a/homeassistant/components/matter/strings.json +++ b/homeassistant/components/matter/strings.json @@ -79,8 +79,26 @@ } }, "sensor": { + "activated_carbon_filter_condition": { + "name": "Activated carbon filter condition" + }, + "air_quality": { + "name": "Air quality", + "state": { + "extremely_poor": "Extremely poor", + "very_poor": "Very poor", + "poor": "Poor", + "fair": "Fair", + "good": "Good", + "moderate": "Moderate", + "unknown": "Unknown" + } + }, "flow": { "name": "Flow" + }, + "hepa_filter_condition": { + "name": "Hepa filter condition" } } }, diff --git a/tests/components/matter/test_sensor.py b/tests/components/matter/test_sensor.py index 42b13e24c9e..2c9bfae94ce 100644 --- a/tests/components/matter/test_sensor.py +++ b/tests/components/matter/test_sensor.py @@ -391,3 +391,62 @@ async def test_air_purifier_sensor( assert state.attributes["unit_of_measurement"] == "ppm" assert state.attributes["device_class"] == "volatile_organic_compounds_parts" assert state.attributes["friendly_name"] == "Air Purifier VOCs" + + # Air Quality + state = hass.states.get("sensor.air_purifier_air_quality") + assert state + assert state.state == "good" + expected_options = [ + "extremely_poor", + "very_poor", + "poor", + "fair", + "good", + "moderate", + "unknown", + ] + assert set(state.attributes["options"]) == set(expected_options) + assert state.attributes["device_class"] == "enum" + assert state.attributes["friendly_name"] == "Air Purifier Air quality" + + # Carbon MonoOxide + state = hass.states.get("sensor.air_purifier_carbon_monoxide") + assert state + assert state.state == "2.0" + assert state.attributes["state_class"] == "measurement" + assert state.attributes["unit_of_measurement"] == "ppm" + assert state.attributes["device_class"] == "carbon_monoxide" + assert state.attributes["friendly_name"] == "Air Purifier Carbon monoxide" + + # Nitrogen Dioxide + state = hass.states.get("sensor.air_purifier_nitrogen_dioxide") + assert state + assert state.state == "2.0" + assert state.attributes["state_class"] == "measurement" + assert state.attributes["unit_of_measurement"] == "ppm" + assert state.attributes["device_class"] == "nitrogen_dioxide" + assert state.attributes["friendly_name"] == "Air Purifier Nitrogen dioxide" + + # Ozone Concentration + state = hass.states.get("sensor.air_purifier_ozone") + assert state + assert state.state == "2.0" + assert state.attributes["state_class"] == "measurement" + assert state.attributes["unit_of_measurement"] == "ppm" + assert state.attributes["device_class"] == "ozone" + assert state.attributes["friendly_name"] == "Air Purifier Ozone" + + # Hepa Filter Condition + state = hass.states.get("sensor.air_purifier_hepa_filter_condition") + assert state + assert state.state == "100" + assert state.attributes["state_class"] == "measurement" + assert state.attributes["unit_of_measurement"] == "%" + assert state.attributes["friendly_name"] == "Air Purifier Hepa filter condition" + + # Activated Carbon Filter Condition + state = hass.states.get("sensor.air_purifier_activated_carbon_filter_condition") + assert state + assert state.state == "100" + assert state.attributes["state_class"] == "measurement" + assert state.attributes["unit_of_measurement"] == "%" From 2814ed5003da1aa9e82bfa4ca0d5fb77ed87a223 Mon Sep 17 00:00:00 2001 From: Ron Weikamp <15732230+ronweikamp@users.noreply.github.com> Date: Thu, 30 May 2024 17:42:34 +0200 Subject: [PATCH 0038/1445] Add allow_negative configuration option to DurationSelector (#116134) * Add configuration option positive to DurationSelector * Rename to allow_negative in conjunction with a deprecation notice Co-authored-by: Erik Montnemery --------- Co-authored-by: Erik Montnemery --- homeassistant/helpers/selector.py | 8 +++++++- tests/helpers/test_selector.py | 5 +++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/homeassistant/helpers/selector.py b/homeassistant/helpers/selector.py index c103999bd33..1db4dd9f80b 100644 --- a/homeassistant/helpers/selector.py +++ b/homeassistant/helpers/selector.py @@ -718,6 +718,7 @@ class DurationSelectorConfig(TypedDict, total=False): """Class to represent a duration selector config.""" enable_day: bool + allow_negative: bool @SELECTORS.register("duration") @@ -731,6 +732,8 @@ class DurationSelector(Selector[DurationSelectorConfig]): # Enable day field in frontend. A selection with `days` set is allowed # even if `enable_day` is not set vol.Optional("enable_day"): cv.boolean, + # Allow negative durations. Will default to False in HA Core 2025.6.0. + vol.Optional("allow_negative"): cv.boolean, } ) @@ -740,7 +743,10 @@ class DurationSelector(Selector[DurationSelectorConfig]): def __call__(self, data: Any) -> dict[str, float]: """Validate the passed selection.""" - cv.time_period_dict(data) + if self.config.get("allow_negative", True): + cv.time_period_dict(data) + else: + cv.positive_time_period_dict(data) return cast(dict[str, float], data) diff --git a/tests/helpers/test_selector.py b/tests/helpers/test_selector.py index 5e6209f2c6c..6db313baa24 100644 --- a/tests/helpers/test_selector.py +++ b/tests/helpers/test_selector.py @@ -745,6 +745,11 @@ def test_attribute_selector_schema( ({"seconds": 10}, {"days": 10}), (None, {}), ), + ( + {"allow_negative": False}, + ({"seconds": 10}, {"days": 10}), + (None, {}, {"seconds": -1}), + ), ], ) def test_duration_selector_schema(schema, valid_selections, invalid_selections) -> None: From 12215c51b3946056ed3ceb47ef3732d47a81f207 Mon Sep 17 00:00:00 2001 From: Tsvi Mostovicz Date: Thu, 30 May 2024 19:27:15 +0300 Subject: [PATCH 0039/1445] Fix Jewish calendar unique id's (#117985) * Initial commit * Fix updating of unique id * Add testing to check the unique id is being updated correctly * Reload the config entry and confirm the unique id has not been changed * Move updating unique_id to __init__.py as suggested * Change the config_entry variable's name back from config to config_entry * Move the loop into the update_unique_ids method * Move test from test_config_flow to test_init * Try an early optimization to check if we need to update the unique ids * Mention the correct version * Implement suggestions * Ensure all entities are migrated correctly * Just to be sure keep the previous assertion as well --- .../components/jewish_calendar/__init__.py | 41 ++++++++-- .../jewish_calendar/binary_sensor.py | 9 ++- .../components/jewish_calendar/sensor.py | 11 ++- .../jewish_calendar/test_config_flow.py | 1 + tests/components/jewish_calendar/test_init.py | 74 +++++++++++++++++++ 5 files changed, 124 insertions(+), 12 deletions(-) create mode 100644 tests/components/jewish_calendar/test_init.py diff --git a/homeassistant/components/jewish_calendar/__init__.py b/homeassistant/components/jewish_calendar/__init__.py index 77a6b8af98c..7c4c0b7f634 100644 --- a/homeassistant/components/jewish_calendar/__init__.py +++ b/homeassistant/components/jewish_calendar/__init__.py @@ -16,11 +16,13 @@ from homeassistant.const import ( CONF_TIME_ZONE, Platform, ) -from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant, callback import homeassistant.helpers.config_validation as cv +import homeassistant.helpers.entity_registry as er from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType +from .binary_sensor import BINARY_SENSORS from .const import ( CONF_CANDLE_LIGHT_MINUTES, CONF_DIASPORA, @@ -32,6 +34,7 @@ from .const import ( DEFAULT_NAME, DOMAIN, ) +from .sensor import INFO_SENSORS, TIME_SENSORS PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.SENSOR] @@ -131,18 +134,24 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b timezone=config_entry.data.get(CONF_TIME_ZONE, hass.config.time_zone), ) - prefix = get_unique_prefix( - location, language, candle_lighting_offset, havdalah_offset - ) hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = { CONF_LANGUAGE: language, CONF_DIASPORA: diaspora, CONF_LOCATION: location, CONF_CANDLE_LIGHT_MINUTES: candle_lighting_offset, CONF_HAVDALAH_OFFSET_MINUTES: havdalah_offset, - "prefix": prefix, } + # Update unique ID to be unrelated to user defined options + old_prefix = get_unique_prefix( + location, language, candle_lighting_offset, havdalah_offset + ) + + ent_reg = er.async_get(hass) + entries = er.async_entries_for_config_entry(ent_reg, config_entry.entry_id) + if not entries or any(entry.unique_id.startswith(old_prefix) for entry in entries): + async_update_unique_ids(ent_reg, config_entry.entry_id, old_prefix) + await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) return True @@ -157,3 +166,25 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> hass.data[DOMAIN].pop(config_entry.entry_id) return unload_ok + + +@callback +def async_update_unique_ids( + ent_reg: er.EntityRegistry, new_prefix: str, old_prefix: str +) -> None: + """Update unique ID to be unrelated to user defined options. + + Introduced with release 2024.6 + """ + platform_descriptions = { + Platform.BINARY_SENSOR: BINARY_SENSORS, + Platform.SENSOR: (*INFO_SENSORS, *TIME_SENSORS), + } + for platform, descriptions in platform_descriptions.items(): + for description in descriptions: + new_unique_id = f"{new_prefix}-{description.key}" + old_unique_id = f"{old_prefix}_{description.key}" + if entity_id := ent_reg.async_get_entity_id( + platform, DOMAIN, old_unique_id + ): + ent_reg.async_update_entity(entity_id, new_unique_id=new_unique_id) diff --git a/homeassistant/components/jewish_calendar/binary_sensor.py b/homeassistant/components/jewish_calendar/binary_sensor.py index 4982016ad66..c28dee88cf5 100644 --- a/homeassistant/components/jewish_calendar/binary_sensor.py +++ b/homeassistant/components/jewish_calendar/binary_sensor.py @@ -70,10 +70,10 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Jewish Calendar binary sensors.""" + entry = hass.data[DOMAIN][config_entry.entry_id] + async_add_entities( - JewishCalendarBinarySensor( - hass.data[DOMAIN][config_entry.entry_id], description - ) + JewishCalendarBinarySensor(config_entry.entry_id, entry, description) for description in BINARY_SENSORS ) @@ -86,13 +86,14 @@ class JewishCalendarBinarySensor(BinarySensorEntity): def __init__( self, + entry_id: str, data: dict[str, Any], description: JewishCalendarBinarySensorEntityDescription, ) -> None: """Initialize the binary sensor.""" self.entity_description = description self._attr_name = f"{DEFAULT_NAME} {description.name}" - self._attr_unique_id = f'{data["prefix"]}_{description.key}' + self._attr_unique_id = f"{entry_id}-{description.key}" self._location = data[CONF_LOCATION] self._hebrew = data[CONF_LANGUAGE] == "hebrew" self._candle_lighting_offset = data[CONF_CANDLE_LIGHT_MINUTES] diff --git a/homeassistant/components/jewish_calendar/sensor.py b/homeassistant/components/jewish_calendar/sensor.py index d2fa872936c..90e504fe8fd 100644 --- a/homeassistant/components/jewish_calendar/sensor.py +++ b/homeassistant/components/jewish_calendar/sensor.py @@ -155,9 +155,13 @@ async def async_setup_entry( ) -> None: """Set up the Jewish calendar sensors .""" entry = hass.data[DOMAIN][config_entry.entry_id] - sensors = [JewishCalendarSensor(entry, description) for description in INFO_SENSORS] + sensors = [ + JewishCalendarSensor(config_entry.entry_id, entry, description) + for description in INFO_SENSORS + ] sensors.extend( - JewishCalendarTimeSensor(entry, description) for description in TIME_SENSORS + JewishCalendarTimeSensor(config_entry.entry_id, entry, description) + for description in TIME_SENSORS ) async_add_entities(sensors) @@ -168,13 +172,14 @@ class JewishCalendarSensor(SensorEntity): def __init__( self, + entry_id: str, data: dict[str, Any], description: SensorEntityDescription, ) -> None: """Initialize the Jewish calendar sensor.""" self.entity_description = description self._attr_name = f"{DEFAULT_NAME} {description.name}" - self._attr_unique_id = f'{data["prefix"]}_{description.key}' + self._attr_unique_id = f"{entry_id}-{description.key}" self._location = data[CONF_LOCATION] self._hebrew = data[CONF_LANGUAGE] == "hebrew" self._candle_lighting_offset = data[CONF_CANDLE_LIGHT_MINUTES] diff --git a/tests/components/jewish_calendar/test_config_flow.py b/tests/components/jewish_calendar/test_config_flow.py index ef16742d8d0..55c2f39b7eb 100644 --- a/tests/components/jewish_calendar/test_config_flow.py +++ b/tests/components/jewish_calendar/test_config_flow.py @@ -93,6 +93,7 @@ async def test_import_with_options(hass: HomeAssistant) -> None: } } + # Simulate HomeAssistant setting up the component assert await async_setup_component(hass, DOMAIN, conf.copy()) await hass.async_block_till_done() diff --git a/tests/components/jewish_calendar/test_init.py b/tests/components/jewish_calendar/test_init.py new file mode 100644 index 00000000000..49dad98fa89 --- /dev/null +++ b/tests/components/jewish_calendar/test_init.py @@ -0,0 +1,74 @@ +"""Tests for the Jewish Calendar component's init.""" + +from hdate import Location + +from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSORS +from homeassistant.components.jewish_calendar import get_unique_prefix +from homeassistant.components.jewish_calendar.const import ( + CONF_CANDLE_LIGHT_MINUTES, + CONF_DIASPORA, + CONF_HAVDALAH_OFFSET_MINUTES, + DEFAULT_DIASPORA, + DEFAULT_LANGUAGE, + DOMAIN, +) +from homeassistant.const import CONF_LANGUAGE, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME +from homeassistant.core import HomeAssistant +import homeassistant.helpers.entity_registry as er +from homeassistant.setup import async_setup_component + + +async def test_import_unique_id_migration(hass: HomeAssistant) -> None: + """Test unique_id migration.""" + yaml_conf = { + DOMAIN: { + CONF_NAME: "test", + CONF_DIASPORA: DEFAULT_DIASPORA, + CONF_LANGUAGE: DEFAULT_LANGUAGE, + CONF_CANDLE_LIGHT_MINUTES: 20, + CONF_HAVDALAH_OFFSET_MINUTES: 50, + CONF_LATITUDE: 31.76, + CONF_LONGITUDE: 35.235, + } + } + + # Create an entry in the entity registry with the data from conf + ent_reg = er.async_get(hass) + location = Location( + latitude=yaml_conf[DOMAIN][CONF_LATITUDE], + longitude=yaml_conf[DOMAIN][CONF_LONGITUDE], + timezone=hass.config.time_zone, + altitude=hass.config.elevation, + diaspora=DEFAULT_DIASPORA, + ) + old_prefix = get_unique_prefix(location, DEFAULT_LANGUAGE, 20, 50) + sample_entity = ent_reg.async_get_or_create( + BINARY_SENSORS, + DOMAIN, + unique_id=f"{old_prefix}_erev_shabbat_hag", + suggested_object_id=f"{DOMAIN}_erev_shabbat_hag", + ) + # Save the existing unique_id, DEFAULT_LANGUAGE should be part of it + old_unique_id = sample_entity.unique_id + assert DEFAULT_LANGUAGE in old_unique_id + + # Simulate HomeAssistant setting up the component + assert await async_setup_component(hass, DOMAIN, yaml_conf.copy()) + await hass.async_block_till_done() + + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + assert entries[0].data == yaml_conf[DOMAIN] + + # Assert that the unique_id was updated + new_unique_id = ent_reg.async_get(sample_entity.entity_id).unique_id + assert new_unique_id != old_unique_id + assert DEFAULT_LANGUAGE not in new_unique_id + + # Confirm that when the component is reloaded, the unique_id is not changed + assert ent_reg.async_get(sample_entity.entity_id).unique_id == new_unique_id + + # Confirm that all the unique_ids are prefixed correctly + await hass.config_entries.async_reload(entries[0].entry_id) + er_entries = er.async_entries_for_config_entry(ent_reg, entries[0].entry_id) + assert all(entry.unique_id.startswith(entries[0].entry_id) for entry in er_entries) From ae3741c364526fddf4795033aadd7851e79b9b35 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 30 May 2024 12:53:42 -0400 Subject: [PATCH 0040/1445] Intent script: allow setting description and platforms (#118500) * Add description to intent_script * Allow setting platforms --- .../components/intent_script/__init__.py | 7 +++++- tests/components/intent_script/test_init.py | 23 +++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/intent_script/__init__.py b/homeassistant/components/intent_script/__init__.py index 63b37c08950..d6fbb1edd80 100644 --- a/homeassistant/components/intent_script/__init__.py +++ b/homeassistant/components/intent_script/__init__.py @@ -8,7 +8,7 @@ from typing import Any, TypedDict import voluptuous as vol from homeassistant.components.script import CONF_MODE -from homeassistant.const import CONF_TYPE, SERVICE_RELOAD +from homeassistant.const import CONF_DESCRIPTION, CONF_TYPE, SERVICE_RELOAD from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.helpers import ( config_validation as cv, @@ -24,6 +24,7 @@ _LOGGER = logging.getLogger(__name__) DOMAIN = "intent_script" +CONF_PLATFORMS = "platforms" CONF_INTENTS = "intents" CONF_SPEECH = "speech" CONF_REPROMPT = "reprompt" @@ -41,6 +42,8 @@ CONFIG_SCHEMA = vol.Schema( { DOMAIN: { cv.string: { + vol.Optional(CONF_DESCRIPTION): cv.string, + vol.Optional(CONF_PLATFORMS): vol.All([cv.string], vol.Coerce(set)), vol.Optional(CONF_ACTION): cv.SCRIPT_SCHEMA, vol.Optional( CONF_ASYNC_ACTION, default=DEFAULT_CONF_ASYNC_ACTION @@ -146,6 +149,8 @@ class ScriptIntentHandler(intent.IntentHandler): """Initialize the script intent handler.""" self.intent_type = intent_type self.config = config + self.description = config.get(CONF_DESCRIPTION) + self.platforms = config.get(CONF_PLATFORMS) async def async_handle(self, intent_obj: intent.Intent) -> intent.IntentResponse: """Handle the intent.""" diff --git a/tests/components/intent_script/test_init.py b/tests/components/intent_script/test_init.py index 14e5dd62d51..5f4c7b97b63 100644 --- a/tests/components/intent_script/test_init.py +++ b/tests/components/intent_script/test_init.py @@ -22,6 +22,8 @@ async def test_intent_script(hass: HomeAssistant) -> None: { "intent_script": { "HelloWorld": { + "description": "Intent to control a test service.", + "platforms": ["switch"], "action": { "service": "test.service", "data_template": {"hello": "{{ name }}"}, @@ -36,6 +38,17 @@ async def test_intent_script(hass: HomeAssistant) -> None: }, ) + handlers = [ + intent_handler + for intent_handler in intent.async_get(hass) + if intent_handler.intent_type == "HelloWorld" + ] + + assert len(handlers) == 1 + handler = handlers[0] + assert handler.description == "Intent to control a test service." + assert handler.platforms == {"switch"} + response = await intent.async_handle( hass, "test", "HelloWorld", {"name": {"value": "Paulus"}} ) @@ -78,6 +91,16 @@ async def test_intent_script_wait_response(hass: HomeAssistant) -> None: }, ) + handlers = [ + intent_handler + for intent_handler in intent.async_get(hass) + if intent_handler.intent_type == "HelloWorldWaitResponse" + ] + + assert len(handlers) == 1 + handler = handlers[0] + assert handler.platforms is None + response = await intent.async_handle( hass, "test", "HelloWorldWaitResponse", {"name": {"value": "Paulus"}} ) From 80588d9c67a14fcc5fecbbf5ed33644923742575 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 30 May 2024 12:53:50 -0400 Subject: [PATCH 0041/1445] Ignore the toggle intent (#118491) --- homeassistant/helpers/llm.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/helpers/llm.py b/homeassistant/helpers/llm.py index 535e2af4d04..b749ff23da3 100644 --- a/homeassistant/helpers/llm.py +++ b/homeassistant/helpers/llm.py @@ -206,10 +206,11 @@ class AssistAPI(API): """API exposing Assist API to LLMs.""" IGNORE_INTENTS = { - intent.INTENT_NEVERMIND, - intent.INTENT_GET_STATE, - INTENT_GET_WEATHER, INTENT_GET_TEMPERATURE, + INTENT_GET_WEATHER, + intent.INTENT_GET_STATE, + intent.INTENT_NEVERMIND, + intent.INTENT_TOGGLE, } def __init__(self, hass: HomeAssistant) -> None: From 34df76776290b810b2dcba021ca09174f58fdaf5 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Thu, 30 May 2024 19:11:19 +0200 Subject: [PATCH 0042/1445] Fix blocking call in holiday (#118496) --- homeassistant/components/holiday/calendar.py | 31 +++++++++++++------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/holiday/calendar.py b/homeassistant/components/holiday/calendar.py index 83988502d18..f56f4f90831 100644 --- a/homeassistant/components/holiday/calendar.py +++ b/homeassistant/components/holiday/calendar.py @@ -18,16 +18,10 @@ from homeassistant.util import dt as dt_util from .const import CONF_PROVINCE, DOMAIN -async def async_setup_entry( - hass: HomeAssistant, - config_entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up the Holiday Calendar config entry.""" - country: str = config_entry.data[CONF_COUNTRY] - province: str | None = config_entry.data.get(CONF_PROVINCE) - language = hass.config.language - +def _get_obj_holidays_and_language( + country: str, province: str | None, language: str +) -> tuple[HolidayBase, str]: + """Get the object for the requested country and year.""" obj_holidays = country_holidays( country, subdiv=province, @@ -58,6 +52,23 @@ async def async_setup_entry( ) language = default_language + return (obj_holidays, language) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Holiday Calendar config entry.""" + country: str = config_entry.data[CONF_COUNTRY] + province: str | None = config_entry.data.get(CONF_PROVINCE) + language = hass.config.language + + obj_holidays, language = await hass.async_add_executor_job( + _get_obj_holidays_and_language, country, province, language + ) + async_add_entities( [ HolidayCalendarEntity( From 796d940f2f8ac7da3996a9213bd3dfb49674118e Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Thu, 30 May 2024 19:14:54 +0200 Subject: [PATCH 0043/1445] Fix group platform dependencies (#118499) --- homeassistant/components/group/manifest.json | 9 ++++ tests/components/group/test_init.py | 55 +++++++++++++++++--- 2 files changed, 56 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/group/manifest.json b/homeassistant/components/group/manifest.json index 7ead19414af..d86fc4ba622 100644 --- a/homeassistant/components/group/manifest.json +++ b/homeassistant/components/group/manifest.json @@ -1,6 +1,15 @@ { "domain": "group", "name": "Group", + "after_dependencies": [ + "alarm_control_panel", + "climate", + "device_tracker", + "person", + "plant", + "vacuum", + "water_heater" + ], "codeowners": ["@home-assistant/core"], "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/group", diff --git a/tests/components/group/test_init.py b/tests/components/group/test_init.py index 4f928e0a8c2..e2e618002ac 100644 --- a/tests/components/group/test_init.py +++ b/tests/components/group/test_init.py @@ -1487,28 +1487,67 @@ async def test_group_vacuum_on(hass: HomeAssistant) -> None: assert hass.states.get("group.group_zero").state == STATE_ON -async def test_device_tracker_not_home(hass: HomeAssistant) -> None: +@pytest.mark.parametrize( + ("entity_state_list", "group_state"), + [ + ( + { + "device_tracker.one": "not_home", + "device_tracker.two": "not_home", + "device_tracker.three": "not_home", + }, + "not_home", + ), + ( + { + "device_tracker.one": "home", + "device_tracker.two": "not_home", + "device_tracker.three": "not_home", + }, + "home", + ), + ( + { + "device_tracker.one": "home", + "device_tracker.two": "elsewhere", + "device_tracker.three": "not_home", + }, + "home", + ), + ( + { + "device_tracker.one": "not_home", + "device_tracker.two": "elsewhere", + "device_tracker.three": "not_home", + }, + "not_home", + ), + ], +) +async def test_device_tracker_or_person_not_home( + hass: HomeAssistant, + entity_state_list: dict[str, str], + group_state: str, +) -> None: """Test group of device_tracker not_home.""" await async_setup_component(hass, "device_tracker", {}) + await async_setup_component(hass, "person", {}) await hass.async_block_till_done() - hass.states.async_set("device_tracker.one", "not_home") - hass.states.async_set("device_tracker.two", "not_home") - hass.states.async_set("device_tracker.three", "not_home") + for entity_id, state in entity_state_list.items(): + hass.states.async_set(entity_id, state) assert await async_setup_component( hass, "group", { "group": { - "group_zero": { - "entities": "device_tracker.one, device_tracker.two, device_tracker.three" - }, + "group_zero": {"entities": ", ".join(entity_state_list)}, } }, ) await hass.async_block_till_done() - assert hass.states.get("group.group_zero").state == "not_home" + assert hass.states.get("group.group_zero").state == group_state async def test_light_removed(hass: HomeAssistant) -> None: From f1465baadad78ab3e262561753eb662c58e10bd7 Mon Sep 17 00:00:00 2001 From: dontinelli <73341522+dontinelli@users.noreply.github.com> Date: Thu, 30 May 2024 19:18:48 +0200 Subject: [PATCH 0044/1445] Adjustment of unit of measurement for light (#116695) --- homeassistant/components/fyta/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/fyta/sensor.py b/homeassistant/components/fyta/sensor.py index c3e90cef28e..3c7ed35746a 100644 --- a/homeassistant/components/fyta/sensor.py +++ b/homeassistant/components/fyta/sensor.py @@ -93,7 +93,7 @@ SENSORS: Final[list[FytaSensorEntityDescription]] = [ FytaSensorEntityDescription( key="light", translation_key="light", - native_unit_of_measurement="mol/d", + native_unit_of_measurement="μmol/s⋅m²", state_class=SensorStateClass.MEASUREMENT, ), FytaSensorEntityDescription( From a3fcd6b32ff08e2edff6a9203dba1bcdd87911a1 Mon Sep 17 00:00:00 2001 From: David Bonnes Date: Thu, 30 May 2024 18:23:58 +0100 Subject: [PATCH 0045/1445] Fix evohome so it doesn't retrieve schedules unnecessarily (#118478) --- homeassistant/components/evohome/__init__.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/evohome/__init__.py b/homeassistant/components/evohome/__init__.py index 33f7e3200e1..133851ba1ea 100644 --- a/homeassistant/components/evohome/__init__.py +++ b/homeassistant/components/evohome/__init__.py @@ -7,7 +7,7 @@ others. from __future__ import annotations from collections.abc import Awaitable -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from http import HTTPStatus import logging import re @@ -452,7 +452,7 @@ class EvoBroker: self.config = client.installation_info[loc_idx][GWS][0][TCS][0] self.tcs: evo.ControlSystem = self._location._gateways[0]._control_systems[0] # noqa: SLF001 - self.tcs_utc_offset = timedelta(minutes=self._location.timeZone[UTC_OFFSET]) + self.loc_utc_offset = timedelta(minutes=self._location.timeZone[UTC_OFFSET]) self.temps: dict[str, float | None] = {} async def save_auth_tokens(self) -> None: @@ -688,7 +688,8 @@ class EvoChild(EvoDevice): if not (schedule := self._schedule.get("DailySchedules")): return {} # no scheduled setpoints when {'DailySchedules': []} - day_time = dt_util.now() + # get dt in the same TZ as the TCS location, so we can compare schedule times + day_time = dt_util.now().astimezone(timezone(self._evo_broker.loc_utc_offset)) day_of_week = day_time.weekday() # for evohome, 0 is Monday time_of_day = day_time.strftime("%H:%M:%S") @@ -702,7 +703,7 @@ class EvoChild(EvoDevice): else: break - # Did the current SP start yesterday? Does the next start SP tomorrow? + # Did this setpoint start yesterday? Does the next setpoint start tomorrow? this_sp_day = -1 if sp_idx == -1 else 0 next_sp_day = 1 if sp_idx + 1 == len(day["Switchpoints"]) else 0 @@ -719,7 +720,7 @@ class EvoChild(EvoDevice): ) assert switchpoint_time_of_day is not None # mypy check dt_aware = _dt_evo_to_aware( - switchpoint_time_of_day, self._evo_broker.tcs_utc_offset + switchpoint_time_of_day, self._evo_broker.loc_utc_offset ) self._setpoints[f"{key}_sp_from"] = dt_aware.isoformat() From 4d27dd0fb06ba49a347d71c3ec8f00a6330bb4d9 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Thu, 30 May 2024 19:24:34 +0200 Subject: [PATCH 0046/1445] Remove not needed hass object from Tag (#118498) --- homeassistant/components/tag/__init__.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/homeassistant/components/tag/__init__.py b/homeassistant/components/tag/__init__.py index ea0c6079e5b..b7c9660ed93 100644 --- a/homeassistant/components/tag/__init__.py +++ b/homeassistant/components/tag/__init__.py @@ -255,7 +255,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: await component.async_add_entities( [ TagEntity( - hass, entity.name or entity.original_name, updated_config[TAG_ID], updated_config.get(LAST_SCANNED), @@ -301,7 +300,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: name = entity.name or entity.original_name entities.append( TagEntity( - hass, name, tag[TAG_ID], tag.get(LAST_SCANNED), @@ -365,14 +363,12 @@ class TagEntity(Entity): def __init__( self, - hass: HomeAssistant, name: str, tag_id: str, last_scanned: str | None, device_id: str | None, ) -> None: """Initialize the Tag event.""" - self.hass = hass self._attr_name = name self._tag_id = tag_id self._attr_unique_id = tag_id From 43c69c71c2d8b251d119fe0051afd9b13844679b Mon Sep 17 00:00:00 2001 From: Ron Weikamp <15732230+ronweikamp@users.noreply.github.com> Date: Thu, 30 May 2024 20:40:23 +0200 Subject: [PATCH 0047/1445] Add time based integration trigger to Riemann sum integral helper sensor (#110685) * Schedule max dt for Riemann Integral sensor * Simplify validation. Dont integrate on change if either old or new state is not numeric. * Add validation to integration methods. Rollback requirement for both states to be always numeric. * Use 0 max_dt for disabling time based updates. * Use docstring instead of pass keyword in abstract methods. * Use time_period config validation for max_dt * Use new_state for scheduling max_dt. Only schedule if new state is numeric. * Use default 0 (None) for max_dt. * Rename max_dt to max_age. * Rollback accidental renaming of different file * Remove unnecessary and nonsensical max value. * Improve new config description * Use DurationSelector in config flow * Rename new config to max_sub_interval * Simplify by checking once for the integration strategy * Use positive time period validation of sub interval in platform schema Co-authored-by: Erik Montnemery * Remove return keyword Co-authored-by: Erik Montnemery * Simplify scheduling of interval exceeded callback Co-authored-by: Erik Montnemery * Improve documentation * Be more clear about when time based integration is disabled. * Update homeassistant/components/integration/config_flow.py --------- Co-authored-by: Erik Montnemery --- .../components/integration/config_flow.py | 4 + homeassistant/components/integration/const.py | 1 + .../components/integration/sensor.py | 116 +++++++++++++- .../components/integration/strings.json | 6 +- .../integration/test_config_flow.py | 7 + tests/components/integration/test_init.py | 1 + tests/components/integration/test_sensor.py | 150 +++++++++++++++++- 7 files changed, 279 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/integration/config_flow.py b/homeassistant/components/integration/config_flow.py index dcf67a6b5ef..28cd280f7f8 100644 --- a/homeassistant/components/integration/config_flow.py +++ b/homeassistant/components/integration/config_flow.py @@ -26,6 +26,7 @@ from homeassistant.helpers.schema_config_entry_flow import ( ) from .const import ( + CONF_MAX_SUB_INTERVAL, CONF_ROUND_DIGITS, CONF_SOURCE_SENSOR, CONF_UNIT_PREFIX, @@ -100,6 +101,9 @@ async def _get_options_dict(handler: SchemaCommonFlowHandler | None) -> dict: min=0, max=6, mode=selector.NumberSelectorMode.BOX ), ), + vol.Optional(CONF_MAX_SUB_INTERVAL): selector.DurationSelector( + selector.DurationSelectorConfig(allow_negative=False) + ), } diff --git a/homeassistant/components/integration/const.py b/homeassistant/components/integration/const.py index b05e4e8f80b..9c3aa04a969 100644 --- a/homeassistant/components/integration/const.py +++ b/homeassistant/components/integration/const.py @@ -7,6 +7,7 @@ CONF_SOURCE_SENSOR = "source" CONF_UNIT_OF_MEASUREMENT = "unit" CONF_UNIT_PREFIX = "unit_prefix" CONF_UNIT_TIME = "unit_time" +CONF_MAX_SUB_INTERVAL = "max_sub_interval" METHOD_TRAPEZOIDAL = "trapezoidal" METHOD_LEFT = "left" diff --git a/homeassistant/components/integration/sensor.py b/homeassistant/components/integration/sensor.py index 9c2e09559af..e935dd5dc14 100644 --- a/homeassistant/components/integration/sensor.py +++ b/homeassistant/components/integration/sensor.py @@ -4,7 +4,9 @@ from __future__ import annotations from abc import ABC, abstractmethod from dataclasses import dataclass +from datetime import UTC, datetime, timedelta from decimal import Decimal, DecimalException, InvalidOperation +from enum import Enum import logging from typing import Any, Final, Self @@ -29,6 +31,7 @@ from homeassistant.const import ( UnitOfTime, ) from homeassistant.core import ( + CALLBACK_TYPE, Event, EventStateChangedData, HomeAssistant, @@ -42,10 +45,11 @@ from homeassistant.helpers import ( ) from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.event import async_track_state_change_event +from homeassistant.helpers.event import async_call_later, async_track_state_change_event from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .const import ( + CONF_MAX_SUB_INTERVAL, CONF_ROUND_DIGITS, CONF_SOURCE_SENSOR, CONF_UNIT_OF_MEASUREMENT, @@ -87,6 +91,7 @@ PLATFORM_SCHEMA = vol.All( vol.Optional(CONF_UNIT_PREFIX): vol.In(UNIT_PREFIXES), vol.Optional(CONF_UNIT_TIME, default=UnitOfTime.HOURS): vol.In(UNIT_TIME), vol.Remove(CONF_UNIT_OF_MEASUREMENT): cv.string, + vol.Optional(CONF_MAX_SUB_INTERVAL): cv.positive_time_period, vol.Optional(CONF_METHOD, default=METHOD_TRAPEZOIDAL): vol.In( INTEGRATION_METHODS ), @@ -176,6 +181,11 @@ _NAME_TO_INTEGRATION_METHOD: dict[str, type[_IntegrationMethod]] = { } +class _IntegrationTrigger(Enum): + StateChange = "state_change" + TimeElapsed = "time_elapsed" + + @dataclass class IntegrationSensorExtraStoredData(SensorExtraStoredData): """Object to hold extra stored data.""" @@ -261,6 +271,11 @@ async def async_setup_entry( # Before we had support for optional selectors, "none" was used for selecting nothing unit_prefix = None + if max_sub_interval_dict := config_entry.options.get(CONF_MAX_SUB_INTERVAL, None): + max_sub_interval = cv.time_period(max_sub_interval_dict) + else: + max_sub_interval = None + round_digits = config_entry.options.get(CONF_ROUND_DIGITS) if round_digits: round_digits = int(round_digits) @@ -274,6 +289,7 @@ async def async_setup_entry( unit_prefix=unit_prefix, unit_time=config_entry.options[CONF_UNIT_TIME], device_info=device_info, + max_sub_interval=max_sub_interval, ) async_add_entities([integral]) @@ -294,6 +310,7 @@ async def async_setup_platform( unique_id=config.get(CONF_UNIQUE_ID), unit_prefix=config.get(CONF_UNIT_PREFIX), unit_time=config[CONF_UNIT_TIME], + max_sub_interval=config.get(CONF_MAX_SUB_INTERVAL), ) async_add_entities([integral]) @@ -315,6 +332,7 @@ class IntegrationSensor(RestoreSensor): unique_id: str | None, unit_prefix: str | None, unit_time: UnitOfTime, + max_sub_interval: timedelta | None, device_info: DeviceInfo | None = None, ) -> None: """Initialize the integration sensor.""" @@ -334,6 +352,14 @@ class IntegrationSensor(RestoreSensor): self._source_entity: str = source_entity self._last_valid_state: Decimal | None = None self._attr_device_info = device_info + self._max_sub_interval: timedelta | None = ( + None # disable time based integration + if max_sub_interval is None or max_sub_interval.total_seconds() == 0 + else max_sub_interval + ) + self._max_sub_interval_exceeded_callback: CALLBACK_TYPE = lambda *args: None + self._last_integration_time: datetime = datetime.now(tz=UTC) + self._last_integration_trigger = _IntegrationTrigger.StateChange self._attr_suggested_display_precision = round_digits or 2 def _calculate_unit(self, source_unit: str) -> str: @@ -421,19 +447,55 @@ class IntegrationSensor(RestoreSensor): self._attr_device_class = state.attributes.get(ATTR_DEVICE_CLASS) self._unit_of_measurement = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + if self._max_sub_interval is not None: + source_state = self.hass.states.get(self._sensor_source_id) + self._schedule_max_sub_interval_exceeded_if_state_is_numeric(source_state) + self.async_on_remove(self._cancel_max_sub_interval_exceeded_callback) + handle_state_change = self._integrate_on_state_change_and_max_sub_interval + else: + handle_state_change = self._integrate_on_state_change_callback + self.async_on_remove( async_track_state_change_event( self.hass, [self._sensor_source_id], - self._handle_state_change, + handle_state_change, ) ) @callback - def _handle_state_change(self, event: Event[EventStateChangedData]) -> None: + def _integrate_on_state_change_and_max_sub_interval( + self, event: Event[EventStateChangedData] + ) -> None: + """Integrate based on state change and time. + + Next to doing the integration based on state change this method cancels and + reschedules time based integration. + """ + self._cancel_max_sub_interval_exceeded_callback() old_state = event.data["old_state"] new_state = event.data["new_state"] + try: + self._integrate_on_state_change(old_state, new_state) + self._last_integration_trigger = _IntegrationTrigger.StateChange + self._last_integration_time = datetime.now(tz=UTC) + finally: + # When max_sub_interval exceeds without state change the source is assumed + # constant with the last known state (new_state). + self._schedule_max_sub_interval_exceeded_if_state_is_numeric(new_state) + @callback + def _integrate_on_state_change_callback( + self, event: Event[EventStateChangedData] + ) -> None: + """Handle the sensor state changes.""" + old_state = event.data["old_state"] + new_state = event.data["new_state"] + return self._integrate_on_state_change(old_state, new_state) + + def _integrate_on_state_change( + self, old_state: State | None, new_state: State | None + ) -> None: if old_state is None or new_state is None: return @@ -451,6 +513,8 @@ class IntegrationSensor(RestoreSensor): elapsed_seconds = Decimal( (new_state.last_updated - old_state.last_updated).total_seconds() + if self._last_integration_trigger == _IntegrationTrigger.StateChange + else (new_state.last_updated - self._last_integration_time).total_seconds() ) area = self._method.calculate_area_with_two_states(elapsed_seconds, *states) @@ -458,6 +522,52 @@ class IntegrationSensor(RestoreSensor): self._update_integral(area) self.async_write_ha_state() + def _schedule_max_sub_interval_exceeded_if_state_is_numeric( + self, source_state: State | None + ) -> None: + """Schedule possible integration using the source state and max_sub_interval. + + The callback reference is stored for possible cancellation if the source state + reports a change before max_sub_interval has passed. + + If the callback is executed, meaning there was no state change reported, the + source_state is assumed constant and integration is done using its value. + """ + if ( + self._max_sub_interval is not None + and source_state is not None + and (source_state_dec := _decimal_state(source_state.state)) + ): + + @callback + def _integrate_on_max_sub_interval_exceeded_callback(now: datetime) -> None: + """Integrate based on time and reschedule.""" + elapsed_seconds = Decimal( + (now - self._last_integration_time).total_seconds() + ) + self._derive_and_set_attributes_from_state(source_state) + area = self._method.calculate_area_with_one_state( + elapsed_seconds, source_state_dec + ) + self._update_integral(area) + self.async_write_ha_state() + + self._last_integration_time = datetime.now(tz=UTC) + self._last_integration_trigger = _IntegrationTrigger.TimeElapsed + + self._schedule_max_sub_interval_exceeded_if_state_is_numeric( + source_state + ) + + self._max_sub_interval_exceeded_callback = async_call_later( + self.hass, + self._max_sub_interval, + _integrate_on_max_sub_interval_exceeded_callback, + ) + + def _cancel_max_sub_interval_exceeded_callback(self) -> None: + self._max_sub_interval_exceeded_callback() + @property def native_value(self) -> Decimal | None: """Return the state of the sensor.""" diff --git a/homeassistant/components/integration/strings.json b/homeassistant/components/integration/strings.json index ed34b0842d5..55d4df1b45e 100644 --- a/homeassistant/components/integration/strings.json +++ b/homeassistant/components/integration/strings.json @@ -11,12 +11,14 @@ "round": "Precision", "source": "Input sensor", "unit_prefix": "Metric prefix", - "unit_time": "Time unit" + "unit_time": "Time unit", + "max_sub_interval": "Max sub-interval" }, "data_description": { "round": "Controls the number of decimal digits in the output.", "unit_prefix": "The output will be scaled according to the selected metric prefix.", - "unit_time": "The output will be scaled according to the selected time unit." + "unit_time": "The output will be scaled according to the selected time unit.", + "max_sub_interval": "Applies time based integration if the source did not change for this duration. Use 0 for no time based updates." } } } diff --git a/tests/components/integration/test_config_flow.py b/tests/components/integration/test_config_flow.py index ede2146185d..0f724158362 100644 --- a/tests/components/integration/test_config_flow.py +++ b/tests/components/integration/test_config_flow.py @@ -36,6 +36,7 @@ async def test_config_flow(hass: HomeAssistant, platform) -> None: "round": 1, "source": input_sensor_entity_id, "unit_time": "min", + "max_sub_interval": {"seconds": 0}, }, ) await hass.async_block_till_done() @@ -49,6 +50,7 @@ async def test_config_flow(hass: HomeAssistant, platform) -> None: "round": 1.0, "source": "sensor.input", "unit_time": "min", + "max_sub_interval": {"seconds": 0}, } assert len(mock_setup_entry.mock_calls) == 1 @@ -60,6 +62,7 @@ async def test_config_flow(hass: HomeAssistant, platform) -> None: "round": 1.0, "source": "sensor.input", "unit_time": "min", + "max_sub_interval": {"seconds": 0}, } assert config_entry.title == "My integration" @@ -89,6 +92,7 @@ async def test_options(hass: HomeAssistant, platform) -> None: "source": "sensor.input", "unit_prefix": "k", "unit_time": "min", + "max_sub_interval": {"minutes": 1}, }, title="My integration", ) @@ -119,6 +123,7 @@ async def test_options(hass: HomeAssistant, platform) -> None: "method": "right", "round": 2.0, "source": "sensor.input", + "max_sub_interval": {"minutes": 1}, }, ) assert result["type"] is FlowResultType.CREATE_ENTRY @@ -129,6 +134,7 @@ async def test_options(hass: HomeAssistant, platform) -> None: "source": "sensor.input", "unit_prefix": "k", "unit_time": "min", + "max_sub_interval": {"minutes": 1}, } assert config_entry.data == {} assert config_entry.options == { @@ -138,6 +144,7 @@ async def test_options(hass: HomeAssistant, platform) -> None: "source": "sensor.input", "unit_prefix": "k", "unit_time": "min", + "max_sub_interval": {"minutes": 1}, } assert config_entry.title == "My integration" diff --git a/tests/components/integration/test_init.py b/tests/components/integration/test_init.py index f92a4a67585..e6ff2a8efb8 100644 --- a/tests/components/integration/test_init.py +++ b/tests/components/integration/test_init.py @@ -30,6 +30,7 @@ async def test_setup_and_remove_config_entry( "source": "sensor.input", "unit_prefix": "k", "unit_time": "min", + "max_sub_interval": {"minutes": 1}, }, title="My integration", ) diff --git a/tests/components/integration/test_sensor.py b/tests/components/integration/test_sensor.py index 555cb44caf5..3fc779423ac 100644 --- a/tests/components/integration/test_sensor.py +++ b/tests/components/integration/test_sensor.py @@ -18,12 +18,17 @@ from homeassistant.const import ( UnitOfTime, ) from homeassistant.core import HomeAssistant, State -from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers import ( + condition, + device_registry as dr, + entity_registry as er, +) from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util from tests.common import ( MockConfigEntry, + async_fire_time_changed, mock_restore_cache, mock_restore_cache_with_extra_data, ) @@ -745,3 +750,146 @@ async def test_device_id( integration_entity = entity_registry.async_get("sensor.integration") assert integration_entity is not None assert integration_entity.device_id == source_entity.device_id + + +def _integral_sensor_config(max_sub_interval: dict[str, int] | None = {"minutes": 1}): + sensor = { + "platform": "integration", + "name": "integration", + "source": "sensor.power", + "method": "right", + } + if max_sub_interval is not None: + sensor["max_sub_interval"] = max_sub_interval + return {"sensor": sensor} + + +async def _setup_integral_sensor( + hass: HomeAssistant, max_sub_interval: dict[str, int] | None = {"minutes": 1} +) -> None: + await async_setup_component( + hass, "sensor", _integral_sensor_config(max_sub_interval=max_sub_interval) + ) + await hass.async_block_till_done() + + +async def _update_source_sensor(hass: HomeAssistant, value: int | str) -> None: + hass.states.async_set( + _integral_sensor_config()["sensor"]["source"], + value, + {ATTR_UNIT_OF_MEASUREMENT: UnitOfPower.KILO_WATT}, + force_update=True, + ) + await hass.async_block_till_done() + + +async def test_on_valid_source_expect_update_on_time( + hass: HomeAssistant, +) -> None: + """Test whether time based integration updates the integral on a valid source.""" + start_time = dt_util.utcnow() + + with freeze_time(start_time) as freezer: + await _setup_integral_sensor(hass) + await _update_source_sensor(hass, 100) + state_before_max_sub_interval_exceeded = hass.states.get("sensor.integration") + + freezer.tick(61) + async_fire_time_changed(hass, dt_util.now()) + await hass.async_block_till_done() + + state = hass.states.get("sensor.integration") + assert ( + condition.async_numeric_state(hass, state_before_max_sub_interval_exceeded) + is False + ) + assert state_before_max_sub_interval_exceeded.state != state.state + assert condition.async_numeric_state(hass, state) is True + assert float(state.state) > 1.69 # approximately 100 * 61 / 3600 + assert float(state.state) < 1.8 + + +async def test_on_unvailable_source_expect_no_update_on_time( + hass: HomeAssistant, +) -> None: + """Test whether time based integration handles unavailability of the source properly.""" + + start_time = dt_util.utcnow() + with freeze_time(start_time) as freezer: + await _setup_integral_sensor(hass) + await _update_source_sensor(hass, 100) + freezer.tick(61) + async_fire_time_changed(hass, dt_util.now()) + await hass.async_block_till_done() + + state = hass.states.get("sensor.integration") + assert condition.async_numeric_state(hass, state) is True + + await _update_source_sensor(hass, STATE_UNAVAILABLE) + await hass.async_block_till_done() + + freezer.tick(61) + async_fire_time_changed(hass, dt_util.now()) + await hass.async_block_till_done() + + state = hass.states.get("sensor.integration") + assert condition.state(hass, state, STATE_UNAVAILABLE) is True + + +async def test_on_statechanges_source_expect_no_update_on_time( + hass: HomeAssistant, +) -> None: + """Test whether state changes cancel time based integration.""" + + start_time = dt_util.utcnow() + with freeze_time(start_time) as freezer: + await _setup_integral_sensor(hass) + await _update_source_sensor(hass, 100) + + freezer.tick(30) + await hass.async_block_till_done() + await _update_source_sensor(hass, 101) + + state_after_30s = hass.states.get("sensor.integration") + assert condition.async_numeric_state(hass, state_after_30s) is True + + freezer.tick(35) + async_fire_time_changed(hass, dt_util.now()) + await hass.async_block_till_done() + state_after_65s = hass.states.get("sensor.integration") + assert (dt_util.now() - start_time).total_seconds() > 60 + # No state change because the timer was cancelled because of an update after 30s + assert state_after_65s == state_after_30s + + freezer.tick(35) + async_fire_time_changed(hass, dt_util.now()) + await hass.async_block_till_done() + state_after_105s = hass.states.get("sensor.integration") + # Update based on time + assert float(state_after_105s.state) > float(state_after_65s.state) + + +async def test_on_no_max_sub_interval_expect_no_timebased_updates( + hass: HomeAssistant, +) -> None: + """Test whether integratal is not updated by time when max_sub_interval is not configured.""" + + start_time = dt_util.utcnow() + with freeze_time(start_time) as freezer: + await _setup_integral_sensor(hass, max_sub_interval=None) + await _update_source_sensor(hass, 100) + await hass.async_block_till_done() + await _update_source_sensor(hass, 101) + await hass.async_block_till_done() + + state_after_last_state_change = hass.states.get("sensor.integration") + + assert ( + condition.async_numeric_state(hass, state_after_last_state_change) is True + ) + + freezer.tick(100) + async_fire_time_changed(hass, dt_util.now()) + await hass.async_block_till_done() + state_after_100s = hass.states.get("sensor.integration") + assert state_after_100s == state_after_last_state_change From 822273a6a3f71362ef5287737a3ffea7bd00aae2 Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Thu, 30 May 2024 19:42:48 +0100 Subject: [PATCH 0048/1445] Add support for V2C Trydan 2.1.7 (#117147) * Support for firmware 2.1.7 * add device ID as unique_id * add device ID as unique_id * add test device id as unique_id * backward compatibility * move outside try * Sensor return type Co-authored-by: Joost Lekkerkerker * not needed * make slave error enum state * fix enum * Update homeassistant/components/v2c/sensor.py Co-authored-by: Joost Lekkerkerker * Update homeassistant/components/v2c/strings.json Co-authored-by: Joost Lekkerkerker * Update homeassistant/components/v2c/strings.json Co-authored-by: Joost Lekkerkerker * simplify tests * fix misspellings from upstream library * add sensor tests * just enough coverage for enum sensor * Refactor V2C tests (#117264) * Refactor V2C tests * fix rebase issues * ruff * review * fix https://github.com/home-assistant/core/issues/117296 --------- Co-authored-by: Joost Lekkerkerker --- .coveragerc | 1 - homeassistant/components/v2c/__init__.py | 3 + homeassistant/components/v2c/config_flow.py | 7 +- homeassistant/components/v2c/icons.json | 6 + homeassistant/components/v2c/manifest.json | 2 +- homeassistant/components/v2c/sensor.py | 25 +- homeassistant/components/v2c/strings.json | 46 ++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/v2c/conftest.py | 1 + .../components/v2c/snapshots/test_sensor.ambr | 458 ++++++++++++++++++ tests/components/v2c/test_sensor.py | 40 ++ 12 files changed, 586 insertions(+), 7 deletions(-) diff --git a/.coveragerc b/.coveragerc index 0ef8c5dfe29..331359c5d0b 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1528,7 +1528,6 @@ omit = homeassistant/components/v2c/coordinator.py homeassistant/components/v2c/entity.py homeassistant/components/v2c/number.py - homeassistant/components/v2c/sensor.py homeassistant/components/v2c/switch.py homeassistant/components/vallox/__init__.py homeassistant/components/vallox/coordinator.py diff --git a/homeassistant/components/v2c/__init__.py b/homeassistant/components/v2c/__init__.py index 75d306b392a..b80163742cb 100644 --- a/homeassistant/components/v2c/__init__.py +++ b/homeassistant/components/v2c/__init__.py @@ -31,6 +31,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator + if coordinator.data.ID and entry.unique_id != coordinator.data.ID: + hass.config_entries.async_update_entry(entry, unique_id=coordinator.data.ID) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/v2c/config_flow.py b/homeassistant/components/v2c/config_flow.py index 7a08c34834e..0421d882ee6 100644 --- a/homeassistant/components/v2c/config_flow.py +++ b/homeassistant/components/v2c/config_flow.py @@ -41,13 +41,18 @@ class V2CConfigFlow(ConfigFlow, domain=DOMAIN): ) try: - await evse.get_data() + data = await evse.get_data() + except TrydanError: errors["base"] = "cannot_connect" except Exception: _LOGGER.exception("Unexpected exception") errors["base"] = "unknown" else: + if data.ID: + await self.async_set_unique_id(data.ID) + self._abort_if_unique_id_configured() + return self.async_create_entry( title=f"EVSE {user_input[CONF_HOST]}", data=user_input ) diff --git a/homeassistant/components/v2c/icons.json b/homeassistant/components/v2c/icons.json index 0c0609de347..fa8449135bb 100644 --- a/homeassistant/components/v2c/icons.json +++ b/homeassistant/components/v2c/icons.json @@ -15,6 +15,12 @@ }, "fv_power": { "default": "mdi:solar-power-variant" + }, + "slave_error": { + "default": "mdi:alert" + }, + "battery_power": { + "default": "mdi:home-battery" } }, "switch": { diff --git a/homeassistant/components/v2c/manifest.json b/homeassistant/components/v2c/manifest.json index fb234d726e8..e26bf80a514 100644 --- a/homeassistant/components/v2c/manifest.json +++ b/homeassistant/components/v2c/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/v2c", "iot_class": "local_polling", - "requirements": ["pytrydan==0.6.0"] + "requirements": ["pytrydan==0.6.1"] } diff --git a/homeassistant/components/v2c/sensor.py b/homeassistant/components/v2c/sensor.py index 871dd65aa75..01b89adea4d 100644 --- a/homeassistant/components/v2c/sensor.py +++ b/homeassistant/components/v2c/sensor.py @@ -7,6 +7,7 @@ from dataclasses import dataclass import logging from pytrydan import TrydanData +from pytrydan.models.trydan import SlaveCommunicationState from homeassistant.components.sensor import ( SensorDeviceClass, @@ -18,6 +19,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import UnitOfEnergy, UnitOfPower, UnitOfTime from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import StateType from .const import DOMAIN from .coordinator import V2CUpdateCoordinator @@ -30,9 +32,11 @@ _LOGGER = logging.getLogger(__name__) class V2CSensorEntityDescription(SensorEntityDescription): """Describes an EVSE Power sensor entity.""" - value_fn: Callable[[TrydanData], float] + value_fn: Callable[[TrydanData], StateType] +_SLAVE_ERROR_OPTIONS = [error.name.lower() for error in SlaveCommunicationState] + TRYDAN_SENSORS = ( V2CSensorEntityDescription( key="charge_power", @@ -75,6 +79,23 @@ TRYDAN_SENSORS = ( device_class=SensorDeviceClass.POWER, value_fn=lambda evse_data: evse_data.fv_power, ), + V2CSensorEntityDescription( + key="slave_error", + translation_key="slave_error", + value_fn=lambda evse_data: evse_data.slave_error.name.lower(), + entity_registry_enabled_default=False, + device_class=SensorDeviceClass.ENUM, + options=_SLAVE_ERROR_OPTIONS, + ), + V2CSensorEntityDescription( + key="battery_power", + translation_key="battery_power", + native_unit_of_measurement=UnitOfPower.WATT, + state_class=SensorStateClass.MEASUREMENT, + device_class=SensorDeviceClass.POWER, + value_fn=lambda evse_data: evse_data.battery_power, + entity_registry_enabled_default=False, + ), ) @@ -108,6 +129,6 @@ class V2CSensorBaseEntity(V2CBaseEntity, SensorEntity): self._attr_unique_id = f"{entry_id}_{description.key}" @property - def native_value(self) -> float | None: + def native_value(self) -> StateType: """Return the state of the sensor.""" return self.entity_description.value_fn(self.data) diff --git a/homeassistant/components/v2c/strings.json b/homeassistant/components/v2c/strings.json index a60b61831fd..bafbbe36e0c 100644 --- a/homeassistant/components/v2c/strings.json +++ b/homeassistant/components/v2c/strings.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + }, "step": { "user": { "data": { @@ -47,6 +50,49 @@ }, "fv_power": { "name": "Photovoltaic power" + }, + "battery_power": { + "name": "Battery power" + }, + "slave_error": { + "name": "Slave error", + "state": { + "no_error": "No error", + "communication": "Communication", + "reading": "Reading", + "slave": "Slave", + "waiting_wifi": "Waiting for Wi-Fi", + "waiting_communication": "Waiting communication", + "wrong_ip": "Wrong IP", + "slave_not_found": "Slave not found", + "wrong_slave": "Wrong slave", + "no_response": "No response", + "clamp_not_connected": "Clamp not connected", + "illegal_function": "Illegal function", + "illegal_data_address": "Illegal data address", + "illegal_data_value": "Illegal data value", + "server_device_failure": "Server device failure", + "acknowledge": "Acknowledge", + "server_device_busy": "Server device busy", + "negative_acknowledge": "Negative acknowledge", + "memory_parity_error": "Memory parity error", + "gateway_path_unavailable": "Gateway path unavailable", + "gateway_target_no_resp": "Gateway target no response", + "server_rtu_inactive244_timeout": "Server RTU inactive/timeout", + "invalid_server": "Invalid server", + "crc_error": "CRC error", + "fc_mismatch": "FC mismatch", + "server_id_mismatch": "Server id mismatch", + "packet_length_error": "Packet length error", + "parameter_count_error": "Parameter count error", + "parameter_limit_error": "Parameter limit error", + "request_queue_full": "Request queue full", + "illegal_ip_or_port": "Illegal IP or port", + "ip_connection_failed": "IP connection failed", + "tcp_head_mismatch": "TCP head mismatch", + "empty_message": "Empty message", + "undefined_error": "Undefined error" + } } }, "switch": { diff --git a/requirements_all.txt b/requirements_all.txt index c733f8f4786..5806c031e78 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2352,7 +2352,7 @@ pytradfri[async]==9.0.1 pytrafikverket==0.3.10 # homeassistant.components.v2c -pytrydan==0.6.0 +pytrydan==0.6.1 # homeassistant.components.usb pyudev==0.24.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4a7b30a9942..bcb2ab8ea06 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1831,7 +1831,7 @@ pytradfri[async]==9.0.1 pytrafikverket==0.3.10 # homeassistant.components.v2c -pytrydan==0.6.0 +pytrydan==0.6.1 # homeassistant.components.usb pyudev==0.24.1 diff --git a/tests/components/v2c/conftest.py b/tests/components/v2c/conftest.py index 3508c0596b2..87c11a3ceef 100644 --- a/tests/components/v2c/conftest.py +++ b/tests/components/v2c/conftest.py @@ -48,4 +48,5 @@ def mock_v2c_client() -> Generator[AsyncMock, None, None]: client = mock_client.return_value get_data_json = load_json_object_fixture("get_data.json", DOMAIN) client.get_data.return_value = TrydanData.from_api(get_data_json) + client.firmware_version = get_data_json["FirmwareVersion"] yield client diff --git a/tests/components/v2c/snapshots/test_sensor.ambr b/tests/components/v2c/snapshots/test_sensor.ambr index 2504aa2e7c8..0ef9bfe8429 100644 --- a/tests/components/v2c/snapshots/test_sensor.ambr +++ b/tests/components/v2c/snapshots/test_sensor.ambr @@ -1,4 +1,340 @@ # serializer version: 1 +# name: test_sensor + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'EVSE 1.1.1.1 Photovoltaic power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.evse_1_1_1_1_photovoltaic_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) + list([ + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.evse_1_1_1_1_charge_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': 'mdi:ev-station', + 'original_name': 'Charge power', + 'platform': 'v2c', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'charge_power', + 'unique_id': '45a36e55aaddb2137c5f6602e0c38e72_charge_power', + 'unit_of_measurement': , + }), + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.evse_1_1_1_1_charge_energy', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Charge energy', + 'platform': 'v2c', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'charge_energy', + 'unique_id': '45a36e55aaddb2137c5f6602e0c38e72_charge_energy', + 'unit_of_measurement': , + }), + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.evse_1_1_1_1_charge_time', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Charge time', + 'platform': 'v2c', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'charge_time', + 'unique_id': '45a36e55aaddb2137c5f6602e0c38e72_charge_time', + 'unit_of_measurement': , + }), + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.evse_1_1_1_1_house_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'House power', + 'platform': 'v2c', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'house_power', + 'unique_id': '45a36e55aaddb2137c5f6602e0c38e72_house_power', + 'unit_of_measurement': , + }), + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.evse_1_1_1_1_photovoltaic_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Photovoltaic power', + 'platform': 'v2c', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'fv_power', + 'unique_id': '45a36e55aaddb2137c5f6602e0c38e72_fv_power', + 'unit_of_measurement': , + }), + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'no_error', + 'communication', + 'reading', + 'slave', + 'waiting_wifi', + 'waiting_communication', + 'wrong_ip', + 'slave_not_found', + 'wrong_slave', + 'no_response', + 'clamp_not_connected', + 'illegal_function', + 'illegal_data_address', + 'illegal_data_value', + 'server_device_failure', + 'acknowledge', + 'server_device_busy', + 'negative_acknowledge', + 'memory_parity_error', + 'gateway_path_unavailable', + 'gateway_target_no_resp', + 'server_rtu_inactive244_timeout', + 'invalid_server', + 'crc_error', + 'fc_missmatch', + 'server_id_missmatch', + 'packet_length_error', + 'parameter_count_error', + 'parameter_limit_error', + 'request_queue_full', + 'illegal_ip_or_port', + 'ip_connection_failed', + 'tcp_head_missmatch', + 'empty_message', + 'undefined_error', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': , + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.evse_1_1_1_1_slave_error', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Slave error', + 'platform': 'v2c', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'slave_error', + 'unique_id': '45a36e55aaddb2137c5f6602e0c38e72_slave_error', + 'unit_of_measurement': None, + }), + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': , + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.evse_1_1_1_1_battery_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery power', + 'platform': 'v2c', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'battery_power', + 'unique_id': '45a36e55aaddb2137c5f6602e0c38e72_battery_power', + 'unit_of_measurement': , + }), + ]) +# --- +# name: test_sensor[sensor.evse_1_1_1_1_battery_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.evse_1_1_1_1_battery_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery power', + 'platform': 'v2c', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'battery_power', + 'unique_id': 'da58ee91f38c2406c2a36d0a1a7f8569_battery_power', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.evse_1_1_1_1_battery_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'EVSE 1.1.1.1 Battery power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.evse_1_1_1_1_battery_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- # name: test_sensor[sensor.evse_1_1_1_1_charge_energy-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -255,3 +591,125 @@ 'state': '0.0', }) # --- +# name: test_sensor[sensor.evse_1_1_1_1_slave_error-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'no_error', + 'communication', + 'reading', + 'slave', + 'waiting_wifi', + 'waiting_communication', + 'wrong_ip', + 'slave_not_found', + 'wrong_slave', + 'no_response', + 'clamp_not_connected', + 'illegal_function', + 'illegal_data_address', + 'illegal_data_value', + 'server_device_failure', + 'acknowledge', + 'server_device_busy', + 'negative_acknowledge', + 'memory_parity_error', + 'gateway_path_unavailable', + 'gateway_target_no_resp', + 'server_rtu_inactive244_timeout', + 'invalid_server', + 'crc_error', + 'fc_mismatch', + 'server_id_mismatch', + 'packet_length_error', + 'parameter_count_error', + 'parameter_limit_error', + 'request_queue_full', + 'illegal_ip_or_port', + 'ip_connection_failed', + 'tcp_head_mismatch', + 'empty_message', + 'undefined_error', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.evse_1_1_1_1_slave_error', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Slave error', + 'platform': 'v2c', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'slave_error', + 'unique_id': 'da58ee91f38c2406c2a36d0a1a7f8569_slave_error', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[sensor.evse_1_1_1_1_slave_error-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'EVSE 1.1.1.1 Slave error', + 'options': list([ + 'no_error', + 'communication', + 'reading', + 'slave', + 'waiting_wifi', + 'waiting_communication', + 'wrong_ip', + 'slave_not_found', + 'wrong_slave', + 'no_response', + 'clamp_not_connected', + 'illegal_function', + 'illegal_data_address', + 'illegal_data_value', + 'server_device_failure', + 'acknowledge', + 'server_device_busy', + 'negative_acknowledge', + 'memory_parity_error', + 'gateway_path_unavailable', + 'gateway_target_no_resp', + 'server_rtu_inactive244_timeout', + 'invalid_server', + 'crc_error', + 'fc_mismatch', + 'server_id_mismatch', + 'packet_length_error', + 'parameter_count_error', + 'parameter_limit_error', + 'request_queue_full', + 'illegal_ip_or_port', + 'ip_connection_failed', + 'tcp_head_mismatch', + 'empty_message', + 'undefined_error', + ]), + }), + 'context': , + 'entity_id': 'sensor.evse_1_1_1_1_slave_error', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'waiting_wifi', + }) +# --- diff --git a/tests/components/v2c/test_sensor.py b/tests/components/v2c/test_sensor.py index b30dfd436ff..a4a7fe6ca34 100644 --- a/tests/components/v2c/test_sensor.py +++ b/tests/components/v2c/test_sensor.py @@ -25,3 +25,43 @@ async def test_sensor( with patch("homeassistant.components.v2c.PLATFORMS", [Platform.SENSOR]): await init_integration(hass, mock_config_entry) await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + from homeassistant.components.v2c.sensor import _SLAVE_ERROR_OPTIONS + + assert [ + "no_error", + "communication", + "reading", + "slave", + "waiting_wifi", + "waiting_communication", + "wrong_ip", + "slave_not_found", + "wrong_slave", + "no_response", + "clamp_not_connected", + "illegal_function", + "illegal_data_address", + "illegal_data_value", + "server_device_failure", + "acknowledge", + "server_device_busy", + "negative_acknowledge", + "memory_parity_error", + "gateway_path_unavailable", + "gateway_target_no_resp", + "server_rtu_inactive244_timeout", + "invalid_server", + "crc_error", + "fc_mismatch", + "server_id_mismatch", + "packet_length_error", + "parameter_count_error", + "parameter_limit_error", + "request_queue_full", + "illegal_ip_or_port", + "ip_connection_failed", + "tcp_head_mismatch", + "empty_message", + "undefined_error", + ] == _SLAVE_ERROR_OPTIONS From 1352c4e42786b0ee903dc257c57b582cec587750 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Thu, 30 May 2024 21:42:11 +0200 Subject: [PATCH 0049/1445] Increase test coverage for KNX Climate (#117903) * Increase test coverage fro KNX Climate * fix test type annotation --- tests/components/knx/test_climate.py | 80 ++++++++++++++++++++-------- 1 file changed, 59 insertions(+), 21 deletions(-) diff --git a/tests/components/knx/test_climate.py b/tests/components/knx/test_climate.py index c81a6fccf15..3b286a0cdb9 100644 --- a/tests/components/knx/test_climate.py +++ b/tests/components/knx/test_climate.py @@ -54,11 +54,12 @@ async def test_climate_basic_temperature_set( assert len(events) == 1 -@pytest.mark.parametrize("heat_cool", [False, True]) +@pytest.mark.parametrize("heat_cool_ga", [None, "4/4/4"]) async def test_climate_on_off( - hass: HomeAssistant, knx: KNXTestKit, heat_cool: bool + hass: HomeAssistant, knx: KNXTestKit, heat_cool_ga: str | None ) -> None: """Test KNX climate on/off.""" + on_off_ga = "3/3/3" await knx.setup_integration( { ClimateSchema.PLATFORM: { @@ -66,15 +67,15 @@ async def test_climate_on_off( ClimateSchema.CONF_TEMPERATURE_ADDRESS: "1/2/3", ClimateSchema.CONF_TARGET_TEMPERATURE_ADDRESS: "1/2/4", ClimateSchema.CONF_TARGET_TEMPERATURE_STATE_ADDRESS: "1/2/5", - ClimateSchema.CONF_ON_OFF_ADDRESS: "1/2/8", + ClimateSchema.CONF_ON_OFF_ADDRESS: on_off_ga, ClimateSchema.CONF_ON_OFF_STATE_ADDRESS: "1/2/9", } | ( { - ClimateSchema.CONF_HEAT_COOL_ADDRESS: "1/2/10", + ClimateSchema.CONF_HEAT_COOL_ADDRESS: heat_cool_ga, ClimateSchema.CONF_HEAT_COOL_STATE_ADDRESS: "1/2/11", } - if heat_cool + if heat_cool_ga else {} ) } @@ -82,7 +83,7 @@ async def test_climate_on_off( await hass.async_block_till_done() # read heat/cool state - if heat_cool: + if heat_cool_ga: await knx.assert_read("1/2/11") await knx.receive_response("1/2/11", 0) # cool # read temperature state @@ -102,7 +103,7 @@ async def test_climate_on_off( {"entity_id": "climate.test"}, blocking=True, ) - await knx.assert_write("1/2/8", 0) + await knx.assert_write(on_off_ga, 0) assert hass.states.get("climate.test").state == "off" # turn on @@ -112,8 +113,8 @@ async def test_climate_on_off( {"entity_id": "climate.test"}, blocking=True, ) - await knx.assert_write("1/2/8", 1) - if heat_cool: + await knx.assert_write(on_off_ga, 1) + if heat_cool_ga: # does not fall back to default hvac mode after turn_on assert hass.states.get("climate.test").state == "cool" else: @@ -126,7 +127,7 @@ async def test_climate_on_off( {"entity_id": "climate.test", "hvac_mode": HVACMode.OFF}, blocking=True, ) - await knx.assert_write("1/2/8", 0) + await knx.assert_write(on_off_ga, 0) # set hvac mode to heat await hass.services.async_call( @@ -135,15 +136,19 @@ async def test_climate_on_off( {"entity_id": "climate.test", "hvac_mode": HVACMode.HEAT}, blocking=True, ) - if heat_cool: + if heat_cool_ga: # only set new hvac_mode without changing on/off - actuator shall handle that - await knx.assert_write("1/2/10", 1) + await knx.assert_write(heat_cool_ga, 1) else: - await knx.assert_write("1/2/8", 1) + await knx.assert_write(on_off_ga, 1) -async def test_climate_hvac_mode(hass: HomeAssistant, knx: KNXTestKit) -> None: +@pytest.mark.parametrize("on_off_ga", [None, "4/4/4"]) +async def test_climate_hvac_mode( + hass: HomeAssistant, knx: KNXTestKit, on_off_ga: str | None +) -> None: """Test KNX climate hvac mode.""" + controller_mode_ga = "3/3/3" await knx.setup_integration( { ClimateSchema.PLATFORM: { @@ -151,11 +156,17 @@ async def test_climate_hvac_mode(hass: HomeAssistant, knx: KNXTestKit) -> None: ClimateSchema.CONF_TEMPERATURE_ADDRESS: "1/2/3", ClimateSchema.CONF_TARGET_TEMPERATURE_ADDRESS: "1/2/4", ClimateSchema.CONF_TARGET_TEMPERATURE_STATE_ADDRESS: "1/2/5", - ClimateSchema.CONF_CONTROLLER_MODE_ADDRESS: "1/2/6", + ClimateSchema.CONF_CONTROLLER_MODE_ADDRESS: controller_mode_ga, ClimateSchema.CONF_CONTROLLER_MODE_STATE_ADDRESS: "1/2/7", - ClimateSchema.CONF_ON_OFF_ADDRESS: "1/2/8", ClimateSchema.CONF_OPERATION_MODES: ["Auto"], } + | ( + { + ClimateSchema.CONF_ON_OFF_ADDRESS: on_off_ga, + } + if on_off_ga + else {} + ) } ) @@ -171,23 +182,50 @@ async def test_climate_hvac_mode(hass: HomeAssistant, knx: KNXTestKit) -> None: await knx.assert_read("1/2/5") await knx.receive_response("1/2/5", RAW_FLOAT_22_0) - # turn hvac mode to off + # turn hvac mode to off - set_hvac_mode() doesn't send to on_off if dedicated hvac mode is available await hass.services.async_call( "climate", "set_hvac_mode", {"entity_id": "climate.test", "hvac_mode": HVACMode.OFF}, blocking=True, ) - await knx.assert_write("1/2/6", (0x06,)) + await knx.assert_write(controller_mode_ga, (0x06,)) - # turn hvac on + # set hvac to non default mode await hass.services.async_call( "climate", "set_hvac_mode", - {"entity_id": "climate.test", "hvac_mode": HVACMode.HEAT}, + {"entity_id": "climate.test", "hvac_mode": HVACMode.COOL}, blocking=True, ) - await knx.assert_write("1/2/6", (0x01,)) + await knx.assert_write(controller_mode_ga, (0x03,)) + + # turn off + await hass.services.async_call( + "climate", + "turn_off", + {"entity_id": "climate.test"}, + blocking=True, + ) + if on_off_ga: + await knx.assert_write(on_off_ga, 0) + else: + await knx.assert_write(controller_mode_ga, (0x06,)) + assert hass.states.get("climate.test").state == "off" + + # turn on + await hass.services.async_call( + "climate", + "turn_on", + {"entity_id": "climate.test"}, + blocking=True, + ) + if on_off_ga: + await knx.assert_write(on_off_ga, 1) + else: + # restore last hvac mode + await knx.assert_write(controller_mode_ga, (0x03,)) + assert hass.states.get("climate.test").state == "cool" async def test_climate_preset_mode( From a5dc4cb1c704a21a2ed112fe952969e13a5c06e7 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 30 May 2024 21:57:09 +0200 Subject: [PATCH 0050/1445] Fix incorrect `zeroconf` type hint in tests (#118465) * Fix incorrect `mock_async_zeroconf` type hint * Adjust thread * One more * Fix mock_zeroconf also * Adjust * Adjust --- pylint/plugins/hass_enforce_type_hints.py | 4 ++-- tests/components/homekit/test_homekit.py | 6 +++--- tests/components/otbr/test_init.py | 4 +++- tests/components/thread/test_dataset_store.py | 10 +++++----- tests/components/thread/test_diagnostics.py | 4 ++-- tests/components/thread/test_discovery.py | 18 ++++++++++-------- tests/components/thread/test_websocket_api.py | 6 ++++-- tests/components/zeroconf/test_init.py | 16 +++++++++------- tests/conftest.py | 4 ++-- 9 files changed, 40 insertions(+), 32 deletions(-) diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index 0fc522f46c2..65248ac2493 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -132,7 +132,7 @@ _TEST_FIXTURES: dict[str, list[str] | str] = { "issue_registry": "IssueRegistry", "legacy_auth": "LegacyApiPasswordAuthProvider", "local_auth": "HassAuthProvider", - "mock_async_zeroconf": "None", + "mock_async_zeroconf": "MagicMock", "mock_bleak_scanner_start": "MagicMock", "mock_bluetooth": "None", "mock_bluetooth_adapters": "None", @@ -140,7 +140,7 @@ _TEST_FIXTURES: dict[str, list[str] | str] = { "mock_get_source_ip": "None", "mock_hass_config": "None", "mock_hass_config_yaml": "None", - "mock_zeroconf": "None", + "mock_zeroconf": "MagicMock", "mqtt_client_mock": "MqttMockPahoClient", "mqtt_mock": "MqttMockHAClient", "mqtt_mock_entry": "MqttMockHAClientGenerator", diff --git a/tests/components/homekit/test_homekit.py b/tests/components/homekit/test_homekit.py index 77931bb74f4..55698db9b2d 100644 --- a/tests/components/homekit/test_homekit.py +++ b/tests/components/homekit/test_homekit.py @@ -297,7 +297,7 @@ async def test_homekit_setup( async def test_homekit_setup_ip_address( - hass: HomeAssistant, hk_driver, mock_async_zeroconf: None + hass: HomeAssistant, hk_driver, mock_async_zeroconf: MagicMock ) -> None: """Test setup with given IP address.""" entry = MockConfigEntry( @@ -344,7 +344,7 @@ async def test_homekit_setup_ip_address( async def test_homekit_with_single_advertise_ips( hass: HomeAssistant, hk_driver, - mock_async_zeroconf: None, + mock_async_zeroconf: MagicMock, hass_storage: dict[str, Any], ) -> None: """Test setup with a single advertise ips.""" @@ -379,7 +379,7 @@ async def test_homekit_with_single_advertise_ips( async def test_homekit_with_many_advertise_ips( hass: HomeAssistant, hk_driver, - mock_async_zeroconf: None, + mock_async_zeroconf: MagicMock, hass_storage: dict[str, Any], ) -> None: """Test setup with many advertise ips.""" diff --git a/tests/components/otbr/test_init.py b/tests/components/otbr/test_init.py index 323e8c02f8b..0c56e9ac8da 100644 --- a/tests/components/otbr/test_init.py +++ b/tests/components/otbr/test_init.py @@ -41,7 +41,9 @@ DATASET_NO_CHANNEL = bytes.fromhex( async def test_import_dataset( - hass: HomeAssistant, mock_async_zeroconf: None, issue_registry: ir.IssueRegistry + hass: HomeAssistant, + mock_async_zeroconf: MagicMock, + issue_registry: ir.IssueRegistry, ) -> None: """Test the active dataset is imported at setup.""" add_service_listener_called = asyncio.Event() diff --git a/tests/components/thread/test_dataset_store.py b/tests/components/thread/test_dataset_store.py index 621867ae9cd..4bec9aea011 100644 --- a/tests/components/thread/test_dataset_store.py +++ b/tests/components/thread/test_dataset_store.py @@ -2,7 +2,7 @@ import asyncio from typing import Any -from unittest.mock import ANY, AsyncMock, patch +from unittest.mock import ANY, AsyncMock, MagicMock, patch import pytest from python_otbr_api.tlv_parser import TLVError @@ -710,7 +710,7 @@ async def test_set_preferred_extended_address(hass: HomeAssistant) -> None: async def test_automatically_set_preferred_dataset( - hass: HomeAssistant, mock_async_zeroconf: None + hass: HomeAssistant, mock_async_zeroconf: MagicMock ) -> None: """Test automatically setting the first dataset as the preferred dataset.""" add_service_listener_called = asyncio.Event() @@ -775,7 +775,7 @@ async def test_automatically_set_preferred_dataset( async def test_automatically_set_preferred_dataset_own_and_other_router( - hass: HomeAssistant, mock_async_zeroconf: None + hass: HomeAssistant, mock_async_zeroconf: MagicMock ) -> None: """Test automatically setting the first dataset as the preferred dataset. @@ -854,7 +854,7 @@ async def test_automatically_set_preferred_dataset_own_and_other_router( async def test_automatically_set_preferred_dataset_other_router( - hass: HomeAssistant, mock_async_zeroconf: None + hass: HomeAssistant, mock_async_zeroconf: MagicMock ) -> None: """Test automatically setting the first dataset as the preferred dataset. @@ -922,7 +922,7 @@ async def test_automatically_set_preferred_dataset_other_router( async def test_automatically_set_preferred_dataset_no_router( - hass: HomeAssistant, mock_async_zeroconf: None + hass: HomeAssistant, mock_async_zeroconf: MagicMock ) -> None: """Test automatically setting the first dataset as the preferred dataset. diff --git a/tests/components/thread/test_diagnostics.py b/tests/components/thread/test_diagnostics.py index 15ab0750316..ce86ba3532c 100644 --- a/tests/components/thread/test_diagnostics.py +++ b/tests/components/thread/test_diagnostics.py @@ -1,7 +1,7 @@ """Test the thread websocket API.""" import dataclasses -from unittest.mock import Mock, patch +from unittest.mock import MagicMock, Mock, patch import pytest from syrupy.assertion import SnapshotAssertion @@ -182,7 +182,7 @@ def ndb() -> Mock: async def test_diagnostics( hass: HomeAssistant, - mock_async_zeroconf: None, + mock_async_zeroconf: MagicMock, ndb: Mock, hass_client: ClientSessionGenerator, snapshot: SnapshotAssertion, diff --git a/tests/components/thread/test_discovery.py b/tests/components/thread/test_discovery.py index bdfd0390b9a..d9895aa72b2 100644 --- a/tests/components/thread/test_discovery.py +++ b/tests/components/thread/test_discovery.py @@ -1,6 +1,6 @@ """Test the thread websocket API.""" -from unittest.mock import ANY, AsyncMock, Mock +from unittest.mock import ANY, AsyncMock, MagicMock, Mock import pytest from zeroconf.asyncio import AsyncServiceInfo @@ -24,7 +24,9 @@ from . import ( ) -async def test_discover_routers(hass: HomeAssistant, mock_async_zeroconf: None) -> None: +async def test_discover_routers( + hass: HomeAssistant, mock_async_zeroconf: MagicMock +) -> None: """Test discovering thread routers.""" mock_async_zeroconf.async_add_service_listener = AsyncMock() mock_async_zeroconf.async_remove_service_listener = AsyncMock() @@ -151,7 +153,7 @@ async def test_discover_routers(hass: HomeAssistant, mock_async_zeroconf: None) ], ) async def test_discover_routers_unconfigured( - hass: HomeAssistant, mock_async_zeroconf: None, data, unconfigured + hass: HomeAssistant, mock_async_zeroconf: MagicMock, data, unconfigured ) -> None: """Test discovering thread routers and setting the unconfigured flag.""" mock_async_zeroconf.async_add_service_listener = AsyncMock() @@ -197,7 +199,7 @@ async def test_discover_routers_unconfigured( "data", [ROUTER_DISCOVERY_HASS_BAD_DATA, ROUTER_DISCOVERY_HASS_MISSING_DATA] ) async def test_discover_routers_bad_or_missing_optional_data( - hass: HomeAssistant, mock_async_zeroconf: None, data + hass: HomeAssistant, mock_async_zeroconf: MagicMock, data ) -> None: """Test discovering thread routers with bad or missing vendor mDNS data.""" mock_async_zeroconf.async_add_service_listener = AsyncMock() @@ -247,7 +249,7 @@ async def test_discover_routers_bad_or_missing_optional_data( ], ) async def test_discover_routers_bad_or_missing_mandatory_data( - hass: HomeAssistant, mock_async_zeroconf: None, service + hass: HomeAssistant, mock_async_zeroconf: MagicMock, service ) -> None: """Test discovering thread routers with missing mandatory mDNS data.""" mock_async_zeroconf.async_add_service_listener = AsyncMock() @@ -281,7 +283,7 @@ async def test_discover_routers_bad_or_missing_mandatory_data( async def test_discover_routers_get_service_info_fails( - hass: HomeAssistant, mock_async_zeroconf: None + hass: HomeAssistant, mock_async_zeroconf: MagicMock ) -> None: """Test discovering thread routers with invalid mDNS data.""" mock_async_zeroconf.async_add_service_listener = AsyncMock() @@ -311,7 +313,7 @@ async def test_discover_routers_get_service_info_fails( async def test_discover_routers_update_unchanged( - hass: HomeAssistant, mock_async_zeroconf: None + hass: HomeAssistant, mock_async_zeroconf: MagicMock ) -> None: """Test discovering thread routers with identical mDNS data in update.""" mock_async_zeroconf.async_add_service_listener = AsyncMock() @@ -353,7 +355,7 @@ async def test_discover_routers_update_unchanged( async def test_discover_routers_stop_twice( - hass: HomeAssistant, mock_async_zeroconf: None + hass: HomeAssistant, mock_async_zeroconf: MagicMock ) -> None: """Test discovering thread routers stopping discovery twice.""" mock_async_zeroconf.async_add_service_listener = AsyncMock() diff --git a/tests/components/thread/test_websocket_api.py b/tests/components/thread/test_websocket_api.py index b277dcafcf4..f3390a9d8b8 100644 --- a/tests/components/thread/test_websocket_api.py +++ b/tests/components/thread/test_websocket_api.py @@ -1,6 +1,6 @@ """Test the thread websocket API.""" -from unittest.mock import ANY, AsyncMock +from unittest.mock import ANY, AsyncMock, MagicMock from zeroconf.asyncio import AsyncServiceInfo @@ -315,7 +315,9 @@ async def test_set_preferred_dataset_wrong_id( async def test_discover_routers( - hass: HomeAssistant, hass_ws_client: WebSocketGenerator, mock_async_zeroconf: None + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + mock_async_zeroconf: MagicMock, ) -> None: """Test discovering thread routers.""" mock_async_zeroconf.async_add_service_listener = AsyncMock() diff --git a/tests/components/zeroconf/test_init.py b/tests/components/zeroconf/test_init.py index 6a21212ed6e..a0b2d546dec 100644 --- a/tests/components/zeroconf/test_init.py +++ b/tests/components/zeroconf/test_init.py @@ -1,7 +1,7 @@ """Test Zeroconf component setup process.""" from typing import Any -from unittest.mock import call, patch +from unittest.mock import MagicMock, call, patch import pytest from zeroconf import ( @@ -148,7 +148,7 @@ def get_zeroconf_info_mock_model(model): return mock_zc_info -async def test_setup(hass: HomeAssistant, mock_async_zeroconf: None) -> None: +async def test_setup(hass: HomeAssistant, mock_async_zeroconf: MagicMock) -> None: """Test configured options for a device are loaded via config entry.""" mock_zc = { "_http._tcp.local.": [ @@ -238,7 +238,7 @@ async def test_setup_with_overly_long_url_and_name( async def test_setup_with_defaults( - hass: HomeAssistant, mock_zeroconf: None, mock_async_zeroconf: None + hass: HomeAssistant, mock_zeroconf: MagicMock, mock_async_zeroconf: None ) -> None: """Test default interface config.""" with ( @@ -994,7 +994,9 @@ async def test_info_from_service_can_return_ipv6(hass: HomeAssistant) -> None: assert info.host == "fd11:1111:1111:0:1234:1234:1234:1234" -async def test_get_instance(hass: HomeAssistant, mock_async_zeroconf: None) -> None: +async def test_get_instance( + hass: HomeAssistant, mock_async_zeroconf: MagicMock +) -> None: """Test we get an instance.""" assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) assert await zeroconf.async_get_async_instance(hass) is mock_async_zeroconf @@ -1285,7 +1287,7 @@ async def test_async_detect_interfaces_explicitly_set_ipv6_freebsd( ) -async def test_no_name(hass: HomeAssistant, mock_async_zeroconf: None) -> None: +async def test_no_name(hass: HomeAssistant, mock_async_zeroconf: MagicMock) -> None: """Test fallback to Home for mDNS announcement if the name is missing.""" hass.config.location_name = "" with patch("homeassistant.components.zeroconf.HaZeroconf"): @@ -1299,7 +1301,7 @@ async def test_no_name(hass: HomeAssistant, mock_async_zeroconf: None) -> None: async def test_setup_with_disallowed_characters_in_local_name( - hass: HomeAssistant, mock_async_zeroconf: None, caplog: pytest.LogCaptureFixture + hass: HomeAssistant, mock_async_zeroconf: MagicMock ) -> None: """Test we still setup with disallowed characters in the location name.""" with ( @@ -1323,7 +1325,7 @@ async def test_setup_with_disallowed_characters_in_local_name( async def test_start_with_frontend( - hass: HomeAssistant, mock_async_zeroconf: None + hass: HomeAssistant, mock_async_zeroconf: MagicMock ) -> None: """Test we start with the frontend.""" with patch("homeassistant.components.zeroconf.HaZeroconf"): diff --git a/tests/conftest.py b/tests/conftest.py index 5d992297855..4a33ea0e482 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1190,7 +1190,7 @@ def disable_translations_once(translations_once): @pytest.fixture -def mock_zeroconf() -> Generator[None, None, None]: +def mock_zeroconf() -> Generator[MagicMock, None, None]: """Mock zeroconf.""" from zeroconf import DNSCache # pylint: disable=import-outside-toplevel @@ -1206,7 +1206,7 @@ def mock_zeroconf() -> Generator[None, None, None]: @pytest.fixture -def mock_async_zeroconf(mock_zeroconf: None) -> Generator[None, None, None]: +def mock_async_zeroconf(mock_zeroconf: MagicMock) -> Generator[MagicMock, None, None]: """Mock AsyncZeroconf.""" from zeroconf import DNSCache, Zeroconf # pylint: disable=import-outside-toplevel from zeroconf.asyncio import ( # pylint: disable=import-outside-toplevel From 6d82cfa91a195d577d47961a468dc0aee1f0502a Mon Sep 17 00:00:00 2001 From: tronikos Date: Thu, 30 May 2024 13:29:13 -0700 Subject: [PATCH 0051/1445] Ignore deprecated open and close cover intents for LLMs (#118515) --- homeassistant/components/cover/intent.py | 2 ++ homeassistant/helpers/llm.py | 3 +++ 2 files changed, 5 insertions(+) diff --git a/homeassistant/components/cover/intent.py b/homeassistant/components/cover/intent.py index dc512795c78..f347c8cc104 100644 --- a/homeassistant/components/cover/intent.py +++ b/homeassistant/components/cover/intent.py @@ -19,6 +19,7 @@ async def async_setup_intents(hass: HomeAssistant) -> None: DOMAIN, SERVICE_OPEN_COVER, "Opened {}", + description="Opens a cover", platforms={DOMAIN}, ), ) @@ -29,6 +30,7 @@ async def async_setup_intents(hass: HomeAssistant) -> None: DOMAIN, SERVICE_CLOSE_COVER, "Closed {}", + description="Closes a cover", platforms={DOMAIN}, ), ) diff --git a/homeassistant/helpers/llm.py b/homeassistant/helpers/llm.py index b749ff23da3..ce539de1fd7 100644 --- a/homeassistant/helpers/llm.py +++ b/homeassistant/helpers/llm.py @@ -14,6 +14,7 @@ from homeassistant.components.conversation.trace import ( ConversationTraceEventType, async_conversation_trace_append, ) +from homeassistant.components.cover.intent import INTENT_CLOSE_COVER, INTENT_OPEN_COVER from homeassistant.components.homeassistant.exposed_entities import async_should_expose from homeassistant.components.intent import async_device_supports_timers from homeassistant.components.weather.intent import INTENT_GET_WEATHER @@ -208,6 +209,8 @@ class AssistAPI(API): IGNORE_INTENTS = { INTENT_GET_TEMPERATURE, INTENT_GET_WEATHER, + INTENT_OPEN_COVER, # deprecated + INTENT_CLOSE_COVER, # deprecated intent.INTENT_GET_STATE, intent.INTENT_NEVERMIND, intent.INTENT_TOGGLE, From 2b016d29c9f23be35e7d993f5c6f36b2773857e5 Mon Sep 17 00:00:00 2001 From: "Mr. Bubbles" Date: Thu, 30 May 2024 22:29:28 +0200 Subject: [PATCH 0052/1445] Fix typing and streamline code in One-Time Password integration (#118511) * Fix some issues * some changes --- homeassistant/components/otp/sensor.py | 28 +++++++++----------------- 1 file changed, 9 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/otp/sensor.py b/homeassistant/components/otp/sensor.py index a9b4368d1e6..3a62677dfc2 100644 --- a/homeassistant/components/otp/sensor.py +++ b/homeassistant/components/otp/sensor.py @@ -12,7 +12,7 @@ from homeassistant.const import CONF_NAME, CONF_TOKEN from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType DEFAULT_NAME = "OTP Sensor" @@ -34,8 +34,8 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the OTP sensor.""" - name = config.get(CONF_NAME) - token = config.get(CONF_TOKEN) + name = config[CONF_NAME] + token = config[CONF_TOKEN] async_add_entities([TOTPSensor(name, token)], True) @@ -46,34 +46,24 @@ class TOTPSensor(SensorEntity): _attr_icon = "mdi:update" _attr_should_poll = False + _attr_native_value: StateType = None + _next_expiration: float | None = None - def __init__(self, name, token): + def __init__(self, name: str, token: str) -> None: """Initialize the sensor.""" - self._name = name + self._attr_name = name self._otp = pyotp.TOTP(token) - self._state = None - self._next_expiration = None async def async_added_to_hass(self) -> None: """Handle when an entity is about to be added to Home Assistant.""" self._call_loop() @callback - def _call_loop(self): - self._state = self._otp.now() + def _call_loop(self) -> None: + self._attr_native_value = self._otp.now() self.async_write_ha_state() # Update must occur at even TIME_STEP, e.g. 12:00:00, 12:00:30, # 12:01:00, etc. in order to have synced time (see RFC6238) self._next_expiration = TIME_STEP - (time.time() % TIME_STEP) self.hass.loop.call_later(self._next_expiration, self._call_loop) - - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def native_value(self): - """Return the state of the sensor.""" - return self._state From 5c6753f4c0e888786a5d252b0f2058cc6a4c392d Mon Sep 17 00:00:00 2001 From: G Johansson Date: Thu, 30 May 2024 22:31:02 +0200 Subject: [PATCH 0053/1445] Fix tado non-string unique id for device trackers (#118505) * Fix tado none string unique id for device trackers * Add comment * Fix comment --- homeassistant/components/tado/device_tracker.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/tado/device_tracker.py b/homeassistant/components/tado/device_tracker.py index dea92ae3890..d3996db7faf 100644 --- a/homeassistant/components/tado/device_tracker.py +++ b/homeassistant/components/tado/device_tracker.py @@ -7,6 +7,7 @@ import logging import voluptuous as vol from homeassistant.components.device_tracker import ( + DOMAIN as DEVICE_TRACKER_DOMAIN, PLATFORM_SCHEMA as BASE_PLATFORM_SCHEMA, DeviceScanner, SourceType, @@ -16,6 +17,7 @@ from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, STATE_HOME, STATE_NOT_HOME from homeassistant.core import HomeAssistant, callback from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers import entity_registry as er import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -78,9 +80,20 @@ async def async_setup_entry( ) -> None: """Set up the Tado device scannery entity.""" _LOGGER.debug("Setting up Tado device scanner entity") - tado = hass.data[DOMAIN][entry.entry_id][DATA] + tado: TadoConnector = hass.data[DOMAIN][entry.entry_id][DATA] tracked: set = set() + # Fix non-string unique_id for device trackers + # Can be removed in 2025.1 + entity_registry = er.async_get(hass) + for device_key in tado.data["mobile_device"]: + if entity_id := entity_registry.async_get_entity_id( + DEVICE_TRACKER_DOMAIN, DOMAIN, device_key + ): + entity_registry.async_update_entity( + entity_id, new_unique_id=str(device_key) + ) + @callback def update_devices() -> None: """Update the values of the devices.""" @@ -134,7 +147,7 @@ class TadoDeviceTrackerEntity(TrackerEntity): ) -> None: """Initialize a Tado Device Tracker entity.""" super().__init__() - self._attr_unique_id = device_id + self._attr_unique_id = str(device_id) self._device_id = device_id self._device_name = device_name self._tado = tado From b5ec24ef63284ecbe880e0dbdf8f313df275de8f Mon Sep 17 00:00:00 2001 From: G Johansson Date: Thu, 30 May 2024 22:35:36 +0200 Subject: [PATCH 0054/1445] Fix key issue in config entry options in Openweathermap (#118506) --- homeassistant/components/openweathermap/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/openweathermap/__init__.py b/homeassistant/components/openweathermap/__init__.py index 7b21ae89b96..44c5179f227 100644 --- a/homeassistant/components/openweathermap/__init__.py +++ b/homeassistant/components/openweathermap/__init__.py @@ -101,6 +101,6 @@ async def async_unload_entry( def _get_config_value(config_entry: ConfigEntry, key: str) -> Any: - if config_entry.options: + if config_entry.options and key in config_entry.options: return config_entry.options[key] return config_entry.data[key] From 0d6c7d097348ecf86f0d0cb6db2ba5b1803b1978 Mon Sep 17 00:00:00 2001 From: tronikos Date: Thu, 30 May 2024 14:14:11 -0700 Subject: [PATCH 0055/1445] Fix LLMs asking which area when there is only one device (#118518) * Ignore deprecated open and close cover intents for LLMs * Fix LLMs asking which area when there is only one device * remove unrelated changed * remove unrelated changes --- homeassistant/helpers/llm.py | 2 +- tests/helpers/test_llm.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/helpers/llm.py b/homeassistant/helpers/llm.py index ce539de1fd7..5591c4a8aba 100644 --- a/homeassistant/helpers/llm.py +++ b/homeassistant/helpers/llm.py @@ -282,7 +282,7 @@ class AssistAPI(API): else: prompt.append( "When a user asks to turn on all devices of a specific type, " - "ask user to specify an area." + "ask user to specify an area, unless there is only one device of that type." ) if not tool_context.device_id or not async_device_supports_timers( diff --git a/tests/helpers/test_llm.py b/tests/helpers/test_llm.py index 63c1214dd6d..1c13d643928 100644 --- a/tests/helpers/test_llm.py +++ b/tests/helpers/test_llm.py @@ -432,7 +432,7 @@ async def test_assist_api_prompt( area_prompt = ( "When a user asks to turn on all devices of a specific type, " - "ask user to specify an area." + "ask user to specify an area, unless there is only one device of that type." ) api = await llm.async_get_api(hass, "assist", tool_context) assert api.api_prompt == ( From 272c51fb389cfb2edec0ada32c7412c8579ef56c Mon Sep 17 00:00:00 2001 From: tronikos Date: Thu, 30 May 2024 16:56:06 -0700 Subject: [PATCH 0056/1445] Fix unnecessary single quotes escaping in Google AI (#118522) --- .../conversation.py | 35 +++++++++++++------ homeassistant/helpers/llm.py | 2 +- .../test_conversation.py | 18 +++++++--- 3 files changed, 38 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/google_generative_ai_conversation/conversation.py b/homeassistant/components/google_generative_ai_conversation/conversation.py index f85cf2530dc..e7aaabb912d 100644 --- a/homeassistant/components/google_generative_ai_conversation/conversation.py +++ b/homeassistant/components/google_generative_ai_conversation/conversation.py @@ -8,6 +8,7 @@ import google.ai.generativelanguage as glm from google.api_core.exceptions import GoogleAPICallError import google.generativeai as genai import google.generativeai.types as genai_types +from google.protobuf.json_format import MessageToDict import voluptuous as vol from voluptuous_openapi import convert @@ -105,6 +106,17 @@ def _format_tool(tool: llm.Tool) -> dict[str, Any]: ) +def _adjust_value(value: Any) -> Any: + """Reverse unnecessary single quotes escaping.""" + if isinstance(value, str): + return value.replace("\\'", "'") + if isinstance(value, list): + return [_adjust_value(item) for item in value] + if isinstance(value, dict): + return {k: _adjust_value(v) for k, v in value.items()} + return value + + class GoogleGenerativeAIConversationEntity( conversation.ConversationEntity, conversation.AbstractConversationAgent ): @@ -295,21 +307,22 @@ class GoogleGenerativeAIConversationEntity( response=intent_response, conversation_id=conversation_id ) self.history[conversation_id] = chat.history - tool_calls = [ + function_calls = [ part.function_call for part in chat_response.parts if part.function_call ] - if not tool_calls or not llm_api: + if not function_calls or not llm_api: break tool_responses = [] - for tool_call in tool_calls: - tool_input = llm.ToolInput( - tool_name=tool_call.name, - tool_args=dict(tool_call.args), - ) - LOGGER.debug( - "Tool call: %s(%s)", tool_input.tool_name, tool_input.tool_args - ) + for function_call in function_calls: + tool_call = MessageToDict(function_call._pb) # noqa: SLF001 + tool_name = tool_call["name"] + tool_args = { + key: _adjust_value(value) + for key, value in tool_call["args"].items() + } + LOGGER.debug("Tool call: %s(%s)", tool_name, tool_args) + tool_input = llm.ToolInput(tool_name=tool_name, tool_args=tool_args) try: function_response = await llm_api.async_call_tool(tool_input) except (HomeAssistantError, vol.Invalid) as e: @@ -321,7 +334,7 @@ class GoogleGenerativeAIConversationEntity( tool_responses.append( glm.Part( function_response=glm.FunctionResponse( - name=tool_call.name, response=function_response + name=tool_name, response=function_response ) ) ) diff --git a/homeassistant/helpers/llm.py b/homeassistant/helpers/llm.py index 5591c4a8aba..57b72bc9618 100644 --- a/homeassistant/helpers/llm.py +++ b/homeassistant/helpers/llm.py @@ -140,7 +140,7 @@ class APIInstance: """Call a LLM tool, validate args and return the response.""" async_conversation_trace_append( ConversationTraceEventType.LLM_TOOL_CALL, - {"tool_name": tool_input.tool_name, "tool_args": str(tool_input.tool_args)}, + {"tool_name": tool_input.tool_name, "tool_args": tool_input.tool_args}, ) for tool in self.tools: diff --git a/tests/components/google_generative_ai_conversation/test_conversation.py b/tests/components/google_generative_ai_conversation/test_conversation.py index 4c7f2de5e2e..b282895baef 100644 --- a/tests/components/google_generative_ai_conversation/test_conversation.py +++ b/tests/components/google_generative_ai_conversation/test_conversation.py @@ -3,6 +3,7 @@ from unittest.mock import AsyncMock, MagicMock, patch from freezegun import freeze_time +from google.ai.generativelanguage_v1beta.types.content import FunctionCall from google.api_core.exceptions import GoogleAPICallError import google.generativeai.types as genai_types import pytest @@ -179,8 +180,13 @@ async def test_function_call( chat_response = MagicMock() mock_chat.send_message_async.return_value = chat_response mock_part = MagicMock() - mock_part.function_call.name = "test_tool" - mock_part.function_call.args = {"param1": ["test_value"]} + mock_part.function_call = FunctionCall( + name="test_tool", + args={ + "param1": ["test_value", "param1\\'s value"], + "param2": "param2\\'s value", + }, + ) def tool_call(hass, tool_input, tool_context): mock_part.function_call = None @@ -220,7 +226,10 @@ async def test_function_call( hass, llm.ToolInput( tool_name="test_tool", - tool_args={"param1": ["test_value"]}, + tool_args={ + "param1": ["test_value", "param1's value"], + "param2": "param2's value", + }, ), llm.ToolContext( platform="google_generative_ai_conversation", @@ -279,8 +288,7 @@ async def test_function_exception( chat_response = MagicMock() mock_chat.send_message_async.return_value = chat_response mock_part = MagicMock() - mock_part.function_call.name = "test_tool" - mock_part.function_call.args = {"param1": 1} + mock_part.function_call = FunctionCall(name="test_tool", args={"param1": 1}) def tool_call(hass, tool_input, tool_context): mock_part.function_call = None From 2bd142d3a63a0967878f8d2b9dce0661e74ff9a0 Mon Sep 17 00:00:00 2001 From: tronikos Date: Thu, 30 May 2024 19:03:57 -0700 Subject: [PATCH 0057/1445] Improve LLM prompt (#118520) --- homeassistant/helpers/llm.py | 3 ++- tests/helpers/test_llm.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/helpers/llm.py b/homeassistant/helpers/llm.py index 57b72bc9618..b4b5f9137c4 100644 --- a/homeassistant/helpers/llm.py +++ b/homeassistant/helpers/llm.py @@ -253,8 +253,9 @@ class AssistAPI(API): prompt = [ ( - "Call the intent tools to control Home Assistant. " + "When controlling Home Assistant always call the intent tools. " "Do not pass the domain to the intent tools as a list. " + "Use HassTurnOn to lock and HassTurnOff to unlock a lock. " "When controlling a device, prefer passing just its name and its domain " "(what comes before the dot in its entity id). " "When controlling an area, prefer passing just area name and domain." diff --git a/tests/helpers/test_llm.py b/tests/helpers/test_llm.py index 1c13d643928..355abf2fe5d 100644 --- a/tests/helpers/test_llm.py +++ b/tests/helpers/test_llm.py @@ -422,8 +422,9 @@ async def test_assist_api_prompt( + yaml.dump(exposed_entities) ) first_part_prompt = ( - "Call the intent tools to control Home Assistant. " + "When controlling Home Assistant always call the intent tools. " "Do not pass the domain to the intent tools as a list. " + "Use HassTurnOn to lock and HassTurnOff to unlock a lock. " "When controlling a device, prefer passing just its name and its domain " "(what comes before the dot in its entity id). " "When controlling an area, prefer passing just area name and domain." From eae04bf2e99e71eb90c1d1772acbacfc82b1092f Mon Sep 17 00:00:00 2001 From: Josef Zweck <24647999+zweckj@users.noreply.github.com> Date: Fri, 31 May 2024 04:13:18 +0200 Subject: [PATCH 0058/1445] Add typing for OpenAI client and fallout (#118514) * typing for client and consequences * Update homeassistant/components/openai_conversation/conversation.py --------- Co-authored-by: Paulus Schoutsen --- .../openai_conversation/conversation.py | 75 ++++++++++++++----- .../openai_conversation/test_conversation.py | 2 - 2 files changed, 56 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/openai_conversation/conversation.py b/homeassistant/components/openai_conversation/conversation.py index f4652a1f820..afc5396e0ba 100644 --- a/homeassistant/components/openai_conversation/conversation.py +++ b/homeassistant/components/openai_conversation/conversation.py @@ -1,9 +1,22 @@ """Conversation support for OpenAI.""" import json -from typing import Any, Literal +from typing import Literal import openai +from openai._types import NOT_GIVEN +from openai.types.chat import ( + ChatCompletionAssistantMessageParam, + ChatCompletionMessage, + ChatCompletionMessageParam, + ChatCompletionMessageToolCallParam, + ChatCompletionSystemMessageParam, + ChatCompletionToolMessageParam, + ChatCompletionToolParam, + ChatCompletionUserMessageParam, +) +from openai.types.chat.chat_completion_message_tool_call_param import Function +from openai.types.shared_params import FunctionDefinition import voluptuous as vol from voluptuous_openapi import convert @@ -45,13 +58,12 @@ async def async_setup_entry( async_add_entities([agent]) -def _format_tool(tool: llm.Tool) -> dict[str, Any]: +def _format_tool(tool: llm.Tool) -> ChatCompletionToolParam: """Format tool specification.""" - tool_spec = {"name": tool.name} + tool_spec = FunctionDefinition(name=tool.name, parameters=convert(tool.parameters)) if tool.description: tool_spec["description"] = tool.description - tool_spec["parameters"] = convert(tool.parameters) - return {"type": "function", "function": tool_spec} + return ChatCompletionToolParam(type="function", function=tool_spec) class OpenAIConversationEntity( @@ -65,7 +77,7 @@ class OpenAIConversationEntity( def __init__(self, entry: ConfigEntry) -> None: """Initialize the agent.""" self.entry = entry - self.history: dict[str, list[dict]] = {} + self.history: dict[str, list[ChatCompletionMessageParam]] = {} self._attr_unique_id = entry.entry_id self._attr_device_info = dr.DeviceInfo( identifiers={(DOMAIN, entry.entry_id)}, @@ -100,7 +112,7 @@ class OpenAIConversationEntity( options = self.entry.options intent_response = intent.IntentResponse(language=user_input.language) llm_api: llm.APIInstance | None = None - tools: list[dict[str, Any]] | None = None + tools: list[ChatCompletionToolParam] | None = None if options.get(CONF_LLM_HASS_API): try: @@ -164,16 +176,18 @@ class OpenAIConversationEntity( response=intent_response, conversation_id=conversation_id ) - messages = [{"role": "system", "content": prompt}] + messages = [ChatCompletionSystemMessageParam(role="system", content=prompt)] - messages.append({"role": "user", "content": user_input.text}) + messages.append( + ChatCompletionUserMessageParam(role="user", content=user_input.text) + ) LOGGER.debug("Prompt: %s", messages) trace.async_conversation_trace_append( trace.ConversationTraceEventType.AGENT_DETAIL, {"messages": messages} ) - client = self.hass.data[DOMAIN][self.entry.entry_id] + client: openai.AsyncClient = self.hass.data[DOMAIN][self.entry.entry_id] # To prevent infinite loops, we limit the number of iterations for _iteration in range(MAX_TOOL_ITERATIONS): @@ -181,7 +195,7 @@ class OpenAIConversationEntity( result = await client.chat.completions.create( model=options.get(CONF_CHAT_MODEL, RECOMMENDED_CHAT_MODEL), messages=messages, - tools=tools or None, + tools=tools or NOT_GIVEN, max_tokens=options.get(CONF_MAX_TOKENS, RECOMMENDED_MAX_TOKENS), top_p=options.get(CONF_TOP_P, RECOMMENDED_TOP_P), temperature=options.get(CONF_TEMPERATURE, RECOMMENDED_TEMPERATURE), @@ -199,7 +213,31 @@ class OpenAIConversationEntity( LOGGER.debug("Response %s", result) response = result.choices[0].message - messages.append(response) + + def message_convert( + message: ChatCompletionMessage, + ) -> ChatCompletionMessageParam: + """Convert from class to TypedDict.""" + tool_calls: list[ChatCompletionMessageToolCallParam] = [] + if message.tool_calls: + tool_calls = [ + ChatCompletionMessageToolCallParam( + id=tool_call.id, + function=Function( + arguments=tool_call.function.arguments, + name=tool_call.function.name, + ), + type=tool_call.type, + ) + for tool_call in message.tool_calls + ] + return ChatCompletionAssistantMessageParam( + role=message.role, + tool_calls=tool_calls, + content=message.content, + ) + + messages.append(message_convert(response)) tool_calls = response.tool_calls if not tool_calls or not llm_api: @@ -223,18 +261,17 @@ class OpenAIConversationEntity( LOGGER.debug("Tool response: %s", tool_response) messages.append( - { - "role": "tool", - "tool_call_id": tool_call.id, - "name": tool_call.function.name, - "content": json.dumps(tool_response), - } + ChatCompletionToolMessageParam( + role="tool", + tool_call_id=tool_call.id, + content=json.dumps(tool_response), + ) ) self.history[conversation_id] = messages intent_response = intent.IntentResponse(language=user_input.language) - intent_response.async_set_speech(response.content) + intent_response.async_set_speech(response.content or "") return conversation.ConversationResult( response=intent_response, conversation_id=conversation_id ) diff --git a/tests/components/openai_conversation/test_conversation.py b/tests/components/openai_conversation/test_conversation.py index 0eec14395e5..4d16973ddfc 100644 --- a/tests/components/openai_conversation/test_conversation.py +++ b/tests/components/openai_conversation/test_conversation.py @@ -184,7 +184,6 @@ async def test_function_call( assert mock_create.mock_calls[1][2]["messages"][3] == { "role": "tool", "tool_call_id": "call_AbCdEfGhIjKlMnOpQrStUvWx", - "name": "test_tool", "content": '"Test response"', } mock_tool.async_call.assert_awaited_once_with( @@ -317,7 +316,6 @@ async def test_function_exception( assert mock_create.mock_calls[1][2]["messages"][3] == { "role": "tool", "tool_call_id": "call_AbCdEfGhIjKlMnOpQrStUvWx", - "name": "test_tool", "content": '{"error": "HomeAssistantError", "error_text": "Test tool exception"}', } mock_tool.async_call.assert_awaited_once_with( From 2b7685b71d62971f179c7c8c43ccc6a9e7b45d02 Mon Sep 17 00:00:00 2001 From: tronikos Date: Thu, 30 May 2024 19:13:54 -0700 Subject: [PATCH 0059/1445] Add Google Assistant SDK diagnostics (#118513) --- .../google_assistant_sdk/diagnostics.py | 24 ++++++++++++ script/hassfest/manifest.py | 1 - .../snapshots/test_diagnostics.ambr | 17 +++++++++ .../google_assistant_sdk/test_diagnostics.py | 38 +++++++++++++++++++ 4 files changed, 79 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/google_assistant_sdk/diagnostics.py create mode 100644 tests/components/google_assistant_sdk/snapshots/test_diagnostics.ambr create mode 100644 tests/components/google_assistant_sdk/test_diagnostics.py diff --git a/homeassistant/components/google_assistant_sdk/diagnostics.py b/homeassistant/components/google_assistant_sdk/diagnostics.py new file mode 100644 index 00000000000..eacded4e2e6 --- /dev/null +++ b/homeassistant/components/google_assistant_sdk/diagnostics.py @@ -0,0 +1,24 @@ +"""Diagnostics support for Google Assistant SDK.""" + +from __future__ import annotations + +from typing import Any + +from homeassistant.components.diagnostics import async_redact_data +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +TO_REDACT = {"access_token", "refresh_token"} + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, entry: ConfigEntry +) -> dict[str, Any]: + """Return diagnostics for a config entry.""" + return async_redact_data( + { + "data": entry.data, + "options": entry.options, + }, + TO_REDACT, + ) diff --git a/script/hassfest/manifest.py b/script/hassfest/manifest.py index e92ec00b117..8ff0750250f 100644 --- a/script/hassfest/manifest.py +++ b/script/hassfest/manifest.py @@ -119,7 +119,6 @@ NO_DIAGNOSTICS = [ "dlna_dms", "gdacs", "geonetnz_quakes", - "google_assistant_sdk", "hyperion", # Modbus is excluded because it doesn't have to have a config flow # according to ADR-0010, since it's a protocol integration. This diff --git a/tests/components/google_assistant_sdk/snapshots/test_diagnostics.ambr b/tests/components/google_assistant_sdk/snapshots/test_diagnostics.ambr new file mode 100644 index 00000000000..134bf6e5ad4 --- /dev/null +++ b/tests/components/google_assistant_sdk/snapshots/test_diagnostics.ambr @@ -0,0 +1,17 @@ +# serializer version: 1 +# name: test_diagnostics + dict({ + 'data': dict({ + 'auth_implementation': 'google_assistant_sdk', + 'token': dict({ + 'access_token': '**REDACTED**', + 'expires_at': 1717074000.0, + 'refresh_token': '**REDACTED**', + 'scope': 'https://www.googleapis.com/auth/assistant-sdk-prototype', + }), + }), + 'options': dict({ + 'language_code': 'en-US', + }), + }) +# --- diff --git a/tests/components/google_assistant_sdk/test_diagnostics.py b/tests/components/google_assistant_sdk/test_diagnostics.py new file mode 100644 index 00000000000..cf815c96943 --- /dev/null +++ b/tests/components/google_assistant_sdk/test_diagnostics.py @@ -0,0 +1,38 @@ +"""Tests for the diagnostics data provided by the Google Assistant SDK integration.""" + +from freezegun import freeze_time +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.google_assistant_sdk.const import CONF_LANGUAGE_CODE +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry +from tests.components.diagnostics import get_diagnostics_for_config_entry +from tests.typing import ClientSessionGenerator + + +@pytest.fixture(autouse=True) +def freeze_the_time(): + """Freeze the time.""" + with freeze_time("2024-05-30 12:00:00", tz_offset=0): + yield + + +async def test_diagnostics( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + config_entry: MockConfigEntry, + snapshot: SnapshotAssertion, +) -> None: + """Test diagnostics.""" + config_entry.add_to_hass(hass) + hass.config_entries.async_update_entry( + config_entry, + options={CONF_LANGUAGE_CODE: "en-US"}, + ) + await hass.config_entries.async_setup(config_entry.entry_id) + assert ( + await get_diagnostics_for_config_entry(hass, hass_client, config_entry) + == snapshot + ) From 83e77720e912dda59539bb888b7c10d2ce94b298 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 31 May 2024 04:16:45 +0200 Subject: [PATCH 0060/1445] Improve type hints for mock_bluetooth/enable_bluetooth (#118484) --- tests/components/airthings_ble/conftest.py | 2 +- tests/components/aranet/conftest.py | 2 +- tests/components/bluemaestro/conftest.py | 2 +- tests/components/bluetooth_adapters/conftest.py | 2 +- tests/components/bluetooth_le_tracker/conftest.py | 2 +- tests/components/bthome/conftest.py | 2 +- tests/components/dormakaba_dkey/conftest.py | 2 +- tests/components/eq3btsmart/conftest.py | 2 +- tests/components/esphome/conftest.py | 2 +- tests/components/eufylife_ble/conftest.py | 2 +- tests/components/fjaraskupan/conftest.py | 2 +- tests/components/govee_ble/conftest.py | 2 +- tests/components/ibeacon/test_coordinator.py | 2 +- tests/components/ibeacon/test_device_tracker.py | 2 +- tests/components/ibeacon/test_init.py | 2 +- tests/components/ibeacon/test_sensor.py | 2 +- tests/components/idasen_desk/conftest.py | 6 +++--- tests/components/improv_ble/conftest.py | 2 +- tests/components/inkbird/conftest.py | 2 +- tests/components/kegtron/conftest.py | 2 +- tests/components/keymitt_ble/conftest.py | 2 +- tests/components/lamarzocco/conftest.py | 2 +- tests/components/ld2410_ble/conftest.py | 2 +- tests/components/leaone/conftest.py | 2 +- tests/components/led_ble/conftest.py | 2 +- tests/components/medcom_ble/conftest.py | 2 +- tests/components/melnor/conftest.py | 2 +- tests/components/moat/conftest.py | 2 +- tests/components/mopeka/conftest.py | 2 +- tests/components/oralb/conftest.py | 3 ++- tests/components/qingping/conftest.py | 2 +- tests/components/rapt_ble/conftest.py | 2 +- tests/components/ruuvi_gateway/conftest.py | 2 +- tests/components/ruuvitag_ble/test_config_flow.py | 2 +- tests/components/sensirion_ble/test_config_flow.py | 2 +- tests/components/sensorpro/conftest.py | 2 +- tests/components/sensorpush/conftest.py | 2 +- tests/components/shelly/conftest.py | 2 +- tests/components/snooz/conftest.py | 2 +- tests/components/switchbot/conftest.py | 2 +- tests/components/thermobeacon/conftest.py | 2 +- tests/components/thermopro/conftest.py | 2 +- tests/components/tilt_ble/conftest.py | 2 +- tests/components/xiaomi_ble/conftest.py | 3 ++- tests/components/yalexs_ble/conftest.py | 2 +- 45 files changed, 49 insertions(+), 47 deletions(-) diff --git a/tests/components/airthings_ble/conftest.py b/tests/components/airthings_ble/conftest.py index 3df082c4361..44f68a1c8ae 100644 --- a/tests/components/airthings_ble/conftest.py +++ b/tests/components/airthings_ble/conftest.py @@ -4,5 +4,5 @@ import pytest @pytest.fixture(autouse=True) -def mock_bluetooth(enable_bluetooth): +def mock_bluetooth(enable_bluetooth: None) -> None: """Auto mock bluetooth.""" diff --git a/tests/components/aranet/conftest.py b/tests/components/aranet/conftest.py index fca081d2e2a..da5c3c81404 100644 --- a/tests/components/aranet/conftest.py +++ b/tests/components/aranet/conftest.py @@ -4,5 +4,5 @@ import pytest @pytest.fixture(autouse=True) -def mock_bluetooth(enable_bluetooth): +def mock_bluetooth(enable_bluetooth: None) -> None: """Auto mock bluetooth.""" diff --git a/tests/components/bluemaestro/conftest.py b/tests/components/bluemaestro/conftest.py index e40cf1e30f4..f35ff087ed3 100644 --- a/tests/components/bluemaestro/conftest.py +++ b/tests/components/bluemaestro/conftest.py @@ -4,5 +4,5 @@ import pytest @pytest.fixture(autouse=True) -def mock_bluetooth(enable_bluetooth): +def mock_bluetooth(enable_bluetooth: None) -> None: """Auto mock bluetooth.""" diff --git a/tests/components/bluetooth_adapters/conftest.py b/tests/components/bluetooth_adapters/conftest.py index 9e56959209e..c0a5766d032 100644 --- a/tests/components/bluetooth_adapters/conftest.py +++ b/tests/components/bluetooth_adapters/conftest.py @@ -4,5 +4,5 @@ import pytest @pytest.fixture(autouse=True) -def mock_bluetooth(enable_bluetooth): +def mock_bluetooth(enable_bluetooth: None) -> None: """Auto mock bluetooth.""" diff --git a/tests/components/bluetooth_le_tracker/conftest.py b/tests/components/bluetooth_le_tracker/conftest.py index 9fce8e85ea8..5a839a9d6b8 100644 --- a/tests/components/bluetooth_le_tracker/conftest.py +++ b/tests/components/bluetooth_le_tracker/conftest.py @@ -4,5 +4,5 @@ import pytest @pytest.fixture(autouse=True) -def mock_bluetooth(enable_bluetooth): +def mock_bluetooth(enable_bluetooth: None) -> None: """Auto mock bluetooth.""" diff --git a/tests/components/bthome/conftest.py b/tests/components/bthome/conftest.py index 9fce8e85ea8..5a839a9d6b8 100644 --- a/tests/components/bthome/conftest.py +++ b/tests/components/bthome/conftest.py @@ -4,5 +4,5 @@ import pytest @pytest.fixture(autouse=True) -def mock_bluetooth(enable_bluetooth): +def mock_bluetooth(enable_bluetooth: None) -> None: """Auto mock bluetooth.""" diff --git a/tests/components/dormakaba_dkey/conftest.py b/tests/components/dormakaba_dkey/conftest.py index d911739943f..1530cb82e33 100644 --- a/tests/components/dormakaba_dkey/conftest.py +++ b/tests/components/dormakaba_dkey/conftest.py @@ -4,5 +4,5 @@ import pytest @pytest.fixture(autouse=True) -def mock_bluetooth(enable_bluetooth): +def mock_bluetooth(enable_bluetooth: None) -> None: """Auto mock bluetooth.""" diff --git a/tests/components/eq3btsmart/conftest.py b/tests/components/eq3btsmart/conftest.py index b16c5088044..92f1be29b70 100644 --- a/tests/components/eq3btsmart/conftest.py +++ b/tests/components/eq3btsmart/conftest.py @@ -11,7 +11,7 @@ from tests.components.bluetooth import generate_ble_device @pytest.fixture(autouse=True) -def mock_bluetooth(enable_bluetooth): +def mock_bluetooth(enable_bluetooth: None) -> None: """Auto mock bluetooth.""" diff --git a/tests/components/esphome/conftest.py b/tests/components/esphome/conftest.py index f71b4196be6..7b9b050ddb3 100644 --- a/tests/components/esphome/conftest.py +++ b/tests/components/esphome/conftest.py @@ -41,7 +41,7 @@ from tests.common import MockConfigEntry @pytest.fixture(autouse=True) -def mock_bluetooth(enable_bluetooth): +def mock_bluetooth(enable_bluetooth: None) -> None: """Auto mock bluetooth.""" diff --git a/tests/components/eufylife_ble/conftest.py b/tests/components/eufylife_ble/conftest.py index 18f5a0ec3a1..210f3dbed69 100644 --- a/tests/components/eufylife_ble/conftest.py +++ b/tests/components/eufylife_ble/conftest.py @@ -4,5 +4,5 @@ import pytest @pytest.fixture(autouse=True) -def mock_bluetooth(enable_bluetooth): +def mock_bluetooth(enable_bluetooth: None) -> None: """Auto mock bluetooth.""" diff --git a/tests/components/fjaraskupan/conftest.py b/tests/components/fjaraskupan/conftest.py index 85493157a3c..1f29b086955 100644 --- a/tests/components/fjaraskupan/conftest.py +++ b/tests/components/fjaraskupan/conftest.py @@ -6,5 +6,5 @@ import pytest @pytest.fixture(autouse=True) -def mock_bluetooth(enable_bluetooth): +def mock_bluetooth(enable_bluetooth: None) -> None: """Auto mock bluetooth.""" diff --git a/tests/components/govee_ble/conftest.py b/tests/components/govee_ble/conftest.py index 382854a5a28..0185cd9557f 100644 --- a/tests/components/govee_ble/conftest.py +++ b/tests/components/govee_ble/conftest.py @@ -4,5 +4,5 @@ import pytest @pytest.fixture(autouse=True) -def mock_bluetooth(enable_bluetooth): +def mock_bluetooth(enable_bluetooth: None) -> None: """Auto mock bluetooth.""" diff --git a/tests/components/ibeacon/test_coordinator.py b/tests/components/ibeacon/test_coordinator.py index 0880f745ec2..c9177362f35 100644 --- a/tests/components/ibeacon/test_coordinator.py +++ b/tests/components/ibeacon/test_coordinator.py @@ -40,7 +40,7 @@ from tests.components.bluetooth import ( @pytest.fixture(autouse=True) -def mock_bluetooth(enable_bluetooth): +def mock_bluetooth(enable_bluetooth: None) -> None: """Auto mock bluetooth.""" diff --git a/tests/components/ibeacon/test_device_tracker.py b/tests/components/ibeacon/test_device_tracker.py index 481a1315325..e34cc480cb0 100644 --- a/tests/components/ibeacon/test_device_tracker.py +++ b/tests/components/ibeacon/test_device_tracker.py @@ -42,7 +42,7 @@ from tests.components.bluetooth import ( @pytest.fixture(autouse=True) -def mock_bluetooth(enable_bluetooth): +def mock_bluetooth(enable_bluetooth: None) -> None: """Auto mock bluetooth.""" diff --git a/tests/components/ibeacon/test_init.py b/tests/components/ibeacon/test_init.py index 5a30417efe1..0604b818acd 100644 --- a/tests/components/ibeacon/test_init.py +++ b/tests/components/ibeacon/test_init.py @@ -15,7 +15,7 @@ from tests.typing import WebSocketGenerator @pytest.fixture(autouse=True) -def mock_bluetooth(enable_bluetooth): +def mock_bluetooth(enable_bluetooth: None) -> None: """Auto mock bluetooth.""" diff --git a/tests/components/ibeacon/test_sensor.py b/tests/components/ibeacon/test_sensor.py index fb6322162d4..f4dba57bced 100644 --- a/tests/components/ibeacon/test_sensor.py +++ b/tests/components/ibeacon/test_sensor.py @@ -34,7 +34,7 @@ from tests.components.bluetooth import ( @pytest.fixture(autouse=True) -def mock_bluetooth(enable_bluetooth): +def mock_bluetooth(enable_bluetooth: None) -> None: """Auto mock bluetooth.""" diff --git a/tests/components/idasen_desk/conftest.py b/tests/components/idasen_desk/conftest.py index c621a54cd95..d99409f8bb2 100644 --- a/tests/components/idasen_desk/conftest.py +++ b/tests/components/idasen_desk/conftest.py @@ -1,6 +1,6 @@ """IKEA Idasen Desk fixtures.""" -from collections.abc import Callable +from collections.abc import Callable, Generator from unittest import mock from unittest.mock import AsyncMock, MagicMock @@ -8,12 +8,12 @@ import pytest @pytest.fixture(autouse=True) -def mock_bluetooth(enable_bluetooth): +def mock_bluetooth(enable_bluetooth: None) -> Generator[None, None, None]: """Auto mock bluetooth.""" with mock.patch( "homeassistant.components.idasen_desk.bluetooth.async_ble_device_from_address" ): - yield MagicMock() + yield @pytest.fixture(autouse=False) diff --git a/tests/components/improv_ble/conftest.py b/tests/components/improv_ble/conftest.py index ea548efeb15..3781be341c5 100644 --- a/tests/components/improv_ble/conftest.py +++ b/tests/components/improv_ble/conftest.py @@ -4,5 +4,5 @@ import pytest @pytest.fixture(autouse=True) -def mock_bluetooth(enable_bluetooth): +def mock_bluetooth(enable_bluetooth: None) -> None: """Auto mock bluetooth.""" diff --git a/tests/components/inkbird/conftest.py b/tests/components/inkbird/conftest.py index 3450cb933fe..cb68332dd83 100644 --- a/tests/components/inkbird/conftest.py +++ b/tests/components/inkbird/conftest.py @@ -4,5 +4,5 @@ import pytest @pytest.fixture(autouse=True) -def mock_bluetooth(enable_bluetooth): +def mock_bluetooth(enable_bluetooth: None) -> None: """Auto mock bluetooth.""" diff --git a/tests/components/kegtron/conftest.py b/tests/components/kegtron/conftest.py index 472cadddada..44728e0e5ce 100644 --- a/tests/components/kegtron/conftest.py +++ b/tests/components/kegtron/conftest.py @@ -4,5 +4,5 @@ import pytest @pytest.fixture(autouse=True) -def mock_bluetooth(enable_bluetooth): +def mock_bluetooth(enable_bluetooth: None) -> None: """Auto mock bluetooth.""" diff --git a/tests/components/keymitt_ble/conftest.py b/tests/components/keymitt_ble/conftest.py index 3df082c4361..44f68a1c8ae 100644 --- a/tests/components/keymitt_ble/conftest.py +++ b/tests/components/keymitt_ble/conftest.py @@ -4,5 +4,5 @@ import pytest @pytest.fixture(autouse=True) -def mock_bluetooth(enable_bluetooth): +def mock_bluetooth(enable_bluetooth: None) -> None: """Auto mock bluetooth.""" diff --git a/tests/components/lamarzocco/conftest.py b/tests/components/lamarzocco/conftest.py index d76e44d60af..5c0f344a640 100644 --- a/tests/components/lamarzocco/conftest.py +++ b/tests/components/lamarzocco/conftest.py @@ -133,5 +133,5 @@ def remove_local_connection( @pytest.fixture(autouse=True) -def mock_bluetooth(enable_bluetooth): +def mock_bluetooth(enable_bluetooth: None) -> None: """Auto mock bluetooth.""" diff --git a/tests/components/ld2410_ble/conftest.py b/tests/components/ld2410_ble/conftest.py index 58dca37ce83..3e9b4f872a2 100644 --- a/tests/components/ld2410_ble/conftest.py +++ b/tests/components/ld2410_ble/conftest.py @@ -4,5 +4,5 @@ import pytest @pytest.fixture(autouse=True) -def mock_bluetooth(enable_bluetooth): +def mock_bluetooth(enable_bluetooth: None) -> None: """Auto mock bluetooth.""" diff --git a/tests/components/leaone/conftest.py b/tests/components/leaone/conftest.py index 2f89e80f893..c2bfa61117a 100644 --- a/tests/components/leaone/conftest.py +++ b/tests/components/leaone/conftest.py @@ -4,5 +4,5 @@ import pytest @pytest.fixture(autouse=True) -def mock_bluetooth(enable_bluetooth): +def mock_bluetooth(enable_bluetooth: None) -> None: """Auto mock bluetooth.""" diff --git a/tests/components/led_ble/conftest.py b/tests/components/led_ble/conftest.py index 280eb0d6f17..aaaa561b66e 100644 --- a/tests/components/led_ble/conftest.py +++ b/tests/components/led_ble/conftest.py @@ -4,5 +4,5 @@ import pytest @pytest.fixture(autouse=True) -def mock_bluetooth(enable_bluetooth): +def mock_bluetooth(enable_bluetooth: None) -> None: """Auto mock bluetooth.""" diff --git a/tests/components/medcom_ble/conftest.py b/tests/components/medcom_ble/conftest.py index 7c5b0dad22e..41f797f3e1d 100644 --- a/tests/components/medcom_ble/conftest.py +++ b/tests/components/medcom_ble/conftest.py @@ -4,5 +4,5 @@ import pytest @pytest.fixture(autouse=True) -def mock_bluetooth(enable_bluetooth): +def mock_bluetooth(enable_bluetooth: None) -> None: """Auto mock bluetooth.""" diff --git a/tests/components/melnor/conftest.py b/tests/components/melnor/conftest.py index b75eb370555..d96a04aa3f7 100644 --- a/tests/components/melnor/conftest.py +++ b/tests/components/melnor/conftest.py @@ -57,7 +57,7 @@ FAKE_SERVICE_INFO_2 = BluetoothServiceInfoBleak( @pytest.fixture(autouse=True) -def mock_bluetooth(enable_bluetooth): +def mock_bluetooth(enable_bluetooth: None) -> None: """Auto mock bluetooth.""" diff --git a/tests/components/moat/conftest.py b/tests/components/moat/conftest.py index 1f7f00c8d2f..2161d304d63 100644 --- a/tests/components/moat/conftest.py +++ b/tests/components/moat/conftest.py @@ -4,5 +4,5 @@ import pytest @pytest.fixture(autouse=True) -def mock_bluetooth(enable_bluetooth): +def mock_bluetooth(enable_bluetooth: None) -> None: """Auto mock bluetooth.""" diff --git a/tests/components/mopeka/conftest.py b/tests/components/mopeka/conftest.py index 1d6d0fc7eb7..d231390845e 100644 --- a/tests/components/mopeka/conftest.py +++ b/tests/components/mopeka/conftest.py @@ -4,5 +4,5 @@ import pytest @pytest.fixture(autouse=True) -def mock_bluetooth(enable_bluetooth): +def mock_bluetooth(enable_bluetooth: None) -> None: """Auto mock bluetooth.""" diff --git a/tests/components/oralb/conftest.py b/tests/components/oralb/conftest.py index 690444d3fb1..f119d6b22b3 100644 --- a/tests/components/oralb/conftest.py +++ b/tests/components/oralb/conftest.py @@ -1,5 +1,6 @@ """OralB session fixtures.""" +from collections.abc import Generator from unittest import mock import pytest @@ -44,7 +45,7 @@ class MockBleakClientBattery49(MockBleakClient): @pytest.fixture(autouse=True) -def mock_bluetooth(enable_bluetooth): +def mock_bluetooth(enable_bluetooth: None) -> Generator[None, None, None]: """Auto mock bluetooth.""" with mock.patch( diff --git a/tests/components/qingping/conftest.py b/tests/components/qingping/conftest.py index e74bf38b26d..21667684562 100644 --- a/tests/components/qingping/conftest.py +++ b/tests/components/qingping/conftest.py @@ -4,5 +4,5 @@ import pytest @pytest.fixture(autouse=True) -def mock_bluetooth(enable_bluetooth): +def mock_bluetooth(enable_bluetooth: None) -> None: """Auto mock bluetooth.""" diff --git a/tests/components/rapt_ble/conftest.py b/tests/components/rapt_ble/conftest.py index 4a890eb60f1..9b62f212584 100644 --- a/tests/components/rapt_ble/conftest.py +++ b/tests/components/rapt_ble/conftest.py @@ -4,5 +4,5 @@ import pytest @pytest.fixture(autouse=True) -def mock_bluetooth(enable_bluetooth): +def mock_bluetooth(enable_bluetooth: None) -> None: """Auto mock bluetooth.""" diff --git a/tests/components/ruuvi_gateway/conftest.py b/tests/components/ruuvi_gateway/conftest.py index 6a57ae00b1e..754fda0fd98 100644 --- a/tests/components/ruuvi_gateway/conftest.py +++ b/tests/components/ruuvi_gateway/conftest.py @@ -4,5 +4,5 @@ import pytest @pytest.fixture(autouse=True) -def mock_bluetooth(enable_bluetooth): +def mock_bluetooth(enable_bluetooth: None) -> None: """Auto mock bluetooth.""" diff --git a/tests/components/ruuvitag_ble/test_config_flow.py b/tests/components/ruuvitag_ble/test_config_flow.py index b6c79f1de0e..3414fa34536 100644 --- a/tests/components/ruuvitag_ble/test_config_flow.py +++ b/tests/components/ruuvitag_ble/test_config_flow.py @@ -15,7 +15,7 @@ from tests.common import MockConfigEntry @pytest.fixture(autouse=True) -def mock_bluetooth(enable_bluetooth): +def mock_bluetooth(enable_bluetooth: None) -> None: """Mock bluetooth for all tests in this module.""" diff --git a/tests/components/sensirion_ble/test_config_flow.py b/tests/components/sensirion_ble/test_config_flow.py index 00e92d37118..a94f4f737e2 100644 --- a/tests/components/sensirion_ble/test_config_flow.py +++ b/tests/components/sensirion_ble/test_config_flow.py @@ -19,7 +19,7 @@ from tests.common import MockConfigEntry @pytest.fixture(autouse=True) -def mock_bluetooth(enable_bluetooth): +def mock_bluetooth(enable_bluetooth: None) -> None: """Mock bluetooth for all tests in this module.""" diff --git a/tests/components/sensorpro/conftest.py b/tests/components/sensorpro/conftest.py index 85c56845ad8..12199e03a97 100644 --- a/tests/components/sensorpro/conftest.py +++ b/tests/components/sensorpro/conftest.py @@ -4,5 +4,5 @@ import pytest @pytest.fixture(autouse=True) -def mock_bluetooth(enable_bluetooth): +def mock_bluetooth(enable_bluetooth: None) -> None: """Auto mock bluetooth.""" diff --git a/tests/components/sensorpush/conftest.py b/tests/components/sensorpush/conftest.py index 2a983a7a4ed..0166f00d1e8 100644 --- a/tests/components/sensorpush/conftest.py +++ b/tests/components/sensorpush/conftest.py @@ -4,5 +4,5 @@ import pytest @pytest.fixture(autouse=True) -def mock_bluetooth(enable_bluetooth): +def mock_bluetooth(enable_bluetooth: None) -> None: """Auto mock bluetooth.""" diff --git a/tests/components/shelly/conftest.py b/tests/components/shelly/conftest.py index 6f2a8cf2711..23ed1f306b1 100644 --- a/tests/components/shelly/conftest.py +++ b/tests/components/shelly/conftest.py @@ -409,5 +409,5 @@ async def mock_rpc_device(): @pytest.fixture(autouse=True) -def mock_bluetooth(enable_bluetooth): +def mock_bluetooth(enable_bluetooth: None) -> None: """Auto mock bluetooth.""" diff --git a/tests/components/snooz/conftest.py b/tests/components/snooz/conftest.py index 8cdc2ec0982..e15c7d836c8 100644 --- a/tests/components/snooz/conftest.py +++ b/tests/components/snooz/conftest.py @@ -10,7 +10,7 @@ from . import SnoozFixture, create_mock_snooz, create_mock_snooz_config_entry @pytest.fixture(autouse=True) -def mock_bluetooth(enable_bluetooth): +def mock_bluetooth(enable_bluetooth: None) -> None: """Auto mock bluetooth.""" diff --git a/tests/components/switchbot/conftest.py b/tests/components/switchbot/conftest.py index 3df082c4361..44f68a1c8ae 100644 --- a/tests/components/switchbot/conftest.py +++ b/tests/components/switchbot/conftest.py @@ -4,5 +4,5 @@ import pytest @pytest.fixture(autouse=True) -def mock_bluetooth(enable_bluetooth): +def mock_bluetooth(enable_bluetooth: None) -> None: """Auto mock bluetooth.""" diff --git a/tests/components/thermobeacon/conftest.py b/tests/components/thermobeacon/conftest.py index ca17cdbfe4c..c4eda1318aa 100644 --- a/tests/components/thermobeacon/conftest.py +++ b/tests/components/thermobeacon/conftest.py @@ -4,5 +4,5 @@ import pytest @pytest.fixture(autouse=True) -def mock_bluetooth(enable_bluetooth): +def mock_bluetooth(enable_bluetooth: None) -> None: """Auto mock bluetooth.""" diff --git a/tests/components/thermopro/conftest.py b/tests/components/thermopro/conftest.py index 1a4c59ff609..445f52b7844 100644 --- a/tests/components/thermopro/conftest.py +++ b/tests/components/thermopro/conftest.py @@ -4,5 +4,5 @@ import pytest @pytest.fixture(autouse=True) -def mock_bluetooth(enable_bluetooth): +def mock_bluetooth(enable_bluetooth: None) -> None: """Auto mock bluetooth.""" diff --git a/tests/components/tilt_ble/conftest.py b/tests/components/tilt_ble/conftest.py index 552b41d10da..248e23d4c6b 100644 --- a/tests/components/tilt_ble/conftest.py +++ b/tests/components/tilt_ble/conftest.py @@ -4,5 +4,5 @@ import pytest @pytest.fixture(autouse=True) -def mock_bluetooth(enable_bluetooth): +def mock_bluetooth(enable_bluetooth: None) -> None: """Auto mock bluetooth.""" diff --git a/tests/components/xiaomi_ble/conftest.py b/tests/components/xiaomi_ble/conftest.py index 3d68d78e27e..bd3480bc586 100644 --- a/tests/components/xiaomi_ble/conftest.py +++ b/tests/components/xiaomi_ble/conftest.py @@ -1,5 +1,6 @@ """Session fixtures.""" +from collections.abc import Generator from unittest import mock import pytest @@ -44,7 +45,7 @@ class MockBleakClientBattery5(MockBleakClient): @pytest.fixture(autouse=True) -def mock_bluetooth(enable_bluetooth): +def mock_bluetooth(enable_bluetooth: None) -> Generator[None, None, None]: """Auto mock bluetooth.""" with mock.patch("xiaomi_ble.parser.BleakClient", MockBleakClientBattery5): diff --git a/tests/components/yalexs_ble/conftest.py b/tests/components/yalexs_ble/conftest.py index c2b947cc863..27c45b9110c 100644 --- a/tests/components/yalexs_ble/conftest.py +++ b/tests/components/yalexs_ble/conftest.py @@ -4,5 +4,5 @@ import pytest @pytest.fixture(autouse=True) -def mock_bluetooth(enable_bluetooth): +def mock_bluetooth(enable_bluetooth: None) -> None: """Auto mock bluetooth.""" From 2b4e9212bce249c80623f3264e1c8ca18b2ef459 Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Fri, 31 May 2024 04:17:44 +0200 Subject: [PATCH 0061/1445] Log aiohttp error in rest_command (#118453) --- homeassistant/components/rest_command/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/rest_command/__init__.py b/homeassistant/components/rest_command/__init__.py index c43e23cf068..b6945c5ce98 100644 --- a/homeassistant/components/rest_command/__init__.py +++ b/homeassistant/components/rest_command/__init__.py @@ -200,6 +200,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: ) from err except aiohttp.ClientError as err: + _LOGGER.error("Error fetching data: %s", err) raise HomeAssistantError( translation_domain=DOMAIN, translation_key="client_error", From cb502263fd78743717ae540ab2b91412bfb91c05 Mon Sep 17 00:00:00 2001 From: Markus Jacobsen Date: Fri, 31 May 2024 04:23:43 +0200 Subject: [PATCH 0062/1445] Bang & Olufsen fix straggler from previous PR (#118488) * Fix callback straggler from previous PR * Update homeassistant/components/bang_olufsen/media_player.py --------- Co-authored-by: Paulus Schoutsen --- homeassistant/components/bang_olufsen/media_player.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/bang_olufsen/media_player.py b/homeassistant/components/bang_olufsen/media_player.py index 725afab88b9..9d4cd81f5cb 100644 --- a/homeassistant/components/bang_olufsen/media_player.py +++ b/homeassistant/components/bang_olufsen/media_player.py @@ -176,7 +176,7 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity): async_dispatcher_connect( self.hass, f"{self._unique_id}_{WebsocketNotification.REMOTE_MENU_CHANGED}", - self._update_sources, + self._async_update_sources, ) ) self.async_on_remove( @@ -235,12 +235,12 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity): self._media_image = get_highest_resolution_artwork(self._playback_metadata) # If the device has been updated with new sources, then the API will fail here. - await self._update_sources() + await self._async_update_sources() # Set the static entity attributes that needed more information. self._attr_source_list = list(self._sources.values()) - async def _update_sources(self) -> None: + async def _async_update_sources(self) -> None: """Get sources for the specific product.""" # Audio sources From cdcf091c9c728371c9ca999b94dcc9dd149652f5 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Fri, 31 May 2024 09:11:52 +0200 Subject: [PATCH 0063/1445] Pass the message as an exception argument in Tractive integration (#118534) Pass the message as an exception argument Co-authored-by: Maciej Bieniek <478555+bieniu@users.noreply.github.com> --- homeassistant/components/tractive/__init__.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/tractive/__init__.py b/homeassistant/components/tractive/__init__.py index 468f11979e8..fd5abe24c06 100644 --- a/homeassistant/components/tractive/__init__.py +++ b/homeassistant/components/tractive/__init__.py @@ -149,11 +149,9 @@ async def _generate_trackables( ) if not tracker_details.get("_id"): - _LOGGER.info( - "Tractive API returns incomplete data for tracker %s", - trackable["device_id"], + raise ConfigEntryNotReady( + f"Tractive API returns incomplete data for tracker {trackable['device_id']}", ) - raise ConfigEntryNotReady return Trackables(tracker, trackable, tracker_details, hw_info, pos_report) From 85d979847c2f192d102d7eadae93857e3fe1d8c6 Mon Sep 17 00:00:00 2001 From: David Bonnes Date: Fri, 31 May 2024 09:22:15 +0100 Subject: [PATCH 0064/1445] Move evohome helper functions to separate module (#118497) initial commit --- homeassistant/components/evohome/__init__.py | 111 ++----------------- homeassistant/components/evohome/helpers.py | 110 ++++++++++++++++++ 2 files changed, 122 insertions(+), 99 deletions(-) create mode 100644 homeassistant/components/evohome/helpers.py diff --git a/homeassistant/components/evohome/__init__.py b/homeassistant/components/evohome/__init__.py index 133851ba1ea..08b65f42688 100644 --- a/homeassistant/components/evohome/__init__.py +++ b/homeassistant/components/evohome/__init__.py @@ -8,9 +8,7 @@ from __future__ import annotations from collections.abc import Awaitable from datetime import datetime, timedelta, timezone -from http import HTTPStatus import logging -import re from typing import Any, Final import evohomeasync as ev1 @@ -80,6 +78,13 @@ from .const import ( UTC_OFFSET, EvoService, ) +from .helpers import ( + convert_dict, + convert_until, + dt_aware_to_naive, + dt_local_to_aware, + handle_evo_exception, +) _LOGGER = logging.getLogger(__name__) @@ -117,98 +122,6 @@ SET_ZONE_OVERRIDE_SCHEMA: Final = vol.Schema( ) -def _dt_local_to_aware(dt_naive: datetime) -> datetime: - dt_aware = dt_util.now() + (dt_naive - datetime.now()) - if dt_aware.microsecond >= 500000: - dt_aware += timedelta(seconds=1) - return dt_aware.replace(microsecond=0) - - -def _dt_aware_to_naive(dt_aware: datetime) -> datetime: - dt_naive = datetime.now() + (dt_aware - dt_util.now()) - if dt_naive.microsecond >= 500000: - dt_naive += timedelta(seconds=1) - return dt_naive.replace(microsecond=0) - - -def convert_until(status_dict: dict, until_key: str) -> None: - """Reformat a dt str from "%Y-%m-%dT%H:%M:%SZ" as local/aware/isoformat.""" - if until_key in status_dict and ( # only present for certain modes - dt_utc_naive := dt_util.parse_datetime(status_dict[until_key]) - ): - status_dict[until_key] = dt_util.as_local(dt_utc_naive).isoformat() - - -def convert_dict(dictionary: dict[str, Any]) -> dict[str, Any]: - """Recursively convert a dict's keys to snake_case.""" - - def convert_key(key: str) -> str: - """Convert a string to snake_case.""" - string = re.sub(r"[\-\.\s]", "_", str(key)) - return ( - (string[0]).lower() - + re.sub( - r"[A-Z]", - lambda matched: f"_{matched.group(0).lower()}", # type:ignore[str-bytes-safe] - string[1:], - ) - ) - - return { - (convert_key(k) if isinstance(k, str) else k): ( - convert_dict(v) if isinstance(v, dict) else v - ) - for k, v in dictionary.items() - } - - -def _handle_exception(err: evo.RequestFailed) -> None: - """Return False if the exception can't be ignored.""" - - try: - raise err - - except evo.AuthenticationFailed: - _LOGGER.error( - ( - "Failed to authenticate with the vendor's server. Check your username" - " and password. NB: Some special password characters that work" - " correctly via the website will not work via the web API. Message" - " is: %s" - ), - err, - ) - - except evo.RequestFailed: - if err.status is None: - _LOGGER.warning( - ( - "Unable to connect with the vendor's server. " - "Check your network and the vendor's service status page. " - "Message is: %s" - ), - err, - ) - - elif err.status == HTTPStatus.SERVICE_UNAVAILABLE: - _LOGGER.warning( - "The vendor says their server is currently unavailable. " - "Check the vendor's service status page" - ) - - elif err.status == HTTPStatus.TOO_MANY_REQUESTS: - _LOGGER.warning( - ( - "The vendor's API rate limit has been exceeded. " - "If this message persists, consider increasing the %s" - ), - CONF_SCAN_INTERVAL, - ) - - else: - raise # we don't expect/handle any other Exceptions - - async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Create a (EMEA/EU-based) Honeywell TCC system.""" @@ -225,7 +138,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: if tokens.get(ACCESS_TOKEN_EXPIRES) is not None and ( expires := dt_util.parse_datetime(tokens[ACCESS_TOKEN_EXPIRES]) ): - tokens[ACCESS_TOKEN_EXPIRES] = _dt_aware_to_naive(expires) + tokens[ACCESS_TOKEN_EXPIRES] = dt_aware_to_naive(expires) user_data = tokens.pop(USER_DATA, {}) return (tokens, user_data) @@ -243,7 +156,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: try: await client_v2.login() except evo.AuthenticationFailed as err: - _handle_exception(err) + handle_evo_exception(err) return False finally: config[DOMAIN][CONF_PASSWORD] = "REDACTED" @@ -458,7 +371,7 @@ class EvoBroker: async def save_auth_tokens(self) -> None: """Save access tokens and session IDs to the store for later use.""" # evohomeasync2 uses naive/local datetimes - access_token_expires = _dt_local_to_aware( + access_token_expires = dt_local_to_aware( self.client.access_token_expires # type: ignore[arg-type] ) @@ -488,7 +401,7 @@ class EvoBroker: try: result = await client_api except evo.RequestFailed as err: - _handle_exception(err) + handle_evo_exception(err) return None if update_state: # wait a moment for system to quiesce before updating state @@ -563,7 +476,7 @@ class EvoBroker: try: status = await self._location.refresh_status() except evo.RequestFailed as err: - _handle_exception(err) + handle_evo_exception(err) else: async_dispatcher_send(self.hass, DOMAIN) _LOGGER.debug("Status = %s", status) diff --git a/homeassistant/components/evohome/helpers.py b/homeassistant/components/evohome/helpers.py new file mode 100644 index 00000000000..f84d2945779 --- /dev/null +++ b/homeassistant/components/evohome/helpers.py @@ -0,0 +1,110 @@ +"""Support for (EMEA/EU-based) Honeywell TCC systems.""" + +from __future__ import annotations + +from datetime import datetime, timedelta +from http import HTTPStatus +import logging +import re +from typing import Any + +import evohomeasync2 as evo + +from homeassistant.const import CONF_SCAN_INTERVAL +import homeassistant.util.dt as dt_util + +_LOGGER = logging.getLogger(__name__) + + +def dt_local_to_aware(dt_naive: datetime) -> datetime: + """Convert a local/naive datetime to TZ-aware.""" + dt_aware = dt_util.now() + (dt_naive - datetime.now()) + if dt_aware.microsecond >= 500000: + dt_aware += timedelta(seconds=1) + return dt_aware.replace(microsecond=0) + + +def dt_aware_to_naive(dt_aware: datetime) -> datetime: + """Convert a TZ-aware datetime to naive/local.""" + dt_naive = datetime.now() + (dt_aware - dt_util.now()) + if dt_naive.microsecond >= 500000: + dt_naive += timedelta(seconds=1) + return dt_naive.replace(microsecond=0) + + +def convert_until(status_dict: dict, until_key: str) -> None: + """Reformat a dt str from "%Y-%m-%dT%H:%M:%SZ" as local/aware/isoformat.""" + if until_key in status_dict and ( # only present for certain modes + dt_utc_naive := dt_util.parse_datetime(status_dict[until_key]) + ): + status_dict[until_key] = dt_util.as_local(dt_utc_naive).isoformat() + + +def convert_dict(dictionary: dict[str, Any]) -> dict[str, Any]: + """Recursively convert a dict's keys to snake_case.""" + + def convert_key(key: str) -> str: + """Convert a string to snake_case.""" + string = re.sub(r"[\-\.\s]", "_", str(key)) + return ( + (string[0]).lower() + + re.sub( + r"[A-Z]", + lambda matched: f"_{matched.group(0).lower()}", # type:ignore[str-bytes-safe] + string[1:], + ) + ) + + return { + (convert_key(k) if isinstance(k, str) else k): ( + convert_dict(v) if isinstance(v, dict) else v + ) + for k, v in dictionary.items() + } + + +def handle_evo_exception(err: evo.RequestFailed) -> None: + """Return False if the exception can't be ignored.""" + + try: + raise err + + except evo.AuthenticationFailed: + _LOGGER.error( + ( + "Failed to authenticate with the vendor's server. Check your username" + " and password. NB: Some special password characters that work" + " correctly via the website will not work via the web API. Message" + " is: %s" + ), + err, + ) + + except evo.RequestFailed: + if err.status is None: + _LOGGER.warning( + ( + "Unable to connect with the vendor's server. " + "Check your network and the vendor's service status page. " + "Message is: %s" + ), + err, + ) + + elif err.status == HTTPStatus.SERVICE_UNAVAILABLE: + _LOGGER.warning( + "The vendor says their server is currently unavailable. " + "Check the vendor's service status page" + ) + + elif err.status == HTTPStatus.TOO_MANY_REQUESTS: + _LOGGER.warning( + ( + "The vendor's API rate limit has been exceeded. " + "If this message persists, consider increasing the %s" + ), + CONF_SCAN_INTERVAL, + ) + + else: + raise # we don't expect/handle any other Exceptions From 780407606449c2908c10ee6bb65adea01237f3a7 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 31 May 2024 11:18:55 +0200 Subject: [PATCH 0065/1445] Drop single-use constant from pylint plugin (#118540) * Drop single-use constant from pylint plugin * Typo --- pylint/plugins/hass_enforce_type_hints.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index 65248ac2493..ac58db37b72 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -155,10 +155,6 @@ _TEST_FIXTURES: dict[str, list[str] | str] = { "unused_tcp_port_factory": "Callable[[], int]", "unused_udp_port_factory": "Callable[[], int]", } -_TEST_FUNCTION_MATCH = TypeHintMatch( - function_name="test_*", - return_type=None, -) _FUNCTION_MATCH: dict[str, list[TypeHintMatch]] = { @@ -3308,12 +3304,12 @@ class HassTypeHintChecker(BaseChecker): def _check_test_function( self, node: nodes.FunctionDef, annotations: list[nodes.NodeNG | None] ) -> None: - # Check the return type. - if not _is_valid_return_type(_TEST_FUNCTION_MATCH, node.returns): + # Check the return type, should always be `None` for test_*** functions. + if not _is_valid_type(None, node.returns, True): self.add_message( "hass-return-type", node=node, - args=(_TEST_FUNCTION_MATCH.return_type or "None", node.name), + args=("None", node.name), ) # Check that all positional arguments are correctly annotated. for arg_name, expected_type in _TEST_FIXTURES.items(): From 8a3b49434e9f846fc254e529ecb85bf4f5998043 Mon Sep 17 00:00:00 2001 From: Alexandre CUER Date: Fri, 31 May 2024 11:50:18 +0200 Subject: [PATCH 0066/1445] Code quality improvements in emoncms integration (#118468) * type hints remove unused var interval * corrections as suggested by epenet * reintroducing property extra_state_attributes so that the extra parameters update correctly --- homeassistant/components/emoncms/sensor.py | 78 ++++++++++------------ 1 file changed, 37 insertions(+), 41 deletions(-) diff --git a/homeassistant/components/emoncms/sensor.py b/homeassistant/components/emoncms/sensor.py index 746877c4e5f..c981fa0cf6c 100644 --- a/homeassistant/components/emoncms/sensor.py +++ b/homeassistant/components/emoncms/sensor.py @@ -5,6 +5,7 @@ from __future__ import annotations from datetime import timedelta from http import HTTPStatus import logging +from typing import Any import requests import voluptuous as vol @@ -18,7 +19,6 @@ from homeassistant.components.sensor import ( from homeassistant.const import ( CONF_API_KEY, CONF_ID, - CONF_SCAN_INTERVAL, CONF_UNIT_OF_MEASUREMENT, CONF_URL, CONF_VALUE_TEMPLATE, @@ -72,7 +72,9 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -def get_id(sensorid, feedtag, feedname, feedid, feeduserid): +def get_id( + sensorid: str, feedtag: str, feedname: str, feedid: str, feeduserid: str +) -> str: """Return unique identifier for feed / sensor.""" return f"emoncms{sensorid}_{feedtag}_{feedname}_{feedid}_{feeduserid}" @@ -84,20 +86,19 @@ def setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the Emoncms sensor.""" - apikey = config.get(CONF_API_KEY) - url = config.get(CONF_URL) - sensorid = config.get(CONF_ID) + apikey = config[CONF_API_KEY] + url = config[CONF_URL] + sensorid = config[CONF_ID] value_template = config.get(CONF_VALUE_TEMPLATE) config_unit = config.get(CONF_UNIT_OF_MEASUREMENT) exclude_feeds = config.get(CONF_EXCLUDE_FEEDID) include_only_feeds = config.get(CONF_ONLY_INCLUDE_FEEDID) sensor_names = config.get(CONF_SENSOR_NAMES) - interval = config.get(CONF_SCAN_INTERVAL) if value_template is not None: value_template.hass = hass - data = EmonCmsData(hass, url, apikey, interval) + data = EmonCmsData(hass, url, apikey) data.update() @@ -140,8 +141,15 @@ class EmonCmsSensor(SensorEntity): """Implementation of an Emoncms sensor.""" def __init__( - self, hass, data, name, value_template, unit_of_measurement, sensorid, elem - ): + self, + hass: HomeAssistant, + data: EmonCmsData, + name: str | None, + value_template: template.Template | None, + unit_of_measurement: str | None, + sensorid: str, + elem: dict[str, Any], + ) -> None: """Initialize the sensor.""" if name is None: # Suppress ID in sensor name if it's 1, since most people won't @@ -150,16 +158,16 @@ class EmonCmsSensor(SensorEntity): id_for_name = "" if str(sensorid) == "1" else sensorid # Use the feed name assigned in EmonCMS or fall back to the feed ID feed_name = elem.get("name") or f"Feed {elem['id']}" - self._name = f"EmonCMS{id_for_name} {feed_name}" + self._attr_name = f"EmonCMS{id_for_name} {feed_name}" else: - self._name = name + self._attr_name = name self._identifier = get_id( sensorid, elem["tag"], elem["name"], elem["id"], elem["userid"] ) self._hass = hass self._data = data self._value_template = value_template - self._unit_of_measurement = unit_of_measurement + self._attr_native_unit_of_measurement = unit_of_measurement self._sensorid = sensorid self._elem = elem @@ -189,32 +197,19 @@ class EmonCmsSensor(SensorEntity): self._attr_state_class = SensorStateClass.MEASUREMENT if self._value_template is not None: - self._state = self._value_template.render_with_possible_json_value( - elem["value"], STATE_UNKNOWN + self._attr_native_value = ( + self._value_template.render_with_possible_json_value( + elem["value"], STATE_UNKNOWN + ) ) elif elem["value"] is not None: - self._state = round(float(elem["value"]), DECIMALS) + self._attr_native_value = round(float(elem["value"]), DECIMALS) else: - self._state = None + self._attr_native_value = None @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def native_unit_of_measurement(self): - """Return the unit of measurement of this entity, if any.""" - return self._unit_of_measurement - - @property - def native_value(self): - """Return the state of the device.""" - return self._state - - @property - def extra_state_attributes(self): - """Return the attributes of the sensor.""" + def extra_state_attributes(self) -> dict[str, Any]: + """Return the sensor extra attributes.""" return { ATTR_FEEDID: self._elem["id"], ATTR_TAG: self._elem["tag"], @@ -254,28 +249,29 @@ class EmonCmsSensor(SensorEntity): self._elem = elem if self._value_template is not None: - self._state = self._value_template.render_with_possible_json_value( - elem["value"], STATE_UNKNOWN + self._attr_native_value = ( + self._value_template.render_with_possible_json_value( + elem["value"], STATE_UNKNOWN + ) ) elif elem["value"] is not None: - self._state = round(float(elem["value"]), DECIMALS) + self._attr_native_value = round(float(elem["value"]), DECIMALS) else: - self._state = None + self._attr_native_value = None class EmonCmsData: """The class for handling the data retrieval.""" - def __init__(self, hass, url, apikey, interval): + def __init__(self, hass: HomeAssistant, url: str, apikey: str) -> None: """Initialize the data object.""" self._apikey = apikey self._url = f"{url}/feed/list.json" - self._interval = interval self._hass = hass - self.data = None + self.data: list[dict[str, Any]] | None = None @Throttle(MIN_TIME_BETWEEN_UPDATES) - def update(self): + def update(self) -> None: """Get the latest data from Emoncms.""" try: parameters = {"apikey": self._apikey} From ec4545ce4a265f4555315597395128567d2a91fd Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 31 May 2024 12:03:29 +0200 Subject: [PATCH 0067/1445] Small performance improvement to pylint plugin (#118475) * Small improvement to pylint plugin * Adjust * Improve * Rename variable and drop used argument --- pylint/plugins/hass_enforce_type_hints.py | 29 +++++++---------------- 1 file changed, 9 insertions(+), 20 deletions(-) diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index ac58db37b72..d82efa2fb3e 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -3092,11 +3092,6 @@ def _get_module_platform(module_name: str) -> str | None: return platform.lstrip(".") if platform else "__init__" -def _is_test_function(module_name: str, node: nodes.FunctionDef) -> bool: - """Return True if function is a pytest function.""" - return module_name.startswith("tests.") and node.name.startswith("test_") - - class HassTypeHintChecker(BaseChecker): """Checker for setup type hints.""" @@ -3136,12 +3131,14 @@ class HassTypeHintChecker(BaseChecker): _class_matchers: list[ClassTypeHintMatch] _function_matchers: list[TypeHintMatch] _module_name: str + _in_test_module: bool def visit_module(self, node: nodes.Module) -> None: """Populate matchers for a Module node.""" self._class_matchers = [] self._function_matchers = [] self._module_name = node.name + self._in_test_module = self._module_name.startswith("tests.") if (module_platform := _get_module_platform(node.name)) is None: return @@ -3233,8 +3230,10 @@ class HassTypeHintChecker(BaseChecker): matchers = _METHOD_MATCH else: matchers = self._function_matchers - if _is_test_function(self._module_name, node): - self._check_test_function(node, annotations) + if self._in_test_module and node.name.startswith("test_"): + self._check_test_function(node) + return + for match in matchers: if not match.need_to_check_function(node): continue @@ -3251,11 +3250,7 @@ class HassTypeHintChecker(BaseChecker): # Check that all positional arguments are correctly annotated. if match.arg_types: for key, expected_type in match.arg_types.items(): - if ( - node.args.args[key].name in _COMMON_ARGUMENTS - or _is_test_function(self._module_name, node) - and node.args.args[key].name in _TEST_FIXTURES - ): + if node.args.args[key].name in _COMMON_ARGUMENTS: # It has already been checked, avoid double-message continue if not _is_valid_type(expected_type, annotations[key]): @@ -3268,11 +3263,7 @@ class HassTypeHintChecker(BaseChecker): # Check that all keyword arguments are correctly annotated. if match.named_arg_types is not None: for arg_name, expected_type in match.named_arg_types.items(): - if ( - arg_name in _COMMON_ARGUMENTS - or _is_test_function(self._module_name, node) - and arg_name in _TEST_FIXTURES - ): + if arg_name in _COMMON_ARGUMENTS: # It has already been checked, avoid double-message continue arg_node, annotation = _get_named_annotation(node, arg_name) @@ -3301,9 +3292,7 @@ class HassTypeHintChecker(BaseChecker): args=(match.return_type or "None", node.name), ) - def _check_test_function( - self, node: nodes.FunctionDef, annotations: list[nodes.NodeNG | None] - ) -> None: + def _check_test_function(self, node: nodes.FunctionDef) -> None: # Check the return type, should always be `None` for test_*** functions. if not _is_valid_type(None, node.returns, True): self.add_message( From 9fc51891caa7469b1d008bc465a2282d54984be1 Mon Sep 17 00:00:00 2001 From: Tsvi Mostovicz Date: Fri, 31 May 2024 13:35:40 +0300 Subject: [PATCH 0068/1445] Fix YAML deprecation breaking version in jewish calendar and media extractor (#118546) * Fix YAML deprecation breaking version * Update * fix media extractor deprecation as well * Add issue_domain --- homeassistant/components/jewish_calendar/__init__.py | 3 ++- homeassistant/components/media_extractor/__init__.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/jewish_calendar/__init__.py b/homeassistant/components/jewish_calendar/__init__.py index 7c4c0b7f634..d4edcadf6f7 100644 --- a/homeassistant/components/jewish_calendar/__init__.py +++ b/homeassistant/components/jewish_calendar/__init__.py @@ -96,7 +96,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: HOMEASSISTANT_DOMAIN, f"deprecated_yaml_{DOMAIN}", is_fixable=False, - breaks_in_ha_version="2024.10.0", + issue_domain=DOMAIN, + breaks_in_ha_version="2024.12.0", severity=IssueSeverity.WARNING, translation_key="deprecated_yaml", translation_placeholders={ diff --git a/homeassistant/components/media_extractor/__init__.py b/homeassistant/components/media_extractor/__init__.py index 479cdf90aaf..b8bb5f98cd0 100644 --- a/homeassistant/components/media_extractor/__init__.py +++ b/homeassistant/components/media_extractor/__init__.py @@ -72,7 +72,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: hass, HOMEASSISTANT_DOMAIN, f"deprecated_yaml_{DOMAIN}", - breaks_in_ha_version="2024.11.0", + breaks_in_ha_version="2024.12.0", is_fixable=False, issue_domain=DOMAIN, severity=IssueSeverity.WARNING, From 7e1f4cd3fb29fd4eb6a11addbdb775461026fd61 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 31 May 2024 12:42:42 +0200 Subject: [PATCH 0069/1445] Check fixtures for type hints in pylint plugin (#118313) * Check fixtures for type hints in pylint plugin * Apply suggestion --- pylint/plugins/hass_enforce_type_hints.py | 26 ++++++-- tests/pylint/test_enforce_type_hints.py | 80 +++++++++++++++++++++++ 2 files changed, 99 insertions(+), 7 deletions(-) diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index d82efa2fb3e..16449e2e5a0 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -3140,7 +3140,10 @@ class HassTypeHintChecker(BaseChecker): self._module_name = node.name self._in_test_module = self._module_name.startswith("tests.") - if (module_platform := _get_module_platform(node.name)) is None: + if ( + self._in_test_module + or (module_platform := _get_module_platform(node.name)) is None + ): return if module_platform in _PLATFORMS: @@ -3229,10 +3232,19 @@ class HassTypeHintChecker(BaseChecker): if node.is_method(): matchers = _METHOD_MATCH else: + if self._in_test_module: + if node.name.startswith("test_"): + self._check_test_function(node, False) + return + if (decoratornames := node.decoratornames()) and ( + # `@pytest.fixture` + "_pytest.fixtures.fixture" in decoratornames + # `@pytest.fixture(...)` + or "_pytest.fixtures.FixtureFunctionMarker" in decoratornames + ): + self._check_test_function(node, True) + return matchers = self._function_matchers - if self._in_test_module and node.name.startswith("test_"): - self._check_test_function(node) - return for match in matchers: if not match.need_to_check_function(node): @@ -3292,9 +3304,9 @@ class HassTypeHintChecker(BaseChecker): args=(match.return_type or "None", node.name), ) - def _check_test_function(self, node: nodes.FunctionDef) -> None: + def _check_test_function(self, node: nodes.FunctionDef, is_fixture: bool) -> None: # Check the return type, should always be `None` for test_*** functions. - if not _is_valid_type(None, node.returns, True): + if not is_fixture and not _is_valid_type(None, node.returns, True): self.add_message( "hass-return-type", node=node, @@ -3303,7 +3315,7 @@ class HassTypeHintChecker(BaseChecker): # Check that all positional arguments are correctly annotated. for arg_name, expected_type in _TEST_FIXTURES.items(): arg_node, annotation = _get_named_annotation(node, arg_name) - if arg_node and expected_type == "None": + if arg_node and expected_type == "None" and not is_fixture: self.add_message( "hass-consider-usefixtures-decorator", node=arg_node, diff --git a/tests/pylint/test_enforce_type_hints.py b/tests/pylint/test_enforce_type_hints.py index 0153214c267..68e1e14a34f 100644 --- a/tests/pylint/test_enforce_type_hints.py +++ b/tests/pylint/test_enforce_type_hints.py @@ -1232,6 +1232,86 @@ def test_pytest_invalid_function( type_hint_checker.visit_asyncfunctiondef(func_node) +def test_pytest_fixture(linter: UnittestLinter, type_hint_checker: BaseChecker) -> None: + """Ensure valid hints are accepted for a test fixture.""" + func_node = astroid.extract_node( + """ + import pytest + + @pytest.fixture + def sample_fixture( #@ + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, + aiohttp_server: Callable[[], TestServer], + unused_tcp_port_factory: Callable[[], int], + enable_custom_integrations: None, + ) -> None: + pass + """, + "tests.components.pylint_test.notify", + ) + type_hint_checker.visit_module(func_node.parent) + + with assert_no_messages( + linter, + ): + type_hint_checker.visit_asyncfunctiondef(func_node) + + +@pytest.mark.parametrize("decorator", ["@pytest.fixture", "@pytest.fixture()"]) +def test_pytest_invalid_fixture( + linter: UnittestLinter, type_hint_checker: BaseChecker, decorator: str +) -> None: + """Ensure invalid hints are rejected for a test fixture.""" + func_node, hass_node, caplog_node, none_node = astroid.extract_node( + f""" + import pytest + + {decorator} + def sample_fixture( #@ + hass: Something, #@ + caplog: SomethingElse, #@ + current_request_with_host, #@ + ) -> Any: + pass + """, + "tests.components.pylint_test.notify", + ) + type_hint_checker.visit_module(func_node.parent) + + with assert_adds_messages( + linter, + pylint.testutils.MessageTest( + msg_id="hass-argument-type", + node=hass_node, + args=("hass", ["HomeAssistant", "HomeAssistant | None"], "sample_fixture"), + line=6, + col_offset=4, + end_line=6, + end_col_offset=19, + ), + pylint.testutils.MessageTest( + msg_id="hass-argument-type", + node=caplog_node, + args=("caplog", "pytest.LogCaptureFixture", "sample_fixture"), + line=7, + col_offset=4, + end_line=7, + end_col_offset=25, + ), + pylint.testutils.MessageTest( + msg_id="hass-argument-type", + node=none_node, + args=("current_request_with_host", "None", "sample_fixture"), + line=8, + col_offset=4, + end_line=8, + end_col_offset=29, + ), + ): + type_hint_checker.visit_asyncfunctiondef(func_node) + + @pytest.mark.parametrize( "entry_annotation", [ From 0974ea9a5a2af44d661728e47c71852b000b01f3 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 31 May 2024 13:06:49 +0200 Subject: [PATCH 0070/1445] Adjust "hass" type hint for test fixtures in pylint plugin (#118548) Adjust "hass" type hint in pylint plugin --- pylint/plugins/hass_enforce_type_hints.py | 21 ++++++------- tests/pylint/test_enforce_type_hints.py | 36 +++++++++++------------ 2 files changed, 29 insertions(+), 28 deletions(-) diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index 16449e2e5a0..c6c6986060f 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -113,6 +113,7 @@ _TEST_FIXTURES: dict[str, list[str] | str] = { "entity_registry_enabled_by_default": "None", "event_loop": "AbstractEventLoop", "freezer": "FrozenDateTimeFactory", + "hass": "HomeAssistant", "hass_access_token": "str", "hass_admin_credential": "Credentials", "hass_admin_user": "MockUser", @@ -3218,16 +3219,6 @@ class HassTypeHintChecker(BaseChecker): if self._ignore_function(node, annotations): return - # Check that common arguments are correctly typed. - for arg_name, expected_type in _COMMON_ARGUMENTS.items(): - arg_node, annotation = _get_named_annotation(node, arg_name) - if arg_node and not _is_valid_type(expected_type, annotation): - self.add_message( - "hass-argument-type", - node=arg_node, - args=(arg_name, expected_type, node.name), - ) - # Check method or function matchers. if node.is_method(): matchers = _METHOD_MATCH @@ -3246,6 +3237,16 @@ class HassTypeHintChecker(BaseChecker): return matchers = self._function_matchers + # Check that common arguments are correctly typed. + for arg_name, expected_type in _COMMON_ARGUMENTS.items(): + arg_node, annotation = _get_named_annotation(node, arg_name) + if arg_node and not _is_valid_type(expected_type, annotation): + self.add_message( + "hass-argument-type", + node=arg_node, + args=(arg_name, expected_type, node.name), + ) + for match in matchers: if not match.need_to_check_function(node): continue diff --git a/tests/pylint/test_enforce_type_hints.py b/tests/pylint/test_enforce_type_hints.py index 68e1e14a34f..9f0f4905dab 100644 --- a/tests/pylint/test_enforce_type_hints.py +++ b/tests/pylint/test_enforce_type_hints.py @@ -1174,15 +1174,6 @@ def test_pytest_invalid_function( with assert_adds_messages( linter, - pylint.testutils.MessageTest( - msg_id="hass-argument-type", - node=hass_node, - args=("hass", ["HomeAssistant", "HomeAssistant | None"], "test_sample"), - line=3, - col_offset=4, - end_line=3, - end_col_offset=19, - ), pylint.testutils.MessageTest( msg_id="hass-return-type", node=func_node, @@ -1228,6 +1219,15 @@ def test_pytest_invalid_function( end_line=6, end_col_offset=36, ), + pylint.testutils.MessageTest( + msg_id="hass-argument-type", + node=hass_node, + args=("hass", "HomeAssistant", "test_sample"), + line=3, + col_offset=4, + end_line=3, + end_col_offset=19, + ), ): type_hint_checker.visit_asyncfunctiondef(func_node) @@ -1281,15 +1281,6 @@ def test_pytest_invalid_fixture( with assert_adds_messages( linter, - pylint.testutils.MessageTest( - msg_id="hass-argument-type", - node=hass_node, - args=("hass", ["HomeAssistant", "HomeAssistant | None"], "sample_fixture"), - line=6, - col_offset=4, - end_line=6, - end_col_offset=19, - ), pylint.testutils.MessageTest( msg_id="hass-argument-type", node=caplog_node, @@ -1308,6 +1299,15 @@ def test_pytest_invalid_fixture( end_line=8, end_col_offset=29, ), + pylint.testutils.MessageTest( + msg_id="hass-argument-type", + node=hass_node, + args=("hass", "HomeAssistant", "sample_fixture"), + line=6, + col_offset=4, + end_line=6, + end_col_offset=19, + ), ): type_hint_checker.visit_asyncfunctiondef(func_node) From a23b5e97e6dfaf2be9079511d2c6cee6be378a5d Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 31 May 2024 14:11:59 +0200 Subject: [PATCH 0071/1445] Fix typo in OWM strings (#118538) Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> --- homeassistant/components/openweathermap/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/openweathermap/strings.json b/homeassistant/components/openweathermap/strings.json index 916e1e0a713..46b5feab75c 100644 --- a/homeassistant/components/openweathermap/strings.json +++ b/homeassistant/components/openweathermap/strings.json @@ -38,7 +38,7 @@ "step": { "migrate": { "title": "OpenWeatherMap API V2.5 deprecated", - "description": "OWM API v2.5 will be closed in June 2024.\nYou need to migrate all your OpenWeatherMap integration to mode v3.0.\n\nBefore the migration, you must have active subscription (be aware subscripiton activation take up to 2h). After your subscription is activated click **Submit** to migrate the integration to API V3.0. Read documentation for more information." + "description": "OWM API v2.5 will be closed in June 2024.\nYou need to migrate all your OpenWeatherMap integrations to v3.0.\n\nBefore the migration, you must have an active subscription (be aware that subscription activation can take up to 2h). After your subscription is activated, select **Submit** to migrate the integration to API V3.0. Read the documentation for more information." } }, "error": { From 76391d71d6049b547a3001ae14d6a3d2d39dfd6b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 31 May 2024 02:44:28 -1000 Subject: [PATCH 0072/1445] Fix snmp doing blocking I/O in the event loop (#118521) --- homeassistant/components/snmp/__init__.py | 4 + .../components/snmp/device_tracker.py | 54 +++++------ homeassistant/components/snmp/sensor.py | 42 +++------ homeassistant/components/snmp/switch.py | 89 +++++++------------ homeassistant/components/snmp/util.py | 76 ++++++++++++++++ tests/components/snmp/test_init.py | 22 +++++ 6 files changed, 176 insertions(+), 111 deletions(-) create mode 100644 homeassistant/components/snmp/util.py create mode 100644 tests/components/snmp/test_init.py diff --git a/homeassistant/components/snmp/__init__.py b/homeassistant/components/snmp/__init__.py index a4c922877f3..4a049ee1553 100644 --- a/homeassistant/components/snmp/__init__.py +++ b/homeassistant/components/snmp/__init__.py @@ -1 +1,5 @@ """The snmp component.""" + +from .util import async_get_snmp_engine + +__all__ = ["async_get_snmp_engine"] diff --git a/homeassistant/components/snmp/device_tracker.py b/homeassistant/components/snmp/device_tracker.py index 5d4f9e5e0d9..d336838117f 100644 --- a/homeassistant/components/snmp/device_tracker.py +++ b/homeassistant/components/snmp/device_tracker.py @@ -4,14 +4,11 @@ from __future__ import annotations import binascii import logging +from typing import TYPE_CHECKING from pysnmp.error import PySnmpError from pysnmp.hlapi.asyncio import ( CommunityData, - ContextData, - ObjectIdentity, - ObjectType, - SnmpEngine, Udp6TransportTarget, UdpTransportTarget, UsmUserData, @@ -43,6 +40,7 @@ from .const import ( DEFAULT_VERSION, SNMP_VERSIONS, ) +from .util import RequestArgsType, async_create_request_cmd_args _LOGGER = logging.getLogger(__name__) @@ -62,7 +60,7 @@ async def async_get_scanner( ) -> SnmpScanner | None: """Validate the configuration and return an SNMP scanner.""" scanner = SnmpScanner(config[DOMAIN]) - await scanner.async_init() + await scanner.async_init(hass) return scanner if scanner.success_init else None @@ -99,33 +97,29 @@ class SnmpScanner(DeviceScanner): if not privkey: privproto = "none" - request_args = [ - SnmpEngine(), - UsmUserData( - community, - authKey=authkey or None, - privKey=privkey or None, - authProtocol=authproto, - privProtocol=privproto, - ), - target, - ContextData(), - ] + self._auth_data = UsmUserData( + community, + authKey=authkey or None, + privKey=privkey or None, + authProtocol=authproto, + privProtocol=privproto, + ) else: - request_args = [ - SnmpEngine(), - CommunityData(community, mpModel=SNMP_VERSIONS[DEFAULT_VERSION]), - target, - ContextData(), - ] + self._auth_data = CommunityData( + community, mpModel=SNMP_VERSIONS[DEFAULT_VERSION] + ) - self.request_args = request_args + self._target = target + self.request_args: RequestArgsType | None = None self.baseoid = baseoid self.last_results = [] self.success_init = False - async def async_init(self): + async def async_init(self, hass: HomeAssistant) -> None: """Make a one-off read to check if the target device is reachable and readable.""" + self.request_args = await async_create_request_cmd_args( + hass, self._auth_data, self._target, self.baseoid + ) data = await self.async_get_snmp_data() self.success_init = data is not None @@ -156,12 +150,18 @@ class SnmpScanner(DeviceScanner): async def async_get_snmp_data(self): """Fetch MAC addresses from access point via SNMP.""" devices = [] + if TYPE_CHECKING: + assert self.request_args is not None + engine, auth_data, target, context_data, object_type = self.request_args walker = bulkWalkCmd( - *self.request_args, + engine, + auth_data, + target, + context_data, 0, 50, - ObjectType(ObjectIdentity(self.baseoid)), + object_type, lexicographicMode=False, ) async for errindication, errstatus, errindex, res in walker: diff --git a/homeassistant/components/snmp/sensor.py b/homeassistant/components/snmp/sensor.py index 939cb13ae35..0e5b215dcd4 100644 --- a/homeassistant/components/snmp/sensor.py +++ b/homeassistant/components/snmp/sensor.py @@ -11,10 +11,6 @@ from pysnmp.error import PySnmpError import pysnmp.hlapi.asyncio as hlapi from pysnmp.hlapi.asyncio import ( CommunityData, - ContextData, - ObjectIdentity, - ObjectType, - SnmpEngine, Udp6TransportTarget, UdpTransportTarget, UsmUserData, @@ -71,6 +67,7 @@ from .const import ( MAP_PRIV_PROTOCOLS, SNMP_VERSIONS, ) +from .util import async_create_request_cmd_args _LOGGER = logging.getLogger(__name__) @@ -119,7 +116,7 @@ async def async_setup_platform( host = config.get(CONF_HOST) port = config.get(CONF_PORT) community = config.get(CONF_COMMUNITY) - baseoid = config.get(CONF_BASEOID) + baseoid: str = config[CONF_BASEOID] version = config[CONF_VERSION] username = config.get(CONF_USERNAME) authkey = config.get(CONF_AUTH_KEY) @@ -145,27 +142,18 @@ async def async_setup_platform( authproto = "none" if not privkey: privproto = "none" - - request_args = [ - SnmpEngine(), - UsmUserData( - username, - authKey=authkey or None, - privKey=privkey or None, - authProtocol=getattr(hlapi, MAP_AUTH_PROTOCOLS[authproto]), - privProtocol=getattr(hlapi, MAP_PRIV_PROTOCOLS[privproto]), - ), - target, - ContextData(), - ] + auth_data = UsmUserData( + username, + authKey=authkey or None, + privKey=privkey or None, + authProtocol=getattr(hlapi, MAP_AUTH_PROTOCOLS[authproto]), + privProtocol=getattr(hlapi, MAP_PRIV_PROTOCOLS[privproto]), + ) else: - request_args = [ - SnmpEngine(), - CommunityData(community, mpModel=SNMP_VERSIONS[version]), - target, - ContextData(), - ] - get_result = await getCmd(*request_args, ObjectType(ObjectIdentity(baseoid))) + auth_data = CommunityData(community, mpModel=SNMP_VERSIONS[version]) + + request_args = await async_create_request_cmd_args(hass, auth_data, target, baseoid) + get_result = await getCmd(*request_args) errindication, _, _, _ = get_result if errindication and not accept_errors: @@ -244,9 +232,7 @@ class SnmpData: async def async_update(self): """Get the latest data from the remote SNMP capable host.""" - get_result = await getCmd( - *self._request_args, ObjectType(ObjectIdentity(self._baseoid)) - ) + get_result = await getCmd(*self._request_args) errindication, errstatus, errindex, restable = get_result if errindication and not self._accept_errors: diff --git a/homeassistant/components/snmp/switch.py b/homeassistant/components/snmp/switch.py index a447cdc8e9c..40083ed4213 100644 --- a/homeassistant/components/snmp/switch.py +++ b/homeassistant/components/snmp/switch.py @@ -8,10 +8,6 @@ from typing import Any import pysnmp.hlapi.asyncio as hlapi from pysnmp.hlapi.asyncio import ( CommunityData, - ContextData, - ObjectIdentity, - ObjectType, - SnmpEngine, UdpTransportTarget, UsmUserData, getCmd, @@ -67,6 +63,7 @@ from .const import ( MAP_PRIV_PROTOCOLS, SNMP_VERSIONS, ) +from .util import RequestArgsType, async_create_request_cmd_args _LOGGER = logging.getLogger(__name__) @@ -132,40 +129,54 @@ async def async_setup_platform( host = config.get(CONF_HOST) port = config.get(CONF_PORT) community = config.get(CONF_COMMUNITY) - baseoid = config.get(CONF_BASEOID) + baseoid: str = config[CONF_BASEOID] command_oid = config.get(CONF_COMMAND_OID) command_payload_on = config.get(CONF_COMMAND_PAYLOAD_ON) command_payload_off = config.get(CONF_COMMAND_PAYLOAD_OFF) - version = config.get(CONF_VERSION) + version: str = config[CONF_VERSION] username = config.get(CONF_USERNAME) authkey = config.get(CONF_AUTH_KEY) - authproto = config.get(CONF_AUTH_PROTOCOL) + authproto: str = config[CONF_AUTH_PROTOCOL] privkey = config.get(CONF_PRIV_KEY) - privproto = config.get(CONF_PRIV_PROTOCOL) + privproto: str = config[CONF_PRIV_PROTOCOL] payload_on = config.get(CONF_PAYLOAD_ON) payload_off = config.get(CONF_PAYLOAD_OFF) vartype = config.get(CONF_VARTYPE) + if version == "3": + if not authkey: + authproto = "none" + if not privkey: + privproto = "none" + + auth_data = UsmUserData( + username, + authKey=authkey or None, + privKey=privkey or None, + authProtocol=getattr(hlapi, MAP_AUTH_PROTOCOLS[authproto]), + privProtocol=getattr(hlapi, MAP_PRIV_PROTOCOLS[privproto]), + ) + else: + auth_data = CommunityData(community, mpModel=SNMP_VERSIONS[version]) + + request_args = await async_create_request_cmd_args( + hass, auth_data, UdpTransportTarget((host, port)), baseoid + ) + async_add_entities( [ SnmpSwitch( name, host, port, - community, baseoid, command_oid, - version, - username, - authkey, - authproto, - privkey, - privproto, payload_on, payload_off, command_payload_on, command_payload_off, vartype, + request_args, ) ], True, @@ -180,21 +191,15 @@ class SnmpSwitch(SwitchEntity): name, host, port, - community, baseoid, commandoid, - version, - username, - authkey, - authproto, - privkey, - privproto, payload_on, payload_off, command_payload_on, command_payload_off, vartype, - ): + request_args, + ) -> None: """Initialize the switch.""" self._name = name @@ -206,35 +211,11 @@ class SnmpSwitch(SwitchEntity): self._command_payload_on = command_payload_on or payload_on self._command_payload_off = command_payload_off or payload_off - self._state = None + self._state: bool | None = None self._payload_on = payload_on self._payload_off = payload_off - - if version == "3": - if not authkey: - authproto = "none" - if not privkey: - privproto = "none" - - self._request_args = [ - SnmpEngine(), - UsmUserData( - username, - authKey=authkey or None, - privKey=privkey or None, - authProtocol=getattr(hlapi, MAP_AUTH_PROTOCOLS[authproto]), - privProtocol=getattr(hlapi, MAP_PRIV_PROTOCOLS[privproto]), - ), - UdpTransportTarget((host, port)), - ContextData(), - ] - else: - self._request_args = [ - SnmpEngine(), - CommunityData(community, mpModel=SNMP_VERSIONS[version]), - UdpTransportTarget((host, port)), - ContextData(), - ] + self._target = UdpTransportTarget((host, port)) + self._request_args: RequestArgsType = request_args async def async_turn_on(self, **kwargs: Any) -> None: """Turn on the switch.""" @@ -259,9 +240,7 @@ class SnmpSwitch(SwitchEntity): async def async_update(self) -> None: """Update the state.""" - get_result = await getCmd( - *self._request_args, ObjectType(ObjectIdentity(self._baseoid)) - ) + get_result = await getCmd(*self._request_args) errindication, errstatus, errindex, restable = get_result if errindication: @@ -296,6 +275,4 @@ class SnmpSwitch(SwitchEntity): return self._state async def _set(self, value): - await setCmd( - *self._request_args, ObjectType(ObjectIdentity(self._commandoid), value) - ) + await setCmd(*self._request_args, value) diff --git a/homeassistant/components/snmp/util.py b/homeassistant/components/snmp/util.py new file mode 100644 index 00000000000..23adbdf0b90 --- /dev/null +++ b/homeassistant/components/snmp/util.py @@ -0,0 +1,76 @@ +"""Support for displaying collected data over SNMP.""" + +from __future__ import annotations + +import logging + +from pysnmp.hlapi.asyncio import ( + CommunityData, + ContextData, + ObjectIdentity, + ObjectType, + SnmpEngine, + Udp6TransportTarget, + UdpTransportTarget, + UsmUserData, +) +from pysnmp.hlapi.asyncio.cmdgen import lcd, vbProcessor +from pysnmp.smi.builder import MibBuilder + +from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.helpers.singleton import singleton + +DATA_SNMP_ENGINE = "snmp_engine" + +_LOGGER = logging.getLogger(__name__) + +type RequestArgsType = tuple[ + SnmpEngine, + UsmUserData | CommunityData, + UdpTransportTarget | Udp6TransportTarget, + ContextData, + ObjectType, +] + + +async def async_create_request_cmd_args( + hass: HomeAssistant, + auth_data: UsmUserData | CommunityData, + target: UdpTransportTarget | Udp6TransportTarget, + object_id: str, +) -> RequestArgsType: + """Create request arguments.""" + return ( + await async_get_snmp_engine(hass), + auth_data, + target, + ContextData(), + ObjectType(ObjectIdentity(object_id)), + ) + + +@singleton(DATA_SNMP_ENGINE) +async def async_get_snmp_engine(hass: HomeAssistant) -> SnmpEngine: + """Get the SNMP engine.""" + engine = await hass.async_add_executor_job(_get_snmp_engine) + + @callback + def _async_shutdown_listener(ev: Event) -> None: + _LOGGER.debug("Unconfiguring SNMP engine") + lcd.unconfigure(engine, None) + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_shutdown_listener) + return engine + + +def _get_snmp_engine() -> SnmpEngine: + """Return a cached instance of SnmpEngine.""" + engine = SnmpEngine() + mib_controller = vbProcessor.getMibViewController(engine) + # Actually load the MIBs from disk so we do + # not do it in the event loop + builder: MibBuilder = mib_controller.mibBuilder + if "PYSNMP-MIB" not in builder.mibSymbols: + builder.loadModules() + return engine diff --git a/tests/components/snmp/test_init.py b/tests/components/snmp/test_init.py new file mode 100644 index 00000000000..0aa97dcc475 --- /dev/null +++ b/tests/components/snmp/test_init.py @@ -0,0 +1,22 @@ +"""SNMP tests.""" + +from unittest.mock import patch + +from pysnmp.hlapi.asyncio import SnmpEngine +from pysnmp.hlapi.asyncio.cmdgen import lcd + +from homeassistant.components import snmp +from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.core import HomeAssistant + + +async def test_async_get_snmp_engine(hass: HomeAssistant) -> None: + """Test async_get_snmp_engine.""" + engine = await snmp.async_get_snmp_engine(hass) + assert isinstance(engine, SnmpEngine) + engine2 = await snmp.async_get_snmp_engine(hass) + assert engine is engine2 + with patch.object(lcd, "unconfigure") as mock_unconfigure: + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + assert mock_unconfigure.called From 5ed9d58a7bcd37b530bbaec5e27b7ec32c5a8a40 Mon Sep 17 00:00:00 2001 From: Luca Angemi Date: Fri, 31 May 2024 14:45:52 +0200 Subject: [PATCH 0073/1445] Fix telegram doing blocking I/O in the event loop (#118531) --- homeassistant/components/telegram_bot/__init__.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/telegram_bot/__init__.py b/homeassistant/components/telegram_bot/__init__.py index 7a056665ed4..df5bebb47d4 100644 --- a/homeassistant/components/telegram_bot/__init__.py +++ b/homeassistant/components/telegram_bot/__init__.py @@ -284,6 +284,12 @@ SERVICE_MAP = { } +def _read_file_as_bytesio(file_path: str) -> io.BytesIO: + """Read a file and return it as a BytesIO object.""" + with open(file_path, "rb") as file: + return io.BytesIO(file.read()) + + async def load_data( hass, url=None, @@ -342,7 +348,9 @@ async def load_data( ) elif filepath is not None: if hass.config.is_allowed_path(filepath): - return open(filepath, "rb") + return await hass.async_add_executor_job( + _read_file_as_bytesio, filepath + ) _LOGGER.warning("'%s' are not secure to load data from!", filepath) else: From c85743822ae8f551c92ba7d064456b83a2c43051 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Fri, 31 May 2024 14:52:43 +0200 Subject: [PATCH 0074/1445] In Brother integration use SnmpEngine from SNMP integration (#118554) Co-authored-by: Maciej Bieniek <478555+bieniu@users.noreply.github.com> Co-authored-by: J. Nick Koston --- homeassistant/components/brother/__init__.py | 22 +++---------- .../components/brother/config_flow.py | 6 ++-- homeassistant/components/brother/const.py | 2 -- .../components/brother/manifest.json | 1 + homeassistant/components/brother/utils.py | 33 ------------------- tests/components/brother/test_init.py | 24 ++------------ 6 files changed, 10 insertions(+), 78 deletions(-) delete mode 100644 homeassistant/components/brother/utils.py diff --git a/homeassistant/components/brother/__init__.py b/homeassistant/components/brother/__init__.py index 68255d66566..e828d35f9c7 100644 --- a/homeassistant/components/brother/__init__.py +++ b/homeassistant/components/brother/__init__.py @@ -3,16 +3,14 @@ from __future__ import annotations from brother import Brother, SnmpError -from pysnmp.hlapi.asyncio.cmdgen import lcd -from homeassistant.config_entries import ConfigEntry, ConfigEntryState +from homeassistant.components.snmp import async_get_snmp_engine +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_TYPE, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady -from .const import DOMAIN, SNMP_ENGINE from .coordinator import BrotherDataUpdateCoordinator -from .utils import get_snmp_engine PLATFORMS = [Platform.SENSOR] @@ -24,7 +22,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: BrotherConfigEntry) -> b host = entry.data[CONF_HOST] printer_type = entry.data[CONF_TYPE] - snmp_engine = get_snmp_engine(hass) + snmp_engine = await async_get_snmp_engine(hass) try: brother = await Brother.create( host, printer_type=printer_type, snmp_engine=snmp_engine @@ -44,16 +42,4 @@ async def async_setup_entry(hass: HomeAssistant, entry: BrotherConfigEntry) -> b async def async_unload_entry(hass: HomeAssistant, entry: BrotherConfigEntry) -> bool: """Unload a config entry.""" - unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - - loaded_entries = [ - entry - for entry in hass.config_entries.async_entries(DOMAIN) - if entry.state == ConfigEntryState.LOADED - ] - # We only want to remove the SNMP engine when unloading the last config entry - if unload_ok and len(loaded_entries) == 1: - lcd.unconfigure(hass.data[SNMP_ENGINE], None) - hass.data.pop(SNMP_ENGINE) - - return unload_ok + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/brother/config_flow.py b/homeassistant/components/brother/config_flow.py index ca2f1ae5a39..2b711186fff 100644 --- a/homeassistant/components/brother/config_flow.py +++ b/homeassistant/components/brother/config_flow.py @@ -8,13 +8,13 @@ from brother import Brother, SnmpError, UnsupportedModelError import voluptuous as vol from homeassistant.components import zeroconf +from homeassistant.components.snmp import async_get_snmp_engine from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_HOST, CONF_TYPE from homeassistant.exceptions import HomeAssistantError from homeassistant.util.network import is_host_valid from .const import DOMAIN, PRINTER_TYPES -from .utils import get_snmp_engine DATA_SCHEMA = vol.Schema( { @@ -45,7 +45,7 @@ class BrotherConfigFlow(ConfigFlow, domain=DOMAIN): if not is_host_valid(user_input[CONF_HOST]): raise InvalidHost - snmp_engine = get_snmp_engine(self.hass) + snmp_engine = await async_get_snmp_engine(self.hass) brother = await Brother.create( user_input[CONF_HOST], snmp_engine=snmp_engine @@ -79,7 +79,7 @@ class BrotherConfigFlow(ConfigFlow, domain=DOMAIN): # Do not probe the device if the host is already configured self._async_abort_entries_match({CONF_HOST: self.host}) - snmp_engine = get_snmp_engine(self.hass) + snmp_engine = await async_get_snmp_engine(self.hass) model = discovery_info.properties.get("product") try: diff --git a/homeassistant/components/brother/const.py b/homeassistant/components/brother/const.py index 1b949e1fa52..c0ae7cf60b0 100644 --- a/homeassistant/components/brother/const.py +++ b/homeassistant/components/brother/const.py @@ -9,6 +9,4 @@ DOMAIN: Final = "brother" PRINTER_TYPES: Final = ["laser", "ink"] -SNMP_ENGINE: Final = "snmp_engine" - UPDATE_INTERVAL = timedelta(seconds=30) diff --git a/homeassistant/components/brother/manifest.json b/homeassistant/components/brother/manifest.json index 3bbaf40f686..6d4912db4cb 100644 --- a/homeassistant/components/brother/manifest.json +++ b/homeassistant/components/brother/manifest.json @@ -1,6 +1,7 @@ { "domain": "brother", "name": "Brother Printer", + "after_dependencies": ["snmp"], "codeowners": ["@bieniu"], "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/brother", diff --git a/homeassistant/components/brother/utils.py b/homeassistant/components/brother/utils.py deleted file mode 100644 index 0d11f7d2e82..00000000000 --- a/homeassistant/components/brother/utils.py +++ /dev/null @@ -1,33 +0,0 @@ -"""Brother helpers functions.""" - -from __future__ import annotations - -import logging - -import pysnmp.hlapi.asyncio as hlapi -from pysnmp.hlapi.asyncio.cmdgen import lcd - -from homeassistant.const import EVENT_HOMEASSISTANT_STOP -from homeassistant.core import Event, HomeAssistant, callback -from homeassistant.helpers import singleton - -from .const import SNMP_ENGINE - -_LOGGER = logging.getLogger(__name__) - - -@singleton.singleton(SNMP_ENGINE) -def get_snmp_engine(hass: HomeAssistant) -> hlapi.SnmpEngine: - """Get SNMP engine.""" - _LOGGER.debug("Creating SNMP engine") - snmp_engine = hlapi.SnmpEngine() - - @callback - def shutdown_listener(ev: Event) -> None: - if hass.data.get(SNMP_ENGINE): - _LOGGER.debug("Unconfiguring SNMP engine") - lcd.unconfigure(hass.data[SNMP_ENGINE], None) - - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, shutdown_listener) - - return snmp_engine diff --git a/tests/components/brother/test_init.py b/tests/components/brother/test_init.py index 2b366348b03..1a2c6bf23f2 100644 --- a/tests/components/brother/test_init.py +++ b/tests/components/brother/test_init.py @@ -7,7 +7,6 @@ import pytest from homeassistant.components.brother.const import DOMAIN from homeassistant.config_entries import ConfigEntryState -from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant from . import init_integration @@ -64,27 +63,8 @@ async def test_unload_entry( assert len(hass.config_entries.async_entries(DOMAIN)) == 1 assert mock_config_entry.state is ConfigEntryState.LOADED - with patch("homeassistant.components.brother.lcd.unconfigure") as mock_unconfigure: - assert await hass.config_entries.async_unload(mock_config_entry.entry_id) - await hass.async_block_till_done() - assert mock_unconfigure.called + assert await hass.config_entries.async_unload(mock_config_entry.entry_id) + await hass.async_block_till_done() assert mock_config_entry.state is ConfigEntryState.NOT_LOADED assert not hass.data.get(DOMAIN) - - -async def test_unconfigure_snmp_engine_on_ha_stop( - hass: HomeAssistant, - mock_brother_client: AsyncMock, - mock_config_entry: MockConfigEntry, -) -> None: - """Test that the SNMP engine is unconfigured when HA stops.""" - await init_integration(hass, mock_config_entry) - - with patch( - "homeassistant.components.brother.utils.lcd.unconfigure" - ) as mock_unconfigure: - hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) - await hass.async_block_till_done() - - assert mock_unconfigure.called From 929568c3b5d540daf65891b29d9a011142f3a77e Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Fri, 31 May 2024 22:54:40 +1000 Subject: [PATCH 0075/1445] Fix off_grid_vehicle_charging_reserve_percent in Teselemetry (#118532) --- homeassistant/components/teslemetry/number.py | 2 +- homeassistant/components/teslemetry/strings.json | 2 +- tests/components/teslemetry/snapshots/test_number.ambr | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/teslemetry/number.py b/homeassistant/components/teslemetry/number.py index 7551529006b..592c20c3e4a 100644 --- a/homeassistant/components/teslemetry/number.py +++ b/homeassistant/components/teslemetry/number.py @@ -82,7 +82,7 @@ ENERGY_INFO_DESCRIPTIONS: tuple[TeslemetryNumberBatteryEntityDescription, ...] = requires="components_battery", ), TeslemetryNumberBatteryEntityDescription( - key="off_grid_vehicle_charging_reserve", + key="off_grid_vehicle_charging_reserve_percent", func=lambda api, value: api.off_grid_vehicle_charging_reserve(int(value)), requires="components_off_grid_vehicle_charging_reserve_supported", ), diff --git a/homeassistant/components/teslemetry/strings.json b/homeassistant/components/teslemetry/strings.json index 98b1f7f1932..b1b794404f4 100644 --- a/homeassistant/components/teslemetry/strings.json +++ b/homeassistant/components/teslemetry/strings.json @@ -254,7 +254,7 @@ "charge_state_charge_limit_soc": { "name": "Charge limit" }, - "off_grid_vehicle_charging_reserve": { + "off_grid_vehicle_charging_reserve_percent": { "name": "Off grid reserve" } }, diff --git a/tests/components/teslemetry/snapshots/test_number.ambr b/tests/components/teslemetry/snapshots/test_number.ambr index 7ead67a1e95..f33b5e15d30 100644 --- a/tests/components/teslemetry/snapshots/test_number.ambr +++ b/tests/components/teslemetry/snapshots/test_number.ambr @@ -90,8 +90,8 @@ 'platform': 'teslemetry', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'off_grid_vehicle_charging_reserve', - 'unique_id': '123456-off_grid_vehicle_charging_reserve', + 'translation_key': 'off_grid_vehicle_charging_reserve_percent', + 'unique_id': '123456-off_grid_vehicle_charging_reserve_percent', 'unit_of_measurement': '%', }) # --- From 8f5ddd5bccba89ecc375a49de2e0f1da2d84c48d Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Fri, 31 May 2024 16:00:33 +0200 Subject: [PATCH 0076/1445] Bump `brother` backend library to version `4.2.0` (#118557) Co-authored-by: Maciej Bieniek <478555+bieniu@users.noreply.github.com> --- homeassistant/components/brother/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/brother/manifest.json b/homeassistant/components/brother/manifest.json index 6d4912db4cb..5caaeb2f1a1 100644 --- a/homeassistant/components/brother/manifest.json +++ b/homeassistant/components/brother/manifest.json @@ -9,7 +9,7 @@ "iot_class": "local_polling", "loggers": ["brother", "pyasn1", "pysmi", "pysnmp"], "quality_scale": "platinum", - "requirements": ["brother==4.1.0"], + "requirements": ["brother==4.2.0"], "zeroconf": [ { "type": "_printer._tcp.local.", diff --git a/requirements_all.txt b/requirements_all.txt index 5806c031e78..fb5fe9b63a1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -613,7 +613,7 @@ bring-api==0.7.1 broadlink==0.19.0 # homeassistant.components.brother -brother==4.1.0 +brother==4.2.0 # homeassistant.components.brottsplatskartan brottsplatskartan==1.0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bcb2ab8ea06..3bf3dbc1c5a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -524,7 +524,7 @@ bring-api==0.7.1 broadlink==0.19.0 # homeassistant.components.brother -brother==4.1.0 +brother==4.2.0 # homeassistant.components.brottsplatskartan brottsplatskartan==1.0.5 From cf3e758aa12955ec2a189f26f5405fdf4fa052e1 Mon Sep 17 00:00:00 2001 From: osohotwateriot <102795312+osohotwateriot@users.noreply.github.com> Date: Fri, 31 May 2024 17:13:20 +0300 Subject: [PATCH 0077/1445] Move OSO Energy base entity class to separate module (#118563) Move base entity class to separate file --- .coveragerc | 1 + .../components/osoenergy/__init__.py | 31 --------------- .../components/osoenergy/binary_sensor.py | 2 +- homeassistant/components/osoenergy/entity.py | 38 +++++++++++++++++++ homeassistant/components/osoenergy/sensor.py | 2 +- .../components/osoenergy/water_heater.py | 2 +- 6 files changed, 42 insertions(+), 34 deletions(-) create mode 100644 homeassistant/components/osoenergy/entity.py diff --git a/.coveragerc b/.coveragerc index 331359c5d0b..4f839ffccdd 100644 --- a/.coveragerc +++ b/.coveragerc @@ -984,6 +984,7 @@ omit = homeassistant/components/orvibo/switch.py homeassistant/components/osoenergy/__init__.py homeassistant/components/osoenergy/binary_sensor.py + homeassistant/components/osoenergy/entity.py homeassistant/components/osoenergy/sensor.py homeassistant/components/osoenergy/water_heater.py homeassistant/components/osramlightify/light.py diff --git a/homeassistant/components/osoenergy/__init__.py b/homeassistant/components/osoenergy/__init__.py index 3ba48eac2d1..ca6d52941f7 100644 --- a/homeassistant/components/osoenergy/__init__.py +++ b/homeassistant/components/osoenergy/__init__.py @@ -4,11 +4,6 @@ from typing import Any from aiohttp.web_exceptions import HTTPException from apyosoenergyapi import OSOEnergy -from apyosoenergyapi.helper.const import ( - OSOEnergyBinarySensorData, - OSOEnergySensorData, - OSOEnergyWaterHeaterData, -) from apyosoenergyapi.helper.osoenergy_exceptions import OSOEnergyReauthRequired from homeassistant.config_entries import ConfigEntry @@ -16,12 +11,9 @@ from homeassistant.const import CONF_API_KEY, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import aiohttp_client -from homeassistant.helpers.device_registry import DeviceInfo -from homeassistant.helpers.entity import Entity from .const import DOMAIN -MANUFACTURER = "OSO Energy" PLATFORMS = [ Platform.BINARY_SENSOR, Platform.SENSOR, @@ -70,26 +62,3 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN].pop(entry.entry_id) return unload_ok - - -class OSOEnergyEntity[ - _OSOEnergyT: ( - OSOEnergyBinarySensorData, - OSOEnergySensorData, - OSOEnergyWaterHeaterData, - ) -](Entity): - """Initiate OSO Energy Base Class.""" - - _attr_has_entity_name = True - - def __init__(self, osoenergy: OSOEnergy, entity_data: _OSOEnergyT) -> None: - """Initialize the instance.""" - self.osoenergy = osoenergy - self.entity_data = entity_data - self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, entity_data.device_id)}, - manufacturer=MANUFACTURER, - model=entity_data.device_type, - name=entity_data.device_name, - ) diff --git a/homeassistant/components/osoenergy/binary_sensor.py b/homeassistant/components/osoenergy/binary_sensor.py index 22081b64f15..0cf0ac74d36 100644 --- a/homeassistant/components/osoenergy/binary_sensor.py +++ b/homeassistant/components/osoenergy/binary_sensor.py @@ -14,8 +14,8 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import OSOEnergyEntity from .const import DOMAIN +from .entity import OSOEnergyEntity @dataclass(frozen=True, kw_only=True) diff --git a/homeassistant/components/osoenergy/entity.py b/homeassistant/components/osoenergy/entity.py new file mode 100644 index 00000000000..2a2210339d7 --- /dev/null +++ b/homeassistant/components/osoenergy/entity.py @@ -0,0 +1,38 @@ +"""Parent class for every OSO Energy device.""" + +from apyosoenergyapi import OSOEnergy +from apyosoenergyapi.helper.const import ( + OSOEnergyBinarySensorData, + OSOEnergySensorData, + OSOEnergyWaterHeaterData, +) + +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity import Entity + +from .const import DOMAIN + +MANUFACTURER = "OSO Energy" + + +class OSOEnergyEntity[ + _OSOEnergyT: ( + OSOEnergyBinarySensorData, + OSOEnergySensorData, + OSOEnergyWaterHeaterData, + ) +](Entity): + """Initiate OSO Energy Base Class.""" + + _attr_has_entity_name = True + + def __init__(self, osoenergy: OSOEnergy, entity_data: _OSOEnergyT) -> None: + """Initialize the instance.""" + self.osoenergy = osoenergy + self.entity_data = entity_data + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, entity_data.device_id)}, + manufacturer=MANUFACTURER, + model=entity_data.device_type, + name=entity_data.device_name, + ) diff --git a/homeassistant/components/osoenergy/sensor.py b/homeassistant/components/osoenergy/sensor.py index 0be6ad83281..772c3c0a69e 100644 --- a/homeassistant/components/osoenergy/sensor.py +++ b/homeassistant/components/osoenergy/sensor.py @@ -18,8 +18,8 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType -from . import OSOEnergyEntity from .const import DOMAIN +from .entity import OSOEnergyEntity @dataclass(frozen=True, kw_only=True) diff --git a/homeassistant/components/osoenergy/water_heater.py b/homeassistant/components/osoenergy/water_heater.py index b7fb2ba16e6..55229e42c2f 100644 --- a/homeassistant/components/osoenergy/water_heater.py +++ b/homeassistant/components/osoenergy/water_heater.py @@ -18,8 +18,8 @@ from homeassistant.const import UnitOfTemperature from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import OSOEnergyEntity from .const import DOMAIN +from .entity import OSOEnergyEntity CURRENT_OPERATION_MAP: dict[str, Any] = { "default": { From bff2d3e2eeaf8f093a13f099123cf06356dcd7b2 Mon Sep 17 00:00:00 2001 From: Bas Brussee <68892092+basbruss@users.noreply.github.com> Date: Fri, 31 May 2024 16:50:22 +0200 Subject: [PATCH 0078/1445] Revert "Fix Tibber sensors state class" (#118409) Revert "Fix Tibber sensors state class (#117085)" This reverts commit 658c1f3d97a8a8eb0d91150e09b36c995a4863c5. --- homeassistant/components/tibber/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/tibber/sensor.py b/homeassistant/components/tibber/sensor.py index f0131173403..8d036157494 100644 --- a/homeassistant/components/tibber/sensor.py +++ b/homeassistant/components/tibber/sensor.py @@ -118,7 +118,7 @@ RT_SENSORS: tuple[SensorEntityDescription, ...] = ( translation_key="accumulated_consumption", device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, - state_class=SensorStateClass.TOTAL_INCREASING, + state_class=SensorStateClass.TOTAL, ), SensorEntityDescription( key="accumulatedConsumptionLastHour", @@ -138,7 +138,7 @@ RT_SENSORS: tuple[SensorEntityDescription, ...] = ( translation_key="accumulated_production", device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, - state_class=SensorStateClass.TOTAL_INCREASING, + state_class=SensorStateClass.TOTAL, ), SensorEntityDescription( key="accumulatedProductionLastHour", From d67f14ac0b46281f5f3559fb8b9e7ef2228cadf5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 31 May 2024 09:51:38 -0500 Subject: [PATCH 0079/1445] Fix openweathermap config entry migration (#118526) * Fix openweathermap config entry migration The options keys were accidentally migrated to data so they could no longer be changed in the options flow * more fixes * adjust * reduce * fix * adjust --- .../components/openweathermap/__init__.py | 22 +++++++++---------- .../components/openweathermap/config_flow.py | 5 +++-- .../components/openweathermap/const.py | 2 +- .../components/openweathermap/utils.py | 20 +++++++++++++++++ 4 files changed, 34 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/openweathermap/__init__.py b/homeassistant/components/openweathermap/__init__.py index 44c5179f227..7aea6aafe20 100644 --- a/homeassistant/components/openweathermap/__init__.py +++ b/homeassistant/components/openweathermap/__init__.py @@ -4,7 +4,6 @@ from __future__ import annotations from dataclasses import dataclass import logging -from typing import Any from pyopenweathermap import OWMClient @@ -22,6 +21,7 @@ from homeassistant.core import HomeAssistant from .const import CONFIG_FLOW_VERSION, OWM_MODE_V25, PLATFORMS from .coordinator import WeatherUpdateCoordinator from .repairs import async_create_issue, async_delete_issue +from .utils import build_data_and_options _LOGGER = logging.getLogger(__name__) @@ -44,8 +44,8 @@ async def async_setup_entry( api_key = entry.data[CONF_API_KEY] latitude = entry.data.get(CONF_LATITUDE, hass.config.latitude) longitude = entry.data.get(CONF_LONGITUDE, hass.config.longitude) - language = _get_config_value(entry, CONF_LANGUAGE) - mode = _get_config_value(entry, CONF_MODE) + language = entry.options[CONF_LANGUAGE] + mode = entry.options[CONF_MODE] if mode == OWM_MODE_V25: async_create_issue(hass, entry.entry_id) @@ -77,10 +77,14 @@ async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: _LOGGER.debug("Migrating OpenWeatherMap entry from version %s", version) - if version < 4: - new_data = {**data, **options, CONF_MODE: OWM_MODE_V25} + if version < 5: + combined_data = {**data, **options, CONF_MODE: OWM_MODE_V25} + new_data, new_options = build_data_and_options(combined_data) config_entries.async_update_entry( - entry, data=new_data, options={}, version=CONFIG_FLOW_VERSION + entry, + data=new_data, + options=new_options, + version=CONFIG_FLOW_VERSION, ) _LOGGER.info("Migration to version %s successful", CONFIG_FLOW_VERSION) @@ -98,9 +102,3 @@ async def async_unload_entry( ) -> bool: """Unload a config entry.""" return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - - -def _get_config_value(config_entry: ConfigEntry, key: str) -> Any: - if config_entry.options and key in config_entry.options: - return config_entry.options[key] - return config_entry.data[key] diff --git a/homeassistant/components/openweathermap/config_flow.py b/homeassistant/components/openweathermap/config_flow.py index 3090af94979..5fe06ea2dcd 100644 --- a/homeassistant/components/openweathermap/config_flow.py +++ b/homeassistant/components/openweathermap/config_flow.py @@ -30,7 +30,7 @@ from .const import ( LANGUAGES, OWM_MODES, ) -from .utils import validate_api_key +from .utils import build_data_and_options, validate_api_key class OpenWeatherMapConfigFlow(ConfigFlow, domain=DOMAIN): @@ -64,8 +64,9 @@ class OpenWeatherMapConfigFlow(ConfigFlow, domain=DOMAIN): ) if not errors: + data, options = build_data_and_options(user_input) return self.async_create_entry( - title=user_input[CONF_NAME], data=user_input + title=user_input[CONF_NAME], data=data, options=options ) schema = vol.Schema( diff --git a/homeassistant/components/openweathermap/const.py b/homeassistant/components/openweathermap/const.py index c074640ebc7..456ec05b038 100644 --- a/homeassistant/components/openweathermap/const.py +++ b/homeassistant/components/openweathermap/const.py @@ -25,7 +25,7 @@ DEFAULT_NAME = "OpenWeatherMap" DEFAULT_LANGUAGE = "en" ATTRIBUTION = "Data provided by OpenWeatherMap" MANUFACTURER = "OpenWeather" -CONFIG_FLOW_VERSION = 4 +CONFIG_FLOW_VERSION = 5 ATTR_API_PRECIPITATION = "precipitation" ATTR_API_PRECIPITATION_KIND = "precipitation_kind" ATTR_API_DATETIME = "datetime" diff --git a/homeassistant/components/openweathermap/utils.py b/homeassistant/components/openweathermap/utils.py index cbdd1eab815..7f2391b21a1 100644 --- a/homeassistant/components/openweathermap/utils.py +++ b/homeassistant/components/openweathermap/utils.py @@ -1,7 +1,15 @@ """Util functions for OpenWeatherMap.""" +from typing import Any + from pyopenweathermap import OWMClient, RequestError +from homeassistant.const import CONF_LANGUAGE, CONF_MODE + +from .const import DEFAULT_LANGUAGE, DEFAULT_OWM_MODE + +OPTION_DEFAULTS = {CONF_LANGUAGE: DEFAULT_LANGUAGE, CONF_MODE: DEFAULT_OWM_MODE} + async def validate_api_key(api_key, mode): """Validate API key.""" @@ -18,3 +26,15 @@ async def validate_api_key(api_key, mode): errors["base"] = "invalid_api_key" return errors, description_placeholders + + +def build_data_and_options( + combined_data: dict[str, Any], +) -> tuple[dict[str, Any], dict[str, Any]]: + """Split combined data and options.""" + data = {k: v for k, v in combined_data.items() if k not in OPTION_DEFAULTS} + options = { + option: combined_data.get(option, default) + for option, default in OPTION_DEFAULTS.items() + } + return (data, options) From 15f726da507249f458f4b98369938c974d0c4cd0 Mon Sep 17 00:00:00 2001 From: Michael Chisholm Date: Sat, 1 Jun 2024 00:52:19 +1000 Subject: [PATCH 0080/1445] Fix KeyError in dlna_dmr SSDP config flow when checking existing config entries (#118549) Fix KeyError checking existing dlna_dmr config entries --- homeassistant/components/dlna_dmr/config_flow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/dlna_dmr/config_flow.py b/homeassistant/components/dlna_dmr/config_flow.py index 7d9efc4096c..6b551f0e999 100644 --- a/homeassistant/components/dlna_dmr/config_flow.py +++ b/homeassistant/components/dlna_dmr/config_flow.py @@ -149,7 +149,7 @@ class DlnaDmrFlowHandler(ConfigFlow, domain=DOMAIN): # case the device doesn't have a static and unique UDN (breaking the # UPnP spec). for entry in self._async_current_entries(include_ignore=True): - if self._location == entry.data[CONF_URL]: + if self._location == entry.data.get(CONF_URL): return self.async_abort(reason="already_configured") if self._mac and self._mac == entry.data.get(CONF_MAC): return self.async_abort(reason="already_configured") From 1fef4fa1f6bef91aa58daef259bd1d759b1e4ea9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 31 May 2024 10:08:22 -0500 Subject: [PATCH 0081/1445] Prevent time.sleep calls from blocking the event loop (#118561) * Prevent time.sleep calls from blocking the event loop We have been warning on these since Jan 2022. 2+ years seems more than enough time to give to fix these. see https://github.com/home-assistant/core/pull/63766 * Prevent time.sleep calls from blocking the event loop We have been warning on these since Jan 2022. 2+ years seems more than enough time to give to fix these. see https://github.com/home-assistant/core/pull/63766 --- homeassistant/block_async_io.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/block_async_io.py b/homeassistant/block_async_io.py index 1e47e30876c..5f58925c53c 100644 --- a/homeassistant/block_async_io.py +++ b/homeassistant/block_async_io.py @@ -52,10 +52,9 @@ def enable() -> None: HTTPConnection.putrequest, loop_thread_id=loop_thread_id ) - # Prevent sleeping in event loop. Non-strict since 2022.02 + # Prevent sleeping in event loop. time.sleep = protect_loop( time.sleep, - strict=False, check_allowed=_check_sleep_call_allowed, loop_thread_id=loop_thread_id, ) From 6656f7d6b9cd3d71b1e06222621ff0b8c5c361dd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 31 May 2024 10:09:19 -0500 Subject: [PATCH 0082/1445] Log directory blocking I/O functions that run in the event loop (#118529) * Log directory I/O functions that run in the event loop * tests --- homeassistant/block_async_io.py | 16 ++++++++++ tests/test_block_async_io.py | 55 +++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+) diff --git a/homeassistant/block_async_io.py b/homeassistant/block_async_io.py index 5f58925c53c..e829ed4925b 100644 --- a/homeassistant/block_async_io.py +++ b/homeassistant/block_async_io.py @@ -2,8 +2,10 @@ import builtins from contextlib import suppress +import glob from http.client import HTTPConnection import importlib +import os import sys import threading import time @@ -59,8 +61,22 @@ def enable() -> None: loop_thread_id=loop_thread_id, ) + glob.glob = protect_loop( + glob.glob, strict_core=False, strict=False, loop_thread_id=loop_thread_id + ) + glob.iglob = protect_loop( + glob.iglob, strict_core=False, strict=False, loop_thread_id=loop_thread_id + ) + if not _IN_TESTS: # Prevent files being opened inside the event loop + os.listdir = protect_loop( # type: ignore[assignment] + os.listdir, strict_core=False, strict=False, loop_thread_id=loop_thread_id + ) + os.scandir = protect_loop( # type: ignore[assignment] + os.scandir, strict_core=False, strict=False, loop_thread_id=loop_thread_id + ) + builtins.open = protect_loop( # type: ignore[assignment] builtins.open, strict_core=False, diff --git a/tests/test_block_async_io.py b/tests/test_block_async_io.py index 11b83bdcd3a..e4f248e80d1 100644 --- a/tests/test_block_async_io.py +++ b/tests/test_block_async_io.py @@ -1,7 +1,9 @@ """Tests for async util methods from Python source.""" import contextlib +import glob import importlib +import os from pathlib import Path, PurePosixPath import time from typing import Any @@ -10,6 +12,7 @@ from unittest.mock import Mock, patch import pytest from homeassistant import block_async_io +from homeassistant.core import HomeAssistant from tests.common import extract_stack_to_frame @@ -235,3 +238,55 @@ async def test_protect_open_path(path: Any, caplog: pytest.LogCaptureFixture) -> open(path).close() assert "Detected blocking call to open with args" in caplog.text + + +async def test_protect_loop_glob( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Test glob calls in the loop are logged.""" + block_async_io.enable() + glob.glob("/dev/null") + assert "Detected blocking call to glob with args" in caplog.text + caplog.clear() + await hass.async_add_executor_job(glob.glob, "/dev/null") + assert "Detected blocking call to glob with args" not in caplog.text + + +async def test_protect_loop_iglob( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Test iglob calls in the loop are logged.""" + block_async_io.enable() + glob.iglob("/dev/null") + assert "Detected blocking call to iglob with args" in caplog.text + caplog.clear() + await hass.async_add_executor_job(glob.iglob, "/dev/null") + assert "Detected blocking call to iglob with args" not in caplog.text + + +async def test_protect_loop_scandir( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Test glob calls in the loop are logged.""" + block_async_io.enable() + with contextlib.suppress(FileNotFoundError): + os.scandir("/path/that/does/not/exists") + assert "Detected blocking call to scandir with args" in caplog.text + caplog.clear() + with contextlib.suppress(FileNotFoundError): + await hass.async_add_executor_job(os.scandir, "/path/that/does/not/exists") + assert "Detected blocking call to listdir with args" not in caplog.text + + +async def test_protect_loop_listdir( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Test listdir calls in the loop are logged.""" + block_async_io.enable() + with contextlib.suppress(FileNotFoundError): + os.listdir("/path/that/does/not/exists") + assert "Detected blocking call to listdir with args" in caplog.text + caplog.clear() + with contextlib.suppress(FileNotFoundError): + await hass.async_add_executor_job(os.listdir, "/path/that/does/not/exists") + assert "Detected blocking call to listdir with args" not in caplog.text From 6dd01dbff744e3a02f69ad8e234d76be2eb5a52f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 31 May 2024 11:11:24 -0400 Subject: [PATCH 0083/1445] Rename llm.ToolContext to llm.LLMContext (#118566) --- .../conversation.py | 2 +- .../openai_conversation/conversation.py | 2 +- homeassistant/helpers/llm.py | 56 +++++++++--------- .../test_conversation.py | 4 +- .../openai_conversation/test_conversation.py | 4 +- tests/helpers/test_llm.py | 58 +++++++++---------- 6 files changed, 62 insertions(+), 64 deletions(-) diff --git a/homeassistant/components/google_generative_ai_conversation/conversation.py b/homeassistant/components/google_generative_ai_conversation/conversation.py index e7aaabb912d..d722403a0be 100644 --- a/homeassistant/components/google_generative_ai_conversation/conversation.py +++ b/homeassistant/components/google_generative_ai_conversation/conversation.py @@ -169,7 +169,7 @@ class GoogleGenerativeAIConversationEntity( llm_api = await llm.async_get_api( self.hass, self.entry.options[CONF_LLM_HASS_API], - llm.ToolContext( + llm.LLMContext( platform=DOMAIN, context=user_input.context, user_prompt=user_input.text, diff --git a/homeassistant/components/openai_conversation/conversation.py b/homeassistant/components/openai_conversation/conversation.py index afc5396e0ba..26acfda979d 100644 --- a/homeassistant/components/openai_conversation/conversation.py +++ b/homeassistant/components/openai_conversation/conversation.py @@ -119,7 +119,7 @@ class OpenAIConversationEntity( llm_api = await llm.async_get_api( self.hass, options[CONF_LLM_HASS_API], - llm.ToolContext( + llm.LLMContext( platform=DOMAIN, context=user_input.context, user_prompt=user_input.text, diff --git a/homeassistant/helpers/llm.py b/homeassistant/helpers/llm.py index b4b5f9137c4..dd380795227 100644 --- a/homeassistant/helpers/llm.py +++ b/homeassistant/helpers/llm.py @@ -71,7 +71,7 @@ def async_register_api(hass: HomeAssistant, api: API) -> None: async def async_get_api( - hass: HomeAssistant, api_id: str, tool_context: ToolContext + hass: HomeAssistant, api_id: str, llm_context: LLMContext ) -> APIInstance: """Get an API.""" apis = _async_get_apis(hass) @@ -79,7 +79,7 @@ async def async_get_api( if api_id not in apis: raise HomeAssistantError(f"API {api_id} not found") - return await apis[api_id].async_get_api_instance(tool_context) + return await apis[api_id].async_get_api_instance(llm_context) @callback @@ -89,7 +89,7 @@ def async_get_apis(hass: HomeAssistant) -> list[API]: @dataclass(slots=True) -class ToolContext: +class LLMContext: """Tool input to be processed.""" platform: str @@ -117,7 +117,7 @@ class Tool: @abstractmethod async def async_call( - self, hass: HomeAssistant, tool_input: ToolInput, tool_context: ToolContext + self, hass: HomeAssistant, tool_input: ToolInput, llm_context: LLMContext ) -> JsonObjectType: """Call the tool.""" raise NotImplementedError @@ -133,7 +133,7 @@ class APIInstance: api: API api_prompt: str - tool_context: ToolContext + llm_context: LLMContext tools: list[Tool] async def async_call_tool(self, tool_input: ToolInput) -> JsonObjectType: @@ -149,7 +149,7 @@ class APIInstance: else: raise HomeAssistantError(f'Tool "{tool_input.tool_name}" not found') - return await tool.async_call(self.api.hass, tool_input, self.tool_context) + return await tool.async_call(self.api.hass, tool_input, self.llm_context) @dataclass(slots=True, kw_only=True) @@ -161,7 +161,7 @@ class API(ABC): name: str @abstractmethod - async def async_get_api_instance(self, tool_context: ToolContext) -> APIInstance: + async def async_get_api_instance(self, llm_context: LLMContext) -> APIInstance: """Return the instance of the API.""" raise NotImplementedError @@ -182,20 +182,20 @@ class IntentTool(Tool): self.parameters = vol.Schema(slot_schema) async def async_call( - self, hass: HomeAssistant, tool_input: ToolInput, tool_context: ToolContext + self, hass: HomeAssistant, tool_input: ToolInput, llm_context: LLMContext ) -> JsonObjectType: """Handle the intent.""" slots = {key: {"value": val} for key, val in tool_input.tool_args.items()} intent_response = await intent.async_handle( hass=hass, - platform=tool_context.platform, + platform=llm_context.platform, intent_type=self.name, slots=slots, - text_input=tool_context.user_prompt, - context=tool_context.context, - language=tool_context.language, - assistant=tool_context.assistant, - device_id=tool_context.device_id, + text_input=llm_context.user_prompt, + context=llm_context.context, + language=llm_context.language, + assistant=llm_context.assistant, + device_id=llm_context.device_id, ) response = intent_response.as_dict() del response["language"] @@ -224,25 +224,25 @@ class AssistAPI(API): name="Assist", ) - async def async_get_api_instance(self, tool_context: ToolContext) -> APIInstance: + async def async_get_api_instance(self, llm_context: LLMContext) -> APIInstance: """Return the instance of the API.""" - if tool_context.assistant: + if llm_context.assistant: exposed_entities: dict | None = _get_exposed_entities( - self.hass, tool_context.assistant + self.hass, llm_context.assistant ) else: exposed_entities = None return APIInstance( api=self, - api_prompt=self._async_get_api_prompt(tool_context, exposed_entities), - tool_context=tool_context, - tools=self._async_get_tools(tool_context, exposed_entities), + api_prompt=self._async_get_api_prompt(llm_context, exposed_entities), + llm_context=llm_context, + tools=self._async_get_tools(llm_context, exposed_entities), ) @callback def _async_get_api_prompt( - self, tool_context: ToolContext, exposed_entities: dict | None + self, llm_context: LLMContext, exposed_entities: dict | None ) -> str: """Return the prompt for the API.""" if not exposed_entities: @@ -263,9 +263,9 @@ class AssistAPI(API): ] area: ar.AreaEntry | None = None floor: fr.FloorEntry | None = None - if tool_context.device_id: + if llm_context.device_id: device_reg = dr.async_get(self.hass) - device = device_reg.async_get(tool_context.device_id) + device = device_reg.async_get(llm_context.device_id) if device: area_reg = ar.async_get(self.hass) @@ -286,8 +286,8 @@ class AssistAPI(API): "ask user to specify an area, unless there is only one device of that type." ) - if not tool_context.device_id or not async_device_supports_timers( - self.hass, tool_context.device_id + if not llm_context.device_id or not async_device_supports_timers( + self.hass, llm_context.device_id ): prompt.append("This device does not support timers.") @@ -301,12 +301,12 @@ class AssistAPI(API): @callback def _async_get_tools( - self, tool_context: ToolContext, exposed_entities: dict | None + self, llm_context: LLMContext, exposed_entities: dict | None ) -> list[Tool]: """Return a list of LLM tools.""" ignore_intents = self.IGNORE_INTENTS - if not tool_context.device_id or not async_device_supports_timers( - self.hass, tool_context.device_id + if not llm_context.device_id or not async_device_supports_timers( + self.hass, llm_context.device_id ): ignore_intents = ignore_intents | { intent.INTENT_START_TIMER, diff --git a/tests/components/google_generative_ai_conversation/test_conversation.py b/tests/components/google_generative_ai_conversation/test_conversation.py index b282895baef..19a855aa17f 100644 --- a/tests/components/google_generative_ai_conversation/test_conversation.py +++ b/tests/components/google_generative_ai_conversation/test_conversation.py @@ -231,7 +231,7 @@ async def test_function_call( "param2": "param2's value", }, ), - llm.ToolContext( + llm.LLMContext( platform="google_generative_ai_conversation", context=context, user_prompt="Please call the test function", @@ -330,7 +330,7 @@ async def test_function_exception( tool_name="test_tool", tool_args={"param1": 1}, ), - llm.ToolContext( + llm.LLMContext( platform="google_generative_ai_conversation", context=context, user_prompt="Please call the test function", diff --git a/tests/components/openai_conversation/test_conversation.py b/tests/components/openai_conversation/test_conversation.py index 4d16973ddfc..10829db7575 100644 --- a/tests/components/openai_conversation/test_conversation.py +++ b/tests/components/openai_conversation/test_conversation.py @@ -192,7 +192,7 @@ async def test_function_call( tool_name="test_tool", tool_args={"param1": "test_value"}, ), - llm.ToolContext( + llm.LLMContext( platform="openai_conversation", context=context, user_prompt="Please call the test function", @@ -324,7 +324,7 @@ async def test_function_exception( tool_name="test_tool", tool_args={"param1": "test_value"}, ), - llm.ToolContext( + llm.LLMContext( platform="openai_conversation", context=context, user_prompt="Please call the test function", diff --git a/tests/helpers/test_llm.py b/tests/helpers/test_llm.py index 355abf2fe5d..9c07295dec7 100644 --- a/tests/helpers/test_llm.py +++ b/tests/helpers/test_llm.py @@ -24,9 +24,9 @@ from tests.common import MockConfigEntry @pytest.fixture -def tool_input_context() -> llm.ToolContext: +def llm_context() -> llm.LLMContext: """Return tool input context.""" - return llm.ToolContext( + return llm.LLMContext( platform="", context=None, user_prompt=None, @@ -37,29 +37,27 @@ def tool_input_context() -> llm.ToolContext: async def test_get_api_no_existing( - hass: HomeAssistant, tool_input_context: llm.ToolContext + hass: HomeAssistant, llm_context: llm.LLMContext ) -> None: """Test getting an llm api where no config exists.""" with pytest.raises(HomeAssistantError): - await llm.async_get_api(hass, "non-existing", tool_input_context) + await llm.async_get_api(hass, "non-existing", llm_context) -async def test_register_api( - hass: HomeAssistant, tool_input_context: llm.ToolContext -) -> None: +async def test_register_api(hass: HomeAssistant, llm_context: llm.LLMContext) -> None: """Test registering an llm api.""" class MyAPI(llm.API): async def async_get_api_instance( - self, tool_input: llm.ToolInput + self, tool_context: llm.ToolInput ) -> llm.APIInstance: """Return a list of tools.""" - return llm.APIInstance(self, "", [], tool_input_context) + return llm.APIInstance(self, "", [], llm_context) api = MyAPI(hass=hass, id="test", name="Test") llm.async_register_api(hass, api) - instance = await llm.async_get_api(hass, "test", tool_input_context) + instance = await llm.async_get_api(hass, "test", llm_context) assert instance.api is api assert api in llm.async_get_apis(hass) @@ -68,10 +66,10 @@ async def test_register_api( async def test_call_tool_no_existing( - hass: HomeAssistant, tool_input_context: llm.ToolContext + hass: HomeAssistant, llm_context: llm.LLMContext ) -> None: """Test calling an llm tool where no config exists.""" - instance = await llm.async_get_api(hass, "assist", tool_input_context) + instance = await llm.async_get_api(hass, "assist", llm_context) with pytest.raises(HomeAssistantError): await instance.async_call_tool( llm.ToolInput("test_tool", {}), @@ -93,7 +91,7 @@ async def test_assist_api( ).write_unavailable_state(hass) test_context = Context() - tool_context = llm.ToolContext( + llm_context = llm.LLMContext( platform="test_platform", context=test_context, user_prompt="test_text", @@ -116,19 +114,19 @@ async def test_assist_api( intent.async_register(hass, intent_handler) assert len(llm.async_get_apis(hass)) == 1 - api = await llm.async_get_api(hass, "assist", tool_context) + api = await llm.async_get_api(hass, "assist", llm_context) assert len(api.tools) == 0 # Match all intent_handler.platforms = None - api = await llm.async_get_api(hass, "assist", tool_context) + api = await llm.async_get_api(hass, "assist", llm_context) assert len(api.tools) == 1 # Match specific domain intent_handler.platforms = {"light"} - api = await llm.async_get_api(hass, "assist", tool_context) + api = await llm.async_get_api(hass, "assist", llm_context) assert len(api.tools) == 1 tool = api.tools[0] assert tool.name == "test_intent" @@ -176,25 +174,25 @@ async def test_assist_api( async def test_assist_api_get_timer_tools( - hass: HomeAssistant, tool_input_context: llm.ToolContext + hass: HomeAssistant, llm_context: llm.LLMContext ) -> None: """Test getting timer tools with Assist API.""" assert await async_setup_component(hass, "homeassistant", {}) assert await async_setup_component(hass, "intent", {}) - api = await llm.async_get_api(hass, "assist", tool_input_context) + api = await llm.async_get_api(hass, "assist", llm_context) assert "HassStartTimer" not in [tool.name for tool in api.tools] - tool_input_context.device_id = "test_device" + llm_context.device_id = "test_device" async_register_timer_handler(hass, "test_device", lambda *args: None) - api = await llm.async_get_api(hass, "assist", tool_input_context) + api = await llm.async_get_api(hass, "assist", llm_context) assert "HassStartTimer" in [tool.name for tool in api.tools] async def test_assist_api_description( - hass: HomeAssistant, tool_input_context: llm.ToolContext + hass: HomeAssistant, llm_context: llm.LLMContext ) -> None: """Test intent description with Assist API.""" @@ -205,7 +203,7 @@ async def test_assist_api_description( intent.async_register(hass, MyIntentHandler()) assert len(llm.async_get_apis(hass)) == 1 - api = await llm.async_get_api(hass, "assist", tool_input_context) + api = await llm.async_get_api(hass, "assist", llm_context) assert len(api.tools) == 1 tool = api.tools[0] assert tool.name == "test_intent" @@ -223,7 +221,7 @@ async def test_assist_api_prompt( assert await async_setup_component(hass, "homeassistant", {}) assert await async_setup_component(hass, "intent", {}) context = Context() - tool_context = llm.ToolContext( + llm_context = llm.LLMContext( platform="test_platform", context=context, user_prompt="test_text", @@ -231,7 +229,7 @@ async def test_assist_api_prompt( assistant="conversation", device_id=None, ) - api = await llm.async_get_api(hass, "assist", tool_context) + api = await llm.async_get_api(hass, "assist", llm_context) assert api.api_prompt == ( "Only if the user wants to control a device, tell them to expose entities to their " "voice assistant in Home Assistant." @@ -360,7 +358,7 @@ async def test_assist_api_prompt( ) ) - exposed_entities = llm._get_exposed_entities(hass, tool_context.assistant) + exposed_entities = llm._get_exposed_entities(hass, llm_context.assistant) assert exposed_entities == { "light.1": { "areas": "Test Area 2", @@ -435,7 +433,7 @@ async def test_assist_api_prompt( "When a user asks to turn on all devices of a specific type, " "ask user to specify an area, unless there is only one device of that type." ) - api = await llm.async_get_api(hass, "assist", tool_context) + api = await llm.async_get_api(hass, "assist", llm_context) assert api.api_prompt == ( f"""{first_part_prompt} {area_prompt} @@ -444,12 +442,12 @@ async def test_assist_api_prompt( ) # Fake that request is made from a specific device ID with an area - tool_context.device_id = device.id + llm_context.device_id = device.id area_prompt = ( "You are in area Test Area and all generic commands like 'turn on the lights' " "should target this area." ) - api = await llm.async_get_api(hass, "assist", tool_context) + api = await llm.async_get_api(hass, "assist", llm_context) assert api.api_prompt == ( f"""{first_part_prompt} {area_prompt} @@ -464,7 +462,7 @@ async def test_assist_api_prompt( "You are in area Test Area (floor 2) and all generic commands like 'turn on the lights' " "should target this area." ) - api = await llm.async_get_api(hass, "assist", tool_context) + api = await llm.async_get_api(hass, "assist", llm_context) assert api.api_prompt == ( f"""{first_part_prompt} {area_prompt} @@ -475,7 +473,7 @@ async def test_assist_api_prompt( # Register device for timers async_register_timer_handler(hass, device.id, lambda *args: None) - api = await llm.async_get_api(hass, "assist", tool_context) + api = await llm.async_get_api(hass, "assist", llm_context) # The no_timer_prompt is gone assert api.api_prompt == ( f"""{first_part_prompt} From ade0f94a207a63c54aff9cac812fc7d832162978 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 31 May 2024 10:11:46 -0500 Subject: [PATCH 0084/1445] Remove duplicate getattr call in entity wrap_attr (#118558) --- homeassistant/helpers/entity.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index d4e160c2672..ee544883a68 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -365,7 +365,7 @@ class CachedProperties(type): attr = getattr(cls, attr_name) if isinstance(attr, (FunctionType, property)): raise TypeError(f"Can't override {attr_name} in subclass") - setattr(cls, private_attr_name, getattr(cls, attr_name)) + setattr(cls, private_attr_name, attr) annotations = cls.__annotations__ if attr_name in annotations: annotations[private_attr_name] = annotations.pop(attr_name) From d956db691a5239903376894e59f78340658a8028 Mon Sep 17 00:00:00 2001 From: Josef Zweck <24647999+zweckj@users.noreply.github.com> Date: Fri, 31 May 2024 17:16:39 +0200 Subject: [PATCH 0085/1445] Migrate openai_conversation to `entry.runtime_data` (#118535) * switch to entry.runtime_data * check for missing config entry * Update homeassistant/components/openai_conversation/__init__.py --------- Co-authored-by: Paulus Schoutsen --- .../openai_conversation/__init__.py | 37 ++++++++++++++----- .../openai_conversation/conversation.py | 8 ++-- .../openai_conversation/strings.json | 5 +++ .../openai_conversation/test_init.py | 24 +++++++++++- 4 files changed, 60 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/openai_conversation/__init__.py b/homeassistant/components/openai_conversation/__init__.py index 2a91f1b1b38..0ba7b53795b 100644 --- a/homeassistant/components/openai_conversation/__init__.py +++ b/homeassistant/components/openai_conversation/__init__.py @@ -2,6 +2,8 @@ from __future__ import annotations +from typing import Literal, cast + import openai import voluptuous as vol @@ -13,7 +15,11 @@ from homeassistant.core import ( ServiceResponse, SupportsResponse, ) -from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError +from homeassistant.exceptions import ( + ConfigEntryNotReady, + HomeAssistantError, + ServiceValidationError, +) from homeassistant.helpers import ( config_validation as cv, issue_registry as ir, @@ -27,13 +33,25 @@ SERVICE_GENERATE_IMAGE = "generate_image" PLATFORMS = (Platform.CONVERSATION,) CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN) +type OpenAIConfigEntry = ConfigEntry[openai.AsyncClient] + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up OpenAI Conversation.""" async def render_image(call: ServiceCall) -> ServiceResponse: """Render an image with dall-e.""" - client = hass.data[DOMAIN][call.data["config_entry"]] + entry_id = call.data["config_entry"] + entry = hass.config_entries.async_get_entry(entry_id) + + if entry is None or entry.domain != DOMAIN: + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="invalid_config_entry", + translation_placeholders={"config_entry": entry_id}, + ) + + client: openai.AsyncClient = entry.runtime_data if call.data["size"] in ("256", "512", "1024"): ir.async_create_issue( @@ -51,6 +69,11 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: else: size = call.data["size"] + size = cast( + Literal["256x256", "512x512", "1024x1024", "1792x1024", "1024x1792"], + size, + ) # size is selector, so no need to check further + try: response = await client.images.generate( model="dall-e-3", @@ -90,7 +113,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: return True -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: OpenAIConfigEntry) -> bool: """Set up OpenAI Conversation from a config entry.""" client = openai.AsyncOpenAI(api_key=entry.data[CONF_API_KEY]) try: @@ -101,7 +124,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: except openai.OpenAIError as err: raise ConfigEntryNotReady(err) from err - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = client + entry.runtime_data = client await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) @@ -110,8 +133,4 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload OpenAI.""" - if not await hass.config_entries.async_unload_platforms(entry, PLATFORMS): - return False - - hass.data[DOMAIN].pop(entry.entry_id) - return True + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/openai_conversation/conversation.py b/homeassistant/components/openai_conversation/conversation.py index 26acfda979d..1c9ccf9a735 100644 --- a/homeassistant/components/openai_conversation/conversation.py +++ b/homeassistant/components/openai_conversation/conversation.py @@ -22,7 +22,6 @@ from voluptuous_openapi import convert from homeassistant.components import assist_pipeline, conversation from homeassistant.components.conversation import trace -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_LLM_HASS_API, MATCH_ALL from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError, TemplateError @@ -30,6 +29,7 @@ from homeassistant.helpers import device_registry as dr, intent, llm, template from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import ulid +from . import OpenAIConfigEntry from .const import ( CONF_CHAT_MODEL, CONF_MAX_TOKENS, @@ -50,7 +50,7 @@ MAX_TOOL_ITERATIONS = 10 async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: OpenAIConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up conversation entities.""" @@ -74,7 +74,7 @@ class OpenAIConversationEntity( _attr_has_entity_name = True _attr_name = None - def __init__(self, entry: ConfigEntry) -> None: + def __init__(self, entry: OpenAIConfigEntry) -> None: """Initialize the agent.""" self.entry = entry self.history: dict[str, list[ChatCompletionMessageParam]] = {} @@ -187,7 +187,7 @@ class OpenAIConversationEntity( trace.ConversationTraceEventType.AGENT_DETAIL, {"messages": messages} ) - client: openai.AsyncClient = self.hass.data[DOMAIN][self.entry.entry_id] + client = self.entry.runtime_data # To prevent infinite loops, we limit the number of iterations for _iteration in range(MAX_TOOL_ITERATIONS): diff --git a/homeassistant/components/openai_conversation/strings.json b/homeassistant/components/openai_conversation/strings.json index 1e93c60b6a9..c5d42eb9521 100644 --- a/homeassistant/components/openai_conversation/strings.json +++ b/homeassistant/components/openai_conversation/strings.json @@ -60,6 +60,11 @@ } } }, + "exceptions": { + "invalid_config_entry": { + "message": "Invalid config entry provided. Got {config_entry}" + } + }, "issues": { "image_size_deprecated_format": { "title": "Deprecated size format for image generation service", diff --git a/tests/components/openai_conversation/test_init.py b/tests/components/openai_conversation/test_init.py index f03013556c7..c9431aa1083 100644 --- a/tests/components/openai_conversation/test_init.py +++ b/tests/components/openai_conversation/test_init.py @@ -14,7 +14,7 @@ from openai.types.images_response import ImagesResponse import pytest from homeassistant.core import HomeAssistant -from homeassistant.exceptions import HomeAssistantError +from homeassistant.exceptions import HomeAssistantError, ServiceValidationError from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry @@ -160,6 +160,28 @@ async def test_generate_image_service_error( ) +async def test_invalid_config_entry( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_init_component, +) -> None: + """Assert exception when invalid config entry is provided.""" + service_data = { + "prompt": "Picture of a dog", + "config_entry": "invalid_entry", + } + with pytest.raises( + ServiceValidationError, match="Invalid config entry provided. Got invalid_entry" + ): + await hass.services.async_call( + "openai_conversation", + "generate_image", + service_data, + blocking=True, + return_response=True, + ) + + @pytest.mark.parametrize( ("side_effect", "error"), [ From 51d8f83a54acb335d13293f223c5001e93d6b00b Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Fri, 31 May 2024 17:55:59 +0200 Subject: [PATCH 0086/1445] Add state translation to Reolink AI detections (#118560) --- homeassistant/components/reolink/strings.json | 120 +++++++++++++++--- 1 file changed, 100 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/reolink/strings.json b/homeassistant/components/reolink/strings.json index 26d2bb82f0c..8191f51d7ef 100644 --- a/homeassistant/components/reolink/strings.json +++ b/homeassistant/components/reolink/strings.json @@ -86,73 +86,153 @@ "entity": { "binary_sensor": { "face": { - "name": "Face" + "name": "Face", + "state": { + "off": "[%key:component::binary_sensor::entity_component::gas::state::off%]", + "on": "[%key:component::binary_sensor::entity_component::gas::state::on%]" + } }, "person": { - "name": "Person" + "name": "Person", + "state": { + "off": "[%key:component::binary_sensor::entity_component::gas::state::off%]", + "on": "[%key:component::binary_sensor::entity_component::gas::state::on%]" + } }, "vehicle": { - "name": "Vehicle" + "name": "Vehicle", + "state": { + "off": "[%key:component::binary_sensor::entity_component::gas::state::off%]", + "on": "[%key:component::binary_sensor::entity_component::gas::state::on%]" + } }, "pet": { - "name": "Pet" + "name": "Pet", + "state": { + "off": "[%key:component::binary_sensor::entity_component::gas::state::off%]", + "on": "[%key:component::binary_sensor::entity_component::gas::state::on%]" + } }, "animal": { - "name": "Animal" + "name": "Animal", + "state": { + "off": "[%key:component::binary_sensor::entity_component::gas::state::off%]", + "on": "[%key:component::binary_sensor::entity_component::gas::state::on%]" + } }, "visitor": { "name": "Visitor" }, "package": { - "name": "Package" + "name": "Package", + "state": { + "off": "[%key:component::binary_sensor::entity_component::gas::state::off%]", + "on": "[%key:component::binary_sensor::entity_component::gas::state::on%]" + } }, "motion_lens_0": { - "name": "Motion lens 0" + "name": "Motion lens 0", + "state": { + "off": "[%key:component::binary_sensor::entity_component::gas::state::off%]", + "on": "[%key:component::binary_sensor::entity_component::gas::state::on%]" + } }, "face_lens_0": { - "name": "Face lens 0" + "name": "Face lens 0", + "state": { + "off": "[%key:component::binary_sensor::entity_component::gas::state::off%]", + "on": "[%key:component::binary_sensor::entity_component::gas::state::on%]" + } }, "person_lens_0": { - "name": "Person lens 0" + "name": "Person lens 0", + "state": { + "off": "[%key:component::binary_sensor::entity_component::gas::state::off%]", + "on": "[%key:component::binary_sensor::entity_component::gas::state::on%]" + } }, "vehicle_lens_0": { - "name": "Vehicle lens 0" + "name": "Vehicle lens 0", + "state": { + "off": "[%key:component::binary_sensor::entity_component::gas::state::off%]", + "on": "[%key:component::binary_sensor::entity_component::gas::state::on%]" + } }, "pet_lens_0": { - "name": "Pet lens 0" + "name": "Pet lens 0", + "state": { + "off": "[%key:component::binary_sensor::entity_component::gas::state::off%]", + "on": "[%key:component::binary_sensor::entity_component::gas::state::on%]" + } }, "animal_lens_0": { - "name": "Animal lens 0" + "name": "Animal lens 0", + "state": { + "off": "[%key:component::binary_sensor::entity_component::gas::state::off%]", + "on": "[%key:component::binary_sensor::entity_component::gas::state::on%]" + } }, "visitor_lens_0": { "name": "Visitor lens 0" }, "package_lens_0": { - "name": "Package lens 0" + "name": "Package lens 0", + "state": { + "off": "[%key:component::binary_sensor::entity_component::gas::state::off%]", + "on": "[%key:component::binary_sensor::entity_component::gas::state::on%]" + } }, "motion_lens_1": { - "name": "Motion lens 1" + "name": "Motion lens 1", + "state": { + "off": "[%key:component::binary_sensor::entity_component::gas::state::off%]", + "on": "[%key:component::binary_sensor::entity_component::gas::state::on%]" + } }, "face_lens_1": { - "name": "Face lens 1" + "name": "Face lens 1", + "state": { + "off": "[%key:component::binary_sensor::entity_component::gas::state::off%]", + "on": "[%key:component::binary_sensor::entity_component::gas::state::on%]" + } }, "person_lens_1": { - "name": "Person lens 1" + "name": "Person lens 1", + "state": { + "off": "[%key:component::binary_sensor::entity_component::gas::state::off%]", + "on": "[%key:component::binary_sensor::entity_component::gas::state::on%]" + } }, "vehicle_lens_1": { - "name": "Vehicle lens 1" + "name": "Vehicle lens 1", + "state": { + "off": "[%key:component::binary_sensor::entity_component::gas::state::off%]", + "on": "[%key:component::binary_sensor::entity_component::gas::state::on%]" + } }, "pet_lens_1": { - "name": "Pet lens 1" + "name": "Pet lens 1", + "state": { + "off": "[%key:component::binary_sensor::entity_component::gas::state::off%]", + "on": "[%key:component::binary_sensor::entity_component::gas::state::on%]" + } }, "animal_lens_1": { - "name": "Animal lens 1" + "name": "Animal lens 1", + "state": { + "off": "[%key:component::binary_sensor::entity_component::gas::state::off%]", + "on": "[%key:component::binary_sensor::entity_component::gas::state::on%]" + } }, "visitor_lens_1": { "name": "Visitor lens 1" }, "package_lens_1": { - "name": "Package lens 1" + "name": "Package lens 1", + "state": { + "off": "[%key:component::binary_sensor::entity_component::gas::state::off%]", + "on": "[%key:component::binary_sensor::entity_component::gas::state::on%]" + } } }, "button": { From 80e9ff672a7bbf706d8503b3a41ee1fefd958039 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 31 May 2024 13:28:52 -0400 Subject: [PATCH 0087/1445] Fix openAI tool calls (#118577) --- .../components/openai_conversation/conversation.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/openai_conversation/conversation.py b/homeassistant/components/openai_conversation/conversation.py index 1c9ccf9a735..6da56d3f9a0 100644 --- a/homeassistant/components/openai_conversation/conversation.py +++ b/homeassistant/components/openai_conversation/conversation.py @@ -231,11 +231,13 @@ class OpenAIConversationEntity( ) for tool_call in message.tool_calls ] - return ChatCompletionAssistantMessageParam( + param = ChatCompletionAssistantMessageParam( role=message.role, - tool_calls=tool_calls, content=message.content, ) + if tool_calls: + param["tool_calls"] = tool_calls + return param messages.append(message_convert(response)) tool_calls = response.tool_calls From 46da43d09daef72192b167214d50174276815f2c Mon Sep 17 00:00:00 2001 From: Denis Shulyaka Date: Sat, 1 Jun 2024 03:28:23 +0800 Subject: [PATCH 0088/1445] Add OpenAI Conversation system prompt `user_name` and `llm_context` variables (#118512) * OpenAI Conversation: Add variables to the system prompt * User name and llm_context * test for user name * test for user id --------- Co-authored-by: Paulus Schoutsen --- .../openai_conversation/conversation.py | 32 ++++++++--- .../openai_conversation/test_conversation.py | 53 ++++++++++++++++++- 2 files changed, 75 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/openai_conversation/conversation.py b/homeassistant/components/openai_conversation/conversation.py index 6da56d3f9a0..7cf4d18cce5 100644 --- a/homeassistant/components/openai_conversation/conversation.py +++ b/homeassistant/components/openai_conversation/conversation.py @@ -113,20 +113,22 @@ class OpenAIConversationEntity( intent_response = intent.IntentResponse(language=user_input.language) llm_api: llm.APIInstance | None = None tools: list[ChatCompletionToolParam] | None = None + user_name: str | None = None + llm_context = llm.LLMContext( + platform=DOMAIN, + context=user_input.context, + user_prompt=user_input.text, + language=user_input.language, + assistant=conversation.DOMAIN, + device_id=user_input.device_id, + ) if options.get(CONF_LLM_HASS_API): try: llm_api = await llm.async_get_api( self.hass, options[CONF_LLM_HASS_API], - llm.LLMContext( - platform=DOMAIN, - context=user_input.context, - user_prompt=user_input.text, - language=user_input.language, - assistant=conversation.DOMAIN, - device_id=user_input.device_id, - ), + llm_context, ) except HomeAssistantError as err: LOGGER.error("Error getting LLM API: %s", err) @@ -144,6 +146,18 @@ class OpenAIConversationEntity( messages = self.history[conversation_id] else: conversation_id = ulid.ulid_now() + + if ( + user_input.context + and user_input.context.user_id + and ( + user := await self.hass.auth.async_get_user( + user_input.context.user_id + ) + ) + ): + user_name = user.name + try: if llm_api: api_prompt = llm_api.api_prompt @@ -158,6 +172,8 @@ class OpenAIConversationEntity( ).async_render( { "ha_name": self.hass.config.location_name, + "user_name": user_name, + "llm_context": llm_context, }, parse_result=False, ), diff --git a/tests/components/openai_conversation/test_conversation.py b/tests/components/openai_conversation/test_conversation.py index 10829db7575..05d62ffd61b 100644 --- a/tests/components/openai_conversation/test_conversation.py +++ b/tests/components/openai_conversation/test_conversation.py @@ -1,6 +1,6 @@ """Tests for the OpenAI integration.""" -from unittest.mock import AsyncMock, patch +from unittest.mock import AsyncMock, Mock, patch from httpx import Response from openai import RateLimitError @@ -73,6 +73,53 @@ async def test_template_error( assert result.response.error_code == "unknown", result +async def test_template_variables( + hass: HomeAssistant, mock_config_entry: MockConfigEntry +) -> None: + """Test that template variables work.""" + context = Context(user_id="12345") + mock_user = Mock() + mock_user.id = "12345" + mock_user.name = "Test User" + + hass.config_entries.async_update_entry( + mock_config_entry, + options={ + "prompt": ( + "The user name is {{ user_name }}. " + "The user id is {{ llm_context.context.user_id }}." + ), + }, + ) + with ( + patch( + "openai.resources.models.AsyncModels.list", + ), + patch( + "openai.resources.chat.completions.AsyncCompletions.create", + new_callable=AsyncMock, + ) as mock_create, + patch("homeassistant.auth.AuthManager.async_get_user", return_value=mock_user), + ): + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + result = await conversation.async_converse( + hass, "hello", None, context, agent_id=mock_config_entry.entry_id + ) + + assert ( + result.response.response_type == intent.IntentResponseType.ACTION_DONE + ), result + assert ( + "The user name is Test User." + in mock_create.mock_calls[0][2]["messages"][0]["content"] + ) + assert ( + "The user id is 12345." + in mock_create.mock_calls[0][2]["messages"][0]["content"] + ) + + async def test_conversation_agent( hass: HomeAssistant, mock_config_entry: MockConfigEntry, @@ -382,7 +429,9 @@ async def test_assist_api_tools_conversion( ), ), ) as mock_create: - await conversation.async_converse(hass, "hello", None, None, agent_id=agent_id) + await conversation.async_converse( + hass, "hello", None, Context(), agent_id=agent_id + ) tools = mock_create.mock_calls[0][2]["tools"] assert tools From bae96e7d3688c817733629ac5c1f31e41aca99e6 Mon Sep 17 00:00:00 2001 From: Denis Shulyaka Date: Sat, 1 Jun 2024 03:28:44 +0800 Subject: [PATCH 0089/1445] Add Google Generative AI Conversation system prompt `user_name` and `llm_context` variables (#118510) * Google Generative AI Conversation: Add variables to the system prompt * User name and llm_context * test for template variables * test for template variables --------- Co-authored-by: Paulus Schoutsen --- .../conversation.py | 29 ++++++++---- .../test_conversation.py | 45 +++++++++++++++++++ 2 files changed, 66 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/google_generative_ai_conversation/conversation.py b/homeassistant/components/google_generative_ai_conversation/conversation.py index d722403a0be..12b1e44b3df 100644 --- a/homeassistant/components/google_generative_ai_conversation/conversation.py +++ b/homeassistant/components/google_generative_ai_conversation/conversation.py @@ -163,20 +163,22 @@ class GoogleGenerativeAIConversationEntity( intent_response = intent.IntentResponse(language=user_input.language) llm_api: llm.APIInstance | None = None tools: list[dict[str, Any]] | None = None + user_name: str | None = None + llm_context = llm.LLMContext( + platform=DOMAIN, + context=user_input.context, + user_prompt=user_input.text, + language=user_input.language, + assistant=conversation.DOMAIN, + device_id=user_input.device_id, + ) if self.entry.options.get(CONF_LLM_HASS_API): try: llm_api = await llm.async_get_api( self.hass, self.entry.options[CONF_LLM_HASS_API], - llm.LLMContext( - platform=DOMAIN, - context=user_input.context, - user_prompt=user_input.text, - language=user_input.language, - assistant=conversation.DOMAIN, - device_id=user_input.device_id, - ), + llm_context, ) except HomeAssistantError as err: LOGGER.error("Error getting LLM API: %s", err) @@ -225,6 +227,15 @@ class GoogleGenerativeAIConversationEntity( conversation_id = ulid.ulid_now() messages = [{}, {}] + if ( + user_input.context + and user_input.context.user_id + and ( + user := await self.hass.auth.async_get_user(user_input.context.user_id) + ) + ): + user_name = user.name + try: if llm_api: api_prompt = llm_api.api_prompt @@ -241,6 +252,8 @@ class GoogleGenerativeAIConversationEntity( ).async_render( { "ha_name": self.hass.config.location_name, + "user_name": user_name, + "llm_context": llm_context, }, parse_result=False, ), diff --git a/tests/components/google_generative_ai_conversation/test_conversation.py b/tests/components/google_generative_ai_conversation/test_conversation.py index 19a855aa17f..13e7bd0c8fb 100644 --- a/tests/components/google_generative_ai_conversation/test_conversation.py +++ b/tests/components/google_generative_ai_conversation/test_conversation.py @@ -449,6 +449,51 @@ async def test_template_error( assert result.response.error_code == "unknown", result +async def test_template_variables( + hass: HomeAssistant, mock_config_entry: MockConfigEntry +) -> None: + """Test that template variables work.""" + context = Context(user_id="12345") + mock_user = MagicMock() + mock_user.id = "12345" + mock_user.name = "Test User" + + hass.config_entries.async_update_entry( + mock_config_entry, + options={ + "prompt": ( + "The user name is {{ user_name }}. " + "The user id is {{ llm_context.context.user_id }}." + ), + }, + ) + with ( + patch("google.generativeai.GenerativeModel") as mock_model, + patch("homeassistant.auth.AuthManager.async_get_user", return_value=mock_user), + ): + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + mock_chat = AsyncMock() + mock_model.return_value.start_chat.return_value = mock_chat + chat_response = MagicMock() + mock_chat.send_message_async.return_value = chat_response + mock_part = MagicMock() + mock_part.text = "Model response" + chat_response.parts = [mock_part] + result = await conversation.async_converse( + hass, "hello", None, context, agent_id=mock_config_entry.entry_id + ) + + assert ( + result.response.response_type == intent.IntentResponseType.ACTION_DONE + ), result + assert ( + "The user name is Test User." + in mock_model.mock_calls[1][2]["history"][0]["parts"] + ) + assert "The user id is 12345." in mock_model.mock_calls[1][2]["history"][0]["parts"] + + async def test_conversation_agent( hass: HomeAssistant, mock_config_entry: MockConfigEntry, From 41e852a01ba449c6b8c253bd6499d81b003d2c92 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 31 May 2024 21:31:44 +0200 Subject: [PATCH 0090/1445] Add ability to replace connections in DeviceRegistry (#118555) * Add ability to replace connections in DeviceRegistry * Add more tests * Improve coverage * Apply suggestion Co-authored-by: Paulus Schoutsen --------- Co-authored-by: Paulus Schoutsen --- homeassistant/helpers/device_registry.py | 8 ++ tests/helpers/test_device_registry.py | 110 ++++++++++++++++++++++- 2 files changed, 117 insertions(+), 1 deletion(-) diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index 75fcda18eac..1f147a1884d 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -798,6 +798,7 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]): model: str | None | UndefinedType = UNDEFINED, name_by_user: str | None | UndefinedType = UNDEFINED, name: str | None | UndefinedType = UNDEFINED, + new_connections: set[tuple[str, str]] | UndefinedType = UNDEFINED, new_identifiers: set[tuple[str, str]] | UndefinedType = UNDEFINED, remove_config_entry_id: str | UndefinedType = UNDEFINED, serial_number: str | None | UndefinedType = UNDEFINED, @@ -813,6 +814,9 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]): config_entries = old.config_entries + if merge_connections is not UNDEFINED and new_connections is not UNDEFINED: + raise HomeAssistantError("Cannot define both merge_connections and new_connections") + if merge_identifiers is not UNDEFINED and new_identifiers is not UNDEFINED: raise HomeAssistantError @@ -873,6 +877,10 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]): new_values[attr_name] = old_value | setvalue old_values[attr_name] = old_value + if new_connections is not UNDEFINED: + new_values["connections"] = _normalize_connections(new_connections) + old_values["connections"] = old.connections + if new_identifiers is not UNDEFINED: new_values["identifiers"] = new_identifiers old_values["identifiers"] = old.identifiers diff --git a/tests/helpers/test_device_registry.py b/tests/helpers/test_device_registry.py index e40b3ca0356..da99f176a3c 100644 --- a/tests/helpers/test_device_registry.py +++ b/tests/helpers/test_device_registry.py @@ -1257,6 +1257,7 @@ async def test_update( connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, identifiers={("hue", "456"), ("bla", "123")}, ) + new_connections = {(dr.CONNECTION_NETWORK_MAC, "65:43:21:FE:DC:BA")} new_identifiers = {("hue", "654"), ("bla", "321")} assert not entry.area_id assert not entry.labels @@ -1275,6 +1276,7 @@ async def test_update( model="Test Model", name_by_user="Test Friendly Name", name="name", + new_connections=new_connections, new_identifiers=new_identifiers, serial_number="serial_no", suggested_area="suggested_area", @@ -1288,7 +1290,7 @@ async def test_update( area_id="12345A", config_entries={mock_config_entry.entry_id}, configuration_url="https://example.com/config", - connections={("mac", "12:34:56:ab:cd:ef")}, + connections={("mac", "65:43:21:fe:dc:ba")}, disabled_by=dr.DeviceEntryDisabler.USER, entry_type=dr.DeviceEntryType.SERVICE, hw_version="hw_version", @@ -1319,6 +1321,12 @@ async def test_update( device_registry.async_get_device( connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")} ) + is None + ) + assert ( + device_registry.async_get_device( + connections={(dr.CONNECTION_NETWORK_MAC, "65:43:21:FE:DC:BA")} + ) == updated_entry ) @@ -1336,6 +1344,7 @@ async def test_update( "device_id": entry.id, "changes": { "area_id": None, + "connections": {("mac", "12:34:56:ab:cd:ef")}, "configuration_url": None, "disabled_by": None, "entry_type": None, @@ -1352,6 +1361,105 @@ async def test_update( "via_device_id": None, }, } + with pytest.raises(HomeAssistantError): + device_registry.async_update_device( + entry.id, + merge_connections=new_connections, + new_connections=new_connections, + ) + + with pytest.raises(HomeAssistantError): + device_registry.async_update_device( + entry.id, + merge_identifiers=new_identifiers, + new_identifiers=new_identifiers, + ) + + +@pytest.mark.parametrize( + ("initial_connections", "new_connections", "updated_connections"), + [ + ( # No connection -> single connection + None, + {(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + {(dr.CONNECTION_NETWORK_MAC, "12:34:56:ab:cd:ef")}, + ), + ( # No connection -> double connection + None, + { + (dr.CONNECTION_NETWORK_MAC, "65:43:21:FE:DC:BA"), + (dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF"), + }, + { + (dr.CONNECTION_NETWORK_MAC, "65:43:21:fe:dc:ba"), + (dr.CONNECTION_NETWORK_MAC, "12:34:56:ab:cd:ef"), + }, + ), + ( # single connection -> no connection + {(dr.CONNECTION_NETWORK_MAC, "65:43:21:FE:DC:BA")}, + set(), + set(), + ), + ( # single connection -> single connection + {(dr.CONNECTION_NETWORK_MAC, "65:43:21:FE:DC:BA")}, + {(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + {(dr.CONNECTION_NETWORK_MAC, "12:34:56:ab:cd:ef")}, + ), + ( # single connection -> double connection + {(dr.CONNECTION_NETWORK_MAC, "65:43:21:FE:DC:BA")}, + { + (dr.CONNECTION_NETWORK_MAC, "65:43:21:FE:DC:BA"), + (dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF"), + }, + { + (dr.CONNECTION_NETWORK_MAC, "65:43:21:fe:dc:ba"), + (dr.CONNECTION_NETWORK_MAC, "12:34:56:ab:cd:ef"), + }, + ), + ( # Double connection -> None + { + (dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF"), + (dr.CONNECTION_NETWORK_MAC, "65:43:21:FE:DC:BA"), + }, + set(), + set(), + ), + ( # Double connection -> single connection + { + (dr.CONNECTION_NETWORK_MAC, "65:43:21:FE:DC:BA"), + (dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF"), + }, + {(dr.CONNECTION_NETWORK_MAC, "65:43:21:FE:DC:BA")}, + {(dr.CONNECTION_NETWORK_MAC, "65:43:21:fe:dc:ba")}, + ), + ], +) +async def test_update_connection( + device_registry: dr.DeviceRegistry, + mock_config_entry: MockConfigEntry, + initial_connections: set[tuple[str, str]] | None, + new_connections: set[tuple[str, str]] | None, + updated_connections: set[tuple[str, str]] | None, +) -> None: + """Verify that we can update some attributes of a device.""" + entry = device_registry.async_get_or_create( + config_entry_id=mock_config_entry.entry_id, + connections=initial_connections, + identifiers={("hue", "456"), ("bla", "123")}, + ) + + with patch.object(device_registry, "async_schedule_save") as mock_save: + updated_entry = device_registry.async_update_device( + entry.id, + new_connections=new_connections, + ) + + assert mock_save.call_count == 1 + assert updated_entry != entry + assert updated_entry.connections == updated_connections + assert ( + device_registry.async_get_device(identifiers={("bla", "123")}) == updated_entry + ) async def test_update_remove_config_entries( From f6800e6968c22837fbebbde28c804e892974789d Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 31 May 2024 21:35:42 +0200 Subject: [PATCH 0091/1445] Improve typing in Zengge (#118547) --- homeassistant/components/zengge/light.py | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/zengge/light.py b/homeassistant/components/zengge/light.py index 5de4f3fdce3..6657bfb9edd 100644 --- a/homeassistant/components/zengge/light.py +++ b/homeassistant/components/zengge/light.py @@ -41,10 +41,7 @@ def setup_platform( """Set up the Zengge platform.""" lights = [] for address, device_config in config[CONF_DEVICES].items(): - device = {} - device["name"] = device_config[CONF_NAME] - device["address"] = address - light = ZenggeLight(device) + light = ZenggeLight(device_config[CONF_NAME], address) if light.is_valid: lights.append(light) @@ -56,22 +53,20 @@ class ZenggeLight(LightEntity): _attr_supported_color_modes = {ColorMode.HS, ColorMode.WHITE} - def __init__(self, device): + def __init__(self, name: str, address: str) -> None: """Initialize the light.""" - self._attr_name = device["name"] - self._attr_unique_id = device["address"] + self._attr_name = name + self._attr_unique_id = address self.is_valid = True - self._bulb = zengge(device["address"]) + self._bulb = zengge(address) self._white = 0 self._attr_brightness = 0 self._attr_hs_color = (0, 0) self._attr_is_on = False if self._bulb.connect() is False: self.is_valid = False - _LOGGER.error( - "Failed to connect to bulb %s, %s", device["address"], device["name"] - ) + _LOGGER.error("Failed to connect to bulb %s, %s", address, name) return @property From 32b51b87924dfa3a96f4d14beecbf12b73036d27 Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Fri, 31 May 2024 22:22:48 +0200 Subject: [PATCH 0092/1445] Run ruff format for device registry (#118582) --- homeassistant/helpers/device_registry.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index 1f147a1884d..cb336d1455b 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -815,7 +815,9 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]): config_entries = old.config_entries if merge_connections is not UNDEFINED and new_connections is not UNDEFINED: - raise HomeAssistantError("Cannot define both merge_connections and new_connections") + raise HomeAssistantError( + "Cannot define both merge_connections and new_connections" + ) if merge_identifiers is not UNDEFINED and new_identifiers is not UNDEFINED: raise HomeAssistantError From 738935a73a2f1bde9d48fa768b7b164b90ffb26d Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 31 May 2024 23:07:51 +0200 Subject: [PATCH 0093/1445] Update device connections in samsungtv (#118556) --- homeassistant/components/samsungtv/__init__.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/samsungtv/__init__.py b/homeassistant/components/samsungtv/__init__.py index fbae0d5552a..f49ae276665 100644 --- a/homeassistant/components/samsungtv/__init__.py +++ b/homeassistant/components/samsungtv/__init__.py @@ -301,9 +301,12 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> for device in dr.async_entries_for_config_entry( dev_reg, config_entry.entry_id ): - for connection in device.connections: - if connection == (dr.CONNECTION_NETWORK_MAC, "none"): - dev_reg.async_remove_device(device.id) + new_connections = device.connections.copy() + new_connections.discard((dr.CONNECTION_NETWORK_MAC, "none")) + if new_connections != device.connections: + dev_reg.async_update_device( + device.id, new_connections=new_connections + ) minor_version = 2 hass.config_entries.async_update_entry(config_entry, minor_version=2) From 3232fd0eaf15b147879fb266caac2a27a148cdca Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sat, 1 Jun 2024 00:27:53 +0200 Subject: [PATCH 0094/1445] Improve UniFi config flow tests (#118587) * Use proper fixtures in config flow tests * Improve rest of config flow tests * Small improvement * Rename fixtures --- tests/components/unifi/conftest.py | 41 ++-- tests/components/unifi/test_button.py | 12 +- tests/components/unifi/test_config_flow.py | 234 ++++++--------------- tests/components/unifi/test_hub.py | 18 +- tests/components/unifi/test_init.py | 16 +- 5 files changed, 105 insertions(+), 216 deletions(-) diff --git a/tests/components/unifi/conftest.py b/tests/components/unifi/conftest.py index e605599700d..2ea772b5173 100644 --- a/tests/components/unifi/conftest.py +++ b/tests/components/unifi/conftest.py @@ -87,7 +87,6 @@ def config_entry_fixture( unique_id="1", data=config_entry_data, options=config_entry_options, - version=1, ) config_entry.add_to_hass(hass) return config_entry @@ -112,8 +111,8 @@ def config_entry_options_fixture() -> MappingProxyType[str, Any]: return {} -@pytest.fixture(name="mock_unifi_requests") -def default_request_fixture( +@pytest.fixture(name="mock_requests") +def request_fixture( aioclient_mock: AiohttpClientMocker, client_payload: list[dict[str, Any]], clients_all_payload: list[dict[str, Any]], @@ -127,7 +126,7 @@ def default_request_fixture( ) -> Callable[[str], None]: """Mock default UniFi requests responses.""" - def __mock_default_requests(host: str, site_id: str) -> None: + def __mock_requests(host: str = DEFAULT_HOST, site_id: str = DEFAULT_SITE) -> None: url = f"https://{host}:{DEFAULT_PORT}" def mock_get_request(path: str, payload: list[dict[str, Any]]) -> None: @@ -153,7 +152,7 @@ def default_request_fixture( mock_get_request(f"/api/s/{site_id}/stat/sysinfo", system_information_payload) mock_get_request(f"/api/s/{site_id}/rest/wlanconf", wlan_payload) - return __mock_default_requests + return __mock_requests # Request payload fixtures @@ -229,22 +228,24 @@ def wlan_data_fixture() -> list[dict[str, Any]]: return [] -@pytest.fixture(name="setup_default_unifi_requests") -def default_vapix_requests_fixture( - config_entry: ConfigEntry, - mock_unifi_requests: Callable[[str, str], None], +@pytest.fixture(name="mock_default_requests") +def default_requests_fixture( + mock_requests: Callable[[str, str], None], ) -> None: - """Mock default UniFi requests responses.""" - mock_unifi_requests(config_entry.data[CONF_HOST], config_entry.data[CONF_SITE_ID]) + """Mock UniFi requests responses with default host and site.""" + mock_requests(DEFAULT_HOST, DEFAULT_SITE) -@pytest.fixture(name="prepare_config_entry") -async def prep_config_entry_fixture( - hass: HomeAssistant, config_entry: ConfigEntry, setup_default_unifi_requests: None +@pytest.fixture(name="config_entry_factory") +async def config_entry_factory_fixture( + hass: HomeAssistant, + config_entry: ConfigEntry, + mock_requests: Callable[[str, str], None], ) -> Callable[[], ConfigEntry]: - """Fixture factory to set up UniFi network integration.""" + """Fixture factory that can set up UniFi network integration.""" async def __mock_setup_config_entry() -> ConfigEntry: + mock_requests(config_entry.data[CONF_HOST], config_entry.data[CONF_SITE_ID]) await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() return config_entry @@ -252,12 +253,12 @@ async def prep_config_entry_fixture( return __mock_setup_config_entry -@pytest.fixture(name="setup_config_entry") -async def setup_config_entry_fixture( - hass: HomeAssistant, prepare_config_entry: Callable[[], ConfigEntry] +@pytest.fixture(name="config_entry_setup") +async def config_entry_setup_fixture( + hass: HomeAssistant, config_entry_factory: Callable[[], ConfigEntry] ) -> ConfigEntry: - """Fixture to set up UniFi network integration.""" - return await prepare_config_entry() + """Fixture providing a set up instance of UniFi network integration.""" + return await config_entry_factory() # Websocket fixtures diff --git a/tests/components/unifi/test_button.py b/tests/components/unifi/test_button.py index 25fef0fc10b..7199a5f3ed6 100644 --- a/tests/components/unifi/test_button.py +++ b/tests/components/unifi/test_button.py @@ -83,11 +83,11 @@ async def test_restart_device_button( hass: HomeAssistant, entity_registry: er.EntityRegistry, aioclient_mock: AiohttpClientMocker, - setup_config_entry, + config_entry_setup, websocket_mock, ) -> None: """Test restarting device button.""" - config_entry = setup_config_entry + config_entry = config_entry_setup assert len(hass.states.async_entity_ids(BUTTON_DOMAIN)) == 1 ent_reg_entry = entity_registry.async_get("button.switch_restart") @@ -169,11 +169,11 @@ async def test_power_cycle_poe( hass: HomeAssistant, entity_registry: er.EntityRegistry, aioclient_mock: AiohttpClientMocker, - setup_config_entry, + config_entry_setup, websocket_mock, ) -> None: """Test restarting device button.""" - config_entry = setup_config_entry + config_entry = config_entry_setup assert len(hass.states.async_entity_ids(BUTTON_DOMAIN)) == 2 ent_reg_entry = entity_registry.async_get("button.switch_port_1_power_cycle") @@ -225,11 +225,11 @@ async def test_wlan_regenerate_password( hass: HomeAssistant, entity_registry: er.EntityRegistry, aioclient_mock: AiohttpClientMocker, - setup_config_entry, + config_entry_setup, websocket_mock, ) -> None: """Test WLAN regenerate password button.""" - config_entry = setup_config_entry + config_entry = config_entry_setup assert len(hass.states.async_entity_ids(BUTTON_DOMAIN)) == 0 button_regenerate_password = "button.ssid_1_regenerate_password" diff --git a/tests/components/unifi/test_config_flow.py b/tests/components/unifi/test_config_flow.py index 06ada29f911..7abf45dd16f 100644 --- a/tests/components/unifi/test_config_flow.py +++ b/tests/components/unifi/test_config_flow.py @@ -1,9 +1,10 @@ """Test UniFi Network config flow.""" import socket -from unittest.mock import patch +from unittest.mock import PropertyMock, patch import aiounifi +import pytest from homeassistant import config_entries from homeassistant.components import ssdp @@ -23,20 +24,17 @@ from homeassistant.components.unifi.const import ( CONF_TRACK_WIRED_CLIENTS, DOMAIN as UNIFI_DOMAIN, ) -from homeassistant.config_entries import SOURCE_REAUTH +from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME, CONF_VERIFY_SSL, - CONTENT_TYPE_JSON, ) from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType -from .test_hub import setup_unifi_integration - from tests.common import MockConfigEntry from tests.test_util.aiohttp import AiohttpClientMocker @@ -98,7 +96,7 @@ DPI_GROUPS = [ async def test_flow_works( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, mock_discovery + hass: HomeAssistant, mock_discovery, mock_default_requests: None ) -> None: """Test config flow.""" mock_discovery.return_value = "1" @@ -116,25 +114,6 @@ async def test_flow_works( CONF_VERIFY_SSL: False, } - aioclient_mock.get("https://1.2.3.4:1234", status=302) - - aioclient_mock.post( - "https://1.2.3.4:1234/api/login", - json={"data": "login successful", "meta": {"rc": "ok"}}, - headers={"content-type": CONTENT_TYPE_JSON}, - ) - - aioclient_mock.get( - "https://1.2.3.4:1234/api/self/sites", - json={ - "data": [ - {"desc": "Site name", "name": "site_id", "role": "admin", "_id": "1"} - ], - "meta": {"rc": "ok"}, - }, - headers={"content-type": CONTENT_TYPE_JSON}, - ) - result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={ @@ -159,7 +138,7 @@ async def test_flow_works( async def test_flow_works_negative_discovery( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, mock_discovery + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test config flow with a negative outcome of async_discovery_unifi.""" result = await hass.config_entries.flow.async_init( @@ -177,8 +156,17 @@ async def test_flow_works_negative_discovery( } +@pytest.mark.parametrize( + "site_payload", + [ + [ + {"name": "default", "role": "admin", "desc": "site name", "_id": "1"}, + {"name": "site2", "role": "admin", "desc": "site2 name", "_id": "2"}, + ] + ], +) async def test_flow_multiple_sites( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, mock_default_requests: None ) -> None: """Test config flow works when finding multiple sites.""" result = await hass.config_entries.flow.async_init( @@ -188,26 +176,6 @@ async def test_flow_multiple_sites( assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" - aioclient_mock.get("https://1.2.3.4:1234", status=302) - - aioclient_mock.post( - "https://1.2.3.4:1234/api/login", - json={"data": "login successful", "meta": {"rc": "ok"}}, - headers={"content-type": CONTENT_TYPE_JSON}, - ) - - aioclient_mock.get( - "https://1.2.3.4:1234/api/self/sites", - json={ - "data": [ - {"name": "default", "role": "admin", "desc": "site name", "_id": "1"}, - {"name": "site2", "role": "admin", "desc": "site2 name", "_id": "2"}, - ], - "meta": {"rc": "ok"}, - }, - headers={"content-type": CONTENT_TYPE_JSON}, - ) - result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={ @@ -226,11 +194,9 @@ async def test_flow_multiple_sites( async def test_flow_raise_already_configured( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, config_entry_setup: ConfigEntry ) -> None: """Test config flow aborts since a connected config entry already exists.""" - await setup_unifi_integration(hass, aioclient_mock) - result = await hass.config_entries.flow.async_init( UNIFI_DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -238,27 +204,6 @@ async def test_flow_raise_already_configured( assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" - aioclient_mock.clear_requests() - - aioclient_mock.get("https://1.2.3.4:1234", status=302) - - aioclient_mock.post( - "https://1.2.3.4:1234/api/login", - json={"data": "login successful", "meta": {"rc": "ok"}}, - headers={"content-type": CONTENT_TYPE_JSON}, - ) - - aioclient_mock.get( - "https://1.2.3.4:1234/api/self/sites", - json={ - "data": [ - {"desc": "Site name", "name": "site_id", "role": "admin", "_id": "1"} - ], - "meta": {"rc": "ok"}, - }, - headers={"content-type": CONTENT_TYPE_JSON}, - ) - result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={ @@ -275,15 +220,9 @@ async def test_flow_raise_already_configured( async def test_flow_aborts_configuration_updated( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, config_entry_setup: ConfigEntry ) -> None: """Test config flow aborts since a connected config entry already exists.""" - entry = MockConfigEntry( - domain=UNIFI_DOMAIN, data={"host": "1.2.3.4", "site": "site_id"}, unique_id="1" - ) - entry.add_to_hass(hass) - entry.runtime_data = None - result = await hass.config_entries.flow.async_init( UNIFI_DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -291,33 +230,17 @@ async def test_flow_aborts_configuration_updated( assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" - aioclient_mock.get("https://1.2.3.4:1234", status=302) - - aioclient_mock.post( - "https://1.2.3.4:1234/api/login", - json={"data": "login successful", "meta": {"rc": "ok"}}, - headers={"content-type": CONTENT_TYPE_JSON}, - ) - - aioclient_mock.get( - "https://1.2.3.4:1234/api/self/sites", - json={ - "data": [ - {"desc": "Site name", "name": "site_id", "role": "admin", "_id": "1"} - ], - "meta": {"rc": "ok"}, - }, - headers={"content-type": CONTENT_TYPE_JSON}, - ) - - with patch("homeassistant.components.unifi.async_setup_entry"): + with patch("homeassistant.components.unifi.async_setup_entry") and patch( + "homeassistant.components.unifi.UnifiHub.available", new_callable=PropertyMock + ) as ws_mock: + ws_mock.return_value = False result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={ CONF_HOST: "1.2.3.4", CONF_USERNAME: "username", CONF_PASSWORD: "password", - CONF_PORT: 1234, + CONF_PORT: 12345, CONF_VERIFY_SSL: True, }, ) @@ -327,7 +250,7 @@ async def test_flow_aborts_configuration_updated( async def test_flow_fails_user_credentials_faulty( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, mock_default_requests: None ) -> None: """Test config flow.""" result = await hass.config_entries.flow.async_init( @@ -337,8 +260,6 @@ async def test_flow_fails_user_credentials_faulty( assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" - aioclient_mock.get("https://1.2.3.4:1234", status=302) - with patch("aiounifi.Controller.login", side_effect=aiounifi.errors.Unauthorized): result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -356,7 +277,7 @@ async def test_flow_fails_user_credentials_faulty( async def test_flow_fails_hub_unavailable( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, mock_default_requests: None ) -> None: """Test config flow.""" result = await hass.config_entries.flow.async_init( @@ -366,8 +287,6 @@ async def test_flow_fails_hub_unavailable( assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" - aioclient_mock.get("https://1.2.3.4:1234", status=302) - with patch("aiounifi.Controller.login", side_effect=aiounifi.errors.RequestError): result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -385,12 +304,10 @@ async def test_flow_fails_hub_unavailable( async def test_reauth_flow_update_configuration( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, config_entry_setup: ConfigEntry ) -> None: """Verify reauth flow can update hub configuration.""" - config_entry = await setup_unifi_integration(hass, aioclient_mock) - hub = config_entry.runtime_data - hub.websocket.available = False + config_entry = config_entry_setup result = await hass.config_entries.flow.async_init( UNIFI_DOMAIN, @@ -405,37 +322,20 @@ async def test_reauth_flow_update_configuration( assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" - aioclient_mock.clear_requests() - - aioclient_mock.get("https://1.2.3.4:1234", status=302) - - aioclient_mock.post( - "https://1.2.3.4:1234/api/login", - json={"data": "login successful", "meta": {"rc": "ok"}}, - headers={"content-type": CONTENT_TYPE_JSON}, - ) - - aioclient_mock.get( - "https://1.2.3.4:1234/api/self/sites", - json={ - "data": [ - {"desc": "Site name", "name": "site_id", "role": "admin", "_id": "1"} - ], - "meta": {"rc": "ok"}, - }, - headers={"content-type": CONTENT_TYPE_JSON}, - ) - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={ - CONF_HOST: "1.2.3.4", - CONF_USERNAME: "new_name", - CONF_PASSWORD: "new_pass", - CONF_PORT: 1234, - CONF_VERIFY_SSL: True, - }, - ) + with patch( + "homeassistant.components.unifi.UnifiHub.available", new_callable=PropertyMock + ) as ws_mock: + ws_mock.return_value = False + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_HOST: "1.2.3.4", + CONF_USERNAME: "new_name", + CONF_PASSWORD: "new_pass", + CONF_PORT: 1234, + CONF_VERIFY_SSL: True, + }, + ) assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" @@ -444,19 +344,15 @@ async def test_reauth_flow_update_configuration( assert config_entry.data[CONF_PASSWORD] == "new_pass" +@pytest.mark.parametrize("client_payload", [CLIENTS]) +@pytest.mark.parametrize("device_payload", [DEVICES]) +@pytest.mark.parametrize("wlan_payload", [WLANS]) +@pytest.mark.parametrize("dpi_group_payload", [DPI_GROUPS]) async def test_advanced_option_flow( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, config_entry_setup: ConfigEntry ) -> None: """Test advanced config flow options.""" - config_entry = await setup_unifi_integration( - hass, - aioclient_mock, - clients_response=CLIENTS, - devices_response=DEVICES, - wlans_response=WLANS, - dpigroup_response=DPI_GROUPS, - dpiapp_response=[], - ) + config_entry = config_entry_setup result = await hass.config_entries.options.async_init( config_entry.entry_id, context={"show_advanced_options": True} @@ -535,13 +431,12 @@ async def test_advanced_option_flow( } +@pytest.mark.parametrize("client_payload", [CLIENTS]) async def test_simple_option_flow( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, config_entry_setup: ConfigEntry ) -> None: """Test simple config flow options.""" - config_entry = await setup_unifi_integration( - hass, aioclient_mock, clients_response=CLIENTS - ) + config_entry = config_entry_setup result = await hass.config_entries.options.async_init( config_entry.entry_id, context={"show_advanced_options": False} @@ -608,21 +503,18 @@ async def test_form_ssdp(hass: HomeAssistant) -> None: } -async def test_form_ssdp_aborts_if_host_already_exists(hass: HomeAssistant) -> None: +async def test_form_ssdp_aborts_if_host_already_exists( + hass: HomeAssistant, config_entry: ConfigEntry +) -> None: """Test we abort if the host is already configured.""" - entry = MockConfigEntry( - domain=UNIFI_DOMAIN, - data={"host": "192.168.208.1", "site": "site_id"}, - ) - entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( UNIFI_DOMAIN, context={"source": config_entries.SOURCE_SSDP}, data=ssdp.SsdpServiceInfo( ssdp_usn="mock_usn", ssdp_st="mock_st", - ssdp_location="http://192.168.208.1:41417/rootDesc.xml", + ssdp_location="http://1.2.3.4:1234/rootDesc.xml", upnp={ "friendlyName": "UniFi Dream Machine", "modelDescription": "UniFi Dream Machine Pro", @@ -634,26 +526,22 @@ async def test_form_ssdp_aborts_if_host_already_exists(hass: HomeAssistant) -> N assert result["reason"] == "already_configured" -async def test_form_ssdp_aborts_if_serial_already_exists(hass: HomeAssistant) -> None: +async def test_form_ssdp_aborts_if_serial_already_exists( + hass: HomeAssistant, config_entry: ConfigEntry +) -> None: """Test we abort if the serial is already configured.""" - entry = MockConfigEntry( - domain=UNIFI_DOMAIN, - data={"controller": {"host": "1.2.3.4", "site": "site_id"}}, - unique_id="e0:63:da:20:14:a9", - ) - entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( UNIFI_DOMAIN, context={"source": config_entries.SOURCE_SSDP}, data=ssdp.SsdpServiceInfo( ssdp_usn="mock_usn", ssdp_st="mock_st", - ssdp_location="http://192.168.208.1:41417/rootDesc.xml", + ssdp_location="http://1.2.3.4:1234/rootDesc.xml", upnp={ "friendlyName": "UniFi Dream Machine", "modelDescription": "UniFi Dream Machine Pro", - "serialNumber": "e0:63:da:20:14:a9", + "serialNumber": "1", }, ), ) @@ -662,7 +550,7 @@ async def test_form_ssdp_aborts_if_serial_already_exists(hass: HomeAssistant) -> async def test_form_ssdp_gets_form_with_ignored_entry(hass: HomeAssistant) -> None: - """Test we can still setup if there is an ignored entry.""" + """Test we can still setup if there is an ignored never configured entry.""" entry = MockConfigEntry( domain=UNIFI_DOMAIN, @@ -676,11 +564,11 @@ async def test_form_ssdp_gets_form_with_ignored_entry(hass: HomeAssistant) -> No data=ssdp.SsdpServiceInfo( ssdp_usn="mock_usn", ssdp_st="mock_st", - ssdp_location="http://1.2.3.4:41417/rootDesc.xml", + ssdp_location="http://1.2.3.4:1234/rootDesc.xml", upnp={ "friendlyName": "UniFi Dream Machine New", "modelDescription": "UniFi Dream Machine Pro", - "serialNumber": "e0:63:da:20:14:a9", + "serialNumber": "1", }, ), ) diff --git a/tests/components/unifi/test_hub.py b/tests/components/unifi/test_hub.py index b39ba1915e6..f158d7e57eb 100644 --- a/tests/components/unifi/test_hub.py +++ b/tests/components/unifi/test_hub.py @@ -239,14 +239,14 @@ async def setup_unifi_integration( async def test_hub_setup( device_registry: dr.DeviceRegistry, - prepare_config_entry: Callable[[], ConfigEntry], + config_entry_factory: Callable[[], ConfigEntry], ) -> None: """Successful setup.""" with patch( "homeassistant.config_entries.ConfigEntries.async_forward_entry_setups", return_value=True, ) as forward_entry_setup: - config_entry = await prepare_config_entry() + config_entry = await config_entry_factory() hub = config_entry.runtime_data entry = hub.config.entry @@ -288,10 +288,10 @@ async def test_hub_setup( async def test_reset_after_successful_setup( - hass: HomeAssistant, setup_config_entry: ConfigEntry + hass: HomeAssistant, config_entry_setup: ConfigEntry ) -> None: """Calling reset when the entry has been setup.""" - config_entry = setup_config_entry + config_entry = config_entry_setup assert config_entry.state is ConfigEntryState.LOADED assert await hass.config_entries.async_unload(config_entry.entry_id) @@ -299,10 +299,10 @@ async def test_reset_after_successful_setup( async def test_reset_fails( - hass: HomeAssistant, setup_config_entry: ConfigEntry + hass: HomeAssistant, config_entry_setup: ConfigEntry ) -> None: """Calling reset when the entry has been setup can return false.""" - config_entry = setup_config_entry + config_entry = config_entry_setup assert config_entry.state is ConfigEntryState.LOADED with patch( @@ -330,7 +330,7 @@ async def test_reset_fails( async def test_connection_state_signalling( hass: HomeAssistant, mock_device_registry, - setup_config_entry: ConfigEntry, + config_entry_setup: ConfigEntry, websocket_mock, ) -> None: """Verify connection statesignalling and connection state are working.""" @@ -349,7 +349,7 @@ async def test_connection_state_signalling( async def test_reconnect_mechanism( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, - setup_config_entry: ConfigEntry, + config_entry_setup: ConfigEntry, websocket_mock, ) -> None: """Verify reconnect prints only on first reconnection try.""" @@ -378,7 +378,7 @@ async def test_reconnect_mechanism( async def test_reconnect_mechanism_exceptions( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, - setup_config_entry: ConfigEntry, + config_entry_setup: ConfigEntry, websocket_mock, exception, ) -> None: diff --git a/tests/components/unifi/test_init.py b/tests/components/unifi/test_init.py index 654635ef59f..ef9ea843bc6 100644 --- a/tests/components/unifi/test_init.py +++ b/tests/components/unifi/test_init.py @@ -35,20 +35,20 @@ async def test_setup_with_no_config(hass: HomeAssistant) -> None: async def test_setup_entry_fails_config_entry_not_ready( - hass: HomeAssistant, prepare_config_entry: Callable[[], ConfigEntry] + hass: HomeAssistant, config_entry_factory: Callable[[], ConfigEntry] ) -> None: """Failed authentication trigger a reauthentication flow.""" with patch( "homeassistant.components.unifi.get_unifi_api", side_effect=CannotConnect, ): - config_entry = await prepare_config_entry() + config_entry = await config_entry_factory() assert config_entry.state == ConfigEntryState.SETUP_RETRY async def test_setup_entry_fails_trigger_reauth_flow( - hass: HomeAssistant, prepare_config_entry: Callable[[], ConfigEntry] + hass: HomeAssistant, config_entry_factory: Callable[[], ConfigEntry] ) -> None: """Failed authentication trigger a reauthentication flow.""" with ( @@ -58,7 +58,7 @@ async def test_setup_entry_fails_trigger_reauth_flow( ), patch.object(hass.config_entries.flow, "async_init") as mock_flow_init, ): - config_entry = await prepare_config_entry() + config_entry = await config_entry_factory() mock_flow_init.assert_called_once() assert config_entry.state == ConfigEntryState.SETUP_ERROR @@ -86,7 +86,7 @@ async def test_setup_entry_fails_trigger_reauth_flow( async def test_wireless_clients( hass: HomeAssistant, hass_storage: dict[str, Any], - prepare_config_entry: Callable[[], ConfigEntry], + config_entry_factory: Callable[[], ConfigEntry], ) -> None: """Verify wireless clients class.""" hass_storage[unifi.STORAGE_KEY] = { @@ -98,7 +98,7 @@ async def test_wireless_clients( }, } - await prepare_config_entry() + await config_entry_factory() await flush_store(hass.data[unifi.UNIFI_WIRELESS_CLIENTS]._store) assert sorted(hass_storage[unifi.STORAGE_KEY]["data"]["wireless_clients"]) == [ @@ -173,14 +173,14 @@ async def test_remove_config_entry_device( hass_storage: dict[str, Any], aioclient_mock: AiohttpClientMocker, device_registry: dr.DeviceRegistry, - prepare_config_entry: Callable[[], ConfigEntry], + config_entry_factory: Callable[[], ConfigEntry], client_payload: list[dict[str, Any]], device_payload: list[dict[str, Any]], mock_unifi_websocket, hass_ws_client: WebSocketGenerator, ) -> None: """Verify removing a device manually.""" - config_entry = await prepare_config_entry() + config_entry = await config_entry_factory() assert await async_setup_component(hass, "config", {}) ws_client = await hass_ws_client(hass) From dfb407728f6fb5928041fc48f01a5f5ee43613b7 Mon Sep 17 00:00:00 2001 From: tronikos Date: Fri, 31 May 2024 17:21:18 -0700 Subject: [PATCH 0095/1445] Stop instructing LLM to not pass the domain as a list (#118590) --- homeassistant/helpers/llm.py | 1 - tests/helpers/test_llm.py | 1 - 2 files changed, 2 deletions(-) diff --git a/homeassistant/helpers/llm.py b/homeassistant/helpers/llm.py index dd380795227..fc00c4ebac6 100644 --- a/homeassistant/helpers/llm.py +++ b/homeassistant/helpers/llm.py @@ -254,7 +254,6 @@ class AssistAPI(API): prompt = [ ( "When controlling Home Assistant always call the intent tools. " - "Do not pass the domain to the intent tools as a list. " "Use HassTurnOn to lock and HassTurnOff to unlock a lock. " "When controlling a device, prefer passing just its name and its domain " "(what comes before the dot in its entity id). " diff --git a/tests/helpers/test_llm.py b/tests/helpers/test_llm.py index 9c07295dec7..9ad58441277 100644 --- a/tests/helpers/test_llm.py +++ b/tests/helpers/test_llm.py @@ -421,7 +421,6 @@ async def test_assist_api_prompt( ) first_part_prompt = ( "When controlling Home Assistant always call the intent tools. " - "Do not pass the domain to the intent tools as a list. " "Use HassTurnOn to lock and HassTurnOff to unlock a lock. " "When controlling a device, prefer passing just its name and its domain " "(what comes before the dot in its entity id). " From f3b20d30ae6fe0e3d43221b8cbd26255897123d9 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 1 Jun 2024 00:21:37 -0400 Subject: [PATCH 0096/1445] Add base prompt for LLMs (#118592) --- .../conversation.py | 3 ++- .../openai_conversation/conversation.py | 3 ++- homeassistant/helpers/llm.py | 7 +++++-- .../snapshots/test_conversation.ambr | 18 ++++++------------ 4 files changed, 15 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/google_generative_ai_conversation/conversation.py b/homeassistant/components/google_generative_ai_conversation/conversation.py index 12b1e44b3df..3e289fbe16d 100644 --- a/homeassistant/components/google_generative_ai_conversation/conversation.py +++ b/homeassistant/components/google_generative_ai_conversation/conversation.py @@ -245,7 +245,8 @@ class GoogleGenerativeAIConversationEntity( prompt = "\n".join( ( template.Template( - self.entry.options.get( + llm.BASE_PROMPT + + self.entry.options.get( CONF_PROMPT, llm.DEFAULT_INSTRUCTIONS_PROMPT ), self.hass, diff --git a/homeassistant/components/openai_conversation/conversation.py b/homeassistant/components/openai_conversation/conversation.py index 7cf4d18cce5..306e4134b9e 100644 --- a/homeassistant/components/openai_conversation/conversation.py +++ b/homeassistant/components/openai_conversation/conversation.py @@ -167,7 +167,8 @@ class OpenAIConversationEntity( prompt = "\n".join( ( template.Template( - options.get(CONF_PROMPT, llm.DEFAULT_INSTRUCTIONS_PROMPT), + llm.BASE_PROMPT + + options.get(CONF_PROMPT, llm.DEFAULT_INSTRUCTIONS_PROMPT), self.hass, ).async_render( { diff --git a/homeassistant/helpers/llm.py b/homeassistant/helpers/llm.py index fc00c4ebac6..ec1bfb7dbc4 100644 --- a/homeassistant/helpers/llm.py +++ b/homeassistant/helpers/llm.py @@ -34,10 +34,13 @@ from .singleton import singleton LLM_API_ASSIST = "assist" +BASE_PROMPT = ( + 'Current time is {{ now().strftime("%X") }}. ' + 'Today\'s date is {{ now().strftime("%x") }}.\n' +) + DEFAULT_INSTRUCTIONS_PROMPT = """You are a voice assistant for Home Assistant. Answer in plain text. Keep it simple and to the point. -The current time is {{ now().strftime("%X") }}. -Today's date is {{ now().strftime("%x") }}. """ diff --git a/tests/components/google_generative_ai_conversation/snapshots/test_conversation.ambr b/tests/components/google_generative_ai_conversation/snapshots/test_conversation.ambr index 40ff556af1c..587586cff17 100644 --- a/tests/components/google_generative_ai_conversation/snapshots/test_conversation.ambr +++ b/tests/components/google_generative_ai_conversation/snapshots/test_conversation.ambr @@ -30,10 +30,9 @@ 'history': list([ dict({ 'parts': ''' + Current time is 05:00:00. Today's date is 05/24/24. You are a voice assistant for Home Assistant. Answer in plain text. Keep it simple and to the point. - The current time is 05:00:00. - Today's date is 05/24/24. Only if the user wants to control a device, tell them to edit the AI configuration and allow access to Home Assistant. ''', 'role': 'user', @@ -82,10 +81,9 @@ 'history': list([ dict({ 'parts': ''' + Current time is 05:00:00. Today's date is 05/24/24. You are a voice assistant for Home Assistant. Answer in plain text. Keep it simple and to the point. - The current time is 05:00:00. - Today's date is 05/24/24. Only if the user wants to control a device, tell them to edit the AI configuration and allow access to Home Assistant. ''', 'role': 'user', @@ -146,10 +144,9 @@ 'history': list([ dict({ 'parts': ''' + Current time is 05:00:00. Today's date is 05/24/24. You are a voice assistant for Home Assistant. Answer in plain text. Keep it simple and to the point. - The current time is 05:00:00. - Today's date is 05/24/24. ''', 'role': 'user', @@ -202,10 +199,9 @@ 'history': list([ dict({ 'parts': ''' + Current time is 05:00:00. Today's date is 05/24/24. You are a voice assistant for Home Assistant. Answer in plain text. Keep it simple and to the point. - The current time is 05:00:00. - Today's date is 05/24/24. ''', 'role': 'user', @@ -258,10 +254,9 @@ 'history': list([ dict({ 'parts': ''' + Current time is 05:00:00. Today's date is 05/24/24. You are a voice assistant for Home Assistant. Answer in plain text. Keep it simple and to the point. - The current time is 05:00:00. - Today's date is 05/24/24. ''', 'role': 'user', @@ -314,10 +309,9 @@ 'history': list([ dict({ 'parts': ''' + Current time is 05:00:00. Today's date is 05/24/24. You are a voice assistant for Home Assistant. Answer in plain text. Keep it simple and to the point. - The current time is 05:00:00. - Today's date is 05/24/24. ''', 'role': 'user', From 7af469f81ea5788b6e5f2cbc86478231fa4c7c53 Mon Sep 17 00:00:00 2001 From: tronikos Date: Fri, 31 May 2024 21:55:52 -0700 Subject: [PATCH 0097/1445] Strip Google AI text responses (#118593) * Strip Google AI test responses * strip each part --- .../google_generative_ai_conversation/conversation.py | 2 +- .../google_generative_ai_conversation/test_conversation.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/google_generative_ai_conversation/conversation.py b/homeassistant/components/google_generative_ai_conversation/conversation.py index 3e289fbe16d..2c0b37a1216 100644 --- a/homeassistant/components/google_generative_ai_conversation/conversation.py +++ b/homeassistant/components/google_generative_ai_conversation/conversation.py @@ -355,7 +355,7 @@ class GoogleGenerativeAIConversationEntity( chat_request = glm.Content(parts=tool_responses) intent_response.async_set_speech( - " ".join([part.text for part in chat_response.parts if part.text]) + " ".join([part.text.strip() for part in chat_response.parts if part.text]) ) return conversation.ConversationResult( response=intent_response, conversation_id=conversation_id diff --git a/tests/components/google_generative_ai_conversation/test_conversation.py b/tests/components/google_generative_ai_conversation/test_conversation.py index 13e7bd0c8fb..901216d262f 100644 --- a/tests/components/google_generative_ai_conversation/test_conversation.py +++ b/tests/components/google_generative_ai_conversation/test_conversation.py @@ -80,7 +80,7 @@ async def test_default_prompt( mock_chat.send_message_async.return_value = chat_response mock_part = MagicMock() mock_part.function_call = None - mock_part.text = "Hi there!" + mock_part.text = "Hi there!\n" chat_response.parts = [mock_part] result = await conversation.async_converse( hass, From a4612143e68130a18b91d69b70c264947336e2cf Mon Sep 17 00:00:00 2001 From: tronikos Date: Fri, 31 May 2024 21:57:14 -0700 Subject: [PATCH 0098/1445] Use gemini-1.5-flash-latest in google_generative_ai_conversation.generate_content (#118594) --- .../components/google_generative_ai_conversation/__init__.py | 3 +-- .../snapshots/test_init.ambr | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/google_generative_ai_conversation/__init__.py b/homeassistant/components/google_generative_ai_conversation/__init__.py index b2723f82030..523198355d1 100644 --- a/homeassistant/components/google_generative_ai_conversation/__init__.py +++ b/homeassistant/components/google_generative_ai_conversation/__init__.py @@ -66,8 +66,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: } ) - model_name = "gemini-pro-vision" if image_filenames else "gemini-pro" - model = genai.GenerativeModel(model_name=model_name) + model = genai.GenerativeModel(model_name=RECOMMENDED_CHAT_MODEL) try: response = await model.generate_content_async(prompt_parts) diff --git a/tests/components/google_generative_ai_conversation/snapshots/test_init.ambr b/tests/components/google_generative_ai_conversation/snapshots/test_init.ambr index aba3f35eb19..f68f4c6bf14 100644 --- a/tests/components/google_generative_ai_conversation/snapshots/test_init.ambr +++ b/tests/components/google_generative_ai_conversation/snapshots/test_init.ambr @@ -6,7 +6,7 @@ tuple( ), dict({ - 'model_name': 'gemini-pro-vision', + 'model_name': 'models/gemini-1.5-flash-latest', }), ), tuple( @@ -32,7 +32,7 @@ tuple( ), dict({ - 'model_name': 'gemini-pro', + 'model_name': 'models/gemini-1.5-flash-latest', }), ), tuple( From 51933b0f470077911acf9ea89aea684cb0ced305 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sat, 1 Jun 2024 07:40:26 +0200 Subject: [PATCH 0099/1445] Improve typing in Zabbix (#118545) --- homeassistant/components/zabbix/__init__.py | 37 +++++++++++++-------- homeassistant/components/zabbix/sensor.py | 27 +++++++++------ 2 files changed, 40 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/zabbix/__init__.py b/homeassistant/components/zabbix/__init__.py index 425da7b853a..851af54da32 100644 --- a/homeassistant/components/zabbix/__init__.py +++ b/homeassistant/components/zabbix/__init__.py @@ -1,5 +1,6 @@ """Support for Zabbix.""" +from collections.abc import Callable from contextlib import suppress import json import logging @@ -24,7 +25,7 @@ from homeassistant.const import ( STATE_UNAVAILABLE, STATE_UNKNOWN, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import Event, EventStateChangedData, HomeAssistant, callback from homeassistant.helpers import event as event_helper, state as state_helper import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entityfilter import ( @@ -100,7 +101,9 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool: hass.data[DOMAIN] = zapi - def event_to_metrics(event, float_keys, string_keys): + def event_to_metrics( + event: Event, float_keys: set[str], string_keys: set[str] + ) -> list[ZabbixMetric] | None: """Add an event to the outgoing Zabbix list.""" state = event.data.get("new_state") if state is None or state.state in (STATE_UNKNOWN, "", STATE_UNAVAILABLE): @@ -158,7 +161,7 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool: if publish_states_host: zabbix_sender = ZabbixSender(zabbix_server=conf[CONF_HOST]) - instance = ZabbixThread(hass, zabbix_sender, event_to_metrics) + instance = ZabbixThread(zabbix_sender, event_to_metrics) instance.setup(hass) return True @@ -169,41 +172,47 @@ class ZabbixThread(threading.Thread): MAX_TRIES = 3 - def __init__(self, hass, zabbix_sender, event_to_metrics): + def __init__( + self, + zabbix_sender: ZabbixSender, + event_to_metrics: Callable[ + [Event, set[str], set[str]], list[ZabbixMetric] | None + ], + ) -> None: """Initialize the listener.""" threading.Thread.__init__(self, name="Zabbix") - self.queue = queue.Queue() + self.queue: queue.Queue = queue.Queue() self.zabbix_sender = zabbix_sender self.event_to_metrics = event_to_metrics self.write_errors = 0 self.shutdown = False - self.float_keys = set() - self.string_keys = set() + self.float_keys: set[str] = set() + self.string_keys: set[str] = set() - def setup(self, hass): + def setup(self, hass: HomeAssistant) -> None: """Set up the thread and start it.""" hass.bus.listen(EVENT_STATE_CHANGED, self._event_listener) hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, self._shutdown) self.start() _LOGGER.debug("Started publishing state changes to Zabbix") - def _shutdown(self, event): + def _shutdown(self, event: Event) -> None: """Shut down the thread.""" self.queue.put(None) self.join() @callback - def _event_listener(self, event): + def _event_listener(self, event: Event[EventStateChangedData]) -> None: """Listen for new messages on the bus and queue them for Zabbix.""" item = (time.monotonic(), event) self.queue.put(item) - def get_metrics(self): + def get_metrics(self) -> tuple[int, list[ZabbixMetric]]: """Return a batch of events formatted for writing.""" queue_seconds = QUEUE_BACKLOG_SECONDS + self.MAX_TRIES * RETRY_DELAY count = 0 - metrics = [] + metrics: list[ZabbixMetric] = [] dropped = 0 @@ -233,7 +242,7 @@ class ZabbixThread(threading.Thread): return count, metrics - def write_to_zabbix(self, metrics): + def write_to_zabbix(self, metrics: list[ZabbixMetric]) -> None: """Write preprocessed events to zabbix, with retry.""" for retry in range(self.MAX_TRIES + 1): @@ -254,7 +263,7 @@ class ZabbixThread(threading.Thread): _LOGGER.error("Write error: %s", err) self.write_errors += len(metrics) - def run(self): + def run(self) -> None: """Process incoming events.""" while not self.shutdown: count, metrics = self.get_metrics() diff --git a/homeassistant/components/zabbix/sensor.py b/homeassistant/components/zabbix/sensor.py index eaa06367408..4c6af57f780 100644 --- a/homeassistant/components/zabbix/sensor.py +++ b/homeassistant/components/zabbix/sensor.py @@ -2,8 +2,11 @@ from __future__ import annotations +from collections.abc import Mapping import logging +from typing import Any +from pyzabbix import ZabbixAPI import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity @@ -11,7 +14,7 @@ from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType from .. import zabbix @@ -88,25 +91,25 @@ def setup_platform( class ZabbixTriggerCountSensor(SensorEntity): """Get the active trigger count for all Zabbix monitored hosts.""" - def __init__(self, zapi, name="Zabbix"): + def __init__(self, zapi: ZabbixAPI, name: str | None = "Zabbix") -> None: """Initialize Zabbix sensor.""" self._name = name self._zapi = zapi - self._state = None - self._attributes = {} + self._state: int | None = None + self._attributes: dict[str, Any] = {} @property - def name(self): + def name(self) -> str | None: """Return the name of the sensor.""" return self._name @property - def native_value(self): + def native_value(self) -> StateType: """Return the state of the sensor.""" return self._state @property - def native_unit_of_measurement(self): + def native_unit_of_measurement(self) -> str: """Return the units of measurement.""" return "issues" @@ -122,7 +125,7 @@ class ZabbixTriggerCountSensor(SensorEntity): self._state = len(triggers) @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> Mapping[str, Any] | None: """Return the state attributes of the device.""" return self._attributes @@ -130,7 +133,9 @@ class ZabbixTriggerCountSensor(SensorEntity): class ZabbixSingleHostTriggerCountSensor(ZabbixTriggerCountSensor): """Get the active trigger count for a single Zabbix monitored host.""" - def __init__(self, zapi, hostid, name=None): + def __init__( + self, zapi: ZabbixAPI, hostid: list[str], name: str | None = None + ) -> None: """Initialize Zabbix sensor.""" super().__init__(zapi, name) self._hostid = hostid @@ -154,7 +159,9 @@ class ZabbixSingleHostTriggerCountSensor(ZabbixTriggerCountSensor): class ZabbixMultipleHostTriggerCountSensor(ZabbixTriggerCountSensor): """Get the active trigger count for specified Zabbix monitored hosts.""" - def __init__(self, zapi, hostids, name=None): + def __init__( + self, zapi: ZabbixAPI, hostids: list[str], name: str | None = None + ) -> None: """Initialize Zabbix sensor.""" super().__init__(zapi, name) self._hostids = hostids From 6115dffd80bc3deb47426c14406972346fda4414 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sat, 1 Jun 2024 08:03:32 +0200 Subject: [PATCH 0100/1445] Cleanup pylint ignore in melnor tests (#118564) --- tests/components/melnor/conftest.py | 7 +++---- tests/components/melnor/test_config_flow.py | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/tests/components/melnor/conftest.py b/tests/components/melnor/conftest.py index d96a04aa3f7..27a4a744202 100644 --- a/tests/components/melnor/conftest.py +++ b/tests/components/melnor/conftest.py @@ -4,7 +4,7 @@ from __future__ import annotations from collections.abc import Generator from datetime import UTC, datetime, time, timedelta -from unittest.mock import AsyncMock, patch +from unittest.mock import AsyncMock, _patch, patch from melnor_bluetooth.device import Device import pytest @@ -253,10 +253,9 @@ def mock_setup_entry() -> Generator[AsyncMock, None, None]: yield mock_setup -# pylint: disable=dangerous-default-value def patch_async_discovered_service_info( - return_value: list[BluetoothServiceInfoBleak] = [FAKE_SERVICE_INFO_1], -): + return_value: list[BluetoothServiceInfoBleak], +) -> _patch: """Patch async_discovered_service_info a mocked device info.""" return patch( "homeassistant.components.melnor.config_flow.async_discovered_service_info", diff --git a/tests/components/melnor/test_config_flow.py b/tests/components/melnor/test_config_flow.py index 377954c22df..b90fdd39ce9 100644 --- a/tests/components/melnor/test_config_flow.py +++ b/tests/components/melnor/test_config_flow.py @@ -40,7 +40,7 @@ async def test_user_step_discovered_devices( ) -> None: """Test we properly handle device picking.""" - with patch_async_discovered_service_info(): + with patch_async_discovered_service_info([FAKE_SERVICE_INFO_1]): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, From ca89d22a34884b789a4154a3e426f93243041042 Mon Sep 17 00:00:00 2001 From: Thomas Ytterdal Date: Sat, 1 Jun 2024 11:27:03 +0200 Subject: [PATCH 0101/1445] Ignore myuplink sensors without a description that provide non-numeric values (#115525) Ignore sensors without a description that provide non-numeric values Co-authored-by: Jan-Philipp Benecke --- homeassistant/components/myuplink/sensor.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/myuplink/sensor.py b/homeassistant/components/myuplink/sensor.py index 6cde6b6b071..45a4590a843 100644 --- a/homeassistant/components/myuplink/sensor.py +++ b/homeassistant/components/myuplink/sensor.py @@ -160,6 +160,11 @@ async def async_setup_entry( if find_matching_platform(device_point) == Platform.SENSOR: description = get_description(device_point) entity_class = MyUplinkDevicePointSensor + # Ignore sensors without a description that provide non-numeric values + if description is None and not isinstance( + device_point.value, (int, float) + ): + continue if ( description is not None and description.device_class == SensorDeviceClass.ENUM From b69789d056b84c413e6e8e17eceb64d5b434163c Mon Sep 17 00:00:00 2001 From: Dave T <17680170+davet2001@users.noreply.github.com> Date: Sat, 1 Jun 2024 15:30:32 +0100 Subject: [PATCH 0102/1445] Don't prompt user to verify still image if none was provided in generic camera (#118599) Skip user prompt for preview image if only stream --- homeassistant/components/generic/config_flow.py | 4 ++++ tests/components/generic/test_config_flow.py | 13 +++---------- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/generic/config_flow.py b/homeassistant/components/generic/config_flow.py index af33ae3b36f..6e287c424b9 100644 --- a/homeassistant/components/generic/config_flow.py +++ b/homeassistant/components/generic/config_flow.py @@ -361,6 +361,10 @@ class GenericIPCamConfigFlow(ConfigFlow, domain=DOMAIN): self.user_input = user_input self.title = name + if still_url is None: + return self.async_create_entry( + title=self.title, data={}, options=self.user_input + ) # temporary preview for user to check the image self.context["preview_cam"] = user_input return await self.async_step_user_confirm_still() diff --git a/tests/components/generic/test_config_flow.py b/tests/components/generic/test_config_flow.py index 841fb710717..7e76d8f3891 100644 --- a/tests/components/generic/test_config_flow.py +++ b/tests/components/generic/test_config_flow.py @@ -409,16 +409,9 @@ async def test_form_only_stream( user_flow["flow_id"], data, ) - assert result1["type"] is FlowResultType.FORM - assert result1["step_id"] == "user_confirm_still" - result3 = await hass.config_entries.flow.async_configure( - result1["flow_id"], - user_input={CONF_CONFIRMED_OK: True}, - ) - await hass.async_block_till_done() - assert result3["type"] is FlowResultType.CREATE_ENTRY - assert result3["title"] == "127_0_0_1" - assert result3["options"] == { + assert result1["type"] is FlowResultType.CREATE_ENTRY + assert result1["title"] == "127_0_0_1" + assert result1["options"] == { CONF_AUTHENTICATION: HTTP_BASIC_AUTHENTICATION, CONF_STREAM_SOURCE: "rtsp://user:pass@127.0.0.1/testurl/2", CONF_USERNAME: "fred_flintstone", From 649d6ec11ad5d7df716e577e103b6fbc250787f6 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sat, 1 Jun 2024 18:10:45 +0200 Subject: [PATCH 0103/1445] Bump `nettigo_air_monitor` library to version `3.2.0` (#118600) * Bump nam to version 3.2.0 * Update test snapshot --------- Co-authored-by: Maciej Bieniek <478555+bieniu@users.noreply.github.com> --- homeassistant/components/nam/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/nam/snapshots/test_diagnostics.ambr | 1 + 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nam/manifest.json b/homeassistant/components/nam/manifest.json index a3cb6f54c7c..3b6dba65325 100644 --- a/homeassistant/components/nam/manifest.json +++ b/homeassistant/components/nam/manifest.json @@ -8,7 +8,7 @@ "iot_class": "local_polling", "loggers": ["nettigo_air_monitor"], "quality_scale": "platinum", - "requirements": ["nettigo-air-monitor==3.1.0"], + "requirements": ["nettigo-air-monitor==3.2.0"], "zeroconf": [ { "type": "_http._tcp.local.", diff --git a/requirements_all.txt b/requirements_all.txt index fb5fe9b63a1..79d0b81bca4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1386,7 +1386,7 @@ netdata==1.1.0 netmap==0.7.0.2 # homeassistant.components.nam -nettigo-air-monitor==3.1.0 +nettigo-air-monitor==3.2.0 # homeassistant.components.neurio_energy neurio==0.3.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3bf3dbc1c5a..4ce47d31c7a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1122,7 +1122,7 @@ nessclient==1.0.0 netmap==0.7.0.2 # homeassistant.components.nam -nettigo-air-monitor==3.1.0 +nettigo-air-monitor==3.2.0 # homeassistant.components.nexia nexia==2.0.8 diff --git a/tests/components/nam/snapshots/test_diagnostics.ambr b/tests/components/nam/snapshots/test_diagnostics.ambr index 2ebc0246090..a8072ee224d 100644 --- a/tests/components/nam/snapshots/test_diagnostics.ambr +++ b/tests/components/nam/snapshots/test_diagnostics.ambr @@ -11,6 +11,7 @@ 'bmp280_temperature': 5.6, 'dht22_humidity': 46.2, 'dht22_temperature': 6.3, + 'ds18b20_temperature': None, 'heca_humidity': 50.0, 'heca_temperature': 8.0, 'mhz14a_carbon_dioxide': 865.0, From daadc4662a7a5a39bb820ab1f70dad8bf8299228 Mon Sep 17 00:00:00 2001 From: Sid <27780930+autinerd@users.noreply.github.com> Date: Sat, 1 Jun 2024 20:04:04 +0200 Subject: [PATCH 0104/1445] Bump ruff to 0.4.7 (#118612) --- .pre-commit-config.yaml | 2 +- pyproject.toml | 2 +- requirements_test_pre_commit.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e353d3a6c17..57ab5e702b5 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.4.6 + rev: v0.4.7 hooks: - id: ruff args: diff --git a/pyproject.toml b/pyproject.toml index 9484420adb9..33d5efde370 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -669,7 +669,7 @@ filterwarnings = [ ] [tool.ruff] -required-version = ">=0.4.6" +required-version = ">=0.4.7" [tool.ruff.lint] select = [ diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index acd443e3040..e465849f02a 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -1,5 +1,5 @@ # Automatically generated from .pre-commit-config.yaml by gen_requirements_all.py, do not edit codespell==2.3.0 -ruff==0.4.6 +ruff==0.4.7 yamllint==1.35.1 From 1f922798d8c6681839ab3a1fb842c186c9ee7841 Mon Sep 17 00:00:00 2001 From: Alexandre CUER Date: Sat, 1 Jun 2024 21:14:18 +0200 Subject: [PATCH 0105/1445] Add new codeowner for emoncms integration (#118609) adding new codeowner --- CODEOWNERS | 2 +- homeassistant/components/emoncms/manifest.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 32f885f6015..a626ebc2f29 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -379,7 +379,7 @@ build.json @home-assistant/supervisor /homeassistant/components/elvia/ @ludeeus /tests/components/elvia/ @ludeeus /homeassistant/components/emby/ @mezz64 -/homeassistant/components/emoncms/ @borpin +/homeassistant/components/emoncms/ @borpin @alexandrecuer /homeassistant/components/emonitor/ @bdraco /tests/components/emonitor/ @bdraco /homeassistant/components/emulated_hue/ @bdraco @Tho85 diff --git a/homeassistant/components/emoncms/manifest.json b/homeassistant/components/emoncms/manifest.json index 21f625acb4a..02008a90ac9 100644 --- a/homeassistant/components/emoncms/manifest.json +++ b/homeassistant/components/emoncms/manifest.json @@ -1,7 +1,7 @@ { "domain": "emoncms", "name": "Emoncms", - "codeowners": ["@borpin"], + "codeowners": ["@borpin", "@alexandrecuer"], "documentation": "https://www.home-assistant.io/integrations/emoncms", "iot_class": "local_polling" } From e485a0c6f26617fa3f864b56a94bc11a36eac7f2 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 1 Jun 2024 22:26:23 +0200 Subject: [PATCH 0106/1445] Update typing-extensions to 4.12.1 (#118615) --- homeassistant/package_constraints.txt | 2 +- pyproject.toml | 2 +- requirements.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 5f823188423..41b1c2c3fef 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -55,7 +55,7 @@ pyudev==0.24.1 PyYAML==6.0.1 requests==2.31.0 SQLAlchemy==2.0.30 -typing-extensions>=4.12.0,<5.0 +typing-extensions>=4.12.1,<5.0 ulid-transform==0.9.0 urllib3>=1.26.5,<2 voluptuous-serialize==2.6.0 diff --git a/pyproject.toml b/pyproject.toml index 33d5efde370..c23a7ea3067 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -61,7 +61,7 @@ dependencies = [ "PyYAML==6.0.1", "requests==2.31.0", "SQLAlchemy==2.0.30", - "typing-extensions>=4.12.0,<5.0", + "typing-extensions>=4.12.1,<5.0", "ulid-transform==0.9.0", # Constrain urllib3 to ensure we deal with CVE-2020-26137 and CVE-2021-33503 # Temporary setting an upper bound, to prevent compat issues with urllib3>=2 diff --git a/requirements.txt b/requirements.txt index d77962d64d7..abf91d7f2ec 100644 --- a/requirements.txt +++ b/requirements.txt @@ -36,7 +36,7 @@ python-slugify==8.0.4 PyYAML==6.0.1 requests==2.31.0 SQLAlchemy==2.0.30 -typing-extensions>=4.12.0,<5.0 +typing-extensions>=4.12.1,<5.0 ulid-transform==0.9.0 urllib3>=1.26.5,<2 voluptuous==0.13.1 From 46eb779c5ca10104c22e802d514b92840cdcf395 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Sat, 1 Jun 2024 23:51:17 +0200 Subject: [PATCH 0107/1445] Avoid future exception during setup of Synology DSM (#118583) * avoid future exception during integration setup * clear future flag during setup * always clear the flag (with comment) --- homeassistant/components/synology_dsm/common.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/synology_dsm/common.py b/homeassistant/components/synology_dsm/common.py index 98a57319f93..e2023aa91a1 100644 --- a/homeassistant/components/synology_dsm/common.py +++ b/homeassistant/components/synology_dsm/common.py @@ -104,6 +104,11 @@ class SynoApi: except BaseException as err: if not self._login_future.done(): self._login_future.set_exception(err) + with suppress(BaseException): + # Clear the flag as its normal that nothing + # will wait for this future to be resolved + # if there are no concurrent login attempts + await self._login_future raise finally: self._login_future = None From d67ed42edc15d02052648679e8d2f032a633cafc Mon Sep 17 00:00:00 2001 From: Luca Angemi Date: Sun, 2 Jun 2024 08:32:24 +0200 Subject: [PATCH 0108/1445] Fix telegram bot send_document (#118616) --- homeassistant/components/telegram_bot/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/telegram_bot/__init__.py b/homeassistant/components/telegram_bot/__init__.py index df5bebb47d4..06c15da5f70 100644 --- a/homeassistant/components/telegram_bot/__init__.py +++ b/homeassistant/components/telegram_bot/__init__.py @@ -287,7 +287,9 @@ SERVICE_MAP = { def _read_file_as_bytesio(file_path: str) -> io.BytesIO: """Read a file and return it as a BytesIO object.""" with open(file_path, "rb") as file: - return io.BytesIO(file.read()) + data = io.BytesIO(file.read()) + data.name = file_path + return data async def load_data( From e976db84432eccff5c9efccb9143921e03200187 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sun, 2 Jun 2024 10:42:42 +0200 Subject: [PATCH 0109/1445] Address late review comment in samsungtv (#118539) Address late comment in samsungtv --- homeassistant/components/samsungtv/bridge.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/samsungtv/bridge.py b/homeassistant/components/samsungtv/bridge.py index 0b8a5d4a268..059c6682857 100644 --- a/homeassistant/components/samsungtv/bridge.py +++ b/homeassistant/components/samsungtv/bridge.py @@ -325,6 +325,11 @@ class SamsungTVLegacyBridge(SamsungTVBridge): """Try to gather infos of this device.""" return None + def _notify_reauth_callback(self) -> None: + """Notify access denied callback.""" + if self._reauth_callback is not None: + self.hass.loop.call_soon_threadsafe(self._reauth_callback) + def _get_remote(self) -> Remote: """Create or return a remote control instance.""" if self._remote is None: From 8f942050143e2acdd7e73a2445835bc7317c9eb6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 2 Jun 2024 05:36:25 -0500 Subject: [PATCH 0110/1445] Include a traceback for non-strict event loop blocking detection (#118620) --- homeassistant/helpers/frame.py | 8 ++++---- homeassistant/util/loop.py | 13 ++++++++----- tests/common.py | 2 ++ tests/helpers/test_frame.py | 6 +++--- tests/test_loader.py | 4 ++-- tests/util/test_loop.py | 11 +++++++++++ 6 files changed, 30 insertions(+), 14 deletions(-) diff --git a/homeassistant/helpers/frame.py b/homeassistant/helpers/frame.py index 3046b718489..e8ba6ba0c07 100644 --- a/homeassistant/helpers/frame.py +++ b/homeassistant/helpers/frame.py @@ -31,17 +31,17 @@ class IntegrationFrame: integration: str module: str | None relative_filename: str - _frame: FrameType + frame: FrameType @cached_property def line_number(self) -> int: """Return the line number of the frame.""" - return self._frame.f_lineno + return self.frame.f_lineno @cached_property def filename(self) -> str: """Return the filename of the frame.""" - return self._frame.f_code.co_filename + return self.frame.f_code.co_filename @cached_property def line(self) -> str: @@ -119,7 +119,7 @@ def get_integration_frame(exclude_integrations: set | None = None) -> Integratio integration=integration, module=found_module, relative_filename=found_frame.f_code.co_filename[index:], - _frame=found_frame, + frame=found_frame, ) diff --git a/homeassistant/util/loop.py b/homeassistant/util/loop.py index cba9f7c3900..64be00cfe35 100644 --- a/homeassistant/util/loop.py +++ b/homeassistant/util/loop.py @@ -7,6 +7,7 @@ import functools import linecache import logging import threading +import traceback from typing import Any from homeassistant.core import async_get_hass_or_none @@ -54,12 +55,14 @@ def raise_for_blocking_call( if not strict_core: _LOGGER.warning( "Detected blocking call to %s with args %s in %s, " - "line %s: %s inside the event loop", + "line %s: %s inside the event loop\n" + "Traceback (most recent call last):\n%s", func.__name__, mapped_args.get("args"), offender_filename, offender_lineno, offender_line, + "".join(traceback.format_stack(f=offender_frame)), ) return @@ -79,10 +82,9 @@ def raise_for_blocking_call( ) _LOGGER.warning( - ( - "Detected blocking call to %s inside the event loop by %sintegration '%s' " - "at %s, line %s: %s (offender: %s, line %s: %s), please %s" - ), + "Detected blocking call to %s inside the event loop by %sintegration '%s' " + "at %s, line %s: %s (offender: %s, line %s: %s), please %s\n" + "Traceback (most recent call last):\n%s", func.__name__, "custom " if integration_frame.custom_integration else "", integration_frame.integration, @@ -93,6 +95,7 @@ def raise_for_blocking_call( offender_lineno, offender_line, report_issue, + "".join(traceback.format_stack(f=integration_frame.frame)), ) if strict: diff --git a/tests/common.py b/tests/common.py index 6e7cf1b21f3..897a28fbffd 100644 --- a/tests/common.py +++ b/tests/common.py @@ -1689,8 +1689,10 @@ def help_test_all(module: ModuleType) -> None: def extract_stack_to_frame(extract_stack: list[Mock]) -> FrameType: """Convert an extract stack to a frame list.""" stack = list(extract_stack) + _globals = globals() for frame in stack: frame.f_back = None + frame.f_globals = _globals frame.f_code.co_filename = frame.filename frame.f_lineno = int(frame.lineno) diff --git a/tests/helpers/test_frame.py b/tests/helpers/test_frame.py index 904bed965c8..e6251963d36 100644 --- a/tests/helpers/test_frame.py +++ b/tests/helpers/test_frame.py @@ -17,7 +17,7 @@ async def test_extract_frame_integration( integration_frame = frame.get_integration_frame() assert integration_frame == frame.IntegrationFrame( custom_integration=False, - _frame=mock_integration_frame, + frame=mock_integration_frame, integration="hue", module=None, relative_filename="homeassistant/components/hue/light.py", @@ -42,7 +42,7 @@ async def test_extract_frame_resolve_module( assert integration_frame == frame.IntegrationFrame( custom_integration=True, - _frame=ANY, + frame=ANY, integration="test_integration_frame", module="custom_components.test_integration_frame", relative_filename="custom_components/test_integration_frame/__init__.py", @@ -98,7 +98,7 @@ async def test_extract_frame_integration_with_excluded_integration( assert integration_frame == frame.IntegrationFrame( custom_integration=False, - _frame=correct_frame, + frame=correct_frame, integration="mdns", module=None, relative_filename="homeassistant/components/mdns/light.py", diff --git a/tests/test_loader.py b/tests/test_loader.py index b2ca8cbd397..fa4a3a14cef 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -1271,7 +1271,7 @@ async def test_hass_components_use_reported( ) integration_frame = frame.IntegrationFrame( custom_integration=True, - _frame=mock_integration_frame, + frame=mock_integration_frame, integration="test_integration_frame", module="custom_components.test_integration_frame", relative_filename="custom_components/test_integration_frame/__init__.py", @@ -1969,7 +1969,7 @@ async def test_hass_helpers_use_reported( """Test that use of hass.components is reported.""" integration_frame = frame.IntegrationFrame( custom_integration=True, - _frame=mock_integration_frame, + frame=mock_integration_frame, integration="test_integration_frame", module="custom_components.test_integration_frame", relative_filename="custom_components/test_integration_frame/__init__.py", diff --git a/tests/util/test_loop.py b/tests/util/test_loop.py index c3cfb3d0f06..506614d7631 100644 --- a/tests/util/test_loop.py +++ b/tests/util/test_loop.py @@ -27,6 +27,7 @@ async def test_raise_for_blocking_call_async_non_strict_core( """Test non_strict_core raise_for_blocking_call detects from event loop without integration context.""" haloop.raise_for_blocking_call(banned_function, strict_core=False) assert "Detected blocking call to banned_function" in caplog.text + assert "Traceback (most recent call last)" in caplog.text async def test_raise_for_blocking_call_async_integration( @@ -130,6 +131,11 @@ async def test_raise_for_blocking_call_async_integration_non_strict( "please create a bug report at https://github.com/home-assistant/core/issues?" "q=is%3Aopen+is%3Aissue+label%3A%22integration%3A+hue%22" in caplog.text ) + assert "Traceback (most recent call last)" in caplog.text + assert ( + 'File "/home/paulus/homeassistant/components/hue/light.py", line 23' + in caplog.text + ) async def test_raise_for_blocking_call_async_custom( @@ -182,6 +188,11 @@ async def test_raise_for_blocking_call_async_custom( "please create a bug report at https://github.com/home-assistant/core/issues?" "q=is%3Aopen+is%3Aissue+label%3A%22integration%3A+hue%22" ) in caplog.text + assert "Traceback (most recent call last)" in caplog.text + assert ( + 'File "/home/paulus/config/custom_components/hue/light.py", line 23' + in caplog.text + ) async def test_raise_for_blocking_call_sync( From dbb27755a4a6407ac8b259cb44327ff75a2d1a81 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sun, 2 Jun 2024 15:28:24 +0200 Subject: [PATCH 0111/1445] Update mypy-dev to 1.11.0a5 (#118519) --- homeassistant/components/nws/weather.py | 4 +++- homeassistant/components/recorder/models/state.py | 4 ++-- homeassistant/components/transmission/__init__.py | 3 ++- homeassistant/util/json.py | 8 ++++---- requirements_test.txt | 2 +- 5 files changed, 12 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/nws/weather.py b/homeassistant/components/nws/weather.py index 21d9a62bbb0..9ae1f9f7ff9 100644 --- a/homeassistant/components/nws/weather.py +++ b/homeassistant/components/nws/weather.py @@ -4,7 +4,7 @@ from __future__ import annotations from functools import partial from types import MappingProxyType -from typing import TYPE_CHECKING, Any, cast +from typing import TYPE_CHECKING, Any, Literal, cast from homeassistant.components.weather import ( ATTR_CONDITION_CLEAR_NIGHT, @@ -156,6 +156,8 @@ class NWSWeather(CoordinatorWeatherEntity[TimestampDataUpdateCoordinator[None]]) for forecast_type in ("twice_daily", "hourly"): if (coordinator := self.forecast_coordinators[forecast_type]) is None: continue + if TYPE_CHECKING: + forecast_type = cast(Literal["twice_daily", "hourly"], forecast_type) self.unsub_forecast[forecast_type] = coordinator.async_add_listener( partial(self._handle_forecast_update, forecast_type) ) diff --git a/homeassistant/components/recorder/models/state.py b/homeassistant/components/recorder/models/state.py index ca70b856d76..139522a3d20 100644 --- a/homeassistant/components/recorder/models/state.py +++ b/homeassistant/components/recorder/models/state.py @@ -74,7 +74,7 @@ class LazyState(State): def last_changed(self) -> datetime: # type: ignore[override] """Last changed datetime.""" return dt_util.utc_from_timestamp( - self._last_changed_ts or self._last_updated_ts + self._last_changed_ts or self._last_updated_ts # type: ignore[arg-type] ) @cached_property @@ -86,7 +86,7 @@ class LazyState(State): def last_reported(self) -> datetime: # type: ignore[override] """Last reported datetime.""" return dt_util.utc_from_timestamp( - self._last_reported_ts or self._last_updated_ts + self._last_reported_ts or self._last_updated_ts # type: ignore[arg-type] ) @cached_property diff --git a/homeassistant/components/transmission/__init__.py b/homeassistant/components/transmission/__init__.py index d7d6ae4ea0c..681b4438099 100644 --- a/homeassistant/components/transmission/__init__.py +++ b/homeassistant/components/transmission/__init__.py @@ -5,7 +5,7 @@ from __future__ import annotations from functools import partial import logging import re -from typing import Any +from typing import Any, Literal import transmission_rpc from transmission_rpc.error import ( @@ -248,6 +248,7 @@ async def get_api( hass: HomeAssistant, entry: dict[str, Any] ) -> transmission_rpc.Client: """Get Transmission client.""" + protocol: Literal["http", "https"] protocol = "https" if entry[CONF_SSL] else "http" host = entry[CONF_HOST] port = entry[CONF_PORT] diff --git a/homeassistant/util/json.py b/homeassistant/util/json.py index 9a30ae8f104..1479550b615 100644 --- a/homeassistant/util/json.py +++ b/homeassistant/util/json.py @@ -17,13 +17,13 @@ from .file import WriteError # noqa: F401 _SENTINEL = object() _LOGGER = logging.getLogger(__name__) -JsonValueType = ( - dict[str, "JsonValueType"] | list["JsonValueType"] | str | int | float | bool | None +type JsonValueType = ( + dict[str, JsonValueType] | list[JsonValueType] | str | int | float | bool | None ) """Any data that can be returned by the standard JSON deserializing process.""" -JsonArrayType = list[JsonValueType] +type JsonArrayType = list[JsonValueType] """List that can be returned by the standard JSON deserializing process.""" -JsonObjectType = dict[str, JsonValueType] +type JsonObjectType = dict[str, JsonValueType] """Dictionary that can be returned by the standard JSON deserializing process.""" JSON_ENCODE_EXCEPTIONS = (TypeError, ValueError) diff --git a/requirements_test.txt b/requirements_test.txt index 1b1afc24c81..5651a411cb0 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -11,7 +11,7 @@ astroid==3.2.2 coverage==7.5.0 freezegun==1.5.0 mock-open==1.4.0 -mypy-dev==1.11.0a3 +mypy-dev==1.11.0a5 pre-commit==3.7.1 pydantic==1.10.15 pylint==3.2.2 From 37fc16d7b6af3fe9addfe4e46a199223294622b6 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sun, 2 Jun 2024 15:34:30 +0200 Subject: [PATCH 0112/1445] Fix incorrect `patch` type hint in main conftest (#118461) --- pylint/plugins/hass_enforce_type_hints.py | 2 +- tests/components/network/conftest.py | 7 ++++++- tests/conftest.py | 10 ++++++---- tests/helpers/test_translation.py | 2 +- 4 files changed, 14 insertions(+), 7 deletions(-) diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index c6c6986060f..e99c5c1ed39 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -138,7 +138,7 @@ _TEST_FIXTURES: dict[str, list[str] | str] = { "mock_bluetooth": "None", "mock_bluetooth_adapters": "None", "mock_device_tracker_conf": "list[Device]", - "mock_get_source_ip": "None", + "mock_get_source_ip": "_patch", "mock_hass_config": "None", "mock_hass_config_yaml": "None", "mock_zeroconf": "MagicMock", diff --git a/tests/components/network/conftest.py b/tests/components/network/conftest.py index 0756ca3b95c..d069fff71b6 100644 --- a/tests/components/network/conftest.py +++ b/tests/components/network/conftest.py @@ -1,5 +1,8 @@ """Tests for the Network Configuration integration.""" +from collections.abc import Generator +from unittest.mock import _patch + import pytest @@ -9,7 +12,9 @@ def mock_network(): @pytest.fixture(autouse=True) -def override_mock_get_source_ip(mock_get_source_ip): +def override_mock_get_source_ip( + mock_get_source_ip: _patch, +) -> Generator[None, None, None]: """Override mock of network util's async_get_source_ip.""" mock_get_source_ip.stop() yield diff --git a/tests/conftest.py b/tests/conftest.py index 4a33ea0e482..13a8daa8ce1 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -15,7 +15,7 @@ import sqlite3 import ssl import threading from typing import TYPE_CHECKING, Any, cast -from unittest.mock import AsyncMock, MagicMock, Mock, patch +from unittest.mock import AsyncMock, MagicMock, Mock, _patch, patch from aiohttp import client from aiohttp.test_utils import ( @@ -1151,7 +1151,7 @@ def mock_network() -> Generator[None, None, None]: @pytest.fixture(autouse=True, scope="session") -def mock_get_source_ip() -> Generator[patch, None, None]: +def mock_get_source_ip() -> Generator[_patch, None, None]: """Mock network util's async_get_source_ip.""" patcher = patch( "homeassistant.components.network.util.async_get_source_ip", @@ -1165,7 +1165,7 @@ def mock_get_source_ip() -> Generator[patch, None, None]: @pytest.fixture(autouse=True, scope="session") -def translations_once() -> Generator[patch, None, None]: +def translations_once() -> Generator[_patch, None, None]: """Only load translations once per session.""" from homeassistant.helpers.translation import _TranslationsCacheData @@ -1182,7 +1182,9 @@ def translations_once() -> Generator[patch, None, None]: @pytest.fixture -def disable_translations_once(translations_once): +def disable_translations_once( + translations_once: _patch, +) -> Generator[None, None, None]: """Override loading translations once.""" translations_once.stop() yield diff --git a/tests/helpers/test_translation.py b/tests/helpers/test_translation.py index 0e8bbfc4b60..d1df7004c99 100644 --- a/tests/helpers/test_translation.py +++ b/tests/helpers/test_translation.py @@ -15,7 +15,7 @@ from homeassistant.setup import async_setup_component @pytest.fixture(autouse=True) -def _disable_translations_once(disable_translations_once): +def _disable_translations_once(disable_translations_once: None) -> None: """Override loading translations once.""" From 54a1a4ab41cb418a6e450b8a9aee00a855acd5ac Mon Sep 17 00:00:00 2001 From: Adam Pasztor Date: Sun, 2 Jun 2024 15:41:44 +0200 Subject: [PATCH 0113/1445] Bump pyads to 3.4.0 (#116934) Co-authored-by: J. Nick Koston --- homeassistant/components/ads/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/ads/manifest.json b/homeassistant/components/ads/manifest.json index e5adb593755..0a2cd118a19 100644 --- a/homeassistant/components/ads/manifest.json +++ b/homeassistant/components/ads/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/ads", "iot_class": "local_push", "loggers": ["pyads"], - "requirements": ["pyads==3.2.2"] + "requirements": ["pyads==3.4.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 79d0b81bca4..7bb21369fbc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1694,7 +1694,7 @@ pyW215==0.7.0 pyW800rf32==0.4 # homeassistant.components.ads -pyads==3.2.2 +pyads==3.4.0 # homeassistant.components.hisense_aehw4a1 pyaehw4a1==0.3.9 From 51ed4f89ec0c0bfc44aa9910006c342b29e14016 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 2 Jun 2024 13:04:53 -0500 Subject: [PATCH 0114/1445] Use more efficient chunked_or_all for recorder table managers (#118646) --- .../components/recorder/table_managers/event_data.py | 8 ++++---- .../components/recorder/table_managers/event_types.py | 4 ++-- .../recorder/table_managers/state_attributes.py | 8 ++++---- .../components/recorder/table_managers/states_meta.py | 4 ++-- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/recorder/table_managers/event_data.py b/homeassistant/components/recorder/table_managers/event_data.py index 28f02127d42..1d2fa580b3c 100644 --- a/homeassistant/components/recorder/table_managers/event_data.py +++ b/homeassistant/components/recorder/table_managers/event_data.py @@ -2,14 +2,14 @@ from __future__ import annotations -from collections.abc import Iterable +from collections.abc import Collection, Iterable import logging from typing import TYPE_CHECKING, cast from sqlalchemy.orm.session import Session from homeassistant.core import Event -from homeassistant.util.collection import chunked +from homeassistant.util.collection import chunked_or_all from homeassistant.util.json import JSON_ENCODE_EXCEPTIONS from ..db_schema import EventData @@ -87,7 +87,7 @@ class EventDataManager(BaseLRUTableManager[EventData]): return results | self._load_from_hashes(missing_hashes, session) def _load_from_hashes( - self, hashes: Iterable[int], session: Session + self, hashes: Collection[int], session: Session ) -> dict[str, int | None]: """Load the shared_datas to data_ids mapping into memory from a list of hashes. @@ -96,7 +96,7 @@ class EventDataManager(BaseLRUTableManager[EventData]): """ results: dict[str, int | None] = {} with session.no_autoflush: - for hashs_chunk in chunked(hashes, self.recorder.max_bind_vars): + for hashs_chunk in chunked_or_all(hashes, self.recorder.max_bind_vars): for data_id, shared_data in execute_stmt_lambda_element( session, get_shared_event_datas(hashs_chunk), orm_rows=False ): diff --git a/homeassistant/components/recorder/table_managers/event_types.py b/homeassistant/components/recorder/table_managers/event_types.py index 29eaf2450ad..266c970fe1f 100644 --- a/homeassistant/components/recorder/table_managers/event_types.py +++ b/homeassistant/components/recorder/table_managers/event_types.py @@ -9,7 +9,7 @@ from lru import LRU from sqlalchemy.orm.session import Session from homeassistant.core import Event -from homeassistant.util.collection import chunked +from homeassistant.util.collection import chunked_or_all from homeassistant.util.event_type import EventType from ..db_schema import EventTypes @@ -88,7 +88,7 @@ class EventTypeManager(BaseLRUTableManager[EventTypes]): return results with session.no_autoflush: - for missing_chunk in chunked(missing, self.recorder.max_bind_vars): + for missing_chunk in chunked_or_all(missing, self.recorder.max_bind_vars): for event_type_id, event_type in execute_stmt_lambda_element( session, find_event_type_ids(missing_chunk), orm_rows=False ): diff --git a/homeassistant/components/recorder/table_managers/state_attributes.py b/homeassistant/components/recorder/table_managers/state_attributes.py index 4a705858d44..5ed67b0504f 100644 --- a/homeassistant/components/recorder/table_managers/state_attributes.py +++ b/homeassistant/components/recorder/table_managers/state_attributes.py @@ -2,14 +2,14 @@ from __future__ import annotations -from collections.abc import Iterable +from collections.abc import Collection, Iterable import logging from typing import TYPE_CHECKING, cast from sqlalchemy.orm.session import Session from homeassistant.core import Event, EventStateChangedData -from homeassistant.util.collection import chunked +from homeassistant.util.collection import chunked_or_all from homeassistant.util.json import JSON_ENCODE_EXCEPTIONS from ..db_schema import StateAttributes @@ -98,7 +98,7 @@ class StateAttributesManager(BaseLRUTableManager[StateAttributes]): return results | self._load_from_hashes(missing_hashes, session) def _load_from_hashes( - self, hashes: Iterable[int], session: Session + self, hashes: Collection[int], session: Session ) -> dict[str, int | None]: """Load the shared_attrs to attributes_ids mapping into memory from a list of hashes. @@ -107,7 +107,7 @@ class StateAttributesManager(BaseLRUTableManager[StateAttributes]): """ results: dict[str, int | None] = {} with session.no_autoflush: - for hashs_chunk in chunked(hashes, self.recorder.max_bind_vars): + for hashs_chunk in chunked_or_all(hashes, self.recorder.max_bind_vars): for attributes_id, shared_attrs in execute_stmt_lambda_element( session, get_shared_attributes(hashs_chunk), orm_rows=False ): diff --git a/homeassistant/components/recorder/table_managers/states_meta.py b/homeassistant/components/recorder/table_managers/states_meta.py index 5e5f2f06796..0ea2c7415b9 100644 --- a/homeassistant/components/recorder/table_managers/states_meta.py +++ b/homeassistant/components/recorder/table_managers/states_meta.py @@ -8,7 +8,7 @@ from typing import TYPE_CHECKING, cast from sqlalchemy.orm.session import Session from homeassistant.core import Event, EventStateChangedData -from homeassistant.util.collection import chunked +from homeassistant.util.collection import chunked_or_all from ..db_schema import StatesMeta from ..queries import find_all_states_metadata_ids, find_states_metadata_ids @@ -107,7 +107,7 @@ class StatesMetaManager(BaseLRUTableManager[StatesMeta]): update_cache = from_recorder or not self._did_first_load with session.no_autoflush: - for missing_chunk in chunked(missing, self.recorder.max_bind_vars): + for missing_chunk in chunked_or_all(missing, self.recorder.max_bind_vars): for metadata_id, entity_id in execute_stmt_lambda_element( session, find_states_metadata_ids(missing_chunk) ): From 51394cefbaaabd0153268e28920e82156fa7bc16 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sun, 2 Jun 2024 20:15:35 +0200 Subject: [PATCH 0115/1445] Fix incorrect placeholder in SharkIQ (#118640) Update strings.json --- homeassistant/components/sharkiq/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/sharkiq/strings.json b/homeassistant/components/sharkiq/strings.json index c1648332975..63d4f6af48b 100644 --- a/homeassistant/components/sharkiq/strings.json +++ b/homeassistant/components/sharkiq/strings.json @@ -43,7 +43,7 @@ }, "exceptions": { "invalid_room": { - "message": "The room { room } is unavailable to your vacuum. Make sure all rooms match the Shark App, including capitalization." + "message": "The room {room} is unavailable to your vacuum. Make sure all rooms match the Shark App, including capitalization." } }, "services": { From afc29fdbe73e548d47bbf9f9326669c7c7460066 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sun, 2 Jun 2024 20:55:36 +0200 Subject: [PATCH 0116/1445] Add support for the DS18B20 temperature sensor to Nettigo Air Monitor integration (#118601) Add support for DS18B20 temperature sensor Co-authored-by: Maciej Bieniek <478555+bieniu@users.noreply.github.com> --- homeassistant/components/nam/const.py | 1 + homeassistant/components/nam/sensor.py | 10 ++++ homeassistant/components/nam/strings.json | 3 ++ tests/components/nam/fixtures/nam_data.json | 1 + .../nam/snapshots/test_diagnostics.ambr | 2 +- .../components/nam/snapshots/test_sensor.ambr | 54 +++++++++++++++++++ 6 files changed, 70 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/nam/const.py b/homeassistant/components/nam/const.py index 2e4d6b0c85a..4b7b50b309a 100644 --- a/homeassistant/components/nam/const.py +++ b/homeassistant/components/nam/const.py @@ -20,6 +20,7 @@ ATTR_BMP280_PRESSURE: Final = "bmp280_pressure" ATTR_BMP280_TEMPERATURE: Final = "bmp280_temperature" ATTR_DHT22_HUMIDITY: Final = "dht22_humidity" ATTR_DHT22_TEMPERATURE: Final = "dht22_temperature" +ATTR_DS18B20_TEMPERATURE: Final = "ds18b20_temperature" ATTR_HECA_HUMIDITY: Final = "heca_humidity" ATTR_HECA_TEMPERATURE: Final = "heca_temperature" ATTR_MHZ14A_CARBON_DIOXIDE: Final = "mhz14a_carbon_dioxide" diff --git a/homeassistant/components/nam/sensor.py b/homeassistant/components/nam/sensor.py index 0f4647d071f..27fae62be8a 100644 --- a/homeassistant/components/nam/sensor.py +++ b/homeassistant/components/nam/sensor.py @@ -43,6 +43,7 @@ from .const import ( ATTR_BMP280_TEMPERATURE, ATTR_DHT22_HUMIDITY, ATTR_DHT22_TEMPERATURE, + ATTR_DS18B20_TEMPERATURE, ATTR_HECA_HUMIDITY, ATTR_HECA_TEMPERATURE, ATTR_MHZ14A_CARBON_DIOXIDE, @@ -145,6 +146,15 @@ SENSORS: tuple[NAMSensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, value=lambda sensors: sensors.bmp280_temperature, ), + NAMSensorEntityDescription( + key=ATTR_DS18B20_TEMPERATURE, + translation_key="ds18b20_temperature", + suggested_display_precision=1, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + value=lambda sensors: sensors.ds18b20_temperature, + ), NAMSensorEntityDescription( key=ATTR_HECA_HUMIDITY, translation_key="heca_humidity", diff --git a/homeassistant/components/nam/strings.json b/homeassistant/components/nam/strings.json index be41f50c7b6..c4921ec52f9 100644 --- a/homeassistant/components/nam/strings.json +++ b/homeassistant/components/nam/strings.json @@ -75,6 +75,9 @@ "bmp280_temperature": { "name": "BMP280 temperature" }, + "ds18b20_temperature": { + "name": "DS18B20 temperature" + }, "heca_humidity": { "name": "HECA humidity" }, diff --git a/tests/components/nam/fixtures/nam_data.json b/tests/components/nam/fixtures/nam_data.json index 93a33d4a552..82dacbefb34 100644 --- a/tests/components/nam/fixtures/nam_data.json +++ b/tests/components/nam/fixtures/nam_data.json @@ -15,6 +15,7 @@ { "value_type": "BME280_temperature", "value": "7.56" }, { "value_type": "BME280_humidity", "value": "45.69" }, { "value_type": "BME280_pressure", "value": "101101.17" }, + { "value_type": "DS18B20_temperature", "value": "12.56" }, { "value_type": "BMP_temperature", "value": "7.56" }, { "value_type": "BMP_pressure", "value": "103201.18" }, { "value_type": "BMP280_temperature", "value": "5.56" }, diff --git a/tests/components/nam/snapshots/test_diagnostics.ambr b/tests/components/nam/snapshots/test_diagnostics.ambr index a8072ee224d..c187dec2866 100644 --- a/tests/components/nam/snapshots/test_diagnostics.ambr +++ b/tests/components/nam/snapshots/test_diagnostics.ambr @@ -11,7 +11,7 @@ 'bmp280_temperature': 5.6, 'dht22_humidity': 46.2, 'dht22_temperature': 6.3, - 'ds18b20_temperature': None, + 'ds18b20_temperature': 12.6, 'heca_humidity': 50.0, 'heca_temperature': 8.0, 'mhz14a_carbon_dioxide': 865.0, diff --git a/tests/components/nam/snapshots/test_sensor.ambr b/tests/components/nam/snapshots/test_sensor.ambr index bbc655ecbb6..ea47998f3de 100644 --- a/tests/components/nam/snapshots/test_sensor.ambr +++ b/tests/components/nam/snapshots/test_sensor.ambr @@ -532,6 +532,60 @@ 'state': '6.3', }) # --- +# name: test_sensor[sensor.nettigo_air_monitor_ds18b20_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.nettigo_air_monitor_ds18b20_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'DS18B20 temperature', + 'platform': 'nam', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'ds18b20_temperature', + 'unique_id': 'aa:bb:cc:dd:ee:ff-ds18b20_temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.nettigo_air_monitor_ds18b20_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Nettigo Air Monitor DS18B20 temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.nettigo_air_monitor_ds18b20_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '12.6', + }) +# --- # name: test_sensor[sensor.nettigo_air_monitor_heca_humidity-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ From 375f48142c6949fb97e42cbd4a5683fc8c1ceb9c Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Sun, 2 Jun 2024 21:25:05 +0200 Subject: [PATCH 0117/1445] Fix handling undecoded mqtt sensor payloads (#118633) --- homeassistant/components/mqtt/sensor.py | 24 ++++++++++------- tests/components/mqtt/test_sensor.py | 36 +++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/mqtt/sensor.py b/homeassistant/components/mqtt/sensor.py index 12de26b2358..043bc9a5c0e 100644 --- a/homeassistant/components/mqtt/sensor.py +++ b/homeassistant/components/mqtt/sensor.py @@ -237,28 +237,32 @@ class MqttSensor(MqttEntity, RestoreSensor): payload = msg.payload if payload is PayloadSentinel.DEFAULT: return - new_value = str(payload) + if not isinstance(payload, str): + _LOGGER.warning( + "Invalid undecoded state message '%s' received from '%s'", + payload, + msg.topic, + ) + return if self._numeric_state_expected: - if new_value == "": + if payload == "": _LOGGER.debug("Ignore empty state from '%s'", msg.topic) - elif new_value == PAYLOAD_NONE: + elif payload == PAYLOAD_NONE: self._attr_native_value = None else: - self._attr_native_value = new_value + self._attr_native_value = payload return if self.device_class in { None, SensorDeviceClass.ENUM, - } and not check_state_too_long(_LOGGER, new_value, self.entity_id, msg): - self._attr_native_value = new_value + } and not check_state_too_long(_LOGGER, payload, self.entity_id, msg): + self._attr_native_value = payload return try: - if (payload_datetime := dt_util.parse_datetime(new_value)) is None: + if (payload_datetime := dt_util.parse_datetime(payload)) is None: raise ValueError except ValueError: - _LOGGER.warning( - "Invalid state message '%s' from '%s'", msg.payload, msg.topic - ) + _LOGGER.warning("Invalid state message '%s' from '%s'", payload, msg.topic) self._attr_native_value = None return if self.device_class == SensorDeviceClass.DATE: diff --git a/tests/components/mqtt/test_sensor.py b/tests/components/mqtt/test_sensor.py index b8270277161..bde85abf3fb 100644 --- a/tests/components/mqtt/test_sensor.py +++ b/tests/components/mqtt/test_sensor.py @@ -110,6 +110,42 @@ async def test_setting_sensor_value_via_mqtt_message( assert state.attributes.get("unit_of_measurement") == "fav unit" +@pytest.mark.parametrize( + "hass_config", + [ + { + mqtt.DOMAIN: { + sensor.DOMAIN: { + "name": "test", + "state_topic": "test-topic", + "unit_of_measurement": "%", + "device_class": "battery", + "encoding": "", + } + } + } + ], +) +async def test_handling_undecoded_sensor_value( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test the setting of the value via MQTT.""" + await mqtt_mock_entry() + + state = hass.states.get("sensor.test") + assert state.state == STATE_UNKNOWN + + async_fire_mqtt_message(hass, "test-topic", b"88") + state = hass.states.get("sensor.test") + assert state.state == STATE_UNKNOWN + assert ( + "Invalid undecoded state message 'b'88'' received from 'test-topic'" + in caplog.text + ) + + @pytest.mark.parametrize( "hass_config", [ From 746939c8cd660363c768a2388b829c48f387ccb9 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Sun, 2 Jun 2024 16:55:48 -0400 Subject: [PATCH 0118/1445] Bump ZHA dependencies (#118658) * Bump bellows to 0.39.0 * Do not create a backup if there is no active ZHA gateway object * Bump universal-silabs-flasher as well --- homeassistant/components/zha/backup.py | 8 +++++++- homeassistant/components/zha/manifest.json | 4 ++-- requirements_all.txt | 4 ++-- requirements_test_all.txt | 4 ++-- tests/components/zha/test_backup.py | 9 ++++++++- 5 files changed, 21 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/zha/backup.py b/homeassistant/components/zha/backup.py index 25d5a83b6a4..e31ae09eeb6 100644 --- a/homeassistant/components/zha/backup.py +++ b/homeassistant/components/zha/backup.py @@ -13,7 +13,13 @@ async def async_pre_backup(hass: HomeAssistant) -> None: """Perform operations before a backup starts.""" _LOGGER.debug("Performing coordinator backup") - zha_gateway = get_zha_gateway(hass) + try: + zha_gateway = get_zha_gateway(hass) + except ValueError: + # If ZHA config is in `configuration.yaml` and ZHA is not set up, do nothing + _LOGGER.warning("No ZHA gateway exists, skipping coordinator backup") + return + await zha_gateway.application_controller.backups.create_backup(load_devices=True) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 1a01ca88fd5..8caf296674c 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -21,7 +21,7 @@ "universal_silabs_flasher" ], "requirements": [ - "bellows==0.38.4", + "bellows==0.39.0", "pyserial==3.5", "zha-quirks==0.0.116", "zigpy-deconz==0.23.1", @@ -29,7 +29,7 @@ "zigpy-xbee==0.20.1", "zigpy-zigate==0.12.0", "zigpy-znp==0.12.1", - "universal-silabs-flasher==0.0.18", + "universal-silabs-flasher==0.0.20", "pyserial-asyncio-fast==0.11" ], "usb": [ diff --git a/requirements_all.txt b/requirements_all.txt index 7bb21369fbc..a0c17559eec 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -547,7 +547,7 @@ beautifulsoup4==4.12.3 # beewi-smartclim==0.0.10 # homeassistant.components.zha -bellows==0.38.4 +bellows==0.39.0 # homeassistant.components.bmw_connected_drive bimmer-connected[china]==0.15.3 @@ -2794,7 +2794,7 @@ unifi_ap==0.0.1 unifiled==0.11 # homeassistant.components.zha -universal-silabs-flasher==0.0.18 +universal-silabs-flasher==0.0.20 # homeassistant.components.upb upb-lib==0.5.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4ce47d31c7a..037534405b5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -472,7 +472,7 @@ base36==0.1.1 beautifulsoup4==4.12.3 # homeassistant.components.zha -bellows==0.38.4 +bellows==0.39.0 # homeassistant.components.bmw_connected_drive bimmer-connected[china]==0.15.3 @@ -2162,7 +2162,7 @@ ultraheat-api==0.5.7 unifi-discovery==1.1.8 # homeassistant.components.zha -universal-silabs-flasher==0.0.18 +universal-silabs-flasher==0.0.20 # homeassistant.components.upb upb-lib==0.5.6 diff --git a/tests/components/zha/test_backup.py b/tests/components/zha/test_backup.py index 9cf88df1707..dc6c5dc29cb 100644 --- a/tests/components/zha/test_backup.py +++ b/tests/components/zha/test_backup.py @@ -1,6 +1,6 @@ """Unit tests for ZHA backup platform.""" -from unittest.mock import AsyncMock +from unittest.mock import AsyncMock, patch from zigpy.application import ControllerApplication @@ -22,6 +22,13 @@ async def test_pre_backup( ) +@patch("homeassistant.components.zha.backup.get_zha_gateway", side_effect=ValueError()) +async def test_pre_backup_no_gateway(hass: HomeAssistant, setup_zha) -> None: + """Test graceful backup failure when no gateway exists.""" + await setup_zha() + await async_pre_backup(hass) + + async def test_post_backup(hass: HomeAssistant, setup_zha) -> None: """Test no-op `async_post_backup`.""" await setup_zha() From dd1d21c77a915e288b3e06f8182b3557acb59590 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Mon, 3 Jun 2024 02:41:16 +0200 Subject: [PATCH 0119/1445] Fix entity state dispatching for Tag entities (#118662) --- homeassistant/components/tag/__init__.py | 4 ++-- tests/components/tag/__init__.py | 2 ++ tests/components/tag/test_init.py | 22 +++++++++++++++++----- 3 files changed, 21 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/tag/__init__.py b/homeassistant/components/tag/__init__.py index b7c9660ed93..afea86baa93 100644 --- a/homeassistant/components/tag/__init__.py +++ b/homeassistant/components/tag/__init__.py @@ -267,7 +267,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # When tags are changed or updated in storage async_dispatcher_send( hass, - SIGNAL_TAG_CHANGED, + f"{SIGNAL_TAG_CHANGED}-{updated_config[TAG_ID]}", updated_config.get(DEVICE_ID), updated_config.get(LAST_SCANNED), ) @@ -414,7 +414,7 @@ class TagEntity(Entity): self.async_on_remove( async_dispatcher_connect( self.hass, - SIGNAL_TAG_CHANGED, + f"{SIGNAL_TAG_CHANGED}-{self._tag_id}", self.async_handle_event, ) ) diff --git a/tests/components/tag/__init__.py b/tests/components/tag/__init__.py index 66b23073d3e..5c701af5d0a 100644 --- a/tests/components/tag/__init__.py +++ b/tests/components/tag/__init__.py @@ -1,5 +1,7 @@ """Tests for the Tag integration.""" TEST_TAG_ID = "test tag id" +TEST_TAG_ID_2 = "test tag id 2" TEST_TAG_NAME = "test tag name" +TEST_TAG_NAME_2 = "test tag name 2" TEST_DEVICE_ID = "device id" diff --git a/tests/components/tag/test_init.py b/tests/components/tag/test_init.py index 4767cc40fdf..db7e9d5dbc7 100644 --- a/tests/components/tag/test_init.py +++ b/tests/components/tag/test_init.py @@ -14,7 +14,7 @@ from homeassistant.helpers import collection, entity_registry as er from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util -from . import TEST_DEVICE_ID, TEST_TAG_ID, TEST_TAG_NAME +from . import TEST_DEVICE_ID, TEST_TAG_ID, TEST_TAG_ID_2, TEST_TAG_NAME, TEST_TAG_NAME_2 from tests.common import async_fire_time_changed from tests.typing import WebSocketGenerator @@ -35,7 +35,11 @@ def storage_setup(hass: HomeAssistant, hass_storage: dict[str, Any]): { "id": TEST_TAG_ID, "tag_id": TEST_TAG_ID, - } + }, + { + "id": TEST_TAG_ID_2, + "tag_id": TEST_TAG_ID_2, + }, ] }, } @@ -43,6 +47,7 @@ def storage_setup(hass: HomeAssistant, hass_storage: dict[str, Any]): hass_storage[DOMAIN] = items entity_registry = er.async_get(hass) _create_entry(entity_registry, TEST_TAG_ID, TEST_TAG_NAME) + _create_entry(entity_registry, TEST_TAG_ID_2, TEST_TAG_NAME_2) config = {DOMAIN: {}} return await async_setup_component(hass, DOMAIN, config) @@ -132,7 +137,8 @@ async def test_ws_list( resp = await client.receive_json() assert resp["success"] assert resp["result"] == [ - {"id": TEST_TAG_ID, "name": "test tag name", "tag_id": TEST_TAG_ID} + {"id": TEST_TAG_ID, "name": "test tag name", "tag_id": TEST_TAG_ID}, + {"id": TEST_TAG_ID_2, "name": "test tag name 2", "tag_id": TEST_TAG_ID_2}, ] @@ -176,7 +182,8 @@ async def test_tag_scanned( result = {item["id"]: item for item in resp["result"]} assert resp["result"] == [ - {"id": TEST_TAG_ID, "name": "test tag name", "tag_id": TEST_TAG_ID} + {"id": TEST_TAG_ID, "name": "test tag name", "tag_id": TEST_TAG_ID}, + {"id": TEST_TAG_ID_2, "name": "test tag name 2", "tag_id": TEST_TAG_ID_2}, ] now = dt_util.utcnow() @@ -189,9 +196,10 @@ async def test_tag_scanned( result = {item["id"]: item for item in resp["result"]} - assert len(result) == 2 + assert len(result) == 3 assert resp["result"] == [ {"id": TEST_TAG_ID, "name": "test tag name", "tag_id": TEST_TAG_ID}, + {"id": TEST_TAG_ID_2, "name": "test tag name 2", "tag_id": TEST_TAG_ID_2}, { "device_id": "some_scanner", "id": "new tag", @@ -257,6 +265,10 @@ async def test_entity( "friendly_name": "test tag name", } + entity = hass.states.get("tag.test_tag_name_2") + assert entity + assert entity.state == STATE_UNKNOWN + async def test_entity_created_and_removed( caplog: pytest.LogCaptureFixture, From 6a8a975fae19580d5f143928df214544e2e50d46 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Mon, 3 Jun 2024 03:04:35 +0200 Subject: [PATCH 0120/1445] Remove config flow import from fastdotcom (#118665) --- .../components/fastdotcom/__init__.py | 29 ++----------------- .../components/fastdotcom/config_flow.py | 23 --------------- .../components/fastdotcom/test_config_flow.py | 17 ----------- tests/components/fastdotcom/test_init.py | 18 ------------ 4 files changed, 3 insertions(+), 84 deletions(-) diff --git a/homeassistant/components/fastdotcom/__init__.py b/homeassistant/components/fastdotcom/__init__.py index 12bd355b82b..4074e9a479d 100644 --- a/homeassistant/components/fastdotcom/__init__.py +++ b/homeassistant/components/fastdotcom/__init__.py @@ -4,46 +4,23 @@ from __future__ import annotations import logging -import voluptuous as vol - -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import CONF_SCAN_INTERVAL +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.start import async_at_started from homeassistant.helpers.typing import ConfigType -from .const import CONF_MANUAL, DEFAULT_INTERVAL, DOMAIN, PLATFORMS +from .const import DOMAIN, PLATFORMS from .coordinator import FastdotcomDataUpdateCoordinator from .services import async_setup_services _LOGGER = logging.getLogger(__name__) -CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - { - vol.Optional(CONF_SCAN_INTERVAL, default=DEFAULT_INTERVAL): vol.All( - cv.time_period, cv.positive_timedelta - ), - vol.Optional(CONF_MANUAL, default=False): cv.boolean, - } - ) - }, - extra=vol.ALLOW_EXTRA, -) +CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN) async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Fastdotcom component.""" - if DOMAIN in config: - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data=config[DOMAIN], - ) - ) async_setup_services(hass) return True diff --git a/homeassistant/components/fastdotcom/config_flow.py b/homeassistant/components/fastdotcom/config_flow.py index 36b6f81ae5b..b84c30cf58d 100644 --- a/homeassistant/components/fastdotcom/config_flow.py +++ b/homeassistant/components/fastdotcom/config_flow.py @@ -5,8 +5,6 @@ from __future__ import annotations from typing import Any from homeassistant.config_entries import ConfigFlow, ConfigFlowResult -from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN -from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from .const import DEFAULT_NAME, DOMAIN @@ -24,24 +22,3 @@ class FastdotcomConfigFlow(ConfigFlow, domain=DOMAIN): return self.async_create_entry(title=DEFAULT_NAME, data={}) return self.async_show_form(step_id="user") - - async def async_step_import( - self, user_input: dict[str, Any] | None = None - ) -> ConfigFlowResult: - """Handle a flow initiated by configuration file.""" - async_create_issue( - self.hass, - HOMEASSISTANT_DOMAIN, - f"deprecated_yaml_{DOMAIN}", - breaks_in_ha_version="2024.6.0", - is_fixable=False, - issue_domain=DOMAIN, - severity=IssueSeverity.WARNING, - translation_key="deprecated_yaml", - translation_placeholders={ - "domain": DOMAIN, - "integration_title": "Fast.com", - }, - ) - - return await self.async_step_user(user_input) diff --git a/tests/components/fastdotcom/test_config_flow.py b/tests/components/fastdotcom/test_config_flow.py index db28aaec703..88dda3a4aae 100644 --- a/tests/components/fastdotcom/test_config_flow.py +++ b/tests/components/fastdotcom/test_config_flow.py @@ -4,7 +4,6 @@ from unittest.mock import patch import pytest -from homeassistant import config_entries from homeassistant.components.fastdotcom.const import DOMAIN from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER from homeassistant.core import HomeAssistant @@ -54,19 +53,3 @@ async def test_single_instance_allowed( assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" - - -async def test_import_flow_success(hass: HomeAssistant) -> None: - """Test import flow.""" - with patch("homeassistant.components.fastdotcom.coordinator.fast_com"): - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={}, - ) - await hass.async_block_till_done() - - assert result["type"] is FlowResultType.CREATE_ENTRY - assert result["title"] == "Fast.com" - assert result["data"] == {} - assert result["options"] == {} diff --git a/tests/components/fastdotcom/test_init.py b/tests/components/fastdotcom/test_init.py index c17b455057b..b1be0b53d34 100644 --- a/tests/components/fastdotcom/test_init.py +++ b/tests/components/fastdotcom/test_init.py @@ -11,7 +11,6 @@ from homeassistant.config_entries import ConfigEntryState from homeassistant.const import EVENT_HOMEASSISTANT_STARTED, STATE_UNKNOWN from homeassistant.core import CoreState, HomeAssistant from homeassistant.helpers import issue_registry as ir -from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry @@ -37,23 +36,6 @@ async def test_unload_entry(hass: HomeAssistant) -> None: assert config_entry.state is ConfigEntryState.NOT_LOADED -async def test_from_import(hass: HomeAssistant) -> None: - """Test imported entry.""" - with patch( - "homeassistant.components.fastdotcom.coordinator.fast_com", return_value=5.0 - ): - await async_setup_component( - hass, - DOMAIN, - {"fastdotcom": {}}, - ) - await hass.async_block_till_done() - - state = hass.states.get("sensor.fast_com_download") - assert state is not None - assert state.state == "5.0" - - async def test_delayed_speedtest_during_startup( hass: HomeAssistant, freezer: FrozenDateTimeFactory ) -> None: From 78e5f9578c3d007870956ee98c862715e53e7b25 Mon Sep 17 00:00:00 2001 From: Thomas55555 <59625598+Thomas55555@users.noreply.github.com> Date: Mon, 3 Jun 2024 05:13:02 +0200 Subject: [PATCH 0121/1445] Clean up Husqvarna Automower number platform (#118641) Address late review and remove unneeded loop in Husqvarna Automower --- .../components/husqvarna_automower/number.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/husqvarna_automower/number.py b/homeassistant/components/husqvarna_automower/number.py index 5e4ba48c230..72c1d360da9 100644 --- a/homeassistant/components/husqvarna_automower/number.py +++ b/homeassistant/components/husqvarna_automower/number.py @@ -129,12 +129,11 @@ async def async_setup_entry( for work_area_id in _work_areas ) async_remove_entities(hass, coordinator, entry, mower_id) - entities.extend( - AutomowerNumberEntity(mower_id, coordinator, description) - for mower_id in coordinator.data - for description in NUMBER_TYPES - if description.exists_fn(coordinator.data[mower_id]) - ) + entities.extend( + AutomowerNumberEntity(mower_id, coordinator, description) + for description in NUMBER_TYPES + if description.exists_fn(coordinator.data[mower_id]) + ) async_add_entities(entities) @@ -185,7 +184,6 @@ class AutomowerWorkAreaNumberEntity(AutomowerControlEntity, NumberEntity): ) -> None: """Set up AutomowerNumberEntity.""" super().__init__(mower_id, coordinator) - self.coordinator = coordinator self.entity_description = description self.work_area_id = work_area_id self._attr_unique_id = f"{mower_id}_{work_area_id}_{description.key}" From c23ec96174d53c7452bf12ed32d58c944de293a0 Mon Sep 17 00:00:00 2001 From: Marlon Date: Mon, 3 Jun 2024 07:28:13 +0200 Subject: [PATCH 0122/1445] Add BaseEntity for apsystems integration (#117514) * Add BaseEntity for apsystems integration * Exclude entity.py from apsystems from coverage * Remove api from BaseEntity from apsystems as it is not yet used * Split BaseEntity and BaseCoordinatorEntity in apsystems integration * Clean up of asserting unique_id everywhere in apsystems integration * Remove BaseCoordinatorEntity from apsystems * Remove double type declaration originating from merge in apsystems --- .coveragerc | 1 + .../components/apsystems/__init__.py | 20 ++++++++++-- homeassistant/components/apsystems/entity.py | 27 ++++++++++++++++ homeassistant/components/apsystems/sensor.py | 31 ++++++++----------- 4 files changed, 58 insertions(+), 21 deletions(-) create mode 100644 homeassistant/components/apsystems/entity.py diff --git a/.coveragerc b/.coveragerc index 4f839ffccdd..625057e9900 100644 --- a/.coveragerc +++ b/.coveragerc @@ -90,6 +90,7 @@ omit = homeassistant/components/aprilaire/entity.py homeassistant/components/apsystems/__init__.py homeassistant/components/apsystems/coordinator.py + homeassistant/components/apsystems/entity.py homeassistant/components/apsystems/sensor.py homeassistant/components/aqualogic/* homeassistant/components/aquostv/media_player.py diff --git a/homeassistant/components/apsystems/__init__.py b/homeassistant/components/apsystems/__init__.py index 1a103244d5b..0231d2975d8 100644 --- a/homeassistant/components/apsystems/__init__.py +++ b/homeassistant/components/apsystems/__init__.py @@ -2,6 +2,8 @@ from __future__ import annotations +from dataclasses import dataclass + from APsystemsEZ1 import APsystemsEZ1M from homeassistant.config_entries import ConfigEntry @@ -12,15 +14,27 @@ from .coordinator import ApSystemsDataCoordinator PLATFORMS: list[Platform] = [Platform.SENSOR] -type ApsystemsConfigEntry = ConfigEntry[ApSystemsDataCoordinator] + +@dataclass +class ApSystemsData: + """Store runtime data.""" + + coordinator: ApSystemsDataCoordinator + device_id: str -async def async_setup_entry(hass: HomeAssistant, entry: ApsystemsConfigEntry) -> bool: +type ApSystemsConfigEntry = ConfigEntry[ApSystemsData] + + +async def async_setup_entry(hass: HomeAssistant, entry: ApSystemsConfigEntry) -> bool: """Set up this integration using UI.""" api = APsystemsEZ1M(ip_address=entry.data[CONF_IP_ADDRESS], timeout=8) coordinator = ApSystemsDataCoordinator(hass, api) await coordinator.async_config_entry_first_refresh() - entry.runtime_data = coordinator + assert entry.unique_id + entry.runtime_data = ApSystemsData( + coordinator=coordinator, device_id=entry.unique_id + ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/apsystems/entity.py b/homeassistant/components/apsystems/entity.py new file mode 100644 index 00000000000..519f4fffb61 --- /dev/null +++ b/homeassistant/components/apsystems/entity.py @@ -0,0 +1,27 @@ +"""APsystems base entity.""" + +from __future__ import annotations + +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity import Entity + +from . import ApSystemsData +from .const import DOMAIN + + +class ApSystemsEntity(Entity): + """Defines a base APsystems entity.""" + + _attr_has_entity_name = True + + def __init__( + self, + data: ApSystemsData, + ) -> None: + """Initialize the APsystems entity.""" + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, data.device_id)}, + serial_number=data.device_id, + manufacturer="APsystems", + model="EZ1-M", + ) diff --git a/homeassistant/components/apsystems/sensor.py b/homeassistant/components/apsystems/sensor.py index 5321498d1b6..fdfe7d0f0b7 100644 --- a/homeassistant/components/apsystems/sensor.py +++ b/homeassistant/components/apsystems/sensor.py @@ -14,16 +14,15 @@ from homeassistant.components.sensor import ( SensorStateClass, StateType, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import UnitOfEnergy, UnitOfPower from homeassistant.core import HomeAssistant -from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import DiscoveryInfoType from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import DOMAIN +from . import ApSystemsConfigEntry, ApSystemsData from .coordinator import ApSystemsDataCoordinator +from .entity import ApSystemsEntity @dataclass(frozen=True, kw_only=True) @@ -111,22 +110,24 @@ SENSORS: tuple[ApsystemsLocalApiSensorDescription, ...] = ( async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: ApSystemsConfigEntry, add_entities: AddEntitiesCallback, discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the sensor platform.""" config = config_entry.runtime_data - device_id = config_entry.unique_id - assert device_id add_entities( - ApSystemsSensorWithDescription(config, desc, device_id) for desc in SENSORS + ApSystemsSensorWithDescription( + data=config, + entity_description=desc, + ) + for desc in SENSORS ) class ApSystemsSensorWithDescription( - CoordinatorEntity[ApSystemsDataCoordinator], SensorEntity + CoordinatorEntity[ApSystemsDataCoordinator], ApSystemsEntity, SensorEntity ): """Base sensor to be used with description.""" @@ -134,20 +135,14 @@ class ApSystemsSensorWithDescription( def __init__( self, - coordinator: ApSystemsDataCoordinator, + data: ApSystemsData, entity_description: ApsystemsLocalApiSensorDescription, - device_id: str, ) -> None: """Initialize the sensor.""" - super().__init__(coordinator) + super().__init__(data.coordinator) + ApSystemsEntity.__init__(self, data) self.entity_description = entity_description - self._attr_unique_id = f"{device_id}_{entity_description.key}" - self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, device_id)}, - serial_number=device_id, - manufacturer="APsystems", - model="EZ1-M", - ) + self._attr_unique_id = f"{data.device_id}_{entity_description.key}" @property def native_value(self) -> StateType: From 7c5a6602b3018de8954c45a182af52a5679d63e0 Mon Sep 17 00:00:00 2001 From: Richard Kroegel <42204099+rikroe@users.noreply.github.com> Date: Mon, 3 Jun 2024 07:48:48 +0200 Subject: [PATCH 0123/1445] Set lock state to unkown on BMW API error (#118559) * Revert to previous lock state on BMW API error * Set lock state to unkown on error and force refresh from API --------- Co-authored-by: Richard --- .../components/bmw_connected_drive/lock.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/bmw_connected_drive/lock.py b/homeassistant/components/bmw_connected_drive/lock.py index bbfadcef9db..e138f31ba24 100644 --- a/homeassistant/components/bmw_connected_drive/lock.py +++ b/homeassistant/components/bmw_connected_drive/lock.py @@ -65,11 +65,13 @@ class BMWLock(BMWBaseEntity, LockEntity): try: await self.vehicle.remote_services.trigger_remote_door_lock() except MyBMWAPIError as ex: - self._attr_is_locked = False + # Set the state to unknown if the command fails + self._attr_is_locked = None self.async_write_ha_state() raise HomeAssistantError(ex) from ex - - self.coordinator.async_update_listeners() + finally: + # Always update the listeners to get the latest state + self.coordinator.async_update_listeners() async def async_unlock(self, **kwargs: Any) -> None: """Unlock the car.""" @@ -83,11 +85,13 @@ class BMWLock(BMWBaseEntity, LockEntity): try: await self.vehicle.remote_services.trigger_remote_door_unlock() except MyBMWAPIError as ex: - self._attr_is_locked = True + # Set the state to unknown if the command fails + self._attr_is_locked = None self.async_write_ha_state() raise HomeAssistantError(ex) from ex - - self.coordinator.async_update_listeners() + finally: + # Always update the listeners to get the latest state + self.coordinator.async_update_listeners() @callback def _handle_coordinator_update(self) -> None: From bb259b607fd963eaed3b916256d86f09810e1a54 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Mon, 3 Jun 2024 08:32:05 +0200 Subject: [PATCH 0124/1445] Refactor incomfort platform attributes (#118667) * Refector incomfort platform attributes * Initialize static entity properties as class level --- .../components/incomfort/__init__.py | 19 ---------- .../components/incomfort/binary_sensor.py | 21 ++++++----- homeassistant/components/incomfort/climate.py | 34 +++++++++--------- homeassistant/components/incomfort/sensor.py | 16 +++++---- .../components/incomfort/water_heater.py | 35 ++++++------------- 5 files changed, 46 insertions(+), 79 deletions(-) diff --git a/homeassistant/components/incomfort/__init__.py b/homeassistant/components/incomfort/__init__.py index 3311bda23ee..72453bb5290 100644 --- a/homeassistant/components/incomfort/__init__.py +++ b/homeassistant/components/incomfort/__init__.py @@ -73,25 +73,6 @@ async def async_setup(hass: HomeAssistant, hass_config: ConfigType) -> bool: class IncomfortEntity(Entity): """Base class for all InComfort entities.""" - def __init__(self) -> None: - """Initialize the class.""" - self._name: str | None = None - self._unique_id: str | None = None - - @property - def unique_id(self) -> str | None: - """Return a unique ID.""" - return self._unique_id - - @property - def name(self) -> str | None: - """Return the name of the sensor.""" - return self._name - - -class IncomfortChild(IncomfortEntity): - """Base class for all InComfort entities (excluding the boiler).""" - _attr_should_poll = False async def async_added_to_hass(self) -> None: diff --git a/homeassistant/components/incomfort/binary_sensor.py b/homeassistant/components/incomfort/binary_sensor.py index 59096038d6c..04c0c17ba2a 100644 --- a/homeassistant/components/incomfort/binary_sensor.py +++ b/homeassistant/components/incomfort/binary_sensor.py @@ -4,15 +4,14 @@ from __future__ import annotations from typing import Any -from homeassistant.components.binary_sensor import ( - DOMAIN as BINARY_SENSOR_DOMAIN, - BinarySensorEntity, -) +from incomfortclient import Gateway as InComfortGateway, Heater as InComfortHeater + +from homeassistant.components.binary_sensor import BinarySensorEntity from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import DOMAIN, IncomfortChild +from . import DOMAIN, IncomfortEntity async def async_setup_platform( @@ -31,20 +30,20 @@ async def async_setup_platform( async_add_entities([IncomfortFailed(client, h) for h in heaters]) -class IncomfortFailed(IncomfortChild, BinarySensorEntity): +class IncomfortFailed(IncomfortEntity, BinarySensorEntity): """Representation of an InComfort Failed sensor.""" - def __init__(self, client, heater) -> None: + _attr_name = "Fault" + + def __init__(self, client: InComfortGateway, heater: InComfortHeater) -> None: """Initialize the binary sensor.""" super().__init__() - self._unique_id = f"{heater.serial_no}_failed" - self.entity_id = f"{BINARY_SENSOR_DOMAIN}.{DOMAIN}_failed" - self._name = "Boiler Fault" - self._client = client self._heater = heater + self._attr_unique_id = f"{heater.serial_no}_failed" + @property def is_on(self) -> bool: """Return the status of the sensor.""" diff --git a/homeassistant/components/incomfort/climate.py b/homeassistant/components/incomfort/climate.py index cc61e179aa4..32816900034 100644 --- a/homeassistant/components/incomfort/climate.py +++ b/homeassistant/components/incomfort/climate.py @@ -4,8 +4,13 @@ from __future__ import annotations from typing import Any +from incomfortclient import ( + Gateway as InComfortGateway, + Heater as InComfortHeater, + Room as InComfortRoom, +) + from homeassistant.components.climate import ( - DOMAIN as CLIMATE_DOMAIN, ClimateEntity, ClimateEntityFeature, HVACMode, @@ -15,7 +20,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import DOMAIN, IncomfortChild +from . import DOMAIN, IncomfortEntity async def async_setup_platform( @@ -36,26 +41,29 @@ async def async_setup_platform( ) -class InComfortClimate(IncomfortChild, ClimateEntity): +class InComfortClimate(IncomfortEntity, ClimateEntity): """Representation of an InComfort/InTouch climate device.""" + _attr_min_temp = 5.0 + _attr_max_temp = 30.0 _attr_hvac_mode = HVACMode.HEAT _attr_hvac_modes = [HVACMode.HEAT] _attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE _attr_temperature_unit = UnitOfTemperature.CELSIUS _enable_turn_on_off_backwards_compatibility = False - def __init__(self, client, heater, room) -> None: + def __init__( + self, client: InComfortGateway, heater: InComfortHeater, room: InComfortRoom + ) -> None: """Initialize the climate device.""" super().__init__() - self._unique_id = f"{heater.serial_no}_{room.room_no}" - self.entity_id = f"{CLIMATE_DOMAIN}.{DOMAIN}_{room.room_no}" - self._name = f"Thermostat {room.room_no}" - self._client = client self._room = room + self._attr_unique_id = f"{heater.serial_no}_{room.room_no}" + self._attr_name = f"Thermostat {room.room_no}" + @property def extra_state_attributes(self) -> dict[str, Any]: """Return the device state attributes.""" @@ -71,16 +79,6 @@ class InComfortClimate(IncomfortChild, ClimateEntity): """Return the temperature we try to reach.""" return self._room.setpoint - @property - def min_temp(self) -> float: - """Return max valid temperature that can be set.""" - return 5.0 - - @property - def max_temp(self) -> float: - """Return max valid temperature that can be set.""" - return 30.0 - async def async_set_temperature(self, **kwargs: Any) -> None: """Set a new target temperature for this zone.""" temperature = kwargs.get(ATTR_TEMPERATURE) diff --git a/homeassistant/components/incomfort/sensor.py b/homeassistant/components/incomfort/sensor.py index 9106afacb26..e75fbee2676 100644 --- a/homeassistant/components/incomfort/sensor.py +++ b/homeassistant/components/incomfort/sensor.py @@ -5,8 +5,9 @@ from __future__ import annotations from dataclasses import dataclass from typing import Any +from incomfortclient import Gateway as InComfortGateway, Heater as InComfortHeater + from homeassistant.components.sensor import ( - DOMAIN as SENSOR_DOMAIN, SensorDeviceClass, SensorEntity, SensorEntityDescription, @@ -17,7 +18,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util import slugify -from . import DOMAIN, IncomfortChild +from . import DOMAIN, IncomfortEntity INCOMFORT_HEATER_TEMP = "CV Temp" INCOMFORT_PRESSURE = "CV Pressure" @@ -80,13 +81,16 @@ async def async_setup_platform( async_add_entities(entities) -class IncomfortSensor(IncomfortChild, SensorEntity): +class IncomfortSensor(IncomfortEntity, SensorEntity): """Representation of an InComfort/InTouch sensor device.""" entity_description: IncomfortSensorEntityDescription def __init__( - self, client, heater, description: IncomfortSensorEntityDescription + self, + client: InComfortGateway, + heater: InComfortHeater, + description: IncomfortSensorEntityDescription, ) -> None: """Initialize the sensor.""" super().__init__() @@ -95,9 +99,7 @@ class IncomfortSensor(IncomfortChild, SensorEntity): self._client = client self._heater = heater - self._unique_id = f"{heater.serial_no}_{slugify(description.name)}" - self.entity_id = f"{SENSOR_DOMAIN}.{DOMAIN}_{slugify(description.name)}" - self._name = f"Boiler {description.name}" + self._attr_unique_id = f"{heater.serial_no}_{slugify(description.name)}" @property def native_value(self) -> str | None: diff --git a/homeassistant/components/incomfort/water_heater.py b/homeassistant/components/incomfort/water_heater.py index 2cd7c84a666..883d8555832 100644 --- a/homeassistant/components/incomfort/water_heater.py +++ b/homeassistant/components/incomfort/water_heater.py @@ -6,11 +6,9 @@ import logging from typing import Any from aiohttp import ClientResponseError +from incomfortclient import Gateway as InComfortGateway, Heater as InComfortHeater -from homeassistant.components.water_heater import ( - DOMAIN as WATER_HEATER_DOMAIN, - WaterHeaterEntity, -) +from homeassistant.components.water_heater import WaterHeaterEntity from homeassistant.const import UnitOfTemperature from homeassistant.core import HomeAssistant from homeassistant.helpers.dispatcher import async_dispatcher_send @@ -43,17 +41,21 @@ async def async_setup_platform( class IncomfortWaterHeater(IncomfortEntity, WaterHeaterEntity): """Representation of an InComfort/Intouch water_heater device.""" - def __init__(self, client, heater) -> None: + _attr_min_temp = 30.0 + _attr_max_temp = 80.0 + _attr_name = "Boiler" + _attr_should_poll = True + _attr_temperature_unit = UnitOfTemperature.CELSIUS + + def __init__(self, client: InComfortGateway, heater: InComfortHeater) -> None: """Initialize the water_heater device.""" super().__init__() - self._unique_id = f"{heater.serial_no}" - self.entity_id = f"{WATER_HEATER_DOMAIN}.{DOMAIN}" - self._name = "Boiler" - self._client = client self._heater = heater + self._attr_unique_id = heater.serial_no + @property def icon(self) -> str: """Return the icon of the water_heater device.""" @@ -73,21 +75,6 @@ class IncomfortWaterHeater(IncomfortEntity, WaterHeaterEntity): return self._heater.heater_temp return max(self._heater.heater_temp, self._heater.tap_temp) - @property - def min_temp(self) -> float: - """Return min valid temperature that can be set.""" - return 30.0 - - @property - def max_temp(self) -> float: - """Return max valid temperature that can be set.""" - return 80.0 - - @property - def temperature_unit(self) -> str: - """Return the unit of measurement.""" - return UnitOfTemperature.CELSIUS - @property def current_operation(self) -> str: """Return the current operation mode.""" From 9a5706fa30bad3c209c7704f8f220655360b8dc6 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 3 Jun 2024 09:28:54 +0200 Subject: [PATCH 0125/1445] Add type hints for pytest.LogCaptureFixture in test fixtures (#118687) --- tests/components/blebox/test_button.py | 2 +- tests/components/modbus/conftest.py | 9 +++++++-- tests/components/modbus/test_init.py | 8 +++++--- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/tests/components/blebox/test_button.py b/tests/components/blebox/test_button.py index fe596c41e33..03d8b22f149 100644 --- a/tests/components/blebox/test_button.py +++ b/tests/components/blebox/test_button.py @@ -21,7 +21,7 @@ query_icon_matching = [ @pytest.fixture(name="tvliftbox") -def tv_lift_box_fixture(caplog): +def tv_lift_box_fixture(caplog: pytest.LogCaptureFixture): """Return simple button entity mock.""" caplog.set_level(logging.ERROR) diff --git a/tests/components/modbus/conftest.py b/tests/components/modbus/conftest.py index 1253a856bbf..153ccb2b888 100644 --- a/tests/components/modbus/conftest.py +++ b/tests/components/modbus/conftest.py @@ -117,7 +117,12 @@ def mock_pymodbus_fixture(do_exception, register_words): @pytest.fixture(name="mock_modbus") async def mock_modbus_fixture( - hass, caplog, check_config_loaded, config_addon, do_config, mock_pymodbus + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, + check_config_loaded, + config_addon, + do_config, + mock_pymodbus, ): """Load integration modbus using mocked pymodbus.""" conf = copy.deepcopy(do_config) @@ -192,6 +197,6 @@ async def mock_modbus_ha_fixture(hass, mock_modbus): @pytest.fixture(name="caplog_setup_text") -async def caplog_setup_text_fixture(caplog): +async def caplog_setup_text_fixture(caplog: pytest.LogCaptureFixture) -> str: """Return setup log of integration.""" return caplog.text diff --git a/tests/components/modbus/test_init.py b/tests/components/modbus/test_init.py index 82c65576f02..920003ad0c9 100644 --- a/tests/components/modbus/test_init.py +++ b/tests/components/modbus/test_init.py @@ -136,7 +136,9 @@ from tests.common import async_fire_time_changed, get_fixture_path @pytest.fixture(name="mock_modbus_with_pymodbus") -async def mock_modbus_with_pymodbus_fixture(hass, caplog, do_config, mock_pymodbus): +async def mock_modbus_with_pymodbus_fixture( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture, do_config, mock_pymodbus +): """Load integration modbus using mocked pymodbus.""" caplog.clear() caplog.set_level(logging.ERROR) @@ -1361,12 +1363,12 @@ async def test_pb_service_write( @pytest.fixture(name="mock_modbus_read_pymodbus") async def mock_modbus_read_pymodbus_fixture( - hass, + hass: HomeAssistant, do_group, do_type, do_scan_interval, do_return, - caplog, + caplog: pytest.LogCaptureFixture, mock_pymodbus, freezer: FrozenDateTimeFactory, ): From 1db7c7946e0655c3c6dbb2081ea16aae3f373389 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 3 Jun 2024 09:29:15 +0200 Subject: [PATCH 0126/1445] Add type hints for MqttMockHAClient in test fixtures (#118683) --- tests/components/mqtt/test_trigger.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/components/mqtt/test_trigger.py b/tests/components/mqtt/test_trigger.py index a13ab001e30..2e0506a02ab 100644 --- a/tests/components/mqtt/test_trigger.py +++ b/tests/components/mqtt/test_trigger.py @@ -10,6 +10,7 @@ from homeassistant.core import HassJobType, HomeAssistant, ServiceCall from homeassistant.setup import async_setup_component from tests.common import async_fire_mqtt_message, async_mock_service, mock_component +from tests.typing import MqttMockHAClient, MqttMockHAClientGenerator @pytest.fixture(autouse=True, name="stub_blueprint_populate") @@ -24,7 +25,9 @@ def calls(hass: HomeAssistant) -> list[ServiceCall]: @pytest.fixture(autouse=True) -async def setup_comp(hass: HomeAssistant, mqtt_mock_entry): +async def setup_comp( + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator +) -> MqttMockHAClient: """Initialize components.""" mock_component(hass, "group") return await mqtt_mock_entry() From a87b422d3ea60cd13fcbceb1946c273470e0133d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Jun 2024 09:52:04 +0200 Subject: [PATCH 0127/1445] Bump github/codeql-action from 3.25.6 to 3.25.7 (#118680) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/codeql.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 437d8afe7ce..9bb5417ec7c 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -24,11 +24,11 @@ jobs: uses: actions/checkout@v4.1.6 - name: Initialize CodeQL - uses: github/codeql-action/init@v3.25.6 + uses: github/codeql-action/init@v3.25.7 with: languages: python - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3.25.6 + uses: github/codeql-action/analyze@v3.25.7 with: category: "/language:python" From 891f9c9578f684e5e8bb1d532f639a2e1641b41d Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 3 Jun 2024 09:58:02 +0200 Subject: [PATCH 0128/1445] Add error message to device registry helper (#118676) --- homeassistant/helpers/device_registry.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index cb336d1455b..962cd01bf00 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -820,7 +820,9 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]): ) if merge_identifiers is not UNDEFINED and new_identifiers is not UNDEFINED: - raise HomeAssistantError + raise HomeAssistantError( + "Cannot define both merge_identifiers and new_identifiers" + ) if isinstance(disabled_by, str) and not isinstance( disabled_by, DeviceEntryDisabler From 134088e1f6356fa92f9b36bf714272fd81b7d784 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 3 Jun 2024 03:11:24 -0500 Subject: [PATCH 0129/1445] Revert "Add websocket API to get list of recorded entities (#92640)" (#118644) Co-authored-by: Paulus Schoutsen --- .../components/recorder/websocket_api.py | 46 +----------- .../components/recorder/test_websocket_api.py | 71 +------------------ 2 files changed, 3 insertions(+), 114 deletions(-) diff --git a/homeassistant/components/recorder/websocket_api.py b/homeassistant/components/recorder/websocket_api.py index b0874d9ea2a..58c362df62e 100644 --- a/homeassistant/components/recorder/websocket_api.py +++ b/homeassistant/components/recorder/websocket_api.py @@ -3,7 +3,7 @@ from __future__ import annotations from datetime import datetime as dt -from typing import TYPE_CHECKING, Any, Literal, cast +from typing import Any, Literal, cast import voluptuous as vol @@ -44,11 +44,7 @@ from .statistics import ( statistics_during_period, validate_statistics, ) -from .util import PERIOD_SCHEMA, get_instance, resolve_period, session_scope - -if TYPE_CHECKING: - from .core import Recorder - +from .util import PERIOD_SCHEMA, get_instance, resolve_period UNIT_SCHEMA = vol.Schema( { @@ -85,7 +81,6 @@ def async_setup(hass: HomeAssistant) -> None: websocket_api.async_register_command(hass, ws_info) websocket_api.async_register_command(hass, ws_update_statistics_metadata) websocket_api.async_register_command(hass, ws_validate_statistics) - websocket_api.async_register_command(hass, ws_get_recorded_entities) def _ws_get_statistic_during_period( @@ -518,40 +513,3 @@ def ws_info( "thread_running": is_running, } connection.send_result(msg["id"], recorder_info) - - -def _get_recorded_entities( - hass: HomeAssistant, msg_id: int, instance: Recorder -) -> bytes: - """Get the list of entities being recorded.""" - with session_scope(hass=hass, read_only=True) as session: - return json_bytes( - messages.result_message( - msg_id, - { - "entity_ids": list( - instance.states_meta_manager.get_metadata_id_to_entity_id( - session - ).values() - ) - }, - ) - ) - - -@websocket_api.websocket_command( - { - vol.Required("type"): "recorder/recorded_entities", - } -) -@websocket_api.async_response -async def ws_get_recorded_entities( - hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any] -) -> None: - """Get the list of entities being recorded.""" - instance = get_instance(hass) - return connection.send_message( - await instance.async_add_executor_job( - _get_recorded_entities, hass, msg["id"], instance - ) - ) diff --git a/tests/components/recorder/test_websocket_api.py b/tests/components/recorder/test_websocket_api.py index 9cb06003415..9c8e0a9203a 100644 --- a/tests/components/recorder/test_websocket_api.py +++ b/tests/components/recorder/test_websocket_api.py @@ -23,7 +23,6 @@ from homeassistant.components.recorder.statistics import ( from homeassistant.components.recorder.util import session_scope from homeassistant.components.recorder.websocket_api import UNIT_SCHEMA from homeassistant.components.sensor import UNIT_CONVERTERS -from homeassistant.const import CONF_DOMAINS, CONF_EXCLUDE from homeassistant.core import HomeAssistant from homeassistant.helpers import recorder as recorder_helper from homeassistant.setup import async_setup_component @@ -39,7 +38,7 @@ from .common import ( ) from tests.common import async_fire_time_changed -from tests.typing import RecorderInstanceGenerator, WebSocketGenerator +from tests.typing import WebSocketGenerator DISTANCE_SENSOR_FT_ATTRIBUTES = { "device_class": "distance", @@ -133,13 +132,6 @@ VOLUME_SENSOR_M3_ATTRIBUTES_TOTAL = { } -@pytest.fixture -async def mock_recorder_before_hass( - async_setup_recorder_instance: RecorderInstanceGenerator, -) -> None: - """Set up recorder.""" - - def test_converters_align_with_sensor() -> None: """Ensure UNIT_SCHEMA is aligned with sensor UNIT_CONVERTERS.""" for converter in UNIT_CONVERTERS.values(): @@ -3185,64 +3177,3 @@ async def test_adjust_sum_statistics_errors( stats = statistics_during_period(hass, zero, period="hour") assert stats != previous_stats previous_stats = stats - - -async def test_recorder_recorded_entities_no_filter( - hass: HomeAssistant, - hass_ws_client: WebSocketGenerator, - async_setup_recorder_instance: RecorderInstanceGenerator, -) -> None: - """Test getting the list of recorded entities without a filter.""" - await async_setup_recorder_instance(hass, {recorder.CONF_COMMIT_INTERVAL: 0}) - client = await hass_ws_client() - - await client.send_json({"id": 1, "type": "recorder/recorded_entities"}) - response = await client.receive_json() - assert response["result"] == {"entity_ids": []} - assert response["id"] == 1 - assert response["success"] - assert response["type"] == "result" - - hass.states.async_set("sensor.test", 10) - await async_wait_recording_done(hass) - - await client.send_json({"id": 2, "type": "recorder/recorded_entities"}) - response = await client.receive_json() - assert response["result"] == {"entity_ids": ["sensor.test"]} - assert response["id"] == 2 - assert response["success"] - assert response["type"] == "result" - - -async def test_recorder_recorded_entities_with_filter( - hass: HomeAssistant, - hass_ws_client: WebSocketGenerator, - async_setup_recorder_instance: RecorderInstanceGenerator, -) -> None: - """Test getting the list of recorded entities with a filter.""" - await async_setup_recorder_instance( - hass, - { - recorder.CONF_COMMIT_INTERVAL: 0, - CONF_EXCLUDE: {CONF_DOMAINS: ["sensor"]}, - }, - ) - client = await hass_ws_client() - - await client.send_json({"id": 1, "type": "recorder/recorded_entities"}) - response = await client.receive_json() - assert response["result"] == {"entity_ids": []} - assert response["id"] == 1 - assert response["success"] - assert response["type"] == "result" - - hass.states.async_set("switch.test", 10) - hass.states.async_set("sensor.test", 10) - await async_wait_recording_done(hass) - - await client.send_json({"id": 2, "type": "recorder/recorded_entities"}) - response = await client.receive_json() - assert response["result"] == {"entity_ids": ["switch.test"]} - assert response["id"] == 2 - assert response["success"] - assert response["type"] == "result" From c93d42d59b477c807ec6a21867d66d63bc59b157 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 3 Jun 2024 10:12:58 +0200 Subject: [PATCH 0130/1445] Add type hints for FrozenDateTimeFactory in test fixtures (#118690) --- tests/components/energy/test_sensor.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/components/energy/test_sensor.py b/tests/components/energy/test_sensor.py index 4128a80c587..b9aca285829 100644 --- a/tests/components/energy/test_sensor.py +++ b/tests/components/energy/test_sensor.py @@ -4,6 +4,7 @@ import copy from datetime import timedelta from typing import Any +from freezegun.api import FrozenDateTimeFactory import pytest from homeassistant.components.energy import data @@ -47,7 +48,7 @@ async def setup_integration(recorder_mock: Recorder): @pytest.fixture(autouse=True) -def frozen_time(freezer): +def frozen_time(freezer: FrozenDateTimeFactory) -> FrozenDateTimeFactory: """Freeze clock for tests.""" freezer.move_to("2022-04-19 07:53:05") return freezer From 666fc2333a44d72048aaa1a3a514dcdf2cd16ed4 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 3 Jun 2024 10:13:22 +0200 Subject: [PATCH 0131/1445] Add type hints for AiohttpClientMocker in test fixtures (#118691) --- tests/components/hassio/test_addon_panel.py | 2 +- tests/components/hassio/test_binary_sensor.py | 2 +- tests/components/hassio/test_diagnostics.py | 3 ++- tests/components/hassio/test_init.py | 2 +- tests/components/hassio/test_update.py | 2 +- tests/components/hassio/test_websocket_api.py | 2 +- tests/components/onboarding/test_views.py | 13 ++++++++++--- 7 files changed, 17 insertions(+), 9 deletions(-) diff --git a/tests/components/hassio/test_addon_panel.py b/tests/components/hassio/test_addon_panel.py index 9b1735287c6..8436b3393b9 100644 --- a/tests/components/hassio/test_addon_panel.py +++ b/tests/components/hassio/test_addon_panel.py @@ -13,7 +13,7 @@ from tests.typing import ClientSessionGenerator @pytest.fixture(autouse=True) -def mock_all(aioclient_mock): +def mock_all(aioclient_mock: AiohttpClientMocker) -> None: """Mock all setup requests.""" aioclient_mock.post("http://127.0.0.1/homeassistant/options", json={"result": "ok"}) aioclient_mock.get("http://127.0.0.1/supervisor/ping", json={"result": "ok"}) diff --git a/tests/components/hassio/test_binary_sensor.py b/tests/components/hassio/test_binary_sensor.py index bbe498223d1..af72ea9d702 100644 --- a/tests/components/hassio/test_binary_sensor.py +++ b/tests/components/hassio/test_binary_sensor.py @@ -17,7 +17,7 @@ MOCK_ENVIRON = {"SUPERVISOR": "127.0.0.1", "SUPERVISOR_TOKEN": "abcdefgh"} @pytest.fixture(autouse=True) -def mock_all(aioclient_mock, request): +def mock_all(aioclient_mock: AiohttpClientMocker) -> None: """Mock all setup requests.""" aioclient_mock.post("http://127.0.0.1/homeassistant/options", json={"result": "ok"}) aioclient_mock.get("http://127.0.0.1/supervisor/ping", json={"result": "ok"}) diff --git a/tests/components/hassio/test_diagnostics.py b/tests/components/hassio/test_diagnostics.py index 83ddd0dbd33..0d648ba9bdb 100644 --- a/tests/components/hassio/test_diagnostics.py +++ b/tests/components/hassio/test_diagnostics.py @@ -11,13 +11,14 @@ from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry from tests.components.diagnostics import get_diagnostics_for_config_entry +from tests.test_util.aiohttp import AiohttpClientMocker from tests.typing import ClientSessionGenerator MOCK_ENVIRON = {"SUPERVISOR": "127.0.0.1", "SUPERVISOR_TOKEN": "abcdefgh"} @pytest.fixture(autouse=True) -def mock_all(aioclient_mock, request): +def mock_all(aioclient_mock: AiohttpClientMocker) -> None: """Mock all setup requests.""" aioclient_mock.post("http://127.0.0.1/homeassistant/options", json={"result": "ok"}) aioclient_mock.get("http://127.0.0.1/supervisor/ping", json={"result": "ok"}) diff --git a/tests/components/hassio/test_init.py b/tests/components/hassio/test_init.py index d4ec2d0149c..eddd4e5e04f 100644 --- a/tests/components/hassio/test_init.py +++ b/tests/components/hassio/test_init.py @@ -52,7 +52,7 @@ def os_info(extra_os_info): @pytest.fixture(autouse=True) -def mock_all(aioclient_mock, request, os_info): +def mock_all(aioclient_mock: AiohttpClientMocker, os_info) -> None: """Mock all setup requests.""" aioclient_mock.post("http://127.0.0.1/homeassistant/options", json={"result": "ok"}) aioclient_mock.get("http://127.0.0.1/supervisor/ping", json={"result": "ok"}) diff --git a/tests/components/hassio/test_update.py b/tests/components/hassio/test_update.py index e79e975a52f..9a047010cc3 100644 --- a/tests/components/hassio/test_update.py +++ b/tests/components/hassio/test_update.py @@ -21,7 +21,7 @@ MOCK_ENVIRON = {"SUPERVISOR": "127.0.0.1", "SUPERVISOR_TOKEN": "abcdefgh"} @pytest.fixture(autouse=True) -def mock_all(aioclient_mock, request): +def mock_all(aioclient_mock: AiohttpClientMocker) -> None: """Mock all setup requests.""" aioclient_mock.post("http://127.0.0.1/homeassistant/options", json={"result": "ok"}) aioclient_mock.get("http://127.0.0.1/supervisor/ping", json={"result": "ok"}) diff --git a/tests/components/hassio/test_websocket_api.py b/tests/components/hassio/test_websocket_api.py index 67252a0bc83..f3be391d9b7 100644 --- a/tests/components/hassio/test_websocket_api.py +++ b/tests/components/hassio/test_websocket_api.py @@ -23,7 +23,7 @@ from tests.typing import WebSocketGenerator @pytest.fixture(autouse=True) -def mock_all(aioclient_mock): +def mock_all(aioclient_mock: AiohttpClientMocker) -> None: """Mock all setup requests.""" aioclient_mock.post("http://127.0.0.1/homeassistant/options", json={"result": "ok"}) aioclient_mock.get("http://127.0.0.1/supervisor/ping", json={"result": "ok"}) diff --git a/tests/components/onboarding/test_views.py b/tests/components/onboarding/test_views.py index 45fa654e20f..a0bff5c280c 100644 --- a/tests/components/onboarding/test_views.py +++ b/tests/components/onboarding/test_views.py @@ -1,6 +1,7 @@ """Test the onboarding views.""" import asyncio +from collections.abc import AsyncGenerator from http import HTTPStatus import os from typing import Any @@ -35,7 +36,9 @@ def auth_active(hass): @pytest.fixture(name="rpi") -async def rpi_fixture(hass, aioclient_mock, mock_supervisor): +async def rpi_fixture( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, mock_supervisor +) -> None: """Mock core info with rpi.""" aioclient_mock.get( "http://127.0.0.1/core/info", @@ -49,7 +52,9 @@ async def rpi_fixture(hass, aioclient_mock, mock_supervisor): @pytest.fixture(name="no_rpi") -async def no_rpi_fixture(hass, aioclient_mock, mock_supervisor): +async def no_rpi_fixture( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, mock_supervisor +) -> None: """Mock core info with rpi.""" aioclient_mock.get( "http://127.0.0.1/core/info", @@ -63,7 +68,9 @@ async def no_rpi_fixture(hass, aioclient_mock, mock_supervisor): @pytest.fixture(name="mock_supervisor") -async def mock_supervisor_fixture(hass, aioclient_mock): +async def mock_supervisor_fixture( + aioclient_mock: AiohttpClientMocker, +) -> AsyncGenerator[None, None]: """Mock supervisor.""" aioclient_mock.post("http://127.0.0.1/homeassistant/options", json={"result": "ok"}) aioclient_mock.post("http://127.0.0.1/supervisor/options", json={"result": "ok"}) From 8772a59f5c05676ea7664076f9a9a3c3a59b83e8 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 3 Jun 2024 10:17:51 +0200 Subject: [PATCH 0132/1445] Add type hints for Recorder in test fixtures (#118685) --- tests/components/energy/test_validate.py | 6 +++++- tests/components/energy/test_websocket_api.py | 4 ++-- .../test_filters_with_entityfilter_schema_37.py | 5 ++++- .../recorder/test_migration_from_schema_32.py | 13 +++++++++++-- 4 files changed, 22 insertions(+), 6 deletions(-) diff --git a/tests/components/energy/test_validate.py b/tests/components/energy/test_validate.py index 7a328e77d76..d7f0485139f 100644 --- a/tests/components/energy/test_validate.py +++ b/tests/components/energy/test_validate.py @@ -5,6 +5,8 @@ from unittest.mock import patch import pytest from homeassistant.components.energy import async_get_manager, validate +from homeassistant.components.energy.data import EnergyManager +from homeassistant.components.recorder import Recorder from homeassistant.const import UnitOfEnergy from homeassistant.core import HomeAssistant from homeassistant.helpers.json import JSON_DUMP @@ -46,7 +48,9 @@ def mock_get_metadata(): @pytest.fixture(autouse=True) -async def mock_energy_manager(recorder_mock, hass): +async def mock_energy_manager( + recorder_mock: Recorder, hass: HomeAssistant +) -> EnergyManager: """Set up energy.""" assert await async_setup_component(hass, "energy", {"energy": {}}) manager = await async_get_manager(hass) diff --git a/tests/components/energy/test_websocket_api.py b/tests/components/energy/test_websocket_api.py index afb23e4e88a..959ec7d1687 100644 --- a/tests/components/energy/test_websocket_api.py +++ b/tests/components/energy/test_websocket_api.py @@ -21,13 +21,13 @@ from tests.typing import WebSocketGenerator @pytest.fixture(autouse=True) -async def setup_integration(recorder_mock, hass): +async def setup_integration(recorder_mock: Recorder, hass: HomeAssistant) -> None: """Set up the integration.""" assert await async_setup_component(hass, "energy", {}) @pytest.fixture -def mock_energy_platform(hass): +def mock_energy_platform(hass: HomeAssistant) -> None: """Mock an energy platform.""" hass.config.components.add("some_domain") mock_platform( diff --git a/tests/components/recorder/test_filters_with_entityfilter_schema_37.py b/tests/components/recorder/test_filters_with_entityfilter_schema_37.py index f5eec10f805..872f694925c 100644 --- a/tests/components/recorder/test_filters_with_entityfilter_schema_37.py +++ b/tests/components/recorder/test_filters_with_entityfilter_schema_37.py @@ -1,5 +1,6 @@ """The tests for the recorder filter matching the EntityFilter component.""" +from collections.abc import AsyncGenerator import json from unittest.mock import patch @@ -38,7 +39,9 @@ def db_schema_32(): @pytest.fixture(name="legacy_recorder_mock") -async def legacy_recorder_mock_fixture(recorder_mock): +async def legacy_recorder_mock_fixture( + recorder_mock: Recorder, +) -> AsyncGenerator[Recorder, None]: """Fixture for legacy recorder mock.""" with patch.object(recorder_mock.states_meta_manager, "active", False): yield recorder_mock diff --git a/tests/components/recorder/test_migration_from_schema_32.py b/tests/components/recorder/test_migration_from_schema_32.py index 646cd338949..13e321e5573 100644 --- a/tests/components/recorder/test_migration_from_schema_32.py +++ b/tests/components/recorder/test_migration_from_schema_32.py @@ -1,5 +1,6 @@ """The tests for the recorder filter matching the EntityFilter component.""" +from collections.abc import AsyncGenerator import datetime import importlib import sys @@ -14,7 +15,13 @@ from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import Session from homeassistant.components import recorder -from homeassistant.components.recorder import core, db_schema, migration, statistics +from homeassistant.components.recorder import ( + Recorder, + core, + db_schema, + migration, + statistics, +) from homeassistant.components.recorder.db_schema import ( Events, EventTypes, @@ -110,7 +117,9 @@ def db_schema_32(): @pytest.fixture(name="legacy_recorder_mock") -async def legacy_recorder_mock_fixture(recorder_mock): +async def legacy_recorder_mock_fixture( + recorder_mock: Recorder, +) -> AsyncGenerator[Recorder, None]: """Fixture for legacy recorder mock.""" with patch.object(recorder_mock.states_meta_manager, "active", False): yield recorder_mock From d5eebb202b2d198923b2f2f5a3ab655a6fc4acbb Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 3 Jun 2024 10:18:36 +0200 Subject: [PATCH 0133/1445] Remove unused fixture from elmax tests (#118684) --- tests/components/elmax/conftest.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/components/elmax/conftest.py b/tests/components/elmax/conftest.py index e69f52f4cad..2166e6476c7 100644 --- a/tests/components/elmax/conftest.py +++ b/tests/components/elmax/conftest.py @@ -1,7 +1,8 @@ """Configuration for Elmax tests.""" +from collections.abc import Generator import json -from unittest.mock import patch +from unittest.mock import AsyncMock, patch from elmax_api.constants import ( BASE_URL, @@ -29,7 +30,7 @@ MOCK_DIRECT_BASE_URI = ( @pytest.fixture(autouse=True) -def httpx_mock_cloud_fixture(requests_mock): +def httpx_mock_cloud_fixture() -> Generator[respx.MockRouter, None, None]: """Configure httpx fixture for cloud API communication.""" with respx.mock(base_url=BASE_URL, assert_all_called=False) as respx_mock: # Mock Login POST. @@ -56,7 +57,7 @@ def httpx_mock_cloud_fixture(requests_mock): @pytest.fixture(autouse=True) -def httpx_mock_direct_fixture(requests_mock): +def httpx_mock_direct_fixture() -> Generator[respx.MockRouter, None, None]: """Configure httpx fixture for direct Panel-API communication.""" with respx.mock( base_url=MOCK_DIRECT_BASE_URI, assert_all_called=False @@ -79,7 +80,7 @@ def httpx_mock_direct_fixture(requests_mock): @pytest.fixture(autouse=True) -def elmax_mock_direct_cert(requests_mock): +def elmax_mock_direct_cert() -> Generator[AsyncMock, None, None]: """Patch elmax library to return a specific PEM for SSL communication.""" with patch( "elmax_api.http.GenericElmax.retrieve_server_certificate", From 77c627e6f3a5dc2442506684389d4d45bff18f68 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 3 Jun 2024 10:19:13 +0200 Subject: [PATCH 0134/1445] Fix incorrect blueprint type hints in tests (#118694) --- tests/components/blueprint/common.py | 3 +-- tests/components/conftest.py | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/components/blueprint/common.py b/tests/components/blueprint/common.py index f1ccf63b26a..45c6a94f401 100644 --- a/tests/components/blueprint/common.py +++ b/tests/components/blueprint/common.py @@ -1,11 +1,10 @@ """Blueprints test helpers.""" from collections.abc import Generator -from typing import Any from unittest.mock import patch -def stub_blueprint_populate_fixture_helper() -> Generator[None, Any, None]: +def stub_blueprint_populate_fixture_helper() -> Generator[None, None, None]: """Stub copying the blueprints to the config folder.""" with patch( "homeassistant.components.blueprint.models.DomainBlueprints.async_populate" diff --git a/tests/components/conftest.py b/tests/components/conftest.py index 5e480383513..8bbb3b83c22 100644 --- a/tests/components/conftest.py +++ b/tests/components/conftest.py @@ -50,7 +50,7 @@ def entity_registry_enabled_by_default() -> Generator[None, None, None]: # Blueprint test fixtures @pytest.fixture(name="stub_blueprint_populate") -def stub_blueprint_populate_fixture() -> Generator[None, Any, None]: +def stub_blueprint_populate_fixture() -> Generator[None, None, None]: """Stub copying the blueprints to the config folder.""" from tests.components.blueprint.common import stub_blueprint_populate_fixture_helper From fdec1b0b161b9e631fc7f7750490ae09ed9d248d Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 3 Jun 2024 10:19:49 +0200 Subject: [PATCH 0135/1445] Add type hints for ClientSessionGenerator in test fixtures (#118689) --- tests/components/aladdin_connect/test_config_flow.py | 10 ++++++---- tests/components/rainbird/test_calendar.py | 3 +-- tests/components/traccar/test_init.py | 7 ++++++- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/tests/components/aladdin_connect/test_config_flow.py b/tests/components/aladdin_connect/test_config_flow.py index d460d62625b..0fca87487dd 100644 --- a/tests/components/aladdin_connect/test_config_flow.py +++ b/tests/components/aladdin_connect/test_config_flow.py @@ -18,6 +18,9 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import config_entry_oauth2_flow from homeassistant.setup import async_setup_component +from tests.test_util.aiohttp import AiohttpClientMocker +from tests.typing import ClientSessionGenerator + CLIENT_ID = "1234" CLIENT_SECRET = "5678" @@ -33,12 +36,11 @@ async def setup_credentials(hass: HomeAssistant) -> None: ) +@pytest.mark.usefixtures("current_request_with_host", "setup_credentials") async def test_full_flow( hass: HomeAssistant, - hass_client_no_auth, - aioclient_mock, - current_request_with_host, - setup_credentials, + hass_client_no_auth: ClientSessionGenerator, + aioclient_mock: AiohttpClientMocker, ) -> None: """Check full flow.""" result = await hass.config_entries.flow.async_init( diff --git a/tests/components/rainbird/test_calendar.py b/tests/components/rainbird/test_calendar.py index 03075038b90..3f5776c7b37 100644 --- a/tests/components/rainbird/test_calendar.py +++ b/tests/components/rainbird/test_calendar.py @@ -7,7 +7,6 @@ from typing import Any import urllib from zoneinfo import ZoneInfo -from aiohttp import ClientSession from freezegun.api import FrozenDateTimeFactory import pytest @@ -115,7 +114,7 @@ def mock_insert_schedule_response( @pytest.fixture(name="get_events") def get_events_fixture( - hass_client: Callable[..., Awaitable[ClientSession]], + hass_client: ClientSessionGenerator, ) -> GetEventsFn: """Fetch calendar events from the HTTP API.""" diff --git a/tests/components/traccar/test_init.py b/tests/components/traccar/test_init.py index 835a3ac78b4..d4b24175348 100644 --- a/tests/components/traccar/test_init.py +++ b/tests/components/traccar/test_init.py @@ -3,6 +3,7 @@ from http import HTTPStatus from unittest.mock import patch +from aiohttp.test_utils import TestClient import pytest from homeassistant import config_entries @@ -17,6 +18,8 @@ from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.dispatcher import DATA_DISPATCHER from homeassistant.setup import async_setup_component +from tests.typing import ClientSessionGenerator + HOME_LATITUDE = 37.239622 HOME_LONGITUDE = -115.815811 @@ -27,7 +30,9 @@ def mock_dev_track(mock_device_tracker_conf): @pytest.fixture(name="client") -async def traccar_client(hass, hass_client_no_auth): +async def traccar_client( + hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator +) -> TestClient: """Mock client for Traccar (unauthenticated).""" assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) From f39dd40be1d9f69eb324315e23f3374c59772942 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 3 Jun 2024 10:20:57 +0200 Subject: [PATCH 0136/1445] Add type hints for hass_storage in test fixtures (#118682) --- tests/components/smartthings/conftest.py | 8 ++++++-- tests/components/tag/test_init.py | 4 ++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/tests/components/smartthings/conftest.py b/tests/components/smartthings/conftest.py index d25cc8849e5..b638b9bbf4f 100644 --- a/tests/components/smartthings/conftest.py +++ b/tests/components/smartthings/conftest.py @@ -1,6 +1,7 @@ """Test configuration and mocks for the SmartThings component.""" import secrets +from typing import Any from unittest.mock import Mock, patch from uuid import uuid4 @@ -45,6 +46,7 @@ from homeassistant.const import ( CONF_CLIENT_SECRET, CONF_WEBHOOK_ID, ) +from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry @@ -74,7 +76,9 @@ async def setup_platform(hass, platform: str, *, devices=None, scenes=None): @pytest.fixture(autouse=True) -async def setup_component(hass, config_file, hass_storage): +async def setup_component( + hass: HomeAssistant, config_file: dict[str, str], hass_storage: dict[str, Any] +) -> None: """Load the SmartThing component.""" hass_storage[STORAGE_KEY] = {"data": config_file, "version": STORAGE_VERSION} await async_process_ha_core_config( @@ -166,7 +170,7 @@ def installed_apps_fixture(installed_app, locations, app): @pytest.fixture(name="config_file") -def config_file_fixture(): +def config_file_fixture() -> dict[str, str]: """Fixture representing the local config file contents.""" return {CONF_INSTANCE_ID: str(uuid4()), CONF_WEBHOOK_ID: secrets.token_hex()} diff --git a/tests/components/tag/test_init.py b/tests/components/tag/test_init.py index db7e9d5dbc7..d2d2bf90a7c 100644 --- a/tests/components/tag/test_init.py +++ b/tests/components/tag/test_init.py @@ -55,7 +55,7 @@ def storage_setup(hass: HomeAssistant, hass_storage: dict[str, Any]): @pytest.fixture -def storage_setup_1_1(hass: HomeAssistant, hass_storage): +def storage_setup_1_1(hass: HomeAssistant, hass_storage: dict[str, Any]): """Storage version 1.1 setup.""" async def _storage(items=None): @@ -87,7 +87,7 @@ async def test_migration( hass_ws_client: WebSocketGenerator, storage_setup_1_1, freezer: FrozenDateTimeFactory, - hass_storage, + hass_storage: dict[str, Any], snapshot: SnapshotAssertion, ) -> None: """Test migrating tag store.""" From 9be972b13e1249e3a73a7dda3715214d93d3fa47 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 3 Jun 2024 10:21:24 +0200 Subject: [PATCH 0137/1445] Add type hints for list[Device] in test fixtures (#118681) --- tests/components/geofency/test_init.py | 3 ++- tests/components/gpslogger/test_init.py | 3 ++- tests/components/locative/test_init.py | 3 ++- tests/components/owntracks/test_init.py | 3 ++- tests/components/traccar/test_init.py | 3 ++- 5 files changed, 10 insertions(+), 5 deletions(-) diff --git a/tests/components/geofency/test_init.py b/tests/components/geofency/test_init.py index 27e548505ac..2228cea80ee 100644 --- a/tests/components/geofency/test_init.py +++ b/tests/components/geofency/test_init.py @@ -8,6 +8,7 @@ import pytest from homeassistant import config_entries from homeassistant.components import zone +from homeassistant.components.device_tracker.legacy import Device from homeassistant.components.geofency import CONF_MOBILE_BEACONS, DOMAIN from homeassistant.config import async_process_ha_core_config from homeassistant.const import ( @@ -116,7 +117,7 @@ BEACON_EXIT_CAR = { @pytest.fixture(autouse=True) -def mock_dev_track(mock_device_tracker_conf): +def mock_dev_track(mock_device_tracker_conf: list[Device]) -> None: """Mock device tracker config loading.""" diff --git a/tests/components/gpslogger/test_init.py b/tests/components/gpslogger/test_init.py index 1511d0160c3..68b95df1702 100644 --- a/tests/components/gpslogger/test_init.py +++ b/tests/components/gpslogger/test_init.py @@ -9,6 +9,7 @@ import pytest from homeassistant import config_entries from homeassistant.components import gpslogger, zone from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER_DOMAIN +from homeassistant.components.device_tracker.legacy import Device from homeassistant.components.gpslogger import DOMAIN, TRACKER_UPDATE from homeassistant.config import async_process_ha_core_config from homeassistant.const import STATE_HOME, STATE_NOT_HOME @@ -25,7 +26,7 @@ HOME_LONGITUDE = -115.815811 @pytest.fixture(autouse=True) -def mock_dev_track(mock_device_tracker_conf): +def mock_dev_track(mock_device_tracker_conf: list[Device]) -> None: """Mock device tracker config loading.""" diff --git a/tests/components/locative/test_init.py b/tests/components/locative/test_init.py index 10683191fba..305497ebbd6 100644 --- a/tests/components/locative/test_init.py +++ b/tests/components/locative/test_init.py @@ -9,6 +9,7 @@ import pytest from homeassistant import config_entries from homeassistant.components import locative from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER_DOMAIN +from homeassistant.components.device_tracker.legacy import Device from homeassistant.components.locative import DOMAIN, TRACKER_UPDATE from homeassistant.config import async_process_ha_core_config from homeassistant.core import HomeAssistant @@ -20,7 +21,7 @@ from tests.typing import ClientSessionGenerator @pytest.fixture(autouse=True) -def mock_dev_track(mock_device_tracker_conf): +def mock_dev_track(mock_device_tracker_conf: list[Device]) -> None: """Mock device tracker config loading.""" diff --git a/tests/components/owntracks/test_init.py b/tests/components/owntracks/test_init.py index 43ba08943a8..5ef0efb0ab9 100644 --- a/tests/components/owntracks/test_init.py +++ b/tests/components/owntracks/test_init.py @@ -4,6 +4,7 @@ from aiohttp.test_utils import TestClient import pytest from homeassistant.components import owntracks +from homeassistant.components.device_tracker.legacy import Device from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component @@ -37,7 +38,7 @@ LOCATION_MESSAGE = { @pytest.fixture(autouse=True) -def mock_dev_track(mock_device_tracker_conf): +def mock_dev_track(mock_device_tracker_conf: list[Device]) -> None: """Mock device tracker config loading.""" diff --git a/tests/components/traccar/test_init.py b/tests/components/traccar/test_init.py index d4b24175348..feacbb7b13f 100644 --- a/tests/components/traccar/test_init.py +++ b/tests/components/traccar/test_init.py @@ -9,6 +9,7 @@ import pytest from homeassistant import config_entries from homeassistant.components import traccar, zone from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER_DOMAIN +from homeassistant.components.device_tracker.legacy import Device from homeassistant.components.traccar import DOMAIN, TRACKER_UPDATE from homeassistant.config import async_process_ha_core_config from homeassistant.const import STATE_HOME, STATE_NOT_HOME @@ -25,7 +26,7 @@ HOME_LONGITUDE = -115.815811 @pytest.fixture(autouse=True) -def mock_dev_track(mock_device_tracker_conf): +def mock_dev_track(mock_device_tracker_conf: list[Device]) -> None: """Mock device tracker config loading.""" From 6cf7889c38898f5fa791f5884a655101f44db7d7 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 3 Jun 2024 10:30:08 +0200 Subject: [PATCH 0138/1445] Add type hints for requests_mock.Mocker in test fixtures (#118678) --- tests/components/abode/conftest.py | 3 ++- tests/components/ecobee/conftest.py | 3 ++- tests/components/vultr/conftest.py | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/tests/components/abode/conftest.py b/tests/components/abode/conftest.py index 0e5e24b24f4..8e42dba4d87 100644 --- a/tests/components/abode/conftest.py +++ b/tests/components/abode/conftest.py @@ -5,6 +5,7 @@ from unittest.mock import AsyncMock, patch from jaraco.abode.helpers import urls as URL import pytest +from requests_mock import Mocker from tests.common import load_fixture from tests.components.light.conftest import mock_light_profiles # noqa: F401 @@ -20,7 +21,7 @@ def mock_setup_entry() -> Generator[AsyncMock, None, None]: @pytest.fixture(autouse=True) -def requests_mock_fixture(requests_mock) -> None: +def requests_mock_fixture(requests_mock: Mocker) -> None: """Fixture to provide a requests mocker.""" # Mocks the login response for abodepy. requests_mock.post(URL.LOGIN, text=load_fixture("login.json", "abode")) diff --git a/tests/components/ecobee/conftest.py b/tests/components/ecobee/conftest.py index 27d5a949c58..68a17dbfe00 100644 --- a/tests/components/ecobee/conftest.py +++ b/tests/components/ecobee/conftest.py @@ -4,6 +4,7 @@ from collections.abc import Generator from unittest.mock import MagicMock, patch import pytest +from requests_mock import Mocker from homeassistant.components.ecobee import ECOBEE_API_KEY, ECOBEE_REFRESH_TOKEN @@ -11,7 +12,7 @@ from tests.common import load_fixture, load_json_object_fixture @pytest.fixture(autouse=True) -def requests_mock_fixture(requests_mock): +def requests_mock_fixture(requests_mock: Mocker) -> None: """Fixture to provide a requests mocker.""" requests_mock.get( "https://api.ecobee.com/1/thermostat", diff --git a/tests/components/vultr/conftest.py b/tests/components/vultr/conftest.py index f8ecd1cf321..ae0ce9d6886 100644 --- a/tests/components/vultr/conftest.py +++ b/tests/components/vultr/conftest.py @@ -4,6 +4,7 @@ import json from unittest.mock import patch import pytest +from requests_mock import Mocker from homeassistant.components import vultr from homeassistant.core import HomeAssistant @@ -14,7 +15,7 @@ from tests.common import load_fixture @pytest.fixture(name="valid_config") -def valid_config(hass: HomeAssistant, requests_mock): +def valid_config(hass: HomeAssistant, requests_mock: Mocker) -> None: """Load a valid config.""" requests_mock.get( "https://api.vultr.com/v1/account/info?api_key=ABCDEFG1234567", From 35dcda29b954b97268e2cb9d85318b7facb96f5f Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 3 Jun 2024 10:34:09 +0200 Subject: [PATCH 0139/1445] Use ULID instead of UUID for config entry id and flow ID (#118677) --- homeassistant/config_entries.py | 6 +++--- tests/common.py | 4 ++-- tests/test_config_entries.py | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 4999eb6d34a..01363ec8129 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -66,7 +66,7 @@ from .setup import ( async_setup_component, async_start_setup, ) -from .util import uuid as uuid_util +from .util import ulid as ulid_util from .util.async_ import create_eager_task from .util.decorator import Registry from .util.enum import try_parse_enum @@ -324,7 +324,7 @@ class ConfigEntry(Generic[_DataT]): """Initialize a config entry.""" _setter = object.__setattr__ # Unique id of the config entry - _setter(self, "entry_id", entry_id or uuid_util.random_uuid_hex()) + _setter(self, "entry_id", entry_id or ulid_util.ulid_now()) # Version of the configuration. _setter(self, "version", version) @@ -1226,7 +1226,7 @@ class ConfigEntriesFlowManager(data_entry_flow.FlowManager[ConfigFlowResult]): if not context or "source" not in context: raise KeyError("Context not set or doesn't have a source set") - flow_id = uuid_util.random_uuid_hex() + flow_id = ulid_util.ulid_now() # Avoid starting a config flow on an integration that only supports # a single config entry, but which already has an entry diff --git a/tests/common.py b/tests/common.py index 897a28fbffd..b1110297d2f 100644 --- a/tests/common.py +++ b/tests/common.py @@ -95,8 +95,8 @@ from homeassistant.util.json import ( json_loads_object, ) from homeassistant.util.signal_type import SignalType +import homeassistant.util.ulid as ulid_util from homeassistant.util.unit_system import METRIC_SYSTEM -import homeassistant.util.uuid as uuid_util import homeassistant.util.yaml.loader as yaml_loader from tests.testing_config.custom_components.test_constant_deprecation import ( @@ -999,7 +999,7 @@ class MockConfigEntry(config_entries.ConfigEntry[_DataT]): "data": data or {}, "disabled_by": disabled_by, "domain": domain, - "entry_id": entry_id or uuid_util.random_uuid_hex(), + "entry_id": entry_id or ulid_util.ulid_now(), "minor_version": minor_version, "options": options or {}, "pref_disable_new_entities": pref_disable_new_entities, diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index f0045584055..a88b6ad31c3 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -2329,7 +2329,7 @@ async def test_entry_id_existing_entry( pytest.raises(HomeAssistantError), patch.dict(config_entries.HANDLERS, {"comp": TestFlow}), patch( - "homeassistant.config_entries.uuid_util.random_uuid_hex", + "homeassistant.config_entries.ulid_util.ulid_now", return_value=collide_entry_id, ), ): From 1b87a2dd73bfc8e497c686939c3fbc15f2c72092 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Mon, 3 Jun 2024 10:38:24 +0200 Subject: [PATCH 0140/1445] Update codeowners incomfort integration (#118700) --- CODEOWNERS | 2 +- homeassistant/components/incomfort/manifest.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index a626ebc2f29..bd2e449e6ea 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -658,7 +658,7 @@ build.json @home-assistant/supervisor /tests/components/imgw_pib/ @bieniu /homeassistant/components/improv_ble/ @emontnemery /tests/components/improv_ble/ @emontnemery -/homeassistant/components/incomfort/ @zxdavb +/homeassistant/components/incomfort/ @jbouwh @zxdavb /homeassistant/components/influxdb/ @mdegat01 /tests/components/influxdb/ @mdegat01 /homeassistant/components/inkbird/ @bdraco diff --git a/homeassistant/components/incomfort/manifest.json b/homeassistant/components/incomfort/manifest.json index e1c14533d8c..5559b81426c 100644 --- a/homeassistant/components/incomfort/manifest.json +++ b/homeassistant/components/incomfort/manifest.json @@ -1,7 +1,7 @@ { "domain": "incomfort", "name": "Intergas InComfort/Intouch Lan2RF gateway", - "codeowners": ["@zxdavb"], + "codeowners": ["@jbouwh", "@zxdavb"], "documentation": "https://www.home-assistant.io/integrations/incomfort", "iot_class": "local_polling", "loggers": ["incomfortclient"], From 855ba68b6295dbc681f576ee264aa495ca7e909b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Strandberg?= Date: Mon, 3 Jun 2024 10:59:42 +0200 Subject: [PATCH 0141/1445] Allow removal of myuplink device from GUI (#117009) * Allow removal of device from GUI * Check that device is orphaned before removing --- homeassistant/components/myuplink/__init__.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/homeassistant/components/myuplink/__init__.py b/homeassistant/components/myuplink/__init__.py index 42bb9007789..6d1932f22df 100644 --- a/homeassistant/components/myuplink/__init__.py +++ b/homeassistant/components/myuplink/__init__.py @@ -16,6 +16,7 @@ from homeassistant.helpers import ( config_entry_oauth2_flow, device_registry as dr, ) +from homeassistant.helpers.device_registry import DeviceEntry from .api import AsyncConfigEntryAuth from .const import DOMAIN, OAUTH2_SCOPES @@ -96,3 +97,14 @@ def create_devices( sw_version=device.firmwareCurrent, serial_number=device.product_serial_number, ) + + +async def async_remove_config_entry_device( + hass: HomeAssistant, config_entry: ConfigEntry, device_entry: DeviceEntry +) -> bool: + """Remove myuplink config entry from a device.""" + + myuplink_data: MyUplinkDataCoordinator = hass.data[DOMAIN][config_entry.entry_id] + return not device_entry.identifiers.intersection( + (DOMAIN, device_id) for device_id in myuplink_data.data.devices + ) From 185ce8221b7a18d72ad95f005e5421b4c48823c4 Mon Sep 17 00:00:00 2001 From: David Bonnes Date: Mon, 3 Jun 2024 10:29:54 +0100 Subject: [PATCH 0142/1445] Update the codeowners of the incomfort integration (#118706) --- CODEOWNERS | 2 +- homeassistant/components/incomfort/manifest.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index bd2e449e6ea..3f1247de891 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -658,7 +658,7 @@ build.json @home-assistant/supervisor /tests/components/imgw_pib/ @bieniu /homeassistant/components/improv_ble/ @emontnemery /tests/components/improv_ble/ @emontnemery -/homeassistant/components/incomfort/ @jbouwh @zxdavb +/homeassistant/components/incomfort/ @jbouwh /homeassistant/components/influxdb/ @mdegat01 /tests/components/influxdb/ @mdegat01 /homeassistant/components/inkbird/ @bdraco diff --git a/homeassistant/components/incomfort/manifest.json b/homeassistant/components/incomfort/manifest.json index 5559b81426c..3b5a1b76e7d 100644 --- a/homeassistant/components/incomfort/manifest.json +++ b/homeassistant/components/incomfort/manifest.json @@ -1,7 +1,7 @@ { "domain": "incomfort", "name": "Intergas InComfort/Intouch Lan2RF gateway", - "codeowners": ["@jbouwh", "@zxdavb"], + "codeowners": ["@jbouwh"], "documentation": "https://www.home-assistant.io/integrations/incomfort", "iot_class": "local_polling", "loggers": ["incomfortclient"], From 87a1b8e83cb1f664863efca5e830a794864ba9c9 Mon Sep 17 00:00:00 2001 From: Mick Vleeshouwer Date: Mon, 3 Jun 2024 11:43:40 +0200 Subject: [PATCH 0143/1445] Bump pyoverkiz to 1.13.11 (#118703) --- homeassistant/components/overkiz/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/overkiz/manifest.json b/homeassistant/components/overkiz/manifest.json index dc2f0df4783..a78eb160a28 100644 --- a/homeassistant/components/overkiz/manifest.json +++ b/homeassistant/components/overkiz/manifest.json @@ -19,7 +19,7 @@ "integration_type": "hub", "iot_class": "local_polling", "loggers": ["boto3", "botocore", "pyhumps", "pyoverkiz", "s3transfer"], - "requirements": ["pyoverkiz==1.13.10"], + "requirements": ["pyoverkiz==1.13.11"], "zeroconf": [ { "type": "_kizbox._tcp.local.", diff --git a/requirements_all.txt b/requirements_all.txt index a0c17559eec..6fb1d0a7d19 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2060,7 +2060,7 @@ pyotgw==2.2.0 pyotp==2.8.0 # homeassistant.components.overkiz -pyoverkiz==1.13.10 +pyoverkiz==1.13.11 # homeassistant.components.onewire pyownet==0.10.0.post1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 037534405b5..e5b2ea7f40a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1617,7 +1617,7 @@ pyotgw==2.2.0 pyotp==2.8.0 # homeassistant.components.overkiz -pyoverkiz==1.13.10 +pyoverkiz==1.13.11 # homeassistant.components.onewire pyownet==0.10.0.post1 From 765114bead65b5336d94c6117189fa91fc6f642b Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 3 Jun 2024 13:11:00 +0200 Subject: [PATCH 0144/1445] Don't store tag_id in tag storage (#118707) --- homeassistant/components/tag/__init__.py | 30 ++++++++++--------- tests/components/tag/snapshots/test_init.ambr | 2 -- tests/components/tag/test_init.py | 18 +++++------ 3 files changed, 24 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/tag/__init__.py b/homeassistant/components/tag/__init__.py index afea86baa93..ca0d53be6d0 100644 --- a/homeassistant/components/tag/__init__.py +++ b/homeassistant/components/tag/__init__.py @@ -9,7 +9,7 @@ import uuid import voluptuous as vol from homeassistant.components import websocket_api -from homeassistant.const import CONF_NAME +from homeassistant.const import CONF_ID, CONF_NAME from homeassistant.core import Context, HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import collection, entity_registry as er @@ -107,7 +107,7 @@ class TagStore(Store[collection.SerializedStorageCollection]): # Version 1.2 moves name to entity registry for tag in data["items"]: # Copy name in tag store to the entity registry - _create_entry(entity_registry, tag[TAG_ID], tag.get(CONF_NAME)) + _create_entry(entity_registry, tag[CONF_ID], tag.get(CONF_NAME)) tag["migrated"] = True if old_major_version > 1: @@ -136,24 +136,26 @@ class TagStorageCollection(collection.DictStorageCollection): data = self.CREATE_SCHEMA(data) if not data[TAG_ID]: data[TAG_ID] = str(uuid.uuid4()) + # Move tag id to id + data[CONF_ID] = data.pop(TAG_ID) # make last_scanned JSON serializeable if LAST_SCANNED in data: data[LAST_SCANNED] = data[LAST_SCANNED].isoformat() # Create entity in entity_registry when creating the tag # This is done early to store name only once in entity registry - _create_entry(self.entity_registry, data[TAG_ID], data.get(CONF_NAME)) + _create_entry(self.entity_registry, data[CONF_ID], data.get(CONF_NAME)) return data @callback def _get_suggested_id(self, info: dict[str, str]) -> str: """Suggest an ID based on the config.""" - return info[TAG_ID] + return info[CONF_ID] async def _update_data(self, item: dict, update_data: dict) -> dict: """Return a new updated data object.""" data = {**item, **self.UPDATE_SCHEMA(update_data)} - tag_id = data[TAG_ID] + tag_id = item[CONF_ID] # make last_scanned JSON serializeable if LAST_SCANNED in update_data: data[LAST_SCANNED] = data[LAST_SCANNED].isoformat() @@ -211,7 +213,7 @@ class TagDictStorageCollectionWebsocket( item = {k: v for k, v in item.items() if k != "migrated"} if ( entity_id := self.entity_registry.async_get_entity_id( - DOMAIN, DOMAIN, item[TAG_ID] + DOMAIN, DOMAIN, item[CONF_ID] ) ) and (entity := self.entity_registry.async_get(entity_id)): item[CONF_NAME] = entity.name or entity.original_name @@ -249,14 +251,14 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: ) if change_type == collection.CHANGE_ADDED: # When tags are added to storage - entity = _create_entry(entity_registry, updated_config[TAG_ID], None) + entity = _create_entry(entity_registry, updated_config[CONF_ID], None) if TYPE_CHECKING: assert entity.original_name await component.async_add_entities( [ TagEntity( entity.name or entity.original_name, - updated_config[TAG_ID], + updated_config[CONF_ID], updated_config.get(LAST_SCANNED), updated_config.get(DEVICE_ID), ) @@ -267,7 +269,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # When tags are changed or updated in storage async_dispatcher_send( hass, - f"{SIGNAL_TAG_CHANGED}-{updated_config[TAG_ID]}", + f"{SIGNAL_TAG_CHANGED}-{updated_config[CONF_ID]}", updated_config.get(DEVICE_ID), updated_config.get(LAST_SCANNED), ) @@ -276,7 +278,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: elif change_type == collection.CHANGE_REMOVED: # When tags are removed from storage entity_id = entity_registry.async_get_entity_id( - DOMAIN, DOMAIN, updated_config[TAG_ID] + DOMAIN, DOMAIN, updated_config[CONF_ID] ) if entity_id: entity_registry.async_remove(entity_id) @@ -287,13 +289,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: for tag in storage_collection.async_items(): if _LOGGER.isEnabledFor(logging.DEBUG): _LOGGER.debug("Adding tag: %s", tag) - entity_id = entity_registry.async_get_entity_id(DOMAIN, DOMAIN, tag[TAG_ID]) + entity_id = entity_registry.async_get_entity_id(DOMAIN, DOMAIN, tag[CONF_ID]) if entity_id := entity_registry.async_get_entity_id( - DOMAIN, DOMAIN, tag[TAG_ID] + DOMAIN, DOMAIN, tag[CONF_ID] ): entity = entity_registry.async_get(entity_id) else: - entity = _create_entry(entity_registry, tag[TAG_ID], None) + entity = _create_entry(entity_registry, tag[CONF_ID], None) if TYPE_CHECKING: assert entity assert entity.original_name @@ -301,7 +303,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: entities.append( TagEntity( name, - tag[TAG_ID], + tag[CONF_ID], tag.get(LAST_SCANNED), tag.get(DEVICE_ID), ) diff --git a/tests/components/tag/snapshots/test_init.ambr b/tests/components/tag/snapshots/test_init.ambr index 8a17079e16d..bfa80d8462e 100644 --- a/tests/components/tag/snapshots/test_init.ambr +++ b/tests/components/tag/snapshots/test_init.ambr @@ -13,11 +13,9 @@ 'device_id': 'some_scanner', 'id': 'new tag', 'last_scanned': '2024-02-29T13:00:00+00:00', - 'tag_id': 'new tag', }), dict({ 'id': '1234567890', - 'tag_id': '1234567890', }), ]), }), diff --git a/tests/components/tag/test_init.py b/tests/components/tag/test_init.py index d2d2bf90a7c..2e4c4b95a16 100644 --- a/tests/components/tag/test_init.py +++ b/tests/components/tag/test_init.py @@ -34,11 +34,9 @@ def storage_setup(hass: HomeAssistant, hass_storage: dict[str, Any]): "items": [ { "id": TEST_TAG_ID, - "tag_id": TEST_TAG_ID, }, { "id": TEST_TAG_ID_2, - "tag_id": TEST_TAG_ID_2, }, ] }, @@ -117,6 +115,7 @@ async def test_migration( ) resp = await client.receive_json() assert resp["success"] + assert resp["result"] == {"id": "1234567890", "name": "Kitchen tag"} # Trigger store freezer.tick(11) @@ -137,8 +136,8 @@ async def test_ws_list( resp = await client.receive_json() assert resp["success"] assert resp["result"] == [ - {"id": TEST_TAG_ID, "name": "test tag name", "tag_id": TEST_TAG_ID}, - {"id": TEST_TAG_ID_2, "name": "test tag name 2", "tag_id": TEST_TAG_ID_2}, + {"id": TEST_TAG_ID, "name": "test tag name"}, + {"id": TEST_TAG_ID_2, "name": "test tag name 2"}, ] @@ -161,7 +160,7 @@ async def test_ws_update( resp = await client.receive_json() assert resp["success"] item = resp["result"] - assert item == {"id": TEST_TAG_ID, "name": "New name", "tag_id": TEST_TAG_ID} + assert item == {"id": TEST_TAG_ID, "name": "New name"} async def test_tag_scanned( @@ -182,8 +181,8 @@ async def test_tag_scanned( result = {item["id"]: item for item in resp["result"]} assert resp["result"] == [ - {"id": TEST_TAG_ID, "name": "test tag name", "tag_id": TEST_TAG_ID}, - {"id": TEST_TAG_ID_2, "name": "test tag name 2", "tag_id": TEST_TAG_ID_2}, + {"id": TEST_TAG_ID, "name": "test tag name"}, + {"id": TEST_TAG_ID_2, "name": "test tag name 2"}, ] now = dt_util.utcnow() @@ -198,14 +197,13 @@ async def test_tag_scanned( assert len(result) == 3 assert resp["result"] == [ - {"id": TEST_TAG_ID, "name": "test tag name", "tag_id": TEST_TAG_ID}, - {"id": TEST_TAG_ID_2, "name": "test tag name 2", "tag_id": TEST_TAG_ID_2}, + {"id": TEST_TAG_ID, "name": "test tag name"}, + {"id": TEST_TAG_ID_2, "name": "test tag name 2"}, { "device_id": "some_scanner", "id": "new tag", "last_scanned": now.isoformat(), "name": "Tag new tag", - "tag_id": "new tag", }, ] From ef7c7f1c054b4ef5004c13af8bccd10141863bf5 Mon Sep 17 00:00:00 2001 From: Richard Kroegel <42204099+rikroe@users.noreply.github.com> Date: Mon, 3 Jun 2024 13:16:12 +0200 Subject: [PATCH 0145/1445] Refactor fixture calling for BMW tests (#118708) * Refactor BMW tests to use pytest.mark.usefixtures * Fix freeze_time --------- Co-authored-by: Richard --- .../bmw_connected_drive/test_button.py | 6 +++--- .../bmw_connected_drive/test_coordinator.py | 16 ++++++++++------ .../bmw_connected_drive/test_diagnostics.py | 12 ++++++------ .../components/bmw_connected_drive/test_init.py | 3 +-- .../bmw_connected_drive/test_number.py | 8 ++++---- .../bmw_connected_drive/test_select.py | 8 ++++---- .../bmw_connected_drive/test_sensor.py | 10 ++++------ .../bmw_connected_drive/test_switch.py | 5 ++--- 8 files changed, 34 insertions(+), 34 deletions(-) diff --git a/tests/components/bmw_connected_drive/test_button.py b/tests/components/bmw_connected_drive/test_button.py index 25d01fa74c9..3c7db219d54 100644 --- a/tests/components/bmw_connected_drive/test_button.py +++ b/tests/components/bmw_connected_drive/test_button.py @@ -14,10 +14,10 @@ from homeassistant.exceptions import HomeAssistantError from . import check_remote_service_call, setup_mocked_integration +@pytest.mark.usefixtures("bmw_fixture") +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_entity_state_attrs( hass: HomeAssistant, - bmw_fixture: respx.Router, - entity_registry_enabled_by_default: None, snapshot: SnapshotAssertion, ) -> None: """Test button options and values.""" @@ -57,9 +57,9 @@ async def test_service_call_success( check_remote_service_call(bmw_fixture, remote_service) +@pytest.mark.usefixtures("bmw_fixture") async def test_service_call_fail( hass: HomeAssistant, - bmw_fixture: respx.Router, monkeypatch: pytest.MonkeyPatch, ) -> None: """Test failed button press.""" diff --git a/tests/components/bmw_connected_drive/test_coordinator.py b/tests/components/bmw_connected_drive/test_coordinator.py index 812d309a257..5b3f99a9414 100644 --- a/tests/components/bmw_connected_drive/test_coordinator.py +++ b/tests/components/bmw_connected_drive/test_coordinator.py @@ -5,7 +5,7 @@ from unittest.mock import patch from bimmer_connected.models import MyBMWAPIError, MyBMWAuthError from freezegun.api import FrozenDateTimeFactory -import respx +import pytest from homeassistant.components.bmw_connected_drive import DOMAIN as BMW_DOMAIN from homeassistant.core import DOMAIN as HA_DOMAIN, HomeAssistant @@ -18,7 +18,8 @@ from . import FIXTURE_CONFIG_ENTRY from tests.common import MockConfigEntry, async_fire_time_changed -async def test_update_success(hass: HomeAssistant, bmw_fixture: respx.Router) -> None: +@pytest.mark.usefixtures("bmw_fixture") +async def test_update_success(hass: HomeAssistant) -> None: """Test the reauth form.""" config_entry = MockConfigEntry(**FIXTURE_CONFIG_ENTRY) config_entry.add_to_hass(hass) @@ -32,8 +33,10 @@ async def test_update_success(hass: HomeAssistant, bmw_fixture: respx.Router) -> ) +@pytest.mark.usefixtures("bmw_fixture") async def test_update_failed( - hass: HomeAssistant, bmw_fixture: respx.Router, freezer: FrozenDateTimeFactory + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, ) -> None: """Test the reauth form.""" config_entry = MockConfigEntry(**FIXTURE_CONFIG_ENTRY) @@ -59,8 +62,10 @@ async def test_update_failed( assert isinstance(coordinator.last_exception, UpdateFailed) is True +@pytest.mark.usefixtures("bmw_fixture") async def test_update_reauth( - hass: HomeAssistant, bmw_fixture: respx.Router, freezer: FrozenDateTimeFactory + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, ) -> None: """Test the reauth form.""" config_entry = MockConfigEntry(**FIXTURE_CONFIG_ENTRY) @@ -96,10 +101,9 @@ async def test_update_reauth( assert isinstance(coordinator.last_exception, ConfigEntryAuthFailed) is True +@pytest.mark.usefixtures("bmw_fixture") async def test_init_reauth( hass: HomeAssistant, - bmw_fixture: respx.Router, - freezer: FrozenDateTimeFactory, issue_registry: ir.IssueRegistry, ) -> None: """Test the reauth form.""" diff --git a/tests/components/bmw_connected_drive/test_diagnostics.py b/tests/components/bmw_connected_drive/test_diagnostics.py index fedfb1c2351..984275eab6a 100644 --- a/tests/components/bmw_connected_drive/test_diagnostics.py +++ b/tests/components/bmw_connected_drive/test_diagnostics.py @@ -19,11 +19,11 @@ from tests.typing import ClientSessionGenerator @pytest.mark.freeze_time(datetime.datetime(2022, 7, 10, 11, tzinfo=datetime.UTC)) +@pytest.mark.usefixtures("bmw_fixture") +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_config_entry_diagnostics( hass: HomeAssistant, hass_client: ClientSessionGenerator, - bmw_fixture, - entity_registry_enabled_by_default: None, snapshot: SnapshotAssertion, ) -> None: """Test config entry diagnostics.""" @@ -38,12 +38,12 @@ async def test_config_entry_diagnostics( @pytest.mark.freeze_time(datetime.datetime(2022, 7, 10, 11, tzinfo=datetime.UTC)) +@pytest.mark.usefixtures("bmw_fixture") +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_device_diagnostics( hass: HomeAssistant, hass_client: ClientSessionGenerator, device_registry: dr.DeviceRegistry, - bmw_fixture, - entity_registry_enabled_by_default: None, snapshot: SnapshotAssertion, ) -> None: """Test device diagnostics.""" @@ -63,12 +63,12 @@ async def test_device_diagnostics( @pytest.mark.freeze_time(datetime.datetime(2022, 7, 10, 11, tzinfo=datetime.UTC)) +@pytest.mark.usefixtures("bmw_fixture") +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_device_diagnostics_vehicle_not_found( hass: HomeAssistant, hass_client: ClientSessionGenerator, device_registry: dr.DeviceRegistry, - bmw_fixture, - entity_registry_enabled_by_default: None, snapshot: SnapshotAssertion, ) -> None: """Test device diagnostics when the vehicle cannot be found.""" diff --git a/tests/components/bmw_connected_drive/test_init.py b/tests/components/bmw_connected_drive/test_init.py index b8081d8d119..d648ad65f5d 100644 --- a/tests/components/bmw_connected_drive/test_init.py +++ b/tests/components/bmw_connected_drive/test_init.py @@ -3,7 +3,6 @@ from unittest.mock import patch import pytest -import respx from homeassistant.components.bmw_connected_drive.const import DOMAIN as BMW_DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN @@ -137,10 +136,10 @@ async def test_dont_migrate_unique_ids( assert entity_migrated != entity_not_changed +@pytest.mark.usefixtures("bmw_fixture") async def test_remove_stale_devices( hass: HomeAssistant, device_registry: dr.DeviceRegistry, - bmw_fixture: respx.Router, ) -> None: """Test remove stale device registry entries.""" mock_config_entry = MockConfigEntry(**FIXTURE_CONFIG_ENTRY) diff --git a/tests/components/bmw_connected_drive/test_number.py b/tests/components/bmw_connected_drive/test_number.py index 1047e595c95..53e61439003 100644 --- a/tests/components/bmw_connected_drive/test_number.py +++ b/tests/components/bmw_connected_drive/test_number.py @@ -14,10 +14,10 @@ from homeassistant.exceptions import HomeAssistantError from . import check_remote_service_call, setup_mocked_integration +@pytest.mark.usefixtures("bmw_fixture") +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_entity_state_attrs( hass: HomeAssistant, - bmw_fixture: respx.Router, - entity_registry_enabled_by_default: None, snapshot: SnapshotAssertion, ) -> None: """Test number options and values..""" @@ -62,6 +62,7 @@ async def test_service_call_success( assert hass.states.get(entity_id).state == new_value +@pytest.mark.usefixtures("bmw_fixture") @pytest.mark.parametrize( ("entity_id", "value"), [ @@ -72,7 +73,6 @@ async def test_service_call_invalid_input( hass: HomeAssistant, entity_id: str, value: str, - bmw_fixture: respx.Router, ) -> None: """Test not allowed values for number inputs.""" @@ -92,6 +92,7 @@ async def test_service_call_invalid_input( assert hass.states.get(entity_id).state == old_value +@pytest.mark.usefixtures("bmw_fixture") @pytest.mark.parametrize( ("raised", "expected"), [ @@ -104,7 +105,6 @@ async def test_service_call_fail( hass: HomeAssistant, raised: Exception, expected: Exception, - bmw_fixture: respx.Router, monkeypatch: pytest.MonkeyPatch, ) -> None: """Test exception handling.""" diff --git a/tests/components/bmw_connected_drive/test_select.py b/tests/components/bmw_connected_drive/test_select.py index 0c78d89cd8a..f3877119e3e 100644 --- a/tests/components/bmw_connected_drive/test_select.py +++ b/tests/components/bmw_connected_drive/test_select.py @@ -14,10 +14,10 @@ from homeassistant.exceptions import HomeAssistantError, ServiceValidationError from . import check_remote_service_call, setup_mocked_integration +@pytest.mark.usefixtures("bmw_fixture") +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_entity_state_attrs( hass: HomeAssistant, - bmw_fixture: respx.Router, - entity_registry_enabled_by_default: None, snapshot: SnapshotAssertion, ) -> None: """Test select options and values..""" @@ -74,6 +74,7 @@ async def test_service_call_success( assert hass.states.get(entity_id).state == new_value +@pytest.mark.usefixtures("bmw_fixture") @pytest.mark.parametrize( ("entity_id", "value"), [ @@ -85,7 +86,6 @@ async def test_service_call_invalid_input( hass: HomeAssistant, entity_id: str, value: str, - bmw_fixture: respx.Router, ) -> None: """Test not allowed values for select inputs.""" @@ -105,6 +105,7 @@ async def test_service_call_invalid_input( assert hass.states.get(entity_id).state == old_value +@pytest.mark.usefixtures("bmw_fixture") @pytest.mark.parametrize( ("raised", "expected"), [ @@ -117,7 +118,6 @@ async def test_service_call_fail( hass: HomeAssistant, raised: Exception, expected: Exception, - bmw_fixture: respx.Router, monkeypatch: pytest.MonkeyPatch, ) -> None: """Test exception handling.""" diff --git a/tests/components/bmw_connected_drive/test_sensor.py b/tests/components/bmw_connected_drive/test_sensor.py index 18c589bb72a..2e48189e4a1 100644 --- a/tests/components/bmw_connected_drive/test_sensor.py +++ b/tests/components/bmw_connected_drive/test_sensor.py @@ -1,8 +1,6 @@ """Test BMW sensors.""" -from freezegun import freeze_time import pytest -import respx from syrupy.assertion import SnapshotAssertion from homeassistant.core import HomeAssistant @@ -15,11 +13,11 @@ from homeassistant.util.unit_system import ( from . import setup_mocked_integration -@freeze_time("2023-06-22 10:30:00+00:00") +@pytest.mark.freeze_time("2023-06-22 10:30:00+00:00") +@pytest.mark.usefixtures("bmw_fixture") +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_entity_state_attrs( hass: HomeAssistant, - bmw_fixture: respx.Router, - entity_registry_enabled_by_default: None, snapshot: SnapshotAssertion, ) -> None: """Test sensor options and values..""" @@ -31,6 +29,7 @@ async def test_entity_state_attrs( assert hass.states.async_all("sensor") == snapshot +@pytest.mark.usefixtures("bmw_fixture") @pytest.mark.parametrize( ("entity_id", "unit_system", "value", "unit_of_measurement"), [ @@ -56,7 +55,6 @@ async def test_unit_conversion( unit_system: UnitSystem, value: str, unit_of_measurement: str, - bmw_fixture, ) -> None: """Test conversion between metric and imperial units for sensors.""" diff --git a/tests/components/bmw_connected_drive/test_switch.py b/tests/components/bmw_connected_drive/test_switch.py index a667966d099..6cf20d8077e 100644 --- a/tests/components/bmw_connected_drive/test_switch.py +++ b/tests/components/bmw_connected_drive/test_switch.py @@ -14,10 +14,9 @@ from homeassistant.exceptions import HomeAssistantError from . import check_remote_service_call, setup_mocked_integration +@pytest.mark.usefixtures("bmw_fixture") async def test_entity_state_attrs( hass: HomeAssistant, - bmw_fixture: respx.Router, - entity_registry_enabled_by_default: None, snapshot: SnapshotAssertion, ) -> None: """Test switch options and values..""" @@ -65,6 +64,7 @@ async def test_service_call_success( assert hass.states.get(entity_id).state == new_value +@pytest.mark.usefixtures("bmw_fixture") @pytest.mark.parametrize( ("raised", "expected"), [ @@ -77,7 +77,6 @@ async def test_service_call_fail( hass: HomeAssistant, raised: Exception, expected: Exception, - bmw_fixture: respx.Router, monkeypatch: pytest.MonkeyPatch, ) -> None: """Test exception handling.""" From a3b60cb054f272227a18ca0c1e704c8523496730 Mon Sep 17 00:00:00 2001 From: Jake Martin Date: Mon, 3 Jun 2024 12:18:15 +0100 Subject: [PATCH 0146/1445] Add Monzo config reauth (#117726) * Add reauth config flow * Trigger reauth on Monzo AuthorisaionExpiredError * Add missing abort strings * Use FlowResultType enum * One extra == swapped for is * Use helper in reauth * Patch correct function in reauth test * Remove unecessary ** * Swap patch and calls check for access token checks * Do reauth trigger test without patch * Remove unnecessary str() on user_id - always str anyway * Update tests/components/monzo/test_config_flow.py --------- Co-authored-by: Joost Lekkerkerker --- homeassistant/components/monzo/config_flow.py | 37 +++- homeassistant/components/monzo/coordinator.py | 11 +- homeassistant/components/monzo/strings.json | 8 +- tests/components/monzo/test_config_flow.py | 160 +++++++++++++++++- 4 files changed, 204 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/monzo/config_flow.py b/homeassistant/components/monzo/config_flow.py index 1d5bc3147b1..2eb51b4d305 100644 --- a/homeassistant/components/monzo/config_flow.py +++ b/homeassistant/components/monzo/config_flow.py @@ -2,12 +2,13 @@ from __future__ import annotations +from collections.abc import Mapping import logging from typing import Any import voluptuous as vol -from homeassistant.config_entries import ConfigFlowResult +from homeassistant.config_entries import ConfigEntry, ConfigFlowResult from homeassistant.const import CONF_TOKEN from homeassistant.helpers import config_entry_oauth2_flow @@ -22,6 +23,7 @@ class MonzoFlowHandler( DOMAIN = DOMAIN oauth_data: dict[str, Any] + reauth_entry: ConfigEntry | None = None @property def logger(self) -> logging.Logger: @@ -33,7 +35,11 @@ class MonzoFlowHandler( ) -> ConfigFlowResult: """Wait for the user to confirm in-app approval.""" if user_input is not None: - return self.async_create_entry(title=DOMAIN, data={**self.oauth_data}) + if not self.reauth_entry: + return self.async_create_entry(title=DOMAIN, data=self.oauth_data) + return self.async_update_reload_and_abort( + self.reauth_entry, data={**self.reauth_entry.data, **self.oauth_data} + ) data_schema = vol.Schema({vol.Required("confirm"): bool}) @@ -43,10 +49,29 @@ class MonzoFlowHandler( async def async_oauth_create_entry(self, data: dict[str, Any]) -> ConfigFlowResult: """Create an entry for the flow.""" - user_id = str(data[CONF_TOKEN]["user_id"]) - await self.async_set_unique_id(user_id) - self._abort_if_unique_id_configured() - self.oauth_data = data + user_id = data[CONF_TOKEN]["user_id"] + if not self.reauth_entry: + await self.async_set_unique_id(user_id) + self._abort_if_unique_id_configured() + elif self.reauth_entry.unique_id != user_id: + return self.async_abort(reason="wrong_account") return await self.async_step_await_approval_confirmation() + + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: + """Perform reauth upon an API authentication error.""" + self.reauth_entry = self.hass.config_entries.async_get_entry( + self.context["entry_id"] + ) + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Confirm reauth dialog.""" + if user_input is None: + return self.async_show_form(step_id="reauth_confirm") + return await self.async_step_user() diff --git a/homeassistant/components/monzo/coordinator.py b/homeassistant/components/monzo/coordinator.py index 67fff38c4f8..223d7b05ffe 100644 --- a/homeassistant/components/monzo/coordinator.py +++ b/homeassistant/components/monzo/coordinator.py @@ -5,7 +5,10 @@ from datetime import timedelta import logging from typing import Any +from monzopy import AuthorisationExpiredError + from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .api import AuthenticatedMonzoAPI @@ -37,6 +40,10 @@ class MonzoCoordinator(DataUpdateCoordinator[MonzoData]): async def _async_update_data(self) -> MonzoData: """Fetch data from Monzo API.""" - accounts = await self.api.user_account.accounts() - pots = await self.api.user_account.pots() + try: + accounts = await self.api.user_account.accounts() + pots = await self.api.user_account.pots() + except AuthorisationExpiredError as err: + raise ConfigEntryAuthFailed from err + return MonzoData(accounts, pots) diff --git a/homeassistant/components/monzo/strings.json b/homeassistant/components/monzo/strings.json index 5c0a894a2e2..e4ec34a8459 100644 --- a/homeassistant/components/monzo/strings.json +++ b/homeassistant/components/monzo/strings.json @@ -4,6 +4,10 @@ "pick_implementation": { "title": "[%key:common::config_flow::title::oauth2_pick_implementation%]" }, + "reauth_confirm": { + "title": "[%key:common::config_flow::title::reauth%]", + "description": "The Monzo integration needs to re-authenticate your account" + }, "await_approval_confirmation": { "title": "Confirm in Monzo app", "description": "Before proceeding, open your Monzo app and approve the request from Home Assistant.", @@ -19,7 +23,9 @@ "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", "authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]", "no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]", - "user_rejected_authorize": "[%key:common::config_flow::abort::oauth2_user_rejected_authorize%]" + "user_rejected_authorize": "[%key:common::config_flow::abort::oauth2_user_rejected_authorize%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", + "wrong_account": "Wrong account: The credentials provided do not match this Monzo account." }, "create_entry": { "default": "[%key:common::config_flow::create_entry::authenticated%]" diff --git a/tests/components/monzo/test_config_flow.py b/tests/components/monzo/test_config_flow.py index bd4d8644457..7ad4c072723 100644 --- a/tests/components/monzo/test_config_flow.py +++ b/tests/components/monzo/test_config_flow.py @@ -1,13 +1,17 @@ """Tests for config flow.""" +from datetime import timedelta from unittest.mock import AsyncMock, patch +from freezegun.api import FrozenDateTimeFactory +from monzopy import AuthorisationExpiredError + from homeassistant.components.monzo.application_credentials import ( OAUTH2_AUTHORIZE, OAUTH2_TOKEN, ) from homeassistant.components.monzo.const import DOMAIN -from homeassistant.config_entries import SOURCE_USER +from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import config_entry_oauth2_flow @@ -15,7 +19,7 @@ from homeassistant.helpers import config_entry_oauth2_flow from . import setup_integration from .conftest import CLIENT_ID, USER_ID -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, async_fire_time_changed from tests.test_util.aiohttp import AiohttpClientMocker from tests.typing import ClientSessionGenerator @@ -59,7 +63,7 @@ async def test_full_flow( "access_token": "mock-access-token", "type": "Bearer", "expires_in": 60, - "user_id": 600, + "user_id": "600", }, ) with patch( @@ -136,3 +140,153 @@ async def test_config_non_unique_profile( result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" + + +async def test_config_reauth_profile( + hass: HomeAssistant, + hass_client_no_auth: ClientSessionGenerator, + aioclient_mock: AiohttpClientMocker, + polling_config_entry: MockConfigEntry, + monzo: AsyncMock, + current_request_with_host: None, +) -> None: + """Test reauth an existing profile reauthenticates the config entry.""" + await setup_integration(hass, polling_config_entry) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": SOURCE_REAUTH, + "entry_id": polling_config_entry.entry_id, + }, + data=polling_config_entry.data, + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + state = config_entry_oauth2_flow._encode_jwt( + hass, + { + "flow_id": result["flow_id"], + "redirect_uri": "https://example.com/auth/external/callback", + }, + ) + assert result["url"] == ( + f"{OAUTH2_AUTHORIZE}/?" + f"response_type=code&client_id={CLIENT_ID}&" + "redirect_uri=https://example.com/auth/external/callback&" + f"state={state}" + ) + client = await hass_client_no_auth() + resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") + assert resp.status == 200 + assert resp.headers["content-type"] == "text/html; charset=utf-8" + + aioclient_mock.clear_requests() + aioclient_mock.post( + OAUTH2_TOKEN, + json={ + "refresh_token": "mock-refresh-token", + "access_token": "new-mock-access-token", + "type": "Bearer", + "expires_in": 60, + "user_id": str(USER_ID), + }, + ) + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "await_approval_confirmation" + assert polling_config_entry.data["token"]["access_token"] == "mock-access-token" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={"confirm": True} + ) + + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + assert result + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reauth_successful" + assert polling_config_entry.data["token"]["access_token"] == "new-mock-access-token" + + +async def test_config_reauth_wrong_account( + hass: HomeAssistant, + hass_client_no_auth: ClientSessionGenerator, + aioclient_mock: AiohttpClientMocker, + polling_config_entry: MockConfigEntry, + current_request_with_host: None, +) -> None: + """Test reauth with wrong account.""" + await setup_integration(hass, polling_config_entry) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": SOURCE_REAUTH, + "entry_id": polling_config_entry.entry_id, + }, + data=polling_config_entry.data, + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + state = config_entry_oauth2_flow._encode_jwt( + hass, + { + "flow_id": result["flow_id"], + "redirect_uri": "https://example.com/auth/external/callback", + }, + ) + assert result["url"] == ( + f"{OAUTH2_AUTHORIZE}/?" + f"response_type=code&client_id={CLIENT_ID}&" + "redirect_uri=https://example.com/auth/external/callback&" + f"state={state}" + ) + client = await hass_client_no_auth() + resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") + assert resp.status == 200 + assert resp.headers["content-type"] == "text/html; charset=utf-8" + + aioclient_mock.clear_requests() + aioclient_mock.post( + OAUTH2_TOKEN, + json={ + "refresh_token": "mock-refresh-token", + "access_token": "mock-access-token", + "type": "Bearer", + "expires_in": 60, + "user_id": 12346, + }, + ) + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "wrong_account" + + +async def test_api_can_trigger_reauth( + hass: HomeAssistant, + polling_config_entry: MockConfigEntry, + monzo: AsyncMock, + freezer: FrozenDateTimeFactory, +) -> None: + """Test reauth an existing profile reauthenticates the config entry.""" + await setup_integration(hass, polling_config_entry) + + monzo.user_account.accounts.side_effect = AuthorisationExpiredError() + freezer.tick(timedelta(minutes=10)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + flows = hass.config_entries.flow.async_progress() + + assert len(flows) == 1 + flow = flows[0] + assert flow["step_id"] == "reauth_confirm" + assert flow["handler"] == DOMAIN + assert flow["context"]["source"] == SOURCE_REAUTH From 26ab4ad91855b824289de54e489a7fe4cb459828 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Mon, 3 Jun 2024 14:37:36 +0200 Subject: [PATCH 0147/1445] Add HDR type attribute to Kodi (#109603) Co-authored-by: Andriy Kushnir Co-authored-by: Erik Montnemery --- homeassistant/components/kodi/media_player.py | 19 +++++++++++++++++++ homeassistant/components/kodi/strings.json | 9 +++++++++ 2 files changed, 28 insertions(+) diff --git a/homeassistant/components/kodi/media_player.py b/homeassistant/components/kodi/media_player.py index 46d3d614bfa..2bfe21b6eaa 100644 --- a/homeassistant/components/kodi/media_player.py +++ b/homeassistant/components/kodi/media_player.py @@ -259,6 +259,7 @@ class KodiEntity(MediaPlayerEntity): _attr_has_entity_name = True _attr_name = None + _attr_translation_key = "media_player" _attr_supported_features = ( MediaPlayerEntityFeature.BROWSE_MEDIA | MediaPlayerEntityFeature.NEXT_TRACK @@ -516,6 +517,7 @@ class KodiEntity(MediaPlayerEntity): "album", "season", "episode", + "streamdetails", ], ) else: @@ -632,6 +634,23 @@ class KodiEntity(MediaPlayerEntity): return None + @property + def extra_state_attributes(self) -> dict[str, str | None]: + """Return the state attributes.""" + state_attr: dict[str, str | None] = {} + if self.state == MediaPlayerState.OFF: + return state_attr + + hdr_type = ( + self._item.get("streamdetails", {}).get("video", [{}])[0].get("hdrtype") + ) + if hdr_type == "": + state_attr["dynamic_range"] = "sdr" + else: + state_attr["dynamic_range"] = hdr_type + + return state_attr + async def async_turn_on(self) -> None: """Turn the media player on.""" _LOGGER.debug("Firing event to turn on device") diff --git a/homeassistant/components/kodi/strings.json b/homeassistant/components/kodi/strings.json index 7c7d53b33ac..5b472e0c193 100644 --- a/homeassistant/components/kodi/strings.json +++ b/homeassistant/components/kodi/strings.json @@ -83,5 +83,14 @@ } } } + }, + "entity": { + "media_player": { + "media_player": { + "state_attributes": { + "dynamic_range": { "name": "Dynamic range" } + } + } + } } } From 6d02453c8a7c1e7d424788ec1acf86b03d2d2c1a Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Mon, 3 Jun 2024 15:39:50 +0200 Subject: [PATCH 0148/1445] Bump python-roborock to 2.2.2 (#118697) --- homeassistant/components/roborock/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/roborock/manifest.json b/homeassistant/components/roborock/manifest.json index 8b46fb4c001..69dea8d0c25 100644 --- a/homeassistant/components/roborock/manifest.json +++ b/homeassistant/components/roborock/manifest.json @@ -7,7 +7,7 @@ "iot_class": "local_polling", "loggers": ["roborock"], "requirements": [ - "python-roborock==2.1.1", + "python-roborock==2.2.2", "vacuum-map-parser-roborock==0.1.2" ] } diff --git a/requirements_all.txt b/requirements_all.txt index 6fb1d0a7d19..7e4e45da51f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2306,7 +2306,7 @@ python-rabbitair==0.0.8 python-ripple-api==0.0.3 # homeassistant.components.roborock -python-roborock==2.1.1 +python-roborock==2.2.2 # homeassistant.components.smarttub python-smarttub==0.0.36 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e5b2ea7f40a..396025f8444 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1794,7 +1794,7 @@ python-qbittorrent==0.4.3 python-rabbitair==0.0.8 # homeassistant.components.roborock -python-roborock==2.1.1 +python-roborock==2.2.2 # homeassistant.components.smarttub python-smarttub==0.0.36 From c32eb97ac04fd592c2d4679b80a0d73701c2783e Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 3 Jun 2024 15:49:51 +0200 Subject: [PATCH 0149/1445] Disable both option in Airgradient select (#118702) --- .../components/airgradient/select.py | 10 ++++---- tests/components/airgradient/conftest.py | 24 ++++++++++++++++++- .../fixtures/get_config_cloud.json | 13 ++++++++++ .../fixtures/get_config_local.json | 13 ++++++++++ .../airgradient/snapshots/test_select.ambr | 8 ++----- tests/components/airgradient/test_select.py | 12 ++++------ 6 files changed, 61 insertions(+), 19 deletions(-) create mode 100644 tests/components/airgradient/fixtures/get_config_cloud.json create mode 100644 tests/components/airgradient/fixtures/get_config_local.json diff --git a/homeassistant/components/airgradient/select.py b/homeassistant/components/airgradient/select.py index 41b5a48c686..5e13ee1d0bb 100644 --- a/homeassistant/components/airgradient/select.py +++ b/homeassistant/components/airgradient/select.py @@ -22,7 +22,7 @@ from .entity import AirGradientEntity class AirGradientSelectEntityDescription(SelectEntityDescription): """Describes AirGradient select entity.""" - value_fn: Callable[[Config], str] + value_fn: Callable[[Config], str | None] set_value_fn: Callable[[AirGradientClient, str], Awaitable[None]] requires_display: bool = False @@ -30,9 +30,11 @@ class AirGradientSelectEntityDescription(SelectEntityDescription): CONFIG_CONTROL_ENTITY = AirGradientSelectEntityDescription( key="configuration_control", translation_key="configuration_control", - options=[x.value for x in ConfigurationControl], + options=[ConfigurationControl.CLOUD.value, ConfigurationControl.LOCAL.value], entity_category=EntityCategory.CONFIG, - value_fn=lambda config: config.configuration_control, + value_fn=lambda config: config.configuration_control + if config.configuration_control is not ConfigurationControl.BOTH + else None, set_value_fn=lambda client, value: client.set_configuration_control( ConfigurationControl(value) ), @@ -96,7 +98,7 @@ class AirGradientSelect(AirGradientEntity, SelectEntity): self._attr_unique_id = f"{coordinator.serial_number}-{description.key}" @property - def current_option(self) -> str: + def current_option(self) -> str | None: """Return the state of the select.""" return self.entity_description.value_fn(self.coordinator.data) diff --git a/tests/components/airgradient/conftest.py b/tests/components/airgradient/conftest.py index aa2c1e783a4..d2495c11a79 100644 --- a/tests/components/airgradient/conftest.py +++ b/tests/components/airgradient/conftest.py @@ -42,11 +42,33 @@ def mock_airgradient_client() -> Generator[AsyncMock, None, None]: load_fixture("current_measures.json", DOMAIN) ) client.get_config.return_value = Config.from_json( - load_fixture("get_config.json", DOMAIN) + load_fixture("get_config_local.json", DOMAIN) ) yield client +@pytest.fixture +def mock_new_airgradient_client( + mock_airgradient_client: AsyncMock, +) -> Generator[AsyncMock, None, None]: + """Mock a new AirGradient client.""" + mock_airgradient_client.get_config.return_value = Config.from_json( + load_fixture("get_config.json", DOMAIN) + ) + return mock_airgradient_client + + +@pytest.fixture +def mock_cloud_airgradient_client( + mock_airgradient_client: AsyncMock, +) -> Generator[AsyncMock, None, None]: + """Mock a new AirGradient client.""" + mock_airgradient_client.get_config.return_value = Config.from_json( + load_fixture("get_config_cloud.json", DOMAIN) + ) + return mock_airgradient_client + + @pytest.fixture def mock_config_entry() -> MockConfigEntry: """Mock a config entry.""" diff --git a/tests/components/airgradient/fixtures/get_config_cloud.json b/tests/components/airgradient/fixtures/get_config_cloud.json new file mode 100644 index 00000000000..a5f27957e04 --- /dev/null +++ b/tests/components/airgradient/fixtures/get_config_cloud.json @@ -0,0 +1,13 @@ +{ + "country": "DE", + "pmStandard": "ugm3", + "ledBarMode": "co2", + "displayMode": "on", + "abcDays": 8, + "tvocLearningOffset": 12, + "noxLearningOffset": 12, + "mqttBrokerUrl": "", + "temperatureUnit": "c", + "configurationControl": "cloud", + "postDataToAirGradient": true +} diff --git a/tests/components/airgradient/fixtures/get_config_local.json b/tests/components/airgradient/fixtures/get_config_local.json new file mode 100644 index 00000000000..09e0e982053 --- /dev/null +++ b/tests/components/airgradient/fixtures/get_config_local.json @@ -0,0 +1,13 @@ +{ + "country": "DE", + "pmStandard": "ugm3", + "ledBarMode": "co2", + "displayMode": "on", + "abcDays": 8, + "tvocLearningOffset": 12, + "noxLearningOffset": 12, + "mqttBrokerUrl": "", + "temperatureUnit": "c", + "configurationControl": "local", + "postDataToAirGradient": true +} diff --git a/tests/components/airgradient/snapshots/test_select.ambr b/tests/components/airgradient/snapshots/test_select.ambr index 986e3c6ebb8..fb201b88204 100644 --- a/tests/components/airgradient/snapshots/test_select.ambr +++ b/tests/components/airgradient/snapshots/test_select.ambr @@ -8,7 +8,6 @@ 'options': list([ 'cloud', 'local', - 'both', ]), }), 'config_entry_id': , @@ -45,7 +44,6 @@ 'options': list([ 'cloud', 'local', - 'both', ]), }), 'context': , @@ -53,7 +51,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'both', + 'state': 'local', }) # --- # name: test_all_entities[select.airgradient_display_temperature_unit-entry] @@ -120,7 +118,6 @@ 'options': list([ 'cloud', 'local', - 'both', ]), }), 'config_entry_id': , @@ -157,7 +154,6 @@ 'options': list([ 'cloud', 'local', - 'both', ]), }), 'context': , @@ -165,6 +161,6 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'both', + 'state': 'local', }) # --- diff --git a/tests/components/airgradient/test_select.py b/tests/components/airgradient/test_select.py index 2988a5918ad..986295bd245 100644 --- a/tests/components/airgradient/test_select.py +++ b/tests/components/airgradient/test_select.py @@ -77,16 +77,12 @@ async def test_setting_value( async def test_setting_protected_value( hass: HomeAssistant, - mock_airgradient_client: AsyncMock, + mock_cloud_airgradient_client: AsyncMock, mock_config_entry: MockConfigEntry, ) -> None: """Test setting protected value.""" await setup_integration(hass, mock_config_entry) - mock_airgradient_client.get_config.return_value.configuration_control = ( - ConfigurationControl.CLOUD - ) - with pytest.raises(ServiceValidationError): await hass.services.async_call( SELECT_DOMAIN, @@ -97,9 +93,9 @@ async def test_setting_protected_value( }, blocking=True, ) - mock_airgradient_client.set_temperature_unit.assert_not_called() + mock_cloud_airgradient_client.set_temperature_unit.assert_not_called() - mock_airgradient_client.get_config.return_value.configuration_control = ( + mock_cloud_airgradient_client.get_config.return_value.configuration_control = ( ConfigurationControl.LOCAL ) @@ -112,4 +108,4 @@ async def test_setting_protected_value( }, blocking=True, ) - mock_airgradient_client.set_temperature_unit.assert_called_once_with("c") + mock_cloud_airgradient_client.set_temperature_unit.assert_called_once_with("c") From 771ed33b146188fe7afbe0fb0cffef22975f0d62 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 3 Jun 2024 15:53:23 +0200 Subject: [PATCH 0150/1445] Bump renault-api to 0.2.3 (#118718) --- homeassistant/components/renault/binary_sensor.py | 2 +- homeassistant/components/renault/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/renault/fixtures/hvac_status.1.json | 2 +- tests/components/renault/fixtures/hvac_status.2.json | 2 +- tests/components/renault/snapshots/test_diagnostics.ambr | 4 ++-- 7 files changed, 8 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/renault/binary_sensor.py b/homeassistant/components/renault/binary_sensor.py index 2041499b711..7ebc77b8e77 100644 --- a/homeassistant/components/renault/binary_sensor.py +++ b/homeassistant/components/renault/binary_sensor.py @@ -81,7 +81,7 @@ BINARY_SENSOR_TYPES: tuple[RenaultBinarySensorEntityDescription, ...] = tuple( key="hvac_status", coordinator="hvac_status", on_key="hvacStatus", - on_value="on", + on_value=2, translation_key="hvac_status", ), RenaultBinarySensorEntityDescription( diff --git a/homeassistant/components/renault/manifest.json b/homeassistant/components/renault/manifest.json index 9891c838950..8407893011c 100644 --- a/homeassistant/components/renault/manifest.json +++ b/homeassistant/components/renault/manifest.json @@ -8,5 +8,5 @@ "iot_class": "cloud_polling", "loggers": ["renault_api"], "quality_scale": "platinum", - "requirements": ["renault-api==0.2.2"] + "requirements": ["renault-api==0.2.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index 7e4e45da51f..8e2848eb58c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2448,7 +2448,7 @@ refoss-ha==1.2.1 regenmaschine==2024.03.0 # homeassistant.components.renault -renault-api==0.2.2 +renault-api==0.2.3 # homeassistant.components.renson renson-endura-delta==1.7.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 396025f8444..afb2ee9d467 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1906,7 +1906,7 @@ refoss-ha==1.2.1 regenmaschine==2024.03.0 # homeassistant.components.renault -renault-api==0.2.2 +renault-api==0.2.3 # homeassistant.components.renson renson-endura-delta==1.7.1 diff --git a/tests/components/renault/fixtures/hvac_status.1.json b/tests/components/renault/fixtures/hvac_status.1.json index f48cbae68ae..7cbd7a9fe37 100644 --- a/tests/components/renault/fixtures/hvac_status.1.json +++ b/tests/components/renault/fixtures/hvac_status.1.json @@ -2,6 +2,6 @@ "data": { "type": "Car", "id": "VF1AAAAA555777999", - "attributes": { "externalTemperature": 8.0, "hvacStatus": "off" } + "attributes": { "externalTemperature": 8.0, "hvacStatus": 1 } } } diff --git a/tests/components/renault/fixtures/hvac_status.2.json b/tests/components/renault/fixtures/hvac_status.2.json index a2ca08a71e9..8bb4f941e06 100644 --- a/tests/components/renault/fixtures/hvac_status.2.json +++ b/tests/components/renault/fixtures/hvac_status.2.json @@ -4,7 +4,7 @@ "id": "VF1AAAAA555777999", "attributes": { "socThreshold": 30.0, - "hvacStatus": "off", + "hvacStatus": 1, "lastUpdateTime": "2020-12-03T00:00:00Z" } } diff --git a/tests/components/renault/snapshots/test_diagnostics.ambr b/tests/components/renault/snapshots/test_diagnostics.ambr index a2921dff35e..ae90115fcb6 100644 --- a/tests/components/renault/snapshots/test_diagnostics.ambr +++ b/tests/components/renault/snapshots/test_diagnostics.ambr @@ -22,7 +22,7 @@ }), 'hvac_status': dict({ 'externalTemperature': 8.0, - 'hvacStatus': 'off', + 'hvacStatus': 1, }), 'res_state': dict({ }), @@ -227,7 +227,7 @@ }), 'hvac_status': dict({ 'externalTemperature': 8.0, - 'hvacStatus': 'off', + 'hvacStatus': 1, }), 'res_state': dict({ }), From 595c9a2e014683f06eeb5b9f575e95b47d0ff9ac Mon Sep 17 00:00:00 2001 From: Matrix Date: Mon, 3 Jun 2024 21:56:42 +0800 Subject: [PATCH 0151/1445] Fixing device model compatibility issues. (#118686) --- homeassistant/components/yolink/const.py | 1 + homeassistant/components/yolink/switch.py | 36 +++++++++++++++-------- 2 files changed, 24 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/yolink/const.py b/homeassistant/components/yolink/const.py index 110b9cb9810..e829fe08d32 100644 --- a/homeassistant/components/yolink/const.py +++ b/homeassistant/components/yolink/const.py @@ -16,3 +16,4 @@ YOLINK_EVENT = f"{DOMAIN}_event" YOLINK_OFFLINE_TIME = 32400 DEV_MODEL_WATER_METER_YS5007 = "YS5007" +DEV_MODEL_MULTI_OUTLET_YS6801 = "YS6801" diff --git a/homeassistant/components/yolink/switch.py b/homeassistant/components/yolink/switch.py index 7a24ec1bd13..2e31100bf3c 100644 --- a/homeassistant/components/yolink/switch.py +++ b/homeassistant/components/yolink/switch.py @@ -25,7 +25,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN +from .const import DEV_MODEL_MULTI_OUTLET_YS6801, DOMAIN from .coordinator import YoLinkCoordinator from .entity import YoLinkEntity @@ -35,7 +35,7 @@ class YoLinkSwitchEntityDescription(SwitchEntityDescription): """YoLink SwitchEntityDescription.""" exists_fn: Callable[[YoLinkDevice], bool] = lambda _: True - plug_index: int | None = None + plug_index_fn: Callable[[YoLinkDevice], int | None] = lambda _: None DEVICE_TYPES: tuple[YoLinkSwitchEntityDescription, ...] = ( @@ -61,36 +61,43 @@ DEVICE_TYPES: tuple[YoLinkSwitchEntityDescription, ...] = ( key="multi_outlet_usb_ports", translation_key="usb_ports", device_class=SwitchDeviceClass.OUTLET, - exists_fn=lambda device: device.device_type == ATTR_DEVICE_MULTI_OUTLET, - plug_index=0, + exists_fn=lambda device: device.device_type == ATTR_DEVICE_MULTI_OUTLET + and device.device_model_name.startswith(DEV_MODEL_MULTI_OUTLET_YS6801), + plug_index_fn=lambda _: 0, ), YoLinkSwitchEntityDescription( key="multi_outlet_plug_1", translation_key="plug_1", device_class=SwitchDeviceClass.OUTLET, exists_fn=lambda device: device.device_type == ATTR_DEVICE_MULTI_OUTLET, - plug_index=1, + plug_index_fn=lambda device: 1 + if device.device_model_name.startswith(DEV_MODEL_MULTI_OUTLET_YS6801) + else 0, ), YoLinkSwitchEntityDescription( key="multi_outlet_plug_2", translation_key="plug_2", device_class=SwitchDeviceClass.OUTLET, exists_fn=lambda device: device.device_type == ATTR_DEVICE_MULTI_OUTLET, - plug_index=2, + plug_index_fn=lambda device: 2 + if device.device_model_name.startswith(DEV_MODEL_MULTI_OUTLET_YS6801) + else 1, ), YoLinkSwitchEntityDescription( key="multi_outlet_plug_3", translation_key="plug_3", device_class=SwitchDeviceClass.OUTLET, - exists_fn=lambda device: device.device_type == ATTR_DEVICE_MULTI_OUTLET, - plug_index=3, + exists_fn=lambda device: device.device_type == ATTR_DEVICE_MULTI_OUTLET + and device.device_model_name.startswith(DEV_MODEL_MULTI_OUTLET_YS6801), + plug_index_fn=lambda _: 3, ), YoLinkSwitchEntityDescription( key="multi_outlet_plug_4", translation_key="plug_4", device_class=SwitchDeviceClass.OUTLET, - exists_fn=lambda device: device.device_type == ATTR_DEVICE_MULTI_OUTLET, - plug_index=4, + exists_fn=lambda device: device.device_type == ATTR_DEVICE_MULTI_OUTLET + and device.device_model_name.startswith(DEV_MODEL_MULTI_OUTLET_YS6801), + plug_index_fn=lambda _: 4, ), ) @@ -152,7 +159,8 @@ class YoLinkSwitchEntity(YoLinkEntity, SwitchEntity): def update_entity_state(self, state: dict[str, str | list[str]]) -> None: """Update HA Entity State.""" self._attr_is_on = self._get_state( - state.get("state"), self.entity_description.plug_index + state.get("state"), + self.entity_description.plug_index_fn(self.coordinator.device), ) self.async_write_ha_state() @@ -164,12 +172,14 @@ class YoLinkSwitchEntity(YoLinkEntity, SwitchEntity): ATTR_DEVICE_MULTI_OUTLET, ]: client_request = OutletRequestBuilder.set_state_request( - state, self.entity_description.plug_index + state, self.entity_description.plug_index_fn(self.coordinator.device) ) else: client_request = ClientRequest("setState", {"state": state}) await self.call_device(client_request) - self._attr_is_on = self._get_state(state, self.entity_description.plug_index) + self._attr_is_on = self._get_state( + state, self.entity_description.plug_index_fn(self.coordinator.device) + ) self.async_write_ha_state() async def async_turn_on(self, **kwargs: Any) -> None: From 8a68529dd141ae50d758938757b381bf9305c7ac Mon Sep 17 00:00:00 2001 From: RJPoelstra <36924801+RJPoelstra@users.noreply.github.com> Date: Mon, 3 Jun 2024 16:27:42 +0200 Subject: [PATCH 0152/1445] Bump python-MotionMount to 2.0.0 (#118719) --- homeassistant/components/motionmount/manifest.json | 2 +- homeassistant/components/motionmount/select.py | 7 +++---- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 6 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/motionmount/manifest.json b/homeassistant/components/motionmount/manifest.json index e6a7bd50fba..b7ce3ad1fd9 100644 --- a/homeassistant/components/motionmount/manifest.json +++ b/homeassistant/components/motionmount/manifest.json @@ -6,6 +6,6 @@ "documentation": "https://www.home-assistant.io/integrations/motionmount", "integration_type": "device", "iot_class": "local_push", - "requirements": ["python-MotionMount==1.0.0"], + "requirements": ["python-MotionMount==2.0.0"], "zeroconf": ["_tvm._tcp.local."] } diff --git a/homeassistant/components/motionmount/select.py b/homeassistant/components/motionmount/select.py index 7d8a6ccdbc4..b9001b55b7f 100644 --- a/homeassistant/components/motionmount/select.py +++ b/homeassistant/components/motionmount/select.py @@ -35,11 +35,10 @@ class MotionMountPresets(MotionMountEntity, SelectEntity): super().__init__(mm, config_entry) self._attr_unique_id = f"{self._base_unique_id}-preset" - def _update_options(self, presets: dict[int, str]) -> None: + def _update_options(self, presets: list[motionmount.Preset]) -> None: """Convert presets to select options.""" - options = [WALL_PRESET_NAME] - for index, name in presets.items(): - options.append(f"{index}: {name}") + options = [f"{preset.index}: {preset.name}" for preset in presets] + options.insert(0, WALL_PRESET_NAME) self._attr_options = options diff --git a/requirements_all.txt b/requirements_all.txt index 8e2848eb58c..3c8cec53d62 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2209,7 +2209,7 @@ pytfiac==0.4 pythinkingcleaner==0.0.3 # homeassistant.components.motionmount -python-MotionMount==1.0.0 +python-MotionMount==2.0.0 # homeassistant.components.awair python-awair==0.2.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index afb2ee9d467..1c1b1ef1070 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1733,7 +1733,7 @@ pytautulli==23.1.1 pytedee-async==0.2.17 # homeassistant.components.motionmount -python-MotionMount==1.0.0 +python-MotionMount==2.0.0 # homeassistant.components.awair python-awair==0.2.4 From bdcfd93129c79ddbf8451cd27daa5307496cd84f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 3 Jun 2024 10:36:41 -0400 Subject: [PATCH 0153/1445] Automatically fill in slots based on LLM context (#118619) * Automatically fill in slots from LLM context * Add tests * Apply suggestions from code review Co-authored-by: Allen Porter --------- Co-authored-by: Allen Porter --- homeassistant/helpers/llm.py | 38 +++++++++++++++++++-- tests/helpers/test_llm.py | 65 +++++++++++++++++++++++++++++++++--- 2 files changed, 97 insertions(+), 6 deletions(-) diff --git a/homeassistant/helpers/llm.py b/homeassistant/helpers/llm.py index ec1bfb7dbc4..37233b0d407 100644 --- a/homeassistant/helpers/llm.py +++ b/homeassistant/helpers/llm.py @@ -181,14 +181,48 @@ class IntentTool(Tool): self.description = ( intent_handler.description or f"Execute Home Assistant {self.name} intent" ) - if slot_schema := intent_handler.slot_schema: - self.parameters = vol.Schema(slot_schema) + self.extra_slots = None + if not (slot_schema := intent_handler.slot_schema): + return + + slot_schema = {**slot_schema} + extra_slots = set() + + for field in ("preferred_area_id", "preferred_floor_id"): + if field in slot_schema: + extra_slots.add(field) + del slot_schema[field] + + self.parameters = vol.Schema(slot_schema) + if extra_slots: + self.extra_slots = extra_slots async def async_call( self, hass: HomeAssistant, tool_input: ToolInput, llm_context: LLMContext ) -> JsonObjectType: """Handle the intent.""" slots = {key: {"value": val} for key, val in tool_input.tool_args.items()} + + if self.extra_slots and llm_context.device_id: + device_reg = dr.async_get(hass) + device = device_reg.async_get(llm_context.device_id) + + area: ar.AreaEntry | None = None + floor: fr.FloorEntry | None = None + if device: + area_reg = ar.async_get(hass) + if device.area_id and (area := area_reg.async_get_area(device.area_id)): + if area.floor_id: + floor_reg = fr.async_get(hass) + floor = floor_reg.async_get_floor(area.floor_id) + + for slot_name, slot_value in ( + ("preferred_area_id", area.id if area else None), + ("preferred_floor_id", floor.floor_id if floor else None), + ): + if slot_value and slot_name in self.extra_slots: + slots[slot_name] = {"value": slot_value} + intent_response = await intent.async_handle( hass=hass, platform=llm_context.platform, diff --git a/tests/helpers/test_llm.py b/tests/helpers/test_llm.py index 9ad58441277..6c9451bc843 100644 --- a/tests/helpers/test_llm.py +++ b/tests/helpers/test_llm.py @@ -77,7 +77,11 @@ async def test_call_tool_no_existing( async def test_assist_api( - hass: HomeAssistant, entity_registry: er.EntityRegistry + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + device_registry: dr.DeviceRegistry, + area_registry: ar.AreaRegistry, + floor_registry: fr.FloorRegistry, ) -> None: """Test Assist API.""" assert await async_setup_component(hass, "homeassistant", {}) @@ -97,11 +101,13 @@ async def test_assist_api( user_prompt="test_text", language="*", assistant="conversation", - device_id="test_device", + device_id=None, ) schema = { vol.Optional("area"): cv.string, vol.Optional("floor"): cv.string, + vol.Optional("preferred_area_id"): cv.string, + vol.Optional("preferred_floor_id"): cv.string, } class MyIntentHandler(intent.IntentHandler): @@ -131,7 +137,13 @@ async def test_assist_api( tool = api.tools[0] assert tool.name == "test_intent" assert tool.description == "Execute Home Assistant test_intent intent" - assert tool.parameters == vol.Schema(intent_handler.slot_schema) + assert tool.parameters == vol.Schema( + { + vol.Optional("area"): cv.string, + vol.Optional("floor"): cv.string, + # No preferred_area_id, preferred_floor_id + } + ) assert str(tool) == "" assert test_context.json_fragment # To reproduce an error case in tracing @@ -160,7 +172,52 @@ async def test_assist_api( context=test_context, language="*", assistant="conversation", - device_id="test_device", + device_id=None, + ) + assert response == { + "data": { + "failed": [], + "success": [], + "targets": [], + }, + "response_type": "action_done", + "speech": {}, + } + + # Call with a device/area/floor + entry = MockConfigEntry(title=None) + entry.add_to_hass(hass) + + device = device_registry.async_get_or_create( + config_entry_id=entry.entry_id, + connections={("test", "1234")}, + suggested_area="Test Area", + ) + area = area_registry.async_get_area_by_name("Test Area") + floor = floor_registry.async_create("2") + area_registry.async_update(area.id, floor_id=floor.floor_id) + llm_context.device_id = device.id + + with patch( + "homeassistant.helpers.intent.async_handle", return_value=intent_response + ) as mock_intent_handle: + response = await api.async_call_tool(tool_input) + + mock_intent_handle.assert_awaited_once_with( + hass=hass, + platform="test_platform", + intent_type="test_intent", + slots={ + "area": {"value": "kitchen"}, + "floor": {"value": "ground_floor"}, + "preferred_area_id": {"value": area.id}, + "preferred_floor_id": {"value": floor.floor_id}, + }, + text_input="test_text", + context=test_context, + language="*", + assistant="conversation", + device_id=device.id, ) assert response == { "data": { From 01b4589ef67a46f587d422d2f5d5710bcbf61af1 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 3 Jun 2024 17:13:48 +0200 Subject: [PATCH 0154/1445] Tweak light service schema (#118720) --- homeassistant/components/light/services.yaml | 34 ++++++++++++++++++-- homeassistant/components/light/strings.json | 8 +++++ 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/light/services.yaml b/homeassistant/components/light/services.yaml index fb7a1539944..0e75380a40c 100644 --- a/homeassistant/components/light/services.yaml +++ b/homeassistant/components/light/services.yaml @@ -23,6 +23,7 @@ turn_on: - light.ColorMode.RGB - light.ColorMode.RGBW - light.ColorMode.RGBWW + example: "[255, 100, 100]" selector: color_rgb: rgbw_color: @@ -250,6 +251,7 @@ turn_on: - light.ColorMode.RGB - light.ColorMode.RGBW - light.ColorMode.RGBWW + advanced: true selector: color_temp: unit: "mired" @@ -265,7 +267,6 @@ turn_on: - light.ColorMode.RGB - light.ColorMode.RGBW - light.ColorMode.RGBWW - advanced: true selector: color_temp: unit: "kelvin" @@ -419,10 +420,35 @@ toggle: - light.ColorMode.RGB - light.ColorMode.RGBW - light.ColorMode.RGBWW - advanced: true example: "[255, 100, 100]" selector: color_rgb: + rgbw_color: + filter: + attribute: + supported_color_modes: + - light.ColorMode.HS + - light.ColorMode.XY + - light.ColorMode.RGB + - light.ColorMode.RGBW + - light.ColorMode.RGBWW + advanced: true + example: "[255, 100, 100, 50]" + selector: + object: + rgbww_color: + filter: + attribute: + supported_color_modes: + - light.ColorMode.HS + - light.ColorMode.XY + - light.ColorMode.RGB + - light.ColorMode.RGBW + - light.ColorMode.RGBWW + advanced: true + example: "[255, 100, 100, 50, 70]" + selector: + object: color_name: filter: attribute: @@ -625,6 +651,9 @@ toggle: advanced: true selector: color_temp: + unit: "mired" + min: 153 + max: 500 kelvin: filter: attribute: @@ -635,7 +664,6 @@ toggle: - light.ColorMode.RGB - light.ColorMode.RGBW - light.ColorMode.RGBWW - advanced: true selector: color_temp: unit: "kelvin" diff --git a/homeassistant/components/light/strings.json b/homeassistant/components/light/strings.json index 8be954f4653..fbabaff4584 100644 --- a/homeassistant/components/light/strings.json +++ b/homeassistant/components/light/strings.json @@ -342,6 +342,14 @@ "name": "[%key:component::light::services::turn_on::fields::rgb_color::name%]", "description": "[%key:component::light::services::turn_on::fields::rgb_color::description%]" }, + "rgbw_color": { + "name": "[%key:component::light::services::turn_on::fields::rgbw_color::name%]", + "description": "[%key:component::light::services::turn_on::fields::rgbw_color::description%]" + }, + "rgbww_color": { + "name": "[%key:component::light::services::turn_on::fields::rgbww_color::name%]", + "description": "[%key:component::light::services::turn_on::fields::rgbww_color::description%]" + }, "color_name": { "name": "[%key:component::light::services::turn_on::fields::color_name::name%]", "description": "[%key:component::light::services::turn_on::fields::color_name::description%]" From 99e02fe2b2cd1a69aaf0ea8fd3d9e25256debda1 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 3 Jun 2024 17:15:57 +0200 Subject: [PATCH 0155/1445] Remove tag_id from tag store (#118713) --- homeassistant/components/tag/__init__.py | 8 +++++++- tests/components/tag/snapshots/test_init.ambr | 3 +-- tests/components/tag/test_init.py | 4 +--- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/tag/__init__.py b/homeassistant/components/tag/__init__.py index ca0d53be6d0..45266652a47 100644 --- a/homeassistant/components/tag/__init__.py +++ b/homeassistant/components/tag/__init__.py @@ -34,7 +34,7 @@ LAST_SCANNED = "last_scanned" LAST_SCANNED_BY_DEVICE_ID = "last_scanned_by_device_id" STORAGE_KEY = DOMAIN STORAGE_VERSION = 1 -STORAGE_VERSION_MINOR = 2 +STORAGE_VERSION_MINOR = 3 TAG_DATA: HassKey[TagStorageCollection] = HassKey(DOMAIN) SIGNAL_TAG_CHANGED = "signal_tag_changed" @@ -109,6 +109,12 @@ class TagStore(Store[collection.SerializedStorageCollection]): # Copy name in tag store to the entity registry _create_entry(entity_registry, tag[CONF_ID], tag.get(CONF_NAME)) tag["migrated"] = True + if old_major_version == 1 and old_minor_version < 3: + # Version 1.3 removes tag_id from the store + for tag in data["items"]: + if TAG_ID not in tag: + continue + del tag[TAG_ID] if old_major_version > 1: raise NotImplementedError diff --git a/tests/components/tag/snapshots/test_init.ambr b/tests/components/tag/snapshots/test_init.ambr index bfa80d8462e..29a9a2665b8 100644 --- a/tests/components/tag/snapshots/test_init.ambr +++ b/tests/components/tag/snapshots/test_init.ambr @@ -7,7 +7,6 @@ 'id': 'test tag id', 'migrated': True, 'name': 'test tag name', - 'tag_id': 'test tag id', }), dict({ 'device_id': 'some_scanner', @@ -20,7 +19,7 @@ ]), }), 'key': 'tag', - 'minor_version': 2, + 'minor_version': 3, 'version': 1, }) # --- diff --git a/tests/components/tag/test_init.py b/tests/components/tag/test_init.py index 2e4c4b95a16..6f309391d2b 100644 --- a/tests/components/tag/test_init.py +++ b/tests/components/tag/test_init.py @@ -98,9 +98,7 @@ async def test_migration( await client.send_json_auto_id({"type": f"{DOMAIN}/list"}) resp = await client.receive_json() assert resp["success"] - assert resp["result"] == [ - {"id": TEST_TAG_ID, "name": "test tag name", "tag_id": TEST_TAG_ID} - ] + assert resp["result"] == [{"id": TEST_TAG_ID, "name": "test tag name"}] # Scan a new tag await async_scan_tag(hass, "new tag", "some_scanner") From dd90fb15e15efdce23f116b88a34a301d28af46c Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 3 Jun 2024 17:16:48 +0200 Subject: [PATCH 0156/1445] Fix incorrect type hint in dremel_3d_printer tests (#118709) --- tests/components/dremel_3d_printer/test_binary_sensor.py | 8 +++----- tests/components/dremel_3d_printer/test_button.py | 5 ++--- tests/components/dremel_3d_printer/test_sensor.py | 5 ++--- 3 files changed, 7 insertions(+), 11 deletions(-) diff --git a/tests/components/dremel_3d_printer/test_binary_sensor.py b/tests/components/dremel_3d_printer/test_binary_sensor.py index 6581b6ff13d..e430d93b585 100644 --- a/tests/components/dremel_3d_printer/test_binary_sensor.py +++ b/tests/components/dremel_3d_printer/test_binary_sensor.py @@ -1,6 +1,6 @@ """Binary sensor tests for the Dremel 3D Printer integration.""" -from unittest.mock import AsyncMock +import pytest from homeassistant.components.binary_sensor import BinarySensorDeviceClass from homeassistant.components.dremel_3d_printer.const import DOMAIN @@ -11,11 +11,9 @@ from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry +@pytest.mark.usefixtures("connection", "entity_registry_enabled_by_default") async def test_binary_sensors( - hass: HomeAssistant, - connection, - config_entry: MockConfigEntry, - entity_registry_enabled_by_default: AsyncMock, + hass: HomeAssistant, config_entry: MockConfigEntry ) -> None: """Test we get binary sensor data.""" await hass.config_entries.async_setup(config_entry.entry_id) diff --git a/tests/components/dremel_3d_printer/test_button.py b/tests/components/dremel_3d_printer/test_button.py index 48b39b09cf1..d2d63bb6a25 100644 --- a/tests/components/dremel_3d_printer/test_button.py +++ b/tests/components/dremel_3d_printer/test_button.py @@ -1,6 +1,6 @@ """Button tests for the Dremel 3D Printer integration.""" -from unittest.mock import AsyncMock, patch +from unittest.mock import patch import pytest @@ -22,11 +22,10 @@ from tests.common import MockConfigEntry ("resume", "resume"), ], ) +@pytest.mark.usefixtures("connection", "entity_registry_enabled_by_default") async def test_buttons( hass: HomeAssistant, - connection: None, config_entry: MockConfigEntry, - entity_registry_enabled_by_default: AsyncMock, button: str, function: str, ) -> None: diff --git a/tests/components/dremel_3d_printer/test_sensor.py b/tests/components/dremel_3d_printer/test_sensor.py index c1e3a9bc14b..74a4fc32f09 100644 --- a/tests/components/dremel_3d_printer/test_sensor.py +++ b/tests/components/dremel_3d_printer/test_sensor.py @@ -1,9 +1,9 @@ """Sensor tests for the Dremel 3D Printer integration.""" from datetime import datetime -from unittest.mock import AsyncMock from freezegun.api import FrozenDateTimeFactory +import pytest from homeassistant.components.dremel_3d_printer.const import DOMAIN from homeassistant.components.sensor import ( @@ -26,11 +26,10 @@ from homeassistant.util.dt import UTC from tests.common import MockConfigEntry +@pytest.mark.usefixtures("connection", "entity_registry_enabled_by_default") async def test_sensors( hass: HomeAssistant, - connection, config_entry: MockConfigEntry, - entity_registry_enabled_by_default: AsyncMock, freezer: FrozenDateTimeFactory, ) -> None: """Test we get sensor data.""" From 827dfec3116570b066aaf3ee7fb59212d3d0e5ed Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Mon, 3 Jun 2024 16:27:06 +0100 Subject: [PATCH 0157/1445] Bump pytrydan to 0.7.0 (#118726) --- homeassistant/components/v2c/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/v2c/manifest.json b/homeassistant/components/v2c/manifest.json index e26bf80a514..ffe4b52ee6e 100644 --- a/homeassistant/components/v2c/manifest.json +++ b/homeassistant/components/v2c/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/v2c", "iot_class": "local_polling", - "requirements": ["pytrydan==0.6.1"] + "requirements": ["pytrydan==0.7.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 3c8cec53d62..5ca7d450f3a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2352,7 +2352,7 @@ pytradfri[async]==9.0.1 pytrafikverket==0.3.10 # homeassistant.components.v2c -pytrydan==0.6.1 +pytrydan==0.7.0 # homeassistant.components.usb pyudev==0.24.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1c1b1ef1070..30ce8810b79 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1831,7 +1831,7 @@ pytradfri[async]==9.0.1 pytrafikverket==0.3.10 # homeassistant.components.v2c -pytrydan==0.6.1 +pytrydan==0.7.0 # homeassistant.components.usb pyudev==0.24.1 From 32d4431f9ba1d3892cdbf8f145c464e122ca9b5a Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Mon, 3 Jun 2024 17:30:17 +0200 Subject: [PATCH 0158/1445] Rename Discovergy to inexogy (#118724) --- homeassistant/components/discovergy/const.py | 2 +- homeassistant/components/discovergy/manifest.json | 2 +- homeassistant/components/discovergy/strings.json | 2 +- homeassistant/generated/integrations.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/discovergy/const.py b/homeassistant/components/discovergy/const.py index 39ff7a7cd4b..80c3c23a8fa 100644 --- a/homeassistant/components/discovergy/const.py +++ b/homeassistant/components/discovergy/const.py @@ -3,4 +3,4 @@ from __future__ import annotations DOMAIN = "discovergy" -MANUFACTURER = "Discovergy" +MANUFACTURER = "inexogy" diff --git a/homeassistant/components/discovergy/manifest.json b/homeassistant/components/discovergy/manifest.json index f4cf7894eda..1061766a64c 100644 --- a/homeassistant/components/discovergy/manifest.json +++ b/homeassistant/components/discovergy/manifest.json @@ -1,6 +1,6 @@ { "domain": "discovergy", - "name": "Discovergy", + "name": "inexogy", "codeowners": ["@jpbede"], "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/discovergy", diff --git a/homeassistant/components/discovergy/strings.json b/homeassistant/components/discovergy/strings.json index 5147440e1b7..34c21bc1cfe 100644 --- a/homeassistant/components/discovergy/strings.json +++ b/homeassistant/components/discovergy/strings.json @@ -26,7 +26,7 @@ }, "system_health": { "info": { - "api_endpoint_reachable": "Discovergy API endpoint reachable" + "api_endpoint_reachable": "inexogy API endpoint reachable" } }, "entity": { diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 881e001cf12..70995bb3d63 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -1240,7 +1240,7 @@ "iot_class": "cloud_push" }, "discovergy": { - "name": "Discovergy", + "name": "inexogy", "integration_type": "service", "config_flow": true, "iot_class": "cloud_polling" From f178467b0e716b19c9e049c85e9878d8ba84b3f8 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 3 Jun 2024 17:43:18 +0200 Subject: [PATCH 0159/1445] Add type hints for TTS test fixtures (#118704) --- pylint/plugins/hass_enforce_type_hints.py | 5 +++ tests/components/assist_pipeline/conftest.py | 3 +- tests/components/cloud/conftest.py | 5 +-- tests/components/conftest.py | 12 ++++--- tests/components/esphome/conftest.py | 3 +- tests/components/google_translate/test_tts.py | 5 +-- tests/components/marytts/test_tts.py | 3 +- tests/components/microsoft/test_tts.py | 3 +- tests/components/tts/common.py | 12 ++++--- tests/components/tts/conftest.py | 6 ++-- tests/components/tts/test_init.py | 35 ++++++++++--------- tests/components/tts/test_legacy.py | 4 ++- tests/components/voicerss/test_tts.py | 6 ++-- tests/components/voip/test_voip.py | 3 +- tests/components/wyoming/conftest.py | 3 +- tests/components/yandextts/test_tts.py | 6 ++-- 16 files changed, 72 insertions(+), 42 deletions(-) diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index e99c5c1ed39..58baeb6d1cd 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -130,6 +130,7 @@ _TEST_FIXTURES: dict[str, list[str] | str] = { "hass_supervisor_access_token": "str", "hass_supervisor_user": "MockUser", "hass_ws_client": "WebSocketGenerator", + "init_tts_cache_dir_side_effect": "Any", "issue_registry": "IssueRegistry", "legacy_auth": "LegacyApiPasswordAuthProvider", "local_auth": "HassAuthProvider", @@ -141,6 +142,9 @@ _TEST_FIXTURES: dict[str, list[str] | str] = { "mock_get_source_ip": "_patch", "mock_hass_config": "None", "mock_hass_config_yaml": "None", + "mock_tts_cache_dir": "Path", + "mock_tts_get_cache_files": "MagicMock", + "mock_tts_init_cache_dir": "MagicMock", "mock_zeroconf": "MagicMock", "mqtt_client_mock": "MqttMockPahoClient", "mqtt_mock": "MqttMockHAClient", @@ -153,6 +157,7 @@ _TEST_FIXTURES: dict[str, list[str] | str] = { "stub_blueprint_populate": "None", "tmp_path": "Path", "tmpdir": "py.path.local", + "tts_mutagen_mock": "MagicMock", "unused_tcp_port_factory": "Callable[[], int]", "unused_udp_port_factory": "Callable[[], int]", } diff --git a/tests/components/assist_pipeline/conftest.py b/tests/components/assist_pipeline/conftest.py index f4c4ddf1730..69d44341f4a 100644 --- a/tests/components/assist_pipeline/conftest.py +++ b/tests/components/assist_pipeline/conftest.py @@ -3,6 +3,7 @@ from __future__ import annotations from collections.abc import AsyncIterable, Generator +from pathlib import Path from typing import Any from unittest.mock import AsyncMock @@ -34,7 +35,7 @@ _TRANSCRIPT = "test transcript" @pytest.fixture(autouse=True) -def mock_tts_cache_dir_autouse(mock_tts_cache_dir): +def mock_tts_cache_dir_autouse(mock_tts_cache_dir: Path) -> Path: """Mock the TTS cache dir with empty dir.""" return mock_tts_cache_dir diff --git a/tests/components/cloud/conftest.py b/tests/components/cloud/conftest.py index 0147556a888..063aa702c88 100644 --- a/tests/components/cloud/conftest.py +++ b/tests/components/cloud/conftest.py @@ -1,6 +1,7 @@ """Fixtures for cloud tests.""" from collections.abc import AsyncGenerator, Callable, Coroutine +from pathlib import Path from typing import Any from unittest.mock import DEFAULT, MagicMock, PropertyMock, patch @@ -180,13 +181,13 @@ def set_cloud_prefs_fixture( @pytest.fixture(autouse=True) -def mock_tts_cache_dir_autouse(mock_tts_cache_dir): +def mock_tts_cache_dir_autouse(mock_tts_cache_dir: Path) -> Path: """Mock the TTS cache dir with empty dir.""" return mock_tts_cache_dir @pytest.fixture(autouse=True) -def tts_mutagen_mock_fixture_autouse(tts_mutagen_mock): +def tts_mutagen_mock_fixture_autouse(tts_mutagen_mock: MagicMock) -> None: """Mock writing tags.""" diff --git a/tests/components/conftest.py b/tests/components/conftest.py index 8bbb3b83c22..ee5806dd1a4 100644 --- a/tests/components/conftest.py +++ b/tests/components/conftest.py @@ -3,6 +3,7 @@ from __future__ import annotations from collections.abc import Callable, Generator +from pathlib import Path from typing import TYPE_CHECKING, Any from unittest.mock import MagicMock, patch @@ -59,7 +60,7 @@ def stub_blueprint_populate_fixture() -> Generator[None, None, None]: # TTS test fixtures @pytest.fixture(name="mock_tts_get_cache_files") -def mock_tts_get_cache_files_fixture(): +def mock_tts_get_cache_files_fixture() -> Generator[MagicMock, None, None]: """Mock the list TTS cache function.""" from tests.components.tts.common import mock_tts_get_cache_files_fixture_helper @@ -88,8 +89,11 @@ def init_tts_cache_dir_side_effect_fixture() -> Any: @pytest.fixture(name="mock_tts_cache_dir") def mock_tts_cache_dir_fixture( - tmp_path, mock_tts_init_cache_dir, mock_tts_get_cache_files, request -): + tmp_path: Path, + mock_tts_init_cache_dir: MagicMock, + mock_tts_get_cache_files: MagicMock, + request: pytest.FixtureRequest, +) -> Generator[Path, None, None]: """Mock the TTS cache dir with empty dir.""" from tests.components.tts.common import mock_tts_cache_dir_fixture_helper @@ -99,7 +103,7 @@ def mock_tts_cache_dir_fixture( @pytest.fixture(name="tts_mutagen_mock") -def tts_mutagen_mock_fixture(): +def tts_mutagen_mock_fixture() -> Generator[MagicMock, None, None]: """Mock writing tags.""" from tests.components.tts.common import tts_mutagen_mock_fixture_helper diff --git a/tests/components/esphome/conftest.py b/tests/components/esphome/conftest.py index 7b9b050ddb3..91d4f140b12 100644 --- a/tests/components/esphome/conftest.py +++ b/tests/components/esphome/conftest.py @@ -5,6 +5,7 @@ from __future__ import annotations import asyncio from asyncio import Event from collections.abc import Awaitable, Callable +from pathlib import Path from typing import Any from unittest.mock import AsyncMock, Mock, patch @@ -57,7 +58,7 @@ async def load_homeassistant(hass) -> None: @pytest.fixture(autouse=True) -def mock_tts(mock_tts_cache_dir): +def mock_tts(mock_tts_cache_dir: Path) -> None: """Auto mock the tts cache.""" diff --git a/tests/components/google_translate/test_tts.py b/tests/components/google_translate/test_tts.py index a9a80e2e8e6..18fd6a24d3b 100644 --- a/tests/components/google_translate/test_tts.py +++ b/tests/components/google_translate/test_tts.py @@ -4,6 +4,7 @@ from __future__ import annotations from collections.abc import Generator from http import HTTPStatus +from pathlib import Path from typing import Any from unittest.mock import MagicMock, patch @@ -28,12 +29,12 @@ from tests.typing import ClientSessionGenerator @pytest.fixture(autouse=True) -def tts_mutagen_mock_fixture_autouse(tts_mutagen_mock): +def tts_mutagen_mock_fixture_autouse(tts_mutagen_mock: MagicMock) -> None: """Mock writing tags.""" @pytest.fixture(autouse=True) -def mock_tts_cache_dir_autouse(mock_tts_cache_dir): +def mock_tts_cache_dir_autouse(mock_tts_cache_dir: Path) -> Path: """Mock the TTS cache dir with empty dir.""" return mock_tts_cache_dir diff --git a/tests/components/marytts/test_tts.py b/tests/components/marytts/test_tts.py index 953c66f58d1..75784bb56c5 100644 --- a/tests/components/marytts/test_tts.py +++ b/tests/components/marytts/test_tts.py @@ -2,6 +2,7 @@ from http import HTTPStatus import io +from pathlib import Path from unittest.mock import patch import wave @@ -33,7 +34,7 @@ def get_empty_wav() -> bytes: @pytest.fixture(autouse=True) -def mock_tts_cache_dir_autouse(mock_tts_cache_dir): +def mock_tts_cache_dir_autouse(mock_tts_cache_dir: Path) -> Path: """Mock the TTS cache dir with empty dir.""" return mock_tts_cache_dir diff --git a/tests/components/microsoft/test_tts.py b/tests/components/microsoft/test_tts.py index 9ee915c99b6..94d77955f52 100644 --- a/tests/components/microsoft/test_tts.py +++ b/tests/components/microsoft/test_tts.py @@ -1,6 +1,7 @@ """Tests for Microsoft text-to-speech.""" from http import HTTPStatus +from pathlib import Path from unittest.mock import patch from pycsspeechtts import pycsspeechtts @@ -24,7 +25,7 @@ from tests.typing import ClientSessionGenerator @pytest.fixture(autouse=True) -def mock_tts_cache_dir_autouse(mock_tts_cache_dir): +def mock_tts_cache_dir_autouse(mock_tts_cache_dir: Path) -> Path: """Mock the TTS cache dir with empty dir.""" return mock_tts_cache_dir diff --git a/tests/components/tts/common.py b/tests/components/tts/common.py index 5bdc156eacc..87a9993c72a 100644 --- a/tests/components/tts/common.py +++ b/tests/components/tts/common.py @@ -4,6 +4,7 @@ from __future__ import annotations from collections.abc import Generator from http import HTTPStatus +from pathlib import Path from typing import Any from unittest.mock import MagicMock, patch @@ -41,7 +42,7 @@ SUPPORT_LANGUAGES = ["de_CH", "de_DE", "en_GB", "en_US"] TEST_DOMAIN = "test" -def mock_tts_get_cache_files_fixture_helper(): +def mock_tts_get_cache_files_fixture_helper() -> Generator[MagicMock, None, None]: """Mock the list TTS cache function.""" with patch( "homeassistant.components.tts._get_cache_files", return_value={} @@ -66,8 +67,11 @@ def init_tts_cache_dir_side_effect_fixture_helper() -> Any: def mock_tts_cache_dir_fixture_helper( - tmp_path, mock_tts_init_cache_dir, mock_tts_get_cache_files, request -): + tmp_path: Path, + mock_tts_init_cache_dir: MagicMock, + mock_tts_get_cache_files: MagicMock, + request: pytest.FixtureRequest, +) -> Generator[Path, None, None]: """Mock the TTS cache dir with empty dir.""" mock_tts_init_cache_dir.return_value = str(tmp_path) @@ -88,7 +92,7 @@ def mock_tts_cache_dir_fixture_helper( pytest.fail("Test failed, see log for details") -def tts_mutagen_mock_fixture_helper(): +def tts_mutagen_mock_fixture_helper() -> Generator[MagicMock, None, None]: """Mock writing tags.""" with patch( "homeassistant.components.tts.SpeechManager.write_tags", diff --git a/tests/components/tts/conftest.py b/tests/components/tts/conftest.py index a8bdeea5545..7ada92f6088 100644 --- a/tests/components/tts/conftest.py +++ b/tests/components/tts/conftest.py @@ -4,6 +4,8 @@ From http://doc.pytest.org/en/latest/example/simple.html#making-test-result-info """ from collections.abc import Generator +from pathlib import Path +from unittest.mock import MagicMock import pytest @@ -37,13 +39,13 @@ def pytest_runtest_makereport(item, call): @pytest.fixture(autouse=True, name="mock_tts_cache_dir") -def mock_tts_cache_dir_fixture_autouse(mock_tts_cache_dir): +def mock_tts_cache_dir_fixture_autouse(mock_tts_cache_dir: Path) -> Path: """Mock the TTS cache dir with empty dir.""" return mock_tts_cache_dir @pytest.fixture(autouse=True) -def tts_mutagen_mock_fixture_autouse(tts_mutagen_mock): +def tts_mutagen_mock_fixture_autouse(tts_mutagen_mock: MagicMock) -> None: """Mock writing tags.""" diff --git a/tests/components/tts/test_init.py b/tests/components/tts/test_init.py index 7d308ec0b23..e0354170b06 100644 --- a/tests/components/tts/test_init.py +++ b/tests/components/tts/test_init.py @@ -2,6 +2,7 @@ import asyncio from http import HTTPStatus +from pathlib import Path from typing import Any from unittest.mock import MagicMock, patch @@ -187,7 +188,7 @@ async def test_setup_component_no_access_cache_folder( ) async def test_service( hass: HomeAssistant, - mock_tts_cache_dir, + mock_tts_cache_dir: Path, setup: str, tts_service: str, service_data: dict[str, Any], @@ -248,7 +249,7 @@ async def test_service( ) async def test_service_default_language( hass: HomeAssistant, - mock_tts_cache_dir, + mock_tts_cache_dir: Path, setup: str, tts_service: str, service_data: dict[str, Any], @@ -309,7 +310,7 @@ async def test_service_default_language( ) async def test_service_default_special_language( hass: HomeAssistant, - mock_tts_cache_dir, + mock_tts_cache_dir: Path, setup: str, tts_service: str, service_data: dict[str, Any], @@ -366,7 +367,7 @@ async def test_service_default_special_language( ) async def test_service_language( hass: HomeAssistant, - mock_tts_cache_dir, + mock_tts_cache_dir: Path, setup: str, tts_service: str, service_data: dict[str, Any], @@ -423,7 +424,7 @@ async def test_service_language( ) async def test_service_wrong_language( hass: HomeAssistant, - mock_tts_cache_dir, + mock_tts_cache_dir: Path, setup: str, tts_service: str, service_data: dict[str, Any], @@ -477,7 +478,7 @@ async def test_service_wrong_language( ) async def test_service_options( hass: HomeAssistant, - mock_tts_cache_dir, + mock_tts_cache_dir: Path, setup: str, tts_service: str, service_data: dict[str, Any], @@ -561,7 +562,7 @@ class MockEntityWithDefaults(MockTTSEntity): ) async def test_service_default_options( hass: HomeAssistant, - mock_tts_cache_dir, + mock_tts_cache_dir: Path, setup: str, tts_service: str, service_data: dict[str, Any], @@ -629,7 +630,7 @@ async def test_service_default_options( ) async def test_merge_default_service_options( hass: HomeAssistant, - mock_tts_cache_dir, + mock_tts_cache_dir: Path, setup: str, tts_service: str, service_data: dict[str, Any], @@ -696,7 +697,7 @@ async def test_merge_default_service_options( ) async def test_service_wrong_options( hass: HomeAssistant, - mock_tts_cache_dir, + mock_tts_cache_dir: Path, setup: str, tts_service: str, service_data: dict[str, Any], @@ -752,7 +753,7 @@ async def test_service_wrong_options( ) async def test_service_clear_cache( hass: HomeAssistant, - mock_tts_cache_dir, + mock_tts_cache_dir: Path, setup: str, tts_service: str, service_data: dict[str, Any], @@ -814,7 +815,7 @@ async def test_service_clear_cache( async def test_service_receive_voice( hass: HomeAssistant, hass_client: ClientSessionGenerator, - mock_tts_cache_dir, + mock_tts_cache_dir: Path, setup: str, tts_service: str, service_data: dict[str, Any], @@ -886,7 +887,7 @@ async def test_service_receive_voice( async def test_service_receive_voice_german( hass: HomeAssistant, hass_client: ClientSessionGenerator, - mock_tts_cache_dir, + mock_tts_cache_dir: Path, setup: str, tts_service: str, service_data: dict[str, Any], @@ -994,7 +995,7 @@ async def test_web_view_wrong_filename( ) async def test_service_without_cache( hass: HomeAssistant, - mock_tts_cache_dir, + mock_tts_cache_dir: Path, setup: str, tts_service: str, service_data: dict[str, Any], @@ -1042,7 +1043,7 @@ class MockEntityBoom(MockTTSEntity): @pytest.mark.parametrize("mock_provider", [MockProviderBoom(DEFAULT_LANG)]) async def test_setup_legacy_cache_dir( hass: HomeAssistant, - mock_tts_cache_dir, + mock_tts_cache_dir: Path, mock_provider: MockProvider, ) -> None: """Set up a TTS platform with cache and call service without cache.""" @@ -1078,7 +1079,7 @@ async def test_setup_legacy_cache_dir( @pytest.mark.parametrize("mock_tts_entity", [MockEntityBoom(DEFAULT_LANG)]) async def test_setup_cache_dir( hass: HomeAssistant, - mock_tts_cache_dir, + mock_tts_cache_dir: Path, mock_tts_entity: MockTTSEntity, ) -> None: """Set up a TTS platform with cache and call service without cache.""" @@ -1185,7 +1186,7 @@ async def test_service_get_tts_error( async def test_load_cache_legacy_retrieve_without_mem_cache( hass: HomeAssistant, mock_provider: MockProvider, - mock_tts_cache_dir, + mock_tts_cache_dir: Path, hass_client: ClientSessionGenerator, ) -> None: """Set up component and load cache and get without mem cache.""" @@ -1211,7 +1212,7 @@ async def test_load_cache_legacy_retrieve_without_mem_cache( async def test_load_cache_retrieve_without_mem_cache( hass: HomeAssistant, mock_tts_entity: MockTTSEntity, - mock_tts_cache_dir, + mock_tts_cache_dir: Path, hass_client: ClientSessionGenerator, ) -> None: """Set up component and load cache and get without mem cache.""" diff --git a/tests/components/tts/test_legacy.py b/tests/components/tts/test_legacy.py index 59194f50d93..05bb6dec10f 100644 --- a/tests/components/tts/test_legacy.py +++ b/tests/components/tts/test_legacy.py @@ -2,6 +2,8 @@ from __future__ import annotations +from pathlib import Path + import pytest from homeassistant.components.media_player import ( @@ -139,7 +141,7 @@ async def test_platform_setup_with_error( async def test_service_without_cache_config( - hass: HomeAssistant, mock_tts_cache_dir, mock_tts + hass: HomeAssistant, mock_tts_cache_dir: Path, mock_tts ) -> None: """Set up a TTS platform without cache.""" calls = async_mock_service(hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) diff --git a/tests/components/voicerss/test_tts.py b/tests/components/voicerss/test_tts.py index d1e7ba3c62f..1a2ad002586 100644 --- a/tests/components/voicerss/test_tts.py +++ b/tests/components/voicerss/test_tts.py @@ -1,6 +1,8 @@ """The tests for the VoiceRSS speech platform.""" from http import HTTPStatus +from pathlib import Path +from unittest.mock import MagicMock import pytest @@ -29,12 +31,12 @@ FORM_DATA = { @pytest.fixture(autouse=True) -def tts_mutagen_mock_fixture_autouse(tts_mutagen_mock): +def tts_mutagen_mock_fixture_autouse(tts_mutagen_mock: MagicMock) -> None: """Mock writing tags.""" @pytest.fixture(autouse=True) -def mock_tts_cache_dir_autouse(mock_tts_cache_dir): +def mock_tts_cache_dir_autouse(mock_tts_cache_dir: Path) -> Path: """Mock the TTS cache dir with empty dir.""" return mock_tts_cache_dir diff --git a/tests/components/voip/test_voip.py b/tests/components/voip/test_voip.py index f5c5fde2518..6c292241237 100644 --- a/tests/components/voip/test_voip.py +++ b/tests/components/voip/test_voip.py @@ -2,6 +2,7 @@ import asyncio import io +from pathlib import Path import time from unittest.mock import AsyncMock, Mock, patch import wave @@ -18,7 +19,7 @@ _MEDIA_ID = "12345" @pytest.fixture(autouse=True) -def mock_tts_cache_dir_autouse(mock_tts_cache_dir): +def mock_tts_cache_dir_autouse(mock_tts_cache_dir: Path) -> Path: """Mock the TTS cache dir with empty dir.""" return mock_tts_cache_dir diff --git a/tests/components/wyoming/conftest.py b/tests/components/wyoming/conftest.py index 4be12312c7a..4ba0c6312cb 100644 --- a/tests/components/wyoming/conftest.py +++ b/tests/components/wyoming/conftest.py @@ -1,6 +1,7 @@ """Common fixtures for the Wyoming tests.""" from collections.abc import Generator +from pathlib import Path from unittest.mock import AsyncMock, patch import pytest @@ -18,7 +19,7 @@ from tests.common import MockConfigEntry @pytest.fixture(autouse=True) -def mock_tts_cache_dir_autouse(mock_tts_cache_dir): +def mock_tts_cache_dir_autouse(mock_tts_cache_dir: Path) -> Path: """Mock the TTS cache dir with empty dir.""" return mock_tts_cache_dir diff --git a/tests/components/yandextts/test_tts.py b/tests/components/yandextts/test_tts.py index 6a4b7e11ce6..496c187469a 100644 --- a/tests/components/yandextts/test_tts.py +++ b/tests/components/yandextts/test_tts.py @@ -1,6 +1,8 @@ """The tests for the Yandex SpeechKit speech platform.""" from http import HTTPStatus +from pathlib import Path +from unittest.mock import MagicMock import pytest @@ -22,12 +24,12 @@ URL = "https://tts.voicetech.yandex.net/generate?" @pytest.fixture(autouse=True) -def tts_mutagen_mock_fixture_autouse(tts_mutagen_mock): +def tts_mutagen_mock_fixture_autouse(tts_mutagen_mock: MagicMock) -> None: """Mock writing tags.""" @pytest.fixture(autouse=True) -def mock_tts_cache_dir_autouse(mock_tts_cache_dir): +def mock_tts_cache_dir_autouse(mock_tts_cache_dir: Path) -> Path: """Mock the TTS cache dir with empty dir.""" return mock_tts_cache_dir From 5d594a509c5ebb6905550ec7b6202f6357b7ca81 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 3 Jun 2024 17:44:13 +0200 Subject: [PATCH 0160/1445] Add type hints for MockAgent in conversation tests (#118701) --- pylint/plugins/hass_enforce_type_hints.py | 1 + tests/components/conversation/conftest.py | 2 +- tests/components/conversation/test_init.py | 12 ++++++------ tests/components/mobile_app/test_webhook.py | 3 ++- 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index 58baeb6d1cd..bd208808366 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -138,6 +138,7 @@ _TEST_FIXTURES: dict[str, list[str] | str] = { "mock_bleak_scanner_start": "MagicMock", "mock_bluetooth": "None", "mock_bluetooth_adapters": "None", + "mock_conversation_agent": "MockAgent", "mock_device_tracker_conf": "list[Device]", "mock_get_source_ip": "_patch", "mock_hass_config": "None", diff --git a/tests/components/conversation/conftest.py b/tests/components/conversation/conftest.py index 4801e506460..6575ab2ac98 100644 --- a/tests/components/conversation/conftest.py +++ b/tests/components/conversation/conftest.py @@ -16,7 +16,7 @@ from tests.common import MockConfigEntry @pytest.fixture -def mock_agent_support_all(hass: HomeAssistant): +def mock_agent_support_all(hass: HomeAssistant) -> MockAgent: """Mock agent that supports all languages.""" entry = MockConfigEntry(entry_id="mock-entry-support-all") entry.add_to_hass(hass) diff --git a/tests/components/conversation/test_init.py b/tests/components/conversation/test_init.py index e1e6683f142..415c80fffbc 100644 --- a/tests/components/conversation/test_init.py +++ b/tests/components/conversation/test_init.py @@ -24,7 +24,7 @@ from homeassistant.helpers import ( ) from homeassistant.setup import async_setup_component -from . import expose_entity, expose_new +from . import MockAgent, expose_entity, expose_new from tests.common import ( MockConfigEntry, @@ -94,7 +94,7 @@ async def test_http_processing_intent_target_ha_agent( init_components, hass_client: ClientSessionGenerator, hass_admin_user: MockUser, - mock_conversation_agent, + mock_conversation_agent: MockAgent, entity_registry: er.EntityRegistry, snapshot: SnapshotAssertion, ) -> None: @@ -663,7 +663,7 @@ async def test_custom_agent( hass: HomeAssistant, hass_client: ClientSessionGenerator, hass_admin_user: MockUser, - mock_conversation_agent, + mock_conversation_agent: MockAgent, snapshot: SnapshotAssertion, ) -> None: """Test a custom conversation agent.""" @@ -1081,8 +1081,8 @@ async def test_agent_id_validator_invalid_agent( async def test_get_agent_list( hass: HomeAssistant, init_components, - mock_conversation_agent, - mock_agent_support_all, + mock_conversation_agent: MockAgent, + mock_agent_support_all: MockAgent, hass_ws_client: WebSocketGenerator, snapshot: SnapshotAssertion, ) -> None: @@ -1139,7 +1139,7 @@ async def test_get_agent_list( async def test_get_agent_info( hass: HomeAssistant, init_components, - mock_conversation_agent, + mock_conversation_agent: MockAgent, snapshot: SnapshotAssertion, ) -> None: """Test get agent info.""" diff --git a/tests/components/mobile_app/test_webhook.py b/tests/components/mobile_app/test_webhook.py index a9346e3728c..9f521cafd38 100644 --- a/tests/components/mobile_app/test_webhook.py +++ b/tests/components/mobile_app/test_webhook.py @@ -24,6 +24,7 @@ from homeassistant.setup import async_setup_component from .const import CALL_SERVICE, FIRE_EVENT, REGISTER_CLEARTEXT, RENDER_TEMPLATE, UPDATE from tests.common import async_capture_events, async_mock_service +from tests.components.conversation import MockAgent @pytest.fixture @@ -1023,7 +1024,7 @@ async def test_webhook_handle_conversation_process( homeassistant, create_registrations, webhook_client, - mock_conversation_agent, + mock_conversation_agent: MockAgent, ) -> None: """Test that we can converse.""" webhook_client.server.app.router._frozen = False From 099ad770781227c8cfbdabd901e34ae4acc370a2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 3 Jun 2024 11:19:00 -0500 Subject: [PATCH 0161/1445] Migrate recorder instance to use HassKey (#118673) --- homeassistant/components/recorder/__init__.py | 4 ++-- homeassistant/components/recorder/const.py | 10 +++++++++- homeassistant/components/recorder/util.py | 10 ++++------ 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index 26b9f471b9e..a5a49e7df60 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -129,8 +129,7 @@ def is_entity_recorded(hass: HomeAssistant, entity_id: str) -> bool: """ if DATA_INSTANCE not in hass.data: return False - instance = get_instance(hass) - return instance.entity_filter(entity_id) + return hass.data[DATA_INSTANCE].entity_filter(entity_id) async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: @@ -165,6 +164,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: entity_filter=entity_filter, exclude_event_types=exclude_event_types, ) + get_instance.cache_clear() instance.async_initialize() instance.async_register() instance.start() diff --git a/homeassistant/components/recorder/const.py b/homeassistant/components/recorder/const.py index 1869bb32239..97418ee364a 100644 --- a/homeassistant/components/recorder/const.py +++ b/homeassistant/components/recorder/const.py @@ -1,6 +1,7 @@ """Recorder constants.""" from enum import StrEnum +from typing import TYPE_CHECKING from homeassistant.const import ( ATTR_ATTRIBUTION, @@ -10,8 +11,15 @@ from homeassistant.const import ( EVENT_RECORDER_HOURLY_STATISTICS_GENERATED, # noqa: F401 ) from homeassistant.helpers.json import JSON_DUMP # noqa: F401 +from homeassistant.util.hass_dict import HassKey + +if TYPE_CHECKING: + from .core import Recorder # noqa: F401 + + +DATA_INSTANCE: HassKey["Recorder"] = HassKey("recorder_instance") + -DATA_INSTANCE = "recorder_instance" SQLITE_URL_PREFIX = "sqlite://" MARIADB_URL_PREFIX = "mariadb://" MARIADB_PYMYSQL_URL_PREFIX = "mariadb+pymysql://" diff --git a/homeassistant/components/recorder/util.py b/homeassistant/components/recorder/util.py index 667150d5a15..939a016c960 100644 --- a/homeassistant/components/recorder/util.py +++ b/homeassistant/components/recorder/util.py @@ -739,8 +739,7 @@ def async_migration_in_progress(hass: HomeAssistant) -> bool: """ if DATA_INSTANCE not in hass.data: return False - instance = get_instance(hass) - return instance.migration_in_progress + return hass.data[DATA_INSTANCE].migration_in_progress def async_migration_is_live(hass: HomeAssistant) -> bool: @@ -751,8 +750,7 @@ def async_migration_is_live(hass: HomeAssistant) -> bool: """ if DATA_INSTANCE not in hass.data: return False - instance: Recorder = hass.data[DATA_INSTANCE] - return instance.migration_is_live + return hass.data[DATA_INSTANCE].migration_is_live def second_sunday(year: int, month: int) -> date: @@ -771,10 +769,10 @@ def is_second_sunday(date_time: datetime) -> bool: return bool(second_sunday(date_time.year, date_time.month).day == date_time.day) +@functools.lru_cache(maxsize=1) def get_instance(hass: HomeAssistant) -> Recorder: """Get the recorder instance.""" - instance: Recorder = hass.data[DATA_INSTANCE] - return instance + return hass.data[DATA_INSTANCE] PERIOD_SCHEMA = vol.Schema( From 9cb113e5d44c11bade4b243353befa3b89d29000 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 3 Jun 2024 11:19:19 -0500 Subject: [PATCH 0162/1445] Convert mqtt to use a timer instead of task sleep loop (#118666) --- homeassistant/components/mqtt/client.py | 41 +++++++++++++------------ 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/mqtt/client.py b/homeassistant/components/mqtt/client.py index 0871a0419e5..d36670baef1 100644 --- a/homeassistant/components/mqtt/client.py +++ b/homeassistant/components/mqtt/client.py @@ -465,7 +465,7 @@ class MQTT: self._subscribe_debouncer = EnsureJobAfterCooldown( INITIAL_SUBSCRIBE_COOLDOWN, self._async_perform_subscriptions ) - self._misc_task: asyncio.Task | None = None + self._misc_timer: asyncio.TimerHandle | None = None self._reconnect_task: asyncio.Task | None = None self._should_reconnect: bool = True self._available_future: asyncio.Future[bool] | None = None @@ -563,14 +563,6 @@ class MQTT: self._mqttc = mqttc - async def _misc_loop(self) -> None: - """Start the MQTT client misc loop.""" - # pylint: disable=import-outside-toplevel - import paho.mqtt.client as mqtt - - while self._mqttc.loop_misc() == mqtt.MQTT_ERR_SUCCESS: - await asyncio.sleep(1) - @callback def _async_reader_callback(self, client: mqtt.Client) -> None: """Handle reading data from the socket.""" @@ -578,13 +570,22 @@ class MQTT: self._async_on_disconnect(status) @callback - def _async_start_misc_loop(self) -> None: - """Start the misc loop.""" - if self._misc_task is None or self._misc_task.done(): - _LOGGER.debug("%s: Starting client misc loop", self.config_entry.title) - self._misc_task = self.config_entry.async_create_background_task( - self.hass, self._misc_loop(), name="mqtt misc loop" - ) + def _async_start_misc_periodic(self) -> None: + """Start the misc periodic.""" + assert self._misc_timer is None, "Misc periodic already started" + _LOGGER.debug("%s: Starting client misc loop", self.config_entry.title) + # pylint: disable=import-outside-toplevel + import paho.mqtt.client as mqtt + + # Inner function to avoid having to check late import + # each time the function is called. + @callback + def _async_misc() -> None: + """Start the MQTT client misc loop.""" + if self._mqttc.loop_misc() == mqtt.MQTT_ERR_SUCCESS: + self._misc_timer = self.loop.call_at(self.loop.time() + 1, _async_misc) + + self._misc_timer = self.loop.call_at(self.loop.time() + 1, _async_misc) def _increase_socket_buffer_size(self, sock: SocketType) -> None: """Increase the socket buffer size.""" @@ -635,7 +636,8 @@ class MQTT: if fileno > -1: self._increase_socket_buffer_size(sock) self.loop.add_reader(sock, partial(self._async_reader_callback, client)) - self._async_start_misc_loop() + if not self._misc_timer: + self._async_start_misc_periodic() # Try to consume the buffer right away so it doesn't fill up # since add_reader will wait for the next loop iteration self._async_reader_callback(client) @@ -652,8 +654,9 @@ class MQTT: self._async_connection_result(False) if fileno > -1: self.loop.remove_reader(sock) - if self._misc_task is not None and not self._misc_task.done(): - self._misc_task.cancel() + if self._misc_timer: + self._misc_timer.cancel() + self._misc_timer = None @callback def _async_writer_callback(self, client: mqtt.Client) -> None: From ca1ed6f6101ade4bd9263d35d6c0b1fc642066f3 Mon Sep 17 00:00:00 2001 From: mkmer Date: Mon, 3 Jun 2024 13:13:18 -0400 Subject: [PATCH 0163/1445] Remove unintended translation key from blink (#118712) --- homeassistant/components/blink/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/blink/strings.json b/homeassistant/components/blink/strings.json index 8a743e98401..8f94f8c9543 100644 --- a/homeassistant/components/blink/strings.json +++ b/homeassistant/components/blink/strings.json @@ -85,7 +85,7 @@ }, "save_recent_clips": { "name": "Save recent clips", - "description": "Saves all recent video clips to local directory with file pattern \"%Y%m%d_%H%M%S_{name}.mp4\".", + "description": "Saves all recent video clips to local directory with file pattern \"%Y%m%d_%H%M%S_[camera name].mp4\".", "fields": { "file_path": { "name": "Output directory", From 91ca7db02f3d484a7802965a08ddf65473392532 Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Mon, 3 Jun 2024 18:22:00 +0100 Subject: [PATCH 0164/1445] Address reviews comments in #117147 (#118714) --- homeassistant/components/v2c/sensor.py | 8 +- homeassistant/components/v2c/strings.json | 10 +- .../components/v2c/snapshots/test_sensor.ambr | 529 ++++-------------- tests/components/v2c/test_sensor.py | 4 +- 4 files changed, 133 insertions(+), 418 deletions(-) diff --git a/homeassistant/components/v2c/sensor.py b/homeassistant/components/v2c/sensor.py index 01b89adea4d..799d6c3d03c 100644 --- a/homeassistant/components/v2c/sensor.py +++ b/homeassistant/components/v2c/sensor.py @@ -35,7 +35,7 @@ class V2CSensorEntityDescription(SensorEntityDescription): value_fn: Callable[[TrydanData], StateType] -_SLAVE_ERROR_OPTIONS = [error.name.lower() for error in SlaveCommunicationState] +_METER_ERROR_OPTIONS = [error.name.lower() for error in SlaveCommunicationState] TRYDAN_SENSORS = ( V2CSensorEntityDescription( @@ -80,12 +80,12 @@ TRYDAN_SENSORS = ( value_fn=lambda evse_data: evse_data.fv_power, ), V2CSensorEntityDescription( - key="slave_error", - translation_key="slave_error", + key="meter_error", + translation_key="meter_error", value_fn=lambda evse_data: evse_data.slave_error.name.lower(), entity_registry_enabled_default=False, device_class=SensorDeviceClass.ENUM, - options=_SLAVE_ERROR_OPTIONS, + options=_METER_ERROR_OPTIONS, ), V2CSensorEntityDescription( key="battery_power", diff --git a/homeassistant/components/v2c/strings.json b/homeassistant/components/v2c/strings.json index bafbbe36e0c..bc0d870b635 100644 --- a/homeassistant/components/v2c/strings.json +++ b/homeassistant/components/v2c/strings.json @@ -54,18 +54,18 @@ "battery_power": { "name": "Battery power" }, - "slave_error": { - "name": "Slave error", + "meter_error": { + "name": "Meter error", "state": { "no_error": "No error", "communication": "Communication", "reading": "Reading", - "slave": "Slave", + "slave": "Meter", "waiting_wifi": "Waiting for Wi-Fi", "waiting_communication": "Waiting communication", "wrong_ip": "Wrong IP", - "slave_not_found": "Slave not found", - "wrong_slave": "Wrong slave", + "slave_not_found": "Meter not found", + "wrong_slave": "Wrong Meter", "no_response": "No response", "clamp_not_connected": "Clamp not connected", "illegal_function": "Illegal function", diff --git a/tests/components/v2c/snapshots/test_sensor.ambr b/tests/components/v2c/snapshots/test_sensor.ambr index 0ef9bfe8429..859e5f83e15 100644 --- a/tests/components/v2c/snapshots/test_sensor.ambr +++ b/tests/components/v2c/snapshots/test_sensor.ambr @@ -1,289 +1,4 @@ # serializer version: 1 -# name: test_sensor - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'power', - 'friendly_name': 'EVSE 1.1.1.1 Photovoltaic power', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.evse_1_1_1_1_photovoltaic_power', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '0.0', - }) - list([ - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.evse_1_1_1_1_charge_power', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': 'mdi:ev-station', - 'original_name': 'Charge power', - 'platform': 'v2c', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'charge_power', - 'unique_id': '45a36e55aaddb2137c5f6602e0c38e72_charge_power', - 'unit_of_measurement': , - }), - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.evse_1_1_1_1_charge_energy', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Charge energy', - 'platform': 'v2c', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'charge_energy', - 'unique_id': '45a36e55aaddb2137c5f6602e0c38e72_charge_energy', - 'unit_of_measurement': , - }), - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.evse_1_1_1_1_charge_time', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Charge time', - 'platform': 'v2c', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'charge_time', - 'unique_id': '45a36e55aaddb2137c5f6602e0c38e72_charge_time', - 'unit_of_measurement': , - }), - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.evse_1_1_1_1_house_power', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'House power', - 'platform': 'v2c', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'house_power', - 'unique_id': '45a36e55aaddb2137c5f6602e0c38e72_house_power', - 'unit_of_measurement': , - }), - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.evse_1_1_1_1_photovoltaic_power', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Photovoltaic power', - 'platform': 'v2c', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'fv_power', - 'unique_id': '45a36e55aaddb2137c5f6602e0c38e72_fv_power', - 'unit_of_measurement': , - }), - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'options': list([ - 'no_error', - 'communication', - 'reading', - 'slave', - 'waiting_wifi', - 'waiting_communication', - 'wrong_ip', - 'slave_not_found', - 'wrong_slave', - 'no_response', - 'clamp_not_connected', - 'illegal_function', - 'illegal_data_address', - 'illegal_data_value', - 'server_device_failure', - 'acknowledge', - 'server_device_busy', - 'negative_acknowledge', - 'memory_parity_error', - 'gateway_path_unavailable', - 'gateway_target_no_resp', - 'server_rtu_inactive244_timeout', - 'invalid_server', - 'crc_error', - 'fc_missmatch', - 'server_id_missmatch', - 'packet_length_error', - 'parameter_count_error', - 'parameter_limit_error', - 'request_queue_full', - 'illegal_ip_or_port', - 'ip_connection_failed', - 'tcp_head_missmatch', - 'empty_message', - 'undefined_error', - ]), - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': , - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.evse_1_1_1_1_slave_error', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Slave error', - 'platform': 'v2c', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'slave_error', - 'unique_id': '45a36e55aaddb2137c5f6602e0c38e72_slave_error', - 'unit_of_measurement': None, - }), - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': , - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.evse_1_1_1_1_battery_power', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Battery power', - 'platform': 'v2c', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'battery_power', - 'unique_id': '45a36e55aaddb2137c5f6602e0c38e72_battery_power', - 'unit_of_measurement': , - }), - ]) -# --- # name: test_sensor[sensor.evse_1_1_1_1_battery_power-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -540,6 +255,128 @@ 'state': '0.0', }) # --- +# name: test_sensor[sensor.evse_1_1_1_1_meter_error-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'no_error', + 'communication', + 'reading', + 'slave', + 'waiting_wifi', + 'waiting_communication', + 'wrong_ip', + 'slave_not_found', + 'wrong_slave', + 'no_response', + 'clamp_not_connected', + 'illegal_function', + 'illegal_data_address', + 'illegal_data_value', + 'server_device_failure', + 'acknowledge', + 'server_device_busy', + 'negative_acknowledge', + 'memory_parity_error', + 'gateway_path_unavailable', + 'gateway_target_no_resp', + 'server_rtu_inactive244_timeout', + 'invalid_server', + 'crc_error', + 'fc_mismatch', + 'server_id_mismatch', + 'packet_length_error', + 'parameter_count_error', + 'parameter_limit_error', + 'request_queue_full', + 'illegal_ip_or_port', + 'ip_connection_failed', + 'tcp_head_mismatch', + 'empty_message', + 'undefined_error', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.evse_1_1_1_1_meter_error', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Meter error', + 'platform': 'v2c', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'meter_error', + 'unique_id': 'da58ee91f38c2406c2a36d0a1a7f8569_meter_error', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[sensor.evse_1_1_1_1_meter_error-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'EVSE 1.1.1.1 Meter error', + 'options': list([ + 'no_error', + 'communication', + 'reading', + 'slave', + 'waiting_wifi', + 'waiting_communication', + 'wrong_ip', + 'slave_not_found', + 'wrong_slave', + 'no_response', + 'clamp_not_connected', + 'illegal_function', + 'illegal_data_address', + 'illegal_data_value', + 'server_device_failure', + 'acknowledge', + 'server_device_busy', + 'negative_acknowledge', + 'memory_parity_error', + 'gateway_path_unavailable', + 'gateway_target_no_resp', + 'server_rtu_inactive244_timeout', + 'invalid_server', + 'crc_error', + 'fc_mismatch', + 'server_id_mismatch', + 'packet_length_error', + 'parameter_count_error', + 'parameter_limit_error', + 'request_queue_full', + 'illegal_ip_or_port', + 'ip_connection_failed', + 'tcp_head_mismatch', + 'empty_message', + 'undefined_error', + ]), + }), + 'context': , + 'entity_id': 'sensor.evse_1_1_1_1_meter_error', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'waiting_wifi', + }) +# --- # name: test_sensor[sensor.evse_1_1_1_1_photovoltaic_power-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -591,125 +428,3 @@ 'state': '0.0', }) # --- -# name: test_sensor[sensor.evse_1_1_1_1_slave_error-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'options': list([ - 'no_error', - 'communication', - 'reading', - 'slave', - 'waiting_wifi', - 'waiting_communication', - 'wrong_ip', - 'slave_not_found', - 'wrong_slave', - 'no_response', - 'clamp_not_connected', - 'illegal_function', - 'illegal_data_address', - 'illegal_data_value', - 'server_device_failure', - 'acknowledge', - 'server_device_busy', - 'negative_acknowledge', - 'memory_parity_error', - 'gateway_path_unavailable', - 'gateway_target_no_resp', - 'server_rtu_inactive244_timeout', - 'invalid_server', - 'crc_error', - 'fc_mismatch', - 'server_id_mismatch', - 'packet_length_error', - 'parameter_count_error', - 'parameter_limit_error', - 'request_queue_full', - 'illegal_ip_or_port', - 'ip_connection_failed', - 'tcp_head_mismatch', - 'empty_message', - 'undefined_error', - ]), - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.evse_1_1_1_1_slave_error', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Slave error', - 'platform': 'v2c', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'slave_error', - 'unique_id': 'da58ee91f38c2406c2a36d0a1a7f8569_slave_error', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensor[sensor.evse_1_1_1_1_slave_error-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'enum', - 'friendly_name': 'EVSE 1.1.1.1 Slave error', - 'options': list([ - 'no_error', - 'communication', - 'reading', - 'slave', - 'waiting_wifi', - 'waiting_communication', - 'wrong_ip', - 'slave_not_found', - 'wrong_slave', - 'no_response', - 'clamp_not_connected', - 'illegal_function', - 'illegal_data_address', - 'illegal_data_value', - 'server_device_failure', - 'acknowledge', - 'server_device_busy', - 'negative_acknowledge', - 'memory_parity_error', - 'gateway_path_unavailable', - 'gateway_target_no_resp', - 'server_rtu_inactive244_timeout', - 'invalid_server', - 'crc_error', - 'fc_mismatch', - 'server_id_mismatch', - 'packet_length_error', - 'parameter_count_error', - 'parameter_limit_error', - 'request_queue_full', - 'illegal_ip_or_port', - 'ip_connection_failed', - 'tcp_head_mismatch', - 'empty_message', - 'undefined_error', - ]), - }), - 'context': , - 'entity_id': 'sensor.evse_1_1_1_1_slave_error', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'waiting_wifi', - }) -# --- diff --git a/tests/components/v2c/test_sensor.py b/tests/components/v2c/test_sensor.py index a4a7fe6ca34..93f7e36327c 100644 --- a/tests/components/v2c/test_sensor.py +++ b/tests/components/v2c/test_sensor.py @@ -26,7 +26,7 @@ async def test_sensor( await init_integration(hass, mock_config_entry) await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) - from homeassistant.components.v2c.sensor import _SLAVE_ERROR_OPTIONS + from homeassistant.components.v2c.sensor import _METER_ERROR_OPTIONS assert [ "no_error", @@ -64,4 +64,4 @@ async def test_sensor( "tcp_head_mismatch", "empty_message", "undefined_error", - ] == _SLAVE_ERROR_OPTIONS + ] == _METER_ERROR_OPTIONS From 16485af7fc6ce9fa02478fd30a2526661e718201 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 3 Jun 2024 19:23:07 +0200 Subject: [PATCH 0165/1445] Configure device in airgradient config flow (#118699) --- .../components/airgradient/config_flow.py | 20 +++++-- .../components/airgradient/strings.json | 3 +- .../airgradient/test_config_flow.py | 56 ++++++++++++++++++- 3 files changed, 69 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/airgradient/config_flow.py b/homeassistant/components/airgradient/config_flow.py index c02ec2a469f..c7b617de272 100644 --- a/homeassistant/components/airgradient/config_flow.py +++ b/homeassistant/components/airgradient/config_flow.py @@ -2,7 +2,7 @@ from typing import Any -from airgradient import AirGradientClient, AirGradientError +from airgradient import AirGradientClient, AirGradientError, ConfigurationControl import voluptuous as vol from homeassistant.components import zeroconf @@ -19,6 +19,14 @@ class AirGradientConfigFlow(ConfigFlow, domain=DOMAIN): def __init__(self) -> None: """Initialize the config flow.""" self.data: dict[str, Any] = {} + self.client: AirGradientClient | None = None + + async def set_configuration_source(self) -> None: + """Set configuration source to local if it hasn't been set yet.""" + assert self.client + config = await self.client.get_config() + if config.configuration_control is ConfigurationControl.BOTH: + await self.client.set_configuration_control(ConfigurationControl.LOCAL) async def async_step_zeroconf( self, discovery_info: zeroconf.ZeroconfServiceInfo @@ -31,8 +39,8 @@ class AirGradientConfigFlow(ConfigFlow, domain=DOMAIN): self._abort_if_unique_id_configured(updates={CONF_HOST: host}) session = async_get_clientsession(self.hass) - air_gradient = AirGradientClient(host, session=session) - await air_gradient.get_current_measures() + self.client = AirGradientClient(host, session=session) + await self.client.get_current_measures() self.context["title_placeholders"] = { "model": self.data[CONF_MODEL], @@ -44,6 +52,7 @@ class AirGradientConfigFlow(ConfigFlow, domain=DOMAIN): ) -> ConfigFlowResult: """Confirm discovery.""" if user_input is not None: + await self.set_configuration_source() return self.async_create_entry( title=self.data[CONF_MODEL], data={CONF_HOST: self.data[CONF_HOST]}, @@ -64,14 +73,15 @@ class AirGradientConfigFlow(ConfigFlow, domain=DOMAIN): errors: dict[str, str] = {} if user_input: session = async_get_clientsession(self.hass) - air_gradient = AirGradientClient(user_input[CONF_HOST], session=session) + self.client = AirGradientClient(user_input[CONF_HOST], session=session) try: - current_measures = await air_gradient.get_current_measures() + current_measures = await self.client.get_current_measures() except AirGradientError: errors["base"] = "cannot_connect" else: await self.async_set_unique_id(current_measures.serial_number) self._abort_if_unique_id_configured() + await self.set_configuration_source() return self.async_create_entry( title=current_measures.model, data={CONF_HOST: user_input[CONF_HOST]}, diff --git a/homeassistant/components/airgradient/strings.json b/homeassistant/components/airgradient/strings.json index f4441a66209..9deaf17d0e4 100644 --- a/homeassistant/components/airgradient/strings.json +++ b/homeassistant/components/airgradient/strings.json @@ -28,8 +28,7 @@ "name": "Configuration source", "state": { "cloud": "Cloud", - "local": "Local", - "both": "Both" + "local": "Local" } }, "display_temperature_unit": { diff --git a/tests/components/airgradient/test_config_flow.py b/tests/components/airgradient/test_config_flow.py index 022a250ebef..6bb951f2e26 100644 --- a/tests/components/airgradient/test_config_flow.py +++ b/tests/components/airgradient/test_config_flow.py @@ -3,7 +3,7 @@ from ipaddress import ip_address from unittest.mock import AsyncMock -from airgradient import AirGradientConnectionError +from airgradient import AirGradientConnectionError, ConfigurationControl from homeassistant.components.airgradient import DOMAIN from homeassistant.components.zeroconf import ZeroconfServiceInfo @@ -32,7 +32,7 @@ ZEROCONF_DISCOVERY = ZeroconfServiceInfo( async def test_full_flow( hass: HomeAssistant, - mock_airgradient_client: AsyncMock, + mock_new_airgradient_client: AsyncMock, mock_setup_entry: AsyncMock, ) -> None: """Test full flow.""" @@ -55,6 +55,31 @@ async def test_full_flow( CONF_HOST: "10.0.0.131", } assert result["result"].unique_id == "84fce612f5b8" + mock_new_airgradient_client.set_configuration_control.assert_awaited_once_with( + ConfigurationControl.LOCAL + ) + + +async def test_flow_with_registered_device( + hass: HomeAssistant, + mock_cloud_airgradient_client: AsyncMock, + mock_setup_entry: AsyncMock, +) -> None: + """Test we don't revert the cloud setting.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_HOST: "10.0.0.131"}, + ) + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["result"].unique_id == "84fce612f5b8" + mock_cloud_airgradient_client.set_configuration_control.assert_not_called() async def test_flow_errors( @@ -123,7 +148,7 @@ async def test_duplicate( async def test_zeroconf_flow( hass: HomeAssistant, - mock_airgradient_client: AsyncMock, + mock_new_airgradient_client: AsyncMock, mock_setup_entry: AsyncMock, ) -> None: """Test zeroconf flow.""" @@ -147,3 +172,28 @@ async def test_zeroconf_flow( CONF_HOST: "10.0.0.131", } assert result["result"].unique_id == "84fce612f5b8" + mock_new_airgradient_client.set_configuration_control.assert_awaited_once_with( + ConfigurationControl.LOCAL + ) + + +async def test_zeroconf_flow_cloud_device( + hass: HomeAssistant, + mock_cloud_airgradient_client: AsyncMock, + mock_setup_entry: AsyncMock, +) -> None: + """Test zeroconf flow doesn't revert the cloud setting.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_ZEROCONF}, + data=ZEROCONF_DISCOVERY, + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "discovery_confirm" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) + assert result["type"] is FlowResultType.CREATE_ENTRY + mock_cloud_airgradient_client.set_configuration_control.assert_not_called() From 049cac3443215de01556b16ba5eccf22b732baa3 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 3 Jun 2024 19:25:01 +0200 Subject: [PATCH 0166/1445] Update frontend to 20240603.0 (#118736) --- 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 c84a54d2642..dd112f5094a 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==20240530.0"] + "requirements": ["home-assistant-frontend==20240603.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 41b1c2c3fef..b2be5a92267 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -32,7 +32,7 @@ habluetooth==3.1.1 hass-nabucasa==0.81.0 hassil==1.7.1 home-assistant-bluetooth==1.12.0 -home-assistant-frontend==20240530.0 +home-assistant-frontend==20240603.0 home-assistant-intents==2024.5.28 httpx==0.27.0 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 5ca7d450f3a..df023c1d897 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1087,7 +1087,7 @@ hole==0.8.0 holidays==0.49 # homeassistant.components.frontend -home-assistant-frontend==20240530.0 +home-assistant-frontend==20240603.0 # homeassistant.components.conversation home-assistant-intents==2024.5.28 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 30ce8810b79..5d6cd92970b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -889,7 +889,7 @@ hole==0.8.0 holidays==0.49 # homeassistant.components.frontend -home-assistant-frontend==20240530.0 +home-assistant-frontend==20240603.0 # homeassistant.components.conversation home-assistant-intents==2024.5.28 From ebe4888c21b1bf1964f1371853f7102f20d451d8 Mon Sep 17 00:00:00 2001 From: David Knowles Date: Mon, 3 Jun 2024 13:29:20 -0400 Subject: [PATCH 0167/1445] Bump pydrawise to 2024.6.2 (#118608) --- homeassistant/components/hydrawise/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/hydrawise/manifest.json b/homeassistant/components/hydrawise/manifest.json index 8a0d52d550c..0426b8bf2cc 100644 --- a/homeassistant/components/hydrawise/manifest.json +++ b/homeassistant/components/hydrawise/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/hydrawise", "iot_class": "cloud_polling", "loggers": ["pydrawise"], - "requirements": ["pydrawise==2024.4.1"] + "requirements": ["pydrawise==2024.6.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index df023c1d897..c6b7681cc81 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1794,7 +1794,7 @@ pydiscovergy==3.0.1 pydoods==1.0.2 # homeassistant.components.hydrawise -pydrawise==2024.4.1 +pydrawise==2024.6.2 # homeassistant.components.android_ip_webcam pydroid-ipcam==2.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5d6cd92970b..ce0764a2c80 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1405,7 +1405,7 @@ pydexcom==0.2.3 pydiscovergy==3.0.1 # homeassistant.components.hydrawise -pydrawise==2024.4.1 +pydrawise==2024.6.2 # homeassistant.components.android_ip_webcam pydroid-ipcam==2.0.0 From 3cc13d454f3dd2e1246fe63caba64b33261866ac Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 3 Jun 2024 13:39:40 -0400 Subject: [PATCH 0168/1445] Remove dispatcher from Tag entity (#118671) * Remove dispatcher from Tag entity * type * Don't use helper * Del is faster than pop * Use id in update --------- Co-authored-by: G Johansson --- homeassistant/components/tag/__init__.py | 34 ++++++++++++------------ 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/tag/__init__.py b/homeassistant/components/tag/__init__.py index 45266652a47..1613601e23a 100644 --- a/homeassistant/components/tag/__init__.py +++ b/homeassistant/components/tag/__init__.py @@ -2,6 +2,7 @@ from __future__ import annotations +from collections.abc import Callable import logging from typing import TYPE_CHECKING, Any, final import uuid @@ -14,10 +15,6 @@ from homeassistant.core import Context, HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import collection, entity_registry as er import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import ( - async_dispatcher_connect, - async_dispatcher_send, -) from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.storage import Store @@ -245,6 +242,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: ).async_setup(hass) entity_registry = er.async_get(hass) + entity_update_handlers: dict[str, Callable[[str | None, str | None], None]] = {} async def tag_change_listener( change_type: str, item_id: str, updated_config: dict @@ -263,6 +261,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: await component.async_add_entities( [ TagEntity( + entity_update_handlers, entity.name or entity.original_name, updated_config[CONF_ID], updated_config.get(LAST_SCANNED), @@ -273,12 +272,11 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: elif change_type == collection.CHANGE_UPDATED: # When tags are changed or updated in storage - async_dispatcher_send( - hass, - f"{SIGNAL_TAG_CHANGED}-{updated_config[CONF_ID]}", - updated_config.get(DEVICE_ID), - updated_config.get(LAST_SCANNED), - ) + if handler := entity_update_handlers.get(updated_config[CONF_ID]): + handler( + updated_config.get(DEVICE_ID), + updated_config.get(LAST_SCANNED), + ) # Deleted tags elif change_type == collection.CHANGE_REMOVED: @@ -308,6 +306,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: name = entity.name or entity.original_name entities.append( TagEntity( + entity_update_handlers, name, tag[CONF_ID], tag.get(LAST_SCANNED), @@ -371,12 +370,14 @@ class TagEntity(Entity): def __init__( self, + entity_update_handlers: dict[str, Callable[[str | None, str | None], None]], name: str, tag_id: str, last_scanned: str | None, device_id: str | None, ) -> None: """Initialize the Tag event.""" + self._entity_update_handlers = entity_update_handlers self._attr_name = name self._tag_id = tag_id self._attr_unique_id = tag_id @@ -419,10 +420,9 @@ class TagEntity(Entity): async def async_added_to_hass(self) -> None: """Handle entity which will be added.""" await super().async_added_to_hass() - self.async_on_remove( - async_dispatcher_connect( - self.hass, - f"{SIGNAL_TAG_CHANGED}-{self._tag_id}", - self.async_handle_event, - ) - ) + self._entity_update_handlers[self._tag_id] = self.async_handle_event + + async def async_will_remove_from_hass(self) -> None: + """Handle entity being removed.""" + await super().async_will_remove_from_hass() + del self._entity_update_handlers[self._tag_id] From 60bcd27a4751ea51fb7d288344f27372887a2719 Mon Sep 17 00:00:00 2001 From: Richard Kroegel <42204099+rikroe@users.noreply.github.com> Date: Mon, 3 Jun 2024 19:46:04 +0200 Subject: [PATCH 0169/1445] Use snapshot_platform helper for BMW tests (#118735) * Use snapshot_platform helper * Remove comments --------- Co-authored-by: Richard --- .../snapshots/test_button.ambr | 1117 ++++++-- .../snapshots/test_number.ambr | 146 +- .../snapshots/test_select.ambr | 424 ++- .../snapshots/test_sensor.ambr | 2451 +++++++++++++---- .../snapshots/test_switch.ambr | 232 +- .../bmw_connected_drive/test_button.py | 16 +- .../bmw_connected_drive/test_number.py | 18 +- .../bmw_connected_drive/test_select.py | 16 +- .../bmw_connected_drive/test_sensor.py | 15 +- .../bmw_connected_drive/test_switch.py | 17 +- 10 files changed, 3486 insertions(+), 966 deletions(-) diff --git a/tests/components/bmw_connected_drive/snapshots/test_button.ambr b/tests/components/bmw_connected_drive/snapshots/test_button.ambr index 17866878ba3..cd3f94c7e5e 100644 --- a/tests/components/bmw_connected_drive/snapshots/test_button.ambr +++ b/tests/components/bmw_connected_drive/snapshots/test_button.ambr @@ -1,233 +1,894 @@ # serializer version: 1 -# name: test_entity_state_attrs - list([ - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by MyBMW', - 'friendly_name': 'iX xDrive50 Flash lights', - }), - 'context': , - 'entity_id': 'button.ix_xdrive50_flash_lights', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unknown', +# name: test_entity_state_attrs[button.i3_rex_activate_air_conditioning-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by MyBMW', - 'friendly_name': 'iX xDrive50 Sound horn', - }), - 'context': , - 'entity_id': 'button.ix_xdrive50_sound_horn', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unknown', + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': None, + 'entity_id': 'button.i3_rex_activate_air_conditioning', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by MyBMW', - 'friendly_name': 'iX xDrive50 Activate air conditioning', - }), - 'context': , - 'entity_id': 'button.ix_xdrive50_activate_air_conditioning', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unknown', + 'name': None, + 'options': dict({ }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by MyBMW', - 'friendly_name': 'iX xDrive50 Deactivate air conditioning', - }), - 'context': , - 'entity_id': 'button.ix_xdrive50_deactivate_air_conditioning', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unknown', - }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by MyBMW', - 'friendly_name': 'iX xDrive50 Find vehicle', - }), - 'context': , - 'entity_id': 'button.ix_xdrive50_find_vehicle', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unknown', - }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by MyBMW', - 'friendly_name': 'i4 eDrive40 Flash lights', - }), - 'context': , - 'entity_id': 'button.i4_edrive40_flash_lights', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unknown', - }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by MyBMW', - 'friendly_name': 'i4 eDrive40 Sound horn', - }), - 'context': , - 'entity_id': 'button.i4_edrive40_sound_horn', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unknown', - }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by MyBMW', - 'friendly_name': 'i4 eDrive40 Activate air conditioning', - }), - 'context': , - 'entity_id': 'button.i4_edrive40_activate_air_conditioning', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unknown', - }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by MyBMW', - 'friendly_name': 'i4 eDrive40 Deactivate air conditioning', - }), - 'context': , - 'entity_id': 'button.i4_edrive40_deactivate_air_conditioning', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unknown', - }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by MyBMW', - 'friendly_name': 'i4 eDrive40 Find vehicle', - }), - 'context': , - 'entity_id': 'button.i4_edrive40_find_vehicle', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unknown', - }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by MyBMW', - 'friendly_name': 'M340i xDrive Flash lights', - }), - 'context': , - 'entity_id': 'button.m340i_xdrive_flash_lights', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unknown', - }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by MyBMW', - 'friendly_name': 'M340i xDrive Sound horn', - }), - 'context': , - 'entity_id': 'button.m340i_xdrive_sound_horn', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unknown', - }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by MyBMW', - 'friendly_name': 'M340i xDrive Activate air conditioning', - }), - 'context': , - 'entity_id': 'button.m340i_xdrive_activate_air_conditioning', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unknown', - }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by MyBMW', - 'friendly_name': 'M340i xDrive Deactivate air conditioning', - }), - 'context': , - 'entity_id': 'button.m340i_xdrive_deactivate_air_conditioning', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unknown', - }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by MyBMW', - 'friendly_name': 'M340i xDrive Find vehicle', - }), - 'context': , - 'entity_id': 'button.m340i_xdrive_find_vehicle', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unknown', - }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by MyBMW', - 'friendly_name': 'i3 (+ REX) Flash lights', - }), - 'context': , - 'entity_id': 'button.i3_rex_flash_lights', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unknown', - }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by MyBMW', - 'friendly_name': 'i3 (+ REX) Sound horn', - }), - 'context': , - 'entity_id': 'button.i3_rex_sound_horn', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unknown', - }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by MyBMW', - 'friendly_name': 'i3 (+ REX) Activate air conditioning', - }), - 'context': , - 'entity_id': 'button.i3_rex_activate_air_conditioning', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unknown', - }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by MyBMW', - 'friendly_name': 'i3 (+ REX) Find vehicle', - }), - 'context': , - 'entity_id': 'button.i3_rex_find_vehicle', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unknown', - }), - ]) + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Activate air conditioning', + 'platform': 'bmw_connected_drive', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'activate_air_conditioning', + 'unique_id': 'WBY00000000REXI01-activate_air_conditioning', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_state_attrs[button.i3_rex_activate_air_conditioning-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'friendly_name': 'i3 (+ REX) Activate air conditioning', + }), + 'context': , + 'entity_id': 'button.i3_rex_activate_air_conditioning', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_entity_state_attrs[button.i3_rex_find_vehicle-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': None, + 'entity_id': 'button.i3_rex_find_vehicle', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Find vehicle', + 'platform': 'bmw_connected_drive', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'find_vehicle', + 'unique_id': 'WBY00000000REXI01-find_vehicle', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_state_attrs[button.i3_rex_find_vehicle-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'friendly_name': 'i3 (+ REX) Find vehicle', + }), + 'context': , + 'entity_id': 'button.i3_rex_find_vehicle', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_entity_state_attrs[button.i3_rex_flash_lights-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': None, + 'entity_id': 'button.i3_rex_flash_lights', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Flash lights', + 'platform': 'bmw_connected_drive', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'light_flash', + 'unique_id': 'WBY00000000REXI01-light_flash', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_state_attrs[button.i3_rex_flash_lights-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'friendly_name': 'i3 (+ REX) Flash lights', + }), + 'context': , + 'entity_id': 'button.i3_rex_flash_lights', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_entity_state_attrs[button.i3_rex_sound_horn-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': None, + 'entity_id': 'button.i3_rex_sound_horn', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Sound horn', + 'platform': 'bmw_connected_drive', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'sound_horn', + 'unique_id': 'WBY00000000REXI01-sound_horn', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_state_attrs[button.i3_rex_sound_horn-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'friendly_name': 'i3 (+ REX) Sound horn', + }), + 'context': , + 'entity_id': 'button.i3_rex_sound_horn', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_entity_state_attrs[button.i4_edrive40_activate_air_conditioning-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': None, + 'entity_id': 'button.i4_edrive40_activate_air_conditioning', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Activate air conditioning', + 'platform': 'bmw_connected_drive', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'activate_air_conditioning', + 'unique_id': 'WBA00000000DEMO02-activate_air_conditioning', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_state_attrs[button.i4_edrive40_activate_air_conditioning-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'friendly_name': 'i4 eDrive40 Activate air conditioning', + }), + 'context': , + 'entity_id': 'button.i4_edrive40_activate_air_conditioning', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_entity_state_attrs[button.i4_edrive40_deactivate_air_conditioning-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': None, + 'entity_id': 'button.i4_edrive40_deactivate_air_conditioning', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Deactivate air conditioning', + 'platform': 'bmw_connected_drive', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'deactivate_air_conditioning', + 'unique_id': 'WBA00000000DEMO02-deactivate_air_conditioning', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_state_attrs[button.i4_edrive40_deactivate_air_conditioning-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'friendly_name': 'i4 eDrive40 Deactivate air conditioning', + }), + 'context': , + 'entity_id': 'button.i4_edrive40_deactivate_air_conditioning', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_entity_state_attrs[button.i4_edrive40_find_vehicle-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': None, + 'entity_id': 'button.i4_edrive40_find_vehicle', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Find vehicle', + 'platform': 'bmw_connected_drive', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'find_vehicle', + 'unique_id': 'WBA00000000DEMO02-find_vehicle', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_state_attrs[button.i4_edrive40_find_vehicle-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'friendly_name': 'i4 eDrive40 Find vehicle', + }), + 'context': , + 'entity_id': 'button.i4_edrive40_find_vehicle', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_entity_state_attrs[button.i4_edrive40_flash_lights-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': None, + 'entity_id': 'button.i4_edrive40_flash_lights', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Flash lights', + 'platform': 'bmw_connected_drive', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'light_flash', + 'unique_id': 'WBA00000000DEMO02-light_flash', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_state_attrs[button.i4_edrive40_flash_lights-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'friendly_name': 'i4 eDrive40 Flash lights', + }), + 'context': , + 'entity_id': 'button.i4_edrive40_flash_lights', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_entity_state_attrs[button.i4_edrive40_sound_horn-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': None, + 'entity_id': 'button.i4_edrive40_sound_horn', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Sound horn', + 'platform': 'bmw_connected_drive', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'sound_horn', + 'unique_id': 'WBA00000000DEMO02-sound_horn', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_state_attrs[button.i4_edrive40_sound_horn-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'friendly_name': 'i4 eDrive40 Sound horn', + }), + 'context': , + 'entity_id': 'button.i4_edrive40_sound_horn', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_entity_state_attrs[button.ix_xdrive50_activate_air_conditioning-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': None, + 'entity_id': 'button.ix_xdrive50_activate_air_conditioning', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Activate air conditioning', + 'platform': 'bmw_connected_drive', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'activate_air_conditioning', + 'unique_id': 'WBA00000000DEMO01-activate_air_conditioning', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_state_attrs[button.ix_xdrive50_activate_air_conditioning-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'friendly_name': 'iX xDrive50 Activate air conditioning', + }), + 'context': , + 'entity_id': 'button.ix_xdrive50_activate_air_conditioning', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_entity_state_attrs[button.ix_xdrive50_deactivate_air_conditioning-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': None, + 'entity_id': 'button.ix_xdrive50_deactivate_air_conditioning', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Deactivate air conditioning', + 'platform': 'bmw_connected_drive', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'deactivate_air_conditioning', + 'unique_id': 'WBA00000000DEMO01-deactivate_air_conditioning', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_state_attrs[button.ix_xdrive50_deactivate_air_conditioning-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'friendly_name': 'iX xDrive50 Deactivate air conditioning', + }), + 'context': , + 'entity_id': 'button.ix_xdrive50_deactivate_air_conditioning', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_entity_state_attrs[button.ix_xdrive50_find_vehicle-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': None, + 'entity_id': 'button.ix_xdrive50_find_vehicle', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Find vehicle', + 'platform': 'bmw_connected_drive', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'find_vehicle', + 'unique_id': 'WBA00000000DEMO01-find_vehicle', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_state_attrs[button.ix_xdrive50_find_vehicle-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'friendly_name': 'iX xDrive50 Find vehicle', + }), + 'context': , + 'entity_id': 'button.ix_xdrive50_find_vehicle', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_entity_state_attrs[button.ix_xdrive50_flash_lights-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': None, + 'entity_id': 'button.ix_xdrive50_flash_lights', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Flash lights', + 'platform': 'bmw_connected_drive', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'light_flash', + 'unique_id': 'WBA00000000DEMO01-light_flash', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_state_attrs[button.ix_xdrive50_flash_lights-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'friendly_name': 'iX xDrive50 Flash lights', + }), + 'context': , + 'entity_id': 'button.ix_xdrive50_flash_lights', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_entity_state_attrs[button.ix_xdrive50_sound_horn-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': None, + 'entity_id': 'button.ix_xdrive50_sound_horn', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Sound horn', + 'platform': 'bmw_connected_drive', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'sound_horn', + 'unique_id': 'WBA00000000DEMO01-sound_horn', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_state_attrs[button.ix_xdrive50_sound_horn-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'friendly_name': 'iX xDrive50 Sound horn', + }), + 'context': , + 'entity_id': 'button.ix_xdrive50_sound_horn', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_entity_state_attrs[button.m340i_xdrive_activate_air_conditioning-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': None, + 'entity_id': 'button.m340i_xdrive_activate_air_conditioning', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Activate air conditioning', + 'platform': 'bmw_connected_drive', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'activate_air_conditioning', + 'unique_id': 'WBA00000000DEMO03-activate_air_conditioning', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_state_attrs[button.m340i_xdrive_activate_air_conditioning-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'friendly_name': 'M340i xDrive Activate air conditioning', + }), + 'context': , + 'entity_id': 'button.m340i_xdrive_activate_air_conditioning', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_entity_state_attrs[button.m340i_xdrive_deactivate_air_conditioning-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': None, + 'entity_id': 'button.m340i_xdrive_deactivate_air_conditioning', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Deactivate air conditioning', + 'platform': 'bmw_connected_drive', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'deactivate_air_conditioning', + 'unique_id': 'WBA00000000DEMO03-deactivate_air_conditioning', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_state_attrs[button.m340i_xdrive_deactivate_air_conditioning-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'friendly_name': 'M340i xDrive Deactivate air conditioning', + }), + 'context': , + 'entity_id': 'button.m340i_xdrive_deactivate_air_conditioning', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_entity_state_attrs[button.m340i_xdrive_find_vehicle-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': None, + 'entity_id': 'button.m340i_xdrive_find_vehicle', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Find vehicle', + 'platform': 'bmw_connected_drive', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'find_vehicle', + 'unique_id': 'WBA00000000DEMO03-find_vehicle', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_state_attrs[button.m340i_xdrive_find_vehicle-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'friendly_name': 'M340i xDrive Find vehicle', + }), + 'context': , + 'entity_id': 'button.m340i_xdrive_find_vehicle', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_entity_state_attrs[button.m340i_xdrive_flash_lights-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': None, + 'entity_id': 'button.m340i_xdrive_flash_lights', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Flash lights', + 'platform': 'bmw_connected_drive', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'light_flash', + 'unique_id': 'WBA00000000DEMO03-light_flash', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_state_attrs[button.m340i_xdrive_flash_lights-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'friendly_name': 'M340i xDrive Flash lights', + }), + 'context': , + 'entity_id': 'button.m340i_xdrive_flash_lights', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_entity_state_attrs[button.m340i_xdrive_sound_horn-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': None, + 'entity_id': 'button.m340i_xdrive_sound_horn', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Sound horn', + 'platform': 'bmw_connected_drive', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'sound_horn', + 'unique_id': 'WBA00000000DEMO03-sound_horn', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_state_attrs[button.m340i_xdrive_sound_horn-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'friendly_name': 'M340i xDrive Sound horn', + }), + 'context': , + 'entity_id': 'button.m340i_xdrive_sound_horn', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) # --- diff --git a/tests/components/bmw_connected_drive/snapshots/test_number.ambr b/tests/components/bmw_connected_drive/snapshots/test_number.ambr index 93580ddc7b7..f24ea43d8e8 100644 --- a/tests/components/bmw_connected_drive/snapshots/test_number.ambr +++ b/tests/components/bmw_connected_drive/snapshots/test_number.ambr @@ -1,39 +1,115 @@ # serializer version: 1 -# name: test_entity_state_attrs - list([ - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by MyBMW', - 'device_class': 'battery', - 'friendly_name': 'iX xDrive50 Target SoC', - 'max': 100.0, - 'min': 20.0, - 'mode': , - 'step': 5.0, - }), - 'context': , - 'entity_id': 'number.ix_xdrive50_target_soc', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '80', +# name: test_entity_state_attrs[number.i4_edrive40_target_soc-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by MyBMW', - 'device_class': 'battery', - 'friendly_name': 'i4 eDrive40 Target SoC', - 'max': 100.0, - 'min': 20.0, - 'mode': , - 'step': 5.0, - }), - 'context': , - 'entity_id': 'number.i4_edrive40_target_soc', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '80', + 'area_id': None, + 'capabilities': dict({ + 'max': 100.0, + 'min': 20.0, + 'mode': , + 'step': 5.0, }), - ]) + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': None, + 'entity_id': 'number.i4_edrive40_target_soc', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Target SoC', + 'platform': 'bmw_connected_drive', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'target_soc', + 'unique_id': 'WBA00000000DEMO02-target_soc', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_state_attrs[number.i4_edrive40_target_soc-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'device_class': 'battery', + 'friendly_name': 'i4 eDrive40 Target SoC', + 'max': 100.0, + 'min': 20.0, + 'mode': , + 'step': 5.0, + }), + 'context': , + 'entity_id': 'number.i4_edrive40_target_soc', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '80', + }) +# --- +# name: test_entity_state_attrs[number.ix_xdrive50_target_soc-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 100.0, + 'min': 20.0, + 'mode': , + 'step': 5.0, + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': None, + 'entity_id': 'number.ix_xdrive50_target_soc', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Target SoC', + 'platform': 'bmw_connected_drive', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'target_soc', + 'unique_id': 'WBA00000000DEMO01-target_soc', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_state_attrs[number.ix_xdrive50_target_soc-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'device_class': 'battery', + 'friendly_name': 'iX xDrive50 Target SoC', + 'max': 100.0, + 'min': 20.0, + 'mode': , + 'step': 5.0, + }), + 'context': , + 'entity_id': 'number.ix_xdrive50_target_soc', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '80', + }) # --- diff --git a/tests/components/bmw_connected_drive/snapshots/test_select.ambr b/tests/components/bmw_connected_drive/snapshots/test_select.ambr index e72708345b1..94155598ef7 100644 --- a/tests/components/bmw_connected_drive/snapshots/test_select.ambr +++ b/tests/components/bmw_connected_drive/snapshots/test_select.ambr @@ -1,109 +1,327 @@ # serializer version: 1 -# name: test_entity_state_attrs - list([ - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by MyBMW', - 'friendly_name': 'iX xDrive50 AC Charging Limit', - 'options': list([ - '6', - '7', - '8', - '9', - '10', - '11', - '12', - '13', - '14', - '15', - '16', - '20', - '32', - ]), - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'select.ix_xdrive50_ac_charging_limit', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '16', +# name: test_entity_state_attrs[select.i3_rex_charging_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by MyBMW', - 'friendly_name': 'iX xDrive50 Charging Mode', - 'options': list([ - 'IMMEDIATE_CHARGING', - 'DELAYED_CHARGING', - ]), - }), - 'context': , - 'entity_id': 'select.ix_xdrive50_charging_mode', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'IMMEDIATE_CHARGING', + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'IMMEDIATE_CHARGING', + 'DELAYED_CHARGING', + ]), }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by MyBMW', - 'friendly_name': 'i4 eDrive40 AC Charging Limit', - 'options': list([ - '6', - '7', - '8', - '9', - '10', - '11', - '12', - '13', - '14', - '15', - '16', - '20', - '32', - ]), - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'select.i4_edrive40_ac_charging_limit', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '16', + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': None, + 'entity_id': 'select.i3_rex_charging_mode', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by MyBMW', - 'friendly_name': 'i4 eDrive40 Charging Mode', - 'options': list([ - 'IMMEDIATE_CHARGING', - 'DELAYED_CHARGING', - ]), - }), - 'context': , - 'entity_id': 'select.i4_edrive40_charging_mode', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'IMMEDIATE_CHARGING', + 'name': None, + 'options': dict({ }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by MyBMW', - 'friendly_name': 'i3 (+ REX) Charging Mode', - 'options': list([ - 'IMMEDIATE_CHARGING', - 'DELAYED_CHARGING', - ]), - }), - 'context': , - 'entity_id': 'select.i3_rex_charging_mode', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'DELAYED_CHARGING', - }), - ]) + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Charging Mode', + 'platform': 'bmw_connected_drive', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'charging_mode', + 'unique_id': 'WBY00000000REXI01-charging_mode', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_state_attrs[select.i3_rex_charging_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'friendly_name': 'i3 (+ REX) Charging Mode', + 'options': list([ + 'IMMEDIATE_CHARGING', + 'DELAYED_CHARGING', + ]), + }), + 'context': , + 'entity_id': 'select.i3_rex_charging_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'DELAYED_CHARGING', + }) +# --- +# name: test_entity_state_attrs[select.i4_edrive40_ac_charging_limit-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + '6', + '7', + '8', + '9', + '10', + '11', + '12', + '13', + '14', + '15', + '16', + '20', + '32', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': None, + 'entity_id': 'select.i4_edrive40_ac_charging_limit', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'AC Charging Limit', + 'platform': 'bmw_connected_drive', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'ac_limit', + 'unique_id': 'WBA00000000DEMO02-ac_limit', + 'unit_of_measurement': , + }) +# --- +# name: test_entity_state_attrs[select.i4_edrive40_ac_charging_limit-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'friendly_name': 'i4 eDrive40 AC Charging Limit', + 'options': list([ + '6', + '7', + '8', + '9', + '10', + '11', + '12', + '13', + '14', + '15', + '16', + '20', + '32', + ]), + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'select.i4_edrive40_ac_charging_limit', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '16', + }) +# --- +# name: test_entity_state_attrs[select.i4_edrive40_charging_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'IMMEDIATE_CHARGING', + 'DELAYED_CHARGING', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': None, + 'entity_id': 'select.i4_edrive40_charging_mode', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Charging Mode', + 'platform': 'bmw_connected_drive', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'charging_mode', + 'unique_id': 'WBA00000000DEMO02-charging_mode', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_state_attrs[select.i4_edrive40_charging_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'friendly_name': 'i4 eDrive40 Charging Mode', + 'options': list([ + 'IMMEDIATE_CHARGING', + 'DELAYED_CHARGING', + ]), + }), + 'context': , + 'entity_id': 'select.i4_edrive40_charging_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'IMMEDIATE_CHARGING', + }) +# --- +# name: test_entity_state_attrs[select.ix_xdrive50_ac_charging_limit-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + '6', + '7', + '8', + '9', + '10', + '11', + '12', + '13', + '14', + '15', + '16', + '20', + '32', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': None, + 'entity_id': 'select.ix_xdrive50_ac_charging_limit', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'AC Charging Limit', + 'platform': 'bmw_connected_drive', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'ac_limit', + 'unique_id': 'WBA00000000DEMO01-ac_limit', + 'unit_of_measurement': , + }) +# --- +# name: test_entity_state_attrs[select.ix_xdrive50_ac_charging_limit-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'friendly_name': 'iX xDrive50 AC Charging Limit', + 'options': list([ + '6', + '7', + '8', + '9', + '10', + '11', + '12', + '13', + '14', + '15', + '16', + '20', + '32', + ]), + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'select.ix_xdrive50_ac_charging_limit', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '16', + }) +# --- +# name: test_entity_state_attrs[select.ix_xdrive50_charging_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'IMMEDIATE_CHARGING', + 'DELAYED_CHARGING', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': None, + 'entity_id': 'select.ix_xdrive50_charging_mode', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Charging Mode', + 'platform': 'bmw_connected_drive', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'charging_mode', + 'unique_id': 'WBA00000000DEMO01-charging_mode', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_state_attrs[select.ix_xdrive50_charging_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'friendly_name': 'iX xDrive50 Charging Mode', + 'options': list([ + 'IMMEDIATE_CHARGING', + 'DELAYED_CHARGING', + ]), + }), + 'context': , + 'entity_id': 'select.ix_xdrive50_charging_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'IMMEDIATE_CHARGING', + }) # --- diff --git a/tests/components/bmw_connected_drive/snapshots/test_sensor.ambr b/tests/components/bmw_connected_drive/snapshots/test_sensor.ambr index bf35398cd90..e3833add777 100644 --- a/tests/components/bmw_connected_drive/snapshots/test_sensor.ambr +++ b/tests/components/bmw_connected_drive/snapshots/test_sensor.ambr @@ -1,537 +1,1924 @@ # serializer version: 1 -# name: test_entity_state_attrs - list([ - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by MyBMW', - 'friendly_name': 'iX xDrive50 AC current limit', - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.ix_xdrive50_ac_current_limit', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '16', +# name: test_entity_state_attrs[sensor.i3_rex_ac_current_limit-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by MyBMW', - 'device_class': 'timestamp', - 'friendly_name': 'iX xDrive50 Charging start time', - }), - 'context': , - 'entity_id': 'sensor.ix_xdrive50_charging_start_time', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unknown', + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.i3_rex_ac_current_limit', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by MyBMW', - 'device_class': 'timestamp', - 'friendly_name': 'iX xDrive50 Charging end time', - }), - 'context': , - 'entity_id': 'sensor.ix_xdrive50_charging_end_time', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '2023-06-22T10:40:00+00:00', + 'name': None, + 'options': dict({ }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by MyBMW', - 'friendly_name': 'iX xDrive50 Charging status', - }), - 'context': , - 'entity_id': 'sensor.ix_xdrive50_charging_status', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'CHARGING', - }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by MyBMW', - 'friendly_name': 'iX xDrive50 Charging target', - 'unit_of_measurement': '%', - }), - 'context': , - 'entity_id': 'sensor.ix_xdrive50_charging_target', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '80', - }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by MyBMW', - 'device_class': 'battery', - 'friendly_name': 'iX xDrive50 Remaining battery percent', - 'state_class': , - 'unit_of_measurement': '%', - }), - 'context': , - 'entity_id': 'sensor.ix_xdrive50_remaining_battery_percent', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '70', - }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by MyBMW', - 'friendly_name': 'iX xDrive50 Mileage', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.ix_xdrive50_mileage', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '1121', - }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by MyBMW', - 'friendly_name': 'iX xDrive50 Remaining range total', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.ix_xdrive50_remaining_range_total', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '340', - }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by MyBMW', - 'friendly_name': 'iX xDrive50 Remaining range electric', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.ix_xdrive50_remaining_range_electric', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '340', - }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by MyBMW', - 'device_class': 'enum', - 'friendly_name': 'iX xDrive50 Climate status', - 'options': list([ - 'cooling', - 'heating', - 'inactive', - 'standby', - ]), - }), - 'context': , - 'entity_id': 'sensor.ix_xdrive50_climate_status', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'inactive', - }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by MyBMW', - 'friendly_name': 'i4 eDrive40 AC current limit', - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.i4_edrive40_ac_current_limit', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '16', - }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by MyBMW', - 'device_class': 'timestamp', - 'friendly_name': 'i4 eDrive40 Charging start time', - }), - 'context': , - 'entity_id': 'sensor.i4_edrive40_charging_start_time', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unknown', - }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by MyBMW', - 'device_class': 'timestamp', - 'friendly_name': 'i4 eDrive40 Charging end time', - }), - 'context': , - 'entity_id': 'sensor.i4_edrive40_charging_end_time', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '2023-06-22T10:40:00+00:00', - }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by MyBMW', - 'friendly_name': 'i4 eDrive40 Charging status', - }), - 'context': , - 'entity_id': 'sensor.i4_edrive40_charging_status', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'NOT_CHARGING', - }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by MyBMW', - 'friendly_name': 'i4 eDrive40 Charging target', - 'unit_of_measurement': '%', - }), - 'context': , - 'entity_id': 'sensor.i4_edrive40_charging_target', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '80', - }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by MyBMW', - 'device_class': 'battery', - 'friendly_name': 'i4 eDrive40 Remaining battery percent', - 'state_class': , - 'unit_of_measurement': '%', - }), - 'context': , - 'entity_id': 'sensor.i4_edrive40_remaining_battery_percent', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '80', - }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by MyBMW', - 'friendly_name': 'i4 eDrive40 Mileage', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.i4_edrive40_mileage', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '1121', - }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by MyBMW', - 'friendly_name': 'i4 eDrive40 Remaining range total', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.i4_edrive40_remaining_range_total', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '472', - }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by MyBMW', - 'friendly_name': 'i4 eDrive40 Remaining range electric', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.i4_edrive40_remaining_range_electric', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '472', - }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by MyBMW', - 'device_class': 'enum', - 'friendly_name': 'i4 eDrive40 Climate status', - 'options': list([ - 'cooling', - 'heating', - 'inactive', - 'standby', - ]), - }), - 'context': , - 'entity_id': 'sensor.i4_edrive40_climate_status', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'heating', - }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by MyBMW', - 'friendly_name': 'M340i xDrive Mileage', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.m340i_xdrive_mileage', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '1121', - }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by MyBMW', - 'friendly_name': 'M340i xDrive Remaining range total', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.m340i_xdrive_remaining_range_total', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '629', - }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by MyBMW', - 'friendly_name': 'M340i xDrive Remaining range fuel', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.m340i_xdrive_remaining_range_fuel', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '629', - }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by MyBMW', - 'friendly_name': 'M340i xDrive Remaining fuel', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.m340i_xdrive_remaining_fuel', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '40', - }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by MyBMW', - 'friendly_name': 'M340i xDrive Remaining fuel percent', - 'state_class': , - 'unit_of_measurement': '%', - }), - 'context': , - 'entity_id': 'sensor.m340i_xdrive_remaining_fuel_percent', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '80', - }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by MyBMW', - 'device_class': 'enum', - 'friendly_name': 'M340i xDrive Climate status', - 'options': list([ - 'cooling', - 'heating', - 'inactive', - 'standby', - ]), - }), - 'context': , - 'entity_id': 'sensor.m340i_xdrive_climate_status', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'inactive', - }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by MyBMW', - 'friendly_name': 'i3 (+ REX) AC current limit', - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.i3_rex_ac_current_limit', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unknown', - }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by MyBMW', - 'device_class': 'timestamp', - 'friendly_name': 'i3 (+ REX) Charging start time', - }), - 'context': , - 'entity_id': 'sensor.i3_rex_charging_start_time', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '2023-06-23T01:01:00+00:00', - }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by MyBMW', - 'device_class': 'timestamp', - 'friendly_name': 'i3 (+ REX) Charging end time', - }), - 'context': , - 'entity_id': 'sensor.i3_rex_charging_end_time', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unknown', - }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by MyBMW', - 'friendly_name': 'i3 (+ REX) Charging status', - }), - 'context': , - 'entity_id': 'sensor.i3_rex_charging_status', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'WAITING_FOR_CHARGING', - }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by MyBMW', - 'friendly_name': 'i3 (+ REX) Charging target', - 'unit_of_measurement': '%', - }), - 'context': , - 'entity_id': 'sensor.i3_rex_charging_target', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '100', - }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by MyBMW', - 'device_class': 'battery', - 'friendly_name': 'i3 (+ REX) Remaining battery percent', - 'state_class': , - 'unit_of_measurement': '%', - }), - 'context': , - 'entity_id': 'sensor.i3_rex_remaining_battery_percent', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '82', - }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by MyBMW', - 'friendly_name': 'i3 (+ REX) Mileage', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.i3_rex_mileage', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '137009', - }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by MyBMW', - 'friendly_name': 'i3 (+ REX) Remaining range total', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.i3_rex_remaining_range_total', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '279', - }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by MyBMW', - 'friendly_name': 'i3 (+ REX) Remaining range electric', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.i3_rex_remaining_range_electric', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '174', - }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by MyBMW', - 'friendly_name': 'i3 (+ REX) Remaining range fuel', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.i3_rex_remaining_range_fuel', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '105', - }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by MyBMW', - 'friendly_name': 'i3 (+ REX) Remaining fuel', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.i3_rex_remaining_fuel', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '6', - }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by MyBMW', - 'friendly_name': 'i3 (+ REX) Remaining fuel percent', - 'state_class': , - 'unit_of_measurement': '%', - }), - 'context': , - 'entity_id': 'sensor.i3_rex_remaining_fuel_percent', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unknown', - }), - ]) + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'AC current limit', + 'platform': 'bmw_connected_drive', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'ac_current_limit', + 'unique_id': 'WBY00000000REXI01-ac_current_limit', + 'unit_of_measurement': , + }) +# --- +# name: test_entity_state_attrs[sensor.i3_rex_ac_current_limit-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'friendly_name': 'i3 (+ REX) AC current limit', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.i3_rex_ac_current_limit', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_entity_state_attrs[sensor.i3_rex_charging_end_time-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.i3_rex_charging_end_time', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Charging end time', + 'platform': 'bmw_connected_drive', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'charging_end_time', + 'unique_id': 'WBY00000000REXI01-charging_end_time', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_state_attrs[sensor.i3_rex_charging_end_time-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'device_class': 'timestamp', + 'friendly_name': 'i3 (+ REX) Charging end time', + }), + 'context': , + 'entity_id': 'sensor.i3_rex_charging_end_time', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_entity_state_attrs[sensor.i3_rex_charging_start_time-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.i3_rex_charging_start_time', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Charging start time', + 'platform': 'bmw_connected_drive', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'charging_start_time', + 'unique_id': 'WBY00000000REXI01-charging_start_time', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_state_attrs[sensor.i3_rex_charging_start_time-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'device_class': 'timestamp', + 'friendly_name': 'i3 (+ REX) Charging start time', + }), + 'context': , + 'entity_id': 'sensor.i3_rex_charging_start_time', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2023-06-23T01:01:00+00:00', + }) +# --- +# name: test_entity_state_attrs[sensor.i3_rex_charging_status-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.i3_rex_charging_status', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Charging status', + 'platform': 'bmw_connected_drive', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'charging_status', + 'unique_id': 'WBY00000000REXI01-charging_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_state_attrs[sensor.i3_rex_charging_status-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'friendly_name': 'i3 (+ REX) Charging status', + }), + 'context': , + 'entity_id': 'sensor.i3_rex_charging_status', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'WAITING_FOR_CHARGING', + }) +# --- +# name: test_entity_state_attrs[sensor.i3_rex_charging_target-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.i3_rex_charging_target', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Charging target', + 'platform': 'bmw_connected_drive', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'charging_target', + 'unique_id': 'WBY00000000REXI01-charging_target', + 'unit_of_measurement': '%', + }) +# --- +# name: test_entity_state_attrs[sensor.i3_rex_charging_target-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'friendly_name': 'i3 (+ REX) Charging target', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.i3_rex_charging_target', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '100', + }) +# --- +# name: test_entity_state_attrs[sensor.i3_rex_mileage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.i3_rex_mileage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Mileage', + 'platform': 'bmw_connected_drive', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'mileage', + 'unique_id': 'WBY00000000REXI01-mileage', + 'unit_of_measurement': , + }) +# --- +# name: test_entity_state_attrs[sensor.i3_rex_mileage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'friendly_name': 'i3 (+ REX) Mileage', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.i3_rex_mileage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '137009', + }) +# --- +# name: test_entity_state_attrs[sensor.i3_rex_remaining_battery_percent-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.i3_rex_remaining_battery_percent', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Remaining battery percent', + 'platform': 'bmw_connected_drive', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'remaining_battery_percent', + 'unique_id': 'WBY00000000REXI01-remaining_battery_percent', + 'unit_of_measurement': '%', + }) +# --- +# name: test_entity_state_attrs[sensor.i3_rex_remaining_battery_percent-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'device_class': 'battery', + 'friendly_name': 'i3 (+ REX) Remaining battery percent', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.i3_rex_remaining_battery_percent', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '82', + }) +# --- +# name: test_entity_state_attrs[sensor.i3_rex_remaining_fuel-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.i3_rex_remaining_fuel', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Remaining fuel', + 'platform': 'bmw_connected_drive', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'remaining_fuel', + 'unique_id': 'WBY00000000REXI01-remaining_fuel', + 'unit_of_measurement': , + }) +# --- +# name: test_entity_state_attrs[sensor.i3_rex_remaining_fuel-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'friendly_name': 'i3 (+ REX) Remaining fuel', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.i3_rex_remaining_fuel', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '6', + }) +# --- +# name: test_entity_state_attrs[sensor.i3_rex_remaining_fuel_percent-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.i3_rex_remaining_fuel_percent', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Remaining fuel percent', + 'platform': 'bmw_connected_drive', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'remaining_fuel_percent', + 'unique_id': 'WBY00000000REXI01-remaining_fuel_percent', + 'unit_of_measurement': '%', + }) +# --- +# name: test_entity_state_attrs[sensor.i3_rex_remaining_fuel_percent-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'friendly_name': 'i3 (+ REX) Remaining fuel percent', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.i3_rex_remaining_fuel_percent', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_entity_state_attrs[sensor.i3_rex_remaining_range_electric-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.i3_rex_remaining_range_electric', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Remaining range electric', + 'platform': 'bmw_connected_drive', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'remaining_range_electric', + 'unique_id': 'WBY00000000REXI01-remaining_range_electric', + 'unit_of_measurement': , + }) +# --- +# name: test_entity_state_attrs[sensor.i3_rex_remaining_range_electric-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'friendly_name': 'i3 (+ REX) Remaining range electric', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.i3_rex_remaining_range_electric', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '174', + }) +# --- +# name: test_entity_state_attrs[sensor.i3_rex_remaining_range_fuel-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.i3_rex_remaining_range_fuel', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Remaining range fuel', + 'platform': 'bmw_connected_drive', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'remaining_range_fuel', + 'unique_id': 'WBY00000000REXI01-remaining_range_fuel', + 'unit_of_measurement': , + }) +# --- +# name: test_entity_state_attrs[sensor.i3_rex_remaining_range_fuel-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'friendly_name': 'i3 (+ REX) Remaining range fuel', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.i3_rex_remaining_range_fuel', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '105', + }) +# --- +# name: test_entity_state_attrs[sensor.i3_rex_remaining_range_total-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.i3_rex_remaining_range_total', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Remaining range total', + 'platform': 'bmw_connected_drive', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'remaining_range_total', + 'unique_id': 'WBY00000000REXI01-remaining_range_total', + 'unit_of_measurement': , + }) +# --- +# name: test_entity_state_attrs[sensor.i3_rex_remaining_range_total-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'friendly_name': 'i3 (+ REX) Remaining range total', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.i3_rex_remaining_range_total', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '279', + }) +# --- +# name: test_entity_state_attrs[sensor.i4_edrive40_ac_current_limit-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.i4_edrive40_ac_current_limit', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'AC current limit', + 'platform': 'bmw_connected_drive', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'ac_current_limit', + 'unique_id': 'WBA00000000DEMO02-ac_current_limit', + 'unit_of_measurement': , + }) +# --- +# name: test_entity_state_attrs[sensor.i4_edrive40_ac_current_limit-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'friendly_name': 'i4 eDrive40 AC current limit', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.i4_edrive40_ac_current_limit', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '16', + }) +# --- +# name: test_entity_state_attrs[sensor.i4_edrive40_charging_end_time-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.i4_edrive40_charging_end_time', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Charging end time', + 'platform': 'bmw_connected_drive', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'charging_end_time', + 'unique_id': 'WBA00000000DEMO02-charging_end_time', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_state_attrs[sensor.i4_edrive40_charging_end_time-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'device_class': 'timestamp', + 'friendly_name': 'i4 eDrive40 Charging end time', + }), + 'context': , + 'entity_id': 'sensor.i4_edrive40_charging_end_time', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2023-06-22T10:40:00+00:00', + }) +# --- +# name: test_entity_state_attrs[sensor.i4_edrive40_charging_start_time-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.i4_edrive40_charging_start_time', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Charging start time', + 'platform': 'bmw_connected_drive', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'charging_start_time', + 'unique_id': 'WBA00000000DEMO02-charging_start_time', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_state_attrs[sensor.i4_edrive40_charging_start_time-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'device_class': 'timestamp', + 'friendly_name': 'i4 eDrive40 Charging start time', + }), + 'context': , + 'entity_id': 'sensor.i4_edrive40_charging_start_time', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_entity_state_attrs[sensor.i4_edrive40_charging_status-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.i4_edrive40_charging_status', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Charging status', + 'platform': 'bmw_connected_drive', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'charging_status', + 'unique_id': 'WBA00000000DEMO02-charging_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_state_attrs[sensor.i4_edrive40_charging_status-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'friendly_name': 'i4 eDrive40 Charging status', + }), + 'context': , + 'entity_id': 'sensor.i4_edrive40_charging_status', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'NOT_CHARGING', + }) +# --- +# name: test_entity_state_attrs[sensor.i4_edrive40_charging_target-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.i4_edrive40_charging_target', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Charging target', + 'platform': 'bmw_connected_drive', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'charging_target', + 'unique_id': 'WBA00000000DEMO02-charging_target', + 'unit_of_measurement': '%', + }) +# --- +# name: test_entity_state_attrs[sensor.i4_edrive40_charging_target-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'friendly_name': 'i4 eDrive40 Charging target', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.i4_edrive40_charging_target', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '80', + }) +# --- +# name: test_entity_state_attrs[sensor.i4_edrive40_climate_status-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'cooling', + 'heating', + 'inactive', + 'standby', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.i4_edrive40_climate_status', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Climate status', + 'platform': 'bmw_connected_drive', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'climate_status', + 'unique_id': 'WBA00000000DEMO02-activity', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_state_attrs[sensor.i4_edrive40_climate_status-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'device_class': 'enum', + 'friendly_name': 'i4 eDrive40 Climate status', + 'options': list([ + 'cooling', + 'heating', + 'inactive', + 'standby', + ]), + }), + 'context': , + 'entity_id': 'sensor.i4_edrive40_climate_status', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'heating', + }) +# --- +# name: test_entity_state_attrs[sensor.i4_edrive40_mileage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.i4_edrive40_mileage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Mileage', + 'platform': 'bmw_connected_drive', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'mileage', + 'unique_id': 'WBA00000000DEMO02-mileage', + 'unit_of_measurement': , + }) +# --- +# name: test_entity_state_attrs[sensor.i4_edrive40_mileage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'friendly_name': 'i4 eDrive40 Mileage', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.i4_edrive40_mileage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1121', + }) +# --- +# name: test_entity_state_attrs[sensor.i4_edrive40_remaining_battery_percent-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.i4_edrive40_remaining_battery_percent', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Remaining battery percent', + 'platform': 'bmw_connected_drive', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'remaining_battery_percent', + 'unique_id': 'WBA00000000DEMO02-remaining_battery_percent', + 'unit_of_measurement': '%', + }) +# --- +# name: test_entity_state_attrs[sensor.i4_edrive40_remaining_battery_percent-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'device_class': 'battery', + 'friendly_name': 'i4 eDrive40 Remaining battery percent', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.i4_edrive40_remaining_battery_percent', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '80', + }) +# --- +# name: test_entity_state_attrs[sensor.i4_edrive40_remaining_range_electric-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.i4_edrive40_remaining_range_electric', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Remaining range electric', + 'platform': 'bmw_connected_drive', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'remaining_range_electric', + 'unique_id': 'WBA00000000DEMO02-remaining_range_electric', + 'unit_of_measurement': , + }) +# --- +# name: test_entity_state_attrs[sensor.i4_edrive40_remaining_range_electric-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'friendly_name': 'i4 eDrive40 Remaining range electric', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.i4_edrive40_remaining_range_electric', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '472', + }) +# --- +# name: test_entity_state_attrs[sensor.i4_edrive40_remaining_range_total-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.i4_edrive40_remaining_range_total', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Remaining range total', + 'platform': 'bmw_connected_drive', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'remaining_range_total', + 'unique_id': 'WBA00000000DEMO02-remaining_range_total', + 'unit_of_measurement': , + }) +# --- +# name: test_entity_state_attrs[sensor.i4_edrive40_remaining_range_total-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'friendly_name': 'i4 eDrive40 Remaining range total', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.i4_edrive40_remaining_range_total', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '472', + }) +# --- +# name: test_entity_state_attrs[sensor.ix_xdrive50_ac_current_limit-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.ix_xdrive50_ac_current_limit', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'AC current limit', + 'platform': 'bmw_connected_drive', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'ac_current_limit', + 'unique_id': 'WBA00000000DEMO01-ac_current_limit', + 'unit_of_measurement': , + }) +# --- +# name: test_entity_state_attrs[sensor.ix_xdrive50_ac_current_limit-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'friendly_name': 'iX xDrive50 AC current limit', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.ix_xdrive50_ac_current_limit', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '16', + }) +# --- +# name: test_entity_state_attrs[sensor.ix_xdrive50_charging_end_time-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.ix_xdrive50_charging_end_time', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Charging end time', + 'platform': 'bmw_connected_drive', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'charging_end_time', + 'unique_id': 'WBA00000000DEMO01-charging_end_time', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_state_attrs[sensor.ix_xdrive50_charging_end_time-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'device_class': 'timestamp', + 'friendly_name': 'iX xDrive50 Charging end time', + }), + 'context': , + 'entity_id': 'sensor.ix_xdrive50_charging_end_time', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2023-06-22T10:40:00+00:00', + }) +# --- +# name: test_entity_state_attrs[sensor.ix_xdrive50_charging_start_time-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.ix_xdrive50_charging_start_time', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Charging start time', + 'platform': 'bmw_connected_drive', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'charging_start_time', + 'unique_id': 'WBA00000000DEMO01-charging_start_time', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_state_attrs[sensor.ix_xdrive50_charging_start_time-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'device_class': 'timestamp', + 'friendly_name': 'iX xDrive50 Charging start time', + }), + 'context': , + 'entity_id': 'sensor.ix_xdrive50_charging_start_time', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_entity_state_attrs[sensor.ix_xdrive50_charging_status-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.ix_xdrive50_charging_status', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Charging status', + 'platform': 'bmw_connected_drive', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'charging_status', + 'unique_id': 'WBA00000000DEMO01-charging_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_state_attrs[sensor.ix_xdrive50_charging_status-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'friendly_name': 'iX xDrive50 Charging status', + }), + 'context': , + 'entity_id': 'sensor.ix_xdrive50_charging_status', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'CHARGING', + }) +# --- +# name: test_entity_state_attrs[sensor.ix_xdrive50_charging_target-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.ix_xdrive50_charging_target', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Charging target', + 'platform': 'bmw_connected_drive', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'charging_target', + 'unique_id': 'WBA00000000DEMO01-charging_target', + 'unit_of_measurement': '%', + }) +# --- +# name: test_entity_state_attrs[sensor.ix_xdrive50_charging_target-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'friendly_name': 'iX xDrive50 Charging target', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.ix_xdrive50_charging_target', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '80', + }) +# --- +# name: test_entity_state_attrs[sensor.ix_xdrive50_climate_status-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'cooling', + 'heating', + 'inactive', + 'standby', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.ix_xdrive50_climate_status', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Climate status', + 'platform': 'bmw_connected_drive', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'climate_status', + 'unique_id': 'WBA00000000DEMO01-activity', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_state_attrs[sensor.ix_xdrive50_climate_status-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'device_class': 'enum', + 'friendly_name': 'iX xDrive50 Climate status', + 'options': list([ + 'cooling', + 'heating', + 'inactive', + 'standby', + ]), + }), + 'context': , + 'entity_id': 'sensor.ix_xdrive50_climate_status', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'inactive', + }) +# --- +# name: test_entity_state_attrs[sensor.ix_xdrive50_mileage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.ix_xdrive50_mileage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Mileage', + 'platform': 'bmw_connected_drive', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'mileage', + 'unique_id': 'WBA00000000DEMO01-mileage', + 'unit_of_measurement': , + }) +# --- +# name: test_entity_state_attrs[sensor.ix_xdrive50_mileage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'friendly_name': 'iX xDrive50 Mileage', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.ix_xdrive50_mileage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1121', + }) +# --- +# name: test_entity_state_attrs[sensor.ix_xdrive50_remaining_battery_percent-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.ix_xdrive50_remaining_battery_percent', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Remaining battery percent', + 'platform': 'bmw_connected_drive', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'remaining_battery_percent', + 'unique_id': 'WBA00000000DEMO01-remaining_battery_percent', + 'unit_of_measurement': '%', + }) +# --- +# name: test_entity_state_attrs[sensor.ix_xdrive50_remaining_battery_percent-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'device_class': 'battery', + 'friendly_name': 'iX xDrive50 Remaining battery percent', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.ix_xdrive50_remaining_battery_percent', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '70', + }) +# --- +# name: test_entity_state_attrs[sensor.ix_xdrive50_remaining_range_electric-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.ix_xdrive50_remaining_range_electric', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Remaining range electric', + 'platform': 'bmw_connected_drive', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'remaining_range_electric', + 'unique_id': 'WBA00000000DEMO01-remaining_range_electric', + 'unit_of_measurement': , + }) +# --- +# name: test_entity_state_attrs[sensor.ix_xdrive50_remaining_range_electric-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'friendly_name': 'iX xDrive50 Remaining range electric', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.ix_xdrive50_remaining_range_electric', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '340', + }) +# --- +# name: test_entity_state_attrs[sensor.ix_xdrive50_remaining_range_total-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.ix_xdrive50_remaining_range_total', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Remaining range total', + 'platform': 'bmw_connected_drive', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'remaining_range_total', + 'unique_id': 'WBA00000000DEMO01-remaining_range_total', + 'unit_of_measurement': , + }) +# --- +# name: test_entity_state_attrs[sensor.ix_xdrive50_remaining_range_total-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'friendly_name': 'iX xDrive50 Remaining range total', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.ix_xdrive50_remaining_range_total', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '340', + }) +# --- +# name: test_entity_state_attrs[sensor.m340i_xdrive_climate_status-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'cooling', + 'heating', + 'inactive', + 'standby', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.m340i_xdrive_climate_status', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Climate status', + 'platform': 'bmw_connected_drive', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'climate_status', + 'unique_id': 'WBA00000000DEMO03-activity', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_state_attrs[sensor.m340i_xdrive_climate_status-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'device_class': 'enum', + 'friendly_name': 'M340i xDrive Climate status', + 'options': list([ + 'cooling', + 'heating', + 'inactive', + 'standby', + ]), + }), + 'context': , + 'entity_id': 'sensor.m340i_xdrive_climate_status', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'inactive', + }) +# --- +# name: test_entity_state_attrs[sensor.m340i_xdrive_mileage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.m340i_xdrive_mileage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Mileage', + 'platform': 'bmw_connected_drive', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'mileage', + 'unique_id': 'WBA00000000DEMO03-mileage', + 'unit_of_measurement': , + }) +# --- +# name: test_entity_state_attrs[sensor.m340i_xdrive_mileage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'friendly_name': 'M340i xDrive Mileage', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.m340i_xdrive_mileage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1121', + }) +# --- +# name: test_entity_state_attrs[sensor.m340i_xdrive_remaining_fuel-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.m340i_xdrive_remaining_fuel', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Remaining fuel', + 'platform': 'bmw_connected_drive', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'remaining_fuel', + 'unique_id': 'WBA00000000DEMO03-remaining_fuel', + 'unit_of_measurement': , + }) +# --- +# name: test_entity_state_attrs[sensor.m340i_xdrive_remaining_fuel-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'friendly_name': 'M340i xDrive Remaining fuel', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.m340i_xdrive_remaining_fuel', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '40', + }) +# --- +# name: test_entity_state_attrs[sensor.m340i_xdrive_remaining_fuel_percent-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.m340i_xdrive_remaining_fuel_percent', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Remaining fuel percent', + 'platform': 'bmw_connected_drive', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'remaining_fuel_percent', + 'unique_id': 'WBA00000000DEMO03-remaining_fuel_percent', + 'unit_of_measurement': '%', + }) +# --- +# name: test_entity_state_attrs[sensor.m340i_xdrive_remaining_fuel_percent-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'friendly_name': 'M340i xDrive Remaining fuel percent', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.m340i_xdrive_remaining_fuel_percent', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '80', + }) +# --- +# name: test_entity_state_attrs[sensor.m340i_xdrive_remaining_range_fuel-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.m340i_xdrive_remaining_range_fuel', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Remaining range fuel', + 'platform': 'bmw_connected_drive', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'remaining_range_fuel', + 'unique_id': 'WBA00000000DEMO03-remaining_range_fuel', + 'unit_of_measurement': , + }) +# --- +# name: test_entity_state_attrs[sensor.m340i_xdrive_remaining_range_fuel-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'friendly_name': 'M340i xDrive Remaining range fuel', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.m340i_xdrive_remaining_range_fuel', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '629', + }) +# --- +# name: test_entity_state_attrs[sensor.m340i_xdrive_remaining_range_total-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.m340i_xdrive_remaining_range_total', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Remaining range total', + 'platform': 'bmw_connected_drive', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'remaining_range_total', + 'unique_id': 'WBA00000000DEMO03-remaining_range_total', + 'unit_of_measurement': , + }) +# --- +# name: test_entity_state_attrs[sensor.m340i_xdrive_remaining_range_total-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'friendly_name': 'M340i xDrive Remaining range total', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.m340i_xdrive_remaining_range_total', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '629', + }) # --- diff --git a/tests/components/bmw_connected_drive/snapshots/test_switch.ambr b/tests/components/bmw_connected_drive/snapshots/test_switch.ambr index a3c8ffb6d3b..5a87a6ddd84 100644 --- a/tests/components/bmw_connected_drive/snapshots/test_switch.ambr +++ b/tests/components/bmw_connected_drive/snapshots/test_switch.ambr @@ -1,53 +1,189 @@ # serializer version: 1 -# name: test_entity_state_attrs - list([ - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by MyBMW', - 'friendly_name': 'iX xDrive50 Climate', - }), - 'context': , - 'entity_id': 'switch.ix_xdrive50_climate', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', +# name: test_entity_state_attrs[switch.i4_edrive40_climate-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by MyBMW', - 'friendly_name': 'iX xDrive50 Charging', - }), - 'context': , - 'entity_id': 'switch.ix_xdrive50_charging', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.i4_edrive40_climate', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by MyBMW', - 'friendly_name': 'i4 eDrive40 Climate', - }), - 'context': , - 'entity_id': 'switch.i4_edrive40_climate', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', + 'name': None, + 'options': dict({ }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by MyBMW', - 'friendly_name': 'M340i xDrive Climate', - }), - 'context': , - 'entity_id': 'switch.m340i_xdrive_climate', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }), - ]) + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Climate', + 'platform': 'bmw_connected_drive', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'climate', + 'unique_id': 'WBA00000000DEMO02-climate', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_state_attrs[switch.i4_edrive40_climate-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'friendly_name': 'i4 eDrive40 Climate', + }), + 'context': , + 'entity_id': 'switch.i4_edrive40_climate', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity_state_attrs[switch.ix_xdrive50_charging-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.ix_xdrive50_charging', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Charging', + 'platform': 'bmw_connected_drive', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'charging', + 'unique_id': 'WBA00000000DEMO01-charging', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_state_attrs[switch.ix_xdrive50_charging-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'friendly_name': 'iX xDrive50 Charging', + }), + 'context': , + 'entity_id': 'switch.ix_xdrive50_charging', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity_state_attrs[switch.ix_xdrive50_climate-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.ix_xdrive50_climate', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Climate', + 'platform': 'bmw_connected_drive', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'climate', + 'unique_id': 'WBA00000000DEMO01-climate', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_state_attrs[switch.ix_xdrive50_climate-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'friendly_name': 'iX xDrive50 Climate', + }), + 'context': , + 'entity_id': 'switch.ix_xdrive50_climate', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_entity_state_attrs[switch.m340i_xdrive_climate-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.m340i_xdrive_climate', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Climate', + 'platform': 'bmw_connected_drive', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'climate', + 'unique_id': 'WBA00000000DEMO03-climate', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_state_attrs[switch.m340i_xdrive_climate-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'friendly_name': 'M340i xDrive Climate', + }), + 'context': , + 'entity_id': 'switch.m340i_xdrive_climate', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) # --- diff --git a/tests/components/bmw_connected_drive/test_button.py b/tests/components/bmw_connected_drive/test_button.py index 3c7db219d54..99cabc900fa 100644 --- a/tests/components/bmw_connected_drive/test_button.py +++ b/tests/components/bmw_connected_drive/test_button.py @@ -1,6 +1,6 @@ """Test BMW buttons.""" -from unittest.mock import AsyncMock +from unittest.mock import AsyncMock, patch from bimmer_connected.models import MyBMWRemoteServiceError from bimmer_connected.vehicle.remote_services import RemoteServices @@ -8,25 +8,33 @@ import pytest import respx from syrupy.assertion import SnapshotAssertion +from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import entity_registry as er from . import check_remote_service_call, setup_mocked_integration +from tests.common import snapshot_platform + @pytest.mark.usefixtures("bmw_fixture") @pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_entity_state_attrs( hass: HomeAssistant, snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, ) -> None: """Test button options and values.""" # Setup component - assert await setup_mocked_integration(hass) + with patch( + "homeassistant.components.bmw_connected_drive.PLATFORMS", + [Platform.BUTTON], + ): + mock_config_entry = await setup_mocked_integration(hass) - # Get all button entities - assert hass.states.async_all("button") == snapshot + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) @pytest.mark.parametrize( diff --git a/tests/components/bmw_connected_drive/test_number.py b/tests/components/bmw_connected_drive/test_number.py index 53e61439003..f2a50ce4df6 100644 --- a/tests/components/bmw_connected_drive/test_number.py +++ b/tests/components/bmw_connected_drive/test_number.py @@ -1,6 +1,6 @@ """Test BMW numbers.""" -from unittest.mock import AsyncMock +from unittest.mock import AsyncMock, patch from bimmer_connected.models import MyBMWAPIError, MyBMWRemoteServiceError from bimmer_connected.vehicle.remote_services import RemoteServices @@ -8,25 +8,33 @@ import pytest import respx from syrupy.assertion import SnapshotAssertion +from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import entity_registry as er from . import check_remote_service_call, setup_mocked_integration +from tests.common import snapshot_platform + @pytest.mark.usefixtures("bmw_fixture") @pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_entity_state_attrs( hass: HomeAssistant, snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, ) -> None: - """Test number options and values..""" + """Test number options and values.""" # Setup component - assert await setup_mocked_integration(hass) + with patch( + "homeassistant.components.bmw_connected_drive.PLATFORMS", + [Platform.NUMBER], + ): + mock_config_entry = await setup_mocked_integration(hass) - # Get all number entities - assert hass.states.async_all("number") == snapshot + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) @pytest.mark.parametrize( diff --git a/tests/components/bmw_connected_drive/test_select.py b/tests/components/bmw_connected_drive/test_select.py index f3877119e3e..37aea4e0839 100644 --- a/tests/components/bmw_connected_drive/test_select.py +++ b/tests/components/bmw_connected_drive/test_select.py @@ -1,6 +1,6 @@ """Test BMW selects.""" -from unittest.mock import AsyncMock +from unittest.mock import AsyncMock, patch from bimmer_connected.models import MyBMWAPIError, MyBMWRemoteServiceError from bimmer_connected.vehicle.remote_services import RemoteServices @@ -8,25 +8,33 @@ import pytest import respx from syrupy.assertion import SnapshotAssertion +from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError, ServiceValidationError +from homeassistant.helpers import entity_registry as er from . import check_remote_service_call, setup_mocked_integration +from tests.common import snapshot_platform + @pytest.mark.usefixtures("bmw_fixture") @pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_entity_state_attrs( hass: HomeAssistant, snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, ) -> None: """Test select options and values..""" # Setup component - assert await setup_mocked_integration(hass) + with patch( + "homeassistant.components.bmw_connected_drive.PLATFORMS", + [Platform.SELECT], + ): + mock_config_entry = await setup_mocked_integration(hass) - # Get all select entities - assert hass.states.async_all("select") == snapshot + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) @pytest.mark.parametrize( diff --git a/tests/components/bmw_connected_drive/test_sensor.py b/tests/components/bmw_connected_drive/test_sensor.py index 2e48189e4a1..b4cdc23ad68 100644 --- a/tests/components/bmw_connected_drive/test_sensor.py +++ b/tests/components/bmw_connected_drive/test_sensor.py @@ -1,9 +1,13 @@ """Test BMW sensors.""" +from unittest.mock import patch + import pytest from syrupy.assertion import SnapshotAssertion +from homeassistant.const import Platform from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er from homeassistant.util.unit_system import ( METRIC_SYSTEM as METRIC, US_CUSTOMARY_SYSTEM as IMPERIAL, @@ -12,6 +16,8 @@ from homeassistant.util.unit_system import ( from . import setup_mocked_integration +from tests.common import snapshot_platform + @pytest.mark.freeze_time("2023-06-22 10:30:00+00:00") @pytest.mark.usefixtures("bmw_fixture") @@ -19,14 +25,17 @@ from . import setup_mocked_integration async def test_entity_state_attrs( hass: HomeAssistant, snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, ) -> None: """Test sensor options and values..""" # Setup component - assert await setup_mocked_integration(hass) + with patch( + "homeassistant.components.bmw_connected_drive.PLATFORMS", [Platform.SENSOR] + ): + mock_config_entry = await setup_mocked_integration(hass) - # Get all select entities - assert hass.states.async_all("sensor") == snapshot + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) @pytest.mark.usefixtures("bmw_fixture") diff --git a/tests/components/bmw_connected_drive/test_switch.py b/tests/components/bmw_connected_drive/test_switch.py index 6cf20d8077e..58bddbfc937 100644 --- a/tests/components/bmw_connected_drive/test_switch.py +++ b/tests/components/bmw_connected_drive/test_switch.py @@ -1,6 +1,6 @@ """Test BMW switches.""" -from unittest.mock import AsyncMock +from unittest.mock import AsyncMock, patch from bimmer_connected.models import MyBMWAPIError, MyBMWRemoteServiceError from bimmer_connected.vehicle.remote_services import RemoteServices @@ -8,24 +8,33 @@ import pytest import respx from syrupy.assertion import SnapshotAssertion +from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import entity_registry as er from . import check_remote_service_call, setup_mocked_integration +from tests.common import snapshot_platform + @pytest.mark.usefixtures("bmw_fixture") +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_entity_state_attrs( hass: HomeAssistant, snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, ) -> None: """Test switch options and values..""" # Setup component - assert await setup_mocked_integration(hass) + with patch( + "homeassistant.components.bmw_connected_drive.PLATFORMS", + [Platform.SWITCH], + ): + mock_config_entry = await setup_mocked_integration(hass) - # Get all switch entities - assert hass.states.async_all("switch") == snapshot + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) @pytest.mark.parametrize( From f9dff1632e1d0d89ac5901e9ffe72a659d7fed43 Mon Sep 17 00:00:00 2001 From: tronikos Date: Mon, 3 Jun 2024 10:48:50 -0700 Subject: [PATCH 0170/1445] Use ISO format when passing date to LLMs (#118705) --- homeassistant/helpers/llm.py | 4 ++-- .../snapshots/test_conversation.ambr | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/homeassistant/helpers/llm.py b/homeassistant/helpers/llm.py index 37233b0d407..31e3c791630 100644 --- a/homeassistant/helpers/llm.py +++ b/homeassistant/helpers/llm.py @@ -35,8 +35,8 @@ from .singleton import singleton LLM_API_ASSIST = "assist" BASE_PROMPT = ( - 'Current time is {{ now().strftime("%X") }}. ' - 'Today\'s date is {{ now().strftime("%x") }}.\n' + 'Current time is {{ now().strftime("%H:%M:%S") }}. ' + 'Today\'s date is {{ now().strftime("%Y-%m-%d") }}.\n' ) DEFAULT_INSTRUCTIONS_PROMPT = """You are a voice assistant for Home Assistant. diff --git a/tests/components/google_generative_ai_conversation/snapshots/test_conversation.ambr b/tests/components/google_generative_ai_conversation/snapshots/test_conversation.ambr index 587586cff17..70db5d11868 100644 --- a/tests/components/google_generative_ai_conversation/snapshots/test_conversation.ambr +++ b/tests/components/google_generative_ai_conversation/snapshots/test_conversation.ambr @@ -30,7 +30,7 @@ 'history': list([ dict({ 'parts': ''' - Current time is 05:00:00. Today's date is 05/24/24. + Current time is 05:00:00. Today's date is 2024-05-24. You are a voice assistant for Home Assistant. Answer in plain text. Keep it simple and to the point. Only if the user wants to control a device, tell them to edit the AI configuration and allow access to Home Assistant. @@ -81,7 +81,7 @@ 'history': list([ dict({ 'parts': ''' - Current time is 05:00:00. Today's date is 05/24/24. + Current time is 05:00:00. Today's date is 2024-05-24. You are a voice assistant for Home Assistant. Answer in plain text. Keep it simple and to the point. Only if the user wants to control a device, tell them to edit the AI configuration and allow access to Home Assistant. @@ -144,7 +144,7 @@ 'history': list([ dict({ 'parts': ''' - Current time is 05:00:00. Today's date is 05/24/24. + Current time is 05:00:00. Today's date is 2024-05-24. You are a voice assistant for Home Assistant. Answer in plain text. Keep it simple and to the point. @@ -199,7 +199,7 @@ 'history': list([ dict({ 'parts': ''' - Current time is 05:00:00. Today's date is 05/24/24. + Current time is 05:00:00. Today's date is 2024-05-24. You are a voice assistant for Home Assistant. Answer in plain text. Keep it simple and to the point. @@ -254,7 +254,7 @@ 'history': list([ dict({ 'parts': ''' - Current time is 05:00:00. Today's date is 05/24/24. + Current time is 05:00:00. Today's date is 2024-05-24. You are a voice assistant for Home Assistant. Answer in plain text. Keep it simple and to the point. @@ -309,7 +309,7 @@ 'history': list([ dict({ 'parts': ''' - Current time is 05:00:00. Today's date is 05/24/24. + Current time is 05:00:00. Today's date is 2024-05-24. You are a voice assistant for Home Assistant. Answer in plain text. Keep it simple and to the point. From 588380392db7108297301d67c30d0d1813095d93 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 3 Jun 2024 12:50:05 -0500 Subject: [PATCH 0171/1445] Small speed up to read-only database sessions (#118674) --- homeassistant/components/recorder/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/recorder/util.py b/homeassistant/components/recorder/util.py index 939a016c960..5894c8c3ce6 100644 --- a/homeassistant/components/recorder/util.py +++ b/homeassistant/components/recorder/util.py @@ -134,7 +134,7 @@ def session_scope( need_rollback = False try: yield session - if session.get_transaction() and not read_only: + if not read_only and session.get_transaction(): need_rollback = True session.commit() except Exception as err: From aac31059b0166729787aafe87fc4e7860dafb730 Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Mon, 3 Jun 2024 13:29:26 -0500 Subject: [PATCH 0172/1445] Resolve areas/floors to ids in intent_script (#118734) --- homeassistant/components/conversation/default_agent.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/conversation/default_agent.py b/homeassistant/components/conversation/default_agent.py index 2366722e929..d5454883292 100644 --- a/homeassistant/components/conversation/default_agent.py +++ b/homeassistant/components/conversation/default_agent.py @@ -871,7 +871,7 @@ class DefaultAgent(ConversationEntity): if device_area is None: return None - return {"area": {"value": device_area.id, "text": device_area.name}} + return {"area": {"value": device_area.name, "text": device_area.name}} def _get_error_text( self, From dd1dd4c6a357ec30be223b8734e9c68699ace637 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Mon, 3 Jun 2024 20:37:48 +0200 Subject: [PATCH 0173/1445] Migrate Intergas InComfort/Intouch Lan2RF gateway YAML to config flow (#118642) * Add config flow * Make sure the device is polled - refactor * Fix * Add tests config flow * Update test requirements * Ensure dispatcher has a unique signal per heater * Followup on review * Follow up comments * One more docstr * Make specific try blocks and refactoring * Handle import exceptions * Restore removed lines * Move initial heater update in try block * Raise issue failed import * Update test codeowners * Remove entity device info * Remove entity device info * Appy suggestions from code review * Remove broad exception handling from entry setup * Test coverage --- .coveragerc | 8 +- CODEOWNERS | 1 + .../components/incomfort/__init__.py | 123 +++++++++---- .../components/incomfort/binary_sensor.py | 22 +-- homeassistant/components/incomfort/climate.py | 22 +-- .../components/incomfort/config_flow.py | 91 ++++++++++ homeassistant/components/incomfort/const.py | 3 + homeassistant/components/incomfort/errors.py | 32 ++++ .../components/incomfort/manifest.json | 1 + homeassistant/components/incomfort/models.py | 40 +++++ homeassistant/components/incomfort/sensor.py | 28 ++- .../components/incomfort/strings.json | 56 ++++++ .../components/incomfort/water_heater.py | 24 ++- homeassistant/generated/config_flows.py | 1 + homeassistant/generated/integrations.json | 2 +- requirements_test_all.txt | 3 + tests/components/incomfort/__init__.py | 1 + tests/components/incomfort/conftest.py | 94 ++++++++++ .../components/incomfort/test_config_flow.py | 163 ++++++++++++++++++ 19 files changed, 621 insertions(+), 94 deletions(-) create mode 100644 homeassistant/components/incomfort/config_flow.py create mode 100644 homeassistant/components/incomfort/const.py create mode 100644 homeassistant/components/incomfort/errors.py create mode 100644 homeassistant/components/incomfort/models.py create mode 100644 homeassistant/components/incomfort/strings.json create mode 100644 tests/components/incomfort/__init__.py create mode 100644 tests/components/incomfort/conftest.py create mode 100644 tests/components/incomfort/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index 625057e9900..0ff06a1184c 100644 --- a/.coveragerc +++ b/.coveragerc @@ -596,7 +596,13 @@ omit = homeassistant/components/ifttt/alarm_control_panel.py homeassistant/components/iglo/light.py homeassistant/components/ihc/* - homeassistant/components/incomfort/* + homeassistant/components/incomfort/__init__.py + homeassistant/components/incomfort/binary_sensor.py + homeassistant/components/incomfort/climate.py + homeassistant/components/incomfort/errors.py + homeassistant/components/incomfort/models.py + homeassistant/components/incomfort/sensor.py + homeassistant/components/incomfort/water_heater.py homeassistant/components/insteon/binary_sensor.py homeassistant/components/insteon/climate.py homeassistant/components/insteon/cover.py diff --git a/CODEOWNERS b/CODEOWNERS index 3f1247de891..a72683c1737 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -659,6 +659,7 @@ build.json @home-assistant/supervisor /homeassistant/components/improv_ble/ @emontnemery /tests/components/improv_ble/ @emontnemery /homeassistant/components/incomfort/ @jbouwh +/tests/components/incomfort/ @jbouwh /homeassistant/components/influxdb/ @mdegat01 /tests/components/influxdb/ @mdegat01 /homeassistant/components/inkbird/ @bdraco diff --git a/homeassistant/components/incomfort/__init__.py b/homeassistant/components/incomfort/__init__.py index 72453bb5290..3f6b36aa27c 100644 --- a/homeassistant/components/incomfort/__init__.py +++ b/homeassistant/components/incomfort/__init__.py @@ -2,24 +2,23 @@ from __future__ import annotations -import logging - from aiohttp import ClientResponseError -from incomfortclient import Gateway as InComfortGateway +from incomfortclient import IncomfortError, InvalidHeaterList import voluptuous as vol +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, Platform -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.discovery import async_load_platform +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant, callback +from homeassistant.data_entry_flow import FlowResultType +from homeassistant.exceptions import ConfigEntryAuthFailed +from homeassistant.helpers import config_validation as cv, issue_registry as ir from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import ConfigType -_LOGGER = logging.getLogger(__name__) - -DOMAIN = "incomfort" +from .const import DOMAIN +from .errors import InConfortTimeout, InConfortUnknownError, NoHeaters, NotFound +from .models import DATA_INCOMFORT, async_connect_gateway CONFIG_SCHEMA = vol.Schema( { @@ -41,35 +40,87 @@ PLATFORMS = ( Platform.CLIMATE, ) +INTEGRATION_TITLE = "Intergas InComfort/Intouch Lan2RF gateway" + + +async def _async_import(hass: HomeAssistant, config: ConfigType) -> None: + """Import config entry from configuration.yaml.""" + if not hass.config_entries.async_entries(DOMAIN): + # Start import flow + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=config + ) + if result["type"] == FlowResultType.ABORT: + ir.async_create_issue( + hass, + DOMAIN, + f"deprecated_yaml_import_issue_{result['reason']}", + breaks_in_ha_version="2025.1.0", + is_fixable=False, + issue_domain=DOMAIN, + severity=ir.IssueSeverity.WARNING, + translation_key=f"deprecated_yaml_import_issue_{result['reason']}", + translation_placeholders={ + "domain": DOMAIN, + "integration_title": INTEGRATION_TITLE, + }, + ) + return + + ir.async_create_issue( + hass, + HOMEASSISTANT_DOMAIN, + f"deprecated_yaml_{DOMAIN}", + breaks_in_ha_version="2025.1.0", + is_fixable=False, + issue_domain=DOMAIN, + severity=ir.IssueSeverity.WARNING, + translation_key="deprecated_yaml", + translation_placeholders={ + "domain": DOMAIN, + "integration_title": INTEGRATION_TITLE, + }, + ) + async def async_setup(hass: HomeAssistant, hass_config: ConfigType) -> bool: """Create an Intergas InComfort/Intouch system.""" - incomfort_data = hass.data[DOMAIN] = {} - - credentials = dict(hass_config[DOMAIN]) - hostname = credentials.pop(CONF_HOST) - - client = incomfort_data["client"] = InComfortGateway( - hostname, **credentials, session=async_get_clientsession(hass) - ) - - try: - heaters = incomfort_data["heaters"] = list(await client.heaters()) - except ClientResponseError as err: - _LOGGER.warning("Setup failed, check your configuration, message is: %s", err) - return False - - for heater in heaters: - await heater.update() - - for platform in PLATFORMS: - hass.async_create_task( - async_load_platform(hass, platform, DOMAIN, {}, hass_config) - ) - + if config := hass_config.get(DOMAIN): + hass.async_create_task(_async_import(hass, config)) return True +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up a config entry.""" + try: + data = await async_connect_gateway(hass, dict(entry.data)) + for heater in data.heaters: + await heater.update() + except InvalidHeaterList as exc: + raise NoHeaters from exc + except IncomfortError as exc: + if isinstance(exc.message, ClientResponseError): + if exc.message.status == 401: + raise ConfigEntryAuthFailed("Incorrect credentials") from exc + if exc.message.status == 404: + raise NotFound from exc + raise InConfortUnknownError from exc + except TimeoutError as exc: + raise InConfortTimeout from exc + + hass.data.setdefault(DATA_INCOMFORT, {entry.entry_id: data}) + + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload config entry.""" + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + del hass.data[DOMAIN][entry.entry_id] + return unload_ok + + class IncomfortEntity(Entity): """Base class for all InComfort entities.""" @@ -77,7 +128,11 @@ class IncomfortEntity(Entity): async def async_added_to_hass(self) -> None: """Set up a listener when this entity is added to HA.""" - self.async_on_remove(async_dispatcher_connect(self.hass, DOMAIN, self._refresh)) + self.async_on_remove( + async_dispatcher_connect( + self.hass, f"{DOMAIN}_{self.unique_id}", self._refresh + ) + ) @callback def _refresh(self) -> None: diff --git a/homeassistant/components/incomfort/binary_sensor.py b/homeassistant/components/incomfort/binary_sensor.py index 04c0c17ba2a..9bfe637e09a 100644 --- a/homeassistant/components/incomfort/binary_sensor.py +++ b/homeassistant/components/incomfort/binary_sensor.py @@ -7,27 +7,23 @@ from typing import Any from incomfortclient import Gateway as InComfortGateway, Heater as InComfortHeater from homeassistant.components.binary_sensor import BinarySensorEntity +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import DOMAIN, IncomfortEntity +from . import DATA_INCOMFORT, IncomfortEntity -async def async_setup_platform( +async def async_setup_entry( hass: HomeAssistant, - config: ConfigType, + entry: ConfigEntry, async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, ) -> None: - """Set up an InComfort/InTouch binary_sensor device.""" - if discovery_info is None: - return - - client = hass.data[DOMAIN]["client"] - heaters = hass.data[DOMAIN]["heaters"] - - async_add_entities([IncomfortFailed(client, h) for h in heaters]) + """Set up an InComfort/InTouch binary_sensor entity.""" + incomfort_data = hass.data[DATA_INCOMFORT][entry.entry_id] + async_add_entities( + IncomfortFailed(incomfort_data.client, h) for h in incomfort_data.heaters + ) class IncomfortFailed(IncomfortEntity, BinarySensorEntity): diff --git a/homeassistant/components/incomfort/climate.py b/homeassistant/components/incomfort/climate.py index 32816900034..21871a66487 100644 --- a/homeassistant/components/incomfort/climate.py +++ b/homeassistant/components/incomfort/climate.py @@ -15,29 +15,25 @@ from homeassistant.components.climate import ( ClimateEntityFeature, HVACMode, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import DOMAIN, IncomfortEntity +from . import DATA_INCOMFORT, IncomfortEntity -async def async_setup_platform( +async def async_setup_entry( hass: HomeAssistant, - config: ConfigType, + entry: ConfigEntry, async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, ) -> None: - """Set up an InComfort/InTouch climate device.""" - if discovery_info is None: - return - - client = hass.data[DOMAIN]["client"] - heaters = hass.data[DOMAIN]["heaters"] - + """Set up InComfort/InTouch climate devices.""" + incomfort_data = hass.data[DATA_INCOMFORT][entry.entry_id] async_add_entities( - [InComfortClimate(client, h, r) for h in heaters for r in h.rooms] + InComfortClimate(incomfort_data.client, h, r) + for h in incomfort_data.heaters + for r in h.rooms ) diff --git a/homeassistant/components/incomfort/config_flow.py b/homeassistant/components/incomfort/config_flow.py new file mode 100644 index 00000000000..bc928997b32 --- /dev/null +++ b/homeassistant/components/incomfort/config_flow.py @@ -0,0 +1,91 @@ +"""Config flow support for Intergas InComfort integration.""" + +from typing import Any + +from aiohttp import ClientResponseError +from incomfortclient import IncomfortError, InvalidHeaterList +import voluptuous as vol + +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import HomeAssistant +from homeassistant.helpers.selector import ( + TextSelector, + TextSelectorConfig, + TextSelectorType, +) + +from .const import DOMAIN +from .models import async_connect_gateway + +TITLE = "Intergas InComfort/Intouch Lan2RF gateway" + +CONFIG_SCHEMA = vol.Schema( + { + vol.Required(CONF_HOST): TextSelector( + TextSelectorConfig(type=TextSelectorType.TEXT) + ), + vol.Optional(CONF_USERNAME): TextSelector( + TextSelectorConfig(type=TextSelectorType.TEXT, autocomplete="admin") + ), + vol.Optional(CONF_PASSWORD): TextSelector( + TextSelectorConfig(type=TextSelectorType.PASSWORD) + ), + } +) + +ERROR_STATUS_MAPPING: dict[int, tuple[str, str]] = { + 401: (CONF_PASSWORD, "auth_error"), + 404: ("base", "not_found"), +} + + +async def async_try_connect_gateway( + hass: HomeAssistant, config: dict[str, Any] +) -> dict[str, str] | None: + """Try to connect to the Lan2RF gateway.""" + try: + await async_connect_gateway(hass, config) + except InvalidHeaterList: + return {"base": "no_heaters"} + except IncomfortError as exc: + if isinstance(exc.message, ClientResponseError): + scope, error = ERROR_STATUS_MAPPING.get( + exc.message.status, ("base", "unknown") + ) + return {scope: error} + return {"base": "unknown"} + except TimeoutError: + return {"base": "timeout_error"} + except Exception: # noqa: BLE001 + return {"base": "unknown"} + + return None + + +class InComfortConfigFlow(ConfigFlow, domain=DOMAIN): + """Config flow to set up an Intergas InComfort boyler and thermostats.""" + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle the initial step.""" + errors: dict[str, str] | None = None + if user_input is not None: + self._async_abort_entries_match({CONF_HOST: user_input[CONF_HOST]}) + if ( + errors := await async_try_connect_gateway(self.hass, user_input) + ) is None: + return self.async_create_entry(title=TITLE, data=user_input) + + return self.async_show_form( + step_id="user", data_schema=CONFIG_SCHEMA, errors=errors + ) + + async def async_step_import(self, import_data: dict[str, Any]) -> ConfigFlowResult: + """Import `incomfort` config entry from configuration.yaml.""" + errors: dict[str, str] | None = None + if (errors := await async_try_connect_gateway(self.hass, import_data)) is None: + return self.async_create_entry(title=TITLE, data=import_data) + reason = next(iter(errors.items()))[1] + return self.async_abort(reason=reason) diff --git a/homeassistant/components/incomfort/const.py b/homeassistant/components/incomfort/const.py new file mode 100644 index 00000000000..721dd8591b0 --- /dev/null +++ b/homeassistant/components/incomfort/const.py @@ -0,0 +1,3 @@ +"""Constants for Intergas InComfort integration.""" + +DOMAIN = "incomfort" diff --git a/homeassistant/components/incomfort/errors.py b/homeassistant/components/incomfort/errors.py new file mode 100644 index 00000000000..1023ce70eec --- /dev/null +++ b/homeassistant/components/incomfort/errors.py @@ -0,0 +1,32 @@ +"""Exceptions raised by Intergas InComfort integration.""" + +from homeassistant.core import DOMAIN +from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError + + +class NotFound(HomeAssistantError): + """Raise exception if no Lan2RF Gateway was found.""" + + translation_domain = DOMAIN + translation_key = "not_found" + + +class NoHeaters(ConfigEntryNotReady): + """Raise exception if no heaters are found.""" + + translation_domain = DOMAIN + translation_key = "no_heaters" + + +class InConfortTimeout(ConfigEntryNotReady): + """Raise exception if no heaters are found.""" + + translation_domain = DOMAIN + translation_key = "timeout_error" + + +class InConfortUnknownError(ConfigEntryNotReady): + """Raise exception if no heaters are found.""" + + translation_domain = DOMAIN + translation_key = "unknown" diff --git a/homeassistant/components/incomfort/manifest.json b/homeassistant/components/incomfort/manifest.json index 3b5a1b76e7d..8ef57047cce 100644 --- a/homeassistant/components/incomfort/manifest.json +++ b/homeassistant/components/incomfort/manifest.json @@ -2,6 +2,7 @@ "domain": "incomfort", "name": "Intergas InComfort/Intouch Lan2RF gateway", "codeowners": ["@jbouwh"], + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/incomfort", "iot_class": "local_polling", "loggers": ["incomfortclient"], diff --git a/homeassistant/components/incomfort/models.py b/homeassistant/components/incomfort/models.py new file mode 100644 index 00000000000..19e4269e0b4 --- /dev/null +++ b/homeassistant/components/incomfort/models.py @@ -0,0 +1,40 @@ +"""Models for Intergas InComfort integration.""" + +from dataclasses import dataclass, field +from typing import Any + +from incomfortclient import Gateway as InComfortGateway, Heater as InComfortHeater + +from homeassistant.const import CONF_HOST +from homeassistant.core import HomeAssistant +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.util.hass_dict import HassKey + +from .const import DOMAIN + + +@dataclass +class InComfortData: + """Keep the Intergas InComfort entry data.""" + + client: InComfortGateway + heaters: list[InComfortHeater] = field(default_factory=list) + + +DATA_INCOMFORT: HassKey[dict[str, InComfortData]] = HassKey(DOMAIN) + + +async def async_connect_gateway( + hass: HomeAssistant, + entry_data: dict[str, Any], +) -> InComfortData: + """Validate the configuration.""" + credentials = dict(entry_data) + hostname = credentials.pop(CONF_HOST) + + client = InComfortGateway( + hostname, **credentials, session=async_get_clientsession(hass) + ) + heaters = await client.heaters() + + return InComfortData(client=client, heaters=heaters) diff --git a/homeassistant/components/incomfort/sensor.py b/homeassistant/components/incomfort/sensor.py index e75fbee2676..d74c6a18e59 100644 --- a/homeassistant/components/incomfort/sensor.py +++ b/homeassistant/components/incomfort/sensor.py @@ -12,13 +12,13 @@ from homeassistant.components.sensor import ( SensorEntity, SensorEntityDescription, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import UnitOfPressure, UnitOfTemperature from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util import slugify -from . import DOMAIN, IncomfortEntity +from . import DATA_INCOMFORT, IncomfortEntity INCOMFORT_HEATER_TEMP = "CV Temp" INCOMFORT_PRESSURE = "CV Pressure" @@ -59,26 +59,18 @@ SENSOR_TYPES: tuple[IncomfortSensorEntityDescription, ...] = ( ) -async def async_setup_platform( +async def async_setup_entry( hass: HomeAssistant, - config: ConfigType, + entry: ConfigEntry, async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, ) -> None: - """Set up an InComfort/InTouch sensor device.""" - if discovery_info is None: - return - - client = hass.data[DOMAIN]["client"] - heaters = hass.data[DOMAIN]["heaters"] - - entities = [ - IncomfortSensor(client, heater, description) - for heater in heaters + """Set up InComfort/InTouch sensor entities.""" + incomfort_data = hass.data[DATA_INCOMFORT][entry.entry_id] + async_add_entities( + IncomfortSensor(incomfort_data.client, heater, description) + for heater in incomfort_data.heaters for description in SENSOR_TYPES - ] - - async_add_entities(entities) + ) class IncomfortSensor(IncomfortEntity, SensorEntity): diff --git a/homeassistant/components/incomfort/strings.json b/homeassistant/components/incomfort/strings.json new file mode 100644 index 00000000000..e94c2e508ad --- /dev/null +++ b/homeassistant/components/incomfort/strings.json @@ -0,0 +1,56 @@ +{ + "config": { + "step": { + "user": { + "description": "Set up new Intergas InComfort Lan2RF Gateway, some older systems might not need credentials to be set up. For newer devices authentication is required.", + "data": { + "host": "[%key:common::config_flow::data::host%]", + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]" + }, + "data_description": { + "host": "Hostname or IP-address of the Intergas InComfort Lan2RF Gateway.", + "username": "The username to log into the gateway. This is `admin` in most cases.", + "password": "The password to log into the gateway, is printed at the bottom of the Lan2RF Gateway or is `intergas` for some older devices." + } + } + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_service%]", + "auth_error": "Invalid credentials.", + "no_heaters": "No heaters found.", + "not_found": "No Lan2RF gateway found.", + "timeout_error": "Time out when connection to Lan2RF gateway.", + "unknown": "Unknown error when connection to Lan2RF gateway." + }, + "error": { + "auth_error": "[%key:component::incomfort::config::abort::auth_error%]", + "no_heaters": "[%key:component::incomfort::config::abort::no_heaters%]", + "not_found": "[%key:component::incomfort::config::abort::not_found%]", + "timeout_error": "[%key:component::incomfort::config::abort::timeout_error%]", + "unknown": "[%key:component::incomfort::config::abort::unknown%]" + } + }, + "issues": { + "deprecated_yaml_import_issue_unknown": { + "title": "YAML import failed with unknown error", + "description": "Configuring {integration_title} using YAML is being removed but there was an unknown error while importing your existing configuration.\nSetup will not proceed.\n\nVerify that your {integration_title} is operating correctly and restart Home Assistant to attempt the import again.\n\nAlternatively, you may remove the `{domain}` configuration from your configuration.yaml entirely, restart Home Assistant, and add the {integration_title} integration manually." + }, + "deprecated_yaml_import_issue_auth_error": { + "title": "YAML import failed due to an authentication error", + "description": "Configuring {integration_title} using YAML is being removed but there was an authentication error while importing your existing configuration.\nSetup will not proceed.\n\nVerify that your {integration_title} is operating correctly and restart Home Assistant to attempt the import again.\n\nAlternatively, you may remove the `{domain}` configuration from your configuration.yaml entirely, restart Home Assistant, and add the {integration_title} integration manually." + }, + "deprecated_yaml_import_issue_no_heaters": { + "title": "YAML import failed because no heaters were found", + "description": "Configuring {integration_title} using YAML is being removed but no heaters were found while importing your existing configuration.\nSetup will not proceed.\n\nVerify that your {integration_title} is operating correctly and restart Home Assistant to attempt the import again.\n\nAlternatively, you may remove the `{domain}` configuration from your configuration.yaml entirely, restart Home Assistant, and add the {integration_title} integration manually." + }, + "deprecated_yaml_import_issue_not_found": { + "title": "YAML import failed because no gateway was found", + "description": "Configuring {integration_title} using YAML is being removed but no Lan2RF gateway was found while importing your existing configuration.\nSetup will not proceed.\n\nVerify that your {integration_title} is operating correctly and restart Home Assistant to attempt the import again.\n\nAlternatively, you may remove the `{domain}` configuration from your configuration.yaml entirely, restart Home Assistant, and add the {integration_title} integration manually." + }, + "deprecated_yaml_import_issue_timeout_error": { + "title": "YAML import failed because of timeout issues", + "description": "Configuring {integration_title} using YAML is being removed but there was a timeout while connecting to your {integration_title} while importing your existing configuration.\nSetup will not proceed.\n\nVerify that your {integration_title} is operating correctly and restart Home Assistant to attempt the import again.\n\nAlternatively, you may remove the `{domain}` configuration from your configuration.yaml entirely, restart Home Assistant, and add the {integration_title} integration manually." + } + } +} diff --git a/homeassistant/components/incomfort/water_heater.py b/homeassistant/components/incomfort/water_heater.py index 883d8555832..6b982b7f71e 100644 --- a/homeassistant/components/incomfort/water_heater.py +++ b/homeassistant/components/incomfort/water_heater.py @@ -9,33 +9,29 @@ from aiohttp import ClientResponseError from incomfortclient import Gateway as InComfortGateway, Heater as InComfortHeater from homeassistant.components.water_heater import WaterHeaterEntity +from homeassistant.config_entries import ConfigEntry from homeassistant.const import UnitOfTemperature from homeassistant.core import HomeAssistant from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import DOMAIN, IncomfortEntity +from . import DATA_INCOMFORT, DOMAIN, IncomfortEntity _LOGGER = logging.getLogger(__name__) HEATER_ATTRS = ["display_code", "display_text", "is_burning"] -async def async_setup_platform( +async def async_setup_entry( hass: HomeAssistant, - config: ConfigType, + entry: ConfigEntry, async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, ) -> None: - """Set up an InComfort/Intouch water_heater device.""" - if discovery_info is None: - return - - client = hass.data[DOMAIN]["client"] - heaters = hass.data[DOMAIN]["heaters"] - - async_add_entities([IncomfortWaterHeater(client, h) for h in heaters]) + """Set up an InComfort/InTouch water_heater device.""" + incomfort_data = hass.data[DATA_INCOMFORT][entry.entry_id] + async_add_entities( + IncomfortWaterHeater(incomfort_data.client, h) for h in incomfort_data.heaters + ) class IncomfortWaterHeater(IncomfortEntity, WaterHeaterEntity): @@ -92,4 +88,4 @@ class IncomfortWaterHeater(IncomfortEntity, WaterHeaterEntity): _LOGGER.warning("Update failed, message is: %s", err) else: - async_dispatcher_send(self.hass, DOMAIN) + async_dispatcher_send(self.hass, f"{DOMAIN}_{self.unique_id}") diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 567c00d63e7..e38513046f1 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -256,6 +256,7 @@ FLOWS = { "imap", "imgw_pib", "improv_ble", + "incomfort", "inkbird", "insteon", "intellifire", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 70995bb3d63..194ca540b3f 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -2809,7 +2809,7 @@ "incomfort": { "name": "Intergas InComfort/Intouch Lan2RF gateway", "integration_type": "hub", - "config_flow": false, + "config_flow": true, "iot_class": "local_polling" }, "indianamichiganpower": { diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ce0764a2c80..3b96777fe38 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -935,6 +935,9 @@ ifaddr==0.2.0 # homeassistant.components.imgw_pib imgw_pib==1.0.1 +# homeassistant.components.incomfort +incomfort-client==0.5.0 + # homeassistant.components.influxdb influxdb-client==1.24.0 diff --git a/tests/components/incomfort/__init__.py b/tests/components/incomfort/__init__.py new file mode 100644 index 00000000000..dd398f37a68 --- /dev/null +++ b/tests/components/incomfort/__init__.py @@ -0,0 +1 @@ +"""Tests for the Intergas InComfort integration.""" diff --git a/tests/components/incomfort/conftest.py b/tests/components/incomfort/conftest.py new file mode 100644 index 00000000000..5f5a2c9be16 --- /dev/null +++ b/tests/components/incomfort/conftest.py @@ -0,0 +1,94 @@ +"""Fixtures for Intergas InComfort integration.""" + +from collections.abc import Generator +from typing import Any +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest + +from homeassistant.core import HomeAssistant + + +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock, None, None]: + """Override async_setup_entry.""" + with patch( + "homeassistant.components.incomfort.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + yield mock_setup_entry + + +@pytest.fixture +def mock_heater_status() -> dict[str, Any]: + """Mock heater status.""" + return { + "display_code": 126, + "display_text": "standby", + "fault_code": None, + "is_burning": False, + "is_failed": False, + "is_pumping": False, + "is_tapping": False, + "heater_temp": 35.34, + "tap_temp": 30.21, + "pressure": 1.86, + "serial_no": "2404c08648", + "nodenr": 249, + "rf_message_rssi": 30, + "rfstatus_cntr": 0, + } + + +@pytest.fixture +def mock_room_status() -> dict[str, Any]: + """Mock room status.""" + return {"room_temp": 21.42, "setpoint": 18.0, "override": 18.0} + + +@pytest.fixture +def mock_incomfort( + hass: HomeAssistant, + mock_heater_status: dict[str, Any], + mock_room_status: dict[str, Any], +) -> Generator[MagicMock, None]: + """Mock the InComfort gateway client.""" + + class MockRoom: + """Mocked InComfort room class.""" + + override: float + room_no: int + room_temp: float + setpoint: float + status: dict[str, Any] + + def __init__(self) -> None: + """Initialize mocked room.""" + self.override = mock_room_status["override"] + self.room_no = 1 + self.room_temp = mock_room_status["room_temp"] + self.setpoint = mock_room_status["setpoint"] + self.status = mock_room_status + + class MockHeater: + """Mocked InComfort heater class.""" + + serial_no: str + status: dict[str, Any] + rooms: list[MockRoom] + + def __init__(self) -> None: + """Initialize mocked heater.""" + self.serial_no = "c0ffeec0ffee" + + async def update(self) -> None: + self.status = mock_heater_status + self.rooms = [MockRoom] + + with patch( + "homeassistant.components.incomfort.models.InComfortGateway", MagicMock() + ) as patch_gateway: + patch_gateway().heaters = AsyncMock() + patch_gateway().heaters.return_value = [MockHeater()] + yield patch_gateway diff --git a/tests/components/incomfort/test_config_flow.py b/tests/components/incomfort/test_config_flow.py new file mode 100644 index 00000000000..08f03d96bdb --- /dev/null +++ b/tests/components/incomfort/test_config_flow.py @@ -0,0 +1,163 @@ +"""Tests for the Intergas InComfort config flow.""" + +from unittest.mock import AsyncMock, MagicMock + +from aiohttp import ClientResponseError +from incomfortclient import IncomfortError, InvalidHeaterList +import pytest + +from homeassistant.components.incomfort import DOMAIN +from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER +from homeassistant.const import CONF_HOST, CONF_PASSWORD +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +from tests.common import MockConfigEntry + +MOCK_CONFIG = { + "host": "192.168.1.12", + "username": "admin", + "password": "verysecret", +} + + +async def test_form( + hass: HomeAssistant, mock_setup_entry: AsyncMock, mock_incomfort: MagicMock +) -> None: + """Test we get the full form.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] is FlowResultType.FORM + assert result["errors"] is None + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], MOCK_CONFIG + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "Intergas InComfort/Intouch Lan2RF gateway" + assert result["data"] == MOCK_CONFIG + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_import( + hass: HomeAssistant, mock_setup_entry: AsyncMock, mock_incomfort: MagicMock +) -> None: + """Test we van import from YAML.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=MOCK_CONFIG + ) + await hass.async_block_till_done() + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "Intergas InComfort/Intouch Lan2RF gateway" + assert result["data"] == MOCK_CONFIG + assert len(mock_setup_entry.mock_calls) == 1 + + +@pytest.mark.parametrize( + ("exc", "abort_reason"), + [ + (IncomfortError(ClientResponseError(None, None, status=401)), "auth_error"), + (IncomfortError(ClientResponseError(None, None, status=404)), "not_found"), + (IncomfortError(ClientResponseError(None, None, status=500)), "unknown"), + (IncomfortError, "unknown"), + (InvalidHeaterList, "no_heaters"), + (ValueError, "unknown"), + (TimeoutError, "timeout_error"), + ], +) +async def test_import_fails( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_incomfort: MagicMock, + exc: Exception, + abort_reason: str, +) -> None: + """Test YAML import fails.""" + mock_incomfort().heaters.side_effect = exc + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=MOCK_CONFIG + ) + await hass.async_block_till_done() + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == abort_reason + assert len(mock_setup_entry.mock_calls) == 0 + + +async def test_entry_already_configured(hass: HomeAssistant) -> None: + """Test aborting if the entry is already configured.""" + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] is FlowResultType.FORM + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_HOST: MOCK_CONFIG[CONF_HOST], + }, + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +@pytest.mark.parametrize( + ("exc", "error", "base"), + [ + ( + IncomfortError(ClientResponseError(None, None, status=401)), + "auth_error", + CONF_PASSWORD, + ), + ( + IncomfortError(ClientResponseError(None, None, status=404)), + "not_found", + "base", + ), + ( + IncomfortError(ClientResponseError(None, None, status=500)), + "unknown", + "base", + ), + (IncomfortError, "unknown", "base"), + (ValueError, "unknown", "base"), + (TimeoutError, "timeout_error", "base"), + (InvalidHeaterList, "no_heaters", "base"), + ], +) +async def test_form_validation( + hass: HomeAssistant, + mock_incomfort: MagicMock, + exc: Exception, + error: str, + base: str, +) -> None: + """Test form validation.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + # Simulate issue and retry + mock_incomfort().heaters.side_effect = exc + result = await hass.config_entries.flow.async_configure( + result["flow_id"], MOCK_CONFIG + ) + assert result["type"] is FlowResultType.FORM + assert result["errors"] == { + base: error, + } + + # Fix the issue and retry + mock_incomfort().heaters.side_effect = None + result = await hass.config_entries.flow.async_configure( + result["flow_id"], MOCK_CONFIG + ) + assert result["type"] is FlowResultType.CREATE_ENTRY + assert "errors" not in result From 2a92f78453e763e7c50bf80be801e15a5bcf0fe7 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 3 Jun 2024 21:08:28 +0200 Subject: [PATCH 0174/1445] Require firmware version 3.1.1 for airgradient (#118744) --- .../components/airgradient/config_flow.py | 9 +++ .../components/airgradient/strings.json | 3 +- tests/components/airgradient/conftest.py | 2 +- .../airgradient/test_config_flow.py | 57 ++++++++++++++++++- 4 files changed, 68 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/airgradient/config_flow.py b/homeassistant/components/airgradient/config_flow.py index c7b617de272..fff2615365e 100644 --- a/homeassistant/components/airgradient/config_flow.py +++ b/homeassistant/components/airgradient/config_flow.py @@ -3,6 +3,8 @@ from typing import Any from airgradient import AirGradientClient, AirGradientError, ConfigurationControl +from awesomeversion import AwesomeVersion +from mashumaro import MissingField import voluptuous as vol from homeassistant.components import zeroconf @@ -12,6 +14,8 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import DOMAIN +MIN_VERSION = AwesomeVersion("3.1.1") + class AirGradientConfigFlow(ConfigFlow, domain=DOMAIN): """AirGradient config flow.""" @@ -38,6 +42,9 @@ class AirGradientConfigFlow(ConfigFlow, domain=DOMAIN): await self.async_set_unique_id(discovery_info.properties["serialno"]) self._abort_if_unique_id_configured(updates={CONF_HOST: host}) + if AwesomeVersion(discovery_info.properties["fw_ver"]) < MIN_VERSION: + return self.async_abort(reason="invalid_version") + session = async_get_clientsession(self.hass) self.client = AirGradientClient(host, session=session) await self.client.get_current_measures() @@ -78,6 +85,8 @@ class AirGradientConfigFlow(ConfigFlow, domain=DOMAIN): current_measures = await self.client.get_current_measures() except AirGradientError: errors["base"] = "cannot_connect" + except MissingField: + return self.async_abort(reason="invalid_version") else: await self.async_set_unique_id(current_measures.serial_number) self._abort_if_unique_id_configured() diff --git a/homeassistant/components/airgradient/strings.json b/homeassistant/components/airgradient/strings.json index 9deaf17d0e4..3b1e9f9ee41 100644 --- a/homeassistant/components/airgradient/strings.json +++ b/homeassistant/components/airgradient/strings.json @@ -15,7 +15,8 @@ } }, "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "invalid_version": "This firmware version is unsupported. Please upgrade the firmware of the device to at least version 3.1.1." }, "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", diff --git a/tests/components/airgradient/conftest.py b/tests/components/airgradient/conftest.py index d2495c11a79..d5857fdc46a 100644 --- a/tests/components/airgradient/conftest.py +++ b/tests/components/airgradient/conftest.py @@ -62,7 +62,7 @@ def mock_new_airgradient_client( def mock_cloud_airgradient_client( mock_airgradient_client: AsyncMock, ) -> Generator[AsyncMock, None, None]: - """Mock a new AirGradient client.""" + """Mock a cloud AirGradient client.""" mock_airgradient_client.get_config.return_value = Config.from_json( load_fixture("get_config_cloud.json", DOMAIN) ) diff --git a/tests/components/airgradient/test_config_flow.py b/tests/components/airgradient/test_config_flow.py index 6bb951f2e26..217d2ac0e8c 100644 --- a/tests/components/airgradient/test_config_flow.py +++ b/tests/components/airgradient/test_config_flow.py @@ -4,6 +4,7 @@ from ipaddress import ip_address from unittest.mock import AsyncMock from airgradient import AirGradientConnectionError, ConfigurationControl +from mashumaro import MissingField from homeassistant.components.airgradient import DOMAIN from homeassistant.components.zeroconf import ZeroconfServiceInfo @@ -14,7 +15,7 @@ from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry -ZEROCONF_DISCOVERY = ZeroconfServiceInfo( +OLD_ZEROCONF_DISCOVERY = ZeroconfServiceInfo( ip_address=ip_address("10.0.0.131"), ip_addresses=[ip_address("10.0.0.131")], hostname="airgradient_84fce612f5b8.local.", @@ -29,6 +30,21 @@ ZEROCONF_DISCOVERY = ZeroconfServiceInfo( }, ) +ZEROCONF_DISCOVERY = ZeroconfServiceInfo( + ip_address=ip_address("10.0.0.131"), + ip_addresses=[ip_address("10.0.0.131")], + hostname="airgradient_84fce612f5b8.local.", + name="airgradient_84fce612f5b8._airgradient._tcp.local.", + port=80, + type="_airgradient._tcp.local.", + properties={ + "vendor": "AirGradient", + "fw_ver": "3.1.1", + "serialno": "84fce612f5b8", + "model": "I-9PSL", + }, +) + async def test_full_flow( hass: HomeAssistant, @@ -119,6 +135,34 @@ async def test_flow_errors( assert result["type"] is FlowResultType.CREATE_ENTRY +async def test_flow_old_firmware_version( + hass: HomeAssistant, + mock_airgradient_client: AsyncMock, + mock_setup_entry: AsyncMock, +) -> None: + """Test flow with old firmware version.""" + mock_airgradient_client.get_current_measures.side_effect = MissingField( + "", object, object + ) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + ) + await hass.async_block_till_done() + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_HOST: "10.0.0.131"}, + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "invalid_version" + + async def test_duplicate( hass: HomeAssistant, mock_airgradient_client: AsyncMock, @@ -197,3 +241,14 @@ async def test_zeroconf_flow_cloud_device( ) assert result["type"] is FlowResultType.CREATE_ENTRY mock_cloud_airgradient_client.set_configuration_control.assert_not_called() + + +async def test_zeroconf_flow_abort_old_firmware(hass: HomeAssistant) -> None: + """Test zeroconf flow aborts with old firmware.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_ZEROCONF}, + data=OLD_ZEROCONF_DISCOVERY, + ) + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "invalid_version" From ff27f8ef103b6fd53018bafb2f292d4d623975d3 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Mon, 3 Jun 2024 21:30:13 +0200 Subject: [PATCH 0175/1445] Add device info to incomfort entities (#118741) * Add device info to incomfort entities * Add DOMAIN import --- homeassistant/components/incomfort/__init__.py | 1 + homeassistant/components/incomfort/binary_sensor.py | 5 +++++ homeassistant/components/incomfort/climate.py | 9 ++++++++- homeassistant/components/incomfort/sensor.py | 5 +++++ homeassistant/components/incomfort/water_heater.py | 11 +++++++++-- 5 files changed, 28 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/incomfort/__init__.py b/homeassistant/components/incomfort/__init__.py index 3f6b36aa27c..c6d479cafb5 100644 --- a/homeassistant/components/incomfort/__init__.py +++ b/homeassistant/components/incomfort/__init__.py @@ -125,6 +125,7 @@ class IncomfortEntity(Entity): """Base class for all InComfort entities.""" _attr_should_poll = False + _attr_has_entity_name = True async def async_added_to_hass(self) -> None: """Set up a listener when this entity is added to HA.""" diff --git a/homeassistant/components/incomfort/binary_sensor.py b/homeassistant/components/incomfort/binary_sensor.py index 9bfe637e09a..a64d028ffc1 100644 --- a/homeassistant/components/incomfort/binary_sensor.py +++ b/homeassistant/components/incomfort/binary_sensor.py @@ -9,9 +9,11 @@ from incomfortclient import Gateway as InComfortGateway, Heater as InComfortHeat from homeassistant.components.binary_sensor import BinarySensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import DATA_INCOMFORT, IncomfortEntity +from .const import DOMAIN async def async_setup_entry( @@ -39,6 +41,9 @@ class IncomfortFailed(IncomfortEntity, BinarySensorEntity): self._heater = heater self._attr_unique_id = f"{heater.serial_no}_failed" + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, heater.serial_no)}, + ) @property def is_on(self) -> bool: diff --git a/homeassistant/components/incomfort/climate.py b/homeassistant/components/incomfort/climate.py index 21871a66487..f1487716d01 100644 --- a/homeassistant/components/incomfort/climate.py +++ b/homeassistant/components/incomfort/climate.py @@ -18,9 +18,11 @@ from homeassistant.components.climate import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import DATA_INCOMFORT, IncomfortEntity +from .const import DOMAIN async def async_setup_entry( @@ -42,6 +44,7 @@ class InComfortClimate(IncomfortEntity, ClimateEntity): _attr_min_temp = 5.0 _attr_max_temp = 30.0 + _attr_name = None _attr_hvac_mode = HVACMode.HEAT _attr_hvac_modes = [HVACMode.HEAT] _attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE @@ -58,7 +61,11 @@ class InComfortClimate(IncomfortEntity, ClimateEntity): self._room = room self._attr_unique_id = f"{heater.serial_no}_{room.room_no}" - self._attr_name = f"Thermostat {room.room_no}" + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, self._attr_unique_id)}, + manufacturer="Intergas", + name=f"Thermostat {room.room_no}", + ) @property def extra_state_attributes(self) -> dict[str, Any]: diff --git a/homeassistant/components/incomfort/sensor.py b/homeassistant/components/incomfort/sensor.py index d74c6a18e59..e12b0a3d199 100644 --- a/homeassistant/components/incomfort/sensor.py +++ b/homeassistant/components/incomfort/sensor.py @@ -15,10 +15,12 @@ from homeassistant.components.sensor import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import UnitOfPressure, UnitOfTemperature from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import slugify from . import DATA_INCOMFORT, IncomfortEntity +from .const import DOMAIN INCOMFORT_HEATER_TEMP = "CV Temp" INCOMFORT_PRESSURE = "CV Pressure" @@ -92,6 +94,9 @@ class IncomfortSensor(IncomfortEntity, SensorEntity): self._heater = heater self._attr_unique_id = f"{heater.serial_no}_{slugify(description.name)}" + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, heater.serial_no)}, + ) @property def native_value(self) -> str | None: diff --git a/homeassistant/components/incomfort/water_heater.py b/homeassistant/components/incomfort/water_heater.py index 6b982b7f71e..239ddae3106 100644 --- a/homeassistant/components/incomfort/water_heater.py +++ b/homeassistant/components/incomfort/water_heater.py @@ -12,10 +12,12 @@ from homeassistant.components.water_heater import WaterHeaterEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import UnitOfTemperature from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import DATA_INCOMFORT, DOMAIN, IncomfortEntity +from . import DATA_INCOMFORT, IncomfortEntity +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) @@ -39,7 +41,7 @@ class IncomfortWaterHeater(IncomfortEntity, WaterHeaterEntity): _attr_min_temp = 30.0 _attr_max_temp = 80.0 - _attr_name = "Boiler" + _attr_name = None _attr_should_poll = True _attr_temperature_unit = UnitOfTemperature.CELSIUS @@ -51,6 +53,11 @@ class IncomfortWaterHeater(IncomfortEntity, WaterHeaterEntity): self._heater = heater self._attr_unique_id = heater.serial_no + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, heater.serial_no)}, + manufacturer="Intergas", + name="Boiler", + ) @property def icon(self) -> str: From 8ea3a6843a584663ca1689ae8e7b54fad7716de6 Mon Sep 17 00:00:00 2001 From: David Bonnes Date: Mon, 3 Jun 2024 20:48:48 +0100 Subject: [PATCH 0176/1445] Harden evohome against failures to retrieve zone schedules (#118517) --- homeassistant/components/evohome/__init__.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/evohome/__init__.py b/homeassistant/components/evohome/__init__.py index 08b65f42688..51b4703ff2c 100644 --- a/homeassistant/components/evohome/__init__.py +++ b/homeassistant/components/evohome/__init__.py @@ -657,16 +657,18 @@ class EvoChild(EvoDevice): assert isinstance(self._evo_device, evo.HotWater | evo.Zone) # mypy check try: - self._schedule = await self._evo_broker.call_client_api( # type: ignore[assignment] + schedule = await self._evo_broker.call_client_api( self._evo_device.get_schedule(), update_state=False ) except evo.InvalidSchedule as err: _LOGGER.warning( - "%s: Unable to retrieve the schedule: %s", + "%s: Unable to retrieve a valid schedule: %s", self._evo_device, err, ) self._schedule = {} + else: + self._schedule = schedule or {} _LOGGER.debug("Schedule['%s'] = %s", self.name, self._schedule) From 299c0de968345ccd0bb70cf5eb8e70e835ee2f32 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 3 Jun 2024 16:27:05 -0400 Subject: [PATCH 0177/1445] Update OpenAI prompt on each interaction (#118747) --- .../openai_conversation/conversation.py | 96 +++++++++---------- .../openai_conversation/test_conversation.py | 50 +++++++++- 2 files changed, 93 insertions(+), 53 deletions(-) diff --git a/homeassistant/components/openai_conversation/conversation.py b/homeassistant/components/openai_conversation/conversation.py index 306e4134b9e..d5e566678f1 100644 --- a/homeassistant/components/openai_conversation/conversation.py +++ b/homeassistant/components/openai_conversation/conversation.py @@ -146,58 +146,58 @@ class OpenAIConversationEntity( messages = self.history[conversation_id] else: conversation_id = ulid.ulid_now() + messages = [] - if ( - user_input.context - and user_input.context.user_id - and ( - user := await self.hass.auth.async_get_user( - user_input.context.user_id - ) + if ( + user_input.context + and user_input.context.user_id + and ( + user := await self.hass.auth.async_get_user(user_input.context.user_id) + ) + ): + user_name = user.name + + try: + if llm_api: + api_prompt = llm_api.api_prompt + else: + api_prompt = llm.async_render_no_api_prompt(self.hass) + + prompt = "\n".join( + ( + template.Template( + llm.BASE_PROMPT + + options.get(CONF_PROMPT, llm.DEFAULT_INSTRUCTIONS_PROMPT), + self.hass, + ).async_render( + { + "ha_name": self.hass.config.location_name, + "user_name": user_name, + "llm_context": llm_context, + }, + parse_result=False, + ), + api_prompt, ) - ): - user_name = user.name + ) - try: - if llm_api: - api_prompt = llm_api.api_prompt - else: - api_prompt = llm.async_render_no_api_prompt(self.hass) + except TemplateError as err: + LOGGER.error("Error rendering prompt: %s", err) + intent_response = intent.IntentResponse(language=user_input.language) + intent_response.async_set_error( + intent.IntentResponseErrorCode.UNKNOWN, + f"Sorry, I had a problem with my template: {err}", + ) + return conversation.ConversationResult( + response=intent_response, conversation_id=conversation_id + ) - prompt = "\n".join( - ( - template.Template( - llm.BASE_PROMPT - + options.get(CONF_PROMPT, llm.DEFAULT_INSTRUCTIONS_PROMPT), - self.hass, - ).async_render( - { - "ha_name": self.hass.config.location_name, - "user_name": user_name, - "llm_context": llm_context, - }, - parse_result=False, - ), - api_prompt, - ) - ) - - except TemplateError as err: - LOGGER.error("Error rendering prompt: %s", err) - intent_response = intent.IntentResponse(language=user_input.language) - intent_response.async_set_error( - intent.IntentResponseErrorCode.UNKNOWN, - f"Sorry, I had a problem with my template: {err}", - ) - return conversation.ConversationResult( - response=intent_response, conversation_id=conversation_id - ) - - messages = [ChatCompletionSystemMessageParam(role="system", content=prompt)] - - messages.append( - ChatCompletionUserMessageParam(role="user", content=user_input.text) - ) + # Create a copy of the variable because we attach it to the trace + messages = [ + ChatCompletionSystemMessageParam(role="system", content=prompt), + *messages[1:], + ChatCompletionUserMessageParam(role="user", content=user_input.text), + ] LOGGER.debug("Prompt: %s", messages) trace.async_conversation_trace_append( diff --git a/tests/components/openai_conversation/test_conversation.py b/tests/components/openai_conversation/test_conversation.py index 05d62ffd61b..002b2df186b 100644 --- a/tests/components/openai_conversation/test_conversation.py +++ b/tests/components/openai_conversation/test_conversation.py @@ -2,6 +2,7 @@ from unittest.mock import AsyncMock, Mock, patch +from freezegun import freeze_time from httpx import Response from openai import RateLimitError from openai.types.chat.chat_completion import ChatCompletion, Choice @@ -214,11 +215,14 @@ async def test_function_call( ), ) - with patch( - "openai.resources.chat.completions.AsyncCompletions.create", - new_callable=AsyncMock, - side_effect=completion_result, - ) as mock_create: + with ( + patch( + "openai.resources.chat.completions.AsyncCompletions.create", + new_callable=AsyncMock, + side_effect=completion_result, + ) as mock_create, + freeze_time("2024-06-03 23:00:00"), + ): result = await conversation.async_converse( hass, "Please call the test function", @@ -227,6 +231,11 @@ async def test_function_call( agent_id=agent_id, ) + assert ( + "Today's date is 2024-06-03." + in mock_create.mock_calls[1][2]["messages"][0]["content"] + ) + assert result.response.response_type == intent.IntentResponseType.ACTION_DONE assert mock_create.mock_calls[1][2]["messages"][3] == { "role": "tool", @@ -262,6 +271,37 @@ async def test_function_call( # AGENT_DETAIL event contains the raw prompt passed to the model detail_event = trace_events[1] assert "Answer in plain text" in detail_event["data"]["messages"][0]["content"] + assert ( + "Today's date is 2024-06-03." + in trace_events[1]["data"]["messages"][0]["content"] + ) + + # Call it again, make sure we have updated prompt + with ( + patch( + "openai.resources.chat.completions.AsyncCompletions.create", + new_callable=AsyncMock, + side_effect=completion_result, + ) as mock_create, + freeze_time("2024-06-04 23:00:00"), + ): + result = await conversation.async_converse( + hass, + "Please call the test function", + None, + context, + agent_id=agent_id, + ) + + assert ( + "Today's date is 2024-06-04." + in mock_create.mock_calls[1][2]["messages"][0]["content"] + ) + # Test old assert message not updated + assert ( + "Today's date is 2024-06-03." + in trace_events[1]["data"]["messages"][0]["content"] + ) @patch( From 035e19be01ae7883eebfae9597a143fb88303252 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 3 Jun 2024 16:29:50 -0400 Subject: [PATCH 0178/1445] Google Gen AI: Copy messages to avoid changing the trace data (#118745) --- .../google_generative_ai_conversation/conversation.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/google_generative_ai_conversation/conversation.py b/homeassistant/components/google_generative_ai_conversation/conversation.py index 2c0b37a1216..6b2f3c11dcc 100644 --- a/homeassistant/components/google_generative_ai_conversation/conversation.py +++ b/homeassistant/components/google_generative_ai_conversation/conversation.py @@ -225,7 +225,7 @@ class GoogleGenerativeAIConversationEntity( messages = self.history[conversation_id] else: conversation_id = ulid.ulid_now() - messages = [{}, {}] + messages = [{}, {"role": "model", "parts": "Ok"}] if ( user_input.context @@ -272,8 +272,11 @@ class GoogleGenerativeAIConversationEntity( response=intent_response, conversation_id=conversation_id ) - messages[0] = {"role": "user", "parts": prompt} - messages[1] = {"role": "model", "parts": "Ok"} + # Make a copy, because we attach it to the trace event. + messages = [ + {"role": "user", "parts": prompt}, + *messages[1:], + ] LOGGER.debug("Input: '%s' with history: %s", user_input.text, messages) trace.async_conversation_trace_append( From 39f5f30ca9b02879212df730492c3a50d7df5164 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Mon, 3 Jun 2024 22:30:37 +0200 Subject: [PATCH 0179/1445] Revert "Allow MQTT device based auto discovery" (#118746) Revert "Allow MQTT device based auto discovery (#109030)" This reverts commit 585892f0678dc054819eb5a0a375077cd9b604b8. --- .../components/mqtt/abbreviations.py | 1 - homeassistant/components/mqtt/const.py | 1 - homeassistant/components/mqtt/discovery.py | 360 +++------ homeassistant/components/mqtt/mixins.py | 35 - homeassistant/components/mqtt/models.py | 10 - homeassistant/components/mqtt/schemas.py | 51 +- tests/components/mqtt/conftest.py | 9 +- tests/components/mqtt/test_device_trigger.py | 38 +- tests/components/mqtt/test_discovery.py | 760 ++---------------- tests/components/mqtt/test_init.py | 2 + tests/components/mqtt/test_tag.py | 10 +- 11 files changed, 171 insertions(+), 1106 deletions(-) diff --git a/homeassistant/components/mqtt/abbreviations.py b/homeassistant/components/mqtt/abbreviations.py index af08fb5218e..c3efe5667ad 100644 --- a/homeassistant/components/mqtt/abbreviations.py +++ b/homeassistant/components/mqtt/abbreviations.py @@ -33,7 +33,6 @@ ABBREVIATIONS = { "cmd_on_tpl": "command_on_template", "cmd_t": "command_topic", "cmd_tpl": "command_template", - "cmp": "components", "cod_arm_req": "code_arm_required", "cod_dis_req": "code_disarm_required", "cod_form": "code_format", diff --git a/homeassistant/components/mqtt/const.py b/homeassistant/components/mqtt/const.py index 2d7b4ecf9e2..9a8e6ae22df 100644 --- a/homeassistant/components/mqtt/const.py +++ b/homeassistant/components/mqtt/const.py @@ -86,7 +86,6 @@ CONF_TEMP_MIN = "min_temp" CONF_CERTIFICATE = "certificate" CONF_CLIENT_KEY = "client_key" CONF_CLIENT_CERT = "client_cert" -CONF_COMPONENTS = "components" CONF_TLS_INSECURE = "tls_insecure" # Device and integration info options diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py index 2893a270be3..2cdd900690c 100644 --- a/homeassistant/components/mqtt/discovery.py +++ b/homeassistant/components/mqtt/discovery.py @@ -10,8 +10,6 @@ import re import time from typing import TYPE_CHECKING, Any -import voluptuous as vol - from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_DEVICE, CONF_PLATFORM from homeassistant.core import HassJobType, HomeAssistant, callback @@ -21,7 +19,7 @@ from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, ) -from homeassistant.helpers.service_info.mqtt import MqttServiceInfo, ReceivePayloadType +from homeassistant.helpers.service_info.mqtt import MqttServiceInfo from homeassistant.helpers.typing import DiscoveryInfoType from homeassistant.loader import async_get_mqtt from homeassistant.util.json import json_loads_object @@ -34,21 +32,15 @@ from .const import ( ATTR_DISCOVERY_PAYLOAD, ATTR_DISCOVERY_TOPIC, CONF_AVAILABILITY, - CONF_COMPONENTS, CONF_ORIGIN, CONF_TOPIC, DOMAIN, SUPPORTED_COMPONENTS, ) -from .models import DATA_MQTT, MqttComponentConfig, MqttOriginInfo, ReceiveMessage -from .schemas import DEVICE_DISCOVERY_SCHEMA, MQTT_ORIGIN_INFO_SCHEMA, SHARED_OPTIONS +from .models import DATA_MQTT, MqttOriginInfo, ReceiveMessage +from .schemas import MQTT_ORIGIN_INFO_SCHEMA from .util import async_forward_entry_setup_and_setup_discovery -ABBREVIATIONS_SET = set(ABBREVIATIONS) -DEVICE_ABBREVIATIONS_SET = set(DEVICE_ABBREVIATIONS) -ORIGIN_ABBREVIATIONS_SET = set(ORIGIN_ABBREVIATIONS) - - _LOGGER = logging.getLogger(__name__) TOPIC_MATCHER = re.compile( @@ -72,7 +64,6 @@ TOPIC_BASE = "~" class MQTTDiscoveryPayload(dict[str, Any]): """Class to hold and MQTT discovery payload and discovery data.""" - device_discovery: bool = False discovery_data: DiscoveryInfoType @@ -91,13 +82,6 @@ def async_log_discovery_origin_info( message: str, discovery_payload: MQTTDiscoveryPayload, level: int = logging.INFO ) -> None: """Log information about the discovery and origin.""" - # We only log origin info once per device discovery - if not _LOGGER.isEnabledFor(level): - # bail early if logging is disabled - return - if discovery_payload.device_discovery: - _LOGGER.log(level, message) - return if CONF_ORIGIN not in discovery_payload: _LOGGER.log(level, message) return @@ -118,151 +102,6 @@ def async_log_discovery_origin_info( ) -@callback -def _replace_abbreviations( - payload: Any | dict[str, Any], - abbreviations: dict[str, str], - abbreviations_set: set[str], -) -> None: - """Replace abbreviations in an MQTT discovery payload.""" - if not isinstance(payload, dict): - return - for key in abbreviations_set.intersection(payload): - payload[abbreviations[key]] = payload.pop(key) - - -@callback -def _replace_all_abbreviations(discovery_payload: Any | dict[str, Any]) -> None: - """Replace all abbreviations in an MQTT discovery payload.""" - - _replace_abbreviations(discovery_payload, ABBREVIATIONS, ABBREVIATIONS_SET) - - if CONF_ORIGIN in discovery_payload: - _replace_abbreviations( - discovery_payload[CONF_ORIGIN], - ORIGIN_ABBREVIATIONS, - ORIGIN_ABBREVIATIONS_SET, - ) - - if CONF_DEVICE in discovery_payload: - _replace_abbreviations( - discovery_payload[CONF_DEVICE], - DEVICE_ABBREVIATIONS, - DEVICE_ABBREVIATIONS_SET, - ) - - if CONF_AVAILABILITY in discovery_payload: - for availability_conf in cv.ensure_list(discovery_payload[CONF_AVAILABILITY]): - _replace_abbreviations(availability_conf, ABBREVIATIONS, ABBREVIATIONS_SET) - - -@callback -def _replace_topic_base(discovery_payload: dict[str, Any]) -> None: - """Replace topic base in MQTT discovery data.""" - base = discovery_payload.pop(TOPIC_BASE) - for key, value in discovery_payload.items(): - if isinstance(value, str) and value: - if value[0] == TOPIC_BASE and key.endswith("topic"): - discovery_payload[key] = f"{base}{value[1:]}" - if value[-1] == TOPIC_BASE and key.endswith("topic"): - discovery_payload[key] = f"{value[:-1]}{base}" - if discovery_payload.get(CONF_AVAILABILITY): - for availability_conf in cv.ensure_list(discovery_payload[CONF_AVAILABILITY]): - if not isinstance(availability_conf, dict): - continue - if topic := str(availability_conf.get(CONF_TOPIC)): - if topic[0] == TOPIC_BASE: - availability_conf[CONF_TOPIC] = f"{base}{topic[1:]}" - if topic[-1] == TOPIC_BASE: - availability_conf[CONF_TOPIC] = f"{topic[:-1]}{base}" - - -@callback -def _generate_device_cleanup_config( - hass: HomeAssistant, object_id: str, node_id: str | None -) -> dict[str, Any]: - """Generate a cleanup message on device cleanup.""" - mqtt_data = hass.data[DATA_MQTT] - device_node_id: str = f"{node_id} {object_id}" if node_id else object_id - config: dict[str, Any] = {CONF_DEVICE: {}, CONF_COMPONENTS: {}} - comp_config = config[CONF_COMPONENTS] - for platform, discover_id in mqtt_data.discovery_already_discovered: - ids = discover_id.split(" ") - component_node_id = ids.pop(0) - component_object_id = " ".join(ids) - if not ids: - continue - if device_node_id == component_node_id: - comp_config[component_object_id] = {CONF_PLATFORM: platform} - - return config if comp_config else {} - - -@callback -def _parse_device_payload( - hass: HomeAssistant, - payload: ReceivePayloadType, - object_id: str, - node_id: str | None, -) -> dict[str, Any]: - """Parse a device discovery payload.""" - device_payload: dict[str, Any] = {} - if payload == "": - if not ( - device_payload := _generate_device_cleanup_config(hass, object_id, node_id) - ): - _LOGGER.warning( - "No device components to cleanup for %s, node_id '%s'", - object_id, - node_id, - ) - return device_payload - try: - device_payload = MQTTDiscoveryPayload(json_loads_object(payload)) - except ValueError: - _LOGGER.warning("Unable to parse JSON %s: '%s'", object_id, payload) - return {} - _replace_all_abbreviations(device_payload) - try: - DEVICE_DISCOVERY_SCHEMA(device_payload) - except vol.Invalid as exc: - _LOGGER.warning( - "Invalid MQTT device discovery payload for %s, %s: '%s'", - object_id, - exc, - payload, - ) - return {} - return device_payload - - -@callback -def _valid_origin_info(discovery_payload: MQTTDiscoveryPayload) -> bool: - """Parse and validate origin info from a single component discovery payload.""" - if CONF_ORIGIN not in discovery_payload: - return True - try: - MQTT_ORIGIN_INFO_SCHEMA(discovery_payload[CONF_ORIGIN]) - except Exception as exc: # noqa:BLE001 - _LOGGER.warning( - "Unable to parse origin information from discovery message: %s, got %s", - exc, - discovery_payload[CONF_ORIGIN], - ) - return False - return True - - -@callback -def _merge_common_options( - component_config: MQTTDiscoveryPayload, device_config: dict[str, Any] -) -> None: - """Merge common options with the component config options.""" - for option in SHARED_OPTIONS: - if option in device_config and option not in component_config: - component_config[option] = device_config.get(option) - - async def async_start( # noqa: C901 hass: HomeAssistant, discovery_topic: str, config_entry: ConfigEntry ) -> None: @@ -306,7 +145,8 @@ async def async_start( # noqa: C901 _LOGGER.warning( ( "Received message on illegal discovery topic '%s'. The topic" - " contains not allowed characters. For more information see " + " contains " + "not allowed characters. For more information see " "https://www.home-assistant.io/integrations/mqtt/#discovery-topic" ), topic, @@ -315,114 +155,108 @@ async def async_start( # noqa: C901 component, node_id, object_id = match.groups() - discovered_components: list[MqttComponentConfig] = [] - if component == CONF_DEVICE: - # Process device based discovery message - # and regenate cleanup config. - device_discovery_payload = _parse_device_payload( - hass, payload, object_id, node_id - ) - if not device_discovery_payload: - return - device_config: dict[str, Any] - origin_config: dict[str, Any] | None - component_configs: dict[str, dict[str, Any]] - device_config = device_discovery_payload[CONF_DEVICE] - origin_config = device_discovery_payload.get(CONF_ORIGIN) - component_configs = device_discovery_payload[CONF_COMPONENTS] - for component_id, config in component_configs.items(): - component = config.pop(CONF_PLATFORM) - # The object_id in the device discovery topic is the unique identifier. - # It is used as node_id for the components it contains. - component_node_id = object_id - # The component_id in the discovery playload is used as object_id - # If we have an additional node_id in the discovery topic, - # we extend the component_id with it. - component_object_id = ( - f"{node_id} {component_id}" if node_id else component_id - ) - _replace_all_abbreviations(config) - # We add wrapper to the discovery payload with the discovery data. - # If the dict is empty after removing the platform, the payload is - # assumed to remove the existing config and we do not want to add - # device or orig or shared availability attributes. - if discovery_payload := MQTTDiscoveryPayload(config): - discovery_payload.device_discovery = True - discovery_payload[CONF_DEVICE] = device_config - discovery_payload[CONF_ORIGIN] = origin_config - # Only assign shared config options - # when they are not set at entity level - _merge_common_options(discovery_payload, device_discovery_payload) - discovered_components.append( - MqttComponentConfig( - component, - component_object_id, - component_node_id, - discovery_payload, - ) - ) - _LOGGER.debug( - "Process device discovery payload %s", device_discovery_payload - ) - device_discovery_id = f"{node_id} {object_id}" if node_id else object_id - message = f"Processing device discovery for '{device_discovery_id}'" - async_log_discovery_origin_info( - message, MQTTDiscoveryPayload(device_discovery_payload) - ) + if component not in SUPPORTED_COMPONENTS: + _LOGGER.warning("Integration %s is not supported", component) + return - else: - # Process component based discovery message + if payload: try: - discovery_payload = MQTTDiscoveryPayload( - json_loads_object(payload) if payload else {} - ) + discovery_payload = MQTTDiscoveryPayload(json_loads_object(payload)) except ValueError: _LOGGER.warning("Unable to parse JSON %s: '%s'", object_id, payload) return - _replace_all_abbreviations(discovery_payload) - if not _valid_origin_info(discovery_payload): - return - discovered_components.append( - MqttComponentConfig(component, object_id, node_id, discovery_payload) - ) + else: + discovery_payload = MQTTDiscoveryPayload({}) - discovery_pending_discovered = mqtt_data.discovery_pending_discovered - for component_config in discovered_components: - component = component_config.component - node_id = component_config.node_id - object_id = component_config.object_id - discovery_payload = component_config.discovery_payload - if component not in SUPPORTED_COMPONENTS: - _LOGGER.warning("Integration %s is not supported", component) - return + for key in list(discovery_payload): + abbreviated_key = key + key = ABBREVIATIONS.get(key, key) + discovery_payload[key] = discovery_payload.pop(abbreviated_key) - if TOPIC_BASE in discovery_payload: - _replace_topic_base(discovery_payload) + if CONF_DEVICE in discovery_payload: + device = discovery_payload[CONF_DEVICE] + for key in list(device): + abbreviated_key = key + key = DEVICE_ABBREVIATIONS.get(key, key) + device[key] = device.pop(abbreviated_key) - # If present, the node_id will be included in the discovery_id. - discovery_id = f"{node_id} {object_id}" if node_id else object_id - discovery_hash = (component, discovery_id) - - if discovery_payload: - # Attach MQTT topic to the payload, used for debug prints - discovery_data = { - ATTR_DISCOVERY_HASH: discovery_hash, - ATTR_DISCOVERY_PAYLOAD: discovery_payload, - ATTR_DISCOVERY_TOPIC: topic, - } - setattr(discovery_payload, "discovery_data", discovery_data) - - if discovery_hash in discovery_pending_discovered: - pending = discovery_pending_discovered[discovery_hash]["pending"] - pending.appendleft(discovery_payload) - _LOGGER.debug( - "Component has already been discovered: %s %s, queuing update", - component, - discovery_id, + if CONF_ORIGIN in discovery_payload: + origin_info: dict[str, Any] = discovery_payload[CONF_ORIGIN] + try: + for key in list(origin_info): + abbreviated_key = key + key = ORIGIN_ABBREVIATIONS.get(key, key) + origin_info[key] = origin_info.pop(abbreviated_key) + MQTT_ORIGIN_INFO_SCHEMA(discovery_payload[CONF_ORIGIN]) + except Exception: # noqa: BLE001 + _LOGGER.warning( + "Unable to parse origin information " + "from discovery message, got %s", + discovery_payload[CONF_ORIGIN], ) return - async_process_discovery_payload(component, discovery_id, discovery_payload) + if CONF_AVAILABILITY in discovery_payload: + for availability_conf in cv.ensure_list( + discovery_payload[CONF_AVAILABILITY] + ): + if isinstance(availability_conf, dict): + for key in list(availability_conf): + abbreviated_key = key + key = ABBREVIATIONS.get(key, key) + availability_conf[key] = availability_conf.pop(abbreviated_key) + + if TOPIC_BASE in discovery_payload: + base = discovery_payload.pop(TOPIC_BASE) + for key, value in discovery_payload.items(): + if isinstance(value, str) and value: + if value[0] == TOPIC_BASE and key.endswith("topic"): + discovery_payload[key] = f"{base}{value[1:]}" + if value[-1] == TOPIC_BASE and key.endswith("topic"): + discovery_payload[key] = f"{value[:-1]}{base}" + if discovery_payload.get(CONF_AVAILABILITY): + for availability_conf in cv.ensure_list( + discovery_payload[CONF_AVAILABILITY] + ): + if not isinstance(availability_conf, dict): + continue + if topic := str(availability_conf.get(CONF_TOPIC)): + if topic[0] == TOPIC_BASE: + availability_conf[CONF_TOPIC] = f"{base}{topic[1:]}" + if topic[-1] == TOPIC_BASE: + availability_conf[CONF_TOPIC] = f"{topic[:-1]}{base}" + + # If present, the node_id will be included in the discovered object id + discovery_id = f"{node_id} {object_id}" if node_id else object_id + discovery_hash = (component, discovery_id) + + if discovery_payload: + # Attach MQTT topic to the payload, used for debug prints + setattr( + discovery_payload, + "__configuration_source__", + f"MQTT (topic: '{topic}')", + ) + discovery_data = { + ATTR_DISCOVERY_HASH: discovery_hash, + ATTR_DISCOVERY_PAYLOAD: discovery_payload, + ATTR_DISCOVERY_TOPIC: topic, + } + setattr(discovery_payload, "discovery_data", discovery_data) + + discovery_payload[CONF_PLATFORM] = "mqtt" + + if discovery_hash in mqtt_data.discovery_pending_discovered: + pending = mqtt_data.discovery_pending_discovered[discovery_hash]["pending"] + pending.appendleft(discovery_payload) + _LOGGER.debug( + "Component has already been discovered: %s %s, queuing update", + component, + discovery_id, + ) + return + + async_process_discovery_payload(component, discovery_id, discovery_payload) @callback def async_process_discovery_payload( @@ -430,7 +264,7 @@ async def async_start( # noqa: C901 ) -> None: """Process the payload of a new discovery.""" - _LOGGER.debug("Process component discovery payload %s", payload) + _LOGGER.debug("Process discovery payload %s", payload) discovery_hash = (component, discovery_id) already_discovered = discovery_hash in mqtt_data.discovery_already_discovered diff --git a/homeassistant/components/mqtt/mixins.py b/homeassistant/components/mqtt/mixins.py index 4ade2f260d4..55b76337db0 100644 --- a/homeassistant/components/mqtt/mixins.py +++ b/homeassistant/components/mqtt/mixins.py @@ -682,7 +682,6 @@ class MqttDiscoveryDeviceUpdateMixin(ABC): self._config_entry = config_entry self._config_entry_id = config_entry.entry_id self._skip_device_removal: bool = False - self._migrate_discovery: str | None = None discovery_hash = get_discovery_hash(discovery_data) self._remove_discovery_updated = async_dispatcher_connect( @@ -721,24 +720,6 @@ class MqttDiscoveryDeviceUpdateMixin(ABC): discovery_hash, discovery_payload, ) - if not discovery_payload and self._migrate_discovery is not None: - # Ignore empty update from migrated and removed discovery config. - self._discovery_data[ATTR_DISCOVERY_TOPIC] = self._migrate_discovery - self._migrate_discovery = None - _LOGGER.info("Component successfully migrated: %s", discovery_hash) - send_discovery_done(self.hass, self._discovery_data) - return - - if discovery_payload and ( - (discovery_topic := discovery_payload.discovery_data[ATTR_DISCOVERY_TOPIC]) - != self._discovery_data[ATTR_DISCOVERY_TOPIC] - ): - # Make sure the migrated discovery topic is removed. - self._migrate_discovery = discovery_topic - _LOGGER.debug("Migrating component: %s", discovery_hash) - self.hass.async_create_task( - async_remove_discovery_payload(self.hass, self._discovery_data) - ) if ( discovery_payload and discovery_payload != self._discovery_data[ATTR_DISCOVERY_PAYLOAD] @@ -835,7 +816,6 @@ class MqttDiscoveryUpdateMixin(Entity): mqtt_data = hass.data[DATA_MQTT] self._registry_hooks = mqtt_data.discovery_registry_hooks discovery_hash: tuple[str, str] = discovery_data[ATTR_DISCOVERY_HASH] - self._migrate_discovery: str | None = None if discovery_hash in self._registry_hooks: self._registry_hooks.pop(discovery_hash)() @@ -918,27 +898,12 @@ class MqttDiscoveryUpdateMixin(Entity): old_payload = self._discovery_data[ATTR_DISCOVERY_PAYLOAD] debug_info.update_entity_discovery_data(self.hass, payload, self.entity_id) if not payload: - if self._migrate_discovery is not None: - # Ignore empty update of the migrated and removed discovery config. - self._discovery_data[ATTR_DISCOVERY_TOPIC] = self._migrate_discovery - self._migrate_discovery = None - _LOGGER.info("Component successfully migrated: %s", self.entity_id) - send_discovery_done(self.hass, self._discovery_data) - return # Empty payload: Remove component _LOGGER.info("Removing component: %s", self.entity_id) self.hass.async_create_task( self._async_process_discovery_update_and_remove() ) elif self._discovery_update: - discovery_topic = payload.discovery_data[ATTR_DISCOVERY_TOPIC] - if discovery_topic != self._discovery_data[ATTR_DISCOVERY_TOPIC]: - # Make sure the migrated discovery topic is removed. - self._migrate_discovery = discovery_topic - _LOGGER.debug("Migrating component: %s", self.entity_id) - self.hass.async_create_task( - async_remove_discovery_payload(self.hass, self._discovery_data) - ) if old_payload != payload: # Non-empty, changed payload: Notify component _LOGGER.info("Updating component: %s", self.entity_id) diff --git a/homeassistant/components/mqtt/models.py b/homeassistant/components/mqtt/models.py index 35276eeb946..f26ed196663 100644 --- a/homeassistant/components/mqtt/models.py +++ b/homeassistant/components/mqtt/models.py @@ -424,15 +424,5 @@ class MqttData: tags: dict[str, dict[str, MQTTTagScanner]] = field(default_factory=dict) -@dataclass(slots=True) -class MqttComponentConfig: - """(component, object_id, node_id, discovery_payload).""" - - component: str - object_id: str - node_id: str | None - discovery_payload: MQTTDiscoveryPayload - - DATA_MQTT: HassKey[MqttData] = HassKey("mqtt") DATA_MQTT_AVAILABLE: HassKey[asyncio.Future[bool]] = HassKey("mqtt_client_available") diff --git a/homeassistant/components/mqtt/schemas.py b/homeassistant/components/mqtt/schemas.py index 587d4f1e154..bbc0194a1a5 100644 --- a/homeassistant/components/mqtt/schemas.py +++ b/homeassistant/components/mqtt/schemas.py @@ -2,8 +2,6 @@ from __future__ import annotations -import logging - import voluptuous as vol from homeassistant.const import ( @@ -12,7 +10,6 @@ from homeassistant.const import ( CONF_ICON, CONF_MODEL, CONF_NAME, - CONF_PLATFORM, CONF_UNIQUE_ID, CONF_VALUE_TEMPLATE, ) @@ -27,13 +24,10 @@ from .const import ( CONF_AVAILABILITY_MODE, CONF_AVAILABILITY_TEMPLATE, CONF_AVAILABILITY_TOPIC, - CONF_COMMAND_TOPIC, - CONF_COMPONENTS, CONF_CONFIGURATION_URL, CONF_CONNECTIONS, CONF_DEPRECATED_VIA_HUB, CONF_ENABLED_BY_DEFAULT, - CONF_ENCODING, CONF_HW_VERSION, CONF_IDENTIFIERS, CONF_JSON_ATTRS_TEMPLATE, @@ -43,9 +37,7 @@ from .const import ( CONF_ORIGIN, CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, - CONF_QOS, CONF_SERIAL_NUMBER, - CONF_STATE_TOPIC, CONF_SUGGESTED_AREA, CONF_SUPPORT_URL, CONF_SW_VERSION, @@ -53,33 +45,8 @@ from .const import ( CONF_VIA_DEVICE, DEFAULT_PAYLOAD_AVAILABLE, DEFAULT_PAYLOAD_NOT_AVAILABLE, - SUPPORTED_COMPONENTS, -) -from .util import valid_publish_topic, valid_qos_schema, valid_subscribe_topic - -_LOGGER = logging.getLogger(__name__) - -# Device discovery options that are also available at entity component level -SHARED_OPTIONS = [ - CONF_AVAILABILITY, - CONF_AVAILABILITY_MODE, - CONF_AVAILABILITY_TEMPLATE, - CONF_AVAILABILITY_TOPIC, - CONF_COMMAND_TOPIC, - CONF_PAYLOAD_AVAILABLE, - CONF_PAYLOAD_NOT_AVAILABLE, - CONF_STATE_TOPIC, -] - -MQTT_ORIGIN_INFO_SCHEMA = vol.All( - vol.Schema( - { - vol.Required(CONF_NAME): cv.string, - vol.Optional(CONF_SW_VERSION): cv.string, - vol.Optional(CONF_SUPPORT_URL): cv.configuration_url, - } - ), ) +from .util import valid_subscribe_topic MQTT_AVAILABILITY_SINGLE_SCHEMA = vol.Schema( { @@ -181,19 +148,3 @@ MQTT_ENTITY_COMMON_SCHEMA = MQTT_AVAILABILITY_SCHEMA.extend( vol.Optional(CONF_UNIQUE_ID): cv.string, } ) - -COMPONENT_CONFIG_SCHEMA = vol.Schema( - {vol.Required(CONF_PLATFORM): vol.In(SUPPORTED_COMPONENTS)} -).extend({}, extra=True) - -DEVICE_DISCOVERY_SCHEMA = MQTT_AVAILABILITY_SCHEMA.extend( - { - vol.Required(CONF_DEVICE): MQTT_ENTITY_DEVICE_INFO_SCHEMA, - vol.Required(CONF_COMPONENTS): vol.Schema({str: COMPONENT_CONFIG_SCHEMA}), - vol.Required(CONF_ORIGIN): MQTT_ORIGIN_INFO_SCHEMA, - vol.Optional(CONF_STATE_TOPIC): valid_subscribe_topic, - vol.Optional(CONF_COMMAND_TOPIC): valid_publish_topic, - vol.Optional(CONF_QOS): valid_qos_schema, - vol.Optional(CONF_ENCODING): cv.string, - } -) diff --git a/tests/components/mqtt/conftest.py b/tests/components/mqtt/conftest.py index 9e82bbbbf7e..91ece381f6d 100644 --- a/tests/components/mqtt/conftest.py +++ b/tests/components/mqtt/conftest.py @@ -2,7 +2,7 @@ from collections.abc import Generator from random import getrandbits -from unittest.mock import AsyncMock, patch +from unittest.mock import patch import pytest @@ -29,10 +29,3 @@ def mock_temp_dir(temp_dir_prefix: str) -> Generator[None, None, str]: f"home-assistant-mqtt-{temp_dir_prefix}-{getrandbits(10):03x}", ) as mocked_temp_dir: yield mocked_temp_dir - - -@pytest.fixture -def tag_mock() -> Generator[AsyncMock, None, None]: - """Fixture to mock tag.""" - with patch("homeassistant.components.tag.async_scan_tag") as mock_tag: - yield mock_tag diff --git a/tests/components/mqtt/test_device_trigger.py b/tests/components/mqtt/test_device_trigger.py index 1971ad70547..9e75ea5168b 100644 --- a/tests/components/mqtt/test_device_trigger.py +++ b/tests/components/mqtt/test_device_trigger.py @@ -35,42 +35,22 @@ def calls(hass: HomeAssistant) -> list[ServiceCall]: return async_mock_service(hass, "test", "automation") -@pytest.mark.parametrize( - ("discovery_topic", "data"), - [ - ( - "homeassistant/device_automation/0AFFD2/bla/config", - '{ "automation_type":"trigger",' - ' "device":{"identifiers":["0AFFD2"]},' - ' "payload": "short_press",' - ' "topic": "foobar/triggers/button1",' - ' "type": "button_short_press",' - ' "subtype": "button_1" }', - ), - ( - "homeassistant/device/0AFFD2/config", - '{ "device":{"identifiers":["0AFFD2"]},' - ' "o": {"name": "foobar"}, "cmp": ' - '{ "bla": {' - ' "automation_type":"trigger", ' - ' "payload": "short_press",' - ' "topic": "foobar/triggers/button1",' - ' "type": "button_short_press",' - ' "subtype": "button_1",' - ' "platform":"device_automation"}}}', - ), - ], -) async def test_get_triggers( hass: HomeAssistant, device_registry: dr.DeviceRegistry, mqtt_mock_entry: MqttMockHAClientGenerator, - discovery_topic: str, - data: str, ) -> None: """Test we get the expected triggers from a discovered mqtt device.""" await mqtt_mock_entry() - async_fire_mqtt_message(hass, discovery_topic, data) + data1 = ( + '{ "automation_type":"trigger",' + ' "device":{"identifiers":["0AFFD2"]},' + ' "payload": "short_press",' + ' "topic": "foobar/triggers/button1",' + ' "type": "button_short_press",' + ' "subtype": "button_1" }' + ) + async_fire_mqtt_message(hass, "homeassistant/device_automation/bla/config", data1) await hass.async_block_till_done() device_entry = device_registry.async_get_device(identifiers={("mqtt", "0AFFD2")}) diff --git a/tests/components/mqtt/test_discovery.py b/tests/components/mqtt/test_discovery.py index 3404190d871..2e1f78c1bd4 100644 --- a/tests/components/mqtt/test_discovery.py +++ b/tests/components/mqtt/test_discovery.py @@ -5,14 +5,12 @@ import copy import json from pathlib import Path import re -from typing import Any -from unittest.mock import ANY, AsyncMock, MagicMock, call, patch +from unittest.mock import AsyncMock, call, patch import pytest from homeassistant import config_entries from homeassistant.components import mqtt -from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.mqtt.abbreviations import ( ABBREVIATIONS, DEVICE_ABBREVIATIONS, @@ -43,13 +41,11 @@ from homeassistant.setup import async_setup_component from homeassistant.util.signal_type import SignalTypeFormat from .test_common import help_all_subscribe_calls, help_test_unload_config_entry -from .test_tag import DEFAULT_TAG_ID, DEFAULT_TAG_SCAN from tests.common import ( MockConfigEntry, async_capture_events, async_fire_mqtt_message, - async_get_device_automations, mock_config_flow, mock_platform, ) @@ -89,8 +85,6 @@ async def test_subscribing_config_topic( [ ("homeassistant/binary_sensor/bla/not_config", False), ("homeassistant/binary_sensor/rörkrökare/config", True), - ("homeassistant/device/bla/not_config", False), - ("homeassistant/device/rörkrökare/config", True), ], ) async def test_invalid_topic( @@ -119,15 +113,10 @@ async def test_invalid_topic( caplog.clear() -@pytest.mark.parametrize( - "discovery_topic", - ["homeassistant/binary_sensor/bla/config", "homeassistant/device/bla/config"], -) async def test_invalid_json( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, caplog: pytest.LogCaptureFixture, - discovery_topic: str, ) -> None: """Test sending in invalid JSON.""" await mqtt_mock_entry() @@ -136,7 +125,9 @@ async def test_invalid_json( ) as mock_dispatcher_send: mock_dispatcher_send = AsyncMock(return_value=None) - async_fire_mqtt_message(hass, discovery_topic, "not json") + async_fire_mqtt_message( + hass, "homeassistant/binary_sensor/bla/config", "not json" + ) await hass.async_block_till_done() assert "Unable to parse JSON" in caplog.text assert not mock_dispatcher_send.called @@ -185,43 +176,6 @@ async def test_invalid_config( assert "Error 'expected int for dictionary value @ data['qos']'" in caplog.text -async def test_invalid_device_discovery_config( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, -) -> None: - """Test sending in JSON that violates the discovery schema if device or platform key is missing.""" - await mqtt_mock_entry() - async_fire_mqtt_message( - hass, - "homeassistant/device/bla/config", - '{ "o": {"name": "foobar"}, "cmp": ' - '{ "acp1": {"name": "abc", "state_topic": "home/alarm", ' - '"command_topic": "home/alarm/set", ' - '"platform":"alarm_control_panel"}}}', - ) - await hass.async_block_till_done() - assert ( - "Invalid MQTT device discovery payload for bla, " - "required key not provided @ data['device']" in caplog.text - ) - - caplog.clear() - async_fire_mqtt_message( - hass, - "homeassistant/device/bla/config", - '{ "o": {"name": "foobar"}, "dev": {"identifiers": ["ABDE03"]}, ' - '"cmp": { "acp1": {"name": "abc", "state_topic": "home/alarm", ' - '"command_topic": "home/alarm/set" }}}', - ) - await hass.async_block_till_done() - assert ( - "Invalid MQTT device discovery payload for bla, " - "required key not provided @ data['components']['acp1']['platform']" - in caplog.text - ) - - async def test_only_valid_components( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, @@ -267,51 +221,17 @@ async def test_correct_config_discovery( assert ("binary_sensor", "bla") in hass.data["mqtt"].discovery_already_discovered -@pytest.mark.parametrize( - ("discovery_topic", "payloads", "discovery_id"), - [ - ( - "homeassistant/binary_sensor/bla/config", - ( - '{"name":"Beer","state_topic": "test-topic",' - '"o":{"name":"bla2mqtt","sw":"1.0"},"dev":{"identifiers":["bla"]}}', - '{"name":"Milk","state_topic": "test-topic",' - '"o":{"name":"bla2mqtt","sw":"1.1",' - '"url":"https://bla2mqtt.example.com/support"},' - '"dev":{"identifiers":["bla"]}}', - ), - "bla", - ), - ( - "homeassistant/device/bla/config", - ( - '{"cmp":{"bin_sens1":{"platform":"binary_sensor",' - '"name":"Beer","state_topic": "test-topic"}},' - '"o":{"name":"bla2mqtt","sw":"1.0"},"dev":{"identifiers":["bla"]}}', - '{"cmp":{"bin_sens1":{"platform":"binary_sensor",' - '"name":"Milk","state_topic": "test-topic"}},' - '"o":{"name":"bla2mqtt","sw":"1.1",' - '"url":"https://bla2mqtt.example.com/support"},' - '"dev":{"identifiers":["bla"]}}', - ), - "bla bin_sens1", - ), - ], -) async def test_discovery_integration_info( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, caplog: pytest.LogCaptureFixture, - discovery_topic: str, - payloads: tuple[str, str], - discovery_id: str, ) -> None: - """Test discovery of integration info.""" + """Test logging discovery of new and updated items.""" await mqtt_mock_entry() async_fire_mqtt_message( hass, - discovery_topic, - payloads[0], + "homeassistant/binary_sensor/bla/config", + '{ "name": "Beer", "state_topic": "test-topic", "o": {"name": "bla2mqtt", "sw": "1.0" } }', ) await hass.async_block_till_done() @@ -321,10 +241,7 @@ async def test_discovery_integration_info( assert state.name == "Beer" assert ( - "Processing device discovery for 'bla' from external " - "application bla2mqtt, version: 1.0" - in caplog.text - or f"Found new component: binary_sensor {discovery_id} from external application bla2mqtt, version: 1.0" + "Found new component: binary_sensor bla from external application bla2mqtt, version: 1.0" in caplog.text ) caplog.clear() @@ -332,8 +249,8 @@ async def test_discovery_integration_info( # Send an update and add support url async_fire_mqtt_message( hass, - discovery_topic, - payloads[1], + "homeassistant/binary_sensor/bla/config", + '{ "name": "Milk", "state_topic": "test-topic", "o": {"name": "bla2mqtt", "sw": "1.1", "url": "https://bla2mqtt.example.com/support" } }', ) await hass.async_block_till_done() state = hass.states.get("binary_sensor.beer") @@ -342,343 +259,31 @@ async def test_discovery_integration_info( assert state.name == "Milk" assert ( - f"Component has already been discovered: binary_sensor {discovery_id}" + "Component has already been discovered: binary_sensor bla, sending update from external application bla2mqtt, version: 1.1, support URL: https://bla2mqtt.example.com/support" in caplog.text ) @pytest.mark.parametrize( - ("single_configs", "device_discovery_topic", "device_config"), + "config_message", [ - ( - [ - ( - "homeassistant/device_automation/0AFFD2/bla1/config", - { - "device": {"identifiers": ["0AFFD2"]}, - "automation_type": "trigger", - "payload": "short_press", - "topic": "foobar/triggers/button1", - "type": "button_short_press", - "subtype": "button_1", - }, - ), - ( - "homeassistant/sensor/0AFFD2/bla2/config", - { - "device": {"identifiers": ["0AFFD2"]}, - "state_topic": "foobar/sensors/bla2/state", - }, - ), - ( - "homeassistant/tag/0AFFD2/bla3/config", - { - "device": {"identifiers": ["0AFFD2"]}, - "topic": "foobar/tags/bla3/see", - }, - ), - ], - "homeassistant/device/0AFFD2/config", - { - "device": {"identifiers": ["0AFFD2"]}, - "o": {"name": "foobar"}, - "cmp": { - "bla1": { - "platform": "device_automation", - "automation_type": "trigger", - "payload": "short_press", - "topic": "foobar/triggers/button1", - "type": "button_short_press", - "subtype": "button_1", - }, - "bla2": { - "platform": "sensor", - "state_topic": "foobar/sensors/bla2/state", - }, - "bla3": { - "platform": "tag", - "topic": "foobar/tags/bla3/see", - }, - }, - }, - ) - ], -) -async def test_discovery_migration( - hass: HomeAssistant, - device_registry: dr.DeviceRegistry, - mqtt_mock_entry: MqttMockHAClientGenerator, - tag_mock: AsyncMock, - single_configs: list[tuple[str, dict[str, Any]]], - device_discovery_topic: str, - device_config: dict[str, Any], -) -> None: - """Test the migration of single discovery to device discovery.""" - mock_mqtt = await mqtt_mock_entry() - publish_mock: MagicMock = mock_mqtt._mqttc.publish - - # Discovery single config schema - for discovery_topic, config in single_configs: - payload = json.dumps(config) - async_fire_mqtt_message( - hass, - discovery_topic, - payload, - ) - await hass.async_block_till_done() - await hass.async_block_till_done() - - async def check_discovered_items(): - # Check the device_trigger was discovered - device_entry = device_registry.async_get_device( - identifiers={("mqtt", "0AFFD2")} - ) - assert device_entry is not None - triggers = await async_get_device_automations( - hass, DeviceAutomationType.TRIGGER, device_entry.id - ) - assert len(triggers) == 1 - # Check the sensor was discovered - state = hass.states.get("sensor.mqtt_sensor") - assert state is not None - - # Check the tag works - async_fire_mqtt_message(hass, "foobar/tags/bla3/see", DEFAULT_TAG_SCAN) - await hass.async_block_till_done() - tag_mock.assert_called_once_with(ANY, DEFAULT_TAG_ID, device_entry.id) - tag_mock.reset_mock() - - await check_discovered_items() - - # Migrate to device based discovery - payload = json.dumps(device_config) - async_fire_mqtt_message( - hass, - device_discovery_topic, - payload, - ) - await hass.async_block_till_done() - # Test the single discovery topics are reset and `None` is published - await check_discovered_items() - assert len(publish_mock.mock_calls) == len(single_configs) - published_topics = {call[1][0] for call in publish_mock.mock_calls} - expected_topics = {item[0] for item in single_configs} - assert published_topics == expected_topics - published_payloads = [call[1][1] for call in publish_mock.mock_calls] - assert published_payloads == [None, None, None] - - -@pytest.mark.parametrize( - ("discovery_topic", "payload", "discovery_id"), - [ - ( - "homeassistant/binary_sensor/bla/config", - '{"name":"Beer","state_topic": "test-topic",' - '"avty": {"topic": "avty-topic"},' - '"o":{"name":"bla2mqtt","sw":"1.0"},"dev":{"identifiers":["bla"]}}', - "bla", - ), - ( - "homeassistant/device/bla/config", - '{"cmp":{"bin_sens1":{"platform":"binary_sensor",' - '"name":"Beer","state_topic": "test-topic"}},' - '"avty": {"topic": "avty-topic"},' - '"o":{"name":"bla2mqtt","sw":"1.0"},"dev":{"identifiers":["bla"]}}', - "bin_sens1 bla", - ), - ], -) -async def test_discovery_availability( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, - discovery_topic: str, - payload: str, - discovery_id: str, -) -> None: - """Test device discovery with shared availability mapping.""" - await mqtt_mock_entry() - async_fire_mqtt_message( - hass, - discovery_topic, - payload, - ) - await hass.async_block_till_done() - state = hass.states.get("binary_sensor.beer") - assert state is not None - assert state.name == "Beer" - assert state.state == STATE_UNAVAILABLE - - async_fire_mqtt_message( - hass, - "avty-topic", - "online", - ) - await hass.async_block_till_done() - state = hass.states.get("binary_sensor.beer") - assert state is not None - assert state.state == STATE_UNKNOWN - - async_fire_mqtt_message( - hass, - "test-topic", - "ON", - ) - await hass.async_block_till_done() - state = hass.states.get("binary_sensor.beer") - assert state is not None - assert state.state == STATE_ON - - -@pytest.mark.parametrize( - ("discovery_topic", "payload", "discovery_id"), - [ - ( - "homeassistant/device/bla/config", - '{"cmp":{"bin_sens1":{"platform":"binary_sensor",' - '"avty": {"topic": "avty-topic-component"},' - '"name":"Beer","state_topic": "test-topic"}},' - '"avty": {"topic": "avty-topic-device"},' - '"o":{"name":"bla2mqtt","sw":"1.0"},"dev":{"identifiers":["bla"]}}', - "bin_sens1 bla", - ), - ( - "homeassistant/device/bla/config", - '{"cmp":{"bin_sens1":{"platform":"binary_sensor",' - '"availability_topic": "avty-topic-component",' - '"name":"Beer","state_topic": "test-topic"}},' - '"availability_topic": "avty-topic-device",' - '"o":{"name":"bla2mqtt","sw":"1.0"},"dev":{"identifiers":["bla"]}}', - "bin_sens1 bla", - ), - ], -) -async def test_discovery_component_availability_overridden( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, - discovery_topic: str, - payload: str, - discovery_id: str, -) -> None: - """Test device discovery with overridden shared availability mapping.""" - await mqtt_mock_entry() - async_fire_mqtt_message( - hass, - discovery_topic, - payload, - ) - await hass.async_block_till_done() - state = hass.states.get("binary_sensor.beer") - assert state is not None - assert state.name == "Beer" - assert state.state == STATE_UNAVAILABLE - - async_fire_mqtt_message( - hass, - "avty-topic-device", - "online", - ) - await hass.async_block_till_done() - state = hass.states.get("binary_sensor.beer") - assert state is not None - assert state.state == STATE_UNAVAILABLE - - async_fire_mqtt_message( - hass, - "avty-topic-component", - "online", - ) - await hass.async_block_till_done() - state = hass.states.get("binary_sensor.beer") - assert state is not None - assert state.state == STATE_UNKNOWN - - async_fire_mqtt_message( - hass, - "test-topic", - "ON", - ) - await hass.async_block_till_done() - state = hass.states.get("binary_sensor.beer") - assert state is not None - assert state.state == STATE_ON - - -@pytest.mark.parametrize( - ("discovery_topic", "config_message", "error_message"), - [ - ( - "homeassistant/binary_sensor/bla/config", - '{ "name": "Beer", "state_topic": "test-topic", "o": "bla2mqtt" }', - "Unable to parse origin information from discovery message", - ), - ( - "homeassistant/binary_sensor/bla/config", - '{ "name": "Beer", "state_topic": "test-topic", "o": 2.0 }', - "Unable to parse origin information from discovery message", - ), - ( - "homeassistant/binary_sensor/bla/config", - '{ "name": "Beer", "state_topic": "test-topic", "o": null }', - "Unable to parse origin information from discovery message", - ), - ( - "homeassistant/binary_sensor/bla/config", - '{ "name": "Beer", "state_topic": "test-topic", "o": {"sw": "bla2mqtt"} }', - "Unable to parse origin information from discovery message", - ), - ( - "homeassistant/device/bla/config", - '{"dev":{"identifiers":["bs1"]},"cmp":{"bs1":' - '{"platform":"binary_sensor","name":"Beer","state_topic":"test-topic"}' - '},"o": "bla2mqtt"' - "}", - "Invalid MQTT device discovery payload for bla, " - "expected a dictionary for dictionary value @ data['origin']", - ), - ( - "homeassistant/device/bla/config", - '{"dev":{"identifiers":["bs1"]},"cmp":{"bs1":' - '{"platform":"binary_sensor","name":"Beer","state_topic":"test-topic"}' - '},"o": 2.0' - "}", - "Invalid MQTT device discovery payload for bla, " - "expected a dictionary for dictionary value @ data['origin']", - ), - ( - "homeassistant/device/bla/config", - '{"dev":{"identifiers":["bs1"]},"cmp":{"bs1":' - '{"platform":"binary_sensor","name":"Beer","state_topic":"test-topic"}' - '},"o": null' - "}", - "Invalid MQTT device discovery payload for bla, " - "expected a dictionary for dictionary value @ data['origin']", - ), - ( - "homeassistant/device/bla/config", - '{"dev":{"identifiers":["bs1"]},"cmp":{"bs1":' - '{"platform":"binary_sensor","name":"Beer","state_topic":"test-topic"}' - '},"o": {"sw": "bla2mqtt"}' - "}", - "Invalid MQTT device discovery payload for bla, " - "required key not provided @ data['origin']['name']", - ), + '{ "name": "Beer", "state_topic": "test-topic", "o": "bla2mqtt" }', + '{ "name": "Beer", "state_topic": "test-topic", "o": 2.0 }', + '{ "name": "Beer", "state_topic": "test-topic", "o": null }', + '{ "name": "Beer", "state_topic": "test-topic", "o": {"sw": "bla2mqtt"} }', ], ) async def test_discovery_with_invalid_integration_info( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, caplog: pytest.LogCaptureFixture, - discovery_topic: str, config_message: str, - error_message: str, ) -> None: """Test sending in correct JSON.""" await mqtt_mock_entry() async_fire_mqtt_message( hass, - discovery_topic, + "homeassistant/binary_sensor/bla/config", config_message, ) await hass.async_block_till_done() @@ -686,7 +291,9 @@ async def test_discovery_with_invalid_integration_info( state = hass.states.get("binary_sensor.beer") assert state is None - assert error_message in caplog.text + assert ( + "Unable to parse origin information from discovery message, got" in caplog.text + ) async def test_discover_fan( @@ -1215,63 +822,35 @@ async def test_duplicate_removal( assert "Component has already been discovered: binary_sensor bla" not in caplog.text -@pytest.mark.parametrize( - ("discovery_topic", "discovery_payload", "entity_ids"), - [ - ( - "homeassistant/sensor/bla/config", - '{ "device":{"identifiers":["0AFFD2"]},' - ' "state_topic": "foobar/sensor",' - ' "unique_id": "unique" }', - ["sensor.none_mqtt_sensor"], - ), - ( - "homeassistant/device/bla/config", - '{ "device":{"identifiers":["0AFFD2"]},' - ' "o": {"name": "foobar"},' - ' "cmp": {"sens1": {' - ' "platform": "sensor",' - ' "name": "sensor1",' - ' "state_topic": "foobar/sensor1",' - ' "unique_id": "unique1"' - ' },"sens2": {' - ' "platform": "sensor",' - ' "name": "sensor2",' - ' "state_topic": "foobar/sensor2",' - ' "unique_id": "unique2"' - "}}}", - ["sensor.none_sensor1", "sensor.none_sensor2"], - ), - ], -) async def test_cleanup_device( hass: HomeAssistant, hass_ws_client: WebSocketGenerator, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, mqtt_mock_entry: MqttMockHAClientGenerator, - discovery_topic: str, - discovery_payload: str, - entity_ids: list[str], ) -> None: """Test discovered device is cleaned up when entry removed from device.""" mqtt_mock = await mqtt_mock_entry() assert await async_setup_component(hass, "config", {}) ws_client = await hass_ws_client(hass) - async_fire_mqtt_message(hass, discovery_topic, discovery_payload) + data = ( + '{ "device":{"identifiers":["0AFFD2"]},' + ' "state_topic": "foobar/sensor",' + ' "unique_id": "unique" }' + ) + + async_fire_mqtt_message(hass, "homeassistant/sensor/bla/config", data) await hass.async_block_till_done() # Verify device and registry entries are created device_entry = device_registry.async_get_device(identifiers={("mqtt", "0AFFD2")}) assert device_entry is not None + entity_entry = entity_registry.async_get("sensor.none_mqtt_sensor") + assert entity_entry is not None - for entity_id in entity_ids: - entity_entry = entity_registry.async_get(entity_id) - assert entity_entry is not None - - state = hass.states.get(entity_id) - assert state is not None + state = hass.states.get("sensor.none_mqtt_sensor") + assert state is not None # Remove MQTT from the device mqtt_config_entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0] @@ -1289,221 +868,60 @@ async def test_cleanup_device( assert entity_entry is None # Verify state is removed - for entity_id in entity_ids: - state = hass.states.get(entity_id) - assert state is None - await hass.async_block_till_done() + state = hass.states.get("sensor.none_mqtt_sensor") + assert state is None + await hass.async_block_till_done() # Verify retained discovery topic has been cleared - mqtt_mock.async_publish.assert_called_with(discovery_topic, None, 0, True) + mqtt_mock.async_publish.assert_called_once_with( + "homeassistant/sensor/bla/config", None, 0, True + ) -@pytest.mark.parametrize( - ("discovery_topic", "discovery_payload", "entity_ids"), - [ - ( - "homeassistant/sensor/bla/config", - '{ "device":{"identifiers":["0AFFD2"]},' - ' "state_topic": "foobar/sensor",' - ' "unique_id": "unique" }', - ["sensor.none_mqtt_sensor"], - ), - ( - "homeassistant/device/bla/config", - '{ "device":{"identifiers":["0AFFD2"]},' - ' "o": {"name": "foobar"},' - ' "cmp": {"sens1": {' - ' "platform": "sensor",' - ' "name": "sensor1",' - ' "state_topic": "foobar/sensor1",' - ' "unique_id": "unique1"' - ' },"sens2": {' - ' "platform": "sensor",' - ' "name": "sensor2",' - ' "state_topic": "foobar/sensor2",' - ' "unique_id": "unique2"' - "}}}", - ["sensor.none_sensor1", "sensor.none_sensor2"], - ), - ], -) async def test_cleanup_device_mqtt( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, mqtt_mock_entry: MqttMockHAClientGenerator, - discovery_topic: str, - discovery_payload: str, - entity_ids: list[str], ) -> None: - """Test discovered device is cleaned up when removed through MQTT.""" + """Test discvered device is cleaned up when removed through MQTT.""" mqtt_mock = await mqtt_mock_entry() - - # set up an existing sensor first data = ( - '{ "device":{"identifiers":["0AFFD3"]},' - ' "name": "sensor_base",' + '{ "device":{"identifiers":["0AFFD2"]},' ' "state_topic": "foobar/sensor",' - ' "unique_id": "unique_base" }' + ' "unique_id": "unique" }' ) - base_discovery_topic = "homeassistant/sensor/bla_base/config" - base_entity_id = "sensor.none_sensor_base" - async_fire_mqtt_message(hass, base_discovery_topic, data) - await hass.async_block_till_done() - # Verify the base entity has been created and it has a state - base_device_entry = device_registry.async_get_device( - identifiers={("mqtt", "0AFFD3")} - ) - assert base_device_entry is not None - entity_entry = entity_registry.async_get(base_entity_id) - assert entity_entry is not None - state = hass.states.get(base_entity_id) - assert state is not None - - async_fire_mqtt_message(hass, discovery_topic, discovery_payload) + async_fire_mqtt_message(hass, "homeassistant/sensor/bla/config", data) await hass.async_block_till_done() # Verify device and registry entries are created device_entry = device_registry.async_get_device(identifiers={("mqtt", "0AFFD2")}) assert device_entry is not None - for entity_id in entity_ids: - entity_entry = entity_registry.async_get(entity_id) - assert entity_entry is not None + entity_entry = entity_registry.async_get("sensor.none_mqtt_sensor") + assert entity_entry is not None - state = hass.states.get(entity_id) - assert state is not None + state = hass.states.get("sensor.none_mqtt_sensor") + assert state is not None - async_fire_mqtt_message(hass, discovery_topic, "") + async_fire_mqtt_message(hass, "homeassistant/sensor/bla/config", "") await hass.async_block_till_done() await hass.async_block_till_done() # Verify device and registry entries are cleared device_entry = device_registry.async_get_device(identifiers={("mqtt", "0AFFD2")}) assert device_entry is None + entity_entry = entity_registry.async_get("sensor.none_mqtt_sensor") + assert entity_entry is None - for entity_id in entity_ids: - entity_entry = entity_registry.async_get(entity_id) - assert entity_entry is None - - # Verify state is removed - state = hass.states.get(entity_id) - assert state is None - await hass.async_block_till_done() + # Verify state is removed + state = hass.states.get("sensor.none_mqtt_sensor") + assert state is None + await hass.async_block_till_done() # Verify retained discovery topics have not been cleared again mqtt_mock.async_publish.assert_not_called() - # Verify the base entity still exists and it has a state - base_device_entry = device_registry.async_get_device( - identifiers={("mqtt", "0AFFD3")} - ) - assert base_device_entry is not None - entity_entry = entity_registry.async_get(base_entity_id) - assert entity_entry is not None - state = hass.states.get(base_entity_id) - assert state is not None - - -async def test_cleanup_device_mqtt_device_discovery( - hass: HomeAssistant, - device_registry: dr.DeviceRegistry, - entity_registry: er.EntityRegistry, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, -) -> None: - """Test discovered device is cleaned up partly when removed through MQTT.""" - await mqtt_mock_entry() - - discovery_topic = "homeassistant/device/bla/config" - discovery_payload = ( - '{ "device":{"identifiers":["0AFFD2"]},' - ' "o": {"name": "foobar"},' - ' "cmp": {"sens1": {' - ' "platform": "sensor",' - ' "name": "sensor1",' - ' "state_topic": "foobar/sensor1",' - ' "unique_id": "unique1"' - ' },"sens2": {' - ' "platform": "sensor",' - ' "name": "sensor2",' - ' "state_topic": "foobar/sensor2",' - ' "unique_id": "unique2"' - "}}}" - ) - entity_ids = ["sensor.none_sensor1", "sensor.none_sensor2"] - async_fire_mqtt_message(hass, discovery_topic, discovery_payload) - await hass.async_block_till_done() - - # Verify device and registry entries are created - device_entry = device_registry.async_get_device(identifiers={("mqtt", "0AFFD2")}) - assert device_entry is not None - for entity_id in entity_ids: - entity_entry = entity_registry.async_get(entity_id) - assert entity_entry is not None - - state = hass.states.get(entity_id) - assert state is not None - - # Do update and remove sensor 2 from device - discovery_payload_update1 = ( - '{ "device":{"identifiers":["0AFFD2"]},' - ' "o": {"name": "foobar"},' - ' "cmp": {"sens1": {' - ' "platform": "sensor",' - ' "name": "sensor1",' - ' "state_topic": "foobar/sensor1",' - ' "unique_id": "unique1"' - ' },"sens2": {' - ' "platform": "sensor"' - "}}}" - ) - async_fire_mqtt_message(hass, discovery_topic, discovery_payload_update1) - await hass.async_block_till_done() - state = hass.states.get(entity_ids[0]) - assert state is not None - state = hass.states.get(entity_ids[1]) - assert state is None - - # Repeating the update - async_fire_mqtt_message(hass, discovery_topic, discovery_payload_update1) - await hass.async_block_till_done() - state = hass.states.get(entity_ids[0]) - assert state is not None - state = hass.states.get(entity_ids[1]) - assert state is None - - # Removing last sensor - discovery_payload_update2 = ( - '{ "device":{"identifiers":["0AFFD2"]},' - ' "o": {"name": "foobar"},' - ' "cmp": {"sens1": {' - ' "platform": "sensor"' - ' },"sens2": {' - ' "platform": "sensor"' - "}}}" - ) - async_fire_mqtt_message(hass, discovery_topic, discovery_payload_update2) - await hass.async_block_till_done() - device_entry = device_registry.async_get_device(identifiers={("mqtt", "0AFFD2")}) - # Verify the device entry was removed with the last sensor - assert device_entry is None - for entity_id in entity_ids: - entity_entry = entity_registry.async_get(entity_id) - assert entity_entry is None - - state = hass.states.get(entity_id) - assert state is None - - # Repeating the update - async_fire_mqtt_message(hass, discovery_topic, discovery_payload_update2) - await hass.async_block_till_done() - - # Clear the empty discovery payload and verify there was nothing to cleanup - async_fire_mqtt_message(hass, discovery_topic, "") - await hass.async_block_till_done() - assert "No device components to cleanup" in caplog.text - async def test_cleanup_device_multiple_config_entries( hass: HomeAssistant, @@ -2388,77 +1806,3 @@ async def test_discovery_dispatcher_signal_type_messages( assert len(calls) == 1 assert calls[0] == test_data unsub() - - -@pytest.mark.parametrize( - ("discovery_topic", "discovery_payload", "entity_ids"), - [ - ( - "homeassistant/device/bla/config", - '{ "device":{"identifiers":["0AFFD2"]},' - ' "o": {"name": "foobar"},' - ' "state_topic": "foobar/sensor-shared",' - ' "cmp": {"sens1": {' - ' "platform": "sensor",' - ' "name": "sensor1",' - ' "unique_id": "unique1"' - ' },"sens2": {' - ' "platform": "sensor",' - ' "name": "sensor2",' - ' "unique_id": "unique2"' - ' },"sens3": {' - ' "platform": "sensor",' - ' "name": "sensor3",' - ' "state_topic": "foobar/sensor3",' - ' "unique_id": "unique3"' - "}}}", - ["sensor.none_sensor1", "sensor.none_sensor2", "sensor.none_sensor3"], - ), - ], -) -async def test_shared_state_topic( - hass: HomeAssistant, - device_registry: dr.DeviceRegistry, - entity_registry: er.EntityRegistry, - mqtt_mock_entry: MqttMockHAClientGenerator, - discovery_topic: str, - discovery_payload: str, - entity_ids: list[str], -) -> None: - """Test a shared state_topic can be used.""" - await mqtt_mock_entry() - - async_fire_mqtt_message(hass, discovery_topic, discovery_payload) - await hass.async_block_till_done() - - # Verify device and registry entries are created - device_entry = device_registry.async_get_device(identifiers={("mqtt", "0AFFD2")}) - assert device_entry is not None - for entity_id in entity_ids: - entity_entry = entity_registry.async_get(entity_id) - assert entity_entry is not None - - state = hass.states.get(entity_id) - assert state is not None - assert state.state == STATE_UNKNOWN - - async_fire_mqtt_message(hass, "foobar/sensor-shared", "New state") - - entity_id = entity_ids[0] - state = hass.states.get(entity_id) - assert state is not None - assert state.state == "New state" - entity_id = entity_ids[1] - state = hass.states.get(entity_id) - assert state is not None - assert state.state == "New state" - entity_id = entity_ids[2] - state = hass.states.get(entity_id) - assert state is not None - assert state.state == STATE_UNKNOWN - - async_fire_mqtt_message(hass, "foobar/sensor3", "New state3") - entity_id = entity_ids[2] - state = hass.states.get(entity_id) - assert state is not None - assert state.state == "New state3" diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index 8c3bd99c562..50b22e986b0 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -3162,6 +3162,7 @@ async def test_mqtt_ws_get_device_debug_info( } data_sensor = json.dumps(config_sensor) data_trigger = json.dumps(config_trigger) + config_sensor["platform"] = config_trigger["platform"] = mqtt.DOMAIN async_fire_mqtt_message(hass, "homeassistant/sensor/bla/config", data_sensor) async_fire_mqtt_message( @@ -3218,6 +3219,7 @@ async def test_mqtt_ws_get_device_debug_info_binary( "unique_id": "unique", } data = json.dumps(config) + config["platform"] = mqtt.DOMAIN async_fire_mqtt_message(hass, "homeassistant/camera/bla/config", data) await hass.async_block_till_done() diff --git a/tests/components/mqtt/test_tag.py b/tests/components/mqtt/test_tag.py index 60c02b9ad4b..1575684e164 100644 --- a/tests/components/mqtt/test_tag.py +++ b/tests/components/mqtt/test_tag.py @@ -1,8 +1,9 @@ """The tests for MQTT tag scanner.""" +from collections.abc import Generator import copy import json -from unittest.mock import ANY, AsyncMock +from unittest.mock import ANY, AsyncMock, patch import pytest @@ -45,6 +46,13 @@ DEFAULT_TAG_SCAN_JSON = ( ) +@pytest.fixture +def tag_mock() -> Generator[AsyncMock, None, None]: + """Fixture to mock tag.""" + with patch("homeassistant.components.tag.async_scan_tag") as mock_tag: + yield mock_tag + + @pytest.mark.no_fail_on_log_exception async def test_discover_bad_tag( hass: HomeAssistant, From 2c206c18d414703aaaec358f6548a0d4f1be5b48 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Mon, 3 Jun 2024 23:38:31 +0200 Subject: [PATCH 0180/1445] Do not log mqtt origin info if the log level does not allow it (#118752) --- homeassistant/components/mqtt/discovery.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py index 2cdd900690c..e8a3ed9a8cb 100644 --- a/homeassistant/components/mqtt/discovery.py +++ b/homeassistant/components/mqtt/discovery.py @@ -82,6 +82,9 @@ def async_log_discovery_origin_info( message: str, discovery_payload: MQTTDiscoveryPayload, level: int = logging.INFO ) -> None: """Log information about the discovery and origin.""" + if not _LOGGER.isEnabledFor(level): + # bail early if logging is disabled + return if CONF_ORIGIN not in discovery_payload: _LOGGER.log(level, message) return From 35a1ecea272ae65b9e16f845f077389694d92806 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 3 Jun 2024 18:08:46 -0500 Subject: [PATCH 0181/1445] Speed up statistics_during_period websocket api (#118672) --- .../components/recorder/websocket_api.py | 15 ++-- .../components/recorder/test_websocket_api.py | 78 +++++++++++++++++++ 2 files changed, 85 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/recorder/websocket_api.py b/homeassistant/components/recorder/websocket_api.py index 58c362df62e..b091343e5a4 100644 --- a/homeassistant/components/recorder/websocket_api.py +++ b/homeassistant/components/recorder/websocket_api.py @@ -160,14 +160,13 @@ def _ws_get_statistics_during_period( units, types, ) - for statistic_id in result: - for item in result[statistic_id]: - if (start := item.get("start")) is not None: - item["start"] = int(start * 1000) - if (end := item.get("end")) is not None: - item["end"] = int(end * 1000) - if (last_reset := item.get("last_reset")) is not None: - item["last_reset"] = int(last_reset * 1000) + include_last_reset = "last_reset" in types + for statistic_rows in result.values(): + for row in statistic_rows: + row["start"] = int(row["start"] * 1000) + row["end"] = int(row["end"] * 1000) + if include_last_reset and (last_reset := row["last_reset"]) is not None: + row["last_reset"] = int(last_reset * 1000) return json_bytes(messages.result_message(msg_id, result)) diff --git a/tests/components/recorder/test_websocket_api.py b/tests/components/recorder/test_websocket_api.py index 9c8e0a9203a..3d35aafb2b3 100644 --- a/tests/components/recorder/test_websocket_api.py +++ b/tests/components/recorder/test_websocket_api.py @@ -3177,3 +3177,81 @@ async def test_adjust_sum_statistics_errors( stats = statistics_during_period(hass, zero, period="hour") assert stats != previous_stats previous_stats = stats + + +async def test_import_statistics_with_last_reset( + recorder_mock: Recorder, + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test importing external statistics with last_reset can be fetched via websocket api.""" + client = await hass_ws_client() + + assert "Compiling statistics for" not in caplog.text + assert "Statistics already compiled" not in caplog.text + + zero = dt_util.utcnow() + last_reset = dt_util.parse_datetime("2022-01-01T00:00:00+02:00") + period1 = zero.replace(minute=0, second=0, microsecond=0) + timedelta(hours=1) + period2 = zero.replace(minute=0, second=0, microsecond=0) + timedelta(hours=2) + + external_statistics1 = { + "start": period1, + "last_reset": last_reset, + "state": 0, + "sum": 2, + } + external_statistics2 = { + "start": period2, + "last_reset": last_reset, + "state": 1, + "sum": 3, + } + + external_metadata = { + "has_mean": False, + "has_sum": True, + "name": "Total imported energy", + "source": "test", + "statistic_id": "test:total_energy_import", + "unit_of_measurement": "kWh", + } + + async_add_external_statistics( + hass, external_metadata, (external_statistics1, external_statistics2) + ) + await async_wait_recording_done(hass) + + client = await hass_ws_client() + await client.send_json_auto_id( + { + "type": "recorder/statistics_during_period", + "start_time": zero.isoformat(), + "end_time": (zero + timedelta(hours=48)).isoformat(), + "statistic_ids": ["test:total_energy_import"], + "period": "hour", + "types": ["change", "last_reset", "max", "mean", "min", "state", "sum"], + } + ) + response = await client.receive_json() + assert response["result"] == { + "test:total_energy_import": [ + { + "change": 2.0, + "end": (period1.timestamp() * 1000) + (3600 * 1000), + "last_reset": last_reset.timestamp() * 1000, + "start": period1.timestamp() * 1000, + "state": 0.0, + "sum": 2.0, + }, + { + "change": 1.0, + "end": (period2.timestamp() * 1000 + (3600 * 1000)), + "last_reset": last_reset.timestamp() * 1000, + "start": period2.timestamp() * 1000, + "state": 1.0, + "sum": 3.0, + }, + ] + } From 0257aa4839fa22c8c95bc521fe96f83dbb207dd9 Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Mon, 3 Jun 2024 21:26:40 -0500 Subject: [PATCH 0182/1445] Clean up exposed domains (#118753) * Remove lock and script * Add media player * Fix tests --- .../homeassistant/exposed_entities.py | 3 +-- .../conversation/test_default_agent.py | 20 ++++++++++++------- .../homeassistant/test_exposed_entities.py | 16 ++++++++++++--- 3 files changed, 27 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/homeassistant/exposed_entities.py b/homeassistant/components/homeassistant/exposed_entities.py index d40105324c4..82848b0e273 100644 --- a/homeassistant/components/homeassistant/exposed_entities.py +++ b/homeassistant/components/homeassistant/exposed_entities.py @@ -35,9 +35,8 @@ DEFAULT_EXPOSED_DOMAINS = { "fan", "humidifier", "light", - "lock", + "media_player", "scene", - "script", "switch", "todo", "vacuum", diff --git a/tests/components/conversation/test_default_agent.py b/tests/components/conversation/test_default_agent.py index 659ee8794b8..511967e3a9c 100644 --- a/tests/components/conversation/test_default_agent.py +++ b/tests/components/conversation/test_default_agent.py @@ -72,15 +72,23 @@ async def test_hidden_entities_skipped( async def test_exposed_domains(hass: HomeAssistant, init_components) -> None: """Test that we can't interact with entities that aren't exposed.""" hass.states.async_set( - "media_player.test", "off", attributes={ATTR_FRIENDLY_NAME: "Test Media Player"} + "lock.front_door", "off", attributes={ATTR_FRIENDLY_NAME: "Front Door"} ) + hass.states.async_set( + "script.my_script", "off", attributes={ATTR_FRIENDLY_NAME: "My Script"} + ) + + # These are match failures instead of handle failures because the domains + # aren't exposed by default. + result = await conversation.async_converse( + hass, "unlock front door", None, Context(), None + ) + assert result.response.response_type == intent.IntentResponseType.ERROR + assert result.response.error_code == intent.IntentResponseErrorCode.NO_VALID_TARGETS result = await conversation.async_converse( - hass, "turn on test media player", None, Context(), None + hass, "run my script", None, Context(), None ) - - # This is a match failure instead of a handle failure because the media - # player domain is not exposed. assert result.response.response_type == intent.IntentResponseType.ERROR assert result.response.error_code == intent.IntentResponseErrorCode.NO_VALID_TARGETS @@ -806,7 +814,6 @@ async def test_error_wrong_state(hass: HomeAssistant, init_components) -> None: media_player.STATE_IDLE, {ATTR_FRIENDLY_NAME: "test player"}, ) - expose_entity(hass, "media_player.test_player", True) result = await conversation.async_converse( hass, "pause test player", None, Context(), None @@ -829,7 +836,6 @@ async def test_error_feature_not_supported( {ATTR_FRIENDLY_NAME: "test player"}, # missing VOLUME_SET feature ) - expose_entity(hass, "media_player.test_player", True) result = await conversation.async_converse( hass, "set test player volume to 100%", None, Context(), None diff --git a/tests/components/homeassistant/test_exposed_entities.py b/tests/components/homeassistant/test_exposed_entities.py index 9a14198b1ef..b3ff6594509 100644 --- a/tests/components/homeassistant/test_exposed_entities.py +++ b/tests/components/homeassistant/test_exposed_entities.py @@ -57,9 +57,12 @@ def entities_unique_id(entity_registry: er.EntityRegistry) -> dict[str, str]: entry_sensor_temperature = entity_registry.async_get_or_create( "sensor", "test", - "unique2", + "unique3", original_device_class="temperature", ) + entry_media_player = entity_registry.async_get_or_create( + "media_player", "test", "unique4", original_device_class="media_player" + ) return { "blocked": entry_blocked.entity_id, "lock": entry_lock.entity_id, @@ -67,6 +70,7 @@ def entities_unique_id(entity_registry: er.EntityRegistry) -> dict[str, str]: "door_sensor": entry_binary_sensor_door.entity_id, "sensor": entry_sensor.entity_id, "temperature_sensor": entry_sensor_temperature.entity_id, + "media_player": entry_media_player.entity_id, } @@ -78,10 +82,12 @@ def entities_no_unique_id(hass: HomeAssistant) -> dict[str, str]: door_sensor = "binary_sensor.door" sensor = "sensor.test" sensor_temperature = "sensor.temperature" + media_player = "media_player.test" hass.states.async_set(binary_sensor, "on", {}) hass.states.async_set(door_sensor, "on", {"device_class": "door"}) hass.states.async_set(sensor, "on", {}) hass.states.async_set(sensor_temperature, "on", {"device_class": "temperature"}) + hass.states.async_set(media_player, "idle", {}) return { "blocked": blocked, "lock": lock, @@ -89,6 +95,7 @@ def entities_no_unique_id(hass: HomeAssistant) -> dict[str, str]: "door_sensor": door_sensor, "sensor": sensor, "temperature_sensor": sensor_temperature, + "media_player": media_player, } @@ -409,8 +416,8 @@ async def test_should_expose( # Blocked entity is not exposed assert async_should_expose(hass, "cloud.alexa", entities["blocked"]) is False - # Lock is exposed - assert async_should_expose(hass, "cloud.alexa", entities["lock"]) is True + # Lock is not exposed + assert async_should_expose(hass, "cloud.alexa", entities["lock"]) is False # Binary sensor without device class is not exposed assert async_should_expose(hass, "cloud.alexa", entities["binary_sensor"]) is False @@ -426,6 +433,9 @@ async def test_should_expose( async_should_expose(hass, "cloud.alexa", entities["temperature_sensor"]) is True ) + # Media player is exposed + assert async_should_expose(hass, "cloud.alexa", entities["media_player"]) is True + # The second time we check, it should load it from storage assert ( async_should_expose(hass, "cloud.alexa", entities["temperature_sensor"]) is True From 289263087c71e621707c1b149c38b8d6b4394930 Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Mon, 3 Jun 2024 21:47:09 -0500 Subject: [PATCH 0183/1445] Bump intents to 2024.6.3 (#118748) --- homeassistant/components/conversation/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/conversation/manifest.json b/homeassistant/components/conversation/manifest.json index d69a65b9c6e..6873e47e647 100644 --- a/homeassistant/components/conversation/manifest.json +++ b/homeassistant/components/conversation/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/conversation", "integration_type": "system", "quality_scale": "internal", - "requirements": ["hassil==1.7.1", "home-assistant-intents==2024.5.28"] + "requirements": ["hassil==1.7.1", "home-assistant-intents==2024.6.3"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index b2be5a92267..e2b47a43132 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -33,7 +33,7 @@ hass-nabucasa==0.81.0 hassil==1.7.1 home-assistant-bluetooth==1.12.0 home-assistant-frontend==20240603.0 -home-assistant-intents==2024.5.28 +home-assistant-intents==2024.6.3 httpx==0.27.0 ifaddr==0.2.0 Jinja2==3.1.4 diff --git a/requirements_all.txt b/requirements_all.txt index c6b7681cc81..fcb54a6c3be 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1090,7 +1090,7 @@ holidays==0.49 home-assistant-frontend==20240603.0 # homeassistant.components.conversation -home-assistant-intents==2024.5.28 +home-assistant-intents==2024.6.3 # homeassistant.components.home_connect homeconnect==0.7.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3b96777fe38..19f25c3ef8b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -892,7 +892,7 @@ holidays==0.49 home-assistant-frontend==20240603.0 # homeassistant.components.conversation -home-assistant-intents==2024.5.28 +home-assistant-intents==2024.6.3 # homeassistant.components.home_connect homeconnect==0.7.2 From e7992705786591553d55358a4e9bfbaa3ed47bd4 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Tue, 4 Jun 2024 06:20:18 +0200 Subject: [PATCH 0184/1445] Recover mqtt abbrevations optimizations (#118762) Co-authored-by: J. Nick Koston --- homeassistant/components/mqtt/discovery.py | 143 ++++++++++++--------- tests/components/mqtt/test_discovery.py | 4 +- 2 files changed, 86 insertions(+), 61 deletions(-) diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py index e8a3ed9a8cb..0d93af26a57 100644 --- a/homeassistant/components/mqtt/discovery.py +++ b/homeassistant/components/mqtt/discovery.py @@ -41,6 +41,10 @@ from .models import DATA_MQTT, MqttOriginInfo, ReceiveMessage from .schemas import MQTT_ORIGIN_INFO_SCHEMA from .util import async_forward_entry_setup_and_setup_discovery +ABBREVIATIONS_SET = set(ABBREVIATIONS) +DEVICE_ABBREVIATIONS_SET = set(DEVICE_ABBREVIATIONS) +ORIGIN_ABBREVIATIONS_SET = set(ORIGIN_ABBREVIATIONS) + _LOGGER = logging.getLogger(__name__) TOPIC_MATCHER = re.compile( @@ -105,6 +109,82 @@ def async_log_discovery_origin_info( ) +@callback +def _replace_abbreviations( + payload: Any | dict[str, Any], + abbreviations: dict[str, str], + abbreviations_set: set[str], +) -> None: + """Replace abbreviations in an MQTT discovery payload.""" + if not isinstance(payload, dict): + return + for key in abbreviations_set.intersection(payload): + payload[abbreviations[key]] = payload.pop(key) + + +@callback +def _replace_all_abbreviations(discovery_payload: Any | dict[str, Any]) -> None: + """Replace all abbreviations in an MQTT discovery payload.""" + + _replace_abbreviations(discovery_payload, ABBREVIATIONS, ABBREVIATIONS_SET) + + if CONF_ORIGIN in discovery_payload: + _replace_abbreviations( + discovery_payload[CONF_ORIGIN], + ORIGIN_ABBREVIATIONS, + ORIGIN_ABBREVIATIONS_SET, + ) + + if CONF_DEVICE in discovery_payload: + _replace_abbreviations( + discovery_payload[CONF_DEVICE], + DEVICE_ABBREVIATIONS, + DEVICE_ABBREVIATIONS_SET, + ) + + if CONF_AVAILABILITY in discovery_payload: + for availability_conf in cv.ensure_list(discovery_payload[CONF_AVAILABILITY]): + _replace_abbreviations(availability_conf, ABBREVIATIONS, ABBREVIATIONS_SET) + + +@callback +def _replace_topic_base(discovery_payload: dict[str, Any]) -> None: + """Replace topic base in MQTT discovery data.""" + base = discovery_payload.pop(TOPIC_BASE) + for key, value in discovery_payload.items(): + if isinstance(value, str) and value: + if value[0] == TOPIC_BASE and key.endswith("topic"): + discovery_payload[key] = f"{base}{value[1:]}" + if value[-1] == TOPIC_BASE and key.endswith("topic"): + discovery_payload[key] = f"{value[:-1]}{base}" + if discovery_payload.get(CONF_AVAILABILITY): + for availability_conf in cv.ensure_list(discovery_payload[CONF_AVAILABILITY]): + if not isinstance(availability_conf, dict): + continue + if topic := str(availability_conf.get(CONF_TOPIC)): + if topic[0] == TOPIC_BASE: + availability_conf[CONF_TOPIC] = f"{base}{topic[1:]}" + if topic[-1] == TOPIC_BASE: + availability_conf[CONF_TOPIC] = f"{topic[:-1]}{base}" + + +@callback +def _valid_origin_info(discovery_payload: MQTTDiscoveryPayload) -> bool: + """Parse and validate origin info from a single component discovery payload.""" + if CONF_ORIGIN not in discovery_payload: + return True + try: + MQTT_ORIGIN_INFO_SCHEMA(discovery_payload[CONF_ORIGIN]) + except Exception as exc: # noqa:BLE001 + _LOGGER.warning( + "Unable to parse origin information from discovery message: %s, got %s", + exc, + discovery_payload[CONF_ORIGIN], + ) + return False + return True + + async def async_start( # noqa: C901 hass: HomeAssistant, discovery_topic: str, config_entry: ConfigEntry ) -> None: @@ -168,67 +248,14 @@ async def async_start( # noqa: C901 except ValueError: _LOGGER.warning("Unable to parse JSON %s: '%s'", object_id, payload) return + _replace_all_abbreviations(discovery_payload) + if not _valid_origin_info(discovery_payload): + return + if TOPIC_BASE in discovery_payload: + _replace_topic_base(discovery_payload) else: discovery_payload = MQTTDiscoveryPayload({}) - for key in list(discovery_payload): - abbreviated_key = key - key = ABBREVIATIONS.get(key, key) - discovery_payload[key] = discovery_payload.pop(abbreviated_key) - - if CONF_DEVICE in discovery_payload: - device = discovery_payload[CONF_DEVICE] - for key in list(device): - abbreviated_key = key - key = DEVICE_ABBREVIATIONS.get(key, key) - device[key] = device.pop(abbreviated_key) - - if CONF_ORIGIN in discovery_payload: - origin_info: dict[str, Any] = discovery_payload[CONF_ORIGIN] - try: - for key in list(origin_info): - abbreviated_key = key - key = ORIGIN_ABBREVIATIONS.get(key, key) - origin_info[key] = origin_info.pop(abbreviated_key) - MQTT_ORIGIN_INFO_SCHEMA(discovery_payload[CONF_ORIGIN]) - except Exception: # noqa: BLE001 - _LOGGER.warning( - "Unable to parse origin information " - "from discovery message, got %s", - discovery_payload[CONF_ORIGIN], - ) - return - - if CONF_AVAILABILITY in discovery_payload: - for availability_conf in cv.ensure_list( - discovery_payload[CONF_AVAILABILITY] - ): - if isinstance(availability_conf, dict): - for key in list(availability_conf): - abbreviated_key = key - key = ABBREVIATIONS.get(key, key) - availability_conf[key] = availability_conf.pop(abbreviated_key) - - if TOPIC_BASE in discovery_payload: - base = discovery_payload.pop(TOPIC_BASE) - for key, value in discovery_payload.items(): - if isinstance(value, str) and value: - if value[0] == TOPIC_BASE and key.endswith("topic"): - discovery_payload[key] = f"{base}{value[1:]}" - if value[-1] == TOPIC_BASE and key.endswith("topic"): - discovery_payload[key] = f"{value[:-1]}{base}" - if discovery_payload.get(CONF_AVAILABILITY): - for availability_conf in cv.ensure_list( - discovery_payload[CONF_AVAILABILITY] - ): - if not isinstance(availability_conf, dict): - continue - if topic := str(availability_conf.get(CONF_TOPIC)): - if topic[0] == TOPIC_BASE: - availability_conf[CONF_TOPIC] = f"{base}{topic[1:]}" - if topic[-1] == TOPIC_BASE: - availability_conf[CONF_TOPIC] = f"{topic[:-1]}{base}" - # If present, the node_id will be included in the discovered object id discovery_id = f"{node_id} {object_id}" if node_id else object_id discovery_hash = (component, discovery_id) diff --git a/tests/components/mqtt/test_discovery.py b/tests/components/mqtt/test_discovery.py index 2e1f78c1bd4..020ab4a09a9 100644 --- a/tests/components/mqtt/test_discovery.py +++ b/tests/components/mqtt/test_discovery.py @@ -291,9 +291,7 @@ async def test_discovery_with_invalid_integration_info( state = hass.states.get("binary_sensor.beer") assert state is None - assert ( - "Unable to parse origin information from discovery message, got" in caplog.text - ) + assert "Unable to parse origin information from discovery message" in caplog.text async def test_discover_fan( From 53ab215dfcd325a32b333e242983e18fc2e9958c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Tue, 4 Jun 2024 06:27:54 +0200 Subject: [PATCH 0185/1445] Update hass-nabucasa to version 0.81.1 (#118768) --- homeassistant/components/cloud/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- pyproject.toml | 2 +- requirements.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/cloud/manifest.json b/homeassistant/components/cloud/manifest.json index f30b6b14f67..529f4fb9be9 100644 --- a/homeassistant/components/cloud/manifest.json +++ b/homeassistant/components/cloud/manifest.json @@ -8,5 +8,5 @@ "integration_type": "system", "iot_class": "cloud_push", "loggers": ["hass_nabucasa"], - "requirements": ["hass-nabucasa==0.81.0"] + "requirements": ["hass-nabucasa==0.81.1"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index e2b47a43132..6160db06385 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -29,7 +29,7 @@ fnv-hash-fast==0.5.0 ha-av==10.1.1 ha-ffmpeg==3.2.0 habluetooth==3.1.1 -hass-nabucasa==0.81.0 +hass-nabucasa==0.81.1 hassil==1.7.1 home-assistant-bluetooth==1.12.0 home-assistant-frontend==20240603.0 diff --git a/pyproject.toml b/pyproject.toml index c23a7ea3067..42bb1bd69af 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,7 +40,7 @@ dependencies = [ "fnv-hash-fast==0.5.0", # hass-nabucasa is imported by helpers which don't depend on the cloud # integration - "hass-nabucasa==0.81.0", + "hass-nabucasa==0.81.1", # When bumping httpx, please check the version pins of # httpcore, anyio, and h11 in gen_requirements_all "httpx==0.27.0", diff --git a/requirements.txt b/requirements.txt index abf91d7f2ec..d3390585c66 100644 --- a/requirements.txt +++ b/requirements.txt @@ -18,7 +18,7 @@ bcrypt==4.1.2 certifi>=2021.5.30 ciso8601==2.3.1 fnv-hash-fast==0.5.0 -hass-nabucasa==0.81.0 +hass-nabucasa==0.81.1 httpx==0.27.0 home-assistant-bluetooth==1.12.0 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index fcb54a6c3be..0a49a7c58b7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1047,7 +1047,7 @@ habitipy==0.3.1 habluetooth==3.1.1 # homeassistant.components.cloud -hass-nabucasa==0.81.0 +hass-nabucasa==0.81.1 # homeassistant.components.splunk hass-splunk==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 19f25c3ef8b..2c79fa6c046 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -861,7 +861,7 @@ habitipy==0.3.1 habluetooth==3.1.1 # homeassistant.components.cloud -hass-nabucasa==0.81.0 +hass-nabucasa==0.81.1 # homeassistant.components.conversation hassil==1.7.1 From 553311cc7d6e5c312e6123aaff0643757bce65b4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 3 Jun 2024 23:53:37 -0500 Subject: [PATCH 0186/1445] Add os.walk to asyncio loop blocking detection (#118769) --- homeassistant/block_async_io.py | 3 +++ tests/test_block_async_io.py | 16 +++++++++++++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/homeassistant/block_async_io.py b/homeassistant/block_async_io.py index e829ed4925b..2dc94fa456a 100644 --- a/homeassistant/block_async_io.py +++ b/homeassistant/block_async_io.py @@ -67,6 +67,9 @@ def enable() -> None: glob.iglob = protect_loop( glob.iglob, strict_core=False, strict=False, loop_thread_id=loop_thread_id ) + os.walk = protect_loop( + os.walk, strict_core=False, strict=False, loop_thread_id=loop_thread_id + ) if not _IN_TESTS: # Prevent files being opened inside the event loop diff --git a/tests/test_block_async_io.py b/tests/test_block_async_io.py index e4f248e80d1..1ceb84c249f 100644 --- a/tests/test_block_async_io.py +++ b/tests/test_block_async_io.py @@ -275,7 +275,7 @@ async def test_protect_loop_scandir( caplog.clear() with contextlib.suppress(FileNotFoundError): await hass.async_add_executor_job(os.scandir, "/path/that/does/not/exists") - assert "Detected blocking call to listdir with args" not in caplog.text + assert "Detected blocking call to scandir with args" not in caplog.text async def test_protect_loop_listdir( @@ -290,3 +290,17 @@ async def test_protect_loop_listdir( with contextlib.suppress(FileNotFoundError): await hass.async_add_executor_job(os.listdir, "/path/that/does/not/exists") assert "Detected blocking call to listdir with args" not in caplog.text + + +async def test_protect_loop_walk( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Test glob calls in the loop are logged.""" + block_async_io.enable() + with contextlib.suppress(FileNotFoundError): + os.walk("/path/that/does/not/exists") + assert "Detected blocking call to walk with args" in caplog.text + caplog.clear() + with contextlib.suppress(FileNotFoundError): + await hass.async_add_executor_job(os.walk, "/path/that/does/not/exists") + assert "Detected blocking call to walk with args" not in caplog.text From d43d12905d6aaed574ae7d0457ec497107aaf259 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Mon, 3 Jun 2024 23:20:37 -0600 Subject: [PATCH 0187/1445] Don't require code to arm SimpliSafe (#118759) --- .../simplisafe/alarm_control_panel.py | 35 ++----------------- 1 file changed, 2 insertions(+), 33 deletions(-) diff --git a/homeassistant/components/simplisafe/alarm_control_panel.py b/homeassistant/components/simplisafe/alarm_control_panel.py index 731400e67d5..28ebd246623 100644 --- a/homeassistant/components/simplisafe/alarm_control_panel.py +++ b/homeassistant/components/simplisafe/alarm_control_panel.py @@ -26,11 +26,9 @@ from simplipy.websocket import ( from homeassistant.components.alarm_control_panel import ( AlarmControlPanelEntity, AlarmControlPanelEntityFeature, - CodeFormat, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( - CONF_CODE, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMING, @@ -124,11 +122,12 @@ async def async_setup_entry( class SimpliSafeAlarm(SimpliSafeEntity, AlarmControlPanelEntity): """Representation of a SimpliSafe alarm.""" + _attr_code_arm_required = False + _attr_name = None _attr_supported_features = ( AlarmControlPanelEntityFeature.ARM_HOME | AlarmControlPanelEntityFeature.ARM_AWAY ) - _attr_name = None def __init__(self, simplisafe: SimpliSafe, system: SystemType) -> None: """Initialize the SimpliSafe alarm.""" @@ -138,30 +137,9 @@ class SimpliSafeAlarm(SimpliSafeEntity, AlarmControlPanelEntity): additional_websocket_events=WEBSOCKET_EVENTS_TO_LISTEN_FOR, ) - if code := self._simplisafe.entry.options.get(CONF_CODE): - if code.isdigit(): - self._attr_code_format = CodeFormat.NUMBER - else: - self._attr_code_format = CodeFormat.TEXT - self._last_event = None - self._set_state_from_system_data() - @callback - def _is_code_valid(self, code: str | None, state: str) -> bool: - """Validate that a code matches the required one.""" - if not self._simplisafe.entry.options.get(CONF_CODE): - return True - - if not code or code != self._simplisafe.entry.options[CONF_CODE]: - LOGGER.warning( - "Incorrect alarm code entered (target state: %s): %s", state, code - ) - return False - - return True - @callback def _set_state_from_system_data(self) -> None: """Set the state based on the latest REST API data.""" @@ -176,9 +154,6 @@ class SimpliSafeAlarm(SimpliSafeEntity, AlarmControlPanelEntity): async def async_alarm_disarm(self, code: str | None = None) -> None: """Send disarm command.""" - if not self._is_code_valid(code, STATE_ALARM_DISARMED): - return - try: await self._system.async_set_off() except SimplipyError as err: @@ -191,9 +166,6 @@ class SimpliSafeAlarm(SimpliSafeEntity, AlarmControlPanelEntity): async def async_alarm_arm_home(self, code: str | None = None) -> None: """Send arm home command.""" - if not self._is_code_valid(code, STATE_ALARM_ARMED_HOME): - return - try: await self._system.async_set_home() except SimplipyError as err: @@ -206,9 +178,6 @@ class SimpliSafeAlarm(SimpliSafeEntity, AlarmControlPanelEntity): async def async_alarm_arm_away(self, code: str | None = None) -> None: """Send arm away command.""" - if not self._is_code_valid(code, STATE_ALARM_ARMED_AWAY): - return - try: await self._system.async_set_away() except SimplipyError as err: From cba07540e926b365174440eede944091b4e6bfd4 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Tue, 4 Jun 2024 08:00:40 +0200 Subject: [PATCH 0188/1445] Bump reolink-aio to 0.9.1 (#118655) Co-authored-by: J. Nick Koston --- homeassistant/components/reolink/entity.py | 31 ++++++++++++++++--- homeassistant/components/reolink/host.py | 23 ++++++++++++-- .../components/reolink/manifest.json | 2 +- homeassistant/components/reolink/select.py | 8 +++-- homeassistant/components/reolink/strings.json | 4 +-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 7 files changed, 56 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/reolink/entity.py b/homeassistant/components/reolink/entity.py index 29c1e95be81..53a81f2b162 100644 --- a/homeassistant/components/reolink/entity.py +++ b/homeassistant/components/reolink/entity.py @@ -89,11 +89,17 @@ class ReolinkHostCoordinatorEntity(ReolinkBaseCoordinatorEntity[None]): async def async_added_to_hass(self) -> None: """Entity created.""" await super().async_added_to_hass() - if ( - self.entity_description.cmd_key is not None - and self.entity_description.cmd_key not in self._host.update_cmd_list - ): - self._host.update_cmd_list.append(self.entity_description.cmd_key) + cmd_key = self.entity_description.cmd_key + if cmd_key is not None: + self._host.async_register_update_cmd(cmd_key) + + async def async_will_remove_from_hass(self) -> None: + """Entity removed.""" + cmd_key = self.entity_description.cmd_key + if cmd_key is not None: + self._host.async_unregister_update_cmd(cmd_key) + + await super().async_will_remove_from_hass() class ReolinkChannelCoordinatorEntity(ReolinkHostCoordinatorEntity): @@ -128,3 +134,18 @@ class ReolinkChannelCoordinatorEntity(ReolinkHostCoordinatorEntity): sw_version=self._host.api.camera_sw_version(dev_ch), configuration_url=self._conf_url, ) + + async def async_added_to_hass(self) -> None: + """Entity created.""" + await super().async_added_to_hass() + cmd_key = self.entity_description.cmd_key + if cmd_key is not None: + self._host.async_register_update_cmd(cmd_key, self._channel) + + async def async_will_remove_from_hass(self) -> None: + """Entity removed.""" + cmd_key = self.entity_description.cmd_key + if cmd_key is not None: + self._host.async_unregister_update_cmd(cmd_key, self._channel) + + await super().async_will_remove_from_hass() diff --git a/homeassistant/components/reolink/host.py b/homeassistant/components/reolink/host.py index fe8b1596e74..b1a1a9adf0f 100644 --- a/homeassistant/components/reolink/host.py +++ b/homeassistant/components/reolink/host.py @@ -3,6 +3,7 @@ from __future__ import annotations import asyncio +from collections import defaultdict from collections.abc import Mapping import logging from typing import Any, Literal @@ -21,7 +22,7 @@ from homeassistant.const import ( CONF_PROTOCOL, CONF_USERNAME, ) -from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant +from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback from homeassistant.helpers import issue_registry as ir from homeassistant.helpers.device_registry import format_mac from homeassistant.helpers.dispatcher import async_dispatcher_send @@ -67,7 +68,9 @@ class ReolinkHost: timeout=DEFAULT_TIMEOUT, ) - self.update_cmd_list: list[str] = [] + self._update_cmd: defaultdict[str, defaultdict[int | None, int]] = defaultdict( + lambda: defaultdict(int) + ) self.webhook_id: str | None = None self._onvif_push_supported: bool = True @@ -84,6 +87,20 @@ class ReolinkHost: self._long_poll_task: asyncio.Task | None = None self._lost_subscription: bool = False + @callback + def async_register_update_cmd(self, cmd: str, channel: int | None = None) -> None: + """Register the command to update the state.""" + self._update_cmd[cmd][channel] += 1 + + @callback + def async_unregister_update_cmd(self, cmd: str, channel: int | None = None) -> None: + """Unregister the command to update the state.""" + self._update_cmd[cmd][channel] -= 1 + if not self._update_cmd[cmd][channel]: + del self._update_cmd[cmd][channel] + if not self._update_cmd[cmd]: + del self._update_cmd[cmd] + @property def unique_id(self) -> str: """Create the unique ID, base for all entities.""" @@ -320,7 +337,7 @@ class ReolinkHost: async def update_states(self) -> None: """Call the API of the camera device to update the internal states.""" - await self._api.get_states(cmd_list=self.update_cmd_list) + await self._api.get_states(cmd_list=self._update_cmd) async def disconnect(self) -> None: """Disconnect from the API, so the connection will be released.""" diff --git a/homeassistant/components/reolink/manifest.json b/homeassistant/components/reolink/manifest.json index f9050ee73c4..36bc8731925 100644 --- a/homeassistant/components/reolink/manifest.json +++ b/homeassistant/components/reolink/manifest.json @@ -18,5 +18,5 @@ "documentation": "https://www.home-assistant.io/integrations/reolink", "iot_class": "local_push", "loggers": ["reolink_aio"], - "requirements": ["reolink-aio==0.8.11"] + "requirements": ["reolink-aio==0.9.1"] } diff --git a/homeassistant/components/reolink/select.py b/homeassistant/components/reolink/select.py index 13757e7bb22..907cc90b8af 100644 --- a/homeassistant/components/reolink/select.py +++ b/homeassistant/components/reolink/select.py @@ -109,12 +109,14 @@ SELECT_ENTITIES = ( ReolinkSelectEntityDescription( key="status_led", cmd_key="GetPowerLed", - translation_key="status_led", + translation_key="doorbell_led", entity_category=EntityCategory.CONFIG, - get_options=[state.name for state in StatusLedEnum], + get_options=lambda api, ch: api.doorbell_led_list(ch), supported=lambda api, ch: api.supported(ch, "doorbell_led"), value=lambda api, ch: StatusLedEnum(api.doorbell_led(ch)).name, - method=lambda api, ch, name: api.set_status_led(ch, StatusLedEnum[name].value), + method=lambda api, ch, name: ( + api.set_status_led(ch, StatusLedEnum[name].value, doorbell=True) + ), ), ) diff --git a/homeassistant/components/reolink/strings.json b/homeassistant/components/reolink/strings.json index 8191f51d7ef..d1fa0f4426b 100644 --- a/homeassistant/components/reolink/strings.json +++ b/homeassistant/components/reolink/strings.json @@ -463,8 +463,8 @@ "pantiltfirst": "Pan/tilt first" } }, - "status_led": { - "name": "Status LED", + "doorbell_led": { + "name": "Doorbell LED", "state": { "stayoff": "Stay off", "auto": "Auto", diff --git a/requirements_all.txt b/requirements_all.txt index 0a49a7c58b7..54aee2cdafd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2454,7 +2454,7 @@ renault-api==0.2.3 renson-endura-delta==1.7.1 # homeassistant.components.reolink -reolink-aio==0.8.11 +reolink-aio==0.9.1 # homeassistant.components.idteck_prox rfk101py==0.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2c79fa6c046..1198fee3cac 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1915,7 +1915,7 @@ renault-api==0.2.3 renson-endura-delta==1.7.1 # homeassistant.components.reolink -reolink-aio==0.8.11 +reolink-aio==0.9.1 # homeassistant.components.rflink rflink==0.0.66 From 7fb2802910c91f7db44dabced802b90468da1ad0 Mon Sep 17 00:00:00 2001 From: Richard Kroegel <42204099+rikroe@users.noreply.github.com> Date: Tue, 4 Jun 2024 09:06:23 +0200 Subject: [PATCH 0189/1445] Allow per-sensor unit conversion on BMW sensors (#110272) * Update BMW sensors to use device_class * Test adjustments * Trigger CI * Remove unneeded cast * Set suggested_display_precision to 0 * Rebase for climate_status * Change charging_status to ENUM device class * Add test for Enum translations * Pin Enum sensor values * Use snapshot_platform helper * Remove translation tests * Formatting * Remove comment * Use const.STATE_UNKOWN * Fix typo * Update strings * Loop through Enum sensors * Revert enum sensor changes --------- Co-authored-by: Richard --- .../components/bmw_connected_drive/sensor.py | 97 ++++++------ .../snapshots/test_sensor.ambr | 141 +++++++++++++++--- .../bmw_connected_drive/test_sensor.py | 10 +- 3 files changed, 172 insertions(+), 76 deletions(-) diff --git a/homeassistant/components/bmw_connected_drive/sensor.py b/homeassistant/components/bmw_connected_drive/sensor.py index 0e8ad9726f1..e7f56075e63 100644 --- a/homeassistant/components/bmw_connected_drive/sensor.py +++ b/homeassistant/components/bmw_connected_drive/sensor.py @@ -6,9 +6,8 @@ from collections.abc import Callable from dataclasses import dataclass import datetime import logging -from typing import cast -from bimmer_connected.models import ValueWithUnit +from bimmer_connected.models import StrEnum, ValueWithUnit from bimmer_connected.vehicle import MyBMWVehicle from homeassistant.components.sensor import ( @@ -18,14 +17,19 @@ from homeassistant.components.sensor import ( SensorStateClass, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import LENGTH, PERCENTAGE, VOLUME, UnitOfElectricCurrent +from homeassistant.const import ( + PERCENTAGE, + STATE_UNKNOWN, + UnitOfElectricCurrent, + UnitOfLength, + UnitOfVolume, +) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import StateType from homeassistant.util import dt as dt_util from . import BMWBaseEntity -from .const import CLIMATE_ACTIVITY_STATE, DOMAIN, UNIT_MAP +from .const import CLIMATE_ACTIVITY_STATE, DOMAIN from .coordinator import BMWDataUpdateCoordinator _LOGGER = logging.getLogger(__name__) @@ -36,34 +40,18 @@ class BMWSensorEntityDescription(SensorEntityDescription): """Describes BMW sensor entity.""" key_class: str | None = None - unit_type: str | None = None - value: Callable = lambda x, y: x is_available: Callable[[MyBMWVehicle], bool] = lambda v: v.is_lsc_enabled -def convert_and_round( - state: ValueWithUnit, - converter: Callable[[float | None, str], float], - precision: int, -) -> float | None: - """Safely convert and round a value from ValueWithUnit.""" - if state.value and state.unit: - return round( - converter(state.value, UNIT_MAP.get(state.unit, state.unit)), precision - ) - if state.value: - return state.value - return None - - SENSOR_TYPES: list[BMWSensorEntityDescription] = [ - # --- Generic --- BMWSensorEntityDescription( key="ac_current_limit", translation_key="ac_current_limit", key_class="charging_profile", - unit_type=UnitOfElectricCurrent.AMPERE, + device_class=SensorDeviceClass.CURRENT, + native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, entity_registry_enabled_default=False, + suggested_display_precision=0, is_available=lambda v: v.is_lsc_enabled and v.has_electric_drivetrain, ), BMWSensorEntityDescription( @@ -85,74 +73,81 @@ SENSOR_TYPES: list[BMWSensorEntityDescription] = [ key="charging_status", translation_key="charging_status", key_class="fuel_and_battery", - value=lambda x, y: x.value, is_available=lambda v: v.is_lsc_enabled and v.has_electric_drivetrain, ), BMWSensorEntityDescription( key="charging_target", translation_key="charging_target", key_class="fuel_and_battery", - unit_type=PERCENTAGE, + device_class=SensorDeviceClass.BATTERY, + native_unit_of_measurement=PERCENTAGE, + suggested_display_precision=0, is_available=lambda v: v.is_lsc_enabled and v.has_electric_drivetrain, ), BMWSensorEntityDescription( key="remaining_battery_percent", translation_key="remaining_battery_percent", key_class="fuel_and_battery", - unit_type=PERCENTAGE, device_class=SensorDeviceClass.BATTERY, + native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, + suggested_display_precision=0, is_available=lambda v: v.is_lsc_enabled and v.has_electric_drivetrain, ), - # --- Specific --- BMWSensorEntityDescription( key="mileage", translation_key="mileage", - unit_type=LENGTH, - value=lambda x, hass: convert_and_round(x, hass.config.units.length, 2), + device_class=SensorDeviceClass.DISTANCE, + native_unit_of_measurement=UnitOfLength.KILOMETERS, state_class=SensorStateClass.TOTAL_INCREASING, + suggested_display_precision=0, ), BMWSensorEntityDescription( key="remaining_range_total", translation_key="remaining_range_total", key_class="fuel_and_battery", - unit_type=LENGTH, - value=lambda x, hass: convert_and_round(x, hass.config.units.length, 2), + device_class=SensorDeviceClass.DISTANCE, + native_unit_of_measurement=UnitOfLength.KILOMETERS, state_class=SensorStateClass.MEASUREMENT, + suggested_display_precision=0, ), BMWSensorEntityDescription( key="remaining_range_electric", translation_key="remaining_range_electric", key_class="fuel_and_battery", - unit_type=LENGTH, - value=lambda x, hass: convert_and_round(x, hass.config.units.length, 2), + device_class=SensorDeviceClass.DISTANCE, + native_unit_of_measurement=UnitOfLength.KILOMETERS, state_class=SensorStateClass.MEASUREMENT, + suggested_display_precision=0, is_available=lambda v: v.is_lsc_enabled and v.has_electric_drivetrain, ), BMWSensorEntityDescription( key="remaining_range_fuel", translation_key="remaining_range_fuel", key_class="fuel_and_battery", - unit_type=LENGTH, - value=lambda x, hass: convert_and_round(x, hass.config.units.length, 2), + device_class=SensorDeviceClass.DISTANCE, + native_unit_of_measurement=UnitOfLength.KILOMETERS, state_class=SensorStateClass.MEASUREMENT, + suggested_display_precision=0, is_available=lambda v: v.is_lsc_enabled and v.has_combustion_drivetrain, ), BMWSensorEntityDescription( key="remaining_fuel", translation_key="remaining_fuel", key_class="fuel_and_battery", - unit_type=VOLUME, - value=lambda x, hass: convert_and_round(x, hass.config.units.volume, 2), + device_class=SensorDeviceClass.VOLUME, + native_unit_of_measurement=UnitOfVolume.LITERS, state_class=SensorStateClass.MEASUREMENT, + suggested_display_precision=0, is_available=lambda v: v.is_lsc_enabled and v.has_combustion_drivetrain, ), BMWSensorEntityDescription( key="remaining_fuel_percent", translation_key="remaining_fuel_percent", key_class="fuel_and_battery", - unit_type=PERCENTAGE, + native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, + suggested_display_precision=0, is_available=lambda v: v.is_lsc_enabled and v.has_combustion_drivetrain, ), BMWSensorEntityDescription( @@ -161,7 +156,6 @@ SENSOR_TYPES: list[BMWSensorEntityDescription] = [ key_class="climate", device_class=SensorDeviceClass.ENUM, options=CLIMATE_ACTIVITY_STATE, - value=lambda x, _: x.lower() if x != "UNKNOWN" else None, is_available=lambda v: v.is_remote_climate_stop_enabled, ), ] @@ -201,13 +195,6 @@ class BMWSensor(BMWBaseEntity, SensorEntity): self.entity_description = description self._attr_unique_id = f"{vehicle.vin}-{description.key}" - # Set the correct unit of measurement based on the unit_type - if description.unit_type: - self._attr_native_unit_of_measurement = ( - coordinator.hass.config.units.as_dict().get(description.unit_type) - or description.unit_type - ) - @callback def _handle_coordinator_update(self) -> None: """Handle updated data from the coordinator.""" @@ -225,8 +212,18 @@ class BMWSensor(BMWBaseEntity, SensorEntity): # For datetime without tzinfo, we assume it to be the same timezone as the HA instance if isinstance(state, datetime.datetime) and state.tzinfo is None: state = state.replace(tzinfo=dt_util.get_default_time_zone()) + # For enum types, we only want the value + elif isinstance(state, ValueWithUnit): + state = state.value + # Get lowercase values from StrEnum + elif isinstance(state, StrEnum): + state = state.value.lower() + if state == STATE_UNKNOWN: + state = None - self._attr_native_value = cast( - StateType, self.entity_description.value(state, self.hass) - ) + # special handling for charging_status to avoid a breaking change + if self.entity_description.key == "charging_status" and state: + state = state.upper() + + self._attr_native_value = state super()._handle_coordinator_update() diff --git a/tests/components/bmw_connected_drive/snapshots/test_sensor.ambr b/tests/components/bmw_connected_drive/snapshots/test_sensor.ambr index e3833add777..3455a4599b5 100644 --- a/tests/components/bmw_connected_drive/snapshots/test_sensor.ambr +++ b/tests/components/bmw_connected_drive/snapshots/test_sensor.ambr @@ -20,8 +20,11 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, 'original_name': 'AC current limit', 'platform': 'bmw_connected_drive', @@ -36,6 +39,7 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by MyBMW', + 'device_class': 'current', 'friendly_name': 'i3 (+ REX) AC current limit', 'unit_of_measurement': , }), @@ -211,8 +215,11 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, 'original_name': 'Charging target', 'platform': 'bmw_connected_drive', @@ -227,6 +234,7 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by MyBMW', + 'device_class': 'battery', 'friendly_name': 'i3 (+ REX) Charging target', 'unit_of_measurement': '%', }), @@ -261,8 +269,11 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, 'original_name': 'Mileage', 'platform': 'bmw_connected_drive', @@ -277,6 +288,7 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by MyBMW', + 'device_class': 'distance', 'friendly_name': 'i3 (+ REX) Mileage', 'state_class': , 'unit_of_measurement': , @@ -312,6 +324,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -364,8 +379,11 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, 'original_name': 'Remaining fuel', 'platform': 'bmw_connected_drive', @@ -380,6 +398,7 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by MyBMW', + 'device_class': 'volume', 'friendly_name': 'i3 (+ REX) Remaining fuel', 'state_class': , 'unit_of_measurement': , @@ -415,6 +434,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': None, 'original_icon': None, @@ -466,8 +488,11 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, 'original_name': 'Remaining range electric', 'platform': 'bmw_connected_drive', @@ -482,6 +507,7 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by MyBMW', + 'device_class': 'distance', 'friendly_name': 'i3 (+ REX) Remaining range electric', 'state_class': , 'unit_of_measurement': , @@ -517,8 +543,11 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, 'original_name': 'Remaining range fuel', 'platform': 'bmw_connected_drive', @@ -533,6 +562,7 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by MyBMW', + 'device_class': 'distance', 'friendly_name': 'i3 (+ REX) Remaining range fuel', 'state_class': , 'unit_of_measurement': , @@ -568,8 +598,11 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, 'original_name': 'Remaining range total', 'platform': 'bmw_connected_drive', @@ -584,6 +617,7 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by MyBMW', + 'device_class': 'distance', 'friendly_name': 'i3 (+ REX) Remaining range total', 'state_class': , 'unit_of_measurement': , @@ -617,8 +651,11 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, 'original_name': 'AC current limit', 'platform': 'bmw_connected_drive', @@ -633,6 +670,7 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by MyBMW', + 'device_class': 'current', 'friendly_name': 'i4 eDrive40 AC current limit', 'unit_of_measurement': , }), @@ -808,8 +846,11 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, 'original_name': 'Charging target', 'platform': 'bmw_connected_drive', @@ -824,6 +865,7 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by MyBMW', + 'device_class': 'battery', 'friendly_name': 'i4 eDrive40 Charging target', 'unit_of_measurement': '%', }), @@ -919,8 +961,11 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, 'original_name': 'Mileage', 'platform': 'bmw_connected_drive', @@ -935,6 +980,7 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by MyBMW', + 'device_class': 'distance', 'friendly_name': 'i4 eDrive40 Mileage', 'state_class': , 'unit_of_measurement': , @@ -970,6 +1016,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -1022,8 +1071,11 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, 'original_name': 'Remaining range electric', 'platform': 'bmw_connected_drive', @@ -1038,6 +1090,7 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by MyBMW', + 'device_class': 'distance', 'friendly_name': 'i4 eDrive40 Remaining range electric', 'state_class': , 'unit_of_measurement': , @@ -1073,8 +1126,11 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, 'original_name': 'Remaining range total', 'platform': 'bmw_connected_drive', @@ -1089,6 +1145,7 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by MyBMW', + 'device_class': 'distance', 'friendly_name': 'i4 eDrive40 Remaining range total', 'state_class': , 'unit_of_measurement': , @@ -1122,8 +1179,11 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, 'original_name': 'AC current limit', 'platform': 'bmw_connected_drive', @@ -1138,6 +1198,7 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by MyBMW', + 'device_class': 'current', 'friendly_name': 'iX xDrive50 AC current limit', 'unit_of_measurement': , }), @@ -1313,8 +1374,11 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, 'original_name': 'Charging target', 'platform': 'bmw_connected_drive', @@ -1329,6 +1393,7 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by MyBMW', + 'device_class': 'battery', 'friendly_name': 'iX xDrive50 Charging target', 'unit_of_measurement': '%', }), @@ -1424,8 +1489,11 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, 'original_name': 'Mileage', 'platform': 'bmw_connected_drive', @@ -1440,6 +1508,7 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by MyBMW', + 'device_class': 'distance', 'friendly_name': 'iX xDrive50 Mileage', 'state_class': , 'unit_of_measurement': , @@ -1475,6 +1544,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -1527,8 +1599,11 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, 'original_name': 'Remaining range electric', 'platform': 'bmw_connected_drive', @@ -1543,6 +1618,7 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by MyBMW', + 'device_class': 'distance', 'friendly_name': 'iX xDrive50 Remaining range electric', 'state_class': , 'unit_of_measurement': , @@ -1578,8 +1654,11 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, 'original_name': 'Remaining range total', 'platform': 'bmw_connected_drive', @@ -1594,6 +1673,7 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by MyBMW', + 'device_class': 'distance', 'friendly_name': 'iX xDrive50 Remaining range total', 'state_class': , 'unit_of_measurement': , @@ -1690,8 +1770,11 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, 'original_name': 'Mileage', 'platform': 'bmw_connected_drive', @@ -1706,6 +1789,7 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by MyBMW', + 'device_class': 'distance', 'friendly_name': 'M340i xDrive Mileage', 'state_class': , 'unit_of_measurement': , @@ -1741,8 +1825,11 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, 'original_name': 'Remaining fuel', 'platform': 'bmw_connected_drive', @@ -1757,6 +1844,7 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by MyBMW', + 'device_class': 'volume', 'friendly_name': 'M340i xDrive Remaining fuel', 'state_class': , 'unit_of_measurement': , @@ -1792,6 +1880,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': None, 'original_icon': None, @@ -1843,8 +1934,11 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, 'original_name': 'Remaining range fuel', 'platform': 'bmw_connected_drive', @@ -1859,6 +1953,7 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by MyBMW', + 'device_class': 'distance', 'friendly_name': 'M340i xDrive Remaining range fuel', 'state_class': , 'unit_of_measurement': , @@ -1894,8 +1989,11 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, 'original_name': 'Remaining range total', 'platform': 'bmw_connected_drive', @@ -1910,6 +2008,7 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by MyBMW', + 'device_class': 'distance', 'friendly_name': 'M340i xDrive Remaining range total', 'state_class': , 'unit_of_measurement': , diff --git a/tests/components/bmw_connected_drive/test_sensor.py b/tests/components/bmw_connected_drive/test_sensor.py index b4cdc23ad68..2f83fa108e5 100644 --- a/tests/components/bmw_connected_drive/test_sensor.py +++ b/tests/components/bmw_connected_drive/test_sensor.py @@ -43,17 +43,17 @@ async def test_entity_state_attrs( ("entity_id", "unit_system", "value", "unit_of_measurement"), [ ("sensor.i3_rex_remaining_range_total", METRIC, "279", "km"), - ("sensor.i3_rex_remaining_range_total", IMPERIAL, "173.36", "mi"), + ("sensor.i3_rex_remaining_range_total", IMPERIAL, "173.362562634216", "mi"), ("sensor.i3_rex_mileage", METRIC, "137009", "km"), - ("sensor.i3_rex_mileage", IMPERIAL, "85133.45", "mi"), + ("sensor.i3_rex_mileage", IMPERIAL, "85133.4456772449", "mi"), ("sensor.i3_rex_remaining_battery_percent", METRIC, "82", "%"), ("sensor.i3_rex_remaining_battery_percent", IMPERIAL, "82", "%"), ("sensor.i3_rex_remaining_range_electric", METRIC, "174", "km"), - ("sensor.i3_rex_remaining_range_electric", IMPERIAL, "108.12", "mi"), + ("sensor.i3_rex_remaining_range_electric", IMPERIAL, "108.118587449296", "mi"), ("sensor.i3_rex_remaining_fuel", METRIC, "6", "L"), - ("sensor.i3_rex_remaining_fuel", IMPERIAL, "1.59", "gal"), + ("sensor.i3_rex_remaining_fuel", IMPERIAL, "1.58503231414889", "gal"), ("sensor.i3_rex_remaining_range_fuel", METRIC, "105", "km"), - ("sensor.i3_rex_remaining_range_fuel", IMPERIAL, "65.24", "mi"), + ("sensor.i3_rex_remaining_range_fuel", IMPERIAL, "65.2439751849201", "mi"), ("sensor.m340i_xdrive_remaining_fuel_percent", METRIC, "80", "%"), ("sensor.m340i_xdrive_remaining_fuel_percent", IMPERIAL, "80", "%"), ], From da408c670383f5c09b278be26196ca75d511dcc1 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 4 Jun 2024 10:13:31 +0200 Subject: [PATCH 0190/1445] Initial cleanup for Aladdin connect (#118777) --- .../components/aladdin_connect/__init__.py | 44 ++++++++++--------- .../components/aladdin_connect/api.py | 11 ++--- .../components/aladdin_connect/config_flow.py | 14 +++--- .../components/aladdin_connect/const.py | 8 ---- .../components/aladdin_connect/cover.py | 10 +++-- 5 files changed, 42 insertions(+), 45 deletions(-) diff --git a/homeassistant/components/aladdin_connect/__init__.py b/homeassistant/components/aladdin_connect/__init__.py index 55c4345beb3..dcd26c6cd04 100644 --- a/homeassistant/components/aladdin_connect/__init__.py +++ b/homeassistant/components/aladdin_connect/__init__.py @@ -5,49 +5,51 @@ from __future__ import annotations from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant -from homeassistant.helpers import aiohttp_client, config_entry_oauth2_flow +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.config_entry_oauth2_flow import ( + OAuth2Session, + async_get_config_entry_implementation, +) -from . import api -from .const import CONFIG_FLOW_MINOR_VERSION, CONFIG_FLOW_VERSION +from .api import AsyncConfigEntryAuth PLATFORMS: list[Platform] = [Platform.COVER] +type AladdinConnectConfigEntry = ConfigEntry[AsyncConfigEntryAuth] -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + +async def async_setup_entry( + hass: HomeAssistant, entry: AladdinConnectConfigEntry +) -> bool: """Set up Aladdin Connect Genie from a config entry.""" - implementation = ( - await config_entry_oauth2_flow.async_get_config_entry_implementation( - hass, entry - ) - ) + implementation = await async_get_config_entry_implementation(hass, entry) - session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation) + session = OAuth2Session(hass, entry, implementation) - # If using an aiohttp-based API lib - entry.runtime_data = api.AsyncConfigEntryAuth( - aiohttp_client.async_get_clientsession(hass), session - ) + entry.runtime_data = AsyncConfigEntryAuth(async_get_clientsession(hass), session) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_unload_entry( + hass: HomeAssistant, entry: AladdinConnectConfigEntry +) -> bool: """Unload a config entry.""" return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) -async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: +async def async_migrate_entry( + hass: HomeAssistant, config_entry: AladdinConnectConfigEntry +) -> bool: """Migrate old config.""" - if config_entry.version < CONFIG_FLOW_VERSION: + if config_entry.version < 2: config_entry.async_start_reauth(hass) - new_data = {**config_entry.data} hass.config_entries.async_update_entry( config_entry, - data=new_data, - version=CONFIG_FLOW_VERSION, - minor_version=CONFIG_FLOW_MINOR_VERSION, + version=2, + minor_version=1, ) return True diff --git a/homeassistant/components/aladdin_connect/api.py b/homeassistant/components/aladdin_connect/api.py index 8100cd1e4d8..c4a19ef0081 100644 --- a/homeassistant/components/aladdin_connect/api.py +++ b/homeassistant/components/aladdin_connect/api.py @@ -1,9 +1,11 @@ """API for Aladdin Connect Genie bound to Home Assistant OAuth.""" +from typing import cast + from aiohttp import ClientSession from genie_partner_sdk.auth import Auth -from homeassistant.helpers import config_entry_oauth2_flow +from homeassistant.helpers.config_entry_oauth2_flow import OAuth2Session API_URL = "https://twdvzuefzh.execute-api.us-east-2.amazonaws.com/v1" API_KEY = "k6QaiQmcTm2zfaNns5L1Z8duBtJmhDOW8JawlCC3" @@ -15,7 +17,7 @@ class AsyncConfigEntryAuth(Auth): # type: ignore[misc] def __init__( self, websession: ClientSession, - oauth_session: config_entry_oauth2_flow.OAuth2Session, + oauth_session: OAuth2Session, ) -> None: """Initialize Aladdin Connect Genie auth.""" super().__init__( @@ -25,7 +27,6 @@ class AsyncConfigEntryAuth(Auth): # type: ignore[misc] async def async_get_access_token(self) -> str: """Return a valid access token.""" - if not self._oauth_session.valid_token: - await self._oauth_session.async_ensure_token_valid() + await self._oauth_session.async_ensure_token_valid() - return str(self._oauth_session.token["access_token"]) + return cast(str, self._oauth_session.token["access_token"]) diff --git a/homeassistant/components/aladdin_connect/config_flow.py b/homeassistant/components/aladdin_connect/config_flow.py index aa42574a005..e1a7b44830d 100644 --- a/homeassistant/components/aladdin_connect/config_flow.py +++ b/homeassistant/components/aladdin_connect/config_flow.py @@ -7,19 +7,17 @@ from typing import Any import voluptuous as vol from homeassistant.config_entries import ConfigEntry, ConfigFlowResult -from homeassistant.helpers import config_entry_oauth2_flow +from homeassistant.helpers.config_entry_oauth2_flow import AbstractOAuth2FlowHandler -from .const import CONFIG_FLOW_MINOR_VERSION, CONFIG_FLOW_VERSION, DOMAIN +from .const import DOMAIN -class OAuth2FlowHandler( - config_entry_oauth2_flow.AbstractOAuth2FlowHandler, domain=DOMAIN -): +class AladdinConnectOAuth2FlowHandler(AbstractOAuth2FlowHandler, domain=DOMAIN): """Config flow to handle Aladdin Connect Genie OAuth2 authentication.""" DOMAIN = DOMAIN - VERSION = CONFIG_FLOW_VERSION - MINOR_VERSION = CONFIG_FLOW_MINOR_VERSION + VERSION = 2 + MINOR_VERSION = 1 reauth_entry: ConfigEntry | None = None @@ -43,7 +41,7 @@ class OAuth2FlowHandler( ) return await self.async_step_user() - async def async_oauth_create_entry(self, data: dict) -> ConfigFlowResult: + async def async_oauth_create_entry(self, data: dict[str, Any]) -> ConfigFlowResult: """Create an oauth config entry or update existing entry for reauth.""" if self.reauth_entry: return self.async_update_reload_and_abort( diff --git a/homeassistant/components/aladdin_connect/const.py b/homeassistant/components/aladdin_connect/const.py index 5312826469e..0fe60724154 100644 --- a/homeassistant/components/aladdin_connect/const.py +++ b/homeassistant/components/aladdin_connect/const.py @@ -1,14 +1,6 @@ """Constants for the Aladdin Connect Genie integration.""" -from typing import Final - -from homeassistant.components.cover import CoverEntityFeature - DOMAIN = "aladdin_connect" -CONFIG_FLOW_VERSION = 2 -CONFIG_FLOW_MINOR_VERSION = 1 OAUTH2_AUTHORIZE = "https://app.aladdinconnect.com/login.html" OAUTH2_TOKEN = "https://twdvzuefzh.execute-api.us-east-2.amazonaws.com/v1/oauth2/token" - -SUPPORTED_FEATURES: Final = CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE diff --git a/homeassistant/components/aladdin_connect/cover.py b/homeassistant/components/aladdin_connect/cover.py index cf31b06cbcd..fa5d5c87a2f 100644 --- a/homeassistant/components/aladdin_connect/cover.py +++ b/homeassistant/components/aladdin_connect/cover.py @@ -5,7 +5,11 @@ from typing import Any from genie_partner_sdk.client import AladdinConnectClient -from homeassistant.components.cover import CoverDeviceClass, CoverEntity +from homeassistant.components.cover import ( + CoverDeviceClass, + CoverEntity, + CoverEntityFeature, +) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import PlatformNotReady @@ -14,7 +18,7 @@ from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import api -from .const import DOMAIN, SUPPORTED_FEATURES +from .const import DOMAIN from .model import GarageDoor SCAN_INTERVAL = timedelta(seconds=15) @@ -75,7 +79,7 @@ class AladdinDevice(CoverEntity): """Representation of Aladdin Connect cover.""" _attr_device_class = CoverDeviceClass.GARAGE - _attr_supported_features = SUPPORTED_FEATURES + _attr_supported_features = CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE _attr_has_entity_name = True _attr_name = None From 16fd19f01af5cd7bee16278aff2016e539007d16 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 4 Jun 2024 10:29:51 +0200 Subject: [PATCH 0191/1445] Use model from Aladdin Connect lib (#118778) * Use model from Aladdin Connect lib * Fix --- .coveragerc | 1 - .../components/aladdin_connect/cover.py | 2 +- .../components/aladdin_connect/model.py | 30 ------------------- .../components/aladdin_connect/sensor.py | 2 +- 4 files changed, 2 insertions(+), 33 deletions(-) delete mode 100644 homeassistant/components/aladdin_connect/model.py diff --git a/.coveragerc b/.coveragerc index 0ff06a1184c..40828381725 100644 --- a/.coveragerc +++ b/.coveragerc @@ -62,7 +62,6 @@ omit = homeassistant/components/aladdin_connect/api.py homeassistant/components/aladdin_connect/application_credentials.py homeassistant/components/aladdin_connect/cover.py - homeassistant/components/aladdin_connect/model.py homeassistant/components/aladdin_connect/sensor.py homeassistant/components/alarmdecoder/__init__.py homeassistant/components/alarmdecoder/alarm_control_panel.py diff --git a/homeassistant/components/aladdin_connect/cover.py b/homeassistant/components/aladdin_connect/cover.py index fa5d5c87a2f..54f0ab32db9 100644 --- a/homeassistant/components/aladdin_connect/cover.py +++ b/homeassistant/components/aladdin_connect/cover.py @@ -4,6 +4,7 @@ from datetime import timedelta from typing import Any from genie_partner_sdk.client import AladdinConnectClient +from genie_partner_sdk.model import GarageDoor from homeassistant.components.cover import ( CoverDeviceClass, @@ -19,7 +20,6 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import api from .const import DOMAIN -from .model import GarageDoor SCAN_INTERVAL = timedelta(seconds=15) diff --git a/homeassistant/components/aladdin_connect/model.py b/homeassistant/components/aladdin_connect/model.py deleted file mode 100644 index db08cb7b8b8..00000000000 --- a/homeassistant/components/aladdin_connect/model.py +++ /dev/null @@ -1,30 +0,0 @@ -"""Models for Aladdin connect cover platform.""" - -from __future__ import annotations - -from typing import TypedDict - - -class GarageDoorData(TypedDict): - """Aladdin door data.""" - - device_id: str - door_number: int - name: str - status: str - link_status: str - battery_level: int - - -class GarageDoor: - """Aladdin Garage Door Entity.""" - - def __init__(self, data: GarageDoorData) -> None: - """Create `GarageDoor` from dictionary of data.""" - self.device_id = data["device_id"] - self.door_number = data["door_number"] - self.unique_id = f"{self.device_id}-{self.door_number}" - self.name = data["name"] - self.status = data["status"] - self.link_status = data["link_status"] - self.battery_level = data["battery_level"] diff --git a/homeassistant/components/aladdin_connect/sensor.py b/homeassistant/components/aladdin_connect/sensor.py index 231928656a8..f9ed2a6aeeb 100644 --- a/homeassistant/components/aladdin_connect/sensor.py +++ b/homeassistant/components/aladdin_connect/sensor.py @@ -7,6 +7,7 @@ from dataclasses import dataclass from typing import cast from genie_partner_sdk.client import AladdinConnectClient +from genie_partner_sdk.model import GarageDoor from homeassistant.components.sensor import ( SensorDeviceClass, @@ -22,7 +23,6 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import api from .const import DOMAIN -from .model import GarageDoor @dataclass(frozen=True, kw_only=True) From b54a68750bec1689d25a3ccac5e01b4bd9b72b7f Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 4 Jun 2024 10:37:54 +0200 Subject: [PATCH 0192/1445] Add type hints for FixtureRequest in tests (#118779) --- pylint/plugins/hass_enforce_type_hints.py | 1 + tests/components/ambient_network/conftest.py | 2 +- tests/components/blebox/conftest.py | 3 ++- tests/components/blueprint/test_models.py | 2 +- tests/components/google/test_init.py | 2 +- tests/components/hdmi_cec/test_media_player.py | 2 +- tests/components/home_connect/conftest.py | 2 +- tests/components/influxdb/test_init.py | 7 +++++-- tests/components/influxdb/test_sensor.py | 5 ++++- tests/components/jvc_projector/conftest.py | 6 ++++-- .../components/media_player/test_async_helpers.py | 2 +- tests/components/met/test_config_flow.py | 4 +++- tests/components/modbus/conftest.py | 5 ++++- .../prosegur/test_alarm_control_panel.py | 3 ++- tests/components/recorder/test_purge.py | 3 ++- tests/components/recorder/test_purge_v32_schema.py | 3 ++- tests/components/renault/test_init.py | 2 +- tests/components/renault/test_services.py | 2 +- tests/components/screenlogic/test_services.py | 5 +++-- tests/components/switcher_kis/conftest.py | 4 ++-- tests/components/tami4/conftest.py | 14 +++++++++----- tests/components/vallox/test_sensor.py | 3 ++- .../weatherflow_cloud/test_config_flow.py | 8 ++++++-- tests/components/whirlpool/conftest.py | 4 ++-- tests/components/xiaomi_miio/test_vacuum.py | 5 ++++- tests/components/zha/conftest.py | 2 +- tests/components/zha/test_device.py | 2 +- 27 files changed, 67 insertions(+), 36 deletions(-) diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index bd208808366..3c6139a41e7 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -152,6 +152,7 @@ _TEST_FIXTURES: dict[str, list[str] | str] = { "mqtt_mock_entry": "MqttMockHAClientGenerator", "recorder_db_url": "str", "recorder_mock": "Recorder", + "request": "pytest.FixtureRequest", "requests_mock": "Mocker", "snapshot": "SnapshotAssertion", "socket_enabled": "None", diff --git a/tests/components/ambient_network/conftest.py b/tests/components/ambient_network/conftest.py index ede44b5d92f..6da3add4fd8 100644 --- a/tests/components/ambient_network/conftest.py +++ b/tests/components/ambient_network/conftest.py @@ -66,7 +66,7 @@ async def mock_aioambient(open_api: OpenAPI): @pytest.fixture(name="config_entry") -def config_entry_fixture(request) -> MockConfigEntry: +def config_entry_fixture(request: pytest.FixtureRequest) -> MockConfigEntry: """Mock config entry.""" return MockConfigEntry( domain=ambient_network.DOMAIN, diff --git a/tests/components/blebox/conftest.py b/tests/components/blebox/conftest.py index 868d936d83a..89229575a0b 100644 --- a/tests/components/blebox/conftest.py +++ b/tests/components/blebox/conftest.py @@ -1,5 +1,6 @@ """PyTest fixtures and test helpers.""" +from typing import Any from unittest import mock from unittest.mock import AsyncMock, PropertyMock, patch @@ -71,7 +72,7 @@ def config_fixture(): @pytest.fixture(name="feature") -def feature_fixture(request): +def feature_fixture(request: pytest.FixtureRequest) -> Any: """Return an entity wrapper from given fixture name.""" return request.getfixturevalue(request.param) diff --git a/tests/components/blueprint/test_models.py b/tests/components/blueprint/test_models.py index ea811d8485b..1b84d4abcbe 100644 --- a/tests/components/blueprint/test_models.py +++ b/tests/components/blueprint/test_models.py @@ -27,7 +27,7 @@ def blueprint_1(): @pytest.fixture(params=[False, True]) -def blueprint_2(request): +def blueprint_2(request: pytest.FixtureRequest) -> models.Blueprint: """Blueprint fixture with default inputs.""" blueprint = { "blueprint": { diff --git a/tests/components/google/test_init.py b/tests/components/google/test_init.py index 7b7ab90fadb..de5e2ea9145 100644 --- a/tests/components/google/test_init.py +++ b/tests/components/google/test_init.py @@ -81,7 +81,7 @@ def assert_state(actual: State | None, expected: State | None) -> None: ) def add_event_call_service( hass: HomeAssistant, - request: Any, + request: pytest.FixtureRequest, ) -> Callable[dict[str, Any], Awaitable[None]]: """Fixture for calling the add or create event service.""" (domain, service_call, data, target) = request.param diff --git a/tests/components/hdmi_cec/test_media_player.py b/tests/components/hdmi_cec/test_media_player.py index 4c2c5f42e6e..e052938f1a0 100644 --- a/tests/components/hdmi_cec/test_media_player.py +++ b/tests/components/hdmi_cec/test_media_player.py @@ -70,7 +70,7 @@ from . import MockHDMIDevice, assert_key_press_release ], ids=["skip_assert_state", "run_assert_state"], ) -def assert_state_fixture(hass, request): +def assert_state_fixture(request: pytest.FixtureRequest): """Allow for skipping the assert state changes. This is broken in this entity, but we still want to test that diff --git a/tests/components/home_connect/conftest.py b/tests/components/home_connect/conftest.py index 5107fb44d69..895782454fc 100644 --- a/tests/components/home_connect/conftest.py +++ b/tests/components/home_connect/conftest.py @@ -131,7 +131,7 @@ def mock_get_appliances() -> Generator[None, Any, None]: @pytest.fixture(name="appliance") -def mock_appliance(request) -> Mock: +def mock_appliance(request: pytest.FixtureRequest) -> MagicMock: """Fixture to mock Appliance.""" app = "Washer" if hasattr(request, "param") and request.param: diff --git a/tests/components/influxdb/test_init.py b/tests/components/influxdb/test_init.py index 9d672b7ceb0..1e39eaef6ce 100644 --- a/tests/components/influxdb/test_init.py +++ b/tests/components/influxdb/test_init.py @@ -1,5 +1,6 @@ """The tests for the InfluxDB component.""" +from collections.abc import Generator from dataclasses import dataclass import datetime from http import HTTPStatus @@ -51,7 +52,9 @@ def mock_batch_timeout(hass, monkeypatch): @pytest.fixture(name="mock_client") -def mock_client_fixture(request): +def mock_client_fixture( + request: pytest.FixtureRequest, +) -> Generator[MagicMock, None, None]: """Patch the InfluxDBClient object with mock for version under test.""" if request.param == influxdb.API_VERSION_2: client_target = f"{INFLUX_CLIENT_PATH}V2" @@ -63,7 +66,7 @@ def mock_client_fixture(request): @pytest.fixture(name="get_mock_call") -def get_mock_call_fixture(request): +def get_mock_call_fixture(request: pytest.FixtureRequest): """Get version specific lambda to make write API call mock.""" def v2_call(body, precision): diff --git a/tests/components/influxdb/test_sensor.py b/tests/components/influxdb/test_sensor.py index d3464c7e417..a0d949d5176 100644 --- a/tests/components/influxdb/test_sensor.py +++ b/tests/components/influxdb/test_sensor.py @@ -2,6 +2,7 @@ from __future__ import annotations +from collections.abc import Generator from dataclasses import dataclass from datetime import timedelta from http import HTTPStatus @@ -79,7 +80,9 @@ class Table: @pytest.fixture(name="mock_client") -def mock_client_fixture(request): +def mock_client_fixture( + request: pytest.FixtureRequest, +) -> Generator[MagicMock, None, None]: """Patch the InfluxDBClient object with mock for version under test.""" if request.param == API_VERSION_2: client_target = f"{INFLUXDB_CLIENT_PATH}V2" diff --git a/tests/components/jvc_projector/conftest.py b/tests/components/jvc_projector/conftest.py index 10603e8ae39..10fc83e2581 100644 --- a/tests/components/jvc_projector/conftest.py +++ b/tests/components/jvc_projector/conftest.py @@ -1,7 +1,7 @@ """Fixtures for JVC Projector integration.""" from collections.abc import Generator -from unittest.mock import AsyncMock, patch +from unittest.mock import MagicMock, patch import pytest @@ -15,7 +15,9 @@ from tests.common import MockConfigEntry @pytest.fixture(name="mock_device") -def fixture_mock_device(request) -> Generator[None, AsyncMock, None]: +def fixture_mock_device( + request: pytest.FixtureRequest, +) -> Generator[MagicMock, None, None]: """Return a mocked JVC Projector device.""" target = "homeassistant.components.jvc_projector.JvcProjector" if hasattr(request, "param"): diff --git a/tests/components/media_player/test_async_helpers.py b/tests/components/media_player/test_async_helpers.py index e3d89a9ca2e..783846d8857 100644 --- a/tests/components/media_player/test_async_helpers.py +++ b/tests/components/media_player/test_async_helpers.py @@ -111,7 +111,7 @@ class DescrMediaPlayer(SimpleMediaPlayer): @pytest.fixture(params=[ExtendedMediaPlayer, SimpleMediaPlayer]) -def player(hass, request): +def player(hass: HomeAssistant, request: pytest.FixtureRequest) -> mp.MediaPlayerEntity: """Return a media player.""" return request.param(hass) diff --git a/tests/components/met/test_config_flow.py b/tests/components/met/test_config_flow.py index c494c4afeb9..c7f0311edef 100644 --- a/tests/components/met/test_config_flow.py +++ b/tests/components/met/test_config_flow.py @@ -1,5 +1,7 @@ """Tests for Met.no config flow.""" +from collections.abc import Generator +from typing import Any from unittest.mock import ANY, patch import pytest @@ -17,7 +19,7 @@ from tests.common import MockConfigEntry @pytest.fixture(name="met_setup", autouse=True) -def met_setup_fixture(request): +def met_setup_fixture(request: pytest.FixtureRequest) -> Generator[Any]: """Patch met setup entry.""" if "disable_autouse_fixture" in request.keywords: yield diff --git a/tests/components/modbus/conftest.py b/tests/components/modbus/conftest.py index 153ccb2b888..067fb2d123d 100644 --- a/tests/components/modbus/conftest.py +++ b/tests/components/modbus/conftest.py @@ -4,6 +4,7 @@ import copy from dataclasses import dataclass from datetime import timedelta import logging +from typing import Any from unittest import mock from freezegun.api import FrozenDateTimeFactory @@ -182,7 +183,9 @@ async def do_next_cycle( @pytest.fixture(name="mock_test_state") -async def mock_test_state_fixture(hass, request): +async def mock_test_state_fixture( + hass: HomeAssistant, request: pytest.FixtureRequest +) -> Any: """Mock restore cache.""" mock_restore_cache(hass, request.param) return request.param diff --git a/tests/components/prosegur/test_alarm_control_panel.py b/tests/components/prosegur/test_alarm_control_panel.py index 43ba5e78665..0cb84d46f04 100644 --- a/tests/components/prosegur/test_alarm_control_panel.py +++ b/tests/components/prosegur/test_alarm_control_panel.py @@ -1,5 +1,6 @@ """Tests for the Prosegur alarm control panel device.""" +from collections.abc import Generator from unittest.mock import AsyncMock, patch from pyprosegur.installation import Status @@ -35,7 +36,7 @@ def mock_auth(): @pytest.fixture(params=list(Status)) -def mock_status(request): +def mock_status(request: pytest.FixtureRequest) -> Generator[None, None, None]: """Mock the status of the alarm.""" install = AsyncMock() diff --git a/tests/components/recorder/test_purge.py b/tests/components/recorder/test_purge.py index e80bc7ca7d1..b3412e513a8 100644 --- a/tests/components/recorder/test_purge.py +++ b/tests/components/recorder/test_purge.py @@ -1,5 +1,6 @@ """Test data purging.""" +from collections.abc import Generator from datetime import datetime, timedelta import json import sqlite3 @@ -58,7 +59,7 @@ TEST_EVENT_TYPES = ( @pytest.fixture(name="use_sqlite") -def mock_use_sqlite(request): +def mock_use_sqlite(request: pytest.FixtureRequest) -> Generator[None, None, None]: """Pytest fixture to switch purge method.""" with patch( "homeassistant.components.recorder.core.Recorder.dialect_name", diff --git a/tests/components/recorder/test_purge_v32_schema.py b/tests/components/recorder/test_purge_v32_schema.py index 3946d8896f7..0682f1a5666 100644 --- a/tests/components/recorder/test_purge_v32_schema.py +++ b/tests/components/recorder/test_purge_v32_schema.py @@ -1,5 +1,6 @@ """Test data purging.""" +from collections.abc import Generator from datetime import datetime, timedelta import json import sqlite3 @@ -54,7 +55,7 @@ def db_schema_32(): @pytest.fixture(name="use_sqlite") -def mock_use_sqlite(request): +def mock_use_sqlite(request: pytest.FixtureRequest) -> Generator[None, None, None]: """Pytest fixture to switch purge method.""" with patch( "homeassistant.components.recorder.core.Recorder.dialect_name", diff --git a/tests/components/renault/test_init.py b/tests/components/renault/test_init.py index afd7bccc3af..0cc203c0485 100644 --- a/tests/components/renault/test_init.py +++ b/tests/components/renault/test_init.py @@ -25,7 +25,7 @@ def override_platforms() -> Generator[None, None, None]: @pytest.fixture(autouse=True, name="vehicle_type", params=["zoe_40"]) -def override_vehicle_type(request) -> str: +def override_vehicle_type(request: pytest.FixtureRequest) -> str: """Parametrize vehicle type.""" return request.param diff --git a/tests/components/renault/test_services.py b/tests/components/renault/test_services.py index 5edd6f90b57..9a6d520ccf1 100644 --- a/tests/components/renault/test_services.py +++ b/tests/components/renault/test_services.py @@ -46,7 +46,7 @@ def override_platforms() -> Generator[None, None, None]: @pytest.fixture(autouse=True, name="vehicle_type", params=["zoe_40"]) -def override_vehicle_type(request) -> str: +def override_vehicle_type(request: pytest.FixtureRequest) -> str: """Parametrize vehicle type.""" return request.param diff --git a/tests/components/screenlogic/test_services.py b/tests/components/screenlogic/test_services.py index be9a61002ae..0e2d059fb84 100644 --- a/tests/components/screenlogic/test_services.py +++ b/tests/components/screenlogic/test_services.py @@ -1,5 +1,6 @@ """Tests for ScreenLogic integration service calls.""" +from collections.abc import AsyncGenerator from typing import Any from unittest.mock import DEFAULT, AsyncMock, patch @@ -49,10 +50,10 @@ def dataset_fixture(): @pytest.fixture(name="service_fixture") async def setup_screenlogic_services_fixture( hass: HomeAssistant, - request, + request: pytest.FixtureRequest, device_registry: dr.DeviceRegistry, mock_config_entry: MockConfigEntry, -): +) -> AsyncGenerator[dict[str, Any], None]: """Define the setup for a patched screenlogic integration.""" data = ( marker.args[0] diff --git a/tests/components/switcher_kis/conftest.py b/tests/components/switcher_kis/conftest.py index eb3b92120e1..c9f98efbc50 100644 --- a/tests/components/switcher_kis/conftest.py +++ b/tests/components/switcher_kis/conftest.py @@ -1,7 +1,7 @@ """Common fixtures and objects for the Switcher integration tests.""" from collections.abc import Generator -from unittest.mock import AsyncMock, Mock, patch +from unittest.mock import AsyncMock, MagicMock, Mock, patch import pytest @@ -16,7 +16,7 @@ def mock_setup_entry() -> Generator[AsyncMock, None, None]: @pytest.fixture -def mock_bridge(request): +def mock_bridge(request: pytest.FixtureRequest) -> Generator[MagicMock, None, None]: """Return a mocked SwitcherBridge.""" with ( patch( diff --git a/tests/components/tami4/conftest.py b/tests/components/tami4/conftest.py index 2e8b4f4ffac..64d45cfeca7 100644 --- a/tests/components/tami4/conftest.py +++ b/tests/components/tami4/conftest.py @@ -2,7 +2,7 @@ from collections.abc import Generator from datetime import datetime -from unittest.mock import AsyncMock, patch +from unittest.mock import AsyncMock, MagicMock, patch import pytest from Tami4EdgeAPI.device import Device @@ -37,7 +37,7 @@ def mock_api(mock__get_devices, mock_get_water_quality): @pytest.fixture -def mock__get_devices(request): +def mock__get_devices(request: pytest.FixtureRequest) -> Generator[None, None, None]: """Fixture to mock _get_devices which makes a call to the API.""" side_effect = getattr(request, "param", None) @@ -60,7 +60,9 @@ def mock__get_devices(request): @pytest.fixture -def mock_get_water_quality(request): +def mock_get_water_quality( + request: pytest.FixtureRequest, +) -> Generator[None, None, None]: """Fixture to mock get_water_quality which makes a call to the API.""" side_effect = getattr(request, "param", None) @@ -98,7 +100,9 @@ def mock_setup_entry() -> Generator[AsyncMock, None, None]: @pytest.fixture -def mock_request_otp(request): +def mock_request_otp( + request: pytest.FixtureRequest, +) -> Generator[MagicMock, None, None]: """Mock request_otp.""" side_effect = getattr(request, "param", None) @@ -112,7 +116,7 @@ def mock_request_otp(request): @pytest.fixture -def mock_submit_otp(request): +def mock_submit_otp(request: pytest.FixtureRequest) -> Generator[MagicMock, None, None]: """Mock submit_otp.""" side_effect = getattr(request, "param", None) diff --git a/tests/components/vallox/test_sensor.py b/tests/components/vallox/test_sensor.py index c16094257f5..d7af7bbb576 100644 --- a/tests/components/vallox/test_sensor.py +++ b/tests/components/vallox/test_sensor.py @@ -1,6 +1,7 @@ """Tests for Vallox sensor platform.""" from datetime import datetime, timedelta, tzinfo +from typing import Any import pytest from vallox_websocket_api import MetricData @@ -12,7 +13,7 @@ from tests.common import MockConfigEntry @pytest.fixture -def set_tz(request): +def set_tz(request: pytest.FixtureRequest) -> Any: """Set the default TZ to the one requested.""" request.getfixturevalue(request.param) diff --git a/tests/components/weatherflow_cloud/test_config_flow.py b/tests/components/weatherflow_cloud/test_config_flow.py index b111ef462e6..7ade007ceac 100644 --- a/tests/components/weatherflow_cloud/test_config_flow.py +++ b/tests/components/weatherflow_cloud/test_config_flow.py @@ -56,14 +56,18 @@ async def test_config_flow_abort(hass: HomeAssistant, mock_get_stations) -> None @pytest.mark.parametrize( - "mock_fixture, expected_error", # noqa: PT006 + ("mock_fixture", "expected_error"), [ ("mock_get_stations_500_error", "cannot_connect"), ("mock_get_stations_401_error", "invalid_api_key"), ], ) async def test_config_errors( - hass: HomeAssistant, request, expected_error, mock_fixture, mock_get_stations + hass: HomeAssistant, + request: pytest.FixtureRequest, + expected_error: str, + mock_fixture: str, + mock_get_stations, ) -> None: """Test the config flow for various error scenarios.""" mock_get_stations_bad = request.getfixturevalue(mock_fixture) diff --git a/tests/components/whirlpool/conftest.py b/tests/components/whirlpool/conftest.py index e386012265c..a5926f55a94 100644 --- a/tests/components/whirlpool/conftest.py +++ b/tests/components/whirlpool/conftest.py @@ -18,7 +18,7 @@ MOCK_SAID4 = "said4" name="region", params=[("EU", Region.EU), ("US", Region.US)], ) -def fixture_region(request): +def fixture_region(request: pytest.FixtureRequest) -> tuple[str, Region]: """Return a region for input.""" return request.param @@ -31,7 +31,7 @@ def fixture_region(request): ("Maytag", Brand.Maytag), ], ) -def fixture_brand(request): +def fixture_brand(request: pytest.FixtureRequest) -> tuple[str, Brand]: """Return a brand for input.""" return request.param diff --git a/tests/components/xiaomi_miio/test_vacuum.py b/tests/components/xiaomi_miio/test_vacuum.py index 2cfc3a4f294..3ac17cc85b7 100644 --- a/tests/components/xiaomi_miio/test_vacuum.py +++ b/tests/components/xiaomi_miio/test_vacuum.py @@ -1,5 +1,6 @@ """The tests for the Xiaomi vacuum platform.""" +from collections.abc import Generator from datetime import datetime, time, timedelta from unittest import mock from unittest.mock import MagicMock, patch @@ -140,7 +141,9 @@ new_fanspeeds = { @pytest.fixture(name="mock_mirobo_fanspeeds", params=[old_fanspeeds, new_fanspeeds]) -def mirobo_old_speeds_fixture(request): +def mirobo_old_speeds_fixture( + request: pytest.FixtureRequest, +) -> Generator[MagicMock, None, None]: """Fixture for testing both types of fanspeeds.""" mock_vacuum = MagicMock() mock_vacuum.status().battery = 32 diff --git a/tests/components/zha/conftest.py b/tests/components/zha/conftest.py index d9f335769ec..9e3d642e0f7 100644 --- a/tests/components/zha/conftest.py +++ b/tests/components/zha/conftest.py @@ -385,7 +385,7 @@ def zha_device_restored(hass, zigpy_app_controller, setup_zha): @pytest.fixture(params=["zha_device_joined", "zha_device_restored"]) -def zha_device_joined_restored(request): +def zha_device_joined_restored(request: pytest.FixtureRequest): """Join or restore ZHA device.""" named_method = request.getfixturevalue(request.param) named_method.name = request.param diff --git a/tests/components/zha/test_device.py b/tests/components/zha/test_device.py index 1dd5a8c0db4..87acdc5fd1c 100644 --- a/tests/components/zha/test_device.py +++ b/tests/components/zha/test_device.py @@ -309,7 +309,7 @@ async def test_ota_sw_version( ) async def test_device_restore_availability( hass: HomeAssistant, - request, + request: pytest.FixtureRequest, device, last_seen_delta, is_available, From 78158401940e3022e50a8af244e68fd2b2378f10 Mon Sep 17 00:00:00 2001 From: "Mr. Bubbles" Date: Tue, 4 Jun 2024 10:45:53 +0200 Subject: [PATCH 0193/1445] Add ista EcoTrend integration (#118360) * Add ista EcoTrend integration * move code out of try * Use account owners name as entry title * update config flow tests * add tests for init * Add reauth flow * Add tests for sensors * add translations for reauth * trigger statistics import on first refresh * Move statistics and reauth flow to other PR * Fix tests * some changes * draft_final_final * remove unnecessary icons * changed tests * move device_registry test to init * add text selectors --- .coveragerc | 1 + CODEOWNERS | 2 + .../components/ista_ecotrend/__init__.py | 64 ++ .../components/ista_ecotrend/config_flow.py | 84 ++ .../components/ista_ecotrend/const.py | 3 + .../components/ista_ecotrend/coordinator.py | 103 ++ .../components/ista_ecotrend/icons.json | 15 + .../components/ista_ecotrend/manifest.json | 9 + .../components/ista_ecotrend/sensor.py | 183 ++++ .../components/ista_ecotrend/strings.json | 56 ++ .../components/ista_ecotrend/util.py | 129 +++ homeassistant/generated/config_flows.py | 1 + homeassistant/generated/integrations.json | 6 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/ista_ecotrend/__init__.py | 1 + tests/components/ista_ecotrend/conftest.py | 166 ++++ .../ista_ecotrend/snapshots/test_init.ambr | 61 ++ .../ista_ecotrend/snapshots/test_sensor.ambr | 915 ++++++++++++++++++ .../ista_ecotrend/snapshots/test_util.ambr | 175 ++++ .../ista_ecotrend/test_config_flow.py | 90 ++ tests/components/ista_ecotrend/test_init.py | 99 ++ tests/components/ista_ecotrend/test_sensor.py | 31 + tests/components/ista_ecotrend/test_util.py | 146 +++ 24 files changed, 2346 insertions(+) create mode 100644 homeassistant/components/ista_ecotrend/__init__.py create mode 100644 homeassistant/components/ista_ecotrend/config_flow.py create mode 100644 homeassistant/components/ista_ecotrend/const.py create mode 100644 homeassistant/components/ista_ecotrend/coordinator.py create mode 100644 homeassistant/components/ista_ecotrend/icons.json create mode 100644 homeassistant/components/ista_ecotrend/manifest.json create mode 100644 homeassistant/components/ista_ecotrend/sensor.py create mode 100644 homeassistant/components/ista_ecotrend/strings.json create mode 100644 homeassistant/components/ista_ecotrend/util.py create mode 100644 tests/components/ista_ecotrend/__init__.py create mode 100644 tests/components/ista_ecotrend/conftest.py create mode 100644 tests/components/ista_ecotrend/snapshots/test_init.ambr create mode 100644 tests/components/ista_ecotrend/snapshots/test_sensor.ambr create mode 100644 tests/components/ista_ecotrend/snapshots/test_util.ambr create mode 100644 tests/components/ista_ecotrend/test_config_flow.py create mode 100644 tests/components/ista_ecotrend/test_init.py create mode 100644 tests/components/ista_ecotrend/test_sensor.py create mode 100644 tests/components/ista_ecotrend/test_util.py diff --git a/.coveragerc b/.coveragerc index 40828381725..e556d0aab85 100644 --- a/.coveragerc +++ b/.coveragerc @@ -631,6 +631,7 @@ omit = homeassistant/components/irish_rail_transport/sensor.py homeassistant/components/iss/__init__.py homeassistant/components/iss/sensor.py + homeassistant/components/ista_ecotrend/coordinator.py homeassistant/components/isy994/__init__.py homeassistant/components/isy994/binary_sensor.py homeassistant/components/isy994/button.py diff --git a/CODEOWNERS b/CODEOWNERS index a72683c1737..90d482ce041 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -703,6 +703,8 @@ build.json @home-assistant/supervisor /tests/components/islamic_prayer_times/ @engrbm87 @cpfair /homeassistant/components/iss/ @DurgNomis-drol /tests/components/iss/ @DurgNomis-drol +/homeassistant/components/ista_ecotrend/ @tr4nt0r +/tests/components/ista_ecotrend/ @tr4nt0r /homeassistant/components/isy994/ @bdraco @shbatm /tests/components/isy994/ @bdraco @shbatm /homeassistant/components/izone/ @Swamp-Ig diff --git a/homeassistant/components/ista_ecotrend/__init__.py b/homeassistant/components/ista_ecotrend/__init__.py new file mode 100644 index 00000000000..2bb41dd6f8b --- /dev/null +++ b/homeassistant/components/ista_ecotrend/__init__.py @@ -0,0 +1,64 @@ +"""The ista Ecotrend integration.""" + +from __future__ import annotations + +import logging + +from pyecotrend_ista.exception_classes import ( + InternalServerError, + KeycloakError, + LoginError, + ServerError, +) +from pyecotrend_ista.pyecotrend_ista import PyEcotrendIsta +from requests.exceptions import RequestException + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, Platform +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryError, ConfigEntryNotReady + +from .const import DOMAIN +from .coordinator import IstaCoordinator + +_LOGGER = logging.getLogger(__name__) + +PLATFORMS: list[Platform] = [Platform.SENSOR] + +type IstaConfigEntry = ConfigEntry[IstaCoordinator] + + +async def async_setup_entry(hass: HomeAssistant, entry: IstaConfigEntry) -> bool: + """Set up ista Ecotrend from a config entry.""" + ista = PyEcotrendIsta( + entry.data[CONF_EMAIL], + entry.data[CONF_PASSWORD], + _LOGGER, + ) + try: + await hass.async_add_executor_job(ista.login) + except (ServerError, InternalServerError, RequestException, TimeoutError) as e: + raise ConfigEntryNotReady( + translation_domain=DOMAIN, + translation_key="connection_exception", + ) from e + except (LoginError, KeycloakError) as e: + raise ConfigEntryError( + translation_domain=DOMAIN, + translation_key="authentication_exception", + translation_placeholders={CONF_EMAIL: entry.data[CONF_EMAIL]}, + ) from e + + coordinator = IstaCoordinator(hass, ista) + await coordinator.async_config_entry_first_refresh() + + entry.runtime_data = coordinator + + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: IstaConfigEntry) -> bool: + """Unload a config entry.""" + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/ista_ecotrend/config_flow.py b/homeassistant/components/ista_ecotrend/config_flow.py new file mode 100644 index 00000000000..b58da0f3a56 --- /dev/null +++ b/homeassistant/components/ista_ecotrend/config_flow.py @@ -0,0 +1,84 @@ +"""Config flow for ista Ecotrend integration.""" + +from __future__ import annotations + +import logging +from typing import Any + +from pyecotrend_ista.exception_classes import ( + InternalServerError, + KeycloakError, + LoginError, + ServerError, +) +from pyecotrend_ista.pyecotrend_ista import PyEcotrendIsta +import voluptuous as vol + +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult +from homeassistant.const import CONF_EMAIL, CONF_PASSWORD +from homeassistant.helpers.selector import ( + TextSelector, + TextSelectorConfig, + TextSelectorType, +) + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +STEP_USER_DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_EMAIL): TextSelector( + TextSelectorConfig( + type=TextSelectorType.EMAIL, + autocomplete="email", + ) + ), + vol.Required(CONF_PASSWORD): TextSelector( + TextSelectorConfig( + type=TextSelectorType.PASSWORD, + autocomplete="current-password", + ) + ), + } +) + + +class IstaConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a config flow for ista Ecotrend.""" + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle the initial step.""" + errors: dict[str, str] = {} + if user_input is not None: + ista = PyEcotrendIsta( + user_input[CONF_EMAIL], + user_input[CONF_PASSWORD], + _LOGGER, + ) + try: + await self.hass.async_add_executor_job(ista.login) + except (ServerError, InternalServerError): + errors["base"] = "cannot_connect" + except (LoginError, KeycloakError): + errors["base"] = "invalid_auth" + except Exception: + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + title = f"{ista._a_firstName} {ista._a_lastName}".strip() # noqa: SLF001 + await self.async_set_unique_id(ista._uuid) # noqa: SLF001 + self._abort_if_unique_id_configured() + return self.async_create_entry( + title=title or "ista EcoTrend", data=user_input + ) + + return self.async_show_form( + step_id="user", + data_schema=self.add_suggested_values_to_schema( + data_schema=STEP_USER_DATA_SCHEMA, suggested_values=user_input + ), + errors=errors, + ) diff --git a/homeassistant/components/ista_ecotrend/const.py b/homeassistant/components/ista_ecotrend/const.py new file mode 100644 index 00000000000..92c12b0f0e4 --- /dev/null +++ b/homeassistant/components/ista_ecotrend/const.py @@ -0,0 +1,3 @@ +"""Constants for the ista Ecotrend integration.""" + +DOMAIN = "ista_ecotrend" diff --git a/homeassistant/components/ista_ecotrend/coordinator.py b/homeassistant/components/ista_ecotrend/coordinator.py new file mode 100644 index 00000000000..78a31d560dd --- /dev/null +++ b/homeassistant/components/ista_ecotrend/coordinator.py @@ -0,0 +1,103 @@ +"""DataUpdateCoordinator for Ista EcoTrend integration.""" + +from __future__ import annotations + +from datetime import timedelta +import logging +from typing import Any + +from pyecotrend_ista.exception_classes import ( + InternalServerError, + KeycloakError, + LoginError, + ServerError, +) +from pyecotrend_ista.pyecotrend_ista import PyEcotrendIsta +from requests.exceptions import RequestException + +from homeassistant.const import CONF_EMAIL +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryError +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +class IstaCoordinator(DataUpdateCoordinator[dict[str, Any]]): + """Ista EcoTrend data update coordinator.""" + + def __init__(self, hass: HomeAssistant, ista: PyEcotrendIsta) -> None: + """Initialize ista EcoTrend data update coordinator.""" + super().__init__( + hass, + _LOGGER, + name=DOMAIN, + update_interval=timedelta(days=1), + ) + self.ista = ista + self.details: dict[str, Any] = {} + + async def _async_update_data(self): + """Fetch ista EcoTrend data.""" + + if not self.details: + self.details = await self.async_get_details() + + try: + return await self.hass.async_add_executor_job(self.get_consumption_data) + except ( + ServerError, + InternalServerError, + RequestException, + TimeoutError, + ) as e: + raise UpdateFailed( + "Unable to connect and retrieve data from ista EcoTrend, try again later" + ) from e + except (LoginError, KeycloakError) as e: + raise ConfigEntryError( + translation_domain=DOMAIN, + translation_key="authentication_exception", + translation_placeholders={CONF_EMAIL: self.ista._email}, # noqa: SLF001 + ) from e + + def get_consumption_data(self) -> dict[str, Any]: + """Get raw json data for all consumption units.""" + + return { + consumption_unit: self.ista.get_raw(consumption_unit) + for consumption_unit in self.ista.getUUIDs() + } + + async def async_get_details(self) -> dict[str, Any]: + """Retrieve details of consumption units.""" + try: + result = await self.hass.async_add_executor_job( + self.ista.get_consumption_unit_details + ) + except ( + ServerError, + InternalServerError, + RequestException, + TimeoutError, + ) as e: + raise UpdateFailed( + "Unable to connect and retrieve data from ista EcoTrend, try again later" + ) from e + except (LoginError, KeycloakError) as e: + raise ConfigEntryError( + translation_domain=DOMAIN, + translation_key="authentication_exception", + translation_placeholders={CONF_EMAIL: self.ista._email}, # noqa: SLF001 + ) from e + else: + return { + consumption_unit: next( + details + for details in result["consumptionUnits"] + if details["id"] == consumption_unit + ) + for consumption_unit in self.ista.getUUIDs() + } diff --git a/homeassistant/components/ista_ecotrend/icons.json b/homeassistant/components/ista_ecotrend/icons.json new file mode 100644 index 00000000000..4223e8488ff --- /dev/null +++ b/homeassistant/components/ista_ecotrend/icons.json @@ -0,0 +1,15 @@ +{ + "entity": { + "sensor": { + "heating": { + "default": "mdi:radiator" + }, + "water": { + "default": "mdi:faucet" + }, + "hot_water": { + "default": "mdi:faucet" + } + } + } +} diff --git a/homeassistant/components/ista_ecotrend/manifest.json b/homeassistant/components/ista_ecotrend/manifest.json new file mode 100644 index 00000000000..679825439e4 --- /dev/null +++ b/homeassistant/components/ista_ecotrend/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "ista_ecotrend", + "name": "ista Ecotrend", + "codeowners": ["@tr4nt0r"], + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/ista_ecotrend", + "iot_class": "cloud_polling", + "requirements": ["pyecotrend-ista==3.1.1"] +} diff --git a/homeassistant/components/ista_ecotrend/sensor.py b/homeassistant/components/ista_ecotrend/sensor.py new file mode 100644 index 00000000000..844b86e1689 --- /dev/null +++ b/homeassistant/components/ista_ecotrend/sensor.py @@ -0,0 +1,183 @@ +"""Sensor platform for Ista EcoTrend integration.""" + +from __future__ import annotations + +from dataclasses import dataclass +from enum import StrEnum + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.const import UnitOfEnergy, UnitOfVolume +from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import StateType +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from . import IstaConfigEntry +from .const import DOMAIN +from .coordinator import IstaCoordinator +from .util import IstaConsumptionType, IstaValueType, get_native_value + + +@dataclass(kw_only=True, frozen=True) +class IstaSensorEntityDescription(SensorEntityDescription): + """Ista EcoTrend Sensor Description.""" + + consumption_type: IstaConsumptionType + value_type: IstaValueType | None = None + + +class IstaSensorEntity(StrEnum): + """Ista EcoTrend Entities.""" + + HEATING = "heating" + HEATING_ENERGY = "heating_energy" + HEATING_COST = "heating_cost" + + HOT_WATER = "hot_water" + HOT_WATER_ENERGY = "hot_water_energy" + HOT_WATER_COST = "hot_water_cost" + + WATER = "water" + WATER_COST = "water_cost" + + +SENSOR_DESCRIPTIONS: tuple[IstaSensorEntityDescription, ...] = ( + IstaSensorEntityDescription( + key=IstaSensorEntity.HEATING, + translation_key=IstaSensorEntity.HEATING, + suggested_display_precision=0, + consumption_type=IstaConsumptionType.HEATING, + ), + IstaSensorEntityDescription( + key=IstaSensorEntity.HEATING_ENERGY, + translation_key=IstaSensorEntity.HEATING_ENERGY, + device_class=SensorDeviceClass.ENERGY, + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + state_class=SensorStateClass.TOTAL, + suggested_display_precision=1, + consumption_type=IstaConsumptionType.HEATING, + value_type=IstaValueType.ENERGY, + ), + IstaSensorEntityDescription( + key=IstaSensorEntity.HEATING_COST, + translation_key=IstaSensorEntity.HEATING_COST, + device_class=SensorDeviceClass.MONETARY, + native_unit_of_measurement="EUR", + state_class=SensorStateClass.TOTAL, + suggested_display_precision=0, + entity_registry_enabled_default=False, + consumption_type=IstaConsumptionType.HEATING, + value_type=IstaValueType.COSTS, + ), + IstaSensorEntityDescription( + key=IstaSensorEntity.HOT_WATER, + translation_key=IstaSensorEntity.HOT_WATER, + device_class=SensorDeviceClass.WATER, + native_unit_of_measurement=UnitOfVolume.CUBIC_METERS, + state_class=SensorStateClass.TOTAL, + suggested_display_precision=1, + consumption_type=IstaConsumptionType.HOT_WATER, + ), + IstaSensorEntityDescription( + key=IstaSensorEntity.HOT_WATER_ENERGY, + translation_key=IstaSensorEntity.HOT_WATER_ENERGY, + device_class=SensorDeviceClass.ENERGY, + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + state_class=SensorStateClass.TOTAL, + suggested_display_precision=1, + consumption_type=IstaConsumptionType.HOT_WATER, + value_type=IstaValueType.ENERGY, + ), + IstaSensorEntityDescription( + key=IstaSensorEntity.HOT_WATER_COST, + translation_key=IstaSensorEntity.HOT_WATER_COST, + device_class=SensorDeviceClass.MONETARY, + native_unit_of_measurement="EUR", + state_class=SensorStateClass.TOTAL, + suggested_display_precision=0, + entity_registry_enabled_default=False, + consumption_type=IstaConsumptionType.HOT_WATER, + value_type=IstaValueType.COSTS, + ), + IstaSensorEntityDescription( + key=IstaSensorEntity.WATER, + translation_key=IstaSensorEntity.WATER, + device_class=SensorDeviceClass.WATER, + native_unit_of_measurement=UnitOfVolume.CUBIC_METERS, + state_class=SensorStateClass.TOTAL, + suggested_display_precision=1, + entity_registry_enabled_default=False, + consumption_type=IstaConsumptionType.WATER, + ), + IstaSensorEntityDescription( + key=IstaSensorEntity.WATER_COST, + translation_key=IstaSensorEntity.WATER_COST, + device_class=SensorDeviceClass.MONETARY, + native_unit_of_measurement="EUR", + state_class=SensorStateClass.TOTAL, + suggested_display_precision=0, + entity_registry_enabled_default=False, + consumption_type=IstaConsumptionType.WATER, + value_type=IstaValueType.COSTS, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: IstaConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the ista EcoTrend sensors.""" + + coordinator = config_entry.runtime_data + + async_add_entities( + IstaSensor(coordinator, description, consumption_unit) + for description in SENSOR_DESCRIPTIONS + for consumption_unit in coordinator.data + ) + + +class IstaSensor(CoordinatorEntity[IstaCoordinator], SensorEntity): + """Ista EcoTrend sensor.""" + + entity_description: IstaSensorEntityDescription + _attr_has_entity_name = True + + def __init__( + self, + coordinator: IstaCoordinator, + entity_description: IstaSensorEntityDescription, + consumption_unit: str, + ) -> None: + """Initialize the ista EcoTrend sensor.""" + super().__init__(coordinator) + self.consumption_unit = consumption_unit + self.entity_description = entity_description + self._attr_unique_id = f"{consumption_unit}_{entity_description.key}" + self._attr_device_info = DeviceInfo( + entry_type=DeviceEntryType.SERVICE, + manufacturer="ista SE", + model="ista EcoTrend", + name=f"{coordinator.details[consumption_unit]["address"]["street"]} " + f"{coordinator.details[consumption_unit]["address"]["houseNumber"]}".strip(), + configuration_url="https://ecotrend.ista.de/", + identifiers={(DOMAIN, consumption_unit)}, + ) + + @property + def native_value(self) -> StateType: + """Return the state of the device.""" + + return get_native_value( + data=self.coordinator.data[self.consumption_unit], + consumption_type=self.entity_description.consumption_type, + value_type=self.entity_description.value_type, + ) diff --git a/homeassistant/components/ista_ecotrend/strings.json b/homeassistant/components/ista_ecotrend/strings.json new file mode 100644 index 00000000000..fa8fcc28c20 --- /dev/null +++ b/homeassistant/components/ista_ecotrend/strings.json @@ -0,0 +1,56 @@ +{ + "config": { + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "step": { + "user": { + "data": { + "email": "[%key:common::config_flow::data::email%]", + "password": "[%key:common::config_flow::data::password%]" + } + } + } + }, + "entity": { + "sensor": { + "heating": { + "name": "Heating" + }, + "heating_cost": { + "name": "Heating cost" + }, + "heating_energy": { + "name": "Heating energy" + }, + "hot_water": { + "name": "Hot water" + }, + "hot_water_cost": { + "name": "Hot water cost" + }, + "hot_water_energy": { + "name": "Hot water energy" + }, + "water": { + "name": "Water" + }, + "water_cost": { + "name": "Water cost" + } + } + }, + "exceptions": { + "authentication_exception": { + "message": "Authentication failed for {email}, check your login credentials" + }, + "connection_exception": { + "message": "Unable to connect and retrieve data from ista EcoTrends, try again later" + } + } +} diff --git a/homeassistant/components/ista_ecotrend/util.py b/homeassistant/components/ista_ecotrend/util.py new file mode 100644 index 00000000000..db64dbf85db --- /dev/null +++ b/homeassistant/components/ista_ecotrend/util.py @@ -0,0 +1,129 @@ +"""Utility functions for Ista EcoTrend integration.""" + +from __future__ import annotations + +import datetime +from enum import StrEnum +from typing import Any + +from homeassistant.util import dt as dt_util + + +class IstaConsumptionType(StrEnum): + """Types of consumptions from ista.""" + + HEATING = "heating" + HOT_WATER = "warmwater" + WATER = "water" + + +class IstaValueType(StrEnum): + """Values type Costs or energy.""" + + COSTS = "costs" + ENERGY = "energy" + + +def get_consumptions( + data: dict[str, Any], value_type: IstaValueType | None = None +) -> list[dict[str, Any]]: + """Get consumption readings and sort in ascending order by date.""" + result: list = [] + if consumptions := data.get( + "costs" if value_type == IstaValueType.COSTS else "consumptions", [] + ): + result = [ + { + "readings": readings.get("costsByEnergyType") + if value_type == IstaValueType.COSTS + else readings.get("readings"), + "date": last_day_of_month(**readings["date"]), + } + for readings in consumptions + ] + result.sort(key=lambda d: d["date"]) + return result + + +def get_values_by_type( + consumptions: dict[str, Any], consumption_type: IstaConsumptionType +) -> dict[str, Any]: + """Get the readings of a certain type.""" + + readings: list = consumptions.get("readings", []) or consumptions.get( + "costsByEnergyType", [] + ) + + return next( + (values for values in readings if values.get("type") == consumption_type.value), + {}, + ) + + +def as_number(value: str | float | None) -> float | int | None: + """Convert readings to float or int. + + Readings in the json response are returned as strings, + float values have comma as decimal separator + """ + if isinstance(value, str): + return int(value) if value.isdigit() else float(value.replace(",", ".")) + + return value + + +def last_day_of_month(month: int, year: int) -> datetime.datetime: + """Get the last day of the month.""" + + return dt_util.as_local( + datetime.datetime( + month=month + 1 if month < 12 else 1, + year=year if month < 12 else year + 1, + day=1, + tzinfo=datetime.UTC, + ) + + datetime.timedelta(days=-1) + ) + + +def get_native_value( + data, + consumption_type: IstaConsumptionType, + value_type: IstaValueType | None = None, +) -> int | float | None: + """Determine the latest value for the sensor.""" + + if last_value := get_statistics(data, consumption_type, value_type): + return last_value[-1].get("value") + return None + + +def get_statistics( + data, + consumption_type: IstaConsumptionType, + value_type: IstaValueType | None = None, +) -> list[dict[str, Any]] | None: + """Determine the latest value for the sensor.""" + + if monthly_consumptions := get_consumptions(data, value_type): + return [ + { + "value": as_number( + get_values_by_type( + consumptions=consumptions, + consumption_type=consumption_type, + ).get( + "additionalValue" + if value_type == IstaValueType.ENERGY + else "value" + ) + ), + "date": consumptions["date"], + } + for consumptions in monthly_consumptions + if get_values_by_type( + consumptions=consumptions, + consumption_type=consumption_type, + ).get("additionalValue" if value_type == IstaValueType.ENERGY else "value") + ] + return None diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index e38513046f1..d6060a360b5 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -267,6 +267,7 @@ FLOWS = { "iqvia", "islamic_prayer_times", "iss", + "ista_ecotrend", "isy994", "izone", "jellyfin", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 194ca540b3f..578f2631b25 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -2922,6 +2922,12 @@ "config_flow": true, "iot_class": "cloud_polling" }, + "ista_ecotrend": { + "name": "ista Ecotrend", + "integration_type": "hub", + "config_flow": true, + "iot_class": "cloud_polling" + }, "isy994": { "name": "Universal Devices ISY/IoX", "integration_type": "hub", diff --git a/requirements_all.txt b/requirements_all.txt index 54aee2cdafd..1bb6417f909 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1808,6 +1808,9 @@ pyecoforest==0.4.0 # homeassistant.components.econet pyeconet==0.1.22 +# homeassistant.components.ista_ecotrend +pyecotrend-ista==3.1.1 + # homeassistant.components.edimax pyedimax==0.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1198fee3cac..1308597ce9f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1419,6 +1419,9 @@ pyecoforest==0.4.0 # homeassistant.components.econet pyeconet==0.1.22 +# homeassistant.components.ista_ecotrend +pyecotrend-ista==3.1.1 + # homeassistant.components.efergy pyefergy==22.5.0 diff --git a/tests/components/ista_ecotrend/__init__.py b/tests/components/ista_ecotrend/__init__.py new file mode 100644 index 00000000000..d636c2a399c --- /dev/null +++ b/tests/components/ista_ecotrend/__init__.py @@ -0,0 +1 @@ +"""Tests for the ista Ecotrend integration.""" diff --git a/tests/components/ista_ecotrend/conftest.py b/tests/components/ista_ecotrend/conftest.py new file mode 100644 index 00000000000..786be230c05 --- /dev/null +++ b/tests/components/ista_ecotrend/conftest.py @@ -0,0 +1,166 @@ +"""Common fixtures for the ista Ecotrend tests.""" + +from collections.abc import Generator +from typing import Any +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest + +from homeassistant.components.ista_ecotrend.const import DOMAIN +from homeassistant.const import CONF_EMAIL, CONF_PASSWORD + +from tests.common import MockConfigEntry + + +@pytest.fixture(name="ista_config_entry") +def mock_ista_config_entry() -> MockConfigEntry: + """Mock ista EcoTrend configuration entry.""" + return MockConfigEntry( + domain=DOMAIN, + data={ + CONF_EMAIL: "test@example.com", + CONF_PASSWORD: "test-password", + }, + unique_id="26e93f1a-c828-11ea-87d0-0242ac130003", + ) + + +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock, None, None]: + """Override async_setup_entry.""" + with patch( + "homeassistant.components.ista_ecotrend.async_setup_entry", return_value=True + ) as mock_setup_entry: + yield mock_setup_entry + + +@pytest.fixture +def mock_ista() -> Generator[MagicMock, None, None]: + """Mock Pyecotrend_ista client.""" + + with ( + patch( + "homeassistant.components.ista_ecotrend.PyEcotrendIsta", + autospec=True, + ) as mock_client, + patch( + "homeassistant.components.ista_ecotrend.config_flow.PyEcotrendIsta", + new=mock_client, + ), + patch( + "homeassistant.components.ista_ecotrend.coordinator.PyEcotrendIsta", + new=mock_client, + ), + ): + client = mock_client.return_value + client._uuid = "26e93f1a-c828-11ea-87d0-0242ac130003" + client._a_firstName = "Max" + client._a_lastName = "Istamann" + client.get_consumption_unit_details.return_value = { + "consumptionUnits": [ + { + "id": "26e93f1a-c828-11ea-87d0-0242ac130003", + "address": { + "street": "Luxemburger Str.", + "houseNumber": "1", + }, + }, + { + "id": "eaf5c5c8-889f-4a3c-b68c-e9a676505762", + "address": { + "street": "Bahnhofsstr.", + "houseNumber": "1A", + }, + }, + ] + } + client.getUUIDs.return_value = [ + "26e93f1a-c828-11ea-87d0-0242ac130003", + "eaf5c5c8-889f-4a3c-b68c-e9a676505762", + ] + client.get_raw = get_raw + + yield client + + +def get_raw(obj_uuid: str | None = None) -> dict[str, Any]: + """Mock function get_raw.""" + return { + "consumptionUnitId": obj_uuid, + "consumptions": [ + { + "date": {"month": 5, "year": 2024}, + "readings": [ + { + "type": "heating", + "value": "35", + "additionalValue": "38,0", + }, + { + "type": "warmwater", + "value": "1,0", + "additionalValue": "57,0", + }, + { + "type": "water", + "value": "5,0", + }, + ], + }, + { + "date": {"month": 4, "year": 2024}, + "readings": [ + { + "type": "heating", + "value": "104", + "additionalValue": "113,0", + }, + { + "type": "warmwater", + "value": "1,1", + "additionalValue": "61,1", + }, + { + "type": "water", + "value": "6,8", + }, + ], + }, + ], + "costs": [ + { + "date": {"month": 5, "year": 2024}, + "costsByEnergyType": [ + { + "type": "heating", + "value": 21, + }, + { + "type": "warmwater", + "value": 7, + }, + { + "type": "water", + "value": 3, + }, + ], + }, + { + "date": {"month": 4, "year": 2024}, + "costsByEnergyType": [ + { + "type": "heating", + "value": 62, + }, + { + "type": "warmwater", + "value": 7, + }, + { + "type": "water", + "value": 2, + }, + ], + }, + ], + } diff --git a/tests/components/ista_ecotrend/snapshots/test_init.ambr b/tests/components/ista_ecotrend/snapshots/test_init.ambr new file mode 100644 index 00000000000..a9d13510b54 --- /dev/null +++ b/tests/components/ista_ecotrend/snapshots/test_init.ambr @@ -0,0 +1,61 @@ +# serializer version: 1 +# name: test_device_registry + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'configuration_url': 'https://ecotrend.ista.de/', + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': , + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'ista_ecotrend', + '26e93f1a-c828-11ea-87d0-0242ac130003', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'ista SE', + 'model': 'ista EcoTrend', + 'name': 'Luxemburger Str. 1', + 'name_by_user': None, + 'serial_number': None, + 'suggested_area': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry.1 + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'configuration_url': 'https://ecotrend.ista.de/', + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': , + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'ista_ecotrend', + 'eaf5c5c8-889f-4a3c-b68c-e9a676505762', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'ista SE', + 'model': 'ista EcoTrend', + 'name': 'Bahnhofsstr. 1A', + 'name_by_user': None, + 'serial_number': None, + 'suggested_area': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- diff --git a/tests/components/ista_ecotrend/snapshots/test_sensor.ambr b/tests/components/ista_ecotrend/snapshots/test_sensor.ambr new file mode 100644 index 00000000000..c312f9b6350 --- /dev/null +++ b/tests/components/ista_ecotrend/snapshots/test_sensor.ambr @@ -0,0 +1,915 @@ +# serializer version: 1 +# name: test_setup.32 + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'configuration_url': 'https://ecotrend.ista.de/', + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': , + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'ista_ecotrend', + '26e93f1a-c828-11ea-87d0-0242ac130003', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'ista SE', + 'model': 'ista EcoTrend', + 'name': 'Luxemburger Str. 1', + 'name_by_user': None, + 'serial_number': None, + 'suggested_area': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_setup.33 + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'configuration_url': 'https://ecotrend.ista.de/', + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': , + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'ista_ecotrend', + 'eaf5c5c8-889f-4a3c-b68c-e9a676505762', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'ista SE', + 'model': 'ista EcoTrend', + 'name': 'Bahnhofsstr. 1A', + 'name_by_user': None, + 'serial_number': None, + 'suggested_area': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_setup[sensor.bahnhofsstr_1a_heating-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.bahnhofsstr_1a_heating', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Heating', + 'platform': 'ista_ecotrend', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'eaf5c5c8-889f-4a3c-b68c-e9a676505762_heating', + 'unit_of_measurement': None, + }) +# --- +# name: test_setup[sensor.bahnhofsstr_1a_heating-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Bahnhofsstr. 1A Heating', + }), + 'context': , + 'entity_id': 'sensor.bahnhofsstr_1a_heating', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '35', + }) +# --- +# name: test_setup[sensor.bahnhofsstr_1a_heating_cost-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.bahnhofsstr_1a_heating_cost', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Heating cost', + 'platform': 'ista_ecotrend', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'eaf5c5c8-889f-4a3c-b68c-e9a676505762_heating_cost', + 'unit_of_measurement': 'EUR', + }) +# --- +# name: test_setup[sensor.bahnhofsstr_1a_heating_cost-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'monetary', + 'friendly_name': 'Bahnhofsstr. 1A Heating cost', + 'state_class': , + 'unit_of_measurement': 'EUR', + }), + 'context': , + 'entity_id': 'sensor.bahnhofsstr_1a_heating_cost', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '21', + }) +# --- +# name: test_setup[sensor.bahnhofsstr_1a_heating_energy-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.bahnhofsstr_1a_heating_energy', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Heating energy', + 'platform': 'ista_ecotrend', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'eaf5c5c8-889f-4a3c-b68c-e9a676505762_heating_energy', + 'unit_of_measurement': , + }) +# --- +# name: test_setup[sensor.bahnhofsstr_1a_heating_energy-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Bahnhofsstr. 1A Heating energy', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.bahnhofsstr_1a_heating_energy', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '38.0', + }) +# --- +# name: test_setup[sensor.bahnhofsstr_1a_hot_water-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.bahnhofsstr_1a_hot_water', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Hot water', + 'platform': 'ista_ecotrend', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'eaf5c5c8-889f-4a3c-b68c-e9a676505762_hot_water', + 'unit_of_measurement': , + }) +# --- +# name: test_setup[sensor.bahnhofsstr_1a_hot_water-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'water', + 'friendly_name': 'Bahnhofsstr. 1A Hot water', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.bahnhofsstr_1a_hot_water', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1.0', + }) +# --- +# name: test_setup[sensor.bahnhofsstr_1a_hot_water_cost-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.bahnhofsstr_1a_hot_water_cost', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Hot water cost', + 'platform': 'ista_ecotrend', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'eaf5c5c8-889f-4a3c-b68c-e9a676505762_hot_water_cost', + 'unit_of_measurement': 'EUR', + }) +# --- +# name: test_setup[sensor.bahnhofsstr_1a_hot_water_cost-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'monetary', + 'friendly_name': 'Bahnhofsstr. 1A Hot water cost', + 'state_class': , + 'unit_of_measurement': 'EUR', + }), + 'context': , + 'entity_id': 'sensor.bahnhofsstr_1a_hot_water_cost', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '7', + }) +# --- +# name: test_setup[sensor.bahnhofsstr_1a_hot_water_energy-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.bahnhofsstr_1a_hot_water_energy', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Hot water energy', + 'platform': 'ista_ecotrend', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'eaf5c5c8-889f-4a3c-b68c-e9a676505762_hot_water_energy', + 'unit_of_measurement': , + }) +# --- +# name: test_setup[sensor.bahnhofsstr_1a_hot_water_energy-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Bahnhofsstr. 1A Hot water energy', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.bahnhofsstr_1a_hot_water_energy', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '57.0', + }) +# --- +# name: test_setup[sensor.bahnhofsstr_1a_water-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.bahnhofsstr_1a_water', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Water', + 'platform': 'ista_ecotrend', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'eaf5c5c8-889f-4a3c-b68c-e9a676505762_water', + 'unit_of_measurement': , + }) +# --- +# name: test_setup[sensor.bahnhofsstr_1a_water-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'water', + 'friendly_name': 'Bahnhofsstr. 1A Water', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.bahnhofsstr_1a_water', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '5.0', + }) +# --- +# name: test_setup[sensor.bahnhofsstr_1a_water_cost-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.bahnhofsstr_1a_water_cost', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Water cost', + 'platform': 'ista_ecotrend', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'eaf5c5c8-889f-4a3c-b68c-e9a676505762_water_cost', + 'unit_of_measurement': 'EUR', + }) +# --- +# name: test_setup[sensor.bahnhofsstr_1a_water_cost-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'monetary', + 'friendly_name': 'Bahnhofsstr. 1A Water cost', + 'state_class': , + 'unit_of_measurement': 'EUR', + }), + 'context': , + 'entity_id': 'sensor.bahnhofsstr_1a_water_cost', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '3', + }) +# --- +# name: test_setup[sensor.luxemburger_str_1_heating-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.luxemburger_str_1_heating', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Heating', + 'platform': 'ista_ecotrend', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': '26e93f1a-c828-11ea-87d0-0242ac130003_heating', + 'unit_of_measurement': None, + }) +# --- +# name: test_setup[sensor.luxemburger_str_1_heating-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Luxemburger Str. 1 Heating', + }), + 'context': , + 'entity_id': 'sensor.luxemburger_str_1_heating', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '35', + }) +# --- +# name: test_setup[sensor.luxemburger_str_1_heating_cost-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.luxemburger_str_1_heating_cost', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Heating cost', + 'platform': 'ista_ecotrend', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': '26e93f1a-c828-11ea-87d0-0242ac130003_heating_cost', + 'unit_of_measurement': 'EUR', + }) +# --- +# name: test_setup[sensor.luxemburger_str_1_heating_cost-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'monetary', + 'friendly_name': 'Luxemburger Str. 1 Heating cost', + 'state_class': , + 'unit_of_measurement': 'EUR', + }), + 'context': , + 'entity_id': 'sensor.luxemburger_str_1_heating_cost', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '21', + }) +# --- +# name: test_setup[sensor.luxemburger_str_1_heating_energy-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.luxemburger_str_1_heating_energy', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Heating energy', + 'platform': 'ista_ecotrend', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': '26e93f1a-c828-11ea-87d0-0242ac130003_heating_energy', + 'unit_of_measurement': , + }) +# --- +# name: test_setup[sensor.luxemburger_str_1_heating_energy-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Luxemburger Str. 1 Heating energy', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.luxemburger_str_1_heating_energy', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '38.0', + }) +# --- +# name: test_setup[sensor.luxemburger_str_1_hot_water-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.luxemburger_str_1_hot_water', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Hot water', + 'platform': 'ista_ecotrend', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': '26e93f1a-c828-11ea-87d0-0242ac130003_hot_water', + 'unit_of_measurement': , + }) +# --- +# name: test_setup[sensor.luxemburger_str_1_hot_water-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'water', + 'friendly_name': 'Luxemburger Str. 1 Hot water', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.luxemburger_str_1_hot_water', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1.0', + }) +# --- +# name: test_setup[sensor.luxemburger_str_1_hot_water_cost-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.luxemburger_str_1_hot_water_cost', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Hot water cost', + 'platform': 'ista_ecotrend', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': '26e93f1a-c828-11ea-87d0-0242ac130003_hot_water_cost', + 'unit_of_measurement': 'EUR', + }) +# --- +# name: test_setup[sensor.luxemburger_str_1_hot_water_cost-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'monetary', + 'friendly_name': 'Luxemburger Str. 1 Hot water cost', + 'state_class': , + 'unit_of_measurement': 'EUR', + }), + 'context': , + 'entity_id': 'sensor.luxemburger_str_1_hot_water_cost', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '7', + }) +# --- +# name: test_setup[sensor.luxemburger_str_1_hot_water_energy-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.luxemburger_str_1_hot_water_energy', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Hot water energy', + 'platform': 'ista_ecotrend', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': '26e93f1a-c828-11ea-87d0-0242ac130003_hot_water_energy', + 'unit_of_measurement': , + }) +# --- +# name: test_setup[sensor.luxemburger_str_1_hot_water_energy-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Luxemburger Str. 1 Hot water energy', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.luxemburger_str_1_hot_water_energy', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '57.0', + }) +# --- +# name: test_setup[sensor.luxemburger_str_1_water-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.luxemburger_str_1_water', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Water', + 'platform': 'ista_ecotrend', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': '26e93f1a-c828-11ea-87d0-0242ac130003_water', + 'unit_of_measurement': , + }) +# --- +# name: test_setup[sensor.luxemburger_str_1_water-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'water', + 'friendly_name': 'Luxemburger Str. 1 Water', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.luxemburger_str_1_water', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '5.0', + }) +# --- +# name: test_setup[sensor.luxemburger_str_1_water_cost-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.luxemburger_str_1_water_cost', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Water cost', + 'platform': 'ista_ecotrend', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': '26e93f1a-c828-11ea-87d0-0242ac130003_water_cost', + 'unit_of_measurement': 'EUR', + }) +# --- +# name: test_setup[sensor.luxemburger_str_1_water_cost-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'monetary', + 'friendly_name': 'Luxemburger Str. 1 Water cost', + 'state_class': , + 'unit_of_measurement': 'EUR', + }), + 'context': , + 'entity_id': 'sensor.luxemburger_str_1_water_cost', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '3', + }) +# --- diff --git a/tests/components/ista_ecotrend/snapshots/test_util.ambr b/tests/components/ista_ecotrend/snapshots/test_util.ambr new file mode 100644 index 00000000000..9536c5336db --- /dev/null +++ b/tests/components/ista_ecotrend/snapshots/test_util.ambr @@ -0,0 +1,175 @@ +# serializer version: 1 +# name: test_get_statistics + list([ + dict({ + 'date': datetime.datetime(2024, 4, 30, 0, 0, tzinfo=datetime.timezone.utc), + 'value': 104, + }), + dict({ + 'date': datetime.datetime(2024, 5, 31, 0, 0, tzinfo=datetime.timezone.utc), + 'value': 35, + }), + ]) +# --- +# name: test_get_statistics.1 + list([ + dict({ + 'date': datetime.datetime(2024, 4, 30, 0, 0, tzinfo=datetime.timezone.utc), + 'value': 113.0, + }), + dict({ + 'date': datetime.datetime(2024, 5, 31, 0, 0, tzinfo=datetime.timezone.utc), + 'value': 38.0, + }), + ]) +# --- +# name: test_get_statistics.2 + list([ + dict({ + 'date': datetime.datetime(2024, 4, 30, 0, 0, tzinfo=datetime.timezone.utc), + 'value': 62, + }), + dict({ + 'date': datetime.datetime(2024, 5, 31, 0, 0, tzinfo=datetime.timezone.utc), + 'value': 21, + }), + ]) +# --- +# name: test_get_statistics.3 + list([ + dict({ + 'date': datetime.datetime(2024, 4, 30, 0, 0, tzinfo=datetime.timezone.utc), + 'value': 1.1, + }), + dict({ + 'date': datetime.datetime(2024, 5, 31, 0, 0, tzinfo=datetime.timezone.utc), + 'value': 1.0, + }), + ]) +# --- +# name: test_get_statistics.4 + list([ + dict({ + 'date': datetime.datetime(2024, 4, 30, 0, 0, tzinfo=datetime.timezone.utc), + 'value': 61.1, + }), + dict({ + 'date': datetime.datetime(2024, 5, 31, 0, 0, tzinfo=datetime.timezone.utc), + 'value': 57.0, + }), + ]) +# --- +# name: test_get_statistics.5 + list([ + dict({ + 'date': datetime.datetime(2024, 4, 30, 0, 0, tzinfo=datetime.timezone.utc), + 'value': 7, + }), + dict({ + 'date': datetime.datetime(2024, 5, 31, 0, 0, tzinfo=datetime.timezone.utc), + 'value': 7, + }), + ]) +# --- +# name: test_get_statistics.6 + list([ + dict({ + 'date': datetime.datetime(2024, 4, 30, 0, 0, tzinfo=datetime.timezone.utc), + 'value': 6.8, + }), + dict({ + 'date': datetime.datetime(2024, 5, 31, 0, 0, tzinfo=datetime.timezone.utc), + 'value': 5.0, + }), + ]) +# --- +# name: test_get_statistics.7 + list([ + ]) +# --- +# name: test_get_statistics.8 + list([ + dict({ + 'date': datetime.datetime(2024, 4, 30, 0, 0, tzinfo=datetime.timezone.utc), + 'value': 2, + }), + dict({ + 'date': datetime.datetime(2024, 5, 31, 0, 0, tzinfo=datetime.timezone.utc), + 'value': 3, + }), + ]) +# --- +# name: test_get_values_by_type + dict({ + 'additionalValue': '38,0', + 'type': 'heating', + 'value': '35', + }) +# --- +# name: test_get_values_by_type.1 + dict({ + 'additionalValue': '57,0', + 'type': 'warmwater', + 'value': '1,0', + }) +# --- +# name: test_get_values_by_type.2 + dict({ + 'type': 'water', + 'value': '5,0', + }) +# --- +# name: test_get_values_by_type.3 + dict({ + 'type': 'heating', + 'value': 21, + }) +# --- +# name: test_get_values_by_type.4 + dict({ + 'type': 'warmwater', + 'value': 7, + }) +# --- +# name: test_get_values_by_type.5 + dict({ + 'type': 'water', + 'value': 3, + }) +# --- +# name: test_last_day_of_month + datetime.datetime(2024, 1, 31, 0, 0, tzinfo=datetime.timezone.utc) +# --- +# name: test_last_day_of_month.1 + datetime.datetime(2024, 2, 29, 0, 0, tzinfo=datetime.timezone.utc) +# --- +# name: test_last_day_of_month.10 + datetime.datetime(2024, 11, 30, 0, 0, tzinfo=datetime.timezone.utc) +# --- +# name: test_last_day_of_month.11 + datetime.datetime(2024, 12, 31, 0, 0, tzinfo=datetime.timezone.utc) +# --- +# name: test_last_day_of_month.2 + datetime.datetime(2024, 3, 31, 0, 0, tzinfo=datetime.timezone.utc) +# --- +# name: test_last_day_of_month.3 + datetime.datetime(2024, 4, 30, 0, 0, tzinfo=datetime.timezone.utc) +# --- +# name: test_last_day_of_month.4 + datetime.datetime(2024, 5, 31, 0, 0, tzinfo=datetime.timezone.utc) +# --- +# name: test_last_day_of_month.5 + datetime.datetime(2024, 6, 30, 0, 0, tzinfo=datetime.timezone.utc) +# --- +# name: test_last_day_of_month.6 + datetime.datetime(2024, 7, 31, 0, 0, tzinfo=datetime.timezone.utc) +# --- +# name: test_last_day_of_month.7 + datetime.datetime(2024, 8, 31, 0, 0, tzinfo=datetime.timezone.utc) +# --- +# name: test_last_day_of_month.8 + datetime.datetime(2024, 9, 30, 0, 0, tzinfo=datetime.timezone.utc) +# --- +# name: test_last_day_of_month.9 + datetime.datetime(2024, 10, 31, 0, 0, tzinfo=datetime.timezone.utc) +# --- diff --git a/tests/components/ista_ecotrend/test_config_flow.py b/tests/components/ista_ecotrend/test_config_flow.py new file mode 100644 index 00000000000..3ff192c85ac --- /dev/null +++ b/tests/components/ista_ecotrend/test_config_flow.py @@ -0,0 +1,90 @@ +"""Test the ista Ecotrend config flow.""" + +from unittest.mock import AsyncMock, MagicMock + +from pyecotrend_ista.exception_classes import LoginError, ServerError +import pytest + +from homeassistant.components.ista_ecotrend.const import DOMAIN +from homeassistant.config_entries import SOURCE_USER +from homeassistant.const import CONF_EMAIL, CONF_PASSWORD +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + + +async def test_form( + hass: HomeAssistant, mock_setup_entry: AsyncMock, mock_ista: MagicMock +) -> None: + """Test we get the form.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_EMAIL: "test@example.com", + CONF_PASSWORD: "test-password", + }, + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "Max Istamann" + assert result["data"] == { + CONF_EMAIL: "test@example.com", + CONF_PASSWORD: "test-password", + } + assert len(mock_setup_entry.mock_calls) == 1 + + +@pytest.mark.parametrize( + ("side_effect", "error_text"), + [ + (LoginError(None), "invalid_auth"), + (ServerError, "cannot_connect"), + (IndexError, "unknown"), + ], +) +async def test_form_invalid_auth( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_ista: MagicMock, + side_effect: Exception, + error_text: str, +) -> None: + """Test we handle invalid auth.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + mock_ista.login.side_effect = side_effect + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_EMAIL: "test@example.com", + CONF_PASSWORD: "test-password", + }, + ) + + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"base": error_text} + + mock_ista.login.side_effect = None + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_EMAIL: "test@example.com", + CONF_PASSWORD: "test-password", + }, + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "Max Istamann" + assert result["data"] == { + CONF_EMAIL: "test@example.com", + CONF_PASSWORD: "test-password", + } + assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/ista_ecotrend/test_init.py b/tests/components/ista_ecotrend/test_init.py new file mode 100644 index 00000000000..11a770d9ec7 --- /dev/null +++ b/tests/components/ista_ecotrend/test_init.py @@ -0,0 +1,99 @@ +"""Test the ista Ecotrend init.""" + +from unittest.mock import MagicMock + +from pyecotrend_ista.exception_classes import ( + InternalServerError, + KeycloakError, + LoginError, + ServerError, +) +import pytest +from requests.exceptions import RequestException +from syrupy.assertion import SnapshotAssertion + +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr + +from tests.common import MockConfigEntry + + +async def test_entry_setup_unload( + hass: HomeAssistant, ista_config_entry: MockConfigEntry, mock_ista: MagicMock +) -> None: + """Test integration setup and unload.""" + + ista_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(ista_config_entry.entry_id) + await hass.async_block_till_done() + + assert ista_config_entry.state is ConfigEntryState.LOADED + + await hass.config_entries.async_unload(ista_config_entry.entry_id) + await hass.async_block_till_done() + + assert ista_config_entry.state is ConfigEntryState.NOT_LOADED + + +@pytest.mark.parametrize( + ("side_effect"), + [ + ServerError, + InternalServerError(None), + RequestException, + TimeoutError, + ], +) +async def test_config_entry_not_ready( + hass: HomeAssistant, + ista_config_entry: MockConfigEntry, + mock_ista: MagicMock, + side_effect: Exception, +) -> None: + """Test config entry not ready.""" + mock_ista.login.side_effect = side_effect + ista_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(ista_config_entry.entry_id) + await hass.async_block_till_done() + + assert ista_config_entry.state is ConfigEntryState.SETUP_RETRY + + +@pytest.mark.parametrize( + ("side_effect"), + [LoginError(None), KeycloakError], +) +async def test_config_entry_error( + hass: HomeAssistant, + ista_config_entry: MockConfigEntry, + mock_ista: MagicMock, + side_effect: Exception, +) -> None: + """Test config entry not ready.""" + mock_ista.login.side_effect = side_effect + ista_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(ista_config_entry.entry_id) + await hass.async_block_till_done() + + assert ista_config_entry.state is ConfigEntryState.SETUP_ERROR + + +async def test_device_registry( + hass: HomeAssistant, + ista_config_entry: MockConfigEntry, + mock_ista: MagicMock, + device_registry: dr.DeviceRegistry, + snapshot: SnapshotAssertion, +) -> None: + """Test device registry.""" + ista_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(ista_config_entry.entry_id) + await hass.async_block_till_done() + + assert ista_config_entry.state is ConfigEntryState.LOADED + + for device in dr.async_entries_for_config_entry( + device_registry, ista_config_entry.entry_id + ): + assert device == snapshot diff --git a/tests/components/ista_ecotrend/test_sensor.py b/tests/components/ista_ecotrend/test_sensor.py new file mode 100644 index 00000000000..ca109455885 --- /dev/null +++ b/tests/components/ista_ecotrend/test_sensor.py @@ -0,0 +1,31 @@ +"""Tests for the ista EcoTrend Sensors.""" + +from unittest.mock import MagicMock + +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from tests.common import MockConfigEntry, snapshot_platform + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_setup( + hass: HomeAssistant, + ista_config_entry: MockConfigEntry, + mock_ista: MagicMock, + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, +) -> None: + """Test setup of ista EcoTrend sensor platform.""" + + ista_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(ista_config_entry.entry_id) + await hass.async_block_till_done() + + assert ista_config_entry.state is ConfigEntryState.LOADED + + await snapshot_platform(hass, entity_registry, snapshot, ista_config_entry.entry_id) diff --git a/tests/components/ista_ecotrend/test_util.py b/tests/components/ista_ecotrend/test_util.py new file mode 100644 index 00000000000..e2e799aa78b --- /dev/null +++ b/tests/components/ista_ecotrend/test_util.py @@ -0,0 +1,146 @@ +"""Tests for the ista EcoTrend utility functions.""" + +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.ista_ecotrend.util import ( + IstaConsumptionType, + IstaValueType, + as_number, + get_native_value, + get_statistics, + get_values_by_type, + last_day_of_month, +) + +from .conftest import get_raw + + +def test_as_number() -> None: + """Test as_number formatting function.""" + assert as_number("10") == 10 + assert isinstance(as_number("10"), int) + + assert as_number("9,5") == 9.5 + assert isinstance(as_number("9,5"), float) + + assert as_number(None) is None + assert isinstance(as_number(10.0), float) + + +def test_last_day_of_month(snapshot: SnapshotAssertion) -> None: + """Test determining last day of month.""" + + for month in range(12): + assert last_day_of_month(month=month + 1, year=2024) == snapshot + + +def test_get_values_by_type(snapshot: SnapshotAssertion) -> None: + """Test get_values_by_type function.""" + consumptions = { + "readings": [ + { + "type": "heating", + "value": "35", + "additionalValue": "38,0", + }, + { + "type": "warmwater", + "value": "1,0", + "additionalValue": "57,0", + }, + { + "type": "water", + "value": "5,0", + }, + ], + } + + assert get_values_by_type(consumptions, IstaConsumptionType.HEATING) == snapshot + assert get_values_by_type(consumptions, IstaConsumptionType.HOT_WATER) == snapshot + assert get_values_by_type(consumptions, IstaConsumptionType.WATER) == snapshot + + costs = { + "costsByEnergyType": [ + { + "type": "heating", + "value": 21, + }, + { + "type": "warmwater", + "value": 7, + }, + { + "type": "water", + "value": 3, + }, + ], + } + + assert get_values_by_type(costs, IstaConsumptionType.HEATING) == snapshot + assert get_values_by_type(costs, IstaConsumptionType.HOT_WATER) == snapshot + assert get_values_by_type(costs, IstaConsumptionType.WATER) == snapshot + + assert get_values_by_type({}, IstaConsumptionType.HEATING) == {} + assert get_values_by_type({"readings": []}, IstaConsumptionType.HEATING) == {} + + +def test_get_native_value() -> None: + """Test getting native value for sensor states.""" + test_data = get_raw("26e93f1a-c828-11ea-87d0-0242ac130003") + + assert get_native_value(test_data, IstaConsumptionType.HEATING) == 35 + assert get_native_value(test_data, IstaConsumptionType.HOT_WATER) == 1.0 + assert get_native_value(test_data, IstaConsumptionType.WATER) == 5.0 + + assert ( + get_native_value(test_data, IstaConsumptionType.HEATING, IstaValueType.COSTS) + == 21 + ) + assert ( + get_native_value(test_data, IstaConsumptionType.HOT_WATER, IstaValueType.COSTS) + == 7 + ) + assert ( + get_native_value(test_data, IstaConsumptionType.WATER, IstaValueType.COSTS) == 3 + ) + + assert ( + get_native_value(test_data, IstaConsumptionType.HEATING, IstaValueType.ENERGY) + == 38.0 + ) + assert ( + get_native_value(test_data, IstaConsumptionType.HOT_WATER, IstaValueType.ENERGY) + == 57.0 + ) + + no_data = {"consumptions": None, "costs": None} + assert get_native_value(no_data, IstaConsumptionType.HEATING) is None + assert ( + get_native_value(no_data, IstaConsumptionType.HEATING, IstaValueType.COSTS) + is None + ) + + +def test_get_statistics(snapshot: SnapshotAssertion) -> None: + """Test get_statistics function.""" + test_data = get_raw("26e93f1a-c828-11ea-87d0-0242ac130003") + for consumption_type in IstaConsumptionType: + assert get_statistics(test_data, consumption_type) == snapshot + assert get_statistics({"consumptions": None}, consumption_type) is None + assert ( + get_statistics(test_data, consumption_type, IstaValueType.ENERGY) + == snapshot + ) + assert ( + get_statistics( + {"consumptions": None}, consumption_type, IstaValueType.ENERGY + ) + is None + ) + assert ( + get_statistics(test_data, consumption_type, IstaValueType.COSTS) == snapshot + ) + assert ( + get_statistics({"costs": None}, consumption_type, IstaValueType.COSTS) + is None + ) From 42414d55e032ae8561bc62a77c8a9c4f19c4d122 Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Tue, 4 Jun 2024 09:50:43 +0100 Subject: [PATCH 0194/1445] Azure DevOps build sensor attributes to new sensors (#114948) * Setup for split * Adjust to allow for None * Create * Add missing * Fix datetime parsing in Azure DevOps sensor * Remove definition id and name These aren't needed and will never change * Add tests for each sensor * Add tests for edge cases * Rename translations * Update * Use base sensor descriptions * Remove * Drop status using this later for an event entity * Switch to timestamp * Switch to timestamp * Merge * Update snapshot * Improvements from @joostlek * Update homeassistant/components/azure_devops/sensor.py --------- Co-authored-by: Joost Lekkerkerker --- .../components/azure_devops/__init__.py | 38 +- .../components/azure_devops/sensor.py | 203 ++- .../components/azure_devops/strings.json | 27 + tests/components/azure_devops/__init__.py | 12 +- .../azure_devops/snapshots/test_sensor.ambr | 1321 +++++++++++++++++ tests/components/azure_devops/test_init.py | 2 +- tests/components/azure_devops/test_sensor.py | 72 +- 7 files changed, 1581 insertions(+), 94 deletions(-) diff --git a/homeassistant/components/azure_devops/__init__.py b/homeassistant/components/azure_devops/__init__.py index 537019fb9c1..27f7f790637 100644 --- a/homeassistant/components/azure_devops/__init__.py +++ b/homeassistant/components/azure_devops/__init__.py @@ -2,14 +2,12 @@ from __future__ import annotations -from dataclasses import dataclass from datetime import timedelta import logging from typing import Final from aioazuredevops.builds import DevOpsBuild from aioazuredevops.client import DevOpsClient -from aioazuredevops.core import DevOpsProject import aiohttp from homeassistant.config_entries import ConfigEntry @@ -18,7 +16,6 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo -from homeassistant.helpers.entity import EntityDescription from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -34,14 +31,6 @@ PLATFORMS = [Platform.SENSOR] BUILDS_QUERY: Final = "?queryOrder=queueTimeDescending&maxBuildsPerDefinition=1" -@dataclass(frozen=True) -class AzureDevOpsEntityDescription(EntityDescription): - """Class describing Azure DevOps entities.""" - - organization: str = "" - project: DevOpsProject = None - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Azure DevOps from a config entry.""" aiohttp_session = async_get_clientsession(hass) @@ -108,32 +97,17 @@ class AzureDevOpsEntity(CoordinatorEntity[DataUpdateCoordinator[list[DevOpsBuild _attr_has_entity_name = True - entity_description: AzureDevOpsEntityDescription - def __init__( self, coordinator: DataUpdateCoordinator[list[DevOpsBuild]], - entity_description: AzureDevOpsEntityDescription, + organization: str, + project_name: str, ) -> None: """Initialize the Azure DevOps entity.""" super().__init__(coordinator) - self.entity_description = entity_description - self._attr_unique_id: str = ( - f"{entity_description.organization}_{entity_description.key}" - ) - self._organization: str = entity_description.organization - self._project_name: str = entity_description.project.name - - -class AzureDevOpsDeviceEntity(AzureDevOpsEntity): - """Defines a Azure DevOps device entity.""" - - @property - def device_info(self) -> DeviceInfo: - """Return device information about this Azure DevOps instance.""" - return DeviceInfo( + self._attr_device_info = DeviceInfo( entry_type=DeviceEntryType.SERVICE, - identifiers={(DOMAIN, self._organization, self._project_name)}, # type: ignore[arg-type] - manufacturer=self._organization, - name=self._project_name, + identifiers={(DOMAIN, organization, project_name)}, # type: ignore[arg-type] + manufacturer=organization, + name=project_name, ) diff --git a/homeassistant/components/azure_devops/sensor.py b/homeassistant/components/azure_devops/sensor.py index 514db5462e9..b1d975f0a70 100644 --- a/homeassistant/components/azure_devops/sensor.py +++ b/homeassistant/components/azure_devops/sensor.py @@ -2,89 +2,186 @@ from __future__ import annotations -from collections.abc import Callable +from collections.abc import Callable, Mapping from dataclasses import dataclass +from datetime import datetime +import logging from typing import Any from aioazuredevops.builds import DevOpsBuild -from homeassistant.components.sensor import SensorEntity, SensorEntityDescription +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, +) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator +from homeassistant.util import dt as dt_util -from . import AzureDevOpsDeviceEntity, AzureDevOpsEntityDescription +from . import AzureDevOpsEntity from .const import CONF_ORG, DOMAIN +_LOGGER = logging.getLogger(__name__) + @dataclass(frozen=True, kw_only=True) -class AzureDevOpsSensorEntityDescription( - AzureDevOpsEntityDescription, SensorEntityDescription -): - """Class describing Azure DevOps sensor entities.""" +class AzureDevOpsBuildSensorEntityDescription(SensorEntityDescription): + """Class describing Azure DevOps base build sensor entities.""" - build_key: int - attrs: Callable[[DevOpsBuild], Any] - value: Callable[[DevOpsBuild], StateType] + attr_fn: Callable[[DevOpsBuild], dict[str, Any] | None] = lambda _: None + value_fn: Callable[[DevOpsBuild], datetime | StateType] + + +BASE_BUILD_SENSOR_DESCRIPTIONS: tuple[AzureDevOpsBuildSensorEntityDescription, ...] = ( + # Attributes are deprecated in 2024.7 and can be removed in 2025.1 + AzureDevOpsBuildSensorEntityDescription( + key="latest_build", + translation_key="latest_build", + attr_fn=lambda build: { + "definition_id": (build.definition.build_id if build.definition else None), + "definition_name": (build.definition.name if build.definition else None), + "id": build.build_id, + "reason": build.reason, + "result": build.result, + "source_branch": build.source_branch, + "source_version": build.source_version, + "status": build.status, + "url": build.links.web if build.links else None, + "queue_time": build.queue_time, + "start_time": build.start_time, + "finish_time": build.finish_time, + }, + value_fn=lambda build: build.build_number, + ), + AzureDevOpsBuildSensorEntityDescription( + key="build_id", + translation_key="build_id", + entity_registry_visible_default=False, + value_fn=lambda build: build.build_id, + ), + AzureDevOpsBuildSensorEntityDescription( + key="reason", + translation_key="reason", + entity_registry_visible_default=False, + value_fn=lambda build: build.reason, + ), + AzureDevOpsBuildSensorEntityDescription( + key="result", + translation_key="result", + entity_registry_visible_default=False, + value_fn=lambda build: build.result, + ), + AzureDevOpsBuildSensorEntityDescription( + key="source_branch", + translation_key="source_branch", + entity_registry_enabled_default=False, + entity_registry_visible_default=False, + value_fn=lambda build: build.source_branch, + ), + AzureDevOpsBuildSensorEntityDescription( + key="source_version", + translation_key="source_version", + entity_registry_visible_default=False, + value_fn=lambda build: build.source_version, + ), + AzureDevOpsBuildSensorEntityDescription( + key="queue_time", + translation_key="queue_time", + device_class=SensorDeviceClass.TIMESTAMP, + entity_registry_enabled_default=False, + entity_registry_visible_default=False, + value_fn=lambda build: parse_datetime(build.queue_time), + ), + AzureDevOpsBuildSensorEntityDescription( + key="start_time", + translation_key="start_time", + device_class=SensorDeviceClass.TIMESTAMP, + entity_registry_visible_default=False, + value_fn=lambda build: parse_datetime(build.start_time), + ), + AzureDevOpsBuildSensorEntityDescription( + key="finish_time", + translation_key="finish_time", + device_class=SensorDeviceClass.TIMESTAMP, + entity_registry_visible_default=False, + value_fn=lambda build: parse_datetime(build.finish_time), + ), + AzureDevOpsBuildSensorEntityDescription( + key="url", + translation_key="url", + value_fn=lambda build: build.links.web if build.links else None, + ), +) + + +def parse_datetime(value: str | None) -> datetime | None: + """Parse datetime string.""" + if value is None: + return None + + return dt_util.parse_datetime(value) async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up Azure DevOps sensor based on a config entry.""" coordinator, project = hass.data[DOMAIN][entry.entry_id] + initial_builds: list[DevOpsBuild] = coordinator.data - sensors = [ - AzureDevOpsSensor( + async_add_entities( + AzureDevOpsBuildSensor( coordinator, - AzureDevOpsSensorEntityDescription( - key=f"{build.project.project_id}_{build.definition.build_id}_latest_build", - translation_key="latest_build", - translation_placeholders={"definition_name": build.definition.name}, - attrs=lambda build: { - "definition_id": ( - build.definition.build_id if build.definition else None - ), - "definition_name": ( - build.definition.name if build.definition else None - ), - "id": build.build_id, - "reason": build.reason, - "result": build.result, - "source_branch": build.source_branch, - "source_version": build.source_version, - "status": build.status, - "url": build.links.web if build.links else None, - "queue_time": build.queue_time, - "start_time": build.start_time, - "finish_time": build.finish_time, - }, - build_key=key, - organization=entry.data[CONF_ORG], - project=project, - value=lambda build: build.build_number, - ), + description, + entry.data[CONF_ORG], + project.name, + key, ) - for key, build in enumerate(coordinator.data) - ] - - async_add_entities(sensors, True) + for description in BASE_BUILD_SENSOR_DESCRIPTIONS + for key, build in enumerate(initial_builds) + if build.project and build.definition + ) -class AzureDevOpsSensor(AzureDevOpsDeviceEntity, SensorEntity): - """Define a Azure DevOps sensor.""" +class AzureDevOpsBuildSensor(AzureDevOpsEntity, SensorEntity): + """Define a Azure DevOps build sensor.""" - entity_description: AzureDevOpsSensorEntityDescription + entity_description: AzureDevOpsBuildSensorEntityDescription + + def __init__( + self, + coordinator: DataUpdateCoordinator[list[DevOpsBuild]], + description: AzureDevOpsBuildSensorEntityDescription, + organization: str, + project_name: str, + item_key: int, + ) -> None: + """Initialize.""" + super().__init__(coordinator, organization, project_name) + self.entity_description = description + self.item_key = item_key + self._attr_unique_id = f"{organization}_{self.build.project.project_id}_{self.build.definition.build_id}_{description.key}" + self._attr_translation_placeholders = { + "definition_name": self.build.definition.name + } @property - def native_value(self) -> StateType: + def build(self) -> DevOpsBuild: + """Return the build.""" + return self.coordinator.data[self.item_key] + + @property + def native_value(self) -> datetime | StateType: """Return the state.""" - build: DevOpsBuild = self.coordinator.data[self.entity_description.build_key] - return self.entity_description.value(build) + return self.entity_description.value_fn(self.build) @property - def extra_state_attributes(self) -> dict[str, Any]: + def extra_state_attributes(self) -> Mapping[str, Any] | None: """Return the state attributes of the entity.""" - build: DevOpsBuild = self.coordinator.data[self.entity_description.build_key] - return self.entity_description.attrs(build) + return self.entity_description.attr_fn(self.build) diff --git a/homeassistant/components/azure_devops/strings.json b/homeassistant/components/azure_devops/strings.json index c163aee5b7f..7bd6d8af561 100644 --- a/homeassistant/components/azure_devops/strings.json +++ b/homeassistant/components/azure_devops/strings.json @@ -31,8 +31,35 @@ }, "entity": { "sensor": { + "build_id": { + "name": "{definition_name} latest build id" + }, + "finish_time": { + "name": "{definition_name} latest build finish time" + }, "latest_build": { "name": "{definition_name} latest build" + }, + "queue_time": { + "name": "{definition_name} latest build queue time" + }, + "reason": { + "name": "{definition_name} latest build reason" + }, + "result": { + "name": "{definition_name} latest build result" + }, + "source_branch": { + "name": "{definition_name} latest build source branch" + }, + "source_version": { + "name": "{definition_name} latest build source version" + }, + "start_time": { + "name": "{definition_name} latest build start time" + }, + "url": { + "name": "{definition_name} latest build url" } } } diff --git a/tests/components/azure_devops/__init__.py b/tests/components/azure_devops/__init__.py index fb0817671b5..7c540cd3c6d 100644 --- a/tests/components/azure_devops/__init__.py +++ b/tests/components/azure_devops/__init__.py @@ -43,7 +43,7 @@ DEVOPS_PROJECT = DevOpsProject( DEVOPS_BUILD_DEFINITION = DevOpsBuildDefinition( build_id=9876, - name="Test Build", + name="CI", url=f"https://dev.azure.com/{ORGANIZATION}/{PROJECT}/_apis/build/definitions/1", path="", build_type="build", @@ -68,6 +68,16 @@ DEVOPS_BUILD = DevOpsBuild( links=None, ) +DEVOPS_BUILD_MISSING_DATA = DevOpsBuild( + build_id=6789, + definition=DEVOPS_BUILD_DEFINITION, + project=DEVOPS_PROJECT, +) + +DEVOPS_BUILD_MISSING_PROJECT_DEFINITION = DevOpsBuild( + build_id=9876, +) + async def setup_integration( hass: HomeAssistant, diff --git a/tests/components/azure_devops/snapshots/test_sensor.ambr b/tests/components/azure_devops/snapshots/test_sensor.ambr index b99d2c4e49d..0ce82cae1e8 100644 --- a/tests/components/azure_devops/snapshots/test_sensor.ambr +++ b/tests/components/azure_devops/snapshots/test_sensor.ambr @@ -1,4 +1,1034 @@ # serializer version: 1 +# name: test_sensors[sensor.testproject_ci_build_finish_time-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.testproject_ci_build_finish_time', + 'has_entity_name': True, + 'hidden_by': , + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'CI build finish time', + 'platform': 'azure_devops', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'finish_time', + 'unique_id': 'testorg_1234_9876_finish_time', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.testproject_ci_build_finish_time-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'date', + 'friendly_name': 'testproject CI build finish time', + }), + 'context': , + 'entity_id': 'sensor.testproject_ci_build_finish_time', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2021-01-01T00:00:00+00:00', + }) +# --- +# name: test_sensors[sensor.testproject_ci_build_id-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.testproject_ci_build_id', + 'has_entity_name': True, + 'hidden_by': , + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'CI build id', + 'platform': 'azure_devops', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'build_id', + 'unique_id': 'testorg_1234_9876_build_id', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.testproject_ci_build_id-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'testproject CI build id', + }), + 'context': , + 'entity_id': 'sensor.testproject_ci_build_id', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '5678', + }) +# --- +# name: test_sensors[sensor.testproject_ci_build_queue_time-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.testproject_ci_build_queue_time', + 'has_entity_name': True, + 'hidden_by': , + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'CI build queue time', + 'platform': 'azure_devops', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'queue_time', + 'unique_id': 'testorg_1234_9876_queue_time', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.testproject_ci_build_queue_time-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'date', + 'friendly_name': 'testproject CI build queue time', + }), + 'context': , + 'entity_id': 'sensor.testproject_ci_build_queue_time', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2021-01-01T00:00:00+00:00', + }) +# --- +# name: test_sensors[sensor.testproject_ci_build_reason-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.testproject_ci_build_reason', + 'has_entity_name': True, + 'hidden_by': , + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'CI build reason', + 'platform': 'azure_devops', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'reason', + 'unique_id': 'testorg_1234_9876_reason', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.testproject_ci_build_reason-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'testproject CI build reason', + }), + 'context': , + 'entity_id': 'sensor.testproject_ci_build_reason', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'manual', + }) +# --- +# name: test_sensors[sensor.testproject_ci_build_result-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.testproject_ci_build_result', + 'has_entity_name': True, + 'hidden_by': , + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'CI build result', + 'platform': 'azure_devops', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'result', + 'unique_id': 'testorg_1234_9876_result', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.testproject_ci_build_result-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'testproject CI build result', + }), + 'context': , + 'entity_id': 'sensor.testproject_ci_build_result', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'succeeded', + }) +# --- +# name: test_sensors[sensor.testproject_ci_build_source_branch-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.testproject_ci_build_source_branch', + 'has_entity_name': True, + 'hidden_by': , + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'CI build source branch', + 'platform': 'azure_devops', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'source_branch', + 'unique_id': 'testorg_1234_9876_source_branch', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.testproject_ci_build_source_branch-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'testproject CI build source branch', + }), + 'context': , + 'entity_id': 'sensor.testproject_ci_build_source_branch', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'main', + }) +# --- +# name: test_sensors[sensor.testproject_ci_build_source_version-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.testproject_ci_build_source_version', + 'has_entity_name': True, + 'hidden_by': , + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'CI build source version', + 'platform': 'azure_devops', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'source_version', + 'unique_id': 'testorg_1234_9876_source_version', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.testproject_ci_build_source_version-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'testproject CI build source version', + }), + 'context': , + 'entity_id': 'sensor.testproject_ci_build_source_version', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '123', + }) +# --- +# name: test_sensors[sensor.testproject_ci_build_start_time-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.testproject_ci_build_start_time', + 'has_entity_name': True, + 'hidden_by': , + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'CI build start time', + 'platform': 'azure_devops', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'start_time', + 'unique_id': 'testorg_1234_9876_start_time', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.testproject_ci_build_start_time-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'date', + 'friendly_name': 'testproject CI build start time', + }), + 'context': , + 'entity_id': 'sensor.testproject_ci_build_start_time', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2021-01-01T00:00:00+00:00', + }) +# --- +# name: test_sensors[sensor.testproject_ci_build_status-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.testproject_ci_build_status', + 'has_entity_name': True, + 'hidden_by': , + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'CI build status', + 'platform': 'azure_devops', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'status', + 'unique_id': 'testorg_1234_9876_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.testproject_ci_build_status-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'testproject CI build status', + }), + 'context': , + 'entity_id': 'sensor.testproject_ci_build_status', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'completed', + }) +# --- +# name: test_sensors[sensor.testproject_ci_build_url-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.testproject_ci_build_url', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'CI build url', + 'platform': 'azure_devops', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'url', + 'unique_id': 'testorg_1234_9876_url', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.testproject_ci_build_url-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'testproject CI build url', + }), + 'context': , + 'entity_id': 'sensor.testproject_ci_build_url', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.testproject_ci_latest_build-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.testproject_ci_latest_build', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'CI latest build', + 'platform': 'azure_devops', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'latest_build', + 'unique_id': 'testorg_1234_9876_latest_build', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.testproject_ci_latest_build-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'definition_id': 9876, + 'definition_name': 'CI', + 'finish_time': '2021-01-01T00:00:00Z', + 'friendly_name': 'testproject CI latest build', + 'id': 5678, + 'queue_time': '2021-01-01T00:00:00Z', + 'reason': 'manual', + 'result': 'succeeded', + 'source_branch': 'main', + 'source_version': '123', + 'start_time': '2021-01-01T00:00:00Z', + 'status': 'completed', + 'url': None, + }), + 'context': , + 'entity_id': 'sensor.testproject_ci_latest_build', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1', + }) +# --- +# name: test_sensors[sensor.testproject_ci_latest_build_finish_time-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.testproject_ci_latest_build_finish_time', + 'has_entity_name': True, + 'hidden_by': , + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'CI latest build finish time', + 'platform': 'azure_devops', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'finish_time', + 'unique_id': 'testorg_1234_9876_finish_time', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.testproject_ci_latest_build_finish_time-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'timestamp', + 'friendly_name': 'testproject CI latest build finish time', + }), + 'context': , + 'entity_id': 'sensor.testproject_ci_latest_build_finish_time', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2021-01-01T00:00:00+00:00', + }) +# --- +# name: test_sensors[sensor.testproject_ci_latest_build_id-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.testproject_ci_latest_build_id', + 'has_entity_name': True, + 'hidden_by': , + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'CI latest build id', + 'platform': 'azure_devops', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'build_id', + 'unique_id': 'testorg_1234_9876_build_id', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.testproject_ci_latest_build_id-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'testproject CI latest build id', + }), + 'context': , + 'entity_id': 'sensor.testproject_ci_latest_build_id', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '5678', + }) +# --- +# name: test_sensors[sensor.testproject_ci_latest_build_queue_time-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.testproject_ci_latest_build_queue_time', + 'has_entity_name': True, + 'hidden_by': , + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'CI latest build queue time', + 'platform': 'azure_devops', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'queue_time', + 'unique_id': 'testorg_1234_9876_queue_time', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.testproject_ci_latest_build_queue_time-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'timestamp', + 'friendly_name': 'testproject CI latest build queue time', + }), + 'context': , + 'entity_id': 'sensor.testproject_ci_latest_build_queue_time', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2021-01-01T00:00:00+00:00', + }) +# --- +# name: test_sensors[sensor.testproject_ci_latest_build_reason-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.testproject_ci_latest_build_reason', + 'has_entity_name': True, + 'hidden_by': , + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'CI latest build reason', + 'platform': 'azure_devops', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'reason', + 'unique_id': 'testorg_1234_9876_reason', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.testproject_ci_latest_build_reason-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'testproject CI latest build reason', + }), + 'context': , + 'entity_id': 'sensor.testproject_ci_latest_build_reason', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'manual', + }) +# --- +# name: test_sensors[sensor.testproject_ci_latest_build_result-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.testproject_ci_latest_build_result', + 'has_entity_name': True, + 'hidden_by': , + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'CI latest build result', + 'platform': 'azure_devops', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'result', + 'unique_id': 'testorg_1234_9876_result', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.testproject_ci_latest_build_result-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'testproject CI latest build result', + }), + 'context': , + 'entity_id': 'sensor.testproject_ci_latest_build_result', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'succeeded', + }) +# --- +# name: test_sensors[sensor.testproject_ci_latest_build_source_branch-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.testproject_ci_latest_build_source_branch', + 'has_entity_name': True, + 'hidden_by': , + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'CI latest build source branch', + 'platform': 'azure_devops', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'source_branch', + 'unique_id': 'testorg_1234_9876_source_branch', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.testproject_ci_latest_build_source_branch-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'testproject CI latest build source branch', + }), + 'context': , + 'entity_id': 'sensor.testproject_ci_latest_build_source_branch', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'main', + }) +# --- +# name: test_sensors[sensor.testproject_ci_latest_build_source_version-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.testproject_ci_latest_build_source_version', + 'has_entity_name': True, + 'hidden_by': , + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'CI latest build source version', + 'platform': 'azure_devops', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'source_version', + 'unique_id': 'testorg_1234_9876_source_version', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.testproject_ci_latest_build_source_version-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'testproject CI latest build source version', + }), + 'context': , + 'entity_id': 'sensor.testproject_ci_latest_build_source_version', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '123', + }) +# --- +# name: test_sensors[sensor.testproject_ci_latest_build_start_time-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.testproject_ci_latest_build_start_time', + 'has_entity_name': True, + 'hidden_by': , + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'CI latest build start time', + 'platform': 'azure_devops', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'start_time', + 'unique_id': 'testorg_1234_9876_start_time', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.testproject_ci_latest_build_start_time-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'timestamp', + 'friendly_name': 'testproject CI latest build start time', + }), + 'context': , + 'entity_id': 'sensor.testproject_ci_latest_build_start_time', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2021-01-01T00:00:00+00:00', + }) +# --- +# name: test_sensors[sensor.testproject_ci_latest_build_status-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.testproject_ci_latest_build_status', + 'has_entity_name': True, + 'hidden_by': , + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'CI latest build status', + 'platform': 'azure_devops', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'status', + 'unique_id': 'testorg_1234_9876_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.testproject_ci_latest_build_status-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'testproject CI latest build status', + }), + 'context': , + 'entity_id': 'sensor.testproject_ci_latest_build_status', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'completed', + }) +# --- +# name: test_sensors[sensor.testproject_ci_latest_build_url-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.testproject_ci_latest_build_url', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'CI latest build url', + 'platform': 'azure_devops', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'url', + 'unique_id': 'testorg_1234_9876_url', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.testproject_ci_latest_build_url-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'testproject CI latest build url', + }), + 'context': , + 'entity_id': 'sensor.testproject_ci_latest_build_url', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.testproject_test_build_build_id-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.testproject_test_build_build_id', + 'has_entity_name': True, + 'hidden_by': , + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Test Build build id', + 'platform': 'azure_devops', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'build_id', + 'unique_id': 'testorg_1234_9876_build_id', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.testproject_test_build_build_id-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'testproject Test Build build id', + }), + 'context': , + 'entity_id': 'sensor.testproject_test_build_build_id', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '5678', + }) +# --- # name: test_sensors[sensor.testproject_test_build_latest_build-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -57,3 +1087,294 @@ 'state': '1', }) # --- +# name: test_sensors_missing_data[sensor.testproject_ci_build_finish_time-state-missing-data] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'date', + 'friendly_name': 'testproject CI build finish time', + }), + 'context': , + 'entity_id': 'sensor.testproject_ci_build_finish_time', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors_missing_data[sensor.testproject_ci_build_id-state-missing-data] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'testproject CI build id', + }), + 'context': , + 'entity_id': 'sensor.testproject_ci_build_id', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '6789', + }) +# --- +# name: test_sensors_missing_data[sensor.testproject_ci_build_queue_time-state-missing-data] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'date', + 'friendly_name': 'testproject CI build queue time', + }), + 'context': , + 'entity_id': 'sensor.testproject_ci_build_queue_time', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors_missing_data[sensor.testproject_ci_build_reason-state-missing-data] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'testproject CI build reason', + }), + 'context': , + 'entity_id': 'sensor.testproject_ci_build_reason', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors_missing_data[sensor.testproject_ci_build_result-state-missing-data] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'testproject CI build result', + }), + 'context': , + 'entity_id': 'sensor.testproject_ci_build_result', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors_missing_data[sensor.testproject_ci_build_source_branch-state-missing-data] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'testproject CI build source branch', + }), + 'context': , + 'entity_id': 'sensor.testproject_ci_build_source_branch', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors_missing_data[sensor.testproject_ci_build_source_version-state-missing-data] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'testproject CI build source version', + }), + 'context': , + 'entity_id': 'sensor.testproject_ci_build_source_version', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors_missing_data[sensor.testproject_ci_build_start_time-state-missing-data] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'date', + 'friendly_name': 'testproject CI build start time', + }), + 'context': , + 'entity_id': 'sensor.testproject_ci_build_start_time', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors_missing_data[sensor.testproject_ci_build_status-state-missing-data] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'testproject CI build status', + }), + 'context': , + 'entity_id': 'sensor.testproject_ci_build_status', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors_missing_data[sensor.testproject_ci_build_url-state-missing-data] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'testproject CI build url', + }), + 'context': , + 'entity_id': 'sensor.testproject_ci_build_url', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors_missing_data[sensor.testproject_ci_latest_build-state-missing-data] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'definition_id': 9876, + 'definition_name': 'CI', + 'finish_time': None, + 'friendly_name': 'testproject CI latest build', + 'id': 6789, + 'queue_time': None, + 'reason': None, + 'result': None, + 'source_branch': None, + 'source_version': None, + 'start_time': None, + 'status': None, + 'url': None, + }), + 'context': , + 'entity_id': 'sensor.testproject_ci_latest_build', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors_missing_data[sensor.testproject_ci_latest_build_finish_time-state-missing-data] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'timestamp', + 'friendly_name': 'testproject CI latest build finish time', + }), + 'context': , + 'entity_id': 'sensor.testproject_ci_latest_build_finish_time', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors_missing_data[sensor.testproject_ci_latest_build_id-state-missing-data] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'testproject CI latest build id', + }), + 'context': , + 'entity_id': 'sensor.testproject_ci_latest_build_id', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '6789', + }) +# --- +# name: test_sensors_missing_data[sensor.testproject_ci_latest_build_queue_time-state-missing-data] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'timestamp', + 'friendly_name': 'testproject CI latest build queue time', + }), + 'context': , + 'entity_id': 'sensor.testproject_ci_latest_build_queue_time', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors_missing_data[sensor.testproject_ci_latest_build_reason-state-missing-data] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'testproject CI latest build reason', + }), + 'context': , + 'entity_id': 'sensor.testproject_ci_latest_build_reason', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors_missing_data[sensor.testproject_ci_latest_build_result-state-missing-data] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'testproject CI latest build result', + }), + 'context': , + 'entity_id': 'sensor.testproject_ci_latest_build_result', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors_missing_data[sensor.testproject_ci_latest_build_source_branch-state-missing-data] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'testproject CI latest build source branch', + }), + 'context': , + 'entity_id': 'sensor.testproject_ci_latest_build_source_branch', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors_missing_data[sensor.testproject_ci_latest_build_source_version-state-missing-data] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'testproject CI latest build source version', + }), + 'context': , + 'entity_id': 'sensor.testproject_ci_latest_build_source_version', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors_missing_data[sensor.testproject_ci_latest_build_start_time-state-missing-data] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'timestamp', + 'friendly_name': 'testproject CI latest build start time', + }), + 'context': , + 'entity_id': 'sensor.testproject_ci_latest_build_start_time', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors_missing_data[sensor.testproject_ci_latest_build_status-state-missing-data] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'testproject CI latest build status', + }), + 'context': , + 'entity_id': 'sensor.testproject_ci_latest_build_status', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors_missing_data[sensor.testproject_ci_latest_build_url-state-missing-data] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'testproject CI latest build url', + }), + 'context': , + 'entity_id': 'sensor.testproject_ci_latest_build_url', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- diff --git a/tests/components/azure_devops/test_init.py b/tests/components/azure_devops/test_init.py index a35acb375ec..240edee82d7 100644 --- a/tests/components/azure_devops/test_init.py +++ b/tests/components/azure_devops/test_init.py @@ -22,7 +22,7 @@ async def test_load_unload_entry( assert mock_devops_client.authorized assert mock_devops_client.authorize.call_count == 1 - assert mock_devops_client.get_builds.call_count == 2 + assert mock_devops_client.get_builds.call_count == 1 assert mock_config_entry.state is ConfigEntryState.LOADED diff --git a/tests/components/azure_devops/test_sensor.py b/tests/components/azure_devops/test_sensor.py index 1c518d919c2..cb49c3d67cd 100644 --- a/tests/components/azure_devops/test_sensor.py +++ b/tests/components/azure_devops/test_sensor.py @@ -8,10 +8,28 @@ from syrupy.assertion import SnapshotAssertion from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from . import setup_integration +from . import ( + DEVOPS_BUILD_MISSING_DATA, + DEVOPS_BUILD_MISSING_PROJECT_DEFINITION, + setup_integration, +) from tests.common import MockConfigEntry +BASE_ENTITY_ID = "sensor.testproject_ci" +SENSOR_KEYS = [ + "latest_build", + "latest_build_id", + "latest_build_reason", + "latest_build_result", + "latest_build_source_branch", + "latest_build_source_version", + "latest_build_queue_time", + "latest_build_start_time", + "latest_build_finish_time", + "latest_build_url", +] + @pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_sensors( @@ -21,13 +39,53 @@ async def test_sensors( mock_config_entry: MockConfigEntry, mock_devops_client: AsyncMock, ) -> None: - """Test the sensor entities.""" + """Test sensor entities.""" assert await setup_integration(hass, mock_config_entry) - assert ( - entry := entity_registry.async_get("sensor.testproject_test_build_latest_build") - ) + for sensor_key in SENSOR_KEYS: + assert (entry := entity_registry.async_get(f"{BASE_ENTITY_ID}_{sensor_key}")) - assert entry == snapshot(name=f"{entry.entity_id}-entry") + assert entry == snapshot(name=f"{entry.entity_id}-entry") - assert hass.states.get(entry.entity_id) == snapshot(name=f"{entry.entity_id}-state") + assert hass.states.get(entry.entity_id) == snapshot( + name=f"{entry.entity_id}-state" + ) + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_sensors_missing_data( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, + mock_config_entry: MockConfigEntry, + mock_devops_client: AsyncMock, +) -> None: + """Test sensor entities with missing data.""" + mock_devops_client.get_builds.return_value = [DEVOPS_BUILD_MISSING_DATA] + + assert await setup_integration(hass, mock_config_entry) + + for sensor_key in SENSOR_KEYS: + assert (entry := entity_registry.async_get(f"{BASE_ENTITY_ID}_{sensor_key}")) + + assert hass.states.get(entry.entity_id) == snapshot( + name=f"{entry.entity_id}-state-missing-data" + ) + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_sensors_missing_project_definition( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + mock_config_entry: MockConfigEntry, + mock_devops_client: AsyncMock, +) -> None: + """Test sensor entities with missing project and definition.""" + mock_devops_client.get_builds.return_value = [ + DEVOPS_BUILD_MISSING_PROJECT_DEFINITION + ] + + assert await setup_integration(hass, mock_config_entry) + + for sensor_key in SENSOR_KEYS: + assert not entity_registry.async_get(f"{BASE_ENTITY_ID}_{sensor_key}") From e9f01be09031bca1cf278800386537f50eca4ced Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 4 Jun 2024 10:51:28 +0200 Subject: [PATCH 0195/1445] Add coordinator to Aladdin Connect (#118781) --- .../components/aladdin_connect/__init__.py | 12 ++- .../components/aladdin_connect/coordinator.py | 38 ++++++++++ .../components/aladdin_connect/cover.py | 73 +++++++------------ .../components/aladdin_connect/entity.py | 27 +++++++ .../components/aladdin_connect/sensor.py | 50 +++++-------- 5 files changed, 118 insertions(+), 82 deletions(-) create mode 100644 homeassistant/components/aladdin_connect/coordinator.py create mode 100644 homeassistant/components/aladdin_connect/entity.py diff --git a/homeassistant/components/aladdin_connect/__init__.py b/homeassistant/components/aladdin_connect/__init__.py index dcd26c6cd04..6317cf8358e 100644 --- a/homeassistant/components/aladdin_connect/__init__.py +++ b/homeassistant/components/aladdin_connect/__init__.py @@ -2,6 +2,8 @@ from __future__ import annotations +from genie_partner_sdk.client import AladdinConnectClient + from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant @@ -12,10 +14,11 @@ from homeassistant.helpers.config_entry_oauth2_flow import ( ) from .api import AsyncConfigEntryAuth +from .coordinator import AladdinConnectCoordinator PLATFORMS: list[Platform] = [Platform.COVER] -type AladdinConnectConfigEntry = ConfigEntry[AsyncConfigEntryAuth] +type AladdinConnectConfigEntry = ConfigEntry[AladdinConnectCoordinator] async def async_setup_entry( @@ -25,8 +28,13 @@ async def async_setup_entry( implementation = await async_get_config_entry_implementation(hass, entry) session = OAuth2Session(hass, entry, implementation) + auth = AsyncConfigEntryAuth(async_get_clientsession(hass), session) + coordinator = AladdinConnectCoordinator(hass, AladdinConnectClient(auth)) - entry.runtime_data = AsyncConfigEntryAuth(async_get_clientsession(hass), session) + await coordinator.async_setup() + await coordinator.async_config_entry_first_refresh() + + entry.runtime_data = coordinator await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) diff --git a/homeassistant/components/aladdin_connect/coordinator.py b/homeassistant/components/aladdin_connect/coordinator.py new file mode 100644 index 00000000000..d9af0da9450 --- /dev/null +++ b/homeassistant/components/aladdin_connect/coordinator.py @@ -0,0 +1,38 @@ +"""Define an object to coordinate fetching Aladdin Connect data.""" + +from datetime import timedelta +import logging + +from genie_partner_sdk.client import AladdinConnectClient +from genie_partner_sdk.model import GarageDoor + +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +class AladdinConnectCoordinator(DataUpdateCoordinator[None]): + """Aladdin Connect Data Update Coordinator.""" + + def __init__(self, hass: HomeAssistant, acc: AladdinConnectClient) -> None: + """Initialize.""" + super().__init__( + hass, + logger=_LOGGER, + name=DOMAIN, + update_interval=timedelta(seconds=15), + ) + self.acc = acc + self.doors: list[GarageDoor] = [] + + async def async_setup(self) -> None: + """Fetch initial data.""" + self.doors = await self.acc.get_doors() + + async def _async_update_data(self) -> None: + """Fetch data from API endpoint.""" + for door in self.doors: + await self.acc.update_door(door.device_id, door.door_number) diff --git a/homeassistant/components/aladdin_connect/cover.py b/homeassistant/components/aladdin_connect/cover.py index 54f0ab32db9..29629593c75 100644 --- a/homeassistant/components/aladdin_connect/cover.py +++ b/homeassistant/components/aladdin_connect/cover.py @@ -1,9 +1,7 @@ """Cover Entity for Genie Garage Door.""" -from datetime import timedelta from typing import Any -from genie_partner_sdk.client import AladdinConnectClient from genie_partner_sdk.model import GarageDoor from homeassistant.components.cover import ( @@ -11,52 +9,36 @@ from homeassistant.components.cover import ( CoverEntity, CoverEntityFeature, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.device_registry as dr -from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import api +from . import AladdinConnectConfigEntry, AladdinConnectCoordinator from .const import DOMAIN - -SCAN_INTERVAL = timedelta(seconds=15) +from .entity import AladdinConnectEntity async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: AladdinConnectConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Aladdin Connect platform.""" - session: api.AsyncConfigEntryAuth = config_entry.runtime_data - acc = AladdinConnectClient(session) - doors = await acc.get_doors() - if doors is None: - raise PlatformNotReady("Error from Aladdin Connect getting doors") - device_registry = dr.async_get(hass) - doors_to_add = [] - for door in doors: - existing = device_registry.async_get(door.unique_id) - if existing is None: - doors_to_add.append(door) + coordinator = config_entry.runtime_data - async_add_entities( - (AladdinDevice(acc, door, config_entry) for door in doors_to_add), - ) - remove_stale_devices(hass, config_entry, doors) + async_add_entities(AladdinDevice(coordinator, door) for door in coordinator.doors) + remove_stale_devices(hass, config_entry) def remove_stale_devices( - hass: HomeAssistant, config_entry: ConfigEntry, devices: list[GarageDoor] + hass: HomeAssistant, config_entry: AladdinConnectConfigEntry ) -> None: """Remove stale devices from device registry.""" device_registry = dr.async_get(hass) device_entries = dr.async_entries_for_config_entry( device_registry, config_entry.entry_id ) - all_device_ids = {door.unique_id for door in devices} + all_device_ids = {door.unique_id for door in config_entry.runtime_data.doors} for device_entry in device_entries: device_id: str | None = None @@ -75,45 +57,38 @@ def remove_stale_devices( ) -class AladdinDevice(CoverEntity): +class AladdinDevice(AladdinConnectEntity, CoverEntity): """Representation of Aladdin Connect cover.""" _attr_device_class = CoverDeviceClass.GARAGE _attr_supported_features = CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE - _attr_has_entity_name = True _attr_name = None def __init__( - self, acc: AladdinConnectClient, device: GarageDoor, entry: ConfigEntry + self, coordinator: AladdinConnectCoordinator, device: GarageDoor ) -> None: """Initialize the Aladdin Connect cover.""" - self._acc = acc - self._device_id = device.device_id - self._number = device.door_number - - self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, device.unique_id)}, - name=device.name, - manufacturer="Overhead Door", - ) + super().__init__(coordinator, device) self._attr_unique_id = device.unique_id async def async_open_cover(self, **kwargs: Any) -> None: """Issue open command to cover.""" - await self._acc.open_door(self._device_id, self._number) + await self.coordinator.acc.open_door( + self._device.device_id, self._device.door_number + ) async def async_close_cover(self, **kwargs: Any) -> None: """Issue close command to cover.""" - await self._acc.close_door(self._device_id, self._number) - - async def async_update(self) -> None: - """Update status of cover.""" - await self._acc.update_door(self._device_id, self._number) + await self.coordinator.acc.close_door( + self._device.device_id, self._device.door_number + ) @property def is_closed(self) -> bool | None: """Update is closed attribute.""" - value = self._acc.get_door_status(self._device_id, self._number) + value = self.coordinator.acc.get_door_status( + self._device.device_id, self._device.door_number + ) if value is None: return None return bool(value == "closed") @@ -121,7 +96,9 @@ class AladdinDevice(CoverEntity): @property def is_closing(self) -> bool | None: """Update is closing attribute.""" - value = self._acc.get_door_status(self._device_id, self._number) + value = self.coordinator.acc.get_door_status( + self._device.device_id, self._device.door_number + ) if value is None: return None return bool(value == "closing") @@ -129,7 +106,9 @@ class AladdinDevice(CoverEntity): @property def is_opening(self) -> bool | None: """Update is opening attribute.""" - value = self._acc.get_door_status(self._device_id, self._number) + value = self.coordinator.acc.get_door_status( + self._device.device_id, self._device.door_number + ) if value is None: return None return bool(value == "opening") diff --git a/homeassistant/components/aladdin_connect/entity.py b/homeassistant/components/aladdin_connect/entity.py new file mode 100644 index 00000000000..8d9eeefcdfb --- /dev/null +++ b/homeassistant/components/aladdin_connect/entity.py @@ -0,0 +1,27 @@ +"""Defines a base Aladdin Connect entity.""" + +from genie_partner_sdk.model import GarageDoor + +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN +from .coordinator import AladdinConnectCoordinator + + +class AladdinConnectEntity(CoordinatorEntity[AladdinConnectCoordinator]): + """Defines a base Aladdin Connect entity.""" + + _attr_has_entity_name = True + + def __init__( + self, coordinator: AladdinConnectCoordinator, device: GarageDoor + ) -> None: + """Initialize the entity.""" + super().__init__(coordinator) + self._device = device + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, device.unique_id)}, + name=device.name, + manufacturer="Overhead Door", + ) diff --git a/homeassistant/components/aladdin_connect/sensor.py b/homeassistant/components/aladdin_connect/sensor.py index f9ed2a6aeeb..2bd0168a500 100644 --- a/homeassistant/components/aladdin_connect/sensor.py +++ b/homeassistant/components/aladdin_connect/sensor.py @@ -4,7 +4,6 @@ from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass -from typing import cast from genie_partner_sdk.client import AladdinConnectClient from genie_partner_sdk.model import GarageDoor @@ -15,21 +14,19 @@ from homeassistant.components.sensor import ( SensorEntityDescription, SensorStateClass, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import PERCENTAGE from homeassistant.core import HomeAssistant -from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import api -from .const import DOMAIN +from . import AladdinConnectConfigEntry, AladdinConnectCoordinator +from .entity import AladdinConnectEntity @dataclass(frozen=True, kw_only=True) class AccSensorEntityDescription(SensorEntityDescription): """Describes AladdinConnect sensor entity.""" - value_fn: Callable + value_fn: Callable[[AladdinConnectClient, str, int], float | None] SENSORS: tuple[AccSensorEntityDescription, ...] = ( @@ -45,52 +42,39 @@ SENSORS: tuple[AccSensorEntityDescription, ...] = ( async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, + entry: AladdinConnectConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up Aladdin Connect sensor devices.""" + coordinator = entry.runtime_data - session: api.AsyncConfigEntryAuth = hass.data[DOMAIN][entry.entry_id] - acc = AladdinConnectClient(session) - - entities = [] - doors = await acc.get_doors() - - for door in doors: - entities.extend( - [AladdinConnectSensor(acc, door, description) for description in SENSORS] - ) - - async_add_entities(entities) + async_add_entities( + AladdinConnectSensor(coordinator, door, description) + for description in SENSORS + for door in coordinator.doors + ) -class AladdinConnectSensor(SensorEntity): +class AladdinConnectSensor(AladdinConnectEntity, SensorEntity): """A sensor implementation for Aladdin Connect devices.""" entity_description: AccSensorEntityDescription - _attr_has_entity_name = True def __init__( self, - acc: AladdinConnectClient, + coordinator: AladdinConnectCoordinator, device: GarageDoor, description: AccSensorEntityDescription, ) -> None: """Initialize a sensor for an Aladdin Connect device.""" - self._device_id = device.device_id - self._number = device.door_number - self._acc = acc + super().__init__(coordinator, device) self.entity_description = description self._attr_unique_id = f"{device.unique_id}-{description.key}" - self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, device.unique_id)}, - name=device.name, - manufacturer="Overhead Door", - ) @property def native_value(self) -> float | None: """Return the state of the sensor.""" - return cast( - float, - self.entity_description.value_fn(self._acc, self._device_id, self._number), + return self.entity_description.value_fn( + self.coordinator.acc, self._device.device_id, self._device.door_number ) From 43a9a4f9ed07c5106ec2dc9a0aba2a710ad5c5e7 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 4 Jun 2024 10:53:16 +0200 Subject: [PATCH 0196/1445] Bump airgradient to 0.4.3 (#118776) --- homeassistant/components/airgradient/config_flow.py | 2 +- homeassistant/components/airgradient/manifest.json | 2 +- homeassistant/components/airgradient/select.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/airgradient/config_flow.py b/homeassistant/components/airgradient/config_flow.py index fff2615365e..6fc12cf7397 100644 --- a/homeassistant/components/airgradient/config_flow.py +++ b/homeassistant/components/airgradient/config_flow.py @@ -29,7 +29,7 @@ class AirGradientConfigFlow(ConfigFlow, domain=DOMAIN): """Set configuration source to local if it hasn't been set yet.""" assert self.client config = await self.client.get_config() - if config.configuration_control is ConfigurationControl.BOTH: + if config.configuration_control is ConfigurationControl.NOT_INITIALIZED: await self.client.set_configuration_control(ConfigurationControl.LOCAL) async def async_step_zeroconf( diff --git a/homeassistant/components/airgradient/manifest.json b/homeassistant/components/airgradient/manifest.json index 474031ccfe1..c30d7a4c42f 100644 --- a/homeassistant/components/airgradient/manifest.json +++ b/homeassistant/components/airgradient/manifest.json @@ -6,6 +6,6 @@ "documentation": "https://www.home-assistant.io/integrations/airgradient", "integration_type": "device", "iot_class": "local_polling", - "requirements": ["airgradient==0.4.2"], + "requirements": ["airgradient==0.4.3"], "zeroconf": ["_airgradient._tcp.local."] } diff --git a/homeassistant/components/airgradient/select.py b/homeassistant/components/airgradient/select.py index 5e13ee1d0bb..7a82d3b8a46 100644 --- a/homeassistant/components/airgradient/select.py +++ b/homeassistant/components/airgradient/select.py @@ -33,7 +33,7 @@ CONFIG_CONTROL_ENTITY = AirGradientSelectEntityDescription( options=[ConfigurationControl.CLOUD.value, ConfigurationControl.LOCAL.value], entity_category=EntityCategory.CONFIG, value_fn=lambda config: config.configuration_control - if config.configuration_control is not ConfigurationControl.BOTH + if config.configuration_control is not ConfigurationControl.NOT_INITIALIZED else None, set_value_fn=lambda client, value: client.set_configuration_control( ConfigurationControl(value) diff --git a/requirements_all.txt b/requirements_all.txt index 1bb6417f909..63dd4030074 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -404,7 +404,7 @@ aiowithings==2.1.0 aioymaps==1.2.2 # homeassistant.components.airgradient -airgradient==0.4.2 +airgradient==0.4.3 # homeassistant.components.airly airly==1.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1308597ce9f..c3907125942 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -377,7 +377,7 @@ aiowithings==2.1.0 aioymaps==1.2.2 # homeassistant.components.airgradient -airgradient==0.4.2 +airgradient==0.4.3 # homeassistant.components.airly airly==1.1.0 From a1e4d4ddd7c5eef1755172f32816ba6a8409969a Mon Sep 17 00:00:00 2001 From: Alexandre CUER Date: Tue, 4 Jun 2024 10:56:43 +0200 Subject: [PATCH 0197/1445] Remove duplicate code in emoncms (#118610) * Remove duplicate & property extra_state_attributes * Add methods _update_attributes and _update_value * correction in _update_value * Update homeassistant/components/emoncms/sensor.py Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> * Update homeassistant/components/emoncms/sensor.py Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> * Update homeassistant/components/emoncms/sensor.py Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> * Update homeassistant/components/emoncms/sensor.py Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> --------- Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> --- homeassistant/components/emoncms/sensor.py | 46 +++++++++------------- 1 file changed, 18 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/emoncms/sensor.py b/homeassistant/components/emoncms/sensor.py index c981fa0cf6c..cf21cb75847 100644 --- a/homeassistant/components/emoncms/sensor.py +++ b/homeassistant/components/emoncms/sensor.py @@ -169,7 +169,6 @@ class EmonCmsSensor(SensorEntity): self._value_template = value_template self._attr_native_unit_of_measurement = unit_of_measurement self._sensorid = sensorid - self._elem = elem if unit_of_measurement in ("kWh", "Wh"): self._attr_device_class = SensorDeviceClass.ENERGY @@ -195,7 +194,24 @@ class EmonCmsSensor(SensorEntity): elif unit_of_measurement == "hPa": self._attr_device_class = SensorDeviceClass.PRESSURE self._attr_state_class = SensorStateClass.MEASUREMENT + self._update_attributes(elem) + def _update_attributes(self, elem: dict[str, Any]) -> None: + """Update entity attributes.""" + self._attr_extra_state_attributes = { + ATTR_FEEDID: elem["id"], + ATTR_TAG: elem["tag"], + ATTR_FEEDNAME: elem["name"], + } + if elem["value"] is not None: + self._attr_extra_state_attributes[ATTR_SIZE] = elem["size"] + self._attr_extra_state_attributes[ATTR_USERID] = elem["userid"] + self._attr_extra_state_attributes[ATTR_LASTUPDATETIME] = elem["time"] + self._attr_extra_state_attributes[ATTR_LASTUPDATETIMESTR] = ( + template.timestamp_local(float(elem["time"])) + ) + + self._attr_native_value = None if self._value_template is not None: self._attr_native_value = ( self._value_template.render_with_possible_json_value( @@ -204,21 +220,6 @@ class EmonCmsSensor(SensorEntity): ) elif elem["value"] is not None: self._attr_native_value = round(float(elem["value"]), DECIMALS) - else: - self._attr_native_value = None - - @property - def extra_state_attributes(self) -> dict[str, Any]: - """Return the sensor extra attributes.""" - return { - ATTR_FEEDID: self._elem["id"], - ATTR_TAG: self._elem["tag"], - ATTR_FEEDNAME: self._elem["name"], - ATTR_SIZE: self._elem["size"], - ATTR_USERID: self._elem["userid"], - ATTR_LASTUPDATETIME: self._elem["time"], - ATTR_LASTUPDATETIMESTR: template.timestamp_local(float(self._elem["time"])), - } def update(self) -> None: """Get the latest data and updates the state.""" @@ -246,18 +247,7 @@ class EmonCmsSensor(SensorEntity): if elem is None: return - self._elem = elem - - if self._value_template is not None: - self._attr_native_value = ( - self._value_template.render_with_possible_json_value( - elem["value"], STATE_UNKNOWN - ) - ) - elif elem["value"] is not None: - self._attr_native_value = round(float(elem["value"]), DECIMALS) - else: - self._attr_native_value = None + self._update_attributes(elem) class EmonCmsData: From d905542f49b63e10fe974d7c040e1f2bbc43c707 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 4 Jun 2024 11:00:00 +0200 Subject: [PATCH 0198/1445] Bump dawidd6/action-download-artifact from 3.1.4 to 4 (#118772) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/builder.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml index b05397280c2..3d1b85666cd 100644 --- a/.github/workflows/builder.yml +++ b/.github/workflows/builder.yml @@ -94,7 +94,7 @@ jobs: - name: Download nightly wheels of frontend if: needs.init.outputs.channel == 'dev' - uses: dawidd6/action-download-artifact@v3.1.4 + uses: dawidd6/action-download-artifact@v4 with: github_token: ${{secrets.GITHUB_TOKEN}} repo: home-assistant/frontend @@ -105,7 +105,7 @@ jobs: - name: Download nightly wheels of intents if: needs.init.outputs.channel == 'dev' - uses: dawidd6/action-download-artifact@v3.1.4 + uses: dawidd6/action-download-artifact@v4 with: github_token: ${{secrets.GITHUB_TOKEN}} repo: home-assistant/intents-package From 7ed119b0b6ebe70040ab054f2f42c49fe5a47d35 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 4 Jun 2024 11:04:10 +0200 Subject: [PATCH 0199/1445] Re-enable sensor platform for Aladdin Connect (#118782) --- homeassistant/components/aladdin_connect/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/aladdin_connect/__init__.py b/homeassistant/components/aladdin_connect/__init__.py index 6317cf8358e..504e53764f0 100644 --- a/homeassistant/components/aladdin_connect/__init__.py +++ b/homeassistant/components/aladdin_connect/__init__.py @@ -16,7 +16,7 @@ from homeassistant.helpers.config_entry_oauth2_flow import ( from .api import AsyncConfigEntryAuth from .coordinator import AladdinConnectCoordinator -PLATFORMS: list[Platform] = [Platform.COVER] +PLATFORMS: list[Platform] = [Platform.COVER, Platform.SENSOR] type AladdinConnectConfigEntry = ConfigEntry[AladdinConnectCoordinator] From c0912a019c377a9b6e6a47111f3defa3560fb033 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 4 Jun 2024 11:30:34 +0200 Subject: [PATCH 0200/1445] Deduplicate light services.yaml (#118738) * Deduplicate light services.yaml * Remove support for .-keys --- homeassistant/components/light/services.yaml | 496 ++----------------- 1 file changed, 45 insertions(+), 451 deletions(-) diff --git a/homeassistant/components/light/services.yaml b/homeassistant/components/light/services.yaml index 0e75380a40c..4f9f4e03b89 100644 --- a/homeassistant/components/light/services.yaml +++ b/homeassistant/components/light/services.yaml @@ -5,7 +5,7 @@ turn_on: entity: domain: light fields: - transition: + transition: &transition filter: supported_features: - light.LightEntityFeature.TRANSITION @@ -14,8 +14,8 @@ turn_on: min: 0 max: 300 unit_of_measurement: seconds - rgb_color: - filter: + rgb_color: &rgb_color + filter: &color_support attribute: supported_color_modes: - light.ColorMode.HS @@ -26,46 +26,25 @@ turn_on: example: "[255, 100, 100]" selector: color_rgb: - rgbw_color: - filter: - attribute: - supported_color_modes: - - light.ColorMode.HS - - light.ColorMode.XY - - light.ColorMode.RGB - - light.ColorMode.RGBW - - light.ColorMode.RGBWW + rgbw_color: &rgbw_color + filter: *color_support advanced: true example: "[255, 100, 100, 50]" selector: object: - rgbww_color: - filter: - attribute: - supported_color_modes: - - light.ColorMode.HS - - light.ColorMode.XY - - light.ColorMode.RGB - - light.ColorMode.RGBW - - light.ColorMode.RGBWW + rgbww_color: &rgbww_color + filter: *color_support advanced: true example: "[255, 100, 100, 50, 70]" selector: object: - color_name: - filter: - attribute: - supported_color_modes: - - light.ColorMode.HS - - light.ColorMode.XY - - light.ColorMode.RGB - - light.ColorMode.RGBW - - light.ColorMode.RGBWW + color_name: &color_name + filter: *color_support advanced: true selector: select: translation_key: color_name - options: + options: &named_colors - "homeassistant" - "aliceblue" - "antiquewhite" @@ -215,34 +194,20 @@ turn_on: - "whitesmoke" - "yellow" - "yellowgreen" - hs_color: - filter: - attribute: - supported_color_modes: - - light.ColorMode.HS - - light.ColorMode.XY - - light.ColorMode.RGB - - light.ColorMode.RGBW - - light.ColorMode.RGBWW + hs_color: &hs_color + filter: *color_support advanced: true example: "[300, 70]" selector: object: - xy_color: - filter: - attribute: - supported_color_modes: - - light.ColorMode.HS - - light.ColorMode.XY - - light.ColorMode.RGB - - light.ColorMode.RGBW - - light.ColorMode.RGBWW + xy_color: &xy_color + filter: *color_support advanced: true example: "[0.52, 0.43]" selector: object: - color_temp: - filter: + color_temp: &color_temp + filter: &color_temp_support attribute: supported_color_modes: - light.ColorMode.COLOR_TEMP @@ -257,23 +222,15 @@ turn_on: unit: "mired" min: 153 max: 500 - kelvin: - filter: - attribute: - supported_color_modes: - - light.ColorMode.COLOR_TEMP - - light.ColorMode.HS - - light.ColorMode.XY - - light.ColorMode.RGB - - light.ColorMode.RGBW - - light.ColorMode.RGBWW + kelvin: &kelvin + filter: *color_temp_support selector: color_temp: unit: "kelvin" min: 2000 max: 6500 - brightness: - filter: + brightness: &brightness + filter: &brightness_support attribute: supported_color_modes: - light.ColorMode.BRIGHTNESS @@ -288,55 +245,28 @@ turn_on: number: min: 0 max: 255 - brightness_pct: - filter: - attribute: - supported_color_modes: - - light.ColorMode.BRIGHTNESS - - light.ColorMode.COLOR_TEMP - - light.ColorMode.HS - - light.ColorMode.XY - - light.ColorMode.RGB - - light.ColorMode.RGBW - - light.ColorMode.RGBWW + brightness_pct: &brightness_pct + filter: *brightness_support selector: number: min: 0 max: 100 unit_of_measurement: "%" brightness_step: - filter: - attribute: - supported_color_modes: - - light.ColorMode.BRIGHTNESS - - light.ColorMode.COLOR_TEMP - - light.ColorMode.HS - - light.ColorMode.XY - - light.ColorMode.RGB - - light.ColorMode.RGBW - - light.ColorMode.RGBWW + filter: *brightness_support advanced: true selector: number: min: -225 max: 255 brightness_step_pct: - filter: - attribute: - supported_color_modes: - - light.ColorMode.BRIGHTNESS - - light.ColorMode.COLOR_TEMP - - light.ColorMode.HS - - light.ColorMode.XY - - light.ColorMode.RGB - - light.ColorMode.RGBW - - light.ColorMode.RGBWW + filter: *brightness_support selector: number: min: -100 max: 100 unit_of_measurement: "%" - white: + white: &white filter: attribute: supported_color_modes: @@ -346,12 +276,12 @@ turn_on: constant: value: true label: Enabled - profile: + profile: &profile advanced: true example: relax selector: text: - flash: + flash: &flash filter: supported_features: - light.LightEntityFeature.FLASH @@ -363,7 +293,7 @@ turn_on: value: "long" - label: "Short" value: "short" - effect: + effect: &effect filter: supported_features: - light.LightEntityFeature.EFFECT @@ -375,362 +305,26 @@ turn_off: entity: domain: light fields: - transition: - filter: - supported_features: - - light.LightEntityFeature.TRANSITION - selector: - number: - min: 0 - max: 300 - unit_of_measurement: seconds - flash: - filter: - supported_features: - - light.LightEntityFeature.FLASH - advanced: true - selector: - select: - options: - - label: "Long" - value: "long" - - label: "Short" - value: "short" + transition: *transition + flash: *flash toggle: target: entity: domain: light fields: - transition: - filter: - supported_features: - - light.LightEntityFeature.TRANSITION - selector: - number: - min: 0 - max: 300 - unit_of_measurement: seconds - rgb_color: - filter: - attribute: - supported_color_modes: - - light.ColorMode.HS - - light.ColorMode.XY - - light.ColorMode.RGB - - light.ColorMode.RGBW - - light.ColorMode.RGBWW - example: "[255, 100, 100]" - selector: - color_rgb: - rgbw_color: - filter: - attribute: - supported_color_modes: - - light.ColorMode.HS - - light.ColorMode.XY - - light.ColorMode.RGB - - light.ColorMode.RGBW - - light.ColorMode.RGBWW - advanced: true - example: "[255, 100, 100, 50]" - selector: - object: - rgbww_color: - filter: - attribute: - supported_color_modes: - - light.ColorMode.HS - - light.ColorMode.XY - - light.ColorMode.RGB - - light.ColorMode.RGBW - - light.ColorMode.RGBWW - advanced: true - example: "[255, 100, 100, 50, 70]" - selector: - object: - color_name: - filter: - attribute: - supported_color_modes: - - light.ColorMode.HS - - light.ColorMode.XY - - light.ColorMode.RGB - - light.ColorMode.RGBW - - light.ColorMode.RGBWW - advanced: true - selector: - select: - translation_key: color_name - options: - - "homeassistant" - - "aliceblue" - - "antiquewhite" - - "aqua" - - "aquamarine" - - "azure" - - "beige" - - "bisque" - # Black is omitted from this list as nonsensical for lights - - "blanchedalmond" - - "blue" - - "blueviolet" - - "brown" - - "burlywood" - - "cadetblue" - - "chartreuse" - - "chocolate" - - "coral" - - "cornflowerblue" - - "cornsilk" - - "crimson" - - "cyan" - - "darkblue" - - "darkcyan" - - "darkgoldenrod" - - "darkgray" - - "darkgreen" - - "darkgrey" - - "darkkhaki" - - "darkmagenta" - - "darkolivegreen" - - "darkorange" - - "darkorchid" - - "darkred" - - "darksalmon" - - "darkseagreen" - - "darkslateblue" - - "darkslategray" - - "darkslategrey" - - "darkturquoise" - - "darkviolet" - - "deeppink" - - "deepskyblue" - - "dimgray" - - "dimgrey" - - "dodgerblue" - - "firebrick" - - "floralwhite" - - "forestgreen" - - "fuchsia" - - "gainsboro" - - "ghostwhite" - - "gold" - - "goldenrod" - - "gray" - - "green" - - "greenyellow" - - "grey" - - "honeydew" - - "hotpink" - - "indianred" - - "indigo" - - "ivory" - - "khaki" - - "lavender" - - "lavenderblush" - - "lawngreen" - - "lemonchiffon" - - "lightblue" - - "lightcoral" - - "lightcyan" - - "lightgoldenrodyellow" - - "lightgray" - - "lightgreen" - - "lightgrey" - - "lightpink" - - "lightsalmon" - - "lightseagreen" - - "lightskyblue" - - "lightslategray" - - "lightslategrey" - - "lightsteelblue" - - "lightyellow" - - "lime" - - "limegreen" - - "linen" - - "magenta" - - "maroon" - - "mediumaquamarine" - - "mediumblue" - - "mediumorchid" - - "mediumpurple" - - "mediumseagreen" - - "mediumslateblue" - - "mediumspringgreen" - - "mediumturquoise" - - "mediumvioletred" - - "midnightblue" - - "mintcream" - - "mistyrose" - - "moccasin" - - "navajowhite" - - "navy" - - "navyblue" - - "oldlace" - - "olive" - - "olivedrab" - - "orange" - - "orangered" - - "orchid" - - "palegoldenrod" - - "palegreen" - - "paleturquoise" - - "palevioletred" - - "papayawhip" - - "peachpuff" - - "peru" - - "pink" - - "plum" - - "powderblue" - - "purple" - - "red" - - "rosybrown" - - "royalblue" - - "saddlebrown" - - "salmon" - - "sandybrown" - - "seagreen" - - "seashell" - - "sienna" - - "silver" - - "skyblue" - - "slateblue" - - "slategray" - - "slategrey" - - "snow" - - "springgreen" - - "steelblue" - - "tan" - - "teal" - - "thistle" - - "tomato" - - "turquoise" - - "violet" - - "wheat" - - "white" - - "whitesmoke" - - "yellow" - - "yellowgreen" - hs_color: - filter: - attribute: - supported_color_modes: - - light.ColorMode.HS - - light.ColorMode.XY - - light.ColorMode.RGB - - light.ColorMode.RGBW - - light.ColorMode.RGBWW - advanced: true - example: "[300, 70]" - selector: - object: - xy_color: - filter: - attribute: - supported_color_modes: - - light.ColorMode.HS - - light.ColorMode.XY - - light.ColorMode.RGB - - light.ColorMode.RGBW - - light.ColorMode.RGBWW - advanced: true - example: "[0.52, 0.43]" - selector: - object: - color_temp: - filter: - attribute: - supported_color_modes: - - light.ColorMode.COLOR_TEMP - - light.ColorMode.HS - - light.ColorMode.XY - - light.ColorMode.RGB - - light.ColorMode.RGBW - - light.ColorMode.RGBWW - advanced: true - selector: - color_temp: - unit: "mired" - min: 153 - max: 500 - kelvin: - filter: - attribute: - supported_color_modes: - - light.ColorMode.COLOR_TEMP - - light.ColorMode.HS - - light.ColorMode.XY - - light.ColorMode.RGB - - light.ColorMode.RGBW - - light.ColorMode.RGBWW - selector: - color_temp: - unit: "kelvin" - min: 2000 - max: 6500 - brightness: - filter: - attribute: - supported_color_modes: - - light.ColorMode.BRIGHTNESS - - light.ColorMode.COLOR_TEMP - - light.ColorMode.HS - - light.ColorMode.XY - - light.ColorMode.RGB - - light.ColorMode.RGBW - - light.ColorMode.RGBWW - advanced: true - selector: - number: - min: 0 - max: 255 - brightness_pct: - filter: - attribute: - supported_color_modes: - - light.ColorMode.BRIGHTNESS - - light.ColorMode.COLOR_TEMP - - light.ColorMode.HS - - light.ColorMode.XY - - light.ColorMode.RGB - - light.ColorMode.RGBW - - light.ColorMode.RGBWW - selector: - number: - min: 0 - max: 100 - unit_of_measurement: "%" - white: - filter: - attribute: - supported_color_modes: - - light.ColorMode.WHITE - advanced: true - selector: - constant: - value: true - label: Enabled - profile: - advanced: true - example: relax - selector: - text: - flash: - filter: - supported_features: - - light.LightEntityFeature.FLASH - advanced: true - selector: - select: - options: - - label: "Long" - value: "long" - - label: "Short" - value: "short" - effect: - filter: - supported_features: - - light.LightEntityFeature.EFFECT - selector: - text: + transition: *transition + rgb_color: *rgb_color + rgbw_color: *rgbw_color + rgbww_color: *rgbww_color + color_name: *color_name + hs_color: *hs_color + xy_color: *xy_color + color_temp: *color_temp + kelvin: *kelvin + brightness: *brightness + brightness_pct: *brightness_pct + white: *white + profile: *profile + flash: *flash + effect: *effect From fce5f2a93f6fb86a197e002893bc5cf5dae19e76 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 4 Jun 2024 11:34:21 +0200 Subject: [PATCH 0201/1445] Move Aladdin stale device removal to init module (#118784) --- .../components/aladdin_connect/__init__.py | 31 +++++++++++++++++++ .../components/aladdin_connect/cover.py | 30 ------------------ 2 files changed, 31 insertions(+), 30 deletions(-) diff --git a/homeassistant/components/aladdin_connect/__init__.py b/homeassistant/components/aladdin_connect/__init__.py index 504e53764f0..436e797271f 100644 --- a/homeassistant/components/aladdin_connect/__init__.py +++ b/homeassistant/components/aladdin_connect/__init__.py @@ -7,6 +7,7 @@ from genie_partner_sdk.client import AladdinConnectClient from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.config_entry_oauth2_flow import ( OAuth2Session, @@ -14,6 +15,7 @@ from homeassistant.helpers.config_entry_oauth2_flow import ( ) from .api import AsyncConfigEntryAuth +from .const import DOMAIN from .coordinator import AladdinConnectCoordinator PLATFORMS: list[Platform] = [Platform.COVER, Platform.SENSOR] @@ -38,6 +40,8 @@ async def async_setup_entry( await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + async_remove_stale_devices(hass, entry) + return True @@ -61,3 +65,30 @@ async def async_migrate_entry( ) return True + + +def async_remove_stale_devices( + hass: HomeAssistant, config_entry: AladdinConnectConfigEntry +) -> None: + """Remove stale devices from device registry.""" + device_registry = dr.async_get(hass) + device_entries = dr.async_entries_for_config_entry( + device_registry, config_entry.entry_id + ) + all_device_ids = {door.unique_id for door in config_entry.runtime_data.doors} + + for device_entry in device_entries: + device_id: str | None = None + + for identifier in device_entry.identifiers: + if identifier[0] == DOMAIN: + device_id = identifier[1] + break + + if device_id is None or device_id not in all_device_ids: + # If device_id is None an invalid device entry was found for this config entry. + # If the device_id is not in existing device ids it's a stale device entry. + # Remove config entry from this device entry in either case. + device_registry.async_update_device( + device_entry.id, remove_config_entry_id=config_entry.entry_id + ) diff --git a/homeassistant/components/aladdin_connect/cover.py b/homeassistant/components/aladdin_connect/cover.py index 29629593c75..b8c48048192 100644 --- a/homeassistant/components/aladdin_connect/cover.py +++ b/homeassistant/components/aladdin_connect/cover.py @@ -10,11 +10,9 @@ from homeassistant.components.cover import ( CoverEntityFeature, ) from homeassistant.core import HomeAssistant -import homeassistant.helpers.device_registry as dr from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import AladdinConnectConfigEntry, AladdinConnectCoordinator -from .const import DOMAIN from .entity import AladdinConnectEntity @@ -27,34 +25,6 @@ async def async_setup_entry( coordinator = config_entry.runtime_data async_add_entities(AladdinDevice(coordinator, door) for door in coordinator.doors) - remove_stale_devices(hass, config_entry) - - -def remove_stale_devices( - hass: HomeAssistant, config_entry: AladdinConnectConfigEntry -) -> None: - """Remove stale devices from device registry.""" - device_registry = dr.async_get(hass) - device_entries = dr.async_entries_for_config_entry( - device_registry, config_entry.entry_id - ) - all_device_ids = {door.unique_id for door in config_entry.runtime_data.doors} - - for device_entry in device_entries: - device_id: str | None = None - - for identifier in device_entry.identifiers: - if identifier[0] == DOMAIN: - device_id = identifier[1] - break - - if device_id is None or device_id not in all_device_ids: - # If device_id is None an invalid device entry was found for this config entry. - # If the device_id is not in existing device ids it's a stale device entry. - # Remove config entry from this device entry in either case. - device_registry.async_update_device( - device_entry.id, remove_config_entry_id=config_entry.entry_id - ) class AladdinDevice(AladdinConnectEntity, CoverEntity): From 3ac0fa53c8b50b17baa8936472719cb1d69a8c03 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 4 Jun 2024 11:49:21 +0200 Subject: [PATCH 0202/1445] Cleanup unused FixtureRequest in tests (#118780) --- tests/components/hassio/test_sensor.py | 2 +- tests/components/homekit_controller/conftest.py | 4 +++- tests/components/ipma/test_config_flow.py | 3 ++- tests/components/knx/conftest.py | 2 +- tests/components/lametric/conftest.py | 2 +- tests/components/nest/conftest.py | 2 +- tests/components/nest/test_config_flow.py | 2 +- tests/components/tedee/conftest.py | 2 +- 8 files changed, 11 insertions(+), 8 deletions(-) diff --git a/tests/components/hassio/test_sensor.py b/tests/components/hassio/test_sensor.py index 8780d57da45..71b867d849d 100644 --- a/tests/components/hassio/test_sensor.py +++ b/tests/components/hassio/test_sensor.py @@ -28,7 +28,7 @@ MOCK_ENVIRON = {"SUPERVISOR": "127.0.0.1", "SUPERVISOR_TOKEN": "abcdefgh"} @pytest.fixture(autouse=True) -def mock_all(aioclient_mock: AiohttpClientMocker, request): +def mock_all(aioclient_mock: AiohttpClientMocker) -> None: """Mock all setup requests.""" _install_default_mocks(aioclient_mock) _install_test_addon_stats_mock(aioclient_mock) diff --git a/tests/components/homekit_controller/conftest.py b/tests/components/homekit_controller/conftest.py index ae2ca721cfa..9376a08697d 100644 --- a/tests/components/homekit_controller/conftest.py +++ b/tests/components/homekit_controller/conftest.py @@ -1,10 +1,12 @@ """HomeKit controller session fixtures.""" +from collections.abc import Generator import datetime import unittest.mock from aiohomekit.testing import FakeController from freezegun import freeze_time +from freezegun.api import FrozenDateTimeFactory import pytest import homeassistant.util.dt as dt_util @@ -15,7 +17,7 @@ pytest.register_assert_rewrite("tests.components.homekit_controller.common") @pytest.fixture(autouse=True) -def freeze_time_in_future(request): +def freeze_time_in_future() -> Generator[FrozenDateTimeFactory, None, None]: """Freeze time at a known point.""" now = dt_util.utcnow() start_dt = datetime.datetime(now.year + 1, 1, 1, 0, 0, 0, tzinfo=now.tzinfo) diff --git a/tests/components/ipma/test_config_flow.py b/tests/components/ipma/test_config_flow.py index ef9b667f03d..38c142ace2a 100644 --- a/tests/components/ipma/test_config_flow.py +++ b/tests/components/ipma/test_config_flow.py @@ -1,5 +1,6 @@ """Tests for IPMA config flow.""" +from collections.abc import Generator from unittest.mock import patch from pyipma import IPMAException @@ -15,7 +16,7 @@ from tests.components.ipma import MockLocation @pytest.fixture(name="ipma_setup", autouse=True) -def ipma_setup_fixture(request): +def ipma_setup_fixture() -> Generator[None, None, None]: """Patch ipma setup entry.""" with patch("homeassistant.components.ipma.async_setup_entry", return_value=True): yield diff --git a/tests/components/knx/conftest.py b/tests/components/knx/conftest.py index 864a160ac1a..5cdeb0d8adb 100644 --- a/tests/components/knx/conftest.py +++ b/tests/components/knx/conftest.py @@ -266,7 +266,7 @@ def mock_config_entry() -> MockConfigEntry: @pytest.fixture -async def knx(request, hass, mock_config_entry: MockConfigEntry): +async def knx(hass: HomeAssistant, mock_config_entry: MockConfigEntry): """Create a KNX TestKit instance.""" knx_test_kit = KNXTestKit(hass, mock_config_entry) yield knx_test_kit diff --git a/tests/components/lametric/conftest.py b/tests/components/lametric/conftest.py index bd2ae275970..946efda9210 100644 --- a/tests/components/lametric/conftest.py +++ b/tests/components/lametric/conftest.py @@ -74,7 +74,7 @@ def device_fixture() -> str: @pytest.fixture -def mock_lametric(request, device_fixture: str) -> Generator[MagicMock, None, None]: +def mock_lametric(device_fixture: str) -> Generator[MagicMock, None, None]: """Return a mocked LaMetric TIME client.""" with ( patch( diff --git a/tests/components/nest/conftest.py b/tests/components/nest/conftest.py index b2e8302a7ad..006792bf35e 100644 --- a/tests/components/nest/conftest.py +++ b/tests/components/nest/conftest.py @@ -196,7 +196,7 @@ def subscriber_id() -> str: @pytest.fixture -def nest_test_config(request) -> NestTestConfig: +def nest_test_config() -> NestTestConfig: """Fixture that sets up the configuration used for the test.""" return TEST_CONFIG_APP_CREDS diff --git a/tests/components/nest/test_config_flow.py b/tests/components/nest/test_config_flow.py index abffb33b6b9..5c8f01c8e39 100644 --- a/tests/components/nest/test_config_flow.py +++ b/tests/components/nest/test_config_flow.py @@ -48,7 +48,7 @@ FAKE_DHCP_DATA = dhcp.DhcpServiceInfo( @pytest.fixture -def nest_test_config(request) -> NestTestConfig: +def nest_test_config() -> NestTestConfig: """Fixture with empty configuration and no existing config entry.""" return TEST_CONFIGFLOW_APP_CREDS diff --git a/tests/components/tedee/conftest.py b/tests/components/tedee/conftest.py index 14499935de2..1a8880936b1 100644 --- a/tests/components/tedee/conftest.py +++ b/tests/components/tedee/conftest.py @@ -46,7 +46,7 @@ def mock_setup_entry() -> Generator[AsyncMock, None, None]: @pytest.fixture -def mock_tedee(request) -> Generator[MagicMock, None, None]: +def mock_tedee() -> Generator[MagicMock, None, None]: """Return a mocked Tedee client.""" with ( patch( From 20b5aa3e0e2036c5ce56a91a1f652b4ca43597e2 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 4 Jun 2024 13:38:32 +0200 Subject: [PATCH 0203/1445] Move entity_registry_enabled_by_default to decorator [q-z] (#118793) --- .../components/qnap_qsw/test_binary_sensor.py | 4 +++- tests/components/qnap_qsw/test_sensor.py | 4 +++- tests/components/radarr/test_sensor.py | 2 +- .../components/sensibo/test_binary_sensor.py | 2 +- tests/components/sensibo/test_climate.py | 10 ++++----- tests/components/sensibo/test_number.py | 4 ++-- tests/components/sensibo/test_sensor.py | 2 +- tests/components/shelly/test_sensor.py | 7 +++--- tests/components/shelly/test_switch.py | 2 +- tests/components/shelly/test_update.py | 14 ++++++------ .../components/solaredge/test_coordinator.py | 2 +- tests/components/sonarr/test_sensor.py | 2 +- tests/components/sun/test_sensor.py | 2 +- tests/components/switchbot/test_sensor.py | 7 +++--- .../systemmonitor/test_binary_sensor.py | 4 ++-- .../components/systemmonitor/test_repairs.py | 5 +++-- tests/components/systemmonitor/test_sensor.py | 22 +++++++++---------- tests/components/systemmonitor/test_util.py | 4 ++-- .../trafikverket_camera/test_binary_sensor.py | 3 ++- .../trafikverket_camera/test_recorder.py | 2 +- .../trafikverket_camera/test_sensor.py | 3 ++- .../trafikverket_ferry/test_coordinator.py | 2 +- .../trafikverket_train/test_sensor.py | 11 +++++----- tests/components/unifi/test_sensor.py | 4 ++-- tests/components/unifiprotect/test_sensor.py | 5 +++-- tests/components/v2c/test_sensor.py | 3 ++- 26 files changed, 71 insertions(+), 61 deletions(-) diff --git a/tests/components/qnap_qsw/test_binary_sensor.py b/tests/components/qnap_qsw/test_binary_sensor.py index 3540eb6ba4a..535ffdfb693 100644 --- a/tests/components/qnap_qsw/test_binary_sensor.py +++ b/tests/components/qnap_qsw/test_binary_sensor.py @@ -1,5 +1,7 @@ """The binary sensor tests for the QNAP QSW platform.""" +import pytest + from homeassistant.components.qnap_qsw.const import ATTR_MESSAGE from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant @@ -8,9 +10,9 @@ from homeassistant.helpers import entity_registry as er from .util import async_init_integration +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_qnap_qsw_create_binary_sensors( hass: HomeAssistant, - entity_registry_enabled_by_default: None, entity_registry: er.EntityRegistry, ) -> None: """Test creation of binary sensors.""" diff --git a/tests/components/qnap_qsw/test_sensor.py b/tests/components/qnap_qsw/test_sensor.py index 673a607acdf..646058add62 100644 --- a/tests/components/qnap_qsw/test_sensor.py +++ b/tests/components/qnap_qsw/test_sensor.py @@ -1,14 +1,16 @@ """The sensor tests for the QNAP QSW platform.""" +import pytest + from homeassistant.components.qnap_qsw.const import ATTR_MAX from homeassistant.core import HomeAssistant from .util import async_init_integration +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_qnap_qsw_create_sensors( hass: HomeAssistant, - entity_registry_enabled_by_default: None, ) -> None: """Test creation of sensors.""" diff --git a/tests/components/radarr/test_sensor.py b/tests/components/radarr/test_sensor.py index bbb89cd43fa..563ac504057 100644 --- a/tests/components/radarr/test_sensor.py +++ b/tests/components/radarr/test_sensor.py @@ -52,10 +52,10 @@ from tests.test_util.aiohttp import AiohttpClientMocker ), ], ) +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_sensors( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, - entity_registry_enabled_by_default: None, windows: bool, single: bool, root_folder: str, diff --git a/tests/components/sensibo/test_binary_sensor.py b/tests/components/sensibo/test_binary_sensor.py index 24653e6b7c7..61b62226679 100644 --- a/tests/components/sensibo/test_binary_sensor.py +++ b/tests/components/sensibo/test_binary_sensor.py @@ -15,9 +15,9 @@ from homeassistant.util import dt as dt_util from tests.common import async_fire_time_changed +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_binary_sensor( hass: HomeAssistant, - entity_registry_enabled_by_default: None, load_int: ConfigEntry, monkeypatch: pytest.MonkeyPatch, get_data: SensiboData, diff --git a/tests/components/sensibo/test_climate.py b/tests/components/sensibo/test_climate.py index 55d404b8331..6b4aedab828 100644 --- a/tests/components/sensibo/test_climate.py +++ b/tests/components/sensibo/test_climate.py @@ -832,9 +832,9 @@ async def test_climate_no_fan_no_swing( assert state.attributes["swing_modes"] is None +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_climate_set_timer( hass: HomeAssistant, - entity_registry_enabled_by_default: None, load_int: ConfigEntry, monkeypatch: pytest.MonkeyPatch, get_data: SensiboData, @@ -947,9 +947,9 @@ async def test_climate_set_timer( ) +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_climate_pure_boost( hass: HomeAssistant, - entity_registry_enabled_by_default: None, load_int: ConfigEntry, monkeypatch: pytest.MonkeyPatch, get_data: SensiboData, @@ -1058,9 +1058,9 @@ async def test_climate_pure_boost( assert state4.state == "s" +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_climate_climate_react( hass: HomeAssistant, - entity_registry_enabled_by_default: None, load_int: ConfigEntry, monkeypatch: pytest.MonkeyPatch, get_data: SensiboData, @@ -1228,9 +1228,9 @@ async def test_climate_climate_react( assert state4.state == "temperature" +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_climate_climate_react_fahrenheit( hass: HomeAssistant, - entity_registry_enabled_by_default: None, load_int: ConfigEntry, monkeypatch: pytest.MonkeyPatch, get_data: SensiboData, @@ -1374,9 +1374,9 @@ async def test_climate_climate_react_fahrenheit( assert state4.state == "temperature" +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_climate_full_ac_state( hass: HomeAssistant, - entity_registry_enabled_by_default: None, load_int: ConfigEntry, monkeypatch: pytest.MonkeyPatch, get_data: SensiboData, diff --git a/tests/components/sensibo/test_number.py b/tests/components/sensibo/test_number.py index e0a5a6a8bde..de369698f50 100644 --- a/tests/components/sensibo/test_number.py +++ b/tests/components/sensibo/test_number.py @@ -22,9 +22,9 @@ from homeassistant.util import dt as dt_util from tests.common import async_fire_time_changed +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_number( hass: HomeAssistant, - entity_registry_enabled_by_default: None, load_int: ConfigEntry, monkeypatch: pytest.MonkeyPatch, get_data: SensiboData, @@ -52,9 +52,9 @@ async def test_number( assert state1.state == "0.2" +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_number_set_value( hass: HomeAssistant, - entity_registry_enabled_by_default: None, load_int: ConfigEntry, get_data: SensiboData, ) -> None: diff --git a/tests/components/sensibo/test_sensor.py b/tests/components/sensibo/test_sensor.py index 4e254568ac4..3c6fb584a6e 100644 --- a/tests/components/sensibo/test_sensor.py +++ b/tests/components/sensibo/test_sensor.py @@ -16,9 +16,9 @@ from homeassistant.util import dt as dt_util from tests.common import async_fire_time_changed +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_sensor( hass: HomeAssistant, - entity_registry_enabled_by_default: None, load_int: ConfigEntry, monkeypatch: pytest.MonkeyPatch, get_data: SensiboData, diff --git a/tests/components/shelly/test_sensor.py b/tests/components/shelly/test_sensor.py index e7bac38c7fd..33008287b98 100644 --- a/tests/components/shelly/test_sensor.py +++ b/tests/components/shelly/test_sensor.py @@ -355,11 +355,11 @@ async def test_rpc_sensor( assert hass.states.get(entity_id).state == STATE_UNKNOWN +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_rpc_rssi_sensor_removal( hass: HomeAssistant, mock_rpc_device: Mock, monkeypatch: pytest.MonkeyPatch, - entity_registry_enabled_by_default: None, ) -> None: """Test RPC RSSI sensor removal if no WiFi stations enabled.""" entity_id = f"{SENSOR_DOMAIN}.test_name_rssi" @@ -548,9 +548,8 @@ async def test_rpc_restored_sleeping_sensor_no_last_state( assert hass.states.get(entity_id).state == "22.9" -async def test_rpc_em1_sensors( - hass: HomeAssistant, mock_rpc_device: Mock, entity_registry_enabled_by_default: None -) -> None: +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_rpc_em1_sensors(hass: HomeAssistant, mock_rpc_device: Mock) -> None: """Test RPC sensors for EM1 component.""" registry = async_get(hass) await init_integration(hass, 2) diff --git a/tests/components/shelly/test_switch.py b/tests/components/shelly/test_switch.py index 212fd4e6bab..ac75e6dd96f 100644 --- a/tests/components/shelly/test_switch.py +++ b/tests/components/shelly/test_switch.py @@ -472,10 +472,10 @@ async def test_wall_display_relay_mode( assert entry.unique_id == "123456789ABC-switch:0" +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_create_issue_valve_switch( hass: HomeAssistant, mock_block_device: Mock, - entity_registry_enabled_by_default: None, monkeypatch: pytest.MonkeyPatch, issue_registry: ir.IssueRegistry, ) -> None: diff --git a/tests/components/shelly/test_update.py b/tests/components/shelly/test_update.py index b4ec42762bb..2b233170254 100644 --- a/tests/components/shelly/test_update.py +++ b/tests/components/shelly/test_update.py @@ -44,13 +44,13 @@ from . import ( from tests.common import mock_restore_cache +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_block_update( hass: HomeAssistant, freezer: FrozenDateTimeFactory, mock_block_device: Mock, entity_registry: EntityRegistry, monkeypatch: pytest.MonkeyPatch, - entity_registry_enabled_by_default: None, ) -> None: """Test block device update entity.""" entity_id = "update.test_name_firmware_update" @@ -96,13 +96,13 @@ async def test_block_update( assert entry.unique_id == "123456789ABC-fwupdate" +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_block_beta_update( hass: HomeAssistant, freezer: FrozenDateTimeFactory, mock_block_device: Mock, entity_registry: EntityRegistry, monkeypatch: pytest.MonkeyPatch, - entity_registry_enabled_by_default: None, ) -> None: """Test block device beta update entity.""" entity_id = "update.test_name_beta_firmware_update" @@ -156,12 +156,12 @@ async def test_block_beta_update( assert entry.unique_id == "123456789ABC-fwupdate_beta" +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_block_update_connection_error( hass: HomeAssistant, mock_block_device: Mock, monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture, - entity_registry_enabled_by_default: None, ) -> None: """Test block device update connection error.""" monkeypatch.setitem(mock_block_device.status["update"], "old_version", "1") @@ -183,11 +183,11 @@ async def test_block_update_connection_error( assert "Error starting OTA update" in caplog.text +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_block_update_auth_error( hass: HomeAssistant, mock_block_device: Mock, monkeypatch: pytest.MonkeyPatch, - entity_registry_enabled_by_default: None, ) -> None: """Test block device update authentication error.""" monkeypatch.setitem(mock_block_device.status["update"], "old_version", "1") @@ -475,13 +475,13 @@ async def test_rpc_restored_sleeping_update_no_last_state( assert state.attributes[ATTR_SUPPORTED_FEATURES] == UpdateEntityFeature(0) +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_rpc_beta_update( hass: HomeAssistant, freezer: FrozenDateTimeFactory, mock_rpc_device: Mock, entity_registry: EntityRegistry, monkeypatch: pytest.MonkeyPatch, - entity_registry_enabled_by_default: None, ) -> None: """Test RPC device beta update entity.""" entity_id = "update.test_name_beta_firmware_update" @@ -601,6 +601,7 @@ async def test_rpc_beta_update( (RpcCallError(-1, "error"), "OTA update request error"), ], ) +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_rpc_update_errors( hass: HomeAssistant, exc: Exception, @@ -608,7 +609,6 @@ async def test_rpc_update_errors( mock_rpc_device: Mock, monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture, - entity_registry_enabled_by_default: None, ) -> None: """Test RPC device update connection/call errors.""" monkeypatch.setitem(mock_rpc_device.shelly, "ver", "1") @@ -635,12 +635,12 @@ async def test_rpc_update_errors( assert error in caplog.text +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_rpc_update_auth_error( hass: HomeAssistant, mock_rpc_device: Mock, entity_registry: EntityRegistry, monkeypatch: pytest.MonkeyPatch, - entity_registry_enabled_by_default: None, ) -> None: """Test RPC device update authentication error.""" monkeypatch.setitem(mock_rpc_device.shelly, "ver", "1") diff --git a/tests/components/solaredge/test_coordinator.py b/tests/components/solaredge/test_coordinator.py index 7a6b3af1cde..984c343a657 100644 --- a/tests/components/solaredge/test_coordinator.py +++ b/tests/components/solaredge/test_coordinator.py @@ -21,7 +21,7 @@ API_KEY = "a1b2c3d4e5f6g7h8" @pytest.fixture(autouse=True) -def enable_all_entities(entity_registry_enabled_by_default): +def enable_all_entities(entity_registry_enabled_by_default: None) -> None: """Make sure all entities are enabled.""" diff --git a/tests/components/sonarr/test_sensor.py b/tests/components/sonarr/test_sensor.py index 1221cc86df3..3ccff4c88ba 100644 --- a/tests/components/sonarr/test_sensor.py +++ b/tests/components/sonarr/test_sensor.py @@ -22,12 +22,12 @@ from tests.common import MockConfigEntry, async_fire_time_changed UPCOMING_ENTITY_ID = f"{SENSOR_DOMAIN}.sonarr_upcoming" +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_sensors( hass: HomeAssistant, entity_registry: er.EntityRegistry, mock_config_entry: MockConfigEntry, mock_sonarr: MagicMock, - entity_registry_enabled_by_default: None, ) -> None: """Test the creation and values of the sensors.""" sensors = { diff --git a/tests/components/sun/test_sensor.py b/tests/components/sun/test_sensor.py index 5cc91f79076..cb97ae565c7 100644 --- a/tests/components/sun/test_sensor.py +++ b/tests/components/sun/test_sensor.py @@ -15,11 +15,11 @@ from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_setting_rising( hass: HomeAssistant, entity_registry: er.EntityRegistry, freezer: FrozenDateTimeFactory, - entity_registry_enabled_by_default: None, ) -> None: """Test retrieving sun setting and rising.""" utc_now = datetime(2016, 11, 1, 8, 0, 0, tzinfo=dt_util.UTC) diff --git a/tests/components/switchbot/test_sensor.py b/tests/components/switchbot/test_sensor.py index 12a570d5b26..030a477596c 100644 --- a/tests/components/switchbot/test_sensor.py +++ b/tests/components/switchbot/test_sensor.py @@ -1,5 +1,7 @@ """Test the switchbot sensors.""" +import pytest + from homeassistant.components.sensor import ATTR_STATE_CLASS from homeassistant.components.switchbot.const import DOMAIN from homeassistant.const import ( @@ -19,9 +21,8 @@ from tests.common import MockConfigEntry from tests.components.bluetooth import inject_bluetooth_service_info -async def test_sensors( - hass: HomeAssistant, entity_registry_enabled_by_default: None -) -> None: +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_sensors(hass: HomeAssistant) -> None: """Test setting up creates the sensors.""" await async_setup_component(hass, DOMAIN, {}) inject_bluetooth_service_info(hass, WOHAND_SERVICE_INFO) diff --git a/tests/components/systemmonitor/test_binary_sensor.py b/tests/components/systemmonitor/test_binary_sensor.py index e3fbdedc081..97369dc2738 100644 --- a/tests/components/systemmonitor/test_binary_sensor.py +++ b/tests/components/systemmonitor/test_binary_sensor.py @@ -20,9 +20,9 @@ from .conftest import MockProcess from tests.common import MockConfigEntry, async_fire_time_changed +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_binary_sensor( hass: HomeAssistant, - entity_registry_enabled_by_default: None, mock_psutil: Mock, mock_os: Mock, entity_registry: er.EntityRegistry, @@ -62,9 +62,9 @@ async def test_binary_sensor( assert state.attributes == snapshot(name=f"{state.name} - attributes") +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_binary_sensor_icon( hass: HomeAssistant, - entity_registry_enabled_by_default: None, mock_psutil: Mock, mock_os: Mock, mock_config_entry: MockConfigEntry, diff --git a/tests/components/systemmonitor/test_repairs.py b/tests/components/systemmonitor/test_repairs.py index d054bfa99a4..6c1ff9dfd16 100644 --- a/tests/components/systemmonitor/test_repairs.py +++ b/tests/components/systemmonitor/test_repairs.py @@ -5,6 +5,7 @@ from __future__ import annotations from http import HTTPStatus from unittest.mock import Mock +import pytest from syrupy.assertion import SnapshotAssertion from homeassistant.components.repairs.websocket_api import ( @@ -22,10 +23,10 @@ from tests.common import ANY, MockConfigEntry from tests.typing import ClientSessionGenerator, WebSocketGenerator +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_migrate_process_sensor( hass: HomeAssistant, entity_registry: er.EntityRegistry, - entity_registry_enabled_by_default: None, mock_psutil: Mock, mock_os: Mock, hass_client: ClientSessionGenerator, @@ -120,11 +121,11 @@ async def test_migrate_process_sensor( assert hass.config_entries.async_entries(DOMAIN) == snapshot(name="after_migration") +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_other_fixable_issues( hass: HomeAssistant, hass_client: ClientSessionGenerator, hass_ws_client: WebSocketGenerator, - entity_registry_enabled_by_default: None, mock_added_config_entry: ConfigEntry, ) -> None: """Test fixing other issues.""" diff --git a/tests/components/systemmonitor/test_sensor.py b/tests/components/systemmonitor/test_sensor.py index a11112d8f86..8f0f316b5f8 100644 --- a/tests/components/systemmonitor/test_sensor.py +++ b/tests/components/systemmonitor/test_sensor.py @@ -24,9 +24,9 @@ from .conftest import MockProcess from tests.common import MockConfigEntry, async_fire_time_changed +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_sensor( hass: HomeAssistant, - entity_registry_enabled_by_default: None, mock_psutil: Mock, mock_os: Mock, entity_registry: er.EntityRegistry, @@ -76,9 +76,9 @@ async def test_sensor( assert state.attributes == snapshot(name=f"{state.name} - attributes") +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_process_sensor_not_loaded( hass: HomeAssistant, - entity_registry_enabled_by_default: None, mock_psutil: Mock, mock_os: Mock, entity_registry: er.EntityRegistry, @@ -108,9 +108,9 @@ async def test_process_sensor_not_loaded( assert process_sensor is None +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_sensor_not_loading_veth_networks( hass: HomeAssistant, - entity_registry_enabled_by_default: None, mock_added_config_entry: ConfigEntry, ) -> None: """Test the sensor.""" @@ -123,9 +123,9 @@ async def test_sensor_not_loading_veth_networks( assert network_sensor_2 is None +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_sensor_icon( hass: HomeAssistant, - entity_registry_enabled_by_default: None, mock_psutil: Mock, mock_os: Mock, mock_config_entry: MockConfigEntry, @@ -142,9 +142,9 @@ async def test_sensor_icon( assert get_cpu_icon() == "mdi:cpu-64-bit" +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_sensor_yaml( hass: HomeAssistant, - entity_registry_enabled_by_default: None, mock_psutil: Mock, mock_os: Mock, ) -> None: @@ -172,10 +172,10 @@ async def test_sensor_yaml( assert process_sensor.state == STATE_ON +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_sensor_yaml_fails_missing_argument( caplog: pytest.LogCaptureFixture, hass: HomeAssistant, - entity_registry_enabled_by_default: None, mock_psutil: Mock, mock_os: Mock, ) -> None: @@ -302,10 +302,10 @@ async def test_sensor_process_fails( assert "Failed to load process with ID: 1, old name: python3" in caplog.text +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_sensor_network_sensors( freezer: FrozenDateTimeFactory, hass: HomeAssistant, - entity_registry_enabled_by_default: None, mock_added_config_entry: ConfigEntry, mock_psutil: Mock, ) -> None: @@ -378,9 +378,9 @@ async def test_sensor_network_sensors( assert throughput_network_out_sensor.state == STATE_UNKNOWN +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_missing_cpu_temperature( hass: HomeAssistant, - entity_registry_enabled_by_default: None, mock_psutil: Mock, mock_os: Mock, mock_config_entry: MockConfigEntry, @@ -402,9 +402,9 @@ async def test_missing_cpu_temperature( assert temp_sensor is None +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_processor_temperature( hass: HomeAssistant, - entity_registry_enabled_by_default: None, mock_psutil: Mock, mock_os: Mock, mock_config_entry: MockConfigEntry, @@ -452,9 +452,9 @@ async def test_processor_temperature( await hass.async_block_till_done() +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_exception_handling_disk_sensor( hass: HomeAssistant, - entity_registry_enabled_by_default: None, mock_psutil: Mock, mock_added_config_entry: ConfigEntry, caplog: pytest.LogCaptureFixture, @@ -511,9 +511,9 @@ async def test_exception_handling_disk_sensor( assert disk_sensor.attributes["unit_of_measurement"] == "%" +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_cpu_percentage_is_zero_returns_unknown( hass: HomeAssistant, - entity_registry_enabled_by_default: None, mock_psutil: Mock, mock_added_config_entry: ConfigEntry, caplog: pytest.LogCaptureFixture, diff --git a/tests/components/systemmonitor/test_util.py b/tests/components/systemmonitor/test_util.py index 439ec88361b..b35c7b2e96c 100644 --- a/tests/components/systemmonitor/test_util.py +++ b/tests/components/systemmonitor/test_util.py @@ -17,9 +17,9 @@ from tests.common import MockConfigEntry (OSError("OS error"), "was excluded because of: OS error"), ], ) +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_disk_setup_failure( hass: HomeAssistant, - entity_registry_enabled_by_default: None, mock_psutil: Mock, mock_os: Mock, mock_config_entry: MockConfigEntry, @@ -40,9 +40,9 @@ async def test_disk_setup_failure( assert error_text in caplog.text +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_disk_util( hass: HomeAssistant, - entity_registry_enabled_by_default: None, mock_psutil: Mock, mock_os: Mock, mock_config_entry: MockConfigEntry, diff --git a/tests/components/trafikverket_camera/test_binary_sensor.py b/tests/components/trafikverket_camera/test_binary_sensor.py index ffdb5b44813..6c694f76233 100644 --- a/tests/components/trafikverket_camera/test_binary_sensor.py +++ b/tests/components/trafikverket_camera/test_binary_sensor.py @@ -2,6 +2,7 @@ from __future__ import annotations +import pytest from pytrafikverket.trafikverket_camera import CameraInfo from homeassistant.config_entries import ConfigEntry @@ -9,9 +10,9 @@ from homeassistant.const import STATE_ON from homeassistant.core import HomeAssistant +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_sensor( hass: HomeAssistant, - entity_registry_enabled_by_default: None, load_int: ConfigEntry, get_camera: CameraInfo, ) -> None: diff --git a/tests/components/trafikverket_camera/test_recorder.py b/tests/components/trafikverket_camera/test_recorder.py index 83645f141fa..23ebd3f2189 100644 --- a/tests/components/trafikverket_camera/test_recorder.py +++ b/tests/components/trafikverket_camera/test_recorder.py @@ -15,9 +15,9 @@ from tests.components.recorder.common import async_wait_recording_done from tests.test_util.aiohttp import AiohttpClientMocker +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_exclude_attributes( recorder_mock: Recorder, - entity_registry_enabled_by_default: None, hass: HomeAssistant, load_int: ConfigEntry, monkeypatch: pytest.MonkeyPatch, diff --git a/tests/components/trafikverket_camera/test_sensor.py b/tests/components/trafikverket_camera/test_sensor.py index 9d357bbd0ca..18ccbe56070 100644 --- a/tests/components/trafikverket_camera/test_sensor.py +++ b/tests/components/trafikverket_camera/test_sensor.py @@ -2,15 +2,16 @@ from __future__ import annotations +import pytest from pytrafikverket.trafikverket_camera import CameraInfo from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_sensor( hass: HomeAssistant, - entity_registry_enabled_by_default: None, load_int: ConfigEntry, get_camera: CameraInfo, ) -> None: diff --git a/tests/components/trafikverket_ferry/test_coordinator.py b/tests/components/trafikverket_ferry/test_coordinator.py index 6ac4eaa3a78..ef6329bfd82 100644 --- a/tests/components/trafikverket_ferry/test_coordinator.py +++ b/tests/components/trafikverket_ferry/test_coordinator.py @@ -22,9 +22,9 @@ from . import ENTRY_CONFIG from tests.common import MockConfigEntry, async_fire_time_changed +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_coordinator( hass: HomeAssistant, - entity_registry_enabled_by_default: None, freezer: FrozenDateTimeFactory, monkeypatch: pytest.MonkeyPatch, get_ferries: list[FerryStop], diff --git a/tests/components/trafikverket_train/test_sensor.py b/tests/components/trafikverket_train/test_sensor.py index 099bcf5ae1e..f21561dd287 100644 --- a/tests/components/trafikverket_train/test_sensor.py +++ b/tests/components/trafikverket_train/test_sensor.py @@ -6,6 +6,7 @@ from datetime import timedelta from unittest.mock import patch from freezegun.api import FrozenDateTimeFactory +import pytest from pytrafikverket.exceptions import InvalidAuthentication, NoTrainAnnouncementFound from pytrafikverket.trafikverket_train import TrainStop from syrupy.assertion import SnapshotAssertion @@ -17,10 +18,10 @@ from homeassistant.core import HomeAssistant from tests.common import async_fire_time_changed +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_sensor_next( hass: HomeAssistant, freezer: FrozenDateTimeFactory, - entity_registry_enabled_by_default: None, load_int: ConfigEntry, get_trains_next: list[TrainStop], get_train_stop: TrainStop, @@ -64,10 +65,10 @@ async def test_sensor_next( assert state == snapshot +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_sensor_single_stop( hass: HomeAssistant, freezer: FrozenDateTimeFactory, - entity_registry_enabled_by_default: None, load_int: ConfigEntry, get_trains_next: list[TrainStop], snapshot: SnapshotAssertion, @@ -80,10 +81,10 @@ async def test_sensor_single_stop( assert state == snapshot +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_sensor_update_auth_failure( hass: HomeAssistant, freezer: FrozenDateTimeFactory, - entity_registry_enabled_by_default: None, load_int: ConfigEntry, get_trains_next: list[TrainStop], snapshot: SnapshotAssertion, @@ -113,10 +114,10 @@ async def test_sensor_update_auth_failure( assert flow == snapshot +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_sensor_update_failure( hass: HomeAssistant, freezer: FrozenDateTimeFactory, - entity_registry_enabled_by_default: None, load_int: ConfigEntry, get_trains_next: list[TrainStop], snapshot: SnapshotAssertion, @@ -143,10 +144,10 @@ async def test_sensor_update_failure( assert state.state == STATE_UNAVAILABLE +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_sensor_update_failure_no_state( hass: HomeAssistant, freezer: FrozenDateTimeFactory, - entity_registry_enabled_by_default: None, load_int: ConfigEntry, get_trains_next: list[TrainStop], snapshot: SnapshotAssertion, diff --git a/tests/components/unifi/test_sensor.py b/tests/components/unifi/test_sensor.py index 26eadfa498e..879de19bfe0 100644 --- a/tests/components/unifi/test_sensor.py +++ b/tests/components/unifi/test_sensor.py @@ -458,13 +458,13 @@ async def test_bandwidth_sensors( (60, 64, 60), ], ) +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_uptime_sensors( hass: HomeAssistant, entity_registry: er.EntityRegistry, aioclient_mock: AiohttpClientMocker, freezer: FrozenDateTimeFactory, mock_unifi_websocket, - entity_registry_enabled_by_default: None, initial_uptime, event_uptime, new_uptime, @@ -545,11 +545,11 @@ async def test_uptime_sensors( assert hass.states.get("sensor.client1_uptime") +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_remove_sensors( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, mock_unifi_websocket, - entity_registry_enabled_by_default: None, ) -> None: """Verify removing of clients work as expected.""" wired_client = { diff --git a/tests/components/unifiprotect/test_sensor.py b/tests/components/unifiprotect/test_sensor.py index e593f224378..5e70238519d 100644 --- a/tests/components/unifiprotect/test_sensor.py +++ b/tests/components/unifiprotect/test_sensor.py @@ -5,6 +5,7 @@ from __future__ import annotations from datetime import datetime, timedelta from unittest.mock import Mock +import pytest from pyunifiprotect.data import ( NVR, Camera, @@ -399,10 +400,10 @@ async def test_sensor_setup_camera( assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_sensor_setup_camera_with_last_trip_time( hass: HomeAssistant, entity_registry: er.EntityRegistry, - entity_registry_enabled_by_default: None, ufp: MockUFPFixture, doorbell: Camera, fixed_now: datetime, @@ -474,10 +475,10 @@ async def test_sensor_update_alarm( await time_changed(hass, 10) +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_sensor_update_alarm_with_last_trip_time( hass: HomeAssistant, entity_registry: er.EntityRegistry, - entity_registry_enabled_by_default: None, ufp: MockUFPFixture, sensor_all: Sensor, fixed_now: datetime, diff --git a/tests/components/v2c/test_sensor.py b/tests/components/v2c/test_sensor.py index 93f7e36327c..4be62d02bd5 100644 --- a/tests/components/v2c/test_sensor.py +++ b/tests/components/v2c/test_sensor.py @@ -2,6 +2,7 @@ from unittest.mock import AsyncMock, patch +import pytest from syrupy import SnapshotAssertion from homeassistant.const import Platform @@ -13,13 +14,13 @@ from . import init_integration from tests.common import MockConfigEntry, snapshot_platform +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_sensor( hass: HomeAssistant, entity_registry: er.EntityRegistry, snapshot: SnapshotAssertion, mock_v2c_client: AsyncMock, mock_config_entry: MockConfigEntry, - entity_registry_enabled_by_default: None, ) -> None: """Test states of the sensor.""" with patch("homeassistant.components.v2c.PLATFORMS", [Platform.SENSOR]): From 1877c1eec909698af3c2c6b7a7785d5b7e72aa1a Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 4 Jun 2024 13:40:06 +0200 Subject: [PATCH 0204/1445] Make Ruuvi a brand (#118786) --- homeassistant/brands/ruuvi.json | 5 +++++ homeassistant/generated/integrations.json | 27 ++++++++++++++--------- 2 files changed, 21 insertions(+), 11 deletions(-) create mode 100644 homeassistant/brands/ruuvi.json diff --git a/homeassistant/brands/ruuvi.json b/homeassistant/brands/ruuvi.json new file mode 100644 index 00000000000..b174424c13c --- /dev/null +++ b/homeassistant/brands/ruuvi.json @@ -0,0 +1,5 @@ +{ + "domain": "ruuvi", + "name": "Ruuvi", + "integrations": ["ruuvi_gateway", "ruuvitag_ble"] +} diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 578f2631b25..8c5d7f0d9e6 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -5160,17 +5160,22 @@ } } }, - "ruuvi_gateway": { - "name": "Ruuvi Gateway", - "integration_type": "hub", - "config_flow": true, - "iot_class": "local_polling" - }, - "ruuvitag_ble": { - "name": "RuuviTag BLE", - "integration_type": "hub", - "config_flow": true, - "iot_class": "local_push" + "ruuvi": { + "name": "Ruuvi", + "integrations": { + "ruuvi_gateway": { + "integration_type": "hub", + "config_flow": true, + "iot_class": "local_polling", + "name": "Ruuvi Gateway" + }, + "ruuvitag_ble": { + "integration_type": "hub", + "config_flow": true, + "iot_class": "local_push", + "name": "RuuviTag BLE" + } + } }, "rympro": { "name": "Read Your Meter Pro", From 0aac4b26a4a1eb3667add7cfc9be6d84244b4e9b Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 4 Jun 2024 13:40:48 +0200 Subject: [PATCH 0205/1445] Make Weatherflow a brand (#118785) --- homeassistant/brands/weatherflow.json | 5 +++++ homeassistant/generated/integrations.json | 23 ++++++++++++++--------- 2 files changed, 19 insertions(+), 9 deletions(-) create mode 100644 homeassistant/brands/weatherflow.json diff --git a/homeassistant/brands/weatherflow.json b/homeassistant/brands/weatherflow.json new file mode 100644 index 00000000000..e1043c88b9b --- /dev/null +++ b/homeassistant/brands/weatherflow.json @@ -0,0 +1,5 @@ +{ + "domain": "weatherflow", + "name": "WeatherFlow", + "integrations": ["weatherflow", "weatherflow_cloud"] +} diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 8c5d7f0d9e6..cc949dec3c4 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -6755,15 +6755,20 @@ }, "weatherflow": { "name": "WeatherFlow", - "integration_type": "hub", - "config_flow": true, - "iot_class": "local_push" - }, - "weatherflow_cloud": { - "name": "WeatherflowCloud", - "integration_type": "hub", - "config_flow": true, - "iot_class": "cloud_polling" + "integrations": { + "weatherflow": { + "integration_type": "hub", + "config_flow": true, + "iot_class": "local_push", + "name": "WeatherFlow" + }, + "weatherflow_cloud": { + "integration_type": "hub", + "config_flow": true, + "iot_class": "cloud_polling", + "name": "WeatherflowCloud" + } + } }, "webhook": { "name": "Webhook", From 67e9e903464bb0383030fbaa0e71cfef8ef28270 Mon Sep 17 00:00:00 2001 From: Markus Jacobsen Date: Tue, 4 Jun 2024 13:48:22 +0200 Subject: [PATCH 0206/1445] Bang & Olufsen add overlay/announce play_media functionality (#113434) * Add overlay service * Convert custom service to play_media announce * Remove debugging --- .../components/bang_olufsen/const.py | 2 + .../components/bang_olufsen/media_player.py | 41 ++++++++++++++++++- 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/bang_olufsen/const.py b/homeassistant/components/bang_olufsen/const.py index 4d53daeb510..91429d0f9b0 100644 --- a/homeassistant/components/bang_olufsen/const.py +++ b/homeassistant/components/bang_olufsen/const.py @@ -55,6 +55,7 @@ class BangOlufsenMediaType(StrEnum): DEEZER = "deezer" RADIO = "radio" TTS = "provider" + OVERLAY_TTS = "overlay_tts" class BangOlufsenModel(StrEnum): @@ -117,6 +118,7 @@ VALID_MEDIA_TYPES: Final[tuple] = ( BangOlufsenMediaType.DEEZER, BangOlufsenMediaType.RADIO, BangOlufsenMediaType.TTS, + BangOlufsenMediaType.OVERLAY_TTS, MediaType.MUSIC, MediaType.URL, MediaType.CHANNEL, diff --git a/homeassistant/components/bang_olufsen/media_player.py b/homeassistant/components/bang_olufsen/media_player.py index 9d4cd81f5cb..0ce8cd22249 100644 --- a/homeassistant/components/bang_olufsen/media_player.py +++ b/homeassistant/components/bang_olufsen/media_player.py @@ -12,6 +12,7 @@ from mozart_api.models import ( Action, Art, OverlayPlayRequest, + OverlayPlayRequestTextToSpeechTextToSpeech, PlaybackContentMetadata, PlaybackError, PlaybackProgress, @@ -69,6 +70,7 @@ _LOGGER = logging.getLogger(__name__) BANG_OLUFSEN_FEATURES = ( MediaPlayerEntityFeature.BROWSE_MEDIA | MediaPlayerEntityFeature.CLEAR_PLAYLIST + | MediaPlayerEntityFeature.MEDIA_ANNOUNCE | MediaPlayerEntityFeature.NEXT_TRACK | MediaPlayerEntityFeature.PAUSE | MediaPlayerEntityFeature.PLAY @@ -547,10 +549,10 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity): self, media_type: MediaType | str, media_id: str, + announce: bool | None = None, **kwargs: Any, ) -> None: """Play from: netradio station id, URI, favourite or Deezer.""" - # Convert audio/mpeg, audio/aac etc. to MediaType.MUSIC if media_type.startswith("audio/"): media_type = MediaType.MUSIC @@ -574,7 +576,42 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity): if media_id.endswith(".m3u"): media_id = media_id.replace(".m3u", "") - if media_type in (MediaType.URL, MediaType.MUSIC): + if announce: + extra = kwargs.get(ATTR_MEDIA_EXTRA, {}) + + absolute_volume = extra.get("overlay_absolute_volume", None) + offset_volume = extra.get("overlay_offset_volume", None) + tts_language = extra.get("overlay_tts_language", "en-us") + + # Construct request + overlay_play_request = OverlayPlayRequest() + + # Define volume level + if absolute_volume: + overlay_play_request.volume_absolute = absolute_volume + + elif offset_volume: + # Ensure that the volume is not above 100 + if not self._volume.level or not self._volume.level.level: + _LOGGER.warning("Error setting volume") + else: + overlay_play_request.volume_absolute = min( + self._volume.level.level + offset_volume, 100 + ) + + if media_type == BangOlufsenMediaType.OVERLAY_TTS: + # Bang & Olufsen cloud TTS + overlay_play_request.text_to_speech = ( + OverlayPlayRequestTextToSpeechTextToSpeech( + lang=tts_language, text=media_id + ) + ) + else: + overlay_play_request.uri = Uri(location=media_id) + + await self._client.post_overlay_play(overlay_play_request) + + elif media_type in (MediaType.URL, MediaType.MUSIC): await self._client.post_uri_source(uri=Uri(location=media_id)) # The "provider" media_type may not be suitable for overlay all the time. From 1eb13b48a2c894b47d6d0b1f5fd45c1e55ae71f9 Mon Sep 17 00:00:00 2001 From: Richard Kroegel <42204099+rikroe@users.noreply.github.com> Date: Tue, 4 Jun 2024 15:08:15 +0200 Subject: [PATCH 0207/1445] Add tests for BMW binary_sensor and lock (#118436) * BMW: Add tests for binary_sensor & lock * Use entity_registry_enabled_by_default fixture * Update tests/components/bmw_connected_drive/test_binary_sensor.py Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> * Move fixtures to decorator Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> * Use fixture decorators if possible * Fix rebase * Spelling adjustments Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> * Use snapshot_platform helper * Spelling * Remove comment --------- Co-authored-by: Richard Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> --- .coveragerc | 5 - .../snapshots/test_binary_sensor.ambr | 1523 +++++++++++++++++ .../snapshots/test_lock.ambr | 205 +++ .../bmw_connected_drive/test_binary_sensor.py | 35 + .../bmw_connected_drive/test_lock.py | 139 ++ 5 files changed, 1902 insertions(+), 5 deletions(-) create mode 100644 tests/components/bmw_connected_drive/snapshots/test_binary_sensor.ambr create mode 100644 tests/components/bmw_connected_drive/snapshots/test_lock.ambr create mode 100644 tests/components/bmw_connected_drive/test_binary_sensor.py create mode 100644 tests/components/bmw_connected_drive/test_lock.py diff --git a/.coveragerc b/.coveragerc index e556d0aab85..034598d2044 100644 --- a/.coveragerc +++ b/.coveragerc @@ -149,12 +149,7 @@ omit = homeassistant/components/bloomsky/* homeassistant/components/bluesound/* homeassistant/components/bluetooth_tracker/* - homeassistant/components/bmw_connected_drive/__init__.py - homeassistant/components/bmw_connected_drive/binary_sensor.py - homeassistant/components/bmw_connected_drive/coordinator.py - homeassistant/components/bmw_connected_drive/lock.py homeassistant/components/bmw_connected_drive/notify.py - homeassistant/components/bmw_connected_drive/sensor.py homeassistant/components/bosch_shc/__init__.py homeassistant/components/bosch_shc/binary_sensor.py homeassistant/components/bosch_shc/cover.py diff --git a/tests/components/bmw_connected_drive/snapshots/test_binary_sensor.ambr b/tests/components/bmw_connected_drive/snapshots/test_binary_sensor.ambr new file mode 100644 index 00000000000..610e194c0e5 --- /dev/null +++ b/tests/components/bmw_connected_drive/snapshots/test_binary_sensor.ambr @@ -0,0 +1,1523 @@ +# serializer version: 1 +# name: test_entity_state_attrs[binary_sensor.i3_rex_charging_status-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.i3_rex_charging_status', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Charging status', + 'platform': 'bmw_connected_drive', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'charging_status', + 'unique_id': 'WBY00000000REXI01-charging_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_state_attrs[binary_sensor.i3_rex_charging_status-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'device_class': 'battery_charging', + 'friendly_name': 'i3 (+ REX) Charging status', + }), + 'context': , + 'entity_id': 'binary_sensor.i3_rex_charging_status', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_entity_state_attrs[binary_sensor.i3_rex_check_control_messages-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.i3_rex_check_control_messages', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Check control messages', + 'platform': 'bmw_connected_drive', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'check_control_messages', + 'unique_id': 'WBY00000000REXI01-check_control_messages', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_state_attrs[binary_sensor.i3_rex_check_control_messages-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'car': 'i3 (+ REX)', + 'device_class': 'problem', + 'friendly_name': 'i3 (+ REX) Check control messages', + 'vin': 'WBY00000000REXI01', + }), + 'context': , + 'entity_id': 'binary_sensor.i3_rex_check_control_messages', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_entity_state_attrs[binary_sensor.i3_rex_condition_based_services-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.i3_rex_condition_based_services', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Condition based services', + 'platform': 'bmw_connected_drive', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'condition_based_services', + 'unique_id': 'WBY00000000REXI01-condition_based_services', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_state_attrs[binary_sensor.i3_rex_condition_based_services-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'brake_fluid': 'OK', + 'brake_fluid_date': '2022-10-01', + 'car': 'i3 (+ REX)', + 'device_class': 'problem', + 'friendly_name': 'i3 (+ REX) Condition based services', + 'vehicle_check': 'OK', + 'vehicle_check_date': '2023-05-01', + 'vehicle_tuv': 'OK', + 'vehicle_tuv_date': '2023-05-01', + 'vin': 'WBY00000000REXI01', + }), + 'context': , + 'entity_id': 'binary_sensor.i3_rex_condition_based_services', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_entity_state_attrs[binary_sensor.i3_rex_connection_status-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.i3_rex_connection_status', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Connection status', + 'platform': 'bmw_connected_drive', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'connection_status', + 'unique_id': 'WBY00000000REXI01-connection_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_state_attrs[binary_sensor.i3_rex_connection_status-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'device_class': 'plug', + 'friendly_name': 'i3 (+ REX) Connection status', + }), + 'context': , + 'entity_id': 'binary_sensor.i3_rex_connection_status', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity_state_attrs[binary_sensor.i3_rex_door_lock_state-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.i3_rex_door_lock_state', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Door lock state', + 'platform': 'bmw_connected_drive', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'door_lock_state', + 'unique_id': 'WBY00000000REXI01-door_lock_state', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_state_attrs[binary_sensor.i3_rex_door_lock_state-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'car': 'i3 (+ REX)', + 'device_class': 'lock', + 'door_lock_state': 'UNLOCKED', + 'friendly_name': 'i3 (+ REX) Door lock state', + 'vin': 'WBY00000000REXI01', + }), + 'context': , + 'entity_id': 'binary_sensor.i3_rex_door_lock_state', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity_state_attrs[binary_sensor.i3_rex_lids-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.i3_rex_lids', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Lids', + 'platform': 'bmw_connected_drive', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lids', + 'unique_id': 'WBY00000000REXI01-lids', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_state_attrs[binary_sensor.i3_rex_lids-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'car': 'i3 (+ REX)', + 'device_class': 'opening', + 'friendly_name': 'i3 (+ REX) Lids', + 'hood': 'CLOSED', + 'leftFront': 'CLOSED', + 'leftRear': 'CLOSED', + 'rightFront': 'CLOSED', + 'rightRear': 'CLOSED', + 'sunRoof': 'CLOSED', + 'trunk': 'CLOSED', + 'vin': 'WBY00000000REXI01', + }), + 'context': , + 'entity_id': 'binary_sensor.i3_rex_lids', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_entity_state_attrs[binary_sensor.i3_rex_pre_entry_climatization-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.i3_rex_pre_entry_climatization', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Pre entry climatization', + 'platform': 'bmw_connected_drive', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'is_pre_entry_climatization_enabled', + 'unique_id': 'WBY00000000REXI01-is_pre_entry_climatization_enabled', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_state_attrs[binary_sensor.i3_rex_pre_entry_climatization-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'friendly_name': 'i3 (+ REX) Pre entry climatization', + }), + 'context': , + 'entity_id': 'binary_sensor.i3_rex_pre_entry_climatization', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_entity_state_attrs[binary_sensor.i3_rex_windows-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.i3_rex_windows', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Windows', + 'platform': 'bmw_connected_drive', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'windows', + 'unique_id': 'WBY00000000REXI01-windows', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_state_attrs[binary_sensor.i3_rex_windows-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'car': 'i3 (+ REX)', + 'device_class': 'opening', + 'friendly_name': 'i3 (+ REX) Windows', + 'leftFront': 'CLOSED', + 'rightFront': 'CLOSED', + 'vin': 'WBY00000000REXI01', + }), + 'context': , + 'entity_id': 'binary_sensor.i3_rex_windows', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_entity_state_attrs[binary_sensor.i4_edrive40_charging_status-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.i4_edrive40_charging_status', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Charging status', + 'platform': 'bmw_connected_drive', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'charging_status', + 'unique_id': 'WBA00000000DEMO02-charging_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_state_attrs[binary_sensor.i4_edrive40_charging_status-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'device_class': 'battery_charging', + 'friendly_name': 'i4 eDrive40 Charging status', + }), + 'context': , + 'entity_id': 'binary_sensor.i4_edrive40_charging_status', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_entity_state_attrs[binary_sensor.i4_edrive40_check_control_messages-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.i4_edrive40_check_control_messages', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Check control messages', + 'platform': 'bmw_connected_drive', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'check_control_messages', + 'unique_id': 'WBA00000000DEMO02-check_control_messages', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_state_attrs[binary_sensor.i4_edrive40_check_control_messages-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'car': 'i4 eDrive40', + 'device_class': 'problem', + 'friendly_name': 'i4 eDrive40 Check control messages', + 'tire_pressure': 'LOW', + 'vin': 'WBA00000000DEMO02', + }), + 'context': , + 'entity_id': 'binary_sensor.i4_edrive40_check_control_messages', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_entity_state_attrs[binary_sensor.i4_edrive40_condition_based_services-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.i4_edrive40_condition_based_services', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Condition based services', + 'platform': 'bmw_connected_drive', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'condition_based_services', + 'unique_id': 'WBA00000000DEMO02-condition_based_services', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_state_attrs[binary_sensor.i4_edrive40_condition_based_services-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'brake_fluid': 'OK', + 'brake_fluid_date': '2024-12-01', + 'brake_fluid_distance': '50000 km', + 'car': 'i4 eDrive40', + 'device_class': 'problem', + 'friendly_name': 'i4 eDrive40 Condition based services', + 'tire_wear_front': 'OK', + 'tire_wear_rear': 'OK', + 'vehicle_check': 'OK', + 'vehicle_check_date': '2024-12-01', + 'vehicle_check_distance': '50000 km', + 'vehicle_tuv': 'OK', + 'vehicle_tuv_date': '2024-12-01', + 'vehicle_tuv_distance': '50000 km', + 'vin': 'WBA00000000DEMO02', + }), + 'context': , + 'entity_id': 'binary_sensor.i4_edrive40_condition_based_services', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_entity_state_attrs[binary_sensor.i4_edrive40_connection_status-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.i4_edrive40_connection_status', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Connection status', + 'platform': 'bmw_connected_drive', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'connection_status', + 'unique_id': 'WBA00000000DEMO02-connection_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_state_attrs[binary_sensor.i4_edrive40_connection_status-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'device_class': 'plug', + 'friendly_name': 'i4 eDrive40 Connection status', + }), + 'context': , + 'entity_id': 'binary_sensor.i4_edrive40_connection_status', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_entity_state_attrs[binary_sensor.i4_edrive40_door_lock_state-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.i4_edrive40_door_lock_state', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Door lock state', + 'platform': 'bmw_connected_drive', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'door_lock_state', + 'unique_id': 'WBA00000000DEMO02-door_lock_state', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_state_attrs[binary_sensor.i4_edrive40_door_lock_state-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'car': 'i4 eDrive40', + 'device_class': 'lock', + 'door_lock_state': 'LOCKED', + 'friendly_name': 'i4 eDrive40 Door lock state', + 'vin': 'WBA00000000DEMO02', + }), + 'context': , + 'entity_id': 'binary_sensor.i4_edrive40_door_lock_state', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_entity_state_attrs[binary_sensor.i4_edrive40_lids-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.i4_edrive40_lids', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Lids', + 'platform': 'bmw_connected_drive', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lids', + 'unique_id': 'WBA00000000DEMO02-lids', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_state_attrs[binary_sensor.i4_edrive40_lids-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'car': 'i4 eDrive40', + 'device_class': 'opening', + 'friendly_name': 'i4 eDrive40 Lids', + 'hood': 'CLOSED', + 'leftFront': 'CLOSED', + 'leftRear': 'CLOSED', + 'rightFront': 'CLOSED', + 'rightRear': 'CLOSED', + 'trunk': 'CLOSED', + 'vin': 'WBA00000000DEMO02', + }), + 'context': , + 'entity_id': 'binary_sensor.i4_edrive40_lids', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_entity_state_attrs[binary_sensor.i4_edrive40_pre_entry_climatization-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.i4_edrive40_pre_entry_climatization', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Pre entry climatization', + 'platform': 'bmw_connected_drive', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'is_pre_entry_climatization_enabled', + 'unique_id': 'WBA00000000DEMO02-is_pre_entry_climatization_enabled', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_state_attrs[binary_sensor.i4_edrive40_pre_entry_climatization-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'friendly_name': 'i4 eDrive40 Pre entry climatization', + }), + 'context': , + 'entity_id': 'binary_sensor.i4_edrive40_pre_entry_climatization', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_entity_state_attrs[binary_sensor.i4_edrive40_windows-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.i4_edrive40_windows', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Windows', + 'platform': 'bmw_connected_drive', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'windows', + 'unique_id': 'WBA00000000DEMO02-windows', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_state_attrs[binary_sensor.i4_edrive40_windows-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'car': 'i4 eDrive40', + 'device_class': 'opening', + 'friendly_name': 'i4 eDrive40 Windows', + 'leftFront': 'CLOSED', + 'leftRear': 'CLOSED', + 'rear': 'CLOSED', + 'rightFront': 'CLOSED', + 'rightRear': 'CLOSED', + 'vin': 'WBA00000000DEMO02', + }), + 'context': , + 'entity_id': 'binary_sensor.i4_edrive40_windows', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_entity_state_attrs[binary_sensor.ix_xdrive50_charging_status-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.ix_xdrive50_charging_status', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Charging status', + 'platform': 'bmw_connected_drive', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'charging_status', + 'unique_id': 'WBA00000000DEMO01-charging_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_state_attrs[binary_sensor.ix_xdrive50_charging_status-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'device_class': 'battery_charging', + 'friendly_name': 'iX xDrive50 Charging status', + }), + 'context': , + 'entity_id': 'binary_sensor.ix_xdrive50_charging_status', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity_state_attrs[binary_sensor.ix_xdrive50_check_control_messages-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.ix_xdrive50_check_control_messages', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Check control messages', + 'platform': 'bmw_connected_drive', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'check_control_messages', + 'unique_id': 'WBA00000000DEMO01-check_control_messages', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_state_attrs[binary_sensor.ix_xdrive50_check_control_messages-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'car': 'iX xDrive50', + 'device_class': 'problem', + 'friendly_name': 'iX xDrive50 Check control messages', + 'tire_pressure': 'LOW', + 'vin': 'WBA00000000DEMO01', + }), + 'context': , + 'entity_id': 'binary_sensor.ix_xdrive50_check_control_messages', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_entity_state_attrs[binary_sensor.ix_xdrive50_condition_based_services-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.ix_xdrive50_condition_based_services', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Condition based services', + 'platform': 'bmw_connected_drive', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'condition_based_services', + 'unique_id': 'WBA00000000DEMO01-condition_based_services', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_state_attrs[binary_sensor.ix_xdrive50_condition_based_services-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'brake_fluid': 'OK', + 'brake_fluid_date': '2024-12-01', + 'brake_fluid_distance': '50000 km', + 'car': 'iX xDrive50', + 'device_class': 'problem', + 'friendly_name': 'iX xDrive50 Condition based services', + 'tire_wear_front': 'OK', + 'tire_wear_rear': 'OK', + 'vehicle_check': 'OK', + 'vehicle_check_date': '2024-12-01', + 'vehicle_check_distance': '50000 km', + 'vehicle_tuv': 'OK', + 'vehicle_tuv_date': '2024-12-01', + 'vehicle_tuv_distance': '50000 km', + 'vin': 'WBA00000000DEMO01', + }), + 'context': , + 'entity_id': 'binary_sensor.ix_xdrive50_condition_based_services', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_entity_state_attrs[binary_sensor.ix_xdrive50_connection_status-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.ix_xdrive50_connection_status', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Connection status', + 'platform': 'bmw_connected_drive', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'connection_status', + 'unique_id': 'WBA00000000DEMO01-connection_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_state_attrs[binary_sensor.ix_xdrive50_connection_status-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'device_class': 'plug', + 'friendly_name': 'iX xDrive50 Connection status', + }), + 'context': , + 'entity_id': 'binary_sensor.ix_xdrive50_connection_status', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity_state_attrs[binary_sensor.ix_xdrive50_door_lock_state-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.ix_xdrive50_door_lock_state', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Door lock state', + 'platform': 'bmw_connected_drive', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'door_lock_state', + 'unique_id': 'WBA00000000DEMO01-door_lock_state', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_state_attrs[binary_sensor.ix_xdrive50_door_lock_state-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'car': 'iX xDrive50', + 'device_class': 'lock', + 'door_lock_state': 'LOCKED', + 'friendly_name': 'iX xDrive50 Door lock state', + 'vin': 'WBA00000000DEMO01', + }), + 'context': , + 'entity_id': 'binary_sensor.ix_xdrive50_door_lock_state', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_entity_state_attrs[binary_sensor.ix_xdrive50_lids-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.ix_xdrive50_lids', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Lids', + 'platform': 'bmw_connected_drive', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lids', + 'unique_id': 'WBA00000000DEMO01-lids', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_state_attrs[binary_sensor.ix_xdrive50_lids-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'car': 'iX xDrive50', + 'device_class': 'opening', + 'friendly_name': 'iX xDrive50 Lids', + 'hood': 'CLOSED', + 'leftFront': 'CLOSED', + 'leftRear': 'CLOSED', + 'rightFront': 'CLOSED', + 'rightRear': 'CLOSED', + 'sunRoof': 'CLOSED', + 'trunk': 'CLOSED', + 'vin': 'WBA00000000DEMO01', + }), + 'context': , + 'entity_id': 'binary_sensor.ix_xdrive50_lids', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_entity_state_attrs[binary_sensor.ix_xdrive50_pre_entry_climatization-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.ix_xdrive50_pre_entry_climatization', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Pre entry climatization', + 'platform': 'bmw_connected_drive', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'is_pre_entry_climatization_enabled', + 'unique_id': 'WBA00000000DEMO01-is_pre_entry_climatization_enabled', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_state_attrs[binary_sensor.ix_xdrive50_pre_entry_climatization-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'friendly_name': 'iX xDrive50 Pre entry climatization', + }), + 'context': , + 'entity_id': 'binary_sensor.ix_xdrive50_pre_entry_climatization', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_entity_state_attrs[binary_sensor.ix_xdrive50_windows-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.ix_xdrive50_windows', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Windows', + 'platform': 'bmw_connected_drive', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'windows', + 'unique_id': 'WBA00000000DEMO01-windows', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_state_attrs[binary_sensor.ix_xdrive50_windows-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'car': 'iX xDrive50', + 'device_class': 'opening', + 'friendly_name': 'iX xDrive50 Windows', + 'leftFront': 'CLOSED', + 'leftRear': 'CLOSED', + 'rear': 'CLOSED', + 'rightFront': 'CLOSED', + 'rightRear': 'CLOSED', + 'vin': 'WBA00000000DEMO01', + }), + 'context': , + 'entity_id': 'binary_sensor.ix_xdrive50_windows', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_entity_state_attrs[binary_sensor.m340i_xdrive_check_control_messages-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.m340i_xdrive_check_control_messages', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Check control messages', + 'platform': 'bmw_connected_drive', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'check_control_messages', + 'unique_id': 'WBA00000000DEMO03-check_control_messages', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_state_attrs[binary_sensor.m340i_xdrive_check_control_messages-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'car': 'M340i xDrive', + 'device_class': 'problem', + 'engine_oil': 'LOW', + 'friendly_name': 'M340i xDrive Check control messages', + 'tire_pressure': 'LOW', + 'vin': 'WBA00000000DEMO03', + }), + 'context': , + 'entity_id': 'binary_sensor.m340i_xdrive_check_control_messages', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_entity_state_attrs[binary_sensor.m340i_xdrive_condition_based_services-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.m340i_xdrive_condition_based_services', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Condition based services', + 'platform': 'bmw_connected_drive', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'condition_based_services', + 'unique_id': 'WBA00000000DEMO03-condition_based_services', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_state_attrs[binary_sensor.m340i_xdrive_condition_based_services-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'brake_fluid': 'OK', + 'brake_fluid_date': '2024-12-01', + 'brake_fluid_distance': '50000 km', + 'car': 'M340i xDrive', + 'device_class': 'problem', + 'friendly_name': 'M340i xDrive Condition based services', + 'oil': 'OK', + 'oil_date': '2024-12-01', + 'oil_distance': '50000 km', + 'tire_wear_front': 'OK', + 'tire_wear_rear': 'OK', + 'vehicle_check': 'OK', + 'vehicle_check_date': '2024-12-01', + 'vehicle_check_distance': '50000 km', + 'vehicle_tuv': 'OK', + 'vehicle_tuv_date': '2024-12-01', + 'vehicle_tuv_distance': '50000 km', + 'vin': 'WBA00000000DEMO03', + }), + 'context': , + 'entity_id': 'binary_sensor.m340i_xdrive_condition_based_services', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_entity_state_attrs[binary_sensor.m340i_xdrive_door_lock_state-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.m340i_xdrive_door_lock_state', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Door lock state', + 'platform': 'bmw_connected_drive', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'door_lock_state', + 'unique_id': 'WBA00000000DEMO03-door_lock_state', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_state_attrs[binary_sensor.m340i_xdrive_door_lock_state-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'car': 'M340i xDrive', + 'device_class': 'lock', + 'door_lock_state': 'LOCKED', + 'friendly_name': 'M340i xDrive Door lock state', + 'vin': 'WBA00000000DEMO03', + }), + 'context': , + 'entity_id': 'binary_sensor.m340i_xdrive_door_lock_state', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_entity_state_attrs[binary_sensor.m340i_xdrive_lids-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.m340i_xdrive_lids', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Lids', + 'platform': 'bmw_connected_drive', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lids', + 'unique_id': 'WBA00000000DEMO03-lids', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_state_attrs[binary_sensor.m340i_xdrive_lids-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'car': 'M340i xDrive', + 'device_class': 'opening', + 'friendly_name': 'M340i xDrive Lids', + 'hood': 'CLOSED', + 'leftFront': 'CLOSED', + 'leftRear': 'CLOSED', + 'rightFront': 'CLOSED', + 'rightRear': 'CLOSED', + 'trunk': 'CLOSED', + 'vin': 'WBA00000000DEMO03', + }), + 'context': , + 'entity_id': 'binary_sensor.m340i_xdrive_lids', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_entity_state_attrs[binary_sensor.m340i_xdrive_windows-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.m340i_xdrive_windows', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Windows', + 'platform': 'bmw_connected_drive', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'windows', + 'unique_id': 'WBA00000000DEMO03-windows', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_state_attrs[binary_sensor.m340i_xdrive_windows-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'car': 'M340i xDrive', + 'device_class': 'opening', + 'friendly_name': 'M340i xDrive Windows', + 'leftFront': 'CLOSED', + 'leftRear': 'CLOSED', + 'rear': 'CLOSED', + 'rightFront': 'CLOSED', + 'rightRear': 'CLOSED', + 'vin': 'WBA00000000DEMO03', + }), + 'context': , + 'entity_id': 'binary_sensor.m340i_xdrive_windows', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- diff --git a/tests/components/bmw_connected_drive/snapshots/test_lock.ambr b/tests/components/bmw_connected_drive/snapshots/test_lock.ambr new file mode 100644 index 00000000000..17e6b118011 --- /dev/null +++ b/tests/components/bmw_connected_drive/snapshots/test_lock.ambr @@ -0,0 +1,205 @@ +# serializer version: 1 +# name: test_entity_state_attrs[lock.i3_rex_lock-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'lock', + 'entity_category': None, + 'entity_id': 'lock.i3_rex_lock', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Lock', + 'platform': 'bmw_connected_drive', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lock', + 'unique_id': 'WBY00000000REXI01-lock', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_state_attrs[lock.i3_rex_lock-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'car': 'i3 (+ REX)', + 'door_lock_state': 'UNLOCKED', + 'friendly_name': 'i3 (+ REX) Lock', + 'supported_features': , + 'vin': 'WBY00000000REXI01', + }), + 'context': , + 'entity_id': 'lock.i3_rex_lock', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unlocked', + }) +# --- +# name: test_entity_state_attrs[lock.i4_edrive40_lock-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'lock', + 'entity_category': None, + 'entity_id': 'lock.i4_edrive40_lock', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Lock', + 'platform': 'bmw_connected_drive', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lock', + 'unique_id': 'WBA00000000DEMO02-lock', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_state_attrs[lock.i4_edrive40_lock-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'car': 'i4 eDrive40', + 'door_lock_state': 'LOCKED', + 'friendly_name': 'i4 eDrive40 Lock', + 'supported_features': , + 'vin': 'WBA00000000DEMO02', + }), + 'context': , + 'entity_id': 'lock.i4_edrive40_lock', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'locked', + }) +# --- +# name: test_entity_state_attrs[lock.ix_xdrive50_lock-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'lock', + 'entity_category': None, + 'entity_id': 'lock.ix_xdrive50_lock', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Lock', + 'platform': 'bmw_connected_drive', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lock', + 'unique_id': 'WBA00000000DEMO01-lock', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_state_attrs[lock.ix_xdrive50_lock-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'car': 'iX xDrive50', + 'door_lock_state': 'LOCKED', + 'friendly_name': 'iX xDrive50 Lock', + 'supported_features': , + 'vin': 'WBA00000000DEMO01', + }), + 'context': , + 'entity_id': 'lock.ix_xdrive50_lock', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'locked', + }) +# --- +# name: test_entity_state_attrs[lock.m340i_xdrive_lock-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'lock', + 'entity_category': None, + 'entity_id': 'lock.m340i_xdrive_lock', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Lock', + 'platform': 'bmw_connected_drive', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lock', + 'unique_id': 'WBA00000000DEMO03-lock', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_state_attrs[lock.m340i_xdrive_lock-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'car': 'M340i xDrive', + 'door_lock_state': 'LOCKED', + 'friendly_name': 'M340i xDrive Lock', + 'supported_features': , + 'vin': 'WBA00000000DEMO03', + }), + 'context': , + 'entity_id': 'lock.m340i_xdrive_lock', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'locked', + }) +# --- diff --git a/tests/components/bmw_connected_drive/test_binary_sensor.py b/tests/components/bmw_connected_drive/test_binary_sensor.py new file mode 100644 index 00000000000..a1b3d69bbbf --- /dev/null +++ b/tests/components/bmw_connected_drive/test_binary_sensor.py @@ -0,0 +1,35 @@ +"""Test BMW binary sensors.""" + +from unittest.mock import patch + +from freezegun import freeze_time +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import setup_mocked_integration + +from tests.common import snapshot_platform + + +@freeze_time("2023-06-22 10:30:00+00:00") +@pytest.mark.usefixtures("bmw_fixture") +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_entity_state_attrs( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, +) -> None: + """Test binary sensor states and attributes.""" + + # Setup component + with patch( + "homeassistant.components.bmw_connected_drive.PLATFORMS", + [Platform.BINARY_SENSOR], + ): + mock_config_entry = await setup_mocked_integration(hass) + + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) diff --git a/tests/components/bmw_connected_drive/test_lock.py b/tests/components/bmw_connected_drive/test_lock.py new file mode 100644 index 00000000000..2fa694d426b --- /dev/null +++ b/tests/components/bmw_connected_drive/test_lock.py @@ -0,0 +1,139 @@ +"""Test BMW locks.""" + +from unittest.mock import AsyncMock, patch + +from bimmer_connected.models import MyBMWRemoteServiceError +from bimmer_connected.vehicle.remote_services import RemoteServices +from freezegun import freeze_time +import pytest +import respx +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.recorder.history import get_significant_states +from homeassistant.const import STATE_UNKNOWN, Platform +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import entity_registry as er +from homeassistant.util import dt as dt_util + +from . import check_remote_service_call, setup_mocked_integration + +from tests.common import snapshot_platform +from tests.components.recorder.common import async_wait_recording_done + + +@freeze_time("2023-06-22 10:30:00+00:00") +@pytest.mark.usefixtures("bmw_fixture") +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_entity_state_attrs( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, +) -> None: + """Test lock states and attributes.""" + + # Setup component + with patch( + "homeassistant.components.bmw_connected_drive.PLATFORMS", [Platform.LOCK] + ): + mock_config_entry = await setup_mocked_integration(hass) + + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + +@pytest.mark.usefixtures("recorder_mock") +@pytest.mark.parametrize( + ("entity_id", "new_value", "old_value", "service", "remote_service"), + [ + ( + "lock.m340i_xdrive_lock", + "locked", + "unlocked", + "lock", + "door-lock", + ), + ("lock.m340i_xdrive_lock", "unlocked", "locked", "unlock", "door-unlock"), + ], +) +async def test_service_call_success( + hass: HomeAssistant, + entity_id: str, + new_value: str, + old_value: str, + service: str, + remote_service: str, + bmw_fixture: respx.Router, +) -> None: + """Test successful service call.""" + + # Setup component + assert await setup_mocked_integration(hass) + hass.states.async_set(entity_id, old_value) + assert hass.states.get(entity_id).state == old_value + + now = dt_util.utcnow() + + # Test + await hass.services.async_call( + "lock", + service, + blocking=True, + target={"entity_id": entity_id}, + ) + check_remote_service_call(bmw_fixture, remote_service) + assert hass.states.get(entity_id).state == new_value + + # wait for the recorder to really store the data + await async_wait_recording_done(hass) + states = await hass.async_add_executor_job( + get_significant_states, hass, now, None, [entity_id] + ) + assert any(s for s in states[entity_id] if s.state == STATE_UNKNOWN) is False + + +@pytest.mark.usefixtures("bmw_fixture") +@pytest.mark.usefixtures("recorder_mock") +@pytest.mark.parametrize( + ("entity_id", "service"), + [ + ("lock.m340i_xdrive_lock", "lock"), + ("lock.m340i_xdrive_lock", "unlock"), + ], +) +async def test_service_call_fail( + hass: HomeAssistant, + entity_id: str, + service: str, + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Test failed service call.""" + + # Setup component + assert await setup_mocked_integration(hass) + old_value = hass.states.get(entity_id).state + + now = dt_util.utcnow() + + # Setup exception + monkeypatch.setattr( + RemoteServices, + "trigger_remote_service", + AsyncMock(side_effect=MyBMWRemoteServiceError), + ) + + # Test + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + "lock", + service, + blocking=True, + target={"entity_id": entity_id}, + ) + assert hass.states.get(entity_id).state == old_value + + # wait for the recorder to really store the data + await async_wait_recording_done(hass) + states = await hass.async_add_executor_job( + get_significant_states, hass, now, None, [entity_id] + ) + assert states[entity_id][-2].state == STATE_UNKNOWN From 2ac5f8db06e6e68ce04ac10e885e6b3e1aa745b4 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 4 Jun 2024 16:00:53 +0200 Subject: [PATCH 0208/1445] Set unique id in aladdin connect config flow (#118798) --- .../components/aladdin_connect/config_flow.py | 28 ++- tests/components/aladdin_connect/conftest.py | 31 ++++ .../aladdin_connect/test_config_flow.py | 171 ++++++++++++++++-- 3 files changed, 208 insertions(+), 22 deletions(-) create mode 100644 tests/components/aladdin_connect/conftest.py diff --git a/homeassistant/components/aladdin_connect/config_flow.py b/homeassistant/components/aladdin_connect/config_flow.py index e1a7b44830d..507085fa27f 100644 --- a/homeassistant/components/aladdin_connect/config_flow.py +++ b/homeassistant/components/aladdin_connect/config_flow.py @@ -4,9 +4,10 @@ from collections.abc import Mapping import logging from typing import Any -import voluptuous as vol +import jwt from homeassistant.config_entries import ConfigEntry, ConfigFlowResult +from homeassistant.const import CONF_ACCESS_TOKEN, CONF_TOKEN from homeassistant.helpers.config_entry_oauth2_flow import AbstractOAuth2FlowHandler from .const import DOMAIN @@ -35,20 +36,33 @@ class AladdinConnectOAuth2FlowHandler(AbstractOAuth2FlowHandler, domain=DOMAIN): ) -> ConfigFlowResult: """Dialog that informs the user that reauth is required.""" if user_input is None: - return self.async_show_form( - step_id="reauth_confirm", - data_schema=vol.Schema({}), - ) + return self.async_show_form(step_id="reauth_confirm") return await self.async_step_user() async def async_oauth_create_entry(self, data: dict[str, Any]) -> ConfigFlowResult: """Create an oauth config entry or update existing entry for reauth.""" - if self.reauth_entry: + token_payload = jwt.decode( + data[CONF_TOKEN][CONF_ACCESS_TOKEN], options={"verify_signature": False} + ) + if not self.reauth_entry: + await self.async_set_unique_id(token_payload["sub"]) + self._abort_if_unique_id_configured() + + return self.async_create_entry( + title=token_payload["username"], + data=data, + ) + + if self.reauth_entry.unique_id == token_payload["username"]: return self.async_update_reload_and_abort( self.reauth_entry, data=data, + unique_id=token_payload["sub"], ) - return await super().async_oauth_create_entry(data) + if self.reauth_entry.unique_id == token_payload["sub"]: + return self.async_update_reload_and_abort(self.reauth_entry, data=data) + + return self.async_abort(reason="wrong_account") @property def logger(self) -> logging.Logger: diff --git a/tests/components/aladdin_connect/conftest.py b/tests/components/aladdin_connect/conftest.py new file mode 100644 index 00000000000..a3f8ae417e1 --- /dev/null +++ b/tests/components/aladdin_connect/conftest.py @@ -0,0 +1,31 @@ +"""Test fixtures for the Aladdin Connect Garage Door integration.""" + +from collections.abc import Generator +from unittest.mock import AsyncMock, patch + +import pytest + +from homeassistant.components.aladdin_connect import DOMAIN + +from tests.common import MockConfigEntry + + +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock, None, None]: + """Override async_setup_entry.""" + with patch( + "homeassistant.components.aladdin_connect.async_setup_entry", return_value=True + ) as mock_setup_entry: + yield mock_setup_entry + + +@pytest.fixture +def mock_config_entry() -> MockConfigEntry: + """Return an Aladdin Connect config entry.""" + return MockConfigEntry( + domain=DOMAIN, + data={}, + title="test@test.com", + unique_id="aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", + version=2, + ) diff --git a/tests/components/aladdin_connect/test_config_flow.py b/tests/components/aladdin_connect/test_config_flow.py index 0fca87487dd..02244420925 100644 --- a/tests/components/aladdin_connect/test_config_flow.py +++ b/tests/components/aladdin_connect/test_config_flow.py @@ -1,10 +1,9 @@ """Test the Aladdin Connect Garage Door config flow.""" -from unittest.mock import patch +from unittest.mock import AsyncMock import pytest -from homeassistant import config_entries from homeassistant.components.aladdin_connect.const import ( DOMAIN, OAUTH2_AUTHORIZE, @@ -14,16 +13,25 @@ from homeassistant.components.application_credentials import ( ClientCredential, async_import_client_credential, ) +from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER, ConfigFlowResult from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import config_entry_oauth2_flow from homeassistant.setup import async_setup_component +from tests.common import MockConfigEntry from tests.test_util.aiohttp import AiohttpClientMocker from tests.typing import ClientSessionGenerator CLIENT_ID = "1234" CLIENT_SECRET = "5678" +EXAMPLE_TOKEN = ( + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhYWFhYWFhYS1iYmJiLWNjY2MtZGRk" + "ZC1lZWVlZWVlZWVlZWUiLCJuYW1lIjoiSm9obiBEb2UiLCJpYXQiOjE1MTYyMzkwMjIsInVzZXJuYW" + "1lIjoidGVzdEB0ZXN0LmNvbSJ9.CTU1YItIrUl8nSM3koJxlFJr5CjLghgc9gS6h45D8dE" +) + @pytest.fixture async def setup_credentials(hass: HomeAssistant) -> None: @@ -36,17 +44,13 @@ async def setup_credentials(hass: HomeAssistant) -> None: ) -@pytest.mark.usefixtures("current_request_with_host", "setup_credentials") -async def test_full_flow( +async def _oauth_actions( hass: HomeAssistant, + result: ConfigFlowResult, hass_client_no_auth: ClientSessionGenerator, aioclient_mock: AiohttpClientMocker, ) -> None: - """Check full flow.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - state = config_entry_oauth2_flow._encode_jwt( # noqa: SLF001 + state = config_entry_oauth2_flow._encode_jwt( hass, { "flow_id": result["flow_id"], @@ -69,16 +73,153 @@ async def test_full_flow( OAUTH2_TOKEN, json={ "refresh_token": "mock-refresh-token", - "access_token": "mock-access-token", + "access_token": EXAMPLE_TOKEN, "type": "Bearer", "expires_in": 60, }, ) - with patch( - "homeassistant.components.aladdin_connect.async_setup_entry", return_value=True - ) as mock_setup: - await hass.config_entries.flow.async_configure(result["flow_id"]) + +async def test_full_flow( + hass: HomeAssistant, + hass_client_no_auth: ClientSessionGenerator, + aioclient_mock: AiohttpClientMocker, + current_request_with_host: None, + setup_credentials: None, + mock_setup_entry: AsyncMock, +) -> None: + """Check full flow.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + await _oauth_actions(hass, result, hass_client_no_auth, aioclient_mock) + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "test@test.com" + assert result["data"]["token"]["access_token"] == EXAMPLE_TOKEN + assert result["data"]["token"]["refresh_token"] == "mock-refresh-token" + assert result["result"].unique_id == "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_duplicate_entry( + hass: HomeAssistant, + hass_client_no_auth: ClientSessionGenerator, + aioclient_mock: AiohttpClientMocker, + current_request_with_host: None, + setup_credentials: None, + mock_config_entry: MockConfigEntry, +) -> None: + """Test we abort with duplicate entry.""" + mock_config_entry.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + await _oauth_actions(hass, result, hass_client_no_auth, aioclient_mock) + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +async def test_reauth( + hass: HomeAssistant, + hass_client_no_auth: ClientSessionGenerator, + aioclient_mock: AiohttpClientMocker, + current_request_with_host: None, + setup_credentials: None, + mock_config_entry: MockConfigEntry, + mock_setup_entry: AsyncMock, +) -> None: + """Test reauthentication.""" + mock_config_entry.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": SOURCE_REAUTH, + "entry_id": mock_config_entry.entry_id, + }, + data=mock_config_entry.data, + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + await _oauth_actions(hass, result, hass_client_no_auth, aioclient_mock) + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reauth_successful" + + +async def test_reauth_wrong_account( + hass: HomeAssistant, + hass_client_no_auth: ClientSessionGenerator, + aioclient_mock: AiohttpClientMocker, + current_request_with_host: None, + setup_credentials: None, + mock_setup_entry: AsyncMock, +) -> None: + """Test reauthentication with wrong account.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={}, + title="test@test.com", + unique_id="aaaaaaaa-bbbb-ffff-dddd-eeeeeeeeeeee", + version=2, + ) + config_entry.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": SOURCE_REAUTH, + "entry_id": config_entry.entry_id, + }, + data=config_entry.data, + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + await _oauth_actions(hass, result, hass_client_no_auth, aioclient_mock) + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "wrong_account" + + +async def test_reauth_old_account( + hass: HomeAssistant, + hass_client_no_auth: ClientSessionGenerator, + aioclient_mock: AiohttpClientMocker, + current_request_with_host: None, + setup_credentials: None, + mock_setup_entry: AsyncMock, +) -> None: + """Test reauthentication with old account.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={}, + title="test@test.com", + unique_id="test@test.com", + version=2, + ) + config_entry.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": SOURCE_REAUTH, + "entry_id": config_entry.entry_id, + }, + data=config_entry.data, + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + await _oauth_actions(hass, result, hass_client_no_auth, aioclient_mock) + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reauth_successful" + assert config_entry.unique_id == "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" From 089874f8184440b41cba9b8f1a6d1358d29f5e6e Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 4 Jun 2024 16:05:56 +0200 Subject: [PATCH 0209/1445] Move mock_hass_config fixture to decorator (#118807) --- tests/components/analytics/test_analytics.py | 32 ++++++++----------- .../triggers/test_homeassistant.py | 3 +- tests/components/knx/test_diagnostic.py | 8 ++--- tests/components/mqtt/test_init.py | 2 +- 4 files changed, 20 insertions(+), 25 deletions(-) diff --git a/tests/components/analytics/test_analytics.py b/tests/components/analytics/test_analytics.py index 587b8600f3f..8b86c505517 100644 --- a/tests/components/analytics/test_analytics.py +++ b/tests/components/analytics/test_analytics.py @@ -266,11 +266,11 @@ async def test_send_usage( assert snapshot == submitted_data +@pytest.mark.usefixtures("mock_hass_config") async def test_send_usage_with_supervisor( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, aioclient_mock: AiohttpClientMocker, - mock_hass_config: None, snapshot: SnapshotAssertion, ) -> None: """Test send usage with supervisor preferences are defined.""" @@ -359,11 +359,9 @@ async def test_send_statistics( assert snapshot == submitted_data +@pytest.mark.usefixtures("mock_hass_config") async def test_send_statistics_one_integration_fails( - hass: HomeAssistant, - caplog: pytest.LogCaptureFixture, - aioclient_mock: AiohttpClientMocker, - mock_hass_config: None, + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test send statistics preferences are defined.""" aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=200) @@ -384,11 +382,11 @@ async def test_send_statistics_one_integration_fails( assert post_call[2]["integration_count"] == 0 +@pytest.mark.usefixtures("mock_hass_config") async def test_send_statistics_disabled_integration( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, aioclient_mock: AiohttpClientMocker, - mock_hass_config: None, installation_type_mock: Generator[Any, Any, None], snapshot: SnapshotAssertion, ) -> None: @@ -422,11 +420,11 @@ async def test_send_statistics_disabled_integration( assert snapshot == submitted_data +@pytest.mark.usefixtures("mock_hass_config") async def test_send_statistics_ignored_integration( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, aioclient_mock: AiohttpClientMocker, - mock_hass_config: None, installation_type_mock: Generator[Any, Any, None], snapshot: SnapshotAssertion, ) -> None: @@ -466,11 +464,9 @@ async def test_send_statistics_ignored_integration( assert snapshot == submitted_data +@pytest.mark.usefixtures("mock_hass_config") async def test_send_statistics_async_get_integration_unknown_exception( - hass: HomeAssistant, - caplog: pytest.LogCaptureFixture, - aioclient_mock: AiohttpClientMocker, - mock_hass_config: None, + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test send statistics preferences are defined.""" aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=200) @@ -490,11 +486,11 @@ async def test_send_statistics_async_get_integration_unknown_exception( await analytics.send_analytics() +@pytest.mark.usefixtures("mock_hass_config") async def test_send_statistics_with_supervisor( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, aioclient_mock: AiohttpClientMocker, - mock_hass_config: None, snapshot: SnapshotAssertion, ) -> None: """Test send statistics preferences are defined.""" @@ -655,10 +651,10 @@ async def test_nightly_endpoint( assert str(payload[1]) == ANALYTICS_ENDPOINT_URL +@pytest.mark.usefixtures("mock_hass_config") async def test_send_with_no_energy( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, - mock_hass_config: None, caplog: pytest.LogCaptureFixture, installation_type_mock: Generator[Any, Any, None], snapshot: SnapshotAssertion, @@ -692,11 +688,10 @@ async def test_send_with_no_energy( assert snapshot == submitted_data +@pytest.mark.usefixtures("recorder_mock", "mock_hass_config") async def test_send_with_no_energy_config( - recorder_mock: Recorder, hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, - mock_hass_config: None, caplog: pytest.LogCaptureFixture, installation_type_mock: Generator[Any, Any, None], snapshot: SnapshotAssertion, @@ -725,11 +720,10 @@ async def test_send_with_no_energy_config( ) +@pytest.mark.usefixtures("recorder_mock", "mock_hass_config") async def test_send_with_energy_config( - recorder_mock: Recorder, hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, - mock_hass_config: None, caplog: pytest.LogCaptureFixture, installation_type_mock: Generator[Any, Any, None], snapshot: SnapshotAssertion, @@ -758,11 +752,11 @@ async def test_send_with_energy_config( ) +@pytest.mark.usefixtures("mock_hass_config") async def test_send_usage_with_certificate( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, aioclient_mock: AiohttpClientMocker, - mock_hass_config: None, installation_type_mock: Generator[Any, Any, None], snapshot: SnapshotAssertion, ) -> None: @@ -836,11 +830,11 @@ async def test_send_with_problems_loading_yaml( assert len(aioclient_mock.mock_calls) == 0 +@pytest.mark.usefixtures("mock_hass_config") async def test_timeout_while_sending( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, aioclient_mock: AiohttpClientMocker, - mock_hass_config: None, ) -> None: """Test timeout error while sending analytics.""" analytics = Analytics(hass) diff --git a/tests/components/homeassistant/triggers/test_homeassistant.py b/tests/components/homeassistant/triggers/test_homeassistant.py index 2afb533cdc0..9c552a0324b 100644 --- a/tests/components/homeassistant/triggers/test_homeassistant.py +++ b/tests/components/homeassistant/triggers/test_homeassistant.py @@ -27,8 +27,9 @@ from tests.common import async_mock_service } ], ) +@pytest.mark.usefixtures("mock_hass_config") async def test_if_fires_on_hass_start( - hass: HomeAssistant, mock_hass_config: None, hass_config: ConfigType + hass: HomeAssistant, hass_config: ConfigType ) -> None: """Test the firing when Home Assistant starts.""" calls = async_mock_service(hass, "test", "automation") diff --git a/tests/components/knx/test_diagnostic.py b/tests/components/knx/test_diagnostic.py index 0b43433c01e..bb60e66f7e7 100644 --- a/tests/components/knx/test_diagnostic.py +++ b/tests/components/knx/test_diagnostic.py @@ -31,12 +31,12 @@ from tests.typing import ClientSessionGenerator @pytest.mark.parametrize("hass_config", [{}]) +@pytest.mark.usefixtures("mock_hass_config") async def test_diagnostics( hass: HomeAssistant, hass_client: ClientSessionGenerator, mock_config_entry: MockConfigEntry, knx: KNXTestKit, - mock_hass_config: None, snapshot: SnapshotAssertion, ) -> None: """Test diagnostics.""" @@ -51,9 +51,9 @@ async def test_diagnostics( @pytest.mark.parametrize("hass_config", [{"knx": {"wrong_key": {}}}]) +@pytest.mark.usefixtures("mock_hass_config") async def test_diagnostic_config_error( hass: HomeAssistant, - mock_hass_config: None, hass_client: ClientSessionGenerator, mock_config_entry: MockConfigEntry, knx: KNXTestKit, @@ -72,10 +72,10 @@ async def test_diagnostic_config_error( @pytest.mark.parametrize("hass_config", [{}]) +@pytest.mark.usefixtures("mock_hass_config") async def test_diagnostic_redact( hass: HomeAssistant, hass_client: ClientSessionGenerator, - mock_hass_config: None, snapshot: SnapshotAssertion, ) -> None: """Test diagnostics redacting data.""" @@ -107,12 +107,12 @@ async def test_diagnostic_redact( @pytest.mark.parametrize("hass_config", [{}]) +@pytest.mark.usefixtures("mock_hass_config") async def test_diagnostics_project( hass: HomeAssistant, hass_client: ClientSessionGenerator, mock_config_entry: MockConfigEntry, knx: KNXTestKit, - mock_hass_config: None, load_knxproj: None, snapshot: SnapshotAssertion, ) -> None: diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index 50b22e986b0..2b9e4260c7e 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -928,9 +928,9 @@ def test_entity_device_info_schema() -> None: } ], ) +@pytest.mark.usefixtures("mock_hass_config") async def test_handle_logging_on_writing_the_entity_state( hass: HomeAssistant, - mock_hass_config: None, mqtt_mock_entry: MqttMockHAClientGenerator, caplog: pytest.LogCaptureFixture, ) -> None: From 3d31af3eb47ef6d241d19ad2340ca3c43bb1b03d Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 4 Jun 2024 16:18:42 +0200 Subject: [PATCH 0210/1445] Move entity_registry_enabled_by_default to decorator [a-p] (#118794) --- tests/components/accuweather/test_sensor.py | 2 +- tests/components/airzone/test_sensor.py | 11 ++++--- tests/components/airzone_cloud/test_select.py | 5 ++-- tests/components/airzone_cloud/test_sensor.py | 7 +++-- tests/components/aranet/test_sensor.py | 22 +++++++------- tests/components/brother/test_sensor.py | 3 +- tests/components/efergy/test_sensor.py | 2 +- tests/components/fritz/test_button.py | 8 ++--- tests/components/goalzero/test_sensor.py | 7 +++-- tests/components/gree/test_switch.py | 18 +++++------- tests/components/harmony/test_switch.py | 4 ++- tests/components/hassio/test_init.py | 2 +- .../homekit_controller/test_sensor.py | 14 ++++----- tests/components/idasen_desk/test_sensors.py | 9 +++--- tests/components/imgw_pib/test_sensor.py | 3 +- .../kostal_plenticore/test_number.py | 10 +++---- tests/components/kraken/test_sensor.py | 3 +- tests/components/lidarr/test_sensor.py | 4 ++- .../litterrobot/test_binary_sensor.py | 2 +- tests/components/nam/test_sensor.py | 2 +- .../netgear_lte/test_binary_sensor.py | 3 +- tests/components/netgear_lte/test_sensor.py | 3 +- tests/components/nextdns/test_sensor.py | 5 ++-- tests/components/nextdns/test_switch.py | 2 +- .../components/nibe_heatpump/test_climate.py | 12 ++++---- .../nibe_heatpump/test_coordinator.py | 8 ++--- tests/components/nibe_heatpump/test_number.py | 4 +-- tests/components/oralb/test_sensor.py | 12 ++++---- tests/components/pegel_online/test_sensor.py | 2 +- tests/components/powerwall/test_sensor.py | 10 +++---- .../private_ble_device/test_device_tracker.py | 8 ++--- .../private_ble_device/test_sensor.py | 29 ++++++------------- 32 files changed, 111 insertions(+), 125 deletions(-) diff --git a/tests/components/accuweather/test_sensor.py b/tests/components/accuweather/test_sensor.py index e16f1e863da..41c1c0d930a 100644 --- a/tests/components/accuweather/test_sensor.py +++ b/tests/components/accuweather/test_sensor.py @@ -31,9 +31,9 @@ from . import init_integration from tests.common import async_fire_time_changed, snapshot_platform +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_sensor( hass: HomeAssistant, - entity_registry_enabled_by_default: None, entity_registry: er.EntityRegistry, mock_accuweather_client: AsyncMock, snapshot: SnapshotAssertion, diff --git a/tests/components/airzone/test_sensor.py b/tests/components/airzone/test_sensor.py index 3d4c54522fc..3d75599d2d2 100644 --- a/tests/components/airzone/test_sensor.py +++ b/tests/components/airzone/test_sensor.py @@ -4,6 +4,7 @@ import copy from unittest.mock import patch from aioairzone.const import API_DATA, API_SYSTEMS +import pytest from homeassistant.components.airzone.coordinator import SCAN_INTERVAL from homeassistant.const import STATE_UNAVAILABLE @@ -22,9 +23,8 @@ from .util import ( from tests.common import async_fire_time_changed -async def test_airzone_create_sensors( - hass: HomeAssistant, entity_registry_enabled_by_default: None -) -> None: +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_airzone_create_sensors(hass: HomeAssistant) -> None: """Test creation of sensors.""" await async_init_integration(hass) @@ -81,9 +81,8 @@ async def test_airzone_create_sensors( assert state is None -async def test_airzone_sensors_availability( - hass: HomeAssistant, entity_registry_enabled_by_default: None -) -> None: +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_airzone_sensors_availability(hass: HomeAssistant) -> None: """Test sensors availability.""" await async_init_integration(hass) diff --git a/tests/components/airzone_cloud/test_select.py b/tests/components/airzone_cloud/test_select.py index 1375b052050..5a6b6104468 100644 --- a/tests/components/airzone_cloud/test_select.py +++ b/tests/components/airzone_cloud/test_select.py @@ -12,9 +12,8 @@ from homeassistant.exceptions import ServiceValidationError from .util import async_init_integration -async def test_airzone_create_selects( - hass: HomeAssistant, entity_registry_enabled_by_default: None -) -> None: +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_airzone_create_selects(hass: HomeAssistant) -> None: """Test creation of selects.""" await async_init_integration(hass) diff --git a/tests/components/airzone_cloud/test_sensor.py b/tests/components/airzone_cloud/test_sensor.py index 5000f1cabea..31fe52f3302 100644 --- a/tests/components/airzone_cloud/test_sensor.py +++ b/tests/components/airzone_cloud/test_sensor.py @@ -1,13 +1,14 @@ """The sensor tests for the Airzone Cloud platform.""" +import pytest + from homeassistant.core import HomeAssistant from .util import async_init_integration -async def test_airzone_create_sensors( - hass: HomeAssistant, entity_registry_enabled_by_default: None -) -> None: +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_airzone_create_sensors(hass: HomeAssistant) -> None: """Test creation of sensors.""" await async_init_integration(hass) diff --git a/tests/components/aranet/test_sensor.py b/tests/components/aranet/test_sensor.py index 0d57f00fdf4..c932a92c1e8 100644 --- a/tests/components/aranet/test_sensor.py +++ b/tests/components/aranet/test_sensor.py @@ -1,5 +1,7 @@ """Test the Aranet sensors.""" +import pytest + from homeassistant.components.aranet.const import DOMAIN from homeassistant.components.sensor import ATTR_STATE_CLASS from homeassistant.const import ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT @@ -16,9 +18,8 @@ from tests.common import MockConfigEntry from tests.components.bluetooth import inject_bluetooth_service_info -async def test_sensors_aranet_radiation( - hass: HomeAssistant, entity_registry_enabled_by_default: None -) -> None: +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_sensors_aranet_radiation(hass: HomeAssistant) -> None: """Test setting up creates the sensors for Aranet Radiation device.""" entry = MockConfigEntry( domain=DOMAIN, @@ -75,9 +76,8 @@ async def test_sensors_aranet_radiation( await hass.async_block_till_done() -async def test_sensors_aranet2( - hass: HomeAssistant, entity_registry_enabled_by_default: None -) -> None: +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_sensors_aranet2(hass: HomeAssistant) -> None: """Test setting up creates the sensors for Aranet2 device.""" entry = MockConfigEntry( domain=DOMAIN, @@ -125,9 +125,8 @@ async def test_sensors_aranet2( await hass.async_block_till_done() -async def test_sensors_aranet4( - hass: HomeAssistant, entity_registry_enabled_by_default: None -) -> None: +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_sensors_aranet4(hass: HomeAssistant) -> None: """Test setting up creates the sensors for Aranet4 device.""" entry = MockConfigEntry( domain=DOMAIN, @@ -189,9 +188,8 @@ async def test_sensors_aranet4( await hass.async_block_till_done() -async def test_smart_home_integration_disabled( - hass: HomeAssistant, entity_registry_enabled_by_default: None -) -> None: +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_smart_home_integration_disabled(hass: HomeAssistant) -> None: """Test disabling smart home integration marks entities as unavailable.""" entry = MockConfigEntry( domain=DOMAIN, diff --git a/tests/components/brother/test_sensor.py b/tests/components/brother/test_sensor.py index 7736b9257ee..8069b27e307 100644 --- a/tests/components/brother/test_sensor.py +++ b/tests/components/brother/test_sensor.py @@ -3,6 +3,7 @@ from unittest.mock import AsyncMock, patch from freezegun.api import FrozenDateTimeFactory +import pytest from syrupy import SnapshotAssertion from homeassistant.components.brother.const import DOMAIN, UPDATE_INTERVAL @@ -16,10 +17,10 @@ from . import init_integration from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_sensors( hass: HomeAssistant, entity_registry: er.EntityRegistry, - entity_registry_enabled_by_default: None, snapshot: SnapshotAssertion, mock_brother_client: AsyncMock, mock_config_entry: MockConfigEntry, diff --git a/tests/components/efergy/test_sensor.py b/tests/components/efergy/test_sensor.py index d7ab3101900..addaa1b9c48 100644 --- a/tests/components/efergy/test_sensor.py +++ b/tests/components/efergy/test_sensor.py @@ -28,7 +28,7 @@ from tests.test_util.aiohttp import AiohttpClientMocker @pytest.fixture(autouse=True) -def enable_all_entities(entity_registry_enabled_by_default): +def enable_all_entities(entity_registry_enabled_by_default: None) -> None: """Make sure all entities are enabled.""" diff --git a/tests/components/fritz/test_button.py b/tests/components/fritz/test_button.py index ca8b8f9291f..8666491eb7a 100644 --- a/tests/components/fritz/test_button.py +++ b/tests/components/fritz/test_button.py @@ -77,9 +77,9 @@ async def test_buttons( assert button.state != STATE_UNKNOWN +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_wol_button( hass: HomeAssistant, - entity_registry_enabled_by_default: None, fc_class_mock, fh_class_mock, ) -> None: @@ -110,9 +110,9 @@ async def test_wol_button( assert button.state != STATE_UNKNOWN +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_wol_button_new_device( hass: HomeAssistant, - entity_registry_enabled_by_default: None, fc_class_mock, fh_class_mock, ) -> None: @@ -138,9 +138,9 @@ async def test_wol_button_new_device( assert hass.states.get("button.server_wake_on_lan") +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_wol_button_absent_for_mesh_slave( hass: HomeAssistant, - entity_registry_enabled_by_default: None, fc_class_mock, fh_class_mock, ) -> None: @@ -160,9 +160,9 @@ async def test_wol_button_absent_for_mesh_slave( assert button is None +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_wol_button_absent_for_non_lan_device( hass: HomeAssistant, - entity_registry_enabled_by_default: None, fc_class_mock, fh_class_mock, ) -> None: diff --git a/tests/components/goalzero/test_sensor.py b/tests/components/goalzero/test_sensor.py index d36d692422e..6421f0c526c 100644 --- a/tests/components/goalzero/test_sensor.py +++ b/tests/components/goalzero/test_sensor.py @@ -1,5 +1,7 @@ """Sensor tests for the Goalzero integration.""" +import pytest + from homeassistant.components.goalzero.const import DEFAULT_NAME from homeassistant.components.sensor import ( ATTR_STATE_CLASS, @@ -25,10 +27,9 @@ from . import async_init_integration from tests.test_util.aiohttp import AiohttpClientMocker +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_sensors( - hass: HomeAssistant, - aioclient_mock: AiohttpClientMocker, - entity_registry_enabled_by_default: None, + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test we get sensor data.""" await async_init_integration(hass, aioclient_mock) diff --git a/tests/components/gree/test_switch.py b/tests/components/gree/test_switch.py index 9c465a9f297..c5684abbf6f 100644 --- a/tests/components/gree/test_switch.py +++ b/tests/components/gree/test_switch.py @@ -61,9 +61,8 @@ async def test_registry_settings( ENTITY_ID_XFAN, ], ) -async def test_send_switch_on( - hass: HomeAssistant, entity, entity_registry_enabled_by_default: None -) -> None: +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_send_switch_on(hass: HomeAssistant, entity: str) -> None: """Test for sending power on command to the device.""" await async_setup_gree(hass) @@ -89,8 +88,9 @@ async def test_send_switch_on( ENTITY_ID_XFAN, ], ) +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_send_switch_on_device_timeout( - hass: HomeAssistant, device, entity, entity_registry_enabled_by_default: None + hass: HomeAssistant, device, entity: str ) -> None: """Test for sending power on command to the device with a device timeout.""" device().push_state_update.side_effect = DeviceTimeoutError @@ -119,9 +119,8 @@ async def test_send_switch_on_device_timeout( ENTITY_ID_XFAN, ], ) -async def test_send_switch_off( - hass: HomeAssistant, entity, entity_registry_enabled_by_default: None -) -> None: +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_send_switch_off(hass: HomeAssistant, entity: str) -> None: """Test for sending power on command to the device.""" await async_setup_gree(hass) @@ -147,9 +146,8 @@ async def test_send_switch_off( ENTITY_ID_XFAN, ], ) -async def test_send_switch_toggle( - hass: HomeAssistant, entity, entity_registry_enabled_by_default: None -) -> None: +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_send_switch_toggle(hass: HomeAssistant, entity: str) -> None: """Test for sending power on command to the device.""" await async_setup_gree(hass) diff --git a/tests/components/harmony/test_switch.py b/tests/components/harmony/test_switch.py index 01f9287ae57..0cfc0e5bead 100644 --- a/tests/components/harmony/test_switch.py +++ b/tests/components/harmony/test_switch.py @@ -2,6 +2,8 @@ from datetime import timedelta +import pytest + from homeassistant.components import automation, script from homeassistant.components.automation import automations_with_entity from homeassistant.components.harmony.const import DOMAIN @@ -142,12 +144,12 @@ async def _toggle_switch_and_wait(hass, service_name, entity): await hass.async_block_till_done() +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_create_issue( harmony_client, mock_hc, hass: HomeAssistant, mock_write_config, - entity_registry_enabled_by_default: None, issue_registry: ir.IssueRegistry, ) -> None: """Test we create an issue when an automation or script is using a deprecated entity.""" diff --git a/tests/components/hassio/test_init.py b/tests/components/hassio/test_init.py index eddd4e5e04f..2971bdbb675 100644 --- a/tests/components/hassio/test_init.py +++ b/tests/components/hassio/test_init.py @@ -999,10 +999,10 @@ async def test_coordinator_updates( assert "Error on Supervisor API: Unknown" in caplog.text +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_coordinator_updates_stats_entities_enabled( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, - entity_registry_enabled_by_default: None, ) -> None: """Test coordinator updates with stats entities enabled.""" await async_setup_component(hass, "homeassistant", {}) diff --git a/tests/components/homekit_controller/test_sensor.py b/tests/components/homekit_controller/test_sensor.py index 8634b33fe3b..461d62742a5 100644 --- a/tests/components/homekit_controller/test_sensor.py +++ b/tests/components/homekit_controller/test_sensor.py @@ -8,6 +8,7 @@ from aiohomekit.model.characteristics.const import ThreadNodeCapabilities, Threa from aiohomekit.model.services import ServicesTypes from aiohomekit.protocol.statuscodes import HapStatusCode from aiohomekit.testing import FakePairing +import pytest from homeassistant.components.homekit_controller.sensor import ( thread_node_capability_to_str, @@ -381,11 +382,8 @@ def test_thread_status_to_str() -> None: assert thread_status_to_str(ThreadStatus.DISABLED) == "disabled" -async def test_rssi_sensor( - hass: HomeAssistant, - entity_registry_enabled_by_default: None, - enable_bluetooth: None, -) -> None: +@pytest.mark.usefixtures("enable_bluetooth", "entity_registry_enabled_by_default") +async def test_rssi_sensor(hass: HomeAssistant) -> None: """Test an rssi sensor.""" inject_bluetooth_service_info(hass, TEST_DEVICE_SERVICE_INFO) @@ -405,11 +403,9 @@ async def test_rssi_sensor( assert hass.states.get("sensor.testdevice_signal_strength").state == "-56" +@pytest.mark.usefixtures("enable_bluetooth", "entity_registry_enabled_by_default") async def test_migrate_rssi_sensor_unique_id( - hass: HomeAssistant, - entity_registry: er.EntityRegistry, - entity_registry_enabled_by_default: None, - enable_bluetooth: None, + hass: HomeAssistant, entity_registry: er.EntityRegistry ) -> None: """Test an rssi sensor unique id migration.""" rssi_sensor = entity_registry.async_get_or_create( diff --git a/tests/components/idasen_desk/test_sensors.py b/tests/components/idasen_desk/test_sensors.py index f56a45104eb..a236555a506 100644 --- a/tests/components/idasen_desk/test_sensors.py +++ b/tests/components/idasen_desk/test_sensors.py @@ -2,16 +2,15 @@ from unittest.mock import MagicMock +import pytest + from homeassistant.core import HomeAssistant from . import init_integration -async def test_height_sensor( - hass: HomeAssistant, - mock_desk_api: MagicMock, - entity_registry_enabled_by_default: None, -) -> None: +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_height_sensor(hass: HomeAssistant, mock_desk_api: MagicMock) -> None: """Test height sensor.""" await init_integration(hass) diff --git a/tests/components/imgw_pib/test_sensor.py b/tests/components/imgw_pib/test_sensor.py index 82e85b4085a..276c021fad5 100644 --- a/tests/components/imgw_pib/test_sensor.py +++ b/tests/components/imgw_pib/test_sensor.py @@ -4,6 +4,7 @@ from unittest.mock import AsyncMock, patch from freezegun.api import FrozenDateTimeFactory from imgw_pib import ApiError +import pytest from syrupy import SnapshotAssertion from homeassistant.components.imgw_pib.const import UPDATE_INTERVAL @@ -18,13 +19,13 @@ from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_plat ENTITY_ID = "sensor.river_name_station_name_water_level" +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_sensor( hass: HomeAssistant, entity_registry: er.EntityRegistry, snapshot: SnapshotAssertion, mock_imgw_pib_client: AsyncMock, mock_config_entry: MockConfigEntry, - entity_registry_enabled_by_default: None, ) -> None: """Test states of the sensor.""" with patch("homeassistant.components.imgw_pib.PLATFORMS", [Platform.SENSOR]): diff --git a/tests/components/kostal_plenticore/test_number.py b/tests/components/kostal_plenticore/test_number.py index a23b6987306..40ab524ef66 100644 --- a/tests/components/kostal_plenticore/test_number.py +++ b/tests/components/kostal_plenticore/test_number.py @@ -92,12 +92,12 @@ def mock_get_setting_values(mock_plenticore_client: ApiClient) -> list: return setting_values +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_setup_all_entries( hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_plenticore_client: ApiClient, mock_get_setting_values: list, - entity_registry_enabled_by_default: None, ) -> None: """Test if all available entries are setup.""" @@ -111,12 +111,12 @@ async def test_setup_all_entries( assert ent_reg.async_get("number.scb_battery_min_home_consumption") is not None +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_setup_no_entries( hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_plenticore_client: ApiClient, mock_get_setting_values: list, - entity_registry_enabled_by_default: None, ) -> None: """Test that no entries are setup if Plenticore does not provide data.""" @@ -145,12 +145,12 @@ async def test_setup_no_entries( assert ent_reg.async_get("number.scb_battery_min_home_consumption") is None +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_number_has_value( hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_plenticore_client: ApiClient, mock_get_setting_values: list, - entity_registry_enabled_by_default: None, ) -> None: """Test if number has a value if data is provided on update.""" @@ -170,12 +170,12 @@ async def test_number_has_value( assert state.attributes[ATTR_MAX] == 100 +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_number_is_unavailable( hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_plenticore_client: ApiClient, mock_get_setting_values: list, - entity_registry_enabled_by_default: None, ) -> None: """Test if number is unavailable if no data is provided on update.""" @@ -191,12 +191,12 @@ async def test_number_is_unavailable( assert state.state == STATE_UNAVAILABLE +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_set_value( hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_plenticore_client: ApiClient, mock_get_setting_values: list, - entity_registry_enabled_by_default: None, ) -> None: """Test if a new value could be set.""" diff --git a/tests/components/kraken/test_sensor.py b/tests/components/kraken/test_sensor.py index fd0a1dc72d1..a08875bfdce 100644 --- a/tests/components/kraken/test_sensor.py +++ b/tests/components/kraken/test_sensor.py @@ -5,6 +5,7 @@ from unittest.mock import patch from freezegun.api import FrozenDateTimeFactory from pykrakenapi.pykrakenapi import KrakenAPIError +import pytest from homeassistant.components.kraken.const import ( CONF_TRACKED_ASSET_PAIRS, @@ -26,10 +27,10 @@ from .const import ( from tests.common import MockConfigEntry, async_fire_time_changed +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_sensor( hass: HomeAssistant, freezer: FrozenDateTimeFactory, - entity_registry_enabled_by_default: None, ) -> None: """Test that sensor has a value.""" with ( diff --git a/tests/components/lidarr/test_sensor.py b/tests/components/lidarr/test_sensor.py index 3b3f661ce23..0c19355a252 100644 --- a/tests/components/lidarr/test_sensor.py +++ b/tests/components/lidarr/test_sensor.py @@ -1,5 +1,7 @@ """The tests for Lidarr sensor platform.""" +import pytest + from homeassistant.components.sensor import CONF_STATE_CLASS, SensorStateClass from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT from homeassistant.core import HomeAssistant @@ -7,10 +9,10 @@ from homeassistant.core import HomeAssistant from .conftest import ComponentSetup +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_sensors( hass: HomeAssistant, setup_integration: ComponentSetup, - entity_registry_enabled_by_default: None, connection, ) -> None: """Test for successfully setting up the Lidarr platform.""" diff --git a/tests/components/litterrobot/test_binary_sensor.py b/tests/components/litterrobot/test_binary_sensor.py index c72f747db88..69b3f7ce3ab 100644 --- a/tests/components/litterrobot/test_binary_sensor.py +++ b/tests/components/litterrobot/test_binary_sensor.py @@ -15,10 +15,10 @@ from .conftest import setup_integration @pytest.mark.freeze_time("2022-09-18 23:00:44+00:00") +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_binary_sensors( hass: HomeAssistant, mock_account: MagicMock, - entity_registry_enabled_by_default: None, ) -> None: """Tests binary sensors.""" await setup_integration(hass, mock_account, PLATFORM_DOMAIN) diff --git a/tests/components/nam/test_sensor.py b/tests/components/nam/test_sensor.py index 9280336779e..53945e1c8a2 100644 --- a/tests/components/nam/test_sensor.py +++ b/tests/components/nam/test_sensor.py @@ -33,9 +33,9 @@ from tests.common import ( ) +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_sensor( hass: HomeAssistant, - entity_registry_enabled_by_default: None, entity_registry: er.EntityRegistry, snapshot: SnapshotAssertion, freezer: FrozenDateTimeFactory, diff --git a/tests/components/netgear_lte/test_binary_sensor.py b/tests/components/netgear_lte/test_binary_sensor.py index 5fbbcfe06f6..e44b7de5da0 100644 --- a/tests/components/netgear_lte/test_binary_sensor.py +++ b/tests/components/netgear_lte/test_binary_sensor.py @@ -1,5 +1,6 @@ """The tests for Netgear LTE binary sensor platform.""" +import pytest from syrupy.assertion import SnapshotAssertion from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN @@ -8,9 +9,9 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_binary_sensors( hass: HomeAssistant, - entity_registry_enabled_by_default: None, setup_integration: None, entity_registry: er.EntityRegistry, snapshot: SnapshotAssertion, diff --git a/tests/components/netgear_lte/test_sensor.py b/tests/components/netgear_lte/test_sensor.py index 075c3db3b08..14533d7216c 100644 --- a/tests/components/netgear_lte/test_sensor.py +++ b/tests/components/netgear_lte/test_sensor.py @@ -1,5 +1,6 @@ """The tests for Netgear LTE sensor platform.""" +import pytest from syrupy.assertion import SnapshotAssertion from homeassistant.components.netgear_lte.const import DOMAIN @@ -8,9 +9,9 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_sensors( hass: HomeAssistant, - entity_registry_enabled_by_default: None, setup_integration: None, entity_registry: er.EntityRegistry, snapshot: SnapshotAssertion, diff --git a/tests/components/nextdns/test_sensor.py b/tests/components/nextdns/test_sensor.py index e7ea7a3f56b..eddf5a1cc5a 100644 --- a/tests/components/nextdns/test_sensor.py +++ b/tests/components/nextdns/test_sensor.py @@ -4,6 +4,7 @@ from datetime import timedelta from unittest.mock import patch from nextdns import ApiError +import pytest from syrupy import SnapshotAssertion from homeassistant.const import STATE_UNAVAILABLE, Platform @@ -16,9 +17,9 @@ from . import init_integration, mock_nextdns from tests.common import async_fire_time_changed, snapshot_platform +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_sensor( hass: HomeAssistant, - entity_registry_enabled_by_default: None, entity_registry: er.EntityRegistry, snapshot: SnapshotAssertion, ) -> None: @@ -29,9 +30,9 @@ async def test_sensor( await snapshot_platform(hass, entity_registry, snapshot, entry.entry_id) +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_availability( hass: HomeAssistant, - entity_registry_enabled_by_default: None, entity_registry: er.EntityRegistry, ) -> None: """Ensure that we mark the entities unavailable correctly when service causes an error.""" diff --git a/tests/components/nextdns/test_switch.py b/tests/components/nextdns/test_switch.py index 2936bad1c67..059585e9ffe 100644 --- a/tests/components/nextdns/test_switch.py +++ b/tests/components/nextdns/test_switch.py @@ -29,9 +29,9 @@ from . import init_integration, mock_nextdns from tests.common import async_fire_time_changed, snapshot_platform +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_switch( hass: HomeAssistant, - entity_registry_enabled_by_default: None, entity_registry: er.EntityRegistry, snapshot: SnapshotAssertion, ) -> None: diff --git a/tests/components/nibe_heatpump/test_climate.py b/tests/components/nibe_heatpump/test_climate.py index 010bd3d71b1..e40b197f58c 100644 --- a/tests/components/nibe_heatpump/test_climate.py +++ b/tests/components/nibe_heatpump/test_climate.py @@ -66,6 +66,7 @@ def _setup_climate_group( (Model.F730, "s1", "climate.climate_system_s1"), ], ) +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_basic( hass: HomeAssistant, mock_connection: MockConnection, @@ -73,7 +74,6 @@ async def test_basic( climate_id: str, entity_id: str, coils: dict[int, Any], - entity_registry_enabled_by_default: None, snapshot: SnapshotAssertion, ) -> None: """Test setting of value.""" @@ -113,6 +113,7 @@ async def test_basic( (Model.F1155, "s3", "climate.climate_system_s3"), ], ) +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_active_accessory( hass: HomeAssistant, mock_connection: MockConnection, @@ -120,7 +121,6 @@ async def test_active_accessory( climate_id: str, entity_id: str, coils: dict[int, Any], - entity_registry_enabled_by_default: None, snapshot: SnapshotAssertion, ) -> None: """Test climate groups that can be deactivated by configuration.""" @@ -141,6 +141,7 @@ async def test_active_accessory( (Model.F1155, "s2", "climate.climate_system_s2"), ], ) +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_set_temperature_supported_cooling( hass: HomeAssistant, mock_connection: MockConnection, @@ -148,7 +149,6 @@ async def test_set_temperature_supported_cooling( climate_id: str, entity_id: str, coils: dict[int, Any], - entity_registry_enabled_by_default: None, snapshot: SnapshotAssertion, ) -> None: """Test setting temperature for models with cooling support.""" @@ -234,6 +234,7 @@ async def test_set_temperature_supported_cooling( (Model.F730, "s1", "climate.climate_system_s1"), ], ) +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_set_temperature_unsupported_cooling( hass: HomeAssistant, mock_connection: MockConnection, @@ -241,7 +242,6 @@ async def test_set_temperature_unsupported_cooling( climate_id: str, entity_id: str, coils: dict[int, Any], - entity_registry_enabled_by_default: None, snapshot: SnapshotAssertion, ) -> None: """Test setting temperature for models that do not support cooling.""" @@ -300,6 +300,7 @@ async def test_set_temperature_unsupported_cooling( (Model.F730, "s1", "climate.climate_system_s1"), ], ) +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_set_hvac_mode( hass: HomeAssistant, mock_connection: MockConnection, @@ -310,7 +311,6 @@ async def test_set_hvac_mode( use_room_sensor: str, hvac_mode: HVACMode, coils: dict[int, Any], - entity_registry_enabled_by_default: None, ) -> None: """Test setting a hvac mode.""" climate, unit = _setup_climate_group(coils, model, climate_id) @@ -349,6 +349,7 @@ async def test_set_hvac_mode( (Model.F730, "s1", "climate.climate_system_s1", HVACMode.COOL), ], ) +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_set_invalid_hvac_mode( hass: HomeAssistant, mock_connection: MockConnection, @@ -357,7 +358,6 @@ async def test_set_invalid_hvac_mode( entity_id: str, unsupported_mode: str, coils: dict[int, Any], - entity_registry_enabled_by_default: None, ) -> None: """Test setting an invalid hvac mode.""" _setup_climate_group(coils, model, climate_id) diff --git a/tests/components/nibe_heatpump/test_coordinator.py b/tests/components/nibe_heatpump/test_coordinator.py index ffd5c545645..2fade8e34d7 100644 --- a/tests/components/nibe_heatpump/test_coordinator.py +++ b/tests/components/nibe_heatpump/test_coordinator.py @@ -22,10 +22,10 @@ async def fixture_single_platform(): yield +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_partial_refresh( hass: HomeAssistant, coils: dict[int, Any], - entity_registry_enabled_by_default: None, snapshot: SnapshotAssertion, ) -> None: """Test that coordinator can handle partial fields.""" @@ -45,10 +45,10 @@ async def test_partial_refresh( assert data == snapshot(name="3. Sensor is available") +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_invalid_coil( hass: HomeAssistant, coils: dict[int, Any], - entity_registry_enabled_by_default: None, snapshot: SnapshotAssertion, freezer_ticker: Any, ) -> None: @@ -67,10 +67,10 @@ async def test_invalid_coil( assert hass.states.get(entity_id) == snapshot(name="Sensor is not available") +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_pushed_update( hass: HomeAssistant, coils: dict[int, Any], - entity_registry_enabled_by_default: None, snapshot: SnapshotAssertion, mock_connection: MockConnection, freezer_ticker: Any, @@ -97,10 +97,10 @@ async def test_pushed_update( assert hass.states.get(entity_id) == snapshot(name="4. final values") +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_shutdown( hass: HomeAssistant, coils: dict[int, Any], - entity_registry_enabled_by_default: None, mock_connection: MockConnection, freezer_ticker: Any, ) -> None: diff --git a/tests/components/nibe_heatpump/test_number.py b/tests/components/nibe_heatpump/test_number.py index 99f8ab22b6c..73fed9ee08a 100644 --- a/tests/components/nibe_heatpump/test_number.py +++ b/tests/components/nibe_heatpump/test_number.py @@ -43,6 +43,7 @@ async def fixture_single_platform(): (Model.F750, 47062, "number.hw_charge_offset_47062", None), ], ) +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_update( hass: HomeAssistant, model: Model, @@ -50,7 +51,6 @@ async def test_update( address: int, value: Any, coils: dict[int, Any], - entity_registry_enabled_by_default: None, snapshot: SnapshotAssertion, ) -> None: """Test setting of value.""" @@ -73,6 +73,7 @@ async def test_update( (Model.F750, 47062, "number.hw_charge_offset_47062", 10), ], ) +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_set_value( hass: HomeAssistant, mock_connection: AsyncMock, @@ -81,7 +82,6 @@ async def test_set_value( address: int, value: Any, coils: dict[int, Any], - entity_registry_enabled_by_default: None, ) -> None: """Test setting of value.""" coils[address] = 0 diff --git a/tests/components/oralb/test_sensor.py b/tests/components/oralb/test_sensor.py index 82f9b86b352..147f20733d6 100644 --- a/tests/components/oralb/test_sensor.py +++ b/tests/components/oralb/test_sensor.py @@ -3,6 +3,8 @@ from datetime import timedelta import time +import pytest + from homeassistant.components.bluetooth import ( FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS, async_address_present, @@ -27,9 +29,8 @@ from tests.components.bluetooth import ( ) -async def test_sensors( - hass: HomeAssistant, entity_registry_enabled_by_default: None -) -> None: +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_sensors(hass: HomeAssistant) -> None: """Test setting up creates the sensors.""" start_monotonic = time.monotonic() entry = MockConfigEntry( @@ -79,9 +80,8 @@ async def test_sensors( assert toothbrush_sensor.state == "running" -async def test_sensors_io_series_4( - hass: HomeAssistant, entity_registry_enabled_by_default: None -) -> None: +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_sensors_io_series_4(hass: HomeAssistant) -> None: """Test setting up creates the sensors with an io series 4.""" start_monotonic = time.monotonic() diff --git a/tests/components/pegel_online/test_sensor.py b/tests/components/pegel_online/test_sensor.py index e911ec571cd..038a320c549 100644 --- a/tests/components/pegel_online/test_sensor.py +++ b/tests/components/pegel_online/test_sensor.py @@ -106,13 +106,13 @@ from tests.common import MockConfigEntry ), ], ) +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_sensor( hass: HomeAssistant, mock_config_entry_data: dict, mock_station_details: Station, mock_station_measurement: StationMeasurements, expected_states: dict, - entity_registry_enabled_by_default: None, ) -> None: """Tests sensor entity.""" entry = MockConfigEntry( diff --git a/tests/components/powerwall/test_sensor.py b/tests/components/powerwall/test_sensor.py index 206411f78c0..fa2d986d12a 100644 --- a/tests/components/powerwall/test_sensor.py +++ b/tests/components/powerwall/test_sensor.py @@ -3,6 +3,7 @@ from datetime import timedelta from unittest.mock import Mock, patch +import pytest from tesla_powerwall import MetersAggregatesResponse from tesla_powerwall.error import MissingAttributeError @@ -25,11 +26,8 @@ from .mocks import MOCK_GATEWAY_DIN, _mock_powerwall_with_fixtures from tests.common import MockConfigEntry, async_fire_time_changed -async def test_sensors( - hass: HomeAssistant, - device_registry: dr.DeviceRegistry, - entity_registry_enabled_by_default: None, -) -> None: +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_sensors(hass: HomeAssistant, device_registry: dr.DeviceRegistry) -> None: """Test creation of the sensors.""" mock_powerwall = await _mock_powerwall_with_fixtures(hass) @@ -245,11 +243,11 @@ async def test_sensors_with_empty_meters(hass: HomeAssistant) -> None: assert hass.states.get("sensor.mysite_solar_power") is None +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_unique_id_migrate( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - entity_registry_enabled_by_default: None, ) -> None: """Test we can migrate unique ids of the sensors.""" config_entry = MockConfigEntry(domain=DOMAIN, data={CONF_IP_ADDRESS: "1.2.3.4"}) diff --git a/tests/components/private_ble_device/test_device_tracker.py b/tests/components/private_ble_device/test_device_tracker.py index 9d784ecdfa7..b1952557316 100644 --- a/tests/components/private_ble_device/test_device_tracker.py +++ b/tests/components/private_ble_device/test_device_tracker.py @@ -3,6 +3,7 @@ import time from habluetooth.advertisement_tracker import ADVERTISING_TIMES_NEEDED +import pytest from homeassistant.components.bluetooth.api import ( async_get_fallback_availability_interval, @@ -184,11 +185,8 @@ async def test_old_tracker_leave_home( assert state.state == "not_home" -async def test_mac_rotation( - hass: HomeAssistant, - enable_bluetooth: None, - entity_registry_enabled_by_default: None, -) -> None: +@pytest.mark.usefixtures("enable_bluetooth", "entity_registry_enabled_by_default") +async def test_mac_rotation(hass: HomeAssistant) -> None: """Test sensors get value when we receive a broadcast.""" await async_mock_config_entry(hass) diff --git a/tests/components/private_ble_device/test_sensor.py b/tests/components/private_ble_device/test_sensor.py index 43667a0e9d2..b1ee10286e0 100644 --- a/tests/components/private_ble_device/test_sensor.py +++ b/tests/components/private_ble_device/test_sensor.py @@ -1,6 +1,7 @@ """Tests for sensors.""" from habluetooth.advertisement_tracker import ADVERTISING_TIMES_NEEDED +import pytest from homeassistant.components.bluetooth import async_set_fallback_availability_interval from homeassistant.core import HomeAssistant @@ -13,11 +14,8 @@ from . import ( ) -async def test_sensor_unavailable( - hass: HomeAssistant, - enable_bluetooth: None, - entity_registry_enabled_by_default: None, -) -> None: +@pytest.mark.usefixtures("enable_bluetooth", "entity_registry_enabled_by_default") +async def test_sensor_unavailable(hass: HomeAssistant) -> None: """Test sensors are unavailable.""" await async_mock_config_entry(hass) @@ -26,11 +24,8 @@ async def test_sensor_unavailable( assert state.state == "unavailable" -async def test_sensors_already_home( - hass: HomeAssistant, - enable_bluetooth: None, - entity_registry_enabled_by_default: None, -) -> None: +@pytest.mark.usefixtures("enable_bluetooth", "entity_registry_enabled_by_default") +async def test_sensors_already_home(hass: HomeAssistant) -> None: """Test sensors get value when we start at home.""" await async_inject_broadcast(hass, MAC_RPA_VALID_1) await async_mock_config_entry(hass) @@ -40,11 +35,8 @@ async def test_sensors_already_home( assert state.state == "-63" -async def test_sensors_come_home( - hass: HomeAssistant, - enable_bluetooth: None, - entity_registry_enabled_by_default: None, -) -> None: +@pytest.mark.usefixtures("enable_bluetooth", "entity_registry_enabled_by_default") +async def test_sensors_come_home(hass: HomeAssistant) -> None: """Test sensors get value when we receive a broadcast.""" await async_mock_config_entry(hass) await async_inject_broadcast(hass, MAC_RPA_VALID_1) @@ -54,11 +46,8 @@ async def test_sensors_come_home( assert state.state == "-63" -async def test_estimated_broadcast_interval( - hass: HomeAssistant, - enable_bluetooth: None, - entity_registry_enabled_by_default: None, -) -> None: +@pytest.mark.usefixtures("enable_bluetooth", "entity_registry_enabled_by_default") +async def test_estimated_broadcast_interval(hass: HomeAssistant) -> None: """Test sensors get value when we receive a broadcast.""" await async_mock_config_entry(hass) await async_inject_broadcast(hass, MAC_RPA_VALID_1) From f120f55d860818c81c4d9f562ebd72f3cec96db3 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 4 Jun 2024 16:20:11 +0200 Subject: [PATCH 0211/1445] Move enable_bluetooth fixture to decorator (#118803) --- tests/components/esphome/test_diagnostics.py | 3 ++- tests/components/ibeacon/test_config_flow.py | 13 ++++++----- .../private_ble_device/test_config_flow.py | 22 ++++++++++++------- 3 files changed, 24 insertions(+), 14 deletions(-) diff --git a/tests/components/esphome/test_diagnostics.py b/tests/components/esphome/test_diagnostics.py index 1cf4f77875f..4fb8f993aca 100644 --- a/tests/components/esphome/test_diagnostics.py +++ b/tests/components/esphome/test_diagnostics.py @@ -2,6 +2,7 @@ from unittest.mock import ANY +import pytest from syrupy import SnapshotAssertion from homeassistant.components import bluetooth @@ -14,11 +15,11 @@ from tests.components.diagnostics import get_diagnostics_for_config_entry from tests.typing import ClientSessionGenerator +@pytest.mark.usefixtures("enable_bluetooth") async def test_diagnostics( hass: HomeAssistant, hass_client: ClientSessionGenerator, init_integration: MockConfigEntry, - enable_bluetooth: None, mock_dashboard, snapshot: SnapshotAssertion, ) -> None: diff --git a/tests/components/ibeacon/test_config_flow.py b/tests/components/ibeacon/test_config_flow.py index 0833508d03f..3b5aadfaeab 100644 --- a/tests/components/ibeacon/test_config_flow.py +++ b/tests/components/ibeacon/test_config_flow.py @@ -2,6 +2,8 @@ from unittest.mock import patch +import pytest + from homeassistant import config_entries from homeassistant.components.ibeacon.const import CONF_ALLOW_NAMELESS_UUIDS, DOMAIN from homeassistant.core import HomeAssistant @@ -22,7 +24,8 @@ async def test_setup_user_no_bluetooth( assert result["reason"] == "bluetooth_not_available" -async def test_setup_user(hass: HomeAssistant, enable_bluetooth: None) -> None: +@pytest.mark.usefixtures("enable_bluetooth") +async def test_setup_user(hass: HomeAssistant) -> None: """Test setting up via user interaction with bluetooth enabled.""" result = await hass.config_entries.flow.async_init( DOMAIN, @@ -39,9 +42,8 @@ async def test_setup_user(hass: HomeAssistant, enable_bluetooth: None) -> None: assert result2["data"] == {} -async def test_setup_user_already_setup( - hass: HomeAssistant, enable_bluetooth: None -) -> None: +@pytest.mark.usefixtures("enable_bluetooth") +async def test_setup_user_already_setup(hass: HomeAssistant) -> None: """Test setting up via user when already setup .""" MockConfigEntry(domain=DOMAIN).add_to_hass(hass) result = await hass.config_entries.flow.async_init( @@ -52,7 +54,8 @@ async def test_setup_user_already_setup( assert result["reason"] == "single_instance_allowed" -async def test_options_flow(hass: HomeAssistant, enable_bluetooth: None) -> None: +@pytest.mark.usefixtures("enable_bluetooth") +async def test_options_flow(hass: HomeAssistant) -> None: """Test config flow options.""" config_entry = MockConfigEntry(domain=DOMAIN) config_entry.add_to_hass(hass) diff --git a/tests/components/private_ble_device/test_config_flow.py b/tests/components/private_ble_device/test_config_flow.py index a8821dddace..7c9b4807621 100644 --- a/tests/components/private_ble_device/test_config_flow.py +++ b/tests/components/private_ble_device/test_config_flow.py @@ -2,6 +2,8 @@ from unittest.mock import patch +import pytest + from homeassistant import config_entries from homeassistant.components.private_ble_device import const from homeassistant.core import HomeAssistant @@ -30,7 +32,8 @@ async def test_setup_user_no_bluetooth( assert result["reason"] == "bluetooth_not_available" -async def test_invalid_irk(hass: HomeAssistant, enable_bluetooth: None) -> None: +@pytest.mark.usefixtures("enable_bluetooth") +async def test_invalid_irk(hass: HomeAssistant) -> None: """Test invalid irk.""" result = await hass.config_entries.flow.async_init( const.DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -43,7 +46,8 @@ async def test_invalid_irk(hass: HomeAssistant, enable_bluetooth: None) -> None: assert_form_error(result, "irk", "irk_not_valid") -async def test_invalid_irk_base64(hass: HomeAssistant, enable_bluetooth: None) -> None: +@pytest.mark.usefixtures("enable_bluetooth") +async def test_invalid_irk_base64(hass: HomeAssistant) -> None: """Test invalid irk.""" result = await hass.config_entries.flow.async_init( const.DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -56,7 +60,8 @@ async def test_invalid_irk_base64(hass: HomeAssistant, enable_bluetooth: None) - assert_form_error(result, "irk", "irk_not_valid") -async def test_invalid_irk_hex(hass: HomeAssistant, enable_bluetooth: None) -> None: +@pytest.mark.usefixtures("enable_bluetooth") +async def test_invalid_irk_hex(hass: HomeAssistant) -> None: """Test invalid irk.""" result = await hass.config_entries.flow.async_init( const.DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -69,7 +74,8 @@ async def test_invalid_irk_hex(hass: HomeAssistant, enable_bluetooth: None) -> N assert_form_error(result, "irk", "irk_not_valid") -async def test_irk_not_found(hass: HomeAssistant, enable_bluetooth: None) -> None: +@pytest.mark.usefixtures("enable_bluetooth") +async def test_irk_not_found(hass: HomeAssistant) -> None: """Test irk not found.""" result = await hass.config_entries.flow.async_init( const.DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -83,7 +89,8 @@ async def test_irk_not_found(hass: HomeAssistant, enable_bluetooth: None) -> Non assert_form_error(result, "irk", "irk_not_found") -async def test_flow_works(hass: HomeAssistant, enable_bluetooth: None) -> None: +@pytest.mark.usefixtures("enable_bluetooth") +async def test_flow_works(hass: HomeAssistant) -> None: """Test config flow works.""" inject_bluetooth_service_info( @@ -120,9 +127,8 @@ async def test_flow_works(hass: HomeAssistant, enable_bluetooth: None) -> None: assert result["result"].unique_id == "00000000000000000000000000000000" -async def test_flow_works_by_base64( - hass: HomeAssistant, enable_bluetooth: None -) -> None: +@pytest.mark.usefixtures("enable_bluetooth") +async def test_flow_works_by_base64(hass: HomeAssistant) -> None: """Test config flow works.""" inject_bluetooth_service_info( From 80975d7a6393048b1405e790b7c090f978c573f8 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 4 Jun 2024 16:26:07 +0200 Subject: [PATCH 0212/1445] Move None bluetooth fixtures to decorator (#118802) --- tests/components/bluetooth/conftest.py | 9 +- .../bluetooth/test_advertisement_tracker.py | 28 +-- tests/components/bluetooth/test_api.py | 14 +- .../components/bluetooth/test_base_scanner.py | 30 ++- .../components/bluetooth/test_config_flow.py | 41 ++-- .../components/bluetooth/test_diagnostics.py | 9 +- tests/components/bluetooth/test_init.py | 197 +++++++++++------- tests/components/bluetooth/test_manager.py | 45 ++-- tests/components/bluetooth/test_models.py | 38 ++-- tests/components/bluetooth/test_scanner.py | 48 +++-- tests/components/bluetooth/test_usage.py | 6 +- tests/components/bluetooth/test_wrappers.py | 18 +- 12 files changed, 253 insertions(+), 230 deletions(-) diff --git a/tests/components/bluetooth/conftest.py b/tests/components/bluetooth/conftest.py index 17fbb318248..b99c1e77eb8 100644 --- a/tests/components/bluetooth/conftest.py +++ b/tests/components/bluetooth/conftest.py @@ -1,5 +1,6 @@ """Tests for the bluetooth component.""" +from collections.abc import Generator from unittest.mock import patch from bleak_retry_connector import bleak_manager @@ -74,7 +75,7 @@ def mock_operating_system_90(): @pytest.fixture(name="macos_adapter") -def macos_adapter(): +def macos_adapter() -> Generator[None, None, None]: """Fixture that mocks the macos adapter.""" with ( patch("bleak.get_platform_scanner_backend_type"), @@ -109,7 +110,7 @@ def windows_adapter(): @pytest.fixture(name="no_adapters") -def no_adapter_fixture(): +def no_adapter_fixture() -> Generator[None, None, None]: """Fixture that mocks no adapters on Linux.""" with ( patch( @@ -137,7 +138,7 @@ def no_adapter_fixture(): @pytest.fixture(name="one_adapter") -def one_adapter_fixture(): +def one_adapter_fixture() -> Generator[None, None, None]: """Fixture that mocks one adapter on Linux.""" with ( patch( @@ -176,7 +177,7 @@ def one_adapter_fixture(): @pytest.fixture(name="two_adapters") -def two_adapters_fixture(): +def two_adapters_fixture() -> Generator[None, None, None]: """Fixture that mocks two adapters on Linux.""" with ( patch( diff --git a/tests/components/bluetooth/test_advertisement_tracker.py b/tests/components/bluetooth/test_advertisement_tracker.py index 12d34e0a7bc..85feca83f00 100644 --- a/tests/components/bluetooth/test_advertisement_tracker.py +++ b/tests/components/bluetooth/test_advertisement_tracker.py @@ -33,11 +33,9 @@ from tests.common import async_fire_time_changed ONE_HOUR_SECONDS = 3600 +@pytest.mark.usefixtures("enable_bluetooth", "macos_adapter") async def test_advertisment_interval_shorter_than_adapter_stack_timeout( hass: HomeAssistant, - caplog: pytest.LogCaptureFixture, - enable_bluetooth: None, - macos_adapter: None, ) -> None: """Test we can determine the advertisement interval.""" start_monotonic_time = time.monotonic() @@ -83,11 +81,9 @@ async def test_advertisment_interval_shorter_than_adapter_stack_timeout( switchbot_device_unavailable_cancel() +@pytest.mark.usefixtures("enable_bluetooth", "macos_adapter") async def test_advertisment_interval_longer_than_adapter_stack_timeout_connectable( hass: HomeAssistant, - caplog: pytest.LogCaptureFixture, - enable_bluetooth: None, - macos_adapter: None, ) -> None: """Test device with a long advertisement interval.""" start_monotonic_time = time.monotonic() @@ -135,11 +131,9 @@ async def test_advertisment_interval_longer_than_adapter_stack_timeout_connectab switchbot_device_unavailable_cancel() +@pytest.mark.usefixtures("enable_bluetooth", "macos_adapter") async def test_advertisment_interval_longer_than_adapter_stack_timeout_adapter_change_connectable( hass: HomeAssistant, - caplog: pytest.LogCaptureFixture, - enable_bluetooth: None, - macos_adapter: None, ) -> None: """Test device with a long advertisement interval with an adapter change.""" start_monotonic_time = time.monotonic() @@ -200,11 +194,9 @@ async def test_advertisment_interval_longer_than_adapter_stack_timeout_adapter_c switchbot_device_unavailable_cancel() +@pytest.mark.usefixtures("enable_bluetooth", "macos_adapter") async def test_advertisment_interval_longer_than_adapter_stack_timeout_not_connectable( hass: HomeAssistant, - caplog: pytest.LogCaptureFixture, - enable_bluetooth: None, - macos_adapter: None, ) -> None: """Test device with a long advertisement interval that is not connectable not reaching the advertising interval.""" start_monotonic_time = time.monotonic() @@ -255,11 +247,9 @@ async def test_advertisment_interval_longer_than_adapter_stack_timeout_not_conne switchbot_device_unavailable_cancel() +@pytest.mark.usefixtures("enable_bluetooth", "macos_adapter") async def test_advertisment_interval_shorter_than_adapter_stack_timeout_adapter_change_not_connectable( hass: HomeAssistant, - caplog: pytest.LogCaptureFixture, - enable_bluetooth: None, - macos_adapter: None, ) -> None: """Test device with a short advertisement interval with an adapter change that is not connectable.""" start_monotonic_time = time.monotonic() @@ -330,11 +320,9 @@ async def test_advertisment_interval_shorter_than_adapter_stack_timeout_adapter_ switchbot_device_unavailable_cancel() +@pytest.mark.usefixtures("enable_bluetooth", "macos_adapter") async def test_advertisment_interval_longer_than_adapter_stack_timeout_adapter_change_not_connectable( hass: HomeAssistant, - caplog: pytest.LogCaptureFixture, - enable_bluetooth: None, - macos_adapter: None, ) -> None: """Test device with a long advertisement interval with an adapter change that is not connectable.""" start_monotonic_time = time.monotonic() @@ -436,11 +424,9 @@ async def test_advertisment_interval_longer_than_adapter_stack_timeout_adapter_c switchbot_device_unavailable_cancel() +@pytest.mark.usefixtures("enable_bluetooth", "macos_adapter") async def test_advertisment_interval_longer_increasing_than_adapter_stack_timeout_adapter_change_not_connectable( hass: HomeAssistant, - caplog: pytest.LogCaptureFixture, - enable_bluetooth: None, - macos_adapter: None, ) -> None: """Test device with a increasing advertisement interval with an adapter change that is not connectable.""" start_monotonic_time = time.monotonic() diff --git a/tests/components/bluetooth/test_api.py b/tests/components/bluetooth/test_api.py index a3ec3814a92..1468367fd9a 100644 --- a/tests/components/bluetooth/test_api.py +++ b/tests/components/bluetooth/test_api.py @@ -24,7 +24,8 @@ from . import ( ) -async def test_scanner_by_source(hass: HomeAssistant, enable_bluetooth: None) -> None: +@pytest.mark.usefixtures("enable_bluetooth") +async def test_scanner_by_source(hass: HomeAssistant) -> None: """Test we can get a scanner by source.""" hci2_scanner = FakeScanner("hci2", "hci2") @@ -40,16 +41,16 @@ async def test_monotonic_time() -> None: assert MONOTONIC_TIME() == pytest.approx(time.monotonic(), abs=0.1) -async def test_async_get_advertisement_callback( - hass: HomeAssistant, enable_bluetooth: None -) -> None: +@pytest.mark.usefixtures("enable_bluetooth") +async def test_async_get_advertisement_callback(hass: HomeAssistant) -> None: """Test getting advertisement callback.""" callback = bluetooth.async_get_advertisement_callback(hass) assert callback is not None +@pytest.mark.usefixtures("enable_bluetooth") async def test_async_scanner_devices_by_address_connectable( - hass: HomeAssistant, enable_bluetooth: None + hass: HomeAssistant, ) -> None: """Test getting scanner devices by address with connectable devices.""" manager = _get_manager() @@ -105,8 +106,9 @@ async def test_async_scanner_devices_by_address_connectable( cancel() +@pytest.mark.usefixtures("enable_bluetooth") async def test_async_scanner_devices_by_address_non_connectable( - hass: HomeAssistant, enable_bluetooth: None + hass: HomeAssistant, ) -> None: """Test getting scanner devices by address with non-connectable devices.""" manager = _get_manager() diff --git a/tests/components/bluetooth/test_base_scanner.py b/tests/components/bluetooth/test_base_scanner.py index 0839c9c56a4..efd9708a167 100644 --- a/tests/components/bluetooth/test_base_scanner.py +++ b/tests/components/bluetooth/test_base_scanner.py @@ -66,9 +66,8 @@ class FakeScanner(BaseHaRemoteScanner): @pytest.mark.parametrize("name_2", [None, "w"]) -async def test_remote_scanner( - hass: HomeAssistant, enable_bluetooth: None, name_2: str | None -) -> None: +@pytest.mark.usefixtures("enable_bluetooth") +async def test_remote_scanner(hass: HomeAssistant, name_2: str | None) -> None: """Test the remote scanner base class merges advertisement_data.""" manager = _get_manager() @@ -159,9 +158,8 @@ async def test_remote_scanner( unsetup() -async def test_remote_scanner_expires_connectable( - hass: HomeAssistant, enable_bluetooth: None -) -> None: +@pytest.mark.usefixtures("enable_bluetooth") +async def test_remote_scanner_expires_connectable(hass: HomeAssistant) -> None: """Test the remote scanner expires stale connectable data.""" manager = _get_manager() @@ -213,9 +211,8 @@ async def test_remote_scanner_expires_connectable( unsetup() -async def test_remote_scanner_expires_non_connectable( - hass: HomeAssistant, enable_bluetooth: None -) -> None: +@pytest.mark.usefixtures("enable_bluetooth") +async def test_remote_scanner_expires_non_connectable(hass: HomeAssistant) -> None: """Test the remote scanner expires stale non connectable data.""" manager = _get_manager() @@ -287,9 +284,8 @@ async def test_remote_scanner_expires_non_connectable( unsetup() -async def test_base_scanner_connecting_behavior( - hass: HomeAssistant, enable_bluetooth: None -) -> None: +@pytest.mark.usefixtures("enable_bluetooth") +async def test_base_scanner_connecting_behavior(hass: HomeAssistant) -> None: """Test that the default behavior is to mark the scanner as not scanning when connecting.""" manager = _get_manager() @@ -392,9 +388,8 @@ async def test_restore_history_remote_adapter( unsetup() -async def test_device_with_ten_minute_advertising_interval( - hass: HomeAssistant, caplog: pytest.LogCaptureFixture, enable_bluetooth: None -) -> None: +@pytest.mark.usefixtures("enable_bluetooth") +async def test_device_with_ten_minute_advertising_interval(hass: HomeAssistant) -> None: """Test a device with a 10 minute advertising interval.""" manager = _get_manager() @@ -496,9 +491,8 @@ async def test_device_with_ten_minute_advertising_interval( unsetup() -async def test_scanner_stops_responding( - hass: HomeAssistant, caplog: pytest.LogCaptureFixture, enable_bluetooth: None -) -> None: +@pytest.mark.usefixtures("enable_bluetooth") +async def test_scanner_stops_responding(hass: HomeAssistant) -> None: """Test we mark a scanner are not scanning when it stops responding.""" manager = _get_manager() diff --git a/tests/components/bluetooth/test_config_flow.py b/tests/components/bluetooth/test_config_flow.py index 33474280ec4..f10c68f8f3f 100644 --- a/tests/components/bluetooth/test_config_flow.py +++ b/tests/components/bluetooth/test_config_flow.py @@ -3,6 +3,7 @@ from unittest.mock import MagicMock, patch from bluetooth_adapters import DEFAULT_ADDRESS, AdapterDetails +import pytest from homeassistant import config_entries from homeassistant.components.bluetooth.const import ( @@ -19,12 +20,12 @@ from tests.common import MockConfigEntry from tests.typing import WebSocketGenerator +@pytest.mark.usefixtures("macos_adapter") async def test_options_flow_disabled_not_setup( hass: HomeAssistant, hass_ws_client: WebSocketGenerator, mock_bleak_scanner_start: MagicMock, mock_bluetooth_adapters: None, - macos_adapter: None, ) -> None: """Test options are disabled if the integration has not been setup.""" await async_setup_component(hass, "config", {}) @@ -49,7 +50,8 @@ async def test_options_flow_disabled_not_setup( await hass.config_entries.async_unload(entry.entry_id) -async def test_async_step_user_macos(hass: HomeAssistant, macos_adapter: None) -> None: +@pytest.mark.usefixtures("macos_adapter") +async def test_async_step_user_macos(hass: HomeAssistant) -> None: """Test setting up manually with one adapter on MacOS.""" result = await hass.config_entries.flow.async_init( DOMAIN, @@ -73,9 +75,8 @@ async def test_async_step_user_macos(hass: HomeAssistant, macos_adapter: None) - assert len(mock_setup_entry.mock_calls) == 1 -async def test_async_step_user_linux_one_adapter( - hass: HomeAssistant, one_adapter: None -) -> None: +@pytest.mark.usefixtures("one_adapter") +async def test_async_step_user_linux_one_adapter(hass: HomeAssistant) -> None: """Test setting up manually with one adapter on Linux.""" result = await hass.config_entries.flow.async_init( DOMAIN, @@ -117,9 +118,8 @@ async def test_async_step_user_linux_crashed_adapter( assert result["reason"] == "no_adapters" -async def test_async_step_user_linux_two_adapters( - hass: HomeAssistant, two_adapters: None -) -> None: +@pytest.mark.usefixtures("two_adapters") +async def test_async_step_user_linux_two_adapters(hass: HomeAssistant) -> None: """Test setting up manually with two adapters on Linux.""" result = await hass.config_entries.flow.async_init( DOMAIN, @@ -147,9 +147,8 @@ async def test_async_step_user_linux_two_adapters( assert len(mock_setup_entry.mock_calls) == 1 -async def test_async_step_user_only_allows_one( - hass: HomeAssistant, macos_adapter: None -) -> None: +@pytest.mark.usefixtures("macos_adapter") +async def test_async_step_user_only_allows_one(hass: HomeAssistant) -> None: """Test setting up manually with an existing entry.""" entry = MockConfigEntry(domain=DOMAIN, unique_id=DEFAULT_ADDRESS) entry.add_to_hass(hass) @@ -199,8 +198,9 @@ async def test_async_step_integration_discovery(hass: HomeAssistant) -> None: assert len(mock_setup_entry.mock_calls) == 1 +@pytest.mark.usefixtures("one_adapter") async def test_async_step_integration_discovery_during_onboarding_one_adapter( - hass: HomeAssistant, one_adapter: None + hass: HomeAssistant, ) -> None: """Test setting up from integration discovery during onboarding.""" details = AdapterDetails( @@ -232,8 +232,9 @@ async def test_async_step_integration_discovery_during_onboarding_one_adapter( assert len(mock_onboarding.mock_calls) == 1 +@pytest.mark.usefixtures("two_adapters") async def test_async_step_integration_discovery_during_onboarding_two_adapters( - hass: HomeAssistant, two_adapters: None + hass: HomeAssistant, ) -> None: """Test setting up from integration discovery during onboarding.""" details1 = AdapterDetails( @@ -281,8 +282,9 @@ async def test_async_step_integration_discovery_during_onboarding_two_adapters( assert len(mock_onboarding.mock_calls) == 2 +@pytest.mark.usefixtures("macos_adapter") async def test_async_step_integration_discovery_during_onboarding( - hass: HomeAssistant, macos_adapter: None + hass: HomeAssistant, ) -> None: """Test setting up from integration discovery during onboarding.""" details = AdapterDetails( @@ -336,11 +338,11 @@ async def test_async_step_integration_discovery_already_exists( assert result["reason"] == "already_configured" +@pytest.mark.usefixtures("one_adapter") async def test_options_flow_linux( hass: HomeAssistant, mock_bleak_scanner_start: MagicMock, mock_bluetooth_adapters: None, - one_adapter: None, ) -> None: """Test options on Linux.""" entry = MockConfigEntry( @@ -390,12 +392,12 @@ async def test_options_flow_linux( await hass.config_entries.async_unload(entry.entry_id) +@pytest.mark.usefixtures("macos_adapter") async def test_options_flow_disabled_macos( hass: HomeAssistant, hass_ws_client: WebSocketGenerator, mock_bleak_scanner_start: MagicMock, mock_bluetooth_adapters: None, - macos_adapter: None, ) -> None: """Test options are disabled on MacOS.""" await async_setup_component(hass, "config", {}) @@ -420,12 +422,12 @@ async def test_options_flow_disabled_macos( await hass.config_entries.async_unload(entry.entry_id) +@pytest.mark.usefixtures("one_adapter") async def test_options_flow_enabled_linux( hass: HomeAssistant, hass_ws_client: WebSocketGenerator, mock_bleak_scanner_start: MagicMock, mock_bluetooth_adapters: None, - one_adapter: None, ) -> None: """Test options are enabled on Linux.""" await async_setup_component(hass, "config", {}) @@ -453,9 +455,8 @@ async def test_options_flow_enabled_linux( await hass.config_entries.async_unload(entry.entry_id) -async def test_async_step_user_linux_adapter_is_ignored( - hass: HomeAssistant, one_adapter: None -) -> None: +@pytest.mark.usefixtures("one_adapter") +async def test_async_step_user_linux_adapter_is_ignored(hass: HomeAssistant) -> None: """Test we give a hint that the adapter is ignored.""" entry = MockConfigEntry( domain=DOMAIN, diff --git a/tests/components/bluetooth/test_diagnostics.py b/tests/components/bluetooth/test_diagnostics.py index 462c43380a8..7050e665df7 100644 --- a/tests/components/bluetooth/test_diagnostics.py +++ b/tests/components/bluetooth/test_diagnostics.py @@ -4,6 +4,7 @@ from unittest.mock import ANY, MagicMock, patch from bleak.backends.scanner import AdvertisementData, BLEDevice from bluetooth_adapters import DEFAULT_ADDRESS +import pytest from homeassistant.components import bluetooth from homeassistant.components.bluetooth import ( @@ -43,12 +44,11 @@ class FakeHaScanner(FakeScannerMixin, HaScanner): @patch("homeassistant.components.bluetooth.HaScanner", FakeHaScanner) +@pytest.mark.usefixtures("enable_bluetooth", "two_adapters") async def test_diagnostics( hass: HomeAssistant, hass_client: ClientSessionGenerator, mock_bleak_scanner_start: MagicMock, - enable_bluetooth: None, - two_adapters: None, ) -> None: """Test we can setup and unsetup bluetooth with multiple adapters.""" # Normally we do not want to patch our classes, but since bleak will import @@ -237,12 +237,12 @@ async def test_diagnostics( @patch("homeassistant.components.bluetooth.HaScanner", FakeHaScanner) +@pytest.mark.usefixtures("macos_adapter") async def test_diagnostics_macos( hass: HomeAssistant, hass_client: ClientSessionGenerator, mock_bleak_scanner_start: MagicMock, mock_bluetooth_adapters: None, - macos_adapter, ) -> None: """Test diagnostics for macos.""" # Normally we do not want to patch our classes, but since bleak will import @@ -414,13 +414,12 @@ async def test_diagnostics_macos( @patch("homeassistant.components.bluetooth.HaScanner", FakeHaScanner) +@pytest.mark.usefixtures("enable_bluetooth", "one_adapter") async def test_diagnostics_remote_adapter( hass: HomeAssistant, hass_client: ClientSessionGenerator, mock_bleak_scanner_start: MagicMock, mock_bluetooth_adapters: None, - enable_bluetooth: None, - one_adapter: None, ) -> None: """Test diagnostics for remote adapter.""" manager = _get_manager() diff --git a/tests/components/bluetooth/test_init.py b/tests/components/bluetooth/test_init.py index a3eb3ef464d..197ca760c5f 100644 --- a/tests/components/bluetooth/test_init.py +++ b/tests/components/bluetooth/test_init.py @@ -59,8 +59,9 @@ from . import ( from tests.common import MockConfigEntry, async_fire_time_changed +@pytest.mark.usefixtures("enable_bluetooth") async def test_setup_and_stop( - hass: HomeAssistant, mock_bleak_scanner_start: MagicMock, enable_bluetooth: None + hass: HomeAssistant, mock_bleak_scanner_start: MagicMock ) -> None: """Test we and setup and stop the scanner.""" mock_bt = [ @@ -84,8 +85,9 @@ async def test_setup_and_stop( assert len(mock_bleak_scanner_start.mock_calls) == 1 +@pytest.mark.usefixtures("one_adapter") async def test_setup_and_stop_passive( - hass: HomeAssistant, mock_bleak_scanner_start: MagicMock, one_adapter: None + hass: HomeAssistant, mock_bleak_scanner_start: MagicMock ) -> None: """Test we and setup and stop the scanner the passive scanner.""" entry = MockConfigEntry( @@ -183,8 +185,9 @@ async def test_setup_and_stop_old_bluez( } +@pytest.mark.usefixtures("one_adapter") async def test_setup_and_stop_no_bluetooth( - hass: HomeAssistant, caplog: pytest.LogCaptureFixture, one_adapter: None + hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: """Test we fail gracefully when bluetooth is not available.""" mock_bt = [ @@ -211,8 +214,9 @@ async def test_setup_and_stop_no_bluetooth( assert "Failed to initialize Bluetooth" in caplog.text +@pytest.mark.usefixtures("macos_adapter") async def test_setup_and_stop_broken_bluetooth( - hass: HomeAssistant, caplog: pytest.LogCaptureFixture, macos_adapter: None + hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: """Test we fail gracefully when bluetooth/dbus is broken.""" mock_bt = [] @@ -236,8 +240,9 @@ async def test_setup_and_stop_broken_bluetooth( assert len(bluetooth.async_discovered_service_info(hass)) == 0 +@pytest.mark.usefixtures("macos_adapter") async def test_setup_and_stop_broken_bluetooth_hanging( - hass: HomeAssistant, caplog: pytest.LogCaptureFixture, macos_adapter: None + hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: """Test we fail gracefully when bluetooth/dbus is hanging.""" mock_bt = [] @@ -265,8 +270,9 @@ async def test_setup_and_stop_broken_bluetooth_hanging( assert "Timed out starting Bluetooth" in caplog.text +@pytest.mark.usefixtures("macos_adapter") async def test_setup_and_retry_adapter_not_yet_available( - hass: HomeAssistant, caplog: pytest.LogCaptureFixture, macos_adapter: None + hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: """Test we retry if the adapter is not yet available.""" mock_bt = [] @@ -304,8 +310,9 @@ async def test_setup_and_retry_adapter_not_yet_available( await hass.async_block_till_done() +@pytest.mark.usefixtures("macos_adapter") async def test_no_race_during_manual_reload_in_retry_state( - hass: HomeAssistant, caplog: pytest.LogCaptureFixture, macos_adapter: None + hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: """Test we can successfully reload when the entry is in a retry state.""" mock_bt = [] @@ -344,8 +351,9 @@ async def test_no_race_during_manual_reload_in_retry_state( await hass.async_block_till_done() +@pytest.mark.usefixtures("macos_adapter") async def test_calling_async_discovered_devices_no_bluetooth( - hass: HomeAssistant, caplog: pytest.LogCaptureFixture, macos_adapter: None + hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: """Test we fail gracefully when asking for discovered devices and there is no blueooth.""" mock_bt = [] @@ -370,8 +378,9 @@ async def test_calling_async_discovered_devices_no_bluetooth( assert not bluetooth.async_address_present(hass, "aa:bb:bb:dd:ee:ff") +@pytest.mark.usefixtures("enable_bluetooth") async def test_discovery_match_by_service_uuid( - hass: HomeAssistant, mock_bleak_scanner_start: MagicMock, enable_bluetooth: None + hass: HomeAssistant, mock_bleak_scanner_start: MagicMock ) -> None: """Test bluetooth discovery match by service_uuid.""" mock_bt = [ @@ -467,8 +476,9 @@ def _domains_from_mock_config_flow(mock_config_flow: Mock) -> list[str]: return [call[1][0] for call in mock_config_flow.mock_calls if call[1][0] != DOMAIN] +@pytest.mark.usefixtures("macos_adapter") async def test_discovery_match_by_service_uuid_connectable( - hass: HomeAssistant, mock_bleak_scanner_start: MagicMock, macos_adapter: None + hass: HomeAssistant, mock_bleak_scanner_start: MagicMock ) -> None: """Test bluetooth discovery match by service_uuid and the ble device is connectable.""" mock_bt = [ @@ -518,8 +528,9 @@ async def test_discovery_match_by_service_uuid_connectable( assert called_domains == ["switchbot"] +@pytest.mark.usefixtures("macos_adapter") async def test_discovery_match_by_service_uuid_not_connectable( - hass: HomeAssistant, mock_bleak_scanner_start: MagicMock, macos_adapter: None + hass: HomeAssistant, mock_bleak_scanner_start: MagicMock ) -> None: """Test bluetooth discovery match by service_uuid and the ble device is not connectable.""" mock_bt = [ @@ -567,8 +578,9 @@ async def test_discovery_match_by_service_uuid_not_connectable( assert len(_domains_from_mock_config_flow(mock_config_flow)) == 0 +@pytest.mark.usefixtures("macos_adapter") async def test_discovery_match_by_name_connectable_false( - hass: HomeAssistant, mock_bleak_scanner_start: MagicMock, macos_adapter: None + hass: HomeAssistant, mock_bleak_scanner_start: MagicMock ) -> None: """Test bluetooth discovery match by name and the integration will take non-connectable devices.""" mock_bt = [ @@ -645,8 +657,9 @@ async def test_discovery_match_by_name_connectable_false( assert _domains_from_mock_config_flow(mock_config_flow) == ["qingping"] +@pytest.mark.usefixtures("macos_adapter") async def test_discovery_match_by_local_name( - hass: HomeAssistant, mock_bleak_scanner_start: MagicMock, macos_adapter: None + hass: HomeAssistant, mock_bleak_scanner_start: MagicMock ) -> None: """Test bluetooth discovery match by local_name.""" mock_bt = [{"domain": "switchbot", "local_name": "wohand"}] @@ -683,8 +696,9 @@ async def test_discovery_match_by_local_name( assert mock_config_flow.mock_calls[0][1][0] == "switchbot" +@pytest.mark.usefixtures("macos_adapter") async def test_discovery_match_by_manufacturer_id_and_manufacturer_data_start( - hass: HomeAssistant, mock_bleak_scanner_start: MagicMock, macos_adapter: None + hass: HomeAssistant, mock_bleak_scanner_start: MagicMock ) -> None: """Test bluetooth discovery match by manufacturer_id and manufacturer_data_start.""" mock_bt = [ @@ -759,8 +773,9 @@ async def test_discovery_match_by_manufacturer_id_and_manufacturer_data_start( assert len(mock_config_flow.mock_calls) == 0 +@pytest.mark.usefixtures("macos_adapter") async def test_discovery_match_by_service_data_uuid_then_others( - hass: HomeAssistant, mock_bleak_scanner_start: MagicMock, macos_adapter: None + hass: HomeAssistant, mock_bleak_scanner_start: MagicMock ) -> None: """Test bluetooth discovery match by service_data_uuid and then other fields.""" mock_bt = [ @@ -913,8 +928,9 @@ async def test_discovery_match_by_service_data_uuid_then_others( assert len(mock_config_flow.mock_calls) == 0 +@pytest.mark.usefixtures("macos_adapter") async def test_discovery_match_by_service_data_uuid_when_format_changes( - hass: HomeAssistant, mock_bleak_scanner_start: MagicMock, macos_adapter: None + hass: HomeAssistant, mock_bleak_scanner_start: MagicMock ) -> None: """Test bluetooth discovery match by service_data_uuid when format changes.""" mock_bt = [ @@ -996,8 +1012,9 @@ async def test_discovery_match_by_service_data_uuid_when_format_changes( mock_config_flow.reset_mock() +@pytest.mark.usefixtures("macos_adapter") async def test_discovery_match_by_service_data_uuid_bthome( - hass: HomeAssistant, mock_bleak_scanner_start: MagicMock, macos_adapter: None + hass: HomeAssistant, mock_bleak_scanner_start: MagicMock ) -> None: """Test bluetooth discovery match by service_data_uuid for bthome.""" mock_bt = [ @@ -1038,8 +1055,9 @@ async def test_discovery_match_by_service_data_uuid_bthome( mock_config_flow.reset_mock() +@pytest.mark.usefixtures("macos_adapter") async def test_discovery_match_first_by_service_uuid_and_then_manufacturer_id( - hass: HomeAssistant, mock_bleak_scanner_start: MagicMock, macos_adapter: None + hass: HomeAssistant, mock_bleak_scanner_start: MagicMock ) -> None: """Test bluetooth discovery matches twice for service_uuid and then manufacturer_id.""" mock_bt = [ @@ -1102,8 +1120,9 @@ async def test_discovery_match_first_by_service_uuid_and_then_manufacturer_id( assert len(mock_config_flow.mock_calls) == 0 +@pytest.mark.usefixtures("enable_bluetooth") async def test_rediscovery( - hass: HomeAssistant, mock_bleak_scanner_start: MagicMock, enable_bluetooth: None + hass: HomeAssistant, mock_bleak_scanner_start: MagicMock ) -> None: """Test bluetooth discovery can be re-enabled for a given domain.""" mock_bt = [ @@ -1149,8 +1168,9 @@ async def test_rediscovery( assert mock_config_flow.mock_calls[1][1][0] == "switchbot" +@pytest.mark.usefixtures("macos_adapter") async def test_async_discovered_device_api( - hass: HomeAssistant, mock_bleak_scanner_start: MagicMock, macos_adapter: None + hass: HomeAssistant, mock_bleak_scanner_start: MagicMock ) -> None: """Test the async_discovered_device API.""" mock_bt = [] @@ -1255,8 +1275,9 @@ async def test_async_discovered_device_api( assert bluetooth.async_address_present(hass, "44:44:33:11:23:45") is True +@pytest.mark.usefixtures("enable_bluetooth") async def test_register_callbacks( - hass: HomeAssistant, mock_bleak_scanner_start: MagicMock, enable_bluetooth: None + hass: HomeAssistant, mock_bleak_scanner_start: MagicMock ) -> None: """Test registering a callback.""" mock_bt = [] @@ -1336,10 +1357,10 @@ async def test_register_callbacks( assert service_info.manufacturer_id == 89 +@pytest.mark.usefixtures("enable_bluetooth") async def test_register_callbacks_raises_exception( hass: HomeAssistant, mock_bleak_scanner_start: MagicMock, - enable_bluetooth: None, caplog: pytest.LogCaptureFixture, ) -> None: """Test registering a callback that raises ValueError.""" @@ -1401,8 +1422,9 @@ async def test_register_callbacks_raises_exception( assert "ValueError" in caplog.text +@pytest.mark.usefixtures("enable_bluetooth") async def test_register_callback_by_address( - hass: HomeAssistant, mock_bleak_scanner_start: MagicMock, enable_bluetooth: None + hass: HomeAssistant, mock_bleak_scanner_start: MagicMock ) -> None: """Test registering a callback by address.""" mock_bt = [] @@ -1492,8 +1514,9 @@ async def test_register_callback_by_address( assert service_info.manufacturer_id == 89 +@pytest.mark.usefixtures("enable_bluetooth") async def test_register_callback_by_address_connectable_only( - hass: HomeAssistant, mock_bleak_scanner_start: MagicMock, enable_bluetooth: None + hass: HomeAssistant, mock_bleak_scanner_start: MagicMock ) -> None: """Test registering a callback by address connectable only.""" mock_bt = [] @@ -1571,8 +1594,9 @@ async def test_register_callback_by_address_connectable_only( assert len(non_connectable_callbacks) == 2 +@pytest.mark.usefixtures("enable_bluetooth") async def test_register_callback_by_manufacturer_id( - hass: HomeAssistant, mock_bleak_scanner_start: MagicMock, enable_bluetooth: None + hass: HomeAssistant, mock_bleak_scanner_start: MagicMock ) -> None: """Test registering a callback by manufacturer_id.""" mock_bt = [] @@ -1626,8 +1650,9 @@ async def test_register_callback_by_manufacturer_id( assert service_info.manufacturer_id == 21 +@pytest.mark.usefixtures("enable_bluetooth") async def test_register_callback_by_connectable( - hass: HomeAssistant, mock_bleak_scanner_start: MagicMock, enable_bluetooth: None + hass: HomeAssistant, mock_bleak_scanner_start: MagicMock ) -> None: """Test registering a callback by connectable.""" mock_bt = [] @@ -1681,8 +1706,9 @@ async def test_register_callback_by_connectable( assert service_info.name == "empty" +@pytest.mark.usefixtures("enable_bluetooth") async def test_not_filtering_wanted_apple_devices( - hass: HomeAssistant, mock_bleak_scanner_start: MagicMock, enable_bluetooth: None + hass: HomeAssistant, mock_bleak_scanner_start: MagicMock ) -> None: """Test filtering noisy apple devices.""" mock_bt = [] @@ -1741,8 +1767,9 @@ async def test_not_filtering_wanted_apple_devices( assert len(callbacks) == 3 +@pytest.mark.usefixtures("enable_bluetooth") async def test_filtering_noisy_apple_devices( - hass: HomeAssistant, mock_bleak_scanner_start: MagicMock, enable_bluetooth: None + hass: HomeAssistant, mock_bleak_scanner_start: MagicMock ) -> None: """Test filtering noisy apple devices.""" mock_bt = [] @@ -1791,8 +1818,9 @@ async def test_filtering_noisy_apple_devices( assert len(callbacks) == 0 +@pytest.mark.usefixtures("enable_bluetooth") async def test_register_callback_by_address_connectable_manufacturer_id( - hass: HomeAssistant, mock_bleak_scanner_start: MagicMock, enable_bluetooth: None + hass: HomeAssistant, mock_bleak_scanner_start: MagicMock ) -> None: """Test registering a callback by address, manufacturer_id, and connectable.""" mock_bt = [] @@ -1845,8 +1873,9 @@ async def test_register_callback_by_address_connectable_manufacturer_id( assert service_info.manufacturer_id == 21 +@pytest.mark.usefixtures("enable_bluetooth") async def test_register_callback_by_manufacturer_id_and_address( - hass: HomeAssistant, mock_bleak_scanner_start: MagicMock, enable_bluetooth: None + hass: HomeAssistant, mock_bleak_scanner_start: MagicMock ) -> None: """Test registering a callback by manufacturer_id and address.""" mock_bt = [] @@ -1910,8 +1939,9 @@ async def test_register_callback_by_manufacturer_id_and_address( assert service_info.manufacturer_id == 21 +@pytest.mark.usefixtures("enable_bluetooth") async def test_register_callback_by_service_uuid_and_address( - hass: HomeAssistant, mock_bleak_scanner_start: MagicMock, enable_bluetooth: None + hass: HomeAssistant, mock_bleak_scanner_start: MagicMock ) -> None: """Test registering a callback by service_uuid and address.""" mock_bt = [] @@ -1983,8 +2013,9 @@ async def test_register_callback_by_service_uuid_and_address( assert service_info.name == "switchbot" +@pytest.mark.usefixtures("enable_bluetooth") async def test_register_callback_by_service_data_uuid_and_address( - hass: HomeAssistant, mock_bleak_scanner_start: MagicMock, enable_bluetooth: None + hass: HomeAssistant, mock_bleak_scanner_start: MagicMock ) -> None: """Test registering a callback by service_data_uuid and address.""" mock_bt = [] @@ -2056,8 +2087,9 @@ async def test_register_callback_by_service_data_uuid_and_address( assert service_info.name == "switchbot" +@pytest.mark.usefixtures("enable_bluetooth") async def test_register_callback_by_local_name( - hass: HomeAssistant, mock_bleak_scanner_start: MagicMock, enable_bluetooth: None + hass: HomeAssistant, mock_bleak_scanner_start: MagicMock ) -> None: """Test registering a callback by local_name.""" mock_bt = [] @@ -2119,11 +2151,9 @@ async def test_register_callback_by_local_name( assert service_info.manufacturer_id == 21 +@pytest.mark.usefixtures("enable_bluetooth") async def test_register_callback_by_local_name_overly_broad( - hass: HomeAssistant, - mock_bleak_scanner_start: MagicMock, - enable_bluetooth: None, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mock_bleak_scanner_start: MagicMock ) -> None: """Test registering a callback by local_name that is too broad.""" mock_bt = [] @@ -2147,8 +2177,9 @@ async def test_register_callback_by_local_name_overly_broad( ) +@pytest.mark.usefixtures("enable_bluetooth") async def test_register_callback_by_service_data_uuid( - hass: HomeAssistant, mock_bleak_scanner_start: MagicMock, enable_bluetooth: None + hass: HomeAssistant, mock_bleak_scanner_start: MagicMock ) -> None: """Test registering a callback by service_data_uuid.""" mock_bt = [] @@ -2202,8 +2233,9 @@ async def test_register_callback_by_service_data_uuid( assert service_info.name == "xiaomi" +@pytest.mark.usefixtures("enable_bluetooth") async def test_register_callback_survives_reload( - hass: HomeAssistant, mock_bleak_scanner_start: MagicMock, enable_bluetooth: None + hass: HomeAssistant, mock_bleak_scanner_start: MagicMock ) -> None: """Test registering a callback by address survives bluetooth being reloaded.""" mock_bt = [] @@ -2265,8 +2297,9 @@ async def test_register_callback_survives_reload( cancel() +@pytest.mark.usefixtures("enable_bluetooth") async def test_process_advertisements_bail_on_good_advertisement( - hass: HomeAssistant, mock_bleak_scanner_start: MagicMock, enable_bluetooth: None + hass: HomeAssistant, mock_bleak_scanner_start: MagicMock ) -> None: """Test as soon as we see a 'good' advertisement we return it.""" done = asyncio.Future() @@ -2304,8 +2337,9 @@ async def test_process_advertisements_bail_on_good_advertisement( assert result.name == "wohand" +@pytest.mark.usefixtures("enable_bluetooth") async def test_process_advertisements_ignore_bad_advertisement( - hass: HomeAssistant, mock_bleak_scanner_start: MagicMock, enable_bluetooth: None + hass: HomeAssistant, mock_bleak_scanner_start: MagicMock ) -> None: """Check that we ignore bad advertisements.""" done = asyncio.Event() @@ -2358,8 +2392,9 @@ async def test_process_advertisements_ignore_bad_advertisement( assert result.service_data["00000d00-0000-1000-8000-00805f9b34fa"] == b"H\x10c" +@pytest.mark.usefixtures("enable_bluetooth") async def test_process_advertisements_timeout( - hass: HomeAssistant, mock_bleak_scanner_start: MagicMock, enable_bluetooth: None + hass: HomeAssistant, mock_bleak_scanner_start: MagicMock ) -> None: """Test we timeout if no advertisements at all.""" @@ -2372,8 +2407,9 @@ async def test_process_advertisements_timeout( ) +@pytest.mark.usefixtures("enable_bluetooth") async def test_wrapped_instance_with_filter( - hass: HomeAssistant, mock_bleak_scanner_start: MagicMock, enable_bluetooth: None + hass: HomeAssistant, mock_bleak_scanner_start: MagicMock ) -> None: """Test consumers can use the wrapped instance with a filter as if it was normal BleakScanner.""" with patch( @@ -2444,8 +2480,9 @@ async def test_wrapped_instance_with_filter( assert len(detected) == 4 +@pytest.mark.usefixtures("enable_bluetooth") async def test_wrapped_instance_with_service_uuids( - hass: HomeAssistant, mock_bleak_scanner_start: MagicMock, enable_bluetooth: None + hass: HomeAssistant, mock_bleak_scanner_start: MagicMock ) -> None: """Test consumers can use the wrapped instance with a service_uuids list as if it was normal BleakScanner.""" with patch( @@ -2500,8 +2537,9 @@ async def test_wrapped_instance_with_service_uuids( assert len(detected) == 2 +@pytest.mark.usefixtures("enable_bluetooth") async def test_wrapped_instance_with_service_uuids_with_coro_callback( - hass: HomeAssistant, mock_bleak_scanner_start: MagicMock, enable_bluetooth: None + hass: HomeAssistant, mock_bleak_scanner_start: MagicMock ) -> None: """Test consumers can use the wrapped instance with a service_uuids list as if it was normal BleakScanner. @@ -2559,8 +2597,9 @@ async def test_wrapped_instance_with_service_uuids_with_coro_callback( assert len(detected) == 2 +@pytest.mark.usefixtures("enable_bluetooth") async def test_wrapped_instance_with_broken_callbacks( - hass: HomeAssistant, mock_bleak_scanner_start: MagicMock, enable_bluetooth: None + hass: HomeAssistant, mock_bleak_scanner_start: MagicMock ) -> None: """Test broken callbacks do not cause the scanner to fail.""" with ( @@ -2606,8 +2645,9 @@ async def test_wrapped_instance_with_broken_callbacks( assert len(detected) == 1 +@pytest.mark.usefixtures("enable_bluetooth") async def test_wrapped_instance_changes_uuids( - hass: HomeAssistant, mock_bleak_scanner_start: MagicMock, enable_bluetooth: None + hass: HomeAssistant, mock_bleak_scanner_start: MagicMock ) -> None: """Test consumers can use the wrapped instance can change the uuids later.""" with patch( @@ -2661,8 +2701,9 @@ async def test_wrapped_instance_changes_uuids( assert len(detected) == 2 +@pytest.mark.usefixtures("enable_bluetooth") async def test_wrapped_instance_changes_filters( - hass: HomeAssistant, mock_bleak_scanner_start: MagicMock, enable_bluetooth: None + hass: HomeAssistant, mock_bleak_scanner_start: MagicMock ) -> None: """Test consumers can use the wrapped instance can change the filter later.""" with patch( @@ -2717,11 +2758,11 @@ async def test_wrapped_instance_changes_filters( assert len(detected) == 2 +@pytest.mark.usefixtures("enable_bluetooth") async def test_wrapped_instance_unsupported_filter( hass: HomeAssistant, mock_bleak_scanner_start: MagicMock, caplog: pytest.LogCaptureFixture, - enable_bluetooth: None, ) -> None: """Test we want when their filter is ineffective.""" with patch( @@ -2743,8 +2784,9 @@ async def test_wrapped_instance_unsupported_filter( assert "Only UUIDs filters are supported" in caplog.text +@pytest.mark.usefixtures("macos_adapter") async def test_async_ble_device_from_address( - hass: HomeAssistant, mock_bleak_scanner_start: MagicMock, macos_adapter: None + hass: HomeAssistant, mock_bleak_scanner_start: MagicMock ) -> None: """Test the async_ble_device_from_address api.""" set_manager(None) @@ -2800,8 +2842,9 @@ async def test_async_ble_device_from_address( ) +@pytest.mark.usefixtures("macos_adapter") async def test_can_unsetup_bluetooth_single_adapter_macos( - hass: HomeAssistant, mock_bleak_scanner_start: MagicMock, macos_adapter: None + hass: HomeAssistant, mock_bleak_scanner_start: MagicMock ) -> None: """Test we can setup and unsetup bluetooth.""" entry = MockConfigEntry(domain=bluetooth.DOMAIN, data={}, unique_id=DEFAULT_ADDRESS) @@ -2815,10 +2858,10 @@ async def test_can_unsetup_bluetooth_single_adapter_macos( await hass.async_block_till_done() +@pytest.mark.usefixtures("one_adapter") async def test_default_address_config_entries_removed_linux( hass: HomeAssistant, mock_bleak_scanner_start: MagicMock, - one_adapter: None, ) -> None: """Test default address entries are removed on linux.""" entry = MockConfigEntry(domain=bluetooth.DOMAIN, data={}, unique_id=DEFAULT_ADDRESS) @@ -2828,11 +2871,9 @@ async def test_default_address_config_entries_removed_linux( assert not hass.config_entries.async_entries(bluetooth.DOMAIN) +@pytest.mark.usefixtures("enable_bluetooth", "one_adapter") async def test_can_unsetup_bluetooth_single_adapter_linux( - hass: HomeAssistant, - mock_bleak_scanner_start: MagicMock, - enable_bluetooth: None, - one_adapter: None, + hass: HomeAssistant, mock_bleak_scanner_start: MagicMock ) -> None: """Test we can setup and unsetup bluetooth.""" entry = MockConfigEntry( @@ -2848,11 +2889,10 @@ async def test_can_unsetup_bluetooth_single_adapter_linux( await hass.async_block_till_done() +@pytest.mark.usefixtures("enable_bluetooth", "two_adapters") async def test_can_unsetup_bluetooth_multiple_adapters( hass: HomeAssistant, mock_bleak_scanner_start: MagicMock, - enable_bluetooth: None, - two_adapters: None, ) -> None: """Test we can setup and unsetup bluetooth with multiple adapters.""" entry1 = MockConfigEntry( @@ -2874,11 +2914,10 @@ async def test_can_unsetup_bluetooth_multiple_adapters( await hass.async_block_till_done() +@pytest.mark.usefixtures("enable_bluetooth", "two_adapters") async def test_three_adapters_one_missing( hass: HomeAssistant, mock_bleak_scanner_start: MagicMock, - enable_bluetooth: None, - two_adapters: None, ) -> None: """Test three adapters but one is missing results in a retry on setup.""" entry = MockConfigEntry( @@ -2890,9 +2929,8 @@ async def test_three_adapters_one_missing( assert entry.state is ConfigEntryState.SETUP_RETRY -async def test_auto_detect_bluetooth_adapters_linux( - hass: HomeAssistant, one_adapter: None -) -> None: +@pytest.mark.usefixtures("one_adapter") +async def test_auto_detect_bluetooth_adapters_linux(hass: HomeAssistant) -> None: """Test we auto detect bluetooth adapters on linux.""" assert await async_setup_component(hass, bluetooth.DOMAIN, {}) await hass.async_block_till_done() @@ -2900,8 +2938,9 @@ async def test_auto_detect_bluetooth_adapters_linux( assert len(hass.config_entries.flow.async_progress(bluetooth.DOMAIN)) == 1 +@pytest.mark.usefixtures("two_adapters") async def test_auto_detect_bluetooth_adapters_linux_multiple( - hass: HomeAssistant, two_adapters: None + hass: HomeAssistant, ) -> None: """Test we auto detect bluetooth adapters on linux with multiple adapters.""" assert await async_setup_component(hass, bluetooth.DOMAIN, {}) @@ -2959,17 +2998,17 @@ async def test_no_auto_detect_bluetooth_adapters_windows(hass: HomeAssistant) -> assert len(hass.config_entries.flow.async_progress(bluetooth.DOMAIN)) == 0 +@pytest.mark.usefixtures("enable_bluetooth") async def test_getting_the_scanner_returns_the_wrapped_instance( - hass: HomeAssistant, enable_bluetooth: None + hass: HomeAssistant, ) -> None: """Test getting the scanner returns the wrapped instance.""" scanner = bluetooth.async_get_scanner(hass) assert isinstance(scanner, HaBleakScannerWrapper) -async def test_scanner_count_connectable( - hass: HomeAssistant, enable_bluetooth: None -) -> None: +@pytest.mark.usefixtures("enable_bluetooth") +async def test_scanner_count_connectable(hass: HomeAssistant) -> None: """Test getting the connectable scanner count.""" scanner = FakeScanner("any", "any") cancel = bluetooth.async_register_scanner(hass, scanner) @@ -2977,7 +3016,8 @@ async def test_scanner_count_connectable( cancel() -async def test_scanner_count(hass: HomeAssistant, enable_bluetooth: None) -> None: +@pytest.mark.usefixtures("enable_bluetooth") +async def test_scanner_count(hass: HomeAssistant) -> None: """Test getting the connectable and non-connectable scanner count.""" scanner = FakeScanner("any", "any") cancel = bluetooth.async_register_scanner(hass, scanner) @@ -2985,8 +3025,9 @@ async def test_scanner_count(hass: HomeAssistant, enable_bluetooth: None) -> Non cancel() +@pytest.mark.usefixtures("macos_adapter") async def test_migrate_single_entry_macos( - hass: HomeAssistant, mock_bleak_scanner_start: MagicMock, macos_adapter: None + hass: HomeAssistant, mock_bleak_scanner_start: MagicMock ) -> None: """Test we can migrate a single entry on MacOS.""" entry = MockConfigEntry(domain=bluetooth.DOMAIN, data={}) @@ -2996,8 +3037,9 @@ async def test_migrate_single_entry_macos( assert entry.unique_id == DEFAULT_ADDRESS +@pytest.mark.usefixtures("one_adapter") async def test_migrate_single_entry_linux( - hass: HomeAssistant, mock_bleak_scanner_start: MagicMock, one_adapter: None + hass: HomeAssistant, mock_bleak_scanner_start: MagicMock ) -> None: """Test we can migrate a single entry on Linux.""" entry = MockConfigEntry(domain=bluetooth.DOMAIN, data={}) @@ -3007,8 +3049,9 @@ async def test_migrate_single_entry_linux( assert entry.unique_id == "00:00:00:00:00:01" +@pytest.mark.usefixtures("one_adapter") async def test_discover_new_usb_adapters( - hass: HomeAssistant, mock_bleak_scanner_start: MagicMock, one_adapter: None + hass: HomeAssistant, mock_bleak_scanner_start: MagicMock ) -> None: """Test we can discover new usb adapters.""" entry = MockConfigEntry( @@ -3067,8 +3110,9 @@ async def test_discover_new_usb_adapters( assert len(hass.config_entries.flow.async_progress(DOMAIN)) == 1 +@pytest.mark.usefixtures("one_adapter") async def test_discover_new_usb_adapters_with_firmware_fallback_delay( - hass: HomeAssistant, mock_bleak_scanner_start: MagicMock, one_adapter: None + hass: HomeAssistant, mock_bleak_scanner_start: MagicMock ) -> None: """Test we can discover new usb adapters with a firmware fallback delay.""" entry = MockConfigEntry( @@ -3146,10 +3190,10 @@ async def test_discover_new_usb_adapters_with_firmware_fallback_delay( assert len(hass.config_entries.flow.async_progress(DOMAIN)) == 1 +@pytest.mark.usefixtures("no_adapters") async def test_issue_outdated_haos_removed( hass: HomeAssistant, mock_bleak_scanner_start: MagicMock, - no_adapters: None, operating_system_85: None, issue_registry: ir.IssueRegistry, ) -> None: @@ -3163,10 +3207,10 @@ async def test_issue_outdated_haos_removed( assert issue is None +@pytest.mark.usefixtures("one_adapter") async def test_haos_9_or_later( hass: HomeAssistant, mock_bleak_scanner_start: MagicMock, - one_adapter: None, operating_system_90: None, issue_registry: ir.IssueRegistry, ) -> None: @@ -3183,8 +3227,9 @@ async def test_haos_9_or_later( assert issue is None +@pytest.mark.usefixtures("one_adapter") async def test_title_updated_if_mac_address( - hass: HomeAssistant, mock_bleak_scanner_start: MagicMock, one_adapter: None + hass: HomeAssistant, mock_bleak_scanner_start: MagicMock ) -> None: """Test the title is updated if it is the mac address.""" entry = MockConfigEntry( diff --git a/tests/components/bluetooth/test_manager.py b/tests/components/bluetooth/test_manager.py index cb2be8a0e8d..5a3b9392ba9 100644 --- a/tests/components/bluetooth/test_manager.py +++ b/tests/components/bluetooth/test_manager.py @@ -71,9 +71,9 @@ def register_hci1_scanner(hass: HomeAssistant) -> Generator[None, None, None]: cancel() +@pytest.mark.usefixtures("enable_bluetooth") async def test_advertisements_do_not_switch_adapters_for_no_reason( hass: HomeAssistant, - enable_bluetooth: None, register_hci0_scanner: None, register_hci1_scanner: None, ) -> None: @@ -128,9 +128,9 @@ async def test_advertisements_do_not_switch_adapters_for_no_reason( ) +@pytest.mark.usefixtures("enable_bluetooth") async def test_switching_adapters_based_on_rssi( hass: HomeAssistant, - enable_bluetooth: None, register_hci0_scanner: None, register_hci1_scanner: None, ) -> None: @@ -189,9 +189,9 @@ async def test_switching_adapters_based_on_rssi( ) +@pytest.mark.usefixtures("enable_bluetooth") async def test_switching_adapters_based_on_zero_rssi( hass: HomeAssistant, - enable_bluetooth: None, register_hci0_scanner: None, register_hci1_scanner: None, ) -> None: @@ -250,9 +250,9 @@ async def test_switching_adapters_based_on_zero_rssi( ) +@pytest.mark.usefixtures("enable_bluetooth") async def test_switching_adapters_based_on_stale( hass: HomeAssistant, - enable_bluetooth: None, register_hci0_scanner: None, register_hci1_scanner: None, ) -> None: @@ -317,9 +317,9 @@ async def test_switching_adapters_based_on_stale( ) +@pytest.mark.usefixtures("enable_bluetooth") async def test_switching_adapters_based_on_stale_with_discovered_interval( hass: HomeAssistant, - enable_bluetooth: None, register_hci0_scanner: None, register_hci1_scanner: None, ) -> None: @@ -400,8 +400,9 @@ async def test_switching_adapters_based_on_stale_with_discovered_interval( ) +@pytest.mark.usefixtures("one_adapter") async def test_restore_history_from_dbus( - hass: HomeAssistant, one_adapter: None, disable_new_discovery_flows + hass: HomeAssistant, disable_new_discovery_flows ) -> None: """Test we can restore history from dbus.""" address = "AA:BB:CC:CC:CC:FF" @@ -423,9 +424,9 @@ async def test_restore_history_from_dbus( assert bluetooth.async_ble_device_from_address(hass, address) is ble_device +@pytest.mark.usefixtures("one_adapter") async def test_restore_history_from_dbus_and_remote_adapters( hass: HomeAssistant, - one_adapter: None, hass_storage: dict[str, Any], disable_new_discovery_flows, ) -> None: @@ -463,9 +464,9 @@ async def test_restore_history_from_dbus_and_remote_adapters( assert disable_new_discovery_flows.call_count > 1 +@pytest.mark.usefixtures("one_adapter") async def test_restore_history_from_dbus_and_corrupted_remote_adapters( hass: HomeAssistant, - one_adapter: None, hass_storage: dict[str, Any], disable_new_discovery_flows, ) -> None: @@ -501,9 +502,9 @@ async def test_restore_history_from_dbus_and_corrupted_remote_adapters( assert disable_new_discovery_flows.call_count >= 1 +@pytest.mark.usefixtures("enable_bluetooth") async def test_switching_adapters_based_on_rssi_connectable_to_non_connectable( hass: HomeAssistant, - enable_bluetooth: None, register_hci0_scanner: None, register_hci1_scanner: None, ) -> None: @@ -589,9 +590,9 @@ async def test_switching_adapters_based_on_rssi_connectable_to_non_connectable( ) +@pytest.mark.usefixtures("enable_bluetooth") async def test_connectable_advertisement_can_be_retrieved_with_best_path_is_non_connectable( hass: HomeAssistant, - enable_bluetooth: None, register_hci0_scanner: None, register_hci1_scanner: None, ) -> None: @@ -640,8 +641,9 @@ async def test_connectable_advertisement_can_be_retrieved_with_best_path_is_non_ ) +@pytest.mark.usefixtures("enable_bluetooth") async def test_switching_adapters_when_one_goes_away( - hass: HomeAssistant, enable_bluetooth: None, register_hci0_scanner: None + hass: HomeAssistant, register_hci0_scanner: None ) -> None: """Test switching adapters when one goes away.""" cancel_hci2 = bluetooth.async_register_scanner(hass, FakeScanner("hci2", "hci2")) @@ -689,8 +691,9 @@ async def test_switching_adapters_when_one_goes_away( ) +@pytest.mark.usefixtures("enable_bluetooth") async def test_switching_adapters_when_one_stop_scanning( - hass: HomeAssistant, enable_bluetooth: None, register_hci0_scanner: None + hass: HomeAssistant, register_hci0_scanner: None ) -> None: """Test switching adapters when stops scanning.""" hci2_scanner = FakeScanner("hci2", "hci2") @@ -1076,9 +1079,9 @@ async def test_goes_unavailable_dismisses_discovery_and_makes_discoverable( cancel_connectable_scanner() +@pytest.mark.usefixtures("enable_bluetooth") async def test_debug_logging( hass: HomeAssistant, - enable_bluetooth: None, register_hci0_scanner: None, register_hci1_scanner: None, caplog: pytest.LogCaptureFixture, @@ -1135,12 +1138,8 @@ async def test_debug_logging( assert "wohand_good_signal_hci0" not in caplog.text -async def test_set_fallback_interval_small( - hass: HomeAssistant, - caplog: pytest.LogCaptureFixture, - enable_bluetooth: None, - macos_adapter: None, -) -> None: +@pytest.mark.usefixtures("enable_bluetooth", "macos_adapter") +async def test_set_fallback_interval_small(hass: HomeAssistant) -> None: """Test we can set the fallback advertisement interval.""" assert async_get_fallback_availability_interval(hass, "44:44:33:11:23:12") is None @@ -1193,12 +1192,8 @@ async def test_set_fallback_interval_small( assert async_get_fallback_availability_interval(hass, "44:44:33:11:23:12") is None -async def test_set_fallback_interval_big( - hass: HomeAssistant, - caplog: pytest.LogCaptureFixture, - enable_bluetooth: None, - macos_adapter: None, -) -> None: +@pytest.mark.usefixtures("enable_bluetooth", "macos_adapter") +async def test_set_fallback_interval_big(hass: HomeAssistant) -> None: """Test we can set the fallback advertisement interval.""" assert async_get_fallback_availability_interval(hass, "44:44:33:11:23:12") is None diff --git a/tests/components/bluetooth/test_models.py b/tests/components/bluetooth/test_models.py index 087d443c5a0..820fa734f73 100644 --- a/tests/components/bluetooth/test_models.py +++ b/tests/components/bluetooth/test_models.py @@ -29,9 +29,8 @@ from . import ( ) -async def test_wrapped_bleak_scanner( - hass: HomeAssistant, enable_bluetooth: None -) -> None: +@pytest.mark.usefixtures("enable_bluetooth") +async def test_wrapped_bleak_scanner(hass: HomeAssistant) -> None: """Test wrapped bleak scanner dispatches calls as expected.""" scanner = HaBleakScannerWrapper() switchbot_device = generate_ble_device("44:44:33:11:23:45", "wohand") @@ -43,9 +42,8 @@ async def test_wrapped_bleak_scanner( assert await scanner.discover() == [switchbot_device] -async def test_wrapped_bleak_client_raises_device_missing( - hass: HomeAssistant, enable_bluetooth: None -) -> None: +@pytest.mark.usefixtures("enable_bluetooth") +async def test_wrapped_bleak_client_raises_device_missing(hass: HomeAssistant) -> None: """Test wrapped bleak client dispatches calls as expected.""" switchbot_device = generate_ble_device("44:44:33:11:23:45", "wohand") client = HaBleakClientWrapper(switchbot_device) @@ -57,8 +55,9 @@ async def test_wrapped_bleak_client_raises_device_missing( assert await client.clear_cache() is False +@pytest.mark.usefixtures("enable_bluetooth") async def test_wrapped_bleak_client_set_disconnected_callback_before_connected( - hass: HomeAssistant, enable_bluetooth: None + hass: HomeAssistant, ) -> None: """Test wrapped bleak client can set a disconnected callback before connected.""" switchbot_device = generate_ble_device("44:44:33:11:23:45", "wohand") @@ -66,9 +65,8 @@ async def test_wrapped_bleak_client_set_disconnected_callback_before_connected( client.set_disconnected_callback(lambda client: None) -async def test_wrapped_bleak_client_local_adapter_only( - hass: HomeAssistant, enable_bluetooth: None, one_adapter: None -) -> None: +@pytest.mark.usefixtures("enable_bluetooth", "one_adapter") +async def test_wrapped_bleak_client_local_adapter_only(hass: HomeAssistant) -> None: """Test wrapped bleak client with only a local adapter.""" manager = _get_manager() @@ -132,8 +130,9 @@ async def test_wrapped_bleak_client_local_adapter_only( cancel() +@pytest.mark.usefixtures("enable_bluetooth", "one_adapter") async def test_wrapped_bleak_client_set_disconnected_callback_after_connected( - hass: HomeAssistant, enable_bluetooth: None, one_adapter: None + hass: HomeAssistant, ) -> None: """Test wrapped bleak client can set a disconnected callback after connected.""" manager = _get_manager() @@ -222,8 +221,9 @@ async def test_wrapped_bleak_client_set_disconnected_callback_after_connected( cancel() +@pytest.mark.usefixtures("enable_bluetooth", "one_adapter") async def test_ble_device_with_proxy_client_out_of_connections_no_scanners( - hass: HomeAssistant, enable_bluetooth: None, one_adapter: None + hass: HomeAssistant, ) -> None: """Test we switch to the next available proxy when one runs out of connections with no scanners.""" manager = _get_manager() @@ -260,8 +260,9 @@ async def test_ble_device_with_proxy_client_out_of_connections_no_scanners( await client.disconnect() +@pytest.mark.usefixtures("enable_bluetooth", "one_adapter") async def test_ble_device_with_proxy_client_out_of_connections( - hass: HomeAssistant, enable_bluetooth: None, one_adapter: None + hass: HomeAssistant, ) -> None: """Test handling all scanners are out of connection slots.""" manager = _get_manager() @@ -326,9 +327,8 @@ async def test_ble_device_with_proxy_client_out_of_connections( cancel() -async def test_ble_device_with_proxy_clear_cache( - hass: HomeAssistant, enable_bluetooth: None, one_adapter: None -) -> None: +@pytest.mark.usefixtures("enable_bluetooth", "one_adapter") +async def test_ble_device_with_proxy_clear_cache(hass: HomeAssistant) -> None: """Test we can clear cache on the proxy.""" manager = _get_manager() @@ -388,8 +388,9 @@ async def test_ble_device_with_proxy_clear_cache( cancel() +@pytest.mark.usefixtures("enable_bluetooth", "one_adapter") async def test_ble_device_with_proxy_client_out_of_connections_uses_best_available( - hass: HomeAssistant, enable_bluetooth: None, one_adapter: None + hass: HomeAssistant, ) -> None: """Test we switch to the next available proxy when one runs out of connections.""" manager = _get_manager() @@ -495,8 +496,9 @@ async def test_ble_device_with_proxy_client_out_of_connections_uses_best_availab cancel() +@pytest.mark.usefixtures("enable_bluetooth", "macos_adapter") async def test_ble_device_with_proxy_client_out_of_connections_uses_best_available_macos( - hass: HomeAssistant, enable_bluetooth: None, macos_adapter: None + hass: HomeAssistant, ) -> None: """Test we switch to the next available proxy when one runs out of connections on MacOS.""" manager = _get_manager() diff --git a/tests/components/bluetooth/test_scanner.py b/tests/components/bluetooth/test_scanner.py index 5658aea523b..dc25f29111c 100644 --- a/tests/components/bluetooth/test_scanner.py +++ b/tests/components/bluetooth/test_scanner.py @@ -39,11 +39,9 @@ NEED_RESET_ERRORS = [ ] +@pytest.mark.usefixtures("enable_bluetooth", "macos_adapter") async def test_config_entry_can_be_reloaded_when_stop_raises( - hass: HomeAssistant, - caplog: pytest.LogCaptureFixture, - enable_bluetooth: None, - macos_adapter: None, + hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: """Test we can reload if stopping the scanner raises.""" entry = hass.config_entries.async_entries(bluetooth.DOMAIN)[0] @@ -60,8 +58,9 @@ async def test_config_entry_can_be_reloaded_when_stop_raises( assert "Error stopping scanner" in caplog.text +@pytest.mark.usefixtures("one_adapter") async def test_dbus_socket_missing_in_container( - hass: HomeAssistant, caplog: pytest.LogCaptureFixture, one_adapter: None + hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: """Test we handle dbus being missing in the container.""" @@ -83,8 +82,9 @@ async def test_dbus_socket_missing_in_container( assert "docker" in caplog.text +@pytest.mark.usefixtures("one_adapter") async def test_dbus_socket_missing( - hass: HomeAssistant, caplog: pytest.LogCaptureFixture, one_adapter: None + hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: """Test we handle dbus being missing.""" @@ -106,8 +106,9 @@ async def test_dbus_socket_missing( assert "docker" not in caplog.text +@pytest.mark.usefixtures("one_adapter") async def test_dbus_broken_pipe_in_container( - hass: HomeAssistant, caplog: pytest.LogCaptureFixture, one_adapter: None + hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: """Test we handle dbus broken pipe in the container.""" @@ -130,8 +131,9 @@ async def test_dbus_broken_pipe_in_container( assert "container" in caplog.text +@pytest.mark.usefixtures("one_adapter") async def test_dbus_broken_pipe( - hass: HomeAssistant, caplog: pytest.LogCaptureFixture, one_adapter: None + hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: """Test we handle dbus broken pipe.""" @@ -154,8 +156,9 @@ async def test_dbus_broken_pipe( assert "container" not in caplog.text +@pytest.mark.usefixtures("one_adapter") async def test_invalid_dbus_message( - hass: HomeAssistant, caplog: pytest.LogCaptureFixture, one_adapter: None + hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: """Test we handle invalid dbus message.""" @@ -174,9 +177,8 @@ async def test_invalid_dbus_message( @pytest.mark.parametrize("error", NEED_RESET_ERRORS) -async def test_adapter_needs_reset_at_start( - hass: HomeAssistant, caplog: pytest.LogCaptureFixture, one_adapter: None, error: str -) -> None: +@pytest.mark.usefixtures("one_adapter") +async def test_adapter_needs_reset_at_start(hass: HomeAssistant, error: str) -> None: """Test we cycle the adapter when it needs a restart.""" with ( @@ -199,9 +201,8 @@ async def test_adapter_needs_reset_at_start( await hass.async_block_till_done() -async def test_recovery_from_dbus_restart( - hass: HomeAssistant, one_adapter: None -) -> None: +@pytest.mark.usefixtures("one_adapter") +async def test_recovery_from_dbus_restart(hass: HomeAssistant) -> None: """Test we can recover when DBus gets restarted out from under us.""" called_start = 0 @@ -281,7 +282,8 @@ async def test_recovery_from_dbus_restart( assert called_start == 2 -async def test_adapter_recovery(hass: HomeAssistant, one_adapter: None) -> None: +@pytest.mark.usefixtures("one_adapter") +async def test_adapter_recovery(hass: HomeAssistant) -> None: """Test we can recover when the adapter stops responding.""" called_start = 0 @@ -365,9 +367,8 @@ async def test_adapter_recovery(hass: HomeAssistant, one_adapter: None) -> None: assert called_start == 2 -async def test_adapter_scanner_fails_to_start_first_time( - hass: HomeAssistant, one_adapter: None -) -> None: +@pytest.mark.usefixtures("one_adapter") +async def test_adapter_scanner_fails_to_start_first_time(hass: HomeAssistant) -> None: """Test we can recover when the adapter stops responding and the first recovery fails.""" called_start = 0 @@ -474,8 +475,9 @@ async def test_adapter_scanner_fails_to_start_first_time( assert called_start == 5 +@pytest.mark.usefixtures("one_adapter") async def test_adapter_fails_to_start_and_takes_a_bit_to_init( - hass: HomeAssistant, one_adapter: None, caplog: pytest.LogCaptureFixture + hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: """Test we can recover the adapter at startup and we wait for Dbus to init.""" assert await async_setup_component(hass, "logger", {}) @@ -545,8 +547,9 @@ async def test_adapter_fails_to_start_and_takes_a_bit_to_init( assert "Waiting for adapter to initialize" in caplog.text +@pytest.mark.usefixtures("one_adapter") async def test_restart_takes_longer_than_watchdog_time( - hass: HomeAssistant, one_adapter: None, caplog: pytest.LogCaptureFixture + hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: """Test we do not try to recover the adapter again if the restart is still in progress.""" @@ -614,8 +617,9 @@ async def test_restart_takes_longer_than_watchdog_time( @pytest.mark.skipif("platform.system() != 'Darwin'") +@pytest.mark.usefixtures("macos_adapter") async def test_setup_and_stop_macos( - hass: HomeAssistant, mock_bleak_scanner_start: MagicMock, macos_adapter: None + hass: HomeAssistant, mock_bleak_scanner_start: MagicMock ) -> None: """Test we enable use_bdaddr on MacOS.""" entry = MockConfigEntry( diff --git a/tests/components/bluetooth/test_usage.py b/tests/components/bluetooth/test_usage.py index 35aa0eb9022..d5d4e7ad9d0 100644 --- a/tests/components/bluetooth/test_usage.py +++ b/tests/components/bluetooth/test_usage.py @@ -8,6 +8,7 @@ from habluetooth.usage import ( uninstall_multiple_bleak_catcher, ) from habluetooth.wrappers import HaBleakClientWrapper, HaBleakScannerWrapper +import pytest from homeassistant.core import HomeAssistant @@ -38,9 +39,8 @@ async def test_multiple_bleak_scanner_instances(hass: HomeAssistant) -> None: assert not isinstance(instance, HaBleakScannerWrapper) -async def test_wrapping_bleak_client( - hass: HomeAssistant, enable_bluetooth: None -) -> None: +@pytest.mark.usefixtures("enable_bluetooth") +async def test_wrapping_bleak_client(hass: HomeAssistant) -> None: """Test we wrap BleakClient.""" install_multiple_bleak_catcher() diff --git a/tests/components/bluetooth/test_wrappers.py b/tests/components/bluetooth/test_wrappers.py index 2acc2b0ddfc..9c537079db7 100644 --- a/tests/components/bluetooth/test_wrappers.py +++ b/tests/components/bluetooth/test_wrappers.py @@ -194,10 +194,9 @@ def _generate_scanners_with_fake_devices(hass): return hci0_device_advs, cancel_hci0, cancel_hci1 +@pytest.mark.usefixtures("enable_bluetooth", "two_adapters") async def test_test_switch_adapters_when_out_of_slots( hass: HomeAssistant, - two_adapters: None, - enable_bluetooth: None, install_bleak_catcher, mock_platform_client, ) -> None: @@ -254,10 +253,9 @@ async def test_test_switch_adapters_when_out_of_slots( cancel_hci1() +@pytest.mark.usefixtures("enable_bluetooth", "two_adapters") async def test_release_slot_on_connect_failure( hass: HomeAssistant, - two_adapters: None, - enable_bluetooth: None, install_bleak_catcher, mock_platform_client_that_fails_to_connect, ) -> None: @@ -283,10 +281,9 @@ async def test_release_slot_on_connect_failure( cancel_hci1() +@pytest.mark.usefixtures("enable_bluetooth", "two_adapters") async def test_release_slot_on_connect_exception( hass: HomeAssistant, - two_adapters: None, - enable_bluetooth: None, install_bleak_catcher, mock_platform_client_that_raises_on_connect, ) -> None: @@ -314,10 +311,9 @@ async def test_release_slot_on_connect_exception( cancel_hci1() +@pytest.mark.usefixtures("enable_bluetooth", "two_adapters") async def test_we_switch_adapters_on_failure( hass: HomeAssistant, - two_adapters: None, - enable_bluetooth: None, install_bleak_catcher, ) -> None: """Ensure we try the next best adapter after a failure.""" @@ -374,10 +370,9 @@ async def test_we_switch_adapters_on_failure( cancel_hci1() +@pytest.mark.usefixtures("enable_bluetooth", "two_adapters") async def test_passing_subclassed_str_as_address( hass: HomeAssistant, - two_adapters: None, - enable_bluetooth: None, install_bleak_catcher, ) -> None: """Ensure the client wrapper can handle a subclassed str as the address.""" @@ -406,10 +401,9 @@ async def test_passing_subclassed_str_as_address( cancel_hci1() +@pytest.mark.usefixtures("enable_bluetooth", "two_adapters") async def test_raise_after_shutdown( hass: HomeAssistant, - two_adapters: None, - enable_bluetooth: None, install_bleak_catcher, mock_platform_client_that_raises_on_connect, ) -> None: From b09f3eb3139b737de6b81c557412b1305bb707fd Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 4 Jun 2024 16:26:39 +0200 Subject: [PATCH 0213/1445] Fix incorrect current_request_with_host type hint (#118809) --- tests/components/application_credentials/test_init.py | 4 ++-- .../homeassistant_hardware/test_silabs_multiprotocol_addon.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/components/application_credentials/test_init.py b/tests/components/application_credentials/test_init.py index b8f5840c4f2..d22b736b39b 100644 --- a/tests/components/application_credentials/test_init.py +++ b/tests/components/application_credentials/test_init.py @@ -113,8 +113,8 @@ class FakeConfigFlow(config_entry_oauth2_flow.AbstractOAuth2FlowHandler): @pytest.fixture(autouse=True) def config_flow_handler( - hass: HomeAssistant, current_request_with_host: Any -) -> Generator[FakeConfigFlow, None, None]: + hass: HomeAssistant, current_request_with_host: None +) -> Generator[None, None, None]: """Fixture for a test config flow.""" mock_platform(hass, f"{TEST_DOMAIN}.config_flow") with mock_config_flow(TEST_DOMAIN, FakeConfigFlow): diff --git a/tests/components/homeassistant_hardware/test_silabs_multiprotocol_addon.py b/tests/components/homeassistant_hardware/test_silabs_multiprotocol_addon.py index d04f725baf6..333e38da53b 100644 --- a/tests/components/homeassistant_hardware/test_silabs_multiprotocol_addon.py +++ b/tests/components/homeassistant_hardware/test_silabs_multiprotocol_addon.py @@ -95,8 +95,8 @@ class FakeOptionsFlow(silabs_multiprotocol_addon.OptionsFlowHandler): @pytest.fixture(autouse=True) def config_flow_handler( - hass: HomeAssistant, current_request_with_host: Any -) -> Generator[FakeConfigFlow, None, None]: + hass: HomeAssistant, current_request_with_host: None +) -> Generator[None, None, None]: """Fixture for a test config flow.""" mock_platform(hass, f"{TEST_DOMAIN}.config_flow") with mock_config_flow(TEST_DOMAIN, FakeConfigFlow): From 861043694857e02d32012259853eb6155125b557 Mon Sep 17 00:00:00 2001 From: ollo69 <60491700+ollo69@users.noreply.github.com> Date: Tue, 4 Jun 2024 17:02:30 +0200 Subject: [PATCH 0214/1445] Add remote entity to AndroidTV (#103496) * Add remote entity to AndroidTV * Add tests for remote entity * Requested changes on tests --- .../components/androidtv/__init__.py | 2 +- homeassistant/components/androidtv/entity.py | 4 + homeassistant/components/androidtv/remote.py | 75 ++++++++ .../components/androidtv/strings.json | 5 + tests/components/androidtv/test_remote.py | 164 ++++++++++++++++++ 5 files changed, 249 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/androidtv/remote.py create mode 100644 tests/components/androidtv/test_remote.py diff --git a/homeassistant/components/androidtv/__init__.py b/homeassistant/components/androidtv/__init__.py index dc7fd95519f..34b324db169 100644 --- a/homeassistant/components/androidtv/__init__.py +++ b/homeassistant/components/androidtv/__init__.py @@ -61,7 +61,7 @@ ADB_PYTHON_EXCEPTIONS: tuple = ( ) ADB_TCP_EXCEPTIONS: tuple = (ConnectionResetError, RuntimeError) -PLATFORMS = [Platform.MEDIA_PLAYER] +PLATFORMS = [Platform.MEDIA_PLAYER, Platform.REMOTE] RELOAD_OPTIONS = [CONF_STATE_DETECTION_RULES] _INVALID_MACS = {"ff:ff:ff:ff:ff:ff"} diff --git a/homeassistant/components/androidtv/entity.py b/homeassistant/components/androidtv/entity.py index 45cb241944c..470a4950ebc 100644 --- a/homeassistant/components/androidtv/entity.py +++ b/homeassistant/components/androidtv/entity.py @@ -18,6 +18,7 @@ from homeassistant.const import ( CONF_HOST, CONF_NAME, ) +from homeassistant.exceptions import ServiceValidationError from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo from homeassistant.helpers.entity import Entity @@ -87,6 +88,9 @@ def adb_decorator[_ADBDeviceT: AndroidTVEntity, **_P, _R]( await self.aftv.adb_close() self._attr_available = False return None + except ServiceValidationError: + # Service validation error is thrown because raised by remote services + raise except Exception as err: # noqa: BLE001 # An unforeseen exception occurred. Close the ADB connection so that # it doesn't happen over and over again. diff --git a/homeassistant/components/androidtv/remote.py b/homeassistant/components/androidtv/remote.py new file mode 100644 index 00000000000..db48b0cf1b6 --- /dev/null +++ b/homeassistant/components/androidtv/remote.py @@ -0,0 +1,75 @@ +"""Support for the AndroidTV remote.""" + +from __future__ import annotations + +from collections.abc import Iterable +import logging +from typing import Any + +from androidtv.constants import KEYS + +from homeassistant.components.remote import ATTR_NUM_REPEATS, RemoteEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ServiceValidationError +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import CONF_TURN_OFF_COMMAND, CONF_TURN_ON_COMMAND, DOMAIN +from .entity import AndroidTVEntity, adb_decorator + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up the AndroidTV remote from a config entry.""" + async_add_entities([AndroidTVRemote(entry)]) + + +class AndroidTVRemote(AndroidTVEntity, RemoteEntity): + """Device that sends commands to a AndroidTV.""" + + _attr_name = None + _attr_should_poll = False + + @adb_decorator() + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn on the device.""" + options = self._entry_runtime_data.dev_opt + if turn_on_cmd := options.get(CONF_TURN_ON_COMMAND): + await self.aftv.adb_shell(turn_on_cmd) + else: + await self.aftv.turn_on() + + @adb_decorator() + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn off the device.""" + options = self._entry_runtime_data.dev_opt + if turn_off_cmd := options.get(CONF_TURN_OFF_COMMAND): + await self.aftv.adb_shell(turn_off_cmd) + else: + await self.aftv.turn_off() + + @adb_decorator() + async def async_send_command(self, command: Iterable[str], **kwargs: Any) -> None: + """Send a command to a device.""" + + num_repeats = kwargs[ATTR_NUM_REPEATS] + command_list = [] + for cmd in command: + if key := KEYS.get(cmd): + command_list.append(f"input keyevent {key}") + else: + command_list.append(cmd) + + for _ in range(num_repeats): + for cmd in command_list: + try: + await self.aftv.adb_shell(cmd) + except UnicodeDecodeError as ex: + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="failed_send", + translation_placeholders={"cmd": cmd}, + ) from ex diff --git a/homeassistant/components/androidtv/strings.json b/homeassistant/components/androidtv/strings.json index 7949c066916..d6fdf78d1fb 100644 --- a/homeassistant/components/androidtv/strings.json +++ b/homeassistant/components/androidtv/strings.json @@ -103,5 +103,10 @@ "name": "Learn sendevent", "description": "Translates a key press on a remote into ADB 'sendevent' commands. You must press one button on the remote within 8 seconds of calling this service." } + }, + "exceptions": { + "failed_send": { + "message": "Failed to send command {cmd}" + } } } diff --git a/tests/components/androidtv/test_remote.py b/tests/components/androidtv/test_remote.py new file mode 100644 index 00000000000..d18e08d4df8 --- /dev/null +++ b/tests/components/androidtv/test_remote.py @@ -0,0 +1,164 @@ +"""The tests for the androidtv remote platform.""" + +from typing import Any +from unittest.mock import call, patch + +from androidtv.constants import KEYS +import pytest + +from homeassistant.components.androidtv.const import ( + CONF_TURN_OFF_COMMAND, + CONF_TURN_ON_COMMAND, +) +from homeassistant.components.remote import ( + ATTR_NUM_REPEATS, + DOMAIN as REMOTE_DOMAIN, + SERVICE_SEND_COMMAND, +) +from homeassistant.const import ( + ATTR_COMMAND, + ATTR_ENTITY_ID, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, +) +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ServiceValidationError + +from . import patchers +from .common import ( + CONFIG_ANDROID_DEFAULT, + CONFIG_FIRETV_DEFAULT, + SHELL_RESPONSE_OFF, + SHELL_RESPONSE_STANDBY, + setup_mock_entry, +) + +from tests.common import MockConfigEntry + + +def _setup(config: dict[str, Any]) -> tuple[str, str, MockConfigEntry]: + """Prepare mock entry for the media player tests.""" + return setup_mock_entry(config, REMOTE_DOMAIN) + + +async def _test_service( + hass: HomeAssistant, + entity_id, + ha_service_name, + androidtv_method, + additional_service_data=None, + expected_call_args=None, +) -> None: + """Test generic Android media player entity service.""" + if expected_call_args is None: + expected_call_args = [None] + + service_data = {ATTR_ENTITY_ID: entity_id} + if additional_service_data: + service_data.update(additional_service_data) + + androidtv_patch = ( + "androidtv.androidtv_async.AndroidTVAsync" + if "android" in entity_id + else "firetv.firetv_async.FireTVAsync" + ) + with patch(f"androidtv.{androidtv_patch}.{androidtv_method}") as api_call: + await hass.services.async_call( + REMOTE_DOMAIN, + ha_service_name, + service_data=service_data, + blocking=True, + ) + assert api_call.called + assert api_call.call_count == len(expected_call_args) + expected_calls = [call(s) if s else call() for s in expected_call_args] + assert api_call.call_args_list == expected_calls + + +@pytest.mark.parametrize("config", [CONFIG_ANDROID_DEFAULT, CONFIG_FIRETV_DEFAULT]) +async def test_services_remote(hass: HomeAssistant, config) -> None: + """Test services for remote entity.""" + patch_key, entity_id, config_entry = _setup(config) + config_entry.add_to_hass(hass) + + with patchers.patch_connect(True)[patch_key]: + with patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]: + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + with ( + patchers.patch_shell(SHELL_RESPONSE_STANDBY)[patch_key], + patchers.PATCH_SCREENCAP, + ): + await _test_service(hass, entity_id, SERVICE_TURN_OFF, "turn_off") + await _test_service(hass, entity_id, SERVICE_TURN_ON, "turn_on") + await _test_service( + hass, + entity_id, + SERVICE_SEND_COMMAND, + "adb_shell", + {ATTR_COMMAND: ["BACK", "test"], ATTR_NUM_REPEATS: 2}, + [ + f"input keyevent {KEYS["BACK"]}", + "test", + f"input keyevent {KEYS["BACK"]}", + "test", + ], + ) + + +@pytest.mark.parametrize("config", [CONFIG_ANDROID_DEFAULT, CONFIG_FIRETV_DEFAULT]) +async def test_services_remote_custom(hass: HomeAssistant, config) -> None: + """Test services with custom options for remote entity.""" + patch_key, entity_id, config_entry = _setup(config) + config_entry.add_to_hass(hass) + hass.config_entries.async_update_entry( + config_entry, + options={ + CONF_TURN_OFF_COMMAND: "test off", + CONF_TURN_ON_COMMAND: "test on", + }, + ) + + with patchers.patch_connect(True)[patch_key]: + with patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]: + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + with ( + patchers.patch_shell(SHELL_RESPONSE_STANDBY)[patch_key], + patchers.PATCH_SCREENCAP, + ): + await _test_service( + hass, entity_id, SERVICE_TURN_OFF, "adb_shell", None, ["test off"] + ) + await _test_service( + hass, entity_id, SERVICE_TURN_ON, "adb_shell", None, ["test on"] + ) + + +async def test_remote_unicode_decode_error(hass: HomeAssistant) -> None: + """Test sending a command via the send_command remote service that raises a UnicodeDecodeError exception.""" + patch_key, entity_id, config_entry = _setup(CONFIG_ANDROID_DEFAULT) + config_entry.add_to_hass(hass) + response = b"test response" + + with patchers.patch_connect(True)[patch_key]: + with patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]: + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + with patch( + "androidtv.basetv.basetv_async.BaseTVAsync.adb_shell", + side_effect=UnicodeDecodeError("utf-8", response, 0, len(response), "TEST"), + ) as api_call: + try: + await hass.services.async_call( + REMOTE_DOMAIN, + SERVICE_SEND_COMMAND, + service_data={ATTR_ENTITY_ID: entity_id, ATTR_COMMAND: "BACK"}, + blocking=True, + ) + pytest.fail("Exception not raised") + except ServiceValidationError: + assert api_call.call_count == 1 From 52ad90a68d432a30cfac08c37b886576b06bb884 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 4 Jun 2024 11:18:07 -0400 Subject: [PATCH 0215/1445] Include script description in LLM exposed entities (#118749) * Include script description in LLM exposed entities * Fix race in test * Fix type * Expose script * Remove fields --- homeassistant/helpers/llm.py | 16 ++++++++++++++++ homeassistant/helpers/service.py | 8 ++++++++ tests/helpers/test_llm.py | 26 ++++++++++++++++++++++++++ 3 files changed, 50 insertions(+) diff --git a/homeassistant/helpers/llm.py b/homeassistant/helpers/llm.py index 31e3c791630..3c240692d52 100644 --- a/homeassistant/helpers/llm.py +++ b/homeassistant/helpers/llm.py @@ -29,6 +29,7 @@ from . import ( entity_registry as er, floor_registry as fr, intent, + service, ) from .singleton import singleton @@ -407,6 +408,7 @@ def _get_exposed_entities( entity_entry = entity_registry.async_get(state.entity_id) names = [state.name] area_names = [] + description: str | None = None if entity_entry is not None: names.extend(entity_entry.aliases) @@ -426,11 +428,25 @@ def _get_exposed_entities( area_names.append(area.name) area_names.extend(area.aliases) + if ( + state.domain == "script" + and entity_entry.unique_id + and ( + service_desc := service.async_get_cached_service_description( + hass, "script", entity_entry.unique_id + ) + ) + ): + description = service_desc.get("description") + info: dict[str, Any] = { "names": ", ".join(names), "state": state.state, } + if description: + info["description"] = description + if area_names: info["areas"] = ", ".join(area_names) diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index d20cba8909f..3a828ada9c2 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -655,6 +655,14 @@ def _load_services_files( return [_load_services_file(hass, integration) for integration in integrations] +@callback +def async_get_cached_service_description( + hass: HomeAssistant, domain: str, service: str +) -> dict[str, Any] | None: + """Return the cached description for a service.""" + return hass.data.get(SERVICE_DESCRIPTION_CACHE, {}).get((domain, service)) + + @bind_hass async def async_get_all_descriptions( hass: HomeAssistant, diff --git a/tests/helpers/test_llm.py b/tests/helpers/test_llm.py index 6c9451bc843..3f61ed8a0ed 100644 --- a/tests/helpers/test_llm.py +++ b/tests/helpers/test_llm.py @@ -5,6 +5,7 @@ from unittest.mock import patch import pytest import voluptuous as vol +from homeassistant.components.homeassistant.exposed_entities import async_expose_entity from homeassistant.components.intent import async_register_timer_handler from homeassistant.core import Context, HomeAssistant, State from homeassistant.exceptions import HomeAssistantError @@ -293,6 +294,26 @@ async def test_assist_api_prompt( ) # Expose entities + + # Create a script with a unique ID + assert await async_setup_component( + hass, + "script", + { + "script": { + "test_script": { + "description": "This is a test script", + "sequence": [], + "fields": { + "beer": {"description": "Number of beers"}, + "wine": {}, + }, + } + } + }, + ) + async_expose_entity(hass, "conversation", "script.test_script", True) + entry = MockConfigEntry(title=None) entry.add_to_hass(hass) device = device_registry.async_get_or_create( @@ -471,6 +492,11 @@ async def test_assist_api_prompt( "names": "Unnamed Device", "state": "unavailable", }, + "script.test_script": { + "description": "This is a test script", + "names": "test_script", + "state": "off", + }, } exposed_entities_prompt = ( "An overview of the areas and the devices in this smart home:\n" From b81f0b600f64590615f08b5cf667d10c63723635 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 4 Jun 2024 17:50:22 +0200 Subject: [PATCH 0216/1445] Move current_request_with_host fixture to decorator (#118810) * Move current_request_with_host fixture to decorator * One more --- .../aladdin_connect/test_config_flow.py | 10 ++-- tests/components/cloud/test_account_link.py | 5 +- .../electric_kiwi/test_config_flow.py | 6 +-- tests/components/fitbit/test_config_flow.py | 15 +++--- .../components/geocaching/test_config_flow.py | 8 ++-- tests/components/google/test_config_flow.py | 2 +- .../google_assistant_sdk/test_config_flow.py | 8 ++-- .../google_mail/test_config_flow.py | 9 ++-- .../google_sheets/test_config_flow.py | 10 ++-- .../google_tasks/test_config_flow.py | 8 ++-- .../home_connect/test_config_flow.py | 4 +- .../husqvarna_automower/test_config_flow.py | 8 ++-- tests/components/lyric/test_config_flow.py | 4 +- .../components/microbees/test_config_flow.py | 12 ++--- tests/components/monzo/test_config_flow.py | 9 ++-- tests/components/myuplink/test_config_flow.py | 6 ++- tests/components/neato/test_config_flow.py | 5 +- tests/components/netatmo/test_config_flow.py | 5 +- .../components/ondilo_ico/test_config_flow.py | 4 +- tests/components/plex/test_config_flow.py | 46 ++++++++----------- tests/components/senz/test_config_flow.py | 3 +- tests/components/smappee/test_config_flow.py | 4 +- tests/components/spotify/test_config_flow.py | 8 ++-- tests/components/toon/test_config_flow.py | 16 ++++--- tests/components/twitch/test_config_flow.py | 12 ++--- tests/components/withings/test_config_flow.py | 12 +++-- tests/components/withings/test_init.py | 2 +- tests/components/xbox/test_config_flow.py | 4 +- tests/components/yolink/test_config_flow.py | 10 ++-- tests/components/youtube/test_config_flow.py | 12 ++--- 30 files changed, 139 insertions(+), 128 deletions(-) diff --git a/tests/components/aladdin_connect/test_config_flow.py b/tests/components/aladdin_connect/test_config_flow.py index 02244420925..1537e0f35da 100644 --- a/tests/components/aladdin_connect/test_config_flow.py +++ b/tests/components/aladdin_connect/test_config_flow.py @@ -80,11 +80,11 @@ async def _oauth_actions( ) +@pytest.mark.usefixtures("current_request_with_host") async def test_full_flow( hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator, aioclient_mock: AiohttpClientMocker, - current_request_with_host: None, setup_credentials: None, mock_setup_entry: AsyncMock, ) -> None: @@ -105,11 +105,11 @@ async def test_full_flow( assert len(mock_setup_entry.mock_calls) == 1 +@pytest.mark.usefixtures("current_request_with_host") async def test_duplicate_entry( hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator, aioclient_mock: AiohttpClientMocker, - current_request_with_host: None, setup_credentials: None, mock_config_entry: MockConfigEntry, ) -> None: @@ -125,11 +125,11 @@ async def test_duplicate_entry( assert result["reason"] == "already_configured" +@pytest.mark.usefixtures("current_request_with_host") async def test_reauth( hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator, aioclient_mock: AiohttpClientMocker, - current_request_with_host: None, setup_credentials: None, mock_config_entry: MockConfigEntry, mock_setup_entry: AsyncMock, @@ -154,11 +154,11 @@ async def test_reauth( assert result["reason"] == "reauth_successful" +@pytest.mark.usefixtures("current_request_with_host") async def test_reauth_wrong_account( hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator, aioclient_mock: AiohttpClientMocker, - current_request_with_host: None, setup_credentials: None, mock_setup_entry: AsyncMock, ) -> None: @@ -189,11 +189,11 @@ async def test_reauth_wrong_account( assert result["reason"] == "wrong_account" +@pytest.mark.usefixtures("current_request_with_host") async def test_reauth_old_account( hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator, aioclient_mock: AiohttpClientMocker, - current_request_with_host: None, setup_credentials: None, mock_setup_entry: AsyncMock, ) -> None: diff --git a/tests/components/cloud/test_account_link.py b/tests/components/cloud/test_account_link.py index 024118eaabf..3f108961bc5 100644 --- a/tests/components/cloud/test_account_link.py +++ b/tests/components/cloud/test_account_link.py @@ -178,9 +178,8 @@ async def test_get_services_error(hass: HomeAssistant) -> None: assert account_link.DATA_SERVICES not in hass.data -async def test_implementation( - hass: HomeAssistant, flow_handler, current_request_with_host: None -) -> None: +@pytest.mark.usefixtures("current_request_with_host") +async def test_implementation(hass: HomeAssistant, flow_handler) -> None: """Test Cloud OAuth2 implementation.""" hass.data["cloud"] = None diff --git a/tests/components/electric_kiwi/test_config_flow.py b/tests/components/electric_kiwi/test_config_flow.py index d74abab7692..bf248aafb13 100644 --- a/tests/components/electric_kiwi/test_config_flow.py +++ b/tests/components/electric_kiwi/test_config_flow.py @@ -53,11 +53,11 @@ async def test_config_flow_no_credentials(hass: HomeAssistant) -> None: assert result.get("reason") == "missing_credentials" +@pytest.mark.usefixtures("current_request_with_host") async def test_full_flow( hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator, aioclient_mock: AiohttpClientMocker, - current_request_with_host: None, setup_credentials: None, mock_setup_entry: AsyncMock, ) -> None: @@ -107,11 +107,11 @@ async def test_full_flow( assert len(mock_setup_entry.mock_calls) == 1 +@pytest.mark.usefixtures("current_request_with_host") async def test_existing_entry( hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator, aioclient_mock: AiohttpClientMocker, - current_request_with_host: None, setup_credentials: None, config_entry: MockConfigEntry, ) -> None: @@ -150,10 +150,10 @@ async def test_existing_entry( assert len(hass.config_entries.async_entries(DOMAIN)) == 1 +@pytest.mark.usefixtures("current_request_with_host") async def test_reauthentication( hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator, - current_request_with_host: None, aioclient_mock: AiohttpClientMocker, mock_setup_entry: MagicMock, config_entry: MockConfigEntry, diff --git a/tests/components/fitbit/test_config_flow.py b/tests/components/fitbit/test_config_flow.py index 843a85dec68..d5f3d09abdd 100644 --- a/tests/components/fitbit/test_config_flow.py +++ b/tests/components/fitbit/test_config_flow.py @@ -32,11 +32,11 @@ from tests.typing import ClientSessionGenerator REDIRECT_URL = "https://example.com/auth/external/callback" +@pytest.mark.usefixtures("current_request_with_host") async def test_full_flow( hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator, aioclient_mock: AiohttpClientMocker, - current_request_with_host: None, profile: None, setup_credentials: None, ) -> None: @@ -97,11 +97,11 @@ async def test_full_flow( (HTTPStatus.INTERNAL_SERVER_ERROR, "cannot_connect"), ], ) +@pytest.mark.usefixtures("current_request_with_host") async def test_token_error( hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator, aioclient_mock: AiohttpClientMocker, - current_request_with_host: None, profile: None, setup_credentials: None, status_code: HTTPStatus, @@ -155,11 +155,11 @@ async def test_token_error( ), ], ) +@pytest.mark.usefixtures("current_request_with_host") async def test_api_failure( hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator, aioclient_mock: AiohttpClientMocker, - current_request_with_host: None, requests_mock: Mocker, setup_credentials: None, http_status: HTTPStatus, @@ -207,12 +207,11 @@ async def test_api_failure( assert result.get("reason") == error_reason +@pytest.mark.usefixtures("current_request_with_host") async def test_config_entry_already_exists( hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator, aioclient_mock: AiohttpClientMocker, - current_request_with_host: None, - requests_mock: Mocker, setup_credentials: None, integration_setup: Callable[[], Awaitable[bool]], config_entry: MockConfigEntry, @@ -457,12 +456,12 @@ async def test_platform_setup_without_import( assert issue.translation_key == "deprecated_yaml_no_import" +@pytest.mark.usefixtures("current_request_with_host") async def test_reauth_flow( hass: HomeAssistant, config_entry: MockConfigEntry, hass_client_no_auth: ClientSessionGenerator, aioclient_mock: AiohttpClientMocker, - current_request_with_host: None, profile: None, setup_credentials: None, ) -> None: @@ -532,12 +531,12 @@ async def test_reauth_flow( @pytest.mark.parametrize("profile_id", ["other-user-id"]) +@pytest.mark.usefixtures("current_request_with_host") async def test_reauth_wrong_user_id( hass: HomeAssistant, config_entry: MockConfigEntry, hass_client_no_auth: ClientSessionGenerator, aioclient_mock: AiohttpClientMocker, - current_request_with_host: None, profile: None, setup_credentials: None, ) -> None: @@ -610,11 +609,11 @@ async def test_reauth_wrong_user_id( ], ids=("full_profile_data", "display_name_only"), ) +@pytest.mark.usefixtures("current_request_with_host") async def test_partial_profile_data( hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator, aioclient_mock: AiohttpClientMocker, - current_request_with_host: None, profile: None, setup_credentials: None, expected_title: str, diff --git a/tests/components/geocaching/test_config_flow.py b/tests/components/geocaching/test_config_flow.py index f4e8f0c8a96..0c2ce66b513 100644 --- a/tests/components/geocaching/test_config_flow.py +++ b/tests/components/geocaching/test_config_flow.py @@ -40,11 +40,11 @@ async def setup_credentials(hass: HomeAssistant) -> None: ) +@pytest.mark.usefixtures("current_request_with_host") async def test_full_flow( hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator, aioclient_mock: AiohttpClientMocker, - current_request_with_host: None, mock_geocaching_config_flow: MagicMock, mock_setup_entry: MagicMock, ) -> None: @@ -90,11 +90,11 @@ async def test_full_flow( assert len(mock_setup_entry.mock_calls) == 1 +@pytest.mark.usefixtures("current_request_with_host") async def test_existing_entry( hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator, aioclient_mock: AiohttpClientMocker, - current_request_with_host: None, mock_geocaching_config_flow: MagicMock, mock_setup_entry: MagicMock, mock_config_entry: MockConfigEntry, @@ -136,11 +136,11 @@ async def test_existing_entry( assert len(hass.config_entries.async_entries(DOMAIN)) == 1 +@pytest.mark.usefixtures("current_request_with_host") async def test_oauth_error( hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator, aioclient_mock: AiohttpClientMocker, - current_request_with_host: None, mock_geocaching_config_flow: MagicMock, mock_setup_entry: MagicMock, ) -> None: @@ -183,11 +183,11 @@ async def test_oauth_error( assert len(mock_setup_entry.mock_calls) == 0 +@pytest.mark.usefixtures("current_request_with_host") async def test_reauthentication( hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator, aioclient_mock: AiohttpClientMocker, - current_request_with_host: None, mock_geocaching_config_flow: MagicMock, mock_setup_entry: MagicMock, mock_config_entry: MockConfigEntry, diff --git a/tests/components/google/test_config_flow.py b/tests/components/google/test_config_flow.py index d75de491baf..53ec06619ac 100644 --- a/tests/components/google/test_config_flow.py +++ b/tests/components/google/test_config_flow.py @@ -656,9 +656,9 @@ async def test_options_flow_no_changes( assert config_entry.options == {"calendar_access": "read_write"} +@pytest.mark.usefixtures("current_request_with_host") async def test_web_auth_compatibility( hass: HomeAssistant, - current_request_with_host: None, mock_code_flow: Mock, aioclient_mock: AiohttpClientMocker, hass_client_no_auth: ClientSessionGenerator, diff --git a/tests/components/google_assistant_sdk/test_config_flow.py b/tests/components/google_assistant_sdk/test_config_flow.py index 4a4931d7bae..d66d12509e8 100644 --- a/tests/components/google_assistant_sdk/test_config_flow.py +++ b/tests/components/google_assistant_sdk/test_config_flow.py @@ -2,6 +2,8 @@ from unittest.mock import patch +import pytest + from homeassistant import config_entries from homeassistant.components.google_assistant_sdk.const import DOMAIN from homeassistant.core import HomeAssistant @@ -19,11 +21,11 @@ GOOGLE_TOKEN_URI = "https://oauth2.googleapis.com/token" TITLE = "Google Assistant SDK" +@pytest.mark.usefixtures("current_request_with_host") async def test_full_flow( hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator, aioclient_mock: AiohttpClientMocker, - current_request_with_host: None, setup_credentials, ) -> None: """Check full flow.""" @@ -80,11 +82,11 @@ async def test_full_flow( ) +@pytest.mark.usefixtures("current_request_with_host") async def test_reauth( hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator, aioclient_mock: AiohttpClientMocker, - current_request_with_host: None, setup_credentials, ) -> None: """Test the reauthentication case updates the existing config entry.""" @@ -155,11 +157,11 @@ async def test_reauth( assert config_entry.data["token"].get("refresh_token") == "mock-refresh-token" +@pytest.mark.usefixtures("current_request_with_host") async def test_single_instance_allowed( hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator, aioclient_mock: AiohttpClientMocker, - current_request_with_host: None, setup_credentials, ) -> None: """Test case where config flow allows a single test.""" diff --git a/tests/components/google_mail/test_config_flow.py b/tests/components/google_mail/test_config_flow.py index f784b654fba..1e933c8932a 100644 --- a/tests/components/google_mail/test_config_flow.py +++ b/tests/components/google_mail/test_config_flow.py @@ -18,10 +18,9 @@ from tests.test_util.aiohttp import AiohttpClientMocker from tests.typing import ClientSessionGenerator +@pytest.mark.usefixtures("current_request_with_host") async def test_full_flow( - hass: HomeAssistant, - hass_client_no_auth: ClientSessionGenerator, - current_request_with_host: None, + hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator ) -> None: """Check full flow.""" result = await hass.config_entries.flow.async_init( @@ -88,11 +87,11 @@ async def test_full_flow( ), ], ) +@pytest.mark.usefixtures("current_request_with_host") async def test_reauth( hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator, aioclient_mock: AiohttpClientMocker, - current_request_with_host: None, config_entry: MockConfigEntry, fixture: str, abort_reason: str, @@ -173,10 +172,10 @@ async def test_reauth( assert config_entry.data["token"].get("refresh_token") == "mock-refresh-token" +@pytest.mark.usefixtures("current_request_with_host") async def test_already_configured( hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator, - current_request_with_host: None, config_entry: MockConfigEntry, ) -> None: """Test case where config flow discovers unique id was already configured.""" diff --git a/tests/components/google_sheets/test_config_flow.py b/tests/components/google_sheets/test_config_flow.py index 5d8a19d1b61..1f51c9477b8 100644 --- a/tests/components/google_sheets/test_config_flow.py +++ b/tests/components/google_sheets/test_config_flow.py @@ -49,11 +49,11 @@ async def mock_client() -> Generator[Mock, None, None]: yield mock_client +@pytest.mark.usefixtures("current_request_with_host") async def test_full_flow( hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator, aioclient_mock: AiohttpClientMocker, - current_request_with_host: None, setup_credentials, mock_client, ) -> None: @@ -116,11 +116,11 @@ async def test_full_flow( ) +@pytest.mark.usefixtures("current_request_with_host") async def test_create_sheet_error( hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator, aioclient_mock: AiohttpClientMocker, - current_request_with_host: None, setup_credentials, mock_client, ) -> None: @@ -168,11 +168,11 @@ async def test_create_sheet_error( assert result.get("reason") == "create_spreadsheet_failure" +@pytest.mark.usefixtures("current_request_with_host") async def test_reauth( hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator, aioclient_mock: AiohttpClientMocker, - current_request_with_host: None, setup_credentials, mock_client, ) -> None: @@ -249,11 +249,11 @@ async def test_reauth( assert config_entry.data["token"].get("refresh_token") == "mock-refresh-token" +@pytest.mark.usefixtures("current_request_with_host") async def test_reauth_abort( hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator, aioclient_mock: AiohttpClientMocker, - current_request_with_host: None, setup_credentials, mock_client, ) -> None: @@ -318,11 +318,11 @@ async def test_reauth_abort( assert result.get("reason") == "open_spreadsheet_failure" +@pytest.mark.usefixtures("current_request_with_host") async def test_already_configured( hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator, aioclient_mock: AiohttpClientMocker, - current_request_with_host: None, setup_credentials, mock_client, ) -> None: diff --git a/tests/components/google_tasks/test_config_flow.py b/tests/components/google_tasks/test_config_flow.py index ba2a0ca8de6..0c56594a966 100644 --- a/tests/components/google_tasks/test_config_flow.py +++ b/tests/components/google_tasks/test_config_flow.py @@ -42,11 +42,11 @@ def setup_userinfo(user_identifier: str) -> Generator[Mock, None, None]: yield mock +@pytest.mark.usefixtures("current_request_with_host") async def test_full_flow( hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator, aioclient_mock: AiohttpClientMocker, - current_request_with_host: None, setup_credentials, setup_userinfo, ) -> None: @@ -97,11 +97,11 @@ async def test_full_flow( assert len(mock_setup.mock_calls) == 1 +@pytest.mark.usefixtures("current_request_with_host") async def test_api_not_enabled( hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator, aioclient_mock: AiohttpClientMocker, - current_request_with_host: None, setup_credentials, setup_userinfo, ) -> None: @@ -158,11 +158,11 @@ async def test_api_not_enabled( ) +@pytest.mark.usefixtures("current_request_with_host") async def test_general_exception( hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator, aioclient_mock: AiohttpClientMocker, - current_request_with_host: None, setup_credentials, setup_userinfo, ) -> None: @@ -235,11 +235,11 @@ async def test_general_exception( ), ], ) +@pytest.mark.usefixtures("current_request_with_host") async def test_reauth( hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator, aioclient_mock: AiohttpClientMocker, - current_request_with_host: None, setup_credentials, setup_userinfo, user_identifier: str, diff --git a/tests/components/home_connect/test_config_flow.py b/tests/components/home_connect/test_config_flow.py index 2c094c74246..80f53e20b39 100644 --- a/tests/components/home_connect/test_config_flow.py +++ b/tests/components/home_connect/test_config_flow.py @@ -3,6 +3,8 @@ from http import HTTPStatus from unittest.mock import patch +import pytest + from homeassistant import config_entries, setup from homeassistant.components.application_credentials import ( ClientCredential, @@ -24,11 +26,11 @@ CLIENT_ID = "1234" CLIENT_SECRET = "5678" +@pytest.mark.usefixtures("current_request_with_host") async def test_full_flow( hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator, aioclient_mock: AiohttpClientMocker, - current_request_with_host: None, ) -> None: """Check full flow.""" assert await setup.async_setup_component(hass, "home_connect", {}) diff --git a/tests/components/husqvarna_automower/test_config_flow.py b/tests/components/husqvarna_automower/test_config_flow.py index efac36b5a7a..31e8a9afcbd 100644 --- a/tests/components/husqvarna_automower/test_config_flow.py +++ b/tests/components/husqvarna_automower/test_config_flow.py @@ -30,11 +30,11 @@ from tests.typing import ClientSessionGenerator ("iam:read", 0), ], ) +@pytest.mark.usefixtures("current_request_with_host") async def test_full_flow( hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator, aioclient_mock: AiohttpClientMocker, - current_request_with_host: None, jwt: str, new_scope: str, amount: int, @@ -87,10 +87,10 @@ async def test_full_flow( assert len(mock_setup.mock_calls) == amount +@pytest.mark.usefixtures("current_request_with_host") async def test_config_non_unique_profile( hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator, - current_request_with_host: None, mock_config_entry: MockConfigEntry, aioclient_mock: AiohttpClientMocker, mock_automower_client: AsyncMock, @@ -148,12 +148,12 @@ async def test_config_non_unique_profile( ("iam:read", "missing_scope", "missing_amc_scope", "iam:read"), ], ) +@pytest.mark.usefixtures("current_request_with_host") async def test_reauth( hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator, aioclient_mock: AiohttpClientMocker, mock_config_entry: MockConfigEntry, - current_request_with_host: None, mock_automower_client: AsyncMock, jwt: str, step_id: str, @@ -228,12 +228,12 @@ async def test_reauth( ("wrong_user_id", "wrong_account"), ], ) +@pytest.mark.usefixtures("current_request_with_host") async def test_reauth_wrong_account( hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator, aioclient_mock: AiohttpClientMocker, mock_config_entry: MockConfigEntry, - current_request_with_host: None, mock_automower_client: AsyncMock, jwt, user_id: str, diff --git a/tests/components/lyric/test_config_flow.py b/tests/components/lyric/test_config_flow.py index 73b3aae2d3d..e1a8d1131dc 100644 --- a/tests/components/lyric/test_config_flow.py +++ b/tests/components/lyric/test_config_flow.py @@ -45,11 +45,11 @@ async def test_abort_if_no_configuration(hass: HomeAssistant) -> None: assert result["reason"] == "missing_credentials" +@pytest.mark.usefixtures("current_request_with_host") async def test_full_flow( hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator, aioclient_mock: AiohttpClientMocker, - current_request_with_host: None, mock_impl, ) -> None: """Check full flow.""" @@ -112,11 +112,11 @@ async def test_full_flow( assert len(mock_setup.mock_calls) == 1 +@pytest.mark.usefixtures("current_request_with_host") async def test_reauthentication_flow( hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator, aioclient_mock: AiohttpClientMocker, - current_request_with_host: None, mock_impl, ) -> None: """Test reauthentication flow.""" diff --git a/tests/components/microbees/test_config_flow.py b/tests/components/microbees/test_config_flow.py index 327d0214f7a..d168dcd5017 100644 --- a/tests/components/microbees/test_config_flow.py +++ b/tests/components/microbees/test_config_flow.py @@ -19,10 +19,10 @@ from tests.test_util.aiohttp import AiohttpClientMocker from tests.typing import ClientSessionGenerator +@pytest.mark.usefixtures("current_request_with_host") async def test_full_flow( hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator, - current_request_with_host: None, aioclient_mock: AiohttpClientMocker, microbees: AsyncMock, ) -> None: @@ -80,10 +80,10 @@ async def test_full_flow( assert result["result"].data["token"]["refresh_token"] == "mock-refresh-token" +@pytest.mark.usefixtures("current_request_with_host") async def test_config_non_unique_profile( hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator, - current_request_with_host: None, microbees: AsyncMock, config_entry: MockConfigEntry, aioclient_mock: AiohttpClientMocker, @@ -133,13 +133,13 @@ async def test_config_non_unique_profile( assert result["reason"] == "already_configured" +@pytest.mark.usefixtures("current_request_with_host") async def test_config_reauth_profile( hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator, aioclient_mock: AiohttpClientMocker, config_entry: MockConfigEntry, microbees: AsyncMock, - current_request_with_host, ) -> None: """Test reauth an existing profile reauthenticates the config entry.""" await setup_integration(hass, config_entry) @@ -194,13 +194,13 @@ async def test_config_reauth_profile( assert result["reason"] == "reauth_successful" +@pytest.mark.usefixtures("current_request_with_host") async def test_config_reauth_wrong_account( hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator, aioclient_mock: AiohttpClientMocker, config_entry: MockConfigEntry, microbees: AsyncMock, - current_request_with_host, ) -> None: """Test reauth with wrong account.""" await setup_integration(hass, config_entry) @@ -255,12 +255,12 @@ async def test_config_reauth_wrong_account( assert result["reason"] == "wrong_account" +@pytest.mark.usefixtures("current_request_with_host") async def test_config_flow_with_invalid_credentials( hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator, aioclient_mock: AiohttpClientMocker, microbees: AsyncMock, - current_request_with_host, ) -> None: """Test flow with invalid credentials.""" result = await hass.config_entries.flow.async_init( @@ -310,6 +310,7 @@ async def test_config_flow_with_invalid_credentials( (Exception("Unexpected error"), "unknown"), ], ) +@pytest.mark.usefixtures("current_request_with_host") async def test_unexpected_exceptions( hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator, @@ -318,7 +319,6 @@ async def test_unexpected_exceptions( microbees: AsyncMock, exception: Exception, error: str, - current_request_with_host, ) -> None: """Test unknown error from server.""" await setup_integration(hass, config_entry) diff --git a/tests/components/monzo/test_config_flow.py b/tests/components/monzo/test_config_flow.py index 7ad4c072723..b7d0de9cdc3 100644 --- a/tests/components/monzo/test_config_flow.py +++ b/tests/components/monzo/test_config_flow.py @@ -5,6 +5,7 @@ from unittest.mock import AsyncMock, patch from freezegun.api import FrozenDateTimeFactory from monzopy import AuthorisationExpiredError +import pytest from homeassistant.components.monzo.application_credentials import ( OAUTH2_AUTHORIZE, @@ -24,10 +25,10 @@ from tests.test_util.aiohttp import AiohttpClientMocker from tests.typing import ClientSessionGenerator +@pytest.mark.usefixtures("current_request_with_host") async def test_full_flow( hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator, - current_request_with_host: None, aioclient_mock: AiohttpClientMocker, ) -> None: """Check full flow.""" @@ -92,10 +93,10 @@ async def test_full_flow( assert result["result"].data["token"]["refresh_token"] == "mock-refresh-token" +@pytest.mark.usefixtures("current_request_with_host") async def test_config_non_unique_profile( hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator, - current_request_with_host: None, monzo: AsyncMock, polling_config_entry: MockConfigEntry, aioclient_mock: AiohttpClientMocker, @@ -142,13 +143,13 @@ async def test_config_non_unique_profile( assert result["reason"] == "already_configured" +@pytest.mark.usefixtures("current_request_with_host") async def test_config_reauth_profile( hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator, aioclient_mock: AiohttpClientMocker, polling_config_entry: MockConfigEntry, monzo: AsyncMock, - current_request_with_host: None, ) -> None: """Test reauth an existing profile reauthenticates the config entry.""" await setup_integration(hass, polling_config_entry) @@ -212,12 +213,12 @@ async def test_config_reauth_profile( assert polling_config_entry.data["token"]["access_token"] == "new-mock-access-token" +@pytest.mark.usefixtures("current_request_with_host") async def test_config_reauth_wrong_account( hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator, aioclient_mock: AiohttpClientMocker, polling_config_entry: MockConfigEntry, - current_request_with_host: None, ) -> None: """Test reauth with wrong account.""" await setup_integration(hass, polling_config_entry) diff --git a/tests/components/myuplink/test_config_flow.py b/tests/components/myuplink/test_config_flow.py index 7f94d4af03f..3ae32575257 100644 --- a/tests/components/myuplink/test_config_flow.py +++ b/tests/components/myuplink/test_config_flow.py @@ -2,6 +2,8 @@ from unittest.mock import patch +import pytest + from homeassistant import config_entries from homeassistant.components.myuplink.const import ( DOMAIN, @@ -22,11 +24,11 @@ REDIRECT_URL = "https://example.com/auth/external/callback" CURRENT_SCOPE = "WRITESYSTEM READSYSTEM offline_access" +@pytest.mark.usefixtures("current_request_with_host") async def test_full_flow( hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator, aioclient_mock: AiohttpClientMocker, - current_request_with_host: None, setup_credentials, ) -> None: """Check full flow.""" @@ -72,11 +74,11 @@ async def test_full_flow( assert len(mock_setup.mock_calls) == 1 +@pytest.mark.usefixtures("current_request_with_host") async def test_flow_reauth( hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator, aioclient_mock: AiohttpClientMocker, - current_request_with_host: None, setup_credentials: None, mock_config_entry: MockConfigEntry, expires_at: float, diff --git a/tests/components/neato/test_config_flow.py b/tests/components/neato/test_config_flow.py index 132b23ef157..1b86c4e9980 100644 --- a/tests/components/neato/test_config_flow.py +++ b/tests/components/neato/test_config_flow.py @@ -3,6 +3,7 @@ from unittest.mock import patch from pybotvac.neato import Neato +import pytest from homeassistant import config_entries, setup from homeassistant.components.application_credentials import ( @@ -27,11 +28,11 @@ OAUTH2_AUTHORIZE = VENDOR.auth_endpoint OAUTH2_TOKEN = VENDOR.token_endpoint +@pytest.mark.usefixtures("current_request_with_host") async def test_full_flow( hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator, aioclient_mock: AiohttpClientMocker, - current_request_with_host: None, ) -> None: """Check full flow.""" assert await setup.async_setup_component(hass, "neato", {}) @@ -98,11 +99,11 @@ async def test_abort_if_already_setup(hass: HomeAssistant) -> None: assert result["reason"] == "already_configured" +@pytest.mark.usefixtures("current_request_with_host") async def test_reauth( hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator, aioclient_mock: AiohttpClientMocker, - current_request_with_host: None, ) -> None: """Test initialization of the reauth flow.""" assert await setup.async_setup_component(hass, "neato", {}) diff --git a/tests/components/netatmo/test_config_flow.py b/tests/components/netatmo/test_config_flow.py index 933f782c9d9..29a065c3be3 100644 --- a/tests/components/netatmo/test_config_flow.py +++ b/tests/components/netatmo/test_config_flow.py @@ -4,6 +4,7 @@ from ipaddress import ip_address from unittest.mock import patch from pyatmo.const import ALL_SCOPES +import pytest from homeassistant import config_entries from homeassistant.components import zeroconf @@ -59,11 +60,11 @@ async def test_abort_if_existing_entry(hass: HomeAssistant) -> None: assert result["reason"] == "already_configured" +@pytest.mark.usefixtures("current_request_with_host") async def test_full_flow( hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator, aioclient_mock: AiohttpClientMocker, - current_request_with_host: None, ) -> None: """Check full flow.""" @@ -226,11 +227,11 @@ async def test_option_flow_wrong_coordinates(hass: HomeAssistant) -> None: assert config_entry.options[CONF_WEATHER_AREAS]["Home"][k] == v +@pytest.mark.usefixtures("current_request_with_host") async def test_reauth( hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator, aioclient_mock: AiohttpClientMocker, - current_request_with_host: None, ) -> None: """Test initialization of the reauth flow.""" diff --git a/tests/components/ondilo_ico/test_config_flow.py b/tests/components/ondilo_ico/test_config_flow.py index 6b8fcbeefea..deab2a8e0b9 100644 --- a/tests/components/ondilo_ico/test_config_flow.py +++ b/tests/components/ondilo_ico/test_config_flow.py @@ -2,6 +2,8 @@ from unittest.mock import patch +import pytest + from homeassistant import config_entries, setup from homeassistant.components.ondilo_ico.const import ( DOMAIN, @@ -34,11 +36,11 @@ async def test_abort_if_existing_entry(hass: HomeAssistant) -> None: assert result["reason"] == "single_instance_allowed" +@pytest.mark.usefixtures("current_request_with_host") async def test_full_flow( hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator, aioclient_mock: AiohttpClientMocker, - current_request_with_host: None, ) -> None: """Check full flow.""" assert await setup.async_setup_component( diff --git a/tests/components/plex/test_config_flow.py b/tests/components/plex/test_config_flow.py index 5f2531992d4..a47ea275ddb 100644 --- a/tests/components/plex/test_config_flow.py +++ b/tests/components/plex/test_config_flow.py @@ -49,9 +49,8 @@ from tests.common import MockConfigEntry from tests.typing import ClientSessionGenerator -async def test_bad_credentials( - hass: HomeAssistant, current_request_with_host: None -) -> None: +@pytest.mark.usefixtures("current_request_with_host") +async def test_bad_credentials(hass: HomeAssistant) -> None: """Test when provided credentials are rejected.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} @@ -81,9 +80,8 @@ async def test_bad_credentials( assert result["errors"][CONF_TOKEN] == "faulty_credentials" -async def test_bad_hostname( - hass: HomeAssistant, mock_plex_calls, current_request_with_host: None -) -> None: +@pytest.mark.usefixtures("current_request_with_host") +async def test_bad_hostname(hass: HomeAssistant, mock_plex_calls) -> None: """Test when an invalid address is provided.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} @@ -114,9 +112,8 @@ async def test_bad_hostname( assert result["errors"][CONF_HOST] == "not_found" -async def test_unknown_exception( - hass: HomeAssistant, current_request_with_host: None -) -> None: +@pytest.mark.usefixtures("current_request_with_host") +async def test_unknown_exception(hass: HomeAssistant) -> None: """Test when an unknown exception is encountered.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} @@ -142,12 +139,12 @@ async def test_unknown_exception( assert result["reason"] == "unknown" +@pytest.mark.usefixtures("current_request_with_host") async def test_no_servers_found( hass: HomeAssistant, mock_plex_calls, requests_mock: requests_mock.Mocker, empty_payload, - current_request_with_host: None, ) -> None: """Test when no servers are on an account.""" requests_mock.get("https://plex.tv/api/v2/resources", text=empty_payload) @@ -176,10 +173,10 @@ async def test_no_servers_found( assert result["errors"]["base"] == "no_servers" +@pytest.mark.usefixtures("current_request_with_host") async def test_single_available_server( hass: HomeAssistant, mock_plex_calls, - current_request_with_host: None, mock_setup_entry: AsyncMock, ) -> None: """Test creating an entry with one server available.""" @@ -218,12 +215,12 @@ async def test_single_available_server( mock_setup_entry.assert_called_once() +@pytest.mark.usefixtures("current_request_with_host") async def test_multiple_servers_with_selection( hass: HomeAssistant, mock_plex_calls, requests_mock: requests_mock.Mocker, plextv_resources_two_servers, - current_request_with_host: None, mock_setup_entry: AsyncMock, ) -> None: """Test creating an entry with multiple servers available.""" @@ -275,12 +272,12 @@ async def test_multiple_servers_with_selection( mock_setup_entry.assert_called_once() +@pytest.mark.usefixtures("current_request_with_host") async def test_adding_last_unconfigured_server( hass: HomeAssistant, mock_plex_calls, requests_mock: requests_mock.Mocker, plextv_resources_two_servers, - current_request_with_host: None, mock_setup_entry: AsyncMock, ) -> None: """Test automatically adding last unconfigured server when multiple servers on account.""" @@ -332,13 +329,13 @@ async def test_adding_last_unconfigured_server( assert mock_setup_entry.call_count == 2 +@pytest.mark.usefixtures("current_request_with_host") async def test_all_available_servers_configured( hass: HomeAssistant, entry, requests_mock: requests_mock.Mocker, plextv_account, plextv_resources_two_servers, - current_request_with_host: None, ) -> None: """Test when all available servers are already configured.""" entry.add_to_hass(hass) @@ -479,9 +476,8 @@ async def test_option_flow_new_users_available( assert "[New]" in multiselect_defaults[user] -async def test_external_timed_out( - hass: HomeAssistant, current_request_with_host: None -) -> None: +@pytest.mark.usefixtures("current_request_with_host") +async def test_external_timed_out(hass: HomeAssistant) -> None: """Test when external flow times out.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} @@ -506,10 +502,10 @@ async def test_external_timed_out( assert result["reason"] == "token_request_timeout" +@pytest.mark.usefixtures("current_request_with_host") async def test_callback_view( hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator, - current_request_with_host: None, ) -> None: """Test callback view.""" result = await hass.config_entries.flow.async_init( @@ -534,9 +530,8 @@ async def test_callback_view( assert resp.status == HTTPStatus.OK -async def test_manual_config( - hass: HomeAssistant, mock_plex_calls, current_request_with_host: None -) -> None: +@pytest.mark.usefixtures("current_request_with_host") +async def test_manual_config(hass: HomeAssistant, mock_plex_calls) -> None: """Test creating via manual configuration.""" class WrongCertValidaitionException(requests.exceptions.SSLError): @@ -739,11 +734,11 @@ async def test_integration_discovery(hass: HomeAssistant) -> None: assert flow["step_id"] == "user" +@pytest.mark.usefixtures("current_request_with_host") async def test_reauth( hass: HomeAssistant, entry: MockConfigEntry, mock_plex_calls: None, - current_request_with_host: None, mock_setup_entry: AsyncMock, ) -> None: """Test setup and reauthorization of a Plex token.""" @@ -783,11 +778,11 @@ async def test_reauth( mock_setup_entry.assert_called_once() +@pytest.mark.usefixtures("current_request_with_host") async def test_reauth_multiple_servers_available( hass: HomeAssistant, entry: MockConfigEntry, mock_plex_calls: None, - current_request_with_host: None, requests_mock: requests_mock.Mocker, plextv_resources_two_servers: str, mock_setup_entry: AsyncMock, @@ -853,9 +848,8 @@ async def test_client_request_missing(hass: HomeAssistant) -> None: ) -async def test_client_header_issues( - hass: HomeAssistant, current_request_with_host: None -) -> None: +@pytest.mark.usefixtures("current_request_with_host") +async def test_client_header_issues(hass: HomeAssistant) -> None: """Test when client headers are not set properly.""" class MockRequest: diff --git a/tests/components/senz/test_config_flow.py b/tests/components/senz/test_config_flow.py index 04ef1a6de0c..4faf8775a62 100644 --- a/tests/components/senz/test_config_flow.py +++ b/tests/components/senz/test_config_flow.py @@ -3,6 +3,7 @@ from unittest.mock import patch from aiosenz import AUTHORIZATION_ENDPOINT, TOKEN_ENDPOINT +import pytest from homeassistant import config_entries from homeassistant.components.application_credentials import ( @@ -21,11 +22,11 @@ CLIENT_ID = "1234" CLIENT_SECRET = "5678" +@pytest.mark.usefixtures("current_request_with_host") async def test_full_flow( hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator, aioclient_mock: AiohttpClientMocker, - current_request_with_host: None, ) -> None: """Check full flow.""" await async_setup_component(hass, DOMAIN, {}) diff --git a/tests/components/smappee/test_config_flow.py b/tests/components/smappee/test_config_flow.py index 82f5baf952f..c06ab551ef6 100644 --- a/tests/components/smappee/test_config_flow.py +++ b/tests/components/smappee/test_config_flow.py @@ -4,6 +4,8 @@ from http import HTTPStatus from ipaddress import ip_address from unittest.mock import patch +import pytest + from homeassistant import setup from homeassistant.components import zeroconf from homeassistant.components.smappee.const import ( @@ -427,11 +429,11 @@ async def test_abort_cloud_flow_if_local_device_exists(hass: HomeAssistant) -> N assert len(hass.config_entries.async_entries(DOMAIN)) == 1 +@pytest.mark.usefixtures("current_request_with_host") async def test_full_user_flow( hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator, aioclient_mock: AiohttpClientMocker, - current_request_with_host: None, ) -> None: """Check full flow.""" assert await setup.async_setup_component( diff --git a/tests/components/spotify/test_config_flow.py b/tests/components/spotify/test_config_flow.py index 6de549c8bc7..6040fcd84f2 100644 --- a/tests/components/spotify/test_config_flow.py +++ b/tests/components/spotify/test_config_flow.py @@ -76,12 +76,12 @@ async def test_zeroconf_abort_if_existing_entry(hass: HomeAssistant) -> None: assert result["reason"] == "already_configured" +@pytest.mark.usefixtures("current_request_with_host") async def test_full_flow( hass: HomeAssistant, component_setup, hass_client_no_auth: ClientSessionGenerator, aioclient_mock: AiohttpClientMocker, - current_request_with_host: None, ) -> None: """Check a full flow.""" result = await hass.config_entries.flow.async_init( @@ -143,12 +143,12 @@ async def test_full_flow( } +@pytest.mark.usefixtures("current_request_with_host") async def test_abort_if_spotify_error( hass: HomeAssistant, component_setup, hass_client_no_auth: ClientSessionGenerator, aioclient_mock: AiohttpClientMocker, - current_request_with_host: None, ) -> None: """Check Spotify errors causes flow to abort.""" result = await hass.config_entries.flow.async_init( @@ -185,12 +185,12 @@ async def test_abort_if_spotify_error( assert result["reason"] == "connection_error" +@pytest.mark.usefixtures("current_request_with_host") async def test_reauthentication( hass: HomeAssistant, component_setup, hass_client_no_auth: ClientSessionGenerator, aioclient_mock: AiohttpClientMocker, - current_request_with_host: None, ) -> None: """Test Spotify reauthentication.""" old_entry = MockConfigEntry( @@ -253,12 +253,12 @@ async def test_reauthentication( } +@pytest.mark.usefixtures("current_request_with_host") async def test_reauth_account_mismatch( hass: HomeAssistant, component_setup, hass_client_no_auth: ClientSessionGenerator, aioclient_mock: AiohttpClientMocker, - current_request_with_host: None, ) -> None: """Test Spotify reauthentication with different account.""" old_entry = MockConfigEntry( diff --git a/tests/components/toon/test_config_flow.py b/tests/components/toon/test_config_flow.py index 7bda813e447..588924b416f 100644 --- a/tests/components/toon/test_config_flow.py +++ b/tests/components/toon/test_config_flow.py @@ -3,6 +3,7 @@ from http import HTTPStatus from unittest.mock import patch +import pytest from toonapi import Agreement, ToonError from homeassistant.components.toon.const import CONF_AGREEMENT, CONF_MIGRATE, DOMAIN @@ -45,11 +46,11 @@ async def test_abort_if_no_configuration(hass: HomeAssistant) -> None: assert result["reason"] == "missing_configuration" +@pytest.mark.usefixtures("current_request_with_host") async def test_full_flow_implementation( hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator, aioclient_mock: AiohttpClientMocker, - current_request_with_host: None, ) -> None: """Test registering an integration and finishing flow works.""" await setup_component(hass) @@ -111,11 +112,11 @@ async def test_full_flow_implementation( } +@pytest.mark.usefixtures("current_request_with_host") async def test_no_agreements( hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator, aioclient_mock: AiohttpClientMocker, - current_request_with_host: None, ) -> None: """Test abort when there are no displays.""" await setup_component(hass) @@ -153,11 +154,11 @@ async def test_no_agreements( assert result3["reason"] == "no_agreements" +@pytest.mark.usefixtures("current_request_with_host") async def test_multiple_agreements( hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator, aioclient_mock: AiohttpClientMocker, - current_request_with_host: None, ) -> None: """Test abort when there are no displays.""" await setup_component(hass) @@ -205,11 +206,11 @@ async def test_multiple_agreements( assert result4["data"]["agreement_id"] == 1 +@pytest.mark.usefixtures("current_request_with_host") async def test_agreement_already_set_up( hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator, aioclient_mock: AiohttpClientMocker, - current_request_with_host: None, ) -> None: """Test showing display form again if display already exists.""" await setup_component(hass) @@ -248,11 +249,11 @@ async def test_agreement_already_set_up( assert result3["reason"] == "already_configured" +@pytest.mark.usefixtures("current_request_with_host") async def test_toon_abort( hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator, aioclient_mock: AiohttpClientMocker, - current_request_with_host: None, ) -> None: """Test we abort on Toon error.""" await setup_component(hass) @@ -290,7 +291,8 @@ async def test_toon_abort( assert result2["reason"] == "connection_error" -async def test_import(hass: HomeAssistant, current_request_with_host: None) -> None: +@pytest.mark.usefixtures("current_request_with_host") +async def test_import(hass: HomeAssistant) -> None: """Test if importing step works.""" await setup_component(hass) @@ -304,11 +306,11 @@ async def test_import(hass: HomeAssistant, current_request_with_host: None) -> N assert result["reason"] == "already_in_progress" +@pytest.mark.usefixtures("current_request_with_host") async def test_import_migration( hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator, aioclient_mock: AiohttpClientMocker, - current_request_with_host: None, ) -> None: """Test if importing step with migration works.""" old_entry = MockConfigEntry(domain=DOMAIN, unique_id=123, version=1) diff --git a/tests/components/twitch/test_config_flow.py b/tests/components/twitch/test_config_flow.py index 7807cd38e1a..7d677df1adb 100644 --- a/tests/components/twitch/test_config_flow.py +++ b/tests/components/twitch/test_config_flow.py @@ -2,6 +2,7 @@ from unittest.mock import AsyncMock +import pytest from twitchAPI.object.api import TwitchUser from homeassistant.components.twitch.const import ( @@ -47,10 +48,10 @@ async def _do_get_token( assert resp.headers["content-type"] == "text/html; charset=utf-8" +@pytest.mark.usefixtures("current_request_with_host") async def test_full_flow( hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator, - current_request_with_host: None, mock_setup_entry, twitch_mock: AsyncMock, scopes: list[str], @@ -75,10 +76,10 @@ async def test_full_flow( assert result["options"] == {CONF_CHANNELS: ["internetofthings", "homeassistant"]} +@pytest.mark.usefixtures("current_request_with_host") async def test_already_configured( hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator, - current_request_with_host: None, config_entry: MockConfigEntry, mock_setup_entry, twitch_mock: AsyncMock, @@ -97,10 +98,10 @@ async def test_already_configured( assert result["reason"] == "already_configured" +@pytest.mark.usefixtures("current_request_with_host") async def test_reauth( hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator, - current_request_with_host: None, config_entry: MockConfigEntry, mock_setup_entry, twitch_mock: AsyncMock, @@ -129,10 +130,10 @@ async def test_reauth( assert result["reason"] == "reauth_successful" +@pytest.mark.usefixtures("current_request_with_host") async def test_reauth_from_import( hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator, - current_request_with_host: None, mock_setup_entry, twitch_mock: AsyncMock, expires_at, @@ -158,7 +159,6 @@ async def test_reauth_from_import( await test_reauth( hass, hass_client_no_auth, - current_request_with_host, config_entry, mock_setup_entry, twitch_mock, @@ -170,10 +170,10 @@ async def test_reauth_from_import( assert entry.options == {CONF_CHANNELS: ["internetofthings", "homeassistant"]} +@pytest.mark.usefixtures("current_request_with_host") async def test_reauth_wrong_account( hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator, - current_request_with_host: None, config_entry: MockConfigEntry, mock_setup_entry, twitch_mock: AsyncMock, diff --git a/tests/components/withings/test_config_flow.py b/tests/components/withings/test_config_flow.py index 9f4b265ed4f..20bef90a31e 100644 --- a/tests/components/withings/test_config_flow.py +++ b/tests/components/withings/test_config_flow.py @@ -2,6 +2,8 @@ from unittest.mock import AsyncMock, patch +import pytest + from homeassistant.components.withings.const import DOMAIN from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER from homeassistant.core import HomeAssistant @@ -16,10 +18,10 @@ from tests.test_util.aiohttp import AiohttpClientMocker from tests.typing import ClientSessionGenerator +@pytest.mark.usefixtures("current_request_with_host") async def test_full_flow( hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator, - current_request_with_host: None, aioclient_mock: AiohttpClientMocker, ) -> None: """Check full flow.""" @@ -79,10 +81,10 @@ async def test_full_flow( assert result["result"].data["token"]["refresh_token"] == "mock-refresh-token" +@pytest.mark.usefixtures("current_request_with_host") async def test_config_non_unique_profile( hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator, - current_request_with_host: None, withings: AsyncMock, polling_config_entry: MockConfigEntry, aioclient_mock: AiohttpClientMocker, @@ -132,13 +134,13 @@ async def test_config_non_unique_profile( assert result["reason"] == "already_configured" +@pytest.mark.usefixtures("current_request_with_host") async def test_config_reauth_profile( hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator, aioclient_mock: AiohttpClientMocker, polling_config_entry: MockConfigEntry, withings: AsyncMock, - current_request_with_host, ) -> None: """Test reauth an existing profile reauthenticates the config entry.""" await setup_integration(hass, polling_config_entry) @@ -194,13 +196,13 @@ async def test_config_reauth_profile( assert result["reason"] == "reauth_successful" +@pytest.mark.usefixtures("current_request_with_host") async def test_config_reauth_wrong_account( hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator, aioclient_mock: AiohttpClientMocker, polling_config_entry: MockConfigEntry, withings: AsyncMock, - current_request_with_host, ) -> None: """Test reauth with wrong account.""" await setup_integration(hass, polling_config_entry) @@ -256,13 +258,13 @@ async def test_config_reauth_wrong_account( assert result["reason"] == "wrong_account" +@pytest.mark.usefixtures("current_request_with_host") async def test_config_flow_with_invalid_credentials( hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator, aioclient_mock: AiohttpClientMocker, polling_config_entry: MockConfigEntry, withings: AsyncMock, - current_request_with_host, ) -> None: """Test flow with invalid credentials.""" result = await hass.config_entries.flow.async_init( diff --git a/tests/components/withings/test_init.py b/tests/components/withings/test_init.py index 3ade0fb7c3a..0375d1869d9 100644 --- a/tests/components/withings/test_init.py +++ b/tests/components/withings/test_init.py @@ -544,6 +544,7 @@ async def test_cloud_disconnect_retry( ), # Success, we ignore the user_id ], ) +@pytest.mark.usefixtures("current_request_with_host") async def test_webhook_post( hass: HomeAssistant, withings: AsyncMock, @@ -551,7 +552,6 @@ async def test_webhook_post( hass_client_no_auth: ClientSessionGenerator, body: dict[str, Any], expected_code: int, - current_request_with_host: None, freezer: FrozenDateTimeFactory, ) -> None: """Test webhook callback.""" diff --git a/tests/components/xbox/test_config_flow.py b/tests/components/xbox/test_config_flow.py index e547909f946..8c2e6df6f89 100644 --- a/tests/components/xbox/test_config_flow.py +++ b/tests/components/xbox/test_config_flow.py @@ -3,6 +3,8 @@ from http import HTTPStatus from unittest.mock import patch +import pytest + from homeassistant import config_entries, setup from homeassistant.components.application_credentials import ( ClientCredential, @@ -32,11 +34,11 @@ async def test_abort_if_existing_entry(hass: HomeAssistant) -> None: assert result["reason"] == "single_instance_allowed" +@pytest.mark.usefixtures("current_request_with_host") async def test_full_flow( hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator, aioclient_mock: AiohttpClientMocker, - current_request_with_host: None, ) -> None: """Check full flow.""" assert await setup.async_setup_component(hass, "application_credentials", {}) diff --git a/tests/components/yolink/test_config_flow.py b/tests/components/yolink/test_config_flow.py index f62bd3ac1ac..d7ba09e4269 100644 --- a/tests/components/yolink/test_config_flow.py +++ b/tests/components/yolink/test_config_flow.py @@ -3,6 +3,7 @@ from http import HTTPStatus from unittest.mock import patch +import pytest from yolink.const import OAUTH2_AUTHORIZE, OAUTH2_TOKEN from homeassistant import config_entries, setup @@ -40,11 +41,11 @@ async def test_abort_if_existing_entry(hass: HomeAssistant) -> None: assert result["reason"] == "already_configured" +@pytest.mark.usefixtures("current_request_with_host") async def test_full_flow( hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator, aioclient_mock: AiohttpClientMocker, - current_request_with_host: None, ) -> None: """Check full flow.""" assert await setup.async_setup_component( @@ -115,9 +116,8 @@ async def test_full_flow( assert len(mock_setup.mock_calls) == 1 -async def test_abort_if_authorization_timeout( - hass: HomeAssistant, current_request_with_host: None -) -> None: +@pytest.mark.usefixtures("current_request_with_host") +async def test_abort_if_authorization_timeout(hass: HomeAssistant) -> None: """Check yolink authorization timeout.""" assert await setup.async_setup_component( hass, @@ -142,11 +142,11 @@ async def test_abort_if_authorization_timeout( assert result["reason"] == "authorize_url_timeout" +@pytest.mark.usefixtures("current_request_with_host") async def test_reauthentication( hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator, aioclient_mock: AiohttpClientMocker, - current_request_with_host: None, ) -> None: """Test yolink reauthentication.""" await setup.async_setup_component( diff --git a/tests/components/youtube/test_config_flow.py b/tests/components/youtube/test_config_flow.py index 1f68047b1c5..73652d9b239 100644 --- a/tests/components/youtube/test_config_flow.py +++ b/tests/components/youtube/test_config_flow.py @@ -26,10 +26,10 @@ from tests.test_util.aiohttp import AiohttpClientMocker from tests.typing import ClientSessionGenerator +@pytest.mark.usefixtures("current_request_with_host") async def test_full_flow( hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator, - current_request_with_host: None, ) -> None: """Check full flow.""" result = await hass.config_entries.flow.async_init( @@ -85,10 +85,10 @@ async def test_full_flow( assert result["options"] == {CONF_CHANNELS: ["UC_x5XG1OV2P6uZZ5FSM9Ttw"]} +@pytest.mark.usefixtures("current_request_with_host") async def test_flow_abort_without_channel( hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator, - current_request_with_host: None, ) -> None: """Check abort flow if user has no channel.""" result = await hass.config_entries.flow.async_init( @@ -126,10 +126,10 @@ async def test_flow_abort_without_channel( assert result["reason"] == "no_channel" +@pytest.mark.usefixtures("current_request_with_host") async def test_flow_abort_without_subscriptions( hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator, - current_request_with_host: None, ) -> None: """Check abort flow if user has no subscriptions.""" result = await hass.config_entries.flow.async_init( @@ -167,10 +167,10 @@ async def test_flow_abort_without_subscriptions( assert result["reason"] == "no_subscriptions" +@pytest.mark.usefixtures("current_request_with_host") async def test_flow_http_error( hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator, - current_request_with_host: None, ) -> None: """Check full flow.""" result = await hass.config_entries.flow.async_init( @@ -229,11 +229,11 @@ async def test_flow_http_error( ), ], ) +@pytest.mark.usefixtures("current_request_with_host") async def test_reauth( hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator, aioclient_mock: AiohttpClientMocker, - current_request_with_host: None, config_entry: MockConfigEntry, fixture: str, abort_reason: str, @@ -312,10 +312,10 @@ async def test_reauth( assert config_entry.data["token"]["refresh_token"] == "mock-refresh-token" +@pytest.mark.usefixtures("current_request_with_host") async def test_flow_exception( hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator, - current_request_with_host: None, ) -> None: """Check full flow.""" result = await hass.config_entries.flow.async_init( From e2f45bfbdc05ba3496e1ac384750d63f9c5d27b9 Mon Sep 17 00:00:00 2001 From: Stefan Agner Date: Tue, 4 Jun 2024 18:23:22 +0200 Subject: [PATCH 0217/1445] Bump Python Matter Server library to 6.1.0 (#118806) --- homeassistant/components/matter/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/matter/manifest.json b/homeassistant/components/matter/manifest.json index d3ad4348950..369657df90c 100644 --- a/homeassistant/components/matter/manifest.json +++ b/homeassistant/components/matter/manifest.json @@ -6,6 +6,6 @@ "dependencies": ["websocket_api"], "documentation": "https://www.home-assistant.io/integrations/matter", "iot_class": "local_push", - "requirements": ["python-matter-server==6.1.0b1"], + "requirements": ["python-matter-server==6.1.0"], "zeroconf": ["_matter._tcp.local.", "_matterc._udp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index 63dd4030074..fe6c5178aaf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2275,7 +2275,7 @@ python-kasa[speedups]==0.6.2.1 # python-lirc==1.2.3 # homeassistant.components.matter -python-matter-server==6.1.0b1 +python-matter-server==6.1.0 # homeassistant.components.xiaomi_miio python-miio==0.5.12 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c3907125942..4e3eed03b02 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1772,7 +1772,7 @@ python-juicenet==1.1.0 python-kasa[speedups]==0.6.2.1 # homeassistant.components.matter -python-matter-server==6.1.0b1 +python-matter-server==6.1.0 # homeassistant.components.xiaomi_miio python-miio==0.5.12 From 6483c469914da2254129847cd407d7a7d8af4122 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 4 Jun 2024 18:26:47 +0200 Subject: [PATCH 0218/1445] Update frontend to 20240604.0 (#118811) --- 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 dd112f5094a..d474e9d2f14 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==20240603.0"] + "requirements": ["home-assistant-frontend==20240604.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 6160db06385..2286189626c 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -32,7 +32,7 @@ habluetooth==3.1.1 hass-nabucasa==0.81.1 hassil==1.7.1 home-assistant-bluetooth==1.12.0 -home-assistant-frontend==20240603.0 +home-assistant-frontend==20240604.0 home-assistant-intents==2024.6.3 httpx==0.27.0 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index fe6c5178aaf..6e4cd5fbf03 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1087,7 +1087,7 @@ hole==0.8.0 holidays==0.49 # homeassistant.components.frontend -home-assistant-frontend==20240603.0 +home-assistant-frontend==20240604.0 # homeassistant.components.conversation home-assistant-intents==2024.6.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4e3eed03b02..b3d792d40c1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -889,7 +889,7 @@ hole==0.8.0 holidays==0.49 # homeassistant.components.frontend -home-assistant-frontend==20240603.0 +home-assistant-frontend==20240604.0 # homeassistant.components.conversation home-assistant-intents==2024.6.3 From 709e32a38abf0d2d452cf7fe8fd4ed4341164752 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Tue, 4 Jun 2024 18:40:18 +0200 Subject: [PATCH 0219/1445] Check if Shelly `entry.runtime_data` is available (#118805) * Check if runtime_data is available * Add tests * Use `is` operator --------- Co-authored-by: Maciej Bieniek <478555+bieniu@users.noreply.github.com> --- .../components/shelly/coordinator.py | 6 +- .../components/shelly/test_device_trigger.py | 90 +++++++++++++++++++ 2 files changed, 94 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/shelly/coordinator.py b/homeassistant/components/shelly/coordinator.py index 9d8416d64d9..cf6e9cc897f 100644 --- a/homeassistant/components/shelly/coordinator.py +++ b/homeassistant/components/shelly/coordinator.py @@ -737,7 +737,8 @@ def get_block_coordinator_by_device_id( entry = hass.config_entries.async_get_entry(config_entry) if ( entry - and entry.state == ConfigEntryState.LOADED + and entry.state is ConfigEntryState.LOADED + and hasattr(entry, "runtime_data") and isinstance(entry.runtime_data, ShellyEntryData) and (coordinator := entry.runtime_data.block) ): @@ -756,7 +757,8 @@ def get_rpc_coordinator_by_device_id( entry = hass.config_entries.async_get_entry(config_entry) if ( entry - and entry.state == ConfigEntryState.LOADED + and entry.state is ConfigEntryState.LOADED + and hasattr(entry, "runtime_data") and isinstance(entry.runtime_data, ShellyEntryData) and (coordinator := entry.runtime_data.rpc) ): diff --git a/tests/components/shelly/test_device_trigger.py b/tests/components/shelly/test_device_trigger.py index 39238f1674a..42ea13aec24 100644 --- a/tests/components/shelly/test_device_trigger.py +++ b/tests/components/shelly/test_device_trigger.py @@ -385,3 +385,93 @@ async def test_validate_trigger_invalid_triggers( ) assert "Invalid (type,subtype): ('single', 'button3')" in caplog.text + + +async def test_rpc_no_runtime_data( + hass: HomeAssistant, + calls: list[ServiceCall], + mock_rpc_device: Mock, + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Test the device trigger for the RPC device when there is no runtime_data in the entry.""" + entry = await init_integration(hass, 2) + monkeypatch.delattr(entry, "runtime_data") + dev_reg = async_get_dev_reg(hass) + device = async_entries_for_config_entry(dev_reg, entry.entry_id)[0] + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + CONF_PLATFORM: "device", + CONF_DOMAIN: DOMAIN, + CONF_DEVICE_ID: device.id, + CONF_TYPE: "single_push", + CONF_SUBTYPE: "button1", + }, + "action": { + "service": "test.automation", + "data_template": {"some": "test_trigger_single_push"}, + }, + }, + ] + }, + ) + message = { + CONF_DEVICE_ID: device.id, + ATTR_CLICK_TYPE: "single_push", + ATTR_CHANNEL: 1, + } + hass.bus.async_fire(EVENT_SHELLY_CLICK, message) + await hass.async_block_till_done() + + assert len(calls) == 1 + assert calls[0].data["some"] == "test_trigger_single_push" + + +async def test_block_no_runtime_data( + hass: HomeAssistant, + calls: list[ServiceCall], + mock_block_device: Mock, + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Test the device trigger for the block device when there is no runtime_data in the entry.""" + entry = await init_integration(hass, 1) + monkeypatch.delattr(entry, "runtime_data") + dev_reg = async_get_dev_reg(hass) + device = async_entries_for_config_entry(dev_reg, entry.entry_id)[0] + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + CONF_PLATFORM: "device", + CONF_DOMAIN: DOMAIN, + CONF_DEVICE_ID: device.id, + CONF_TYPE: "single", + CONF_SUBTYPE: "button1", + }, + "action": { + "service": "test.automation", + "data_template": {"some": "test_trigger_single"}, + }, + }, + ] + }, + ) + message = { + CONF_DEVICE_ID: device.id, + ATTR_CLICK_TYPE: "single", + ATTR_CHANNEL: 1, + } + hass.bus.async_fire(EVENT_SHELLY_CLICK, message) + await hass.async_block_till_done() + + assert len(calls) == 1 + assert calls[0].data["some"] == "test_trigger_single" From 72e4aee155affe8c1b0d9a650227aee5acf7eb43 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 4 Jun 2024 11:48:29 -0500 Subject: [PATCH 0220/1445] Ensure name of task is logged for unhandled loop exceptions (#118822) --- homeassistant/runner.py | 6 ++++-- tests/test_runner.py | 7 ++++--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/homeassistant/runner.py b/homeassistant/runner.py index 523dafdecf3..a1510336302 100644 --- a/homeassistant/runner.py +++ b/homeassistant/runner.py @@ -137,16 +137,18 @@ def _async_loop_exception_handler(_: Any, context: dict[str, Any]) -> None: if source_traceback := context.get("source_traceback"): stack_summary = "".join(traceback.format_list(source_traceback)) logger.error( - "Error doing job: %s: %s", + "Error doing job: %s (%s): %s", context["message"], + context.get("task"), stack_summary, **kwargs, # type: ignore[arg-type] ) return logger.error( - "Error doing job: %s", + "Error doing job: %s (%s)", context["message"], + context.get("task"), **kwargs, # type: ignore[arg-type] ) diff --git a/tests/test_runner.py b/tests/test_runner.py index 79768aaf7cf..a4bec12bc0d 100644 --- a/tests/test_runner.py +++ b/tests/test_runner.py @@ -115,11 +115,11 @@ def test_run_does_not_block_forever_with_shielded_task( tasks.append(asyncio.ensure_future(asyncio.shield(async_shielded()))) tasks.append(asyncio.ensure_future(asyncio.sleep(2))) tasks.append(asyncio.ensure_future(async_raise())) - await asyncio.sleep(0.1) + await asyncio.sleep(0) return 0 with ( - patch.object(runner, "TASK_CANCELATION_TIMEOUT", 1), + patch.object(runner, "TASK_CANCELATION_TIMEOUT", 0.1), patch("homeassistant.bootstrap.async_setup_hass", return_value=hass), patch("threading._shutdown"), patch("homeassistant.core.HomeAssistant.async_run", _async_create_tasks), @@ -145,7 +145,7 @@ async def test_unhandled_exception_traceback( try: hass.loop.set_debug(True) - task = asyncio.create_task(_unhandled_exception()) + task = asyncio.create_task(_unhandled_exception(), name="name_of_task") await raised.wait() # Delete it without checking result to trigger unhandled exception del task @@ -155,6 +155,7 @@ async def test_unhandled_exception_traceback( assert "Task exception was never retrieved" in caplog.text assert "This is unhandled" in caplog.text assert "_unhandled_exception" in caplog.text + assert "name_of_task" in caplog.text def test_enable_posix_spawn() -> None: From c8e72985565cd8690322b4ef7ffcbc8e26ae9964 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 4 Jun 2024 13:21:56 -0500 Subject: [PATCH 0221/1445] Remove myself as codeowner for unifiprotect (#118824) --- CODEOWNERS | 2 -- homeassistant/components/unifiprotect/manifest.json | 3 +-- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 90d482ce041..ba7484d34d1 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1489,8 +1489,6 @@ build.json @home-assistant/supervisor /tests/components/unifi/ @Kane610 /homeassistant/components/unifi_direct/ @tofuSCHNITZEL /homeassistant/components/unifiled/ @florisvdk -/homeassistant/components/unifiprotect/ @bdraco -/tests/components/unifiprotect/ @bdraco /homeassistant/components/upb/ @gwww /tests/components/upb/ @gwww /homeassistant/components/upc_connect/ @pvizeli @fabaff diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index 5570d088a7d..a09db1cf01a 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -1,7 +1,7 @@ { "domain": "unifiprotect", "name": "UniFi Protect", - "codeowners": ["@bdraco"], + "codeowners": [], "config_flow": true, "dependencies": ["http", "repairs"], "dhcp": [ @@ -40,7 +40,6 @@ "integration_type": "hub", "iot_class": "local_push", "loggers": ["pyunifiprotect", "unifi_discovery"], - "quality_scale": "platinum", "requirements": ["pyunifiprotect==5.1.2", "unifi-discovery==1.1.8"], "ssdp": [ { From c83aba0fd1e69e539bdb8bbcc99bdfcd95e57e0a Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Tue, 4 Jun 2024 20:47:06 +0200 Subject: [PATCH 0222/1445] Initialize the Sentry SDK within an import executor job to not block event loop (#118830) --- homeassistant/components/sentry/__init__.py | 46 +++++++++++++-------- 1 file changed, 28 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/sentry/__init__.py b/homeassistant/components/sentry/__init__.py index dcbcc59a749..8c042621db6 100644 --- a/homeassistant/components/sentry/__init__.py +++ b/homeassistant/components/sentry/__init__.py @@ -21,6 +21,7 @@ from homeassistant.helpers import config_validation as cv, entity_platform, inst from homeassistant.helpers.event import async_call_later from homeassistant.helpers.system_info import async_get_system_info from homeassistant.loader import Integration, async_get_custom_components +from homeassistant.setup import SetupPhases, async_pause_setup from .const import ( CONF_DSN, @@ -41,7 +42,6 @@ from .const import ( CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) - LOGGER_INFO_REGEX = re.compile(r"^(\w+)\.?(\w+)?\.?(\w+)?\.?(\w+)?(?:\..*)?$") @@ -81,23 +81,33 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ), } - sentry_sdk.init( - dsn=entry.data[CONF_DSN], - environment=entry.options.get(CONF_ENVIRONMENT), - integrations=[sentry_logging, AioHttpIntegration(), SqlalchemyIntegration()], - release=current_version, - before_send=lambda event, hint: process_before_send( - hass, - entry.options, - channel, - huuid, - system_info, - custom_components, - event, - hint, - ), - **tracing, - ) + with async_pause_setup(hass, SetupPhases.WAIT_IMPORT_PACKAGES): + # sentry_sdk.init imports modules based on the selected integrations + def _init_sdk(): + """Initialize the Sentry SDK.""" + sentry_sdk.init( + dsn=entry.data[CONF_DSN], + environment=entry.options.get(CONF_ENVIRONMENT), + integrations=[ + sentry_logging, + AioHttpIntegration(), + SqlalchemyIntegration(), + ], + release=current_version, + before_send=lambda event, hint: process_before_send( + hass, + entry.options, + channel, + huuid, + system_info, + custom_components, + event, + hint, + ), + **tracing, + ) + + await hass.async_add_import_executor_job(_init_sdk) async def update_system_info(now): nonlocal system_info From 5fca2c09c56b4419e02c969fe4536637340f7d87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Huryn?= Date: Tue, 4 Jun 2024 20:49:00 +0200 Subject: [PATCH 0223/1445] blebox: update codeowners (#118817) --- CODEOWNERS | 4 ++-- homeassistant/components/blebox/manifest.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index ba7484d34d1..d9abbd9b851 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -184,8 +184,8 @@ build.json @home-assistant/supervisor /homeassistant/components/binary_sensor/ @home-assistant/core /tests/components/binary_sensor/ @home-assistant/core /homeassistant/components/bizkaibus/ @UgaitzEtxebarria -/homeassistant/components/blebox/ @bbx-a @riokuu @swistakm -/tests/components/blebox/ @bbx-a @riokuu @swistakm +/homeassistant/components/blebox/ @bbx-a @swistakm +/tests/components/blebox/ @bbx-a @swistakm /homeassistant/components/blink/ @fronzbot @mkmer /tests/components/blink/ @fronzbot @mkmer /homeassistant/components/blue_current/ @Floris272 @gleeuwen diff --git a/homeassistant/components/blebox/manifest.json b/homeassistant/components/blebox/manifest.json index 566935c405f..4b0a6403f67 100644 --- a/homeassistant/components/blebox/manifest.json +++ b/homeassistant/components/blebox/manifest.json @@ -1,7 +1,7 @@ { "domain": "blebox", "name": "BleBox devices", - "codeowners": ["@bbx-a", "@riokuu", "@swistakm"], + "codeowners": ["@bbx-a", "@swistakm"], "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/blebox", "iot_class": "local_polling", From 956623d9642e332178d8f5602ad13be496f6bc03 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Tue, 4 Jun 2024 20:51:34 +0200 Subject: [PATCH 0224/1445] Fix device name not set on all incomfort platforms (#118827) * Prelimenary tests for incomfort integration * Use snapshot_platform * Use helper * Ensure the device name is set in device info * Move snapshot tests to platform test modules * Move unused snapshot file * Naming and docstr * update snapshots * cleanup snapshots * Add water heater tests --- .coveragerc | 4 - .../components/incomfort/binary_sensor.py | 2 + homeassistant/components/incomfort/sensor.py | 2 + tests/components/incomfort/conftest.py | 65 +++++++- .../snapshots/test_binary_sensor.ambr | 95 +++++++++++ .../incomfort/snapshots/test_climate.ambr | 66 ++++++++ .../incomfort/snapshots/test_sensor.ambr | 147 ++++++++++++++++++ .../snapshots/test_water_heater.ambr | 61 ++++++++ .../incomfort/test_binary_sensor.py | 25 +++ tests/components/incomfort/test_climate.py | 25 +++ .../components/incomfort/test_config_flow.py | 10 +- tests/components/incomfort/test_init.py | 23 +++ tests/components/incomfort/test_sensor.py | 25 +++ .../components/incomfort/test_water_heater.py | 25 +++ 14 files changed, 559 insertions(+), 16 deletions(-) create mode 100644 tests/components/incomfort/snapshots/test_binary_sensor.ambr create mode 100644 tests/components/incomfort/snapshots/test_climate.ambr create mode 100644 tests/components/incomfort/snapshots/test_sensor.ambr create mode 100644 tests/components/incomfort/snapshots/test_water_heater.ambr create mode 100644 tests/components/incomfort/test_binary_sensor.py create mode 100644 tests/components/incomfort/test_climate.py create mode 100644 tests/components/incomfort/test_init.py create mode 100644 tests/components/incomfort/test_sensor.py create mode 100644 tests/components/incomfort/test_water_heater.py diff --git a/.coveragerc b/.coveragerc index 034598d2044..071fdade647 100644 --- a/.coveragerc +++ b/.coveragerc @@ -591,11 +591,7 @@ omit = homeassistant/components/iglo/light.py homeassistant/components/ihc/* homeassistant/components/incomfort/__init__.py - homeassistant/components/incomfort/binary_sensor.py homeassistant/components/incomfort/climate.py - homeassistant/components/incomfort/errors.py - homeassistant/components/incomfort/models.py - homeassistant/components/incomfort/sensor.py homeassistant/components/incomfort/water_heater.py homeassistant/components/insteon/binary_sensor.py homeassistant/components/insteon/climate.py diff --git a/homeassistant/components/incomfort/binary_sensor.py b/homeassistant/components/incomfort/binary_sensor.py index a64d028ffc1..f60ce2f4b59 100644 --- a/homeassistant/components/incomfort/binary_sensor.py +++ b/homeassistant/components/incomfort/binary_sensor.py @@ -43,6 +43,8 @@ class IncomfortFailed(IncomfortEntity, BinarySensorEntity): self._attr_unique_id = f"{heater.serial_no}_failed" self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, heater.serial_no)}, + manufacturer="Intergas", + name="Boiler", ) @property diff --git a/homeassistant/components/incomfort/sensor.py b/homeassistant/components/incomfort/sensor.py index e12b0a3d199..a31488603b3 100644 --- a/homeassistant/components/incomfort/sensor.py +++ b/homeassistant/components/incomfort/sensor.py @@ -96,6 +96,8 @@ class IncomfortSensor(IncomfortEntity, SensorEntity): self._attr_unique_id = f"{heater.serial_no}_{slugify(description.name)}" self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, heater.serial_no)}, + manufacturer="Intergas", + name="Boiler", ) @property diff --git a/tests/components/incomfort/conftest.py b/tests/components/incomfort/conftest.py index 5f5a2c9be16..34c38995895 100644 --- a/tests/components/incomfort/conftest.py +++ b/tests/components/incomfort/conftest.py @@ -6,8 +6,18 @@ from unittest.mock import AsyncMock, MagicMock, patch import pytest +from homeassistant.components.incomfort import DOMAIN +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from tests.common import MockConfigEntry + +MOCK_CONFIG = { + "host": "192.168.1.12", + "username": "admin", + "password": "verysecret", +} + @pytest.fixture def mock_setup_entry() -> Generator[AsyncMock, None, None]: @@ -19,6 +29,22 @@ def mock_setup_entry() -> Generator[AsyncMock, None, None]: yield mock_setup_entry +@pytest.fixture +def mock_entry_data() -> dict[str, Any]: + """Mock config entry data for fixture.""" + return MOCK_CONFIG + + +@pytest.fixture +def mock_config_entry( + hass: HomeAssistant, mock_entry_data: dict[str, Any] +) -> ConfigEntry: + """Mock a config entry setup for incomfort integration.""" + entry = MockConfigEntry(domain=DOMAIN, data=mock_entry_data) + entry.add_to_hass(hass) + return entry + + @pytest.fixture def mock_heater_status() -> dict[str, Any]: """Mock heater status.""" @@ -33,7 +59,7 @@ def mock_heater_status() -> dict[str, Any]: "heater_temp": 35.34, "tap_temp": 30.21, "pressure": 1.86, - "serial_no": "2404c08648", + "serial_no": "c0ffeec0ffee", "nodenr": 249, "rf_message_rssi": 30, "rfstatus_cntr": 0, @@ -62,14 +88,25 @@ def mock_incomfort( room_temp: float setpoint: float status: dict[str, Any] + set_override: MagicMock def __init__(self) -> None: """Initialize mocked room.""" - self.override = mock_room_status["override"] self.room_no = 1 - self.room_temp = mock_room_status["room_temp"] - self.setpoint = mock_room_status["setpoint"] self.status = mock_room_status + self.set_override = MagicMock() + + @property + def override(self) -> str: + return mock_room_status["override"] + + @property + def room_temp(self) -> float: + return mock_room_status["room_temp"] + + @property + def setpoint(self) -> float: + return mock_room_status["setpoint"] class MockHeater: """Mocked InComfort heater class.""" @@ -77,6 +114,20 @@ def mock_incomfort( serial_no: str status: dict[str, Any] rooms: list[MockRoom] + is_failed: bool + is_pumping: bool + display_code: int + display_text: str | None + fault_code: int | None + is_burning: bool + is_tapping: bool + heater_temp: float + tap_temp: float + pressure: float + serial_no: str + nodenr: int + rf_message_rssi: int + rfstatus_cntr: int def __init__(self) -> None: """Initialize mocked heater.""" @@ -84,11 +135,15 @@ def mock_incomfort( async def update(self) -> None: self.status = mock_heater_status - self.rooms = [MockRoom] + for key, value in mock_heater_status.items(): + setattr(self, key, value) + self.rooms = [MockRoom()] with patch( "homeassistant.components.incomfort.models.InComfortGateway", MagicMock() ) as patch_gateway: patch_gateway().heaters = AsyncMock() patch_gateway().heaters.return_value = [MockHeater()] + patch_gateway().mock_heater_status = mock_heater_status + patch_gateway().mock_room_status = mock_room_status yield patch_gateway diff --git a/tests/components/incomfort/snapshots/test_binary_sensor.ambr b/tests/components/incomfort/snapshots/test_binary_sensor.ambr new file mode 100644 index 00000000000..0316f37502d --- /dev/null +++ b/tests/components/incomfort/snapshots/test_binary_sensor.ambr @@ -0,0 +1,95 @@ +# serializer version: 1 +# name: test_setup_platform[binary_sensor.boiler_fault-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.boiler_fault', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Fault', + 'platform': 'incomfort', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'c0ffeec0ffee_failed', + 'unit_of_measurement': None, + }) +# --- +# name: test_setup_platform[binary_sensor.boiler_fault-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'fault_code': None, + 'friendly_name': 'Boiler Fault', + }), + 'context': , + 'entity_id': 'binary_sensor.boiler_fault', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_setup_platforms[binary_sensor.boiler_fault-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.boiler_fault', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Fault', + 'platform': 'incomfort', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'c0ffeec0ffee_failed', + 'unit_of_measurement': None, + }) +# --- +# name: test_setup_platforms[binary_sensor.boiler_fault-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'fault_code': None, + 'friendly_name': 'Boiler Fault', + }), + 'context': , + 'entity_id': 'binary_sensor.boiler_fault', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- diff --git a/tests/components/incomfort/snapshots/test_climate.ambr b/tests/components/incomfort/snapshots/test_climate.ambr new file mode 100644 index 00000000000..b9a86d26139 --- /dev/null +++ b/tests/components/incomfort/snapshots/test_climate.ambr @@ -0,0 +1,66 @@ +# serializer version: 1 +# name: test_setup_platform[climate.thermostat_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'hvac_modes': list([ + , + ]), + 'max_temp': 30.0, + 'min_temp': 5.0, + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'climate', + 'entity_category': None, + 'entity_id': 'climate.thermostat_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'incomfort', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': 'c0ffeec0ffee_1', + 'unit_of_measurement': None, + }) +# --- +# name: test_setup_platform[climate.thermostat_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'current_temperature': 21.4, + 'friendly_name': 'Thermostat 1', + 'hvac_modes': list([ + , + ]), + 'max_temp': 30.0, + 'min_temp': 5.0, + 'status': dict({ + 'override': 18.0, + 'room_temp': 21.42, + 'setpoint': 18.0, + }), + 'supported_features': , + 'temperature': 18.0, + }), + 'context': , + 'entity_id': 'climate.thermostat_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'heat', + }) +# --- diff --git a/tests/components/incomfort/snapshots/test_sensor.ambr b/tests/components/incomfort/snapshots/test_sensor.ambr new file mode 100644 index 00000000000..831be411b46 --- /dev/null +++ b/tests/components/incomfort/snapshots/test_sensor.ambr @@ -0,0 +1,147 @@ +# serializer version: 1 +# name: test_setup_platform[sensor.boiler_cv_pressure-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.boiler_cv_pressure', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'CV Pressure', + 'platform': 'incomfort', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'c0ffeec0ffee_cv_pressure', + 'unit_of_measurement': , + }) +# --- +# name: test_setup_platform[sensor.boiler_cv_pressure-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'pressure', + 'friendly_name': 'Boiler CV Pressure', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.boiler_cv_pressure', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1.86', + }) +# --- +# name: test_setup_platform[sensor.boiler_cv_temp-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.boiler_cv_temp', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'CV Temp', + 'platform': 'incomfort', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'c0ffeec0ffee_cv_temp', + 'unit_of_measurement': , + }) +# --- +# name: test_setup_platform[sensor.boiler_cv_temp-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Boiler CV Temp', + 'is_pumping': False, + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.boiler_cv_temp', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '35.34', + }) +# --- +# name: test_setup_platform[sensor.boiler_tap_temp-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.boiler_tap_temp', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Tap Temp', + 'platform': 'incomfort', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'c0ffeec0ffee_tap_temp', + 'unit_of_measurement': , + }) +# --- +# name: test_setup_platform[sensor.boiler_tap_temp-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Boiler Tap Temp', + 'is_tapping': False, + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.boiler_tap_temp', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '30.21', + }) +# --- diff --git a/tests/components/incomfort/snapshots/test_water_heater.ambr b/tests/components/incomfort/snapshots/test_water_heater.ambr new file mode 100644 index 00000000000..7e277da99f1 --- /dev/null +++ b/tests/components/incomfort/snapshots/test_water_heater.ambr @@ -0,0 +1,61 @@ +# serializer version: 1 +# name: test_setup_platform[water_heater.boiler-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max_temp': 80.0, + 'min_temp': 30.0, + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'water_heater', + 'entity_category': None, + 'entity_id': 'water_heater.boiler', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:thermometer-lines', + 'original_name': None, + 'platform': 'incomfort', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'c0ffeec0ffee', + 'unit_of_measurement': None, + }) +# --- +# name: test_setup_platform[water_heater.boiler-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'current_temperature': 35.3, + 'display_code': 126, + 'display_text': 'standby', + 'friendly_name': 'Boiler', + 'icon': 'mdi:thermometer-lines', + 'is_burning': False, + 'max_temp': 80.0, + 'min_temp': 30.0, + 'supported_features': , + 'target_temp_high': None, + 'target_temp_low': None, + 'temperature': None, + }), + 'context': , + 'entity_id': 'water_heater.boiler', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'standby', + }) +# --- diff --git a/tests/components/incomfort/test_binary_sensor.py b/tests/components/incomfort/test_binary_sensor.py new file mode 100644 index 00000000000..3a50a08d9d1 --- /dev/null +++ b/tests/components/incomfort/test_binary_sensor.py @@ -0,0 +1,25 @@ +"""Binary sensor tests for Intergas InComfort integration.""" + +from unittest.mock import MagicMock, patch + +from syrupy import SnapshotAssertion + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from tests.common import snapshot_platform + + +@patch("homeassistant.components.incomfort.PLATFORMS", [Platform.BINARY_SENSOR]) +async def test_setup_platform( + hass: HomeAssistant, + mock_incomfort: MagicMock, + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, + mock_config_entry: ConfigEntry, +) -> None: + """Test the incomfort entities are set up correctly.""" + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) diff --git a/tests/components/incomfort/test_climate.py b/tests/components/incomfort/test_climate.py new file mode 100644 index 00000000000..d5f7397aaaf --- /dev/null +++ b/tests/components/incomfort/test_climate.py @@ -0,0 +1,25 @@ +"""Climate sensor tests for Intergas InComfort integration.""" + +from unittest.mock import MagicMock, patch + +from syrupy import SnapshotAssertion + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from tests.common import snapshot_platform + + +@patch("homeassistant.components.incomfort.PLATFORMS", [Platform.CLIMATE]) +async def test_setup_platform( + hass: HomeAssistant, + mock_incomfort: MagicMock, + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, + mock_config_entry: ConfigEntry, +) -> None: + """Test the incomfort entities are set up correctly.""" + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) diff --git a/tests/components/incomfort/test_config_flow.py b/tests/components/incomfort/test_config_flow.py index 08f03d96bdb..7a942dab817 100644 --- a/tests/components/incomfort/test_config_flow.py +++ b/tests/components/incomfort/test_config_flow.py @@ -12,13 +12,9 @@ from homeassistant.const import CONF_HOST, CONF_PASSWORD from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType -from tests.common import MockConfigEntry +from .conftest import MOCK_CONFIG -MOCK_CONFIG = { - "host": "192.168.1.12", - "username": "admin", - "password": "verysecret", -} +from tests.common import MockConfigEntry async def test_form( @@ -144,7 +140,7 @@ async def test_form_validation( DOMAIN, context={"source": SOURCE_USER} ) - # Simulate issue and retry + # Simulate an issue mock_incomfort().heaters.side_effect = exc result = await hass.config_entries.flow.async_configure( result["flow_id"], MOCK_CONFIG diff --git a/tests/components/incomfort/test_init.py b/tests/components/incomfort/test_init.py new file mode 100644 index 00000000000..7c0a8b395a8 --- /dev/null +++ b/tests/components/incomfort/test_init.py @@ -0,0 +1,23 @@ +"""Tests for Intergas InComfort integration.""" + +from unittest.mock import MagicMock, patch + +from syrupy import SnapshotAssertion + +from homeassistant.config_entries import ConfigEntry, ConfigEntryState +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + + +@patch("homeassistant.components.incomfort.PLATFORMS", [Platform.SENSOR]) +async def test_setup_platforms( + hass: HomeAssistant, + mock_incomfort: MagicMock, + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, + mock_config_entry: ConfigEntry, +) -> None: + """Test the incomfort integration is set up correctly.""" + await hass.config_entries.async_setup(mock_config_entry.entry_id) + assert mock_config_entry.state is ConfigEntryState.LOADED diff --git a/tests/components/incomfort/test_sensor.py b/tests/components/incomfort/test_sensor.py new file mode 100644 index 00000000000..d01fd9b403e --- /dev/null +++ b/tests/components/incomfort/test_sensor.py @@ -0,0 +1,25 @@ +"""Sensor tests for Intergas InComfort integration.""" + +from unittest.mock import MagicMock, patch + +from syrupy import SnapshotAssertion + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from tests.common import snapshot_platform + + +@patch("homeassistant.components.incomfort.PLATFORMS", [Platform.SENSOR]) +async def test_setup_platform( + hass: HomeAssistant, + mock_incomfort: MagicMock, + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, + mock_config_entry: ConfigEntry, +) -> None: + """Test the incomfort entities are set up correctly.""" + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) diff --git a/tests/components/incomfort/test_water_heater.py b/tests/components/incomfort/test_water_heater.py new file mode 100644 index 00000000000..5b7aebc50a8 --- /dev/null +++ b/tests/components/incomfort/test_water_heater.py @@ -0,0 +1,25 @@ +"""Water heater tests for Intergas InComfort integration.""" + +from unittest.mock import MagicMock, patch + +from syrupy import SnapshotAssertion + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from tests.common import snapshot_platform + + +@patch("homeassistant.components.incomfort.PLATFORMS", [Platform.WATER_HEATER]) +async def test_setup_platform( + hass: HomeAssistant, + mock_incomfort: MagicMock, + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, + mock_config_entry: ConfigEntry, +) -> None: + """Test the incomfort entities are set up correctly.""" + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) From f18ddb628c3574bc82e21563d9ba901bd75bc8b5 Mon Sep 17 00:00:00 2001 From: Gerben Jongerius Date: Tue, 4 Jun 2024 20:52:37 +0200 Subject: [PATCH 0225/1445] Bump youless dependency version to 2.1.0 (#118820) --- homeassistant/components/youless/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/youless/manifest.json b/homeassistant/components/youless/manifest.json index 6342d3fb76a..9a81de38388 100644 --- a/homeassistant/components/youless/manifest.json +++ b/homeassistant/components/youless/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/youless", "iot_class": "local_polling", "loggers": ["youless_api"], - "requirements": ["youless-api==1.1.1"] + "requirements": ["youless-api==2.1.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 6e4cd5fbf03..9e47da0b200 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2942,7 +2942,7 @@ yeelightsunflower==0.0.10 yolink-api==0.4.4 # homeassistant.components.youless -youless-api==1.1.1 +youless-api==2.1.0 # homeassistant.components.youtube youtubeaio==1.1.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b3d792d40c1..be53e018379 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2295,7 +2295,7 @@ yeelight==0.7.14 yolink-api==0.4.4 # homeassistant.components.youless -youless-api==1.1.1 +youless-api==2.1.0 # homeassistant.components.youtube youtubeaio==1.1.5 From 98455cbd932c714e8fa0e554fc6ae4df1cc14995 Mon Sep 17 00:00:00 2001 From: Tsvi Mostovicz Date: Tue, 4 Jun 2024 21:55:38 +0300 Subject: [PATCH 0226/1445] Fix updating options in Jewish Calendar (#118643) --- .../components/jewish_calendar/__init__.py | 10 ++++++++-- .../components/jewish_calendar/config_flow.py | 15 ++++++++++++++- .../jewish_calendar/test_config_flow.py | 19 ++++++++++--------- tests/components/jewish_calendar/test_init.py | 5 ++++- .../components/jewish_calendar/test_sensor.py | 2 ++ 5 files changed, 38 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/jewish_calendar/__init__.py b/homeassistant/components/jewish_calendar/__init__.py index d4edcadf6f7..8383f9181fc 100644 --- a/homeassistant/components/jewish_calendar/__init__.py +++ b/homeassistant/components/jewish_calendar/__init__.py @@ -119,10 +119,10 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b """Set up a configuration entry for Jewish calendar.""" language = config_entry.data.get(CONF_LANGUAGE, DEFAULT_LANGUAGE) diaspora = config_entry.data.get(CONF_DIASPORA, DEFAULT_DIASPORA) - candle_lighting_offset = config_entry.data.get( + candle_lighting_offset = config_entry.options.get( CONF_CANDLE_LIGHT_MINUTES, DEFAULT_CANDLE_LIGHT ) - havdalah_offset = config_entry.data.get( + havdalah_offset = config_entry.options.get( CONF_HAVDALAH_OFFSET_MINUTES, DEFAULT_HAVDALAH_OFFSET_MINUTES ) @@ -154,6 +154,12 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b async_update_unique_ids(ent_reg, config_entry.entry_id, old_prefix) await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) + + async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: + # Trigger update of states for all platforms + await hass.config_entries.async_reload(config_entry.entry_id) + + config_entry.async_on_unload(config_entry.add_update_listener(update_listener)) return True diff --git a/homeassistant/components/jewish_calendar/config_flow.py b/homeassistant/components/jewish_calendar/config_flow.py index 626dc168db8..8f04d73915f 100644 --- a/homeassistant/components/jewish_calendar/config_flow.py +++ b/homeassistant/components/jewish_calendar/config_flow.py @@ -100,10 +100,23 @@ class JewishCalendarConfigFlow(ConfigFlow, domain=DOMAIN): ) -> ConfigFlowResult: """Handle the initial step.""" if user_input is not None: + _options = {} + if CONF_CANDLE_LIGHT_MINUTES in user_input: + _options[CONF_CANDLE_LIGHT_MINUTES] = user_input[ + CONF_CANDLE_LIGHT_MINUTES + ] + del user_input[CONF_CANDLE_LIGHT_MINUTES] + if CONF_HAVDALAH_OFFSET_MINUTES in user_input: + _options[CONF_HAVDALAH_OFFSET_MINUTES] = user_input[ + CONF_HAVDALAH_OFFSET_MINUTES + ] + del user_input[CONF_HAVDALAH_OFFSET_MINUTES] if CONF_LOCATION in user_input: user_input[CONF_LATITUDE] = user_input[CONF_LOCATION][CONF_LATITUDE] user_input[CONF_LONGITUDE] = user_input[CONF_LOCATION][CONF_LONGITUDE] - return self.async_create_entry(title=DEFAULT_NAME, data=user_input) + return self.async_create_entry( + title=DEFAULT_NAME, data=user_input, options=_options + ) return self.async_show_form( step_id="user", diff --git a/tests/components/jewish_calendar/test_config_flow.py b/tests/components/jewish_calendar/test_config_flow.py index 55c2f39b7eb..3189571a5a7 100644 --- a/tests/components/jewish_calendar/test_config_flow.py +++ b/tests/components/jewish_calendar/test_config_flow.py @@ -9,9 +9,7 @@ from homeassistant.components.jewish_calendar.const import ( CONF_CANDLE_LIGHT_MINUTES, CONF_DIASPORA, CONF_HAVDALAH_OFFSET_MINUTES, - DEFAULT_CANDLE_LIGHT, DEFAULT_DIASPORA, - DEFAULT_HAVDALAH_OFFSET_MINUTES, DEFAULT_LANGUAGE, DOMAIN, ) @@ -73,10 +71,8 @@ async def test_import_no_options(hass: HomeAssistant, language, diaspora) -> Non entries = hass.config_entries.async_entries(DOMAIN) assert len(entries) == 1 - assert entries[0].data == conf[DOMAIN] | { - CONF_CANDLE_LIGHT_MINUTES: DEFAULT_CANDLE_LIGHT, - CONF_HAVDALAH_OFFSET_MINUTES: DEFAULT_HAVDALAH_OFFSET_MINUTES, - } + for entry_key, entry_val in entries[0].data.items(): + assert entry_val == conf[DOMAIN][entry_key] async def test_import_with_options(hass: HomeAssistant) -> None: @@ -99,7 +95,10 @@ async def test_import_with_options(hass: HomeAssistant) -> None: entries = hass.config_entries.async_entries(DOMAIN) assert len(entries) == 1 - assert entries[0].data == conf[DOMAIN] + for entry_key, entry_val in entries[0].data.items(): + assert entry_val == conf[DOMAIN][entry_key] + for entry_key, entry_val in entries[0].options.items(): + assert entry_val == conf[DOMAIN][entry_key] async def test_single_instance_allowed( @@ -135,5 +134,7 @@ async def test_options(hass: HomeAssistant, mock_config_entry: MockConfigEntry) ) assert result["type"] is FlowResultType.CREATE_ENTRY - assert result["data"][CONF_CANDLE_LIGHT_MINUTES] == 25 - assert result["data"][CONF_HAVDALAH_OFFSET_MINUTES] == 34 + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + assert entries[0].options[CONF_CANDLE_LIGHT_MINUTES] == 25 + assert entries[0].options[CONF_HAVDALAH_OFFSET_MINUTES] == 34 diff --git a/tests/components/jewish_calendar/test_init.py b/tests/components/jewish_calendar/test_init.py index 49dad98fa89..f052d4e7f46 100644 --- a/tests/components/jewish_calendar/test_init.py +++ b/tests/components/jewish_calendar/test_init.py @@ -58,7 +58,10 @@ async def test_import_unique_id_migration(hass: HomeAssistant) -> None: entries = hass.config_entries.async_entries(DOMAIN) assert len(entries) == 1 - assert entries[0].data == yaml_conf[DOMAIN] + for entry_key, entry_val in entries[0].data.items(): + assert entry_val == yaml_conf[DOMAIN][entry_key] + for entry_key, entry_val in entries[0].options.items(): + assert entry_val == yaml_conf[DOMAIN][entry_key] # Assert that the unique_id was updated new_unique_id = ent_reg.async_get(sample_entity.entity_id).unique_id diff --git a/tests/components/jewish_calendar/test_sensor.py b/tests/components/jewish_calendar/test_sensor.py index 729eca78467..965e461083b 100644 --- a/tests/components/jewish_calendar/test_sensor.py +++ b/tests/components/jewish_calendar/test_sensor.py @@ -519,6 +519,8 @@ async def test_shabbat_times_sensor( data={ CONF_LANGUAGE: language, CONF_DIASPORA: diaspora, + }, + options={ CONF_CANDLE_LIGHT_MINUTES: candle_lighting, CONF_HAVDALAH_OFFSET_MINUTES: havdalah, }, From 513262c0ff593ca975d84d15a428d87af471166a Mon Sep 17 00:00:00 2001 From: arturyak <109509698+arturyak@users.noreply.github.com> Date: Tue, 4 Jun 2024 21:58:58 +0300 Subject: [PATCH 0227/1445] Add missing FAN_ONLY mode to ccm15 (#118804) --- homeassistant/components/ccm15/climate.py | 1 + tests/components/ccm15/snapshots/test_climate.ambr | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/homeassistant/components/ccm15/climate.py b/homeassistant/components/ccm15/climate.py index b4038fbbf43..a6e5d2cab61 100644 --- a/homeassistant/components/ccm15/climate.py +++ b/homeassistant/components/ccm15/climate.py @@ -57,6 +57,7 @@ class CCM15Climate(CoordinatorEntity[CCM15Coordinator], ClimateEntity): HVACMode.HEAT, HVACMode.COOL, HVACMode.DRY, + HVACMode.FAN_ONLY, HVACMode.AUTO, ] _attr_fan_modes = [FAN_AUTO, FAN_LOW, FAN_MEDIUM, FAN_HIGH] diff --git a/tests/components/ccm15/snapshots/test_climate.ambr b/tests/components/ccm15/snapshots/test_climate.ambr index 10423919187..27dcbcb3405 100644 --- a/tests/components/ccm15/snapshots/test_climate.ambr +++ b/tests/components/ccm15/snapshots/test_climate.ambr @@ -16,6 +16,7 @@ , , , + , , ]), 'max_temp': 35, @@ -70,6 +71,7 @@ , , , + , , ]), 'max_temp': 35, @@ -125,6 +127,7 @@ , , , + , , ]), 'max_temp': 35, @@ -164,6 +167,7 @@ , , , + , , ]), 'max_temp': 35, @@ -202,6 +206,7 @@ , , , + , , ]), 'max_temp': 35, @@ -256,6 +261,7 @@ , , , + , , ]), 'max_temp': 35, @@ -308,6 +314,7 @@ , , , + , , ]), 'max_temp': 35, @@ -342,6 +349,7 @@ , , , + , , ]), 'max_temp': 35, From c2e245f9d403de3387f24b3b3219491548aae980 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Tue, 4 Jun 2024 21:00:50 +0200 Subject: [PATCH 0228/1445] Use fixtures in UniFi update tests (#118818) --- tests/components/unifi/test_update.py | 62 ++++++++++----------------- 1 file changed, 22 insertions(+), 40 deletions(-) diff --git a/tests/components/unifi/test_update.py b/tests/components/unifi/test_update.py index 4094c544431..5f9039aa48e 100644 --- a/tests/components/unifi/test_update.py +++ b/tests/components/unifi/test_update.py @@ -3,6 +3,7 @@ from copy import deepcopy from aiounifi.models.message import MessageKey +import pytest from yarl import URL from homeassistant.components.unifi.const import CONF_SITE_ID @@ -26,8 +27,6 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant -from .test_hub import SITE, setup_unifi_integration - from tests.test_util.aiohttp import AiohttpClientMocker DEVICE_1 = { @@ -60,28 +59,14 @@ DEVICE_2 = { } -async def test_no_entities( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker -) -> None: - """Test the update_clients function when no clients are found.""" - await setup_unifi_integration(hass, aioclient_mock) - - assert len(hass.states.async_entity_ids(UPDATE_DOMAIN)) == 0 - - -async def test_device_updates( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, mock_unifi_websocket -) -> None: +@pytest.mark.parametrize("device_payload", [[DEVICE_1, DEVICE_2]]) +@pytest.mark.usefixtures("config_entry_setup") +async def test_device_updates(hass: HomeAssistant, mock_unifi_websocket) -> None: """Test the update_items function with some devices.""" - device_1 = deepcopy(DEVICE_1) - await setup_unifi_integration( - hass, - aioclient_mock, - devices_response=[device_1, DEVICE_2], - ) - assert len(hass.states.async_entity_ids(UPDATE_DOMAIN)) == 2 + # Device with new firmware available + device_1_state = hass.states.get("update.device_1") assert device_1_state.state == STATE_ON assert device_1_state.attributes[ATTR_INSTALLED_VERSION] == "4.0.42.10433" @@ -93,6 +78,8 @@ async def test_device_updates( == UpdateEntityFeature.PROGRESS | UpdateEntityFeature.INSTALL ) + # Device without new firmware available + device_2_state = hass.states.get("update.device_2") assert device_2_state.state == STATE_OFF assert device_2_state.attributes[ATTR_INSTALLED_VERSION] == "4.0.42.10433" @@ -106,6 +93,7 @@ async def test_device_updates( # Simulate start of update + device_1 = deepcopy(DEVICE_1) device_1["state"] = 4 mock_unifi_websocket(message=MessageKey.DEVICE, data=device_1) await hass.async_block_till_done() @@ -132,17 +120,14 @@ async def test_device_updates( assert device_1_state.attributes[ATTR_IN_PROGRESS] is False -async def test_not_admin( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker -) -> None: +@pytest.mark.parametrize("device_payload", [[DEVICE_1]]) +@pytest.mark.parametrize( + "site_payload", + [[{"desc": "Site name", "name": "site_id", "role": "not admin", "_id": "1"}]], +) +@pytest.mark.usefixtures("config_entry_setup") +async def test_not_admin(hass: HomeAssistant) -> None: """Test that the INSTALL feature is not available on a non-admin account.""" - site = deepcopy(SITE) - site[0]["role"] = "not admin" - - await setup_unifi_integration( - hass, aioclient_mock, sites=site, devices_response=[DEVICE_1] - ) - assert len(hass.states.async_entity_ids(UPDATE_DOMAIN)) == 1 device_state = hass.states.get("update.device_1") assert device_state.state == STATE_ON @@ -151,13 +136,12 @@ async def test_not_admin( ) +@pytest.mark.parametrize("device_payload", [[DEVICE_1]]) async def test_install( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, config_entry_setup ) -> None: """Test the device update install call.""" - config_entry = await setup_unifi_integration( - hass, aioclient_mock, devices_response=[DEVICE_1] - ) + config_entry = config_entry_setup assert len(hass.states.async_entity_ids(UPDATE_DOMAIN)) == 1 device_state = hass.states.get("update.device_1") @@ -187,12 +171,10 @@ async def test_install( ) -async def test_hub_state_change( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, websocket_mock -) -> None: +@pytest.mark.parametrize("device_payload", [[DEVICE_1]]) +@pytest.mark.usefixtures("config_entry_setup") +async def test_hub_state_change(hass: HomeAssistant, websocket_mock) -> None: """Verify entities state reflect on hub becoming unavailable.""" - await setup_unifi_integration(hass, aioclient_mock, devices_response=[DEVICE_1]) - assert len(hass.states.async_entity_ids(UPDATE_DOMAIN)) == 1 assert hass.states.get("update.device_1").state == STATE_ON From b4f632527874dac9733ff960bf9e9e778d718925 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Tue, 4 Jun 2024 21:01:03 +0200 Subject: [PATCH 0229/1445] Use fixtures in UniFi switch tests (#118831) --- tests/components/unifi/test_switch.py | 461 ++++++++++++-------------- 1 file changed, 211 insertions(+), 250 deletions(-) diff --git a/tests/components/unifi/test_switch.py b/tests/components/unifi/test_switch.py index 9b63113e750..ed8d5b29a2a 100644 --- a/tests/components/unifi/test_switch.py +++ b/tests/components/unifi/test_switch.py @@ -35,9 +35,9 @@ from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity_registry import RegistryEntryDisabler from homeassistant.util import dt as dt_util -from .test_hub import CONTROLLER_HOST, ENTRY_CONFIG, SITE, setup_unifi_integration +from .test_hub import CONTROLLER_HOST -from tests.common import MockConfigEntry, async_fire_time_changed +from tests.common import async_fire_time_changed from tests.test_util.aiohttp import AiohttpClientMocker CLIENT_1 = { @@ -760,77 +760,50 @@ WLAN = { } -async def test_no_clients( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker -) -> None: - """Test the update_clients function when no clients are found.""" - await setup_unifi_integration( - hass, - aioclient_mock, - options={ +@pytest.mark.parametrize("client_payload", [[CONTROLLER_HOST]]) +@pytest.mark.parametrize("device_payload", [[DEVICE_1]]) +@pytest.mark.usefixtures("config_entry_setup") +async def test_hub_not_client(hass: HomeAssistant) -> None: + """Test that the cloud key doesn't become a switch.""" + assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 0 + assert hass.states.get("switch.cloud_key") is None + + +@pytest.mark.parametrize("client_payload", [[CLIENT_1]]) +@pytest.mark.parametrize("device_payload", [[DEVICE_1]]) +@pytest.mark.parametrize( + "site_payload", + [[{"desc": "Site name", "name": "site_id", "role": "not admin", "_id": "1"}]], +) +@pytest.mark.usefixtures("config_entry_setup") +async def test_not_admin(hass: HomeAssistant) -> None: + """Test that switch platform only work on an admin account.""" + assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 0 + + +@pytest.mark.parametrize( + "config_entry_options", + [ + { + CONF_BLOCK_CLIENT: [BLOCKED["mac"], UNBLOCKED["mac"]], CONF_TRACK_CLIENTS: False, CONF_TRACK_DEVICES: False, - CONF_DPI_RESTRICTIONS: False, - }, - ) - - assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 0 - - -async def test_hub_not_client( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker -) -> None: - """Test that the cloud key doesn't become a switch.""" - await setup_unifi_integration( - hass, - aioclient_mock, - options={CONF_TRACK_CLIENTS: False, CONF_TRACK_DEVICES: False}, - clients_response=[CONTROLLER_HOST], - devices_response=[DEVICE_1], - ) - - assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 0 - cloudkey = hass.states.get("switch.cloud_key") - assert cloudkey is None - - -async def test_not_admin( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker -) -> None: - """Test that switch platform only work on an admin account.""" - site = deepcopy(SITE) - site[0]["role"] = "not admin" - await setup_unifi_integration( - hass, - aioclient_mock, - options={CONF_TRACK_CLIENTS: False, CONF_TRACK_DEVICES: False}, - sites=site, - clients_response=[CLIENT_1], - devices_response=[DEVICE_1], - ) - - assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 0 - - + } + ], +) +@pytest.mark.parametrize("client_payload", [[CLIENT_4]]) +@pytest.mark.parametrize("clients_all_payload", [[BLOCKED, UNBLOCKED, CLIENT_1]]) +@pytest.mark.parametrize("dpi_app_payload", [DPI_APPS]) +@pytest.mark.parametrize("dpi_group_payload", [DPI_GROUPS]) +@pytest.mark.usefixtures("config_entry_setup") async def test_switches( hass: HomeAssistant, entity_registry: er.EntityRegistry, aioclient_mock: AiohttpClientMocker, + config_entry_setup, ) -> None: """Test the update_items function with some clients.""" - config_entry = await setup_unifi_integration( - hass, - aioclient_mock, - options={ - CONF_BLOCK_CLIENT: [BLOCKED["mac"], UNBLOCKED["mac"]], - CONF_TRACK_CLIENTS: False, - CONF_TRACK_DEVICES: False, - }, - clients_response=[CLIENT_4], - clients_all_response=[BLOCKED, UNBLOCKED, CLIENT_1], - dpigroup_response=DPI_GROUPS, - dpiapp_response=DPI_APPS, - ) + config_entry = config_entry_setup assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 3 @@ -906,19 +879,15 @@ async def test_switches( assert aioclient_mock.mock_calls[1][2] == {"enabled": True} -async def test_remove_switches( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, mock_unifi_websocket -) -> None: +@pytest.mark.parametrize( + "config_entry_options", [{CONF_BLOCK_CLIENT: [UNBLOCKED["mac"]]}] +) +@pytest.mark.parametrize("client_payload", [[UNBLOCKED]]) +@pytest.mark.parametrize("dpi_app_payload", [DPI_APPS]) +@pytest.mark.parametrize("dpi_group_payload", [DPI_GROUPS]) +@pytest.mark.usefixtures("config_entry_setup") +async def test_remove_switches(hass: HomeAssistant, mock_unifi_websocket) -> None: """Test the update_items function with some clients.""" - await setup_unifi_integration( - hass, - aioclient_mock, - options={CONF_BLOCK_CLIENT: [UNBLOCKED["mac"]]}, - clients_response=[UNBLOCKED], - dpigroup_response=DPI_GROUPS, - dpiapp_response=DPI_APPS, - ) - assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 2 assert hass.states.get("switch.block_client_2") is not None @@ -939,21 +908,26 @@ async def test_remove_switches( assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 0 -async def test_block_switches( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, mock_unifi_websocket -) -> None: - """Test the update_items function with some clients.""" - config_entry = await setup_unifi_integration( - hass, - aioclient_mock, - options={ +@pytest.mark.parametrize( + "config_entry_options", + [ + { CONF_BLOCK_CLIENT: [BLOCKED["mac"], UNBLOCKED["mac"]], CONF_TRACK_CLIENTS: False, CONF_TRACK_DEVICES: False, - }, - clients_response=[UNBLOCKED], - clients_all_response=[BLOCKED], - ) + } + ], +) +@pytest.mark.parametrize("client_payload", [[UNBLOCKED]]) +@pytest.mark.parametrize("clients_all_payload", [[BLOCKED]]) +async def test_block_switches( + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, + mock_unifi_websocket, + config_entry_setup, +) -> None: + """Test the update_items function with some clients.""" + config_entry = config_entry_setup assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 2 @@ -1006,20 +980,13 @@ async def test_block_switches( } +@pytest.mark.parametrize("dpi_app_payload", [DPI_APPS]) +@pytest.mark.parametrize("dpi_group_payload", [DPI_GROUPS]) +@pytest.mark.usefixtures("config_entry_setup") async def test_dpi_switches( - hass: HomeAssistant, - aioclient_mock: AiohttpClientMocker, - mock_unifi_websocket, - websocket_mock, + hass: HomeAssistant, mock_unifi_websocket, websocket_mock ) -> None: """Test the update_items function with some clients.""" - await setup_unifi_integration( - hass, - aioclient_mock, - dpigroup_response=DPI_GROUPS, - dpiapp_response=DPI_APPS, - ) - assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 1 dpi_switch = hass.states.get("switch.block_media_streaming") @@ -1050,17 +1017,13 @@ async def test_dpi_switches( assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 0 +@pytest.mark.parametrize("dpi_app_payload", [DPI_APPS]) +@pytest.mark.parametrize("dpi_group_payload", [DPI_GROUPS]) +@pytest.mark.usefixtures("config_entry_setup") async def test_dpi_switches_add_second_app( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, mock_unifi_websocket + hass: HomeAssistant, mock_unifi_websocket ) -> None: """Test the update_items function with some clients.""" - await setup_unifi_integration( - hass, - aioclient_mock, - dpigroup_response=DPI_GROUPS, - dpiapp_response=DPI_APPS, - ) - assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 1 assert hass.states.get("switch.block_media_streaming").state == STATE_ON @@ -1109,43 +1072,29 @@ async def test_dpi_switches_add_second_app( @pytest.mark.parametrize( - ("entity_id", "test_data", "outlet_index", "expected_switches"), + ("device_payload", "entity_id", "outlet_index", "expected_switches"), [ - ( - "plug_outlet_1", - OUTLET_UP1, - 1, - 1, - ), - ( - "dummy_usp_pdu_pro_usb_outlet_1", - PDU_DEVICE_1, - 1, - 2, - ), - ( - "dummy_usp_pdu_pro_outlet_2", - PDU_DEVICE_1, - 2, - 2, - ), + ([OUTLET_UP1], "plug_outlet_1", 1, 1), + ([PDU_DEVICE_1], "dummy_usp_pdu_pro_usb_outlet_1", 1, 2), + ([PDU_DEVICE_1], "dummy_usp_pdu_pro_outlet_2", 2, 2), ], ) async def test_outlet_switches( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, mock_unifi_websocket, + config_entry_setup, + device_payload, websocket_mock, entity_id: str, - test_data: any, outlet_index: int, expected_switches: int, ) -> None: """Test the outlet entities.""" - config_entry = await setup_unifi_integration( - hass, aioclient_mock, devices_response=[test_data] - ) + config_entry = config_entry_setup + assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == expected_switches + # Validate state object switch_1 = hass.states.get(f"switch.{entity_id}") assert switch_1 is not None @@ -1153,14 +1102,14 @@ async def test_outlet_switches( assert switch_1.attributes.get(ATTR_DEVICE_CLASS) == SwitchDeviceClass.OUTLET # Update state object - device_1 = deepcopy(test_data) + device_1 = deepcopy(device_payload[0]) device_1["outlet_table"][outlet_index - 1]["relay_state"] = False mock_unifi_websocket(message=MessageKey.DEVICE, data=device_1) await hass.async_block_till_done() assert hass.states.get(f"switch.{entity_id}").state == STATE_OFF # Turn off outlet - device_id = test_data["device_id"] + device_id = device_payload[0]["device_id"] aioclient_mock.clear_requests() aioclient_mock.put( f"https://{config_entry.data[CONF_HOST]}:1234" @@ -1229,21 +1178,22 @@ async def test_outlet_switches( assert hass.states.get(f"switch.{entity_id}") is None -async def test_new_client_discovered_on_block_control( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, mock_unifi_websocket -) -> None: - """Test if 2nd update has a new client.""" - await setup_unifi_integration( - hass, - aioclient_mock, - options={ +@pytest.mark.parametrize( + "config_entry_options", + [ + { CONF_BLOCK_CLIENT: [BLOCKED["mac"]], CONF_TRACK_CLIENTS: False, CONF_TRACK_DEVICES: False, CONF_DPI_RESTRICTIONS: False, - }, - ) - + } + ], +) +@pytest.mark.usefixtures("config_entry_setup") +async def test_new_client_discovered_on_block_control( + hass: HomeAssistant, mock_unifi_websocket +) -> None: + """Test if 2nd update has a new client.""" assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 0 assert hass.states.get("switch.block_client_1") is None @@ -1254,22 +1204,27 @@ async def test_new_client_discovered_on_block_control( assert hass.states.get("switch.block_client_1") is not None +@pytest.mark.parametrize( + "config_entry_options", [{CONF_BLOCK_CLIENT: [BLOCKED["mac"]]}] +) +@pytest.mark.parametrize("clients_all_payload", [[BLOCKED, UNBLOCKED]]) async def test_option_block_clients( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, config_entry_setup, clients_all_payload ) -> None: """Test the changes to option reflects accordingly.""" - config_entry = await setup_unifi_integration( - hass, - aioclient_mock, - options={CONF_BLOCK_CLIENT: [BLOCKED["mac"]]}, - clients_all_response=[BLOCKED, UNBLOCKED], - ) + config_entry = config_entry_setup + assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 1 # Add a second switch hass.config_entries.async_update_entry( config_entry, - options={CONF_BLOCK_CLIENT: [BLOCKED["mac"], UNBLOCKED["mac"]]}, + options={ + CONF_BLOCK_CLIENT: [ + clients_all_payload[0]["mac"], + clients_all_payload[1]["mac"], + ] + }, ) await hass.async_block_till_done() assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 1 @@ -1277,15 +1232,15 @@ async def test_option_block_clients( # Remove the second switch again hass.config_entries.async_update_entry( config_entry, - options={CONF_BLOCK_CLIENT: [BLOCKED["mac"]]}, + options={CONF_BLOCK_CLIENT: [clients_all_payload[0]["mac"]]}, ) await hass.async_block_till_done() assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 1 - # Enable one and remove another one + # Enable one and remove the other one hass.config_entries.async_update_entry( config_entry, - options={CONF_BLOCK_CLIENT: [UNBLOCKED["mac"]]}, + options={CONF_BLOCK_CLIENT: [clients_all_payload[1]["mac"]]}, ) await hass.async_block_till_done() assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 0 @@ -1299,21 +1254,17 @@ async def test_option_block_clients( assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 0 -async def test_option_remove_switches( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker -) -> None: +@pytest.mark.parametrize( + "config_entry_options", + [{CONF_TRACK_CLIENTS: False, CONF_TRACK_DEVICES: False}], +) +@pytest.mark.parametrize("client_payload", [[CLIENT_1]]) +@pytest.mark.parametrize("dpi_app_payload", [DPI_APPS]) +@pytest.mark.parametrize("dpi_group_payload", [DPI_GROUPS]) +async def test_option_remove_switches(hass: HomeAssistant, config_entry_setup) -> None: """Test removal of DPI switch when options updated.""" - config_entry = await setup_unifi_integration( - hass, - aioclient_mock, - options={ - CONF_TRACK_CLIENTS: False, - CONF_TRACK_DEVICES: False, - }, - clients_response=[CLIENT_1], - dpigroup_response=DPI_GROUPS, - dpiapp_response=DPI_APPS, - ) + config_entry = config_entry_setup + assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 1 # Disable DPI Switches @@ -1325,17 +1276,18 @@ async def test_option_remove_switches( assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 0 +@pytest.mark.parametrize("device_payload", [[DEVICE_1]]) async def test_poe_port_switches( hass: HomeAssistant, entity_registry: er.EntityRegistry, aioclient_mock: AiohttpClientMocker, mock_unifi_websocket, websocket_mock, + config_entry_setup, + device_payload, ) -> None: - """Test the update_items function with some clients.""" - config_entry = await setup_unifi_integration( - hass, aioclient_mock, devices_response=[DEVICE_1] - ) + """Test PoE port entities work.""" + config_entry = config_entry_setup assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 0 @@ -1350,7 +1302,7 @@ async def test_poe_port_switches( entity_registry.async_update_entity( entity_id="switch.mock_name_port_2_poe", disabled_by=None ) - await hass.async_block_till_done() + # await hass.async_block_till_done() async_fire_time_changed( hass, @@ -1365,7 +1317,7 @@ async def test_poe_port_switches( assert switch_1.attributes.get(ATTR_DEVICE_CLASS) == SwitchDeviceClass.OUTLET # Update state object - device_1 = deepcopy(DEVICE_1) + device_1 = deepcopy(device_payload[0]) device_1["port_table"][0]["poe_mode"] = "off" mock_unifi_websocket(message=MessageKey.DEVICE, data=device_1) await hass.async_block_till_done() @@ -1437,17 +1389,18 @@ async def test_poe_port_switches( assert hass.states.get("switch.mock_name_port_1_poe").state == STATE_OFF +@pytest.mark.parametrize("wlan_payload", [[WLAN]]) async def test_wlan_switches( hass: HomeAssistant, entity_registry: er.EntityRegistry, aioclient_mock: AiohttpClientMocker, mock_unifi_websocket, websocket_mock, + config_entry_setup, + wlan_payload, ) -> None: """Test control of UniFi WLAN availability.""" - config_entry = await setup_unifi_integration( - hass, aioclient_mock, wlans_response=[WLAN] - ) + config_entry = config_entry_setup assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 1 @@ -1462,7 +1415,7 @@ async def test_wlan_switches( assert switch_1.attributes.get(ATTR_DEVICE_CLASS) == SwitchDeviceClass.SWITCH # Update state object - wlan = deepcopy(WLAN) + wlan = deepcopy(wlan_payload[0]) wlan["enabled"] = False mock_unifi_websocket(message=MessageKey.WLAN_CONF_UPDATED, data=wlan) await hass.async_block_till_done() @@ -1472,7 +1425,7 @@ async def test_wlan_switches( aioclient_mock.clear_requests() aioclient_mock.put( f"https://{config_entry.data[CONF_HOST]}:1234" - f"/api/s/{config_entry.data[CONF_SITE_ID]}/rest/wlanconf/{WLAN['_id']}", + f"/api/s/{config_entry.data[CONF_SITE_ID]}/rest/wlanconf/{wlan['_id']}", ) await hass.services.async_call( @@ -1505,30 +1458,36 @@ async def test_wlan_switches( assert hass.states.get("switch.ssid_1").state == STATE_OFF +@pytest.mark.parametrize( + "port_forward_payload", + [ + [ + { + "_id": "5a32aa4ee4b0412345678911", + "dst_port": "12345", + "enabled": True, + "fwd_port": "23456", + "fwd": "10.0.0.2", + "name": "plex", + "pfwd_interface": "wan", + "proto": "tcp_udp", + "site_id": "5a32aa4ee4b0412345678910", + "src": "any", + } + ] + ], +) async def test_port_forwarding_switches( hass: HomeAssistant, entity_registry: er.EntityRegistry, aioclient_mock: AiohttpClientMocker, mock_unifi_websocket, websocket_mock, + config_entry_setup, + port_forward_payload, ) -> None: """Test control of UniFi port forwarding.""" - _data = { - "_id": "5a32aa4ee4b0412345678911", - "dst_port": "12345", - "enabled": True, - "fwd_port": "23456", - "fwd": "10.0.0.2", - "name": "plex", - "pfwd_interface": "wan", - "proto": "tcp_udp", - "site_id": "5a32aa4ee4b0412345678910", - "src": "any", - } - config_entry = await setup_unifi_integration( - hass, aioclient_mock, port_forward_response=[_data.copy()] - ) - + config_entry = config_entry_setup assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 1 ent_reg_entry = entity_registry.async_get("switch.unifi_network_plex") @@ -1542,7 +1501,7 @@ async def test_port_forwarding_switches( assert switch_1.attributes.get(ATTR_DEVICE_CLASS) == SwitchDeviceClass.SWITCH # Update state object - data = _data.copy() + data = port_forward_payload[0].copy() data["enabled"] = False mock_unifi_websocket(message=MessageKey.PORT_FORWARD_UPDATED, data=data) await hass.async_block_till_done() @@ -1562,7 +1521,7 @@ async def test_port_forwarding_switches( blocking=True, ) assert aioclient_mock.call_count == 1 - data = _data.copy() + data = port_forward_payload[0].copy() data["enabled"] = False assert aioclient_mock.mock_calls[0][2] == data @@ -1574,7 +1533,7 @@ async def test_port_forwarding_switches( blocking=True, ) assert aioclient_mock.call_count == 2 - assert aioclient_mock.mock_calls[1][2] == _data + assert aioclient_mock.mock_calls[1][2] == port_forward_payload[0] # Availability signalling @@ -1587,72 +1546,74 @@ async def test_port_forwarding_switches( assert hass.states.get("switch.unifi_network_plex").state == STATE_OFF # Remove entity on deleted message - mock_unifi_websocket(message=MessageKey.PORT_FORWARD_DELETED, data=_data) + mock_unifi_websocket( + message=MessageKey.PORT_FORWARD_DELETED, data=port_forward_payload[0] + ) await hass.async_block_till_done() assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 0 +@pytest.mark.parametrize( + "device_payload", + [ + [ + OUTLET_UP1, + { + "board_rev": 3, + "device_id": "mock-id", + "ip": "10.0.0.1", + "last_seen": 1562600145, + "mac": "00:00:00:00:01:01", + "model": "US16P150", + "name": "switch", + "state": 1, + "type": "usw", + "version": "4.0.42.10433", + "port_table": [ + { + "media": "GE", + "name": "Port 1", + "port_idx": 1, + "poe_caps": 7, + "poe_class": "Class 4", + "poe_enable": True, + "poe_mode": "auto", + "poe_power": "2.56", + "poe_voltage": "53.40", + "portconf_id": "1a1", + "port_poe": True, + "up": True, + }, + ], + }, + ] + ], +) async def test_updating_unique_id( hass: HomeAssistant, entity_registry: er.EntityRegistry, - aioclient_mock: AiohttpClientMocker, + config_entry_factory, + config_entry, + device_payload, ) -> None: """Verify outlet control and poe control unique ID update works.""" - poe_device = { - "board_rev": 3, - "device_id": "mock-id", - "ip": "10.0.0.1", - "last_seen": 1562600145, - "mac": "00:00:00:00:01:01", - "model": "US16P150", - "name": "switch", - "state": 1, - "type": "usw", - "version": "4.0.42.10433", - "port_table": [ - { - "media": "GE", - "name": "Port 1", - "port_idx": 1, - "poe_caps": 7, - "poe_class": "Class 4", - "poe_enable": True, - "poe_mode": "auto", - "poe_power": "2.56", - "poe_voltage": "53.40", - "portconf_id": "1a1", - "port_poe": True, - "up": True, - }, - ], - } - - config_entry = MockConfigEntry( - domain=UNIFI_DOMAIN, - data=ENTRY_CONFIG, - source="test", - options={}, - entry_id="1", - ) - entity_registry.async_get_or_create( SWITCH_DOMAIN, UNIFI_DOMAIN, - f'{poe_device["mac"]}-poe-1', - suggested_object_id="switch_port_1_poe", - config_entry=config_entry, - ) - entity_registry.async_get_or_create( - SWITCH_DOMAIN, - UNIFI_DOMAIN, - f'{OUTLET_UP1["mac"]}-outlet-1', + f'{device_payload[0]["mac"]}-outlet-1', suggested_object_id="plug_outlet_1", config_entry=config_entry, ) - - await setup_unifi_integration( - hass, aioclient_mock, devices_response=[poe_device, OUTLET_UP1] + entity_registry.async_get_or_create( + SWITCH_DOMAIN, + UNIFI_DOMAIN, + f'{device_payload[1]["mac"]}-poe-1', + suggested_object_id="switch_port_1_poe", + config_entry=config_entry, ) + + await config_entry_factory() + assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 2 - assert hass.states.get("switch.switch_port_1_poe") assert hass.states.get("switch.plug_outlet_1") + assert hass.states.get("switch.switch_port_1_poe") From 278751607f9eee61adb7b3b4d965752719f5974a Mon Sep 17 00:00:00 2001 From: Jack Boswell Date: Wed, 5 Jun 2024 07:19:09 +1200 Subject: [PATCH 0230/1445] Fix calculation of Starlink sleep end setting (#115507) Co-authored-by: J. Nick Koston --- homeassistant/components/starlink/coordinator.py | 6 +++++- homeassistant/components/starlink/time.py | 2 ++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/starlink/coordinator.py b/homeassistant/components/starlink/coordinator.py index 7a09b2f2dee..a891941fb8e 100644 --- a/homeassistant/components/starlink/coordinator.py +++ b/homeassistant/components/starlink/coordinator.py @@ -119,12 +119,16 @@ class StarlinkUpdateCoordinator(DataUpdateCoordinator[StarlinkData]): async def async_set_sleep_duration(self, end: int) -> None: """Set Starlink system sleep schedule end time.""" + duration = end - self.data.sleep[0] + if duration < 0: + # If the duration pushed us into the next day, add one days worth to correct that. + duration += 1440 async with asyncio.timeout(4): try: await self.hass.async_add_executor_job( set_sleep_config, self.data.sleep[0], - end, + duration, self.data.sleep[2], self.channel_context, ) diff --git a/homeassistant/components/starlink/time.py b/homeassistant/components/starlink/time.py index 6475610564d..7395ec101ba 100644 --- a/homeassistant/components/starlink/time.py +++ b/homeassistant/components/starlink/time.py @@ -62,6 +62,8 @@ class StarlinkTimeEntity(StarlinkEntity, TimeEntity): def _utc_minutes_to_time(utc_minutes: int, timezone: tzinfo) -> time: hour = math.floor(utc_minutes / 60) + if hour > 23: + hour -= 24 minute = utc_minutes % 60 try: utc = datetime.now(UTC).replace( From 67b3be84321a3bccb3e81e980a85e27744cd8a46 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 4 Jun 2024 14:21:03 -0500 Subject: [PATCH 0231/1445] Remove useless threading locks in mqtt (#118737) --- homeassistant/components/mqtt/async_client.py | 60 +++++++++++++++++++ homeassistant/components/mqtt/client.py | 16 +++-- tests/components/mqtt/test_config_flow.py | 8 ++- tests/components/mqtt/test_init.py | 33 +++++++--- tests/conftest.py | 4 +- 5 files changed, 106 insertions(+), 15 deletions(-) create mode 100644 homeassistant/components/mqtt/async_client.py diff --git a/homeassistant/components/mqtt/async_client.py b/homeassistant/components/mqtt/async_client.py new file mode 100644 index 00000000000..c0b847f35a1 --- /dev/null +++ b/homeassistant/components/mqtt/async_client.py @@ -0,0 +1,60 @@ +"""Async wrappings for mqtt client.""" + +from __future__ import annotations + +from functools import lru_cache +from types import TracebackType +from typing import Self + +from paho.mqtt.client import Client as MQTTClient + +_MQTT_LOCK_COUNT = 7 + + +class NullLock: + """Null lock.""" + + @lru_cache(maxsize=_MQTT_LOCK_COUNT) + def __enter__(self) -> Self: + """Enter the lock.""" + return self + + @lru_cache(maxsize=_MQTT_LOCK_COUNT) + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + traceback: TracebackType | None, + ) -> None: + """Exit the lock.""" + + @lru_cache(maxsize=_MQTT_LOCK_COUNT) + def acquire(self, blocking: bool = False, timeout: int = -1) -> None: + """Acquire the lock.""" + + @lru_cache(maxsize=_MQTT_LOCK_COUNT) + def release(self) -> None: + """Release the lock.""" + + +class AsyncMQTTClient(MQTTClient): + """Async MQTT Client. + + Wrapper around paho.mqtt.client.Client to remove the locking + that is not needed since we are running in an async event loop. + """ + + def async_setup(self) -> None: + """Set up the client. + + All the threading locks are replaced with NullLock + since the client is running in an async event loop + and will never run in multiple threads. + """ + self._in_callback_mutex = NullLock() + self._callback_mutex = NullLock() + self._msgtime_mutex = NullLock() + self._out_message_mutex = NullLock() + self._in_message_mutex = NullLock() + self._reconnect_delay_mutex = NullLock() + self._mid_generate_mutex = NullLock() diff --git a/homeassistant/components/mqtt/client.py b/homeassistant/components/mqtt/client.py index d36670baef1..f01cb9c948f 100644 --- a/homeassistant/components/mqtt/client.py +++ b/homeassistant/components/mqtt/client.py @@ -91,6 +91,8 @@ if TYPE_CHECKING: # because integrations should be able to optionally rely on MQTT. import paho.mqtt.client as mqtt + from .async_client import AsyncMQTTClient + _LOGGER = logging.getLogger(__name__) MIN_BUFFER_SIZE = 131072 # Minimum buffer size to use if preferred size fails @@ -281,6 +283,9 @@ class MqttClientSetup: # should be able to optionally rely on MQTT. import paho.mqtt.client as mqtt # pylint: disable=import-outside-toplevel + # pylint: disable-next=import-outside-toplevel + from .async_client import AsyncMQTTClient + if (protocol := config.get(CONF_PROTOCOL, DEFAULT_PROTOCOL)) == PROTOCOL_31: proto = mqtt.MQTTv31 elif protocol == PROTOCOL_5: @@ -293,9 +298,10 @@ class MqttClientSetup: # However, that feature is not mandatory so we generate our own. client_id = mqtt.base62(uuid.uuid4().int, padding=22) transport = config.get(CONF_TRANSPORT, DEFAULT_TRANSPORT) - self._client = mqtt.Client( + self._client = AsyncMQTTClient( client_id, protocol=proto, transport=transport, reconnect_on_failure=False ) + self._client.async_setup() # Enable logging self._client.enable_logger() @@ -329,7 +335,7 @@ class MqttClientSetup: self._client.tls_insecure_set(tls_insecure) @property - def client(self) -> mqtt.Client: + def client(self) -> AsyncMQTTClient: """Return the paho MQTT client.""" return self._client @@ -434,7 +440,7 @@ class EnsureJobAfterCooldown: class MQTT: """Home Assistant MQTT client.""" - _mqttc: mqtt.Client + _mqttc: AsyncMQTTClient _last_subscribe: float _mqtt_data: MqttData @@ -533,7 +539,9 @@ class MQTT: async def async_init_client(self) -> None: """Initialize paho client.""" with async_pause_setup(self.hass, SetupPhases.WAIT_IMPORT_PACKAGES): - await async_import_module(self.hass, "paho.mqtt.client") + await async_import_module( + self.hass, "homeassistant.components.mqtt.async_client" + ) mqttc = MqttClientSetup(self.conf).client # on_socket_unregister_write and _async_on_socket_close diff --git a/tests/components/mqtt/test_config_flow.py b/tests/components/mqtt/test_config_flow.py index 576ba3f94b2..f218a5b0447 100644 --- a/tests/components/mqtt/test_config_flow.py +++ b/tests/components/mqtt/test_config_flow.py @@ -121,7 +121,9 @@ def mock_try_connection_success() -> Generator[MqttMockPahoClient, None, None]: mock_client().on_unsubscribe(mock_client, 0, mid) return (0, mid) - with patch("paho.mqtt.client.Client") as mock_client: + with patch( + "homeassistant.components.mqtt.async_client.AsyncMQTTClient" + ) as mock_client: mock_client().loop_start = loop_start mock_client().subscribe = _subscribe mock_client().unsubscribe = _unsubscribe @@ -135,7 +137,9 @@ def mock_try_connection_time_out() -> Generator[MagicMock, None, None]: # Patch prevent waiting 5 sec for a timeout with ( - patch("paho.mqtt.client.Client") as mock_client, + patch( + "homeassistant.components.mqtt.async_client.AsyncMQTTClient" + ) as mock_client, patch("homeassistant.components.mqtt.config_flow.MQTT_TIMEOUT", 0), ): mock_client().loop_start = lambda *args: 1 diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index 2b9e4260c7e..5189196ac2b 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -180,7 +180,9 @@ async def test_mqtt_await_ack_at_disconnect( mid = 100 rc = 0 - with patch("paho.mqtt.client.Client") as mock_client: + with patch( + "homeassistant.components.mqtt.async_client.AsyncMQTTClient" + ) as mock_client: mqtt_client = mock_client.return_value mqtt_client.connect = MagicMock( return_value=0, @@ -191,10 +193,15 @@ async def test_mqtt_await_ack_at_disconnect( mqtt_client.publish = MagicMock(return_value=FakeInfo()) entry = MockConfigEntry( domain=mqtt.DOMAIN, - data={"certificate": "auto", mqtt.CONF_BROKER: "test-broker"}, + data={ + "certificate": "auto", + mqtt.CONF_BROKER: "test-broker", + mqtt.CONF_DISCOVERY: False, + }, ) entry.add_to_hass(hass) assert await hass.config_entries.async_setup(entry.entry_id) + mqtt_client = mock_client.return_value # publish from MQTT client without awaiting @@ -2219,7 +2226,9 @@ async def test_publish_error( entry.add_to_hass(hass) # simulate an Out of memory error - with patch("paho.mqtt.client.Client") as mock_client: + with patch( + "homeassistant.components.mqtt.async_client.AsyncMQTTClient" + ) as mock_client: mock_client().connect = lambda *args: 1 mock_client().publish().rc = 1 assert await hass.config_entries.async_setup(entry.entry_id) @@ -2354,7 +2363,9 @@ async def test_setup_mqtt_client_protocol( protocol: int, ) -> None: """Test MQTT client protocol setup.""" - with patch("paho.mqtt.client.Client") as mock_client: + with patch( + "homeassistant.components.mqtt.async_client.AsyncMQTTClient" + ) as mock_client: await mqtt_mock_entry() # check if protocol setup was correctly @@ -2374,7 +2385,9 @@ async def test_handle_mqtt_timeout_on_callback( mid = 100 rc = 0 - with patch("paho.mqtt.client.Client") as mock_client: + with patch( + "homeassistant.components.mqtt.async_client.AsyncMQTTClient" + ) as mock_client: def _mock_ack(topic: str, qos: int = 0) -> tuple[int, int]: # Handle ACK for subscribe normally @@ -2419,7 +2432,9 @@ async def test_setup_raises_config_entry_not_ready_if_no_connect_broker( entry = MockConfigEntry(domain=mqtt.DOMAIN, data={mqtt.CONF_BROKER: "test-broker"}) entry.add_to_hass(hass) - with patch("paho.mqtt.client.Client") as mock_client: + with patch( + "homeassistant.components.mqtt.async_client.AsyncMQTTClient" + ) as mock_client: mock_client().connect = MagicMock(side_effect=OSError("Connection error")) assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() @@ -2454,7 +2469,9 @@ async def test_setup_uses_certificate_on_certificate_set_to_auto_and_insecure( def mock_tls_insecure_set(insecure_param) -> None: insecure_check["insecure"] = insecure_param - with patch("paho.mqtt.client.Client") as mock_client: + with patch( + "homeassistant.components.mqtt.async_client.AsyncMQTTClient" + ) as mock_client: mock_client().tls_set = mock_tls_set mock_client().tls_insecure_set = mock_tls_insecure_set await mqtt_mock_entry() @@ -4023,7 +4040,7 @@ async def test_link_config_entry( assert _check_entities() == 2 # reload entry and assert again - with patch("paho.mqtt.client.Client"): + with patch("homeassistant.components.mqtt.async_client.AsyncMQTTClient"): await hass.config_entries.async_reload(mqtt_config_entry.entry_id) await hass.async_block_till_done() diff --git a/tests/conftest.py b/tests/conftest.py index 13a8daa8ce1..a6f9c34c568 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -920,7 +920,9 @@ def mqtt_client_mock(hass: HomeAssistant) -> Generator[MqttMockPahoClient, None, self.mid = mid self.rc = 0 - with patch("paho.mqtt.client.Client") as mock_client: + with patch( + "homeassistant.components.mqtt.async_client.AsyncMQTTClient" + ) as mock_client: # The below use a call_soon for the on_publish/on_subscribe/on_unsubscribe # callbacks to simulate the behavior of the real MQTT client which will # not be synchronous. From ed0568c65512a138843c42e73d041e36feab5904 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 4 Jun 2024 20:34:39 -0500 Subject: [PATCH 0232/1445] Ensure config entries are not unloaded while their platforms are setting up (#118767) * Report non-awaited/non-locked config entry platform forwards Its currently possible for config entries to be reloaded while their platforms are being forwarded if platform forwards are not awaited or done after the config entry is setup since the lock will not be held in this case. In https://developers.home-assistant.io/blog/2022/07/08/config_entry_forwards we advised to await platform forwards to ensure this does not happen, however for sleeping devices and late discovered devices, platform forwards may happen later. If config platform forwards are happening during setup, they should be awaited If config entry platform forwards are not happening during setup, instead async_late_forward_entry_setups should be used which will hold the lock to prevent the config entry from being unloaded while its platforms are being setup * Report non-awaited/non-locked config entry platform forwards Its currently possible for config entries to be reloaded while their platforms are being forwarded if platform forwards are not awaited or done after the config entry is setup since the lock will not be held in this case. In https://developers.home-assistant.io/blog/2022/07/08/config_entry_forwards we advised to await platform forwards to ensure this does not happen, however for sleeping devices and late discovered devices, platform forwards may happen later. If config platform forwards are happening during setup, they should be awaited If config entry platform forwards are not happening during setup, instead async_late_forward_entry_setups should be used which will hold the lock to prevent the config entry from being unloaded while its platforms are being setup * run with error on to find them * cert_exp, hold lock * cert_exp, hold lock * shelly async_late_forward_entry_setups * compact * compact * found another * patch up mobileapp * patch up hue tests * patch up smartthings * fix mqtt * fix esphome * zwave_js * mqtt * rework * fixes * fix mocking * fix mocking * do not call async_forward_entry_setup directly * docstrings * docstrings * docstrings * add comments * doc strings * fixed all in core, turn off strict * coverage * coverage * missing * coverage --- .coveragerc | 1 + .../components/ambient_station/__init__.py | 2 +- .../components/cert_expiry/__init__.py | 2 +- .../components/esphome/entry_data.py | 22 +- homeassistant/components/esphome/manager.py | 2 +- homeassistant/components/knx/__init__.py | 12 +- homeassistant/components/mqtt/__init__.py | 4 +- homeassistant/components/mqtt/discovery.py | 2 +- homeassistant/components/mqtt/util.py | 17 +- homeassistant/components/point/__init__.py | 4 +- .../components/shelly/coordinator.py | 4 +- .../components/tellduslive/__init__.py | 4 +- homeassistant/components/vesync/__init__.py | 10 +- homeassistant/components/zwave_js/__init__.py | 4 +- homeassistant/config_entries.py | 101 +++++++- .../alarm_control_panel/conftest.py | 4 +- .../components/assist_pipeline/test_select.py | 18 +- tests/components/binary_sensor/test_init.py | 8 +- tests/components/button/test_init.py | 2 +- tests/components/calendar/conftest.py | 2 +- tests/components/climate/test_intent.py | 2 +- tests/components/deconz/test_gateway.py | 6 +- .../devolo_home_network/test_init.py | 4 +- tests/components/esphome/test_update.py | 15 +- tests/components/event/test_init.py | 2 +- .../components/homematicip_cloud/test_hap.py | 15 +- tests/components/hue/conftest.py | 5 +- tests/components/hue/test_bridge.py | 9 +- tests/components/hue/test_light_v1.py | 2 +- tests/components/hue/test_sensor_v2.py | 8 +- tests/components/image/conftest.py | 6 +- tests/components/lawn_mower/test_init.py | 4 +- tests/components/lock/conftest.py | 4 +- .../mobile_app/test_device_tracker.py | 4 +- tests/components/notify/test_init.py | 2 +- tests/components/number/test_init.py | 2 +- tests/components/sensor/test_init.py | 4 +- tests/components/smartthings/conftest.py | 5 +- tests/components/stt/test_init.py | 2 +- tests/components/todo/test_init.py | 2 +- tests/components/tts/common.py | 2 +- tests/components/update/test_init.py | 4 +- tests/components/vacuum/__init__.py | 2 +- tests/components/valve/test_init.py | 4 +- tests/components/wake_word/test_init.py | 4 +- tests/ignore_uncaught_exceptions.py | 6 + tests/test_config_entries.py | 218 +++++++++++++++++- 47 files changed, 457 insertions(+), 111 deletions(-) diff --git a/.coveragerc b/.coveragerc index 071fdade647..fefd9205b05 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1556,6 +1556,7 @@ omit = homeassistant/components/verisure/sensor.py homeassistant/components/verisure/switch.py homeassistant/components/versasense/* + homeassistant/components/vesync/__init__.py homeassistant/components/vesync/fan.py homeassistant/components/vesync/light.py homeassistant/components/vesync/sensor.py diff --git a/homeassistant/components/ambient_station/__init__.py b/homeassistant/components/ambient_station/__init__.py index d0b04e53e67..aded84427a5 100644 --- a/homeassistant/components/ambient_station/__init__.py +++ b/homeassistant/components/ambient_station/__init__.py @@ -182,7 +182,7 @@ class AmbientStation: # already been done): if not self._entry_setup_complete: self._hass.async_create_task( - self._hass.config_entries.async_forward_entry_setups( + self._hass.config_entries.async_late_forward_entry_setups( self._entry, PLATFORMS ), eager_start=True, diff --git a/homeassistant/components/cert_expiry/__init__.py b/homeassistant/components/cert_expiry/__init__.py index bc6ae29ee8e..2a59b10588f 100644 --- a/homeassistant/components/cert_expiry/__init__.py +++ b/homeassistant/components/cert_expiry/__init__.py @@ -28,7 +28,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: CertExpiryConfigEntry) - async def _async_finish_startup(_: HomeAssistant) -> None: await coordinator.async_refresh() - await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + await hass.config_entries.async_late_forward_entry_setups(entry, PLATFORMS) async_at_started(hass, _async_finish_startup) return True diff --git a/homeassistant/components/esphome/entry_data.py b/homeassistant/components/esphome/entry_data.py index 19e5267e8bc..c45a6dcf253 100644 --- a/homeassistant/components/esphome/entry_data.py +++ b/homeassistant/components/esphome/entry_data.py @@ -244,15 +244,29 @@ class RuntimeEntryData: callback_(static_info) async def _ensure_platforms_loaded( - self, hass: HomeAssistant, entry: ConfigEntry, platforms: set[Platform] + self, + hass: HomeAssistant, + entry: ConfigEntry, + platforms: set[Platform], + late: bool, ) -> None: async with self.platform_load_lock: if needed := platforms - self.loaded_platforms: - await hass.config_entries.async_forward_entry_setups(entry, needed) + if late: + await hass.config_entries.async_late_forward_entry_setups( + entry, needed + ) + else: + await hass.config_entries.async_forward_entry_setups(entry, needed) self.loaded_platforms |= needed async def async_update_static_infos( - self, hass: HomeAssistant, entry: ConfigEntry, infos: list[EntityInfo], mac: str + self, + hass: HomeAssistant, + entry: ConfigEntry, + infos: list[EntityInfo], + mac: str, + late: bool = False, ) -> None: """Distribute an update of static infos to all platforms.""" # First, load all platforms @@ -282,7 +296,7 @@ class RuntimeEntryData: ): ent_reg.async_update_entity(old_entry, new_unique_id=new_unique_id) - await self._ensure_platforms_loaded(hass, entry, needed_platforms) + await self._ensure_platforms_loaded(hass, entry, needed_platforms, late) # Make a dict of the EntityInfo by type and send # them to the listeners for each specific EntityInfo type diff --git a/homeassistant/components/esphome/manager.py b/homeassistant/components/esphome/manager.py index f191c36c574..09a751eb72e 100644 --- a/homeassistant/components/esphome/manager.py +++ b/homeassistant/components/esphome/manager.py @@ -491,7 +491,7 @@ class ESPHomeManager: entry_data.async_update_device_state() await entry_data.async_update_static_infos( - hass, entry, entity_infos, device_info.mac_address + hass, entry, entity_infos, device_info.mac_address, late=True ) _setup_services(hass, entry_data, services) diff --git a/homeassistant/components/knx/__init__.py b/homeassistant/components/knx/__init__.py index da68dc36a6d..9c64b4e1b31 100644 --- a/homeassistant/components/knx/__init__.py +++ b/homeassistant/components/knx/__init__.py @@ -191,15 +191,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: create_knx_exposure(hass, knx_module.xknx, expose_config) ) # always forward sensor for system entities (telegram counter, etc.) - await hass.config_entries.async_forward_entry_setup(entry, Platform.SENSOR) - await hass.config_entries.async_forward_entry_setups( - entry, - [ - platform - for platform in SUPPORTED_PLATFORMS - if platform in config and platform is not Platform.SENSOR - ], - ) + platforms = {platform for platform in SUPPORTED_PLATFORMS if platform in config} + platforms.add(Platform.SENSOR) + await hass.config_entries.async_forward_entry_setups(entry, platforms) # set up notify service for backwards compatibility - remove 2024.11 if NotifySchema.PLATFORM in config: diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index ea520e88366..687e1b14247 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -379,7 +379,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: new_config: list[ConfigType] = config_yaml.get(DOMAIN, []) platforms_used = platforms_from_config(new_config) new_platforms = platforms_used - mqtt_data.platforms_loaded - await async_forward_entry_setup_and_setup_discovery(hass, entry, new_platforms) + await async_forward_entry_setup_and_setup_discovery( + hass, entry, new_platforms, late=True + ) # Check the schema before continuing reload await async_check_config_schema(hass, config_yaml) diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py index 0d93af26a57..2ee7dffc18f 100644 --- a/homeassistant/components/mqtt/discovery.py +++ b/homeassistant/components/mqtt/discovery.py @@ -211,7 +211,7 @@ async def async_start( # noqa: C901 async with platform_setup_lock.setdefault(component, asyncio.Lock()): if component not in mqtt_data.platforms_loaded: await async_forward_entry_setup_and_setup_discovery( - hass, config_entry, {component} + hass, config_entry, {component}, late=True ) _async_add_component(discovery_payload) diff --git a/homeassistant/components/mqtt/util.py b/homeassistant/components/mqtt/util.py index eeca2361305..747a2c43f76 100644 --- a/homeassistant/components/mqtt/util.py +++ b/homeassistant/components/mqtt/util.py @@ -47,7 +47,10 @@ def platforms_from_config(config: list[ConfigType]) -> set[Platform | str]: async def async_forward_entry_setup_and_setup_discovery( - hass: HomeAssistant, config_entry: ConfigEntry, platforms: set[Platform | str] + hass: HomeAssistant, + config_entry: ConfigEntry, + platforms: set[Platform | str], + late: bool = False, ) -> None: """Forward the config entry setup to the platforms and set up discovery.""" mqtt_data = hass.data[DATA_MQTT] @@ -69,13 +72,11 @@ async def async_forward_entry_setup_and_setup_discovery( tasks.append(create_eager_task(tag.async_setup_entry(hass, config_entry))) if new_entity_platforms := (new_platforms - {"tag", "device_automation"}): - tasks.append( - create_eager_task( - hass.config_entries.async_forward_entry_setups( - config_entry, new_entity_platforms - ) - ) - ) + if late: + coro = hass.config_entries.async_late_forward_entry_setups + else: + coro = hass.config_entries.async_forward_entry_setups + tasks.append(create_eager_task(coro(config_entry, new_entity_platforms))) if not tasks: return await asyncio.gather(*tasks) diff --git a/homeassistant/components/point/__init__.py b/homeassistant/components/point/__init__.py index e1536379084..138bc8be596 100644 --- a/homeassistant/components/point/__init__.py +++ b/homeassistant/components/point/__init__.py @@ -205,8 +205,8 @@ class MinutPointClient: config_entries_key = f"{platform}.{DOMAIN}" async with self._hass.data[DATA_CONFIG_ENTRY_LOCK]: if config_entries_key not in self._hass.data[CONFIG_ENTRY_IS_SETUP]: - await self._hass.config_entries.async_forward_entry_setup( - self._config_entry, platform + await self._hass.config_entries.async_forward_entry_setups( + self._config_entry, [platform] ) self._hass.data[CONFIG_ENTRY_IS_SETUP].add(config_entries_key) diff --git a/homeassistant/components/shelly/coordinator.py b/homeassistant/components/shelly/coordinator.py index cf6e9cc897f..2fe3f6a9943 100644 --- a/homeassistant/components/shelly/coordinator.py +++ b/homeassistant/components/shelly/coordinator.py @@ -200,7 +200,9 @@ class ShellyCoordinatorBase[_DeviceT: BlockDevice | RpcDevice]( self.hass.config_entries.async_update_entry(self.entry, data=data) # Resume platform setup - await self.hass.config_entries.async_forward_entry_setups(self.entry, platforms) + await self.hass.config_entries.async_late_forward_entry_setups( + self.entry, platforms + ) return True diff --git a/homeassistant/components/tellduslive/__init__.py b/homeassistant/components/tellduslive/__init__.py index 92e61edec56..4f88b47b531 100644 --- a/homeassistant/components/tellduslive/__init__.py +++ b/homeassistant/components/tellduslive/__init__.py @@ -180,8 +180,8 @@ class TelldusLiveClient: ) async with self._hass.data[DATA_CONFIG_ENTRY_LOCK]: if component not in self._hass.data[CONFIG_ENTRY_IS_SETUP]: - await self._hass.config_entries.async_forward_entry_setup( - self._config_entry, component + await self._hass.config_entries.async_forward_entry_setups( + self._config_entry, [component] ) self._hass.data[CONFIG_ENTRY_IS_SETUP].add(component) device_ids = [] diff --git a/homeassistant/components/vesync/__init__.py b/homeassistant/components/vesync/__init__.py index e758636900b..7dceb1b3f8f 100644 --- a/homeassistant/components/vesync/__init__.py +++ b/homeassistant/components/vesync/__init__.py @@ -46,7 +46,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b device_dict = await async_process_devices(hass, manager) - forward_setup = hass.config_entries.async_forward_entry_setup + forward_setups = hass.config_entries.async_forward_entry_setups hass.data[DOMAIN] = {} hass.data[DOMAIN][VS_MANAGER] = manager @@ -97,7 +97,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b return if new_switches and not switches: switches.extend(new_switches) - hass.async_create_task(forward_setup(config_entry, Platform.SWITCH)) + hass.async_create_task(forward_setups(config_entry, [Platform.SWITCH])) fan_set = set(fan_devs) new_fans = list(fan_set.difference(fans)) @@ -107,7 +107,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b return if new_fans and not fans: fans.extend(new_fans) - hass.async_create_task(forward_setup(config_entry, Platform.FAN)) + hass.async_create_task(forward_setups(config_entry, [Platform.FAN])) light_set = set(light_devs) new_lights = list(light_set.difference(lights)) @@ -117,7 +117,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b return if new_lights and not lights: lights.extend(new_lights) - hass.async_create_task(forward_setup(config_entry, Platform.LIGHT)) + hass.async_create_task(forward_setups(config_entry, [Platform.LIGHT])) sensor_set = set(sensor_devs) new_sensors = list(sensor_set.difference(sensors)) @@ -127,7 +127,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b return if new_sensors and not sensors: sensors.extend(new_sensors) - hass.async_create_task(forward_setup(config_entry, Platform.SENSOR)) + hass.async_create_task(forward_setups(config_entry, [Platform.SENSOR])) hass.services.async_register( DOMAIN, SERVICE_UPDATE_DEVS, async_new_device_discovery diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index efd9ab717ad..2b685212642 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -324,8 +324,8 @@ class DriverEvents: """Set up platform if needed.""" if platform not in self.platform_setup_tasks: self.platform_setup_tasks[platform] = self.hass.async_create_task( - self.hass.config_entries.async_forward_entry_setup( - self.config_entry, platform + self.hass.config_entries.async_late_forward_entry_setups( + self.config_entry, [platform] ) ) await self.platform_setup_tasks[platform] diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 01363ec8129..8da9b50ffa9 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -1178,6 +1178,24 @@ class FlowCancelledError(Exception): """Error to indicate that a flow has been cancelled.""" +def _report_non_locked_platform_forwards(entry: ConfigEntry) -> None: + """Report non awaited and non-locked platform forwards.""" + report( + f"calls async_forward_entry_setup after the entry for " + f"integration, {entry.domain} with title: {entry.title} " + f"and entry_id: {entry.entry_id}, has been set up, " + "without holding the setup lock that prevents the config " + "entry from being set up multiple times. " + "Instead await hass.config_entries.async_forward_entry_setup " + "during setup of the config entry or call " + "hass.config_entries.async_late_forward_entry_setups " + "in a tracked task. " + "This will stop working in Home Assistant 2025.1", + error_if_integration=False, + error_if_core=False, + ) + + class ConfigEntriesFlowManager(data_entry_flow.FlowManager[ConfigFlowResult]): """Manage all the config entry flows that are in progress.""" @@ -2024,15 +2042,32 @@ class ConfigEntries: async def async_forward_entry_setups( self, entry: ConfigEntry, platforms: Iterable[Platform | str] ) -> None: - """Forward the setup of an entry to platforms.""" + """Forward the setup of an entry to platforms. + + This method should be awaited before async_setup_entry is finished + in each integration. This is to ensure that all platforms are loaded + before the entry is set up. This ensures that the config entry cannot + be unloaded before all platforms are loaded. + + If platforms must be loaded late (after the config entry is setup), + use async_late_forward_entry_setup instead. + + This method is more efficient than async_forward_entry_setup as + it can load multiple platforms at once and does not require a separate + import executor job for each platform. + """ integration = await loader.async_get_integration(self.hass, entry.domain) if not integration.platforms_are_loaded(platforms): with async_pause_setup(self.hass, SetupPhases.WAIT_IMPORT_PLATFORMS): await integration.async_get_platforms(platforms) + if non_locked_platform_forwards := not entry.setup_lock.locked(): + _report_non_locked_platform_forwards(entry) await asyncio.gather( *( create_eager_task( - self._async_forward_entry_setup(entry, platform, False), + self._async_forward_entry_setup( + entry, platform, False, non_locked_platform_forwards + ), name=( f"config entry forward setup {entry.title} " f"{entry.domain} {entry.entry_id} {platform}" @@ -2043,6 +2078,25 @@ class ConfigEntries: ) ) + async def async_late_forward_entry_setups( + self, entry: ConfigEntry, platforms: Iterable[Platform | str] + ) -> None: + """Forward the setup of an entry to platforms after setup. + + If platforms must be loaded late (after the config entry is setup), + use this method instead of async_forward_entry_setups as it holds + the setup lock until the platforms are loaded to ensure that the + config entry cannot be unloaded while platforms are loaded. + """ + async with entry.setup_lock: + if entry.state is not ConfigEntryState.LOADED: + raise OperationNotAllowed( + f"The config entry {entry.title} ({entry.domain}) with entry_id" + f" {entry.entry_id} cannot forward setup for {platforms} " + f"because it is not loaded in the {entry.state} state" + ) + await self.async_forward_entry_setups(entry, platforms) + async def async_forward_entry_setup( self, entry: ConfigEntry, domain: Platform | str ) -> bool: @@ -2051,11 +2105,38 @@ class ConfigEntries: By default an entry is setup with the component it belongs to. If that component also has related platforms, the component will have to forward the entry to be setup by that component. + + This method is deprecated and will stop working in Home Assistant 2025.6. + + Instead, await async_forward_entry_setups as it can load + multiple platforms at once and is more efficient since it + does not require a separate import executor job for each platform. + + If platforms must be loaded late (after the config entry is setup), + use async_late_forward_entry_setup instead. """ - return await self._async_forward_entry_setup(entry, domain, True) + if non_locked_platform_forwards := not entry.setup_lock.locked(): + _report_non_locked_platform_forwards(entry) + else: + report( + "calls async_forward_entry_setup for " + f"integration, {entry.domain} with title: {entry.title} " + f"and entry_id: {entry.entry_id}, which is deprecated and " + "will stop working in Home Assistant 2025.6, " + "await async_forward_entry_setups instead", + error_if_core=False, + error_if_integration=False, + ) + return await self._async_forward_entry_setup( + entry, domain, True, non_locked_platform_forwards + ) async def _async_forward_entry_setup( - self, entry: ConfigEntry, domain: Platform | str, preload_platform: bool + self, + entry: ConfigEntry, + domain: Platform | str, + preload_platform: bool, + non_locked_platform_forwards: bool, ) -> bool: """Forward the setup of an entry to a different component.""" # Setup Component if not set up yet @@ -2079,6 +2160,12 @@ class ConfigEntries: integration = loader.async_get_loaded_integration(self.hass, domain) await entry.async_setup(self.hass, integration=integration) + + # Check again after setup to make sure the lock + # is still there because it could have been released + # unless we already reported it. + if not non_locked_platform_forwards and not entry.setup_lock.locked(): + _report_non_locked_platform_forwards(entry) return True async def async_unload_platforms( @@ -2104,7 +2191,11 @@ class ConfigEntries: async def async_forward_entry_unload( self, entry: ConfigEntry, domain: Platform | str ) -> bool: - """Forward the unloading of an entry to a different component.""" + """Forward the unloading of an entry to a different component. + + Its is preferred to call async_unload_platforms instead + of directly calling this method. + """ # It was never loaded. if domain not in self.hass.config.components: return True diff --git a/tests/components/alarm_control_panel/conftest.py b/tests/components/alarm_control_panel/conftest.py index c076dd8ab67..9cb832abca0 100644 --- a/tests/components/alarm_control_panel/conftest.py +++ b/tests/components/alarm_control_panel/conftest.py @@ -155,8 +155,8 @@ async def setup_lock_platform_test_entity( hass: HomeAssistant, config_entry: ConfigEntry ) -> bool: """Set up test config entry.""" - await hass.config_entries.async_forward_entry_setup( - config_entry, ALARM_CONTROL_PANEL_DOMAIN + await hass.config_entries.async_forward_entry_setups( + config_entry, [ALARM_CONTROL_PANEL_DOMAIN] ) return True diff --git a/tests/components/assist_pipeline/test_select.py b/tests/components/assist_pipeline/test_select.py index 73c069ddd04..35f1e015d5d 100644 --- a/tests/components/assist_pipeline/test_select.py +++ b/tests/components/assist_pipeline/test_select.py @@ -15,7 +15,7 @@ from homeassistant.components.assist_pipeline.select import ( VadSensitivitySelect, ) from homeassistant.components.assist_pipeline.vad import VadSensitivity -from homeassistant.config_entries import ConfigEntry +from homeassistant.config_entries import ConfigEntry, ConfigEntryState from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr from homeassistant.helpers.device_registry import DeviceInfo @@ -49,9 +49,11 @@ class SelectPlatform(MockPlatform): async def init_select(hass: HomeAssistant, init_components) -> ConfigEntry: """Initialize select entity.""" mock_platform(hass, "assist_pipeline.select", SelectPlatform()) - config_entry = MockConfigEntry(domain="assist_pipeline") + config_entry = MockConfigEntry( + domain="assist_pipeline", state=ConfigEntryState.LOADED + ) config_entry.add_to_hass(hass) - assert await hass.config_entries.async_forward_entry_setup(config_entry, "select") + await hass.config_entries.async_late_forward_entry_setups(config_entry, ["select"]) return config_entry @@ -123,13 +125,14 @@ async def test_select_entity_registering_device( async def test_select_entity_changing_pipelines( hass: HomeAssistant, - init_select: ConfigEntry, + init_select: MockConfigEntry, pipeline_1: Pipeline, pipeline_2: Pipeline, pipeline_storage: PipelineStorageCollection, ) -> None: """Test entity tracking pipeline changes.""" config_entry = init_select # nicer naming + config_entry.mock_state(hass, ConfigEntryState.LOADED) state = hass.states.get("select.assist_pipeline_test_prefix_pipeline") assert state is not None @@ -158,7 +161,7 @@ async def test_select_entity_changing_pipelines( # Reload config entry to test selected option persists assert await hass.config_entries.async_forward_entry_unload(config_entry, "select") - assert await hass.config_entries.async_forward_entry_setup(config_entry, "select") + await hass.config_entries.async_late_forward_entry_setups(config_entry, ["select"]) state = hass.states.get("select.assist_pipeline_test_prefix_pipeline") assert state is not None @@ -179,10 +182,11 @@ async def test_select_entity_changing_pipelines( async def test_select_entity_changing_vad_sensitivity( hass: HomeAssistant, - init_select: ConfigEntry, + init_select: MockConfigEntry, ) -> None: """Test entity tracking pipeline changes.""" config_entry = init_select # nicer naming + config_entry.mock_state(hass, ConfigEntryState.LOADED) state = hass.states.get("select.assist_pipeline_test_vad_sensitivity") assert state is not None @@ -205,7 +209,7 @@ async def test_select_entity_changing_vad_sensitivity( # Reload config entry to test selected option persists assert await hass.config_entries.async_forward_entry_unload(config_entry, "select") - assert await hass.config_entries.async_forward_entry_setup(config_entry, "select") + await hass.config_entries.async_late_forward_entry_setups(config_entry, ["select"]) state = hass.states.get("select.assist_pipeline_test_vad_sensitivity") assert state is not None diff --git a/tests/components/binary_sensor/test_init.py b/tests/components/binary_sensor/test_init.py index 335b9b40d50..63a921b4c3e 100644 --- a/tests/components/binary_sensor/test_init.py +++ b/tests/components/binary_sensor/test_init.py @@ -63,8 +63,8 @@ async def test_name(hass: HomeAssistant) -> None: hass: HomeAssistant, config_entry: ConfigEntry ) -> bool: """Set up test config entry.""" - await hass.config_entries.async_forward_entry_setup( - config_entry, binary_sensor.DOMAIN + await hass.config_entries.async_forward_entry_setups( + config_entry, [binary_sensor.DOMAIN] ) return True @@ -143,8 +143,8 @@ async def test_entity_category_config_raises_error( hass: HomeAssistant, config_entry: ConfigEntry ) -> bool: """Set up test config entry.""" - await hass.config_entries.async_forward_entry_setup( - config_entry, binary_sensor.DOMAIN + await hass.config_entries.async_forward_entry_setups( + config_entry, [binary_sensor.DOMAIN] ) return True diff --git a/tests/components/button/test_init.py b/tests/components/button/test_init.py index 0641bbe29dc..6cb2f1a5700 100644 --- a/tests/components/button/test_init.py +++ b/tests/components/button/test_init.py @@ -139,7 +139,7 @@ async def test_name(hass: HomeAssistant) -> None: hass: HomeAssistant, config_entry: ConfigEntry ) -> bool: """Set up test config entry.""" - await hass.config_entries.async_forward_entry_setup(config_entry, DOMAIN) + await hass.config_entries.async_forward_entry_setups(config_entry, [DOMAIN]) return True mock_platform(hass, f"{TEST_DOMAIN}.config_flow") diff --git a/tests/components/calendar/conftest.py b/tests/components/calendar/conftest.py index ba0064cb4e4..94a2e72e0f4 100644 --- a/tests/components/calendar/conftest.py +++ b/tests/components/calendar/conftest.py @@ -120,7 +120,7 @@ def mock_setup_integration( hass: HomeAssistant, config_entry: ConfigEntry ) -> bool: """Set up test config entry.""" - await hass.config_entries.async_forward_entry_setup(config_entry, DOMAIN) + await hass.config_entries.async_forward_entry_setups(config_entry, [DOMAIN]) return True async def async_unload_entry_init( diff --git a/tests/components/climate/test_intent.py b/tests/components/climate/test_intent.py index 1aaea386320..8e2ec09650c 100644 --- a/tests/components/climate/test_intent.py +++ b/tests/components/climate/test_intent.py @@ -50,7 +50,7 @@ def mock_setup_integration(hass: HomeAssistant) -> None: hass: HomeAssistant, config_entry: ConfigEntry ) -> bool: """Set up test config entry.""" - await hass.config_entries.async_forward_entry_setup(config_entry, DOMAIN) + await hass.config_entries.async_forward_entry_setups(config_entry, [DOMAIN]) return True async def async_unload_entry_init( diff --git a/tests/components/deconz/test_gateway.py b/tests/components/deconz/test_gateway.py index 5a55fb64090..610aea3b01b 100644 --- a/tests/components/deconz/test_gateway.py +++ b/tests/components/deconz/test_gateway.py @@ -141,6 +141,8 @@ async def test_gateway_setup( device_registry: dr.DeviceRegistry, ) -> None: """Successful setup.""" + # Patching async_forward_entry_setup* is not advisable, and should be refactored + # in the future. with patch( "homeassistant.config_entries.ConfigEntries.async_forward_entry_setups", return_value=True, @@ -190,8 +192,10 @@ async def test_gateway_device_configuration_url_when_addon( device_registry: dr.DeviceRegistry, ) -> None: """Successful setup.""" + # Patching async_forward_entry_setup* is not advisable, and should be refactored + # in the future. with patch( - "homeassistant.config_entries.ConfigEntries.async_forward_entry_setup", + "homeassistant.config_entries.ConfigEntries.async_forward_entry_setups", return_value=True, ): config_entry = await setup_deconz_integration( diff --git a/tests/components/devolo_home_network/test_init.py b/tests/components/devolo_home_network/test_init.py index c4a02f9e375..1b8903c568e 100644 --- a/tests/components/devolo_home_network/test_init.py +++ b/tests/components/devolo_home_network/test_init.py @@ -53,9 +53,11 @@ async def test_setup_without_password(hass: HomeAssistant) -> None: } entry = MockConfigEntry(domain=DOMAIN, data=config) entry.add_to_hass(hass) + # Patching async_forward_entry_setup* is not advisable, and should be refactored + # in the future. with ( patch( - "homeassistant.config_entries.ConfigEntries.async_forward_entry_setup", + "homeassistant.config_entries.ConfigEntries.async_forward_entry_setups", return_value=True, ), patch("homeassistant.core.EventBus.async_listen_once"), diff --git a/tests/components/esphome/test_update.py b/tests/components/esphome/test_update.py index b3deb2f33ee..50ca6104aa4 100644 --- a/tests/components/esphome/test_update.py +++ b/tests/components/esphome/test_update.py @@ -85,9 +85,8 @@ async def test_update_entity( "homeassistant.components.esphome.update.DomainData.get_entry_data", return_value=Mock(available=True, device_info=mock_device_info), ): - assert await hass.config_entries.async_forward_entry_setup( - mock_config_entry, "update" - ) + assert await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() state = hass.states.get("update.none_firmware") assert state is not None @@ -275,9 +274,8 @@ async def test_update_entity_dashboard_not_available_startup( ), ): await async_get_dashboard(hass).async_refresh() - assert await hass.config_entries.async_forward_entry_setup( - mock_config_entry, "update" - ) + assert await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() # We have a dashboard but it is not available state = hass.states.get("update.none_firmware") @@ -362,9 +360,8 @@ async def test_update_entity_not_present_without_dashboard( "homeassistant.components.esphome.update.DomainData.get_entry_data", return_value=Mock(available=True, device_info=mock_device_info), ): - assert await hass.config_entries.async_forward_entry_setup( - mock_config_entry, "update" - ) + assert await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() state = hass.states.get("update.none_firmware") assert state is None diff --git a/tests/components/event/test_init.py b/tests/components/event/test_init.py index fd3cf0eaf9b..8e3f1a8a932 100644 --- a/tests/components/event/test_init.py +++ b/tests/components/event/test_init.py @@ -254,7 +254,7 @@ async def test_name(hass: HomeAssistant) -> None: hass: HomeAssistant, config_entry: ConfigEntry ) -> bool: """Set up test config entry.""" - await hass.config_entries.async_forward_entry_setup(config_entry, DOMAIN) + await hass.config_entries.async_forward_entry_setups(config_entry, [DOMAIN]) return True mock_platform(hass, f"{TEST_DOMAIN}.config_flow") diff --git a/tests/components/homematicip_cloud/test_hap.py b/tests/components/homematicip_cloud/test_hap.py index 3cb8b7d61e9..2da32b2844d 100644 --- a/tests/components/homematicip_cloud/test_hap.py +++ b/tests/components/homematicip_cloud/test_hap.py @@ -88,7 +88,8 @@ async def test_hap_setup_works(hass: HomeAssistant) -> None: home = Mock() hap = HomematicipHAP(hass, entry) with patch.object(hap, "get_hap", return_value=home): - assert await hap.async_setup() + async with entry.setup_lock: + assert await hap.async_setup() assert hap.home is home @@ -96,14 +97,17 @@ async def test_hap_setup_works(hass: HomeAssistant) -> None: async def test_hap_setup_connection_error() -> None: """Test a failed accesspoint setup.""" hass = Mock() - entry = Mock() - entry.data = {HMIPC_HAPID: "ABC123", HMIPC_AUTHTOKEN: "123", HMIPC_NAME: "hmip"} + entry = MockConfigEntry( + domain=HMIPC_DOMAIN, + data={HMIPC_HAPID: "ABC123", HMIPC_AUTHTOKEN: "123", HMIPC_NAME: "hmip"}, + ) hap = HomematicipHAP(hass, entry) with ( patch.object(hap, "get_hap", side_effect=HmipcConnectionError), pytest.raises(ConfigEntryNotReady), ): - assert not await hap.async_setup() + async with entry.setup_lock: + assert not await hap.async_setup() assert not hass.async_run_hass_job.mock_calls assert not hass.config_entries.flow.async_init.mock_calls @@ -132,7 +136,8 @@ async def test_hap_create( hap = HomematicipHAP(hass, hmip_config_entry) assert hap with patch.object(hap, "async_connect"): - assert await hap.async_setup() + async with hmip_config_entry.setup_lock: + assert await hap.async_setup() async def test_hap_create_exception( diff --git a/tests/components/hue/conftest.py b/tests/components/hue/conftest.py index 39b860fadf2..dd27a657e2a 100644 --- a/tests/components/hue/conftest.py +++ b/tests/components/hue/conftest.py @@ -15,6 +15,7 @@ import pytest from homeassistant.components import hue from homeassistant.components.hue.v1 import sensor_base as hue_sensor_base from homeassistant.components.hue.v2.device import async_setup_devices +from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.setup import async_setup_component @@ -275,8 +276,8 @@ async def setup_platform( assert await async_setup_component(hass, hue.DOMAIN, {}) is True await hass.async_block_till_done() - for platform in platforms: - await hass.config_entries.async_forward_entry_setup(config_entry, platform) + config_entry.mock_state(hass, ConfigEntryState.LOADED) + await hass.config_entries.async_late_forward_entry_setups(config_entry, platforms) # and make sure it completes before going further await hass.async_block_till_done() diff --git a/tests/components/hue/test_bridge.py b/tests/components/hue/test_bridge.py index 5d103e47870..42631215035 100644 --- a/tests/components/hue/test_bridge.py +++ b/tests/components/hue/test_bridge.py @@ -34,7 +34,8 @@ async def test_bridge_setup_v1(hass: HomeAssistant, mock_api_v1) -> None: patch.object(hass.config_entries, "async_forward_entry_setups") as mock_forward, ): hue_bridge = bridge.HueBridge(hass, config_entry) - assert await hue_bridge.async_initialize_bridge() is True + async with config_entry.setup_lock: + assert await hue_bridge.async_initialize_bridge() is True assert hue_bridge.api is mock_api_v1 assert isinstance(hue_bridge.api, HueBridgeV1) @@ -125,7 +126,8 @@ async def test_reset_unloads_entry_if_setup(hass: HomeAssistant, mock_api_v1) -> patch.object(hass.config_entries, "async_forward_entry_setups") as mock_forward, ): hue_bridge = bridge.HueBridge(hass, config_entry) - assert await hue_bridge.async_initialize_bridge() is True + async with config_entry.setup_lock: + assert await hue_bridge.async_initialize_bridge() is True await asyncio.sleep(0) @@ -151,7 +153,8 @@ async def test_handle_unauthorized(hass: HomeAssistant, mock_api_v1) -> None: with patch.object(bridge, "HueBridgeV1", return_value=mock_api_v1): hue_bridge = bridge.HueBridge(hass, config_entry) - assert await hue_bridge.async_initialize_bridge() is True + async with config_entry.setup_lock: + assert await hue_bridge.async_initialize_bridge() is True with patch.object(bridge, "create_config_flow") as mock_create: await hue_bridge.handle_unauthorized_error() diff --git a/tests/components/hue/test_light_v1.py b/tests/components/hue/test_light_v1.py index 9a74d9cd994..3172e834954 100644 --- a/tests/components/hue/test_light_v1.py +++ b/tests/components/hue/test_light_v1.py @@ -186,7 +186,7 @@ async def setup_bridge(hass: HomeAssistant, mock_bridge_v1): config_entry.mock_state(hass, ConfigEntryState.LOADED) mock_bridge_v1.config_entry = config_entry hass.data[hue.DOMAIN] = {config_entry.entry_id: mock_bridge_v1} - await hass.config_entries.async_forward_entry_setup(config_entry, "light") + await hass.config_entries.async_late_forward_entry_setups(config_entry, ["light"]) # To flush out the service call to update the group await hass.async_block_till_done() diff --git a/tests/components/hue/test_sensor_v2.py b/tests/components/hue/test_sensor_v2.py index 4c1f8defc95..ae02c775191 100644 --- a/tests/components/hue/test_sensor_v2.py +++ b/tests/components/hue/test_sensor_v2.py @@ -75,7 +75,9 @@ async def test_enable_sensor( assert await async_setup_component(hass, hue.DOMAIN, {}) is True await hass.async_block_till_done() - await hass.config_entries.async_forward_entry_setup(mock_config_entry_v2, "sensor") + await hass.config_entries.async_late_forward_entry_setups( + mock_config_entry_v2, ["sensor"] + ) entity_id = "sensor.wall_switch_with_2_controls_zigbee_connectivity" entity_entry = entity_registry.async_get(entity_id) @@ -93,7 +95,9 @@ async def test_enable_sensor( # reload platform and check if entity is correctly there await hass.config_entries.async_forward_entry_unload(mock_config_entry_v2, "sensor") - await hass.config_entries.async_forward_entry_setup(mock_config_entry_v2, "sensor") + await hass.config_entries.async_late_forward_entry_setups( + mock_config_entry_v2, ["sensor"] + ) await hass.async_block_till_done() state = hass.states.get(entity_id) diff --git a/tests/components/image/conftest.py b/tests/components/image/conftest.py index 35c9f0a86af..4592ccf58d5 100644 --- a/tests/components/image/conftest.py +++ b/tests/components/image/conftest.py @@ -147,14 +147,16 @@ async def mock_image_config_entry_fixture( hass: HomeAssistant, config_entry: ConfigEntry ) -> bool: """Set up test config entry.""" - await hass.config_entries.async_forward_entry_setup(config_entry, image.DOMAIN) + await hass.config_entries.async_forward_entry_setups( + config_entry, [image.DOMAIN] + ) return True async def async_unload_entry_init( hass: HomeAssistant, config_entry: ConfigEntry ) -> bool: """Unload test config entry.""" - await hass.config_entries.async_forward_entry_unload(config_entry, image.DOMAIN) + await hass.config_entries.async_unload_platforms(config_entry, [image.DOMAIN]) return True mock_integration( diff --git a/tests/components/lawn_mower/test_init.py b/tests/components/lawn_mower/test_init.py index 87115cb1900..7dc59fb6f91 100644 --- a/tests/components/lawn_mower/test_init.py +++ b/tests/components/lawn_mower/test_init.py @@ -67,8 +67,8 @@ async def test_lawn_mower_setup(hass: HomeAssistant) -> None: hass: HomeAssistant, config_entry: ConfigEntry ) -> bool: """Set up test config entry.""" - await hass.config_entries.async_forward_entry_setup( - config_entry, Platform.LAWN_MOWER + await hass.config_entries.async_forward_entry_setups( + config_entry, [Platform.LAWN_MOWER] ) return True diff --git a/tests/components/lock/conftest.py b/tests/components/lock/conftest.py index 07399a39e92..9c0240b098a 100644 --- a/tests/components/lock/conftest.py +++ b/tests/components/lock/conftest.py @@ -98,7 +98,9 @@ async def setup_lock_platform_test_entity( hass: HomeAssistant, config_entry: ConfigEntry ) -> bool: """Set up test config entry.""" - await hass.config_entries.async_forward_entry_setup(config_entry, LOCK_DOMAIN) + await hass.config_entries.async_forward_entry_setups( + config_entry, [LOCK_DOMAIN] + ) return True MockPlatform(hass, f"{TEST_DOMAIN}.config_flow") diff --git a/tests/components/mobile_app/test_device_tracker.py b/tests/components/mobile_app/test_device_tracker.py index 21d4d80c791..52abe75f966 100644 --- a/tests/components/mobile_app/test_device_tracker.py +++ b/tests/components/mobile_app/test_device_tracker.py @@ -104,7 +104,9 @@ async def test_restoring_location( # mobile app doesn't support unloading, so we just reload device tracker await hass.config_entries.async_forward_entry_unload(config_entry, "device_tracker") - await hass.config_entries.async_forward_entry_setup(config_entry, "device_tracker") + await hass.config_entries.async_late_forward_entry_setups( + config_entry, ["device_tracker"] + ) await hass.async_block_till_done() state_2 = hass.states.get("device_tracker.test_1_2") diff --git a/tests/components/notify/test_init.py b/tests/components/notify/test_init.py index cfafae28b6e..0c559ad779f 100644 --- a/tests/components/notify/test_init.py +++ b/tests/components/notify/test_init.py @@ -56,7 +56,7 @@ async def help_async_setup_entry_init( hass: HomeAssistant, config_entry: ConfigEntry ) -> bool: """Set up test config entry.""" - await hass.config_entries.async_forward_entry_setup(config_entry, DOMAIN) + await hass.config_entries.async_forward_entry_setups(config_entry, [DOMAIN]) return True diff --git a/tests/components/number/test_init.py b/tests/components/number/test_init.py index 919c79403c4..1ca1264c53b 100644 --- a/tests/components/number/test_init.py +++ b/tests/components/number/test_init.py @@ -874,7 +874,7 @@ async def test_name(hass: HomeAssistant) -> None: hass: HomeAssistant, config_entry: ConfigEntry ) -> bool: """Set up test config entry.""" - await hass.config_entries.async_forward_entry_setup(config_entry, DOMAIN) + await hass.config_entries.async_forward_entry_setups(config_entry, [DOMAIN]) return True mock_platform(hass, f"{TEST_DOMAIN}.config_flow") diff --git a/tests/components/sensor/test_init.py b/tests/components/sensor/test_init.py index 100b7ec7186..8dc82483a40 100644 --- a/tests/components/sensor/test_init.py +++ b/tests/components/sensor/test_init.py @@ -2399,7 +2399,9 @@ async def test_name(hass: HomeAssistant) -> None: hass: HomeAssistant, config_entry: ConfigEntry ) -> bool: """Set up test config entry.""" - await hass.config_entries.async_forward_entry_setup(config_entry, SENSOR_DOMAIN) + await hass.config_entries.async_forward_entry_setups( + config_entry, [SENSOR_DOMAIN] + ) return True mock_platform(hass, f"{TEST_DOMAIN}.config_flow") diff --git a/tests/components/smartthings/conftest.py b/tests/components/smartthings/conftest.py index b638b9bbf4f..abe7657021c 100644 --- a/tests/components/smartthings/conftest.py +++ b/tests/components/smartthings/conftest.py @@ -39,7 +39,7 @@ from homeassistant.components.smartthings.const import ( STORAGE_VERSION, ) from homeassistant.config import async_process_ha_core_config -from homeassistant.config_entries import SOURCE_USER +from homeassistant.config_entries import SOURCE_USER, ConfigEntryState from homeassistant.const import ( CONF_ACCESS_TOKEN, CONF_CLIENT_ID, @@ -70,7 +70,8 @@ async def setup_platform(hass, platform: str, *, devices=None, scenes=None): ) hass.data[DOMAIN] = {DATA_BROKERS: {config_entry.entry_id: broker}} - await hass.config_entries.async_forward_entry_setup(config_entry, platform) + config_entry.mock_state(hass, ConfigEntryState.LOADED) + await hass.config_entries.async_late_forward_entry_setups(config_entry, [platform]) await hass.async_block_till_done() return config_entry diff --git a/tests/components/stt/test_init.py b/tests/components/stt/test_init.py index 165a520c653..9aa889f27c9 100644 --- a/tests/components/stt/test_init.py +++ b/tests/components/stt/test_init.py @@ -187,7 +187,7 @@ async def mock_config_entry_setup( hass: HomeAssistant, config_entry: ConfigEntry ) -> bool: """Set up test config entry.""" - await hass.config_entries.async_forward_entry_setup(config_entry, DOMAIN) + await hass.config_entries.async_forward_entry_setups(config_entry, [DOMAIN]) return True async def async_unload_entry_init( diff --git a/tests/components/todo/test_init.py b/tests/components/todo/test_init.py index 4b8e35c9061..44ebc785913 100644 --- a/tests/components/todo/test_init.py +++ b/tests/components/todo/test_init.py @@ -91,7 +91,7 @@ def mock_setup_integration(hass: HomeAssistant) -> None: hass: HomeAssistant, config_entry: ConfigEntry ) -> bool: """Set up test config entry.""" - await hass.config_entries.async_forward_entry_setup(config_entry, DOMAIN) + await hass.config_entries.async_forward_entry_setups(config_entry, [DOMAIN]) return True async def async_unload_entry_init( diff --git a/tests/components/tts/common.py b/tests/components/tts/common.py index 87a9993c72a..06712deea99 100644 --- a/tests/components/tts/common.py +++ b/tests/components/tts/common.py @@ -226,7 +226,7 @@ async def mock_config_entry_setup( hass: HomeAssistant, config_entry: ConfigEntry ) -> bool: """Set up test config entry.""" - await hass.config_entries.async_forward_entry_setup(config_entry, TTS_DOMAIN) + await hass.config_entries.async_forward_entry_setups(config_entry, [TTS_DOMAIN]) return True async def async_unload_entry_init( diff --git a/tests/components/update/test_init.py b/tests/components/update/test_init.py index 02ca605eed4..04e2e5c7076 100644 --- a/tests/components/update/test_init.py +++ b/tests/components/update/test_init.py @@ -782,7 +782,7 @@ async def test_name(hass: HomeAssistant) -> None: hass: HomeAssistant, config_entry: ConfigEntry ) -> bool: """Set up test config entry.""" - await hass.config_entries.async_forward_entry_setup(config_entry, DOMAIN) + await hass.config_entries.async_forward_entry_setups(config_entry, [DOMAIN]) return True mock_platform(hass, f"{TEST_DOMAIN}.config_flow") @@ -890,7 +890,7 @@ async def test_deprecated_supported_features_ints_with_service_call( hass: HomeAssistant, config_entry: ConfigEntry ) -> bool: """Set up test config entry.""" - await hass.config_entries.async_forward_entry_setup(config_entry, DOMAIN) + await hass.config_entries.async_forward_entry_setups(config_entry, [DOMAIN]) return True mock_platform(hass, f"{TEST_DOMAIN}.config_flow") diff --git a/tests/components/vacuum/__init__.py b/tests/components/vacuum/__init__.py index 98a02155b65..0a681730cb2 100644 --- a/tests/components/vacuum/__init__.py +++ b/tests/components/vacuum/__init__.py @@ -71,7 +71,7 @@ async def help_async_setup_entry_init( hass: HomeAssistant, config_entry: ConfigEntry ) -> bool: """Set up test config entry.""" - await hass.config_entries.async_forward_entry_setup(config_entry, DOMAIN) + await hass.config_entries.async_forward_entry_setups(config_entry, [DOMAIN]) return True diff --git a/tests/components/valve/test_init.py b/tests/components/valve/test_init.py index eee215d2e29..1f9f141d89f 100644 --- a/tests/components/valve/test_init.py +++ b/tests/components/valve/test_init.py @@ -152,8 +152,8 @@ def mock_config_entry(hass) -> tuple[MockConfigEntry, list[ValveEntity]]: hass: HomeAssistant, config_entry: ConfigEntry ) -> bool: """Set up test config entry.""" - await hass.config_entries.async_forward_entry_setup( - config_entry, Platform.VALVE + await hass.config_entries.async_forward_entry_setups( + config_entry, [Platform.VALVE] ) return True diff --git a/tests/components/wake_word/test_init.py b/tests/components/wake_word/test_init.py index 1e957ad7a2c..c4793653c9a 100644 --- a/tests/components/wake_word/test_init.py +++ b/tests/components/wake_word/test_init.py @@ -117,8 +117,8 @@ async def mock_config_entry_setup( hass: HomeAssistant, config_entry: ConfigEntry ) -> bool: """Set up test config entry.""" - await hass.config_entries.async_forward_entry_setup( - config_entry, wake_word.DOMAIN + await hass.config_entries.async_forward_entry_setups( + config_entry, [wake_word.DOMAIN] ) return True diff --git a/tests/ignore_uncaught_exceptions.py b/tests/ignore_uncaught_exceptions.py index aaf6cbe3efe..7be10571222 100644 --- a/tests/ignore_uncaught_exceptions.py +++ b/tests/ignore_uncaught_exceptions.py @@ -13,6 +13,12 @@ IGNORE_UNCAUGHT_EXCEPTIONS = [ "tests.helpers.test_event", "test_track_point_in_time_repr", ), + ( + # This test explicitly throws an uncaught exception + # and should not be removed. + "tests.test_config_entries", + "test_config_entry_unloaded_during_platform_setup", + ), ( "test_homeassistant_bridge", "test_homeassistant_bridge_fan_setup", diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index a88b6ad31c3..017bc5bff25 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -957,7 +957,9 @@ async def test_as_dict(snapshot: SnapshotAssertion) -> None: async def test_forward_entry_sets_up_component(hass: HomeAssistant) -> None: """Test we setup the component entry is forwarded to.""" - entry = MockConfigEntry(domain="original") + entry = MockConfigEntry( + domain="original", state=config_entries.ConfigEntryState.LOADED + ) mock_original_setup_entry = AsyncMock(return_value=True) integration = mock_integration( @@ -969,10 +971,10 @@ async def test_forward_entry_sets_up_component(hass: HomeAssistant) -> None: hass, MockModule("forwarded", async_setup_entry=mock_forwarded_setup_entry) ) - with patch.object(integration, "async_get_platform") as mock_async_get_platform: - await hass.config_entries.async_forward_entry_setup(entry, "forwarded") + with patch.object(integration, "async_get_platforms") as mock_async_get_platforms: + await hass.config_entries.async_late_forward_entry_setups(entry, ["forwarded"]) - mock_async_get_platform.assert_called_once_with("forwarded") + mock_async_get_platforms.assert_called_once_with(["forwarded"]) assert len(mock_original_setup_entry.mock_calls) == 0 assert len(mock_forwarded_setup_entry.mock_calls) == 1 @@ -981,7 +983,14 @@ async def test_forward_entry_does_not_setup_entry_if_setup_fails( hass: HomeAssistant, ) -> None: """Test we do not set up entry if component setup fails.""" - entry = MockConfigEntry(domain="original") + entry = MockConfigEntry( + domain="original", state=config_entries.ConfigEntryState.LOADED + ) + + mock_original_setup_entry = AsyncMock(return_value=True) + integration = mock_integration( + hass, MockModule("original", async_setup_entry=mock_original_setup_entry) + ) mock_setup = AsyncMock(return_value=False) mock_setup_entry = AsyncMock() @@ -992,11 +1001,64 @@ async def test_forward_entry_does_not_setup_entry_if_setup_fails( ), ) - await hass.config_entries.async_forward_entry_setup(entry, "forwarded") + with patch.object(integration, "async_get_platforms"): + await hass.config_entries.async_late_forward_entry_setups(entry, ["forwarded"]) assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 0 +async def test_async_forward_entry_setup_deprecated( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Test async_forward_entry_setup is deprecated.""" + entry = MockConfigEntry( + domain="original", state=config_entries.ConfigEntryState.LOADED + ) + + mock_original_setup_entry = AsyncMock(return_value=True) + integration = mock_integration( + hass, MockModule("original", async_setup_entry=mock_original_setup_entry) + ) + + mock_setup = AsyncMock(return_value=False) + mock_setup_entry = AsyncMock() + mock_integration( + hass, + MockModule( + "forwarded", async_setup=mock_setup, async_setup_entry=mock_setup_entry + ), + ) + + with patch.object(integration, "async_get_platforms"): + await hass.config_entries.async_forward_entry_setup(entry, "forwarded") + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 0 + entry_id = entry.entry_id + assert ( + "Detected code that calls async_forward_entry_setup after the entry " + "for integration, original with title: Mock Title and entry_id: " + f"{entry_id}, has been set up, without holding the setup lock that " + "prevents the config entry from being set up multiple times. " + "Instead await hass.config_entries.async_forward_entry_setup " + "during setup of the config entry or call " + "hass.config_entries.async_late_forward_entry_setups " + "in a tracked task. This will stop working in Home Assistant " + "2025.1. Please report this issue." + ) in caplog.text + + caplog.clear() + with patch.object(integration, "async_get_platforms"): + async with entry.setup_lock: + await hass.config_entries.async_forward_entry_setup(entry, "forwarded") + + assert ( + "Detected code that calls async_forward_entry_setup for integration, " + f"original with title: Mock Title and entry_id: {entry_id}, " + "which is deprecated and will stop working in Home Assistant 2025.6, " + "await async_forward_entry_setups instead. Please report this issue." + ) in caplog.text + + async def test_discovery_notification( hass: HomeAssistant, manager: config_entries.ConfigEntries ) -> None: @@ -5483,3 +5545,147 @@ async def test_raise_wrong_exception_in_forwarded_platform( f"Instead raise {exc_type_name} before calling async_forward_entry_setups" in caplog.text ) + + +async def test_non_awaited_async_forward_entry_setups( + hass: HomeAssistant, + manager: config_entries.ConfigEntries, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test async_forward_entry_setups not being awaited.""" + + async def mock_setup_entry( + hass: HomeAssistant, entry: config_entries.ConfigEntry + ) -> bool: + """Mock setting up entry.""" + # Call async_forward_entry_setups without awaiting it + # This is not allowed and will raise a warning + hass.async_create_task( + hass.config_entries.async_forward_entry_setups(entry, ["light"]) + ) + return True + + async def mock_unload_entry( + hass: HomeAssistant, entry: config_entries.ConfigEntry + ) -> bool: + """Mock unloading an entry.""" + result = await hass.config_entries.async_unload_platforms(entry, ["light"]) + assert result + return result + + mock_remove_entry = AsyncMock(return_value=None) + + async def mock_setup_entry_platform( + hass: HomeAssistant, + entry: config_entries.ConfigEntry, + async_add_entities: AddEntitiesCallback, + ) -> None: + """Mock setting up platform.""" + await asyncio.sleep(0) + + mock_integration( + hass, + MockModule( + "test", + async_setup_entry=mock_setup_entry, + async_unload_entry=mock_unload_entry, + async_remove_entry=mock_remove_entry, + ), + ) + mock_platform( + hass, "test.light", MockPlatform(async_setup_entry=mock_setup_entry_platform) + ) + mock_platform(hass, "test.config_flow", None) + + entry = MockConfigEntry(domain="test", entry_id="test2") + entry.add_to_manager(manager) + + # Setup entry + await manager.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert ( + "Detected code that calls async_forward_entry_setup after the " + "entry for integration, test with title: Mock Title and entry_id:" + " test2, has been set up, without holding the setup lock that " + "prevents the config entry from being set up multiple times. " + "Instead await hass.config_entries.async_forward_entry_setup " + "during setup of the config entry or call " + "hass.config_entries.async_late_forward_entry_setups " + "in a tracked task. This will stop working in Home Assistant" + " 2025.1. Please report this issue." + ) in caplog.text + + +async def test_config_entry_unloaded_during_platform_setup( + hass: HomeAssistant, + manager: config_entries.ConfigEntries, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test async_forward_entry_setups not being awaited.""" + task = None + + async def mock_setup_entry( + hass: HomeAssistant, entry: config_entries.ConfigEntry + ) -> bool: + """Mock setting up entry.""" + + # Call async_late_forward_entry_setups in a non-tracked task + # so we can unload the config entry during the setup + def _late_setup(): + nonlocal task + task = asyncio.create_task( + hass.config_entries.async_late_forward_entry_setups(entry, ["light"]) + ) + + hass.loop.call_soon(_late_setup) + return True + + async def mock_unload_entry( + hass: HomeAssistant, entry: config_entries.ConfigEntry + ) -> bool: + """Mock unloading an entry.""" + result = await hass.config_entries.async_unload_platforms(entry, ["light"]) + assert result + return result + + mock_remove_entry = AsyncMock(return_value=None) + + async def mock_setup_entry_platform( + hass: HomeAssistant, + entry: config_entries.ConfigEntry, + async_add_entities: AddEntitiesCallback, + ) -> None: + """Mock setting up platform.""" + await asyncio.sleep(0) + await asyncio.sleep(0) + + mock_integration( + hass, + MockModule( + "test", + async_setup_entry=mock_setup_entry, + async_unload_entry=mock_unload_entry, + async_remove_entry=mock_remove_entry, + ), + ) + mock_platform( + hass, "test.light", MockPlatform(async_setup_entry=mock_setup_entry_platform) + ) + mock_platform(hass, "test.config_flow", None) + + entry = MockConfigEntry(domain="test", entry_id="test2") + entry.add_to_manager(manager) + + # Setup entry + await manager.async_setup(entry.entry_id) + await hass.async_block_till_done() + await manager.async_unload(entry.entry_id) + await hass.async_block_till_done() + del task + + assert ( + "OperationNotAllowed: The config entry Mock Title (test) with " + "entry_id test2 cannot forward setup for ['light'] because it is " + "not loaded in the ConfigEntryState.NOT_LOADED state" + ) in caplog.text From 46bb9cb014ae1bd387748de21ecb09faa0d7ae76 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 5 Jun 2024 03:35:54 +0200 Subject: [PATCH 0233/1445] Fix capitalization of protocols in Reolink option flow (#118839) --- .../components/reolink/config_flow.py | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/reolink/config_flow.py b/homeassistant/components/reolink/config_flow.py index 773c4f3bc30..29da4a55ea1 100644 --- a/homeassistant/components/reolink/config_flow.py +++ b/homeassistant/components/reolink/config_flow.py @@ -25,7 +25,7 @@ from homeassistant.const import ( ) from homeassistant.core import callback from homeassistant.data_entry_flow import AbortFlow -from homeassistant.helpers import config_validation as cv +from homeassistant.helpers import config_validation as cv, selector from homeassistant.helpers.device_registry import format_mac from .const import CONF_USE_HTTPS, DOMAIN @@ -60,7 +60,24 @@ class ReolinkOptionsFlowHandler(OptionsFlow): vol.Required( CONF_PROTOCOL, default=self.config_entry.options[CONF_PROTOCOL], - ): vol.In(["rtsp", "rtmp", "flv"]), + ): selector.SelectSelector( + selector.SelectSelectorConfig( + options=[ + selector.SelectOptionDict( + value="rtsp", + label="RTSP", + ), + selector.SelectOptionDict( + value="rtmp", + label="RTMP", + ), + selector.SelectOptionDict( + value="flv", + label="FLV", + ), + ], + ), + ), } ), ) From 8723441227f52a8d2aa11cb6391f0ad635576425 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Wed, 5 Jun 2024 03:37:40 +0200 Subject: [PATCH 0234/1445] Add Reolink serial number to device info of IPC camera (#118834) * Add UID to dev info * Add camera_uid to test --- homeassistant/components/reolink/entity.py | 1 + tests/components/reolink/conftest.py | 1 + 2 files changed, 2 insertions(+) diff --git a/homeassistant/components/reolink/entity.py b/homeassistant/components/reolink/entity.py index 53a81f2b162..bf62c9cbeee 100644 --- a/homeassistant/components/reolink/entity.py +++ b/homeassistant/components/reolink/entity.py @@ -132,6 +132,7 @@ class ReolinkChannelCoordinatorEntity(ReolinkHostCoordinatorEntity): model=self._host.api.camera_model(dev_ch), manufacturer=self._host.api.manufacturer, sw_version=self._host.api.camera_sw_version(dev_ch), + serial_number=self._host.api.camera_uid(dev_ch), configuration_url=self._conf_url, ) diff --git a/tests/components/reolink/conftest.py b/tests/components/reolink/conftest.py index ba4e9615e8c..6cf88b9b00d 100644 --- a/tests/components/reolink/conftest.py +++ b/tests/components/reolink/conftest.py @@ -85,6 +85,7 @@ def reolink_connect_class() -> Generator[MagicMock, None, None]: host_mock.camera_model.return_value = TEST_CAM_MODEL host_mock.camera_name.return_value = TEST_NVR_NAME host_mock.camera_sw_version.return_value = "v1.1.0.0.0.0000" + host_mock.camera_uid.return_value = TEST_UID host_mock.session_active = True host_mock.timeout = 60 host_mock.renewtimer.return_value = 600 From 678c06beb37e5a1ee0679c8eff1cb2708c0118c2 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Wed, 5 Jun 2024 03:54:31 +0200 Subject: [PATCH 0235/1445] Conserve Reolink battery by not waking the camera on each update (#118773) * update to new cmd_list type * Wake battery cams each 1 hour * fix styling * fix epoch * fix timezone * force full update when using generic update service * improve comment * Use time.time() instead of datetime * fix import order --- homeassistant/components/reolink/entity.py | 5 +++++ homeassistant/components/reolink/host.py | 14 +++++++++++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/reolink/entity.py b/homeassistant/components/reolink/entity.py index bf62c9cbeee..309e5b54fe0 100644 --- a/homeassistant/components/reolink/entity.py +++ b/homeassistant/components/reolink/entity.py @@ -101,6 +101,11 @@ class ReolinkHostCoordinatorEntity(ReolinkBaseCoordinatorEntity[None]): await super().async_will_remove_from_hass() + async def async_update(self) -> None: + """Force full update from the generic entity update service.""" + self._host.last_wake = 0 + await super().async_update() + class ReolinkChannelCoordinatorEntity(ReolinkHostCoordinatorEntity): """Parent class for Reolink hardware camera entities connected to a channel of the NVR.""" diff --git a/homeassistant/components/reolink/host.py b/homeassistant/components/reolink/host.py index b1a1a9adf0f..e557eb1d60e 100644 --- a/homeassistant/components/reolink/host.py +++ b/homeassistant/components/reolink/host.py @@ -6,6 +6,7 @@ import asyncio from collections import defaultdict from collections.abc import Mapping import logging +from time import time from typing import Any, Literal import aiohttp @@ -40,6 +41,10 @@ POLL_INTERVAL_NO_PUSH = 5 LONG_POLL_COOLDOWN = 0.75 LONG_POLL_ERROR_COOLDOWN = 30 +# Conserve battery by not waking the battery cameras each minute during normal update +# Most props are cached in the Home Hub and updated, but some are skipped +BATTERY_WAKE_UPDATE_INTERVAL = 3600 # seconds + _LOGGER = logging.getLogger(__name__) @@ -68,6 +73,7 @@ class ReolinkHost: timeout=DEFAULT_TIMEOUT, ) + self.last_wake: float = 0 self._update_cmd: defaultdict[str, defaultdict[int | None, int]] = defaultdict( lambda: defaultdict(int) ) @@ -337,7 +343,13 @@ class ReolinkHost: async def update_states(self) -> None: """Call the API of the camera device to update the internal states.""" - await self._api.get_states(cmd_list=self._update_cmd) + wake = False + if time() - self.last_wake > BATTERY_WAKE_UPDATE_INTERVAL: + # wake the battery cameras for a complete update + wake = True + self.last_wake = time() + + await self._api.get_states(cmd_list=self._update_cmd, wake=wake) async def disconnect(self) -> None: """Disconnect from the API, so the connection will be released.""" From 72309364f5848acc24793a5b7156d66c589183da Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 4 Jun 2024 23:59:25 -0400 Subject: [PATCH 0236/1445] Fix the radio browser doing I/O in the event loop (#118842) --- homeassistant/components/radio_browser/media_source.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/radio_browser/media_source.py b/homeassistant/components/radio_browser/media_source.py index d23d09cce3a..2f95acf407d 100644 --- a/homeassistant/components/radio_browser/media_source.py +++ b/homeassistant/components/radio_browser/media_source.py @@ -5,6 +5,7 @@ from __future__ import annotations import mimetypes from radios import FilterBy, Order, RadioBrowser, Station +from radios.radio_browser import pycountry from homeassistant.components.media_player import MediaClass, MediaType from homeassistant.components.media_source.error import Unresolvable @@ -145,6 +146,8 @@ class RadioMediaSource(MediaSource): # We show country in the root additionally, when there is no item if not item.identifier or category == "country": + # Trigger the lazy loading of the country database to happen inside the executor + await self.hass.async_add_executor_job(lambda: len(pycountry.countries)) countries = await radios.countries(order=Order.NAME) return [ BrowseMediaSource( From ba7f82d5e2ac30edfacab53a250faf76f38fd25c Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Wed, 5 Jun 2024 07:55:49 +0100 Subject: [PATCH 0237/1445] Add diagnostic to V2C (#118823) * add diagnostic platform * add diagnostic platform * add diagnostic platform --- homeassistant/components/v2c/diagnostics.py | 35 +++++++++++++++++++ tests/components/v2c/conftest.py | 6 ++++ .../v2c/snapshots/test_diagnostics.ambr | 25 +++++++++++++ tests/components/v2c/test_diagnostics.py | 30 ++++++++++++++++ 4 files changed, 96 insertions(+) create mode 100644 homeassistant/components/v2c/diagnostics.py create mode 100644 tests/components/v2c/snapshots/test_diagnostics.ambr create mode 100644 tests/components/v2c/test_diagnostics.py diff --git a/homeassistant/components/v2c/diagnostics.py b/homeassistant/components/v2c/diagnostics.py new file mode 100644 index 00000000000..9f9df8723e0 --- /dev/null +++ b/homeassistant/components/v2c/diagnostics.py @@ -0,0 +1,35 @@ +"""Diagnostics support for V2C.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any + +from homeassistant.components.diagnostics import async_redact_data +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST +from homeassistant.core import HomeAssistant + +from .const import DOMAIN +from .coordinator import V2CUpdateCoordinator + +TO_REDACT = {CONF_HOST, "title"} + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, entry: ConfigEntry +) -> dict[str, Any]: + """Return diagnostics for a config entry.""" + coordinator: V2CUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + + if TYPE_CHECKING: + assert coordinator.evse + + coordinator_data = coordinator.evse.data + evse_raw_data = coordinator.evse.raw_data + + return { + "config_entry": async_redact_data(entry.as_dict(), TO_REDACT), + "data": str(coordinator_data), + "raw_data": evse_raw_data["content"].decode("utf-8"), # type: ignore[attr-defined] + "host_status": evse_raw_data["status_code"], + } diff --git a/tests/components/v2c/conftest.py b/tests/components/v2c/conftest.py index 87c11a3ceef..5dc8d96aab4 100644 --- a/tests/components/v2c/conftest.py +++ b/tests/components/v2c/conftest.py @@ -8,6 +8,7 @@ from pytrydan.models.trydan import TrydanData from homeassistant.components.v2c import DOMAIN from homeassistant.const import CONF_HOST +from homeassistant.helpers.json import json_dumps from tests.common import MockConfigEntry, load_json_object_fixture @@ -47,6 +48,11 @@ def mock_v2c_client() -> Generator[AsyncMock, None, None]: ): client = mock_client.return_value get_data_json = load_json_object_fixture("get_data.json", DOMAIN) + client.raw_data = { + "content": json_dumps(get_data_json).encode("utf-8"), + "status_code": 200, + } client.get_data.return_value = TrydanData.from_api(get_data_json) + client.data = client.get_data.return_value client.firmware_version = get_data_json["FirmwareVersion"] yield client diff --git a/tests/components/v2c/snapshots/test_diagnostics.ambr b/tests/components/v2c/snapshots/test_diagnostics.ambr new file mode 100644 index 00000000000..a4f6cad4cc8 --- /dev/null +++ b/tests/components/v2c/snapshots/test_diagnostics.ambr @@ -0,0 +1,25 @@ +# serializer version: 1 +# name: test_entry_diagnostics + dict({ + 'config_entry': dict({ + 'data': dict({ + 'host': '**REDACTED**', + }), + 'disabled_by': None, + 'domain': 'v2c', + 'entry_id': 'da58ee91f38c2406c2a36d0a1a7f8569', + 'minor_version': 1, + 'options': dict({ + }), + 'pref_disable_new_entities': False, + 'pref_disable_polling': False, + 'source': 'user', + 'title': '**REDACTED**', + 'unique_id': 'ABC123', + 'version': 1, + }), + 'data': "TrydanData(ID='ABC123', charge_state=, ready_state=, charge_power=1500.27, charge_energy=1.8, slave_error=, charge_time=4355, house_power=0.0, fv_power=0.0, battery_power=0.0, paused=, locked=, timer=, intensity=6, dynamic=, min_intensity=6, max_intensity=16, pause_dynamic=, dynamic_power_mode=, contracted_power=4600, firmware_version='2.1.7')", + 'host_status': 200, + 'raw_data': '{"ID":"ABC123","ChargeState":2,"ReadyState":0,"ChargePower":1500.27,"ChargeEnergy":1.8,"SlaveError":4,"ChargeTime":4355,"HousePower":0.0,"FVPower":0.0,"BatteryPower":0.0,"Paused":0,"Locked":0,"Timer":0,"Intensity":6,"Dynamic":0,"MinIntensity":6,"MaxIntensity":16,"PauseDynamic":0,"FirmwareVersion":"2.1.7","DynamicPowerMode":2,"ContractedPower":4600}', + }) +# --- diff --git a/tests/components/v2c/test_diagnostics.py b/tests/components/v2c/test_diagnostics.py new file mode 100644 index 00000000000..770b00e988b --- /dev/null +++ b/tests/components/v2c/test_diagnostics.py @@ -0,0 +1,30 @@ +"""Test V2C diagnostics.""" + +from unittest.mock import AsyncMock + +from syrupy import SnapshotAssertion + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +from . import init_integration + +from tests.components.diagnostics import get_diagnostics_for_config_entry +from tests.typing import ClientSessionGenerator + + +async def test_entry_diagnostics( + hass: HomeAssistant, + mock_config_entry: ConfigEntry, + mock_v2c_client: AsyncMock, + hass_client: ClientSessionGenerator, + snapshot: SnapshotAssertion, +) -> None: + """Test config entry diagnostics.""" + + await init_integration(hass, mock_config_entry) + + assert ( + await get_diagnostics_for_config_entry(hass, hass_client, mock_config_entry) + == snapshot() + ) From 357cc7d4cc5c6fc9c42b7aa8144a34d2ac435b7a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 5 Jun 2024 08:58:48 +0200 Subject: [PATCH 0238/1445] Bump github/codeql-action from 3.25.7 to 3.25.8 (#118850) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/codeql.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 9bb5417ec7c..0ad7747347d 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -24,11 +24,11 @@ jobs: uses: actions/checkout@v4.1.6 - name: Initialize CodeQL - uses: github/codeql-action/init@v3.25.7 + uses: github/codeql-action/init@v3.25.8 with: languages: python - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3.25.7 + uses: github/codeql-action/analyze@v3.25.8 with: category: "/language:python" From 985e42e50c2205f7dbaaac4d369d67bd9c97f758 Mon Sep 17 00:00:00 2001 From: Erwin Douna Date: Wed, 5 Jun 2024 09:05:31 +0200 Subject: [PATCH 0239/1445] Add more typing to DSMR Reader (#118852) --- homeassistant/components/dsmr_reader/definitions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/dsmr_reader/definitions.py b/homeassistant/components/dsmr_reader/definitions.py index e020be02e21..9003c4d4334 100644 --- a/homeassistant/components/dsmr_reader/definitions.py +++ b/homeassistant/components/dsmr_reader/definitions.py @@ -25,14 +25,14 @@ PRICE_EUR_KWH: Final = f"EUR/{UnitOfEnergy.KILO_WATT_HOUR}" PRICE_EUR_M3: Final = f"EUR/{UnitOfVolume.CUBIC_METERS}" -def dsmr_transform(value): +def dsmr_transform(value: str) -> float | str: """Transform DSMR version value to right format.""" if value.isdigit(): return float(value) / 10 return value -def tariff_transform(value): +def tariff_transform(value: str) -> str: """Transform tariff from number to description.""" if value == "1": return "low" From c7e065c413bc5f78284ed0eede7d4db3ad92522e Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 5 Jun 2024 09:20:08 +0200 Subject: [PATCH 0240/1445] Move enable_custom_integrations fixture to decorator (#118844) --- tests/components/analytics/test_analytics.py | 2 +- tests/components/button/test_init.py | 13 +++++------- .../components/config/test_config_entries.py | 18 +++++++--------- .../device_sun_light_trigger/test_init.py | 5 ++--- tests/components/diagnostics/test_init.py | 5 ++--- tests/components/group/test_switch.py | 7 ++++--- .../components/image_processing/test_init.py | 6 +++--- .../components/input_boolean/test_recorder.py | 8 +++---- .../components/input_button/test_recorder.py | 8 +++---- .../input_datetime/test_recorder.py | 8 +++---- .../components/input_number/test_recorder.py | 8 +++---- .../components/input_select/test_recorder.py | 8 +++---- tests/components/input_text/test_recorder.py | 8 +++---- tests/components/light/test_device_action.py | 4 ++-- .../components/light/test_device_condition.py | 4 ++-- tests/components/light/test_device_trigger.py | 6 +++--- tests/components/light/test_init.py | 2 +- tests/components/person/test_recorder.py | 6 +++--- tests/components/remote/test_device_action.py | 4 ++-- .../remote/test_device_condition.py | 6 +++--- .../components/remote/test_device_trigger.py | 6 +++--- tests/components/scene/test_init.py | 21 +++++++++---------- tests/components/schedule/test_recorder.py | 10 ++++----- .../sensor/test_device_condition.py | 10 ++++----- .../components/sensor/test_device_trigger.py | 12 +++++------ tests/components/sensor/test_recorder.py | 5 ++--- tests/components/switch/test_device_action.py | 4 ++-- .../switch/test_device_condition.py | 6 +++--- .../components/switch/test_device_trigger.py | 6 +++--- tests/components/switch/test_init.py | 11 +++++----- tests/components/trace/test_websocket_api.py | 4 ++-- tests/components/webhook/test_init.py | 2 +- 32 files changed, 112 insertions(+), 121 deletions(-) diff --git a/tests/components/analytics/test_analytics.py b/tests/components/analytics/test_analytics.py index 8b86c505517..60882cda874 100644 --- a/tests/components/analytics/test_analytics.py +++ b/tests/components/analytics/test_analytics.py @@ -566,10 +566,10 @@ async def test_reusing_uuid( assert analytics.uuid == "NOT_MOCK_UUID" +@pytest.mark.usefixtures("enable_custom_integrations") async def test_custom_integrations( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, - enable_custom_integrations: None, caplog: pytest.LogCaptureFixture, installation_type_mock: Generator[Any, Any, None], snapshot: SnapshotAssertion, diff --git a/tests/components/button/test_init.py b/tests/components/button/test_init.py index 6cb2f1a5700..02a320ea3fd 100644 --- a/tests/components/button/test_init.py +++ b/tests/components/button/test_init.py @@ -55,12 +55,11 @@ async def test_button(hass: HomeAssistant) -> None: assert button.press.called +@pytest.mark.usefixtures("enable_custom_integrations", "setup_platform") async def test_custom_integration( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, - enable_custom_integrations: None, freezer: FrozenDateTimeFactory, - setup_platform: None, ) -> None: """Test we integration.""" assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) @@ -95,9 +94,8 @@ async def test_custom_integration( assert hass.states.get("button.button_1").state == new_time_isoformat -async def test_restore_state( - hass: HomeAssistant, enable_custom_integrations: None, setup_platform: None -) -> None: +@pytest.mark.usefixtures("enable_custom_integrations", "setup_platform") +async def test_restore_state(hass: HomeAssistant) -> None: """Test we restore state integration.""" mock_restore_cache(hass, (State("button.button_1", "2021-01-01T23:59:59+00:00"),)) @@ -107,9 +105,8 @@ async def test_restore_state( assert hass.states.get("button.button_1").state == "2021-01-01T23:59:59+00:00" -async def test_restore_state_does_not_restore_unavailable( - hass: HomeAssistant, enable_custom_integrations: None, setup_platform: None -) -> None: +@pytest.mark.usefixtures("enable_custom_integrations", "setup_platform") +async def test_restore_state_does_not_restore_unavailable(hass: HomeAssistant) -> None: """Test we restore state integration except for unavailable.""" mock_restore_cache(hass, (State("button.button_1", STATE_UNAVAILABLE),)) diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py index 320bc91fae4..17cc7d8c6de 100644 --- a/tests/components/config/test_config_entries.py +++ b/tests/components/config/test_config_entries.py @@ -507,9 +507,8 @@ async def test_abort(hass: HomeAssistant, client) -> None: } -async def test_create_account( - hass: HomeAssistant, client, enable_custom_integrations: None -) -> None: +@pytest.mark.usefixtures("enable_custom_integrations") +async def test_create_account(hass: HomeAssistant, client) -> None: """Test a flow that creates an account.""" mock_platform(hass, "test.config_flow", None) @@ -566,9 +565,8 @@ async def test_create_account( } -async def test_two_step_flow( - hass: HomeAssistant, client, enable_custom_integrations: None -) -> None: +@pytest.mark.usefixtures("enable_custom_integrations") +async def test_two_step_flow(hass: HomeAssistant, client) -> None: """Test we can finish a two step flow.""" mock_integration( hass, MockModule("test", async_setup_entry=AsyncMock(return_value=True)) @@ -2227,9 +2225,8 @@ async def test_flow_with_multiple_schema_errors_base( } -async def test_supports_reconfigure( - hass: HomeAssistant, client, enable_custom_integrations: None -) -> None: +@pytest.mark.usefixtures("enable_custom_integrations") +async def test_supports_reconfigure(hass: HomeAssistant, client) -> None: """Test a flow that support reconfigure step.""" mock_platform(hass, "test.config_flow", None) @@ -2317,8 +2314,9 @@ async def test_supports_reconfigure( } +@pytest.mark.usefixtures("enable_custom_integrations") async def test_does_not_support_reconfigure( - hass: HomeAssistant, client: TestClient, enable_custom_integrations: None + hass: HomeAssistant, client: TestClient ) -> None: """Test a flow that does not support reconfigure step.""" mock_platform(hass, "test.config_flow", None) diff --git a/tests/components/device_sun_light_trigger/test_init.py b/tests/components/device_sun_light_trigger/test_init.py index 5f44593aabe..65afd5743f5 100644 --- a/tests/components/device_sun_light_trigger/test_init.py +++ b/tests/components/device_sun_light_trigger/test_init.py @@ -108,9 +108,8 @@ async def test_lights_on_when_sun_sets( ) -async def test_lights_turn_off_when_everyone_leaves( - hass: HomeAssistant, enable_custom_integrations: None -) -> None: +@pytest.mark.usefixtures("enable_custom_integrations") +async def test_lights_turn_off_when_everyone_leaves(hass: HomeAssistant) -> None: """Test lights turn off when everyone leaves the house.""" assert await async_setup_component( hass, "light", {light.DOMAIN: {CONF_PLATFORM: "test"}} diff --git a/tests/components/diagnostics/test_init.py b/tests/components/diagnostics/test_init.py index 85f0b8fe788..1189cc6a65d 100644 --- a/tests/components/diagnostics/test_init.py +++ b/tests/components/diagnostics/test_init.py @@ -80,10 +80,9 @@ async def test_websocket( } +@pytest.mark.usefixtures("enable_custom_integrations") async def test_download_diagnostics( - hass: HomeAssistant, - hass_client: ClientSessionGenerator, - enable_custom_integrations: None, + hass: HomeAssistant, hass_client: ClientSessionGenerator ) -> None: """Test download diagnostics.""" config_entry = MockConfigEntry(domain="fake_integration") diff --git a/tests/components/group/test_switch.py b/tests/components/group/test_switch.py index 32b21fcb0d7..4230a6ee86f 100644 --- a/tests/components/group/test_switch.py +++ b/tests/components/group/test_switch.py @@ -3,6 +3,8 @@ import asyncio from unittest.mock import patch +import pytest + from homeassistant import config as hass_config from homeassistant.components.group import DOMAIN, SERVICE_RELOAD from homeassistant.components.switch import ( @@ -232,9 +234,8 @@ async def test_state_reporting_all(hass: HomeAssistant) -> None: assert hass.states.get("switch.switch_group").state == STATE_UNAVAILABLE -async def test_service_calls( - hass: HomeAssistant, enable_custom_integrations: None -) -> None: +@pytest.mark.usefixtures("enable_custom_integrations") +async def test_service_calls(hass: HomeAssistant) -> None: """Test service calls.""" await async_setup_component( hass, diff --git a/tests/components/image_processing/test_init.py b/tests/components/image_processing/test_init.py index 2bc093ce9a9..577d3fc47db 100644 --- a/tests/components/image_processing/test_init.py +++ b/tests/components/image_processing/test_init.py @@ -89,11 +89,11 @@ async def test_setup_component_with_service(hass: HomeAssistant) -> None: "homeassistant.components.demo.camera.Path.read_bytes", return_value=b"Test", ) +@pytest.mark.usefixtures("enable_custom_integrations") async def test_get_image_from_camera( mock_camera_read, hass: HomeAssistant, aiohttp_unused_port_factory, - enable_custom_integrations: None, ) -> None: """Grab an image from camera entity.""" await setup_image_processing(hass, aiohttp_unused_port_factory) @@ -112,11 +112,11 @@ async def test_get_image_from_camera( "homeassistant.components.image_processing.async_get_image", side_effect=HomeAssistantError(), ) +@pytest.mark.usefixtures("enable_custom_integrations") async def test_get_image_without_exists_camera( mock_image, hass: HomeAssistant, aiohttp_unused_port_factory, - enable_custom_integrations: None, ) -> None: """Try to get image without exists camera.""" await setup_image_processing(hass, aiohttp_unused_port_factory) @@ -188,10 +188,10 @@ async def test_face_event_call_no_confidence( assert event_data[0]["entity_id"] == "image_processing.demo_face" +@pytest.mark.usefixtures("enable_custom_integrations") async def test_update_missing_camera( hass: HomeAssistant, aiohttp_unused_port_factory, - enable_custom_integrations: None, caplog: pytest.LogCaptureFixture, ) -> None: """Test when entity does not set camera.""" diff --git a/tests/components/input_boolean/test_recorder.py b/tests/components/input_boolean/test_recorder.py index 8f041d6c848..8e2f078a5e4 100644 --- a/tests/components/input_boolean/test_recorder.py +++ b/tests/components/input_boolean/test_recorder.py @@ -4,8 +4,9 @@ from __future__ import annotations from datetime import timedelta +import pytest + from homeassistant.components.input_boolean import DOMAIN -from homeassistant.components.recorder import Recorder from homeassistant.components.recorder.history import get_significant_states from homeassistant.const import ATTR_EDITABLE from homeassistant.core import HomeAssistant @@ -16,9 +17,8 @@ from tests.common import async_fire_time_changed from tests.components.recorder.common import async_wait_recording_done -async def test_exclude_attributes( - recorder_mock: Recorder, hass: HomeAssistant, enable_custom_integrations: None -) -> None: +@pytest.mark.usefixtures("recorder_mock", "enable_custom_integrations") +async def test_exclude_attributes(hass: HomeAssistant) -> None: """Test attributes to be excluded.""" now = dt_util.utcnow() assert await async_setup_component(hass, DOMAIN, {DOMAIN: {"test": {}}}) diff --git a/tests/components/input_button/test_recorder.py b/tests/components/input_button/test_recorder.py index 74023b73342..19ff8427dac 100644 --- a/tests/components/input_button/test_recorder.py +++ b/tests/components/input_button/test_recorder.py @@ -4,8 +4,9 @@ from __future__ import annotations from datetime import timedelta +import pytest + from homeassistant.components.input_button import DOMAIN -from homeassistant.components.recorder import Recorder from homeassistant.components.recorder.history import get_significant_states from homeassistant.const import ATTR_EDITABLE from homeassistant.core import HomeAssistant @@ -16,9 +17,8 @@ from tests.common import async_fire_time_changed from tests.components.recorder.common import async_wait_recording_done -async def test_exclude_attributes( - recorder_mock: Recorder, hass: HomeAssistant, enable_custom_integrations: None -) -> None: +@pytest.mark.usefixtures("recorder_mock", "enable_custom_integrations") +async def test_exclude_attributes(hass: HomeAssistant) -> None: """Test attributes to be excluded.""" now = dt_util.utcnow() assert await async_setup_component(hass, DOMAIN, {DOMAIN: {"test": {}}}) diff --git a/tests/components/input_datetime/test_recorder.py b/tests/components/input_datetime/test_recorder.py index d32e8ec3471..dafe1d5301b 100644 --- a/tests/components/input_datetime/test_recorder.py +++ b/tests/components/input_datetime/test_recorder.py @@ -4,8 +4,9 @@ from __future__ import annotations from datetime import timedelta +import pytest + from homeassistant.components.input_datetime import CONF_HAS_DATE, CONF_HAS_TIME, DOMAIN -from homeassistant.components.recorder import Recorder from homeassistant.components.recorder.history import get_significant_states from homeassistant.const import ATTR_EDITABLE from homeassistant.core import HomeAssistant @@ -16,9 +17,8 @@ from tests.common import async_fire_time_changed from tests.components.recorder.common import async_wait_recording_done -async def test_exclude_attributes( - recorder_mock: Recorder, hass: HomeAssistant, enable_custom_integrations: None -) -> None: +@pytest.mark.usefixtures("recorder_mock", "enable_custom_integrations") +async def test_exclude_attributes(hass: HomeAssistant) -> None: """Test attributes to be excluded.""" now = dt_util.utcnow() assert await async_setup_component( diff --git a/tests/components/input_number/test_recorder.py b/tests/components/input_number/test_recorder.py index 78f709511de..986f53e9311 100644 --- a/tests/components/input_number/test_recorder.py +++ b/tests/components/input_number/test_recorder.py @@ -4,6 +4,8 @@ from __future__ import annotations from datetime import timedelta +import pytest + from homeassistant.components.input_number import ( ATTR_MAX, ATTR_MIN, @@ -11,7 +13,6 @@ from homeassistant.components.input_number import ( ATTR_STEP, DOMAIN, ) -from homeassistant.components.recorder import Recorder from homeassistant.components.recorder.history import get_significant_states from homeassistant.const import ATTR_EDITABLE from homeassistant.core import HomeAssistant @@ -22,9 +23,8 @@ from tests.common import async_fire_time_changed from tests.components.recorder.common import async_wait_recording_done -async def test_exclude_attributes( - recorder_mock: Recorder, hass: HomeAssistant, enable_custom_integrations: None -) -> None: +@pytest.mark.usefixtures("recorder_mock", "enable_custom_integrations") +async def test_exclude_attributes(hass: HomeAssistant) -> None: """Test attributes to be excluded.""" now = dt_util.utcnow() assert await async_setup_component( diff --git a/tests/components/input_select/test_recorder.py b/tests/components/input_select/test_recorder.py index b12fe57d431..107608b7774 100644 --- a/tests/components/input_select/test_recorder.py +++ b/tests/components/input_select/test_recorder.py @@ -4,8 +4,9 @@ from __future__ import annotations from datetime import timedelta +import pytest + from homeassistant.components.input_select import ATTR_OPTIONS, DOMAIN -from homeassistant.components.recorder import Recorder from homeassistant.components.recorder.history import get_significant_states from homeassistant.const import ATTR_EDITABLE from homeassistant.core import HomeAssistant @@ -16,9 +17,8 @@ from tests.common import async_fire_time_changed from tests.components.recorder.common import async_wait_recording_done -async def test_exclude_attributes( - recorder_mock: Recorder, hass: HomeAssistant, enable_custom_integrations: None -) -> None: +@pytest.mark.usefixtures("recorder_mock", "enable_custom_integrations") +async def test_exclude_attributes(hass: HomeAssistant) -> None: """Test attributes to be excluded.""" now = dt_util.utcnow() assert await async_setup_component( diff --git a/tests/components/input_text/test_recorder.py b/tests/components/input_text/test_recorder.py index a81160b32c7..21309f0a8ab 100644 --- a/tests/components/input_text/test_recorder.py +++ b/tests/components/input_text/test_recorder.py @@ -4,6 +4,8 @@ from __future__ import annotations from datetime import timedelta +import pytest + from homeassistant.components.input_text import ( ATTR_MAX, ATTR_MIN, @@ -12,7 +14,6 @@ from homeassistant.components.input_text import ( DOMAIN, MODE_TEXT, ) -from homeassistant.components.recorder import Recorder from homeassistant.components.recorder.history import get_significant_states from homeassistant.const import ATTR_EDITABLE from homeassistant.core import HomeAssistant @@ -23,9 +24,8 @@ from tests.common import async_fire_time_changed from tests.components.recorder.common import async_wait_recording_done -async def test_exclude_attributes( - recorder_mock: Recorder, hass: HomeAssistant, enable_custom_integrations: None -) -> None: +@pytest.mark.usefixtures("recorder_mock", "enable_custom_integrations") +async def test_exclude_attributes(hass: HomeAssistant) -> None: """Test attributes to be excluded.""" now = dt_util.utcnow() assert await async_setup_component(hass, DOMAIN, {DOMAIN: {"test": {}}}) diff --git a/tests/components/light/test_device_action.py b/tests/components/light/test_device_action.py index 764321fe346..1013942f96b 100644 --- a/tests/components/light/test_device_action.py +++ b/tests/components/light/test_device_action.py @@ -466,12 +466,12 @@ async def test_get_action_capabilities_features_legacy( assert capabilities == expected +@pytest.mark.usefixtures("enable_custom_integrations") async def test_action( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, calls: list[ServiceCall], - enable_custom_integrations: None, ) -> None: """Test for turn_on and turn_off actions.""" config_entry = MockConfigEntry(domain="test", data={}) @@ -631,12 +631,12 @@ async def test_action( assert turn_on_calls[-1].data == {"entity_id": entry.entity_id, "flash": FLASH_LONG} +@pytest.mark.usefixtures("enable_custom_integrations") async def test_action_legacy( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, calls: list[ServiceCall], - enable_custom_integrations: None, ) -> None: """Test for turn_on and turn_off actions.""" config_entry = MockConfigEntry(domain="test", data={}) diff --git a/tests/components/light/test_device_condition.py b/tests/components/light/test_device_condition.py index a5459dd078d..01b735bd5af 100644 --- a/tests/components/light/test_device_condition.py +++ b/tests/components/light/test_device_condition.py @@ -180,12 +180,12 @@ async def test_get_condition_capabilities_legacy( assert capabilities == expected_capabilities +@pytest.mark.usefixtures("enable_custom_integrations") async def test_if_state( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, calls: list[ServiceCall], - enable_custom_integrations: None, ) -> None: """Test for turn_on and turn_off conditions.""" config_entry = MockConfigEntry(domain="test", data={}) @@ -267,12 +267,12 @@ async def test_if_state( assert calls[1].data["some"] == "is_off event - test_event2" +@pytest.mark.usefixtures("enable_custom_integrations") async def test_if_state_legacy( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, calls: list[ServiceCall], - enable_custom_integrations: None, ) -> None: """Test for turn_on and turn_off conditions.""" config_entry = MockConfigEntry(domain="test", data={}) diff --git a/tests/components/light/test_device_trigger.py b/tests/components/light/test_device_trigger.py index ca919fc9143..b61b69fef25 100644 --- a/tests/components/light/test_device_trigger.py +++ b/tests/components/light/test_device_trigger.py @@ -184,12 +184,12 @@ async def test_get_trigger_capabilities_legacy( assert capabilities == expected_capabilities +@pytest.mark.usefixtures("enable_custom_integrations") async def test_if_fires_on_state_change( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, calls: list[ServiceCall], - enable_custom_integrations: None, ) -> None: """Test for turn_on and turn_off triggers firing.""" config_entry = MockConfigEntry(domain="test", data={}) @@ -277,12 +277,12 @@ async def test_if_fires_on_state_change( } +@pytest.mark.usefixtures("enable_custom_integrations") async def test_if_fires_on_state_change_legacy( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, calls: list[ServiceCall], - enable_custom_integrations: None, ) -> None: """Test for turn_on and turn_off triggers firing.""" config_entry = MockConfigEntry(domain="test", data={}) @@ -331,12 +331,12 @@ async def test_if_fires_on_state_change_legacy( ) +@pytest.mark.usefixtures("enable_custom_integrations") async def test_if_fires_on_state_change_with_for( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, calls: list[ServiceCall], - enable_custom_integrations: None, ) -> None: """Test for triggers firing with delay.""" config_entry = MockConfigEntry(domain="test", data={}) diff --git a/tests/components/light/test_init.py b/tests/components/light/test_init.py index 6a04d5e33cc..6832b5812e2 100644 --- a/tests/components/light/test_init.py +++ b/tests/components/light/test_init.py @@ -980,9 +980,9 @@ async def test_light_brightness_step(hass: HomeAssistant) -> None: assert entity0.state == "off" # 126 - 126; brightness is 0, light should turn off +@pytest.mark.usefixtures("enable_custom_integrations") async def test_light_brightness_pct_conversion( hass: HomeAssistant, - enable_custom_integrations: None, mock_light_entities: list[MockLight], ) -> None: """Test that light brightness percent conversion.""" diff --git a/tests/components/person/test_recorder.py b/tests/components/person/test_recorder.py index 4d25ce7add4..5551a051df0 100644 --- a/tests/components/person/test_recorder.py +++ b/tests/components/person/test_recorder.py @@ -4,8 +4,9 @@ from __future__ import annotations from datetime import timedelta +import pytest + from homeassistant.components.person import ATTR_DEVICE_TRACKERS, DOMAIN -from homeassistant.components.recorder import Recorder from homeassistant.components.recorder.history import get_significant_states from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component @@ -15,10 +16,9 @@ from tests.common import MockUser, async_fire_time_changed from tests.components.recorder.common import async_wait_recording_done +@pytest.mark.usefixtures("recorder_mock", "enable_custom_integrations") async def test_exclude_attributes( - recorder_mock: Recorder, hass: HomeAssistant, - enable_custom_integrations: None, hass_admin_user: MockUser, storage_setup, ) -> None: diff --git a/tests/components/remote/test_device_action.py b/tests/components/remote/test_device_action.py index 9ee48009c11..e228810149c 100644 --- a/tests/components/remote/test_device_action.py +++ b/tests/components/remote/test_device_action.py @@ -109,12 +109,12 @@ async def test_get_actions_hidden_auxiliary( assert actions == unordered(expected_actions) +@pytest.mark.usefixtures("enable_custom_integrations") async def test_action( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, calls: list[ServiceCall], - enable_custom_integrations: None, ) -> None: """Test for turn_on and turn_off actions.""" config_entry = MockConfigEntry(domain="test", data={}) @@ -184,12 +184,12 @@ async def test_action( assert turn_on_calls[-1].data == {"entity_id": entry.entity_id} +@pytest.mark.usefixtures("enable_custom_integrations") async def test_action_legacy( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, calls: list[ServiceCall], - enable_custom_integrations: None, ) -> None: """Test for turn_on and turn_off actions.""" config_entry = MockConfigEntry(domain="test", data={}) diff --git a/tests/components/remote/test_device_condition.py b/tests/components/remote/test_device_condition.py index 3e8b331e02b..e0c5f6d862b 100644 --- a/tests/components/remote/test_device_condition.py +++ b/tests/components/remote/test_device_condition.py @@ -178,12 +178,12 @@ async def test_get_condition_capabilities_legacy( assert capabilities == expected_capabilities +@pytest.mark.usefixtures("enable_custom_integrations") async def test_if_state( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, calls: list[ServiceCall], - enable_custom_integrations: None, ) -> None: """Test for turn_on and turn_off conditions.""" config_entry = MockConfigEntry(domain="test", data={}) @@ -265,12 +265,12 @@ async def test_if_state( assert calls[1].data["some"] == "is_off event - test_event2" +@pytest.mark.usefixtures("enable_custom_integrations") async def test_if_state_legacy( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, calls: list[ServiceCall], - enable_custom_integrations: None, ) -> None: """Test for turn_on and turn_off conditions.""" config_entry = MockConfigEntry(domain="test", data={}) @@ -324,12 +324,12 @@ async def test_if_state_legacy( assert calls[0].data["some"] == "is_on event - test_event1" +@pytest.mark.usefixtures("enable_custom_integrations") async def test_if_fires_on_for_condition( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, calls: list[ServiceCall], - enable_custom_integrations: None, ) -> None: """Test for firing if condition is on with delay.""" point1 = dt_util.utcnow() diff --git a/tests/components/remote/test_device_trigger.py b/tests/components/remote/test_device_trigger.py index 8c0d6d01051..7e8f91a91dc 100644 --- a/tests/components/remote/test_device_trigger.py +++ b/tests/components/remote/test_device_trigger.py @@ -176,12 +176,12 @@ async def test_get_trigger_capabilities_legacy( assert capabilities == expected_capabilities +@pytest.mark.usefixtures("enable_custom_integrations") async def test_if_fires_on_state_change( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, calls: list[ServiceCall], - enable_custom_integrations: None, ) -> None: """Test for turn_on and turn_off triggers firing.""" config_entry = MockConfigEntry(domain="test", data={}) @@ -286,12 +286,12 @@ async def test_if_fires_on_state_change( } +@pytest.mark.usefixtures("enable_custom_integrations") async def test_if_fires_on_state_change_legacy( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, calls: list[ServiceCall], - enable_custom_integrations: None, ) -> None: """Test for turn_on and turn_off triggers firing.""" config_entry = MockConfigEntry(domain="test", data={}) @@ -346,12 +346,12 @@ async def test_if_fires_on_state_change_legacy( ) +@pytest.mark.usefixtures("enable_custom_integrations") async def test_if_fires_on_state_change_with_for( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, calls: list[ServiceCall], - enable_custom_integrations: None, ) -> None: """Test for triggers firing with delay.""" config_entry = MockConfigEntry(domain="test", data={}) diff --git a/tests/components/scene/test_init.py b/tests/components/scene/test_init.py index a878b27614e..5afdebda9da 100644 --- a/tests/components/scene/test_init.py +++ b/tests/components/scene/test_init.py @@ -37,8 +37,9 @@ def entities( return entities +@pytest.mark.usefixtures("enable_custom_integrations") async def test_config_yaml_alias_anchor( - hass: HomeAssistant, entities, enable_custom_integrations: None + hass: HomeAssistant, entities: list[MockLight] ) -> None: """Test the usage of YAML aliases and anchors. @@ -84,9 +85,8 @@ async def test_config_yaml_alias_anchor( assert light_2.last_call("turn_on")[1].get("brightness") == 100 -async def test_config_yaml_bool( - hass: HomeAssistant, entities, enable_custom_integrations: None -) -> None: +@pytest.mark.usefixtures("enable_custom_integrations") +async def test_config_yaml_bool(hass: HomeAssistant, entities: list[MockLight]) -> None: """Test parsing of booleans in yaml config.""" light_1, light_2 = await setup_lights(hass, entities) @@ -113,9 +113,8 @@ async def test_config_yaml_bool( assert light_2.last_call("turn_on")[1].get("brightness") == 100 -async def test_activate_scene( - hass: HomeAssistant, entities, enable_custom_integrations: None -) -> None: +@pytest.mark.usefixtures("enable_custom_integrations") +async def test_activate_scene(hass: HomeAssistant, entities: list[MockLight]) -> None: """Test active scene.""" light_1, light_2 = await setup_lights(hass, entities) @@ -167,9 +166,8 @@ async def test_activate_scene( assert calls[0].data.get("transition") == 42 -async def test_restore_state( - hass: HomeAssistant, entities, enable_custom_integrations: None -) -> None: +@pytest.mark.usefixtures("enable_custom_integrations") +async def test_restore_state(hass: HomeAssistant, entities: list[MockLight]) -> None: """Test we restore state integration.""" mock_restore_cache(hass, (State("scene.test", "2021-01-01T23:59:59+00:00"),)) @@ -195,8 +193,9 @@ async def test_restore_state( assert hass.states.get("scene.test").state == "2021-01-01T23:59:59+00:00" +@pytest.mark.usefixtures("enable_custom_integrations") async def test_restore_state_does_not_restore_unavailable( - hass: HomeAssistant, entities, enable_custom_integrations: None + hass: HomeAssistant, entities: list[MockLight] ) -> None: """Test we restore state integration but ignore unavailable.""" mock_restore_cache(hass, (State("scene.test", STATE_UNAVAILABLE),)) diff --git a/tests/components/schedule/test_recorder.py b/tests/components/schedule/test_recorder.py index df28730ee79..a7410472a44 100644 --- a/tests/components/schedule/test_recorder.py +++ b/tests/components/schedule/test_recorder.py @@ -4,7 +4,8 @@ from __future__ import annotations from datetime import timedelta -from homeassistant.components.recorder import Recorder +import pytest + from homeassistant.components.recorder.history import get_significant_states from homeassistant.components.schedule.const import ATTR_NEXT_EVENT, DOMAIN from homeassistant.const import ATTR_EDITABLE, ATTR_FRIENDLY_NAME, ATTR_ICON @@ -16,11 +17,8 @@ from tests.common import async_fire_time_changed from tests.components.recorder.common import async_wait_recording_done -async def test_exclude_attributes( - recorder_mock: Recorder, - hass: HomeAssistant, - enable_custom_integrations: None, -) -> None: +@pytest.mark.usefixtures("recorder_mock", "enable_custom_integrations") +async def test_exclude_attributes(hass: HomeAssistant) -> None: """Test attributes to be excluded.""" now = dt_util.utcnow() assert await async_setup_component( diff --git a/tests/components/sensor/test_device_condition.py b/tests/components/sensor/test_device_condition.py index 4c1f2010c12..02eaa2c9739 100644 --- a/tests/components/sensor/test_device_condition.py +++ b/tests/components/sensor/test_device_condition.py @@ -462,13 +462,13 @@ async def test_get_condition_capabilities_none( assert capabilities == expected_capabilities +@pytest.mark.usefixtures("enable_custom_integrations") async def test_if_state_not_above_below( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, calls: list[ServiceCall], caplog: pytest.LogCaptureFixture, - enable_custom_integrations: None, ) -> None: """Test for bad value conditions.""" config_entry = MockConfigEntry(domain="test", data={}) @@ -505,12 +505,12 @@ async def test_if_state_not_above_below( assert "must contain at least one of below, above" in caplog.text +@pytest.mark.usefixtures("enable_custom_integrations") async def test_if_state_above( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, calls: list[ServiceCall], - enable_custom_integrations: None, ) -> None: """Test for value conditions.""" config_entry = MockConfigEntry(domain="test", data={}) @@ -574,12 +574,12 @@ async def test_if_state_above( assert calls[0].data["some"] == "event - test_event1" +@pytest.mark.usefixtures("enable_custom_integrations") async def test_if_state_above_legacy( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, calls: list[ServiceCall], - enable_custom_integrations: None, ) -> None: """Test for value conditions.""" config_entry = MockConfigEntry(domain="test", data={}) @@ -643,12 +643,12 @@ async def test_if_state_above_legacy( assert calls[0].data["some"] == "event - test_event1" +@pytest.mark.usefixtures("enable_custom_integrations") async def test_if_state_below( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, calls: list[ServiceCall], - enable_custom_integrations: None, ) -> None: """Test for value conditions.""" config_entry = MockConfigEntry(domain="test", data={}) @@ -712,12 +712,12 @@ async def test_if_state_below( assert calls[0].data["some"] == "event - test_event1" +@pytest.mark.usefixtures("enable_custom_integrations") async def test_if_state_between( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, calls: list[ServiceCall], - enable_custom_integrations: None, ) -> None: """Test for value conditions.""" config_entry = MockConfigEntry(domain="test", data={}) diff --git a/tests/components/sensor/test_device_trigger.py b/tests/components/sensor/test_device_trigger.py index fe188d63078..c98fe1e3a52 100644 --- a/tests/components/sensor/test_device_trigger.py +++ b/tests/components/sensor/test_device_trigger.py @@ -419,13 +419,13 @@ async def test_get_trigger_capabilities_none( assert capabilities == expected_capabilities +@pytest.mark.usefixtures("enable_custom_integrations") async def test_if_fires_not_on_above_below( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, calls: list[ServiceCall], caplog: pytest.LogCaptureFixture, - enable_custom_integrations: None, ) -> None: """Test for value triggers firing.""" config_entry = MockConfigEntry(domain="test", data={}) @@ -459,12 +459,12 @@ async def test_if_fires_not_on_above_below( assert "must contain at least one of below, above" in caplog.text +@pytest.mark.usefixtures("enable_custom_integrations") async def test_if_fires_on_state_above( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, calls: list[ServiceCall], - enable_custom_integrations: None, ) -> None: """Test for value triggers firing.""" config_entry = MockConfigEntry(domain="test", data={}) @@ -524,12 +524,12 @@ async def test_if_fires_on_state_above( ) +@pytest.mark.usefixtures("enable_custom_integrations") async def test_if_fires_on_state_below( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, calls: list[ServiceCall], - enable_custom_integrations: None, ) -> None: """Test for value triggers firing.""" config_entry = MockConfigEntry(domain="test", data={}) @@ -589,12 +589,12 @@ async def test_if_fires_on_state_below( ) +@pytest.mark.usefixtures("enable_custom_integrations") async def test_if_fires_on_state_between( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, calls: list[ServiceCall], - enable_custom_integrations: None, ) -> None: """Test for value triggers firing.""" config_entry = MockConfigEntry(domain="test", data={}) @@ -666,12 +666,12 @@ async def test_if_fires_on_state_between( ) +@pytest.mark.usefixtures("enable_custom_integrations") async def test_if_fires_on_state_legacy( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, calls: list[ServiceCall], - enable_custom_integrations: None, ) -> None: """Test for value triggers firing.""" config_entry = MockConfigEntry(domain="test", data={}) @@ -731,12 +731,12 @@ async def test_if_fires_on_state_legacy( ) +@pytest.mark.usefixtures("enable_custom_integrations") async def test_if_fires_on_state_change_with_for( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, calls: list[ServiceCall], - enable_custom_integrations: None, ) -> None: """Test for triggers firing with delay.""" config_entry = MockConfigEntry(domain="test", data={}) diff --git a/tests/components/sensor/test_recorder.py b/tests/components/sensor/test_recorder.py index ec43d81fc4a..ea02674a8d1 100644 --- a/tests/components/sensor/test_recorder.py +++ b/tests/components/sensor/test_recorder.py @@ -5234,9 +5234,8 @@ async def async_record_states_partially_unavailable(hass, zero, entity_id, attri return four, states -async def test_exclude_attributes( - hass: HomeAssistant, enable_custom_integrations: None -) -> None: +@pytest.mark.usefixtures("enable_custom_integrations") +async def test_exclude_attributes(hass: HomeAssistant) -> None: """Test sensor attributes to be excluded.""" entity0 = MockSensor( has_entity_name=True, diff --git a/tests/components/switch/test_device_action.py b/tests/components/switch/test_device_action.py index 2a49dd99c90..ed3ff6f55ac 100644 --- a/tests/components/switch/test_device_action.py +++ b/tests/components/switch/test_device_action.py @@ -110,12 +110,12 @@ async def test_get_actions_hidden_auxiliary( assert actions == unordered(expected_actions) +@pytest.mark.usefixtures("enable_custom_integrations") async def test_action( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, calls: list[ServiceCall], - enable_custom_integrations: None, ) -> None: """Test for turn_on and turn_off actions.""" config_entry = MockConfigEntry(domain="test", data={}) @@ -185,12 +185,12 @@ async def test_action( assert turn_on_calls[-1].data == {"entity_id": entry.entity_id} +@pytest.mark.usefixtures("enable_custom_integrations") async def test_action_legacy( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, calls: list[ServiceCall], - enable_custom_integrations: None, ) -> None: """Test for turn_on and turn_off actions.""" config_entry = MockConfigEntry(domain="test", data={}) diff --git a/tests/components/switch/test_device_condition.py b/tests/components/switch/test_device_condition.py index df7f39b82fb..43a91b8628a 100644 --- a/tests/components/switch/test_device_condition.py +++ b/tests/components/switch/test_device_condition.py @@ -178,12 +178,12 @@ async def test_get_condition_capabilities_legacy( assert capabilities == expected_capabilities +@pytest.mark.usefixtures("enable_custom_integrations") async def test_if_state( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, calls: list[ServiceCall], - enable_custom_integrations: None, ) -> None: """Test for turn_on and turn_off conditions.""" config_entry = MockConfigEntry(domain="test", data={}) @@ -265,12 +265,12 @@ async def test_if_state( assert calls[1].data["some"] == "is_off event - test_event2" +@pytest.mark.usefixtures("enable_custom_integrations") async def test_if_state_legacy( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, calls: list[ServiceCall], - enable_custom_integrations: None, ) -> None: """Test for turn_on and turn_off conditions.""" config_entry = MockConfigEntry(domain="test", data={}) @@ -323,12 +323,12 @@ async def test_if_state_legacy( assert calls[0].data["some"] == "is_on event - test_event1" +@pytest.mark.usefixtures("enable_custom_integrations") async def test_if_fires_on_for_condition( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, calls: list[ServiceCall], - enable_custom_integrations: None, ) -> None: """Test for firing if condition is on with delay.""" point1 = dt_util.utcnow() diff --git a/tests/components/switch/test_device_trigger.py b/tests/components/switch/test_device_trigger.py index 5b210e9ae3f..96479ba1900 100644 --- a/tests/components/switch/test_device_trigger.py +++ b/tests/components/switch/test_device_trigger.py @@ -176,12 +176,12 @@ async def test_get_trigger_capabilities_legacy( assert capabilities == expected_capabilities +@pytest.mark.usefixtures("enable_custom_integrations") async def test_if_fires_on_state_change( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, calls: list[ServiceCall], - enable_custom_integrations: None, ) -> None: """Test for turn_on and turn_off triggers firing.""" config_entry = MockConfigEntry(domain="test", data={}) @@ -287,12 +287,12 @@ async def test_if_fires_on_state_change( } +@pytest.mark.usefixtures("enable_custom_integrations") async def test_if_fires_on_state_change_legacy( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, calls: list[ServiceCall], - enable_custom_integrations: None, ) -> None: """Test for turn_on and turn_off triggers firing.""" config_entry = MockConfigEntry(domain="test", data={}) @@ -348,12 +348,12 @@ async def test_if_fires_on_state_change_legacy( ) +@pytest.mark.usefixtures("enable_custom_integrations") async def test_if_fires_on_state_change_with_for( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, calls: list[ServiceCall], - enable_custom_integrations: None, ) -> None: """Test for triggers firing with delay.""" config_entry = MockConfigEntry(domain="test", data={}) diff --git a/tests/components/switch/test_init.py b/tests/components/switch/test_init.py index aa3e4ccce58..989b10c11d6 100644 --- a/tests/components/switch/test_init.py +++ b/tests/components/switch/test_init.py @@ -20,15 +20,16 @@ from tests.common import ( @pytest.fixture(autouse=True) -def entities(hass: HomeAssistant, mock_switch_entities: list[MockSwitch]): +def entities( + hass: HomeAssistant, mock_switch_entities: list[MockSwitch] +) -> list[MockSwitch]: """Initialize the test switch.""" setup_test_component_platform(hass, switch.DOMAIN, mock_switch_entities) return mock_switch_entities -async def test_methods( - hass: HomeAssistant, entities, enable_custom_integrations: None -) -> None: +@pytest.mark.usefixtures("enable_custom_integrations") +async def test_methods(hass: HomeAssistant, entities: list[MockSwitch]) -> None: """Test is_on, turn_on, turn_off methods.""" switch_1, switch_2, switch_3 = entities assert await async_setup_component( @@ -60,11 +61,11 @@ async def test_methods( assert switch.is_on(hass, switch_3.entity_id) +@pytest.mark.usefixtures("enable_custom_integrations") async def test_switch_context( hass: HomeAssistant, entities, hass_admin_user: MockUser, - enable_custom_integrations: None, ) -> None: """Test that switch context works.""" assert await async_setup_component(hass, "switch", {"switch": {"platform": "test"}}) diff --git a/tests/components/trace/test_websocket_api.py b/tests/components/trace/test_websocket_api.py index 91e651ba6e3..92ba2c67020 100644 --- a/tests/components/trace/test_websocket_api.py +++ b/tests/components/trace/test_websocket_api.py @@ -119,6 +119,7 @@ async def _assert_contexts(client, next_id, contexts, domain=None, item_id=None) ("script", "sequence", [set(), set()], [UNDEFINED, UNDEFINED], "id", []), ], ) +@pytest.mark.usefixtures("enable_custom_integrations") async def test_get_trace( hass: HomeAssistant, hass_storage: dict[str, Any], @@ -129,7 +130,6 @@ async def test_get_trace( trigger, context_key, condition_results, - enable_custom_integrations: None, ) -> None: """Test tracing a script or automation.""" await async_setup_component(hass, "homeassistant", {}) @@ -1573,10 +1573,10 @@ async def test_script_mode_2( assert trace["script_execution"] == "finished" +@pytest.mark.usefixtures("enable_custom_integrations") async def test_trace_blueprint_automation( hass: HomeAssistant, hass_ws_client: WebSocketGenerator, - enable_custom_integrations: None, ) -> None: """Test trace of blueprint automation.""" await async_setup_component(hass, "homeassistant", {}) diff --git a/tests/components/webhook/test_init.py b/tests/components/webhook/test_init.py index 826c65cf6bc..b3d309f1f24 100644 --- a/tests/components/webhook/test_init.py +++ b/tests/components/webhook/test_init.py @@ -249,11 +249,11 @@ async def test_webhook_local_only(hass: HomeAssistant, mock_client) -> None: assert len(hooks) == 1 +@pytest.mark.usefixtures("enable_custom_integrations") async def test_listing_webhook( hass: HomeAssistant, hass_ws_client: WebSocketGenerator, hass_access_token: str, - enable_custom_integrations: None, ) -> None: """Test unregistering a webhook.""" assert await async_setup_component(hass, "webhook", {}) From 9c8aa8456eb3eab0028989d112e4dbcbb3a5eee7 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 5 Jun 2024 09:20:48 +0200 Subject: [PATCH 0241/1445] Move enable_bluetooth fixture to decorator (#118849) --- .../private_ble_device/test_device_tracker.py | 33 ++++++++++--------- tests/components/ruuvitag_ble/test_sensor.py | 5 ++- 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/tests/components/private_ble_device/test_device_tracker.py b/tests/components/private_ble_device/test_device_tracker.py index b1952557316..8fd1f694d84 100644 --- a/tests/components/private_ble_device/test_device_tracker.py +++ b/tests/components/private_ble_device/test_device_tracker.py @@ -22,7 +22,8 @@ from . import ( from tests.components.bluetooth.test_advertisement_tracker import ONE_HOUR_SECONDS -async def test_tracker_created(hass: HomeAssistant, enable_bluetooth: None) -> None: +@pytest.mark.usefixtures("enable_bluetooth") +async def test_tracker_created(hass: HomeAssistant) -> None: """Test creating a tracker entity when no devices have been seen.""" await async_mock_config_entry(hass) @@ -31,9 +32,8 @@ async def test_tracker_created(hass: HomeAssistant, enable_bluetooth: None) -> N assert state.state == "not_home" -async def test_tracker_ignore_other_rpa( - hass: HomeAssistant, enable_bluetooth: None -) -> None: +@pytest.mark.usefixtures("enable_bluetooth") +async def test_tracker_ignore_other_rpa(hass: HomeAssistant) -> None: """Test that tracker ignores RPA's that don't match us.""" await async_mock_config_entry(hass) await async_inject_broadcast(hass, MAC_STATIC) @@ -43,9 +43,8 @@ async def test_tracker_ignore_other_rpa( assert state.state == "not_home" -async def test_tracker_already_home( - hass: HomeAssistant, enable_bluetooth: None -) -> None: +@pytest.mark.usefixtures("enable_bluetooth") +async def test_tracker_already_home(hass: HomeAssistant) -> None: """Test creating a tracker and the device was already discovered by HA.""" await async_inject_broadcast(hass, MAC_RPA_VALID_1) await async_mock_config_entry(hass) @@ -55,7 +54,8 @@ async def test_tracker_already_home( assert state.state == "home" -async def test_tracker_arrive_home(hass: HomeAssistant, enable_bluetooth: None) -> None: +@pytest.mark.usefixtures("enable_bluetooth") +async def test_tracker_arrive_home(hass: HomeAssistant) -> None: """Test transition from not_home to home.""" await async_mock_config_entry(hass) await async_inject_broadcast(hass, MAC_RPA_VALID_1, b"1") @@ -85,7 +85,8 @@ async def test_tracker_arrive_home(hass: HomeAssistant, enable_bluetooth: None) assert state.attributes["current_address"] == "40:01:02:0a:c4:a6" -async def test_tracker_isolation(hass: HomeAssistant, enable_bluetooth: None) -> None: +@pytest.mark.usefixtures("enable_bluetooth") +async def test_tracker_isolation(hass: HomeAssistant) -> None: """Test creating 2 tracker entities doesn't confuse anything.""" await async_mock_config_entry(hass) await async_mock_config_entry(hass, irk="1" * 32) @@ -102,7 +103,8 @@ async def test_tracker_isolation(hass: HomeAssistant, enable_bluetooth: None) -> assert state.state == "not_home" -async def test_tracker_mac_rotate(hass: HomeAssistant, enable_bluetooth: None) -> None: +@pytest.mark.usefixtures("enable_bluetooth") +async def test_tracker_mac_rotate(hass: HomeAssistant) -> None: """Test MAC address rotation.""" await async_inject_broadcast(hass, MAC_RPA_VALID_1) await async_mock_config_entry(hass) @@ -119,7 +121,8 @@ async def test_tracker_mac_rotate(hass: HomeAssistant, enable_bluetooth: None) - assert state.attributes["current_address"] == MAC_RPA_VALID_2 -async def test_tracker_start_stale(hass: HomeAssistant, enable_bluetooth: None) -> None: +@pytest.mark.usefixtures("enable_bluetooth") +async def test_tracker_start_stale(hass: HomeAssistant) -> None: """Test edge case where we find an existing stale record, and it expires before we see any more.""" time.monotonic() @@ -138,7 +141,8 @@ async def test_tracker_start_stale(hass: HomeAssistant, enable_bluetooth: None) assert state.state == "not_home" -async def test_tracker_leave_home(hass: HomeAssistant, enable_bluetooth: None) -> None: +@pytest.mark.usefixtures("enable_bluetooth") +async def test_tracker_leave_home(hass: HomeAssistant) -> None: """Test tracker notices we have left.""" time.monotonic() @@ -157,9 +161,8 @@ async def test_tracker_leave_home(hass: HomeAssistant, enable_bluetooth: None) - assert state.state == "not_home" -async def test_old_tracker_leave_home( - hass: HomeAssistant, enable_bluetooth: None -) -> None: +@pytest.mark.usefixtures("enable_bluetooth") +async def test_old_tracker_leave_home(hass: HomeAssistant) -> None: """Test tracker ignores an old stale mac address timing out.""" start_time = time.monotonic() diff --git a/tests/components/ruuvitag_ble/test_sensor.py b/tests/components/ruuvitag_ble/test_sensor.py index 12cf0a4c0d6..c33e0453c53 100644 --- a/tests/components/ruuvitag_ble/test_sensor.py +++ b/tests/components/ruuvitag_ble/test_sensor.py @@ -2,6 +2,8 @@ from __future__ import annotations +import pytest + from homeassistant.components.ruuvitag_ble.const import DOMAIN from homeassistant.components.sensor import ATTR_STATE_CLASS from homeassistant.const import ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT @@ -13,7 +15,8 @@ from tests.common import MockConfigEntry from tests.components.bluetooth import inject_bluetooth_service_info -async def test_sensors(enable_bluetooth: None, hass: HomeAssistant) -> None: +@pytest.mark.usefixtures("enable_bluetooth") +async def test_sensors(hass: HomeAssistant) -> None: """Test the RuuviTag BLE sensors.""" entry = MockConfigEntry(domain=DOMAIN, unique_id=RUUVITAG_SERVICE_INFO.address) entry.add_to_hass(hass) From adc21e7c55b047b1fce97bcd8153893a6689b77b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ethem=20Cem=20=C3=96zkan?= Date: Wed, 5 Jun 2024 10:22:05 +0200 Subject: [PATCH 0242/1445] Bump python-roborock to 2.2.3 (#118853) Co-authored-by: G Johansson --- homeassistant/components/roborock/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/roborock/manifest.json b/homeassistant/components/roborock/manifest.json index 69dea8d0c25..3fd6dd7d782 100644 --- a/homeassistant/components/roborock/manifest.json +++ b/homeassistant/components/roborock/manifest.json @@ -7,7 +7,7 @@ "iot_class": "local_polling", "loggers": ["roborock"], "requirements": [ - "python-roborock==2.2.2", + "python-roborock==2.2.3", "vacuum-map-parser-roborock==0.1.2" ] } diff --git a/requirements_all.txt b/requirements_all.txt index 9e47da0b200..1c55d0536d7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2309,7 +2309,7 @@ python-rabbitair==0.0.8 python-ripple-api==0.0.3 # homeassistant.components.roborock -python-roborock==2.2.2 +python-roborock==2.2.3 # homeassistant.components.smarttub python-smarttub==0.0.36 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index be53e018379..b6039ed2ea9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1800,7 +1800,7 @@ python-qbittorrent==0.4.3 python-rabbitair==0.0.8 # homeassistant.components.roborock -python-roborock==2.2.2 +python-roborock==2.2.3 # homeassistant.components.smarttub python-smarttub==0.0.36 From 9a510cfe321feea11dc6230d02dbaf7c6e8e174c Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Wed, 5 Jun 2024 10:45:01 +0200 Subject: [PATCH 0243/1445] Add data coordinator to incomfort integration (#118816) * Add data coordinator to incomfort integration * Remove unused code and redundant comment, move entity class * Use freezer * Cleanup snapshot * Use entry.runtime_data * Use freezer, use mock_config_entry * Use tick * Use ConfigEntryError while we do not yet support a re-auth flow, update tests * Use tick with async_fire_time_changed --- .../components/incomfort/__init__.py | 35 ++------- .../components/incomfort/binary_sensor.py | 23 +++--- homeassistant/components/incomfort/climate.py | 29 ++++--- .../components/incomfort/config_flow.py | 2 +- .../components/incomfort/coordinator.py | 75 ++++++++++++++++++ homeassistant/components/incomfort/entity.py | 11 +++ homeassistant/components/incomfort/models.py | 40 ---------- homeassistant/components/incomfort/sensor.py | 21 ++--- .../components/incomfort/water_heater.py | 36 +++------ tests/components/incomfort/conftest.py | 2 +- tests/components/incomfort/test_init.py | 78 ++++++++++++++++++- 11 files changed, 219 insertions(+), 133 deletions(-) create mode 100644 homeassistant/components/incomfort/coordinator.py create mode 100644 homeassistant/components/incomfort/entity.py delete mode 100644 homeassistant/components/incomfort/models.py diff --git a/homeassistant/components/incomfort/__init__.py b/homeassistant/components/incomfort/__init__.py index c6d479cafb5..39e471b7614 100644 --- a/homeassistant/components/incomfort/__init__.py +++ b/homeassistant/components/incomfort/__init__.py @@ -8,17 +8,15 @@ import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, Platform -from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant, callback +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant from homeassistant.data_entry_flow import FlowResultType from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers import config_validation as cv, issue_registry as ir -from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import ConfigType from .const import DOMAIN +from .coordinator import InComfortDataCoordinator, async_connect_gateway from .errors import InConfortTimeout, InConfortUnknownError, NoHeaters, NotFound -from .models import DATA_INCOMFORT, async_connect_gateway CONFIG_SCHEMA = vol.Schema( { @@ -42,6 +40,8 @@ PLATFORMS = ( INTEGRATION_TITLE = "Intergas InComfort/Intouch Lan2RF gateway" +type InComfortConfigEntry = ConfigEntry[InComfortDataCoordinator] + async def _async_import(hass: HomeAssistant, config: ConfigType) -> None: """Import config entry from configuration.yaml.""" @@ -108,7 +108,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: except TimeoutError as exc: raise InConfortTimeout from exc - hass.data.setdefault(DATA_INCOMFORT, {entry.entry_id: data}) + coordinator = InComfortDataCoordinator(hass, data) + entry.runtime_data = coordinator + await coordinator.async_config_entry_first_refresh() await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True @@ -116,25 +118,4 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload config entry.""" - unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - del hass.data[DOMAIN][entry.entry_id] - return unload_ok - - -class IncomfortEntity(Entity): - """Base class for all InComfort entities.""" - - _attr_should_poll = False - _attr_has_entity_name = True - - async def async_added_to_hass(self) -> None: - """Set up a listener when this entity is added to HA.""" - self.async_on_remove( - async_dispatcher_connect( - self.hass, f"{DOMAIN}_{self.unique_id}", self._refresh - ) - ) - - @callback - def _refresh(self) -> None: - self.async_schedule_update_ha_state(force_refresh=True) + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/incomfort/binary_sensor.py b/homeassistant/components/incomfort/binary_sensor.py index f60ce2f4b59..238f1812aa2 100644 --- a/homeassistant/components/incomfort/binary_sensor.py +++ b/homeassistant/components/incomfort/binary_sensor.py @@ -4,28 +4,28 @@ from __future__ import annotations from typing import Any -from incomfortclient import Gateway as InComfortGateway, Heater as InComfortHeater +from incomfortclient import Heater as InComfortHeater from homeassistant.components.binary_sensor import BinarySensorEntity -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import DATA_INCOMFORT, IncomfortEntity +from . import InComfortConfigEntry from .const import DOMAIN +from .coordinator import InComfortDataCoordinator +from .entity import IncomfortEntity async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: InComfortConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up an InComfort/InTouch binary_sensor entity.""" - incomfort_data = hass.data[DATA_INCOMFORT][entry.entry_id] - async_add_entities( - IncomfortFailed(incomfort_data.client, h) for h in incomfort_data.heaters - ) + incomfort_coordinator = entry.runtime_data + heaters = incomfort_coordinator.data.heaters + async_add_entities(IncomfortFailed(incomfort_coordinator, h) for h in heaters) class IncomfortFailed(IncomfortEntity, BinarySensorEntity): @@ -33,11 +33,12 @@ class IncomfortFailed(IncomfortEntity, BinarySensorEntity): _attr_name = "Fault" - def __init__(self, client: InComfortGateway, heater: InComfortHeater) -> None: + def __init__( + self, coordinator: InComfortDataCoordinator, heater: InComfortHeater + ) -> None: """Initialize the binary sensor.""" - super().__init__() + super().__init__(coordinator) - self._client = client self._heater = heater self._attr_unique_id = f"{heater.serial_no}_failed" diff --git a/homeassistant/components/incomfort/climate.py b/homeassistant/components/incomfort/climate.py index f1487716d01..7e5cbd08f18 100644 --- a/homeassistant/components/incomfort/climate.py +++ b/homeassistant/components/incomfort/climate.py @@ -4,38 +4,34 @@ from __future__ import annotations from typing import Any -from incomfortclient import ( - Gateway as InComfortGateway, - Heater as InComfortHeater, - Room as InComfortRoom, -) +from incomfortclient import Heater as InComfortHeater, Room as InComfortRoom from homeassistant.components.climate import ( ClimateEntity, ClimateEntityFeature, HVACMode, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import DATA_INCOMFORT, IncomfortEntity +from . import InComfortConfigEntry from .const import DOMAIN +from .coordinator import InComfortDataCoordinator +from .entity import IncomfortEntity async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: InComfortConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up InComfort/InTouch climate devices.""" - incomfort_data = hass.data[DATA_INCOMFORT][entry.entry_id] + incomfort_coordinator = entry.runtime_data + heaters = incomfort_coordinator.data.heaters async_add_entities( - InComfortClimate(incomfort_data.client, h, r) - for h in incomfort_data.heaters - for r in h.rooms + InComfortClimate(incomfort_coordinator, h, r) for h in heaters for r in h.rooms ) @@ -52,12 +48,14 @@ class InComfortClimate(IncomfortEntity, ClimateEntity): _enable_turn_on_off_backwards_compatibility = False def __init__( - self, client: InComfortGateway, heater: InComfortHeater, room: InComfortRoom + self, + coordinator: InComfortDataCoordinator, + heater: InComfortHeater, + room: InComfortRoom, ) -> None: """Initialize the climate device.""" - super().__init__() + super().__init__(coordinator) - self._client = client self._room = room self._attr_unique_id = f"{heater.serial_no}_{room.room_no}" @@ -86,6 +84,7 @@ class InComfortClimate(IncomfortEntity, ClimateEntity): """Set a new target temperature for this zone.""" temperature = kwargs.get(ATTR_TEMPERATURE) await self._room.set_override(temperature) + await self.coordinator.async_refresh() async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set new target hvac mode.""" diff --git a/homeassistant/components/incomfort/config_flow.py b/homeassistant/components/incomfort/config_flow.py index bc928997b32..e905f0d743d 100644 --- a/homeassistant/components/incomfort/config_flow.py +++ b/homeassistant/components/incomfort/config_flow.py @@ -16,7 +16,7 @@ from homeassistant.helpers.selector import ( ) from .const import DOMAIN -from .models import async_connect_gateway +from .coordinator import async_connect_gateway TITLE = "Intergas InComfort/Intouch Lan2RF gateway" diff --git a/homeassistant/components/incomfort/coordinator.py b/homeassistant/components/incomfort/coordinator.py new file mode 100644 index 00000000000..a5c8da0c208 --- /dev/null +++ b/homeassistant/components/incomfort/coordinator.py @@ -0,0 +1,75 @@ +"""Datacoordinator for InComfort integration.""" + +from dataclasses import dataclass, field +from datetime import timedelta +import logging +from typing import Any + +from aiohttp import ClientResponseError +from incomfortclient import ( + Gateway as InComfortGateway, + Heater as InComfortHeater, + IncomfortError, +) + +from homeassistant.const import CONF_HOST +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryError +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +_LOGGER = logging.getLogger(__name__) + +UPDATE_INTERVAL = 30 + + +@dataclass +class InComfortData: + """Keep the Intergas InComfort entry data.""" + + client: InComfortGateway + heaters: list[InComfortHeater] = field(default_factory=list) + + +async def async_connect_gateway( + hass: HomeAssistant, + entry_data: dict[str, Any], +) -> InComfortData: + """Validate the configuration.""" + credentials = dict(entry_data) + hostname = credentials.pop(CONF_HOST) + + client = InComfortGateway( + hostname, **credentials, session=async_get_clientsession(hass) + ) + heaters = await client.heaters() + + return InComfortData(client=client, heaters=heaters) + + +class InComfortDataCoordinator(DataUpdateCoordinator[InComfortData]): + """Data coordinator for InComfort entities.""" + + def __init__(self, hass: HomeAssistant, incomfort_data: InComfortData) -> None: + """Initialize coordinator.""" + super().__init__( + hass, + _LOGGER, + name="InComfort datacoordinator", + update_interval=timedelta(seconds=UPDATE_INTERVAL), + ) + self.incomfort_data = incomfort_data + + async def _async_update_data(self) -> InComfortData: + """Fetch data from API endpoint.""" + try: + for heater in self.incomfort_data.heaters: + await heater.update() + except TimeoutError as exc: + raise UpdateFailed from exc + except IncomfortError as exc: + if isinstance(exc.message, ClientResponseError): + if exc.message.status == 401: + raise ConfigEntryError("Incorrect credentials") from exc + raise UpdateFailed from exc + return self.incomfort_data diff --git a/homeassistant/components/incomfort/entity.py b/homeassistant/components/incomfort/entity.py new file mode 100644 index 00000000000..7b4a535bff6 --- /dev/null +++ b/homeassistant/components/incomfort/entity.py @@ -0,0 +1,11 @@ +"""Common entity classes for InComfort integration.""" + +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .coordinator import InComfortDataCoordinator + + +class IncomfortEntity(CoordinatorEntity[InComfortDataCoordinator]): + """Base class for all InComfort entities.""" + + _attr_has_entity_name = True diff --git a/homeassistant/components/incomfort/models.py b/homeassistant/components/incomfort/models.py deleted file mode 100644 index 19e4269e0b4..00000000000 --- a/homeassistant/components/incomfort/models.py +++ /dev/null @@ -1,40 +0,0 @@ -"""Models for Intergas InComfort integration.""" - -from dataclasses import dataclass, field -from typing import Any - -from incomfortclient import Gateway as InComfortGateway, Heater as InComfortHeater - -from homeassistant.const import CONF_HOST -from homeassistant.core import HomeAssistant -from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.util.hass_dict import HassKey - -from .const import DOMAIN - - -@dataclass -class InComfortData: - """Keep the Intergas InComfort entry data.""" - - client: InComfortGateway - heaters: list[InComfortHeater] = field(default_factory=list) - - -DATA_INCOMFORT: HassKey[dict[str, InComfortData]] = HassKey(DOMAIN) - - -async def async_connect_gateway( - hass: HomeAssistant, - entry_data: dict[str, Any], -) -> InComfortData: - """Validate the configuration.""" - credentials = dict(entry_data) - hostname = credentials.pop(CONF_HOST) - - client = InComfortGateway( - hostname, **credentials, session=async_get_clientsession(hass) - ) - heaters = await client.heaters() - - return InComfortData(client=client, heaters=heaters) diff --git a/homeassistant/components/incomfort/sensor.py b/homeassistant/components/incomfort/sensor.py index a31488603b3..044443c8ac0 100644 --- a/homeassistant/components/incomfort/sensor.py +++ b/homeassistant/components/incomfort/sensor.py @@ -5,22 +5,23 @@ from __future__ import annotations from dataclasses import dataclass from typing import Any -from incomfortclient import Gateway as InComfortGateway, Heater as InComfortHeater +from incomfortclient import Heater as InComfortHeater from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, SensorEntityDescription, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import UnitOfPressure, UnitOfTemperature from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import slugify -from . import DATA_INCOMFORT, IncomfortEntity +from . import InComfortConfigEntry from .const import DOMAIN +from .coordinator import InComfortDataCoordinator +from .entity import IncomfortEntity INCOMFORT_HEATER_TEMP = "CV Temp" INCOMFORT_PRESSURE = "CV Pressure" @@ -63,14 +64,15 @@ SENSOR_TYPES: tuple[IncomfortSensorEntityDescription, ...] = ( async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: InComfortConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up InComfort/InTouch sensor entities.""" - incomfort_data = hass.data[DATA_INCOMFORT][entry.entry_id] + incomfort_coordinator = entry.runtime_data + heaters = incomfort_coordinator.data.heaters async_add_entities( - IncomfortSensor(incomfort_data.client, heater, description) - for heater in incomfort_data.heaters + IncomfortSensor(incomfort_coordinator, heater, description) + for heater in heaters for description in SENSOR_TYPES ) @@ -82,15 +84,14 @@ class IncomfortSensor(IncomfortEntity, SensorEntity): def __init__( self, - client: InComfortGateway, + coordinator: InComfortDataCoordinator, heater: InComfortHeater, description: IncomfortSensorEntityDescription, ) -> None: """Initialize the sensor.""" - super().__init__() + super().__init__(coordinator) self.entity_description = description - self._client = client self._heater = heater self._attr_unique_id = f"{heater.serial_no}_{slugify(description.name)}" diff --git a/homeassistant/components/incomfort/water_heater.py b/homeassistant/components/incomfort/water_heater.py index 239ddae3106..e21e2d5100f 100644 --- a/homeassistant/components/incomfort/water_heater.py +++ b/homeassistant/components/incomfort/water_heater.py @@ -5,19 +5,18 @@ from __future__ import annotations import logging from typing import Any -from aiohttp import ClientResponseError -from incomfortclient import Gateway as InComfortGateway, Heater as InComfortHeater +from incomfortclient import Heater as InComfortHeater from homeassistant.components.water_heater import WaterHeaterEntity -from homeassistant.config_entries import ConfigEntry from homeassistant.const import UnitOfTemperature from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceInfo -from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import DATA_INCOMFORT, IncomfortEntity +from . import InComfortConfigEntry from .const import DOMAIN +from .coordinator import InComfortDataCoordinator +from .entity import IncomfortEntity _LOGGER = logging.getLogger(__name__) @@ -26,14 +25,13 @@ HEATER_ATTRS = ["display_code", "display_text", "is_burning"] async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: InComfortConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up an InComfort/InTouch water_heater device.""" - incomfort_data = hass.data[DATA_INCOMFORT][entry.entry_id] - async_add_entities( - IncomfortWaterHeater(incomfort_data.client, h) for h in incomfort_data.heaters - ) + incomfort_coordinator = entry.runtime_data + heaters = incomfort_coordinator.data.heaters + async_add_entities(IncomfortWaterHeater(incomfort_coordinator, h) for h in heaters) class IncomfortWaterHeater(IncomfortEntity, WaterHeaterEntity): @@ -45,11 +43,12 @@ class IncomfortWaterHeater(IncomfortEntity, WaterHeaterEntity): _attr_should_poll = True _attr_temperature_unit = UnitOfTemperature.CELSIUS - def __init__(self, client: InComfortGateway, heater: InComfortHeater) -> None: + def __init__( + self, coordinator: InComfortDataCoordinator, heater: InComfortHeater + ) -> None: """Initialize the water_heater device.""" - super().__init__() + super().__init__(coordinator) - self._client = client self._heater = heater self._attr_unique_id = heater.serial_no @@ -85,14 +84,3 @@ class IncomfortWaterHeater(IncomfortEntity, WaterHeaterEntity): return f"Fault code: {self._heater.fault_code}" return self._heater.display_text - - async def async_update(self) -> None: - """Get the latest state data from the gateway.""" - try: - await self._heater.update() - - except (ClientResponseError, TimeoutError) as err: - _LOGGER.warning("Update failed, message is: %s", err) - - else: - async_dispatcher_send(self.hass, f"{DOMAIN}_{self.unique_id}") diff --git a/tests/components/incomfort/conftest.py b/tests/components/incomfort/conftest.py index 34c38995895..8c4bc5b2e31 100644 --- a/tests/components/incomfort/conftest.py +++ b/tests/components/incomfort/conftest.py @@ -140,7 +140,7 @@ def mock_incomfort( self.rooms = [MockRoom()] with patch( - "homeassistant.components.incomfort.models.InComfortGateway", MagicMock() + "homeassistant.components.incomfort.coordinator.InComfortGateway", MagicMock() ) as patch_gateway: patch_gateway().heaters = AsyncMock() patch_gateway().heaters.return_value = [MockHeater()] diff --git a/tests/components/incomfort/test_init.py b/tests/components/incomfort/test_init.py index 7c0a8b395a8..47365a836d2 100644 --- a/tests/components/incomfort/test_init.py +++ b/tests/components/incomfort/test_init.py @@ -1,23 +1,93 @@ """Tests for Intergas InComfort integration.""" +from datetime import timedelta from unittest.mock import MagicMock, patch -from syrupy import SnapshotAssertion +from aiohttp import ClientResponseError +from freezegun.api import FrozenDateTimeFactory +from incomfortclient import IncomfortError +import pytest +from homeassistant.components.incomfort.coordinator import UPDATE_INTERVAL from homeassistant.config_entries import ConfigEntry, ConfigEntryState -from homeassistant.const import Platform +from homeassistant.const import STATE_UNAVAILABLE from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er +from tests.common import async_fire_time_changed + -@patch("homeassistant.components.incomfort.PLATFORMS", [Platform.SENSOR]) async def test_setup_platforms( hass: HomeAssistant, mock_incomfort: MagicMock, entity_registry: er.EntityRegistry, - snapshot: SnapshotAssertion, mock_config_entry: ConfigEntry, ) -> None: """Test the incomfort integration is set up correctly.""" await hass.config_entries.async_setup(mock_config_entry.entry_id) assert mock_config_entry.state is ConfigEntryState.LOADED + + +async def test_coordinator_updates( + hass: HomeAssistant, + mock_incomfort: MagicMock, + freezer: FrozenDateTimeFactory, + mock_config_entry: ConfigEntry, +) -> None: + """Test the incomfort coordinator is updating.""" + await hass.config_entries.async_setup(mock_config_entry.entry_id) + state = hass.states.get("climate.thermostat_1") + assert state is not None + assert state.attributes["current_temperature"] == 21.4 + mock_incomfort().mock_room_status["room_temp"] = 20.91 + + state = hass.states.get("sensor.boiler_cv_pressure") + assert state is not None + assert state.state == "1.86" + mock_incomfort().mock_heater_status["pressure"] = 1.84 + + freezer.tick(timedelta(seconds=UPDATE_INTERVAL + 5)) + async_fire_time_changed(hass) + await hass.async_block_till_done(wait_background_tasks=True) + + state = hass.states.get("climate.thermostat_1") + assert state is not None + assert state.attributes["current_temperature"] == 20.9 + + state = hass.states.get("sensor.boiler_cv_pressure") + assert state is not None + assert state.state == "1.84" + + +@pytest.mark.parametrize( + "exc", + [ + IncomfortError(ClientResponseError(None, None, status=401)), + IncomfortError(ClientResponseError(None, None, status=500)), + IncomfortError(ValueError("some_error")), + TimeoutError, + ], +) +async def test_coordinator_update_fails( + hass: HomeAssistant, + mock_incomfort: MagicMock, + freezer: FrozenDateTimeFactory, + exc: Exception, + mock_config_entry: ConfigEntry, +) -> None: + """Test the incomfort coordinator update fails.""" + await hass.config_entries.async_setup(mock_config_entry.entry_id) + state = hass.states.get("sensor.boiler_cv_pressure") + assert state is not None + assert state.state == "1.86" + + with patch.object( + mock_incomfort().heaters.return_value[0], "update", side_effect=exc + ): + freezer.tick(timedelta(seconds=UPDATE_INTERVAL + 5)) + async_fire_time_changed(hass) + await hass.async_block_till_done(wait_background_tasks=True) + + state = hass.states.get("sensor.boiler_cv_pressure") + assert state is not None + assert state.state == STATE_UNAVAILABLE From 7a7a9c610af4021574e5203ffbdf4edb2c5d8553 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 5 Jun 2024 11:40:37 +0200 Subject: [PATCH 0244/1445] Detach name from unique id in incomfort (#118862) * Detach name from unique id in incomfort * Add entity descriptions to incomfort * Revert "Add entity descriptions to incomfort" This reverts commit 2b6ccd4c3bb921a1b607239a33ef15834dd23e8c. --- homeassistant/components/incomfort/sensor.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/incomfort/sensor.py b/homeassistant/components/incomfort/sensor.py index 044443c8ac0..3ee42dbd78f 100644 --- a/homeassistant/components/incomfort/sensor.py +++ b/homeassistant/components/incomfort/sensor.py @@ -16,7 +16,7 @@ from homeassistant.const import UnitOfPressure, UnitOfTemperature from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.util import slugify +from homeassistant.helpers.typing import StateType from . import InComfortConfigEntry from .const import DOMAIN @@ -28,10 +28,11 @@ INCOMFORT_PRESSURE = "CV Pressure" INCOMFORT_TAP_TEMP = "Tap Temp" -@dataclass(frozen=True) +@dataclass(frozen=True, kw_only=True) class IncomfortSensorEntityDescription(SensorEntityDescription): """Describes Incomfort sensor entity.""" + value_key: str extra_key: str | None = None # IncomfortSensor does not support UNDEFINED or None, # restrict the type to str @@ -40,17 +41,19 @@ class IncomfortSensorEntityDescription(SensorEntityDescription): SENSOR_TYPES: tuple[IncomfortSensorEntityDescription, ...] = ( IncomfortSensorEntityDescription( - key="pressure", + key="cv_pressure", name=INCOMFORT_PRESSURE, device_class=SensorDeviceClass.PRESSURE, native_unit_of_measurement=UnitOfPressure.BAR, + value_key="pressure", ), IncomfortSensorEntityDescription( - key="heater_temp", + key="cv_temp", name=INCOMFORT_HEATER_TEMP, device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=UnitOfTemperature.CELSIUS, extra_key="is_pumping", + value_key="heater_temp", ), IncomfortSensorEntityDescription( key="tap_temp", @@ -58,6 +61,7 @@ SENSOR_TYPES: tuple[IncomfortSensorEntityDescription, ...] = ( device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=UnitOfTemperature.CELSIUS, extra_key="is_tapping", + value_key="tap_temp", ), ) @@ -94,7 +98,7 @@ class IncomfortSensor(IncomfortEntity, SensorEntity): self._heater = heater - self._attr_unique_id = f"{heater.serial_no}_{slugify(description.name)}" + self._attr_unique_id = f"{heater.serial_no}_{description.key}" self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, heater.serial_no)}, manufacturer="Intergas", @@ -102,9 +106,9 @@ class IncomfortSensor(IncomfortEntity, SensorEntity): ) @property - def native_value(self) -> str | None: + def native_value(self) -> StateType: """Return the state of the sensor.""" - return self._heater.status[self.entity_description.key] + return self._heater.status[self.entity_description.value_key] @property def extra_state_attributes(self) -> dict[str, Any] | None: From 5e9eae14fca2789cc694525adc44da8236502bca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Jaworski?= Date: Wed, 5 Jun 2024 12:11:49 +0200 Subject: [PATCH 0245/1445] Bump blebox-uniapi fom 2.2.2 to 2.4.2 (#118836) * blebox: udpdate version in manifest and add new sensor key mapping * blebox: add more sensor types * blebox: use blebox_uniapi==2.4.1 * blebox: use blebox_uniapi==2.4.2 * blebox: update requirements_all.txt * blebox: revert introduction of illuminance and power meter sensors set --- homeassistant/components/blebox/manifest.json | 2 +- homeassistant/components/blebox/sensor.py | 4 ++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/blebox/manifest.json b/homeassistant/components/blebox/manifest.json index 4b0a6403f67..a2c6495cc56 100644 --- a/homeassistant/components/blebox/manifest.json +++ b/homeassistant/components/blebox/manifest.json @@ -6,6 +6,6 @@ "documentation": "https://www.home-assistant.io/integrations/blebox", "iot_class": "local_polling", "loggers": ["blebox_uniapi"], - "requirements": ["blebox-uniapi==2.2.2"], + "requirements": ["blebox-uniapi==2.4.2"], "zeroconf": ["_bbxsrv._tcp.local."] } diff --git a/homeassistant/components/blebox/sensor.py b/homeassistant/components/blebox/sensor.py index dbdf034faee..5aff62eb11e 100644 --- a/homeassistant/components/blebox/sensor.py +++ b/homeassistant/components/blebox/sensor.py @@ -45,7 +45,7 @@ SENSOR_TYPES = ( native_unit_of_measurement=UnitOfTemperature.CELSIUS, ), SensorEntityDescription( - key="powerMeasurement", + key="powerConsumption", device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, state_class=SensorStateClass.TOTAL, @@ -56,7 +56,7 @@ SENSOR_TYPES = ( native_unit_of_measurement=PERCENTAGE, ), SensorEntityDescription( - key="wind_speed", + key="wind", device_class=SensorDeviceClass.WIND_SPEED, native_unit_of_measurement=UnitOfSpeed.METERS_PER_SECOND, ), diff --git a/requirements_all.txt b/requirements_all.txt index 1c55d0536d7..e1a7e7c766f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -566,7 +566,7 @@ bleak-retry-connector==3.5.0 bleak==0.22.1 # homeassistant.components.blebox -blebox-uniapi==2.2.2 +blebox-uniapi==2.4.2 # homeassistant.components.blink blinkpy==0.22.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b6039ed2ea9..47e0ecf684f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -488,7 +488,7 @@ bleak-retry-connector==3.5.0 bleak==0.22.1 # homeassistant.components.blebox -blebox-uniapi==2.2.2 +blebox-uniapi==2.4.2 # homeassistant.components.blink blinkpy==0.22.6 From aedb0a3ca49a890476c0ce45fafada1eaf97d336 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Jaworski?= Date: Wed, 5 Jun 2024 12:17:56 +0200 Subject: [PATCH 0246/1445] Add new sensors to blebox (#118837) blebox: add mapping for new sensor types introduced in blebox_uniapi>-2.4.0 Co-authored-by: Erik Montnemery --- homeassistant/components/blebox/sensor.py | 52 +++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/homeassistant/components/blebox/sensor.py b/homeassistant/components/blebox/sensor.py index 5aff62eb11e..2642bfd0139 100644 --- a/homeassistant/components/blebox/sensor.py +++ b/homeassistant/components/blebox/sensor.py @@ -12,8 +12,15 @@ from homeassistant.components.sensor import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + LIGHT_LUX, PERCENTAGE, + POWER_VOLT_AMPERE_REACTIVE, + UnitOfApparentPower, + UnitOfElectricCurrent, + UnitOfElectricPotential, UnitOfEnergy, + UnitOfFrequency, + UnitOfPower, UnitOfSpeed, UnitOfTemperature, ) @@ -60,6 +67,51 @@ SENSOR_TYPES = ( device_class=SensorDeviceClass.WIND_SPEED, native_unit_of_measurement=UnitOfSpeed.METERS_PER_SECOND, ), + SensorEntityDescription( + key="illuminance", + device_class=SensorDeviceClass.ILLUMINANCE, + native_unit_of_measurement=LIGHT_LUX, + ), + SensorEntityDescription( + key="forwardActiveEnergy", + device_class=SensorDeviceClass.ENERGY, + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + ), + SensorEntityDescription( + key="reverseActiveEnergy", + device_class=SensorDeviceClass.ENERGY, + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + ), + SensorEntityDescription( + key="reactivePower", + device_class=SensorDeviceClass.POWER, + native_unit_of_measurement=POWER_VOLT_AMPERE_REACTIVE, + ), + SensorEntityDescription( + key="activePower", + device_class=SensorDeviceClass.POWER, + native_unit_of_measurement=UnitOfPower.WATT, + ), + SensorEntityDescription( + key="apparentPower", + device_class=SensorDeviceClass.APPARENT_POWER, + native_unit_of_measurement=UnitOfApparentPower.VOLT_AMPERE, + ), + SensorEntityDescription( + key="voltage", + device_class=SensorDeviceClass.VOLTAGE, + native_unit_of_measurement=UnitOfElectricPotential.VOLT, + ), + SensorEntityDescription( + key="current", + device_class=SensorDeviceClass.CURRENT, + native_unit_of_measurement=UnitOfElectricCurrent.MILLIAMPERE, + ), + SensorEntityDescription( + key="frequency", + device_class=SensorDeviceClass.FREQUENCY, + native_unit_of_measurement=UnitOfFrequency.HERTZ, + ), ) From 68a537a05a499b08d718e5854639c6e0f73be7aa Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Wed, 5 Jun 2024 12:47:52 +0200 Subject: [PATCH 0247/1445] Fix TypeAliasType not callable in senz (#118872) --- homeassistant/components/senz/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/senz/__init__.py b/homeassistant/components/senz/__init__.py index 288bf005a5c..bd4dfae4571 100644 --- a/homeassistant/components/senz/__init__.py +++ b/homeassistant/components/senz/__init__.py @@ -57,7 +57,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: except RequestError as err: raise ConfigEntryNotReady from err - coordinator = SENZDataUpdateCoordinator( + coordinator: SENZDataUpdateCoordinator = DataUpdateCoordinator( hass, _LOGGER, name=account.username, From 239984f87d7a563ce809dae3022fb276efced129 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 5 Jun 2024 12:51:39 +0200 Subject: [PATCH 0248/1445] Add entity descriptions to incomfort binary sensor (#118863) * Detach name from unique id in incomfort * Add entity descriptions to incomfort * Revert "Detach name from unique id in incomfort" This reverts commit 17448444664f6b84c5e5e2a18899444eafe75785. * yes --- .../components/incomfort/binary_sensor.py | 51 +++++++++++++++---- 1 file changed, 41 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/incomfort/binary_sensor.py b/homeassistant/components/incomfort/binary_sensor.py index 238f1812aa2..aecbd96f472 100644 --- a/homeassistant/components/incomfort/binary_sensor.py +++ b/homeassistant/components/incomfort/binary_sensor.py @@ -2,11 +2,16 @@ from __future__ import annotations +from collections.abc import Callable +from dataclasses import dataclass from typing import Any from incomfortclient import Heater as InComfortHeater -from homeassistant.components.binary_sensor import BinarySensorEntity +from homeassistant.components.binary_sensor import ( + BinarySensorEntity, + BinarySensorEntityDescription, +) from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -17,6 +22,24 @@ from .coordinator import InComfortDataCoordinator from .entity import IncomfortEntity +@dataclass(frozen=True, kw_only=True) +class IncomfortBinarySensorEntityDescription(BinarySensorEntityDescription): + """Describes Incomfort binary sensor entity.""" + + value_key: str + extra_state_attributes_fn: Callable[[dict[str, Any]], dict[str, Any]] + + +SENSOR_TYPES: tuple[IncomfortBinarySensorEntityDescription, ...] = ( + IncomfortBinarySensorEntityDescription( + key="failed", + name="Fault", + value_key="is_failed", + extra_state_attributes_fn=lambda status: {"fault_code": status["fault_code"]}, + ), +) + + async def async_setup_entry( hass: HomeAssistant, entry: InComfortConfigEntry, @@ -25,23 +48,31 @@ async def async_setup_entry( """Set up an InComfort/InTouch binary_sensor entity.""" incomfort_coordinator = entry.runtime_data heaters = incomfort_coordinator.data.heaters - async_add_entities(IncomfortFailed(incomfort_coordinator, h) for h in heaters) + async_add_entities( + IncomfortBinarySensor(incomfort_coordinator, h, description) + for h in heaters + for description in SENSOR_TYPES + ) -class IncomfortFailed(IncomfortEntity, BinarySensorEntity): - """Representation of an InComfort Failed sensor.""" +class IncomfortBinarySensor(IncomfortEntity, BinarySensorEntity): + """Representation of an InComfort binary sensor.""" - _attr_name = "Fault" + entity_description: IncomfortBinarySensorEntityDescription def __init__( - self, coordinator: InComfortDataCoordinator, heater: InComfortHeater + self, + coordinator: InComfortDataCoordinator, + heater: InComfortHeater, + description: IncomfortBinarySensorEntityDescription, ) -> None: """Initialize the binary sensor.""" super().__init__(coordinator) + self.entity_description = description self._heater = heater - self._attr_unique_id = f"{heater.serial_no}_failed" + self._attr_unique_id = f"{heater.serial_no}_{description.key}" self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, heater.serial_no)}, manufacturer="Intergas", @@ -51,9 +82,9 @@ class IncomfortFailed(IncomfortEntity, BinarySensorEntity): @property def is_on(self) -> bool: """Return the status of the sensor.""" - return self._heater.status["is_failed"] + return self._heater.status[self.entity_description.value_key] @property - def extra_state_attributes(self) -> dict[str, Any] | None: + def extra_state_attributes(self) -> dict[str, Any]: """Return the device state attributes.""" - return {"fault_code": self._heater.status["fault_code"]} + return self.entity_description.extra_state_attributes_fn(self._heater.status) From 8d11279bc93076c08ecac05d26b7b627bcbcada2 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 5 Jun 2024 12:55:40 +0200 Subject: [PATCH 0249/1445] Remove obsolete polling from incomfort water heater (#118860) Remove obsolete polling --- homeassistant/components/incomfort/water_heater.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/incomfort/water_heater.py b/homeassistant/components/incomfort/water_heater.py index e21e2d5100f..c60da9669ec 100644 --- a/homeassistant/components/incomfort/water_heater.py +++ b/homeassistant/components/incomfort/water_heater.py @@ -40,7 +40,6 @@ class IncomfortWaterHeater(IncomfortEntity, WaterHeaterEntity): _attr_min_temp = 30.0 _attr_max_temp = 80.0 _attr_name = None - _attr_should_poll = True _attr_temperature_unit = UnitOfTemperature.CELSIUS def __init__( From 986d8986a9dc6e3582111c17c3581227967112cc Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 5 Jun 2024 13:09:24 +0200 Subject: [PATCH 0250/1445] Introduce incomfort boiler entity (#118861) --- .../components/incomfort/binary_sensor.py | 16 +++------------- homeassistant/components/incomfort/entity.py | 18 ++++++++++++++++++ homeassistant/components/incomfort/sensor.py | 16 +++------------- .../components/incomfort/water_heater.py | 16 +++------------- 4 files changed, 27 insertions(+), 39 deletions(-) diff --git a/homeassistant/components/incomfort/binary_sensor.py b/homeassistant/components/incomfort/binary_sensor.py index aecbd96f472..e3563c183da 100644 --- a/homeassistant/components/incomfort/binary_sensor.py +++ b/homeassistant/components/incomfort/binary_sensor.py @@ -13,13 +13,11 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntityDescription, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import InComfortConfigEntry -from .const import DOMAIN from .coordinator import InComfortDataCoordinator -from .entity import IncomfortEntity +from .entity import IncomfortBoilerEntity @dataclass(frozen=True, kw_only=True) @@ -55,7 +53,7 @@ async def async_setup_entry( ) -class IncomfortBinarySensor(IncomfortEntity, BinarySensorEntity): +class IncomfortBinarySensor(IncomfortBoilerEntity, BinarySensorEntity): """Representation of an InComfort binary sensor.""" entity_description: IncomfortBinarySensorEntityDescription @@ -67,17 +65,9 @@ class IncomfortBinarySensor(IncomfortEntity, BinarySensorEntity): description: IncomfortBinarySensorEntityDescription, ) -> None: """Initialize the binary sensor.""" - super().__init__(coordinator) + super().__init__(coordinator, heater) self.entity_description = description - - self._heater = heater - self._attr_unique_id = f"{heater.serial_no}_{description.key}" - self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, heater.serial_no)}, - manufacturer="Intergas", - name="Boiler", - ) @property def is_on(self) -> bool: diff --git a/homeassistant/components/incomfort/entity.py b/homeassistant/components/incomfort/entity.py index 7b4a535bff6..33037a78edf 100644 --- a/homeassistant/components/incomfort/entity.py +++ b/homeassistant/components/incomfort/entity.py @@ -1,7 +1,11 @@ """Common entity classes for InComfort integration.""" +from incomfortclient import Heater + +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity +from .const import DOMAIN from .coordinator import InComfortDataCoordinator @@ -9,3 +13,17 @@ class IncomfortEntity(CoordinatorEntity[InComfortDataCoordinator]): """Base class for all InComfort entities.""" _attr_has_entity_name = True + + +class IncomfortBoilerEntity(IncomfortEntity): + """Base class for all InComfort boiler entities.""" + + def __init__(self, coordinator: InComfortDataCoordinator, heater: Heater) -> None: + """Initialize the boiler entity.""" + super().__init__(coordinator) + self._heater = heater + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, heater.serial_no)}, + manufacturer="Intergas", + name="Boiler", + ) diff --git a/homeassistant/components/incomfort/sensor.py b/homeassistant/components/incomfort/sensor.py index 3ee42dbd78f..4bba56382b3 100644 --- a/homeassistant/components/incomfort/sensor.py +++ b/homeassistant/components/incomfort/sensor.py @@ -14,14 +14,12 @@ from homeassistant.components.sensor import ( ) from homeassistant.const import UnitOfPressure, UnitOfTemperature from homeassistant.core import HomeAssistant -from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from . import InComfortConfigEntry -from .const import DOMAIN from .coordinator import InComfortDataCoordinator -from .entity import IncomfortEntity +from .entity import IncomfortBoilerEntity INCOMFORT_HEATER_TEMP = "CV Temp" INCOMFORT_PRESSURE = "CV Pressure" @@ -81,7 +79,7 @@ async def async_setup_entry( ) -class IncomfortSensor(IncomfortEntity, SensorEntity): +class IncomfortSensor(IncomfortBoilerEntity, SensorEntity): """Representation of an InComfort/InTouch sensor device.""" entity_description: IncomfortSensorEntityDescription @@ -93,17 +91,9 @@ class IncomfortSensor(IncomfortEntity, SensorEntity): description: IncomfortSensorEntityDescription, ) -> None: """Initialize the sensor.""" - super().__init__(coordinator) + super().__init__(coordinator, heater) self.entity_description = description - - self._heater = heater - self._attr_unique_id = f"{heater.serial_no}_{description.key}" - self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, heater.serial_no)}, - manufacturer="Intergas", - name="Boiler", - ) @property def native_value(self) -> StateType: diff --git a/homeassistant/components/incomfort/water_heater.py b/homeassistant/components/incomfort/water_heater.py index c60da9669ec..f652cc21c8f 100644 --- a/homeassistant/components/incomfort/water_heater.py +++ b/homeassistant/components/incomfort/water_heater.py @@ -10,13 +10,11 @@ from incomfortclient import Heater as InComfortHeater from homeassistant.components.water_heater import WaterHeaterEntity from homeassistant.const import UnitOfTemperature from homeassistant.core import HomeAssistant -from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import InComfortConfigEntry -from .const import DOMAIN from .coordinator import InComfortDataCoordinator -from .entity import IncomfortEntity +from .entity import IncomfortBoilerEntity _LOGGER = logging.getLogger(__name__) @@ -34,7 +32,7 @@ async def async_setup_entry( async_add_entities(IncomfortWaterHeater(incomfort_coordinator, h) for h in heaters) -class IncomfortWaterHeater(IncomfortEntity, WaterHeaterEntity): +class IncomfortWaterHeater(IncomfortBoilerEntity, WaterHeaterEntity): """Representation of an InComfort/Intouch water_heater device.""" _attr_min_temp = 30.0 @@ -46,16 +44,8 @@ class IncomfortWaterHeater(IncomfortEntity, WaterHeaterEntity): self, coordinator: InComfortDataCoordinator, heater: InComfortHeater ) -> None: """Initialize the water_heater device.""" - super().__init__(coordinator) - - self._heater = heater - + super().__init__(coordinator, heater) self._attr_unique_id = heater.serial_no - self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, heater.serial_no)}, - manufacturer="Intergas", - name="Boiler", - ) @property def icon(self) -> str: From 873a8421664b02572ccbcbc7814bc23f06f3431d Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Wed, 5 Jun 2024 13:31:31 +0200 Subject: [PATCH 0251/1445] Update coverage to 7.5.3 (#118870) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 5651a411cb0..8ab1efe3d69 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -8,7 +8,7 @@ -c homeassistant/package_constraints.txt -r requirements_test_pre_commit.txt astroid==3.2.2 -coverage==7.5.0 +coverage==7.5.3 freezegun==1.5.0 mock-open==1.4.0 mypy-dev==1.11.0a5 From 4b663dbf01501fdb731157d6eedf55b9c70f3a43 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 5 Jun 2024 13:31:59 +0200 Subject: [PATCH 0252/1445] Rename esphome fixture (#118865) --- .../components/esphome/test_voice_assistant.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/components/esphome/test_voice_assistant.py b/tests/components/esphome/test_voice_assistant.py index 21fa0dabac5..305d0e395a3 100644 --- a/tests/components/esphome/test_voice_assistant.py +++ b/tests/components/esphome/test_voice_assistant.py @@ -99,7 +99,7 @@ def voice_assistant_udp_pipeline_v2( @pytest.fixture -def test_wav() -> bytes: +def mock_wav() -> bytes: """Return one second of empty WAV audio.""" with io.BytesIO() as wav_io: with wave.open(wav_io, "wb") as wav_file: @@ -560,12 +560,12 @@ async def test_send_tts_not_called_when_empty( async def test_send_tts_udp( hass: HomeAssistant, voice_assistant_udp_pipeline_v2: VoiceAssistantUDPPipeline, - test_wav, + mock_wav: bytes, ) -> None: """Test the UDP server calls sendto to transmit audio data to device.""" with patch( "homeassistant.components.esphome.voice_assistant.tts.async_get_media_source_audio", - return_value=("wav", test_wav), + return_value=("wav", mock_wav), ): voice_assistant_udp_pipeline_v2.started = True voice_assistant_udp_pipeline_v2.transport = Mock(spec=asyncio.DatagramTransport) @@ -593,12 +593,12 @@ async def test_send_tts_api( hass: HomeAssistant, mock_client: APIClient, voice_assistant_api_pipeline: VoiceAssistantAPIPipeline, - test_wav, + mock_wav: bytes, ) -> None: """Test the API pipeline calls cli.send_voice_assistant_audio to transmit audio data to device.""" with patch( "homeassistant.components.esphome.voice_assistant.tts.async_get_media_source_audio", - return_value=("wav", test_wav), + return_value=("wav", mock_wav), ): voice_assistant_api_pipeline.started = True @@ -686,12 +686,12 @@ async def test_send_tts_wrong_format( async def test_send_tts_not_started( hass: HomeAssistant, voice_assistant_udp_pipeline_v2: VoiceAssistantUDPPipeline, - test_wav, + mock_wav: bytes, ) -> None: """Test the UDP server does not call sendto when not started.""" with patch( "homeassistant.components.esphome.voice_assistant.tts.async_get_media_source_audio", - return_value=("wav", test_wav), + return_value=("wav", mock_wav), ): voice_assistant_udp_pipeline_v2.started = False voice_assistant_udp_pipeline_v2.transport = Mock(spec=asyncio.DatagramTransport) @@ -713,13 +713,13 @@ async def test_send_tts_not_started( async def test_send_tts_transport_none( hass: HomeAssistant, voice_assistant_udp_pipeline_v2: VoiceAssistantUDPPipeline, - test_wav, + mock_wav: bytes, caplog: pytest.LogCaptureFixture, ) -> None: """Test the UDP server does not call sendto when transport is None.""" with patch( "homeassistant.components.esphome.voice_assistant.tts.async_get_media_source_audio", - return_value=("wav", test_wav), + return_value=("wav", mock_wav), ): voice_assistant_udp_pipeline_v2.started = True voice_assistant_udp_pipeline_v2.transport = None From 3a4b84a4ce684ddf421e00a99e831d836bbe6c94 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 5 Jun 2024 13:32:50 +0200 Subject: [PATCH 0253/1445] Update frontend to 20240605.0 (#118875) --- 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 d474e9d2f14..27322b423d0 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==20240604.0"] + "requirements": ["home-assistant-frontend==20240605.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 2286189626c..56f3d920641 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -32,7 +32,7 @@ habluetooth==3.1.1 hass-nabucasa==0.81.1 hassil==1.7.1 home-assistant-bluetooth==1.12.0 -home-assistant-frontend==20240604.0 +home-assistant-frontend==20240605.0 home-assistant-intents==2024.6.3 httpx==0.27.0 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index e1a7e7c766f..330391eafa8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1087,7 +1087,7 @@ hole==0.8.0 holidays==0.49 # homeassistant.components.frontend -home-assistant-frontend==20240604.0 +home-assistant-frontend==20240605.0 # homeassistant.components.conversation home-assistant-intents==2024.6.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 47e0ecf684f..3b422a731e2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -889,7 +889,7 @@ hole==0.8.0 holidays==0.49 # homeassistant.components.frontend -home-assistant-frontend==20240604.0 +home-assistant-frontend==20240605.0 # homeassistant.components.conversation home-assistant-intents==2024.6.3 From 0d1fb1fc9ff3eda2ddc740a358fdd12ac8410098 Mon Sep 17 00:00:00 2001 From: Pete Sage <76050312+PeteRager@users.noreply.github.com> Date: Wed, 5 Jun 2024 08:18:41 -0400 Subject: [PATCH 0254/1445] Fix Hydrawise sensor availability (#118669) Co-authored-by: Robert Resch --- .../components/hydrawise/binary_sensor.py | 13 +++- homeassistant/components/hydrawise/entity.py | 5 ++ .../hydrawise/test_binary_sensor.py | 24 ++++++- .../hydrawise/test_entity_availability.py | 65 +++++++++++++++++++ 4 files changed, 104 insertions(+), 3 deletions(-) create mode 100644 tests/components/hydrawise/test_entity_availability.py diff --git a/homeassistant/components/hydrawise/binary_sensor.py b/homeassistant/components/hydrawise/binary_sensor.py index d3382dbce39..e8426e5423a 100644 --- a/homeassistant/components/hydrawise/binary_sensor.py +++ b/homeassistant/components/hydrawise/binary_sensor.py @@ -24,13 +24,17 @@ class HydrawiseBinarySensorEntityDescription(BinarySensorEntityDescription): """Describes Hydrawise binary sensor.""" value_fn: Callable[[HydrawiseBinarySensor], bool | None] + always_available: bool = False CONTROLLER_BINARY_SENSORS: tuple[HydrawiseBinarySensorEntityDescription, ...] = ( HydrawiseBinarySensorEntityDescription( key="status", device_class=BinarySensorDeviceClass.CONNECTIVITY, - value_fn=lambda status_sensor: status_sensor.coordinator.last_update_success, + value_fn=lambda status_sensor: status_sensor.coordinator.last_update_success + and status_sensor.controller.online, + # Connectivtiy sensor is always available + always_available=True, ), ) @@ -98,3 +102,10 @@ class HydrawiseBinarySensor(HydrawiseEntity, BinarySensorEntity): def _update_attrs(self) -> None: """Update state attributes.""" self._attr_is_on = self.entity_description.value_fn(self) + + @property + def available(self) -> bool: + """Set the entity availability.""" + if self.entity_description.always_available: + return True + return super().available diff --git a/homeassistant/components/hydrawise/entity.py b/homeassistant/components/hydrawise/entity.py index 7b3ce6551a5..67dd6375b0e 100644 --- a/homeassistant/components/hydrawise/entity.py +++ b/homeassistant/components/hydrawise/entity.py @@ -70,3 +70,8 @@ class HydrawiseEntity(CoordinatorEntity[HydrawiseDataUpdateCoordinator]): self.controller = self.coordinator.data.controllers[self.controller.id] self._update_attrs() super()._handle_coordinator_update() + + @property + def available(self) -> bool: + """Set the entity availability.""" + return super().available and self.controller.online diff --git a/tests/components/hydrawise/test_binary_sensor.py b/tests/components/hydrawise/test_binary_sensor.py index 6343b345d99..a42f9b1c044 100644 --- a/tests/components/hydrawise/test_binary_sensor.py +++ b/tests/components/hydrawise/test_binary_sensor.py @@ -6,10 +6,11 @@ from unittest.mock import AsyncMock, patch from aiohttp import ClientError from freezegun.api import FrozenDateTimeFactory +from pydrawise.schema import Controller from syrupy.assertion import SnapshotAssertion from homeassistant.components.hydrawise.const import SCAN_INTERVAL -from homeassistant.const import Platform +from homeassistant.const import STATE_OFF, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -47,4 +48,23 @@ async def test_update_data_fails( connectivity = hass.states.get("binary_sensor.home_controller_connectivity") assert connectivity is not None - assert connectivity.state == "unavailable" + assert connectivity.state == STATE_OFF + + +async def test_controller_offline( + hass: HomeAssistant, + mock_added_config_entry: MockConfigEntry, + mock_pydrawise: AsyncMock, + freezer: FrozenDateTimeFactory, + controller: Controller, +) -> None: + """Test the binary_sensor for the controller being online.""" + # Make the coordinator refresh data. + controller.online = False + freezer.tick(SCAN_INTERVAL + timedelta(seconds=30)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + connectivity = hass.states.get("binary_sensor.home_controller_connectivity") + assert connectivity + assert connectivity.state == STATE_OFF diff --git a/tests/components/hydrawise/test_entity_availability.py b/tests/components/hydrawise/test_entity_availability.py new file mode 100644 index 00000000000..58ded5fe6c3 --- /dev/null +++ b/tests/components/hydrawise/test_entity_availability.py @@ -0,0 +1,65 @@ +"""Test entity availability.""" + +from collections.abc import Awaitable, Callable +from datetime import timedelta +from unittest.mock import AsyncMock + +from aiohttp import ClientError +from freezegun.api import FrozenDateTimeFactory +from pydrawise.schema import Controller + +from homeassistant.components.hydrawise.const import SCAN_INTERVAL +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import STATE_OFF, STATE_UNAVAILABLE +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from tests.common import MockConfigEntry, async_fire_time_changed + +_SPECIAL_ENTITIES = {"binary_sensor.home_controller_connectivity": STATE_OFF} + + +async def test_controller_offline( + hass: HomeAssistant, + mock_add_config_entry: Callable[[], Awaitable[MockConfigEntry]], + entity_registry: er.EntityRegistry, + controller: Controller, +) -> None: + """Test availability for sensors when controller is offline.""" + controller.online = False + config_entry = await mock_add_config_entry() + _test_availability(hass, config_entry, entity_registry) + + +async def test_api_offline( + hass: HomeAssistant, + mock_add_config_entry: Callable[[], Awaitable[MockConfigEntry]], + entity_registry: er.EntityRegistry, + mock_pydrawise: AsyncMock, + freezer: FrozenDateTimeFactory, +) -> None: + """Test availability of sensors when API call fails.""" + config_entry = await mock_add_config_entry() + mock_pydrawise.get_user.reset_mock(return_value=True) + mock_pydrawise.get_user.side_effect = ClientError + freezer.tick(SCAN_INTERVAL + timedelta(seconds=30)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + _test_availability(hass, config_entry, entity_registry) + + +def _test_availability( + hass: HomeAssistant, + config_entry: ConfigEntry, + entity_registry: er.EntityRegistry, +) -> None: + entity_entries = er.async_entries_for_config_entry( + entity_registry, config_entry.entry_id + ) + assert entity_entries + for entity_entry in entity_entries: + state = hass.states.get(entity_entry.entity_id) + assert state, f"State not found for {entity_entry.entity_id}" + assert state.state == _SPECIAL_ENTITIES.get( + entity_entry.entity_id, STATE_UNAVAILABLE + ) From edd3c45c09b34715e180070435db75463d31228a Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Wed, 5 Jun 2024 14:30:15 +0200 Subject: [PATCH 0255/1445] Add binary "sleeping" sensor to Reolink (#118774) --- .../components/reolink/binary_sensor.py | 27 +++++++++++++++++-- homeassistant/components/reolink/icons.json | 6 +++++ homeassistant/components/reolink/strings.json | 7 +++++ 3 files changed, 38 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/reolink/binary_sensor.py b/homeassistant/components/reolink/binary_sensor.py index fe80177da12..d19987c3bc6 100644 --- a/homeassistant/components/reolink/binary_sensor.py +++ b/homeassistant/components/reolink/binary_sensor.py @@ -21,6 +21,7 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry +from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -40,7 +41,7 @@ class ReolinkBinarySensorEntityDescription( value: Callable[[Host, int], bool] -BINARY_SENSORS = ( +BINARY_PUSH_SENSORS = ( ReolinkBinarySensorEntityDescription( key="motion", device_class=BinarySensorDeviceClass.MOTION, @@ -93,6 +94,17 @@ BINARY_SENSORS = ( ), ) +BINARY_SENSORS = ( + ReolinkBinarySensorEntityDescription( + key="sleep", + cmd_key="GetChannelstatus", + translation_key="sleep", + entity_category=EntityCategory.DIAGNOSTIC, + value=lambda api, ch: api.sleeping(ch), + supported=lambda api, ch: api.supported(ch, "sleep"), + ), +) + async def async_setup_entry( hass: HomeAssistant, @@ -104,6 +116,13 @@ async def async_setup_entry( entities: list[ReolinkBinarySensorEntity] = [] for channel in reolink_data.host.api.channels: + entities.extend( + [ + ReolinkPushBinarySensorEntity(reolink_data, channel, entity_description) + for entity_description in BINARY_PUSH_SENSORS + if entity_description.supported(reolink_data.host.api, channel) + ] + ) entities.extend( [ ReolinkBinarySensorEntity(reolink_data, channel, entity_description) @@ -116,7 +135,7 @@ async def async_setup_entry( class ReolinkBinarySensorEntity(ReolinkChannelCoordinatorEntity, BinarySensorEntity): - """Base binary-sensor class for Reolink IP camera motion sensors.""" + """Base binary-sensor class for Reolink IP camera.""" entity_description: ReolinkBinarySensorEntityDescription @@ -142,6 +161,10 @@ class ReolinkBinarySensorEntity(ReolinkChannelCoordinatorEntity, BinarySensorEnt """State of the sensor.""" return self.entity_description.value(self._host.api, self._channel) + +class ReolinkPushBinarySensorEntity(ReolinkBinarySensorEntity): + """Binary-sensor class for Reolink IP camera motion sensors.""" + async def async_added_to_hass(self) -> None: """Entity created.""" await super().async_added_to_hass() diff --git a/homeassistant/components/reolink/icons.json b/homeassistant/components/reolink/icons.json index 6346881e8f7..a06293abf9a 100644 --- a/homeassistant/components/reolink/icons.json +++ b/homeassistant/components/reolink/icons.json @@ -42,6 +42,12 @@ "state": { "on": "mdi:motion-sensor" } + }, + "sleep": { + "default": "mdi:sleep-off", + "state": { + "on": "mdi:sleep" + } } }, "button": { diff --git a/homeassistant/components/reolink/strings.json b/homeassistant/components/reolink/strings.json index d1fa0f4426b..799e7f2cac5 100644 --- a/homeassistant/components/reolink/strings.json +++ b/homeassistant/components/reolink/strings.json @@ -233,6 +233,13 @@ "off": "[%key:component::binary_sensor::entity_component::gas::state::off%]", "on": "[%key:component::binary_sensor::entity_component::gas::state::on%]" } + }, + "sleep": { + "name": "Sleep status", + "state": { + "off": "Awake", + "on": "Sleeping" + } } }, "button": { From 066cd6dbefbac37c1a4b5c042714e5f52b14836d Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Wed, 5 Jun 2024 15:41:22 +0200 Subject: [PATCH 0256/1445] Improve repair issue when notify service is still being used (#118855) Co-authored-by: Joost Lekkerkerker Co-authored-by: Martin Hjelmare --- homeassistant/components/ecobee/notify.py | 4 +++- homeassistant/components/file/notify.py | 4 +++- homeassistant/components/knx/notify.py | 4 +++- homeassistant/components/notify/repairs.py | 24 +++++++++++++++++++- homeassistant/components/notify/strings.json | 11 +++++++++ homeassistant/components/tibber/notify.py | 8 ++++++- tests/components/ecobee/test_repairs.py | 6 ++--- tests/components/knx/test_repairs.py | 6 ++--- tests/components/notify/test_repairs.py | 18 +++++++++++---- tests/components/tibber/test_repairs.py | 6 ++--- 10 files changed, 72 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/ecobee/notify.py b/homeassistant/components/ecobee/notify.py index b9dafae0f4e..167233e4071 100644 --- a/homeassistant/components/ecobee/notify.py +++ b/homeassistant/components/ecobee/notify.py @@ -43,7 +43,9 @@ class EcobeeNotificationService(BaseNotificationService): async def async_send_message(self, message: str = "", **kwargs: Any) -> None: """Send a message and raise issue.""" - migrate_notify_issue(self.hass, DOMAIN, "Ecobee", "2024.11.0") + migrate_notify_issue( + self.hass, DOMAIN, "Ecobee", "2024.11.0", service_name=self._service_name + ) await self.hass.async_add_executor_job( partial(self.send_message, message, **kwargs) ) diff --git a/homeassistant/components/file/notify.py b/homeassistant/components/file/notify.py index b51be280e75..244bd69aa32 100644 --- a/homeassistant/components/file/notify.py +++ b/homeassistant/components/file/notify.py @@ -69,7 +69,9 @@ class FileNotificationService(BaseNotificationService): """Send a message to a file.""" # The use of the legacy notify service was deprecated with HA Core 2024.6.0 # and will be removed with HA Core 2024.12 - migrate_notify_issue(self.hass, DOMAIN, "File", "2024.12.0") + migrate_notify_issue( + self.hass, DOMAIN, "File", "2024.12.0", service_name=self._service_name + ) await self.hass.async_add_executor_job( partial(self.send_message, message, **kwargs) ) diff --git a/homeassistant/components/knx/notify.py b/homeassistant/components/knx/notify.py index 1b6cd325f21..997bdb81057 100644 --- a/homeassistant/components/knx/notify.py +++ b/homeassistant/components/knx/notify.py @@ -60,7 +60,9 @@ class KNXNotificationService(BaseNotificationService): async def async_send_message(self, message: str = "", **kwargs: Any) -> None: """Send a notification to knx bus.""" - migrate_notify_issue(self.hass, DOMAIN, "KNX", "2024.11.0") + migrate_notify_issue( + self.hass, DOMAIN, "KNX", "2024.11.0", service_name=self._service_name + ) if "target" in kwargs: await self._async_send_to_device(message, kwargs["target"]) else: diff --git a/homeassistant/components/notify/repairs.py b/homeassistant/components/notify/repairs.py index 5c91a9a4731..d188f07c2ed 100644 --- a/homeassistant/components/notify/repairs.py +++ b/homeassistant/components/notify/repairs.py @@ -12,9 +12,31 @@ from .const import DOMAIN @callback def migrate_notify_issue( - hass: HomeAssistant, domain: str, integration_title: str, breaks_in_ha_version: str + hass: HomeAssistant, + domain: str, + integration_title: str, + breaks_in_ha_version: str, + service_name: str | None = None, ) -> None: """Ensure an issue is registered.""" + if service_name is not None: + ir.async_create_issue( + hass, + DOMAIN, + f"migrate_notify_{domain}_{service_name}", + breaks_in_ha_version=breaks_in_ha_version, + issue_domain=domain, + is_fixable=True, + is_persistent=True, + translation_key="migrate_notify_service", + translation_placeholders={ + "domain": domain, + "integration_title": integration_title, + "service_name": service_name, + }, + severity=ir.IssueSeverity.WARNING, + ) + return ir.async_create_issue( hass, DOMAIN, diff --git a/homeassistant/components/notify/strings.json b/homeassistant/components/notify/strings.json index 96482f5a7d5..947b192c4cd 100644 --- a/homeassistant/components/notify/strings.json +++ b/homeassistant/components/notify/strings.json @@ -72,6 +72,17 @@ } } } + }, + "migrate_notify_service": { + "title": "Legacy service `notify.{service_name}` stll being used", + "fix_flow": { + "step": { + "confirm": { + "description": "The {integration_title} `notify.{service_name}` service is migrated, but it seems the old `notify` service is still being used.\n\nA new `notify` entity is available now to replace each legacy `notify` service.\n\nUpdate any automations or scripts to use the new `notify.send_message` service exposed with this new entity. When this is done, select Submit and restart Home Assistant.", + "title": "Migrate legacy {integration_title} notify service for domain `{domain}`" + } + } + } } } } diff --git a/homeassistant/components/tibber/notify.py b/homeassistant/components/tibber/notify.py index 24ae86c9e7f..1c9f86ed502 100644 --- a/homeassistant/components/tibber/notify.py +++ b/homeassistant/components/tibber/notify.py @@ -50,7 +50,13 @@ class TibberNotificationService(BaseNotificationService): async def async_send_message(self, message: str = "", **kwargs: Any) -> None: """Send a message to Tibber devices.""" - migrate_notify_issue(self.hass, TIBBER_DOMAIN, "Tibber", "2024.12.0") + migrate_notify_issue( + self.hass, + TIBBER_DOMAIN, + "Tibber", + "2024.12.0", + service_name=self._service_name, + ) title = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT) try: await self._notify(title=title, message=message) diff --git a/tests/components/ecobee/test_repairs.py b/tests/components/ecobee/test_repairs.py index 897594c582f..9821d31ac64 100644 --- a/tests/components/ecobee/test_repairs.py +++ b/tests/components/ecobee/test_repairs.py @@ -49,13 +49,13 @@ async def test_ecobee_repair_flow( # Assert the issue is present assert issue_registry.async_get_issue( domain="notify", - issue_id=f"migrate_notify_{DOMAIN}", + issue_id=f"migrate_notify_{DOMAIN}_{DOMAIN}", ) assert len(issue_registry.issues) == 1 url = RepairsFlowIndexView.url resp = await http_client.post( - url, json={"handler": "notify", "issue_id": f"migrate_notify_{DOMAIN}"} + url, json={"handler": "notify", "issue_id": f"migrate_notify_{DOMAIN}_{DOMAIN}"} ) assert resp.status == HTTPStatus.OK data = await resp.json() @@ -74,6 +74,6 @@ async def test_ecobee_repair_flow( # Assert the issue is no longer present assert not issue_registry.async_get_issue( domain="notify", - issue_id="migrate_notify", + issue_id=f"migrate_notify_{DOMAIN}_{DOMAIN}", ) assert len(issue_registry.issues) == 0 diff --git a/tests/components/knx/test_repairs.py b/tests/components/knx/test_repairs.py index 025f298e123..690d6e450cb 100644 --- a/tests/components/knx/test_repairs.py +++ b/tests/components/knx/test_repairs.py @@ -55,13 +55,13 @@ async def test_knx_notify_service_issue( assert len(issue_registry.issues) == 1 assert issue_registry.async_get_issue( domain="notify", - issue_id=f"migrate_notify_{DOMAIN}", + issue_id=f"migrate_notify_{DOMAIN}_notify", ) # Test confirm step in repair flow resp = await http_client.post( RepairsFlowIndexView.url, - json={"handler": "notify", "issue_id": f"migrate_notify_{DOMAIN}"}, + json={"handler": "notify", "issue_id": f"migrate_notify_{DOMAIN}_notify"}, ) assert resp.status == HTTPStatus.OK data = await resp.json() @@ -79,6 +79,6 @@ async def test_knx_notify_service_issue( # Assert the issue is no longer present assert not issue_registry.async_get_issue( domain="notify", - issue_id=f"migrate_notify_{DOMAIN}", + issue_id=f"migrate_notify_{DOMAIN}_notify", ) assert len(issue_registry.issues) == 0 diff --git a/tests/components/notify/test_repairs.py b/tests/components/notify/test_repairs.py index f4e016418fe..fef5818e1e6 100644 --- a/tests/components/notify/test_repairs.py +++ b/tests/components/notify/test_repairs.py @@ -3,6 +3,8 @@ from http import HTTPStatus from unittest.mock import AsyncMock +import pytest + from homeassistant.components.notify import ( DOMAIN as NOTIFY_DOMAIN, migrate_notify_issue, @@ -24,11 +26,17 @@ from tests.typing import ClientSessionGenerator THERMOSTAT_ID = 0 +@pytest.mark.usefixtures("config_flow_fixture") +@pytest.mark.parametrize( + ("service_name", "translation_key"), + [(None, "migrate_notify_test"), ("bla", "migrate_notify_test_bla")], +) async def test_notify_migration_repair_flow( hass: HomeAssistant, hass_client: ClientSessionGenerator, issue_registry: ir.IssueRegistry, - config_flow_fixture: None, + service_name: str | None, + translation_key: str, ) -> None: """Test the notify service repair flow is triggered.""" await async_setup_component(hass, NOTIFY_DOMAIN, {}) @@ -49,18 +57,18 @@ async def test_notify_migration_repair_flow( assert await hass.config_entries.async_setup(config_entry.entry_id) # Simulate legacy service being used and issue being registered - migrate_notify_issue(hass, "test", "Test", "2024.12.0") + migrate_notify_issue(hass, "test", "Test", "2024.12.0", service_name=service_name) await hass.async_block_till_done() # Assert the issue is present assert issue_registry.async_get_issue( domain=NOTIFY_DOMAIN, - issue_id="migrate_notify_test", + issue_id=translation_key, ) assert len(issue_registry.issues) == 1 url = RepairsFlowIndexView.url resp = await http_client.post( - url, json={"handler": NOTIFY_DOMAIN, "issue_id": "migrate_notify_test"} + url, json={"handler": NOTIFY_DOMAIN, "issue_id": translation_key} ) assert resp.status == HTTPStatus.OK data = await resp.json() @@ -79,6 +87,6 @@ async def test_notify_migration_repair_flow( # Assert the issue is no longer present assert not issue_registry.async_get_issue( domain=NOTIFY_DOMAIN, - issue_id="migrate_notify_test", + issue_id=translation_key, ) assert len(issue_registry.issues) == 0 diff --git a/tests/components/tibber/test_repairs.py b/tests/components/tibber/test_repairs.py index 9aaec81618d..89e85e5f8e1 100644 --- a/tests/components/tibber/test_repairs.py +++ b/tests/components/tibber/test_repairs.py @@ -36,13 +36,13 @@ async def test_repair_flow( # Assert the issue is present assert issue_registry.async_get_issue( domain="notify", - issue_id="migrate_notify_tibber", + issue_id=f"migrate_notify_tibber_{service}", ) assert len(issue_registry.issues) == 1 url = RepairsFlowIndexView.url resp = await http_client.post( - url, json={"handler": "notify", "issue_id": "migrate_notify_tibber"} + url, json={"handler": "notify", "issue_id": f"migrate_notify_tibber_{service}"} ) assert resp.status == HTTPStatus.OK data = await resp.json() @@ -61,6 +61,6 @@ async def test_repair_flow( # Assert the issue is no longer present assert not issue_registry.async_get_issue( domain="notify", - issue_id="migrate_notify_tibber", + issue_id=f"migrate_notify_tibber_{service}", ) assert len(issue_registry.issues) == 0 From d0a036c6176db9d0a576cb3bd0475bd6e17597e1 Mon Sep 17 00:00:00 2001 From: Josef Zweck <24647999+zweckj@users.noreply.github.com> Date: Wed, 5 Jun 2024 15:55:37 +0200 Subject: [PATCH 0257/1445] Allow more input params to webhook generate_url helper (#112334) * allow more params to helper * switch back to f-string * add test * switch to proper method * add allow_external, internal params * fx default * add signature comparison * remove test, change prefer_external --------- Co-authored-by: Erik Montnemery --- homeassistant/components/tedee/__init__.py | 7 ++++--- homeassistant/components/webhook/__init__.py | 11 +++++++++-- tests/components/webhook/test_init.py | 16 ++++++++++++++++ 3 files changed, 29 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/tedee/__init__.py b/homeassistant/components/tedee/__init__.py index b661d993db8..a1b87cf13a4 100644 --- a/homeassistant/components/tedee/__init__.py +++ b/homeassistant/components/tedee/__init__.py @@ -12,7 +12,7 @@ from pytedee_async.exception import TedeeDataUpdateException, TedeeWebhookExcept from homeassistant.components.http import HomeAssistantView from homeassistant.components.webhook import ( async_generate_id as webhook_generate_id, - async_generate_path as webhook_generate_path, + async_generate_url as webhook_generate_url, async_register as webhook_register, async_unregister as webhook_unregister, ) @@ -66,8 +66,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: TedeeConfigEntry) -> boo await coordinator.tedee_client.cleanup_webhooks_by_host(instance_url) except (TedeeDataUpdateException, TedeeWebhookException) as ex: _LOGGER.warning("Failed to cleanup Tedee webhooks by host: %s", ex) - webhook_url = ( - f"{instance_url}{webhook_generate_path(entry.data[CONF_WEBHOOK_ID])}" + + webhook_url = webhook_generate_url( + hass, entry.data[CONF_WEBHOOK_ID], allow_external=False, allow_ip=True ) webhook_name = "Tedee" if entry.title != NAME: diff --git a/homeassistant/components/webhook/__init__.py b/homeassistant/components/webhook/__init__.py index 04234b2ac42..7d282b8aef3 100644 --- a/homeassistant/components/webhook/__init__.py +++ b/homeassistant/components/webhook/__init__.py @@ -87,10 +87,17 @@ def async_generate_id() -> str: @callback @bind_hass -def async_generate_url(hass: HomeAssistant, webhook_id: str) -> str: +def async_generate_url( + hass: HomeAssistant, + webhook_id: str, + allow_internal: bool = True, + allow_external: bool = True, + allow_ip: bool | None = None, + prefer_external: bool | None = True, +) -> str: """Generate the full URL for a webhook_id.""" return ( - f"{get_url(hass, prefer_external=True, allow_cloud=False)}" + f"{get_url(hass,allow_internal=allow_internal, allow_external=allow_external, allow_cloud=False, allow_ip=allow_ip, prefer_external=prefer_external,)}" f"{async_generate_path(webhook_id)}" ) diff --git a/tests/components/webhook/test_init.py b/tests/components/webhook/test_init.py index b3d309f1f24..6f4ae1ebefc 100644 --- a/tests/components/webhook/test_init.py +++ b/tests/components/webhook/test_init.py @@ -56,6 +56,22 @@ async def test_generate_webhook_url(hass: HomeAssistant) -> None: assert url == "https://example.com/api/webhook/some_id" +async def test_generate_webhook_url_internal(hass: HomeAssistant) -> None: + """Test we can get the internal URL.""" + await async_process_ha_core_config( + hass, + { + "internal_url": "http://192.168.1.100:8123", + "external_url": "https://example.com", + }, + ) + url = webhook.async_generate_url( + hass, "some_id", allow_external=False, allow_ip=True + ) + + assert url == "http://192.168.1.100:8123/api/webhook/some_id" + + async def test_async_generate_path(hass: HomeAssistant) -> None: """Test generating just the path component of the url correctly.""" path = webhook.async_generate_path("some_id") From 906c9066532b9e418b86d82676263b977c56fe90 Mon Sep 17 00:00:00 2001 From: Dmitriy Date: Wed, 5 Jun 2024 17:55:59 +0400 Subject: [PATCH 0258/1445] Fix Ezviz last alarm picture (#112074) * Fix Ezviz last alarm picture * Review fixes --- homeassistant/components/ezviz/image.py | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/ezviz/image.py b/homeassistant/components/ezviz/image.py index 0c362f8cbe7..0fbc5cc6a68 100644 --- a/homeassistant/components/ezviz/image.py +++ b/homeassistant/components/ezviz/image.py @@ -4,8 +4,12 @@ from __future__ import annotations import logging +from pyezviz.exceptions import PyEzvizError +from pyezviz.utils import decrypt_image + from homeassistant.components.image import Image, ImageEntity, ImageEntityDescription from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_PASSWORD from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import dt as dt_util @@ -51,12 +55,28 @@ class EzvizLastMotion(EzvizEntity, ImageEntity): self._attr_image_last_updated = dt_util.parse_datetime( str(self.data["last_alarm_time"]) ) + camera = hass.config_entries.async_entry_for_domain_unique_id(DOMAIN, serial) + self.alarm_image_password = ( + camera.data[CONF_PASSWORD] if camera is not None else None + ) async def _async_load_image_from_url(self, url: str) -> Image | None: """Load an image by url.""" if response := await self._fetch_url(url): + image_data = response.content + if self.data["encrypted"] and self.alarm_image_password is not None: + try: + image_data = decrypt_image( + response.content, self.alarm_image_password + ) + except PyEzvizError: + _LOGGER.warning( + "%s: Can't decrypt last alarm picture, looks like it was encrypted with other password", + self.entity_id, + ) + image_data = response.content return Image( - content=response.content, + content=image_data, content_type="image/jpeg", # Actually returns binary/octet-stream ) return None From 60c06732b1e72311bae4150b2a5fb18a817478fe Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 5 Jun 2024 16:06:23 +0200 Subject: [PATCH 0259/1445] Add state and device class to incomfort (#118877) * Add state and device class to incomfort * Add state and device class to incomfort --- .../components/incomfort/binary_sensor.py | 2 ++ homeassistant/components/incomfort/sensor.py | 4 ++++ .../incomfort/snapshots/test_binary_sensor.ambr | 3 ++- .../incomfort/snapshots/test_sensor.ambr | 15 ++++++++++++--- 4 files changed, 20 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/incomfort/binary_sensor.py b/homeassistant/components/incomfort/binary_sensor.py index e3563c183da..580001238bd 100644 --- a/homeassistant/components/incomfort/binary_sensor.py +++ b/homeassistant/components/incomfort/binary_sensor.py @@ -9,6 +9,7 @@ from typing import Any from incomfortclient import Heater as InComfortHeater from homeassistant.components.binary_sensor import ( + BinarySensorDeviceClass, BinarySensorEntity, BinarySensorEntityDescription, ) @@ -32,6 +33,7 @@ SENSOR_TYPES: tuple[IncomfortBinarySensorEntityDescription, ...] = ( IncomfortBinarySensorEntityDescription( key="failed", name="Fault", + device_class=BinarySensorDeviceClass.PROBLEM, value_key="is_failed", extra_state_attributes_fn=lambda status: {"fault_code": status["fault_code"]}, ), diff --git a/homeassistant/components/incomfort/sensor.py b/homeassistant/components/incomfort/sensor.py index 4bba56382b3..191d3715122 100644 --- a/homeassistant/components/incomfort/sensor.py +++ b/homeassistant/components/incomfort/sensor.py @@ -11,6 +11,7 @@ from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, SensorEntityDescription, + SensorStateClass, ) from homeassistant.const import UnitOfPressure, UnitOfTemperature from homeassistant.core import HomeAssistant @@ -42,6 +43,7 @@ SENSOR_TYPES: tuple[IncomfortSensorEntityDescription, ...] = ( key="cv_pressure", name=INCOMFORT_PRESSURE, device_class=SensorDeviceClass.PRESSURE, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfPressure.BAR, value_key="pressure", ), @@ -49,6 +51,7 @@ SENSOR_TYPES: tuple[IncomfortSensorEntityDescription, ...] = ( key="cv_temp", name=INCOMFORT_HEATER_TEMP, device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfTemperature.CELSIUS, extra_key="is_pumping", value_key="heater_temp", @@ -57,6 +60,7 @@ SENSOR_TYPES: tuple[IncomfortSensorEntityDescription, ...] = ( key="tap_temp", name=INCOMFORT_TAP_TEMP, device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfTemperature.CELSIUS, extra_key="is_tapping", value_key="tap_temp", diff --git a/tests/components/incomfort/snapshots/test_binary_sensor.ambr b/tests/components/incomfort/snapshots/test_binary_sensor.ambr index 0316f37502d..e7832459974 100644 --- a/tests/components/incomfort/snapshots/test_binary_sensor.ambr +++ b/tests/components/incomfort/snapshots/test_binary_sensor.ambr @@ -21,7 +21,7 @@ 'name': None, 'options': dict({ }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, 'original_name': 'Fault', 'platform': 'incomfort', @@ -35,6 +35,7 @@ # name: test_setup_platform[binary_sensor.boiler_fault-state] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'problem', 'fault_code': None, 'friendly_name': 'Boiler Fault', }), diff --git a/tests/components/incomfort/snapshots/test_sensor.ambr b/tests/components/incomfort/snapshots/test_sensor.ambr index 831be411b46..3998141c306 100644 --- a/tests/components/incomfort/snapshots/test_sensor.ambr +++ b/tests/components/incomfort/snapshots/test_sensor.ambr @@ -4,7 +4,9 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'device_class': None, 'device_id': , @@ -37,6 +39,7 @@ 'attributes': ReadOnlyDict({ 'device_class': 'pressure', 'friendly_name': 'Boiler CV Pressure', + 'state_class': , 'unit_of_measurement': , }), 'context': , @@ -52,7 +55,9 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'device_class': None, 'device_id': , @@ -86,6 +91,7 @@ 'device_class': 'temperature', 'friendly_name': 'Boiler CV Temp', 'is_pumping': False, + 'state_class': , 'unit_of_measurement': , }), 'context': , @@ -101,7 +107,9 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'device_class': None, 'device_id': , @@ -135,6 +143,7 @@ 'device_class': 'temperature', 'friendly_name': 'Boiler Tap Temp', 'is_tapping': False, + 'state_class': , 'unit_of_measurement': , }), 'context': , From c4cfd9adf09320e961ee5735017a4c14691cd8fc Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 5 Jun 2024 17:03:58 +0200 Subject: [PATCH 0260/1445] Add entity translations to incomfort (#118876) --- .../components/incomfort/binary_sensor.py | 2 +- homeassistant/components/incomfort/sensor.py | 11 +- .../components/incomfort/strings.json | 12 ++ .../snapshots/test_binary_sensor.ambr | 49 +------ .../incomfort/snapshots/test_sensor.ambr | 128 +++++++++--------- tests/components/incomfort/test_init.py | 8 +- 6 files changed, 83 insertions(+), 127 deletions(-) diff --git a/homeassistant/components/incomfort/binary_sensor.py b/homeassistant/components/incomfort/binary_sensor.py index 580001238bd..9a2ec9414eb 100644 --- a/homeassistant/components/incomfort/binary_sensor.py +++ b/homeassistant/components/incomfort/binary_sensor.py @@ -32,7 +32,7 @@ class IncomfortBinarySensorEntityDescription(BinarySensorEntityDescription): SENSOR_TYPES: tuple[IncomfortBinarySensorEntityDescription, ...] = ( IncomfortBinarySensorEntityDescription( key="failed", - name="Fault", + translation_key="fault", device_class=BinarySensorDeviceClass.PROBLEM, value_key="is_failed", extra_state_attributes_fn=lambda status: {"fault_code": status["fault_code"]}, diff --git a/homeassistant/components/incomfort/sensor.py b/homeassistant/components/incomfort/sensor.py index 191d3715122..e0d6740f1d4 100644 --- a/homeassistant/components/incomfort/sensor.py +++ b/homeassistant/components/incomfort/sensor.py @@ -22,10 +22,6 @@ from . import InComfortConfigEntry from .coordinator import InComfortDataCoordinator from .entity import IncomfortBoilerEntity -INCOMFORT_HEATER_TEMP = "CV Temp" -INCOMFORT_PRESSURE = "CV Pressure" -INCOMFORT_TAP_TEMP = "Tap Temp" - @dataclass(frozen=True, kw_only=True) class IncomfortSensorEntityDescription(SensorEntityDescription): @@ -33,15 +29,11 @@ class IncomfortSensorEntityDescription(SensorEntityDescription): value_key: str extra_key: str | None = None - # IncomfortSensor does not support UNDEFINED or None, - # restrict the type to str - name: str = "" SENSOR_TYPES: tuple[IncomfortSensorEntityDescription, ...] = ( IncomfortSensorEntityDescription( key="cv_pressure", - name=INCOMFORT_PRESSURE, device_class=SensorDeviceClass.PRESSURE, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfPressure.BAR, @@ -49,7 +41,6 @@ SENSOR_TYPES: tuple[IncomfortSensorEntityDescription, ...] = ( ), IncomfortSensorEntityDescription( key="cv_temp", - name=INCOMFORT_HEATER_TEMP, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfTemperature.CELSIUS, @@ -58,7 +49,7 @@ SENSOR_TYPES: tuple[IncomfortSensorEntityDescription, ...] = ( ), IncomfortSensorEntityDescription( key="tap_temp", - name=INCOMFORT_TAP_TEMP, + translation_key="tap_temperature", device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfTemperature.CELSIUS, diff --git a/homeassistant/components/incomfort/strings.json b/homeassistant/components/incomfort/strings.json index e94c2e508ad..d4c01e4d0ed 100644 --- a/homeassistant/components/incomfort/strings.json +++ b/homeassistant/components/incomfort/strings.json @@ -52,5 +52,17 @@ "title": "YAML import failed because of timeout issues", "description": "Configuring {integration_title} using YAML is being removed but there was a timeout while connecting to your {integration_title} while importing your existing configuration.\nSetup will not proceed.\n\nVerify that your {integration_title} is operating correctly and restart Home Assistant to attempt the import again.\n\nAlternatively, you may remove the `{domain}` configuration from your configuration.yaml entirely, restart Home Assistant, and add the {integration_title} integration manually." } + }, + "entity": { + "binary_sensor": { + "fault": { + "name": "Fault" + } + }, + "sensor": { + "tap_temperature": { + "name": "Tap temperature" + } + } } } diff --git a/tests/components/incomfort/snapshots/test_binary_sensor.ambr b/tests/components/incomfort/snapshots/test_binary_sensor.ambr index e7832459974..633f3fdf01c 100644 --- a/tests/components/incomfort/snapshots/test_binary_sensor.ambr +++ b/tests/components/incomfort/snapshots/test_binary_sensor.ambr @@ -27,7 +27,7 @@ 'platform': 'incomfort', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'fault', 'unique_id': 'c0ffeec0ffee_failed', 'unit_of_measurement': None, }) @@ -47,50 +47,3 @@ 'state': 'off', }) # --- -# name: test_setup_platforms[binary_sensor.boiler_fault-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'binary_sensor', - 'entity_category': None, - 'entity_id': 'binary_sensor.boiler_fault', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Fault', - 'platform': 'incomfort', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': 'c0ffeec0ffee_failed', - 'unit_of_measurement': None, - }) -# --- -# name: test_setup_platforms[binary_sensor.boiler_fault-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'fault_code': None, - 'friendly_name': 'Boiler Fault', - }), - 'context': , - 'entity_id': 'binary_sensor.boiler_fault', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- diff --git a/tests/components/incomfort/snapshots/test_sensor.ambr b/tests/components/incomfort/snapshots/test_sensor.ambr index 3998141c306..8c9ea60f455 100644 --- a/tests/components/incomfort/snapshots/test_sensor.ambr +++ b/tests/components/incomfort/snapshots/test_sensor.ambr @@ -1,5 +1,5 @@ # serializer version: 1 -# name: test_setup_platform[sensor.boiler_cv_pressure-entry] +# name: test_setup_platform[sensor.boiler_pressure-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -13,7 +13,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.boiler_cv_pressure', + 'entity_id': 'sensor.boiler_pressure', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -25,7 +25,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'CV Pressure', + 'original_name': 'Pressure', 'platform': 'incomfort', 'previous_unique_id': None, 'supported_features': 0, @@ -34,23 +34,23 @@ 'unit_of_measurement': , }) # --- -# name: test_setup_platform[sensor.boiler_cv_pressure-state] +# name: test_setup_platform[sensor.boiler_pressure-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'pressure', - 'friendly_name': 'Boiler CV Pressure', + 'friendly_name': 'Boiler Pressure', 'state_class': , 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.boiler_cv_pressure', + 'entity_id': 'sensor.boiler_pressure', 'last_changed': , 'last_reported': , 'last_updated': , 'state': '1.86', }) # --- -# name: test_setup_platform[sensor.boiler_cv_temp-entry] +# name: test_setup_platform[sensor.boiler_tap_temperature-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -64,7 +64,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.boiler_cv_temp', + 'entity_id': 'sensor.boiler_tap_temperature', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -76,7 +76,59 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'CV Temp', + 'original_name': 'Tap temperature', + 'platform': 'incomfort', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'tap_temperature', + 'unique_id': 'c0ffeec0ffee_tap_temp', + 'unit_of_measurement': , + }) +# --- +# name: test_setup_platform[sensor.boiler_tap_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Boiler Tap temperature', + 'is_tapping': False, + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.boiler_tap_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '30.21', + }) +# --- +# name: test_setup_platform[sensor.boiler_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.boiler_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Temperature', 'platform': 'incomfort', 'previous_unique_id': None, 'supported_features': 0, @@ -85,72 +137,20 @@ 'unit_of_measurement': , }) # --- -# name: test_setup_platform[sensor.boiler_cv_temp-state] +# name: test_setup_platform[sensor.boiler_temperature-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'temperature', - 'friendly_name': 'Boiler CV Temp', + 'friendly_name': 'Boiler Temperature', 'is_pumping': False, 'state_class': , 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.boiler_cv_temp', + 'entity_id': 'sensor.boiler_temperature', 'last_changed': , 'last_reported': , 'last_updated': , 'state': '35.34', }) # --- -# name: test_setup_platform[sensor.boiler_tap_temp-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.boiler_tap_temp', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Tap Temp', - 'platform': 'incomfort', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': 'c0ffeec0ffee_tap_temp', - 'unit_of_measurement': , - }) -# --- -# name: test_setup_platform[sensor.boiler_tap_temp-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'temperature', - 'friendly_name': 'Boiler Tap Temp', - 'is_tapping': False, - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.boiler_tap_temp', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '30.21', - }) -# --- diff --git a/tests/components/incomfort/test_init.py b/tests/components/incomfort/test_init.py index 47365a836d2..0390a47a616 100644 --- a/tests/components/incomfort/test_init.py +++ b/tests/components/incomfort/test_init.py @@ -41,7 +41,7 @@ async def test_coordinator_updates( assert state.attributes["current_temperature"] == 21.4 mock_incomfort().mock_room_status["room_temp"] = 20.91 - state = hass.states.get("sensor.boiler_cv_pressure") + state = hass.states.get("sensor.boiler_pressure") assert state is not None assert state.state == "1.86" mock_incomfort().mock_heater_status["pressure"] = 1.84 @@ -54,7 +54,7 @@ async def test_coordinator_updates( assert state is not None assert state.attributes["current_temperature"] == 20.9 - state = hass.states.get("sensor.boiler_cv_pressure") + state = hass.states.get("sensor.boiler_pressure") assert state is not None assert state.state == "1.84" @@ -77,7 +77,7 @@ async def test_coordinator_update_fails( ) -> None: """Test the incomfort coordinator update fails.""" await hass.config_entries.async_setup(mock_config_entry.entry_id) - state = hass.states.get("sensor.boiler_cv_pressure") + state = hass.states.get("sensor.boiler_pressure") assert state is not None assert state.state == "1.86" @@ -88,6 +88,6 @@ async def test_coordinator_update_fails( async_fire_time_changed(hass) await hass.async_block_till_done(wait_background_tasks=True) - state = hass.states.get("sensor.boiler_cv_pressure") + state = hass.states.get("sensor.boiler_pressure") assert state is not None assert state.state == STATE_UNAVAILABLE From 862c04a4b6599f96f517de76ac056643e3991596 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 5 Jun 2024 17:04:28 +0200 Subject: [PATCH 0261/1445] Use fixtures in UniFi service tests (#118838) * Use fixtures in UniFi service tests * Fix comments --- tests/components/unifi/test_services.py | 213 ++++++++++++------------ 1 file changed, 109 insertions(+), 104 deletions(-) diff --git a/tests/components/unifi/test_services.py b/tests/components/unifi/test_services.py index 8cd029b1cf5..210d52d1fb9 100644 --- a/tests/components/unifi/test_services.py +++ b/tests/components/unifi/test_services.py @@ -1,34 +1,35 @@ """deCONZ service tests.""" +from typing import Any +from unittest.mock import PropertyMock, patch + +import pytest + from homeassistant.components.unifi.const import CONF_SITE_ID, DOMAIN as UNIFI_DOMAIN from homeassistant.components.unifi.services import ( SERVICE_RECONNECT_CLIENT, SERVICE_REMOVE_CLIENTS, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_DEVICE_ID, CONF_HOST from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr -from .test_hub import setup_unifi_integration - from tests.test_util.aiohttp import AiohttpClientMocker +@pytest.mark.parametrize( + "client_payload", [[{"is_wired": False, "mac": "00:00:00:00:00:01"}]] +) async def test_reconnect_client( hass: HomeAssistant, device_registry: dr.DeviceRegistry, aioclient_mock: AiohttpClientMocker, + config_entry_setup: ConfigEntry, + client_payload: list[dict[str, Any]], ) -> None: """Verify call to reconnect client is performed as expected.""" - clients = [ - { - "is_wired": False, - "mac": "00:00:00:00:00:01", - } - ] - config_entry = await setup_unifi_integration( - hass, aioclient_mock, clients_response=clients - ) + config_entry = config_entry_setup aioclient_mock.clear_requests() aioclient_mock.post( @@ -38,7 +39,7 @@ async def test_reconnect_client( device_entry = device_registry.async_get_or_create( config_entry_id=config_entry.entry_id, - connections={(dr.CONNECTION_NETWORK_MAC, clients[0]["mac"])}, + connections={(dr.CONNECTION_NETWORK_MAC, client_payload[0]["mac"])}, ) await hass.services.async_call( @@ -50,12 +51,11 @@ async def test_reconnect_client( assert aioclient_mock.call_count == 1 +@pytest.mark.usefixtures("config_entry_setup") async def test_reconnect_non_existant_device( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Verify no call is made if device does not exist.""" - await setup_unifi_integration(hass, aioclient_mock) - aioclient_mock.clear_requests() await hass.services.async_call( @@ -71,9 +71,10 @@ async def test_reconnect_device_without_mac( hass: HomeAssistant, device_registry: dr.DeviceRegistry, aioclient_mock: AiohttpClientMocker, + config_entry_setup: ConfigEntry, ) -> None: """Verify no call is made if device does not have a known mac.""" - config_entry = await setup_unifi_integration(hass, aioclient_mock) + config_entry = config_entry_setup aioclient_mock.clear_requests() @@ -91,23 +92,18 @@ async def test_reconnect_device_without_mac( assert aioclient_mock.call_count == 0 +@pytest.mark.parametrize( + "client_payload", [[{"is_wired": False, "mac": "00:00:00:00:00:01"}]] +) async def test_reconnect_client_hub_unavailable( hass: HomeAssistant, device_registry: dr.DeviceRegistry, aioclient_mock: AiohttpClientMocker, + config_entry_setup: ConfigEntry, + client_payload: list[dict[str, Any]], ) -> None: """Verify no call is made if hub is unavailable.""" - clients = [ - { - "is_wired": False, - "mac": "00:00:00:00:00:01", - } - ] - config_entry = await setup_unifi_integration( - hass, aioclient_mock, clients_response=clients - ) - hub = config_entry.runtime_data - hub.websocket.available = False + config_entry = config_entry_setup aioclient_mock.clear_requests() aioclient_mock.post( @@ -117,15 +113,19 @@ async def test_reconnect_client_hub_unavailable( device_entry = device_registry.async_get_or_create( config_entry_id=config_entry.entry_id, - connections={(dr.CONNECTION_NETWORK_MAC, clients[0]["mac"])}, + connections={(dr.CONNECTION_NETWORK_MAC, client_payload[0]["mac"])}, ) - await hass.services.async_call( - UNIFI_DOMAIN, - SERVICE_RECONNECT_CLIENT, - service_data={ATTR_DEVICE_ID: device_entry.id}, - blocking=True, - ) + with patch( + "homeassistant.components.unifi.UnifiHub.available", new_callable=PropertyMock + ) as ws_mock: + ws_mock.return_value = False + await hass.services.async_call( + UNIFI_DOMAIN, + SERVICE_RECONNECT_CLIENT, + service_data={ATTR_DEVICE_ID: device_entry.id}, + blocking=True, + ) assert aioclient_mock.call_count == 0 @@ -133,9 +133,10 @@ async def test_reconnect_client_unknown_mac( hass: HomeAssistant, device_registry: dr.DeviceRegistry, aioclient_mock: AiohttpClientMocker, + config_entry_setup: ConfigEntry, ) -> None: """Verify no call is made if trying to reconnect a mac unknown to hub.""" - config_entry = await setup_unifi_integration(hass, aioclient_mock) + config_entry = config_entry_setup aioclient_mock.clear_requests() @@ -153,27 +154,24 @@ async def test_reconnect_client_unknown_mac( assert aioclient_mock.call_count == 0 +@pytest.mark.parametrize( + "client_payload", [[{"is_wired": True, "mac": "00:00:00:00:00:01"}]] +) async def test_reconnect_wired_client( hass: HomeAssistant, device_registry: dr.DeviceRegistry, aioclient_mock: AiohttpClientMocker, + config_entry_setup: ConfigEntry, + client_payload: list[dict[str, Any]], ) -> None: """Verify no call is made if client is wired.""" - clients = [ - { - "is_wired": True, - "mac": "00:00:00:00:00:01", - } - ] - config_entry = await setup_unifi_integration( - hass, aioclient_mock, clients_response=clients - ) + config_entry = config_entry_setup aioclient_mock.clear_requests() device_entry = device_registry.async_get_or_create( config_entry_id=config_entry.entry_id, - connections={(dr.CONNECTION_NETWORK_MAC, clients[0]["mac"])}, + connections={(dr.CONNECTION_NETWORK_MAC, client_payload[0]["mac"])}, ) await hass.services.async_call( @@ -185,46 +183,43 @@ async def test_reconnect_wired_client( assert aioclient_mock.call_count == 0 +@pytest.mark.parametrize( + "clients_all_payload", + [ + [ + { + "mac": "00:00:00:00:00:00", + }, + {"first_seen": 100, "last_seen": 500, "mac": "00:00:00:00:00:01"}, + {"first_seen": 100, "last_seen": 1100, "mac": "00:00:00:00:00:02"}, + { + "first_seen": 100, + "last_seen": 500, + "fixed_ip": "1.2.3.4", + "mac": "00:00:00:00:00:03", + }, + { + "first_seen": 100, + "last_seen": 500, + "hostname": "hostname", + "mac": "00:00:00:00:00:04", + }, + { + "first_seen": 100, + "last_seen": 500, + "name": "name", + "mac": "00:00:00:00:00:05", + }, + ] + ], +) async def test_remove_clients( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, + config_entry_setup: ConfigEntry, ) -> None: """Verify removing different variations of clients work.""" - clients = [ - { - "mac": "00:00:00:00:00:00", - }, - { - "first_seen": 100, - "last_seen": 500, - "mac": "00:00:00:00:00:01", - }, - { - "first_seen": 100, - "last_seen": 1100, - "mac": "00:00:00:00:00:02", - }, - { - "first_seen": 100, - "last_seen": 500, - "fixed_ip": "1.2.3.4", - "mac": "00:00:00:00:00:03", - }, - { - "first_seen": 100, - "last_seen": 500, - "hostname": "hostname", - "mac": "00:00:00:00:00:04", - }, - { - "first_seen": 100, - "last_seen": 500, - "name": "name", - "mac": "00:00:00:00:00:05", - }, - ] - config_entry = await setup_unifi_integration( - hass, aioclient_mock, clients_all_response=clients - ) + config_entry = config_entry_setup aioclient_mock.clear_requests() aioclient_mock.post( @@ -241,42 +236,52 @@ async def test_remove_clients( assert await hass.config_entries.async_unload(config_entry.entry_id) +@pytest.mark.parametrize( + "clients_all_payload", + [ + [ + { + "first_seen": 100, + "last_seen": 500, + "mac": "00:00:00:00:00:01", + } + ] + ], +) +@pytest.mark.usefixtures("config_entry_setup") async def test_remove_clients_hub_unavailable( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Verify no call is made if UniFi Network is unavailable.""" - clients = [ - { - "first_seen": 100, - "last_seen": 500, - "mac": "00:00:00:00:00:01", - } - ] - config_entry = await setup_unifi_integration( - hass, aioclient_mock, clients_all_response=clients - ) - hub = config_entry.runtime_data - hub.websocket.available = False - aioclient_mock.clear_requests() - await hass.services.async_call(UNIFI_DOMAIN, SERVICE_REMOVE_CLIENTS, blocking=True) + with patch( + "homeassistant.components.unifi.UnifiHub.available", new_callable=PropertyMock + ) as ws_mock: + ws_mock.return_value = False + await hass.services.async_call( + UNIFI_DOMAIN, SERVICE_REMOVE_CLIENTS, blocking=True + ) assert aioclient_mock.call_count == 0 +@pytest.mark.parametrize( + "clients_all_payload", + [ + [ + { + "first_seen": 100, + "last_seen": 1100, + "mac": "00:00:00:00:00:01", + } + ] + ], +) +@pytest.mark.usefixtures("config_entry_setup") async def test_remove_clients_no_call_on_empty_list( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Verify no call is made if no fitting client has been added to the list.""" - clients = [ - { - "first_seen": 100, - "last_seen": 1100, - "mac": "00:00:00:00:00:01", - } - ] - await setup_unifi_integration(hass, aioclient_mock, clients_all_response=clients) - aioclient_mock.clear_requests() await hass.services.async_call(UNIFI_DOMAIN, SERVICE_REMOVE_CLIENTS, blocking=True) From 0487b38ed3c1dadb8ad5f5ebee65f45bdc5e8f13 Mon Sep 17 00:00:00 2001 From: r-xyz <100710244+r-xyz@users.noreply.github.com> Date: Wed, 5 Jun 2024 17:41:40 +0200 Subject: [PATCH 0262/1445] Add support for sending telegram messages to topics (#112715) * Add support for sending telegram messages to topics Based on original PR #104059 by [jgresty](https://github.com/jgresty). Did not manage to merge conflicts, so I remade the changes from scratch, including suggestions from previous PR reviews. Topics were added to telegram groups in November 2022, and to the telegram-bot library in version 20.0. They are a purely additive change that is exposed by a single parameter `message_thread_id`. Not passing this parameter will not change the behaviour from current. This same parameter is used to send messages to threads and messages to topics inside groups. https://telegram.org/blog/topics-in-groups-collectible-usernames/it?setln=en#topics-in-groups Fixes #81888 Fixes #91750 * telegram_bot: add tests for threads feature. * telegram_bot: fixed tests for threads. * telegram_bot: fixed wrong line. * Update test_telegram_bot.py --------- Co-authored-by: J. Nick Koston Co-authored-by: Erik Montnemery --- homeassistant/components/telegram/notify.py | 6 ++++ .../components/telegram_bot/__init__.py | 18 ++++++++++ .../components/telegram_bot/services.yaml | 36 +++++++++++++++++++ .../components/telegram_bot/strings.json | 36 +++++++++++++++++++ .../telegram_bot/test_telegram_bot.py | 22 +++++++++++- 5 files changed, 117 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/telegram/notify.py b/homeassistant/components/telegram/notify.py index df20b98070c..16952868525 100644 --- a/homeassistant/components/telegram/notify.py +++ b/homeassistant/components/telegram/notify.py @@ -18,6 +18,7 @@ from homeassistant.components.telegram_bot import ( ATTR_DISABLE_NOTIF, ATTR_DISABLE_WEB_PREV, ATTR_MESSAGE_TAG, + ATTR_MESSAGE_THREAD_ID, ATTR_PARSER, ) from homeassistant.const import ATTR_LOCATION @@ -91,6 +92,11 @@ class TelegramNotificationService(BaseNotificationService): disable_web_page_preview = data[ATTR_DISABLE_WEB_PREV] service_data.update({ATTR_DISABLE_WEB_PREV: disable_web_page_preview}) + # Set message_thread_id + if data is not None and ATTR_MESSAGE_THREAD_ID in data: + message_thread_id = data[ATTR_MESSAGE_THREAD_ID] + service_data.update({ATTR_MESSAGE_THREAD_ID: message_thread_id}) + # Get keyboard info if data is not None and ATTR_KEYBOARD in data: keys = data.get(ATTR_KEYBOARD) diff --git a/homeassistant/components/telegram_bot/__init__.py b/homeassistant/components/telegram_bot/__init__.py index 06c15da5f70..f37a84a83a6 100644 --- a/homeassistant/components/telegram_bot/__init__.py +++ b/homeassistant/components/telegram_bot/__init__.py @@ -90,6 +90,7 @@ ATTR_ANSWERS = "answers" ATTR_OPEN_PERIOD = "open_period" ATTR_IS_ANONYMOUS = "is_anonymous" ATTR_ALLOWS_MULTIPLE_ANSWERS = "allows_multiple_answers" +ATTR_MESSAGE_THREAD_ID = "message_thread_id" CONF_ALLOWED_CHAT_IDS = "allowed_chat_ids" CONF_PROXY_URL = "proxy_url" @@ -639,6 +640,7 @@ class TelegramNotificationService: ATTR_REPLYMARKUP: None, ATTR_TIMEOUT: None, ATTR_MESSAGE_TAG: None, + ATTR_MESSAGE_THREAD_ID: None, } if data is not None: if ATTR_PARSER in data: @@ -655,6 +657,8 @@ class TelegramNotificationService: params[ATTR_REPLY_TO_MSGID] = data[ATTR_REPLY_TO_MSGID] if ATTR_MESSAGE_TAG in data: params[ATTR_MESSAGE_TAG] = data[ATTR_MESSAGE_TAG] + if ATTR_MESSAGE_THREAD_ID in data: + params[ATTR_MESSAGE_THREAD_ID] = data[ATTR_MESSAGE_THREAD_ID] # Keyboards: if ATTR_KEYBOARD in data: keys = data.get(ATTR_KEYBOARD) @@ -698,6 +702,10 @@ class TelegramNotificationService: } if message_tag is not None: event_data[ATTR_MESSAGE_TAG] = message_tag + if kwargs_msg[ATTR_MESSAGE_THREAD_ID] is not None: + event_data[ATTR_MESSAGE_THREAD_ID] = kwargs_msg[ + ATTR_MESSAGE_THREAD_ID + ] self.hass.bus.async_fire( EVENT_TELEGRAM_SENT, event_data, context=context ) @@ -731,6 +739,7 @@ class TelegramNotificationService: reply_to_message_id=params[ATTR_REPLY_TO_MSGID], reply_markup=params[ATTR_REPLYMARKUP], read_timeout=params[ATTR_TIMEOUT], + message_thread_id=params[ATTR_MESSAGE_THREAD_ID], context=context, ) @@ -864,6 +873,7 @@ class TelegramNotificationService: reply_markup=params[ATTR_REPLYMARKUP], read_timeout=params[ATTR_TIMEOUT], parse_mode=params[ATTR_PARSER], + message_thread_id=params[ATTR_MESSAGE_THREAD_ID], context=context, ) @@ -878,6 +888,7 @@ class TelegramNotificationService: reply_to_message_id=params[ATTR_REPLY_TO_MSGID], reply_markup=params[ATTR_REPLYMARKUP], read_timeout=params[ATTR_TIMEOUT], + message_thread_id=params[ATTR_MESSAGE_THREAD_ID], context=context, ) @@ -894,6 +905,7 @@ class TelegramNotificationService: reply_markup=params[ATTR_REPLYMARKUP], read_timeout=params[ATTR_TIMEOUT], parse_mode=params[ATTR_PARSER], + message_thread_id=params[ATTR_MESSAGE_THREAD_ID], context=context, ) elif file_type == SERVICE_SEND_DOCUMENT: @@ -909,6 +921,7 @@ class TelegramNotificationService: reply_markup=params[ATTR_REPLYMARKUP], read_timeout=params[ATTR_TIMEOUT], parse_mode=params[ATTR_PARSER], + message_thread_id=params[ATTR_MESSAGE_THREAD_ID], context=context, ) elif file_type == SERVICE_SEND_VOICE: @@ -923,6 +936,7 @@ class TelegramNotificationService: reply_to_message_id=params[ATTR_REPLY_TO_MSGID], reply_markup=params[ATTR_REPLYMARKUP], read_timeout=params[ATTR_TIMEOUT], + message_thread_id=params[ATTR_MESSAGE_THREAD_ID], context=context, ) elif file_type == SERVICE_SEND_ANIMATION: @@ -938,6 +952,7 @@ class TelegramNotificationService: reply_markup=params[ATTR_REPLYMARKUP], read_timeout=params[ATTR_TIMEOUT], parse_mode=params[ATTR_PARSER], + message_thread_id=params[ATTR_MESSAGE_THREAD_ID], context=context, ) @@ -961,6 +976,7 @@ class TelegramNotificationService: reply_to_message_id=params[ATTR_REPLY_TO_MSGID], reply_markup=params[ATTR_REPLYMARKUP], read_timeout=params[ATTR_TIMEOUT], + message_thread_id=params[ATTR_MESSAGE_THREAD_ID], context=context, ) else: @@ -987,6 +1003,7 @@ class TelegramNotificationService: disable_notification=params[ATTR_DISABLE_NOTIF], reply_to_message_id=params[ATTR_REPLY_TO_MSGID], read_timeout=params[ATTR_TIMEOUT], + message_thread_id=params[ATTR_MESSAGE_THREAD_ID], context=context, ) @@ -1018,6 +1035,7 @@ class TelegramNotificationService: disable_notification=params[ATTR_DISABLE_NOTIF], reply_to_message_id=params[ATTR_REPLY_TO_MSGID], read_timeout=params[ATTR_TIMEOUT], + message_thread_id=params[ATTR_MESSAGE_THREAD_ID], context=context, ) diff --git a/homeassistant/components/telegram_bot/services.yaml b/homeassistant/components/telegram_bot/services.yaml index d2195c1d6ce..a09f4d8f79b 100644 --- a/homeassistant/components/telegram_bot/services.yaml +++ b/homeassistant/components/telegram_bot/services.yaml @@ -54,6 +54,10 @@ send_message: selector: number: mode: box + message_thread_id: + selector: + number: + mode: box send_photo: fields: @@ -126,6 +130,10 @@ send_photo: selector: number: mode: box + message_thread_id: + selector: + number: + mode: box send_sticker: fields: @@ -190,6 +198,10 @@ send_sticker: selector: number: mode: box + message_thread_id: + selector: + number: + mode: box send_animation: fields: @@ -262,6 +274,10 @@ send_animation: selector: number: mode: box + message_thread_id: + selector: + number: + mode: box send_video: fields: @@ -334,6 +350,10 @@ send_video: selector: number: mode: box + message_thread_id: + selector: + number: + mode: box send_voice: fields: @@ -398,6 +418,10 @@ send_voice: selector: number: mode: box + message_thread_id: + selector: + number: + mode: box send_document: fields: @@ -470,6 +494,10 @@ send_document: selector: number: mode: box + message_thread_id: + selector: + number: + mode: box send_location: fields: @@ -520,6 +548,10 @@ send_location: selector: number: mode: box + message_thread_id: + selector: + number: + mode: box send_poll: fields: @@ -564,6 +596,10 @@ send_poll: selector: number: mode: box + message_thread_id: + selector: + number: + mode: box edit_message: fields: diff --git a/homeassistant/components/telegram_bot/strings.json b/homeassistant/components/telegram_bot/strings.json index aad42081274..1a02543d4ab 100644 --- a/homeassistant/components/telegram_bot/strings.json +++ b/homeassistant/components/telegram_bot/strings.json @@ -47,6 +47,10 @@ "reply_to_message_id": { "name": "Reply to message id", "description": "Mark the message as a reply to a previous message." + }, + "message_thread_id": { + "name": "Message thread id", + "description": "Unique identifier for the target message thread (topic) of the forum; for forum supergroups only." } } }, @@ -113,6 +117,10 @@ "reply_to_message_id": { "name": "[%key:component::telegram_bot::services::send_message::fields::reply_to_message_id::name%]", "description": "[%key:component::telegram_bot::services::send_message::fields::reply_to_message_id::description%]" + }, + "message_thread_id": { + "name": "[%key:component::telegram_bot::services::send_message::fields::message_thread_id::name%]", + "description": "[%key:component::telegram_bot::services::send_message::fields::message_thread_id::description%]" } } }, @@ -175,6 +183,10 @@ "reply_to_message_id": { "name": "[%key:component::telegram_bot::services::send_message::fields::reply_to_message_id::name%]", "description": "[%key:component::telegram_bot::services::send_message::fields::reply_to_message_id::description%]" + }, + "message_thread_id": { + "name": "[%key:component::telegram_bot::services::send_message::fields::message_thread_id::name%]", + "description": "[%key:component::telegram_bot::services::send_message::fields::message_thread_id::description%]" } } }, @@ -241,6 +253,10 @@ "reply_to_message_id": { "name": "[%key:component::telegram_bot::services::send_message::fields::reply_to_message_id::name%]", "description": "[%key:component::telegram_bot::services::send_message::fields::reply_to_message_id::description%]" + }, + "message_thread_id": { + "name": "[%key:component::telegram_bot::services::send_message::fields::message_thread_id::name%]", + "description": "[%key:component::telegram_bot::services::send_message::fields::message_thread_id::description%]" } } }, @@ -307,6 +323,10 @@ "reply_to_message_id": { "name": "[%key:component::telegram_bot::services::send_message::fields::reply_to_message_id::name%]", "description": "[%key:component::telegram_bot::services::send_message::fields::reply_to_message_id::description%]" + }, + "message_thread_id": { + "name": "[%key:component::telegram_bot::services::send_message::fields::message_thread_id::name%]", + "description": "[%key:component::telegram_bot::services::send_message::fields::message_thread_id::description%]" } } }, @@ -369,6 +389,10 @@ "reply_to_message_id": { "name": "[%key:component::telegram_bot::services::send_message::fields::reply_to_message_id::name%]", "description": "[%key:component::telegram_bot::services::send_message::fields::reply_to_message_id::description%]" + }, + "message_thread_id": { + "name": "[%key:component::telegram_bot::services::send_message::fields::message_thread_id::name%]", + "description": "[%key:component::telegram_bot::services::send_message::fields::message_thread_id::description%]" } } }, @@ -435,6 +459,10 @@ "reply_to_message_id": { "name": "[%key:component::telegram_bot::services::send_message::fields::reply_to_message_id::name%]", "description": "[%key:component::telegram_bot::services::send_message::fields::reply_to_message_id::description%]" + }, + "message_thread_id": { + "name": "[%key:component::telegram_bot::services::send_message::fields::message_thread_id::name%]", + "description": "[%key:component::telegram_bot::services::send_message::fields::message_thread_id::description%]" } } }, @@ -477,6 +505,10 @@ "reply_to_message_id": { "name": "[%key:component::telegram_bot::services::send_message::fields::reply_to_message_id::name%]", "description": "[%key:component::telegram_bot::services::send_message::fields::reply_to_message_id::description%]" + }, + "message_thread_id": { + "name": "[%key:component::telegram_bot::services::send_message::fields::message_thread_id::name%]", + "description": "[%key:component::telegram_bot::services::send_message::fields::message_thread_id::description%]" } } }, @@ -523,6 +555,10 @@ "reply_to_message_id": { "name": "[%key:component::telegram_bot::services::send_message::fields::reply_to_message_id::name%]", "description": "[%key:component::telegram_bot::services::send_message::fields::reply_to_message_id::description%]" + }, + "message_thread_id": { + "name": "[%key:component::telegram_bot::services::send_message::fields::message_thread_id::name%]", + "description": "[%key:component::telegram_bot::services::send_message::fields::message_thread_id::description%]" } } }, diff --git a/tests/components/telegram_bot/test_telegram_bot.py b/tests/components/telegram_bot/test_telegram_bot.py index b748b58ad1a..aad758827ca 100644 --- a/tests/components/telegram_bot/test_telegram_bot.py +++ b/tests/components/telegram_bot/test_telegram_bot.py @@ -6,6 +6,7 @@ from telegram import Update from homeassistant.components.telegram_bot import ( ATTR_MESSAGE, + ATTR_MESSAGE_THREAD_ID, DOMAIN, SERVICE_SEND_MESSAGE, ) @@ -35,7 +36,7 @@ async def test_send_message(hass: HomeAssistant, webhook_platform) -> None: await hass.services.async_call( DOMAIN, SERVICE_SEND_MESSAGE, - {ATTR_MESSAGE: "test_message"}, + {ATTR_MESSAGE: "test_message", ATTR_MESSAGE_THREAD_ID: "123"}, blocking=True, context=context, ) @@ -45,6 +46,25 @@ async def test_send_message(hass: HomeAssistant, webhook_platform) -> None: assert events[0].context == context +async def test_send_message_thread(hass: HomeAssistant, webhook_platform) -> None: + """Test the send_message service for threads.""" + context = Context() + events = async_capture_events(hass, "telegram_sent") + + await hass.services.async_call( + DOMAIN, + SERVICE_SEND_MESSAGE, + {ATTR_MESSAGE: "test_message", ATTR_MESSAGE_THREAD_ID: "123"}, + blocking=True, + context=context, + ) + await hass.async_block_till_done() + + assert len(events) == 1 + assert events[0].context == context + assert events[0].data[ATTR_MESSAGE_THREAD_ID] == "123" + + async def test_webhook_endpoint_generates_telegram_text_event( hass: HomeAssistant, webhook_platform, From e2dd88be6ef0792b764b0bb39374ffe0b0d511eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Strandberg?= Date: Wed, 5 Jun 2024 17:45:52 +0200 Subject: [PATCH 0263/1445] Add more unit-based sensor descriptions to myuplink (#113104) * Add more unit-based sensor descriptions * Move late addition to icon translations --- homeassistant/components/myuplink/icons.json | 3 ++ homeassistant/components/myuplink/sensor.py | 43 ++++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/homeassistant/components/myuplink/icons.json b/homeassistant/components/myuplink/icons.json index 580b83b1b15..4b96a1a3381 100644 --- a/homeassistant/components/myuplink/icons.json +++ b/homeassistant/components/myuplink/icons.json @@ -26,6 +26,9 @@ "priority": { "default": "mdi:priority-high" }, + "rpm": { + "default": "mdi:rotate-right" + }, "status_compressor": { "default": "mdi:heat-pump-outline" } diff --git a/homeassistant/components/myuplink/sensor.py b/homeassistant/components/myuplink/sensor.py index 45a4590a843..9d23584f389 100644 --- a/homeassistant/components/myuplink/sensor.py +++ b/homeassistant/components/myuplink/sensor.py @@ -10,6 +10,7 @@ from homeassistant.components.sensor import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( + REVOLUTIONS_PER_MINUTE, Platform, UnitOfElectricCurrent, UnitOfEnergy, @@ -54,6 +55,13 @@ DEVICE_POINT_UNIT_DESCRIPTIONS: dict[str, SensorEntityDescription] = { state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfPressure.BAR, ), + "days": SensorEntityDescription( + key="days", + device_class=SensorDeviceClass.DURATION, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfTime.DAYS, + suggested_display_precision=0, + ), "h": SensorEntityDescription( key="hours", device_class=SensorDeviceClass.DURATION, @@ -61,6 +69,13 @@ DEVICE_POINT_UNIT_DESCRIPTIONS: dict[str, SensorEntityDescription] = { native_unit_of_measurement=UnitOfTime.HOURS, suggested_display_precision=1, ), + "hrs": SensorEntityDescription( + key="hours_hrs", + device_class=SensorDeviceClass.DURATION, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfTime.HOURS, + suggested_display_precision=1, + ), "Hz": SensorEntityDescription( key="hertz", device_class=SensorDeviceClass.FREQUENCY, @@ -86,6 +101,27 @@ DEVICE_POINT_UNIT_DESCRIPTIONS: dict[str, SensorEntityDescription] = { state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfVolumeFlowRate.CUBIC_METERS_PER_HOUR, ), + "min": SensorEntityDescription( + key="minutes", + device_class=SensorDeviceClass.DURATION, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfTime.MINUTES, + suggested_display_precision=0, + ), + "Pa": SensorEntityDescription( + key="pressure_pa", + device_class=SensorDeviceClass.PRESSURE, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfPressure.PA, + suggested_display_precision=0, + ), + "rpm": SensorEntityDescription( + key="rpm", + translation_key="rpm", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=REVOLUTIONS_PER_MINUTE, + suggested_display_precision=0, + ), "s": SensorEntityDescription( key="seconds", device_class=SensorDeviceClass.DURATION, @@ -93,6 +129,13 @@ DEVICE_POINT_UNIT_DESCRIPTIONS: dict[str, SensorEntityDescription] = { native_unit_of_measurement=UnitOfTime.SECONDS, suggested_display_precision=0, ), + "sec": SensorEntityDescription( + key="seconds_sec", + device_class=SensorDeviceClass.DURATION, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfTime.SECONDS, + suggested_display_precision=0, + ), } MARKER_FOR_UNKNOWN_VALUE = -32768 From 0562c3085f1fe58b9f441d687882af2e8fc288de Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Wed, 5 Jun 2024 18:21:03 +0200 Subject: [PATCH 0264/1445] Replace slave by meter in v2c (#118893) --- homeassistant/components/v2c/icons.json | 2 +- homeassistant/components/v2c/sensor.py | 9 +++++++-- homeassistant/components/v2c/strings.json | 6 +++--- tests/components/v2c/snapshots/test_sensor.ambr | 12 ++++++------ tests/components/v2c/test_sensor.py | 6 +++--- 5 files changed, 20 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/v2c/icons.json b/homeassistant/components/v2c/icons.json index fa8449135bb..1b76b669956 100644 --- a/homeassistant/components/v2c/icons.json +++ b/homeassistant/components/v2c/icons.json @@ -16,7 +16,7 @@ "fv_power": { "default": "mdi:solar-power-variant" }, - "slave_error": { + "meter_error": { "default": "mdi:alert" }, "battery_power": { diff --git a/homeassistant/components/v2c/sensor.py b/homeassistant/components/v2c/sensor.py index 799d6c3d03c..0c59993ac0e 100644 --- a/homeassistant/components/v2c/sensor.py +++ b/homeassistant/components/v2c/sensor.py @@ -35,7 +35,12 @@ class V2CSensorEntityDescription(SensorEntityDescription): value_fn: Callable[[TrydanData], StateType] -_METER_ERROR_OPTIONS = [error.name.lower() for error in SlaveCommunicationState] +def get_meter_value(value: SlaveCommunicationState) -> str: + """Return the value of the enum and replace slave by meter.""" + return value.name.lower().replace("slave", "meter") + + +_METER_ERROR_OPTIONS = [get_meter_value(error) for error in SlaveCommunicationState] TRYDAN_SENSORS = ( V2CSensorEntityDescription( @@ -82,7 +87,7 @@ TRYDAN_SENSORS = ( V2CSensorEntityDescription( key="meter_error", translation_key="meter_error", - value_fn=lambda evse_data: evse_data.slave_error.name.lower(), + value_fn=lambda evse_data: get_meter_value(evse_data.slave_error), entity_registry_enabled_default=False, device_class=SensorDeviceClass.ENUM, options=_METER_ERROR_OPTIONS, diff --git a/homeassistant/components/v2c/strings.json b/homeassistant/components/v2c/strings.json index bc0d870b635..3342652cfb4 100644 --- a/homeassistant/components/v2c/strings.json +++ b/homeassistant/components/v2c/strings.json @@ -60,12 +60,12 @@ "no_error": "No error", "communication": "Communication", "reading": "Reading", - "slave": "Meter", + "meter": "Meter", "waiting_wifi": "Waiting for Wi-Fi", "waiting_communication": "Waiting communication", "wrong_ip": "Wrong IP", - "slave_not_found": "Meter not found", - "wrong_slave": "Wrong Meter", + "meter_not_found": "Meter not found", + "wrong_meter": "Wrong meter", "no_response": "No response", "clamp_not_connected": "Clamp not connected", "illegal_function": "Illegal function", diff --git a/tests/components/v2c/snapshots/test_sensor.ambr b/tests/components/v2c/snapshots/test_sensor.ambr index 859e5f83e15..cc8077333cb 100644 --- a/tests/components/v2c/snapshots/test_sensor.ambr +++ b/tests/components/v2c/snapshots/test_sensor.ambr @@ -265,12 +265,12 @@ 'no_error', 'communication', 'reading', - 'slave', + 'meter', 'waiting_wifi', 'waiting_communication', 'wrong_ip', - 'slave_not_found', - 'wrong_slave', + 'meter_not_found', + 'wrong_meter', 'no_response', 'clamp_not_connected', 'illegal_function', @@ -335,12 +335,12 @@ 'no_error', 'communication', 'reading', - 'slave', + 'meter', 'waiting_wifi', 'waiting_communication', 'wrong_ip', - 'slave_not_found', - 'wrong_slave', + 'meter_not_found', + 'wrong_meter', 'no_response', 'clamp_not_connected', 'illegal_function', diff --git a/tests/components/v2c/test_sensor.py b/tests/components/v2c/test_sensor.py index 4be62d02bd5..b48a173821c 100644 --- a/tests/components/v2c/test_sensor.py +++ b/tests/components/v2c/test_sensor.py @@ -33,12 +33,12 @@ async def test_sensor( "no_error", "communication", "reading", - "slave", + "meter", "waiting_wifi", "waiting_communication", "wrong_ip", - "slave_not_found", - "wrong_slave", + "meter_not_found", + "wrong_meter", "no_response", "clamp_not_connected", "illegal_function", From 6efc3b95a41d5b70b58a14c7491fdf6b36c95a62 Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Wed, 5 Jun 2024 11:43:28 -0500 Subject: [PATCH 0265/1445] Bump intents to 2024.6.5 (#118890) --- homeassistant/components/conversation/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/conversation/manifest.json b/homeassistant/components/conversation/manifest.json index 6873e47e647..a3af6607aba 100644 --- a/homeassistant/components/conversation/manifest.json +++ b/homeassistant/components/conversation/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/conversation", "integration_type": "system", "quality_scale": "internal", - "requirements": ["hassil==1.7.1", "home-assistant-intents==2024.6.3"] + "requirements": ["hassil==1.7.1", "home-assistant-intents==2024.6.5"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 56f3d920641..627845d062f 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -33,7 +33,7 @@ hass-nabucasa==0.81.1 hassil==1.7.1 home-assistant-bluetooth==1.12.0 home-assistant-frontend==20240605.0 -home-assistant-intents==2024.6.3 +home-assistant-intents==2024.6.5 httpx==0.27.0 ifaddr==0.2.0 Jinja2==3.1.4 diff --git a/requirements_all.txt b/requirements_all.txt index 330391eafa8..e43a47aa19c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1090,7 +1090,7 @@ holidays==0.49 home-assistant-frontend==20240605.0 # homeassistant.components.conversation -home-assistant-intents==2024.6.3 +home-assistant-intents==2024.6.5 # homeassistant.components.home_connect homeconnect==0.7.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3b422a731e2..765ff7b74f7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -892,7 +892,7 @@ holidays==0.49 home-assistant-frontend==20240605.0 # homeassistant.components.conversation -home-assistant-intents==2024.6.3 +home-assistant-intents==2024.6.5 # homeassistant.components.home_connect homeconnect==0.7.2 From 8099ea8817ff9d4ad1f6f4f3599f151603b84c8e Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 5 Jun 2024 18:53:44 +0200 Subject: [PATCH 0266/1445] Improve WS command validate_config (#118864) Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> Co-authored-by: Robert Resch --- .../components/websocket_api/commands.py | 5 ++- .../components/websocket_api/test_commands.py | 32 +++++++++++++++---- 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/websocket_api/commands.py b/homeassistant/components/websocket_api/commands.py index e159880c8bc..f66930c8d00 100644 --- a/homeassistant/components/websocket_api/commands.py +++ b/homeassistant/components/websocket_api/commands.py @@ -862,7 +862,10 @@ async def handle_validate_config( try: await validator(hass, schema(msg[key])) - except vol.Invalid as err: + except ( + vol.Invalid, + HomeAssistantError, + ) as err: result[key] = {"valid": False, "error": str(err)} else: result[key] = {"valid": True, "error": None} diff --git a/tests/components/websocket_api/test_commands.py b/tests/components/websocket_api/test_commands.py index 655d8adf1ea..a51e51b81b0 100644 --- a/tests/components/websocket_api/test_commands.py +++ b/tests/components/websocket_api/test_commands.py @@ -3,6 +3,7 @@ import asyncio from copy import deepcopy import logging +from typing import Any from unittest.mock import ANY, AsyncMock, Mock, patch import pytest @@ -2529,13 +2530,14 @@ async def test_integration_setup_info( ], ) async def test_validate_config_works( - websocket_client: MockHAClientWebSocket, key, config + websocket_client: MockHAClientWebSocket, + key: str, + config: dict[str, Any] | list[dict[str, Any]], ) -> None: """Test config validation.""" - await websocket_client.send_json({"id": 7, "type": "validate_config", key: config}) + await websocket_client.send_json_auto_id({"type": "validate_config", key: config}) msg = await websocket_client.receive_json() - assert msg["id"] == 7 assert msg["type"] == const.TYPE_RESULT assert msg["success"] assert msg["result"] == {key: {"valid": True, "error": None}} @@ -2544,11 +2546,13 @@ async def test_validate_config_works( @pytest.mark.parametrize( ("key", "config", "error"), [ + # Raises vol.Invalid ( "trigger", {"platform": "non_existing", "event_type": "hello"}, "Invalid platform 'non_existing' specified", ), + # Raises vol.Invalid ( "condition", { @@ -2562,6 +2566,20 @@ async def test_validate_config_works( "@ data[0]" ), ), + # Raises HomeAssistantError + ( + "condition", + { + "above": 50, + "condition": "device", + "device_id": "a51a57e5af051eb403d56eb9e6fd691c", + "domain": "sensor", + "entity_id": "7d18a157b7c00adbf2982ea7de0d0362", + "type": "is_carbon_dioxide", + }, + "Unknown device 'a51a57e5af051eb403d56eb9e6fd691c'", + ), + # Raises vol.Invalid ( "action", {"non_existing": "domain_test.test_service"}, @@ -2570,13 +2588,15 @@ async def test_validate_config_works( ], ) async def test_validate_config_invalid( - websocket_client: MockHAClientWebSocket, key, config, error + websocket_client: MockHAClientWebSocket, + key: str, + config: dict[str, Any], + error: str, ) -> None: """Test config validation.""" - await websocket_client.send_json({"id": 7, "type": "validate_config", key: config}) + await websocket_client.send_json_auto_id({"type": "validate_config", key: config}) msg = await websocket_client.receive_json() - assert msg["id"] == 7 assert msg["type"] == const.TYPE_RESULT assert msg["success"] assert msg["result"] == {key: {"valid": False, "error": error}} From feaf50eee7ba9f72aae0ba1781176bc8472989d9 Mon Sep 17 00:00:00 2001 From: Josef Zweck <24647999+zweckj@users.noreply.github.com> Date: Wed, 5 Jun 2024 22:38:23 +0200 Subject: [PATCH 0267/1445] Address Webhook `async_generate_url` review (#118910) code styling --- homeassistant/components/webhook/__init__.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/webhook/__init__.py b/homeassistant/components/webhook/__init__.py index 7d282b8aef3..34e11f49978 100644 --- a/homeassistant/components/webhook/__init__.py +++ b/homeassistant/components/webhook/__init__.py @@ -97,7 +97,14 @@ def async_generate_url( ) -> str: """Generate the full URL for a webhook_id.""" return ( - f"{get_url(hass,allow_internal=allow_internal, allow_external=allow_external, allow_cloud=False, allow_ip=allow_ip, prefer_external=prefer_external,)}" + f"{get_url( + hass, + allow_internal=allow_internal, + allow_external=allow_external, + allow_cloud=False, + allow_ip=allow_ip, + prefer_external=prefer_external, + )}" f"{async_generate_path(webhook_id)}" ) From 7407995ff12f937e0885565bda9d45c0c4c9c70c Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Wed, 5 Jun 2024 23:37:14 +0200 Subject: [PATCH 0268/1445] Fix flaky Google Assistant test (#118914) * Fix flaky Google Assistant test * Trigger full ci --- tests/components/google_assistant/test_http.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/components/google_assistant/test_http.py b/tests/components/google_assistant/test_http.py index 1dac75875a6..416d569b286 100644 --- a/tests/components/google_assistant/test_http.py +++ b/tests/components/google_assistant/test_http.py @@ -577,6 +577,8 @@ async def test_async_get_users_from_store(tmpdir: py.path.local) -> None: assert await async_get_users(hass) == ["agent_1"] + await hass.async_stop() + VALID_STORE_DATA = json.dumps( { From a0957069f0ec6e4fc582dbc1209746370dae3436 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 5 Jun 2024 22:37:35 -0500 Subject: [PATCH 0269/1445] Revert "Bump orjson to 3.10.3 (#116945)" (#118920) This reverts commit dc50095d0618f545a7ee80d2f10b9997c1bc40da. --- homeassistant/package_constraints.txt | 2 +- pyproject.toml | 2 +- requirements.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 627845d062f..c4f22de09aa 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -39,7 +39,7 @@ ifaddr==0.2.0 Jinja2==3.1.4 lru-dict==1.3.0 mutagen==1.47.0 -orjson==3.10.3 +orjson==3.9.15 packaging>=23.1 paho-mqtt==1.6.1 Pillow==10.3.0 diff --git a/pyproject.toml b/pyproject.toml index 42bb1bd69af..daf13dc537b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -53,7 +53,7 @@ dependencies = [ "cryptography==42.0.5", "Pillow==10.3.0", "pyOpenSSL==24.1.0", - "orjson==3.10.3", + "orjson==3.9.15", "packaging>=23.1", "pip>=21.3.1", "psutil-home-assistant==0.0.1", diff --git a/requirements.txt b/requirements.txt index d3390585c66..3db2624655d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -28,7 +28,7 @@ PyJWT==2.8.0 cryptography==42.0.5 Pillow==10.3.0 pyOpenSSL==24.1.0 -orjson==3.10.3 +orjson==3.9.15 packaging>=23.1 pip>=21.3.1 psutil-home-assistant==0.0.1 From 64b23419e0467b8f3d6a10ed6049ec260cb165e5 Mon Sep 17 00:00:00 2001 From: Ernst Klamer Date: Thu, 6 Jun 2024 05:38:30 +0200 Subject: [PATCH 0270/1445] Bump bthome-ble to 3.9.1 (#118907) bump bthome-ble to 3.9.1 --- homeassistant/components/bthome/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/bthome/manifest.json b/homeassistant/components/bthome/manifest.json index 7c90c6f3bbc..42fbe794918 100644 --- a/homeassistant/components/bthome/manifest.json +++ b/homeassistant/components/bthome/manifest.json @@ -20,5 +20,5 @@ "dependencies": ["bluetooth_adapters"], "documentation": "https://www.home-assistant.io/integrations/bthome", "iot_class": "local_push", - "requirements": ["bthome-ble==3.8.1"] + "requirements": ["bthome-ble==3.9.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index e43a47aa19c..79407029d6a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -625,7 +625,7 @@ brunt==1.2.0 bt-proximity==0.2.1 # homeassistant.components.bthome -bthome-ble==3.8.1 +bthome-ble==3.9.1 # homeassistant.components.bt_home_hub_5 bthomehub5-devicelist==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 765ff7b74f7..4d0495cf3b3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -533,7 +533,7 @@ brottsplatskartan==1.0.5 brunt==1.2.0 # homeassistant.components.bthome -bthome-ble==3.8.1 +bthome-ble==3.9.1 # homeassistant.components.buienradar buienradar==1.0.5 From 475c20d5296cc67373af3bc7787260a169e941ec Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 5 Jun 2024 22:41:55 -0500 Subject: [PATCH 0271/1445] Always do thread safety check when writing state (#118886) * Always do thread safety check when writing state Refactor the 3 most common places where the thread safety check for the event loop to be inline to make the check fast enough that we can keep it long term. While code review catches most of the thread safety issues in core, some of them still make it through, and new ones keep getting added. Its not possible to catch them all with manual code review, so its worth the tiny overhead to check each time. Previously the check was limited to custom components because they were the most common source of thread safety issues. * Always do thread safety check when writing state Refactor the 3 most common places where the thread safety check for the event loop to be inline to make the check fast enough that we can keep it long term. While code review catches most of the thread safety issues in core, some of them still make it through, and new ones keep getting added. Its not possible to catch them all with manual code review, so its worth the tiny overhead to check each time. Previously the check was limited to custom components because they were the most common source of thread safety issues. * async_fire is more common than expected with ccs * fix mock * fix hass mocking --- homeassistant/core.py | 35 +++++++------------ homeassistant/helpers/entity.py | 9 +++-- homeassistant/helpers/frame.py | 13 +++++++ tests/common.py | 2 +- tests/components/zha/test_cluster_handlers.py | 2 ++ tests/helpers/test_entity.py | 6 ++-- 6 files changed, 34 insertions(+), 33 deletions(-) diff --git a/homeassistant/core.py b/homeassistant/core.py index ad04c6d1366..d0e80ad8bd1 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -434,25 +434,17 @@ class HomeAssistant: self.import_executor = InterruptibleThreadPoolExecutor( max_workers=1, thread_name_prefix="ImportExecutor" ) - self._loop_thread_id = getattr( + self.loop_thread_id = getattr( self.loop, "_thread_ident", getattr(self.loop, "_thread_id") ) def verify_event_loop_thread(self, what: str) -> None: """Report and raise if we are not running in the event loop thread.""" - if self._loop_thread_id != threading.get_ident(): + if self.loop_thread_id != threading.get_ident(): + # frame is a circular import, so we import it here from .helpers import frame # pylint: disable=import-outside-toplevel - # frame is a circular import, so we import it here - frame.report( - f"calls {what} from a thread other than the event loop, " - "which may cause Home Assistant to crash or data to corrupt. " - "For more information, see " - "https://developers.home-assistant.io/docs/asyncio_thread_safety/" - f"#{what.replace('.', '')}", - error_if_core=True, - error_if_integration=True, - ) + frame.report_non_thread_safe_operation(what) @property def _active_tasks(self) -> set[asyncio.Future[Any]]: @@ -793,16 +785,10 @@ class HomeAssistant: target: target to call. """ - # We turned on asyncio debug in April 2024 in the dev containers - # in the hope of catching some of the issues that have been - # reported. It will take a while to get all the issues fixed in - # custom components. - # - # In 2025.5 we should guard the `verify_event_loop_thread` - # check with a check for the `hass.config.debug` flag being set as - # long term we don't want to be checking this in production - # environments since it is a performance hit. - self.verify_event_loop_thread("hass.async_create_task") + if self.loop_thread_id != threading.get_ident(): + from .helpers import frame # pylint: disable=import-outside-toplevel + + frame.report_non_thread_safe_operation("hass.async_create_task") return self.async_create_task_internal(target, name, eager_start) @callback @@ -1497,7 +1483,10 @@ class EventBus: This method must be run in the event loop. """ _verify_event_type_length_or_raise(event_type) - self._hass.verify_event_loop_thread("hass.bus.async_fire") + if self._hass.loop_thread_id != threading.get_ident(): + from .helpers import frame # pylint: disable=import-outside-toplevel + + frame.report_non_thread_safe_operation("hass.bus.async_fire") return self.async_fire_internal( event_type, event_data, origin, context, time_fired ) diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index ee544883a68..9a2bb4b6fca 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -14,6 +14,7 @@ import logging import math from operator import attrgetter import sys +import threading import time from types import FunctionType from typing import TYPE_CHECKING, Any, Final, Literal, NotRequired, TypedDict, final @@ -63,6 +64,7 @@ from .event import ( async_track_device_registry_updated_event, async_track_entity_registry_updated_event, ) +from .frame import report_non_thread_safe_operation from .typing import UNDEFINED, StateType, UndefinedType timer = time.time @@ -512,7 +514,6 @@ class Entity( # While not purely typed, it makes typehinting more useful for us # and removes the need for constant None checks or asserts. _state_info: StateInfo = None # type: ignore[assignment] - _is_custom_component: bool = False __capabilities_updated_at: deque[float] __capabilities_updated_at_reported: bool = False @@ -995,8 +996,8 @@ class Entity( def async_write_ha_state(self) -> None: """Write the state to the state machine.""" self._async_verify_state_writable() - if self._is_custom_component or self.hass.config.debug: - self.hass.verify_event_loop_thread("async_write_ha_state") + if self.hass.loop_thread_id != threading.get_ident(): + report_non_thread_safe_operation("async_write_ha_state") self._async_write_ha_state() def _stringify_state(self, available: bool) -> str: @@ -1440,8 +1441,6 @@ class Entity( "domain": self.platform.platform_name, "custom_component": is_custom_component, } - self._is_custom_component = is_custom_component - if self.platform.config_entry: entity_info["config_entry"] = self.platform.config_entry.entry_id diff --git a/homeassistant/helpers/frame.py b/homeassistant/helpers/frame.py index e8ba6ba0c07..8a30c26886e 100644 --- a/homeassistant/helpers/frame.py +++ b/homeassistant/helpers/frame.py @@ -218,3 +218,16 @@ def warn_use[_CallableT: Callable](func: _CallableT, what: str) -> _CallableT: report(what) return cast(_CallableT, report_use) + + +def report_non_thread_safe_operation(what: str) -> None: + """Report a non-thread safe operation.""" + report( + f"calls {what} from a thread other than the event loop, " + "which may cause Home Assistant to crash or data to corrupt. " + "For more information, see " + "https://developers.home-assistant.io/docs/asyncio_thread_safety/" + f"#{what.replace('.', '')}", + error_if_core=True, + error_if_integration=True, + ) diff --git a/tests/common.py b/tests/common.py index b1110297d2f..88d7a86fcf4 100644 --- a/tests/common.py +++ b/tests/common.py @@ -174,7 +174,7 @@ def get_test_home_assistant() -> Generator[HomeAssistant, None, None]: """Run event loop.""" loop._thread_ident = threading.get_ident() - hass._loop_thread_id = loop._thread_ident + hass.loop_thread_id = loop._thread_ident loop.run_forever() loop_stop_event.set() diff --git a/tests/components/zha/test_cluster_handlers.py b/tests/components/zha/test_cluster_handlers.py index cc9fb8d1918..d09883c38e3 100644 --- a/tests/components/zha/test_cluster_handlers.py +++ b/tests/components/zha/test_cluster_handlers.py @@ -3,6 +3,7 @@ from collections.abc import Callable import logging import math +import threading from types import NoneType from unittest import mock from unittest.mock import AsyncMock, patch @@ -86,6 +87,7 @@ def endpoint(zigpy_coordinator_device): type(endpoint_mock.device).skip_configuration = mock.PropertyMock( return_value=False ) + endpoint_mock.device.hass.loop_thread_id = threading.get_ident() endpoint_mock.id = 1 return endpoint_mock diff --git a/tests/helpers/test_entity.py b/tests/helpers/test_entity.py index a80674e0f76..c8da7a118aa 100644 --- a/tests/helpers/test_entity.py +++ b/tests/helpers/test_entity.py @@ -2617,13 +2617,12 @@ async def test_async_write_ha_state_thread_safety(hass: HomeAssistant) -> None: assert not hass.states.get(ent2.entity_id) -async def test_async_write_ha_state_thread_safety_custom_component( +async def test_async_write_ha_state_thread_safety_always( hass: HomeAssistant, ) -> None: - """Test async_write_ha_state thread safe for custom components.""" + """Test async_write_ha_state thread safe check.""" ent = entity.Entity() - ent._is_custom_component = True ent.entity_id = "test.any" ent.hass = hass ent.platform = MockEntityPlatform(hass, domain="test") @@ -2631,7 +2630,6 @@ async def test_async_write_ha_state_thread_safety_custom_component( assert hass.states.get(ent.entity_id) ent2 = entity.Entity() - ent2._is_custom_component = True ent2.entity_id = "test.any2" ent2.hass = hass ent2.platform = MockEntityPlatform(hass, domain="test") From f9205cd88d24ceaf5352e87f577bc6cf0723cf62 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 5 Jun 2024 22:43:34 -0500 Subject: [PATCH 0272/1445] Avoid additional timestamp conversion to set state (#118885) Avoid addtional timestamp conversion to set state Since we already have the timestamp, we can pass it on to the State object and avoid the additional timestamp conversion which can be as much as 30% of the state write runtime. Since datetime objects are limited to microsecond precision, we need to adjust some tests to account for the additional precision that we will now be able to get in the database --- homeassistant/core.py | 5 +- .../components/history/test_websocket_api.py | 384 +++++++++++++----- .../components/logbook/test_websocket_api.py | 38 +- .../components/websocket_api/test_messages.py | 20 +- tests/test_core.py | 28 +- 5 files changed, 332 insertions(+), 143 deletions(-) diff --git a/homeassistant/core.py b/homeassistant/core.py index d0e80ad8bd1..7aa823dc042 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -1762,6 +1762,7 @@ class State: context: Context | None = None, validate_entity_id: bool | None = True, state_info: StateInfo | None = None, + last_updated_timestamp: float | None = None, ) -> None: """Initialize a new state.""" state = str(state) @@ -1793,7 +1794,8 @@ class State: # so we will set the timestamp values here to avoid the overhead of # the function call in the property we know will always be called. last_updated = self.last_updated - last_updated_timestamp = last_updated.timestamp() + if not last_updated_timestamp: + last_updated_timestamp = last_updated.timestamp() self.last_updated_timestamp = last_updated_timestamp if self.last_changed == last_updated: self.__dict__["last_changed_timestamp"] = last_updated_timestamp @@ -2309,6 +2311,7 @@ class StateMachine: context, old_state is None, state_info, + timestamp, ) if old_state is not None: old_state.expire() diff --git a/tests/components/history/test_websocket_api.py b/tests/components/history/test_websocket_api.py index 580853fb83f..e5c33d0e7af 100644 --- a/tests/components/history/test_websocket_api.py +++ b/tests/components/history/test_websocket_api.py @@ -466,16 +466,24 @@ async def test_history_stream_historical_only( await async_setup_component(hass, "sensor", {}) await async_recorder_block_till_done(hass) hass.states.async_set("sensor.one", "on", attributes={"any": "attr"}) - sensor_one_last_updated = hass.states.get("sensor.one").last_updated + sensor_one_last_updated_timestamp = hass.states.get( + "sensor.one" + ).last_updated_timestamp await async_recorder_block_till_done(hass) hass.states.async_set("sensor.two", "off", attributes={"any": "attr"}) - sensor_two_last_updated = hass.states.get("sensor.two").last_updated + sensor_two_last_updated_timestamp = hass.states.get( + "sensor.two" + ).last_updated_timestamp await async_recorder_block_till_done(hass) hass.states.async_set("sensor.three", "off", attributes={"any": "changed"}) - sensor_three_last_updated = hass.states.get("sensor.three").last_updated + sensor_three_last_updated_timestamp = hass.states.get( + "sensor.three" + ).last_updated_timestamp await async_recorder_block_till_done(hass) hass.states.async_set("sensor.four", "off", attributes={"any": "again"}) - sensor_four_last_updated = hass.states.get("sensor.four").last_updated + sensor_four_last_updated_timestamp = hass.states.get( + "sensor.four" + ).last_updated_timestamp await async_recorder_block_till_done(hass) hass.states.async_set("switch.excluded", "off", attributes={"any": "again"}) await async_wait_recording_done(hass) @@ -506,17 +514,27 @@ async def test_history_stream_historical_only( assert response == { "event": { - "end_time": sensor_four_last_updated.timestamp(), - "start_time": now.timestamp(), + "end_time": pytest.approx(sensor_four_last_updated_timestamp), + "start_time": pytest.approx(now.timestamp()), "states": { "sensor.four": [ - {"lu": sensor_four_last_updated.timestamp(), "s": "off"} + { + "lu": pytest.approx(sensor_four_last_updated_timestamp), + "s": "off", + } + ], + "sensor.one": [ + {"lu": pytest.approx(sensor_one_last_updated_timestamp), "s": "on"} ], - "sensor.one": [{"lu": sensor_one_last_updated.timestamp(), "s": "on"}], "sensor.three": [ - {"lu": sensor_three_last_updated.timestamp(), "s": "off"} + { + "lu": pytest.approx(sensor_three_last_updated_timestamp), + "s": "off", + } + ], + "sensor.two": [ + {"lu": pytest.approx(sensor_two_last_updated_timestamp), "s": "off"} ], - "sensor.two": [{"lu": sensor_two_last_updated.timestamp(), "s": "off"}], }, }, "id": 1, @@ -817,10 +835,14 @@ async def test_history_stream_live_no_attributes_minimal_response( await async_setup_component(hass, "sensor", {}) await async_recorder_block_till_done(hass) hass.states.async_set("sensor.one", "on", attributes={"any": "attr"}) - sensor_one_last_updated = hass.states.get("sensor.one").last_updated + sensor_one_last_updated_timestamp = hass.states.get( + "sensor.one" + ).last_updated_timestamp await async_recorder_block_till_done(hass) hass.states.async_set("sensor.two", "off", attributes={"any": "attr"}) - sensor_two_last_updated = hass.states.get("sensor.two").last_updated + sensor_two_last_updated_timestamp = hass.states.get( + "sensor.two" + ).last_updated_timestamp await async_recorder_block_till_done(hass) hass.states.async_set("switch.excluded", "off", attributes={"any": "again"}) await async_wait_recording_done(hass) @@ -846,15 +868,19 @@ async def test_history_stream_live_no_attributes_minimal_response( assert response["type"] == "result" response = await client.receive_json() - first_end_time = sensor_two_last_updated.timestamp() + first_end_time = sensor_two_last_updated_timestamp assert response == { "event": { - "end_time": first_end_time, - "start_time": now.timestamp(), + "end_time": pytest.approx(first_end_time), + "start_time": pytest.approx(now.timestamp()), "states": { - "sensor.one": [{"lu": sensor_one_last_updated.timestamp(), "s": "on"}], - "sensor.two": [{"lu": sensor_two_last_updated.timestamp(), "s": "off"}], + "sensor.one": [ + {"lu": pytest.approx(sensor_one_last_updated_timestamp), "s": "on"} + ], + "sensor.two": [ + {"lu": pytest.approx(sensor_two_last_updated_timestamp), "s": "off"} + ], }, }, "id": 1, @@ -866,14 +892,22 @@ async def test_history_stream_live_no_attributes_minimal_response( hass.states.async_set("sensor.two", "two", attributes={"any": "attr"}) await async_recorder_block_till_done(hass) - sensor_one_last_updated = hass.states.get("sensor.one").last_updated - sensor_two_last_updated = hass.states.get("sensor.two").last_updated + sensor_one_last_updated_timestamp = hass.states.get( + "sensor.one" + ).last_updated_timestamp + sensor_two_last_updated_timestamp = hass.states.get( + "sensor.two" + ).last_updated_timestamp response = await client.receive_json() assert response == { "event": { "states": { - "sensor.one": [{"lu": sensor_one_last_updated.timestamp(), "s": "one"}], - "sensor.two": [{"lu": sensor_two_last_updated.timestamp(), "s": "two"}], + "sensor.one": [ + {"lu": pytest.approx(sensor_one_last_updated_timestamp), "s": "one"} + ], + "sensor.two": [ + {"lu": pytest.approx(sensor_two_last_updated_timestamp), "s": "two"} + ], }, }, "id": 1, @@ -894,10 +928,14 @@ async def test_history_stream_live( await async_setup_component(hass, "sensor", {}) await async_recorder_block_till_done(hass) hass.states.async_set("sensor.one", "on", attributes={"any": "attr"}) - sensor_one_last_updated = hass.states.get("sensor.one").last_updated + sensor_one_last_updated_timestamp = hass.states.get( + "sensor.one" + ).last_updated_timestamp await async_recorder_block_till_done(hass) hass.states.async_set("sensor.two", "off", attributes={"any": "attr"}) - sensor_two_last_updated = hass.states.get("sensor.two").last_updated + sensor_two_last_updated_timestamp = hass.states.get( + "sensor.two" + ).last_updated_timestamp await async_recorder_block_till_done(hass) hass.states.async_set("switch.excluded", "off", attributes={"any": "again"}) await async_wait_recording_done(hass) @@ -923,24 +961,24 @@ async def test_history_stream_live( assert response["type"] == "result" response = await client.receive_json() - first_end_time = sensor_two_last_updated.timestamp() + first_end_time = sensor_two_last_updated_timestamp assert response == { "event": { - "end_time": first_end_time, - "start_time": now.timestamp(), + "end_time": pytest.approx(first_end_time), + "start_time": pytest.approx(now.timestamp()), "states": { "sensor.one": [ { "a": {"any": "attr"}, - "lu": sensor_one_last_updated.timestamp(), + "lu": pytest.approx(sensor_one_last_updated_timestamp), "s": "on", } ], "sensor.two": [ { "a": {"any": "attr"}, - "lu": sensor_two_last_updated.timestamp(), + "lu": pytest.approx(sensor_two_last_updated_timestamp), "s": "off", } ], @@ -955,24 +993,30 @@ async def test_history_stream_live( hass.states.async_set("sensor.two", "two", attributes={"any": "attr"}) await async_recorder_block_till_done(hass) - sensor_one_last_updated = hass.states.get("sensor.one").last_updated - sensor_one_last_changed = hass.states.get("sensor.one").last_changed - sensor_two_last_updated = hass.states.get("sensor.two").last_updated + sensor_one_last_updated_timestamp = hass.states.get( + "sensor.one" + ).last_updated_timestamp + sensor_one_last_changed_timestamp = hass.states.get( + "sensor.one" + ).last_changed_timestamp + sensor_two_last_updated_timestamp = hass.states.get( + "sensor.two" + ).last_updated_timestamp response = await client.receive_json() assert response == { "event": { "states": { "sensor.one": [ { - "lc": sensor_one_last_changed.timestamp(), - "lu": sensor_one_last_updated.timestamp(), + "lc": pytest.approx(sensor_one_last_changed_timestamp), + "lu": pytest.approx(sensor_one_last_updated_timestamp), "s": "on", "a": {"diff": "attr"}, } ], "sensor.two": [ { - "lu": sensor_two_last_updated.timestamp(), + "lu": pytest.approx(sensor_two_last_updated_timestamp), "s": "two", "a": {"any": "attr"}, } @@ -997,10 +1041,14 @@ async def test_history_stream_live_minimal_response( await async_setup_component(hass, "sensor", {}) await async_recorder_block_till_done(hass) hass.states.async_set("sensor.one", "on", attributes={"any": "attr"}) - sensor_one_last_updated = hass.states.get("sensor.one").last_updated + sensor_one_last_updated_timestamp = hass.states.get( + "sensor.one" + ).last_updated_timestamp await async_recorder_block_till_done(hass) hass.states.async_set("sensor.two", "off", attributes={"any": "attr"}) - sensor_two_last_updated = hass.states.get("sensor.two").last_updated + sensor_two_last_updated_timestamp = hass.states.get( + "sensor.two" + ).last_updated_timestamp await async_recorder_block_till_done(hass) hass.states.async_set("switch.excluded", "off", attributes={"any": "again"}) await async_wait_recording_done(hass) @@ -1026,24 +1074,24 @@ async def test_history_stream_live_minimal_response( assert response["type"] == "result" response = await client.receive_json() - first_end_time = sensor_two_last_updated.timestamp() + first_end_time = sensor_two_last_updated_timestamp assert response == { "event": { - "end_time": first_end_time, + "end_time": pytest.approx(first_end_time), "start_time": now.timestamp(), "states": { "sensor.one": [ { "a": {"any": "attr"}, - "lu": sensor_one_last_updated.timestamp(), + "lu": pytest.approx(sensor_one_last_updated_timestamp), "s": "on", } ], "sensor.two": [ { "a": {"any": "attr"}, - "lu": sensor_two_last_updated.timestamp(), + "lu": pytest.approx(sensor_two_last_updated_timestamp), "s": "off", } ], @@ -1057,8 +1105,12 @@ async def test_history_stream_live_minimal_response( hass.states.async_set("sensor.one", "on", attributes={"diff": "attr"}) hass.states.async_set("sensor.two", "two", attributes={"any": "attr"}) # Only sensor.two has changed - sensor_one_last_updated = hass.states.get("sensor.one").last_updated - sensor_two_last_updated = hass.states.get("sensor.two").last_updated + sensor_one_last_updated_timestamp = hass.states.get( + "sensor.one" + ).last_updated_timestamp + sensor_two_last_updated_timestamp = hass.states.get( + "sensor.two" + ).last_updated_timestamp hass.states.async_remove("sensor.one") hass.states.async_remove("sensor.two") await async_recorder_block_till_done(hass) @@ -1069,7 +1121,7 @@ async def test_history_stream_live_minimal_response( "states": { "sensor.two": [ { - "lu": sensor_two_last_updated.timestamp(), + "lu": pytest.approx(sensor_two_last_updated_timestamp), "s": "two", "a": {"any": "attr"}, } @@ -1094,10 +1146,14 @@ async def test_history_stream_live_no_attributes( await async_setup_component(hass, "sensor", {}) await async_recorder_block_till_done(hass) hass.states.async_set("sensor.one", "on", attributes={"any": "attr"}) - sensor_one_last_updated = hass.states.get("sensor.one").last_updated + sensor_one_last_updated_timestamp = hass.states.get( + "sensor.one" + ).last_updated_timestamp await async_recorder_block_till_done(hass) hass.states.async_set("sensor.two", "off", attributes={"any": "attr"}) - sensor_two_last_updated = hass.states.get("sensor.two").last_updated + sensor_two_last_updated_timestamp = hass.states.get( + "sensor.two" + ).last_updated_timestamp await async_recorder_block_till_done(hass) hass.states.async_set("switch.excluded", "off", attributes={"any": "again"}) await async_wait_recording_done(hass) @@ -1123,18 +1179,26 @@ async def test_history_stream_live_no_attributes( assert response["type"] == "result" response = await client.receive_json() - first_end_time = sensor_two_last_updated.timestamp() + first_end_time = sensor_two_last_updated_timestamp assert response == { "event": { - "end_time": first_end_time, - "start_time": now.timestamp(), + "end_time": pytest.approx(first_end_time), + "start_time": pytest.approx(now.timestamp()), "states": { "sensor.one": [ - {"a": {}, "lu": sensor_one_last_updated.timestamp(), "s": "on"} + { + "a": {}, + "lu": pytest.approx(sensor_one_last_updated_timestamp), + "s": "on", + } ], "sensor.two": [ - {"a": {}, "lu": sensor_two_last_updated.timestamp(), "s": "off"} + { + "a": {}, + "lu": pytest.approx(sensor_two_last_updated_timestamp), + "s": "off", + } ], }, }, @@ -1147,14 +1211,22 @@ async def test_history_stream_live_no_attributes( hass.states.async_set("sensor.two", "two", attributes={"diff": "attr"}) await async_recorder_block_till_done(hass) - sensor_one_last_updated = hass.states.get("sensor.one").last_updated - sensor_two_last_updated = hass.states.get("sensor.two").last_updated + sensor_one_last_updated_timestamp = hass.states.get( + "sensor.one" + ).last_updated_timestamp + sensor_two_last_updated_timestamp = hass.states.get( + "sensor.two" + ).last_updated_timestamp response = await client.receive_json() assert response == { "event": { "states": { - "sensor.one": [{"lu": sensor_one_last_updated.timestamp(), "s": "one"}], - "sensor.two": [{"lu": sensor_two_last_updated.timestamp(), "s": "two"}], + "sensor.one": [ + {"lu": pytest.approx(sensor_one_last_updated_timestamp), "s": "one"} + ], + "sensor.two": [ + {"lu": pytest.approx(sensor_two_last_updated_timestamp), "s": "two"} + ], }, }, "id": 1, @@ -1176,10 +1248,14 @@ async def test_history_stream_live_no_attributes_minimal_response_specific_entit await async_setup_component(hass, "sensor", {}) await async_recorder_block_till_done(hass) hass.states.async_set("sensor.one", "on", attributes={"any": "attr"}) - sensor_one_last_updated = hass.states.get("sensor.one").last_updated + sensor_one_last_updated_timestamp = hass.states.get( + "sensor.one" + ).last_updated_timestamp await async_recorder_block_till_done(hass) hass.states.async_set("sensor.two", "off", attributes={"any": "attr"}) - sensor_two_last_updated = hass.states.get("sensor.two").last_updated + sensor_two_last_updated_timestamp = hass.states.get( + "sensor.two" + ).last_updated_timestamp await async_recorder_block_till_done(hass) hass.states.async_set("switch.excluded", "off", attributes={"any": "again"}) await async_wait_recording_done(hass) @@ -1205,15 +1281,19 @@ async def test_history_stream_live_no_attributes_minimal_response_specific_entit assert response["type"] == "result" response = await client.receive_json() - first_end_time = sensor_two_last_updated.timestamp() + first_end_time = sensor_two_last_updated_timestamp assert response == { "event": { - "end_time": first_end_time, - "start_time": now.timestamp(), + "end_time": pytest.approx(first_end_time), + "start_time": pytest.approx(now.timestamp()), "states": { - "sensor.one": [{"lu": sensor_one_last_updated.timestamp(), "s": "on"}], - "sensor.two": [{"lu": sensor_two_last_updated.timestamp(), "s": "off"}], + "sensor.one": [ + {"lu": pytest.approx(sensor_one_last_updated_timestamp), "s": "on"} + ], + "sensor.two": [ + {"lu": pytest.approx(sensor_two_last_updated_timestamp), "s": "off"} + ], }, }, "id": 1, @@ -1225,14 +1305,22 @@ async def test_history_stream_live_no_attributes_minimal_response_specific_entit hass.states.async_set("sensor.two", "two", attributes={"any": "attr"}) await async_recorder_block_till_done(hass) - sensor_one_last_updated = hass.states.get("sensor.one").last_updated - sensor_two_last_updated = hass.states.get("sensor.two").last_updated + sensor_one_last_updated_timestamp = hass.states.get( + "sensor.one" + ).last_updated_timestamp + sensor_two_last_updated_timestamp = hass.states.get( + "sensor.two" + ).last_updated_timestamp response = await client.receive_json() assert response == { "event": { "states": { - "sensor.one": [{"lu": sensor_one_last_updated.timestamp(), "s": "one"}], - "sensor.two": [{"lu": sensor_two_last_updated.timestamp(), "s": "two"}], + "sensor.one": [ + {"lu": pytest.approx(sensor_one_last_updated_timestamp), "s": "one"} + ], + "sensor.two": [ + {"lu": pytest.approx(sensor_two_last_updated_timestamp), "s": "two"} + ], }, }, "id": 1, @@ -1254,10 +1342,14 @@ async def test_history_stream_live_with_future_end_time( await async_setup_component(hass, "sensor", {}) await async_recorder_block_till_done(hass) hass.states.async_set("sensor.one", "on", attributes={"any": "attr"}) - sensor_one_last_updated = hass.states.get("sensor.one").last_updated + sensor_one_last_updated_timestamp = hass.states.get( + "sensor.one" + ).last_updated_timestamp await async_recorder_block_till_done(hass) hass.states.async_set("sensor.two", "off", attributes={"any": "attr"}) - sensor_two_last_updated = hass.states.get("sensor.two").last_updated + sensor_two_last_updated_timestamp = hass.states.get( + "sensor.two" + ).last_updated_timestamp await async_recorder_block_till_done(hass) hass.states.async_set("switch.excluded", "off", attributes={"any": "again"}) await async_wait_recording_done(hass) @@ -1287,15 +1379,19 @@ async def test_history_stream_live_with_future_end_time( assert response["type"] == "result" response = await client.receive_json() - first_end_time = sensor_two_last_updated.timestamp() + first_end_time = sensor_two_last_updated_timestamp assert response == { "event": { - "end_time": first_end_time, - "start_time": now.timestamp(), + "end_time": pytest.approx(first_end_time), + "start_time": pytest.approx(now.timestamp()), "states": { - "sensor.one": [{"lu": sensor_one_last_updated.timestamp(), "s": "on"}], - "sensor.two": [{"lu": sensor_two_last_updated.timestamp(), "s": "off"}], + "sensor.one": [ + {"lu": pytest.approx(sensor_one_last_updated_timestamp), "s": "on"} + ], + "sensor.two": [ + {"lu": pytest.approx(sensor_two_last_updated_timestamp), "s": "off"} + ], }, }, "id": 1, @@ -1307,14 +1403,22 @@ async def test_history_stream_live_with_future_end_time( hass.states.async_set("sensor.two", "two", attributes={"any": "attr"}) await async_recorder_block_till_done(hass) - sensor_one_last_updated = hass.states.get("sensor.one").last_updated - sensor_two_last_updated = hass.states.get("sensor.two").last_updated + sensor_one_last_updated_timestamp = hass.states.get( + "sensor.one" + ).last_updated_timestamp + sensor_two_last_updated_timestamp = hass.states.get( + "sensor.two" + ).last_updated_timestamp response = await client.receive_json() assert response == { "event": { "states": { - "sensor.one": [{"lu": sensor_one_last_updated.timestamp(), "s": "one"}], - "sensor.two": [{"lu": sensor_two_last_updated.timestamp(), "s": "two"}], + "sensor.one": [ + {"lu": pytest.approx(sensor_one_last_updated_timestamp), "s": "one"} + ], + "sensor.two": [ + {"lu": pytest.approx(sensor_two_last_updated_timestamp), "s": "two"} + ], }, }, "id": 1, @@ -1450,10 +1554,14 @@ async def test_overflow_queue( await async_setup_component(hass, "sensor", {}) await async_recorder_block_till_done(hass) hass.states.async_set("sensor.one", "on", attributes={"any": "attr"}) - sensor_one_last_updated = hass.states.get("sensor.one").last_updated + sensor_one_last_updated_timestamp = hass.states.get( + "sensor.one" + ).last_updated_timestamp await async_recorder_block_till_done(hass) hass.states.async_set("sensor.two", "off", attributes={"any": "attr"}) - sensor_two_last_updated = hass.states.get("sensor.two").last_updated + sensor_two_last_updated_timestamp = hass.states.get( + "sensor.two" + ).last_updated_timestamp await async_recorder_block_till_done(hass) hass.states.async_set("switch.excluded", "off", attributes={"any": "again"}) await async_wait_recording_done(hass) @@ -1481,18 +1589,24 @@ async def test_overflow_queue( assert response["type"] == "result" response = await client.receive_json() - first_end_time = sensor_two_last_updated.timestamp() + first_end_time = sensor_two_last_updated_timestamp assert response == { "event": { - "end_time": first_end_time, - "start_time": now.timestamp(), + "end_time": pytest.approx(first_end_time), + "start_time": pytest.approx(now.timestamp()), "states": { "sensor.one": [ - {"lu": sensor_one_last_updated.timestamp(), "s": "on"} + { + "lu": pytest.approx(sensor_one_last_updated_timestamp), + "s": "on", + } ], "sensor.two": [ - {"lu": sensor_two_last_updated.timestamp(), "s": "off"} + { + "lu": pytest.approx(sensor_two_last_updated_timestamp), + "s": "off", + } ], }, }, @@ -1522,10 +1636,14 @@ async def test_history_during_period_for_invalid_entity_ids( await async_setup_component(hass, "sensor", {}) await async_recorder_block_till_done(hass) hass.states.async_set("sensor.one", "on", attributes={"any": "attr"}) - sensor_one_last_updated = hass.states.get("sensor.one").last_updated + sensor_one_last_updated_timestamp = hass.states.get( + "sensor.one" + ).last_updated_timestamp await async_recorder_block_till_done(hass) hass.states.async_set("sensor.two", "off", attributes={"any": "attr"}) - sensor_two_last_updated = hass.states.get("sensor.two").last_updated + sensor_two_last_updated_timestamp = hass.states.get( + "sensor.two" + ).last_updated_timestamp await async_recorder_block_till_done(hass) hass.states.async_set("sensor.three", "off", attributes={"any": "again"}) await async_recorder_block_till_done(hass) @@ -1550,7 +1668,11 @@ async def test_history_during_period_for_invalid_entity_ids( assert response == { "result": { "sensor.one": [ - {"a": {}, "lu": sensor_one_last_updated.timestamp(), "s": "on"} + { + "a": {}, + "lu": pytest.approx(sensor_one_last_updated_timestamp), + "s": "on", + } ], }, "id": 1, @@ -1574,10 +1696,18 @@ async def test_history_during_period_for_invalid_entity_ids( assert response == { "result": { "sensor.one": [ - {"a": {}, "lu": sensor_one_last_updated.timestamp(), "s": "on"} + { + "a": {}, + "lu": pytest.approx(sensor_one_last_updated_timestamp), + "s": "on", + } ], "sensor.two": [ - {"a": {}, "lu": sensor_two_last_updated.timestamp(), "s": "off"} + { + "a": {}, + "lu": pytest.approx(sensor_two_last_updated_timestamp), + "s": "off", + } ], }, "id": 2, @@ -1670,10 +1800,14 @@ async def test_history_stream_for_invalid_entity_ids( await async_setup_component(hass, "sensor", {}) await async_recorder_block_till_done(hass) hass.states.async_set("sensor.one", "on", attributes={"any": "attr"}) - sensor_one_last_updated = hass.states.get("sensor.one").last_updated + sensor_one_last_updated_timestamp = hass.states.get( + "sensor.one" + ).last_updated_timestamp await async_recorder_block_till_done(hass) hass.states.async_set("sensor.two", "off", attributes={"any": "attr"}) - sensor_two_last_updated = hass.states.get("sensor.two").last_updated + sensor_two_last_updated_timestamp = hass.states.get( + "sensor.two" + ).last_updated_timestamp await async_recorder_block_till_done(hass) hass.states.async_set("sensor.three", "off", attributes={"any": "again"}) await async_recorder_block_till_done(hass) @@ -1703,10 +1837,12 @@ async def test_history_stream_for_invalid_entity_ids( response = await client.receive_json() assert response == { "event": { - "end_time": sensor_one_last_updated.timestamp(), - "start_time": now.timestamp(), + "end_time": pytest.approx(sensor_one_last_updated_timestamp), + "start_time": pytest.approx(now.timestamp()), "states": { - "sensor.one": [{"lu": sensor_one_last_updated.timestamp(), "s": "on"}], + "sensor.one": [ + {"lu": pytest.approx(sensor_one_last_updated_timestamp), "s": "on"} + ], }, }, "id": 1, @@ -1733,11 +1869,15 @@ async def test_history_stream_for_invalid_entity_ids( response = await client.receive_json() assert response == { "event": { - "end_time": sensor_two_last_updated.timestamp(), - "start_time": now.timestamp(), + "end_time": pytest.approx(sensor_two_last_updated_timestamp), + "start_time": pytest.approx(now.timestamp()), "states": { - "sensor.one": [{"lu": sensor_one_last_updated.timestamp(), "s": "on"}], - "sensor.two": [{"lu": sensor_two_last_updated.timestamp(), "s": "off"}], + "sensor.one": [ + {"lu": pytest.approx(sensor_one_last_updated_timestamp), "s": "on"} + ], + "sensor.two": [ + {"lu": pytest.approx(sensor_two_last_updated_timestamp), "s": "off"} + ], }, }, "id": 2, @@ -1841,21 +1981,31 @@ async def test_history_stream_historical_only_with_start_time_state_past( now = dt_util.utcnow() await async_recorder_block_till_done(hass) hass.states.async_set("sensor.one", "second", attributes={"any": "attr"}) - sensor_one_last_updated_second = hass.states.get("sensor.one").last_updated + sensor_one_last_updated_second_timestamp = hass.states.get( + "sensor.one" + ).last_updated_timestamp await asyncio.sleep(0.00001) hass.states.async_set("sensor.one", "third", attributes={"any": "attr"}) - sensor_one_last_updated_third = hass.states.get("sensor.one").last_updated + sensor_one_last_updated_third_timestamp = hass.states.get( + "sensor.one" + ).last_updated_timestamp await async_recorder_block_till_done(hass) hass.states.async_set("sensor.two", "off", attributes={"any": "attr"}) - sensor_two_last_updated = hass.states.get("sensor.two").last_updated + sensor_two_last_updated_timestamp = hass.states.get( + "sensor.two" + ).last_updated_timestamp await async_recorder_block_till_done(hass) hass.states.async_set("sensor.three", "off", attributes={"any": "changed"}) - sensor_three_last_updated = hass.states.get("sensor.three").last_updated + sensor_three_last_updated_timestamp = hass.states.get( + "sensor.three" + ).last_updated_timestamp await async_recorder_block_till_done(hass) hass.states.async_set("sensor.four", "off", attributes={"any": "again"}) - sensor_four_last_updated = hass.states.get("sensor.four").last_updated + sensor_four_last_updated_timestamp = hass.states.get( + "sensor.four" + ).last_updated_timestamp await async_recorder_block_till_done(hass) hass.states.async_set("switch.excluded", "off", attributes={"any": "again"}) await async_wait_recording_done(hass) @@ -1885,24 +2035,38 @@ async def test_history_stream_historical_only_with_start_time_state_past( assert response == { "event": { - "end_time": sensor_four_last_updated.timestamp(), - "start_time": now.timestamp(), + "end_time": pytest.approx(sensor_four_last_updated_timestamp), + "start_time": pytest.approx(now.timestamp()), "states": { "sensor.four": [ - {"lu": sensor_four_last_updated.timestamp(), "s": "off"} + { + "lu": pytest.approx(sensor_four_last_updated_timestamp), + "s": "off", + } ], "sensor.one": [ { - "lu": now.timestamp(), + "lu": pytest.approx(now.timestamp()), "s": "first", }, # should use start time state - {"lu": sensor_one_last_updated_second.timestamp(), "s": "second"}, - {"lu": sensor_one_last_updated_third.timestamp(), "s": "third"}, + { + "lu": pytest.approx(sensor_one_last_updated_second_timestamp), + "s": "second", + }, + { + "lu": pytest.approx(sensor_one_last_updated_third_timestamp), + "s": "third", + }, ], "sensor.three": [ - {"lu": sensor_three_last_updated.timestamp(), "s": "off"} + { + "lu": pytest.approx(sensor_three_last_updated_timestamp), + "s": "off", + } + ], + "sensor.two": [ + {"lu": pytest.approx(sensor_two_last_updated_timestamp), "s": "off"} ], - "sensor.two": [{"lu": sensor_two_last_updated.timestamp(), "s": "off"}], }, }, "id": 1, diff --git a/tests/components/logbook/test_websocket_api.py b/tests/components/logbook/test_websocket_api.py index 1fb0e6eb24b..bd11c87f4df 100644 --- a/tests/components/logbook/test_websocket_api.py +++ b/tests/components/logbook/test_websocket_api.py @@ -630,7 +630,7 @@ async def test_subscribe_unsubscribe_logbook_stream_excluded_entities( { "entity_id": "binary_sensor.is_light", "state": "off", - "when": state.last_updated.timestamp(), + "when": state.last_updated_timestamp, } ] assert msg["event"]["start_time"] == now.timestamp() @@ -679,17 +679,17 @@ async def test_subscribe_unsubscribe_logbook_stream_excluded_entities( { "entity_id": "light.alpha", "state": "off", - "when": alpha_off_state.last_updated.timestamp(), + "when": alpha_off_state.last_updated_timestamp, }, { "entity_id": "light.zulu", "state": "off", - "when": zulu_off_state.last_updated.timestamp(), + "when": zulu_off_state.last_updated_timestamp, }, { "entity_id": "light.zulu", "state": "on", - "when": zulu_on_state.last_updated.timestamp(), + "when": zulu_on_state.last_updated_timestamp, }, ] @@ -1033,7 +1033,7 @@ async def test_logbook_stream_excluded_entities_inherits_filters_from_recorder( { "entity_id": "binary_sensor.is_light", "state": "off", - "when": state.last_updated.timestamp(), + "when": state.last_updated_timestamp, } ] assert msg["event"]["start_time"] == now.timestamp() @@ -1082,17 +1082,17 @@ async def test_logbook_stream_excluded_entities_inherits_filters_from_recorder( { "entity_id": "light.alpha", "state": "off", - "when": alpha_off_state.last_updated.timestamp(), + "when": alpha_off_state.last_updated_timestamp, }, { "entity_id": "light.zulu", "state": "off", - "when": zulu_off_state.last_updated.timestamp(), + "when": zulu_off_state.last_updated_timestamp, }, { "entity_id": "light.zulu", "state": "on", - "when": zulu_on_state.last_updated.timestamp(), + "when": zulu_on_state.last_updated_timestamp, }, ] @@ -1201,7 +1201,7 @@ async def test_subscribe_unsubscribe_logbook_stream( { "entity_id": "binary_sensor.is_light", "state": "off", - "when": state.last_updated.timestamp(), + "when": state.last_updated_timestamp, } ] assert msg["event"]["start_time"] == now.timestamp() @@ -1241,17 +1241,17 @@ async def test_subscribe_unsubscribe_logbook_stream( { "entity_id": "light.alpha", "state": "off", - "when": alpha_off_state.last_updated.timestamp(), + "when": alpha_off_state.last_updated_timestamp, }, { "entity_id": "light.zulu", "state": "off", - "when": zulu_off_state.last_updated.timestamp(), + "when": zulu_off_state.last_updated_timestamp, }, { "entity_id": "light.zulu", "state": "on", - "when": zulu_on_state.last_updated.timestamp(), + "when": zulu_on_state.last_updated_timestamp, }, ] @@ -1514,7 +1514,7 @@ async def test_subscribe_unsubscribe_logbook_stream_entities( { "entity_id": "binary_sensor.is_light", "state": "off", - "when": state.last_updated.timestamp(), + "when": state.last_updated_timestamp, } ] @@ -1613,7 +1613,7 @@ async def test_subscribe_unsubscribe_logbook_stream_entities_with_end_time( { "entity_id": "binary_sensor.is_light", "state": "off", - "when": state.last_updated.timestamp(), + "when": state.last_updated_timestamp, } ] @@ -1716,7 +1716,7 @@ async def test_subscribe_unsubscribe_logbook_stream_entities_past_only( { "entity_id": "binary_sensor.is_light", "state": "off", - "when": state.last_updated.timestamp(), + "when": state.last_updated_timestamp, } ] @@ -1804,7 +1804,7 @@ async def test_subscribe_unsubscribe_logbook_stream_big_query( { "entity_id": "binary_sensor.is_light", "state": "on", - "when": current_state.last_updated.timestamp(), + "when": current_state.last_updated_timestamp, } ] @@ -1817,7 +1817,7 @@ async def test_subscribe_unsubscribe_logbook_stream_big_query( { "entity_id": "binary_sensor.four_days_ago", "state": "off", - "when": four_day_old_state.last_updated.timestamp(), + "when": four_day_old_state.last_updated_timestamp, } ] @@ -2363,7 +2363,7 @@ async def test_subscribe_disconnected( { "entity_id": "binary_sensor.is_light", "state": "off", - "when": state.last_updated.timestamp(), + "when": state.last_updated_timestamp, } ] @@ -2790,7 +2790,7 @@ async def test_logbook_stream_ignores_forced_updates( { "entity_id": "binary_sensor.is_light", "state": "off", - "when": state.last_updated.timestamp(), + "when": state.last_updated_timestamp, } ] assert msg["event"]["start_time"] == now.timestamp() diff --git a/tests/components/websocket_api/test_messages.py b/tests/components/websocket_api/test_messages.py index 6294b6a2628..cb8a026fe0d 100644 --- a/tests/components/websocket_api/test_messages.py +++ b/tests/components/websocket_api/test_messages.py @@ -96,9 +96,7 @@ async def test_state_diff_event(hass: HomeAssistant) -> None: message = _state_diff_event(last_state_event) assert message == { "c": { - "light.window": { - "+": {"lc": new_state.last_changed.timestamp(), "s": "off"} - } + "light.window": {"+": {"lc": new_state.last_changed_timestamp, "s": "off"}} } } @@ -117,7 +115,7 @@ async def test_state_diff_event(hass: HomeAssistant) -> None: "light.window": { "+": { "c": {"parent_id": "new-parent-id"}, - "lc": new_state.last_changed.timestamp(), + "lc": new_state.last_changed_timestamp, "s": "red", } } @@ -144,7 +142,7 @@ async def test_state_diff_event(hass: HomeAssistant) -> None: "parent_id": "another-new-parent-id", "user_id": "new-user-id", }, - "lc": new_state.last_changed.timestamp(), + "lc": new_state.last_changed_timestamp, "s": "green", } } @@ -168,7 +166,7 @@ async def test_state_diff_event(hass: HomeAssistant) -> None: "light.window": { "+": { "c": {"user_id": "another-new-user-id"}, - "lc": new_state.last_changed.timestamp(), + "lc": new_state.last_changed_timestamp, "s": "blue", } } @@ -194,7 +192,7 @@ async def test_state_diff_event(hass: HomeAssistant) -> None: "light.window": { "+": { "c": "id-new", - "lc": new_state.last_changed.timestamp(), + "lc": new_state.last_changed_timestamp, "s": "yellow", } } @@ -216,7 +214,7 @@ async def test_state_diff_event(hass: HomeAssistant) -> None: "+": { "a": {"new": "attr"}, "c": {"id": new_context.id, "parent_id": None, "user_id": None}, - "lc": new_state.last_changed.timestamp(), + "lc": new_state.last_changed_timestamp, "s": "purple", } } @@ -232,7 +230,7 @@ async def test_state_diff_event(hass: HomeAssistant) -> None: assert message == { "c": { "light.window": { - "+": {"lc": new_state.last_changed.timestamp(), "s": "green"}, + "+": {"lc": new_state.last_changed_timestamp, "s": "green"}, "-": {"a": ["new"]}, } } @@ -254,7 +252,7 @@ async def test_state_diff_event(hass: HomeAssistant) -> None: "light.window": { "+": { "a": {"list_attr": ["a", "b", "c", "d"], "list_attr_2": ["a", "b"]}, - "lu": new_state.last_updated.timestamp(), + "lu": new_state.last_updated_timestamp, } } } @@ -275,7 +273,7 @@ async def test_state_diff_event(hass: HomeAssistant) -> None: "light.window": { "+": { "a": {"list_attr": ["a", "b", "c", "e"]}, - "lu": new_state.last_updated.timestamp(), + "lu": new_state.last_updated_timestamp, }, "-": {"a": ["list_attr_2"]}, } diff --git a/tests/test_core.py b/tests/test_core.py index fa94b4e658c..6848d209d02 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -2834,8 +2834,32 @@ async def test_state_change_events_context_id_match_state_time( assert state.last_updated == events[0].time_fired assert len(state.context.id) == 26 # ULIDs store time to 3 decimal places compared to python timestamps - assert _ulid_timestamp(state.context.id) == int( - state.last_updated.timestamp() * 1000 + assert _ulid_timestamp(state.context.id) == int(state.last_updated_timestamp * 1000) + + +async def test_state_change_events_match_time_with_limits_of_precision( + hass: HomeAssistant, +) -> None: + """Ensure last_updated matches last_updated_timestamp within limits of precision. + + The last_updated_timestamp uses the same precision as time.time() which is + a bit better than the precision of datetime.now() which is used for last_updated + on some platforms. + """ + events = async_capture_events(hass, ha.EVENT_STATE_CHANGED) + hass.states.async_set("light.bedroom", "on") + await hass.async_block_till_done() + state: State = hass.states.get("light.bedroom") + assert state.last_updated == events[0].time_fired + assert state.last_updated_timestamp == pytest.approx( + events[0].time_fired.timestamp() + ) + assert state.last_updated_timestamp == pytest.approx(state.last_updated.timestamp()) + assert state.last_updated_timestamp == state.last_changed_timestamp + assert state.last_updated_timestamp == pytest.approx(state.last_changed.timestamp()) + assert state.last_updated_timestamp == state.last_reported_timestamp + assert state.last_updated_timestamp == pytest.approx( + state.last_reported.timestamp() ) From 4596b89201a5667d18b25d5c3ddc02bc5a259730 Mon Sep 17 00:00:00 2001 From: "Mr. Bubbles" Date: Thu, 6 Jun 2024 09:10:46 +0200 Subject: [PATCH 0273/1445] Bump pyecotrend_ista to 3.2.0 (#118924) --- homeassistant/components/ista_ecotrend/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ista_ecotrend/manifest.json b/homeassistant/components/ista_ecotrend/manifest.json index 679825439e4..988f278a1c9 100644 --- a/homeassistant/components/ista_ecotrend/manifest.json +++ b/homeassistant/components/ista_ecotrend/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ista_ecotrend", "iot_class": "cloud_polling", - "requirements": ["pyecotrend-ista==3.1.1"] + "requirements": ["pyecotrend-ista==3.2.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 79407029d6a..bba2bfcd190 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1809,7 +1809,7 @@ pyecoforest==0.4.0 pyeconet==0.1.22 # homeassistant.components.ista_ecotrend -pyecotrend-ista==3.1.1 +pyecotrend-ista==3.2.0 # homeassistant.components.edimax pyedimax==0.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4d0495cf3b3..4bbed5d9091 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1420,7 +1420,7 @@ pyecoforest==0.4.0 pyeconet==0.1.22 # homeassistant.components.ista_ecotrend -pyecotrend-ista==3.1.1 +pyecotrend-ista==3.2.0 # homeassistant.components.efergy pyefergy==22.5.0 From b5b7c9bcd5cfbf4a66f8656146f125d40f91ed87 Mon Sep 17 00:00:00 2001 From: Regin Larsen Date: Thu, 6 Jun 2024 09:16:57 +0200 Subject: [PATCH 0274/1445] Bump xiaomi-ble to 0.29.0 (#118895) --- homeassistant/components/xiaomi_ble/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/xiaomi_ble/manifest.json b/homeassistant/components/xiaomi_ble/manifest.json index ef0556b6966..2a1d253b603 100644 --- a/homeassistant/components/xiaomi_ble/manifest.json +++ b/homeassistant/components/xiaomi_ble/manifest.json @@ -24,5 +24,5 @@ "dependencies": ["bluetooth_adapters"], "documentation": "https://www.home-assistant.io/integrations/xiaomi_ble", "iot_class": "local_push", - "requirements": ["xiaomi-ble==0.28.0"] + "requirements": ["xiaomi-ble==0.29.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index bba2bfcd190..66892f06e3c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2903,7 +2903,7 @@ wyoming==1.5.4 xbox-webapi==2.0.11 # homeassistant.components.xiaomi_ble -xiaomi-ble==0.28.0 +xiaomi-ble==0.29.0 # homeassistant.components.knx xknx==2.12.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4bbed5d9091..6c026c36110 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2262,7 +2262,7 @@ wyoming==1.5.4 xbox-webapi==2.0.11 # homeassistant.components.xiaomi_ble -xiaomi-ble==0.28.0 +xiaomi-ble==0.29.0 # homeassistant.components.knx xknx==2.12.2 From c7cc465e5cd56c139c8f491424ae2eec85e5facd Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 6 Jun 2024 10:11:29 +0200 Subject: [PATCH 0275/1445] Add return type hints in tests (k-z) (#118942) --- tests/components/knx/test_device_trigger.py | 2 +- tests/components/knx/test_telegrams.py | 6 +++--- tests/components/knx/test_websocket.py | 20 +++++++++---------- .../components/lg_netcast/test_config_flow.py | 4 ++-- tests/components/lg_netcast/test_trigger.py | 2 +- .../local_calendar/test_calendar.py | 2 +- tests/components/logbook/test_models.py | 2 +- tests/components/loqed/test_init.py | 16 +++++++++------ tests/components/matrix/test_commands.py | 6 +++--- tests/components/matrix/test_login.py | 4 ++-- tests/components/matrix/test_matrix_bot.py | 2 +- tests/components/matrix/test_send_message.py | 4 ++-- tests/components/matter/test_fan.py | 10 +++++----- tests/components/matter/test_init.py | 6 +++--- tests/components/nibe_heatpump/test_button.py | 2 +- tests/components/openhome/test_update.py | 8 ++++---- tests/components/opensky/test_sensor.py | 6 +++--- tests/components/ping/test_binary_sensor.py | 4 ++-- tests/components/ping/test_device_tracker.py | 9 +++------ tests/components/rest_command/test_init.py | 2 +- tests/components/template/test_light.py | 14 ++++++------- tests/components/tplink/test_diagnostics.py | 2 +- tests/components/vultr/test_switch.py | 6 +++--- tests/components/wemo/test_coordinator.py | 8 +++++--- tests/components/zha/test_base.py | 4 ++-- tests/components/zha/test_gateway.py | 4 ++-- 26 files changed, 79 insertions(+), 76 deletions(-) diff --git a/tests/components/knx/test_device_trigger.py b/tests/components/knx/test_device_trigger.py index 278267c4f8a..2fd15150503 100644 --- a/tests/components/knx/test_device_trigger.py +++ b/tests/components/knx/test_device_trigger.py @@ -397,7 +397,7 @@ async def test_invalid_trigger_configuration( hass: HomeAssistant, device_registry: dr.DeviceRegistry, knx: KNXTestKit, -): +) -> None: """Test invalid telegram device trigger configuration at attach_trigger.""" await knx.setup_integration({}) device_entry = device_registry.async_get_device( diff --git a/tests/components/knx/test_telegrams.py b/tests/components/knx/test_telegrams.py index 844fc073d61..4d72a9583a1 100644 --- a/tests/components/knx/test_telegrams.py +++ b/tests/components/knx/test_telegrams.py @@ -66,7 +66,7 @@ async def test_store_telegam_history( hass: HomeAssistant, knx: KNXTestKit, hass_storage: dict[str, Any], -): +) -> None: """Test storing telegram history.""" await knx.setup_integration({}) @@ -89,7 +89,7 @@ async def test_load_telegam_history( hass: HomeAssistant, knx: KNXTestKit, hass_storage: dict[str, Any], -): +) -> None: """Test telegram history restoration.""" hass_storage["knx/telegrams_history.json"] = {"version": 1, "data": MOCK_TELEGRAMS} await knx.setup_integration({}) @@ -103,7 +103,7 @@ async def test_remove_telegam_history( hass: HomeAssistant, knx: KNXTestKit, hass_storage: dict[str, Any], -): +) -> None: """Test telegram history removal when configured to size 0.""" hass_storage["knx/telegrams_history.json"] = {"version": 1, "data": MOCK_TELEGRAMS} knx.mock_config_entry.add_to_hass(hass) diff --git a/tests/components/knx/test_websocket.py b/tests/components/knx/test_websocket.py index 78cbb98a7a0..ca60905b0ba 100644 --- a/tests/components/knx/test_websocket.py +++ b/tests/components/knx/test_websocket.py @@ -14,7 +14,7 @@ from tests.typing import WebSocketGenerator async def test_knx_info_command( hass: HomeAssistant, knx: KNXTestKit, hass_ws_client: WebSocketGenerator -): +) -> None: """Test knx/info command.""" await knx.setup_integration({}) client = await hass_ws_client(hass) @@ -33,7 +33,7 @@ async def test_knx_info_command_with_project( knx: KNXTestKit, hass_ws_client: WebSocketGenerator, load_knxproj: None, -): +) -> None: """Test knx/info command with loaded project.""" await knx.setup_integration({}) client = await hass_ws_client(hass) @@ -55,7 +55,7 @@ async def test_knx_project_file_process( knx: KNXTestKit, hass_ws_client: WebSocketGenerator, hass_storage: dict[str, Any], -): +) -> None: """Test knx/project_file_process command for storing and loading new data.""" _file_id = "1234" _password = "pw-test" @@ -93,7 +93,7 @@ async def test_knx_project_file_process_error( hass: HomeAssistant, knx: KNXTestKit, hass_ws_client: WebSocketGenerator, -): +) -> None: """Test knx/project_file_process exception handling.""" await knx.setup_integration({}) client = await hass_ws_client(hass) @@ -126,7 +126,7 @@ async def test_knx_project_file_remove( knx: KNXTestKit, hass_ws_client: WebSocketGenerator, load_knxproj: None, -): +) -> None: """Test knx/project_file_remove command.""" await knx.setup_integration({}) client = await hass_ws_client(hass) @@ -146,7 +146,7 @@ async def test_knx_get_project( knx: KNXTestKit, hass_ws_client: WebSocketGenerator, load_knxproj: None, -): +) -> None: """Test retrieval of kxnproject from store.""" await knx.setup_integration({}) client = await hass_ws_client(hass) @@ -161,7 +161,7 @@ async def test_knx_get_project( async def test_knx_group_monitor_info_command( hass: HomeAssistant, knx: KNXTestKit, hass_ws_client: WebSocketGenerator -): +) -> None: """Test knx/group_monitor_info command.""" await knx.setup_integration({}) client = await hass_ws_client(hass) @@ -176,7 +176,7 @@ async def test_knx_group_monitor_info_command( async def test_knx_subscribe_telegrams_command_recent_telegrams( hass: HomeAssistant, knx: KNXTestKit, hass_ws_client: WebSocketGenerator -): +) -> None: """Test knx/subscribe_telegrams command sending recent telegrams.""" await knx.setup_integration( { @@ -224,7 +224,7 @@ async def test_knx_subscribe_telegrams_command_recent_telegrams( async def test_knx_subscribe_telegrams_command_no_project( hass: HomeAssistant, knx: KNXTestKit, hass_ws_client: WebSocketGenerator -): +) -> None: """Test knx/subscribe_telegrams command without project data.""" await knx.setup_integration( { @@ -299,7 +299,7 @@ async def test_knx_subscribe_telegrams_command_project( knx: KNXTestKit, hass_ws_client: WebSocketGenerator, load_knxproj: None, -): +) -> None: """Test knx/subscribe_telegrams command with project data.""" await knx.setup_integration({}) client = await hass_ws_client(hass) diff --git a/tests/components/lg_netcast/test_config_flow.py b/tests/components/lg_netcast/test_config_flow.py index c159b8fb9d2..2ecbadbaf44 100644 --- a/tests/components/lg_netcast/test_config_flow.py +++ b/tests/components/lg_netcast/test_config_flow.py @@ -187,7 +187,7 @@ async def test_import_not_online(hass: HomeAssistant) -> None: assert result["reason"] == "cannot_connect" -async def test_import_duplicate_error(hass): +async def test_import_duplicate_error(hass: HomeAssistant) -> None: """Test that errors are shown when duplicates are added during import.""" config_entry = MockConfigEntry( domain=DOMAIN, @@ -217,7 +217,7 @@ async def test_import_duplicate_error(hass): assert result["reason"] == "already_configured" -async def test_display_access_token_aborted(hass: HomeAssistant): +async def test_display_access_token_aborted(hass: HomeAssistant) -> None: """Test Access token display is cancelled.""" def _async_track_time_interval( diff --git a/tests/components/lg_netcast/test_trigger.py b/tests/components/lg_netcast/test_trigger.py index f448c08ffd0..b0c2a86ec21 100644 --- a/tests/components/lg_netcast/test_trigger.py +++ b/tests/components/lg_netcast/test_trigger.py @@ -79,7 +79,7 @@ async def test_lg_netcast_turn_on_trigger_device_id( async def test_lg_netcast_turn_on_trigger_entity_id( hass: HomeAssistant, calls: list[ServiceCall] -): +) -> None: """Test for turn_on triggers by entity firing.""" await setup_lgnetcast(hass) diff --git a/tests/components/local_calendar/test_calendar.py b/tests/components/local_calendar/test_calendar.py index 2fa0063dfd8..61908faeca6 100644 --- a/tests/components/local_calendar/test_calendar.py +++ b/tests/components/local_calendar/test_calendar.py @@ -785,7 +785,7 @@ async def test_all_day_iter_order( setup_integration: None, get_events: GetEventsFn, event_order: list[str], -): +) -> None: """Test the sort order of an all day events depending on the time zone.""" client = await ws_client() await client.cmd_result( diff --git a/tests/components/logbook/test_models.py b/tests/components/logbook/test_models.py index 459fd0e06c9..7021711014f 100644 --- a/tests/components/logbook/test_models.py +++ b/tests/components/logbook/test_models.py @@ -5,7 +5,7 @@ from unittest.mock import Mock from homeassistant.components.logbook.models import LazyEventPartialState -def test_lazy_event_partial_state_context(): +def test_lazy_event_partial_state_context() -> None: """Test we can extract context from a lazy event partial state.""" state = LazyEventPartialState( Mock( diff --git a/tests/components/loqed/test_init.py b/tests/components/loqed/test_init.py index ef05f2b757a..ed38b63fdb1 100644 --- a/tests/components/loqed/test_init.py +++ b/tests/components/loqed/test_init.py @@ -42,7 +42,7 @@ async def test_webhook_accepts_valid_message( async def test_setup_webhook_in_bridge( hass: HomeAssistant, config_entry: MockConfigEntry, lock: loqed.Lock -): +) -> None: """Test webhook setup in loqed bridge.""" config: dict[str, Any] = {DOMAIN: {}} config_entry.add_to_hass(hass) @@ -65,7 +65,7 @@ async def test_setup_webhook_in_bridge( async def test_cannot_connect_to_bridge_will_retry( hass: HomeAssistant, config_entry: MockConfigEntry, lock: loqed.Lock -): +) -> None: """Test webhook setup in loqed bridge.""" config: dict[str, Any] = {DOMAIN: {}} config_entry.add_to_hass(hass) @@ -81,7 +81,7 @@ async def test_cannot_connect_to_bridge_will_retry( async def test_setup_cloudhook_in_bridge( hass: HomeAssistant, config_entry: MockConfigEntry, lock: loqed.Lock -): +) -> None: """Test webhook setup in loqed bridge.""" config: dict[str, Any] = {DOMAIN: {}} config_entry.add_to_hass(hass) @@ -112,7 +112,7 @@ async def test_setup_cloudhook_in_bridge( async def test_setup_cloudhook_from_entry_in_bridge( hass: HomeAssistant, cloud_config_entry: MockConfigEntry, lock: loqed.Lock -): +) -> None: """Test webhook setup in loqed bridge.""" webhooks_fixture = json.loads(load_fixture("loqed/get_all_webhooks.json")) @@ -143,7 +143,9 @@ async def test_setup_cloudhook_from_entry_in_bridge( lock.registerWebhook.assert_called_with(f"{get_url(hass)}/api/webhook/Webhook_id") -async def test_unload_entry(hass, integration: MockConfigEntry, lock: loqed.Lock): +async def test_unload_entry( + hass, integration: MockConfigEntry, lock: loqed.Lock +) -> None: """Test successful unload of entry.""" assert await hass.config_entries.async_unload(integration.entry_id) @@ -154,7 +156,9 @@ async def test_unload_entry(hass, integration: MockConfigEntry, lock: loqed.Lock assert not hass.data.get(DOMAIN) -async def test_unload_entry_fails(hass, integration: MockConfigEntry, lock: loqed.Lock): +async def test_unload_entry_fails( + hass, integration: MockConfigEntry, lock: loqed.Lock +) -> None: """Test unsuccessful unload of entry.""" lock.deleteWebhook = AsyncMock(side_effect=Exception) diff --git a/tests/components/matrix/test_commands.py b/tests/components/matrix/test_commands.py index f71ec22e794..17d92760fa0 100644 --- a/tests/components/matrix/test_commands.py +++ b/tests/components/matrix/test_commands.py @@ -131,7 +131,7 @@ async def test_commands( matrix_bot: MatrixBot, command_events: list[Event], command_params: CommandTestParameters, -): +) -> None: """Test that the configured commands are used correctly.""" room = MatrixRoom(room_id=command_params.room_id, own_user_id=matrix_bot._mx_id) @@ -160,7 +160,7 @@ async def test_non_commands( matrix_bot: MatrixBot, command_events: list[Event], command_params: CommandTestParameters, -): +) -> None: """Test that normal/non-qualifying messages don't wrongly trigger commands.""" room = MatrixRoom(room_id=command_params.room_id, own_user_id=matrix_bot._mx_id) @@ -173,7 +173,7 @@ async def test_non_commands( assert len(command_events) == 0 -async def test_commands_parsing(hass: HomeAssistant, matrix_bot: MatrixBot): +async def test_commands_parsing(hass: HomeAssistant, matrix_bot: MatrixBot) -> None: """Test that the configured commands were parsed correctly.""" await hass.async_start() diff --git a/tests/components/matrix/test_login.py b/tests/components/matrix/test_login.py index 8112d98fc8c..caf74576d4e 100644 --- a/tests/components/matrix/test_login.py +++ b/tests/components/matrix/test_login.py @@ -90,7 +90,7 @@ bad_password_missing_access_token = LoginTestParameters( ) async def test_login( matrix_bot: MatrixBot, caplog: pytest.LogCaptureFixture, params: LoginTestParameters -): +) -> None: """Test logging in with the given parameters and expected state.""" await matrix_bot._client.logout() matrix_bot._password = params.password @@ -105,7 +105,7 @@ async def test_login( assert set(caplog.messages).issuperset(params.expected_caplog_messages) -async def test_get_auth_tokens(matrix_bot: MatrixBot, mock_load_json): +async def test_get_auth_tokens(matrix_bot: MatrixBot, mock_load_json) -> None: """Test loading access_tokens from a mocked file.""" # Test loading good tokens. diff --git a/tests/components/matrix/test_matrix_bot.py b/tests/components/matrix/test_matrix_bot.py index bfd6d5824cb..cae8dbef76d 100644 --- a/tests/components/matrix/test_matrix_bot.py +++ b/tests/components/matrix/test_matrix_bot.py @@ -11,7 +11,7 @@ from homeassistant.core import HomeAssistant from .conftest import TEST_NOTIFIER_NAME -async def test_services(hass: HomeAssistant, matrix_bot: MatrixBot): +async def test_services(hass: HomeAssistant, matrix_bot: MatrixBot) -> None: """Test hass/MatrixBot state.""" services = hass.services.async_services() diff --git a/tests/components/matrix/test_send_message.py b/tests/components/matrix/test_send_message.py index 0f3a57e90f1..58c0573a22e 100644 --- a/tests/components/matrix/test_send_message.py +++ b/tests/components/matrix/test_send_message.py @@ -21,7 +21,7 @@ async def test_send_message( image_path, matrix_events, caplog: pytest.LogCaptureFixture, -): +) -> None: """Test the send_message service.""" await hass.async_start() @@ -65,7 +65,7 @@ async def test_unsendable_message( matrix_bot: MatrixBot, matrix_events, caplog: pytest.LogCaptureFixture, -): +) -> None: """Test the send_message service with an invalid room.""" assert len(matrix_events) == 0 await matrix_bot._login() diff --git a/tests/components/matter/test_fan.py b/tests/components/matter/test_fan.py index fe466aa15b3..3c4a990018b 100644 --- a/tests/components/matter/test_fan.py +++ b/tests/components/matter/test_fan.py @@ -98,7 +98,7 @@ async def test_fan_turn_on_with_percentage( hass: HomeAssistant, matter_client: MagicMock, air_purifier: MatterNode, -): +) -> None: """Test turning on the fan with a specific percentage.""" entity_id = "fan.air_purifier" await hass.services.async_call( @@ -119,7 +119,7 @@ async def test_fan_turn_on_with_preset_mode( hass: HomeAssistant, matter_client: MagicMock, air_purifier: MatterNode, -): +) -> None: """Test turning on the fan with a specific preset mode.""" entity_id = "fan.air_purifier" await hass.services.async_call( @@ -191,7 +191,7 @@ async def test_fan_turn_off( hass: HomeAssistant, matter_client: MagicMock, air_purifier: MatterNode, -): +) -> None: """Test turning off the fan.""" entity_id = "fan.air_purifier" await hass.services.async_call( @@ -233,7 +233,7 @@ async def test_fan_oscillate( hass: HomeAssistant, matter_client: MagicMock, air_purifier: MatterNode, -): +) -> None: """Test oscillating the fan.""" entity_id = "fan.air_purifier" for oscillating, value in ((True, 1), (False, 0)): @@ -256,7 +256,7 @@ async def test_fan_set_direction( hass: HomeAssistant, matter_client: MagicMock, air_purifier: MatterNode, -): +) -> None: """Test oscillating the fan.""" entity_id = "fan.air_purifier" for direction, value in ((DIRECTION_FORWARD, 0), (DIRECTION_REVERSE, 1)): diff --git a/tests/components/matter/test_init.py b/tests/components/matter/test_init.py index 6e0a22188ec..9809220099f 100644 --- a/tests/components/matter/test_init.py +++ b/tests/components/matter/test_init.py @@ -386,7 +386,7 @@ async def test_update_addon( backup_calls: int, update_addon_side_effect: Exception | None, create_backup_side_effect: Exception | None, -): +) -> None: """Test update the Matter add-on during entry setup.""" addon_info.return_value["version"] = addon_version addon_info.return_value["update_available"] = update_available @@ -453,7 +453,7 @@ async def test_issue_registry_invalid_version( ], ) async def test_stop_addon( - hass, + hass: HomeAssistant, matter_client: MagicMock, addon_installed: AsyncMock, addon_running: AsyncMock, @@ -461,7 +461,7 @@ async def test_stop_addon( stop_addon: AsyncMock, stop_addon_side_effect: Exception | None, entry_state: ConfigEntryState, -): +) -> None: """Test stop the Matter add-on on entry unload if entry is disabled.""" stop_addon.side_effect = stop_addon_side_effect entry = MockConfigEntry( diff --git a/tests/components/nibe_heatpump/test_button.py b/tests/components/nibe_heatpump/test_button.py index e660340c549..5015bba4092 100644 --- a/tests/components/nibe_heatpump/test_button.py +++ b/tests/components/nibe_heatpump/test_button.py @@ -41,7 +41,7 @@ async def test_reset_button( entity_id: str, coils: dict[int, Any], freezer_ticker: Any, -): +) -> None: """Test reset button.""" unit = UNIT_COILGROUPS[model.series]["main"] diff --git a/tests/components/openhome/test_update.py b/tests/components/openhome/test_update.py index d3a328b9f9e..354ed26af64 100644 --- a/tests/components/openhome/test_update.py +++ b/tests/components/openhome/test_update.py @@ -89,7 +89,7 @@ async def setup_integration( await hass.async_block_till_done() -async def test_not_supported(hass: HomeAssistant): +async def test_not_supported(hass: HomeAssistant) -> None: """Ensure update entity works if service not supported.""" update_firmware = AsyncMock() @@ -107,7 +107,7 @@ async def test_not_supported(hass: HomeAssistant): update_firmware.assert_not_called() -async def test_on_latest_firmware(hass: HomeAssistant): +async def test_on_latest_firmware(hass: HomeAssistant) -> None: """Test device on latest firmware.""" update_firmware = AsyncMock() @@ -125,7 +125,7 @@ async def test_on_latest_firmware(hass: HomeAssistant): update_firmware.assert_not_called() -async def test_update_available(hass: HomeAssistant): +async def test_update_available(hass: HomeAssistant) -> None: """Test device has firmware update available.""" update_firmware = AsyncMock() @@ -158,7 +158,7 @@ async def test_update_available(hass: HomeAssistant): update_firmware.assert_called_once() -async def test_firmware_update_not_required(hass: HomeAssistant): +async def test_firmware_update_not_required(hass: HomeAssistant) -> None: """Ensure firmware install does nothing if up to date.""" update_firmware = AsyncMock() diff --git a/tests/components/opensky/test_sensor.py b/tests/components/opensky/test_sensor.py index 801980ec5b9..0c84762dd50 100644 --- a/tests/components/opensky/test_sensor.py +++ b/tests/components/opensky/test_sensor.py @@ -27,7 +27,7 @@ async def test_sensor( config_entry: MockConfigEntry, snapshot: SnapshotAssertion, opensky_client: AsyncMock, -): +) -> None: """Test setup sensor.""" await setup_integration(hass, config_entry) @@ -48,7 +48,7 @@ async def test_sensor_altitude( config_entry_altitude: MockConfigEntry, opensky_client: AsyncMock, snapshot: SnapshotAssertion, -): +) -> None: """Test setup sensor with a set altitude.""" await setup_integration(hass, config_entry_altitude) @@ -62,7 +62,7 @@ async def test_sensor_updating( opensky_client: AsyncMock, freezer: FrozenDateTimeFactory, snapshot: SnapshotAssertion, -): +) -> None: """Test updating sensor.""" await setup_integration(hass, config_entry) diff --git a/tests/components/ping/test_binary_sensor.py b/tests/components/ping/test_binary_sensor.py index a8346b9a634..ea3145af253 100644 --- a/tests/components/ping/test_binary_sensor.py +++ b/tests/components/ping/test_binary_sensor.py @@ -49,7 +49,7 @@ async def test_disabled_after_import( hass: HomeAssistant, config_entry: MockConfigEntry, entity_registry: er.EntityRegistry, -): +) -> None: """Test if binary sensor is disabled after import.""" config_entry.add_to_hass(hass) hass.config_entries.async_update_entry( @@ -69,7 +69,7 @@ async def test_disabled_after_import( async def test_import_issue_creation( hass: HomeAssistant, issue_registry: ir.IssueRegistry, -): +) -> None: """Test if import issue is raised.""" await async_setup_component( diff --git a/tests/components/ping/test_device_tracker.py b/tests/components/ping/test_device_tracker.py index a01bd0fa1bf..b1e08c3607b 100644 --- a/tests/components/ping/test_device_tracker.py +++ b/tests/components/ping/test_device_tracker.py @@ -88,7 +88,7 @@ async def test_setup_and_update( async def test_import_issue_creation( hass: HomeAssistant, issue_registry: ir.IssueRegistry, -): +) -> None: """Test if import issue is raised.""" await async_setup_component( @@ -107,10 +107,7 @@ async def test_import_issue_creation( assert issue -async def test_import_delete_known_devices( - hass: HomeAssistant, - entity_registry: er.EntityRegistry, -): +async def test_import_delete_known_devices(hass: HomeAssistant) -> None: """Test if import deletes known devices.""" yaml_devices = { "test": { @@ -147,7 +144,7 @@ async def test_reload_not_triggering_home( hass: HomeAssistant, freezer: FrozenDateTimeFactory, config_entry: MockConfigEntry, -): +) -> None: """Test if reload/restart does not trigger home when device is unavailable.""" assert hass.states.get("device_tracker.10_10_10_10").state == "home" diff --git a/tests/components/rest_command/test_init.py b/tests/components/rest_command/test_init.py index 4f88e1b9d34..4429fe4011e 100644 --- a/tests/components/rest_command/test_init.py +++ b/tests/components/rest_command/test_init.py @@ -154,7 +154,7 @@ async def test_rest_command_methods( setup_component: ComponentSetup, aioclient_mock: AiohttpClientMocker, method: str, -): +) -> None: """Test various http methods.""" await setup_component() diff --git a/tests/components/template/test_light.py b/tests/components/template/test_light.py index e2b08242453..ad97146d0fb 100644 --- a/tests/components/template/test_light.py +++ b/tests/components/template/test_light.py @@ -881,7 +881,7 @@ async def test_legacy_color_action_no_template( hass: HomeAssistant, setup_light, calls: list[ServiceCall], -): +) -> None: """Test setting color with optimistic template.""" state = hass.states.get("light.test_template_light") assert state.attributes.get("hs_color") is None @@ -1103,12 +1103,12 @@ async def test_rgbww_color_action_no_template( ], ) async def test_legacy_color_template( - hass, - expected_hs, - expected_color_mode, - count, - color_template, -): + hass: HomeAssistant, + expected_hs: tuple[float, float] | None, + expected_color_mode: ColorMode, + count: int, + color_template: str, +) -> None: """Test the template for the color.""" light_config = { "test_template_light": { diff --git a/tests/components/tplink/test_diagnostics.py b/tests/components/tplink/test_diagnostics.py index bda5b143a6a..3543cf95572 100644 --- a/tests/components/tplink/test_diagnostics.py +++ b/tests/components/tplink/test_diagnostics.py @@ -38,7 +38,7 @@ async def test_diagnostics( fixture_file: str, sysinfo_vars: list[str], expected_oui: str | None, -): +) -> None: """Test diagnostics for config entry.""" diagnostics_data = json.loads(load_fixture(fixture_file, "tplink")) diff --git a/tests/components/vultr/test_switch.py b/tests/components/vultr/test_switch.py index f75021efa05..14c88d1e878 100644 --- a/tests/components/vultr/test_switch.py +++ b/tests/components/vultr/test_switch.py @@ -50,7 +50,7 @@ def load_hass_devices(hass: HomeAssistant): @pytest.mark.usefixtures("valid_config") -def test_switch(hass: HomeAssistant, hass_devices: list[vultr.VultrSwitch]): +def test_switch(hass: HomeAssistant, hass_devices: list[vultr.VultrSwitch]) -> None: """Test successful instance.""" assert len(hass_devices) == 3 @@ -97,7 +97,7 @@ def test_switch(hass: HomeAssistant, hass_devices: list[vultr.VultrSwitch]): @pytest.mark.usefixtures("valid_config") -def test_turn_on(hass: HomeAssistant, hass_devices: list[vultr.VultrSwitch]): +def test_turn_on(hass: HomeAssistant, hass_devices: list[vultr.VultrSwitch]) -> None: """Test turning a subscription on.""" with ( patch( @@ -116,7 +116,7 @@ def test_turn_on(hass: HomeAssistant, hass_devices: list[vultr.VultrSwitch]): @pytest.mark.usefixtures("valid_config") -def test_turn_off(hass: HomeAssistant, hass_devices: list[vultr.VultrSwitch]): +def test_turn_off(hass: HomeAssistant, hass_devices: list[vultr.VultrSwitch]) -> None: """Test turning a subscription off.""" with ( patch( diff --git a/tests/components/wemo/test_coordinator.py b/tests/components/wemo/test_coordinator.py index 2ef096d2228..198b132bbd0 100644 --- a/tests/components/wemo/test_coordinator.py +++ b/tests/components/wemo/test_coordinator.py @@ -191,8 +191,8 @@ async def test_dli_device_info( async def test_options_enable_subscription_false( - hass, pywemo_registry, pywemo_device, wemo_entity -): + hass: HomeAssistant, pywemo_registry, pywemo_device, wemo_entity +) -> None: """Test setting Options.enable_subscription = False.""" config_entry = hass.config_entries.async_get_entry(wemo_entity.config_entry_id) assert hass.config_entries.async_update_entry( @@ -203,7 +203,9 @@ async def test_options_enable_subscription_false( pywemo_registry.unregister.assert_called_once_with(pywemo_device) -async def test_options_enable_long_press_false(hass, pywemo_device, wemo_entity): +async def test_options_enable_long_press_false( + hass: HomeAssistant, pywemo_device, wemo_entity +) -> None: """Test setting Options.enable_long_press = False.""" config_entry = hass.config_entries.async_get_entry(wemo_entity.config_entry_id) assert hass.config_entries.async_update_entry( diff --git a/tests/components/zha/test_base.py b/tests/components/zha/test_base.py index e9c5a0a8e9c..ee5293d16b9 100644 --- a/tests/components/zha/test_base.py +++ b/tests/components/zha/test_base.py @@ -9,11 +9,11 @@ from tests.components.zha.test_cluster_handlers import ( # noqa: F401 ) -def test_parse_and_log_command(poll_control_ch): # noqa: F811 +def test_parse_and_log_command(poll_control_ch) -> None: # noqa: F811 """Test that `parse_and_log_command` correctly parses a known command.""" assert parse_and_log_command(poll_control_ch, 0x00, 0x01, []) == "fast_poll_stop" -def test_parse_and_log_command_unknown(poll_control_ch): # noqa: F811 +def test_parse_and_log_command_unknown(poll_control_ch) -> None: # noqa: F811 """Test that `parse_and_log_command` correctly parses an unknown command.""" assert parse_and_log_command(poll_control_ch, 0x00, 0xAB, []) == "0xAB" diff --git a/tests/components/zha/test_gateway.py b/tests/components/zha/test_gateway.py index 666594bd854..3a576ed6e55 100644 --- a/tests/components/zha/test_gateway.py +++ b/tests/components/zha/test_gateway.py @@ -300,7 +300,7 @@ async def test_single_reload_on_multiple_connection_loss( hass: HomeAssistant, zigpy_app_controller: ControllerApplication, config_entry: MockConfigEntry, -): +) -> None: """Test that we only reload once when we lose the connection multiple times.""" config_entry.add_to_hass(hass) @@ -333,7 +333,7 @@ async def test_startup_concurrency_limit( zigpy_app_controller: ControllerApplication, config_entry: MockConfigEntry, zigpy_device_mock, -): +) -> None: """Test ZHA gateway limits concurrency on startup.""" config_entry.add_to_hass(hass) zha_gateway = ZHAGateway(hass, {}, config_entry) From e37ee6843441544b2b0a5ac80f25b6e0505dc12c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Jun 2024 03:20:39 -0500 Subject: [PATCH 0276/1445] Bump cryptography to 42.0.8 (#118889) --- homeassistant/package_constraints.txt | 2 +- pyproject.toml | 2 +- requirements.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index c4f22de09aa..5fce2838b1d 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -23,7 +23,7 @@ bluetooth-data-tools==1.19.0 cached_ipaddress==0.3.0 certifi>=2021.5.30 ciso8601==2.3.1 -cryptography==42.0.5 +cryptography==42.0.8 dbus-fast==2.21.3 fnv-hash-fast==0.5.0 ha-av==10.1.1 diff --git a/pyproject.toml b/pyproject.toml index daf13dc537b..9fd47c71346 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -50,7 +50,7 @@ dependencies = [ "lru-dict==1.3.0", "PyJWT==2.8.0", # PyJWT has loose dependency. We want the latest one. - "cryptography==42.0.5", + "cryptography==42.0.8", "Pillow==10.3.0", "pyOpenSSL==24.1.0", "orjson==3.9.15", diff --git a/requirements.txt b/requirements.txt index 3db2624655d..ebb78cdf9d1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -25,7 +25,7 @@ ifaddr==0.2.0 Jinja2==3.1.4 lru-dict==1.3.0 PyJWT==2.8.0 -cryptography==42.0.5 +cryptography==42.0.8 Pillow==10.3.0 pyOpenSSL==24.1.0 orjson==3.9.15 From 093e85d59acc78e18afcb6b49bbfbebebd6bf11e Mon Sep 17 00:00:00 2001 From: "Mr. Bubbles" Date: Thu, 6 Jun 2024 10:43:12 +0200 Subject: [PATCH 0277/1445] Fix some minor typos in ista EcoTrend integration (#118949) Fix typos --- homeassistant/components/ista_ecotrend/__init__.py | 2 +- homeassistant/components/ista_ecotrend/config_flow.py | 4 ++-- homeassistant/components/ista_ecotrend/manifest.json | 2 +- homeassistant/components/ista_ecotrend/strings.json | 2 +- homeassistant/generated/integrations.json | 2 +- tests/components/ista_ecotrend/__init__.py | 2 +- tests/components/ista_ecotrend/conftest.py | 2 +- tests/components/ista_ecotrend/test_config_flow.py | 2 +- tests/components/ista_ecotrend/test_init.py | 2 +- 9 files changed, 10 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/ista_ecotrend/__init__.py b/homeassistant/components/ista_ecotrend/__init__.py index 2bb41dd6f8b..e1be000ccc4 100644 --- a/homeassistant/components/ista_ecotrend/__init__.py +++ b/homeassistant/components/ista_ecotrend/__init__.py @@ -29,7 +29,7 @@ type IstaConfigEntry = ConfigEntry[IstaCoordinator] async def async_setup_entry(hass: HomeAssistant, entry: IstaConfigEntry) -> bool: - """Set up ista Ecotrend from a config entry.""" + """Set up ista EcoTrend from a config entry.""" ista = PyEcotrendIsta( entry.data[CONF_EMAIL], entry.data[CONF_PASSWORD], diff --git a/homeassistant/components/ista_ecotrend/config_flow.py b/homeassistant/components/ista_ecotrend/config_flow.py index b58da0f3a56..0bf1685eff4 100644 --- a/homeassistant/components/ista_ecotrend/config_flow.py +++ b/homeassistant/components/ista_ecotrend/config_flow.py @@ -1,4 +1,4 @@ -"""Config flow for ista Ecotrend integration.""" +"""Config flow for ista EcoTrend integration.""" from __future__ import annotations @@ -45,7 +45,7 @@ STEP_USER_DATA_SCHEMA = vol.Schema( class IstaConfigFlow(ConfigFlow, domain=DOMAIN): - """Handle a config flow for ista Ecotrend.""" + """Handle a config flow for ista EcoTrend.""" async def async_step_user( self, user_input: dict[str, Any] | None = None diff --git a/homeassistant/components/ista_ecotrend/manifest.json b/homeassistant/components/ista_ecotrend/manifest.json index 988f278a1c9..497d3d4a984 100644 --- a/homeassistant/components/ista_ecotrend/manifest.json +++ b/homeassistant/components/ista_ecotrend/manifest.json @@ -1,6 +1,6 @@ { "domain": "ista_ecotrend", - "name": "ista Ecotrend", + "name": "ista EcoTrend", "codeowners": ["@tr4nt0r"], "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ista_ecotrend", diff --git a/homeassistant/components/ista_ecotrend/strings.json b/homeassistant/components/ista_ecotrend/strings.json index fa8fcc28c20..af976e89e09 100644 --- a/homeassistant/components/ista_ecotrend/strings.json +++ b/homeassistant/components/ista_ecotrend/strings.json @@ -50,7 +50,7 @@ "message": "Authentication failed for {email}, check your login credentials" }, "connection_exception": { - "message": "Unable to connect and retrieve data from ista EcoTrends, try again later" + "message": "Unable to connect and retrieve data from ista EcoTrend, try again later" } } } diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index cc949dec3c4..0665ba30351 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -2923,7 +2923,7 @@ "iot_class": "cloud_polling" }, "ista_ecotrend": { - "name": "ista Ecotrend", + "name": "ista EcoTrend", "integration_type": "hub", "config_flow": true, "iot_class": "cloud_polling" diff --git a/tests/components/ista_ecotrend/__init__.py b/tests/components/ista_ecotrend/__init__.py index d636c2a399c..93426111a06 100644 --- a/tests/components/ista_ecotrend/__init__.py +++ b/tests/components/ista_ecotrend/__init__.py @@ -1 +1 @@ -"""Tests for the ista Ecotrend integration.""" +"""Tests for the ista EcoTrend integration.""" diff --git a/tests/components/ista_ecotrend/conftest.py b/tests/components/ista_ecotrend/conftest.py index 786be230c05..097ed07ff10 100644 --- a/tests/components/ista_ecotrend/conftest.py +++ b/tests/components/ista_ecotrend/conftest.py @@ -1,4 +1,4 @@ -"""Common fixtures for the ista Ecotrend tests.""" +"""Common fixtures for the ista EcoTrend tests.""" from collections.abc import Generator from typing import Any diff --git a/tests/components/ista_ecotrend/test_config_flow.py b/tests/components/ista_ecotrend/test_config_flow.py index 3ff192c85ac..6dfa692841a 100644 --- a/tests/components/ista_ecotrend/test_config_flow.py +++ b/tests/components/ista_ecotrend/test_config_flow.py @@ -1,4 +1,4 @@ -"""Test the ista Ecotrend config flow.""" +"""Test the ista EcoTrend config flow.""" from unittest.mock import AsyncMock, MagicMock diff --git a/tests/components/ista_ecotrend/test_init.py b/tests/components/ista_ecotrend/test_init.py index 11a770d9ec7..c7faa2171d6 100644 --- a/tests/components/ista_ecotrend/test_init.py +++ b/tests/components/ista_ecotrend/test_init.py @@ -1,4 +1,4 @@ -"""Test the ista Ecotrend init.""" +"""Test the ista EcoTrend init.""" from unittest.mock import MagicMock From 7eda8aafc80690abbd9e4a8cf059041ce964faf0 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 6 Jun 2024 10:43:31 +0200 Subject: [PATCH 0278/1445] Ignore nested functions when enforcing type hints in tests (#118948) --- pylint/plugins/hass_enforce_type_hints.py | 8 ++++---- tests/pylint/test_enforce_type_hints.py | 22 ++++++++++++++++++++++ 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index 3c6139a41e7..0adebaf98f6 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -3138,15 +3138,15 @@ class HassTypeHintChecker(BaseChecker): _class_matchers: list[ClassTypeHintMatch] _function_matchers: list[TypeHintMatch] - _module_name: str + _module_node: nodes.Module _in_test_module: bool def visit_module(self, node: nodes.Module) -> None: """Populate matchers for a Module node.""" self._class_matchers = [] self._function_matchers = [] - self._module_name = node.name - self._in_test_module = self._module_name.startswith("tests.") + self._module_node = node + self._in_test_module = node.name.startswith("tests.") if ( self._in_test_module @@ -3230,7 +3230,7 @@ class HassTypeHintChecker(BaseChecker): if node.is_method(): matchers = _METHOD_MATCH else: - if self._in_test_module: + if self._in_test_module and node.parent is self._module_node: if node.name.startswith("test_"): self._check_test_function(node, False) return diff --git a/tests/pylint/test_enforce_type_hints.py b/tests/pylint/test_enforce_type_hints.py index 9f0f4905dab..5b1c494568d 100644 --- a/tests/pylint/test_enforce_type_hints.py +++ b/tests/pylint/test_enforce_type_hints.py @@ -1152,6 +1152,28 @@ def test_pytest_function( type_hint_checker.visit_asyncfunctiondef(func_node) +def test_pytest_nested_function( + linter: UnittestLinter, type_hint_checker: BaseChecker +) -> None: + """Ensure valid hints are accepted for a test function.""" + func_node, nested_func_node = astroid.extract_node( + """ + async def some_function( #@ + ): + def test_value(value: str) -> bool: #@ + return value == "Yes" + return test_value + """, + "tests.components.pylint_test.notify", + ) + type_hint_checker.visit_module(func_node.parent) + + with assert_no_messages( + linter, + ): + type_hint_checker.visit_asyncfunctiondef(nested_func_node) + + def test_pytest_invalid_function( linter: UnittestLinter, type_hint_checker: BaseChecker ) -> None: From c373e36995cbb7fcafc818ff339bfeeee2597a8e Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 6 Jun 2024 10:44:02 +0200 Subject: [PATCH 0279/1445] Centralize duplicate fixtures in rainforest_raven tests (#118945) --- tests/components/rainforest_raven/__init__.py | 4 +-- tests/components/rainforest_raven/conftest.py | 33 +++++++++++++++++++ .../rainforest_raven/test_coordinator.py | 15 +-------- .../rainforest_raven/test_diagnostics.py | 24 +------------- .../components/rainforest_raven/test_init.py | 27 --------------- .../rainforest_raven/test_sensor.py | 27 --------------- 6 files changed, 37 insertions(+), 93 deletions(-) create mode 100644 tests/components/rainforest_raven/conftest.py diff --git a/tests/components/rainforest_raven/__init__.py b/tests/components/rainforest_raven/__init__.py index 0269e4cf0f4..eb3cb4efcc2 100644 --- a/tests/components/rainforest_raven/__init__.py +++ b/tests/components/rainforest_raven/__init__.py @@ -17,7 +17,7 @@ from .const import ( from tests.common import AsyncMock, MockConfigEntry -def create_mock_device(): +def create_mock_device() -> AsyncMock: """Create a mock instance of RAVEnStreamDevice.""" device = AsyncMock() @@ -33,7 +33,7 @@ def create_mock_device(): return device -def create_mock_entry(no_meters=False): +def create_mock_entry(no_meters: bool = False) -> MockConfigEntry: """Create a mock config entry for a RAVEn device.""" return MockConfigEntry( domain=DOMAIN, diff --git a/tests/components/rainforest_raven/conftest.py b/tests/components/rainforest_raven/conftest.py new file mode 100644 index 00000000000..e935dbd3692 --- /dev/null +++ b/tests/components/rainforest_raven/conftest.py @@ -0,0 +1,33 @@ +"""Fixtures for the Rainforest RAVEn tests.""" + +from collections.abc import Generator +from unittest.mock import AsyncMock, patch + +import pytest + +from homeassistant.core import HomeAssistant + +from . import create_mock_device, create_mock_entry + +from tests.common import MockConfigEntry + + +@pytest.fixture +def mock_device() -> Generator[AsyncMock, None, None]: + """Mock a functioning RAVEn device.""" + mock_device = create_mock_device() + with patch( + "homeassistant.components.rainforest_raven.coordinator.RAVEnSerialDevice", + return_value=mock_device, + ): + yield mock_device + + +@pytest.fixture +async def mock_entry(hass: HomeAssistant, mock_device: AsyncMock) -> MockConfigEntry: + """Mock a functioning RAVEn config entry.""" + mock_entry = create_mock_entry() + mock_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + return mock_entry diff --git a/tests/components/rainforest_raven/test_coordinator.py b/tests/components/rainforest_raven/test_coordinator.py index 1a5f4d3d3f7..cc0bcac3978 100644 --- a/tests/components/rainforest_raven/test_coordinator.py +++ b/tests/components/rainforest_raven/test_coordinator.py @@ -10,20 +10,7 @@ from homeassistant.components.rainforest_raven.coordinator import RAVEnDataCoord from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady -from . import create_mock_device, create_mock_entry - -from tests.common import patch - - -@pytest.fixture -def mock_device(): - """Mock a functioning RAVEn device.""" - mock_device = create_mock_device() - with patch( - "homeassistant.components.rainforest_raven.coordinator.RAVEnSerialDevice", - return_value=mock_device, - ): - yield mock_device +from . import create_mock_entry async def test_coordinator_device_info(hass: HomeAssistant, mock_device): diff --git a/tests/components/rainforest_raven/test_diagnostics.py b/tests/components/rainforest_raven/test_diagnostics.py index d8caeb32f4a..86a86032ac6 100644 --- a/tests/components/rainforest_raven/test_diagnostics.py +++ b/tests/components/rainforest_raven/test_diagnostics.py @@ -8,35 +8,13 @@ from homeassistant.components.diagnostics import REDACTED from homeassistant.const import CONF_MAC from homeassistant.core import HomeAssistant -from . import create_mock_device, create_mock_entry +from . import create_mock_entry from .const import DEMAND, NETWORK_INFO, PRICE_CLUSTER, SUMMATION -from tests.common import patch from tests.components.diagnostics import get_diagnostics_for_config_entry from tests.typing import ClientSessionGenerator -@pytest.fixture -def mock_device(): - """Mock a functioning RAVEn device.""" - mock_device = create_mock_device() - with patch( - "homeassistant.components.rainforest_raven.coordinator.RAVEnSerialDevice", - return_value=mock_device, - ): - yield mock_device - - -@pytest.fixture -async def mock_entry(hass: HomeAssistant, mock_device): - """Mock a functioning RAVEn config entry.""" - mock_entry = create_mock_entry() - mock_entry.add_to_hass(hass) - await hass.config_entries.async_setup(mock_entry.entry_id) - await hass.async_block_till_done() - return mock_entry - - @pytest.fixture async def mock_entry_no_meters(hass: HomeAssistant, mock_device): """Mock a RAVEn config entry with no meters.""" diff --git a/tests/components/rainforest_raven/test_init.py b/tests/components/rainforest_raven/test_init.py index 1cc50971e09..5214e1ca563 100644 --- a/tests/components/rainforest_raven/test_init.py +++ b/tests/components/rainforest_raven/test_init.py @@ -1,36 +1,9 @@ """Tests for the Rainforest RAVEn component initialisation.""" -import pytest - from homeassistant.components.rainforest_raven.const import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant -from . import create_mock_device, create_mock_entry - -from tests.common import patch - - -@pytest.fixture -def mock_device(): - """Mock a functioning RAVEn device.""" - mock_device = create_mock_device() - with patch( - "homeassistant.components.rainforest_raven.coordinator.RAVEnSerialDevice", - return_value=mock_device, - ): - yield mock_device - - -@pytest.fixture -async def mock_entry(hass: HomeAssistant, mock_device): - """Mock a functioning RAVEn config entry.""" - mock_entry = create_mock_entry() - mock_entry.add_to_hass(hass) - await hass.config_entries.async_setup(mock_entry.entry_id) - await hass.async_block_till_done() - return mock_entry - async def test_load_unload_entry(hass: HomeAssistant, mock_entry): """Test load and unload.""" diff --git a/tests/components/rainforest_raven/test_sensor.py b/tests/components/rainforest_raven/test_sensor.py index 36e6572149f..3259d8d7f2f 100644 --- a/tests/components/rainforest_raven/test_sensor.py +++ b/tests/components/rainforest_raven/test_sensor.py @@ -1,34 +1,7 @@ """Tests for the Rainforest RAVEn sensors.""" -import pytest - from homeassistant.core import HomeAssistant -from . import create_mock_device, create_mock_entry - -from tests.common import patch - - -@pytest.fixture -def mock_device(): - """Mock a functioning RAVEn device.""" - mock_device = create_mock_device() - with patch( - "homeassistant.components.rainforest_raven.coordinator.RAVEnSerialDevice", - return_value=mock_device, - ): - yield mock_device - - -@pytest.fixture -async def mock_entry(hass: HomeAssistant, mock_device): - """Mock a functioning RAVEn config entry.""" - mock_entry = create_mock_entry() - mock_entry.add_to_hass(hass) - await hass.config_entries.async_setup(mock_entry.entry_id) - await hass.async_block_till_done() - return mock_entry - async def test_sensors(hass: HomeAssistant, mock_device, mock_entry): """Test the sensors.""" From 121bfc9766f5d44ea551d90cd136570a6345de7b Mon Sep 17 00:00:00 2001 From: Sid <27780930+autinerd@users.noreply.github.com> Date: Thu, 6 Jun 2024 12:05:35 +0200 Subject: [PATCH 0280/1445] Bump ruff to 0.4.8 (#118894) --- .pre-commit-config.yaml | 2 +- pyproject.toml | 2 +- requirements_test_pre_commit.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 57ab5e702b5..1d47ba2b3f1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.4.7 + rev: v0.4.8 hooks: - id: ruff args: diff --git a/pyproject.toml b/pyproject.toml index 9fd47c71346..58ce5128ad6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -669,7 +669,7 @@ filterwarnings = [ ] [tool.ruff] -required-version = ">=0.4.7" +required-version = ">=0.4.8" [tool.ruff.lint] select = [ diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index e465849f02a..94758f58e32 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -1,5 +1,5 @@ # Automatically generated from .pre-commit-config.yaml by gen_requirements_all.py, do not edit codespell==2.3.0 -ruff==0.4.7 +ruff==0.4.8 yamllint==1.35.1 From f4254208997901eeb554e3c1a3de0357dd63ab7a Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 6 Jun 2024 12:10:13 +0200 Subject: [PATCH 0281/1445] Improve type hints in rainforest_raven tests (#118950) --- .../rainforest_raven/test_config_flow.py | 59 +++++++++---------- .../rainforest_raven/test_coordinator.py | 24 ++++++-- .../components/rainforest_raven/test_init.py | 6 +- .../rainforest_raven/test_sensor.py | 5 +- 4 files changed, 56 insertions(+), 38 deletions(-) diff --git a/tests/components/rainforest_raven/test_config_flow.py b/tests/components/rainforest_raven/test_config_flow.py index d86dee6e0f6..36e03254dc5 100644 --- a/tests/components/rainforest_raven/test_config_flow.py +++ b/tests/components/rainforest_raven/test_config_flow.py @@ -1,10 +1,11 @@ """Test Rainforest RAVEn config flow.""" -from unittest.mock import patch +from collections.abc import Generator +from unittest.mock import AsyncMock, patch from aioraven.device import RAVEnConnectionError import pytest -import serial.tools.list_ports +from serial.tools.list_ports_common import ListPortInfo from homeassistant.components.rainforest_raven.const import DOMAIN from homeassistant.config_entries import SOURCE_USB, SOURCE_USER @@ -19,7 +20,7 @@ from tests.common import MockConfigEntry @pytest.fixture -def mock_device(): +def mock_device() -> Generator[AsyncMock, None, None]: """Mock a functioning RAVEn device.""" device = create_mock_device() with patch( @@ -30,7 +31,7 @@ def mock_device(): @pytest.fixture -def mock_device_no_open(mock_device): +def mock_device_no_open(mock_device: AsyncMock) -> AsyncMock: """Mock a device which fails to open.""" mock_device.__aenter__.side_effect = RAVEnConnectionError mock_device.open.side_effect = RAVEnConnectionError @@ -38,7 +39,7 @@ def mock_device_no_open(mock_device): @pytest.fixture -def mock_device_comm_error(mock_device): +def mock_device_comm_error(mock_device: AsyncMock) -> AsyncMock: """Mock a device which fails to read or parse raw data.""" mock_device.get_meter_list.side_effect = RAVEnConnectionError mock_device.get_meter_info.side_effect = RAVEnConnectionError @@ -46,7 +47,7 @@ def mock_device_comm_error(mock_device): @pytest.fixture -def mock_device_timeout(mock_device): +def mock_device_timeout(mock_device: AsyncMock) -> AsyncMock: """Mock a device which times out when queried.""" mock_device.get_meter_list.side_effect = TimeoutError mock_device.get_meter_info.side_effect = TimeoutError @@ -54,9 +55,9 @@ def mock_device_timeout(mock_device): @pytest.fixture -def mock_comports(): +def mock_comports() -> Generator[list[ListPortInfo], None, None]: """Mock serial port list.""" - port = serial.tools.list_ports_common.ListPortInfo(DISCOVERY_INFO.device) + port = ListPortInfo(DISCOVERY_INFO.device) port.serial_number = DISCOVERY_INFO.serial_number port.manufacturer = DISCOVERY_INFO.manufacturer port.device = DISCOVERY_INFO.device @@ -68,7 +69,8 @@ def mock_comports(): yield comports -async def test_flow_usb(hass: HomeAssistant, mock_comports, mock_device): +@pytest.mark.usefixtures("mock_comports", "mock_device") +async def test_flow_usb(hass: HomeAssistant) -> None: """Test usb flow connection.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_USB}, data=DISCOVERY_INFO @@ -86,9 +88,8 @@ async def test_flow_usb(hass: HomeAssistant, mock_comports, mock_device): assert result.get("type") is FlowResultType.CREATE_ENTRY -async def test_flow_usb_cannot_connect( - hass: HomeAssistant, mock_comports, mock_device_no_open -): +@pytest.mark.usefixtures("mock_comports", "mock_device_no_open") +async def test_flow_usb_cannot_connect(hass: HomeAssistant) -> None: """Test usb flow connection error.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_USB}, data=DISCOVERY_INFO @@ -98,9 +99,8 @@ async def test_flow_usb_cannot_connect( assert result.get("reason") == "cannot_connect" -async def test_flow_usb_timeout_connect( - hass: HomeAssistant, mock_comports, mock_device_timeout -): +@pytest.mark.usefixtures("mock_comports", "mock_device_timeout") +async def test_flow_usb_timeout_connect(hass: HomeAssistant) -> None: """Test usb flow connection timeout.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_USB}, data=DISCOVERY_INFO @@ -110,9 +110,8 @@ async def test_flow_usb_timeout_connect( assert result.get("reason") == "timeout_connect" -async def test_flow_usb_comm_error( - hass: HomeAssistant, mock_comports, mock_device_comm_error -): +@pytest.mark.usefixtures("mock_comports", "mock_device_comm_error") +async def test_flow_usb_comm_error(hass: HomeAssistant) -> None: """Test usb flow connection failure to communicate.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_USB}, data=DISCOVERY_INFO @@ -122,7 +121,8 @@ async def test_flow_usb_comm_error( assert result.get("reason") == "cannot_connect" -async def test_flow_user(hass: HomeAssistant, mock_comports, mock_device): +@pytest.mark.usefixtures("mock_comports", "mock_device") +async def test_flow_user(hass: HomeAssistant) -> None: """Test user flow connection.""" result = await hass.config_entries.flow.async_init( DOMAIN, @@ -153,7 +153,8 @@ async def test_flow_user(hass: HomeAssistant, mock_comports, mock_device): assert result.get("type") is FlowResultType.CREATE_ENTRY -async def test_flow_user_no_available_devices(hass: HomeAssistant, mock_comports): +@pytest.mark.usefixtures("mock_comports") +async def test_flow_user_no_available_devices(hass: HomeAssistant) -> None: """Test user flow with no available devices.""" entry = MockConfigEntry( domain=DOMAIN, @@ -169,7 +170,8 @@ async def test_flow_user_no_available_devices(hass: HomeAssistant, mock_comports assert result.get("reason") == "no_devices_found" -async def test_flow_user_in_progress(hass: HomeAssistant, mock_comports): +@pytest.mark.usefixtures("mock_comports") +async def test_flow_user_in_progress(hass: HomeAssistant) -> None: """Test user flow with no available devices.""" result = await hass.config_entries.flow.async_init( DOMAIN, @@ -190,9 +192,8 @@ async def test_flow_user_in_progress(hass: HomeAssistant, mock_comports): assert result.get("reason") == "already_in_progress" -async def test_flow_user_cannot_connect( - hass: HomeAssistant, mock_comports, mock_device_no_open -): +@pytest.mark.usefixtures("mock_comports", "mock_device_no_open") +async def test_flow_user_cannot_connect(hass: HomeAssistant) -> None: """Test user flow connection failure to communicate.""" result = await hass.config_entries.flow.async_init( DOMAIN, @@ -206,9 +207,8 @@ async def test_flow_user_cannot_connect( assert result.get("errors") == {CONF_DEVICE: "cannot_connect"} -async def test_flow_user_timeout_connect( - hass: HomeAssistant, mock_comports, mock_device_timeout -): +@pytest.mark.usefixtures("mock_comports", "mock_device_timeout") +async def test_flow_user_timeout_connect(hass: HomeAssistant) -> None: """Test user flow connection failure to communicate.""" result = await hass.config_entries.flow.async_init( DOMAIN, @@ -222,9 +222,8 @@ async def test_flow_user_timeout_connect( assert result.get("errors") == {CONF_DEVICE: "timeout_connect"} -async def test_flow_user_comm_error( - hass: HomeAssistant, mock_comports, mock_device_comm_error -): +@pytest.mark.usefixtures("mock_comports", "mock_device_comm_error") +async def test_flow_user_comm_error(hass: HomeAssistant) -> None: """Test user flow connection failure to communicate.""" result = await hass.config_entries.flow.async_init( DOMAIN, diff --git a/tests/components/rainforest_raven/test_coordinator.py b/tests/components/rainforest_raven/test_coordinator.py index cc0bcac3978..db70118f7b9 100644 --- a/tests/components/rainforest_raven/test_coordinator.py +++ b/tests/components/rainforest_raven/test_coordinator.py @@ -2,6 +2,7 @@ import asyncio import functools +from unittest.mock import AsyncMock from aioraven.device import RAVEnConnectionError import pytest @@ -13,7 +14,8 @@ from homeassistant.exceptions import ConfigEntryNotReady from . import create_mock_entry -async def test_coordinator_device_info(hass: HomeAssistant, mock_device): +@pytest.mark.usefixtures("mock_device") +async def test_coordinator_device_info(hass: HomeAssistant) -> None: """Test reporting device information from the coordinator.""" entry = create_mock_entry() coordinator = RAVEnDataCoordinator(hass, entry) @@ -37,7 +39,9 @@ async def test_coordinator_device_info(hass: HomeAssistant, mock_device): assert coordinator.device_name == "RAVEn Device" -async def test_coordinator_cache_device(hass: HomeAssistant, mock_device): +async def test_coordinator_cache_device( + hass: HomeAssistant, mock_device: AsyncMock +) -> None: """Test that the device isn't re-opened for subsequent refreshes.""" entry = create_mock_entry() coordinator = RAVEnDataCoordinator(hass, entry) @@ -51,7 +55,9 @@ async def test_coordinator_cache_device(hass: HomeAssistant, mock_device): assert mock_device.open.call_count == 1 -async def test_coordinator_device_error_setup(hass: HomeAssistant, mock_device): +async def test_coordinator_device_error_setup( + hass: HomeAssistant, mock_device: AsyncMock +) -> None: """Test handling of a device error during initialization.""" entry = create_mock_entry() coordinator = RAVEnDataCoordinator(hass, entry) @@ -61,7 +67,9 @@ async def test_coordinator_device_error_setup(hass: HomeAssistant, mock_device): await coordinator.async_config_entry_first_refresh() -async def test_coordinator_device_error_update(hass: HomeAssistant, mock_device): +async def test_coordinator_device_error_update( + hass: HomeAssistant, mock_device: AsyncMock +) -> None: """Test handling of a device error during an update.""" entry = create_mock_entry() coordinator = RAVEnDataCoordinator(hass, entry) @@ -74,7 +82,9 @@ async def test_coordinator_device_error_update(hass: HomeAssistant, mock_device) assert coordinator.last_update_success is False -async def test_coordinator_device_timeout_update(hass: HomeAssistant, mock_device): +async def test_coordinator_device_timeout_update( + hass: HomeAssistant, mock_device: AsyncMock +) -> None: """Test handling of a device timeout during an update.""" entry = create_mock_entry() coordinator = RAVEnDataCoordinator(hass, entry) @@ -87,7 +97,9 @@ async def test_coordinator_device_timeout_update(hass: HomeAssistant, mock_devic assert coordinator.last_update_success is False -async def test_coordinator_comm_error(hass: HomeAssistant, mock_device): +async def test_coordinator_comm_error( + hass: HomeAssistant, mock_device: AsyncMock +) -> None: """Test handling of an error parsing or reading raw device data.""" entry = create_mock_entry() coordinator = RAVEnDataCoordinator(hass, entry) diff --git a/tests/components/rainforest_raven/test_init.py b/tests/components/rainforest_raven/test_init.py index 5214e1ca563..974c45150a6 100644 --- a/tests/components/rainforest_raven/test_init.py +++ b/tests/components/rainforest_raven/test_init.py @@ -4,8 +4,12 @@ from homeassistant.components.rainforest_raven.const import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant +from tests.common import MockConfigEntry -async def test_load_unload_entry(hass: HomeAssistant, mock_entry): + +async def test_load_unload_entry( + hass: HomeAssistant, mock_entry: MockConfigEntry +) -> None: """Test load and unload.""" assert len(hass.config_entries.async_entries(DOMAIN)) == 1 assert mock_entry.state is ConfigEntryState.LOADED diff --git a/tests/components/rainforest_raven/test_sensor.py b/tests/components/rainforest_raven/test_sensor.py index 3259d8d7f2f..3b859621cb4 100644 --- a/tests/components/rainforest_raven/test_sensor.py +++ b/tests/components/rainforest_raven/test_sensor.py @@ -1,9 +1,12 @@ """Tests for the Rainforest RAVEn sensors.""" +import pytest + from homeassistant.core import HomeAssistant -async def test_sensors(hass: HomeAssistant, mock_device, mock_entry): +@pytest.mark.usefixtures("mock_entry") +async def test_sensors(hass: HomeAssistant) -> None: """Test the sensors.""" assert len(hass.states.async_all()) == 5 From 6e8d6f599419f6cea2c9b5531743fbcbb8582d23 Mon Sep 17 00:00:00 2001 From: "Mr. Bubbles" Date: Thu, 6 Jun 2024 12:15:13 +0200 Subject: [PATCH 0282/1445] Load fixture with decorator to avoid variable not accessed error (#118954) Use fixture decorator --- tests/components/ista_ecotrend/test_config_flow.py | 5 ++--- tests/components/ista_ecotrend/test_init.py | 5 +++-- tests/components/ista_ecotrend/test_sensor.py | 5 +---- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/tests/components/ista_ecotrend/test_config_flow.py b/tests/components/ista_ecotrend/test_config_flow.py index 6dfa692841a..e465d85e517 100644 --- a/tests/components/ista_ecotrend/test_config_flow.py +++ b/tests/components/ista_ecotrend/test_config_flow.py @@ -12,9 +12,8 @@ from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType -async def test_form( - hass: HomeAssistant, mock_setup_entry: AsyncMock, mock_ista: MagicMock -) -> None: +@pytest.mark.usefixtures("mock_ista") +async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: """Test we get the form.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} diff --git a/tests/components/ista_ecotrend/test_init.py b/tests/components/ista_ecotrend/test_init.py index c7faa2171d6..13b17333bbe 100644 --- a/tests/components/ista_ecotrend/test_init.py +++ b/tests/components/ista_ecotrend/test_init.py @@ -19,8 +19,9 @@ from homeassistant.helpers import device_registry as dr from tests.common import MockConfigEntry +@pytest.mark.usefixtures("mock_ista") async def test_entry_setup_unload( - hass: HomeAssistant, ista_config_entry: MockConfigEntry, mock_ista: MagicMock + hass: HomeAssistant, ista_config_entry: MockConfigEntry ) -> None: """Test integration setup and unload.""" @@ -79,10 +80,10 @@ async def test_config_entry_error( assert ista_config_entry.state is ConfigEntryState.SETUP_ERROR +@pytest.mark.usefixtures("mock_ista") async def test_device_registry( hass: HomeAssistant, ista_config_entry: MockConfigEntry, - mock_ista: MagicMock, device_registry: dr.DeviceRegistry, snapshot: SnapshotAssertion, ) -> None: diff --git a/tests/components/ista_ecotrend/test_sensor.py b/tests/components/ista_ecotrend/test_sensor.py index ca109455885..82a15872b59 100644 --- a/tests/components/ista_ecotrend/test_sensor.py +++ b/tests/components/ista_ecotrend/test_sensor.py @@ -1,7 +1,5 @@ """Tests for the ista EcoTrend Sensors.""" -from unittest.mock import MagicMock - import pytest from syrupy.assertion import SnapshotAssertion @@ -12,11 +10,10 @@ from homeassistant.helpers import entity_registry as er from tests.common import MockConfigEntry, snapshot_platform -@pytest.mark.usefixtures("entity_registry_enabled_by_default") +@pytest.mark.usefixtures("mock_ista", "entity_registry_enabled_by_default") async def test_setup( hass: HomeAssistant, ista_config_entry: MockConfigEntry, - mock_ista: MagicMock, entity_registry: er.EntityRegistry, snapshot: SnapshotAssertion, ) -> None: From 492b1588186174afe0dd8169e63229b44b1f33d4 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 6 Jun 2024 12:17:08 +0200 Subject: [PATCH 0283/1445] Add return type hints in tests (a-i) (#118939) --- tests/components/airthings_ble/test_sensor.py | 8 ++++---- .../alarm_control_panel/test_device_trigger.py | 2 +- tests/components/anova/test_sensor.py | 4 +++- tests/components/blebox/test_helpers.py | 4 ++-- tests/components/button/test_device_action.py | 2 +- tests/components/button/test_device_trigger.py | 4 ++-- tests/components/cloud/test_repairs.py | 17 +++++++++-------- tests/components/cloudflare/test_helpers.py | 2 +- tests/components/co2signal/test_sensor.py | 2 +- tests/components/device_tracker/test_legacy.py | 2 +- .../components/dsmr_reader/test_definitions.py | 10 ++++------ tests/components/duotecno/test_config_flow.py | 4 +++- tests/components/dynalite/test_config_flow.py | 4 ++-- .../components/electrasmart/test_config_flow.py | 12 ++++++------ tests/components/elmax/test_config_flow.py | 12 ++++++------ tests/components/enigma2/test_config_flow.py | 2 +- tests/components/epson/test_media_player.py | 5 ++--- tests/components/ffmpeg/test_binary_sensor.py | 12 ++++++++---- tests/components/ffmpeg/test_init.py | 4 ++-- tests/components/frontend/test_init.py | 5 ++--- .../google_assistant/test_data_redaction.py | 2 +- tests/components/google_assistant/test_trait.py | 4 ++-- .../test_silabs_multiprotocol_addon.py | 11 ++++------- .../homeassistant_sky_connect/test_const.py | 2 +- .../components/homekit_controller/test_utils.py | 2 +- tests/components/html5/test_notify.py | 14 +++++++------- tests/components/huawei_lte/test_config_flow.py | 2 +- tests/components/ipma/test_sensor.py | 6 ++++-- 28 files changed, 82 insertions(+), 78 deletions(-) diff --git a/tests/components/airthings_ble/test_sensor.py b/tests/components/airthings_ble/test_sensor.py index 9949528ccc7..abbc373ab2e 100644 --- a/tests/components/airthings_ble/test_sensor.py +++ b/tests/components/airthings_ble/test_sensor.py @@ -30,7 +30,7 @@ async def test_migration_from_v1_to_v3_unique_id( hass: HomeAssistant, entity_registry: er.EntityRegistry, device_registry: dr.DeviceRegistry, -): +) -> None: """Verify that we can migrate from v1 (pre 2023.9.0) to the latest unique id format.""" entry = create_entry(hass) device = create_device(entry, device_registry) @@ -71,7 +71,7 @@ async def test_migration_from_v2_to_v3_unique_id( hass: HomeAssistant, entity_registry: er.EntityRegistry, device_registry: dr.DeviceRegistry, -): +) -> None: """Verify that we can migrate from v2 (introduced in 2023.9.0) to the latest unique id format.""" entry = create_entry(hass) device = create_device(entry, device_registry) @@ -112,7 +112,7 @@ async def test_migration_from_v1_and_v2_to_v3_unique_id( hass: HomeAssistant, entity_registry: er.EntityRegistry, device_registry: dr.DeviceRegistry, -): +) -> None: """Test if migration works when we have both v1 (pre 2023.9.0) and v2 (introduced in 2023.9.0) unique ids.""" entry = create_entry(hass) device = create_device(entry, device_registry) @@ -162,7 +162,7 @@ async def test_migration_with_all_unique_ids( hass: HomeAssistant, entity_registry: er.EntityRegistry, device_registry: dr.DeviceRegistry, -): +) -> None: """Test if migration works when we have all unique ids.""" entry = create_entry(hass) device = create_device(entry, device_registry) diff --git a/tests/components/alarm_control_panel/test_device_trigger.py b/tests/components/alarm_control_panel/test_device_trigger.py index fb2d4e0a504..ff77cb7c264 100644 --- a/tests/components/alarm_control_panel/test_device_trigger.py +++ b/tests/components/alarm_control_panel/test_device_trigger.py @@ -251,7 +251,7 @@ async def test_if_fires_on_state_change( device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, calls: list[ServiceCall], -): +) -> None: """Test for turn_on and turn_off triggers firing.""" config_entry = MockConfigEntry(domain="test", data={}) config_entry.add_to_hass(hass) diff --git a/tests/components/anova/test_sensor.py b/tests/components/anova/test_sensor.py index a60f87c56a0..459af55e2c4 100644 --- a/tests/components/anova/test_sensor.py +++ b/tests/components/anova/test_sensor.py @@ -3,6 +3,7 @@ import logging from anova_wifi import AnovaApi +import pytest from homeassistant.core import HomeAssistant @@ -40,7 +41,8 @@ async def test_sensors(hass: HomeAssistant, anova_api: AnovaApi) -> None: ) -async def test_no_data_sensors(hass: HomeAssistant, anova_api_no_data: AnovaApi): +@pytest.mark.usefixtures("anova_api_no_data") +async def test_no_data_sensors(hass: HomeAssistant) -> None: """Test that if we have no data for the device, and we have not set it up previously, It is not immediately set up.""" await async_init_integration(hass) assert hass.states.get("sensor.anova_precision_cooker_triac_temperature") is None diff --git a/tests/components/blebox/test_helpers.py b/tests/components/blebox/test_helpers.py index bf355612f14..2acfb8d3b36 100644 --- a/tests/components/blebox/test_helpers.py +++ b/tests/components/blebox/test_helpers.py @@ -6,13 +6,13 @@ from homeassistant.components.blebox.helpers import get_maybe_authenticated_sess from homeassistant.core import HomeAssistant -async def test_get_maybe_authenticated_session_none(hass: HomeAssistant): +async def test_get_maybe_authenticated_session_none(hass: HomeAssistant) -> None: """Tests if session auth is None.""" session = get_maybe_authenticated_session(hass=hass, username="", password="") assert session.auth is None -async def test_get_maybe_authenticated_session_auth(hass: HomeAssistant): +async def test_get_maybe_authenticated_session_auth(hass: HomeAssistant) -> None: """Tests if session have BasicAuth.""" session = get_maybe_authenticated_session( hass=hass, username="user", password="password" diff --git a/tests/components/button/test_device_action.py b/tests/components/button/test_device_action.py index f0d34e25e37..c3ba03b60e6 100644 --- a/tests/components/button/test_device_action.py +++ b/tests/components/button/test_device_action.py @@ -63,7 +63,7 @@ async def test_get_actions_hidden_auxiliary( entity_registry: er.EntityRegistry, hidden_by: er.RegistryEntryHider | None, entity_category: EntityCategory | None, -): +) -> None: """Test we get the expected actions from a hidden or auxiliary entity.""" config_entry = MockConfigEntry(domain="test", data={}) config_entry.add_to_hass(hass) diff --git a/tests/components/button/test_device_trigger.py b/tests/components/button/test_device_trigger.py index 9819c226e3f..1d9a84b0e8f 100644 --- a/tests/components/button/test_device_trigger.py +++ b/tests/components/button/test_device_trigger.py @@ -67,12 +67,12 @@ async def test_get_triggers( ], ) async def test_get_triggers_hidden_auxiliary( - hass, + hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, hidden_by: er.RegistryEntryHider | None, entity_category: EntityCategory | None, -): +) -> None: """Test we get the expected triggers from a hidden or auxiliary entity.""" config_entry = MockConfigEntry(domain="test", data={}) config_entry.add_to_hass(hass) diff --git a/tests/components/cloud/test_repairs.py b/tests/components/cloud/test_repairs.py index abfc917016d..7ca20d84bce 100644 --- a/tests/components/cloud/test_repairs.py +++ b/tests/components/cloud/test_repairs.py @@ -1,9 +1,10 @@ """Test cloud repairs.""" -from collections.abc import Generator from datetime import timedelta from http import HTTPStatus -from unittest.mock import AsyncMock, patch +from unittest.mock import patch + +import pytest from homeassistant.components.cloud import DOMAIN import homeassistant.components.cloud.repairs as cloud_repairs @@ -36,12 +37,12 @@ async def test_do_not_create_repair_issues_at_startup_if_not_logged_in( ) +@pytest.mark.usefixtures("mock_auth") async def test_create_repair_issues_at_startup_if_logged_in( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, - mock_auth: Generator[None, AsyncMock, None], issue_registry: ir.IssueRegistry, -): +) -> None: """Test that we create repair issue at startup if we are logged in.""" aioclient_mock.get( "https://accounts.nabucasa.com/payments/subscription_info", @@ -75,13 +76,13 @@ async def test_legacy_subscription_delete_issue_if_no_longer_legacy( ) +@pytest.mark.usefixtures("mock_auth") async def test_legacy_subscription_repair_flow( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, - mock_auth: Generator[None, AsyncMock, None], hass_client: ClientSessionGenerator, issue_registry: ir.IssueRegistry, -): +) -> None: """Test desired flow of the fix flow for legacy subscription.""" aioclient_mock.get( "https://accounts.nabucasa.com/payments/subscription_info", @@ -160,13 +161,13 @@ async def test_legacy_subscription_repair_flow( ) +@pytest.mark.usefixtures("mock_auth") async def test_legacy_subscription_repair_flow_timeout( hass: HomeAssistant, hass_client: ClientSessionGenerator, - mock_auth: Generator[None, AsyncMock, None], aioclient_mock: AiohttpClientMocker, issue_registry: ir.IssueRegistry, -): +) -> None: """Test timeout flow of the fix flow for legacy subscription.""" aioclient_mock.post( "https://accounts.nabucasa.com/payments/migrate_paypal_agreement", diff --git a/tests/components/cloudflare/test_helpers.py b/tests/components/cloudflare/test_helpers.py index 2d0546882dd..0edb0bb58b8 100644 --- a/tests/components/cloudflare/test_helpers.py +++ b/tests/components/cloudflare/test_helpers.py @@ -3,7 +3,7 @@ from homeassistant.components.cloudflare.helpers import get_zone_id -def test_get_zone_id(): +def test_get_zone_id() -> None: """Test get_zone_id.""" zones = [ {"id": "1", "name": "example.com"}, diff --git a/tests/components/co2signal/test_sensor.py b/tests/components/co2signal/test_sensor.py index d3e02023142..e9f46e483d1 100644 --- a/tests/components/co2signal/test_sensor.py +++ b/tests/components/co2signal/test_sensor.py @@ -91,7 +91,7 @@ async def test_sensor_reauth_triggered( hass: HomeAssistant, freezer: FrozenDateTimeFactory, electricity_maps: AsyncMock, -): +) -> None: """Test if reauth flow is triggered.""" assert (state := hass.states.get("sensor.electricity_maps_co2_intensity")) assert state.state == "45.9862319009581" diff --git a/tests/components/device_tracker/test_legacy.py b/tests/components/device_tracker/test_legacy.py index dba069c410b..c2df3a74770 100644 --- a/tests/components/device_tracker/test_legacy.py +++ b/tests/components/device_tracker/test_legacy.py @@ -9,7 +9,7 @@ from homeassistant.util.yaml import dump from tests.common import patch_yaml_files -def test_remove_device_from_config(hass: HomeAssistant): +def test_remove_device_from_config(hass: HomeAssistant) -> None: """Test the removal of a device from a config.""" yaml_devices = { "test": { diff --git a/tests/components/dsmr_reader/test_definitions.py b/tests/components/dsmr_reader/test_definitions.py index 3aef66c85d9..2ddd8395e78 100644 --- a/tests/components/dsmr_reader/test_definitions.py +++ b/tests/components/dsmr_reader/test_definitions.py @@ -13,7 +13,6 @@ from homeassistant.const import STATE_UNKNOWN from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry, async_fire_mqtt_message -from tests.typing import MqttMockHAClient @pytest.mark.parametrize( @@ -40,10 +39,8 @@ async def test_tariff_transform(input, expected) -> None: assert tariff_transform(input) == expected -async def test_entity_tariff( - hass: HomeAssistant, - mqtt_mock: MqttMockHAClient, -): +@pytest.mark.usefixtures("mqtt_mock") +async def test_entity_tariff(hass: HomeAssistant) -> None: """Test the state attribute of DSMRReaderSensorEntityDescription when a tariff transform is needed.""" config_entry = MockConfigEntry( domain=DOMAIN, @@ -74,7 +71,8 @@ async def test_entity_tariff( assert hass.states.get(electricity_tariff).state == "low" -async def test_entity_dsmr_transform(hass: HomeAssistant, mqtt_mock: MqttMockHAClient): +@pytest.mark.usefixtures("mqtt_mock") +async def test_entity_dsmr_transform(hass: HomeAssistant) -> None: """Test the state attribute of DSMRReaderSensorEntityDescription when a dsmr transform is needed.""" config_entry = MockConfigEntry( domain=DOMAIN, diff --git a/tests/components/duotecno/test_config_flow.py b/tests/components/duotecno/test_config_flow.py index 77946babd8c..f1fb60d2f0f 100644 --- a/tests/components/duotecno/test_config_flow.py +++ b/tests/components/duotecno/test_config_flow.py @@ -55,7 +55,9 @@ async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: (Exception, "unknown"), ], ) -async def test_invalid(hass: HomeAssistant, test_side_effect, test_error): +async def test_invalid( + hass: HomeAssistant, test_side_effect: Exception, test_error: str +) -> None: """Test all side_effects on the controller.connect via parameters.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} diff --git a/tests/components/dynalite/test_config_flow.py b/tests/components/dynalite/test_config_flow.py index 33e8ea84b47..8bb47fd67e3 100644 --- a/tests/components/dynalite/test_config_flow.py +++ b/tests/components/dynalite/test_config_flow.py @@ -139,7 +139,7 @@ async def test_two_entries(hass: HomeAssistant) -> None: assert result["result"].state is ConfigEntryState.LOADED -async def test_setup_user(hass): +async def test_setup_user(hass: HomeAssistant) -> None: """Test configuration via the user flow.""" host = "3.4.5.6" port = 1234 @@ -169,7 +169,7 @@ async def test_setup_user(hass): } -async def test_setup_user_existing_host(hass): +async def test_setup_user_existing_host(hass: HomeAssistant) -> None: """Test that when we setup a host that is defined, we get an error.""" host = "3.4.5.6" MockConfigEntry( diff --git a/tests/components/electrasmart/test_config_flow.py b/tests/components/electrasmart/test_config_flow.py index cf0d1b5ab15..6b943014cbc 100644 --- a/tests/components/electrasmart/test_config_flow.py +++ b/tests/components/electrasmart/test_config_flow.py @@ -16,7 +16,7 @@ from homeassistant.data_entry_flow import FlowResultType from tests.common import load_fixture -async def test_form(hass: HomeAssistant): +async def test_form(hass: HomeAssistant) -> None: """Test user config.""" mock_generate_token = loads(load_fixture("generate_token_response.json", DOMAIN)) @@ -44,7 +44,7 @@ async def test_form(hass: HomeAssistant): assert result["step_id"] == CONF_OTP -async def test_one_time_password(hass: HomeAssistant): +async def test_one_time_password(hass: HomeAssistant) -> None: """Test one time password.""" mock_generate_token = loads(load_fixture("generate_token_response.json", DOMAIN)) @@ -76,7 +76,7 @@ async def test_one_time_password(hass: HomeAssistant): assert result["type"] is FlowResultType.CREATE_ENTRY -async def test_one_time_password_api_error(hass: HomeAssistant): +async def test_one_time_password_api_error(hass: HomeAssistant) -> None: """Test one time password.""" mock_generate_token = loads(load_fixture("generate_token_response.json", DOMAIN)) with ( @@ -102,7 +102,7 @@ async def test_one_time_password_api_error(hass: HomeAssistant): assert result["type"] is FlowResultType.FORM -async def test_cannot_connect(hass: HomeAssistant): +async def test_cannot_connect(hass: HomeAssistant) -> None: """Test cannot connect.""" with patch( @@ -120,7 +120,7 @@ async def test_cannot_connect(hass: HomeAssistant): assert result["errors"] == {"base": "cannot_connect"} -async def test_invalid_phone_number(hass: HomeAssistant): +async def test_invalid_phone_number(hass: HomeAssistant) -> None: """Test invalid phone number.""" mock_invalid_phone_number_response = loads( @@ -143,7 +143,7 @@ async def test_invalid_phone_number(hass: HomeAssistant): assert result["errors"] == {"phone_number": "invalid_phone_number"} -async def test_invalid_auth(hass: HomeAssistant): +async def test_invalid_auth(hass: HomeAssistant) -> None: """Test invalid auth.""" mock_generate_token_response = loads( diff --git a/tests/components/elmax/test_config_flow.py b/tests/components/elmax/test_config_flow.py index c00de2003c2..85e14dd0a3f 100644 --- a/tests/components/elmax/test_config_flow.py +++ b/tests/components/elmax/test_config_flow.py @@ -172,7 +172,7 @@ async def test_cloud_setup(hass: HomeAssistant) -> None: assert result["type"] is FlowResultType.CREATE_ENTRY -async def test_zeroconf_form_setup_api_not_supported(hass): +async def test_zeroconf_form_setup_api_not_supported(hass: HomeAssistant) -> None: """Test the zeroconf setup case.""" result = await hass.config_entries.flow.async_init( DOMAIN, @@ -183,7 +183,7 @@ async def test_zeroconf_form_setup_api_not_supported(hass): assert result["reason"] == "not_supported" -async def test_zeroconf_discovery(hass): +async def test_zeroconf_discovery(hass: HomeAssistant) -> None: """Test discovery of Elmax local api panel.""" result = await hass.config_entries.flow.async_init( DOMAIN, @@ -195,7 +195,7 @@ async def test_zeroconf_discovery(hass): assert result["errors"] is None -async def test_zeroconf_setup_show_form(hass): +async def test_zeroconf_setup_show_form(hass: HomeAssistant) -> None: """Test discovery shows a form when activated.""" result = await hass.config_entries.flow.async_init( DOMAIN, @@ -211,7 +211,7 @@ async def test_zeroconf_setup_show_form(hass): assert result["step_id"] == "zeroconf_setup" -async def test_zeroconf_setup(hass): +async def test_zeroconf_setup(hass: HomeAssistant) -> None: """Test the successful creation of config entry via discovery flow.""" result = await hass.config_entries.flow.async_init( DOMAIN, @@ -231,7 +231,7 @@ async def test_zeroconf_setup(hass): assert result["type"] is FlowResultType.CREATE_ENTRY -async def test_zeroconf_already_configured(hass): +async def test_zeroconf_already_configured(hass: HomeAssistant) -> None: """Ensure local discovery aborts when same panel is already added to ha.""" MockConfigEntry( domain=DOMAIN, @@ -257,7 +257,7 @@ async def test_zeroconf_already_configured(hass): assert result["reason"] == "already_configured" -async def test_zeroconf_panel_changed_ip(hass): +async def test_zeroconf_panel_changed_ip(hass: HomeAssistant) -> None: """Ensure local discovery updates the panel data when a the panel changes its IP.""" # Simulate an entry already exists for ip MOCK_DIRECT_HOST. config_entry = MockConfigEntry( diff --git a/tests/components/enigma2/test_config_flow.py b/tests/components/enigma2/test_config_flow.py index 08d8d04c3b9..b4bcb29f0ac 100644 --- a/tests/components/enigma2/test_config_flow.py +++ b/tests/components/enigma2/test_config_flow.py @@ -41,7 +41,7 @@ async def user_flow(hass: HomeAssistant) -> str: ) async def test_form_user( hass: HomeAssistant, user_flow: str, test_config: dict[str, Any] -): +) -> None: """Test a successful user initiated flow.""" with ( patch( diff --git a/tests/components/epson/test_media_player.py b/tests/components/epson/test_media_player.py index 000071054f1..e529746dcd0 100644 --- a/tests/components/epson/test_media_player.py +++ b/tests/components/epson/test_media_player.py @@ -8,7 +8,7 @@ from freezegun.api import FrozenDateTimeFactory from homeassistant.components.epson.const import DOMAIN from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant -from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers import entity_registry as er from tests.common import MockConfigEntry, async_fire_time_changed @@ -16,9 +16,8 @@ from tests.common import MockConfigEntry, async_fire_time_changed async def test_set_unique_id( hass: HomeAssistant, entity_registry: er.EntityRegistry, - device_registry: dr.DeviceRegistry, freezer: FrozenDateTimeFactory, -): +) -> None: """Test the unique id is set on runtime.""" entry = MockConfigEntry( domain=DOMAIN, diff --git a/tests/components/ffmpeg/test_binary_sensor.py b/tests/components/ffmpeg/test_binary_sensor.py index 8b1a5115f86..535ac863361 100644 --- a/tests/components/ffmpeg/test_binary_sensor.py +++ b/tests/components/ffmpeg/test_binary_sensor.py @@ -30,7 +30,7 @@ async def test_noise_setup_component(hass: HomeAssistant) -> None: @patch("haffmpeg.sensor.SensorNoise.open_sensor", side_effect=AsyncMock()) -async def test_noise_setup_component_start(mock_start, hass: HomeAssistant): +async def test_noise_setup_component_start(mock_start, hass: HomeAssistant) -> None: """Set up ffmpeg component.""" with assert_setup_component(1, "binary_sensor"): await async_setup_component(hass, "binary_sensor", CONFIG_NOISE) @@ -48,7 +48,9 @@ async def test_noise_setup_component_start(mock_start, hass: HomeAssistant): @patch("haffmpeg.sensor.SensorNoise") -async def test_noise_setup_component_start_callback(mock_ffmpeg, hass: HomeAssistant): +async def test_noise_setup_component_start_callback( + mock_ffmpeg, hass: HomeAssistant +) -> None: """Set up ffmpeg component.""" mock_ffmpeg().open_sensor.side_effect = AsyncMock() mock_ffmpeg().close = AsyncMock() @@ -86,7 +88,7 @@ async def test_motion_setup_component(hass: HomeAssistant) -> None: @patch("haffmpeg.sensor.SensorMotion.open_sensor", side_effect=AsyncMock()) -async def test_motion_setup_component_start(mock_start, hass: HomeAssistant): +async def test_motion_setup_component_start(mock_start, hass: HomeAssistant) -> None: """Set up ffmpeg component.""" with assert_setup_component(1, "binary_sensor"): await async_setup_component(hass, "binary_sensor", CONFIG_MOTION) @@ -104,7 +106,9 @@ async def test_motion_setup_component_start(mock_start, hass: HomeAssistant): @patch("haffmpeg.sensor.SensorMotion") -async def test_motion_setup_component_start_callback(mock_ffmpeg, hass: HomeAssistant): +async def test_motion_setup_component_start_callback( + mock_ffmpeg, hass: HomeAssistant +) -> None: """Set up ffmpeg component.""" mock_ffmpeg().open_sensor.side_effect = AsyncMock() mock_ffmpeg().close = AsyncMock() diff --git a/tests/components/ffmpeg/test_init.py b/tests/components/ffmpeg/test_init.py index 60d24baa302..353b8fdfcc0 100644 --- a/tests/components/ffmpeg/test_init.py +++ b/tests/components/ffmpeg/test_init.py @@ -77,7 +77,7 @@ class MockFFmpegDev(ffmpeg.FFmpegBase): self.called_entities = entity_ids -def test_setup_component(): +def test_setup_component() -> None: """Set up ffmpeg component.""" with get_test_home_assistant() as hass: with assert_setup_component(1): @@ -87,7 +87,7 @@ def test_setup_component(): hass.stop() -def test_setup_component_test_service(): +def test_setup_component_test_service() -> None: """Set up ffmpeg component test services.""" with get_test_home_assistant() as hass: with assert_setup_component(1): diff --git a/tests/components/frontend/test_init.py b/tests/components/frontend/test_init.py index 57ee04da47f..f7ef7da6d1b 100644 --- a/tests/components/frontend/test_init.py +++ b/tests/components/frontend/test_init.py @@ -405,9 +405,8 @@ async def test_missing_themes(hass: HomeAssistant, ws_client) -> None: assert msg["result"]["themes"] == {} -async def test_extra_js( - hass: HomeAssistant, mock_http_client_with_extra_js, mock_onboarded -): +@pytest.mark.usefixtures("mock_onboarded") +async def test_extra_js(hass: HomeAssistant, mock_http_client_with_extra_js) -> None: """Test that extra javascript is loaded.""" resp = await mock_http_client_with_extra_js.get("") assert resp.status == 200 diff --git a/tests/components/google_assistant/test_data_redaction.py b/tests/components/google_assistant/test_data_redaction.py index d650a223e15..9ec8393ad25 100644 --- a/tests/components/google_assistant/test_data_redaction.py +++ b/tests/components/google_assistant/test_data_redaction.py @@ -7,7 +7,7 @@ from homeassistant.components.google_assistant.data_redaction import async_redac from tests.common import load_fixture -def test_redact_msg(): +def test_redact_msg() -> None: """Test async_redact_msg.""" messages = json.loads(load_fixture("data_redaction.json", "google_assistant")) agent_user_id = "333dee20-1234-1234-1234-2225a0d70d4c" diff --git a/tests/components/google_assistant/test_trait.py b/tests/components/google_assistant/test_trait.py index 0ed4d960edc..de0b8b3da4e 100644 --- a/tests/components/google_assistant/test_trait.py +++ b/tests/components/google_assistant/test_trait.py @@ -2160,13 +2160,13 @@ async def test_fan_speed_without_percentage_step(hass: HomeAssistant) -> None: ], ) async def test_fan_speed_ordered( - hass, + hass: HomeAssistant, percentage: int, percentage_step: float, speed: str, speeds: list[list[str]], percentage_result: int, -): +) -> None: """Test FanSpeed trait speed control support for fan domain.""" assert helpers.get_google_type(fan.DOMAIN, None) is not None assert trait.FanSpeedTrait.supported( diff --git a/tests/components/homeassistant_hardware/test_silabs_multiprotocol_addon.py b/tests/components/homeassistant_hardware/test_silabs_multiprotocol_addon.py index 333e38da53b..f24d1f82fce 100644 --- a/tests/components/homeassistant_hardware/test_silabs_multiprotocol_addon.py +++ b/tests/components/homeassistant_hardware/test_silabs_multiprotocol_addon.py @@ -171,13 +171,10 @@ def get_suggested(schema, key): "homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.ADDON_STATE_POLL_INTERVAL", 0, ) -async def test_uninstall_addon_waiting( - hass: HomeAssistant, - addon_store_info, - addon_info, - install_addon, - uninstall_addon, -): +@pytest.mark.usefixtures( + "addon_store_info", "addon_info", "install_addon", "uninstall_addon" +) +async def test_uninstall_addon_waiting(hass: HomeAssistant) -> None: """Test the synchronous addon uninstall helper.""" multipan_manager = await silabs_multiprotocol_addon.get_multiprotocol_addon_manager( diff --git a/tests/components/homeassistant_sky_connect/test_const.py b/tests/components/homeassistant_sky_connect/test_const.py index 24a39270061..b439d8a8830 100644 --- a/tests/components/homeassistant_sky_connect/test_const.py +++ b/tests/components/homeassistant_sky_connect/test_const.py @@ -19,7 +19,7 @@ def test_hardware_variant( assert HardwareVariant.from_usb_product_name(usb_product_name) == expected_variant -def test_hardware_variant_invalid(): +def test_hardware_variant_invalid() -> None: """Test hardware variant parsing with an invalid product.""" with pytest.raises( ValueError, match=r"^Unknown SkyConnect product name: Some other product$" diff --git a/tests/components/homekit_controller/test_utils.py b/tests/components/homekit_controller/test_utils.py index 703cf288f63..92c7e4c5a4d 100644 --- a/tests/components/homekit_controller/test_utils.py +++ b/tests/components/homekit_controller/test_utils.py @@ -3,7 +3,7 @@ from homeassistant.components.homekit_controller.utils import unique_id_to_iids -def test_unique_id_to_iids(): +def test_unique_id_to_iids() -> None: """Check that unique_id_to_iids is safe against different invalid ids.""" assert unique_id_to_iids("pairingid_1_2_3") == (1, 2, 3) assert unique_id_to_iids("pairingid_1_2") == (1, 2, None) diff --git a/tests/components/html5/test_notify.py b/tests/components/html5/test_notify.py index ec14b38cd69..f54ec9fa8f7 100644 --- a/tests/components/html5/test_notify.py +++ b/tests/components/html5/test_notify.py @@ -83,7 +83,7 @@ async def mock_client(hass, hass_client, registrations=None): return await hass_client() -async def test_get_service_with_no_json(hass: HomeAssistant): +async def test_get_service_with_no_json(hass: HomeAssistant) -> None: """Test empty json file.""" await async_setup_component(hass, "http", {}) m = mock_open() @@ -94,7 +94,7 @@ async def test_get_service_with_no_json(hass: HomeAssistant): @patch("homeassistant.components.html5.notify.WebPusher") -async def test_dismissing_message(mock_wp, hass: HomeAssistant): +async def test_dismissing_message(mock_wp, hass: HomeAssistant) -> None: """Test dismissing message.""" await async_setup_component(hass, "http", {}) mock_wp().send().status_code = 201 @@ -123,7 +123,7 @@ async def test_dismissing_message(mock_wp, hass: HomeAssistant): @patch("homeassistant.components.html5.notify.WebPusher") -async def test_sending_message(mock_wp, hass: HomeAssistant): +async def test_sending_message(mock_wp, hass: HomeAssistant) -> None: """Test sending message.""" await async_setup_component(hass, "http", {}) mock_wp().send().status_code = 201 @@ -154,7 +154,7 @@ async def test_sending_message(mock_wp, hass: HomeAssistant): @patch("homeassistant.components.html5.notify.WebPusher") -async def test_fcm_key_include(mock_wp, hass: HomeAssistant): +async def test_fcm_key_include(mock_wp, hass: HomeAssistant) -> None: """Test if the FCM header is included.""" await async_setup_component(hass, "http", {}) mock_wp().send().status_code = 201 @@ -179,7 +179,7 @@ async def test_fcm_key_include(mock_wp, hass: HomeAssistant): @patch("homeassistant.components.html5.notify.WebPusher") -async def test_fcm_send_with_unknown_priority(mock_wp, hass: HomeAssistant): +async def test_fcm_send_with_unknown_priority(mock_wp, hass: HomeAssistant) -> None: """Test if the gcm_key is only included for GCM endpoints.""" await async_setup_component(hass, "http", {}) mock_wp().send().status_code = 201 @@ -204,7 +204,7 @@ async def test_fcm_send_with_unknown_priority(mock_wp, hass: HomeAssistant): @patch("homeassistant.components.html5.notify.WebPusher") -async def test_fcm_no_targets(mock_wp, hass: HomeAssistant): +async def test_fcm_no_targets(mock_wp, hass: HomeAssistant) -> None: """Test if the gcm_key is only included for GCM endpoints.""" await async_setup_component(hass, "http", {}) mock_wp().send().status_code = 201 @@ -229,7 +229,7 @@ async def test_fcm_no_targets(mock_wp, hass: HomeAssistant): @patch("homeassistant.components.html5.notify.WebPusher") -async def test_fcm_additional_data(mock_wp, hass: HomeAssistant): +async def test_fcm_additional_data(mock_wp, hass: HomeAssistant) -> None: """Test if the gcm_key is only included for GCM endpoints.""" await async_setup_component(hass, "http", {}) mock_wp().send().status_code = 201 diff --git a/tests/components/huawei_lte/test_config_flow.py b/tests/components/huawei_lte/test_config_flow.py index 329f06795d2..862af02963c 100644 --- a/tests/components/huawei_lte/test_config_flow.py +++ b/tests/components/huawei_lte/test_config_flow.py @@ -119,7 +119,7 @@ async def test_connection_errors( exception: Exception, errors: dict[str, str], data_patch: dict[str, Any], -): +) -> None: """Test we show user form on various errors.""" requests_mock.request(ANY, ANY, exc=exception) result = await hass.config_entries.flow.async_init( diff --git a/tests/components/ipma/test_sensor.py b/tests/components/ipma/test_sensor.py index d5f6a3ab5bb..adff8206add 100644 --- a/tests/components/ipma/test_sensor.py +++ b/tests/components/ipma/test_sensor.py @@ -2,12 +2,14 @@ from unittest.mock import patch +from homeassistant.core import HomeAssistant + from . import ENTRY_CONFIG, MockLocation from tests.common import MockConfigEntry -async def test_ipma_fire_risk_create_sensors(hass): +async def test_ipma_fire_risk_create_sensors(hass: HomeAssistant) -> None: """Test creation of fire risk sensors.""" with patch("pyipma.location.Location.get", return_value=MockLocation()): @@ -21,7 +23,7 @@ async def test_ipma_fire_risk_create_sensors(hass): assert state.state == "3" -async def test_ipma_uv_index_create_sensors(hass): +async def test_ipma_uv_index_create_sensors(hass: HomeAssistant) -> None: """Test creation of uv index sensors.""" with patch("pyipma.location.Location.get", return_value=MockLocation()): From 857b5c9c1b78e2f79ea7c5a5f516b4ef5a40cd20 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 6 Jun 2024 12:19:41 +0200 Subject: [PATCH 0284/1445] Fix type hints in google tests (#118941) --- tests/components/google/conftest.py | 10 +++++----- tests/components/google/test_calendar.py | 10 ++++------ tests/components/google/test_diagnostics.py | 8 ++++---- 3 files changed, 13 insertions(+), 15 deletions(-) diff --git a/tests/components/google/conftest.py b/tests/components/google/conftest.py index d69770a9b0b..aff60ee0b04 100644 --- a/tests/components/google/conftest.py +++ b/tests/components/google/conftest.py @@ -92,14 +92,14 @@ CLIENT_ID = "client-id" CLIENT_SECRET = "client-secret" -@pytest.fixture(name="calendar_access_role") -def test_calendar_access_role() -> str: - """Default access role to use for test_api_calendar in tests.""" +@pytest.fixture +def calendar_access_role() -> str: + """Set default access role to use for test_api_calendar in tests.""" return "owner" -@pytest.fixture -def test_api_calendar(calendar_access_role: str) -> None: +@pytest.fixture(name="test_api_calendar") +def api_calendar(calendar_access_role: str) -> dict[str, Any]: """Return a test calendar object used in API responses.""" return { **TEST_API_CALENDAR, diff --git a/tests/components/google/test_calendar.py b/tests/components/google/test_calendar.py index 4f0e399bbbb..a5c65412c15 100644 --- a/tests/components/google/test_calendar.py +++ b/tests/components/google/test_calendar.py @@ -42,9 +42,9 @@ TEST_ENTITY_NAME = TEST_API_ENTITY_NAME @pytest.fixture(autouse=True) def mock_test_setup( - test_api_calendar, - mock_calendars_list, -): + test_api_calendar: dict[str, Any], + mock_calendars_list: ApiResult, +) -> None: """Fixture that sets up the default API responses during integration setup.""" mock_calendars_list({"items": [test_api_calendar]}) @@ -447,9 +447,7 @@ async def test_http_event_api_failure( hass: HomeAssistant, hass_client: ClientSessionGenerator, component_setup, - mock_calendars_list, mock_events_list, - aioclient_mock: AiohttpClientMocker, ) -> None: """Test the Rest API response during a calendar failure.""" mock_events_list({}, exc=ClientError()) @@ -570,7 +568,7 @@ async def test_opaque_event( async def test_scan_calendar_error( hass: HomeAssistant, component_setup, - mock_calendars_list, + mock_calendars_list: ApiResult, config_entry, ) -> None: """Test that the calendar update handles a server error.""" diff --git a/tests/components/google/test_diagnostics.py b/tests/components/google/test_diagnostics.py index 32ed2ab3224..69a1929b5ed 100644 --- a/tests/components/google/test_diagnostics.py +++ b/tests/components/google/test_diagnostics.py @@ -13,7 +13,7 @@ from homeassistant.auth.models import Credentials from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component -from .conftest import TEST_EVENT, ComponentSetup +from .conftest import TEST_EVENT, ApiResult, ComponentSetup from tests.common import CLIENT_ID, MockConfigEntry, MockUser from tests.components.diagnostics import get_diagnostics_for_config_entry @@ -23,9 +23,9 @@ from tests.typing import ClientSessionGenerator @pytest.fixture(autouse=True) def mock_test_setup( - test_api_calendar, - mock_calendars_list, -): + test_api_calendar: dict[str, Any], + mock_calendars_list: ApiResult, +) -> None: """Fixture that sets up the default API responses during integration setup.""" mock_calendars_list({"items": [test_api_calendar]}) From 29952d81284cffd02d9d7ce27f9a0f60d0f1d7ac Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Thu, 6 Jun 2024 12:26:07 +0200 Subject: [PATCH 0285/1445] Bump `imgw-pib` backend library to version `1.0.2` (#118953) Bump imgw-pib to version 1.0.2 Co-authored-by: Maciej Bieniek <478555+bieniu@users.noreply.github.com> --- homeassistant/components/imgw_pib/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/imgw_pib/manifest.json b/homeassistant/components/imgw_pib/manifest.json index c6a230244ec..9a9994a73e5 100644 --- a/homeassistant/components/imgw_pib/manifest.json +++ b/homeassistant/components/imgw_pib/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/imgw_pib", "iot_class": "cloud_polling", "quality_scale": "platinum", - "requirements": ["imgw_pib==1.0.1"] + "requirements": ["imgw_pib==1.0.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 66892f06e3c..17266a8b467 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1146,7 +1146,7 @@ iglo==1.2.7 ihcsdk==2.8.5 # homeassistant.components.imgw_pib -imgw_pib==1.0.1 +imgw_pib==1.0.2 # homeassistant.components.incomfort incomfort-client==0.5.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6c026c36110..e07132b66ce 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -933,7 +933,7 @@ idasen-ha==2.5.3 ifaddr==0.2.0 # homeassistant.components.imgw_pib -imgw_pib==1.0.1 +imgw_pib==1.0.2 # homeassistant.components.incomfort incomfort-client==0.5.0 From 622a69447d1ba94ac8e456f63617ad222e6dc515 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 6 Jun 2024 12:39:24 +0200 Subject: [PATCH 0286/1445] Add type hints to hdmi_cec assert_state function (#118940) Co-authored-by: Marc Mueller <30130371+cdce8p@users.noreply.github.com> --- .../components/hdmi_cec/test_media_player.py | 30 ++++++++++++------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/tests/components/hdmi_cec/test_media_player.py b/tests/components/hdmi_cec/test_media_player.py index e052938f1a0..988279a235f 100644 --- a/tests/components/hdmi_cec/test_media_player.py +++ b/tests/components/hdmi_cec/test_media_player.py @@ -1,5 +1,7 @@ """Tests for the HDMI-CEC media player platform.""" +from collections.abc import Callable + from pycec.const import ( DEVICE_TYPE_NAMES, KEY_BACKWARD, @@ -54,6 +56,8 @@ from homeassistant.core import HomeAssistant from . import MockHDMIDevice, assert_key_press_release +type AssertState = Callable[[str, str], None] + @pytest.fixture( name="assert_state", @@ -70,20 +74,20 @@ from . import MockHDMIDevice, assert_key_press_release ], ids=["skip_assert_state", "run_assert_state"], ) -def assert_state_fixture(request: pytest.FixtureRequest): +def assert_state_fixture(request: pytest.FixtureRequest) -> AssertState: """Allow for skipping the assert state changes. This is broken in this entity, but we still want to test that the rest of the code works as expected. """ - def test_state(state, expected): + def _test_state(state: str, expected: str) -> None: if request.param: assert state == expected else: assert True - return test_state + return _test_state async def test_load_platform( @@ -128,7 +132,10 @@ async def test_load_types( async def test_service_on( - hass: HomeAssistant, create_hdmi_network, create_cec_entity, assert_state + hass: HomeAssistant, + create_hdmi_network, + create_cec_entity, + assert_state: AssertState, ) -> None: """Test that media_player triggers on `on` service.""" hdmi_network = await create_hdmi_network({"platform": "media_player"}) @@ -152,7 +159,10 @@ async def test_service_on( async def test_service_off( - hass: HomeAssistant, create_hdmi_network, create_cec_entity, assert_state + hass: HomeAssistant, + create_hdmi_network, + create_cec_entity, + assert_state: AssertState, ) -> None: """Test that media_player triggers on `off` service.""" hdmi_network = await create_hdmi_network({"platform": "media_player"}) @@ -352,10 +362,10 @@ async def test_playback_services( hass: HomeAssistant, create_hdmi_network, create_cec_entity, - assert_state, - service, - key, - expected_state, + assert_state: AssertState, + service: str, + key: int, + expected_state: str, ) -> None: """Test playback related commands.""" hdmi_network = await create_hdmi_network({"platform": "media_player"}) @@ -382,7 +392,7 @@ async def test_play_pause_service( hass: HomeAssistant, create_hdmi_network, create_cec_entity, - assert_state, + assert_state: AssertState, ) -> None: """Test play pause service.""" hdmi_network = await create_hdmi_network({"platform": "media_player"}) From a5959cfb83c793b3a4f353351f955a49dcbb1fc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20L=C3=B6vdahl?= Date: Thu, 6 Jun 2024 13:52:57 +0300 Subject: [PATCH 0287/1445] Address post-merge review comments from Vallox reconfigure support PR (#118903) Address late review comments from Vallox reconfigure support PR --- tests/components/vallox/test_config_flow.py | 122 ++++++++++++++++++++ 1 file changed, 122 insertions(+) diff --git a/tests/components/vallox/test_config_flow.py b/tests/components/vallox/test_config_flow.py index 3cd14dbcaff..b0c3412c579 100644 --- a/tests/components/vallox/test_config_flow.py +++ b/tests/components/vallox/test_config_flow.py @@ -69,6 +69,26 @@ async def test_form_invalid_ip(hass: HomeAssistant) -> None: assert result["type"] is FlowResultType.FORM assert result["errors"] == {"host": "invalid_host"} + with ( + patch( + "homeassistant.components.vallox.config_flow.Vallox.fetch_metric_data", + return_value=None, + ), + patch( + "homeassistant.components.vallox.async_setup_entry", + return_value=True, + ), + ): + result = await hass.config_entries.flow.async_configure( + init["flow_id"], + {"host": "1.2.3.4"}, + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "Vallox" + assert result["data"] == {"host": "1.2.3.4", "name": "Vallox"} + async def test_form_vallox_api_exception_cannot_connect(hass: HomeAssistant) -> None: """Test that cannot connect error is handled.""" @@ -89,6 +109,26 @@ async def test_form_vallox_api_exception_cannot_connect(hass: HomeAssistant) -> assert result["type"] is FlowResultType.FORM assert result["errors"] == {"host": "cannot_connect"} + with ( + patch( + "homeassistant.components.vallox.config_flow.Vallox.fetch_metric_data", + return_value=None, + ), + patch( + "homeassistant.components.vallox.async_setup_entry", + return_value=True, + ), + ): + result = await hass.config_entries.flow.async_configure( + init["flow_id"], + {"host": "1.2.3.4"}, + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "Vallox" + assert result["data"] == {"host": "1.2.3.4", "name": "Vallox"} + async def test_form_os_error_cannot_connect(hass: HomeAssistant) -> None: """Test that cannot connect error is handled.""" @@ -109,6 +149,26 @@ async def test_form_os_error_cannot_connect(hass: HomeAssistant) -> None: assert result["type"] is FlowResultType.FORM assert result["errors"] == {"host": "cannot_connect"} + with ( + patch( + "homeassistant.components.vallox.config_flow.Vallox.fetch_metric_data", + return_value=None, + ), + patch( + "homeassistant.components.vallox.async_setup_entry", + return_value=True, + ), + ): + result = await hass.config_entries.flow.async_configure( + init["flow_id"], + {"host": "1.2.3.4"}, + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "Vallox" + assert result["data"] == {"host": "1.2.3.4", "name": "Vallox"} + async def test_form_unknown_exception(hass: HomeAssistant) -> None: """Test that unknown exceptions are handled.""" @@ -129,6 +189,26 @@ async def test_form_unknown_exception(hass: HomeAssistant) -> None: assert result["type"] is FlowResultType.FORM assert result["errors"] == {"host": "unknown"} + with ( + patch( + "homeassistant.components.vallox.config_flow.Vallox.fetch_metric_data", + return_value=None, + ), + patch( + "homeassistant.components.vallox.async_setup_entry", + return_value=True, + ), + ): + result = await hass.config_entries.flow.async_configure( + init["flow_id"], + {"host": "1.2.3.4"}, + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "Vallox" + assert result["data"] == {"host": "1.2.3.4", "name": "Vallox"} + async def test_form_already_configured(hass: HomeAssistant) -> None: """Test that already configured error is handled.""" @@ -209,6 +289,20 @@ async def test_reconfigure_host_to_invalid_ip_fails( # entry not changed assert entry.data["host"] == "192.168.100.50" + # makes sure we can recover and continue + reconfigure_result = await hass.config_entries.flow.async_configure( + init_flow_result["flow_id"], + { + "host": "192.168.100.60", + }, + ) + await hass.async_block_till_done() + assert reconfigure_result["type"] is FlowResultType.ABORT + assert reconfigure_result["reason"] == "reconfigure_successful" + + # changed entry + assert entry.data["host"] == "192.168.100.60" + async def test_reconfigure_host_vallox_api_exception_cannot_connect( hass: HomeAssistant, init_reconfigure_flow @@ -234,6 +328,20 @@ async def test_reconfigure_host_vallox_api_exception_cannot_connect( # entry not changed assert entry.data["host"] == "192.168.100.50" + # makes sure we can recover and continue + reconfigure_result = await hass.config_entries.flow.async_configure( + init_flow_result["flow_id"], + { + "host": "192.168.100.60", + }, + ) + await hass.async_block_till_done() + assert reconfigure_result["type"] is FlowResultType.ABORT + assert reconfigure_result["reason"] == "reconfigure_successful" + + # changed entry + assert entry.data["host"] == "192.168.100.60" + async def test_reconfigure_host_unknown_exception( hass: HomeAssistant, init_reconfigure_flow @@ -258,3 +366,17 @@ async def test_reconfigure_host_unknown_exception( # entry not changed assert entry.data["host"] == "192.168.100.50" + + # makes sure we can recover and continue + reconfigure_result = await hass.config_entries.flow.async_configure( + init_flow_result["flow_id"], + { + "host": "192.168.100.60", + }, + ) + await hass.async_block_till_done() + assert reconfigure_result["type"] is FlowResultType.ABORT + assert reconfigure_result["reason"] == "reconfigure_successful" + + # changed entry + assert entry.data["host"] == "192.168.100.60" From 2a4f7439a25d9e05dfbfd5da00d7ad0b60739085 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Thu, 6 Jun 2024 14:21:30 +0300 Subject: [PATCH 0288/1445] Fix Alarm control panel not require code in several integrations (#118961) --- homeassistant/components/agent_dvr/alarm_control_panel.py | 1 + homeassistant/components/blink/alarm_control_panel.py | 1 + homeassistant/components/egardia/alarm_control_panel.py | 1 + homeassistant/components/hive/alarm_control_panel.py | 1 + homeassistant/components/ialarm/alarm_control_panel.py | 1 + homeassistant/components/lupusec/alarm_control_panel.py | 1 + homeassistant/components/nx584/alarm_control_panel.py | 1 + homeassistant/components/overkiz/alarm_control_panel.py | 1 + homeassistant/components/point/alarm_control_panel.py | 1 + homeassistant/components/spc/alarm_control_panel.py | 1 + homeassistant/components/tuya/alarm_control_panel.py | 1 + homeassistant/components/xiaomi_miio/alarm_control_panel.py | 1 + 12 files changed, 12 insertions(+) diff --git a/homeassistant/components/agent_dvr/alarm_control_panel.py b/homeassistant/components/agent_dvr/alarm_control_panel.py index e703bcad6ae..f098184321f 100644 --- a/homeassistant/components/agent_dvr/alarm_control_panel.py +++ b/homeassistant/components/agent_dvr/alarm_control_panel.py @@ -43,6 +43,7 @@ class AgentBaseStation(AlarmControlPanelEntity): | AlarmControlPanelEntityFeature.ARM_AWAY | AlarmControlPanelEntityFeature.ARM_NIGHT ) + _attr_code_arm_required = False _attr_has_entity_name = True _attr_name = None diff --git a/homeassistant/components/blink/alarm_control_panel.py b/homeassistant/components/blink/alarm_control_panel.py index b7dc50a5c51..0ad15cf0d31 100644 --- a/homeassistant/components/blink/alarm_control_panel.py +++ b/homeassistant/components/blink/alarm_control_panel.py @@ -46,6 +46,7 @@ class BlinkSyncModuleHA( """Representation of a Blink Alarm Control Panel.""" _attr_supported_features = AlarmControlPanelEntityFeature.ARM_AWAY + _attr_code_arm_required = False _attr_has_entity_name = True _attr_name = None diff --git a/homeassistant/components/egardia/alarm_control_panel.py b/homeassistant/components/egardia/alarm_control_panel.py index ad08b8cbc4d..706ba0db719 100644 --- a/homeassistant/components/egardia/alarm_control_panel.py +++ b/homeassistant/components/egardia/alarm_control_panel.py @@ -67,6 +67,7 @@ class EgardiaAlarm(AlarmControlPanelEntity): """Representation of a Egardia alarm.""" _attr_state: str | None + _attr_code_arm_required = False _attr_supported_features = ( AlarmControlPanelEntityFeature.ARM_HOME | AlarmControlPanelEntityFeature.ARM_AWAY diff --git a/homeassistant/components/hive/alarm_control_panel.py b/homeassistant/components/hive/alarm_control_panel.py index 78e8606a43c..06383784a3f 100644 --- a/homeassistant/components/hive/alarm_control_panel.py +++ b/homeassistant/components/hive/alarm_control_panel.py @@ -51,6 +51,7 @@ class HiveAlarmControlPanelEntity(HiveEntity, AlarmControlPanelEntity): | AlarmControlPanelEntityFeature.ARM_AWAY | AlarmControlPanelEntityFeature.TRIGGER ) + _attr_code_arm_required = False async def async_alarm_disarm(self, code: str | None = None) -> None: """Send disarm command.""" diff --git a/homeassistant/components/ialarm/alarm_control_panel.py b/homeassistant/components/ialarm/alarm_control_panel.py index a7118fb03cc..912f04a1d1e 100644 --- a/homeassistant/components/ialarm/alarm_control_panel.py +++ b/homeassistant/components/ialarm/alarm_control_panel.py @@ -37,6 +37,7 @@ class IAlarmPanel( AlarmControlPanelEntityFeature.ARM_HOME | AlarmControlPanelEntityFeature.ARM_AWAY ) + _attr_code_arm_required = False def __init__(self, coordinator: IAlarmDataUpdateCoordinator) -> None: """Create the entity with a DataUpdateCoordinator.""" diff --git a/homeassistant/components/lupusec/alarm_control_panel.py b/homeassistant/components/lupusec/alarm_control_panel.py index 090d9ab3ced..73aba775a2a 100644 --- a/homeassistant/components/lupusec/alarm_control_panel.py +++ b/homeassistant/components/lupusec/alarm_control_panel.py @@ -48,6 +48,7 @@ class LupusecAlarm(LupusecDevice, AlarmControlPanelEntity): AlarmControlPanelEntityFeature.ARM_HOME | AlarmControlPanelEntityFeature.ARM_AWAY ) + _attr_code_arm_required = False def __init__( self, data: lupupy.Lupusec, device: lupupy.devices.LupusecAlarm, entry_id: str diff --git a/homeassistant/components/nx584/alarm_control_panel.py b/homeassistant/components/nx584/alarm_control_panel.py index a86cda83dd7..2e306de5908 100644 --- a/homeassistant/components/nx584/alarm_control_panel.py +++ b/homeassistant/components/nx584/alarm_control_panel.py @@ -100,6 +100,7 @@ class NX584Alarm(AlarmControlPanelEntity): AlarmControlPanelEntityFeature.ARM_HOME | AlarmControlPanelEntityFeature.ARM_AWAY ) + _attr_code_arm_required = False def __init__(self, name: str, alarm_client: client.Client, url: str) -> None: """Init the nx584 alarm panel.""" diff --git a/homeassistant/components/overkiz/alarm_control_panel.py b/homeassistant/components/overkiz/alarm_control_panel.py index 72c99982a1b..151f91790cf 100644 --- a/homeassistant/components/overkiz/alarm_control_panel.py +++ b/homeassistant/components/overkiz/alarm_control_panel.py @@ -240,6 +240,7 @@ class OverkizAlarmControlPanel(OverkizDescriptiveEntity, AlarmControlPanelEntity """Representation of an Overkiz Alarm Control Panel.""" entity_description: OverkizAlarmDescription + _attr_code_arm_required = False def __init__( self, diff --git a/homeassistant/components/point/alarm_control_panel.py b/homeassistant/components/point/alarm_control_panel.py index b04742af06a..844d1eba553 100644 --- a/homeassistant/components/point/alarm_control_panel.py +++ b/homeassistant/components/point/alarm_control_panel.py @@ -55,6 +55,7 @@ class MinutPointAlarmControl(AlarmControlPanelEntity): """The platform class required by Home Assistant.""" _attr_supported_features = AlarmControlPanelEntityFeature.ARM_AWAY + _attr_code_arm_required = False def __init__(self, point_client: MinutPointClient, home_id: str) -> None: """Initialize the entity.""" diff --git a/homeassistant/components/spc/alarm_control_panel.py b/homeassistant/components/spc/alarm_control_panel.py index ae349d2497e..7e584ff5e63 100644 --- a/homeassistant/components/spc/alarm_control_panel.py +++ b/homeassistant/components/spc/alarm_control_panel.py @@ -62,6 +62,7 @@ class SpcAlarm(AlarmControlPanelEntity): | AlarmControlPanelEntityFeature.ARM_AWAY | AlarmControlPanelEntityFeature.ARM_NIGHT ) + _attr_code_arm_required = False def __init__(self, area: Area, api: SpcWebGateway) -> None: """Initialize the SPC alarm panel.""" diff --git a/homeassistant/components/tuya/alarm_control_panel.py b/homeassistant/components/tuya/alarm_control_panel.py index 868f6634bc9..29da625a990 100644 --- a/homeassistant/components/tuya/alarm_control_panel.py +++ b/homeassistant/components/tuya/alarm_control_panel.py @@ -88,6 +88,7 @@ class TuyaAlarmEntity(TuyaEntity, AlarmControlPanelEntity): """Tuya Alarm Entity.""" _attr_name = None + _attr_code_arm_required = False def __init__( self, diff --git a/homeassistant/components/xiaomi_miio/alarm_control_panel.py b/homeassistant/components/xiaomi_miio/alarm_control_panel.py index 72530227e88..58d5ed247ad 100644 --- a/homeassistant/components/xiaomi_miio/alarm_control_panel.py +++ b/homeassistant/components/xiaomi_miio/alarm_control_panel.py @@ -54,6 +54,7 @@ class XiaomiGatewayAlarm(AlarmControlPanelEntity): _attr_icon = "mdi:shield-home" _attr_supported_features = AlarmControlPanelEntityFeature.ARM_AWAY + _attr_code_arm_required = False def __init__( self, gateway_device, gateway_name, model, mac_address, gateway_device_id From 3d8fc965929ecfce8910611fe4a8567733ccaaf4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Strandberg?= Date: Thu, 6 Jun 2024 13:31:47 +0200 Subject: [PATCH 0289/1445] Migrate myuplink to runtime_data (#118960) --- homeassistant/components/myuplink/__init__.py | 18 +++++++++--------- .../components/myuplink/binary_sensor.py | 8 +++----- .../components/myuplink/diagnostics.py | 9 +++------ homeassistant/components/myuplink/number.py | 8 +++----- homeassistant/components/myuplink/sensor.py | 8 +++----- homeassistant/components/myuplink/switch.py | 8 +++----- homeassistant/components/myuplink/update.py | 8 +++----- 7 files changed, 27 insertions(+), 40 deletions(-) diff --git a/homeassistant/components/myuplink/__init__.py b/homeassistant/components/myuplink/__init__.py index 6d1932f22df..a8307cf8c6c 100644 --- a/homeassistant/components/myuplink/__init__.py +++ b/homeassistant/components/myuplink/__init__.py @@ -30,10 +30,13 @@ PLATFORMS: list[Platform] = [ Platform.UPDATE, ] +type MyUplinkConfigEntry = ConfigEntry[MyUplinkDataCoordinator] -async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: + +async def async_setup_entry( + hass: HomeAssistant, config_entry: MyUplinkConfigEntry +) -> bool: """Set up myUplink from a config entry.""" - hass.data.setdefault(DOMAIN, {}) implementation = ( await config_entry_oauth2_flow.async_get_config_entry_implementation( @@ -59,7 +62,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b api = MyUplinkAPI(auth) coordinator = MyUplinkDataCoordinator(hass, api) await coordinator.async_config_entry_first_refresh() - hass.data[DOMAIN][config_entry.entry_id] = coordinator + config_entry.runtime_data = coordinator # Update device registry create_devices(hass, config_entry, coordinator) @@ -71,10 +74,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): - hass.data[DOMAIN].pop(entry.entry_id) - - return unload_ok + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) @callback @@ -100,11 +100,11 @@ def create_devices( async def async_remove_config_entry_device( - hass: HomeAssistant, config_entry: ConfigEntry, device_entry: DeviceEntry + hass: HomeAssistant, config_entry: MyUplinkConfigEntry, device_entry: DeviceEntry ) -> bool: """Remove myuplink config entry from a device.""" - myuplink_data: MyUplinkDataCoordinator = hass.data[DOMAIN][config_entry.entry_id] + myuplink_data = config_entry.runtime_data return not device_entry.identifiers.intersection( (DOMAIN, device_id) for device_id in myuplink_data.data.devices ) diff --git a/homeassistant/components/myuplink/binary_sensor.py b/homeassistant/components/myuplink/binary_sensor.py index f22565b42ed..1478ed9c8b0 100644 --- a/homeassistant/components/myuplink/binary_sensor.py +++ b/homeassistant/components/myuplink/binary_sensor.py @@ -7,13 +7,11 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntity, BinarySensorEntityDescription, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import MyUplinkDataCoordinator -from .const import DOMAIN +from . import MyUplinkConfigEntry, MyUplinkDataCoordinator from .entity import MyUplinkEntity, MyUplinkSystemEntity from .helpers import find_matching_platform @@ -51,12 +49,12 @@ def get_description(device_point: DevicePoint) -> BinarySensorEntityDescription async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: MyUplinkConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up myUplink binary_sensor.""" entities: list[BinarySensorEntity] = [] - coordinator: MyUplinkDataCoordinator = hass.data[DOMAIN][config_entry.entry_id] + coordinator = config_entry.runtime_data # Setup device point bound sensors for device_id, point_data in coordinator.data.points.items(): diff --git a/homeassistant/components/myuplink/diagnostics.py b/homeassistant/components/myuplink/diagnostics.py index 15b643ffd92..5e26cf273b4 100644 --- a/homeassistant/components/myuplink/diagnostics.py +++ b/homeassistant/components/myuplink/diagnostics.py @@ -4,25 +4,22 @@ from __future__ import annotations from typing import Any -from myuplink import MyUplinkAPI - from homeassistant.components.diagnostics import async_redact_data -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from .const import DOMAIN +from . import MyUplinkConfigEntry TO_REDACT = {"access_token", "refresh_token", "serialNumber"} async def async_get_config_entry_diagnostics( - hass: HomeAssistant, config_entry: ConfigEntry + hass: HomeAssistant, config_entry: MyUplinkConfigEntry ) -> dict[str, Any]: """Return diagnostics for a config entry. Pick up fresh data from API and dump it. """ - api: MyUplinkAPI = hass.data[DOMAIN][config_entry.entry_id].api + api = config_entry.runtime_data.api myuplink_data = {} myuplink_data["my_systems"] = await api.async_get_systems_json() myuplink_data["my_systems"]["devices"] = [] diff --git a/homeassistant/components/myuplink/number.py b/homeassistant/components/myuplink/number.py index 89d6658d368..7c63a8ec8a2 100644 --- a/homeassistant/components/myuplink/number.py +++ b/homeassistant/components/myuplink/number.py @@ -4,14 +4,12 @@ from aiohttp import ClientError from myuplink import DevicePoint from homeassistant.components.number import NumberEntity, NumberEntityDescription -from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import MyUplinkDataCoordinator -from .const import DOMAIN +from . import MyUplinkConfigEntry, MyUplinkDataCoordinator from .entity import MyUplinkEntity from .helpers import find_matching_platform, skip_entity @@ -55,12 +53,12 @@ def get_description(device_point: DevicePoint) -> NumberEntityDescription | None async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: MyUplinkConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up myUplink number.""" entities: list[NumberEntity] = [] - coordinator: MyUplinkDataCoordinator = hass.data[DOMAIN][config_entry.entry_id] + coordinator = config_entry.runtime_data # Setup device point number entities for device_id, point_data in coordinator.data.points.items(): diff --git a/homeassistant/components/myuplink/sensor.py b/homeassistant/components/myuplink/sensor.py index 9d23584f389..e7c8054e304 100644 --- a/homeassistant/components/myuplink/sensor.py +++ b/homeassistant/components/myuplink/sensor.py @@ -8,7 +8,6 @@ from homeassistant.components.sensor import ( SensorEntityDescription, SensorStateClass, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( REVOLUTIONS_PER_MINUTE, Platform, @@ -25,8 +24,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType -from . import MyUplinkDataCoordinator -from .const import DOMAIN +from . import MyUplinkConfigEntry, MyUplinkDataCoordinator from .entity import MyUplinkEntity from .helpers import find_matching_platform, skip_entity @@ -187,13 +185,13 @@ def get_description(device_point: DevicePoint) -> SensorEntityDescription | None async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: MyUplinkConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up myUplink sensor.""" entities: list[SensorEntity] = [] - coordinator: MyUplinkDataCoordinator = hass.data[DOMAIN][config_entry.entry_id] + coordinator = config_entry.runtime_data # Setup device point sensors for device_id, point_data in coordinator.data.points.items(): diff --git a/homeassistant/components/myuplink/switch.py b/homeassistant/components/myuplink/switch.py index 11dca1e2ac0..1589701fcbc 100644 --- a/homeassistant/components/myuplink/switch.py +++ b/homeassistant/components/myuplink/switch.py @@ -6,14 +6,12 @@ import aiohttp from myuplink import DevicePoint from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription -from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import MyUplinkDataCoordinator -from .const import DOMAIN +from . import MyUplinkConfigEntry, MyUplinkDataCoordinator from .entity import MyUplinkEntity from .helpers import find_matching_platform, skip_entity @@ -44,12 +42,12 @@ def get_description(device_point: DevicePoint) -> SwitchEntityDescription | None async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: MyUplinkConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up myUplink switch.""" entities: list[SwitchEntity] = [] - coordinator: MyUplinkDataCoordinator = hass.data[DOMAIN][config_entry.entry_id] + coordinator = config_entry.runtime_data # Setup device point switches for device_id, point_data in coordinator.data.points.items(): diff --git a/homeassistant/components/myuplink/update.py b/homeassistant/components/myuplink/update.py index 6a38741a562..9e94de0a503 100644 --- a/homeassistant/components/myuplink/update.py +++ b/homeassistant/components/myuplink/update.py @@ -5,12 +5,10 @@ from homeassistant.components.update import ( UpdateEntity, UpdateEntityDescription, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import MyUplinkDataCoordinator -from .const import DOMAIN +from . import MyUplinkConfigEntry, MyUplinkDataCoordinator from .entity import MyUplinkEntity UPDATE_DESCRIPTION = UpdateEntityDescription( @@ -21,11 +19,11 @@ UPDATE_DESCRIPTION = UpdateEntityDescription( async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: MyUplinkConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up update entity.""" - coordinator: MyUplinkDataCoordinator = hass.data[DOMAIN][config_entry.entry_id] + coordinator = config_entry.runtime_data async_add_entities( MyUplinkDeviceUpdate( From 0d2e441de583911a0d7e2fd079a6da136adb0c7f Mon Sep 17 00:00:00 2001 From: G Johansson Date: Thu, 6 Jun 2024 14:34:03 +0300 Subject: [PATCH 0290/1445] Bump python-holidays to 0.50 (#118965) --- homeassistant/components/holiday/manifest.json | 2 +- homeassistant/components/workday/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/holiday/manifest.json b/homeassistant/components/holiday/manifest.json index 5ac6611592d..bc7ce0e8dd1 100644 --- a/homeassistant/components/holiday/manifest.json +++ b/homeassistant/components/holiday/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/holiday", "iot_class": "local_polling", - "requirements": ["holidays==0.49", "babel==2.13.1"] + "requirements": ["holidays==0.50", "babel==2.13.1"] } diff --git a/homeassistant/components/workday/manifest.json b/homeassistant/components/workday/manifest.json index 7faf82ad71a..71c26a30e94 100644 --- a/homeassistant/components/workday/manifest.json +++ b/homeassistant/components/workday/manifest.json @@ -7,5 +7,5 @@ "iot_class": "local_polling", "loggers": ["holidays"], "quality_scale": "internal", - "requirements": ["holidays==0.49"] + "requirements": ["holidays==0.50"] } diff --git a/requirements_all.txt b/requirements_all.txt index 17266a8b467..bef4971b555 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1084,7 +1084,7 @@ hole==0.8.0 # homeassistant.components.holiday # homeassistant.components.workday -holidays==0.49 +holidays==0.50 # homeassistant.components.frontend home-assistant-frontend==20240605.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e07132b66ce..ce3e7cc0160 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -886,7 +886,7 @@ hole==0.8.0 # homeassistant.components.holiday # homeassistant.components.workday -holidays==0.49 +holidays==0.50 # homeassistant.components.frontend home-assistant-frontend==20240605.0 From 4ec6ba445bb5104f7b55e5fd1b8330fecc464c46 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Thu, 6 Jun 2024 14:49:17 +0300 Subject: [PATCH 0291/1445] Remove unused constant in Tag (#118966) Remove not used constant in Tag --- homeassistant/components/tag/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/tag/__init__.py b/homeassistant/components/tag/__init__.py index 1613601e23a..af3d06cf2d4 100644 --- a/homeassistant/components/tag/__init__.py +++ b/homeassistant/components/tag/__init__.py @@ -34,7 +34,6 @@ STORAGE_VERSION = 1 STORAGE_VERSION_MINOR = 3 TAG_DATA: HassKey[TagStorageCollection] = HassKey(DOMAIN) -SIGNAL_TAG_CHANGED = "signal_tag_changed" CREATE_FIELDS = { vol.Optional(TAG_ID): cv.string, From cab58fa9b20d101db57ec0f860110608ba29446e Mon Sep 17 00:00:00 2001 From: Rami Mosleh Date: Thu, 6 Jun 2024 16:10:03 +0400 Subject: [PATCH 0292/1445] Check if imap message text has a value instead of checking if its not None (#118901) * Check if message_text has a value instead of checking if its not None * Strip message_text to ensure that its actually empty or not * Add test with multipart payload having empty plain text --- homeassistant/components/imap/coordinator.py | 6 +-- tests/components/imap/const.py | 39 ++++++++++++++++++++ tests/components/imap/test_init.py | 3 ++ 3 files changed, 45 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/imap/coordinator.py b/homeassistant/components/imap/coordinator.py index c0123b89ee4..a9d0fdfbd48 100644 --- a/homeassistant/components/imap/coordinator.py +++ b/homeassistant/components/imap/coordinator.py @@ -195,13 +195,13 @@ class ImapMessage: ): message_untyped_text = str(part.get_payload()) - if message_text is not None: + if message_text is not None and message_text.strip(): return message_text - if message_html is not None: + if message_html: return message_html - if message_untyped_text is not None: + if message_untyped_text: return message_untyped_text return str(self.email_message.get_payload()) diff --git a/tests/components/imap/const.py b/tests/components/imap/const.py index 677eea7a473..037960c9e5d 100644 --- a/tests/components/imap/const.py +++ b/tests/components/imap/const.py @@ -59,6 +59,11 @@ TEST_CONTENT_TEXT_PLAIN = ( b"Content-Transfer-Encoding: 7bit\r\n\r\nTest body\r\n" ) +TEST_CONTENT_TEXT_PLAIN_EMPTY = ( + b'Content-Type: text/plain; charset="utf-8"\r\n' + b"Content-Transfer-Encoding: 7bit\r\n\r\n \r\n" +) + TEST_CONTENT_TEXT_BASE64 = ( b'Content-Type: text/plain; charset="utf-8"\r\n' b"Content-Transfer-Encoding: base64\r\n\r\nVGVzdCBib2R5\r\n" @@ -108,6 +113,15 @@ TEST_CONTENT_MULTIPART = ( + b"\r\n--Mark=_100584970350292485166--\r\n" ) +TEST_CONTENT_MULTIPART_EMPTY_PLAIN = ( + b"\r\nThis is a multi-part message in MIME format.\r\n" + b"\r\n--Mark=_100584970350292485166\r\n" + + TEST_CONTENT_TEXT_PLAIN_EMPTY + + b"\r\n--Mark=_100584970350292485166\r\n" + + TEST_CONTENT_HTML + + b"\r\n--Mark=_100584970350292485166--\r\n" +) + TEST_CONTENT_MULTIPART_BASE64 = ( b"\r\nThis is a multi-part message in MIME format.\r\n" b"\r\n--Mark=_100584970350292485166\r\n" @@ -155,6 +169,18 @@ TEST_FETCH_RESPONSE_TEXT_PLAIN = ( ], ) +TEST_FETCH_RESPONSE_TEXT_PLAIN_EMPTY = ( + "OK", + [ + b"1 FETCH (BODY[] {" + + str(len(TEST_MESSAGE + TEST_CONTENT_TEXT_PLAIN_EMPTY)).encode("utf-8") + + b"}", + bytearray(TEST_MESSAGE + TEST_CONTENT_TEXT_PLAIN_EMPTY), + b")", + b"Fetch completed (0.0001 + 0.000 secs).", + ], +) + TEST_FETCH_RESPONSE_TEXT_PLAIN_ALT = ( "OK", [ @@ -249,6 +275,19 @@ TEST_FETCH_RESPONSE_MULTIPART = ( b"Fetch completed (0.0001 + 0.000 secs).", ], ) +TEST_FETCH_RESPONSE_MULTIPART_EMPTY_PLAIN = ( + "OK", + [ + b"1 FETCH (BODY[] {" + + str(len(TEST_MESSAGE_MULTIPART + TEST_CONTENT_MULTIPART_EMPTY_PLAIN)).encode( + "utf-8" + ) + + b"}", + bytearray(TEST_MESSAGE_MULTIPART + TEST_CONTENT_MULTIPART_EMPTY_PLAIN), + b")", + b"Fetch completed (0.0001 + 0.000 secs).", + ], +) TEST_FETCH_RESPONSE_MULTIPART_BASE64 = ( "OK", [ diff --git a/tests/components/imap/test_init.py b/tests/components/imap/test_init.py index e6e6ffe7114..fe10770fc64 100644 --- a/tests/components/imap/test_init.py +++ b/tests/components/imap/test_init.py @@ -29,6 +29,7 @@ from .const import ( TEST_FETCH_RESPONSE_MULTIPART, TEST_FETCH_RESPONSE_MULTIPART_BASE64, TEST_FETCH_RESPONSE_MULTIPART_BASE64_INVALID, + TEST_FETCH_RESPONSE_MULTIPART_EMPTY_PLAIN, TEST_FETCH_RESPONSE_NO_SUBJECT_TO_FROM, TEST_FETCH_RESPONSE_TEXT_BARE, TEST_FETCH_RESPONSE_TEXT_OTHER, @@ -116,6 +117,7 @@ async def test_entry_startup_fails( (TEST_FETCH_RESPONSE_TEXT_OTHER, True), (TEST_FETCH_RESPONSE_HTML, True), (TEST_FETCH_RESPONSE_MULTIPART, True), + (TEST_FETCH_RESPONSE_MULTIPART_EMPTY_PLAIN, True), (TEST_FETCH_RESPONSE_MULTIPART_BASE64, True), (TEST_FETCH_RESPONSE_BINARY, True), ], @@ -129,6 +131,7 @@ async def test_entry_startup_fails( "other", "html", "multipart", + "multipart_empty_plain", "multipart_base64", "binary", ], From 69708db8e0849eb7c0a51b8f93e03faf34051957 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Thu, 6 Jun 2024 16:14:39 +0200 Subject: [PATCH 0293/1445] Update mypy-dev to 1.11.0a6 (#118881) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 8ab1efe3d69..8ba327285a0 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -11,7 +11,7 @@ astroid==3.2.2 coverage==7.5.3 freezegun==1.5.0 mock-open==1.4.0 -mypy-dev==1.11.0a5 +mypy-dev==1.11.0a6 pre-commit==3.7.1 pydantic==1.10.15 pylint==3.2.2 From fe21e2b8ba679b495fe50bc3d3d80e08496edf72 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Thu, 6 Jun 2024 17:02:13 +0200 Subject: [PATCH 0294/1445] Import Generator from typing_extensions (1) (#118986) --- .../components/alexa/capabilities.py | 5 +- homeassistant/components/alexa/entities.py | 50 ++++++++++--------- .../components/assist_pipeline/pipeline.py | 11 ++-- .../assist_pipeline/websocket_api.py | 5 +- homeassistant/components/automation/trace.py | 5 +- .../bluetooth/passive_update_coordinator.py | 6 ++- .../components/ecovacs/controller.py | 5 +- .../components/homekit/aidmanager.py | 4 +- .../homekit_controller/device_trigger.py | 5 +- homeassistant/components/knx/config_flow.py | 4 +- homeassistant/components/logbook/processor.py | 9 ++-- homeassistant/components/matter/discovery.py | 7 ++- homeassistant/components/mqtt/client.py | 5 +- homeassistant/components/profiler/__init__.py | 4 +- homeassistant/components/recorder/util.py | 7 +-- homeassistant/components/stream/fmp4utils.py | 5 +- homeassistant/components/stream/worker.py | 5 +- .../components/unifiprotect/camera.py | 4 +- homeassistant/components/unifiprotect/data.py | 5 +- .../components/unifiprotect/utils.py | 5 +- homeassistant/components/wemo/entity.py | 4 +- homeassistant/components/wyoming/satellite.py | 4 +- .../components/zwave_js/discovery.py | 8 +-- homeassistant/components/zwave_js/services.py | 5 +- homeassistant/config_entries.py | 14 ++---- homeassistant/exceptions.py | 12 +++-- homeassistant/helpers/condition.py | 5 +- homeassistant/helpers/script.py | 5 +- homeassistant/helpers/template.py | 9 ++-- homeassistant/helpers/trace.py | 6 ++- homeassistant/helpers/update_coordinator.py | 6 +-- homeassistant/setup.py | 10 ++-- .../templates/config_flow/tests/conftest.py | 4 +- .../config_flow_helper/tests/conftest.py | 4 +- 34 files changed, 134 insertions(+), 118 deletions(-) diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py index 8a636fd744e..047e981ab0d 100644 --- a/homeassistant/components/alexa/capabilities.py +++ b/homeassistant/components/alexa/capabilities.py @@ -2,10 +2,11 @@ from __future__ import annotations -from collections.abc import Generator import logging from typing import Any +from typing_extensions import Generator + from homeassistant.components import ( button, climate, @@ -260,7 +261,7 @@ class AlexaCapability: return result - def serialize_properties(self) -> Generator[dict[str, Any], None, None]: + def serialize_properties(self) -> Generator[dict[str, Any]]: """Return properties serialized for an API response.""" for prop in self.properties_supported(): prop_name = prop["name"] diff --git a/homeassistant/components/alexa/entities.py b/homeassistant/components/alexa/entities.py index 1ab4aafc081..8d45ac3a11b 100644 --- a/homeassistant/components/alexa/entities.py +++ b/homeassistant/components/alexa/entities.py @@ -2,10 +2,12 @@ from __future__ import annotations -from collections.abc import Generator, Iterable +from collections.abc import Iterable import logging from typing import TYPE_CHECKING, Any +from typing_extensions import Generator + from homeassistant.components import ( alarm_control_panel, alert, @@ -319,7 +321,7 @@ class AlexaEntity: """ raise NotImplementedError - def serialize_properties(self) -> Generator[dict[str, Any], None, None]: + def serialize_properties(self) -> Generator[dict[str, Any]]: """Yield each supported property in API format.""" for interface in self.interfaces(): if not interface.properties_proactively_reported(): @@ -405,7 +407,7 @@ class GenericCapabilities(AlexaEntity): return [DisplayCategory.OTHER] - def interfaces(self) -> Generator[AlexaCapability, None, None]: + def interfaces(self) -> Generator[AlexaCapability]: """Yield the supported interfaces.""" yield AlexaPowerController(self.entity) yield AlexaEndpointHealth(self.hass, self.entity) @@ -428,7 +430,7 @@ class SwitchCapabilities(AlexaEntity): return [DisplayCategory.SWITCH] - def interfaces(self) -> Generator[AlexaCapability, None, None]: + def interfaces(self) -> Generator[AlexaCapability]: """Yield the supported interfaces.""" yield AlexaPowerController(self.entity) yield AlexaContactSensor(self.hass, self.entity) @@ -445,7 +447,7 @@ class ButtonCapabilities(AlexaEntity): """Return the display categories for this entity.""" return [DisplayCategory.ACTIVITY_TRIGGER] - def interfaces(self) -> Generator[AlexaCapability, None, None]: + def interfaces(self) -> Generator[AlexaCapability]: """Yield the supported interfaces.""" yield AlexaSceneController(self.entity, supports_deactivation=False) yield AlexaEventDetectionSensor(self.hass, self.entity) @@ -464,7 +466,7 @@ class ClimateCapabilities(AlexaEntity): return [DisplayCategory.WATER_HEATER] return [DisplayCategory.THERMOSTAT] - def interfaces(self) -> Generator[AlexaCapability, None, None]: + def interfaces(self) -> Generator[AlexaCapability]: """Yield the supported interfaces.""" # If we support two modes, one being off, we allow turning on too. supported_features = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) @@ -532,7 +534,7 @@ class CoverCapabilities(AlexaEntity): return [DisplayCategory.OTHER] - def interfaces(self) -> Generator[AlexaCapability, None, None]: + def interfaces(self) -> Generator[AlexaCapability]: """Yield the supported interfaces.""" device_class = self.entity.attributes.get(ATTR_DEVICE_CLASS) if device_class not in ( @@ -570,7 +572,7 @@ class EventCapabilities(AlexaEntity): return [DisplayCategory.DOORBELL] return None - def interfaces(self) -> Generator[AlexaCapability, None, None]: + def interfaces(self) -> Generator[AlexaCapability]: """Yield the supported interfaces.""" if self.default_display_categories() is not None: yield AlexaDoorbellEventSource(self.entity) @@ -586,7 +588,7 @@ class LightCapabilities(AlexaEntity): """Return the display categories for this entity.""" return [DisplayCategory.LIGHT] - def interfaces(self) -> Generator[AlexaCapability, None, None]: + def interfaces(self) -> Generator[AlexaCapability]: """Yield the supported interfaces.""" yield AlexaPowerController(self.entity) @@ -610,7 +612,7 @@ class FanCapabilities(AlexaEntity): """Return the display categories for this entity.""" return [DisplayCategory.FAN] - def interfaces(self) -> Generator[AlexaCapability, None, None]: + def interfaces(self) -> Generator[AlexaCapability]: """Yield the supported interfaces.""" yield AlexaPowerController(self.entity) force_range_controller = True @@ -653,7 +655,7 @@ class HumidifierCapabilities(AlexaEntity): """Return the display categories for this entity.""" return [DisplayCategory.OTHER] - def interfaces(self) -> Generator[AlexaCapability, None, None]: + def interfaces(self) -> Generator[AlexaCapability]: """Yield the supported interfaces.""" yield AlexaPowerController(self.entity) supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) @@ -677,7 +679,7 @@ class LockCapabilities(AlexaEntity): """Return the display categories for this entity.""" return [DisplayCategory.SMARTLOCK] - def interfaces(self) -> Generator[AlexaCapability, None, None]: + def interfaces(self) -> Generator[AlexaCapability]: """Yield the supported interfaces.""" yield AlexaLockController(self.entity) yield AlexaEndpointHealth(self.hass, self.entity) @@ -696,7 +698,7 @@ class MediaPlayerCapabilities(AlexaEntity): return [DisplayCategory.TV] - def interfaces(self) -> Generator[AlexaCapability, None, None]: + def interfaces(self) -> Generator[AlexaCapability]: """Yield the supported interfaces.""" yield AlexaPowerController(self.entity) @@ -766,7 +768,7 @@ class SceneCapabilities(AlexaEntity): """Return the display categories for this entity.""" return [DisplayCategory.SCENE_TRIGGER] - def interfaces(self) -> Generator[AlexaCapability, None, None]: + def interfaces(self) -> Generator[AlexaCapability]: """Yield the supported interfaces.""" yield AlexaSceneController(self.entity, supports_deactivation=False) yield Alexa(self.entity) @@ -780,7 +782,7 @@ class ScriptCapabilities(AlexaEntity): """Return the display categories for this entity.""" return [DisplayCategory.ACTIVITY_TRIGGER] - def interfaces(self) -> Generator[AlexaCapability, None, None]: + def interfaces(self) -> Generator[AlexaCapability]: """Yield the supported interfaces.""" yield AlexaSceneController(self.entity, supports_deactivation=True) yield Alexa(self.entity) @@ -796,7 +798,7 @@ class SensorCapabilities(AlexaEntity): # sensors are currently ignored. return [DisplayCategory.TEMPERATURE_SENSOR] - def interfaces(self) -> Generator[AlexaCapability, None, None]: + def interfaces(self) -> Generator[AlexaCapability]: """Yield the supported interfaces.""" attrs = self.entity.attributes if attrs.get(ATTR_UNIT_OF_MEASUREMENT) in { @@ -827,7 +829,7 @@ class BinarySensorCapabilities(AlexaEntity): return [DisplayCategory.CAMERA] return None - def interfaces(self) -> Generator[AlexaCapability, None, None]: + def interfaces(self) -> Generator[AlexaCapability]: """Yield the supported interfaces.""" sensor_type = self.get_type() if sensor_type is self.TYPE_CONTACT: @@ -883,7 +885,7 @@ class AlarmControlPanelCapabilities(AlexaEntity): """Return the display categories for this entity.""" return [DisplayCategory.SECURITY_PANEL] - def interfaces(self) -> Generator[AlexaCapability, None, None]: + def interfaces(self) -> Generator[AlexaCapability]: """Yield the supported interfaces.""" if not self.entity.attributes.get("code_arm_required"): yield AlexaSecurityPanelController(self.hass, self.entity) @@ -899,7 +901,7 @@ class ImageProcessingCapabilities(AlexaEntity): """Return the display categories for this entity.""" return [DisplayCategory.CAMERA] - def interfaces(self) -> Generator[AlexaCapability, None, None]: + def interfaces(self) -> Generator[AlexaCapability]: """Yield the supported interfaces.""" yield AlexaEventDetectionSensor(self.hass, self.entity) yield AlexaEndpointHealth(self.hass, self.entity) @@ -915,7 +917,7 @@ class InputNumberCapabilities(AlexaEntity): """Return the display categories for this entity.""" return [DisplayCategory.OTHER] - def interfaces(self) -> Generator[AlexaCapability, None, None]: + def interfaces(self) -> Generator[AlexaCapability]: """Yield the supported interfaces.""" domain = self.entity.domain yield AlexaRangeController(self.entity, instance=f"{domain}.value") @@ -931,7 +933,7 @@ class TimerCapabilities(AlexaEntity): """Return the display categories for this entity.""" return [DisplayCategory.OTHER] - def interfaces(self) -> Generator[AlexaCapability, None, None]: + def interfaces(self) -> Generator[AlexaCapability]: """Yield the supported interfaces.""" yield AlexaTimeHoldController(self.entity, allow_remote_resume=True) yield AlexaPowerController(self.entity) @@ -946,7 +948,7 @@ class VacuumCapabilities(AlexaEntity): """Return the display categories for this entity.""" return [DisplayCategory.VACUUM_CLEANER] - def interfaces(self) -> Generator[AlexaCapability, None, None]: + def interfaces(self) -> Generator[AlexaCapability]: """Yield the supported interfaces.""" supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) if ( @@ -981,7 +983,7 @@ class ValveCapabilities(AlexaEntity): """Return the display categories for this entity.""" return [DisplayCategory.OTHER] - def interfaces(self) -> Generator[AlexaCapability, None, None]: + def interfaces(self) -> Generator[AlexaCapability]: """Yield the supported interfaces.""" supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) if supported & valve.ValveEntityFeature.SET_POSITION: @@ -1006,7 +1008,7 @@ class CameraCapabilities(AlexaEntity): """Return the display categories for this entity.""" return [DisplayCategory.CAMERA] - def interfaces(self) -> Generator[AlexaCapability, None, None]: + def interfaces(self) -> Generator[AlexaCapability]: """Yield the supported interfaces.""" if self._check_requirements(): supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) diff --git a/homeassistant/components/assist_pipeline/pipeline.py b/homeassistant/components/assist_pipeline/pipeline.py index 2b4b306b68e..4bc008d895b 100644 --- a/homeassistant/components/assist_pipeline/pipeline.py +++ b/homeassistant/components/assist_pipeline/pipeline.py @@ -5,7 +5,7 @@ from __future__ import annotations import array import asyncio from collections import defaultdict, deque -from collections.abc import AsyncGenerator, AsyncIterable, Callable, Iterable +from collections.abc import AsyncIterable, Callable, Iterable from dataclasses import asdict, dataclass, field from enum import StrEnum import logging @@ -16,6 +16,7 @@ import time from typing import TYPE_CHECKING, Any, Final, Literal, cast import wave +from typing_extensions import AsyncGenerator import voluptuous as vol if TYPE_CHECKING: @@ -922,7 +923,7 @@ class PipelineRun: stt_vad: VoiceCommandSegmenter | None, sample_rate: int = 16000, sample_width: int = 2, - ) -> AsyncGenerator[bytes, None]: + ) -> AsyncGenerator[bytes]: """Yield audio chunks until VAD detects silence or speech-to-text completes.""" chunk_seconds = AUDIO_PROCESSOR_SAMPLES / sample_rate sent_vad_start = False @@ -1185,7 +1186,7 @@ class PipelineRun: audio_stream: AsyncIterable[bytes], sample_rate: int = 16000, sample_width: int = 2, - ) -> AsyncGenerator[ProcessedAudioChunk, None]: + ) -> AsyncGenerator[ProcessedAudioChunk]: """Apply volume transformation only (no VAD/audio enhancements) with optional chunking.""" ms_per_sample = sample_rate // 1000 ms_per_chunk = (AUDIO_PROCESSOR_SAMPLES // sample_width) // ms_per_sample @@ -1220,7 +1221,7 @@ class PipelineRun: audio_stream: AsyncIterable[bytes], sample_rate: int = 16000, sample_width: int = 2, - ) -> AsyncGenerator[ProcessedAudioChunk, None]: + ) -> AsyncGenerator[ProcessedAudioChunk]: """Split audio into 10 ms chunks and apply VAD/noise suppression/auto gain/volume transformation.""" assert self.audio_processor is not None @@ -1386,7 +1387,7 @@ class PipelineInput: # Send audio in the buffer first to speech-to-text, then move on to stt_stream. # This is basically an async itertools.chain. async def buffer_then_audio_stream() -> ( - AsyncGenerator[ProcessedAudioChunk, None] + AsyncGenerator[ProcessedAudioChunk] ): # Buffered audio for chunk in stt_audio_buffer: diff --git a/homeassistant/components/assist_pipeline/websocket_api.py b/homeassistant/components/assist_pipeline/websocket_api.py index 3e8cdf6fa42..56effd50a3e 100644 --- a/homeassistant/components/assist_pipeline/websocket_api.py +++ b/homeassistant/components/assist_pipeline/websocket_api.py @@ -5,12 +5,13 @@ import asyncio # Suppressing disable=deprecated-module is needed for Python 3.11 import audioop # pylint: disable=deprecated-module import base64 -from collections.abc import AsyncGenerator, Callable +from collections.abc import Callable import contextlib import logging import math from typing import Any, Final +from typing_extensions import AsyncGenerator import voluptuous as vol from homeassistant.components import conversation, stt, tts, websocket_api @@ -165,7 +166,7 @@ async def websocket_run( elif start_stage == PipelineStage.STT: wake_word_phrase = msg["input"].get("wake_word_phrase") - async def stt_stream() -> AsyncGenerator[bytes, None]: + async def stt_stream() -> AsyncGenerator[bytes]: state = None # Yield until we receive an empty chunk diff --git a/homeassistant/components/automation/trace.py b/homeassistant/components/automation/trace.py index e7f671e6f05..08f42167ceb 100644 --- a/homeassistant/components/automation/trace.py +++ b/homeassistant/components/automation/trace.py @@ -2,10 +2,11 @@ from __future__ import annotations -from collections.abc import Generator from contextlib import contextmanager from typing import Any +from typing_extensions import Generator + from homeassistant.components.trace import ( CONF_STORED_TRACES, ActionTrace, @@ -55,7 +56,7 @@ def trace_automation( blueprint_inputs: ConfigType | None, context: Context, trace_config: ConfigType, -) -> Generator[AutomationTrace, None, None]: +) -> Generator[AutomationTrace]: """Trace action execution of automation with automation_id.""" trace = AutomationTrace(automation_id, config, blueprint_inputs, context) async_store_trace(hass, trace, trace_config[CONF_STORED_TRACES]) diff --git a/homeassistant/components/bluetooth/passive_update_coordinator.py b/homeassistant/components/bluetooth/passive_update_coordinator.py index 75e5910554b..524faad510b 100644 --- a/homeassistant/components/bluetooth/passive_update_coordinator.py +++ b/homeassistant/components/bluetooth/passive_update_coordinator.py @@ -15,9 +15,11 @@ from homeassistant.helpers.update_coordinator import ( from .update_coordinator import BasePassiveBluetoothCoordinator if TYPE_CHECKING: - from collections.abc import Callable, Generator + from collections.abc import Callable import logging + from typing_extensions import Generator + from . import BluetoothChange, BluetoothScanningMode, BluetoothServiceInfoBleak _PassiveBluetoothDataUpdateCoordinatorT = TypeVar( @@ -81,7 +83,7 @@ class PassiveBluetoothDataUpdateCoordinator( self._listeners[remove_listener] = (update_callback, context) return remove_listener - def async_contexts(self) -> Generator[Any, None, None]: + def async_contexts(self) -> Generator[Any]: """Return all registered contexts.""" yield from ( context for _, context in self._listeners.values() if context is not None diff --git a/homeassistant/components/ecovacs/controller.py b/homeassistant/components/ecovacs/controller.py index 690f4e56cc9..3e2d2ebdd9a 100644 --- a/homeassistant/components/ecovacs/controller.py +++ b/homeassistant/components/ecovacs/controller.py @@ -2,7 +2,7 @@ from __future__ import annotations -from collections.abc import Generator, Mapping +from collections.abc import Mapping import logging import ssl from typing import Any @@ -18,6 +18,7 @@ from deebot_client.mqtt_client import MqttClient, create_mqtt_config from deebot_client.util import md5 from deebot_client.util.continents import get_continent from sucks import EcoVacsAPI, VacBot +from typing_extensions import Generator from homeassistant.const import CONF_COUNTRY, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant, callback @@ -119,7 +120,7 @@ class EcovacsController: await self._authenticator.teardown() @callback - def devices(self, capability: type[Capabilities]) -> Generator[Device, None, None]: + def devices(self, capability: type[Capabilities]) -> Generator[Device]: """Return generator for devices with a specific capability.""" for device in self._devices: if isinstance(device.capabilities, capability): diff --git a/homeassistant/components/homekit/aidmanager.py b/homeassistant/components/homekit/aidmanager.py index 36df47e8a93..8049c4fd5e2 100644 --- a/homeassistant/components/homekit/aidmanager.py +++ b/homeassistant/components/homekit/aidmanager.py @@ -11,10 +11,10 @@ This module generates and stores them in a HA storage. from __future__ import annotations -from collections.abc import Generator import random from fnv_hash_fast import fnv1a_32 +from typing_extensions import Generator from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_registry as er @@ -39,7 +39,7 @@ def get_system_unique_id(entity: er.RegistryEntry, entity_unique_id: str) -> str return f"{entity.platform}.{entity.domain}.{entity_unique_id}" -def _generate_aids(unique_id: str | None, entity_id: str) -> Generator[int, None, None]: +def _generate_aids(unique_id: str | None, entity_id: str) -> Generator[int]: """Generate accessory aid.""" if unique_id: diff --git a/homeassistant/components/homekit_controller/device_trigger.py b/homeassistant/components/homekit_controller/device_trigger.py index a68241d7fc0..631ba43116a 100644 --- a/homeassistant/components/homekit_controller/device_trigger.py +++ b/homeassistant/components/homekit_controller/device_trigger.py @@ -2,13 +2,14 @@ from __future__ import annotations -from collections.abc import Callable, Generator +from collections.abc import Callable from typing import TYPE_CHECKING, Any from aiohomekit.model.characteristics import CharacteristicsTypes from aiohomekit.model.characteristics.const import InputEventValues from aiohomekit.model.services import Service, ServicesTypes from aiohomekit.utils import clamp_enum_to_char +from typing_extensions import Generator import voluptuous as vol from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA @@ -88,7 +89,7 @@ class TriggerSource: for event_handler in self._callbacks.get(trigger_key, []): event_handler(ev) - def async_get_triggers(self) -> Generator[tuple[str, str], None, None]: + def async_get_triggers(self) -> Generator[tuple[str, str]]: """List device triggers for HomeKit devices.""" yield from self._triggers diff --git a/homeassistant/components/knx/config_flow.py b/homeassistant/components/knx/config_flow.py index af1eee89af7..22c4a647e80 100644 --- a/homeassistant/components/knx/config_flow.py +++ b/homeassistant/components/knx/config_flow.py @@ -3,9 +3,9 @@ from __future__ import annotations from abc import ABC, abstractmethod -from collections.abc import AsyncGenerator from typing import Any, Final +from typing_extensions import AsyncGenerator import voluptuous as vol from xknx import XKNX from xknx.exceptions.exception import ( @@ -118,7 +118,7 @@ class KNXCommonFlow(ABC, ConfigEntryBaseFlow): self._tunnel_endpoints: list[XMLInterface] = [] self._gatewayscanner: GatewayScanner | None = None - self._async_scan_gen: AsyncGenerator[GatewayDescriptor, None] | None = None + self._async_scan_gen: AsyncGenerator[GatewayDescriptor] | None = None @abstractmethod def finish_flow(self) -> ConfigFlowResult: diff --git a/homeassistant/components/logbook/processor.py b/homeassistant/components/logbook/processor.py index e25faf090b6..4e245189154 100644 --- a/homeassistant/components/logbook/processor.py +++ b/homeassistant/components/logbook/processor.py @@ -2,7 +2,7 @@ from __future__ import annotations -from collections.abc import Callable, Generator, Sequence +from collections.abc import Callable, Sequence from contextlib import suppress from dataclasses import dataclass from datetime import datetime as dt @@ -11,6 +11,7 @@ from typing import Any from sqlalchemy.engine import Result from sqlalchemy.engine.row import Row +from typing_extensions import Generator from homeassistant.components.recorder import get_instance from homeassistant.components.recorder.filters import Filters @@ -173,7 +174,7 @@ class EventProcessor: ) def humanify( - self, rows: Generator[EventAsRow, None, None] | Sequence[Row] | Result + self, rows: Generator[EventAsRow] | Sequence[Row] | Result ) -> list[dict[str, str]]: """Humanify rows.""" return list( @@ -189,11 +190,11 @@ class EventProcessor: def _humanify( hass: HomeAssistant, - rows: Generator[EventAsRow, None, None] | Sequence[Row] | Result, + rows: Generator[EventAsRow] | Sequence[Row] | Result, ent_reg: er.EntityRegistry, logbook_run: LogbookRun, context_augmenter: ContextAugmenter, -) -> Generator[dict[str, Any], None, None]: +) -> Generator[dict[str, Any]]: """Generate a converted list of events into entries.""" # Continuous sensors, will be excluded from the logbook continuous_sensors: dict[str, bool] = {} diff --git a/homeassistant/components/matter/discovery.py b/homeassistant/components/matter/discovery.py index e898150e5ed..d69c2393083 100644 --- a/homeassistant/components/matter/discovery.py +++ b/homeassistant/components/matter/discovery.py @@ -2,10 +2,9 @@ from __future__ import annotations -from collections.abc import Generator - from chip.clusters.Objects import ClusterAttributeDescriptor from matter_server.client.models.node import MatterEndpoint +from typing_extensions import Generator from homeassistant.const import Platform from homeassistant.core import callback @@ -36,7 +35,7 @@ SUPPORTED_PLATFORMS = tuple(DISCOVERY_SCHEMAS) @callback -def iter_schemas() -> Generator[MatterDiscoverySchema, None, None]: +def iter_schemas() -> Generator[MatterDiscoverySchema]: """Iterate over all available discovery schemas.""" for platform_schemas in DISCOVERY_SCHEMAS.values(): yield from platform_schemas @@ -45,7 +44,7 @@ def iter_schemas() -> Generator[MatterDiscoverySchema, None, None]: @callback def async_discover_entities( endpoint: MatterEndpoint, -) -> Generator[MatterEntityInfo, None, None]: +) -> Generator[MatterEntityInfo]: """Run discovery on MatterEndpoint and return matching MatterEntityInfo(s).""" discovered_attributes: set[type[ClusterAttributeDescriptor]] = set() device_info = endpoint.device_info diff --git a/homeassistant/components/mqtt/client.py b/homeassistant/components/mqtt/client.py index f01cb9c948f..13f33c44047 100644 --- a/homeassistant/components/mqtt/client.py +++ b/homeassistant/components/mqtt/client.py @@ -4,7 +4,7 @@ from __future__ import annotations import asyncio from collections import defaultdict -from collections.abc import AsyncGenerator, Callable, Coroutine, Iterable +from collections.abc import Callable, Coroutine, Iterable import contextlib from dataclasses import dataclass from functools import lru_cache, partial @@ -18,6 +18,7 @@ from typing import TYPE_CHECKING, Any import uuid import certifi +from typing_extensions import AsyncGenerator from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -521,7 +522,7 @@ class MQTT: self._cleanup_on_unload.pop()() @contextlib.asynccontextmanager - async def _async_connect_in_executor(self) -> AsyncGenerator[None, None]: + async def _async_connect_in_executor(self) -> AsyncGenerator[None]: # While we are connecting in the executor we need to # handle on_socket_open and on_socket_register_write # in the executor as well. diff --git a/homeassistant/components/profiler/__init__.py b/homeassistant/components/profiler/__init__.py index d0e9fc7db75..b9b833647df 100644 --- a/homeassistant/components/profiler/__init__.py +++ b/homeassistant/components/profiler/__init__.py @@ -1,7 +1,6 @@ """The profiler integration.""" import asyncio -from collections.abc import Generator import contextlib from contextlib import suppress from datetime import timedelta @@ -15,6 +14,7 @@ import traceback from typing import Any, cast from lru import LRU +from typing_extensions import Generator import voluptuous as vol from homeassistant.components import persistent_notification @@ -586,7 +586,7 @@ def _log_object_sources( @contextlib.contextmanager -def _increase_repr_limit() -> Generator[None, None, None]: +def _increase_repr_limit() -> Generator[None]: """Increase the repr limit.""" arepr = reprlib.aRepr original_maxstring = arepr.maxstring diff --git a/homeassistant/components/recorder/util.py b/homeassistant/components/recorder/util.py index 5894c8c3ce6..b4ee90a8323 100644 --- a/homeassistant/components/recorder/util.py +++ b/homeassistant/components/recorder/util.py @@ -2,7 +2,7 @@ from __future__ import annotations -from collections.abc import Callable, Generator, Sequence +from collections.abc import Callable, Sequence import contextlib from contextlib import contextmanager from datetime import date, datetime, timedelta @@ -25,6 +25,7 @@ from sqlalchemy.exc import OperationalError, SQLAlchemyError, StatementError from sqlalchemy.orm.query import Query from sqlalchemy.orm.session import Session from sqlalchemy.sql.lambdas import StatementLambdaElement +from typing_extensions import Generator import voluptuous as vol from homeassistant.core import HomeAssistant, callback @@ -118,7 +119,7 @@ def session_scope( session: Session | None = None, exception_filter: Callable[[Exception], bool] | None = None, read_only: bool = False, -) -> Generator[Session, None, None]: +) -> Generator[Session]: """Provide a transactional scope around a series of operations. read_only is used to indicate that the session is only used for reading @@ -714,7 +715,7 @@ def periodic_db_cleanups(instance: Recorder) -> None: @contextmanager -def write_lock_db_sqlite(instance: Recorder) -> Generator[None, None, None]: +def write_lock_db_sqlite(instance: Recorder) -> Generator[None]: """Lock database for writes.""" assert instance.engine is not None with instance.engine.connect() as connection: diff --git a/homeassistant/components/stream/fmp4utils.py b/homeassistant/components/stream/fmp4utils.py index e611e07cd71..e0e3a8ba009 100644 --- a/homeassistant/components/stream/fmp4utils.py +++ b/homeassistant/components/stream/fmp4utils.py @@ -2,9 +2,10 @@ from __future__ import annotations -from collections.abc import Generator from typing import TYPE_CHECKING +from typing_extensions import Generator + from homeassistant.exceptions import HomeAssistantError from .core import Orientation @@ -15,7 +16,7 @@ if TYPE_CHECKING: def find_box( mp4_bytes: bytes, target_type: bytes, box_start: int = 0 -) -> Generator[int, None, None]: +) -> Generator[int]: """Find location of first box (or sub box if box_start provided) of given type.""" if box_start == 0: index = 0 diff --git a/homeassistant/components/stream/worker.py b/homeassistant/components/stream/worker.py index 741dc341880..4fd9b27d02f 100644 --- a/homeassistant/components/stream/worker.py +++ b/homeassistant/components/stream/worker.py @@ -3,7 +3,7 @@ from __future__ import annotations from collections import defaultdict, deque -from collections.abc import Callable, Generator, Iterator, Mapping +from collections.abc import Callable, Iterator, Mapping import contextlib from dataclasses import fields import datetime @@ -13,6 +13,7 @@ from threading import Event from typing import Any, Self, cast import av +from typing_extensions import Generator from homeassistant.core import HomeAssistant from homeassistant.util import dt as dt_util @@ -415,7 +416,7 @@ class PeekIterator(Iterator): self._next = self._iterator.__next__ return self._next() - def peek(self) -> Generator[av.Packet, None, None]: + def peek(self) -> Generator[av.Packet]: """Return items without consuming from the iterator.""" # Items consumed are added to a buffer for future calls to __next__ # or peek. First iterate over the buffer from previous calls to peek. diff --git a/homeassistant/components/unifiprotect/camera.py b/homeassistant/components/unifiprotect/camera.py index 8e10c09872b..7a73c94c535 100644 --- a/homeassistant/components/unifiprotect/camera.py +++ b/homeassistant/components/unifiprotect/camera.py @@ -2,7 +2,6 @@ from __future__ import annotations -from collections.abc import Generator import logging from typing import Any, cast @@ -14,6 +13,7 @@ from pyunifiprotect.data import ( ProtectModelWithId, StateType, ) +from typing_extensions import Generator from homeassistant.components.camera import Camera, CameraEntityFeature from homeassistant.config_entries import ConfigEntry @@ -71,7 +71,7 @@ def _get_camera_channels( entry: ConfigEntry, data: ProtectData, ufp_device: UFPCamera | None = None, -) -> Generator[tuple[UFPCamera, CameraChannel, bool], None, None]: +) -> Generator[tuple[UFPCamera, CameraChannel, bool]]: """Get all the camera channels.""" devices = ( diff --git a/homeassistant/components/unifiprotect/data.py b/homeassistant/components/unifiprotect/data.py index b64a08749d5..52d40d9e89e 100644 --- a/homeassistant/components/unifiprotect/data.py +++ b/homeassistant/components/unifiprotect/data.py @@ -2,7 +2,7 @@ from __future__ import annotations -from collections.abc import Callable, Generator, Iterable +from collections.abc import Callable, Iterable from datetime import datetime, timedelta from functools import partial import logging @@ -22,6 +22,7 @@ from pyunifiprotect.data import ( ) from pyunifiprotect.exceptions import ClientError, NotAuthorized from pyunifiprotect.utils import log_event +from typing_extensions import Generator from homeassistant.config_entries import ConfigEntry from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback @@ -94,7 +95,7 @@ class ProtectData: def get_by_types( self, device_types: Iterable[ModelType], ignore_unadopted: bool = True - ) -> Generator[ProtectAdoptableDeviceModel, None, None]: + ) -> Generator[ProtectAdoptableDeviceModel]: """Get all devices matching types.""" for device_type in device_types: devices = async_get_devices_by_type( diff --git a/homeassistant/components/unifiprotect/utils.py b/homeassistant/components/unifiprotect/utils.py index 8199d729943..4f422a846a3 100644 --- a/homeassistant/components/unifiprotect/utils.py +++ b/homeassistant/components/unifiprotect/utils.py @@ -2,7 +2,7 @@ from __future__ import annotations -from collections.abc import Generator, Iterable +from collections.abc import Iterable import contextlib from enum import Enum from pathlib import Path @@ -19,6 +19,7 @@ from pyunifiprotect.data import ( LightModeType, ProtectAdoptableDeviceModel, ) +from typing_extensions import Generator from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -99,7 +100,7 @@ def async_get_devices_by_type( @callback def async_get_devices( bootstrap: Bootstrap, model_type: Iterable[ModelType] -) -> Generator[ProtectAdoptableDeviceModel, None, None]: +) -> Generator[ProtectAdoptableDeviceModel]: """Return all device by type.""" return ( device diff --git a/homeassistant/components/wemo/entity.py b/homeassistant/components/wemo/entity.py index 809ebcc7a1a..db64aa3137e 100644 --- a/homeassistant/components/wemo/entity.py +++ b/homeassistant/components/wemo/entity.py @@ -2,11 +2,11 @@ from __future__ import annotations -from collections.abc import Generator import contextlib import logging from pywemo.exceptions import ActionException +from typing_extensions import Generator from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -65,7 +65,7 @@ class WemoEntity(CoordinatorEntity[DeviceCoordinator]): return self._device_info @contextlib.contextmanager - def _wemo_call_wrapper(self, message: str) -> Generator[None, None, None]: + def _wemo_call_wrapper(self, message: str) -> Generator[None]: """Wrap calls to the device that change its state. 1. Takes care of making available=False when communications with the diff --git a/homeassistant/components/wyoming/satellite.py b/homeassistant/components/wyoming/satellite.py index 1409925a894..41ca2887d88 100644 --- a/homeassistant/components/wyoming/satellite.py +++ b/homeassistant/components/wyoming/satellite.py @@ -1,12 +1,12 @@ """Support for Wyoming satellite services.""" import asyncio -from collections.abc import AsyncGenerator import io import logging from typing import Final import wave +from typing_extensions import AsyncGenerator from wyoming.asr import Transcribe, Transcript from wyoming.audio import AudioChunk, AudioChunkConverter, AudioStart, AudioStop from wyoming.client import AsyncTcpClient @@ -550,7 +550,7 @@ class WyomingSatellite: await self._client.write_event(AudioStop(timestamp=timestamp).event()) _LOGGER.debug("TTS streaming complete") - async def _stt_stream(self) -> AsyncGenerator[bytes, None]: + async def _stt_stream(self) -> AsyncGenerator[bytes]: """Yield audio chunks from a queue.""" try: is_first_chunk = True diff --git a/homeassistant/components/zwave_js/discovery.py b/homeassistant/components/zwave_js/discovery.py index cc5b96e2963..0dda3d639bc 100644 --- a/homeassistant/components/zwave_js/discovery.py +++ b/homeassistant/components/zwave_js/discovery.py @@ -2,12 +2,12 @@ from __future__ import annotations -from collections.abc import Generator from dataclasses import asdict, dataclass, field from enum import StrEnum from typing import TYPE_CHECKING, Any, cast from awesomeversion import AwesomeVersion +from typing_extensions import Generator from zwave_js_server.const import ( CURRENT_STATE_PROPERTY, CURRENT_VALUE_PROPERTY, @@ -1186,7 +1186,7 @@ DISCOVERY_SCHEMAS = [ @callback def async_discover_node_values( node: ZwaveNode, device: DeviceEntry, discovered_value_ids: dict[str, set[str]] -) -> Generator[ZwaveDiscoveryInfo, None, None]: +) -> Generator[ZwaveDiscoveryInfo]: """Run discovery on ZWave node and return matching (primary) values.""" for value in node.values.values(): # We don't need to rediscover an already processed value_id @@ -1197,7 +1197,7 @@ def async_discover_node_values( @callback def async_discover_single_value( value: ZwaveValue, device: DeviceEntry, discovered_value_ids: dict[str, set[str]] -) -> Generator[ZwaveDiscoveryInfo, None, None]: +) -> Generator[ZwaveDiscoveryInfo]: """Run discovery on a single ZWave value and return matching schema info.""" discovered_value_ids[device.id].add(value.value_id) for schema in DISCOVERY_SCHEMAS: @@ -1318,7 +1318,7 @@ def async_discover_single_value( @callback def async_discover_single_configuration_value( value: ConfigurationValue, -) -> Generator[ZwaveDiscoveryInfo, None, None]: +) -> Generator[ZwaveDiscoveryInfo]: """Run discovery on single Z-Wave configuration value and return schema matches.""" if value.metadata.writeable and value.metadata.readable: if value.configuration_value_type == ConfigurationValueType.ENUMERATED: diff --git a/homeassistant/components/zwave_js/services.py b/homeassistant/components/zwave_js/services.py index ba78777fa51..66d09714723 100644 --- a/homeassistant/components/zwave_js/services.py +++ b/homeassistant/components/zwave_js/services.py @@ -3,11 +3,12 @@ from __future__ import annotations import asyncio -from collections.abc import Collection, Generator, Sequence +from collections.abc import Collection, Sequence import logging import math from typing import Any +from typing_extensions import Generator import voluptuous as vol from zwave_js_server.client import Client as ZwaveClient from zwave_js_server.const import SET_VALUE_SUCCESS, CommandClass, CommandStatus @@ -83,7 +84,7 @@ def broadcast_command(val: dict[str, Any]) -> dict[str, Any]: def get_valid_responses_from_results[_T: ZwaveNode | Endpoint]( zwave_objects: Sequence[_T], results: Sequence[Any] -) -> Generator[tuple[_T, Any], None, None]: +) -> Generator[tuple[_T, Any]]: """Return valid responses from a list of results.""" for zwave_object, result in zip(zwave_objects, results, strict=False): if not isinstance(result, Exception): diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 8da9b50ffa9..eac7f5f25ab 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -4,15 +4,7 @@ from __future__ import annotations import asyncio from collections import UserDict -from collections.abc import ( - Callable, - Coroutine, - Generator, - Hashable, - Iterable, - Mapping, - ValuesView, -) +from collections.abc import Callable, Coroutine, Hashable, Iterable, Mapping, ValuesView from contextvars import ContextVar from copy import deepcopy from enum import Enum, StrEnum @@ -24,7 +16,7 @@ from types import MappingProxyType from typing import TYPE_CHECKING, Any, Generic, Self, cast from async_interrupt import interrupt -from typing_extensions import TypeVar +from typing_extensions import Generator, TypeVar from . import data_entry_flow, loader from .components import persistent_notification @@ -1105,7 +1097,7 @@ class ConfigEntry(Generic[_DataT]): @callback def async_get_active_flows( self, hass: HomeAssistant, sources: set[str] - ) -> Generator[ConfigFlowResult, None, None]: + ) -> Generator[ConfigFlowResult]: """Get any active flows of certain sources for this entry.""" return ( flow diff --git a/homeassistant/exceptions.py b/homeassistant/exceptions.py index 044a41aab7a..01e22d16e79 100644 --- a/homeassistant/exceptions.py +++ b/homeassistant/exceptions.py @@ -2,10 +2,12 @@ from __future__ import annotations -from collections.abc import Callable, Generator, Sequence +from collections.abc import Callable, Sequence from dataclasses import dataclass from typing import TYPE_CHECKING, Any +from typing_extensions import Generator + from .util.event_type import EventType if TYPE_CHECKING: @@ -138,7 +140,7 @@ class ConditionError(HomeAssistantError): """Return indentation.""" return " " * indent + message - def output(self, indent: int) -> Generator[str, None, None]: + def output(self, indent: int) -> Generator[str]: """Yield an indented representation.""" raise NotImplementedError @@ -154,7 +156,7 @@ class ConditionErrorMessage(ConditionError): # A message describing this error message: str - def output(self, indent: int) -> Generator[str, None, None]: + def output(self, indent: int) -> Generator[str]: """Yield an indented representation.""" yield self._indent(indent, f"In '{self.type}' condition: {self.message}") @@ -170,7 +172,7 @@ class ConditionErrorIndex(ConditionError): # The error that this error wraps error: ConditionError - def output(self, indent: int) -> Generator[str, None, None]: + def output(self, indent: int) -> Generator[str]: """Yield an indented representation.""" if self.total > 1: yield self._indent( @@ -189,7 +191,7 @@ class ConditionErrorContainer(ConditionError): # List of ConditionErrors that this error wraps errors: Sequence[ConditionError] - def output(self, indent: int) -> Generator[str, None, None]: + def output(self, indent: int) -> Generator[str]: """Yield an indented representation.""" for item in self.errors: yield from item.output(indent) diff --git a/homeassistant/helpers/condition.py b/homeassistant/helpers/condition.py index bda2f67d803..e15b40a78df 100644 --- a/homeassistant/helpers/condition.py +++ b/homeassistant/helpers/condition.py @@ -4,7 +4,7 @@ from __future__ import annotations import asyncio from collections import deque -from collections.abc import Callable, Container, Generator +from collections.abc import Callable, Container from contextlib import contextmanager from datetime import datetime, time as dt_time, timedelta import functools as ft @@ -12,6 +12,7 @@ import re import sys from typing import Any, Protocol, cast +from typing_extensions import Generator import voluptuous as vol from homeassistant.components import zone as zone_cmp @@ -150,7 +151,7 @@ def condition_trace_update_result(**kwargs: Any) -> None: @contextmanager -def trace_condition(variables: TemplateVarsType) -> Generator[TraceElement, None, None]: +def trace_condition(variables: TemplateVarsType) -> Generator[TraceElement]: """Trace condition evaluation.""" should_pop = True trace_element = trace_stack_top(trace_stack_cv) diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index 4d315f428c3..61cb8852334 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -3,7 +3,7 @@ from __future__ import annotations import asyncio -from collections.abc import AsyncGenerator, Callable, Mapping, Sequence +from collections.abc import Callable, Mapping, Sequence from contextlib import asynccontextmanager from contextvars import ContextVar from copy import copy @@ -16,6 +16,7 @@ from types import MappingProxyType from typing import Any, Literal, TypedDict, cast import async_interrupt +from typing_extensions import AsyncGenerator import voluptuous as vol from homeassistant import exceptions @@ -190,7 +191,7 @@ async def trace_action( script_run: _ScriptRun, stop: asyncio.Future[None], variables: dict[str, Any], -) -> AsyncGenerator[TraceElement, None]: +) -> AsyncGenerator[TraceElement]: """Trace action execution.""" path = trace_path_get() trace_element = action_trace_append(variables, path) diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 314e58290ad..f5c796ef46d 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -6,7 +6,7 @@ from ast import literal_eval import asyncio import base64 import collections.abc -from collections.abc import Callable, Generator, Iterable +from collections.abc import Callable, Iterable from contextlib import AbstractContextManager from contextvars import ContextVar from datetime import date, datetime, time, timedelta @@ -34,6 +34,7 @@ from jinja2.sandbox import ImmutableSandboxedEnvironment from jinja2.utils import Namespace from lru import LRU import orjson +from typing_extensions import Generator import voluptuous as vol from homeassistant.const import ( @@ -882,7 +883,7 @@ class AllStates: if (render_info := _render_info.get()) is not None: render_info.all_states_lifecycle = True - def __iter__(self) -> Generator[TemplateState, None, None]: + def __iter__(self) -> Generator[TemplateState]: """Return all states.""" self._collect_all() return _state_generator(self._hass, None) @@ -972,7 +973,7 @@ class DomainStates: if (entity_collect := _render_info.get()) is not None: entity_collect.domains_lifecycle.add(self._domain) # type: ignore[attr-defined] - def __iter__(self) -> Generator[TemplateState, None, None]: + def __iter__(self) -> Generator[TemplateState]: """Return the iteration over all the states.""" self._collect_domain() return _state_generator(self._hass, self._domain) @@ -1160,7 +1161,7 @@ def _collect_state(hass: HomeAssistant, entity_id: str) -> None: def _state_generator( hass: HomeAssistant, domain: str | None -) -> Generator[TemplateState, None, None]: +) -> Generator[TemplateState]: """State generator for a domain or all states.""" states = hass.states # If domain is None, we want to iterate over all states, but making diff --git a/homeassistant/helpers/trace.py b/homeassistant/helpers/trace.py index 17019863d9f..6f29ff23bec 100644 --- a/homeassistant/helpers/trace.py +++ b/homeassistant/helpers/trace.py @@ -3,12 +3,14 @@ from __future__ import annotations from collections import deque -from collections.abc import Callable, Coroutine, Generator +from collections.abc import Callable, Coroutine from contextlib import contextmanager from contextvars import ContextVar from functools import wraps from typing import Any +from typing_extensions import Generator + from homeassistant.core import ServiceResponse import homeassistant.util.dt as dt_util @@ -248,7 +250,7 @@ def script_execution_get() -> str | None: @contextmanager -def trace_path(suffix: str | list[str]) -> Generator[None, None, None]: +def trace_path(suffix: str | list[str]) -> Generator[None]: """Go deeper in the config tree. Can not be used as a decorator on couroutine functions. diff --git a/homeassistant/helpers/update_coordinator.py b/homeassistant/helpers/update_coordinator.py index f89ba98181c..8451c69d2b3 100644 --- a/homeassistant/helpers/update_coordinator.py +++ b/homeassistant/helpers/update_coordinator.py @@ -4,7 +4,7 @@ from __future__ import annotations from abc import abstractmethod import asyncio -from collections.abc import Awaitable, Callable, Coroutine, Generator +from collections.abc import Awaitable, Callable, Coroutine from datetime import datetime, timedelta import logging from random import randint @@ -14,7 +14,7 @@ import urllib.error import aiohttp import requests -from typing_extensions import TypeVar +from typing_extensions import Generator, TypeVar from homeassistant import config_entries from homeassistant.const import EVENT_HOMEASSISTANT_STOP @@ -177,7 +177,7 @@ class DataUpdateCoordinator(BaseDataUpdateCoordinatorProtocol, Generic[_DataT]): self._async_unsub_refresh() self._debounced_refresh.async_cancel() - def async_contexts(self) -> Generator[Any, None, None]: + def async_contexts(self) -> Generator[Any]: """Return all registered contexts.""" yield from ( context for _, context in self._listeners.values() if context is not None diff --git a/homeassistant/setup.py b/homeassistant/setup.py index 1f71adaf486..9775a3fee45 100644 --- a/homeassistant/setup.py +++ b/homeassistant/setup.py @@ -4,7 +4,7 @@ from __future__ import annotations import asyncio from collections import defaultdict -from collections.abc import Awaitable, Callable, Generator, Mapping +from collections.abc import Awaitable, Callable, Mapping import contextlib import contextvars from enum import StrEnum @@ -14,6 +14,8 @@ import time from types import ModuleType from typing import Any, Final, TypedDict +from typing_extensions import Generator + from . import config as conf_util, core, loader, requirements from .const import ( BASE_PLATFORMS, # noqa: F401 @@ -674,9 +676,7 @@ def _setup_started( @contextlib.contextmanager -def async_pause_setup( - hass: core.HomeAssistant, phase: SetupPhases -) -> Generator[None, None, None]: +def async_pause_setup(hass: core.HomeAssistant, phase: SetupPhases) -> Generator[None]: """Keep track of time we are blocked waiting for other operations. We want to count the time we wait for importing and @@ -724,7 +724,7 @@ def async_start_setup( integration: str, phase: SetupPhases, group: str | None = None, -) -> Generator[None, None, None]: +) -> Generator[None]: """Keep track of when setup starts and finishes. :param hass: Home Assistant instance diff --git a/script/scaffold/templates/config_flow/tests/conftest.py b/script/scaffold/templates/config_flow/tests/conftest.py index 84b6bb381bf..fc217636705 100644 --- a/script/scaffold/templates/config_flow/tests/conftest.py +++ b/script/scaffold/templates/config_flow/tests/conftest.py @@ -1,13 +1,13 @@ """Common fixtures for the NEW_NAME tests.""" -from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest +from typing_extensions import Generator @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Override async_setup_entry.""" with patch( "homeassistant.components.NEW_DOMAIN.async_setup_entry", return_value=True diff --git a/script/scaffold/templates/config_flow_helper/tests/conftest.py b/script/scaffold/templates/config_flow_helper/tests/conftest.py index 84b6bb381bf..fc217636705 100644 --- a/script/scaffold/templates/config_flow_helper/tests/conftest.py +++ b/script/scaffold/templates/config_flow_helper/tests/conftest.py @@ -1,13 +1,13 @@ """Common fixtures for the NEW_NAME tests.""" -from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest +from typing_extensions import Generator @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Override async_setup_entry.""" with patch( "homeassistant.components.NEW_DOMAIN.async_setup_entry", return_value=True From c3456215b853474f0a715f9e0d90341a4cddcce6 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Thu, 6 Jun 2024 17:11:49 +0200 Subject: [PATCH 0295/1445] Update requests to 2.32.3 (#118868) Co-authored-by: Robert Resch --- homeassistant/package_constraints.txt | 2 +- pyproject.toml | 2 +- requirements.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 5fce2838b1d..12fc76335d8 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -53,7 +53,7 @@ python-slugify==8.0.4 PyTurboJPEG==1.7.1 pyudev==0.24.1 PyYAML==6.0.1 -requests==2.31.0 +requests==2.32.3 SQLAlchemy==2.0.30 typing-extensions>=4.12.1,<5.0 ulid-transform==0.9.0 diff --git a/pyproject.toml b/pyproject.toml index 58ce5128ad6..beda86314a7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -59,7 +59,7 @@ dependencies = [ "psutil-home-assistant==0.0.1", "python-slugify==8.0.4", "PyYAML==6.0.1", - "requests==2.31.0", + "requests==2.32.3", "SQLAlchemy==2.0.30", "typing-extensions>=4.12.1,<5.0", "ulid-transform==0.9.0", diff --git a/requirements.txt b/requirements.txt index ebb78cdf9d1..21da099bcb5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -34,7 +34,7 @@ pip>=21.3.1 psutil-home-assistant==0.0.1 python-slugify==8.0.4 PyYAML==6.0.1 -requests==2.31.0 +requests==2.32.3 SQLAlchemy==2.0.30 typing-extensions>=4.12.1,<5.0 ulid-transform==0.9.0 From 49c7b1aab920ed681766c00c80a291114be14aff Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Thu, 6 Jun 2024 17:14:02 +0200 Subject: [PATCH 0296/1445] Bump `imgw-pib` library to version `1.0.4` (#118978) Co-authored-by: Maciej Bieniek <478555+bieniu@users.noreply.github.com> --- homeassistant/components/imgw_pib/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/imgw_pib/manifest.json b/homeassistant/components/imgw_pib/manifest.json index 9a9994a73e5..fe714691f13 100644 --- a/homeassistant/components/imgw_pib/manifest.json +++ b/homeassistant/components/imgw_pib/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/imgw_pib", "iot_class": "cloud_polling", "quality_scale": "platinum", - "requirements": ["imgw_pib==1.0.2"] + "requirements": ["imgw_pib==1.0.4"] } diff --git a/requirements_all.txt b/requirements_all.txt index bef4971b555..89318c7c522 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1146,7 +1146,7 @@ iglo==1.2.7 ihcsdk==2.8.5 # homeassistant.components.imgw_pib -imgw_pib==1.0.2 +imgw_pib==1.0.4 # homeassistant.components.incomfort incomfort-client==0.5.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ce3e7cc0160..f5a2abf737e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -933,7 +933,7 @@ idasen-ha==2.5.3 ifaddr==0.2.0 # homeassistant.components.imgw_pib -imgw_pib==1.0.2 +imgw_pib==1.0.4 # homeassistant.components.incomfort incomfort-client==0.5.0 From 279483ddb0fd374c822fba13d619a4559b44bc20 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Thu, 6 Jun 2024 17:24:22 +0200 Subject: [PATCH 0297/1445] Import Generator from typing_extensions (2) (#118989) --- tests/common.py | 11 ++- tests/components/abode/conftest.py | 4 +- tests/components/accuweather/conftest.py | 4 +- tests/components/aemet/conftest.py | 4 +- tests/components/aftership/conftest.py | 4 +- tests/components/agent_dvr/conftest.py | 4 +- tests/components/airgradient/conftest.py | 10 +-- tests/components/airq/conftest.py | 4 +- tests/components/airtouch5/conftest.py | 4 +- tests/components/airvisual/conftest.py | 4 +- tests/components/airvisual_pro/conftest.py | 4 +- tests/components/aladdin_connect/conftest.py | 4 +- .../alarm_control_panel/conftest.py | 4 +- tests/components/amberelectric/conftest.py | 4 +- tests/components/ambient_network/conftest.py | 4 +- .../components/analytics_insights/conftest.py | 6 +- tests/components/androidtv_remote/conftest.py | 7 +- tests/components/aosmith/conftest.py | 6 +- tests/components/aosmith/test_sensor.py | 4 +- tests/components/aosmith/test_water_heater.py | 4 +- .../application_credentials/test_init.py | 5 +- tests/components/aprs/test_device_tracker.py | 4 +- tests/components/apsystems/conftest.py | 4 +- tests/components/arve/conftest.py | 4 +- tests/components/assist_pipeline/conftest.py | 5 +- .../assist_pipeline/test_pipeline.py | 4 +- tests/components/asterisk_mbox/test_init.py | 4 +- tests/components/atag/conftest.py | 4 +- tests/components/aurora/conftest.py | 6 +- tests/components/axis/conftest.py | 7 +- .../azure_data_explorer/conftest.py | 4 +- tests/components/azure_devops/conftest.py | 6 +- tests/components/balboa/conftest.py | 5 +- tests/components/bang_olufsen/conftest.py | 4 +- tests/components/binary_sensor/test_init.py | 4 +- tests/components/blueprint/common.py | 5 +- tests/components/bluetooth/conftest.py | 10 +-- tests/components/bluetooth/test_manager.py | 6 +- .../bmw_connected_drive/conftest.py | 5 +- tests/components/braviatv/conftest.py | 4 +- tests/components/bring/conftest.py | 6 +- tests/components/brother/conftest.py | 6 +- .../components/brottsplatskartan/conftest.py | 6 +- tests/components/brunt/conftest.py | 4 +- tests/components/bsblan/conftest.py | 4 +- tests/components/buienradar/conftest.py | 4 +- tests/components/button/test_init.py | 4 +- tests/components/caldav/test_config_flow.py | 4 +- tests/components/calendar/conftest.py | 4 +- tests/components/calendar/test_init.py | 4 +- tests/components/calendar/test_trigger.py | 7 +- tests/components/ccm15/conftest.py | 8 +-- tests/components/cert_expiry/conftest.py | 4 +- tests/components/climate/conftest.py | 5 +- tests/components/climate/test_intent.py | 4 +- tests/components/cloud/conftest.py | 5 +- tests/components/cloud/test_binary_sensor.py | 4 +- tests/components/cloud/test_stt.py | 4 +- tests/components/cloud/test_tts.py | 5 +- tests/components/conftest.py | 21 +++--- tests/components/cpuspeed/conftest.py | 8 +-- .../components/crownstone/test_config_flow.py | 4 +- .../device_tracker/test_config_entry.py | 4 +- .../devolo_home_control/conftest.py | 6 +- tests/components/discovergy/conftest.py | 4 +- tests/components/dlink/conftest.py | 7 +- tests/components/duotecno/conftest.py | 4 +- .../dwd_weather_warnings/conftest.py | 6 +- tests/components/easyenergy/conftest.py | 6 +- tests/components/ecoforest/conftest.py | 4 +- tests/components/ecovacs/conftest.py | 12 ++-- tests/components/edl21/conftest.py | 4 +- tests/components/electric_kiwi/conftest.py | 7 +- tests/components/elgato/conftest.py | 4 +- tests/components/elmax/conftest.py | 8 +-- tests/components/elvia/conftest.py | 4 +- tests/components/emulated_hue/test_upnp.py | 4 +- .../energenie_power_sockets/conftest.py | 6 +- tests/components/energyzero/conftest.py | 6 +- tests/components/event/test_init.py | 4 +- tests/components/fibaro/conftest.py | 6 +- tests/components/file/conftest.py | 4 +- tests/components/filesize/conftest.py | 4 +- tests/components/fitbit/conftest.py | 5 +- tests/components/flexit_bacnet/conftest.py | 6 +- tests/components/folder_watcher/conftest.py | 4 +- tests/components/forecast_solar/conftest.py | 4 +- tests/components/freedompro/conftest.py | 4 +- tests/components/frontier_silicon/conftest.py | 10 +-- tests/components/fully_kiosk/conftest.py | 8 +-- tests/components/fyta/conftest.py | 4 +- tests/conftest.py | 71 +++++++++---------- tests/helpers/test_config_entry_flow.py | 6 +- tests/test_bootstrap.py | 19 ++--- tests/test_config_entries.py | 4 +- tests/util/yaml/test_init.py | 4 +- 96 files changed, 298 insertions(+), 290 deletions(-) diff --git a/tests/common.py b/tests/common.py index 88d7a86fcf4..21e810be1e8 100644 --- a/tests/common.py +++ b/tests/common.py @@ -3,7 +3,7 @@ from __future__ import annotations import asyncio -from collections.abc import AsyncGenerator, Generator, Mapping, Sequence +from collections.abc import Mapping, Sequence from contextlib import asynccontextmanager, contextmanager from datetime import UTC, datetime, timedelta from enum import Enum @@ -23,6 +23,7 @@ from unittest.mock import AsyncMock, Mock, patch from aiohttp.test_utils import unused_port as get_test_instance_port # noqa: F401 import pytest from syrupy import SnapshotAssertion +from typing_extensions import AsyncGenerator, Generator import voluptuous as vol from homeassistant import auth, bootstrap, config_entries, loader @@ -161,7 +162,7 @@ def get_test_config_dir(*add_path): @contextmanager -def get_test_home_assistant() -> Generator[HomeAssistant, None, None]: +def get_test_home_assistant() -> Generator[HomeAssistant]: """Return a Home Assistant object pointing at test config directory.""" loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) @@ -222,7 +223,7 @@ async def async_test_home_assistant( event_loop: asyncio.AbstractEventLoop | None = None, load_registries: bool = True, config_dir: str | None = None, -) -> AsyncGenerator[HomeAssistant, None]: +) -> AsyncGenerator[HomeAssistant]: """Return a Home Assistant object pointing at test config dir.""" hass = HomeAssistant(config_dir or get_test_config_dir()) store = auth_store.AuthStore(hass) @@ -1325,9 +1326,7 @@ class MockEntity(entity.Entity): @contextmanager -def mock_storage( - data: dict[str, Any] | None = None, -) -> Generator[dict[str, Any], None, None]: +def mock_storage(data: dict[str, Any] | None = None) -> Generator[dict[str, Any]]: """Mock storage. Data is a dict {'key': {'version': version, 'data': data}} diff --git a/tests/components/abode/conftest.py b/tests/components/abode/conftest.py index 8e42dba4d87..21b236540d0 100644 --- a/tests/components/abode/conftest.py +++ b/tests/components/abode/conftest.py @@ -1,18 +1,18 @@ """Configuration for Abode tests.""" -from collections.abc import Generator from unittest.mock import AsyncMock, patch from jaraco.abode.helpers import urls as URL import pytest from requests_mock import Mocker +from typing_extensions import Generator from tests.common import load_fixture from tests.components.light.conftest import mock_light_profiles # noqa: F401 @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Override async_setup_entry.""" with patch( "homeassistant.components.abode.async_setup_entry", return_value=True diff --git a/tests/components/accuweather/conftest.py b/tests/components/accuweather/conftest.py index 959557606c6..3b0006068ea 100644 --- a/tests/components/accuweather/conftest.py +++ b/tests/components/accuweather/conftest.py @@ -1,9 +1,9 @@ """Common fixtures for the AccuWeather tests.""" -from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest +from typing_extensions import Generator from homeassistant.components.accuweather.const import DOMAIN @@ -11,7 +11,7 @@ from tests.common import load_json_array_fixture, load_json_object_fixture @pytest.fixture -def mock_accuweather_client() -> Generator[AsyncMock, None, None]: +def mock_accuweather_client() -> Generator[AsyncMock]: """Mock a AccuWeather client.""" current = load_json_object_fixture("current_conditions_data.json", DOMAIN) forecast = load_json_array_fixture("forecast_data.json", DOMAIN) diff --git a/tests/components/aemet/conftest.py b/tests/components/aemet/conftest.py index ead27103348..aa4f537c7fb 100644 --- a/tests/components/aemet/conftest.py +++ b/tests/components/aemet/conftest.py @@ -1,13 +1,13 @@ """Test fixtures for aemet.""" -from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest +from typing_extensions import Generator @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Override async_setup_entry.""" with patch( "homeassistant.components.aemet.async_setup_entry", return_value=True diff --git a/tests/components/aftership/conftest.py b/tests/components/aftership/conftest.py index 0bea797dce6..1704b099cc2 100644 --- a/tests/components/aftership/conftest.py +++ b/tests/components/aftership/conftest.py @@ -1,13 +1,13 @@ """Common fixtures for the AfterShip tests.""" -from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest +from typing_extensions import Generator @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Override async_setup_entry.""" with patch( "homeassistant.components.aftership.async_setup_entry", return_value=True diff --git a/tests/components/agent_dvr/conftest.py b/tests/components/agent_dvr/conftest.py index e9f719a6eeb..a62e1738850 100644 --- a/tests/components/agent_dvr/conftest.py +++ b/tests/components/agent_dvr/conftest.py @@ -1,13 +1,13 @@ """Test fixtures for Agent DVR.""" -from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest +from typing_extensions import Generator @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Override async_setup_entry.""" with patch( "homeassistant.components.agent_dvr.async_setup_entry", return_value=True diff --git a/tests/components/airgradient/conftest.py b/tests/components/airgradient/conftest.py index d5857fdc46a..c5cc46cc8eb 100644 --- a/tests/components/airgradient/conftest.py +++ b/tests/components/airgradient/conftest.py @@ -1,10 +1,10 @@ """AirGradient tests configuration.""" -from collections.abc import Generator from unittest.mock import patch from airgradient import Config, Measures import pytest +from typing_extensions import Generator from homeassistant.components.airgradient.const import DOMAIN from homeassistant.const import CONF_HOST @@ -14,7 +14,7 @@ from tests.components.smhi.common import AsyncMock @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Override async_setup_entry.""" with patch( "homeassistant.components.airgradient.async_setup_entry", @@ -24,7 +24,7 @@ def mock_setup_entry() -> Generator[AsyncMock, None, None]: @pytest.fixture -def mock_airgradient_client() -> Generator[AsyncMock, None, None]: +def mock_airgradient_client() -> Generator[AsyncMock]: """Mock an AirGradient client.""" with ( patch( @@ -50,7 +50,7 @@ def mock_airgradient_client() -> Generator[AsyncMock, None, None]: @pytest.fixture def mock_new_airgradient_client( mock_airgradient_client: AsyncMock, -) -> Generator[AsyncMock, None, None]: +) -> Generator[AsyncMock]: """Mock a new AirGradient client.""" mock_airgradient_client.get_config.return_value = Config.from_json( load_fixture("get_config.json", DOMAIN) @@ -61,7 +61,7 @@ def mock_new_airgradient_client( @pytest.fixture def mock_cloud_airgradient_client( mock_airgradient_client: AsyncMock, -) -> Generator[AsyncMock, None, None]: +) -> Generator[AsyncMock]: """Mock a cloud AirGradient client.""" mock_airgradient_client.get_config.return_value = Config.from_json( load_fixture("get_config_cloud.json", DOMAIN) diff --git a/tests/components/airq/conftest.py b/tests/components/airq/conftest.py index 647569b63f0..5df032c0308 100644 --- a/tests/components/airq/conftest.py +++ b/tests/components/airq/conftest.py @@ -1,13 +1,13 @@ """Test fixtures for air-Q.""" -from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest +from typing_extensions import Generator @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Override async_setup_entry.""" with patch( "homeassistant.components.airq.async_setup_entry", return_value=True diff --git a/tests/components/airtouch5/conftest.py b/tests/components/airtouch5/conftest.py index 016843e6874..d6d55689f17 100644 --- a/tests/components/airtouch5/conftest.py +++ b/tests/components/airtouch5/conftest.py @@ -1,13 +1,13 @@ """Common fixtures for the Airtouch 5 tests.""" -from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest +from typing_extensions import Generator @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Override async_setup_entry.""" with patch( "homeassistant.components.airtouch5.async_setup_entry", return_value=True diff --git a/tests/components/airvisual/conftest.py b/tests/components/airvisual/conftest.py index 1538af28a08..90e13e2f4be 100644 --- a/tests/components/airvisual/conftest.py +++ b/tests/components/airvisual/conftest.py @@ -1,10 +1,10 @@ """Define test fixtures for AirVisual.""" -from collections.abc import Generator import json from unittest.mock import AsyncMock, Mock, patch import pytest +from typing_extensions import Generator from homeassistant.components.airvisual import ( CONF_CITY, @@ -152,7 +152,7 @@ async def setup_config_entry_fixture(hass, config_entry, mock_pyairvisual): @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Override async_setup_entry.""" with patch( "homeassistant.components.airvisual.async_setup_entry", return_value=True diff --git a/tests/components/airvisual_pro/conftest.py b/tests/components/airvisual_pro/conftest.py index 164264634b8..d81d7471cac 100644 --- a/tests/components/airvisual_pro/conftest.py +++ b/tests/components/airvisual_pro/conftest.py @@ -1,10 +1,10 @@ """Define test fixtures for AirVisual Pro.""" -from collections.abc import Generator import json from unittest.mock import AsyncMock, Mock, patch import pytest +from typing_extensions import Generator from homeassistant.components.airvisual_pro.const import DOMAIN from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD @@ -14,7 +14,7 @@ from tests.common import MockConfigEntry, load_fixture @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Override async_setup_entry.""" with patch( "homeassistant.components.airvisual_pro.async_setup_entry", return_value=True diff --git a/tests/components/aladdin_connect/conftest.py b/tests/components/aladdin_connect/conftest.py index a3f8ae417e1..c7e5190d527 100644 --- a/tests/components/aladdin_connect/conftest.py +++ b/tests/components/aladdin_connect/conftest.py @@ -1,9 +1,9 @@ """Test fixtures for the Aladdin Connect Garage Door integration.""" -from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest +from typing_extensions import Generator from homeassistant.components.aladdin_connect import DOMAIN @@ -11,7 +11,7 @@ from tests.common import MockConfigEntry @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Override async_setup_entry.""" with patch( "homeassistant.components.aladdin_connect.async_setup_entry", return_value=True diff --git a/tests/components/alarm_control_panel/conftest.py b/tests/components/alarm_control_panel/conftest.py index 9cb832abca0..34a4b483e3b 100644 --- a/tests/components/alarm_control_panel/conftest.py +++ b/tests/components/alarm_control_panel/conftest.py @@ -1,9 +1,9 @@ """Fixturs for Alarm Control Panel tests.""" -from collections.abc import Generator from unittest.mock import MagicMock import pytest +from typing_extensions import Generator from homeassistant.components.alarm_control_panel import ( DOMAIN as ALARM_CONTROL_PANEL_DOMAIN, @@ -108,7 +108,7 @@ class MockFlow(ConfigFlow): @pytest.fixture(autouse=True) -def config_flow_fixture(hass: HomeAssistant) -> Generator[None, None, None]: +def config_flow_fixture(hass: HomeAssistant) -> Generator[None]: """Mock config flow.""" mock_platform(hass, f"{TEST_DOMAIN}.config_flow") diff --git a/tests/components/amberelectric/conftest.py b/tests/components/amberelectric/conftest.py index 8912c248a71..9de865fae6c 100644 --- a/tests/components/amberelectric/conftest.py +++ b/tests/components/amberelectric/conftest.py @@ -1,13 +1,13 @@ """Provide common Amber fixtures.""" -from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest +from typing_extensions import Generator @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Override async_setup_entry.""" with patch( "homeassistant.components.amberelectric.async_setup_entry", return_value=True diff --git a/tests/components/ambient_network/conftest.py b/tests/components/ambient_network/conftest.py index 6da3add4fd8..2900f8ae5fe 100644 --- a/tests/components/ambient_network/conftest.py +++ b/tests/components/ambient_network/conftest.py @@ -1,11 +1,11 @@ """Common fixtures for the Ambient Weather Network integration tests.""" -from collections.abc import Generator from typing import Any from unittest.mock import AsyncMock, Mock, patch from aioambient import OpenAPI import pytest +from typing_extensions import Generator from homeassistant.components import ambient_network from homeassistant.core import HomeAssistant @@ -18,7 +18,7 @@ from tests.common import ( @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Override async_setup_entry.""" with patch( "homeassistant.components.ambient_network.async_setup_entry", return_value=True diff --git a/tests/components/analytics_insights/conftest.py b/tests/components/analytics_insights/conftest.py index 51d25f0a2cc..75d47c41f4e 100644 --- a/tests/components/analytics_insights/conftest.py +++ b/tests/components/analytics_insights/conftest.py @@ -1,11 +1,11 @@ """Common fixtures for the Homeassistant Analytics tests.""" -from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest from python_homeassistant_analytics import CurrentAnalytics from python_homeassistant_analytics.models import CustomIntegration, Integration +from typing_extensions import Generator from homeassistant.components.analytics_insights.const import ( CONF_TRACKED_CUSTOM_INTEGRATIONS, @@ -17,7 +17,7 @@ from tests.common import MockConfigEntry, load_fixture, load_json_object_fixture @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Override async_setup_entry.""" with patch( "homeassistant.components.analytics_insights.async_setup_entry", @@ -27,7 +27,7 @@ def mock_setup_entry() -> Generator[AsyncMock, None, None]: @pytest.fixture -def mock_analytics_client() -> Generator[AsyncMock, None, None]: +def mock_analytics_client() -> Generator[AsyncMock]: """Mock a Homeassistant Analytics client.""" with ( patch( diff --git a/tests/components/androidtv_remote/conftest.py b/tests/components/androidtv_remote/conftest.py index 3b69da6d742..7855e1cefb3 100644 --- a/tests/components/androidtv_remote/conftest.py +++ b/tests/components/androidtv_remote/conftest.py @@ -1,9 +1,10 @@ """Fixtures for the Android TV Remote integration tests.""" -from collections.abc import Callable, Generator +from collections.abc import Callable from unittest.mock import AsyncMock, MagicMock, patch import pytest +from typing_extensions import Generator from homeassistant.components.androidtv_remote.const import DOMAIN from homeassistant.config_entries import ConfigEntryState @@ -12,7 +13,7 @@ from tests.common import MockConfigEntry @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Mock setting up a config entry.""" with patch( "homeassistant.components.androidtv_remote.async_setup_entry", @@ -22,7 +23,7 @@ def mock_setup_entry() -> Generator[AsyncMock, None, None]: @pytest.fixture -def mock_unload_entry() -> Generator[AsyncMock, None, None]: +def mock_unload_entry() -> Generator[AsyncMock]: """Mock unloading a config entry.""" with patch( "homeassistant.components.androidtv_remote.async_unload_entry", diff --git a/tests/components/aosmith/conftest.py b/tests/components/aosmith/conftest.py index 74e995deaf1..d67ae1ea627 100644 --- a/tests/components/aosmith/conftest.py +++ b/tests/components/aosmith/conftest.py @@ -1,6 +1,5 @@ """Common fixtures for the A. O. Smith tests.""" -from collections.abc import Generator from unittest.mock import AsyncMock, MagicMock, patch from py_aosmith import AOSmithAPIClient @@ -15,6 +14,7 @@ from py_aosmith.models import ( SupportedOperationModeInfo, ) import pytest +from typing_extensions import Generator from homeassistant.components.aosmith.const import DOMAIN from homeassistant.const import CONF_EMAIL, CONF_PASSWORD @@ -128,7 +128,7 @@ def mock_config_entry() -> MockConfigEntry: @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Override async_setup_entry.""" with patch( "homeassistant.components.aosmith.async_setup_entry", return_value=True @@ -166,7 +166,7 @@ async def mock_client( get_devices_fixture_mode_pending: bool, get_devices_fixture_setpoint_pending: bool, get_devices_fixture_has_vacation_mode: bool, -) -> Generator[MagicMock, None, None]: +) -> Generator[MagicMock]: """Return a mocked client.""" get_devices_fixture = [ build_device_fixture( diff --git a/tests/components/aosmith/test_sensor.py b/tests/components/aosmith/test_sensor.py index d6acd8865d8..a77e4e4576d 100644 --- a/tests/components/aosmith/test_sensor.py +++ b/tests/components/aosmith/test_sensor.py @@ -1,10 +1,10 @@ """Tests for the sensor platform of the A. O. Smith integration.""" -from collections.abc import AsyncGenerator from unittest.mock import patch import pytest from syrupy.assertion import SnapshotAssertion +from typing_extensions import AsyncGenerator from homeassistant.const import Platform from homeassistant.core import HomeAssistant @@ -14,7 +14,7 @@ from tests.common import MockConfigEntry, snapshot_platform @pytest.fixture(autouse=True) -async def platforms() -> AsyncGenerator[list[str], None]: +async def platforms() -> AsyncGenerator[list[str]]: """Return the platforms to be loaded for this test.""" with patch("homeassistant.components.aosmith.PLATFORMS", [Platform.SENSOR]): yield diff --git a/tests/components/aosmith/test_water_heater.py b/tests/components/aosmith/test_water_heater.py index 567121ac0b0..ab4a4a33bca 100644 --- a/tests/components/aosmith/test_water_heater.py +++ b/tests/components/aosmith/test_water_heater.py @@ -1,11 +1,11 @@ """Tests for the water heater platform of the A. O. Smith integration.""" -from collections.abc import AsyncGenerator from unittest.mock import MagicMock, patch from py_aosmith.models import OperationMode import pytest from syrupy.assertion import SnapshotAssertion +from typing_extensions import AsyncGenerator from homeassistant.components.water_heater import ( ATTR_AWAY_MODE, @@ -29,7 +29,7 @@ from tests.common import MockConfigEntry, snapshot_platform @pytest.fixture(autouse=True) -async def platforms() -> AsyncGenerator[list[str], None]: +async def platforms() -> AsyncGenerator[list[str]]: """Return the platforms to be loaded for this test.""" with patch("homeassistant.components.aosmith.PLATFORMS", [Platform.WATER_HEATER]): yield diff --git a/tests/components/application_credentials/test_init.py b/tests/components/application_credentials/test_init.py index d22b736b39b..c427b1d07e0 100644 --- a/tests/components/application_credentials/test_init.py +++ b/tests/components/application_credentials/test_init.py @@ -2,12 +2,13 @@ from __future__ import annotations -from collections.abc import Callable, Generator +from collections.abc import Callable import logging from typing import Any from unittest.mock import AsyncMock, Mock, patch import pytest +from typing_extensions import Generator from homeassistant import config_entries, data_entry_flow from homeassistant.components.application_credentials import ( @@ -114,7 +115,7 @@ class FakeConfigFlow(config_entry_oauth2_flow.AbstractOAuth2FlowHandler): @pytest.fixture(autouse=True) def config_flow_handler( hass: HomeAssistant, current_request_with_host: None -) -> Generator[None, None, None]: +) -> Generator[None]: """Fixture for a test config flow.""" mock_platform(hass, f"{TEST_DOMAIN}.config_flow") with mock_config_flow(TEST_DOMAIN, FakeConfigFlow): diff --git a/tests/components/aprs/test_device_tracker.py b/tests/components/aprs/test_device_tracker.py index 5967bf18c4e..4cdff41598f 100644 --- a/tests/components/aprs/test_device_tracker.py +++ b/tests/components/aprs/test_device_tracker.py @@ -1,11 +1,11 @@ """Test APRS device tracker.""" -from collections.abc import Generator from unittest.mock import MagicMock, Mock, patch import aprslib from aprslib import IS import pytest +from typing_extensions import Generator from homeassistant.components.aprs import device_tracker from homeassistant.core import HomeAssistant @@ -20,7 +20,7 @@ TEST_PASSWORD = "testpass" @pytest.fixture(name="mock_ais") -def mock_ais() -> Generator[MagicMock, None, None]: +def mock_ais() -> Generator[MagicMock]: """Mock aprslib.""" with patch("aprslib.IS") as mock_ais: yield mock_ais diff --git a/tests/components/apsystems/conftest.py b/tests/components/apsystems/conftest.py index a1f8e78f89e..ab8b7db5a75 100644 --- a/tests/components/apsystems/conftest.py +++ b/tests/components/apsystems/conftest.py @@ -1,13 +1,13 @@ """Common fixtures for the APsystems Local API tests.""" -from collections.abc import Generator from unittest.mock import AsyncMock, MagicMock, patch import pytest +from typing_extensions import Generator @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Override async_setup_entry.""" with patch( "homeassistant.components.apsystems.async_setup_entry", diff --git a/tests/components/arve/conftest.py b/tests/components/arve/conftest.py index f1dfee8ba41..40a5f98291b 100644 --- a/tests/components/arve/conftest.py +++ b/tests/components/arve/conftest.py @@ -1,10 +1,10 @@ """Common fixtures for the Arve tests.""" -from collections.abc import Generator from unittest.mock import AsyncMock, MagicMock, patch from asyncarve import ArveCustomer, ArveDevices, ArveSensPro, ArveSensProData import pytest +from typing_extensions import Generator from homeassistant.components.arve.const import DOMAIN from homeassistant.core import HomeAssistant @@ -15,7 +15,7 @@ from tests.common import MockConfigEntry @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Override async_setup_entry.""" with patch( "homeassistant.components.arve.async_setup_entry", return_value=True diff --git a/tests/components/assist_pipeline/conftest.py b/tests/components/assist_pipeline/conftest.py index 69d44341f4a..6fba61b0679 100644 --- a/tests/components/assist_pipeline/conftest.py +++ b/tests/components/assist_pipeline/conftest.py @@ -2,12 +2,13 @@ from __future__ import annotations -from collections.abc import AsyncIterable, Generator +from collections.abc import AsyncIterable from pathlib import Path from typing import Any from unittest.mock import AsyncMock import pytest +from typing_extensions import Generator from homeassistant.components import stt, tts, wake_word from homeassistant.components.assist_pipeline import DOMAIN, select as assist_select @@ -272,7 +273,7 @@ class MockFlow(ConfigFlow): @pytest.fixture -def config_flow_fixture(hass: HomeAssistant) -> Generator[None, None, None]: +def config_flow_fixture(hass: HomeAssistant) -> Generator[None]: """Mock config flow.""" mock_platform(hass, "test.config_flow") diff --git a/tests/components/assist_pipeline/test_pipeline.py b/tests/components/assist_pipeline/test_pipeline.py index cf3afff0172..c0b4640b124 100644 --- a/tests/components/assist_pipeline/test_pipeline.py +++ b/tests/components/assist_pipeline/test_pipeline.py @@ -1,10 +1,10 @@ """Websocket tests for Voice Assistant integration.""" -from collections.abc import AsyncGenerator from typing import Any from unittest.mock import ANY, patch import pytest +from typing_extensions import AsyncGenerator from homeassistant.components import conversation from homeassistant.components.assist_pipeline.const import DOMAIN @@ -32,7 +32,7 @@ from tests.common import flush_store @pytest.fixture(autouse=True) -async def delay_save_fixture() -> AsyncGenerator[None, None]: +async def delay_save_fixture() -> AsyncGenerator[None]: """Load the homeassistant integration.""" with patch("homeassistant.helpers.collection.SAVE_DELAY", new=0): yield diff --git a/tests/components/asterisk_mbox/test_init.py b/tests/components/asterisk_mbox/test_init.py index 9c6bbf01f0e..4800ada0ec4 100644 --- a/tests/components/asterisk_mbox/test_init.py +++ b/tests/components/asterisk_mbox/test_init.py @@ -1,9 +1,9 @@ """Test mailbox.""" -from collections.abc import Generator from unittest.mock import Mock, patch import pytest +from typing_extensions import Generator from homeassistant.components.asterisk_mbox import DOMAIN from homeassistant.core import HomeAssistant @@ -14,7 +14,7 @@ from .const import CONFIG @pytest.fixture -def client() -> Generator[Mock, None, None]: +def client() -> Generator[Mock]: """Mock client.""" with patch( "homeassistant.components.asterisk_mbox.asteriskClient", autospec=True diff --git a/tests/components/atag/conftest.py b/tests/components/atag/conftest.py index 567b835d8b4..83ba3e37aad 100644 --- a/tests/components/atag/conftest.py +++ b/tests/components/atag/conftest.py @@ -1,14 +1,14 @@ """Provide common Atag fixtures.""" import asyncio -from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest +from typing_extensions import Generator @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Override async_setup_entry.""" with patch( "homeassistant.components.atag.async_setup_entry", return_value=True diff --git a/tests/components/aurora/conftest.py b/tests/components/aurora/conftest.py index f4236ae8a1c..916f0925c4a 100644 --- a/tests/components/aurora/conftest.py +++ b/tests/components/aurora/conftest.py @@ -1,9 +1,9 @@ """Common fixtures for the Aurora tests.""" -from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest +from typing_extensions import Generator from homeassistant.components.aurora.const import CONF_THRESHOLD, DOMAIN from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE @@ -12,7 +12,7 @@ from tests.common import MockConfigEntry @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Override async_setup_entry.""" with patch( "homeassistant.components.aurora.async_setup_entry", @@ -22,7 +22,7 @@ def mock_setup_entry() -> Generator[AsyncMock, None, None]: @pytest.fixture -def mock_aurora_client() -> Generator[AsyncMock, None, None]: +def mock_aurora_client() -> Generator[AsyncMock]: """Mock a Homeassistant Analytics client.""" with ( patch( diff --git a/tests/components/axis/conftest.py b/tests/components/axis/conftest.py index 7a4e446a0cc..eba0af91393 100644 --- a/tests/components/axis/conftest.py +++ b/tests/components/axis/conftest.py @@ -2,7 +2,7 @@ from __future__ import annotations -from collections.abc import Callable, Generator +from collections.abc import Callable from copy import deepcopy from types import MappingProxyType from typing import Any @@ -11,6 +11,7 @@ from unittest.mock import AsyncMock, patch from axis.rtsp import Signal, State import pytest import respx +from typing_extensions import Generator from homeassistant.components.axis.const import DOMAIN as AXIS_DOMAIN from homeassistant.config_entries import ConfigEntry @@ -49,7 +50,7 @@ from tests.common import MockConfigEntry @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Override async_setup_entry.""" with patch( "homeassistant.components.axis.async_setup_entry", return_value=True @@ -280,7 +281,7 @@ async def setup_config_entry_fixture( @pytest.fixture(autouse=True) -def mock_axis_rtspclient() -> Generator[Callable[[dict | None, str], None], None, None]: +def mock_axis_rtspclient() -> Generator[Callable[[dict | None, str], None]]: """No real RTSP communication allowed.""" with patch("axis.stream_manager.RTSPClient") as rtsp_client_mock: rtsp_client_mock.return_value.session.state = State.STOPPED diff --git a/tests/components/azure_data_explorer/conftest.py b/tests/components/azure_data_explorer/conftest.py index ac05451506f..28743bec719 100644 --- a/tests/components/azure_data_explorer/conftest.py +++ b/tests/components/azure_data_explorer/conftest.py @@ -1,12 +1,12 @@ """Test fixtures for Azure Data Explorer.""" -from collections.abc import Generator from datetime import timedelta import logging from typing import Any from unittest.mock import Mock, patch import pytest +from typing_extensions import Generator from homeassistant.components.azure_data_explorer.const import ( CONF_FILTER, @@ -94,7 +94,7 @@ async def mock_entry_with_one_event( # Fixtures for config_flow tests @pytest.fixture -def mock_setup_entry() -> Generator[MockConfigEntry, None, None]: +def mock_setup_entry() -> Generator[MockConfigEntry]: """Mock the setup entry call, used for config flow tests.""" with patch( f"{AZURE_DATA_EXPLORER_PATH}.async_setup_entry", return_value=True diff --git a/tests/components/azure_devops/conftest.py b/tests/components/azure_devops/conftest.py index d51142cdced..29569da2c90 100644 --- a/tests/components/azure_devops/conftest.py +++ b/tests/components/azure_devops/conftest.py @@ -1,9 +1,9 @@ """Test fixtures for Azure DevOps.""" -from collections.abc import AsyncGenerator, Generator from unittest.mock import AsyncMock, MagicMock, patch import pytest +from typing_extensions import AsyncGenerator, Generator from homeassistant.components.azure_devops.const import DOMAIN @@ -13,7 +13,7 @@ from tests.common import MockConfigEntry @pytest.fixture -async def mock_devops_client() -> AsyncGenerator[MagicMock, None]: +async def mock_devops_client() -> AsyncGenerator[MagicMock]: """Mock the Azure DevOps client.""" with ( @@ -49,7 +49,7 @@ async def mock_config_entry() -> MockConfigEntry: @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Override async_setup_entry.""" with patch( "homeassistant.components.azure_devops.async_setup_entry", diff --git a/tests/components/balboa/conftest.py b/tests/components/balboa/conftest.py index 7f679773f93..fbdc2f8a759 100644 --- a/tests/components/balboa/conftest.py +++ b/tests/components/balboa/conftest.py @@ -2,11 +2,12 @@ from __future__ import annotations -from collections.abc import Callable, Generator +from collections.abc import Callable from unittest.mock import AsyncMock, MagicMock, patch from pybalboa.enums import HeatMode, LowHighRange import pytest +from typing_extensions import Generator from homeassistant.core import HomeAssistant @@ -22,7 +23,7 @@ async def integration_fixture(hass: HomeAssistant) -> MockConfigEntry: @pytest.fixture(name="client") -def client_fixture() -> Generator[MagicMock, None, None]: +def client_fixture() -> Generator[MagicMock]: """Mock balboa spa client.""" with patch( "homeassistant.components.balboa.SpaClient", autospec=True diff --git a/tests/components/bang_olufsen/conftest.py b/tests/components/bang_olufsen/conftest.py index d076316e36c..e77dc4d16a9 100644 --- a/tests/components/bang_olufsen/conftest.py +++ b/tests/components/bang_olufsen/conftest.py @@ -1,10 +1,10 @@ """Test fixtures for bang_olufsen.""" -from collections.abc import Generator from unittest.mock import AsyncMock, patch from mozart_api.models import BeolinkPeer import pytest +from typing_extensions import Generator from homeassistant.components.bang_olufsen.const import DOMAIN @@ -31,7 +31,7 @@ def mock_config_entry(): @pytest.fixture -def mock_mozart_client() -> Generator[AsyncMock, None, None]: +def mock_mozart_client() -> Generator[AsyncMock]: """Mock MozartClient.""" with ( diff --git a/tests/components/binary_sensor/test_init.py b/tests/components/binary_sensor/test_init.py index 63a921b4c3e..8f14063e011 100644 --- a/tests/components/binary_sensor/test_init.py +++ b/tests/components/binary_sensor/test_init.py @@ -1,9 +1,9 @@ """The tests for the Binary sensor component.""" -from collections.abc import Generator from unittest import mock import pytest +from typing_extensions import Generator from homeassistant.components import binary_sensor from homeassistant.config_entries import ConfigEntry, ConfigFlow @@ -48,7 +48,7 @@ class MockFlow(ConfigFlow): @pytest.fixture(autouse=True) -def config_flow_fixture(hass: HomeAssistant) -> Generator[None, None, None]: +def config_flow_fixture(hass: HomeAssistant) -> Generator[None]: """Mock config flow.""" mock_platform(hass, f"{TEST_DOMAIN}.config_flow") diff --git a/tests/components/blueprint/common.py b/tests/components/blueprint/common.py index 45c6a94f401..dd59b6df082 100644 --- a/tests/components/blueprint/common.py +++ b/tests/components/blueprint/common.py @@ -1,10 +1,11 @@ """Blueprints test helpers.""" -from collections.abc import Generator from unittest.mock import patch +from typing_extensions import Generator -def stub_blueprint_populate_fixture_helper() -> Generator[None, None, None]: + +def stub_blueprint_populate_fixture_helper() -> Generator[None]: """Stub copying the blueprints to the config folder.""" with patch( "homeassistant.components.blueprint.models.DomainBlueprints.async_populate" diff --git a/tests/components/bluetooth/conftest.py b/tests/components/bluetooth/conftest.py index b99c1e77eb8..4373ec3f915 100644 --- a/tests/components/bluetooth/conftest.py +++ b/tests/components/bluetooth/conftest.py @@ -1,12 +1,12 @@ """Tests for the bluetooth component.""" -from collections.abc import Generator from unittest.mock import patch from bleak_retry_connector import bleak_manager from dbus_fast.aio import message_bus import habluetooth.util as habluetooth_utils import pytest +from typing_extensions import Generator @pytest.fixture(name="disable_bluez_manager_socket", autouse=True, scope="package") @@ -75,7 +75,7 @@ def mock_operating_system_90(): @pytest.fixture(name="macos_adapter") -def macos_adapter() -> Generator[None, None, None]: +def macos_adapter() -> Generator[None]: """Fixture that mocks the macos adapter.""" with ( patch("bleak.get_platform_scanner_backend_type"), @@ -110,7 +110,7 @@ def windows_adapter(): @pytest.fixture(name="no_adapters") -def no_adapter_fixture() -> Generator[None, None, None]: +def no_adapter_fixture() -> Generator[None]: """Fixture that mocks no adapters on Linux.""" with ( patch( @@ -138,7 +138,7 @@ def no_adapter_fixture() -> Generator[None, None, None]: @pytest.fixture(name="one_adapter") -def one_adapter_fixture() -> Generator[None, None, None]: +def one_adapter_fixture() -> Generator[None]: """Fixture that mocks one adapter on Linux.""" with ( patch( @@ -177,7 +177,7 @@ def one_adapter_fixture() -> Generator[None, None, None]: @pytest.fixture(name="two_adapters") -def two_adapters_fixture() -> Generator[None, None, None]: +def two_adapters_fixture() -> Generator[None]: """Fixture that mocks two adapters on Linux.""" with ( patch( diff --git a/tests/components/bluetooth/test_manager.py b/tests/components/bluetooth/test_manager.py index 5a3b9392ba9..f8cdc654b65 100644 --- a/tests/components/bluetooth/test_manager.py +++ b/tests/components/bluetooth/test_manager.py @@ -1,6 +1,5 @@ """Tests for the Bluetooth integration manager.""" -from collections.abc import Generator from datetime import timedelta import time from typing import Any @@ -10,6 +9,7 @@ from bleak.backends.scanner import AdvertisementData, BLEDevice from bluetooth_adapters import AdvertisementHistory from habluetooth.advertisement_tracker import TRACKER_BUFFERING_WOBBLE_SECONDS import pytest +from typing_extensions import Generator from homeassistant.components import bluetooth from homeassistant.components.bluetooth import ( @@ -54,7 +54,7 @@ from tests.common import async_fire_time_changed, load_fixture @pytest.fixture -def register_hci0_scanner(hass: HomeAssistant) -> Generator[None, None, None]: +def register_hci0_scanner(hass: HomeAssistant) -> Generator[None]: """Register an hci0 scanner.""" hci0_scanner = FakeScanner("hci0", "hci0") cancel = bluetooth.async_register_scanner(hass, hci0_scanner) @@ -63,7 +63,7 @@ def register_hci0_scanner(hass: HomeAssistant) -> Generator[None, None, None]: @pytest.fixture -def register_hci1_scanner(hass: HomeAssistant) -> Generator[None, None, None]: +def register_hci1_scanner(hass: HomeAssistant) -> Generator[None]: """Register an hci1 scanner.""" hci1_scanner = FakeScanner("hci1", "hci1") cancel = bluetooth.async_register_scanner(hass, hci1_scanner) diff --git a/tests/components/bmw_connected_drive/conftest.py b/tests/components/bmw_connected_drive/conftest.py index f43a7c089c7..a3db2cea91f 100644 --- a/tests/components/bmw_connected_drive/conftest.py +++ b/tests/components/bmw_connected_drive/conftest.py @@ -1,18 +1,17 @@ """Fixtures for BMW tests.""" -from collections.abc import Generator - from bimmer_connected.tests import ALL_CHARGING_SETTINGS, ALL_PROFILES, ALL_STATES from bimmer_connected.tests.common import MyBMWMockRouter from bimmer_connected.vehicle import remote_services import pytest import respx +from typing_extensions import Generator @pytest.fixture def bmw_fixture( request: pytest.FixtureRequest, monkeypatch: pytest.MonkeyPatch -) -> Generator[respx.MockRouter, None, None]: +) -> Generator[respx.MockRouter]: """Patch MyBMW login API calls.""" # we use the library's mock router to mock the API calls, but only with a subset of vehicles diff --git a/tests/components/braviatv/conftest.py b/tests/components/braviatv/conftest.py index 33f55fbb390..186f4e12337 100644 --- a/tests/components/braviatv/conftest.py +++ b/tests/components/braviatv/conftest.py @@ -1,13 +1,13 @@ """Test fixtures for Bravia TV.""" -from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest +from typing_extensions import Generator @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Override async_setup_entry.""" with patch( "homeassistant.components.braviatv.async_setup_entry", return_value=True diff --git a/tests/components/bring/conftest.py b/tests/components/bring/conftest.py index e399e18dfbe..eef333e07ca 100644 --- a/tests/components/bring/conftest.py +++ b/tests/components/bring/conftest.py @@ -1,9 +1,9 @@ """Common fixtures for the Bring! tests.""" -from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest +from typing_extensions import Generator from homeassistant.components.bring import DOMAIN from homeassistant.const import CONF_EMAIL, CONF_PASSWORD @@ -17,7 +17,7 @@ UUID = "00000000-00000000-00000000-00000000" @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Override async_setup_entry.""" with patch( "homeassistant.components.bring.async_setup_entry", return_value=True @@ -26,7 +26,7 @@ def mock_setup_entry() -> Generator[AsyncMock, None, None]: @pytest.fixture -def mock_bring_client() -> Generator[AsyncMock, None, None]: +def mock_bring_client() -> Generator[AsyncMock]: """Mock a Bring client.""" with ( patch( diff --git a/tests/components/brother/conftest.py b/tests/components/brother/conftest.py index d546df731a9..66f92f5907d 100644 --- a/tests/components/brother/conftest.py +++ b/tests/components/brother/conftest.py @@ -1,11 +1,11 @@ """Test fixtures for brother.""" -from collections.abc import Generator from datetime import UTC, datetime from unittest.mock import AsyncMock, patch from brother import BrotherSensors import pytest +from typing_extensions import Generator from homeassistant.components.brother.const import DOMAIN from homeassistant.const import CONF_HOST, CONF_TYPE @@ -78,7 +78,7 @@ BROTHER_DATA = BrotherSensors( @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Override async_setup_entry.""" with patch( "homeassistant.components.brother.async_setup_entry", return_value=True @@ -87,7 +87,7 @@ def mock_setup_entry() -> Generator[AsyncMock, None, None]: @pytest.fixture -def mock_brother_client() -> Generator[AsyncMock, None, None]: +def mock_brother_client() -> Generator[AsyncMock]: """Mock Brother client.""" with ( patch("homeassistant.components.brother.Brother", autospec=True) as mock_client, diff --git a/tests/components/brottsplatskartan/conftest.py b/tests/components/brottsplatskartan/conftest.py index 6d3769edd71..c10093f18b9 100644 --- a/tests/components/brottsplatskartan/conftest.py +++ b/tests/components/brottsplatskartan/conftest.py @@ -1,13 +1,13 @@ """Test fixtures for Brottplatskartan.""" -from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest +from typing_extensions import Generator @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Override async_setup_entry.""" with patch( "homeassistant.components.brottsplatskartan.async_setup_entry", @@ -17,7 +17,7 @@ def mock_setup_entry() -> Generator[AsyncMock, None, None]: @pytest.fixture(autouse=True) -def uuid_generator() -> Generator[AsyncMock, None, None]: +def uuid_generator() -> Generator[AsyncMock]: """Generate uuid for app-id.""" with patch( "homeassistant.components.brottsplatskartan.config_flow.uuid.getnode", diff --git a/tests/components/brunt/conftest.py b/tests/components/brunt/conftest.py index f9a518292ac..bfbca238446 100644 --- a/tests/components/brunt/conftest.py +++ b/tests/components/brunt/conftest.py @@ -1,13 +1,13 @@ """Configuration for brunt tests.""" -from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest +from typing_extensions import Generator @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Override async_setup_entry.""" with patch( "homeassistant.components.brunt.async_setup_entry", return_value=True diff --git a/tests/components/bsblan/conftest.py b/tests/components/bsblan/conftest.py index a9120832ac4..72d05c58b49 100644 --- a/tests/components/bsblan/conftest.py +++ b/tests/components/bsblan/conftest.py @@ -1,10 +1,10 @@ """Fixtures for BSBLAN integration tests.""" -from collections.abc import Generator from unittest.mock import AsyncMock, MagicMock, patch from bsblan import Device, Info, State import pytest +from typing_extensions import Generator from homeassistant.components.bsblan.const import CONF_PASSKEY, DOMAIN from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME @@ -31,7 +31,7 @@ def mock_config_entry() -> MockConfigEntry: @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Mock setting up a config entry.""" with patch( "homeassistant.components.bsblan.async_setup_entry", return_value=True diff --git a/tests/components/buienradar/conftest.py b/tests/components/buienradar/conftest.py index 616976b292f..7c9027c7715 100644 --- a/tests/components/buienradar/conftest.py +++ b/tests/components/buienradar/conftest.py @@ -1,13 +1,13 @@ """Test fixtures for buienradar2.""" -from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest +from typing_extensions import Generator @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Override async_setup_entry.""" with patch( "homeassistant.components.buienradar.async_setup_entry", return_value=True diff --git a/tests/components/button/test_init.py b/tests/components/button/test_init.py index 02a320ea3fd..583c625e1b2 100644 --- a/tests/components/button/test_init.py +++ b/tests/components/button/test_init.py @@ -1,11 +1,11 @@ """The tests for the Button component.""" -from collections.abc import Generator from datetime import timedelta from unittest.mock import MagicMock from freezegun.api import FrozenDateTimeFactory import pytest +from typing_extensions import Generator from homeassistant.components.button import ( DOMAIN, @@ -121,7 +121,7 @@ class MockFlow(ConfigFlow): @pytest.fixture(autouse=True) -def config_flow_fixture(hass: HomeAssistant) -> Generator[None, None, None]: +def config_flow_fixture(hass: HomeAssistant) -> Generator[None]: """Mock config flow.""" mock_platform(hass, f"{TEST_DOMAIN}.config_flow") diff --git a/tests/components/caldav/test_config_flow.py b/tests/components/caldav/test_config_flow.py index c6d5552c874..7c47ea14607 100644 --- a/tests/components/caldav/test_config_flow.py +++ b/tests/components/caldav/test_config_flow.py @@ -1,11 +1,11 @@ """Test the CalDAV config flow.""" -from collections.abc import Generator from unittest.mock import AsyncMock, Mock, patch from caldav.lib.error import AuthorizationError, DAVError import pytest import requests +from typing_extensions import Generator from homeassistant import config_entries from homeassistant.components.caldav.const import DOMAIN @@ -19,7 +19,7 @@ from tests.common import MockConfigEntry @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Override async_setup_entry.""" with patch( f"homeassistant.components.{DOMAIN}.async_setup_entry", return_value=True diff --git a/tests/components/calendar/conftest.py b/tests/components/calendar/conftest.py index 94a2e72e0f4..83ecaca97d3 100644 --- a/tests/components/calendar/conftest.py +++ b/tests/components/calendar/conftest.py @@ -1,12 +1,12 @@ """Test fixtures for calendar sensor platforms.""" -from collections.abc import Generator import datetime import secrets from typing import Any from unittest.mock import AsyncMock import pytest +from typing_extensions import Generator from homeassistant.components.calendar import DOMAIN, CalendarEntity, CalendarEvent from homeassistant.config_entries import ConfigEntry, ConfigFlow @@ -92,7 +92,7 @@ class MockCalendarEntity(CalendarEntity): @pytest.fixture -def config_flow_fixture(hass: HomeAssistant) -> Generator[None, None, None]: +def config_flow_fixture(hass: HomeAssistant) -> Generator[None]: """Mock config flow.""" mock_platform(hass, f"{TEST_DOMAIN}.config_flow") diff --git a/tests/components/calendar/test_init.py b/tests/components/calendar/test_init.py index 325accae72f..19209574fa9 100644 --- a/tests/components/calendar/test_init.py +++ b/tests/components/calendar/test_init.py @@ -2,7 +2,6 @@ from __future__ import annotations -from collections.abc import Generator from datetime import timedelta from http import HTTPStatus from typing import Any @@ -10,6 +9,7 @@ from typing import Any from freezegun import freeze_time import pytest from syrupy.assertion import SnapshotAssertion +from typing_extensions import Generator import voluptuous as vol from homeassistant.components.calendar import ( @@ -37,7 +37,7 @@ def mock_frozen_time() -> None: @pytest.fixture(autouse=True) -def mock_set_frozen_time(frozen_time: Any) -> Generator[None, None, None]: +def mock_set_frozen_time(frozen_time: Any) -> Generator[None]: """Fixture to freeze time that also can work for other fixtures.""" if not frozen_time: yield diff --git a/tests/components/calendar/test_trigger.py b/tests/components/calendar/test_trigger.py index 3315b780135..3b415d46e63 100644 --- a/tests/components/calendar/test_trigger.py +++ b/tests/components/calendar/test_trigger.py @@ -9,7 +9,7 @@ forward exercising the triggers. from __future__ import annotations -from collections.abc import AsyncIterator, Callable, Generator +from collections.abc import AsyncIterator, Callable from contextlib import asynccontextmanager import datetime import logging @@ -19,6 +19,7 @@ import zoneinfo from freezegun.api import FrozenDateTimeFactory import pytest +from typing_extensions import Generator from homeassistant.components import automation, calendar from homeassistant.components.calendar.trigger import EVENT_END, EVENT_START @@ -86,7 +87,7 @@ class FakeSchedule: @pytest.fixture def fake_schedule( hass: HomeAssistant, freezer: FrozenDateTimeFactory -) -> Generator[FakeSchedule, None, None]: +) -> Generator[FakeSchedule]: """Fixture that tests can use to make fake events.""" # Setup start time for all tests @@ -161,7 +162,7 @@ def calls_data(hass: HomeAssistant) -> Callable[[], list[dict[str, Any]]]: @pytest.fixture(autouse=True) -def mock_update_interval() -> Generator[None, None, None]: +def mock_update_interval() -> Generator[None]: """Fixture to override the update interval for refreshing events.""" with patch( "homeassistant.components.calendar.trigger.UPDATE_INTERVAL", diff --git a/tests/components/ccm15/conftest.py b/tests/components/ccm15/conftest.py index 6098a95b3ce..d6cc66d77dc 100644 --- a/tests/components/ccm15/conftest.py +++ b/tests/components/ccm15/conftest.py @@ -1,14 +1,14 @@ """Common fixtures for the Midea ccm15 AC Controller tests.""" -from collections.abc import Generator from unittest.mock import AsyncMock, patch from ccm15 import CCM15DeviceState, CCM15SlaveDevice import pytest +from typing_extensions import Generator @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Override async_setup_entry.""" with patch( "homeassistant.components.ccm15.async_setup_entry", return_value=True @@ -17,7 +17,7 @@ def mock_setup_entry() -> Generator[AsyncMock, None, None]: @pytest.fixture -def ccm15_device() -> Generator[AsyncMock, None, None]: +def ccm15_device() -> Generator[AsyncMock]: """Mock ccm15 device.""" ccm15_devices = { 0: CCM15SlaveDevice(bytes.fromhex("000000b0b8001b")), @@ -32,7 +32,7 @@ def ccm15_device() -> Generator[AsyncMock, None, None]: @pytest.fixture -def network_failure_ccm15_device() -> Generator[AsyncMock, None, None]: +def network_failure_ccm15_device() -> Generator[AsyncMock]: """Mock empty set of ccm15 device.""" device_state = CCM15DeviceState(devices={}) with patch( diff --git a/tests/components/cert_expiry/conftest.py b/tests/components/cert_expiry/conftest.py index 41c2d90b1a0..2a86c669970 100644 --- a/tests/components/cert_expiry/conftest.py +++ b/tests/components/cert_expiry/conftest.py @@ -1,13 +1,13 @@ """Configuration for cert_expiry tests.""" -from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest +from typing_extensions import Generator @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Override async_setup_entry.""" with patch( "homeassistant.components.cert_expiry.async_setup_entry", return_value=True diff --git a/tests/components/climate/conftest.py b/tests/components/climate/conftest.py index c65414ea68d..a3a6af6e8a3 100644 --- a/tests/components/climate/conftest.py +++ b/tests/components/climate/conftest.py @@ -1,8 +1,7 @@ """Fixtures for Climate platform tests.""" -from collections.abc import Generator - import pytest +from typing_extensions import Generator from homeassistant.config_entries import ConfigFlow from homeassistant.core import HomeAssistant @@ -15,7 +14,7 @@ class MockFlow(ConfigFlow): @pytest.fixture -def config_flow_fixture(hass: HomeAssistant) -> Generator[None, None, None]: +def config_flow_fixture(hass: HomeAssistant) -> Generator[None]: """Mock config flow.""" mock_platform(hass, "test.config_flow") diff --git a/tests/components/climate/test_intent.py b/tests/components/climate/test_intent.py index 8e2ec09650c..cc78d09ff06 100644 --- a/tests/components/climate/test_intent.py +++ b/tests/components/climate/test_intent.py @@ -1,9 +1,9 @@ """Test climate intents.""" -from collections.abc import Generator from unittest.mock import patch import pytest +from typing_extensions import Generator from homeassistant.components.climate import ( DOMAIN, @@ -34,7 +34,7 @@ class MockFlow(ConfigFlow): @pytest.fixture(autouse=True) -def config_flow_fixture(hass: HomeAssistant) -> Generator[None, None, None]: +def config_flow_fixture(hass: HomeAssistant) -> Generator[None]: """Mock config flow.""" mock_platform(hass, f"{TEST_DOMAIN}.config_flow") diff --git a/tests/components/cloud/conftest.py b/tests/components/cloud/conftest.py index 063aa702c88..617492c0416 100644 --- a/tests/components/cloud/conftest.py +++ b/tests/components/cloud/conftest.py @@ -1,6 +1,6 @@ """Fixtures for cloud tests.""" -from collections.abc import AsyncGenerator, Callable, Coroutine +from collections.abc import Callable, Coroutine from pathlib import Path from typing import Any from unittest.mock import DEFAULT, MagicMock, PropertyMock, patch @@ -15,6 +15,7 @@ from hass_nabucasa.remote import RemoteUI from hass_nabucasa.voice import Voice import jwt import pytest +from typing_extensions import AsyncGenerator from homeassistant.components.cloud import CloudClient, const, prefs from homeassistant.core import HomeAssistant @@ -34,7 +35,7 @@ async def load_homeassistant(hass: HomeAssistant) -> None: @pytest.fixture(name="cloud") -async def cloud_fixture() -> AsyncGenerator[MagicMock, None]: +async def cloud_fixture() -> AsyncGenerator[MagicMock]: """Mock the cloud object. See the real hass_nabucasa.Cloud class for how to configure the mock. diff --git a/tests/components/cloud/test_binary_sensor.py b/tests/components/cloud/test_binary_sensor.py index 5e83fa34c3c..789947f3c7d 100644 --- a/tests/components/cloud/test_binary_sensor.py +++ b/tests/components/cloud/test_binary_sensor.py @@ -1,10 +1,10 @@ """Tests for the cloud binary sensor.""" -from collections.abc import Generator from unittest.mock import MagicMock, patch from hass_nabucasa.const import DISPATCH_REMOTE_CONNECT, DISPATCH_REMOTE_DISCONNECT import pytest +from typing_extensions import Generator from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_registry import EntityRegistry @@ -12,7 +12,7 @@ from homeassistant.setup import async_setup_component @pytest.fixture(autouse=True) -def mock_wait_until() -> Generator[None, None, None]: +def mock_wait_until() -> Generator[None]: """Mock WAIT_UNTIL_CHANGE to execute callback immediately.""" with patch("homeassistant.components.cloud.binary_sensor.WAIT_UNTIL_CHANGE", 0): yield diff --git a/tests/components/cloud/test_stt.py b/tests/components/cloud/test_stt.py index 540aa173beb..a20325d6dc3 100644 --- a/tests/components/cloud/test_stt.py +++ b/tests/components/cloud/test_stt.py @@ -1,6 +1,5 @@ """Test the speech-to-text platform for the cloud integration.""" -from collections.abc import AsyncGenerator from copy import deepcopy from http import HTTPStatus from typing import Any @@ -8,6 +7,7 @@ from unittest.mock import AsyncMock, MagicMock, patch from hass_nabucasa.voice import STTResponse, VoiceError import pytest +from typing_extensions import AsyncGenerator from homeassistant.components.assist_pipeline.pipeline import STORAGE_KEY from homeassistant.components.cloud import DOMAIN @@ -21,7 +21,7 @@ from tests.typing import ClientSessionGenerator @pytest.fixture(autouse=True) -async def delay_save_fixture() -> AsyncGenerator[None, None]: +async def delay_save_fixture() -> AsyncGenerator[None]: """Load the homeassistant integration.""" with patch("homeassistant.helpers.collection.SAVE_DELAY", new=0): yield diff --git a/tests/components/cloud/test_tts.py b/tests/components/cloud/test_tts.py index 6e5acdf6aa3..00466d0d177 100644 --- a/tests/components/cloud/test_tts.py +++ b/tests/components/cloud/test_tts.py @@ -1,6 +1,6 @@ """Tests for cloud tts.""" -from collections.abc import AsyncGenerator, Callable, Coroutine +from collections.abc import Callable, Coroutine from copy import deepcopy from http import HTTPStatus from typing import Any @@ -8,6 +8,7 @@ from unittest.mock import AsyncMock, MagicMock, patch from hass_nabucasa.voice import TTS_VOICES, VoiceError, VoiceTokenError import pytest +from typing_extensions import AsyncGenerator import voluptuous as vol from homeassistant.components.assist_pipeline.pipeline import STORAGE_KEY @@ -39,7 +40,7 @@ from tests.typing import ClientSessionGenerator @pytest.fixture(autouse=True) -async def delay_save_fixture() -> AsyncGenerator[None, None]: +async def delay_save_fixture() -> AsyncGenerator[None]: """Load the homeassistant integration.""" with patch("homeassistant.helpers.collection.SAVE_DELAY", new=0): yield diff --git a/tests/components/conftest.py b/tests/components/conftest.py index ee5806dd1a4..e44479873d8 100644 --- a/tests/components/conftest.py +++ b/tests/components/conftest.py @@ -2,12 +2,13 @@ from __future__ import annotations -from collections.abc import Callable, Generator +from collections.abc import Callable from pathlib import Path from typing import TYPE_CHECKING, Any from unittest.mock import MagicMock, patch import pytest +from typing_extensions import Generator from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant @@ -21,7 +22,7 @@ if TYPE_CHECKING: @pytest.fixture(scope="session", autouse=True) -def patch_zeroconf_multiple_catcher() -> Generator[None, None, None]: +def patch_zeroconf_multiple_catcher() -> Generator[None]: """Patch zeroconf wrapper that detects if multiple instances are used.""" with patch( "homeassistant.components.zeroconf.install_multiple_zeroconf_catcher", @@ -31,7 +32,7 @@ def patch_zeroconf_multiple_catcher() -> Generator[None, None, None]: @pytest.fixture(scope="session", autouse=True) -def prevent_io() -> Generator[None, None, None]: +def prevent_io() -> Generator[None]: """Fixture to prevent certain I/O from happening.""" with patch( "homeassistant.components.http.ban.load_yaml_config_file", @@ -40,7 +41,7 @@ def prevent_io() -> Generator[None, None, None]: @pytest.fixture -def entity_registry_enabled_by_default() -> Generator[None, None, None]: +def entity_registry_enabled_by_default() -> Generator[None]: """Test fixture that ensures all entities are enabled in the registry.""" with patch( "homeassistant.helpers.entity.Entity.entity_registry_enabled_default", @@ -51,7 +52,7 @@ def entity_registry_enabled_by_default() -> Generator[None, None, None]: # Blueprint test fixtures @pytest.fixture(name="stub_blueprint_populate") -def stub_blueprint_populate_fixture() -> Generator[None, None, None]: +def stub_blueprint_populate_fixture() -> Generator[None]: """Stub copying the blueprints to the config folder.""" from tests.components.blueprint.common import stub_blueprint_populate_fixture_helper @@ -60,7 +61,7 @@ def stub_blueprint_populate_fixture() -> Generator[None, None, None]: # TTS test fixtures @pytest.fixture(name="mock_tts_get_cache_files") -def mock_tts_get_cache_files_fixture() -> Generator[MagicMock, None, None]: +def mock_tts_get_cache_files_fixture() -> Generator[MagicMock]: """Mock the list TTS cache function.""" from tests.components.tts.common import mock_tts_get_cache_files_fixture_helper @@ -70,7 +71,7 @@ def mock_tts_get_cache_files_fixture() -> Generator[MagicMock, None, None]: @pytest.fixture(name="mock_tts_init_cache_dir") def mock_tts_init_cache_dir_fixture( init_tts_cache_dir_side_effect: Any, -) -> Generator[MagicMock, None, None]: +) -> Generator[MagicMock]: """Mock the TTS cache dir in memory.""" from tests.components.tts.common import mock_tts_init_cache_dir_fixture_helper @@ -93,7 +94,7 @@ def mock_tts_cache_dir_fixture( mock_tts_init_cache_dir: MagicMock, mock_tts_get_cache_files: MagicMock, request: pytest.FixtureRequest, -) -> Generator[Path, None, None]: +) -> Generator[Path]: """Mock the TTS cache dir with empty dir.""" from tests.components.tts.common import mock_tts_cache_dir_fixture_helper @@ -103,7 +104,7 @@ def mock_tts_cache_dir_fixture( @pytest.fixture(name="tts_mutagen_mock") -def tts_mutagen_mock_fixture() -> Generator[MagicMock, None, None]: +def tts_mutagen_mock_fixture() -> Generator[MagicMock]: """Mock writing tags.""" from tests.components.tts.common import tts_mutagen_mock_fixture_helper @@ -121,7 +122,7 @@ def mock_conversation_agent_fixture(hass: HomeAssistant) -> MockAgent: @pytest.fixture(scope="session", autouse=True) -def prevent_ffmpeg_subprocess() -> Generator[None, None, None]: +def prevent_ffmpeg_subprocess() -> Generator[None]: """Prevent ffmpeg from creating a subprocess.""" with patch( "homeassistant.components.ffmpeg.FFVersion.get_version", return_value="6.0" diff --git a/tests/components/cpuspeed/conftest.py b/tests/components/cpuspeed/conftest.py index 82dfb5eac30..e3ea1432659 100644 --- a/tests/components/cpuspeed/conftest.py +++ b/tests/components/cpuspeed/conftest.py @@ -2,10 +2,10 @@ from __future__ import annotations -from collections.abc import Generator from unittest.mock import AsyncMock, MagicMock, patch import pytest +from typing_extensions import Generator from homeassistant.components.cpuspeed.const import DOMAIN from homeassistant.core import HomeAssistant @@ -25,7 +25,7 @@ def mock_config_entry() -> MockConfigEntry: @pytest.fixture -def mock_cpuinfo_config_flow() -> Generator[MagicMock, None, None]: +def mock_cpuinfo_config_flow() -> Generator[MagicMock]: """Return a mocked get_cpu_info. It is only used to check truthy or falsy values, so it is mocked @@ -39,7 +39,7 @@ def mock_cpuinfo_config_flow() -> Generator[MagicMock, None, None]: @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Mock setting up a config entry.""" with patch( "homeassistant.components.cpuspeed.async_setup_entry", return_value=True @@ -48,7 +48,7 @@ def mock_setup_entry() -> Generator[AsyncMock, None, None]: @pytest.fixture -def mock_cpuinfo() -> Generator[MagicMock, None, None]: +def mock_cpuinfo() -> Generator[MagicMock]: """Return a mocked get_cpu_info.""" info = { "hz_actual": (3200000001, 0), diff --git a/tests/components/crownstone/test_config_flow.py b/tests/components/crownstone/test_config_flow.py index d8b2d805c8e..be9086e02da 100644 --- a/tests/components/crownstone/test_config_flow.py +++ b/tests/components/crownstone/test_config_flow.py @@ -2,7 +2,6 @@ from __future__ import annotations -from collections.abc import Generator from unittest.mock import AsyncMock, MagicMock, patch from crownstone_cloud.cloud_models.spheres import Spheres @@ -12,6 +11,7 @@ from crownstone_cloud.exceptions import ( ) import pytest from serial.tools.list_ports_common import ListPortInfo +from typing_extensions import Generator from homeassistant.components import usb from homeassistant.components.crownstone.const import ( @@ -30,7 +30,7 @@ from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry -type MockFixture = Generator[MagicMock | AsyncMock, None, None] +type MockFixture = Generator[MagicMock | AsyncMock] @pytest.fixture(name="crownstone_setup") diff --git a/tests/components/device_tracker/test_config_entry.py b/tests/components/device_tracker/test_config_entry.py index 077e964f0af..45b94012051 100644 --- a/tests/components/device_tracker/test_config_entry.py +++ b/tests/components/device_tracker/test_config_entry.py @@ -1,9 +1,9 @@ """Test Device Tracker config entry things.""" -from collections.abc import Generator from typing import Any import pytest +from typing_extensions import Generator from homeassistant.components.device_tracker import ( ATTR_HOST_NAME, @@ -55,7 +55,7 @@ class MockFlow(ConfigFlow): @pytest.fixture(autouse=True) -def config_flow_fixture(hass: HomeAssistant) -> Generator[None, None, None]: +def config_flow_fixture(hass: HomeAssistant) -> Generator[None]: """Mock config flow.""" mock_platform(hass, f"{TEST_DOMAIN}.config_flow") diff --git a/tests/components/devolo_home_control/conftest.py b/tests/components/devolo_home_control/conftest.py index 6ce9b73ff83..5d67bffddfd 100644 --- a/tests/components/devolo_home_control/conftest.py +++ b/tests/components/devolo_home_control/conftest.py @@ -1,9 +1,9 @@ """Fixtures for tests.""" -from collections.abc import Generator from unittest.mock import patch import pytest +from typing_extensions import Generator @pytest.fixture @@ -19,9 +19,7 @@ def maintenance() -> bool: @pytest.fixture(autouse=True) -def patch_mydevolo( - credentials_valid: bool, maintenance: bool -) -> Generator[None, None, None]: +def patch_mydevolo(credentials_valid: bool, maintenance: bool) -> Generator[None]: """Fixture to patch mydevolo into a desired state.""" with ( patch( diff --git a/tests/components/discovergy/conftest.py b/tests/components/discovergy/conftest.py index 913e33f6367..0d0e68c487a 100644 --- a/tests/components/discovergy/conftest.py +++ b/tests/components/discovergy/conftest.py @@ -1,10 +1,10 @@ """Fixtures for Discovergy integration tests.""" -from collections.abc import Generator from unittest.mock import AsyncMock, patch from pydiscovergy.models import Reading import pytest +from typing_extensions import Generator from homeassistant.components.discovergy.const import DOMAIN from homeassistant.const import CONF_EMAIL, CONF_PASSWORD @@ -25,7 +25,7 @@ def _meter_last_reading(meter_id: str) -> Reading: @pytest.fixture(name="discovergy") -def mock_discovergy() -> Generator[AsyncMock, None, None]: +def mock_discovergy() -> Generator[AsyncMock]: """Mock the pydiscovergy client.""" with ( patch( diff --git a/tests/components/dlink/conftest.py b/tests/components/dlink/conftest.py index c57aaffc1c7..4bbf99000a9 100644 --- a/tests/components/dlink/conftest.py +++ b/tests/components/dlink/conftest.py @@ -1,10 +1,11 @@ """Configure pytest for D-Link tests.""" -from collections.abc import Awaitable, Callable, Generator +from collections.abc import Awaitable, Callable from copy import deepcopy from unittest.mock import MagicMock, patch import pytest +from typing_extensions import Generator from homeassistant.components import dhcp from homeassistant.components.dlink.const import CONF_USE_LEGACY_PROTOCOL, DOMAIN @@ -130,7 +131,7 @@ async def setup_integration( hass: HomeAssistant, config_entry_with_uid: MockConfigEntry, mocked_plug: MagicMock, -) -> Generator[ComponentSetup, None, None]: +) -> Generator[ComponentSetup]: """Set up the D-Link integration in Home Assistant.""" async def func() -> None: @@ -144,7 +145,7 @@ async def setup_integration_legacy( hass: HomeAssistant, config_entry_with_uid: MockConfigEntry, mocked_plug_legacy: MagicMock, -) -> Generator[ComponentSetup, None, None]: +) -> Generator[ComponentSetup]: """Set up the D-Link integration in Home Assistant with different data.""" async def func() -> None: diff --git a/tests/components/duotecno/conftest.py b/tests/components/duotecno/conftest.py index c79210bdfe0..1b6ba8f65e5 100644 --- a/tests/components/duotecno/conftest.py +++ b/tests/components/duotecno/conftest.py @@ -1,13 +1,13 @@ """Common fixtures for the duotecno tests.""" -from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest +from typing_extensions import Generator @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Override async_setup_entry.""" with patch( "homeassistant.components.duotecno.async_setup_entry", return_value=True diff --git a/tests/components/dwd_weather_warnings/conftest.py b/tests/components/dwd_weather_warnings/conftest.py index a2932944cc2..40c8bf3cfa0 100644 --- a/tests/components/dwd_weather_warnings/conftest.py +++ b/tests/components/dwd_weather_warnings/conftest.py @@ -1,9 +1,9 @@ """Configuration for Deutscher Wetterdienst (DWD) Weather Warnings tests.""" -from collections.abc import Generator from unittest.mock import AsyncMock, MagicMock, Mock, patch import pytest +from typing_extensions import Generator from homeassistant.components.dwd_weather_warnings.const import ( ADVANCE_WARNING_SENSOR, @@ -23,7 +23,7 @@ MOCK_CONDITIONS = [CURRENT_WARNING_SENSOR, ADVANCE_WARNING_SENSOR] @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Override async_setup_entry.""" with patch( "homeassistant.components.dwd_weather_warnings.async_setup_entry", @@ -59,7 +59,7 @@ def mock_tracker_entry() -> MockConfigEntry: @pytest.fixture -def mock_dwdwfsapi() -> Generator[MagicMock, None, None]: +def mock_dwdwfsapi() -> Generator[MagicMock]: """Return a mocked dwdwfsapi API client.""" with ( patch( diff --git a/tests/components/easyenergy/conftest.py b/tests/components/easyenergy/conftest.py index dd8abae4d4a..96d356b8906 100644 --- a/tests/components/easyenergy/conftest.py +++ b/tests/components/easyenergy/conftest.py @@ -1,11 +1,11 @@ """Fixtures for easyEnergy integration tests.""" -from collections.abc import Generator import json from unittest.mock import AsyncMock, MagicMock, patch from easyenergy import Electricity, Gas import pytest +from typing_extensions import Generator from homeassistant.components.easyenergy.const import DOMAIN from homeassistant.core import HomeAssistant @@ -14,7 +14,7 @@ from tests.common import MockConfigEntry, load_fixture @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Mock setting up a config entry.""" with patch( "homeassistant.components.easyenergy.async_setup_entry", return_value=True @@ -34,7 +34,7 @@ def mock_config_entry() -> MockConfigEntry: @pytest.fixture -def mock_easyenergy() -> Generator[MagicMock, None, None]: +def mock_easyenergy() -> Generator[MagicMock]: """Return a mocked easyEnergy client.""" with patch( "homeassistant.components.easyenergy.coordinator.EasyEnergy", autospec=True diff --git a/tests/components/ecoforest/conftest.py b/tests/components/ecoforest/conftest.py index 79d1ea7f77b..3eb13e58aee 100644 --- a/tests/components/ecoforest/conftest.py +++ b/tests/components/ecoforest/conftest.py @@ -1,10 +1,10 @@ """Common fixtures for the Ecoforest tests.""" -from collections.abc import Generator from unittest.mock import AsyncMock, Mock, patch from pyecoforest.models.device import Alarm, Device, OperationMode, State import pytest +from typing_extensions import Generator from homeassistant.components.ecoforest import DOMAIN from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME @@ -14,7 +14,7 @@ from tests.common import MockConfigEntry @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Override async_setup_entry.""" with patch( "homeassistant.components.ecoforest.async_setup_entry", return_value=True diff --git a/tests/components/ecovacs/conftest.py b/tests/components/ecovacs/conftest.py index f227b6092fd..8d0033a6bc9 100644 --- a/tests/components/ecovacs/conftest.py +++ b/tests/components/ecovacs/conftest.py @@ -1,6 +1,5 @@ """Common fixtures for the Ecovacs tests.""" -from collections.abc import AsyncGenerator, Generator from typing import Any from unittest.mock import AsyncMock, Mock, patch @@ -10,6 +9,7 @@ from deebot_client.device import Device from deebot_client.exceptions import ApiError from deebot_client.models import Credentials import pytest +from typing_extensions import AsyncGenerator, Generator from homeassistant.components.ecovacs import PLATFORMS from homeassistant.components.ecovacs.const import DOMAIN @@ -23,7 +23,7 @@ from tests.common import MockConfigEntry, load_json_object_fixture @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Override async_setup_entry.""" with patch( "homeassistant.components.ecovacs.async_setup_entry", return_value=True @@ -54,7 +54,7 @@ def device_fixture() -> str: @pytest.fixture -def mock_authenticator(device_fixture: str) -> Generator[Mock, None, None]: +def mock_authenticator(device_fixture: str) -> Generator[Mock]: """Mock the authenticator.""" with ( patch( @@ -99,7 +99,7 @@ def mock_authenticator_authenticate(mock_authenticator: Mock) -> AsyncMock: @pytest.fixture -def mock_mqtt_client(mock_authenticator: Mock) -> Generator[Mock, None, None]: +def mock_mqtt_client(mock_authenticator: Mock) -> Generator[Mock]: """Mock the MQTT client.""" with ( patch( @@ -118,7 +118,7 @@ def mock_mqtt_client(mock_authenticator: Mock) -> Generator[Mock, None, None]: @pytest.fixture -def mock_device_execute() -> Generator[AsyncMock, None, None]: +def mock_device_execute() -> Generator[AsyncMock]: """Mock the device execute function.""" with patch.object( Device, @@ -142,7 +142,7 @@ async def init_integration( mock_mqtt_client: Mock, mock_device_execute: AsyncMock, platforms: Platform | list[Platform], -) -> AsyncGenerator[MockConfigEntry, None]: +) -> AsyncGenerator[MockConfigEntry]: """Set up the Ecovacs integration for testing.""" if not isinstance(platforms, list): platforms = [platforms] diff --git a/tests/components/edl21/conftest.py b/tests/components/edl21/conftest.py index dc64659d2b8..b6af4ea9cef 100644 --- a/tests/components/edl21/conftest.py +++ b/tests/components/edl21/conftest.py @@ -1,13 +1,13 @@ """Define test fixtures for EDL21.""" -from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest +from typing_extensions import Generator @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Override async_setup_entry.""" with patch( "homeassistant.components.edl21.async_setup_entry", return_value=True diff --git a/tests/components/electric_kiwi/conftest.py b/tests/components/electric_kiwi/conftest.py index b1e222cdc46..5d08aa1ba77 100644 --- a/tests/components/electric_kiwi/conftest.py +++ b/tests/components/electric_kiwi/conftest.py @@ -2,12 +2,13 @@ from __future__ import annotations -from collections.abc import Awaitable, Callable, Generator +from collections.abc import Awaitable, Callable from time import time from unittest.mock import AsyncMock, patch from electrickiwi_api.model import AccountBalance, Hop, HopIntervals import pytest +from typing_extensions import Generator from homeassistant.components.application_credentials import ( ClientCredential, @@ -23,7 +24,7 @@ CLIENT_ID = "1234" CLIENT_SECRET = "5678" REDIRECT_URI = "https://example.com/auth/external/callback" -type YieldFixture = Generator[AsyncMock, None, None] +type YieldFixture = Generator[AsyncMock] type ComponentSetup = Callable[[], Awaitable[bool]] @@ -79,7 +80,7 @@ def mock_config_entry(hass: HomeAssistant) -> MockConfigEntry: @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Mock setting up a config entry.""" with patch( "homeassistant.components.electric_kiwi.async_setup_entry", return_value=True diff --git a/tests/components/elgato/conftest.py b/tests/components/elgato/conftest.py index 5a783c509c2..abbc1bc0463 100644 --- a/tests/components/elgato/conftest.py +++ b/tests/components/elgato/conftest.py @@ -1,10 +1,10 @@ """Fixtures for Elgato integration tests.""" -from collections.abc import Generator from unittest.mock import AsyncMock, MagicMock, patch from elgato import BatteryInfo, ElgatoNoBatteryError, Info, Settings, State import pytest +from typing_extensions import Generator from homeassistant.components.elgato.const import DOMAIN from homeassistant.const import CONF_HOST, CONF_MAC, CONF_PORT @@ -42,7 +42,7 @@ def mock_config_entry() -> MockConfigEntry: @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Mock setting up a config entry.""" with patch( "homeassistant.components.elgato.async_setup_entry", return_value=True diff --git a/tests/components/elmax/conftest.py b/tests/components/elmax/conftest.py index 2166e6476c7..552aa138f1b 100644 --- a/tests/components/elmax/conftest.py +++ b/tests/components/elmax/conftest.py @@ -1,6 +1,5 @@ """Configuration for Elmax tests.""" -from collections.abc import Generator import json from unittest.mock import AsyncMock, patch @@ -13,6 +12,7 @@ from elmax_api.constants import ( from httpx import Response import pytest import respx +from typing_extensions import Generator from . import ( MOCK_DIRECT_HOST, @@ -30,7 +30,7 @@ MOCK_DIRECT_BASE_URI = ( @pytest.fixture(autouse=True) -def httpx_mock_cloud_fixture() -> Generator[respx.MockRouter, None, None]: +def httpx_mock_cloud_fixture() -> Generator[respx.MockRouter]: """Configure httpx fixture for cloud API communication.""" with respx.mock(base_url=BASE_URL, assert_all_called=False) as respx_mock: # Mock Login POST. @@ -57,7 +57,7 @@ def httpx_mock_cloud_fixture() -> Generator[respx.MockRouter, None, None]: @pytest.fixture(autouse=True) -def httpx_mock_direct_fixture() -> Generator[respx.MockRouter, None, None]: +def httpx_mock_direct_fixture() -> Generator[respx.MockRouter]: """Configure httpx fixture for direct Panel-API communication.""" with respx.mock( base_url=MOCK_DIRECT_BASE_URI, assert_all_called=False @@ -80,7 +80,7 @@ def httpx_mock_direct_fixture() -> Generator[respx.MockRouter, None, None]: @pytest.fixture(autouse=True) -def elmax_mock_direct_cert() -> Generator[AsyncMock, None, None]: +def elmax_mock_direct_cert() -> Generator[AsyncMock]: """Patch elmax library to return a specific PEM for SSL communication.""" with patch( "elmax_api.http.GenericElmax.retrieve_server_certificate", diff --git a/tests/components/elvia/conftest.py b/tests/components/elvia/conftest.py index c8b98f18f3f..0708e5c698a 100644 --- a/tests/components/elvia/conftest.py +++ b/tests/components/elvia/conftest.py @@ -1,13 +1,13 @@ """Common fixtures for the Elvia tests.""" -from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest +from typing_extensions import Generator @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Override async_setup_entry.""" with patch( "homeassistant.components.elvia.async_setup_entry", return_value=True diff --git a/tests/components/emulated_hue/test_upnp.py b/tests/components/emulated_hue/test_upnp.py index 86b9f0c2c97..c1469b29bf4 100644 --- a/tests/components/emulated_hue/test_upnp.py +++ b/tests/components/emulated_hue/test_upnp.py @@ -1,7 +1,6 @@ """The tests for the emulated Hue component.""" from asyncio import AbstractEventLoop -from collections.abc import Generator from http import HTTPStatus import json import unittest @@ -11,6 +10,7 @@ from aiohttp import web from aiohttp.test_utils import TestClient import defusedxml.ElementTree as ET import pytest +from typing_extensions import Generator from homeassistant import setup from homeassistant.components import emulated_hue @@ -49,7 +49,7 @@ def aiohttp_client( @pytest.fixture def hue_client( aiohttp_client: ClientSessionGenerator, -) -> Generator[TestClient, None, None]: +) -> Generator[TestClient]: """Return a hue API client.""" app = web.Application() with unittest.mock.patch( diff --git a/tests/components/energenie_power_sockets/conftest.py b/tests/components/energenie_power_sockets/conftest.py index f119c0008f7..64eb8bbd2a8 100644 --- a/tests/components/energenie_power_sockets/conftest.py +++ b/tests/components/energenie_power_sockets/conftest.py @@ -1,11 +1,11 @@ """Configure tests for Energenie-Power-Sockets.""" -from collections.abc import Generator from typing import Final from unittest.mock import MagicMock, patch from pyegps.fakes.powerstrip import FakePowerStrip import pytest +from typing_extensions import Generator from homeassistant.components.energenie_power_sockets.const import ( CONF_DEVICE_API_ID, @@ -58,7 +58,7 @@ def get_pyegps_device_mock() -> MagicMock: @pytest.fixture(name="mock_get_device") -def patch_get_device(pyegps_device_mock: MagicMock) -> Generator[MagicMock, None, None]: +def patch_get_device(pyegps_device_mock: MagicMock) -> Generator[MagicMock]: """Fixture to patch the `get_device` api method.""" with ( patch("homeassistant.components.energenie_power_sockets.get_device") as m1, @@ -74,7 +74,7 @@ def patch_get_device(pyegps_device_mock: MagicMock) -> Generator[MagicMock, None @pytest.fixture(name="mock_search_for_devices") def patch_search_devices( pyegps_device_mock: MagicMock, -) -> Generator[MagicMock, None, None]: +) -> Generator[MagicMock]: """Fixture to patch the `search_for_devices` api method.""" with patch( "homeassistant.components.energenie_power_sockets.config_flow.search_for_devices", diff --git a/tests/components/energyzero/conftest.py b/tests/components/energyzero/conftest.py index 2198e8c0c79..49f6c18b09e 100644 --- a/tests/components/energyzero/conftest.py +++ b/tests/components/energyzero/conftest.py @@ -1,11 +1,11 @@ """Fixtures for EnergyZero integration tests.""" -from collections.abc import Generator import json from unittest.mock import AsyncMock, MagicMock, patch from energyzero import Electricity, Gas import pytest +from typing_extensions import Generator from homeassistant.components.energyzero.const import DOMAIN from homeassistant.core import HomeAssistant @@ -14,7 +14,7 @@ from tests.common import MockConfigEntry, load_fixture @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Mock setting up a config entry.""" with patch( "homeassistant.components.energyzero.async_setup_entry", return_value=True @@ -34,7 +34,7 @@ def mock_config_entry() -> MockConfigEntry: @pytest.fixture -def mock_energyzero() -> Generator[MagicMock, None, None]: +def mock_energyzero() -> Generator[MagicMock]: """Return a mocked EnergyZero client.""" with patch( "homeassistant.components.energyzero.coordinator.EnergyZero", autospec=True diff --git a/tests/components/event/test_init.py b/tests/components/event/test_init.py index 8e3f1a8a932..981a7744beb 100644 --- a/tests/components/event/test_init.py +++ b/tests/components/event/test_init.py @@ -1,10 +1,10 @@ """The tests for the event integration.""" -from collections.abc import Generator from typing import Any from freezegun import freeze_time import pytest +from typing_extensions import Generator from homeassistant.components.event import ( ATTR_EVENT_TYPE, @@ -238,7 +238,7 @@ class MockFlow(ConfigFlow): @pytest.fixture -def config_flow_fixture(hass: HomeAssistant) -> Generator[None, None, None]: +def config_flow_fixture(hass: HomeAssistant) -> Generator[None]: """Mock config flow.""" mock_platform(hass, f"{TEST_DOMAIN}.config_flow") diff --git a/tests/components/fibaro/conftest.py b/tests/components/fibaro/conftest.py index 345668c23bd..d2f004a160c 100644 --- a/tests/components/fibaro/conftest.py +++ b/tests/components/fibaro/conftest.py @@ -1,9 +1,9 @@ """Test helpers.""" -from collections.abc import Generator from unittest.mock import AsyncMock, Mock, patch import pytest +from typing_extensions import Generator from homeassistant.components.fibaro import CONF_IMPORT_PLUGINS, DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_URL, CONF_USERNAME @@ -21,7 +21,7 @@ TEST_MODEL = "HC3" @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Override async_setup_entry.""" with patch( "homeassistant.components.fibaro.async_setup_entry", return_value=True @@ -66,7 +66,7 @@ def mock_config_entry(hass: HomeAssistant) -> MockConfigEntry: @pytest.fixture -def mock_fibaro_client() -> Generator[Mock, None, None]: +def mock_fibaro_client() -> Generator[Mock]: """Return a mocked FibaroClient.""" info_mock = Mock() info_mock.serial_number = TEST_SERIALNUMBER diff --git a/tests/components/file/conftest.py b/tests/components/file/conftest.py index 082483266a2..a9b817a7dcf 100644 --- a/tests/components/file/conftest.py +++ b/tests/components/file/conftest.py @@ -1,15 +1,15 @@ """Test fixtures for file platform.""" -from collections.abc import Generator from unittest.mock import AsyncMock, MagicMock, patch import pytest +from typing_extensions import Generator from homeassistant.core import HomeAssistant @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Override async_setup_entry.""" with patch( "homeassistant.components.file.async_setup_entry", return_value=True diff --git a/tests/components/filesize/conftest.py b/tests/components/filesize/conftest.py index 81aea2aee54..859886a3058 100644 --- a/tests/components/filesize/conftest.py +++ b/tests/components/filesize/conftest.py @@ -2,11 +2,11 @@ from __future__ import annotations -from collections.abc import Generator from pathlib import Path from unittest.mock import patch import pytest +from typing_extensions import Generator from homeassistant.components.filesize.const import DOMAIN from homeassistant.const import CONF_FILE_PATH @@ -29,7 +29,7 @@ def mock_config_entry(tmp_path: Path) -> MockConfigEntry: @pytest.fixture -def mock_setup_entry() -> Generator[None, None, None]: +def mock_setup_entry() -> Generator[None]: """Mock setting up a config entry.""" with patch( "homeassistant.components.filesize.async_setup_entry", return_value=True diff --git a/tests/components/fitbit/conftest.py b/tests/components/fitbit/conftest.py index a4bfed43cba..b1ff8a94e12 100644 --- a/tests/components/fitbit/conftest.py +++ b/tests/components/fitbit/conftest.py @@ -1,6 +1,6 @@ """Test fixtures for fitbit.""" -from collections.abc import Awaitable, Callable, Generator +from collections.abc import Awaitable, Callable import datetime from http import HTTPStatus import time @@ -9,6 +9,7 @@ from unittest.mock import patch import pytest from requests_mock.mocker import Mocker +from typing_extensions import Generator from homeassistant.components.application_credentials import ( ClientCredential, @@ -122,7 +123,7 @@ def mock_fitbit_config_yaml(token_expiration_time: float) -> dict[str, Any] | No @pytest.fixture(name="fitbit_config_setup") def mock_fitbit_config_setup( fitbit_config_yaml: dict[str, Any] | None, -) -> Generator[None, None, None]: +) -> Generator[None]: """Fixture to mock out fitbit.conf file data loading and persistence.""" has_config = fitbit_config_yaml is not None with ( diff --git a/tests/components/flexit_bacnet/conftest.py b/tests/components/flexit_bacnet/conftest.py index d7e7962003b..e1b98070d25 100644 --- a/tests/components/flexit_bacnet/conftest.py +++ b/tests/components/flexit_bacnet/conftest.py @@ -1,10 +1,10 @@ """Configuration for Flexit Nordic (BACnet) tests.""" -from collections.abc import Generator from unittest.mock import AsyncMock, patch from flexit_bacnet import FlexitBACnet import pytest +from typing_extensions import Generator from homeassistant import config_entries from homeassistant.components.flexit_bacnet.const import DOMAIN @@ -29,7 +29,7 @@ async def flow_id(hass: HomeAssistant) -> str: @pytest.fixture -def mock_flexit_bacnet() -> Generator[AsyncMock, None, None]: +def mock_flexit_bacnet() -> Generator[AsyncMock]: """Mock data from the device.""" flexit_bacnet = AsyncMock(spec=FlexitBACnet) with ( @@ -83,7 +83,7 @@ def mock_flexit_bacnet() -> Generator[AsyncMock, None, None]: @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Mock setting up a config entry.""" with patch( "homeassistant.components.flexit_bacnet.async_setup_entry", return_value=True diff --git a/tests/components/folder_watcher/conftest.py b/tests/components/folder_watcher/conftest.py index 875a90f7cbb..6de9c69d574 100644 --- a/tests/components/folder_watcher/conftest.py +++ b/tests/components/folder_watcher/conftest.py @@ -2,12 +2,12 @@ from __future__ import annotations -from collections.abc import Generator from pathlib import Path from unittest.mock import patch from freezegun.api import FrozenDateTimeFactory import pytest +from typing_extensions import Generator from homeassistant.components.folder_watcher.const import DOMAIN from homeassistant.config_entries import SOURCE_USER @@ -17,7 +17,7 @@ from tests.common import MockConfigEntry @pytest.fixture -def mock_setup_entry() -> Generator[None, None, None]: +def mock_setup_entry() -> Generator[None]: """Mock setting up a config entry.""" with patch( "homeassistant.components.folder_watcher.async_setup_entry", return_value=True diff --git a/tests/components/forecast_solar/conftest.py b/tests/components/forecast_solar/conftest.py index bc101d81388..346a5c8fac5 100644 --- a/tests/components/forecast_solar/conftest.py +++ b/tests/components/forecast_solar/conftest.py @@ -1,11 +1,11 @@ """Fixtures for Forecast.Solar integration tests.""" -from collections.abc import Generator from datetime import datetime, timedelta from unittest.mock import AsyncMock, MagicMock, patch from forecast_solar import models import pytest +from typing_extensions import Generator from homeassistant.components.forecast_solar.const import ( CONF_AZIMUTH, @@ -24,7 +24,7 @@ from tests.common import MockConfigEntry @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Mock setting up a config entry.""" with patch( "homeassistant.components.forecast_solar.async_setup_entry", return_value=True diff --git a/tests/components/freedompro/conftest.py b/tests/components/freedompro/conftest.py index 27e6c767223..daafc7e8dc7 100644 --- a/tests/components/freedompro/conftest.py +++ b/tests/components/freedompro/conftest.py @@ -2,12 +2,12 @@ from __future__ import annotations -from collections.abc import Generator from copy import deepcopy from typing import Any from unittest.mock import AsyncMock, patch import pytest +from typing_extensions import Generator from homeassistant.components.freedompro.const import DOMAIN @@ -17,7 +17,7 @@ from tests.common import MockConfigEntry @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Override async_setup_entry.""" with patch( "homeassistant.components.freedompro.async_setup_entry", return_value=True diff --git a/tests/components/frontier_silicon/conftest.py b/tests/components/frontier_silicon/conftest.py index 65a5ede5b26..2322740c69a 100644 --- a/tests/components/frontier_silicon/conftest.py +++ b/tests/components/frontier_silicon/conftest.py @@ -1,9 +1,9 @@ """Configuration for frontier_silicon tests.""" -from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest +from typing_extensions import Generator from homeassistant.components.frontier_silicon.const import CONF_WEBFSAPI_URL, DOMAIN from homeassistant.const import CONF_PIN @@ -22,7 +22,7 @@ def config_entry() -> MockConfigEntry: @pytest.fixture(autouse=True) -def mock_valid_device_url() -> Generator[None, None, None]: +def mock_valid_device_url() -> Generator[None]: """Return a valid webfsapi endpoint.""" with patch( "afsapi.AFSAPI.get_webfsapi_endpoint", @@ -32,7 +32,7 @@ def mock_valid_device_url() -> Generator[None, None, None]: @pytest.fixture(autouse=True) -def mock_valid_pin() -> Generator[None, None, None]: +def mock_valid_pin() -> Generator[None]: """Make get_friendly_name return a value, indicating a valid pin.""" with patch( "afsapi.AFSAPI.get_friendly_name", @@ -42,14 +42,14 @@ def mock_valid_pin() -> Generator[None, None, None]: @pytest.fixture(autouse=True) -def mock_radio_id() -> Generator[None, None, None]: +def mock_radio_id() -> Generator[None]: """Return a valid radio_id.""" with patch("afsapi.AFSAPI.get_radio_id", return_value="mock_radio_id"): yield @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Override async_setup_entry.""" with patch( "homeassistant.components.frontier_silicon.async_setup_entry", return_value=True diff --git a/tests/components/fully_kiosk/conftest.py b/tests/components/fully_kiosk/conftest.py index ff732d0e223..3f7c2985daf 100644 --- a/tests/components/fully_kiosk/conftest.py +++ b/tests/components/fully_kiosk/conftest.py @@ -2,11 +2,11 @@ from __future__ import annotations -from collections.abc import Generator import json from unittest.mock import AsyncMock, MagicMock, patch import pytest +from typing_extensions import Generator from homeassistant.components.fully_kiosk.const import DOMAIN from homeassistant.const import ( @@ -39,7 +39,7 @@ def mock_config_entry() -> MockConfigEntry: @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Mock setting up a config entry.""" with patch( "homeassistant.components.fully_kiosk.async_setup_entry", return_value=True @@ -48,7 +48,7 @@ def mock_setup_entry() -> Generator[AsyncMock, None, None]: @pytest.fixture -def mock_fully_kiosk_config_flow() -> Generator[MagicMock, None, None]: +def mock_fully_kiosk_config_flow() -> Generator[MagicMock]: """Return a mocked Fully Kiosk client for the config flow.""" with patch( "homeassistant.components.fully_kiosk.config_flow.FullyKiosk", @@ -64,7 +64,7 @@ def mock_fully_kiosk_config_flow() -> Generator[MagicMock, None, None]: @pytest.fixture -def mock_fully_kiosk() -> Generator[MagicMock, None, None]: +def mock_fully_kiosk() -> Generator[MagicMock]: """Return a mocked Fully Kiosk client.""" with patch( "homeassistant.components.fully_kiosk.coordinator.FullyKiosk", diff --git a/tests/components/fyta/conftest.py b/tests/components/fyta/conftest.py index cf6fb69e83d..de5dece776c 100644 --- a/tests/components/fyta/conftest.py +++ b/tests/components/fyta/conftest.py @@ -1,10 +1,10 @@ """Test helpers for FYTA.""" -from collections.abc import Generator from datetime import UTC, datetime from unittest.mock import AsyncMock, patch import pytest +from typing_extensions import Generator from homeassistant.components.fyta.const import CONF_EXPIRATION, DOMAIN as FYTA_DOMAIN from homeassistant.const import CONF_ACCESS_TOKEN, CONF_PASSWORD, CONF_USERNAME @@ -69,7 +69,7 @@ def mock_fyta_connector(): @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Override async_setup_entry.""" with patch( "homeassistant.components.fyta.async_setup_entry", return_value=True diff --git a/tests/conftest.py b/tests/conftest.py index a6f9c34c568..35da0215247 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,7 +3,7 @@ from __future__ import annotations import asyncio -from collections.abc import AsyncGenerator, Callable, Coroutine, Generator +from collections.abc import Callable, Coroutine from contextlib import asynccontextmanager, contextmanager import functools import gc @@ -32,6 +32,7 @@ import pytest import pytest_socket import requests_mock from syrupy.assertion import SnapshotAssertion +from typing_extensions import AsyncGenerator, Generator # Setup patching if dt_util time functions before any other Home Assistant imports from . import patch_time # noqa: F401, isort:skip @@ -292,7 +293,7 @@ def wait_for_stop_scripts_after_shutdown() -> bool: @pytest.fixture(autouse=True) def skip_stop_scripts( wait_for_stop_scripts_after_shutdown: bool, -) -> Generator[None, None, None]: +) -> Generator[None]: """Add ability to bypass _schedule_stop_scripts_after_shutdown.""" if wait_for_stop_scripts_after_shutdown: yield @@ -305,7 +306,7 @@ def skip_stop_scripts( @contextmanager -def long_repr_strings() -> Generator[None, None, None]: +def long_repr_strings() -> Generator[None]: """Increase reprlib maxstring and maxother to 300.""" arepr = reprlib.aRepr original_maxstring = arepr.maxstring @@ -330,7 +331,7 @@ def verify_cleanup( event_loop: asyncio.AbstractEventLoop, expected_lingering_tasks: bool, expected_lingering_timers: bool, -) -> Generator[None, None, None]: +) -> Generator[None]: """Verify that the test has cleaned up resources correctly.""" threads_before = frozenset(threading.enumerate()) tasks_before = asyncio.all_tasks(event_loop) @@ -378,14 +379,14 @@ def verify_cleanup( @pytest.fixture(autouse=True) -def reset_hass_threading_local_object() -> Generator[None, None, None]: +def reset_hass_threading_local_object() -> Generator[None]: """Reset the _Hass threading.local object for every test case.""" yield ha._hass.__dict__.clear() @pytest.fixture(scope="session", autouse=True) -def bcrypt_cost() -> Generator[None, None, None]: +def bcrypt_cost() -> Generator[None]: """Run with reduced rounds during tests, to speed up uses.""" import bcrypt @@ -400,7 +401,7 @@ def bcrypt_cost() -> Generator[None, None, None]: @pytest.fixture -def hass_storage() -> Generator[dict[str, Any], None, None]: +def hass_storage() -> Generator[dict[str, Any]]: """Fixture to mock storage.""" with mock_storage() as stored_data: yield stored_data @@ -458,7 +459,7 @@ def aiohttp_client_cls() -> type[CoalescingClient]: @pytest.fixture def aiohttp_client( event_loop: asyncio.AbstractEventLoop, -) -> Generator[ClientSessionGenerator, None, None]: +) -> Generator[ClientSessionGenerator]: """Override the default aiohttp_client since 3.x does not support aiohttp_client_cls. Remove this when upgrading to 4.x as aiohttp_client_cls @@ -523,7 +524,7 @@ async def hass( hass_storage: dict[str, Any], request: pytest.FixtureRequest, mock_recorder_before_hass: None, -) -> AsyncGenerator[HomeAssistant, None]: +) -> AsyncGenerator[HomeAssistant]: """Create a test instance of Home Assistant.""" loop = asyncio.get_running_loop() @@ -582,7 +583,7 @@ async def hass( @pytest.fixture -async def stop_hass() -> AsyncGenerator[None, None]: +async def stop_hass() -> AsyncGenerator[None]: """Make sure all hass are stopped.""" orig_hass = ha.HomeAssistant @@ -608,21 +609,21 @@ async def stop_hass() -> AsyncGenerator[None, None]: @pytest.fixture(name="requests_mock") -def requests_mock_fixture() -> Generator[requests_mock.Mocker, None, None]: +def requests_mock_fixture() -> Generator[requests_mock.Mocker]: """Fixture to provide a requests mocker.""" with requests_mock.mock() as m: yield m @pytest.fixture -def aioclient_mock() -> Generator[AiohttpClientMocker, None, None]: +def aioclient_mock() -> Generator[AiohttpClientMocker]: """Fixture to mock aioclient calls.""" with mock_aiohttp_client() as mock_session: yield mock_session @pytest.fixture -def mock_device_tracker_conf() -> Generator[list[Device], None, None]: +def mock_device_tracker_conf() -> Generator[list[Device]]: """Prevent device tracker from reading/writing data.""" devices: list[Device] = [] @@ -801,7 +802,7 @@ def hass_client_no_auth( @pytest.fixture -def current_request() -> Generator[MagicMock, None, None]: +def current_request() -> Generator[MagicMock]: """Mock current request.""" with patch("homeassistant.components.http.current_request") as mock_request_context: mocked_request = make_mocked_request( @@ -851,7 +852,7 @@ def hass_ws_client( auth_ok = await websocket.receive_json() assert auth_ok["type"] == TYPE_AUTH_OK - def _get_next_id() -> Generator[int, None, None]: + def _get_next_id() -> Generator[int]: i = 0 while True: yield (i := i + 1) @@ -903,7 +904,7 @@ def mqtt_config_entry_data() -> dict[str, Any] | None: @pytest.fixture -def mqtt_client_mock(hass: HomeAssistant) -> Generator[MqttMockPahoClient, None, None]: +def mqtt_client_mock(hass: HomeAssistant) -> Generator[MqttMockPahoClient]: """Fixture to mock MQTT client.""" mid: int = 0 @@ -975,7 +976,7 @@ async def mqtt_mock( mqtt_client_mock: MqttMockPahoClient, mqtt_config_entry_data: dict[str, Any] | None, mqtt_mock_entry: MqttMockHAClientGenerator, -) -> AsyncGenerator[MqttMockHAClient, None]: +) -> AsyncGenerator[MqttMockHAClient]: """Fixture to mock MQTT component.""" return await mqtt_mock_entry() @@ -985,7 +986,7 @@ async def _mqtt_mock_entry( hass: HomeAssistant, mqtt_client_mock: MqttMockPahoClient, mqtt_config_entry_data: dict[str, Any] | None, -) -> AsyncGenerator[MqttMockHAClientGenerator, None]: +) -> AsyncGenerator[MqttMockHAClientGenerator]: """Fixture to mock a delayed setup of the MQTT config entry.""" # Local import to avoid processing MQTT modules when running a testcase # which does not use MQTT. @@ -1059,9 +1060,7 @@ def hass_config() -> ConfigType: @pytest.fixture -def mock_hass_config( - hass: HomeAssistant, hass_config: ConfigType -) -> Generator[None, None, None]: +def mock_hass_config(hass: HomeAssistant, hass_config: ConfigType) -> Generator[None]: """Fixture to mock the content of main configuration. Patches homeassistant.config.load_yaml_config_file and hass.config_entries @@ -1100,7 +1099,7 @@ def hass_config_yaml_files(hass_config_yaml: str) -> dict[str, str]: @pytest.fixture def mock_hass_config_yaml( hass: HomeAssistant, hass_config_yaml_files: dict[str, str] -) -> Generator[None, None, None]: +) -> Generator[None]: """Fixture to mock the content of the yaml configuration files. Patches yaml configuration files using the `hass_config_yaml` @@ -1115,7 +1114,7 @@ async def mqtt_mock_entry( hass: HomeAssistant, mqtt_client_mock: MqttMockPahoClient, mqtt_config_entry_data: dict[str, Any] | None, -) -> AsyncGenerator[MqttMockHAClientGenerator, None]: +) -> AsyncGenerator[MqttMockHAClientGenerator]: """Set up an MQTT config entry.""" async def _async_setup_config_entry( @@ -1137,7 +1136,7 @@ async def mqtt_mock_entry( @pytest.fixture(autouse=True, scope="session") -def mock_network() -> Generator[None, None, None]: +def mock_network() -> Generator[None]: """Mock network.""" with patch( "homeassistant.components.network.util.ifaddr.get_adapters", @@ -1153,7 +1152,7 @@ def mock_network() -> Generator[None, None, None]: @pytest.fixture(autouse=True, scope="session") -def mock_get_source_ip() -> Generator[_patch, None, None]: +def mock_get_source_ip() -> Generator[_patch]: """Mock network util's async_get_source_ip.""" patcher = patch( "homeassistant.components.network.util.async_get_source_ip", @@ -1167,7 +1166,7 @@ def mock_get_source_ip() -> Generator[_patch, None, None]: @pytest.fixture(autouse=True, scope="session") -def translations_once() -> Generator[_patch, None, None]: +def translations_once() -> Generator[_patch]: """Only load translations once per session.""" from homeassistant.helpers.translation import _TranslationsCacheData @@ -1186,7 +1185,7 @@ def translations_once() -> Generator[_patch, None, None]: @pytest.fixture def disable_translations_once( translations_once: _patch, -) -> Generator[None, None, None]: +) -> Generator[None]: """Override loading translations once.""" translations_once.stop() yield @@ -1194,7 +1193,7 @@ def disable_translations_once( @pytest.fixture -def mock_zeroconf() -> Generator[MagicMock, None, None]: +def mock_zeroconf() -> Generator[MagicMock]: """Mock zeroconf.""" from zeroconf import DNSCache # pylint: disable=import-outside-toplevel @@ -1210,7 +1209,7 @@ def mock_zeroconf() -> Generator[MagicMock, None, None]: @pytest.fixture -def mock_async_zeroconf(mock_zeroconf: MagicMock) -> Generator[MagicMock, None, None]: +def mock_async_zeroconf(mock_zeroconf: MagicMock) -> Generator[MagicMock]: """Mock AsyncZeroconf.""" from zeroconf import DNSCache, Zeroconf # pylint: disable=import-outside-toplevel from zeroconf.asyncio import ( # pylint: disable=import-outside-toplevel @@ -1315,7 +1314,7 @@ def recorder_config() -> dict[str, Any] | None: def recorder_db_url( pytestconfig: pytest.Config, hass_fixture_setup: list[bool], -) -> Generator[str, None, None]: +) -> Generator[str]: """Prepare a default database for tests and return a connection URL.""" assert not hass_fixture_setup @@ -1368,7 +1367,7 @@ def hass_recorder( enable_migrate_event_type_ids: bool, enable_migrate_entity_ids: bool, hass_storage, -) -> Generator[Callable[..., HomeAssistant], None, None]: +) -> Generator[Callable[..., HomeAssistant]]: """Home Assistant fixture with in-memory recorder.""" # pylint: disable-next=import-outside-toplevel from homeassistant.components import recorder @@ -1509,7 +1508,7 @@ async def async_setup_recorder_instance( enable_migrate_context_ids: bool, enable_migrate_event_type_ids: bool, enable_migrate_entity_ids: bool, -) -> AsyncGenerator[RecorderInstanceGenerator, None]: +) -> AsyncGenerator[RecorderInstanceGenerator]: """Yield callable to setup recorder instance.""" # pylint: disable-next=import-outside-toplevel from homeassistant.components import recorder @@ -1632,7 +1631,7 @@ async def mock_enable_bluetooth( hass: HomeAssistant, mock_bleak_scanner_start: MagicMock, mock_bluetooth_adapters: None, -) -> AsyncGenerator[None, None]: +) -> AsyncGenerator[None]: """Fixture to mock starting the bleak scanner.""" entry = MockConfigEntry(domain="bluetooth", unique_id="00:00:00:00:00:01") entry.add_to_hass(hass) @@ -1644,7 +1643,7 @@ async def mock_enable_bluetooth( @pytest.fixture(scope="session") -def mock_bluetooth_adapters() -> Generator[None, None, None]: +def mock_bluetooth_adapters() -> Generator[None]: """Fixture to mock bluetooth adapters.""" with ( patch("bluetooth_auto_recovery.recover_adapter"), @@ -1670,7 +1669,7 @@ def mock_bluetooth_adapters() -> Generator[None, None, None]: @pytest.fixture -def mock_bleak_scanner_start() -> Generator[MagicMock, None, None]: +def mock_bleak_scanner_start() -> Generator[MagicMock]: """Fixture to mock starting the bleak scanner.""" # Late imports to avoid loading bleak unless we need it @@ -1693,7 +1692,7 @@ def mock_bleak_scanner_start() -> Generator[MagicMock, None, None]: @pytest.fixture -def mock_integration_frame() -> Generator[Mock, None, None]: +def mock_integration_frame() -> Generator[Mock]: """Mock as if we're calling code from inside an integration.""" correct_frame = Mock( filename="/home/paulus/homeassistant/components/hue/light.py", diff --git a/tests/helpers/test_config_entry_flow.py b/tests/helpers/test_config_entry_flow.py index e99cfbb2f58..6a198b7a297 100644 --- a/tests/helpers/test_config_entry_flow.py +++ b/tests/helpers/test_config_entry_flow.py @@ -1,9 +1,9 @@ """Tests for the Config Entry Flow helper.""" -from collections.abc import Generator from unittest.mock import Mock, PropertyMock, patch import pytest +from typing_extensions import Generator from homeassistant import config_entries, data_entry_flow, setup from homeassistant.config import async_process_ha_core_config @@ -14,7 +14,7 @@ from tests.common import MockConfigEntry, MockModule, mock_integration, mock_pla @pytest.fixture -def discovery_flow_conf(hass: HomeAssistant) -> Generator[dict[str, bool], None, None]: +def discovery_flow_conf(hass: HomeAssistant) -> Generator[dict[str, bool]]: """Register a handler.""" handler_conf = {"discovered": False} @@ -30,7 +30,7 @@ def discovery_flow_conf(hass: HomeAssistant) -> Generator[dict[str, bool], None, @pytest.fixture -def webhook_flow_conf(hass: HomeAssistant) -> Generator[None, None, None]: +def webhook_flow_conf(hass: HomeAssistant) -> Generator[None]: """Register a handler.""" with patch.dict(config_entries.HANDLERS): config_entry_flow.register_webhook_flow("test_single", "Test Single", {}, False) diff --git a/tests/test_bootstrap.py b/tests/test_bootstrap.py index 308bcffa795..afd95ca61cf 100644 --- a/tests/test_bootstrap.py +++ b/tests/test_bootstrap.py @@ -1,7 +1,7 @@ """Test the bootstrapping.""" import asyncio -from collections.abc import Generator, Iterable +from collections.abc import Iterable import contextlib import glob import logging @@ -11,6 +11,7 @@ from typing import Any from unittest.mock import AsyncMock, Mock, patch import pytest +from typing_extensions import Generator from homeassistant import bootstrap, loader, runner import homeassistant.config as config_util @@ -38,7 +39,7 @@ VERSION_PATH = os.path.join(get_test_config_dir(), config_util.VERSION_FILE) @pytest.fixture(autouse=True) -def disable_installed_check() -> Generator[None, None, None]: +def disable_installed_check() -> Generator[None]: """Disable package installed check.""" with patch("homeassistant.util.package.is_installed", return_value=True): yield @@ -55,7 +56,7 @@ async def apply_stop_hass(stop_hass: None) -> None: @pytest.fixture(scope="module", autouse=True) -def mock_http_start_stop() -> Generator[None, None, None]: +def mock_http_start_stop() -> Generator[None]: """Mock HTTP start and stop.""" with ( patch("homeassistant.components.http.start_http_server_and_save_config"), @@ -583,7 +584,7 @@ async def test_setup_after_deps_not_present(hass: HomeAssistant) -> None: @pytest.fixture -def mock_is_virtual_env() -> Generator[Mock, None, None]: +def mock_is_virtual_env() -> Generator[Mock]: """Mock is_virtual_env.""" with patch( "homeassistant.bootstrap.is_virtual_env", return_value=False @@ -592,14 +593,14 @@ def mock_is_virtual_env() -> Generator[Mock, None, None]: @pytest.fixture -def mock_enable_logging() -> Generator[Mock, None, None]: +def mock_enable_logging() -> Generator[Mock]: """Mock enable logging.""" with patch("homeassistant.bootstrap.async_enable_logging") as enable_logging: yield enable_logging @pytest.fixture -def mock_mount_local_lib_path() -> Generator[AsyncMock, None, None]: +def mock_mount_local_lib_path() -> Generator[AsyncMock]: """Mock enable logging.""" with patch( "homeassistant.bootstrap.async_mount_local_lib_path" @@ -608,7 +609,7 @@ def mock_mount_local_lib_path() -> Generator[AsyncMock, None, None]: @pytest.fixture -def mock_process_ha_config_upgrade() -> Generator[Mock, None, None]: +def mock_process_ha_config_upgrade() -> Generator[Mock]: """Mock enable logging.""" with patch( "homeassistant.config.process_ha_config_upgrade" @@ -617,7 +618,7 @@ def mock_process_ha_config_upgrade() -> Generator[Mock, None, None]: @pytest.fixture -def mock_ensure_config_exists() -> Generator[AsyncMock, None, None]: +def mock_ensure_config_exists() -> Generator[AsyncMock]: """Mock enable logging.""" with patch( "homeassistant.config.async_ensure_config_exists", return_value=True @@ -1179,7 +1180,7 @@ async def test_bootstrap_empty_integrations( @pytest.fixture(name="mock_mqtt_config_flow") -def mock_mqtt_config_flow_fixture() -> Generator[None, None, None]: +def mock_mqtt_config_flow_fixture() -> Generator[None]: """Mock MQTT config flow.""" class MockConfigFlow: diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 017bc5bff25..010d322775e 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -3,7 +3,6 @@ from __future__ import annotations import asyncio -from collections.abc import Generator from datetime import timedelta from functools import cached_property import logging @@ -13,6 +12,7 @@ from unittest.mock import ANY, AsyncMock, Mock, patch from freezegun.api import FrozenDateTimeFactory import pytest from syrupy.assertion import SnapshotAssertion +from typing_extensions import Generator from homeassistant import config_entries, data_entry_flow, loader from homeassistant.components import dhcp @@ -53,7 +53,7 @@ from tests.common import async_get_persistent_notifications @pytest.fixture(autouse=True) -def mock_handlers() -> Generator[None, None, None]: +def mock_handlers() -> Generator[None]: """Mock config flows.""" class MockFlowHandler(config_entries.ConfigFlow): diff --git a/tests/util/yaml/test_init.py b/tests/util/yaml/test_init.py index f17489e1488..ed6226693c2 100644 --- a/tests/util/yaml/test_init.py +++ b/tests/util/yaml/test_init.py @@ -1,6 +1,5 @@ """Test Home Assistant yaml loader.""" -from collections.abc import Generator import importlib import io import os @@ -10,6 +9,7 @@ import unittest from unittest.mock import Mock, patch import pytest +from typing_extensions import Generator import voluptuous as vol import yaml as pyyaml @@ -604,7 +604,7 @@ async def test_loading_actual_file_with_syntax_error( @pytest.fixture -def mock_integration_frame() -> Generator[Mock, None, None]: +def mock_integration_frame() -> Generator[Mock]: """Mock as if we're calling code from inside an integration.""" correct_frame = Mock( filename="/home/paulus/homeassistant/components/hue/light.py", From 6de26ca811ff3d1d13761e0ffee9b5aaa5eadd99 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 6 Jun 2024 17:24:48 +0200 Subject: [PATCH 0298/1445] Unhide facebook tests (#118867) --- tests/components/facebook/test_notify.py | 72 ++++++++++++++---------- 1 file changed, 41 insertions(+), 31 deletions(-) diff --git a/tests/components/facebook/test_notify.py b/tests/components/facebook/test_notify.py index bbaa1f12516..77ae544646d 100644 --- a/tests/components/facebook/test_notify.py +++ b/tests/components/facebook/test_notify.py @@ -10,13 +10,15 @@ from homeassistant.core import HomeAssistant @pytest.fixture -def facebook(): +def facebook() -> fb.FacebookNotificationService: """Fixture for facebook.""" access_token = "page-access-token" return fb.FacebookNotificationService(access_token) -async def test_send_simple_message(hass: HomeAssistant, facebook) -> None: +async def test_send_simple_message( + hass: HomeAssistant, facebook: fb.FacebookNotificationService +) -> None: """Test sending a simple message with success.""" with requests_mock.Mocker() as mock: mock.register_uri(requests_mock.POST, fb.BASE_URL, status_code=HTTPStatus.OK) @@ -40,7 +42,9 @@ async def test_send_simple_message(hass: HomeAssistant, facebook) -> None: assert mock.last_request.qs == expected_params -async def test_send_multiple_message(hass: HomeAssistant, facebook) -> None: +async def test_send_multiple_message( + hass: HomeAssistant, facebook: fb.FacebookNotificationService +) -> None: """Test sending a message to multiple targets.""" with requests_mock.Mocker() as mock: mock.register_uri(requests_mock.POST, fb.BASE_URL, status_code=HTTPStatus.OK) @@ -66,7 +70,9 @@ async def test_send_multiple_message(hass: HomeAssistant, facebook) -> None: assert request.qs == expected_params -async def test_send_message_attachment(hass: HomeAssistant, facebook) -> None: +async def test_send_message_attachment( + hass: HomeAssistant, facebook: fb.FacebookNotificationService +) -> None: """Test sending a message with a remote attachment.""" with requests_mock.Mocker() as mock: mock.register_uri(requests_mock.POST, fb.BASE_URL, status_code=HTTPStatus.OK) @@ -95,32 +101,36 @@ async def test_send_message_attachment(hass: HomeAssistant, facebook) -> None: expected_params = {"access_token": ["page-access-token"]} assert mock.last_request.qs == expected_params - async def test_send_targetless_message(hass, facebook): - """Test sending a message without a target.""" - with requests_mock.Mocker() as mock: - mock.register_uri( - requests_mock.POST, fb.BASE_URL, status_code=HTTPStatus.OK - ) - facebook.send_message(message="going nowhere") - assert not mock.called +async def test_send_targetless_message( + hass: HomeAssistant, facebook: fb.FacebookNotificationService +) -> None: + """Test sending a message without a target.""" + with requests_mock.Mocker() as mock: + mock.register_uri(requests_mock.POST, fb.BASE_URL, status_code=HTTPStatus.OK) - async def test_send_message_with_400(hass, facebook): - """Test sending a message with a 400 from Facebook.""" - with requests_mock.Mocker() as mock: - mock.register_uri( - requests_mock.POST, - fb.BASE_URL, - status_code=HTTPStatus.BAD_REQUEST, - json={ - "error": { - "message": "Invalid OAuth access token.", - "type": "OAuthException", - "code": 190, - "fbtrace_id": "G4Da2pFp2Dp", - } - }, - ) - facebook.send_message(message="nope!", target=["+15555551234"]) - assert mock.called - assert mock.call_count == 1 + facebook.send_message(message="going nowhere") + assert not mock.called + + +async def test_send_message_with_400( + hass: HomeAssistant, facebook: fb.FacebookNotificationService +) -> None: + """Test sending a message with a 400 from Facebook.""" + with requests_mock.Mocker() as mock: + mock.register_uri( + requests_mock.POST, + fb.BASE_URL, + status_code=HTTPStatus.BAD_REQUEST, + json={ + "error": { + "message": "Invalid OAuth access token.", + "type": "OAuthException", + "code": 190, + "fbtrace_id": "G4Da2pFp2Dp", + } + }, + ) + facebook.send_message(message="nope!", target=["+15555551234"]) + assert mock.called + assert mock.call_count == 1 From fb5116307561d1921a447347804ab383702fae63 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 6 Jun 2024 17:27:38 +0200 Subject: [PATCH 0299/1445] Move socket_enabled fixture to decorator (#118847) --- tests/components/esphome/test_voice_assistant.py | 9 +++------ tests/components/google/test_diagnostics.py | 2 +- tests/components/hassio/test_handler.py | 2 +- tests/components/local_calendar/test_diagnostics.py | 4 ++-- tests/components/utility_meter/test_diagnostics.py | 3 ++- tests/components/voip/test_sip.py | 3 ++- 6 files changed, 11 insertions(+), 12 deletions(-) diff --git a/tests/components/esphome/test_voice_assistant.py b/tests/components/esphome/test_voice_assistant.py index 305d0e395a3..701ce76a207 100644 --- a/tests/components/esphome/test_voice_assistant.py +++ b/tests/components/esphome/test_voice_assistant.py @@ -186,9 +186,8 @@ async def test_pipeline_events( ) +@pytest.mark.usefixtures("socket_enabled") async def test_udp_server( - hass: HomeAssistant, - socket_enabled: None, unused_udp_port_factory: Callable[[], int], voice_assistant_udp_pipeline_v1: VoiceAssistantUDPPipeline, ) -> None: @@ -313,9 +312,8 @@ async def test_error_calls_handle_finished( voice_assistant_udp_pipeline_v1.handle_finished.assert_called() +@pytest.mark.usefixtures("socket_enabled") async def test_udp_server_multiple( - hass: HomeAssistant, - socket_enabled: None, unused_udp_port_factory: Callable[[], int], voice_assistant_udp_pipeline_v1: VoiceAssistantUDPPipeline, ) -> None: @@ -336,9 +334,8 @@ async def test_udp_server_multiple( await voice_assistant_udp_pipeline_v1.start_server() +@pytest.mark.usefixtures("socket_enabled") async def test_udp_server_after_stopped( - hass: HomeAssistant, - socket_enabled: None, unused_udp_port_factory: Callable[[], int], voice_assistant_udp_pipeline_v1: VoiceAssistantUDPPipeline, ) -> None: diff --git a/tests/components/google/test_diagnostics.py b/tests/components/google/test_diagnostics.py index 69a1929b5ed..5d6259309b8 100644 --- a/tests/components/google/test_diagnostics.py +++ b/tests/components/google/test_diagnostics.py @@ -62,6 +62,7 @@ async def setup_diag(hass): @freeze_time("2023-03-13 12:05:00-07:00") +@pytest.mark.usefixtures("socket_enabled") async def test_diagnostics( hass: HomeAssistant, component_setup: ComponentSetup, @@ -70,7 +71,6 @@ async def test_diagnostics( hass_admin_credential: Credentials, config_entry: MockConfigEntry, aiohttp_client: ClientSessionGenerator, - socket_enabled: None, snapshot: SnapshotAssertion, aioclient_mock: AiohttpClientMocker, ) -> None: diff --git a/tests/components/hassio/test_handler.py b/tests/components/hassio/test_handler.py index 5089613285d..c418576a802 100644 --- a/tests/components/hassio/test_handler.py +++ b/tests/components/hassio/test_handler.py @@ -320,10 +320,10 @@ async def test_api_ingress_panels( ("update_diagnostics", "POST", True), ], ) +@pytest.mark.usefixtures("socket_enabled") async def test_api_headers( aiohttp_raw_server, # 'aiohttp_raw_server' must be before 'hass'! hass: HomeAssistant, - socket_enabled: None, api_call: str, method: Literal["GET", "POST"], payload: Any, diff --git a/tests/components/local_calendar/test_diagnostics.py b/tests/components/local_calendar/test_diagnostics.py index 721eed19736..ed12391f8a9 100644 --- a/tests/components/local_calendar/test_diagnostics.py +++ b/tests/components/local_calendar/test_diagnostics.py @@ -48,6 +48,7 @@ async def setup_diag(hass): @freeze_time("2023-03-13 12:05:00-07:00") +@pytest.mark.usefixtures("socket_enabled") async def test_empty_calendar( hass: HomeAssistant, setup_integration: None, @@ -55,7 +56,6 @@ async def test_empty_calendar( hass_admin_credential: Credentials, config_entry: MockConfigEntry, aiohttp_client: ClientSessionGenerator, - socket_enabled: None, snapshot: SnapshotAssertion, ) -> None: """Test diagnostics against an empty calendar.""" @@ -76,6 +76,7 @@ async def test_empty_calendar( @freeze_time("2023-03-13 12:05:00-07:00") +@pytest.mark.usefixtures("socket_enabled") async def test_api_date_time_event( hass: HomeAssistant, setup_integration: None, @@ -84,7 +85,6 @@ async def test_api_date_time_event( config_entry: MockConfigEntry, hass_ws_client: WebSocketGenerator, aiohttp_client: ClientSessionGenerator, - socket_enabled: None, snapshot: SnapshotAssertion, ) -> None: """Test an event with a start/end date time.""" diff --git a/tests/components/utility_meter/test_diagnostics.py b/tests/components/utility_meter/test_diagnostics.py index 083fd965e90..cefd17fc7e4 100644 --- a/tests/components/utility_meter/test_diagnostics.py +++ b/tests/components/utility_meter/test_diagnostics.py @@ -2,6 +2,7 @@ from aiohttp.test_utils import TestClient from freezegun import freeze_time +import pytest from syrupy import SnapshotAssertion from homeassistant.auth.models import Credentials @@ -50,12 +51,12 @@ def limit_diagnostic_attrs(prop, path) -> bool: @freeze_time("2024-04-06 00:00:00+00:00") +@pytest.mark.usefixtures("socket_enabled") async def test_diagnostics( hass: HomeAssistant, aiohttp_client: ClientSessionGenerator, hass_admin_user: MockUser, hass_admin_credential: Credentials, - socket_enabled: None, snapshot: SnapshotAssertion, ) -> None: """Test generating diagnostics for a config entry.""" diff --git a/tests/components/voip/test_sip.py b/tests/components/voip/test_sip.py index 1ca2f4aaaf2..8c070df7247 100644 --- a/tests/components/voip/test_sip.py +++ b/tests/components/voip/test_sip.py @@ -9,7 +9,8 @@ from homeassistant.components import voip from homeassistant.core import HomeAssistant -async def test_create_sip_server(hass: HomeAssistant, socket_enabled: None) -> None: +@pytest.mark.usefixtures("socket_enabled") +async def test_create_sip_server(hass: HomeAssistant) -> None: """Tests starting/stopping SIP server.""" result = await hass.config_entries.flow.async_init( voip.DOMAIN, context={"source": config_entries.SOURCE_USER} From 0ecab967dda826b46d2f0b2a5b872f3fe084c7b5 Mon Sep 17 00:00:00 2001 From: David Knowles Date: Thu, 6 Jun 2024 11:28:13 -0400 Subject: [PATCH 0300/1445] Bump pydrawise to 2024.6.3 (#118977) --- homeassistant/components/hydrawise/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/hydrawise/manifest.json b/homeassistant/components/hydrawise/manifest.json index 0426b8bf2cc..dc6408407e7 100644 --- a/homeassistant/components/hydrawise/manifest.json +++ b/homeassistant/components/hydrawise/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/hydrawise", "iot_class": "cloud_polling", "loggers": ["pydrawise"], - "requirements": ["pydrawise==2024.6.2"] + "requirements": ["pydrawise==2024.6.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index 89318c7c522..0b016e1ceca 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1794,7 +1794,7 @@ pydiscovergy==3.0.1 pydoods==1.0.2 # homeassistant.components.hydrawise -pydrawise==2024.6.2 +pydrawise==2024.6.3 # homeassistant.components.android_ip_webcam pydroid-ipcam==2.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f5a2abf737e..de5a135ee24 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1408,7 +1408,7 @@ pydexcom==0.2.3 pydiscovergy==3.0.1 # homeassistant.components.hydrawise -pydrawise==2024.6.2 +pydrawise==2024.6.3 # homeassistant.components.android_ip_webcam pydroid-ipcam==2.0.0 From 33ed4fd862e9b293bc5ade6c6ad55c34152c49ab Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Thu, 6 Jun 2024 17:28:59 +0200 Subject: [PATCH 0301/1445] Import Generator from typing_extensions (3) (#118990) --- .../components/gardena_bluetooth/conftest.py | 5 ++-- tests/components/geo_json_events/conftest.py | 4 ++-- tests/components/geocaching/conftest.py | 4 ++-- tests/components/github/conftest.py | 4 ++-- tests/components/google/conftest.py | 7 +++--- .../google_sheets/test_config_flow.py | 4 ++-- .../google_tasks/test_config_flow.py | 4 ++-- tests/components/google_translate/conftest.py | 4 ++-- tests/components/google_translate/test_tts.py | 4 ++-- .../components/govee_light_local/conftest.py | 4 ++-- tests/components/gpsd/conftest.py | 4 ++-- tests/components/gree/conftest.py | 4 ++-- tests/components/greeneye_monitor/conftest.py | 4 ++-- tests/components/guardian/conftest.py | 4 ++-- tests/components/hassio/test_addon_manager.py | 24 +++++++++---------- tests/components/holiday/conftest.py | 4 ++-- .../homeassistant_hardware/conftest.py | 6 ++--- .../test_silabs_multiprotocol_addon.py | 10 ++++---- .../homeassistant_sky_connect/conftest.py | 6 ++--- .../homeassistant_yellow/conftest.py | 6 ++--- .../homeassistant_yellow/test_config_flow.py | 4 ++-- .../components/homekit_controller/conftest.py | 4 ++-- tests/components/homewizard/conftest.py | 6 ++--- tests/components/homeworks/conftest.py | 4 ++-- .../hunterdouglas_powerview/conftest.py | 6 ++--- .../husqvarna_automower/conftest.py | 4 ++-- tests/components/hydrawise/conftest.py | 11 +++++---- tests/components/idasen_desk/conftest.py | 5 ++-- tests/components/image/conftest.py | 5 ++-- tests/components/imap/conftest.py | 6 ++--- tests/components/imgw_pib/conftest.py | 6 ++--- tests/components/incomfort/conftest.py | 4 ++-- tests/components/influxdb/test_init.py | 4 ++-- tests/components/influxdb/test_sensor.py | 4 ++-- tests/components/intellifire/conftest.py | 4 ++-- tests/components/ipma/test_config_flow.py | 4 ++-- tests/components/ipp/conftest.py | 4 ++-- .../islamic_prayer_times/conftest.py | 4 ++-- tests/components/ista_ecotrend/conftest.py | 6 ++--- tests/components/jellyfin/conftest.py | 4 ++-- tests/components/jewish_calendar/conftest.py | 4 ++-- tests/components/jvc_projector/conftest.py | 4 ++-- tests/components/kitchen_sink/test_notify.py | 4 ++-- tests/components/kmtronic/conftest.py | 4 ++-- .../components/kostal_plenticore/conftest.py | 4 ++-- .../kostal_plenticore/test_config_flow.py | 4 ++-- .../kostal_plenticore/test_helper.py | 4 ++-- .../kostal_plenticore/test_number.py | 4 ++-- tests/components/lacrosse_view/conftest.py | 4 ++-- tests/components/lamarzocco/conftest.py | 4 ++-- tests/components/lametric/conftest.py | 8 +++---- .../landisgyr_heat_meter/conftest.py | 4 ++-- tests/components/lawn_mower/test_init.py | 4 ++-- tests/components/lidarr/conftest.py | 5 ++-- .../components/linear_garage_door/conftest.py | 6 ++--- tests/components/local_calendar/conftest.py | 7 +++--- tests/components/local_todo/conftest.py | 8 +++---- tests/components/lock/conftest.py | 4 ++-- tests/components/loqed/conftest.py | 4 ++-- tests/components/lovelace/test_cast.py | 4 ++-- tests/components/lovelace/test_dashboard.py | 4 ++-- tests/components/lovelace/test_init.py | 8 +++---- .../components/lovelace/test_system_health.py | 4 ++-- tests/components/luftdaten/conftest.py | 4 ++-- tests/components/lutron/conftest.py | 4 ++-- tests/components/map/test_init.py | 8 +++---- tests/components/matter/conftest.py | 20 ++++++++-------- tests/components/matter/test_binary_sensor.py | 4 ++-- tests/components/matter/test_config_flow.py | 18 +++++++------- tests/components/matter/test_init.py | 6 ++--- tests/components/media_extractor/conftest.py | 4 ++-- .../media_source/test_local_source.py | 4 ++-- tests/components/melnor/conftest.py | 4 ++-- tests/components/mjpeg/conftest.py | 6 ++--- tests/components/moon/conftest.py | 4 ++-- tests/components/motionmount/conftest.py | 4 ++-- tests/components/mqtt/test_config_flow.py | 21 ++++++++-------- tests/components/mqtt/test_tag.py | 4 ++-- tests/components/mysensors/conftest.py | 9 +++---- tests/components/mystrom/conftest.py | 4 ++-- tests/components/myuplink/conftest.py | 6 ++--- tests/components/nest/common.py | 5 ++-- tests/components/nest/conftest.py | 4 ++-- tests/components/nest/test_init.py | 6 ++--- tests/components/nest/test_media_source.py | 4 ++-- tests/components/network/conftest.py | 4 ++-- tests/components/nextbus/test_config_flow.py | 6 ++--- tests/components/nextbus/test_sensor.py | 6 ++--- tests/components/nextcloud/conftest.py | 4 ++-- tests/components/nibe_heatpump/conftest.py | 4 ++-- tests/components/notify/conftest.py | 5 ++-- tests/components/notion/conftest.py | 4 ++-- tests/components/number/test_init.py | 4 ++-- 93 files changed, 258 insertions(+), 257 deletions(-) diff --git a/tests/components/gardena_bluetooth/conftest.py b/tests/components/gardena_bluetooth/conftest.py index 052de4bf311..830984bc07f 100644 --- a/tests/components/gardena_bluetooth/conftest.py +++ b/tests/components/gardena_bluetooth/conftest.py @@ -1,6 +1,6 @@ """Common fixtures for the Gardena Bluetooth tests.""" -from collections.abc import Awaitable, Callable, Generator +from collections.abc import Awaitable, Callable from typing import Any from unittest.mock import AsyncMock, Mock, patch @@ -10,6 +10,7 @@ from gardena_bluetooth.const import DeviceInformation from gardena_bluetooth.exceptions import CharacteristicNotFound from gardena_bluetooth.parse import Characteristic import pytest +from typing_extensions import Generator from homeassistant.components.gardena_bluetooth.const import DOMAIN from homeassistant.components.gardena_bluetooth.coordinator import SCAN_INTERVAL @@ -30,7 +31,7 @@ def mock_entry(): @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Override async_setup_entry.""" with patch( "homeassistant.components.gardena_bluetooth.async_setup_entry", diff --git a/tests/components/geo_json_events/conftest.py b/tests/components/geo_json_events/conftest.py index 80e06f4880c..beab7bf1403 100644 --- a/tests/components/geo_json_events/conftest.py +++ b/tests/components/geo_json_events/conftest.py @@ -1,9 +1,9 @@ """Configuration for GeoJSON Events tests.""" -from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest +from typing_extensions import Generator from homeassistant.components.geo_json_events import DOMAIN from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_RADIUS, CONF_URL @@ -30,7 +30,7 @@ def config_entry() -> MockConfigEntry: @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Mock geo_json_events entry setup.""" with patch( "homeassistant.components.geo_json_events.async_setup_entry", return_value=True diff --git a/tests/components/geocaching/conftest.py b/tests/components/geocaching/conftest.py index 68041672efb..bedd6fe8b0c 100644 --- a/tests/components/geocaching/conftest.py +++ b/tests/components/geocaching/conftest.py @@ -2,11 +2,11 @@ from __future__ import annotations -from collections.abc import Generator from unittest.mock import AsyncMock, MagicMock, patch from geocachingapi import GeocachingStatus import pytest +from typing_extensions import Generator from homeassistant.components.geocaching.const import DOMAIN @@ -28,7 +28,7 @@ def mock_config_entry() -> MockConfigEntry: @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Mock setting up a config entry.""" with patch( "homeassistant.components.geocaching.async_setup_entry", return_value=True diff --git a/tests/components/github/conftest.py b/tests/components/github/conftest.py index 2951a58702a..df7de604c2c 100644 --- a/tests/components/github/conftest.py +++ b/tests/components/github/conftest.py @@ -1,9 +1,9 @@ """conftest for the GitHub integration.""" -from collections.abc import Generator from unittest.mock import patch import pytest +from typing_extensions import Generator from homeassistant.components.github.const import CONF_REPOSITORIES, DOMAIN from homeassistant.const import CONF_ACCESS_TOKEN @@ -27,7 +27,7 @@ def mock_config_entry() -> MockConfigEntry: @pytest.fixture -def mock_setup_entry() -> Generator[None, None, None]: +def mock_setup_entry() -> Generator[None]: """Mock setting up a config entry.""" with patch("homeassistant.components.github.async_setup_entry", return_value=True): yield diff --git a/tests/components/google/conftest.py b/tests/components/google/conftest.py index aff60ee0b04..26a32a64b21 100644 --- a/tests/components/google/conftest.py +++ b/tests/components/google/conftest.py @@ -2,7 +2,7 @@ from __future__ import annotations -from collections.abc import AsyncGenerator, Awaitable, Callable, Generator +from collections.abc import Awaitable, Callable import datetime import http import time @@ -13,6 +13,7 @@ from aiohttp.client_exceptions import ClientError from gcal_sync.auth import API_BASE_URL from oauth2client.client import OAuth2Credentials import pytest +from typing_extensions import AsyncGenerator, Generator import yaml from homeassistant.components.application_credentials import ( @@ -29,7 +30,7 @@ from tests.test_util.aiohttp import AiohttpClientMocker type ApiResult = Callable[[dict[str, Any]], None] type ComponentSetup = Callable[[], Awaitable[bool]] -type AsyncYieldFixture[_T] = AsyncGenerator[_T, None] +type AsyncYieldFixture[_T] = AsyncGenerator[_T] CALENDAR_ID = "qwertyuiopasdfghjklzxcvbnm@import.calendar.google.com" @@ -150,7 +151,7 @@ def calendars_config(calendars_config_entity: dict[str, Any]) -> list[dict[str, def mock_calendars_yaml( hass: HomeAssistant, calendars_config: list[dict[str, Any]], -) -> Generator[Mock, None, None]: +) -> Generator[Mock]: """Fixture that prepares the google_calendars.yaml mocks.""" mocked_open_function = mock_open( read_data=yaml.dump(calendars_config) if calendars_config else None diff --git a/tests/components/google_sheets/test_config_flow.py b/tests/components/google_sheets/test_config_flow.py index 1f51c9477b8..0da046645d2 100644 --- a/tests/components/google_sheets/test_config_flow.py +++ b/tests/components/google_sheets/test_config_flow.py @@ -1,10 +1,10 @@ """Test the Google Sheets config flow.""" -from collections.abc import Generator from unittest.mock import Mock, patch from gspread import GSpreadException import pytest +from typing_extensions import Generator from homeassistant import config_entries from homeassistant.components.application_credentials import ( @@ -41,7 +41,7 @@ async def setup_credentials(hass: HomeAssistant) -> None: @pytest.fixture(autouse=True) -async def mock_client() -> Generator[Mock, None, None]: +async def mock_client() -> Generator[Mock]: """Fixture to setup a fake spreadsheet client library.""" with patch( "homeassistant.components.google_sheets.config_flow.Client" diff --git a/tests/components/google_tasks/test_config_flow.py b/tests/components/google_tasks/test_config_flow.py index 0c56594a966..f2655afd602 100644 --- a/tests/components/google_tasks/test_config_flow.py +++ b/tests/components/google_tasks/test_config_flow.py @@ -1,11 +1,11 @@ """Test the Google Tasks config flow.""" -from collections.abc import Generator from unittest.mock import Mock, patch from googleapiclient.errors import HttpError from httplib2 import Response import pytest +from typing_extensions import Generator from homeassistant import config_entries from homeassistant.components.google_tasks.const import ( @@ -32,7 +32,7 @@ def user_identifier() -> str: @pytest.fixture -def setup_userinfo(user_identifier: str) -> Generator[Mock, None, None]: +def setup_userinfo(user_identifier: str) -> Generator[Mock]: """Set up userinfo.""" with patch("homeassistant.components.google_tasks.config_flow.build") as mock: mock.return_value.userinfo.return_value.get.return_value.execute.return_value = { diff --git a/tests/components/google_translate/conftest.py b/tests/components/google_translate/conftest.py index 3600fae3841..82f8d50b83c 100644 --- a/tests/components/google_translate/conftest.py +++ b/tests/components/google_translate/conftest.py @@ -1,13 +1,13 @@ """Common fixtures for the Google Translate text-to-speech tests.""" -from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest +from typing_extensions import Generator @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Override async_setup_entry.""" with patch( "homeassistant.components.google_translate.async_setup_entry", return_value=True diff --git a/tests/components/google_translate/test_tts.py b/tests/components/google_translate/test_tts.py index 18fd6a24d3b..d19b1269438 100644 --- a/tests/components/google_translate/test_tts.py +++ b/tests/components/google_translate/test_tts.py @@ -2,7 +2,6 @@ from __future__ import annotations -from collections.abc import Generator from http import HTTPStatus from pathlib import Path from typing import Any @@ -10,6 +9,7 @@ from unittest.mock import MagicMock, patch from gtts import gTTSError import pytest +from typing_extensions import Generator from homeassistant.components import tts from homeassistant.components.google_translate.const import CONF_TLD, DOMAIN @@ -54,7 +54,7 @@ async def setup_internal_url(hass: HomeAssistant) -> None: @pytest.fixture -def mock_gtts() -> Generator[MagicMock, None, None]: +def mock_gtts() -> Generator[MagicMock]: """Mock gtts.""" with patch("homeassistant.components.google_translate.tts.gTTS") as mock_gtts: yield mock_gtts diff --git a/tests/components/govee_light_local/conftest.py b/tests/components/govee_light_local/conftest.py index 1c0f678e485..90a9f8e6827 100644 --- a/tests/components/govee_light_local/conftest.py +++ b/tests/components/govee_light_local/conftest.py @@ -1,11 +1,11 @@ """Tests configuration for Govee Local API.""" from asyncio import Event -from collections.abc import Generator from unittest.mock import AsyncMock, MagicMock, patch from govee_local_api import GoveeLightCapability import pytest +from typing_extensions import Generator from homeassistant.components.govee_light_local.coordinator import GoveeController @@ -25,7 +25,7 @@ def fixture_mock_govee_api(): @pytest.fixture(name="mock_setup_entry") -def fixture_mock_setup_entry() -> Generator[AsyncMock, None, None]: +def fixture_mock_setup_entry() -> Generator[AsyncMock]: """Override async_setup_entry.""" with patch( "homeassistant.components.govee_light_local.async_setup_entry", diff --git a/tests/components/gpsd/conftest.py b/tests/components/gpsd/conftest.py index 71bb3aa61bf..c323365e8fd 100644 --- a/tests/components/gpsd/conftest.py +++ b/tests/components/gpsd/conftest.py @@ -1,13 +1,13 @@ """Common fixtures for the GPSD tests.""" -from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest +from typing_extensions import Generator @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Override async_setup_entry.""" with patch( "homeassistant.components.gpsd.async_setup_entry", return_value=True diff --git a/tests/components/gree/conftest.py b/tests/components/gree/conftest.py index eb1361beea3..88bcaea33c2 100644 --- a/tests/components/gree/conftest.py +++ b/tests/components/gree/conftest.py @@ -1,15 +1,15 @@ """Pytest module configuration.""" -from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest +from typing_extensions import Generator from .common import FakeDiscovery, build_device_mock @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Override async_setup_entry.""" with patch( "homeassistant.components.gree.async_setup_entry", return_value=True diff --git a/tests/components/greeneye_monitor/conftest.py b/tests/components/greeneye_monitor/conftest.py index 8d25a671806..add823237c5 100644 --- a/tests/components/greeneye_monitor/conftest.py +++ b/tests/components/greeneye_monitor/conftest.py @@ -1,10 +1,10 @@ """Common fixtures for testing greeneye_monitor.""" -from collections.abc import Generator from typing import Any from unittest.mock import AsyncMock, MagicMock, patch import pytest +from typing_extensions import Generator from homeassistant.components.greeneye_monitor import DOMAIN from homeassistant.components.sensor import SensorDeviceClass @@ -99,7 +99,7 @@ def assert_sensor_registered( @pytest.fixture -def monitors() -> Generator[AsyncMock, None, None]: +def monitors() -> Generator[AsyncMock]: """Provide a mock greeneye.Monitors object that has listeners and can add new monitors.""" with patch("greeneye.Monitors", autospec=True) as mock_monitors: mock = mock_monitors.return_value diff --git a/tests/components/guardian/conftest.py b/tests/components/guardian/conftest.py index df517aba603..87ff96aff45 100644 --- a/tests/components/guardian/conftest.py +++ b/tests/components/guardian/conftest.py @@ -1,10 +1,10 @@ """Define fixtures for Elexa Guardian tests.""" -from collections.abc import Generator import json from unittest.mock import AsyncMock, patch import pytest +from typing_extensions import Generator from homeassistant.components.guardian import CONF_UID, DOMAIN from homeassistant.const import CONF_IP_ADDRESS, CONF_PORT @@ -14,7 +14,7 @@ from tests.common import MockConfigEntry, load_fixture @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Override async_setup_entry.""" with patch( "homeassistant.components.guardian.async_setup_entry", return_value=True diff --git a/tests/components/hassio/test_addon_manager.py b/tests/components/hassio/test_addon_manager.py index 69b9f5555a3..55c663d66cc 100644 --- a/tests/components/hassio/test_addon_manager.py +++ b/tests/components/hassio/test_addon_manager.py @@ -3,12 +3,12 @@ from __future__ import annotations import asyncio -from collections.abc import Generator import logging from typing import Any from unittest.mock import AsyncMock, call, patch import pytest +from typing_extensions import Generator from homeassistant.components.hassio.addon_manager import ( AddonError, @@ -56,7 +56,7 @@ def mock_addon_installed( @pytest.fixture(name="get_addon_discovery_info") -def get_addon_discovery_info_fixture() -> Generator[AsyncMock, None, None]: +def get_addon_discovery_info_fixture() -> Generator[AsyncMock]: """Mock get add-on discovery info.""" with patch( "homeassistant.components.hassio.addon_manager.async_get_addon_discovery_info" @@ -65,7 +65,7 @@ def get_addon_discovery_info_fixture() -> Generator[AsyncMock, None, None]: @pytest.fixture(name="addon_store_info") -def addon_store_info_fixture() -> Generator[AsyncMock, None, None]: +def addon_store_info_fixture() -> Generator[AsyncMock]: """Mock Supervisor add-on store info.""" with patch( "homeassistant.components.hassio.addon_manager.async_get_addon_store_info" @@ -80,7 +80,7 @@ def addon_store_info_fixture() -> Generator[AsyncMock, None, None]: @pytest.fixture(name="addon_info") -def addon_info_fixture() -> Generator[AsyncMock, None, None]: +def addon_info_fixture() -> Generator[AsyncMock]: """Mock Supervisor add-on info.""" with patch( "homeassistant.components.hassio.addon_manager.async_get_addon_info", @@ -97,7 +97,7 @@ def addon_info_fixture() -> Generator[AsyncMock, None, None]: @pytest.fixture(name="set_addon_options") -def set_addon_options_fixture() -> Generator[AsyncMock, None, None]: +def set_addon_options_fixture() -> Generator[AsyncMock]: """Mock set add-on options.""" with patch( "homeassistant.components.hassio.addon_manager.async_set_addon_options" @@ -106,7 +106,7 @@ def set_addon_options_fixture() -> Generator[AsyncMock, None, None]: @pytest.fixture(name="install_addon") -def install_addon_fixture() -> Generator[AsyncMock, None, None]: +def install_addon_fixture() -> Generator[AsyncMock]: """Mock install add-on.""" with patch( "homeassistant.components.hassio.addon_manager.async_install_addon" @@ -115,7 +115,7 @@ def install_addon_fixture() -> Generator[AsyncMock, None, None]: @pytest.fixture(name="uninstall_addon") -def uninstall_addon_fixture() -> Generator[AsyncMock, None, None]: +def uninstall_addon_fixture() -> Generator[AsyncMock]: """Mock uninstall add-on.""" with patch( "homeassistant.components.hassio.addon_manager.async_uninstall_addon" @@ -124,7 +124,7 @@ def uninstall_addon_fixture() -> Generator[AsyncMock, None, None]: @pytest.fixture(name="start_addon") -def start_addon_fixture() -> Generator[AsyncMock, None, None]: +def start_addon_fixture() -> Generator[AsyncMock]: """Mock start add-on.""" with patch( "homeassistant.components.hassio.addon_manager.async_start_addon" @@ -133,7 +133,7 @@ def start_addon_fixture() -> Generator[AsyncMock, None, None]: @pytest.fixture(name="restart_addon") -def restart_addon_fixture() -> Generator[AsyncMock, None, None]: +def restart_addon_fixture() -> Generator[AsyncMock]: """Mock restart add-on.""" with patch( "homeassistant.components.hassio.addon_manager.async_restart_addon" @@ -142,7 +142,7 @@ def restart_addon_fixture() -> Generator[AsyncMock, None, None]: @pytest.fixture(name="stop_addon") -def stop_addon_fixture() -> Generator[AsyncMock, None, None]: +def stop_addon_fixture() -> Generator[AsyncMock]: """Mock stop add-on.""" with patch( "homeassistant.components.hassio.addon_manager.async_stop_addon" @@ -151,7 +151,7 @@ def stop_addon_fixture() -> Generator[AsyncMock, None, None]: @pytest.fixture(name="create_backup") -def create_backup_fixture() -> Generator[AsyncMock, None, None]: +def create_backup_fixture() -> Generator[AsyncMock]: """Mock create backup.""" with patch( "homeassistant.components.hassio.addon_manager.async_create_backup" @@ -160,7 +160,7 @@ def create_backup_fixture() -> Generator[AsyncMock, None, None]: @pytest.fixture(name="update_addon") -def mock_update_addon() -> Generator[AsyncMock, None, None]: +def mock_update_addon() -> Generator[AsyncMock]: """Mock update add-on.""" with patch( "homeassistant.components.hassio.addon_manager.async_update_addon" diff --git a/tests/components/holiday/conftest.py b/tests/components/holiday/conftest.py index 92f46c8b238..1ac595aa1f9 100644 --- a/tests/components/holiday/conftest.py +++ b/tests/components/holiday/conftest.py @@ -1,13 +1,13 @@ """Common fixtures for the Holiday tests.""" -from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest +from typing_extensions import Generator @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Override async_setup_entry.""" with patch( "homeassistant.components.holiday.async_setup_entry", return_value=True diff --git a/tests/components/homeassistant_hardware/conftest.py b/tests/components/homeassistant_hardware/conftest.py index ae9ee6e1d2e..72e937396ea 100644 --- a/tests/components/homeassistant_hardware/conftest.py +++ b/tests/components/homeassistant_hardware/conftest.py @@ -1,14 +1,14 @@ """Test fixtures for the Home Assistant Hardware integration.""" -from collections.abc import Generator from typing import Any from unittest.mock import AsyncMock, MagicMock, patch import pytest +from typing_extensions import Generator @pytest.fixture(autouse=True) -def mock_zha_config_flow_setup() -> Generator[None, None, None]: +def mock_zha_config_flow_setup() -> Generator[None]: """Mock the radio connection and probing of the ZHA config flow.""" def mock_probe(config: dict[str, Any]) -> None: @@ -39,7 +39,7 @@ def mock_zha_config_flow_setup() -> Generator[None, None, None]: @pytest.fixture(autouse=True) -def mock_zha_get_last_network_settings() -> Generator[None, None, None]: +def mock_zha_get_last_network_settings() -> Generator[None]: """Mock zha.api.async_get_last_network_settings.""" with patch( diff --git a/tests/components/homeassistant_hardware/test_silabs_multiprotocol_addon.py b/tests/components/homeassistant_hardware/test_silabs_multiprotocol_addon.py index f24d1f82fce..c7e469b5bbb 100644 --- a/tests/components/homeassistant_hardware/test_silabs_multiprotocol_addon.py +++ b/tests/components/homeassistant_hardware/test_silabs_multiprotocol_addon.py @@ -2,11 +2,11 @@ from __future__ import annotations -from collections.abc import Generator from typing import Any from unittest.mock import AsyncMock, Mock, patch import pytest +from typing_extensions import Generator from homeassistant.components.hassio import AddonError, AddonInfo, AddonState, HassIO from homeassistant.components.hassio.handler import HassioAPIError @@ -96,7 +96,7 @@ class FakeOptionsFlow(silabs_multiprotocol_addon.OptionsFlowHandler): @pytest.fixture(autouse=True) def config_flow_handler( hass: HomeAssistant, current_request_with_host: None -) -> Generator[None, None, None]: +) -> Generator[None]: """Fixture for a test config flow.""" mock_platform(hass, f"{TEST_DOMAIN}.config_flow") with mock_config_flow(TEST_DOMAIN, FakeConfigFlow): @@ -104,7 +104,7 @@ def config_flow_handler( @pytest.fixture -def options_flow_poll_addon_state() -> Generator[None, None, None]: +def options_flow_poll_addon_state() -> Generator[None]: """Fixture for patching options flow addon state polling.""" with patch( "homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.WaitingAddonManager.async_wait_until_addon_state" @@ -113,7 +113,7 @@ def options_flow_poll_addon_state() -> Generator[None, None, None]: @pytest.fixture(autouse=True) -def hassio_integration(hass: HomeAssistant) -> Generator[None, None, None]: +def hassio_integration(hass: HomeAssistant) -> Generator[None]: """Fixture to mock the `hassio` integration.""" mock_component(hass, "hassio") hass.data["hassio"] = Mock(spec_set=HassIO) @@ -148,7 +148,7 @@ class MockMultiprotocolPlatform(MockPlatform): @pytest.fixture def mock_multiprotocol_platform( hass: HomeAssistant, -) -> Generator[FakeConfigFlow, None, None]: +) -> Generator[FakeConfigFlow]: """Fixture for a test silabs multiprotocol platform.""" hass.config.components.add(TEST_DOMAIN) platform = MockMultiprotocolPlatform() diff --git a/tests/components/homeassistant_sky_connect/conftest.py b/tests/components/homeassistant_sky_connect/conftest.py index de8576e2a0a..099582999d5 100644 --- a/tests/components/homeassistant_sky_connect/conftest.py +++ b/tests/components/homeassistant_sky_connect/conftest.py @@ -1,13 +1,13 @@ """Test fixtures for the Home Assistant SkyConnect integration.""" -from collections.abc import Generator from unittest.mock import AsyncMock, MagicMock, patch import pytest +from typing_extensions import Generator @pytest.fixture(name="mock_usb_serial_by_id", autouse=True) -def mock_usb_serial_by_id_fixture() -> Generator[MagicMock, None, None]: +def mock_usb_serial_by_id_fixture() -> Generator[MagicMock]: """Mock usb serial by id.""" with patch( "homeassistant.components.zha.config_flow.usb.get_serial_by_id" @@ -39,7 +39,7 @@ def mock_zha(): @pytest.fixture(autouse=True) -def mock_zha_get_last_network_settings() -> Generator[None, None, None]: +def mock_zha_get_last_network_settings() -> Generator[None]: """Mock zha.api.async_get_last_network_settings.""" with patch( diff --git a/tests/components/homeassistant_yellow/conftest.py b/tests/components/homeassistant_yellow/conftest.py index 070047648fc..38398eb719f 100644 --- a/tests/components/homeassistant_yellow/conftest.py +++ b/tests/components/homeassistant_yellow/conftest.py @@ -1,14 +1,14 @@ """Test fixtures for the Home Assistant Yellow integration.""" -from collections.abc import Generator from typing import Any from unittest.mock import AsyncMock, MagicMock, patch import pytest +from typing_extensions import Generator @pytest.fixture(autouse=True) -def mock_zha_config_flow_setup() -> Generator[None, None, None]: +def mock_zha_config_flow_setup() -> Generator[None]: """Mock the radio connection and probing of the ZHA config flow.""" def mock_probe(config: dict[str, Any]) -> None: @@ -39,7 +39,7 @@ def mock_zha_config_flow_setup() -> Generator[None, None, None]: @pytest.fixture(autouse=True) -def mock_zha_get_last_network_settings() -> Generator[None, None, None]: +def mock_zha_get_last_network_settings() -> Generator[None]: """Mock zha.api.async_get_last_network_settings.""" with patch( diff --git a/tests/components/homeassistant_yellow/test_config_flow.py b/tests/components/homeassistant_yellow/test_config_flow.py index 206ad4dce15..34946f20b05 100644 --- a/tests/components/homeassistant_yellow/test_config_flow.py +++ b/tests/components/homeassistant_yellow/test_config_flow.py @@ -1,9 +1,9 @@ """Test the Home Assistant Yellow config flow.""" -from collections.abc import Generator from unittest.mock import Mock, patch import pytest +from typing_extensions import Generator from homeassistant.components.hassio import DOMAIN as HASSIO_DOMAIN from homeassistant.components.homeassistant_yellow.const import DOMAIN @@ -16,7 +16,7 @@ from tests.common import MockConfigEntry, MockModule, mock_integration @pytest.fixture(autouse=True) -def config_flow_handler(hass: HomeAssistant) -> Generator[None, None, None]: +def config_flow_handler(hass: HomeAssistant) -> Generator[None]: """Fixture for a test config flow.""" with patch( "homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.WaitingAddonManager.async_wait_until_addon_state" diff --git a/tests/components/homekit_controller/conftest.py b/tests/components/homekit_controller/conftest.py index 9376a08697d..8bfb78b9840 100644 --- a/tests/components/homekit_controller/conftest.py +++ b/tests/components/homekit_controller/conftest.py @@ -1,6 +1,5 @@ """HomeKit controller session fixtures.""" -from collections.abc import Generator import datetime import unittest.mock @@ -8,6 +7,7 @@ from aiohomekit.testing import FakeController from freezegun import freeze_time from freezegun.api import FrozenDateTimeFactory import pytest +from typing_extensions import Generator import homeassistant.util.dt as dt_util @@ -17,7 +17,7 @@ pytest.register_assert_rewrite("tests.components.homekit_controller.common") @pytest.fixture(autouse=True) -def freeze_time_in_future() -> Generator[FrozenDateTimeFactory, None, None]: +def freeze_time_in_future() -> Generator[FrozenDateTimeFactory]: """Freeze time at a known point.""" now = dt_util.utcnow() start_dt = datetime.datetime(now.year + 1, 1, 1, 0, 0, 0, tzinfo=now.tzinfo) diff --git a/tests/components/homewizard/conftest.py b/tests/components/homewizard/conftest.py index bc661da390d..eb638492941 100644 --- a/tests/components/homewizard/conftest.py +++ b/tests/components/homewizard/conftest.py @@ -1,11 +1,11 @@ """Fixtures for HomeWizard integration tests.""" -from collections.abc import Generator from unittest.mock import AsyncMock, MagicMock, patch from homewizard_energy.errors import NotFoundError from homewizard_energy.models import Data, Device, State, System import pytest +from typing_extensions import Generator from homeassistant.components.homewizard.const import DOMAIN from homeassistant.const import CONF_IP_ADDRESS @@ -62,7 +62,7 @@ def mock_homewizardenergy( @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Mock setting up a config entry.""" with patch( "homeassistant.components.homewizard.async_setup_entry", return_value=True @@ -102,7 +102,7 @@ async def init_integration( @pytest.fixture -def mock_onboarding() -> Generator[MagicMock, None, None]: +def mock_onboarding() -> Generator[MagicMock]: """Mock that Home Assistant is currently onboarding.""" with patch( "homeassistant.components.onboarding.async_is_onboarded", diff --git a/tests/components/homeworks/conftest.py b/tests/components/homeworks/conftest.py index ccff56ae3d1..c5d52d20edf 100644 --- a/tests/components/homeworks/conftest.py +++ b/tests/components/homeworks/conftest.py @@ -1,9 +1,9 @@ """Common fixtures for the Lutron Homeworks Series 4 and 8 tests.""" -from collections.abc import Generator from unittest.mock import AsyncMock, MagicMock, patch import pytest +from typing_extensions import Generator from homeassistant.components.homeworks.const import ( CONF_ADDR, @@ -103,7 +103,7 @@ def mock_homeworks() -> Generator[None, MagicMock, None]: @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Override async_setup_entry.""" with patch( "homeassistant.components.homeworks.async_setup_entry", return_value=True diff --git a/tests/components/hunterdouglas_powerview/conftest.py b/tests/components/hunterdouglas_powerview/conftest.py index e55e252f670..da339914aac 100644 --- a/tests/components/hunterdouglas_powerview/conftest.py +++ b/tests/components/hunterdouglas_powerview/conftest.py @@ -1,10 +1,10 @@ """Common fixtures for Hunter Douglas Powerview tests.""" -from collections.abc import Generator from unittest.mock import AsyncMock, MagicMock, PropertyMock, patch from aiopvapi.resources.shade import ShadePosition import pytest +from typing_extensions import Generator from homeassistant.components.hunterdouglas_powerview.const import DOMAIN @@ -12,7 +12,7 @@ from tests.common import load_json_object_fixture, load_json_value_fixture @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Override async_setup_entry.""" with patch( "homeassistant.components.hunterdouglas_powerview.async_setup_entry", @@ -29,7 +29,7 @@ def mock_hunterdouglas_hub( rooms_json: str, scenes_json: str, shades_json: str, -) -> Generator[MagicMock, None, None]: +) -> Generator[MagicMock]: """Return a mocked Powerview Hub with all data populated.""" with ( patch( diff --git a/tests/components/husqvarna_automower/conftest.py b/tests/components/husqvarna_automower/conftest.py index 6c6eb0430d3..7ace3b76808 100644 --- a/tests/components/husqvarna_automower/conftest.py +++ b/tests/components/husqvarna_automower/conftest.py @@ -1,6 +1,5 @@ """Test helpers for Husqvarna Automower.""" -from collections.abc import Generator import time from unittest.mock import AsyncMock, patch @@ -8,6 +7,7 @@ from aioautomower.session import AutomowerSession, _MowerCommands from aioautomower.utils import mower_list_to_dictionary_dataclass from aiohttp import ClientWebSocketResponse import pytest +from typing_extensions import Generator from homeassistant.components.application_credentials import ( ClientCredential, @@ -81,7 +81,7 @@ async def setup_credentials(hass: HomeAssistant) -> None: @pytest.fixture -def mock_automower_client() -> Generator[AsyncMock, None, None]: +def mock_automower_client() -> Generator[AsyncMock]: """Mock a Husqvarna Automower client.""" mower_dict = mower_list_to_dictionary_dataclass( diff --git a/tests/components/hydrawise/conftest.py b/tests/components/hydrawise/conftest.py index 550e944db36..8bca1de5fed 100644 --- a/tests/components/hydrawise/conftest.py +++ b/tests/components/hydrawise/conftest.py @@ -1,6 +1,6 @@ """Common fixtures for the Hydrawise tests.""" -from collections.abc import Awaitable, Callable, Generator +from collections.abc import Awaitable, Callable from datetime import datetime, timedelta from unittest.mock import AsyncMock, patch @@ -19,6 +19,7 @@ from pydrawise.schema import ( Zone, ) import pytest +from typing_extensions import Generator from homeassistant.components.hydrawise.const import DOMAIN from homeassistant.const import CONF_API_KEY, CONF_PASSWORD, CONF_USERNAME @@ -29,7 +30,7 @@ from tests.common import MockConfigEntry @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Override async_setup_entry.""" with patch( "homeassistant.components.hydrawise.async_setup_entry", return_value=True @@ -42,7 +43,7 @@ def mock_legacy_pydrawise( user: User, controller: Controller, zones: list[Zone], -) -> Generator[AsyncMock, None, None]: +) -> Generator[AsyncMock]: """Mock LegacyHydrawiseAsync.""" with patch( "pydrawise.legacy.LegacyHydrawiseAsync", autospec=True @@ -61,7 +62,7 @@ def mock_pydrawise( zones: list[Zone], sensors: list[Sensor], controller_water_use_summary: ControllerWaterUseSummary, -) -> Generator[AsyncMock, None, None]: +) -> Generator[AsyncMock]: """Mock Hydrawise.""" with patch("pydrawise.client.Hydrawise", autospec=True) as mock_pydrawise: user.controllers = [controller] @@ -75,7 +76,7 @@ def mock_pydrawise( @pytest.fixture -def mock_auth() -> Generator[AsyncMock, None, None]: +def mock_auth() -> Generator[AsyncMock]: """Mock pydrawise Auth.""" with patch("pydrawise.auth.Auth", autospec=True) as mock_auth: yield mock_auth.return_value diff --git a/tests/components/idasen_desk/conftest.py b/tests/components/idasen_desk/conftest.py index d99409f8bb2..91f3f2de40e 100644 --- a/tests/components/idasen_desk/conftest.py +++ b/tests/components/idasen_desk/conftest.py @@ -1,14 +1,15 @@ """IKEA Idasen Desk fixtures.""" -from collections.abc import Callable, Generator +from collections.abc import Callable from unittest import mock from unittest.mock import AsyncMock, MagicMock import pytest +from typing_extensions import Generator @pytest.fixture(autouse=True) -def mock_bluetooth(enable_bluetooth: None) -> Generator[None, None, None]: +def mock_bluetooth(enable_bluetooth: None) -> Generator[None]: """Auto mock bluetooth.""" with mock.patch( "homeassistant.components.idasen_desk.bluetooth.async_ble_device_from_address" diff --git a/tests/components/image/conftest.py b/tests/components/image/conftest.py index 4592ccf58d5..65bbf2e0c4f 100644 --- a/tests/components/image/conftest.py +++ b/tests/components/image/conftest.py @@ -1,8 +1,7 @@ """Test helpers for image.""" -from collections.abc import Generator - import pytest +from typing_extensions import Generator from homeassistant.components import image from homeassistant.config_entries import ConfigEntry, ConfigFlow @@ -125,7 +124,7 @@ class MockImagePlatform: @pytest.fixture(name="config_flow") -def config_flow_fixture(hass: HomeAssistant) -> Generator[None, None, None]: +def config_flow_fixture(hass: HomeAssistant) -> Generator[None]: """Mock config flow.""" class MockFlow(ConfigFlow): diff --git a/tests/components/imap/conftest.py b/tests/components/imap/conftest.py index dfe5fa2040f..354c9fbe24e 100644 --- a/tests/components/imap/conftest.py +++ b/tests/components/imap/conftest.py @@ -1,16 +1,16 @@ """Fixtures for imap tests.""" -from collections.abc import AsyncGenerator, Generator from unittest.mock import AsyncMock, MagicMock, patch from aioimaplib import AUTH, LOGOUT, NONAUTH, SELECTED, STARTED, Response import pytest +from typing_extensions import AsyncGenerator, Generator from .const import EMPTY_SEARCH_RESPONSE, TEST_FETCH_RESPONSE_TEXT_PLAIN @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Override async_setup_entry.""" with patch( "homeassistant.components.imap.async_setup_entry", return_value=True @@ -62,7 +62,7 @@ async def mock_imap_protocol( imap_pending_idle: bool, imap_login_state: str, imap_select_state: str, -) -> AsyncGenerator[MagicMock, None]: +) -> AsyncGenerator[MagicMock]: """Mock the aioimaplib IMAP protocol handler.""" with patch( diff --git a/tests/components/imgw_pib/conftest.py b/tests/components/imgw_pib/conftest.py index b22b8b68661..1d278856b5b 100644 --- a/tests/components/imgw_pib/conftest.py +++ b/tests/components/imgw_pib/conftest.py @@ -1,11 +1,11 @@ """Common fixtures for the IMGW-PIB tests.""" -from collections.abc import Generator from datetime import UTC, datetime from unittest.mock import AsyncMock, patch from imgw_pib import HydrologicalData, SensorData import pytest +from typing_extensions import Generator from homeassistant.components.imgw_pib.const import DOMAIN @@ -27,7 +27,7 @@ HYDROLOGICAL_DATA = HydrologicalData( @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Override async_setup_entry.""" with patch( "homeassistant.components.imgw_pib.async_setup_entry", return_value=True @@ -36,7 +36,7 @@ def mock_setup_entry() -> Generator[AsyncMock, None, None]: @pytest.fixture -def mock_imgw_pib_client() -> Generator[AsyncMock, None, None]: +def mock_imgw_pib_client() -> Generator[AsyncMock]: """Mock a ImgwPib client.""" with ( patch( diff --git a/tests/components/incomfort/conftest.py b/tests/components/incomfort/conftest.py index 8c4bc5b2e31..d3675b4abea 100644 --- a/tests/components/incomfort/conftest.py +++ b/tests/components/incomfort/conftest.py @@ -1,10 +1,10 @@ """Fixtures for Intergas InComfort integration.""" -from collections.abc import Generator from typing import Any from unittest.mock import AsyncMock, MagicMock, patch import pytest +from typing_extensions import Generator from homeassistant.components.incomfort import DOMAIN from homeassistant.config_entries import ConfigEntry @@ -20,7 +20,7 @@ MOCK_CONFIG = { @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Override async_setup_entry.""" with patch( "homeassistant.components.incomfort.async_setup_entry", diff --git a/tests/components/influxdb/test_init.py b/tests/components/influxdb/test_init.py index 1e39eaef6ce..aba153cf8a8 100644 --- a/tests/components/influxdb/test_init.py +++ b/tests/components/influxdb/test_init.py @@ -1,6 +1,5 @@ """The tests for the InfluxDB component.""" -from collections.abc import Generator from dataclasses import dataclass import datetime from http import HTTPStatus @@ -8,6 +7,7 @@ import logging from unittest.mock import ANY, MagicMock, Mock, call, patch import pytest +from typing_extensions import Generator from homeassistant.components import influxdb from homeassistant.components.influxdb.const import DEFAULT_BUCKET @@ -54,7 +54,7 @@ def mock_batch_timeout(hass, monkeypatch): @pytest.fixture(name="mock_client") def mock_client_fixture( request: pytest.FixtureRequest, -) -> Generator[MagicMock, None, None]: +) -> Generator[MagicMock]: """Patch the InfluxDBClient object with mock for version under test.""" if request.param == influxdb.API_VERSION_2: client_target = f"{INFLUX_CLIENT_PATH}V2" diff --git a/tests/components/influxdb/test_sensor.py b/tests/components/influxdb/test_sensor.py index a0d949d5176..08c92923bd3 100644 --- a/tests/components/influxdb/test_sensor.py +++ b/tests/components/influxdb/test_sensor.py @@ -2,7 +2,6 @@ from __future__ import annotations -from collections.abc import Generator from dataclasses import dataclass from datetime import timedelta from http import HTTPStatus @@ -11,6 +10,7 @@ from unittest.mock import MagicMock, patch from influxdb.exceptions import InfluxDBClientError, InfluxDBServerError from influxdb_client.rest import ApiException import pytest +from typing_extensions import Generator from voluptuous import Invalid from homeassistant.components import sensor @@ -82,7 +82,7 @@ class Table: @pytest.fixture(name="mock_client") def mock_client_fixture( request: pytest.FixtureRequest, -) -> Generator[MagicMock, None, None]: +) -> Generator[MagicMock]: """Patch the InfluxDBClient object with mock for version under test.""" if request.param == API_VERSION_2: client_target = f"{INFLUXDB_CLIENT_PATH}V2" diff --git a/tests/components/intellifire/conftest.py b/tests/components/intellifire/conftest.py index fa7a48ef9ac..d1ddfed2b5b 100644 --- a/tests/components/intellifire/conftest.py +++ b/tests/components/intellifire/conftest.py @@ -1,14 +1,14 @@ """Fixtures for IntelliFire integration tests.""" -from collections.abc import Generator from unittest.mock import AsyncMock, MagicMock, Mock, patch from aiohttp.client_reqrep import ConnectionKey import pytest +from typing_extensions import Generator @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Mock setting up a config entry.""" with patch( "homeassistant.components.intellifire.async_setup_entry", return_value=True diff --git a/tests/components/ipma/test_config_flow.py b/tests/components/ipma/test_config_flow.py index 38c142ace2a..b007534e09f 100644 --- a/tests/components/ipma/test_config_flow.py +++ b/tests/components/ipma/test_config_flow.py @@ -1,10 +1,10 @@ """Tests for IPMA config flow.""" -from collections.abc import Generator from unittest.mock import patch from pyipma import IPMAException import pytest +from typing_extensions import Generator from homeassistant.components.ipma.const import DOMAIN from homeassistant.config_entries import SOURCE_USER @@ -16,7 +16,7 @@ from tests.components.ipma import MockLocation @pytest.fixture(name="ipma_setup", autouse=True) -def ipma_setup_fixture() -> Generator[None, None, None]: +def ipma_setup_fixture() -> Generator[None]: """Patch ipma setup entry.""" with patch("homeassistant.components.ipma.async_setup_entry", return_value=True): yield diff --git a/tests/components/ipp/conftest.py b/tests/components/ipp/conftest.py index f650b370200..ae098da5698 100644 --- a/tests/components/ipp/conftest.py +++ b/tests/components/ipp/conftest.py @@ -1,11 +1,11 @@ """Fixtures for IPP integration tests.""" -from collections.abc import Generator import json from unittest.mock import AsyncMock, MagicMock, patch from pyipp import Printer import pytest +from typing_extensions import Generator from homeassistant.components.ipp.const import CONF_BASE_PATH, DOMAIN from homeassistant.const import ( @@ -39,7 +39,7 @@ def mock_config_entry() -> MockConfigEntry: @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Mock setting up a config entry.""" with patch( "homeassistant.components.ipp.async_setup_entry", return_value=True diff --git a/tests/components/islamic_prayer_times/conftest.py b/tests/components/islamic_prayer_times/conftest.py index f1b4a8f675c..ae9b1f45eb9 100644 --- a/tests/components/islamic_prayer_times/conftest.py +++ b/tests/components/islamic_prayer_times/conftest.py @@ -1,13 +1,13 @@ """Common fixtures for the islamic_prayer_times tests.""" -from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest +from typing_extensions import Generator @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Override async_setup_entry.""" with patch( "homeassistant.components.islamic_prayer_times.async_setup_entry", diff --git a/tests/components/ista_ecotrend/conftest.py b/tests/components/ista_ecotrend/conftest.py index 097ed07ff10..a9eee5cd026 100644 --- a/tests/components/ista_ecotrend/conftest.py +++ b/tests/components/ista_ecotrend/conftest.py @@ -1,10 +1,10 @@ """Common fixtures for the ista EcoTrend tests.""" -from collections.abc import Generator from typing import Any from unittest.mock import AsyncMock, MagicMock, patch import pytest +from typing_extensions import Generator from homeassistant.components.ista_ecotrend.const import DOMAIN from homeassistant.const import CONF_EMAIL, CONF_PASSWORD @@ -26,7 +26,7 @@ def mock_ista_config_entry() -> MockConfigEntry: @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Override async_setup_entry.""" with patch( "homeassistant.components.ista_ecotrend.async_setup_entry", return_value=True @@ -35,7 +35,7 @@ def mock_setup_entry() -> Generator[AsyncMock, None, None]: @pytest.fixture -def mock_ista() -> Generator[MagicMock, None, None]: +def mock_ista() -> Generator[MagicMock]: """Mock Pyecotrend_ista client.""" with ( diff --git a/tests/components/jellyfin/conftest.py b/tests/components/jellyfin/conftest.py index 4ef28a1cf20..60b0db61729 100644 --- a/tests/components/jellyfin/conftest.py +++ b/tests/components/jellyfin/conftest.py @@ -2,7 +2,6 @@ from __future__ import annotations -from collections.abc import Generator from unittest.mock import AsyncMock, MagicMock, create_autospec, patch from jellyfin_apiclient_python import JellyfinClient @@ -10,6 +9,7 @@ from jellyfin_apiclient_python.api import API from jellyfin_apiclient_python.configuration import Config from jellyfin_apiclient_python.connection_manager import ConnectionManager import pytest +from typing_extensions import Generator from homeassistant.components.jellyfin.const import DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_URL, CONF_USERNAME @@ -37,7 +37,7 @@ def mock_config_entry() -> MockConfigEntry: @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Mock setting up a config entry.""" with patch( "homeassistant.components.jellyfin.async_setup_entry", return_value=True diff --git a/tests/components/jewish_calendar/conftest.py b/tests/components/jewish_calendar/conftest.py index f7dba01576d..5e16289f473 100644 --- a/tests/components/jewish_calendar/conftest.py +++ b/tests/components/jewish_calendar/conftest.py @@ -1,9 +1,9 @@ """Common fixtures for the jewish_calendar tests.""" -from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest +from typing_extensions import Generator from homeassistant.components.jewish_calendar.const import DEFAULT_NAME, DOMAIN @@ -20,7 +20,7 @@ def mock_config_entry() -> MockConfigEntry: @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Override async_setup_entry.""" with patch( "homeassistant.components.jewish_calendar.async_setup_entry", return_value=True diff --git a/tests/components/jvc_projector/conftest.py b/tests/components/jvc_projector/conftest.py index 10fc83e2581..dd012d3f355 100644 --- a/tests/components/jvc_projector/conftest.py +++ b/tests/components/jvc_projector/conftest.py @@ -1,9 +1,9 @@ """Fixtures for JVC Projector integration.""" -from collections.abc import Generator from unittest.mock import MagicMock, patch import pytest +from typing_extensions import Generator from homeassistant.components.jvc_projector.const import DOMAIN from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT @@ -17,7 +17,7 @@ from tests.common import MockConfigEntry @pytest.fixture(name="mock_device") def fixture_mock_device( request: pytest.FixtureRequest, -) -> Generator[MagicMock, None, None]: +) -> Generator[MagicMock]: """Return a mocked JVC Projector device.""" target = "homeassistant.components.jvc_projector.JvcProjector" if hasattr(request, "param"): diff --git a/tests/components/kitchen_sink/test_notify.py b/tests/components/kitchen_sink/test_notify.py index 6d02bacb7be..25fdc61a019 100644 --- a/tests/components/kitchen_sink/test_notify.py +++ b/tests/components/kitchen_sink/test_notify.py @@ -1,10 +1,10 @@ """The tests for the demo button component.""" -from collections.abc import AsyncGenerator from unittest.mock import patch from freezegun.api import FrozenDateTimeFactory import pytest +from typing_extensions import AsyncGenerator from homeassistant.components.kitchen_sink import DOMAIN from homeassistant.components.notify import ( @@ -21,7 +21,7 @@ ENTITY_DIRECT_MESSAGE = "notify.mybox_personal_notifier" @pytest.fixture -async def notify_only() -> AsyncGenerator[None, None]: +async def notify_only() -> AsyncGenerator[None]: """Enable only the button platform.""" with patch( "homeassistant.components.kitchen_sink.COMPONENTS_WITH_DEMO_PLATFORM", diff --git a/tests/components/kmtronic/conftest.py b/tests/components/kmtronic/conftest.py index 98205288aa3..5dc349508e3 100644 --- a/tests/components/kmtronic/conftest.py +++ b/tests/components/kmtronic/conftest.py @@ -1,13 +1,13 @@ """Define fixtures for kmtronic tests.""" -from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest +from typing_extensions import Generator @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Override async_setup_entry.""" with patch( "homeassistant.components.kmtronic.async_setup_entry", return_value=True diff --git a/tests/components/kostal_plenticore/conftest.py b/tests/components/kostal_plenticore/conftest.py index 25cce2ec248..af958f19f3a 100644 --- a/tests/components/kostal_plenticore/conftest.py +++ b/tests/components/kostal_plenticore/conftest.py @@ -2,11 +2,11 @@ from __future__ import annotations -from collections.abc import Generator from unittest.mock import AsyncMock, MagicMock, patch from pykoplenti import MeData, VersionData import pytest +from typing_extensions import Generator from homeassistant.components.kostal_plenticore.coordinator import Plenticore from homeassistant.core import HomeAssistant @@ -27,7 +27,7 @@ def mock_config_entry() -> MockConfigEntry: @pytest.fixture -def mock_plenticore() -> Generator[Plenticore, None, None]: +def mock_plenticore() -> Generator[Plenticore]: """Set up a Plenticore mock with some default values.""" with patch( "homeassistant.components.kostal_plenticore.Plenticore", autospec=True diff --git a/tests/components/kostal_plenticore/test_config_flow.py b/tests/components/kostal_plenticore/test_config_flow.py index d94256ebf1a..c982e2af818 100644 --- a/tests/components/kostal_plenticore/test_config_flow.py +++ b/tests/components/kostal_plenticore/test_config_flow.py @@ -1,10 +1,10 @@ """Test the Kostal Plenticore Solar Inverter config flow.""" -from collections.abc import Generator from unittest.mock import ANY, AsyncMock, MagicMock, patch from pykoplenti import ApiClient, AuthenticationException, SettingsData import pytest +from typing_extensions import Generator from homeassistant import config_entries from homeassistant.components.kostal_plenticore.const import DOMAIN @@ -25,7 +25,7 @@ def mock_apiclient() -> ApiClient: @pytest.fixture -def mock_apiclient_class(mock_apiclient) -> Generator[type[ApiClient], None, None]: +def mock_apiclient_class(mock_apiclient) -> Generator[type[ApiClient]]: """Return a mocked ApiClient class.""" with patch( "homeassistant.components.kostal_plenticore.config_flow.ApiClient", diff --git a/tests/components/kostal_plenticore/test_helper.py b/tests/components/kostal_plenticore/test_helper.py index fe0398a43fc..a18cf32c5a1 100644 --- a/tests/components/kostal_plenticore/test_helper.py +++ b/tests/components/kostal_plenticore/test_helper.py @@ -1,10 +1,10 @@ """Test Kostal Plenticore helper.""" -from collections.abc import Generator from unittest.mock import AsyncMock, MagicMock, patch from pykoplenti import ApiClient, ExtendedApiClient, SettingsData import pytest +from typing_extensions import Generator from homeassistant.components.kostal_plenticore.const import DOMAIN from homeassistant.core import HomeAssistant @@ -14,7 +14,7 @@ from tests.common import MockConfigEntry @pytest.fixture -def mock_apiclient() -> Generator[ApiClient, None, None]: +def mock_apiclient() -> Generator[ApiClient]: """Return a mocked ApiClient class.""" with patch( "homeassistant.components.kostal_plenticore.coordinator.ExtendedApiClient", diff --git a/tests/components/kostal_plenticore/test_number.py b/tests/components/kostal_plenticore/test_number.py index 40ab524ef66..bb401898de5 100644 --- a/tests/components/kostal_plenticore/test_number.py +++ b/tests/components/kostal_plenticore/test_number.py @@ -1,11 +1,11 @@ """Test Kostal Plenticore number.""" -from collections.abc import Generator from datetime import timedelta from unittest.mock import patch from pykoplenti import ApiClient, SettingsData import pytest +from typing_extensions import Generator from homeassistant.components.number import ( ATTR_MAX, @@ -23,7 +23,7 @@ from tests.common import MockConfigEntry, async_fire_time_changed @pytest.fixture -def mock_plenticore_client() -> Generator[ApiClient, None, None]: +def mock_plenticore_client() -> Generator[ApiClient]: """Return a patched ExtendedApiClient.""" with patch( "homeassistant.components.kostal_plenticore.coordinator.ExtendedApiClient", diff --git a/tests/components/lacrosse_view/conftest.py b/tests/components/lacrosse_view/conftest.py index 8edee952bf0..a6294c64210 100644 --- a/tests/components/lacrosse_view/conftest.py +++ b/tests/components/lacrosse_view/conftest.py @@ -1,13 +1,13 @@ """Define fixtures for LaCrosse View tests.""" -from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest +from typing_extensions import Generator @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Override async_setup_entry.""" with patch( "homeassistant.components.lacrosse_view.async_setup_entry", return_value=True diff --git a/tests/components/lamarzocco/conftest.py b/tests/components/lamarzocco/conftest.py index 5c0f344a640..13d2154735d 100644 --- a/tests/components/lamarzocco/conftest.py +++ b/tests/components/lamarzocco/conftest.py @@ -1,10 +1,10 @@ """Lamarzocco session fixtures.""" -from collections.abc import Generator from unittest.mock import MagicMock, patch from lmcloud.const import LaMarzoccoModel import pytest +from typing_extensions import Generator from homeassistant.components.lamarzocco.const import CONF_MACHINE, DOMAIN from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME @@ -59,7 +59,7 @@ def device_fixture() -> LaMarzoccoModel: @pytest.fixture def mock_lamarzocco( request: pytest.FixtureRequest, device_fixture: LaMarzoccoModel -) -> Generator[MagicMock, None, None]: +) -> Generator[MagicMock]: """Return a mocked LM client.""" model_name = device_fixture diff --git a/tests/components/lametric/conftest.py b/tests/components/lametric/conftest.py index 946efda9210..8202caa3b94 100644 --- a/tests/components/lametric/conftest.py +++ b/tests/components/lametric/conftest.py @@ -2,12 +2,12 @@ from __future__ import annotations -from collections.abc import Generator from unittest.mock import AsyncMock, MagicMock, patch from demetriek import CloudDevice, Device from pydantic import parse_raw_as import pytest +from typing_extensions import Generator from homeassistant.components.application_credentials import ( ClientCredential, @@ -46,7 +46,7 @@ def mock_config_entry() -> MockConfigEntry: @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Mock setting up a config entry.""" with patch( "homeassistant.components.lametric.async_setup_entry", return_value=True @@ -55,7 +55,7 @@ def mock_setup_entry() -> Generator[AsyncMock, None, None]: @pytest.fixture -def mock_lametric_cloud() -> Generator[MagicMock, None, None]: +def mock_lametric_cloud() -> Generator[MagicMock]: """Return a mocked LaMetric Cloud client.""" with patch( "homeassistant.components.lametric.config_flow.LaMetricCloud", autospec=True @@ -74,7 +74,7 @@ def device_fixture() -> str: @pytest.fixture -def mock_lametric(device_fixture: str) -> Generator[MagicMock, None, None]: +def mock_lametric(device_fixture: str) -> Generator[MagicMock]: """Return a mocked LaMetric TIME client.""" with ( patch( diff --git a/tests/components/landisgyr_heat_meter/conftest.py b/tests/components/landisgyr_heat_meter/conftest.py index df7e4a44ce9..22f29b3a4b1 100644 --- a/tests/components/landisgyr_heat_meter/conftest.py +++ b/tests/components/landisgyr_heat_meter/conftest.py @@ -1,13 +1,13 @@ """Define fixtures for Landis + Gyr Heat Meter tests.""" -from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest +from typing_extensions import Generator @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Override async_setup_entry.""" with patch( "homeassistant.components.landisgyr_heat_meter.async_setup_entry", diff --git a/tests/components/lawn_mower/test_init.py b/tests/components/lawn_mower/test_init.py index 7dc59fb6f91..e7066ed43c1 100644 --- a/tests/components/lawn_mower/test_init.py +++ b/tests/components/lawn_mower/test_init.py @@ -1,9 +1,9 @@ """The tests for the lawn mower integration.""" -from collections.abc import Generator from unittest.mock import MagicMock import pytest +from typing_extensions import Generator from homeassistant.components.lawn_mower import ( DOMAIN as LAWN_MOWER_DOMAIN, @@ -52,7 +52,7 @@ class MockLawnMowerEntity(LawnMowerEntity): @pytest.fixture(autouse=True) -def config_flow_fixture(hass: HomeAssistant) -> Generator[None, None, None]: +def config_flow_fixture(hass: HomeAssistant) -> Generator[None]: """Mock config flow.""" mock_platform(hass, f"{TEST_DOMAIN}.config_flow") diff --git a/tests/components/lidarr/conftest.py b/tests/components/lidarr/conftest.py index f32d29a7827..588acb2b87f 100644 --- a/tests/components/lidarr/conftest.py +++ b/tests/components/lidarr/conftest.py @@ -2,12 +2,13 @@ from __future__ import annotations -from collections.abc import Awaitable, Callable, Generator +from collections.abc import Awaitable, Callable from http import HTTPStatus from aiohttp.client_exceptions import ClientError from aiopyarr.lidarr_client import LidarrClient import pytest +from typing_extensions import Generator from homeassistant.components.lidarr.const import DOMAIN from homeassistant.const import ( @@ -132,7 +133,7 @@ def mock_config_entry(hass: HomeAssistant) -> MockConfigEntry: async def mock_setup_integration( hass: HomeAssistant, config_entry: MockConfigEntry, -) -> Generator[ComponentSetup, None, None]: +) -> Generator[ComponentSetup]: """Set up the lidarr integration in Home Assistant.""" config_entry.add_to_hass(hass) diff --git a/tests/components/linear_garage_door/conftest.py b/tests/components/linear_garage_door/conftest.py index 5e7fcdeee68..306da23ebf9 100644 --- a/tests/components/linear_garage_door/conftest.py +++ b/tests/components/linear_garage_door/conftest.py @@ -1,9 +1,9 @@ """Common fixtures for the Linear Garage Door tests.""" -from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest +from typing_extensions import Generator from homeassistant.components.linear_garage_door import DOMAIN from homeassistant.const import CONF_EMAIL, CONF_PASSWORD @@ -16,7 +16,7 @@ from tests.common import ( @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Override async_setup_entry.""" with patch( "homeassistant.components.linear_garage_door.async_setup_entry", @@ -26,7 +26,7 @@ def mock_setup_entry() -> Generator[AsyncMock, None, None]: @pytest.fixture -def mock_linear() -> Generator[AsyncMock, None, None]: +def mock_linear() -> Generator[AsyncMock]: """Mock a Linear Garage Door client.""" with ( patch( diff --git a/tests/components/local_calendar/conftest.py b/tests/components/local_calendar/conftest.py index 9556a7c2ca5..8d50036bbbe 100644 --- a/tests/components/local_calendar/conftest.py +++ b/tests/components/local_calendar/conftest.py @@ -1,6 +1,6 @@ """Fixtures for local calendar.""" -from collections.abc import Awaitable, Callable, Generator +from collections.abc import Awaitable, Callable from http import HTTPStatus from pathlib import Path from typing import Any @@ -9,6 +9,7 @@ import urllib from aiohttp import ClientWebSocketResponse import pytest +from typing_extensions import Generator from homeassistant.components.local_calendar import LocalCalendarStore from homeassistant.components.local_calendar.const import CONF_CALENDAR_NAME, DOMAIN @@ -60,9 +61,7 @@ def mock_store_read_side_effect() -> Any | None: @pytest.fixture(name="store", autouse=True) -def mock_store( - ics_content: str, store_read_side_effect: Any | None -) -> Generator[None, None, None]: +def mock_store(ics_content: str, store_read_side_effect: Any | None) -> Generator[None]: """Test cleanup, remove any media storage persisted during the test.""" stores: dict[Path, FakeStore] = {} diff --git a/tests/components/local_todo/conftest.py b/tests/components/local_todo/conftest.py index ca0ef4d3965..67ef76172b7 100644 --- a/tests/components/local_todo/conftest.py +++ b/tests/components/local_todo/conftest.py @@ -1,11 +1,11 @@ """Common fixtures for the local_todo tests.""" -from collections.abc import Generator from pathlib import Path from typing import Any from unittest.mock import AsyncMock, Mock, patch import pytest +from typing_extensions import Generator from homeassistant.components.local_todo import LocalTodoListStore from homeassistant.components.local_todo.const import ( @@ -24,7 +24,7 @@ TEST_ENTITY = "todo.my_tasks" @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Override async_setup_entry.""" with patch( "homeassistant.components.local_todo.async_setup_entry", return_value=True @@ -72,9 +72,7 @@ def mock_store_read_side_effect() -> Any | None: @pytest.fixture(name="store", autouse=True) -def mock_store( - ics_content: str, store_read_side_effect: Any | None -) -> Generator[None, None, None]: +def mock_store(ics_content: str, store_read_side_effect: Any | None) -> Generator[None]: """Fixture that sets up a fake local storage object.""" stores: dict[Path, FakeStore] = {} diff --git a/tests/components/lock/conftest.py b/tests/components/lock/conftest.py index 9c0240b098a..e8291badd0b 100644 --- a/tests/components/lock/conftest.py +++ b/tests/components/lock/conftest.py @@ -1,10 +1,10 @@ """Fixtures for the lock entity platform tests.""" -from collections.abc import Generator from typing import Any from unittest.mock import MagicMock import pytest +from typing_extensions import Generator from homeassistant.components.lock import ( DOMAIN as LOCK_DOMAIN, @@ -65,7 +65,7 @@ class MockFlow(ConfigFlow): @pytest.fixture(autouse=True) -def config_flow_fixture(hass: HomeAssistant) -> Generator[None, None, None]: +def config_flow_fixture(hass: HomeAssistant) -> Generator[None]: """Mock config flow.""" mock_platform(hass, f"{TEST_DOMAIN}.config_flow") diff --git a/tests/components/loqed/conftest.py b/tests/components/loqed/conftest.py index b4265873457..57ef19d0fcb 100644 --- a/tests/components/loqed/conftest.py +++ b/tests/components/loqed/conftest.py @@ -1,12 +1,12 @@ """Contains fixtures for Loqed tests.""" -from collections.abc import AsyncGenerator import json from typing import Any from unittest.mock import AsyncMock, Mock, patch from loqedAPI import loqed import pytest +from typing_extensions import AsyncGenerator from homeassistant.components.loqed import DOMAIN from homeassistant.components.loqed.const import CONF_CLOUDHOOK_URL @@ -81,7 +81,7 @@ def lock_fixture() -> loqed.Lock: @pytest.fixture(name="integration") async def integration_fixture( hass: HomeAssistant, config_entry: MockConfigEntry, lock: loqed.Lock -) -> AsyncGenerator[MockConfigEntry, None]: +) -> AsyncGenerator[MockConfigEntry]: """Set up the loqed integration with a config entry.""" config: dict[str, Any] = {DOMAIN: {CONF_API_TOKEN: ""}} config_entry.add_to_hass(hass) diff --git a/tests/components/lovelace/test_cast.py b/tests/components/lovelace/test_cast.py index f0a193ec705..632ea731d0c 100644 --- a/tests/components/lovelace/test_cast.py +++ b/tests/components/lovelace/test_cast.py @@ -1,10 +1,10 @@ """Test the Lovelace Cast platform.""" -from collections.abc import Generator from time import time from unittest.mock import MagicMock, patch import pytest +from typing_extensions import Generator from homeassistant.components.lovelace import cast as lovelace_cast from homeassistant.components.media_player import MediaClass @@ -17,7 +17,7 @@ from tests.common import async_mock_service @pytest.fixture(autouse=True) -def mock_onboarding_done() -> Generator[MagicMock, None, None]: +def mock_onboarding_done() -> Generator[MagicMock]: """Mock that Home Assistant is currently onboarding. Enabled to prevent creating default dashboards during test execution. diff --git a/tests/components/lovelace/test_dashboard.py b/tests/components/lovelace/test_dashboard.py index affa5e1479f..7577c4dcc0d 100644 --- a/tests/components/lovelace/test_dashboard.py +++ b/tests/components/lovelace/test_dashboard.py @@ -1,11 +1,11 @@ """Test the Lovelace initialization.""" -from collections.abc import Generator import time from typing import Any from unittest.mock import MagicMock, patch import pytest +from typing_extensions import Generator from homeassistant.components import frontend from homeassistant.components.lovelace import const, dashboard @@ -17,7 +17,7 @@ from tests.typing import WebSocketGenerator @pytest.fixture(autouse=True) -def mock_onboarding_done() -> Generator[MagicMock, None, None]: +def mock_onboarding_done() -> Generator[MagicMock]: """Mock that Home Assistant is currently onboarding. Enabled to prevent creating default dashboards during test execution. diff --git a/tests/components/lovelace/test_init.py b/tests/components/lovelace/test_init.py index a88745e4500..dc111ab601e 100644 --- a/tests/components/lovelace/test_init.py +++ b/tests/components/lovelace/test_init.py @@ -1,10 +1,10 @@ """Test the Lovelace initialization.""" -from collections.abc import Generator from typing import Any from unittest.mock import MagicMock, patch import pytest +from typing_extensions import Generator from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component @@ -13,7 +13,7 @@ from tests.typing import WebSocketGenerator @pytest.fixture -def mock_onboarding_not_done() -> Generator[MagicMock, None, None]: +def mock_onboarding_not_done() -> Generator[MagicMock]: """Mock that Home Assistant is currently onboarding.""" with patch( "homeassistant.components.onboarding.async_is_onboarded", @@ -23,7 +23,7 @@ def mock_onboarding_not_done() -> Generator[MagicMock, None, None]: @pytest.fixture -def mock_onboarding_done() -> Generator[MagicMock, None, None]: +def mock_onboarding_done() -> Generator[MagicMock]: """Mock that Home Assistant is currently onboarding.""" with patch( "homeassistant.components.onboarding.async_is_onboarded", @@ -33,7 +33,7 @@ def mock_onboarding_done() -> Generator[MagicMock, None, None]: @pytest.fixture -def mock_add_onboarding_listener() -> Generator[MagicMock, None, None]: +def mock_add_onboarding_listener() -> Generator[MagicMock]: """Mock that Home Assistant is currently onboarding.""" with patch( "homeassistant.components.onboarding.async_add_listener", diff --git a/tests/components/lovelace/test_system_health.py b/tests/components/lovelace/test_system_health.py index 9bd8543004c..d53ebf2871f 100644 --- a/tests/components/lovelace/test_system_health.py +++ b/tests/components/lovelace/test_system_health.py @@ -1,10 +1,10 @@ """Tests for Lovelace system health.""" -from collections.abc import Generator from typing import Any from unittest.mock import MagicMock, patch import pytest +from typing_extensions import Generator from homeassistant.components.lovelace import dashboard from homeassistant.core import HomeAssistant @@ -14,7 +14,7 @@ from tests.common import get_system_health_info @pytest.fixture(autouse=True) -def mock_onboarding_done() -> Generator[MagicMock, None, None]: +def mock_onboarding_done() -> Generator[MagicMock]: """Mock that Home Assistant is currently onboarding. Enabled to prevent creating default dashboards during test execution. diff --git a/tests/components/luftdaten/conftest.py b/tests/components/luftdaten/conftest.py index e083e8c97c7..49e9a85d811 100644 --- a/tests/components/luftdaten/conftest.py +++ b/tests/components/luftdaten/conftest.py @@ -2,10 +2,10 @@ from __future__ import annotations -from collections.abc import Generator from unittest.mock import MagicMock, patch import pytest +from typing_extensions import Generator from homeassistant.components.luftdaten.const import CONF_SENSOR_ID, DOMAIN from homeassistant.const import CONF_SHOW_ON_MAP @@ -26,7 +26,7 @@ def mock_config_entry() -> MockConfigEntry: @pytest.fixture -def mock_setup_entry() -> Generator[None, None, None]: +def mock_setup_entry() -> Generator[None]: """Mock setting up a config entry.""" with patch( "homeassistant.components.luftdaten.async_setup_entry", return_value=True diff --git a/tests/components/lutron/conftest.py b/tests/components/lutron/conftest.py index e94e337ce1d..90f96f1783d 100644 --- a/tests/components/lutron/conftest.py +++ b/tests/components/lutron/conftest.py @@ -1,13 +1,13 @@ """Provide common Lutron fixtures and mocks.""" -from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest +from typing_extensions import Generator @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Override async_setup_entry.""" with patch( "homeassistant.components.lutron.async_setup_entry", return_value=True diff --git a/tests/components/map/test_init.py b/tests/components/map/test_init.py index 69579dd40a6..afafdd1eb16 100644 --- a/tests/components/map/test_init.py +++ b/tests/components/map/test_init.py @@ -1,10 +1,10 @@ """Test the Map initialization.""" -from collections.abc import Generator from typing import Any from unittest.mock import MagicMock, patch import pytest +from typing_extensions import Generator from homeassistant.components.map import DOMAIN from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant @@ -15,7 +15,7 @@ from tests.common import MockModule, mock_integration @pytest.fixture -def mock_onboarding_not_done() -> Generator[MagicMock, None, None]: +def mock_onboarding_not_done() -> Generator[MagicMock]: """Mock that Home Assistant is currently onboarding.""" with patch( "homeassistant.components.onboarding.async_is_onboarded", @@ -25,7 +25,7 @@ def mock_onboarding_not_done() -> Generator[MagicMock, None, None]: @pytest.fixture -def mock_onboarding_done() -> Generator[MagicMock, None, None]: +def mock_onboarding_done() -> Generator[MagicMock]: """Mock that Home Assistant is currently onboarding.""" with patch( "homeassistant.components.onboarding.async_is_onboarded", @@ -35,7 +35,7 @@ def mock_onboarding_done() -> Generator[MagicMock, None, None]: @pytest.fixture -def mock_create_map_dashboard() -> Generator[MagicMock, None, None]: +def mock_create_map_dashboard() -> Generator[MagicMock]: """Mock the create map dashboard function.""" with patch( "homeassistant.components.map._create_map_dashboard", diff --git a/tests/components/matter/conftest.py b/tests/components/matter/conftest.py index a04bf68d28a..05fd776e57a 100644 --- a/tests/components/matter/conftest.py +++ b/tests/components/matter/conftest.py @@ -3,13 +3,13 @@ from __future__ import annotations import asyncio -from collections.abc import AsyncGenerator, Generator from unittest.mock import AsyncMock, MagicMock, patch from matter_server.client.models.node import MatterNode from matter_server.common.const import SCHEMA_VERSION from matter_server.common.models import ServerInfoMessage import pytest +from typing_extensions import AsyncGenerator, Generator from homeassistant.core import HomeAssistant @@ -22,7 +22,7 @@ MOCK_COMPR_FABRIC_ID = 1234 @pytest.fixture(name="matter_client") -async def matter_client_fixture() -> AsyncGenerator[MagicMock, None]: +async def matter_client_fixture() -> AsyncGenerator[MagicMock]: """Fixture for a Matter client.""" with patch( "homeassistant.components.matter.MatterClient", autospec=True @@ -70,7 +70,7 @@ async def integration_fixture( @pytest.fixture(name="create_backup") -def create_backup_fixture() -> Generator[AsyncMock, None, None]: +def create_backup_fixture() -> Generator[AsyncMock]: """Mock Supervisor create backup of add-on.""" with patch( "homeassistant.components.hassio.addon_manager.async_create_backup" @@ -79,7 +79,7 @@ def create_backup_fixture() -> Generator[AsyncMock, None, None]: @pytest.fixture(name="addon_store_info") -def addon_store_info_fixture() -> Generator[AsyncMock, None, None]: +def addon_store_info_fixture() -> Generator[AsyncMock]: """Mock Supervisor add-on store info.""" with patch( "homeassistant.components.hassio.addon_manager.async_get_addon_store_info" @@ -94,7 +94,7 @@ def addon_store_info_fixture() -> Generator[AsyncMock, None, None]: @pytest.fixture(name="addon_info") -def addon_info_fixture() -> Generator[AsyncMock, None, None]: +def addon_info_fixture() -> Generator[AsyncMock]: """Mock Supervisor add-on info.""" with patch( "homeassistant.components.hassio.addon_manager.async_get_addon_info", @@ -158,7 +158,7 @@ def addon_running_fixture( @pytest.fixture(name="install_addon") def install_addon_fixture( addon_store_info: AsyncMock, addon_info: AsyncMock -) -> Generator[AsyncMock, None, None]: +) -> Generator[AsyncMock]: """Mock install add-on.""" async def install_addon_side_effect(hass: HomeAssistant, slug: str) -> None: @@ -181,7 +181,7 @@ def install_addon_fixture( @pytest.fixture(name="start_addon") -def start_addon_fixture() -> Generator[AsyncMock, None, None]: +def start_addon_fixture() -> Generator[AsyncMock]: """Mock start add-on.""" with patch( "homeassistant.components.hassio.addon_manager.async_start_addon" @@ -190,7 +190,7 @@ def start_addon_fixture() -> Generator[AsyncMock, None, None]: @pytest.fixture(name="stop_addon") -def stop_addon_fixture() -> Generator[AsyncMock, None, None]: +def stop_addon_fixture() -> Generator[AsyncMock]: """Mock stop add-on.""" with patch( "homeassistant.components.hassio.addon_manager.async_stop_addon" @@ -199,7 +199,7 @@ def stop_addon_fixture() -> Generator[AsyncMock, None, None]: @pytest.fixture(name="uninstall_addon") -def uninstall_addon_fixture() -> Generator[AsyncMock, None, None]: +def uninstall_addon_fixture() -> Generator[AsyncMock]: """Mock uninstall add-on.""" with patch( "homeassistant.components.hassio.addon_manager.async_uninstall_addon" @@ -208,7 +208,7 @@ def uninstall_addon_fixture() -> Generator[AsyncMock, None, None]: @pytest.fixture(name="update_addon") -def update_addon_fixture() -> Generator[AsyncMock, None, None]: +def update_addon_fixture() -> Generator[AsyncMock]: """Mock update add-on.""" with patch( "homeassistant.components.hassio.addon_manager.async_update_addon" diff --git a/tests/components/matter/test_binary_sensor.py b/tests/components/matter/test_binary_sensor.py index 97a22d6dc98..24928520ee5 100644 --- a/tests/components/matter/test_binary_sensor.py +++ b/tests/components/matter/test_binary_sensor.py @@ -1,10 +1,10 @@ """Test Matter binary sensors.""" -from collections.abc import Generator from unittest.mock import MagicMock, patch from matter_server.client.models.node import MatterNode import pytest +from typing_extensions import Generator from homeassistant.components.matter.binary_sensor import ( DISCOVERY_SCHEMAS as BINARY_SENSOR_SCHEMAS, @@ -21,7 +21,7 @@ from .common import ( @pytest.fixture(autouse=True) -def binary_sensor_platform() -> Generator[None, None, None]: +def binary_sensor_platform() -> Generator[None]: """Load only the binary sensor platform.""" with patch( "homeassistant.components.matter.discovery.DISCOVERY_SCHEMAS", diff --git a/tests/components/matter/test_config_flow.py b/tests/components/matter/test_config_flow.py index 39ae40172c1..562cf4bb86a 100644 --- a/tests/components/matter/test_config_flow.py +++ b/tests/components/matter/test_config_flow.py @@ -2,13 +2,13 @@ from __future__ import annotations -from collections.abc import Generator from ipaddress import ip_address from typing import Any from unittest.mock import DEFAULT, AsyncMock, MagicMock, call, patch from matter_server.client.exceptions import CannotConnect, InvalidServerVersion import pytest +from typing_extensions import Generator from homeassistant import config_entries from homeassistant.components.hassio import HassioAPIError, HassioServiceInfo @@ -58,7 +58,7 @@ ZEROCONF_INFO_UDP = ZeroconfServiceInfo( @pytest.fixture(name="setup_entry", autouse=True) -def setup_entry_fixture() -> Generator[AsyncMock, None, None]: +def setup_entry_fixture() -> Generator[AsyncMock]: """Mock entry setup.""" with patch( "homeassistant.components.matter.async_setup_entry", return_value=True @@ -67,7 +67,7 @@ def setup_entry_fixture() -> Generator[AsyncMock, None, None]: @pytest.fixture(name="unload_entry", autouse=True) -def unload_entry_fixture() -> Generator[AsyncMock, None, None]: +def unload_entry_fixture() -> Generator[AsyncMock]: """Mock entry unload.""" with patch( "homeassistant.components.matter.async_unload_entry", return_value=True @@ -76,7 +76,7 @@ def unload_entry_fixture() -> Generator[AsyncMock, None, None]: @pytest.fixture(name="client_connect", autouse=True) -def client_connect_fixture() -> Generator[AsyncMock, None, None]: +def client_connect_fixture() -> Generator[AsyncMock]: """Mock server version.""" with patch( "homeassistant.components.matter.config_flow.MatterClient.connect" @@ -85,7 +85,7 @@ def client_connect_fixture() -> Generator[AsyncMock, None, None]: @pytest.fixture(name="supervisor") -def supervisor_fixture() -> Generator[MagicMock, None, None]: +def supervisor_fixture() -> Generator[MagicMock]: """Mock Supervisor.""" with patch( "homeassistant.components.matter.config_flow.is_hassio", return_value=True @@ -100,9 +100,7 @@ def discovery_info_fixture() -> Any: @pytest.fixture(name="get_addon_discovery_info", autouse=True) -def get_addon_discovery_info_fixture( - discovery_info: Any, -) -> Generator[AsyncMock, None, None]: +def get_addon_discovery_info_fixture(discovery_info: Any) -> Generator[AsyncMock]: """Mock get add-on discovery info.""" with patch( "homeassistant.components.hassio.addon_manager.async_get_addon_discovery_info", @@ -112,7 +110,7 @@ def get_addon_discovery_info_fixture( @pytest.fixture(name="addon_setup_time", autouse=True) -def addon_setup_time_fixture() -> Generator[int, None, None]: +def addon_setup_time_fixture() -> Generator[int]: """Mock add-on setup sleep time.""" with patch( "homeassistant.components.matter.config_flow.ADDON_SETUP_TIMEOUT", new=0 @@ -121,7 +119,7 @@ def addon_setup_time_fixture() -> Generator[int, None, None]: @pytest.fixture(name="not_onboarded") -def mock_onboarded_fixture() -> Generator[MagicMock, None, None]: +def mock_onboarded_fixture() -> Generator[MagicMock]: """Mock that Home Assistant is not yet onboarded.""" with patch( "homeassistant.components.matter.config_flow.async_is_onboarded", diff --git a/tests/components/matter/test_init.py b/tests/components/matter/test_init.py index 9809220099f..e3d8e799658 100644 --- a/tests/components/matter/test_init.py +++ b/tests/components/matter/test_init.py @@ -3,7 +3,6 @@ from __future__ import annotations import asyncio -from collections.abc import Generator from unittest.mock import AsyncMock, MagicMock, call, patch from matter_server.client.exceptions import CannotConnect, InvalidServerVersion @@ -12,6 +11,7 @@ from matter_server.common.errors import MatterError from matter_server.common.helpers.util import dataclass_from_dict from matter_server.common.models import MatterNodeData import pytest +from typing_extensions import Generator from homeassistant.components.hassio import HassioAPIError from homeassistant.components.matter.const import DOMAIN @@ -32,14 +32,14 @@ from tests.typing import WebSocketGenerator @pytest.fixture(name="connect_timeout") -def connect_timeout_fixture() -> Generator[int, None, None]: +def connect_timeout_fixture() -> Generator[int]: """Mock the connect timeout.""" with patch("homeassistant.components.matter.CONNECT_TIMEOUT", new=0) as timeout: yield timeout @pytest.fixture(name="listen_ready_timeout") -def listen_ready_timeout_fixture() -> Generator[int, None, None]: +def listen_ready_timeout_fixture() -> Generator[int]: """Mock the listen ready timeout.""" with patch( "homeassistant.components.matter.LISTEN_READY_TIMEOUT", new=0 diff --git a/tests/components/media_extractor/conftest.py b/tests/components/media_extractor/conftest.py index 5aca118e2ef..91cff851ab0 100644 --- a/tests/components/media_extractor/conftest.py +++ b/tests/components/media_extractor/conftest.py @@ -1,10 +1,10 @@ """Common fixtures for the Media Extractor tests.""" -from collections.abc import Generator from typing import Any from unittest.mock import AsyncMock, patch import pytest +from typing_extensions import Generator from homeassistant.components.media_extractor import DOMAIN from homeassistant.core import HomeAssistant, ServiceCall @@ -57,7 +57,7 @@ def audio_media_extractor_config() -> dict[str, Any]: @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Override async_setup_entry.""" with patch( "homeassistant.components.media_extractor.async_setup_entry", return_value=True diff --git a/tests/components/media_source/test_local_source.py b/tests/components/media_source/test_local_source.py index 9902aa689ae..4c7fbd06edc 100644 --- a/tests/components/media_source/test_local_source.py +++ b/tests/components/media_source/test_local_source.py @@ -1,6 +1,5 @@ """Test Local Media Source.""" -from collections.abc import AsyncGenerator from http import HTTPStatus import io from pathlib import Path @@ -8,6 +7,7 @@ from tempfile import TemporaryDirectory from unittest.mock import patch import pytest +from typing_extensions import AsyncGenerator from homeassistant.components import media_source, websocket_api from homeassistant.components.media_source import const @@ -20,7 +20,7 @@ from tests.typing import ClientSessionGenerator, WebSocketGenerator @pytest.fixture -async def temp_dir(hass: HomeAssistant) -> AsyncGenerator[str, None]: +async def temp_dir(hass: HomeAssistant) -> AsyncGenerator[str]: """Return a temp dir.""" with TemporaryDirectory() as tmpdirname: target_dir = Path(tmpdirname) / "another_subdir" diff --git a/tests/components/melnor/conftest.py b/tests/components/melnor/conftest.py index 27a4a744202..38bc1a62d51 100644 --- a/tests/components/melnor/conftest.py +++ b/tests/components/melnor/conftest.py @@ -2,12 +2,12 @@ from __future__ import annotations -from collections.abc import Generator from datetime import UTC, datetime, time, timedelta from unittest.mock import AsyncMock, _patch, patch from melnor_bluetooth.device import Device import pytest +from typing_extensions import Generator from homeassistant.components.bluetooth.models import BluetoothServiceInfoBleak from homeassistant.components.melnor.const import DOMAIN @@ -245,7 +245,7 @@ def mock_melnor_device(): @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Patch async setup entry to return True.""" with patch( "homeassistant.components.melnor.async_setup_entry", return_value=True diff --git a/tests/components/mjpeg/conftest.py b/tests/components/mjpeg/conftest.py index e10c267d718..00eaf946113 100644 --- a/tests/components/mjpeg/conftest.py +++ b/tests/components/mjpeg/conftest.py @@ -2,11 +2,11 @@ from __future__ import annotations -from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest from requests_mock import Mocker +from typing_extensions import Generator from homeassistant.components.mjpeg.const import ( CONF_MJPEG_URL, @@ -44,7 +44,7 @@ def mock_config_entry() -> MockConfigEntry: @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Mock setting up a config entry.""" with patch( "homeassistant.components.mjpeg.async_setup_entry", return_value=True @@ -53,7 +53,7 @@ def mock_setup_entry() -> Generator[AsyncMock, None, None]: @pytest.fixture -def mock_reload_entry() -> Generator[AsyncMock, None, None]: +def mock_reload_entry() -> Generator[AsyncMock]: """Mock setting up a config entry.""" with patch("homeassistant.components.mjpeg.async_reload_entry") as mock_reload: yield mock_reload diff --git a/tests/components/moon/conftest.py b/tests/components/moon/conftest.py index 57e957077ab..6fa54fcb603 100644 --- a/tests/components/moon/conftest.py +++ b/tests/components/moon/conftest.py @@ -2,10 +2,10 @@ from __future__ import annotations -from collections.abc import Generator from unittest.mock import patch import pytest +from typing_extensions import Generator from homeassistant.components.moon.const import DOMAIN @@ -22,7 +22,7 @@ def mock_config_entry() -> MockConfigEntry: @pytest.fixture -def mock_setup_entry() -> Generator[None, None, None]: +def mock_setup_entry() -> Generator[None]: """Mock setting up a config entry.""" with patch("homeassistant.components.moon.async_setup_entry", return_value=True): yield diff --git a/tests/components/motionmount/conftest.py b/tests/components/motionmount/conftest.py index f0b8e2f7df7..7d09351fff6 100644 --- a/tests/components/motionmount/conftest.py +++ b/tests/components/motionmount/conftest.py @@ -1,9 +1,9 @@ """Fixtures for Vogel's MotionMount integration tests.""" -from collections.abc import Generator from unittest.mock import AsyncMock, MagicMock, patch import pytest +from typing_extensions import Generator from homeassistant.components.motionmount.const import DOMAIN from homeassistant.const import CONF_HOST, CONF_PORT @@ -25,7 +25,7 @@ def mock_config_entry() -> MockConfigEntry: @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Mock setting up a config entry.""" with patch( "homeassistant.components.motionmount.async_setup_entry", return_value=True diff --git a/tests/components/mqtt/test_config_flow.py b/tests/components/mqtt/test_config_flow.py index f218a5b0447..8df5de8e2fb 100644 --- a/tests/components/mqtt/test_config_flow.py +++ b/tests/components/mqtt/test_config_flow.py @@ -1,6 +1,6 @@ """Test config flow.""" -from collections.abc import Generator, Iterator +from collections.abc import Iterator from contextlib import contextmanager from pathlib import Path from ssl import SSLError @@ -9,6 +9,7 @@ from unittest.mock import AsyncMock, MagicMock, patch from uuid import uuid4 import pytest +from typing_extensions import Generator import voluptuous as vol from homeassistant import config_entries @@ -33,7 +34,7 @@ MOCK_CLIENT_KEY = b"## mock key file ##" @pytest.fixture(autouse=True) -def mock_finish_setup() -> Generator[MagicMock, None, None]: +def mock_finish_setup() -> Generator[MagicMock]: """Mock out the finish setup method.""" with patch( "homeassistant.components.mqtt.MQTT.async_connect", return_value=True @@ -42,7 +43,7 @@ def mock_finish_setup() -> Generator[MagicMock, None, None]: @pytest.fixture -def mock_client_cert_check_fail() -> Generator[MagicMock, None, None]: +def mock_client_cert_check_fail() -> Generator[MagicMock]: """Mock the client certificate check.""" with patch( "homeassistant.components.mqtt.config_flow.load_pem_x509_certificate", @@ -52,7 +53,7 @@ def mock_client_cert_check_fail() -> Generator[MagicMock, None, None]: @pytest.fixture -def mock_client_key_check_fail() -> Generator[MagicMock, None, None]: +def mock_client_key_check_fail() -> Generator[MagicMock]: """Mock the client key file check.""" with patch( "homeassistant.components.mqtt.config_flow.load_pem_private_key", @@ -62,7 +63,7 @@ def mock_client_key_check_fail() -> Generator[MagicMock, None, None]: @pytest.fixture -def mock_ssl_context() -> Generator[dict[str, MagicMock], None, None]: +def mock_ssl_context() -> Generator[dict[str, MagicMock]]: """Mock the SSL context used to load the cert chain and to load verify locations.""" with ( patch("homeassistant.components.mqtt.config_flow.SSLContext") as mock_context, @@ -81,7 +82,7 @@ def mock_ssl_context() -> Generator[dict[str, MagicMock], None, None]: @pytest.fixture -def mock_reload_after_entry_update() -> Generator[MagicMock, None, None]: +def mock_reload_after_entry_update() -> Generator[MagicMock]: """Mock out the reload after updating the entry.""" with patch( "homeassistant.components.mqtt._async_config_entry_updated" @@ -90,14 +91,14 @@ def mock_reload_after_entry_update() -> Generator[MagicMock, None, None]: @pytest.fixture -def mock_try_connection() -> Generator[MagicMock, None, None]: +def mock_try_connection() -> Generator[MagicMock]: """Mock the try connection method.""" with patch("homeassistant.components.mqtt.config_flow.try_connection") as mock_try: yield mock_try @pytest.fixture -def mock_try_connection_success() -> Generator[MqttMockPahoClient, None, None]: +def mock_try_connection_success() -> Generator[MqttMockPahoClient]: """Mock the try connection method with success.""" _mid = 1 @@ -132,7 +133,7 @@ def mock_try_connection_success() -> Generator[MqttMockPahoClient, None, None]: @pytest.fixture -def mock_try_connection_time_out() -> Generator[MagicMock, None, None]: +def mock_try_connection_time_out() -> Generator[MagicMock]: """Mock the try connection method with a time out.""" # Patch prevent waiting 5 sec for a timeout @@ -149,7 +150,7 @@ def mock_try_connection_time_out() -> Generator[MagicMock, None, None]: @pytest.fixture def mock_process_uploaded_file( tmp_path: Path, mock_temp_dir: str -) -> Generator[MagicMock, None, None]: +) -> Generator[MagicMock]: """Mock upload certificate files.""" file_id_ca = str(uuid4()) file_id_cert = str(uuid4()) diff --git a/tests/components/mqtt/test_tag.py b/tests/components/mqtt/test_tag.py index 1575684e164..0d0765258f2 100644 --- a/tests/components/mqtt/test_tag.py +++ b/tests/components/mqtt/test_tag.py @@ -1,11 +1,11 @@ """The tests for MQTT tag scanner.""" -from collections.abc import Generator import copy import json from unittest.mock import ANY, AsyncMock, patch import pytest +from typing_extensions import Generator from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.mqtt.const import DOMAIN as MQTT_DOMAIN @@ -47,7 +47,7 @@ DEFAULT_TAG_SCAN_JSON = ( @pytest.fixture -def tag_mock() -> Generator[AsyncMock, None, None]: +def tag_mock() -> Generator[AsyncMock]: """Fixture to mock tag.""" with patch("homeassistant.components.tag.async_scan_tag") as mock_tag: yield mock_tag diff --git a/tests/components/mysensors/conftest.py b/tests/components/mysensors/conftest.py index 01d6f5d9620..f1b86c9ce5b 100644 --- a/tests/components/mysensors/conftest.py +++ b/tests/components/mysensors/conftest.py @@ -2,7 +2,7 @@ from __future__ import annotations -from collections.abc import AsyncGenerator, Callable, Generator +from collections.abc import Callable from copy import deepcopy import json from typing import Any @@ -12,6 +12,7 @@ from mysensors import BaseSyncGateway from mysensors.persistence import MySensorsJSONDecoder from mysensors.sensor import Sensor import pytest +from typing_extensions import AsyncGenerator, Generator from homeassistant.components.mqtt import DOMAIN as MQTT_DOMAIN from homeassistant.components.mysensors.config_flow import DEFAULT_BAUD_RATE @@ -36,7 +37,7 @@ def mock_mqtt_fixture(hass: HomeAssistant) -> None: @pytest.fixture(name="is_serial_port") -def is_serial_port_fixture() -> Generator[MagicMock, None, None]: +def is_serial_port_fixture() -> Generator[MagicMock]: """Patch the serial port check.""" with patch("homeassistant.components.mysensors.gateway.cv.isdevice") as is_device: is_device.side_effect = lambda device: device @@ -53,7 +54,7 @@ def gateway_nodes_fixture() -> dict[int, Sensor]: async def serial_transport_fixture( gateway_nodes: dict[int, Sensor], is_serial_port: MagicMock, -) -> AsyncGenerator[dict[int, Sensor], None]: +) -> AsyncGenerator[dict[int, Sensor]]: """Mock a serial transport.""" with ( patch( @@ -136,7 +137,7 @@ def config_entry_fixture(serial_entry: MockConfigEntry) -> MockConfigEntry: @pytest.fixture(name="integration") async def integration_fixture( hass: HomeAssistant, transport: MagicMock, config_entry: MockConfigEntry -) -> AsyncGenerator[MockConfigEntry, None]: +) -> AsyncGenerator[MockConfigEntry]: """Set up the mysensors integration with a config entry.""" config: dict[str, Any] = {} config_entry.add_to_hass(hass) diff --git a/tests/components/mystrom/conftest.py b/tests/components/mystrom/conftest.py index 04b8fc221ed..f5405055805 100644 --- a/tests/components/mystrom/conftest.py +++ b/tests/components/mystrom/conftest.py @@ -1,9 +1,9 @@ """Provide common mystrom fixtures and mocks.""" -from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest +from typing_extensions import Generator from homeassistant.components.mystrom.const import DOMAIN from homeassistant.const import CONF_HOST @@ -16,7 +16,7 @@ DEVICE_MAC = "6001940376EB" @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Override async_setup_entry.""" with patch( "homeassistant.components.mystrom.async_setup_entry", return_value=True diff --git a/tests/components/myuplink/conftest.py b/tests/components/myuplink/conftest.py index 3ecb7e08356..dd05bedcaf4 100644 --- a/tests/components/myuplink/conftest.py +++ b/tests/components/myuplink/conftest.py @@ -1,6 +1,5 @@ """Test helpers for myuplink.""" -from collections.abc import AsyncGenerator, Generator import time from typing import Any from unittest.mock import MagicMock, patch @@ -8,6 +7,7 @@ from unittest.mock import MagicMock, patch from myuplink import Device, DevicePoint, System import orjson import pytest +from typing_extensions import AsyncGenerator, Generator from homeassistant.components.application_credentials import ( ClientCredential, @@ -135,7 +135,7 @@ def mock_myuplink_client( device_points_fixture, system_fixture, load_systems_jv_file, -) -> Generator[MagicMock, None, None]: +) -> Generator[MagicMock]: """Mock a myuplink client.""" with patch( @@ -182,7 +182,7 @@ async def setup_platform( hass: HomeAssistant, mock_config_entry: MockConfigEntry, platforms, -) -> AsyncGenerator[None, None]: +) -> AsyncGenerator[None]: """Set up one or all platforms.""" with patch(f"homeassistant.components.{DOMAIN}.PLATFORMS", platforms): diff --git a/tests/components/nest/common.py b/tests/components/nest/common.py index 08e3a4d1ddc..d4eec5ae592 100644 --- a/tests/components/nest/common.py +++ b/tests/components/nest/common.py @@ -2,7 +2,7 @@ from __future__ import annotations -from collections.abc import Awaitable, Callable, Generator +from collections.abc import Awaitable, Callable import copy from dataclasses import dataclass, field import time @@ -14,13 +14,14 @@ from google_nest_sdm.device_manager import DeviceManager from google_nest_sdm.event import EventMessage from google_nest_sdm.event_media import CachePolicy from google_nest_sdm.google_nest_subscriber import GoogleNestSubscriber +from typing_extensions import Generator from homeassistant.components.application_credentials import ClientCredential from homeassistant.components.nest import DOMAIN # Typing helpers type PlatformSetup = Callable[[], Awaitable[None]] -type YieldFixture[_T] = Generator[_T, None, None] +type YieldFixture[_T] = Generator[_T] WEB_AUTH_DOMAIN = DOMAIN APP_AUTH_DOMAIN = f"{DOMAIN}.installed" diff --git a/tests/components/nest/conftest.py b/tests/components/nest/conftest.py index 006792bf35e..de0fc2079fa 100644 --- a/tests/components/nest/conftest.py +++ b/tests/components/nest/conftest.py @@ -3,7 +3,6 @@ from __future__ import annotations from asyncio import AbstractEventLoop -from collections.abc import Generator import copy import shutil import time @@ -16,6 +15,7 @@ from google_nest_sdm import diagnostics from google_nest_sdm.auth import AbstractAuth from google_nest_sdm.device_manager import DeviceManager import pytest +from typing_extensions import Generator from homeassistant.components.application_credentials import ( async_import_client_credential, @@ -298,7 +298,7 @@ async def setup_platform( @pytest.fixture(autouse=True) -def reset_diagnostics() -> Generator[None, None, None]: +def reset_diagnostics() -> Generator[None]: """Fixture to reset client library diagnostic counters.""" yield diagnostics.reset() diff --git a/tests/components/nest/test_init.py b/tests/components/nest/test_init.py index ccd99bb2fd6..f9813ca63ee 100644 --- a/tests/components/nest/test_init.py +++ b/tests/components/nest/test_init.py @@ -8,7 +8,6 @@ mode (e.g. yaml, ConfigEntry, etc) however some tests override and just run in relevant modes. """ -from collections.abc import Generator import logging from typing import Any from unittest.mock import patch @@ -20,6 +19,7 @@ from google_nest_sdm.exceptions import ( SubscriberException, ) import pytest +from typing_extensions import Generator from homeassistant.components.nest import DOMAIN from homeassistant.config_entries import ConfigEntryState @@ -51,7 +51,7 @@ def platforms() -> list[str]: @pytest.fixture def error_caplog( caplog: pytest.LogCaptureFixture, -) -> Generator[pytest.LogCaptureFixture, None, None]: +) -> Generator[pytest.LogCaptureFixture]: """Fixture to capture nest init error messages.""" with caplog.at_level(logging.ERROR, logger="homeassistant.components.nest"): yield caplog @@ -60,7 +60,7 @@ def error_caplog( @pytest.fixture def warning_caplog( caplog: pytest.LogCaptureFixture, -) -> Generator[pytest.LogCaptureFixture, None, None]: +) -> Generator[pytest.LogCaptureFixture]: """Fixture to capture nest init warning messages.""" with caplog.at_level(logging.WARNING, logger="homeassistant.components.nest"): yield caplog diff --git a/tests/components/nest/test_media_source.py b/tests/components/nest/test_media_source.py index 1edfc5d551a..bbc08229d37 100644 --- a/tests/components/nest/test_media_source.py +++ b/tests/components/nest/test_media_source.py @@ -4,7 +4,6 @@ These tests simulate recent camera events received by the subscriber exposed as media in the media source. """ -from collections.abc import Generator import datetime from http import HTTPStatus import io @@ -16,6 +15,7 @@ import av from google_nest_sdm.event import EventMessage import numpy as np import pytest +from typing_extensions import Generator from homeassistant.components.media_player.errors import BrowseError from homeassistant.components.media_source import ( @@ -1097,7 +1097,7 @@ async def test_multiple_devices( @pytest.fixture -def event_store() -> Generator[None, None, None]: +def event_store() -> Generator[None]: """Persist changes to event store immediately.""" with patch( "homeassistant.components.nest.media_source.STORAGE_SAVE_DELAY_SECONDS", diff --git a/tests/components/network/conftest.py b/tests/components/network/conftest.py index d069fff71b6..36d9c449d27 100644 --- a/tests/components/network/conftest.py +++ b/tests/components/network/conftest.py @@ -1,9 +1,9 @@ """Tests for the Network Configuration integration.""" -from collections.abc import Generator from unittest.mock import _patch import pytest +from typing_extensions import Generator @pytest.fixture(autouse=True) @@ -14,7 +14,7 @@ def mock_network(): @pytest.fixture(autouse=True) def override_mock_get_source_ip( mock_get_source_ip: _patch, -) -> Generator[None, None, None]: +) -> Generator[None]: """Override mock of network util's async_get_source_ip.""" mock_get_source_ip.stop() yield diff --git a/tests/components/nextbus/test_config_flow.py b/tests/components/nextbus/test_config_flow.py index 1af2cff0897..0a64bc97d9a 100644 --- a/tests/components/nextbus/test_config_flow.py +++ b/tests/components/nextbus/test_config_flow.py @@ -1,9 +1,9 @@ """Test the NextBus config flow.""" -from collections.abc import Generator from unittest.mock import MagicMock, patch import pytest +from typing_extensions import Generator from homeassistant import config_entries, setup from homeassistant.components.nextbus.const import CONF_AGENCY, CONF_ROUTE, DOMAIN @@ -13,7 +13,7 @@ from homeassistant.data_entry_flow import FlowResultType @pytest.fixture -def mock_setup_entry() -> Generator[MagicMock, None, None]: +def mock_setup_entry() -> Generator[MagicMock]: """Create a mock for the nextbus component setup.""" with patch( "homeassistant.components.nextbus.async_setup_entry", @@ -23,7 +23,7 @@ def mock_setup_entry() -> Generator[MagicMock, None, None]: @pytest.fixture -def mock_nextbus() -> Generator[MagicMock, None, None]: +def mock_nextbus() -> Generator[MagicMock]: """Create a mock py_nextbus module.""" with patch("homeassistant.components.nextbus.config_flow.NextBusClient") as client: yield client diff --git a/tests/components/nextbus/test_sensor.py b/tests/components/nextbus/test_sensor.py index 5e4f322e1eb..3630ff88855 100644 --- a/tests/components/nextbus/test_sensor.py +++ b/tests/components/nextbus/test_sensor.py @@ -1,12 +1,12 @@ """The tests for the nexbus sensor component.""" -from collections.abc import Generator from copy import deepcopy from unittest.mock import MagicMock, patch from urllib.error import HTTPError from py_nextbus.client import NextBusFormatError, NextBusHTTPError import pytest +from typing_extensions import Generator from homeassistant.components import sensor from homeassistant.components.nextbus.const import CONF_AGENCY, CONF_ROUTE, DOMAIN @@ -66,7 +66,7 @@ BASIC_RESULTS = { @pytest.fixture -def mock_nextbus() -> Generator[MagicMock, None, None]: +def mock_nextbus() -> Generator[MagicMock]: """Create a mock py_nextbus module.""" with patch("homeassistant.components.nextbus.coordinator.NextBusClient") as client: yield client @@ -75,7 +75,7 @@ def mock_nextbus() -> Generator[MagicMock, None, None]: @pytest.fixture def mock_nextbus_predictions( mock_nextbus: MagicMock, -) -> Generator[MagicMock, None, None]: +) -> Generator[MagicMock]: """Create a mock of NextBusClient predictions.""" instance = mock_nextbus.return_value instance.get_predictions_for_multi_stops.return_value = BASIC_RESULTS diff --git a/tests/components/nextcloud/conftest.py b/tests/components/nextcloud/conftest.py index 58b37359d42..d6cd39e7fc8 100644 --- a/tests/components/nextcloud/conftest.py +++ b/tests/components/nextcloud/conftest.py @@ -1,9 +1,9 @@ """Fixtrues for the Nextcloud integration tests.""" -from collections.abc import Generator from unittest.mock import AsyncMock, Mock, patch import pytest +from typing_extensions import Generator @pytest.fixture @@ -15,7 +15,7 @@ def mock_nextcloud_monitor() -> Mock: @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Override async_setup_entry.""" with patch( "homeassistant.components.nextcloud.async_setup_entry", return_value=True diff --git a/tests/components/nibe_heatpump/conftest.py b/tests/components/nibe_heatpump/conftest.py index 00d4c92c68b..c44875414e2 100644 --- a/tests/components/nibe_heatpump/conftest.py +++ b/tests/components/nibe_heatpump/conftest.py @@ -1,12 +1,12 @@ """Test configuration for Nibe Heat Pump.""" -from collections.abc import Generator from contextlib import ExitStack from unittest.mock import AsyncMock, Mock, patch from freezegun.api import FrozenDateTimeFactory from nibe.exceptions import CoilNotFoundException import pytest +from typing_extensions import Generator from homeassistant.core import HomeAssistant @@ -16,7 +16,7 @@ from tests.common import async_fire_time_changed @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Make sure we never actually run setup.""" with patch( "homeassistant.components.nibe_heatpump.async_setup_entry", return_value=True diff --git a/tests/components/notify/conftest.py b/tests/components/notify/conftest.py index 23930132f7b..0efb3a4689d 100644 --- a/tests/components/notify/conftest.py +++ b/tests/components/notify/conftest.py @@ -1,8 +1,7 @@ """Fixtures for Notify platform tests.""" -from collections.abc import Generator - import pytest +from typing_extensions import Generator from homeassistant.config_entries import ConfigFlow from homeassistant.core import HomeAssistant @@ -15,7 +14,7 @@ class MockFlow(ConfigFlow): @pytest.fixture -def config_flow_fixture(hass: HomeAssistant) -> Generator[None, None, None]: +def config_flow_fixture(hass: HomeAssistant) -> Generator[None]: """Mock config flow.""" mock_platform(hass, "test.config_flow") diff --git a/tests/components/notion/conftest.py b/tests/components/notion/conftest.py index e69905ed72c..17bea306ad8 100644 --- a/tests/components/notion/conftest.py +++ b/tests/components/notion/conftest.py @@ -1,6 +1,5 @@ """Define fixtures for Notion tests.""" -from collections.abc import Generator import json from unittest.mock import AsyncMock, Mock, patch @@ -9,6 +8,7 @@ from aionotion.listener.models import Listener from aionotion.sensor.models import Sensor from aionotion.user.models import UserPreferences import pytest +from typing_extensions import Generator from homeassistant.components.notion import CONF_REFRESH_TOKEN, CONF_USER_UUID, DOMAIN from homeassistant.const import CONF_USERNAME @@ -23,7 +23,7 @@ TEST_USER_UUID = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Override async_setup_entry.""" with patch( "homeassistant.components.notion.async_setup_entry", return_value=True diff --git a/tests/components/number/test_init.py b/tests/components/number/test_init.py index 1ca1264c53b..9fe9322c731 100644 --- a/tests/components/number/test_init.py +++ b/tests/components/number/test_init.py @@ -1,10 +1,10 @@ """The tests for the Number component.""" -from collections.abc import Generator from typing import Any from unittest.mock import MagicMock import pytest +from typing_extensions import Generator from homeassistant.components.number import ( ATTR_MAX, @@ -859,7 +859,7 @@ class MockFlow(ConfigFlow): @pytest.fixture(autouse=True) -def config_flow_fixture(hass: HomeAssistant) -> Generator[None, None, None]: +def config_flow_fixture(hass: HomeAssistant) -> Generator[None]: """Mock config flow.""" mock_platform(hass, f"{TEST_DOMAIN}.config_flow") From 632238a7f90dae83fbd7857255e4aec6ac8b0ec2 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 6 Jun 2024 17:31:08 +0200 Subject: [PATCH 0302/1445] Move mock_bluetooth* fixtures to decorator (#118846) --- .../test_active_update_coordinator.py | 50 ++++-------- .../bluetooth/test_active_update_processor.py | 48 ++++------- .../components/bluetooth/test_config_flow.py | 39 ++++----- .../components/bluetooth/test_diagnostics.py | 21 ++--- tests/components/bluetooth/test_init.py | 2 +- tests/components/bluetooth/test_manager.py | 6 +- .../test_passive_update_coordinator.py | 22 ++--- .../test_passive_update_processor.py | 80 ++++++------------- .../test_device_tracker.py | 44 +++------- tests/components/default_config/test_init.py | 5 +- tests/components/ibeacon/test_config_flow.py | 5 +- .../private_ble_device/test_config_flow.py | 5 +- 12 files changed, 113 insertions(+), 214 deletions(-) diff --git a/tests/components/bluetooth/test_active_update_coordinator.py b/tests/components/bluetooth/test_active_update_coordinator.py index 0aa59ed0c78..38726143ea5 100644 --- a/tests/components/bluetooth/test_active_update_coordinator.py +++ b/tests/components/bluetooth/test_active_update_coordinator.py @@ -9,6 +9,7 @@ from typing import Any from unittest.mock import MagicMock from bleak.exc import BleakError +import pytest from homeassistant.components.bluetooth import ( DOMAIN, @@ -96,11 +97,8 @@ class MyCoordinator(ActiveBluetoothDataUpdateCoordinator[dict[str, Any]]): super()._async_handle_bluetooth_event(service_info, change) -async def test_basic_usage( - hass: HomeAssistant, - mock_bleak_scanner_start: MagicMock, - mock_bluetooth_adapters: None, -) -> None: +@pytest.mark.usefixtures("mock_bleak_scanner_start", "mock_bluetooth_adapters") +async def test_basic_usage(hass: HomeAssistant) -> None: """Test basic usage of the ActiveBluetoothDataUpdateCoordinator.""" await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) @@ -136,11 +134,8 @@ async def test_basic_usage( unregister_listener() -async def test_bleak_error_during_polling( - hass: HomeAssistant, - mock_bleak_scanner_start: MagicMock, - mock_bluetooth_adapters: None, -) -> None: +@pytest.mark.usefixtures("mock_bleak_scanner_start", "mock_bluetooth_adapters") +async def test_bleak_error_during_polling(hass: HomeAssistant) -> None: """Test bleak error during polling ActiveBluetoothDataUpdateCoordinator.""" await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) poll_count = 0 @@ -189,11 +184,8 @@ async def test_bleak_error_during_polling( unregister_listener() -async def test_generic_exception_during_polling( - hass: HomeAssistant, - mock_bleak_scanner_start: MagicMock, - mock_bluetooth_adapters: None, -) -> None: +@pytest.mark.usefixtures("mock_bleak_scanner_start", "mock_bluetooth_adapters") +async def test_generic_exception_during_polling(hass: HomeAssistant) -> None: """Test generic exception during polling ActiveBluetoothDataUpdateCoordinator.""" await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) poll_count = 0 @@ -242,11 +234,8 @@ async def test_generic_exception_during_polling( unregister_listener() -async def test_polling_debounce( - hass: HomeAssistant, - mock_bleak_scanner_start: MagicMock, - mock_bluetooth_adapters: None, -) -> None: +@pytest.mark.usefixtures("mock_bleak_scanner_start", "mock_bluetooth_adapters") +async def test_polling_debounce(hass: HomeAssistant) -> None: """Test basic usage of the ActiveBluetoothDataUpdateCoordinator.""" await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) poll_count = 0 @@ -288,11 +277,8 @@ async def test_polling_debounce( unregister_listener() -async def test_polling_debounce_with_custom_debouncer( - hass: HomeAssistant, - mock_bleak_scanner_start: MagicMock, - mock_bluetooth_adapters: None, -) -> None: +@pytest.mark.usefixtures("mock_bleak_scanner_start", "mock_bluetooth_adapters") +async def test_polling_debounce_with_custom_debouncer(hass: HomeAssistant) -> None: """Test basic usage of the ActiveBluetoothDataUpdateCoordinator.""" await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) poll_count = 0 @@ -337,11 +323,8 @@ async def test_polling_debounce_with_custom_debouncer( unregister_listener() -async def test_polling_rejecting_the_first_time( - hass: HomeAssistant, - mock_bleak_scanner_start: MagicMock, - mock_bluetooth_adapters: None, -) -> None: +@pytest.mark.usefixtures("mock_bleak_scanner_start", "mock_bluetooth_adapters") +async def test_polling_rejecting_the_first_time(hass: HomeAssistant) -> None: """Test need_poll rejects the first time ActiveBluetoothDataUpdateCoordinator.""" await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) attempt = 0 @@ -399,11 +382,8 @@ async def test_polling_rejecting_the_first_time( unregister_listener() -async def test_no_polling_after_stop_event( - hass: HomeAssistant, - mock_bleak_scanner_start: MagicMock, - mock_bluetooth_adapters: None, -) -> None: +@pytest.mark.usefixtures("mock_bleak_scanner_start", "mock_bluetooth_adapters") +async def test_no_polling_after_stop_event(hass: HomeAssistant) -> None: """Test we do not poll after the stop event.""" await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) needs_poll_calls = 0 diff --git a/tests/components/bluetooth/test_active_update_processor.py b/tests/components/bluetooth/test_active_update_processor.py index e854233451e..e19ef1fd6f8 100644 --- a/tests/components/bluetooth/test_active_update_processor.py +++ b/tests/components/bluetooth/test_active_update_processor.py @@ -49,11 +49,8 @@ GENERIC_BLUETOOTH_SERVICE_INFO_2 = BluetoothServiceInfo( ) -async def test_basic_usage( - hass: HomeAssistant, - mock_bleak_scanner_start: MagicMock, - mock_bluetooth_adapters: None, -) -> None: +@pytest.mark.usefixtures("mock_bleak_scanner_start", "mock_bluetooth_adapters") +async def test_basic_usage(hass: HomeAssistant) -> None: """Test basic usage of the ActiveBluetoothProcessorCoordinator.""" await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) @@ -98,11 +95,8 @@ async def test_basic_usage( cancel() -async def test_poll_can_be_skipped( - hass: HomeAssistant, - mock_bleak_scanner_start: MagicMock, - mock_bluetooth_adapters: None, -) -> None: +@pytest.mark.usefixtures("mock_bleak_scanner_start", "mock_bluetooth_adapters") +async def test_poll_can_be_skipped(hass: HomeAssistant) -> None: """Test need_poll callback works and can skip a poll if its not needed.""" await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) @@ -157,11 +151,9 @@ async def test_poll_can_be_skipped( cancel() +@pytest.mark.usefixtures("mock_bleak_scanner_start", "mock_bluetooth_adapters") async def test_bleak_error_and_recover( - hass: HomeAssistant, - mock_bleak_scanner_start: MagicMock, - mock_bluetooth_adapters: None, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: """Test bleak error handling and recovery.""" await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) @@ -222,11 +214,8 @@ async def test_bleak_error_and_recover( cancel() -async def test_poll_failure_and_recover( - hass: HomeAssistant, - mock_bleak_scanner_start: MagicMock, - mock_bluetooth_adapters: None, -) -> None: +@pytest.mark.usefixtures("mock_bleak_scanner_start", "mock_bluetooth_adapters") +async def test_poll_failure_and_recover(hass: HomeAssistant) -> None: """Test error handling and recovery.""" await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) @@ -281,11 +270,8 @@ async def test_poll_failure_and_recover( cancel() -async def test_second_poll_needed( - hass: HomeAssistant, - mock_bleak_scanner_start: MagicMock, - mock_bluetooth_adapters: None, -) -> None: +@pytest.mark.usefixtures("mock_bleak_scanner_start", "mock_bluetooth_adapters") +async def test_second_poll_needed(hass: HomeAssistant) -> None: """If a poll is queued, by the time it starts it may no longer be needed.""" await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) @@ -332,11 +318,8 @@ async def test_second_poll_needed( cancel() -async def test_rate_limit( - hass: HomeAssistant, - mock_bleak_scanner_start: MagicMock, - mock_bluetooth_adapters: None, -) -> None: +@pytest.mark.usefixtures("mock_bleak_scanner_start", "mock_bluetooth_adapters") +async def test_rate_limit(hass: HomeAssistant) -> None: """Test error handling and recovery.""" await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) @@ -384,11 +367,8 @@ async def test_rate_limit( cancel() -async def test_no_polling_after_stop_event( - hass: HomeAssistant, - mock_bleak_scanner_start: MagicMock, - mock_bluetooth_adapters: None, -) -> None: +@pytest.mark.usefixtures("mock_bleak_scanner_start", "mock_bluetooth_adapters") +async def test_no_polling_after_stop_event(hass: HomeAssistant) -> None: """Test we do not poll after the stop event.""" await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) needs_poll_calls = 0 diff --git a/tests/components/bluetooth/test_config_flow.py b/tests/components/bluetooth/test_config_flow.py index f10c68f8f3f..0a0cb3fa8e0 100644 --- a/tests/components/bluetooth/test_config_flow.py +++ b/tests/components/bluetooth/test_config_flow.py @@ -1,6 +1,6 @@ """Test the bluetooth config flow.""" -from unittest.mock import MagicMock, patch +from unittest.mock import patch from bluetooth_adapters import DEFAULT_ADDRESS, AdapterDetails import pytest @@ -20,12 +20,11 @@ from tests.common import MockConfigEntry from tests.typing import WebSocketGenerator -@pytest.mark.usefixtures("macos_adapter") +@pytest.mark.usefixtures( + "macos_adapter", "mock_bleak_scanner_start", "mock_bluetooth_adapters" +) async def test_options_flow_disabled_not_setup( - hass: HomeAssistant, - hass_ws_client: WebSocketGenerator, - mock_bleak_scanner_start: MagicMock, - mock_bluetooth_adapters: None, + hass: HomeAssistant, hass_ws_client: WebSocketGenerator ) -> None: """Test options are disabled if the integration has not been setup.""" await async_setup_component(hass, "config", {}) @@ -338,12 +337,10 @@ async def test_async_step_integration_discovery_already_exists( assert result["reason"] == "already_configured" -@pytest.mark.usefixtures("one_adapter") -async def test_options_flow_linux( - hass: HomeAssistant, - mock_bleak_scanner_start: MagicMock, - mock_bluetooth_adapters: None, -) -> None: +@pytest.mark.usefixtures( + "one_adapter", "mock_bleak_scanner_start", "mock_bluetooth_adapters" +) +async def test_options_flow_linux(hass: HomeAssistant) -> None: """Test options on Linux.""" entry = MockConfigEntry( domain=DOMAIN, @@ -392,12 +389,11 @@ async def test_options_flow_linux( await hass.config_entries.async_unload(entry.entry_id) -@pytest.mark.usefixtures("macos_adapter") +@pytest.mark.usefixtures( + "macos_adapter", "mock_bleak_scanner_start", "mock_bluetooth_adapters" +) async def test_options_flow_disabled_macos( - hass: HomeAssistant, - hass_ws_client: WebSocketGenerator, - mock_bleak_scanner_start: MagicMock, - mock_bluetooth_adapters: None, + hass: HomeAssistant, hass_ws_client: WebSocketGenerator ) -> None: """Test options are disabled on MacOS.""" await async_setup_component(hass, "config", {}) @@ -422,12 +418,11 @@ async def test_options_flow_disabled_macos( await hass.config_entries.async_unload(entry.entry_id) -@pytest.mark.usefixtures("one_adapter") +@pytest.mark.usefixtures( + "one_adapter", "mock_bleak_scanner_start", "mock_bluetooth_adapters" +) async def test_options_flow_enabled_linux( - hass: HomeAssistant, - hass_ws_client: WebSocketGenerator, - mock_bleak_scanner_start: MagicMock, - mock_bluetooth_adapters: None, + hass: HomeAssistant, hass_ws_client: WebSocketGenerator ) -> None: """Test options are enabled on Linux.""" await async_setup_component(hass, "config", {}) diff --git a/tests/components/bluetooth/test_diagnostics.py b/tests/components/bluetooth/test_diagnostics.py index 7050e665df7..be4412db4d8 100644 --- a/tests/components/bluetooth/test_diagnostics.py +++ b/tests/components/bluetooth/test_diagnostics.py @@ -237,12 +237,11 @@ async def test_diagnostics( @patch("homeassistant.components.bluetooth.HaScanner", FakeHaScanner) -@pytest.mark.usefixtures("macos_adapter") +@pytest.mark.usefixtures( + "macos_adapter", "mock_bleak_scanner_start", "mock_bluetooth_adapters" +) async def test_diagnostics_macos( - hass: HomeAssistant, - hass_client: ClientSessionGenerator, - mock_bleak_scanner_start: MagicMock, - mock_bluetooth_adapters: None, + hass: HomeAssistant, hass_client: ClientSessionGenerator ) -> None: """Test diagnostics for macos.""" # Normally we do not want to patch our classes, but since bleak will import @@ -414,12 +413,14 @@ async def test_diagnostics_macos( @patch("homeassistant.components.bluetooth.HaScanner", FakeHaScanner) -@pytest.mark.usefixtures("enable_bluetooth", "one_adapter") +@pytest.mark.usefixtures( + "enable_bluetooth", + "one_adapter", + "mock_bleak_scanner_start", + "mock_bluetooth_adapters", +) async def test_diagnostics_remote_adapter( - hass: HomeAssistant, - hass_client: ClientSessionGenerator, - mock_bleak_scanner_start: MagicMock, - mock_bluetooth_adapters: None, + hass: HomeAssistant, hass_client: ClientSessionGenerator ) -> None: """Test diagnostics for remote adapter.""" manager = _get_manager() diff --git a/tests/components/bluetooth/test_init.py b/tests/components/bluetooth/test_init.py index 197ca760c5f..f132a6aa150 100644 --- a/tests/components/bluetooth/test_init.py +++ b/tests/components/bluetooth/test_init.py @@ -432,11 +432,11 @@ async def test_discovery_match_by_service_uuid( } ], ) +@pytest.mark.usefixtures("mock_bluetooth_adapters") async def test_discovery_match_by_service_uuid_and_short_local_name( mock_async_get_bluetooth: AsyncMock, hass: HomeAssistant, mock_bleak_scanner_start: MagicMock, - mock_bluetooth_adapters: None, ) -> None: """Test bluetooth discovery match by service_uuid and short local name.""" entry = MockConfigEntry(domain="bluetooth", unique_id="00:00:00:00:00:01") diff --git a/tests/components/bluetooth/test_manager.py b/tests/components/bluetooth/test_manager.py index f8cdc654b65..6a607838d36 100644 --- a/tests/components/bluetooth/test_manager.py +++ b/tests/components/bluetooth/test_manager.py @@ -744,8 +744,9 @@ async def test_switching_adapters_when_one_stop_scanning( cancel_hci2() +@pytest.mark.usefixtures("mock_bluetooth_adapters") async def test_goes_unavailable_connectable_only_and_recovers( - hass: HomeAssistant, mock_bluetooth_adapters: None + hass: HomeAssistant, ) -> None: """Test all connectable scanners go unavailable, and than recover when there is a non-connectable scanner.""" assert await async_setup_component(hass, bluetooth.DOMAIN, {}) @@ -907,8 +908,9 @@ async def test_goes_unavailable_connectable_only_and_recovers( unsetup_not_connectable_scanner() +@pytest.mark.usefixtures("mock_bluetooth_adapters") async def test_goes_unavailable_dismisses_discovery_and_makes_discoverable( - hass: HomeAssistant, mock_bluetooth_adapters: None + hass: HomeAssistant, ) -> None: """Test that unavailable will dismiss any active discoveries and make device discoverable again.""" mock_bt = [ diff --git a/tests/components/bluetooth/test_passive_update_coordinator.py b/tests/components/bluetooth/test_passive_update_coordinator.py index 54d4f8d5662..53a18e88683 100644 --- a/tests/components/bluetooth/test_passive_update_coordinator.py +++ b/tests/components/bluetooth/test_passive_update_coordinator.py @@ -8,6 +8,8 @@ import time from typing import Any from unittest.mock import MagicMock, patch +import pytest + from homeassistant.components.bluetooth import ( DOMAIN, FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS, @@ -65,11 +67,8 @@ class MyCoordinator(PassiveBluetoothDataUpdateCoordinator): super()._async_handle_bluetooth_event(service_info, change) -async def test_basic_usage( - hass: HomeAssistant, - mock_bleak_scanner_start: MagicMock, - mock_bluetooth_adapters: None, -) -> None: +@pytest.mark.usefixtures("mock_bleak_scanner_start", "mock_bluetooth_adapters") +async def test_basic_usage(hass: HomeAssistant) -> None: """Test basic usage of the PassiveBluetoothDataUpdateCoordinator.""" await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) coordinator = MyCoordinator( @@ -97,10 +96,9 @@ async def test_basic_usage( cancel() +@pytest.mark.usefixtures("mock_bleak_scanner_start", "mock_bluetooth_adapters") async def test_context_compatiblity_with_data_update_coordinator( hass: HomeAssistant, - mock_bleak_scanner_start: MagicMock, - mock_bluetooth_adapters: None, ) -> None: """Test contexts can be passed for compatibility with DataUpdateCoordinator.""" await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) @@ -135,10 +133,9 @@ async def test_context_compatiblity_with_data_update_coordinator( assert not set(coordinator.async_contexts()) +@pytest.mark.usefixtures("mock_bleak_scanner_start", "mock_bluetooth_adapters") async def test_unavailable_callbacks_mark_the_coordinator_unavailable( hass: HomeAssistant, - mock_bleak_scanner_start: MagicMock, - mock_bluetooth_adapters: None, ) -> None: """Test that the coordinator goes unavailable when the bluetooth stack no longer sees the device.""" start_monotonic = time.monotonic() @@ -196,11 +193,8 @@ async def test_unavailable_callbacks_mark_the_coordinator_unavailable( assert coordinator.available is False -async def test_passive_bluetooth_coordinator_entity( - hass: HomeAssistant, - mock_bleak_scanner_start: MagicMock, - mock_bluetooth_adapters: None, -) -> None: +@pytest.mark.usefixtures("mock_bleak_scanner_start", "mock_bluetooth_adapters") +async def test_passive_bluetooth_coordinator_entity(hass: HomeAssistant) -> None: """Test integration of PassiveBluetoothDataUpdateCoordinator with PassiveBluetoothCoordinatorEntity.""" await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) coordinator = MyCoordinator( diff --git a/tests/components/bluetooth/test_passive_update_processor.py b/tests/components/bluetooth/test_passive_update_processor.py index 047034bbf63..24cf344a31c 100644 --- a/tests/components/bluetooth/test_passive_update_processor.py +++ b/tests/components/bluetooth/test_passive_update_processor.py @@ -174,11 +174,8 @@ GENERIC_PASSIVE_BLUETOOTH_DATA_UPDATE_WITH_DEVICE_NAME_AND_TEMP_CHANGE = ( ) -async def test_basic_usage( - hass: HomeAssistant, - mock_bleak_scanner_start: MagicMock, - mock_bluetooth_adapters: None, -) -> None: +@pytest.mark.usefixtures("mock_bleak_scanner_start", "mock_bluetooth_adapters") +async def test_basic_usage(hass: HomeAssistant) -> None: """Test basic usage of the PassiveBluetoothProcessorCoordinator.""" await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) @@ -276,10 +273,9 @@ async def test_basic_usage( cancel_coordinator() +@pytest.mark.usefixtures("mock_bleak_scanner_start", "mock_bluetooth_adapters") async def test_entity_key_is_dispatched_on_entity_key_change( hass: HomeAssistant, - mock_bleak_scanner_start: MagicMock, - mock_bluetooth_adapters: None, ) -> None: """Test entity key listeners are only dispatched on change.""" await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) @@ -398,11 +394,8 @@ async def test_entity_key_is_dispatched_on_entity_key_change( cancel_coordinator() -async def test_unavailable_after_no_data( - hass: HomeAssistant, - mock_bleak_scanner_start: MagicMock, - mock_bluetooth_adapters: None, -) -> None: +@pytest.mark.usefixtures("mock_bleak_scanner_start", "mock_bluetooth_adapters") +async def test_unavailable_after_no_data(hass: HomeAssistant) -> None: """Test that the coordinator is unavailable after no data for a while.""" start_monotonic = time.monotonic() @@ -513,11 +506,8 @@ async def test_unavailable_after_no_data( cancel_coordinator() -async def test_no_updates_once_stopping( - hass: HomeAssistant, - mock_bleak_scanner_start: MagicMock, - mock_bluetooth_adapters: None, -) -> None: +@pytest.mark.usefixtures("mock_bleak_scanner_start", "mock_bluetooth_adapters") +async def test_no_updates_once_stopping(hass: HomeAssistant) -> None: """Test updates are ignored once hass is stopping.""" await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) @@ -570,11 +560,9 @@ async def test_no_updates_once_stopping( cancel_coordinator() +@pytest.mark.usefixtures("mock_bleak_scanner_start", "mock_bluetooth_adapters") async def test_exception_from_update_method( - hass: HomeAssistant, - caplog: pytest.LogCaptureFixture, - mock_bleak_scanner_start: MagicMock, - mock_bluetooth_adapters: None, + hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: """Test we handle exceptions from the update method.""" await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) @@ -639,11 +627,8 @@ async def test_exception_from_update_method( cancel_coordinator() -async def test_bad_data_from_update_method( - hass: HomeAssistant, - mock_bleak_scanner_start: MagicMock, - mock_bluetooth_adapters: None, -) -> None: +@pytest.mark.usefixtures("mock_bleak_scanner_start", "mock_bluetooth_adapters") +async def test_bad_data_from_update_method(hass: HomeAssistant) -> None: """Test we handle bad data from the update method.""" await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) @@ -996,11 +981,8 @@ GOVEE_B5178_PRIMARY_AND_REMOTE_PASSIVE_BLUETOOTH_DATA_UPDATE = ( ) -async def test_integration_with_entity( - hass: HomeAssistant, - mock_bleak_scanner_start: MagicMock, - mock_bluetooth_adapters: None, -) -> None: +@pytest.mark.usefixtures("mock_bleak_scanner_start", "mock_bluetooth_adapters") +async def test_integration_with_entity(hass: HomeAssistant) -> None: """Test integration of PassiveBluetoothProcessorCoordinator with PassiveBluetoothCoordinatorEntity.""" await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) @@ -1158,11 +1140,8 @@ NO_DEVICES_PASSIVE_BLUETOOTH_DATA_UPDATE = PassiveBluetoothDataUpdate( ) -async def test_integration_with_entity_without_a_device( - hass: HomeAssistant, - mock_bleak_scanner_start: MagicMock, - mock_bluetooth_adapters: None, -) -> None: +@pytest.mark.usefixtures("mock_bleak_scanner_start", "mock_bluetooth_adapters") +async def test_integration_with_entity_without_a_device(hass: HomeAssistant) -> None: """Test integration with PassiveBluetoothCoordinatorEntity with no device.""" await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) @@ -1224,10 +1203,9 @@ async def test_integration_with_entity_without_a_device( cancel_coordinator() +@pytest.mark.usefixtures("mock_bleak_scanner_start", "mock_bluetooth_adapters") async def test_passive_bluetooth_entity_with_entity_platform( hass: HomeAssistant, - mock_bleak_scanner_start: MagicMock, - mock_bluetooth_adapters: None, ) -> None: """Test with a mock entity platform.""" await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) @@ -1331,11 +1309,8 @@ DEVICE_ONLY_PASSIVE_BLUETOOTH_DATA_UPDATE = PassiveBluetoothDataUpdate( ) -async def test_integration_multiple_entity_platforms( - hass: HomeAssistant, - mock_bleak_scanner_start: MagicMock, - mock_bluetooth_adapters: None, -) -> None: +@pytest.mark.usefixtures("mock_bleak_scanner_start", "mock_bluetooth_adapters") +async def test_integration_multiple_entity_platforms(hass: HomeAssistant) -> None: """Test integration of PassiveBluetoothProcessorCoordinator with multiple platforms.""" await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) @@ -1426,11 +1401,9 @@ async def test_integration_multiple_entity_platforms( cancel_coordinator() +@pytest.mark.usefixtures("mock_bleak_scanner_start", "mock_bluetooth_adapters") async def test_exception_from_coordinator_update_method( - hass: HomeAssistant, - caplog: pytest.LogCaptureFixture, - mock_bleak_scanner_start: MagicMock, - mock_bluetooth_adapters: None, + hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: """Test we handle exceptions from the update method.""" await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) @@ -1485,11 +1458,9 @@ async def test_exception_from_coordinator_update_method( cancel_coordinator() +@pytest.mark.usefixtures("mock_bleak_scanner_start", "mock_bluetooth_adapters") async def test_integration_multiple_entity_platforms_with_reload_and_restart( - hass: HomeAssistant, - mock_bleak_scanner_start: MagicMock, - mock_bluetooth_adapters: None, - hass_storage: dict[str, Any], + hass: HomeAssistant, hass_storage: dict[str, Any] ) -> None: """Test integration of PassiveBluetoothProcessorCoordinator with multiple platforms with reload.""" await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) @@ -1791,11 +1762,8 @@ NAMING_PASSIVE_BLUETOOTH_DATA_UPDATE = PassiveBluetoothDataUpdate( ) -async def test_naming( - hass: HomeAssistant, - mock_bleak_scanner_start: MagicMock, - mock_bluetooth_adapters: None, -) -> None: +@pytest.mark.usefixtures("mock_bleak_scanner_start", "mock_bluetooth_adapters") +async def test_naming(hass: HomeAssistant) -> None: """Test basic usage of the PassiveBluetoothProcessorCoordinator.""" await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) diff --git a/tests/components/bluetooth_le_tracker/test_device_tracker.py b/tests/components/bluetooth_le_tracker/test_device_tracker.py index 6346b094eab..f183f987cde 100644 --- a/tests/components/bluetooth_le_tracker/test_device_tracker.py +++ b/tests/components/bluetooth_le_tracker/test_device_tracker.py @@ -5,6 +5,7 @@ from unittest.mock import patch from bleak import BleakError from freezegun import freeze_time +import pytest from homeassistant.components.bluetooth import BluetoothServiceInfoBleak from homeassistant.components.bluetooth_le_tracker import device_tracker @@ -17,7 +18,6 @@ from homeassistant.components.device_tracker import ( CONF_SCAN_INTERVAL, CONF_TRACK_NEW, DOMAIN, - legacy, ) from homeassistant.const import CONF_PLATFORM from homeassistant.core import HomeAssistant @@ -66,11 +66,8 @@ class MockBleakClientBattery5(MockBleakClient): return b"\x05" -async def test_do_not_see_device_if_time_not_updated( - hass: HomeAssistant, - mock_bluetooth: None, - mock_device_tracker_conf: list[legacy.Device], -) -> None: +@pytest.mark.usefixtures("mock_bluetooth", "mock_device_tracker_conf") +async def test_do_not_see_device_if_time_not_updated(hass: HomeAssistant) -> None: """Test device going not_home after consider_home threshold from first scan if the subsequent scans have not incremented last seen time.""" address = "DE:AD:BE:EF:13:37" @@ -132,11 +129,8 @@ async def test_do_not_see_device_if_time_not_updated( assert state.state == "not_home" -async def test_see_device_if_time_updated( - hass: HomeAssistant, - mock_bluetooth: None, - mock_device_tracker_conf: list[legacy.Device], -) -> None: +@pytest.mark.usefixtures("mock_bluetooth", "mock_device_tracker_conf") +async def test_see_device_if_time_updated(hass: HomeAssistant) -> None: """Test device remaining home after consider_home threshold from first scan if the subsequent scans have incremented last seen time.""" address = "DE:AD:BE:EF:13:37" @@ -214,11 +208,8 @@ async def test_see_device_if_time_updated( assert state.state == "home" -async def test_preserve_new_tracked_device_name( - hass: HomeAssistant, - mock_bluetooth: None, - mock_device_tracker_conf: list[legacy.Device], -) -> None: +@pytest.mark.usefixtures("mock_bluetooth", "mock_device_tracker_conf") +async def test_preserve_new_tracked_device_name(hass: HomeAssistant) -> None: """Test preserving tracked device name across new seens.""" address = "DE:AD:BE:EF:13:37" @@ -284,11 +275,8 @@ async def test_preserve_new_tracked_device_name( assert state.name == name -async def test_tracking_battery_times_out( - hass: HomeAssistant, - mock_bluetooth: None, - mock_device_tracker_conf: list[legacy.Device], -) -> None: +@pytest.mark.usefixtures("mock_bluetooth", "mock_device_tracker_conf") +async def test_tracking_battery_times_out(hass: HomeAssistant) -> None: """Test tracking the battery times out.""" address = "DE:AD:BE:EF:13:37" @@ -353,11 +341,8 @@ async def test_tracking_battery_times_out( assert "battery" not in state.attributes -async def test_tracking_battery_fails( - hass: HomeAssistant, - mock_bluetooth: None, - mock_device_tracker_conf: list[legacy.Device], -) -> None: +@pytest.mark.usefixtures("mock_bluetooth", "mock_device_tracker_conf") +async def test_tracking_battery_fails(hass: HomeAssistant) -> None: """Test tracking the battery fails.""" address = "DE:AD:BE:EF:13:37" @@ -421,11 +406,8 @@ async def test_tracking_battery_fails( assert "battery" not in state.attributes -async def test_tracking_battery_successful( - hass: HomeAssistant, - mock_bluetooth: None, - mock_device_tracker_conf: list[legacy.Device], -) -> None: +@pytest.mark.usefixtures("mock_bluetooth", "mock_device_tracker_conf") +async def test_tracking_battery_successful(hass: HomeAssistant) -> None: """Test tracking the battery gets a value.""" address = "DE:AD:BE:EF:13:37" diff --git a/tests/components/default_config/test_init.py b/tests/components/default_config/test_init.py index 9f8467af9db..1a6665b2404 100644 --- a/tests/components/default_config/test_init.py +++ b/tests/components/default_config/test_init.py @@ -33,9 +33,8 @@ def recorder_url_mock(): yield -async def test_setup( - hass: HomeAssistant, mock_zeroconf: None, mock_bluetooth: None -) -> None: +@pytest.mark.usefixtures("mock_bluetooth", "mock_zeroconf") +async def test_setup(hass: HomeAssistant) -> None: """Test setup.""" recorder_helper.async_initialize_recorder(hass) # default_config needs the homeassistant integration, assert it will be diff --git a/tests/components/ibeacon/test_config_flow.py b/tests/components/ibeacon/test_config_flow.py index 3b5aadfaeab..f7a1aec7edb 100644 --- a/tests/components/ibeacon/test_config_flow.py +++ b/tests/components/ibeacon/test_config_flow.py @@ -12,9 +12,8 @@ from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry -async def test_setup_user_no_bluetooth( - hass: HomeAssistant, mock_bluetooth_adapters: None -) -> None: +@pytest.mark.usefixtures("mock_bluetooth_adapters") +async def test_setup_user_no_bluetooth(hass: HomeAssistant) -> None: """Test setting up via user interaction when bluetooth is not enabled.""" result = await hass.config_entries.flow.async_init( DOMAIN, diff --git a/tests/components/private_ble_device/test_config_flow.py b/tests/components/private_ble_device/test_config_flow.py index 7c9b4807621..0d4ebdfd99d 100644 --- a/tests/components/private_ble_device/test_config_flow.py +++ b/tests/components/private_ble_device/test_config_flow.py @@ -20,9 +20,8 @@ def assert_form_error(result: FlowResult, key: str, value: str) -> None: assert result["errors"][key] == value -async def test_setup_user_no_bluetooth( - hass: HomeAssistant, mock_bluetooth_adapters: None -) -> None: +@pytest.mark.usefixtures("mock_bluetooth_adapters") +async def test_setup_user_no_bluetooth(hass: HomeAssistant) -> None: """Test setting up via user interaction when bluetooth is not enabled.""" result = await hass.config_entries.flow.async_init( const.DOMAIN, From 59e178df3b86426fe4a0319e32c40af3efa6547d Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Thu, 6 Jun 2024 17:33:27 +0200 Subject: [PATCH 0303/1445] Import Generator from typing_extensions (5) (#118993) --- tests/components/tailscale/conftest.py | 4 ++-- tests/components/tailwind/conftest.py | 6 +++--- tests/components/tami4/conftest.py | 12 ++++++------ tests/components/tankerkoenig/conftest.py | 4 ++-- tests/components/technove/conftest.py | 8 ++++---- tests/components/tedee/conftest.py | 6 +++--- tests/components/time_date/conftest.py | 4 ++-- tests/components/todo/test_init.py | 4 ++-- tests/components/todoist/conftest.py | 4 ++-- tests/components/tplink/conftest.py | 6 +++--- tests/components/tplink_omada/conftest.py | 10 ++++------ tests/components/traccar_server/conftest.py | 4 ++-- tests/components/traccar_server/test_config_flow.py | 12 ++++++------ tests/components/traccar_server/test_diagnostics.py | 8 ++++---- tests/components/tractive/conftest.py | 4 ++-- tests/components/tradfri/conftest.py | 7 ++++--- tests/components/tts/common.py | 10 +++++----- tests/components/tts/conftest.py | 4 ++-- tests/components/tuya/conftest.py | 6 +++--- tests/components/twentemilieu/conftest.py | 6 +++--- tests/components/twitch/__init__.py | 7 ++++--- tests/components/twitch/conftest.py | 6 +++--- tests/components/update/test_init.py | 4 ++-- tests/components/uptime/conftest.py | 4 ++-- tests/components/v2c/conftest.py | 6 +++--- tests/components/vacuum/conftest.py | 5 ++--- tests/components/valve/test_init.py | 5 ++--- tests/components/velbus/conftest.py | 4 ++-- tests/components/velbus/test_config_flow.py | 6 +++--- tests/components/velux/conftest.py | 4 ++-- tests/components/verisure/conftest.py | 4 ++-- tests/components/vicare/conftest.py | 6 +++--- tests/components/vilfo/conftest.py | 8 ++++---- tests/components/wake_word/test_init.py | 5 +++-- tests/components/waqi/conftest.py | 4 ++-- tests/components/water_heater/conftest.py | 5 ++--- tests/components/weather/conftest.py | 5 ++--- tests/components/weatherflow/conftest.py | 10 +++++----- tests/components/weatherflow_cloud/conftest.py | 10 +++++----- tests/components/weatherkit/conftest.py | 4 ++-- tests/components/webmin/conftest.py | 4 ++-- tests/components/webostv/conftest.py | 4 ++-- tests/components/whois/conftest.py | 8 ++++---- tests/components/wiffi/conftest.py | 4 ++-- tests/components/wled/conftest.py | 8 ++++---- tests/components/workday/conftest.py | 4 ++-- tests/components/wyoming/conftest.py | 4 ++-- tests/components/xiaomi_ble/conftest.py | 4 ++-- tests/components/xiaomi_miio/test_vacuum.py | 4 ++-- tests/components/yardian/conftest.py | 4 ++-- tests/components/youtube/__init__.py | 10 +++++----- tests/components/zamg/conftest.py | 4 ++-- tests/components/zha/conftest.py | 5 +++-- tests/components/zha/test_radio_manager.py | 4 ++-- tests/components/zwave_js/test_config_flow.py | 6 +++--- 55 files changed, 158 insertions(+), 160 deletions(-) diff --git a/tests/components/tailscale/conftest.py b/tests/components/tailscale/conftest.py index 5cf3f344739..c07717cd31e 100644 --- a/tests/components/tailscale/conftest.py +++ b/tests/components/tailscale/conftest.py @@ -2,11 +2,11 @@ from __future__ import annotations -from collections.abc import Generator from unittest.mock import AsyncMock, MagicMock, patch import pytest from tailscale.models import Devices +from typing_extensions import Generator from homeassistant.components.tailscale.const import CONF_TAILNET, DOMAIN from homeassistant.const import CONF_API_KEY @@ -27,7 +27,7 @@ def mock_config_entry() -> MockConfigEntry: @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Mock setting up a config entry.""" with patch( "homeassistant.components.tailscale.async_setup_entry", return_value=True diff --git a/tests/components/tailwind/conftest.py b/tests/components/tailwind/conftest.py index b7443e59581..f23463548bc 100644 --- a/tests/components/tailwind/conftest.py +++ b/tests/components/tailwind/conftest.py @@ -2,11 +2,11 @@ from __future__ import annotations -from collections.abc import Generator from unittest.mock import AsyncMock, MagicMock, patch from gotailwind import TailwindDeviceStatus import pytest +from typing_extensions import Generator from homeassistant.components.tailwind.const import DOMAIN from homeassistant.const import CONF_HOST, CONF_TOKEN @@ -36,7 +36,7 @@ def mock_config_entry() -> MockConfigEntry: @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Mock setting up a config entry.""" with patch( "homeassistant.components.tailwind.async_setup_entry", return_value=True @@ -45,7 +45,7 @@ def mock_setup_entry() -> Generator[AsyncMock, None, None]: @pytest.fixture -def mock_tailwind(device_fixture: str) -> Generator[MagicMock, None, None]: +def mock_tailwind(device_fixture: str) -> Generator[MagicMock]: """Return a mocked Tailwind client.""" with ( patch( diff --git a/tests/components/tami4/conftest.py b/tests/components/tami4/conftest.py index 64d45cfeca7..26d6e043dea 100644 --- a/tests/components/tami4/conftest.py +++ b/tests/components/tami4/conftest.py @@ -1,12 +1,12 @@ """Common fixutres with default mocks as well as common test helper methods.""" -from collections.abc import Generator from datetime import datetime from unittest.mock import AsyncMock, MagicMock, patch import pytest from Tami4EdgeAPI.device import Device from Tami4EdgeAPI.water_quality import UV, Filter, WaterQuality +from typing_extensions import Generator from homeassistant.components.tami4.const import CONF_REFRESH_TOKEN, DOMAIN from homeassistant.core import HomeAssistant @@ -37,7 +37,7 @@ def mock_api(mock__get_devices, mock_get_water_quality): @pytest.fixture -def mock__get_devices(request: pytest.FixtureRequest) -> Generator[None, None, None]: +def mock__get_devices(request: pytest.FixtureRequest) -> Generator[None]: """Fixture to mock _get_devices which makes a call to the API.""" side_effect = getattr(request, "param", None) @@ -62,7 +62,7 @@ def mock__get_devices(request: pytest.FixtureRequest) -> Generator[None, None, N @pytest.fixture def mock_get_water_quality( request: pytest.FixtureRequest, -) -> Generator[None, None, None]: +) -> Generator[None]: """Fixture to mock get_water_quality which makes a call to the API.""" side_effect = getattr(request, "param", None) @@ -90,7 +90,7 @@ def mock_get_water_quality( @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Mock setting up a config entry.""" with patch( @@ -102,7 +102,7 @@ def mock_setup_entry() -> Generator[AsyncMock, None, None]: @pytest.fixture def mock_request_otp( request: pytest.FixtureRequest, -) -> Generator[MagicMock, None, None]: +) -> Generator[MagicMock]: """Mock request_otp.""" side_effect = getattr(request, "param", None) @@ -116,7 +116,7 @@ def mock_request_otp( @pytest.fixture -def mock_submit_otp(request: pytest.FixtureRequest) -> Generator[MagicMock, None, None]: +def mock_submit_otp(request: pytest.FixtureRequest) -> Generator[MagicMock]: """Mock submit_otp.""" side_effect = getattr(request, "param", None) diff --git a/tests/components/tankerkoenig/conftest.py b/tests/components/tankerkoenig/conftest.py index 1a3dcb6f991..8f2e2c2fb53 100644 --- a/tests/components/tankerkoenig/conftest.py +++ b/tests/components/tankerkoenig/conftest.py @@ -1,9 +1,9 @@ """Fixtures for Tankerkoenig integration tests.""" -from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest +from typing_extensions import Generator from homeassistant.components.tankerkoenig import DOMAIN from homeassistant.const import CONF_SHOW_ON_MAP @@ -16,7 +16,7 @@ from tests.common import MockConfigEntry @pytest.fixture(name="tankerkoenig") -def mock_tankerkoenig() -> Generator[AsyncMock, None, None]: +def mock_tankerkoenig() -> Generator[AsyncMock]: """Mock the aiotankerkoenig client.""" with ( patch( diff --git a/tests/components/technove/conftest.py b/tests/components/technove/conftest.py index 06db6e24f47..be34ebfefa5 100644 --- a/tests/components/technove/conftest.py +++ b/tests/components/technove/conftest.py @@ -1,10 +1,10 @@ """Fixtures for TechnoVE integration tests.""" -from collections.abc import Generator from unittest.mock import AsyncMock, MagicMock, patch import pytest from technove import Station as TechnoVEStation +from typing_extensions import Generator from homeassistant.components.technove.const import DOMAIN from homeassistant.const import CONF_HOST @@ -24,7 +24,7 @@ def mock_config_entry() -> MockConfigEntry: @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Mock setting up a config entry.""" with patch( "homeassistant.components.technove.async_setup_entry", return_value=True @@ -33,7 +33,7 @@ def mock_setup_entry() -> Generator[AsyncMock, None, None]: @pytest.fixture -def mock_onboarding() -> Generator[MagicMock, None, None]: +def mock_onboarding() -> Generator[MagicMock]: """Mock that Home Assistant is currently onboarding.""" with patch( "homeassistant.components.onboarding.async_is_onboarded", @@ -49,7 +49,7 @@ def device_fixture() -> TechnoVEStation: @pytest.fixture -def mock_technove(device_fixture: TechnoVEStation) -> Generator[MagicMock, None, None]: +def mock_technove(device_fixture: TechnoVEStation) -> Generator[MagicMock]: """Return a mocked TechnoVE client.""" with ( patch( diff --git a/tests/components/tedee/conftest.py b/tests/components/tedee/conftest.py index 1a8880936b1..295e34fd541 100644 --- a/tests/components/tedee/conftest.py +++ b/tests/components/tedee/conftest.py @@ -2,13 +2,13 @@ from __future__ import annotations -from collections.abc import Generator import json from unittest.mock import AsyncMock, MagicMock, patch from pytedee_async.bridge import TedeeBridge from pytedee_async.lock import TedeeLock import pytest +from typing_extensions import Generator from homeassistant.components.tedee.const import CONF_LOCAL_ACCESS_TOKEN, DOMAIN from homeassistant.const import CONF_HOST, CONF_WEBHOOK_ID @@ -37,7 +37,7 @@ def mock_config_entry() -> MockConfigEntry: @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Mock setting up a config entry.""" with patch( "homeassistant.components.tedee.async_setup_entry", return_value=True @@ -46,7 +46,7 @@ def mock_setup_entry() -> Generator[AsyncMock, None, None]: @pytest.fixture -def mock_tedee() -> Generator[MagicMock, None, None]: +def mock_tedee() -> Generator[MagicMock]: """Return a mocked Tedee client.""" with ( patch( diff --git a/tests/components/time_date/conftest.py b/tests/components/time_date/conftest.py index 72363dcdf9e..4bcaa887b6f 100644 --- a/tests/components/time_date/conftest.py +++ b/tests/components/time_date/conftest.py @@ -1,13 +1,13 @@ """Fixtures for Time & Date integration tests.""" -from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest +from typing_extensions import Generator @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Mock setting up a config entry.""" with patch( "homeassistant.components.time_date.async_setup_entry", return_value=True diff --git a/tests/components/todo/test_init.py b/tests/components/todo/test_init.py index 44ebc785913..951a0035017 100644 --- a/tests/components/todo/test_init.py +++ b/tests/components/todo/test_init.py @@ -1,12 +1,12 @@ """Tests for the todo integration.""" -from collections.abc import Generator import datetime from typing import Any from unittest.mock import AsyncMock import zoneinfo import pytest +from typing_extensions import Generator import voluptuous as vol from homeassistant.components.todo import ( @@ -75,7 +75,7 @@ class MockTodoListEntity(TodoListEntity): @pytest.fixture(autouse=True) -def config_flow_fixture(hass: HomeAssistant) -> Generator[None, None, None]: +def config_flow_fixture(hass: HomeAssistant) -> Generator[None]: """Mock config flow.""" mock_platform(hass, f"{TEST_DOMAIN}.config_flow") diff --git a/tests/components/todoist/conftest.py b/tests/components/todoist/conftest.py index 4968b6beefb..386385a0ddb 100644 --- a/tests/components/todoist/conftest.py +++ b/tests/components/todoist/conftest.py @@ -1,6 +1,5 @@ """Common fixtures for the todoist tests.""" -from collections.abc import Generator from http import HTTPStatus from unittest.mock import AsyncMock, patch @@ -8,6 +7,7 @@ import pytest from requests.exceptions import HTTPError from requests.models import Response from todoist_api_python.models import Collaborator, Due, Label, Project, Task +from typing_extensions import Generator from homeassistant.components.todoist import DOMAIN from homeassistant.const import CONF_TOKEN, Platform @@ -24,7 +24,7 @@ TODAY = dt_util.now().strftime("%Y-%m-%d") @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Override async_setup_entry.""" with patch( "homeassistant.components.todoist.async_setup_entry", return_value=True diff --git a/tests/components/tplink/conftest.py b/tests/components/tplink/conftest.py index 4576f97ed83..88da9b699a7 100644 --- a/tests/components/tplink/conftest.py +++ b/tests/components/tplink/conftest.py @@ -1,10 +1,10 @@ """tplink conftest.""" -from collections.abc import Generator import copy from unittest.mock import DEFAULT, AsyncMock, patch import pytest +from typing_extensions import Generator from homeassistant.components.tplink import DOMAIN from homeassistant.core import HomeAssistant @@ -85,7 +85,7 @@ def entity_reg_fixture(hass): @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Override async_setup_entry.""" with patch.multiple( async_setup=DEFAULT, @@ -97,7 +97,7 @@ def mock_setup_entry() -> Generator[AsyncMock, None, None]: @pytest.fixture -def mock_init() -> Generator[AsyncMock, None, None]: +def mock_init() -> Generator[AsyncMock]: """Override async_setup_entry.""" with patch.multiple( "homeassistant.components.tplink", diff --git a/tests/components/tplink_omada/conftest.py b/tests/components/tplink_omada/conftest.py index afedaa2df3c..56af55ffd07 100644 --- a/tests/components/tplink_omada/conftest.py +++ b/tests/components/tplink_omada/conftest.py @@ -1,6 +1,5 @@ """Test fixtures for TP-Link Omada integration.""" -from collections.abc import Generator import json from unittest.mock import AsyncMock, MagicMock, patch @@ -11,6 +10,7 @@ from tplink_omada_client.devices import ( OmadaSwitch, OmadaSwitchPortDetails, ) +from typing_extensions import Generator from homeassistant.components.tplink_omada.config_flow import CONF_SITE from homeassistant.components.tplink_omada.const import DOMAIN @@ -38,7 +38,7 @@ def mock_config_entry() -> MockConfigEntry: @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Mock setting up a config entry.""" with patch( "homeassistant.components.tplink_omada.async_setup_entry", return_value=True @@ -47,7 +47,7 @@ def mock_setup_entry() -> Generator[AsyncMock, None, None]: @pytest.fixture -def mock_omada_site_client() -> Generator[AsyncMock, None, None]: +def mock_omada_site_client() -> Generator[AsyncMock]: """Mock Omada site client.""" site_client = AsyncMock() @@ -73,9 +73,7 @@ def mock_omada_site_client() -> Generator[AsyncMock, None, None]: @pytest.fixture -def mock_omada_client( - mock_omada_site_client: AsyncMock, -) -> Generator[MagicMock, None, None]: +def mock_omada_client(mock_omada_site_client: AsyncMock) -> Generator[MagicMock]: """Mock Omada client.""" with patch( "homeassistant.components.tplink_omada.create_omada_client", diff --git a/tests/components/traccar_server/conftest.py b/tests/components/traccar_server/conftest.py index e5a65bfeabd..6a8e428e7a2 100644 --- a/tests/components/traccar_server/conftest.py +++ b/tests/components/traccar_server/conftest.py @@ -1,10 +1,10 @@ """Common fixtures for the Traccar Server tests.""" -from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest from pytraccar import ApiClient, SubscriptionStatus +from typing_extensions import Generator from homeassistant.components.traccar_server.const import ( CONF_CUSTOM_ATTRIBUTES, @@ -30,7 +30,7 @@ from tests.common import ( @pytest.fixture -def mock_traccar_api_client() -> Generator[AsyncMock, None, None]: +def mock_traccar_api_client() -> Generator[AsyncMock]: """Mock a Traccar ApiClient client.""" with ( patch( diff --git a/tests/components/traccar_server/test_config_flow.py b/tests/components/traccar_server/test_config_flow.py index fdc22f9ff97..5da6f592957 100644 --- a/tests/components/traccar_server/test_config_flow.py +++ b/tests/components/traccar_server/test_config_flow.py @@ -1,11 +1,11 @@ """Test the Traccar Server config flow.""" -from collections.abc import Generator from typing import Any from unittest.mock import AsyncMock import pytest from pytraccar import TraccarException +from typing_extensions import Generator from homeassistant import config_entries from homeassistant.components.traccar.device_tracker import PLATFORM_SCHEMA @@ -34,7 +34,7 @@ from tests.common import MockConfigEntry async def test_form( hass: HomeAssistant, - mock_traccar_api_client: Generator[AsyncMock, None, None], + mock_traccar_api_client: Generator[AsyncMock], ) -> None: """Test we get the form.""" result = await hass.config_entries.flow.async_init( @@ -77,7 +77,7 @@ async def test_form_cannot_connect( hass: HomeAssistant, side_effect: Exception, error: str, - mock_traccar_api_client: Generator[AsyncMock, None, None], + mock_traccar_api_client: Generator[AsyncMock], ) -> None: """Test we handle cannot connect error.""" result = await hass.config_entries.flow.async_init( @@ -127,7 +127,7 @@ async def test_form_cannot_connect( async def test_options( hass: HomeAssistant, mock_config_entry: MockConfigEntry, - mock_traccar_api_client: Generator[AsyncMock, None, None], + mock_traccar_api_client: Generator[AsyncMock], ) -> None: """Test options flow.""" mock_config_entry.add_to_hass(hass) @@ -231,7 +231,7 @@ async def test_import_from_yaml( imported: dict[str, Any], data: dict[str, Any], options: dict[str, Any], - mock_traccar_api_client: Generator[AsyncMock, None, None], + mock_traccar_api_client: Generator[AsyncMock], ) -> None: """Test importing configuration from YAML.""" result = await hass.config_entries.flow.async_init( @@ -277,7 +277,7 @@ async def test_abort_import_already_configured(hass: HomeAssistant) -> None: async def test_abort_already_configured( hass: HomeAssistant, mock_config_entry: MockConfigEntry, - mock_traccar_api_client: Generator[AsyncMock, None, None], + mock_traccar_api_client: Generator[AsyncMock], ) -> None: """Test abort for existing server.""" mock_config_entry.add_to_hass(hass) diff --git a/tests/components/traccar_server/test_diagnostics.py b/tests/components/traccar_server/test_diagnostics.py index 9019cd0ebf1..15d74ef9ef5 100644 --- a/tests/components/traccar_server/test_diagnostics.py +++ b/tests/components/traccar_server/test_diagnostics.py @@ -1,9 +1,9 @@ """Test Traccar Server diagnostics.""" -from collections.abc import Generator from unittest.mock import AsyncMock from syrupy import SnapshotAssertion +from typing_extensions import Generator from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er @@ -21,7 +21,7 @@ from tests.typing import ClientSessionGenerator async def test_entry_diagnostics( hass: HomeAssistant, hass_client: ClientSessionGenerator, - mock_traccar_api_client: Generator[AsyncMock, None, None], + mock_traccar_api_client: Generator[AsyncMock], mock_config_entry: MockConfigEntry, snapshot: SnapshotAssertion, ) -> None: @@ -44,7 +44,7 @@ async def test_entry_diagnostics( async def test_device_diagnostics( hass: HomeAssistant, hass_client: ClientSessionGenerator, - mock_traccar_api_client: Generator[AsyncMock, None, None], + mock_traccar_api_client: Generator[AsyncMock], mock_config_entry: MockConfigEntry, snapshot: SnapshotAssertion, device_registry: dr.DeviceRegistry, @@ -86,7 +86,7 @@ async def test_device_diagnostics( async def test_device_diagnostics_with_disabled_entity( hass: HomeAssistant, hass_client: ClientSessionGenerator, - mock_traccar_api_client: Generator[AsyncMock, None, None], + mock_traccar_api_client: Generator[AsyncMock], mock_config_entry: MockConfigEntry, snapshot: SnapshotAssertion, device_registry: dr.DeviceRegistry, diff --git a/tests/components/tractive/conftest.py b/tests/components/tractive/conftest.py index 5492f58b2ba..9a17a557c49 100644 --- a/tests/components/tractive/conftest.py +++ b/tests/components/tractive/conftest.py @@ -1,12 +1,12 @@ """Common fixtures for the Tractive tests.""" -from collections.abc import Generator from typing import Any from unittest.mock import AsyncMock, Mock, patch from aiotractive.trackable_object import TrackableObject from aiotractive.tracker import Tracker import pytest +from typing_extensions import Generator from homeassistant.components.tractive.const import DOMAIN, SERVER_UNAVAILABLE from homeassistant.const import CONF_EMAIL, CONF_PASSWORD @@ -16,7 +16,7 @@ from tests.common import MockConfigEntry, load_json_object_fixture @pytest.fixture -def mock_tractive_client() -> Generator[AsyncMock, None, None]: +def mock_tractive_client() -> Generator[AsyncMock]: """Mock a Tractive client.""" def send_hardware_event( diff --git a/tests/components/tradfri/conftest.py b/tests/components/tradfri/conftest.py index 73cfea59ce1..08afe77b4a3 100644 --- a/tests/components/tradfri/conftest.py +++ b/tests/components/tradfri/conftest.py @@ -2,7 +2,7 @@ from __future__ import annotations -from collections.abc import Callable, Generator +from collections.abc import Callable import json from typing import Any from unittest.mock import AsyncMock, MagicMock, patch @@ -12,6 +12,7 @@ from pytradfri.command import Command from pytradfri.const import ATTR_FIRMWARE_VERSION, ATTR_GATEWAY_ID from pytradfri.device import Device from pytradfri.gateway import Gateway +from typing_extensions import Generator from homeassistant.components.tradfri.const import DOMAIN @@ -22,7 +23,7 @@ from tests.common import load_fixture @pytest.fixture -def mock_entry_setup() -> Generator[AsyncMock, None, None]: +def mock_entry_setup() -> Generator[AsyncMock]: """Mock entry setup.""" with patch(f"{TRADFRI_PATH}.async_setup_entry") as mock_setup: mock_setup.return_value = True @@ -76,7 +77,7 @@ def mock_api_fixture( @pytest.fixture(autouse=True) def mock_api_factory( mock_api: Callable[[Command | list[Command], float | None], Any | None], -) -> Generator[MagicMock, None, None]: +) -> Generator[MagicMock]: """Mock pytradfri api factory.""" with patch(f"{TRADFRI_PATH}.APIFactory", autospec=True) as factory_class: factory = factory_class.return_value diff --git a/tests/components/tts/common.py b/tests/components/tts/common.py index 06712deea99..e1d9d973f25 100644 --- a/tests/components/tts/common.py +++ b/tests/components/tts/common.py @@ -2,13 +2,13 @@ from __future__ import annotations -from collections.abc import Generator from http import HTTPStatus from pathlib import Path from typing import Any from unittest.mock import MagicMock, patch import pytest +from typing_extensions import Generator import voluptuous as vol from homeassistant.components import media_source @@ -42,7 +42,7 @@ SUPPORT_LANGUAGES = ["de_CH", "de_DE", "en_GB", "en_US"] TEST_DOMAIN = "test" -def mock_tts_get_cache_files_fixture_helper() -> Generator[MagicMock, None, None]: +def mock_tts_get_cache_files_fixture_helper() -> Generator[MagicMock]: """Mock the list TTS cache function.""" with patch( "homeassistant.components.tts._get_cache_files", return_value={} @@ -52,7 +52,7 @@ def mock_tts_get_cache_files_fixture_helper() -> Generator[MagicMock, None, None def mock_tts_init_cache_dir_fixture_helper( init_tts_cache_dir_side_effect: Any, -) -> Generator[MagicMock, None, None]: +) -> Generator[MagicMock]: """Mock the TTS cache dir in memory.""" with patch( "homeassistant.components.tts._init_tts_cache_dir", @@ -71,7 +71,7 @@ def mock_tts_cache_dir_fixture_helper( mock_tts_init_cache_dir: MagicMock, mock_tts_get_cache_files: MagicMock, request: pytest.FixtureRequest, -) -> Generator[Path, None, None]: +) -> Generator[Path]: """Mock the TTS cache dir with empty dir.""" mock_tts_init_cache_dir.return_value = str(tmp_path) @@ -92,7 +92,7 @@ def mock_tts_cache_dir_fixture_helper( pytest.fail("Test failed, see log for details") -def tts_mutagen_mock_fixture_helper() -> Generator[MagicMock, None, None]: +def tts_mutagen_mock_fixture_helper() -> Generator[MagicMock]: """Mock writing tags.""" with patch( "homeassistant.components.tts.SpeechManager.write_tags", diff --git a/tests/components/tts/conftest.py b/tests/components/tts/conftest.py index 7ada92f6088..b8abb086260 100644 --- a/tests/components/tts/conftest.py +++ b/tests/components/tts/conftest.py @@ -3,11 +3,11 @@ From http://doc.pytest.org/en/latest/example/simple.html#making-test-result-information-available-in-fixtures """ -from collections.abc import Generator from pathlib import Path from unittest.mock import MagicMock import pytest +from typing_extensions import Generator from homeassistant.config import async_process_ha_core_config from homeassistant.config_entries import ConfigFlow @@ -82,7 +82,7 @@ class TTSFlow(ConfigFlow): @pytest.fixture(autouse=True) -def config_flow_fixture(hass: HomeAssistant) -> Generator[None, None, None]: +def config_flow_fixture(hass: HomeAssistant) -> Generator[None]: """Mock config flow.""" mock_platform(hass, f"{TEST_DOMAIN}.config_flow") diff --git a/tests/components/tuya/conftest.py b/tests/components/tuya/conftest.py index 541e2f1c9e3..981e12ecceb 100644 --- a/tests/components/tuya/conftest.py +++ b/tests/components/tuya/conftest.py @@ -2,10 +2,10 @@ from __future__ import annotations -from collections.abc import Generator from unittest.mock import AsyncMock, MagicMock, patch import pytest +from typing_extensions import Generator from homeassistant.components.tuya.const import CONF_APP_TYPE, CONF_USER_CODE, DOMAIN @@ -35,14 +35,14 @@ def mock_config_entry() -> MockConfigEntry: @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Mock setting up a config entry.""" with patch("homeassistant.components.tuya.async_setup_entry", return_value=True): yield @pytest.fixture -def mock_tuya_login_control() -> Generator[MagicMock, None, None]: +def mock_tuya_login_control() -> Generator[MagicMock]: """Return a mocked Tuya login control.""" with patch( "homeassistant.components.tuya.config_flow.LoginControl", autospec=True diff --git a/tests/components/twentemilieu/conftest.py b/tests/components/twentemilieu/conftest.py index 670bd648cac..7b157572824 100644 --- a/tests/components/twentemilieu/conftest.py +++ b/tests/components/twentemilieu/conftest.py @@ -2,12 +2,12 @@ from __future__ import annotations -from collections.abc import Generator from datetime import date from unittest.mock import MagicMock, patch import pytest from twentemilieu import WasteType +from typing_extensions import Generator from homeassistant.components.twentemilieu.const import ( CONF_HOUSE_LETTER, @@ -38,7 +38,7 @@ def mock_config_entry() -> MockConfigEntry: @pytest.fixture -def mock_setup_entry() -> Generator[None, None, None]: +def mock_setup_entry() -> Generator[None]: """Mock setting up a config entry.""" with patch( "homeassistant.components.twentemilieu.async_setup_entry", return_value=True @@ -47,7 +47,7 @@ def mock_setup_entry() -> Generator[None, None, None]: @pytest.fixture -def mock_twentemilieu() -> Generator[MagicMock, None, None]: +def mock_twentemilieu() -> Generator[MagicMock]: """Return a mocked Twente Milieu client.""" with ( patch( diff --git a/tests/components/twitch/__init__.py b/tests/components/twitch/__init__.py index 3a6643392f1..0238bbdadba 100644 --- a/tests/components/twitch/__init__.py +++ b/tests/components/twitch/__init__.py @@ -1,9 +1,10 @@ """Tests for the Twitch component.""" -from collections.abc import AsyncGenerator, AsyncIterator +from collections.abc import AsyncIterator from typing import Any, Generic, TypeVar from twitchAPI.object.base import TwitchObject +from typing_extensions import AsyncGenerator from homeassistant.components.twitch import DOMAIN from homeassistant.core import HomeAssistant @@ -40,7 +41,7 @@ class TwitchIterObject(Generic[TwitchType]): async def get_generator( fixture: str, target_type: type[TwitchType] -) -> AsyncGenerator[TwitchType, None]: +) -> AsyncGenerator[TwitchType]: """Return async generator.""" data = load_json_array_fixture(fixture, DOMAIN) async for item in get_generator_from_data(data, target_type): @@ -49,7 +50,7 @@ async def get_generator( async def get_generator_from_data( items: list[dict[str, Any]], target_type: type[TwitchType] -) -> AsyncGenerator[TwitchType, None]: +) -> AsyncGenerator[TwitchType]: """Return async generator.""" for item in items: yield target_type(**item) diff --git a/tests/components/twitch/conftest.py b/tests/components/twitch/conftest.py index 054b4b38a7c..6c243a8dbbf 100644 --- a/tests/components/twitch/conftest.py +++ b/tests/components/twitch/conftest.py @@ -1,11 +1,11 @@ """Configure tests for the Twitch integration.""" -from collections.abc import Generator import time from unittest.mock import AsyncMock, patch import pytest from twitchAPI.object.api import FollowedChannel, Stream, TwitchUser, UserSubscription +from typing_extensions import Generator from homeassistant.components.application_credentials import ( ClientCredential, @@ -26,7 +26,7 @@ TITLE = "Test" @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Override async_setup_entry.""" with patch( "homeassistant.components.twitch.async_setup_entry", return_value=True @@ -93,7 +93,7 @@ def mock_connection(aioclient_mock: AiohttpClientMocker) -> None: @pytest.fixture -def twitch_mock() -> Generator[AsyncMock, None, None]: +def twitch_mock() -> Generator[AsyncMock]: """Return as fixture to inject other mocks.""" with ( patch( diff --git a/tests/components/update/test_init.py b/tests/components/update/test_init.py index 04e2e5c7076..c03559d76d0 100644 --- a/tests/components/update/test_init.py +++ b/tests/components/update/test_init.py @@ -1,9 +1,9 @@ """The tests for the Update component.""" -from collections.abc import Generator from unittest.mock import MagicMock, patch import pytest +from typing_extensions import Generator from homeassistant.components.update import ( ATTR_BACKUP, @@ -767,7 +767,7 @@ class MockFlow(ConfigFlow): @pytest.fixture(autouse=True) -def config_flow_fixture(hass: HomeAssistant) -> Generator[None, None, None]: +def config_flow_fixture(hass: HomeAssistant) -> Generator[None]: """Mock config flow.""" mock_platform(hass, f"{TEST_DOMAIN}.config_flow") diff --git a/tests/components/uptime/conftest.py b/tests/components/uptime/conftest.py index a681fb40173..2fe96b91b63 100644 --- a/tests/components/uptime/conftest.py +++ b/tests/components/uptime/conftest.py @@ -2,10 +2,10 @@ from __future__ import annotations -from collections.abc import Generator from unittest.mock import patch import pytest +from typing_extensions import Generator from homeassistant.components.uptime.const import DOMAIN from homeassistant.core import HomeAssistant @@ -23,7 +23,7 @@ def mock_config_entry() -> MockConfigEntry: @pytest.fixture -def mock_setup_entry() -> Generator[None, None, None]: +def mock_setup_entry() -> Generator[None]: """Mock setting up a config entry.""" with patch("homeassistant.components.uptime.async_setup_entry", return_value=True): yield diff --git a/tests/components/v2c/conftest.py b/tests/components/v2c/conftest.py index 5dc8d96aab4..9cc3e4ed9e2 100644 --- a/tests/components/v2c/conftest.py +++ b/tests/components/v2c/conftest.py @@ -1,10 +1,10 @@ """Common fixtures for the V2C tests.""" -from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest from pytrydan.models.trydan import TrydanData +from typing_extensions import Generator from homeassistant.components.v2c import DOMAIN from homeassistant.const import CONF_HOST @@ -14,7 +14,7 @@ from tests.common import MockConfigEntry, load_json_object_fixture @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Override async_setup_entry.""" with patch( "homeassistant.components.v2c.async_setup_entry", return_value=True @@ -34,7 +34,7 @@ def mock_config_entry() -> MockConfigEntry: @pytest.fixture -def mock_v2c_client() -> Generator[AsyncMock, None, None]: +def mock_v2c_client() -> Generator[AsyncMock]: """Mock a V2C client.""" with ( patch( diff --git a/tests/components/vacuum/conftest.py b/tests/components/vacuum/conftest.py index e99879d2c35..5167c868f9f 100644 --- a/tests/components/vacuum/conftest.py +++ b/tests/components/vacuum/conftest.py @@ -1,8 +1,7 @@ """Fixtures for Vacuum platform tests.""" -from collections.abc import Generator - import pytest +from typing_extensions import Generator from homeassistant.config_entries import ConfigFlow from homeassistant.core import HomeAssistant @@ -15,7 +14,7 @@ class MockFlow(ConfigFlow): @pytest.fixture -def config_flow_fixture(hass: HomeAssistant) -> Generator[None, None, None]: +def config_flow_fixture(hass: HomeAssistant) -> Generator[None]: """Mock config flow.""" mock_platform(hass, "test.config_flow") diff --git a/tests/components/valve/test_init.py b/tests/components/valve/test_init.py index 1f9f141d89f..704f690f2f8 100644 --- a/tests/components/valve/test_init.py +++ b/tests/components/valve/test_init.py @@ -1,9 +1,8 @@ """The tests for Valve.""" -from collections.abc import Generator - import pytest from syrupy.assertion import SnapshotAssertion +from typing_extensions import Generator from homeassistant.components.valve import ( DOMAIN, @@ -123,7 +122,7 @@ class MockBinaryValveEntity(ValveEntity): @pytest.fixture(autouse=True) -def config_flow_fixture(hass: HomeAssistant) -> Generator[None, None, None]: +def config_flow_fixture(hass: HomeAssistant) -> Generator[None]: """Mock config flow.""" mock_platform(hass, f"{TEST_DOMAIN}.config_flow") diff --git a/tests/components/velbus/conftest.py b/tests/components/velbus/conftest.py index f393ebb819d..3d59ad615c6 100644 --- a/tests/components/velbus/conftest.py +++ b/tests/components/velbus/conftest.py @@ -1,9 +1,9 @@ """Fixtures for the Velbus tests.""" -from collections.abc import Generator from unittest.mock import MagicMock, patch import pytest +from typing_extensions import Generator from homeassistant.components.velbus.const import DOMAIN from homeassistant.config_entries import ConfigEntry @@ -16,7 +16,7 @@ from tests.common import MockConfigEntry @pytest.fixture(name="controller") -def mock_controller() -> Generator[MagicMock, None, None]: +def mock_controller() -> Generator[MagicMock]: """Mock a successful velbus controller.""" with patch("homeassistant.components.velbus.Velbus", autospec=True) as controller: yield controller diff --git a/tests/components/velbus/test_config_flow.py b/tests/components/velbus/test_config_flow.py index 79d67415c4f..59effcae706 100644 --- a/tests/components/velbus/test_config_flow.py +++ b/tests/components/velbus/test_config_flow.py @@ -1,10 +1,10 @@ """Tests for the Velbus config flow.""" -from collections.abc import Generator from unittest.mock import AsyncMock, MagicMock, patch import pytest import serial.tools.list_ports +from typing_extensions import Generator from velbusaio.exceptions import VelbusConnectionFailed from homeassistant.components import usb @@ -39,7 +39,7 @@ def com_port(): @pytest.fixture(name="controller") -def mock_controller() -> Generator[MagicMock, None, None]: +def mock_controller() -> Generator[MagicMock]: """Mock a successful velbus controller.""" with patch( "homeassistant.components.velbus.config_flow.velbusaio.controller.Velbus", @@ -49,7 +49,7 @@ def mock_controller() -> Generator[MagicMock, None, None]: @pytest.fixture(autouse=True) -def override_async_setup_entry() -> Generator[AsyncMock, None, None]: +def override_async_setup_entry() -> Generator[AsyncMock]: """Override async_setup_entry.""" with patch( "homeassistant.components.velbus.async_setup_entry", return_value=True diff --git a/tests/components/velux/conftest.py b/tests/components/velux/conftest.py index a3ebaf51d7a..692216827b2 100644 --- a/tests/components/velux/conftest.py +++ b/tests/components/velux/conftest.py @@ -1,13 +1,13 @@ """Configuration for Velux tests.""" -from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest +from typing_extensions import Generator @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Override async_setup_entry.""" with patch( "homeassistant.components.velux.async_setup_entry", return_value=True diff --git a/tests/components/verisure/conftest.py b/tests/components/verisure/conftest.py index 445b7b95300..401f0e05d7c 100644 --- a/tests/components/verisure/conftest.py +++ b/tests/components/verisure/conftest.py @@ -2,10 +2,10 @@ from __future__ import annotations -from collections.abc import Generator from unittest.mock import AsyncMock, MagicMock, patch import pytest +from typing_extensions import Generator from homeassistant.components.verisure.const import CONF_GIID, DOMAIN from homeassistant.const import CONF_EMAIL, CONF_PASSWORD @@ -29,7 +29,7 @@ def mock_config_entry() -> MockConfigEntry: @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Mock setting up a config entry.""" with patch( "homeassistant.components.verisure.async_setup_entry", return_value=True diff --git a/tests/components/vicare/conftest.py b/tests/components/vicare/conftest.py index fac85b5052a..6899839a0e1 100644 --- a/tests/components/vicare/conftest.py +++ b/tests/components/vicare/conftest.py @@ -2,13 +2,13 @@ from __future__ import annotations -from collections.abc import AsyncGenerator, Generator from dataclasses import dataclass from unittest.mock import AsyncMock, Mock, patch import pytest from PyViCare.PyViCareDeviceConfig import PyViCareDeviceConfig from PyViCare.PyViCareService import ViCareDeviceAccessor, readFeature +from typing_extensions import AsyncGenerator, Generator from homeassistant.components.vicare.const import DOMAIN from homeassistant.core import HomeAssistant @@ -80,7 +80,7 @@ def mock_config_entry() -> MockConfigEntry: @pytest.fixture async def mock_vicare_gas_boiler( hass: HomeAssistant, mock_config_entry: MockConfigEntry -) -> AsyncGenerator[MockConfigEntry, None]: +) -> AsyncGenerator[MockConfigEntry]: """Return a mocked ViCare API representing a single gas boiler device.""" fixtures: list[Fixture] = [Fixture({"type:boiler"}, "vicare/Vitodens300W.json")] with patch( @@ -96,7 +96,7 @@ async def mock_vicare_gas_boiler( @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Mock setting up a config entry.""" with patch(f"{MODULE}.async_setup_entry", return_value=True) as mock_setup_entry: yield mock_setup_entry diff --git a/tests/components/vilfo/conftest.py b/tests/components/vilfo/conftest.py index 75ed352c839..11b620b82e0 100644 --- a/tests/components/vilfo/conftest.py +++ b/tests/components/vilfo/conftest.py @@ -1,9 +1,9 @@ """Vilfo tests conftest.""" -from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest +from typing_extensions import Generator from homeassistant.components.vilfo import DOMAIN from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST @@ -12,7 +12,7 @@ from tests.common import MockConfigEntry @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Override async_setup_entry.""" with patch( "homeassistant.components.vilfo.async_setup_entry", @@ -22,7 +22,7 @@ def mock_setup_entry() -> Generator[AsyncMock, None, None]: @pytest.fixture -def mock_vilfo_client() -> Generator[AsyncMock, None, None]: +def mock_vilfo_client() -> Generator[AsyncMock]: """Mock a Vilfo client.""" with patch( "homeassistant.components.vilfo.config_flow.VilfoClient", @@ -38,7 +38,7 @@ def mock_vilfo_client() -> Generator[AsyncMock, None, None]: @pytest.fixture -def mock_is_valid_host() -> Generator[AsyncMock, None, None]: +def mock_is_valid_host() -> Generator[AsyncMock]: """Mock is_valid_host.""" with patch( "homeassistant.components.vilfo.config_flow.is_host_valid", diff --git a/tests/components/wake_word/test_init.py b/tests/components/wake_word/test_init.py index c4793653c9a..c19d3e7032f 100644 --- a/tests/components/wake_word/test_init.py +++ b/tests/components/wake_word/test_init.py @@ -1,13 +1,14 @@ """Test wake_word component setup.""" import asyncio -from collections.abc import AsyncIterable, Generator +from collections.abc import AsyncIterable from functools import partial from pathlib import Path from unittest.mock import patch from freezegun import freeze_time import pytest +from typing_extensions import Generator from homeassistant.components import wake_word from homeassistant.config_entries import ConfigEntry, ConfigEntryState, ConfigFlow @@ -88,7 +89,7 @@ class WakeWordFlow(ConfigFlow): @pytest.fixture(autouse=True) -def config_flow_fixture(hass: HomeAssistant) -> Generator[None, None, None]: +def config_flow_fixture(hass: HomeAssistant) -> Generator[None]: """Mock config flow.""" mock_platform(hass, f"{TEST_DOMAIN}.config_flow") diff --git a/tests/components/waqi/conftest.py b/tests/components/waqi/conftest.py index f42c8be6097..b2e1a7d77d4 100644 --- a/tests/components/waqi/conftest.py +++ b/tests/components/waqi/conftest.py @@ -1,9 +1,9 @@ """Common fixtures for the World Air Quality Index (WAQI) tests.""" -from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest +from typing_extensions import Generator from homeassistant.components.waqi.const import CONF_STATION_NUMBER, DOMAIN from homeassistant.const import CONF_API_KEY @@ -12,7 +12,7 @@ from tests.common import MockConfigEntry @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Override async_setup_entry.""" with patch( "homeassistant.components.waqi.async_setup_entry", return_value=True diff --git a/tests/components/water_heater/conftest.py b/tests/components/water_heater/conftest.py index d6858fe08e1..619d5e5c359 100644 --- a/tests/components/water_heater/conftest.py +++ b/tests/components/water_heater/conftest.py @@ -1,8 +1,7 @@ """Fixtures for water heater platform tests.""" -from collections.abc import Generator - import pytest +from typing_extensions import Generator from homeassistant.config_entries import ConfigFlow from homeassistant.core import HomeAssistant @@ -15,7 +14,7 @@ class MockFlow(ConfigFlow): @pytest.fixture -def config_flow_fixture(hass: HomeAssistant) -> Generator[None, None, None]: +def config_flow_fixture(hass: HomeAssistant) -> Generator[None]: """Mock config flow.""" mock_platform(hass, "test.config_flow") diff --git a/tests/components/weather/conftest.py b/tests/components/weather/conftest.py index 073af7ab8ef..e3e790300a0 100644 --- a/tests/components/weather/conftest.py +++ b/tests/components/weather/conftest.py @@ -1,8 +1,7 @@ """Fixtures for Weather platform tests.""" -from collections.abc import Generator - import pytest +from typing_extensions import Generator from homeassistant.config_entries import ConfigFlow from homeassistant.core import HomeAssistant @@ -15,7 +14,7 @@ class MockFlow(ConfigFlow): @pytest.fixture -def config_flow_fixture(hass: HomeAssistant) -> Generator[None, None, None]: +def config_flow_fixture(hass: HomeAssistant) -> Generator[None]: """Mock config flow.""" mock_platform(hass, "test.config_flow") diff --git a/tests/components/weatherflow/conftest.py b/tests/components/weatherflow/conftest.py index dc533f153e2..c0811597228 100644 --- a/tests/components/weatherflow/conftest.py +++ b/tests/components/weatherflow/conftest.py @@ -1,12 +1,12 @@ """Fixtures for Weatherflow integration tests.""" import asyncio -from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest from pyweatherflowudp.client import EVENT_DEVICE_DISCOVERED from pyweatherflowudp.device import WeatherFlowDevice +from typing_extensions import Generator from homeassistant.components.weatherflow.const import DOMAIN @@ -14,7 +14,7 @@ from tests.common import MockConfigEntry, load_json_object_fixture @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Mock setting up a config entry.""" with patch( "homeassistant.components.weatherflow.async_setup_entry", return_value=True @@ -29,7 +29,7 @@ def mock_config_entry() -> MockConfigEntry: @pytest.fixture -def mock_has_devices() -> Generator[AsyncMock, None, None]: +def mock_has_devices() -> Generator[AsyncMock]: """Return a mock has_devices function.""" with patch( "homeassistant.components.weatherflow.config_flow.WeatherFlowListener.on", @@ -39,7 +39,7 @@ def mock_has_devices() -> Generator[AsyncMock, None, None]: @pytest.fixture -def mock_stop() -> Generator[AsyncMock, None, None]: +def mock_stop() -> Generator[AsyncMock]: """Return a fixture to handle the stop of udp.""" async def mock_stop_listening(self): @@ -54,7 +54,7 @@ def mock_stop() -> Generator[AsyncMock, None, None]: @pytest.fixture -def mock_start() -> Generator[AsyncMock, None, None]: +def mock_start() -> Generator[AsyncMock]: """Return fixture for starting upd.""" device = WeatherFlowDevice( diff --git a/tests/components/weatherflow_cloud/conftest.py b/tests/components/weatherflow_cloud/conftest.py index e07abe2b924..d47da3c7d1b 100644 --- a/tests/components/weatherflow_cloud/conftest.py +++ b/tests/components/weatherflow_cloud/conftest.py @@ -1,14 +1,14 @@ """Common fixtures for the WeatherflowCloud tests.""" -from collections.abc import Generator from unittest.mock import AsyncMock, Mock, patch from aiohttp import ClientResponseError import pytest +from typing_extensions import Generator @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Override async_setup_entry.""" with patch( "homeassistant.components.weatherflow_cloud.async_setup_entry", @@ -18,7 +18,7 @@ def mock_setup_entry() -> Generator[AsyncMock, None, None]: @pytest.fixture -def mock_get_stations() -> Generator[AsyncMock, None, None]: +def mock_get_stations() -> Generator[AsyncMock]: """Mock get_stations with a sequence of responses.""" side_effects = [ True, @@ -32,7 +32,7 @@ def mock_get_stations() -> Generator[AsyncMock, None, None]: @pytest.fixture -def mock_get_stations_500_error() -> Generator[AsyncMock, None, None]: +def mock_get_stations_500_error() -> Generator[AsyncMock]: """Mock get_stations with a sequence of responses.""" side_effects = [ ClientResponseError(Mock(), (), status=500), @@ -47,7 +47,7 @@ def mock_get_stations_500_error() -> Generator[AsyncMock, None, None]: @pytest.fixture -def mock_get_stations_401_error() -> Generator[AsyncMock, None, None]: +def mock_get_stations_401_error() -> Generator[AsyncMock]: """Mock get_stations with a sequence of responses.""" side_effects = [ClientResponseError(Mock(), (), status=401), True, True, True] diff --git a/tests/components/weatherkit/conftest.py b/tests/components/weatherkit/conftest.py index ac1dab76a86..d4b849115f6 100644 --- a/tests/components/weatherkit/conftest.py +++ b/tests/components/weatherkit/conftest.py @@ -1,13 +1,13 @@ """Common fixtures for the Apple WeatherKit tests.""" -from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest +from typing_extensions import Generator @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Override async_setup_entry.""" with patch( "homeassistant.components.weatherkit.async_setup_entry", return_value=True diff --git a/tests/components/webmin/conftest.py b/tests/components/webmin/conftest.py index 4fd674c66c8..c3ad43510d5 100644 --- a/tests/components/webmin/conftest.py +++ b/tests/components/webmin/conftest.py @@ -1,9 +1,9 @@ """Fixtures for Webmin integration tests.""" -from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest +from typing_extensions import Generator from homeassistant.components.webmin.const import DEFAULT_PORT, DOMAIN from homeassistant.const import ( @@ -29,7 +29,7 @@ TEST_USER_INPUT = { @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Mock setting up a config entry.""" with patch( "homeassistant.components.webmin.async_setup_entry", return_value=True diff --git a/tests/components/webostv/conftest.py b/tests/components/webostv/conftest.py index b610bf51ef8..2b5d701f899 100644 --- a/tests/components/webostv/conftest.py +++ b/tests/components/webostv/conftest.py @@ -1,9 +1,9 @@ """Common fixtures and objects for the LG webOS integration tests.""" -from collections.abc import Generator from unittest.mock import AsyncMock, Mock, patch import pytest +from typing_extensions import Generator from homeassistant.components.webostv.const import LIVE_TV_APP_ID from homeassistant.core import HomeAssistant, ServiceCall @@ -14,7 +14,7 @@ from tests.common import async_mock_service @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Override async_setup_entry.""" with patch( "homeassistant.components.webostv.async_setup_entry", return_value=True diff --git a/tests/components/whois/conftest.py b/tests/components/whois/conftest.py index 457c06db598..5fe420abb92 100644 --- a/tests/components/whois/conftest.py +++ b/tests/components/whois/conftest.py @@ -2,11 +2,11 @@ from __future__ import annotations -from collections.abc import Generator from datetime import datetime from unittest.mock import AsyncMock, MagicMock, Mock, patch import pytest +from typing_extensions import Generator from homeassistant.components.whois.const import DOMAIN from homeassistant.const import CONF_DOMAIN @@ -30,7 +30,7 @@ def mock_config_entry() -> MockConfigEntry: @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Mock setting up a config entry.""" with patch( "homeassistant.components.whois.async_setup_entry", return_value=True @@ -39,7 +39,7 @@ def mock_setup_entry() -> Generator[AsyncMock, None, None]: @pytest.fixture -def mock_whois() -> Generator[MagicMock, None, None]: +def mock_whois() -> Generator[MagicMock]: """Return a mocked query.""" with ( patch( @@ -68,7 +68,7 @@ def mock_whois() -> Generator[MagicMock, None, None]: @pytest.fixture -def mock_whois_missing_some_attrs() -> Generator[Mock, None, None]: +def mock_whois_missing_some_attrs() -> Generator[Mock]: """Return a mocked query that only sets admin.""" class LimitedWhoisMock: diff --git a/tests/components/wiffi/conftest.py b/tests/components/wiffi/conftest.py index 644c3c460ed..5f16d676e81 100644 --- a/tests/components/wiffi/conftest.py +++ b/tests/components/wiffi/conftest.py @@ -1,13 +1,13 @@ """Configuration for Wiffi tests.""" -from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest +from typing_extensions import Generator @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Override async_setup_entry.""" with patch( "homeassistant.components.wiffi.async_setup_entry", return_value=True diff --git a/tests/components/wled/conftest.py b/tests/components/wled/conftest.py index d2f124a517b..0d839fc8666 100644 --- a/tests/components/wled/conftest.py +++ b/tests/components/wled/conftest.py @@ -1,10 +1,10 @@ """Fixtures for WLED integration tests.""" -from collections.abc import Generator from unittest.mock import AsyncMock, MagicMock, patch from freezegun.api import FrozenDateTimeFactory import pytest +from typing_extensions import Generator from wled import Device as WLEDDevice from homeassistant.components.wled.const import DOMAIN @@ -26,7 +26,7 @@ def mock_config_entry() -> MockConfigEntry: @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Mock setting up a config entry.""" with patch( "homeassistant.components.wled.async_setup_entry", return_value=True @@ -35,7 +35,7 @@ def mock_setup_entry() -> Generator[AsyncMock, None, None]: @pytest.fixture -def mock_onboarding() -> Generator[MagicMock, None, None]: +def mock_onboarding() -> Generator[MagicMock]: """Mock that Home Assistant is currently onboarding.""" with patch( "homeassistant.components.onboarding.async_is_onboarded", @@ -51,7 +51,7 @@ def device_fixture() -> str: @pytest.fixture -def mock_wled(device_fixture: str) -> Generator[MagicMock, None, None]: +def mock_wled(device_fixture: str) -> Generator[MagicMock]: """Return a mocked WLED client.""" with ( patch( diff --git a/tests/components/workday/conftest.py b/tests/components/workday/conftest.py index 1f3d9bcaabc..33bf98f90c3 100644 --- a/tests/components/workday/conftest.py +++ b/tests/components/workday/conftest.py @@ -1,13 +1,13 @@ """Fixtures for Workday integration tests.""" -from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest +from typing_extensions import Generator @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Mock setting up a config entry.""" with patch( "homeassistant.components.workday.async_setup_entry", return_value=True diff --git a/tests/components/wyoming/conftest.py b/tests/components/wyoming/conftest.py index 4ba0c6312cb..47ef0566dc6 100644 --- a/tests/components/wyoming/conftest.py +++ b/tests/components/wyoming/conftest.py @@ -1,10 +1,10 @@ """Common fixtures for the Wyoming tests.""" -from collections.abc import Generator from pathlib import Path from unittest.mock import AsyncMock, patch import pytest +from typing_extensions import Generator from homeassistant.components import stt from homeassistant.components.wyoming import DOMAIN @@ -31,7 +31,7 @@ async def init_components(hass: HomeAssistant): @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Override async_setup_entry.""" with patch( "homeassistant.components.wyoming.async_setup_entry", return_value=True diff --git a/tests/components/xiaomi_ble/conftest.py b/tests/components/xiaomi_ble/conftest.py index bd3480bc586..bb74b3c7af3 100644 --- a/tests/components/xiaomi_ble/conftest.py +++ b/tests/components/xiaomi_ble/conftest.py @@ -1,9 +1,9 @@ """Session fixtures.""" -from collections.abc import Generator from unittest import mock import pytest +from typing_extensions import Generator class MockServices: @@ -45,7 +45,7 @@ class MockBleakClientBattery5(MockBleakClient): @pytest.fixture(autouse=True) -def mock_bluetooth(enable_bluetooth: None) -> Generator[None, None, None]: +def mock_bluetooth(enable_bluetooth: None) -> Generator[None]: """Auto mock bluetooth.""" with mock.patch("xiaomi_ble.parser.BleakClient", MockBleakClientBattery5): diff --git a/tests/components/xiaomi_miio/test_vacuum.py b/tests/components/xiaomi_miio/test_vacuum.py index 3ac17cc85b7..6a65c1b7b9a 100644 --- a/tests/components/xiaomi_miio/test_vacuum.py +++ b/tests/components/xiaomi_miio/test_vacuum.py @@ -1,12 +1,12 @@ """The tests for the Xiaomi vacuum platform.""" -from collections.abc import Generator from datetime import datetime, time, timedelta from unittest import mock from unittest.mock import MagicMock, patch from miio import DeviceException import pytest +from typing_extensions import Generator from homeassistant.components.vacuum import ( ATTR_BATTERY_ICON, @@ -143,7 +143,7 @@ new_fanspeeds = { @pytest.fixture(name="mock_mirobo_fanspeeds", params=[old_fanspeeds, new_fanspeeds]) def mirobo_old_speeds_fixture( request: pytest.FixtureRequest, -) -> Generator[MagicMock, None, None]: +) -> Generator[MagicMock]: """Fixture for testing both types of fanspeeds.""" mock_vacuum = MagicMock() mock_vacuum.status().battery = 32 diff --git a/tests/components/yardian/conftest.py b/tests/components/yardian/conftest.py index 985d2303fdf..26a01f889b7 100644 --- a/tests/components/yardian/conftest.py +++ b/tests/components/yardian/conftest.py @@ -1,13 +1,13 @@ """Common fixtures for the Yardian tests.""" -from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest +from typing_extensions import Generator @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Override async_setup_entry.""" with patch( "homeassistant.components.yardian.async_setup_entry", return_value=True diff --git a/tests/components/youtube/__init__.py b/tests/components/youtube/__init__.py index 62808bc7ad9..8f6da97481a 100644 --- a/tests/components/youtube/__init__.py +++ b/tests/components/youtube/__init__.py @@ -1,8 +1,8 @@ """Tests for the YouTube integration.""" -from collections.abc import AsyncGenerator import json +from typing_extensions import AsyncGenerator from youtubeaio.models import YouTubeChannel, YouTubePlaylistItem, YouTubeSubscription from youtubeaio.types import AuthScope @@ -30,7 +30,7 @@ class MockYouTube: ) -> None: """Authenticate the user.""" - async def get_user_channels(self) -> AsyncGenerator[YouTubeChannel, None]: + async def get_user_channels(self) -> AsyncGenerator[YouTubeChannel]: """Get channels for authenticated user.""" channels = json.loads(load_fixture(self._channel_fixture)) for item in channels["items"]: @@ -38,7 +38,7 @@ class MockYouTube: async def get_channels( self, channel_ids: list[str] - ) -> AsyncGenerator[YouTubeChannel, None]: + ) -> AsyncGenerator[YouTubeChannel]: """Get channels.""" if self._thrown_error is not None: raise self._thrown_error @@ -48,13 +48,13 @@ class MockYouTube: async def get_playlist_items( self, playlist_id: str, amount: int - ) -> AsyncGenerator[YouTubePlaylistItem, None]: + ) -> AsyncGenerator[YouTubePlaylistItem]: """Get channels.""" channels = json.loads(load_fixture(self._playlist_items_fixture)) for item in channels["items"]: yield YouTubePlaylistItem(**item) - async def get_user_subscriptions(self) -> AsyncGenerator[YouTubeSubscription, None]: + async def get_user_subscriptions(self) -> AsyncGenerator[YouTubeSubscription]: """Get channels for authenticated user.""" channels = json.loads(load_fixture(self._subscriptions_fixture)) for item in channels["items"]: diff --git a/tests/components/zamg/conftest.py b/tests/components/zamg/conftest.py index 0598e2adfb4..164c943c2ac 100644 --- a/tests/components/zamg/conftest.py +++ b/tests/components/zamg/conftest.py @@ -1,10 +1,10 @@ """Fixtures for Zamg integration tests.""" -from collections.abc import Generator import json from unittest.mock import MagicMock, patch import pytest +from typing_extensions import Generator from zamg import ZamgData as ZamgDevice from homeassistant.components.zamg.const import CONF_STATION_ID, DOMAIN @@ -30,7 +30,7 @@ def mock_config_entry() -> MockConfigEntry: @pytest.fixture -def mock_setup_entry() -> Generator[None, None, None]: +def mock_setup_entry() -> Generator[None]: """Mock setting up a config entry.""" with patch("homeassistant.components.zamg.async_setup_entry", return_value=True): yield diff --git a/tests/components/zha/conftest.py b/tests/components/zha/conftest.py index 9e3d642e0f7..97388fd17cc 100644 --- a/tests/components/zha/conftest.py +++ b/tests/components/zha/conftest.py @@ -1,6 +1,6 @@ """Test configuration for the ZHA component.""" -from collections.abc import Callable, Generator +from collections.abc import Callable import itertools import time from typing import Any @@ -8,6 +8,7 @@ from unittest.mock import AsyncMock, MagicMock, create_autospec, patch import warnings import pytest +from typing_extensions import Generator import zigpy from zigpy.application import ControllerApplication import zigpy.backups @@ -225,7 +226,7 @@ async def config_entry_fixture(hass) -> MockConfigEntry: @pytest.fixture def mock_zigpy_connect( zigpy_app_controller: ControllerApplication, -) -> Generator[ControllerApplication, None, None]: +) -> Generator[ControllerApplication]: """Patch the zigpy radio connection with our mock application.""" with ( patch( diff --git a/tests/components/zha/test_radio_manager.py b/tests/components/zha/test_radio_manager.py index 0363821ac47..280b3d05daf 100644 --- a/tests/components/zha/test_radio_manager.py +++ b/tests/components/zha/test_radio_manager.py @@ -1,10 +1,10 @@ """Tests for ZHA config flow.""" -from collections.abc import Generator from unittest.mock import AsyncMock, MagicMock, create_autospec, patch import pytest import serial.tools.list_ports +from typing_extensions import Generator from zigpy.backups import BackupManager import zigpy.config from zigpy.config import CONF_DEVICE_PATH @@ -87,7 +87,7 @@ def com_port(device="/dev/ttyUSB1234"): @pytest.fixture -def mock_connect_zigpy_app() -> Generator[MagicMock, None, None]: +def mock_connect_zigpy_app() -> Generator[MagicMock]: """Mock the radio connection.""" mock_connect_app = MagicMock() diff --git a/tests/components/zwave_js/test_config_flow.py b/tests/components/zwave_js/test_config_flow.py index 8da17e228be..3fa59b22305 100644 --- a/tests/components/zwave_js/test_config_flow.py +++ b/tests/components/zwave_js/test_config_flow.py @@ -1,7 +1,6 @@ """Test the Z-Wave JS config flow.""" import asyncio -from collections.abc import Generator from copy import copy from ipaddress import ip_address from unittest.mock import DEFAULT, MagicMock, call, patch @@ -9,6 +8,7 @@ from unittest.mock import DEFAULT, MagicMock, call, patch import aiohttp import pytest from serial.tools.list_ports_common import ListPortInfo +from typing_extensions import Generator from zwave_js_server.version import VersionInfo from homeassistant import config_entries @@ -159,7 +159,7 @@ def serial_port_fixture() -> ListPortInfo: @pytest.fixture(name="mock_list_ports", autouse=True) -def mock_list_ports_fixture(serial_port) -> Generator[MagicMock, None, None]: +def mock_list_ports_fixture(serial_port) -> Generator[MagicMock]: """Mock list ports.""" with patch( "homeassistant.components.zwave_js.config_flow.list_ports.comports" @@ -179,7 +179,7 @@ def mock_list_ports_fixture(serial_port) -> Generator[MagicMock, None, None]: @pytest.fixture(name="mock_usb_serial_by_id", autouse=True) -def mock_usb_serial_by_id_fixture() -> Generator[MagicMock, None, None]: +def mock_usb_serial_by_id_fixture() -> Generator[MagicMock]: """Mock usb serial by id.""" with patch( "homeassistant.components.zwave_js.config_flow.usb.get_serial_by_id" From 837ee7c4fb87049fc1993f4f809a26312adabbe1 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Thu, 6 Jun 2024 17:41:37 +0200 Subject: [PATCH 0304/1445] Import Generator from typing_extensions (4) (#118992) --- tests/components/obihai/conftest.py | 6 +++--- tests/components/onboarding/test_views.py | 4 ++-- tests/components/ondilo_ico/conftest.py | 4 ++-- tests/components/onewire/conftest.py | 4 ++-- tests/components/onewire/test_binary_sensor.py | 4 ++-- tests/components/onewire/test_diagnostics.py | 4 ++-- tests/components/onewire/test_sensor.py | 4 ++-- tests/components/onewire/test_switch.py | 4 ++-- tests/components/open_meteo/conftest.py | 4 ++-- tests/components/openexchangerates/conftest.py | 6 +++--- .../components/openexchangerates/test_config_flow.py | 4 ++-- tests/components/opengarage/conftest.py | 4 ++-- tests/components/openuv/conftest.py | 4 ++-- tests/components/opower/test_config_flow.py | 6 +++--- tests/components/oralb/conftest.py | 4 ++-- tests/components/ourgroceries/conftest.py | 4 ++-- tests/components/overkiz/conftest.py | 4 ++-- tests/components/permobil/conftest.py | 4 ++-- tests/components/philips_js/conftest.py | 4 ++-- tests/components/ping/test_device_tracker.py | 4 ++-- tests/components/plex/conftest.py | 4 ++-- tests/components/plugwise/conftest.py | 4 ++-- tests/components/poolsense/conftest.py | 6 +++--- .../components/prosegur/test_alarm_control_panel.py | 4 ++-- tests/components/ps4/conftest.py | 10 +++++----- tests/components/pure_energie/conftest.py | 4 ++-- tests/components/pvoutput/conftest.py | 4 ++-- tests/components/qbittorrent/conftest.py | 6 +++--- tests/components/qnap/conftest.py | 6 +++--- tests/components/rabbitair/test_config_flow.py | 4 ++-- tests/components/radio_browser/conftest.py | 4 ++-- tests/components/rainbird/conftest.py | 4 ++-- tests/components/rainforest_raven/conftest.py | 4 ++-- .../components/rainforest_raven/test_config_flow.py | 6 +++--- tests/components/rdw/conftest.py | 4 ++-- tests/components/recorder/conftest.py | 6 ++---- .../test_filters_with_entityfilter_schema_37.py | 4 ++-- tests/components/recorder/test_init.py | 4 ++-- .../recorder/test_migration_from_schema_32.py | 4 ++-- tests/components/recorder/test_purge.py | 4 ++-- tests/components/recorder/test_purge_v32_schema.py | 4 ++-- tests/components/refoss/conftest.py | 4 ++-- tests/components/renault/conftest.py | 4 ++-- tests/components/renault/test_binary_sensor.py | 4 ++-- tests/components/renault/test_button.py | 4 ++-- tests/components/renault/test_device_tracker.py | 4 ++-- tests/components/renault/test_init.py | 4 ++-- tests/components/renault/test_select.py | 4 ++-- tests/components/renault/test_sensor.py | 4 ++-- tests/components/renault/test_services.py | 4 ++-- tests/components/reolink/conftest.py | 10 +++++----- tests/components/ring/conftest.py | 4 ++-- tests/components/roku/conftest.py | 4 ++-- tests/components/rtsp_to_webrtc/conftest.py | 7 ++++--- tests/components/sabnzbd/conftest.py | 4 ++-- tests/components/samsungtv/conftest.py | 5 +++-- tests/components/sanix/conftest.py | 4 ++-- tests/components/schlage/conftest.py | 4 ++-- tests/components/scrape/conftest.py | 4 ++-- tests/components/screenlogic/test_services.py | 4 ++-- tests/components/season/conftest.py | 4 ++-- tests/components/sensor/test_init.py | 4 ++-- tests/components/seventeentrack/conftest.py | 4 ++-- tests/components/sfr_box/conftest.py | 12 ++++++------ tests/components/sfr_box/test_binary_sensor.py | 4 ++-- tests/components/sfr_box/test_button.py | 4 ++-- tests/components/sfr_box/test_diagnostics.py | 4 ++-- tests/components/sfr_box/test_init.py | 4 ++-- tests/components/sfr_box/test_sensor.py | 4 ++-- tests/components/sleepiq/conftest.py | 8 ++++---- tests/components/slimproto/conftest.py | 4 ++-- tests/components/snapcast/conftest.py | 6 +++--- tests/components/sonarr/conftest.py | 4 ++-- tests/components/stream/conftest.py | 4 ++-- tests/components/streamlabswater/conftest.py | 6 +++--- tests/components/stt/test_init.py | 5 +++-- tests/components/suez_water/conftest.py | 4 ++-- tests/components/swiss_public_transport/conftest.py | 4 ++-- tests/components/switch_as_x/conftest.py | 4 ++-- tests/components/switchbot_cloud/conftest.py | 4 ++-- tests/components/switcher_kis/conftest.py | 6 +++--- tests/components/synology_dsm/conftest.py | 4 ++-- tests/components/systemmonitor/conftest.py | 6 +++--- 83 files changed, 193 insertions(+), 192 deletions(-) diff --git a/tests/components/obihai/conftest.py b/tests/components/obihai/conftest.py index 751f41f315a..c4edfdedf65 100644 --- a/tests/components/obihai/conftest.py +++ b/tests/components/obihai/conftest.py @@ -1,14 +1,14 @@ """Define test fixtures for Obihai.""" -from collections.abc import Generator from socket import gaierror from unittest.mock import AsyncMock, patch import pytest +from typing_extensions import Generator @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Override async_setup_entry.""" with patch( @@ -18,7 +18,7 @@ def mock_setup_entry() -> Generator[AsyncMock, None, None]: @pytest.fixture -def mock_gaierror() -> Generator[AsyncMock, None, None]: +def mock_gaierror() -> Generator[AsyncMock]: """Override async_setup_entry.""" with patch( diff --git a/tests/components/onboarding/test_views.py b/tests/components/onboarding/test_views.py index a0bff5c280c..e9ba720adb3 100644 --- a/tests/components/onboarding/test_views.py +++ b/tests/components/onboarding/test_views.py @@ -1,13 +1,13 @@ """Test the onboarding views.""" import asyncio -from collections.abc import AsyncGenerator from http import HTTPStatus import os from typing import Any from unittest.mock import Mock, patch import pytest +from typing_extensions import AsyncGenerator from homeassistant.components import onboarding from homeassistant.components.onboarding import const, views @@ -70,7 +70,7 @@ async def no_rpi_fixture( @pytest.fixture(name="mock_supervisor") async def mock_supervisor_fixture( aioclient_mock: AiohttpClientMocker, -) -> AsyncGenerator[None, None]: +) -> AsyncGenerator[None]: """Mock supervisor.""" aioclient_mock.post("http://127.0.0.1/homeassistant/options", json={"result": "ok"}) aioclient_mock.post("http://127.0.0.1/supervisor/options", json={"result": "ok"}) diff --git a/tests/components/ondilo_ico/conftest.py b/tests/components/ondilo_ico/conftest.py index 06ed994b332..6a03d6961c2 100644 --- a/tests/components/ondilo_ico/conftest.py +++ b/tests/components/ondilo_ico/conftest.py @@ -1,10 +1,10 @@ """Provide basic Ondilo fixture.""" -from collections.abc import Generator from typing import Any from unittest.mock import MagicMock, patch import pytest +from typing_extensions import Generator from homeassistant.components.ondilo_ico.const import DOMAIN @@ -31,7 +31,7 @@ def mock_ondilo_client( ico_details1: dict[str, Any], ico_details2: dict[str, Any], last_measures: list[dict[str, Any]], -) -> Generator[MagicMock, None, None]: +) -> Generator[MagicMock]: """Mock a Homeassistant Ondilo client.""" with ( patch( diff --git a/tests/components/onewire/conftest.py b/tests/components/onewire/conftest.py index 03a8443049e..47b50ab10e0 100644 --- a/tests/components/onewire/conftest.py +++ b/tests/components/onewire/conftest.py @@ -1,10 +1,10 @@ """Provide common 1-Wire fixtures.""" -from collections.abc import Generator from unittest.mock import AsyncMock, MagicMock, patch from pyownet.protocol import ConnError import pytest +from typing_extensions import Generator from homeassistant.components.onewire.const import DOMAIN from homeassistant.config_entries import SOURCE_USER, ConfigEntry @@ -17,7 +17,7 @@ from tests.common import MockConfigEntry @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Override async_setup_entry.""" with patch( "homeassistant.components.onewire.async_setup_entry", return_value=True diff --git a/tests/components/onewire/test_binary_sensor.py b/tests/components/onewire/test_binary_sensor.py index 26b1ed5aed7..8b1129529d5 100644 --- a/tests/components/onewire/test_binary_sensor.py +++ b/tests/components/onewire/test_binary_sensor.py @@ -1,10 +1,10 @@ """Tests for 1-Wire binary sensors.""" -from collections.abc import Generator from unittest.mock import MagicMock, patch import pytest from syrupy.assertion import SnapshotAssertion +from typing_extensions import Generator from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform @@ -15,7 +15,7 @@ from . import setup_owproxy_mock_devices @pytest.fixture(autouse=True) -def override_platforms() -> Generator[None, None, None]: +def override_platforms() -> Generator[None]: """Override PLATFORMS.""" with patch("homeassistant.components.onewire.PLATFORMS", [Platform.BINARY_SENSOR]): yield diff --git a/tests/components/onewire/test_diagnostics.py b/tests/components/onewire/test_diagnostics.py index dd08e825221..62b045c4516 100644 --- a/tests/components/onewire/test_diagnostics.py +++ b/tests/components/onewire/test_diagnostics.py @@ -1,10 +1,10 @@ """Test 1-Wire diagnostics.""" -from collections.abc import Generator from unittest.mock import MagicMock, patch import pytest from syrupy.assertion import SnapshotAssertion +from typing_extensions import Generator from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform @@ -17,7 +17,7 @@ from tests.typing import ClientSessionGenerator @pytest.fixture(autouse=True) -def override_platforms() -> Generator[None, None, None]: +def override_platforms() -> Generator[None]: """Override PLATFORMS.""" with patch("homeassistant.components.onewire.PLATFORMS", [Platform.SWITCH]): yield diff --git a/tests/components/onewire/test_sensor.py b/tests/components/onewire/test_sensor.py index 848489c837f..df0a81920c9 100644 --- a/tests/components/onewire/test_sensor.py +++ b/tests/components/onewire/test_sensor.py @@ -1,6 +1,5 @@ """Tests for 1-Wire sensors.""" -from collections.abc import Generator from copy import deepcopy import logging from unittest.mock import MagicMock, _patch_dict, patch @@ -8,6 +7,7 @@ from unittest.mock import MagicMock, _patch_dict, patch from pyownet.protocol import OwnetError import pytest from syrupy.assertion import SnapshotAssertion +from typing_extensions import Generator from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform @@ -19,7 +19,7 @@ from .const import ATTR_INJECT_READS, MOCK_OWPROXY_DEVICES @pytest.fixture(autouse=True) -def override_platforms() -> Generator[None, None, None]: +def override_platforms() -> Generator[None]: """Override PLATFORMS.""" with patch("homeassistant.components.onewire.PLATFORMS", [Platform.SENSOR]): yield diff --git a/tests/components/onewire/test_switch.py b/tests/components/onewire/test_switch.py index c6d84d38848..b1b8e5ddbd0 100644 --- a/tests/components/onewire/test_switch.py +++ b/tests/components/onewire/test_switch.py @@ -1,10 +1,10 @@ """Tests for 1-Wire switches.""" -from collections.abc import Generator from unittest.mock import MagicMock, patch import pytest from syrupy.assertion import SnapshotAssertion +from typing_extensions import Generator from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.config_entries import ConfigEntry @@ -22,7 +22,7 @@ from . import setup_owproxy_mock_devices @pytest.fixture(autouse=True) -def override_platforms() -> Generator[None, None, None]: +def override_platforms() -> Generator[None]: """Override PLATFORMS.""" with patch("homeassistant.components.onewire.PLATFORMS", [Platform.SWITCH]): yield diff --git a/tests/components/open_meteo/conftest.py b/tests/components/open_meteo/conftest.py index 466d593cd73..b5026fad35d 100644 --- a/tests/components/open_meteo/conftest.py +++ b/tests/components/open_meteo/conftest.py @@ -2,11 +2,11 @@ from __future__ import annotations -from collections.abc import Generator from unittest.mock import MagicMock, patch from open_meteo import Forecast import pytest +from typing_extensions import Generator from homeassistant.components.open_meteo.const import DOMAIN from homeassistant.const import CONF_ZONE @@ -26,7 +26,7 @@ def mock_config_entry() -> MockConfigEntry: @pytest.fixture -def mock_setup_entry() -> Generator[None, None, None]: +def mock_setup_entry() -> Generator[None]: """Mock setting up a config entry.""" with patch( "homeassistant.components.open_meteo.async_setup_entry", return_value=True diff --git a/tests/components/openexchangerates/conftest.py b/tests/components/openexchangerates/conftest.py index 5cb97e0cc53..fa3c9cd6de0 100644 --- a/tests/components/openexchangerates/conftest.py +++ b/tests/components/openexchangerates/conftest.py @@ -1,9 +1,9 @@ """Provide common fixtures for tests.""" -from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest +from typing_extensions import Generator from homeassistant.components.openexchangerates.const import DOMAIN @@ -19,7 +19,7 @@ def mock_config_entry() -> MockConfigEntry: @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Mock setting up a config entry.""" with patch( "homeassistant.components.openexchangerates.async_setup_entry", @@ -31,7 +31,7 @@ def mock_setup_entry() -> Generator[AsyncMock, None, None]: @pytest.fixture def mock_latest_rates_config_flow( request: pytest.FixtureRequest, -) -> Generator[AsyncMock, None, None]: +) -> Generator[AsyncMock]: """Return a mocked WLED client.""" with patch( "homeassistant.components.openexchangerates.config_flow.Client.get_latest", diff --git a/tests/components/openexchangerates/test_config_flow.py b/tests/components/openexchangerates/test_config_flow.py index 2bc24e6852b..30ea619d646 100644 --- a/tests/components/openexchangerates/test_config_flow.py +++ b/tests/components/openexchangerates/test_config_flow.py @@ -1,7 +1,6 @@ """Test the Open Exchange Rates config flow.""" import asyncio -from collections.abc import Generator from typing import Any from unittest.mock import AsyncMock, patch @@ -10,6 +9,7 @@ from aioopenexchangerates import ( OpenExchangeRatesClientError, ) import pytest +from typing_extensions import Generator from homeassistant import config_entries from homeassistant.components.openexchangerates.const import DOMAIN @@ -20,7 +20,7 @@ from tests.common import MockConfigEntry @pytest.fixture(name="currencies", autouse=True) -def currencies_fixture(hass: HomeAssistant) -> Generator[AsyncMock, None, None]: +def currencies_fixture(hass: HomeAssistant) -> Generator[AsyncMock]: """Mock currencies.""" with patch( "homeassistant.components.openexchangerates.config_flow.Client.get_currencies", diff --git a/tests/components/opengarage/conftest.py b/tests/components/opengarage/conftest.py index 24dc8134e4b..c960e723289 100644 --- a/tests/components/opengarage/conftest.py +++ b/tests/components/opengarage/conftest.py @@ -2,10 +2,10 @@ from __future__ import annotations -from collections.abc import Generator from unittest.mock import MagicMock, patch import pytest +from typing_extensions import Generator from homeassistant.components.opengarage.const import CONF_DEVICE_KEY, DOMAIN from homeassistant.const import CONF_HOST, CONF_PORT, CONF_VERIFY_SSL @@ -31,7 +31,7 @@ def mock_config_entry() -> MockConfigEntry: @pytest.fixture -def mock_opengarage() -> Generator[MagicMock, None, None]: +def mock_opengarage() -> Generator[MagicMock]: """Return a mocked OpenGarage client.""" with patch( "homeassistant.components.opengarage.opengarage.OpenGarage", diff --git a/tests/components/openuv/conftest.py b/tests/components/openuv/conftest.py index 5aad7d5b1a6..69563c94c64 100644 --- a/tests/components/openuv/conftest.py +++ b/tests/components/openuv/conftest.py @@ -1,10 +1,10 @@ """Define test fixtures for OpenUV.""" -from collections.abc import Generator import json from unittest.mock import AsyncMock, Mock, patch import pytest +from typing_extensions import Generator from homeassistant.components.openuv import CONF_FROM_WINDOW, CONF_TO_WINDOW, DOMAIN from homeassistant.const import ( @@ -23,7 +23,7 @@ TEST_LONGITUDE = -0.3817765 @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Override async_setup_entry.""" with patch( "homeassistant.components.openuv.async_setup_entry", return_value=True diff --git a/tests/components/opower/test_config_flow.py b/tests/components/opower/test_config_flow.py index 18a7caf23df..a236494f2c9 100644 --- a/tests/components/opower/test_config_flow.py +++ b/tests/components/opower/test_config_flow.py @@ -1,10 +1,10 @@ """Test the Opower config flow.""" -from collections.abc import Generator from unittest.mock import AsyncMock, patch from opower import CannotConnect, InvalidAuth import pytest +from typing_extensions import Generator from homeassistant import config_entries from homeassistant.components.opower.const import DOMAIN @@ -17,7 +17,7 @@ from tests.common import MockConfigEntry @pytest.fixture(autouse=True, name="mock_setup_entry") -def override_async_setup_entry() -> Generator[AsyncMock, None, None]: +def override_async_setup_entry() -> Generator[AsyncMock]: """Override async_setup_entry.""" with patch( "homeassistant.components.opower.async_setup_entry", return_value=True @@ -26,7 +26,7 @@ def override_async_setup_entry() -> Generator[AsyncMock, None, None]: @pytest.fixture -def mock_unload_entry() -> Generator[AsyncMock, None, None]: +def mock_unload_entry() -> Generator[AsyncMock]: """Mock unloading a config entry.""" with patch( "homeassistant.components.opower.async_unload_entry", diff --git a/tests/components/oralb/conftest.py b/tests/components/oralb/conftest.py index f119d6b22b3..fa4ba463357 100644 --- a/tests/components/oralb/conftest.py +++ b/tests/components/oralb/conftest.py @@ -1,9 +1,9 @@ """OralB session fixtures.""" -from collections.abc import Generator from unittest import mock import pytest +from typing_extensions import Generator class MockServices: @@ -45,7 +45,7 @@ class MockBleakClientBattery49(MockBleakClient): @pytest.fixture(autouse=True) -def mock_bluetooth(enable_bluetooth: None) -> Generator[None, None, None]: +def mock_bluetooth(enable_bluetooth: None) -> Generator[None]: """Auto mock bluetooth.""" with mock.patch( diff --git a/tests/components/ourgroceries/conftest.py b/tests/components/ourgroceries/conftest.py index 00aab0df834..bc8c632b511 100644 --- a/tests/components/ourgroceries/conftest.py +++ b/tests/components/ourgroceries/conftest.py @@ -1,9 +1,9 @@ """Common fixtures for the OurGroceries tests.""" -from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest +from typing_extensions import Generator from homeassistant.components.ourgroceries import DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME @@ -19,7 +19,7 @@ PASSWORD = "test-password" @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Override async_setup_entry.""" with patch( "homeassistant.components.ourgroceries.async_setup_entry", return_value=True diff --git a/tests/components/overkiz/conftest.py b/tests/components/overkiz/conftest.py index d1da5d89134..ea021ccef1e 100644 --- a/tests/components/overkiz/conftest.py +++ b/tests/components/overkiz/conftest.py @@ -1,9 +1,9 @@ """Configuration for overkiz tests.""" -from collections.abc import Generator from unittest.mock import AsyncMock, Mock, patch import pytest +from typing_extensions import Generator from homeassistant.components.overkiz.const import DOMAIN from homeassistant.core import HomeAssistant @@ -32,7 +32,7 @@ def mock_config_entry() -> MockConfigEntry: @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Mock setting up a config entry.""" with patch( "homeassistant.components.overkiz.async_setup_entry", return_value=True diff --git a/tests/components/permobil/conftest.py b/tests/components/permobil/conftest.py index 74d17616af7..ed6a843b206 100644 --- a/tests/components/permobil/conftest.py +++ b/tests/components/permobil/conftest.py @@ -1,16 +1,16 @@ """Common fixtures for the MyPermobil tests.""" -from collections.abc import Generator from unittest.mock import AsyncMock, Mock, patch from mypermobil import MyPermobil import pytest +from typing_extensions import Generator from .const import MOCK_REGION_NAME, MOCK_TOKEN, MOCK_URL @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Override async_setup_entry.""" with patch( "homeassistant.components.permobil.async_setup_entry", return_value=True diff --git a/tests/components/philips_js/conftest.py b/tests/components/philips_js/conftest.py index 3591546dfe9..b6c78fe9e5e 100644 --- a/tests/components/philips_js/conftest.py +++ b/tests/components/philips_js/conftest.py @@ -1,10 +1,10 @@ """Standard setup for tests.""" -from collections.abc import Generator from unittest.mock import AsyncMock, create_autospec, patch from haphilipsjs import PhilipsTV import pytest +from typing_extensions import Generator from homeassistant.components.philips_js.const import DOMAIN @@ -14,7 +14,7 @@ from tests.common import MockConfigEntry, mock_device_registry @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Disable component setup.""" with ( patch( diff --git a/tests/components/ping/test_device_tracker.py b/tests/components/ping/test_device_tracker.py index b1e08c3607b..f65f619b3c6 100644 --- a/tests/components/ping/test_device_tracker.py +++ b/tests/components/ping/test_device_tracker.py @@ -1,12 +1,12 @@ """Test the binary sensor platform of ping.""" -from collections.abc import Generator from datetime import timedelta from unittest.mock import patch from freezegun.api import FrozenDateTimeFactory from icmplib import Host import pytest +from typing_extensions import Generator from homeassistant.components.device_tracker import legacy from homeassistant.components.ping.const import DOMAIN @@ -20,7 +20,7 @@ from tests.common import MockConfigEntry, async_fire_time_changed, patch_yaml_fi @pytest.fixture -def entity_registry_enabled_by_default() -> Generator[None, None, None]: +def entity_registry_enabled_by_default() -> Generator[None]: """Test fixture that ensures ping device_tracker entities are enabled in the registry.""" with patch( "homeassistant.components.ping.device_tracker.PingDeviceTracker.entity_registry_enabled_default", diff --git a/tests/components/plex/conftest.py b/tests/components/plex/conftest.py index 480573216bc..8c2b1434f17 100644 --- a/tests/components/plex/conftest.py +++ b/tests/components/plex/conftest.py @@ -1,10 +1,10 @@ """Fixtures for Plex tests.""" -from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest import requests_mock +from typing_extensions import Generator from homeassistant.components.plex.const import DOMAIN, PLEX_SERVER_CONFIG, SERVERS from homeassistant.const import CONF_URL @@ -22,7 +22,7 @@ def plex_server_url(entry): @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Override async_setup_entry.""" with patch( "homeassistant.components.plex.async_setup_entry", return_value=True diff --git a/tests/components/plugwise/conftest.py b/tests/components/plugwise/conftest.py index c211cd0a741..7264922cd86 100644 --- a/tests/components/plugwise/conftest.py +++ b/tests/components/plugwise/conftest.py @@ -2,13 +2,13 @@ from __future__ import annotations -from collections.abc import Generator import json from typing import Any from unittest.mock import AsyncMock, MagicMock, patch from plugwise import PlugwiseData import pytest +from typing_extensions import Generator from homeassistant.components.plugwise.const import DOMAIN from homeassistant.const import ( @@ -47,7 +47,7 @@ def mock_config_entry() -> MockConfigEntry: @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Mock setting up a config entry.""" with patch( "homeassistant.components.plugwise.async_setup_entry", return_value=True diff --git a/tests/components/poolsense/conftest.py b/tests/components/poolsense/conftest.py index 1095fb66a40..ac16ef23ff3 100644 --- a/tests/components/poolsense/conftest.py +++ b/tests/components/poolsense/conftest.py @@ -1,10 +1,10 @@ """Common fixtures for the Poolsense tests.""" -from collections.abc import Generator from datetime import UTC, datetime from unittest.mock import AsyncMock, patch import pytest +from typing_extensions import Generator from homeassistant.components.poolsense.const import DOMAIN from homeassistant.const import CONF_EMAIL, CONF_PASSWORD @@ -13,7 +13,7 @@ from tests.common import MockConfigEntry @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Override async_setup_entry.""" with patch( "homeassistant.components.poolsense.async_setup_entry", @@ -23,7 +23,7 @@ def mock_setup_entry() -> Generator[AsyncMock, None, None]: @pytest.fixture -def mock_poolsense_client() -> Generator[AsyncMock, None, None]: +def mock_poolsense_client() -> Generator[AsyncMock]: """Mock a PoolSense client.""" with ( patch( diff --git a/tests/components/prosegur/test_alarm_control_panel.py b/tests/components/prosegur/test_alarm_control_panel.py index 0cb84d46f04..b65b86b3049 100644 --- a/tests/components/prosegur/test_alarm_control_panel.py +++ b/tests/components/prosegur/test_alarm_control_panel.py @@ -1,10 +1,10 @@ """Tests for the Prosegur alarm control panel device.""" -from collections.abc import Generator from unittest.mock import AsyncMock, patch from pyprosegur.installation import Status import pytest +from typing_extensions import Generator from homeassistant.components.alarm_control_panel import DOMAIN as ALARM_DOMAIN from homeassistant.const import ( @@ -36,7 +36,7 @@ def mock_auth(): @pytest.fixture(params=list(Status)) -def mock_status(request: pytest.FixtureRequest) -> Generator[None, None, None]: +def mock_status(request: pytest.FixtureRequest) -> Generator[None]: """Mock the status of the alarm.""" install = AsyncMock() diff --git a/tests/components/ps4/conftest.py b/tests/components/ps4/conftest.py index d95acc7e92f..bc84ea3b4db 100644 --- a/tests/components/ps4/conftest.py +++ b/tests/components/ps4/conftest.py @@ -1,14 +1,14 @@ """Test configuration for PS4.""" -from collections.abc import Generator from unittest.mock import MagicMock, patch from pyps4_2ndscreen.ddp import DEFAULT_UDP_PORT, DDPProtocol import pytest +from typing_extensions import Generator @pytest.fixture -def patch_load_json_object() -> Generator[MagicMock, None, None]: +def patch_load_json_object() -> Generator[MagicMock]: """Prevent load JSON being used.""" with patch( "homeassistant.components.ps4.load_json_object", return_value={} @@ -17,21 +17,21 @@ def patch_load_json_object() -> Generator[MagicMock, None, None]: @pytest.fixture -def patch_save_json() -> Generator[MagicMock, None, None]: +def patch_save_json() -> Generator[MagicMock]: """Prevent save JSON being used.""" with patch("homeassistant.components.ps4.save_json") as mock_save: yield mock_save @pytest.fixture -def patch_get_status() -> Generator[MagicMock, None, None]: +def patch_get_status() -> Generator[MagicMock]: """Prevent save JSON being used.""" with patch("pyps4_2ndscreen.ps4.get_status", return_value=None) as mock_get_status: yield mock_get_status @pytest.fixture -def mock_ddp_endpoint() -> Generator[None, None, None]: +def mock_ddp_endpoint() -> Generator[None]: """Mock pyps4_2ndscreen.ddp.async_create_ddp_endpoint.""" protocol = DDPProtocol() protocol._local_port = DEFAULT_UDP_PORT diff --git a/tests/components/pure_energie/conftest.py b/tests/components/pure_energie/conftest.py index ada8d4d84f7..5abee8d9488 100644 --- a/tests/components/pure_energie/conftest.py +++ b/tests/components/pure_energie/conftest.py @@ -1,11 +1,11 @@ """Fixtures for Pure Energie integration tests.""" -from collections.abc import Generator import json from unittest.mock import AsyncMock, MagicMock, patch from gridnet import Device as GridNetDevice, SmartBridge import pytest +from typing_extensions import Generator from homeassistant.components.pure_energie.const import DOMAIN from homeassistant.const import CONF_HOST @@ -26,7 +26,7 @@ def mock_config_entry() -> MockConfigEntry: @pytest.fixture -def mock_setup_entry() -> Generator[None, None, None]: +def mock_setup_entry() -> Generator[None]: """Mock setting up a config entry.""" with patch( "homeassistant.components.pure_energie.async_setup_entry", return_value=True diff --git a/tests/components/pvoutput/conftest.py b/tests/components/pvoutput/conftest.py index 122b55ca4c2..e3f0b253279 100644 --- a/tests/components/pvoutput/conftest.py +++ b/tests/components/pvoutput/conftest.py @@ -2,11 +2,11 @@ from __future__ import annotations -from collections.abc import Generator from unittest.mock import AsyncMock, MagicMock, patch from pvo import Status, System import pytest +from typing_extensions import Generator from homeassistant.components.pvoutput.const import CONF_SYSTEM_ID, DOMAIN from homeassistant.const import CONF_API_KEY @@ -27,7 +27,7 @@ def mock_config_entry() -> MockConfigEntry: @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Mock setting up a config entry.""" with patch( "homeassistant.components.pvoutput.async_setup_entry", return_value=True diff --git a/tests/components/qbittorrent/conftest.py b/tests/components/qbittorrent/conftest.py index 9a5ead35a05..b15e2a6865b 100644 --- a/tests/components/qbittorrent/conftest.py +++ b/tests/components/qbittorrent/conftest.py @@ -1,14 +1,14 @@ """Fixtures for testing qBittorrent component.""" -from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest import requests_mock +from typing_extensions import Generator @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Mock qbittorrent entry setup.""" with patch( "homeassistant.components.qbittorrent.async_setup_entry", return_value=True @@ -17,7 +17,7 @@ def mock_setup_entry() -> Generator[AsyncMock, None, None]: @pytest.fixture -def mock_api() -> Generator[requests_mock.Mocker, None, None]: +def mock_api() -> Generator[requests_mock.Mocker]: """Mock the qbittorrent API.""" with requests_mock.Mocker() as mocker: mocker.get("http://localhost:8080/api/v2/app/preferences", status_code=403) diff --git a/tests/components/qnap/conftest.py b/tests/components/qnap/conftest.py index 512ebc35159..c0947318f60 100644 --- a/tests/components/qnap/conftest.py +++ b/tests/components/qnap/conftest.py @@ -1,9 +1,9 @@ """Setup the QNAP tests.""" -from collections.abc import Generator from unittest.mock import AsyncMock, MagicMock, patch import pytest +from typing_extensions import Generator TEST_HOST = "1.2.3.4" TEST_USERNAME = "admin" @@ -15,7 +15,7 @@ TEST_SYSTEM_STATS = {"system": {"serial_number": TEST_SERIAL, "name": TEST_NAS_N @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Override async_setup_entry.""" with patch( "homeassistant.components.qnap.async_setup_entry", return_value=True @@ -24,7 +24,7 @@ def mock_setup_entry() -> Generator[AsyncMock, None, None]: @pytest.fixture -def qnap_connect() -> Generator[MagicMock, None, None]: +def qnap_connect() -> Generator[MagicMock]: """Mock qnap connection.""" with patch( "homeassistant.components.qnap.config_flow.QNAPStats", autospec=True diff --git a/tests/components/rabbitair/test_config_flow.py b/tests/components/rabbitair/test_config_flow.py index 7ec411d6a48..57b7287db8c 100644 --- a/tests/components/rabbitair/test_config_flow.py +++ b/tests/components/rabbitair/test_config_flow.py @@ -2,12 +2,12 @@ from __future__ import annotations -from collections.abc import Generator from ipaddress import ip_address from unittest.mock import Mock, patch import pytest from rabbitair import Mode, Model, Speed +from typing_extensions import Generator from homeassistant import config_entries from homeassistant.components import zeroconf @@ -43,7 +43,7 @@ def use_mocked_zeroconf(mock_async_zeroconf): @pytest.fixture -def rabbitair_connect() -> Generator[None, None, None]: +def rabbitair_connect() -> Generator[None]: """Mock connection.""" with ( patch("rabbitair.UdpClient.get_info", return_value=get_mock_info()), diff --git a/tests/components/radio_browser/conftest.py b/tests/components/radio_browser/conftest.py index fa732912dc0..95fda545a6c 100644 --- a/tests/components/radio_browser/conftest.py +++ b/tests/components/radio_browser/conftest.py @@ -2,10 +2,10 @@ from __future__ import annotations -from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest +from typing_extensions import Generator from homeassistant.components.radio_browser.const import DOMAIN @@ -23,7 +23,7 @@ def mock_config_entry() -> MockConfigEntry: @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Mock setting up a config entry.""" with patch( "homeassistant.components.radio_browser.async_setup_entry", return_value=True diff --git a/tests/components/rainbird/conftest.py b/tests/components/rainbird/conftest.py index 59471f5eed4..a2c26c71231 100644 --- a/tests/components/rainbird/conftest.py +++ b/tests/components/rainbird/conftest.py @@ -2,7 +2,6 @@ from __future__ import annotations -from collections.abc import Generator from http import HTTPStatus import json from typing import Any @@ -10,6 +9,7 @@ from unittest.mock import patch from pyrainbird import encryption import pytest +from typing_extensions import Generator from homeassistant.components.rainbird import DOMAIN from homeassistant.components.rainbird.const import ( @@ -157,7 +157,7 @@ def setup_platforms( @pytest.fixture(autouse=True) -def aioclient_mock(hass: HomeAssistant) -> Generator[AiohttpClientMocker, None, None]: +def aioclient_mock(hass: HomeAssistant) -> Generator[AiohttpClientMocker]: """Context manager to mock aiohttp client.""" mocker = AiohttpClientMocker() diff --git a/tests/components/rainforest_raven/conftest.py b/tests/components/rainforest_raven/conftest.py index e935dbd3692..0a809c6430a 100644 --- a/tests/components/rainforest_raven/conftest.py +++ b/tests/components/rainforest_raven/conftest.py @@ -1,9 +1,9 @@ """Fixtures for the Rainforest RAVEn tests.""" -from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest +from typing_extensions import Generator from homeassistant.core import HomeAssistant @@ -13,7 +13,7 @@ from tests.common import MockConfigEntry @pytest.fixture -def mock_device() -> Generator[AsyncMock, None, None]: +def mock_device() -> Generator[AsyncMock]: """Mock a functioning RAVEn device.""" mock_device = create_mock_device() with patch( diff --git a/tests/components/rainforest_raven/test_config_flow.py b/tests/components/rainforest_raven/test_config_flow.py index 36e03254dc5..7f7041cbcd8 100644 --- a/tests/components/rainforest_raven/test_config_flow.py +++ b/tests/components/rainforest_raven/test_config_flow.py @@ -1,11 +1,11 @@ """Test Rainforest RAVEn config flow.""" -from collections.abc import Generator from unittest.mock import AsyncMock, patch from aioraven.device import RAVEnConnectionError import pytest from serial.tools.list_ports_common import ListPortInfo +from typing_extensions import Generator from homeassistant.components.rainforest_raven.const import DOMAIN from homeassistant.config_entries import SOURCE_USB, SOURCE_USER @@ -20,7 +20,7 @@ from tests.common import MockConfigEntry @pytest.fixture -def mock_device() -> Generator[AsyncMock, None, None]: +def mock_device() -> Generator[AsyncMock]: """Mock a functioning RAVEn device.""" device = create_mock_device() with patch( @@ -55,7 +55,7 @@ def mock_device_timeout(mock_device: AsyncMock) -> AsyncMock: @pytest.fixture -def mock_comports() -> Generator[list[ListPortInfo], None, None]: +def mock_comports() -> Generator[list[ListPortInfo]]: """Mock serial port list.""" port = ListPortInfo(DISCOVERY_INFO.device) port.serial_number = DISCOVERY_INFO.serial_number diff --git a/tests/components/rdw/conftest.py b/tests/components/rdw/conftest.py index 7e9f485eaef..47d7b02c950 100644 --- a/tests/components/rdw/conftest.py +++ b/tests/components/rdw/conftest.py @@ -2,10 +2,10 @@ from __future__ import annotations -from collections.abc import Generator from unittest.mock import MagicMock, patch import pytest +from typing_extensions import Generator from vehicle import Vehicle from homeassistant.components.rdw.const import CONF_LICENSE_PLATE, DOMAIN @@ -26,7 +26,7 @@ def mock_config_entry() -> MockConfigEntry: @pytest.fixture -def mock_setup_entry() -> Generator[None, None, None]: +def mock_setup_entry() -> Generator[None]: """Mock setting up a config entry.""" with patch("homeassistant.components.rdw.async_setup_entry", return_value=True): yield diff --git a/tests/components/recorder/conftest.py b/tests/components/recorder/conftest.py index 834a8c0a16b..4db573fa65f 100644 --- a/tests/components/recorder/conftest.py +++ b/tests/components/recorder/conftest.py @@ -1,18 +1,16 @@ """Fixtures for the recorder component tests.""" -from collections.abc import Generator from unittest.mock import patch import pytest +from typing_extensions import Generator from homeassistant.components import recorder from homeassistant.core import HomeAssistant @pytest.fixture -def recorder_dialect_name( - hass: HomeAssistant, db_engine: str -) -> Generator[None, None, None]: +def recorder_dialect_name(hass: HomeAssistant, db_engine: str) -> Generator[None]: """Patch the recorder dialect.""" if instance := hass.data.get(recorder.DATA_INSTANCE): instance.__dict__.pop("dialect_name", None) diff --git a/tests/components/recorder/test_filters_with_entityfilter_schema_37.py b/tests/components/recorder/test_filters_with_entityfilter_schema_37.py index 872f694925c..9c66d2ee169 100644 --- a/tests/components/recorder/test_filters_with_entityfilter_schema_37.py +++ b/tests/components/recorder/test_filters_with_entityfilter_schema_37.py @@ -1,12 +1,12 @@ """The tests for the recorder filter matching the EntityFilter component.""" -from collections.abc import AsyncGenerator import json from unittest.mock import patch import pytest from sqlalchemy import select from sqlalchemy.engine.row import Row +from typing_extensions import AsyncGenerator from homeassistant.components.recorder import Recorder, get_instance from homeassistant.components.recorder.db_schema import EventData, Events, States @@ -41,7 +41,7 @@ def db_schema_32(): @pytest.fixture(name="legacy_recorder_mock") async def legacy_recorder_mock_fixture( recorder_mock: Recorder, -) -> AsyncGenerator[Recorder, None]: +) -> AsyncGenerator[Recorder]: """Fixture for legacy recorder mock.""" with patch.object(recorder_mock.states_meta_manager, "active", False): yield recorder_mock diff --git a/tests/components/recorder/test_init.py b/tests/components/recorder/test_init.py index fb43799b4a3..c8cd2807c2e 100644 --- a/tests/components/recorder/test_init.py +++ b/tests/components/recorder/test_init.py @@ -3,7 +3,6 @@ from __future__ import annotations import asyncio -from collections.abc import Generator from datetime import datetime, timedelta from pathlib import Path import sqlite3 @@ -15,6 +14,7 @@ from freezegun.api import FrozenDateTimeFactory import pytest from sqlalchemy.exc import DatabaseError, OperationalError, SQLAlchemyError from sqlalchemy.pool import QueuePool +from typing_extensions import Generator from homeassistant.components import recorder from homeassistant.components.recorder import ( @@ -115,7 +115,7 @@ def setup_recorder(recorder_mock: Recorder) -> None: @pytest.fixture -def small_cache_size() -> Generator[None, None, None]: +def small_cache_size() -> Generator[None]: """Patch the default cache size to 8.""" with ( patch.object(state_attributes_table_manager, "CACHE_SIZE", 8), diff --git a/tests/components/recorder/test_migration_from_schema_32.py b/tests/components/recorder/test_migration_from_schema_32.py index 13e321e5573..8fda495cf60 100644 --- a/tests/components/recorder/test_migration_from_schema_32.py +++ b/tests/components/recorder/test_migration_from_schema_32.py @@ -1,6 +1,5 @@ """The tests for the recorder filter matching the EntityFilter component.""" -from collections.abc import AsyncGenerator import datetime import importlib import sys @@ -13,6 +12,7 @@ import pytest from sqlalchemy import create_engine, inspect from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import Session +from typing_extensions import AsyncGenerator from homeassistant.components import recorder from homeassistant.components.recorder import ( @@ -119,7 +119,7 @@ def db_schema_32(): @pytest.fixture(name="legacy_recorder_mock") async def legacy_recorder_mock_fixture( recorder_mock: Recorder, -) -> AsyncGenerator[Recorder, None]: +) -> AsyncGenerator[Recorder]: """Fixture for legacy recorder mock.""" with patch.object(recorder_mock.states_meta_manager, "active", False): yield recorder_mock diff --git a/tests/components/recorder/test_purge.py b/tests/components/recorder/test_purge.py index b3412e513a8..1ccbaada265 100644 --- a/tests/components/recorder/test_purge.py +++ b/tests/components/recorder/test_purge.py @@ -1,6 +1,5 @@ """Test data purging.""" -from collections.abc import Generator from datetime import datetime, timedelta import json import sqlite3 @@ -10,6 +9,7 @@ from freezegun import freeze_time import pytest from sqlalchemy.exc import DatabaseError, OperationalError from sqlalchemy.orm.session import Session +from typing_extensions import Generator from voluptuous.error import MultipleInvalid from homeassistant.components import recorder @@ -59,7 +59,7 @@ TEST_EVENT_TYPES = ( @pytest.fixture(name="use_sqlite") -def mock_use_sqlite(request: pytest.FixtureRequest) -> Generator[None, None, None]: +def mock_use_sqlite(request: pytest.FixtureRequest) -> Generator[None]: """Pytest fixture to switch purge method.""" with patch( "homeassistant.components.recorder.core.Recorder.dialect_name", diff --git a/tests/components/recorder/test_purge_v32_schema.py b/tests/components/recorder/test_purge_v32_schema.py index 0682f1a5666..e5bd0eae060 100644 --- a/tests/components/recorder/test_purge_v32_schema.py +++ b/tests/components/recorder/test_purge_v32_schema.py @@ -1,6 +1,5 @@ """Test data purging.""" -from collections.abc import Generator from datetime import datetime, timedelta import json import sqlite3 @@ -11,6 +10,7 @@ import pytest from sqlalchemy import text, update from sqlalchemy.exc import DatabaseError, OperationalError from sqlalchemy.orm.session import Session +from typing_extensions import Generator from homeassistant.components import recorder from homeassistant.components.recorder import migration @@ -55,7 +55,7 @@ def db_schema_32(): @pytest.fixture(name="use_sqlite") -def mock_use_sqlite(request: pytest.FixtureRequest) -> Generator[None, None, None]: +def mock_use_sqlite(request: pytest.FixtureRequest) -> Generator[None]: """Pytest fixture to switch purge method.""" with patch( "homeassistant.components.recorder.core.Recorder.dialect_name", diff --git a/tests/components/refoss/conftest.py b/tests/components/refoss/conftest.py index d627af5b5ab..80b3f4d8b75 100644 --- a/tests/components/refoss/conftest.py +++ b/tests/components/refoss/conftest.py @@ -1,13 +1,13 @@ """Pytest module configuration.""" -from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest +from typing_extensions import Generator @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Override async_setup_entry.""" with patch( "homeassistant.components.refoss.async_setup_entry", return_value=True diff --git a/tests/components/renault/conftest.py b/tests/components/renault/conftest.py index c06abc8efd0..a5af01b504a 100644 --- a/tests/components/renault/conftest.py +++ b/tests/components/renault/conftest.py @@ -1,6 +1,5 @@ """Provide common Renault fixtures.""" -from collections.abc import Generator import contextlib from types import MappingProxyType from typing import Any @@ -9,6 +8,7 @@ from unittest.mock import AsyncMock, patch import pytest from renault_api.kamereon import exceptions, schemas from renault_api.renault_account import RenaultAccount +from typing_extensions import Generator from homeassistant.components.renault.const import DOMAIN from homeassistant.config_entries import SOURCE_USER, ConfigEntry @@ -21,7 +21,7 @@ from tests.common import MockConfigEntry, load_fixture @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Override async_setup_entry.""" with patch( "homeassistant.components.renault.async_setup_entry", return_value=True diff --git a/tests/components/renault/test_binary_sensor.py b/tests/components/renault/test_binary_sensor.py index 7a0d593a4c4..a0264493544 100644 --- a/tests/components/renault/test_binary_sensor.py +++ b/tests/components/renault/test_binary_sensor.py @@ -1,10 +1,10 @@ """Tests for Renault binary sensors.""" -from collections.abc import Generator from unittest.mock import patch import pytest from syrupy.assertion import SnapshotAssertion +from typing_extensions import Generator from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform @@ -18,7 +18,7 @@ pytestmark = pytest.mark.usefixtures("patch_renault_account", "patch_get_vehicle @pytest.fixture(autouse=True) -def override_platforms() -> Generator[None, None, None]: +def override_platforms() -> Generator[None]: """Override PLATFORMS.""" with patch("homeassistant.components.renault.PLATFORMS", [Platform.BINARY_SENSOR]): yield diff --git a/tests/components/renault/test_button.py b/tests/components/renault/test_button.py index d592f040c97..bed188d8881 100644 --- a/tests/components/renault/test_button.py +++ b/tests/components/renault/test_button.py @@ -1,11 +1,11 @@ """Tests for Renault sensors.""" -from collections.abc import Generator from unittest.mock import patch import pytest from renault_api.kamereon import schemas from syrupy.assertion import SnapshotAssertion +from typing_extensions import Generator from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS from homeassistant.config_entries import ConfigEntry @@ -22,7 +22,7 @@ pytestmark = pytest.mark.usefixtures("patch_renault_account", "patch_get_vehicle @pytest.fixture(autouse=True) -def override_platforms() -> Generator[None, None, None]: +def override_platforms() -> Generator[None]: """Override PLATFORMS.""" with patch("homeassistant.components.renault.PLATFORMS", [Platform.BUTTON]): yield diff --git a/tests/components/renault/test_device_tracker.py b/tests/components/renault/test_device_tracker.py index a809ce82e6e..d8bee097eda 100644 --- a/tests/components/renault/test_device_tracker.py +++ b/tests/components/renault/test_device_tracker.py @@ -1,10 +1,10 @@ """Tests for Renault sensors.""" -from collections.abc import Generator from unittest.mock import patch import pytest from syrupy.assertion import SnapshotAssertion +from typing_extensions import Generator from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform @@ -18,7 +18,7 @@ pytestmark = pytest.mark.usefixtures("patch_renault_account", "patch_get_vehicle @pytest.fixture(autouse=True) -def override_platforms() -> Generator[None, None, None]: +def override_platforms() -> Generator[None]: """Override PLATFORMS.""" with patch("homeassistant.components.renault.PLATFORMS", [Platform.DEVICE_TRACKER]): yield diff --git a/tests/components/renault/test_init.py b/tests/components/renault/test_init.py index 0cc203c0485..90963fd3521 100644 --- a/tests/components/renault/test_init.py +++ b/tests/components/renault/test_init.py @@ -1,12 +1,12 @@ """Tests for Renault setup process.""" -from collections.abc import Generator from typing import Any from unittest.mock import Mock, patch import aiohttp import pytest from renault_api.gigya.exceptions import GigyaException, InvalidCredentialsException +from typing_extensions import Generator from homeassistant.components.renault.const import DOMAIN from homeassistant.config_entries import ConfigEntry, ConfigEntryState @@ -18,7 +18,7 @@ from tests.typing import WebSocketGenerator @pytest.fixture(autouse=True) -def override_platforms() -> Generator[None, None, None]: +def override_platforms() -> Generator[None]: """Override PLATFORMS.""" with patch("homeassistant.components.renault.PLATFORMS", []): yield diff --git a/tests/components/renault/test_select.py b/tests/components/renault/test_select.py index 5dcd798def2..0577966d514 100644 --- a/tests/components/renault/test_select.py +++ b/tests/components/renault/test_select.py @@ -1,11 +1,11 @@ """Tests for Renault selects.""" -from collections.abc import Generator from unittest.mock import patch import pytest from renault_api.kamereon import schemas from syrupy.assertion import SnapshotAssertion +from typing_extensions import Generator from homeassistant.components.select import ( ATTR_OPTION, @@ -26,7 +26,7 @@ pytestmark = pytest.mark.usefixtures("patch_renault_account", "patch_get_vehicle @pytest.fixture(autouse=True) -def override_platforms() -> Generator[None, None, None]: +def override_platforms() -> Generator[None]: """Override PLATFORMS.""" with patch("homeassistant.components.renault.PLATFORMS", [Platform.SELECT]): yield diff --git a/tests/components/renault/test_sensor.py b/tests/components/renault/test_sensor.py index bd94aa8d8e1..7e8e4f24c77 100644 --- a/tests/components/renault/test_sensor.py +++ b/tests/components/renault/test_sensor.py @@ -1,10 +1,10 @@ """Tests for Renault sensors.""" -from collections.abc import Generator from unittest.mock import patch import pytest from syrupy.assertion import SnapshotAssertion +from typing_extensions import Generator from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform @@ -18,7 +18,7 @@ pytestmark = pytest.mark.usefixtures("patch_renault_account", "patch_get_vehicle @pytest.fixture(autouse=True) -def override_platforms() -> Generator[None, None, None]: +def override_platforms() -> Generator[None]: """Override PLATFORMS.""" with patch("homeassistant.components.renault.PLATFORMS", [Platform.SENSOR]): yield diff --git a/tests/components/renault/test_services.py b/tests/components/renault/test_services.py index 9a6d520ccf1..d30626e4117 100644 --- a/tests/components/renault/test_services.py +++ b/tests/components/renault/test_services.py @@ -1,6 +1,5 @@ """Tests for Renault sensors.""" -from collections.abc import Generator from datetime import datetime from unittest.mock import patch @@ -8,6 +7,7 @@ import pytest from renault_api.exceptions import RenaultException from renault_api.kamereon import schemas from renault_api.kamereon.models import ChargeSchedule +from typing_extensions import Generator from homeassistant.components.renault.const import DOMAIN from homeassistant.components.renault.services import ( @@ -39,7 +39,7 @@ pytestmark = pytest.mark.usefixtures("patch_renault_account", "patch_get_vehicle @pytest.fixture(autouse=True) -def override_platforms() -> Generator[None, None, None]: +def override_platforms() -> Generator[None]: """Override PLATFORMS.""" with patch("homeassistant.components.renault.PLATFORMS", []): yield diff --git a/tests/components/reolink/conftest.py b/tests/components/reolink/conftest.py index 6cf88b9b00d..d997b57bb52 100644 --- a/tests/components/reolink/conftest.py +++ b/tests/components/reolink/conftest.py @@ -1,9 +1,9 @@ """Setup the Reolink tests.""" -from collections.abc import Generator from unittest.mock import AsyncMock, MagicMock, patch import pytest +from typing_extensions import Generator from homeassistant.components.reolink import const from homeassistant.components.reolink.config_flow import DEFAULT_PROTOCOL @@ -38,7 +38,7 @@ TEST_CAM_MODEL = "RLC-123" @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Override async_setup_entry.""" with patch( "homeassistant.components.reolink.async_setup_entry", return_value=True @@ -47,7 +47,7 @@ def mock_setup_entry() -> Generator[AsyncMock, None, None]: @pytest.fixture -def reolink_connect_class() -> Generator[MagicMock, None, None]: +def reolink_connect_class() -> Generator[MagicMock]: """Mock reolink connection and return both the host_mock and host_mock_class.""" with ( patch( @@ -105,13 +105,13 @@ def reolink_connect_class() -> Generator[MagicMock, None, None]: @pytest.fixture def reolink_connect( reolink_connect_class: MagicMock, -) -> Generator[MagicMock, None, None]: +) -> Generator[MagicMock]: """Mock reolink connection.""" return reolink_connect_class.return_value @pytest.fixture -def reolink_platforms() -> Generator[None, None, None]: +def reolink_platforms() -> Generator[None]: """Mock reolink entry setup.""" with patch("homeassistant.components.reolink.PLATFORMS", return_value=[]): yield diff --git a/tests/components/ring/conftest.py b/tests/components/ring/conftest.py index 70c067af887..82526d87b22 100644 --- a/tests/components/ring/conftest.py +++ b/tests/components/ring/conftest.py @@ -1,11 +1,11 @@ """Configuration for Ring tests.""" -from collections.abc import Generator import re from unittest.mock import AsyncMock, Mock, patch import pytest import requests_mock +from typing_extensions import Generator from homeassistant.components.ring import DOMAIN from homeassistant.const import CONF_USERNAME @@ -16,7 +16,7 @@ from tests.components.light.conftest import mock_light_profiles # noqa: F401 @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Override async_setup_entry.""" with patch( "homeassistant.components.ring.async_setup_entry", return_value=True diff --git a/tests/components/roku/conftest.py b/tests/components/roku/conftest.py index 4cec3e233e6..09e62933d3d 100644 --- a/tests/components/roku/conftest.py +++ b/tests/components/roku/conftest.py @@ -1,11 +1,11 @@ """Fixtures for Roku integration tests.""" -from collections.abc import Generator import json from unittest.mock import MagicMock, patch import pytest from rokuecp import Device as RokuDevice +from typing_extensions import Generator from homeassistant.components.roku.const import DOMAIN from homeassistant.const import CONF_HOST @@ -32,7 +32,7 @@ def mock_config_entry() -> MockConfigEntry: @pytest.fixture -def mock_setup_entry() -> Generator[None, None, None]: +def mock_setup_entry() -> Generator[None]: """Mock setting up a config entry.""" with patch("homeassistant.components.roku.async_setup_entry", return_value=True): yield diff --git a/tests/components/rtsp_to_webrtc/conftest.py b/tests/components/rtsp_to_webrtc/conftest.py index f80aedb2808..cdb7a9d0cfc 100644 --- a/tests/components/rtsp_to_webrtc/conftest.py +++ b/tests/components/rtsp_to_webrtc/conftest.py @@ -2,12 +2,13 @@ from __future__ import annotations -from collections.abc import AsyncGenerator, Awaitable, Callable +from collections.abc import Awaitable, Callable from typing import Any from unittest.mock import patch import pytest import rtsp_to_webrtc +from typing_extensions import AsyncGenerator from homeassistant.components import camera from homeassistant.components.rtsp_to_webrtc import DOMAIN @@ -24,7 +25,7 @@ CONFIG_ENTRY_DATA = {"server_url": SERVER_URL} # Typing helpers type ComponentSetup = Callable[[], Awaitable[None]] -type AsyncYieldFixture[_T] = AsyncGenerator[_T, None] +type AsyncYieldFixture[_T] = AsyncGenerator[_T] @pytest.fixture(autouse=True) @@ -38,7 +39,7 @@ async def webrtc_server() -> None: @pytest.fixture -async def mock_camera(hass) -> AsyncGenerator[None, None]: +async def mock_camera(hass) -> AsyncGenerator[None]: """Initialize a demo camera platform.""" assert await async_setup_component( hass, "camera", {camera.DOMAIN: {"platform": "demo"}} diff --git a/tests/components/sabnzbd/conftest.py b/tests/components/sabnzbd/conftest.py index d1854017452..7d68d3108f0 100644 --- a/tests/components/sabnzbd/conftest.py +++ b/tests/components/sabnzbd/conftest.py @@ -1,13 +1,13 @@ """Configuration for Sabnzbd tests.""" -from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest +from typing_extensions import Generator @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Override async_setup_entry.""" with patch( "homeassistant.components.sabnzbd.async_setup_entry", return_value=True diff --git a/tests/components/samsungtv/conftest.py b/tests/components/samsungtv/conftest.py index c7ac8785cbe..8518fc4c586 100644 --- a/tests/components/samsungtv/conftest.py +++ b/tests/components/samsungtv/conftest.py @@ -2,7 +2,7 @@ from __future__ import annotations -from collections.abc import Awaitable, Callable, Generator +from collections.abc import Awaitable, Callable from datetime import datetime from socket import AddressFamily from typing import Any @@ -19,6 +19,7 @@ from samsungtvws.encrypted.remote import SamsungTVEncryptedWSAsyncRemote from samsungtvws.event import ED_INSTALLED_APP_EVENT from samsungtvws.exceptions import ResponseError from samsungtvws.remote import ChannelEmitCommand +from typing_extensions import Generator from homeassistant.components.samsungtv.const import WEBSOCKET_SSL_PORT from homeassistant.core import HomeAssistant, ServiceCall @@ -30,7 +31,7 @@ from tests.common import async_mock_service @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Override async_setup_entry.""" with patch( "homeassistant.components.samsungtv.async_setup_entry", return_value=True diff --git a/tests/components/sanix/conftest.py b/tests/components/sanix/conftest.py index d1f4424b166..86eaa870770 100644 --- a/tests/components/sanix/conftest.py +++ b/tests/components/sanix/conftest.py @@ -1,6 +1,5 @@ """Sanix tests configuration.""" -from collections.abc import Generator from datetime import datetime from unittest.mock import AsyncMock, patch from zoneinfo import ZoneInfo @@ -17,6 +16,7 @@ from sanix import ( ATTR_API_TIME, ) from sanix.models import Measurement +from typing_extensions import Generator from homeassistant.components.sanix.const import CONF_SERIAL_NUMBER, DOMAIN from homeassistant.const import CONF_TOKEN @@ -67,7 +67,7 @@ def mock_config_entry() -> MockConfigEntry: @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Override async_setup_entry.""" with patch( "homeassistant.components.sanix.async_setup_entry", diff --git a/tests/components/schlage/conftest.py b/tests/components/schlage/conftest.py index 40d880b73f8..dcb6bc52a7b 100644 --- a/tests/components/schlage/conftest.py +++ b/tests/components/schlage/conftest.py @@ -1,10 +1,10 @@ """Common fixtures for the Schlage tests.""" -from collections.abc import Generator from unittest.mock import AsyncMock, Mock, create_autospec, patch from pyschlage.lock import Lock import pytest +from typing_extensions import Generator from homeassistant.components.schlage.const import DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME @@ -46,7 +46,7 @@ async def mock_added_config_entry( @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Override async_setup_entry.""" with patch( "homeassistant.components.schlage.async_setup_entry", return_value=True diff --git a/tests/components/scrape/conftest.py b/tests/components/scrape/conftest.py index a7181943884..f6109dbc19a 100644 --- a/tests/components/scrape/conftest.py +++ b/tests/components/scrape/conftest.py @@ -2,12 +2,12 @@ from __future__ import annotations -from collections.abc import Generator from typing import Any from unittest.mock import AsyncMock, patch import uuid import pytest +from typing_extensions import Generator from homeassistant.components.rest.data import DEFAULT_TIMEOUT from homeassistant.components.rest.schema import DEFAULT_METHOD, DEFAULT_VERIFY_SSL @@ -35,7 +35,7 @@ from tests.common import MockConfigEntry @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Automatically path uuid generator.""" with patch( "homeassistant.components.scrape.async_setup_entry", diff --git a/tests/components/screenlogic/test_services.py b/tests/components/screenlogic/test_services.py index 0e2d059fb84..d175ea27c84 100644 --- a/tests/components/screenlogic/test_services.py +++ b/tests/components/screenlogic/test_services.py @@ -1,12 +1,12 @@ """Tests for ScreenLogic integration service calls.""" -from collections.abc import AsyncGenerator from typing import Any from unittest.mock import DEFAULT, AsyncMock, patch import pytest from screenlogicpy import ScreenLogicGateway from screenlogicpy.device_const.system import COLOR_MODE +from typing_extensions import AsyncGenerator from homeassistant.components.screenlogic import DOMAIN from homeassistant.components.screenlogic.const import ( @@ -53,7 +53,7 @@ async def setup_screenlogic_services_fixture( request: pytest.FixtureRequest, device_registry: dr.DeviceRegistry, mock_config_entry: MockConfigEntry, -) -> AsyncGenerator[dict[str, Any], None]: +) -> AsyncGenerator[dict[str, Any]]: """Define the setup for a patched screenlogic integration.""" data = ( marker.args[0] diff --git a/tests/components/season/conftest.py b/tests/components/season/conftest.py index b0b4f1058d9..a45a2078d9b 100644 --- a/tests/components/season/conftest.py +++ b/tests/components/season/conftest.py @@ -2,10 +2,10 @@ from __future__ import annotations -from collections.abc import Generator from unittest.mock import patch import pytest +from typing_extensions import Generator from homeassistant.components.season.const import DOMAIN, TYPE_ASTRONOMICAL from homeassistant.const import CONF_TYPE @@ -25,7 +25,7 @@ def mock_config_entry() -> MockConfigEntry: @pytest.fixture -def mock_setup_entry() -> Generator[None, None, None]: +def mock_setup_entry() -> Generator[None]: """Mock setting up a config entry.""" with patch("homeassistant.components.season.async_setup_entry", return_value=True): yield diff --git a/tests/components/sensor/test_init.py b/tests/components/sensor/test_init.py index 8dc82483a40..9a1af587a0a 100644 --- a/tests/components/sensor/test_init.py +++ b/tests/components/sensor/test_init.py @@ -2,7 +2,6 @@ from __future__ import annotations -from collections.abc import Generator from datetime import UTC, date, datetime from decimal import Decimal import logging @@ -10,6 +9,7 @@ from types import ModuleType from typing import Any import pytest +from typing_extensions import Generator from homeassistant.components import sensor from homeassistant.components.number import NumberDeviceClass @@ -2384,7 +2384,7 @@ class MockFlow(ConfigFlow): @pytest.fixture(autouse=True) -def config_flow_fixture(hass: HomeAssistant) -> Generator[None, None, None]: +def config_flow_fixture(hass: HomeAssistant) -> Generator[None]: """Mock config flow.""" mock_platform(hass, f"{TEST_DOMAIN}.config_flow") diff --git a/tests/components/seventeentrack/conftest.py b/tests/components/seventeentrack/conftest.py index 2e266a9b13c..1ab4eed11ee 100644 --- a/tests/components/seventeentrack/conftest.py +++ b/tests/components/seventeentrack/conftest.py @@ -1,10 +1,10 @@ """Configuration for 17Track tests.""" -from collections.abc import Generator from unittest.mock import AsyncMock, patch from py17track.package import Package import pytest +from typing_extensions import Generator from homeassistant.components.seventeentrack.const import ( CONF_SHOW_ARCHIVED, @@ -69,7 +69,7 @@ VALID_PLATFORM_CONFIG_FULL = { @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Override async_setup_entry.""" with patch( "homeassistant.components.seventeentrack.async_setup_entry", return_value=True diff --git a/tests/components/sfr_box/conftest.py b/tests/components/sfr_box/conftest.py index dec99738a03..e86cd06650e 100644 --- a/tests/components/sfr_box/conftest.py +++ b/tests/components/sfr_box/conftest.py @@ -1,11 +1,11 @@ """Provide common SFR Box fixtures.""" -from collections.abc import Generator import json from unittest.mock import AsyncMock, patch import pytest from sfrbox_api.models import DslInfo, FtthInfo, SystemInfo, WanInfo +from typing_extensions import Generator from homeassistant.components.sfr_box.const import DOMAIN from homeassistant.config_entries import SOURCE_USER, ConfigEntry @@ -16,7 +16,7 @@ from tests.common import MockConfigEntry, load_fixture @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Override async_setup_entry.""" with patch( "homeassistant.components.sfr_box.async_setup_entry", return_value=True @@ -59,7 +59,7 @@ def get_config_entry_with_auth(hass: HomeAssistant) -> ConfigEntry: @pytest.fixture -def dsl_get_info() -> Generator[DslInfo, None, None]: +def dsl_get_info() -> Generator[DslInfo]: """Fixture for SFRBox.dsl_get_info.""" dsl_info = DslInfo(**json.loads(load_fixture("dsl_getInfo.json", DOMAIN))) with patch( @@ -70,7 +70,7 @@ def dsl_get_info() -> Generator[DslInfo, None, None]: @pytest.fixture -def ftth_get_info() -> Generator[FtthInfo, None, None]: +def ftth_get_info() -> Generator[FtthInfo]: """Fixture for SFRBox.ftth_get_info.""" info = FtthInfo(**json.loads(load_fixture("ftth_getInfo.json", DOMAIN))) with patch( @@ -81,7 +81,7 @@ def ftth_get_info() -> Generator[FtthInfo, None, None]: @pytest.fixture -def system_get_info() -> Generator[SystemInfo, None, None]: +def system_get_info() -> Generator[SystemInfo]: """Fixture for SFRBox.system_get_info.""" info = SystemInfo(**json.loads(load_fixture("system_getInfo.json", DOMAIN))) with patch( @@ -92,7 +92,7 @@ def system_get_info() -> Generator[SystemInfo, None, None]: @pytest.fixture -def wan_get_info() -> Generator[WanInfo, None, None]: +def wan_get_info() -> Generator[WanInfo]: """Fixture for SFRBox.wan_get_info.""" info = WanInfo(**json.loads(load_fixture("wan_getInfo.json", DOMAIN))) with patch( diff --git a/tests/components/sfr_box/test_binary_sensor.py b/tests/components/sfr_box/test_binary_sensor.py index f3d012712ca..8dba537f6cb 100644 --- a/tests/components/sfr_box/test_binary_sensor.py +++ b/tests/components/sfr_box/test_binary_sensor.py @@ -1,11 +1,11 @@ """Test the SFR Box binary sensors.""" -from collections.abc import Generator from unittest.mock import patch import pytest from sfrbox_api.models import SystemInfo from syrupy.assertion import SnapshotAssertion +from typing_extensions import Generator from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform @@ -18,7 +18,7 @@ pytestmark = pytest.mark.usefixtures( @pytest.fixture(autouse=True) -def override_platforms() -> Generator[None, None, None]: +def override_platforms() -> Generator[None]: """Override PLATFORMS.""" with patch("homeassistant.components.sfr_box.PLATFORMS", [Platform.BINARY_SENSOR]): yield diff --git a/tests/components/sfr_box/test_button.py b/tests/components/sfr_box/test_button.py index 618ad6fc34b..4f20a2f34a3 100644 --- a/tests/components/sfr_box/test_button.py +++ b/tests/components/sfr_box/test_button.py @@ -1,11 +1,11 @@ """Test the SFR Box buttons.""" -from collections.abc import Generator from unittest.mock import patch import pytest from sfrbox_api.exceptions import SFRBoxError from syrupy.assertion import SnapshotAssertion +from typing_extensions import Generator from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS from homeassistant.config_entries import ConfigEntry @@ -18,7 +18,7 @@ pytestmark = pytest.mark.usefixtures("system_get_info", "dsl_get_info", "wan_get @pytest.fixture(autouse=True) -def override_platforms() -> Generator[None, None, None]: +def override_platforms() -> Generator[None]: """Override PLATFORMS_WITH_AUTH.""" with ( patch( diff --git a/tests/components/sfr_box/test_diagnostics.py b/tests/components/sfr_box/test_diagnostics.py index 512a737d434..597631d12f1 100644 --- a/tests/components/sfr_box/test_diagnostics.py +++ b/tests/components/sfr_box/test_diagnostics.py @@ -1,11 +1,11 @@ """Test the SFR Box diagnostics.""" -from collections.abc import Generator from unittest.mock import patch import pytest from sfrbox_api.models import SystemInfo from syrupy.assertion import SnapshotAssertion +from typing_extensions import Generator from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -19,7 +19,7 @@ pytestmark = pytest.mark.usefixtures( @pytest.fixture(autouse=True) -def override_platforms() -> Generator[None, None, None]: +def override_platforms() -> Generator[None]: """Override PLATFORMS.""" with patch("homeassistant.components.sfr_box.PLATFORMS", []): yield diff --git a/tests/components/sfr_box/test_init.py b/tests/components/sfr_box/test_init.py index 4bcd4ae9208..14688009c5c 100644 --- a/tests/components/sfr_box/test_init.py +++ b/tests/components/sfr_box/test_init.py @@ -1,10 +1,10 @@ """Test the SFR Box setup process.""" -from collections.abc import Generator from unittest.mock import patch import pytest from sfrbox_api.exceptions import SFRBoxAuthenticationError, SFRBoxError +from typing_extensions import Generator from homeassistant.components.sfr_box.const import DOMAIN from homeassistant.config_entries import ConfigEntry, ConfigEntryState @@ -12,7 +12,7 @@ from homeassistant.core import HomeAssistant @pytest.fixture(autouse=True) -def override_platforms() -> Generator[None, None, None]: +def override_platforms() -> Generator[None]: """Override PLATFORMS.""" with patch("homeassistant.components.sfr_box.PLATFORMS", []): yield diff --git a/tests/components/sfr_box/test_sensor.py b/tests/components/sfr_box/test_sensor.py index afdcf87b9db..506e1ed8962 100644 --- a/tests/components/sfr_box/test_sensor.py +++ b/tests/components/sfr_box/test_sensor.py @@ -1,10 +1,10 @@ """Test the SFR Box sensors.""" -from collections.abc import Generator from unittest.mock import patch import pytest from syrupy.assertion import SnapshotAssertion +from typing_extensions import Generator from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform @@ -15,7 +15,7 @@ pytestmark = pytest.mark.usefixtures("system_get_info", "dsl_get_info", "wan_get @pytest.fixture(autouse=True) -def override_platforms() -> Generator[None, None, None]: +def override_platforms() -> Generator[None]: """Override PLATFORMS.""" with patch("homeassistant.components.sfr_box.PLATFORMS", [Platform.SENSOR]): yield diff --git a/tests/components/sleepiq/conftest.py b/tests/components/sleepiq/conftest.py index 3a53e8ce684..fd07cc414e7 100644 --- a/tests/components/sleepiq/conftest.py +++ b/tests/components/sleepiq/conftest.py @@ -2,7 +2,6 @@ from __future__ import annotations -from collections.abc import Generator from unittest.mock import AsyncMock, MagicMock, create_autospec, patch from asyncsleepiq import ( @@ -18,6 +17,7 @@ from asyncsleepiq import ( SleepIQSleeper, ) import pytest +from typing_extensions import Generator from homeassistant.components.sleepiq import DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME @@ -46,7 +46,7 @@ SLEEPIQ_CONFIG = { @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Override async_setup_entry.""" with patch( "homeassistant.components.sleepiq.async_setup_entry", return_value=True @@ -97,7 +97,7 @@ def mock_bed() -> MagicMock: @pytest.fixture def mock_asyncsleepiq_single_foundation( mock_bed: MagicMock, -) -> Generator[MagicMock, None, None]: +) -> Generator[MagicMock]: """Mock an AsyncSleepIQ object with a single foundation.""" with patch("homeassistant.components.sleepiq.AsyncSleepIQ", autospec=True) as mock: client = mock.return_value @@ -131,7 +131,7 @@ def mock_asyncsleepiq_single_foundation( @pytest.fixture -def mock_asyncsleepiq(mock_bed: MagicMock) -> Generator[MagicMock, None, None]: +def mock_asyncsleepiq(mock_bed: MagicMock) -> Generator[MagicMock]: """Mock an AsyncSleepIQ object with a split foundation.""" with patch("homeassistant.components.sleepiq.AsyncSleepIQ", autospec=True) as mock: client = mock.return_value diff --git a/tests/components/slimproto/conftest.py b/tests/components/slimproto/conftest.py index 637f5ec0a99..ece30d3e5cf 100644 --- a/tests/components/slimproto/conftest.py +++ b/tests/components/slimproto/conftest.py @@ -2,10 +2,10 @@ from __future__ import annotations -from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest +from typing_extensions import Generator from homeassistant.components.slimproto.const import DOMAIN @@ -23,7 +23,7 @@ def mock_config_entry() -> MockConfigEntry: @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Mock setting up a config entry.""" with patch( "homeassistant.components.slimproto.async_setup_entry", diff --git a/tests/components/snapcast/conftest.py b/tests/components/snapcast/conftest.py index 9e3325bd73a..e5806ac5f40 100644 --- a/tests/components/snapcast/conftest.py +++ b/tests/components/snapcast/conftest.py @@ -1,13 +1,13 @@ """Test the snapcast config flow.""" -from collections.abc import Generator from unittest.mock import AsyncMock, MagicMock, patch import pytest +from typing_extensions import Generator @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Override async_setup_entry.""" with patch( "homeassistant.components.snapcast.async_setup_entry", return_value=True @@ -16,7 +16,7 @@ def mock_setup_entry() -> Generator[AsyncMock, None, None]: @pytest.fixture -def mock_create_server() -> Generator[AsyncMock, None, None]: +def mock_create_server() -> Generator[AsyncMock]: """Create mock snapcast connection.""" mock_connection = AsyncMock() mock_connection.start = AsyncMock(return_value=None) diff --git a/tests/components/sonarr/conftest.py b/tests/components/sonarr/conftest.py index 7c18fb372a1..06a08eb7724 100644 --- a/tests/components/sonarr/conftest.py +++ b/tests/components/sonarr/conftest.py @@ -1,6 +1,5 @@ """Fixtures for Sonarr integration tests.""" -from collections.abc import Generator import json from unittest.mock import MagicMock, patch @@ -14,6 +13,7 @@ from aiopyarr import ( SystemStatus, ) import pytest +from typing_extensions import Generator from homeassistant.components.sonarr.const import ( CONF_BASE_PATH, @@ -102,7 +102,7 @@ def mock_config_entry() -> MockConfigEntry: @pytest.fixture -def mock_setup_entry() -> Generator[None, None, None]: +def mock_setup_entry() -> Generator[None]: """Mock setting up a config entry.""" with patch("homeassistant.components.sonarr.async_setup_entry", return_value=True): yield diff --git a/tests/components/stream/conftest.py b/tests/components/stream/conftest.py index 280d15cd1ef..3cf3de54940 100644 --- a/tests/components/stream/conftest.py +++ b/tests/components/stream/conftest.py @@ -13,13 +13,13 @@ so that it can inspect the output. from __future__ import annotations import asyncio -from collections.abc import Generator import logging import threading from unittest.mock import Mock, patch from aiohttp import web import pytest +from typing_extensions import Generator from homeassistant.components.stream.core import StreamOutput from homeassistant.components.stream.worker import StreamState @@ -175,7 +175,7 @@ def hls_sync(): @pytest.fixture(autouse=True) -def should_retry() -> Generator[Mock, None, None]: +def should_retry() -> Generator[Mock]: """Fixture to disable stream worker retries in tests by default.""" with patch( "homeassistant.components.stream._should_retry", return_value=False diff --git a/tests/components/streamlabswater/conftest.py b/tests/components/streamlabswater/conftest.py index c303c1b7ef0..5a53c7204fa 100644 --- a/tests/components/streamlabswater/conftest.py +++ b/tests/components/streamlabswater/conftest.py @@ -1,10 +1,10 @@ """Common fixtures for the StreamLabs tests.""" -from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest from streamlabswater.streamlabswater import StreamlabsClient +from typing_extensions import Generator from homeassistant.components.streamlabswater import DOMAIN from homeassistant.const import CONF_API_KEY @@ -13,7 +13,7 @@ from tests.common import MockConfigEntry, load_json_object_fixture @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Override async_setup_entry.""" with patch( "homeassistant.components.streamlabswater.async_setup_entry", return_value=True @@ -32,7 +32,7 @@ def mock_config_entry() -> MockConfigEntry: @pytest.fixture(name="streamlabswater") -def mock_streamlabswater() -> Generator[AsyncMock, None, None]: +def mock_streamlabswater() -> Generator[AsyncMock]: """Mock the StreamLabs client.""" locations = load_json_object_fixture("streamlabswater/get_locations.json") diff --git a/tests/components/stt/test_init.py b/tests/components/stt/test_init.py index 9aa889f27c9..d28d9c308a7 100644 --- a/tests/components/stt/test_init.py +++ b/tests/components/stt/test_init.py @@ -1,11 +1,12 @@ """Test STT component setup.""" -from collections.abc import AsyncIterable, Generator +from collections.abc import AsyncIterable from http import HTTPStatus from pathlib import Path from unittest.mock import AsyncMock import pytest +from typing_extensions import Generator from homeassistant.components.stt import ( DOMAIN, @@ -131,7 +132,7 @@ def config_flow_test_domain_fixture() -> str: @pytest.fixture(autouse=True) def config_flow_fixture( hass: HomeAssistant, config_flow_test_domain: str -) -> Generator[None, None, None]: +) -> Generator[None]: """Mock config flow.""" mock_platform(hass, f"{config_flow_test_domain}.config_flow") diff --git a/tests/components/suez_water/conftest.py b/tests/components/suez_water/conftest.py index 6c124bec30e..51ade6009dc 100644 --- a/tests/components/suez_water/conftest.py +++ b/tests/components/suez_water/conftest.py @@ -1,13 +1,13 @@ """Common fixtures for the Suez Water tests.""" -from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest +from typing_extensions import Generator @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Override async_setup_entry.""" with patch( "homeassistant.components.suez_water.async_setup_entry", return_value=True diff --git a/tests/components/swiss_public_transport/conftest.py b/tests/components/swiss_public_transport/conftest.py index d01fba0f9d0..c139b99e54d 100644 --- a/tests/components/swiss_public_transport/conftest.py +++ b/tests/components/swiss_public_transport/conftest.py @@ -1,13 +1,13 @@ """Common fixtures for the swiss_public_transport tests.""" -from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest +from typing_extensions import Generator @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Override async_setup_entry.""" with patch( "homeassistant.components.swiss_public_transport.async_setup_entry", diff --git a/tests/components/switch_as_x/conftest.py b/tests/components/switch_as_x/conftest.py index 82827924070..88a86892d2d 100644 --- a/tests/components/switch_as_x/conftest.py +++ b/tests/components/switch_as_x/conftest.py @@ -2,10 +2,10 @@ from __future__ import annotations -from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest +from typing_extensions import Generator from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component @@ -18,7 +18,7 @@ async def setup_homeassistant(hass: HomeAssistant): @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Mock setting up a config entry.""" with patch( "homeassistant.components.switch_as_x.async_setup_entry", return_value=True diff --git a/tests/components/switchbot_cloud/conftest.py b/tests/components/switchbot_cloud/conftest.py index bfaea2c5a31..ed233ff2de9 100644 --- a/tests/components/switchbot_cloud/conftest.py +++ b/tests/components/switchbot_cloud/conftest.py @@ -1,13 +1,13 @@ """Common fixtures for the SwitchBot via API tests.""" -from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest +from typing_extensions import Generator @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Override async_setup_entry.""" with patch( "homeassistant.components.switchbot_cloud.async_setup_entry", diff --git a/tests/components/switcher_kis/conftest.py b/tests/components/switcher_kis/conftest.py index c9f98efbc50..8ff395fcab3 100644 --- a/tests/components/switcher_kis/conftest.py +++ b/tests/components/switcher_kis/conftest.py @@ -1,13 +1,13 @@ """Common fixtures and objects for the Switcher integration tests.""" -from collections.abc import Generator from unittest.mock import AsyncMock, MagicMock, Mock, patch import pytest +from typing_extensions import Generator @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Override async_setup_entry.""" with patch( "homeassistant.components.switcher_kis.async_setup_entry", return_value=True @@ -16,7 +16,7 @@ def mock_setup_entry() -> Generator[AsyncMock, None, None]: @pytest.fixture -def mock_bridge(request: pytest.FixtureRequest) -> Generator[MagicMock, None, None]: +def mock_bridge(request: pytest.FixtureRequest) -> Generator[MagicMock]: """Return a mocked SwitcherBridge.""" with ( patch( diff --git a/tests/components/synology_dsm/conftest.py b/tests/components/synology_dsm/conftest.py index 044a3738543..2f05d0187be 100644 --- a/tests/components/synology_dsm/conftest.py +++ b/tests/components/synology_dsm/conftest.py @@ -1,16 +1,16 @@ """Configure Synology DSM tests.""" -from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest +from typing_extensions import Generator from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Mock setting up a config entry.""" with patch( "homeassistant.components.synology_dsm.async_setup_entry", return_value=True diff --git a/tests/components/systemmonitor/conftest.py b/tests/components/systemmonitor/conftest.py index c8cf614e04d..e16debdf263 100644 --- a/tests/components/systemmonitor/conftest.py +++ b/tests/components/systemmonitor/conftest.py @@ -2,13 +2,13 @@ from __future__ import annotations -from collections.abc import Generator import socket from unittest.mock import AsyncMock, Mock, NonCallableMock, patch from psutil import NoSuchProcess, Process from psutil._common import sdiskpart, sdiskusage, shwtemp, snetio, snicaddr, sswap import pytest +from typing_extensions import Generator from homeassistant.components.systemmonitor.const import DOMAIN from homeassistant.components.systemmonitor.coordinator import VirtualMemory @@ -18,7 +18,7 @@ from tests.common import MockConfigEntry @pytest.fixture(autouse=True) -def mock_sys_platform() -> Generator[None, None, None]: +def mock_sys_platform() -> Generator[None]: """Mock sys platform to Linux.""" with patch("sys.platform", "linux"): yield @@ -42,7 +42,7 @@ class MockProcess(Process): @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Mock setup entry.""" with patch( "homeassistant.components.systemmonitor.async_setup_entry", From 5914ff0de80a5bf576f006c011a5274736df4732 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 6 Jun 2024 17:44:22 +0200 Subject: [PATCH 0305/1445] Improve type hints in apple_tv tests (#118980) --- tests/components/apple_tv/conftest.py | 27 ++-- tests/components/apple_tv/test_config_flow.py | 134 ++++++++++-------- 2 files changed, 86 insertions(+), 75 deletions(-) diff --git a/tests/components/apple_tv/conftest.py b/tests/components/apple_tv/conftest.py index b516cc5e71e..36061924db5 100644 --- a/tests/components/apple_tv/conftest.py +++ b/tests/components/apple_tv/conftest.py @@ -1,17 +1,18 @@ """Fixtures for component.""" -from unittest.mock import patch +from unittest.mock import AsyncMock, MagicMock, patch from pyatv import conf from pyatv.const import PairingRequirement, Protocol from pyatv.support import http import pytest +from typing_extensions import Generator from .common import MockPairingHandler, airplay_service, create_conf, mrp_service @pytest.fixture(autouse=True, name="mock_scan") -def mock_scan_fixture(): +def mock_scan_fixture() -> Generator[AsyncMock]: """Mock pyatv.scan.""" with patch("homeassistant.components.apple_tv.config_flow.scan") as mock_scan: @@ -29,7 +30,7 @@ def mock_scan_fixture(): @pytest.fixture(name="dmap_pin") -def dmap_pin_fixture(): +def dmap_pin_fixture() -> Generator[MagicMock]: """Mock pyatv.scan.""" with patch("homeassistant.components.apple_tv.config_flow.randrange") as mock_pin: mock_pin.side_effect = lambda start, stop: 1111 @@ -37,7 +38,7 @@ def dmap_pin_fixture(): @pytest.fixture -def pairing(): +def pairing() -> Generator[AsyncMock]: """Mock pyatv.scan.""" with patch("homeassistant.components.apple_tv.config_flow.pair") as mock_pair: @@ -54,7 +55,7 @@ def pairing(): @pytest.fixture -def pairing_mock(): +def pairing_mock() -> Generator[AsyncMock]: """Mock pyatv.scan.""" with patch("homeassistant.components.apple_tv.config_flow.pair") as mock_pair: @@ -75,7 +76,7 @@ def pairing_mock(): @pytest.fixture -def full_device(mock_scan, dmap_pin): +def full_device(mock_scan: AsyncMock, dmap_pin: MagicMock) -> AsyncMock: """Mock pyatv.scan.""" mock_scan.result.append( create_conf( @@ -96,7 +97,7 @@ def full_device(mock_scan, dmap_pin): @pytest.fixture -def mrp_device(mock_scan): +def mrp_device(mock_scan: AsyncMock) -> AsyncMock: """Mock pyatv.scan.""" mock_scan.result.extend( [ @@ -116,7 +117,7 @@ def mrp_device(mock_scan): @pytest.fixture -def airplay_with_disabled_mrp(mock_scan): +def airplay_with_disabled_mrp(mock_scan: AsyncMock) -> AsyncMock: """Mock pyatv.scan.""" mock_scan.result.append( create_conf( @@ -136,7 +137,7 @@ def airplay_with_disabled_mrp(mock_scan): @pytest.fixture -def dmap_device(mock_scan): +def dmap_device(mock_scan: AsyncMock) -> AsyncMock: """Mock pyatv.scan.""" mock_scan.result.append( create_conf( @@ -156,7 +157,7 @@ def dmap_device(mock_scan): @pytest.fixture -def dmap_device_with_credentials(mock_scan): +def dmap_device_with_credentials(mock_scan: AsyncMock) -> AsyncMock: """Mock pyatv.scan.""" mock_scan.result.append( create_conf( @@ -176,7 +177,7 @@ def dmap_device_with_credentials(mock_scan): @pytest.fixture -def airplay_device_with_password(mock_scan): +def airplay_device_with_password(mock_scan: AsyncMock) -> AsyncMock: """Mock pyatv.scan.""" mock_scan.result.append( create_conf( @@ -191,7 +192,9 @@ def airplay_device_with_password(mock_scan): @pytest.fixture -def dmap_with_requirement(mock_scan, pairing_requirement): +def dmap_with_requirement( + mock_scan: AsyncMock, pairing_requirement: PairingRequirement +) -> AsyncMock: """Mock pyatv.scan.""" mock_scan.result.append( create_conf( diff --git a/tests/components/apple_tv/test_config_flow.py b/tests/components/apple_tv/test_config_flow.py index e7bfa68bdaf..b8f49e7c8f5 100644 --- a/tests/components/apple_tv/test_config_flow.py +++ b/tests/components/apple_tv/test_config_flow.py @@ -1,11 +1,12 @@ """Test config flow.""" from ipaddress import IPv4Address, ip_address -from unittest.mock import ANY, Mock, patch +from unittest.mock import ANY, AsyncMock, MagicMock, Mock, patch from pyatv import exceptions from pyatv.const import PairingRequirement, Protocol import pytest +from typing_extensions import Generator from homeassistant import config_entries from homeassistant.components import zeroconf @@ -45,19 +46,19 @@ RAOP_SERVICE = zeroconf.ZeroconfServiceInfo( @pytest.fixture(autouse=True) -def zero_aggregation_time(): +def zero_aggregation_time() -> Generator[None]: """Prevent the aggregation time from delaying the tests.""" with patch.object(config_flow, "DISCOVERY_AGGREGATION_TIME", 0): yield @pytest.fixture(autouse=True) -def use_mocked_zeroconf(mock_async_zeroconf): +def use_mocked_zeroconf(mock_async_zeroconf: MagicMock) -> None: """Mock zeroconf in all tests.""" @pytest.fixture(autouse=True) -def mock_setup_entry(): +def mock_setup_entry() -> Generator[None]: """Mock setting up a config entry.""" with patch( "homeassistant.components.apple_tv.async_setup_entry", return_value=True @@ -68,7 +69,8 @@ def mock_setup_entry(): # User Flows -async def test_user_input_device_not_found(hass: HomeAssistant, mrp_device) -> None: +@pytest.mark.usefixtures("mrp_device") +async def test_user_input_device_not_found(hass: HomeAssistant) -> None: """Test when user specifies a non-existing device.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -85,7 +87,9 @@ async def test_user_input_device_not_found(hass: HomeAssistant, mrp_device) -> N assert result2["errors"] == {"base": "no_devices_found"} -async def test_user_input_unexpected_error(hass: HomeAssistant, mock_scan) -> None: +async def test_user_input_unexpected_error( + hass: HomeAssistant, mock_scan: AsyncMock +) -> None: """Test that unexpected error yields an error message.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -101,7 +105,8 @@ async def test_user_input_unexpected_error(hass: HomeAssistant, mock_scan) -> No assert result2["errors"] == {"base": "unknown"} -async def test_user_adds_full_device(hass: HomeAssistant, full_device, pairing) -> None: +@pytest.mark.usefixtures("full_device", "pairing") +async def test_user_adds_full_device(hass: HomeAssistant) -> None: """Test adding device with all services.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -149,9 +154,8 @@ async def test_user_adds_full_device(hass: HomeAssistant, full_device, pairing) } -async def test_user_adds_dmap_device( - hass: HomeAssistant, dmap_device, dmap_pin, pairing -) -> None: +@pytest.mark.usefixtures("dmap_device", "dmap_pin", "pairing") +async def test_user_adds_dmap_device(hass: HomeAssistant) -> None: """Test adding device with only DMAP service.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -183,8 +187,9 @@ async def test_user_adds_dmap_device( } +@pytest.mark.usefixtures("dmap_device", "dmap_pin") async def test_user_adds_dmap_device_failed( - hass: HomeAssistant, dmap_device, dmap_pin, pairing + hass: HomeAssistant, pairing: AsyncMock ) -> None: """Test adding DMAP device where remote device did not attempt to pair.""" pairing.always_fail = True @@ -205,9 +210,8 @@ async def test_user_adds_dmap_device_failed( assert result2["reason"] == "device_did_not_pair" -async def test_user_adds_device_with_ip_filter( - hass: HomeAssistant, dmap_device_with_credentials, mock_scan -) -> None: +@pytest.mark.usefixtures("dmap_device_with_credentials", "mock_scan") +async def test_user_adds_device_with_ip_filter(hass: HomeAssistant) -> None: """Test add device filtering by IP.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -225,9 +229,8 @@ async def test_user_adds_device_with_ip_filter( @pytest.mark.parametrize("pairing_requirement", [(PairingRequirement.NotNeeded)]) -async def test_user_pair_no_interaction( - hass: HomeAssistant, dmap_with_requirement, pairing_mock -) -> None: +@pytest.mark.usefixtures("dmap_with_requirement", "pairing_mock") +async def test_user_pair_no_interaction(hass: HomeAssistant) -> None: """Test pairing service without user interaction.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -251,7 +254,7 @@ async def test_user_pair_no_interaction( async def test_user_adds_device_by_ip_uses_unicast_scan( - hass: HomeAssistant, mock_scan + hass: HomeAssistant, mock_scan: AsyncMock ) -> None: """Test add device by IP-address, verify unicast scan is used.""" result = await hass.config_entries.flow.async_init( @@ -266,7 +269,8 @@ async def test_user_adds_device_by_ip_uses_unicast_scan( assert str(mock_scan.hosts[0]) == "127.0.0.1" -async def test_user_adds_existing_device(hass: HomeAssistant, mrp_device) -> None: +@pytest.mark.usefixtures("mrp_device") +async def test_user_adds_existing_device(hass: HomeAssistant) -> None: """Test that it is not possible to add existing device.""" MockConfigEntry(domain="apple_tv", unique_id="mrpid").add_to_hass(hass) @@ -282,8 +286,9 @@ async def test_user_adds_existing_device(hass: HomeAssistant, mrp_device) -> Non assert result2["errors"] == {"base": "already_configured"} +@pytest.mark.usefixtures("mrp_device") async def test_user_connection_failed( - hass: HomeAssistant, mrp_device, pairing_mock + hass: HomeAssistant, pairing_mock: AsyncMock ) -> None: """Test error message when connection to device fails.""" pairing_mock.begin.side_effect = exceptions.ConnectionFailedError @@ -310,8 +315,9 @@ async def test_user_connection_failed( assert result2["reason"] == "setup_failed" +@pytest.mark.usefixtures("mrp_device") async def test_user_start_pair_error_failed( - hass: HomeAssistant, mrp_device, pairing_mock + hass: HomeAssistant, pairing_mock: AsyncMock ) -> None: """Test initiating pairing fails.""" pairing_mock.begin.side_effect = exceptions.PairingError @@ -333,9 +339,8 @@ async def test_user_start_pair_error_failed( assert result2["reason"] == "invalid_auth" -async def test_user_pair_service_with_password( - hass: HomeAssistant, airplay_device_with_password, pairing_mock -) -> None: +@pytest.mark.usefixtures("airplay_device_with_password", "pairing_mock") +async def test_user_pair_service_with_password(hass: HomeAssistant) -> None: """Test pairing with service requiring a password (not supported).""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -362,9 +367,8 @@ async def test_user_pair_service_with_password( @pytest.mark.parametrize("pairing_requirement", [(PairingRequirement.Disabled)]) -async def test_user_pair_disabled_service( - hass: HomeAssistant, dmap_with_requirement, pairing_mock -) -> None: +@pytest.mark.usefixtures("dmap_with_requirement", "pairing_mock") +async def test_user_pair_disabled_service(hass: HomeAssistant) -> None: """Test pairing with disabled service (is ignored with message).""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -391,9 +395,8 @@ async def test_user_pair_disabled_service( @pytest.mark.parametrize("pairing_requirement", [(PairingRequirement.Unsupported)]) -async def test_user_pair_ignore_unsupported( - hass: HomeAssistant, dmap_with_requirement, pairing_mock -) -> None: +@pytest.mark.usefixtures("dmap_with_requirement", "pairing_mock") +async def test_user_pair_ignore_unsupported(hass: HomeAssistant) -> None: """Test pairing with disabled service (is ignored silently).""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -412,8 +415,9 @@ async def test_user_pair_ignore_unsupported( assert result["reason"] == "setup_failed" +@pytest.mark.usefixtures("mrp_device") async def test_user_pair_invalid_pin( - hass: HomeAssistant, mrp_device, pairing_mock + hass: HomeAssistant, pairing_mock: AsyncMock ) -> None: """Test pairing with invalid pin.""" pairing_mock.finish.side_effect = exceptions.PairingError @@ -440,8 +444,9 @@ async def test_user_pair_invalid_pin( assert result2["errors"] == {"base": "invalid_auth"} +@pytest.mark.usefixtures("mrp_device") async def test_user_pair_unexpected_error( - hass: HomeAssistant, mrp_device, pairing_mock + hass: HomeAssistant, pairing_mock: AsyncMock ) -> None: """Test unexpected error when entering PIN code.""" @@ -468,8 +473,9 @@ async def test_user_pair_unexpected_error( assert result2["errors"] == {"base": "unknown"} +@pytest.mark.usefixtures("mrp_device") async def test_user_pair_backoff_error( - hass: HomeAssistant, mrp_device, pairing_mock + hass: HomeAssistant, pairing_mock: AsyncMock ) -> None: """Test that backoff error is displayed in case device requests it.""" pairing_mock.begin.side_effect = exceptions.BackOffError @@ -491,8 +497,9 @@ async def test_user_pair_backoff_error( assert result2["reason"] == "backoff" +@pytest.mark.usefixtures("mrp_device") async def test_user_pair_begin_unexpected_error( - hass: HomeAssistant, mrp_device, pairing_mock + hass: HomeAssistant, pairing_mock: AsyncMock ) -> None: """Test unexpected error during start of pairing.""" pairing_mock.begin.side_effect = Exception @@ -514,9 +521,8 @@ async def test_user_pair_begin_unexpected_error( assert result2["reason"] == "unknown" -async def test_ignores_disabled_service( - hass: HomeAssistant, airplay_with_disabled_mrp, pairing -) -> None: +@pytest.mark.usefixtures("airplay_with_disabled_mrp", "pairing") +async def test_ignores_disabled_service(hass: HomeAssistant) -> None: """Test adding device with only DMAP service.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -573,9 +579,8 @@ async def test_zeroconf_unsupported_service_aborts(hass: HomeAssistant) -> None: assert result["reason"] == "unknown" -async def test_zeroconf_add_mrp_device( - hass: HomeAssistant, mrp_device, pairing -) -> None: +@pytest.mark.usefixtures("mrp_device", "pairing") +async def test_zeroconf_add_mrp_device(hass: HomeAssistant) -> None: """Test add MRP device discovered by zeroconf.""" unrelated_result = await hass.config_entries.flow.async_init( DOMAIN, @@ -630,9 +635,8 @@ async def test_zeroconf_add_mrp_device( } -async def test_zeroconf_add_dmap_device( - hass: HomeAssistant, dmap_device, dmap_pin, pairing -) -> None: +@pytest.mark.usefixtures("dmap_device", "dmap_pin", "pairing") +async def test_zeroconf_add_dmap_device(hass: HomeAssistant) -> None: """Test add DMAP device discovered by zeroconf.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=DMAP_SERVICE @@ -660,7 +664,7 @@ async def test_zeroconf_add_dmap_device( } -async def test_zeroconf_ip_change(hass: HomeAssistant, mock_scan) -> None: +async def test_zeroconf_ip_change(hass: HomeAssistant, mock_scan: AsyncMock) -> None: """Test that the config entry gets updated when the ip changes and reloads.""" entry = MockConfigEntry( domain="apple_tv", unique_id="mrpid", data={CONF_ADDRESS: "127.0.0.2"} @@ -694,7 +698,7 @@ async def test_zeroconf_ip_change(hass: HomeAssistant, mock_scan) -> None: async def test_zeroconf_ip_change_after_ip_conflict_with_ignored_entry( - hass: HomeAssistant, mock_scan + hass: HomeAssistant, mock_scan: AsyncMock ) -> None: """Test that the config entry gets updated when the ip changes and reloads.""" entry = MockConfigEntry( @@ -732,7 +736,7 @@ async def test_zeroconf_ip_change_after_ip_conflict_with_ignored_entry( async def test_zeroconf_ip_change_via_secondary_identifier( - hass: HomeAssistant, mock_scan + hass: HomeAssistant, mock_scan: AsyncMock ) -> None: """Test that the config entry gets updated when the ip changes and reloads. @@ -774,7 +778,7 @@ async def test_zeroconf_ip_change_via_secondary_identifier( async def test_zeroconf_updates_identifiers_for_ignored_entries( - hass: HomeAssistant, mock_scan + hass: HomeAssistant, mock_scan: AsyncMock ) -> None: """Test that an ignored config entry gets updated when the ip changes. @@ -818,7 +822,8 @@ async def test_zeroconf_updates_identifiers_for_ignored_entries( assert set(entry.data[CONF_IDENTIFIERS]) == {"airplayid", "mrpid"} -async def test_zeroconf_add_existing_aborts(hass: HomeAssistant, dmap_device) -> None: +@pytest.mark.usefixtures("dmap_device") +async def test_zeroconf_add_existing_aborts(hass: HomeAssistant) -> None: """Test start new zeroconf flow while existing flow is active aborts.""" await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=DMAP_SERVICE @@ -831,9 +836,8 @@ async def test_zeroconf_add_existing_aborts(hass: HomeAssistant, dmap_device) -> assert result["reason"] == "already_in_progress" -async def test_zeroconf_add_but_device_not_found( - hass: HomeAssistant, mock_scan -) -> None: +@pytest.mark.usefixtures("mock_scan") +async def test_zeroconf_add_but_device_not_found(hass: HomeAssistant) -> None: """Test add device which is not found with another scan.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=DMAP_SERVICE @@ -842,7 +846,8 @@ async def test_zeroconf_add_but_device_not_found( assert result["reason"] == "no_devices_found" -async def test_zeroconf_add_existing_device(hass: HomeAssistant, dmap_device) -> None: +@pytest.mark.usefixtures("dmap_device") +async def test_zeroconf_add_existing_device(hass: HomeAssistant) -> None: """Test add already existing device from zeroconf.""" MockConfigEntry(domain="apple_tv", unique_id="dmapid").add_to_hass(hass) @@ -853,7 +858,9 @@ async def test_zeroconf_add_existing_device(hass: HomeAssistant, dmap_device) -> assert result["reason"] == "already_configured" -async def test_zeroconf_unexpected_error(hass: HomeAssistant, mock_scan) -> None: +async def test_zeroconf_unexpected_error( + hass: HomeAssistant, mock_scan: AsyncMock +) -> None: """Test unexpected error aborts in zeroconf.""" mock_scan.side_effect = Exception @@ -865,7 +872,7 @@ async def test_zeroconf_unexpected_error(hass: HomeAssistant, mock_scan) -> None async def test_zeroconf_abort_if_other_in_progress( - hass: HomeAssistant, mock_scan + hass: HomeAssistant, mock_scan: AsyncMock ) -> None: """Test discovering unsupported zeroconf service.""" mock_scan.result = [ @@ -912,8 +919,9 @@ async def test_zeroconf_abort_if_other_in_progress( assert result2["reason"] == "already_in_progress" +@pytest.mark.usefixtures("pairing", "mock_zeroconf") async def test_zeroconf_missing_device_during_protocol_resolve( - hass: HomeAssistant, mock_scan, pairing, mock_zeroconf: None + hass: HomeAssistant, mock_scan: AsyncMock ) -> None: """Test discovery after service been added to existing flow with missing device.""" mock_scan.result = [ @@ -970,8 +978,9 @@ async def test_zeroconf_missing_device_during_protocol_resolve( assert result2["reason"] == "device_not_found" +@pytest.mark.usefixtures("pairing", "mock_zeroconf") async def test_zeroconf_additional_protocol_resolve_failure( - hass: HomeAssistant, mock_scan, pairing, mock_zeroconf: None + hass: HomeAssistant, mock_scan: AsyncMock ) -> None: """Test discovery with missing service.""" mock_scan.result = [ @@ -1030,8 +1039,9 @@ async def test_zeroconf_additional_protocol_resolve_failure( assert result2["reason"] == "inconsistent_device" +@pytest.mark.usefixtures("pairing", "mock_zeroconf") async def test_zeroconf_pair_additionally_found_protocols( - hass: HomeAssistant, mock_scan, pairing, mock_zeroconf: None + hass: HomeAssistant, mock_scan: AsyncMock ) -> None: """Test discovered protocols are merged to original flow.""" mock_scan.result = [ @@ -1132,9 +1142,8 @@ async def test_zeroconf_pair_additionally_found_protocols( assert result5["type"] is FlowResultType.CREATE_ENTRY -async def test_zeroconf_mismatch( - hass: HomeAssistant, mock_scan, pairing, mock_zeroconf: None -) -> None: +@pytest.mark.usefixtures("pairing", "mock_zeroconf") +async def test_zeroconf_mismatch(hass: HomeAssistant, mock_scan: AsyncMock) -> None: """Test the technically possible case where a protocol has no service. This could happen in case of mDNS issues. @@ -1172,9 +1181,8 @@ async def test_zeroconf_mismatch( # Re-configuration -async def test_reconfigure_update_credentials( - hass: HomeAssistant, mrp_device, pairing -) -> None: +@pytest.mark.usefixtures("mrp_device", "pairing") +async def test_reconfigure_update_credentials(hass: HomeAssistant) -> None: """Test that reconfigure flow updates config entry.""" config_entry = MockConfigEntry( domain="apple_tv", unique_id="mrpid", data={"identifiers": ["mrpid"]} From ffea72f866fc0441c74cb8ed45d2250d005ccb8e Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Thu, 6 Jun 2024 18:25:24 +0200 Subject: [PATCH 0306/1445] Increment ci cache version (#118998) --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 8c1b11e13ff..f2ffd03f1a8 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -33,7 +33,7 @@ on: type: boolean env: - CACHE_VERSION: 8 + CACHE_VERSION: 9 UV_CACHE_VERSION: 1 MYPY_CACHE_VERSION: 8 HA_SHORT_VERSION: "2024.7" From 62b1bde0e8205635102b0d379f27953b27ab015d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Jun 2024 11:46:44 -0500 Subject: [PATCH 0307/1445] Only entity verify state writable once after success unless hass is missing (#118896) --- homeassistant/helpers/entity.py | 12 ++++++++++-- tests/helpers/test_entity.py | 10 +++++----- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 9a2bb4b6fca..aab6fa9f59b 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -473,6 +473,10 @@ class Entity( # Protect for multiple updates _update_staged = False + # _verified_state_writable is set to True if the entity has been verified + # to be writable. This is used to avoid repeated checks. + _verified_state_writable = False + # Process updates in parallel parallel_updates: asyncio.Semaphore | None = None @@ -986,16 +990,20 @@ class Entity( f"No entity id specified for entity {self.name}" ) + self._verified_state_writable = True + @callback def _async_write_ha_state_from_call_soon_threadsafe(self) -> None: """Write the state to the state machine from the event loop thread.""" - self._async_verify_state_writable() + if not self.hass or not self._verified_state_writable: + self._async_verify_state_writable() self._async_write_ha_state() @callback def async_write_ha_state(self) -> None: """Write the state to the state machine.""" - self._async_verify_state_writable() + if not self.hass or not self._verified_state_writable: + self._async_verify_state_writable() if self.hass.loop_thread_id != threading.get_ident(): report_non_thread_safe_operation("async_write_ha_state") self._async_write_ha_state() diff --git a/tests/helpers/test_entity.py b/tests/helpers/test_entity.py index c8da7a118aa..d105ffad791 100644 --- a/tests/helpers/test_entity.py +++ b/tests/helpers/test_entity.py @@ -1662,11 +1662,6 @@ async def test_warn_no_platform( ent.entity_id = "hello.world" error_message = "does not have a platform" - # No warning if the entity has a platform - caplog.clear() - ent.async_write_ha_state() - assert error_message not in caplog.text - # Without a platform, it should trigger the warning ent.platform = None caplog.clear() @@ -1678,6 +1673,11 @@ async def test_warn_no_platform( ent.async_write_ha_state() assert error_message not in caplog.text + # No warning if the entity has a platform + caplog.clear() + ent.async_write_ha_state() + assert error_message not in caplog.text + async def test_invalid_state( hass: HomeAssistant, caplog: pytest.LogCaptureFixture From d40c940c201b53d4d5a9ee0b76712896fda69f60 Mon Sep 17 00:00:00 2001 From: David Bonnes Date: Thu, 6 Jun 2024 18:02:50 +0100 Subject: [PATCH 0308/1445] Move evohome's API broker to the coordinator module (#118565) * move Broker to coordinator module * mypy tweak * mypy --- homeassistant/components/evohome/__init__.py | 167 +-------------- .../components/evohome/coordinator.py | 191 ++++++++++++++++++ 2 files changed, 196 insertions(+), 162 deletions(-) create mode 100644 homeassistant/components/evohome/coordinator.py diff --git a/homeassistant/components/evohome/__init__.py b/homeassistant/components/evohome/__init__.py index 51b4703ff2c..782e4c4e674 100644 --- a/homeassistant/components/evohome/__init__.py +++ b/homeassistant/components/evohome/__init__.py @@ -6,13 +6,12 @@ others. from __future__ import annotations -from collections.abc import Awaitable from datetime import datetime, timedelta, timezone import logging from typing import Any, Final import evohomeasync as ev1 -from evohomeasync.schema import SZ_ID, SZ_SESSION_ID, SZ_TEMP +from evohomeasync.schema import SZ_SESSION_ID import evohomeasync2 as evo from evohomeasync2.schema.const import ( SZ_ALLOWED_SYSTEM_MODES, @@ -51,14 +50,13 @@ from homeassistant.helpers.dispatcher import ( async_dispatcher_send, ) from homeassistant.helpers.entity import Entity -from homeassistant.helpers.event import async_call_later, async_track_time_interval +from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.service import verify_domain_control from homeassistant.helpers.storage import Store from homeassistant.helpers.typing import ConfigType import homeassistant.util.dt as dt_util from .const import ( - ACCESS_TOKEN, ACCESS_TOKEN_EXPIRES, ATTR_DURATION_DAYS, ATTR_DURATION_HOURS, @@ -68,21 +66,19 @@ from .const import ( CONF_LOCATION_IDX, DOMAIN, GWS, - REFRESH_TOKEN, SCAN_INTERVAL_DEFAULT, SCAN_INTERVAL_MINIMUM, STORAGE_KEY, STORAGE_VER, TCS, USER_DATA, - UTC_OFFSET, EvoService, ) +from .coordinator import EvoBroker from .helpers import ( convert_dict, convert_until, dt_aware_to_naive, - dt_local_to_aware, handle_evo_exception, ) @@ -161,6 +157,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: finally: config[DOMAIN][CONF_PASSWORD] = "REDACTED" + assert isinstance(client_v2.installation_info, list) # mypy + loc_idx = config[DOMAIN][CONF_LOCATION_IDX] try: loc_config = client_v2.installation_info[loc_idx] @@ -342,161 +340,6 @@ def setup_service_functions(hass: HomeAssistant, broker: EvoBroker) -> None: ) -class EvoBroker: - """Container for evohome client and data.""" - - def __init__( - self, - hass: HomeAssistant, - client: evo.EvohomeClient, - client_v1: ev1.EvohomeClient | None, - store: Store[dict[str, Any]], - params: ConfigType, - ) -> None: - """Initialize the evohome client and its data structure.""" - self.hass = hass - self.client = client - self.client_v1 = client_v1 - self._store = store - self.params = params - - loc_idx = params[CONF_LOCATION_IDX] - self._location: evo.Location = client.locations[loc_idx] - - self.config = client.installation_info[loc_idx][GWS][0][TCS][0] - self.tcs: evo.ControlSystem = self._location._gateways[0]._control_systems[0] # noqa: SLF001 - self.loc_utc_offset = timedelta(minutes=self._location.timeZone[UTC_OFFSET]) - self.temps: dict[str, float | None] = {} - - async def save_auth_tokens(self) -> None: - """Save access tokens and session IDs to the store for later use.""" - # evohomeasync2 uses naive/local datetimes - access_token_expires = dt_local_to_aware( - self.client.access_token_expires # type: ignore[arg-type] - ) - - app_storage: dict[str, Any] = { - CONF_USERNAME: self.client.username, - REFRESH_TOKEN: self.client.refresh_token, - ACCESS_TOKEN: self.client.access_token, - ACCESS_TOKEN_EXPIRES: access_token_expires.isoformat(), - } - - if self.client_v1: - app_storage[USER_DATA] = { - SZ_SESSION_ID: self.client_v1.broker.session_id, - } # this is the schema for STORAGE_VER == 1 - else: - app_storage[USER_DATA] = {} - - await self._store.async_save(app_storage) - - async def call_client_api( - self, - client_api: Awaitable[dict[str, Any] | None], - update_state: bool = True, - ) -> dict[str, Any] | None: - """Call a client API and update the broker state if required.""" - - try: - result = await client_api - except evo.RequestFailed as err: - handle_evo_exception(err) - return None - - if update_state: # wait a moment for system to quiesce before updating state - async_call_later(self.hass, 1, self._update_v2_api_state) - - return result - - async def _update_v1_api_temps(self) -> None: - """Get the latest high-precision temperatures of the default Location.""" - - assert self.client_v1 is not None # mypy check - - def get_session_id(client_v1: ev1.EvohomeClient) -> str | None: - user_data = client_v1.user_data if client_v1 else None - return user_data.get(SZ_SESSION_ID) if user_data else None # type: ignore[return-value] - - session_id = get_session_id(self.client_v1) - - try: - temps = await self.client_v1.get_temperatures() - - except ev1.InvalidSchema as err: - _LOGGER.warning( - ( - "Unable to obtain high-precision temperatures. " - "It appears the JSON schema is not as expected, " - "so the high-precision feature will be disabled until next restart." - "Message is: %s" - ), - err, - ) - self.client_v1 = None - - except ev1.RequestFailed as err: - _LOGGER.warning( - ( - "Unable to obtain the latest high-precision temperatures. " - "Check your network and the vendor's service status page. " - "Proceeding without high-precision temperatures for now. " - "Message is: %s" - ), - err, - ) - self.temps = {} # high-precision temps now considered stale - - except Exception: - self.temps = {} # high-precision temps now considered stale - raise - - else: - if str(self.client_v1.location_id) != self._location.locationId: - _LOGGER.warning( - "The v2 API's configured location doesn't match " - "the v1 API's default location (there is more than one location), " - "so the high-precision feature will be disabled until next restart" - ) - self.client_v1 = None - else: - self.temps = {str(i[SZ_ID]): i[SZ_TEMP] for i in temps} - - finally: - if self.client_v1 and session_id != self.client_v1.broker.session_id: - await self.save_auth_tokens() - - _LOGGER.debug("Temperatures = %s", self.temps) - - async def _update_v2_api_state(self, *args: Any) -> None: - """Get the latest modes, temperatures, setpoints of a Location.""" - - access_token = self.client.access_token # maybe receive a new token? - - try: - status = await self._location.refresh_status() - except evo.RequestFailed as err: - handle_evo_exception(err) - else: - async_dispatcher_send(self.hass, DOMAIN) - _LOGGER.debug("Status = %s", status) - finally: - if access_token != self.client.access_token: - await self.save_auth_tokens() - - async def async_update(self, *args: Any) -> None: - """Get the latest state data of an entire Honeywell TCC Location. - - This includes state data for a Controller and all its child devices, such as the - operating mode of the Controller and the current temp of its children (e.g. - Zones, DHW controller). - """ - await self._update_v2_api_state() - - if self.client_v1: - await self._update_v1_api_temps() - - class EvoDevice(Entity): """Base for any evohome device. diff --git a/homeassistant/components/evohome/coordinator.py b/homeassistant/components/evohome/coordinator.py new file mode 100644 index 00000000000..6b54c5f4640 --- /dev/null +++ b/homeassistant/components/evohome/coordinator.py @@ -0,0 +1,191 @@ +"""Support for (EMEA/EU-based) Honeywell TCC systems.""" + +from __future__ import annotations + +from collections.abc import Awaitable +from datetime import timedelta +import logging +from typing import Any + +import evohomeasync as ev1 +from evohomeasync.schema import SZ_ID, SZ_SESSION_ID, SZ_TEMP +import evohomeasync2 as evo + +from homeassistant.const import CONF_USERNAME +from homeassistant.core import HomeAssistant +from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.event import async_call_later +from homeassistant.helpers.storage import Store +from homeassistant.helpers.typing import ConfigType + +from .const import ( + ACCESS_TOKEN, + ACCESS_TOKEN_EXPIRES, + CONF_LOCATION_IDX, + DOMAIN, + GWS, + REFRESH_TOKEN, + TCS, + USER_DATA, + UTC_OFFSET, +) +from .helpers import dt_local_to_aware, handle_evo_exception + +_LOGGER = logging.getLogger(__name__.rpartition(".")[0]) + + +class EvoBroker: + """Container for evohome client and data.""" + + def __init__( + self, + hass: HomeAssistant, + client: evo.EvohomeClient, + client_v1: ev1.EvohomeClient | None, + store: Store[dict[str, Any]], + params: ConfigType, + ) -> None: + """Initialize the evohome client and its data structure.""" + self.hass = hass + self.client = client + self.client_v1 = client_v1 + self._store = store + self.params = params + + loc_idx = params[CONF_LOCATION_IDX] + self._location: evo.Location = client.locations[loc_idx] + + assert isinstance(client.installation_info, list) # mypy + + self.config = client.installation_info[loc_idx][GWS][0][TCS][0] + self.tcs: evo.ControlSystem = self._location._gateways[0]._control_systems[0] # noqa: SLF001 + self.loc_utc_offset = timedelta(minutes=self._location.timeZone[UTC_OFFSET]) + self.temps: dict[str, float | None] = {} + + async def save_auth_tokens(self) -> None: + """Save access tokens and session IDs to the store for later use.""" + # evohomeasync2 uses naive/local datetimes + access_token_expires = dt_local_to_aware( + self.client.access_token_expires # type: ignore[arg-type] + ) + + app_storage: dict[str, Any] = { + CONF_USERNAME: self.client.username, + REFRESH_TOKEN: self.client.refresh_token, + ACCESS_TOKEN: self.client.access_token, + ACCESS_TOKEN_EXPIRES: access_token_expires.isoformat(), + } + + if self.client_v1: + app_storage[USER_DATA] = { + SZ_SESSION_ID: self.client_v1.broker.session_id, + } # this is the schema for STORAGE_VER == 1 + else: + app_storage[USER_DATA] = {} + + await self._store.async_save(app_storage) + + async def call_client_api( + self, + client_api: Awaitable[dict[str, Any] | None], + update_state: bool = True, + ) -> dict[str, Any] | None: + """Call a client API and update the broker state if required.""" + + try: + result = await client_api + except evo.RequestFailed as err: + handle_evo_exception(err) + return None + + if update_state: # wait a moment for system to quiesce before updating state + async_call_later(self.hass, 1, self._update_v2_api_state) + + return result + + async def _update_v1_api_temps(self) -> None: + """Get the latest high-precision temperatures of the default Location.""" + + assert self.client_v1 is not None # mypy check + + def get_session_id(client_v1: ev1.EvohomeClient) -> str | None: + user_data = client_v1.user_data if client_v1 else None + return user_data.get(SZ_SESSION_ID) if user_data else None # type: ignore[return-value] + + session_id = get_session_id(self.client_v1) + + try: + temps = await self.client_v1.get_temperatures() + + except ev1.InvalidSchema as err: + _LOGGER.warning( + ( + "Unable to obtain high-precision temperatures. " + "It appears the JSON schema is not as expected, " + "so the high-precision feature will be disabled until next restart." + "Message is: %s" + ), + err, + ) + self.client_v1 = None + + except ev1.RequestFailed as err: + _LOGGER.warning( + ( + "Unable to obtain the latest high-precision temperatures. " + "Check your network and the vendor's service status page. " + "Proceeding without high-precision temperatures for now. " + "Message is: %s" + ), + err, + ) + self.temps = {} # high-precision temps now considered stale + + except Exception: + self.temps = {} # high-precision temps now considered stale + raise + + else: + if str(self.client_v1.location_id) != self._location.locationId: + _LOGGER.warning( + "The v2 API's configured location doesn't match " + "the v1 API's default location (there is more than one location), " + "so the high-precision feature will be disabled until next restart" + ) + self.client_v1 = None + else: + self.temps = {str(i[SZ_ID]): i[SZ_TEMP] for i in temps} + + finally: + if self.client_v1 and session_id != self.client_v1.broker.session_id: + await self.save_auth_tokens() + + _LOGGER.debug("Temperatures = %s", self.temps) + + async def _update_v2_api_state(self, *args: Any) -> None: + """Get the latest modes, temperatures, setpoints of a Location.""" + + access_token = self.client.access_token # maybe receive a new token? + + try: + status = await self._location.refresh_status() + except evo.RequestFailed as err: + handle_evo_exception(err) + else: + async_dispatcher_send(self.hass, DOMAIN) + _LOGGER.debug("Status = %s", status) + finally: + if access_token != self.client.access_token: + await self.save_auth_tokens() + + async def async_update(self, *args: Any) -> None: + """Get the latest state data of an entire Honeywell TCC Location. + + This includes state data for a Controller and all its child devices, such as the + operating mode of the Controller and the current temp of its children (e.g. + Zones, DHW controller). + """ + await self._update_v2_api_state() + + if self.client_v1: + await self._update_v1_api_temps() From 99b85e16d1b28500b3d2cc1f070087768d467214 Mon Sep 17 00:00:00 2001 From: "Mr. Bubbles" Date: Thu, 6 Jun 2024 20:03:58 +0200 Subject: [PATCH 0309/1445] Set username as entry title in Bring integration (#118974) Set username as entry title --- homeassistant/components/bring/config_flow.py | 4 ++-- tests/components/bring/conftest.py | 2 +- tests/components/bring/test_config_flow.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/bring/config_flow.py b/homeassistant/components/bring/config_flow.py index 1f730abb432..756b2312e88 100644 --- a/homeassistant/components/bring/config_flow.py +++ b/homeassistant/components/bring/config_flow.py @@ -53,7 +53,7 @@ class BringConfigFlow(ConfigFlow, domain=DOMAIN): bring = Bring(session, user_input[CONF_EMAIL], user_input[CONF_PASSWORD]) try: - await bring.login() + info = await bring.login() await bring.load_lists() except BringRequestException: errors["base"] = "cannot_connect" @@ -66,7 +66,7 @@ class BringConfigFlow(ConfigFlow, domain=DOMAIN): await self.async_set_unique_id(bring.uuid) self._abort_if_unique_id_configured() return self.async_create_entry( - title=user_input[CONF_EMAIL], data=user_input + title=info["name"] or user_input[CONF_EMAIL], data=user_input ) return self.async_show_form( diff --git a/tests/components/bring/conftest.py b/tests/components/bring/conftest.py index eef333e07ca..0760bdd296a 100644 --- a/tests/components/bring/conftest.py +++ b/tests/components/bring/conftest.py @@ -40,7 +40,7 @@ def mock_bring_client() -> Generator[AsyncMock]: ): client = mock_client.return_value client.uuid = UUID - client.login.return_value = True + client.login.return_value = {"name": "Bring"} client.load_lists.return_value = {"lists": []} yield client diff --git a/tests/components/bring/test_config_flow.py b/tests/components/bring/test_config_flow.py index 351ba533101..86fdbc1853b 100644 --- a/tests/components/bring/test_config_flow.py +++ b/tests/components/bring/test_config_flow.py @@ -45,7 +45,7 @@ async def test_form( await hass.async_block_till_done() assert result["type"] is FlowResultType.CREATE_ENTRY - assert result["title"] == MOCK_DATA_STEP["email"] + assert result["title"] == "Bring" assert result["data"] == MOCK_DATA_STEP assert len(mock_setup_entry.mock_calls) == 1 @@ -87,7 +87,7 @@ async def test_flow_user_init_data_unknown_error_and_recover( ) assert result["type"] is FlowResultType.CREATE_ENTRY - assert result["result"].title == MOCK_DATA_STEP["email"] + assert result["result"].title == "Bring" assert result["data"] == MOCK_DATA_STEP From 333ac5690442888062d08bc25d86548ebf76378b Mon Sep 17 00:00:00 2001 From: Steven B <51370195+sdb9696@users.noreply.github.com> Date: Thu, 6 Jun 2024 19:13:19 +0100 Subject: [PATCH 0310/1445] Fully mock the ring_doorbell api and remove requests_mock (#113140) * Fully mock ring_doorbell library * Add comments and docstrings * Simplify devices_mocks and fake RingDevices * Update post review * Consolidate device filtering in conftest * Fix ruff lambda assignment failure * Fix ruff check fail * Update post review --- tests/components/ring/conftest.py | 156 ++++--- tests/components/ring/device_mocks.py | 179 ++++++++ .../ring/fixtures/chime_devices.json | 35 -- tests/components/ring/fixtures/devices.json | 10 +- .../ring/fixtures/devices_updated.json | 382 ------------------ .../components/ring/fixtures/ding_active.json | 7 + ...h_attrs.json => doorbot_health_attrs.json} | 0 .../ring/fixtures/doorbot_history.json | 10 + .../fixtures/doorbot_siren_on_response.json | 6 - tests/components/ring/fixtures/groups.json | 24 -- tests/components/ring/fixtures/oauth.json | 8 - tests/components/ring/fixtures/session.json | 38 -- tests/components/ring/test_binary_sensor.py | 24 +- tests/components/ring/test_button.py | 20 +- tests/components/ring/test_camera.py | 50 ++- tests/components/ring/test_config_flow.py | 2 +- tests/components/ring/test_diagnostics.py | 3 +- tests/components/ring/test_init.py | 204 +++++----- tests/components/ring/test_light.py | 55 +-- tests/components/ring/test_sensor.py | 138 +++++-- tests/components/ring/test_siren.py | 88 ++-- tests/components/ring/test_switch.py | 54 +-- 22 files changed, 570 insertions(+), 923 deletions(-) create mode 100644 tests/components/ring/device_mocks.py delete mode 100644 tests/components/ring/fixtures/chime_devices.json delete mode 100644 tests/components/ring/fixtures/devices_updated.json rename tests/components/ring/fixtures/{doorboot_health_attrs.json => doorbot_health_attrs.json} (100%) delete mode 100644 tests/components/ring/fixtures/doorbot_siren_on_response.json delete mode 100644 tests/components/ring/fixtures/groups.json delete mode 100644 tests/components/ring/fixtures/oauth.json delete mode 100644 tests/components/ring/fixtures/session.json diff --git a/tests/components/ring/conftest.py b/tests/components/ring/conftest.py index 82526d87b22..58e77184f55 100644 --- a/tests/components/ring/conftest.py +++ b/tests/components/ring/conftest.py @@ -1,17 +1,19 @@ """Configuration for Ring tests.""" -import re -from unittest.mock import AsyncMock, Mock, patch +from itertools import chain +from unittest.mock import AsyncMock, Mock, create_autospec, patch import pytest -import requests_mock +import ring_doorbell from typing_extensions import Generator from homeassistant.components.ring import DOMAIN from homeassistant.const import CONF_USERNAME from homeassistant.core import HomeAssistant -from tests.common import MockConfigEntry, load_fixture +from .device_mocks import get_active_alerts, get_devices_data, get_mock_devices + +from tests.common import MockConfigEntry from tests.components.light.conftest import mock_light_profiles # noqa: F401 @@ -36,6 +38,67 @@ def mock_ring_auth(): yield mock_ring_auth.return_value +@pytest.fixture +def mock_ring_devices(): + """Mock Ring devices.""" + + devices = get_mock_devices() + device_list = list(chain.from_iterable(devices.values())) + + def filter_devices(device_api_ai: int, device_family: set | None = None): + return next( + iter( + [ + device + for device in device_list + if device.id == device_api_ai + and (not device_family or device.family in device_family) + ] + ) + ) + + class FakeRingDevices: + """Class fakes the RingDevices class.""" + + all_devices = device_list + video_devices = ( + devices["stickup_cams"] + + devices["doorbots"] + + devices["authorized_doorbots"] + ) + stickup_cams = devices["stickup_cams"] + other = devices["other"] + chimes = devices["chimes"] + + def get_device(self, id): + return filter_devices(id) + + def get_video_device(self, id): + return filter_devices( + id, {"stickup_cams", "doorbots", "authorized_doorbots"} + ) + + def get_stickup_cam(self, id): + return filter_devices(id, {"stickup_cams"}) + + def get_other(self, id): + return filter_devices(id, {"other"}) + + return FakeRingDevices() + + +@pytest.fixture +def mock_ring_client(mock_ring_auth, mock_ring_devices): + """Mock ring client api.""" + mock_client = create_autospec(ring_doorbell.Ring) + mock_client.return_value.devices_data = get_devices_data() + mock_client.return_value.devices.return_value = mock_ring_devices + mock_client.return_value.active_alerts.side_effect = get_active_alerts + + with patch("homeassistant.components.ring.Ring", new=mock_client): + yield mock_client.return_value + + @pytest.fixture def mock_config_entry() -> MockConfigEntry: """Mock ConfigEntry.""" @@ -55,91 +118,10 @@ async def mock_added_config_entry( hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_ring_auth: Mock, + mock_ring_client: Mock, ) -> MockConfigEntry: """Mock ConfigEntry that's been added to HA.""" mock_config_entry.add_to_hass(hass) - await hass.config_entries.async_setup(mock_config_entry.entry_id) + assert await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() - assert DOMAIN in hass.config_entries.async_domains() return mock_config_entry - - -@pytest.fixture(name="requests_mock") -def requests_mock_fixture(): - """Fixture to provide a requests mocker.""" - with requests_mock.mock() as mock: - # Note all devices have an id of 987652, but a different device_id. - # the device_id is used as our unique_id, but the id is what is sent - # to the APIs, which is why every mock uses that id. - - # Mocks the response for authenticating - mock.post( - "https://oauth.ring.com/oauth/token", - text=load_fixture("oauth.json", "ring"), - ) - # Mocks the response for getting the login session - mock.post( - "https://api.ring.com/clients_api/session", - text=load_fixture("session.json", "ring"), - ) - # Mocks the response for getting all the devices - mock.get( - "https://api.ring.com/clients_api/ring_devices", - text=load_fixture("devices.json", "ring"), - ) - mock.get( - "https://api.ring.com/clients_api/dings/active", - text=load_fixture("ding_active.json", "ring"), - ) - # Mocks the response for getting the history of a device - mock.get( - re.compile( - r"https:\/\/api\.ring\.com\/clients_api\/doorbots\/\d+\/history" - ), - text=load_fixture("doorbot_history.json", "ring"), - ) - # Mocks the response for getting the health of a device - mock.get( - re.compile(r"https:\/\/api\.ring\.com\/clients_api\/doorbots\/\d+\/health"), - text=load_fixture("doorboot_health_attrs.json", "ring"), - ) - # Mocks the response for getting a chimes health - mock.get( - re.compile(r"https:\/\/api\.ring\.com\/clients_api\/chimes\/\d+\/health"), - text=load_fixture("chime_health_attrs.json", "ring"), - ) - mock.get( - re.compile( - r"https:\/\/api\.ring\.com\/clients_api\/dings\/\d+\/share/play" - ), - status_code=200, - json={"url": "http://127.0.0.1/foo"}, - ) - mock.get( - "https://api.ring.com/groups/v1/locations/mock-location-id/groups", - text=load_fixture("groups.json", "ring"), - ) - # Mocks the response for getting the history of the intercom - mock.get( - "https://api.ring.com/clients_api/doorbots/185036587/history", - text=load_fixture("intercom_history.json", "ring"), - ) - # Mocks the response for setting properties in settings (i.e. motion_detection) - mock.patch( - re.compile( - r"https:\/\/api\.ring\.com\/devices\/v1\/devices\/\d+\/settings" - ), - text="ok", - ) - # Mocks the open door command for intercom devices - mock.put( - "https://api.ring.com/commands/v1/devices/185036587/device_rpc", - status_code=200, - text="{}", - ) - # Mocks the response for getting the history of the intercom - mock.get( - "https://api.ring.com/clients_api/doorbots/185036587/history", - text=load_fixture("intercom_history.json", "ring"), - ) - yield mock diff --git a/tests/components/ring/device_mocks.py b/tests/components/ring/device_mocks.py new file mode 100644 index 00000000000..f43370c918d --- /dev/null +++ b/tests/components/ring/device_mocks.py @@ -0,0 +1,179 @@ +"""Module for ring device mocks. + +Creates a MagicMock for all device families, i.e. chimes, doorbells, stickup_cams and other. + +Each device entry in the devices.json will have a MagicMock instead of the RingObject. + +Mocks the api calls on the devices such as history() and health(). +""" + +from copy import deepcopy +from datetime import datetime +from time import time +from unittest.mock import MagicMock + +from ring_doorbell import ( + RingCapability, + RingChime, + RingDoorBell, + RingOther, + RingStickUpCam, +) + +from homeassistant.components.ring.const import DOMAIN +from homeassistant.util import dt as dt_util + +from tests.common import load_json_value_fixture + +DEVICES_FIXTURE = load_json_value_fixture("devices.json", DOMAIN) +DOORBOT_HISTORY = load_json_value_fixture("doorbot_history.json", DOMAIN) +INTERCOM_HISTORY = load_json_value_fixture("intercom_history.json", DOMAIN) +DOORBOT_HEALTH = load_json_value_fixture("doorbot_health_attrs.json", DOMAIN) +CHIME_HEALTH = load_json_value_fixture("chime_health_attrs.json", DOMAIN) +DEVICE_ALERTS = load_json_value_fixture("ding_active.json", DOMAIN) + + +def get_mock_devices(): + """Return list of mock devices keyed by device_type.""" + devices = {} + for device_family, device_class in DEVICE_TYPES.items(): + devices[device_family] = [ + _mocked_ring_device( + device, device_family, device_class, DEVICE_CAPABILITIES[device_class] + ) + for device in DEVICES_FIXTURE[device_family] + ] + return devices + + +def get_devices_data(): + """Return devices raw json used by the diagnostics module.""" + return { + device_type: {obj["id"]: obj for obj in devices} + for device_type, devices in DEVICES_FIXTURE.items() + } + + +def get_active_alerts(): + """Return active alerts set to now.""" + dings_fixture = deepcopy(DEVICE_ALERTS) + for ding in dings_fixture: + ding["now"] = time() + return dings_fixture + + +DEVICE_TYPES = { + "doorbots": RingDoorBell, + "authorized_doorbots": RingDoorBell, + "stickup_cams": RingStickUpCam, + "chimes": RingChime, + "other": RingOther, +} + +DEVICE_CAPABILITIES = { + RingDoorBell: [ + RingCapability.BATTERY, + RingCapability.VOLUME, + RingCapability.MOTION_DETECTION, + RingCapability.VIDEO, + RingCapability.HISTORY, + ], + RingStickUpCam: [ + RingCapability.BATTERY, + RingCapability.VOLUME, + RingCapability.MOTION_DETECTION, + RingCapability.VIDEO, + RingCapability.HISTORY, + RingCapability.SIREN, + RingCapability.LIGHT, + ], + RingChime: [RingCapability.VOLUME], + RingOther: [RingCapability.OPEN, RingCapability.HISTORY], +} + + +def _mocked_ring_device(device_dict, device_family, device_class, capabilities): + """Return a mocked device.""" + mock_device = MagicMock(spec=device_class, name=f"Mocked {device_family!s}") + + def has_capability(capability): + return ( + capability in capabilities + if isinstance(capability, RingCapability) + else RingCapability.from_name(capability) in capabilities + ) + + def update_health_data(fixture): + mock_device.configure_mock( + wifi_signal_category=fixture["device_health"].get("latest_signal_category"), + wifi_signal_strength=fixture["device_health"].get("latest_signal_strength"), + ) + + def update_history_data(fixture): + for entry in fixture: # Mimic the api date parsing + if isinstance(entry["created_at"], str): + dt_at = datetime.strptime(entry["created_at"], "%Y-%m-%dT%H:%M:%S.%f%z") + entry["created_at"] = dt_util.as_utc(dt_at) + mock_device.configure_mock(last_history=fixture) # Set last_history + return fixture + + # Configure the device attributes + mock_device.configure_mock(**device_dict) + + # Configure the Properties on the device + mock_device.configure_mock( + model=device_family, + device_api_id=device_dict["id"], + name=device_dict["description"], + wifi_signal_category=None, + wifi_signal_strength=None, + family=device_family, + ) + + # Configure common methods + mock_device.has_capability.side_effect = has_capability + mock_device.update_health_data.side_effect = lambda: update_health_data( + DOORBOT_HEALTH if device_family != "chimes" else CHIME_HEALTH + ) + # Configure methods based on capability + if has_capability(RingCapability.HISTORY): + mock_device.configure_mock(last_history=[]) + mock_device.history.side_effect = lambda *_, **__: update_history_data( + DOORBOT_HISTORY if device_family != "other" else INTERCOM_HISTORY + ) + + if has_capability(RingCapability.MOTION_DETECTION): + mock_device.configure_mock( + motion_detection=device_dict["settings"].get("motion_detection_enabled"), + ) + + if has_capability(RingCapability.LIGHT): + mock_device.configure_mock(lights=device_dict.get("led_status")) + + if has_capability(RingCapability.VOLUME): + mock_device.configure_mock( + volume=device_dict["settings"].get( + "doorbell_volume", device_dict["settings"].get("volume") + ) + ) + + if has_capability(RingCapability.SIREN): + mock_device.configure_mock( + siren=device_dict["siren_status"].get("seconds_remaining") + ) + + if has_capability(RingCapability.BATTERY): + mock_device.configure_mock( + battery_life=min( + 100, device_dict.get("battery_life", device_dict.get("battery_life2")) + ) + ) + + if device_family == "other": + mock_device.configure_mock( + doorbell_volume=device_dict["settings"].get("doorbell_volume"), + mic_volume=device_dict["settings"].get("mic_volume"), + voice_volume=device_dict["settings"].get("voice_volume"), + ) + + return mock_device diff --git a/tests/components/ring/fixtures/chime_devices.json b/tests/components/ring/fixtures/chime_devices.json deleted file mode 100644 index 5c3e60ec655..00000000000 --- a/tests/components/ring/fixtures/chime_devices.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "authorized_doorbots": [], - "chimes": [ - { - "address": "123 Main St", - "alerts": { "connection": "online" }, - "description": "Downstairs", - "device_id": "abcdef123", - "do_not_disturb": { "seconds_left": 0 }, - "features": { "ringtones_enabled": true }, - "firmware_version": "1.2.3", - "id": 123456, - "kind": "chime", - "latitude": 12.0, - "longitude": -70.12345, - "owned": true, - "owner": { - "email": "foo@bar.org", - "first_name": "Marcelo", - "id": 999999, - "last_name": "Assistant" - }, - "settings": { - "ding_audio_id": null, - "ding_audio_user_id": null, - "motion_audio_id": null, - "motion_audio_user_id": null, - "volume": 2 - }, - "time_zone": "America/New_York" - } - ], - "doorbots": [], - "stickup_cams": [] -} diff --git a/tests/components/ring/fixtures/devices.json b/tests/components/ring/fixtures/devices.json index 8deee7ec413..fc708115500 100644 --- a/tests/components/ring/fixtures/devices.json +++ b/tests/components/ring/fixtures/devices.json @@ -5,7 +5,7 @@ "address": "123 Main St", "alerts": { "connection": "online" }, "description": "Downstairs", - "device_id": "abcdef123", + "device_id": "abcdef123456", "do_not_disturb": { "seconds_left": 0 }, "features": { "ringtones_enabled": true }, "firmware_version": "1.2.3", @@ -36,7 +36,7 @@ "alerts": { "connection": "online" }, "battery_life": 4081, "description": "Front Door", - "device_id": "aacdef123", + "device_id": "aacdef987654", "external_connection": false, "features": { "advanced_motion_enabled": false, @@ -85,7 +85,7 @@ "alerts": { "connection": "online" }, "battery_life": 80, "description": "Front", - "device_id": "aacdef123", + "device_id": "aacdef765432", "external_connection": false, "features": { "advanced_motion_enabled": false, @@ -234,7 +234,7 @@ "alerts": { "connection": "online" }, "battery_life": 80, "description": "Internal", - "device_id": "aacdef124", + "device_id": "aacdef345678", "external_connection": false, "features": { "advanced_motion_enabled": false, @@ -395,7 +395,7 @@ "last_name": "", "email": "" }, - "device_id": "124ba1b3fe1a", + "device_id": "abcdef185036587", "time_zone": "Europe/Rome", "firmware_version": "Up to Date", "owned": true, diff --git a/tests/components/ring/fixtures/devices_updated.json b/tests/components/ring/fixtures/devices_updated.json deleted file mode 100644 index 01ea2ca25f5..00000000000 --- a/tests/components/ring/fixtures/devices_updated.json +++ /dev/null @@ -1,382 +0,0 @@ -{ - "authorized_doorbots": [], - "chimes": [ - { - "address": "123 Main St", - "alerts": { "connection": "online" }, - "description": "Downstairs", - "device_id": "abcdef123", - "do_not_disturb": { "seconds_left": 0 }, - "features": { "ringtones_enabled": true }, - "firmware_version": "1.2.3", - "id": 123456, - "kind": "chime", - "latitude": 12.0, - "longitude": -70.12345, - "owned": true, - "owner": { - "email": "foo@bar.org", - "first_name": "Marcelo", - "id": 999999, - "last_name": "Assistant" - }, - "settings": { - "ding_audio_id": null, - "ding_audio_user_id": null, - "motion_audio_id": null, - "motion_audio_user_id": null, - "volume": 2 - }, - "time_zone": "America/New_York" - } - ], - "doorbots": [ - { - "address": "123 Main St", - "alerts": { "connection": "online" }, - "battery_life": 4081, - "description": "Front Door", - "device_id": "aacdef123", - "external_connection": false, - "features": { - "advanced_motion_enabled": false, - "motion_message_enabled": false, - "motions_enabled": true, - "people_only_enabled": false, - "shadow_correction_enabled": false, - "show_recordings": true - }, - "firmware_version": "1.4.26", - "id": 987654, - "kind": "lpd_v1", - "latitude": 12.0, - "longitude": -70.12345, - "motion_snooze": null, - "owned": true, - "owner": { - "email": "foo@bar.org", - "first_name": "Home", - "id": 999999, - "last_name": "Assistant" - }, - "settings": { - "chime_settings": { - "duration": 3, - "enable": true, - "type": 0 - }, - "doorbell_volume": 1, - "enable_vod": true, - "live_view_preset_profile": "highest", - "live_view_presets": ["low", "middle", "high", "highest"], - "motion_detection_enabled": true, - "motion_announcement": false, - "motion_snooze_preset_profile": "low", - "motion_snooze_presets": ["null", "low", "medium", "high"] - }, - "subscribed": true, - "subscribed_motions": true, - "time_zone": "America/New_York" - } - ], - "stickup_cams": [ - { - "address": "123 Main St", - "alerts": { "connection": "online" }, - "battery_life": 80, - "description": "Front", - "device_id": "aacdef123", - "external_connection": false, - "features": { - "advanced_motion_enabled": false, - "motion_message_enabled": false, - "motions_enabled": true, - "night_vision_enabled": false, - "people_only_enabled": false, - "shadow_correction_enabled": false, - "show_recordings": true - }, - "firmware_version": "1.9.3", - "id": 765432, - "kind": "hp_cam_v1", - "latitude": 12.0, - "led_status": "on", - "location_id": null, - "longitude": -70.12345, - "motion_snooze": { "scheduled": true }, - "night_mode_status": "false", - "owned": true, - "owner": { - "email": "foo@bar.org", - "first_name": "Foo", - "id": 999999, - "last_name": "Bar" - }, - "ring_cam_light_installed": "false", - "ring_id": null, - "settings": { - "chime_settings": { - "duration": 10, - "enable": true, - "type": 0 - }, - "doorbell_volume": 11, - "enable_vod": true, - "floodlight_settings": { - "duration": 30, - "priority": 0 - }, - "light_schedule_settings": { - "end_hour": 0, - "end_minute": 0, - "start_hour": 0, - "start_minute": 0 - }, - "live_view_preset_profile": "highest", - "live_view_presets": ["low", "middle", "high", "highest"], - "motion_detection_enabled": true, - "motion_announcement": false, - "motion_snooze_preset_profile": "low", - "motion_snooze_presets": ["none", "low", "medium", "high"], - "motion_zones": { - "active_motion_filter": 1, - "advanced_object_settings": { - "human_detection_confidence": { - "day": 0.7, - "night": 0.7 - }, - "motion_zone_overlap": { - "day": 0.1, - "night": 0.2 - }, - "object_size_maximum": { - "day": 0.8, - "night": 0.8 - }, - "object_size_minimum": { - "day": 0.03, - "night": 0.05 - }, - "object_time_overlap": { - "day": 0.1, - "night": 0.6 - } - }, - "enable_audio": false, - "pir_settings": { - "sensitivity1": 1, - "sensitivity2": 1, - "sensitivity3": 1, - "zone_mask": 6 - }, - "sensitivity": 5, - "zone1": { - "name": "Zone 1", - "state": 2, - "vertex1": { "x": 0.0, "y": 0.0 }, - "vertex2": { "x": 0.0, "y": 0.0 }, - "vertex3": { "x": 0.0, "y": 0.0 }, - "vertex4": { "x": 0.0, "y": 0.0 }, - "vertex5": { "x": 0.0, "y": 0.0 }, - "vertex6": { "x": 0.0, "y": 0.0 }, - "vertex7": { "x": 0.0, "y": 0.0 }, - "vertex8": { "x": 0.0, "y": 0.0 } - }, - "zone2": { - "name": "Zone 2", - "state": 2, - "vertex1": { "x": 0.0, "y": 0.0 }, - "vertex2": { "x": 0.0, "y": 0.0 }, - "vertex3": { "x": 0.0, "y": 0.0 }, - "vertex4": { "x": 0.0, "y": 0.0 }, - "vertex5": { "x": 0.0, "y": 0.0 }, - "vertex6": { "x": 0.0, "y": 0.0 }, - "vertex7": { "x": 0.0, "y": 0.0 }, - "vertex8": { "x": 0.0, "y": 0.0 } - }, - "zone3": { - "name": "Zone 3", - "state": 2, - "vertex1": { "x": 0.0, "y": 0.0 }, - "vertex2": { "x": 0.0, "y": 0.0 }, - "vertex3": { "x": 0.0, "y": 0.0 }, - "vertex4": { "x": 0.0, "y": 0.0 }, - "vertex5": { "x": 0.0, "y": 0.0 }, - "vertex6": { "x": 0.0, "y": 0.0 }, - "vertex7": { "x": 0.0, "y": 0.0 }, - "vertex8": { "x": 0.0, "y": 0.0 } - } - }, - "pir_motion_zones": [0, 1, 1], - "pir_settings": { - "sensitivity1": 1, - "sensitivity2": 1, - "sensitivity3": 1, - "zone_mask": 6 - }, - "stream_setting": 0, - "video_settings": { - "ae_level": 0, - "birton": null, - "brightness": 0, - "contrast": 64, - "saturation": 80 - } - }, - "siren_status": { "seconds_remaining": 30 }, - "stolen": false, - "subscribed": true, - "subscribed_motions": true, - "time_zone": "America/New_York" - }, - { - "address": "123 Main St", - "alerts": { "connection": "online" }, - "battery_life": 80, - "description": "Internal", - "device_id": "aacdef124", - "external_connection": false, - "features": { - "advanced_motion_enabled": false, - "motion_message_enabled": false, - "motions_enabled": true, - "night_vision_enabled": false, - "people_only_enabled": false, - "shadow_correction_enabled": false, - "show_recordings": true - }, - "firmware_version": "1.9.3", - "id": 345678, - "kind": "hp_cam_v1", - "latitude": 12.0, - "led_status": "off", - "location_id": null, - "longitude": -70.12345, - "motion_snooze": { "scheduled": true }, - "night_mode_status": "false", - "owned": true, - "owner": { - "email": "foo@bar.org", - "first_name": "Foo", - "id": 999999, - "last_name": "Bar" - }, - "ring_cam_light_installed": "false", - "ring_id": null, - "settings": { - "chime_settings": { - "duration": 10, - "enable": true, - "type": 0 - }, - "doorbell_volume": 11, - "enable_vod": true, - "floodlight_settings": { - "duration": 30, - "priority": 0 - }, - "light_schedule_settings": { - "end_hour": 0, - "end_minute": 0, - "start_hour": 0, - "start_minute": 0 - }, - "live_view_preset_profile": "highest", - "live_view_presets": ["low", "middle", "high", "highest"], - "motion_detection_enabled": false, - "motion_announcement": false, - "motion_snooze_preset_profile": "low", - "motion_snooze_presets": ["none", "low", "medium", "high"], - "motion_zones": { - "active_motion_filter": 1, - "advanced_object_settings": { - "human_detection_confidence": { - "day": 0.7, - "night": 0.7 - }, - "motion_zone_overlap": { - "day": 0.1, - "night": 0.2 - }, - "object_size_maximum": { - "day": 0.8, - "night": 0.8 - }, - "object_size_minimum": { - "day": 0.03, - "night": 0.05 - }, - "object_time_overlap": { - "day": 0.1, - "night": 0.6 - } - }, - "enable_audio": false, - "pir_settings": { - "sensitivity1": 1, - "sensitivity2": 1, - "sensitivity3": 1, - "zone_mask": 6 - }, - "sensitivity": 5, - "zone1": { - "name": "Zone 1", - "state": 2, - "vertex1": { "x": 0.0, "y": 0.0 }, - "vertex2": { "x": 0.0, "y": 0.0 }, - "vertex3": { "x": 0.0, "y": 0.0 }, - "vertex4": { "x": 0.0, "y": 0.0 }, - "vertex5": { "x": 0.0, "y": 0.0 }, - "vertex6": { "x": 0.0, "y": 0.0 }, - "vertex7": { "x": 0.0, "y": 0.0 }, - "vertex8": { "x": 0.0, "y": 0.0 } - }, - "zone2": { - "name": "Zone 2", - "state": 2, - "vertex1": { "x": 0.0, "y": 0.0 }, - "vertex2": { "x": 0.0, "y": 0.0 }, - "vertex3": { "x": 0.0, "y": 0.0 }, - "vertex4": { "x": 0.0, "y": 0.0 }, - "vertex5": { "x": 0.0, "y": 0.0 }, - "vertex6": { "x": 0.0, "y": 0.0 }, - "vertex7": { "x": 0.0, "y": 0.0 }, - "vertex8": { "x": 0.0, "y": 0.0 } - }, - "zone3": { - "name": "Zone 3", - "state": 2, - "vertex1": { "x": 0.0, "y": 0.0 }, - "vertex2": { "x": 0.0, "y": 0.0 }, - "vertex3": { "x": 0.0, "y": 0.0 }, - "vertex4": { "x": 0.0, "y": 0.0 }, - "vertex5": { "x": 0.0, "y": 0.0 }, - "vertex6": { "x": 0.0, "y": 0.0 }, - "vertex7": { "x": 0.0, "y": 0.0 }, - "vertex8": { "x": 0.0, "y": 0.0 } - } - }, - "pir_motion_zones": [0, 1, 1], - "pir_settings": { - "sensitivity1": 1, - "sensitivity2": 1, - "sensitivity3": 1, - "zone_mask": 6 - }, - "stream_setting": 0, - "video_settings": { - "ae_level": 0, - "birton": null, - "brightness": 0, - "contrast": 64, - "saturation": 80 - } - }, - "siren_status": { "seconds_remaining": 30 }, - "stolen": false, - "subscribed": true, - "subscribed_motions": true, - "time_zone": "America/New_York" - } - ] -} diff --git a/tests/components/ring/fixtures/ding_active.json b/tests/components/ring/fixtures/ding_active.json index b367369fcff..1d089ab454e 100644 --- a/tests/components/ring/fixtures/ding_active.json +++ b/tests/components/ring/fixtures/ding_active.json @@ -24,5 +24,12 @@ "snapshot_url": "", "state": "ringing", "video_jitter_buffer_ms": 0 + }, + { + "kind": "motion", + "doorbot_id": 987654, + "state": "ringing", + "now": 1490949469.5498993, + "expires_in": 180 } ] diff --git a/tests/components/ring/fixtures/doorboot_health_attrs.json b/tests/components/ring/fixtures/doorbot_health_attrs.json similarity index 100% rename from tests/components/ring/fixtures/doorboot_health_attrs.json rename to tests/components/ring/fixtures/doorbot_health_attrs.json diff --git a/tests/components/ring/fixtures/doorbot_history.json b/tests/components/ring/fixtures/doorbot_history.json index 2f6b44318bb..1c4c97e51c7 100644 --- a/tests/components/ring/fixtures/doorbot_history.json +++ b/tests/components/ring/fixtures/doorbot_history.json @@ -1,4 +1,14 @@ [ + { + "answered": false, + "created_at": "2018-03-05T15:03:40.000Z", + "events": [], + "favorite": false, + "id": 987654321, + "kind": "ding", + "recording": { "status": "ready" }, + "snapshot_url": "" + }, { "answered": false, "created_at": "2017-03-05T15:03:40.000Z", diff --git a/tests/components/ring/fixtures/doorbot_siren_on_response.json b/tests/components/ring/fixtures/doorbot_siren_on_response.json deleted file mode 100644 index 288800ed5fa..00000000000 --- a/tests/components/ring/fixtures/doorbot_siren_on_response.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "started_at": "2019-07-28T16:58:27.593+00:00", - "duration": 30, - "ends_at": "2019-07-28T16:58:57.593+00:00", - "seconds_remaining": 30 -} diff --git a/tests/components/ring/fixtures/groups.json b/tests/components/ring/fixtures/groups.json deleted file mode 100644 index 399aaac1641..00000000000 --- a/tests/components/ring/fixtures/groups.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "device_groups": [ - { - "device_group_id": "mock-group-id", - "location_id": "mock-location-id", - "name": "Landscape", - "devices": [ - { - "doorbot_id": 12345678, - "location_id": "mock-location-id", - "type": "beams_ct200_transformer", - "mac_address": null, - "hardware_id": "1234567890", - "name": "Mock Transformer", - "deleted_at": null - } - ], - "created_at": "2020-11-03T22:07:05Z", - "updated_at": "2020-11-19T03:52:59Z", - "deleted_at": null, - "external_id": "12345678-1234-5678-90ab-1234567890ab" - } - ] -} diff --git a/tests/components/ring/fixtures/oauth.json b/tests/components/ring/fixtures/oauth.json deleted file mode 100644 index 902e40a4110..00000000000 --- a/tests/components/ring/fixtures/oauth.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "access_token": "eyJ0eWfvEQwqfJNKyQ9999", - "token_type": "bearer", - "expires_in": 3600, - "refresh_token": "67695a26bdefc1ac8999", - "scope": "client", - "created_at": 1529099870 -} diff --git a/tests/components/ring/fixtures/session.json b/tests/components/ring/fixtures/session.json deleted file mode 100644 index 62c8efa1d8f..00000000000 --- a/tests/components/ring/fixtures/session.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "profile": { - "authentication_token": "12345678910", - "email": "foo@bar.org", - "features": { - "chime_dnd_enabled": false, - "chime_pro_enabled": true, - "delete_all_enabled": true, - "delete_all_settings_enabled": false, - "device_health_alerts_enabled": true, - "floodlight_cam_enabled": true, - "live_view_settings_enabled": true, - "lpd_enabled": true, - "lpd_motion_announcement_enabled": false, - "multiple_calls_enabled": true, - "multiple_delete_enabled": true, - "nw_enabled": true, - "nw_larger_area_enabled": false, - "nw_user_activated": false, - "owner_proactive_snoozing_enabled": true, - "power_cable_enabled": false, - "proactive_snoozing_enabled": false, - "reactive_snoozing_enabled": false, - "remote_logging_format_storing": false, - "remote_logging_level": 1, - "ringplus_enabled": true, - "starred_events_enabled": true, - "stickupcam_setup_enabled": true, - "subscriptions_enabled": true, - "ujet_enabled": false, - "video_search_enabled": false, - "vod_enabled": false - }, - "first_name": "Home", - "id": 999999, - "last_name": "Assistant" - } -} diff --git a/tests/components/ring/test_binary_sensor.py b/tests/components/ring/test_binary_sensor.py index ba73de05c9b..16bc6e872c1 100644 --- a/tests/components/ring/test_binary_sensor.py +++ b/tests/components/ring/test_binary_sensor.py @@ -1,32 +1,14 @@ """The tests for the Ring binary sensor platform.""" -from time import time -from unittest.mock import patch - -import requests_mock - +from homeassistant.const import Platform from homeassistant.core import HomeAssistant from .common import setup_platform -async def test_binary_sensor( - hass: HomeAssistant, requests_mock: requests_mock.Mocker -) -> None: +async def test_binary_sensor(hass: HomeAssistant, mock_ring_client) -> None: """Test the Ring binary sensors.""" - with patch( - "ring_doorbell.Ring.active_alerts", - return_value=[ - { - "kind": "motion", - "doorbot_id": 987654, - "state": "ringing", - "now": time(), - "expires_in": 180, - } - ], - ): - await setup_platform(hass, "binary_sensor") + await setup_platform(hass, Platform.BINARY_SENSOR) motion_state = hass.states.get("binary_sensor.front_door_motion") assert motion_state is not None diff --git a/tests/components/ring/test_button.py b/tests/components/ring/test_button.py index 6b2200b2bf3..6fef3295159 100644 --- a/tests/components/ring/test_button.py +++ b/tests/components/ring/test_button.py @@ -1,7 +1,5 @@ """The tests for the Ring button platform.""" -import requests_mock - from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -11,7 +9,7 @@ from .common import setup_platform async def test_entity_registry( hass: HomeAssistant, - requests_mock: requests_mock.Mocker, + mock_ring_client, entity_registry: er.EntityRegistry, ) -> None: """Tests that the devices are registered in the entity registry.""" @@ -22,21 +20,19 @@ async def test_entity_registry( async def test_button_opens_door( - hass: HomeAssistant, requests_mock: requests_mock.Mocker + hass: HomeAssistant, + mock_ring_client, + mock_ring_devices, ) -> None: """Tests the door open button works correctly.""" await setup_platform(hass, Platform.BUTTON) - # Mocks the response for opening door - mock = requests_mock.put( - "https://api.ring.com/commands/v1/devices/185036587/device_rpc", - status_code=200, - text="{}", - ) + mock_intercom = mock_ring_devices.get_device(185036587) + mock_intercom.open_door.assert_not_called() await hass.services.async_call( "button", "press", {"entity_id": "button.ingress_open_door"}, blocking=True ) - await hass.async_block_till_done() - assert mock.call_count == 1 + await hass.async_block_till_done(wait_background_tasks=True) + mock_intercom.open_door.assert_called_once() diff --git a/tests/components/ring/test_camera.py b/tests/components/ring/test_camera.py index 1b7023f931b..20a9ed5f0c9 100644 --- a/tests/components/ring/test_camera.py +++ b/tests/components/ring/test_camera.py @@ -1,9 +1,8 @@ """The tests for the Ring switch platform.""" -from unittest.mock import PropertyMock, patch +from unittest.mock import PropertyMock import pytest -import requests_mock import ring_doorbell from homeassistant.config_entries import SOURCE_REAUTH @@ -14,13 +13,11 @@ from homeassistant.helpers import entity_registry as er from .common import setup_platform -from tests.common import load_fixture - async def test_entity_registry( hass: HomeAssistant, entity_registry: er.EntityRegistry, - requests_mock: requests_mock.Mocker, + mock_ring_client, ) -> None: """Tests that the devices are registered in the entity registry.""" await setup_platform(hass, Platform.CAMERA) @@ -42,7 +39,7 @@ async def test_entity_registry( ) async def test_camera_motion_detection_state_reports_correctly( hass: HomeAssistant, - requests_mock: requests_mock.Mocker, + mock_ring_client, entity_name, expected_state, friendly_name, @@ -56,7 +53,7 @@ async def test_camera_motion_detection_state_reports_correctly( async def test_camera_motion_detection_can_be_turned_on( - hass: HomeAssistant, requests_mock: requests_mock.Mocker + hass: HomeAssistant, mock_ring_client ) -> None: """Tests the siren turns on correctly.""" await setup_platform(hass, Platform.CAMERA) @@ -78,17 +75,15 @@ async def test_camera_motion_detection_can_be_turned_on( async def test_updates_work( - hass: HomeAssistant, requests_mock: requests_mock.Mocker + hass: HomeAssistant, mock_ring_client, mock_ring_devices ) -> None: """Tests the update service works correctly.""" await setup_platform(hass, Platform.CAMERA) state = hass.states.get("camera.internal") assert state.attributes.get("motion_detection") is True - # Changes the return to indicate that the switch is now on. - requests_mock.get( - "https://api.ring.com/clients_api/ring_devices", - text=load_fixture("devices_updated.json", "ring"), - ) + + internal_camera_mock = mock_ring_devices.get_device(345678) + internal_camera_mock.motion_detection = False await hass.services.async_call("ring", "update", {}, blocking=True) @@ -109,7 +104,8 @@ async def test_updates_work( ) async def test_motion_detection_errors_when_turned_on( hass: HomeAssistant, - requests_mock: requests_mock.Mocker, + mock_ring_client, + mock_ring_devices, exception_type, reauth_expected, ) -> None: @@ -119,19 +115,19 @@ async def test_motion_detection_errors_when_turned_on( assert not any(config_entry.async_get_active_flows(hass, {SOURCE_REAUTH})) - with patch.object( - ring_doorbell.RingDoorBell, "motion_detection", new_callable=PropertyMock - ) as mock_motion_detection: - mock_motion_detection.side_effect = exception_type - with pytest.raises(HomeAssistantError): - await hass.services.async_call( - "camera", - "enable_motion_detection", - {"entity_id": "camera.front"}, - blocking=True, - ) - await hass.async_block_till_done() - assert mock_motion_detection.call_count == 1 + front_camera_mock = mock_ring_devices.get_device(765432) + p = PropertyMock(side_effect=exception_type) + type(front_camera_mock).motion_detection = p + + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + "camera", + "enable_motion_detection", + {"entity_id": "camera.front"}, + blocking=True, + ) + await hass.async_block_till_done() + p.assert_called_once() assert ( any( flow diff --git a/tests/components/ring/test_config_flow.py b/tests/components/ring/test_config_flow.py index bedb4604814..2420bb9cc50 100644 --- a/tests/components/ring/test_config_flow.py +++ b/tests/components/ring/test_config_flow.py @@ -17,7 +17,7 @@ from tests.common import MockConfigEntry async def test_form( hass: HomeAssistant, mock_setup_entry: AsyncMock, - mock_ring_auth: Mock, + mock_ring_client: Mock, ) -> None: """Test we get the form.""" diff --git a/tests/components/ring/test_diagnostics.py b/tests/components/ring/test_diagnostics.py index 269446c3ad5..7d6eb8a7f76 100644 --- a/tests/components/ring/test_diagnostics.py +++ b/tests/components/ring/test_diagnostics.py @@ -1,6 +1,5 @@ """Test Ring diagnostics.""" -import requests_mock from syrupy.assertion import SnapshotAssertion from homeassistant.core import HomeAssistant @@ -14,7 +13,7 @@ async def test_entry_diagnostics( hass: HomeAssistant, hass_client: ClientSessionGenerator, mock_config_entry: MockConfigEntry, - requests_mock: requests_mock.Mocker, + mock_ring_client, snapshot: SnapshotAssertion, ) -> None: """Test Ring diagnostics.""" diff --git a/tests/components/ring/test_init.py b/tests/components/ring/test_init.py index f4958f8e497..feb2485303a 100644 --- a/tests/components/ring/test_init.py +++ b/tests/components/ring/test_init.py @@ -1,67 +1,69 @@ """The tests for the Ring component.""" -from datetime import timedelta -from unittest.mock import patch - +from freezegun.api import FrozenDateTimeFactory import pytest -import requests_mock from ring_doorbell import AuthenticationError, RingError, RingTimeout from homeassistant.components import ring from homeassistant.components.camera import DOMAIN as CAMERA_DOMAIN from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN from homeassistant.components.ring import DOMAIN +from homeassistant.components.ring.const import SCAN_INTERVAL from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState from homeassistant.const import CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er, issue_registry as ir from homeassistant.setup import async_setup_component -from homeassistant.util import dt as dt_util -from tests.common import MockConfigEntry, async_fire_time_changed, load_fixture +from tests.common import MockConfigEntry, async_fire_time_changed -async def test_setup(hass: HomeAssistant, requests_mock: requests_mock.Mocker) -> None: +async def test_setup(hass: HomeAssistant, mock_ring_client) -> None: """Test the setup.""" await async_setup_component(hass, ring.DOMAIN, {}) - requests_mock.post( - "https://oauth.ring.com/oauth/token", text=load_fixture("oauth.json", "ring") - ) - requests_mock.post( - "https://api.ring.com/clients_api/session", - text=load_fixture("session.json", "ring"), - ) - requests_mock.get( - "https://api.ring.com/clients_api/ring_devices", - text=load_fixture("devices.json", "ring"), - ) - requests_mock.get( - "https://api.ring.com/clients_api/chimes/999999/health", - text=load_fixture("chime_health_attrs.json", "ring"), - ) - requests_mock.get( - "https://api.ring.com/clients_api/doorbots/987652/health", - text=load_fixture("doorboot_health_attrs.json", "ring"), - ) + +async def test_setup_entry( + hass: HomeAssistant, + mock_ring_client, + mock_added_config_entry: MockConfigEntry, +) -> None: + """Test setup entry.""" + assert mock_added_config_entry.state is ConfigEntryState.LOADED + + +async def test_setup_entry_device_update( + hass: HomeAssistant, + mock_ring_client, + mock_ring_devices, + freezer: FrozenDateTimeFactory, + mock_added_config_entry: MockConfigEntry, + caplog, +) -> None: + """Test devices are updating after setup entry.""" + + front_door_doorbell = mock_ring_devices.get_device(987654) + front_door_doorbell.history.assert_not_called() + freezer.tick(SCAN_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done(wait_background_tasks=True) + front_door_doorbell.history.assert_called_once() async def test_auth_failed_on_setup( hass: HomeAssistant, - requests_mock: requests_mock.Mocker, + mock_ring_client, mock_config_entry: MockConfigEntry, ) -> None: """Test auth failure on setup entry.""" mock_config_entry.add_to_hass(hass) - with patch( - "ring_doorbell.Ring.update_data", - side_effect=AuthenticationError, - ): - assert not any(mock_config_entry.async_get_active_flows(hass, {SOURCE_REAUTH})) - await hass.config_entries.async_setup(mock_config_entry.entry_id) - await hass.async_block_till_done() - assert any(mock_config_entry.async_get_active_flows(hass, {SOURCE_REAUTH})) - assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR + mock_ring_client.update_data.side_effect = AuthenticationError + + assert not any(mock_config_entry.async_get_active_flows(hass, {SOURCE_REAUTH})) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + assert any(mock_config_entry.async_get_active_flows(hass, {SOURCE_REAUTH})) + assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR @pytest.mark.parametrize( @@ -80,37 +82,30 @@ async def test_auth_failed_on_setup( ) async def test_error_on_setup( hass: HomeAssistant, - requests_mock: requests_mock.Mocker, + mock_ring_client, mock_config_entry: MockConfigEntry, caplog: pytest.LogCaptureFixture, error_type, log_msg, ) -> None: - """Test auth failure on setup entry.""" + """Test non-auth errors on setup entry.""" mock_config_entry.add_to_hass(hass) - with patch( - "ring_doorbell.Ring.update_data", - side_effect=error_type, - ): - await hass.config_entries.async_setup(mock_config_entry.entry_id) - await hass.async_block_till_done() - assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY + mock_ring_client.update_data.side_effect = error_type - assert [ - record.message - for record in caplog.records - if record.levelname == "DEBUG" - and record.name == "homeassistant.config_entries" - and log_msg in record.message - and DOMAIN in record.message - ] + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY + + assert log_msg in caplog.text async def test_auth_failure_on_global_update( hass: HomeAssistant, - requests_mock: requests_mock.Mocker, + mock_ring_client, mock_config_entry: MockConfigEntry, + freezer: FrozenDateTimeFactory, caplog: pytest.LogCaptureFixture, ) -> None: """Test authentication failure on global data update.""" @@ -118,27 +113,24 @@ async def test_auth_failure_on_global_update( await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() assert not any(mock_config_entry.async_get_active_flows(hass, {SOURCE_REAUTH})) - with patch( - "ring_doorbell.Ring.update_devices", - side_effect=AuthenticationError, - ): - async_fire_time_changed(hass, dt_util.now() + timedelta(minutes=20)) - await hass.async_block_till_done() - assert "Authentication failed while fetching devices data: " in [ - record.message - for record in caplog.records - if record.levelname == "ERROR" - and record.name == "homeassistant.components.ring.coordinator" - ] + mock_ring_client.update_devices.side_effect = AuthenticationError - assert any(mock_config_entry.async_get_active_flows(hass, {SOURCE_REAUTH})) + freezer.tick(SCAN_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert "Authentication failed while fetching devices data: " in caplog.text + + assert any(mock_config_entry.async_get_active_flows(hass, {SOURCE_REAUTH})) async def test_auth_failure_on_device_update( hass: HomeAssistant, - requests_mock: requests_mock.Mocker, + mock_ring_client, + mock_ring_devices, mock_config_entry: MockConfigEntry, + freezer: FrozenDateTimeFactory, caplog: pytest.LogCaptureFixture, ) -> None: """Test authentication failure on device data update.""" @@ -146,21 +138,17 @@ async def test_auth_failure_on_device_update( await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() assert not any(mock_config_entry.async_get_active_flows(hass, {SOURCE_REAUTH})) - with patch( - "ring_doorbell.RingDoorBell.history", - side_effect=AuthenticationError, - ): - async_fire_time_changed(hass, dt_util.now() + timedelta(minutes=20)) - await hass.async_block_till_done(wait_background_tasks=True) - assert "Authentication failed while fetching devices data: " in [ - record.message - for record in caplog.records - if record.levelname == "ERROR" - and record.name == "homeassistant.components.ring.coordinator" - ] + front_door_doorbell = mock_ring_devices.get_device(987654) + front_door_doorbell.history.side_effect = AuthenticationError - assert any(mock_config_entry.async_get_active_flows(hass, {SOURCE_REAUTH})) + freezer.tick(SCAN_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done(wait_background_tasks=True) + + assert "Authentication failed while fetching devices data: " in caplog.text + + assert any(mock_config_entry.async_get_active_flows(hass, {SOURCE_REAUTH})) @pytest.mark.parametrize( @@ -179,29 +167,27 @@ async def test_auth_failure_on_device_update( ) async def test_error_on_global_update( hass: HomeAssistant, - requests_mock: requests_mock.Mocker, + mock_ring_client, mock_config_entry: MockConfigEntry, + freezer: FrozenDateTimeFactory, caplog: pytest.LogCaptureFixture, error_type, log_msg, ) -> None: - """Test error on global data update.""" + """Test non-auth errors on global data update.""" mock_config_entry.add_to_hass(hass) await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() - with patch( - "ring_doorbell.Ring.update_devices", - side_effect=error_type, - ): - async_fire_time_changed(hass, dt_util.now() + timedelta(minutes=20)) - await hass.async_block_till_done(wait_background_tasks=True) + mock_ring_client.update_devices.side_effect = error_type - assert log_msg in [ - record.message for record in caplog.records if record.levelname == "ERROR" - ] + freezer.tick(SCAN_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done(wait_background_tasks=True) - assert mock_config_entry.entry_id in hass.data[DOMAIN] + assert log_msg in caplog.text + + assert mock_config_entry.entry_id in hass.data[DOMAIN] @pytest.mark.parametrize( @@ -220,35 +206,35 @@ async def test_error_on_global_update( ) async def test_error_on_device_update( hass: HomeAssistant, - requests_mock: requests_mock.Mocker, + mock_ring_client, + mock_ring_devices, mock_config_entry: MockConfigEntry, + freezer: FrozenDateTimeFactory, caplog: pytest.LogCaptureFixture, error_type, log_msg, ) -> None: - """Test auth failure on data update.""" + """Test non-auth errors on device update.""" mock_config_entry.add_to_hass(hass) await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() - with patch( - "ring_doorbell.RingDoorBell.history", - side_effect=error_type, - ): - async_fire_time_changed(hass, dt_util.now() + timedelta(minutes=20)) - await hass.async_block_till_done(wait_background_tasks=True) + front_door_doorbell = mock_ring_devices.get_device(765432) + front_door_doorbell.history.side_effect = error_type - assert log_msg in [ - record.message for record in caplog.records if record.levelname == "ERROR" - ] - assert mock_config_entry.entry_id in hass.data[DOMAIN] + freezer.tick(SCAN_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done(wait_background_tasks=True) + + assert log_msg in caplog.text + assert mock_config_entry.entry_id in hass.data[DOMAIN] async def test_issue_deprecated_service_ring_update( hass: HomeAssistant, issue_registry: ir.IssueRegistry, caplog: pytest.LogCaptureFixture, - requests_mock: requests_mock.Mocker, + mock_ring_client, mock_config_entry: MockConfigEntry, ) -> None: """Test the issue is raised on deprecated service ring.update.""" @@ -288,7 +274,7 @@ async def test_update_unique_id( hass: HomeAssistant, entity_registry: er.EntityRegistry, caplog: pytest.LogCaptureFixture, - requests_mock: requests_mock.Mocker, + mock_ring_client, domain: str, old_unique_id: int | str, ) -> None: @@ -324,7 +310,7 @@ async def test_update_unique_id_existing( hass: HomeAssistant, entity_registry: er.EntityRegistry, caplog: pytest.LogCaptureFixture, - requests_mock: requests_mock.Mocker, + mock_ring_client, ) -> None: """Test unique_id update of integration.""" old_unique_id = 123456 @@ -372,7 +358,7 @@ async def test_update_unique_id_no_update( hass: HomeAssistant, entity_registry: er.EntityRegistry, caplog: pytest.LogCaptureFixture, - requests_mock: requests_mock.Mocker, + mock_ring_client, ) -> None: """Test unique_id update of integration.""" correct_unique_id = "123456" diff --git a/tests/components/ring/test_light.py b/tests/components/ring/test_light.py index 1dcafadd86d..c2d21a22951 100644 --- a/tests/components/ring/test_light.py +++ b/tests/components/ring/test_light.py @@ -1,9 +1,8 @@ """The tests for the Ring light platform.""" -from unittest.mock import PropertyMock, patch +from unittest.mock import PropertyMock import pytest -import requests_mock import ring_doorbell from homeassistant.config_entries import SOURCE_REAUTH @@ -14,13 +13,11 @@ from homeassistant.helpers import entity_registry as er from .common import setup_platform -from tests.common import load_fixture - async def test_entity_registry( hass: HomeAssistant, entity_registry: er.EntityRegistry, - requests_mock: requests_mock.Mocker, + mock_ring_client, ) -> None: """Tests that the devices are registered in the entity registry.""" await setup_platform(hass, Platform.LIGHT) @@ -33,7 +30,7 @@ async def test_entity_registry( async def test_light_off_reports_correctly( - hass: HomeAssistant, requests_mock: requests_mock.Mocker + hass: HomeAssistant, mock_ring_client ) -> None: """Tests that the initial state of a device that should be off is correct.""" await setup_platform(hass, Platform.LIGHT) @@ -44,7 +41,7 @@ async def test_light_off_reports_correctly( async def test_light_on_reports_correctly( - hass: HomeAssistant, requests_mock: requests_mock.Mocker + hass: HomeAssistant, mock_ring_client ) -> None: """Tests that the initial state of a device that should be on is correct.""" await setup_platform(hass, Platform.LIGHT) @@ -54,18 +51,10 @@ async def test_light_on_reports_correctly( assert state.attributes.get("friendly_name") == "Internal Light" -async def test_light_can_be_turned_on( - hass: HomeAssistant, requests_mock: requests_mock.Mocker -) -> None: +async def test_light_can_be_turned_on(hass: HomeAssistant, mock_ring_client) -> None: """Tests the light turns on correctly.""" await setup_platform(hass, Platform.LIGHT) - # Mocks the response for turning a light on - requests_mock.put( - "https://api.ring.com/clients_api/doorbots/765432/floodlight_light_on", - text=load_fixture("doorbot_siren_on_response.json", "ring"), - ) - state = hass.states.get("light.front_light") assert state.state == "off" @@ -79,17 +68,15 @@ async def test_light_can_be_turned_on( async def test_updates_work( - hass: HomeAssistant, requests_mock: requests_mock.Mocker + hass: HomeAssistant, mock_ring_client, mock_ring_devices ) -> None: """Tests the update service works correctly.""" await setup_platform(hass, Platform.LIGHT) state = hass.states.get("light.front_light") assert state.state == "off" - # Changes the return to indicate that the light is now on. - requests_mock.get( - "https://api.ring.com/clients_api/ring_devices", - text=load_fixture("devices_updated.json", "ring"), - ) + + front_light_mock = mock_ring_devices.get_device(765432) + front_light_mock.lights = "on" await hass.services.async_call("ring", "update", {}, blocking=True) @@ -110,7 +97,8 @@ async def test_updates_work( ) async def test_light_errors_when_turned_on( hass: HomeAssistant, - requests_mock: requests_mock.Mocker, + mock_ring_client, + mock_ring_devices, exception_type, reauth_expected, ) -> None: @@ -120,16 +108,17 @@ async def test_light_errors_when_turned_on( assert not any(config_entry.async_get_active_flows(hass, {SOURCE_REAUTH})) - with patch.object( - ring_doorbell.RingStickUpCam, "lights", new_callable=PropertyMock - ) as mock_lights: - mock_lights.side_effect = exception_type - with pytest.raises(HomeAssistantError): - await hass.services.async_call( - "light", "turn_on", {"entity_id": "light.front_light"}, blocking=True - ) - await hass.async_block_till_done() - assert mock_lights.call_count == 1 + front_light_mock = mock_ring_devices.get_device(765432) + p = PropertyMock(side_effect=exception_type) + type(front_light_mock).lights = p + + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + "light", "turn_on", {"entity_id": "light.front_light"}, blocking=True + ) + await hass.async_block_till_done() + p.assert_called_once() + assert ( any( flow diff --git a/tests/components/ring/test_sensor.py b/tests/components/ring/test_sensor.py index c7c2d64e892..1f05c120251 100644 --- a/tests/components/ring/test_sensor.py +++ b/tests/components/ring/test_sensor.py @@ -4,21 +4,19 @@ import logging from freezegun.api import FrozenDateTimeFactory import pytest -import requests_mock from homeassistant.components.ring.const import SCAN_INTERVAL from homeassistant.components.sensor import ATTR_STATE_CLASS, SensorStateClass from homeassistant.const import Platform from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er from .common import setup_platform -from tests.common import async_fire_time_changed, load_fixture - -WIFI_ENABLED = False +from tests.common import async_fire_time_changed -async def test_sensor(hass: HomeAssistant, requests_mock: requests_mock.Mocker) -> None: +async def test_sensor(hass: HomeAssistant, mock_ring_client) -> None: """Test the Ring sensors.""" await setup_platform(hass, "sensor") @@ -41,10 +39,6 @@ async def test_sensor(hass: HomeAssistant, requests_mock: requests_mock.Mocker) assert downstairs_volume_state is not None assert downstairs_volume_state.state == "2" - downstairs_wifi_signal_strength_state = hass.states.get( - "sensor.downstairs_wifi_signal_strength" - ) - ingress_mic_volume_state = hass.states.get("sensor.ingress_mic_volume") assert ingress_mic_volume_state.state == "11" @@ -54,56 +48,118 @@ async def test_sensor(hass: HomeAssistant, requests_mock: requests_mock.Mocker) ingress_voice_volume_state = hass.states.get("sensor.ingress_voice_volume") assert ingress_voice_volume_state.state == "11" - if not WIFI_ENABLED: - return - assert downstairs_wifi_signal_strength_state is not None - assert downstairs_wifi_signal_strength_state.state == "-39" - - front_door_wifi_signal_category_state = hass.states.get( - "sensor.front_door_wifi_signal_category" - ) - assert front_door_wifi_signal_category_state is not None - assert front_door_wifi_signal_category_state.state == "good" - - front_door_wifi_signal_strength_state = hass.states.get( - "sensor.front_door_wifi_signal_strength" - ) - assert front_door_wifi_signal_strength_state is not None - assert front_door_wifi_signal_strength_state.state == "-58" - - -async def test_history( +@pytest.mark.parametrize( + ("device_id", "device_name", "sensor_name", "expected_value"), + [ + (987654, "front_door", "wifi_signal_category", "good"), + (987654, "front_door", "wifi_signal_strength", "-58"), + (123456, "downstairs", "wifi_signal_category", "good"), + (123456, "downstairs", "wifi_signal_strength", "-39"), + (765432, "front", "wifi_signal_category", "good"), + (765432, "front", "wifi_signal_strength", "-58"), + ], + ids=[ + "doorbell-category", + "doorbell-strength", + "chime-category", + "chime-strength", + "stickup_cam-category", + "stickup_cam-strength", + ], +) +async def test_health_sensor( hass: HomeAssistant, + mock_ring_client, freezer: FrozenDateTimeFactory, - requests_mock: requests_mock.Mocker, + entity_registry: er.EntityRegistry, + device_id, + device_name, + sensor_name, + expected_value, ) -> None: - """Test history derived sensors.""" - await setup_platform(hass, Platform.SENSOR) + """Test the Ring health sensors.""" + entity_id = f"sensor.{device_name}_{sensor_name}" + # Enable the sensor as the health sensors are disabled by default + entity_entry = entity_registry.async_get_or_create( + "sensor", + "ring", + f"{device_id}-{sensor_name}", + suggested_object_id=f"{device_name}_{sensor_name}", + disabled_by=None, + ) + assert entity_entry.disabled is False + assert entity_entry.entity_id == entity_id + + await setup_platform(hass, "sensor") + await hass.async_block_till_done() + + sensor_state = hass.states.get(entity_id) + assert sensor_state is not None + assert sensor_state.state == "unknown" + freezer.tick(SCAN_INTERVAL) async_fire_time_changed(hass) - await hass.async_block_till_done(True) + await hass.async_block_till_done(wait_background_tasks=True) + sensor_state = hass.states.get(entity_id) + assert sensor_state is not None + assert sensor_state.state == expected_value - front_door_last_activity_state = hass.states.get("sensor.front_door_last_activity") - assert front_door_last_activity_state.state == "2017-03-05T15:03:40+00:00" - ingress_last_activity_state = hass.states.get("sensor.ingress_last_activity") - assert ingress_last_activity_state.state == "2024-02-02T11:21:24+00:00" +@pytest.mark.parametrize( + ("device_name", "sensor_name", "expected_value"), + [ + ("front_door", "last_motion", "2017-03-05T15:03:40+00:00"), + ("front_door", "last_ding", "2018-03-05T15:03:40+00:00"), + ("front_door", "last_activity", "2018-03-05T15:03:40+00:00"), + ("front", "last_motion", "2017-03-05T15:03:40+00:00"), + ("ingress", "last_activity", "2024-02-02T11:21:24+00:00"), + ], + ids=[ + "doorbell-motion", + "doorbell-ding", + "doorbell-activity", + "stickup_cam-motion", + "other-activity", + ], +) +async def test_history_sensor( + hass: HomeAssistant, + mock_ring_client, + freezer: FrozenDateTimeFactory, + device_name, + sensor_name, + expected_value, +) -> None: + """Test the Ring sensors.""" + await setup_platform(hass, "sensor") + + entity_id = f"sensor.{device_name}_{sensor_name}" + sensor_state = hass.states.get(entity_id) + assert sensor_state is not None + assert sensor_state.state == "unknown" + + freezer.tick(SCAN_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done(wait_background_tasks=True) + sensor_state = hass.states.get(entity_id) + assert sensor_state is not None + assert sensor_state.state == expected_value async def test_only_chime_devices( hass: HomeAssistant, - requests_mock: requests_mock.Mocker, + mock_ring_client, + mock_ring_devices, freezer: FrozenDateTimeFactory, caplog: pytest.LogCaptureFixture, ) -> None: """Tests the update service works correctly if only chimes are returned.""" await hass.config.async_set_time_zone("UTC") freezer.move_to("2021-01-09 12:00:00+00:00") - requests_mock.get( - "https://api.ring.com/clients_api/ring_devices", - text=load_fixture("chime_devices.json", "ring"), - ) + + mock_ring_devices.all_devices = mock_ring_devices.chimes + await setup_platform(hass, Platform.SENSOR) await hass.async_block_till_done() caplog.set_level(logging.DEBUG) diff --git a/tests/components/ring/test_siren.py b/tests/components/ring/test_siren.py index 8206f0c4ad3..7d3f673b61f 100644 --- a/tests/components/ring/test_siren.py +++ b/tests/components/ring/test_siren.py @@ -1,9 +1,6 @@ """The tests for the Ring button platform.""" -from unittest.mock import patch - import pytest -import requests_mock import ring_doorbell from homeassistant.config_entries import SOURCE_REAUTH @@ -18,7 +15,7 @@ from .common import setup_platform async def test_entity_registry( hass: HomeAssistant, entity_registry: er.EntityRegistry, - requests_mock: requests_mock.Mocker, + mock_ring_client, ) -> None: """Tests that the devices are registered in the entity registry.""" await setup_platform(hass, Platform.SIREN) @@ -27,9 +24,7 @@ async def test_entity_registry( assert entry.unique_id == "123456-siren" -async def test_sirens_report_correctly( - hass: HomeAssistant, requests_mock: requests_mock.Mocker -) -> None: +async def test_sirens_report_correctly(hass: HomeAssistant, mock_ring_client) -> None: """Tests that the initial state of a device that should be on is correct.""" await setup_platform(hass, Platform.SIREN) @@ -39,16 +34,11 @@ async def test_sirens_report_correctly( async def test_default_ding_chime_can_be_played( - hass: HomeAssistant, requests_mock: requests_mock.Mocker + hass: HomeAssistant, mock_ring_client, mock_ring_devices ) -> None: """Tests the play chime request is sent correctly.""" await setup_platform(hass, Platform.SIREN) - # Mocks the response for playing a test sound - requests_mock.post( - "https://api.ring.com/clients_api/chimes/123456/play_sound", - text="SUCCESS", - ) await hass.services.async_call( "siren", "turn_on", @@ -58,26 +48,19 @@ async def test_default_ding_chime_can_be_played( await hass.async_block_till_done() - assert requests_mock.request_history[-1].url.startswith( - "https://api.ring.com/clients_api/chimes/123456/play_sound?" - ) - assert "kind=ding" in requests_mock.request_history[-1].url + downstairs_chime_mock = mock_ring_devices.get_device(123456) + downstairs_chime_mock.test_sound.assert_called_once_with(kind="ding") state = hass.states.get("siren.downstairs_siren") assert state.state == "unknown" async def test_turn_on_plays_default_chime( - hass: HomeAssistant, requests_mock: requests_mock.Mocker + hass: HomeAssistant, mock_ring_client, mock_ring_devices ) -> None: """Tests the play chime request is sent correctly when turned on.""" await setup_platform(hass, Platform.SIREN) - # Mocks the response for playing a test sound - requests_mock.post( - "https://api.ring.com/clients_api/chimes/123456/play_sound", - text="SUCCESS", - ) await hass.services.async_call( "siren", "turn_on", @@ -87,26 +70,21 @@ async def test_turn_on_plays_default_chime( await hass.async_block_till_done() - assert requests_mock.request_history[-1].url.startswith( - "https://api.ring.com/clients_api/chimes/123456/play_sound?" - ) - assert "kind=ding" in requests_mock.request_history[-1].url + downstairs_chime_mock = mock_ring_devices.get_device(123456) + downstairs_chime_mock.test_sound.assert_called_once_with(kind="ding") state = hass.states.get("siren.downstairs_siren") assert state.state == "unknown" async def test_explicit_ding_chime_can_be_played( - hass: HomeAssistant, requests_mock: requests_mock.Mocker + hass: HomeAssistant, + mock_ring_client, + mock_ring_devices, ) -> None: """Tests the play chime request is sent correctly.""" await setup_platform(hass, Platform.SIREN) - # Mocks the response for playing a test sound - requests_mock.post( - "https://api.ring.com/clients_api/chimes/123456/play_sound", - text="SUCCESS", - ) await hass.services.async_call( "siren", "turn_on", @@ -116,26 +94,19 @@ async def test_explicit_ding_chime_can_be_played( await hass.async_block_till_done() - assert requests_mock.request_history[-1].url.startswith( - "https://api.ring.com/clients_api/chimes/123456/play_sound?" - ) - assert "kind=ding" in requests_mock.request_history[-1].url + downstairs_chime_mock = mock_ring_devices.get_device(123456) + downstairs_chime_mock.test_sound.assert_called_once_with(kind="ding") state = hass.states.get("siren.downstairs_siren") assert state.state == "unknown" async def test_motion_chime_can_be_played( - hass: HomeAssistant, requests_mock: requests_mock.Mocker + hass: HomeAssistant, mock_ring_client, mock_ring_devices ) -> None: """Tests the play chime request is sent correctly.""" await setup_platform(hass, Platform.SIREN) - # Mocks the response for playing a test sound - requests_mock.post( - "https://api.ring.com/clients_api/chimes/123456/play_sound", - text="SUCCESS", - ) await hass.services.async_call( "siren", "turn_on", @@ -145,10 +116,8 @@ async def test_motion_chime_can_be_played( await hass.async_block_till_done() - assert requests_mock.request_history[-1].url.startswith( - "https://api.ring.com/clients_api/chimes/123456/play_sound?" - ) - assert "kind=motion" in requests_mock.request_history[-1].url + downstairs_chime_mock = mock_ring_devices.get_device(123456) + downstairs_chime_mock.test_sound.assert_called_once_with(kind="motion") state = hass.states.get("siren.downstairs_siren") assert state.state == "unknown" @@ -165,7 +134,8 @@ async def test_motion_chime_can_be_played( ) async def test_siren_errors_when_turned_on( hass: HomeAssistant, - requests_mock: requests_mock.Mocker, + mock_ring_client, + mock_ring_devices, exception_type, reauth_expected, ) -> None: @@ -175,18 +145,18 @@ async def test_siren_errors_when_turned_on( assert not any(config_entry.async_get_active_flows(hass, {SOURCE_REAUTH})) - with patch.object( - ring_doorbell.RingChime, "test_sound", side_effect=exception_type - ) as mock_siren: - with pytest.raises(HomeAssistantError): - await hass.services.async_call( - "siren", - "turn_on", - {"entity_id": "siren.downstairs_siren", "tone": "motion"}, - blocking=True, - ) + downstairs_chime_mock = mock_ring_devices.get_device(123456) + downstairs_chime_mock.test_sound.side_effect = exception_type + + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + "siren", + "turn_on", + {"entity_id": "siren.downstairs_siren", "tone": "motion"}, + blocking=True, + ) await hass.async_block_till_done() - assert mock_siren.call_count == 1 + downstairs_chime_mock.test_sound.assert_called_once_with(kind="motion") assert ( any( flow diff --git a/tests/components/ring/test_switch.py b/tests/components/ring/test_switch.py index 8e49a815a0b..405f20420b7 100644 --- a/tests/components/ring/test_switch.py +++ b/tests/components/ring/test_switch.py @@ -1,9 +1,8 @@ """The tests for the Ring switch platform.""" -from unittest.mock import PropertyMock, patch +from unittest.mock import PropertyMock import pytest -import requests_mock import ring_doorbell from homeassistant.config_entries import SOURCE_REAUTH @@ -15,13 +14,11 @@ from homeassistant.setup import async_setup_component from .common import setup_platform -from tests.common import load_fixture - async def test_entity_registry( hass: HomeAssistant, entity_registry: er.EntityRegistry, - requests_mock: requests_mock.Mocker, + mock_ring_client, ) -> None: """Tests that the devices are registered in the entity registry.""" await setup_platform(hass, Platform.SWITCH) @@ -34,7 +31,7 @@ async def test_entity_registry( async def test_siren_off_reports_correctly( - hass: HomeAssistant, requests_mock: requests_mock.Mocker + hass: HomeAssistant, mock_ring_client ) -> None: """Tests that the initial state of a device that should be off is correct.""" await setup_platform(hass, Platform.SWITCH) @@ -45,7 +42,7 @@ async def test_siren_off_reports_correctly( async def test_siren_on_reports_correctly( - hass: HomeAssistant, requests_mock: requests_mock.Mocker + hass: HomeAssistant, mock_ring_client ) -> None: """Tests that the initial state of a device that should be on is correct.""" await setup_platform(hass, Platform.SWITCH) @@ -55,18 +52,10 @@ async def test_siren_on_reports_correctly( assert state.attributes.get("friendly_name") == "Internal Siren" -async def test_siren_can_be_turned_on( - hass: HomeAssistant, requests_mock: requests_mock.Mocker -) -> None: +async def test_siren_can_be_turned_on(hass: HomeAssistant, mock_ring_client) -> None: """Tests the siren turns on correctly.""" await setup_platform(hass, Platform.SWITCH) - # Mocks the response for turning a siren on - requests_mock.put( - "https://api.ring.com/clients_api/doorbots/765432/siren_on", - text=load_fixture("doorbot_siren_on_response.json", "ring"), - ) - state = hass.states.get("switch.front_siren") assert state.state == "off" @@ -80,17 +69,15 @@ async def test_siren_can_be_turned_on( async def test_updates_work( - hass: HomeAssistant, requests_mock: requests_mock.Mocker + hass: HomeAssistant, mock_ring_client, mock_ring_devices ) -> None: """Tests the update service works correctly.""" await setup_platform(hass, Platform.SWITCH) state = hass.states.get("switch.front_siren") assert state.state == "off" - # Changes the return to indicate that the siren is now on. - requests_mock.get( - "https://api.ring.com/clients_api/ring_devices", - text=load_fixture("devices_updated.json", "ring"), - ) + + front_siren_mock = mock_ring_devices.get_device(765432) + front_siren_mock.siren = 20 await async_setup_component(hass, "homeassistant", {}) await hass.services.async_call( @@ -117,7 +104,8 @@ async def test_updates_work( ) async def test_switch_errors_when_turned_on( hass: HomeAssistant, - requests_mock: requests_mock.Mocker, + mock_ring_client, + mock_ring_devices, exception_type, reauth_expected, ) -> None: @@ -127,16 +115,16 @@ async def test_switch_errors_when_turned_on( assert not any(config_entry.async_get_active_flows(hass, {SOURCE_REAUTH})) - with patch.object( - ring_doorbell.RingStickUpCam, "siren", new_callable=PropertyMock - ) as mock_switch: - mock_switch.side_effect = exception_type - with pytest.raises(HomeAssistantError): - await hass.services.async_call( - "switch", "turn_on", {"entity_id": "switch.front_siren"}, blocking=True - ) - await hass.async_block_till_done() - assert mock_switch.call_count == 1 + front_siren_mock = mock_ring_devices.get_device(765432) + p = PropertyMock(side_effect=exception_type) + type(front_siren_mock).siren = p + + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + "switch", "turn_on", {"entity_id": "switch.front_siren"}, blocking=True + ) + await hass.async_block_till_done() + p.assert_called_once() assert ( any( flow From 696a079ba8f5acd437031dc51cb53475d15e2cd5 Mon Sep 17 00:00:00 2001 From: Gedaliah Knizhnik <36511858+gedaliahknizhnik@users.noreply.github.com> Date: Thu, 6 Jun 2024 14:17:52 -0400 Subject: [PATCH 0311/1445] Add extra sensor to the Jewish Calendar integration (#116734) * Added sensor for always three star tzeit * Changed sensor name * Change sensor name --- homeassistant/components/jewish_calendar/sensor.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/jewish_calendar/sensor.py b/homeassistant/components/jewish_calendar/sensor.py index 90e504fe8fd..88d8ecf1751 100644 --- a/homeassistant/components/jewish_calendar/sensor.py +++ b/homeassistant/components/jewish_calendar/sensor.py @@ -125,6 +125,11 @@ TIME_SENSORS: tuple[SensorEntityDescription, ...] = ( name="T'set Hakochavim", icon="mdi:weather-night", ), + SensorEntityDescription( + key="three_stars", + name="T'set Hakochavim, 3 stars", + icon="mdi:weather-night", + ), SensorEntityDescription( key="upcoming_shabbat_candle_lighting", name="Upcoming Shabbat Candle Lighting", From bca8958d4bdba83b8bad7d941237a63c5373b5c7 Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Thu, 6 Jun 2024 13:20:34 -0500 Subject: [PATCH 0312/1445] Prioritize literal text with name slots in sentence matching (#118900) Prioritize literal text with name slots --- .../components/conversation/default_agent.py | 11 ++++- .../test_default_agent_intents.py | 48 +++++++++++++++++++ 2 files changed, 57 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/conversation/default_agent.py b/homeassistant/components/conversation/default_agent.py index d5454883292..7bb2c2182b3 100644 --- a/homeassistant/components/conversation/default_agent.py +++ b/homeassistant/components/conversation/default_agent.py @@ -429,8 +429,15 @@ class DefaultAgent(ConversationEntity): intent_context=intent_context, language=language, ): - if ("name" in result.entities) and ( - not result.entities["name"].is_wildcard + # Prioritize results with a "name" slot, but still prefer ones with + # more literal text matched. + if ( + ("name" in result.entities) + and (not result.entities["name"].is_wildcard) + and ( + (name_result is None) + or (result.text_chunks_matched > name_result.text_chunks_matched) + ) ): name_result = result diff --git a/tests/components/conversation/test_default_agent_intents.py b/tests/components/conversation/test_default_agent_intents.py index f5050f4483e..b1c4a6d51af 100644 --- a/tests/components/conversation/test_default_agent_intents.py +++ b/tests/components/conversation/test_default_agent_intents.py @@ -1,5 +1,7 @@ """Test intents for the default agent.""" +from unittest.mock import patch + import pytest from homeassistant.components import ( @@ -7,6 +9,7 @@ from homeassistant.components import ( cover, light, media_player, + todo, vacuum, valve, ) @@ -35,6 +38,27 @@ from homeassistant.setup import async_setup_component from tests.common import async_mock_service +class MockTodoListEntity(todo.TodoListEntity): + """Test todo list entity.""" + + def __init__(self, items: list[todo.TodoItem] | None = None) -> None: + """Initialize entity.""" + self._attr_todo_items = items or [] + + @property + def items(self) -> list[todo.TodoItem]: + """Return the items in the To-do list.""" + return self._attr_todo_items + + async def async_create_todo_item(self, item: todo.TodoItem) -> None: + """Add an item to the To-do list.""" + self._attr_todo_items.append(item) + + async def async_delete_todo_items(self, uids: list[str]) -> None: + """Delete an item in the To-do list.""" + self._attr_todo_items = [item for item in self.items if item.uid not in uids] + + @pytest.fixture async def init_components(hass: HomeAssistant): """Initialize relevant components with empty configs.""" @@ -365,3 +389,27 @@ async def test_turn_floor_lights_on_off( assert {s.entity_id for s in result.response.matched_states} == { bedroom_light.entity_id } + + +async def test_todo_add_item_fr( + hass: HomeAssistant, + init_components, +) -> None: + """Test that wildcard matches prioritize results with more literal text matched.""" + assert await async_setup_component(hass, todo.DOMAIN, {}) + hass.states.async_set("todo.liste_des_courses", 0, {}) + + with ( + patch.object(hass.config, "language", "fr"), + patch( + "homeassistant.components.todo.intent.ListAddItemIntent.async_handle", + return_value=intent.IntentResponse(hass.config.language), + ) as mock_handle, + ): + await conversation.async_converse( + hass, "Ajoute de la farine a la liste des courses", None, Context(), None + ) + mock_handle.assert_called_once() + assert mock_handle.call_args.args + intent_obj = mock_handle.call_args.args[0] + assert intent_obj.slots.get("item", {}).get("value", "").strip() == "farine" From d695660164ab9f0321683517f596146c88a12757 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Thu, 6 Jun 2024 20:22:41 +0200 Subject: [PATCH 0313/1445] Use fixtures in UniFi diagnostics tests (#118905) --- .../unifi/snapshots/test_diagnostics.ambr | 129 ++++++++++++ tests/components/unifi/test_diagnostics.py | 187 ++++-------------- 2 files changed, 170 insertions(+), 146 deletions(-) create mode 100644 tests/components/unifi/snapshots/test_diagnostics.ambr diff --git a/tests/components/unifi/snapshots/test_diagnostics.ambr b/tests/components/unifi/snapshots/test_diagnostics.ambr new file mode 100644 index 00000000000..fb7415c59ab --- /dev/null +++ b/tests/components/unifi/snapshots/test_diagnostics.ambr @@ -0,0 +1,129 @@ +# serializer version: 1 +# name: test_entry_diagnostics[dpi_group_payload0-dpi_app_payload0-device_payload0-client_payload0-config_entry_options0] + dict({ + 'clients': dict({ + '00:00:00:00:00:00': dict({ + 'blocked': False, + 'hostname': 'client_1', + 'ip': '10.0.0.1', + 'is_wired': True, + 'last_seen': 1562600145, + 'mac': '00:00:00:00:00:00', + 'name': 'POE Client 1', + 'oui': 'Producer', + 'sw_mac': '00:00:00:00:00:01', + 'sw_port': 1, + 'wired-rx_bytes': 1234000000, + 'wired-tx_bytes': 5678000000, + }), + }), + 'config': dict({ + 'data': dict({ + 'host': '**REDACTED**', + 'password': '**REDACTED**', + 'port': 1234, + 'site': 'site_id', + 'username': '**REDACTED**', + 'verify_ssl': False, + }), + 'disabled_by': None, + 'domain': 'unifi', + 'entry_id': '1', + 'minor_version': 1, + 'options': dict({ + 'allow_bandwidth_sensors': True, + 'allow_uptime_sensors': True, + 'block_client': list([ + '00:00:00:00:00:00', + ]), + }), + 'pref_disable_new_entities': False, + 'pref_disable_polling': False, + 'source': 'user', + 'title': 'Mock Title', + 'unique_id': '1', + 'version': 1, + }), + 'devices': dict({ + '00:00:00:00:00:01': dict({ + 'board_rev': '1.2.3', + 'device_id': 'mock-id', + 'ethernet_table': list([ + dict({ + 'mac': '00:00:00:00:00:02', + 'name': 'eth0', + 'num_port': 2, + }), + ]), + 'ip': '10.0.1.1', + 'last_seen': 1562600145, + 'mac': '00:00:00:00:00:01', + 'model': 'US16P150', + 'name': 'mock-name', + 'port_overrides': list([ + ]), + 'port_table': list([ + dict({ + 'mac_table': list([ + dict({ + 'age': 1, + 'mac': '00:00:00:00:00:00', + 'static': False, + 'uptime': 3971792, + 'vlan': 1, + }), + dict({ + 'age': 1, + 'mac': '**REDACTED**', + 'static': True, + 'uptime': 0, + 'vlan': 0, + }), + ]), + 'media': 'GE', + 'name': 'Port 1', + 'poe_class': 'Class 4', + 'poe_enable': True, + 'poe_mode': 'auto', + 'poe_power': '2.56', + 'poe_voltage': '53.40', + 'port_idx': 1, + 'port_poe': True, + 'portconf_id': '1a1', + 'up': True, + }), + ]), + 'state': 1, + 'type': 'usw', + 'version': '4.0.42.10433', + }), + }), + 'dpi_apps': dict({ + '5f976f62e3c58f018ec7e17d': dict({ + '_id': '5f976f62e3c58f018ec7e17d', + 'apps': list([ + ]), + 'blocked': True, + 'cats': list([ + '4', + ]), + 'enabled': True, + 'log': True, + 'site_id': 'name', + }), + }), + 'dpi_groups': dict({ + '5f976f4ae3c58f018ec7dff6': dict({ + '_id': '5f976f4ae3c58f018ec7dff6', + 'dpiapp_ids': list([ + '5f976f62e3c58f018ec7e17d', + ]), + 'name': 'Block Media Streaming', + 'site_id': 'name', + }), + }), + 'role_is_admin': True, + 'wlans': dict({ + }), + }) +# --- diff --git a/tests/components/unifi/test_diagnostics.py b/tests/components/unifi/test_diagnostics.py index 792512683d3..fcaba59cbad 100644 --- a/tests/components/unifi/test_diagnostics.py +++ b/tests/components/unifi/test_diagnostics.py @@ -1,27 +1,21 @@ """Test UniFi Network diagnostics.""" -from homeassistant.components.diagnostics import REDACTED +import pytest +from syrupy.assertion import SnapshotAssertion + from homeassistant.components.unifi.const import ( CONF_ALLOW_BANDWIDTH_SENSORS, CONF_ALLOW_UPTIME_SENSORS, CONF_BLOCK_CLIENT, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from .test_hub import setup_unifi_integration - from tests.components.diagnostics import get_diagnostics_for_config_entry -from tests.test_util.aiohttp import AiohttpClientMocker from tests.typing import ClientSessionGenerator - -async def test_entry_diagnostics( - hass: HomeAssistant, - hass_client: ClientSessionGenerator, - aioclient_mock: AiohttpClientMocker, -) -> None: - """Test config entry diagnostics.""" - client = { +CLIENT_DATA = [ + { "blocked": False, "hostname": "client_1", "ip": "10.0.0.1", @@ -35,7 +29,9 @@ async def test_entry_diagnostics( "wired-rx_bytes": 1234000000, "wired-tx_bytes": 5678000000, } - device = { +] +DEVICE_DATA = [ + { "board_rev": "1.2.3", "ethernet_table": [ { @@ -86,7 +82,9 @@ async def test_entry_diagnostics( "type": "usw", "version": "4.0.42.10433", } - dpi_app = { +] +DPI_APP_DATA = [ + { "_id": "5f976f62e3c58f018ec7e17d", "apps": [], "blocked": True, @@ -95,142 +93,39 @@ async def test_entry_diagnostics( "log": True, "site_id": "name", } - dpi_group = { +] +DPI_GROUP_DATA = [ + { "_id": "5f976f4ae3c58f018ec7dff6", "name": "Block Media Streaming", "site_id": "name", "dpiapp_ids": ["5f976f62e3c58f018ec7e17d"], } +] - options = { - CONF_ALLOW_BANDWIDTH_SENSORS: True, - CONF_ALLOW_UPTIME_SENSORS: True, - CONF_BLOCK_CLIENT: ["00:00:00:00:00:01"], - } - config_entry = await setup_unifi_integration( - hass, - aioclient_mock, - options=options, - clients_response=[client], - devices_response=[device], - dpiapp_response=[dpi_app], - dpigroup_response=[dpi_group], + +@pytest.mark.parametrize( + "config_entry_options", + [ + { + CONF_ALLOW_BANDWIDTH_SENSORS: True, + CONF_ALLOW_UPTIME_SENSORS: True, + CONF_BLOCK_CLIENT: ["00:00:00:00:00:01"], + } + ], +) +@pytest.mark.parametrize("client_payload", [CLIENT_DATA]) +@pytest.mark.parametrize("device_payload", [DEVICE_DATA]) +@pytest.mark.parametrize("dpi_app_payload", [DPI_APP_DATA]) +@pytest.mark.parametrize("dpi_group_payload", [DPI_GROUP_DATA]) +async def test_entry_diagnostics( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + config_entry_setup: ConfigEntry, + snapshot: SnapshotAssertion, +) -> None: + """Test config entry diagnostics.""" + assert ( + await get_diagnostics_for_config_entry(hass, hass_client, config_entry_setup) + == snapshot ) - - assert await get_diagnostics_for_config_entry(hass, hass_client, config_entry) == { - "config": { - "data": { - "host": REDACTED, - "password": REDACTED, - "port": 1234, - "site": "site_id", - "username": REDACTED, - "verify_ssl": False, - }, - "disabled_by": None, - "domain": "unifi", - "entry_id": "1", - "minor_version": 1, - "options": { - "allow_bandwidth_sensors": True, - "allow_uptime_sensors": True, - "block_client": ["00:00:00:00:00:00"], - }, - "pref_disable_new_entities": False, - "pref_disable_polling": False, - "source": "user", - "title": "Mock Title", - "unique_id": "1", - "version": 1, - }, - "role_is_admin": True, - "clients": { - "00:00:00:00:00:00": { - "blocked": False, - "hostname": "client_1", - "ip": "10.0.0.1", - "is_wired": True, - "last_seen": 1562600145, - "mac": "00:00:00:00:00:00", - "name": "POE Client 1", - "oui": "Producer", - "sw_mac": "00:00:00:00:00:01", - "sw_port": 1, - "wired-rx_bytes": 1234000000, - "wired-tx_bytes": 5678000000, - } - }, - "devices": { - "00:00:00:00:00:01": { - "board_rev": "1.2.3", - "ethernet_table": [ - { - "mac": "00:00:00:00:00:02", - "num_port": 2, - "name": "eth0", - } - ], - "device_id": "mock-id", - "ip": "10.0.1.1", - "mac": "00:00:00:00:00:01", - "last_seen": 1562600145, - "model": "US16P150", - "name": "mock-name", - "port_overrides": [], - "port_table": [ - { - "mac_table": [ - { - "age": 1, - "mac": "00:00:00:00:00:00", - "static": False, - "uptime": 3971792, - "vlan": 1, - }, - { - "age": 1, - "mac": REDACTED, - "static": True, - "uptime": 0, - "vlan": 0, - }, - ], - "media": "GE", - "name": "Port 1", - "port_idx": 1, - "poe_class": "Class 4", - "poe_enable": True, - "poe_mode": "auto", - "poe_power": "2.56", - "poe_voltage": "53.40", - "portconf_id": "1a1", - "port_poe": True, - "up": True, - }, - ], - "state": 1, - "type": "usw", - "version": "4.0.42.10433", - } - }, - "dpi_apps": { - "5f976f62e3c58f018ec7e17d": { - "_id": "5f976f62e3c58f018ec7e17d", - "apps": [], - "blocked": True, - "cats": ["4"], - "enabled": True, - "log": True, - "site_id": "name", - } - }, - "dpi_groups": { - "5f976f4ae3c58f018ec7dff6": { - "_id": "5f976f4ae3c58f018ec7dff6", - "name": "Block Media Streaming", - "site_id": "name", - "dpiapp_ids": ["5f976f62e3c58f018ec7e17d"], - } - }, - "wlans": {}, - } From ec3a976410fbcb69b689fa6063db0b3f495a142f Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Thu, 6 Jun 2024 20:23:10 +0200 Subject: [PATCH 0314/1445] Use fixtures in UniFi image tests (#118887) --- tests/components/unifi/snapshots/test_image.ambr | 6 ++++++ tests/components/unifi/test_image.py | 10 +++------- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/tests/components/unifi/snapshots/test_image.ambr b/tests/components/unifi/snapshots/test_image.ambr index 77b171118a1..83d76688ea3 100644 --- a/tests/components/unifi/snapshots/test_image.ambr +++ b/tests/components/unifi/snapshots/test_image.ambr @@ -5,3 +5,9 @@ # name: test_wlan_qr_code.1 b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x94\x00\x00\x00\x94\x01\x00\x00\x00\x00]G=y\x00\x00\x00\xfdIDATx\xda\xedV1\x8e\x041\x0cB\xf7\x01\xff\xff\x97\xfc\xc0\x0bd\xb6\xda\xe6\xeeB\xb9V\xa4dR \xc7`<\xd8\x8f \xbew\x7f\xb9\x030\x98!\xb5\xe9\xb8\xfc\xc1g\xfc\xf6Nx\xa3%\x9c\x84\xbf\xae\xf1\x84\xb5 \xe796\xf0\\\npjx~1[xZ\\\xbfy+\xf5\xc3\x9b\x8c\xe9\xf0\xeb\xd0k]\xbe\xa3\xa1\xeb\xfaI\x850\xa2Ex\x9f\x1f-\xeb\xe46!\xba\xc0G\x18\xde\xb0|\x8f\x07e8\xca\xd0\xc0,\xd4/\xed&PA\x1a\xf5\xbe~R2m\x07\x8fa\\\xe3\x9d\xc4DnG\x7f\xb0F&\xc4L\xa3~J\xcciy\xdfF\xff\x9a`i\xda$w\xfcom\xcc\x02Kw\x14\xf4\xc2\xd3fn\xba-\xf0A&A\xe2\x0c\x92\x8e\xbfL<\xcb.\xd8\xf1?0~o\xc14\xfcy\xdc\xc48\xa6\xd0\x98\x1f\x99\xbd\xfb\xd0\xd3\x98o\xd1tFR\x07\x8f\xe95lo\xbeE\x88`\x8f\xdf\x8c`lE\x7f\xdf\xff\xc4\x7f\xde\xbd\x00\xfc\xb3\x80\x95k\x06#\x19\x00\x00\x00\x00IEND\xaeB`\x82' # --- +# name: test_wlan_qr_code[wlan_payload0] + b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x84\x00\x00\x00\x84\x01\x00\x00\x00\x00y?\xbe\n\x00\x00\x00\xcaIDATx\xda\xedV[\n\xc30\x0c\x13\xbb\x80\xef\x7fK\xdd\xc0\x93\x94\xfd\xac\x1fcL\xfbl(\xc4\x04*\xacG\xdcb/\x8b\xb8O\xdeO\x00\xccP\x95\x8b\xe5\x03\xd7\xf5\xcd\x89pF\xcf\x8c \\48\x08\nS\x948\x03p\xfe\x80C\xa8\x9d\x16\xc7P\xabvJ}\xe2\xd7\x84[\xe5W\xfc7\xbbS\xfd\xde\xcfB\xf115\xa2\xe3%\x99\xad\x93\xa0:\xbf6\xbeS\xec\x1a^\xb4\xed\xfb\xb2\xab\xd1\x99\xc9\xcdAjx\x89\x0e\xc5\xea\xf4T\xf9\xee\xe40m58\xb6<\x1b\xab~\xf4\xban\xd7:\xceu\x9e\x05\xc4I\xa6\xbb\xfb%q<7:\xbf\xa2\x90wo\xf5 None: """Test the update_clients function when no clients are found.""" - await setup_unifi_integration(hass, aioclient_mock, wlans_response=[WLAN]) assert len(hass.states.async_entity_ids(IMAGE_DOMAIN)) == 0 ent_reg_entry = entity_registry.async_get("image.ssid_1_qr_code") @@ -80,8 +78,6 @@ async def test_wlan_qr_code( entity_registry.async_update_entity( entity_id="image.ssid_1_qr_code", disabled_by=None ) - await hass.async_block_till_done() - async_fire_time_changed( hass, dt_util.utcnow() + timedelta(seconds=RELOAD_AFTER_UPDATE_DELAY + 1), From 7219a4fa98e20c4727e9946024cdcc28651859cc Mon Sep 17 00:00:00 2001 From: Jordi Date: Thu, 6 Jun 2024 22:33:58 +0200 Subject: [PATCH 0315/1445] Add Aquacell integration (#117117) * Initial commit * Support changed API Change sensor entity descriptions * Fix sensor not handling coordinator update * Implement re-authentication flow and handle token expiry * Bump aioaquacell * Bump aioaquacell * Cleanup and initial tests * Fixes for config flow tests * Cleanup * Fixes * Formatted * Use config entry runtime Use icon translations Removed reauth Removed last updated sensor Changed lid in place to binary sensor Cleanup * Remove reauth strings * Removed binary_sensor platform Fixed sensors not updating properly * Remove reauth tests Bump aioaquacell * Moved softener property to entity class Inlined validate_input method Renaming of entities Do a single async_add_entities call to add all entities Reduced code in try blocks * Made tests parameterized and use test fixture for api Cleaned up unused code Removed traces of reauth * Add check if refresh token is expired Add tests * Add missing unique_id to config entry mock Inlined _update_config_entry_refresh_token method Fix incorrect test method name and comment * Add snapshot test Changed WiFi level to WiFi strength * Bump aioaquacell to 0.1.7 * Move test_coordinator tests to test_init Add test for duplicate config entry --- CODEOWNERS | 2 + homeassistant/components/aquacell/__init__.py | 37 +++ .../components/aquacell/config_flow.py | 71 ++++ homeassistant/components/aquacell/const.py | 12 + .../components/aquacell/coordinator.py | 90 ++++++ homeassistant/components/aquacell/entity.py | 41 +++ homeassistant/components/aquacell/icons.json | 20 ++ .../components/aquacell/manifest.json | 12 + homeassistant/components/aquacell/sensor.py | 117 +++++++ .../components/aquacell/strings.json | 45 +++ homeassistant/generated/config_flows.py | 1 + homeassistant/generated/integrations.json | 6 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/aquacell/__init__.py | 33 ++ tests/components/aquacell/conftest.py | 77 +++++ .../get_all_softeners_one_softener.json | 40 +++ .../aquacell/snapshots/test_sensor.ambr | 303 ++++++++++++++++++ tests/components/aquacell/test_config_flow.py | 111 +++++++ tests/components/aquacell/test_init.py | 102 ++++++ tests/components/aquacell/test_sensor.py | 25 ++ 21 files changed, 1151 insertions(+) create mode 100644 homeassistant/components/aquacell/__init__.py create mode 100644 homeassistant/components/aquacell/config_flow.py create mode 100644 homeassistant/components/aquacell/const.py create mode 100644 homeassistant/components/aquacell/coordinator.py create mode 100644 homeassistant/components/aquacell/entity.py create mode 100644 homeassistant/components/aquacell/icons.json create mode 100644 homeassistant/components/aquacell/manifest.json create mode 100644 homeassistant/components/aquacell/sensor.py create mode 100644 homeassistant/components/aquacell/strings.json create mode 100644 tests/components/aquacell/__init__.py create mode 100644 tests/components/aquacell/conftest.py create mode 100644 tests/components/aquacell/fixtures/get_all_softeners_one_softener.json create mode 100644 tests/components/aquacell/snapshots/test_sensor.ambr create mode 100644 tests/components/aquacell/test_config_flow.py create mode 100644 tests/components/aquacell/test_init.py create mode 100644 tests/components/aquacell/test_sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index d9abbd9b851..3df0b4e54cf 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -129,6 +129,8 @@ build.json @home-assistant/supervisor /tests/components/aprs/ @PhilRW /homeassistant/components/apsystems/ @mawoka-myblock @SonnenladenGmbH /tests/components/apsystems/ @mawoka-myblock @SonnenladenGmbH +/homeassistant/components/aquacell/ @Jordi1990 +/tests/components/aquacell/ @Jordi1990 /homeassistant/components/aranet/ @aschmitz @thecode @anrijs /tests/components/aranet/ @aschmitz @thecode @anrijs /homeassistant/components/arcam_fmj/ @elupus diff --git a/homeassistant/components/aquacell/__init__.py b/homeassistant/components/aquacell/__init__.py new file mode 100644 index 00000000000..fc67a3f2c53 --- /dev/null +++ b/homeassistant/components/aquacell/__init__.py @@ -0,0 +1,37 @@ +"""The Aquacell integration.""" + +from __future__ import annotations + +from aioaquacell import AquacellApi + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .coordinator import AquacellCoordinator + +PLATFORMS = [Platform.SENSOR] + +AquacellConfigEntry = ConfigEntry[AquacellCoordinator] + + +async def async_setup_entry(hass: HomeAssistant, entry: AquacellConfigEntry) -> bool: + """Set up Aquacell from a config entry.""" + session = async_get_clientsession(hass) + + aquacell_api = AquacellApi(session) + + coordinator = AquacellCoordinator(hass, aquacell_api) + + await coordinator.async_config_entry_first_refresh() + entry.runtime_data = coordinator + + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/aquacell/config_flow.py b/homeassistant/components/aquacell/config_flow.py new file mode 100644 index 00000000000..a9c749e9e2d --- /dev/null +++ b/homeassistant/components/aquacell/config_flow.py @@ -0,0 +1,71 @@ +"""Config flow for Aquacell integration.""" + +from __future__ import annotations + +from datetime import datetime +import logging +from typing import Any + +from aioaquacell import ApiException, AquacellApi, AuthenticationFailed +import voluptuous as vol + +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult +from homeassistant.const import CONF_EMAIL, CONF_PASSWORD +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .const import CONF_REFRESH_TOKEN, CONF_REFRESH_TOKEN_CREATION_TIME, DOMAIN + +_LOGGER = logging.getLogger(__name__) + +DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_EMAIL): str, + vol.Required(CONF_PASSWORD): str, + } +) + + +class AquaCellConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a config flow for Aquacell.""" + + VERSION = 1 + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle the initial step.""" + errors: dict[str, str] = {} + if user_input is not None: + await self.async_set_unique_id( + user_input[CONF_EMAIL].lower(), raise_on_progress=False + ) + self._abort_if_unique_id_configured() + + session = async_get_clientsession(self.hass) + api = AquacellApi(session) + try: + refresh_token = await api.authenticate( + user_input[CONF_EMAIL], user_input[CONF_PASSWORD] + ) + except ApiException: + errors["base"] = "cannot_connect" + except AuthenticationFailed: + errors["base"] = "invalid_auth" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + return self.async_create_entry( + title=user_input[CONF_EMAIL], + data={ + **user_input, + CONF_REFRESH_TOKEN: refresh_token, + CONF_REFRESH_TOKEN_CREATION_TIME: datetime.now().timestamp(), + }, + ) + + return self.async_show_form( + step_id="user", + data_schema=DATA_SCHEMA, + errors=errors, + ) diff --git a/homeassistant/components/aquacell/const.py b/homeassistant/components/aquacell/const.py new file mode 100644 index 00000000000..96568d2286b --- /dev/null +++ b/homeassistant/components/aquacell/const.py @@ -0,0 +1,12 @@ +"""Constants for the Aquacell integration.""" + +from datetime import timedelta + +DOMAIN = "aquacell" +DATA_AQUACELL = "DATA_AQUACELL" + +CONF_REFRESH_TOKEN = "refresh_token" +CONF_REFRESH_TOKEN_CREATION_TIME = "refresh_token_creation_time" + +REFRESH_TOKEN_EXPIRY_TIME = timedelta(days=30) +UPDATE_INTERVAL = timedelta(days=1) diff --git a/homeassistant/components/aquacell/coordinator.py b/homeassistant/components/aquacell/coordinator.py new file mode 100644 index 00000000000..dd5dfcd2d0d --- /dev/null +++ b/homeassistant/components/aquacell/coordinator.py @@ -0,0 +1,90 @@ +"""Coordinator to update data from Aquacell API.""" + +import asyncio +from datetime import datetime +import logging + +from aioaquacell import ( + AquacellApi, + AquacellApiException, + AuthenticationFailed, + Softener, +) + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_EMAIL, CONF_PASSWORD +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryError +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import ( + CONF_REFRESH_TOKEN, + CONF_REFRESH_TOKEN_CREATION_TIME, + REFRESH_TOKEN_EXPIRY_TIME, + UPDATE_INTERVAL, +) + +_LOGGER = logging.getLogger(__name__) + + +class AquacellCoordinator(DataUpdateCoordinator[dict[str, Softener]]): + """My aquacell coordinator.""" + + config_entry: ConfigEntry + + def __init__(self, hass: HomeAssistant, aquacell_api: AquacellApi) -> None: + """Initialize coordinator.""" + super().__init__( + hass, + _LOGGER, + name="Aquacell Coordinator", + update_interval=UPDATE_INTERVAL, + ) + + self.refresh_token = self.config_entry.data[CONF_REFRESH_TOKEN] + self.refresh_token_creation_time = self.config_entry.data[ + CONF_REFRESH_TOKEN_CREATION_TIME + ] + self.email = self.config_entry.data[CONF_EMAIL] + self.password = self.config_entry.data[CONF_PASSWORD] + self.aquacell_api = aquacell_api + + async def _async_update_data(self) -> dict[str, Softener]: + """Fetch data from API endpoint. + + This is the place to pre-process the data to lookup tables + so entities can quickly look up their data. + """ + + async with asyncio.timeout(10): + # Check if the refresh token is expired + expiry_time = ( + self.refresh_token_creation_time + + REFRESH_TOKEN_EXPIRY_TIME.total_seconds() + ) + try: + if datetime.now().timestamp() >= expiry_time: + await self._reauthenticate() + else: + await self.aquacell_api.authenticate_refresh(self.refresh_token) + _LOGGER.debug("Logged in using: %s", self.refresh_token) + + softeners = await self.aquacell_api.get_all_softeners() + except AuthenticationFailed as err: + raise ConfigEntryError from err + except AquacellApiException as err: + raise UpdateFailed(f"Error communicating with API: {err}") from err + + return {softener.dsn: softener for softener in softeners} + + async def _reauthenticate(self) -> None: + _LOGGER.debug("Attempting to renew refresh token") + refresh_token = await self.aquacell_api.authenticate(self.email, self.password) + self.refresh_token = refresh_token + data = { + **self.config_entry.data, + CONF_REFRESH_TOKEN: self.refresh_token, + CONF_REFRESH_TOKEN_CREATION_TIME: datetime.now().timestamp(), + } + + self.hass.config_entries.async_update_entry(self.config_entry, data=data) diff --git a/homeassistant/components/aquacell/entity.py b/homeassistant/components/aquacell/entity.py new file mode 100644 index 00000000000..6c746ded24c --- /dev/null +++ b/homeassistant/components/aquacell/entity.py @@ -0,0 +1,41 @@ +"""Aquacell entity.""" + +from aioaquacell import Softener + +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN +from .coordinator import AquacellCoordinator + + +class AquacellEntity(CoordinatorEntity[AquacellCoordinator]): + """Representation of an aquacell entity.""" + + _attr_has_entity_name = True + + def __init__( + self, + coordinator: AquacellCoordinator, + softener_key: str, + entity_key: str, + ) -> None: + """Initialize the aquacell entity.""" + super().__init__(coordinator) + + self.softener_key = softener_key + + self._attr_unique_id = f"{softener_key}-{entity_key}" + self._attr_device_info = DeviceInfo( + name=self.softener.name, + hw_version=self.softener.fwVersion, + identifiers={(DOMAIN, str(softener_key))}, + manufacturer=self.softener.brand, + model=self.softener.ssn, + serial_number=softener_key, + ) + + @property + def softener(self) -> Softener: + """Handle updated data from the coordinator.""" + return self.coordinator.data[self.softener_key] diff --git a/homeassistant/components/aquacell/icons.json b/homeassistant/components/aquacell/icons.json new file mode 100644 index 00000000000..d7383f54d72 --- /dev/null +++ b/homeassistant/components/aquacell/icons.json @@ -0,0 +1,20 @@ +{ + "entity": { + "sensor": { + "salt_left_side_percentage": { + "default": "mdi:basket-fill" + }, + "salt_right_side_percentage": { + "default": "mdi:basket-fill" + }, + "wi_fi_strength": { + "default": "mdi:wifi", + "state": { + "low": "mdi:wifi-strength-1", + "medium": "mdi:wifi-strength-2", + "high": "mdi:wifi-strength-4" + } + } + } + } +} diff --git a/homeassistant/components/aquacell/manifest.json b/homeassistant/components/aquacell/manifest.json new file mode 100644 index 00000000000..1f43fa214d3 --- /dev/null +++ b/homeassistant/components/aquacell/manifest.json @@ -0,0 +1,12 @@ +{ + "domain": "aquacell", + "name": "Aquacell", + "codeowners": ["@Jordi1990"], + "config_flow": true, + "dependencies": ["http", "network"], + "documentation": "https://www.home-assistant.io/integrations/aquacell", + "integration_type": "device", + "iot_class": "cloud_polling", + "loggers": ["aioaquacell"], + "requirements": ["aioaquacell==0.1.7"] +} diff --git a/homeassistant/components/aquacell/sensor.py b/homeassistant/components/aquacell/sensor.py new file mode 100644 index 00000000000..702d75a0215 --- /dev/null +++ b/homeassistant/components/aquacell/sensor.py @@ -0,0 +1,117 @@ +"""Sensors exposing properties of the softener device.""" + +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass + +from aioaquacell import Softener + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.const import PERCENTAGE, UnitOfTime +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import StateType + +from . import AquacellConfigEntry +from .coordinator import AquacellCoordinator +from .entity import AquacellEntity + +PARALLEL_UPDATES = 1 + + +@dataclass(frozen=True, kw_only=True) +class SoftenerSensorEntityDescription(SensorEntityDescription): + """Describes Softener sensor entity.""" + + value_fn: Callable[[Softener], StateType] + + +SENSORS: tuple[SoftenerSensorEntityDescription, ...] = ( + SoftenerSensorEntityDescription( + key="salt_left_side_percentage", + translation_key="salt_left_side_percentage", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=PERCENTAGE, + value_fn=lambda softener: softener.salt.leftPercent, + ), + SoftenerSensorEntityDescription( + key="salt_right_side_percentage", + translation_key="salt_right_side_percentage", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=PERCENTAGE, + value_fn=lambda softener: softener.salt.rightPercent, + ), + SoftenerSensorEntityDescription( + key="salt_left_side_time_remaining", + translation_key="salt_left_side_time_remaining", + device_class=SensorDeviceClass.DURATION, + native_unit_of_measurement=UnitOfTime.DAYS, + value_fn=lambda softener: softener.salt.leftDays, + ), + SoftenerSensorEntityDescription( + key="salt_right_side_time_remaining", + translation_key="salt_right_side_time_remaining", + device_class=SensorDeviceClass.DURATION, + native_unit_of_measurement=UnitOfTime.DAYS, + value_fn=lambda softener: softener.salt.rightDays, + ), + SoftenerSensorEntityDescription( + key="battery", + device_class=SensorDeviceClass.BATTERY, + native_unit_of_measurement=PERCENTAGE, + value_fn=lambda softener: softener.battery, + ), + SoftenerSensorEntityDescription( + key="wi_fi_strength", + translation_key="wi_fi_strength", + value_fn=lambda softener: softener.wifiLevel, + device_class=SensorDeviceClass.ENUM, + options=[ + "high", + "medium", + "low", + ], + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: AquacellConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the sensors.""" + softeners = config_entry.runtime_data.data + async_add_entities( + SoftenerSensor(config_entry.runtime_data, sensor, softener_key) + for sensor in SENSORS + for softener_key in softeners + ) + + +class SoftenerSensor(AquacellEntity, SensorEntity): + """Softener sensor.""" + + entity_description: SoftenerSensorEntityDescription + + def __init__( + self, + coordinator: AquacellCoordinator, + description: SoftenerSensorEntityDescription, + softener_key: str, + ) -> None: + """Pass coordinator to CoordinatorEntity.""" + super().__init__(coordinator, softener_key, description.key) + + self.entity_description = description + + @property + def native_value(self) -> StateType: + """Return the state of the sensor.""" + return self.entity_description.value_fn(self.softener) diff --git a/homeassistant/components/aquacell/strings.json b/homeassistant/components/aquacell/strings.json new file mode 100644 index 00000000000..32b6bba943a --- /dev/null +++ b/homeassistant/components/aquacell/strings.json @@ -0,0 +1,45 @@ +{ + "config": { + "step": { + "user": { + "description": "Fill in your Aquacell mobile app credentials", + "data": { + "email": "[%key:common::config_flow::data::email%]", + "password": "[%key:common::config_flow::data::password%]" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + }, + "entity": { + "sensor": { + "salt_left_side_percentage": { + "name": "Salt left side percentage" + }, + "salt_right_side_percentage": { + "name": "Salt right side percentage" + }, + "salt_left_side_time_remaining": { + "name": "Salt left side time remaining" + }, + "salt_right_side_time_remaining": { + "name": "Salt right side time remaining" + }, + "wi_fi_strength": { + "name": "Wi-Fi strength", + "state": { + "low": "Low", + "medium": "Medium", + "high": "High" + } + } + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index d6060a360b5..745bad093d2 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -55,6 +55,7 @@ FLOWS = { "apple_tv", "aprilaire", "apsystems", + "aquacell", "aranet", "arcam_fmj", "arve", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 0665ba30351..9d7ffca6246 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -414,6 +414,12 @@ "config_flow": true, "iot_class": "local_polling" }, + "aquacell": { + "name": "Aquacell", + "integration_type": "device", + "config_flow": true, + "iot_class": "cloud_polling" + }, "aqualogic": { "name": "AquaLogic", "integration_type": "hub", diff --git a/requirements_all.txt b/requirements_all.txt index 0b016e1ceca..86dc53bc5a0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -191,6 +191,9 @@ aioambient==2024.01.0 # homeassistant.components.apcupsd aioapcaccess==0.4.2 +# homeassistant.components.aquacell +aioaquacell==0.1.7 + # homeassistant.components.aseko_pool_live aioaseko==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index de5a135ee24..79ae24f8edc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -170,6 +170,9 @@ aioambient==2024.01.0 # homeassistant.components.apcupsd aioapcaccess==0.4.2 +# homeassistant.components.aquacell +aioaquacell==0.1.7 + # homeassistant.components.aseko_pool_live aioaseko==0.1.1 diff --git a/tests/components/aquacell/__init__.py b/tests/components/aquacell/__init__.py new file mode 100644 index 00000000000..c54bc539496 --- /dev/null +++ b/tests/components/aquacell/__init__.py @@ -0,0 +1,33 @@ +"""Tests for the Aquacell integration.""" + +from homeassistant.components.aquacell.const import ( + CONF_REFRESH_TOKEN, + CONF_REFRESH_TOKEN_CREATION_TIME, +) +from homeassistant.const import CONF_EMAIL, CONF_PASSWORD +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + +TEST_CONFIG_ENTRY = { + CONF_EMAIL: "test@test.com", + CONF_PASSWORD: "test-password", + CONF_REFRESH_TOKEN: "refresh-token", + CONF_REFRESH_TOKEN_CREATION_TIME: 0, +} + +TEST_USER_INPUT = { + CONF_EMAIL: "test@test.com", + CONF_PASSWORD: "test-password", +} + +DSN = "DSN" + + +async def setup_integration(hass: HomeAssistant, config_entry: MockConfigEntry) -> None: + """Fixture for setting up the component.""" + config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(config_entry.entry_id) + + await hass.async_block_till_done() diff --git a/tests/components/aquacell/conftest.py b/tests/components/aquacell/conftest.py new file mode 100644 index 00000000000..0d0949aee2a --- /dev/null +++ b/tests/components/aquacell/conftest.py @@ -0,0 +1,77 @@ +"""Common fixtures for the Aquacell tests.""" + +from collections.abc import Generator +from datetime import datetime +from unittest.mock import AsyncMock, patch + +from aioaquacell import AquacellApi, Softener +import pytest + +from homeassistant.components.aquacell.const import ( + CONF_REFRESH_TOKEN_CREATION_TIME, + DOMAIN, +) +from homeassistant.const import CONF_EMAIL + +from tests.common import MockConfigEntry, load_json_array_fixture +from tests.components.aquacell import TEST_CONFIG_ENTRY + + +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock, None, None]: + """Override async_setup_entry.""" + with patch( + "homeassistant.components.aquacell.async_setup_entry", return_value=True + ) as mock_setup_entry: + yield mock_setup_entry + + +@pytest.fixture +def mock_aquacell_api() -> Generator[AsyncMock, None, None]: + """Build a fixture for the Aquacell API that authenticates successfully and returns a single softener.""" + with ( + patch( + "homeassistant.components.aquacell.AquacellApi", + autospec=True, + ) as mock_client, + patch( + "homeassistant.components.aquacell.config_flow.AquacellApi", + new=mock_client, + ), + ): + mock_aquacell_api: AquacellApi = mock_client.return_value + mock_aquacell_api.authenticate.return_value = "refresh-token" + + softeners_dict = load_json_array_fixture( + "aquacell/get_all_softeners_one_softener.json" + ) + + softeners = [Softener(softener) for softener in softeners_dict] + mock_aquacell_api.get_all_softeners.return_value = softeners + + yield mock_aquacell_api + + +@pytest.fixture +def mock_config_entry_expired() -> MockConfigEntry: + """Mock a config entry.""" + return MockConfigEntry( + domain=DOMAIN, + title="Aquacell", + unique_id=TEST_CONFIG_ENTRY[CONF_EMAIL], + data=TEST_CONFIG_ENTRY, + ) + + +@pytest.fixture +def mock_config_entry() -> MockConfigEntry: + """Mock a config entry.""" + return MockConfigEntry( + domain=DOMAIN, + title="Aquacell", + unique_id=TEST_CONFIG_ENTRY[CONF_EMAIL], + data={ + **TEST_CONFIG_ENTRY, + CONF_REFRESH_TOKEN_CREATION_TIME: datetime.now().timestamp(), + }, + ) diff --git a/tests/components/aquacell/fixtures/get_all_softeners_one_softener.json b/tests/components/aquacell/fixtures/get_all_softeners_one_softener.json new file mode 100644 index 00000000000..c8c61011c99 --- /dev/null +++ b/tests/components/aquacell/fixtures/get_all_softeners_one_softener.json @@ -0,0 +1,40 @@ +[ + { + "halfLevelNotificationEnabled": false, + "thresholds": {}, + "on_boarding_date": 1672751375085, + "dummy": "D", + "name": "AquaCell name", + "ssn": "SSN", + "dsn": "DSN", + "salt": { + "leftPercent": 100, + "rightPercent": 100, + "leftDays": 30, + "rightDays": 30, + "leftBlocks": 2, + "rightBlocks": 2, + "daysLeft": 30 + }, + "wifiLevel": "high", + "fwVersion": "HSWS 1.0 v1.0 Apr 16 2021 15:10:32", + "lastUpdate": 1715327070000, + "battery": 40, + "lidInPlace": true, + "buzzerNotificationEnabled": false, + "brand": "harvey", + "numberOfPeople": 1, + "location": { + "address": "address", + "postcode": "postal", + "country": "country" + }, + "dealer": { + "website": "", + "dealerId": "", + "shop": {}, + "name": "", + "support": {} + } + } +] diff --git a/tests/components/aquacell/snapshots/test_sensor.ambr b/tests/components/aquacell/snapshots/test_sensor.ambr new file mode 100644 index 00000000000..a237f59881a --- /dev/null +++ b/tests/components/aquacell/snapshots/test_sensor.ambr @@ -0,0 +1,303 @@ +# serializer version: 1 +# name: test_sensors[sensor.aquacell_name_battery-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.aquacell_name_battery', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery', + 'platform': 'aquacell', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'DSN-battery', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensors[sensor.aquacell_name_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'AquaCell name Battery', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.aquacell_name_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '40', + }) +# --- +# name: test_sensors[sensor.aquacell_name_salt_left_side_percentage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.aquacell_name_salt_left_side_percentage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Salt left side percentage', + 'platform': 'aquacell', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'salt_left_side_percentage', + 'unique_id': 'DSN-salt_left_side_percentage', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensors[sensor.aquacell_name_salt_left_side_percentage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'AquaCell name Salt left side percentage', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.aquacell_name_salt_left_side_percentage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '100', + }) +# --- +# name: test_sensors[sensor.aquacell_name_salt_left_side_time_remaining-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.aquacell_name_salt_left_side_time_remaining', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Salt left side time remaining', + 'platform': 'aquacell', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'salt_left_side_time_remaining', + 'unique_id': 'DSN-salt_left_side_time_remaining', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.aquacell_name_salt_left_side_time_remaining-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'duration', + 'friendly_name': 'AquaCell name Salt left side time remaining', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.aquacell_name_salt_left_side_time_remaining', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '30', + }) +# --- +# name: test_sensors[sensor.aquacell_name_salt_right_side_percentage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.aquacell_name_salt_right_side_percentage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Salt right side percentage', + 'platform': 'aquacell', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'salt_right_side_percentage', + 'unique_id': 'DSN-salt_right_side_percentage', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensors[sensor.aquacell_name_salt_right_side_percentage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'AquaCell name Salt right side percentage', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.aquacell_name_salt_right_side_percentage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '100', + }) +# --- +# name: test_sensors[sensor.aquacell_name_salt_right_side_time_remaining-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.aquacell_name_salt_right_side_time_remaining', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Salt right side time remaining', + 'platform': 'aquacell', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'salt_right_side_time_remaining', + 'unique_id': 'DSN-salt_right_side_time_remaining', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.aquacell_name_salt_right_side_time_remaining-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'duration', + 'friendly_name': 'AquaCell name Salt right side time remaining', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.aquacell_name_salt_right_side_time_remaining', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '30', + }) +# --- +# name: test_sensors[sensor.aquacell_name_wi_fi_strength-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'high', + 'medium', + 'low', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.aquacell_name_wi_fi_strength', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Wi-Fi strength', + 'platform': 'aquacell', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'wi_fi_strength', + 'unique_id': 'DSN-wi_fi_strength', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.aquacell_name_wi_fi_strength-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'AquaCell name Wi-Fi strength', + 'options': list([ + 'high', + 'medium', + 'low', + ]), + }), + 'context': , + 'entity_id': 'sensor.aquacell_name_wi_fi_strength', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'high', + }) +# --- diff --git a/tests/components/aquacell/test_config_flow.py b/tests/components/aquacell/test_config_flow.py new file mode 100644 index 00000000000..7e348c47c78 --- /dev/null +++ b/tests/components/aquacell/test_config_flow.py @@ -0,0 +1,111 @@ +"""Test the Aquacell config flow.""" + +from unittest.mock import AsyncMock + +from aioaquacell import ApiException, AuthenticationFailed +import pytest + +from homeassistant.components.aquacell.const import CONF_REFRESH_TOKEN, DOMAIN +from homeassistant.config_entries import SOURCE_USER +from homeassistant.const import CONF_EMAIL, CONF_PASSWORD +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +from tests.common import MockConfigEntry +from tests.components.aquacell import TEST_CONFIG_ENTRY, TEST_USER_INPUT + + +async def test_config_flow_already_configured(hass: HomeAssistant) -> None: + """Test already configured.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={ + **TEST_CONFIG_ENTRY, + }, + unique_id=TEST_CONFIG_ENTRY[CONF_EMAIL], + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + TEST_USER_INPUT, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +async def test_full_flow( + hass: HomeAssistant, mock_setup_entry: AsyncMock, mock_aquacell_api: AsyncMock +) -> None: + """Test the full config flow.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {} + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + TEST_USER_INPUT, + ) + await hass.async_block_till_done() + + assert result2["type"] is FlowResultType.CREATE_ENTRY + assert result2["title"] == TEST_CONFIG_ENTRY[CONF_EMAIL] + assert result2["data"][CONF_EMAIL] == TEST_CONFIG_ENTRY[CONF_EMAIL] + assert result2["data"][CONF_PASSWORD] == TEST_CONFIG_ENTRY[CONF_PASSWORD] + assert result2["data"][CONF_REFRESH_TOKEN] == TEST_CONFIG_ENTRY[CONF_REFRESH_TOKEN] + assert len(mock_setup_entry.mock_calls) == 1 + + +@pytest.mark.parametrize( + ("exception", "error"), + [ + (ApiException, "cannot_connect"), + (AuthenticationFailed, "invalid_auth"), + (Exception, "unknown"), + ], +) +async def test_form_exceptions( + hass: HomeAssistant, + exception: Exception, + error: str, + mock_setup_entry: AsyncMock, + mock_aquacell_api: AsyncMock, +) -> None: + """Test we handle form exceptions.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + mock_aquacell_api.authenticate.side_effect = exception + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], TEST_USER_INPUT + ) + + assert result2["type"] is FlowResultType.FORM + assert result2["errors"] == {"base": error} + + mock_aquacell_api.authenticate.side_effect = None + + result3 = await hass.config_entries.flow.async_configure( + result["flow_id"], + TEST_USER_INPUT, + ) + await hass.async_block_till_done() + + assert result3["type"] is FlowResultType.CREATE_ENTRY + assert result3["title"] == TEST_CONFIG_ENTRY[CONF_EMAIL] + assert result3["data"][CONF_EMAIL] == TEST_CONFIG_ENTRY[CONF_EMAIL] + assert result3["data"][CONF_PASSWORD] == TEST_CONFIG_ENTRY[CONF_PASSWORD] + assert result3["data"][CONF_REFRESH_TOKEN] == TEST_CONFIG_ENTRY[CONF_REFRESH_TOKEN] + assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/aquacell/test_init.py b/tests/components/aquacell/test_init.py new file mode 100644 index 00000000000..215b50719be --- /dev/null +++ b/tests/components/aquacell/test_init.py @@ -0,0 +1,102 @@ +"""Test the Aquacell init module.""" + +from __future__ import annotations + +from datetime import datetime +from unittest.mock import AsyncMock, patch + +from aioaquacell import AquacellApiException, AuthenticationFailed +import pytest + +from homeassistant.components.aquacell.const import ( + CONF_REFRESH_TOKEN, + CONF_REFRESH_TOKEN_CREATION_TIME, + DOMAIN, +) +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry +from tests.components.aquacell import setup_integration + + +async def test_load_unload_entry( + hass: HomeAssistant, + mock_aquacell_api: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test load and unload entry.""" + await setup_integration(hass, mock_config_entry) + entry = hass.config_entries.async_entries(DOMAIN)[0] + + assert entry.state is ConfigEntryState.LOADED + + await hass.config_entries.async_remove(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state is ConfigEntryState.NOT_LOADED + + +async def test_coordinator_update_valid_refresh_token( + hass: HomeAssistant, + mock_aquacell_api: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test load and unload entry.""" + await setup_integration(hass, mock_config_entry) + entry = hass.config_entries.async_entries(DOMAIN)[0] + + assert entry.state is ConfigEntryState.LOADED + + assert len(mock_aquacell_api.authenticate.mock_calls) == 0 + assert len(mock_aquacell_api.authenticate_refresh.mock_calls) == 1 + assert len(mock_aquacell_api.get_all_softeners.mock_calls) == 1 + + +async def test_coordinator_update_expired_refresh_token( + hass: HomeAssistant, + mock_aquacell_api: AsyncMock, + mock_config_entry_expired: MockConfigEntry, +) -> None: + """Test load and unload entry.""" + mock_aquacell_api.authenticate.return_value = "new-refresh-token" + + now = datetime.now() + with patch( + "homeassistant.components.aquacell.coordinator.datetime" + ) as datetime_mock: + datetime_mock.now.return_value = now + await setup_integration(hass, mock_config_entry_expired) + + entry = hass.config_entries.async_entries(DOMAIN)[0] + + assert entry.state is ConfigEntryState.LOADED + + assert len(mock_aquacell_api.authenticate.mock_calls) == 1 + assert len(mock_aquacell_api.authenticate_refresh.mock_calls) == 0 + assert len(mock_aquacell_api.get_all_softeners.mock_calls) == 1 + + assert entry.data[CONF_REFRESH_TOKEN] == "new-refresh-token" + assert entry.data[CONF_REFRESH_TOKEN_CREATION_TIME] == now.timestamp() + + +@pytest.mark.parametrize( + ("exception", "expected_state"), + [ + (AuthenticationFailed, ConfigEntryState.SETUP_ERROR), + (AquacellApiException, ConfigEntryState.SETUP_RETRY), + ], +) +async def test_load_exceptions( + hass: HomeAssistant, + mock_aquacell_api: AsyncMock, + mock_config_entry: MockConfigEntry, + exception: Exception, + expected_state: ConfigEntryState, +) -> None: + """Test load and unload entry.""" + mock_aquacell_api.authenticate_refresh.side_effect = exception + await setup_integration(hass, mock_config_entry) + entry = hass.config_entries.async_entries(DOMAIN)[0] + + assert entry.state is expected_state diff --git a/tests/components/aquacell/test_sensor.py b/tests/components/aquacell/test_sensor.py new file mode 100644 index 00000000000..8c52c3caa1f --- /dev/null +++ b/tests/components/aquacell/test_sensor.py @@ -0,0 +1,25 @@ +"""Test the Aquacell init module.""" + +from __future__ import annotations + +from unittest.mock import AsyncMock + +from syrupy import SnapshotAssertion + +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from tests.common import MockConfigEntry, snapshot_platform +from tests.components.aquacell import setup_integration + + +async def test_sensors( + hass: HomeAssistant, + mock_aquacell_api: AsyncMock, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, +) -> None: + """Test the creation of Aquacell sensors.""" + await setup_integration(hass, mock_config_entry) + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) From a7429e5f50a7b2de867af4b57533a37098f869e3 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Thu, 6 Jun 2024 22:40:04 +0200 Subject: [PATCH 0316/1445] Fix KNX `climate.set_hvac_mode` not turning `on` (#119012) --- homeassistant/components/knx/climate.py | 5 +---- tests/components/knx/test_climate.py | 10 +++++++++- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/knx/climate.py b/homeassistant/components/knx/climate.py index 674e76d66e3..e1179641cdc 100644 --- a/homeassistant/components/knx/climate.py +++ b/homeassistant/components/knx/climate.py @@ -283,16 +283,13 @@ class KNXClimate(KnxEntity, ClimateEntity): ) if knx_controller_mode in self._device.mode.controller_modes: await self._device.mode.set_controller_mode(knx_controller_mode) - self.async_write_ha_state() - return if self._device.supports_on_off: if hvac_mode == HVACMode.OFF: await self._device.turn_off() elif not self._device.is_on: - # for default hvac mode, otherwise above would have triggered await self._device.turn_on() - self.async_write_ha_state() + self.async_write_ha_state() @property def preset_mode(self) -> str | None: diff --git a/tests/components/knx/test_climate.py b/tests/components/knx/test_climate.py index 3b286a0cdb9..9c431386b43 100644 --- a/tests/components/knx/test_climate.py +++ b/tests/components/knx/test_climate.py @@ -128,6 +128,7 @@ async def test_climate_on_off( blocking=True, ) await knx.assert_write(on_off_ga, 0) + assert hass.states.get("climate.test").state == "off" # set hvac mode to heat await hass.services.async_call( @@ -137,10 +138,11 @@ async def test_climate_on_off( blocking=True, ) if heat_cool_ga: - # only set new hvac_mode without changing on/off - actuator shall handle that await knx.assert_write(heat_cool_ga, 1) + await knx.assert_write(on_off_ga, 1) else: await knx.assert_write(on_off_ga, 1) + assert hass.states.get("climate.test").state == "heat" @pytest.mark.parametrize("on_off_ga", [None, "4/4/4"]) @@ -190,6 +192,9 @@ async def test_climate_hvac_mode( blocking=True, ) await knx.assert_write(controller_mode_ga, (0x06,)) + if on_off_ga: + await knx.assert_write(on_off_ga, 0) + assert hass.states.get("climate.test").state == "off" # set hvac to non default mode await hass.services.async_call( @@ -199,6 +204,9 @@ async def test_climate_hvac_mode( blocking=True, ) await knx.assert_write(controller_mode_ga, (0x03,)) + if on_off_ga: + await knx.assert_write(on_off_ga, 1) + assert hass.states.get("climate.test").state == "cool" # turn off await hass.services.async_call( From 7337c1374745844110e37114c0d2d7f7cf4153af Mon Sep 17 00:00:00 2001 From: Rami Mosleh Date: Fri, 7 Jun 2024 02:44:21 +0400 Subject: [PATCH 0317/1445] Use torrent id to identify torrents that should trigger events (#118897) --- .../components/transmission/coordinator.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/transmission/coordinator.py b/homeassistant/components/transmission/coordinator.py index 1c379685c1c..d6b5b695656 100644 --- a/homeassistant/components/transmission/coordinator.py +++ b/homeassistant/components/transmission/coordinator.py @@ -93,16 +93,14 @@ class TransmissionDataUpdateCoordinator(DataUpdateCoordinator[SessionStats]): def check_completed_torrent(self) -> None: """Get completed torrent functionality.""" - old_completed_torrent_names = { - torrent.name for torrent in self._completed_torrents - } + old_completed_torrents = {torrent.id for torrent in self._completed_torrents} current_completed_torrents = [ torrent for torrent in self.torrents if torrent.status == "seeding" ] for torrent in current_completed_torrents: - if torrent.name not in old_completed_torrent_names: + if torrent.id not in old_completed_torrents: self.hass.bus.fire( EVENT_DOWNLOADED_TORRENT, {"name": torrent.name, "id": torrent.id} ) @@ -111,14 +109,14 @@ class TransmissionDataUpdateCoordinator(DataUpdateCoordinator[SessionStats]): def check_started_torrent(self) -> None: """Get started torrent functionality.""" - old_started_torrent_names = {torrent.name for torrent in self._started_torrents} + old_started_torrents = {torrent.id for torrent in self._started_torrents} current_started_torrents = [ torrent for torrent in self.torrents if torrent.status == "downloading" ] for torrent in current_started_torrents: - if torrent.name not in old_started_torrent_names: + if torrent.id not in old_started_torrents: self.hass.bus.fire( EVENT_STARTED_TORRENT, {"name": torrent.name, "id": torrent.id} ) @@ -127,10 +125,10 @@ class TransmissionDataUpdateCoordinator(DataUpdateCoordinator[SessionStats]): def check_removed_torrent(self) -> None: """Get removed torrent functionality.""" - current_torrent_names = {torrent.name for torrent in self.torrents} + current_torrents = {torrent.id for torrent in self.torrents} for torrent in self._all_torrents: - if torrent.name not in current_torrent_names: + if torrent.id not in current_torrents: self.hass.bus.fire( EVENT_REMOVED_TORRENT, {"name": torrent.name, "id": torrent.id} ) From 8c025ea1f7f254251d5d63f436b6da27363c83e4 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Fri, 7 Jun 2024 00:48:23 +0200 Subject: [PATCH 0318/1445] Update gardena library to 1.4.2 (#119010) --- homeassistant/components/gardena_bluetooth/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/gardena_bluetooth/manifest.json b/homeassistant/components/gardena_bluetooth/manifest.json index 6598aeaafd8..1e3ef156d72 100644 --- a/homeassistant/components/gardena_bluetooth/manifest.json +++ b/homeassistant/components/gardena_bluetooth/manifest.json @@ -13,5 +13,5 @@ "dependencies": ["bluetooth_adapters"], "documentation": "https://www.home-assistant.io/integrations/gardena_bluetooth", "iot_class": "local_polling", - "requirements": ["gardena-bluetooth==1.4.1"] + "requirements": ["gardena-bluetooth==1.4.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 86dc53bc5a0..27751e09c8e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -915,7 +915,7 @@ fyta_cli==0.4.1 gTTS==2.2.4 # homeassistant.components.gardena_bluetooth -gardena-bluetooth==1.4.1 +gardena-bluetooth==1.4.2 # homeassistant.components.google_assistant_sdk gassist-text==0.0.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 79ae24f8edc..416b836a329 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -750,7 +750,7 @@ fyta_cli==0.4.1 gTTS==2.2.4 # homeassistant.components.gardena_bluetooth -gardena-bluetooth==1.4.1 +gardena-bluetooth==1.4.2 # homeassistant.components.google_assistant_sdk gassist-text==0.0.11 From 87114bf19b97df930e47a09e106c3a830cc3c1fb Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Thu, 6 Jun 2024 20:41:25 -0500 Subject: [PATCH 0319/1445] Fix exposure checks on some intents (#118988) * Check exposure in climate intent * Check exposure in todo list * Check exposure for weather * Check exposure in humidity intents * Add extra checks to weather tests * Add more checks to todo intent test * Move climate intents to async_match_targets * Update test_intent.py * Update test_intent.py * Remove patch --- homeassistant/components/climate/intent.py | 90 ++------ homeassistant/components/humidifier/intent.py | 45 ++-- homeassistant/components/todo/intent.py | 20 +- homeassistant/components/weather/intent.py | 52 ++--- homeassistant/helpers/intent.py | 2 + tests/components/climate/test_intent.py | 203 +++++++++++++++--- tests/components/humidifier/test_intent.py | 128 ++++++++++- tests/components/todo/test_init.py | 42 +++- tests/components/weather/test_intent.py | 76 ++++--- 9 files changed, 453 insertions(+), 205 deletions(-) diff --git a/homeassistant/components/climate/intent.py b/homeassistant/components/climate/intent.py index 48b5c134bbd..53d0891fcda 100644 --- a/homeassistant/components/climate/intent.py +++ b/homeassistant/components/climate/intent.py @@ -4,11 +4,10 @@ from __future__ import annotations import voluptuous as vol -from homeassistant.core import HomeAssistant, State +from homeassistant.core import HomeAssistant from homeassistant.helpers import intent -from homeassistant.helpers.entity_component import EntityComponent -from . import DOMAIN, ClimateEntity +from . import DOMAIN INTENT_GET_TEMPERATURE = "HassClimateGetTemperature" @@ -23,7 +22,10 @@ class GetTemperatureIntent(intent.IntentHandler): intent_type = INTENT_GET_TEMPERATURE description = "Gets the current temperature of a climate device or entity" - slot_schema = {vol.Optional("area"): str, vol.Optional("name"): str} + slot_schema = { + vol.Optional("area"): intent.non_empty_string, + vol.Optional("name"): intent.non_empty_string, + } platforms = {DOMAIN} async def async_handle(self, intent_obj: intent.Intent) -> intent.IntentResponse: @@ -31,74 +33,24 @@ class GetTemperatureIntent(intent.IntentHandler): hass = intent_obj.hass slots = self.async_validate_slots(intent_obj.slots) - component: EntityComponent[ClimateEntity] = hass.data[DOMAIN] - entities: list[ClimateEntity] = list(component.entities) - climate_entity: ClimateEntity | None = None - climate_state: State | None = None + name: str | None = None + if "name" in slots: + name = slots["name"]["value"] - if not entities: - raise intent.IntentHandleError("No climate entities") + area: str | None = None + if "area" in slots: + area = slots["area"]["value"] - name_slot = slots.get("name", {}) - entity_name: str | None = name_slot.get("value") - entity_text: str | None = name_slot.get("text") - - area_slot = slots.get("area", {}) - area_id = area_slot.get("value") - - if area_id: - # Filter by area and optionally name - area_name = area_slot.get("text") - - for maybe_climate in intent.async_match_states( - hass, name=entity_name, area_name=area_id, domains=[DOMAIN] - ): - climate_state = maybe_climate - break - - if climate_state is None: - raise intent.NoStatesMatchedError( - reason=intent.MatchFailedReason.AREA, - name=entity_text or entity_name, - area=area_name or area_id, - floor=None, - domains={DOMAIN}, - device_classes=None, - ) - - climate_entity = component.get_entity(climate_state.entity_id) - elif entity_name: - # Filter by name - for maybe_climate in intent.async_match_states( - hass, name=entity_name, domains=[DOMAIN] - ): - climate_state = maybe_climate - break - - if climate_state is None: - raise intent.NoStatesMatchedError( - reason=intent.MatchFailedReason.NAME, - name=entity_name, - area=None, - floor=None, - domains={DOMAIN}, - device_classes=None, - ) - - climate_entity = component.get_entity(climate_state.entity_id) - else: - # First entity - climate_entity = entities[0] - climate_state = hass.states.get(climate_entity.entity_id) - - assert climate_entity is not None - - if climate_state is None: - raise intent.IntentHandleError(f"No state for {climate_entity.name}") - - assert climate_state is not None + match_constraints = intent.MatchTargetsConstraints( + name=name, area_name=area, domains=[DOMAIN], assistant=intent_obj.assistant + ) + match_result = intent.async_match_targets(hass, match_constraints) + if not match_result.is_match: + raise intent.MatchFailedError( + result=match_result, constraints=match_constraints + ) response = intent_obj.create_response() response.response_type = intent.IntentResponseType.QUERY_ANSWER - response.async_set_states(matched_states=[climate_state]) + response.async_set_states(matched_states=match_result.states) return response diff --git a/homeassistant/components/humidifier/intent.py b/homeassistant/components/humidifier/intent.py index c713f08b857..425fdbcc679 100644 --- a/homeassistant/components/humidifier/intent.py +++ b/homeassistant/components/humidifier/intent.py @@ -35,7 +35,7 @@ class HumidityHandler(intent.IntentHandler): intent_type = INTENT_HUMIDITY description = "Set desired humidity level" slot_schema = { - vol.Required("name"): cv.string, + vol.Required("name"): intent.non_empty_string, vol.Required("humidity"): vol.All(vol.Coerce(int), vol.Range(0, 100)), } platforms = {DOMAIN} @@ -44,18 +44,19 @@ class HumidityHandler(intent.IntentHandler): """Handle the hass intent.""" hass = intent_obj.hass slots = self.async_validate_slots(intent_obj.slots) - states = list( - intent.async_match_states( - hass, - name=slots["name"]["value"], - states=hass.states.async_all(DOMAIN), - ) + + match_constraints = intent.MatchTargetsConstraints( + name=slots["name"]["value"], + domains=[DOMAIN], + assistant=intent_obj.assistant, ) + match_result = intent.async_match_targets(hass, match_constraints) + if not match_result.is_match: + raise intent.MatchFailedError( + result=match_result, constraints=match_constraints + ) - if not states: - raise intent.IntentHandleError("No entities matched") - - state = states[0] + state = match_result.states[0] service_data = {ATTR_ENTITY_ID: state.entity_id} humidity = slots["humidity"]["value"] @@ -89,7 +90,7 @@ class SetModeHandler(intent.IntentHandler): intent_type = INTENT_MODE description = "Set humidifier mode" slot_schema = { - vol.Required("name"): cv.string, + vol.Required("name"): intent.non_empty_string, vol.Required("mode"): cv.string, } platforms = {DOMAIN} @@ -98,18 +99,18 @@ class SetModeHandler(intent.IntentHandler): """Handle the hass intent.""" hass = intent_obj.hass slots = self.async_validate_slots(intent_obj.slots) - states = list( - intent.async_match_states( - hass, - name=slots["name"]["value"], - states=hass.states.async_all(DOMAIN), - ) + match_constraints = intent.MatchTargetsConstraints( + name=slots["name"]["value"], + domains=[DOMAIN], + assistant=intent_obj.assistant, ) + match_result = intent.async_match_targets(hass, match_constraints) + if not match_result.is_match: + raise intent.MatchFailedError( + result=match_result, constraints=match_constraints + ) - if not states: - raise intent.IntentHandleError("No entities matched") - - state = states[0] + state = match_result.states[0] service_data = {ATTR_ENTITY_ID: state.entity_id} intent.async_test_feature(state, HumidifierEntityFeature.MODES, "modes") diff --git a/homeassistant/components/todo/intent.py b/homeassistant/components/todo/intent.py index c3c18ea304f..50afe916b27 100644 --- a/homeassistant/components/todo/intent.py +++ b/homeassistant/components/todo/intent.py @@ -4,7 +4,6 @@ from __future__ import annotations from homeassistant.core import HomeAssistant from homeassistant.helpers import intent -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_component import EntityComponent from . import DOMAIN, TodoItem, TodoItemStatus, TodoListEntity @@ -22,7 +21,7 @@ class ListAddItemIntent(intent.IntentHandler): intent_type = INTENT_LIST_ADD_ITEM description = "Add item to a todo list" - slot_schema = {"item": cv.string, "name": cv.string} + slot_schema = {"item": intent.non_empty_string, "name": intent.non_empty_string} platforms = {DOMAIN} async def async_handle(self, intent_obj: intent.Intent) -> intent.IntentResponse: @@ -37,18 +36,19 @@ class ListAddItemIntent(intent.IntentHandler): target_list: TodoListEntity | None = None # Find matching list - for list_state in intent.async_match_states( - hass, name=list_name, domains=[DOMAIN] - ): - target_list = component.get_entity(list_state.entity_id) - if target_list is not None: - break + match_constraints = intent.MatchTargetsConstraints( + name=list_name, domains=[DOMAIN], assistant=intent_obj.assistant + ) + match_result = intent.async_match_targets(hass, match_constraints) + if not match_result.is_match: + raise intent.MatchFailedError( + result=match_result, constraints=match_constraints + ) + target_list = component.get_entity(match_result.states[0].entity_id) if target_list is None: raise intent.IntentHandleError(f"No to-do list: {list_name}") - assert target_list is not None - # Add to list await target_list.async_create_todo_item( TodoItem(summary=item, status=TodoItemStatus.NEEDS_ACTION) diff --git a/homeassistant/components/weather/intent.py b/homeassistant/components/weather/intent.py index cbb46b943e8..e00a386b619 100644 --- a/homeassistant/components/weather/intent.py +++ b/homeassistant/components/weather/intent.py @@ -6,10 +6,8 @@ import voluptuous as vol from homeassistant.core import HomeAssistant, State from homeassistant.helpers import intent -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity_component import EntityComponent -from . import DOMAIN, WeatherEntity +from . import DOMAIN INTENT_GET_WEATHER = "HassGetWeather" @@ -24,7 +22,7 @@ class GetWeatherIntent(intent.IntentHandler): intent_type = INTENT_GET_WEATHER description = "Gets the current weather" - slot_schema = {vol.Optional("name"): cv.string} + slot_schema = {vol.Optional("name"): intent.non_empty_string} platforms = {DOMAIN} async def async_handle(self, intent_obj: intent.Intent) -> intent.IntentResponse: @@ -32,43 +30,21 @@ class GetWeatherIntent(intent.IntentHandler): hass = intent_obj.hass slots = self.async_validate_slots(intent_obj.slots) - weather: WeatherEntity | None = None weather_state: State | None = None - component: EntityComponent[WeatherEntity] = hass.data[DOMAIN] - entities = list(component.entities) - + name: str | None = None if "name" in slots: - # Named weather entity - weather_name = slots["name"]["value"] + name = slots["name"]["value"] - # Find matching weather entity - matching_states = intent.async_match_states( - hass, name=weather_name, domains=[DOMAIN] + match_constraints = intent.MatchTargetsConstraints( + name=name, domains=[DOMAIN], assistant=intent_obj.assistant + ) + match_result = intent.async_match_targets(hass, match_constraints) + if not match_result.is_match: + raise intent.MatchFailedError( + result=match_result, constraints=match_constraints ) - for maybe_weather_state in matching_states: - weather = component.get_entity(maybe_weather_state.entity_id) - if weather is not None: - weather_state = maybe_weather_state - break - if weather is None: - raise intent.IntentHandleError( - f"No weather entity named {weather_name}" - ) - elif entities: - # First weather entity - weather = entities[0] - weather_name = weather.name - weather_state = hass.states.get(weather.entity_id) - - if weather is None: - raise intent.IntentHandleError("No weather entity") - - if weather_state is None: - raise intent.IntentHandleError(f"No state for weather: {weather.name}") - - assert weather is not None - assert weather_state is not None + weather_state = match_result.states[0] # Create response response = intent_obj.create_response() @@ -77,8 +53,8 @@ class GetWeatherIntent(intent.IntentHandler): success_results=[ intent.IntentResponseTarget( type=intent.IntentResponseTargetType.ENTITY, - name=weather_name, - id=weather.entity_id, + name=weather_state.name, + id=weather_state.entity_id, ) ] ) diff --git a/homeassistant/helpers/intent.py b/homeassistant/helpers/intent.py index ccef934d6ad..d7c0f90e2f9 100644 --- a/homeassistant/helpers/intent.py +++ b/homeassistant/helpers/intent.py @@ -712,6 +712,7 @@ def async_match_states( domains: Collection[str] | None = None, device_classes: Collection[str] | None = None, states: list[State] | None = None, + assistant: str | None = None, ) -> Iterable[State]: """Simplified interface to async_match_targets that returns states matching the constraints.""" result = async_match_targets( @@ -722,6 +723,7 @@ def async_match_states( floor_name=floor_name, domains=domains, device_classes=device_classes, + assistant=assistant, ), states=states, ) diff --git a/tests/components/climate/test_intent.py b/tests/components/climate/test_intent.py index cc78d09ff06..ab1e3629ef8 100644 --- a/tests/components/climate/test_intent.py +++ b/tests/components/climate/test_intent.py @@ -1,21 +1,22 @@ """Test climate intents.""" -from unittest.mock import patch - import pytest from typing_extensions import Generator +from homeassistant.components import conversation from homeassistant.components.climate import ( DOMAIN, ClimateEntity, HVACMode, intent as climate_intent, ) +from homeassistant.components.homeassistant.exposed_entities import async_expose_entity from homeassistant.config_entries import ConfigEntry, ConfigFlow from homeassistant.const import Platform, UnitOfTemperature from homeassistant.core import HomeAssistant from homeassistant.helpers import area_registry as ar, entity_registry as er, intent from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.setup import async_setup_component from tests.common import ( MockConfigEntry, @@ -113,6 +114,7 @@ async def test_get_temperature( entity_registry: er.EntityRegistry, ) -> None: """Test HassClimateGetTemperature intent.""" + assert await async_setup_component(hass, "homeassistant", {}) await climate_intent.async_setup_intents(hass) climate_1 = MockClimateEntity() @@ -148,10 +150,14 @@ async def test_get_temperature( # First climate entity will be selected (no area) response = await intent.async_handle( - hass, "test", climate_intent.INTENT_GET_TEMPERATURE, {} + hass, + "test", + climate_intent.INTENT_GET_TEMPERATURE, + {}, + assistant=conversation.DOMAIN, ) assert response.response_type == intent.IntentResponseType.QUERY_ANSWER - assert len(response.matched_states) == 1 + assert response.matched_states assert response.matched_states[0].entity_id == climate_1.entity_id state = response.matched_states[0] assert state.attributes["current_temperature"] == 10.0 @@ -162,6 +168,7 @@ async def test_get_temperature( "test", climate_intent.INTENT_GET_TEMPERATURE, {"area": {"value": bedroom_area.name}}, + assistant=conversation.DOMAIN, ) assert response.response_type == intent.IntentResponseType.QUERY_ANSWER assert len(response.matched_states) == 1 @@ -175,6 +182,7 @@ async def test_get_temperature( "test", climate_intent.INTENT_GET_TEMPERATURE, {"name": {"value": "Climate 2"}}, + assistant=conversation.DOMAIN, ) assert response.response_type == intent.IntentResponseType.QUERY_ANSWER assert len(response.matched_states) == 1 @@ -189,6 +197,7 @@ async def test_get_temperature( "test", climate_intent.INTENT_GET_TEMPERATURE, {"area": {"value": office_area.name}}, + assistant=conversation.DOMAIN, ) # Exception should contain details of what we tried to match @@ -197,7 +206,7 @@ async def test_get_temperature( constraints = error.value.constraints assert constraints.name is None assert constraints.area_name == office_area.name - assert constraints.domains == {DOMAIN} + assert constraints.domains and (set(constraints.domains) == {DOMAIN}) assert constraints.device_classes is None # Check wrong name @@ -214,7 +223,7 @@ async def test_get_temperature( constraints = error.value.constraints assert constraints.name == "Does not exist" assert constraints.area_name is None - assert constraints.domains == {DOMAIN} + assert constraints.domains and (set(constraints.domains) == {DOMAIN}) assert constraints.device_classes is None # Check wrong name with area @@ -231,7 +240,7 @@ async def test_get_temperature( constraints = error.value.constraints assert constraints.name == "Climate 1" assert constraints.area_name == bedroom_area.name - assert constraints.domains == {DOMAIN} + assert constraints.domains and (set(constraints.domains) == {DOMAIN}) assert constraints.device_classes is None @@ -239,62 +248,190 @@ async def test_get_temperature_no_entities( hass: HomeAssistant, ) -> None: """Test HassClimateGetTemperature intent with no climate entities.""" + assert await async_setup_component(hass, "homeassistant", {}) await climate_intent.async_setup_intents(hass) await create_mock_platform(hass, []) - with pytest.raises(intent.IntentHandleError): + with pytest.raises(intent.MatchFailedError) as err: await intent.async_handle( - hass, "test", climate_intent.INTENT_GET_TEMPERATURE, {} + hass, + "test", + climate_intent.INTENT_GET_TEMPERATURE, + {}, + assistant=conversation.DOMAIN, ) + assert err.value.result.no_match_reason == intent.MatchFailedReason.DOMAIN -async def test_get_temperature_no_state( +async def test_not_exposed( hass: HomeAssistant, area_registry: ar.AreaRegistry, entity_registry: er.EntityRegistry, ) -> None: - """Test HassClimateGetTemperature intent when states are missing.""" + """Test HassClimateGetTemperature intent when entities aren't exposed.""" + assert await async_setup_component(hass, "homeassistant", {}) await climate_intent.async_setup_intents(hass) climate_1 = MockClimateEntity() climate_1._attr_name = "Climate 1" climate_1._attr_unique_id = "1234" + climate_1._attr_current_temperature = 10.0 entity_registry.async_get_or_create( DOMAIN, "test", "1234", suggested_object_id="climate_1" ) - await create_mock_platform(hass, [climate_1]) + climate_2 = MockClimateEntity() + climate_2._attr_name = "Climate 2" + climate_2._attr_unique_id = "5678" + climate_2._attr_current_temperature = 22.0 + entity_registry.async_get_or_create( + DOMAIN, "test", "5678", suggested_object_id="climate_2" + ) + await create_mock_platform(hass, [climate_1, climate_2]) + + # Add climate entities to same area living_room_area = area_registry.async_create(name="Living Room") + bedroom_area = area_registry.async_create(name="Bedroom") entity_registry.async_update_entity( climate_1.entity_id, area_id=living_room_area.id ) + entity_registry.async_update_entity( + climate_2.entity_id, area_id=living_room_area.id + ) - with ( - patch("homeassistant.core.StateMachine.get", return_value=None), - pytest.raises(intent.IntentHandleError), - ): - await intent.async_handle( - hass, "test", climate_intent.INTENT_GET_TEMPERATURE, {} - ) - - with ( - patch("homeassistant.core.StateMachine.async_all", return_value=[]), - pytest.raises(intent.MatchFailedError) as error, - ): + # Should fail with empty name + with pytest.raises(intent.InvalidSlotInfo): await intent.async_handle( hass, "test", climate_intent.INTENT_GET_TEMPERATURE, - {"area": {"value": "Living Room"}}, + {"name": {"value": ""}}, + assistant=conversation.DOMAIN, ) - # Exception should contain details of what we tried to match - assert isinstance(error.value, intent.MatchFailedError) - assert error.value.result.no_match_reason == intent.MatchFailedReason.AREA - constraints = error.value.constraints - assert constraints.name is None - assert constraints.area_name == "Living Room" - assert constraints.domains == {DOMAIN} - assert constraints.device_classes is None + # Should fail with empty area + with pytest.raises(intent.InvalidSlotInfo): + await intent.async_handle( + hass, + "test", + climate_intent.INTENT_GET_TEMPERATURE, + {"area": {"value": ""}}, + assistant=conversation.DOMAIN, + ) + + # Expose second, hide first + async_expose_entity(hass, conversation.DOMAIN, climate_1.entity_id, False) + async_expose_entity(hass, conversation.DOMAIN, climate_2.entity_id, True) + + # Second climate entity is exposed + response = await intent.async_handle( + hass, + "test", + climate_intent.INTENT_GET_TEMPERATURE, + {}, + assistant=conversation.DOMAIN, + ) + assert response.response_type == intent.IntentResponseType.QUERY_ANSWER + assert len(response.matched_states) == 1 + assert response.matched_states[0].entity_id == climate_2.entity_id + + # Using the area should work + response = await intent.async_handle( + hass, + "test", + climate_intent.INTENT_GET_TEMPERATURE, + {"area": {"value": living_room_area.name}}, + assistant=conversation.DOMAIN, + ) + assert response.response_type == intent.IntentResponseType.QUERY_ANSWER + assert len(response.matched_states) == 1 + assert response.matched_states[0].entity_id == climate_2.entity_id + + # Using the name of the exposed entity should work + response = await intent.async_handle( + hass, + "test", + climate_intent.INTENT_GET_TEMPERATURE, + {"name": {"value": climate_2.name}}, + assistant=conversation.DOMAIN, + ) + assert response.response_type == intent.IntentResponseType.QUERY_ANSWER + assert len(response.matched_states) == 1 + assert response.matched_states[0].entity_id == climate_2.entity_id + + # Using the name of the *unexposed* entity should fail + with pytest.raises(intent.MatchFailedError) as err: + await intent.async_handle( + hass, + "test", + climate_intent.INTENT_GET_TEMPERATURE, + {"name": {"value": climate_1.name}}, + assistant=conversation.DOMAIN, + ) + assert err.value.result.no_match_reason == intent.MatchFailedReason.NAME + + # Expose first, hide second + async_expose_entity(hass, conversation.DOMAIN, climate_1.entity_id, True) + async_expose_entity(hass, conversation.DOMAIN, climate_2.entity_id, False) + + # Second climate entity is exposed + response = await intent.async_handle( + hass, + "test", + climate_intent.INTENT_GET_TEMPERATURE, + {}, + assistant=conversation.DOMAIN, + ) + assert response.response_type == intent.IntentResponseType.QUERY_ANSWER + assert len(response.matched_states) == 1 + assert response.matched_states[0].entity_id == climate_1.entity_id + + # Wrong area name + with pytest.raises(intent.MatchFailedError) as err: + await intent.async_handle( + hass, + "test", + climate_intent.INTENT_GET_TEMPERATURE, + {"area": {"value": bedroom_area.name}}, + assistant=conversation.DOMAIN, + ) + assert err.value.result.no_match_reason == intent.MatchFailedReason.AREA + + # Neither are exposed + async_expose_entity(hass, conversation.DOMAIN, climate_1.entity_id, False) + async_expose_entity(hass, conversation.DOMAIN, climate_2.entity_id, False) + + with pytest.raises(intent.MatchFailedError) as err: + await intent.async_handle( + hass, + "test", + climate_intent.INTENT_GET_TEMPERATURE, + {}, + assistant=conversation.DOMAIN, + ) + assert err.value.result.no_match_reason == intent.MatchFailedReason.ASSISTANT + + # Should fail with area + with pytest.raises(intent.MatchFailedError) as err: + await intent.async_handle( + hass, + "test", + climate_intent.INTENT_GET_TEMPERATURE, + {"area": {"value": living_room_area.name}}, + assistant=conversation.DOMAIN, + ) + assert err.value.result.no_match_reason == intent.MatchFailedReason.ASSISTANT + + # Should fail with both names + for name in (climate_1.name, climate_2.name): + with pytest.raises(intent.MatchFailedError) as err: + await intent.async_handle( + hass, + "test", + climate_intent.INTENT_GET_TEMPERATURE, + {"name": {"value": name}}, + assistant=conversation.DOMAIN, + ) + assert err.value.result.no_match_reason == intent.MatchFailedReason.ASSISTANT diff --git a/tests/components/humidifier/test_intent.py b/tests/components/humidifier/test_intent.py index 936369f8aa7..6318c5f136d 100644 --- a/tests/components/humidifier/test_intent.py +++ b/tests/components/humidifier/test_intent.py @@ -2,6 +2,8 @@ import pytest +from homeassistant.components import conversation +from homeassistant.components.homeassistant.exposed_entities import async_expose_entity from homeassistant.components.humidifier import ( ATTR_AVAILABLE_MODES, ATTR_HUMIDITY, @@ -19,13 +21,22 @@ from homeassistant.const import ( STATE_ON, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers.intent import IntentHandleError, async_handle +from homeassistant.helpers.intent import ( + IntentHandleError, + IntentResponseType, + InvalidSlotInfo, + MatchFailedError, + MatchFailedReason, + async_handle, +) +from homeassistant.setup import async_setup_component from tests.common import async_mock_service async def test_intent_set_humidity(hass: HomeAssistant) -> None: """Test the set humidity intent.""" + assert await async_setup_component(hass, "homeassistant", {}) hass.states.async_set( "humidifier.bedroom_humidifier", STATE_ON, {ATTR_HUMIDITY: 40} ) @@ -38,6 +49,7 @@ async def test_intent_set_humidity(hass: HomeAssistant) -> None: "test", intent.INTENT_HUMIDITY, {"name": {"value": "Bedroom humidifier"}, "humidity": {"value": "50"}}, + assistant=conversation.DOMAIN, ) await hass.async_block_till_done() @@ -54,6 +66,7 @@ async def test_intent_set_humidity(hass: HomeAssistant) -> None: async def test_intent_set_humidity_and_turn_on(hass: HomeAssistant) -> None: """Test the set humidity intent for turned off humidifier.""" + assert await async_setup_component(hass, "homeassistant", {}) hass.states.async_set( "humidifier.bedroom_humidifier", STATE_OFF, {ATTR_HUMIDITY: 40} ) @@ -66,6 +79,7 @@ async def test_intent_set_humidity_and_turn_on(hass: HomeAssistant) -> None: "test", intent.INTENT_HUMIDITY, {"name": {"value": "Bedroom humidifier"}, "humidity": {"value": "50"}}, + assistant=conversation.DOMAIN, ) await hass.async_block_till_done() @@ -89,6 +103,7 @@ async def test_intent_set_humidity_and_turn_on(hass: HomeAssistant) -> None: async def test_intent_set_mode(hass: HomeAssistant) -> None: """Test the set mode intent.""" + assert await async_setup_component(hass, "homeassistant", {}) hass.states.async_set( "humidifier.bedroom_humidifier", STATE_ON, @@ -108,6 +123,7 @@ async def test_intent_set_mode(hass: HomeAssistant) -> None: "test", intent.INTENT_MODE, {"name": {"value": "Bedroom humidifier"}, "mode": {"value": "away"}}, + assistant=conversation.DOMAIN, ) await hass.async_block_till_done() @@ -127,6 +143,7 @@ async def test_intent_set_mode(hass: HomeAssistant) -> None: async def test_intent_set_mode_and_turn_on(hass: HomeAssistant) -> None: """Test the set mode intent.""" + assert await async_setup_component(hass, "homeassistant", {}) hass.states.async_set( "humidifier.bedroom_humidifier", STATE_OFF, @@ -146,6 +163,7 @@ async def test_intent_set_mode_and_turn_on(hass: HomeAssistant) -> None: "test", intent.INTENT_MODE, {"name": {"value": "Bedroom humidifier"}, "mode": {"value": "away"}}, + assistant=conversation.DOMAIN, ) await hass.async_block_till_done() @@ -169,6 +187,7 @@ async def test_intent_set_mode_and_turn_on(hass: HomeAssistant) -> None: async def test_intent_set_mode_tests_feature(hass: HomeAssistant) -> None: """Test the set mode intent where modes are not supported.""" + assert await async_setup_component(hass, "homeassistant", {}) hass.states.async_set( "humidifier.bedroom_humidifier", STATE_ON, {ATTR_HUMIDITY: 40} ) @@ -181,6 +200,7 @@ async def test_intent_set_mode_tests_feature(hass: HomeAssistant) -> None: "test", intent.INTENT_MODE, {"name": {"value": "Bedroom humidifier"}, "mode": {"value": "away"}}, + assistant=conversation.DOMAIN, ) assert str(excinfo.value) == "Entity bedroom humidifier does not support modes" assert len(mode_calls) == 0 @@ -191,6 +211,7 @@ async def test_intent_set_unknown_mode( hass: HomeAssistant, available_modes: list[str] | None ) -> None: """Test the set mode intent for unsupported mode.""" + assert await async_setup_component(hass, "homeassistant", {}) hass.states.async_set( "humidifier.bedroom_humidifier", STATE_ON, @@ -210,6 +231,111 @@ async def test_intent_set_unknown_mode( "test", intent.INTENT_MODE, {"name": {"value": "Bedroom humidifier"}, "mode": {"value": "eco"}}, + assistant=conversation.DOMAIN, ) assert str(excinfo.value) == "Entity bedroom humidifier does not support eco mode" assert len(mode_calls) == 0 + + +async def test_intent_errors(hass: HomeAssistant) -> None: + """Test the error conditions for set humidity and set mode intents.""" + assert await async_setup_component(hass, "homeassistant", {}) + entity_id = "humidifier.bedroom_humidifier" + hass.states.async_set( + entity_id, + STATE_ON, + { + ATTR_HUMIDITY: 40, + ATTR_SUPPORTED_FEATURES: 1, + ATTR_AVAILABLE_MODES: ["home", "away"], + ATTR_MODE: None, + }, + ) + async_mock_service(hass, DOMAIN, SERVICE_SET_HUMIDITY) + async_mock_service(hass, DOMAIN, SERVICE_SET_MODE) + await intent.async_setup_intents(hass) + + # Humidifiers are exposed by default + result = await async_handle( + hass, + "test", + intent.INTENT_HUMIDITY, + {"name": {"value": "Bedroom humidifier"}, "humidity": {"value": "50"}}, + assistant=conversation.DOMAIN, + ) + assert result.response_type == IntentResponseType.ACTION_DONE + + result = await async_handle( + hass, + "test", + intent.INTENT_MODE, + {"name": {"value": "Bedroom humidifier"}, "mode": {"value": "away"}}, + assistant=conversation.DOMAIN, + ) + assert result.response_type == IntentResponseType.ACTION_DONE + + # Unexposing it should fail + async_expose_entity(hass, conversation.DOMAIN, entity_id, False) + + with pytest.raises(MatchFailedError) as err: + await async_handle( + hass, + "test", + intent.INTENT_HUMIDITY, + {"name": {"value": "Bedroom humidifier"}, "humidity": {"value": "50"}}, + assistant=conversation.DOMAIN, + ) + assert err.value.result.no_match_reason == MatchFailedReason.ASSISTANT + + with pytest.raises(MatchFailedError) as err: + await async_handle( + hass, + "test", + intent.INTENT_MODE, + {"name": {"value": "Bedroom humidifier"}, "mode": {"value": "away"}}, + assistant=conversation.DOMAIN, + ) + assert err.value.result.no_match_reason == MatchFailedReason.ASSISTANT + + # Expose again to test other errors + async_expose_entity(hass, conversation.DOMAIN, entity_id, True) + + # Empty name should fail + with pytest.raises(InvalidSlotInfo): + await async_handle( + hass, + "test", + intent.INTENT_HUMIDITY, + {"name": {"value": ""}, "humidity": {"value": "50"}}, + assistant=conversation.DOMAIN, + ) + + with pytest.raises(InvalidSlotInfo): + await async_handle( + hass, + "test", + intent.INTENT_MODE, + {"name": {"value": ""}, "mode": {"value": "away"}}, + assistant=conversation.DOMAIN, + ) + + # Wrong name should fail + with pytest.raises(MatchFailedError) as err: + await async_handle( + hass, + "test", + intent.INTENT_HUMIDITY, + {"name": {"value": "does not exist"}, "humidity": {"value": "50"}}, + assistant=conversation.DOMAIN, + ) + assert err.value.result.no_match_reason == MatchFailedReason.NAME + + with pytest.raises(MatchFailedError) as err: + await async_handle( + hass, + "test", + intent.INTENT_MODE, + {"name": {"value": "does not exist"}, "mode": {"value": "away"}}, + assistant=conversation.DOMAIN, + ) + assert err.value.result.no_match_reason == MatchFailedReason.NAME diff --git a/tests/components/todo/test_init.py b/tests/components/todo/test_init.py index 951a0035017..5999b4b9fbe 100644 --- a/tests/components/todo/test_init.py +++ b/tests/components/todo/test_init.py @@ -9,6 +9,8 @@ import pytest from typing_extensions import Generator import voluptuous as vol +from homeassistant.components import conversation +from homeassistant.components.homeassistant.exposed_entities import async_expose_entity from homeassistant.components.todo import ( DOMAIN, TodoItem, @@ -23,6 +25,7 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError, ServiceValidationError from homeassistant.helpers import intent from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.setup import async_setup_component from tests.common import ( MockConfigEntry, @@ -1110,6 +1113,7 @@ async def test_add_item_intent( hass_ws_client: WebSocketGenerator, ) -> None: """Test adding items to lists using an intent.""" + assert await async_setup_component(hass, "homeassistant", {}) await todo_intent.async_setup_intents(hass) entity1 = MockTodoListEntity() @@ -1128,6 +1132,7 @@ async def test_add_item_intent( "test", todo_intent.INTENT_LIST_ADD_ITEM, {"item": {"value": "beer"}, "name": {"value": "list 1"}}, + assistant=conversation.DOMAIN, ) assert response.response_type == intent.IntentResponseType.ACTION_DONE @@ -1143,6 +1148,7 @@ async def test_add_item_intent( "test", todo_intent.INTENT_LIST_ADD_ITEM, {"item": {"value": "cheese"}, "name": {"value": "List 2"}}, + assistant=conversation.DOMAIN, ) assert response.response_type == intent.IntentResponseType.ACTION_DONE @@ -1157,6 +1163,7 @@ async def test_add_item_intent( "test", todo_intent.INTENT_LIST_ADD_ITEM, {"item": {"value": "wine"}, "name": {"value": "lIST 2"}}, + assistant=conversation.DOMAIN, ) assert response.response_type == intent.IntentResponseType.ACTION_DONE @@ -1165,13 +1172,46 @@ async def test_add_item_intent( assert entity2.items[1].summary == "wine" assert entity2.items[1].status == TodoItemStatus.NEEDS_ACTION + # Should fail if lists are not exposed + async_expose_entity(hass, conversation.DOMAIN, entity1.entity_id, False) + async_expose_entity(hass, conversation.DOMAIN, entity2.entity_id, False) + with pytest.raises(intent.MatchFailedError) as err: + await intent.async_handle( + hass, + "test", + todo_intent.INTENT_LIST_ADD_ITEM, + {"item": {"value": "cookies"}, "name": {"value": "list 1"}}, + assistant=conversation.DOMAIN, + ) + assert err.value.result.no_match_reason == intent.MatchFailedReason.ASSISTANT + # Missing list - with pytest.raises(intent.IntentHandleError): + with pytest.raises(intent.MatchFailedError): await intent.async_handle( hass, "test", todo_intent.INTENT_LIST_ADD_ITEM, {"item": {"value": "wine"}, "name": {"value": "This list does not exist"}}, + assistant=conversation.DOMAIN, + ) + + # Fail with empty name/item + with pytest.raises(intent.InvalidSlotInfo): + await intent.async_handle( + hass, + "test", + todo_intent.INTENT_LIST_ADD_ITEM, + {"item": {"value": "wine"}, "name": {"value": ""}}, + assistant=conversation.DOMAIN, + ) + + with pytest.raises(intent.InvalidSlotInfo): + await intent.async_handle( + hass, + "test", + todo_intent.INTENT_LIST_ADD_ITEM, + {"item": {"value": ""}, "name": {"value": "list 1"}}, + assistant=conversation.DOMAIN, ) diff --git a/tests/components/weather/test_intent.py b/tests/components/weather/test_intent.py index 1fde5882d6e..0f9884791a5 100644 --- a/tests/components/weather/test_intent.py +++ b/tests/components/weather/test_intent.py @@ -1,9 +1,9 @@ """Test weather intents.""" -from unittest.mock import patch - import pytest +from homeassistant.components import conversation +from homeassistant.components.homeassistant.exposed_entities import async_expose_entity from homeassistant.components.weather import ( DOMAIN, WeatherEntity, @@ -16,15 +16,18 @@ from homeassistant.setup import async_setup_component async def test_get_weather(hass: HomeAssistant) -> None: """Test get weather for first entity and by name.""" + assert await async_setup_component(hass, "homeassistant", {}) assert await async_setup_component(hass, "weather", {"weather": {}}) entity1 = WeatherEntity() entity1._attr_name = "Weather 1" entity1.entity_id = "weather.test_1" + async_expose_entity(hass, conversation.DOMAIN, entity1.entity_id, True) entity2 = WeatherEntity() entity2._attr_name = "Weather 2" entity2.entity_id = "weather.test_2" + async_expose_entity(hass, conversation.DOMAIN, entity2.entity_id, True) await hass.data[DOMAIN].async_add_entities([entity1, entity2]) @@ -45,15 +48,31 @@ async def test_get_weather(hass: HomeAssistant) -> None: "test", weather_intent.INTENT_GET_WEATHER, {"name": {"value": "Weather 2"}}, + assistant=conversation.DOMAIN, ) assert response.response_type == intent.IntentResponseType.QUERY_ANSWER assert len(response.matched_states) == 1 state = response.matched_states[0] assert state.entity_id == entity2.entity_id + # Should fail if not exposed + async_expose_entity(hass, conversation.DOMAIN, entity1.entity_id, False) + async_expose_entity(hass, conversation.DOMAIN, entity2.entity_id, False) + for name in (entity1.name, entity2.name): + with pytest.raises(intent.MatchFailedError) as err: + await intent.async_handle( + hass, + "test", + weather_intent.INTENT_GET_WEATHER, + {"name": {"value": name}}, + assistant=conversation.DOMAIN, + ) + assert err.value.result.no_match_reason == intent.MatchFailedReason.ASSISTANT + async def test_get_weather_wrong_name(hass: HomeAssistant) -> None: """Test get weather with the wrong name.""" + assert await async_setup_component(hass, "homeassistant", {}) assert await async_setup_component(hass, "weather", {"weather": {}}) entity1 = WeatherEntity() @@ -63,48 +82,43 @@ async def test_get_weather_wrong_name(hass: HomeAssistant) -> None: await hass.data[DOMAIN].async_add_entities([entity1]) await weather_intent.async_setup_intents(hass) + async_expose_entity(hass, conversation.DOMAIN, entity1.entity_id, True) # Incorrect name - with pytest.raises(intent.IntentHandleError): + with pytest.raises(intent.MatchFailedError) as err: await intent.async_handle( hass, "test", weather_intent.INTENT_GET_WEATHER, {"name": {"value": "not the right name"}}, + assistant=conversation.DOMAIN, + ) + assert err.value.result.no_match_reason == intent.MatchFailedReason.NAME + + # Empty name + with pytest.raises(intent.InvalidSlotInfo): + await intent.async_handle( + hass, + "test", + weather_intent.INTENT_GET_WEATHER, + {"name": {"value": ""}}, + assistant=conversation.DOMAIN, ) async def test_get_weather_no_entities(hass: HomeAssistant) -> None: """Test get weather with no weather entities.""" + assert await async_setup_component(hass, "homeassistant", {}) assert await async_setup_component(hass, "weather", {"weather": {}}) await weather_intent.async_setup_intents(hass) # No weather entities - with pytest.raises(intent.IntentHandleError): - await intent.async_handle(hass, "test", weather_intent.INTENT_GET_WEATHER, {}) - - -async def test_get_weather_no_state(hass: HomeAssistant) -> None: - """Test get weather when state is not returned.""" - assert await async_setup_component(hass, "weather", {"weather": {}}) - - entity1 = WeatherEntity() - entity1._attr_name = "Weather 1" - entity1.entity_id = "weather.test_1" - - await hass.data[DOMAIN].async_add_entities([entity1]) - - await weather_intent.async_setup_intents(hass) - - # Success with state - response = await intent.async_handle( - hass, "test", weather_intent.INTENT_GET_WEATHER, {} - ) - assert response.response_type == intent.IntentResponseType.QUERY_ANSWER - - # Failure without state - with ( - patch("homeassistant.core.StateMachine.get", return_value=None), - pytest.raises(intent.IntentHandleError), - ): - await intent.async_handle(hass, "test", weather_intent.INTENT_GET_WEATHER, {}) + with pytest.raises(intent.MatchFailedError) as err: + await intent.async_handle( + hass, + "test", + weather_intent.INTENT_GET_WEATHER, + {}, + assistant=conversation.DOMAIN, + ) + assert err.value.result.no_match_reason == intent.MatchFailedReason.DOMAIN From 27e8a4ed6f7e0b19a30ad88297f848a8e6df687d Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Fri, 7 Jun 2024 07:23:44 +0200 Subject: [PATCH 0320/1445] Add the missing humidity value to the Accuweather daily forecast (#119013) Add the missing humidity value to the daily forecast Co-authored-by: Maciej Bieniek <478555+bieniu@users.noreply.github.com> --- .../components/accuweather/weather.py | 2 ++ .../accuweather/fixtures/forecast_data.json | 5 +++++ .../accuweather/snapshots/test_weather.ambr | 20 +++++++++++++++++++ 3 files changed, 27 insertions(+) diff --git a/homeassistant/components/accuweather/weather.py b/homeassistant/components/accuweather/weather.py index dba45d5c24f..72d717f2703 100644 --- a/homeassistant/components/accuweather/weather.py +++ b/homeassistant/components/accuweather/weather.py @@ -7,6 +7,7 @@ from typing import cast from homeassistant.components.weather import ( ATTR_FORECAST_CLOUD_COVERAGE, ATTR_FORECAST_CONDITION, + ATTR_FORECAST_HUMIDITY, ATTR_FORECAST_NATIVE_APPARENT_TEMP, ATTR_FORECAST_NATIVE_PRECIPITATION, ATTR_FORECAST_NATIVE_TEMP, @@ -183,6 +184,7 @@ class AccuWeatherEntity( { ATTR_FORECAST_TIME: utc_from_timestamp(item["EpochDate"]).isoformat(), ATTR_FORECAST_CLOUD_COVERAGE: item["CloudCoverDay"], + ATTR_FORECAST_HUMIDITY: item["RelativeHumidityDay"]["Average"], ATTR_FORECAST_NATIVE_TEMP: item["TemperatureMax"][ATTR_VALUE], ATTR_FORECAST_NATIVE_TEMP_LOW: item["TemperatureMin"][ATTR_VALUE], ATTR_FORECAST_NATIVE_APPARENT_TEMP: item["RealFeelTemperatureMax"][ diff --git a/tests/components/accuweather/fixtures/forecast_data.json b/tests/components/accuweather/fixtures/forecast_data.json index a7d57af113a..cd40705314b 100644 --- a/tests/components/accuweather/fixtures/forecast_data.json +++ b/tests/components/accuweather/fixtures/forecast_data.json @@ -76,6 +76,7 @@ "Unit": "C", "UnitType": 17 }, + "RelativeHumidityDay": { "Average": 60 }, "IconDay": 17, "IconPhraseDay": "Partly sunny w/ t-storms", "HasPrecipitationDay": true, @@ -286,6 +287,7 @@ "Unit": "C", "UnitType": 17 }, + "RelativeHumidityDay": { "Average": 58 }, "IconDay": 4, "IconPhraseDay": "Intermittent clouds", "HasPrecipitationDay": false, @@ -492,6 +494,7 @@ "Unit": "C", "UnitType": 17 }, + "RelativeHumidityDay": { "Average": 52 }, "IconDay": 4, "IconPhraseDay": "Intermittent clouds", "HasPrecipitationDay": false, @@ -698,6 +701,7 @@ "Unit": "C", "UnitType": 17 }, + "RelativeHumidityDay": { "Average": 65 }, "IconDay": 3, "IconPhraseDay": "Partly sunny", "HasPrecipitationDay": false, @@ -904,6 +908,7 @@ "Unit": "C", "UnitType": 17 }, + "RelativeHumidityDay": { "Average": 55 }, "IconDay": 4, "IconPhraseDay": "Intermittent clouds", "HasPrecipitationDay": false, diff --git a/tests/components/accuweather/snapshots/test_weather.ambr b/tests/components/accuweather/snapshots/test_weather.ambr index 1542d22aa7b..49bf4008884 100644 --- a/tests/components/accuweather/snapshots/test_weather.ambr +++ b/tests/components/accuweather/snapshots/test_weather.ambr @@ -7,6 +7,7 @@ 'cloud_coverage': 58, 'condition': 'lightning-rainy', 'datetime': '2020-07-26T05:00:00+00:00', + 'humidity': 60, 'precipitation': 2.5, 'precipitation_probability': 60, 'temperature': 29.5, @@ -21,6 +22,7 @@ 'cloud_coverage': 52, 'condition': 'partlycloudy', 'datetime': '2020-07-27T05:00:00+00:00', + 'humidity': 58, 'precipitation': 0.0, 'precipitation_probability': 25, 'temperature': 26.2, @@ -35,6 +37,7 @@ 'cloud_coverage': 65, 'condition': 'partlycloudy', 'datetime': '2020-07-28T05:00:00+00:00', + 'humidity': 52, 'precipitation': 0.0, 'precipitation_probability': 10, 'temperature': 31.7, @@ -49,6 +52,7 @@ 'cloud_coverage': 45, 'condition': 'partlycloudy', 'datetime': '2020-07-29T05:00:00+00:00', + 'humidity': 65, 'precipitation': 0.0, 'precipitation_probability': 9, 'temperature': 24.0, @@ -63,6 +67,7 @@ 'cloud_coverage': 50, 'condition': 'partlycloudy', 'datetime': '2020-07-30T05:00:00+00:00', + 'humidity': 55, 'precipitation': 0.0, 'precipitation_probability': 1, 'temperature': 21.4, @@ -84,6 +89,7 @@ 'cloud_coverage': 58, 'condition': 'lightning-rainy', 'datetime': '2020-07-26T05:00:00+00:00', + 'humidity': 60, 'precipitation': 2.5, 'precipitation_probability': 60, 'temperature': 29.5, @@ -98,6 +104,7 @@ 'cloud_coverage': 52, 'condition': 'partlycloudy', 'datetime': '2020-07-27T05:00:00+00:00', + 'humidity': 58, 'precipitation': 0.0, 'precipitation_probability': 25, 'temperature': 26.2, @@ -112,6 +119,7 @@ 'cloud_coverage': 65, 'condition': 'partlycloudy', 'datetime': '2020-07-28T05:00:00+00:00', + 'humidity': 52, 'precipitation': 0.0, 'precipitation_probability': 10, 'temperature': 31.7, @@ -126,6 +134,7 @@ 'cloud_coverage': 45, 'condition': 'partlycloudy', 'datetime': '2020-07-29T05:00:00+00:00', + 'humidity': 65, 'precipitation': 0.0, 'precipitation_probability': 9, 'temperature': 24.0, @@ -140,6 +149,7 @@ 'cloud_coverage': 50, 'condition': 'partlycloudy', 'datetime': '2020-07-30T05:00:00+00:00', + 'humidity': 55, 'precipitation': 0.0, 'precipitation_probability': 1, 'temperature': 21.4, @@ -160,6 +170,7 @@ 'cloud_coverage': 58, 'condition': 'lightning-rainy', 'datetime': '2020-07-26T05:00:00+00:00', + 'humidity': 60, 'precipitation': 2.5, 'precipitation_probability': 60, 'temperature': 29.5, @@ -174,6 +185,7 @@ 'cloud_coverage': 52, 'condition': 'partlycloudy', 'datetime': '2020-07-27T05:00:00+00:00', + 'humidity': 58, 'precipitation': 0.0, 'precipitation_probability': 25, 'temperature': 26.2, @@ -188,6 +200,7 @@ 'cloud_coverage': 65, 'condition': 'partlycloudy', 'datetime': '2020-07-28T05:00:00+00:00', + 'humidity': 52, 'precipitation': 0.0, 'precipitation_probability': 10, 'temperature': 31.7, @@ -202,6 +215,7 @@ 'cloud_coverage': 45, 'condition': 'partlycloudy', 'datetime': '2020-07-29T05:00:00+00:00', + 'humidity': 65, 'precipitation': 0.0, 'precipitation_probability': 9, 'temperature': 24.0, @@ -216,6 +230,7 @@ 'cloud_coverage': 50, 'condition': 'partlycloudy', 'datetime': '2020-07-30T05:00:00+00:00', + 'humidity': 55, 'precipitation': 0.0, 'precipitation_probability': 1, 'temperature': 21.4, @@ -234,6 +249,7 @@ 'cloud_coverage': 58, 'condition': 'lightning-rainy', 'datetime': '2020-07-26T05:00:00+00:00', + 'humidity': 60, 'precipitation': 2.5, 'precipitation_probability': 60, 'temperature': 29.5, @@ -248,6 +264,7 @@ 'cloud_coverage': 52, 'condition': 'partlycloudy', 'datetime': '2020-07-27T05:00:00+00:00', + 'humidity': 58, 'precipitation': 0.0, 'precipitation_probability': 25, 'temperature': 26.2, @@ -262,6 +279,7 @@ 'cloud_coverage': 65, 'condition': 'partlycloudy', 'datetime': '2020-07-28T05:00:00+00:00', + 'humidity': 52, 'precipitation': 0.0, 'precipitation_probability': 10, 'temperature': 31.7, @@ -276,6 +294,7 @@ 'cloud_coverage': 45, 'condition': 'partlycloudy', 'datetime': '2020-07-29T05:00:00+00:00', + 'humidity': 65, 'precipitation': 0.0, 'precipitation_probability': 9, 'temperature': 24.0, @@ -290,6 +309,7 @@ 'cloud_coverage': 50, 'condition': 'partlycloudy', 'datetime': '2020-07-30T05:00:00+00:00', + 'humidity': 55, 'precipitation': 0.0, 'precipitation_probability': 1, 'temperature': 21.4, From 7195a2112636b3bf6d1668d2de3a96432eec4f7b Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Fri, 7 Jun 2024 08:34:38 +0200 Subject: [PATCH 0321/1445] Fix Generator annotations in tests (2) (#119019) --- tests/components/demo/test_notify.py | 4 ++-- tests/components/ecobee/conftest.py | 4 ++-- tests/components/file/conftest.py | 4 +--- tests/components/gardena_bluetooth/conftest.py | 8 ++++---- tests/components/mqtt/test_init.py | 4 ++-- tests/components/mqtt_json/test_device_tracker.py | 4 ++-- tests/components/opensky/conftest.py | 6 +++--- tests/components/rainbird/test_config_flow.py | 6 +++--- 8 files changed, 19 insertions(+), 21 deletions(-) diff --git a/tests/components/demo/test_notify.py b/tests/components/demo/test_notify.py index 9b8d4aac0b2..4ebbfbdac04 100644 --- a/tests/components/demo/test_notify.py +++ b/tests/components/demo/test_notify.py @@ -1,9 +1,9 @@ """The tests for the notify demo platform.""" -from collections.abc import Generator from unittest.mock import patch import pytest +from typing_extensions import Generator from homeassistant.components import notify from homeassistant.components.demo import DOMAIN @@ -16,7 +16,7 @@ from tests.common import MockConfigEntry, async_capture_events @pytest.fixture -def notify_only() -> Generator[None, None]: +def notify_only() -> Generator[None]: """Enable only the notify platform.""" with patch( "homeassistant.components.demo.COMPONENTS_WITH_CONFIG_ENTRY_DEMO_PLATFORM", diff --git a/tests/components/ecobee/conftest.py b/tests/components/ecobee/conftest.py index 68a17dbfe00..d9583e15986 100644 --- a/tests/components/ecobee/conftest.py +++ b/tests/components/ecobee/conftest.py @@ -1,10 +1,10 @@ """Fixtures for tests.""" -from collections.abc import Generator from unittest.mock import MagicMock, patch import pytest from requests_mock import Mocker +from typing_extensions import Generator from homeassistant.components.ecobee import ECOBEE_API_KEY, ECOBEE_REFRESH_TOKEN @@ -25,7 +25,7 @@ def requests_mock_fixture(requests_mock: Mocker) -> None: @pytest.fixture -def mock_ecobee() -> Generator[None, MagicMock]: +def mock_ecobee() -> Generator[MagicMock]: """Mock an Ecobee object.""" ecobee = MagicMock() ecobee.request_pin.return_value = True diff --git a/tests/components/file/conftest.py b/tests/components/file/conftest.py index a9b817a7dcf..265acde36ca 100644 --- a/tests/components/file/conftest.py +++ b/tests/components/file/conftest.py @@ -24,9 +24,7 @@ def is_allowed() -> bool: @pytest.fixture -def mock_is_allowed_path( - hass: HomeAssistant, is_allowed: bool -) -> Generator[None, MagicMock]: +def mock_is_allowed_path(hass: HomeAssistant, is_allowed: bool) -> Generator[MagicMock]: """Mock is_allowed_path method.""" with patch.object( hass.config, "is_allowed_path", return_value=is_allowed diff --git a/tests/components/gardena_bluetooth/conftest.py b/tests/components/gardena_bluetooth/conftest.py index 830984bc07f..08f698b4b67 100644 --- a/tests/components/gardena_bluetooth/conftest.py +++ b/tests/components/gardena_bluetooth/conftest.py @@ -1,6 +1,6 @@ """Common fixtures for the Gardena Bluetooth tests.""" -from collections.abc import Awaitable, Callable +from collections.abc import Callable, Coroutine from typing import Any from unittest.mock import AsyncMock, Mock, patch @@ -52,12 +52,12 @@ def mock_read_char_raw(): @pytest.fixture async def scan_step( hass: HomeAssistant, freezer: FrozenDateTimeFactory -) -> Generator[None, None, Callable[[], Awaitable[None]]]: +) -> Callable[[], Coroutine[Any, Any, None]]: """Step system time forward.""" freezer.move_to("2023-01-01T01:00:00Z") - async def delay(): + async def delay() -> None: """Trigger delay in system.""" freezer.tick(delta=SCAN_INTERVAL) async_fire_time_changed(hass) @@ -69,7 +69,7 @@ async def scan_step( @pytest.fixture(autouse=True) def mock_client( enable_bluetooth: None, scan_step, mock_read_char_raw: dict[str, Any] -) -> None: +) -> Generator[Mock]: """Auto mock bluetooth.""" client = Mock(spec_set=Client) diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index 5189196ac2b..a780fce83c0 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -1,7 +1,6 @@ """The tests for the MQTT component.""" import asyncio -from collections.abc import Generator from copy import deepcopy from datetime import datetime, timedelta from functools import partial @@ -16,6 +15,7 @@ from unittest.mock import ANY, MagicMock, Mock, call, mock_open, patch from freezegun.api import FrozenDateTimeFactory import paho.mqtt.client as paho_mqtt import pytest +from typing_extensions import Generator import voluptuous as vol from homeassistant.components import mqtt @@ -118,7 +118,7 @@ def record_calls(recorded_calls: list[ReceiveMessage]) -> MessageCallbackType: @pytest.fixture -def client_debug_log() -> Generator[None, None]: +def client_debug_log() -> Generator[None]: """Set the mqtt client log level to DEBUG.""" logger = logging.getLogger("mqtt_client_tests_debug") logger.setLevel(logging.DEBUG) diff --git a/tests/components/mqtt_json/test_device_tracker.py b/tests/components/mqtt_json/test_device_tracker.py index fdee4f685ff..a992c985057 100644 --- a/tests/components/mqtt_json/test_device_tracker.py +++ b/tests/components/mqtt_json/test_device_tracker.py @@ -1,12 +1,12 @@ """The tests for the JSON MQTT device tracker platform.""" -from collections.abc import Generator import json import logging import os from unittest.mock import patch import pytest +from typing_extensions import AsyncGenerator from homeassistant.components.device_tracker.legacy import ( DOMAIN as DT_DOMAIN, @@ -34,7 +34,7 @@ LOCATION_MESSAGE_INCOMPLETE = {"longitude": 2.0} @pytest.fixture(autouse=True) async def setup_comp( hass: HomeAssistant, mqtt_mock: MqttMockHAClient -) -> Generator[None, None, None]: +) -> AsyncGenerator[None]: """Initialize components.""" yaml_devices = hass.config.path(YAML_DEVICES) yield diff --git a/tests/components/opensky/conftest.py b/tests/components/opensky/conftest.py index 665fdd90e69..c48f3bec8d8 100644 --- a/tests/components/opensky/conftest.py +++ b/tests/components/opensky/conftest.py @@ -1,10 +1,10 @@ """Configure tests for the OpenSky integration.""" -from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest from python_opensky import StatesResponse +from typing_extensions import AsyncGenerator, Generator from homeassistant.components.opensky.const import ( CONF_ALTITUDE, @@ -23,7 +23,7 @@ from tests.common import MockConfigEntry, load_json_object_fixture @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Override async_setup_entry.""" with patch( "homeassistant.components.opensky.async_setup_entry", @@ -87,7 +87,7 @@ def mock_config_entry_authenticated() -> MockConfigEntry: @pytest.fixture -async def opensky_client() -> Generator[AsyncMock, None, None]: +async def opensky_client() -> AsyncGenerator[AsyncMock]: """Mock the OpenSky client.""" with ( patch( diff --git a/tests/components/rainbird/test_config_flow.py b/tests/components/rainbird/test_config_flow.py index b4cd51d6b3e..cdcef95f458 100644 --- a/tests/components/rainbird/test_config_flow.py +++ b/tests/components/rainbird/test_config_flow.py @@ -1,11 +1,11 @@ """Tests for the Rain Bird config flow.""" -from collections.abc import Generator from http import HTTPStatus from typing import Any -from unittest.mock import Mock, patch +from unittest.mock import AsyncMock, Mock, patch import pytest +from typing_extensions import AsyncGenerator from homeassistant import config_entries from homeassistant.components.rainbird import DOMAIN @@ -46,7 +46,7 @@ async def config_entry_data() -> None: @pytest.fixture(autouse=True) -async def mock_setup() -> Generator[Mock, None, None]: +async def mock_setup() -> AsyncGenerator[AsyncMock]: """Fixture for patching out integration setup.""" with patch( From 274cd41d57caec91c46865c5d965cbaa4eab2b56 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Fri, 7 Jun 2024 08:43:32 +0200 Subject: [PATCH 0322/1445] Fix Generator annotations in tests (1) (#119018) --- tests/components/androidtv/conftest.py | 8 +++---- tests/components/androidtv_remote/conftest.py | 2 +- tests/components/bsblan/conftest.py | 2 +- tests/components/co2signal/conftest.py | 4 ++-- tests/components/elgato/conftest.py | 6 ++--- tests/components/forecast_solar/conftest.py | 2 +- tests/components/geocaching/conftest.py | 2 +- tests/components/homeworks/conftest.py | 2 +- tests/components/intellifire/conftest.py | 6 ++--- tests/components/ipp/conftest.py | 6 ++--- tests/components/jellyfin/conftest.py | 4 ++-- tests/components/kaleidescape/conftest.py | 4 ++-- tests/components/luftdaten/conftest.py | 2 +- tests/components/motionmount/conftest.py | 2 +- tests/components/mqtt/conftest.py | 4 ++-- tests/components/open_meteo/conftest.py | 2 +- tests/components/plugwise/conftest.py | 22 +++++++++---------- tests/components/pure_energie/conftest.py | 2 +- tests/components/pvoutput/conftest.py | 2 +- tests/components/rdw/conftest.py | 4 ++-- tests/components/roku/conftest.py | 6 ++--- tests/components/sonarr/conftest.py | 4 ++-- tests/components/srp_energy/conftest.py | 6 ++--- tests/components/tailscale/conftest.py | 4 ++-- .../ukraine_alarm/test_config_flow.py | 4 ++-- tests/components/verisure/conftest.py | 2 +- tests/components/wake_on_lan/conftest.py | 6 ++--- tests/components/zamg/conftest.py | 14 ++++-------- 28 files changed, 60 insertions(+), 74 deletions(-) diff --git a/tests/components/androidtv/conftest.py b/tests/components/androidtv/conftest.py index 7c8815d8bc0..befb9db7a8c 100644 --- a/tests/components/androidtv/conftest.py +++ b/tests/components/androidtv/conftest.py @@ -1,15 +1,15 @@ """Fixtures for the Android TV integration tests.""" -from collections.abc import Generator from unittest.mock import Mock, patch import pytest +from typing_extensions import Generator from . import patchers @pytest.fixture(autouse=True) -def adb_device_tcp_fixture() -> Generator[None, patchers.AdbDeviceTcpAsyncFake, None]: +def adb_device_tcp_fixture() -> Generator[None]: """Patch ADB Device TCP.""" with patch( "androidtv.adb_manager.adb_manager_async.AdbDeviceTcpAsync", @@ -19,7 +19,7 @@ def adb_device_tcp_fixture() -> Generator[None, patchers.AdbDeviceTcpAsyncFake, @pytest.fixture(autouse=True) -def load_adbkey_fixture() -> Generator[None, str, None]: +def load_adbkey_fixture() -> Generator[None]: """Patch load_adbkey.""" with patch( "homeassistant.components.androidtv.ADBPythonSync.load_adbkey", @@ -29,7 +29,7 @@ def load_adbkey_fixture() -> Generator[None, str, None]: @pytest.fixture(autouse=True) -def keygen_fixture() -> Generator[None, Mock, None]: +def keygen_fixture() -> Generator[None]: """Patch keygen.""" with patch( "homeassistant.components.androidtv.keygen", diff --git a/tests/components/androidtv_remote/conftest.py b/tests/components/androidtv_remote/conftest.py index 7855e1cefb3..aa5583927d1 100644 --- a/tests/components/androidtv_remote/conftest.py +++ b/tests/components/androidtv_remote/conftest.py @@ -33,7 +33,7 @@ def mock_unload_entry() -> Generator[AsyncMock]: @pytest.fixture -def mock_api() -> Generator[None, MagicMock, None]: +def mock_api() -> Generator[MagicMock]: """Return a mocked AndroidTVRemote.""" with patch( "homeassistant.components.androidtv_remote.helpers.AndroidTVRemote", diff --git a/tests/components/bsblan/conftest.py b/tests/components/bsblan/conftest.py index 72d05c58b49..8309b1d64ef 100644 --- a/tests/components/bsblan/conftest.py +++ b/tests/components/bsblan/conftest.py @@ -40,7 +40,7 @@ def mock_setup_entry() -> Generator[AsyncMock]: @pytest.fixture -def mock_bsblan(request: pytest.FixtureRequest) -> Generator[None, MagicMock, None]: +def mock_bsblan(request: pytest.FixtureRequest) -> Generator[MagicMock]: """Return a mocked BSBLAN client.""" with ( diff --git a/tests/components/co2signal/conftest.py b/tests/components/co2signal/conftest.py index 64972e6403f..8d71672dcac 100644 --- a/tests/components/co2signal/conftest.py +++ b/tests/components/co2signal/conftest.py @@ -1,9 +1,9 @@ """Fixtures for Electricity maps integration tests.""" -from collections.abc import Generator from unittest.mock import AsyncMock, MagicMock, patch import pytest +from typing_extensions import Generator from homeassistant.components.co2signal import DOMAIN from homeassistant.const import CONF_API_KEY @@ -15,7 +15,7 @@ from tests.components.co2signal import VALID_RESPONSE @pytest.fixture(name="electricity_maps") -def mock_electricity_maps() -> Generator[None, MagicMock, None]: +def mock_electricity_maps() -> Generator[MagicMock]: """Mock the ElectricityMaps client.""" with ( diff --git a/tests/components/elgato/conftest.py b/tests/components/elgato/conftest.py index abbc1bc0463..aaaed0dc8da 100644 --- a/tests/components/elgato/conftest.py +++ b/tests/components/elgato/conftest.py @@ -51,7 +51,7 @@ def mock_setup_entry() -> Generator[AsyncMock]: @pytest.fixture -def mock_onboarding() -> Generator[None, MagicMock, None]: +def mock_onboarding() -> Generator[MagicMock]: """Mock that Home Assistant is currently onboarding.""" with patch( "homeassistant.components.onboarding.async_is_onboarded", @@ -61,9 +61,7 @@ def mock_onboarding() -> Generator[None, MagicMock, None]: @pytest.fixture -def mock_elgato( - device_fixtures: str, state_variant: str -) -> Generator[None, MagicMock, None]: +def mock_elgato(device_fixtures: str, state_variant: str) -> Generator[MagicMock]: """Return a mocked Elgato client.""" with ( patch( diff --git a/tests/components/forecast_solar/conftest.py b/tests/components/forecast_solar/conftest.py index 346a5c8fac5..d1eacad8dbe 100644 --- a/tests/components/forecast_solar/conftest.py +++ b/tests/components/forecast_solar/conftest.py @@ -57,7 +57,7 @@ def mock_config_entry() -> MockConfigEntry: @pytest.fixture -def mock_forecast_solar(hass) -> Generator[None, MagicMock, None]: +def mock_forecast_solar(hass: HomeAssistant) -> Generator[MagicMock]: """Return a mocked Forecast.Solar client. hass fixture included because it sets the time zone. diff --git a/tests/components/geocaching/conftest.py b/tests/components/geocaching/conftest.py index bedd6fe8b0c..155cd2c5a7e 100644 --- a/tests/components/geocaching/conftest.py +++ b/tests/components/geocaching/conftest.py @@ -37,7 +37,7 @@ def mock_setup_entry() -> Generator[AsyncMock]: @pytest.fixture -def mock_geocaching_config_flow() -> Generator[None, MagicMock, None]: +def mock_geocaching_config_flow() -> Generator[MagicMock]: """Return a mocked Geocaching API client.""" mock_status = GeocachingStatus() diff --git a/tests/components/homeworks/conftest.py b/tests/components/homeworks/conftest.py index c5d52d20edf..ca0e08e9215 100644 --- a/tests/components/homeworks/conftest.py +++ b/tests/components/homeworks/conftest.py @@ -88,7 +88,7 @@ def mock_empty_config_entry() -> MockConfigEntry: @pytest.fixture -def mock_homeworks() -> Generator[None, MagicMock, None]: +def mock_homeworks() -> Generator[MagicMock]: """Return a mocked Homeworks client.""" with ( patch( diff --git a/tests/components/intellifire/conftest.py b/tests/components/intellifire/conftest.py index d1ddfed2b5b..1aae4fb6dd6 100644 --- a/tests/components/intellifire/conftest.py +++ b/tests/components/intellifire/conftest.py @@ -17,7 +17,7 @@ def mock_setup_entry() -> Generator[AsyncMock]: @pytest.fixture -def mock_fireplace_finder_none() -> Generator[None, MagicMock, None]: +def mock_fireplace_finder_none() -> Generator[MagicMock]: """Mock fireplace finder.""" mock_found_fireplaces = Mock() mock_found_fireplaces.ips = [] @@ -28,7 +28,7 @@ def mock_fireplace_finder_none() -> Generator[None, MagicMock, None]: @pytest.fixture -def mock_fireplace_finder_single() -> Generator[None, MagicMock, None]: +def mock_fireplace_finder_single() -> Generator[MagicMock]: """Mock fireplace finder.""" mock_found_fireplaces = Mock() mock_found_fireplaces.ips = ["192.168.1.69"] @@ -39,7 +39,7 @@ def mock_fireplace_finder_single() -> Generator[None, MagicMock, None]: @pytest.fixture -def mock_intellifire_config_flow() -> Generator[None, MagicMock, None]: +def mock_intellifire_config_flow() -> Generator[MagicMock]: """Return a mocked IntelliFire client.""" data_mock = Mock() data_mock.serial = "12345" diff --git a/tests/components/ipp/conftest.py b/tests/components/ipp/conftest.py index ae098da5698..653a821483a 100644 --- a/tests/components/ipp/conftest.py +++ b/tests/components/ipp/conftest.py @@ -60,9 +60,7 @@ async def mock_printer( @pytest.fixture -def mock_ipp_config_flow( - mock_printer: Printer, -) -> Generator[None, MagicMock, None]: +def mock_ipp_config_flow(mock_printer: Printer) -> Generator[MagicMock]: """Return a mocked IPP client.""" with patch( @@ -76,7 +74,7 @@ def mock_ipp_config_flow( @pytest.fixture def mock_ipp( request: pytest.FixtureRequest, mock_printer: Printer -) -> Generator[None, MagicMock, None]: +) -> Generator[MagicMock]: """Return a mocked IPP client.""" with patch( diff --git a/tests/components/jellyfin/conftest.py b/tests/components/jellyfin/conftest.py index 60b0db61729..40d03212ceb 100644 --- a/tests/components/jellyfin/conftest.py +++ b/tests/components/jellyfin/conftest.py @@ -46,7 +46,7 @@ def mock_setup_entry() -> Generator[AsyncMock]: @pytest.fixture -def mock_client_device_id() -> Generator[None, MagicMock, None]: +def mock_client_device_id() -> Generator[MagicMock]: """Mock generating device id.""" with patch( "homeassistant.components.jellyfin.config_flow._generate_client_device_id" @@ -108,7 +108,7 @@ def mock_client( @pytest.fixture -def mock_jellyfin(mock_client: MagicMock) -> Generator[None, MagicMock, None]: +def mock_jellyfin(mock_client: MagicMock) -> Generator[MagicMock]: """Return a mocked Jellyfin.""" with patch( "homeassistant.components.jellyfin.client_wrapper.Jellyfin", autospec=True diff --git a/tests/components/kaleidescape/conftest.py b/tests/components/kaleidescape/conftest.py index c86d8f2ccd0..f956e620da6 100644 --- a/tests/components/kaleidescape/conftest.py +++ b/tests/components/kaleidescape/conftest.py @@ -1,11 +1,11 @@ """Fixtures for Kaleidescape integration.""" -from collections.abc import Generator from unittest.mock import AsyncMock, patch from kaleidescape import Dispatcher from kaleidescape.device import Automation, Movie, Power, System import pytest +from typing_extensions import Generator from homeassistant.components.kaleidescape.const import DOMAIN from homeassistant.const import CONF_HOST @@ -17,7 +17,7 @@ from tests.common import MockConfigEntry @pytest.fixture(name="mock_device") -def fixture_mock_device() -> Generator[None, AsyncMock, None]: +def fixture_mock_device() -> Generator[AsyncMock]: """Return a mocked Kaleidescape device.""" with patch( "homeassistant.components.kaleidescape.KaleidescapeDevice", autospec=True diff --git a/tests/components/luftdaten/conftest.py b/tests/components/luftdaten/conftest.py index 49e9a85d811..e1aac7caeb0 100644 --- a/tests/components/luftdaten/conftest.py +++ b/tests/components/luftdaten/conftest.py @@ -35,7 +35,7 @@ def mock_setup_entry() -> Generator[None]: @pytest.fixture -def mock_luftdaten() -> Generator[None, MagicMock, None]: +def mock_luftdaten() -> Generator[MagicMock]: """Return a mocked Luftdaten client.""" with ( patch( diff --git a/tests/components/motionmount/conftest.py b/tests/components/motionmount/conftest.py index 7d09351fff6..9e5b0355387 100644 --- a/tests/components/motionmount/conftest.py +++ b/tests/components/motionmount/conftest.py @@ -34,7 +34,7 @@ def mock_setup_entry() -> Generator[AsyncMock]: @pytest.fixture -def mock_motionmount_config_flow() -> Generator[None, MagicMock, None]: +def mock_motionmount_config_flow() -> Generator[MagicMock]: """Return a mocked MotionMount config flow.""" with patch( diff --git a/tests/components/mqtt/conftest.py b/tests/components/mqtt/conftest.py index 91ece381f6d..bc4fa2e6634 100644 --- a/tests/components/mqtt/conftest.py +++ b/tests/components/mqtt/conftest.py @@ -1,10 +1,10 @@ """Test fixtures for mqtt component.""" -from collections.abc import Generator from random import getrandbits from unittest.mock import patch import pytest +from typing_extensions import Generator from tests.components.light.conftest import mock_light_profiles # noqa: F401 @@ -21,7 +21,7 @@ def temp_dir_prefix() -> str: @pytest.fixture -def mock_temp_dir(temp_dir_prefix: str) -> Generator[None, None, str]: +def mock_temp_dir(temp_dir_prefix: str) -> Generator[str]: """Mock the certificate temp directory.""" with patch( # Patch temp dir name to avoid tests fail running in parallel diff --git a/tests/components/open_meteo/conftest.py b/tests/components/open_meteo/conftest.py index b5026fad35d..0d3e1274693 100644 --- a/tests/components/open_meteo/conftest.py +++ b/tests/components/open_meteo/conftest.py @@ -35,7 +35,7 @@ def mock_setup_entry() -> Generator[None]: @pytest.fixture -def mock_open_meteo(request: pytest.FixtureRequest) -> Generator[None, MagicMock, None]: +def mock_open_meteo(request: pytest.FixtureRequest) -> Generator[MagicMock]: """Return a mocked Open-Meteo client.""" fixture: str = "forecast.json" if hasattr(request, "param") and request.param: diff --git a/tests/components/plugwise/conftest.py b/tests/components/plugwise/conftest.py index 7264922cd86..83826a0a543 100644 --- a/tests/components/plugwise/conftest.py +++ b/tests/components/plugwise/conftest.py @@ -56,7 +56,7 @@ def mock_setup_entry() -> Generator[AsyncMock]: @pytest.fixture -def mock_smile_config_flow() -> Generator[None, MagicMock, None]: +def mock_smile_config_flow() -> Generator[MagicMock]: """Return a mocked Smile client.""" with patch( "homeassistant.components.plugwise.config_flow.Smile", @@ -71,7 +71,7 @@ def mock_smile_config_flow() -> Generator[None, MagicMock, None]: @pytest.fixture -def mock_smile_adam() -> Generator[None, MagicMock, None]: +def mock_smile_adam() -> Generator[MagicMock]: """Create a Mock Adam environment for testing exceptions.""" chosen_env = "adam_multiple_devices_per_zone" @@ -97,7 +97,7 @@ def mock_smile_adam() -> Generator[None, MagicMock, None]: @pytest.fixture -def mock_smile_adam_2() -> Generator[None, MagicMock, None]: +def mock_smile_adam_2() -> Generator[MagicMock]: """Create a 2nd Mock Adam environment for testing exceptions.""" chosen_env = "m_adam_heating" @@ -123,7 +123,7 @@ def mock_smile_adam_2() -> Generator[None, MagicMock, None]: @pytest.fixture -def mock_smile_adam_3() -> Generator[None, MagicMock, None]: +def mock_smile_adam_3() -> Generator[MagicMock]: """Create a 3rd Mock Adam environment for testing exceptions.""" chosen_env = "m_adam_cooling" @@ -149,7 +149,7 @@ def mock_smile_adam_3() -> Generator[None, MagicMock, None]: @pytest.fixture -def mock_smile_adam_4() -> Generator[None, MagicMock, None]: +def mock_smile_adam_4() -> Generator[MagicMock]: """Create a 4th Mock Adam environment for testing exceptions.""" chosen_env = "m_adam_jip" @@ -175,7 +175,7 @@ def mock_smile_adam_4() -> Generator[None, MagicMock, None]: @pytest.fixture -def mock_smile_anna() -> Generator[None, MagicMock, None]: +def mock_smile_anna() -> Generator[MagicMock]: """Create a Mock Anna environment for testing exceptions.""" chosen_env = "anna_heatpump_heating" with patch( @@ -200,7 +200,7 @@ def mock_smile_anna() -> Generator[None, MagicMock, None]: @pytest.fixture -def mock_smile_anna_2() -> Generator[None, MagicMock, None]: +def mock_smile_anna_2() -> Generator[MagicMock]: """Create a 2nd Mock Anna environment for testing exceptions.""" chosen_env = "m_anna_heatpump_cooling" with patch( @@ -225,7 +225,7 @@ def mock_smile_anna_2() -> Generator[None, MagicMock, None]: @pytest.fixture -def mock_smile_anna_3() -> Generator[None, MagicMock, None]: +def mock_smile_anna_3() -> Generator[MagicMock]: """Create a 3rd Mock Anna environment for testing exceptions.""" chosen_env = "m_anna_heatpump_idle" with patch( @@ -250,7 +250,7 @@ def mock_smile_anna_3() -> Generator[None, MagicMock, None]: @pytest.fixture -def mock_smile_p1() -> Generator[None, MagicMock, None]: +def mock_smile_p1() -> Generator[MagicMock]: """Create a Mock P1 DSMR environment for testing exceptions.""" chosen_env = "p1v4_442_single" with patch( @@ -275,7 +275,7 @@ def mock_smile_p1() -> Generator[None, MagicMock, None]: @pytest.fixture -def mock_smile_p1_2() -> Generator[None, MagicMock, None]: +def mock_smile_p1_2() -> Generator[MagicMock]: """Create a Mock P1 3-phase DSMR environment for testing exceptions.""" chosen_env = "p1v4_442_triple" with patch( @@ -300,7 +300,7 @@ def mock_smile_p1_2() -> Generator[None, MagicMock, None]: @pytest.fixture -def mock_stretch() -> Generator[None, MagicMock, None]: +def mock_stretch() -> Generator[MagicMock]: """Create a Mock Stretch environment for testing exceptions.""" chosen_env = "stretch_v31" with patch( diff --git a/tests/components/pure_energie/conftest.py b/tests/components/pure_energie/conftest.py index 5abee8d9488..12a996049bb 100644 --- a/tests/components/pure_energie/conftest.py +++ b/tests/components/pure_energie/conftest.py @@ -37,7 +37,7 @@ def mock_setup_entry() -> Generator[None]: @pytest.fixture def mock_pure_energie_config_flow( request: pytest.FixtureRequest, -) -> Generator[None, MagicMock, None]: +) -> Generator[MagicMock]: """Return a mocked Pure Energie client.""" with patch( "homeassistant.components.pure_energie.config_flow.GridNet", autospec=True diff --git a/tests/components/pvoutput/conftest.py b/tests/components/pvoutput/conftest.py index e3f0b253279..d19f09d9e6c 100644 --- a/tests/components/pvoutput/conftest.py +++ b/tests/components/pvoutput/conftest.py @@ -36,7 +36,7 @@ def mock_setup_entry() -> Generator[AsyncMock]: @pytest.fixture -def mock_pvoutput() -> Generator[None, MagicMock, None]: +def mock_pvoutput() -> Generator[MagicMock]: """Return a mocked PVOutput client.""" with ( patch( diff --git a/tests/components/rdw/conftest.py b/tests/components/rdw/conftest.py index 47d7b02c950..3f45f44e3d8 100644 --- a/tests/components/rdw/conftest.py +++ b/tests/components/rdw/conftest.py @@ -33,7 +33,7 @@ def mock_setup_entry() -> Generator[None]: @pytest.fixture -def mock_rdw_config_flow() -> Generator[None, MagicMock, None]: +def mock_rdw_config_flow() -> Generator[MagicMock]: """Return a mocked RDW client.""" with patch( "homeassistant.components.rdw.config_flow.RDW", autospec=True @@ -44,7 +44,7 @@ def mock_rdw_config_flow() -> Generator[None, MagicMock, None]: @pytest.fixture -def mock_rdw(request: pytest.FixtureRequest) -> Generator[None, MagicMock, None]: +def mock_rdw(request: pytest.FixtureRequest) -> Generator[MagicMock]: """Return a mocked WLED client.""" fixture: str = "rdw/11ZKZ3.json" if hasattr(request, "param") and request.param: diff --git a/tests/components/roku/conftest.py b/tests/components/roku/conftest.py index 09e62933d3d..ed36dd09517 100644 --- a/tests/components/roku/conftest.py +++ b/tests/components/roku/conftest.py @@ -51,9 +51,7 @@ async def mock_device( @pytest.fixture -def mock_roku_config_flow( - mock_device: RokuDevice, -) -> Generator[None, MagicMock, None]: +def mock_roku_config_flow(mock_device: RokuDevice) -> Generator[MagicMock]: """Return a mocked Roku client.""" with patch( @@ -68,7 +66,7 @@ def mock_roku_config_flow( @pytest.fixture def mock_roku( request: pytest.FixtureRequest, mock_device: RokuDevice -) -> Generator[None, MagicMock, None]: +) -> Generator[MagicMock]: """Return a mocked Roku client.""" with patch( diff --git a/tests/components/sonarr/conftest.py b/tests/components/sonarr/conftest.py index 06a08eb7724..739880a99aa 100644 --- a/tests/components/sonarr/conftest.py +++ b/tests/components/sonarr/conftest.py @@ -109,7 +109,7 @@ def mock_setup_entry() -> Generator[None]: @pytest.fixture -def mock_sonarr_config_flow() -> Generator[None, MagicMock, None]: +def mock_sonarr_config_flow() -> Generator[MagicMock]: """Return a mocked Sonarr client.""" with patch( "homeassistant.components.sonarr.config_flow.SonarrClient", autospec=True @@ -127,7 +127,7 @@ def mock_sonarr_config_flow() -> Generator[None, MagicMock, None]: @pytest.fixture -def mock_sonarr() -> Generator[None, MagicMock, None]: +def mock_sonarr() -> Generator[MagicMock]: """Return a mocked Sonarr client.""" with patch( "homeassistant.components.sonarr.SonarrClient", autospec=True diff --git a/tests/components/srp_energy/conftest.py b/tests/components/srp_energy/conftest.py index b83fff778ac..45eb726443f 100644 --- a/tests/components/srp_energy/conftest.py +++ b/tests/components/srp_energy/conftest.py @@ -2,12 +2,12 @@ from __future__ import annotations -from collections.abc import Generator import datetime as dt from unittest.mock import MagicMock, patch from freezegun.api import FrozenDateTimeFactory import pytest +from typing_extensions import Generator from homeassistant.components.srp_energy.const import DOMAIN, PHOENIX_TIME_ZONE from homeassistant.const import CONF_ID @@ -48,7 +48,7 @@ def fixture_mock_config_entry() -> MockConfigEntry: @pytest.fixture(name="mock_srp_energy") -def fixture_mock_srp_energy() -> Generator[None, MagicMock, None]: +def fixture_mock_srp_energy() -> Generator[MagicMock]: """Return a mocked SrpEnergyClient client.""" with patch( "homeassistant.components.srp_energy.SrpEnergyClient", autospec=True @@ -60,7 +60,7 @@ def fixture_mock_srp_energy() -> Generator[None, MagicMock, None]: @pytest.fixture(name="mock_srp_energy_config_flow") -def fixture_mock_srp_energy_config_flow() -> Generator[None, MagicMock, None]: +def fixture_mock_srp_energy_config_flow() -> Generator[MagicMock]: """Return a mocked config_flow SrpEnergyClient client.""" with patch( "homeassistant.components.srp_energy.config_flow.SrpEnergyClient", autospec=True diff --git a/tests/components/tailscale/conftest.py b/tests/components/tailscale/conftest.py index c07717cd31e..cb7419daf89 100644 --- a/tests/components/tailscale/conftest.py +++ b/tests/components/tailscale/conftest.py @@ -36,7 +36,7 @@ def mock_setup_entry() -> Generator[AsyncMock]: @pytest.fixture -def mock_tailscale_config_flow() -> Generator[None, MagicMock, None]: +def mock_tailscale_config_flow() -> Generator[MagicMock]: """Return a mocked Tailscale client.""" with patch( "homeassistant.components.tailscale.config_flow.Tailscale", autospec=True @@ -49,7 +49,7 @@ def mock_tailscale_config_flow() -> Generator[None, MagicMock, None]: @pytest.fixture -def mock_tailscale(request: pytest.FixtureRequest) -> Generator[None, MagicMock, None]: +def mock_tailscale(request: pytest.FixtureRequest) -> Generator[MagicMock]: """Return a mocked Tailscale client.""" fixture: str = "tailscale/devices.json" if hasattr(request, "param") and request.param: diff --git a/tests/components/ukraine_alarm/test_config_flow.py b/tests/components/ukraine_alarm/test_config_flow.py index ba37f188079..58b5dde2bac 100644 --- a/tests/components/ukraine_alarm/test_config_flow.py +++ b/tests/components/ukraine_alarm/test_config_flow.py @@ -1,10 +1,10 @@ """Test the Ukraine Alarm config flow.""" -from collections.abc import Generator from unittest.mock import AsyncMock, patch from aiohttp import ClientConnectionError, ClientError, ClientResponseError, RequestInfo import pytest +from typing_extensions import Generator from yarl import URL from homeassistant import config_entries @@ -41,7 +41,7 @@ REGIONS = { @pytest.fixture(autouse=True) -def mock_get_regions() -> Generator[None, AsyncMock, None]: +def mock_get_regions() -> Generator[AsyncMock]: """Mock the get_regions method.""" with patch( diff --git a/tests/components/verisure/conftest.py b/tests/components/verisure/conftest.py index 401f0e05d7c..03086ac2ead 100644 --- a/tests/components/verisure/conftest.py +++ b/tests/components/verisure/conftest.py @@ -38,7 +38,7 @@ def mock_setup_entry() -> Generator[AsyncMock]: @pytest.fixture -def mock_verisure_config_flow() -> Generator[None, MagicMock, None]: +def mock_verisure_config_flow() -> Generator[MagicMock]: """Return a mocked Tailscale client.""" with patch( "homeassistant.components.verisure.config_flow.Verisure", autospec=True diff --git a/tests/components/wake_on_lan/conftest.py b/tests/components/wake_on_lan/conftest.py index 66782531ef1..cec3076d83e 100644 --- a/tests/components/wake_on_lan/conftest.py +++ b/tests/components/wake_on_lan/conftest.py @@ -2,10 +2,10 @@ from __future__ import annotations -from collections.abc import Generator from unittest.mock import AsyncMock, MagicMock, patch import pytest +from typing_extensions import Generator @pytest.fixture @@ -22,9 +22,7 @@ def subprocess_call_return_value() -> int | None: @pytest.fixture(autouse=True) -def mock_subprocess_call( - subprocess_call_return_value: int, -) -> Generator[None, None, MagicMock]: +def mock_subprocess_call(subprocess_call_return_value: int) -> Generator[MagicMock]: """Mock magic packet.""" with patch("homeassistant.components.wake_on_lan.switch.sp.call") as mock_sp: mock_sp.return_value = subprocess_call_return_value diff --git a/tests/components/zamg/conftest.py b/tests/components/zamg/conftest.py index 164c943c2ac..fa2eccf42d1 100644 --- a/tests/components/zamg/conftest.py +++ b/tests/components/zamg/conftest.py @@ -37,9 +37,7 @@ def mock_setup_entry() -> Generator[None]: @pytest.fixture -def mock_zamg_config_flow( - request: pytest.FixtureRequest, -) -> Generator[None, MagicMock, None]: +def mock_zamg_config_flow(request: pytest.FixtureRequest) -> Generator[MagicMock]: """Return a mocked Zamg client.""" with patch( "homeassistant.components.zamg.sensor.ZamgData", autospec=True @@ -53,7 +51,7 @@ def mock_zamg_config_flow( @pytest.fixture -def mock_zamg(request: pytest.FixtureRequest) -> Generator[None, MagicMock, None]: +def mock_zamg(request: pytest.FixtureRequest) -> Generator[MagicMock]: """Return a mocked Zamg client.""" with patch( @@ -72,9 +70,7 @@ def mock_zamg(request: pytest.FixtureRequest) -> Generator[None, MagicMock, None @pytest.fixture -def mock_zamg_coordinator( - request: pytest.FixtureRequest, -) -> Generator[None, MagicMock, None]: +def mock_zamg_coordinator(request: pytest.FixtureRequest) -> Generator[MagicMock]: """Return a mocked Zamg client.""" with patch( @@ -93,9 +89,7 @@ def mock_zamg_coordinator( @pytest.fixture -def mock_zamg_stations( - request: pytest.FixtureRequest, -) -> Generator[None, MagicMock, None]: +def mock_zamg_stations(request: pytest.FixtureRequest) -> Generator[MagicMock]: """Return a mocked Zamg client.""" with patch( "homeassistant.components.zamg.config_flow.ZamgData.zamg_stations" From 8628a1e44932a086cc59c1ca95a6607c6a2bfee8 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 7 Jun 2024 09:03:35 +0200 Subject: [PATCH 0323/1445] Improve type hints in airnow tests (#119038) --- tests/components/airnow/conftest.py | 23 ++++++++------ tests/components/airnow/test_config_flow.py | 34 +++++++++++++++------ tests/components/airnow/test_diagnostics.py | 6 ++-- 3 files changed, 43 insertions(+), 20 deletions(-) diff --git a/tests/components/airnow/conftest.py b/tests/components/airnow/conftest.py index db4400f85d3..676595250f1 100644 --- a/tests/components/airnow/conftest.py +++ b/tests/components/airnow/conftest.py @@ -1,18 +1,23 @@ """Define fixtures for AirNow tests.""" -import json +from typing import Any from unittest.mock import AsyncMock, patch import pytest +from typing_extensions import Generator from homeassistant.components.airnow import DOMAIN from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_RADIUS +from homeassistant.core import HomeAssistant +from homeassistant.util.json import JsonArrayType -from tests.common import MockConfigEntry, load_fixture +from tests.common import MockConfigEntry, load_json_array_fixture @pytest.fixture(name="config_entry") -def config_entry_fixture(hass, config, options): +def config_entry_fixture( + hass: HomeAssistant, config: dict[str, Any], options: dict[str, Any] +) -> MockConfigEntry: """Define a config entry fixture.""" entry = MockConfigEntry( domain=DOMAIN, @@ -27,7 +32,7 @@ def config_entry_fixture(hass, config, options): @pytest.fixture(name="config") -def config_fixture(hass): +def config_fixture() -> dict[str, Any]: """Define a config entry data fixture.""" return { CONF_API_KEY: "abc123", @@ -37,7 +42,7 @@ def config_fixture(hass): @pytest.fixture(name="options") -def options_fixture(hass): +def options_fixture() -> dict[str, Any]: """Define a config options data fixture.""" return { CONF_RADIUS: 150, @@ -45,19 +50,19 @@ def options_fixture(hass): @pytest.fixture(name="data", scope="package") -def data_fixture(): +def data_fixture() -> JsonArrayType: """Define a fixture for response data.""" - return json.loads(load_fixture("response.json", "airnow")) + return load_json_array_fixture("response.json", "airnow") @pytest.fixture(name="mock_api_get") -def mock_api_get_fixture(data): +def mock_api_get_fixture(data: JsonArrayType) -> AsyncMock: """Define a fixture for a mock "get" coroutine function.""" return AsyncMock(return_value=data) @pytest.fixture(name="setup_airnow") -async def setup_airnow_fixture(hass, config, mock_api_get): +def setup_airnow_fixture(mock_api_get: AsyncMock) -> Generator[None]: """Define a fixture to set up AirNow.""" with ( patch("pyairnow.WebServiceAPI._get", mock_api_get), diff --git a/tests/components/airnow/test_config_flow.py b/tests/components/airnow/test_config_flow.py index b62cb43844b..6507eea1fcb 100644 --- a/tests/components/airnow/test_config_flow.py +++ b/tests/components/airnow/test_config_flow.py @@ -1,5 +1,6 @@ """Test the AirNow config flow.""" +from typing import Any from unittest.mock import AsyncMock, patch from pyairnow.errors import AirNowError, EmptyResponseError, InvalidKeyError @@ -14,7 +15,10 @@ from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry -async def test_form(hass: HomeAssistant, config, options, setup_airnow) -> None: +@pytest.mark.usefixtures("setup_airnow") +async def test_form( + hass: HomeAssistant, config: dict[str, Any], options: dict[str, Any] +) -> None: """Test we get the form.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -29,7 +33,8 @@ async def test_form(hass: HomeAssistant, config, options, setup_airnow) -> None: @pytest.mark.parametrize("mock_api_get", [AsyncMock(side_effect=InvalidKeyError)]) -async def test_form_invalid_auth(hass: HomeAssistant, config, setup_airnow) -> None: +@pytest.mark.usefixtures("setup_airnow") +async def test_form_invalid_auth(hass: HomeAssistant, config: dict[str, Any]) -> None: """Test we handle invalid auth.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -40,7 +45,10 @@ async def test_form_invalid_auth(hass: HomeAssistant, config, setup_airnow) -> N @pytest.mark.parametrize("data", [{}]) -async def test_form_invalid_location(hass: HomeAssistant, config, setup_airnow) -> None: +@pytest.mark.usefixtures("setup_airnow") +async def test_form_invalid_location( + hass: HomeAssistant, config: dict[str, Any] +) -> None: """Test we handle invalid location.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -51,7 +59,8 @@ async def test_form_invalid_location(hass: HomeAssistant, config, setup_airnow) @pytest.mark.parametrize("mock_api_get", [AsyncMock(side_effect=AirNowError)]) -async def test_form_cannot_connect(hass: HomeAssistant, config, setup_airnow) -> None: +@pytest.mark.usefixtures("setup_airnow") +async def test_form_cannot_connect(hass: HomeAssistant, config: dict[str, Any]) -> None: """Test we handle cannot connect error.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -62,7 +71,8 @@ async def test_form_cannot_connect(hass: HomeAssistant, config, setup_airnow) -> @pytest.mark.parametrize("mock_api_get", [AsyncMock(side_effect=EmptyResponseError)]) -async def test_form_empty_result(hass: HomeAssistant, config, setup_airnow) -> None: +@pytest.mark.usefixtures("setup_airnow") +async def test_form_empty_result(hass: HomeAssistant, config: dict[str, Any]) -> None: """Test we handle empty response error.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -73,7 +83,8 @@ async def test_form_empty_result(hass: HomeAssistant, config, setup_airnow) -> N @pytest.mark.parametrize("mock_api_get", [AsyncMock(side_effect=RuntimeError)]) -async def test_form_unexpected(hass: HomeAssistant, config, setup_airnow) -> None: +@pytest.mark.usefixtures("setup_airnow") +async def test_form_unexpected(hass: HomeAssistant, config: dict[str, Any]) -> None: """Test we handle an unexpected error.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -83,7 +94,10 @@ async def test_form_unexpected(hass: HomeAssistant, config, setup_airnow) -> Non assert result2["errors"] == {"base": "unknown"} -async def test_entry_already_exists(hass: HomeAssistant, config, config_entry) -> None: +@pytest.mark.usefixtures("config_entry") +async def test_entry_already_exists( + hass: HomeAssistant, config: dict[str, Any] +) -> None: """Test that the form aborts if the Lat/Lng is already configured.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -93,7 +107,8 @@ async def test_entry_already_exists(hass: HomeAssistant, config, config_entry) - assert result2["reason"] == "already_configured" -async def test_config_migration_v2(hass: HomeAssistant, setup_airnow) -> None: +@pytest.mark.usefixtures("setup_airnow") +async def test_config_migration_v2(hass: HomeAssistant) -> None: """Test that the config migration from Version 1 to Version 2 works.""" config_entry = MockConfigEntry( version=1, @@ -119,7 +134,8 @@ async def test_config_migration_v2(hass: HomeAssistant, setup_airnow) -> None: assert config_entry.options.get(CONF_RADIUS) == 25 -async def test_options_flow(hass: HomeAssistant, setup_airnow) -> None: +@pytest.mark.usefixtures("setup_airnow") +async def test_options_flow(hass: HomeAssistant) -> None: """Test that the options flow works.""" config_entry = MockConfigEntry( version=2, diff --git a/tests/components/airnow/test_diagnostics.py b/tests/components/airnow/test_diagnostics.py index 78f6c410fdf..a1348b49531 100644 --- a/tests/components/airnow/test_diagnostics.py +++ b/tests/components/airnow/test_diagnostics.py @@ -1,18 +1,20 @@ """Test AirNow diagnostics.""" +import pytest from syrupy import SnapshotAssertion from homeassistant.core import HomeAssistant +from tests.common import MockConfigEntry from tests.components.diagnostics import get_diagnostics_for_config_entry from tests.typing import ClientSessionGenerator +@pytest.mark.usefixtures("setup_airnow") async def test_entry_diagnostics( hass: HomeAssistant, - config_entry, + config_entry: MockConfigEntry, hass_client: ClientSessionGenerator, - setup_airnow, snapshot: SnapshotAssertion, ) -> None: """Test config entry diagnostics.""" From aa0a90cd98e417cd53b4c69763fa23aa33dbab97 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 7 Jun 2024 02:07:47 -0500 Subject: [PATCH 0324/1445] Fix remember_the_milk calling configurator async api from the wrong thread (#119029) --- homeassistant/components/remember_the_milk/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/remember_the_milk/__init__.py b/homeassistant/components/remember_the_milk/__init__.py index 3d1654960a7..425a12d5c4d 100644 --- a/homeassistant/components/remember_the_milk/__init__.py +++ b/homeassistant/components/remember_the_milk/__init__.py @@ -137,7 +137,7 @@ def _register_new_account( configurator.request_done(hass, request_id) - request_id = configurator.async_request_config( + request_id = configurator.request_config( hass, f"{DOMAIN} - {account_name}", callback=register_account_callback, From 6027af3d3603a0e3053fd1b464d70ddb89e1f1d1 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 7 Jun 2024 09:17:36 +0200 Subject: [PATCH 0325/1445] Fix unit of measurement for airgradient sensor (#118981) --- homeassistant/components/airgradient/sensor.py | 1 + homeassistant/components/airgradient/strings.json | 2 +- .../airgradient/snapshots/test_sensor.ambr | 15 ++++++++------- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/airgradient/sensor.py b/homeassistant/components/airgradient/sensor.py index e2fc580fce5..f21f13b80ab 100644 --- a/homeassistant/components/airgradient/sensor.py +++ b/homeassistant/components/airgradient/sensor.py @@ -103,6 +103,7 @@ SENSOR_TYPES: tuple[AirGradientSensorEntityDescription, ...] = ( AirGradientSensorEntityDescription( key="pm003", translation_key="pm003_count", + native_unit_of_measurement="particles/dL", state_class=SensorStateClass.MEASUREMENT, value_fn=lambda status: status.pm003_count, ), diff --git a/homeassistant/components/airgradient/strings.json b/homeassistant/components/airgradient/strings.json index 3b1e9f9ee41..20322eed33c 100644 --- a/homeassistant/components/airgradient/strings.json +++ b/homeassistant/components/airgradient/strings.json @@ -48,7 +48,7 @@ "name": "Nitrogen index" }, "pm003_count": { - "name": "PM0.3 count" + "name": "PM0.3" }, "raw_total_volatile_organic_component": { "name": "Raw total VOC" diff --git a/tests/components/airgradient/snapshots/test_sensor.ambr b/tests/components/airgradient/snapshots/test_sensor.ambr index 27d8043a395..b9b6be41ff4 100644 --- a/tests/components/airgradient/snapshots/test_sensor.ambr +++ b/tests/components/airgradient/snapshots/test_sensor.ambr @@ -150,7 +150,7 @@ 'state': '1', }) # --- -# name: test_all_entities[sensor.airgradient_pm0_3_count-entry] +# name: test_all_entities[sensor.airgradient_pm0_3-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -164,7 +164,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.airgradient_pm0_3_count', + 'entity_id': 'sensor.airgradient_pm0_3', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -176,23 +176,24 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'PM0.3 count', + 'original_name': 'PM0.3', 'platform': 'airgradient', 'previous_unique_id': None, 'supported_features': 0, 'translation_key': 'pm003_count', 'unique_id': '84fce612f5b8-pm003', - 'unit_of_measurement': None, + 'unit_of_measurement': 'particles/dL', }) # --- -# name: test_all_entities[sensor.airgradient_pm0_3_count-state] +# name: test_all_entities[sensor.airgradient_pm0_3-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Airgradient PM0.3 count', + 'friendly_name': 'Airgradient PM0.3', 'state_class': , + 'unit_of_measurement': 'particles/dL', }), 'context': , - 'entity_id': 'sensor.airgradient_pm0_3_count', + 'entity_id': 'sensor.airgradient_pm0_3', 'last_changed': , 'last_reported': , 'last_updated': , From 6e9db52a5f6a13c3ee9b89fe9df12af80bbab11f Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 7 Jun 2024 09:18:16 +0200 Subject: [PATCH 0326/1445] Fix AirGradient name (#119046) --- homeassistant/components/airgradient/manifest.json | 2 +- homeassistant/generated/integrations.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/airgradient/manifest.json b/homeassistant/components/airgradient/manifest.json index c30d7a4c42f..b9a1e2da54f 100644 --- a/homeassistant/components/airgradient/manifest.json +++ b/homeassistant/components/airgradient/manifest.json @@ -1,6 +1,6 @@ { "domain": "airgradient", - "name": "Airgradient", + "name": "AirGradient", "codeowners": ["@airgradienthq", "@joostlek"], "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/airgradient", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 9d7ffca6246..7f2f4292748 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -94,7 +94,7 @@ "iot_class": "local_polling" }, "airgradient": { - "name": "Airgradient", + "name": "AirGradient", "integration_type": "device", "config_flow": true, "iot_class": "local_polling" From 4f6a98cee3bb05287d7fff0b865abb07a835b13d Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 7 Jun 2024 09:19:03 +0200 Subject: [PATCH 0327/1445] Remove unused request fixtures (#119044) --- tests/components/backup/test_websocket.py | 1 - tests/components/bmw_connected_drive/conftest.py | 4 +--- tests/components/bsblan/conftest.py | 2 +- tests/components/ipp/conftest.py | 4 +--- tests/components/lamarzocco/conftest.py | 4 +--- tests/components/openexchangerates/conftest.py | 4 +--- tests/components/pure_energie/conftest.py | 4 +--- tests/components/roku/conftest.py | 4 +--- 8 files changed, 7 insertions(+), 20 deletions(-) diff --git a/tests/components/backup/test_websocket.py b/tests/components/backup/test_websocket.py index 79d682c69fe..e11278202e0 100644 --- a/tests/components/backup/test_websocket.py +++ b/tests/components/backup/test_websocket.py @@ -120,7 +120,6 @@ async def test_backup_end( hass: HomeAssistant, hass_ws_client: WebSocketGenerator, snapshot: SnapshotAssertion, - request: pytest.FixtureRequest, sync_access_token_proxy: str, *, access_token_fixture_name: str, diff --git a/tests/components/bmw_connected_drive/conftest.py b/tests/components/bmw_connected_drive/conftest.py index a3db2cea91f..f69763dae77 100644 --- a/tests/components/bmw_connected_drive/conftest.py +++ b/tests/components/bmw_connected_drive/conftest.py @@ -9,9 +9,7 @@ from typing_extensions import Generator @pytest.fixture -def bmw_fixture( - request: pytest.FixtureRequest, monkeypatch: pytest.MonkeyPatch -) -> Generator[respx.MockRouter]: +def bmw_fixture(monkeypatch: pytest.MonkeyPatch) -> Generator[respx.MockRouter]: """Patch MyBMW login API calls.""" # we use the library's mock router to mock the API calls, but only with a subset of vehicles diff --git a/tests/components/bsblan/conftest.py b/tests/components/bsblan/conftest.py index 8309b1d64ef..224e0e0b157 100644 --- a/tests/components/bsblan/conftest.py +++ b/tests/components/bsblan/conftest.py @@ -40,7 +40,7 @@ def mock_setup_entry() -> Generator[AsyncMock]: @pytest.fixture -def mock_bsblan(request: pytest.FixtureRequest) -> Generator[MagicMock]: +def mock_bsblan() -> Generator[MagicMock]: """Return a mocked BSBLAN client.""" with ( diff --git a/tests/components/ipp/conftest.py b/tests/components/ipp/conftest.py index 653a821483a..5e39a16f3b1 100644 --- a/tests/components/ipp/conftest.py +++ b/tests/components/ipp/conftest.py @@ -72,9 +72,7 @@ def mock_ipp_config_flow(mock_printer: Printer) -> Generator[MagicMock]: @pytest.fixture -def mock_ipp( - request: pytest.FixtureRequest, mock_printer: Printer -) -> Generator[MagicMock]: +def mock_ipp(mock_printer: Printer) -> Generator[MagicMock]: """Return a mocked IPP client.""" with patch( diff --git a/tests/components/lamarzocco/conftest.py b/tests/components/lamarzocco/conftest.py index 13d2154735d..49aa20e3a46 100644 --- a/tests/components/lamarzocco/conftest.py +++ b/tests/components/lamarzocco/conftest.py @@ -57,9 +57,7 @@ def device_fixture() -> LaMarzoccoModel: @pytest.fixture -def mock_lamarzocco( - request: pytest.FixtureRequest, device_fixture: LaMarzoccoModel -) -> Generator[MagicMock]: +def mock_lamarzocco(device_fixture: LaMarzoccoModel) -> Generator[MagicMock]: """Return a mocked LM client.""" model_name = device_fixture diff --git a/tests/components/openexchangerates/conftest.py b/tests/components/openexchangerates/conftest.py index fa3c9cd6de0..6bd7da2c7af 100644 --- a/tests/components/openexchangerates/conftest.py +++ b/tests/components/openexchangerates/conftest.py @@ -29,9 +29,7 @@ def mock_setup_entry() -> Generator[AsyncMock]: @pytest.fixture -def mock_latest_rates_config_flow( - request: pytest.FixtureRequest, -) -> Generator[AsyncMock]: +def mock_latest_rates_config_flow() -> Generator[AsyncMock]: """Return a mocked WLED client.""" with patch( "homeassistant.components.openexchangerates.config_flow.Client.get_latest", diff --git a/tests/components/pure_energie/conftest.py b/tests/components/pure_energie/conftest.py index 12a996049bb..7174befbf5b 100644 --- a/tests/components/pure_energie/conftest.py +++ b/tests/components/pure_energie/conftest.py @@ -35,9 +35,7 @@ def mock_setup_entry() -> Generator[None]: @pytest.fixture -def mock_pure_energie_config_flow( - request: pytest.FixtureRequest, -) -> Generator[MagicMock]: +def mock_pure_energie_config_flow() -> Generator[MagicMock]: """Return a mocked Pure Energie client.""" with patch( "homeassistant.components.pure_energie.config_flow.GridNet", autospec=True diff --git a/tests/components/roku/conftest.py b/tests/components/roku/conftest.py index ed36dd09517..160a1bf3127 100644 --- a/tests/components/roku/conftest.py +++ b/tests/components/roku/conftest.py @@ -64,9 +64,7 @@ def mock_roku_config_flow(mock_device: RokuDevice) -> Generator[MagicMock]: @pytest.fixture -def mock_roku( - request: pytest.FixtureRequest, mock_device: RokuDevice -) -> Generator[MagicMock]: +def mock_roku(mock_device: RokuDevice) -> Generator[MagicMock]: """Return a mocked Roku client.""" with patch( From c60dee16bc75457b6410ee26cb216bc5e309d9d0 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 7 Jun 2024 09:21:04 +0200 Subject: [PATCH 0328/1445] Ignore deprecation warning in python-holidays (#119007) --- pyproject.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index beda86314a7..f956f77250f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -526,6 +526,8 @@ filterwarnings = [ "ignore:pkg_resources is deprecated as an API:DeprecationWarning:ttls", # https://github.com/mvantellingen/python-zeep/pull/1364 - >4.2.1 "ignore:'cgi' is deprecated and slated for removal in Python 3.13:DeprecationWarning:zeep.utils", + # https://github.com/vacanza/python-holidays/discussions/1800 + "ignore::DeprecationWarning:holidays", # -- fixed for Python 3.13 # https://github.com/rhasspy/wyoming/commit/e34af30d455b6f2bb9e5cfb25fad8d276914bc54 - >=1.4.2 From 6ba8b7a5d6f0cf39d0f77e9dc748259a0d68d240 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 7 Jun 2024 02:21:53 -0500 Subject: [PATCH 0329/1445] Remove isal from after_dependencies in http (#119000) --- homeassistant/bootstrap.py | 13 ++++++++++--- homeassistant/components/http/manifest.json | 1 - tests/test_circular_imports.py | 4 ++-- tests/test_requirements.py | 9 ++++----- 4 files changed, 16 insertions(+), 11 deletions(-) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 391c6ebfa45..74196cdc625 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -134,8 +134,15 @@ COOLDOWN_TIME = 60 DEBUGGER_INTEGRATIONS = {"debugpy"} + +# Core integrations are unconditionally loaded CORE_INTEGRATIONS = {"homeassistant", "persistent_notification"} -LOGGING_INTEGRATIONS = { + +# Integrations that are loaded right after the core is set up +LOGGING_AND_HTTP_DEPS_INTEGRATIONS = { + # isal is loaded right away before `http` to ensure if its + # enabled, that `isal` is up to date. + "isal", # Set log levels "logger", # Error logging @@ -214,8 +221,8 @@ CRITICAL_INTEGRATIONS = { } SETUP_ORDER = ( - # Load logging as soon as possible - ("logging", LOGGING_INTEGRATIONS), + # Load logging and http deps as soon as possible + ("logging, http deps", LOGGING_AND_HTTP_DEPS_INTEGRATIONS), # Setup frontend and recorder ("frontend, recorder", {*FRONTEND_INTEGRATIONS, *RECORDER_INTEGRATIONS}), # Start up debuggers. Start these first in case they want to wait. diff --git a/homeassistant/components/http/manifest.json b/homeassistant/components/http/manifest.json index b48a188cf47..fb804251edc 100644 --- a/homeassistant/components/http/manifest.json +++ b/homeassistant/components/http/manifest.json @@ -1,7 +1,6 @@ { "domain": "http", "name": "HTTP", - "after_dependencies": ["isal"], "codeowners": ["@home-assistant/core"], "documentation": "https://www.home-assistant.io/integrations/http", "integration_type": "system", diff --git a/tests/test_circular_imports.py b/tests/test_circular_imports.py index 79f0fd9caf7..dfdee65b2b0 100644 --- a/tests/test_circular_imports.py +++ b/tests/test_circular_imports.py @@ -10,7 +10,7 @@ from homeassistant.bootstrap import ( DEBUGGER_INTEGRATIONS, DEFAULT_INTEGRATIONS, FRONTEND_INTEGRATIONS, - LOGGING_INTEGRATIONS, + LOGGING_AND_HTTP_DEPS_INTEGRATIONS, RECORDER_INTEGRATIONS, STAGE_1_INTEGRATIONS, ) @@ -23,7 +23,7 @@ from homeassistant.bootstrap import ( { *DEBUGGER_INTEGRATIONS, *CORE_INTEGRATIONS, - *LOGGING_INTEGRATIONS, + *LOGGING_AND_HTTP_DEPS_INTEGRATIONS, *FRONTEND_INTEGRATIONS, *RECORDER_INTEGRATIONS, *STAGE_1_INTEGRATIONS, diff --git a/tests/test_requirements.py b/tests/test_requirements.py index 2b2415e22a8..73f3f54c3c4 100644 --- a/tests/test_requirements.py +++ b/tests/test_requirements.py @@ -591,7 +591,7 @@ async def test_discovery_requirements_mqtt(hass: HomeAssistant) -> None: ) as mock_process: await async_get_integration_with_requirements(hass, "mqtt_comp") - assert len(mock_process.mock_calls) == 2 + assert len(mock_process.mock_calls) == 1 assert mock_process.mock_calls[0][1][1] == mqtt.requirements @@ -608,13 +608,12 @@ async def test_discovery_requirements_ssdp(hass: HomeAssistant) -> None: ) as mock_process: await async_get_integration_with_requirements(hass, "ssdp_comp") - assert len(mock_process.mock_calls) == 4 + assert len(mock_process.mock_calls) == 3 assert mock_process.mock_calls[0][1][1] == ssdp.requirements assert { mock_process.mock_calls[1][1][0], mock_process.mock_calls[2][1][0], - mock_process.mock_calls[3][1][0], - } == {"network", "recorder", "isal"} + } == {"network", "recorder"} @pytest.mark.parametrize( @@ -638,7 +637,7 @@ async def test_discovery_requirements_zeroconf( ) as mock_process: await async_get_integration_with_requirements(hass, "comp") - assert len(mock_process.mock_calls) == 4 + assert len(mock_process.mock_calls) == 3 assert mock_process.mock_calls[0][1][1] == zeroconf.requirements From 9a6902d827a0d712d3a5af6c6275cf1289df839d Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Fri, 7 Jun 2024 10:50:05 +0300 Subject: [PATCH 0330/1445] Hold connection lock in Shelly RPC reconnect (#119009) --- homeassistant/components/shelly/coordinator.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/shelly/coordinator.py b/homeassistant/components/shelly/coordinator.py index 2fe3f6a9943..b6ccc1540f1 100644 --- a/homeassistant/components/shelly/coordinator.py +++ b/homeassistant/components/shelly/coordinator.py @@ -586,11 +586,13 @@ class ShellyRpcCoordinator(ShellyCoordinatorBase[RpcDevice]): raise UpdateFailed( f"Sleeping device did not update within {self.sleep_period} seconds interval" ) - if self.device.connected: - return - if not await self._async_device_connect_task(): - raise UpdateFailed("Device reconnect error") + async with self._connection_lock: + if self.device.connected: # Already connected + return + + if not await self._async_device_connect_task(): + raise UpdateFailed("Device reconnect error") async def _async_disconnected(self, reconnect: bool) -> None: """Handle device disconnected.""" From 78c7af40ed050c422988d87434572cbb98d003c3 Mon Sep 17 00:00:00 2001 From: Lorenzo Monaco <1611929+lnx85@users.noreply.github.com> Date: Fri, 7 Jun 2024 10:11:49 +0200 Subject: [PATCH 0331/1445] Ecovacs get_positions service (#118572) Co-authored-by: Robert Resch --- homeassistant/components/ecovacs/icons.json | 3 + .../components/ecovacs/services.yaml | 4 + homeassistant/components/ecovacs/strings.json | 9 ++ homeassistant/components/ecovacs/vacuum.py | 39 +++++++- tests/components/ecovacs/test_services.py | 89 +++++++++++++++++++ 5 files changed, 143 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/ecovacs/services.yaml create mode 100644 tests/components/ecovacs/test_services.py diff --git a/homeassistant/components/ecovacs/icons.json b/homeassistant/components/ecovacs/icons.json index b627ada718c..d129273e891 100644 --- a/homeassistant/components/ecovacs/icons.json +++ b/homeassistant/components/ecovacs/icons.json @@ -140,5 +140,8 @@ "default": "mdi:laser-pointer" } } + }, + "services": { + "raw_get_positions": "mdi:map-marker-radius-outline" } } diff --git a/homeassistant/components/ecovacs/services.yaml b/homeassistant/components/ecovacs/services.yaml new file mode 100644 index 00000000000..0d884a24feb --- /dev/null +++ b/homeassistant/components/ecovacs/services.yaml @@ -0,0 +1,4 @@ +raw_get_positions: + target: + entity: + domain: vacuum diff --git a/homeassistant/components/ecovacs/strings.json b/homeassistant/components/ecovacs/strings.json index d1ea3eb4faf..25fd9b1b978 100644 --- a/homeassistant/components/ecovacs/strings.json +++ b/homeassistant/components/ecovacs/strings.json @@ -226,6 +226,9 @@ }, "vacuum_send_command_params_required": { "message": "Params are required for the command: {command}" + }, + "vacuum_raw_get_positions_not_supported": { + "message": "Getting the positions of the charges and the device itself is not supported" } }, "issues": { @@ -261,5 +264,11 @@ "self_hosted": "Self-hosted" } } + }, + "services": { + "raw_get_positions": { + "name": "Get raw positions", + "description": "Get the raw response for the positions of the chargers and the device itself." + } } } diff --git a/homeassistant/components/ecovacs/vacuum.py b/homeassistant/components/ecovacs/vacuum.py index 5c898694cbb..e637eb14fd6 100644 --- a/homeassistant/components/ecovacs/vacuum.py +++ b/homeassistant/components/ecovacs/vacuum.py @@ -23,8 +23,9 @@ from homeassistant.components.vacuum import ( StateVacuumEntityDescription, VacuumEntityFeature, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, SupportsResponse from homeassistant.exceptions import ServiceValidationError +from homeassistant.helpers import entity_platform from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.icon import icon_for_battery_level from homeassistant.util import slugify @@ -39,6 +40,8 @@ _LOGGER = logging.getLogger(__name__) ATTR_ERROR = "error" ATTR_COMPONENT_PREFIX = "component_" +SERVICE_RAW_GET_POSITIONS = "raw_get_positions" + async def async_setup_entry( hass: HomeAssistant, @@ -46,6 +49,7 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Ecovacs vacuums.""" + controller = config_entry.runtime_data vacuums: list[EcovacsVacuum | EcovacsLegacyVacuum] = [ EcovacsVacuum(device) for device in controller.devices(VacuumCapabilities) @@ -56,6 +60,14 @@ async def async_setup_entry( _LOGGER.debug("Adding Ecovacs Vacuums to Home Assistant: %s", vacuums) async_add_entities(vacuums) + platform = entity_platform.async_get_current_platform() + platform.async_register_entity_service( + SERVICE_RAW_GET_POSITIONS, + {}, + "async_raw_get_positions", + supports_response=SupportsResponse.ONLY, + ) + class EcovacsLegacyVacuum(StateVacuumEntity): """Legacy Ecovacs vacuums.""" @@ -197,6 +209,15 @@ class EcovacsLegacyVacuum(StateVacuumEntity): """Send a command to a vacuum cleaner.""" self.device.run(sucks.VacBotCommand(command, params)) + async def async_raw_get_positions( + self, + ) -> None: + """Get bot and chargers positions.""" + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="vacuum_raw_get_positions_not_supported", + ) + _STATE_TO_VACUUM_STATE = { State.IDLE: STATE_IDLE, @@ -377,3 +398,19 @@ class EcovacsVacuum( await self._device.execute_command( self._capability.custom.set(command, params) ) + + async def async_raw_get_positions( + self, + ) -> dict[str, Any]: + """Get bot and chargers positions.""" + _LOGGER.debug("async_raw_get_positions") + + if not (map_cap := self._capability.map) or not ( + position_commands := map_cap.position.get + ): + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="vacuum_raw_get_positions_not_supported", + ) + + return await self._device.execute_command(position_commands[0]) diff --git a/tests/components/ecovacs/test_services.py b/tests/components/ecovacs/test_services.py new file mode 100644 index 00000000000..973c63782ec --- /dev/null +++ b/tests/components/ecovacs/test_services.py @@ -0,0 +1,89 @@ +"""Tests for Ecovacs services.""" + +from collections.abc import Generator +from typing import Any +from unittest.mock import patch + +from deebot_client.device import Device +import pytest + +from homeassistant.components.ecovacs.const import DOMAIN +from homeassistant.components.ecovacs.vacuum import SERVICE_RAW_GET_POSITIONS +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.core import HomeAssistant + +pytestmark = [pytest.mark.usefixtures("init_integration")] + + +@pytest.fixture +def mock_device_execute_response( + data: dict[str, Any], +) -> Generator[dict[str, Any], None, None]: + """Mock the device execute function response.""" + + response = { + "ret": "ok", + "resp": { + "header": { + "pri": 1, + "tzm": 480, + "ts": "1717113600000", + "ver": "0.0.1", + "fwVer": "1.2.0", + "hwVer": "0.1.0", + }, + "body": { + "code": 0, + "msg": "ok", + "data": data, + }, + }, + "id": "xRV3", + "payloadType": "j", + } + + with patch.object( + Device, + "execute_command", + return_value=response, + ): + yield response + + +@pytest.mark.usefixtures("mock_device_execute_response") +@pytest.mark.parametrize( + "data", + [ + { + "deebotPos": {"x": 1, "y": 5, "a": 85}, + "chargePos": {"x": 5, "y": 9, "a": 85}, + }, + { + "deebotPos": {"x": 375, "y": 313, "a": 90}, + "chargePos": [{"x": 112, "y": 768, "a": 32}, {"x": 489, "y": 322, "a": 0}], + }, + ], +) +@pytest.mark.parametrize( + ("device_fixture", "entity_id"), + [ + ("yna5x1", "vacuum.ozmo_950"), + ], + ids=["yna5x1"], +) +async def test_get_positions_service( + hass: HomeAssistant, + mock_device_execute_response: dict[str], + entity_id: str, +) -> None: + """Test that get_positions service response snapshots match.""" + vacuum = hass.states.get(entity_id) + assert vacuum + + assert await hass.services.async_call( + DOMAIN, + SERVICE_RAW_GET_POSITIONS, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + return_response=True, + ) == {entity_id: mock_device_execute_response} From 539b9d76fcc2f249d547c41d5f07dcf18ae0c0af Mon Sep 17 00:00:00 2001 From: Christopher Fenner <9592452+CFenner@users.noreply.github.com> Date: Fri, 7 Jun 2024 10:21:24 +0200 Subject: [PATCH 0332/1445] Add photovoltaic sensors to ViCare integration (#113664) * Add photovoltaic sensors * Update strings.json * Apply suggestions from code review * change uom for daily sensor --- homeassistant/components/vicare/sensor.py | 40 ++++++++++++++++++++ homeassistant/components/vicare/strings.json | 12 ++++++ 2 files changed, 52 insertions(+) diff --git a/homeassistant/components/vicare/sensor.py b/homeassistant/components/vicare/sensor.py index 41266f8bde7..0e98729e40f 100644 --- a/homeassistant/components/vicare/sensor.py +++ b/homeassistant/components/vicare/sensor.py @@ -694,10 +694,50 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( key="photovoltaic_energy_production_today", translation_key="photovoltaic_energy_production_today", native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, + suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, state_class=SensorStateClass.TOTAL_INCREASING, value_getter=lambda api: api.getPhotovoltaicProductionCumulatedCurrentDay(), unit_getter=lambda api: api.getPhotovoltaicProductionCumulatedUnit(), ), + ViCareSensorEntityDescription( + key="photovoltaic_energy_production_this_week", + translation_key="photovoltaic_energy_production_this_week", + native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, + suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + state_class=SensorStateClass.TOTAL_INCREASING, + value_getter=lambda api: api.getPhotovoltaicProductionCumulatedCurrentWeek(), + unit_getter=lambda api: api.getPhotovoltaicProductionCumulatedUnit(), + entity_registry_enabled_default=False, + ), + ViCareSensorEntityDescription( + key="photovoltaic_energy_production_this_month", + translation_key="photovoltaic_energy_production_this_month", + native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, + suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + state_class=SensorStateClass.TOTAL_INCREASING, + value_getter=lambda api: api.getPhotovoltaicProductionCumulatedCurrentMonth(), + unit_getter=lambda api: api.getPhotovoltaicProductionCumulatedUnit(), + entity_registry_enabled_default=False, + ), + ViCareSensorEntityDescription( + key="photovoltaic_energy_production_this_year", + translation_key="photovoltaic_energy_production_this_year", + native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, + suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + state_class=SensorStateClass.TOTAL_INCREASING, + value_getter=lambda api: api.getPhotovoltaicProductionCumulatedCurrentYear(), + unit_getter=lambda api: api.getPhotovoltaicProductionCumulatedUnit(), + entity_registry_enabled_default=False, + ), + ViCareSensorEntityDescription( + key="photovoltaic_energy_production_total", + translation_key="photovoltaic_energy_production_total", + native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, + suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + state_class=SensorStateClass.TOTAL_INCREASING, + value_getter=lambda api: api.getPhotovoltaicProductionCumulatedLifeCycle(), + unit_getter=lambda api: api.getPhotovoltaicProductionCumulatedUnit(), + ), ViCareSensorEntityDescription( key="photovoltaic_status", translation_key="photovoltaic_status", diff --git a/homeassistant/components/vicare/strings.json b/homeassistant/components/vicare/strings.json index f81d01b71cf..de92d0ec271 100644 --- a/homeassistant/components/vicare/strings.json +++ b/homeassistant/components/vicare/strings.json @@ -319,6 +319,18 @@ "photovoltaic_energy_production_today": { "name": "Solar energy production today" }, + "photovoltaic_energy_production_this_week": { + "name": "Solar energy production this week" + }, + "photovoltaic_energy_production_this_month": { + "name": "Solar energy production this month" + }, + "photovoltaic_energy_production_this_year": { + "name": "Solar energy production this year" + }, + "photovoltaic_energy_production_total": { + "name": "Solar energy production total" + }, "photovoltaic_status": { "name": "Solar state", "state": { From d5a68ad31192a79bd51c244bf9358ba42fc24851 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 7 Jun 2024 10:25:11 +0200 Subject: [PATCH 0333/1445] Improve type hints in zamg tests (#119042) --- tests/components/zamg/conftest.py | 23 ++++-------------- tests/components/zamg/test_config_flow.py | 29 +++++++---------------- tests/components/zamg/test_init.py | 8 +++---- 3 files changed, 16 insertions(+), 44 deletions(-) diff --git a/tests/components/zamg/conftest.py b/tests/components/zamg/conftest.py index fa2eccf42d1..1795baa7fad 100644 --- a/tests/components/zamg/conftest.py +++ b/tests/components/zamg/conftest.py @@ -37,7 +37,7 @@ def mock_setup_entry() -> Generator[None]: @pytest.fixture -def mock_zamg_config_flow(request: pytest.FixtureRequest) -> Generator[MagicMock]: +def mock_zamg_config_flow() -> Generator[MagicMock]: """Return a mocked Zamg client.""" with patch( "homeassistant.components.zamg.sensor.ZamgData", autospec=True @@ -51,7 +51,7 @@ def mock_zamg_config_flow(request: pytest.FixtureRequest) -> Generator[MagicMock @pytest.fixture -def mock_zamg(request: pytest.FixtureRequest) -> Generator[MagicMock]: +def mock_zamg() -> Generator[MagicMock]: """Return a mocked Zamg client.""" with patch( @@ -70,7 +70,7 @@ def mock_zamg(request: pytest.FixtureRequest) -> Generator[MagicMock]: @pytest.fixture -def mock_zamg_coordinator(request: pytest.FixtureRequest) -> Generator[MagicMock]: +def mock_zamg_coordinator() -> Generator[MagicMock]: """Return a mocked Zamg client.""" with patch( @@ -89,22 +89,7 @@ def mock_zamg_coordinator(request: pytest.FixtureRequest) -> Generator[MagicMock @pytest.fixture -def mock_zamg_stations(request: pytest.FixtureRequest) -> Generator[MagicMock]: - """Return a mocked Zamg client.""" - with patch( - "homeassistant.components.zamg.config_flow.ZamgData.zamg_stations" - ) as zamg_mock: - zamg_mock.return_value = { - "11240": (46.99305556, 15.43916667, "GRAZ-FLUGHAFEN"), - "11244": (46.87222222, 15.90361111, "BAD GLEICHENBERG"), - } - yield zamg_mock - - -@pytest.fixture -async def init_integration( - hass: HomeAssistant, -) -> MockConfigEntry: +async def init_integration(hass: HomeAssistant) -> MockConfigEntry: """Set up the Zamg integration for testing.""" mock_config_entry.add_to_hass(hass) diff --git a/tests/components/zamg/test_config_flow.py b/tests/components/zamg/test_config_flow.py index f67eda67a49..949f14df89c 100644 --- a/tests/components/zamg/test_config_flow.py +++ b/tests/components/zamg/test_config_flow.py @@ -2,6 +2,7 @@ from unittest.mock import MagicMock +import pytest from zamg.exceptions import ZamgApiError from homeassistant.components.zamg.const import CONF_STATION_ID, DOMAIN, LOGGER @@ -12,11 +13,8 @@ from homeassistant.data_entry_flow import FlowResultType from .conftest import TEST_STATION_ID -async def test_full_user_flow_implementation( - hass: HomeAssistant, - mock_zamg: MagicMock, - mock_setup_entry: None, -) -> None: +@pytest.mark.usefixtures("mock_zamg", "mock_setup_entry") +async def test_full_user_flow_implementation(hass: HomeAssistant) -> None: """Test the full manual user flow from start to finish.""" result = await hass.config_entries.flow.async_init( DOMAIN, @@ -37,11 +35,8 @@ async def test_full_user_flow_implementation( assert result["result"].unique_id == TEST_STATION_ID -async def test_error_closest_station( - hass: HomeAssistant, - mock_zamg: MagicMock, - mock_setup_entry: None, -) -> None: +@pytest.mark.usefixtures("mock_setup_entry") +async def test_error_closest_station(hass: HomeAssistant, mock_zamg: MagicMock) -> None: """Test with error of reading from Zamg.""" mock_zamg.closest_station.side_effect = ZamgApiError result = await hass.config_entries.flow.async_init( @@ -52,11 +47,8 @@ async def test_error_closest_station( assert result.get("reason") == "cannot_connect" -async def test_error_update( - hass: HomeAssistant, - mock_zamg: MagicMock, - mock_setup_entry: None, -) -> None: +@pytest.mark.usefixtures("mock_setup_entry") +async def test_error_update(hass: HomeAssistant, mock_zamg: MagicMock) -> None: """Test with error of reading from Zamg.""" result = await hass.config_entries.flow.async_init( DOMAIN, @@ -75,11 +67,8 @@ async def test_error_update( assert result.get("reason") == "cannot_connect" -async def test_user_flow_duplicate( - hass: HomeAssistant, - mock_zamg: MagicMock, - mock_setup_entry: None, -) -> None: +@pytest.mark.usefixtures("mock_zamg", "mock_setup_entry") +async def test_user_flow_duplicate(hass: HomeAssistant) -> None: """Test the full manual user flow from start to finish.""" result = await hass.config_entries.flow.async_init( DOMAIN, diff --git a/tests/components/zamg/test_init.py b/tests/components/zamg/test_init.py index eec7dcef101..9f05882853a 100644 --- a/tests/components/zamg/test_init.py +++ b/tests/components/zamg/test_init.py @@ -1,7 +1,5 @@ """Test Zamg component init.""" -from unittest.mock import MagicMock - import pytest from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN @@ -62,10 +60,10 @@ from tests.common import MockConfigEntry ), ], ) +@pytest.mark.usefixtures("mock_zamg_coordinator") async def test_migrate_unique_ids( hass: HomeAssistant, entity_registry: er.EntityRegistry, - mock_zamg_coordinator: MagicMock, entitydata: dict, old_unique_id: str, new_unique_id: str, @@ -108,10 +106,10 @@ async def test_migrate_unique_ids( ), ], ) +@pytest.mark.usefixtures("mock_zamg_coordinator") async def test_dont_migrate_unique_ids( hass: HomeAssistant, entity_registry: er.EntityRegistry, - mock_zamg_coordinator: MagicMock, entitydata: dict, old_unique_id: str, new_unique_id: str, @@ -167,10 +165,10 @@ async def test_dont_migrate_unique_ids( ), ], ) +@pytest.mark.usefixtures("mock_zamg_coordinator") async def test_unload_entry( hass: HomeAssistant, entity_registry: er.EntityRegistry, - mock_zamg_coordinator: MagicMock, entitydata: dict, unique_id: str, ) -> None: From c107d980faf01d89928e36529724fb4707247c4f Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 7 Jun 2024 10:43:56 +0200 Subject: [PATCH 0334/1445] Improve type hints in motionblinds_ble tests (#119049) --- tests/components/motionblinds_ble/conftest.py | 5 +++- .../motionblinds_ble/test_config_flow.py | 25 +++++++++---------- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/tests/components/motionblinds_ble/conftest.py b/tests/components/motionblinds_ble/conftest.py index ae487957302..342e958eae4 100644 --- a/tests/components/motionblinds_ble/conftest.py +++ b/tests/components/motionblinds_ble/conftest.py @@ -3,6 +3,7 @@ from unittest.mock import AsyncMock, Mock, patch import pytest +from typing_extensions import Generator TEST_MAC = "abcd" TEST_NAME = f"MOTION_{TEST_MAC.upper()}" @@ -10,7 +11,9 @@ TEST_ADDRESS = "test_adress" @pytest.fixture(name="motionblinds_ble_connect", autouse=True) -def motion_blinds_connect_fixture(enable_bluetooth): +def motion_blinds_connect_fixture( + enable_bluetooth: None, +) -> Generator[tuple[AsyncMock, Mock]]: """Mock motion blinds ble connection and entry setup.""" device = Mock() device.name = TEST_NAME diff --git a/tests/components/motionblinds_ble/test_config_flow.py b/tests/components/motionblinds_ble/test_config_flow.py index f5a988a628d..90d2cbdcbc6 100644 --- a/tests/components/motionblinds_ble/test_config_flow.py +++ b/tests/components/motionblinds_ble/test_config_flow.py @@ -1,8 +1,9 @@ """Test the Motionblinds Bluetooth config flow.""" -from unittest.mock import patch +from unittest.mock import AsyncMock, Mock, patch from motionblindsble.const import MotionBlindType +import pytest from homeassistant import config_entries from homeassistant.components.bluetooth.models import BluetoothServiceInfoBleak @@ -43,9 +44,8 @@ BLIND_SERVICE_INFO = BluetoothServiceInfoBleak( ) -async def test_config_flow_manual_success( - hass: HomeAssistant, motionblinds_ble_connect -) -> None: +@pytest.mark.usefixtures("motionblinds_ble_connect") +async def test_config_flow_manual_success(hass: HomeAssistant) -> None: """Successful flow manually initialized by the user.""" result = await hass.config_entries.flow.async_init( const.DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -76,9 +76,8 @@ async def test_config_flow_manual_success( assert result["options"] == {} -async def test_config_flow_manual_error_invalid_mac( - hass: HomeAssistant, motionblinds_ble_connect -) -> None: +@pytest.mark.usefixtures("motionblinds_ble_connect") +async def test_config_flow_manual_error_invalid_mac(hass: HomeAssistant) -> None: """Invalid MAC code error flow manually initialized by the user.""" # Initialize @@ -122,8 +121,9 @@ async def test_config_flow_manual_error_invalid_mac( assert result["options"] == {} +@pytest.mark.usefixtures("motionblinds_ble_connect") async def test_config_flow_manual_error_no_bluetooth_adapter( - hass: HomeAssistant, motionblinds_ble_connect + hass: HomeAssistant, ) -> None: """No Bluetooth adapter error flow manually initialized by the user.""" @@ -159,7 +159,7 @@ async def test_config_flow_manual_error_no_bluetooth_adapter( async def test_config_flow_manual_error_could_not_find_motor( - hass: HomeAssistant, motionblinds_ble_connect + hass: HomeAssistant, motionblinds_ble_connect: tuple[AsyncMock, Mock] ) -> None: """Could not find motor error flow manually initialized by the user.""" @@ -207,7 +207,7 @@ async def test_config_flow_manual_error_could_not_find_motor( async def test_config_flow_manual_error_no_devices_found( - hass: HomeAssistant, motionblinds_ble_connect + hass: HomeAssistant, motionblinds_ble_connect: tuple[AsyncMock, Mock] ) -> None: """No devices found error flow manually initialized by the user.""" @@ -229,9 +229,8 @@ async def test_config_flow_manual_error_no_devices_found( assert result["reason"] == const.ERROR_NO_DEVICES_FOUND -async def test_config_flow_bluetooth_success( - hass: HomeAssistant, motionblinds_ble_connect -) -> None: +@pytest.mark.usefixtures("motionblinds_ble_connect") +async def test_config_flow_bluetooth_success(hass: HomeAssistant) -> None: """Successful bluetooth discovery flow.""" result = await hass.config_entries.flow.async_init( const.DOMAIN, From 7638380add9595ac99030e7c906556f0dc20486f Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 7 Jun 2024 10:49:09 +0200 Subject: [PATCH 0335/1445] Improve type hints in kaleidescape tests (#119040) --- tests/components/kaleidescape/conftest.py | 5 +-- .../kaleidescape/test_config_flow.py | 29 ++++++++--------- tests/components/kaleidescape/test_init.py | 16 ++++------ .../kaleidescape/test_media_player.py | 32 ++++++------------- tests/components/kaleidescape/test_remote.py | 23 ++++--------- tests/components/kaleidescape/test_sensor.py | 9 ++---- 6 files changed, 41 insertions(+), 73 deletions(-) diff --git a/tests/components/kaleidescape/conftest.py b/tests/components/kaleidescape/conftest.py index f956e620da6..5cd2a8ebb18 100644 --- a/tests/components/kaleidescape/conftest.py +++ b/tests/components/kaleidescape/conftest.py @@ -1,6 +1,6 @@ """Fixtures for Kaleidescape integration.""" -from unittest.mock import AsyncMock, patch +from unittest.mock import MagicMock, patch from kaleidescape import Dispatcher from kaleidescape.device import Automation, Movie, Power, System @@ -17,7 +17,7 @@ from tests.common import MockConfigEntry @pytest.fixture(name="mock_device") -def fixture_mock_device() -> Generator[AsyncMock]: +def fixture_mock_device() -> Generator[MagicMock]: """Return a mocked Kaleidescape device.""" with patch( "homeassistant.components.kaleidescape.KaleidescapeDevice", autospec=True @@ -64,6 +64,7 @@ def fixture_mock_config_entry() -> MockConfigEntry: @pytest.fixture(name="mock_integration") async def fixture_mock_integration( hass: HomeAssistant, + mock_device: MagicMock, mock_config_entry: MockConfigEntry, ) -> MockConfigEntry: """Return a mock ConfigEntry setup for Kaleidescape integration.""" diff --git a/tests/components/kaleidescape/test_config_flow.py b/tests/components/kaleidescape/test_config_flow.py index 5d9f8dba146..ecb5b164093 100644 --- a/tests/components/kaleidescape/test_config_flow.py +++ b/tests/components/kaleidescape/test_config_flow.py @@ -1,7 +1,9 @@ """Tests for Kaleidescape config flow.""" import dataclasses -from unittest.mock import AsyncMock +from unittest.mock import MagicMock + +import pytest from homeassistant.components.kaleidescape.const import DOMAIN from homeassistant.config_entries import SOURCE_SSDP, SOURCE_USER @@ -11,12 +13,9 @@ from homeassistant.data_entry_flow import FlowResultType from . import MOCK_HOST, MOCK_SSDP_DISCOVERY_INFO -from tests.common import MockConfigEntry - -async def test_user_config_flow_success( - hass: HomeAssistant, mock_device: AsyncMock -) -> None: +@pytest.mark.usefixtures("mock_device") +async def test_user_config_flow_success(hass: HomeAssistant) -> None: """Test user config flow success.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} @@ -35,7 +34,7 @@ async def test_user_config_flow_success( async def test_user_config_flow_bad_connect_errors( - hass: HomeAssistant, mock_device: AsyncMock + hass: HomeAssistant, mock_device: MagicMock ) -> None: """Test errors when connection error occurs.""" mock_device.connect.side_effect = ConnectionError @@ -50,7 +49,7 @@ async def test_user_config_flow_bad_connect_errors( async def test_user_config_flow_unsupported_device_errors( - hass: HomeAssistant, mock_device: AsyncMock + hass: HomeAssistant, mock_device: MagicMock ) -> None: """Test errors when connecting to unsupported device.""" mock_device.is_server_only = True @@ -64,9 +63,8 @@ async def test_user_config_flow_unsupported_device_errors( assert result["errors"] == {"base": "unsupported"} -async def test_user_config_flow_device_exists_abort( - hass: HomeAssistant, mock_device: AsyncMock, mock_integration: MockConfigEntry -) -> None: +@pytest.mark.usefixtures("mock_device", "mock_integration") +async def test_user_config_flow_device_exists_abort(hass: HomeAssistant) -> None: """Test flow aborts when device already configured.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data={CONF_HOST: MOCK_HOST} @@ -75,9 +73,8 @@ async def test_user_config_flow_device_exists_abort( assert result["reason"] == "already_configured" -async def test_ssdp_config_flow_success( - hass: HomeAssistant, mock_device: AsyncMock -) -> None: +@pytest.mark.usefixtures("mock_device") +async def test_ssdp_config_flow_success(hass: HomeAssistant) -> None: """Test ssdp config flow success.""" discovery_info = dataclasses.replace(MOCK_SSDP_DISCOVERY_INFO) result = await hass.config_entries.flow.async_init( @@ -97,7 +94,7 @@ async def test_ssdp_config_flow_success( async def test_ssdp_config_flow_bad_connect_aborts( - hass: HomeAssistant, mock_device: AsyncMock + hass: HomeAssistant, mock_device: MagicMock ) -> None: """Test abort when connection error occurs.""" mock_device.connect.side_effect = ConnectionError @@ -112,7 +109,7 @@ async def test_ssdp_config_flow_bad_connect_aborts( async def test_ssdp_config_flow_unsupported_device_aborts( - hass: HomeAssistant, mock_device: AsyncMock + hass: HomeAssistant, mock_device: MagicMock ) -> None: """Test abort when connecting to unsupported device.""" mock_device.is_server_only = True diff --git a/tests/components/kaleidescape/test_init.py b/tests/components/kaleidescape/test_init.py index 28d90290996..01769b9fc57 100644 --- a/tests/components/kaleidescape/test_init.py +++ b/tests/components/kaleidescape/test_init.py @@ -1,6 +1,8 @@ """Tests for Kaleidescape config entry.""" -from unittest.mock import AsyncMock +from unittest.mock import MagicMock + +import pytest from homeassistant.components.kaleidescape.const import DOMAIN from homeassistant.config_entries import ConfigEntryState @@ -14,7 +16,7 @@ from tests.common import MockConfigEntry async def test_unload_config_entry( hass: HomeAssistant, - mock_device: AsyncMock, + mock_device: MagicMock, mock_integration: MockConfigEntry, ) -> None: """Test config entry loading and unloading.""" @@ -32,7 +34,7 @@ async def test_unload_config_entry( async def test_config_entry_not_ready( hass: HomeAssistant, - mock_device: AsyncMock, + mock_device: MagicMock, mock_config_entry: MockConfigEntry, ) -> None: """Test config entry not ready.""" @@ -45,12 +47,8 @@ async def test_config_entry_not_ready( assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY -async def test_device( - hass: HomeAssistant, - device_registry: dr.DeviceRegistry, - mock_device: AsyncMock, - mock_integration: MockConfigEntry, -) -> None: +@pytest.mark.usefixtures("mock_device", "mock_integration") +async def test_device(device_registry: dr.DeviceRegistry) -> None: """Test device.""" device = device_registry.async_get_device( identifiers={("kaleidescape", MOCK_SERIAL)} diff --git a/tests/components/kaleidescape/test_media_player.py b/tests/components/kaleidescape/test_media_player.py index ad7dcbcaa51..2180a6b7d0d 100644 --- a/tests/components/kaleidescape/test_media_player.py +++ b/tests/components/kaleidescape/test_media_player.py @@ -4,6 +4,7 @@ from unittest.mock import MagicMock from kaleidescape import const as kaleidescape_const from kaleidescape.device import Movie +import pytest from homeassistant.components.media_player import DOMAIN as MEDIA_PLAYER_DOMAIN from homeassistant.const import ( @@ -25,17 +26,12 @@ from homeassistant.helpers import device_registry as dr from . import MOCK_SERIAL -from tests.common import MockConfigEntry - ENTITY_ID = f"media_player.kaleidescape_device_{MOCK_SERIAL}" FRIENDLY_NAME = f"Kaleidescape Device {MOCK_SERIAL}" -async def test_entity( - hass: HomeAssistant, - mock_device: MagicMock, - mock_integration: MockConfigEntry, -) -> None: +@pytest.mark.usefixtures("mock_device", "mock_integration") +async def test_entity(hass: HomeAssistant) -> None: """Test entity attributes.""" entity = hass.states.get(ENTITY_ID) assert entity is not None @@ -43,11 +39,8 @@ async def test_entity( assert entity.attributes["friendly_name"] == FRIENDLY_NAME -async def test_update_state( - hass: HomeAssistant, - mock_device: MagicMock, - mock_integration: MockConfigEntry, -) -> None: +@pytest.mark.usefixtures("mock_integration") +async def test_update_state(hass: HomeAssistant, mock_device: MagicMock) -> None: """Tests dispatched signals update player.""" entity = hass.states.get(ENTITY_ID) assert entity is not None @@ -105,11 +98,8 @@ async def test_update_state( assert entity.state == STATE_PAUSED -async def test_services( - hass: HomeAssistant, - mock_device: MagicMock, - mock_integration: MockConfigEntry, -) -> None: +@pytest.mark.usefixtures("mock_integration") +async def test_services(hass: HomeAssistant, mock_device: MagicMock) -> None: """Test service calls.""" await hass.services.async_call( MEDIA_PLAYER_DOMAIN, @@ -168,12 +158,8 @@ async def test_services( assert mock_device.previous.call_count == 1 -async def test_device( - hass: HomeAssistant, - device_registry: dr.DeviceRegistry, - mock_device: MagicMock, - mock_integration: MockConfigEntry, -) -> None: +@pytest.mark.usefixtures("mock_device", "mock_integration") +async def test_device(device_registry: dr.DeviceRegistry) -> None: """Test device attributes.""" device = device_registry.async_get_device( identifiers={("kaleidescape", MOCK_SERIAL)} diff --git a/tests/components/kaleidescape/test_remote.py b/tests/components/kaleidescape/test_remote.py index 3573d04395d..a1db5a60999 100644 --- a/tests/components/kaleidescape/test_remote.py +++ b/tests/components/kaleidescape/test_remote.py @@ -15,25 +15,17 @@ from homeassistant.exceptions import HomeAssistantError from . import MOCK_SERIAL -from tests.common import MockConfigEntry - ENTITY_ID = f"remote.kaleidescape_device_{MOCK_SERIAL}" -async def test_entity( - hass: HomeAssistant, - mock_device: MagicMock, - mock_integration: MockConfigEntry, -) -> None: +@pytest.mark.usefixtures("mock_device", "mock_integration") +async def test_entity(hass: HomeAssistant) -> None: """Test entity attributes.""" assert hass.states.get(ENTITY_ID) -async def test_commands( - hass: HomeAssistant, - mock_device: MagicMock, - mock_integration: MockConfigEntry, -) -> None: +@pytest.mark.usefixtures("mock_integration") +async def test_commands(hass: HomeAssistant, mock_device: MagicMock) -> None: """Test service calls.""" await hass.services.async_call( REMOTE_DOMAIN, @@ -140,11 +132,8 @@ async def test_commands( assert mock_device.menu_toggle.call_count == 1 -async def test_unknown_command( - hass: HomeAssistant, - mock_device: MagicMock, - mock_integration: MockConfigEntry, -) -> None: +@pytest.mark.usefixtures("mock_device", "mock_integration") +async def test_unknown_command(hass: HomeAssistant) -> None: """Test service calls.""" with pytest.raises(HomeAssistantError) as err: await hass.services.async_call( diff --git a/tests/components/kaleidescape/test_sensor.py b/tests/components/kaleidescape/test_sensor.py index 70406872464..e68b065f4b8 100644 --- a/tests/components/kaleidescape/test_sensor.py +++ b/tests/components/kaleidescape/test_sensor.py @@ -3,6 +3,7 @@ from unittest.mock import MagicMock from kaleidescape import const as kaleidescape_const +import pytest from homeassistant.const import ATTR_FRIENDLY_NAME from homeassistant.core import HomeAssistant @@ -10,17 +11,13 @@ from homeassistant.helpers import entity_registry as er from . import MOCK_SERIAL -from tests.common import MockConfigEntry - ENTITY_ID = f"sensor.kaleidescape_device_{MOCK_SERIAL}" FRIENDLY_NAME = f"Kaleidescape Device {MOCK_SERIAL}" +@pytest.mark.usefixtures("mock_integration") async def test_sensors( - hass: HomeAssistant, - entity_registry: er.EntityRegistry, - mock_device: MagicMock, - mock_integration: MockConfigEntry, + hass: HomeAssistant, entity_registry: er.EntityRegistry, mock_device: MagicMock ) -> None: """Test sensors.""" entity = hass.states.get(f"{ENTITY_ID}_media_location") From 42b1cfe6b9625d6b76212aa787d351f29d0aa2e8 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 7 Jun 2024 10:50:03 +0200 Subject: [PATCH 0336/1445] Improve type hints in azure_event_hub tests (#119047) Co-authored-by: Marc Mueller <30130371+cdce8p@users.noreply.github.com> --- tests/components/azure_event_hub/conftest.py | 32 +++++++++++++------ .../azure_event_hub/test_config_flow.py | 29 +++++++++-------- tests/components/azure_event_hub/test_init.py | 27 ++++++++++++---- 3 files changed, 57 insertions(+), 31 deletions(-) diff --git a/tests/components/azure_event_hub/conftest.py b/tests/components/azure_event_hub/conftest.py index a29fc13b495..a34f2e646f2 100644 --- a/tests/components/azure_event_hub/conftest.py +++ b/tests/components/azure_event_hub/conftest.py @@ -3,10 +3,12 @@ from dataclasses import dataclass from datetime import timedelta import logging -from unittest.mock import MagicMock, patch +from typing import Any +from unittest.mock import AsyncMock, MagicMock, patch from azure.eventhub.aio import EventHubProducerClient import pytest +from typing_extensions import AsyncGenerator, Generator from homeassistant.components.azure_event_hub.const import ( CONF_FILTER, @@ -15,6 +17,7 @@ from homeassistant.components.azure_event_hub.const import ( ) from homeassistant.config_entries import ConfigEntryState from homeassistant.const import STATE_ON +from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow @@ -27,20 +30,25 @@ _LOGGER = logging.getLogger(__name__) # fixtures for both init and config flow tests @pytest.fixture(autouse=True, name="mock_get_eventhub_properties") -def mock_get_eventhub_properties_fixture(): +def mock_get_eventhub_properties_fixture() -> Generator[AsyncMock]: """Mock azure event hub properties, used to test the connection.""" with patch(f"{PRODUCER_PATH}.get_eventhub_properties") as get_eventhub_properties: yield get_eventhub_properties @pytest.fixture(name="filter_schema") -def mock_filter_schema(): +def mock_filter_schema() -> dict[str, Any]: """Return an empty filter.""" return {} @pytest.fixture(name="entry") -async def mock_entry_fixture(hass, filter_schema, mock_create_batch, mock_send_batch): +async def mock_entry_fixture( + hass: HomeAssistant, + filter_schema: dict[str, Any], + mock_create_batch: MagicMock, + mock_send_batch: AsyncMock, +) -> AsyncGenerator[MockConfigEntry]: """Create the setup in HA.""" entry = MockConfigEntry( domain=DOMAIN, @@ -68,7 +76,9 @@ async def mock_entry_fixture(hass, filter_schema, mock_create_batch, mock_send_b # fixtures for init tests @pytest.fixture(name="entry_with_one_event") -async def mock_entry_with_one_event(hass, entry): +def mock_entry_with_one_event( + hass: HomeAssistant, entry: MockConfigEntry +) -> MockConfigEntry: """Use the entry and add a single test event to the queue.""" assert entry.state is ConfigEntryState.LOADED hass.states.async_set("sensor.test", STATE_ON) @@ -84,14 +94,16 @@ class FilterTest: @pytest.fixture(name="mock_send_batch") -def mock_send_batch_fixture(): +def mock_send_batch_fixture() -> Generator[AsyncMock]: """Mock send_batch.""" with patch(f"{PRODUCER_PATH}.send_batch") as mock_send_batch: yield mock_send_batch @pytest.fixture(autouse=True, name="mock_client") -def mock_client_fixture(mock_send_batch): +def mock_client_fixture( + mock_send_batch: AsyncMock, +) -> Generator[tuple[AsyncMock, AsyncMock]]: """Mock the azure event hub producer client.""" with patch(f"{PRODUCER_PATH}.close") as mock_close: yield ( @@ -101,7 +113,7 @@ def mock_client_fixture(mock_send_batch): @pytest.fixture(name="mock_create_batch") -def mock_create_batch_fixture(): +def mock_create_batch_fixture() -> Generator[MagicMock]: """Mock batch creator and return mocked batch object.""" mock_batch = MagicMock() with patch(f"{PRODUCER_PATH}.create_batch", return_value=mock_batch): @@ -110,7 +122,7 @@ def mock_create_batch_fixture(): # fixtures for config flow tests @pytest.fixture(name="mock_from_connection_string") -def mock_from_connection_string_fixture(): +def mock_from_connection_string_fixture() -> Generator[MagicMock]: """Mock AEH from connection string creation.""" mock_aeh = MagicMock(spec=EventHubProducerClient) mock_aeh.__aenter__.return_value = mock_aeh @@ -122,7 +134,7 @@ def mock_from_connection_string_fixture(): @pytest.fixture -def mock_setup_entry(): +def mock_setup_entry() -> Generator[AsyncMock]: """Mock the setup entry call, used for config flow tests.""" with patch( f"{AZURE_EVENT_HUB_PATH}.async_setup_entry", return_value=True diff --git a/tests/components/azure_event_hub/test_config_flow.py b/tests/components/azure_event_hub/test_config_flow.py index cedbc5b43d6..52685c36bbe 100644 --- a/tests/components/azure_event_hub/test_config_flow.py +++ b/tests/components/azure_event_hub/test_config_flow.py @@ -1,7 +1,8 @@ """Test the AEH config flow.""" import logging -from unittest.mock import AsyncMock +from typing import Any +from unittest.mock import AsyncMock, MagicMock from azure.eventhub.exceptions import EventHubError import pytest @@ -43,14 +44,14 @@ pytestmark = pytest.mark.usefixtures("mock_setup_entry") ], ids=["connection_string", "sas"], ) +@pytest.mark.usefixtures("mock_from_connection_string") async def test_form( hass: HomeAssistant, mock_setup_entry: AsyncMock, - mock_from_connection_string, - step1_config, - step_id, - step2_config, - data_config, + step1_config: dict[str, Any], + step_id: str, + step2_config: dict[str, str], + data_config: dict[str, str], ) -> None: """Test we get the form.""" result = await hass.config_entries.flow.async_init( @@ -101,7 +102,7 @@ async def test_import(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: [config_entries.SOURCE_USER, config_entries.SOURCE_IMPORT], ids=["user", "import"], ) -async def test_single_instance(hass: HomeAssistant, source) -> None: +async def test_single_instance(hass: HomeAssistant, source: str) -> None: """Test uniqueness of username.""" entry = MockConfigEntry( domain=DOMAIN, @@ -126,9 +127,9 @@ async def test_single_instance(hass: HomeAssistant, source) -> None: ) async def test_connection_error_sas( hass: HomeAssistant, - mock_get_eventhub_properties, - side_effect, - error_message, + mock_get_eventhub_properties: AsyncMock, + side_effect: Exception, + error_message: str, ) -> None: """Test we handle connection errors.""" result = await hass.config_entries.flow.async_init( @@ -155,9 +156,9 @@ async def test_connection_error_sas( ) async def test_connection_error_cs( hass: HomeAssistant, - mock_from_connection_string, - side_effect, - error_message, + mock_from_connection_string: MagicMock, + side_effect: Exception, + error_message: str, ) -> None: """Test we handle connection errors.""" result = await hass.config_entries.flow.async_init( @@ -178,7 +179,7 @@ async def test_connection_error_cs( assert result2["errors"] == {"base": error_message} -async def test_options_flow(hass: HomeAssistant, entry) -> None: +async def test_options_flow(hass: HomeAssistant, entry: MockConfigEntry) -> None: """Test options flow.""" result = await hass.config_entries.options.async_init(entry.entry_id) diff --git a/tests/components/azure_event_hub/test_init.py b/tests/components/azure_event_hub/test_init.py index 1440bc2ede9..1b0550b147b 100644 --- a/tests/components/azure_event_hub/test_init.py +++ b/tests/components/azure_event_hub/test_init.py @@ -2,7 +2,7 @@ from datetime import timedelta import logging -from unittest.mock import patch +from unittest.mock import AsyncMock, MagicMock, patch from azure.eventhub.exceptions import EventHubError import pytest @@ -60,7 +60,9 @@ async def test_filter_only_config(hass: HomeAssistant) -> None: assert await async_setup_component(hass, DOMAIN, config) -async def test_unload_entry(hass: HomeAssistant, entry, mock_create_batch) -> None: +async def test_unload_entry( + hass: HomeAssistant, entry: MockConfigEntry, mock_create_batch: MagicMock +) -> None: """Test being able to unload an entry. Queue should be empty, so adding events to the batch should not be called, @@ -73,7 +75,7 @@ async def test_unload_entry(hass: HomeAssistant, entry, mock_create_batch) -> No async def test_failed_test_connection( - hass: HomeAssistant, mock_get_eventhub_properties + hass: HomeAssistant, mock_get_eventhub_properties: AsyncMock ) -> None: """Test being able to unload an entry.""" entry = MockConfigEntry( @@ -89,7 +91,9 @@ async def test_failed_test_connection( async def test_send_batch_error( - hass: HomeAssistant, entry_with_one_event, mock_send_batch + hass: HomeAssistant, + entry_with_one_event: MockConfigEntry, + mock_send_batch: AsyncMock, ) -> None: """Test a error in send_batch, including recovering at the next interval.""" mock_send_batch.reset_mock() @@ -111,7 +115,9 @@ async def test_send_batch_error( async def test_late_event( - hass: HomeAssistant, entry_with_one_event, mock_create_batch + hass: HomeAssistant, + entry_with_one_event: MockConfigEntry, + mock_create_batch: MagicMock, ) -> None: """Test the check on late events.""" with patch( @@ -128,7 +134,9 @@ async def test_late_event( async def test_full_batch( - hass: HomeAssistant, entry_with_one_event, mock_create_batch + hass: HomeAssistant, + entry_with_one_event: MockConfigEntry, + mock_create_batch: MagicMock, ) -> None: """Test the full batch behaviour.""" mock_create_batch.add.side_effect = [ValueError, None] @@ -208,7 +216,12 @@ async def test_full_batch( ], ids=["allowlist", "denylist", "filtered_allowlist", "filtered_denylist"], ) -async def test_filter(hass: HomeAssistant, entry, tests, mock_create_batch) -> None: +async def test_filter( + hass: HomeAssistant, + entry: MockConfigEntry, + tests: list[FilterTest], + mock_create_batch: MagicMock, +) -> None: """Test different filters. Filter_schema is also a fixture which is replaced by the filter_schema From 78f53341b759f1717cf0fcb3e9a48dd94d172524 Mon Sep 17 00:00:00 2001 From: Mike Degatano Date: Fri, 7 Jun 2024 04:52:15 -0400 Subject: [PATCH 0337/1445] Always have addon url in detached_addon_missing (#119011) --- homeassistant/components/hassio/issues.py | 7 +++---- tests/components/hassio/test_issues.py | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/hassio/issues.py b/homeassistant/components/hassio/issues.py index 2de6f71d838..9c2152489d6 100644 --- a/homeassistant/components/hassio/issues.py +++ b/homeassistant/components/hassio/issues.py @@ -267,15 +267,14 @@ class SupervisorIssues: placeholders = {PLACEHOLDER_KEY_REFERENCE: issue.reference} if issue.key == ISSUE_KEY_ADDON_DETACHED_ADDON_MISSING: + placeholders[PLACEHOLDER_KEY_ADDON_URL] = ( + f"/hassio/addon/{issue.reference}" + ) addons = get_addons_info(self._hass) if addons and issue.reference in addons: placeholders[PLACEHOLDER_KEY_ADDON] = addons[issue.reference][ "name" ] - if "url" in addons[issue.reference]: - placeholders[PLACEHOLDER_KEY_ADDON_URL] = addons[ - issue.reference - ]["url"] else: placeholders[PLACEHOLDER_KEY_ADDON] = issue.reference diff --git a/tests/components/hassio/test_issues.py b/tests/components/hassio/test_issues.py index c6db7d56261..ff0e4a8dd92 100644 --- a/tests/components/hassio/test_issues.py +++ b/tests/components/hassio/test_issues.py @@ -878,6 +878,6 @@ async def test_supervisor_issues_detached_addon_missing( placeholders={ "reference": "test", "addon": "test", - "addon_url": "https://github.com/home-assistant/addons/test", + "addon_url": "/hassio/addon/test", }, ) From f2d674d28d2e74d0e43605979e433b0c7cec8a99 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 7 Jun 2024 10:53:54 +0200 Subject: [PATCH 0338/1445] Bump babel to 2.15.0 (#119006) --- homeassistant/components/holiday/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/holiday/manifest.json b/homeassistant/components/holiday/manifest.json index bc7ce0e8dd1..c026c3e6363 100644 --- a/homeassistant/components/holiday/manifest.json +++ b/homeassistant/components/holiday/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/holiday", "iot_class": "local_polling", - "requirements": ["holidays==0.50", "babel==2.13.1"] + "requirements": ["holidays==0.50", "babel==2.15.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 27751e09c8e..ceca496ea2a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -529,7 +529,7 @@ azure-kusto-ingest==3.1.0 azure-servicebus==7.10.0 # homeassistant.components.holiday -babel==2.13.1 +babel==2.15.0 # homeassistant.components.baidu baidu-aip==1.6.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 416b836a329..88255a6d3e4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -466,7 +466,7 @@ azure-kusto-data[aio]==3.1.0 azure-kusto-ingest==3.1.0 # homeassistant.components.holiday -babel==2.13.1 +babel==2.15.0 # homeassistant.components.homekit base36==0.1.1 From 5fafbebf87020d09e2faf76df252fde30c39be92 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 7 Jun 2024 10:54:31 +0200 Subject: [PATCH 0339/1445] Bump dawidd6/action-download-artifact from 4 to 5 (#118851) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/builder.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml index 3d1b85666cd..80c32d47c1c 100644 --- a/.github/workflows/builder.yml +++ b/.github/workflows/builder.yml @@ -94,7 +94,7 @@ jobs: - name: Download nightly wheels of frontend if: needs.init.outputs.channel == 'dev' - uses: dawidd6/action-download-artifact@v4 + uses: dawidd6/action-download-artifact@v5 with: github_token: ${{secrets.GITHUB_TOKEN}} repo: home-assistant/frontend @@ -105,7 +105,7 @@ jobs: - name: Download nightly wheels of intents if: needs.init.outputs.channel == 'dev' - uses: dawidd6/action-download-artifact@v4 + uses: dawidd6/action-download-artifact@v5 with: github_token: ${{secrets.GITHUB_TOKEN}} repo: home-assistant/intents-package From 7947f63860d686a67a4b9ca218dff4fcc32e391f Mon Sep 17 00:00:00 2001 From: Huyuwei Date: Fri, 7 Jun 2024 16:56:12 +0800 Subject: [PATCH 0340/1445] Enable retrieving sensor data from WoHub2 device and update pySwitchbot to 0.47.2 (#118567) --- homeassistant/components/switchbot/__init__.py | 1 + homeassistant/components/switchbot/const.py | 2 ++ homeassistant/components/switchbot/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 6 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/switchbot/__init__.py b/homeassistant/components/switchbot/__init__.py index 6bad3c25142..82860db6745 100644 --- a/homeassistant/components/switchbot/__init__.py +++ b/homeassistant/components/switchbot/__init__.py @@ -55,6 +55,7 @@ PLATFORMS_BY_TYPE = { Platform.BINARY_SENSOR, Platform.SENSOR, ], + SupportedModels.HUB2.value: [Platform.SENSOR], } CLASS_BY_DEVICE = { SupportedModels.CEILING_LIGHT.value: switchbot.SwitchbotCeilingLight, diff --git a/homeassistant/components/switchbot/const.py b/homeassistant/components/switchbot/const.py index 9993bd95415..7e7a1d185f2 100644 --- a/homeassistant/components/switchbot/const.py +++ b/homeassistant/components/switchbot/const.py @@ -27,6 +27,7 @@ class SupportedModels(StrEnum): HUMIDIFIER = "humidifier" LOCK = "lock" BLIND_TILT = "blind_tilt" + HUB2 = "hub2" CONNECTABLE_SUPPORTED_MODEL_TYPES = { @@ -39,6 +40,7 @@ CONNECTABLE_SUPPORTED_MODEL_TYPES = { SwitchbotModel.HUMIDIFIER: SupportedModels.HUMIDIFIER, SwitchbotModel.LOCK: SupportedModels.LOCK, SwitchbotModel.BLIND_TILT: SupportedModels.BLIND_TILT, + SwitchbotModel.HUB2: SupportedModels.HUB2, } NON_CONNECTABLE_SUPPORTED_MODEL_TYPES = { diff --git a/homeassistant/components/switchbot/manifest.json b/homeassistant/components/switchbot/manifest.json index 2388e5a98b3..c408a369761 100644 --- a/homeassistant/components/switchbot/manifest.json +++ b/homeassistant/components/switchbot/manifest.json @@ -39,5 +39,5 @@ "documentation": "https://www.home-assistant.io/integrations/switchbot", "iot_class": "local_push", "loggers": ["switchbot"], - "requirements": ["PySwitchbot==0.46.1"] + "requirements": ["PySwitchbot==0.47.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index ceca496ea2a..479677849bc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -87,7 +87,7 @@ PyQRCode==1.2.1 PyRMVtransport==0.3.3 # homeassistant.components.switchbot -PySwitchbot==0.46.1 +PySwitchbot==0.47.2 # homeassistant.components.switchmate PySwitchmate==0.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 88255a6d3e4..630d356bcaf 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -75,7 +75,7 @@ PyQRCode==1.2.1 PyRMVtransport==0.3.3 # homeassistant.components.switchbot -PySwitchbot==0.46.1 +PySwitchbot==0.47.2 # homeassistant.components.syncthru PySyncThru==0.7.10 From 4600960895741a6fb99d4230d4bab11d9e2c660e Mon Sep 17 00:00:00 2001 From: Jeef Date: Fri, 7 Jun 2024 05:02:41 -0400 Subject: [PATCH 0341/1445] Align weatherflow_cloud weather conditions with Home Assistant supported conditions (#114497) * WeatherFlow Cloud changing icon mapping specificaly for Colorado * changing name to state_map --- .../components/weatherflow_cloud/const.py | 22 +++++++++++++++++++ .../components/weatherflow_cloud/weather.py | 4 ++-- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/weatherflow_cloud/const.py b/homeassistant/components/weatherflow_cloud/const.py index 43594863e14..24ae2f3a3cb 100644 --- a/homeassistant/components/weatherflow_cloud/const.py +++ b/homeassistant/components/weatherflow_cloud/const.py @@ -7,3 +7,25 @@ LOGGER = logging.getLogger(__package__) ATTR_ATTRIBUTION = "Weather data delivered by WeatherFlow/Tempest REST Api" MANUFACTURER = "WeatherFlow" + +STATE_MAP = { + "clear-day": "sunny", + "clear-night": "clear-night", + "cloudy": "cloudy", + "foggy": "fog", + "partly-cloudy-day": "partlycloudy", + "partly-cloudy-night": "partlycloudy", + "possibly-rainy-day": "rainy", + "possibly-rainy-night": "rainy", + "possibly-sleet-day": "snowy-rainy", + "possibly-sleet-night": "snowy-rainy", + "possibly-snow-day": "snowy", + "possibly-snow-night": "snowy", + "possibly-thunderstorm-day": "lightning-rainy", + "possibly-thunderstorm-night": "lightning-rainy", + "rainy": "rainy", + "sleet": "snowy-rainy", + "snow": "snowy", + "thunderstorm": "lightning", + "windy": "windy", +} diff --git a/homeassistant/components/weatherflow_cloud/weather.py b/homeassistant/components/weatherflow_cloud/weather.py index 23aa6b1a031..47e2b6a28df 100644 --- a/homeassistant/components/weatherflow_cloud/weather.py +++ b/homeassistant/components/weatherflow_cloud/weather.py @@ -20,7 +20,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import ATTR_ATTRIBUTION, DOMAIN, MANUFACTURER +from .const import ATTR_ATTRIBUTION, DOMAIN, MANUFACTURER, STATE_MAP from .coordinator import WeatherFlowCloudDataUpdateCoordinator @@ -86,7 +86,7 @@ class WeatherFlowWeather( @property def condition(self) -> str | None: """Return current condition - required property.""" - return self.local_data.weather.current_conditions.icon.ha_icon + return STATE_MAP[self.local_data.weather.current_conditions.icon.value] @property def native_temperature(self) -> float | None: From af65da3875c8a98636f558ef157e92c148fa6d1d Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Fri, 7 Jun 2024 11:13:33 +0200 Subject: [PATCH 0342/1445] Add type ignore comments (#119052) --- homeassistant/components/google_assistant_sdk/__init__.py | 2 +- homeassistant/components/google_assistant_sdk/helpers.py | 2 +- homeassistant/components/google_sheets/__init__.py | 4 +++- homeassistant/components/google_sheets/config_flow.py | 4 +++- homeassistant/components/nest/api.py | 4 ++-- 5 files changed, 10 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/google_assistant_sdk/__init__.py b/homeassistant/components/google_assistant_sdk/__init__.py index 52950a82b93..b92b3c54579 100644 --- a/homeassistant/components/google_assistant_sdk/__init__.py +++ b/homeassistant/components/google_assistant_sdk/__init__.py @@ -165,7 +165,7 @@ class GoogleAssistantConversationAgent(conversation.AbstractConversationAgent): await session.async_ensure_token_valid() self.assistant = None if not self.assistant or user_input.language != self.language: - credentials = Credentials(session.token[CONF_ACCESS_TOKEN]) + credentials = Credentials(session.token[CONF_ACCESS_TOKEN]) # type: ignore[no-untyped-call] self.language = user_input.language self.assistant = TextAssistant(credentials, self.language) diff --git a/homeassistant/components/google_assistant_sdk/helpers.py b/homeassistant/components/google_assistant_sdk/helpers.py index b6b13f92fcf..24da381e8e0 100644 --- a/homeassistant/components/google_assistant_sdk/helpers.py +++ b/homeassistant/components/google_assistant_sdk/helpers.py @@ -72,7 +72,7 @@ async def async_send_text_commands( entry.async_start_reauth(hass) raise - credentials = Credentials(session.token[CONF_ACCESS_TOKEN]) + credentials = Credentials(session.token[CONF_ACCESS_TOKEN]) # type: ignore[no-untyped-call] language_code = entry.options.get(CONF_LANGUAGE_CODE, default_language_code(hass)) with TextAssistant( credentials, language_code, audio_out=bool(media_players) diff --git a/homeassistant/components/google_sheets/__init__.py b/homeassistant/components/google_sheets/__init__.py index f346f913e0c..713a801257d 100644 --- a/homeassistant/components/google_sheets/__init__.py +++ b/homeassistant/components/google_sheets/__init__.py @@ -93,7 +93,9 @@ async def async_setup_service(hass: HomeAssistant) -> None: def _append_to_sheet(call: ServiceCall, entry: ConfigEntry) -> None: """Run append in the executor.""" - service = Client(Credentials(entry.data[CONF_TOKEN][CONF_ACCESS_TOKEN])) + service = Client( + Credentials(entry.data[CONF_TOKEN][CONF_ACCESS_TOKEN]) # type: ignore[no-untyped-call] + ) try: sheet = service.open_by_key(entry.unique_id) except RefreshError: diff --git a/homeassistant/components/google_sheets/config_flow.py b/homeassistant/components/google_sheets/config_flow.py index a0a99742249..ab0c084c317 100644 --- a/homeassistant/components/google_sheets/config_flow.py +++ b/homeassistant/components/google_sheets/config_flow.py @@ -61,7 +61,9 @@ class OAuth2FlowHandler( async def async_oauth_create_entry(self, data: dict[str, Any]) -> ConfigFlowResult: """Create an entry for the flow, or update existing entry.""" - service = Client(Credentials(data[CONF_TOKEN][CONF_ACCESS_TOKEN])) + service = Client( + Credentials(data[CONF_TOKEN][CONF_ACCESS_TOKEN]) # type: ignore[no-untyped-call] + ) if self.reauth_entry: _LOGGER.debug("service.open_by_key") diff --git a/homeassistant/components/nest/api.py b/homeassistant/components/nest/api.py index 8c9ca4bec96..3ef26747115 100644 --- a/homeassistant/components/nest/api.py +++ b/homeassistant/components/nest/api.py @@ -57,7 +57,7 @@ class AsyncConfigEntryAuth(AbstractAuth): # even when it is expired to fully hand off this responsibility and # know it is working at startup (then if not, fail loudly). token = self._oauth_session.token - creds = Credentials( + creds = Credentials( # type: ignore[no-untyped-call] token=token["access_token"], refresh_token=token["refresh_token"], token_uri=OAUTH2_TOKEN, @@ -92,7 +92,7 @@ class AccessTokenAuthImpl(AbstractAuth): async def async_get_creds(self) -> Credentials: """Return an OAuth credential for Pub/Sub Subscriber.""" - return Credentials( + return Credentials( # type: ignore[no-untyped-call] token=self._access_token, token_uri=OAUTH2_TOKEN, scopes=SDM_SCOPES, From b3a71dcea31fb8c12bf718753a1c154729b547cd Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 7 Jun 2024 11:31:45 +0200 Subject: [PATCH 0343/1445] Improve type hints in homekit_controller tests (#119053) --- tests/components/homekit_controller/conftest.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/components/homekit_controller/conftest.py b/tests/components/homekit_controller/conftest.py index 8bfb78b9840..427c5285436 100644 --- a/tests/components/homekit_controller/conftest.py +++ b/tests/components/homekit_controller/conftest.py @@ -1,7 +1,7 @@ """HomeKit controller session fixtures.""" import datetime -import unittest.mock +from unittest.mock import MagicMock, patch from aiohomekit.testing import FakeController from freezegun import freeze_time @@ -26,10 +26,10 @@ def freeze_time_in_future() -> Generator[FrozenDateTimeFactory]: @pytest.fixture -def controller(hass): +def controller() -> Generator[FakeController]: """Replace aiohomekit.Controller with an instance of aiohomekit.testing.FakeController.""" instance = FakeController() - with unittest.mock.patch( + with patch( "homeassistant.components.homekit_controller.utils.Controller", return_value=instance, ): @@ -37,10 +37,10 @@ def controller(hass): @pytest.fixture(autouse=True) -def hk_mock_async_zeroconf(mock_async_zeroconf): +def hk_mock_async_zeroconf(mock_async_zeroconf: MagicMock) -> None: """Auto mock zeroconf.""" @pytest.fixture(autouse=True) -def auto_mock_bluetooth(mock_bluetooth): +def auto_mock_bluetooth(mock_bluetooth: None) -> None: """Auto mock bluetooth.""" From 1c8a9cc3b8db314ce38b83e9b1e41ba11402e8f8 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 7 Jun 2024 11:38:56 +0200 Subject: [PATCH 0344/1445] Remove unused caplog fixtures in tests (#119056) --- tests/components/automation/test_init.py | 6 ++---- tests/components/ring/test_init.py | 1 - tests/test_block_async_io.py | 6 ++---- tests/test_bootstrap.py | 4 +--- tests/test_loader.py | 12 +++--------- 5 files changed, 8 insertions(+), 21 deletions(-) diff --git a/tests/components/automation/test_init.py b/tests/components/automation/test_init.py index 7b3d4c4010e..a8e89d0ad97 100644 --- a/tests/components/automation/test_init.py +++ b/tests/components/automation/test_init.py @@ -2940,9 +2940,7 @@ def test_deprecated_constants( ) -async def test_automation_turns_off_other_automation( - hass: HomeAssistant, caplog: pytest.LogCaptureFixture -) -> None: +async def test_automation_turns_off_other_automation(hass: HomeAssistant) -> None: """Test an automation that turns off another automation.""" hass.set_state(CoreState.not_running) calls = async_mock_service(hass, "persistent_notification", "create") @@ -3021,7 +3019,7 @@ async def test_automation_turns_off_other_automation( async def test_two_automations_call_restart_script_same_time( - hass: HomeAssistant, caplog: pytest.LogCaptureFixture + hass: HomeAssistant, ) -> None: """Test two automations that call a restart mode script at the same.""" hass.states.async_set("binary_sensor.presence", "off") diff --git a/tests/components/ring/test_init.py b/tests/components/ring/test_init.py index feb2485303a..d8529e874b9 100644 --- a/tests/components/ring/test_init.py +++ b/tests/components/ring/test_init.py @@ -38,7 +38,6 @@ async def test_setup_entry_device_update( mock_ring_devices, freezer: FrozenDateTimeFactory, mock_added_config_entry: MockConfigEntry, - caplog, ) -> None: """Test devices are updating after setup entry.""" diff --git a/tests/test_block_async_io.py b/tests/test_block_async_io.py index 1ceb84c249f..b7ecb034981 100644 --- a/tests/test_block_async_io.py +++ b/tests/test_block_async_io.py @@ -43,7 +43,7 @@ async def test_protect_loop_debugger_sleep(caplog: pytest.LogCaptureFixture) -> assert "Detected blocking call inside the event loop" not in caplog.text -async def test_protect_loop_sleep(caplog: pytest.LogCaptureFixture) -> None: +async def test_protect_loop_sleep() -> None: """Test time.sleep not injected by the debugger raises.""" block_async_io.enable() frames = extract_stack_to_frame( @@ -71,9 +71,7 @@ async def test_protect_loop_sleep(caplog: pytest.LogCaptureFixture) -> None: time.sleep(0) -async def test_protect_loop_sleep_get_current_frame_raises( - caplog: pytest.LogCaptureFixture, -) -> None: +async def test_protect_loop_sleep_get_current_frame_raises() -> None: """Test time.sleep when get_current_frame raises ValueError.""" block_async_io.enable() frames = extract_stack_to_frame( diff --git a/tests/test_bootstrap.py b/tests/test_bootstrap.py index afd95ca61cf..9e04421a58a 100644 --- a/tests/test_bootstrap.py +++ b/tests/test_bootstrap.py @@ -1171,9 +1171,7 @@ async def test_bootstrap_is_cancellation_safe( @pytest.mark.parametrize("load_registries", [False]) -async def test_bootstrap_empty_integrations( - hass: HomeAssistant, caplog: pytest.LogCaptureFixture -) -> None: +async def test_bootstrap_empty_integrations(hass: HomeAssistant) -> None: """Test setting up an empty integrations does not raise.""" await bootstrap.async_setup_multi_components(hass, set(), {}) await hass.async_block_till_done() diff --git a/tests/test_loader.py b/tests/test_loader.py index fa4a3a14cef..328b55ddf80 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -1034,9 +1034,7 @@ async def test_get_custom_components_recovery_mode(hass: HomeAssistant) -> None: assert await loader.async_get_custom_components(hass) == {} -async def test_custom_integration_missing_version( - hass: HomeAssistant, caplog: pytest.LogCaptureFixture -) -> None: +async def test_custom_integration_missing_version(hass: HomeAssistant) -> None: """Test trying to load a custom integration without a version twice does not deadlock.""" with pytest.raises(loader.IntegrationNotFound): await loader.async_get_integration(hass, "test_no_version") @@ -1045,9 +1043,7 @@ async def test_custom_integration_missing_version( await loader.async_get_integration(hass, "test_no_version") -async def test_custom_integration_missing( - hass: HomeAssistant, caplog: pytest.LogCaptureFixture -) -> None: +async def test_custom_integration_missing(hass: HomeAssistant) -> None: """Test trying to load a custom integration that is missing twice not deadlock.""" with patch("homeassistant.loader.async_get_custom_components") as mock_get: mock_get.return_value = {} @@ -1296,7 +1292,7 @@ async def test_hass_components_use_reported( async def test_async_get_component_preloads_config_and_config_flow( - hass: HomeAssistant, caplog: pytest.LogCaptureFixture + hass: HomeAssistant, ) -> None: """Verify async_get_component will try to preload the config and config_flow platform.""" executor_import_integration = _get_test_integration( @@ -1407,7 +1403,6 @@ async def test_async_get_component_loads_loop_if_already_in_sys_modules( async def test_async_get_component_concurrent_loads( hass: HomeAssistant, - caplog: pytest.LogCaptureFixture, enable_custom_integrations: None, ) -> None: """Verify async_get_component waits if the first load if called again when still in progress.""" @@ -1882,7 +1877,6 @@ async def test_async_get_platforms_loads_loop_if_already_in_sys_modules( async def test_async_get_platforms_concurrent_loads( hass: HomeAssistant, - caplog: pytest.LogCaptureFixture, enable_custom_integrations: None, ) -> None: """Verify async_get_platforms waits if the first load if called again. From 907297cd1aacb67f73b425f2a7904b107b1ff80c Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 7 Jun 2024 11:40:03 +0200 Subject: [PATCH 0345/1445] Improve type hints in config tests (#119055) --- tests/components/config/test_auth.py | 6 ++++-- tests/components/config/test_script.py | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/components/config/test_auth.py b/tests/components/config/test_auth.py index b839d2de7a0..c6a9547b451 100644 --- a/tests/components/config/test_auth.py +++ b/tests/components/config/test_auth.py @@ -7,11 +7,13 @@ from homeassistant.components.config import auth as auth_config from homeassistant.core import HomeAssistant from tests.common import CLIENT_ID, MockGroup, MockUser -from tests.typing import WebSocketGenerator +from tests.typing import ClientSessionGenerator, WebSocketGenerator @pytest.fixture(autouse=True) -async def setup_config(hass, aiohttp_client): +async def setup_config( + hass: HomeAssistant, aiohttp_client: ClientSessionGenerator +) -> None: """Fixture that sets up the auth provider homeassistant module.""" auth_config.async_setup(hass) diff --git a/tests/components/config/test_script.py b/tests/components/config/test_script.py index 3c1970a9bca..3ee45aec26a 100644 --- a/tests/components/config/test_script.py +++ b/tests/components/config/test_script.py @@ -24,7 +24,7 @@ def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None: @pytest.fixture(autouse=True) -async def setup_script(hass, script_config, stub_blueprint_populate): +async def setup_script(hass: HomeAssistant, script_config: dict[str, Any]) -> None: """Set up script integration.""" assert await async_setup_component(hass, "script", {"script": script_config}) From 5f309b69cfcb4aa2397e34465b27baa861956ae5 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 7 Jun 2024 11:40:23 +0200 Subject: [PATCH 0346/1445] Add type hints to current_request_with_host in tests (#119054) --- tests/components/electric_kiwi/conftest.py | 3 +-- tests/components/google/test_config_flow.py | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/components/electric_kiwi/conftest.py b/tests/components/electric_kiwi/conftest.py index 5d08aa1ba77..c9f9c7e04f0 100644 --- a/tests/components/electric_kiwi/conftest.py +++ b/tests/components/electric_kiwi/conftest.py @@ -29,9 +29,8 @@ type ComponentSetup = Callable[[], Awaitable[bool]] @pytest.fixture(autouse=True) -async def request_setup(current_request_with_host) -> None: +async def request_setup(current_request_with_host: None) -> None: """Request setup.""" - return @pytest.fixture diff --git a/tests/components/google/test_config_flow.py b/tests/components/google/test_config_flow.py index 53ec06619ac..12281f6d348 100644 --- a/tests/components/google/test_config_flow.py +++ b/tests/components/google/test_config_flow.py @@ -50,9 +50,8 @@ OAUTH2_TOKEN = "https://oauth2.googleapis.com/token" @pytest.fixture(autouse=True) -async def request_setup(current_request_with_host) -> None: +async def request_setup(current_request_with_host: None) -> None: """Request setup.""" - return @pytest.fixture(autouse=True) From bfff3c05244c9d4fe3e3c4defcb68cf40018743c Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 7 Jun 2024 12:09:18 +0200 Subject: [PATCH 0347/1445] Add type hint to mock_async_zeroconf in test fixtures (#119057) --- tests/components/bosch_shc/conftest.py | 4 +++- tests/components/default_config/conftest.py | 4 +++- tests/components/devolo_home_control/conftest.py | 4 ++-- tests/components/devolo_home_network/conftest.py | 4 ++-- tests/components/esphome/conftest.py | 4 ++-- tests/components/otbr/conftest.py | 4 ++-- tests/components/rabbitair/test_config_flow.py | 4 ++-- tests/components/thread/conftest.py | 4 +++- 8 files changed, 19 insertions(+), 13 deletions(-) diff --git a/tests/components/bosch_shc/conftest.py b/tests/components/bosch_shc/conftest.py index 6a3797ad094..1f45623e30f 100644 --- a/tests/components/bosch_shc/conftest.py +++ b/tests/components/bosch_shc/conftest.py @@ -1,8 +1,10 @@ """bosch_shc session fixtures.""" +from unittest.mock import MagicMock + import pytest @pytest.fixture(autouse=True) -def bosch_shc_mock_async_zeroconf(mock_async_zeroconf): +def bosch_shc_mock_async_zeroconf(mock_async_zeroconf: MagicMock) -> None: """Auto mock zeroconf.""" diff --git a/tests/components/default_config/conftest.py b/tests/components/default_config/conftest.py index 4714102eff9..ce1b3ad8de4 100644 --- a/tests/components/default_config/conftest.py +++ b/tests/components/default_config/conftest.py @@ -1,8 +1,10 @@ """default_config session fixtures.""" +from unittest.mock import MagicMock + import pytest @pytest.fixture(autouse=True) -def default_config_mock_async_zeroconf(mock_async_zeroconf): +def default_config_mock_async_zeroconf(mock_async_zeroconf: MagicMock) -> None: """Auto mock zeroconf.""" diff --git a/tests/components/devolo_home_control/conftest.py b/tests/components/devolo_home_control/conftest.py index 5d67bffddfd..04752da5925 100644 --- a/tests/components/devolo_home_control/conftest.py +++ b/tests/components/devolo_home_control/conftest.py @@ -1,6 +1,6 @@ """Fixtures for tests.""" -from unittest.mock import patch +from unittest.mock import MagicMock, patch import pytest from typing_extensions import Generator @@ -39,5 +39,5 @@ def patch_mydevolo(credentials_valid: bool, maintenance: bool) -> Generator[None @pytest.fixture(autouse=True) -def devolo_home_control_mock_async_zeroconf(mock_async_zeroconf): +def devolo_home_control_mock_async_zeroconf(mock_async_zeroconf: MagicMock) -> None: """Auto mock zeroconf.""" diff --git a/tests/components/devolo_home_network/conftest.py b/tests/components/devolo_home_network/conftest.py index f6a6e233b6d..fd03063cd34 100644 --- a/tests/components/devolo_home_network/conftest.py +++ b/tests/components/devolo_home_network/conftest.py @@ -1,7 +1,7 @@ """Fixtures for tests.""" from itertools import cycle -from unittest.mock import patch +from unittest.mock import MagicMock, patch import pytest @@ -50,5 +50,5 @@ def mock_validate_input(): @pytest.fixture(autouse=True) -def devolo_home_network_mock_async_zeroconf(mock_async_zeroconf): +def devolo_home_network_mock_async_zeroconf(mock_async_zeroconf: MagicMock) -> None: """Auto mock zeroconf.""" diff --git a/tests/components/esphome/conftest.py b/tests/components/esphome/conftest.py index 91d4f140b12..f1fae38e0e3 100644 --- a/tests/components/esphome/conftest.py +++ b/tests/components/esphome/conftest.py @@ -7,7 +7,7 @@ from asyncio import Event from collections.abc import Awaitable, Callable from pathlib import Path from typing import Any -from unittest.mock import AsyncMock, Mock, patch +from unittest.mock import AsyncMock, MagicMock, Mock, patch from aioesphomeapi import ( APIClient, @@ -47,7 +47,7 @@ def mock_bluetooth(enable_bluetooth: None) -> None: @pytest.fixture(autouse=True) -def esphome_mock_async_zeroconf(mock_async_zeroconf): +def esphome_mock_async_zeroconf(mock_async_zeroconf: MagicMock) -> None: """Auto mock zeroconf.""" diff --git a/tests/components/otbr/conftest.py b/tests/components/otbr/conftest.py index 82f167cdd23..ba0f43c4a71 100644 --- a/tests/components/otbr/conftest.py +++ b/tests/components/otbr/conftest.py @@ -1,6 +1,6 @@ """Test fixtures for the Open Thread Border Router integration.""" -from unittest.mock import Mock, patch +from unittest.mock import MagicMock, Mock, patch import pytest @@ -73,7 +73,7 @@ async def otbr_config_entry_thread_fixture(hass): @pytest.fixture(autouse=True) -def use_mocked_zeroconf(mock_async_zeroconf): +def use_mocked_zeroconf(mock_async_zeroconf: MagicMock) -> None: """Mock zeroconf in all tests.""" diff --git a/tests/components/rabbitair/test_config_flow.py b/tests/components/rabbitair/test_config_flow.py index 57b7287db8c..2e0cfba38c0 100644 --- a/tests/components/rabbitair/test_config_flow.py +++ b/tests/components/rabbitair/test_config_flow.py @@ -3,7 +3,7 @@ from __future__ import annotations from ipaddress import ip_address -from unittest.mock import Mock, patch +from unittest.mock import MagicMock, Mock, patch import pytest from rabbitair import Mode, Model, Speed @@ -38,7 +38,7 @@ ZEROCONF_DATA = zeroconf.ZeroconfServiceInfo( @pytest.fixture(autouse=True) -def use_mocked_zeroconf(mock_async_zeroconf): +def use_mocked_zeroconf(mock_async_zeroconf: MagicMock) -> None: """Mock zeroconf in all tests.""" diff --git a/tests/components/thread/conftest.py b/tests/components/thread/conftest.py index 2b0f00a097f..1230d379b82 100644 --- a/tests/components/thread/conftest.py +++ b/tests/components/thread/conftest.py @@ -1,5 +1,7 @@ """Test fixtures for the Thread integration.""" +from unittest.mock import MagicMock + import pytest from homeassistant.components import thread @@ -24,5 +26,5 @@ async def thread_config_entry_fixture(hass: HomeAssistant): @pytest.fixture(autouse=True) -def use_mocked_zeroconf(mock_async_zeroconf): +def use_mocked_zeroconf(mock_async_zeroconf: MagicMock) -> None: """Mock zeroconf in all tests.""" From f6c66dfd27a3aad8463d4945f5bf97c586b221d3 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 7 Jun 2024 12:11:15 +0200 Subject: [PATCH 0348/1445] Bump aiowithings to 3.0.1 (#118854) --- .../components/withings/coordinator.py | 13 +- .../components/withings/manifest.json | 2 +- homeassistant/components/withings/sensor.py | 97 ++- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../withings/fixtures/measurements.json | 471 +++++++++++++++ .../withings/snapshots/test_diagnostics.ambr | 552 +++++++++++++++--- 7 files changed, 1034 insertions(+), 105 deletions(-) diff --git a/homeassistant/components/withings/coordinator.py b/homeassistant/components/withings/coordinator.py index 35df34ab5a4..361a20acafd 100644 --- a/homeassistant/components/withings/coordinator.py +++ b/homeassistant/components/withings/coordinator.py @@ -9,6 +9,7 @@ from typing import TYPE_CHECKING from aiowithings import ( Activity, Goals, + MeasurementPosition, MeasurementType, NotificationCategory, SleepSummary, @@ -85,7 +86,9 @@ class WithingsDataUpdateCoordinator[_DataT](DataUpdateCoordinator[_DataT]): class WithingsMeasurementDataUpdateCoordinator( - WithingsDataUpdateCoordinator[dict[MeasurementType, float]] + WithingsDataUpdateCoordinator[ + dict[tuple[MeasurementType, MeasurementPosition | None], float] + ] ): """Withings measurement coordinator.""" @@ -98,9 +101,13 @@ class WithingsMeasurementDataUpdateCoordinator( NotificationCategory.WEIGHT, NotificationCategory.PRESSURE, } - self._previous_data: dict[MeasurementType, float] = {} + self._previous_data: dict[ + tuple[MeasurementType, MeasurementPosition | None], float + ] = {} - async def _internal_update_data(self) -> dict[MeasurementType, float]: + async def _internal_update_data( + self, + ) -> dict[tuple[MeasurementType, MeasurementPosition | None], float]: """Retrieve measurement data.""" if self._last_valid_update is None: now = dt_util.utcnow() diff --git a/homeassistant/components/withings/manifest.json b/homeassistant/components/withings/manifest.json index 36e34ffc187..4c97f43fd80 100644 --- a/homeassistant/components/withings/manifest.json +++ b/homeassistant/components/withings/manifest.json @@ -9,5 +9,5 @@ "iot_class": "cloud_push", "loggers": ["aiowithings"], "quality_scale": "platinum", - "requirements": ["aiowithings==2.1.0"] + "requirements": ["aiowithings==3.0.1"] } diff --git a/homeassistant/components/withings/sensor.py b/homeassistant/components/withings/sensor.py index 6d4d18bedd8..e205af7bdda 100644 --- a/homeassistant/components/withings/sensor.py +++ b/homeassistant/components/withings/sensor.py @@ -10,6 +10,7 @@ from typing import Any from aiowithings import ( Activity, Goals, + MeasurementPosition, MeasurementType, SleepSummary, Workout, @@ -63,12 +64,14 @@ class WithingsMeasurementSensorEntityDescription(SensorEntityDescription): """Immutable class for describing withings data.""" measurement_type: MeasurementType + measurement_position: MeasurementPosition | None = None MEASUREMENT_SENSORS: dict[ - MeasurementType, WithingsMeasurementSensorEntityDescription + tuple[MeasurementType, MeasurementPosition | None], + WithingsMeasurementSensorEntityDescription, ] = { - MeasurementType.WEIGHT: WithingsMeasurementSensorEntityDescription( + (MeasurementType.WEIGHT, None): WithingsMeasurementSensorEntityDescription( key="weight_kg", measurement_type=MeasurementType.WEIGHT, native_unit_of_measurement=UnitOfMass.KILOGRAMS, @@ -76,7 +79,7 @@ MEASUREMENT_SENSORS: dict[ device_class=SensorDeviceClass.WEIGHT, state_class=SensorStateClass.MEASUREMENT, ), - MeasurementType.FAT_MASS_WEIGHT: WithingsMeasurementSensorEntityDescription( + (MeasurementType.FAT_MASS_WEIGHT, None): WithingsMeasurementSensorEntityDescription( key="fat_mass_kg", measurement_type=MeasurementType.FAT_MASS_WEIGHT, translation_key="fat_mass", @@ -85,7 +88,7 @@ MEASUREMENT_SENSORS: dict[ device_class=SensorDeviceClass.WEIGHT, state_class=SensorStateClass.MEASUREMENT, ), - MeasurementType.FAT_FREE_MASS: WithingsMeasurementSensorEntityDescription( + (MeasurementType.FAT_FREE_MASS, None): WithingsMeasurementSensorEntityDescription( key="fat_free_mass_kg", measurement_type=MeasurementType.FAT_FREE_MASS, translation_key="fat_free_mass", @@ -94,7 +97,7 @@ MEASUREMENT_SENSORS: dict[ device_class=SensorDeviceClass.WEIGHT, state_class=SensorStateClass.MEASUREMENT, ), - MeasurementType.MUSCLE_MASS: WithingsMeasurementSensorEntityDescription( + (MeasurementType.MUSCLE_MASS, None): WithingsMeasurementSensorEntityDescription( key="muscle_mass_kg", measurement_type=MeasurementType.MUSCLE_MASS, translation_key="muscle_mass", @@ -103,7 +106,7 @@ MEASUREMENT_SENSORS: dict[ device_class=SensorDeviceClass.WEIGHT, state_class=SensorStateClass.MEASUREMENT, ), - MeasurementType.BONE_MASS: WithingsMeasurementSensorEntityDescription( + (MeasurementType.BONE_MASS, None): WithingsMeasurementSensorEntityDescription( key="bone_mass_kg", measurement_type=MeasurementType.BONE_MASS, translation_key="bone_mass", @@ -112,7 +115,7 @@ MEASUREMENT_SENSORS: dict[ device_class=SensorDeviceClass.WEIGHT, state_class=SensorStateClass.MEASUREMENT, ), - MeasurementType.HEIGHT: WithingsMeasurementSensorEntityDescription( + (MeasurementType.HEIGHT, None): WithingsMeasurementSensorEntityDescription( key="height_m", measurement_type=MeasurementType.HEIGHT, translation_key="height", @@ -122,14 +125,17 @@ MEASUREMENT_SENSORS: dict[ state_class=SensorStateClass.MEASUREMENT, entity_registry_enabled_default=False, ), - MeasurementType.TEMPERATURE: WithingsMeasurementSensorEntityDescription( + (MeasurementType.TEMPERATURE, None): WithingsMeasurementSensorEntityDescription( key="temperature_c", measurement_type=MeasurementType.TEMPERATURE, native_unit_of_measurement=UnitOfTemperature.CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), - MeasurementType.BODY_TEMPERATURE: WithingsMeasurementSensorEntityDescription( + ( + MeasurementType.BODY_TEMPERATURE, + None, + ): WithingsMeasurementSensorEntityDescription( key="body_temperature_c", measurement_type=MeasurementType.BODY_TEMPERATURE, translation_key="body_temperature", @@ -137,7 +143,10 @@ MEASUREMENT_SENSORS: dict[ device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), - MeasurementType.SKIN_TEMPERATURE: WithingsMeasurementSensorEntityDescription( + ( + MeasurementType.SKIN_TEMPERATURE, + None, + ): WithingsMeasurementSensorEntityDescription( key="skin_temperature_c", measurement_type=MeasurementType.SKIN_TEMPERATURE, translation_key="skin_temperature", @@ -145,7 +154,7 @@ MEASUREMENT_SENSORS: dict[ device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), - MeasurementType.FAT_RATIO: WithingsMeasurementSensorEntityDescription( + (MeasurementType.FAT_RATIO, None): WithingsMeasurementSensorEntityDescription( key="fat_ratio_pct", measurement_type=MeasurementType.FAT_RATIO, translation_key="fat_ratio", @@ -153,35 +162,41 @@ MEASUREMENT_SENSORS: dict[ suggested_display_precision=2, state_class=SensorStateClass.MEASUREMENT, ), - MeasurementType.DIASTOLIC_BLOOD_PRESSURE: WithingsMeasurementSensorEntityDescription( + ( + MeasurementType.DIASTOLIC_BLOOD_PRESSURE, + None, + ): WithingsMeasurementSensorEntityDescription( key="diastolic_blood_pressure_mmhg", measurement_type=MeasurementType.DIASTOLIC_BLOOD_PRESSURE, translation_key="diastolic_blood_pressure", native_unit_of_measurement=UOM_MMHG, state_class=SensorStateClass.MEASUREMENT, ), - MeasurementType.SYSTOLIC_BLOOD_PRESSURE: WithingsMeasurementSensorEntityDescription( + ( + MeasurementType.SYSTOLIC_BLOOD_PRESSURE, + None, + ): WithingsMeasurementSensorEntityDescription( key="systolic_blood_pressure_mmhg", measurement_type=MeasurementType.SYSTOLIC_BLOOD_PRESSURE, translation_key="systolic_blood_pressure", native_unit_of_measurement=UOM_MMHG, state_class=SensorStateClass.MEASUREMENT, ), - MeasurementType.HEART_RATE: WithingsMeasurementSensorEntityDescription( + (MeasurementType.HEART_RATE, None): WithingsMeasurementSensorEntityDescription( key="heart_pulse_bpm", measurement_type=MeasurementType.HEART_RATE, translation_key="heart_pulse", native_unit_of_measurement=UOM_BEATS_PER_MINUTE, state_class=SensorStateClass.MEASUREMENT, ), - MeasurementType.SP02: WithingsMeasurementSensorEntityDescription( + (MeasurementType.SP02, None): WithingsMeasurementSensorEntityDescription( key="spo2_pct", measurement_type=MeasurementType.SP02, translation_key="spo2", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, ), - MeasurementType.HYDRATION: WithingsMeasurementSensorEntityDescription( + (MeasurementType.HYDRATION, None): WithingsMeasurementSensorEntityDescription( key="hydration", measurement_type=MeasurementType.HYDRATION, translation_key="hydration", @@ -190,7 +205,10 @@ MEASUREMENT_SENSORS: dict[ state_class=SensorStateClass.MEASUREMENT, entity_registry_enabled_default=False, ), - MeasurementType.PULSE_WAVE_VELOCITY: WithingsMeasurementSensorEntityDescription( + ( + MeasurementType.PULSE_WAVE_VELOCITY, + None, + ): WithingsMeasurementSensorEntityDescription( key="pulse_wave_velocity", measurement_type=MeasurementType.PULSE_WAVE_VELOCITY, translation_key="pulse_wave_velocity", @@ -198,7 +216,7 @@ MEASUREMENT_SENSORS: dict[ device_class=SensorDeviceClass.SPEED, state_class=SensorStateClass.MEASUREMENT, ), - MeasurementType.VO2: WithingsMeasurementSensorEntityDescription( + (MeasurementType.VO2, None): WithingsMeasurementSensorEntityDescription( key="vo2_max", measurement_type=MeasurementType.VO2, translation_key="vo2_max", @@ -206,7 +224,10 @@ MEASUREMENT_SENSORS: dict[ state_class=SensorStateClass.MEASUREMENT, entity_registry_enabled_default=False, ), - MeasurementType.EXTRACELLULAR_WATER: WithingsMeasurementSensorEntityDescription( + ( + MeasurementType.EXTRACELLULAR_WATER, + None, + ): WithingsMeasurementSensorEntityDescription( key="extracellular_water", measurement_type=MeasurementType.EXTRACELLULAR_WATER, translation_key="extracellular_water", @@ -215,7 +236,10 @@ MEASUREMENT_SENSORS: dict[ state_class=SensorStateClass.MEASUREMENT, entity_registry_enabled_default=False, ), - MeasurementType.INTRACELLULAR_WATER: WithingsMeasurementSensorEntityDescription( + ( + MeasurementType.INTRACELLULAR_WATER, + None, + ): WithingsMeasurementSensorEntityDescription( key="intracellular_water", measurement_type=MeasurementType.INTRACELLULAR_WATER, translation_key="intracellular_water", @@ -224,33 +248,42 @@ MEASUREMENT_SENSORS: dict[ state_class=SensorStateClass.MEASUREMENT, entity_registry_enabled_default=False, ), - MeasurementType.VASCULAR_AGE: WithingsMeasurementSensorEntityDescription( + (MeasurementType.VASCULAR_AGE, None): WithingsMeasurementSensorEntityDescription( key="vascular_age", measurement_type=MeasurementType.VASCULAR_AGE, translation_key="vascular_age", entity_registry_enabled_default=False, ), - MeasurementType.VISCERAL_FAT: WithingsMeasurementSensorEntityDescription( + (MeasurementType.VISCERAL_FAT, None): WithingsMeasurementSensorEntityDescription( key="visceral_fat", measurement_type=MeasurementType.VISCERAL_FAT, translation_key="visceral_fat_index", entity_registry_enabled_default=False, ), - MeasurementType.ELECTRODERMAL_ACTIVITY_FEET: WithingsMeasurementSensorEntityDescription( + ( + MeasurementType.ELECTRODERMAL_ACTIVITY_FEET, + None, + ): WithingsMeasurementSensorEntityDescription( key="electrodermal_activity_feet", measurement_type=MeasurementType.ELECTRODERMAL_ACTIVITY_FEET, translation_key="electrodermal_activity_feet", native_unit_of_measurement=PERCENTAGE, entity_registry_enabled_default=False, ), - MeasurementType.ELECTRODERMAL_ACTIVITY_LEFT_FOOT: WithingsMeasurementSensorEntityDescription( + ( + MeasurementType.ELECTRODERMAL_ACTIVITY_LEFT_FOOT, + None, + ): WithingsMeasurementSensorEntityDescription( key="electrodermal_activity_left_foot", measurement_type=MeasurementType.ELECTRODERMAL_ACTIVITY_LEFT_FOOT, translation_key="electrodermal_activity_left_foot", native_unit_of_measurement=PERCENTAGE, entity_registry_enabled_default=False, ), - MeasurementType.ELECTRODERMAL_ACTIVITY_RIGHT_FOOT: WithingsMeasurementSensorEntityDescription( + ( + MeasurementType.ELECTRODERMAL_ACTIVITY_RIGHT_FOOT, + None, + ): WithingsMeasurementSensorEntityDescription( key="electrodermal_activity_right_foot", measurement_type=MeasurementType.ELECTRODERMAL_ACTIVITY_RIGHT_FOOT, translation_key="electrodermal_activity_right_foot", @@ -650,6 +683,7 @@ async def async_setup_entry( measurement_coordinator, MEASUREMENT_SENSORS[measurement_type] ) for measurement_type in new_measurement_types + if measurement_type in MEASUREMENT_SENSORS ) measurement_coordinator.async_add_listener(_async_measurement_listener) @@ -796,14 +830,23 @@ class WithingsMeasurementSensor( @property def native_value(self) -> float: """Return the state of the entity.""" - return self.coordinator.data[self.entity_description.measurement_type] + return self.coordinator.data[ + ( + self.entity_description.measurement_type, + self.entity_description.measurement_position, + ) + ] @property def available(self) -> bool: """Return if the sensor is available.""" return ( super().available - and self.entity_description.measurement_type in self.coordinator.data + and ( + self.entity_description.measurement_type, + self.entity_description.measurement_position, + ) + in self.coordinator.data ) diff --git a/requirements_all.txt b/requirements_all.txt index 479677849bc..1ade0104498 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -401,7 +401,7 @@ aiowatttime==0.1.1 aiowebostv==0.4.0 # homeassistant.components.withings -aiowithings==2.1.0 +aiowithings==3.0.1 # homeassistant.components.yandex_transport aioymaps==1.2.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 630d356bcaf..9c1d370b59b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -374,7 +374,7 @@ aiowatttime==0.1.1 aiowebostv==0.4.0 # homeassistant.components.withings -aiowithings==2.1.0 +aiowithings==3.0.1 # homeassistant.components.yandex_transport aioymaps==1.2.2 diff --git a/tests/components/withings/fixtures/measurements.json b/tests/components/withings/fixtures/measurements.json index 03222521877..31603d9a332 100644 --- a/tests/components/withings/fixtures/measurements.json +++ b/tests/components/withings/fixtures/measurements.json @@ -323,5 +323,476 @@ "modelid": 45, "model": "BPM Connect", "comment": null + }, + + { + "grpid": 5149666502, + "attrib": 0, + "date": 1560000000, + "created": 1560000000, + "modified": 1560000000, + "category": 1, + "deviceid": "51e8d0e7d16da9ff673accf10958ca94fea55d6e", + "hash_deviceid": "51e8d0e7d16da9ff673accf10958ca94fea55d6e", + "measures": [ + { + "value": 95854, + "type": 1, + "unit": -3, + "algo": 218235904, + "fm": 3 + }, + { + "value": 7718, + "type": 5, + "unit": -2, + "algo": 218235904, + "fm": 3, + "position": 7 + }, + { + "value": 7718, + "type": 5, + "unit": -2, + "algo": 218235904, + "fm": 3, + "position": 7 + }, + { + "value": 1866, + "type": 8, + "unit": -2, + "algo": 218235904, + "fm": 3, + "position": 7 + }, + { + "value": 1866, + "type": 8, + "unit": -2, + "algo": 218235904, + "fm": 3, + "position": 7 + }, + { + "value": 7338, + "type": 76, + "unit": -2, + "algo": 218235904, + "fm": 3, + "position": 7 + }, + { + "value": 5205, + "type": 77, + "unit": -2, + "algo": 218235904, + "fm": 3, + "position": 7 + }, + { + "value": 380, + "type": 88, + "unit": -2, + "algo": 218235904, + "fm": 3, + "position": 7 + }, + { + "value": 2162, + "type": 168, + "unit": -2, + "algo": 218235904, + "fm": 3, + "position": 7 + }, + { + "value": 3043, + "type": 169, + "unit": -2, + "algo": 218235904, + "fm": 3, + "position": 7 + }, + { + "value": 32, + "type": 170, + "unit": -1, + "algo": 218235904, + "fm": 3, + "position": 7 + }, + { + "value": 4000, + "type": 173, + "unit": -2, + "algo": 218235904, + "fm": 3, + "position": 12 + }, + { + "value": 1350, + "type": 173, + "unit": -2, + "algo": 218235904, + "fm": 3, + "position": 10 + }, + { + "value": 469, + "type": 173, + "unit": -2, + "algo": 218235904, + "fm": 3, + "position": 3 + }, + { + "value": 1406, + "type": 173, + "unit": -2, + "algo": 218235904, + "fm": 3, + "position": 11 + }, + { + "value": 491, + "type": 173, + "unit": -2, + "algo": 218235904, + "fm": 3, + "position": 2 + }, + { + "value": 1209, + "type": 174, + "unit": -2, + "algo": 218235904, + "fm": 3, + "position": 12 + }, + { + "value": 241, + "type": 174, + "unit": -2, + "algo": 218235904, + "fm": 3, + "position": 10 + }, + { + "value": 107, + "type": 174, + "unit": -2, + "algo": 218235904, + "fm": 3, + "position": 3 + }, + { + "value": 207, + "type": 174, + "unit": -2, + "algo": 218235904, + "fm": 3, + "position": 11 + }, + { + "value": 99, + "type": 174, + "unit": -2, + "algo": 218235904, + "fm": 3, + "position": 2 + }, + { + "value": 3823, + "type": 175, + "unit": -2, + "algo": 218235904, + "fm": 3, + "position": 12 + }, + { + "value": 1277, + "type": 175, + "unit": -2, + "algo": 218235904, + "fm": 3, + "position": 10 + }, + { + "value": 442, + "type": 175, + "unit": -2, + "algo": 218235904, + "fm": 3, + "position": 3 + }, + { + "value": 1330, + "type": 175, + "unit": -2, + "algo": 218235904, + "fm": 3, + "position": 11 + }, + { + "value": 463, + "type": 175, + "unit": -2, + "algo": 218235904, + "fm": 3, + "position": 2 + }, + { + "value": 2263, + "type": 226, + "unit": 0, + "algo": 218235904, + "fm": 3, + "position": 7 + }, + { + "value": 19467, + "type": 6, + "unit": -3 + } + ], + "modelid": 10, + "model": "Body Scan", + "comment": null + }, + { + "grpid": 5156052100, + "attrib": 0, + "date": 1560000000, + "created": 1560000000, + "modified": 1560000000, + "category": 1, + "deviceid": "51e8d0e7d16da9ff673accf10958ca94fea55d6e", + "hash_deviceid": "51e8d0e7d16da9ff673accf10958ca94fea55d6e", + "measures": [ + { + "value": 96440, + "type": 1, + "unit": -3, + "algo": 218235904, + "fm": 3 + }, + { + "value": 7863, + "type": 5, + "unit": -2, + "algo": 218235904, + "fm": 3, + "position": 7 + }, + { + "value": 7863, + "type": 5, + "unit": -2, + "algo": 218235904, + "fm": 3, + "position": 7 + }, + { + "value": 1780, + "type": 8, + "unit": -2, + "algo": 218235904, + "fm": 3, + "position": 7 + }, + { + "value": 1780, + "type": 8, + "unit": -2, + "algo": 218235904, + "fm": 3, + "position": 7 + }, + { + "value": 7475, + "type": 76, + "unit": -2, + "algo": 218235904, + "fm": 3, + "position": 7 + }, + { + "value": 5296, + "type": 77, + "unit": -2, + "algo": 218235904, + "fm": 3, + "position": 7 + }, + { + "value": 387, + "type": 88, + "unit": -2, + "algo": 218235904, + "fm": 3, + "position": 7 + }, + { + "value": 2175, + "type": 168, + "unit": -2, + "algo": 218235904, + "fm": 3, + "position": 7 + }, + { + "value": 3120, + "type": 169, + "unit": -2, + "algo": 218235904, + "fm": 3, + "position": 7 + }, + { + "value": 31, + "type": 170, + "unit": -1, + "algo": 218235904, + "fm": 3, + "position": 7 + }, + { + "value": 4049, + "type": 173, + "unit": -2, + "algo": 218235904, + "fm": 3, + "position": 12 + }, + { + "value": 1384, + "type": 173, + "unit": -2, + "algo": 218235904, + "fm": 3, + "position": 10 + }, + { + "value": 505, + "type": 173, + "unit": -2, + "algo": 218235904, + "fm": 3, + "position": 3 + }, + { + "value": 1405, + "type": 173, + "unit": -2, + "algo": 218235904, + "fm": 3, + "position": 11 + }, + { + "value": 518, + "type": 173, + "unit": -2, + "algo": 218235904, + "fm": 3, + "position": 2 + }, + { + "value": 1099, + "type": 174, + "unit": -2, + "algo": 218235904, + "fm": 3, + "position": 12 + }, + { + "value": 245, + "type": 174, + "unit": -2, + "algo": 218235904, + "fm": 3, + "position": 10 + }, + { + "value": 103, + "type": 174, + "unit": -2, + "algo": 218235904, + "fm": 3, + "position": 3 + }, + { + "value": 233, + "type": 174, + "unit": -2, + "algo": 218235904, + "fm": 3, + "position": 11 + }, + { + "value": 99, + "type": 174, + "unit": -2, + "algo": 218235904, + "fm": 3, + "position": 2 + }, + { + "value": 3870, + "type": 175, + "unit": -2, + "algo": 218235904, + "fm": 3, + "position": 12 + }, + { + "value": 1309, + "type": 175, + "unit": -2, + "algo": 218235904, + "fm": 3, + "position": 10 + }, + { + "value": 477, + "type": 175, + "unit": -2, + "algo": 218235904, + "fm": 3, + "position": 3 + }, + { + "value": 1329, + "type": 175, + "unit": -2, + "algo": 218235904, + "fm": 3, + "position": 11 + }, + { + "value": 489, + "type": 175, + "unit": -2, + "algo": 218235904, + "fm": 3, + "position": 2 + }, + { + "value": 2308, + "type": 226, + "unit": 0, + "algo": 218235904, + "fm": 3, + "position": 7 + }, + { + "value": 18457, + "type": 6, + "unit": -3 + } + ], + "modelid": 10, + "model": "Body Scan", + "comment": null } ] diff --git a/tests/components/withings/snapshots/test_diagnostics.ambr b/tests/components/withings/snapshots/test_diagnostics.ambr index 3dc7e824230..8ed8116f0c5 100644 --- a/tests/components/withings/snapshots/test_diagnostics.ambr +++ b/tests/components/withings/snapshots/test_diagnostics.ambr @@ -5,30 +5,166 @@ 'has_valid_external_webhook_url': True, 'received_activity_data': False, 'received_measurements': list([ - 1, - 8, - 5, - 76, - 88, - 4, - 12, - 71, - 73, - 6, - 9, - 10, - 11, - 54, - 77, - 91, - 123, - 155, - 168, - 169, - 198, - 197, - 196, - 170, + list([ + 1, + None, + ]), + list([ + 5, + None, + ]), + list([ + 8, + None, + ]), + list([ + 76, + None, + ]), + list([ + 77, + None, + ]), + list([ + 88, + None, + ]), + list([ + 168, + None, + ]), + list([ + 169, + None, + ]), + list([ + 170, + None, + ]), + list([ + 173, + 12, + ]), + list([ + 173, + 10, + ]), + list([ + 173, + 3, + ]), + list([ + 173, + 11, + ]), + list([ + 173, + 2, + ]), + list([ + 174, + 12, + ]), + list([ + 174, + 10, + ]), + list([ + 174, + 3, + ]), + list([ + 174, + 11, + ]), + list([ + 174, + 2, + ]), + list([ + 175, + 12, + ]), + list([ + 175, + 10, + ]), + list([ + 175, + 3, + ]), + list([ + 175, + 11, + ]), + list([ + 175, + 2, + ]), + list([ + 0, + None, + ]), + list([ + 6, + None, + ]), + list([ + 4, + None, + ]), + list([ + 12, + None, + ]), + list([ + 71, + None, + ]), + list([ + 73, + None, + ]), + list([ + 9, + None, + ]), + list([ + 10, + None, + ]), + list([ + 11, + None, + ]), + list([ + 54, + None, + ]), + list([ + 91, + None, + ]), + list([ + 123, + None, + ]), + list([ + 155, + None, + ]), + list([ + 198, + None, + ]), + list([ + 197, + None, + ]), + list([ + 196, + None, + ]), ]), 'received_sleep_data': True, 'received_workout_data': True, @@ -41,30 +177,166 @@ 'has_valid_external_webhook_url': False, 'received_activity_data': False, 'received_measurements': list([ - 1, - 8, - 5, - 76, - 88, - 4, - 12, - 71, - 73, - 6, - 9, - 10, - 11, - 54, - 77, - 91, - 123, - 155, - 168, - 169, - 198, - 197, - 196, - 170, + list([ + 1, + None, + ]), + list([ + 5, + None, + ]), + list([ + 8, + None, + ]), + list([ + 76, + None, + ]), + list([ + 77, + None, + ]), + list([ + 88, + None, + ]), + list([ + 168, + None, + ]), + list([ + 169, + None, + ]), + list([ + 170, + None, + ]), + list([ + 173, + 12, + ]), + list([ + 173, + 10, + ]), + list([ + 173, + 3, + ]), + list([ + 173, + 11, + ]), + list([ + 173, + 2, + ]), + list([ + 174, + 12, + ]), + list([ + 174, + 10, + ]), + list([ + 174, + 3, + ]), + list([ + 174, + 11, + ]), + list([ + 174, + 2, + ]), + list([ + 175, + 12, + ]), + list([ + 175, + 10, + ]), + list([ + 175, + 3, + ]), + list([ + 175, + 11, + ]), + list([ + 175, + 2, + ]), + list([ + 0, + None, + ]), + list([ + 6, + None, + ]), + list([ + 4, + None, + ]), + list([ + 12, + None, + ]), + list([ + 71, + None, + ]), + list([ + 73, + None, + ]), + list([ + 9, + None, + ]), + list([ + 10, + None, + ]), + list([ + 11, + None, + ]), + list([ + 54, + None, + ]), + list([ + 91, + None, + ]), + list([ + 123, + None, + ]), + list([ + 155, + None, + ]), + list([ + 198, + None, + ]), + list([ + 197, + None, + ]), + list([ + 196, + None, + ]), ]), 'received_sleep_data': True, 'received_workout_data': True, @@ -77,30 +349,166 @@ 'has_valid_external_webhook_url': True, 'received_activity_data': False, 'received_measurements': list([ - 1, - 8, - 5, - 76, - 88, - 4, - 12, - 71, - 73, - 6, - 9, - 10, - 11, - 54, - 77, - 91, - 123, - 155, - 168, - 169, - 198, - 197, - 196, - 170, + list([ + 1, + None, + ]), + list([ + 5, + None, + ]), + list([ + 8, + None, + ]), + list([ + 76, + None, + ]), + list([ + 77, + None, + ]), + list([ + 88, + None, + ]), + list([ + 168, + None, + ]), + list([ + 169, + None, + ]), + list([ + 170, + None, + ]), + list([ + 173, + 12, + ]), + list([ + 173, + 10, + ]), + list([ + 173, + 3, + ]), + list([ + 173, + 11, + ]), + list([ + 173, + 2, + ]), + list([ + 174, + 12, + ]), + list([ + 174, + 10, + ]), + list([ + 174, + 3, + ]), + list([ + 174, + 11, + ]), + list([ + 174, + 2, + ]), + list([ + 175, + 12, + ]), + list([ + 175, + 10, + ]), + list([ + 175, + 3, + ]), + list([ + 175, + 11, + ]), + list([ + 175, + 2, + ]), + list([ + 0, + None, + ]), + list([ + 6, + None, + ]), + list([ + 4, + None, + ]), + list([ + 12, + None, + ]), + list([ + 71, + None, + ]), + list([ + 73, + None, + ]), + list([ + 9, + None, + ]), + list([ + 10, + None, + ]), + list([ + 11, + None, + ]), + list([ + 54, + None, + ]), + list([ + 91, + None, + ]), + list([ + 123, + None, + ]), + list([ + 155, + None, + ]), + list([ + 198, + None, + ]), + list([ + 197, + None, + ]), + list([ + 196, + None, + ]), ]), 'received_sleep_data': True, 'received_workout_data': True, From a8becb124891bda726a6a4aa780a0ac921cde7f6 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 7 Jun 2024 12:15:03 +0200 Subject: [PATCH 0349/1445] Use fixtures in UniFi sensor tests (#118921) --- tests/components/unifi/test_sensor.py | 647 ++++++++++++++------------ 1 file changed, 343 insertions(+), 304 deletions(-) diff --git a/tests/components/unifi/test_sensor.py b/tests/components/unifi/test_sensor.py index 879de19bfe0..e59fe45181c 100644 --- a/tests/components/unifi/test_sensor.py +++ b/tests/components/unifi/test_sensor.py @@ -1,7 +1,10 @@ """UniFi Network sensor platform tests.""" +from collections.abc import Callable from copy import deepcopy from datetime import datetime, timedelta +from types import MappingProxyType +from typing import Any from unittest.mock import patch from aiounifi.models.device import DeviceState @@ -25,17 +28,14 @@ from homeassistant.components.unifi.const import ( DEFAULT_DETECTION_TIME, DEVICE_STATES, ) -from homeassistant.config_entries import RELOAD_AFTER_UPDATE_DELAY +from homeassistant.config_entries import RELOAD_AFTER_UPDATE_DELAY, ConfigEntry from homeassistant.const import ATTR_DEVICE_CLASS, STATE_UNAVAILABLE, EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity_registry import RegistryEntryDisabler import homeassistant.util.dt as dt_util -from .test_hub import setup_unifi_integration - from tests.common import async_fire_time_changed -from tests.test_util.aiohttp import AiohttpClientMocker DEVICE_1 = { "board_rev": 2, @@ -309,56 +309,58 @@ PDU_OUTLETS_UPDATE_DATA = [ ] -async def test_no_clients( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker -) -> None: +@pytest.mark.parametrize( + "config_entry_options", + [{CONF_ALLOW_BANDWIDTH_SENSORS: True, CONF_ALLOW_UPTIME_SENSORS: True}], +) +@pytest.mark.usefixtures("config_entry_setup") +async def test_no_clients(hass: HomeAssistant) -> None: """Test the update_clients function when no clients are found.""" - await setup_unifi_integration( - hass, - aioclient_mock, - options={ - CONF_ALLOW_BANDWIDTH_SENSORS: True, - CONF_ALLOW_UPTIME_SENSORS: True, - }, - ) - assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 0 +@pytest.mark.parametrize( + "config_entry_options", + [ + { + CONF_ALLOW_BANDWIDTH_SENSORS: True, + CONF_ALLOW_UPTIME_SENSORS: False, + CONF_TRACK_CLIENTS: False, + CONF_TRACK_DEVICES: False, + } + ], +) +@pytest.mark.parametrize( + "client_payload", + [ + [ + { + "hostname": "Wired client", + "is_wired": True, + "mac": "00:00:00:00:00:01", + "oui": "Producer", + "wired-rx_bytes-r": 1234000000, + "wired-tx_bytes-r": 5678000000, + }, + { + "is_wired": False, + "mac": "00:00:00:00:00:02", + "name": "Wireless client", + "oui": "Producer", + "rx_bytes-r": 2345000000.0, + "tx_bytes-r": 6789000000.0, + }, + ] + ], +) async def test_bandwidth_sensors( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, mock_unifi_websocket + hass: HomeAssistant, + mock_unifi_websocket, + config_entry_options: MappingProxyType[str, Any], + config_entry_setup: ConfigEntry, + client_payload: list[dict[str, Any]], ) -> None: """Verify that bandwidth sensors are working as expected.""" - wired_client = { - "hostname": "Wired client", - "is_wired": True, - "mac": "00:00:00:00:00:01", - "oui": "Producer", - "wired-rx_bytes-r": 1234000000, - "wired-tx_bytes-r": 5678000000, - } - wireless_client = { - "is_wired": False, - "mac": "00:00:00:00:00:02", - "name": "Wireless client", - "oui": "Producer", - "rx_bytes-r": 2345000000.0, - "tx_bytes-r": 6789000000.0, - } - options = { - CONF_ALLOW_BANDWIDTH_SENSORS: True, - CONF_ALLOW_UPTIME_SENSORS: False, - CONF_TRACK_CLIENTS: False, - CONF_TRACK_DEVICES: False, - } - - config_entry = await setup_unifi_integration( - hass, - aioclient_mock, - options=options, - clients_response=[wired_client, wireless_client], - ) - assert len(hass.states.async_all()) == 5 assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 4 @@ -385,7 +387,7 @@ async def test_bandwidth_sensors( assert wltx_sensor.state == "6789.0" # Verify state update - + wireless_client = client_payload[1] wireless_client["rx_bytes-r"] = 3456000000 wireless_client["tx_bytes-r"] = 7891000000 @@ -412,7 +414,8 @@ async def test_bandwidth_sensors( new_time += timedelta( seconds=( - config_entry.options.get(CONF_DETECTION_TIME, DEFAULT_DETECTION_TIME) + 1 + config_entry_setup.options.get(CONF_DETECTION_TIME, DEFAULT_DETECTION_TIME) + + 1 ) ) with freeze_time(new_time): @@ -423,9 +426,9 @@ async def test_bandwidth_sensors( assert hass.states.get("sensor.wireless_client_tx").state == STATE_UNAVAILABLE # Disable option - + options = deepcopy(config_entry_options) options[CONF_ALLOW_BANDWIDTH_SENSORS] = False - hass.config_entries.async_update_entry(config_entry, options=options.copy()) + hass.config_entries.async_update_entry(config_entry_setup, options=options) await hass.async_block_till_done() assert len(hass.states.async_all()) == 1 @@ -436,9 +439,9 @@ async def test_bandwidth_sensors( assert hass.states.get("sensor.wired_client_tx") is None # Enable option - + options = deepcopy(config_entry_options) options[CONF_ALLOW_BANDWIDTH_SENSORS] = True - hass.config_entries.async_update_entry(config_entry, options=options.copy()) + hass.config_entries.async_update_entry(config_entry_setup, options=options) await hass.async_block_till_done() assert len(hass.states.async_all()) == 5 @@ -449,6 +452,30 @@ async def test_bandwidth_sensors( assert hass.states.get("sensor.wired_client_tx") +@pytest.mark.parametrize( + "config_entry_options", + [ + { + CONF_ALLOW_BANDWIDTH_SENSORS: False, + CONF_ALLOW_UPTIME_SENSORS: True, + CONF_TRACK_CLIENTS: False, + CONF_TRACK_DEVICES: False, + } + ], +) +@pytest.mark.parametrize( + "client_payload", + [ + [ + { + "mac": "00:00:00:00:00:01", + "name": "client1", + "oui": "Producer", + "uptime": 0, + } + ] + ], +) @pytest.mark.parametrize( ("initial_uptime", "event_uptime", "new_uptime"), [ @@ -462,40 +489,23 @@ async def test_bandwidth_sensors( async def test_uptime_sensors( hass: HomeAssistant, entity_registry: er.EntityRegistry, - aioclient_mock: AiohttpClientMocker, freezer: FrozenDateTimeFactory, mock_unifi_websocket, + config_entry_options: MappingProxyType[str, Any], + config_entry_factory: Callable[[], ConfigEntry], + client_payload: list[dict[str, Any]], initial_uptime, event_uptime, new_uptime, ) -> None: """Verify that uptime sensors are working as expected.""" - uptime_client = { - "mac": "00:00:00:00:00:01", - "name": "client1", - "oui": "Producer", - "uptime": initial_uptime, - } - options = { - CONF_ALLOW_BANDWIDTH_SENSORS: False, - CONF_ALLOW_UPTIME_SENSORS: True, - CONF_TRACK_CLIENTS: False, - CONF_TRACK_DEVICES: False, - } + uptime_client = client_payload[0] + uptime_client["uptime"] = initial_uptime + freezer.move_to(datetime(2021, 1, 1, 1, 1, 0, tzinfo=dt_util.UTC)) + config_entry = await config_entry_factory() - now = datetime(2021, 1, 1, 1, 1, 0, tzinfo=dt_util.UTC) - freezer.move_to(now) - config_entry = await setup_unifi_integration( - hass, - aioclient_mock, - options=options, - clients_response=[uptime_client], - ) - - assert len(hass.states.async_all()) == 2 assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 1 assert hass.states.get("sensor.client1_uptime").state == "2021-01-01T01:00:00+00:00" - assert ( entity_registry.async_get("sensor.client1_uptime").entity_category is EntityCategory.DIAGNOSTIC @@ -503,7 +513,6 @@ async def test_uptime_sensors( # Verify normal new event doesn't change uptime # 4 seconds has passed - uptime_client["uptime"] = event_uptime now = datetime(2021, 1, 1, 1, 1, 4, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.now", return_value=now): @@ -514,7 +523,6 @@ async def test_uptime_sensors( # Verify new event change uptime # 1 month has passed - uptime_client["uptime"] = new_uptime now = datetime(2021, 2, 1, 1, 1, 0, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.now", return_value=now): @@ -524,63 +532,60 @@ async def test_uptime_sensors( assert hass.states.get("sensor.client1_uptime").state == "2021-02-01T01:00:00+00:00" # Disable option - + options = deepcopy(config_entry_options) options[CONF_ALLOW_UPTIME_SENSORS] = False - hass.config_entries.async_update_entry(config_entry, options=options.copy()) + hass.config_entries.async_update_entry(config_entry, options=options) await hass.async_block_till_done() - assert len(hass.states.async_all()) == 1 assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 0 assert hass.states.get("sensor.client1_uptime") is None # Enable option - + options = deepcopy(config_entry_options) options[CONF_ALLOW_UPTIME_SENSORS] = True with patch("homeassistant.util.dt.now", return_value=now): - hass.config_entries.async_update_entry(config_entry, options=options.copy()) + hass.config_entries.async_update_entry(config_entry, options=options) await hass.async_block_till_done() - assert len(hass.states.async_all()) == 2 assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 1 assert hass.states.get("sensor.client1_uptime") +@pytest.mark.parametrize( + "config_entry_options", + [{CONF_ALLOW_BANDWIDTH_SENSORS: True, CONF_ALLOW_UPTIME_SENSORS: True}], +) +@pytest.mark.parametrize( + "client_payload", + [ + [ + { + "hostname": "Wired client", + "is_wired": True, + "mac": "00:00:00:00:00:01", + "oui": "Producer", + "wired-rx_bytes": 1234000000, + "wired-tx_bytes": 5678000000, + "uptime": 1600094505, + }, + { + "is_wired": False, + "mac": "00:00:00:00:00:02", + "name": "Wireless client", + "oui": "Producer", + "rx_bytes": 2345000000, + "tx_bytes": 6789000000, + "uptime": 60, + }, + ] + ], +) +@pytest.mark.usefixtures("config_entry_setup") @pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_remove_sensors( - hass: HomeAssistant, - aioclient_mock: AiohttpClientMocker, - mock_unifi_websocket, + hass: HomeAssistant, mock_unifi_websocket, client_payload: list[dict[str, Any]] ) -> None: """Verify removing of clients work as expected.""" - wired_client = { - "hostname": "Wired client", - "is_wired": True, - "mac": "00:00:00:00:00:01", - "oui": "Producer", - "wired-rx_bytes": 1234000000, - "wired-tx_bytes": 5678000000, - "uptime": 1600094505, - } - wireless_client = { - "is_wired": False, - "mac": "00:00:00:00:00:02", - "name": "Wireless client", - "oui": "Producer", - "rx_bytes": 2345000000, - "tx_bytes": 6789000000, - "uptime": 60, - } - - await setup_unifi_integration( - hass, - aioclient_mock, - options={ - CONF_ALLOW_BANDWIDTH_SENSORS: True, - CONF_ALLOW_UPTIME_SENSORS: True, - }, - clients_response=[wired_client, wireless_client], - ) - assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 6 assert hass.states.get("sensor.wired_client_rx") assert hass.states.get("sensor.wired_client_tx") @@ -590,8 +595,7 @@ async def test_remove_sensors( assert hass.states.get("sensor.wireless_client_uptime") # Remove wired client - - mock_unifi_websocket(message=MessageKey.CLIENT_REMOVED, data=wired_client) + mock_unifi_websocket(message=MessageKey.CLIENT_REMOVED, data=client_payload[0]) await hass.async_block_till_done() assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 3 @@ -603,15 +607,15 @@ async def test_remove_sensors( assert hass.states.get("sensor.wireless_client_uptime") +@pytest.mark.parametrize("device_payload", [[DEVICE_1]]) +@pytest.mark.usefixtures("config_entry_setup") async def test_poe_port_switches( hass: HomeAssistant, entity_registry: er.EntityRegistry, - aioclient_mock: AiohttpClientMocker, mock_unifi_websocket, websocket_mock, ) -> None: """Test the update_items function with some clients.""" - await setup_unifi_integration(hass, aioclient_mock, devices_response=[DEVICE_1]) assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 2 ent_reg_entry = entity_registry.async_get("sensor.mock_name_port_1_poe_power") @@ -678,42 +682,43 @@ async def test_poe_port_switches( assert hass.states.get("sensor.mock_name_port_1_poe_power") +@pytest.mark.parametrize( + "client_payload", + [ + [ + { + "essid": "SSID 1", + "is_wired": False, + "last_seen": dt_util.as_timestamp(dt_util.utcnow()), + "mac": "00:00:00:00:00:01", + "name": "Wireless client", + "oui": "Producer", + "rx_bytes-r": 2345000000, + "tx_bytes-r": 6789000000, + }, + { + "essid": "SSID 2", + "is_wired": False, + "last_seen": dt_util.as_timestamp(dt_util.utcnow()), + "mac": "00:00:00:00:00:02", + "name": "Wireless client2", + "oui": "Producer2", + "rx_bytes-r": 2345000000, + "tx_bytes-r": 6789000000, + }, + ] + ], +) +@pytest.mark.parametrize("wlan_payload", [[WLAN]]) +@pytest.mark.usefixtures("config_entry_setup") async def test_wlan_client_sensors( hass: HomeAssistant, entity_registry: er.EntityRegistry, - aioclient_mock: AiohttpClientMocker, mock_unifi_websocket, websocket_mock, + client_payload: list[dict[str, Any]], ) -> None: """Verify that WLAN client sensors are working as expected.""" - wireless_client_1 = { - "essid": "SSID 1", - "is_wired": False, - "last_seen": dt_util.as_timestamp(dt_util.utcnow()), - "mac": "00:00:00:00:00:01", - "name": "Wireless client", - "oui": "Producer", - "rx_bytes-r": 2345000000, - "tx_bytes-r": 6789000000, - } - wireless_client_2 = { - "essid": "SSID 2", - "is_wired": False, - "last_seen": dt_util.as_timestamp(dt_util.utcnow()), - "mac": "00:00:00:00:00:02", - "name": "Wireless client2", - "oui": "Producer2", - "rx_bytes-r": 2345000000, - "tx_bytes-r": 6789000000, - } - - await setup_unifi_integration( - hass, - aioclient_mock, - clients_response=[wireless_client_1, wireless_client_2], - wlans_response=[WLAN], - ) - assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 1 ent_reg_entry = entity_registry.async_get("sensor.ssid_1") @@ -726,11 +731,11 @@ async def test_wlan_client_sensors( assert ssid_1.state == "1" # Verify state update - increasing number - + wireless_client_1 = client_payload[0] wireless_client_1["essid"] = "SSID 1" - wireless_client_2["essid"] = "SSID 1" - mock_unifi_websocket(message=MessageKey.CLIENT, data=wireless_client_1) + wireless_client_2 = client_payload[1] + wireless_client_2["essid"] = "SSID 1" mock_unifi_websocket(message=MessageKey.CLIENT, data=wireless_client_2) await hass.async_block_till_done() @@ -821,11 +826,13 @@ async def test_wlan_client_sensors( ), ], ) +@pytest.mark.parametrize("device_payload", [[PDU_DEVICE_1]]) +@pytest.mark.usefixtures("config_entry_setup") async def test_outlet_power_readings( hass: HomeAssistant, entity_registry: er.EntityRegistry, - aioclient_mock: AiohttpClientMocker, mock_unifi_websocket, + device_payload: list[dict[str, Any]], entity_id: str, expected_unique_id: str, expected_value: any, @@ -833,8 +840,6 @@ async def test_outlet_power_readings( expected_update_value: any, ) -> None: """Test the outlet power reporting on PDU devices.""" - await setup_unifi_integration(hass, aioclient_mock, devices_response=[PDU_DEVICE_1]) - assert len(hass.states.async_all()) == 13 assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 7 @@ -847,7 +852,7 @@ async def test_outlet_power_readings( assert sensor_data.state == expected_value if changed_data is not None: - updated_device_data = deepcopy(PDU_DEVICE_1) + updated_device_data = deepcopy(device_payload[0]) updated_device_data.update(changed_data) mock_unifi_websocket(message=MessageKey.DEVICE, data=updated_device_data) @@ -857,35 +862,42 @@ async def test_outlet_power_readings( assert sensor_data.state == expected_update_value +@pytest.mark.parametrize( + "device_payload", + [ + [ + { + "board_rev": 3, + "device_id": "mock-id", + "has_fan": True, + "fan_level": 0, + "ip": "10.0.1.1", + "last_seen": 1562600145, + "mac": "00:00:00:00:01:01", + "model": "US16P150", + "name": "Device", + "next_interval": 20, + "overheating": True, + "state": 1, + "type": "usw", + "upgradable": True, + "uptime": 60, + "version": "4.0.42.10433", + } + ] + ], +) async def test_device_uptime( hass: HomeAssistant, entity_registry: er.EntityRegistry, - aioclient_mock: AiohttpClientMocker, mock_unifi_websocket, + config_entry_factory: Callable[[], ConfigEntry], + device_payload: list[dict[str, Any]], ) -> None: """Verify that uptime sensors are working as expected.""" - device = { - "board_rev": 3, - "device_id": "mock-id", - "has_fan": True, - "fan_level": 0, - "ip": "10.0.1.1", - "last_seen": 1562600145, - "mac": "00:00:00:00:01:01", - "model": "US16P150", - "name": "Device", - "next_interval": 20, - "overheating": True, - "state": 1, - "type": "usw", - "upgradable": True, - "uptime": 60, - "version": "4.0.42.10433", - } - now = datetime(2021, 1, 1, 1, 1, 0, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.now", return_value=now): - await setup_unifi_integration(hass, aioclient_mock, devices_response=[device]) + await config_entry_factory() assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 2 assert hass.states.get("sensor.device_uptime").state == "2021-01-01T01:00:00+00:00" @@ -896,7 +908,7 @@ async def test_device_uptime( # Verify normal new event doesn't change uptime # 4 seconds has passed - + device = device_payload[0] device["uptime"] = 64 now = datetime(2021, 1, 1, 1, 1, 4, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.now", return_value=now): @@ -915,111 +927,128 @@ async def test_device_uptime( assert hass.states.get("sensor.device_uptime").state == "2021-02-01T01:00:00+00:00" +@pytest.mark.parametrize( + "device_payload", + [ + [ + { + "board_rev": 3, + "device_id": "mock-id", + "general_temperature": 30, + "has_fan": True, + "has_temperature": True, + "fan_level": 0, + "ip": "10.0.1.1", + "last_seen": 1562600145, + "mac": "00:00:00:00:01:01", + "model": "US16P150", + "name": "Device", + "next_interval": 20, + "overheating": True, + "state": 1, + "type": "usw", + "upgradable": True, + "uptime": 60, + "version": "4.0.42.10433", + } + ] + ], +) +@pytest.mark.usefixtures("config_entry_setup") async def test_device_temperature( hass: HomeAssistant, entity_registry: er.EntityRegistry, - aioclient_mock: AiohttpClientMocker, mock_unifi_websocket, + device_payload: list[dict[str, Any]], ) -> None: """Verify that temperature sensors are working as expected.""" - device = { - "board_rev": 3, - "device_id": "mock-id", - "general_temperature": 30, - "has_fan": True, - "has_temperature": True, - "fan_level": 0, - "ip": "10.0.1.1", - "last_seen": 1562600145, - "mac": "00:00:00:00:01:01", - "model": "US16P150", - "name": "Device", - "next_interval": 20, - "overheating": True, - "state": 1, - "type": "usw", - "upgradable": True, - "uptime": 60, - "version": "4.0.42.10433", - } - - await setup_unifi_integration(hass, aioclient_mock, devices_response=[device]) assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 3 assert hass.states.get("sensor.device_temperature").state == "30" - assert ( entity_registry.async_get("sensor.device_temperature").entity_category is EntityCategory.DIAGNOSTIC ) # Verify new event change temperature + device = device_payload[0] device["general_temperature"] = 60 mock_unifi_websocket(message=MessageKey.DEVICE, data=device) assert hass.states.get("sensor.device_temperature").state == "60" +@pytest.mark.parametrize( + "device_payload", + [ + [ + { + "board_rev": 3, + "device_id": "mock-id", + "general_temperature": 30, + "has_fan": True, + "has_temperature": True, + "fan_level": 0, + "ip": "10.0.1.1", + "last_seen": 1562600145, + "mac": "00:00:00:00:01:01", + "model": "US16P150", + "name": "Device", + "next_interval": 20, + "overheating": True, + "state": 1, + "type": "usw", + "upgradable": True, + "uptime": 60, + "version": "4.0.42.10433", + } + ] + ], +) +@pytest.mark.usefixtures("config_entry_setup") async def test_device_state( hass: HomeAssistant, entity_registry: er.EntityRegistry, - aioclient_mock: AiohttpClientMocker, mock_unifi_websocket, + device_payload: list[dict[str, Any]], ) -> None: """Verify that state sensors are working as expected.""" - device = { - "board_rev": 3, - "device_id": "mock-id", - "general_temperature": 30, - "has_fan": True, - "has_temperature": True, - "fan_level": 0, - "ip": "10.0.1.1", - "last_seen": 1562600145, - "mac": "00:00:00:00:01:01", - "model": "US16P150", - "name": "Device", - "next_interval": 20, - "overheating": True, - "state": 1, - "type": "usw", - "upgradable": True, - "uptime": 60, - "version": "4.0.42.10433", - } - - await setup_unifi_integration(hass, aioclient_mock, devices_response=[device]) assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 3 - assert ( entity_registry.async_get("sensor.device_state").entity_category is EntityCategory.DIAGNOSTIC ) + device = device_payload[0] for i in list(map(int, DeviceState)): device["state"] = i mock_unifi_websocket(message=MessageKey.DEVICE, data=device) assert hass.states.get("sensor.device_state").state == DEVICE_STATES[i] +@pytest.mark.parametrize( + "device_payload", + [ + [ + { + "device_id": "mock-id", + "mac": "00:00:00:00:01:01", + "model": "US16P150", + "name": "Device", + "state": 1, + "version": "4.0.42.10433", + "system-stats": {"cpu": 5.8, "mem": 31.1, "uptime": 7316}, + } + ] + ], +) +@pytest.mark.usefixtures("config_entry_setup") async def test_device_system_stats( hass: HomeAssistant, entity_registry: er.EntityRegistry, - aioclient_mock: AiohttpClientMocker, mock_unifi_websocket, + device_payload: list[dict[str, Any]], ) -> None: """Verify that device stats sensors are working as expected.""" - device = { - "device_id": "mock-id", - "mac": "00:00:00:00:01:01", - "model": "US16P150", - "name": "Device", - "state": 1, - "version": "4.0.42.10433", - "system-stats": {"cpu": 5.8, "mem": 31.1, "uptime": 7316}, - } - - await setup_unifi_integration(hass, aioclient_mock, devices_response=[device]) - assert len(hass.states.async_all()) == 8 assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 4 @@ -1037,6 +1066,7 @@ async def test_device_system_stats( ) # Verify new event change system-stats + device = device_payload[0] device["system-stats"] = {"cpu": 7.7, "mem": 33.3, "uptime": 7316} mock_unifi_websocket(message=MessageKey.DEVICE, data=device) @@ -1044,72 +1074,79 @@ async def test_device_system_stats( assert hass.states.get("sensor.device_memory_utilization").state == "33.3" +@pytest.mark.parametrize( + "config_entry_options", + [ + { + CONF_ALLOW_BANDWIDTH_SENSORS: True, + CONF_ALLOW_UPTIME_SENSORS: False, + CONF_TRACK_CLIENTS: False, + CONF_TRACK_DEVICES: False, + } + ], +) +@pytest.mark.parametrize( + "device_payload", + [ + [ + { + "board_rev": 2, + "device_id": "mock-id", + "ip": "10.0.1.1", + "mac": "10:00:00:00:01:01", + "last_seen": 1562600145, + "model": "US16P150", + "name": "mock-name", + "port_overrides": [], + "port_table": [ + { + "media": "GE", + "name": "Port 1", + "port_idx": 1, + "poe_class": "Class 4", + "poe_enable": False, + "poe_mode": "auto", + "poe_power": "2.56", + "poe_voltage": "53.40", + "portconf_id": "1a1", + "port_poe": False, + "up": True, + "rx_bytes-r": 1151, + "tx_bytes-r": 5111, + }, + { + "media": "GE", + "name": "Port 2", + "port_idx": 2, + "poe_class": "Class 4", + "poe_enable": False, + "poe_mode": "auto", + "poe_power": "2.56", + "poe_voltage": "53.40", + "portconf_id": "1a2", + "port_poe": False, + "up": True, + "rx_bytes-r": 1536, + "tx_bytes-r": 3615, + }, + ], + "state": 1, + "type": "usw", + "version": "4.0.42.10433", + } + ] + ], +) +@pytest.mark.usefixtures("config_entry_setup") async def test_bandwidth_port_sensors( hass: HomeAssistant, entity_registry: er.EntityRegistry, - aioclient_mock: AiohttpClientMocker, mock_unifi_websocket, + config_entry_setup: ConfigEntry, + config_entry_options: MappingProxyType[str, Any], + device_payload, ) -> None: """Verify that port bandwidth sensors are working as expected.""" - device_reponse = { - "board_rev": 2, - "device_id": "mock-id", - "ip": "10.0.1.1", - "mac": "10:00:00:00:01:01", - "last_seen": 1562600145, - "model": "US16P150", - "name": "mock-name", - "port_overrides": [], - "port_table": [ - { - "media": "GE", - "name": "Port 1", - "port_idx": 1, - "poe_class": "Class 4", - "poe_enable": False, - "poe_mode": "auto", - "poe_power": "2.56", - "poe_voltage": "53.40", - "portconf_id": "1a1", - "port_poe": False, - "up": True, - "rx_bytes-r": 1151, - "tx_bytes-r": 5111, - }, - { - "media": "GE", - "name": "Port 2", - "port_idx": 2, - "poe_class": "Class 4", - "poe_enable": False, - "poe_mode": "auto", - "poe_power": "2.56", - "poe_voltage": "53.40", - "portconf_id": "1a2", - "port_poe": False, - "up": True, - "rx_bytes-r": 1536, - "tx_bytes-r": 3615, - }, - ], - "state": 1, - "type": "usw", - "version": "4.0.42.10433", - } - options = { - CONF_ALLOW_BANDWIDTH_SENSORS: True, - CONF_ALLOW_UPTIME_SENSORS: False, - CONF_TRACK_CLIENTS: False, - CONF_TRACK_DEVICES: False, - } - - config_entry = await setup_unifi_integration( - hass, - aioclient_mock, - options=options, - devices_response=[device_reponse], - ) - assert len(hass.states.async_all()) == 5 assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 2 @@ -1168,18 +1205,20 @@ async def test_bandwidth_port_sensors( assert p2tx_sensor.state == "0.02892" # Verify state update - device_reponse["port_table"][0]["rx_bytes-r"] = 3456000000 - device_reponse["port_table"][0]["tx_bytes-r"] = 7891000000 + device_1 = device_payload[0] + device_1["port_table"][0]["rx_bytes-r"] = 3456000000 + device_1["port_table"][0]["tx_bytes-r"] = 7891000000 - mock_unifi_websocket(message=MessageKey.DEVICE, data=device_reponse) + mock_unifi_websocket(message=MessageKey.DEVICE, data=device_1) await hass.async_block_till_done() assert hass.states.get("sensor.mock_name_port_1_rx").state == "27648.00000" assert hass.states.get("sensor.mock_name_port_1_tx").state == "63128.00000" # Disable option + options = config_entry_options.copy() options[CONF_ALLOW_BANDWIDTH_SENSORS] = False - hass.config_entries.async_update_entry(config_entry, options=options.copy()) + hass.config_entries.async_update_entry(config_entry_setup, options=options) await hass.async_block_till_done() assert len(hass.states.async_all()) == 5 From 92ed20ffbfe024fd8c4449cb973cdce09cbe91f6 Mon Sep 17 00:00:00 2001 From: Greg Dowling Date: Fri, 7 Jun 2024 11:25:14 +0100 Subject: [PATCH 0350/1445] Add mute_toggle to roon volume events (#114171) Add mute_toggle event. --- homeassistant/components/roon/event.py | 16 +++++++++------- homeassistant/components/roon/strings.json | 3 ++- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/roon/event.py b/homeassistant/components/roon/event.py index 073b58160f6..7bc6ea27dd9 100644 --- a/homeassistant/components/roon/event.py +++ b/homeassistant/components/roon/event.py @@ -47,7 +47,7 @@ class RoonEventEntity(EventEntity): """Representation of a Roon Event entity.""" _attr_device_class = EventDeviceClass.BUTTON - _attr_event_types = ["volume_up", "volume_down"] + _attr_event_types = ["volume_up", "volume_down", "mute_toggle"] _attr_translation_key = "volume" def __init__(self, server, player_data): @@ -77,15 +77,17 @@ class RoonEventEntity(EventEntity): ) -> None: """Callbacks from the roon api with volume request.""" - if event != "set_volume": + if event == "set_mute": + event = "mute_toggle" + elif event == "set_volume": + if value > 0: + event = "volume_up" + else: + event = "volume_down" + else: _LOGGER.debug("Received unsupported roon volume event %s", event) return - if value > 0: - event = "volume_up" - else: - event = "volume_down" - self._trigger_event(event) self.schedule_update_ha_state() diff --git a/homeassistant/components/roon/strings.json b/homeassistant/components/roon/strings.json index a95c6908312..853bcc6c585 100644 --- a/homeassistant/components/roon/strings.json +++ b/homeassistant/components/roon/strings.json @@ -29,7 +29,8 @@ "event_type": { "state": { "volume_up": "Volume up", - "volume_down": "Volume down" + "volume_down": "Volume down", + "mute_toggle": "Mute toggle" } } } From 81ee5fb46baaba18cc36d78eed91c6310e3945cc Mon Sep 17 00:00:00 2001 From: Yuxin Wang Date: Fri, 7 Jun 2024 06:28:59 -0400 Subject: [PATCH 0351/1445] Refine sensor descriptions for APCUPSD (#114137) * Refine sensor descriptions for APCUPSD * Add device class for cumonbatt * Add UoM to STESTI and TIMELEFT * Remove device class for STESTI --- homeassistant/components/apcupsd/sensor.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/homeassistant/components/apcupsd/sensor.py b/homeassistant/components/apcupsd/sensor.py index 6ac33072856..8d2c1ee2af1 100644 --- a/homeassistant/components/apcupsd/sensor.py +++ b/homeassistant/components/apcupsd/sensor.py @@ -87,7 +87,9 @@ SENSORS: dict[str, SensorEntityDescription] = { "cumonbatt": SensorEntityDescription( key="cumonbatt", translation_key="total_time_on_battery", + native_unit_of_measurement=UnitOfTime.SECONDS, state_class=SensorStateClass.TOTAL_INCREASING, + device_class=SensorDeviceClass.DURATION, ), "date": SensorEntityDescription( key="date", @@ -340,12 +342,16 @@ SENSORS: dict[str, SensorEntityDescription] = { "timeleft": SensorEntityDescription( key="timeleft", translation_key="time_left", + native_unit_of_measurement=UnitOfTime.MINUTES, state_class=SensorStateClass.MEASUREMENT, + device_class=SensorDeviceClass.DURATION, ), "tonbatt": SensorEntityDescription( key="tonbatt", translation_key="time_on_battery", + native_unit_of_measurement=UnitOfTime.SECONDS, state_class=SensorStateClass.TOTAL_INCREASING, + device_class=SensorDeviceClass.DURATION, ), "upsmode": SensorEntityDescription( key="upsmode", From 549f66fd6f3982f19bc78f348e167a6a579752d6 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 7 Jun 2024 12:30:02 +0200 Subject: [PATCH 0352/1445] Move mock_async_zeroconf to decorator in homekit tests (#119060) --- tests/components/homekit/conftest.py | 4 +- tests/components/homekit/test_config_flow.py | 16 +- tests/components/homekit/test_diagnostics.py | 10 +- tests/components/homekit/test_homekit.py | 156 +++++++++---------- tests/components/homekit/test_init.py | 2 +- 5 files changed, 90 insertions(+), 98 deletions(-) diff --git a/tests/components/homekit/conftest.py b/tests/components/homekit/conftest.py index 19676538261..26333b0b807 100644 --- a/tests/components/homekit/conftest.py +++ b/tests/components/homekit/conftest.py @@ -5,7 +5,7 @@ from collections.abc import Generator from contextlib import suppress import os from typing import Any -from unittest.mock import patch +from unittest.mock import MagicMock, patch import pytest @@ -88,7 +88,7 @@ def mock_hap( hass: HomeAssistant, event_loop: AbstractEventLoop, iid_storage: AccessoryIIDStorage, - mock_zeroconf: None, + mock_zeroconf: MagicMock, ) -> Generator[HomeDriver, Any, None]: """Return a custom AccessoryDriver instance for HomeKit accessory init.""" with ( diff --git a/tests/components/homekit/test_config_flow.py b/tests/components/homekit/test_config_flow.py index 23f15bb344a..d6d0c7118db 100644 --- a/tests/components/homekit/test_config_flow.py +++ b/tests/components/homekit/test_config_flow.py @@ -15,7 +15,7 @@ from homeassistant.config_entries import SOURCE_IGNORE, SOURCE_IMPORT from homeassistant.const import CONF_NAME, CONF_PORT, EntityCategory from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType -from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entityfilter import CONF_INCLUDE_DOMAINS from homeassistant.setup import async_setup_component @@ -405,13 +405,12 @@ async def test_options_flow_exclude_mode_basic(hass: HomeAssistant) -> None: @patch(f"{PATH_HOMEKIT}.async_port_is_available", return_value=True) +@pytest.mark.usefixtures("mock_async_zeroconf") async def test_options_flow_devices( port_mock, hass: HomeAssistant, demo_cleanup, - device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - mock_async_zeroconf: None, ) -> None: """Test devices can be bridged.""" config_entry = MockConfigEntry( @@ -502,8 +501,9 @@ async def test_options_flow_devices( @patch(f"{PATH_HOMEKIT}.async_port_is_available", return_value=True) +@pytest.mark.usefixtures("mock_async_zeroconf") async def test_options_flow_devices_preserved_when_advanced_off( - port_mock, hass: HomeAssistant, mock_async_zeroconf: None + port_mock, hass: HomeAssistant ) -> None: """Test devices are preserved if they were added in advanced mode but it was turned off.""" config_entry = MockConfigEntry( @@ -1161,11 +1161,11 @@ async def test_options_flow_blocked_when_from_yaml(hass: HomeAssistant) -> None: @patch(f"{PATH_HOMEKIT}.async_port_is_available", return_value=True) +@pytest.mark.usefixtures("mock_async_zeroconf") async def test_options_flow_include_mode_basic_accessory( port_mock, hass: HomeAssistant, hk_driver, - mock_async_zeroconf: None, ) -> None: """Test config flow options in include mode with a single accessory.""" config_entry = _mock_config_entry_with_options_populated() @@ -1387,11 +1387,11 @@ def _get_schema_default(schema, key_name): @patch(f"{PATH_HOMEKIT}.async_port_is_available", return_value=True) +@pytest.mark.usefixtures("mock_async_zeroconf") async def test_options_flow_exclude_mode_skips_category_entities( port_mock, hass: HomeAssistant, hk_driver, - mock_async_zeroconf: None, entity_registry: er.EntityRegistry, ) -> None: """Ensure exclude mode does not offer category entities.""" @@ -1491,11 +1491,11 @@ async def test_options_flow_exclude_mode_skips_category_entities( @patch(f"{PATH_HOMEKIT}.async_port_is_available", return_value=True) +@pytest.mark.usefixtures("mock_async_zeroconf") async def test_options_flow_exclude_mode_skips_hidden_entities( port_mock, hass: HomeAssistant, hk_driver, - mock_async_zeroconf: None, entity_registry: er.EntityRegistry, ) -> None: """Ensure exclude mode does not offer hidden entities.""" @@ -1575,11 +1575,11 @@ async def test_options_flow_exclude_mode_skips_hidden_entities( @patch(f"{PATH_HOMEKIT}.async_port_is_available", return_value=True) +@pytest.mark.usefixtures("mock_async_zeroconf") async def test_options_flow_include_mode_allows_hidden_entities( port_mock, hass: HomeAssistant, hk_driver, - mock_async_zeroconf: None, entity_registry: er.EntityRegistry, ) -> None: """Ensure include mode does not offer hidden entities.""" diff --git a/tests/components/homekit/test_diagnostics.py b/tests/components/homekit/test_diagnostics.py index 9fe4fc6fcc7..728624da0d0 100644 --- a/tests/components/homekit/test_diagnostics.py +++ b/tests/components/homekit/test_diagnostics.py @@ -2,6 +2,8 @@ from unittest.mock import ANY, MagicMock, patch +import pytest + from homeassistant.components.homekit.const import ( CONF_DEVICES, CONF_HOMEKIT_MODE, @@ -20,11 +22,11 @@ from tests.components.diagnostics import get_diagnostics_for_config_entry from tests.typing import ClientSessionGenerator +@pytest.mark.usefixtures("mock_async_zeroconf") async def test_config_entry_not_running( hass: HomeAssistant, hass_client: ClientSessionGenerator, hk_driver, - mock_async_zeroconf: None, ) -> None: """Test generating diagnostics for a config entry.""" entry = await async_init_integration(hass) @@ -40,11 +42,11 @@ async def test_config_entry_not_running( } +@pytest.mark.usefixtures("mock_async_zeroconf") async def test_config_entry_running( hass: HomeAssistant, hass_client: ClientSessionGenerator, hk_driver, - mock_async_zeroconf: None, ) -> None: """Test generating diagnostics for a bridge config entry.""" entry = MockConfigEntry( @@ -152,11 +154,11 @@ async def test_config_entry_running( await hass.async_block_till_done() +@pytest.mark.usefixtures("mock_async_zeroconf") async def test_config_entry_accessory( hass: HomeAssistant, hass_client: ClientSessionGenerator, hk_driver, - mock_async_zeroconf: None, ) -> None: """Test generating diagnostics for an accessory config entry.""" hass.states.async_set("light.demo", "on") @@ -314,11 +316,11 @@ async def test_config_entry_accessory( await hass.async_block_till_done() +@pytest.mark.usefixtures("mock_async_zeroconf") async def test_config_entry_with_trigger_accessory( hass: HomeAssistant, hass_client: ClientSessionGenerator, hk_driver, - mock_async_zeroconf: None, events, demo_cleanup, device_registry: dr.DeviceRegistry, diff --git a/tests/components/homekit/test_homekit.py b/tests/components/homekit/test_homekit.py index 55698db9b2d..33bfc6e66d3 100644 --- a/tests/components/homekit/test_homekit.py +++ b/tests/components/homekit/test_homekit.py @@ -154,7 +154,8 @@ def _mock_pyhap_bridge(): ) -async def test_setup_min(hass: HomeAssistant, mock_async_zeroconf: None) -> None: +@pytest.mark.usefixtures("mock_async_zeroconf") +async def test_setup_min(hass: HomeAssistant) -> None: """Test async_setup with min config options.""" entry = MockConfigEntry( @@ -198,9 +199,8 @@ async def test_setup_min(hass: HomeAssistant, mock_async_zeroconf: None) -> None @patch(f"{PATH_HOMEKIT}.async_port_is_available", return_value=True) -async def test_removing_entry( - port_mock, hass: HomeAssistant, mock_async_zeroconf: None -) -> None: +@pytest.mark.usefixtures("mock_async_zeroconf") +async def test_removing_entry(port_mock, hass: HomeAssistant) -> None: """Test removing a config entry.""" entry = MockConfigEntry( @@ -246,9 +246,8 @@ async def test_removing_entry( await hass.async_block_till_done() -async def test_homekit_setup( - hass: HomeAssistant, hk_driver, mock_async_zeroconf: None -) -> None: +@pytest.mark.usefixtures("mock_async_zeroconf") +async def test_homekit_setup(hass: HomeAssistant, hk_driver) -> None: """Test setup of bridge and driver.""" entry = MockConfigEntry( domain=DOMAIN, @@ -415,9 +414,8 @@ async def test_homekit_with_many_advertise_ips( ) -async def test_homekit_setup_advertise_ips( - hass: HomeAssistant, hk_driver, mock_async_zeroconf: None -) -> None: +@pytest.mark.usefixtures("mock_async_zeroconf") +async def test_homekit_setup_advertise_ips(hass: HomeAssistant, hk_driver) -> None: """Test setup with given IP address to advertise.""" entry = MockConfigEntry( domain=DOMAIN, @@ -461,9 +459,8 @@ async def test_homekit_setup_advertise_ips( ) -async def test_homekit_add_accessory( - hass: HomeAssistant, mock_async_zeroconf: None, mock_hap -) -> None: +@pytest.mark.usefixtures("mock_async_zeroconf") +async def test_homekit_add_accessory(hass: HomeAssistant, mock_hap) -> None: """Add accessory if config exists and get_acc returns an accessory.""" entry = MockConfigEntry( @@ -501,10 +498,10 @@ async def test_homekit_add_accessory( @pytest.mark.parametrize("acc_category", [CATEGORY_TELEVISION, CATEGORY_CAMERA]) +@pytest.mark.usefixtures("mock_async_zeroconf") async def test_homekit_warn_add_accessory_bridge( hass: HomeAssistant, acc_category, - mock_async_zeroconf: None, mock_hap, caplog: pytest.LogCaptureFixture, ) -> None: @@ -535,9 +532,8 @@ async def test_homekit_warn_add_accessory_bridge( assert "accessory mode" in caplog.text -async def test_homekit_remove_accessory( - hass: HomeAssistant, mock_async_zeroconf: None -) -> None: +@pytest.mark.usefixtures("mock_async_zeroconf") +async def test_homekit_remove_accessory(hass: HomeAssistant) -> None: """Remove accessory from bridge.""" entry = await async_init_integration(hass) @@ -554,9 +550,8 @@ async def test_homekit_remove_accessory( assert len(homekit.bridge.accessories) == 0 -async def test_homekit_entity_filter( - hass: HomeAssistant, mock_async_zeroconf: None -) -> None: +@pytest.mark.usefixtures("mock_async_zeroconf") +async def test_homekit_entity_filter(hass: HomeAssistant) -> None: """Test the entity filter.""" entry = await async_init_integration(hass) @@ -575,9 +570,8 @@ async def test_homekit_entity_filter( assert hass.states.get("light.demo") not in filtered_states -async def test_homekit_entity_glob_filter( - hass: HomeAssistant, mock_async_zeroconf: None -) -> None: +@pytest.mark.usefixtures("mock_async_zeroconf") +async def test_homekit_entity_glob_filter(hass: HomeAssistant) -> None: """Test the entity filter.""" entry = await async_init_integration(hass) @@ -601,8 +595,9 @@ async def test_homekit_entity_glob_filter( assert hass.states.get("light.included_test") in filtered_states +@pytest.mark.usefixtures("mock_async_zeroconf") async def test_homekit_entity_glob_filter_with_config_entities( - hass: HomeAssistant, mock_async_zeroconf: None, entity_registry: er.EntityRegistry + hass: HomeAssistant, entity_registry: er.EntityRegistry ) -> None: """Test the entity filter with configuration entities.""" entry = await async_init_integration(hass) @@ -654,8 +649,9 @@ async def test_homekit_entity_glob_filter_with_config_entities( assert hass.states.get("select.keep") in filtered_states +@pytest.mark.usefixtures("mock_async_zeroconf") async def test_homekit_entity_glob_filter_with_hidden_entities( - hass: HomeAssistant, mock_async_zeroconf: None, entity_registry: er.EntityRegistry + hass: HomeAssistant, entity_registry: er.EntityRegistry ) -> None: """Test the entity filter with hidden entities.""" entry = await async_init_integration(hass) @@ -707,10 +703,10 @@ async def test_homekit_entity_glob_filter_with_hidden_entities( assert hass.states.get("select.keep") in filtered_states +@pytest.mark.usefixtures("mock_async_zeroconf") async def test_homekit_start( hass: HomeAssistant, hk_driver, - mock_async_zeroconf: None, device_registry: dr.DeviceRegistry, ) -> None: """Test HomeKit start method.""" @@ -794,8 +790,9 @@ async def test_homekit_start( assert homekit.driver.state.config_version == 1 +@pytest.mark.usefixtures("mock_async_zeroconf") async def test_homekit_start_with_a_broken_accessory( - hass: HomeAssistant, hk_driver, mock_async_zeroconf: None + hass: HomeAssistant, hk_driver ) -> None: """Test HomeKit start method.""" entry = MockConfigEntry( @@ -835,10 +832,10 @@ async def test_homekit_start_with_a_broken_accessory( assert not hk_driver_start.called +@pytest.mark.usefixtures("mock_async_zeroconf") async def test_homekit_start_with_a_device( hass: HomeAssistant, hk_driver, - mock_async_zeroconf: None, demo_cleanup, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, @@ -908,9 +905,8 @@ async def test_homekit_stop(hass: HomeAssistant) -> None: assert homekit.driver.async_stop.called is True -async def test_homekit_reset_accessories( - hass: HomeAssistant, mock_async_zeroconf: None, mock_hap -) -> None: +@pytest.mark.usefixtures("mock_async_zeroconf") +async def test_homekit_reset_accessories(hass: HomeAssistant, mock_hap) -> None: """Test resetting HomeKit accessories.""" entry = MockConfigEntry( @@ -946,8 +942,9 @@ async def test_homekit_reset_accessories( await homekit.async_stop() +@pytest.mark.usefixtures("mock_async_zeroconf") async def test_homekit_reload_accessory_can_change_class( - hass: HomeAssistant, mock_async_zeroconf: None, mock_hap + hass: HomeAssistant, mock_hap ) -> None: """Test reloading a HomeKit Accessory in brdige mode. @@ -981,8 +978,9 @@ async def test_homekit_reload_accessory_can_change_class( await homekit.async_stop() +@pytest.mark.usefixtures("mock_async_zeroconf") async def test_homekit_reload_accessory_in_accessory_mode( - hass: HomeAssistant, mock_async_zeroconf: None, mock_hap + hass: HomeAssistant, mock_hap ) -> None: """Test reloading a HomeKit Accessory in accessory mode. @@ -1016,8 +1014,9 @@ async def test_homekit_reload_accessory_in_accessory_mode( await homekit.async_stop() +@pytest.mark.usefixtures("mock_async_zeroconf") async def test_homekit_reload_accessory_same_class( - hass: HomeAssistant, mock_async_zeroconf: None, mock_hap + hass: HomeAssistant, mock_hap ) -> None: """Test reloading a HomeKit Accessory in bridge mode. @@ -1060,8 +1059,9 @@ async def test_homekit_reload_accessory_same_class( await homekit.async_stop() +@pytest.mark.usefixtures("mock_async_zeroconf") async def test_homekit_unpair( - hass: HomeAssistant, device_registry: dr.DeviceRegistry, mock_async_zeroconf: None + hass: HomeAssistant, device_registry: dr.DeviceRegistry ) -> None: """Test unpairing HomeKit accessories.""" @@ -1110,9 +1110,8 @@ async def test_homekit_unpair( homekit.status = STATUS_STOPPED -async def test_homekit_unpair_missing_device_id( - hass: HomeAssistant, device_registry: dr.DeviceRegistry, mock_async_zeroconf: None -) -> None: +@pytest.mark.usefixtures("mock_async_zeroconf") +async def test_homekit_unpair_missing_device_id(hass: HomeAssistant) -> None: """Test unpairing HomeKit accessories with invalid device id.""" entry = MockConfigEntry( @@ -1152,8 +1151,9 @@ async def test_homekit_unpair_missing_device_id( homekit.status = STATUS_STOPPED +@pytest.mark.usefixtures("mock_async_zeroconf") async def test_homekit_unpair_not_homekit_device( - hass: HomeAssistant, device_registry: dr.DeviceRegistry, mock_async_zeroconf: None + hass: HomeAssistant, device_registry: dr.DeviceRegistry ) -> None: """Test unpairing HomeKit accessories with a non-homekit device id.""" @@ -1205,9 +1205,8 @@ async def test_homekit_unpair_not_homekit_device( homekit.status = STATUS_STOPPED -async def test_homekit_reset_accessories_not_supported( - hass: HomeAssistant, mock_async_zeroconf: None -) -> None: +@pytest.mark.usefixtures("mock_async_zeroconf") +async def test_homekit_reset_accessories_not_supported(hass: HomeAssistant) -> None: """Test resetting HomeKit accessories with an unsupported entity.""" entry = MockConfigEntry( @@ -1251,9 +1250,8 @@ async def test_homekit_reset_accessories_not_supported( homekit.status = STATUS_STOPPED -async def test_homekit_reset_accessories_state_missing( - hass: HomeAssistant, mock_async_zeroconf: None -) -> None: +@pytest.mark.usefixtures("mock_async_zeroconf") +async def test_homekit_reset_accessories_state_missing(hass: HomeAssistant) -> None: """Test resetting HomeKit accessories when the state goes missing.""" entry = MockConfigEntry( @@ -1295,9 +1293,8 @@ async def test_homekit_reset_accessories_state_missing( homekit.status = STATUS_STOPPED -async def test_homekit_reset_accessories_not_bridged( - hass: HomeAssistant, mock_async_zeroconf: None -) -> None: +@pytest.mark.usefixtures("mock_async_zeroconf") +async def test_homekit_reset_accessories_not_bridged(hass: HomeAssistant) -> None: """Test resetting HomeKit accessories when the state is not bridged.""" entry = MockConfigEntry( @@ -1342,9 +1339,8 @@ async def test_homekit_reset_accessories_not_bridged( homekit.status = STATUS_STOPPED -async def test_homekit_reset_single_accessory( - hass: HomeAssistant, mock_hap, mock_async_zeroconf: None -) -> None: +@pytest.mark.usefixtures("mock_async_zeroconf") +async def test_homekit_reset_single_accessory(hass: HomeAssistant, mock_hap) -> None: """Test resetting HomeKit single accessory.""" entry = MockConfigEntry( @@ -1381,9 +1377,8 @@ async def test_homekit_reset_single_accessory( await homekit.async_stop() -async def test_homekit_reset_single_accessory_unsupported( - hass: HomeAssistant, mock_async_zeroconf: None -) -> None: +@pytest.mark.usefixtures("mock_async_zeroconf") +async def test_homekit_reset_single_accessory_unsupported(hass: HomeAssistant) -> None: """Test resetting HomeKit single accessory with an unsupported entity.""" entry = MockConfigEntry( @@ -1422,8 +1417,9 @@ async def test_homekit_reset_single_accessory_unsupported( homekit.status = STATUS_STOPPED +@pytest.mark.usefixtures("mock_async_zeroconf") async def test_homekit_reset_single_accessory_state_missing( - hass: HomeAssistant, mock_async_zeroconf: None + hass: HomeAssistant, ) -> None: """Test resetting HomeKit single accessory when the state goes missing.""" @@ -1462,9 +1458,8 @@ async def test_homekit_reset_single_accessory_state_missing( homekit.status = STATUS_STOPPED -async def test_homekit_reset_single_accessory_no_match( - hass: HomeAssistant, mock_async_zeroconf: None -) -> None: +@pytest.mark.usefixtures("mock_async_zeroconf") +async def test_homekit_reset_single_accessory_no_match(hass: HomeAssistant) -> None: """Test resetting HomeKit single accessory when the entity id does not match.""" entry = MockConfigEntry( @@ -1502,11 +1497,11 @@ async def test_homekit_reset_single_accessory_no_match( homekit.status = STATUS_STOPPED +@pytest.mark.usefixtures("mock_async_zeroconf") async def test_homekit_too_many_accessories( hass: HomeAssistant, hk_driver, caplog: pytest.LogCaptureFixture, - mock_async_zeroconf: None, ) -> None: """Test adding too many accessories to HomeKit.""" entry = await async_init_integration(hass) @@ -1538,12 +1533,12 @@ async def test_homekit_too_many_accessories( assert "would exceed" in caplog.text +@pytest.mark.usefixtures("mock_async_zeroconf") async def test_homekit_finds_linked_batteries( hass: HomeAssistant, hk_driver, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - mock_async_zeroconf: None, ) -> None: """Test HomeKit start method.""" entry = await async_init_integration(hass) @@ -1617,12 +1612,12 @@ async def test_homekit_finds_linked_batteries( ) +@pytest.mark.usefixtures("mock_async_zeroconf") async def test_homekit_async_get_integration_fails( hass: HomeAssistant, hk_driver, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - mock_async_zeroconf: None, ) -> None: """Test that we continue if async_get_integration fails.""" entry = await async_init_integration(hass) @@ -1692,9 +1687,8 @@ async def test_homekit_async_get_integration_fails( ) -async def test_yaml_updates_update_config_entry_for_name( - hass: HomeAssistant, mock_async_zeroconf: None -) -> None: +@pytest.mark.usefixtures("mock_async_zeroconf") +async def test_yaml_updates_update_config_entry_for_name(hass: HomeAssistant) -> None: """Test async_setup with imported config.""" entry = MockConfigEntry( @@ -1742,9 +1736,8 @@ async def test_yaml_updates_update_config_entry_for_name( mock_homekit().async_start.assert_called() -async def test_yaml_can_link_with_default_name( - hass: HomeAssistant, mock_async_zeroconf: None -) -> None: +@pytest.mark.usefixtures("mock_async_zeroconf") +async def test_yaml_can_link_with_default_name(hass: HomeAssistant) -> None: """Test async_setup with imported config linked by default name.""" entry = MockConfigEntry( domain=DOMAIN, @@ -1776,9 +1769,8 @@ async def test_yaml_can_link_with_default_name( assert entry.options["entity_config"]["camera.back_camera"]["stream_count"] == 3 -async def test_yaml_can_link_with_port( - hass: HomeAssistant, mock_async_zeroconf: None -) -> None: +@pytest.mark.usefixtures("mock_async_zeroconf") +async def test_yaml_can_link_with_port(hass: HomeAssistant) -> None: """Test async_setup with imported config linked by port.""" entry = MockConfigEntry( domain=DOMAIN, @@ -1830,9 +1822,8 @@ async def test_yaml_can_link_with_port( assert entry3.options == {} -async def test_homekit_uses_system_zeroconf( - hass: HomeAssistant, hk_driver, mock_async_zeroconf: None -) -> None: +@pytest.mark.usefixtures("mock_async_zeroconf") +async def test_homekit_uses_system_zeroconf(hass: HomeAssistant, hk_driver) -> None: """Test HomeKit uses system zeroconf.""" entry = MockConfigEntry( domain=DOMAIN, @@ -1856,12 +1847,12 @@ async def test_homekit_uses_system_zeroconf( await hass.async_block_till_done() +@pytest.mark.usefixtures("mock_async_zeroconf") async def test_homekit_ignored_missing_devices( hass: HomeAssistant, hk_driver, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - mock_async_zeroconf: None, ) -> None: """Test HomeKit handles a device in the entity registry but missing from the device registry.""" @@ -1947,12 +1938,12 @@ async def test_homekit_ignored_missing_devices( ) +@pytest.mark.usefixtures("mock_async_zeroconf") async def test_homekit_finds_linked_motion_sensors( hass: HomeAssistant, hk_driver, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - mock_async_zeroconf: None, ) -> None: """Test HomeKit start method.""" entry = await async_init_integration(hass) @@ -2014,12 +2005,12 @@ async def test_homekit_finds_linked_motion_sensors( ) +@pytest.mark.usefixtures("mock_async_zeroconf") async def test_homekit_finds_linked_humidity_sensors( hass: HomeAssistant, hk_driver, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - mock_async_zeroconf: None, ) -> None: """Test HomeKit start method.""" entry = await async_init_integration(hass) @@ -2084,7 +2075,8 @@ async def test_homekit_finds_linked_humidity_sensors( ) -async def test_reload(hass: HomeAssistant, mock_async_zeroconf: None) -> None: +@pytest.mark.usefixtures("mock_async_zeroconf") +async def test_reload(hass: HomeAssistant) -> None: """Test we can reload from yaml.""" entry = MockConfigEntry( @@ -2166,10 +2158,10 @@ async def test_reload(hass: HomeAssistant, mock_async_zeroconf: None) -> None: ) +@pytest.mark.usefixtures("mock_async_zeroconf") async def test_homekit_start_in_accessory_mode( hass: HomeAssistant, hk_driver, - mock_async_zeroconf: None, device_registry: dr.DeviceRegistry, ) -> None: """Test HomeKit start method in accessory mode.""" @@ -2210,11 +2202,10 @@ async def test_homekit_start_in_accessory_mode( assert len(device_registry.devices) == 1 +@pytest.mark.usefixtures("mock_async_zeroconf") async def test_homekit_start_in_accessory_mode_unsupported_entity( hass: HomeAssistant, hk_driver, - mock_async_zeroconf: None, - device_registry: dr.DeviceRegistry, caplog: pytest.LogCaptureFixture, ) -> None: """Test HomeKit start method in accessory mode with an unsupported entity.""" @@ -2244,11 +2235,10 @@ async def test_homekit_start_in_accessory_mode_unsupported_entity( assert "entity not supported" in caplog.text +@pytest.mark.usefixtures("mock_async_zeroconf") async def test_homekit_start_in_accessory_mode_missing_entity( hass: HomeAssistant, hk_driver, - mock_async_zeroconf: None, - device_registry: dr.DeviceRegistry, caplog: pytest.LogCaptureFixture, ) -> None: """Test HomeKit start method in accessory mode when entity is not available.""" @@ -2275,10 +2265,10 @@ async def test_homekit_start_in_accessory_mode_missing_entity( assert "entity not available" in caplog.text +@pytest.mark.usefixtures("mock_async_zeroconf") async def test_wait_for_port_to_free( hass: HomeAssistant, hk_driver, - mock_async_zeroconf: None, caplog: pytest.LogCaptureFixture, ) -> None: """Test we wait for the port to free before declaring unload success.""" diff --git a/tests/components/homekit/test_init.py b/tests/components/homekit/test_init.py index 2b251c7858d..fdf599f41ea 100644 --- a/tests/components/homekit/test_init.py +++ b/tests/components/homekit/test_init.py @@ -70,10 +70,10 @@ async def test_humanify_homekit_changed_event(hass: HomeAssistant, hk_driver) -> assert event2["entity_id"] == "cover.window" +@pytest.mark.usefixtures("mock_async_zeroconf") async def test_bridge_with_triggers( hass: HomeAssistant, hk_driver, - mock_async_zeroconf: None, entity_registry: er.EntityRegistry, caplog: pytest.LogCaptureFixture, ) -> None: From 59c8270b1a03046252e6c9f0dbcebc29d2d4eebd Mon Sep 17 00:00:00 2001 From: Dos Moonen Date: Fri, 7 Jun 2024 15:16:07 +0200 Subject: [PATCH 0353/1445] Bump solax from 3.1.0 to 3.1.1 (#118888) --- homeassistant/components/solax/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/solax/manifest.json b/homeassistant/components/solax/manifest.json index be81dd65e89..2ca246a4e77 100644 --- a/homeassistant/components/solax/manifest.json +++ b/homeassistant/components/solax/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/solax", "iot_class": "local_polling", "loggers": ["solax"], - "requirements": ["solax==3.1.0"] + "requirements": ["solax==3.1.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 1ade0104498..75baa58a008 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2599,7 +2599,7 @@ soco==0.30.4 solaredge-local==0.2.3 # homeassistant.components.solax -solax==3.1.0 +solax==3.1.1 # homeassistant.components.somfy_mylink somfy-mylink-synergy==1.0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9c1d370b59b..6657e58adb1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2018,7 +2018,7 @@ snapcast==2.3.6 soco==0.30.4 # homeassistant.components.solax -solax==3.1.0 +solax==3.1.1 # homeassistant.components.somfy_mylink somfy-mylink-synergy==1.0.6 From 5bf42e64e30c95c3ef3b4705b4ec6ee942398ce7 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 7 Jun 2024 15:33:43 +0200 Subject: [PATCH 0354/1445] Improve type hints in arcam_fmj tests (#119072) --- tests/components/arcam_fmj/conftest.py | 16 ++++++++++------ tests/components/arcam_fmj/test_config_flow.py | 13 ++++++++----- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/tests/components/arcam_fmj/conftest.py b/tests/components/arcam_fmj/conftest.py index f5a9ab6315a..66850933cc7 100644 --- a/tests/components/arcam_fmj/conftest.py +++ b/tests/components/arcam_fmj/conftest.py @@ -5,10 +5,12 @@ from unittest.mock import Mock, patch from arcam.fmj.client import Client from arcam.fmj.state import State import pytest +from typing_extensions import AsyncGenerator from homeassistant.components.arcam_fmj.const import DEFAULT_NAME from homeassistant.components.arcam_fmj.media_player import ArcamFmj from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry, MockEntityPlatform @@ -27,7 +29,7 @@ MOCK_CONFIG_ENTRY = {CONF_HOST: MOCK_HOST, CONF_PORT: MOCK_PORT} @pytest.fixture(name="client") -def client_fixture(): +def client_fixture() -> Mock: """Get a mocked client.""" client = Mock(Client) client.host = MOCK_HOST @@ -36,7 +38,7 @@ def client_fixture(): @pytest.fixture(name="state_1") -def state_1_fixture(client): +def state_1_fixture(client: Mock) -> State: """Get a mocked state.""" state = Mock(State) state.client = client @@ -51,7 +53,7 @@ def state_1_fixture(client): @pytest.fixture(name="state_2") -def state_2_fixture(client): +def state_2_fixture(client: Mock) -> State: """Get a mocked state.""" state = Mock(State) state.client = client @@ -66,13 +68,13 @@ def state_2_fixture(client): @pytest.fixture(name="state") -def state_fixture(state_1): +def state_fixture(state_1: State) -> State: """Get a mocked state.""" return state_1 @pytest.fixture(name="player") -def player_fixture(hass, state): +def player_fixture(hass: HomeAssistant, state: State) -> ArcamFmj: """Get standard player.""" player = ArcamFmj(MOCK_NAME, state, MOCK_UUID) player.entity_id = MOCK_ENTITY_ID @@ -83,7 +85,9 @@ def player_fixture(hass, state): @pytest.fixture(name="player_setup") -async def player_setup_fixture(hass, state_1, state_2, client): +async def player_setup_fixture( + hass: HomeAssistant, state_1: State, state_2: State, client: Mock +) -> AsyncGenerator[str]: """Get standard player.""" config_entry = MockConfigEntry( domain="arcam_fmj", data=MOCK_CONFIG_ENTRY, title=MOCK_NAME diff --git a/tests/components/arcam_fmj/test_config_flow.py b/tests/components/arcam_fmj/test_config_flow.py index 65991c313ee..26e93354900 100644 --- a/tests/components/arcam_fmj/test_config_flow.py +++ b/tests/components/arcam_fmj/test_config_flow.py @@ -1,10 +1,11 @@ """Tests for the Arcam FMJ config flow module.""" from dataclasses import replace -from unittest.mock import AsyncMock, patch +from unittest.mock import AsyncMock, MagicMock, patch from arcam.fmj.client import ConnectionFailed import pytest +from typing_extensions import Generator from homeassistant.components import ssdp from homeassistant.components.arcam_fmj.config_flow import get_entry_client @@ -53,7 +54,7 @@ MOCK_DISCOVER = ssdp.SsdpServiceInfo( @pytest.fixture(name="dummy_client", autouse=True) -def dummy_client_fixture(hass): +def dummy_client_fixture() -> Generator[MagicMock]: """Mock out the real client.""" with patch("homeassistant.components.arcam_fmj.config_flow.Client") as client: client.return_value.start.side_effect = AsyncMock(return_value=None) @@ -61,7 +62,7 @@ def dummy_client_fixture(hass): yield client.return_value -async def test_ssdp(hass: HomeAssistant, dummy_client) -> None: +async def test_ssdp(hass: HomeAssistant) -> None: """Test a ssdp import flow.""" result = await hass.config_entries.flow.async_init( DOMAIN, @@ -93,7 +94,9 @@ async def test_ssdp_abort(hass: HomeAssistant) -> None: assert result["reason"] == "already_configured" -async def test_ssdp_unable_to_connect(hass: HomeAssistant, dummy_client) -> None: +async def test_ssdp_unable_to_connect( + hass: HomeAssistant, dummy_client: MagicMock +) -> None: """Test a ssdp import flow.""" dummy_client.start.side_effect = AsyncMock(side_effect=ConnectionFailed) @@ -110,7 +113,7 @@ async def test_ssdp_unable_to_connect(hass: HomeAssistant, dummy_client) -> None assert result["reason"] == "cannot_connect" -async def test_ssdp_invalid_id(hass: HomeAssistant, dummy_client) -> None: +async def test_ssdp_invalid_id(hass: HomeAssistant) -> None: """Test a ssdp with invalid UDN.""" discover = replace( MOCK_DISCOVER, upnp=MOCK_DISCOVER.upnp | {ssdp.ATTR_UPNP_UDN: "invalid"} From 37b0e8fa335e0f013a35d88dcde9a3c394b586be Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 7 Jun 2024 15:35:33 +0200 Subject: [PATCH 0355/1445] Improve type hints in airvisual test fixtures (#119079) --- tests/components/airvisual/conftest.py | 41 ++++++++++++++-------- tests/components/airvisual_pro/conftest.py | 30 ++++++++++------ 2 files changed, 45 insertions(+), 26 deletions(-) diff --git a/tests/components/airvisual/conftest.py b/tests/components/airvisual/conftest.py index 90e13e2f4be..a82dc0ab78c 100644 --- a/tests/components/airvisual/conftest.py +++ b/tests/components/airvisual/conftest.py @@ -1,10 +1,10 @@ """Define test fixtures for AirVisual.""" -import json +from typing import Any from unittest.mock import AsyncMock, Mock, patch import pytest -from typing_extensions import Generator +from typing_extensions import AsyncGenerator, Generator from homeassistant.components.airvisual import ( CONF_CITY, @@ -21,8 +21,10 @@ from homeassistant.const import ( CONF_SHOW_ON_MAP, CONF_STATE, ) +from homeassistant.core import HomeAssistant +from homeassistant.util.json import JsonObjectType -from tests.common import MockConfigEntry, load_fixture +from tests.common import MockConfigEntry, load_json_object_fixture TEST_API_KEY = "abcde12345" TEST_LATITUDE = 51.528308 @@ -55,7 +57,7 @@ NAME_CONFIG = { @pytest.fixture(name="cloud_api") -def cloud_api_fixture(data_cloud): +def cloud_api_fixture(data_cloud: JsonObjectType) -> Mock: """Define a mock CloudAPI object.""" return Mock( air_quality=Mock( @@ -66,7 +68,12 @@ def cloud_api_fixture(data_cloud): @pytest.fixture(name="config_entry") -def config_entry_fixture(hass, config, config_entry_version, integration_type): +def config_entry_fixture( + hass: HomeAssistant, + config: dict[str, Any], + config_entry_version: int, + integration_type: str, +) -> MockConfigEntry: """Define a config entry fixture.""" entry = MockConfigEntry( domain=DOMAIN, @@ -81,37 +88,39 @@ def config_entry_fixture(hass, config, config_entry_version, integration_type): @pytest.fixture(name="config_entry_version") -def config_entry_version_fixture(): +def config_entry_version_fixture() -> int: """Define a config entry version fixture.""" return 2 @pytest.fixture(name="config") -def config_fixture(): +def config_fixture() -> dict[str, Any]: """Define a config entry data fixture.""" return COORDS_CONFIG @pytest.fixture(name="data_cloud", scope="package") -def data_cloud_fixture(): +def data_cloud_fixture() -> JsonObjectType: """Define an update coordinator data example.""" - return json.loads(load_fixture("data.json", "airvisual")) + return load_json_object_fixture("data.json", "airvisual") @pytest.fixture(name="data_pro", scope="package") -def data_pro_fixture(): +def data_pro_fixture() -> JsonObjectType: """Define an update coordinator data example for the Pro.""" - return json.loads(load_fixture("data.json", "airvisual_pro")) + return load_json_object_fixture("data.json", "airvisual_pro") @pytest.fixture(name="integration_type") -def integration_type_fixture(): +def integration_type_fixture() -> str: """Define an integration type.""" return INTEGRATION_TYPE_GEOGRAPHY_COORDS @pytest.fixture(name="mock_pyairvisual") -async def mock_pyairvisual_fixture(cloud_api, node_samba): +async def mock_pyairvisual_fixture( + cloud_api: Mock, node_samba: Mock +) -> AsyncGenerator[None]: """Define a fixture to patch pyairvisual.""" with ( patch( @@ -135,7 +144,7 @@ async def mock_pyairvisual_fixture(cloud_api, node_samba): @pytest.fixture(name="node_samba") -def node_samba_fixture(data_pro): +def node_samba_fixture(data_pro: JsonObjectType) -> Mock: """Define a mock NodeSamba object.""" return Mock( async_connect=AsyncMock(), @@ -145,7 +154,9 @@ def node_samba_fixture(data_pro): @pytest.fixture(name="setup_config_entry") -async def setup_config_entry_fixture(hass, config_entry, mock_pyairvisual): +async def setup_config_entry_fixture( + hass: HomeAssistant, config_entry: MockConfigEntry, mock_pyairvisual: None +) -> None: """Define a fixture to set up airvisual.""" assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/airvisual_pro/conftest.py b/tests/components/airvisual_pro/conftest.py index d81d7471cac..d25e9821d91 100644 --- a/tests/components/airvisual_pro/conftest.py +++ b/tests/components/airvisual_pro/conftest.py @@ -1,16 +1,18 @@ """Define test fixtures for AirVisual Pro.""" -import json +from typing import Any from unittest.mock import AsyncMock, Mock, patch import pytest -from typing_extensions import Generator +from typing_extensions import AsyncGenerator, Generator from homeassistant.components.airvisual_pro.const import DOMAIN from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD +from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component +from homeassistant.util.json import JsonObjectType -from tests.common import MockConfigEntry, load_fixture +from tests.common import MockConfigEntry, load_json_object_fixture @pytest.fixture @@ -23,7 +25,9 @@ def mock_setup_entry() -> Generator[AsyncMock]: @pytest.fixture(name="config_entry") -def config_entry_fixture(hass, config): +def config_entry_fixture( + hass: HomeAssistant, config: dict[str, Any] +) -> MockConfigEntry: """Define a config entry fixture.""" entry = MockConfigEntry( domain=DOMAIN, @@ -36,7 +40,7 @@ def config_entry_fixture(hass, config): @pytest.fixture(name="config") -def config_fixture(hass): +def config_fixture() -> dict[str, Any]: """Define a config entry data fixture.""" return { CONF_IP_ADDRESS: "192.168.1.101", @@ -45,25 +49,27 @@ def config_fixture(hass): @pytest.fixture(name="connect") -def connect_fixture(): +def connect_fixture() -> AsyncMock: """Define a mocked async_connect method.""" return AsyncMock(return_value=True) @pytest.fixture(name="disconnect") -def disconnect_fixture(): +def disconnect_fixture() -> AsyncMock: """Define a mocked async_connect method.""" return AsyncMock() @pytest.fixture(name="data", scope="package") -def data_fixture(): +def data_fixture() -> JsonObjectType: """Define an update coordinator data example.""" - return json.loads(load_fixture("data.json", "airvisual_pro")) + return load_json_object_fixture("data.json", "airvisual_pro") @pytest.fixture(name="pro") -def pro_fixture(connect, data, disconnect): +def pro_fixture( + connect: AsyncMock, data: JsonObjectType, disconnect: AsyncMock +) -> Mock: """Define a mocked NodeSamba object.""" return Mock( async_connect=connect, @@ -73,7 +79,9 @@ def pro_fixture(connect, data, disconnect): @pytest.fixture(name="setup_airvisual_pro") -async def setup_airvisual_pro_fixture(hass, config, pro): +async def setup_airvisual_pro_fixture( + hass: HomeAssistant, config, pro +) -> AsyncGenerator[None]: """Define a fixture to set up AirVisual Pro.""" with ( patch( From 0556d9d4ed8d456179d406f556fe12dbb5c2a1a0 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 7 Jun 2024 16:31:50 +0200 Subject: [PATCH 0356/1445] Fix Azure Data Explorer strings (#119067) --- homeassistant/components/azure_data_explorer/strings.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/azure_data_explorer/strings.json b/homeassistant/components/azure_data_explorer/strings.json index a3a82a6eb3c..64005872579 100644 --- a/homeassistant/components/azure_data_explorer/strings.json +++ b/homeassistant/components/azure_data_explorer/strings.json @@ -5,12 +5,13 @@ "title": "Setup your Azure Data Explorer integration", "description": "Enter connection details.", "data": { - "clusteringesturi": "Cluster Ingest URI", + "cluster_ingest_uri": "Cluster ingest URI", "database": "Database name", "table": "Table name", "client_id": "Client ID", "client_secret": "Client secret", - "authority_id": "Authority ID" + "authority_id": "Authority ID", + "use_queued_ingestion": "Use queued ingestion" } } }, From 624017a0f95cdd77b295162c57ffbc326b6574b4 Mon Sep 17 00:00:00 2001 From: paulusbrand <75862178+paulusbrand@users.noreply.github.com> Date: Fri, 7 Jun 2024 17:01:35 +0200 Subject: [PATCH 0357/1445] Add template Base64 decode encoding parameter (#116603) Co-authored-by: Robert Resch --- homeassistant/helpers/template.py | 12 ++++++++---- tests/helpers/test_template.py | 12 ++++++++++++ 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index f5c796ef46d..f10913c2478 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -2397,14 +2397,18 @@ def struct_unpack(value: bytes, format_string: str, offset: int = 0) -> Any | No return None -def base64_encode(value): +def base64_encode(value: str) -> str: """Perform base64 encode.""" return base64.b64encode(value.encode("utf-8")).decode("utf-8") -def base64_decode(value): - """Perform base64 denode.""" - return base64.b64decode(value).decode("utf-8") +def base64_decode(value: str, encoding: str | None = "utf-8") -> str | bytes: + """Perform base64 decode.""" + decoded = base64.b64decode(value) + if encoding: + return decoded.decode(encoding) + + return decoded def ordinal(value): diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index 71e1bc748a6..3d8dad1d23e 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -1643,6 +1643,18 @@ def test_base64_decode(hass: HomeAssistant) -> None: ).async_render() == "homeassistant" ) + assert ( + template.Template( + '{{ "aG9tZWFzc2lzdGFudA==" | base64_decode(None) }}', hass + ).async_render() + == b"homeassistant" + ) + assert ( + template.Template( + '{{ "aG9tZWFzc2lzdGFudA==" | base64_decode("ascii") }}', hass + ).async_render() + == "homeassistant" + ) def test_slugify(hass: HomeAssistant) -> None: From 9008b4295ce49d6383db586b8a3541e9f964b5b1 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 7 Jun 2024 19:39:25 +0200 Subject: [PATCH 0358/1445] Improve type hints in assist_pipeline tests (#119066) --- tests/components/assist_pipeline/conftest.py | 6 ++-- .../assist_pipeline/test_pipeline.py | 30 ++++++++----------- 2 files changed, 16 insertions(+), 20 deletions(-) diff --git a/tests/components/assist_pipeline/conftest.py b/tests/components/assist_pipeline/conftest.py index 6fba61b0679..f19e70a8ec1 100644 --- a/tests/components/assist_pipeline/conftest.py +++ b/tests/components/assist_pipeline/conftest.py @@ -154,7 +154,7 @@ class MockTTSPlatform(MockPlatform): @pytest.fixture -async def mock_tts_provider(hass) -> MockTTSProvider: +async def mock_tts_provider() -> MockTTSProvider: """Mock TTS provider.""" return MockTTSProvider() @@ -257,13 +257,13 @@ class MockWakeWordEntity2(wake_word.WakeWordDetectionEntity): @pytest.fixture -async def mock_wake_word_provider_entity(hass) -> MockWakeWordEntity: +async def mock_wake_word_provider_entity() -> MockWakeWordEntity: """Mock wake word provider.""" return MockWakeWordEntity() @pytest.fixture -async def mock_wake_word_provider_entity2(hass) -> MockWakeWordEntity2: +async def mock_wake_word_provider_entity2() -> MockWakeWordEntity2: """Mock wake word provider.""" return MockWakeWordEntity2() diff --git a/tests/components/assist_pipeline/test_pipeline.py b/tests/components/assist_pipeline/test_pipeline.py index c0b4640b124..3e1e99412d8 100644 --- a/tests/components/assist_pipeline/test_pipeline.py +++ b/tests/components/assist_pipeline/test_pipeline.py @@ -39,12 +39,13 @@ async def delay_save_fixture() -> AsyncGenerator[None]: @pytest.fixture(autouse=True) -async def load_homeassistant(hass) -> None: +async def load_homeassistant(hass: HomeAssistant) -> None: """Load the homeassistant integration.""" assert await async_setup_component(hass, "homeassistant", {}) -async def test_load_pipelines(hass: HomeAssistant, init_components) -> None: +@pytest.mark.usefixtures("init_components") +async def test_load_pipelines(hass: HomeAssistant) -> None: """Make sure that we can load/save data correctly.""" pipelines = [ @@ -247,9 +248,8 @@ async def test_migrate_pipeline_store( assert store.async_get_preferred_item() == "01GX8ZWBAQYWNB1XV3EXEZ75DY" -async def test_create_default_pipeline( - hass: HomeAssistant, init_supporting_components -) -> None: +@pytest.mark.usefixtures("init_supporting_components") +async def test_create_default_pipeline(hass: HomeAssistant) -> None: """Test async_create_default_pipeline.""" assert await async_setup_component(hass, "assist_pipeline", {}) @@ -395,9 +395,9 @@ async def test_default_pipeline_no_stt_tts( ("pt", "br", "pt-br", "pt", "pt-br", "pt-br"), ], ) +@pytest.mark.usefixtures("init_supporting_components") async def test_default_pipeline( hass: HomeAssistant, - init_supporting_components, mock_stt_provider: MockSttProvider, mock_tts_provider: MockTTSProvider, ha_language: str, @@ -439,10 +439,9 @@ async def test_default_pipeline( ) +@pytest.mark.usefixtures("init_supporting_components") async def test_default_pipeline_unsupported_stt_language( - hass: HomeAssistant, - init_supporting_components, - mock_stt_provider: MockSttProvider, + hass: HomeAssistant, mock_stt_provider: MockSttProvider ) -> None: """Test async_get_pipeline.""" with patch.object(mock_stt_provider, "_supported_languages", ["smurfish"]): @@ -470,10 +469,9 @@ async def test_default_pipeline_unsupported_stt_language( ) +@pytest.mark.usefixtures("init_supporting_components") async def test_default_pipeline_unsupported_tts_language( - hass: HomeAssistant, - init_supporting_components, - mock_tts_provider: MockTTSProvider, + hass: HomeAssistant, mock_tts_provider: MockTTSProvider ) -> None: """Test async_get_pipeline.""" with patch.object(mock_tts_provider, "_supported_languages", ["smurfish"]): @@ -502,8 +500,7 @@ async def test_default_pipeline_unsupported_tts_language( async def test_update_pipeline( - hass: HomeAssistant, - hass_storage: dict[str, Any], + hass: HomeAssistant, hass_storage: dict[str, Any] ) -> None: """Test async_update_pipeline.""" assert await async_setup_component(hass, "assist_pipeline", {}) @@ -623,9 +620,8 @@ async def test_update_pipeline( } -async def test_migrate_after_load( - hass: HomeAssistant, init_supporting_components -) -> None: +@pytest.mark.usefixtures("init_supporting_components") +async def test_migrate_after_load(hass: HomeAssistant) -> None: """Test migrating an engine after done loading.""" assert await async_setup_component(hass, "assist_pipeline", {}) From cd7f2f9f7720d827be121d141ec4853e2f772e3a Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 7 Jun 2024 20:07:38 +0200 Subject: [PATCH 0359/1445] Fix incorrect type hints in azure_data_explorer tests (#119065) --- .../azure_data_explorer/conftest.py | 10 ++--- .../azure_data_explorer/test_config_flow.py | 10 +++-- .../azure_data_explorer/test_init.py | 37 +++++++++---------- 3 files changed, 28 insertions(+), 29 deletions(-) diff --git a/tests/components/azure_data_explorer/conftest.py b/tests/components/azure_data_explorer/conftest.py index 28743bec719..4168021b333 100644 --- a/tests/components/azure_data_explorer/conftest.py +++ b/tests/components/azure_data_explorer/conftest.py @@ -3,7 +3,7 @@ from datetime import timedelta import logging from typing import Any -from unittest.mock import Mock, patch +from unittest.mock import AsyncMock, MagicMock, patch import pytest from typing_extensions import Generator @@ -94,7 +94,7 @@ async def mock_entry_with_one_event( # Fixtures for config_flow tests @pytest.fixture -def mock_setup_entry() -> Generator[MockConfigEntry]: +def mock_setup_entry() -> Generator[AsyncMock]: """Mock the setup entry call, used for config flow tests.""" with patch( f"{AZURE_DATA_EXPLORER_PATH}.async_setup_entry", return_value=True @@ -104,7 +104,7 @@ def mock_setup_entry() -> Generator[MockConfigEntry]: # Fixtures for mocking the Azure Data Explorer SDK calls. @pytest.fixture(autouse=True) -def mock_managed_streaming() -> Generator[mock_entry_fixture_managed, Any, Any]: +def mock_managed_streaming() -> Generator[MagicMock]: """mock_azure_data_explorer_ManagedStreamingIngestClient_ingest_data.""" with patch( "azure.kusto.ingest.ManagedStreamingIngestClient.ingest_from_stream", @@ -114,7 +114,7 @@ def mock_managed_streaming() -> Generator[mock_entry_fixture_managed, Any, Any]: @pytest.fixture(autouse=True) -def mock_queued_ingest() -> Generator[mock_entry_fixture_queued, Any, Any]: +def mock_queued_ingest() -> Generator[MagicMock]: """mock_azure_data_explorer_QueuedIngestClient_ingest_data.""" with patch( "azure.kusto.ingest.QueuedIngestClient.ingest_from_stream", @@ -124,7 +124,7 @@ def mock_queued_ingest() -> Generator[mock_entry_fixture_queued, Any, Any]: @pytest.fixture(autouse=True) -def mock_execute_query() -> Generator[Mock, Any, Any]: +def mock_execute_query() -> Generator[MagicMock]: """Mock KustoClient execute_query.""" with patch( "azure.kusto.data.KustoClient.execute_query", diff --git a/tests/components/azure_data_explorer/test_config_flow.py b/tests/components/azure_data_explorer/test_config_flow.py index 5c9fe6506fa..a700299be33 100644 --- a/tests/components/azure_data_explorer/test_config_flow.py +++ b/tests/components/azure_data_explorer/test_config_flow.py @@ -1,5 +1,7 @@ """Test the Azure Data Explorer config flow.""" +from unittest.mock import AsyncMock, MagicMock + from azure.kusto.data.exceptions import KustoAuthenticationError, KustoServiceError import pytest @@ -10,7 +12,7 @@ from homeassistant.core import HomeAssistant from .const import BASE_CONFIG -async def test_config_flow(hass, mock_setup_entry) -> None: +async def test_config_flow(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: """Test we get the form.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=None @@ -36,10 +38,10 @@ async def test_config_flow(hass, mock_setup_entry) -> None: ], ) async def test_config_flow_errors( - test_input, - expected, + test_input: Exception, + expected: str, hass: HomeAssistant, - mock_execute_query, + mock_execute_query: MagicMock, ) -> None: """Test we handle connection KustoServiceError.""" result = await hass.config_entries.flow.async_init( diff --git a/tests/components/azure_data_explorer/test_init.py b/tests/components/azure_data_explorer/test_init.py index dcafcfce500..4d339728d09 100644 --- a/tests/components/azure_data_explorer/test_init.py +++ b/tests/components/azure_data_explorer/test_init.py @@ -2,7 +2,7 @@ from datetime import datetime, timedelta import logging -from unittest.mock import Mock, patch +from unittest.mock import MagicMock, Mock, patch from azure.kusto.data.exceptions import KustoAuthenticationError, KustoServiceError from azure.kusto.ingest import StreamDescriptor @@ -28,11 +28,9 @@ _LOGGER = logging.getLogger(__name__) @pytest.mark.freeze_time("2024-01-01 00:00:00") +@pytest.mark.usefixtures("entry_managed") async def test_put_event_on_queue_with_managed_client( - hass: HomeAssistant, - entry_managed, - mock_managed_streaming: Mock, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mock_managed_streaming: Mock ) -> None: """Test listening to events from Hass. and writing to ADX with managed client.""" @@ -59,12 +57,12 @@ async def test_put_event_on_queue_with_managed_client( ], ids=["KustoServiceError", "KustoAuthenticationError"], ) +@pytest.mark.usefixtures("entry_managed") async def test_put_event_on_queue_with_managed_client_with_errors( hass: HomeAssistant, - entry_managed, mock_managed_streaming: Mock, - sideeffect, - log_message, + sideeffect: Exception, + log_message: str, caplog: pytest.LogCaptureFixture, ) -> None: """Test listening to events from Hass. and writing to ADX with managed client.""" @@ -83,7 +81,7 @@ async def test_put_event_on_queue_with_managed_client_with_errors( async def test_put_event_on_queue_with_queueing_client( hass: HomeAssistant, - entry_queued, + entry_queued: MockConfigEntry, mock_queued_ingest: Mock, ) -> None: """Test listening to events from Hass. and writing to ADX with managed client.""" @@ -124,7 +122,7 @@ async def test_import(hass: HomeAssistant) -> None: async def test_unload_entry( hass: HomeAssistant, - entry_managed, + entry_managed: MockConfigEntry, mock_managed_streaming: Mock, ) -> None: """Test being able to unload an entry. @@ -140,11 +138,8 @@ async def test_unload_entry( @pytest.mark.freeze_time("2024-01-01 00:00:00") -async def test_late_event( - hass: HomeAssistant, - entry_with_one_event, - mock_managed_streaming: Mock, -) -> None: +@pytest.mark.usefixtures("entry_with_one_event") +async def test_late_event(hass: HomeAssistant, mock_managed_streaming: Mock) -> None: """Test the check on late events.""" with patch( f"{AZURE_DATA_EXPLORER_PATH}.utcnow", @@ -225,8 +220,8 @@ async def test_late_event( ) async def test_filter( hass: HomeAssistant, - entry_managed, - tests, + entry_managed: MockConfigEntry, + tests: list[FilterTest], mock_managed_streaming: Mock, ) -> None: """Test different filters. @@ -254,9 +249,9 @@ async def test_filter( ) async def test_event( hass: HomeAssistant, - entry_managed, + entry_managed: MockConfigEntry, mock_managed_streaming: Mock, - event, + event: str | None, ) -> None: """Test listening to events from Hass. and getting an event with a newline in the state.""" @@ -279,7 +274,9 @@ async def test_event( ], ids=["KustoServiceError", "KustoAuthenticationError", "Exception"], ) -async def test_connection(hass, mock_execute_query, sideeffect) -> None: +async def test_connection( + hass: HomeAssistant, mock_execute_query: MagicMock, sideeffect: Exception +) -> None: """Test Error when no getting proper connection with Exception.""" entry = MockConfigEntry( domain=azure_data_explorer.DOMAIN, From 440185be254b8ffdafd453fcf71b33dcb769cc1b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 7 Jun 2024 13:09:48 -0500 Subject: [PATCH 0360/1445] Fix refactoring error in snmp switch (#119028) --- homeassistant/components/snmp/switch.py | 76 ++++++++++++++----------- homeassistant/components/snmp/util.py | 36 +++++++++--- 2 files changed, 72 insertions(+), 40 deletions(-) diff --git a/homeassistant/components/snmp/switch.py b/homeassistant/components/snmp/switch.py index 40083ed4213..02a94aeb8c1 100644 --- a/homeassistant/components/snmp/switch.py +++ b/homeassistant/components/snmp/switch.py @@ -8,6 +8,8 @@ from typing import Any import pysnmp.hlapi.asyncio as hlapi from pysnmp.hlapi.asyncio import ( CommunityData, + ObjectIdentity, + ObjectType, UdpTransportTarget, UsmUserData, getCmd, @@ -63,7 +65,12 @@ from .const import ( MAP_PRIV_PROTOCOLS, SNMP_VERSIONS, ) -from .util import RequestArgsType, async_create_request_cmd_args +from .util import ( + CommandArgsType, + RequestArgsType, + async_create_command_cmd_args, + async_create_request_cmd_args, +) _LOGGER = logging.getLogger(__name__) @@ -125,23 +132,23 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the SNMP switch.""" - name = config.get(CONF_NAME) - host = config.get(CONF_HOST) - port = config.get(CONF_PORT) + name: str = config[CONF_NAME] + host: str = config[CONF_HOST] + port: int = config[CONF_PORT] community = config.get(CONF_COMMUNITY) baseoid: str = config[CONF_BASEOID] - command_oid = config.get(CONF_COMMAND_OID) - command_payload_on = config.get(CONF_COMMAND_PAYLOAD_ON) - command_payload_off = config.get(CONF_COMMAND_PAYLOAD_OFF) + command_oid: str | None = config.get(CONF_COMMAND_OID) + command_payload_on: str | None = config.get(CONF_COMMAND_PAYLOAD_ON) + command_payload_off: str | None = config.get(CONF_COMMAND_PAYLOAD_OFF) version: str = config[CONF_VERSION] username = config.get(CONF_USERNAME) authkey = config.get(CONF_AUTH_KEY) authproto: str = config[CONF_AUTH_PROTOCOL] privkey = config.get(CONF_PRIV_KEY) privproto: str = config[CONF_PRIV_PROTOCOL] - payload_on = config.get(CONF_PAYLOAD_ON) - payload_off = config.get(CONF_PAYLOAD_OFF) - vartype = config.get(CONF_VARTYPE) + payload_on: str = config[CONF_PAYLOAD_ON] + payload_off: str = config[CONF_PAYLOAD_OFF] + vartype: str = config[CONF_VARTYPE] if version == "3": if not authkey: @@ -159,9 +166,11 @@ async def async_setup_platform( else: auth_data = CommunityData(community, mpModel=SNMP_VERSIONS[version]) + transport = UdpTransportTarget((host, port)) request_args = await async_create_request_cmd_args( - hass, auth_data, UdpTransportTarget((host, port)), baseoid + hass, auth_data, transport, baseoid ) + command_args = await async_create_command_cmd_args(hass, auth_data, transport) async_add_entities( [ @@ -177,6 +186,7 @@ async def async_setup_platform( command_payload_off, vartype, request_args, + command_args, ) ], True, @@ -188,21 +198,22 @@ class SnmpSwitch(SwitchEntity): def __init__( self, - name, - host, - port, - baseoid, - commandoid, - payload_on, - payload_off, - command_payload_on, - command_payload_off, - vartype, - request_args, + name: str, + host: str, + port: int, + baseoid: str, + commandoid: str | None, + payload_on: str, + payload_off: str, + command_payload_on: str | None, + command_payload_off: str | None, + vartype: str, + request_args: RequestArgsType, + command_args: CommandArgsType, ) -> None: """Initialize the switch.""" - self._name = name + self._attr_name = name self._baseoid = baseoid self._vartype = vartype @@ -215,7 +226,8 @@ class SnmpSwitch(SwitchEntity): self._payload_on = payload_on self._payload_off = payload_off self._target = UdpTransportTarget((host, port)) - self._request_args: RequestArgsType = request_args + self._request_args = request_args + self._command_args = command_args async def async_turn_on(self, **kwargs: Any) -> None: """Turn on the switch.""" @@ -226,7 +238,7 @@ class SnmpSwitch(SwitchEntity): """Turn off the switch.""" await self._execute_command(self._command_payload_off) - async def _execute_command(self, command): + async def _execute_command(self, command: str) -> None: # User did not set vartype and command is not a digit if self._vartype == "none" and not self._command_payload_on.isdigit(): await self._set(command) @@ -265,14 +277,12 @@ class SnmpSwitch(SwitchEntity): self._state = None @property - def name(self): - """Return the switch's name.""" - return self._name - - @property - def is_on(self): + def is_on(self) -> bool | None: """Return true if switch is on; False if off. None if unknown.""" return self._state - async def _set(self, value): - await setCmd(*self._request_args, value) + async def _set(self, value: Any) -> None: + """Set the state of the switch.""" + await setCmd( + *self._command_args, ObjectType(ObjectIdentity(self._commandoid), value) + ) diff --git a/homeassistant/components/snmp/util.py b/homeassistant/components/snmp/util.py index 23adbdf0b90..dd3e9a6b6d2 100644 --- a/homeassistant/components/snmp/util.py +++ b/homeassistant/components/snmp/util.py @@ -25,6 +25,14 @@ DATA_SNMP_ENGINE = "snmp_engine" _LOGGER = logging.getLogger(__name__) +type CommandArgsType = tuple[ + SnmpEngine, + UsmUserData | CommunityData, + UdpTransportTarget | Udp6TransportTarget, + ContextData, +] + + type RequestArgsType = tuple[ SnmpEngine, UsmUserData | CommunityData, @@ -34,20 +42,34 @@ type RequestArgsType = tuple[ ] +async def async_create_command_cmd_args( + hass: HomeAssistant, + auth_data: UsmUserData | CommunityData, + target: UdpTransportTarget | Udp6TransportTarget, +) -> CommandArgsType: + """Create command arguments. + + The ObjectType needs to be created dynamically by the caller. + """ + engine = await async_get_snmp_engine(hass) + return (engine, auth_data, target, ContextData()) + + async def async_create_request_cmd_args( hass: HomeAssistant, auth_data: UsmUserData | CommunityData, target: UdpTransportTarget | Udp6TransportTarget, object_id: str, ) -> RequestArgsType: - """Create request arguments.""" - return ( - await async_get_snmp_engine(hass), - auth_data, - target, - ContextData(), - ObjectType(ObjectIdentity(object_id)), + """Create request arguments. + + The same ObjectType is used for all requests. + """ + engine, auth_data, target, context_data = await async_create_command_cmd_args( + hass, auth_data, target ) + object_type = ObjectType(ObjectIdentity(object_id)) + return (engine, auth_data, target, context_data, object_type) @singleton(DATA_SNMP_ENGINE) From aa121ebf7332087ceea77ae2b04a73a123251859 Mon Sep 17 00:00:00 2001 From: OzGav Date: Sat, 8 Jun 2024 04:34:22 +1000 Subject: [PATCH 0361/1445] Add previous track intent (#113222) * add previous track intent * add stop and clear playlist * Remove clear_playlist and stop * Remove clear_playlist and stop * Use extra constraints --------- Co-authored-by: Michael Hansen --- .../components/media_player/intent.py | 15 ++++++ tests/components/media_player/test_intent.py | 54 +++++++++++++++++++ 2 files changed, 69 insertions(+) diff --git a/homeassistant/components/media_player/intent.py b/homeassistant/components/media_player/intent.py index f8b00935358..77220a87622 100644 --- a/homeassistant/components/media_player/intent.py +++ b/homeassistant/components/media_player/intent.py @@ -10,6 +10,7 @@ from homeassistant.const import ( SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_PLAY, + SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_VOLUME_SET, ) from homeassistant.core import Context, HomeAssistant, State @@ -21,6 +22,7 @@ from .const import MediaPlayerEntityFeature, MediaPlayerState INTENT_MEDIA_PAUSE = "HassMediaPause" INTENT_MEDIA_UNPAUSE = "HassMediaUnpause" INTENT_MEDIA_NEXT = "HassMediaNext" +INTENT_MEDIA_PREVIOUS = "HassMediaPrevious" INTENT_SET_VOLUME = "HassSetVolume" @@ -69,6 +71,19 @@ async def async_setup_intents(hass: HomeAssistant) -> None: platforms={DOMAIN}, ), ) + intent.async_register( + hass, + intent.ServiceIntentHandler( + INTENT_MEDIA_PREVIOUS, + DOMAIN, + SERVICE_MEDIA_PREVIOUS_TRACK, + required_domains={DOMAIN}, + required_features=MediaPlayerEntityFeature.PREVIOUS_TRACK, + required_states={MediaPlayerState.PLAYING}, + description="Replays the previous item for a media player", + platforms={DOMAIN}, + ), + ) intent.async_register( hass, intent.ServiceIntentHandler( diff --git a/tests/components/media_player/test_intent.py b/tests/components/media_player/test_intent.py index e73104eeb39..df47296d90c 100644 --- a/tests/components/media_player/test_intent.py +++ b/tests/components/media_player/test_intent.py @@ -7,6 +7,7 @@ from homeassistant.components.media_player import ( SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_PLAY, + SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_VOLUME_SET, intent as media_player_intent, ) @@ -173,6 +174,59 @@ async def test_next_media_player_intent(hass: HomeAssistant) -> None: await hass.async_block_till_done() +async def test_previous_media_player_intent(hass: HomeAssistant) -> None: + """Test HassMediaPrevious intent for media players.""" + await media_player_intent.async_setup_intents(hass) + + entity_id = f"{DOMAIN}.test_media_player" + attributes = {ATTR_SUPPORTED_FEATURES: MediaPlayerEntityFeature.PREVIOUS_TRACK} + + hass.states.async_set(entity_id, STATE_PLAYING, attributes=attributes) + + calls = async_mock_service(hass, DOMAIN, SERVICE_MEDIA_PREVIOUS_TRACK) + + response = await intent.async_handle( + hass, + "test", + media_player_intent.INTENT_MEDIA_PREVIOUS, + ) + await hass.async_block_till_done() + + assert response.response_type == intent.IntentResponseType.ACTION_DONE + assert len(calls) == 1 + call = calls[0] + assert call.domain == DOMAIN + assert call.service == SERVICE_MEDIA_PREVIOUS_TRACK + assert call.data == {"entity_id": entity_id} + + # Test if not playing + hass.states.async_set(entity_id, STATE_IDLE, attributes=attributes) + + with pytest.raises(intent.MatchFailedError): + response = await intent.async_handle( + hass, + "test", + media_player_intent.INTENT_MEDIA_PREVIOUS, + ) + await hass.async_block_till_done() + + # Test feature not supported + hass.states.async_set( + entity_id, + STATE_PLAYING, + attributes={ATTR_SUPPORTED_FEATURES: MediaPlayerEntityFeature(0)}, + ) + + with pytest.raises(intent.MatchFailedError): + response = await intent.async_handle( + hass, + "test", + media_player_intent.INTENT_MEDIA_PREVIOUS, + {"name": {"value": "test media player"}}, + ) + await hass.async_block_till_done() + + async def test_volume_media_player_intent(hass: HomeAssistant) -> None: """Test HassSetVolume intent for media players.""" await media_player_intent.async_setup_intents(hass) From 1bda33b1e9f84bf4b820f813a31790e06f9201f3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 7 Jun 2024 13:48:36 -0500 Subject: [PATCH 0362/1445] Bump home-assistant-bluetooth to 1.12.1 (#119026) --- homeassistant/package_constraints.txt | 2 +- pyproject.toml | 2 +- requirements.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 12fc76335d8..0b05f400be0 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -31,7 +31,7 @@ ha-ffmpeg==3.2.0 habluetooth==3.1.1 hass-nabucasa==0.81.1 hassil==1.7.1 -home-assistant-bluetooth==1.12.0 +home-assistant-bluetooth==1.12.1 home-assistant-frontend==20240605.0 home-assistant-intents==2024.6.5 httpx==0.27.0 diff --git a/pyproject.toml b/pyproject.toml index f956f77250f..ba234a1c1f1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,7 +44,7 @@ dependencies = [ # When bumping httpx, please check the version pins of # httpcore, anyio, and h11 in gen_requirements_all "httpx==0.27.0", - "home-assistant-bluetooth==1.12.0", + "home-assistant-bluetooth==1.12.1", "ifaddr==0.2.0", "Jinja2==3.1.4", "lru-dict==1.3.0", diff --git a/requirements.txt b/requirements.txt index 21da099bcb5..781e15e5fbe 100644 --- a/requirements.txt +++ b/requirements.txt @@ -20,7 +20,7 @@ ciso8601==2.3.1 fnv-hash-fast==0.5.0 hass-nabucasa==0.81.1 httpx==0.27.0 -home-assistant-bluetooth==1.12.0 +home-assistant-bluetooth==1.12.1 ifaddr==0.2.0 Jinja2==3.1.4 lru-dict==1.3.0 From 4a4c98caadaaef4962c2535d8fdb9af86b0dfd2d Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 7 Jun 2024 20:49:58 +0200 Subject: [PATCH 0363/1445] Move mock_async_zeroconf to decorator in zeroconf tests (#119063) --- tests/components/zeroconf/test_init.py | 101 ++++++++++++------------- 1 file changed, 50 insertions(+), 51 deletions(-) diff --git a/tests/components/zeroconf/test_init.py b/tests/components/zeroconf/test_init.py index a0b2d546dec..0a552f37aa9 100644 --- a/tests/components/zeroconf/test_init.py +++ b/tests/components/zeroconf/test_init.py @@ -194,8 +194,9 @@ async def test_setup(hass: HomeAssistant, mock_async_zeroconf: MagicMock) -> Non assert await zeroconf.async_get_async_instance(hass) is mock_async_zeroconf +@pytest.mark.usefixtures("mock_async_zeroconf") async def test_setup_with_overly_long_url_and_name( - hass: HomeAssistant, mock_async_zeroconf: None, caplog: pytest.LogCaptureFixture + hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: """Test we still setup with long urls and names.""" with ( @@ -237,8 +238,9 @@ async def test_setup_with_overly_long_url_and_name( assert "German Umlaut" in caplog.text +@pytest.mark.usefixtures("mock_async_zeroconf") async def test_setup_with_defaults( - hass: HomeAssistant, mock_zeroconf: MagicMock, mock_async_zeroconf: None + hass: HomeAssistant, mock_zeroconf: MagicMock ) -> None: """Test default interface config.""" with ( @@ -258,9 +260,8 @@ async def test_setup_with_defaults( ) -async def test_zeroconf_match_macaddress( - hass: HomeAssistant, mock_async_zeroconf: None -) -> None: +@pytest.mark.usefixtures("mock_async_zeroconf") +async def test_zeroconf_match_macaddress(hass: HomeAssistant) -> None: """Test configured options for a device are loaded via config entry.""" def http_only_service_update_mock(zeroconf, services, handlers): @@ -305,9 +306,8 @@ async def test_zeroconf_match_macaddress( assert mock_config_flow.mock_calls[0][2]["context"] == {"source": "zeroconf"} -async def test_zeroconf_match_manufacturer( - hass: HomeAssistant, mock_async_zeroconf: None -) -> None: +@pytest.mark.usefixtures("mock_async_zeroconf") +async def test_zeroconf_match_manufacturer(hass: HomeAssistant) -> None: """Test configured options for a device are loaded via config entry.""" def http_only_service_update_mock(zeroconf, services, handlers): @@ -347,9 +347,8 @@ async def test_zeroconf_match_manufacturer( assert mock_config_flow.mock_calls[0][1][0] == "samsungtv" -async def test_zeroconf_match_model( - hass: HomeAssistant, mock_async_zeroconf: None -) -> None: +@pytest.mark.usefixtures("mock_async_zeroconf") +async def test_zeroconf_match_model(hass: HomeAssistant) -> None: """Test matching a specific model in zeroconf.""" def http_only_service_update_mock(zeroconf, services, handlers): @@ -389,9 +388,8 @@ async def test_zeroconf_match_model( assert mock_config_flow.mock_calls[0][1][0] == "appletv" -async def test_zeroconf_match_manufacturer_not_present( - hass: HomeAssistant, mock_async_zeroconf: None -) -> None: +@pytest.mark.usefixtures("mock_async_zeroconf") +async def test_zeroconf_match_manufacturer_not_present(hass: HomeAssistant) -> None: """Test matchers reject when a property is missing.""" def http_only_service_update_mock(zeroconf, services, handlers): @@ -430,9 +428,8 @@ async def test_zeroconf_match_manufacturer_not_present( assert len(mock_config_flow.mock_calls) == 0 -async def test_zeroconf_no_match( - hass: HomeAssistant, mock_async_zeroconf: None -) -> None: +@pytest.mark.usefixtures("mock_async_zeroconf") +async def test_zeroconf_no_match(hass: HomeAssistant) -> None: """Test configured options for a device are loaded via config entry.""" def http_only_service_update_mock(zeroconf, services, handlers): @@ -467,9 +464,8 @@ async def test_zeroconf_no_match( assert len(mock_config_flow.mock_calls) == 0 -async def test_zeroconf_no_match_manufacturer( - hass: HomeAssistant, mock_async_zeroconf: None -) -> None: +@pytest.mark.usefixtures("mock_async_zeroconf") +async def test_zeroconf_no_match_manufacturer(hass: HomeAssistant) -> None: """Test configured options for a device are loaded via config entry.""" def http_only_service_update_mock(zeroconf, services, handlers): @@ -508,9 +504,8 @@ async def test_zeroconf_no_match_manufacturer( assert len(mock_config_flow.mock_calls) == 0 -async def test_homekit_match_partial_space( - hass: HomeAssistant, mock_async_zeroconf: None -) -> None: +@pytest.mark.usefixtures("mock_async_zeroconf") +async def test_homekit_match_partial_space(hass: HomeAssistant) -> None: """Test configured options for a device are loaded via config entry.""" with ( patch.dict( @@ -550,8 +545,9 @@ async def test_homekit_match_partial_space( } +@pytest.mark.usefixtures("mock_async_zeroconf") async def test_device_with_invalid_name( - hass: HomeAssistant, mock_async_zeroconf: None, caplog: pytest.LogCaptureFixture + hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: """Test we ignore devices with an invalid name.""" with ( @@ -587,9 +583,8 @@ async def test_device_with_invalid_name( assert "Bad name in zeroconf record" in caplog.text -async def test_homekit_match_partial_dash( - hass: HomeAssistant, mock_async_zeroconf: None -) -> None: +@pytest.mark.usefixtures("mock_async_zeroconf") +async def test_homekit_match_partial_dash(hass: HomeAssistant) -> None: """Test configured options for a device are loaded via config entry.""" with ( patch.dict( @@ -626,9 +621,8 @@ async def test_homekit_match_partial_dash( assert mock_config_flow.mock_calls[0][1][0] == "lutron_caseta" -async def test_homekit_match_partial_fnmatch( - hass: HomeAssistant, mock_async_zeroconf: None -) -> None: +@pytest.mark.usefixtures("mock_async_zeroconf") +async def test_homekit_match_partial_fnmatch(hass: HomeAssistant) -> None: """Test matching homekit devices with fnmatch.""" with ( patch.dict( @@ -663,9 +657,8 @@ async def test_homekit_match_partial_fnmatch( assert mock_config_flow.mock_calls[0][1][0] == "yeelight" -async def test_homekit_match_full( - hass: HomeAssistant, mock_async_zeroconf: None -) -> None: +@pytest.mark.usefixtures("mock_async_zeroconf") +async def test_homekit_match_full(hass: HomeAssistant) -> None: """Test configured options for a device are loaded via config entry.""" with ( patch.dict( @@ -700,9 +693,8 @@ async def test_homekit_match_full( assert mock_config_flow.mock_calls[0][1][0] == "hue" -async def test_homekit_already_paired( - hass: HomeAssistant, mock_async_zeroconf: None -) -> None: +@pytest.mark.usefixtures("mock_async_zeroconf") +async def test_homekit_already_paired(hass: HomeAssistant) -> None: """Test that an already paired device is sent to homekit_controller.""" with ( patch.dict( @@ -741,9 +733,8 @@ async def test_homekit_already_paired( assert mock_config_flow.mock_calls[1][1][0] == "homekit_controller" -async def test_homekit_invalid_paring_status( - hass: HomeAssistant, mock_async_zeroconf: None -) -> None: +@pytest.mark.usefixtures("mock_async_zeroconf") +async def test_homekit_invalid_paring_status(hass: HomeAssistant) -> None: """Test that missing paring data is not sent to homekit_controller.""" with ( patch.dict( @@ -778,9 +769,8 @@ async def test_homekit_invalid_paring_status( assert mock_config_flow.mock_calls[0][1][0] == "lutron_caseta" -async def test_homekit_not_paired( - hass: HomeAssistant, mock_async_zeroconf: None -) -> None: +@pytest.mark.usefixtures("mock_async_zeroconf") +async def test_homekit_not_paired(hass: HomeAssistant) -> None: """Test that an not paired device is sent to homekit_controller.""" with ( patch.dict( @@ -808,8 +798,9 @@ async def test_homekit_not_paired( assert mock_config_flow.mock_calls[0][1][0] == "homekit_controller" +@pytest.mark.usefixtures("mock_async_zeroconf") async def test_homekit_controller_still_discovered_unpaired_for_cloud( - hass: HomeAssistant, mock_async_zeroconf: None + hass: HomeAssistant, ) -> None: """Test discovery is still passed to homekit controller when unpaired. @@ -852,8 +843,9 @@ async def test_homekit_controller_still_discovered_unpaired_for_cloud( assert mock_config_flow.mock_calls[1][1][0] == "homekit_controller" +@pytest.mark.usefixtures("mock_async_zeroconf") async def test_homekit_controller_still_discovered_unpaired_for_polling( - hass: HomeAssistant, mock_async_zeroconf: None + hass: HomeAssistant, ) -> None: """Test discovery is still passed to homekit controller when unpaired. @@ -1010,7 +1002,8 @@ async def test_get_instance( assert len(mock_async_zeroconf.ha_async_close.mock_calls) == 1 -async def test_removed_ignored(hass: HomeAssistant, mock_async_zeroconf: None) -> None: +@pytest.mark.usefixtures("mock_async_zeroconf") +async def test_removed_ignored(hass: HomeAssistant) -> None: """Test we remove it when a zeroconf entry is removed.""" def service_update_mock(zeroconf, services, handlers): @@ -1062,8 +1055,9 @@ _ADAPTER_WITH_DEFAULT_ENABLED = [ ] +@pytest.mark.usefixtures("mock_async_zeroconf") async def test_async_detect_interfaces_setting_non_loopback_route( - hass: HomeAssistant, mock_async_zeroconf: None + hass: HomeAssistant, ) -> None: """Test without default interface and the route returns a non-loopback address.""" with ( @@ -1148,8 +1142,9 @@ _ADAPTERS_WITH_MANUAL_CONFIG = [ ] +@pytest.mark.usefixtures("mock_async_zeroconf") async def test_async_detect_interfaces_setting_empty_route_linux( - hass: HomeAssistant, mock_async_zeroconf: None + hass: HomeAssistant, ) -> None: """Test without default interface config and the route returns nothing on linux.""" with ( @@ -1181,8 +1176,9 @@ async def test_async_detect_interfaces_setting_empty_route_linux( ) +@pytest.mark.usefixtures("mock_async_zeroconf") async def test_async_detect_interfaces_setting_empty_route_freebsd( - hass: HomeAssistant, mock_async_zeroconf: None + hass: HomeAssistant, ) -> None: """Test without default interface and the route returns nothing on freebsd.""" with ( @@ -1231,8 +1227,9 @@ _ADAPTER_WITH_DEFAULT_ENABLED_AND_IPV6 = [ ] +@pytest.mark.usefixtures("mock_async_zeroconf") async def test_async_detect_interfaces_explicitly_set_ipv6_linux( - hass: HomeAssistant, mock_async_zeroconf: None + hass: HomeAssistant, ) -> None: """Test interfaces are explicitly set when IPv6 is present on linux.""" with ( @@ -1259,8 +1256,9 @@ async def test_async_detect_interfaces_explicitly_set_ipv6_linux( ) +@pytest.mark.usefixtures("mock_async_zeroconf") async def test_async_detect_interfaces_explicitly_set_ipv6_freebsd( - hass: HomeAssistant, mock_async_zeroconf: None + hass: HomeAssistant, ) -> None: """Test interfaces are explicitly set when IPv6 is present on freebsd.""" with ( @@ -1336,7 +1334,8 @@ async def test_start_with_frontend( mock_async_zeroconf.async_register_service.assert_called_once() -async def test_zeroconf_removed(hass: HomeAssistant, mock_async_zeroconf: None) -> None: +@pytest.mark.usefixtures("mock_async_zeroconf") +async def test_zeroconf_removed(hass: HomeAssistant) -> None: """Test we dismiss flows when a PTR record is removed.""" def _device_removed_mock(zeroconf, services, handlers): From ae59d0eadf2f2da3b99a6deb30dd36b49cc10960 Mon Sep 17 00:00:00 2001 From: tronikos Date: Fri, 7 Jun 2024 11:50:55 -0700 Subject: [PATCH 0364/1445] Bump google-generativeai to 0.6.0 (#119062) --- .../google_generative_ai_conversation/conversation.py | 10 +++++----- .../google_generative_ai_conversation/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/google_generative_ai_conversation/conversation.py b/homeassistant/components/google_generative_ai_conversation/conversation.py index 6b2f3c11dcc..6c2bd64a7b5 100644 --- a/homeassistant/components/google_generative_ai_conversation/conversation.py +++ b/homeassistant/components/google_generative_ai_conversation/conversation.py @@ -4,9 +4,9 @@ from __future__ import annotations from typing import Any, Literal -import google.ai.generativelanguage as glm from google.api_core.exceptions import GoogleAPICallError import google.generativeai as genai +from google.generativeai import protos import google.generativeai.types as genai_types from google.protobuf.json_format import MessageToDict import voluptuous as vol @@ -93,7 +93,7 @@ def _format_tool(tool: llm.Tool) -> dict[str, Any]: parameters = _format_schema(convert(tool.parameters)) - return glm.Tool( + return protos.Tool( { "function_declarations": [ { @@ -349,13 +349,13 @@ class GoogleGenerativeAIConversationEntity( LOGGER.debug("Tool response: %s", function_response) tool_responses.append( - glm.Part( - function_response=glm.FunctionResponse( + protos.Part( + function_response=protos.FunctionResponse( name=tool_name, response=function_response ) ) ) - chat_request = glm.Content(parts=tool_responses) + chat_request = protos.Content(parts=tool_responses) intent_response.async_set_speech( " ".join([part.text.strip() for part in chat_response.parts if part.text]) diff --git a/homeassistant/components/google_generative_ai_conversation/manifest.json b/homeassistant/components/google_generative_ai_conversation/manifest.json index 1886b16985f..168fee105a0 100644 --- a/homeassistant/components/google_generative_ai_conversation/manifest.json +++ b/homeassistant/components/google_generative_ai_conversation/manifest.json @@ -9,5 +9,5 @@ "integration_type": "service", "iot_class": "cloud_polling", "quality_scale": "platinum", - "requirements": ["google-generativeai==0.5.4", "voluptuous-openapi==0.0.4"] + "requirements": ["google-generativeai==0.6.0", "voluptuous-openapi==0.0.4"] } diff --git a/requirements_all.txt b/requirements_all.txt index 75baa58a008..ff0a2be7cb8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -977,7 +977,7 @@ google-cloud-pubsub==2.13.11 google-cloud-texttospeech==2.12.3 # homeassistant.components.google_generative_ai_conversation -google-generativeai==0.5.4 +google-generativeai==0.6.0 # homeassistant.components.nest google-nest-sdm==4.0.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6657e58adb1..a7d963ee7d9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -803,7 +803,7 @@ google-api-python-client==2.71.0 google-cloud-pubsub==2.13.11 # homeassistant.components.google_generative_ai_conversation -google-generativeai==0.5.4 +google-generativeai==0.6.0 # homeassistant.components.nest google-nest-sdm==4.0.4 From b2a54c50e2fb5437df0ebd21003788e62084189f Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 7 Jun 2024 20:55:20 +0200 Subject: [PATCH 0365/1445] Move mock_zeroconf to decorator in tests (#119061) --- .../components/bosch_shc/test_config_flow.py | 54 +++++------ .../cast/test_home_assistant_cast.py | 14 +-- .../devolo_home_control/test_init.py | 6 +- tests/components/esphome/test_config_flow.py | 92 +++++++++++-------- tests/components/esphome/test_init.py | 7 +- tests/components/esphome/test_manager.py | 24 +++-- tests/components/zeroconf/test_usage.py | 12 +-- 7 files changed, 116 insertions(+), 93 deletions(-) diff --git a/tests/components/bosch_shc/test_config_flow.py b/tests/components/bosch_shc/test_config_flow.py index b3a28151c93..2c43ec0a370 100644 --- a/tests/components/bosch_shc/test_config_flow.py +++ b/tests/components/bosch_shc/test_config_flow.py @@ -10,6 +10,7 @@ from boschshcpy.exceptions import ( SHCSessionError, ) from boschshcpy.information import SHCInformation +import pytest from homeassistant import config_entries from homeassistant.components import zeroconf @@ -35,7 +36,8 @@ DISCOVERY_INFO = zeroconf.ZeroconfServiceInfo( ) -async def test_form_user(hass: HomeAssistant, mock_zeroconf: None) -> None: +@pytest.mark.usefixtures("mock_zeroconf") +async def test_form_user(hass: HomeAssistant) -> None: """Test we get the form.""" result = await hass.config_entries.flow.async_init( @@ -107,9 +109,8 @@ async def test_form_user(hass: HomeAssistant, mock_zeroconf: None) -> None: assert len(mock_setup_entry.mock_calls) == 1 -async def test_form_get_info_connection_error( - hass: HomeAssistant, mock_zeroconf: None -) -> None: +@pytest.mark.usefixtures("mock_zeroconf") +async def test_form_get_info_connection_error(hass: HomeAssistant) -> None: """Test we handle connection error.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -153,7 +154,8 @@ async def test_form_get_info_exception(hass: HomeAssistant) -> None: assert result2["errors"] == {"base": "unknown"} -async def test_form_pairing_error(hass: HomeAssistant, mock_zeroconf: None) -> None: +@pytest.mark.usefixtures("mock_zeroconf") +async def test_form_pairing_error(hass: HomeAssistant) -> None: """Test we handle pairing error.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -199,7 +201,8 @@ async def test_form_pairing_error(hass: HomeAssistant, mock_zeroconf: None) -> N assert result3["errors"] == {"base": "pairing_failed"} -async def test_form_user_invalid_auth(hass: HomeAssistant, mock_zeroconf: None) -> None: +@pytest.mark.usefixtures("mock_zeroconf") +async def test_form_user_invalid_auth(hass: HomeAssistant) -> None: """Test we handle invalid auth.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -257,9 +260,8 @@ async def test_form_user_invalid_auth(hass: HomeAssistant, mock_zeroconf: None) assert result3["errors"] == {"base": "invalid_auth"} -async def test_form_validate_connection_error( - hass: HomeAssistant, mock_zeroconf: None -) -> None: +@pytest.mark.usefixtures("mock_zeroconf") +async def test_form_validate_connection_error(hass: HomeAssistant) -> None: """Test we handle connection error.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -317,9 +319,8 @@ async def test_form_validate_connection_error( assert result3["errors"] == {"base": "cannot_connect"} -async def test_form_validate_session_error( - hass: HomeAssistant, mock_zeroconf: None -) -> None: +@pytest.mark.usefixtures("mock_zeroconf") +async def test_form_validate_session_error(hass: HomeAssistant) -> None: """Test we handle session error.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -377,9 +378,8 @@ async def test_form_validate_session_error( assert result3["errors"] == {"base": "session_error"} -async def test_form_validate_exception( - hass: HomeAssistant, mock_zeroconf: None -) -> None: +@pytest.mark.usefixtures("mock_zeroconf") +async def test_form_validate_exception(hass: HomeAssistant) -> None: """Test we handle exception.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -437,9 +437,8 @@ async def test_form_validate_exception( assert result3["errors"] == {"base": "unknown"} -async def test_form_already_configured( - hass: HomeAssistant, mock_zeroconf: None -) -> None: +@pytest.mark.usefixtures("mock_zeroconf") +async def test_form_already_configured(hass: HomeAssistant) -> None: """Test we get the form.""" entry = MockConfigEntry( @@ -479,7 +478,8 @@ async def test_form_already_configured( assert entry.data["host"] == "1.1.1.1" -async def test_zeroconf(hass: HomeAssistant, mock_zeroconf: None) -> None: +@pytest.mark.usefixtures("mock_zeroconf") +async def test_zeroconf(hass: HomeAssistant) -> None: """Test we get the form.""" with ( @@ -557,9 +557,8 @@ async def test_zeroconf(hass: HomeAssistant, mock_zeroconf: None) -> None: assert len(mock_setup_entry.mock_calls) == 1 -async def test_zeroconf_already_configured( - hass: HomeAssistant, mock_zeroconf: None -) -> None: +@pytest.mark.usefixtures("mock_zeroconf") +async def test_zeroconf_already_configured(hass: HomeAssistant) -> None: """Test we get the form.""" entry = MockConfigEntry( @@ -596,9 +595,8 @@ async def test_zeroconf_already_configured( assert entry.data["host"] == "1.1.1.1" -async def test_zeroconf_cannot_connect( - hass: HomeAssistant, mock_zeroconf: None -) -> None: +@pytest.mark.usefixtures("mock_zeroconf") +async def test_zeroconf_cannot_connect(hass: HomeAssistant) -> None: """Test we get the form.""" with patch( "boschshcpy.session.SHCSession.mdns_info", side_effect=SHCConnectionError @@ -612,7 +610,8 @@ async def test_zeroconf_cannot_connect( assert result["reason"] == "cannot_connect" -async def test_zeroconf_not_bosch_shc(hass: HomeAssistant, mock_zeroconf: None) -> None: +@pytest.mark.usefixtures("mock_zeroconf") +async def test_zeroconf_not_bosch_shc(hass: HomeAssistant) -> None: """Test we filter out non-bosch_shc devices.""" result = await hass.config_entries.flow.async_init( DOMAIN, @@ -631,7 +630,8 @@ async def test_zeroconf_not_bosch_shc(hass: HomeAssistant, mock_zeroconf: None) assert result["reason"] == "not_bosch_shc" -async def test_reauth(hass: HomeAssistant, mock_zeroconf: None) -> None: +@pytest.mark.usefixtures("mock_zeroconf") +async def test_reauth(hass: HomeAssistant) -> None: """Test we get the form.""" mock_config = MockConfigEntry( diff --git a/tests/components/cast/test_home_assistant_cast.py b/tests/components/cast/test_home_assistant_cast.py index 74ab776ec3b..c9e311bb024 100644 --- a/tests/components/cast/test_home_assistant_cast.py +++ b/tests/components/cast/test_home_assistant_cast.py @@ -12,7 +12,8 @@ from homeassistant.exceptions import HomeAssistantError from tests.common import MockConfigEntry, async_mock_signal -async def test_service_show_view(hass: HomeAssistant, mock_zeroconf: None) -> None: +@pytest.mark.usefixtures("mock_zeroconf") +async def test_service_show_view(hass: HomeAssistant) -> None: """Test showing a view.""" entry = MockConfigEntry(domain=DOMAIN) entry.add_to_hass(hass) @@ -51,9 +52,8 @@ async def test_service_show_view(hass: HomeAssistant, mock_zeroconf: None) -> No assert url_path is None -async def test_service_show_view_dashboard( - hass: HomeAssistant, mock_zeroconf: None -) -> None: +@pytest.mark.usefixtures("mock_zeroconf") +async def test_service_show_view_dashboard(hass: HomeAssistant) -> None: """Test casting a specific dashboard.""" await async_process_ha_core_config( hass, @@ -82,7 +82,8 @@ async def test_service_show_view_dashboard( assert url_path == "mock-dashboard" -async def test_use_cloud_url(hass: HomeAssistant, mock_zeroconf: None) -> None: +@pytest.mark.usefixtures("mock_zeroconf") +async def test_use_cloud_url(hass: HomeAssistant) -> None: """Test that we fall back to cloud url.""" await async_process_ha_core_config( hass, @@ -111,7 +112,8 @@ async def test_use_cloud_url(hass: HomeAssistant, mock_zeroconf: None) -> None: assert controller_data["hass_url"] == "https://something.nabu.casa" -async def test_remove_entry(hass: HomeAssistant, mock_zeroconf: None) -> None: +@pytest.mark.usefixtures("mock_zeroconf") +async def test_remove_entry(hass: HomeAssistant) -> None: """Test removing config entry removes user.""" entry = MockConfigEntry( data={}, diff --git a/tests/components/devolo_home_control/test_init.py b/tests/components/devolo_home_control/test_init.py index 9c3b1668991..da007303688 100644 --- a/tests/components/devolo_home_control/test_init.py +++ b/tests/components/devolo_home_control/test_init.py @@ -19,7 +19,8 @@ from .mocks import HomeControlMock, HomeControlMockBinarySensor from tests.typing import WebSocketGenerator -async def test_setup_entry(hass: HomeAssistant, mock_zeroconf: None) -> None: +@pytest.mark.usefixtures("mock_zeroconf") +async def test_setup_entry(hass: HomeAssistant) -> None: """Test setup entry.""" entry = configure_integration(hass) with patch("homeassistant.components.devolo_home_control.HomeControl"): @@ -43,7 +44,8 @@ async def test_setup_entry_maintenance(hass: HomeAssistant) -> None: assert entry.state is ConfigEntryState.SETUP_RETRY -async def test_setup_gateway_offline(hass: HomeAssistant, mock_zeroconf: None) -> None: +@pytest.mark.usefixtures("mock_zeroconf") +async def test_setup_gateway_offline(hass: HomeAssistant) -> None: """Test setup entry fails on gateway offline.""" entry = configure_integration(hass) with patch( diff --git a/tests/components/esphome/test_config_flow.py b/tests/components/esphome/test_config_flow.py index c5052220313..9c61a5d0615 100644 --- a/tests/components/esphome/test_config_flow.py +++ b/tests/components/esphome/test_config_flow.py @@ -47,8 +47,9 @@ def mock_setup_entry(): yield +@pytest.mark.usefixtures("mock_zeroconf") async def test_user_connection_works( - hass: HomeAssistant, mock_client, mock_zeroconf: None, mock_setup_entry: None + hass: HomeAssistant, mock_client, mock_setup_entry: None ) -> None: """Test we can finish a config flow.""" result = await hass.config_entries.flow.async_init( @@ -89,8 +90,9 @@ async def test_user_connection_works( assert mock_client.noise_psk is None +@pytest.mark.usefixtures("mock_zeroconf") async def test_user_connection_updates_host( - hass: HomeAssistant, mock_client, mock_zeroconf: None, mock_setup_entry: None + hass: HomeAssistant, mock_client, mock_setup_entry: None ) -> None: """Test setup up the same name updates the host.""" entry = MockConfigEntry( @@ -118,8 +120,9 @@ async def test_user_connection_updates_host( assert entry.data[CONF_HOST] == "127.0.0.1" +@pytest.mark.usefixtures("mock_zeroconf") async def test_user_sets_unique_id( - hass: HomeAssistant, mock_client, mock_zeroconf: None, mock_setup_entry: None + hass: HomeAssistant, mock_client, mock_setup_entry: None ) -> None: """Test that the user flow sets the unique id.""" service_info = zeroconf.ZeroconfServiceInfo( @@ -170,8 +173,9 @@ async def test_user_sets_unique_id( assert result["reason"] == "already_configured" +@pytest.mark.usefixtures("mock_zeroconf") async def test_user_resolve_error( - hass: HomeAssistant, mock_client, mock_zeroconf: None, mock_setup_entry: None + hass: HomeAssistant, mock_client, mock_setup_entry: None ) -> None: """Test user step with IP resolve error.""" @@ -195,8 +199,9 @@ async def test_user_resolve_error( assert len(mock_client.disconnect.mock_calls) == 1 +@pytest.mark.usefixtures("mock_zeroconf") async def test_user_causes_zeroconf_to_abort( - hass: HomeAssistant, mock_client, mock_zeroconf: None, mock_setup_entry: None + hass: HomeAssistant, mock_client, mock_setup_entry: None ) -> None: """Test that the user flow sets the unique id and aborts the zeroconf flow.""" service_info = zeroconf.ZeroconfServiceInfo( @@ -242,8 +247,9 @@ async def test_user_causes_zeroconf_to_abort( assert not hass.config_entries.flow.async_progress_by_handler(DOMAIN) +@pytest.mark.usefixtures("mock_zeroconf") async def test_user_connection_error( - hass: HomeAssistant, mock_client, mock_zeroconf: None, mock_setup_entry: None + hass: HomeAssistant, mock_client, mock_setup_entry: None ) -> None: """Test user step with connection error.""" mock_client.device_info.side_effect = APIConnectionError @@ -263,8 +269,9 @@ async def test_user_connection_error( assert len(mock_client.disconnect.mock_calls) == 1 +@pytest.mark.usefixtures("mock_zeroconf") async def test_user_with_password( - hass: HomeAssistant, mock_client, mock_zeroconf: None, mock_setup_entry: None + hass: HomeAssistant, mock_client, mock_setup_entry: None ) -> None: """Test user step with password.""" mock_client.device_info.return_value = DeviceInfo(uses_password=True, name="test") @@ -293,9 +300,8 @@ async def test_user_with_password( assert mock_client.password == "password1" -async def test_user_invalid_password( - hass: HomeAssistant, mock_client, mock_zeroconf: None -) -> None: +@pytest.mark.usefixtures("mock_zeroconf") +async def test_user_invalid_password(hass: HomeAssistant, mock_client) -> None: """Test user step with invalid password.""" mock_client.device_info.return_value = DeviceInfo(uses_password=True, name="test") @@ -319,11 +325,11 @@ async def test_user_invalid_password( assert result["errors"] == {"base": "invalid_auth"} +@pytest.mark.usefixtures("mock_zeroconf") async def test_user_dashboard_has_wrong_key( hass: HomeAssistant, mock_client, mock_dashboard, - mock_zeroconf: None, mock_setup_entry: None, ) -> None: """Test user step with key from dashboard that is incorrect.""" @@ -366,11 +372,11 @@ async def test_user_dashboard_has_wrong_key( assert mock_client.noise_psk == VALID_NOISE_PSK +@pytest.mark.usefixtures("mock_zeroconf") async def test_user_discovers_name_and_gets_key_from_dashboard( hass: HomeAssistant, mock_client, mock_dashboard, - mock_zeroconf: None, mock_setup_entry: None, ) -> None: """Test user step can discover the name and get the key from the dashboard.""" @@ -418,12 +424,12 @@ async def test_user_discovers_name_and_gets_key_from_dashboard( "dashboard_exception", [aiohttp.ClientError(), json.JSONDecodeError("test", "test", 0)], ) +@pytest.mark.usefixtures("mock_zeroconf") async def test_user_discovers_name_and_gets_key_from_dashboard_fails( hass: HomeAssistant, dashboard_exception: Exception, mock_client, mock_dashboard, - mock_zeroconf: None, mock_setup_entry: None, ) -> None: """Test user step can discover the name and get the key from the dashboard.""" @@ -474,11 +480,11 @@ async def test_user_discovers_name_and_gets_key_from_dashboard_fails( assert mock_client.noise_psk == VALID_NOISE_PSK +@pytest.mark.usefixtures("mock_zeroconf") async def test_user_discovers_name_and_dashboard_is_unavailable( hass: HomeAssistant, mock_client, mock_dashboard, - mock_zeroconf: None, mock_setup_entry: None, ) -> None: """Test user step can discover the name but the dashboard is unavailable.""" @@ -529,8 +535,9 @@ async def test_user_discovers_name_and_dashboard_is_unavailable( assert mock_client.noise_psk == VALID_NOISE_PSK +@pytest.mark.usefixtures("mock_zeroconf") async def test_login_connection_error( - hass: HomeAssistant, mock_client, mock_zeroconf: None, mock_setup_entry: None + hass: HomeAssistant, mock_client, mock_setup_entry: None ) -> None: """Test user step with connection error on login attempt.""" mock_client.device_info.return_value = DeviceInfo(uses_password=True, name="test") @@ -555,8 +562,9 @@ async def test_login_connection_error( assert result["errors"] == {"base": "connection_error"} +@pytest.mark.usefixtures("mock_zeroconf") async def test_discovery_initiation( - hass: HomeAssistant, mock_client, mock_zeroconf: None, mock_setup_entry: None + hass: HomeAssistant, mock_client, mock_setup_entry: None ) -> None: """Test discovery importing works.""" service_info = zeroconf.ZeroconfServiceInfo( @@ -587,8 +595,9 @@ async def test_discovery_initiation( assert result["result"].unique_id == "11:22:33:44:55:aa" +@pytest.mark.usefixtures("mock_zeroconf") async def test_discovery_no_mac( - hass: HomeAssistant, mock_client, mock_zeroconf: None, mock_setup_entry: None + hass: HomeAssistant, mock_client, mock_setup_entry: None ) -> None: """Test discovery aborted if old ESPHome without mac in zeroconf.""" service_info = zeroconf.ZeroconfServiceInfo( @@ -694,8 +703,9 @@ async def test_discovery_updates_unique_id( assert entry.unique_id == "11:22:33:44:55:aa" +@pytest.mark.usefixtures("mock_zeroconf") async def test_user_requires_psk( - hass: HomeAssistant, mock_client, mock_zeroconf: None, mock_setup_entry: None + hass: HomeAssistant, mock_client, mock_setup_entry: None ) -> None: """Test user step with requiring encryption key.""" mock_client.device_info.side_effect = RequiresEncryptionAPIError @@ -715,8 +725,9 @@ async def test_user_requires_psk( assert len(mock_client.disconnect.mock_calls) == 2 +@pytest.mark.usefixtures("mock_zeroconf") async def test_encryption_key_valid_psk( - hass: HomeAssistant, mock_client, mock_zeroconf: None, mock_setup_entry: None + hass: HomeAssistant, mock_client, mock_setup_entry: None ) -> None: """Test encryption key step with valid key.""" @@ -749,8 +760,9 @@ async def test_encryption_key_valid_psk( assert mock_client.noise_psk == VALID_NOISE_PSK +@pytest.mark.usefixtures("mock_zeroconf") async def test_encryption_key_invalid_psk( - hass: HomeAssistant, mock_client, mock_zeroconf: None, mock_setup_entry: None + hass: HomeAssistant, mock_client, mock_setup_entry: None ) -> None: """Test encryption key step with invalid key.""" @@ -776,9 +788,8 @@ async def test_encryption_key_invalid_psk( assert mock_client.noise_psk == INVALID_NOISE_PSK -async def test_reauth_initiation( - hass: HomeAssistant, mock_client, mock_zeroconf: None -) -> None: +@pytest.mark.usefixtures("mock_zeroconf") +async def test_reauth_initiation(hass: HomeAssistant, mock_client) -> None: """Test reauth initiation shows form.""" entry = MockConfigEntry( domain=DOMAIN, @@ -798,8 +809,9 @@ async def test_reauth_initiation( assert result["step_id"] == "reauth_confirm" +@pytest.mark.usefixtures("mock_zeroconf") async def test_reauth_confirm_valid( - hass: HomeAssistant, mock_client, mock_zeroconf: None, mock_setup_entry: None + hass: HomeAssistant, mock_client, mock_setup_entry: None ) -> None: """Test reauth initiation with valid PSK.""" entry = MockConfigEntry( @@ -827,10 +839,10 @@ async def test_reauth_confirm_valid( assert entry.data[CONF_NOISE_PSK] == VALID_NOISE_PSK +@pytest.mark.usefixtures("mock_zeroconf") async def test_reauth_fixed_via_dashboard( hass: HomeAssistant, mock_client, - mock_zeroconf: None, mock_dashboard, mock_setup_entry: None, ) -> None: @@ -878,10 +890,10 @@ async def test_reauth_fixed_via_dashboard( assert len(mock_get_encryption_key.mock_calls) == 1 +@pytest.mark.usefixtures("mock_zeroconf") async def test_reauth_fixed_via_dashboard_add_encryption_remove_password( hass: HomeAssistant, mock_client, - mock_zeroconf: None, mock_dashboard, mock_config_entry, mock_setup_entry: None, @@ -946,10 +958,10 @@ async def test_reauth_fixed_via_remove_password( assert mock_config_entry.data[CONF_PASSWORD] == "" +@pytest.mark.usefixtures("mock_zeroconf") async def test_reauth_fixed_via_dashboard_at_confirm( hass: HomeAssistant, mock_client, - mock_zeroconf: None, mock_dashboard, mock_setup_entry: None, ) -> None: @@ -1003,8 +1015,9 @@ async def test_reauth_fixed_via_dashboard_at_confirm( assert len(mock_get_encryption_key.mock_calls) == 1 +@pytest.mark.usefixtures("mock_zeroconf") async def test_reauth_confirm_invalid( - hass: HomeAssistant, mock_client, mock_zeroconf: None, mock_setup_entry: None + hass: HomeAssistant, mock_client, mock_setup_entry: None ) -> None: """Test reauth initiation with invalid PSK.""" entry = MockConfigEntry( @@ -1044,8 +1057,9 @@ async def test_reauth_confirm_invalid( assert entry.data[CONF_NOISE_PSK] == VALID_NOISE_PSK +@pytest.mark.usefixtures("mock_zeroconf") async def test_reauth_confirm_invalid_with_unique_id( - hass: HomeAssistant, mock_client, mock_zeroconf: None, mock_setup_entry: None + hass: HomeAssistant, mock_client, mock_setup_entry: None ) -> None: """Test reauth initiation with invalid PSK.""" entry = MockConfigEntry( @@ -1166,10 +1180,10 @@ async def test_discovery_hassio(hass: HomeAssistant, mock_dashboard) -> None: assert dash.addon_slug == "mock-slug" +@pytest.mark.usefixtures("mock_zeroconf") async def test_zeroconf_encryption_key_via_dashboard( hass: HomeAssistant, mock_client, - mock_zeroconf: None, mock_dashboard, mock_setup_entry: None, ) -> None: @@ -1232,10 +1246,10 @@ async def test_zeroconf_encryption_key_via_dashboard( assert mock_client.noise_psk == VALID_NOISE_PSK +@pytest.mark.usefixtures("mock_zeroconf") async def test_zeroconf_encryption_key_via_dashboard_with_api_encryption_prop( hass: HomeAssistant, mock_client, - mock_zeroconf: None, mock_dashboard, mock_setup_entry: None, ) -> None: @@ -1298,10 +1312,10 @@ async def test_zeroconf_encryption_key_via_dashboard_with_api_encryption_prop( assert mock_client.noise_psk == VALID_NOISE_PSK +@pytest.mark.usefixtures("mock_zeroconf") async def test_zeroconf_no_encryption_key_via_dashboard( hass: HomeAssistant, mock_client, - mock_zeroconf: None, mock_dashboard, mock_setup_entry: None, ) -> None: @@ -1375,10 +1389,10 @@ async def test_option_flow( assert len(mock_reload.mock_calls) == int(option_value) +@pytest.mark.usefixtures("mock_zeroconf") async def test_user_discovers_name_no_dashboard( hass: HomeAssistant, mock_client, - mock_zeroconf: None, mock_setup_entry: None, ) -> None: """Test user step can discover the name and the there is not dashboard.""" @@ -1434,22 +1448,25 @@ async def mqtt_discovery_test_abort(hass: HomeAssistant, payload: str, reason: s assert flow["reason"] == reason +@pytest.mark.usefixtures("mock_zeroconf") async def test_discovery_mqtt_no_mac( - hass: HomeAssistant, mock_client, mock_zeroconf: None, mock_setup_entry: None + hass: HomeAssistant, mock_client, mock_setup_entry: None ) -> None: """Test discovery aborted if mac is missing in MQTT payload.""" await mqtt_discovery_test_abort(hass, "{}", "mqtt_missing_mac") +@pytest.mark.usefixtures("mock_zeroconf") async def test_discovery_mqtt_no_api( - hass: HomeAssistant, mock_client, mock_zeroconf: None, mock_setup_entry: None + hass: HomeAssistant, mock_client, mock_setup_entry: None ) -> None: """Test discovery aborted if api/port is missing in MQTT payload.""" await mqtt_discovery_test_abort(hass, '{"mac":"abcdef123456"}', "mqtt_missing_api") +@pytest.mark.usefixtures("mock_zeroconf") async def test_discovery_mqtt_no_ip( - hass: HomeAssistant, mock_client, mock_zeroconf: None, mock_setup_entry: None + hass: HomeAssistant, mock_client, mock_setup_entry: None ) -> None: """Test discovery aborted if ip is missing in MQTT payload.""" await mqtt_discovery_test_abort( @@ -1457,8 +1474,9 @@ async def test_discovery_mqtt_no_ip( ) +@pytest.mark.usefixtures("mock_zeroconf") async def test_discovery_mqtt_initiation( - hass: HomeAssistant, mock_client, mock_zeroconf: None, mock_setup_entry: None + hass: HomeAssistant, mock_client, mock_setup_entry: None ) -> None: """Test discovery importing works.""" service_info = MqttServiceInfo( diff --git a/tests/components/esphome/test_init.py b/tests/components/esphome/test_init.py index 7e008cde212..9e4c9709e7d 100644 --- a/tests/components/esphome/test_init.py +++ b/tests/components/esphome/test_init.py @@ -1,5 +1,7 @@ """ESPHome set up tests.""" +import pytest + from homeassistant.components.esphome import DOMAIN from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT from homeassistant.core import HomeAssistant @@ -7,9 +9,8 @@ from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry -async def test_delete_entry( - hass: HomeAssistant, mock_client, mock_zeroconf: None -) -> None: +@pytest.mark.usefixtures("mock_zeroconf") +async def test_delete_entry(hass: HomeAssistant, mock_client) -> None: """Test we can delete an entry with error.""" entry = MockConfigEntry( domain=DOMAIN, diff --git a/tests/components/esphome/test_manager.py b/tests/components/esphome/test_manager.py index a63f60e4dcb..c17ff9a7d8c 100644 --- a/tests/components/esphome/test_manager.py +++ b/tests/components/esphome/test_manager.py @@ -354,9 +354,8 @@ async def test_esphome_device_with_current_bluetooth( ) -async def test_unique_id_updated_to_mac( - hass: HomeAssistant, mock_client, mock_zeroconf: None -) -> None: +@pytest.mark.usefixtures("mock_zeroconf") +async def test_unique_id_updated_to_mac(hass: HomeAssistant, mock_client) -> None: """Test we update config entry unique ID to MAC address.""" entry = MockConfigEntry( domain=DOMAIN, @@ -384,8 +383,9 @@ async def test_unique_id_updated_to_mac( assert entry.unique_id == "11:22:33:44:55:aa" +@pytest.mark.usefixtures("mock_zeroconf") async def test_unique_id_not_updated_if_name_same_and_already_mac( - hass: HomeAssistant, mock_client: APIClient, mock_zeroconf: None + hass: HomeAssistant, mock_client: APIClient ) -> None: """Test we never update the entry unique ID event if the name is the same.""" entry = MockConfigEntry( @@ -418,8 +418,9 @@ async def test_unique_id_not_updated_if_name_same_and_already_mac( assert entry.unique_id == "11:22:33:44:55:aa" +@pytest.mark.usefixtures("mock_zeroconf") async def test_unique_id_updated_if_name_unset_and_already_mac( - hass: HomeAssistant, mock_client: APIClient, mock_zeroconf: None + hass: HomeAssistant, mock_client: APIClient ) -> None: """Test we never update config entry unique ID even if the name is unset.""" entry = MockConfigEntry( @@ -447,8 +448,9 @@ async def test_unique_id_updated_if_name_unset_and_already_mac( assert entry.unique_id == "11:22:33:44:55:aa" +@pytest.mark.usefixtures("mock_zeroconf") async def test_unique_id_not_updated_if_name_different_and_already_mac( - hass: HomeAssistant, mock_client: APIClient, mock_zeroconf: None + hass: HomeAssistant, mock_client: APIClient ) -> None: """Test we do not update config entry unique ID if the name is different.""" entry = MockConfigEntry( @@ -483,8 +485,9 @@ async def test_unique_id_not_updated_if_name_different_and_already_mac( assert entry.data[CONF_DEVICE_NAME] == "test" +@pytest.mark.usefixtures("mock_zeroconf") async def test_name_updated_only_if_mac_matches( - hass: HomeAssistant, mock_client: APIClient, mock_zeroconf: None + hass: HomeAssistant, mock_client: APIClient ) -> None: """Test we update config entry name only if the mac matches.""" entry = MockConfigEntry( @@ -517,8 +520,9 @@ async def test_name_updated_only_if_mac_matches( assert entry.data[CONF_DEVICE_NAME] == "new" +@pytest.mark.usefixtures("mock_zeroconf") async def test_name_updated_only_if_mac_was_unset( - hass: HomeAssistant, mock_client: APIClient, mock_zeroconf: None + hass: HomeAssistant, mock_client: APIClient ) -> None: """Test we update config entry name if the old unique id was not a mac.""" entry = MockConfigEntry( @@ -551,10 +555,10 @@ async def test_name_updated_only_if_mac_was_unset( assert entry.data[CONF_DEVICE_NAME] == "new" +@pytest.mark.usefixtures("mock_zeroconf") async def test_connection_aborted_wrong_device( hass: HomeAssistant, mock_client: APIClient, - mock_zeroconf: None, caplog: pytest.LogCaptureFixture, ) -> None: """Test we abort the connection if the unique id is a mac and neither name or mac match.""" @@ -615,10 +619,10 @@ async def test_connection_aborted_wrong_device( assert "Unexpected device found at" not in caplog.text +@pytest.mark.usefixtures("mock_zeroconf") async def test_failure_during_connect( hass: HomeAssistant, mock_client: APIClient, - mock_zeroconf: None, caplog: pytest.LogCaptureFixture, ) -> None: """Test we disconnect when there is a failure during connection setup.""" diff --git a/tests/components/zeroconf/test_usage.py b/tests/components/zeroconf/test_usage.py index 9f5b68c2956..e79f2319915 100644 --- a/tests/components/zeroconf/test_usage.py +++ b/tests/components/zeroconf/test_usage.py @@ -15,11 +15,9 @@ from tests.common import extract_stack_to_frame DOMAIN = "zeroconf" +@pytest.mark.usefixtures("mock_async_zeroconf", "mock_zeroconf") async def test_multiple_zeroconf_instances( - hass: HomeAssistant, - mock_async_zeroconf: None, - mock_zeroconf: None, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: """Test creating multiple zeroconf throws without an integration.""" assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) @@ -34,11 +32,9 @@ async def test_multiple_zeroconf_instances( assert "Zeroconf" in caplog.text +@pytest.mark.usefixtures("mock_async_zeroconf", "mock_zeroconf") async def test_multiple_zeroconf_instances_gives_shared( - hass: HomeAssistant, - mock_async_zeroconf: None, - mock_zeroconf: None, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: """Test creating multiple zeroconf gives the shared instance to an integration.""" assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) From 6c15351c183f59047e6af7037b1603d0d5d324ee Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 7 Jun 2024 20:59:26 +0200 Subject: [PATCH 0366/1445] Add support for common references in strings.json (#118783) * Add support for common references in strings.json * Update tests --- homeassistant/components/light/strings.json | 172 ++++++++++++-------- script/hassfest/translations.py | 1 + tests/helpers/test_translation.py | 4 +- 3 files changed, 107 insertions(+), 70 deletions(-) diff --git a/homeassistant/components/light/strings.json b/homeassistant/components/light/strings.json index fbabaff4584..f17044d4d74 100644 --- a/homeassistant/components/light/strings.json +++ b/homeassistant/components/light/strings.json @@ -1,5 +1,41 @@ { "title": "Light", + "common": { + "field_brightness_description": "Number indicating brightness, where 0 turns the light off, 1 is the minimum brightness, and 255 is the maximum brightness.", + "field_brightness_name": "Brightness value", + "field_brightness_pct_description": "Number indicating the percentage of full brightness, where 0 turns the light off, 1 is the minimum brightness, and 100 is the maximum brightness.", + "field_brightness_pct_name": "Brightness", + "field_brightness_step_description": "Change brightness by an amount.", + "field_brightness_step_name": "Brightness step value", + "field_brightness_step_pct_description": "Change brightness by a percentage.", + "field_brightness_step_pct_name": "Brightness step", + "field_color_name_description": "A human-readable color name.", + "field_color_name_name": "Color name", + "field_color_temp_description": "Color temperature in mireds.", + "field_color_temp_name": "Color temperature", + "field_effect_description": "Light effect.", + "field_effect_name": "Effect", + "field_flash_description": "Tell light to flash, can be either value short or long.", + "field_flash_name": "Flash", + "field_hs_color_description": "Color in hue/sat format. A list of two integers. Hue is 0-360 and Sat is 0-100.", + "field_hs_color_name": "Hue/Sat color", + "field_kelvin_description": "Color temperature in Kelvin.", + "field_kelvin_name": "Color temperature", + "field_profile_description": "Name of a light profile to use.", + "field_profile_name": "Profile", + "field_rgb_color_description": "The color in RGB format. A list of three integers between 0 and 255 representing the values of red, green, and blue.", + "field_rgb_color_name": "Color", + "field_rgbw_color_description": "The color in RGBW format. A list of four integers between 0 and 255 representing the values of red, green, blue, and white.", + "field_rgbw_color_name": "RGBW-color", + "field_rgbww_color_description": "The color in RGBWW format. A list of five integers between 0 and 255 representing the values of red, green, blue, cold white, and warm white.", + "field_rgbww_color_name": "RGBWW-color", + "field_transition_description": "Duration it takes to get to next state.", + "field_transition_name": "Transition", + "field_white_description": "Set the light to white mode.", + "field_white_name": "White", + "field_xy_color_description": "Color in XY-format. A list of two decimal numbers between 0 and 1.", + "field_xy_color_name": "XY-color" + }, "device_automation": { "action_type": { "brightness_decrease": "Decrease {entity_name} brightness", @@ -247,72 +283,72 @@ "description": "Turn on one or more lights and adjust properties of the light, even when they are turned on already.", "fields": { "transition": { - "name": "Transition", - "description": "Duration it takes to get to next state." + "name": "[%key:component::light::common::field_transition_name%]", + "description": "[%key:component::light::common::field_transition_description%]" }, "rgb_color": { - "name": "Color", - "description": "The color in RGB format. A list of three integers between 0 and 255 representing the values of red, green, and blue." + "name": "[%key:component::light::common::field_rgb_color_name%]", + "description": "[%key:component::light::common::field_rgb_color_description%]" }, "rgbw_color": { - "name": "RGBW-color", - "description": "The color in RGBW format. A list of four integers between 0 and 255 representing the values of red, green, blue, and white." + "name": "[%key:component::light::common::field_rgbw_color_name%]", + "description": "[%key:component::light::common::field_rgbw_color_description%]" }, "rgbww_color": { - "name": "RGBWW-color", - "description": "The color in RGBWW format. A list of five integers between 0 and 255 representing the values of red, green, blue, cold white, and warm white." + "name": "[%key:component::light::common::field_rgbww_color_name%]", + "description": "[%key:component::light::common::field_rgbww_color_description%]" }, "color_name": { - "name": "Color name", - "description": "A human-readable color name." + "name": "[%key:component::light::common::field_color_name_name%]", + "description": "[%key:component::light::common::field_color_name_description%]" }, "hs_color": { - "name": "Hue/Sat color", - "description": "Color in hue/sat format. A list of two integers. Hue is 0-360 and Sat is 0-100." + "name": "[%key:component::light::common::field_hs_color_name%]", + "description": "[%key:component::light::common::field_hs_color_description%]" }, "xy_color": { - "name": "XY-color", - "description": "Color in XY-format. A list of two decimal numbers between 0 and 1." + "name": "[%key:component::light::common::field_xy_color_name%]", + "description": "[%key:component::light::common::field_xy_color_description%]" }, "color_temp": { - "name": "Color temperature", - "description": "Color temperature in mireds." + "name": "[%key:component::light::common::field_color_temp_name%]", + "description": "[%key:component::light::common::field_color_temp_description%]" }, "kelvin": { - "name": "Color temperature", - "description": "Color temperature in Kelvin." + "name": "[%key:component::light::common::field_kelvin_name%]", + "description": "[%key:component::light::common::field_kelvin_description%]" }, "brightness": { - "name": "Brightness value", - "description": "Number indicating brightness, where 0 turns the light off, 1 is the minimum brightness, and 255 is the maximum brightness." + "name": "[%key:component::light::common::field_brightness_name%]", + "description": "[%key:component::light::common::field_brightness_description%]" }, "brightness_pct": { - "name": "Brightness", - "description": "Number indicating the percentage of full brightness, where 0 turns the light off, 1 is the minimum brightness, and 100 is the maximum brightness." + "name": "[%key:component::light::common::field_brightness_pct_name%]", + "description": "[%key:component::light::common::field_brightness_pct_description%]" }, "brightness_step": { - "name": "Brightness step value", - "description": "Change brightness by an amount." + "name": "[%key:component::light::common::field_brightness_step_name%]", + "description": "[%key:component::light::common::field_brightness_step_description%]" }, "brightness_step_pct": { - "name": "Brightness step", - "description": "Change brightness by a percentage." + "name": "[%key:component::light::common::field_brightness_step_pct_name%]", + "description": "[%key:component::light::common::field_brightness_step_pct_description%]" }, "white": { - "name": "White", - "description": "Set the light to white mode." + "name": "[%key:component::light::common::field_white_name%]", + "description": "[%key:component::light::common::field_white_description%]" }, "profile": { - "name": "Profile", - "description": "Name of a light profile to use." + "name": "[%key:component::light::common::field_profile_name%]", + "description": "[%key:component::light::common::field_profile_description%]" }, "flash": { - "name": "Flash", - "description": "Tell light to flash, can be either value short or long." + "name": "[%key:component::light::common::field_flash_name%]", + "description": "[%key:component::light::common::field_flash_description%]" }, "effect": { - "name": "Effect", - "description": "Light effect." + "name": "[%key:component::light::common::field_effect_name%]", + "description": "[%key:component::light::common::field_effect_description%]" } } }, @@ -321,12 +357,12 @@ "description": "Turn off one or more lights.", "fields": { "transition": { - "name": "[%key:component::light::services::turn_on::fields::transition::name%]", - "description": "[%key:component::light::services::turn_on::fields::transition::description%]" + "name": "[%key:component::light::common::field_transition_name%]", + "description": "[%key:component::light::common::field_transition_description%]" }, "flash": { - "name": "[%key:component::light::services::turn_on::fields::flash::name%]", - "description": "[%key:component::light::services::turn_on::fields::flash::description%]" + "name": "[%key:component::light::common::field_flash_name%]", + "description": "[%key:component::light::common::field_flash_description%]" } } }, @@ -335,64 +371,64 @@ "description": "Toggles one or more lights, from on to off, or, off to on, based on their current state.", "fields": { "transition": { - "name": "[%key:component::light::services::turn_on::fields::transition::name%]", - "description": "[%key:component::light::services::turn_on::fields::transition::description%]" + "name": "[%key:component::light::common::field_transition_name%]", + "description": "[%key:component::light::common::field_transition_description%]" }, "rgb_color": { - "name": "[%key:component::light::services::turn_on::fields::rgb_color::name%]", - "description": "[%key:component::light::services::turn_on::fields::rgb_color::description%]" + "name": "[%key:component::light::common::field_rgb_color_name%]", + "description": "[%key:component::light::common::field_rgb_color_description%]" }, "rgbw_color": { - "name": "[%key:component::light::services::turn_on::fields::rgbw_color::name%]", - "description": "[%key:component::light::services::turn_on::fields::rgbw_color::description%]" + "name": "[%key:component::light::common::field_rgbw_color_name%]", + "description": "[%key:component::light::common::field_rgbw_color_description%]" }, "rgbww_color": { - "name": "[%key:component::light::services::turn_on::fields::rgbww_color::name%]", - "description": "[%key:component::light::services::turn_on::fields::rgbww_color::description%]" + "name": "[%key:component::light::common::field_rgbww_color_name%]", + "description": "[%key:component::light::common::field_rgbww_color_description%]" }, "color_name": { - "name": "[%key:component::light::services::turn_on::fields::color_name::name%]", - "description": "[%key:component::light::services::turn_on::fields::color_name::description%]" + "name": "[%key:component::light::common::field_color_name_name%]", + "description": "[%key:component::light::common::field_color_name_description%]" }, "hs_color": { - "name": "[%key:component::light::services::turn_on::fields::hs_color::name%]", - "description": "[%key:component::light::services::turn_on::fields::hs_color::description%]" + "name": "[%key:component::light::common::field_hs_color_name%]", + "description": "[%key:component::light::common::field_hs_color_description%]" }, "xy_color": { - "name": "[%key:component::light::services::turn_on::fields::xy_color::name%]", - "description": "[%key:component::light::services::turn_on::fields::xy_color::description%]" + "name": "[%key:component::light::common::field_xy_color_name%]", + "description": "[%key:component::light::common::field_xy_color_description%]" }, "color_temp": { - "name": "[%key:component::light::services::turn_on::fields::color_temp::name%]", - "description": "[%key:component::light::services::turn_on::fields::color_temp::description%]" + "name": "[%key:component::light::common::field_color_temp_name%]", + "description": "[%key:component::light::common::field_color_temp_description%]" }, "kelvin": { - "name": "[%key:component::light::services::turn_on::fields::kelvin::name%]", - "description": "[%key:component::light::services::turn_on::fields::kelvin::description%]" + "name": "[%key:component::light::common::field_kelvin_name%]", + "description": "[%key:component::light::common::field_kelvin_description%]" }, "brightness": { - "name": "[%key:component::light::services::turn_on::fields::brightness::name%]", - "description": "[%key:component::light::services::turn_on::fields::brightness::description%]" + "name": "[%key:component::light::common::field_brightness_name%]", + "description": "[%key:component::light::common::field_brightness_description%]" }, "brightness_pct": { - "name": "[%key:component::light::services::turn_on::fields::brightness_pct::name%]", - "description": "[%key:component::light::services::turn_on::fields::brightness_pct::description%]" + "name": "[%key:component::light::common::field_brightness_pct_name%]", + "description": "[%key:component::light::common::field_brightness_pct_description%]" }, "white": { - "name": "[%key:component::light::services::turn_on::fields::white::name%]", - "description": "[%key:component::light::services::turn_on::fields::white::description%]" + "name": "[%key:component::light::common::field_white_name%]", + "description": "[%key:component::light::common::field_white_description%]" }, "profile": { - "name": "[%key:component::light::services::turn_on::fields::profile::name%]", - "description": "[%key:component::light::services::turn_on::fields::profile::description%]" + "name": "[%key:component::light::common::field_profile_name%]", + "description": "[%key:component::light::common::field_profile_description%]" }, "flash": { - "name": "[%key:component::light::services::turn_on::fields::flash::name%]", - "description": "[%key:component::light::services::turn_on::fields::flash::description%]" + "name": "[%key:component::light::common::field_flash_name%]", + "description": "[%key:component::light::common::field_flash_description%]" }, "effect": { - "name": "[%key:component::light::services::turn_on::fields::effect::name%]", - "description": "[%key:component::light::services::turn_on::fields::effect::description%]" + "name": "[%key:component::light::common::field_effect_name%]", + "description": "[%key:component::light::common::field_effect_description%]" } } } diff --git a/script/hassfest/translations.py b/script/hassfest/translations.py index e815a66b4bb..c508f4ee36e 100644 --- a/script/hassfest/translations.py +++ b/script/hassfest/translations.py @@ -375,6 +375,7 @@ def gen_strings_schema(config: Config, integration: Integration) -> vol.Schema: vol.Required("done"): translation_value_validator, }, }, + vol.Optional("common"): vol.Schema({cv.slug: translation_value_validator}), } ) diff --git a/tests/helpers/test_translation.py b/tests/helpers/test_translation.py index d1df7004c99..dfe96562a4a 100644 --- a/tests/helpers/test_translation.py +++ b/tests/helpers/test_translation.py @@ -426,10 +426,10 @@ async def test_caching(hass: HomeAssistant) -> None: side_effect=translation.build_resources, ) as mock_build_resources: load1 = await translation.async_get_translations(hass, "en", "entity_component") - assert len(mock_build_resources.mock_calls) == 5 + assert len(mock_build_resources.mock_calls) == 6 load2 = await translation.async_get_translations(hass, "en", "entity_component") - assert len(mock_build_resources.mock_calls) == 5 + assert len(mock_build_resources.mock_calls) == 6 assert load1 == load2 From 20df747806ab7f9f6f5ee5e223428aa638c0118f Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 7 Jun 2024 21:28:02 +0200 Subject: [PATCH 0367/1445] Use fixtures in UniFi device tracker tests (#118912) --- tests/components/unifi/conftest.py | 22 + tests/components/unifi/test_device_tracker.py | 1160 +++++++++-------- 2 files changed, 636 insertions(+), 546 deletions(-) diff --git a/tests/components/unifi/conftest.py b/tests/components/unifi/conftest.py index 2ea772b5173..5fdeb1889fe 100644 --- a/tests/components/unifi/conftest.py +++ b/tests/components/unifi/conftest.py @@ -12,6 +12,7 @@ from unittest.mock import patch from aiounifi.models.message import MessageKey import pytest +from homeassistant.components.unifi import STORAGE_KEY, STORAGE_VERSION from homeassistant.components.unifi.const import CONF_SITE_ID, DOMAIN as UNIFI_DOMAIN from homeassistant.components.unifi.hub.websocket import RETRY_TIMER from homeassistant.config_entries import ConfigEntry @@ -111,6 +112,27 @@ def config_entry_options_fixture() -> MappingProxyType[str, Any]: return {} +# Known wireless clients + + +@pytest.fixture(name="known_wireless_clients") +def known_wireless_clients_fixture() -> list[str]: + """Known previously observed wireless clients.""" + return [] + + +@pytest.fixture(autouse=True) +def mock_wireless_client_storage(hass_storage, known_wireless_clients: list[str]): + """Mock the known wireless storage.""" + data: dict[str, list[str]] = ( + {"wireless_clients": known_wireless_clients} if known_wireless_clients else {} + ) + hass_storage[STORAGE_KEY] = {"version": STORAGE_VERSION, "data": data} + + +# UniFi request mocks + + @pytest.fixture(name="mock_requests") def request_fixture( aioclient_mock: AiohttpClientMocker, diff --git a/tests/components/unifi/test_device_tracker.py b/tests/components/unifi/test_device_tracker.py index 4037d976430..1bc4c4ff632 100644 --- a/tests/components/unifi/test_device_tracker.py +++ b/tests/components/unifi/test_device_tracker.py @@ -1,9 +1,12 @@ """The tests for the UniFi Network device tracker platform.""" +from collections.abc import Callable from datetime import timedelta +from typing import Any from aiounifi.models.message import MessageKey from freezegun.api import FrozenDateTimeFactory, freeze_time +import pytest from homeassistant.components.device_tracker import DOMAIN as TRACKER_DOMAIN from homeassistant.components.unifi.const import ( @@ -18,51 +21,44 @@ from homeassistant.components.unifi.const import ( DEFAULT_DETECTION_TIME, DOMAIN as UNIFI_DOMAIN, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_HOME, STATE_NOT_HOME, STATE_UNAVAILABLE from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er import homeassistant.util.dt as dt_util -from .test_hub import ENTRY_CONFIG, setup_unifi_integration - -from tests.common import MockConfigEntry, async_fire_time_changed -from tests.test_util.aiohttp import AiohttpClientMocker - - -async def test_no_entities( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker -) -> None: - """Test the update_clients function when no clients are found.""" - await setup_unifi_integration(hass, aioclient_mock) - - assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 0 +from tests.common import async_fire_time_changed +@pytest.mark.parametrize( + "client_payload", + [ + [ + { + "ap_mac": "00:00:00:00:02:01", + "essid": "ssid", + "hostname": "client", + "ip": "10.0.0.1", + "is_wired": False, + "last_seen": 1562600145, + "mac": "00:00:00:00:00:01", + } + ] + ], +) +@pytest.mark.usefixtures("mock_device_registry") async def test_tracked_wireless_clients( hass: HomeAssistant, - aioclient_mock: AiohttpClientMocker, - mock_device_registry, mock_unifi_websocket, + config_entry_setup: ConfigEntry, + client_payload: list[dict[str, Any]], ) -> None: """Verify tracking of wireless clients.""" - client = { - "ap_mac": "00:00:00:00:02:01", - "essid": "ssid", - "hostname": "client", - "ip": "10.0.0.1", - "is_wired": False, - "last_seen": 1562600145, - "mac": "00:00:00:00:00:01", - } - config_entry = await setup_unifi_integration( - hass, aioclient_mock, clients_response=[client] - ) - assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 1 assert hass.states.get("device_tracker.client").state == STATE_NOT_HOME # Updated timestamp marks client as home - + client = client_payload[0] client["last_seen"] = dt_util.as_timestamp(dt_util.utcnow()) mock_unifi_websocket(message=MessageKey.CLIENT, data=client) await hass.async_block_till_done() @@ -70,9 +66,10 @@ async def test_tracked_wireless_clients( assert hass.states.get("device_tracker.client").state == STATE_HOME # Change time to mark client as away - new_time = dt_util.utcnow() + timedelta( - seconds=config_entry.options.get(CONF_DETECTION_TIME, DEFAULT_DETECTION_TIME) + seconds=config_entry_setup.options.get( + CONF_DETECTION_TIME, DEFAULT_DETECTION_TIME + ) ) with freeze_time(new_time): async_fire_time_changed(hass, new_time) @@ -81,76 +78,77 @@ async def test_tracked_wireless_clients( assert hass.states.get("device_tracker.client").state == STATE_NOT_HOME # Same timestamp doesn't explicitly mark client as away - mock_unifi_websocket(message=MessageKey.CLIENT, data=client) await hass.async_block_till_done() assert hass.states.get("device_tracker.client").state == STATE_HOME +@pytest.mark.parametrize( + "config_entry_options", + [{CONF_SSID_FILTER: ["ssid"], CONF_CLIENT_SOURCE: ["00:00:00:00:00:06"]}], +) +@pytest.mark.parametrize( + "client_payload", + [ + [ + { + "ap_mac": "00:00:00:00:02:01", + "essid": "ssid", + "hostname": "client_1", + "ip": "10.0.0.1", + "is_wired": False, + "last_seen": 1562600145, + "mac": "00:00:00:00:00:01", + }, + { + "ip": "10.0.0.2", + "is_wired": True, + "last_seen": 1562600145, + "mac": "00:00:00:00:00:02", + "name": "Client 2", + }, + { + "essid": "ssid2", + "hostname": "client_3", + "ip": "10.0.0.3", + "is_wired": False, + "last_seen": 1562600145, + "mac": "00:00:00:00:00:03", + }, + { + "essid": "ssid", + "hostname": "client_4", + "ip": "10.0.0.4", + "is_wired": True, + "last_seen": dt_util.as_timestamp(dt_util.utcnow()), + "mac": "00:00:00:00:00:04", + }, + { + "essid": "ssid", + "hostname": "client_5", + "ip": "10.0.0.5", + "is_wired": True, + "last_seen": None, + "mac": "00:00:00:00:00:05", + }, + { + "hostname": "client_6", + "ip": "10.0.0.6", + "is_wired": True, + "last_seen": 1562600145, + "mac": "00:00:00:00:00:06", + }, + ] + ], +) +@pytest.mark.parametrize("known_wireless_clients", [["00:00:00:00:00:04"]]) +@pytest.mark.usefixtures("config_entry_setup") +@pytest.mark.usefixtures("mock_device_registry") async def test_tracked_clients( - hass: HomeAssistant, - aioclient_mock: AiohttpClientMocker, - mock_unifi_websocket, - mock_device_registry, + hass: HomeAssistant, mock_unifi_websocket, client_payload: list[dict[str, Any]] ) -> None: """Test the update_items function with some clients.""" - client_1 = { - "ap_mac": "00:00:00:00:02:01", - "essid": "ssid", - "hostname": "client_1", - "ip": "10.0.0.1", - "is_wired": False, - "last_seen": 1562600145, - "mac": "00:00:00:00:00:01", - } - client_2 = { - "ip": "10.0.0.2", - "is_wired": True, - "last_seen": 1562600145, - "mac": "00:00:00:00:00:02", - "name": "Client 2", - } - client_3 = { - "essid": "ssid2", - "hostname": "client_3", - "ip": "10.0.0.3", - "is_wired": False, - "last_seen": 1562600145, - "mac": "00:00:00:00:00:03", - } - client_4 = { - "essid": "ssid", - "hostname": "client_4", - "ip": "10.0.0.4", - "is_wired": True, - "last_seen": dt_util.as_timestamp(dt_util.utcnow()), - "mac": "00:00:00:00:00:04", - } - client_5 = { - "essid": "ssid", - "hostname": "client_5", - "ip": "10.0.0.5", - "is_wired": True, - "last_seen": None, - "mac": "00:00:00:00:00:05", - } - client_6 = { - "hostname": "client_6", - "ip": "10.0.0.6", - "is_wired": True, - "last_seen": 1562600145, - "mac": "00:00:00:00:00:06", - } - - await setup_unifi_integration( - hass, - aioclient_mock, - options={CONF_SSID_FILTER: ["ssid"], CONF_CLIENT_SOURCE: [client_6["mac"]]}, - clients_response=[client_1, client_2, client_3, client_4, client_5, client_6], - known_wireless_clients=(client_4["mac"],), - ) - assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 5 assert hass.states.get("device_tracker.client_1").state == STATE_NOT_HOME assert hass.states.get("device_tracker.client_2").state == STATE_NOT_HOME @@ -170,6 +168,7 @@ async def test_tracked_clients( # State change signalling works + client_1 = client_payload[0] client_1["last_seen"] = dt_util.as_timestamp(dt_util.utcnow()) mock_unifi_websocket(message=MessageKey.CLIENT, data=client_1) await hass.async_block_till_done() @@ -177,34 +176,38 @@ async def test_tracked_clients( assert hass.states.get("device_tracker.client_1").state == STATE_HOME +@pytest.mark.parametrize( + "client_payload", + [ + [ + { + "ap_mac": "00:00:00:00:02:01", + "essid": "ssid", + "hostname": "client", + "ip": "10.0.0.1", + "is_wired": False, + "last_seen": 1562600145, + "mac": "00:00:00:00:00:01", + } + ] + ], +) +@pytest.mark.usefixtures("mock_device_registry") async def test_tracked_wireless_clients_event_source( hass: HomeAssistant, - aioclient_mock: AiohttpClientMocker, freezer: FrozenDateTimeFactory, mock_unifi_websocket, - mock_device_registry, + config_entry_setup: ConfigEntry, + client_payload: list[dict[str, Any]], ) -> None: """Verify tracking of wireless clients based on event source.""" - client = { - "ap_mac": "00:00:00:00:02:01", - "essid": "ssid", - "hostname": "client", - "ip": "10.0.0.1", - "is_wired": False, - "last_seen": 1562600145, - "mac": "00:00:00:00:00:01", - } - config_entry = await setup_unifi_integration( - hass, aioclient_mock, clients_response=[client] - ) - assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 1 assert hass.states.get("device_tracker.client").state == STATE_NOT_HOME # State change signalling works with events # Connected event - + client = client_payload[0] event = { "user": client["mac"], "ssid": client["essid"], @@ -217,7 +220,10 @@ async def test_tracked_wireless_clients_event_source( "site_id": "name", "time": 1587753456179, "datetime": "2020-04-24T18:37:36Z", - "msg": f'User{[client["mac"]]} has connected to AP[{client["ap_mac"]}] with SSID "{client["essid"]}" on "channel 44(na)"', + "msg": ( + f'User{[client["mac"]]} has connected to AP[{client["ap_mac"]}] ' + f'with SSID "{client["essid"]}" on "channel 44(na)"' + ), "_id": "5ea331fa30c49e00f90ddc1a", } mock_unifi_websocket(message=MessageKey.EVENT, data=event) @@ -225,7 +231,6 @@ async def test_tracked_wireless_clients_event_source( assert hass.states.get("device_tracker.client").state == STATE_HOME # Disconnected event - event = { "user": client["mac"], "ssid": client["essid"], @@ -238,7 +243,10 @@ async def test_tracked_wireless_clients_event_source( "site_id": "name", "time": 1587752927000, "datetime": "2020-04-24T18:28:47Z", - "msg": f'User{[client["mac"]]} disconnected from "{client["essid"]}" (7m 47s connected, 448.28K bytes, last AP[{client["ap_mac"]}])', + "msg": ( + f'User{[client["mac"]]} disconnected from "{client["essid"]}" ' + f'(7m 47s connected, 448.28K bytes, last AP[{client["ap_mac"]}])' + ), "_id": "5ea32ff730c49e00f90dca1a", } mock_unifi_websocket(message=MessageKey.EVENT, data=event) @@ -249,7 +257,9 @@ async def test_tracked_wireless_clients_event_source( freezer.tick( timedelta( seconds=( - config_entry.options.get(CONF_DETECTION_TIME, DEFAULT_DETECTION_TIME) + config_entry_setup.options.get( + CONF_DETECTION_TIME, DEFAULT_DETECTION_TIME + ) + 1 ) ) @@ -264,14 +274,12 @@ async def test_tracked_wireless_clients_event_source( # once real data is received events will be ignored. # New data - client["last_seen"] = dt_util.as_timestamp(dt_util.utcnow()) mock_unifi_websocket(message=MessageKey.CLIENT, data=client) await hass.async_block_till_done() assert hass.states.get("device_tracker.client").state == STATE_HOME # Disconnection event will be ignored - event = { "user": client["mac"], "ssid": client["essid"], @@ -284,7 +292,10 @@ async def test_tracked_wireless_clients_event_source( "site_id": "name", "time": 1587752927000, "datetime": "2020-04-24T18:28:47Z", - "msg": f'User{[client["mac"]]} disconnected from "{client["essid"]}" (7m 47s connected, 448.28K bytes, last AP[{client["ap_mac"]}])', + "msg": ( + f'User{[client["mac"]]} disconnected from "{client["essid"]}" ' + f'(7m 47s connected, 448.28K bytes, last AP[{client["ap_mac"]}])' + ), "_id": "5ea32ff730c49e00f90dca1a", } mock_unifi_websocket(message=MessageKey.EVENT, data=event) @@ -295,7 +306,9 @@ async def test_tracked_wireless_clients_event_source( freezer.tick( timedelta( seconds=( - config_entry.options.get(CONF_DETECTION_TIME, DEFAULT_DETECTION_TIME) + config_entry_setup.options.get( + CONF_DETECTION_TIME, DEFAULT_DETECTION_TIME + ) + 1 ) ) @@ -306,57 +319,60 @@ async def test_tracked_wireless_clients_event_source( assert hass.states.get("device_tracker.client").state == STATE_NOT_HOME +@pytest.mark.parametrize( + "device_payload", + [ + [ + { + "board_rev": 3, + "device_id": "mock-id", + "has_fan": True, + "fan_level": 0, + "ip": "10.0.1.1", + "last_seen": 1562600145, + "mac": "00:00:00:00:01:01", + "model": "US16P150", + "name": "Device 1", + "next_interval": 20, + "overheating": True, + "state": 1, + "type": "usw", + "upgradable": True, + "version": "4.0.42.10433", + }, + { + "board_rev": 3, + "device_id": "mock-id", + "has_fan": True, + "ip": "10.0.1.2", + "mac": "00:00:00:00:01:02", + "model": "US16P150", + "name": "Device 2", + "next_interval": 20, + "state": 0, + "type": "usw", + "version": "4.0.42.10433", + }, + ] + ], +) +@pytest.mark.usefixtures("config_entry_setup") +@pytest.mark.usefixtures("mock_device_registry") async def test_tracked_devices( hass: HomeAssistant, - aioclient_mock: AiohttpClientMocker, freezer: FrozenDateTimeFactory, mock_unifi_websocket, - mock_device_registry, + device_payload: list[dict[str, Any]], ) -> None: """Test the update_items function with some devices.""" - device_1 = { - "board_rev": 3, - "device_id": "mock-id", - "has_fan": True, - "fan_level": 0, - "ip": "10.0.1.1", - "last_seen": 1562600145, - "mac": "00:00:00:00:01:01", - "model": "US16P150", - "name": "Device 1", - "next_interval": 20, - "overheating": True, - "state": 1, - "type": "usw", - "upgradable": True, - "version": "4.0.42.10433", - } - device_2 = { - "board_rev": 3, - "device_id": "mock-id", - "has_fan": True, - "ip": "10.0.1.2", - "mac": "00:00:00:00:01:02", - "model": "US16P150", - "name": "Device 2", - "next_interval": 20, - "state": 0, - "type": "usw", - "version": "4.0.42.10433", - } - await setup_unifi_integration( - hass, - aioclient_mock, - devices_response=[device_1, device_2], - ) - assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 2 assert hass.states.get("device_tracker.device_1").state == STATE_HOME assert hass.states.get("device_tracker.device_2").state == STATE_NOT_HOME # State change signalling work - + device_1 = device_payload[0] device_1["next_interval"] = 20 + device_2 = device_payload[1] device_2["state"] = 1 device_2["next_interval"] = 50 mock_unifi_websocket(message=MessageKey.DEVICE, data=[device_1, device_2]) @@ -366,7 +382,6 @@ async def test_tracked_devices( assert hass.states.get("device_tracker.device_2").state == STATE_HOME # Change of time can mark device not_home outside of expected reporting interval - new_time = dt_util.utcnow() + timedelta(seconds=90) freezer.move_to(new_time) async_fire_time_changed(hass, new_time) @@ -376,7 +391,6 @@ async def test_tracked_devices( assert hass.states.get("device_tracker.device_2").state == STATE_HOME # Disabled device is unavailable - device_1["disabled"] = True mock_unifi_websocket(message=MessageKey.DEVICE, data=device_1) await hass.async_block_till_done() @@ -385,37 +399,38 @@ async def test_tracked_devices( assert hass.states.get("device_tracker.device_2").state == STATE_HOME +@pytest.mark.parametrize( + "client_payload", + [ + [ + { + "essid": "ssid", + "hostname": "client_1", + "is_wired": False, + "last_seen": 1562600145, + "mac": "00:00:00:00:00:01", + }, + { + "hostname": "client_2", + "is_wired": True, + "last_seen": 1562600145, + "mac": "00:00:00:00:00:02", + }, + ] + ], +) +@pytest.mark.usefixtures("config_entry_setup") +@pytest.mark.usefixtures("mock_device_registry") async def test_remove_clients( - hass: HomeAssistant, - aioclient_mock: AiohttpClientMocker, - mock_unifi_websocket, - mock_device_registry, + hass: HomeAssistant, mock_unifi_websocket, client_payload: list[dict[str, Any]] ) -> None: """Test the remove_items function with some clients.""" - client_1 = { - "essid": "ssid", - "hostname": "client_1", - "is_wired": False, - "last_seen": 1562600145, - "mac": "00:00:00:00:00:01", - } - client_2 = { - "hostname": "client_2", - "is_wired": True, - "last_seen": 1562600145, - "mac": "00:00:00:00:00:02", - } - await setup_unifi_integration( - hass, aioclient_mock, clients_response=[client_1, client_2] - ) - assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 2 assert hass.states.get("device_tracker.client_1") assert hass.states.get("device_tracker.client_2") # Remove client - - mock_unifi_websocket(message=MessageKey.CLIENT_REMOVED, data=client_1) + mock_unifi_websocket(message=MessageKey.CLIENT_REMOVED, data=client_payload[0]) await hass.async_block_till_done() await hass.async_block_till_done() @@ -424,45 +439,48 @@ async def test_remove_clients( assert hass.states.get("device_tracker.client_2") -async def test_hub_state_change( - hass: HomeAssistant, - aioclient_mock: AiohttpClientMocker, - websocket_mock, - mock_device_registry, -) -> None: +@pytest.mark.parametrize( + "client_payload", + [ + [ + { + "essid": "ssid", + "hostname": "client", + "is_wired": False, + "last_seen": 1562600145, + "mac": "00:00:00:00:00:01", + } + ] + ], +) +@pytest.mark.parametrize( + "device_payload", + [ + [ + { + "board_rev": 3, + "device_id": "mock-id", + "has_fan": True, + "fan_level": 0, + "ip": "10.0.1.1", + "last_seen": 1562600145, + "mac": "00:00:00:00:01:01", + "model": "US16P150", + "name": "Device", + "next_interval": 20, + "overheating": True, + "state": 1, + "type": "usw", + "upgradable": True, + "version": "4.0.42.10433", + } + ] + ], +) +@pytest.mark.usefixtures("config_entry_setup") +@pytest.mark.usefixtures("mock_device_registry") +async def test_hub_state_change(hass: HomeAssistant, websocket_mock) -> None: """Verify entities state reflect on hub connection becoming unavailable.""" - client = { - "essid": "ssid", - "hostname": "client", - "is_wired": False, - "last_seen": 1562600145, - "mac": "00:00:00:00:00:01", - } - device = { - "board_rev": 3, - "device_id": "mock-id", - "has_fan": True, - "fan_level": 0, - "ip": "10.0.1.1", - "last_seen": 1562600145, - "mac": "00:00:00:00:01:01", - "model": "US16P150", - "name": "Device", - "next_interval": 20, - "overheating": True, - "state": 1, - "type": "usw", - "upgradable": True, - "version": "4.0.42.10433", - } - - await setup_unifi_integration( - hass, - aioclient_mock, - clients_response=[client], - devices_response=[device], - ) - assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 2 assert hass.states.get("device_tracker.client").state == STATE_NOT_HOME assert hass.states.get("device_tracker.device").state == STATE_HOME @@ -478,47 +496,55 @@ async def test_hub_state_change( assert hass.states.get("device_tracker.device").state == STATE_HOME +@pytest.mark.parametrize( + "client_payload", + [ + [ + { + "essid": "ssid", + "hostname": "wireless_client", + "is_wired": False, + "last_seen": 1562600145, + "mac": "00:00:00:00:00:01", + }, + { + "is_wired": True, + "last_seen": 1562600145, + "mac": "00:00:00:00:00:02", + "name": "Wired Client", + }, + ] + ], +) +@pytest.mark.parametrize( + "device_payload", + [ + [ + { + "board_rev": 3, + "device_id": "mock-id", + "has_fan": True, + "fan_level": 0, + "ip": "10.0.1.1", + "last_seen": 1562600145, + "mac": "00:00:00:00:01:01", + "model": "US16P150", + "name": "Device", + "next_interval": 20, + "overheating": True, + "state": 1, + "type": "usw", + "upgradable": True, + "version": "4.0.42.10433", + } + ] + ], +) +@pytest.mark.usefixtures("mock_device_registry") async def test_option_track_clients( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, mock_device_registry + hass: HomeAssistant, config_entry_setup: ConfigEntry ) -> None: """Test the tracking of clients can be turned off.""" - wireless_client = { - "essid": "ssid", - "hostname": "wireless_client", - "is_wired": False, - "last_seen": 1562600145, - "mac": "00:00:00:00:00:01", - } - wired_client = { - "is_wired": True, - "last_seen": 1562600145, - "mac": "00:00:00:00:00:02", - "name": "Wired Client", - } - device = { - "board_rev": 3, - "device_id": "mock-id", - "has_fan": True, - "fan_level": 0, - "ip": "10.0.1.1", - "last_seen": 1562600145, - "mac": "00:00:00:00:01:01", - "model": "US16P150", - "name": "Device", - "next_interval": 20, - "overheating": True, - "state": 1, - "type": "usw", - "upgradable": True, - "version": "4.0.42.10433", - } - - config_entry = await setup_unifi_integration( - hass, - aioclient_mock, - clients_response=[wireless_client, wired_client], - devices_response=[device], - ) assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 3 assert hass.states.get("device_tracker.wireless_client") @@ -526,8 +552,7 @@ async def test_option_track_clients( assert hass.states.get("device_tracker.device") hass.config_entries.async_update_entry( - config_entry, - options={CONF_TRACK_CLIENTS: False}, + config_entry_setup, options={CONF_TRACK_CLIENTS: False} ) await hass.async_block_till_done() @@ -536,8 +561,7 @@ async def test_option_track_clients( assert hass.states.get("device_tracker.device") hass.config_entries.async_update_entry( - config_entry, - options={CONF_TRACK_CLIENTS: True}, + config_entry_setup, options={CONF_TRACK_CLIENTS: True} ) await hass.async_block_till_done() @@ -546,56 +570,62 @@ async def test_option_track_clients( assert hass.states.get("device_tracker.device") +@pytest.mark.parametrize( + "client_payload", + [ + [ + { + "essid": "ssid", + "hostname": "wireless_client", + "is_wired": False, + "last_seen": 1562600145, + "mac": "00:00:00:00:00:01", + }, + { + "is_wired": True, + "last_seen": 1562600145, + "mac": "00:00:00:00:00:02", + "name": "Wired Client", + }, + ] + ], +) +@pytest.mark.parametrize( + "device_payload", + [ + [ + { + "board_rev": 3, + "device_id": "mock-id", + "has_fan": True, + "fan_level": 0, + "ip": "10.0.1.1", + "last_seen": 1562600145, + "mac": "00:00:00:00:01:01", + "model": "US16P150", + "name": "Device", + "next_interval": 20, + "overheating": True, + "state": 1, + "type": "usw", + "upgradable": True, + "version": "4.0.42.10433", + } + ] + ], +) +@pytest.mark.usefixtures("mock_device_registry") async def test_option_track_wired_clients( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, mock_device_registry + hass: HomeAssistant, config_entry_setup: ConfigEntry ) -> None: """Test the tracking of wired clients can be turned off.""" - wireless_client = { - "essid": "ssid", - "hostname": "wireless_client", - "is_wired": False, - "last_seen": 1562600145, - "mac": "00:00:00:00:00:01", - } - wired_client = { - "is_wired": True, - "last_seen": 1562600145, - "mac": "00:00:00:00:00:02", - "name": "Wired Client", - } - device = { - "board_rev": 3, - "device_id": "mock-id", - "has_fan": True, - "fan_level": 0, - "ip": "10.0.1.1", - "last_seen": 1562600145, - "mac": "00:00:00:00:01:01", - "model": "US16P150", - "name": "Device", - "next_interval": 20, - "overheating": True, - "state": 1, - "type": "usw", - "upgradable": True, - "version": "4.0.42.10433", - } - - config_entry = await setup_unifi_integration( - hass, - aioclient_mock, - clients_response=[wireless_client, wired_client], - devices_response=[device], - ) - assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 3 assert hass.states.get("device_tracker.wireless_client") assert hass.states.get("device_tracker.wired_client") assert hass.states.get("device_tracker.device") hass.config_entries.async_update_entry( - config_entry, - options={CONF_TRACK_WIRED_CLIENTS: False}, + config_entry_setup, options={CONF_TRACK_WIRED_CLIENTS: False} ) await hass.async_block_till_done() @@ -604,8 +634,7 @@ async def test_option_track_wired_clients( assert hass.states.get("device_tracker.device") hass.config_entries.async_update_entry( - config_entry, - options={CONF_TRACK_WIRED_CLIENTS: True}, + config_entry_setup, options={CONF_TRACK_WIRED_CLIENTS: True} ) await hass.async_block_till_done() @@ -614,46 +643,52 @@ async def test_option_track_wired_clients( assert hass.states.get("device_tracker.device") +@pytest.mark.parametrize( + "client_payload", + [ + [ + { + "hostname": "client", + "is_wired": True, + "last_seen": 1562600145, + "mac": "00:00:00:00:00:01", + } + ] + ], +) +@pytest.mark.parametrize( + "device_payload", + [ + [ + { + "board_rev": 3, + "device_id": "mock-id", + "last_seen": 1562600145, + "ip": "10.0.1.1", + "mac": "00:00:00:00:01:01", + "model": "US16P150", + "name": "Device", + "next_interval": 20, + "overheating": True, + "state": 1, + "type": "usw", + "upgradable": True, + "version": "4.0.42.10433", + } + ] + ], +) +@pytest.mark.usefixtures("mock_device_registry") async def test_option_track_devices( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, mock_device_registry + hass: HomeAssistant, config_entry_setup: ConfigEntry ) -> None: """Test the tracking of devices can be turned off.""" - client = { - "hostname": "client", - "is_wired": True, - "last_seen": 1562600145, - "mac": "00:00:00:00:00:01", - } - device = { - "board_rev": 3, - "device_id": "mock-id", - "last_seen": 1562600145, - "ip": "10.0.1.1", - "mac": "00:00:00:00:01:01", - "model": "US16P150", - "name": "Device", - "next_interval": 20, - "overheating": True, - "state": 1, - "type": "usw", - "upgradable": True, - "version": "4.0.42.10433", - } - - config_entry = await setup_unifi_integration( - hass, - aioclient_mock, - clients_response=[client], - devices_response=[device], - ) - assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 2 assert hass.states.get("device_tracker.client") assert hass.states.get("device_tracker.device") hass.config_entries.async_update_entry( - config_entry, - options={CONF_TRACK_DEVICES: False}, + config_entry_setup, options={CONF_TRACK_DEVICES: False} ) await hass.async_block_till_done() @@ -661,8 +696,7 @@ async def test_option_track_devices( assert not hass.states.get("device_tracker.device") hass.config_entries.async_update_entry( - config_entry, - options={CONF_TRACK_DEVICES: True}, + config_entry_setup, options={CONF_TRACK_DEVICES: True} ) await hass.async_block_till_done() @@ -670,44 +704,46 @@ async def test_option_track_devices( assert hass.states.get("device_tracker.device") +@pytest.mark.parametrize( + "client_payload", + [ + [ + { + "essid": "ssid", + "hostname": "client", + "is_wired": False, + "last_seen": dt_util.as_timestamp(dt_util.utcnow()), + "mac": "00:00:00:00:00:01", + }, + { + "essid": "ssid2", + "hostname": "client_on_ssid2", + "is_wired": False, + "last_seen": 1562600145, + "mac": "00:00:00:00:00:02", + }, + ] + ], +) +@pytest.mark.usefixtures("mock_device_registry") async def test_option_ssid_filter( hass: HomeAssistant, - aioclient_mock: AiohttpClientMocker, mock_unifi_websocket, - mock_device_registry, + config_entry_setup: ConfigEntry, + client_payload: list[dict[str, Any]], ) -> None: """Test the SSID filter works. Client will travel from a supported SSID to an unsupported ssid. Client on SSID2 will be removed on change of options. """ - client = { - "essid": "ssid", - "hostname": "client", - "is_wired": False, - "last_seen": dt_util.as_timestamp(dt_util.utcnow()), - "mac": "00:00:00:00:00:01", - } - client_on_ssid2 = { - "essid": "ssid2", - "hostname": "client_on_ssid2", - "is_wired": False, - "last_seen": 1562600145, - "mac": "00:00:00:00:00:02", - } - - config_entry = await setup_unifi_integration( - hass, aioclient_mock, clients_response=[client, client_on_ssid2] - ) - assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 2 assert hass.states.get("device_tracker.client").state == STATE_HOME assert hass.states.get("device_tracker.client_on_ssid2").state == STATE_NOT_HOME # Setting SSID filter will remove clients outside of filter hass.config_entries.async_update_entry( - config_entry, - options={CONF_SSID_FILTER: ["ssid"]}, + config_entry_setup, options={CONF_SSID_FILTER: ["ssid"]} ) await hass.async_block_till_done() @@ -718,17 +754,20 @@ async def test_option_ssid_filter( assert not hass.states.get("device_tracker.client_on_ssid2") # Roams to SSID outside of filter + client = client_payload[0] client["essid"] = "other_ssid" mock_unifi_websocket(message=MessageKey.CLIENT, data=client) # Data update while SSID filter is in effect shouldn't create the client + client_on_ssid2 = client_payload[1] client_on_ssid2["last_seen"] = dt_util.as_timestamp(dt_util.utcnow()) mock_unifi_websocket(message=MessageKey.CLIENT, data=client_on_ssid2) await hass.async_block_till_done() new_time = dt_util.utcnow() + timedelta( seconds=( - config_entry.options.get(CONF_DETECTION_TIME, DEFAULT_DETECTION_TIME) + 1 + config_entry_setup.options.get(CONF_DETECTION_TIME, DEFAULT_DETECTION_TIME) + + 1 ) ) with freeze_time(new_time): @@ -743,8 +782,7 @@ async def test_option_ssid_filter( # Remove SSID filter hass.config_entries.async_update_entry( - config_entry, - options={CONF_SSID_FILTER: []}, + config_entry_setup, options={CONF_SSID_FILTER: []} ) await hass.async_block_till_done() @@ -757,10 +795,10 @@ async def test_option_ssid_filter( assert hass.states.get("device_tracker.client_on_ssid2").state == STATE_HOME # Time pass to mark client as away - new_time += timedelta( seconds=( - config_entry.options.get(CONF_DETECTION_TIME, DEFAULT_DETECTION_TIME) + 1 + config_entry_setup.options.get(CONF_DETECTION_TIME, DEFAULT_DETECTION_TIME) + + 1 ) ) with freeze_time(new_time): @@ -782,7 +820,9 @@ async def test_option_ssid_filter( await hass.async_block_till_done() new_time += timedelta( - seconds=(config_entry.options.get(CONF_DETECTION_TIME, DEFAULT_DETECTION_TIME)) + seconds=( + config_entry_setup.options.get(CONF_DETECTION_TIME, DEFAULT_DETECTION_TIME) + ) ) with freeze_time(new_time): async_fire_time_changed(hass, new_time) @@ -791,29 +831,32 @@ async def test_option_ssid_filter( assert hass.states.get("device_tracker.client_on_ssid2").state == STATE_NOT_HOME +@pytest.mark.parametrize( + "client_payload", + [ + [ + { + "essid": "ssid", + "hostname": "client", + "ip": "10.0.0.1", + "is_wired": False, + "last_seen": dt_util.as_timestamp(dt_util.utcnow()), + "mac": "00:00:00:00:00:01", + } + ] + ], +) +@pytest.mark.usefixtures("mock_device_registry") async def test_wireless_client_go_wired_issue( hass: HomeAssistant, - aioclient_mock: AiohttpClientMocker, mock_unifi_websocket, - mock_device_registry, + config_entry_setup: ConfigEntry, + client_payload: list[dict[str, Any]], ) -> None: """Test the solution to catch wireless device go wired UniFi issue. UniFi Network has a known issue that when a wireless device goes away it sometimes gets marked as wired. """ - client = { - "essid": "ssid", - "hostname": "client", - "ip": "10.0.0.1", - "is_wired": False, - "last_seen": dt_util.as_timestamp(dt_util.utcnow()), - "mac": "00:00:00:00:00:01", - } - - config_entry = await setup_unifi_integration( - hass, aioclient_mock, clients_response=[client] - ) - assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 1 # Client is wireless @@ -821,6 +864,7 @@ async def test_wireless_client_go_wired_issue( assert client_state.state == STATE_HOME # Trigger wired bug + client = client_payload[0] client["last_seen"] = dt_util.as_timestamp(dt_util.utcnow()) client["is_wired"] = True mock_unifi_websocket(message=MessageKey.CLIENT, data=client) @@ -832,7 +876,9 @@ async def test_wireless_client_go_wired_issue( # Pass time new_time = dt_util.utcnow() + timedelta( - seconds=(config_entry.options.get(CONF_DETECTION_TIME, DEFAULT_DETECTION_TIME)) + seconds=( + config_entry_setup.options.get(CONF_DETECTION_TIME, DEFAULT_DETECTION_TIME) + ) ) with freeze_time(new_time): async_fire_time_changed(hass, new_time) @@ -862,29 +908,31 @@ async def test_wireless_client_go_wired_issue( assert client_state.state == STATE_HOME +@pytest.mark.parametrize("config_entry_options", [{CONF_IGNORE_WIRED_BUG: True}]) +@pytest.mark.parametrize( + "client_payload", + [ + [ + { + "ap_mac": "00:00:00:00:02:01", + "essid": "ssid", + "hostname": "client", + "ip": "10.0.0.1", + "is_wired": False, + "last_seen": dt_util.as_timestamp(dt_util.utcnow()), + "mac": "00:00:00:00:00:01", + } + ] + ], +) +@pytest.mark.usefixtures("mock_device_registry") async def test_option_ignore_wired_bug( hass: HomeAssistant, - aioclient_mock: AiohttpClientMocker, mock_unifi_websocket, - mock_device_registry, + config_entry_setup: ConfigEntry, + client_payload: list[dict[str, Any]], ) -> None: """Test option to ignore wired bug.""" - client = { - "ap_mac": "00:00:00:00:02:01", - "essid": "ssid", - "hostname": "client", - "ip": "10.0.0.1", - "is_wired": False, - "last_seen": dt_util.as_timestamp(dt_util.utcnow()), - "mac": "00:00:00:00:00:01", - } - - config_entry = await setup_unifi_integration( - hass, - aioclient_mock, - options={CONF_IGNORE_WIRED_BUG: True}, - clients_response=[client], - ) assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 1 # Client is wireless @@ -892,6 +940,7 @@ async def test_option_ignore_wired_bug( assert client_state.state == STATE_HOME # Trigger wired bug + client = client_payload[0] client["is_wired"] = True mock_unifi_websocket(message=MessageKey.CLIENT, data=client) await hass.async_block_till_done() @@ -902,7 +951,9 @@ async def test_option_ignore_wired_bug( # pass time new_time = dt_util.utcnow() + timedelta( - seconds=config_entry.options.get(CONF_DETECTION_TIME, DEFAULT_DETECTION_TIME) + seconds=config_entry_setup.options.get( + CONF_DETECTION_TIME, DEFAULT_DETECTION_TIME + ) ) with freeze_time(new_time): async_fire_time_changed(hass, new_time) @@ -932,62 +983,67 @@ async def test_option_ignore_wired_bug( assert client_state.state == STATE_HOME +@pytest.mark.parametrize( + "config_entry_options", [{CONF_BLOCK_CLIENT: ["00:00:00:00:00:02"]}] +) +@pytest.mark.parametrize( + "client_payload", + [ + [ + { + "hostname": "client", + "is_wired": True, + "last_seen": 1562600145, + "mac": "00:00:00:00:00:01", + } + ] + ], +) +@pytest.mark.parametrize( + "clients_all_payload", + [ + [ + { + "hostname": "restored", + "is_wired": True, + "last_seen": 1562600145, + "mac": "00:00:00:00:00:02", + }, + { # Not previously seen by integration, will not be restored + "hostname": "not_restored", + "is_wired": True, + "last_seen": 1562600145, + "mac": "00:00:00:00:00:03", + }, + ] + ], +) +@pytest.mark.usefixtures("mock_device_registry") async def test_restoring_client( hass: HomeAssistant, entity_registry: er.EntityRegistry, - aioclient_mock: AiohttpClientMocker, - mock_device_registry, + config_entry: ConfigEntry, + config_entry_factory: Callable[[], ConfigEntry], + client_payload: list[dict[str, Any]], + clients_all_payload: list[dict[str, Any]], ) -> None: """Verify clients are restored from clients_all if they ever was registered to entity registry.""" - client = { - "hostname": "client", - "is_wired": True, - "last_seen": 1562600145, - "mac": "00:00:00:00:00:01", - } - restored = { - "hostname": "restored", - "is_wired": True, - "last_seen": 1562600145, - "mac": "00:00:00:00:00:02", - } - not_restored = { - "hostname": "not_restored", - "is_wired": True, - "last_seen": 1562600145, - "mac": "00:00:00:00:00:03", - } - - config_entry = MockConfigEntry( - domain=UNIFI_DOMAIN, - data=ENTRY_CONFIG, - source="test", - options={}, - entry_id="1", - ) - - entity_registry.async_get_or_create( # Unique ID updated + entity_registry.async_get_or_create( # Make sure unique ID converts to site_id-mac TRACKER_DOMAIN, UNIFI_DOMAIN, - f'{restored["mac"]}-site_id', - suggested_object_id=restored["hostname"], + f'{clients_all_payload[0]["mac"]}-site_id', + suggested_object_id=clients_all_payload[0]["hostname"], config_entry=config_entry, ) - entity_registry.async_get_or_create( # Unique ID already updated + entity_registry.async_get_or_create( # Unique ID already follow format site_id-mac TRACKER_DOMAIN, UNIFI_DOMAIN, - f'site_id-{client["mac"]}', - suggested_object_id=client["hostname"], + f'site_id-{client_payload[0]["mac"]}', + suggested_object_id=client_payload[0]["hostname"], config_entry=config_entry, ) - await setup_unifi_integration( - hass, - aioclient_mock, - options={CONF_BLOCK_CLIENT: [restored["mac"]]}, - clients_response=[client], - clients_all_response=[restored, not_restored], - ) + await config_entry_factory() assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 2 assert hass.states.get("device_tracker.client") @@ -995,59 +1051,65 @@ async def test_restoring_client( assert not hass.states.get("device_tracker.not_restored") +@pytest.mark.parametrize("config_entry_options", [{CONF_TRACK_CLIENTS: False}]) +@pytest.mark.parametrize( + "client_payload", + [ + [ + { + "essid": "ssid", + "hostname": "Wireless client", + "ip": "10.0.0.1", + "is_wired": False, + "last_seen": 1562600145, + "mac": "00:00:00:00:00:01", + }, + { + "hostname": "Wired client", + "ip": "10.0.0.2", + "is_wired": True, + "last_seen": 1562600145, + "mac": "00:00:00:00:00:02", + }, + ] + ], +) +@pytest.mark.parametrize( + "device_payload", + [ + [ + { + "board_rev": 3, + "device_id": "mock-id", + "has_fan": True, + "fan_level": 0, + "ip": "10.0.1.1", + "last_seen": 1562600145, + "mac": "00:00:00:00:01:01", + "model": "US16P150", + "name": "Device", + "next_interval": 20, + "overheating": True, + "state": 1, + "type": "usw", + "upgradable": True, + "version": "4.0.42.10433", + }, + ] + ], +) +@pytest.mark.usefixtures("mock_device_registry") async def test_dont_track_clients( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, mock_device_registry + hass: HomeAssistant, config_entry_setup: ConfigEntry ) -> None: """Test don't track clients config works.""" - wireless_client = { - "essid": "ssid", - "hostname": "Wireless client", - "ip": "10.0.0.1", - "is_wired": False, - "last_seen": 1562600145, - "mac": "00:00:00:00:00:01", - } - wired_client = { - "hostname": "Wired client", - "ip": "10.0.0.2", - "is_wired": True, - "last_seen": 1562600145, - "mac": "00:00:00:00:00:02", - } - device = { - "board_rev": 3, - "device_id": "mock-id", - "has_fan": True, - "fan_level": 0, - "ip": "10.0.1.1", - "last_seen": 1562600145, - "mac": "00:00:00:00:01:01", - "model": "US16P150", - "name": "Device", - "next_interval": 20, - "overheating": True, - "state": 1, - "type": "usw", - "upgradable": True, - "version": "4.0.42.10433", - } - - config_entry = await setup_unifi_integration( - hass, - aioclient_mock, - options={CONF_TRACK_CLIENTS: False}, - clients_response=[wireless_client, wired_client], - devices_response=[device], - ) - assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 1 assert not hass.states.get("device_tracker.wireless_client") assert not hass.states.get("device_tracker.wired_client") assert hass.states.get("device_tracker.device") hass.config_entries.async_update_entry( - config_entry, - options={CONF_TRACK_CLIENTS: True}, + config_entry_setup, options={CONF_TRACK_CLIENTS: True} ) await hass.async_block_till_done() @@ -1057,49 +1119,55 @@ async def test_dont_track_clients( assert hass.states.get("device_tracker.device") +@pytest.mark.parametrize("config_entry_options", [{CONF_TRACK_DEVICES: False}]) +@pytest.mark.parametrize( + "client_payload", + [ + [ + { + "hostname": "client", + "is_wired": True, + "last_seen": 1562600145, + "mac": "00:00:00:00:00:01", + }, + ] + ], +) +@pytest.mark.parametrize( + "device_payload", + [ + [ + { + "board_rev": 3, + "device_id": "mock-id", + "has_fan": True, + "fan_level": 0, + "ip": "10.0.1.1", + "last_seen": 1562600145, + "mac": "00:00:00:00:01:01", + "model": "US16P150", + "name": "Device", + "next_interval": 20, + "overheating": True, + "state": 1, + "type": "usw", + "upgradable": True, + "version": "4.0.42.10433", + }, + ] + ], +) +@pytest.mark.usefixtures("mock_device_registry") async def test_dont_track_devices( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, mock_device_registry + hass: HomeAssistant, config_entry_setup: ConfigEntry ) -> None: """Test don't track devices config works.""" - client = { - "hostname": "client", - "is_wired": True, - "last_seen": 1562600145, - "mac": "00:00:00:00:00:01", - } - device = { - "board_rev": 3, - "device_id": "mock-id", - "has_fan": True, - "fan_level": 0, - "ip": "10.0.1.1", - "last_seen": 1562600145, - "mac": "00:00:00:00:01:01", - "model": "US16P150", - "name": "Device", - "next_interval": 20, - "overheating": True, - "state": 1, - "type": "usw", - "upgradable": True, - "version": "4.0.42.10433", - } - - config_entry = await setup_unifi_integration( - hass, - aioclient_mock, - options={CONF_TRACK_DEVICES: False}, - clients_response=[client], - devices_response=[device], - ) - assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 1 assert hass.states.get("device_tracker.client") assert not hass.states.get("device_tracker.device") hass.config_entries.async_update_entry( - config_entry, - options={CONF_TRACK_DEVICES: True}, + config_entry_setup, options={CONF_TRACK_DEVICES: True} ) await hass.async_block_till_done() @@ -1108,38 +1176,38 @@ async def test_dont_track_devices( assert hass.states.get("device_tracker.device") +@pytest.mark.parametrize("config_entry_options", [{CONF_TRACK_WIRED_CLIENTS: False}]) +@pytest.mark.parametrize( + "client_payload", + [ + [ + { + "essid": "ssid", + "hostname": "Wireless Client", + "is_wired": False, + "last_seen": 1562600145, + "mac": "00:00:00:00:00:01", + }, + { + "is_wired": True, + "last_seen": 1562600145, + "mac": "00:00:00:00:00:02", + "name": "Wired Client", + }, + ] + ], +) +@pytest.mark.usefixtures("mock_device_registry") async def test_dont_track_wired_clients( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, mock_device_registry + hass: HomeAssistant, config_entry_setup: ConfigEntry ) -> None: """Test don't track wired clients config works.""" - wireless_client = { - "essid": "ssid", - "hostname": "Wireless Client", - "is_wired": False, - "last_seen": 1562600145, - "mac": "00:00:00:00:00:01", - } - wired_client = { - "is_wired": True, - "last_seen": 1562600145, - "mac": "00:00:00:00:00:02", - "name": "Wired Client", - } - - config_entry = await setup_unifi_integration( - hass, - aioclient_mock, - options={CONF_TRACK_WIRED_CLIENTS: False}, - clients_response=[wireless_client, wired_client], - ) - assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 1 assert hass.states.get("device_tracker.wireless_client") assert not hass.states.get("device_tracker.wired_client") hass.config_entries.async_update_entry( - config_entry, - options={CONF_TRACK_WIRED_CLIENTS: True}, + config_entry_setup, options={CONF_TRACK_WIRED_CLIENTS: True} ) await hass.async_block_till_done() From 00f78dc522af4b166f3dbe6a74563293bd37de3f Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Fri, 7 Jun 2024 22:06:51 +0200 Subject: [PATCH 0368/1445] Update typing-extensions to 4.12.2 (#119098) --- homeassistant/package_constraints.txt | 2 +- pyproject.toml | 2 +- requirements.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 0b05f400be0..05086aadd4b 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -55,7 +55,7 @@ pyudev==0.24.1 PyYAML==6.0.1 requests==2.32.3 SQLAlchemy==2.0.30 -typing-extensions>=4.12.1,<5.0 +typing-extensions>=4.12.2,<5.0 ulid-transform==0.9.0 urllib3>=1.26.5,<2 voluptuous-serialize==2.6.0 diff --git a/pyproject.toml b/pyproject.toml index ba234a1c1f1..23ebd376469 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -61,7 +61,7 @@ dependencies = [ "PyYAML==6.0.1", "requests==2.32.3", "SQLAlchemy==2.0.30", - "typing-extensions>=4.12.1,<5.0", + "typing-extensions>=4.12.2,<5.0", "ulid-transform==0.9.0", # Constrain urllib3 to ensure we deal with CVE-2020-26137 and CVE-2021-33503 # Temporary setting an upper bound, to prevent compat issues with urllib3>=2 diff --git a/requirements.txt b/requirements.txt index 781e15e5fbe..a81815a2651 100644 --- a/requirements.txt +++ b/requirements.txt @@ -36,7 +36,7 @@ python-slugify==8.0.4 PyYAML==6.0.1 requests==2.32.3 SQLAlchemy==2.0.30 -typing-extensions>=4.12.1,<5.0 +typing-extensions>=4.12.2,<5.0 ulid-transform==0.9.0 urllib3>=1.26.5,<2 voluptuous==0.13.1 From e4be3d8435c37177ca206232e1c765b97977d2bb Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Sat, 8 Jun 2024 06:11:35 +1000 Subject: [PATCH 0369/1445] Improve the reliability of tests in Tessie (#118596) --- .../components/tessie/config_flow.py | 6 +- tests/components/tessie/common.py | 12 +-- tests/components/tessie/conftest.py | 8 +- tests/components/tessie/test_config_flow.py | 101 ++++++++++++------ tests/components/tessie/test_init.py | 21 ++-- 5 files changed, 89 insertions(+), 59 deletions(-) diff --git a/homeassistant/components/tessie/config_flow.py b/homeassistant/components/tessie/config_flow.py index 7eb365a139f..f3761d4c4ce 100644 --- a/homeassistant/components/tessie/config_flow.py +++ b/homeassistant/components/tessie/config_flow.py @@ -93,13 +93,9 @@ class TessieConfigFlow(ConfigFlow, domain=DOMAIN): except ClientConnectionError: errors["base"] = "cannot_connect" else: - self.hass.config_entries.async_update_entry( + return self.async_update_reload_and_abort( self._reauth_entry, data=user_input ) - self.hass.async_create_task( - self.hass.config_entries.async_reload(self._reauth_entry.entry_id) - ) - return self.async_abort(reason="reauth_successful") return self.async_show_form( step_id="reauth_confirm", diff --git a/tests/components/tessie/common.py b/tests/components/tessie/common.py index 2f213c4e798..7182e28837a 100644 --- a/tests/components/tessie/common.py +++ b/tests/components/tessie/common.py @@ -7,6 +7,7 @@ from aiohttp import ClientConnectionError, ClientResponseError from aiohttp.client import RequestInfo from syrupy import SnapshotAssertion +from homeassistant.components.tessie import PLATFORMS from homeassistant.components.tessie.const import DOMAIN, TessieStatus from homeassistant.const import CONF_ACCESS_TOKEN, Platform from homeassistant.core import HomeAssistant @@ -47,7 +48,7 @@ ERROR_CONNECTION = ClientConnectionError() async def setup_platform( - hass: HomeAssistant, platforms: list[Platform] = [], side_effect=None + hass: HomeAssistant, platforms: list[Platform] = PLATFORMS ) -> MockConfigEntry: """Set up the Tessie platform.""" @@ -57,14 +58,7 @@ async def setup_platform( ) mock_entry.add_to_hass(hass) - with ( - patch( - "homeassistant.components.tessie.get_state_of_all_vehicles", - return_value=TEST_STATE_OF_ALL_VEHICLES, - side_effect=side_effect, - ), - patch("homeassistant.components.tessie.PLATFORMS", platforms), - ): + with patch("homeassistant.components.tessie.PLATFORMS", platforms): await hass.config_entries.async_setup(mock_entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/tessie/conftest.py b/tests/components/tessie/conftest.py index f38ef6c7e3f..77d1e3fd3e2 100644 --- a/tests/components/tessie/conftest.py +++ b/tests/components/tessie/conftest.py @@ -13,7 +13,7 @@ from .common import ( ) -@pytest.fixture +@pytest.fixture(autouse=True) def mock_get_state(): """Mock get_state function.""" with patch( @@ -23,7 +23,7 @@ def mock_get_state(): yield mock_get_state -@pytest.fixture +@pytest.fixture(autouse=True) def mock_get_status(): """Mock get_status function.""" with patch( @@ -33,11 +33,11 @@ def mock_get_status(): yield mock_get_status -@pytest.fixture +@pytest.fixture(autouse=True) def mock_get_state_of_all_vehicles(): """Mock get_state_of_all_vehicles function.""" with patch( - "homeassistant.components.tessie.config_flow.get_state_of_all_vehicles", + "homeassistant.components.tessie.get_state_of_all_vehicles", return_value=TEST_STATE_OF_ALL_VEHICLES, ) as mock_get_state_of_all_vehicles: yield mock_get_state_of_all_vehicles diff --git a/tests/components/tessie/test_config_flow.py b/tests/components/tessie/test_config_flow.py index ac3217f864b..f3dc98e6e18 100644 --- a/tests/components/tessie/test_config_flow.py +++ b/tests/components/tessie/test_config_flow.py @@ -6,7 +6,7 @@ import pytest from homeassistant import config_entries from homeassistant.components.tessie.const import DOMAIN -from homeassistant.const import CONF_ACCESS_TOKEN, Platform +from homeassistant.const import CONF_ACCESS_TOKEN from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -15,13 +15,37 @@ from .common import ( ERROR_CONNECTION, ERROR_UNKNOWN, TEST_CONFIG, - setup_platform, + TEST_STATE_OF_ALL_VEHICLES, ) from tests.common import MockConfigEntry -async def test_form(hass: HomeAssistant, mock_get_state_of_all_vehicles) -> None: +@pytest.fixture(autouse=True) +def mock_config_flow_get_state_of_all_vehicles(): + """Mock get_state_of_all_vehicles in config flow.""" + with patch( + "homeassistant.components.tessie.config_flow.get_state_of_all_vehicles", + return_value=TEST_STATE_OF_ALL_VEHICLES, + ) as mock_config_flow_get_state_of_all_vehicles: + yield mock_config_flow_get_state_of_all_vehicles + + +@pytest.fixture(autouse=True) +def mock_async_setup_entry(): + """Mock async_setup_entry.""" + with patch( + "homeassistant.components.tessie.async_setup_entry", + return_value=True, + ) as mock_async_setup_entry: + yield mock_async_setup_entry + + +async def test_form( + hass: HomeAssistant, + mock_config_flow_get_state_of_all_vehicles, + mock_async_setup_entry, +) -> None: """Test we get the form.""" result1 = await hass.config_entries.flow.async_init( @@ -30,17 +54,13 @@ async def test_form(hass: HomeAssistant, mock_get_state_of_all_vehicles) -> None assert result1["type"] is FlowResultType.FORM assert not result1["errors"] - with patch( - "homeassistant.components.tessie.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result2 = await hass.config_entries.flow.async_configure( - result1["flow_id"], - TEST_CONFIG, - ) - await hass.async_block_till_done() - assert len(mock_setup_entry.mock_calls) == 1 - assert len(mock_get_state_of_all_vehicles.mock_calls) == 1 + result2 = await hass.config_entries.flow.async_configure( + result1["flow_id"], + TEST_CONFIG, + ) + await hass.async_block_till_done() + assert len(mock_async_setup_entry.mock_calls) == 1 + assert len(mock_config_flow_get_state_of_all_vehicles.mock_calls) == 1 assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Tessie" @@ -56,7 +76,7 @@ async def test_form(hass: HomeAssistant, mock_get_state_of_all_vehicles) -> None ], ) async def test_form_errors( - hass: HomeAssistant, side_effect, error, mock_get_state_of_all_vehicles + hass: HomeAssistant, side_effect, error, mock_config_flow_get_state_of_all_vehicles ) -> None: """Test errors are handled.""" @@ -64,7 +84,7 @@ async def test_form_errors( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - mock_get_state_of_all_vehicles.side_effect = side_effect + mock_config_flow_get_state_of_all_vehicles.side_effect = side_effect result2 = await hass.config_entries.flow.async_configure( result1["flow_id"], TEST_CONFIG, @@ -74,15 +94,20 @@ async def test_form_errors( assert result2["errors"] == error # Complete the flow - mock_get_state_of_all_vehicles.side_effect = None + mock_config_flow_get_state_of_all_vehicles.side_effect = None result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], TEST_CONFIG, ) + assert "errors" not in result3 assert result3["type"] is FlowResultType.CREATE_ENTRY -async def test_reauth(hass: HomeAssistant, mock_get_state_of_all_vehicles) -> None: +async def test_reauth( + hass: HomeAssistant, + mock_config_flow_get_state_of_all_vehicles, + mock_async_setup_entry, +) -> None: """Test reauth flow.""" mock_entry = MockConfigEntry( @@ -104,17 +129,13 @@ async def test_reauth(hass: HomeAssistant, mock_get_state_of_all_vehicles) -> No assert result1["step_id"] == "reauth_confirm" assert not result1["errors"] - with patch( - "homeassistant.components.tessie.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result2 = await hass.config_entries.flow.async_configure( - result1["flow_id"], - TEST_CONFIG, - ) - await hass.async_block_till_done() - assert len(mock_setup_entry.mock_calls) == 1 - assert len(mock_get_state_of_all_vehicles.mock_calls) == 1 + result2 = await hass.config_entries.flow.async_configure( + result1["flow_id"], + TEST_CONFIG, + ) + await hass.async_block_till_done() + assert len(mock_async_setup_entry.mock_calls) == 1 + assert len(mock_config_flow_get_state_of_all_vehicles.mock_calls) == 1 assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" @@ -130,14 +151,23 @@ async def test_reauth(hass: HomeAssistant, mock_get_state_of_all_vehicles) -> No ], ) async def test_reauth_errors( - hass: HomeAssistant, mock_get_state_of_all_vehicles, side_effect, error + hass: HomeAssistant, + mock_config_flow_get_state_of_all_vehicles, + mock_async_setup_entry, + side_effect, + error, ) -> None: """Test reauth flows that fail.""" - mock_entry = await setup_platform(hass, [Platform.BINARY_SENSOR]) - mock_get_state_of_all_vehicles.side_effect = side_effect + mock_config_flow_get_state_of_all_vehicles.side_effect = side_effect - result = await hass.config_entries.flow.async_init( + mock_entry = MockConfigEntry( + domain=DOMAIN, + data=TEST_CONFIG, + ) + mock_entry.add_to_hass(hass) + + result1 = await hass.config_entries.flow.async_init( DOMAIN, context={ "source": config_entries.SOURCE_REAUTH, @@ -148,7 +178,7 @@ async def test_reauth_errors( ) result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], + result1["flow_id"], TEST_CONFIG, ) await hass.async_block_till_done() @@ -157,7 +187,7 @@ async def test_reauth_errors( assert result2["errors"] == error # Complete the flow - mock_get_state_of_all_vehicles.side_effect = None + mock_config_flow_get_state_of_all_vehicles.side_effect = None result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], TEST_CONFIG, @@ -166,3 +196,4 @@ async def test_reauth_errors( assert result3["type"] is FlowResultType.ABORT assert result3["reason"] == "reauth_successful" assert mock_entry.data == TEST_CONFIG + assert len(mock_async_setup_entry.mock_calls) == 1 diff --git a/tests/components/tessie/test_init.py b/tests/components/tessie/test_init.py index 68d6fcf7777..81d1d758edf 100644 --- a/tests/components/tessie/test_init.py +++ b/tests/components/tessie/test_init.py @@ -16,22 +16,31 @@ async def test_load_unload(hass: HomeAssistant) -> None: assert entry.state is ConfigEntryState.NOT_LOADED -async def test_auth_failure(hass: HomeAssistant) -> None: +async def test_auth_failure( + hass: HomeAssistant, mock_get_state_of_all_vehicles +) -> None: """Test init with an authentication error.""" - entry = await setup_platform(hass, side_effect=ERROR_AUTH) + mock_get_state_of_all_vehicles.side_effect = ERROR_AUTH + entry = await setup_platform(hass) assert entry.state is ConfigEntryState.SETUP_ERROR -async def test_unknown_failure(hass: HomeAssistant) -> None: +async def test_unknown_failure( + hass: HomeAssistant, mock_get_state_of_all_vehicles +) -> None: """Test init with an client response error.""" - entry = await setup_platform(hass, side_effect=ERROR_UNKNOWN) + mock_get_state_of_all_vehicles.side_effect = ERROR_UNKNOWN + entry = await setup_platform(hass) assert entry.state is ConfigEntryState.SETUP_ERROR -async def test_connection_failure(hass: HomeAssistant) -> None: +async def test_connection_failure( + hass: HomeAssistant, mock_get_state_of_all_vehicles +) -> None: """Test init with a network connection error.""" - entry = await setup_platform(hass, side_effect=ERROR_CONNECTION) + mock_get_state_of_all_vehicles.side_effect = ERROR_CONNECTION + entry = await setup_platform(hass) assert entry.state is ConfigEntryState.SETUP_RETRY From 5fdfafd57f1032a2a3ec75380d11fd26ccc5d70b Mon Sep 17 00:00:00 2001 From: tronikos Date: Fri, 7 Jun 2024 23:51:42 -0700 Subject: [PATCH 0370/1445] Catch GoogleAPICallError in Google Generative AI (#119118) --- .../components/google_generative_ai_conversation/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/google_generative_ai_conversation/__init__.py b/homeassistant/components/google_generative_ai_conversation/__init__.py index 523198355d1..f115f3923b6 100644 --- a/homeassistant/components/google_generative_ai_conversation/__init__.py +++ b/homeassistant/components/google_generative_ai_conversation/__init__.py @@ -71,7 +71,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: try: response = await model.generate_content_async(prompt_parts) except ( - ClientError, + GoogleAPICallError, ValueError, genai_types.BlockedPromptException, genai_types.StopCandidateException, From f07e7ec5433f6f809c38bd32b4135d4ca17c45e0 Mon Sep 17 00:00:00 2001 From: rwalker777 <49888088+rwalker777@users.noreply.github.com> Date: Sat, 8 Jun 2024 01:59:14 -0500 Subject: [PATCH 0371/1445] Add Tuya based bluetooth lights (#119103) --- homeassistant/components/led_ble/manifest.json | 3 +++ homeassistant/generated/bluetooth.py | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/homeassistant/components/led_ble/manifest.json b/homeassistant/components/led_ble/manifest.json index 9a496dbd049..ee5d0431fc8 100644 --- a/homeassistant/components/led_ble/manifest.json +++ b/homeassistant/components/led_ble/manifest.json @@ -25,6 +25,9 @@ }, { "local_name": "AP-*" + }, + { + "local_name": "MELK-*" } ], "codeowners": ["@bdraco"], diff --git a/homeassistant/generated/bluetooth.py b/homeassistant/generated/bluetooth.py index 03b40ad258f..17461225851 100644 --- a/homeassistant/generated/bluetooth.py +++ b/homeassistant/generated/bluetooth.py @@ -348,6 +348,10 @@ BLUETOOTH: Final[list[dict[str, bool | str | int | list[int]]]] = [ "domain": "led_ble", "local_name": "AP-*", }, + { + "domain": "led_ble", + "local_name": "MELK-*", + }, { "domain": "medcom_ble", "service_uuid": "39b31fec-b63a-4ef7-b163-a7317872007f", From f605c10f42fd6d521d6a64e75d531111b25812cd Mon Sep 17 00:00:00 2001 From: tronikos Date: Sat, 8 Jun 2024 00:02:00 -0700 Subject: [PATCH 0372/1445] Properly handle escaped unicode characters passed to tools in Google Generative AI (#119117) --- .../conversation.py | 16 +++++++--------- .../test_conversation.py | 18 ++++++++++++++++++ 2 files changed, 25 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/google_generative_ai_conversation/conversation.py b/homeassistant/components/google_generative_ai_conversation/conversation.py index 6c2bd64a7b5..65c0dc7fd93 100644 --- a/homeassistant/components/google_generative_ai_conversation/conversation.py +++ b/homeassistant/components/google_generative_ai_conversation/conversation.py @@ -2,6 +2,7 @@ from __future__ import annotations +import codecs from typing import Any, Literal from google.api_core.exceptions import GoogleAPICallError @@ -106,14 +107,14 @@ def _format_tool(tool: llm.Tool) -> dict[str, Any]: ) -def _adjust_value(value: Any) -> Any: - """Reverse unnecessary single quotes escaping.""" +def _escape_decode(value: Any) -> Any: + """Recursively call codecs.escape_decode on all values.""" if isinstance(value, str): - return value.replace("\\'", "'") + return codecs.escape_decode(bytes(value, "utf-8"))[0].decode("utf-8") # type: ignore[attr-defined] if isinstance(value, list): - return [_adjust_value(item) for item in value] + return [_escape_decode(item) for item in value] if isinstance(value, dict): - return {k: _adjust_value(v) for k, v in value.items()} + return {k: _escape_decode(v) for k, v in value.items()} return value @@ -334,10 +335,7 @@ class GoogleGenerativeAIConversationEntity( for function_call in function_calls: tool_call = MessageToDict(function_call._pb) # noqa: SLF001 tool_name = tool_call["name"] - tool_args = { - key: _adjust_value(value) - for key, value in tool_call["args"].items() - } + tool_args = _escape_decode(tool_call["args"]) LOGGER.debug("Tool call: %s(%s)", tool_name, tool_args) tool_input = llm.ToolInput(tool_name=tool_name, tool_args=tool_args) try: diff --git a/tests/components/google_generative_ai_conversation/test_conversation.py b/tests/components/google_generative_ai_conversation/test_conversation.py index 901216d262f..e84efffe7df 100644 --- a/tests/components/google_generative_ai_conversation/test_conversation.py +++ b/tests/components/google_generative_ai_conversation/test_conversation.py @@ -12,6 +12,9 @@ import voluptuous as vol from homeassistant.components import conversation from homeassistant.components.conversation import trace +from homeassistant.components.google_generative_ai_conversation.conversation import ( + _escape_decode, +) from homeassistant.const import CONF_LLM_HASS_API from homeassistant.core import Context, HomeAssistant from homeassistant.exceptions import HomeAssistantError @@ -504,3 +507,18 @@ async def test_conversation_agent( mock_config_entry.entry_id ) assert agent.supported_languages == "*" + + +async def test_escape_decode() -> None: + """Test _escape_decode.""" + assert _escape_decode( + { + "param1": ["test_value", "param1\\'s value"], + "param2": "param2\\'s value", + "param3": {"param31": "Cheminée", "param32": "Chemin\\303\\251e"}, + } + ) == { + "param1": ["test_value", "param1's value"], + "param2": "param2's value", + "param3": {"param31": "Cheminée", "param32": "Cheminée"}, + } From deac59f1ee538306eaa09d580683241eebf4513e Mon Sep 17 00:00:00 2001 From: t0bst4r <82281152+t0bst4r@users.noreply.github.com> Date: Sat, 8 Jun 2024 09:50:15 +0200 Subject: [PATCH 0373/1445] Add intelligent language matching for Google Assistant SDK Agents (#112600) Co-authored-by: Erik Montnemery --- .../google_assistant_sdk/__init__.py | 22 ++++++++-- .../google_assistant_sdk/helpers.py | 27 +++++++++++++ .../google_assistant_sdk/test_helpers.py | 40 +++++++++++++++++++ 3 files changed, 85 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/google_assistant_sdk/__init__.py b/homeassistant/components/google_assistant_sdk/__init__.py index b92b3c54579..4ea496f2824 100644 --- a/homeassistant/components/google_assistant_sdk/__init__.py +++ b/homeassistant/components/google_assistant_sdk/__init__.py @@ -26,11 +26,18 @@ from homeassistant.helpers.config_entry_oauth2_flow import ( ) from homeassistant.helpers.typing import ConfigType -from .const import DATA_MEM_STORAGE, DATA_SESSION, DOMAIN, SUPPORTED_LANGUAGE_CODES +from .const import ( + CONF_LANGUAGE_CODE, + DATA_MEM_STORAGE, + DATA_SESSION, + DOMAIN, + SUPPORTED_LANGUAGE_CODES, +) from .helpers import ( GoogleAssistantSDKAudioView, InMemoryStorage, async_send_text_commands, + best_matching_language_code, ) SERVICE_SEND_TEXT_COMMAND = "send_text_command" @@ -164,9 +171,16 @@ class GoogleAssistantConversationAgent(conversation.AbstractConversationAgent): if not session.valid_token: await session.async_ensure_token_valid() self.assistant = None - if not self.assistant or user_input.language != self.language: + + language = best_matching_language_code( + self.hass, + user_input.language, + self.entry.options.get(CONF_LANGUAGE_CODE), + ) + + if not self.assistant or language != self.language: credentials = Credentials(session.token[CONF_ACCESS_TOKEN]) # type: ignore[no-untyped-call] - self.language = user_input.language + self.language = language self.assistant = TextAssistant(credentials, self.language) resp = await self.hass.async_add_executor_job( @@ -174,7 +188,7 @@ class GoogleAssistantConversationAgent(conversation.AbstractConversationAgent): ) text_response = resp[0] or "" - intent_response = intent.IntentResponse(language=user_input.language) + intent_response = intent.IntentResponse(language=language) intent_response.async_set_speech(text_response) return conversation.ConversationResult( response=intent_response, conversation_id=user_input.conversation_id diff --git a/homeassistant/components/google_assistant_sdk/helpers.py b/homeassistant/components/google_assistant_sdk/helpers.py index 24da381e8e0..f9d332cd735 100644 --- a/homeassistant/components/google_assistant_sdk/helpers.py +++ b/homeassistant/components/google_assistant_sdk/helpers.py @@ -113,6 +113,33 @@ def default_language_code(hass: HomeAssistant) -> str: return DEFAULT_LANGUAGE_CODES.get(hass.config.language, "en-US") +def best_matching_language_code( + hass: HomeAssistant, assist_language: str, agent_language: str | None = None +) -> str: + """Get the best matching language, based on the preferred assist language and the configured agent language.""" + + # Use the assist language if supported + if assist_language in SUPPORTED_LANGUAGE_CODES: + return assist_language + language = assist_language.split("-")[0] + + # Use the agent language if assist and agent start with the same language part + if agent_language is not None and agent_language.startswith(language): + return best_matching_language_code(hass, agent_language) + + # If assist and agent are not matching, try to find the default language + default_language = DEFAULT_LANGUAGE_CODES.get(language) + if default_language is not None: + return default_language + + # If no default agent is available, use the agent language + if agent_language is not None: + return best_matching_language_code(hass, agent_language) + + # Fallback to the system default language + return default_language_code(hass) + + class InMemoryStorage: """Temporarily store and retrieve data from in memory storage.""" diff --git a/tests/components/google_assistant_sdk/test_helpers.py b/tests/components/google_assistant_sdk/test_helpers.py index 1090eb9da45..4632a86f40f 100644 --- a/tests/components/google_assistant_sdk/test_helpers.py +++ b/tests/components/google_assistant_sdk/test_helpers.py @@ -3,6 +3,7 @@ from homeassistant.components.google_assistant_sdk.const import SUPPORTED_LANGUAGE_CODES from homeassistant.components.google_assistant_sdk.helpers import ( DEFAULT_LANGUAGE_CODES, + best_matching_language_code, default_language_code, ) from homeassistant.core import HomeAssistant @@ -46,3 +47,42 @@ def test_default_language_code(hass: HomeAssistant) -> None: hass.config.language = "el" hass.config.country = "GR" assert default_language_code(hass) == "en-US" + + +def test_best_matching_language_code(hass: HomeAssistant) -> None: + """Test best_matching_language_code.""" + hass.config.language = "es" + hass.config.country = "MX" + + # Assist Language is supported + assert best_matching_language_code(hass, "de-DE", "en-AU") == "de-DE" + assert best_matching_language_code(hass, "de-DE") == "de-DE" + + # Assist Language is not supported, but agent language has the same "lang" part, and is supported + assert best_matching_language_code(hass, "en", "en-AU") == "en-AU" + assert best_matching_language_code(hass, "en-XYZ", "en-AU") == "en-AU" + # Assist Language is not supported, but agent language has the same "lang" part, but is not supported + assert best_matching_language_code(hass, "en", "en-XYZ") == "en-US" + assert best_matching_language_code(hass, "en-XYZ", "en-ABC") == "en-US" + + # Assist Language is not supported, agent is not matching or available, falling back to the default of assist lang + assert best_matching_language_code(hass, "de", "en-AU") == "de-DE" + assert best_matching_language_code(hass, "de-XYZ", "en-AU") == "de-DE" + assert best_matching_language_code(hass, "de") == "de-DE" + assert best_matching_language_code(hass, "de-XYZ") == "de-DE" + + # Assist language is not existing at all, agent is supported + assert best_matching_language_code(hass, "abc-XYZ", "en-AU") == "en-AU" + + # Assist language is not existing at all, agent is not supported, falling back to the agent default + assert best_matching_language_code(hass, "abc-XYZ", "de-XYZ") == "de-DE" + + # Assist language is not existing at all, agent is not existing or available, falling back to system default + assert best_matching_language_code(hass, "abc-XYZ", "def-XYZ") == "es-MX" + assert best_matching_language_code(hass, "abc-XYZ") == "es-MX" + + # Assist language is not existing at all, agent is not existing or available, system default is not supported + hass.config.language = "el" + hass.config.country = "GR" + assert best_matching_language_code(hass, "abc-XYZ", "def-XYZ") == "en-US" + assert best_matching_language_code(hass, "abc-XYZ") == "en-US" From 742dd61d36f0379c3bed8102fa23205aea33be3e Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Sat, 8 Jun 2024 11:44:37 +0300 Subject: [PATCH 0374/1445] Bump aioshelly to 10.0.1 (#119123) --- homeassistant/components/shelly/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/shelly/manifest.json b/homeassistant/components/shelly/manifest.json index 2e8c2d59c1e..b1b00e40c66 100644 --- a/homeassistant/components/shelly/manifest.json +++ b/homeassistant/components/shelly/manifest.json @@ -9,7 +9,7 @@ "iot_class": "local_push", "loggers": ["aioshelly"], "quality_scale": "platinum", - "requirements": ["aioshelly==10.0.0"], + "requirements": ["aioshelly==10.0.1"], "zeroconf": [ { "type": "_http._tcp.local.", diff --git a/requirements_all.txt b/requirements_all.txt index ff0a2be7cb8..b809725fa3a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -356,7 +356,7 @@ aioruuvigateway==0.1.0 aiosenz==1.0.0 # homeassistant.components.shelly -aioshelly==10.0.0 +aioshelly==10.0.1 # homeassistant.components.skybell aioskybell==22.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a7d963ee7d9..d640851996b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -329,7 +329,7 @@ aioruuvigateway==0.1.0 aiosenz==1.0.0 # homeassistant.components.shelly -aioshelly==10.0.0 +aioshelly==10.0.1 # homeassistant.components.skybell aioskybell==22.7.0 From ad2ff500de0efa6d12c58c2202c9e9f63f1dd322 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20Mind=C3=AAllo=20de=20Andrade?= Date: Sat, 8 Jun 2024 05:57:44 -0300 Subject: [PATCH 0375/1445] Bump sunweg to 3.0.1 (#118435) --- homeassistant/components/sunweg/manifest.json | 2 +- homeassistant/components/sunweg/sensor_types/total.py | 5 ----- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 3 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/sunweg/manifest.json b/homeassistant/components/sunweg/manifest.json index 3e41d331e8c..bcf1ad9dae2 100644 --- a/homeassistant/components/sunweg/manifest.json +++ b/homeassistant/components/sunweg/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/sunweg/", "iot_class": "cloud_polling", "loggers": ["sunweg"], - "requirements": ["sunweg==2.1.1"] + "requirements": ["sunweg==3.0.1"] } diff --git a/homeassistant/components/sunweg/sensor_types/total.py b/homeassistant/components/sunweg/sensor_types/total.py index 5ae8be6dba3..2b94446a165 100644 --- a/homeassistant/components/sunweg/sensor_types/total.py +++ b/homeassistant/components/sunweg/sensor_types/total.py @@ -41,11 +41,6 @@ TOTAL_SENSOR_TYPES: tuple[SunWEGSensorEntityDescription, ...] = ( state_class=SensorStateClass.TOTAL, never_resets=True, ), - SunWEGSensorEntityDescription( - key="kwh_per_kwp", - name="kWh por kWp", - api_variable_key="_kwh_per_kwp", - ), SunWEGSensorEntityDescription( key="last_update", name="Last Update", diff --git a/requirements_all.txt b/requirements_all.txt index b809725fa3a..b228624e9e9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2662,7 +2662,7 @@ subarulink==0.7.11 sunwatcher==0.2.1 # homeassistant.components.sunweg -sunweg==2.1.1 +sunweg==3.0.1 # homeassistant.components.surepetcare surepy==0.9.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d640851996b..30e719486f9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2078,7 +2078,7 @@ subarulink==0.7.11 sunwatcher==0.2.1 # homeassistant.components.sunweg -sunweg==2.1.1 +sunweg==3.0.1 # homeassistant.components.surepetcare surepy==0.9.0 From fff5715a063e34291801193b05e154ef61537264 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Sat, 8 Jun 2024 11:09:52 +0200 Subject: [PATCH 0376/1445] Require KNX boolean service descriptor selectors (#118597) --- homeassistant/components/knx/services.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/knx/services.yaml b/homeassistant/components/knx/services.yaml index 813bf758eb0..5eaaca25fd7 100644 --- a/homeassistant/components/knx/services.yaml +++ b/homeassistant/components/knx/services.yaml @@ -16,6 +16,7 @@ send: selector: text: response: + required: true default: false selector: boolean: @@ -40,6 +41,7 @@ event_register: text: remove: default: false + required: true selector: boolean: exposure_register: @@ -68,6 +70,7 @@ exposure_register: object: remove: default: false + required: true selector: boolean: reload: From 675048cc3838990bdf1b4ca0b80619cfff6a8e72 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sat, 8 Jun 2024 11:28:45 +0200 Subject: [PATCH 0377/1445] Bump aiowaqi to 3.1.0 (#119124) --- homeassistant/components/waqi/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/waqi/manifest.json b/homeassistant/components/waqi/manifest.json index d742fd72858..cb04bd7d6ac 100644 --- a/homeassistant/components/waqi/manifest.json +++ b/homeassistant/components/waqi/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/waqi", "iot_class": "cloud_polling", "loggers": ["aiowaqi"], - "requirements": ["aiowaqi==3.0.1"] + "requirements": ["aiowaqi==3.1.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index b228624e9e9..f27cd482fc0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -392,7 +392,7 @@ aiovlc==0.3.2 aiovodafone==0.6.0 # homeassistant.components.waqi -aiowaqi==3.0.1 +aiowaqi==3.1.0 # homeassistant.components.watttime aiowatttime==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 30e719486f9..3fffb5901c9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -365,7 +365,7 @@ aiovlc==0.3.2 aiovodafone==0.6.0 # homeassistant.components.waqi -aiowaqi==3.0.1 +aiowaqi==3.1.0 # homeassistant.components.watttime aiowatttime==0.1.1 From 522a1e9d56ce8571b730c9ddf2108d58969b5044 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sat, 8 Jun 2024 11:56:23 +0200 Subject: [PATCH 0378/1445] Add support for segmental measurements in Withings (#119126) --- homeassistant/components/withings/icons.json | 18 + homeassistant/components/withings/sensor.py | 135 +-- .../components/withings/strings.json | 45 + .../withings/fixtures/measurements.json | 8 + .../withings/snapshots/test_diagnostics.ambr | 12 + .../withings/snapshots/test_sensor.ambr | 810 ++++++++++++++++++ 6 files changed, 964 insertions(+), 64 deletions(-) diff --git a/homeassistant/components/withings/icons.json b/homeassistant/components/withings/icons.json index f76761ce953..f6fb5e74136 100644 --- a/homeassistant/components/withings/icons.json +++ b/homeassistant/components/withings/icons.json @@ -19,6 +19,24 @@ "hydration": { "default": "mdi:water" }, + "muscle_mass_for_segments_left_arm": { + "default": "mdi:arm-flex" + }, + "muscle_mass_for_segments_right_arm": { + "default": "mdi:arm-flex" + }, + "fat_free_mass_for_segments_left_arm": { + "default": "mdi:arm-flex" + }, + "fat_free_mass_for_segments_right_arm": { + "default": "mdi:arm-flex" + }, + "fat_mass_for_segments_left_arm": { + "default": "mdi:arm-flex" + }, + "fat_mass_for_segments_right_arm": { + "default": "mdi:arm-flex" + }, "deep_sleep": { "default": "mdi:sleep" }, diff --git a/homeassistant/components/withings/sensor.py b/homeassistant/components/withings/sensor.py index e205af7bdda..20fd72845ae 100644 --- a/homeassistant/components/withings/sensor.py +++ b/homeassistant/components/withings/sensor.py @@ -68,10 +68,9 @@ class WithingsMeasurementSensorEntityDescription(SensorEntityDescription): MEASUREMENT_SENSORS: dict[ - tuple[MeasurementType, MeasurementPosition | None], - WithingsMeasurementSensorEntityDescription, + MeasurementType, WithingsMeasurementSensorEntityDescription ] = { - (MeasurementType.WEIGHT, None): WithingsMeasurementSensorEntityDescription( + MeasurementType.WEIGHT: WithingsMeasurementSensorEntityDescription( key="weight_kg", measurement_type=MeasurementType.WEIGHT, native_unit_of_measurement=UnitOfMass.KILOGRAMS, @@ -79,7 +78,7 @@ MEASUREMENT_SENSORS: dict[ device_class=SensorDeviceClass.WEIGHT, state_class=SensorStateClass.MEASUREMENT, ), - (MeasurementType.FAT_MASS_WEIGHT, None): WithingsMeasurementSensorEntityDescription( + MeasurementType.FAT_MASS_WEIGHT: WithingsMeasurementSensorEntityDescription( key="fat_mass_kg", measurement_type=MeasurementType.FAT_MASS_WEIGHT, translation_key="fat_mass", @@ -88,7 +87,7 @@ MEASUREMENT_SENSORS: dict[ device_class=SensorDeviceClass.WEIGHT, state_class=SensorStateClass.MEASUREMENT, ), - (MeasurementType.FAT_FREE_MASS, None): WithingsMeasurementSensorEntityDescription( + MeasurementType.FAT_FREE_MASS: WithingsMeasurementSensorEntityDescription( key="fat_free_mass_kg", measurement_type=MeasurementType.FAT_FREE_MASS, translation_key="fat_free_mass", @@ -97,7 +96,7 @@ MEASUREMENT_SENSORS: dict[ device_class=SensorDeviceClass.WEIGHT, state_class=SensorStateClass.MEASUREMENT, ), - (MeasurementType.MUSCLE_MASS, None): WithingsMeasurementSensorEntityDescription( + MeasurementType.MUSCLE_MASS: WithingsMeasurementSensorEntityDescription( key="muscle_mass_kg", measurement_type=MeasurementType.MUSCLE_MASS, translation_key="muscle_mass", @@ -106,7 +105,7 @@ MEASUREMENT_SENSORS: dict[ device_class=SensorDeviceClass.WEIGHT, state_class=SensorStateClass.MEASUREMENT, ), - (MeasurementType.BONE_MASS, None): WithingsMeasurementSensorEntityDescription( + MeasurementType.BONE_MASS: WithingsMeasurementSensorEntityDescription( key="bone_mass_kg", measurement_type=MeasurementType.BONE_MASS, translation_key="bone_mass", @@ -115,7 +114,7 @@ MEASUREMENT_SENSORS: dict[ device_class=SensorDeviceClass.WEIGHT, state_class=SensorStateClass.MEASUREMENT, ), - (MeasurementType.HEIGHT, None): WithingsMeasurementSensorEntityDescription( + MeasurementType.HEIGHT: WithingsMeasurementSensorEntityDescription( key="height_m", measurement_type=MeasurementType.HEIGHT, translation_key="height", @@ -125,17 +124,14 @@ MEASUREMENT_SENSORS: dict[ state_class=SensorStateClass.MEASUREMENT, entity_registry_enabled_default=False, ), - (MeasurementType.TEMPERATURE, None): WithingsMeasurementSensorEntityDescription( + MeasurementType.TEMPERATURE: WithingsMeasurementSensorEntityDescription( key="temperature_c", measurement_type=MeasurementType.TEMPERATURE, native_unit_of_measurement=UnitOfTemperature.CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), - ( - MeasurementType.BODY_TEMPERATURE, - None, - ): WithingsMeasurementSensorEntityDescription( + MeasurementType.BODY_TEMPERATURE: WithingsMeasurementSensorEntityDescription( key="body_temperature_c", measurement_type=MeasurementType.BODY_TEMPERATURE, translation_key="body_temperature", @@ -143,10 +139,7 @@ MEASUREMENT_SENSORS: dict[ device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), - ( - MeasurementType.SKIN_TEMPERATURE, - None, - ): WithingsMeasurementSensorEntityDescription( + MeasurementType.SKIN_TEMPERATURE: WithingsMeasurementSensorEntityDescription( key="skin_temperature_c", measurement_type=MeasurementType.SKIN_TEMPERATURE, translation_key="skin_temperature", @@ -154,7 +147,7 @@ MEASUREMENT_SENSORS: dict[ device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), - (MeasurementType.FAT_RATIO, None): WithingsMeasurementSensorEntityDescription( + MeasurementType.FAT_RATIO: WithingsMeasurementSensorEntityDescription( key="fat_ratio_pct", measurement_type=MeasurementType.FAT_RATIO, translation_key="fat_ratio", @@ -162,41 +155,35 @@ MEASUREMENT_SENSORS: dict[ suggested_display_precision=2, state_class=SensorStateClass.MEASUREMENT, ), - ( - MeasurementType.DIASTOLIC_BLOOD_PRESSURE, - None, - ): WithingsMeasurementSensorEntityDescription( + MeasurementType.DIASTOLIC_BLOOD_PRESSURE: WithingsMeasurementSensorEntityDescription( key="diastolic_blood_pressure_mmhg", measurement_type=MeasurementType.DIASTOLIC_BLOOD_PRESSURE, translation_key="diastolic_blood_pressure", native_unit_of_measurement=UOM_MMHG, state_class=SensorStateClass.MEASUREMENT, ), - ( - MeasurementType.SYSTOLIC_BLOOD_PRESSURE, - None, - ): WithingsMeasurementSensorEntityDescription( + MeasurementType.SYSTOLIC_BLOOD_PRESSURE: WithingsMeasurementSensorEntityDescription( key="systolic_blood_pressure_mmhg", measurement_type=MeasurementType.SYSTOLIC_BLOOD_PRESSURE, translation_key="systolic_blood_pressure", native_unit_of_measurement=UOM_MMHG, state_class=SensorStateClass.MEASUREMENT, ), - (MeasurementType.HEART_RATE, None): WithingsMeasurementSensorEntityDescription( + MeasurementType.HEART_RATE: WithingsMeasurementSensorEntityDescription( key="heart_pulse_bpm", measurement_type=MeasurementType.HEART_RATE, translation_key="heart_pulse", native_unit_of_measurement=UOM_BEATS_PER_MINUTE, state_class=SensorStateClass.MEASUREMENT, ), - (MeasurementType.SP02, None): WithingsMeasurementSensorEntityDescription( + MeasurementType.SP02: WithingsMeasurementSensorEntityDescription( key="spo2_pct", measurement_type=MeasurementType.SP02, translation_key="spo2", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, ), - (MeasurementType.HYDRATION, None): WithingsMeasurementSensorEntityDescription( + MeasurementType.HYDRATION: WithingsMeasurementSensorEntityDescription( key="hydration", measurement_type=MeasurementType.HYDRATION, translation_key="hydration", @@ -205,10 +192,7 @@ MEASUREMENT_SENSORS: dict[ state_class=SensorStateClass.MEASUREMENT, entity_registry_enabled_default=False, ), - ( - MeasurementType.PULSE_WAVE_VELOCITY, - None, - ): WithingsMeasurementSensorEntityDescription( + MeasurementType.PULSE_WAVE_VELOCITY: WithingsMeasurementSensorEntityDescription( key="pulse_wave_velocity", measurement_type=MeasurementType.PULSE_WAVE_VELOCITY, translation_key="pulse_wave_velocity", @@ -216,7 +200,7 @@ MEASUREMENT_SENSORS: dict[ device_class=SensorDeviceClass.SPEED, state_class=SensorStateClass.MEASUREMENT, ), - (MeasurementType.VO2, None): WithingsMeasurementSensorEntityDescription( + MeasurementType.VO2: WithingsMeasurementSensorEntityDescription( key="vo2_max", measurement_type=MeasurementType.VO2, translation_key="vo2_max", @@ -224,10 +208,7 @@ MEASUREMENT_SENSORS: dict[ state_class=SensorStateClass.MEASUREMENT, entity_registry_enabled_default=False, ), - ( - MeasurementType.EXTRACELLULAR_WATER, - None, - ): WithingsMeasurementSensorEntityDescription( + MeasurementType.EXTRACELLULAR_WATER: WithingsMeasurementSensorEntityDescription( key="extracellular_water", measurement_type=MeasurementType.EXTRACELLULAR_WATER, translation_key="extracellular_water", @@ -236,10 +217,7 @@ MEASUREMENT_SENSORS: dict[ state_class=SensorStateClass.MEASUREMENT, entity_registry_enabled_default=False, ), - ( - MeasurementType.INTRACELLULAR_WATER, - None, - ): WithingsMeasurementSensorEntityDescription( + MeasurementType.INTRACELLULAR_WATER: WithingsMeasurementSensorEntityDescription( key="intracellular_water", measurement_type=MeasurementType.INTRACELLULAR_WATER, translation_key="intracellular_water", @@ -248,42 +226,33 @@ MEASUREMENT_SENSORS: dict[ state_class=SensorStateClass.MEASUREMENT, entity_registry_enabled_default=False, ), - (MeasurementType.VASCULAR_AGE, None): WithingsMeasurementSensorEntityDescription( + MeasurementType.VASCULAR_AGE: WithingsMeasurementSensorEntityDescription( key="vascular_age", measurement_type=MeasurementType.VASCULAR_AGE, translation_key="vascular_age", entity_registry_enabled_default=False, ), - (MeasurementType.VISCERAL_FAT, None): WithingsMeasurementSensorEntityDescription( + MeasurementType.VISCERAL_FAT: WithingsMeasurementSensorEntityDescription( key="visceral_fat", measurement_type=MeasurementType.VISCERAL_FAT, translation_key="visceral_fat_index", entity_registry_enabled_default=False, ), - ( - MeasurementType.ELECTRODERMAL_ACTIVITY_FEET, - None, - ): WithingsMeasurementSensorEntityDescription( + MeasurementType.ELECTRODERMAL_ACTIVITY_FEET: WithingsMeasurementSensorEntityDescription( key="electrodermal_activity_feet", measurement_type=MeasurementType.ELECTRODERMAL_ACTIVITY_FEET, translation_key="electrodermal_activity_feet", native_unit_of_measurement=PERCENTAGE, entity_registry_enabled_default=False, ), - ( - MeasurementType.ELECTRODERMAL_ACTIVITY_LEFT_FOOT, - None, - ): WithingsMeasurementSensorEntityDescription( + MeasurementType.ELECTRODERMAL_ACTIVITY_LEFT_FOOT: WithingsMeasurementSensorEntityDescription( key="electrodermal_activity_left_foot", measurement_type=MeasurementType.ELECTRODERMAL_ACTIVITY_LEFT_FOOT, translation_key="electrodermal_activity_left_foot", native_unit_of_measurement=PERCENTAGE, entity_registry_enabled_default=False, ), - ( - MeasurementType.ELECTRODERMAL_ACTIVITY_RIGHT_FOOT, - None, - ): WithingsMeasurementSensorEntityDescription( + MeasurementType.ELECTRODERMAL_ACTIVITY_RIGHT_FOOT: WithingsMeasurementSensorEntityDescription( key="electrodermal_activity_right_foot", measurement_type=MeasurementType.ELECTRODERMAL_ACTIVITY_RIGHT_FOOT, translation_key="electrodermal_activity_right_foot", @@ -293,6 +262,47 @@ MEASUREMENT_SENSORS: dict[ } +def get_positional_measurement_description( + measurement_type: MeasurementType, measurement_position: MeasurementPosition +) -> WithingsMeasurementSensorEntityDescription | None: + """Get the sensor description for a measurement type.""" + if measurement_position not in ( + MeasurementPosition.TORSO, + MeasurementPosition.LEFT_ARM, + MeasurementPosition.RIGHT_ARM, + MeasurementPosition.LEFT_LEG, + MeasurementPosition.RIGHT_LEG, + ) or measurement_type not in ( + MeasurementType.MUSCLE_MASS_FOR_SEGMENTS, + MeasurementType.FAT_FREE_MASS_FOR_SEGMENTS, + MeasurementType.FAT_MASS_FOR_SEGMENTS, + ): + return None + return WithingsMeasurementSensorEntityDescription( + key=f"{measurement_type.name.lower()}_{measurement_position.name.lower()}", + measurement_type=measurement_type, + measurement_position=measurement_position, + translation_key=f"{measurement_type.name.lower()}_{measurement_position.name.lower()}", + native_unit_of_measurement=UnitOfMass.KILOGRAMS, + suggested_display_precision=2, + device_class=SensorDeviceClass.WEIGHT, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + ) + + +def get_measurement_description( + measurement: tuple[MeasurementType, MeasurementPosition | None], +) -> WithingsMeasurementSensorEntityDescription | None: + """Get the sensor description for a measurement type.""" + measurement_type, measurement_position = measurement + if measurement_position is not None: + return get_positional_measurement_description( + measurement_type, measurement_position + ) + return MEASUREMENT_SENSORS.get(measurement_type) + + @dataclass(frozen=True, kw_only=True) class WithingsSleepSensorEntityDescription(SensorEntityDescription): """Immutable class for describing withings data.""" @@ -663,11 +673,9 @@ async def async_setup_entry( entities: list[SensorEntity] = [] entities.extend( - WithingsMeasurementSensor( - measurement_coordinator, MEASUREMENT_SENSORS[measurement_type] - ) + WithingsMeasurementSensor(measurement_coordinator, description) for measurement_type in measurement_coordinator.data - if measurement_type in MEASUREMENT_SENSORS + if (description := get_measurement_description(measurement_type)) is not None ) current_measurement_types = set(measurement_coordinator.data) @@ -679,11 +687,10 @@ async def async_setup_entry( if new_measurement_types: current_measurement_types.update(new_measurement_types) async_add_entities( - WithingsMeasurementSensor( - measurement_coordinator, MEASUREMENT_SENSORS[measurement_type] - ) + WithingsMeasurementSensor(measurement_coordinator, description) for measurement_type in new_measurement_types - if measurement_type in MEASUREMENT_SENSORS + if (description := get_measurement_description(measurement_type)) + is not None ) measurement_coordinator.async_add_listener(_async_measurement_listener) diff --git a/homeassistant/components/withings/strings.json b/homeassistant/components/withings/strings.json index a142dd23eac..fb86b16c3be 100644 --- a/homeassistant/components/withings/strings.json +++ b/homeassistant/components/withings/strings.json @@ -104,6 +104,51 @@ "electrodermal_activity_right_foot": { "name": "Electrodermal activity right foot" }, + "muscle_mass_for_segments_torso": { + "name": "Muscle mass in torso" + }, + "muscle_mass_for_segments_left_arm": { + "name": "Muscle mass in left arm" + }, + "muscle_mass_for_segments_right_arm": { + "name": "Muscle mass in right arm" + }, + "muscle_mass_for_segments_left_leg": { + "name": "Muscle mass in left leg" + }, + "muscle_mass_for_segments_right_leg": { + "name": "Muscle mass in right leg" + }, + "fat_free_mass_for_segments_torso": { + "name": "Fat free mass in torso" + }, + "fat_free_mass_for_segments_left_arm": { + "name": "Fat free mass in left arm" + }, + "fat_free_mass_for_segments_right_arm": { + "name": "Fat free mass in right arm" + }, + "fat_free_mass_for_segments_left_leg": { + "name": "Fat free mass in left leg" + }, + "fat_free_mass_for_segments_right_leg": { + "name": "Fat free mass in right leg" + }, + "fat_mass_for_segments_torso": { + "name": "Fat mass in torso" + }, + "fat_mass_for_segments_left_arm": { + "name": "Fat mass in left arm" + }, + "fat_mass_for_segments_right_arm": { + "name": "Fat mass in right arm" + }, + "fat_mass_for_segments_left_leg": { + "name": "Fat mass in left leg" + }, + "fat_mass_for_segments_right_leg": { + "name": "Fat mass in right leg" + }, "breathing_disturbances_intensity": { "name": "Breathing disturbances intensity" }, diff --git a/tests/components/withings/fixtures/measurements.json b/tests/components/withings/fixtures/measurements.json index 31603d9a332..9c68d2e3e47 100644 --- a/tests/components/withings/fixtures/measurements.json +++ b/tests/components/withings/fixtures/measurements.json @@ -777,6 +777,14 @@ "fm": 3, "position": 2 }, + { + "value": 489, + "type": 1, + "unit": -2, + "algo": 218235904, + "fm": 3, + "position": 2 + }, { "value": 2308, "type": 226, diff --git a/tests/components/withings/snapshots/test_diagnostics.ambr b/tests/components/withings/snapshots/test_diagnostics.ambr index 8ed8116f0c5..ca3cd2147d3 100644 --- a/tests/components/withings/snapshots/test_diagnostics.ambr +++ b/tests/components/withings/snapshots/test_diagnostics.ambr @@ -109,6 +109,10 @@ 6, None, ]), + list([ + 1, + 2, + ]), list([ 4, None, @@ -281,6 +285,10 @@ 6, None, ]), + list([ + 1, + 2, + ]), list([ 4, None, @@ -453,6 +461,10 @@ 6, None, ]), + list([ + 1, + 2, + ]), list([ 4, None, diff --git a/tests/components/withings/snapshots/test_sensor.ambr b/tests/components/withings/snapshots/test_sensor.ambr index 37635ece403..70a86c79038 100644 --- a/tests/components/withings/snapshots/test_sensor.ambr +++ b/tests/components/withings/snapshots/test_sensor.ambr @@ -965,6 +965,276 @@ 'state': '60', }) # --- +# name: test_all_entities[sensor.henk_fat_free_mass_in_left_arm-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.henk_fat_free_mass_in_left_arm', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Fat free mass in left arm', + 'platform': 'withings', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'fat_free_mass_for_segments_left_arm', + 'unique_id': 'withings_12345_fat_free_mass_for_segments_left_arm', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.henk_fat_free_mass_in_left_arm-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'weight', + 'friendly_name': 'henk Fat free mass in left arm', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.henk_fat_free_mass_in_left_arm', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '5.05', + }) +# --- +# name: test_all_entities[sensor.henk_fat_free_mass_in_left_leg-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.henk_fat_free_mass_in_left_leg', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Fat free mass in left leg', + 'platform': 'withings', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'fat_free_mass_for_segments_left_leg', + 'unique_id': 'withings_12345_fat_free_mass_for_segments_left_leg', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.henk_fat_free_mass_in_left_leg-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'weight', + 'friendly_name': 'henk Fat free mass in left leg', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.henk_fat_free_mass_in_left_leg', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '13.84', + }) +# --- +# name: test_all_entities[sensor.henk_fat_free_mass_in_right_arm-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.henk_fat_free_mass_in_right_arm', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Fat free mass in right arm', + 'platform': 'withings', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'fat_free_mass_for_segments_right_arm', + 'unique_id': 'withings_12345_fat_free_mass_for_segments_right_arm', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.henk_fat_free_mass_in_right_arm-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'weight', + 'friendly_name': 'henk Fat free mass in right arm', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.henk_fat_free_mass_in_right_arm', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '5.18', + }) +# --- +# name: test_all_entities[sensor.henk_fat_free_mass_in_right_leg-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.henk_fat_free_mass_in_right_leg', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Fat free mass in right leg', + 'platform': 'withings', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'fat_free_mass_for_segments_right_leg', + 'unique_id': 'withings_12345_fat_free_mass_for_segments_right_leg', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.henk_fat_free_mass_in_right_leg-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'weight', + 'friendly_name': 'henk Fat free mass in right leg', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.henk_fat_free_mass_in_right_leg', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '14.05', + }) +# --- +# name: test_all_entities[sensor.henk_fat_free_mass_in_torso-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.henk_fat_free_mass_in_torso', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Fat free mass in torso', + 'platform': 'withings', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'fat_free_mass_for_segments_torso', + 'unique_id': 'withings_12345_fat_free_mass_for_segments_torso', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.henk_fat_free_mass_in_torso-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'weight', + 'friendly_name': 'henk Fat free mass in torso', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.henk_fat_free_mass_in_torso', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '40.49', + }) +# --- # name: test_all_entities[sensor.henk_fat_mass-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -1019,6 +1289,276 @@ 'state': '5', }) # --- +# name: test_all_entities[sensor.henk_fat_mass_in_left_arm-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.henk_fat_mass_in_left_arm', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Fat mass in left arm', + 'platform': 'withings', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'fat_mass_for_segments_left_arm', + 'unique_id': 'withings_12345_fat_mass_for_segments_left_arm', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.henk_fat_mass_in_left_arm-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'weight', + 'friendly_name': 'henk Fat mass in left arm', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.henk_fat_mass_in_left_arm', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1.03', + }) +# --- +# name: test_all_entities[sensor.henk_fat_mass_in_left_leg-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.henk_fat_mass_in_left_leg', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Fat mass in left leg', + 'platform': 'withings', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'fat_mass_for_segments_left_leg', + 'unique_id': 'withings_12345_fat_mass_for_segments_left_leg', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.henk_fat_mass_in_left_leg-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'weight', + 'friendly_name': 'henk Fat mass in left leg', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.henk_fat_mass_in_left_leg', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2.45', + }) +# --- +# name: test_all_entities[sensor.henk_fat_mass_in_right_arm-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.henk_fat_mass_in_right_arm', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Fat mass in right arm', + 'platform': 'withings', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'fat_mass_for_segments_right_arm', + 'unique_id': 'withings_12345_fat_mass_for_segments_right_arm', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.henk_fat_mass_in_right_arm-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'weight', + 'friendly_name': 'henk Fat mass in right arm', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.henk_fat_mass_in_right_arm', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.99', + }) +# --- +# name: test_all_entities[sensor.henk_fat_mass_in_right_leg-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.henk_fat_mass_in_right_leg', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Fat mass in right leg', + 'platform': 'withings', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'fat_mass_for_segments_right_leg', + 'unique_id': 'withings_12345_fat_mass_for_segments_right_leg', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.henk_fat_mass_in_right_leg-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'weight', + 'friendly_name': 'henk Fat mass in right leg', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.henk_fat_mass_in_right_leg', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2.33', + }) +# --- +# name: test_all_entities[sensor.henk_fat_mass_in_torso-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.henk_fat_mass_in_torso', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Fat mass in torso', + 'platform': 'withings', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'fat_mass_for_segments_torso', + 'unique_id': 'withings_12345_fat_mass_for_segments_torso', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.henk_fat_mass_in_torso-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'weight', + 'friendly_name': 'henk Fat mass in torso', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.henk_fat_mass_in_torso', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '10.99', + }) +# --- # name: test_all_entities[sensor.henk_fat_ratio-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -1940,6 +2480,276 @@ 'state': '50', }) # --- +# name: test_all_entities[sensor.henk_muscle_mass_in_left_arm-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.henk_muscle_mass_in_left_arm', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Muscle mass in left arm', + 'platform': 'withings', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'muscle_mass_for_segments_left_arm', + 'unique_id': 'withings_12345_muscle_mass_for_segments_left_arm', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.henk_muscle_mass_in_left_arm-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'weight', + 'friendly_name': 'henk Muscle mass in left arm', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.henk_muscle_mass_in_left_arm', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '4.77', + }) +# --- +# name: test_all_entities[sensor.henk_muscle_mass_in_left_leg-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.henk_muscle_mass_in_left_leg', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Muscle mass in left leg', + 'platform': 'withings', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'muscle_mass_for_segments_left_leg', + 'unique_id': 'withings_12345_muscle_mass_for_segments_left_leg', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.henk_muscle_mass_in_left_leg-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'weight', + 'friendly_name': 'henk Muscle mass in left leg', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.henk_muscle_mass_in_left_leg', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '13.09', + }) +# --- +# name: test_all_entities[sensor.henk_muscle_mass_in_right_arm-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.henk_muscle_mass_in_right_arm', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Muscle mass in right arm', + 'platform': 'withings', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'muscle_mass_for_segments_right_arm', + 'unique_id': 'withings_12345_muscle_mass_for_segments_right_arm', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.henk_muscle_mass_in_right_arm-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'weight', + 'friendly_name': 'henk Muscle mass in right arm', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.henk_muscle_mass_in_right_arm', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '4.89', + }) +# --- +# name: test_all_entities[sensor.henk_muscle_mass_in_right_leg-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.henk_muscle_mass_in_right_leg', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Muscle mass in right leg', + 'platform': 'withings', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'muscle_mass_for_segments_right_leg', + 'unique_id': 'withings_12345_muscle_mass_for_segments_right_leg', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.henk_muscle_mass_in_right_leg-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'weight', + 'friendly_name': 'henk Muscle mass in right leg', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.henk_muscle_mass_in_right_leg', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '13.29', + }) +# --- +# name: test_all_entities[sensor.henk_muscle_mass_in_torso-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.henk_muscle_mass_in_torso', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Muscle mass in torso', + 'platform': 'withings', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'muscle_mass_for_segments_torso', + 'unique_id': 'withings_12345_muscle_mass_for_segments_torso', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.henk_muscle_mass_in_torso-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'weight', + 'friendly_name': 'henk Muscle mass in torso', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.henk_muscle_mass_in_torso', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '38.7', + }) +# --- # name: test_all_entities[sensor.henk_pause_during_last_workout-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ From 53f1cd8e722bb14e4c4f7c3a4ded7a04cb1e1dea Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sat, 8 Jun 2024 12:27:24 +0200 Subject: [PATCH 0379/1445] Improve withings diagnostics (#119128) --- .../components/withings/diagnostics.py | 21 +- .../withings/fixtures/measurements.json | 4 +- .../withings/snapshots/test_diagnostics.ambr | 651 +++++------------- 3 files changed, 178 insertions(+), 498 deletions(-) diff --git a/homeassistant/components/withings/diagnostics.py b/homeassistant/components/withings/diagnostics.py index 1f74f2be444..d8b59075368 100644 --- a/homeassistant/components/withings/diagnostics.py +++ b/homeassistant/components/withings/diagnostics.py @@ -26,11 +26,30 @@ async def async_get_config_entry_diagnostics( withings_data = entry.runtime_data + positional_measurements: dict[str, list[str]] = {} + measurements: list[str] = [] + + for measurement in withings_data.measurement_coordinator.data: + measurement_type, measurement_position = measurement + measurement_type_name = measurement_type.name.lower() + if measurement_position is not None: + measurement_position_name = measurement_position.name.lower() + if measurement_type_name not in positional_measurements: + positional_measurements[measurement_type_name] = [] + positional_measurements[measurement_type_name].append( + measurement_position_name + ) + else: + measurements.append(measurement_type_name) + return { "has_valid_external_webhook_url": has_valid_external_webhook_url, "has_cloudhooks": has_cloudhooks, "webhooks_connected": withings_data.measurement_coordinator.webhooks_connected, - "received_measurements": list(withings_data.measurement_coordinator.data), + "received_measurements": { + "positional": positional_measurements, + "non_positional": measurements, + }, "received_sleep_data": withings_data.sleep_coordinator.data is not None, "received_workout_data": withings_data.workout_coordinator.data is not None, "received_activity_data": withings_data.activity_coordinator.data is not None, diff --git a/tests/components/withings/fixtures/measurements.json b/tests/components/withings/fixtures/measurements.json index 9c68d2e3e47..5ce14b6c774 100644 --- a/tests/components/withings/fixtures/measurements.json +++ b/tests/components/withings/fixtures/measurements.json @@ -779,11 +779,11 @@ }, { "value": 489, - "type": 1, + "type": 175, "unit": -2, "algo": 218235904, "fm": 3, - "position": 2 + "position": 1 }, { "value": 2308, diff --git a/tests/components/withings/snapshots/test_diagnostics.ambr b/tests/components/withings/snapshots/test_diagnostics.ambr index ca3cd2147d3..df2a3b95388 100644 --- a/tests/components/withings/snapshots/test_diagnostics.ambr +++ b/tests/components/withings/snapshots/test_diagnostics.ambr @@ -4,172 +4,59 @@ 'has_cloudhooks': True, 'has_valid_external_webhook_url': True, 'received_activity_data': False, - 'received_measurements': list([ - list([ - 1, - None, + 'received_measurements': dict({ + 'non_positional': list([ + 'weight', + 'fat_free_mass', + 'fat_mass_weight', + 'muscle_mass', + 'hydration', + 'bone_mass', + 'extracellular_water', + 'intracellular_water', + 'visceral_fat', + 'unknown', + 'fat_ratio', + 'height', + 'temperature', + 'body_temperature', + 'skin_temperature', + 'diastolic_blood_pressure', + 'systolic_blood_pressure', + 'heart_rate', + 'sp02', + 'pulse_wave_velocity', + 'vo2', + 'vascular_age', + 'electrodermal_activity_right_foot', + 'electrodermal_activity_left_foot', + 'electrodermal_activity_feet', ]), - list([ - 5, - None, - ]), - list([ - 8, - None, - ]), - list([ - 76, - None, - ]), - list([ - 77, - None, - ]), - list([ - 88, - None, - ]), - list([ - 168, - None, - ]), - list([ - 169, - None, - ]), - list([ - 170, - None, - ]), - list([ - 173, - 12, - ]), - list([ - 173, - 10, - ]), - list([ - 173, - 3, - ]), - list([ - 173, - 11, - ]), - list([ - 173, - 2, - ]), - list([ - 174, - 12, - ]), - list([ - 174, - 10, - ]), - list([ - 174, - 3, - ]), - list([ - 174, - 11, - ]), - list([ - 174, - 2, - ]), - list([ - 175, - 12, - ]), - list([ - 175, - 10, - ]), - list([ - 175, - 3, - ]), - list([ - 175, - 11, - ]), - list([ - 175, - 2, - ]), - list([ - 0, - None, - ]), - list([ - 6, - None, - ]), - list([ - 1, - 2, - ]), - list([ - 4, - None, - ]), - list([ - 12, - None, - ]), - list([ - 71, - None, - ]), - list([ - 73, - None, - ]), - list([ - 9, - None, - ]), - list([ - 10, - None, - ]), - list([ - 11, - None, - ]), - list([ - 54, - None, - ]), - list([ - 91, - None, - ]), - list([ - 123, - None, - ]), - list([ - 155, - None, - ]), - list([ - 198, - None, - ]), - list([ - 197, - None, - ]), - list([ - 196, - None, - ]), - ]), + 'positional': dict({ + 'fat_free_mass_for_segments': list([ + 'torso', + 'left_leg', + 'left_arm', + 'right_leg', + 'right_arm', + ]), + 'fat_mass_for_segments': list([ + 'torso', + 'left_leg', + 'left_arm', + 'right_leg', + 'right_arm', + ]), + 'muscle_mass_for_segments': list([ + 'torso', + 'left_leg', + 'left_arm', + 'right_leg', + 'right_arm', + 'left_wrist', + ]), + }), + }), 'received_sleep_data': True, 'received_workout_data': True, 'webhooks_connected': True, @@ -180,172 +67,59 @@ 'has_cloudhooks': False, 'has_valid_external_webhook_url': False, 'received_activity_data': False, - 'received_measurements': list([ - list([ - 1, - None, + 'received_measurements': dict({ + 'non_positional': list([ + 'weight', + 'fat_free_mass', + 'fat_mass_weight', + 'muscle_mass', + 'hydration', + 'bone_mass', + 'extracellular_water', + 'intracellular_water', + 'visceral_fat', + 'unknown', + 'fat_ratio', + 'height', + 'temperature', + 'body_temperature', + 'skin_temperature', + 'diastolic_blood_pressure', + 'systolic_blood_pressure', + 'heart_rate', + 'sp02', + 'pulse_wave_velocity', + 'vo2', + 'vascular_age', + 'electrodermal_activity_right_foot', + 'electrodermal_activity_left_foot', + 'electrodermal_activity_feet', ]), - list([ - 5, - None, - ]), - list([ - 8, - None, - ]), - list([ - 76, - None, - ]), - list([ - 77, - None, - ]), - list([ - 88, - None, - ]), - list([ - 168, - None, - ]), - list([ - 169, - None, - ]), - list([ - 170, - None, - ]), - list([ - 173, - 12, - ]), - list([ - 173, - 10, - ]), - list([ - 173, - 3, - ]), - list([ - 173, - 11, - ]), - list([ - 173, - 2, - ]), - list([ - 174, - 12, - ]), - list([ - 174, - 10, - ]), - list([ - 174, - 3, - ]), - list([ - 174, - 11, - ]), - list([ - 174, - 2, - ]), - list([ - 175, - 12, - ]), - list([ - 175, - 10, - ]), - list([ - 175, - 3, - ]), - list([ - 175, - 11, - ]), - list([ - 175, - 2, - ]), - list([ - 0, - None, - ]), - list([ - 6, - None, - ]), - list([ - 1, - 2, - ]), - list([ - 4, - None, - ]), - list([ - 12, - None, - ]), - list([ - 71, - None, - ]), - list([ - 73, - None, - ]), - list([ - 9, - None, - ]), - list([ - 10, - None, - ]), - list([ - 11, - None, - ]), - list([ - 54, - None, - ]), - list([ - 91, - None, - ]), - list([ - 123, - None, - ]), - list([ - 155, - None, - ]), - list([ - 198, - None, - ]), - list([ - 197, - None, - ]), - list([ - 196, - None, - ]), - ]), + 'positional': dict({ + 'fat_free_mass_for_segments': list([ + 'torso', + 'left_leg', + 'left_arm', + 'right_leg', + 'right_arm', + ]), + 'fat_mass_for_segments': list([ + 'torso', + 'left_leg', + 'left_arm', + 'right_leg', + 'right_arm', + ]), + 'muscle_mass_for_segments': list([ + 'torso', + 'left_leg', + 'left_arm', + 'right_leg', + 'right_arm', + 'left_wrist', + ]), + }), + }), 'received_sleep_data': True, 'received_workout_data': True, 'webhooks_connected': False, @@ -356,172 +130,59 @@ 'has_cloudhooks': False, 'has_valid_external_webhook_url': True, 'received_activity_data': False, - 'received_measurements': list([ - list([ - 1, - None, + 'received_measurements': dict({ + 'non_positional': list([ + 'weight', + 'fat_free_mass', + 'fat_mass_weight', + 'muscle_mass', + 'hydration', + 'bone_mass', + 'extracellular_water', + 'intracellular_water', + 'visceral_fat', + 'unknown', + 'fat_ratio', + 'height', + 'temperature', + 'body_temperature', + 'skin_temperature', + 'diastolic_blood_pressure', + 'systolic_blood_pressure', + 'heart_rate', + 'sp02', + 'pulse_wave_velocity', + 'vo2', + 'vascular_age', + 'electrodermal_activity_right_foot', + 'electrodermal_activity_left_foot', + 'electrodermal_activity_feet', ]), - list([ - 5, - None, - ]), - list([ - 8, - None, - ]), - list([ - 76, - None, - ]), - list([ - 77, - None, - ]), - list([ - 88, - None, - ]), - list([ - 168, - None, - ]), - list([ - 169, - None, - ]), - list([ - 170, - None, - ]), - list([ - 173, - 12, - ]), - list([ - 173, - 10, - ]), - list([ - 173, - 3, - ]), - list([ - 173, - 11, - ]), - list([ - 173, - 2, - ]), - list([ - 174, - 12, - ]), - list([ - 174, - 10, - ]), - list([ - 174, - 3, - ]), - list([ - 174, - 11, - ]), - list([ - 174, - 2, - ]), - list([ - 175, - 12, - ]), - list([ - 175, - 10, - ]), - list([ - 175, - 3, - ]), - list([ - 175, - 11, - ]), - list([ - 175, - 2, - ]), - list([ - 0, - None, - ]), - list([ - 6, - None, - ]), - list([ - 1, - 2, - ]), - list([ - 4, - None, - ]), - list([ - 12, - None, - ]), - list([ - 71, - None, - ]), - list([ - 73, - None, - ]), - list([ - 9, - None, - ]), - list([ - 10, - None, - ]), - list([ - 11, - None, - ]), - list([ - 54, - None, - ]), - list([ - 91, - None, - ]), - list([ - 123, - None, - ]), - list([ - 155, - None, - ]), - list([ - 198, - None, - ]), - list([ - 197, - None, - ]), - list([ - 196, - None, - ]), - ]), + 'positional': dict({ + 'fat_free_mass_for_segments': list([ + 'torso', + 'left_leg', + 'left_arm', + 'right_leg', + 'right_arm', + ]), + 'fat_mass_for_segments': list([ + 'torso', + 'left_leg', + 'left_arm', + 'right_leg', + 'right_arm', + ]), + 'muscle_mass_for_segments': list([ + 'torso', + 'left_leg', + 'left_arm', + 'right_leg', + 'right_arm', + 'left_wrist', + ]), + }), + }), 'received_sleep_data': True, 'received_workout_data': True, 'webhooks_connected': True, From 27df79daf1126a756e5624604e23275b5376e657 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sat, 8 Jun 2024 14:00:55 +0200 Subject: [PATCH 0380/1445] Use translation placeholders in AccuWeather (#118760) * Use translation placeholder * Update test snapshot --------- Co-authored-by: Maciej Bieniek <478555+bieniu@users.noreply.github.com> --- .../components/accuweather/sensor.py | 430 ++-- .../components/accuweather/strings.json | 744 +------ .../accuweather/snapshots/test_sensor.ambr | 1892 ++++++++--------- 3 files changed, 1201 insertions(+), 1865 deletions(-) diff --git a/homeassistant/components/accuweather/sensor.py b/homeassistant/components/accuweather/sensor.py index e7a3216ad04..190fc311c1a 100644 --- a/homeassistant/components/accuweather/sensor.py +++ b/homeassistant/components/accuweather/sensor.py @@ -55,284 +55,185 @@ class AccuWeatherSensorDescription(SensorEntityDescription): attr_fn: Callable[[dict[str, Any]], dict[str, Any]] = lambda _: {} -@dataclass(frozen=True, kw_only=True) -class AccuWeatherForecastSensorDescription(AccuWeatherSensorDescription): - """Class describing AccuWeather sensor entities.""" - - day: int - - -FORECAST_SENSOR_TYPES: tuple[AccuWeatherForecastSensorDescription, ...] = ( - *( - AccuWeatherForecastSensorDescription( - key="AirQuality", - icon="mdi:air-filter", - value_fn=lambda data: cast(str, data[ATTR_CATEGORY]), - device_class=SensorDeviceClass.ENUM, - options=["good", "hazardous", "high", "low", "moderate", "unhealthy"], - translation_key=f"air_quality_{day}d", - day=day, - ) - for day in range(MAX_FORECAST_DAYS + 1) +FORECAST_SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = ( + AccuWeatherSensorDescription( + key="AirQuality", + icon="mdi:air-filter", + value_fn=lambda data: cast(str, data[ATTR_CATEGORY]), + device_class=SensorDeviceClass.ENUM, + options=["good", "hazardous", "high", "low", "moderate", "unhealthy"], + translation_key="air_quality", ), - *( - AccuWeatherForecastSensorDescription( - key="CloudCoverDay", - icon="mdi:weather-cloudy", - entity_registry_enabled_default=False, - native_unit_of_measurement=PERCENTAGE, - value_fn=lambda data: cast(int, data), - translation_key=f"cloud_cover_day_{day}d", - day=day, - ) - for day in range(MAX_FORECAST_DAYS + 1) + AccuWeatherSensorDescription( + key="CloudCoverDay", + icon="mdi:weather-cloudy", + entity_registry_enabled_default=False, + native_unit_of_measurement=PERCENTAGE, + value_fn=lambda data: cast(int, data), + translation_key="cloud_cover_day", ), - *( - AccuWeatherForecastSensorDescription( - key="CloudCoverNight", - icon="mdi:weather-cloudy", - entity_registry_enabled_default=False, - native_unit_of_measurement=PERCENTAGE, - value_fn=lambda data: cast(int, data), - translation_key=f"cloud_cover_night_{day}d", - day=day, - ) - for day in range(MAX_FORECAST_DAYS + 1) + AccuWeatherSensorDescription( + key="CloudCoverNight", + icon="mdi:weather-cloudy", + entity_registry_enabled_default=False, + native_unit_of_measurement=PERCENTAGE, + value_fn=lambda data: cast(int, data), + translation_key="cloud_cover_night", ), - *( - AccuWeatherForecastSensorDescription( - key="Grass", - icon="mdi:grass", - entity_registry_enabled_default=False, - native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, - value_fn=lambda data: cast(int, data[ATTR_VALUE]), - attr_fn=lambda data: {ATTR_LEVEL: data[ATTR_CATEGORY]}, - translation_key=f"grass_pollen_{day}d", - day=day, - ) - for day in range(MAX_FORECAST_DAYS + 1) + AccuWeatherSensorDescription( + key="Grass", + icon="mdi:grass", + entity_registry_enabled_default=False, + native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, + value_fn=lambda data: cast(int, data[ATTR_VALUE]), + attr_fn=lambda data: {ATTR_LEVEL: data[ATTR_CATEGORY]}, + translation_key="grass_pollen", ), - *( - AccuWeatherForecastSensorDescription( - key="HoursOfSun", - icon="mdi:weather-partly-cloudy", - native_unit_of_measurement=UnitOfTime.HOURS, - value_fn=lambda data: cast(float, data), - translation_key=f"hours_of_sun_{day}d", - day=day, - ) - for day in range(MAX_FORECAST_DAYS + 1) + AccuWeatherSensorDescription( + key="HoursOfSun", + icon="mdi:weather-partly-cloudy", + native_unit_of_measurement=UnitOfTime.HOURS, + value_fn=lambda data: cast(float, data), + translation_key="hours_of_sun", ), - *( - AccuWeatherForecastSensorDescription( - key="LongPhraseDay", - value_fn=lambda data: cast(str, data), - translation_key=f"condition_day_{day}d", - day=day, - ) - for day in range(MAX_FORECAST_DAYS + 1) + AccuWeatherSensorDescription( + key="LongPhraseDay", + value_fn=lambda data: cast(str, data), + translation_key="condition_day", ), - *( - AccuWeatherForecastSensorDescription( - key="LongPhraseNight", - value_fn=lambda data: cast(str, data), - translation_key=f"condition_night_{day}d", - day=day, - ) - for day in range(MAX_FORECAST_DAYS + 1) + AccuWeatherSensorDescription( + key="LongPhraseNight", + value_fn=lambda data: cast(str, data), + translation_key="condition_night", ), - *( - AccuWeatherForecastSensorDescription( - key="Mold", - icon="mdi:blur", - entity_registry_enabled_default=False, - native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, - value_fn=lambda data: cast(int, data[ATTR_VALUE]), - attr_fn=lambda data: {ATTR_LEVEL: data[ATTR_CATEGORY]}, - translation_key=f"mold_pollen_{day}d", - day=day, - ) - for day in range(MAX_FORECAST_DAYS + 1) + AccuWeatherSensorDescription( + key="Mold", + icon="mdi:blur", + entity_registry_enabled_default=False, + native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, + value_fn=lambda data: cast(int, data[ATTR_VALUE]), + attr_fn=lambda data: {ATTR_LEVEL: data[ATTR_CATEGORY]}, + translation_key="mold_pollen", ), - *( - AccuWeatherForecastSensorDescription( - key="Ragweed", - icon="mdi:sprout", - native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, - entity_registry_enabled_default=False, - value_fn=lambda data: cast(int, data[ATTR_VALUE]), - attr_fn=lambda data: {ATTR_LEVEL: data[ATTR_CATEGORY]}, - translation_key=f"ragweed_pollen_{day}d", - day=day, - ) - for day in range(MAX_FORECAST_DAYS + 1) + AccuWeatherSensorDescription( + key="Ragweed", + icon="mdi:sprout", + native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, + entity_registry_enabled_default=False, + value_fn=lambda data: cast(int, data[ATTR_VALUE]), + attr_fn=lambda data: {ATTR_LEVEL: data[ATTR_CATEGORY]}, + translation_key="ragweed_pollen", ), - *( - AccuWeatherForecastSensorDescription( - key="RealFeelTemperatureMax", - device_class=SensorDeviceClass.TEMPERATURE, - native_unit_of_measurement=UnitOfTemperature.CELSIUS, - value_fn=lambda data: cast(float, data[ATTR_VALUE]), - translation_key=f"realfeel_temperature_max_{day}d", - day=day, - ) - for day in range(MAX_FORECAST_DAYS + 1) + AccuWeatherSensorDescription( + key="RealFeelTemperatureMax", + device_class=SensorDeviceClass.TEMPERATURE, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + value_fn=lambda data: cast(float, data[ATTR_VALUE]), + translation_key="realfeel_temperature_max", ), - *( - AccuWeatherForecastSensorDescription( - key="RealFeelTemperatureMin", - device_class=SensorDeviceClass.TEMPERATURE, - native_unit_of_measurement=UnitOfTemperature.CELSIUS, - value_fn=lambda data: cast(float, data[ATTR_VALUE]), - translation_key=f"realfeel_temperature_min_{day}d", - day=day, - ) - for day in range(MAX_FORECAST_DAYS + 1) + AccuWeatherSensorDescription( + key="RealFeelTemperatureMin", + device_class=SensorDeviceClass.TEMPERATURE, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + value_fn=lambda data: cast(float, data[ATTR_VALUE]), + translation_key="realfeel_temperature_min", ), - *( - AccuWeatherForecastSensorDescription( - key="RealFeelTemperatureShadeMax", - device_class=SensorDeviceClass.TEMPERATURE, - entity_registry_enabled_default=False, - native_unit_of_measurement=UnitOfTemperature.CELSIUS, - value_fn=lambda data: cast(float, data[ATTR_VALUE]), - translation_key=f"realfeel_temperature_shade_max_{day}d", - day=day, - ) - for day in range(MAX_FORECAST_DAYS + 1) + AccuWeatherSensorDescription( + key="RealFeelTemperatureShadeMax", + device_class=SensorDeviceClass.TEMPERATURE, + entity_registry_enabled_default=False, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + value_fn=lambda data: cast(float, data[ATTR_VALUE]), + translation_key="realfeel_temperature_shade_max", ), - *( - AccuWeatherForecastSensorDescription( - key="RealFeelTemperatureShadeMin", - device_class=SensorDeviceClass.TEMPERATURE, - entity_registry_enabled_default=False, - native_unit_of_measurement=UnitOfTemperature.CELSIUS, - value_fn=lambda data: cast(float, data[ATTR_VALUE]), - translation_key=f"realfeel_temperature_shade_min_{day}d", - day=day, - ) - for day in range(MAX_FORECAST_DAYS + 1) + AccuWeatherSensorDescription( + key="RealFeelTemperatureShadeMin", + device_class=SensorDeviceClass.TEMPERATURE, + entity_registry_enabled_default=False, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + value_fn=lambda data: cast(float, data[ATTR_VALUE]), + translation_key="realfeel_temperature_shade_min", ), - *( - AccuWeatherForecastSensorDescription( - key="SolarIrradianceDay", - icon="mdi:weather-sunny", - entity_registry_enabled_default=False, - native_unit_of_measurement=UnitOfIrradiance.WATTS_PER_SQUARE_METER, - value_fn=lambda data: cast(float, data[ATTR_VALUE]), - translation_key=f"solar_irradiance_day_{day}d", - day=day, - ) - for day in range(MAX_FORECAST_DAYS + 1) + AccuWeatherSensorDescription( + key="SolarIrradianceDay", + icon="mdi:weather-sunny", + entity_registry_enabled_default=False, + native_unit_of_measurement=UnitOfIrradiance.WATTS_PER_SQUARE_METER, + value_fn=lambda data: cast(float, data[ATTR_VALUE]), + translation_key="solar_irradiance_day", ), - *( - AccuWeatherForecastSensorDescription( - key="SolarIrradianceNight", - icon="mdi:weather-sunny", - entity_registry_enabled_default=False, - native_unit_of_measurement=UnitOfIrradiance.WATTS_PER_SQUARE_METER, - value_fn=lambda data: cast(float, data[ATTR_VALUE]), - translation_key=f"solar_irradiance_night_{day}d", - day=day, - ) - for day in range(MAX_FORECAST_DAYS + 1) + AccuWeatherSensorDescription( + key="SolarIrradianceNight", + icon="mdi:weather-sunny", + entity_registry_enabled_default=False, + native_unit_of_measurement=UnitOfIrradiance.WATTS_PER_SQUARE_METER, + value_fn=lambda data: cast(float, data[ATTR_VALUE]), + translation_key="solar_irradiance_night", ), - *( - AccuWeatherForecastSensorDescription( - key="ThunderstormProbabilityDay", - icon="mdi:weather-lightning", - native_unit_of_measurement=PERCENTAGE, - value_fn=lambda data: cast(int, data), - translation_key=f"thunderstorm_probability_day_{day}d", - day=day, - ) - for day in range(MAX_FORECAST_DAYS + 1) + AccuWeatherSensorDescription( + key="ThunderstormProbabilityDay", + icon="mdi:weather-lightning", + native_unit_of_measurement=PERCENTAGE, + value_fn=lambda data: cast(int, data), + translation_key="thunderstorm_probability_day", ), - *( - AccuWeatherForecastSensorDescription( - key="ThunderstormProbabilityNight", - icon="mdi:weather-lightning", - native_unit_of_measurement=PERCENTAGE, - value_fn=lambda data: cast(int, data), - translation_key=f"thunderstorm_probability_night_{day}d", - day=day, - ) - for day in range(MAX_FORECAST_DAYS + 1) + AccuWeatherSensorDescription( + key="ThunderstormProbabilityNight", + icon="mdi:weather-lightning", + native_unit_of_measurement=PERCENTAGE, + value_fn=lambda data: cast(int, data), + translation_key="thunderstorm_probability_night", ), - *( - AccuWeatherForecastSensorDescription( - key="Tree", - icon="mdi:tree-outline", - native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, - entity_registry_enabled_default=False, - value_fn=lambda data: cast(int, data[ATTR_VALUE]), - attr_fn=lambda data: {ATTR_LEVEL: data[ATTR_CATEGORY]}, - translation_key=f"tree_pollen_{day}d", - day=day, - ) - for day in range(MAX_FORECAST_DAYS + 1) + AccuWeatherSensorDescription( + key="Tree", + icon="mdi:tree-outline", + native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, + entity_registry_enabled_default=False, + value_fn=lambda data: cast(int, data[ATTR_VALUE]), + attr_fn=lambda data: {ATTR_LEVEL: data[ATTR_CATEGORY]}, + translation_key="tree_pollen", ), - *( - AccuWeatherForecastSensorDescription( - key="UVIndex", - icon="mdi:weather-sunny", - native_unit_of_measurement=UV_INDEX, - value_fn=lambda data: cast(int, data[ATTR_VALUE]), - attr_fn=lambda data: {ATTR_LEVEL: data[ATTR_CATEGORY]}, - translation_key=f"uv_index_{day}d", - day=day, - ) - for day in range(MAX_FORECAST_DAYS + 1) + AccuWeatherSensorDescription( + key="UVIndex", + icon="mdi:weather-sunny", + native_unit_of_measurement=UV_INDEX, + value_fn=lambda data: cast(int, data[ATTR_VALUE]), + attr_fn=lambda data: {ATTR_LEVEL: data[ATTR_CATEGORY]}, + translation_key="uv_index_forecast", ), - *( - AccuWeatherForecastSensorDescription( - key="WindGustDay", - device_class=SensorDeviceClass.WIND_SPEED, - entity_registry_enabled_default=False, - native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR, - value_fn=lambda data: cast(float, data[ATTR_SPEED][ATTR_VALUE]), - attr_fn=lambda data: {"direction": data[ATTR_DIRECTION][ATTR_ENGLISH]}, - translation_key=f"wind_gust_speed_day_{day}d", - day=day, - ) - for day in range(MAX_FORECAST_DAYS + 1) + AccuWeatherSensorDescription( + key="WindGustDay", + device_class=SensorDeviceClass.WIND_SPEED, + entity_registry_enabled_default=False, + native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR, + value_fn=lambda data: cast(float, data[ATTR_SPEED][ATTR_VALUE]), + attr_fn=lambda data: {"direction": data[ATTR_DIRECTION][ATTR_ENGLISH]}, + translation_key="wind_gust_speed_day", ), - *( - AccuWeatherForecastSensorDescription( - key="WindGustNight", - device_class=SensorDeviceClass.WIND_SPEED, - entity_registry_enabled_default=False, - native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR, - value_fn=lambda data: cast(float, data[ATTR_SPEED][ATTR_VALUE]), - attr_fn=lambda data: {"direction": data[ATTR_DIRECTION][ATTR_ENGLISH]}, - translation_key=f"wind_gust_speed_night_{day}d", - day=day, - ) - for day in range(MAX_FORECAST_DAYS + 1) + AccuWeatherSensorDescription( + key="WindGustNight", + device_class=SensorDeviceClass.WIND_SPEED, + entity_registry_enabled_default=False, + native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR, + value_fn=lambda data: cast(float, data[ATTR_SPEED][ATTR_VALUE]), + attr_fn=lambda data: {"direction": data[ATTR_DIRECTION][ATTR_ENGLISH]}, + translation_key="wind_gust_speed_night", ), - *( - AccuWeatherForecastSensorDescription( - key="WindDay", - device_class=SensorDeviceClass.WIND_SPEED, - native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR, - value_fn=lambda data: cast(float, data[ATTR_SPEED][ATTR_VALUE]), - attr_fn=lambda data: {"direction": data[ATTR_DIRECTION][ATTR_ENGLISH]}, - translation_key=f"wind_speed_day_{day}d", - day=day, - ) - for day in range(MAX_FORECAST_DAYS + 1) + AccuWeatherSensorDescription( + key="WindDay", + device_class=SensorDeviceClass.WIND_SPEED, + native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR, + value_fn=lambda data: cast(float, data[ATTR_SPEED][ATTR_VALUE]), + attr_fn=lambda data: {"direction": data[ATTR_DIRECTION][ATTR_ENGLISH]}, + translation_key="wind_speed_day", ), - *( - AccuWeatherForecastSensorDescription( - key="WindNight", - device_class=SensorDeviceClass.WIND_SPEED, - native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR, - value_fn=lambda data: cast(float, data[ATTR_SPEED][ATTR_VALUE]), - attr_fn=lambda data: {"direction": data[ATTR_DIRECTION][ATTR_ENGLISH]}, - translation_key=f"wind_speed_night_{day}d", - day=day, - ) - for day in range(MAX_FORECAST_DAYS + 1) + AccuWeatherSensorDescription( + key="WindNight", + device_class=SensorDeviceClass.WIND_SPEED, + native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR, + value_fn=lambda data: cast(float, data[ATTR_SPEED][ATTR_VALUE]), + attr_fn=lambda data: {"direction": data[ATTR_DIRECTION][ATTR_ENGLISH]}, + translation_key="wind_speed_night", ), ) @@ -475,9 +376,10 @@ async def async_setup_entry( sensors.extend( [ - AccuWeatherForecastSensor(forecast_daily_coordinator, description) + AccuWeatherForecastSensor(forecast_daily_coordinator, description, day) + for day in range(MAX_FORECAST_DAYS + 1) for description in FORECAST_SENSOR_TYPES - if description.key in forecast_daily_coordinator.data[description.day] + if description.key in forecast_daily_coordinator.data[day] ] ) @@ -543,25 +445,27 @@ class AccuWeatherForecastSensor( _attr_attribution = ATTRIBUTION _attr_has_entity_name = True - entity_description: AccuWeatherForecastSensorDescription + entity_description: AccuWeatherSensorDescription def __init__( self, coordinator: AccuWeatherDailyForecastDataUpdateCoordinator, - description: AccuWeatherForecastSensorDescription, + description: AccuWeatherSensorDescription, + forecast_day: int, ) -> None: """Initialize.""" super().__init__(coordinator) - self.forecast_day = description.day self.entity_description = description self._sensor_data = self._get_sensor_data( - coordinator.data, description.key, self.forecast_day + coordinator.data, description.key, forecast_day ) self._attr_unique_id = ( - f"{coordinator.location_key}-{description.key}-{self.forecast_day}".lower() + f"{coordinator.location_key}-{description.key}-{forecast_day}".lower() ) self._attr_device_info = coordinator.device_info + self._attr_translation_placeholders = {"forecast_day": str(forecast_day)} + self.forecast_day = forecast_day @property def native_value(self) -> str | int | float | None: diff --git a/homeassistant/components/accuweather/strings.json b/homeassistant/components/accuweather/strings.json index 9d8fce865fd..78a49b8b877 100644 --- a/homeassistant/components/accuweather/strings.json +++ b/homeassistant/components/accuweather/strings.json @@ -21,8 +21,8 @@ }, "entity": { "sensor": { - "air_quality_0d": { - "name": "Air quality today", + "air_quality": { + "name": "Air quality day {forecast_day}", "state": { "good": "Good", "hazardous": "Hazardous", @@ -32,50 +32,6 @@ "unhealthy": "Unhealthy" } }, - "air_quality_1d": { - "name": "Air quality day 1", - "state": { - "good": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::good%]", - "hazardous": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::hazardous%]", - "high": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::high%]", - "low": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::low%]", - "moderate": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::moderate%]", - "unhealthy": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::unhealthy%]" - } - }, - "air_quality_2d": { - "name": "Air quality day 2", - "state": { - "good": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::good%]", - "hazardous": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::hazardous%]", - "high": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::high%]", - "low": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::low%]", - "moderate": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::moderate%]", - "unhealthy": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::unhealthy%]" - } - }, - "air_quality_3d": { - "name": "Air quality day 3", - "state": { - "good": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::good%]", - "hazardous": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::hazardous%]", - "high": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::high%]", - "low": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::low%]", - "moderate": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::moderate%]", - "unhealthy": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::unhealthy%]" - } - }, - "air_quality_4d": { - "name": "Air quality day 4", - "state": { - "good": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::good%]", - "hazardous": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::hazardous%]", - "high": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::high%]", - "low": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::low%]", - "moderate": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::moderate%]", - "unhealthy": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::unhealthy%]" - } - }, "apparent_temperature": { "name": "Apparent temperature" }, @@ -85,240 +41,52 @@ "cloud_cover": { "name": "Cloud cover" }, - "cloud_cover_day_0d": { - "name": "Cloud cover today" + "cloud_cover_day": { + "name": "Cloud cover day {forecast_day}" }, - "cloud_cover_day_1d": { - "name": "Cloud cover day 1" + "cloud_cover_night": { + "name": "Cloud cover night {forecast_day}" }, - "cloud_cover_day_2d": { - "name": "Cloud cover day 2" + "condition_day": { + "name": "Condition day {forecast_day}" }, - "cloud_cover_day_3d": { - "name": "Cloud cover day 3" - }, - "cloud_cover_day_4d": { - "name": "Cloud cover day 4" - }, - "cloud_cover_night_0d": { - "name": "Cloud cover tonight" - }, - "cloud_cover_night_1d": { - "name": "Cloud cover night 1" - }, - "cloud_cover_night_2d": { - "name": "Cloud cover night 2" - }, - "cloud_cover_night_3d": { - "name": "Cloud cover night 3" - }, - "cloud_cover_night_4d": { - "name": "Cloud cover night 4" - }, - "condition_day_0d": { - "name": "Condition today" - }, - "condition_day_1d": { - "name": "Condition day 1" - }, - "condition_day_2d": { - "name": "Condition day 2" - }, - "condition_day_3d": { - "name": "Condition day 3" - }, - "condition_day_4d": { - "name": "Condition day 4" - }, - "condition_night_0d": { - "name": "Condition tonight" - }, - "condition_night_1d": { - "name": "Condition night 1" - }, - "condition_night_2d": { - "name": "Condition night 2" - }, - "condition_night_3d": { - "name": "Condition night 3" - }, - "condition_night_4d": { - "name": "Condition night 4" + "condition_night": { + "name": "Condition night {forecast_day}" }, "dew_point": { "name": "Dew point" }, - "grass_pollen_0d": { - "name": "Grass pollen today", + "grass_pollen": { + "name": "Grass pollen day {forecast_day}", "state_attributes": { "level": { "name": "Level", "state": { - "good": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::good%]", - "hazardous": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::hazardous%]", - "high": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::high%]", - "low": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::low%]", - "moderate": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::moderate%]", - "unhealthy": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::unhealthy%]" + "good": "[%key:component::accuweather::entity::sensor::air_quality::state::good%]", + "hazardous": "[%key:component::accuweather::entity::sensor::air_quality::state::hazardous%]", + "high": "[%key:component::accuweather::entity::sensor::air_quality::state::high%]", + "low": "[%key:component::accuweather::entity::sensor::air_quality::state::low%]", + "moderate": "[%key:component::accuweather::entity::sensor::air_quality::state::moderate%]", + "unhealthy": "[%key:component::accuweather::entity::sensor::air_quality::state::unhealthy%]" } } } }, - "grass_pollen_1d": { - "name": "Grass pollen day 1", + "hours_of_sun": { + "name": "Hours of sun day {forecast_day}" + }, + "mold_pollen": { + "name": "Mold pollen day {forecast_day}", "state_attributes": { "level": { - "name": "[%key:component::accuweather::entity::sensor::grass_pollen_0d::state_attributes::level::name%]", + "name": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::name%]", "state": { - "good": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::good%]", - "hazardous": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::hazardous%]", - "high": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::high%]", - "low": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::low%]", - "moderate": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::moderate%]", - "unhealthy": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::unhealthy%]" - } - } - } - }, - "grass_pollen_2d": { - "name": "Grass pollen day 2", - "state_attributes": { - "level": { - "name": "[%key:component::accuweather::entity::sensor::grass_pollen_0d::state_attributes::level::name%]", - "state": { - "good": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::good%]", - "hazardous": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::hazardous%]", - "high": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::high%]", - "low": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::low%]", - "moderate": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::moderate%]", - "unhealthy": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::unhealthy%]" - } - } - } - }, - "grass_pollen_3d": { - "name": "Grass pollen day 3", - "state_attributes": { - "level": { - "name": "[%key:component::accuweather::entity::sensor::grass_pollen_0d::state_attributes::level::name%]", - "state": { - "good": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::good%]", - "hazardous": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::hazardous%]", - "high": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::high%]", - "low": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::low%]", - "moderate": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::moderate%]", - "unhealthy": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::unhealthy%]" - } - } - } - }, - "grass_pollen_4d": { - "name": "Grass pollen day 4", - "state_attributes": { - "level": { - "name": "[%key:component::accuweather::entity::sensor::grass_pollen_0d::state_attributes::level::name%]", - "state": { - "good": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::good%]", - "hazardous": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::hazardous%]", - "high": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::high%]", - "low": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::low%]", - "moderate": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::moderate%]", - "unhealthy": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::unhealthy%]" - } - } - } - }, - "hours_of_sun_0d": { - "name": "Hours of sun today" - }, - "hours_of_sun_1d": { - "name": "Hours of sun day 1" - }, - "hours_of_sun_2d": { - "name": "Hours of sun day 2" - }, - "hours_of_sun_3d": { - "name": "Hours of sun day 3" - }, - "hours_of_sun_4d": { - "name": "Hours of sun day 4" - }, - "mold_pollen_0d": { - "name": "Mold pollen today", - "state_attributes": { - "level": { - "name": "[%key:component::accuweather::entity::sensor::grass_pollen_0d::state_attributes::level::name%]", - "state": { - "good": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::good%]", - "hazardous": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::hazardous%]", - "high": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::high%]", - "low": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::low%]", - "moderate": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::moderate%]", - "unhealthy": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::unhealthy%]" - } - } - } - }, - "mold_pollen_1d": { - "name": "Mold pollen day 1", - "state_attributes": { - "level": { - "name": "[%key:component::accuweather::entity::sensor::grass_pollen_0d::state_attributes::level::name%]", - "state": { - "good": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::good%]", - "hazardous": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::hazardous%]", - "high": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::high%]", - "low": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::low%]", - "moderate": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::moderate%]", - "unhealthy": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::unhealthy%]" - } - } - } - }, - "mold_pollen_2d": { - "name": "Mold pollen day 2", - "state_attributes": { - "level": { - "name": "[%key:component::accuweather::entity::sensor::grass_pollen_0d::state_attributes::level::name%]", - "state": { - "good": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::good%]", - "hazardous": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::hazardous%]", - "high": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::high%]", - "low": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::low%]", - "moderate": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::moderate%]", - "unhealthy": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::unhealthy%]" - } - } - } - }, - "mold_pollen_3d": { - "name": "Mold pollen day 3", - "state_attributes": { - "level": { - "name": "[%key:component::accuweather::entity::sensor::grass_pollen_0d::state_attributes::level::name%]", - "state": { - "good": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::good%]", - "hazardous": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::hazardous%]", - "high": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::high%]", - "low": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::low%]", - "moderate": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::moderate%]", - "unhealthy": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::unhealthy%]" - } - } - } - }, - "mold_pollen_4d": { - "name": "Mold pollen day 4", - "state_attributes": { - "level": { - "name": "[%key:component::accuweather::entity::sensor::grass_pollen_0d::state_attributes::level::name%]", - "state": { - "good": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::good%]", - "hazardous": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::hazardous%]", - "high": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::high%]", - "low": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::low%]", - "moderate": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::moderate%]", - "unhealthy": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::unhealthy%]" + "good": "[%key:component::accuweather::entity::sensor::air_quality::state::good%]", + "hazardous": "[%key:component::accuweather::entity::sensor::air_quality::state::hazardous%]", + "high": "[%key:component::accuweather::entity::sensor::air_quality::state::high%]", + "low": "[%key:component::accuweather::entity::sensor::air_quality::state::low%]", + "moderate": "[%key:component::accuweather::entity::sensor::air_quality::state::moderate%]", + "unhealthy": "[%key:component::accuweather::entity::sensor::air_quality::state::unhealthy%]" } } } @@ -334,82 +102,18 @@ "falling": "Falling" } }, - "ragweed_pollen_0d": { - "name": "Ragweed pollen today", + "ragweed_pollen": { + "name": "Ragweed pollen day {forecast_day}", "state_attributes": { "level": { - "name": "[%key:component::accuweather::entity::sensor::grass_pollen_0d::state_attributes::level::name%]", + "name": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::name%]", "state": { - "good": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::good%]", - "hazardous": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::hazardous%]", - "high": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::high%]", - "low": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::low%]", - "moderate": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::moderate%]", - "unhealthy": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::unhealthy%]" - } - } - } - }, - "ragweed_pollen_1d": { - "name": "Ragweed pollen day 1", - "state_attributes": { - "level": { - "name": "[%key:component::accuweather::entity::sensor::grass_pollen_0d::state_attributes::level::name%]", - "state": { - "good": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::good%]", - "hazardous": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::hazardous%]", - "high": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::high%]", - "low": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::low%]", - "moderate": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::moderate%]", - "unhealthy": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::unhealthy%]" - } - } - } - }, - "ragweed_pollen_2d": { - "name": "Ragweed pollen day 2", - "state_attributes": { - "level": { - "name": "[%key:component::accuweather::entity::sensor::grass_pollen_0d::state_attributes::level::name%]", - "state": { - "good": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::good%]", - "hazardous": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::hazardous%]", - "high": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::high%]", - "low": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::low%]", - "moderate": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::moderate%]", - "unhealthy": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::unhealthy%]" - } - } - } - }, - "ragweed_pollen_3d": { - "name": "Ragweed pollen day 3", - "state_attributes": { - "level": { - "name": "[%key:component::accuweather::entity::sensor::grass_pollen_0d::state_attributes::level::name%]", - "state": { - "good": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::good%]", - "hazardous": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::hazardous%]", - "high": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::high%]", - "low": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::low%]", - "moderate": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::moderate%]", - "unhealthy": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::unhealthy%]" - } - } - } - }, - "ragweed_pollen_4d": { - "name": "Ragweed pollen day 4", - "state_attributes": { - "level": { - "name": "[%key:component::accuweather::entity::sensor::grass_pollen_0d::state_attributes::level::name%]", - "state": { - "good": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::good%]", - "hazardous": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::hazardous%]", - "high": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::high%]", - "low": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::low%]", - "moderate": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::moderate%]", - "unhealthy": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::unhealthy%]" + "good": "[%key:component::accuweather::entity::sensor::air_quality::state::good%]", + "hazardous": "[%key:component::accuweather::entity::sensor::air_quality::state::hazardous%]", + "high": "[%key:component::accuweather::entity::sensor::air_quality::state::high%]", + "low": "[%key:component::accuweather::entity::sensor::air_quality::state::low%]", + "moderate": "[%key:component::accuweather::entity::sensor::air_quality::state::moderate%]", + "unhealthy": "[%key:component::accuweather::entity::sensor::air_quality::state::unhealthy%]" } } } @@ -417,205 +121,45 @@ "realfeel_temperature": { "name": "RealFeel temperature" }, - "realfeel_temperature_max_0d": { - "name": "RealFeel temperature max today" + "realfeel_temperature_max": { + "name": "RealFeel temperature max day {forecast_day}" }, - "realfeel_temperature_max_1d": { - "name": "RealFeel temperature max day 1" - }, - "realfeel_temperature_max_2d": { - "name": "RealFeel temperature max day 2" - }, - "realfeel_temperature_max_3d": { - "name": "RealFeel temperature max day 3" - }, - "realfeel_temperature_max_4d": { - "name": "RealFeel temperature max day 4" - }, - "realfeel_temperature_min_0d": { - "name": "RealFeel temperature min today" - }, - "realfeel_temperature_min_1d": { - "name": "RealFeel temperature min day 1" - }, - "realfeel_temperature_min_2d": { - "name": "RealFeel temperature min day 2" - }, - "realfeel_temperature_min_3d": { - "name": "RealFeel temperature min day 3" - }, - "realfeel_temperature_min_4d": { - "name": "RealFeel temperature min day 4" + "realfeel_temperature_min": { + "name": "RealFeel temperature min day {forecast_day}" }, "realfeel_temperature_shade": { "name": "RealFeel temperature shade" }, - "realfeel_temperature_shade_max_0d": { - "name": "RealFeel temperature shade max today" + "realfeel_temperature_shade_max": { + "name": "RealFeel temperature shade max day {forecast_day}" }, - "realfeel_temperature_shade_max_1d": { - "name": "RealFeel temperature shade max day 1" + "realfeel_temperature_shade_min": { + "name": "RealFeel temperature shade min day {forecast_day}" }, - "realfeel_temperature_shade_max_2d": { - "name": "RealFeel temperature shade max day 2" + "solar_irradiance_day": { + "name": "Solar irradiance day {forecast_day}" }, - "realfeel_temperature_shade_max_3d": { - "name": "RealFeel temperature shade max day 3" + "solar_irradiance_night": { + "name": "Solar irradiance night {forecast_day}" }, - "realfeel_temperature_shade_max_4d": { - "name": "RealFeel temperature shade max day 4" + "thunderstorm_probability_day": { + "name": "Thunderstorm probability day {forecast_day}" }, - "realfeel_temperature_shade_min_0d": { - "name": "RealFeel temperature shade min today" + "thunderstorm_probability_night": { + "name": "Thunderstorm probability night {forecast_day}" }, - "realfeel_temperature_shade_min_1d": { - "name": "RealFeel temperature shade min day 1" - }, - "realfeel_temperature_shade_min_2d": { - "name": "RealFeel temperature shade min day 2" - }, - "realfeel_temperature_shade_min_3d": { - "name": "RealFeel temperature shade min day 3" - }, - "realfeel_temperature_shade_min_4d": { - "name": "RealFeel temperature shade min day 4" - }, - "solar_irradiance_day_0d": { - "name": "Solar irradiance today" - }, - "solar_irradiance_day_1d": { - "name": "Solar irradiance day 1" - }, - "solar_irradiance_day_2d": { - "name": "Solar irradiance day 2" - }, - "solar_irradiance_day_3d": { - "name": "Solar irradiance day 3" - }, - "solar_irradiance_day_4d": { - "name": "Solar irradiance day 4" - }, - "solar_irradiance_night_0d": { - "name": "Solar irradiance tonight" - }, - "solar_irradiance_night_1d": { - "name": "Solar irradiance night 1" - }, - "solar_irradiance_night_2d": { - "name": "Solar irradiance night 2" - }, - "solar_irradiance_night_3d": { - "name": "Solar irradiance night 3" - }, - "solar_irradiance_night_4d": { - "name": "Solar irradiance night 4" - }, - "thunderstorm_probability_day_0d": { - "name": "Thunderstorm probability today" - }, - "thunderstorm_probability_day_1d": { - "name": "Thunderstorm probability day 1" - }, - "thunderstorm_probability_day_2d": { - "name": "Thunderstorm probability day 2" - }, - "thunderstorm_probability_day_3d": { - "name": "Thunderstorm probability day 3" - }, - "thunderstorm_probability_day_4d": { - "name": "Thunderstorm probability day 4" - }, - "thunderstorm_probability_night_0d": { - "name": "Thunderstorm probability tonight" - }, - "thunderstorm_probability_night_1d": { - "name": "Thunderstorm probability night 1" - }, - "thunderstorm_probability_night_2d": { - "name": "Thunderstorm probability night 2" - }, - "thunderstorm_probability_night_3d": { - "name": "Thunderstorm probability night 3" - }, - "thunderstorm_probability_night_4d": { - "name": "Thunderstorm probability night 4" - }, - "tree_pollen_0d": { - "name": "Tree pollen today", + "tree_pollen": { + "name": "Tree pollen day {forecast_day}", "state_attributes": { "level": { - "name": "[%key:component::accuweather::entity::sensor::grass_pollen_0d::state_attributes::level::name%]", + "name": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::name%]", "state": { - "good": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::good%]", - "hazardous": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::hazardous%]", - "high": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::high%]", - "low": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::low%]", - "moderate": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::moderate%]", - "unhealthy": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::unhealthy%]" - } - } - } - }, - "tree_pollen_1d": { - "name": "Tree pollen day 1", - "state_attributes": { - "level": { - "name": "[%key:component::accuweather::entity::sensor::grass_pollen_0d::state_attributes::level::name%]", - "state": { - "good": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::good%]", - "hazardous": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::hazardous%]", - "high": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::high%]", - "low": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::low%]", - "moderate": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::moderate%]", - "unhealthy": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::unhealthy%]" - } - } - } - }, - "tree_pollen_2d": { - "name": "Tree pollen day 2", - "state_attributes": { - "level": { - "name": "[%key:component::accuweather::entity::sensor::grass_pollen_0d::state_attributes::level::name%]", - "state": { - "good": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::good%]", - "hazardous": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::hazardous%]", - "high": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::high%]", - "low": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::low%]", - "moderate": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::moderate%]", - "unhealthy": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::unhealthy%]" - } - } - } - }, - "tree_pollen_3d": { - "name": "Tree pollen day 3", - "state_attributes": { - "level": { - "name": "[%key:component::accuweather::entity::sensor::grass_pollen_0d::state_attributes::level::name%]", - "state": { - "good": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::good%]", - "hazardous": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::hazardous%]", - "high": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::high%]", - "low": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::low%]", - "moderate": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::moderate%]", - "unhealthy": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::unhealthy%]" - } - } - } - }, - "tree_pollen_4d": { - "name": "Tree pollen day 4", - "state_attributes": { - "level": { - "name": "[%key:component::accuweather::entity::sensor::grass_pollen_0d::state_attributes::level::name%]", - "state": { - "good": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::good%]", - "hazardous": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::hazardous%]", - "high": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::high%]", - "low": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::low%]", - "moderate": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::moderate%]", - "unhealthy": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::unhealthy%]" + "good": "[%key:component::accuweather::entity::sensor::air_quality::state::good%]", + "hazardous": "[%key:component::accuweather::entity::sensor::air_quality::state::hazardous%]", + "high": "[%key:component::accuweather::entity::sensor::air_quality::state::high%]", + "low": "[%key:component::accuweather::entity::sensor::air_quality::state::low%]", + "moderate": "[%key:component::accuweather::entity::sensor::air_quality::state::moderate%]", + "unhealthy": "[%key:component::accuweather::entity::sensor::air_quality::state::unhealthy%]" } } } @@ -624,94 +168,30 @@ "name": "UV index", "state_attributes": { "level": { - "name": "[%key:component::accuweather::entity::sensor::grass_pollen_0d::state_attributes::level::name%]", + "name": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::name%]", "state": { - "good": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::good%]", - "hazardous": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::hazardous%]", - "high": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::high%]", - "low": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::low%]", - "moderate": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::moderate%]", - "unhealthy": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::unhealthy%]" + "good": "[%key:component::accuweather::entity::sensor::air_quality::state::good%]", + "hazardous": "[%key:component::accuweather::entity::sensor::air_quality::state::hazardous%]", + "high": "[%key:component::accuweather::entity::sensor::air_quality::state::high%]", + "low": "[%key:component::accuweather::entity::sensor::air_quality::state::low%]", + "moderate": "[%key:component::accuweather::entity::sensor::air_quality::state::moderate%]", + "unhealthy": "[%key:component::accuweather::entity::sensor::air_quality::state::unhealthy%]" } } } }, - "uv_index_0d": { - "name": "UV index today", + "uv_index_forecast": { + "name": "UV index day {forecast_day}", "state_attributes": { "level": { - "name": "[%key:component::accuweather::entity::sensor::grass_pollen_0d::state_attributes::level::name%]", + "name": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::name%]", "state": { - "good": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::good%]", - "hazardous": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::hazardous%]", - "high": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::high%]", - "low": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::low%]", - "moderate": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::moderate%]", - "unhealthy": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::unhealthy%]" - } - } - } - }, - "uv_index_1d": { - "name": "UV index day 1", - "state_attributes": { - "level": { - "name": "[%key:component::accuweather::entity::sensor::grass_pollen_0d::state_attributes::level::name%]", - "state": { - "good": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::good%]", - "hazardous": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::hazardous%]", - "high": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::high%]", - "low": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::low%]", - "moderate": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::moderate%]", - "unhealthy": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::unhealthy%]" - } - } - } - }, - "uv_index_2d": { - "name": "UV index day 2", - "state_attributes": { - "level": { - "name": "[%key:component::accuweather::entity::sensor::grass_pollen_0d::state_attributes::level::name%]", - "state": { - "good": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::good%]", - "hazardous": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::hazardous%]", - "high": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::high%]", - "low": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::low%]", - "moderate": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::moderate%]", - "unhealthy": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::unhealthy%]" - } - } - } - }, - "uv_index_3d": { - "name": "UV index day 3", - "state_attributes": { - "level": { - "name": "[%key:component::accuweather::entity::sensor::grass_pollen_0d::state_attributes::level::name%]", - "state": { - "good": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::good%]", - "hazardous": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::hazardous%]", - "high": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::high%]", - "low": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::low%]", - "moderate": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::moderate%]", - "unhealthy": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::unhealthy%]" - } - } - } - }, - "uv_index_4d": { - "name": "UV index day 4", - "state_attributes": { - "level": { - "name": "[%key:component::accuweather::entity::sensor::grass_pollen_0d::state_attributes::level::name%]", - "state": { - "good": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::good%]", - "hazardous": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::hazardous%]", - "high": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::high%]", - "low": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::low%]", - "moderate": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::moderate%]", - "unhealthy": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::unhealthy%]" + "good": "[%key:component::accuweather::entity::sensor::air_quality::state::good%]", + "hazardous": "[%key:component::accuweather::entity::sensor::air_quality::state::hazardous%]", + "high": "[%key:component::accuweather::entity::sensor::air_quality::state::high%]", + "low": "[%key:component::accuweather::entity::sensor::air_quality::state::low%]", + "moderate": "[%key:component::accuweather::entity::sensor::air_quality::state::moderate%]", + "unhealthy": "[%key:component::accuweather::entity::sensor::air_quality::state::unhealthy%]" } } } @@ -728,65 +208,17 @@ "wind_gust_speed": { "name": "[%key:component::weather::entity_component::_::state_attributes::wind_gust_speed::name%]" }, - "wind_gust_speed_day_0d": { - "name": "Wind gust speed today" + "wind_gust_speed_day": { + "name": "Wind gust speed day {forecast_day}" }, - "wind_gust_speed_day_1d": { - "name": "Wind gust speed day 1" + "wind_gust_speed_night": { + "name": "Wind gust speed night {forecast_day}" }, - "wind_gust_speed_day_2d": { - "name": "Wind gust speed day 2" + "wind_speed_day": { + "name": "Wind speed day {forecast_day}" }, - "wind_gust_speed_day_3d": { - "name": "Wind gust speed day 3" - }, - "wind_gust_speed_day_4d": { - "name": "Wind gust speed day 4" - }, - "wind_gust_speed_night_0d": { - "name": "Wind gust speed tonight" - }, - "wind_gust_speed_night_1d": { - "name": "Wind gust speed night 1" - }, - "wind_gust_speed_night_2d": { - "name": "Wind gust speed night 2" - }, - "wind_gust_speed_night_3d": { - "name": "Wind gust speed night 3" - }, - "wind_gust_speed_night_4d": { - "name": "Wind gust speed night 4" - }, - "wind_speed_day_0d": { - "name": "Wind speed today" - }, - "wind_speed_day_1d": { - "name": "Wind speed day 1" - }, - "wind_speed_day_2d": { - "name": "Wind speed day 2" - }, - "wind_speed_day_3d": { - "name": "Wind speed day 3" - }, - "wind_speed_day_4d": { - "name": "Wind speed day 4" - }, - "wind_speed_night_0d": { - "name": "Wind speed tonight" - }, - "wind_speed_night_1d": { - "name": "Wind speed night 1" - }, - "wind_speed_night_2d": { - "name": "Wind speed night 2" - }, - "wind_speed_night_3d": { - "name": "Wind speed night 3" - }, - "wind_speed_night_4d": { - "name": "Wind speed night 4" + "wind_speed_night": { + "name": "Wind speed night {forecast_day}" } } }, diff --git a/tests/components/accuweather/snapshots/test_sensor.ambr b/tests/components/accuweather/snapshots/test_sensor.ambr index 42783f375b0..61e37047bda 100644 --- a/tests/components/accuweather/snapshots/test_sensor.ambr +++ b/tests/components/accuweather/snapshots/test_sensor.ambr @@ -1,4 +1,70 @@ # serializer version: 1 +# name: test_sensor[sensor.home_air_quality_day_0-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'good', + 'hazardous', + 'high', + 'low', + 'moderate', + 'unhealthy', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.home_air_quality_day_0', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': 'mdi:air-filter', + 'original_name': 'Air quality day 0', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'air_quality', + 'unique_id': '0123456-airquality-0', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[sensor.home_air_quality_day_0-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'device_class': 'enum', + 'friendly_name': 'Home Air quality day 0', + 'icon': 'mdi:air-filter', + 'options': list([ + 'good', + 'hazardous', + 'high', + 'low', + 'moderate', + 'unhealthy', + ]), + }), + 'context': , + 'entity_id': 'sensor.home_air_quality_day_0', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'good', + }) +# --- # name: test_sensor[sensor.home_air_quality_day_1-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -36,7 +102,7 @@ 'platform': 'accuweather', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'air_quality_1d', + 'translation_key': 'air_quality', 'unique_id': '0123456-airquality-1', 'unit_of_measurement': None, }) @@ -102,7 +168,7 @@ 'platform': 'accuweather', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'air_quality_2d', + 'translation_key': 'air_quality', 'unique_id': '0123456-airquality-2', 'unit_of_measurement': None, }) @@ -168,7 +234,7 @@ 'platform': 'accuweather', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'air_quality_3d', + 'translation_key': 'air_quality', 'unique_id': '0123456-airquality-3', 'unit_of_measurement': None, }) @@ -234,7 +300,7 @@ 'platform': 'accuweather', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'air_quality_4d', + 'translation_key': 'air_quality', 'unique_id': '0123456-airquality-4', 'unit_of_measurement': None, }) @@ -263,72 +329,6 @@ 'state': 'good', }) # --- -# name: test_sensor[sensor.home_air_quality_today-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'options': list([ - 'good', - 'hazardous', - 'high', - 'low', - 'moderate', - 'unhealthy', - ]), - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.home_air_quality_today', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': 'mdi:air-filter', - 'original_name': 'Air quality today', - 'platform': 'accuweather', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'air_quality_0d', - 'unique_id': '0123456-airquality-0', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensor[sensor.home_air_quality_today-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by AccuWeather', - 'device_class': 'enum', - 'friendly_name': 'Home Air quality today', - 'icon': 'mdi:air-filter', - 'options': list([ - 'good', - 'hazardous', - 'high', - 'low', - 'moderate', - 'unhealthy', - ]), - }), - 'context': , - 'entity_id': 'sensor.home_air_quality_today', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'good', - }) -# --- # name: test_sensor[sensor.home_apparent_temperature-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -489,6 +489,55 @@ 'state': '10', }) # --- +# name: test_sensor[sensor.home_cloud_cover_day_0-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.home_cloud_cover_day_0', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:weather-cloudy', + 'original_name': 'Cloud cover day 0', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'cloud_cover_day', + 'unique_id': '0123456-cloudcoverday-0', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensor[sensor.home_cloud_cover_day_0-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home Cloud cover day 0', + 'icon': 'mdi:weather-cloudy', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.home_cloud_cover_day_0', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '58', + }) +# --- # name: test_sensor[sensor.home_cloud_cover_day_1-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -517,7 +566,7 @@ 'platform': 'accuweather', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'cloud_cover_day_1d', + 'translation_key': 'cloud_cover_day', 'unique_id': '0123456-cloudcoverday-1', 'unit_of_measurement': '%', }) @@ -566,7 +615,7 @@ 'platform': 'accuweather', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'cloud_cover_day_2d', + 'translation_key': 'cloud_cover_day', 'unique_id': '0123456-cloudcoverday-2', 'unit_of_measurement': '%', }) @@ -615,7 +664,7 @@ 'platform': 'accuweather', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'cloud_cover_day_3d', + 'translation_key': 'cloud_cover_day', 'unique_id': '0123456-cloudcoverday-3', 'unit_of_measurement': '%', }) @@ -664,7 +713,7 @@ 'platform': 'accuweather', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'cloud_cover_day_4d', + 'translation_key': 'cloud_cover_day', 'unique_id': '0123456-cloudcoverday-4', 'unit_of_measurement': '%', }) @@ -685,6 +734,55 @@ 'state': '50', }) # --- +# name: test_sensor[sensor.home_cloud_cover_night_0-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.home_cloud_cover_night_0', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:weather-cloudy', + 'original_name': 'Cloud cover night 0', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'cloud_cover_night', + 'unique_id': '0123456-cloudcovernight-0', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensor[sensor.home_cloud_cover_night_0-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home Cloud cover night 0', + 'icon': 'mdi:weather-cloudy', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.home_cloud_cover_night_0', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '65', + }) +# --- # name: test_sensor[sensor.home_cloud_cover_night_1-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -713,7 +811,7 @@ 'platform': 'accuweather', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'cloud_cover_night_1d', + 'translation_key': 'cloud_cover_night', 'unique_id': '0123456-cloudcovernight-1', 'unit_of_measurement': '%', }) @@ -762,7 +860,7 @@ 'platform': 'accuweather', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'cloud_cover_night_2d', + 'translation_key': 'cloud_cover_night', 'unique_id': '0123456-cloudcovernight-2', 'unit_of_measurement': '%', }) @@ -811,7 +909,7 @@ 'platform': 'accuweather', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'cloud_cover_night_3d', + 'translation_key': 'cloud_cover_night', 'unique_id': '0123456-cloudcovernight-3', 'unit_of_measurement': '%', }) @@ -860,7 +958,7 @@ 'platform': 'accuweather', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'cloud_cover_night_4d', + 'translation_key': 'cloud_cover_night', 'unique_id': '0123456-cloudcovernight-4', 'unit_of_measurement': '%', }) @@ -881,7 +979,7 @@ 'state': '13', }) # --- -# name: test_sensor[sensor.home_cloud_cover_today-entry] +# name: test_sensor[sensor.home_condition_day_0-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -893,7 +991,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.home_cloud_cover_today', + 'entity_id': 'sensor.home_condition_day_0', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -904,79 +1002,28 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:weather-cloudy', - 'original_name': 'Cloud cover today', + 'original_icon': None, + 'original_name': 'Condition day 0', 'platform': 'accuweather', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'cloud_cover_day_0d', - 'unique_id': '0123456-cloudcoverday-0', - 'unit_of_measurement': '%', + 'translation_key': 'condition_day', + 'unique_id': '0123456-longphraseday-0', + 'unit_of_measurement': None, }) # --- -# name: test_sensor[sensor.home_cloud_cover_today-state] +# name: test_sensor[sensor.home_condition_day_0-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by AccuWeather', - 'friendly_name': 'Home Cloud cover today', - 'icon': 'mdi:weather-cloudy', - 'unit_of_measurement': '%', + 'friendly_name': 'Home Condition day 0', }), 'context': , - 'entity_id': 'sensor.home_cloud_cover_today', + 'entity_id': 'sensor.home_condition_day_0', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '58', - }) -# --- -# name: test_sensor[sensor.home_cloud_cover_tonight-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.home_cloud_cover_tonight', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': 'mdi:weather-cloudy', - 'original_name': 'Cloud cover tonight', - 'platform': 'accuweather', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'cloud_cover_night_0d', - 'unique_id': '0123456-cloudcovernight-0', - 'unit_of_measurement': '%', - }) -# --- -# name: test_sensor[sensor.home_cloud_cover_tonight-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by AccuWeather', - 'friendly_name': 'Home Cloud cover tonight', - 'icon': 'mdi:weather-cloudy', - 'unit_of_measurement': '%', - }), - 'context': , - 'entity_id': 'sensor.home_cloud_cover_tonight', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '65', + 'state': 'Clouds and sunshine with a couple of showers and a thunderstorm around late this afternoon', }) # --- # name: test_sensor[sensor.home_condition_day_1-entry] @@ -1007,7 +1054,7 @@ 'platform': 'accuweather', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'condition_day_1d', + 'translation_key': 'condition_day', 'unique_id': '0123456-longphraseday-1', 'unit_of_measurement': None, }) @@ -1054,7 +1101,7 @@ 'platform': 'accuweather', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'condition_day_2d', + 'translation_key': 'condition_day', 'unique_id': '0123456-longphraseday-2', 'unit_of_measurement': None, }) @@ -1101,7 +1148,7 @@ 'platform': 'accuweather', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'condition_day_3d', + 'translation_key': 'condition_day', 'unique_id': '0123456-longphraseday-3', 'unit_of_measurement': None, }) @@ -1148,7 +1195,7 @@ 'platform': 'accuweather', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'condition_day_4d', + 'translation_key': 'condition_day', 'unique_id': '0123456-longphraseday-4', 'unit_of_measurement': None, }) @@ -1167,6 +1214,53 @@ 'state': 'Intervals of clouds and sunshine', }) # --- +# name: test_sensor[sensor.home_condition_night_0-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.home_condition_night_0', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Condition night 0', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'condition_night', + 'unique_id': '0123456-longphrasenight-0', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[sensor.home_condition_night_0-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home Condition night 0', + }), + 'context': , + 'entity_id': 'sensor.home_condition_night_0', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'Partly cloudy', + }) +# --- # name: test_sensor[sensor.home_condition_night_1-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -1195,7 +1289,7 @@ 'platform': 'accuweather', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'condition_night_1d', + 'translation_key': 'condition_night', 'unique_id': '0123456-longphrasenight-1', 'unit_of_measurement': None, }) @@ -1242,7 +1336,7 @@ 'platform': 'accuweather', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'condition_night_2d', + 'translation_key': 'condition_night', 'unique_id': '0123456-longphrasenight-2', 'unit_of_measurement': None, }) @@ -1289,7 +1383,7 @@ 'platform': 'accuweather', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'condition_night_3d', + 'translation_key': 'condition_night', 'unique_id': '0123456-longphrasenight-3', 'unit_of_measurement': None, }) @@ -1336,7 +1430,7 @@ 'platform': 'accuweather', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'condition_night_4d', + 'translation_key': 'condition_night', 'unique_id': '0123456-longphrasenight-4', 'unit_of_measurement': None, }) @@ -1355,100 +1449,6 @@ 'state': 'Mostly clear', }) # --- -# name: test_sensor[sensor.home_condition_today-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.home_condition_today', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Condition today', - 'platform': 'accuweather', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'condition_day_0d', - 'unique_id': '0123456-longphraseday-0', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensor[sensor.home_condition_today-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by AccuWeather', - 'friendly_name': 'Home Condition today', - }), - 'context': , - 'entity_id': 'sensor.home_condition_today', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'Clouds and sunshine with a couple of showers and a thunderstorm around late this afternoon', - }) -# --- -# name: test_sensor[sensor.home_condition_tonight-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.home_condition_tonight', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Condition tonight', - 'platform': 'accuweather', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'condition_night_0d', - 'unique_id': '0123456-longphrasenight-0', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensor[sensor.home_condition_tonight-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by AccuWeather', - 'friendly_name': 'Home Condition tonight', - }), - 'context': , - 'entity_id': 'sensor.home_condition_tonight', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'Partly cloudy', - }) -# --- # name: test_sensor[sensor.home_dew_point-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -1501,6 +1501,56 @@ 'state': '16.2', }) # --- +# name: test_sensor[sensor.home_grass_pollen_day_0-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.home_grass_pollen_day_0', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:grass', + 'original_name': 'Grass pollen day 0', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'grass_pollen', + 'unique_id': '0123456-grass-0', + 'unit_of_measurement': 'p/m³', + }) +# --- +# name: test_sensor[sensor.home_grass_pollen_day_0-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home Grass pollen day 0', + 'icon': 'mdi:grass', + 'level': 'low', + 'unit_of_measurement': 'p/m³', + }), + 'context': , + 'entity_id': 'sensor.home_grass_pollen_day_0', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- # name: test_sensor[sensor.home_grass_pollen_day_1-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -1529,7 +1579,7 @@ 'platform': 'accuweather', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'grass_pollen_1d', + 'translation_key': 'grass_pollen', 'unique_id': '0123456-grass-1', 'unit_of_measurement': 'p/m³', }) @@ -1579,7 +1629,7 @@ 'platform': 'accuweather', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'grass_pollen_2d', + 'translation_key': 'grass_pollen', 'unique_id': '0123456-grass-2', 'unit_of_measurement': 'p/m³', }) @@ -1629,7 +1679,7 @@ 'platform': 'accuweather', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'grass_pollen_3d', + 'translation_key': 'grass_pollen', 'unique_id': '0123456-grass-3', 'unit_of_measurement': 'p/m³', }) @@ -1679,7 +1729,7 @@ 'platform': 'accuweather', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'grass_pollen_4d', + 'translation_key': 'grass_pollen', 'unique_id': '0123456-grass-4', 'unit_of_measurement': 'p/m³', }) @@ -1701,7 +1751,7 @@ 'state': '0', }) # --- -# name: test_sensor[sensor.home_grass_pollen_today-entry] +# name: test_sensor[sensor.home_hours_of_sun_day_0-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1713,7 +1763,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.home_grass_pollen_today', + 'entity_id': 'sensor.home_hours_of_sun_day_0', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -1724,31 +1774,30 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:grass', - 'original_name': 'Grass pollen today', + 'original_icon': 'mdi:weather-partly-cloudy', + 'original_name': 'Hours of sun day 0', 'platform': 'accuweather', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'grass_pollen_0d', - 'unique_id': '0123456-grass-0', - 'unit_of_measurement': 'p/m³', + 'translation_key': 'hours_of_sun', + 'unique_id': '0123456-hoursofsun-0', + 'unit_of_measurement': , }) # --- -# name: test_sensor[sensor.home_grass_pollen_today-state] +# name: test_sensor[sensor.home_hours_of_sun_day_0-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by AccuWeather', - 'friendly_name': 'Home Grass pollen today', - 'icon': 'mdi:grass', - 'level': 'low', - 'unit_of_measurement': 'p/m³', + 'friendly_name': 'Home Hours of sun day 0', + 'icon': 'mdi:weather-partly-cloudy', + 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.home_grass_pollen_today', + 'entity_id': 'sensor.home_hours_of_sun_day_0', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '0', + 'state': '7.2', }) # --- # name: test_sensor[sensor.home_hours_of_sun_day_1-entry] @@ -1779,7 +1828,7 @@ 'platform': 'accuweather', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'hours_of_sun_1d', + 'translation_key': 'hours_of_sun', 'unique_id': '0123456-hoursofsun-1', 'unit_of_measurement': , }) @@ -1828,7 +1877,7 @@ 'platform': 'accuweather', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'hours_of_sun_2d', + 'translation_key': 'hours_of_sun', 'unique_id': '0123456-hoursofsun-2', 'unit_of_measurement': , }) @@ -1877,7 +1926,7 @@ 'platform': 'accuweather', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'hours_of_sun_3d', + 'translation_key': 'hours_of_sun', 'unique_id': '0123456-hoursofsun-3', 'unit_of_measurement': , }) @@ -1926,7 +1975,7 @@ 'platform': 'accuweather', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'hours_of_sun_4d', + 'translation_key': 'hours_of_sun', 'unique_id': '0123456-hoursofsun-4', 'unit_of_measurement': , }) @@ -1947,7 +1996,7 @@ 'state': '9.2', }) # --- -# name: test_sensor[sensor.home_hours_of_sun_today-entry] +# name: test_sensor[sensor.home_mold_pollen_day_0-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1959,7 +2008,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.home_hours_of_sun_today', + 'entity_id': 'sensor.home_mold_pollen_day_0', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -1970,30 +2019,31 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:weather-partly-cloudy', - 'original_name': 'Hours of sun today', + 'original_icon': 'mdi:blur', + 'original_name': 'Mold pollen day 0', 'platform': 'accuweather', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'hours_of_sun_0d', - 'unique_id': '0123456-hoursofsun-0', - 'unit_of_measurement': , + 'translation_key': 'mold_pollen', + 'unique_id': '0123456-mold-0', + 'unit_of_measurement': 'p/m³', }) # --- -# name: test_sensor[sensor.home_hours_of_sun_today-state] +# name: test_sensor[sensor.home_mold_pollen_day_0-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by AccuWeather', - 'friendly_name': 'Home Hours of sun today', - 'icon': 'mdi:weather-partly-cloudy', - 'unit_of_measurement': , + 'friendly_name': 'Home Mold pollen day 0', + 'icon': 'mdi:blur', + 'level': 'low', + 'unit_of_measurement': 'p/m³', }), 'context': , - 'entity_id': 'sensor.home_hours_of_sun_today', + 'entity_id': 'sensor.home_mold_pollen_day_0', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '7.2', + 'state': '0', }) # --- # name: test_sensor[sensor.home_mold_pollen_day_1-entry] @@ -2024,7 +2074,7 @@ 'platform': 'accuweather', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'mold_pollen_1d', + 'translation_key': 'mold_pollen', 'unique_id': '0123456-mold-1', 'unit_of_measurement': 'p/m³', }) @@ -2074,7 +2124,7 @@ 'platform': 'accuweather', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'mold_pollen_2d', + 'translation_key': 'mold_pollen', 'unique_id': '0123456-mold-2', 'unit_of_measurement': 'p/m³', }) @@ -2124,7 +2174,7 @@ 'platform': 'accuweather', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'mold_pollen_3d', + 'translation_key': 'mold_pollen', 'unique_id': '0123456-mold-3', 'unit_of_measurement': 'p/m³', }) @@ -2174,7 +2224,7 @@ 'platform': 'accuweather', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'mold_pollen_4d', + 'translation_key': 'mold_pollen', 'unique_id': '0123456-mold-4', 'unit_of_measurement': 'p/m³', }) @@ -2196,56 +2246,6 @@ 'state': '0', }) # --- -# name: test_sensor[sensor.home_mold_pollen_today-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.home_mold_pollen_today', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': 'mdi:blur', - 'original_name': 'Mold pollen today', - 'platform': 'accuweather', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'mold_pollen_0d', - 'unique_id': '0123456-mold-0', - 'unit_of_measurement': 'p/m³', - }) -# --- -# name: test_sensor[sensor.home_mold_pollen_today-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by AccuWeather', - 'friendly_name': 'Home Mold pollen today', - 'icon': 'mdi:blur', - 'level': 'low', - 'unit_of_measurement': 'p/m³', - }), - 'context': , - 'entity_id': 'sensor.home_mold_pollen_today', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '0', - }) -# --- # name: test_sensor[sensor.home_precipitation-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -2359,6 +2359,56 @@ 'state': 'falling', }) # --- +# name: test_sensor[sensor.home_ragweed_pollen_day_0-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.home_ragweed_pollen_day_0', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:sprout', + 'original_name': 'Ragweed pollen day 0', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'ragweed_pollen', + 'unique_id': '0123456-ragweed-0', + 'unit_of_measurement': 'p/m³', + }) +# --- +# name: test_sensor[sensor.home_ragweed_pollen_day_0-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home Ragweed pollen day 0', + 'icon': 'mdi:sprout', + 'level': 'low', + 'unit_of_measurement': 'p/m³', + }), + 'context': , + 'entity_id': 'sensor.home_ragweed_pollen_day_0', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- # name: test_sensor[sensor.home_ragweed_pollen_day_1-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -2387,7 +2437,7 @@ 'platform': 'accuweather', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'ragweed_pollen_1d', + 'translation_key': 'ragweed_pollen', 'unique_id': '0123456-ragweed-1', 'unit_of_measurement': 'p/m³', }) @@ -2437,7 +2487,7 @@ 'platform': 'accuweather', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'ragweed_pollen_2d', + 'translation_key': 'ragweed_pollen', 'unique_id': '0123456-ragweed-2', 'unit_of_measurement': 'p/m³', }) @@ -2487,7 +2537,7 @@ 'platform': 'accuweather', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'ragweed_pollen_3d', + 'translation_key': 'ragweed_pollen', 'unique_id': '0123456-ragweed-3', 'unit_of_measurement': 'p/m³', }) @@ -2537,7 +2587,7 @@ 'platform': 'accuweather', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'ragweed_pollen_4d', + 'translation_key': 'ragweed_pollen', 'unique_id': '0123456-ragweed-4', 'unit_of_measurement': 'p/m³', }) @@ -2559,56 +2609,6 @@ 'state': '0', }) # --- -# name: test_sensor[sensor.home_ragweed_pollen_today-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.home_ragweed_pollen_today', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': 'mdi:sprout', - 'original_name': 'Ragweed pollen today', - 'platform': 'accuweather', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'ragweed_pollen_0d', - 'unique_id': '0123456-ragweed-0', - 'unit_of_measurement': 'p/m³', - }) -# --- -# name: test_sensor[sensor.home_ragweed_pollen_today-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by AccuWeather', - 'friendly_name': 'Home Ragweed pollen today', - 'icon': 'mdi:sprout', - 'level': 'low', - 'unit_of_measurement': 'p/m³', - }), - 'context': , - 'entity_id': 'sensor.home_ragweed_pollen_today', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '0', - }) -# --- # name: test_sensor[sensor.home_realfeel_temperature-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -2661,6 +2661,55 @@ 'state': '25.1', }) # --- +# name: test_sensor[sensor.home_realfeel_temperature_max_day_0-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.home_realfeel_temperature_max_day_0', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'RealFeel temperature max day 0', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'realfeel_temperature_max', + 'unique_id': '0123456-realfeeltemperaturemax-0', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_realfeel_temperature_max_day_0-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'device_class': 'temperature', + 'friendly_name': 'Home RealFeel temperature max day 0', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_realfeel_temperature_max_day_0', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '29.8', + }) +# --- # name: test_sensor[sensor.home_realfeel_temperature_max_day_1-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -2689,7 +2738,7 @@ 'platform': 'accuweather', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'realfeel_temperature_max_1d', + 'translation_key': 'realfeel_temperature_max', 'unique_id': '0123456-realfeeltemperaturemax-1', 'unit_of_measurement': , }) @@ -2738,7 +2787,7 @@ 'platform': 'accuweather', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'realfeel_temperature_max_2d', + 'translation_key': 'realfeel_temperature_max', 'unique_id': '0123456-realfeeltemperaturemax-2', 'unit_of_measurement': , }) @@ -2787,7 +2836,7 @@ 'platform': 'accuweather', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'realfeel_temperature_max_3d', + 'translation_key': 'realfeel_temperature_max', 'unique_id': '0123456-realfeeltemperaturemax-3', 'unit_of_measurement': , }) @@ -2836,7 +2885,7 @@ 'platform': 'accuweather', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'realfeel_temperature_max_4d', + 'translation_key': 'realfeel_temperature_max', 'unique_id': '0123456-realfeeltemperaturemax-4', 'unit_of_measurement': , }) @@ -2857,7 +2906,7 @@ 'state': '22.2', }) # --- -# name: test_sensor[sensor.home_realfeel_temperature_max_today-entry] +# name: test_sensor[sensor.home_realfeel_temperature_min_day_0-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2869,7 +2918,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.home_realfeel_temperature_max_today', + 'entity_id': 'sensor.home_realfeel_temperature_min_day_0', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -2881,29 +2930,29 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'RealFeel temperature max today', + 'original_name': 'RealFeel temperature min day 0', 'platform': 'accuweather', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'realfeel_temperature_max_0d', - 'unique_id': '0123456-realfeeltemperaturemax-0', + 'translation_key': 'realfeel_temperature_min', + 'unique_id': '0123456-realfeeltemperaturemin-0', 'unit_of_measurement': , }) # --- -# name: test_sensor[sensor.home_realfeel_temperature_max_today-state] +# name: test_sensor[sensor.home_realfeel_temperature_min_day_0-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by AccuWeather', 'device_class': 'temperature', - 'friendly_name': 'Home RealFeel temperature max today', + 'friendly_name': 'Home RealFeel temperature min day 0', 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.home_realfeel_temperature_max_today', + 'entity_id': 'sensor.home_realfeel_temperature_min_day_0', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '29.8', + 'state': '15.1', }) # --- # name: test_sensor[sensor.home_realfeel_temperature_min_day_1-entry] @@ -2934,7 +2983,7 @@ 'platform': 'accuweather', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'realfeel_temperature_min_1d', + 'translation_key': 'realfeel_temperature_min', 'unique_id': '0123456-realfeeltemperaturemin-1', 'unit_of_measurement': , }) @@ -2983,7 +3032,7 @@ 'platform': 'accuweather', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'realfeel_temperature_min_2d', + 'translation_key': 'realfeel_temperature_min', 'unique_id': '0123456-realfeeltemperaturemin-2', 'unit_of_measurement': , }) @@ -3032,7 +3081,7 @@ 'platform': 'accuweather', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'realfeel_temperature_min_3d', + 'translation_key': 'realfeel_temperature_min', 'unique_id': '0123456-realfeeltemperaturemin-3', 'unit_of_measurement': , }) @@ -3081,7 +3130,7 @@ 'platform': 'accuweather', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'realfeel_temperature_min_4d', + 'translation_key': 'realfeel_temperature_min', 'unique_id': '0123456-realfeeltemperaturemin-4', 'unit_of_measurement': , }) @@ -3102,55 +3151,6 @@ 'state': '11.3', }) # --- -# name: test_sensor[sensor.home_realfeel_temperature_min_today-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.home_realfeel_temperature_min_today', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'RealFeel temperature min today', - 'platform': 'accuweather', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'realfeel_temperature_min_0d', - 'unique_id': '0123456-realfeeltemperaturemin-0', - 'unit_of_measurement': , - }) -# --- -# name: test_sensor[sensor.home_realfeel_temperature_min_today-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by AccuWeather', - 'device_class': 'temperature', - 'friendly_name': 'Home RealFeel temperature min today', - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.home_realfeel_temperature_min_today', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '15.1', - }) -# --- # name: test_sensor[sensor.home_realfeel_temperature_shade-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -3203,6 +3203,55 @@ 'state': '21.1', }) # --- +# name: test_sensor[sensor.home_realfeel_temperature_shade_max_day_0-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.home_realfeel_temperature_shade_max_day_0', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'RealFeel temperature shade max day 0', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'realfeel_temperature_shade_max', + 'unique_id': '0123456-realfeeltemperatureshademax-0', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_realfeel_temperature_shade_max_day_0-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'device_class': 'temperature', + 'friendly_name': 'Home RealFeel temperature shade max day 0', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_realfeel_temperature_shade_max_day_0', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '28.0', + }) +# --- # name: test_sensor[sensor.home_realfeel_temperature_shade_max_day_1-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -3231,7 +3280,7 @@ 'platform': 'accuweather', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'realfeel_temperature_shade_max_1d', + 'translation_key': 'realfeel_temperature_shade_max', 'unique_id': '0123456-realfeeltemperatureshademax-1', 'unit_of_measurement': , }) @@ -3280,7 +3329,7 @@ 'platform': 'accuweather', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'realfeel_temperature_shade_max_2d', + 'translation_key': 'realfeel_temperature_shade_max', 'unique_id': '0123456-realfeeltemperatureshademax-2', 'unit_of_measurement': , }) @@ -3329,7 +3378,7 @@ 'platform': 'accuweather', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'realfeel_temperature_shade_max_3d', + 'translation_key': 'realfeel_temperature_shade_max', 'unique_id': '0123456-realfeeltemperatureshademax-3', 'unit_of_measurement': , }) @@ -3378,7 +3427,7 @@ 'platform': 'accuweather', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'realfeel_temperature_shade_max_4d', + 'translation_key': 'realfeel_temperature_shade_max', 'unique_id': '0123456-realfeeltemperatureshademax-4', 'unit_of_measurement': , }) @@ -3399,7 +3448,7 @@ 'state': '19.5', }) # --- -# name: test_sensor[sensor.home_realfeel_temperature_shade_max_today-entry] +# name: test_sensor[sensor.home_realfeel_temperature_shade_min_day_0-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -3411,7 +3460,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.home_realfeel_temperature_shade_max_today', + 'entity_id': 'sensor.home_realfeel_temperature_shade_min_day_0', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -3423,29 +3472,29 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'RealFeel temperature shade max today', + 'original_name': 'RealFeel temperature shade min day 0', 'platform': 'accuweather', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'realfeel_temperature_shade_max_0d', - 'unique_id': '0123456-realfeeltemperatureshademax-0', + 'translation_key': 'realfeel_temperature_shade_min', + 'unique_id': '0123456-realfeeltemperatureshademin-0', 'unit_of_measurement': , }) # --- -# name: test_sensor[sensor.home_realfeel_temperature_shade_max_today-state] +# name: test_sensor[sensor.home_realfeel_temperature_shade_min_day_0-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by AccuWeather', 'device_class': 'temperature', - 'friendly_name': 'Home RealFeel temperature shade max today', + 'friendly_name': 'Home RealFeel temperature shade min day 0', 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.home_realfeel_temperature_shade_max_today', + 'entity_id': 'sensor.home_realfeel_temperature_shade_min_day_0', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '28.0', + 'state': '15.1', }) # --- # name: test_sensor[sensor.home_realfeel_temperature_shade_min_day_1-entry] @@ -3476,7 +3525,7 @@ 'platform': 'accuweather', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'realfeel_temperature_shade_min_1d', + 'translation_key': 'realfeel_temperature_shade_min', 'unique_id': '0123456-realfeeltemperatureshademin-1', 'unit_of_measurement': , }) @@ -3525,7 +3574,7 @@ 'platform': 'accuweather', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'realfeel_temperature_shade_min_2d', + 'translation_key': 'realfeel_temperature_shade_min', 'unique_id': '0123456-realfeeltemperatureshademin-2', 'unit_of_measurement': , }) @@ -3574,7 +3623,7 @@ 'platform': 'accuweather', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'realfeel_temperature_shade_min_3d', + 'translation_key': 'realfeel_temperature_shade_min', 'unique_id': '0123456-realfeeltemperatureshademin-3', 'unit_of_measurement': , }) @@ -3623,7 +3672,7 @@ 'platform': 'accuweather', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'realfeel_temperature_shade_min_4d', + 'translation_key': 'realfeel_temperature_shade_min', 'unique_id': '0123456-realfeeltemperatureshademin-4', 'unit_of_measurement': , }) @@ -3644,7 +3693,7 @@ 'state': '11.3', }) # --- -# name: test_sensor[sensor.home_realfeel_temperature_shade_min_today-entry] +# name: test_sensor[sensor.home_solar_irradiance_day_0-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -3656,7 +3705,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.home_realfeel_temperature_shade_min_today', + 'entity_id': 'sensor.home_solar_irradiance_day_0', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -3666,31 +3715,31 @@ 'name': None, 'options': dict({ }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'RealFeel temperature shade min today', + 'original_device_class': None, + 'original_icon': 'mdi:weather-sunny', + 'original_name': 'Solar irradiance day 0', 'platform': 'accuweather', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'realfeel_temperature_shade_min_0d', - 'unique_id': '0123456-realfeeltemperatureshademin-0', - 'unit_of_measurement': , + 'translation_key': 'solar_irradiance_day', + 'unique_id': '0123456-solarirradianceday-0', + 'unit_of_measurement': , }) # --- -# name: test_sensor[sensor.home_realfeel_temperature_shade_min_today-state] +# name: test_sensor[sensor.home_solar_irradiance_day_0-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by AccuWeather', - 'device_class': 'temperature', - 'friendly_name': 'Home RealFeel temperature shade min today', - 'unit_of_measurement': , + 'friendly_name': 'Home Solar irradiance day 0', + 'icon': 'mdi:weather-sunny', + 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.home_realfeel_temperature_shade_min_today', + 'entity_id': 'sensor.home_solar_irradiance_day_0', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '15.1', + 'state': '7447.1', }) # --- # name: test_sensor[sensor.home_solar_irradiance_day_1-entry] @@ -3721,7 +3770,7 @@ 'platform': 'accuweather', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'solar_irradiance_day_1d', + 'translation_key': 'solar_irradiance_day', 'unique_id': '0123456-solarirradianceday-1', 'unit_of_measurement': , }) @@ -3770,7 +3819,7 @@ 'platform': 'accuweather', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'solar_irradiance_day_2d', + 'translation_key': 'solar_irradiance_day', 'unique_id': '0123456-solarirradianceday-2', 'unit_of_measurement': , }) @@ -3819,7 +3868,7 @@ 'platform': 'accuweather', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'solar_irradiance_day_3d', + 'translation_key': 'solar_irradiance_day', 'unique_id': '0123456-solarirradianceday-3', 'unit_of_measurement': , }) @@ -3868,7 +3917,7 @@ 'platform': 'accuweather', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'solar_irradiance_day_4d', + 'translation_key': 'solar_irradiance_day', 'unique_id': '0123456-solarirradianceday-4', 'unit_of_measurement': , }) @@ -3889,6 +3938,55 @@ 'state': '7447.1', }) # --- +# name: test_sensor[sensor.home_solar_irradiance_night_0-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.home_solar_irradiance_night_0', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:weather-sunny', + 'original_name': 'Solar irradiance night 0', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'solar_irradiance_night', + 'unique_id': '0123456-solarirradiancenight-0', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_solar_irradiance_night_0-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home Solar irradiance night 0', + 'icon': 'mdi:weather-sunny', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_solar_irradiance_night_0', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '271.6', + }) +# --- # name: test_sensor[sensor.home_solar_irradiance_night_1-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -3917,7 +4015,7 @@ 'platform': 'accuweather', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'solar_irradiance_night_1d', + 'translation_key': 'solar_irradiance_night', 'unique_id': '0123456-solarirradiancenight-1', 'unit_of_measurement': , }) @@ -3966,7 +4064,7 @@ 'platform': 'accuweather', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'solar_irradiance_night_2d', + 'translation_key': 'solar_irradiance_night', 'unique_id': '0123456-solarirradiancenight-2', 'unit_of_measurement': , }) @@ -4015,7 +4113,7 @@ 'platform': 'accuweather', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'solar_irradiance_night_3d', + 'translation_key': 'solar_irradiance_night', 'unique_id': '0123456-solarirradiancenight-3', 'unit_of_measurement': , }) @@ -4064,7 +4162,7 @@ 'platform': 'accuweather', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'solar_irradiance_night_4d', + 'translation_key': 'solar_irradiance_night', 'unique_id': '0123456-solarirradiancenight-4', 'unit_of_measurement': , }) @@ -4085,7 +4183,7 @@ 'state': '276.1', }) # --- -# name: test_sensor[sensor.home_solar_irradiance_today-entry] +# name: test_sensor[sensor.home_thunderstorm_probability_day_0-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -4097,7 +4195,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.home_solar_irradiance_today', + 'entity_id': 'sensor.home_thunderstorm_probability_day_0', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -4108,79 +4206,30 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:weather-sunny', - 'original_name': 'Solar irradiance today', + 'original_icon': 'mdi:weather-lightning', + 'original_name': 'Thunderstorm probability day 0', 'platform': 'accuweather', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'solar_irradiance_day_0d', - 'unique_id': '0123456-solarirradianceday-0', - 'unit_of_measurement': , + 'translation_key': 'thunderstorm_probability_day', + 'unique_id': '0123456-thunderstormprobabilityday-0', + 'unit_of_measurement': '%', }) # --- -# name: test_sensor[sensor.home_solar_irradiance_today-state] +# name: test_sensor[sensor.home_thunderstorm_probability_day_0-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by AccuWeather', - 'friendly_name': 'Home Solar irradiance today', - 'icon': 'mdi:weather-sunny', - 'unit_of_measurement': , + 'friendly_name': 'Home Thunderstorm probability day 0', + 'icon': 'mdi:weather-lightning', + 'unit_of_measurement': '%', }), 'context': , - 'entity_id': 'sensor.home_solar_irradiance_today', + 'entity_id': 'sensor.home_thunderstorm_probability_day_0', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '7447.1', - }) -# --- -# name: test_sensor[sensor.home_solar_irradiance_tonight-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.home_solar_irradiance_tonight', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': 'mdi:weather-sunny', - 'original_name': 'Solar irradiance tonight', - 'platform': 'accuweather', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'solar_irradiance_night_0d', - 'unique_id': '0123456-solarirradiancenight-0', - 'unit_of_measurement': , - }) -# --- -# name: test_sensor[sensor.home_solar_irradiance_tonight-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by AccuWeather', - 'friendly_name': 'Home Solar irradiance tonight', - 'icon': 'mdi:weather-sunny', - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.home_solar_irradiance_tonight', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '271.6', + 'state': '40', }) # --- # name: test_sensor[sensor.home_thunderstorm_probability_day_1-entry] @@ -4211,7 +4260,7 @@ 'platform': 'accuweather', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'thunderstorm_probability_day_1d', + 'translation_key': 'thunderstorm_probability_day', 'unique_id': '0123456-thunderstormprobabilityday-1', 'unit_of_measurement': '%', }) @@ -4260,7 +4309,7 @@ 'platform': 'accuweather', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'thunderstorm_probability_day_2d', + 'translation_key': 'thunderstorm_probability_day', 'unique_id': '0123456-thunderstormprobabilityday-2', 'unit_of_measurement': '%', }) @@ -4309,7 +4358,7 @@ 'platform': 'accuweather', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'thunderstorm_probability_day_3d', + 'translation_key': 'thunderstorm_probability_day', 'unique_id': '0123456-thunderstormprobabilityday-3', 'unit_of_measurement': '%', }) @@ -4358,7 +4407,7 @@ 'platform': 'accuweather', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'thunderstorm_probability_day_4d', + 'translation_key': 'thunderstorm_probability_day', 'unique_id': '0123456-thunderstormprobabilityday-4', 'unit_of_measurement': '%', }) @@ -4379,6 +4428,55 @@ 'state': '0', }) # --- +# name: test_sensor[sensor.home_thunderstorm_probability_night_0-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.home_thunderstorm_probability_night_0', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:weather-lightning', + 'original_name': 'Thunderstorm probability night 0', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'thunderstorm_probability_night', + 'unique_id': '0123456-thunderstormprobabilitynight-0', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensor[sensor.home_thunderstorm_probability_night_0-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home Thunderstorm probability night 0', + 'icon': 'mdi:weather-lightning', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.home_thunderstorm_probability_night_0', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '40', + }) +# --- # name: test_sensor[sensor.home_thunderstorm_probability_night_1-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -4407,7 +4505,7 @@ 'platform': 'accuweather', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'thunderstorm_probability_night_1d', + 'translation_key': 'thunderstorm_probability_night', 'unique_id': '0123456-thunderstormprobabilitynight-1', 'unit_of_measurement': '%', }) @@ -4456,7 +4554,7 @@ 'platform': 'accuweather', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'thunderstorm_probability_night_2d', + 'translation_key': 'thunderstorm_probability_night', 'unique_id': '0123456-thunderstormprobabilitynight-2', 'unit_of_measurement': '%', }) @@ -4505,7 +4603,7 @@ 'platform': 'accuweather', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'thunderstorm_probability_night_3d', + 'translation_key': 'thunderstorm_probability_night', 'unique_id': '0123456-thunderstormprobabilitynight-3', 'unit_of_measurement': '%', }) @@ -4554,7 +4652,7 @@ 'platform': 'accuweather', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'thunderstorm_probability_night_4d', + 'translation_key': 'thunderstorm_probability_night', 'unique_id': '0123456-thunderstormprobabilitynight-4', 'unit_of_measurement': '%', }) @@ -4575,7 +4673,7 @@ 'state': '0', }) # --- -# name: test_sensor[sensor.home_thunderstorm_probability_today-entry] +# name: test_sensor[sensor.home_tree_pollen_day_0-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -4587,7 +4685,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.home_thunderstorm_probability_today', + 'entity_id': 'sensor.home_tree_pollen_day_0', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -4598,79 +4696,31 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:weather-lightning', - 'original_name': 'Thunderstorm probability today', + 'original_icon': 'mdi:tree-outline', + 'original_name': 'Tree pollen day 0', 'platform': 'accuweather', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'thunderstorm_probability_day_0d', - 'unique_id': '0123456-thunderstormprobabilityday-0', - 'unit_of_measurement': '%', + 'translation_key': 'tree_pollen', + 'unique_id': '0123456-tree-0', + 'unit_of_measurement': 'p/m³', }) # --- -# name: test_sensor[sensor.home_thunderstorm_probability_today-state] +# name: test_sensor[sensor.home_tree_pollen_day_0-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by AccuWeather', - 'friendly_name': 'Home Thunderstorm probability today', - 'icon': 'mdi:weather-lightning', - 'unit_of_measurement': '%', + 'friendly_name': 'Home Tree pollen day 0', + 'icon': 'mdi:tree-outline', + 'level': 'low', + 'unit_of_measurement': 'p/m³', }), 'context': , - 'entity_id': 'sensor.home_thunderstorm_probability_today', + 'entity_id': 'sensor.home_tree_pollen_day_0', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '40', - }) -# --- -# name: test_sensor[sensor.home_thunderstorm_probability_tonight-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.home_thunderstorm_probability_tonight', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': 'mdi:weather-lightning', - 'original_name': 'Thunderstorm probability tonight', - 'platform': 'accuweather', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'thunderstorm_probability_night_0d', - 'unique_id': '0123456-thunderstormprobabilitynight-0', - 'unit_of_measurement': '%', - }) -# --- -# name: test_sensor[sensor.home_thunderstorm_probability_tonight-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by AccuWeather', - 'friendly_name': 'Home Thunderstorm probability tonight', - 'icon': 'mdi:weather-lightning', - 'unit_of_measurement': '%', - }), - 'context': , - 'entity_id': 'sensor.home_thunderstorm_probability_tonight', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '40', + 'state': '0', }) # --- # name: test_sensor[sensor.home_tree_pollen_day_1-entry] @@ -4701,7 +4751,7 @@ 'platform': 'accuweather', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'tree_pollen_1d', + 'translation_key': 'tree_pollen', 'unique_id': '0123456-tree-1', 'unit_of_measurement': 'p/m³', }) @@ -4751,7 +4801,7 @@ 'platform': 'accuweather', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'tree_pollen_2d', + 'translation_key': 'tree_pollen', 'unique_id': '0123456-tree-2', 'unit_of_measurement': 'p/m³', }) @@ -4801,7 +4851,7 @@ 'platform': 'accuweather', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'tree_pollen_3d', + 'translation_key': 'tree_pollen', 'unique_id': '0123456-tree-3', 'unit_of_measurement': 'p/m³', }) @@ -4851,7 +4901,7 @@ 'platform': 'accuweather', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'tree_pollen_4d', + 'translation_key': 'tree_pollen', 'unique_id': '0123456-tree-4', 'unit_of_measurement': 'p/m³', }) @@ -4873,56 +4923,6 @@ 'state': '0', }) # --- -# name: test_sensor[sensor.home_tree_pollen_today-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.home_tree_pollen_today', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': 'mdi:tree-outline', - 'original_name': 'Tree pollen today', - 'platform': 'accuweather', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'tree_pollen_0d', - 'unique_id': '0123456-tree-0', - 'unit_of_measurement': 'p/m³', - }) -# --- -# name: test_sensor[sensor.home_tree_pollen_today-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by AccuWeather', - 'friendly_name': 'Home Tree pollen today', - 'icon': 'mdi:tree-outline', - 'level': 'low', - 'unit_of_measurement': 'p/m³', - }), - 'context': , - 'entity_id': 'sensor.home_tree_pollen_today', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '0', - }) -# --- # name: test_sensor[sensor.home_uv_index-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -4976,6 +4976,56 @@ 'state': '6', }) # --- +# name: test_sensor[sensor.home_uv_index_day_0-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.home_uv_index_day_0', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:weather-sunny', + 'original_name': 'UV index day 0', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'uv_index_forecast', + 'unique_id': '0123456-uvindex-0', + 'unit_of_measurement': 'UV index', + }) +# --- +# name: test_sensor[sensor.home_uv_index_day_0-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home UV index day 0', + 'icon': 'mdi:weather-sunny', + 'level': 'moderate', + 'unit_of_measurement': 'UV index', + }), + 'context': , + 'entity_id': 'sensor.home_uv_index_day_0', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '5', + }) +# --- # name: test_sensor[sensor.home_uv_index_day_1-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -5004,7 +5054,7 @@ 'platform': 'accuweather', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'uv_index_1d', + 'translation_key': 'uv_index_forecast', 'unique_id': '0123456-uvindex-1', 'unit_of_measurement': 'UV index', }) @@ -5054,7 +5104,7 @@ 'platform': 'accuweather', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'uv_index_2d', + 'translation_key': 'uv_index_forecast', 'unique_id': '0123456-uvindex-2', 'unit_of_measurement': 'UV index', }) @@ -5104,7 +5154,7 @@ 'platform': 'accuweather', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'uv_index_3d', + 'translation_key': 'uv_index_forecast', 'unique_id': '0123456-uvindex-3', 'unit_of_measurement': 'UV index', }) @@ -5154,7 +5204,7 @@ 'platform': 'accuweather', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'uv_index_4d', + 'translation_key': 'uv_index_forecast', 'unique_id': '0123456-uvindex-4', 'unit_of_measurement': 'UV index', }) @@ -5176,56 +5226,6 @@ 'state': '7', }) # --- -# name: test_sensor[sensor.home_uv_index_today-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.home_uv_index_today', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': 'mdi:weather-sunny', - 'original_name': 'UV index today', - 'platform': 'accuweather', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'uv_index_0d', - 'unique_id': '0123456-uvindex-0', - 'unit_of_measurement': 'UV index', - }) -# --- -# name: test_sensor[sensor.home_uv_index_today-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by AccuWeather', - 'friendly_name': 'Home UV index today', - 'icon': 'mdi:weather-sunny', - 'level': 'moderate', - 'unit_of_measurement': 'UV index', - }), - 'context': , - 'entity_id': 'sensor.home_uv_index_today', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '5', - }) -# --- # name: test_sensor[sensor.home_wet_bulb_temperature-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -5382,6 +5382,56 @@ 'state': '20.3', }) # --- +# name: test_sensor[sensor.home_wind_gust_speed_day_0-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.home_wind_gust_speed_day_0', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Wind gust speed day 0', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'wind_gust_speed_day', + 'unique_id': '0123456-windgustday-0', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_wind_gust_speed_day_0-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'device_class': 'wind_speed', + 'direction': 'S', + 'friendly_name': 'Home Wind gust speed day 0', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_wind_gust_speed_day_0', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '29.6', + }) +# --- # name: test_sensor[sensor.home_wind_gust_speed_day_1-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -5410,7 +5460,7 @@ 'platform': 'accuweather', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'wind_gust_speed_day_1d', + 'translation_key': 'wind_gust_speed_day', 'unique_id': '0123456-windgustday-1', 'unit_of_measurement': , }) @@ -5460,7 +5510,7 @@ 'platform': 'accuweather', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'wind_gust_speed_day_2d', + 'translation_key': 'wind_gust_speed_day', 'unique_id': '0123456-windgustday-2', 'unit_of_measurement': , }) @@ -5510,7 +5560,7 @@ 'platform': 'accuweather', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'wind_gust_speed_day_3d', + 'translation_key': 'wind_gust_speed_day', 'unique_id': '0123456-windgustday-3', 'unit_of_measurement': , }) @@ -5560,7 +5610,7 @@ 'platform': 'accuweather', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'wind_gust_speed_day_4d', + 'translation_key': 'wind_gust_speed_day', 'unique_id': '0123456-windgustday-4', 'unit_of_measurement': , }) @@ -5582,6 +5632,56 @@ 'state': '27.8', }) # --- +# name: test_sensor[sensor.home_wind_gust_speed_night_0-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.home_wind_gust_speed_night_0', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Wind gust speed night 0', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'wind_gust_speed_night', + 'unique_id': '0123456-windgustnight-0', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_wind_gust_speed_night_0-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'device_class': 'wind_speed', + 'direction': 'WSW', + 'friendly_name': 'Home Wind gust speed night 0', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_wind_gust_speed_night_0', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '18.5', + }) +# --- # name: test_sensor[sensor.home_wind_gust_speed_night_1-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -5610,7 +5710,7 @@ 'platform': 'accuweather', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'wind_gust_speed_night_1d', + 'translation_key': 'wind_gust_speed_night', 'unique_id': '0123456-windgustnight-1', 'unit_of_measurement': , }) @@ -5660,7 +5760,7 @@ 'platform': 'accuweather', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'wind_gust_speed_night_2d', + 'translation_key': 'wind_gust_speed_night', 'unique_id': '0123456-windgustnight-2', 'unit_of_measurement': , }) @@ -5710,7 +5810,7 @@ 'platform': 'accuweather', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'wind_gust_speed_night_3d', + 'translation_key': 'wind_gust_speed_night', 'unique_id': '0123456-windgustnight-3', 'unit_of_measurement': , }) @@ -5760,7 +5860,7 @@ 'platform': 'accuweather', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'wind_gust_speed_night_4d', + 'translation_key': 'wind_gust_speed_night', 'unique_id': '0123456-windgustnight-4', 'unit_of_measurement': , }) @@ -5782,106 +5882,6 @@ 'state': '18.5', }) # --- -# name: test_sensor[sensor.home_wind_gust_speed_today-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.home_wind_gust_speed_today', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Wind gust speed today', - 'platform': 'accuweather', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'wind_gust_speed_day_0d', - 'unique_id': '0123456-windgustday-0', - 'unit_of_measurement': , - }) -# --- -# name: test_sensor[sensor.home_wind_gust_speed_today-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by AccuWeather', - 'device_class': 'wind_speed', - 'direction': 'S', - 'friendly_name': 'Home Wind gust speed today', - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.home_wind_gust_speed_today', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '29.6', - }) -# --- -# name: test_sensor[sensor.home_wind_gust_speed_tonight-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.home_wind_gust_speed_tonight', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Wind gust speed tonight', - 'platform': 'accuweather', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'wind_gust_speed_night_0d', - 'unique_id': '0123456-windgustnight-0', - 'unit_of_measurement': , - }) -# --- -# name: test_sensor[sensor.home_wind_gust_speed_tonight-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by AccuWeather', - 'device_class': 'wind_speed', - 'direction': 'WSW', - 'friendly_name': 'Home Wind gust speed tonight', - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.home_wind_gust_speed_tonight', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '18.5', - }) -# --- # name: test_sensor[sensor.home_wind_speed-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -5934,6 +5934,56 @@ 'state': '14.5', }) # --- +# name: test_sensor[sensor.home_wind_speed_day_0-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.home_wind_speed_day_0', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Wind speed day 0', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'wind_speed_day', + 'unique_id': '0123456-windday-0', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_wind_speed_day_0-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'device_class': 'wind_speed', + 'direction': 'SSE', + 'friendly_name': 'Home Wind speed day 0', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_wind_speed_day_0', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '13.0', + }) +# --- # name: test_sensor[sensor.home_wind_speed_day_1-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -5962,7 +6012,7 @@ 'platform': 'accuweather', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'wind_speed_day_1d', + 'translation_key': 'wind_speed_day', 'unique_id': '0123456-windday-1', 'unit_of_measurement': , }) @@ -6012,7 +6062,7 @@ 'platform': 'accuweather', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'wind_speed_day_2d', + 'translation_key': 'wind_speed_day', 'unique_id': '0123456-windday-2', 'unit_of_measurement': , }) @@ -6062,7 +6112,7 @@ 'platform': 'accuweather', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'wind_speed_day_3d', + 'translation_key': 'wind_speed_day', 'unique_id': '0123456-windday-3', 'unit_of_measurement': , }) @@ -6112,7 +6162,7 @@ 'platform': 'accuweather', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'wind_speed_day_4d', + 'translation_key': 'wind_speed_day', 'unique_id': '0123456-windday-4', 'unit_of_measurement': , }) @@ -6134,6 +6184,56 @@ 'state': '18.5', }) # --- +# name: test_sensor[sensor.home_wind_speed_night_0-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.home_wind_speed_night_0', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Wind speed night 0', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'wind_speed_night', + 'unique_id': '0123456-windnight-0', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_wind_speed_night_0-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'device_class': 'wind_speed', + 'direction': 'WNW', + 'friendly_name': 'Home Wind speed night 0', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_wind_speed_night_0', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '7.4', + }) +# --- # name: test_sensor[sensor.home_wind_speed_night_1-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -6162,7 +6262,7 @@ 'platform': 'accuweather', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'wind_speed_night_1d', + 'translation_key': 'wind_speed_night', 'unique_id': '0123456-windnight-1', 'unit_of_measurement': , }) @@ -6212,7 +6312,7 @@ 'platform': 'accuweather', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'wind_speed_night_2d', + 'translation_key': 'wind_speed_night', 'unique_id': '0123456-windnight-2', 'unit_of_measurement': , }) @@ -6262,7 +6362,7 @@ 'platform': 'accuweather', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'wind_speed_night_3d', + 'translation_key': 'wind_speed_night', 'unique_id': '0123456-windnight-3', 'unit_of_measurement': , }) @@ -6312,7 +6412,7 @@ 'platform': 'accuweather', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'wind_speed_night_4d', + 'translation_key': 'wind_speed_night', 'unique_id': '0123456-windnight-4', 'unit_of_measurement': , }) @@ -6334,103 +6434,3 @@ 'state': '9.3', }) # --- -# name: test_sensor[sensor.home_wind_speed_today-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.home_wind_speed_today', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Wind speed today', - 'platform': 'accuweather', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'wind_speed_day_0d', - 'unique_id': '0123456-windday-0', - 'unit_of_measurement': , - }) -# --- -# name: test_sensor[sensor.home_wind_speed_today-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by AccuWeather', - 'device_class': 'wind_speed', - 'direction': 'SSE', - 'friendly_name': 'Home Wind speed today', - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.home_wind_speed_today', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '13.0', - }) -# --- -# name: test_sensor[sensor.home_wind_speed_tonight-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.home_wind_speed_tonight', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Wind speed tonight', - 'platform': 'accuweather', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'wind_speed_night_0d', - 'unique_id': '0123456-windnight-0', - 'unit_of_measurement': , - }) -# --- -# name: test_sensor[sensor.home_wind_speed_tonight-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by AccuWeather', - 'device_class': 'wind_speed', - 'direction': 'WNW', - 'friendly_name': 'Home Wind speed tonight', - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.home_wind_speed_tonight', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '7.4', - }) -# --- From cb672b85f4ac03d79dcbea4c49c9639312767351 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sat, 8 Jun 2024 15:57:22 +0200 Subject: [PATCH 0381/1445] Add icon translations to AccuWeather (#119134) Co-authored-by: Maciej Bieniek <478555+bieniu@users.noreply.github.com> --- .../components/accuweather/icons.json | 51 ++++ .../components/accuweather/sensor.py | 19 +- .../accuweather/snapshots/test_sensor.ambr | 237 +++++++----------- 3 files changed, 142 insertions(+), 165 deletions(-) create mode 100644 homeassistant/components/accuweather/icons.json diff --git a/homeassistant/components/accuweather/icons.json b/homeassistant/components/accuweather/icons.json new file mode 100644 index 00000000000..183b4d2731d --- /dev/null +++ b/homeassistant/components/accuweather/icons.json @@ -0,0 +1,51 @@ +{ + "entity": { + "sensor": { + "cloud_ceiling": { + "default": "mdi:weather-fog" + }, + "cloud_cover": { + "default": "mdi:weather-cloudy" + }, + "cloud_cover_day": { + "default": "mdi:weather-cloudy" + }, + "cloud_cover_night": { + "default": "mdi:weather-cloudy" + }, + "grass_pollen": { + "default": "mdi:grass" + }, + "hours_of_sun": { + "default": "mdi:weather-partly-cloudy" + }, + "mold_pollen": { + "default": "mdi:blur" + }, + "pressure_tendency": { + "default": "mdi:gauge" + }, + "ragweed_pollen": { + "default": "mdi:sprout" + }, + "thunderstorm_probability_day": { + "default": "mdi:weather-lightning" + }, + "thunderstorm_probability_night": { + "default": "mdi:weather-lightning" + }, + "translation_key": { + "default": "mdi:air-filter" + }, + "tree_pollen": { + "default": "mdi:tree-outline" + }, + "uv_index": { + "default": "mdi:weather-sunny" + }, + "uv_index_forecast": { + "default": "mdi:weather-sunny" + } + } + } +} diff --git a/homeassistant/components/accuweather/sensor.py b/homeassistant/components/accuweather/sensor.py index 190fc311c1a..fac3a2a4ba3 100644 --- a/homeassistant/components/accuweather/sensor.py +++ b/homeassistant/components/accuweather/sensor.py @@ -58,7 +58,6 @@ class AccuWeatherSensorDescription(SensorEntityDescription): FORECAST_SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = ( AccuWeatherSensorDescription( key="AirQuality", - icon="mdi:air-filter", value_fn=lambda data: cast(str, data[ATTR_CATEGORY]), device_class=SensorDeviceClass.ENUM, options=["good", "hazardous", "high", "low", "moderate", "unhealthy"], @@ -66,7 +65,6 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = ( ), AccuWeatherSensorDescription( key="CloudCoverDay", - icon="mdi:weather-cloudy", entity_registry_enabled_default=False, native_unit_of_measurement=PERCENTAGE, value_fn=lambda data: cast(int, data), @@ -74,7 +72,6 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = ( ), AccuWeatherSensorDescription( key="CloudCoverNight", - icon="mdi:weather-cloudy", entity_registry_enabled_default=False, native_unit_of_measurement=PERCENTAGE, value_fn=lambda data: cast(int, data), @@ -82,7 +79,6 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = ( ), AccuWeatherSensorDescription( key="Grass", - icon="mdi:grass", entity_registry_enabled_default=False, native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, value_fn=lambda data: cast(int, data[ATTR_VALUE]), @@ -91,7 +87,6 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = ( ), AccuWeatherSensorDescription( key="HoursOfSun", - icon="mdi:weather-partly-cloudy", native_unit_of_measurement=UnitOfTime.HOURS, value_fn=lambda data: cast(float, data), translation_key="hours_of_sun", @@ -108,7 +103,6 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = ( ), AccuWeatherSensorDescription( key="Mold", - icon="mdi:blur", entity_registry_enabled_default=False, native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, value_fn=lambda data: cast(int, data[ATTR_VALUE]), @@ -117,7 +111,6 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = ( ), AccuWeatherSensorDescription( key="Ragweed", - icon="mdi:sprout", native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, entity_registry_enabled_default=False, value_fn=lambda data: cast(int, data[ATTR_VALUE]), @@ -156,7 +149,7 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = ( ), AccuWeatherSensorDescription( key="SolarIrradianceDay", - icon="mdi:weather-sunny", + device_class=SensorDeviceClass.IRRADIANCE, entity_registry_enabled_default=False, native_unit_of_measurement=UnitOfIrradiance.WATTS_PER_SQUARE_METER, value_fn=lambda data: cast(float, data[ATTR_VALUE]), @@ -164,7 +157,7 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = ( ), AccuWeatherSensorDescription( key="SolarIrradianceNight", - icon="mdi:weather-sunny", + device_class=SensorDeviceClass.IRRADIANCE, entity_registry_enabled_default=False, native_unit_of_measurement=UnitOfIrradiance.WATTS_PER_SQUARE_METER, value_fn=lambda data: cast(float, data[ATTR_VALUE]), @@ -172,21 +165,18 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = ( ), AccuWeatherSensorDescription( key="ThunderstormProbabilityDay", - icon="mdi:weather-lightning", native_unit_of_measurement=PERCENTAGE, value_fn=lambda data: cast(int, data), translation_key="thunderstorm_probability_day", ), AccuWeatherSensorDescription( key="ThunderstormProbabilityNight", - icon="mdi:weather-lightning", native_unit_of_measurement=PERCENTAGE, value_fn=lambda data: cast(int, data), translation_key="thunderstorm_probability_night", ), AccuWeatherSensorDescription( key="Tree", - icon="mdi:tree-outline", native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, entity_registry_enabled_default=False, value_fn=lambda data: cast(int, data[ATTR_VALUE]), @@ -195,7 +185,6 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = ( ), AccuWeatherSensorDescription( key="UVIndex", - icon="mdi:weather-sunny", native_unit_of_measurement=UV_INDEX, value_fn=lambda data: cast(int, data[ATTR_VALUE]), attr_fn=lambda data: {ATTR_LEVEL: data[ATTR_CATEGORY]}, @@ -250,7 +239,6 @@ SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = ( AccuWeatherSensorDescription( key="Ceiling", device_class=SensorDeviceClass.DISTANCE, - icon="mdi:weather-fog", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfLength.METERS, value_fn=lambda data: cast(float, data[API_METRIC][ATTR_VALUE]), @@ -259,7 +247,6 @@ SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = ( ), AccuWeatherSensorDescription( key="CloudCover", - icon="mdi:weather-cloudy", entity_registry_enabled_default=False, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=PERCENTAGE, @@ -304,14 +291,12 @@ SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = ( AccuWeatherSensorDescription( key="PressureTendency", device_class=SensorDeviceClass.ENUM, - icon="mdi:gauge", options=["falling", "rising", "steady"], value_fn=lambda data: cast(str, data["LocalizedText"]).lower(), translation_key="pressure_tendency", ), AccuWeatherSensorDescription( key="UVIndex", - icon="mdi:weather-sunny", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UV_INDEX, value_fn=lambda data: cast(int, data), diff --git a/tests/components/accuweather/snapshots/test_sensor.ambr b/tests/components/accuweather/snapshots/test_sensor.ambr index 61e37047bda..5e28be5a72b 100644 --- a/tests/components/accuweather/snapshots/test_sensor.ambr +++ b/tests/components/accuweather/snapshots/test_sensor.ambr @@ -31,7 +31,7 @@ 'options': dict({ }), 'original_device_class': , - 'original_icon': 'mdi:air-filter', + 'original_icon': None, 'original_name': 'Air quality day 0', 'platform': 'accuweather', 'previous_unique_id': None, @@ -47,7 +47,6 @@ 'attribution': 'Data provided by AccuWeather', 'device_class': 'enum', 'friendly_name': 'Home Air quality day 0', - 'icon': 'mdi:air-filter', 'options': list([ 'good', 'hazardous', @@ -97,7 +96,7 @@ 'options': dict({ }), 'original_device_class': , - 'original_icon': 'mdi:air-filter', + 'original_icon': None, 'original_name': 'Air quality day 1', 'platform': 'accuweather', 'previous_unique_id': None, @@ -113,7 +112,6 @@ 'attribution': 'Data provided by AccuWeather', 'device_class': 'enum', 'friendly_name': 'Home Air quality day 1', - 'icon': 'mdi:air-filter', 'options': list([ 'good', 'hazardous', @@ -163,7 +161,7 @@ 'options': dict({ }), 'original_device_class': , - 'original_icon': 'mdi:air-filter', + 'original_icon': None, 'original_name': 'Air quality day 2', 'platform': 'accuweather', 'previous_unique_id': None, @@ -179,7 +177,6 @@ 'attribution': 'Data provided by AccuWeather', 'device_class': 'enum', 'friendly_name': 'Home Air quality day 2', - 'icon': 'mdi:air-filter', 'options': list([ 'good', 'hazardous', @@ -229,7 +226,7 @@ 'options': dict({ }), 'original_device_class': , - 'original_icon': 'mdi:air-filter', + 'original_icon': None, 'original_name': 'Air quality day 3', 'platform': 'accuweather', 'previous_unique_id': None, @@ -245,7 +242,6 @@ 'attribution': 'Data provided by AccuWeather', 'device_class': 'enum', 'friendly_name': 'Home Air quality day 3', - 'icon': 'mdi:air-filter', 'options': list([ 'good', 'hazardous', @@ -295,7 +291,7 @@ 'options': dict({ }), 'original_device_class': , - 'original_icon': 'mdi:air-filter', + 'original_icon': None, 'original_name': 'Air quality day 4', 'platform': 'accuweather', 'previous_unique_id': None, @@ -311,7 +307,6 @@ 'attribution': 'Data provided by AccuWeather', 'device_class': 'enum', 'friendly_name': 'Home Air quality day 4', - 'icon': 'mdi:air-filter', 'options': list([ 'good', 'hazardous', @@ -409,7 +404,7 @@ }), }), 'original_device_class': , - 'original_icon': 'mdi:weather-fog', + 'original_icon': None, 'original_name': 'Cloud ceiling', 'platform': 'accuweather', 'previous_unique_id': None, @@ -425,7 +420,6 @@ 'attribution': 'Data provided by AccuWeather', 'device_class': 'distance', 'friendly_name': 'Home Cloud ceiling', - 'icon': 'mdi:weather-fog', 'state_class': , 'unit_of_measurement': , }), @@ -462,7 +456,7 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:weather-cloudy', + 'original_icon': None, 'original_name': 'Cloud cover', 'platform': 'accuweather', 'previous_unique_id': None, @@ -477,7 +471,6 @@ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by AccuWeather', 'friendly_name': 'Home Cloud cover', - 'icon': 'mdi:weather-cloudy', 'state_class': , 'unit_of_measurement': '%', }), @@ -512,7 +505,7 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:weather-cloudy', + 'original_icon': None, 'original_name': 'Cloud cover day 0', 'platform': 'accuweather', 'previous_unique_id': None, @@ -527,7 +520,6 @@ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by AccuWeather', 'friendly_name': 'Home Cloud cover day 0', - 'icon': 'mdi:weather-cloudy', 'unit_of_measurement': '%', }), 'context': , @@ -561,7 +553,7 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:weather-cloudy', + 'original_icon': None, 'original_name': 'Cloud cover day 1', 'platform': 'accuweather', 'previous_unique_id': None, @@ -576,7 +568,6 @@ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by AccuWeather', 'friendly_name': 'Home Cloud cover day 1', - 'icon': 'mdi:weather-cloudy', 'unit_of_measurement': '%', }), 'context': , @@ -610,7 +601,7 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:weather-cloudy', + 'original_icon': None, 'original_name': 'Cloud cover day 2', 'platform': 'accuweather', 'previous_unique_id': None, @@ -625,7 +616,6 @@ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by AccuWeather', 'friendly_name': 'Home Cloud cover day 2', - 'icon': 'mdi:weather-cloudy', 'unit_of_measurement': '%', }), 'context': , @@ -659,7 +649,7 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:weather-cloudy', + 'original_icon': None, 'original_name': 'Cloud cover day 3', 'platform': 'accuweather', 'previous_unique_id': None, @@ -674,7 +664,6 @@ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by AccuWeather', 'friendly_name': 'Home Cloud cover day 3', - 'icon': 'mdi:weather-cloudy', 'unit_of_measurement': '%', }), 'context': , @@ -708,7 +697,7 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:weather-cloudy', + 'original_icon': None, 'original_name': 'Cloud cover day 4', 'platform': 'accuweather', 'previous_unique_id': None, @@ -723,7 +712,6 @@ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by AccuWeather', 'friendly_name': 'Home Cloud cover day 4', - 'icon': 'mdi:weather-cloudy', 'unit_of_measurement': '%', }), 'context': , @@ -757,7 +745,7 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:weather-cloudy', + 'original_icon': None, 'original_name': 'Cloud cover night 0', 'platform': 'accuweather', 'previous_unique_id': None, @@ -772,7 +760,6 @@ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by AccuWeather', 'friendly_name': 'Home Cloud cover night 0', - 'icon': 'mdi:weather-cloudy', 'unit_of_measurement': '%', }), 'context': , @@ -806,7 +793,7 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:weather-cloudy', + 'original_icon': None, 'original_name': 'Cloud cover night 1', 'platform': 'accuweather', 'previous_unique_id': None, @@ -821,7 +808,6 @@ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by AccuWeather', 'friendly_name': 'Home Cloud cover night 1', - 'icon': 'mdi:weather-cloudy', 'unit_of_measurement': '%', }), 'context': , @@ -855,7 +841,7 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:weather-cloudy', + 'original_icon': None, 'original_name': 'Cloud cover night 2', 'platform': 'accuweather', 'previous_unique_id': None, @@ -870,7 +856,6 @@ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by AccuWeather', 'friendly_name': 'Home Cloud cover night 2', - 'icon': 'mdi:weather-cloudy', 'unit_of_measurement': '%', }), 'context': , @@ -904,7 +889,7 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:weather-cloudy', + 'original_icon': None, 'original_name': 'Cloud cover night 3', 'platform': 'accuweather', 'previous_unique_id': None, @@ -919,7 +904,6 @@ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by AccuWeather', 'friendly_name': 'Home Cloud cover night 3', - 'icon': 'mdi:weather-cloudy', 'unit_of_measurement': '%', }), 'context': , @@ -953,7 +937,7 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:weather-cloudy', + 'original_icon': None, 'original_name': 'Cloud cover night 4', 'platform': 'accuweather', 'previous_unique_id': None, @@ -968,7 +952,6 @@ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by AccuWeather', 'friendly_name': 'Home Cloud cover night 4', - 'icon': 'mdi:weather-cloudy', 'unit_of_measurement': '%', }), 'context': , @@ -1524,7 +1507,7 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:grass', + 'original_icon': None, 'original_name': 'Grass pollen day 0', 'platform': 'accuweather', 'previous_unique_id': None, @@ -1539,7 +1522,6 @@ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by AccuWeather', 'friendly_name': 'Home Grass pollen day 0', - 'icon': 'mdi:grass', 'level': 'low', 'unit_of_measurement': 'p/m³', }), @@ -1574,7 +1556,7 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:grass', + 'original_icon': None, 'original_name': 'Grass pollen day 1', 'platform': 'accuweather', 'previous_unique_id': None, @@ -1589,7 +1571,6 @@ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by AccuWeather', 'friendly_name': 'Home Grass pollen day 1', - 'icon': 'mdi:grass', 'level': 'low', 'unit_of_measurement': 'p/m³', }), @@ -1624,7 +1605,7 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:grass', + 'original_icon': None, 'original_name': 'Grass pollen day 2', 'platform': 'accuweather', 'previous_unique_id': None, @@ -1639,7 +1620,6 @@ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by AccuWeather', 'friendly_name': 'Home Grass pollen day 2', - 'icon': 'mdi:grass', 'level': 'low', 'unit_of_measurement': 'p/m³', }), @@ -1674,7 +1654,7 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:grass', + 'original_icon': None, 'original_name': 'Grass pollen day 3', 'platform': 'accuweather', 'previous_unique_id': None, @@ -1689,7 +1669,6 @@ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by AccuWeather', 'friendly_name': 'Home Grass pollen day 3', - 'icon': 'mdi:grass', 'level': 'low', 'unit_of_measurement': 'p/m³', }), @@ -1724,7 +1703,7 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:grass', + 'original_icon': None, 'original_name': 'Grass pollen day 4', 'platform': 'accuweather', 'previous_unique_id': None, @@ -1739,7 +1718,6 @@ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by AccuWeather', 'friendly_name': 'Home Grass pollen day 4', - 'icon': 'mdi:grass', 'level': 'low', 'unit_of_measurement': 'p/m³', }), @@ -1774,7 +1752,7 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:weather-partly-cloudy', + 'original_icon': None, 'original_name': 'Hours of sun day 0', 'platform': 'accuweather', 'previous_unique_id': None, @@ -1789,7 +1767,6 @@ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by AccuWeather', 'friendly_name': 'Home Hours of sun day 0', - 'icon': 'mdi:weather-partly-cloudy', 'unit_of_measurement': , }), 'context': , @@ -1823,7 +1800,7 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:weather-partly-cloudy', + 'original_icon': None, 'original_name': 'Hours of sun day 1', 'platform': 'accuweather', 'previous_unique_id': None, @@ -1838,7 +1815,6 @@ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by AccuWeather', 'friendly_name': 'Home Hours of sun day 1', - 'icon': 'mdi:weather-partly-cloudy', 'unit_of_measurement': , }), 'context': , @@ -1872,7 +1848,7 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:weather-partly-cloudy', + 'original_icon': None, 'original_name': 'Hours of sun day 2', 'platform': 'accuweather', 'previous_unique_id': None, @@ -1887,7 +1863,6 @@ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by AccuWeather', 'friendly_name': 'Home Hours of sun day 2', - 'icon': 'mdi:weather-partly-cloudy', 'unit_of_measurement': , }), 'context': , @@ -1921,7 +1896,7 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:weather-partly-cloudy', + 'original_icon': None, 'original_name': 'Hours of sun day 3', 'platform': 'accuweather', 'previous_unique_id': None, @@ -1936,7 +1911,6 @@ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by AccuWeather', 'friendly_name': 'Home Hours of sun day 3', - 'icon': 'mdi:weather-partly-cloudy', 'unit_of_measurement': , }), 'context': , @@ -1970,7 +1944,7 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:weather-partly-cloudy', + 'original_icon': None, 'original_name': 'Hours of sun day 4', 'platform': 'accuweather', 'previous_unique_id': None, @@ -1985,7 +1959,6 @@ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by AccuWeather', 'friendly_name': 'Home Hours of sun day 4', - 'icon': 'mdi:weather-partly-cloudy', 'unit_of_measurement': , }), 'context': , @@ -2019,7 +1992,7 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:blur', + 'original_icon': None, 'original_name': 'Mold pollen day 0', 'platform': 'accuweather', 'previous_unique_id': None, @@ -2034,7 +2007,6 @@ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by AccuWeather', 'friendly_name': 'Home Mold pollen day 0', - 'icon': 'mdi:blur', 'level': 'low', 'unit_of_measurement': 'p/m³', }), @@ -2069,7 +2041,7 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:blur', + 'original_icon': None, 'original_name': 'Mold pollen day 1', 'platform': 'accuweather', 'previous_unique_id': None, @@ -2084,7 +2056,6 @@ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by AccuWeather', 'friendly_name': 'Home Mold pollen day 1', - 'icon': 'mdi:blur', 'level': 'low', 'unit_of_measurement': 'p/m³', }), @@ -2119,7 +2090,7 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:blur', + 'original_icon': None, 'original_name': 'Mold pollen day 2', 'platform': 'accuweather', 'previous_unique_id': None, @@ -2134,7 +2105,6 @@ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by AccuWeather', 'friendly_name': 'Home Mold pollen day 2', - 'icon': 'mdi:blur', 'level': 'low', 'unit_of_measurement': 'p/m³', }), @@ -2169,7 +2139,7 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:blur', + 'original_icon': None, 'original_name': 'Mold pollen day 3', 'platform': 'accuweather', 'previous_unique_id': None, @@ -2184,7 +2154,6 @@ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by AccuWeather', 'friendly_name': 'Home Mold pollen day 3', - 'icon': 'mdi:blur', 'level': 'low', 'unit_of_measurement': 'p/m³', }), @@ -2219,7 +2188,7 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:blur', + 'original_icon': None, 'original_name': 'Mold pollen day 4', 'platform': 'accuweather', 'previous_unique_id': None, @@ -2234,7 +2203,6 @@ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by AccuWeather', 'friendly_name': 'Home Mold pollen day 4', - 'icon': 'mdi:blur', 'level': 'low', 'unit_of_measurement': 'p/m³', }), @@ -2328,7 +2296,7 @@ 'options': dict({ }), 'original_device_class': , - 'original_icon': 'mdi:gauge', + 'original_icon': None, 'original_name': 'Pressure tendency', 'platform': 'accuweather', 'previous_unique_id': None, @@ -2344,7 +2312,6 @@ 'attribution': 'Data provided by AccuWeather', 'device_class': 'enum', 'friendly_name': 'Home Pressure tendency', - 'icon': 'mdi:gauge', 'options': list([ 'falling', 'rising', @@ -2382,7 +2349,7 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:sprout', + 'original_icon': None, 'original_name': 'Ragweed pollen day 0', 'platform': 'accuweather', 'previous_unique_id': None, @@ -2397,7 +2364,6 @@ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by AccuWeather', 'friendly_name': 'Home Ragweed pollen day 0', - 'icon': 'mdi:sprout', 'level': 'low', 'unit_of_measurement': 'p/m³', }), @@ -2432,7 +2398,7 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:sprout', + 'original_icon': None, 'original_name': 'Ragweed pollen day 1', 'platform': 'accuweather', 'previous_unique_id': None, @@ -2447,7 +2413,6 @@ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by AccuWeather', 'friendly_name': 'Home Ragweed pollen day 1', - 'icon': 'mdi:sprout', 'level': 'low', 'unit_of_measurement': 'p/m³', }), @@ -2482,7 +2447,7 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:sprout', + 'original_icon': None, 'original_name': 'Ragweed pollen day 2', 'platform': 'accuweather', 'previous_unique_id': None, @@ -2497,7 +2462,6 @@ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by AccuWeather', 'friendly_name': 'Home Ragweed pollen day 2', - 'icon': 'mdi:sprout', 'level': 'low', 'unit_of_measurement': 'p/m³', }), @@ -2532,7 +2496,7 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:sprout', + 'original_icon': None, 'original_name': 'Ragweed pollen day 3', 'platform': 'accuweather', 'previous_unique_id': None, @@ -2547,7 +2511,6 @@ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by AccuWeather', 'friendly_name': 'Home Ragweed pollen day 3', - 'icon': 'mdi:sprout', 'level': 'low', 'unit_of_measurement': 'p/m³', }), @@ -2582,7 +2545,7 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:sprout', + 'original_icon': None, 'original_name': 'Ragweed pollen day 4', 'platform': 'accuweather', 'previous_unique_id': None, @@ -2597,7 +2560,6 @@ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by AccuWeather', 'friendly_name': 'Home Ragweed pollen day 4', - 'icon': 'mdi:sprout', 'level': 'low', 'unit_of_measurement': 'p/m³', }), @@ -3715,8 +3677,8 @@ 'name': None, 'options': dict({ }), - 'original_device_class': None, - 'original_icon': 'mdi:weather-sunny', + 'original_device_class': , + 'original_icon': None, 'original_name': 'Solar irradiance day 0', 'platform': 'accuweather', 'previous_unique_id': None, @@ -3730,8 +3692,8 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by AccuWeather', + 'device_class': 'irradiance', 'friendly_name': 'Home Solar irradiance day 0', - 'icon': 'mdi:weather-sunny', 'unit_of_measurement': , }), 'context': , @@ -3764,8 +3726,8 @@ 'name': None, 'options': dict({ }), - 'original_device_class': None, - 'original_icon': 'mdi:weather-sunny', + 'original_device_class': , + 'original_icon': None, 'original_name': 'Solar irradiance day 1', 'platform': 'accuweather', 'previous_unique_id': None, @@ -3779,8 +3741,8 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by AccuWeather', + 'device_class': 'irradiance', 'friendly_name': 'Home Solar irradiance day 1', - 'icon': 'mdi:weather-sunny', 'unit_of_measurement': , }), 'context': , @@ -3813,8 +3775,8 @@ 'name': None, 'options': dict({ }), - 'original_device_class': None, - 'original_icon': 'mdi:weather-sunny', + 'original_device_class': , + 'original_icon': None, 'original_name': 'Solar irradiance day 2', 'platform': 'accuweather', 'previous_unique_id': None, @@ -3828,8 +3790,8 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by AccuWeather', + 'device_class': 'irradiance', 'friendly_name': 'Home Solar irradiance day 2', - 'icon': 'mdi:weather-sunny', 'unit_of_measurement': , }), 'context': , @@ -3862,8 +3824,8 @@ 'name': None, 'options': dict({ }), - 'original_device_class': None, - 'original_icon': 'mdi:weather-sunny', + 'original_device_class': , + 'original_icon': None, 'original_name': 'Solar irradiance day 3', 'platform': 'accuweather', 'previous_unique_id': None, @@ -3877,8 +3839,8 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by AccuWeather', + 'device_class': 'irradiance', 'friendly_name': 'Home Solar irradiance day 3', - 'icon': 'mdi:weather-sunny', 'unit_of_measurement': , }), 'context': , @@ -3911,8 +3873,8 @@ 'name': None, 'options': dict({ }), - 'original_device_class': None, - 'original_icon': 'mdi:weather-sunny', + 'original_device_class': , + 'original_icon': None, 'original_name': 'Solar irradiance day 4', 'platform': 'accuweather', 'previous_unique_id': None, @@ -3926,8 +3888,8 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by AccuWeather', + 'device_class': 'irradiance', 'friendly_name': 'Home Solar irradiance day 4', - 'icon': 'mdi:weather-sunny', 'unit_of_measurement': , }), 'context': , @@ -3960,8 +3922,8 @@ 'name': None, 'options': dict({ }), - 'original_device_class': None, - 'original_icon': 'mdi:weather-sunny', + 'original_device_class': , + 'original_icon': None, 'original_name': 'Solar irradiance night 0', 'platform': 'accuweather', 'previous_unique_id': None, @@ -3975,8 +3937,8 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by AccuWeather', + 'device_class': 'irradiance', 'friendly_name': 'Home Solar irradiance night 0', - 'icon': 'mdi:weather-sunny', 'unit_of_measurement': , }), 'context': , @@ -4009,8 +3971,8 @@ 'name': None, 'options': dict({ }), - 'original_device_class': None, - 'original_icon': 'mdi:weather-sunny', + 'original_device_class': , + 'original_icon': None, 'original_name': 'Solar irradiance night 1', 'platform': 'accuweather', 'previous_unique_id': None, @@ -4024,8 +3986,8 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by AccuWeather', + 'device_class': 'irradiance', 'friendly_name': 'Home Solar irradiance night 1', - 'icon': 'mdi:weather-sunny', 'unit_of_measurement': , }), 'context': , @@ -4058,8 +4020,8 @@ 'name': None, 'options': dict({ }), - 'original_device_class': None, - 'original_icon': 'mdi:weather-sunny', + 'original_device_class': , + 'original_icon': None, 'original_name': 'Solar irradiance night 2', 'platform': 'accuweather', 'previous_unique_id': None, @@ -4073,8 +4035,8 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by AccuWeather', + 'device_class': 'irradiance', 'friendly_name': 'Home Solar irradiance night 2', - 'icon': 'mdi:weather-sunny', 'unit_of_measurement': , }), 'context': , @@ -4107,8 +4069,8 @@ 'name': None, 'options': dict({ }), - 'original_device_class': None, - 'original_icon': 'mdi:weather-sunny', + 'original_device_class': , + 'original_icon': None, 'original_name': 'Solar irradiance night 3', 'platform': 'accuweather', 'previous_unique_id': None, @@ -4122,8 +4084,8 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by AccuWeather', + 'device_class': 'irradiance', 'friendly_name': 'Home Solar irradiance night 3', - 'icon': 'mdi:weather-sunny', 'unit_of_measurement': , }), 'context': , @@ -4156,8 +4118,8 @@ 'name': None, 'options': dict({ }), - 'original_device_class': None, - 'original_icon': 'mdi:weather-sunny', + 'original_device_class': , + 'original_icon': None, 'original_name': 'Solar irradiance night 4', 'platform': 'accuweather', 'previous_unique_id': None, @@ -4171,8 +4133,8 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by AccuWeather', + 'device_class': 'irradiance', 'friendly_name': 'Home Solar irradiance night 4', - 'icon': 'mdi:weather-sunny', 'unit_of_measurement': , }), 'context': , @@ -4206,7 +4168,7 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:weather-lightning', + 'original_icon': None, 'original_name': 'Thunderstorm probability day 0', 'platform': 'accuweather', 'previous_unique_id': None, @@ -4221,7 +4183,6 @@ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by AccuWeather', 'friendly_name': 'Home Thunderstorm probability day 0', - 'icon': 'mdi:weather-lightning', 'unit_of_measurement': '%', }), 'context': , @@ -4255,7 +4216,7 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:weather-lightning', + 'original_icon': None, 'original_name': 'Thunderstorm probability day 1', 'platform': 'accuweather', 'previous_unique_id': None, @@ -4270,7 +4231,6 @@ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by AccuWeather', 'friendly_name': 'Home Thunderstorm probability day 1', - 'icon': 'mdi:weather-lightning', 'unit_of_measurement': '%', }), 'context': , @@ -4304,7 +4264,7 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:weather-lightning', + 'original_icon': None, 'original_name': 'Thunderstorm probability day 2', 'platform': 'accuweather', 'previous_unique_id': None, @@ -4319,7 +4279,6 @@ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by AccuWeather', 'friendly_name': 'Home Thunderstorm probability day 2', - 'icon': 'mdi:weather-lightning', 'unit_of_measurement': '%', }), 'context': , @@ -4353,7 +4312,7 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:weather-lightning', + 'original_icon': None, 'original_name': 'Thunderstorm probability day 3', 'platform': 'accuweather', 'previous_unique_id': None, @@ -4368,7 +4327,6 @@ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by AccuWeather', 'friendly_name': 'Home Thunderstorm probability day 3', - 'icon': 'mdi:weather-lightning', 'unit_of_measurement': '%', }), 'context': , @@ -4402,7 +4360,7 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:weather-lightning', + 'original_icon': None, 'original_name': 'Thunderstorm probability day 4', 'platform': 'accuweather', 'previous_unique_id': None, @@ -4417,7 +4375,6 @@ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by AccuWeather', 'friendly_name': 'Home Thunderstorm probability day 4', - 'icon': 'mdi:weather-lightning', 'unit_of_measurement': '%', }), 'context': , @@ -4451,7 +4408,7 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:weather-lightning', + 'original_icon': None, 'original_name': 'Thunderstorm probability night 0', 'platform': 'accuweather', 'previous_unique_id': None, @@ -4466,7 +4423,6 @@ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by AccuWeather', 'friendly_name': 'Home Thunderstorm probability night 0', - 'icon': 'mdi:weather-lightning', 'unit_of_measurement': '%', }), 'context': , @@ -4500,7 +4456,7 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:weather-lightning', + 'original_icon': None, 'original_name': 'Thunderstorm probability night 1', 'platform': 'accuweather', 'previous_unique_id': None, @@ -4515,7 +4471,6 @@ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by AccuWeather', 'friendly_name': 'Home Thunderstorm probability night 1', - 'icon': 'mdi:weather-lightning', 'unit_of_measurement': '%', }), 'context': , @@ -4549,7 +4504,7 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:weather-lightning', + 'original_icon': None, 'original_name': 'Thunderstorm probability night 2', 'platform': 'accuweather', 'previous_unique_id': None, @@ -4564,7 +4519,6 @@ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by AccuWeather', 'friendly_name': 'Home Thunderstorm probability night 2', - 'icon': 'mdi:weather-lightning', 'unit_of_measurement': '%', }), 'context': , @@ -4598,7 +4552,7 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:weather-lightning', + 'original_icon': None, 'original_name': 'Thunderstorm probability night 3', 'platform': 'accuweather', 'previous_unique_id': None, @@ -4613,7 +4567,6 @@ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by AccuWeather', 'friendly_name': 'Home Thunderstorm probability night 3', - 'icon': 'mdi:weather-lightning', 'unit_of_measurement': '%', }), 'context': , @@ -4647,7 +4600,7 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:weather-lightning', + 'original_icon': None, 'original_name': 'Thunderstorm probability night 4', 'platform': 'accuweather', 'previous_unique_id': None, @@ -4662,7 +4615,6 @@ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by AccuWeather', 'friendly_name': 'Home Thunderstorm probability night 4', - 'icon': 'mdi:weather-lightning', 'unit_of_measurement': '%', }), 'context': , @@ -4696,7 +4648,7 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:tree-outline', + 'original_icon': None, 'original_name': 'Tree pollen day 0', 'platform': 'accuweather', 'previous_unique_id': None, @@ -4711,7 +4663,6 @@ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by AccuWeather', 'friendly_name': 'Home Tree pollen day 0', - 'icon': 'mdi:tree-outline', 'level': 'low', 'unit_of_measurement': 'p/m³', }), @@ -4746,7 +4697,7 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:tree-outline', + 'original_icon': None, 'original_name': 'Tree pollen day 1', 'platform': 'accuweather', 'previous_unique_id': None, @@ -4761,7 +4712,6 @@ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by AccuWeather', 'friendly_name': 'Home Tree pollen day 1', - 'icon': 'mdi:tree-outline', 'level': 'low', 'unit_of_measurement': 'p/m³', }), @@ -4796,7 +4746,7 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:tree-outline', + 'original_icon': None, 'original_name': 'Tree pollen day 2', 'platform': 'accuweather', 'previous_unique_id': None, @@ -4811,7 +4761,6 @@ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by AccuWeather', 'friendly_name': 'Home Tree pollen day 2', - 'icon': 'mdi:tree-outline', 'level': 'low', 'unit_of_measurement': 'p/m³', }), @@ -4846,7 +4795,7 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:tree-outline', + 'original_icon': None, 'original_name': 'Tree pollen day 3', 'platform': 'accuweather', 'previous_unique_id': None, @@ -4861,7 +4810,6 @@ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by AccuWeather', 'friendly_name': 'Home Tree pollen day 3', - 'icon': 'mdi:tree-outline', 'level': 'low', 'unit_of_measurement': 'p/m³', }), @@ -4896,7 +4844,7 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:tree-outline', + 'original_icon': None, 'original_name': 'Tree pollen day 4', 'platform': 'accuweather', 'previous_unique_id': None, @@ -4911,7 +4859,6 @@ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by AccuWeather', 'friendly_name': 'Home Tree pollen day 4', - 'icon': 'mdi:tree-outline', 'level': 'low', 'unit_of_measurement': 'p/m³', }), @@ -4948,7 +4895,7 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:weather-sunny', + 'original_icon': None, 'original_name': 'UV index', 'platform': 'accuweather', 'previous_unique_id': None, @@ -4963,7 +4910,6 @@ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by AccuWeather', 'friendly_name': 'Home UV index', - 'icon': 'mdi:weather-sunny', 'level': 'High', 'state_class': , 'unit_of_measurement': 'UV index', @@ -4999,7 +4945,7 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:weather-sunny', + 'original_icon': None, 'original_name': 'UV index day 0', 'platform': 'accuweather', 'previous_unique_id': None, @@ -5014,7 +4960,6 @@ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by AccuWeather', 'friendly_name': 'Home UV index day 0', - 'icon': 'mdi:weather-sunny', 'level': 'moderate', 'unit_of_measurement': 'UV index', }), @@ -5049,7 +4994,7 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:weather-sunny', + 'original_icon': None, 'original_name': 'UV index day 1', 'platform': 'accuweather', 'previous_unique_id': None, @@ -5064,7 +5009,6 @@ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by AccuWeather', 'friendly_name': 'Home UV index day 1', - 'icon': 'mdi:weather-sunny', 'level': 'high', 'unit_of_measurement': 'UV index', }), @@ -5099,7 +5043,7 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:weather-sunny', + 'original_icon': None, 'original_name': 'UV index day 2', 'platform': 'accuweather', 'previous_unique_id': None, @@ -5114,7 +5058,6 @@ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by AccuWeather', 'friendly_name': 'Home UV index day 2', - 'icon': 'mdi:weather-sunny', 'level': 'high', 'unit_of_measurement': 'UV index', }), @@ -5149,7 +5092,7 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:weather-sunny', + 'original_icon': None, 'original_name': 'UV index day 3', 'platform': 'accuweather', 'previous_unique_id': None, @@ -5164,7 +5107,6 @@ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by AccuWeather', 'friendly_name': 'Home UV index day 3', - 'icon': 'mdi:weather-sunny', 'level': 'high', 'unit_of_measurement': 'UV index', }), @@ -5199,7 +5141,7 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:weather-sunny', + 'original_icon': None, 'original_name': 'UV index day 4', 'platform': 'accuweather', 'previous_unique_id': None, @@ -5214,7 +5156,6 @@ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by AccuWeather', 'friendly_name': 'Home UV index day 4', - 'icon': 'mdi:weather-sunny', 'level': 'high', 'unit_of_measurement': 'UV index', }), From b04a65f4d1d66f8f170c6085d9748cf45b086b40 Mon Sep 17 00:00:00 2001 From: Richard Kroegel <42204099+rikroe@users.noreply.github.com> Date: Sat, 8 Jun 2024 16:25:45 +0200 Subject: [PATCH 0382/1445] Change BMW select and sensor enums to lowercase (#118751) Co-authored-by: Richard --- .../components/bmw_connected_drive/select.py | 4 +-- .../components/bmw_connected_drive/sensor.py | 4 --- .../bmw_connected_drive/strings.json | 22 ++++++++++++-- .../snapshots/test_select.ambr | 30 +++++++++---------- .../snapshots/test_sensor.ambr | 6 ++-- .../bmw_connected_drive/test_select.py | 10 +++---- 6 files changed, 45 insertions(+), 31 deletions(-) diff --git a/homeassistant/components/bmw_connected_drive/select.py b/homeassistant/components/bmw_connected_drive/select.py index 409002b48e9..db54627b5b6 100644 --- a/homeassistant/components/bmw_connected_drive/select.py +++ b/homeassistant/components/bmw_connected_drive/select.py @@ -52,8 +52,8 @@ SELECT_TYPES: dict[str, BMWSelectEntityDescription] = { key="charging_mode", translation_key="charging_mode", is_available=lambda v: v.is_charging_plan_supported, - options=[c.value for c in ChargingMode if c != ChargingMode.UNKNOWN], - current_option=lambda v: str(v.charging_profile.charging_mode.value), # type: ignore[union-attr] + options=[c.value.lower() for c in ChargingMode if c != ChargingMode.UNKNOWN], + current_option=lambda v: str(v.charging_profile.charging_mode.value).lower(), # type: ignore[union-attr] remote_service=lambda v, o: v.remote_services.trigger_charging_profile_update( charging_mode=ChargingMode(o) ), diff --git a/homeassistant/components/bmw_connected_drive/sensor.py b/homeassistant/components/bmw_connected_drive/sensor.py index e7f56075e63..34169817f47 100644 --- a/homeassistant/components/bmw_connected_drive/sensor.py +++ b/homeassistant/components/bmw_connected_drive/sensor.py @@ -221,9 +221,5 @@ class BMWSensor(BMWBaseEntity, SensorEntity): if state == STATE_UNKNOWN: state = None - # special handling for charging_status to avoid a breaking change - if self.entity_description.key == "charging_status" and state: - state = state.upper() - self._attr_native_value = state super()._handle_coordinator_update() diff --git a/homeassistant/components/bmw_connected_drive/strings.json b/homeassistant/components/bmw_connected_drive/strings.json index 539c281a1a5..587b13f084d 100644 --- a/homeassistant/components/bmw_connected_drive/strings.json +++ b/homeassistant/components/bmw_connected_drive/strings.json @@ -83,7 +83,11 @@ "name": "AC Charging Limit" }, "charging_mode": { - "name": "Charging Mode" + "name": "Charging Mode", + "state": { + "immediate_charging": "Immediate charging", + "delayed_charging": "Delayed charging" + } } }, "sensor": { @@ -97,7 +101,21 @@ "name": "Charging end time" }, "charging_status": { - "name": "Charging status" + "name": "Charging status", + "state": { + "default": "Default", + "charging": "Charging", + "error": "Error", + "complete": "Complete", + "fully_charged": "Fully charged", + "finished_fully_charged": "Finished, fully charged", + "finished_not_full": "Finished, not full", + "invalid": "Invalid", + "not_charging": "Not charging", + "plugged_in": "Plugged in", + "waiting_for_charging": "Waiting for charging", + "target_reached": "Target reached" + } }, "charging_target": { "name": "Charging target" diff --git a/tests/components/bmw_connected_drive/snapshots/test_select.ambr b/tests/components/bmw_connected_drive/snapshots/test_select.ambr index 94155598ef7..34a8817c8db 100644 --- a/tests/components/bmw_connected_drive/snapshots/test_select.ambr +++ b/tests/components/bmw_connected_drive/snapshots/test_select.ambr @@ -6,8 +6,8 @@ 'area_id': None, 'capabilities': dict({ 'options': list([ - 'IMMEDIATE_CHARGING', - 'DELAYED_CHARGING', + 'immediate_charging', + 'delayed_charging', ]), }), 'config_entry_id': , @@ -43,8 +43,8 @@ 'attribution': 'Data provided by MyBMW', 'friendly_name': 'i3 (+ REX) Charging Mode', 'options': list([ - 'IMMEDIATE_CHARGING', - 'DELAYED_CHARGING', + 'immediate_charging', + 'delayed_charging', ]), }), 'context': , @@ -52,7 +52,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'DELAYED_CHARGING', + 'state': 'delayed_charging', }) # --- # name: test_entity_state_attrs[select.i4_edrive40_ac_charging_limit-entry] @@ -141,8 +141,8 @@ 'area_id': None, 'capabilities': dict({ 'options': list([ - 'IMMEDIATE_CHARGING', - 'DELAYED_CHARGING', + 'immediate_charging', + 'delayed_charging', ]), }), 'config_entry_id': , @@ -178,8 +178,8 @@ 'attribution': 'Data provided by MyBMW', 'friendly_name': 'i4 eDrive40 Charging Mode', 'options': list([ - 'IMMEDIATE_CHARGING', - 'DELAYED_CHARGING', + 'immediate_charging', + 'delayed_charging', ]), }), 'context': , @@ -187,7 +187,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'IMMEDIATE_CHARGING', + 'state': 'immediate_charging', }) # --- # name: test_entity_state_attrs[select.ix_xdrive50_ac_charging_limit-entry] @@ -276,8 +276,8 @@ 'area_id': None, 'capabilities': dict({ 'options': list([ - 'IMMEDIATE_CHARGING', - 'DELAYED_CHARGING', + 'immediate_charging', + 'delayed_charging', ]), }), 'config_entry_id': , @@ -313,8 +313,8 @@ 'attribution': 'Data provided by MyBMW', 'friendly_name': 'iX xDrive50 Charging Mode', 'options': list([ - 'IMMEDIATE_CHARGING', - 'DELAYED_CHARGING', + 'immediate_charging', + 'delayed_charging', ]), }), 'context': , @@ -322,6 +322,6 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'IMMEDIATE_CHARGING', + 'state': 'immediate_charging', }) # --- diff --git a/tests/components/bmw_connected_drive/snapshots/test_sensor.ambr b/tests/components/bmw_connected_drive/snapshots/test_sensor.ambr index 3455a4599b5..eaa33038baf 100644 --- a/tests/components/bmw_connected_drive/snapshots/test_sensor.ambr +++ b/tests/components/bmw_connected_drive/snapshots/test_sensor.ambr @@ -191,7 +191,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'WAITING_FOR_CHARGING', + 'state': 'waiting_for_charging', }) # --- # name: test_entity_state_attrs[sensor.i3_rex_charging_target-entry] @@ -822,7 +822,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'NOT_CHARGING', + 'state': 'not_charging', }) # --- # name: test_entity_state_attrs[sensor.i4_edrive40_charging_target-entry] @@ -1350,7 +1350,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'CHARGING', + 'state': 'charging', }) # --- # name: test_entity_state_attrs[sensor.ix_xdrive50_charging_target-entry] diff --git a/tests/components/bmw_connected_drive/test_select.py b/tests/components/bmw_connected_drive/test_select.py index 37aea4e0839..55e19482ef6 100644 --- a/tests/components/bmw_connected_drive/test_select.py +++ b/tests/components/bmw_connected_drive/test_select.py @@ -42,15 +42,15 @@ async def test_entity_state_attrs( [ ( "select.i3_rex_charging_mode", - "IMMEDIATE_CHARGING", - "DELAYED_CHARGING", + "immediate_charging", + "delayed_charging", "charging-profile", ), ("select.i4_edrive40_ac_charging_limit", "12", "16", "charging-settings"), ( "select.i4_edrive40_charging_mode", - "DELAYED_CHARGING", - "IMMEDIATE_CHARGING", + "delayed_charging", + "immediate_charging", "charging-profile", ), ], @@ -87,7 +87,7 @@ async def test_service_call_success( ("entity_id", "value"), [ ("select.i4_edrive40_ac_charging_limit", "17"), - ("select.i4_edrive40_charging_mode", "BONKERS_MODE"), + ("select.i4_edrive40_charging_mode", "bonkers_mode"), ], ) async def test_service_call_invalid_input( From 5166426d0a5185caed0f0a5dd0da0dc653cfcae8 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sat, 8 Jun 2024 16:32:27 +0200 Subject: [PATCH 0383/1445] Add type hints for service_calls fixture in pylint plugin (#118356) --- pylint/plugins/hass_enforce_type_hints.py | 1 + .../test_device_condition.py | 50 ++++++++---------- .../test_device_trigger.py | 51 ++++++++----------- tests/conftest.py | 20 +++++++- 4 files changed, 63 insertions(+), 59 deletions(-) diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index 0adebaf98f6..72cbf2ee04a 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -154,6 +154,7 @@ _TEST_FIXTURES: dict[str, list[str] | str] = { "recorder_mock": "Recorder", "request": "pytest.FixtureRequest", "requests_mock": "Mocker", + "service_calls": "list[ServiceCall]", "snapshot": "SnapshotAssertion", "socket_enabled": "None", "stub_blueprint_populate": "None", diff --git a/tests/components/alarm_control_panel/test_device_condition.py b/tests/components/alarm_control_panel/test_device_condition.py index b6ee6b2faaa..9f8f56ccb6f 100644 --- a/tests/components/alarm_control_panel/test_device_condition.py +++ b/tests/components/alarm_control_panel/test_device_condition.py @@ -23,11 +23,7 @@ from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.setup import async_setup_component -from tests.common import ( - MockConfigEntry, - async_get_device_automations, - async_mock_service, -) +from tests.common import MockConfigEntry, async_get_device_automations @pytest.fixture(autouse=True, name="stub_blueprint_populate") @@ -35,12 +31,6 @@ def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None: """Stub copying the blueprints to the config folder.""" -@pytest.fixture -def calls(hass: HomeAssistant) -> list[ServiceCall]: - """Track calls to a mock service.""" - return async_mock_service(hass, "test", "automation") - - @pytest.mark.parametrize( ("set_state", "features_reg", "features_state", "expected_condition_types"), [ @@ -189,7 +179,7 @@ async def test_if_state( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - calls: list[ServiceCall], + service_calls: list[ServiceCall], ) -> None: """Test for all conditions.""" config_entry = MockConfigEntry(domain="test", data={}) @@ -373,8 +363,8 @@ async def test_if_state( hass.bus.async_fire("test_event6") hass.bus.async_fire("test_event7") await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["some"] == "is_triggered - event - test_event1" + assert len(service_calls) == 1 + assert service_calls[0].data["some"] == "is_triggered - event - test_event1" hass.states.async_set(entry.entity_id, STATE_ALARM_DISARMED) hass.bus.async_fire("test_event1") @@ -385,8 +375,8 @@ async def test_if_state( hass.bus.async_fire("test_event6") hass.bus.async_fire("test_event7") await hass.async_block_till_done() - assert len(calls) == 2 - assert calls[1].data["some"] == "is_disarmed - event - test_event2" + assert len(service_calls) == 2 + assert service_calls[1].data["some"] == "is_disarmed - event - test_event2" hass.states.async_set(entry.entity_id, STATE_ALARM_ARMED_HOME) hass.bus.async_fire("test_event1") @@ -397,8 +387,8 @@ async def test_if_state( hass.bus.async_fire("test_event6") hass.bus.async_fire("test_event7") await hass.async_block_till_done() - assert len(calls) == 3 - assert calls[2].data["some"] == "is_armed_home - event - test_event3" + assert len(service_calls) == 3 + assert service_calls[2].data["some"] == "is_armed_home - event - test_event3" hass.states.async_set(entry.entity_id, STATE_ALARM_ARMED_AWAY) hass.bus.async_fire("test_event1") @@ -409,8 +399,8 @@ async def test_if_state( hass.bus.async_fire("test_event6") hass.bus.async_fire("test_event7") await hass.async_block_till_done() - assert len(calls) == 4 - assert calls[3].data["some"] == "is_armed_away - event - test_event4" + assert len(service_calls) == 4 + assert service_calls[3].data["some"] == "is_armed_away - event - test_event4" hass.states.async_set(entry.entity_id, STATE_ALARM_ARMED_NIGHT) hass.bus.async_fire("test_event1") @@ -421,8 +411,8 @@ async def test_if_state( hass.bus.async_fire("test_event6") hass.bus.async_fire("test_event7") await hass.async_block_till_done() - assert len(calls) == 5 - assert calls[4].data["some"] == "is_armed_night - event - test_event5" + assert len(service_calls) == 5 + assert service_calls[4].data["some"] == "is_armed_night - event - test_event5" hass.states.async_set(entry.entity_id, STATE_ALARM_ARMED_VACATION) hass.bus.async_fire("test_event1") @@ -433,8 +423,8 @@ async def test_if_state( hass.bus.async_fire("test_event6") hass.bus.async_fire("test_event7") await hass.async_block_till_done() - assert len(calls) == 6 - assert calls[5].data["some"] == "is_armed_vacation - event - test_event6" + assert len(service_calls) == 6 + assert service_calls[5].data["some"] == "is_armed_vacation - event - test_event6" hass.states.async_set(entry.entity_id, STATE_ALARM_ARMED_CUSTOM_BYPASS) hass.bus.async_fire("test_event1") @@ -445,15 +435,17 @@ async def test_if_state( hass.bus.async_fire("test_event6") hass.bus.async_fire("test_event7") await hass.async_block_till_done() - assert len(calls) == 7 - assert calls[6].data["some"] == "is_armed_custom_bypass - event - test_event7" + assert len(service_calls) == 7 + assert ( + service_calls[6].data["some"] == "is_armed_custom_bypass - event - test_event7" + ) async def test_if_state_legacy( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - calls: list[ServiceCall], + service_calls: list[ServiceCall], ) -> None: """Test for all conditions.""" config_entry = MockConfigEntry(domain="test", data={}) @@ -499,5 +491,5 @@ async def test_if_state_legacy( hass.states.async_set(entry.entity_id, STATE_ALARM_TRIGGERED) hass.bus.async_fire("test_event1") await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["some"] == "is_triggered - event - test_event1" + assert len(service_calls) == 1 + assert service_calls[0].data["some"] == "is_triggered - event - test_event1" diff --git a/tests/components/alarm_control_panel/test_device_trigger.py b/tests/components/alarm_control_panel/test_device_trigger.py index ff77cb7c264..6be15cca097 100644 --- a/tests/components/alarm_control_panel/test_device_trigger.py +++ b/tests/components/alarm_control_panel/test_device_trigger.py @@ -31,7 +31,6 @@ from tests.common import ( async_fire_time_changed, async_get_device_automation_capabilities, async_get_device_automations, - async_mock_service, ) @@ -40,12 +39,6 @@ def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None: """Stub copying the blueprints to the config folder.""" -@pytest.fixture -def calls(hass: HomeAssistant) -> list[ServiceCall]: - """Track calls to a mock service.""" - return async_mock_service(hass, "test", "automation") - - @pytest.mark.parametrize( ("set_state", "features_reg", "features_state", "expected_trigger_types"), [ @@ -250,7 +243,7 @@ async def test_if_fires_on_state_change( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - calls: list[ServiceCall], + service_calls: list[ServiceCall], ) -> None: """Test for turn_on and turn_off triggers firing.""" config_entry = MockConfigEntry(domain="test", data={}) @@ -409,54 +402,54 @@ async def test_if_fires_on_state_change( # Fake that the entity is triggered. hass.states.async_set(entry.entity_id, STATE_ALARM_TRIGGERED) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 assert ( - calls[0].data["some"] + service_calls[0].data["some"] == f"triggered - device - {entry.entity_id} - pending - triggered - None" ) # Fake that the entity is disarmed. hass.states.async_set(entry.entity_id, STATE_ALARM_DISARMED) await hass.async_block_till_done() - assert len(calls) == 2 + assert len(service_calls) == 2 assert ( - calls[1].data["some"] + service_calls[1].data["some"] == f"disarmed - device - {entry.entity_id} - triggered - disarmed - None" ) # Fake that the entity is armed home. hass.states.async_set(entry.entity_id, STATE_ALARM_ARMED_HOME) await hass.async_block_till_done() - assert len(calls) == 3 + assert len(service_calls) == 3 assert ( - calls[2].data["some"] + service_calls[2].data["some"] == f"armed_home - device - {entry.entity_id} - disarmed - armed_home - None" ) # Fake that the entity is armed away. hass.states.async_set(entry.entity_id, STATE_ALARM_ARMED_AWAY) await hass.async_block_till_done() - assert len(calls) == 4 + assert len(service_calls) == 4 assert ( - calls[3].data["some"] + service_calls[3].data["some"] == f"armed_away - device - {entry.entity_id} - armed_home - armed_away - None" ) # Fake that the entity is armed night. hass.states.async_set(entry.entity_id, STATE_ALARM_ARMED_NIGHT) await hass.async_block_till_done() - assert len(calls) == 5 + assert len(service_calls) == 5 assert ( - calls[4].data["some"] + service_calls[4].data["some"] == f"armed_night - device - {entry.entity_id} - armed_away - armed_night - None" ) # Fake that the entity is armed vacation. hass.states.async_set(entry.entity_id, STATE_ALARM_ARMED_VACATION) await hass.async_block_till_done() - assert len(calls) == 6 + assert len(service_calls) == 6 assert ( - calls[5].data["some"] + service_calls[5].data["some"] == f"armed_vacation - device - {entry.entity_id} - armed_night - armed_vacation - None" ) @@ -465,7 +458,7 @@ async def test_if_fires_on_state_change_with_for( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - calls: list[ServiceCall], + service_calls: list[ServiceCall], ) -> None: """Test for triggers firing with delay.""" config_entry = MockConfigEntry(domain="test", data={}) @@ -511,17 +504,17 @@ async def test_if_fires_on_state_change_with_for( }, ) await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 hass.states.async_set(entry.entity_id, STATE_ALARM_TRIGGERED) await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10)) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 await hass.async_block_till_done() assert ( - calls[0].data["some"] + service_calls[0].data["some"] == f"turn_off device - {entry.entity_id} - disarmed - triggered - 0:00:05" ) @@ -530,7 +523,7 @@ async def test_if_fires_on_state_change_legacy( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - calls: list[ServiceCall], + service_calls: list[ServiceCall], ) -> None: """Test for triggers firing with delay.""" config_entry = MockConfigEntry(domain="test", data={}) @@ -575,12 +568,12 @@ async def test_if_fires_on_state_change_legacy( }, ) await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 hass.states.async_set(entry.entity_id, STATE_ALARM_TRIGGERED) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 assert ( - calls[0].data["some"] + service_calls[0].data["some"] == f"turn_off device - {entry.entity_id} - disarmed - triggered - None" ) diff --git a/tests/conftest.py b/tests/conftest.py index 35da0215247..78fb6835abe 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -51,7 +51,7 @@ from homeassistant.components.websocket_api.http import URL from homeassistant.config import YAML_CONFIG_FILE from homeassistant.config_entries import ConfigEntries, ConfigEntry, ConfigEntryState from homeassistant.const import HASSIO_USER_NAME -from homeassistant.core import CoreState, HassJob, HomeAssistant +from homeassistant.core import CoreState, HassJob, HomeAssistant, ServiceCall from homeassistant.helpers import ( area_registry as ar, category_registry as cr, @@ -1775,6 +1775,24 @@ def label_registry(hass: HomeAssistant) -> lr.LabelRegistry: return lr.async_get(hass) +@pytest.fixture +def service_calls() -> Generator[None, None, list[ServiceCall]]: + """Track all service calls.""" + calls = [] + + async def _async_call( + self, + domain: str, + service: str, + service_data: dict[str, Any] | None = None, + **kwargs: Any, + ): + calls.append(ServiceCall(domain, service, service_data)) + + with patch("homeassistant.core.ServiceRegistry.async_call", _async_call): + yield calls + + @pytest.fixture def snapshot(snapshot: SnapshotAssertion) -> SnapshotAssertion: """Return snapshot assertion fixture with the Home Assistant extension.""" From a2504dafbcc96a57e30d325a7c2b19c0ed48238e Mon Sep 17 00:00:00 2001 From: Christian Neumeier <47736781+NECH2004@users.noreply.github.com> Date: Sat, 8 Jun 2024 16:34:17 +0200 Subject: [PATCH 0384/1445] Refactor Zeversolar init tests (#118551) Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> --- tests/components/zeversolar/conftest.py | 47 ++++++++++++++++++++++ tests/components/zeversolar/test_init.py | 51 ++++++++++++++---------- 2 files changed, 76 insertions(+), 22 deletions(-) create mode 100644 tests/components/zeversolar/conftest.py diff --git a/tests/components/zeversolar/conftest.py b/tests/components/zeversolar/conftest.py new file mode 100644 index 00000000000..55d84f50a1b --- /dev/null +++ b/tests/components/zeversolar/conftest.py @@ -0,0 +1,47 @@ +"""Define mocks and test objects.""" + +import pytest +from zeversolar import StatusEnum, ZeverSolarData + +from homeassistant.components.zeversolar.const import DOMAIN +from homeassistant.const import CONF_HOST, CONF_PORT + +from tests.common import MockConfigEntry + +MOCK_HOST_ZEVERSOLAR = "zeversolar-fake-host" +MOCK_PORT_ZEVERSOLAR = 10200 + + +@pytest.fixture +def config_entry() -> MockConfigEntry: + """Create a mock config entry.""" + + return MockConfigEntry( + data={ + CONF_HOST: MOCK_HOST_ZEVERSOLAR, + CONF_PORT: MOCK_PORT_ZEVERSOLAR, + }, + domain=DOMAIN, + unique_id="my_id_2", + ) + + +@pytest.fixture +def zeversolar_data() -> ZeverSolarData: + """Create a ZeverSolarData structure for tests.""" + + return ZeverSolarData( + wifi_enabled=False, + serial_or_registry_id="1223", + registry_key="A-2", + hardware_version="M10", + software_version="123-23", + reported_datetime="19900101 23:00", + communication_status=StatusEnum.OK, + num_inverters=1, + serial_number="123456778", + pac=1234, + energy_today=123, + status=StatusEnum.OK, + meter_status=StatusEnum.OK, + ) diff --git a/tests/components/zeversolar/test_init.py b/tests/components/zeversolar/test_init.py index 56d06db414c..3eee530a9a2 100644 --- a/tests/components/zeversolar/test_init.py +++ b/tests/components/zeversolar/test_init.py @@ -1,32 +1,39 @@ """Test the init file code.""" -import pytest +from unittest.mock import patch -import homeassistant.components.zeversolar.__init__ as init -from homeassistant.components.zeversolar.const import DOMAIN -from homeassistant.const import CONF_HOST, CONF_PORT +from zeversolar import ZeverSolarData +from zeversolar.exceptions import ZeverSolarTimeout + +from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady -from tests.common import MockConfigEntry, MockModule, mock_integration - -MOCK_HOST_ZEVERSOLAR = "zeversolar-fake-host" -MOCK_PORT_ZEVERSOLAR = 10200 +from tests.common import MockConfigEntry -async def test_async_setup_entry_fails(hass: HomeAssistant) -> None: - """Test the sensor setup.""" - mock_integration(hass, MockModule(DOMAIN)) +async def test_async_setup_entry_fails( + hass: HomeAssistant, config_entry: MockConfigEntry, zeversolar_data: ZeverSolarData +) -> None: + """Test to load/unload the integration.""" - config = MockConfigEntry( - data={ - CONF_HOST: MOCK_HOST_ZEVERSOLAR, - CONF_PORT: MOCK_PORT_ZEVERSOLAR, - }, - domain=DOMAIN, - ) + config_entry.add_to_hass(hass) - config.add_to_hass(hass) + with ( + patch("zeversolar.ZeverSolarClient.get_data", side_effect=ZeverSolarTimeout), + ): + await hass.config_entries.async_setup(config_entry.entry_id) + assert config_entry.state is ConfigEntryState.SETUP_RETRY - with pytest.raises(ConfigEntryNotReady): - await init.async_setup_entry(hass, config) + with ( + patch("homeassistant.components.zeversolar.PLATFORMS", []), + patch("zeversolar.ZeverSolarClient.get_data", return_value=zeversolar_data), + ): + hass.config_entries.async_schedule_reload(config_entry.entry_id) + assert config_entry.state is ConfigEntryState.LOADED + + with ( + patch("homeassistant.components.zeversolar.PLATFORMS", []), + ): + result = await hass.config_entries.async_unload(config_entry.entry_id) + assert result is True + assert config_entry.state is ConfigEntryState.NOT_LOADED From 43343ea44aa2925b6cffa4ab2ca8d963645a1f6a Mon Sep 17 00:00:00 2001 From: Richard Kroegel <42204099+rikroe@users.noreply.github.com> Date: Sat, 8 Jun 2024 17:08:37 +0200 Subject: [PATCH 0385/1445] Adjust BMW enum sensors translations (#118754) Co-authored-by: Richard --- .../components/bmw_connected_drive/const.py | 7 -- .../components/bmw_connected_drive/select.py | 12 +-- .../components/bmw_connected_drive/sensor.py | 12 ++- .../snapshots/test_sensor.ambr | 102 ++++++++++++++++-- .../bmw_connected_drive/test_select.py | 29 +++++ .../bmw_connected_drive/test_sensor.py | 30 ++++++ 6 files changed, 171 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/bmw_connected_drive/const.py b/homeassistant/components/bmw_connected_drive/const.py index 5374b52e684..49990977f71 100644 --- a/homeassistant/components/bmw_connected_drive/const.py +++ b/homeassistant/components/bmw_connected_drive/const.py @@ -28,10 +28,3 @@ SCAN_INTERVALS = { "north_america": 600, "rest_of_world": 300, } - -CLIMATE_ACTIVITY_STATE: list[str] = [ - "cooling", - "heating", - "inactive", - "standby", -] diff --git a/homeassistant/components/bmw_connected_drive/select.py b/homeassistant/components/bmw_connected_drive/select.py index db54627b5b6..2522c6bf2a6 100644 --- a/homeassistant/components/bmw_connected_drive/select.py +++ b/homeassistant/components/bmw_connected_drive/select.py @@ -33,8 +33,8 @@ class BMWSelectEntityDescription(SelectEntityDescription): dynamic_options: Callable[[MyBMWVehicle], list[str]] | None = None -SELECT_TYPES: dict[str, BMWSelectEntityDescription] = { - "ac_limit": BMWSelectEntityDescription( +SELECT_TYPES: tuple[BMWSelectEntityDescription, ...] = ( + BMWSelectEntityDescription( key="ac_limit", translation_key="ac_limit", is_available=lambda v: v.is_remote_set_ac_limit_enabled, @@ -48,17 +48,17 @@ SELECT_TYPES: dict[str, BMWSelectEntityDescription] = { ), unit_of_measurement=UnitOfElectricCurrent.AMPERE, ), - "charging_mode": BMWSelectEntityDescription( + BMWSelectEntityDescription( key="charging_mode", translation_key="charging_mode", is_available=lambda v: v.is_charging_plan_supported, options=[c.value.lower() for c in ChargingMode if c != ChargingMode.UNKNOWN], - current_option=lambda v: str(v.charging_profile.charging_mode.value).lower(), # type: ignore[union-attr] + current_option=lambda v: v.charging_profile.charging_mode.value.lower(), # type: ignore[union-attr] remote_service=lambda v, o: v.remote_services.trigger_charging_profile_update( charging_mode=ChargingMode(o) ), ), -} +) async def async_setup_entry( @@ -76,7 +76,7 @@ async def async_setup_entry( entities.extend( [ BMWSelect(coordinator, vehicle, description) - for description in SELECT_TYPES.values() + for description in SELECT_TYPES if description.is_available(vehicle) ] ) diff --git a/homeassistant/components/bmw_connected_drive/sensor.py b/homeassistant/components/bmw_connected_drive/sensor.py index 34169817f47..1d9737c7d5f 100644 --- a/homeassistant/components/bmw_connected_drive/sensor.py +++ b/homeassistant/components/bmw_connected_drive/sensor.py @@ -9,6 +9,8 @@ import logging from bimmer_connected.models import StrEnum, ValueWithUnit from bimmer_connected.vehicle import MyBMWVehicle +from bimmer_connected.vehicle.climate import ClimateActivityState +from bimmer_connected.vehicle.fuel_and_battery import ChargingState from homeassistant.components.sensor import ( SensorDeviceClass, @@ -29,7 +31,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import dt as dt_util from . import BMWBaseEntity -from .const import CLIMATE_ACTIVITY_STATE, DOMAIN +from .const import DOMAIN from .coordinator import BMWDataUpdateCoordinator _LOGGER = logging.getLogger(__name__) @@ -73,6 +75,8 @@ SENSOR_TYPES: list[BMWSensorEntityDescription] = [ key="charging_status", translation_key="charging_status", key_class="fuel_and_battery", + device_class=SensorDeviceClass.ENUM, + options=[s.value.lower() for s in ChargingState if s != ChargingState.UNKNOWN], is_available=lambda v: v.is_lsc_enabled and v.has_electric_drivetrain, ), BMWSensorEntityDescription( @@ -155,7 +159,11 @@ SENSOR_TYPES: list[BMWSensorEntityDescription] = [ translation_key="climate_status", key_class="climate", device_class=SensorDeviceClass.ENUM, - options=CLIMATE_ACTIVITY_STATE, + options=[ + s.value.lower() + for s in ClimateActivityState + if s != ClimateActivityState.UNKNOWN + ], is_available=lambda v: v.is_remote_climate_stop_enabled, ), ] diff --git a/tests/components/bmw_connected_drive/snapshots/test_sensor.ambr b/tests/components/bmw_connected_drive/snapshots/test_sensor.ambr index eaa33038baf..6ba87c029ee 100644 --- a/tests/components/bmw_connected_drive/snapshots/test_sensor.ambr +++ b/tests/components/bmw_connected_drive/snapshots/test_sensor.ambr @@ -152,7 +152,22 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'options': list([ + 'default', + 'charging', + 'error', + 'complete', + 'fully_charged', + 'finished_fully_charged', + 'finished_not_full', + 'invalid', + 'not_charging', + 'plugged_in', + 'waiting_for_charging', + 'target_reached', + ]), + }), 'config_entry_id': , 'device_class': None, 'device_id': , @@ -169,7 +184,7 @@ 'name': None, 'options': dict({ }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, 'original_name': 'Charging status', 'platform': 'bmw_connected_drive', @@ -184,7 +199,22 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by MyBMW', + 'device_class': 'enum', 'friendly_name': 'i3 (+ REX) Charging status', + 'options': list([ + 'default', + 'charging', + 'error', + 'complete', + 'fully_charged', + 'finished_fully_charged', + 'finished_not_full', + 'invalid', + 'not_charging', + 'plugged_in', + 'waiting_for_charging', + 'target_reached', + ]), }), 'context': , 'entity_id': 'sensor.i3_rex_charging_status', @@ -783,7 +813,22 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'options': list([ + 'default', + 'charging', + 'error', + 'complete', + 'fully_charged', + 'finished_fully_charged', + 'finished_not_full', + 'invalid', + 'not_charging', + 'plugged_in', + 'waiting_for_charging', + 'target_reached', + ]), + }), 'config_entry_id': , 'device_class': None, 'device_id': , @@ -800,7 +845,7 @@ 'name': None, 'options': dict({ }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, 'original_name': 'Charging status', 'platform': 'bmw_connected_drive', @@ -815,7 +860,22 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by MyBMW', + 'device_class': 'enum', 'friendly_name': 'i4 eDrive40 Charging status', + 'options': list([ + 'default', + 'charging', + 'error', + 'complete', + 'fully_charged', + 'finished_fully_charged', + 'finished_not_full', + 'invalid', + 'not_charging', + 'plugged_in', + 'waiting_for_charging', + 'target_reached', + ]), }), 'context': , 'entity_id': 'sensor.i4_edrive40_charging_status', @@ -1311,7 +1371,22 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'options': list([ + 'default', + 'charging', + 'error', + 'complete', + 'fully_charged', + 'finished_fully_charged', + 'finished_not_full', + 'invalid', + 'not_charging', + 'plugged_in', + 'waiting_for_charging', + 'target_reached', + ]), + }), 'config_entry_id': , 'device_class': None, 'device_id': , @@ -1328,7 +1403,7 @@ 'name': None, 'options': dict({ }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, 'original_name': 'Charging status', 'platform': 'bmw_connected_drive', @@ -1343,7 +1418,22 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by MyBMW', + 'device_class': 'enum', 'friendly_name': 'iX xDrive50 Charging status', + 'options': list([ + 'default', + 'charging', + 'error', + 'complete', + 'fully_charged', + 'finished_fully_charged', + 'finished_not_full', + 'invalid', + 'not_charging', + 'plugged_in', + 'waiting_for_charging', + 'target_reached', + ]), }), 'context': , 'entity_id': 'sensor.ix_xdrive50_charging_status', diff --git a/tests/components/bmw_connected_drive/test_select.py b/tests/components/bmw_connected_drive/test_select.py index 55e19482ef6..a270f38ee01 100644 --- a/tests/components/bmw_connected_drive/test_select.py +++ b/tests/components/bmw_connected_drive/test_select.py @@ -8,10 +8,13 @@ import pytest import respx from syrupy.assertion import SnapshotAssertion +from homeassistant.components.bmw_connected_drive import DOMAIN as BMW_DOMAIN +from homeassistant.components.bmw_connected_drive.select import SELECT_TYPES from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError, ServiceValidationError from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.translation import async_get_translations from . import check_remote_service_call, setup_mocked_integration @@ -152,3 +155,29 @@ async def test_service_call_fail( target={"entity_id": entity_id}, ) assert hass.states.get(entity_id).state == old_value + + +@pytest.mark.usefixtures("bmw_fixture") +async def test_entity_option_translations( + hass: HomeAssistant, +) -> None: + """Ensure all enum sensor values are translated.""" + + # Setup component to load translations + assert await setup_mocked_integration(hass) + + prefix = f"component.{BMW_DOMAIN}.entity.{Platform.SELECT.value}" + + translations = await async_get_translations(hass, "en", "entity", [BMW_DOMAIN]) + translation_states = { + k for k in translations if k.startswith(prefix) and ".state." in k + } + + sensor_options = { + f"{prefix}.{entity_description.translation_key}.state.{option}" + for entity_description in SELECT_TYPES + if entity_description.options + for option in entity_description.options + } + + assert sensor_options == translation_states diff --git a/tests/components/bmw_connected_drive/test_sensor.py b/tests/components/bmw_connected_drive/test_sensor.py index 2f83fa108e5..c89df2caa7a 100644 --- a/tests/components/bmw_connected_drive/test_sensor.py +++ b/tests/components/bmw_connected_drive/test_sensor.py @@ -5,9 +5,13 @@ from unittest.mock import patch import pytest from syrupy.assertion import SnapshotAssertion +from homeassistant.components.bmw_connected_drive import DOMAIN as BMW_DOMAIN +from homeassistant.components.bmw_connected_drive.sensor import SENSOR_TYPES +from homeassistant.components.sensor.const import SensorDeviceClass from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.translation import async_get_translations from homeassistant.util.unit_system import ( METRIC_SYSTEM as METRIC, US_CUSTOMARY_SYSTEM as IMPERIAL, @@ -77,3 +81,29 @@ async def test_unit_conversion( entity = hass.states.get(entity_id) assert entity.state == value assert entity.attributes.get("unit_of_measurement") == unit_of_measurement + + +@pytest.mark.usefixtures("bmw_fixture") +async def test_entity_option_translations( + hass: HomeAssistant, +) -> None: + """Ensure all enum sensor values are translated.""" + + # Setup component to load translations + assert await setup_mocked_integration(hass) + + prefix = f"component.{BMW_DOMAIN}.entity.{Platform.SENSOR.value}" + + translations = await async_get_translations(hass, "en", "entity", [BMW_DOMAIN]) + translation_states = { + k for k in translations if k.startswith(prefix) and ".state." in k + } + + sensor_options = { + f"{prefix}.{entity_description.translation_key}.state.{option}" + for entity_description in SENSOR_TYPES + if entity_description.device_class == SensorDeviceClass.ENUM + for option in entity_description.options + } + + assert sensor_options == translation_states From 2c41451abc2e4a16da814affc6f0f25342df92cb Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Sat, 8 Jun 2024 11:31:05 -0400 Subject: [PATCH 0386/1445] Add new security keys to zwave_js config flow (#115835) --- homeassistant/components/zwave_js/__init__.py | 27 +++- .../components/zwave_js/config_flow.py | 52 +++++++ homeassistant/components/zwave_js/const.py | 7 + tests/components/zwave_js/test_config_flow.py | 145 ++++++++++++++++++ tests/components/zwave_js/test_init.py | 24 +++ 5 files changed, 254 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index 2b685212642..4b0cc4ac7a9 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -8,6 +8,7 @@ from contextlib import suppress import logging from typing import Any +from awesomeversion import AwesomeVersion from zwave_js_server.client import Client as ZwaveClient from zwave_js_server.const import CommandClass, RemoveNodeReason from zwave_js_server.exceptions import BaseZwaveJSServerError, InvalidServerVersion @@ -78,6 +79,8 @@ from .const import ( ATTR_VALUE, ATTR_VALUE_RAW, CONF_ADDON_DEVICE, + CONF_ADDON_LR_S2_ACCESS_CONTROL_KEY, + CONF_ADDON_LR_S2_AUTHENTICATED_KEY, CONF_ADDON_NETWORK_KEY, CONF_ADDON_S0_LEGACY_KEY, CONF_ADDON_S2_ACCESS_CONTROL_KEY, @@ -85,6 +88,8 @@ from .const import ( CONF_ADDON_S2_UNAUTHENTICATED_KEY, CONF_DATA_COLLECTION_OPTED_IN, CONF_INTEGRATION_CREATED_ADDON, + CONF_LR_S2_ACCESS_CONTROL_KEY, + CONF_LR_S2_AUTHENTICATED_KEY, CONF_NETWORK_KEY, CONF_S0_LEGACY_KEY, CONF_S2_ACCESS_CONTROL_KEY, @@ -97,6 +102,7 @@ from .const import ( EVENT_DEVICE_ADDED_TO_REGISTRY, LIB_LOGGER, LOGGER, + LR_ADDON_VERSION, USER_AGENT, ZWAVE_JS_NOTIFICATION_EVENT, ZWAVE_JS_VALUE_NOTIFICATION_EVENT, @@ -1051,8 +1057,9 @@ async def async_ensure_addon_running(hass: HomeAssistant, entry: ConfigEntry) -> s2_access_control_key: str = entry.data.get(CONF_S2_ACCESS_CONTROL_KEY, "") s2_authenticated_key: str = entry.data.get(CONF_S2_AUTHENTICATED_KEY, "") s2_unauthenticated_key: str = entry.data.get(CONF_S2_UNAUTHENTICATED_KEY, "") + lr_s2_access_control_key: str = entry.data.get(CONF_LR_S2_ACCESS_CONTROL_KEY, "") + lr_s2_authenticated_key: str = entry.data.get(CONF_LR_S2_AUTHENTICATED_KEY, "") addon_state = addon_info.state - addon_config = { CONF_ADDON_DEVICE: usb_path, CONF_ADDON_S0_LEGACY_KEY: s0_legacy_key, @@ -1060,6 +1067,9 @@ async def async_ensure_addon_running(hass: HomeAssistant, entry: ConfigEntry) -> CONF_ADDON_S2_AUTHENTICATED_KEY: s2_authenticated_key, CONF_ADDON_S2_UNAUTHENTICATED_KEY: s2_unauthenticated_key, } + if addon_info.version and AwesomeVersion(addon_info.version) >= LR_ADDON_VERSION: + addon_config[CONF_ADDON_LR_S2_ACCESS_CONTROL_KEY] = lr_s2_access_control_key + addon_config[CONF_ADDON_LR_S2_AUTHENTICATED_KEY] = lr_s2_authenticated_key if addon_state == AddonState.NOT_INSTALLED: addon_manager.async_schedule_install_setup_addon( @@ -1099,6 +1109,21 @@ async def async_ensure_addon_running(hass: HomeAssistant, entry: ConfigEntry) -> updates[CONF_S2_AUTHENTICATED_KEY] = addon_s2_authenticated_key if s2_unauthenticated_key != addon_s2_unauthenticated_key: updates[CONF_S2_UNAUTHENTICATED_KEY] = addon_s2_unauthenticated_key + + if addon_info.version and AwesomeVersion(addon_info.version) >= AwesomeVersion( + LR_ADDON_VERSION + ): + addon_lr_s2_access_control_key = addon_options.get( + CONF_ADDON_LR_S2_ACCESS_CONTROL_KEY, "" + ) + addon_lr_s2_authenticated_key = addon_options.get( + CONF_ADDON_LR_S2_AUTHENTICATED_KEY, "" + ) + if lr_s2_access_control_key != addon_lr_s2_access_control_key: + updates[CONF_LR_S2_ACCESS_CONTROL_KEY] = addon_lr_s2_access_control_key + if lr_s2_authenticated_key != addon_lr_s2_authenticated_key: + updates[CONF_LR_S2_AUTHENTICATED_KEY] = addon_lr_s2_authenticated_key + if updates: hass.config_entries.async_update_entry(entry, data={**entry.data, **updates}) diff --git a/homeassistant/components/zwave_js/config_flow.py b/homeassistant/components/zwave_js/config_flow.py index 069d9f6d003..dff582558b1 100644 --- a/homeassistant/components/zwave_js/config_flow.py +++ b/homeassistant/components/zwave_js/config_flow.py @@ -46,12 +46,16 @@ from .const import ( CONF_ADDON_DEVICE, CONF_ADDON_EMULATE_HARDWARE, CONF_ADDON_LOG_LEVEL, + CONF_ADDON_LR_S2_ACCESS_CONTROL_KEY, + CONF_ADDON_LR_S2_AUTHENTICATED_KEY, CONF_ADDON_NETWORK_KEY, CONF_ADDON_S0_LEGACY_KEY, CONF_ADDON_S2_ACCESS_CONTROL_KEY, CONF_ADDON_S2_AUTHENTICATED_KEY, CONF_ADDON_S2_UNAUTHENTICATED_KEY, CONF_INTEGRATION_CREATED_ADDON, + CONF_LR_S2_ACCESS_CONTROL_KEY, + CONF_LR_S2_AUTHENTICATED_KEY, CONF_S0_LEGACY_KEY, CONF_S2_ACCESS_CONTROL_KEY, CONF_S2_AUTHENTICATED_KEY, @@ -86,6 +90,8 @@ ADDON_USER_INPUT_MAP = { CONF_ADDON_S2_ACCESS_CONTROL_KEY: CONF_S2_ACCESS_CONTROL_KEY, CONF_ADDON_S2_AUTHENTICATED_KEY: CONF_S2_AUTHENTICATED_KEY, CONF_ADDON_S2_UNAUTHENTICATED_KEY: CONF_S2_UNAUTHENTICATED_KEY, + CONF_ADDON_LR_S2_ACCESS_CONTROL_KEY: CONF_LR_S2_ACCESS_CONTROL_KEY, + CONF_ADDON_LR_S2_AUTHENTICATED_KEY: CONF_LR_S2_AUTHENTICATED_KEY, CONF_ADDON_LOG_LEVEL: CONF_LOG_LEVEL, CONF_ADDON_EMULATE_HARDWARE: CONF_EMULATE_HARDWARE, } @@ -172,6 +178,8 @@ class BaseZwaveJSFlow(ConfigEntryBaseFlow, ABC): self.s2_access_control_key: str | None = None self.s2_authenticated_key: str | None = None self.s2_unauthenticated_key: str | None = None + self.lr_s2_access_control_key: str | None = None + self.lr_s2_authenticated_key: str | None = None self.usb_path: str | None = None self.ws_address: str | None = None self.restart_addon: bool = False @@ -565,6 +573,12 @@ class ZWaveJSConfigFlow(BaseZwaveJSFlow, ConfigFlow, domain=DOMAIN): self.s2_unauthenticated_key = addon_config.get( CONF_ADDON_S2_UNAUTHENTICATED_KEY, "" ) + self.lr_s2_access_control_key = addon_config.get( + CONF_ADDON_LR_S2_ACCESS_CONTROL_KEY, "" + ) + self.lr_s2_authenticated_key = addon_config.get( + CONF_ADDON_LR_S2_AUTHENTICATED_KEY, "" + ) return await self.async_step_finish_addon_setup() if addon_info.state == AddonState.NOT_RUNNING: @@ -584,6 +598,8 @@ class ZWaveJSConfigFlow(BaseZwaveJSFlow, ConfigFlow, domain=DOMAIN): self.s2_access_control_key = user_input[CONF_S2_ACCESS_CONTROL_KEY] self.s2_authenticated_key = user_input[CONF_S2_AUTHENTICATED_KEY] self.s2_unauthenticated_key = user_input[CONF_S2_UNAUTHENTICATED_KEY] + self.lr_s2_access_control_key = user_input[CONF_LR_S2_ACCESS_CONTROL_KEY] + self.lr_s2_authenticated_key = user_input[CONF_LR_S2_AUTHENTICATED_KEY] if not self._usb_discovery: self.usb_path = user_input[CONF_USB_PATH] @@ -594,6 +610,8 @@ class ZWaveJSConfigFlow(BaseZwaveJSFlow, ConfigFlow, domain=DOMAIN): CONF_ADDON_S2_ACCESS_CONTROL_KEY: self.s2_access_control_key, CONF_ADDON_S2_AUTHENTICATED_KEY: self.s2_authenticated_key, CONF_ADDON_S2_UNAUTHENTICATED_KEY: self.s2_unauthenticated_key, + CONF_ADDON_LR_S2_ACCESS_CONTROL_KEY: self.lr_s2_access_control_key, + CONF_ADDON_LR_S2_AUTHENTICATED_KEY: self.lr_s2_authenticated_key, } if new_addon_config != addon_config: @@ -614,6 +632,12 @@ class ZWaveJSConfigFlow(BaseZwaveJSFlow, ConfigFlow, domain=DOMAIN): s2_unauthenticated_key = addon_config.get( CONF_ADDON_S2_UNAUTHENTICATED_KEY, self.s2_unauthenticated_key or "" ) + lr_s2_access_control_key = addon_config.get( + CONF_ADDON_LR_S2_ACCESS_CONTROL_KEY, self.lr_s2_access_control_key or "" + ) + lr_s2_authenticated_key = addon_config.get( + CONF_ADDON_LR_S2_AUTHENTICATED_KEY, self.lr_s2_authenticated_key or "" + ) schema = { vol.Optional(CONF_S0_LEGACY_KEY, default=s0_legacy_key): str, @@ -624,6 +648,12 @@ class ZWaveJSConfigFlow(BaseZwaveJSFlow, ConfigFlow, domain=DOMAIN): vol.Optional( CONF_S2_UNAUTHENTICATED_KEY, default=s2_unauthenticated_key ): str, + vol.Optional( + CONF_LR_S2_ACCESS_CONTROL_KEY, default=lr_s2_access_control_key + ): str, + vol.Optional( + CONF_LR_S2_AUTHENTICATED_KEY, default=lr_s2_authenticated_key + ): str, } if not self._usb_discovery: @@ -670,6 +700,8 @@ class ZWaveJSConfigFlow(BaseZwaveJSFlow, ConfigFlow, domain=DOMAIN): CONF_S2_ACCESS_CONTROL_KEY: self.s2_access_control_key, CONF_S2_AUTHENTICATED_KEY: self.s2_authenticated_key, CONF_S2_UNAUTHENTICATED_KEY: self.s2_unauthenticated_key, + CONF_LR_S2_ACCESS_CONTROL_KEY: self.lr_s2_access_control_key, + CONF_LR_S2_AUTHENTICATED_KEY: self.lr_s2_authenticated_key, } ) return self._async_create_entry_from_vars() @@ -690,6 +722,8 @@ class ZWaveJSConfigFlow(BaseZwaveJSFlow, ConfigFlow, domain=DOMAIN): CONF_S2_ACCESS_CONTROL_KEY: self.s2_access_control_key, CONF_S2_AUTHENTICATED_KEY: self.s2_authenticated_key, CONF_S2_UNAUTHENTICATED_KEY: self.s2_unauthenticated_key, + CONF_LR_S2_ACCESS_CONTROL_KEY: self.lr_s2_access_control_key, + CONF_LR_S2_AUTHENTICATED_KEY: self.lr_s2_authenticated_key, CONF_USE_ADDON: self.use_addon, CONF_INTEGRATION_CREATED_ADDON: self.integration_created_addon, }, @@ -801,6 +835,8 @@ class OptionsFlowHandler(BaseZwaveJSFlow, OptionsFlow): self.s2_access_control_key = user_input[CONF_S2_ACCESS_CONTROL_KEY] self.s2_authenticated_key = user_input[CONF_S2_AUTHENTICATED_KEY] self.s2_unauthenticated_key = user_input[CONF_S2_UNAUTHENTICATED_KEY] + self.lr_s2_access_control_key = user_input[CONF_LR_S2_ACCESS_CONTROL_KEY] + self.lr_s2_authenticated_key = user_input[CONF_LR_S2_AUTHENTICATED_KEY] self.usb_path = user_input[CONF_USB_PATH] new_addon_config = { @@ -810,6 +846,8 @@ class OptionsFlowHandler(BaseZwaveJSFlow, OptionsFlow): CONF_ADDON_S2_ACCESS_CONTROL_KEY: self.s2_access_control_key, CONF_ADDON_S2_AUTHENTICATED_KEY: self.s2_authenticated_key, CONF_ADDON_S2_UNAUTHENTICATED_KEY: self.s2_unauthenticated_key, + CONF_ADDON_LR_S2_ACCESS_CONTROL_KEY: self.lr_s2_access_control_key, + CONF_ADDON_LR_S2_AUTHENTICATED_KEY: self.lr_s2_authenticated_key, CONF_ADDON_LOG_LEVEL: user_input[CONF_LOG_LEVEL], CONF_ADDON_EMULATE_HARDWARE: user_input.get( CONF_EMULATE_HARDWARE, False @@ -850,6 +888,12 @@ class OptionsFlowHandler(BaseZwaveJSFlow, OptionsFlow): s2_unauthenticated_key = addon_config.get( CONF_ADDON_S2_UNAUTHENTICATED_KEY, self.s2_unauthenticated_key or "" ) + lr_s2_access_control_key = addon_config.get( + CONF_ADDON_LR_S2_ACCESS_CONTROL_KEY, self.lr_s2_access_control_key or "" + ) + lr_s2_authenticated_key = addon_config.get( + CONF_ADDON_LR_S2_AUTHENTICATED_KEY, self.lr_s2_authenticated_key or "" + ) log_level = addon_config.get(CONF_ADDON_LOG_LEVEL, "info") emulate_hardware = addon_config.get(CONF_ADDON_EMULATE_HARDWARE, False) @@ -868,6 +912,12 @@ class OptionsFlowHandler(BaseZwaveJSFlow, OptionsFlow): vol.Optional( CONF_S2_UNAUTHENTICATED_KEY, default=s2_unauthenticated_key ): str, + vol.Optional( + CONF_LR_S2_ACCESS_CONTROL_KEY, default=lr_s2_access_control_key + ): str, + vol.Optional( + CONF_LR_S2_AUTHENTICATED_KEY, default=lr_s2_authenticated_key + ): str, vol.Optional(CONF_LOG_LEVEL, default=log_level): vol.In( ADDON_LOG_LEVELS ), @@ -921,6 +971,8 @@ class OptionsFlowHandler(BaseZwaveJSFlow, OptionsFlow): CONF_S2_ACCESS_CONTROL_KEY: self.s2_access_control_key, CONF_S2_AUTHENTICATED_KEY: self.s2_authenticated_key, CONF_S2_UNAUTHENTICATED_KEY: self.s2_unauthenticated_key, + CONF_LR_S2_ACCESS_CONTROL_KEY: self.lr_s2_access_control_key, + CONF_LR_S2_AUTHENTICATED_KEY: self.lr_s2_authenticated_key, CONF_USE_ADDON: True, CONF_INTEGRATION_CREATED_ADDON: self.integration_created_addon, } diff --git a/homeassistant/components/zwave_js/const.py b/homeassistant/components/zwave_js/const.py index f022cd42d20..a04f9247548 100644 --- a/homeassistant/components/zwave_js/const.py +++ b/homeassistant/components/zwave_js/const.py @@ -4,12 +4,15 @@ from __future__ import annotations import logging +from awesomeversion import AwesomeVersion from zwave_js_server.const.command_class.window_covering import ( WindowCoveringPropertyKey, ) from homeassistant.const import APPLICATION_NAME, __version__ as HA_VERSION +LR_ADDON_VERSION = AwesomeVersion("0.5.0") + USER_AGENT = {APPLICATION_NAME: HA_VERSION} CONF_ADDON_DEVICE = "device" @@ -20,12 +23,16 @@ CONF_ADDON_S0_LEGACY_KEY = "s0_legacy_key" CONF_ADDON_S2_ACCESS_CONTROL_KEY = "s2_access_control_key" CONF_ADDON_S2_AUTHENTICATED_KEY = "s2_authenticated_key" CONF_ADDON_S2_UNAUTHENTICATED_KEY = "s2_unauthenticated_key" +CONF_ADDON_LR_S2_ACCESS_CONTROL_KEY = "lr_s2_access_control_key" +CONF_ADDON_LR_S2_AUTHENTICATED_KEY = "lr_s2_authenticated_key" CONF_INTEGRATION_CREATED_ADDON = "integration_created_addon" CONF_NETWORK_KEY = "network_key" CONF_S0_LEGACY_KEY = "s0_legacy_key" CONF_S2_ACCESS_CONTROL_KEY = "s2_access_control_key" CONF_S2_AUTHENTICATED_KEY = "s2_authenticated_key" CONF_S2_UNAUTHENTICATED_KEY = "s2_unauthenticated_key" +CONF_LR_S2_ACCESS_CONTROL_KEY = "lr_s2_access_control_key" +CONF_LR_S2_AUTHENTICATED_KEY = "lr_s2_authenticated_key" CONF_USB_PATH = "usb_path" CONF_USE_ADDON = "use_addon" CONF_DATA_COLLECTION_OPTED_IN = "data_collection_opted_in" diff --git a/tests/components/zwave_js/test_config_flow.py b/tests/components/zwave_js/test_config_flow.py index 3fa59b22305..10fd5edfabb 100644 --- a/tests/components/zwave_js/test_config_flow.py +++ b/tests/components/zwave_js/test_config_flow.py @@ -222,6 +222,8 @@ async def test_manual(hass: HomeAssistant) -> None: "s2_access_control_key": None, "s2_authenticated_key": None, "s2_unauthenticated_key": None, + "lr_s2_access_control_key": None, + "lr_s2_authenticated_key": None, "use_addon": False, "integration_created_addon": False, } @@ -343,6 +345,8 @@ async def test_supervisor_discovery( addon_options["s2_access_control_key"] = "new456" addon_options["s2_authenticated_key"] = "new789" addon_options["s2_unauthenticated_key"] = "new987" + addon_options["lr_s2_access_control_key"] = "new654" + addon_options["lr_s2_authenticated_key"] = "new321" result = await hass.config_entries.flow.async_init( DOMAIN, @@ -376,6 +380,8 @@ async def test_supervisor_discovery( "s2_access_control_key": "new456", "s2_authenticated_key": "new789", "s2_unauthenticated_key": "new987", + "lr_s2_access_control_key": "new654", + "lr_s2_authenticated_key": "new321", "use_addon": True, "integration_created_addon": False, } @@ -422,6 +428,8 @@ async def test_clean_discovery_on_user_create( addon_options["s2_access_control_key"] = "new456" addon_options["s2_authenticated_key"] = "new789" addon_options["s2_unauthenticated_key"] = "new987" + addon_options["lr_s2_access_control_key"] = "new654" + addon_options["lr_s2_authenticated_key"] = "new321" result = await hass.config_entries.flow.async_init( DOMAIN, @@ -477,6 +485,8 @@ async def test_clean_discovery_on_user_create( "s2_access_control_key": None, "s2_authenticated_key": None, "s2_unauthenticated_key": None, + "lr_s2_access_control_key": None, + "lr_s2_authenticated_key": None, "use_addon": False, "integration_created_addon": False, } @@ -606,6 +616,8 @@ async def test_usb_discovery( "s2_access_control_key": "new456", "s2_authenticated_key": "new789", "s2_unauthenticated_key": "new987", + "lr_s2_access_control_key": "new654", + "lr_s2_authenticated_key": "new321", }, ) @@ -619,6 +631,8 @@ async def test_usb_discovery( "s2_access_control_key": "new456", "s2_authenticated_key": "new789", "s2_unauthenticated_key": "new987", + "lr_s2_access_control_key": "new654", + "lr_s2_authenticated_key": "new321", } }, ) @@ -650,6 +664,8 @@ async def test_usb_discovery( "s2_access_control_key": "new456", "s2_authenticated_key": "new789", "s2_unauthenticated_key": "new987", + "lr_s2_access_control_key": "new654", + "lr_s2_authenticated_key": "new321", "use_addon": True, "integration_created_addon": True, } @@ -690,6 +706,8 @@ async def test_usb_discovery_addon_not_running( "s2_access_control_key": "", "s2_authenticated_key": "", "s2_unauthenticated_key": "", + "lr_s2_access_control_key": "", + "lr_s2_authenticated_key": "", } result = await hass.config_entries.flow.async_configure( @@ -699,6 +717,8 @@ async def test_usb_discovery_addon_not_running( "s2_access_control_key": "new456", "s2_authenticated_key": "new789", "s2_unauthenticated_key": "new987", + "lr_s2_access_control_key": "new654", + "lr_s2_authenticated_key": "new321", }, ) @@ -712,6 +732,8 @@ async def test_usb_discovery_addon_not_running( "s2_access_control_key": "new456", "s2_authenticated_key": "new789", "s2_unauthenticated_key": "new987", + "lr_s2_access_control_key": "new654", + "lr_s2_authenticated_key": "new321", } }, ) @@ -743,6 +765,8 @@ async def test_usb_discovery_addon_not_running( "s2_access_control_key": "new456", "s2_authenticated_key": "new789", "s2_unauthenticated_key": "new987", + "lr_s2_access_control_key": "new654", + "lr_s2_authenticated_key": "new321", "use_addon": True, "integration_created_addon": False, } @@ -788,6 +812,8 @@ async def test_discovery_addon_not_running( "s2_access_control_key": "new456", "s2_authenticated_key": "new789", "s2_unauthenticated_key": "new987", + "lr_s2_access_control_key": "new654", + "lr_s2_authenticated_key": "new321", }, ) @@ -801,6 +827,8 @@ async def test_discovery_addon_not_running( "s2_access_control_key": "new456", "s2_authenticated_key": "new789", "s2_unauthenticated_key": "new987", + "lr_s2_access_control_key": "new654", + "lr_s2_authenticated_key": "new321", } }, ) @@ -832,6 +860,8 @@ async def test_discovery_addon_not_running( "s2_access_control_key": "new456", "s2_authenticated_key": "new789", "s2_unauthenticated_key": "new987", + "lr_s2_access_control_key": "new654", + "lr_s2_authenticated_key": "new321", "use_addon": True, "integration_created_addon": False, } @@ -885,6 +915,8 @@ async def test_discovery_addon_not_installed( "s2_access_control_key": "new456", "s2_authenticated_key": "new789", "s2_unauthenticated_key": "new987", + "lr_s2_access_control_key": "new654", + "lr_s2_authenticated_key": "new321", }, ) @@ -898,6 +930,8 @@ async def test_discovery_addon_not_installed( "s2_access_control_key": "new456", "s2_authenticated_key": "new789", "s2_unauthenticated_key": "new987", + "lr_s2_access_control_key": "new654", + "lr_s2_authenticated_key": "new321", } }, ) @@ -929,6 +963,8 @@ async def test_discovery_addon_not_installed( "s2_access_control_key": "new456", "s2_authenticated_key": "new789", "s2_unauthenticated_key": "new987", + "lr_s2_access_control_key": "new654", + "lr_s2_authenticated_key": "new321", "use_addon": True, "integration_created_addon": True, } @@ -1068,6 +1104,8 @@ async def test_not_addon(hass: HomeAssistant, supervisor) -> None: "s2_access_control_key": None, "s2_authenticated_key": None, "s2_unauthenticated_key": None, + "lr_s2_access_control_key": None, + "lr_s2_authenticated_key": None, "use_addon": False, "integration_created_addon": False, } @@ -1089,6 +1127,8 @@ async def test_addon_running( addon_options["s2_access_control_key"] = "new456" addon_options["s2_authenticated_key"] = "new789" addon_options["s2_unauthenticated_key"] = "new987" + addon_options["lr_s2_access_control_key"] = "new654" + addon_options["lr_s2_authenticated_key"] = "new321" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -1120,6 +1160,8 @@ async def test_addon_running( "s2_access_control_key": "new456", "s2_authenticated_key": "new789", "s2_unauthenticated_key": "new987", + "lr_s2_access_control_key": "new654", + "lr_s2_authenticated_key": "new321", "use_addon": True, "integration_created_addon": False, } @@ -1207,6 +1249,9 @@ async def test_addon_running_already_configured( addon_options["s2_access_control_key"] = "new456" addon_options["s2_authenticated_key"] = "new789" addon_options["s2_unauthenticated_key"] = "new987" + addon_options["lr_s2_access_control_key"] = "new654" + addon_options["lr_s2_authenticated_key"] = "new321" + entry = MockConfigEntry( domain=DOMAIN, data={ @@ -1217,6 +1262,8 @@ async def test_addon_running_already_configured( "s2_access_control_key": "old456", "s2_authenticated_key": "old789", "s2_unauthenticated_key": "old987", + "lr_s2_access_control_key": "old654", + "lr_s2_authenticated_key": "old321", }, title=TITLE, unique_id=1234, # Unique ID is purposely set to int to test migration logic @@ -1243,6 +1290,8 @@ async def test_addon_running_already_configured( assert entry.data["s2_access_control_key"] == "new456" assert entry.data["s2_authenticated_key"] == "new789" assert entry.data["s2_unauthenticated_key"] == "new987" + assert entry.data["lr_s2_access_control_key"] == "new654" + assert entry.data["lr_s2_authenticated_key"] == "new321" @pytest.mark.parametrize("discovery_info", [{"config": ADDON_DISCOVERY_INFO}]) @@ -1279,6 +1328,8 @@ async def test_addon_installed( "s2_access_control_key": "new456", "s2_authenticated_key": "new789", "s2_unauthenticated_key": "new987", + "lr_s2_access_control_key": "new654", + "lr_s2_authenticated_key": "new321", }, ) @@ -1292,6 +1343,8 @@ async def test_addon_installed( "s2_access_control_key": "new456", "s2_authenticated_key": "new789", "s2_unauthenticated_key": "new987", + "lr_s2_access_control_key": "new654", + "lr_s2_authenticated_key": "new321", } }, ) @@ -1323,6 +1376,8 @@ async def test_addon_installed( "s2_access_control_key": "new456", "s2_authenticated_key": "new789", "s2_unauthenticated_key": "new987", + "lr_s2_access_control_key": "new654", + "lr_s2_authenticated_key": "new321", "use_addon": True, "integration_created_addon": False, } @@ -1367,6 +1422,8 @@ async def test_addon_installed_start_failure( "s2_access_control_key": "new456", "s2_authenticated_key": "new789", "s2_unauthenticated_key": "new987", + "lr_s2_access_control_key": "new654", + "lr_s2_authenticated_key": "new321", }, ) @@ -1380,6 +1437,8 @@ async def test_addon_installed_start_failure( "s2_access_control_key": "new456", "s2_authenticated_key": "new789", "s2_unauthenticated_key": "new987", + "lr_s2_access_control_key": "new654", + "lr_s2_authenticated_key": "new321", } }, ) @@ -1442,6 +1501,8 @@ async def test_addon_installed_failures( "s2_access_control_key": "new456", "s2_authenticated_key": "new789", "s2_unauthenticated_key": "new987", + "lr_s2_access_control_key": "new654", + "lr_s2_authenticated_key": "new321", }, ) @@ -1455,6 +1516,8 @@ async def test_addon_installed_failures( "s2_access_control_key": "new456", "s2_authenticated_key": "new789", "s2_unauthenticated_key": "new987", + "lr_s2_access_control_key": "new654", + "lr_s2_authenticated_key": "new321", } }, ) @@ -1508,6 +1571,8 @@ async def test_addon_installed_set_options_failure( "s2_access_control_key": "new456", "s2_authenticated_key": "new789", "s2_unauthenticated_key": "new987", + "lr_s2_access_control_key": "new654", + "lr_s2_authenticated_key": "new321", }, ) @@ -1521,6 +1586,8 @@ async def test_addon_installed_set_options_failure( "s2_access_control_key": "new456", "s2_authenticated_key": "new789", "s2_unauthenticated_key": "new987", + "lr_s2_access_control_key": "new654", + "lr_s2_authenticated_key": "new321", } }, ) @@ -1552,6 +1619,8 @@ async def test_addon_installed_already_configured( "s2_access_control_key": "old456", "s2_authenticated_key": "old789", "s2_unauthenticated_key": "old987", + "lr_s2_access_control_key": "old654", + "lr_s2_authenticated_key": "old321", }, title=TITLE, unique_id="1234", @@ -1580,6 +1649,8 @@ async def test_addon_installed_already_configured( "s2_access_control_key": "new456", "s2_authenticated_key": "new789", "s2_unauthenticated_key": "new987", + "lr_s2_access_control_key": "new654", + "lr_s2_authenticated_key": "new321", }, ) @@ -1593,6 +1664,8 @@ async def test_addon_installed_already_configured( "s2_access_control_key": "new456", "s2_authenticated_key": "new789", "s2_unauthenticated_key": "new987", + "lr_s2_access_control_key": "new654", + "lr_s2_authenticated_key": "new321", } }, ) @@ -1613,6 +1686,8 @@ async def test_addon_installed_already_configured( assert entry.data["s2_access_control_key"] == "new456" assert entry.data["s2_authenticated_key"] == "new789" assert entry.data["s2_unauthenticated_key"] == "new987" + assert entry.data["lr_s2_access_control_key"] == "new654" + assert entry.data["lr_s2_authenticated_key"] == "new321" @pytest.mark.parametrize("discovery_info", [{"config": ADDON_DISCOVERY_INFO}]) @@ -1659,6 +1734,8 @@ async def test_addon_not_installed( "s2_access_control_key": "new456", "s2_authenticated_key": "new789", "s2_unauthenticated_key": "new987", + "lr_s2_access_control_key": "new654", + "lr_s2_authenticated_key": "new321", }, ) @@ -1672,6 +1749,8 @@ async def test_addon_not_installed( "s2_access_control_key": "new456", "s2_authenticated_key": "new789", "s2_unauthenticated_key": "new987", + "lr_s2_access_control_key": "new654", + "lr_s2_authenticated_key": "new321", } }, ) @@ -1703,6 +1782,8 @@ async def test_addon_not_installed( "s2_access_control_key": "new456", "s2_authenticated_key": "new789", "s2_unauthenticated_key": "new987", + "lr_s2_access_control_key": "new654", + "lr_s2_authenticated_key": "new321", "use_addon": True, "integration_created_addon": True, } @@ -1844,6 +1925,8 @@ async def test_options_not_addon( "s2_access_control_key": "old456", "s2_authenticated_key": "old789", "s2_unauthenticated_key": "old987", + "lr_s2_access_control_key": "old654", + "lr_s2_authenticated_key": "old321", }, { "usb_path": "/new", @@ -1851,6 +1934,8 @@ async def test_options_not_addon( "s2_access_control_key": "new456", "s2_authenticated_key": "new789", "s2_unauthenticated_key": "new987", + "lr_s2_access_control_key": "new654", + "lr_s2_authenticated_key": "new321", "log_level": "info", "emulate_hardware": False, }, @@ -1866,6 +1951,8 @@ async def test_options_not_addon( "s2_access_control_key": "old456", "s2_authenticated_key": "old789", "s2_unauthenticated_key": "old987", + "lr_s2_access_control_key": "old654", + "lr_s2_authenticated_key": "old321", }, { "usb_path": "/new", @@ -1873,6 +1960,8 @@ async def test_options_not_addon( "s2_access_control_key": "new456", "s2_authenticated_key": "new789", "s2_unauthenticated_key": "new987", + "lr_s2_access_control_key": "new654", + "lr_s2_authenticated_key": "new321", "log_level": "info", "emulate_hardware": False, }, @@ -1956,6 +2045,14 @@ async def test_options_addon_running( entry.data["s2_unauthenticated_key"] == new_addon_options["s2_unauthenticated_key"] ) + assert ( + entry.data["lr_s2_access_control_key"] + == new_addon_options["lr_s2_access_control_key"] + ) + assert ( + entry.data["lr_s2_authenticated_key"] + == new_addon_options["lr_s2_authenticated_key"] + ) assert entry.data["use_addon"] is True assert entry.data["integration_created_addon"] is False assert client.connect.call_count == 2 @@ -1975,6 +2072,8 @@ async def test_options_addon_running( "s2_access_control_key": "old456", "s2_authenticated_key": "old789", "s2_unauthenticated_key": "old987", + "lr_s2_access_control_key": "old654", + "lr_s2_authenticated_key": "old321", "log_level": "info", "emulate_hardware": False, }, @@ -1984,6 +2083,8 @@ async def test_options_addon_running( "s2_access_control_key": "old456", "s2_authenticated_key": "old789", "s2_unauthenticated_key": "old987", + "lr_s2_access_control_key": "old654", + "lr_s2_authenticated_key": "old321", "log_level": "info", "emulate_hardware": False, }, @@ -2053,6 +2154,14 @@ async def test_options_addon_running_no_changes( entry.data["s2_unauthenticated_key"] == new_addon_options["s2_unauthenticated_key"] ) + assert ( + entry.data["lr_s2_access_control_key"] + == new_addon_options["lr_s2_access_control_key"] + ) + assert ( + entry.data["lr_s2_authenticated_key"] + == new_addon_options["lr_s2_authenticated_key"] + ) assert entry.data["use_addon"] is True assert entry.data["integration_created_addon"] is False assert client.connect.call_count == 2 @@ -2090,6 +2199,8 @@ async def different_device_server_version(*args): "s2_access_control_key": "old456", "s2_authenticated_key": "old789", "s2_unauthenticated_key": "old987", + "lr_s2_access_control_key": "old654", + "lr_s2_authenticated_key": "old321", "log_level": "info", "emulate_hardware": False, }, @@ -2099,6 +2210,8 @@ async def different_device_server_version(*args): "s2_access_control_key": "new456", "s2_authenticated_key": "new789", "s2_unauthenticated_key": "new987", + "lr_s2_access_control_key": "new654", + "lr_s2_authenticated_key": "new321", "log_level": "info", "emulate_hardware": False, }, @@ -2115,6 +2228,8 @@ async def different_device_server_version(*args): "s2_access_control_key": "old456", "s2_authenticated_key": "old789", "s2_unauthenticated_key": "old987", + "lr_s2_access_control_key": "old654", + "lr_s2_authenticated_key": "old321", "log_level": "info", }, { @@ -2123,6 +2238,8 @@ async def different_device_server_version(*args): "s2_access_control_key": "new456", "s2_authenticated_key": "new789", "s2_unauthenticated_key": "new987", + "lr_s2_access_control_key": "new654", + "lr_s2_authenticated_key": "new321", "log_level": "info", "emulate_hardware": False, }, @@ -2244,6 +2361,8 @@ async def test_options_different_device( "s2_access_control_key": "old456", "s2_authenticated_key": "old789", "s2_unauthenticated_key": "old987", + "lr_s2_access_control_key": "old654", + "lr_s2_authenticated_key": "old321", "log_level": "info", "emulate_hardware": False, }, @@ -2253,6 +2372,8 @@ async def test_options_different_device( "s2_access_control_key": "new456", "s2_authenticated_key": "new789", "s2_unauthenticated_key": "new987", + "lr_s2_access_control_key": "new654", + "lr_s2_authenticated_key": "new321", "log_level": "info", "emulate_hardware": False, }, @@ -2269,6 +2390,8 @@ async def test_options_different_device( "s2_access_control_key": "old456", "s2_authenticated_key": "old789", "s2_unauthenticated_key": "old987", + "lr_s2_access_control_key": "old654", + "lr_s2_authenticated_key": "old321", "log_level": "info", "emulate_hardware": False, }, @@ -2278,6 +2401,8 @@ async def test_options_different_device( "s2_access_control_key": "new456", "s2_authenticated_key": "new789", "s2_unauthenticated_key": "new987", + "lr_s2_access_control_key": "new654", + "lr_s2_authenticated_key": "new321", "log_level": "info", "emulate_hardware": False, }, @@ -2399,6 +2524,8 @@ async def test_options_addon_restart_failed( "s2_access_control_key": "old456", "s2_authenticated_key": "old789", "s2_unauthenticated_key": "old987", + "lr_s2_access_control_key": "old654", + "lr_s2_authenticated_key": "old321", "log_level": "info", "emulate_hardware": False, }, @@ -2408,6 +2535,8 @@ async def test_options_addon_restart_failed( "s2_access_control_key": "old456", "s2_authenticated_key": "old789", "s2_unauthenticated_key": "old987", + "lr_s2_access_control_key": "old654", + "lr_s2_authenticated_key": "old321", "log_level": "info", "emulate_hardware": False, }, @@ -2488,6 +2617,8 @@ async def test_options_addon_running_server_info_failure( "s2_access_control_key": "old456", "s2_authenticated_key": "old789", "s2_unauthenticated_key": "old987", + "lr_s2_access_control_key": "old654", + "lr_s2_authenticated_key": "old321", }, { "usb_path": "/new", @@ -2495,6 +2626,8 @@ async def test_options_addon_running_server_info_failure( "s2_access_control_key": "new456", "s2_authenticated_key": "new789", "s2_unauthenticated_key": "new987", + "lr_s2_access_control_key": "new654", + "lr_s2_authenticated_key": "new321", "log_level": "info", "emulate_hardware": False, }, @@ -2510,6 +2643,8 @@ async def test_options_addon_running_server_info_failure( "s2_access_control_key": "old456", "s2_authenticated_key": "old789", "s2_unauthenticated_key": "old987", + "lr_s2_access_control_key": "old654", + "lr_s2_authenticated_key": "old321", }, { "usb_path": "/new", @@ -2517,6 +2652,8 @@ async def test_options_addon_running_server_info_failure( "s2_access_control_key": "new456", "s2_authenticated_key": "new789", "s2_unauthenticated_key": "new987", + "lr_s2_access_control_key": "new654", + "lr_s2_authenticated_key": "new321", "log_level": "info", "emulate_hardware": False, }, @@ -2647,6 +2784,8 @@ async def test_import_addon_installed( "s2_access_control_key": "", "s2_authenticated_key": "", "s2_unauthenticated_key": "", + "lr_s2_access_control_key": "", + "lr_s2_authenticated_key": "", } result = await hass.config_entries.flow.async_configure( @@ -2663,6 +2802,8 @@ async def test_import_addon_installed( "s2_access_control_key": "", "s2_authenticated_key": "", "s2_unauthenticated_key": "", + "lr_s2_access_control_key": "", + "lr_s2_authenticated_key": "", } }, ) @@ -2694,6 +2835,8 @@ async def test_import_addon_installed( "s2_access_control_key": "", "s2_authenticated_key": "", "s2_unauthenticated_key": "", + "lr_s2_access_control_key": "", + "lr_s2_authenticated_key": "", "use_addon": True, "integration_created_addon": False, } @@ -2742,6 +2885,8 @@ async def test_zeroconf(hass: HomeAssistant) -> None: "s2_access_control_key": None, "s2_authenticated_key": None, "s2_unauthenticated_key": None, + "lr_s2_access_control_key": None, + "lr_s2_authenticated_key": None, "use_addon": False, "integration_created_addon": False, } diff --git a/tests/components/zwave_js/test_init.py b/tests/components/zwave_js/test_init.py index d26cc438d04..8c9c05a124e 100644 --- a/tests/components/zwave_js/test_init.py +++ b/tests/components/zwave_js/test_init.py @@ -519,12 +519,16 @@ async def test_start_addon( s2_access_control_key = "s2_access_control" s2_authenticated_key = "s2_authenticated" s2_unauthenticated_key = "s2_unauthenticated" + lr_s2_access_control_key = "lr_s2_access_control" + lr_s2_authenticated_key = "lr_s2_authenticated" addon_options = { "device": device, "s0_legacy_key": s0_legacy_key, "s2_access_control_key": s2_access_control_key, "s2_authenticated_key": s2_authenticated_key, "s2_unauthenticated_key": s2_unauthenticated_key, + "lr_s2_access_control_key": lr_s2_access_control_key, + "lr_s2_authenticated_key": lr_s2_authenticated_key, } entry = MockConfigEntry( domain=DOMAIN, @@ -536,6 +540,8 @@ async def test_start_addon( "s2_access_control_key": s2_access_control_key, "s2_authenticated_key": s2_authenticated_key, "s2_unauthenticated_key": s2_unauthenticated_key, + "lr_s2_access_control_key": lr_s2_access_control_key, + "lr_s2_authenticated_key": lr_s2_authenticated_key, }, ) entry.add_to_hass(hass) @@ -641,6 +647,10 @@ async def test_addon_info_failure( "new_s2_authenticated_key", "old_s2_unauthenticated_key", "new_s2_unauthenticated_key", + "old_lr_s2_access_control_key", + "new_lr_s2_access_control_key", + "old_lr_s2_authenticated_key", + "new_lr_s2_authenticated_key", ), [ ( @@ -654,6 +664,10 @@ async def test_addon_info_failure( "new789", "old987", "new987", + "old654", + "new654", + "old321", + "new321", ) ], ) @@ -675,6 +689,10 @@ async def test_addon_options_changed( new_s2_authenticated_key, old_s2_unauthenticated_key, new_s2_unauthenticated_key, + old_lr_s2_access_control_key, + new_lr_s2_access_control_key, + old_lr_s2_authenticated_key, + new_lr_s2_authenticated_key, ) -> None: """Test update config entry data on entry setup if add-on options changed.""" addon_options["device"] = new_device @@ -682,6 +700,8 @@ async def test_addon_options_changed( addon_options["s2_access_control_key"] = new_s2_access_control_key addon_options["s2_authenticated_key"] = new_s2_authenticated_key addon_options["s2_unauthenticated_key"] = new_s2_unauthenticated_key + addon_options["lr_s2_access_control_key"] = new_lr_s2_access_control_key + addon_options["lr_s2_authenticated_key"] = new_lr_s2_authenticated_key entry = MockConfigEntry( domain=DOMAIN, title="Z-Wave JS", @@ -693,6 +713,8 @@ async def test_addon_options_changed( "s2_access_control_key": old_s2_access_control_key, "s2_authenticated_key": old_s2_authenticated_key, "s2_unauthenticated_key": old_s2_unauthenticated_key, + "lr_s2_access_control_key": old_lr_s2_access_control_key, + "lr_s2_authenticated_key": old_lr_s2_authenticated_key, }, ) entry.add_to_hass(hass) @@ -706,6 +728,8 @@ async def test_addon_options_changed( assert entry.data["s2_access_control_key"] == new_s2_access_control_key assert entry.data["s2_authenticated_key"] == new_s2_authenticated_key assert entry.data["s2_unauthenticated_key"] == new_s2_unauthenticated_key + assert entry.data["lr_s2_access_control_key"] == new_lr_s2_access_control_key + assert entry.data["lr_s2_authenticated_key"] == new_lr_s2_authenticated_key assert install_addon.call_count == 0 assert start_addon.call_count == 0 From a64d6e548c831e62bf2c8e4b36df48d9f1251ab1 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 8 Jun 2024 17:52:23 +0200 Subject: [PATCH 0387/1445] Update Home Assistant base image to 2024.06.0 (#119147) --- build.yaml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/build.yaml b/build.yaml index 044358b1f9d..7607998bacd 100644 --- a/build.yaml +++ b/build.yaml @@ -1,10 +1,10 @@ image: ghcr.io/home-assistant/{arch}-homeassistant build_from: - aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2024.03.0 - armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2024.03.0 - armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2024.03.0 - amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2024.03.0 - i386: ghcr.io/home-assistant/i386-homeassistant-base:2024.03.0 + aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2024.06.0 + armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2024.06.0 + armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2024.06.0 + amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2024.06.0 + i386: ghcr.io/home-assistant/i386-homeassistant-base:2024.06.0 codenotary: signer: notary@home-assistant.io base_image: notary@home-assistant.io From fff2c1115dbbde017da10b60030149384b340135 Mon Sep 17 00:00:00 2001 From: Tom Brien Date: Sat, 8 Jun 2024 16:53:20 +0100 Subject: [PATCH 0388/1445] Fix workday timezone (#119148) --- homeassistant/components/workday/binary_sensor.py | 2 +- tests/components/workday/test_binary_sensor.py | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/workday/binary_sensor.py b/homeassistant/components/workday/binary_sensor.py index 205f500746e..5df8e6c3d75 100644 --- a/homeassistant/components/workday/binary_sensor.py +++ b/homeassistant/components/workday/binary_sensor.py @@ -269,7 +269,7 @@ class IsWorkdaySensor(BinarySensorEntity): def _update_state_and_setup_listener(self) -> None: """Update state and setup listener for next interval.""" - now = dt_util.utcnow() + now = dt_util.now() self.update_data(now) self.unsub = async_track_point_in_utc_time( self.hass, self.point_in_time_listener, self.get_next_interval(now) diff --git a/tests/components/workday/test_binary_sensor.py b/tests/components/workday/test_binary_sensor.py index e9f0e8023bc..9aa4dd6b5b4 100644 --- a/tests/components/workday/test_binary_sensor.py +++ b/tests/components/workday/test_binary_sensor.py @@ -1,6 +1,6 @@ """Tests the Home Assistant workday binary sensor.""" -from datetime import date, datetime, timedelta +from datetime import date, datetime, timedelta, timezone from typing import Any from freezegun.api import FrozenDateTimeFactory @@ -68,7 +68,9 @@ async def test_setup( freezer: FrozenDateTimeFactory, ) -> None: """Test setup from various configs.""" - freezer.move_to(datetime(2022, 4, 15, 12, tzinfo=UTC)) # Friday + # Start on a Friday + await hass.config.async_set_time_zone("Europe/Paris") + freezer.move_to(datetime(2022, 4, 15, 0, tzinfo=timezone(timedelta(hours=1)))) await init_integration(hass, config) state = hass.states.get("binary_sensor.workday_sensor") From c49ca5ed56818a91594295d6e44b7edb1ef24665 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 8 Jun 2024 11:53:47 -0400 Subject: [PATCH 0389/1445] Ensure intent tools have safe names (#119144) --- homeassistant/helpers/llm.py | 13 +++++++++++-- tests/helpers/test_llm.py | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/homeassistant/helpers/llm.py b/homeassistant/helpers/llm.py index 3c240692d52..903e52af1a2 100644 --- a/homeassistant/helpers/llm.py +++ b/homeassistant/helpers/llm.py @@ -5,8 +5,10 @@ from __future__ import annotations from abc import ABC, abstractmethod from dataclasses import dataclass from enum import Enum +from functools import cache, partial from typing import Any +import slugify as unicode_slug import voluptuous as vol from homeassistant.components.climate.intent import INTENT_GET_TEMPERATURE @@ -175,10 +177,11 @@ class IntentTool(Tool): def __init__( self, + name: str, intent_handler: intent.IntentHandler, ) -> None: """Init the class.""" - self.name = intent_handler.intent_type + self.name = name self.description = ( intent_handler.description or f"Execute Home Assistant {self.name} intent" ) @@ -261,6 +264,9 @@ class AssistAPI(API): id=LLM_API_ASSIST, name="Assist", ) + self.cached_slugify = cache( + partial(unicode_slug.slugify, separator="_", lowercase=False) + ) async def async_get_api_instance(self, llm_context: LLMContext) -> APIInstance: """Return the instance of the API.""" @@ -373,7 +379,10 @@ class AssistAPI(API): or intent_handler.platforms & exposed_domains ] - return [IntentTool(intent_handler) for intent_handler in intent_handlers] + return [ + IntentTool(self.cached_slugify(intent_handler.intent_type), intent_handler) + for intent_handler in intent_handlers + ] def _get_exposed_entities( diff --git a/tests/helpers/test_llm.py b/tests/helpers/test_llm.py index 3f61ed8a0ed..6ac17a2fe0e 100644 --- a/tests/helpers/test_llm.py +++ b/tests/helpers/test_llm.py @@ -249,6 +249,39 @@ async def test_assist_api_get_timer_tools( assert "HassStartTimer" in [tool.name for tool in api.tools] +async def test_assist_api_tools( + hass: HomeAssistant, llm_context: llm.LLMContext +) -> None: + """Test getting timer tools with Assist API.""" + assert await async_setup_component(hass, "homeassistant", {}) + assert await async_setup_component(hass, "intent", {}) + + llm_context.device_id = "test_device" + + async_register_timer_handler(hass, "test_device", lambda *args: None) + + class MyIntentHandler(intent.IntentHandler): + intent_type = "Super crazy intent with unique nåme" + description = "my intent handler" + + intent.async_register(hass, MyIntentHandler()) + + api = await llm.async_get_api(hass, "assist", llm_context) + assert [tool.name for tool in api.tools] == [ + "HassTurnOn", + "HassTurnOff", + "HassSetPosition", + "HassStartTimer", + "HassCancelTimer", + "HassIncreaseTimer", + "HassDecreaseTimer", + "HassPauseTimer", + "HassUnpauseTimer", + "HassTimerStatus", + "Super_crazy_intent_with_unique_name", + ] + + async def test_assist_api_description( hass: HomeAssistant, llm_context: llm.LLMContext ) -> None: From 915658daa1d33fa6d416210dd4cb03df85b8fb48 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sat, 8 Jun 2024 17:58:47 +0200 Subject: [PATCH 0390/1445] Fix failing UniFi tests related to utcnow (#119131) * test * Fix missed test --- tests/components/unifi/test_device_tracker.py | 125 ++++++++---------- tests/components/unifi/test_hub.py | 29 ++-- tests/components/unifi/test_sensor.py | 53 ++++---- 3 files changed, 92 insertions(+), 115 deletions(-) diff --git a/tests/components/unifi/test_device_tracker.py b/tests/components/unifi/test_device_tracker.py index 1bc4c4ff632..0a3aaff581d 100644 --- a/tests/components/unifi/test_device_tracker.py +++ b/tests/components/unifi/test_device_tracker.py @@ -704,32 +704,11 @@ async def test_option_track_devices( assert hass.states.get("device_tracker.device") -@pytest.mark.parametrize( - "client_payload", - [ - [ - { - "essid": "ssid", - "hostname": "client", - "is_wired": False, - "last_seen": dt_util.as_timestamp(dt_util.utcnow()), - "mac": "00:00:00:00:00:01", - }, - { - "essid": "ssid2", - "hostname": "client_on_ssid2", - "is_wired": False, - "last_seen": 1562600145, - "mac": "00:00:00:00:00:02", - }, - ] - ], -) @pytest.mark.usefixtures("mock_device_registry") async def test_option_ssid_filter( hass: HomeAssistant, mock_unifi_websocket, - config_entry_setup: ConfigEntry, + config_entry_factory: Callable[[], ConfigEntry], client_payload: list[dict[str, Any]], ) -> None: """Test the SSID filter works. @@ -737,13 +716,31 @@ async def test_option_ssid_filter( Client will travel from a supported SSID to an unsupported ssid. Client on SSID2 will be removed on change of options. """ + client_payload += [ + { + "essid": "ssid", + "hostname": "client", + "is_wired": False, + "last_seen": dt_util.as_timestamp(dt_util.utcnow()), + "mac": "00:00:00:00:00:01", + }, + { + "essid": "ssid2", + "hostname": "client_on_ssid2", + "is_wired": False, + "last_seen": 1562600145, + "mac": "00:00:00:00:00:02", + }, + ] + config_entry = await config_entry_factory() + assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 2 assert hass.states.get("device_tracker.client").state == STATE_HOME assert hass.states.get("device_tracker.client_on_ssid2").state == STATE_NOT_HOME # Setting SSID filter will remove clients outside of filter hass.config_entries.async_update_entry( - config_entry_setup, options={CONF_SSID_FILTER: ["ssid"]} + config_entry, options={CONF_SSID_FILTER: ["ssid"]} ) await hass.async_block_till_done() @@ -766,8 +763,7 @@ async def test_option_ssid_filter( new_time = dt_util.utcnow() + timedelta( seconds=( - config_entry_setup.options.get(CONF_DETECTION_TIME, DEFAULT_DETECTION_TIME) - + 1 + config_entry.options.get(CONF_DETECTION_TIME, DEFAULT_DETECTION_TIME) + 1 ) ) with freeze_time(new_time): @@ -781,9 +777,7 @@ async def test_option_ssid_filter( assert not hass.states.get("device_tracker.client_on_ssid2") # Remove SSID filter - hass.config_entries.async_update_entry( - config_entry_setup, options={CONF_SSID_FILTER: []} - ) + hass.config_entries.async_update_entry(config_entry, options={CONF_SSID_FILTER: []}) await hass.async_block_till_done() client["last_seen"] += 1 @@ -797,8 +791,7 @@ async def test_option_ssid_filter( # Time pass to mark client as away new_time += timedelta( seconds=( - config_entry_setup.options.get(CONF_DETECTION_TIME, DEFAULT_DETECTION_TIME) - + 1 + config_entry.options.get(CONF_DETECTION_TIME, DEFAULT_DETECTION_TIME) + 1 ) ) with freeze_time(new_time): @@ -820,9 +813,7 @@ async def test_option_ssid_filter( await hass.async_block_till_done() new_time += timedelta( - seconds=( - config_entry_setup.options.get(CONF_DETECTION_TIME, DEFAULT_DETECTION_TIME) - ) + seconds=(config_entry.options.get(CONF_DETECTION_TIME, DEFAULT_DETECTION_TIME)) ) with freeze_time(new_time): async_fire_time_changed(hass, new_time) @@ -831,32 +822,29 @@ async def test_option_ssid_filter( assert hass.states.get("device_tracker.client_on_ssid2").state == STATE_NOT_HOME -@pytest.mark.parametrize( - "client_payload", - [ - [ - { - "essid": "ssid", - "hostname": "client", - "ip": "10.0.0.1", - "is_wired": False, - "last_seen": dt_util.as_timestamp(dt_util.utcnow()), - "mac": "00:00:00:00:00:01", - } - ] - ], -) @pytest.mark.usefixtures("mock_device_registry") async def test_wireless_client_go_wired_issue( hass: HomeAssistant, mock_unifi_websocket, - config_entry_setup: ConfigEntry, + config_entry_factory: Callable[[], ConfigEntry], client_payload: list[dict[str, Any]], ) -> None: """Test the solution to catch wireless device go wired UniFi issue. UniFi Network has a known issue that when a wireless device goes away it sometimes gets marked as wired. """ + client_payload.append( + { + "essid": "ssid", + "hostname": "client", + "ip": "10.0.0.1", + "is_wired": False, + "last_seen": dt_util.as_timestamp(dt_util.utcnow()), + "mac": "00:00:00:00:00:01", + } + ) + config_entry = await config_entry_factory() + assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 1 # Client is wireless @@ -876,9 +864,7 @@ async def test_wireless_client_go_wired_issue( # Pass time new_time = dt_util.utcnow() + timedelta( - seconds=( - config_entry_setup.options.get(CONF_DETECTION_TIME, DEFAULT_DETECTION_TIME) - ) + seconds=(config_entry.options.get(CONF_DETECTION_TIME, DEFAULT_DETECTION_TIME)) ) with freeze_time(new_time): async_fire_time_changed(hass, new_time) @@ -909,30 +895,27 @@ async def test_wireless_client_go_wired_issue( @pytest.mark.parametrize("config_entry_options", [{CONF_IGNORE_WIRED_BUG: True}]) -@pytest.mark.parametrize( - "client_payload", - [ - [ - { - "ap_mac": "00:00:00:00:02:01", - "essid": "ssid", - "hostname": "client", - "ip": "10.0.0.1", - "is_wired": False, - "last_seen": dt_util.as_timestamp(dt_util.utcnow()), - "mac": "00:00:00:00:00:01", - } - ] - ], -) @pytest.mark.usefixtures("mock_device_registry") async def test_option_ignore_wired_bug( hass: HomeAssistant, mock_unifi_websocket, - config_entry_setup: ConfigEntry, + config_entry_factory: Callable[[], ConfigEntry], client_payload: list[dict[str, Any]], ) -> None: """Test option to ignore wired bug.""" + client_payload.append( + { + "ap_mac": "00:00:00:00:02:01", + "essid": "ssid", + "hostname": "client", + "ip": "10.0.0.1", + "is_wired": False, + "last_seen": dt_util.as_timestamp(dt_util.utcnow()), + "mac": "00:00:00:00:00:01", + } + ) + config_entry = await config_entry_factory() + assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 1 # Client is wireless @@ -951,9 +934,7 @@ async def test_option_ignore_wired_bug( # pass time new_time = dt_util.utcnow() + timedelta( - seconds=config_entry_setup.options.get( - CONF_DETECTION_TIME, DEFAULT_DETECTION_TIME - ) + seconds=config_entry.options.get(CONF_DETECTION_TIME, DEFAULT_DETECTION_TIME) ) with freeze_time(new_time): async_fire_time_changed(hass, new_time) diff --git a/tests/components/unifi/test_hub.py b/tests/components/unifi/test_hub.py index f158d7e57eb..b81273e9745 100644 --- a/tests/components/unifi/test_hub.py +++ b/tests/components/unifi/test_hub.py @@ -4,6 +4,7 @@ from collections.abc import Callable from copy import deepcopy from datetime import timedelta from http import HTTPStatus +from typing import Any from unittest.mock import patch import aiounifi @@ -313,27 +314,25 @@ async def test_reset_fails( assert config_entry.state is ConfigEntryState.LOADED -@pytest.mark.parametrize( - "client_payload", - [ - [ - { - "hostname": "client", - "ip": "10.0.0.1", - "is_wired": True, - "last_seen": dt_util.as_timestamp(dt_util.utcnow()), - "mac": "00:00:00:00:00:01", - }, - ] - ], -) async def test_connection_state_signalling( hass: HomeAssistant, mock_device_registry, - config_entry_setup: ConfigEntry, websocket_mock, + config_entry_factory: Callable[[], ConfigEntry], + client_payload: list[dict[str, Any]], ) -> None: """Verify connection statesignalling and connection state are working.""" + client_payload.append( + { + "hostname": "client", + "ip": "10.0.0.1", + "is_wired": True, + "last_seen": dt_util.as_timestamp(dt_util.utcnow()), + "mac": "00:00:00:00:00:01", + } + ) + await config_entry_factory() + # Controller is connected assert hass.states.get("device_tracker.client").state == "home" diff --git a/tests/components/unifi/test_sensor.py b/tests/components/unifi/test_sensor.py index e59fe45181c..c8f9e9fb17e 100644 --- a/tests/components/unifi/test_sensor.py +++ b/tests/components/unifi/test_sensor.py @@ -682,43 +682,40 @@ async def test_poe_port_switches( assert hass.states.get("sensor.mock_name_port_1_poe_power") -@pytest.mark.parametrize( - "client_payload", - [ - [ - { - "essid": "SSID 1", - "is_wired": False, - "last_seen": dt_util.as_timestamp(dt_util.utcnow()), - "mac": "00:00:00:00:00:01", - "name": "Wireless client", - "oui": "Producer", - "rx_bytes-r": 2345000000, - "tx_bytes-r": 6789000000, - }, - { - "essid": "SSID 2", - "is_wired": False, - "last_seen": dt_util.as_timestamp(dt_util.utcnow()), - "mac": "00:00:00:00:00:02", - "name": "Wireless client2", - "oui": "Producer2", - "rx_bytes-r": 2345000000, - "tx_bytes-r": 6789000000, - }, - ] - ], -) @pytest.mark.parametrize("wlan_payload", [[WLAN]]) -@pytest.mark.usefixtures("config_entry_setup") async def test_wlan_client_sensors( hass: HomeAssistant, entity_registry: er.EntityRegistry, mock_unifi_websocket, websocket_mock, + config_entry_factory: Callable[[], ConfigEntry], client_payload: list[dict[str, Any]], ) -> None: """Verify that WLAN client sensors are working as expected.""" + client_payload += [ + { + "essid": "SSID 1", + "is_wired": False, + "last_seen": dt_util.as_timestamp(dt_util.utcnow()), + "mac": "00:00:00:00:00:01", + "name": "Wireless client", + "oui": "Producer", + "rx_bytes-r": 2345000000, + "tx_bytes-r": 6789000000, + }, + { + "essid": "SSID 2", + "is_wired": False, + "last_seen": dt_util.as_timestamp(dt_util.utcnow()), + "mac": "00:00:00:00:00:02", + "name": "Wireless client2", + "oui": "Producer2", + "rx_bytes-r": 2345000000, + "tx_bytes-r": 6789000000, + }, + ] + await config_entry_factory() + assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 1 ent_reg_entry = entity_registry.async_get("sensor.ssid_1") From 721b2c2ca8727a815fbc61775e4a5fbe7fa18e1c Mon Sep 17 00:00:00 2001 From: Sid <27780930+autinerd@users.noreply.github.com> Date: Sat, 8 Jun 2024 17:59:08 +0200 Subject: [PATCH 0391/1445] Enable Ruff PT012 (#113957) --- pyproject.toml | 1 - .../components/advantage_air/test_climate.py | 7 +- tests/components/alexa/test_smart_home.py | 9 +- tests/components/august/test_lock.py | 1 - tests/components/blue_current/test_init.py | 3 +- tests/components/bond/test_fan.py | 1 - tests/components/bond/test_light.py | 9 -- tests/components/bond/test_switch.py | 1 - tests/components/cloudflare/test_init.py | 1 - .../color_extractor/test_service.py | 1 - tests/components/deconz/test_services.py | 1 - tests/components/demo/test_camera.py | 2 +- tests/components/demo/test_fan.py | 4 - .../devolo_home_network/test_button.py | 1 - .../enphase_envoy/test_config_flow.py | 1 - .../esphome/test_voice_assistant.py | 16 ++- tests/components/fan/test_init.py | 2 +- tests/components/flo/test_services.py | 3 +- .../google_assistant/test_button.py | 3 +- .../components/google_assistant/test_trait.py | 91 +++++++-------- .../test_init.py | 40 ++++--- tests/components/heos/test_init.py | 2 - tests/components/home_connect/test_init.py | 4 +- tests/components/homeassistant/test_init.py | 11 +- tests/components/homeassistant/test_scene.py | 3 - .../homekit/test_type_media_players.py | 1 - tests/components/homekit/test_type_remote.py | 1 - tests/components/iaqualink/test_utils.py | 5 +- tests/components/knx/test_services.py | 2 +- tests/components/lametric/test_button.py | 2 - tests/components/lametric/test_number.py | 2 - tests/components/lametric/test_select.py | 2 - tests/components/lametric/test_switch.py | 2 - tests/components/matter/test_door_lock.py | 2 +- .../maxcube/test_maxcube_climate.py | 2 +- tests/components/media_player/test_intent.py | 10 -- tests/components/motioneye/test_camera.py | 3 - .../mqtt/test_alarm_control_panel.py | 1 - tests/components/mqtt/test_climate.py | 8 +- tests/components/myuplink/test_number.py | 5 +- tests/components/myuplink/test_switch.py | 5 +- tests/components/nest/test_climate.py | 8 -- tests/components/netatmo/test_climate.py | 3 - .../components/nibe_heatpump/test_climate.py | 1 - tests/components/notify/test_legacy.py | 1 - tests/components/numato/test_init.py | 7 +- tests/components/octoprint/test_button.py | 44 ++++--- tests/components/peco/test_config_flow.py | 1 - tests/components/pilight/test_init.py | 1 - tests/components/plex/test_media_search.py | 41 ++++--- tests/components/plex/test_playback.py | 33 +++--- tests/components/powerwall/test_switch.py | 3 +- tests/components/prosegur/test_camera.py | 4 +- tests/components/rainbird/test_switch.py | 2 - tests/components/recorder/test_backup.py | 4 +- tests/components/recorder/test_init.py | 4 +- tests/components/ring/test_siren.py | 1 - tests/components/sharkiq/test_vacuum.py | 3 +- tests/components/shelly/test_update.py | 10 +- .../signal_messenger/test_notify.py | 11 +- tests/components/smtp/test_notify.py | 1 - tests/components/stream/test_worker.py | 10 +- tests/components/subaru/test_lock.py | 6 +- tests/components/tado/test_service.py | 6 +- tests/components/teslemetry/test_climate.py | 13 ++- tests/components/tessie/common.py | 2 +- tests/components/tessie/test_climate.py | 4 +- tests/components/tessie/test_cover.py | 8 +- tests/components/tessie/test_select.py | 4 +- tests/components/tibber/test_notify.py | 26 +++-- tests/components/timer/test_init.py | 1 - tests/components/totalconnect/test_button.py | 2 +- tests/components/wilight/test_switch.py | 1 - tests/components/wled/test_button.py | 1 - tests/components/zha/test_climate.py | 2 +- tests/components/zha/test_discover.py | 42 +++---- tests/hassfest/test_version.py | 4 +- tests/helpers/test_area_registry.py | 11 +- tests/helpers/test_condition.py | 107 +++++++++--------- tests/helpers/test_config_validation.py | 7 +- tests/helpers/test_entity_component.py | 2 +- tests/helpers/test_entity_platform.py | 1 - .../test_normalized_name_base_registry.py | 8 +- tests/helpers/test_script.py | 5 +- tests/helpers/test_template.py | 4 +- tests/test_block_async_io.py | 8 +- tests/test_core.py | 1 - tests/test_requirements.py | 6 - tests/util/test_timeout.py | 20 ++-- tests/util/yaml/test_init.py | 4 +- 90 files changed, 341 insertions(+), 429 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 23ebd376469..da08e9cee84 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -767,7 +767,6 @@ ignore = [ "PLW2901", # Outer {outer_kind} variable {name} overwritten by inner {inner_kind} target "PT004", # Fixture {fixture} does not return anything, add leading underscore "PT011", # pytest.raises({exception}) is too broad, set the `match` parameter or use a more specific exception - "PT012", # `pytest.raises()` block should contain a single simple statement "PT018", # Assertion should be broken down into multiple parts "RUF001", # String contains ambiguous unicode character. "RUF002", # Docstring contains ambiguous unicode character. diff --git a/tests/components/advantage_air/test_climate.py b/tests/components/advantage_air/test_climate.py index 66f8f869ae1..fc9aaade634 100644 --- a/tests/components/advantage_air/test_climate.py +++ b/tests/components/advantage_air/test_climate.py @@ -254,13 +254,14 @@ async def test_climate_async_failed_update( ) -> None: """Test climate change failure.""" + mock_update.side_effect = ApiError + await add_mock_config(hass) with pytest.raises(HomeAssistantError): - mock_update.side_effect = ApiError - await add_mock_config(hass) await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_TEMPERATURE, {ATTR_ENTITY_ID: ["climate.myzone"], ATTR_TEMPERATURE: 25}, blocking=True, ) - mock_update.assert_called_once() + + mock_update.assert_called_once() diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index fa8d7a2c9fb..43d92f1a533 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -882,7 +882,7 @@ async def test_direction_fan(hass: HomeAssistant) -> None: payload={}, instance=None, ) - assert call.data + assert call.data async def test_preset_mode_fan( @@ -1823,12 +1823,6 @@ async def test_media_player_seek_error(hass: HomeAssistant) -> None: payload={"deltaPositionMilliseconds": 30000}, ) - assert "event" in msg - msg = msg["event"] - assert msg["header"]["name"] == "ErrorResponse" - assert msg["header"]["namespace"] == "Alexa.Video" - assert msg["payload"]["type"] == "ACTION_NOT_PERMITTED_FOR_CONTENT" - @pytest.mark.freeze_time("2022-04-19 07:53:05") async def test_alert(hass: HomeAssistant) -> None: @@ -3827,7 +3821,6 @@ async def test_disabled(hass: HomeAssistant) -> None: await smart_home.async_handle_message( hass, get_default_config(hass), request, enabled=False ) - await hass.async_block_till_done() async def test_endpoint_good_health(hass: HomeAssistant) -> None: diff --git a/tests/components/august/test_lock.py b/tests/components/august/test_lock.py index a0912e48378..a79ee7ffbf1 100644 --- a/tests/components/august/test_lock.py +++ b/tests/components/august/test_lock.py @@ -373,7 +373,6 @@ async def test_lock_throws_exception_on_unknown_status_code( data = {ATTR_ENTITY_ID: "lock.online_with_doorsense_name"} with pytest.raises(ClientResponseError): await hass.services.async_call(LOCK_DOMAIN, SERVICE_UNLOCK, data, blocking=True) - await hass.async_block_till_done() async def test_one_lock_unknown_state(hass: HomeAssistant) -> None: diff --git a/tests/components/blue_current/test_init.py b/tests/components/blue_current/test_init.py index 723dd993006..b740e6c91f9 100644 --- a/tests/components/blue_current/test_init.py +++ b/tests/components/blue_current/test_init.py @@ -62,6 +62,8 @@ async def test_config_exceptions( config_error: IntegrationError, ) -> None: """Test if the correct config error is raised when connecting to the api fails.""" + config_entry.add_to_hass(hass) + with ( patch( "homeassistant.components.blue_current.Client.validate_api_token", @@ -69,7 +71,6 @@ async def test_config_exceptions( ), pytest.raises(config_error), ): - config_entry.add_to_hass(hass) await async_setup_entry(hass, config_entry) diff --git a/tests/components/bond/test_fan.py b/tests/components/bond/test_fan.py index 6a0160fbec9..6a7ec6d1615 100644 --- a/tests/components/bond/test_fan.py +++ b/tests/components/bond/test_fan.py @@ -396,7 +396,6 @@ async def test_set_speed_belief_speed_api_error(hass: HomeAssistant) -> None: {ATTR_ENTITY_ID: "fan.name_1", "speed": 100}, blocking=True, ) - await hass.async_block_till_done() async def test_set_speed_belief_speed_100(hass: HomeAssistant) -> None: diff --git a/tests/components/bond/test_light.py b/tests/components/bond/test_light.py index 37cd82fc321..ce245c838ba 100644 --- a/tests/components/bond/test_light.py +++ b/tests/components/bond/test_light.py @@ -341,7 +341,6 @@ async def test_light_set_brightness_belief_api_error(hass: HomeAssistant) -> Non {ATTR_ENTITY_ID: "light.name_1", ATTR_BRIGHTNESS: 255}, blocking=True, ) - await hass.async_block_till_done() async def test_fp_light_set_brightness_belief_full(hass: HomeAssistant) -> None: @@ -387,7 +386,6 @@ async def test_fp_light_set_brightness_belief_api_error(hass: HomeAssistant) -> {ATTR_ENTITY_ID: "light.name_1", ATTR_BRIGHTNESS: 255}, blocking=True, ) - await hass.async_block_till_done() async def test_light_set_brightness_belief_brightness_not_supported( @@ -408,7 +406,6 @@ async def test_light_set_brightness_belief_brightness_not_supported( {ATTR_ENTITY_ID: "light.name_1", ATTR_BRIGHTNESS: 255}, blocking=True, ) - await hass.async_block_till_done() async def test_light_set_brightness_belief_zero(hass: HomeAssistant) -> None: @@ -500,7 +497,6 @@ async def test_light_set_power_belief_api_error(hass: HomeAssistant) -> None: {ATTR_ENTITY_ID: "light.name_1", ATTR_POWER_STATE: False}, blocking=True, ) - await hass.async_block_till_done() async def test_fp_light_set_power_belief(hass: HomeAssistant) -> None: @@ -546,7 +542,6 @@ async def test_fp_light_set_power_belief_api_error(hass: HomeAssistant) -> None: {ATTR_ENTITY_ID: "light.name_1", ATTR_POWER_STATE: False}, blocking=True, ) - await hass.async_block_till_done() async def test_fp_light_set_brightness_belief_brightness_not_supported( @@ -567,7 +562,6 @@ async def test_fp_light_set_brightness_belief_brightness_not_supported( {ATTR_ENTITY_ID: "light.name_1", ATTR_BRIGHTNESS: 255}, blocking=True, ) - await hass.async_block_till_done() async def test_light_start_increasing_brightness(hass: HomeAssistant) -> None: @@ -608,7 +602,6 @@ async def test_light_start_increasing_brightness_missing_service( {ATTR_ENTITY_ID: "light.name_1"}, blocking=True, ) - await hass.async_block_till_done() async def test_light_start_decreasing_brightness(hass: HomeAssistant) -> None: @@ -652,7 +645,6 @@ async def test_light_start_decreasing_brightness_missing_service( {ATTR_ENTITY_ID: "light.name_1"}, blocking=True, ) - await hass.async_block_till_done() async def test_light_stop(hass: HomeAssistant) -> None: @@ -694,7 +686,6 @@ async def test_light_stop_missing_service( {ATTR_ENTITY_ID: "light.name_1"}, blocking=True, ) - await hass.async_block_till_done() async def test_turn_on_light(hass: HomeAssistant) -> None: diff --git a/tests/components/bond/test_switch.py b/tests/components/bond/test_switch.py index 3d3ad663656..3155ec0b167 100644 --- a/tests/components/bond/test_switch.py +++ b/tests/components/bond/test_switch.py @@ -123,7 +123,6 @@ async def test_switch_set_power_belief_api_error(hass: HomeAssistant) -> None: {ATTR_ENTITY_ID: "switch.name_1", ATTR_POWER_STATE: False}, blocking=True, ) - await hass.async_block_till_done() async def test_update_reports_switch_is_on(hass: HomeAssistant) -> None: diff --git a/tests/components/cloudflare/test_init.py b/tests/components/cloudflare/test_init.py index 9d96b437733..3b2a6803566 100644 --- a/tests/components/cloudflare/test_init.py +++ b/tests/components/cloudflare/test_init.py @@ -140,7 +140,6 @@ async def test_integration_services_with_issue(hass: HomeAssistant, cfupdate) -> {}, blocking=True, ) - await hass.async_block_till_done() instance.update_dns_record.assert_not_called() diff --git a/tests/components/color_extractor/test_service.py b/tests/components/color_extractor/test_service.py index 6ad4830c2c4..941a0710067 100644 --- a/tests/components/color_extractor/test_service.py +++ b/tests/components/color_extractor/test_service.py @@ -111,7 +111,6 @@ async def test_missing_url_and_path(hass: HomeAssistant, setup_integration) -> N await hass.services.async_call( DOMAIN, SERVICE_TURN_ON, service_data, blocking=True ) - await hass.async_block_till_done() # check light is still off, unchanged due to bad parameters on service call state = hass.states.get(LIGHT_ENTITY) diff --git a/tests/components/deconz/test_services.py b/tests/components/deconz/test_services.py index 6ce3081e3c4..9c5c21bc0ff 100644 --- a/tests/components/deconz/test_services.py +++ b/tests/components/deconz/test_services.py @@ -150,7 +150,6 @@ async def test_configure_service_with_faulty_field( await hass.services.async_call( DECONZ_DOMAIN, SERVICE_CONFIGURE_DEVICE, service_data=data ) - await hass.async_block_till_done() async def test_configure_service_with_faulty_entity( diff --git a/tests/components/demo/test_camera.py b/tests/components/demo/test_camera.py index ea115e72f72..ecbd3fecee3 100644 --- a/tests/components/demo/test_camera.py +++ b/tests/components/demo/test_camera.py @@ -83,7 +83,7 @@ async def test_turn_off_image(hass: HomeAssistant) -> None: with pytest.raises(HomeAssistantError) as error: await async_get_image(hass, ENTITY_CAMERA) - assert error.args[0] == "Camera is off" + assert error.value.args[0] == "Camera is off" async def test_turn_off_invalid_camera(hass: HomeAssistant) -> None: diff --git a/tests/components/demo/test_fan.py b/tests/components/demo/test_fan.py index bd42ae3a953..bf6b8479a12 100644 --- a/tests/components/demo/test_fan.py +++ b/tests/components/demo/test_fan.py @@ -189,7 +189,6 @@ async def test_turn_on_with_preset_mode_only( {ATTR_ENTITY_ID: fan_entity_id, fan.ATTR_PRESET_MODE: "invalid"}, blocking=True, ) - await hass.async_block_till_done() assert exc.value.translation_domain == fan.DOMAIN assert exc.value.translation_key == "not_valid_preset_mode" assert exc.value.translation_placeholders == { @@ -263,7 +262,6 @@ async def test_turn_on_with_preset_mode_and_speed( {ATTR_ENTITY_ID: fan_entity_id, fan.ATTR_PRESET_MODE: "invalid"}, blocking=True, ) - await hass.async_block_till_done() assert exc.value.translation_domain == fan.DOMAIN assert exc.value.translation_key == "not_valid_preset_mode" assert exc.value.translation_placeholders == { @@ -362,7 +360,6 @@ async def test_set_preset_mode_invalid(hass: HomeAssistant, fan_entity_id) -> No {ATTR_ENTITY_ID: fan_entity_id, fan.ATTR_PRESET_MODE: "invalid"}, blocking=True, ) - await hass.async_block_till_done() assert exc.value.translation_domain == fan.DOMAIN assert exc.value.translation_key == "not_valid_preset_mode" @@ -373,7 +370,6 @@ async def test_set_preset_mode_invalid(hass: HomeAssistant, fan_entity_id) -> No {ATTR_ENTITY_ID: fan_entity_id, fan.ATTR_PRESET_MODE: "invalid"}, blocking=True, ) - await hass.async_block_till_done() assert exc.value.translation_domain == fan.DOMAIN assert exc.value.translation_key == "not_valid_preset_mode" diff --git a/tests/components/devolo_home_network/test_button.py b/tests/components/devolo_home_network/test_button.py index 1097c0271cb..b2d410b03f9 100644 --- a/tests/components/devolo_home_network/test_button.py +++ b/tests/components/devolo_home_network/test_button.py @@ -106,7 +106,6 @@ async def test_button( {ATTR_ENTITY_ID: state_key}, blocking=True, ) - await hass.async_block_till_done() await hass.config_entries.async_unload(entry.entry_id) diff --git a/tests/components/enphase_envoy/test_config_flow.py b/tests/components/enphase_envoy/test_config_flow.py index 667c769fbbb..7e1808ffa52 100644 --- a/tests/components/enphase_envoy/test_config_flow.py +++ b/tests/components/enphase_envoy/test_config_flow.py @@ -514,7 +514,6 @@ async def test_zero_conf_malformed_serial_property( type="mock_type", ), ) - await hass.async_block_till_done() assert "serialnum" in str(ex.value) result3 = await hass.config_entries.flow.async_configure( diff --git a/tests/components/esphome/test_voice_assistant.py b/tests/components/esphome/test_voice_assistant.py index 701ce76a207..bcd49f91c03 100644 --- a/tests/components/esphome/test_voice_assistant.py +++ b/tests/components/esphome/test_voice_assistant.py @@ -629,12 +629,9 @@ async def test_send_tts_wrong_sample_rate( wav_file.writeframes(bytes(_ONE_SECOND)) wav_bytes = wav_io.getvalue() - with ( - patch( - "homeassistant.components.esphome.voice_assistant.tts.async_get_media_source_audio", - return_value=("wav", wav_bytes), - ), - pytest.raises(ValueError), + with patch( + "homeassistant.components.esphome.voice_assistant.tts.async_get_media_source_audio", + return_value=("wav", wav_bytes), ): voice_assistant_api_pipeline.started = True voice_assistant_api_pipeline.transport = Mock(spec=asyncio.DatagramTransport) @@ -649,7 +646,8 @@ async def test_send_tts_wrong_sample_rate( ) assert voice_assistant_api_pipeline._tts_task is not None - await voice_assistant_api_pipeline._tts_task # raises ValueError + with pytest.raises(ValueError): + await voice_assistant_api_pipeline._tts_task async def test_send_tts_wrong_format( @@ -662,7 +660,6 @@ async def test_send_tts_wrong_format( "homeassistant.components.esphome.voice_assistant.tts.async_get_media_source_audio", return_value=("raw", bytes(1024)), ), - pytest.raises(ValueError), ): voice_assistant_api_pipeline.started = True voice_assistant_api_pipeline.transport = Mock(spec=asyncio.DatagramTransport) @@ -677,7 +674,8 @@ async def test_send_tts_wrong_format( ) assert voice_assistant_api_pipeline._tts_task is not None - await voice_assistant_api_pipeline._tts_task # raises ValueError + with pytest.raises(ValueError): + await voice_assistant_api_pipeline._tts_task async def test_send_tts_not_started( diff --git a/tests/components/fan/test_init.py b/tests/components/fan/test_init.py index e6bcc5542bd..2f1b583d7f2 100644 --- a/tests/components/fan/test_init.py +++ b/tests/components/fan/test_init.py @@ -148,7 +148,7 @@ async def test_preset_mode_validation( }, blocking=True, ) - assert exc.value.translation_key == "not_valid_preset_mode" + assert exc.value.translation_key == "not_valid_preset_mode" with pytest.raises(NotValidPresetModeError) as exc: await test_fan._valid_preset_mode_or_raise("invalid") diff --git a/tests/components/flo/test_services.py b/tests/components/flo/test_services.py index c65aa7937ee..d8837d9c6b6 100644 --- a/tests/components/flo/test_services.py +++ b/tests/components/flo/test_services.py @@ -106,5 +106,4 @@ async def test_services( }, blocking=True, ) - await hass.async_block_till_done() - assert aioclient_mock.call_count == 13 + assert aioclient_mock.call_count == 13 diff --git a/tests/components/google_assistant/test_button.py b/tests/components/google_assistant/test_button.py index 11ca77bf733..6fdb94a5610 100644 --- a/tests/components/google_assistant/test_button.py +++ b/tests/components/google_assistant/test_button.py @@ -43,9 +43,8 @@ async def test_sync_button(hass: HomeAssistant, hass_owner_user: MockUser) -> No ) mock_sync_entities.assert_called_once_with(hass_owner_user.id) + mock_sync_entities.return_value = 400 with pytest.raises(HomeAssistantError): - mock_sync_entities.return_value = 400 - await hass.services.async_call( "button", "press", diff --git a/tests/components/google_assistant/test_trait.py b/tests/components/google_assistant/test_trait.py index de0b8b3da4e..4d5f438831a 100644 --- a/tests/components/google_assistant/test_trait.py +++ b/tests/components/google_assistant/test_trait.py @@ -1782,16 +1782,16 @@ async def test_arm_disarm_arm_away(hass: HomeAssistant) -> None: # Test with no secure_pin configured + trt = trait.ArmDisArmTrait( + hass, + State( + "alarm_control_panel.alarm", + STATE_ALARM_DISARMED, + {alarm_control_panel.ATTR_CODE_ARM_REQUIRED: True}, + ), + BASIC_CONFIG, + ) with pytest.raises(error.SmartHomeError) as err: - trt = trait.ArmDisArmTrait( - hass, - State( - "alarm_control_panel.alarm", - STATE_ALARM_DISARMED, - {alarm_control_panel.ATTR_CODE_ARM_REQUIRED: True}, - ), - BASIC_CONFIG, - ) await trt.execute( trait.COMMAND_ARMDISARM, BASIC_DATA, @@ -1845,16 +1845,16 @@ async def test_arm_disarm_arm_away(hass: HomeAssistant) -> None: assert len(calls) == 1 # Test already armed + trt = trait.ArmDisArmTrait( + hass, + State( + "alarm_control_panel.alarm", + STATE_ALARM_ARMED_AWAY, + {alarm_control_panel.ATTR_CODE_ARM_REQUIRED: True}, + ), + PIN_CONFIG, + ) with pytest.raises(error.SmartHomeError) as err: - trt = trait.ArmDisArmTrait( - hass, - State( - "alarm_control_panel.alarm", - STATE_ALARM_ARMED_AWAY, - {alarm_control_panel.ATTR_CODE_ARM_REQUIRED: True}, - ), - PIN_CONFIG, - ) await trt.execute( trait.COMMAND_ARMDISARM, PIN_DATA, @@ -1940,16 +1940,16 @@ async def test_arm_disarm_disarm(hass: HomeAssistant) -> None: ) # Test without secure_pin configured + trt = trait.ArmDisArmTrait( + hass, + State( + "alarm_control_panel.alarm", + STATE_ALARM_ARMED_AWAY, + {alarm_control_panel.ATTR_CODE_ARM_REQUIRED: True}, + ), + BASIC_CONFIG, + ) with pytest.raises(error.SmartHomeError) as err: - trt = trait.ArmDisArmTrait( - hass, - State( - "alarm_control_panel.alarm", - STATE_ALARM_ARMED_AWAY, - {alarm_control_panel.ATTR_CODE_ARM_REQUIRED: True}, - ), - BASIC_CONFIG, - ) await trt.execute(trait.COMMAND_ARMDISARM, BASIC_DATA, {"arm": False}, {}) assert len(calls) == 0 @@ -1989,31 +1989,32 @@ async def test_arm_disarm_disarm(hass: HomeAssistant) -> None: assert len(calls) == 1 # Test already disarmed + trt = trait.ArmDisArmTrait( + hass, + State( + "alarm_control_panel.alarm", + STATE_ALARM_DISARMED, + {alarm_control_panel.ATTR_CODE_ARM_REQUIRED: True}, + ), + PIN_CONFIG, + ) with pytest.raises(error.SmartHomeError) as err: - trt = trait.ArmDisArmTrait( - hass, - State( - "alarm_control_panel.alarm", - STATE_ALARM_DISARMED, - {alarm_control_panel.ATTR_CODE_ARM_REQUIRED: True}, - ), - PIN_CONFIG, - ) await trt.execute(trait.COMMAND_ARMDISARM, PIN_DATA, {"arm": False}, {}) assert len(calls) == 1 assert err.value.code == const.ERR_ALREADY_DISARMED + trt = trait.ArmDisArmTrait( + hass, + State( + "alarm_control_panel.alarm", + STATE_ALARM_ARMED_AWAY, + {alarm_control_panel.ATTR_CODE_ARM_REQUIRED: False}, + ), + PIN_CONFIG, + ) + # Cancel arming after already armed will require pin with pytest.raises(error.SmartHomeError) as err: - trt = trait.ArmDisArmTrait( - hass, - State( - "alarm_control_panel.alarm", - STATE_ALARM_ARMED_AWAY, - {alarm_control_panel.ATTR_CODE_ARM_REQUIRED: False}, - ), - PIN_CONFIG, - ) await trt.execute( trait.COMMAND_ARMDISARM, PIN_DATA, {"arm": True, "cancel": True}, {} ) diff --git a/tests/components/google_generative_ai_conversation/test_init.py b/tests/components/google_generative_ai_conversation/test_init.py index a3926338b20..7afa9b4a31e 100644 --- a/tests/components/google_generative_ai_conversation/test_init.py +++ b/tests/components/google_generative_ai_conversation/test_init.py @@ -94,22 +94,20 @@ async def test_generate_content_service_error( mock_config_entry: MockConfigEntry, ) -> None: """Test generate content service handles errors.""" - with ( - patch("google.generativeai.GenerativeModel") as mock_model, - pytest.raises( - HomeAssistantError, match="Error generating content: None reason" - ), - ): + with patch("google.generativeai.GenerativeModel") as mock_model: mock_model.return_value.generate_content_async = AsyncMock( side_effect=ClientError("reason") ) - await hass.services.async_call( - "google_generative_ai_conversation", - "generate_content", - {"prompt": "write a story about an epic fail"}, - blocking=True, - return_response=True, - ) + with pytest.raises( + HomeAssistantError, match="Error generating content: None reason" + ): + await hass.services.async_call( + "google_generative_ai_conversation", + "generate_content", + {"prompt": "write a story about an epic fail"}, + blocking=True, + return_response=True, + ) @pytest.mark.usefixtures("mock_init_component") @@ -120,20 +118,20 @@ async def test_generate_content_response_has_empty_parts( """Test generate content service handles response with empty parts.""" with ( patch("google.generativeai.GenerativeModel") as mock_model, - pytest.raises(HomeAssistantError, match="Error generating content"), ): mock_response = MagicMock() mock_response.parts = [] mock_model.return_value.generate_content_async = AsyncMock( return_value=mock_response ) - await hass.services.async_call( - "google_generative_ai_conversation", - "generate_content", - {"prompt": "write a story about an epic fail"}, - blocking=True, - return_response=True, - ) + with pytest.raises(HomeAssistantError, match="Error generating content"): + await hass.services.async_call( + "google_generative_ai_conversation", + "generate_content", + {"prompt": "write a story about an epic fail"}, + blocking=True, + return_response=True, + ) async def test_generate_content_service_with_image_not_allowed_path( diff --git a/tests/components/heos/test_init.py b/tests/components/heos/test_init.py index fd453c70ebf..9341c8fbace 100644 --- a/tests/components/heos/test_init.py +++ b/tests/components/heos/test_init.py @@ -140,7 +140,6 @@ async def test_async_setup_entry_connect_failure( controller.connect.side_effect = HeosError() with pytest.raises(ConfigEntryNotReady): await async_setup_entry(hass, config_entry) - await hass.async_block_till_done() assert controller.connect.call_count == 1 assert controller.disconnect.call_count == 1 controller.connect.reset_mock() @@ -155,7 +154,6 @@ async def test_async_setup_entry_player_failure( controller.get_players.side_effect = HeosError() with pytest.raises(ConfigEntryNotReady): await async_setup_entry(hass, config_entry) - await hass.async_block_till_done() assert controller.connect.call_count == 1 assert controller.disconnect.call_count == 1 controller.connect.reset_mock() diff --git a/tests/components/home_connect/test_init.py b/tests/components/home_connect/test_init.py index 6c12f5b6738..10e7d8ca911 100644 --- a/tests/components/home_connect/test_init.py +++ b/tests/components/home_connect/test_init.py @@ -295,7 +295,7 @@ async def test_services_exception( service_call = SERVICE_KV_CALL_PARAMS[0] + service_call["service_data"]["device_id"] = "DOES_NOT_EXISTS" + with pytest.raises(ValueError): - service_call["service_data"]["device_id"] = "DOES_NOT_EXISTS" await hass.services.async_call(**service_call) - await hass.async_block_till diff --git a/tests/components/homeassistant/test_init.py b/tests/components/homeassistant/test_init.py index 84319df2888..d090da280a0 100644 --- a/tests/components/homeassistant/test_init.py +++ b/tests/components/homeassistant/test_init.py @@ -355,7 +355,6 @@ async def test_require_admin( context=ha.Context(user_id=hass_read_only_user.id), blocking=True, ) - pytest.fail(f"Should have raises for {service}") with pytest.raises(Unauthorized): await hass.services.async_call( @@ -485,8 +484,8 @@ async def test_raises_when_db_upgrade_in_progress( service, blocking=True, ) - assert "The system cannot" in caplog.text - assert "while a database upgrade in progress" in caplog.text + assert "The system cannot" in caplog.text + assert "while a database upgrade is in progress" in caplog.text assert mock_async_migration_in_progress.called caplog.clear() @@ -530,9 +529,9 @@ async def test_raises_when_config_is_invalid( SERVICE_HOMEASSISTANT_RESTART, blocking=True, ) - assert "The system cannot" in caplog.text - assert "because the configuration is not valid" in caplog.text - assert "Error 1" in caplog.text + assert "The system cannot" in caplog.text + assert "because the configuration is not valid" in caplog.text + assert "Error 1" in caplog.text assert mock_async_check_ha_config_file.called caplog.clear() diff --git a/tests/components/homeassistant/test_scene.py b/tests/components/homeassistant/test_scene.py index a1a532db162..3055f6b21b1 100644 --- a/tests/components/homeassistant/test_scene.py +++ b/tests/components/homeassistant/test_scene.py @@ -198,7 +198,6 @@ async def test_delete_service( }, blocking=True, ) - await hass.async_block_till_done() with pytest.raises(ServiceValidationError): await hass.services.async_call( @@ -209,7 +208,6 @@ async def test_delete_service( }, blocking=True, ) - await hass.async_block_till_done() assert hass.states.get("scene.hallo_2") is not None assert hass.states.get("scene.hallo") is not None @@ -303,7 +301,6 @@ async def test_ensure_no_intersection(hass: HomeAssistant) -> None: }, blocking=True, ) - await hass.async_block_till_done() assert "entities and snapshot_entities must not overlap" in str(ex.value) assert hass.states.get("scene.hallo") is None diff --git a/tests/components/homekit/test_type_media_players.py b/tests/components/homekit/test_type_media_players.py index b17f16231af..fb7233e5262 100644 --- a/tests/components/homekit/test_type_media_players.py +++ b/tests/components/homekit/test_type_media_players.py @@ -357,7 +357,6 @@ async def test_media_player_television( with pytest.raises(ValueError): acc.char_remote_key.client_update_value(20) - await hass.async_block_till_done() acc.char_remote_key.client_update_value(7) await hass.async_block_till_done() diff --git a/tests/components/homekit/test_type_remote.py b/tests/components/homekit/test_type_remote.py index 988950c64a8..bd4ead58a7b 100644 --- a/tests/components/homekit/test_type_remote.py +++ b/tests/components/homekit/test_type_remote.py @@ -133,7 +133,6 @@ async def test_activity_remote( with pytest.raises(ValueError): acc.char_remote_key.client_update_value(20) - await hass.async_block_till_done() acc.char_remote_key.client_update_value(7) await hass.async_block_till_done() diff --git a/tests/components/iaqualink/test_utils.py b/tests/components/iaqualink/test_utils.py index b9aba93523c..7a7b213f1a7 100644 --- a/tests/components/iaqualink/test_utils.py +++ b/tests/components/iaqualink/test_utils.py @@ -16,10 +16,9 @@ async def test_await_or_reraise(hass: HomeAssistant) -> None: await await_or_reraise(async_noop()) with pytest.raises(Exception) as exc_info: - async_ex = async_raises(Exception("Test exception")) - await await_or_reraise(async_ex()) + await await_or_reraise(async_raises(Exception("Test exception"))()) assert str(exc_info.value) == "Test exception" + async_ex = async_raises(AqualinkServiceException) with pytest.raises(HomeAssistantError): - async_ex = async_raises(AqualinkServiceException) await await_or_reraise(async_ex()) diff --git a/tests/components/knx/test_services.py b/tests/components/knx/test_services.py index b95ab985093..7f748af5ceb 100644 --- a/tests/components/knx/test_services.py +++ b/tests/components/knx/test_services.py @@ -299,4 +299,4 @@ async def test_service_setup_failed(hass: HomeAssistant, knx: KNXTestKit) -> Non {"address": "1/2/3", "payload": True, "response": False}, blocking=True, ) - assert str(exc_info.value) == "KNX entry not loaded" + assert str(exc_info.value) == "KNX entry not loaded" diff --git a/tests/components/lametric/test_button.py b/tests/components/lametric/test_button.py index e755329b93d..a6cdca5b426 100644 --- a/tests/components/lametric/test_button.py +++ b/tests/components/lametric/test_button.py @@ -227,7 +227,6 @@ async def test_button_error( {ATTR_ENTITY_ID: "button.frenck_s_lametric_next_app"}, blocking=True, ) - await hass.async_block_till_done() state = hass.states.get("button.frenck_s_lametric_next_app") assert state @@ -250,7 +249,6 @@ async def test_button_connection_error( {ATTR_ENTITY_ID: "button.frenck_s_lametric_next_app"}, blocking=True, ) - await hass.async_block_till_done() state = hass.states.get("button.frenck_s_lametric_next_app") assert state diff --git a/tests/components/lametric/test_number.py b/tests/components/lametric/test_number.py index d5466abbd41..681abf850d2 100644 --- a/tests/components/lametric/test_number.py +++ b/tests/components/lametric/test_number.py @@ -150,7 +150,6 @@ async def test_number_error( }, blocking=True, ) - await hass.async_block_till_done() state = hass.states.get("number.frenck_s_lametric_volume") assert state @@ -180,7 +179,6 @@ async def test_number_connection_error( }, blocking=True, ) - await hass.async_block_till_done() state = hass.states.get("number.frenck_s_lametric_volume") assert state diff --git a/tests/components/lametric/test_select.py b/tests/components/lametric/test_select.py index bd7bc775714..6b3fa291e9c 100644 --- a/tests/components/lametric/test_select.py +++ b/tests/components/lametric/test_select.py @@ -94,7 +94,6 @@ async def test_select_error( }, blocking=True, ) - await hass.async_block_till_done() state = hass.states.get("select.frenck_s_lametric_brightness_mode") assert state @@ -124,7 +123,6 @@ async def test_select_connection_error( }, blocking=True, ) - await hass.async_block_till_done() state = hass.states.get("select.frenck_s_lametric_brightness_mode") assert state diff --git a/tests/components/lametric/test_switch.py b/tests/components/lametric/test_switch.py index b81428bb402..367d5605e06 100644 --- a/tests/components/lametric/test_switch.py +++ b/tests/components/lametric/test_switch.py @@ -114,7 +114,6 @@ async def test_switch_error( }, blocking=True, ) - await hass.async_block_till_done() state = hass.states.get("switch.frenck_s_lametric_bluetooth") assert state @@ -143,7 +142,6 @@ async def test_switch_connection_error( }, blocking=True, ) - await hass.async_block_till_done() state = hass.states.get("switch.frenck_s_lametric_bluetooth") assert state diff --git a/tests/components/matter/test_door_lock.py b/tests/components/matter/test_door_lock.py index a44b5929f65..7f6abeff62b 100644 --- a/tests/components/matter/test_door_lock.py +++ b/tests/components/matter/test_door_lock.py @@ -115,9 +115,9 @@ async def test_lock_requires_pin( # set door state to unlocked set_node_attribute(door_lock, 1, 257, 0, 2) + await trigger_subscription_callback(hass, matter_client) with pytest.raises(ServiceValidationError): # Lock door using invalid code format - await trigger_subscription_callback(hass, matter_client) await hass.services.async_call( "lock", "lock", diff --git a/tests/components/maxcube/test_maxcube_climate.py b/tests/components/maxcube/test_maxcube_climate.py index e1e7dc57c47..48e616f8fd2 100644 --- a/tests/components/maxcube/test_maxcube_climate.py +++ b/tests/components/maxcube/test_maxcube_climate.py @@ -227,7 +227,7 @@ async def test_thermostat_set_no_temperature( }, blocking=True, ) - cube.set_temperature_mode.assert_not_called() + cube.set_temperature_mode.assert_not_called() async def test_thermostat_set_preset_on( diff --git a/tests/components/media_player/test_intent.py b/tests/components/media_player/test_intent.py index df47296d90c..9ddf50d04f4 100644 --- a/tests/components/media_player/test_intent.py +++ b/tests/components/media_player/test_intent.py @@ -66,7 +66,6 @@ async def test_pause_media_player_intent(hass: HomeAssistant) -> None: "test", media_player_intent.INTENT_MEDIA_PAUSE, ) - await hass.async_block_till_done() # Test feature not supported hass.states.async_set( @@ -81,7 +80,6 @@ async def test_pause_media_player_intent(hass: HomeAssistant) -> None: "test", media_player_intent.INTENT_MEDIA_PAUSE, ) - await hass.async_block_till_done() async def test_unpause_media_player_intent(hass: HomeAssistant) -> None: @@ -118,7 +116,6 @@ async def test_unpause_media_player_intent(hass: HomeAssistant) -> None: "test", media_player_intent.INTENT_MEDIA_UNPAUSE, ) - await hass.async_block_till_done() async def test_next_media_player_intent(hass: HomeAssistant) -> None: @@ -155,7 +152,6 @@ async def test_next_media_player_intent(hass: HomeAssistant) -> None: "test", media_player_intent.INTENT_MEDIA_NEXT, ) - await hass.async_block_till_done() # Test feature not supported hass.states.async_set( @@ -171,7 +167,6 @@ async def test_next_media_player_intent(hass: HomeAssistant) -> None: media_player_intent.INTENT_MEDIA_NEXT, {"name": {"value": "test media player"}}, ) - await hass.async_block_till_done() async def test_previous_media_player_intent(hass: HomeAssistant) -> None: @@ -208,7 +203,6 @@ async def test_previous_media_player_intent(hass: HomeAssistant) -> None: "test", media_player_intent.INTENT_MEDIA_PREVIOUS, ) - await hass.async_block_till_done() # Test feature not supported hass.states.async_set( @@ -224,7 +218,6 @@ async def test_previous_media_player_intent(hass: HomeAssistant) -> None: media_player_intent.INTENT_MEDIA_PREVIOUS, {"name": {"value": "test media player"}}, ) - await hass.async_block_till_done() async def test_volume_media_player_intent(hass: HomeAssistant) -> None: @@ -262,7 +255,6 @@ async def test_volume_media_player_intent(hass: HomeAssistant) -> None: media_player_intent.INTENT_SET_VOLUME, {"volume_level": {"value": 50}}, ) - await hass.async_block_till_done() # Test feature not supported hass.states.async_set( @@ -278,7 +270,6 @@ async def test_volume_media_player_intent(hass: HomeAssistant) -> None: media_player_intent.INTENT_SET_VOLUME, {"volume_level": {"value": 50}}, ) - await hass.async_block_till_done() async def test_multiple_media_players( @@ -402,7 +393,6 @@ async def test_multiple_media_players( media_player_intent.INTENT_MEDIA_PAUSE, {"name": {"value": "TV"}}, ) - await hass.async_block_till_done() # Pause the upstairs TV calls = async_mock_service(hass, DOMAIN, SERVICE_MEDIA_PAUSE) diff --git a/tests/components/motioneye/test_camera.py b/tests/components/motioneye/test_camera.py index 048ae19217a..0f3a7d6f904 100644 --- a/tests/components/motioneye/test_camera.py +++ b/tests/components/motioneye/test_camera.py @@ -432,7 +432,6 @@ async def test_set_text_overlay_bad_entity_identifier(hass: HomeAssistant) -> No client.reset_mock() with pytest.raises(vol.error.MultipleInvalid): await hass.services.async_call(DOMAIN, SERVICE_SET_TEXT_OVERLAY, data) - await hass.async_block_till_done() async def test_set_text_overlay_bad_empty(hass: HomeAssistant) -> None: @@ -441,7 +440,6 @@ async def test_set_text_overlay_bad_empty(hass: HomeAssistant) -> None: await setup_mock_motioneye_config_entry(hass, client=client) with pytest.raises(vol.error.MultipleInvalid): await hass.services.async_call(DOMAIN, SERVICE_SET_TEXT_OVERLAY, {}) - await hass.async_block_till_done() async def test_set_text_overlay_bad_no_left_or_right(hass: HomeAssistant) -> None: @@ -452,7 +450,6 @@ async def test_set_text_overlay_bad_no_left_or_right(hass: HomeAssistant) -> Non data = {ATTR_ENTITY_ID: TEST_CAMERA_ENTITY_ID} with pytest.raises(vol.error.MultipleInvalid): await hass.services.async_call(DOMAIN, SERVICE_SET_TEXT_OVERLAY, data) - await hass.async_block_till_done() async def test_set_text_overlay_good(hass: HomeAssistant) -> None: diff --git a/tests/components/mqtt/test_alarm_control_panel.py b/tests/components/mqtt/test_alarm_control_panel.py index df226de7002..a90e71cebe5 100644 --- a/tests/components/mqtt/test_alarm_control_panel.py +++ b/tests/components/mqtt/test_alarm_control_panel.py @@ -1463,7 +1463,6 @@ async def test_reload_after_invalid_config( {}, blocking=True, ) - await hass.async_block_till_done() # Make sure the config is loaded now assert hass.states.get("alarm_control_panel.test") is not None diff --git a/tests/components/mqtt/test_climate.py b/tests/components/mqtt/test_climate.py index ba5c15bd4ff..2bf78e59e42 100644 --- a/tests/components/mqtt/test_climate.py +++ b/tests/components/mqtt/test_climate.py @@ -1088,9 +1088,9 @@ async def test_set_preset_mode_optimistic( state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get("preset_mode") == "comfort" - with pytest.raises(ServiceValidationError): + with pytest.raises(ServiceValidationError) as excinfo: await common.async_set_preset_mode(hass, "invalid", ENTITY_CLIMATE) - assert "'invalid' is not a valid preset mode" in caplog.text + assert "Preset mode invalid is not valid." in str(excinfo.value) @pytest.mark.parametrize( @@ -1146,9 +1146,9 @@ async def test_set_preset_mode_explicit_optimistic( state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get("preset_mode") == "comfort" - with pytest.raises(ServiceValidationError): + with pytest.raises(ServiceValidationError) as excinfo: await common.async_set_preset_mode(hass, "invalid", ENTITY_CLIMATE) - assert "'invalid' is not a valid preset mode" in caplog.text + assert "Preset mode invalid is not valid." in str(excinfo.value) @pytest.mark.parametrize( diff --git a/tests/components/myuplink/test_number.py b/tests/components/myuplink/test_number.py index 899b2302b3c..273c35ab749 100644 --- a/tests/components/myuplink/test_number.py +++ b/tests/components/myuplink/test_number.py @@ -74,16 +74,15 @@ async def test_api_failure( ) -> None: """Test handling of exception from API.""" + mock_myuplink_client.async_set_device_points.side_effect = ClientError with pytest.raises(HomeAssistantError): - mock_myuplink_client.async_set_device_points.side_effect = ClientError await hass.services.async_call( TEST_PLATFORM, SERVICE_SET_VALUE, {ATTR_ENTITY_ID: ENTITY_ID, "value": -125}, blocking=True, ) - await hass.async_block_till_done() - mock_myuplink_client.async_set_device_points.assert_called_once() + mock_myuplink_client.async_set_device_points.assert_called_once() @pytest.mark.parametrize( diff --git a/tests/components/myuplink/test_switch.py b/tests/components/myuplink/test_switch.py index efbc2c88371..5e309e7152e 100644 --- a/tests/components/myuplink/test_switch.py +++ b/tests/components/myuplink/test_switch.py @@ -86,14 +86,13 @@ async def test_api_failure( service: str, ) -> None: """Test handling of exception from API.""" + mock_myuplink_client.async_set_device_points.side_effect = ClientError with pytest.raises(HomeAssistantError): - mock_myuplink_client.async_set_device_points.side_effect = ClientError await hass.services.async_call( TEST_PLATFORM, service, {ATTR_ENTITY_ID: ENTITY_ID}, blocking=True ) - await hass.async_block_till_done() - mock_myuplink_client.async_set_device_points.assert_called_once() + mock_myuplink_client.async_set_device_points.assert_called_once() @pytest.mark.parametrize( diff --git a/tests/components/nest/test_climate.py b/tests/components/nest/test_climate.py index 05ce5ad80f1..88847759a16 100644 --- a/tests/components/nest/test_climate.py +++ b/tests/components/nest/test_climate.py @@ -516,7 +516,6 @@ async def test_thermostat_invalid_hvac_mode( with pytest.raises(ValueError): await common.async_set_hvac_mode(hass, HVACMode.DRY) - await hass.async_block_till_done() assert thermostat.state == HVACMode.OFF assert auth.method is None # No communication with API @@ -1206,7 +1205,6 @@ async def test_thermostat_invalid_fan_mode( with pytest.raises(ServiceValidationError): await common.async_set_fan_mode(hass, FAN_LOW) - await hass.async_block_till_done() async def test_thermostat_target_temp( @@ -1378,7 +1376,6 @@ async def test_thermostat_unexpected_hvac_status( with pytest.raises(ValueError): await common.async_set_hvac_mode(hass, HVACMode.DRY) - await hass.async_block_till_done() assert thermostat.state == HVACMode.OFF @@ -1488,7 +1485,6 @@ async def test_thermostat_invalid_set_preset_mode( # Set preset mode that is invalid with pytest.raises(ServiceValidationError): await common.async_set_preset_mode(hass, PRESET_SLEEP) - await hass.async_block_till_done() # No RPC sent assert auth.method is None @@ -1538,7 +1534,6 @@ async def test_thermostat_hvac_mode_failure( auth.responses = [aiohttp.web.Response(status=HTTPStatus.BAD_REQUEST)] with pytest.raises(HomeAssistantError) as e_info: await common.async_set_hvac_mode(hass, HVACMode.HEAT) - await hass.async_block_till_done() assert "HVAC mode" in str(e_info) assert "climate.my_thermostat" in str(e_info) assert HVACMode.HEAT in str(e_info) @@ -1546,7 +1541,6 @@ async def test_thermostat_hvac_mode_failure( auth.responses = [aiohttp.web.Response(status=HTTPStatus.BAD_REQUEST)] with pytest.raises(HomeAssistantError) as e_info: await common.async_set_temperature(hass, temperature=25.0) - await hass.async_block_till_done() assert "temperature" in str(e_info) assert "climate.my_thermostat" in str(e_info) assert "25.0" in str(e_info) @@ -1554,7 +1548,6 @@ async def test_thermostat_hvac_mode_failure( auth.responses = [aiohttp.web.Response(status=HTTPStatus.BAD_REQUEST)] with pytest.raises(HomeAssistantError) as e_info: await common.async_set_fan_mode(hass, FAN_ON) - await hass.async_block_till_done() assert "fan mode" in str(e_info) assert "climate.my_thermostat" in str(e_info) assert FAN_ON in str(e_info) @@ -1562,7 +1555,6 @@ async def test_thermostat_hvac_mode_failure( auth.responses = [aiohttp.web.Response(status=HTTPStatus.BAD_REQUEST)] with pytest.raises(HomeAssistantError) as e_info: await common.async_set_preset_mode(hass, PRESET_ECO) - await hass.async_block_till_done() assert "preset mode" in str(e_info) assert "climate.my_thermostat" in str(e_info) assert PRESET_ECO in str(e_info) diff --git a/tests/components/netatmo/test_climate.py b/tests/components/netatmo/test_climate.py index b25f78b5e2f..4b908580346 100644 --- a/tests/components/netatmo/test_climate.py +++ b/tests/components/netatmo/test_climate.py @@ -750,7 +750,6 @@ async def test_service_preset_mode_with_end_time_thermostats( }, blocking=True, ) - await hass.async_block_till_done() # Test setting a valid preset mode (that allow an end datetime in Netatmo == THERM_MODES) without an end datetime with pytest.raises(MultipleInvalid): @@ -763,7 +762,6 @@ async def test_service_preset_mode_with_end_time_thermostats( }, blocking=True, ) - await hass.async_block_till_done() async def test_service_preset_mode_already_boost_valves( @@ -914,7 +912,6 @@ async def test_service_preset_mode_invalid( {ATTR_ENTITY_ID: "climate.cocina", ATTR_PRESET_MODE: "invalid"}, blocking=True, ) - await hass.async_block_till_done() async def test_valves_service_turn_off( diff --git a/tests/components/nibe_heatpump/test_climate.py b/tests/components/nibe_heatpump/test_climate.py index e40b197f58c..073e142f7ff 100644 --- a/tests/components/nibe_heatpump/test_climate.py +++ b/tests/components/nibe_heatpump/test_climate.py @@ -373,6 +373,5 @@ async def test_set_invalid_hvac_mode( }, blocking=True, ) - await hass.async_block_till_done() assert mock_connection.write_coil.mock_calls == [] diff --git a/tests/components/notify/test_legacy.py b/tests/components/notify/test_legacy.py index 71424beeda9..cc2192461ae 100644 --- a/tests/components/notify/test_legacy.py +++ b/tests/components/notify/test_legacy.py @@ -507,7 +507,6 @@ async def test_sending_none_message(hass: HomeAssistant, tmp_path: Path) -> None await hass.services.async_call( notify.DOMAIN, notify.SERVICE_NOTIFY, {notify.ATTR_MESSAGE: None} ) - await hass.async_block_till_done() assert ( str(exc.value) == "template value is None for dictionary value @ data['message']" diff --git a/tests/components/numato/test_init.py b/tests/components/numato/test_init.py index 1e84813df94..35dd102ec9e 100644 --- a/tests/components/numato/test_init.py +++ b/tests/components/numato/test_init.py @@ -47,10 +47,12 @@ async def test_hass_numato_api_wrong_port_directions( api = numato.NumatoAPI() api.setup_output(0, 5) api.setup_input(0, 2) - api.setup_input(0, 6) + api.setup_output(0, 6) with pytest.raises(NumatoGpioError): api.read_adc_input(0, 5) # adc_read from output + with pytest.raises(NumatoGpioError): api.read_input(0, 6) # read from output + with pytest.raises(NumatoGpioError): api.write_output(0, 2, 1) # write to input @@ -66,8 +68,11 @@ async def test_hass_numato_api_errors( api = numato.NumatoAPI() with pytest.raises(NumatoGpioError): api.setup_input(0, 5) + with pytest.raises(NumatoGpioError): api.read_adc_input(0, 1) + with pytest.raises(NumatoGpioError): api.read_input(0, 2) + with pytest.raises(NumatoGpioError): api.write_output(0, 2, 1) diff --git a/tests/components/octoprint/test_button.py b/tests/components/octoprint/test_button.py index 7f272f9927e..cf9008d8b58 100644 --- a/tests/components/octoprint/test_button.py +++ b/tests/components/octoprint/test_button.py @@ -57,24 +57,22 @@ async def test_pause_job(hass: HomeAssistant) -> None: assert len(pause_command.mock_calls) == 0 # Test pausing the printer when it is stopped - with ( - patch("pyoctoprintapi.OctoprintClient.pause_job") as pause_command, - pytest.raises(InvalidPrinterState), - ): + with patch("pyoctoprintapi.OctoprintClient.pause_job") as pause_command: coordinator.data["printer"] = OctoprintPrinterInfo( { "state": {"flags": {"printing": False, "paused": False}}, "temperature": [], } ) - await hass.services.async_call( - BUTTON_DOMAIN, - SERVICE_PRESS, - { - ATTR_ENTITY_ID: "button.octoprint_pause_job", - }, - blocking=True, - ) + with pytest.raises(InvalidPrinterState): + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + { + ATTR_ENTITY_ID: "button.octoprint_pause_job", + }, + blocking=True, + ) async def test_resume_job(hass: HomeAssistant) -> None: @@ -118,24 +116,22 @@ async def test_resume_job(hass: HomeAssistant) -> None: assert len(resume_command.mock_calls) == 0 # Test resuming the printer when it is stopped - with ( - patch("pyoctoprintapi.OctoprintClient.resume_job") as resume_command, - pytest.raises(InvalidPrinterState), - ): + with patch("pyoctoprintapi.OctoprintClient.resume_job") as resume_command: coordinator.data["printer"] = OctoprintPrinterInfo( { "state": {"flags": {"printing": False, "paused": False}}, "temperature": [], } ) - await hass.services.async_call( - BUTTON_DOMAIN, - SERVICE_PRESS, - { - ATTR_ENTITY_ID: "button.octoprint_resume_job", - }, - blocking=True, - ) + with pytest.raises(InvalidPrinterState): + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + { + ATTR_ENTITY_ID: "button.octoprint_resume_job", + }, + blocking=True, + ) async def test_stop_job(hass: HomeAssistant) -> None: diff --git a/tests/components/peco/test_config_flow.py b/tests/components/peco/test_config_flow.py index 112d160fa81..16a193139b4 100644 --- a/tests/components/peco/test_config_flow.py +++ b/tests/components/peco/test_config_flow.py @@ -62,7 +62,6 @@ async def test_invalid_county(hass: HomeAssistant) -> None: "county": "INVALID_COUNTY_THAT_SHOULDNT_EXIST", }, ) - await hass.async_block_till_done() async def test_meter_value_error(hass: HomeAssistant) -> None: diff --git a/tests/components/pilight/test_init.py b/tests/components/pilight/test_init.py index 621d002bb62..7c7c39d5616 100644 --- a/tests/components/pilight/test_init.py +++ b/tests/components/pilight/test_init.py @@ -114,7 +114,6 @@ async def test_send_code_no_protocol(hass: HomeAssistant) -> None: service_data={"noprotocol": "test", "value": 42}, blocking=True, ) - await hass.async_block_till_done() assert "required key not provided @ data['protocol']" in str(excinfo.value) diff --git a/tests/components/plex/test_media_search.py b/tests/components/plex/test_media_search.py index 5578ecd2550..8219cbe27b6 100644 --- a/tests/components/plex/test_media_search.py +++ b/tests/components/plex/test_media_search.py @@ -59,14 +59,13 @@ async def test_media_lookups( # TV show searches with pytest.raises(MediaNotFound) as excinfo: - payload = '{"library_name": "Not a Library", "show_name": "TV Show"}' await hass.services.async_call( MEDIA_PLAYER_DOMAIN, SERVICE_PLAY_MEDIA, { ATTR_ENTITY_ID: media_player_id, ATTR_MEDIA_CONTENT_TYPE: MediaType.EPISODE, - ATTR_MEDIA_CONTENT_ID: payload, + ATTR_MEDIA_CONTENT_ID: '{"library_name": "Not a Library", "show_name": "TV Show"}', }, True, ) @@ -251,36 +250,36 @@ async def test_media_lookups( search.assert_called_with(title="Movie 1", libtype=None) with pytest.raises(MediaNotFound) as excinfo: - payload = '{"title": "Movie 1"}' await hass.services.async_call( MEDIA_PLAYER_DOMAIN, SERVICE_PLAY_MEDIA, { ATTR_ENTITY_ID: media_player_id, ATTR_MEDIA_CONTENT_TYPE: MediaType.VIDEO, - ATTR_MEDIA_CONTENT_ID: payload, + ATTR_MEDIA_CONTENT_ID: '{"title": "Movie 1"}', }, True, ) assert "Must specify 'library_name' for this search" in str(excinfo.value) - with pytest.raises(MediaNotFound) as excinfo: - payload = '{"library_name": "Movies", "title": "Not a Movie"}' - with patch( + with ( + pytest.raises(MediaNotFound) as excinfo, + patch( "plexapi.library.LibrarySection.search", side_effect=BadRequest, __qualname__="search", - ): - await hass.services.async_call( - MEDIA_PLAYER_DOMAIN, - SERVICE_PLAY_MEDIA, - { - ATTR_ENTITY_ID: media_player_id, - ATTR_MEDIA_CONTENT_TYPE: MediaType.VIDEO, - ATTR_MEDIA_CONTENT_ID: payload, - }, - True, - ) + ), + ): + await hass.services.async_call( + MEDIA_PLAYER_DOMAIN, + SERVICE_PLAY_MEDIA, + { + ATTR_ENTITY_ID: media_player_id, + ATTR_MEDIA_CONTENT_TYPE: MediaType.VIDEO, + ATTR_MEDIA_CONTENT_ID: '{"library_name": "Movies", "title": "Not a Movie"}', + }, + True, + ) assert "Problem in query" in str(excinfo.value) # Playlist searches @@ -296,28 +295,26 @@ async def test_media_lookups( ) with pytest.raises(MediaNotFound) as excinfo: - payload = '{"playlist_name": "Not a Playlist"}' await hass.services.async_call( MEDIA_PLAYER_DOMAIN, SERVICE_PLAY_MEDIA, { ATTR_ENTITY_ID: media_player_id, ATTR_MEDIA_CONTENT_TYPE: MediaType.PLAYLIST, - ATTR_MEDIA_CONTENT_ID: payload, + ATTR_MEDIA_CONTENT_ID: '{"playlist_name": "Not a Playlist"}', }, True, ) assert "Playlist 'Not a Playlist' not found" in str(excinfo.value) with pytest.raises(MediaNotFound) as excinfo: - payload = "{}" await hass.services.async_call( MEDIA_PLAYER_DOMAIN, SERVICE_PLAY_MEDIA, { ATTR_ENTITY_ID: media_player_id, ATTR_MEDIA_CONTENT_TYPE: MediaType.PLAYLIST, - ATTR_MEDIA_CONTENT_ID: payload, + ATTR_MEDIA_CONTENT_ID: "{}", }, True, ) diff --git a/tests/components/plex/test_playback.py b/tests/components/plex/test_playback.py index 33c8b130749..183a779c940 100644 --- a/tests/components/plex/test_playback.py +++ b/tests/components/plex/test_playback.py @@ -83,7 +83,7 @@ async def test_media_player_playback( }, True, ) - assert not playmedia_mock.called + assert not playmedia_mock.called assert f"No {MediaType.MOVIE} results in 'Movies' for" in str(excinfo.value) movie1 = MockPlexMedia("Movie", "movie") @@ -197,24 +197,25 @@ async def test_media_player_playback( # Test multiple choices without exact match playmedia_mock.reset() movies = [movie2, movie3] - with pytest.raises(HomeAssistantError) as excinfo: - payload = '{"library_name": "Movies", "title": "Movie" }' - with patch( + with ( + pytest.raises(HomeAssistantError) as excinfo, + patch( "plexapi.library.LibrarySection.search", return_value=movies, __qualname__="search", - ): - await hass.services.async_call( - MP_DOMAIN, - SERVICE_PLAY_MEDIA, - { - ATTR_ENTITY_ID: media_player, - ATTR_MEDIA_CONTENT_TYPE: MediaType.MOVIE, - ATTR_MEDIA_CONTENT_ID: payload, - }, - True, - ) - assert not playmedia_mock.called + ), + ): + await hass.services.async_call( + MP_DOMAIN, + SERVICE_PLAY_MEDIA, + { + ATTR_ENTITY_ID: media_player, + ATTR_MEDIA_CONTENT_TYPE: MediaType.MOVIE, + ATTR_MEDIA_CONTENT_ID: '{"library_name": "Movies", "title": "Movie" }', + }, + True, + ) + assert not playmedia_mock.called assert "Multiple matches, make content_id more specific" in str(excinfo.value) # Test multiple choices with allow_multiple diff --git a/tests/components/powerwall/test_switch.py b/tests/components/powerwall/test_switch.py index fdcdd5150ed..b01f60210a6 100644 --- a/tests/components/powerwall/test_switch.py +++ b/tests/components/powerwall/test_switch.py @@ -95,9 +95,8 @@ async def test_exception_on_powerwall_error( ) -> None: """Ensure that an exception in the tesla_powerwall library causes a HomeAssistantError.""" + mock_powerwall.set_island_mode.side_effect = PowerwallError("Mock exception") with pytest.raises(HomeAssistantError, match="Setting off-grid operation to"): - mock_powerwall.set_island_mode.side_effect = PowerwallError("Mock exception") - await hass.services.async_call( SWITCH_DOMAIN, SERVICE_TURN_ON, diff --git a/tests/components/prosegur/test_camera.py b/tests/components/prosegur/test_camera.py index ed503d676ff..9cce5d484d4 100644 --- a/tests/components/prosegur/test_camera.py +++ b/tests/components/prosegur/test_camera.py @@ -40,9 +40,9 @@ async def test_camera_fail( ): await camera.async_get_image(hass, "camera.contract_1234abcd_test_cam") - assert "Unable to get image" in str(exc.value) + assert "Unable to get image" in str(exc.value) - assert "Image test_cam doesn't exist" in caplog.text + assert "Image test_cam doesn't exist" in caplog.text async def test_request_image( diff --git a/tests/components/rainbird/test_switch.py b/tests/components/rainbird/test_switch.py index 1352a4a633d..c2f8fa29ca3 100644 --- a/tests/components/rainbird/test_switch.py +++ b/tests/components/rainbird/test_switch.py @@ -270,13 +270,11 @@ async def test_switch_error( with pytest.raises(HomeAssistantError, match=expected_msg): await switch_common.async_turn_on(hass, "switch.rain_bird_sprinkler_3") - await hass.async_block_till_done() responses.append(mock_response_error(status=status)) with pytest.raises(HomeAssistantError, match=expected_msg): await switch_common.async_turn_off(hass, "switch.rain_bird_sprinkler_3") - await hass.async_block_till_done() @pytest.mark.parametrize( diff --git a/tests/components/recorder/test_backup.py b/tests/components/recorder/test_backup.py index d181c449bbf..08fbef01bdd 100644 --- a/tests/components/recorder/test_backup.py +++ b/tests/components/recorder/test_backup.py @@ -31,7 +31,7 @@ async def test_async_pre_backup_with_timeout( pytest.raises(TimeoutError), ): await async_pre_backup(hass) - assert lock_mock.called + assert lock_mock.called async def test_async_pre_backup_with_migration( @@ -69,4 +69,4 @@ async def test_async_post_backup_failure( pytest.raises(HomeAssistantError), ): await async_post_backup(hass) - assert unlock_mock.called + assert unlock_mock.called diff --git a/tests/components/recorder/test_init.py b/tests/components/recorder/test_init.py index c8cd2807c2e..bb449cf279a 100644 --- a/tests/components/recorder/test_init.py +++ b/tests/components/recorder/test_init.py @@ -1853,8 +1853,8 @@ async def test_database_lock_and_unlock( # Recording can't be finished while lock is held with pytest.raises(TimeoutError): await asyncio.wait_for(asyncio.shield(task), timeout=0.25) - db_events = await hass.async_add_executor_job(_get_db_events) - assert len(db_events) == 0 + db_events = await hass.async_add_executor_job(_get_db_events) + assert len(db_events) == 0 assert instance.unlock_database() diff --git a/tests/components/ring/test_siren.py b/tests/components/ring/test_siren.py index 7d3f673b61f..695b54c3971 100644 --- a/tests/components/ring/test_siren.py +++ b/tests/components/ring/test_siren.py @@ -155,7 +155,6 @@ async def test_siren_errors_when_turned_on( {"entity_id": "siren.downstairs_siren", "tone": "motion"}, blocking=True, ) - await hass.async_block_till_done() downstairs_chime_mock.test_sound.assert_called_once_with(kind="motion") assert ( any( diff --git a/tests/components/sharkiq/test_vacuum.py b/tests/components/sharkiq/test_vacuum.py index edf27101d6e..e5154008f56 100644 --- a/tests/components/sharkiq/test_vacuum.py +++ b/tests/components/sharkiq/test_vacuum.py @@ -248,8 +248,9 @@ async def test_clean_room_error( hass: HomeAssistant, room_list: list, exception: Exception ) -> None: """Test clean_room errors.""" + data = {ATTR_ENTITY_ID: VAC_ENTITY_ID, ATTR_ROOMS: room_list} + with pytest.raises(exception): - data = {ATTR_ENTITY_ID: VAC_ENTITY_ID, ATTR_ROOMS: room_list} await hass.services.async_call(DOMAIN, SERVICE_CLEAN_ROOM, data, blocking=True) diff --git a/tests/components/shelly/test_update.py b/tests/components/shelly/test_update.py index 2b233170254..9b779da093e 100644 --- a/tests/components/shelly/test_update.py +++ b/tests/components/shelly/test_update.py @@ -173,14 +173,14 @@ async def test_block_update_connection_error( ) await init_integration(hass, 1) - with pytest.raises(HomeAssistantError): + with pytest.raises(HomeAssistantError) as excinfo: await hass.services.async_call( UPDATE_DOMAIN, SERVICE_INSTALL, {ATTR_ENTITY_ID: "update.test_name_firmware_update"}, blocking=True, ) - assert "Error starting OTA update" in caplog.text + assert "Error starting OTA update" in str(excinfo.value) @pytest.mark.usefixtures("entity_registry_enabled_by_default") @@ -597,7 +597,7 @@ async def test_rpc_beta_update( @pytest.mark.parametrize( ("exc", "error"), [ - (DeviceConnectionError, "Error starting OTA update"), + (DeviceConnectionError, "OTA update connection error: DeviceConnectionError()"), (RpcCallError(-1, "error"), "OTA update request error"), ], ) @@ -625,14 +625,14 @@ async def test_rpc_update_errors( ) await init_integration(hass, 2) - with pytest.raises(HomeAssistantError): + with pytest.raises(HomeAssistantError) as excinfo: await hass.services.async_call( UPDATE_DOMAIN, SERVICE_INSTALL, {ATTR_ENTITY_ID: "update.test_name_firmware_update"}, blocking=True, ) - assert error in caplog.text + assert error in str(excinfo.value) @pytest.mark.usefixtures("entity_registry_enabled_by_default") diff --git a/tests/components/signal_messenger/test_notify.py b/tests/components/signal_messenger/test_notify.py index e2f76d54c87..012de07df0e 100644 --- a/tests/components/signal_messenger/test_notify.py +++ b/tests/components/signal_messenger/test_notify.py @@ -97,8 +97,7 @@ def test_send_message_with_bad_data_throws_vol_error( ), pytest.raises(vol.Invalid) as exc, ): - data = {"test": "test"} - signal_notification_service.send_message(MESSAGE, data=data) + signal_notification_service.send_message(MESSAGE, data={"test": "test"}) assert "Sending signal message" in caplog.text assert "extra keys not allowed" in str(exc.value) @@ -192,8 +191,9 @@ def test_get_attachments_with_large_attachment( """Test getting attachments as URL with large attachment (per Content-Length header) throws error.""" signal_requests_mock = signal_requests_mock_factory(True, str(len(CONTENT) + 1)) with pytest.raises(ValueError) as exc: - data = {"urls": [URL_ATTACHMENT]} - signal_notification_service.get_attachments_as_bytes(data, len(CONTENT), hass) + signal_notification_service.get_attachments_as_bytes( + {"urls": [URL_ATTACHMENT]}, len(CONTENT), hass + ) assert signal_requests_mock.called assert signal_requests_mock.call_count == 1 @@ -208,9 +208,8 @@ def test_get_attachments_with_large_attachment_no_header( """Test getting attachments as URL with large attachment (per content length) throws error.""" signal_requests_mock = signal_requests_mock_factory() with pytest.raises(ValueError) as exc: - data = {"urls": [URL_ATTACHMENT]} signal_notification_service.get_attachments_as_bytes( - data, len(CONTENT) - 1, hass + {"urls": [URL_ATTACHMENT]}, len(CONTENT) - 1, hass ) assert signal_requests_mock.called diff --git a/tests/components/smtp/test_notify.py b/tests/components/smtp/test_notify.py index 15be7b66d27..901d7e547fe 100644 --- a/tests/components/smtp/test_notify.py +++ b/tests/components/smtp/test_notify.py @@ -173,7 +173,6 @@ def test_sending_insecure_files_fails( pytest.raises(ServiceValidationError) as exc, ): result, _ = message.send_message(message_data, data=data) - assert content_type in result assert exc.value.translation_key == "remote_path_not_allowed" assert exc.value.translation_domain == DOMAIN assert ( diff --git a/tests/components/stream/test_worker.py b/tests/components/stream/test_worker.py index 0d47a63a000..c8f3f22196f 100644 --- a/tests/components/stream/test_worker.py +++ b/tests/components/stream/test_worker.py @@ -341,9 +341,10 @@ async def test_stream_open_fails(hass: HomeAssistant) -> None: dynamic_stream_settings(), ) stream.add_provider(HLS_PROVIDER) - with patch("av.open") as av_open, pytest.raises(StreamWorkerError): + with patch("av.open") as av_open: av_open.side_effect = av.error.InvalidDataError(-2, "error") - run_worker(hass, stream, STREAM_SOURCE) + with pytest.raises(StreamWorkerError): + run_worker(hass, stream, STREAM_SOURCE) await hass.async_block_till_done() av_open.assert_called_once() @@ -768,9 +769,10 @@ async def test_worker_log( ) stream.add_provider(HLS_PROVIDER) - with patch("av.open") as av_open, pytest.raises(StreamWorkerError) as err: + with patch("av.open") as av_open: av_open.side_effect = av.error.InvalidDataError(-2, "error") - run_worker(hass, stream, stream_url) + with pytest.raises(StreamWorkerError) as err: + run_worker(hass, stream, stream_url) await hass.async_block_till_done() assert ( str(err.value) == f"Error opening stream (ERRORTYPE_-2, error) {redacted_url}" diff --git a/tests/components/subaru/test_lock.py b/tests/components/subaru/test_lock.py index 34bbd7da9e2..c954634cf63 100644 --- a/tests/components/subaru/test_lock.py +++ b/tests/components/subaru/test_lock.py @@ -61,8 +61,7 @@ async def test_lock_cmd_fails(hass: HomeAssistant, ev_entry) -> None: await hass.services.async_call( LOCK_DOMAIN, SERVICE_UNLOCK, {ATTR_ENTITY_ID: DEVICE_ID}, blocking=True ) - await hass.async_block_till_done() - mock_lock.assert_called_once() + mock_lock.assert_not_called() async def test_unlock_specific_door(hass: HomeAssistant, ev_entry) -> None: @@ -87,5 +86,4 @@ async def test_unlock_specific_door_invalid(hass: HomeAssistant, ev_entry) -> No {ATTR_ENTITY_ID: DEVICE_ID, ATTR_DOOR: "bad_value"}, blocking=True, ) - await hass.async_block_till_done() - mock_unlock.assert_not_called() + mock_unlock.assert_not_called() diff --git a/tests/components/tado/test_service.py b/tests/components/tado/test_service.py index 759470cb5ea..994f135199f 100644 --- a/tests/components/tado/test_service.py +++ b/tests/components/tado/test_service.py @@ -80,7 +80,7 @@ async def test_add_meter_readings_exception( blocking=True, ) - assert "Could not set meter reading" in str(exc) + assert "Could not set meter reading" in str(exc) async def test_add_meter_readings_invalid( @@ -109,7 +109,7 @@ async def test_add_meter_readings_invalid( blocking=True, ) - assert "invalid new reading" in str(exc) + assert "invalid new reading" in str(exc) async def test_add_meter_readings_duplicate( @@ -138,4 +138,4 @@ async def test_add_meter_readings_duplicate( blocking=True, ) - assert "reading already exists for date" in str(exc) + assert "reading already exists for date" in str(exc) diff --git a/tests/components/teslemetry/test_climate.py b/tests/components/teslemetry/test_climate.py index edb10872139..0e21533083c 100644 --- a/tests/components/teslemetry/test_climate.py +++ b/tests/components/teslemetry/test_climate.py @@ -154,8 +154,11 @@ async def test_invalid_error(hass: HomeAssistant) -> None: {ATTR_ENTITY_ID: [entity_id]}, blocking=True, ) - mock_on.assert_called_once() - assert error.from_exception == InvalidCommand + mock_on.assert_called_once() + assert ( + str(error.value) + == "Teslemetry command failed, The data request or command is unknown." + ) @pytest.mark.parametrize("response", COMMAND_ERRORS) @@ -178,7 +181,7 @@ async def test_errors(hass: HomeAssistant, response: str) -> None: {ATTR_ENTITY_ID: [entity_id]}, blocking=True, ) - mock_on.assert_called_once() + mock_on.assert_called_once() async def test_ignored_error( @@ -232,7 +235,7 @@ async def test_asleep_or_offline( {ATTR_ENTITY_ID: [entity_id]}, blocking=True, ) - assert error + assert str(error.value) == "The data request or command is unknown." mock_wake_up.assert_called_once() mock_wake_up.side_effect = None @@ -251,7 +254,7 @@ async def test_asleep_or_offline( {ATTR_ENTITY_ID: [entity_id]}, blocking=True, ) - assert error + assert str(error.value) == "Could not wake up vehicle" mock_wake_up.assert_called_once() mock_vehicle.assert_called() diff --git a/tests/components/tessie/common.py b/tests/components/tessie/common.py index 7182e28837a..d4fc002ba25 100644 --- a/tests/components/tessie/common.py +++ b/tests/components/tessie/common.py @@ -21,7 +21,7 @@ TEST_VEHICLE_STATUS_AWAKE = {"status": TessieStatus.AWAKE} TEST_VEHICLE_STATUS_ASLEEP = {"status": TessieStatus.ASLEEP} TEST_RESPONSE = {"result": True} -TEST_RESPONSE_ERROR = {"result": False, "reason": "reason why"} +TEST_RESPONSE_ERROR = {"result": False, "reason": "reason_why"} TEST_CONFIG = {CONF_ACCESS_TOKEN: "1234567890"} TESSIE_URL = "https://api.tessie.com/" diff --git a/tests/components/tessie/test_climate.py b/tests/components/tessie/test_climate.py index df86f0b2986..bc688e1ca70 100644 --- a/tests/components/tessie/test_climate.py +++ b/tests/components/tessie/test_climate.py @@ -128,5 +128,5 @@ async def test_errors(hass: HomeAssistant) -> None: {ATTR_ENTITY_ID: [entity_id]}, blocking=True, ) - mock_set.assert_called_once() - assert error.from_exception == ERROR_UNKNOWN + mock_set.assert_called_once() + assert error.value.__cause__ == ERROR_UNKNOWN diff --git a/tests/components/tessie/test_cover.py b/tests/components/tessie/test_cover.py index ebf4c503110..b0e3d770ced 100644 --- a/tests/components/tessie/test_cover.py +++ b/tests/components/tessie/test_cover.py @@ -94,8 +94,8 @@ async def test_errors(hass: HomeAssistant) -> None: {ATTR_ENTITY_ID: [entity_id]}, blocking=True, ) - mock_set.assert_called_once() - assert error.from_exception == ERROR_UNKNOWN + mock_set.assert_called_once() + assert error.value.__cause__ == ERROR_UNKNOWN # Test setting cover open with unknown error with ( @@ -111,5 +111,5 @@ async def test_errors(hass: HomeAssistant) -> None: {ATTR_ENTITY_ID: [entity_id]}, blocking=True, ) - mock_set.assert_called_once() - assert str(error) == TEST_RESPONSE_ERROR["reason"] + mock_set.assert_called_once() + assert str(error.value) == TEST_RESPONSE_ERROR["reason"] diff --git a/tests/components/tessie/test_select.py b/tests/components/tessie/test_select.py index 7f79dbe3297..f9526bf0a47 100644 --- a/tests/components/tessie/test_select.py +++ b/tests/components/tessie/test_select.py @@ -66,5 +66,5 @@ async def test_errors(hass: HomeAssistant) -> None: {ATTR_ENTITY_ID: [entity_id], ATTR_OPTION: TessieSeatHeaterOptions.LOW}, blocking=True, ) - mock_set.assert_called_once() - assert error.from_exception == ERROR_UNKNOWN + mock_set.assert_called_once() + assert error.value.__cause__ == ERROR_UNKNOWN diff --git a/tests/components/tibber/test_notify.py b/tests/components/tibber/test_notify.py index 2e157e9415a..69af92c4d5d 100644 --- a/tests/components/tibber/test_notify.py +++ b/tests/components/tibber/test_notify.py @@ -46,16 +46,22 @@ async def test_notification_services( with pytest.raises(HomeAssistantError): # Test legacy notify service - service = "tibber" - service_data = {"message": "The message", "title": "A title"} - await hass.services.async_call("notify", service, service_data, blocking=True) + await hass.services.async_call( + "notify", + service="tibber", + service_data={"message": "The message", "title": "A title"}, + blocking=True, + ) with pytest.raises(HomeAssistantError): # Test notify entity service - service = "send_message" - service_data = { - "entity_id": "notify.tibber", - "message": "The message", - "title": "A title", - } - await hass.services.async_call("notify", service, service_data, blocking=True) + await hass.services.async_call( + "notify", + service="send_message", + service_data={ + "entity_id": "notify.tibber", + "message": "The message", + "title": "A title", + }, + blocking=True, + ) diff --git a/tests/components/timer/test_init.py b/tests/components/timer/test_init.py index 854ba10fe9f..95baa07eaa9 100644 --- a/tests/components/timer/test_init.py +++ b/tests/components/timer/test_init.py @@ -308,7 +308,6 @@ async def test_start_service(hass: HomeAssistant) -> None: {CONF_ENTITY_ID: "timer.test1", CONF_DURATION: 10}, blocking=True, ) - await hass.async_block_till_done() await hass.services.async_call( DOMAIN, diff --git a/tests/components/totalconnect/test_button.py b/tests/components/totalconnect/test_button.py index 03b08316be2..80de004be1d 100644 --- a/tests/components/totalconnect/test_button.py +++ b/tests/components/totalconnect/test_button.py @@ -48,7 +48,7 @@ async def test_bypass_button(hass: HomeAssistant, entity_id: str) -> None: service_data={ATTR_ENTITY_ID: entity_id}, blocking=True, ) - assert mock_request.call_count == 1 + assert mock_request.call_count == 1 # try to bypass, works this time await hass.services.async_call( diff --git a/tests/components/wilight/test_switch.py b/tests/components/wilight/test_switch.py index 8b3f2225c4b..7140a0780ef 100644 --- a/tests/components/wilight/test_switch.py +++ b/tests/components/wilight/test_switch.py @@ -260,5 +260,4 @@ async def test_switch_services( blocking=True, ) - await hass.async_block_till_done() assert str(exc_info.value) == "Entity is not a WiLight valve switch" diff --git a/tests/components/wled/test_button.py b/tests/components/wled/test_button.py index ef662fb4ded..b3061e6594a 100644 --- a/tests/components/wled/test_button.py +++ b/tests/components/wled/test_button.py @@ -58,7 +58,6 @@ async def test_button_restart( {ATTR_ENTITY_ID: "button.wled_rgb_light_restart"}, blocking=True, ) - await hass.async_block_till_done() # Ensure this didn't made the entity unavailable assert (state := hass.states.get("button.wled_rgb_light_restart")) diff --git a/tests/components/zha/test_climate.py b/tests/components/zha/test_climate.py index 16563f62e06..cac5ef66937 100644 --- a/tests/components/zha/test_climate.py +++ b/tests/components/zha/test_climate.py @@ -1262,7 +1262,7 @@ async def test_set_fan_mode_not_supported( {ATTR_ENTITY_ID: entity_id, ATTR_FAN_MODE: FAN_LOW}, blocking=True, ) - assert fan_cluster.write_attributes.await_count == 0 + assert fan_cluster.write_attributes.await_count == 0 async def test_set_fan_mode(hass: HomeAssistant, device_climate_fan) -> None: diff --git a/tests/components/zha/test_discover.py b/tests/components/zha/test_discover.py index 242dfe564ca..de30bc44b87 100644 --- a/tests/components/zha/test_discover.py +++ b/tests/components/zha/test_discover.py @@ -986,30 +986,30 @@ async def test_quirks_v2_metadata_errors( validate_metadata(validate_method) # ensure the error is caught and raised - with pytest.raises(ValueError, match=expected_exception_string): - try: - # introduce an error - zigpy_device = _get_test_device( - zigpy_device_mock, + try: + # introduce an error + zigpy_device = _get_test_device( + zigpy_device_mock, + "Ikea of Sweden4", + "TRADFRI remote control4", + augment_method=augment_method, + ) + await zha_device_joined(zigpy_device) + + validate_metadata(validate_method) + # if the device was created we remove it + # so we don't pollute the rest of the tests + zigpy.quirks._DEVICE_REGISTRY.remove(zigpy_device) + except ValueError: + # if the device was not created we remove it + # so we don't pollute the rest of the tests + zigpy.quirks._DEVICE_REGISTRY._registry_v2.pop( + ( "Ikea of Sweden4", "TRADFRI remote control4", - augment_method=augment_method, - ) - await zha_device_joined(zigpy_device) - - validate_metadata(validate_method) - # if the device was created we remove it - # so we don't pollute the rest of the tests - zigpy.quirks._DEVICE_REGISTRY.remove(zigpy_device) - except ValueError: - # if the device was not created we remove it - # so we don't pollute the rest of the tests - zigpy.quirks._DEVICE_REGISTRY._registry_v2.pop( - ( - "Ikea of Sweden4", - "TRADFRI remote control4", - ) ) + ) + with pytest.raises(ValueError, match=expected_exception_string): raise diff --git a/tests/hassfest/test_version.py b/tests/hassfest/test_version.py index 9cc1bbb11e5..bfe15018fe2 100644 --- a/tests/hassfest/test_version.py +++ b/tests/hassfest/test_version.py @@ -34,12 +34,12 @@ def test_validate_version_no_key(integration: Integration) -> None: def test_validate_custom_integration_manifest(integration: Integration) -> None: """Test validate custom integration manifest.""" + integration.manifest["version"] = "lorem_ipsum" with pytest.raises(vol.Invalid): - integration.manifest["version"] = "lorem_ipsum" CUSTOM_INTEGRATION_MANIFEST_SCHEMA(integration.manifest) + integration.manifest["version"] = None with pytest.raises(vol.Invalid): - integration.manifest["version"] = None CUSTOM_INTEGRATION_MANIFEST_SCHEMA(integration.manifest) integration.manifest["version"] = "1" diff --git a/tests/helpers/test_area_registry.py b/tests/helpers/test_area_registry.py index 3824442c86e..e6d637d1a99 100644 --- a/tests/helpers/test_area_registry.py +++ b/tests/helpers/test_area_registry.py @@ -85,12 +85,11 @@ async def test_create_area_with_name_already_in_use( ) -> None: """Make sure that we can't create an area with a name already in use.""" update_events = async_capture_events(hass, ar.EVENT_AREA_REGISTRY_UPDATED) - area1 = area_registry.async_create("mock") + area_registry.async_create("mock") with pytest.raises(ValueError) as e_info: - area2 = area_registry.async_create("mock") - assert area1 != area2 - assert e_info == "The name mock 2 (mock2) is already in use" + area_registry.async_create("mock") + assert str(e_info.value) == "The name mock (mock) is already in use" await hass.async_block_till_done() @@ -226,7 +225,7 @@ async def test_update_area_with_name_already_in_use( with pytest.raises(ValueError) as e_info: area_registry.async_update(area1.id, name="mock2") - assert e_info == "The name mock 2 (mock2) is already in use" + assert str(e_info.value) == "The name mock2 (mock2) is already in use" assert area1.name == "mock1" assert area2.name == "mock2" @@ -242,7 +241,7 @@ async def test_update_area_with_normalized_name_already_in_use( with pytest.raises(ValueError) as e_info: area_registry.async_update(area1.id, name="mock2") - assert e_info == "The name mock 2 (mock2) is already in use" + assert str(e_info.value) == "The name mock2 (mock2) is already in use" assert area1.name == "mock1" assert area2.name == "Moc k2" diff --git a/tests/helpers/test_condition.py b/tests/helpers/test_condition.py index 7f090f5e63b..ce114058453 100644 --- a/tests/helpers/test_condition.py +++ b/tests/helpers/test_condition.py @@ -1104,17 +1104,18 @@ async def test_state_raises(hass: HomeAssistant) -> None: test(hass) # Unknown state entity - with pytest.raises(ConditionError, match="input_text.missing"): - config = { - "condition": "state", - "entity_id": "sensor.door", - "state": "input_text.missing", - } - config = cv.CONDITION_SCHEMA(config) - config = await condition.async_validate_condition_config(hass, config) - test = await condition.async_from_config(hass, config) - hass.states.async_set("sensor.door", "open") + config = { + "condition": "state", + "entity_id": "sensor.door", + "state": "input_text.missing", + } + config = cv.CONDITION_SCHEMA(config) + config = await condition.async_validate_condition_config(hass, config) + test = await condition.async_from_config(hass, config) + + hass.states.async_set("sensor.door", "open") + with pytest.raises(ConditionError, match="input_text.missing"): test(hass) @@ -1549,76 +1550,76 @@ async def test_numeric_state_raises(hass: HomeAssistant) -> None: test(hass) # Template error - with pytest.raises(ConditionError, match="ZeroDivisionError"): - config = { - "condition": "numeric_state", - "entity_id": "sensor.temperature", - "value_template": "{{ 1 / 0 }}", - "above": 0, - } - config = cv.CONDITION_SCHEMA(config) - config = await condition.async_validate_condition_config(hass, config) - test = await condition.async_from_config(hass, config) + config = { + "condition": "numeric_state", + "entity_id": "sensor.temperature", + "value_template": "{{ 1 / 0 }}", + "above": 0, + } + config = cv.CONDITION_SCHEMA(config) + config = await condition.async_validate_condition_config(hass, config) + test = await condition.async_from_config(hass, config) - hass.states.async_set("sensor.temperature", 50) + hass.states.async_set("sensor.temperature", 50) + with pytest.raises(ConditionError, match="ZeroDivisionError"): test(hass) # Bad number - with pytest.raises(ConditionError, match="cannot be processed as a number"): - config = { - "condition": "numeric_state", - "entity_id": "sensor.temperature", - "above": 0, - } - config = cv.CONDITION_SCHEMA(config) - config = await condition.async_validate_condition_config(hass, config) - test = await condition.async_from_config(hass, config) + config = { + "condition": "numeric_state", + "entity_id": "sensor.temperature", + "above": 0, + } + config = cv.CONDITION_SCHEMA(config) + config = await condition.async_validate_condition_config(hass, config) + test = await condition.async_from_config(hass, config) - hass.states.async_set("sensor.temperature", "fifty") + hass.states.async_set("sensor.temperature", "fifty") + with pytest.raises(ConditionError, match="cannot be processed as a number"): test(hass) # Below entity missing - with pytest.raises(ConditionError, match="'below' entity"): - config = { - "condition": "numeric_state", - "entity_id": "sensor.temperature", - "below": "input_number.missing", - } - config = cv.CONDITION_SCHEMA(config) - config = await condition.async_validate_condition_config(hass, config) - test = await condition.async_from_config(hass, config) + config = { + "condition": "numeric_state", + "entity_id": "sensor.temperature", + "below": "input_number.missing", + } + config = cv.CONDITION_SCHEMA(config) + config = await condition.async_validate_condition_config(hass, config) + test = await condition.async_from_config(hass, config) - hass.states.async_set("sensor.temperature", 50) + hass.states.async_set("sensor.temperature", 50) + with pytest.raises(ConditionError, match="'below' entity"): test(hass) # Below entity not a number + hass.states.async_set("input_number.missing", "number") with pytest.raises( ConditionError, match="'below'.*input_number.missing.*cannot be processed as a number", ): - hass.states.async_set("input_number.missing", "number") test(hass) # Above entity missing - with pytest.raises(ConditionError, match="'above' entity"): - config = { - "condition": "numeric_state", - "entity_id": "sensor.temperature", - "above": "input_number.missing", - } - config = cv.CONDITION_SCHEMA(config) - config = await condition.async_validate_condition_config(hass, config) - test = await condition.async_from_config(hass, config) + config = { + "condition": "numeric_state", + "entity_id": "sensor.temperature", + "above": "input_number.missing", + } + config = cv.CONDITION_SCHEMA(config) + config = await condition.async_validate_condition_config(hass, config) + test = await condition.async_from_config(hass, config) - hass.states.async_set("sensor.temperature", 50) + hass.states.async_set("sensor.temperature", 50) + with pytest.raises(ConditionError, match="'above' entity"): test(hass) # Above entity not a number + hass.states.async_set("input_number.missing", "number") with pytest.raises( ConditionError, match="'above'.*input_number.missing.*cannot be processed as a number", ): - hass.states.async_set("input_number.missing", "number") test(hass) diff --git a/tests/helpers/test_config_validation.py b/tests/helpers/test_config_validation.py index a22fcfcd3a6..f7c6a9bc99a 100644 --- a/tests/helpers/test_config_validation.py +++ b/tests/helpers/test_config_validation.py @@ -602,7 +602,9 @@ def test_x10_address() -> None: schema = vol.Schema(cv.x10_address) with pytest.raises(vol.Invalid): schema("Q1") + with pytest.raises(vol.Invalid): schema("q55") + with pytest.raises(vol.Invalid): schema("garbage_addr") schema("a1") @@ -809,6 +811,7 @@ def test_multi_select() -> None: with pytest.raises(vol.Invalid): schema("robban") + with pytest.raises(vol.Invalid): schema(["paulus", "martinhj"]) schema(["robban", "paulus"]) @@ -1335,7 +1338,7 @@ def test_key_value_schemas() -> None: with pytest.raises(vol.Invalid) as excinfo: schema(True) - assert str(excinfo.value) == "Expected a dictionary" + assert str(excinfo.value) == "Expected a dictionary" for mode in None, {"a": "dict"}, "invalid": with pytest.raises(vol.Invalid) as excinfo: @@ -1373,7 +1376,7 @@ def test_key_value_schemas_with_default() -> None: with pytest.raises(vol.Invalid) as excinfo: schema(True) - assert str(excinfo.value) == "Expected a dictionary" + assert str(excinfo.value) == "Expected a dictionary" for mode in None, {"a": "dict"}, "invalid": with pytest.raises(vol.Invalid) as excinfo: diff --git a/tests/helpers/test_entity_component.py b/tests/helpers/test_entity_component.py index e04e24018ee..39cb48eed0e 100644 --- a/tests/helpers/test_entity_component.py +++ b/tests/helpers/test_entity_component.py @@ -510,7 +510,7 @@ async def test_register_entity_service(hass: HomeAssistant) -> None: {"entity_id": entity.entity_id, "invalid": "data"}, blocking=True, ) - assert len(calls) == 0 + assert len(calls) == 0 await hass.services.async_call( DOMAIN, "hello", {"entity_id": entity.entity_id, "some": "data"}, blocking=True diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py index fda66734431..55b5d98fd30 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -1855,7 +1855,6 @@ async def test_cancellation_is_not_blocked( with pytest.raises(asyncio.CancelledError): assert await platform.async_setup_entry(config_entry) - await hass.async_block_till_done() full_name = f"{config_entry.domain}.{platform.domain}" assert full_name not in hass.config.components diff --git a/tests/helpers/test_normalized_name_base_registry.py b/tests/helpers/test_normalized_name_base_registry.py index 495d147340f..71f5c94285a 100644 --- a/tests/helpers/test_normalized_name_base_registry.py +++ b/tests/helpers/test_normalized_name_base_registry.py @@ -60,9 +60,9 @@ def test_key_already_in_use( # should raise ValueError if we update a # key with a entry with the same normalized name + entry = NormalizedNameBaseRegistryEntry( + name="Hello World 2", normalized_name="helloworld2" + ) + registry_items["key2"] = entry with pytest.raises(ValueError): - entry = NormalizedNameBaseRegistryEntry( - name="Hello World 2", normalized_name="helloworld2" - ) - registry_items["key2"] = entry registry_items["key"] = entry diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py index 47221a77cee..08c196a04d3 100644 --- a/tests/helpers/test_script.py +++ b/tests/helpers/test_script.py @@ -450,7 +450,6 @@ async def test_service_response_data_errors( with pytest.raises(vol.Invalid, match=expected_error): await script_obj.async_run(context=context) - await hass.async_block_till_done() async def test_data_template_with_templated_key(hass: HomeAssistant) -> None: @@ -4903,15 +4902,15 @@ async def test_script_mode_queued_cancel(hass: HomeAssistant) -> None: assert script_obj.is_running assert script_obj.runs == 2 + task2.cancel() with pytest.raises(asyncio.CancelledError): - task2.cancel() await task2 assert script_obj.is_running assert script_obj.runs == 2 + task1.cancel() with pytest.raises(asyncio.CancelledError): - task1.cancel() await task1 assert not script_obj.is_running diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index 3d8dad1d23e..fd19ef019c2 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -3879,8 +3879,8 @@ async def test_device_attr( assert_result_info(info, None) assert info.rate_limit is None + info = render_to_info(hass, "{{ device_attr(56, 'id') }}") with pytest.raises(TemplateError): - info = render_to_info(hass, "{{ device_attr(56, 'id') }}") assert_result_info(info, None) # Test non existing device ids (is_device_attr) @@ -3888,8 +3888,8 @@ async def test_device_attr( assert_result_info(info, False) assert info.rate_limit is None + info = render_to_info(hass, "{{ is_device_attr(56, 'id', 'test') }}") with pytest.raises(TemplateError): - info = render_to_info(hass, "{{ is_device_attr(56, 'id', 'test') }}") assert_result_info(info, False) # Test non existing entity id (device_attr) diff --git a/tests/test_block_async_io.py b/tests/test_block_async_io.py index b7ecb034981..5a1e38d78cd 100644 --- a/tests/test_block_async_io.py +++ b/tests/test_block_async_io.py @@ -113,7 +113,6 @@ async def test_protect_loop_importlib_import_module_non_integration( ] ) with ( - pytest.raises(ImportError), patch.object(block_async_io, "_IN_TESTS", False), patch( "homeassistant.block_async_io.get_current_frame", @@ -125,7 +124,8 @@ async def test_protect_loop_importlib_import_module_non_integration( ), ): block_async_io.enable() - importlib.import_module("not_loaded_module") + with pytest.raises(ImportError): + importlib.import_module("not_loaded_module") assert "Detected blocking call to import_module" in caplog.text @@ -184,7 +184,6 @@ async def test_protect_loop_importlib_import_module_in_integration( ] ) with ( - pytest.raises(ImportError), patch.object(block_async_io, "_IN_TESTS", False), patch( "homeassistant.block_async_io.get_current_frame", @@ -196,7 +195,8 @@ async def test_protect_loop_importlib_import_module_in_integration( ), ): block_async_io.enable() - importlib.import_module("not_loaded_module") + with pytest.raises(ImportError): + importlib.import_module("not_loaded_module") assert ( "Detected blocking call to import_module inside the event loop by " diff --git a/tests/test_core.py b/tests/test_core.py index 6848d209d02..f8e96640fd1 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -1835,7 +1835,6 @@ async def test_serviceregistry_return_response_invalid( blocking=True, return_response=True, ) - await hass.async_block_till_done() @pytest.mark.parametrize( diff --git a/tests/test_requirements.py b/tests/test_requirements.py index 73f3f54c3c4..161214160aa 100644 --- a/tests/test_requirements.py +++ b/tests/test_requirements.py @@ -356,8 +356,6 @@ async def test_get_integration_with_requirements_pip_install_fails_two_passes( integration = await async_get_integration_with_requirements( hass, "test_component" ) - assert integration - assert integration.domain == "test_component" assert len(mock_is_installed.mock_calls) == 3 assert sorted(mock_call[1][0] for mock_call in mock_is_installed.mock_calls) == [ @@ -391,8 +389,6 @@ async def test_get_integration_with_requirements_pip_install_fails_two_passes( integration = await async_get_integration_with_requirements( hass, "test_component" ) - assert integration - assert integration.domain == "test_component" assert len(mock_is_installed.mock_calls) == 0 # On another attempt we remember failures and don't try again @@ -414,8 +410,6 @@ async def test_get_integration_with_requirements_pip_install_fails_two_passes( integration = await async_get_integration_with_requirements( hass, "test_component" ) - assert integration - assert integration.domain == "test_component" assert len(mock_is_installed.mock_calls) == 2 assert sorted(mock_call[1][0] for mock_call in mock_is_installed.mock_calls) == [ diff --git a/tests/util/test_timeout.py b/tests/util/test_timeout.py index d49008d608b..797c849db3c 100644 --- a/tests/util/test_timeout.py +++ b/tests/util/test_timeout.py @@ -110,7 +110,7 @@ async def test_mix_global_timeout_freeze_and_zone_freeze_other_zone_inside_execu with timeout.freeze("not_recorder"): time.sleep(0.3) - with pytest.raises(TimeoutError): + with pytest.raises(TimeoutError): # noqa: PT012 async with timeout.async_timeout(0.1): async with ( timeout.async_timeout(0.2, zone_name="recorder"), @@ -129,7 +129,7 @@ async def test_mix_global_timeout_freeze_and_zone_freeze_inside_executor_job_sec with timeout.freeze("recorder"): time.sleep(0.3) - with pytest.raises(TimeoutError): + with pytest.raises(TimeoutError): # noqa: PT012 async with timeout.async_timeout(0.1): async with timeout.async_timeout(0.2, zone_name="recorder"): await hass.async_add_executor_job(_some_sync_work) @@ -150,7 +150,7 @@ async def test_simple_global_timeout_freeze_reset() -> None: """Test a simple global timeout freeze reset.""" timeout = TimeoutManager() - with pytest.raises(TimeoutError): + with pytest.raises(TimeoutError): # noqa: PT012 async with timeout.async_timeout(0.2): async with timeout.async_freeze(): await asyncio.sleep(0.1) @@ -170,7 +170,7 @@ async def test_multiple_zone_timeout() -> None: """Test a simple zone timeout.""" timeout = TimeoutManager() - with pytest.raises(TimeoutError): + with pytest.raises(TimeoutError): # noqa: PT012 async with timeout.async_timeout(0.1, "test"): async with timeout.async_timeout(0.5, "test"): await asyncio.sleep(0.3) @@ -180,7 +180,7 @@ async def test_different_zone_timeout() -> None: """Test a simple zone timeout.""" timeout = TimeoutManager() - with pytest.raises(TimeoutError): + with pytest.raises(TimeoutError): # noqa: PT012 async with timeout.async_timeout(0.1, "test"): async with timeout.async_timeout(0.5, "other"): await asyncio.sleep(0.3) @@ -206,7 +206,7 @@ async def test_simple_zone_timeout_freeze_reset() -> None: """Test a simple zone timeout freeze reset.""" timeout = TimeoutManager() - with pytest.raises(TimeoutError): + with pytest.raises(TimeoutError): # noqa: PT012 async with timeout.async_timeout(0.2, "test"): async with timeout.async_freeze("test"): await asyncio.sleep(0.1) @@ -259,7 +259,7 @@ async def test_mix_zone_timeout_trigger_global() -> None: """Test a mix zone timeout global with trigger it.""" timeout = TimeoutManager() - with pytest.raises(TimeoutError): + with pytest.raises(TimeoutError): # noqa: PT012 async with timeout.async_timeout(0.1): with suppress(TimeoutError): async with timeout.async_timeout(0.1, "test"): @@ -308,7 +308,7 @@ async def test_simple_zone_timeout_freeze_without_timeout_cleanup2( async with timeout.async_freeze("test"): await asyncio.sleep(0.2) - with pytest.raises(TimeoutError): + with pytest.raises(TimeoutError): # noqa: PT012 async with timeout.async_timeout(0.1): hass.async_create_task(background()) await asyncio.sleep(0.3) @@ -318,7 +318,7 @@ async def test_simple_zone_timeout_freeze_without_timeout_exeption() -> None: """Test a simple zone timeout freeze on a zone that does not have a timeout set.""" timeout = TimeoutManager() - with pytest.raises(TimeoutError): + with pytest.raises(TimeoutError): # noqa: PT012 async with timeout.async_timeout(0.1): with suppress(RuntimeError): async with timeout.async_freeze("test"): @@ -331,7 +331,7 @@ async def test_simple_zone_timeout_zone_with_timeout_exeption() -> None: """Test a simple zone timeout freeze on a zone that does not have a timeout set.""" timeout = TimeoutManager() - with pytest.raises(TimeoutError): + with pytest.raises(TimeoutError): # noqa: PT012 async with timeout.async_timeout(0.1): with suppress(RuntimeError): async with timeout.async_timeout(0.3, "test"): diff --git a/tests/util/yaml/test_init.py b/tests/util/yaml/test_init.py index ed6226693c2..b900bd9dbce 100644 --- a/tests/util/yaml/test_init.py +++ b/tests/util/yaml/test_init.py @@ -596,10 +596,8 @@ async def test_loading_actual_file_with_syntax_error( hass: HomeAssistant, try_both_loaders ) -> None: """Test loading a real file with syntax errors.""" + fixture_path = pathlib.Path(__file__).parent.joinpath("fixtures", "bad.yaml.txt") with pytest.raises(HomeAssistantError): - fixture_path = pathlib.Path(__file__).parent.joinpath( - "fixtures", "bad.yaml.txt" - ) await hass.async_add_executor_job(load_yaml_config_file, fixture_path) From ae0e751a6d9d5fe986ca95cd5419d74ace58733a Mon Sep 17 00:00:00 2001 From: xyzroe Date: Sat, 8 Jun 2024 18:06:25 +0200 Subject: [PATCH 0392/1445] Add ZHA XZG firmware discovery (#116828) --- homeassistant/components/zha/manifest.json | 4 ++++ homeassistant/generated/zeroconf.py | 6 ++++++ 2 files changed, 10 insertions(+) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 8caf296674c..4f72f226fe2 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -132,6 +132,10 @@ { "type": "_slzb-06._tcp.local.", "name": "slzb-06*" + }, + { + "type": "_xzg._tcp.local.", + "name": "xzg*" } ] } diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index aea3fa341df..26078394331 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -811,6 +811,12 @@ ZEROCONF = { "domain": "kodi", }, ], + "_xzg._tcp.local.": [ + { + "domain": "zha", + "name": "xzg*", + }, + ], "_zigate-zigbee-gateway._tcp.local.": [ { "domain": "zha", From a662ee772c780127ae61cdc32bd4e86bdd10c9d8 Mon Sep 17 00:00:00 2001 From: Sid <27780930+autinerd@users.noreply.github.com> Date: Sat, 8 Jun 2024 20:40:34 +0200 Subject: [PATCH 0393/1445] Use runtime_data for enigma2 (#119154) * Use runtime_data for enigma2 * Update __init__.py --- homeassistant/components/enigma2/__init__.py | 11 ++++------- homeassistant/components/enigma2/media_player.py | 5 +++-- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/enigma2/__init__.py b/homeassistant/components/enigma2/__init__.py index 241ca7444fb..4e4f8bdb687 100644 --- a/homeassistant/components/enigma2/__init__.py +++ b/homeassistant/components/enigma2/__init__.py @@ -16,12 +16,12 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_create_clientsession -from .const import DOMAIN +type Enigma2ConfigEntry = ConfigEntry[OpenWebIfDevice] PLATFORMS = [Platform.MEDIA_PLAYER] -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: Enigma2ConfigEntry) -> bool: """Set up Enigma2 from a config entry.""" base_url = URL.build( scheme="http" if not entry.data[CONF_SSL] else "https", @@ -35,14 +35,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass, verify_ssl=entry.data[CONF_VERIFY_SSL], base_url=base_url ) - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = OpenWebIfDevice(session) + entry.runtime_data = OpenWebIfDevice(session) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): - hass.data[DOMAIN].pop(entry.entry_id) - - return unload_ok + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/enigma2/media_player.py b/homeassistant/components/enigma2/media_player.py index 037d82cd6c0..adda8f9e1c8 100644 --- a/homeassistant/components/enigma2/media_player.py +++ b/homeassistant/components/enigma2/media_player.py @@ -32,6 +32,7 @@ from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from . import Enigma2ConfigEntry from .const import ( CONF_DEEP_STANDBY, CONF_MAC_ADDRESS, @@ -102,12 +103,12 @@ async def async_setup_platform( async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: Enigma2ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Enigma2 media player platform.""" - device: OpenWebIfDevice = hass.data[DOMAIN][entry.entry_id] + device = entry.runtime_data about = await device.get_about() device.mac_address = about["info"]["ifaces"][0]["mac"] entity = Enigma2Device(entry, device, about) From d6ec8a4a9613978dd631d1719fb0455fe05a610a Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Sat, 8 Jun 2024 21:24:59 +0200 Subject: [PATCH 0394/1445] Bump py-synologydsm-api to 2.4.4 (#119156) bump py-synologydsm-api to 2.4.4 --- homeassistant/components/synology_dsm/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/synology_dsm/manifest.json b/homeassistant/components/synology_dsm/manifest.json index caecfcbd0c9..b1133fd61ad 100644 --- a/homeassistant/components/synology_dsm/manifest.json +++ b/homeassistant/components/synology_dsm/manifest.json @@ -7,7 +7,7 @@ "documentation": "https://www.home-assistant.io/integrations/synology_dsm", "iot_class": "local_polling", "loggers": ["synology_dsm"], - "requirements": ["py-synologydsm-api==2.4.2"], + "requirements": ["py-synologydsm-api==2.4.4"], "ssdp": [ { "manufacturer": "Synology", diff --git a/requirements_all.txt b/requirements_all.txt index f27cd482fc0..025cb02f0a3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1652,7 +1652,7 @@ py-schluter==0.1.7 py-sucks==0.9.10 # homeassistant.components.synology_dsm -py-synologydsm-api==2.4.2 +py-synologydsm-api==2.4.4 # homeassistant.components.zabbix py-zabbix==1.1.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3fffb5901c9..084913891a4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1317,7 +1317,7 @@ py-nightscout==1.2.2 py-sucks==0.9.10 # homeassistant.components.synology_dsm -py-synologydsm-api==2.4.2 +py-synologydsm-api==2.4.4 # homeassistant.components.seventeentrack py17track==2021.12.2 From d6097573f599ae5b184fd585704035fb513bf8b1 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sat, 8 Jun 2024 22:44:24 +0200 Subject: [PATCH 0395/1445] Remove old UniFi test infrastructure (#119160) Clean up hub --- tests/components/unifi/conftest.py | 15 ++ tests/components/unifi/test_hub.py | 295 +++----------------------- tests/components/unifi/test_init.py | 2 +- tests/components/unifi/test_switch.py | 2 +- 4 files changed, 45 insertions(+), 269 deletions(-) diff --git a/tests/components/unifi/conftest.py b/tests/components/unifi/conftest.py index 5fdeb1889fe..316be2bea47 100644 --- a/tests/components/unifi/conftest.py +++ b/tests/components/unifi/conftest.py @@ -36,6 +36,21 @@ DEFAULT_HOST = "1.2.3.4" DEFAULT_PORT = 1234 DEFAULT_SITE = "site_id" +CONTROLLER_HOST = { + "hostname": "controller_host", + "ip": DEFAULT_HOST, + "is_wired": True, + "last_seen": 1562600145, + "mac": "10:00:00:00:00:01", + "name": "Controller host", + "oui": "Producer", + "sw_mac": "00:00:00:00:01:01", + "sw_port": 1, + "wired-rx_bytes": 1234000000, + "wired-tx_bytes": 5678000000, + "uptime": 1562600160, +} + @pytest.fixture(autouse=True) def mock_discovery(): diff --git a/tests/components/unifi/test_hub.py b/tests/components/unifi/test_hub.py index b81273e9745..932c95af4f9 100644 --- a/tests/components/unifi/test_hub.py +++ b/tests/components/unifi/test_hub.py @@ -1,242 +1,25 @@ """Test UniFi Network.""" from collections.abc import Callable -from copy import deepcopy -from datetime import timedelta from http import HTTPStatus +from types import MappingProxyType from typing import Any from unittest.mock import patch import aiounifi import pytest -from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN -from homeassistant.components.device_tracker import DOMAIN as TRACKER_DOMAIN -from homeassistant.components.image import DOMAIN as IMAGE_DOMAIN -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN -from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN -from homeassistant.components.unifi.const import ( - CONF_SITE_ID, - DEFAULT_ALLOW_BANDWIDTH_SENSORS, - DEFAULT_ALLOW_UPTIME_SENSORS, - DEFAULT_DETECTION_TIME, - DEFAULT_TRACK_CLIENTS, - DEFAULT_TRACK_DEVICES, - DEFAULT_TRACK_WIRED_CLIENTS, - DOMAIN as UNIFI_DOMAIN, - UNIFI_WIRELESS_CLIENTS, -) +from homeassistant.components.unifi.const import DOMAIN as UNIFI_DOMAIN from homeassistant.components.unifi.errors import AuthenticationRequired, CannotConnect from homeassistant.components.unifi.hub import get_unifi_api -from homeassistant.components.update import DOMAIN as UPDATE_DOMAIN from homeassistant.config_entries import ConfigEntry, ConfigEntryState -from homeassistant.const import ( - CONF_HOST, - CONF_PASSWORD, - CONF_PORT, - CONF_USERNAME, - CONF_VERIFY_SSL, - CONTENT_TYPE_JSON, -) +from homeassistant.const import CONF_HOST, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr -from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.common import MockConfigEntry from tests.test_util.aiohttp import AiohttpClientMocker -DEFAULT_CONFIG_ENTRY_ID = "1" -DEFAULT_HOST = "1.2.3.4" -DEFAULT_SITE = "site_id" - -CONTROLLER_HOST = { - "hostname": "controller_host", - "ip": DEFAULT_HOST, - "is_wired": True, - "last_seen": 1562600145, - "mac": "10:00:00:00:00:01", - "name": "Controller host", - "oui": "Producer", - "sw_mac": "00:00:00:00:01:01", - "sw_port": 1, - "wired-rx_bytes": 1234000000, - "wired-tx_bytes": 5678000000, - "uptime": 1562600160, -} - -ENTRY_CONFIG = { - CONF_HOST: DEFAULT_HOST, - CONF_USERNAME: "username", - CONF_PASSWORD: "password", - CONF_PORT: 1234, - CONF_SITE_ID: DEFAULT_SITE, - CONF_VERIFY_SSL: False, -} -ENTRY_OPTIONS = {} - -CONFIGURATION = [] - -SITE = [{"desc": "Site name", "name": "site_id", "role": "admin", "_id": "1"}] - -SYSTEM_INFORMATION = [ - { - "anonymous_controller_id": "24f81231-a456-4c32-abcd-f5612345385f", - "build": "atag_7.4.162_21057", - "console_display_version": "3.1.15", - "hostname": "UDMP", - "name": "UDMP", - "previous_version": "7.4.156", - "timezone": "Europe/Stockholm", - "ubnt_device_type": "UDMPRO", - "udm_version": "3.0.20.9281", - "update_available": False, - "update_downloaded": False, - "uptime": 1196290, - "version": "7.4.162", - } -] - - -def mock_default_unifi_requests( - aioclient_mock, - host, - site_id, - sites=None, - clients_response=None, - clients_all_response=None, - devices_response=None, - dpiapp_response=None, - dpigroup_response=None, - port_forward_response=None, - system_information_response=None, - wlans_response=None, -): - """Mock default UniFi requests responses.""" - aioclient_mock.get(f"https://{host}:1234", status=302) # Check UniFi OS - - aioclient_mock.post( - f"https://{host}:1234/api/login", - json={"data": "login successful", "meta": {"rc": "ok"}}, - headers={"content-type": CONTENT_TYPE_JSON}, - ) - - aioclient_mock.get( - f"https://{host}:1234/api/self/sites", - json={"data": sites or [], "meta": {"rc": "ok"}}, - headers={"content-type": CONTENT_TYPE_JSON}, - ) - - aioclient_mock.get( - f"https://{host}:1234/api/s/{site_id}/stat/sta", - json={"data": clients_response or [], "meta": {"rc": "ok"}}, - headers={"content-type": CONTENT_TYPE_JSON}, - ) - aioclient_mock.get( - f"https://{host}:1234/api/s/{site_id}/rest/user", - json={"data": clients_all_response or [], "meta": {"rc": "ok"}}, - headers={"content-type": CONTENT_TYPE_JSON}, - ) - aioclient_mock.get( - f"https://{host}:1234/api/s/{site_id}/stat/device", - json={"data": devices_response or [], "meta": {"rc": "ok"}}, - headers={"content-type": CONTENT_TYPE_JSON}, - ) - aioclient_mock.get( - f"https://{host}:1234/api/s/{site_id}/rest/dpiapp", - json={"data": dpiapp_response or [], "meta": {"rc": "ok"}}, - headers={"content-type": CONTENT_TYPE_JSON}, - ) - aioclient_mock.get( - f"https://{host}:1234/api/s/{site_id}/rest/dpigroup", - json={"data": dpigroup_response or [], "meta": {"rc": "ok"}}, - headers={"content-type": CONTENT_TYPE_JSON}, - ) - aioclient_mock.get( - f"https://{host}:1234/api/s/{site_id}/rest/portforward", - json={"data": port_forward_response or [], "meta": {"rc": "ok"}}, - headers={"content-type": CONTENT_TYPE_JSON}, - ) - aioclient_mock.get( - f"https://{host}:1234/api/s/{site_id}/stat/sysinfo", - json={"data": system_information_response or [], "meta": {"rc": "ok"}}, - headers={"content-type": CONTENT_TYPE_JSON}, - ) - aioclient_mock.get( - f"https://{host}:1234/api/s/{site_id}/rest/wlanconf", - json={"data": wlans_response or [], "meta": {"rc": "ok"}}, - headers={"content-type": CONTENT_TYPE_JSON}, - ) - aioclient_mock.get( - f"https://{host}:1234/v2/api/site/{site_id}/trafficroutes", - json=[{}], - headers={"content-type": CONTENT_TYPE_JSON}, - ) - aioclient_mock.get( - f"https://{host}:1234/v2/api/site/{site_id}/trafficrules", - json=[{}], - headers={"content-type": CONTENT_TYPE_JSON}, - ) - - -async def setup_unifi_integration( - hass, - aioclient_mock=None, - *, - config=ENTRY_CONFIG, - options=ENTRY_OPTIONS, - sites=SITE, - clients_response=None, - clients_all_response=None, - devices_response=None, - dpiapp_response=None, - dpigroup_response=None, - port_forward_response=None, - system_information_response=None, - wlans_response=None, - known_wireless_clients=None, - unique_id="1", - config_entry_id=DEFAULT_CONFIG_ENTRY_ID, -): - """Create the UniFi Network instance.""" - assert await async_setup_component(hass, UNIFI_DOMAIN, {}) - - config_entry = MockConfigEntry( - domain=UNIFI_DOMAIN, - data=deepcopy(config), - options=deepcopy(options), - unique_id=unique_id, - entry_id=config_entry_id, - version=1, - ) - config_entry.add_to_hass(hass) - - if known_wireless_clients: - hass.data[UNIFI_WIRELESS_CLIENTS].wireless_clients.update( - known_wireless_clients - ) - - if aioclient_mock: - mock_default_unifi_requests( - aioclient_mock, - host=config_entry.data[CONF_HOST], - site_id=config_entry.data[CONF_SITE_ID], - sites=sites, - clients_response=clients_response, - clients_all_response=clients_all_response, - devices_response=devices_response, - dpiapp_response=dpiapp_response, - dpigroup_response=dpigroup_response, - port_forward_response=port_forward_response, - system_information_response=system_information_response, - wlans_response=wlans_response, - ) - - await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - - return config_entry - async def test_hub_setup( device_registry: dr.DeviceRegistry, @@ -248,38 +31,20 @@ async def test_hub_setup( return_value=True, ) as forward_entry_setup: config_entry = await config_entry_factory() - hub = config_entry.runtime_data - entry = hub.config.entry assert len(forward_entry_setup.mock_calls) == 1 assert forward_entry_setup.mock_calls[0][1] == ( - entry, + config_entry, [ - BUTTON_DOMAIN, - TRACKER_DOMAIN, - IMAGE_DOMAIN, - SENSOR_DOMAIN, - SWITCH_DOMAIN, - UPDATE_DOMAIN, + Platform.BUTTON, + Platform.DEVICE_TRACKER, + Platform.IMAGE, + Platform.SENSOR, + Platform.SWITCH, + Platform.UPDATE, ], ) - assert hub.config.host == ENTRY_CONFIG[CONF_HOST] - assert hub.is_admin == (SITE[0]["role"] == "admin") - - assert hub.config.option_allow_bandwidth_sensors == DEFAULT_ALLOW_BANDWIDTH_SENSORS - assert hub.config.option_allow_uptime_sensors == DEFAULT_ALLOW_UPTIME_SENSORS - assert isinstance(hub.config.option_block_clients, list) - assert hub.config.option_track_clients == DEFAULT_TRACK_CLIENTS - assert hub.config.option_track_devices == DEFAULT_TRACK_DEVICES - assert hub.config.option_track_wired_clients == DEFAULT_TRACK_WIRED_CLIENTS - assert hub.config.option_detection_time == timedelta(seconds=DEFAULT_DETECTION_TIME) - assert isinstance(hub.config.option_ssid_filter, set) - - assert hub.signal_reachable == "unifi-reachable-1" - assert hub.signal_options_update == "unifi-options-1" - assert hub.signal_heartbeat_missed == "unifi-heartbeat-missed" - device_entry = device_registry.async_get_or_create( config_entry_id=config_entry.entry_id, identifiers={(UNIFI_DOMAIN, config_entry.unique_id)}, @@ -292,26 +57,24 @@ async def test_reset_after_successful_setup( hass: HomeAssistant, config_entry_setup: ConfigEntry ) -> None: """Calling reset when the entry has been setup.""" - config_entry = config_entry_setup - assert config_entry.state is ConfigEntryState.LOADED + assert config_entry_setup.state is ConfigEntryState.LOADED - assert await hass.config_entries.async_unload(config_entry.entry_id) - assert config_entry.state is ConfigEntryState.NOT_LOADED + assert await hass.config_entries.async_unload(config_entry_setup.entry_id) + assert config_entry_setup.state is ConfigEntryState.NOT_LOADED async def test_reset_fails( hass: HomeAssistant, config_entry_setup: ConfigEntry ) -> None: """Calling reset when the entry has been setup can return false.""" - config_entry = config_entry_setup - assert config_entry.state is ConfigEntryState.LOADED + assert config_entry_setup.state is ConfigEntryState.LOADED with patch( "homeassistant.config_entries.ConfigEntries.async_forward_entry_unload", return_value=False, ): - assert not await hass.config_entries.async_unload(config_entry.entry_id) - assert config_entry.state is ConfigEntryState.LOADED + assert not await hass.config_entries.async_unload(config_entry_setup.entry_id) + assert config_entry_setup.state is ConfigEntryState.LOADED async def test_connection_state_signalling( @@ -346,14 +109,14 @@ async def test_connection_state_signalling( async def test_reconnect_mechanism( - hass: HomeAssistant, - aioclient_mock: AiohttpClientMocker, - config_entry_setup: ConfigEntry, - websocket_mock, + aioclient_mock: AiohttpClientMocker, websocket_mock, config_entry_setup: ConfigEntry ) -> None: """Verify reconnect prints only on first reconnection try.""" aioclient_mock.clear_requests() - aioclient_mock.get(f"https://{DEFAULT_HOST}:1234/", status=HTTPStatus.BAD_GATEWAY) + aioclient_mock.get( + f"https://{config_entry_setup.data[CONF_HOST]}:1234/", + status=HTTPStatus.BAD_GATEWAY, + ) await websocket_mock.disconnect() assert aioclient_mock.call_count == 0 @@ -374,13 +137,8 @@ async def test_reconnect_mechanism( aiounifi.AiounifiException, ], ) -async def test_reconnect_mechanism_exceptions( - hass: HomeAssistant, - aioclient_mock: AiohttpClientMocker, - config_entry_setup: ConfigEntry, - websocket_mock, - exception, -) -> None: +@pytest.mark.usefixtures("config_entry_setup") +async def test_reconnect_mechanism_exceptions(websocket_mock, exception) -> None: """Verify async_reconnect calls expected methods.""" with ( patch("aiounifi.Controller.login", side_effect=exception), @@ -409,11 +167,14 @@ async def test_reconnect_mechanism_exceptions( ], ) async def test_get_unifi_api_fails_to_connect( - hass: HomeAssistant, side_effect, raised_exception + hass: HomeAssistant, + side_effect, + raised_exception, + config_entry_data: MappingProxyType[str, Any], ) -> None: """Check that get_unifi_api can handle UniFi Network being unavailable.""" with ( patch("aiounifi.Controller.login", side_effect=side_effect), pytest.raises(raised_exception), ): - await get_unifi_api(hass, ENTRY_CONFIG) + await get_unifi_api(hass, config_entry_data) diff --git a/tests/components/unifi/test_init.py b/tests/components/unifi/test_init.py index ef9ea843bc6..914f272e118 100644 --- a/tests/components/unifi/test_init.py +++ b/tests/components/unifi/test_init.py @@ -21,7 +21,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr from homeassistant.setup import async_setup_component -from .test_hub import DEFAULT_CONFIG_ENTRY_ID +from .conftest import DEFAULT_CONFIG_ENTRY_ID from tests.common import flush_store from tests.test_util.aiohttp import AiohttpClientMocker diff --git a/tests/components/unifi/test_switch.py b/tests/components/unifi/test_switch.py index ed8d5b29a2a..4d5661a48ba 100644 --- a/tests/components/unifi/test_switch.py +++ b/tests/components/unifi/test_switch.py @@ -35,7 +35,7 @@ from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity_registry import RegistryEntryDisabler from homeassistant.util import dt as dt_util -from .test_hub import CONTROLLER_HOST +from .conftest import CONTROLLER_HOST from tests.common import async_fire_time_changed from tests.test_util.aiohttp import AiohttpClientMocker From 7e1806229b62265ba39d16d72c9cc3846407730d Mon Sep 17 00:00:00 2001 From: Guy Shefer Date: Sat, 8 Jun 2024 23:52:15 +0300 Subject: [PATCH 0396/1445] Fix Tami4 component breaking API changes (#119158) * fix tami4 api breaking changes * fix tests --- homeassistant/components/tami4/__init__.py | 4 +- homeassistant/components/tami4/config_flow.py | 3 +- homeassistant/components/tami4/coordinator.py | 22 ++++------- homeassistant/components/tami4/entity.py | 6 +-- homeassistant/components/tami4/manifest.json | 2 +- homeassistant/components/tami4/sensor.py | 26 ++++--------- homeassistant/components/tami4/strings.json | 8 ++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/tami4/conftest.py | 38 ++++++++++++------- tests/components/tami4/test_config_flow.py | 10 ++--- tests/components/tami4/test_init.py | 6 +-- 12 files changed, 63 insertions(+), 66 deletions(-) diff --git a/homeassistant/components/tami4/__init__.py b/homeassistant/components/tami4/__init__.py index 2755157214e..8c597409c77 100644 --- a/homeassistant/components/tami4/__init__.py +++ b/homeassistant/components/tami4/__init__.py @@ -10,7 +10,7 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryError, ConfigEntryNotReady from .const import API, CONF_REFRESH_TOKEN, COORDINATOR, DOMAIN -from .coordinator import Tami4EdgeWaterQualityCoordinator +from .coordinator import Tami4EdgeCoordinator PLATFORMS: list[Platform] = [Platform.BUTTON, Platform.SENSOR] @@ -26,7 +26,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: except exceptions.TokenRefreshFailedException as ex: raise ConfigEntryNotReady("Error connecting to API") from ex - coordinator = Tami4EdgeWaterQualityCoordinator(hass, api) + coordinator = Tami4EdgeCoordinator(hass, api) await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {})[entry.entry_id] = { diff --git a/homeassistant/components/tami4/config_flow.py b/homeassistant/components/tami4/config_flow.py index 83d426f47de..8c1edbfb60f 100644 --- a/homeassistant/components/tami4/config_flow.py +++ b/homeassistant/components/tami4/config_flow.py @@ -83,7 +83,8 @@ class Tami4ConfigFlow(ConfigFlow, domain=DOMAIN): errors["base"] = "unknown" else: return self.async_create_entry( - title=api.device.name, data={CONF_REFRESH_TOKEN: refresh_token} + title=api.device_metadata.name, + data={CONF_REFRESH_TOKEN: refresh_token}, ) return self.async_show_form( diff --git a/homeassistant/components/tami4/coordinator.py b/homeassistant/components/tami4/coordinator.py index 78a3723a876..4764562bc34 100644 --- a/homeassistant/components/tami4/coordinator.py +++ b/homeassistant/components/tami4/coordinator.py @@ -17,27 +17,23 @@ _LOGGER = logging.getLogger(__name__) class FlattenedWaterQuality: """Flattened WaterQuality dataclass.""" - uv_last_replacement: date uv_upcoming_replacement: date - uv_status: str - filter_last_replacement: date + uv_installed: bool filter_upcoming_replacement: date - filter_status: str + filter_installed: bool filter_litters_passed: float def __init__(self, water_quality: WaterQuality) -> None: - """Flatten WaterQuality dataclass.""" + """Flattened WaterQuality dataclass.""" - self.uv_last_replacement = water_quality.uv.last_replacement self.uv_upcoming_replacement = water_quality.uv.upcoming_replacement - self.uv_status = water_quality.uv.status - self.filter_last_replacement = water_quality.filter.last_replacement + self.uv_installed = water_quality.uv.installed self.filter_upcoming_replacement = water_quality.filter.upcoming_replacement - self.filter_status = water_quality.filter.status + self.filter_installed = water_quality.filter.installed self.filter_litters_passed = water_quality.filter.milli_litters_passed / 1000 -class Tami4EdgeWaterQualityCoordinator(DataUpdateCoordinator[FlattenedWaterQuality]): +class Tami4EdgeCoordinator(DataUpdateCoordinator[FlattenedWaterQuality]): """Tami4Edge water quality coordinator.""" def __init__(self, hass: HomeAssistant, api: Tami4EdgeAPI) -> None: @@ -53,10 +49,8 @@ class Tami4EdgeWaterQualityCoordinator(DataUpdateCoordinator[FlattenedWaterQuali async def _async_update_data(self) -> FlattenedWaterQuality: """Fetch data from the API endpoint.""" try: - water_quality = await self.hass.async_add_executor_job( - self._api.get_water_quality - ) + device = await self.hass.async_add_executor_job(self._api.get_device) - return FlattenedWaterQuality(water_quality) + return FlattenedWaterQuality(device.water_quality) except exceptions.APIRequestFailedException as ex: raise UpdateFailed("Error communicating with API") from ex diff --git a/homeassistant/components/tami4/entity.py b/homeassistant/components/tami4/entity.py index d84cd82f39a..b99ca21663d 100644 --- a/homeassistant/components/tami4/entity.py +++ b/homeassistant/components/tami4/entity.py @@ -21,14 +21,14 @@ class Tami4EdgeBaseEntity(Entity): """Initialize the Tami4Edge.""" self._state = None self._api = api - device_id = api.device.psn + device_id = api.device_metadata.psn self.entity_description = entity_description self._attr_unique_id = f"{device_id}_{self.entity_description.key}" self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, device_id)}, manufacturer="Stratuss", - name=api.device.name, + name=api.device_metadata.name, model="Tami4", - sw_version=api.device.device_firmware, + sw_version=api.device_metadata.device_firmware, suggested_area="Kitchen", ) diff --git a/homeassistant/components/tami4/manifest.json b/homeassistant/components/tami4/manifest.json index 49cbf6fe1c6..e09970c341d 100644 --- a/homeassistant/components/tami4/manifest.json +++ b/homeassistant/components/tami4/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tami4", "iot_class": "cloud_polling", - "requirements": ["Tami4EdgeAPI==2.1"] + "requirements": ["Tami4EdgeAPI==3.0"] } diff --git a/homeassistant/components/tami4/sensor.py b/homeassistant/components/tami4/sensor.py index 3772ef0bccb..888acda9372 100644 --- a/homeassistant/components/tami4/sensor.py +++ b/homeassistant/components/tami4/sensor.py @@ -17,30 +17,20 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import API, COORDINATOR, DOMAIN -from .coordinator import Tami4EdgeWaterQualityCoordinator +from .coordinator import Tami4EdgeCoordinator from .entity import Tami4EdgeBaseEntity _LOGGER = logging.getLogger(__name__) ENTITY_DESCRIPTIONS = [ - SensorEntityDescription( - key="uv_last_replacement", - translation_key="uv_last_replacement", - device_class=SensorDeviceClass.DATE, - ), SensorEntityDescription( key="uv_upcoming_replacement", translation_key="uv_upcoming_replacement", device_class=SensorDeviceClass.DATE, ), SensorEntityDescription( - key="uv_status", - translation_key="uv_status", - ), - SensorEntityDescription( - key="filter_last_replacement", - translation_key="filter_last_replacement", - device_class=SensorDeviceClass.DATE, + key="uv_installed", + translation_key="uv_installed", ), SensorEntityDescription( key="filter_upcoming_replacement", @@ -48,8 +38,8 @@ ENTITY_DESCRIPTIONS = [ device_class=SensorDeviceClass.DATE, ), SensorEntityDescription( - key="filter_status", - translation_key="filter_status", + key="filter_installed", + translation_key="filter_installed", ), SensorEntityDescription( key="filter_litters_passed", @@ -67,7 +57,7 @@ async def async_setup_entry( """Perform the setup for Tami4Edge.""" data = hass.data[DOMAIN][entry.entry_id] api: Tami4EdgeAPI = data[API] - coordinator: Tami4EdgeWaterQualityCoordinator = data[COORDINATOR] + coordinator: Tami4EdgeCoordinator = data[COORDINATOR] async_add_entities( Tami4EdgeSensorEntity( @@ -81,14 +71,14 @@ async def async_setup_entry( class Tami4EdgeSensorEntity( Tami4EdgeBaseEntity, - CoordinatorEntity[Tami4EdgeWaterQualityCoordinator], + CoordinatorEntity[Tami4EdgeCoordinator], SensorEntity, ): """Representation of the entity.""" def __init__( self, - coordinator: Tami4EdgeWaterQualityCoordinator, + coordinator: Tami4EdgeCoordinator, api: Tami4EdgeAPI, entity_description: SensorEntityDescription, ) -> None: diff --git a/homeassistant/components/tami4/strings.json b/homeassistant/components/tami4/strings.json index 79447d93e9e..406964a3bff 100644 --- a/homeassistant/components/tami4/strings.json +++ b/homeassistant/components/tami4/strings.json @@ -7,8 +7,8 @@ "uv_upcoming_replacement": { "name": "UV upcoming replacement" }, - "uv_status": { - "name": "UV status" + "uv_installed": { + "name": "UV installed" }, "filter_last_replacement": { "name": "Filter last replacement" @@ -16,8 +16,8 @@ "filter_upcoming_replacement": { "name": "Filter upcoming replacement" }, - "filter_status": { - "name": "Filter status" + "filter_installed": { + "name": "Filter installed" }, "filter_litters_passed": { "name": "Filter water passed" diff --git a/requirements_all.txt b/requirements_all.txt index 025cb02f0a3..00b10baaced 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -122,7 +122,7 @@ RtmAPI==0.7.2 SQLAlchemy==2.0.30 # homeassistant.components.tami4 -Tami4EdgeAPI==2.1 +Tami4EdgeAPI==3.0 # homeassistant.components.travisci TravisPy==0.3.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 084913891a4..dfc3cb7b674 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -107,7 +107,7 @@ RtmAPI==0.7.2 SQLAlchemy==2.0.30 # homeassistant.components.tami4 -Tami4EdgeAPI==2.1 +Tami4EdgeAPI==3.0 # homeassistant.components.onvif WSDiscovery==2.0.0 diff --git a/tests/components/tami4/conftest.py b/tests/components/tami4/conftest.py index 26d6e043dea..84b96c04735 100644 --- a/tests/components/tami4/conftest.py +++ b/tests/components/tami4/conftest.py @@ -5,6 +5,7 @@ from unittest.mock import AsyncMock, MagicMock, patch import pytest from Tami4EdgeAPI.device import Device +from Tami4EdgeAPI.device_metadata import DeviceMetadata from Tami4EdgeAPI.water_quality import UV, Filter, WaterQuality from typing_extensions import Generator @@ -32,17 +33,17 @@ async def create_config_entry(hass: HomeAssistant) -> MockConfigEntry: @pytest.fixture -def mock_api(mock__get_devices, mock_get_water_quality): +def mock_api(mock__get_devices_metadata, mock_get_device): """Fixture to mock all API calls.""" @pytest.fixture -def mock__get_devices(request: pytest.FixtureRequest) -> Generator[None]: +def mock__get_devices_metadata(request: pytest.FixtureRequest) -> Generator[None]: """Fixture to mock _get_devices which makes a call to the API.""" side_effect = getattr(request, "param", None) - device = Device( + device_metadata = DeviceMetadata( id=1, name="Drink Water", connected=True, @@ -52,38 +53,49 @@ def mock__get_devices(request: pytest.FixtureRequest) -> Generator[None]: ) with patch( - "Tami4EdgeAPI.Tami4EdgeAPI.Tami4EdgeAPI._get_devices", - return_value=[device], + "Tami4EdgeAPI.Tami4EdgeAPI.Tami4EdgeAPI._get_devices_metadata", + return_value=[device_metadata], side_effect=side_effect, ): yield @pytest.fixture -def mock_get_water_quality( +def mock_get_device( request: pytest.FixtureRequest, ) -> Generator[None]: - """Fixture to mock get_water_quality which makes a call to the API.""" + """Fixture to mock get_device which makes a call to the API.""" side_effect = getattr(request, "param", None) water_quality = WaterQuality( uv=UV( - last_replacement=int(datetime.now().timestamp()), upcoming_replacement=int(datetime.now().timestamp()), - status="on", + installed=True, ), filter=Filter( - last_replacement=int(datetime.now().timestamp()), upcoming_replacement=int(datetime.now().timestamp()), - status="on", milli_litters_passed=1000, + installed=True, ), ) + device_metadata = DeviceMetadata( + id=1, + name="Drink Water", + connected=True, + psn="psn", + type="type", + device_firmware="v1.1", + ) + + device = Device( + water_quality=water_quality, device_metadata=device_metadata, drinks=[] + ) + with patch( - "Tami4EdgeAPI.Tami4EdgeAPI.Tami4EdgeAPI.get_water_quality", - return_value=water_quality, + "Tami4EdgeAPI.Tami4EdgeAPI.Tami4EdgeAPI.get_device", + return_value=device, side_effect=side_effect, ): yield diff --git a/tests/components/tami4/test_config_flow.py b/tests/components/tami4/test_config_flow.py index cf81b015254..4210c391d70 100644 --- a/tests/components/tami4/test_config_flow.py +++ b/tests/components/tami4/test_config_flow.py @@ -13,7 +13,7 @@ async def test_step_user_valid_number( hass: HomeAssistant, mock_setup_entry, mock_request_otp, - mock__get_devices, + mock__get_devices_metadata, ) -> None: """Test user step with valid phone number.""" @@ -37,7 +37,7 @@ async def test_step_user_invalid_number( hass: HomeAssistant, mock_setup_entry, mock_request_otp, - mock__get_devices, + mock__get_devices_metadata, ) -> None: """Test user step with invalid phone number.""" @@ -66,7 +66,7 @@ async def test_step_user_exception( hass: HomeAssistant, mock_setup_entry, mock_request_otp, - mock__get_devices, + mock__get_devices_metadata, expected_error, ) -> None: """Test user step with exception.""" @@ -92,7 +92,7 @@ async def test_step_otp_valid( mock_setup_entry, mock_request_otp, mock_submit_otp, - mock__get_devices, + mock__get_devices_metadata, ) -> None: """Test user step with valid phone number.""" @@ -134,7 +134,7 @@ async def test_step_otp_exception( mock_setup_entry, mock_request_otp, mock_submit_otp, - mock__get_devices, + mock__get_devices_metadata, expected_error, ) -> None: """Test user step with valid phone number.""" diff --git a/tests/components/tami4/test_init.py b/tests/components/tami4/test_init.py index 2e9663c2728..2fe16d84cdb 100644 --- a/tests/components/tami4/test_init.py +++ b/tests/components/tami4/test_init.py @@ -17,7 +17,7 @@ async def test_init_success(mock_api, hass: HomeAssistant) -> None: @pytest.mark.parametrize( - "mock_get_water_quality", [exceptions.APIRequestFailedException], indirect=True + "mock_get_device", [exceptions.APIRequestFailedException], indirect=True ) async def test_init_with_api_error(mock_api, hass: HomeAssistant) -> None: """Test init with api error.""" @@ -27,7 +27,7 @@ async def test_init_with_api_error(mock_api, hass: HomeAssistant) -> None: @pytest.mark.parametrize( - ("mock__get_devices", "expected_state"), + ("mock__get_devices_metadata", "expected_state"), [ ( exceptions.RefreshTokenExpiredException, @@ -38,7 +38,7 @@ async def test_init_with_api_error(mock_api, hass: HomeAssistant) -> None: ConfigEntryState.SETUP_RETRY, ), ], - indirect=["mock__get_devices"], + indirect=["mock__get_devices_metadata"], ) async def test_init_error_raised( mock_api, hass: HomeAssistant, expected_state: ConfigEntryState From 0ca4314d486e14d23b50cd8c36b615596e9ba003 Mon Sep 17 00:00:00 2001 From: tronikos Date: Sat, 8 Jun 2024 13:54:32 -0700 Subject: [PATCH 0397/1445] Make supported_features of manual alarm_control_panel configurable (#119122) --- .../components/manual/alarm_control_panel.py | 26 ++++--- .../manual/test_alarm_control_panel.py | 68 +++++++++++++++++++ 2 files changed, 86 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/manual/alarm_control_panel.py b/homeassistant/components/manual/alarm_control_panel.py index 37580011a5e..5b344dd01ac 100644 --- a/homeassistant/components/manual/alarm_control_panel.py +++ b/homeassistant/components/manual/alarm_control_panel.py @@ -42,6 +42,7 @@ import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) +CONF_ARMING_STATES = "arming_states" CONF_CODE_TEMPLATE = "code_template" CONF_CODE_ARM_REQUIRED = "code_arm_required" @@ -71,6 +72,14 @@ SUPPORTED_ARMING_STATES = [ if state not in (STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED) ] +SUPPORTED_ARMING_STATE_TO_FEATURE = { + STATE_ALARM_ARMED_AWAY: AlarmControlPanelEntityFeature.ARM_AWAY, + STATE_ALARM_ARMED_HOME: AlarmControlPanelEntityFeature.ARM_HOME, + STATE_ALARM_ARMED_NIGHT: AlarmControlPanelEntityFeature.ARM_NIGHT, + STATE_ALARM_ARMED_VACATION: AlarmControlPanelEntityFeature.ARM_VACATION, + STATE_ALARM_ARMED_CUSTOM_BYPASS: AlarmControlPanelEntityFeature.ARM_CUSTOM_BYPASS, +} + ATTR_PREVIOUS_STATE = "previous_state" ATTR_NEXT_STATE = "next_state" @@ -128,6 +137,9 @@ PLATFORM_SCHEMA = vol.Schema( vol.Optional( CONF_DISARM_AFTER_TRIGGER, default=DEFAULT_DISARM_AFTER_TRIGGER ): cv.boolean, + vol.Optional(CONF_ARMING_STATES, default=SUPPORTED_ARMING_STATES): vol.All( + cv.ensure_list, [vol.In(SUPPORTED_ARMING_STATES)] + ), vol.Optional(STATE_ALARM_ARMED_AWAY, default={}): _state_schema( STATE_ALARM_ARMED_AWAY ), @@ -188,14 +200,6 @@ class ManualAlarm(AlarmControlPanelEntity, RestoreEntity): """ _attr_should_poll = False - _attr_supported_features = ( - AlarmControlPanelEntityFeature.ARM_HOME - | AlarmControlPanelEntityFeature.ARM_AWAY - | AlarmControlPanelEntityFeature.ARM_NIGHT - | AlarmControlPanelEntityFeature.ARM_VACATION - | AlarmControlPanelEntityFeature.TRIGGER - | AlarmControlPanelEntityFeature.ARM_CUSTOM_BYPASS - ) def __init__( self, @@ -233,6 +237,12 @@ class ManualAlarm(AlarmControlPanelEntity, RestoreEntity): state: config[state][CONF_ARMING_TIME] for state in SUPPORTED_ARMING_STATES } + self._attr_supported_features = AlarmControlPanelEntityFeature.TRIGGER + for arming_state in config.get(CONF_ARMING_STATES, SUPPORTED_ARMING_STATES): + self._attr_supported_features |= SUPPORTED_ARMING_STATE_TO_FEATURE[ + arming_state + ] + @property def state(self) -> str: """Return the state of the device.""" diff --git a/tests/components/manual/test_alarm_control_panel.py b/tests/components/manual/test_alarm_control_panel.py index 5910cc3ec9b..6c9ba9ee9a0 100644 --- a/tests/components/manual/test_alarm_control_panel.py +++ b/tests/components/manual/test_alarm_control_panel.py @@ -7,6 +7,7 @@ from freezegun import freeze_time import pytest from homeassistant.components import alarm_control_panel +from homeassistant.components.alarm_control_panel import AlarmControlPanelEntityFeature from homeassistant.components.demo import alarm_control_panel as demo from homeassistant.const import ( ATTR_CODE, @@ -1456,3 +1457,70 @@ async def test_restore_state_triggered_long_ago(hass: HomeAssistant) -> None: state = hass.states.get(entity_id) assert state.state == STATE_ALARM_DISARMED + + +async def test_default_arming_states(hass: HomeAssistant) -> None: + """Test default arming_states.""" + assert await async_setup_component( + hass, + alarm_control_panel.DOMAIN, + { + "alarm_control_panel": { + "platform": "manual", + "name": "test", + } + }, + ) + await hass.async_block_till_done() + + state = hass.states.get("alarm_control_panel.test") + assert state.attributes["supported_features"] == ( + AlarmControlPanelEntityFeature.ARM_HOME + | AlarmControlPanelEntityFeature.ARM_AWAY + | AlarmControlPanelEntityFeature.ARM_NIGHT + | AlarmControlPanelEntityFeature.ARM_VACATION + | AlarmControlPanelEntityFeature.TRIGGER + | AlarmControlPanelEntityFeature.ARM_CUSTOM_BYPASS + ) + + +async def test_arming_states(hass: HomeAssistant) -> None: + """Test arming_states.""" + assert await async_setup_component( + hass, + alarm_control_panel.DOMAIN, + { + "alarm_control_panel": { + "platform": "manual", + "name": "test", + "arming_states": ["armed_away", "armed_home"], + } + }, + ) + await hass.async_block_till_done() + + state = hass.states.get("alarm_control_panel.test") + assert state.attributes["supported_features"] == ( + AlarmControlPanelEntityFeature.ARM_HOME + | AlarmControlPanelEntityFeature.ARM_AWAY + | AlarmControlPanelEntityFeature.TRIGGER + ) + + +async def test_invalid_arming_states(hass: HomeAssistant) -> None: + """Test invalid arming_states.""" + assert await async_setup_component( + hass, + alarm_control_panel.DOMAIN, + { + "alarm_control_panel": { + "platform": "manual", + "name": "test", + "arming_states": ["invalid", "armed_home"], + } + }, + ) + await hass.async_block_till_done() + + state = hass.states.get("alarm_control_panel.test") + assert state is None From ad7097399ed17217a9e240b150039dfd827123fb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 8 Jun 2024 16:07:39 -0500 Subject: [PATCH 0398/1445] Ensure multiple executions of a restart automation in the same event loop iteration are allowed (#119100) * Add test for restarting automation related issue #119097 * fix * add a delay since restart is an infinite loop * tests --- homeassistant/helpers/script.py | 4 - tests/components/automation/test_init.py | 137 ++++++++++++++++++++++- 2 files changed, 136 insertions(+), 5 deletions(-) diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index 61cb8852334..84dabb114cd 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -1759,10 +1759,6 @@ class Script: # runs before sleeping as otherwise if two runs are started at the exact # same time they will cancel each other out. self._log("Restarting") - # Important: yield to the event loop to allow the script to start in case - # the script is restarting itself so it ends up in the script stack and - # the recursion check above will prevent the script from running. - await asyncio.sleep(0) await self.async_stop(update_state=False, spare=run) if started_action: diff --git a/tests/components/automation/test_init.py b/tests/components/automation/test_init.py index a8e89d0ad97..b4d9e45b7d3 100644 --- a/tests/components/automation/test_init.py +++ b/tests/components/automation/test_init.py @@ -2771,6 +2771,7 @@ async def test_recursive_automation_starting_script( ], "action": [ {"service": "test.automation_started"}, + {"delay": 0.001}, {"service": "script.script1"}, ], } @@ -2817,7 +2818,10 @@ async def test_recursive_automation_starting_script( assert script_warning_msg in caplog.text -@pytest.mark.parametrize("automation_mode", SCRIPT_MODE_CHOICES) +@pytest.mark.parametrize( + "automation_mode", + [mode for mode in SCRIPT_MODE_CHOICES if mode != SCRIPT_MODE_RESTART], +) @pytest.mark.parametrize("wait_for_stop_scripts_after_shutdown", [True]) async def test_recursive_automation( hass: HomeAssistant, automation_mode, caplog: pytest.LogCaptureFixture @@ -2878,6 +2882,68 @@ async def test_recursive_automation( assert "Disallowed recursion detected" not in caplog.text +@pytest.mark.parametrize("wait_for_stop_scripts_after_shutdown", [True]) +async def test_recursive_automation_restart_mode( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Test automation restarting itself. + + The automation is an infinite loop since it keeps restarting itself + + - Illegal recursion detection should not be triggered + - Home Assistant should not hang on shut down + """ + stop_scripts_at_shutdown_called = asyncio.Event() + real_stop_scripts_at_shutdown = _async_stop_scripts_at_shutdown + + async def stop_scripts_at_shutdown(*args): + await real_stop_scripts_at_shutdown(*args) + stop_scripts_at_shutdown_called.set() + + with patch( + "homeassistant.helpers.script._async_stop_scripts_at_shutdown", + wraps=stop_scripts_at_shutdown, + ): + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "mode": SCRIPT_MODE_RESTART, + "trigger": [ + {"platform": "event", "event_type": "trigger_automation"}, + ], + "action": [ + {"event": "trigger_automation"}, + {"service": "test.automation_done"}, + ], + } + }, + ) + + service_called = asyncio.Event() + + async def async_service_handler(service): + if service.service == "automation_done": + service_called.set() + + hass.services.async_register("test", "automation_done", async_service_handler) + + hass.bus.async_fire("trigger_automation") + await asyncio.sleep(0) + + # Trigger 1st stage script shutdown + hass.set_state(CoreState.stopping) + hass.bus.async_fire("homeassistant_stop") + await asyncio.wait_for(stop_scripts_at_shutdown_called.wait(), 1) + + # Trigger 2nd stage script shutdown + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=90)) + await hass.async_block_till_done() + + assert "Disallowed recursion detected" not in caplog.text + + async def test_websocket_config( hass: HomeAssistant, hass_ws_client: WebSocketGenerator ) -> None: @@ -3095,3 +3161,72 @@ async def test_two_automations_call_restart_script_same_time( await hass.async_block_till_done() assert len(events) == 2 cancel() + + +async def test_two_automation_call_restart_script_right_after_each_other( + hass: HomeAssistant, +) -> None: + """Test two automations call a restart script right after each other.""" + + events = async_capture_events(hass, "repeat_test_script_finished") + + assert await async_setup_component( + hass, + input_boolean.DOMAIN, + { + input_boolean.DOMAIN: { + "test_1": None, + "test_2": None, + } + }, + ) + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "state", + "entity_id": ["input_boolean.test_1", "input_boolean.test_1"], + "from": "off", + "to": "on", + }, + "action": [ + { + "repeat": { + "count": 2, + "sequence": [ + { + "delay": { + "hours": 0, + "minutes": 0, + "seconds": 0, + "milliseconds": 100, + } + } + ], + } + }, + {"event": "repeat_test_script_finished", "event_data": {}}, + ], + "id": "automation_0", + "mode": "restart", + }, + ] + }, + ) + hass.states.async_set("input_boolean.test_1", "off") + hass.states.async_set("input_boolean.test_2", "off") + await hass.async_block_till_done() + hass.states.async_set("input_boolean.test_1", "on") + hass.states.async_set("input_boolean.test_2", "on") + await asyncio.sleep(0) + hass.states.async_set("input_boolean.test_1", "off") + hass.states.async_set("input_boolean.test_2", "off") + await asyncio.sleep(0) + hass.states.async_set("input_boolean.test_1", "on") + hass.states.async_set("input_boolean.test_2", "on") + await hass.async_block_till_done() + assert len(events) == 1 From b577ce61b84448ad0d34eaf327305a11a14977a9 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Sat, 8 Jun 2024 23:52:14 +0200 Subject: [PATCH 0399/1445] Use more conservative timeout values in Synology DSM (#119169) use ClientTimeout object --- homeassistant/components/synology_dsm/const.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/synology_dsm/const.py b/homeassistant/components/synology_dsm/const.py index 35d3008b416..11839caf8be 100644 --- a/homeassistant/components/synology_dsm/const.py +++ b/homeassistant/components/synology_dsm/const.py @@ -2,6 +2,7 @@ from __future__ import annotations +from aiohttp import ClientTimeout from synology_dsm.api.surveillance_station.const import SNAPSHOT_PROFILE_BALANCED from synology_dsm.exceptions import ( SynologyDSMAPIErrorException, @@ -40,7 +41,7 @@ DEFAULT_PORT = 5000 DEFAULT_PORT_SSL = 5001 # Options DEFAULT_SCAN_INTERVAL = 15 # min -DEFAULT_TIMEOUT = 30 # sec +DEFAULT_TIMEOUT = ClientTimeout(total=60, connect=15) DEFAULT_SNAPSHOT_QUALITY = SNAPSHOT_PROFILE_BALANCED ENTITY_UNIT_LOAD = "load" From a38d88730d604e0c677c5a5fda4b6b132697645f Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Sun, 9 Jun 2024 03:44:56 -0400 Subject: [PATCH 0400/1445] Remove Netgear LTE yaml import (#119180) Remove Netgear LTE yaml config --- .../components/netgear_lte/__init__.py | 165 ++---------------- .../components/netgear_lte/config_flow.py | 16 -- .../components/netgear_lte/strings.json | 10 -- .../netgear_lte/test_config_flow.py | 35 +--- 4 files changed, 14 insertions(+), 212 deletions(-) diff --git a/homeassistant/components/netgear_lte/__init__.py b/homeassistant/components/netgear_lte/__init__.py index 8c54cb96b3d..c47a5088887 100644 --- a/homeassistant/components/netgear_lte/__init__.py +++ b/homeassistant/components/netgear_lte/__init__.py @@ -5,25 +5,21 @@ from datetime import timedelta from aiohttp.cookiejar import CookieJar import attr import eternalegypt -import voluptuous as vol -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry, ConfigEntryState +from homeassistant.config_entries import ConfigEntry, ConfigEntryState from homeassistant.const import ( CONF_HOST, - CONF_MONITORED_CONDITIONS, CONF_NAME, CONF_PASSWORD, - CONF_RECIPIENT, EVENT_HOMEASSISTANT_STOP, Platform, ) -from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, Event, HomeAssistant +from homeassistant.core import Event, HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import config_validation as cv, discovery from homeassistant.helpers.aiohttp_client import async_create_clientsession from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.event import async_track_time_interval -from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType from .const import ( @@ -31,9 +27,6 @@ from .const import ( ATTR_HOST, ATTR_MESSAGE, ATTR_SMS_ID, - CONF_BINARY_SENSOR, - CONF_NOTIFY, - CONF_SENSOR, DATA_HASS_CONFIG, DISPATCHER_NETGEAR_LTE, DOMAIN, @@ -67,60 +60,14 @@ ALL_BINARY_SENSORS = [ "mobile_connected", ] - -NOTIFY_SCHEMA = vol.Schema( - { - vol.Optional(CONF_NAME, default=DOMAIN): cv.string, - vol.Optional(CONF_RECIPIENT, default=[]): vol.All(cv.ensure_list, [cv.string]), - } -) - -SENSOR_SCHEMA = vol.Schema( - { - vol.Optional(CONF_MONITORED_CONDITIONS, default=["usage"]): vol.All( - cv.ensure_list, [vol.In(ALL_SENSORS)] - ) - } -) - -BINARY_SENSOR_SCHEMA = vol.Schema( - { - vol.Optional(CONF_MONITORED_CONDITIONS, default=["mobile_connected"]): vol.All( - cv.ensure_list, [vol.In(ALL_BINARY_SENSORS)] - ) - } -) - -CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.All( - cv.ensure_list, - [ - vol.Schema( - { - vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_NOTIFY, default={}): vol.All( - cv.ensure_list, [NOTIFY_SCHEMA] - ), - vol.Optional(CONF_SENSOR, default={}): SENSOR_SCHEMA, - vol.Optional( - CONF_BINARY_SENSOR, default={} - ): BINARY_SENSOR_SCHEMA, - } - ) - ], - ) - }, - extra=vol.ALLOW_EXTRA, -) - PLATFORMS = [ Platform.BINARY_SENSOR, Platform.NOTIFY, Platform.SENSOR, ] +CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN) + @attr.s class ModemData: @@ -170,44 +117,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up Netgear LTE component.""" hass.data[DATA_HASS_CONFIG] = config - if lte_config := config.get(DOMAIN): - hass.async_create_task(import_yaml(hass, lte_config)) - return True -async def import_yaml(hass: HomeAssistant, lte_config: ConfigType) -> None: - """Import yaml if we can connect. Create appropriate issue registry entries.""" - for entry in lte_config: - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=entry - ) - if result.get("reason") == "cannot_connect": - async_create_issue( - hass, - DOMAIN, - "import_failure", - is_fixable=False, - severity=IssueSeverity.ERROR, - translation_key="import_failure", - ) - else: - async_create_issue( - hass, - HOMEASSISTANT_DOMAIN, - f"deprecated_yaml_{DOMAIN}", - breaks_in_ha_version="2024.7.0", - is_fixable=False, - issue_domain=DOMAIN, - severity=IssueSeverity.WARNING, - translation_key="deprecated_yaml", - translation_placeholders={ - "domain": DOMAIN, - "integration_title": "Netgear LTE", - }, - ) - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Netgear LTE from a config entry.""" host = entry.data[CONF_HOST] @@ -241,7 +153,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await async_setup_services(hass) - _legacy_task(hass, entry) + await discovery.async_load_platform( + hass, + Platform.NOTIFY, + DOMAIN, + {CONF_HOST: entry.data[CONF_HOST], CONF_NAME: entry.title}, + hass.data[DATA_HASS_CONFIG], + ) await hass.config_entries.async_forward_entry_setups( entry, [platform for platform in PLATFORMS if platform != Platform.NOTIFY] @@ -285,64 +203,3 @@ async def _login(hass: HomeAssistant, modem_data: ModemData, password: str) -> N await modem_data.async_update() hass.data[DOMAIN].modem_data[modem_data.host] = modem_data - - -def _legacy_task(hass: HomeAssistant, entry: ConfigEntry) -> None: - """Create notify service and add a repair issue when appropriate.""" - # Discovery can happen up to 2 times for notify depending on existing yaml config - # One for the name of the config entry, allows the user to customize the name - # One for each notify described in the yaml config which goes away with config flow - # One for the default if the user never specified one - hass.async_create_task( - discovery.async_load_platform( - hass, - Platform.NOTIFY, - DOMAIN, - {CONF_HOST: entry.data[CONF_HOST], CONF_NAME: entry.title}, - hass.data[DATA_HASS_CONFIG], - ) - ) - if not (lte_configs := hass.data[DATA_HASS_CONFIG].get(DOMAIN, [])): - return - async_create_issue( - hass, - DOMAIN, - "deprecated_notify", - breaks_in_ha_version="2024.7.0", - is_fixable=False, - severity=IssueSeverity.WARNING, - translation_key="deprecated_notify", - translation_placeholders={ - "name": f"{Platform.NOTIFY}.{entry.title.lower().replace(' ', '_')}" - }, - ) - - for lte_config in lte_configs: - if lte_config[CONF_HOST] == entry.data[CONF_HOST]: - if not lte_config[CONF_NOTIFY]: - hass.async_create_task( - discovery.async_load_platform( - hass, - Platform.NOTIFY, - DOMAIN, - {CONF_HOST: entry.data[CONF_HOST], CONF_NAME: DOMAIN}, - hass.data[DATA_HASS_CONFIG], - ) - ) - break - for notify_conf in lte_config[CONF_NOTIFY]: - discovery_info = { - CONF_HOST: lte_config[CONF_HOST], - CONF_NAME: notify_conf.get(CONF_NAME), - CONF_NOTIFY: notify_conf, - } - hass.async_create_task( - discovery.async_load_platform( - hass, - Platform.NOTIFY, - DOMAIN, - discovery_info, - hass.data[DATA_HASS_CONFIG], - ) - ) - break diff --git a/homeassistant/components/netgear_lte/config_flow.py b/homeassistant/components/netgear_lte/config_flow.py index fe411f79699..0b8f68246ca 100644 --- a/homeassistant/components/netgear_lte/config_flow.py +++ b/homeassistant/components/netgear_lte/config_flow.py @@ -20,22 +20,6 @@ from .const import DEFAULT_HOST, DOMAIN, LOGGER, MANUFACTURER class NetgearLTEFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a config flow for Netgear LTE.""" - async def async_step_import(self, config: dict[str, Any]) -> ConfigFlowResult: - """Import a configuration from config.yaml.""" - host = config[CONF_HOST] - password = config[CONF_PASSWORD] - self._async_abort_entries_match({CONF_HOST: host}) - try: - info = await self._async_validate_input(host, password) - except InputValidationError: - return self.async_abort(reason="cannot_connect") - await self.async_set_unique_id(info.serial_number) - self._abort_if_unique_id_configured() - return self.async_create_entry( - title=f"{MANUFACTURER} {info.items['general.devicename']}", - data={CONF_HOST: host, CONF_PASSWORD: password}, - ) - async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: diff --git a/homeassistant/components/netgear_lte/strings.json b/homeassistant/components/netgear_lte/strings.json index 5719d693d15..0b1446b33ca 100644 --- a/homeassistant/components/netgear_lte/strings.json +++ b/homeassistant/components/netgear_lte/strings.json @@ -17,16 +17,6 @@ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" } }, - "issues": { - "deprecated_notify": { - "title": "The Netgear LTE notify service is changing", - "description": "The Netgear LTE notify service was previously set up via YAML configuration.\n\nThis created a service for a specified recipient without having to include the phone number.\n\nPlease adjust any automations or scripts you may have to use the `{name}` service and include target for specifying a recipient." - }, - "import_failure": { - "title": "The Netgear LTE integration failed to import", - "description": "The Netgear LTE notify service was previously set up via YAML configuration.\n\nAn error occurred when trying to communicate with the device while attempting to import the configuration to the UI.\n\nPlease remove the Netgear LTE notify section from your YAML configuration and set it up in the UI instead." - } - }, "services": { "delete_sms": { "name": "Delete SMS", diff --git a/tests/components/netgear_lte/test_config_flow.py b/tests/components/netgear_lte/test_config_flow.py index 6b969e33475..16feb88172b 100644 --- a/tests/components/netgear_lte/test_config_flow.py +++ b/tests/components/netgear_lte/test_config_flow.py @@ -2,11 +2,9 @@ from unittest.mock import patch -import pytest - from homeassistant import data_entry_flow from homeassistant.components.netgear_lte.const import DOMAIN -from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER +from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_SOURCE from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -41,14 +39,13 @@ async def test_flow_user_form(hass: HomeAssistant, connection: None) -> None: assert result["context"]["unique_id"] == "FFFFFFFFFFFFF" -@pytest.mark.parametrize("source", [SOURCE_USER, SOURCE_IMPORT]) async def test_flow_already_configured( - hass: HomeAssistant, setup_integration: None, source: str + hass: HomeAssistant, setup_integration: None ) -> None: """Test config flow aborts when already configured.""" result = await hass.config_entries.flow.async_init( DOMAIN, - context={CONF_SOURCE: source}, + context={CONF_SOURCE: SOURCE_USER}, data=CONF_DATA, ) @@ -84,29 +81,3 @@ async def test_flow_user_unknown_error(hass: HomeAssistant, unknown: None) -> No assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "user" assert result["errors"]["base"] == "unknown" - - -async def test_flow_import(hass: HomeAssistant, connection: None) -> None: - """Test import step.""" - with _patch_setup(): - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={CONF_SOURCE: SOURCE_IMPORT}, - data=CONF_DATA, - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == "Netgear LM1200" - assert result["data"] == CONF_DATA - - -async def test_flow_import_failure(hass: HomeAssistant, cannot_connect: None) -> None: - """Test import step failure.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={CONF_SOURCE: SOURCE_IMPORT}, - data=CONF_DATA, - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "cannot_connect" From 9e7a6408c25a4156f3e03d9a1943a138cd46b8fd Mon Sep 17 00:00:00 2001 From: tronikos Date: Sun, 9 Jun 2024 00:45:59 -0700 Subject: [PATCH 0401/1445] Bump opower to 0.4.7 (#119183) --- homeassistant/components/opower/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/opower/manifest.json b/homeassistant/components/opower/manifest.json index 7e16bacdfda..d419fdcb043 100644 --- a/homeassistant/components/opower/manifest.json +++ b/homeassistant/components/opower/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/opower", "iot_class": "cloud_polling", "loggers": ["opower"], - "requirements": ["opower==0.4.6"] + "requirements": ["opower==0.4.7"] } diff --git a/requirements_all.txt b/requirements_all.txt index 00b10baaced..fc80c0d87bf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1504,7 +1504,7 @@ openwrt-luci-rpc==1.1.17 openwrt-ubus-rpc==0.0.2 # homeassistant.components.opower -opower==0.4.6 +opower==0.4.7 # homeassistant.components.oralb oralb-ble==0.17.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index dfc3cb7b674..df36cf3c3cf 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1207,7 +1207,7 @@ openhomedevice==2.2.0 openwebifpy==4.2.4 # homeassistant.components.opower -opower==0.4.6 +opower==0.4.7 # homeassistant.components.oralb oralb-ble==0.17.6 From f32b29e700e4265fa05c40338a32c2b8415f7fd5 Mon Sep 17 00:00:00 2001 From: Jakob Schlyter Date: Sun, 9 Jun 2024 11:57:54 +0200 Subject: [PATCH 0402/1445] Add myself as codeowner for `amazon_polly` (#119189) --- CODEOWNERS | 1 + homeassistant/components/amazon_polly/manifest.json | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CODEOWNERS b/CODEOWNERS index 3df0b4e54cf..f3a33c394ca 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -88,6 +88,7 @@ build.json @home-assistant/supervisor /tests/components/alert/ @home-assistant/core @frenck /homeassistant/components/alexa/ @home-assistant/cloud @ochlocracy @jbouwh /tests/components/alexa/ @home-assistant/cloud @ochlocracy @jbouwh +/homeassistant/components/amazon_polly/ @jschlyter /homeassistant/components/amberelectric/ @madpilot /tests/components/amberelectric/ @madpilot /homeassistant/components/ambient_network/ @thomaskistler diff --git a/homeassistant/components/amazon_polly/manifest.json b/homeassistant/components/amazon_polly/manifest.json index 803bf8b80aa..73bbdd67162 100644 --- a/homeassistant/components/amazon_polly/manifest.json +++ b/homeassistant/components/amazon_polly/manifest.json @@ -1,7 +1,7 @@ { "domain": "amazon_polly", "name": "Amazon Polly", - "codeowners": [], + "codeowners": ["@jschlyter"], "documentation": "https://www.home-assistant.io/integrations/amazon_polly", "iot_class": "cloud_push", "loggers": ["boto3", "botocore", "s3transfer"], From b937fc0cfea3e249c287d758e6ae3599735060f1 Mon Sep 17 00:00:00 2001 From: Sid <27780930+autinerd@users.noreply.github.com> Date: Sun, 9 Jun 2024 11:59:14 +0200 Subject: [PATCH 0403/1445] Add fallback to entry_id when no mac address is retrieved in enigma2 (#119185) --- homeassistant/components/enigma2/media_player.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/enigma2/media_player.py b/homeassistant/components/enigma2/media_player.py index adda8f9e1c8..8e090e7cecb 100644 --- a/homeassistant/components/enigma2/media_player.py +++ b/homeassistant/components/enigma2/media_player.py @@ -142,10 +142,10 @@ class Enigma2Device(MediaPlayerEntity): self._device: OpenWebIfDevice = device self._entry = entry - self._attr_unique_id = device.mac_address + self._attr_unique_id = device.mac_address or entry.entry_id self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, device.mac_address)}, + identifiers={(DOMAIN, self._attr_unique_id)}, manufacturer=about["info"]["brand"], model=about["info"]["model"], configuration_url=device.base, From 04222c32b53cd5dac15c7730cc64a532a694434c Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Sun, 9 Jun 2024 12:59:40 +0300 Subject: [PATCH 0404/1445] Handle Shelly BLE errors during connect and disconnect (#119174) --- homeassistant/components/shelly/__init__.py | 9 +--- .../components/shelly/coordinator.py | 18 ++++++- tests/components/shelly/test_coordinator.py | 47 +++++++++++++++++++ 3 files changed, 64 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index 1bcd9c7c1e4..cc1ea5e81a6 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -2,7 +2,6 @@ from __future__ import annotations -import contextlib from typing import Final from aioshelly.block_device import BlockDevice @@ -301,13 +300,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ShellyConfigEntry) -> b entry, platforms ): if shelly_entry_data.rpc: - with contextlib.suppress(DeviceConnectionError): - # If the device is restarting or has gone offline before - # the ping/pong timeout happens, the shutdown command - # will fail, but we don't care since we are unloading - # and if we setup again, we will fix anything that is - # in an inconsistent state at that time. - await shelly_entry_data.rpc.shutdown() + await shelly_entry_data.rpc.shutdown() return unload_ok diff --git a/homeassistant/components/shelly/coordinator.py b/homeassistant/components/shelly/coordinator.py index b6ccc1540f1..3415f1b22db 100644 --- a/homeassistant/components/shelly/coordinator.py +++ b/homeassistant/components/shelly/coordinator.py @@ -627,7 +627,13 @@ class ShellyRpcCoordinator(ShellyCoordinatorBase[RpcDevice]): if self.connected: # Already connected return self.connected = True - await self._async_run_connected_events() + try: + await self._async_run_connected_events() + except DeviceConnectionError as err: + LOGGER.error( + "Error running connected events for device %s: %s", self.name, err + ) + self.last_update_success = False async def _async_run_connected_events(self) -> None: """Run connected events. @@ -701,10 +707,18 @@ class ShellyRpcCoordinator(ShellyCoordinatorBase[RpcDevice]): if self.device.connected: try: await async_stop_scanner(self.device) + await super().shutdown() except InvalidAuthError: self.entry.async_start_reauth(self.hass) return - await super().shutdown() + except DeviceConnectionError as err: + # If the device is restarting or has gone offline before + # the ping/pong timeout happens, the shutdown command + # will fail, but we don't care since we are unloading + # and if we setup again, we will fix anything that is + # in an inconsistent state at that time. + LOGGER.debug("Error during shutdown for device %s: %s", self.name, err) + return await self._async_disconnected(False) diff --git a/tests/components/shelly/test_coordinator.py b/tests/components/shelly/test_coordinator.py index 1dc45a98c44..cd750e53f0b 100644 --- a/tests/components/shelly/test_coordinator.py +++ b/tests/components/shelly/test_coordinator.py @@ -15,12 +15,14 @@ from homeassistant.components.shelly.const import ( ATTR_CLICK_TYPE, ATTR_DEVICE, ATTR_GENERATION, + CONF_BLE_SCANNER_MODE, DOMAIN, ENTRY_RELOAD_COOLDOWN, MAX_PUSH_UPDATE_FAILURES, RPC_RECONNECT_INTERVAL, SLEEP_PERIOD_MULTIPLIER, UPDATE_PERIOD_MULTIPLIER, + BLEScannerMode, ) from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState from homeassistant.const import ATTR_DEVICE_ID, STATE_ON, STATE_UNAVAILABLE @@ -485,6 +487,25 @@ async def test_rpc_reload_with_invalid_auth( assert flow["context"].get("entry_id") == entry.entry_id +async def test_rpc_connection_error_during_unload( + hass: HomeAssistant, mock_rpc_device: Mock, caplog: pytest.LogCaptureFixture +) -> None: + """Test RPC DeviceConnectionError suppressed during config entry unload.""" + entry = await init_integration(hass, 2) + + assert entry.state is ConfigEntryState.LOADED + + with patch( + "homeassistant.components.shelly.coordinator.async_stop_scanner", + side_effect=DeviceConnectionError, + ): + await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + assert "Error during shutdown for device" in caplog.text + assert entry.state is ConfigEntryState.NOT_LOADED + + async def test_rpc_click_event( hass: HomeAssistant, mock_rpc_device: Mock, @@ -713,6 +734,32 @@ async def test_rpc_reconnect_error( assert get_entity_state(hass, "switch.test_switch_0") == STATE_UNAVAILABLE +async def test_rpc_error_running_connected_events( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + mock_rpc_device: Mock, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test RPC error while running connected events.""" + with patch( + "homeassistant.components.shelly.coordinator.async_ensure_ble_enabled", + side_effect=DeviceConnectionError, + ): + await init_integration( + hass, 2, options={CONF_BLE_SCANNER_MODE: BLEScannerMode.ACTIVE} + ) + + assert "Error running connected events for device" in caplog.text + assert get_entity_state(hass, "switch.test_switch_0") == STATE_UNAVAILABLE + + # Move time to generate reconnect without error + freezer.tick(timedelta(seconds=RPC_RECONNECT_INTERVAL)) + async_fire_time_changed(hass) + await hass.async_block_till_done(wait_background_tasks=True) + + assert get_entity_state(hass, "switch.test_switch_0") == STATE_ON + + async def test_rpc_polling_connection_error( hass: HomeAssistant, freezer: FrozenDateTimeFactory, From 5829d9d8ab290633ddbe71871b15f42690ec8a9c Mon Sep 17 00:00:00 2001 From: Jonathan Jogenfors Date: Sun, 9 Jun 2024 12:03:15 +0200 Subject: [PATCH 0405/1445] Fix sia custom bypass arming in night mode (#119168) --- homeassistant/components/sia/alarm_control_panel.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sia/alarm_control_panel.py b/homeassistant/components/sia/alarm_control_panel.py index 8c995da542a..42ce81cbfc1 100644 --- a/homeassistant/components/sia/alarm_control_panel.py +++ b/homeassistant/components/sia/alarm_control_panel.py @@ -64,8 +64,8 @@ ENTITY_DESCRIPTION_ALARM = SIAAlarmControlPanelEntityDescription( "OS": STATE_ALARM_DISARMED, "NC": STATE_ALARM_ARMED_NIGHT, "NL": STATE_ALARM_ARMED_NIGHT, - "NE": STATE_ALARM_ARMED_CUSTOM_BYPASS, - "NF": STATE_ALARM_ARMED_CUSTOM_BYPASS, + "NE": STATE_ALARM_ARMED_NIGHT, + "NF": STATE_ALARM_ARMED_NIGHT, "BR": PREVIOUS_STATE, }, ) From ff493a8a9d74f01b3141b77945edce76eebf4907 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sun, 9 Jun 2024 12:25:06 +0200 Subject: [PATCH 0406/1445] Rewrite the UniFi button entity tests (#118771) --- tests/components/unifi/test_button.py | 434 ++++++++++++++------------ 1 file changed, 234 insertions(+), 200 deletions(-) diff --git a/tests/components/unifi/test_button.py b/tests/components/unifi/test_button.py index 7199a5f3ed6..08e9b52a2ca 100644 --- a/tests/components/unifi/test_button.py +++ b/tests/components/unifi/test_button.py @@ -1,12 +1,14 @@ """UniFi Network button platform tests.""" from datetime import timedelta +from typing import Any +from unittest.mock import patch import pytest from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, ButtonDeviceClass from homeassistant.components.unifi.const import CONF_SITE_ID -from homeassistant.config_entries import RELOAD_AFTER_UPDATE_DELAY +from homeassistant.config_entries import RELOAD_AFTER_UPDATE_DELAY, ConfigEntry from homeassistant.const import ( ATTR_DEVICE_CLASS, CONF_HOST, @@ -22,266 +24,298 @@ import homeassistant.util.dt as dt_util from tests.common import async_fire_time_changed from tests.test_util.aiohttp import AiohttpClientMocker -WLAN_ID = "_id" -WLAN = { - WLAN_ID: "012345678910111213141516", - "bc_filter_enabled": False, - "bc_filter_list": [], - "dtim_mode": "default", - "dtim_na": 1, - "dtim_ng": 1, - "enabled": True, - "group_rekey": 3600, - "mac_filter_enabled": False, - "mac_filter_list": [], - "mac_filter_policy": "allow", - "minrate_na_advertising_rates": False, - "minrate_na_beacon_rate_kbps": 6000, - "minrate_na_data_rate_kbps": 6000, - "minrate_na_enabled": False, - "minrate_na_mgmt_rate_kbps": 6000, - "minrate_ng_advertising_rates": False, - "minrate_ng_beacon_rate_kbps": 1000, - "minrate_ng_data_rate_kbps": 1000, - "minrate_ng_enabled": False, - "minrate_ng_mgmt_rate_kbps": 1000, - "name": "SSID 1", - "no2ghz_oui": False, - "schedule": [], - "security": "wpapsk", - "site_id": "5a32aa4ee4b0412345678910", - "usergroup_id": "012345678910111213141518", - "wep_idx": 1, - "wlangroup_id": "012345678910111213141519", - "wpa_enc": "ccmp", - "wpa_mode": "wpa2", - "x_iapp_key": "01234567891011121314151617181920", - "x_passphrase": "password", -} +RANDOM_TOKEN = "random_token" -@pytest.mark.parametrize( - "device_payload", - [ - [ +@pytest.fixture(autouse=True) +def mock_secret(): + """Mock secret.""" + with patch("secrets.token_urlsafe", return_value=RANDOM_TOKEN): + yield + + +DEVICE_RESTART = [ + { + "board_rev": 3, + "device_id": "mock-id", + "ip": "10.0.0.1", + "last_seen": 1562600145, + "mac": "00:00:00:00:01:01", + "model": "US16P150", + "name": "switch", + "state": 1, + "type": "usw", + "version": "4.0.42.10433", + } +] + +DEVICE_POWER_CYCLE_POE = [ + { + "board_rev": 3, + "device_id": "mock-id", + "ip": "10.0.0.1", + "last_seen": 1562600145, + "mac": "00:00:00:00:01:01", + "model": "US16P150", + "name": "switch", + "state": 1, + "type": "usw", + "version": "4.0.42.10433", + "port_table": [ { - "board_rev": 3, - "device_id": "mock-id", - "ip": "10.0.0.1", - "last_seen": 1562600145, - "mac": "00:00:00:00:01:01", - "model": "US16P150", - "name": "switch", - "state": 1, - "type": "usw", - "version": "4.0.42.10433", - } - ] - ], -) -async def test_restart_device_button( + "media": "GE", + "name": "Port 1", + "port_idx": 1, + "poe_caps": 7, + "poe_class": "Class 4", + "poe_enable": True, + "poe_mode": "auto", + "poe_power": "2.56", + "poe_voltage": "53.40", + "portconf_id": "1a1", + "port_poe": True, + "up": True, + }, + ], + } +] + +WLAN_REGENERATE_PASSWORD = [ + { + "_id": "012345678910111213141516", + "bc_filter_enabled": False, + "bc_filter_list": [], + "dtim_mode": "default", + "dtim_na": 1, + "dtim_ng": 1, + "enabled": True, + "group_rekey": 3600, + "mac_filter_enabled": False, + "mac_filter_list": [], + "mac_filter_policy": "allow", + "minrate_na_advertising_rates": False, + "minrate_na_beacon_rate_kbps": 6000, + "minrate_na_data_rate_kbps": 6000, + "minrate_na_enabled": False, + "minrate_na_mgmt_rate_kbps": 6000, + "minrate_ng_advertising_rates": False, + "minrate_ng_beacon_rate_kbps": 1000, + "minrate_ng_data_rate_kbps": 1000, + "minrate_ng_enabled": False, + "minrate_ng_mgmt_rate_kbps": 1000, + "name": "SSID 1", + "no2ghz_oui": False, + "schedule": [], + "security": "wpapsk", + "site_id": "5a32aa4ee4b0412345678910", + "usergroup_id": "012345678910111213141518", + "wep_idx": 1, + "wlangroup_id": "012345678910111213141519", + "wpa_enc": "ccmp", + "wpa_mode": "wpa2", + "x_iapp_key": "01234567891011121314151617181920", + "x_passphrase": "password", + } +] + + +async def _test_button_entity( hass: HomeAssistant, entity_registry: er.EntityRegistry, aioclient_mock: AiohttpClientMocker, - config_entry_setup, websocket_mock, + config_entry: ConfigEntry, + entity_count: int, + entity_id: str, + unique_id: str, + device_class: ButtonDeviceClass, + request_method: str, + request_path: str, + request_data: dict[str, Any], + call: dict[str, str], ) -> None: - """Test restarting device button.""" - config_entry = config_entry_setup - assert len(hass.states.async_entity_ids(BUTTON_DOMAIN)) == 1 + """Test button entity.""" + assert len(hass.states.async_entity_ids(BUTTON_DOMAIN)) == entity_count - ent_reg_entry = entity_registry.async_get("button.switch_restart") - assert ent_reg_entry.unique_id == "device_restart-00:00:00:00:01:01" + ent_reg_entry = entity_registry.async_get(entity_id) + assert ent_reg_entry.unique_id == unique_id assert ent_reg_entry.entity_category is EntityCategory.CONFIG # Validate state object - button = hass.states.get("button.switch_restart") + button = hass.states.get(entity_id) assert button is not None - assert button.attributes.get(ATTR_DEVICE_CLASS) == ButtonDeviceClass.RESTART + assert button.attributes.get(ATTR_DEVICE_CLASS) == device_class - # Send restart device command + # Send and validate device command aioclient_mock.clear_requests() - aioclient_mock.post( + aioclient_mock.request( + request_method, f"https://{config_entry.data[CONF_HOST]}:1234" - f"/api/s/{config_entry.data[CONF_SITE_ID]}/cmd/devmgr", + f"/api/s/{config_entry.data[CONF_SITE_ID]}{request_path}", + **request_data, ) await hass.services.async_call( - BUTTON_DOMAIN, - "press", - {"entity_id": "button.switch_restart"}, - blocking=True, + BUTTON_DOMAIN, "press", {"entity_id": entity_id}, blocking=True ) assert aioclient_mock.call_count == 1 - assert aioclient_mock.mock_calls[0][2] == { - "cmd": "restart", - "mac": "00:00:00:00:01:01", - "reboot_type": "soft", - } + assert aioclient_mock.mock_calls[0][2] == call # Availability signalling # Controller disconnects await websocket_mock.disconnect() - assert hass.states.get("button.switch_restart").state == STATE_UNAVAILABLE + assert hass.states.get(entity_id).state == STATE_UNAVAILABLE # Controller reconnects await websocket_mock.reconnect() - assert hass.states.get("button.switch_restart").state != STATE_UNAVAILABLE + assert hass.states.get(entity_id).state != STATE_UNAVAILABLE @pytest.mark.parametrize( - "device_payload", + ( + "device_payload", + "entity_count", + "entity_id", + "unique_id", + "device_class", + "request_method", + "request_path", + "call", + ), [ - [ + ( + DEVICE_RESTART, + 1, + "button.switch_restart", + "device_restart-00:00:00:00:01:01", + ButtonDeviceClass.RESTART, + "post", + "/cmd/devmgr", { - "board_rev": 3, - "device_id": "mock-id", - "ip": "10.0.0.1", - "last_seen": 1562600145, + "cmd": "restart", "mac": "00:00:00:00:01:01", - "model": "US16P150", - "name": "switch", - "state": 1, - "type": "usw", - "version": "4.0.42.10433", - "port_table": [ - { - "media": "GE", - "name": "Port 1", - "port_idx": 1, - "poe_caps": 7, - "poe_class": "Class 4", - "poe_enable": True, - "poe_mode": "auto", - "poe_power": "2.56", - "poe_voltage": "53.40", - "portconf_id": "1a1", - "port_poe": True, - "up": True, - }, - ], - } - ] + "reboot_type": "soft", + }, + ), + ( + DEVICE_POWER_CYCLE_POE, + 2, + "button.switch_port_1_power_cycle", + "power_cycle-00:00:00:00:01:01_1", + ButtonDeviceClass.RESTART, + "post", + "/cmd/devmgr", + { + "cmd": "power-cycle", + "mac": "00:00:00:00:01:01", + "port_idx": 1, + }, + ), ], ) -async def test_power_cycle_poe( +async def test_device_button_entities( hass: HomeAssistant, entity_registry: er.EntityRegistry, aioclient_mock: AiohttpClientMocker, config_entry_setup, websocket_mock, + entity_count: int, + entity_id: str, + unique_id: str, + device_class: ButtonDeviceClass, + request_method: str, + request_path: str, + call: dict[str, str], ) -> None: - """Test restarting device button.""" - config_entry = config_entry_setup - assert len(hass.states.async_entity_ids(BUTTON_DOMAIN)) == 2 - - ent_reg_entry = entity_registry.async_get("button.switch_port_1_power_cycle") - assert ent_reg_entry.unique_id == "power_cycle-00:00:00:00:01:01_1" - assert ent_reg_entry.entity_category is EntityCategory.CONFIG - - # Validate state object - button = hass.states.get("button.switch_port_1_power_cycle") - assert button is not None - assert button.attributes.get(ATTR_DEVICE_CLASS) == ButtonDeviceClass.RESTART - - # Send restart device command - aioclient_mock.clear_requests() - aioclient_mock.post( - f"https://{config_entry.data[CONF_HOST]}:1234" - f"/api/s/{config_entry.data[CONF_SITE_ID]}/cmd/devmgr", - ) - - await hass.services.async_call( - BUTTON_DOMAIN, - "press", - {"entity_id": "button.switch_port_1_power_cycle"}, - blocking=True, - ) - assert aioclient_mock.call_count == 1 - assert aioclient_mock.mock_calls[0][2] == { - "cmd": "power-cycle", - "mac": "00:00:00:00:01:01", - "port_idx": 1, - } - - # Availability signalling - - # Controller disconnects - await websocket_mock.disconnect() - assert ( - hass.states.get("button.switch_port_1_power_cycle").state == STATE_UNAVAILABLE - ) - - # Controller reconnects - await websocket_mock.reconnect() - assert ( - hass.states.get("button.switch_port_1_power_cycle").state != STATE_UNAVAILABLE + """Test button entities based on device sources.""" + await _test_button_entity( + hass, + entity_registry, + aioclient_mock, + websocket_mock, + config_entry_setup, + entity_count, + entity_id, + unique_id, + device_class, + request_method, + request_path, + {}, + call, ) -@pytest.mark.parametrize("wlan_payload", [[WLAN]]) -async def test_wlan_regenerate_password( +@pytest.mark.parametrize( + ( + "wlan_payload", + "entity_count", + "entity_id", + "unique_id", + "device_class", + "request_method", + "request_path", + "request_data", + "call", + ), + [ + ( + WLAN_REGENERATE_PASSWORD, + 1, + "button.ssid_1_regenerate_password", + "regenerate_password-012345678910111213141516", + ButtonDeviceClass.UPDATE, + "put", + f"/rest/wlanconf/{WLAN_REGENERATE_PASSWORD[0]["_id"]}", + { + "json": {"data": "password changed successfully", "meta": {"rc": "ok"}}, + "headers": {"content-type": CONTENT_TYPE_JSON}, + }, + {"x_passphrase": RANDOM_TOKEN}, + ), + ], +) +async def test_wlan_button_entities( hass: HomeAssistant, entity_registry: er.EntityRegistry, aioclient_mock: AiohttpClientMocker, config_entry_setup, websocket_mock, + entity_count: int, + entity_id: str, + unique_id: str, + device_class: ButtonDeviceClass, + request_method: str, + request_path: str, + request_data: dict[str, Any], + call: dict[str, str], ) -> None: - """Test WLAN regenerate password button.""" - config_entry = config_entry_setup + """Test button entities based on WLAN sources.""" assert len(hass.states.async_entity_ids(BUTTON_DOMAIN)) == 0 - button_regenerate_password = "button.ssid_1_regenerate_password" - - ent_reg_entry = entity_registry.async_get(button_regenerate_password) - assert ent_reg_entry.unique_id == "regenerate_password-012345678910111213141516" + ent_reg_entry = entity_registry.async_get(entity_id) assert ent_reg_entry.disabled_by == RegistryEntryDisabler.INTEGRATION - assert ent_reg_entry.entity_category is EntityCategory.CONFIG # Enable entity - entity_registry.async_update_entity( - entity_id=button_regenerate_password, disabled_by=None - ) - await hass.async_block_till_done() - + entity_registry.async_update_entity(entity_id=entity_id, disabled_by=None) async_fire_time_changed( hass, dt_util.utcnow() + timedelta(seconds=RELOAD_AFTER_UPDATE_DELAY + 1), ) await hass.async_block_till_done() - assert len(hass.states.async_entity_ids(BUTTON_DOMAIN)) == 1 - - # Validate state object - button = hass.states.get(button_regenerate_password) - assert button is not None - assert button.attributes.get(ATTR_DEVICE_CLASS) == ButtonDeviceClass.UPDATE - - aioclient_mock.clear_requests() - aioclient_mock.put( - f"https://{config_entry.data[CONF_HOST]}:1234" - f"/api/s/{config_entry.data[CONF_SITE_ID]}/rest/wlanconf/{WLAN[WLAN_ID]}", - json={"data": "password changed successfully", "meta": {"rc": "ok"}}, - headers={"content-type": CONTENT_TYPE_JSON}, + await _test_button_entity( + hass, + entity_registry, + aioclient_mock, + websocket_mock, + config_entry_setup, + entity_count, + entity_id, + unique_id, + device_class, + request_method, + request_path, + request_data, + call, ) - - # Send WLAN regenerate password command - await hass.services.async_call( - BUTTON_DOMAIN, - "press", - {"entity_id": button_regenerate_password}, - blocking=True, - ) - assert aioclient_mock.call_count == 1 - assert next(iter(aioclient_mock.mock_calls[0][2])) == "x_passphrase" - - # Availability signalling - - # Controller disconnects - await websocket_mock.disconnect() - assert hass.states.get(button_regenerate_password).state == STATE_UNAVAILABLE - - # Controller reconnects - await websocket_mock.reconnect() - assert hass.states.get(button_regenerate_password).state != STATE_UNAVAILABLE From d9f1d40805f1b15e07289cef0c8529b2988f0162 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 9 Jun 2024 05:30:41 -0500 Subject: [PATCH 0407/1445] Migrate august to use yalexs 5.2.0 (#119178) --- homeassistant/components/august/__init__.py | 60 ++--- homeassistant/components/august/activity.py | 231 ------------------ .../components/august/config_flow.py | 7 +- homeassistant/components/august/exceptions.py | 15 -- homeassistant/components/august/gateway.py | 137 +---------- homeassistant/components/august/manifest.json | 2 +- homeassistant/components/august/subscriber.py | 98 -------- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/august/mocks.py | 9 +- tests/components/august/test_config_flow.py | 22 +- tests/components/august/test_gateway.py | 13 +- tests/components/august/test_lock.py | 2 +- 13 files changed, 65 insertions(+), 535 deletions(-) delete mode 100644 homeassistant/components/august/activity.py delete mode 100644 homeassistant/components/august/exceptions.py delete mode 100644 homeassistant/components/august/subscriber.py diff --git a/homeassistant/components/august/__init__.py b/homeassistant/components/august/__init__.py index 89595fdebc4..c21bfbc1042 100644 --- a/homeassistant/components/august/__init__.py +++ b/homeassistant/components/august/__init__.py @@ -7,33 +7,37 @@ from collections.abc import Callable, Coroutine, Iterable, ValuesView from datetime import datetime from itertools import chain import logging -from typing import Any +from typing import Any, cast from aiohttp import ClientError, ClientResponseError +from path import Path from yalexs.activity import ActivityTypes from yalexs.const import DEFAULT_BRAND from yalexs.doorbell import Doorbell, DoorbellDetail from yalexs.exceptions import AugustApiAIOHTTPError from yalexs.lock import Lock, LockDetail +from yalexs.manager.activity import ActivityStream +from yalexs.manager.const import CONF_BRAND +from yalexs.manager.exceptions import CannotConnect, InvalidAuth, RequireValidation +from yalexs.manager.gateway import Config as YaleXSConfig +from yalexs.manager.subscriber import SubscriberMixin from yalexs.pubnub_activity import activities_from_pubnub_message from yalexs.pubnub_async import AugustPubNub, async_create_pubnub from yalexs_ble import YaleXSBLEDiscovery from homeassistant.config_entries import SOURCE_INTEGRATION_DISCOVERY, ConfigEntry -from homeassistant.const import CONF_PASSWORD -from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback +from homeassistant.const import CONF_PASSWORD, EVENT_HOMEASSISTANT_STOP +from homeassistant.core import Event, HomeAssistant, callback from homeassistant.exceptions import ( ConfigEntryAuthFailed, ConfigEntryNotReady, HomeAssistantError, ) from homeassistant.helpers import device_registry as dr, discovery_flow +from homeassistant.util.async_ import create_eager_task -from .activity import ActivityStream -from .const import CONF_BRAND, DOMAIN, MIN_TIME_BETWEEN_DETAIL_UPDATES, PLATFORMS -from .exceptions import CannotConnect, InvalidAuth, RequireValidation +from .const import DOMAIN, MIN_TIME_BETWEEN_DETAIL_UPDATES, PLATFORMS from .gateway import AugustGateway -from .subscriber import AugustSubscriberMixin from .util import async_create_august_clientsession _LOGGER = logging.getLogger(__name__) @@ -52,10 +56,8 @@ type AugustConfigEntry = ConfigEntry[AugustData] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up August from a config entry.""" session = async_create_august_clientsession(hass) - august_gateway = AugustGateway(hass, session) - + august_gateway = AugustGateway(Path(hass.config.config_dir), session) try: - await august_gateway.async_setup(entry.data) return await async_setup_august(hass, entry, august_gateway) except (RequireValidation, InvalidAuth) as err: raise ConfigEntryAuthFailed from err @@ -67,7 +69,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: AugustConfigEntry) -> bool: """Unload a config entry.""" - entry.runtime_data.async_stop() return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) @@ -75,6 +76,8 @@ async def async_setup_august( hass: HomeAssistant, config_entry: AugustConfigEntry, august_gateway: AugustGateway ) -> bool: """Set up the August component.""" + config = cast(YaleXSConfig, config_entry.data) + await august_gateway.async_setup(config) if CONF_PASSWORD in config_entry.data: # We no longer need to store passwords since we do not @@ -116,7 +119,7 @@ def _async_trigger_ble_lock_discovery( ) -class AugustData(AugustSubscriberMixin): +class AugustData(SubscriberMixin): """August data object.""" def __init__( @@ -126,17 +129,17 @@ class AugustData(AugustSubscriberMixin): august_gateway: AugustGateway, ) -> None: """Init August data object.""" - super().__init__(hass, MIN_TIME_BETWEEN_DETAIL_UPDATES) + super().__init__(MIN_TIME_BETWEEN_DETAIL_UPDATES) self._config_entry = config_entry self._hass = hass self._august_gateway = august_gateway - self.activity_stream: ActivityStream = None # type: ignore[assignment] + self.activity_stream: ActivityStream = None self._api = august_gateway.api self._device_detail_by_id: dict[str, LockDetail | DoorbellDetail] = {} self._doorbells_by_id: dict[str, Doorbell] = {} self._locks_by_id: dict[str, Lock] = {} self._house_ids: set[str] = set() - self._pubnub_unsub: CALLBACK_TYPE | None = None + self._pubnub_unsub: Callable[[], Coroutine[Any, Any, None]] | None = None @property def brand(self) -> str: @@ -148,13 +151,8 @@ class AugustData(AugustSubscriberMixin): token = self._august_gateway.access_token # This used to be a gather but it was less reliable with august's recent api changes. user_data = await self._api.async_get_user(token) - locks: list[Lock] = await self._api.async_get_operable_locks(token) - doorbells: list[Doorbell] = await self._api.async_get_doorbells(token) - if not doorbells: - doorbells = [] - if not locks: - locks = [] - + locks: list[Lock] = await self._api.async_get_operable_locks(token) or [] + doorbells: list[Doorbell] = await self._api.async_get_doorbells(token) or [] self._doorbells_by_id = {device.device_id: device for device in doorbells} self._locks_by_id = {device.device_id: device for device in locks} self._house_ids = {device.house_id for device in chain(locks, doorbells)} @@ -175,9 +173,14 @@ class AugustData(AugustSubscriberMixin): pubnub.register_device(device) self.activity_stream = ActivityStream( - self._hass, self._api, self._august_gateway, self._house_ids, pubnub + self._api, self._august_gateway, self._house_ids, pubnub ) + self._config_entry.async_on_unload( + self._hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, self.async_stop) + ) + self._config_entry.async_on_unload(self.async_stop) await self.activity_stream.async_setup() + pubnub.subscribe(self.async_pubnub_message) self._pubnub_unsub = async_create_pubnub( user_data["UserID"], @@ -200,8 +203,10 @@ class AugustData(AugustSubscriberMixin): # awake when they come back online for result in await asyncio.gather( *[ - self.async_status_async( - device_id, bool(detail.bridge and detail.bridge.hyper_bridge) + create_eager_task( + self.async_status_async( + device_id, bool(detail.bridge and detail.bridge.hyper_bridge) + ) ) for device_id, detail in self._device_detail_by_id.items() if device_id in self._locks_by_id @@ -231,11 +236,10 @@ class AugustData(AugustSubscriberMixin): self.async_signal_device_id_update(device.device_id) activity_stream.async_schedule_house_id_refresh(device.house_id) - @callback - def async_stop(self) -> None: + async def async_stop(self, event: Event | None = None) -> None: """Stop the subscriptions.""" if self._pubnub_unsub: - self._pubnub_unsub() + await self._pubnub_unsub() self.activity_stream.async_stop() @property diff --git a/homeassistant/components/august/activity.py b/homeassistant/components/august/activity.py deleted file mode 100644 index ee180ab5480..00000000000 --- a/homeassistant/components/august/activity.py +++ /dev/null @@ -1,231 +0,0 @@ -"""Consume the august activity stream.""" - -from __future__ import annotations - -from datetime import datetime -from functools import partial -import logging -from time import monotonic - -from aiohttp import ClientError -from yalexs.activity import Activity, ActivityType -from yalexs.api_async import ApiAsync -from yalexs.pubnub_async import AugustPubNub -from yalexs.util import get_latest_activity - -from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback -from homeassistant.helpers.debounce import Debouncer -from homeassistant.helpers.event import async_call_later -from homeassistant.util.dt import utcnow - -from .const import ACTIVITY_UPDATE_INTERVAL -from .gateway import AugustGateway -from .subscriber import AugustSubscriberMixin - -_LOGGER = logging.getLogger(__name__) - -ACTIVITY_STREAM_FETCH_LIMIT = 10 -ACTIVITY_CATCH_UP_FETCH_LIMIT = 2500 - -INITIAL_LOCK_RESYNC_TIME = 60 - -# If there is a storm of activity (ie lock, unlock, door open, door close, etc) -# we want to debounce the updates so we don't hammer the activity api too much. -ACTIVITY_DEBOUNCE_COOLDOWN = 4 - - -@callback -def _async_cancel_future_scheduled_updates(cancels: list[CALLBACK_TYPE]) -> None: - """Cancel future scheduled updates.""" - for cancel in cancels: - cancel() - cancels.clear() - - -class ActivityStream(AugustSubscriberMixin): - """August activity stream handler.""" - - def __init__( - self, - hass: HomeAssistant, - api: ApiAsync, - august_gateway: AugustGateway, - house_ids: set[str], - pubnub: AugustPubNub, - ) -> None: - """Init August activity stream object.""" - super().__init__(hass, ACTIVITY_UPDATE_INTERVAL) - self._hass = hass - self._schedule_updates: dict[str, list[CALLBACK_TYPE]] = {} - self._august_gateway = august_gateway - self._api = api - self._house_ids = house_ids - self._latest_activities: dict[str, dict[ActivityType, Activity]] = {} - self._did_first_update = False - self.pubnub = pubnub - self._update_debounce: dict[str, Debouncer] = {} - self._update_debounce_jobs: dict[str, HassJob] = {} - self._start_time: float | None = None - - @callback - def _async_update_house_id_later(self, debouncer: Debouncer, _: datetime) -> None: - """Call a debouncer from async_call_later.""" - debouncer.async_schedule_call() - - async def async_setup(self) -> None: - """Token refresh check and catch up the activity stream.""" - self._start_time = monotonic() - update_debounce = self._update_debounce - update_debounce_jobs = self._update_debounce_jobs - for house_id in self._house_ids: - debouncer = Debouncer( - self._hass, - _LOGGER, - cooldown=ACTIVITY_DEBOUNCE_COOLDOWN, - immediate=True, - function=partial(self._async_update_house_id, house_id), - background=True, - ) - update_debounce[house_id] = debouncer - update_debounce_jobs[house_id] = HassJob( - partial(self._async_update_house_id_later, debouncer), - f"debounced august activity update for {house_id}", - cancel_on_shutdown=True, - ) - - await self._async_refresh(utcnow()) - self._did_first_update = True - - @callback - def async_stop(self) -> None: - """Cleanup any debounces.""" - for debouncer in self._update_debounce.values(): - debouncer.async_cancel() - for cancels in self._schedule_updates.values(): - _async_cancel_future_scheduled_updates(cancels) - - def get_latest_device_activity( - self, device_id: str, activity_types: set[ActivityType] - ) -> Activity | None: - """Return latest activity that is one of the activity_types.""" - if not (latest_device_activities := self._latest_activities.get(device_id)): - return None - - latest_activity: Activity | None = None - - for activity_type in activity_types: - if activity := latest_device_activities.get(activity_type): - if ( - latest_activity - and activity.activity_start_time - <= latest_activity.activity_start_time - ): - continue - latest_activity = activity - - return latest_activity - - async def _async_refresh(self, time: datetime) -> None: - """Update the activity stream from August.""" - # This is the only place we refresh the api token - await self._august_gateway.async_refresh_access_token_if_needed() - if self.pubnub.connected: - _LOGGER.debug("Skipping update because pubnub is connected") - return - _LOGGER.debug("Start retrieving device activities") - # Await in sequence to avoid hammering the API - for debouncer in self._update_debounce.values(): - await debouncer.async_call() - - @callback - def async_schedule_house_id_refresh(self, house_id: str) -> None: - """Update for a house activities now and once in the future.""" - if future_updates := self._schedule_updates.setdefault(house_id, []): - _async_cancel_future_scheduled_updates(future_updates) - - debouncer = self._update_debounce[house_id] - debouncer.async_schedule_call() - - # Schedule two updates past the debounce time - # to ensure we catch the case where the activity - # api does not update right away and we need to poll - # it again. Sometimes the lock operator or a doorbell - # will not show up in the activity stream right away. - # Only do additional polls if we are past - # the initial lock resync time to avoid a storm - # of activity at setup. - if ( - not self._start_time - or monotonic() - self._start_time < INITIAL_LOCK_RESYNC_TIME - ): - _LOGGER.debug( - "Skipping additional updates due to ongoing initial lock resync time" - ) - return - - _LOGGER.debug("Scheduling additional updates for house id %s", house_id) - job = self._update_debounce_jobs[house_id] - for step in (1, 2): - future_updates.append( - async_call_later( - self._hass, - (step * ACTIVITY_DEBOUNCE_COOLDOWN) + 0.1, - job, - ) - ) - - async def _async_update_house_id(self, house_id: str) -> None: - """Update device activities for a house.""" - if self._did_first_update: - limit = ACTIVITY_STREAM_FETCH_LIMIT - else: - limit = ACTIVITY_CATCH_UP_FETCH_LIMIT - - _LOGGER.debug("Updating device activity for house id %s", house_id) - try: - activities = await self._api.async_get_house_activities( - self._august_gateway.access_token, house_id, limit=limit - ) - except ClientError as ex: - _LOGGER.error( - "Request error trying to retrieve activity for house id %s: %s", - house_id, - ex, - ) - # Make sure we process the next house if one of them fails - return - - _LOGGER.debug( - "Completed retrieving device activities for house id %s", house_id - ) - for device_id in self.async_process_newer_device_activities(activities): - _LOGGER.debug( - "async_signal_device_id_update (from activity stream): %s", - device_id, - ) - self.async_signal_device_id_update(device_id) - - def async_process_newer_device_activities( - self, activities: list[Activity] - ) -> set[str]: - """Process activities if they are newer than the last one.""" - updated_device_ids = set() - latest_activities = self._latest_activities - for activity in activities: - device_id = activity.device_id - activity_type = activity.activity_type - device_activities = latest_activities.setdefault(device_id, {}) - # Ignore activities that are older than the latest one unless it is a non - # locking or unlocking activity with the exact same start time. - last_activity = device_activities.get(activity_type) - # The activity stream can have duplicate activities. So we need - # to call get_latest_activity to figure out if if the activity - # is actually newer than the last one. - latest_activity = get_latest_activity(activity, last_activity) - if latest_activity != activity: - continue - - device_activities[activity_type] = activity - updated_device_ids.add(device_id) - - return updated_device_ids diff --git a/homeassistant/components/august/config_flow.py b/homeassistant/components/august/config_flow.py index 08401e15b84..75543311fdd 100644 --- a/homeassistant/components/august/config_flow.py +++ b/homeassistant/components/august/config_flow.py @@ -3,12 +3,14 @@ from collections.abc import Mapping from dataclasses import dataclass import logging +from pathlib import Path from typing import Any import aiohttp import voluptuous as vol from yalexs.authenticator import ValidationResult from yalexs.const import BRANDS, DEFAULT_BRAND +from yalexs.manager.exceptions import CannotConnect, InvalidAuth, RequireValidation from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_PASSWORD, CONF_USERNAME @@ -23,7 +25,6 @@ from .const import ( LOGIN_METHODS, VERIFICATION_CODE_KEY, ) -from .exceptions import CannotConnect, InvalidAuth, RequireValidation from .gateway import AugustGateway from .util import async_create_august_clientsession @@ -164,7 +165,9 @@ class AugustConfigFlow(ConfigFlow, domain=DOMAIN): if self._august_gateway is not None: return self._august_gateway self._aiohttp_session = async_create_august_clientsession(self.hass) - self._august_gateway = AugustGateway(self.hass, self._aiohttp_session) + self._august_gateway = AugustGateway( + Path(self.hass.config.config_dir), self._aiohttp_session + ) return self._august_gateway @callback diff --git a/homeassistant/components/august/exceptions.py b/homeassistant/components/august/exceptions.py deleted file mode 100644 index edd418c9519..00000000000 --- a/homeassistant/components/august/exceptions.py +++ /dev/null @@ -1,15 +0,0 @@ -"""Shared exceptions for the august integration.""" - -from homeassistant import exceptions - - -class RequireValidation(exceptions.HomeAssistantError): - """Error to indicate we require validation (2fa).""" - - -class CannotConnect(exceptions.HomeAssistantError): - """Error to indicate we cannot connect.""" - - -class InvalidAuth(exceptions.HomeAssistantError): - """Error to indicate there is invalid auth.""" diff --git a/homeassistant/components/august/gateway.py b/homeassistant/components/august/gateway.py index 63bc085b811..2c6ad739bdc 100644 --- a/homeassistant/components/august/gateway.py +++ b/homeassistant/components/august/gateway.py @@ -1,56 +1,23 @@ """Handle August connection setup and authentication.""" -import asyncio -from collections.abc import Mapping -from http import HTTPStatus -import logging -import os from typing import Any -from aiohttp import ClientError, ClientResponseError, ClientSession -from yalexs.api_async import ApiAsync -from yalexs.authenticator_async import AuthenticationState, AuthenticatorAsync -from yalexs.authenticator_common import Authentication from yalexs.const import DEFAULT_BRAND -from yalexs.exceptions import AugustApiAIOHTTPError +from yalexs.manager.gateway import Gateway -from homeassistant.const import CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME -from homeassistant.core import HomeAssistant, callback +from homeassistant.const import CONF_USERNAME from .const import ( CONF_ACCESS_TOKEN_CACHE_FILE, CONF_BRAND, CONF_INSTALL_ID, CONF_LOGIN_METHOD, - DEFAULT_AUGUST_CONFIG_FILE, - DEFAULT_TIMEOUT, - VERIFICATION_CODE_KEY, ) -from .exceptions import CannotConnect, InvalidAuth, RequireValidation - -_LOGGER = logging.getLogger(__name__) -class AugustGateway: +class AugustGateway(Gateway): """Handle the connection to August.""" - api: ApiAsync - authenticator: AuthenticatorAsync - authentication: Authentication - _access_token_cache_file: str - - def __init__(self, hass: HomeAssistant, aiohttp_session: ClientSession) -> None: - """Init the connection.""" - self._aiohttp_session = aiohttp_session - self._token_refresh_lock = asyncio.Lock() - self._hass: HomeAssistant = hass - self._config: Mapping[str, Any] | None = None - - @property - def access_token(self) -> str: - """Access token for the api.""" - return self.authentication.access_token - def config_entry(self) -> dict[str, Any]: """Config entry.""" assert self._config is not None @@ -61,101 +28,3 @@ class AugustGateway: CONF_INSTALL_ID: self._config.get(CONF_INSTALL_ID), CONF_ACCESS_TOKEN_CACHE_FILE: self._access_token_cache_file, } - - @callback - def async_configure_access_token_cache_file( - self, username: str, access_token_cache_file: str | None - ) -> str: - """Configure the access token cache file.""" - file = access_token_cache_file or f".{username}{DEFAULT_AUGUST_CONFIG_FILE}" - self._access_token_cache_file = file - return self._hass.config.path(file) - - async def async_setup(self, conf: Mapping[str, Any]) -> None: - """Create the api and authenticator objects.""" - if conf.get(VERIFICATION_CODE_KEY): - return - - access_token_cache_file_path = self.async_configure_access_token_cache_file( - conf[CONF_USERNAME], conf.get(CONF_ACCESS_TOKEN_CACHE_FILE) - ) - self._config = conf - - self.api = ApiAsync( - self._aiohttp_session, - timeout=self._config.get(CONF_TIMEOUT, DEFAULT_TIMEOUT), - brand=self._config.get(CONF_BRAND, DEFAULT_BRAND), - ) - - self.authenticator = AuthenticatorAsync( - self.api, - self._config[CONF_LOGIN_METHOD], - self._config[CONF_USERNAME], - self._config.get(CONF_PASSWORD, ""), - install_id=self._config.get(CONF_INSTALL_ID), - access_token_cache_file=access_token_cache_file_path, - ) - - await self.authenticator.async_setup_authentication() - - async def async_authenticate(self) -> Authentication: - """Authenticate with the details provided to setup.""" - try: - self.authentication = await self.authenticator.async_authenticate() - if self.authentication.state == AuthenticationState.AUTHENTICATED: - # Call the locks api to verify we are actually - # authenticated because we can be authenticated - # by have no access - await self.api.async_get_operable_locks(self.access_token) - except AugustApiAIOHTTPError as ex: - if ex.auth_failed: - raise InvalidAuth from ex - raise CannotConnect from ex - except ClientResponseError as ex: - if ex.status == HTTPStatus.UNAUTHORIZED: - raise InvalidAuth from ex - - raise CannotConnect from ex - except ClientError as ex: - _LOGGER.error("Unable to connect to August service: %s", str(ex)) - raise CannotConnect from ex - - if self.authentication.state == AuthenticationState.BAD_PASSWORD: - raise InvalidAuth - - if self.authentication.state == AuthenticationState.REQUIRES_VALIDATION: - raise RequireValidation - - if self.authentication.state != AuthenticationState.AUTHENTICATED: - _LOGGER.error("Unknown authentication state: %s", self.authentication.state) - raise InvalidAuth - - return self.authentication - - async def async_reset_authentication(self) -> None: - """Remove the cache file.""" - await self._hass.async_add_executor_job(self._reset_authentication) - - def _reset_authentication(self) -> None: - """Remove the cache file.""" - path = self._hass.config.path(self._access_token_cache_file) - if os.path.exists(path): - os.unlink(path) - - async def async_refresh_access_token_if_needed(self) -> None: - """Refresh the august access token if needed.""" - if not self.authenticator.should_refresh(): - return - async with self._token_refresh_lock: - refreshed_authentication = ( - await self.authenticator.async_refresh_access_token(force=False) - ) - _LOGGER.info( - ( - "Refreshed august access token. The old token expired at %s, and" - " the new token expires at %s" - ), - self.authentication.access_token_expires, - refreshed_authentication.access_token_expires, - ) - self.authentication = refreshed_authentication diff --git a/homeassistant/components/august/manifest.json b/homeassistant/components/august/manifest.json index f85e75664eb..179e85de7f0 100644 --- a/homeassistant/components/august/manifest.json +++ b/homeassistant/components/august/manifest.json @@ -28,5 +28,5 @@ "documentation": "https://www.home-assistant.io/integrations/august", "iot_class": "cloud_push", "loggers": ["pubnub", "yalexs"], - "requirements": ["yalexs==3.1.0", "yalexs-ble==2.4.2"] + "requirements": ["yalexs==5.2.0", "yalexs-ble==2.4.2"] } diff --git a/homeassistant/components/august/subscriber.py b/homeassistant/components/august/subscriber.py deleted file mode 100644 index bec8e2f0b97..00000000000 --- a/homeassistant/components/august/subscriber.py +++ /dev/null @@ -1,98 +0,0 @@ -"""Base class for August entity.""" - -from __future__ import annotations - -from abc import abstractmethod -from datetime import datetime, timedelta - -from homeassistant.const import EVENT_HOMEASSISTANT_STOP -from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, callback -from homeassistant.helpers.event import async_track_time_interval - - -class AugustSubscriberMixin: - """Base implementation for a subscriber.""" - - def __init__(self, hass: HomeAssistant, update_interval: timedelta) -> None: - """Initialize an subscriber.""" - super().__init__() - self._hass = hass - self._update_interval = update_interval - self._subscriptions: dict[str, list[CALLBACK_TYPE]] = {} - self._unsub_interval: CALLBACK_TYPE | None = None - self._stop_interval: CALLBACK_TYPE | None = None - - @callback - def async_subscribe_device_id( - self, device_id: str, update_callback: CALLBACK_TYPE - ) -> CALLBACK_TYPE: - """Add an callback subscriber. - - Returns a callable that can be used to unsubscribe. - """ - if not self._subscriptions: - self._async_setup_listeners() - - self._subscriptions.setdefault(device_id, []).append(update_callback) - - def _unsubscribe() -> None: - self.async_unsubscribe_device_id(device_id, update_callback) - - return _unsubscribe - - @abstractmethod - async def _async_refresh(self, time: datetime) -> None: - """Refresh data.""" - - @callback - def _async_scheduled_refresh(self, now: datetime) -> None: - """Call the refresh method.""" - self._hass.async_create_background_task( - self._async_refresh(now), name=f"{self} schedule refresh", eager_start=True - ) - - @callback - def _async_cancel_update_interval(self, _: Event | None = None) -> None: - """Cancel the scheduled update.""" - if self._unsub_interval: - self._unsub_interval() - self._unsub_interval = None - - @callback - def _async_setup_listeners(self) -> None: - """Create interval and stop listeners.""" - self._async_cancel_update_interval() - self._unsub_interval = async_track_time_interval( - self._hass, - self._async_scheduled_refresh, - self._update_interval, - name="august refresh", - ) - - if not self._stop_interval: - self._stop_interval = self._hass.bus.async_listen( - EVENT_HOMEASSISTANT_STOP, - self._async_cancel_update_interval, - ) - - @callback - def async_unsubscribe_device_id( - self, device_id: str, update_callback: CALLBACK_TYPE - ) -> None: - """Remove a callback subscriber.""" - self._subscriptions[device_id].remove(update_callback) - if not self._subscriptions[device_id]: - del self._subscriptions[device_id] - - if self._subscriptions: - return - self._async_cancel_update_interval() - - @callback - def async_signal_device_id_update(self, device_id: str) -> None: - """Call the callbacks for a device_id.""" - if not self._subscriptions.get(device_id): - return - - for update_callback in self._subscriptions[device_id]: - update_callback() diff --git a/requirements_all.txt b/requirements_all.txt index fc80c0d87bf..7acc4348952 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2933,7 +2933,7 @@ yalesmartalarmclient==0.3.9 yalexs-ble==2.4.2 # homeassistant.components.august -yalexs==3.1.0 +yalexs==5.2.0 # homeassistant.components.yeelight yeelight==0.7.14 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index df36cf3c3cf..6c8d765f9e9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2289,7 +2289,7 @@ yalesmartalarmclient==0.3.9 yalexs-ble==2.4.2 # homeassistant.components.august -yalexs==3.1.0 +yalexs==5.2.0 # homeassistant.components.yeelight yeelight==0.7.14 diff --git a/tests/components/august/mocks.py b/tests/components/august/mocks.py index e0bc67f510f..b8d394fa067 100644 --- a/tests/components/august/mocks.py +++ b/tests/components/august/mocks.py @@ -58,8 +58,8 @@ def _mock_authenticator(auth_state): return authenticator -@patch("homeassistant.components.august.gateway.ApiAsync") -@patch("homeassistant.components.august.gateway.AuthenticatorAsync.async_authenticate") +@patch("yalexs.manager.gateway.ApiAsync") +@patch("yalexs.manager.gateway.AuthenticatorAsync.async_authenticate") async def _mock_setup_august( hass, api_instance, pubnub_mock, authenticate_mock, api_mock, brand ): @@ -77,7 +77,10 @@ async def _mock_setup_august( ) entry.add_to_hass(hass) with ( - patch("homeassistant.components.august.async_create_pubnub"), + patch( + "homeassistant.components.august.async_create_pubnub", + return_value=AsyncMock(), + ), patch("homeassistant.components.august.AugustPubNub", return_value=pubnub_mock), ): assert await hass.config_entries.async_setup(entry.entry_id) diff --git a/tests/components/august/test_config_flow.py b/tests/components/august/test_config_flow.py index e1e6f622c2e..aec08864c65 100644 --- a/tests/components/august/test_config_flow.py +++ b/tests/components/august/test_config_flow.py @@ -3,6 +3,7 @@ from unittest.mock import patch from yalexs.authenticator import ValidationResult +from yalexs.manager.exceptions import CannotConnect, InvalidAuth, RequireValidation from homeassistant import config_entries from homeassistant.components.august.const import ( @@ -13,11 +14,6 @@ from homeassistant.components.august.const import ( DOMAIN, VERIFICATION_CODE_KEY, ) -from homeassistant.components.august.exceptions import ( - CannotConnect, - InvalidAuth, - RequireValidation, -) from homeassistant.const import CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -151,7 +147,7 @@ async def test_form_needs_validate(hass: HomeAssistant) -> None: side_effect=RequireValidation, ), patch( - "homeassistant.components.august.gateway.AuthenticatorAsync.async_send_verification_code", + "yalexs.manager.gateway.AuthenticatorAsync.async_send_verification_code", return_value=True, ) as mock_send_verification_code, ): @@ -176,11 +172,11 @@ async def test_form_needs_validate(hass: HomeAssistant) -> None: side_effect=RequireValidation, ), patch( - "homeassistant.components.august.gateway.AuthenticatorAsync.async_validate_verification_code", + "yalexs.manager.gateway.AuthenticatorAsync.async_validate_verification_code", return_value=ValidationResult.INVALID_VERIFICATION_CODE, ) as mock_validate_verification_code, patch( - "homeassistant.components.august.gateway.AuthenticatorAsync.async_send_verification_code", + "yalexs.manager.gateway.AuthenticatorAsync.async_send_verification_code", return_value=True, ) as mock_send_verification_code, ): @@ -204,11 +200,11 @@ async def test_form_needs_validate(hass: HomeAssistant) -> None: return_value=True, ), patch( - "homeassistant.components.august.gateway.AuthenticatorAsync.async_validate_verification_code", + "yalexs.manager.gateway.AuthenticatorAsync.async_validate_verification_code", return_value=ValidationResult.VALIDATED, ) as mock_validate_verification_code, patch( - "homeassistant.components.august.gateway.AuthenticatorAsync.async_send_verification_code", + "yalexs.manager.gateway.AuthenticatorAsync.async_send_verification_code", return_value=True, ) as mock_send_verification_code, patch( @@ -310,7 +306,7 @@ async def test_form_reauth_with_2fa(hass: HomeAssistant) -> None: side_effect=RequireValidation, ), patch( - "homeassistant.components.august.gateway.AuthenticatorAsync.async_send_verification_code", + "yalexs.manager.gateway.AuthenticatorAsync.async_send_verification_code", return_value=True, ) as mock_send_verification_code, ): @@ -334,11 +330,11 @@ async def test_form_reauth_with_2fa(hass: HomeAssistant) -> None: return_value=True, ), patch( - "homeassistant.components.august.gateway.AuthenticatorAsync.async_validate_verification_code", + "yalexs.manager.gateway.AuthenticatorAsync.async_validate_verification_code", return_value=ValidationResult.VALIDATED, ) as mock_validate_verification_code, patch( - "homeassistant.components.august.gateway.AuthenticatorAsync.async_send_verification_code", + "yalexs.manager.gateway.AuthenticatorAsync.async_send_verification_code", return_value=True, ) as mock_send_verification_code, patch( diff --git a/tests/components/august/test_gateway.py b/tests/components/august/test_gateway.py index 535e547d915..e605fd74f0a 100644 --- a/tests/components/august/test_gateway.py +++ b/tests/components/august/test_gateway.py @@ -1,5 +1,6 @@ """The gateway tests for the august platform.""" +from pathlib import Path from unittest.mock import MagicMock, patch from yalexs.authenticator_common import AuthenticationState @@ -16,12 +17,10 @@ async def test_refresh_access_token(hass: HomeAssistant) -> None: await _patched_refresh_access_token(hass, "new_token", 5678) -@patch("homeassistant.components.august.gateway.ApiAsync.async_get_operable_locks") -@patch("homeassistant.components.august.gateway.AuthenticatorAsync.async_authenticate") -@patch("homeassistant.components.august.gateway.AuthenticatorAsync.should_refresh") -@patch( - "homeassistant.components.august.gateway.AuthenticatorAsync.async_refresh_access_token" -) +@patch("yalexs.manager.gateway.ApiAsync.async_get_operable_locks") +@patch("yalexs.manager.gateway.AuthenticatorAsync.async_authenticate") +@patch("yalexs.manager.gateway.AuthenticatorAsync.should_refresh") +@patch("yalexs.manager.gateway.AuthenticatorAsync.async_refresh_access_token") async def _patched_refresh_access_token( hass, new_token, @@ -36,7 +35,7 @@ async def _patched_refresh_access_token( "original_token", 1234, AuthenticationState.AUTHENTICATED ) ) - august_gateway = AugustGateway(hass, MagicMock()) + august_gateway = AugustGateway(Path(hass.config.config_dir), MagicMock()) mocked_config = _mock_get_config() await august_gateway.async_setup(mocked_config[DOMAIN]) await august_gateway.async_authenticate() diff --git a/tests/components/august/test_lock.py b/tests/components/august/test_lock.py index a79ee7ffbf1..8bb71826d24 100644 --- a/tests/components/august/test_lock.py +++ b/tests/components/august/test_lock.py @@ -6,9 +6,9 @@ from unittest.mock import Mock from aiohttp import ClientResponseError from freezegun.api import FrozenDateTimeFactory import pytest +from yalexs.manager.activity import INITIAL_LOCK_RESYNC_TIME from yalexs.pubnub_async import AugustPubNub -from homeassistant.components.august.activity import INITIAL_LOCK_RESYNC_TIME from homeassistant.components.lock import ( DOMAIN as LOCK_DOMAIN, STATE_JAMMED, From 279f183ce372ef7a188d9a0ceab907b9c42ff2a3 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sun, 9 Jun 2024 15:51:01 +0200 Subject: [PATCH 0408/1445] Remove Harmony switches (#119206) --- homeassistant/components/harmony/const.py | 2 +- .../components/harmony/manifest.json | 2 +- homeassistant/components/harmony/strings.json | 10 - homeassistant/components/harmony/switch.py | 110 ---------- tests/components/harmony/test_switch.py | 203 ------------------ 5 files changed, 2 insertions(+), 325 deletions(-) delete mode 100644 homeassistant/components/harmony/switch.py delete mode 100644 tests/components/harmony/test_switch.py diff --git a/homeassistant/components/harmony/const.py b/homeassistant/components/harmony/const.py index 69ef2cb66c9..f474783b736 100644 --- a/homeassistant/components/harmony/const.py +++ b/homeassistant/components/harmony/const.py @@ -5,7 +5,7 @@ from homeassistant.const import Platform DOMAIN = "harmony" SERVICE_SYNC = "sync" SERVICE_CHANGE_CHANNEL = "change_channel" -PLATFORMS = [Platform.REMOTE, Platform.SELECT, Platform.SWITCH] +PLATFORMS = [Platform.REMOTE, Platform.SELECT] UNIQUE_ID = "unique_id" ACTIVITY_POWER_OFF = "PowerOff" HARMONY_OPTIONS_UPDATE = "harmony_options_update" diff --git a/homeassistant/components/harmony/manifest.json b/homeassistant/components/harmony/manifest.json index 8acc4307d1f..d37801376ec 100644 --- a/homeassistant/components/harmony/manifest.json +++ b/homeassistant/components/harmony/manifest.json @@ -3,7 +3,7 @@ "name": "Logitech Harmony Hub", "codeowners": ["@ehendrix23", "@bdraco", "@mkeesey", "@Aohzan"], "config_flow": true, - "dependencies": ["remote", "switch"], + "dependencies": ["remote"], "documentation": "https://www.home-assistant.io/integrations/harmony", "iot_class": "local_push", "loggers": ["aioharmony", "slixmpp"], diff --git a/homeassistant/components/harmony/strings.json b/homeassistant/components/harmony/strings.json index 444097395c9..e13573a9ea3 100644 --- a/homeassistant/components/harmony/strings.json +++ b/homeassistant/components/harmony/strings.json @@ -46,16 +46,6 @@ } } }, - "issues": { - "deprecated_switches": { - "title": "The Logitech Harmony switch platform is being removed", - "description": "Using the switch platform to change the current activity is now deprecated and will be removed in a future version of Home Assistant.\n\nPlease adjust any automations or scripts that use switch entities to instead use the select entity." - }, - "deprecated_switches_entity": { - "title": "Deprecated Harmony entity detected in {info}", - "description": "Your Harmony entity `{entity}` is being used in `{info}`. A select entity is available and should be used going forward.\n\nPlease adjust `{info}` to fix this issue." - } - }, "services": { "sync": { "name": "Sync", diff --git a/homeassistant/components/harmony/switch.py b/homeassistant/components/harmony/switch.py deleted file mode 100644 index 0cb07e5cb1e..00000000000 --- a/homeassistant/components/harmony/switch.py +++ /dev/null @@ -1,110 +0,0 @@ -"""Support for Harmony Hub activities.""" - -import logging -from typing import Any, cast - -from homeassistant.components.automation import automations_with_entity -from homeassistant.components.script import scripts_with_entity -from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN, SwitchEntity -from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HassJob, HomeAssistant, callback -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue - -from .const import DOMAIN, HARMONY_DATA -from .data import HarmonyData -from .entity import HarmonyEntity -from .subscriber import HarmonyCallback - -_LOGGER = logging.getLogger(__name__) - - -async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback -) -> None: - """Set up harmony activity switches.""" - data: HarmonyData = hass.data[DOMAIN][entry.entry_id][HARMONY_DATA] - - async_add_entities( - (HarmonyActivitySwitch(activity, data) for activity in data.activities), True - ) - - -class HarmonyActivitySwitch(HarmonyEntity, SwitchEntity): - """Switch representation of a Harmony activity.""" - - def __init__(self, activity: dict, data: HarmonyData) -> None: - """Initialize HarmonyActivitySwitch class.""" - super().__init__(data=data) - self._activity_name = self._attr_name = activity["label"] - self._activity_id = activity["id"] - self._attr_entity_registry_enabled_default = False - self._attr_unique_id = f"activity_{self._activity_id}" - self._attr_device_info = self._data.device_info(DOMAIN) - - @property - def is_on(self) -> bool: - """Return if the current activity is the one for this switch.""" - _, activity_name = self._data.current_activity - return activity_name == self._activity_name - - async def async_turn_on(self, **kwargs: Any) -> None: - """Start this activity.""" - async_create_issue( - self.hass, - DOMAIN, - "deprecated_switches", - breaks_in_ha_version="2024.6.0", - is_fixable=False, - severity=IssueSeverity.WARNING, - translation_key="deprecated_switches", - ) - await self._data.async_start_activity(self._activity_name) - - async def async_turn_off(self, **kwargs: Any) -> None: - """Stop this activity.""" - async_create_issue( - self.hass, - DOMAIN, - "deprecated_switches", - breaks_in_ha_version="2024.6.0", - is_fixable=False, - severity=IssueSeverity.WARNING, - translation_key="deprecated_switches", - ) - await self._data.async_power_off() - - async def async_added_to_hass(self) -> None: - """Call when entity is added to hass.""" - activity_update_job = HassJob(self._async_activity_update) - self.async_on_remove( - self._data.async_subscribe( - HarmonyCallback( - connected=HassJob(self.async_got_connected), - disconnected=HassJob(self.async_got_disconnected), - activity_starting=activity_update_job, - activity_started=activity_update_job, - config_updated=None, - ) - ) - ) - entity_automations = automations_with_entity(self.hass, self.entity_id) - entity_scripts = scripts_with_entity(self.hass, self.entity_id) - for item in entity_automations + entity_scripts: - async_create_issue( - self.hass, - DOMAIN, - f"deprecated_switches_{self.entity_id}_{item}", - breaks_in_ha_version="2024.6.0", - is_fixable=False, - severity=IssueSeverity.WARNING, - translation_key="deprecated_switches_entity", - translation_placeholders={ - "entity": f"{SWITCH_DOMAIN}.{cast(str, self.name).lower().replace(' ', '_')}", - "info": item, - }, - ) - - @callback - def _async_activity_update(self, activity_info: tuple) -> None: - self.async_write_ha_state() diff --git a/tests/components/harmony/test_switch.py b/tests/components/harmony/test_switch.py deleted file mode 100644 index 0cfc0e5bead..00000000000 --- a/tests/components/harmony/test_switch.py +++ /dev/null @@ -1,203 +0,0 @@ -"""Test the Logitech Harmony Hub activity switches.""" - -from datetime import timedelta - -import pytest - -from homeassistant.components import automation, script -from homeassistant.components.automation import automations_with_entity -from homeassistant.components.harmony.const import DOMAIN -from homeassistant.components.script import scripts_with_entity -from homeassistant.components.switch import ( - DOMAIN as SWITCH_DOMAIN, - SERVICE_TURN_OFF, - SERVICE_TURN_ON, -) -from homeassistant.const import ( - ATTR_ENTITY_ID, - CONF_HOST, - CONF_NAME, - STATE_OFF, - STATE_ON, - STATE_UNAVAILABLE, -) -from homeassistant.core import HomeAssistant -from homeassistant.helpers import entity_registry as er -import homeassistant.helpers.issue_registry as ir -from homeassistant.setup import async_setup_component -from homeassistant.util import utcnow - -from .const import ENTITY_PLAY_MUSIC, ENTITY_REMOTE, ENTITY_WATCH_TV, HUB_NAME - -from tests.common import MockConfigEntry, async_fire_time_changed - - -async def test_connection_state_changes( - harmony_client, - mock_hc, - hass: HomeAssistant, - mock_write_config, - entity_registry: er.EntityRegistry, -) -> None: - """Ensure connection changes are reflected in the switch states.""" - entry = MockConfigEntry( - domain=DOMAIN, data={CONF_HOST: "192.0.2.0", CONF_NAME: HUB_NAME} - ) - - entry.add_to_hass(hass) - await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - - # check if switch entities are disabled by default - assert not hass.states.get(ENTITY_WATCH_TV) - assert not hass.states.get(ENTITY_PLAY_MUSIC) - - # enable switch entities - entity_registry.async_update_entity(ENTITY_WATCH_TV, disabled_by=None) - entity_registry.async_update_entity(ENTITY_PLAY_MUSIC, disabled_by=None) - await hass.config_entries.async_reload(entry.entry_id) - await hass.async_block_till_done() - - # mocks start with current activity == Watch TV - assert hass.states.is_state(ENTITY_WATCH_TV, STATE_ON) - assert hass.states.is_state(ENTITY_PLAY_MUSIC, STATE_OFF) - - harmony_client.mock_disconnection() - await hass.async_block_till_done() - - # Entities do not immediately show as unavailable - assert hass.states.is_state(ENTITY_WATCH_TV, STATE_ON) - assert hass.states.is_state(ENTITY_PLAY_MUSIC, STATE_OFF) - - future_time = utcnow() + timedelta(seconds=10) - async_fire_time_changed(hass, future_time) - await hass.async_block_till_done() - assert hass.states.is_state(ENTITY_WATCH_TV, STATE_UNAVAILABLE) - assert hass.states.is_state(ENTITY_PLAY_MUSIC, STATE_UNAVAILABLE) - - harmony_client.mock_reconnection() - await hass.async_block_till_done() - - assert hass.states.is_state(ENTITY_WATCH_TV, STATE_ON) - assert hass.states.is_state(ENTITY_PLAY_MUSIC, STATE_OFF) - - harmony_client.mock_disconnection() - harmony_client.mock_reconnection() - future_time = utcnow() + timedelta(seconds=10) - async_fire_time_changed(hass, future_time) - - await hass.async_block_till_done() - assert hass.states.is_state(ENTITY_WATCH_TV, STATE_ON) - assert hass.states.is_state(ENTITY_PLAY_MUSIC, STATE_OFF) - - -async def test_switch_toggles( - mock_hc, - hass: HomeAssistant, - mock_write_config, - entity_registry: er.EntityRegistry, - mock_config_entry: MockConfigEntry, -) -> None: - """Ensure calls to the switch modify the harmony state.""" - - mock_config_entry.add_to_hass(hass) - await hass.config_entries.async_setup(mock_config_entry.entry_id) - await hass.async_block_till_done() - - # enable switch entities - entity_registry.async_update_entity(ENTITY_WATCH_TV, disabled_by=None) - entity_registry.async_update_entity(ENTITY_PLAY_MUSIC, disabled_by=None) - await hass.config_entries.async_reload(mock_config_entry.entry_id) - await hass.async_block_till_done() - - # mocks start with current activity == Watch TV - assert hass.states.is_state(ENTITY_REMOTE, STATE_ON) - assert hass.states.is_state(ENTITY_WATCH_TV, STATE_ON) - assert hass.states.is_state(ENTITY_PLAY_MUSIC, STATE_OFF) - - # turn off watch tv switch - await _toggle_switch_and_wait(hass, SERVICE_TURN_OFF, ENTITY_WATCH_TV) - assert hass.states.is_state(ENTITY_REMOTE, STATE_OFF) - assert hass.states.is_state(ENTITY_WATCH_TV, STATE_OFF) - assert hass.states.is_state(ENTITY_PLAY_MUSIC, STATE_OFF) - - # turn on play music switch - await _toggle_switch_and_wait(hass, SERVICE_TURN_ON, ENTITY_PLAY_MUSIC) - assert hass.states.is_state(ENTITY_REMOTE, STATE_ON) - assert hass.states.is_state(ENTITY_WATCH_TV, STATE_OFF) - assert hass.states.is_state(ENTITY_PLAY_MUSIC, STATE_ON) - - # turn on watch tv switch - await _toggle_switch_and_wait(hass, SERVICE_TURN_ON, ENTITY_WATCH_TV) - assert hass.states.is_state(ENTITY_REMOTE, STATE_ON) - assert hass.states.is_state(ENTITY_WATCH_TV, STATE_ON) - assert hass.states.is_state(ENTITY_PLAY_MUSIC, STATE_OFF) - - -async def _toggle_switch_and_wait(hass, service_name, entity): - await hass.services.async_call( - SWITCH_DOMAIN, - service_name, - {ATTR_ENTITY_ID: entity}, - blocking=True, - ) - await hass.async_block_till_done() - - -@pytest.mark.usefixtures("entity_registry_enabled_by_default") -async def test_create_issue( - harmony_client, - mock_hc, - hass: HomeAssistant, - mock_write_config, - issue_registry: ir.IssueRegistry, -) -> None: - """Test we create an issue when an automation or script is using a deprecated entity.""" - assert await async_setup_component( - hass, - automation.DOMAIN, - { - automation.DOMAIN: { - "alias": "test", - "trigger": {"platform": "state", "entity_id": ENTITY_WATCH_TV}, - "action": {"service": "switch.turn_on", "entity_id": ENTITY_WATCH_TV}, - } - }, - ) - assert await async_setup_component( - hass, - script.DOMAIN, - { - script.DOMAIN: { - "test": { - "sequence": [ - { - "service": "switch.turn_on", - "data": {"entity_id": ENTITY_WATCH_TV}, - }, - ], - } - } - }, - ) - - entry = MockConfigEntry( - domain=DOMAIN, data={CONF_HOST: "192.0.2.0", CONF_NAME: HUB_NAME} - ) - - entry.add_to_hass(hass) - await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - - assert automations_with_entity(hass, ENTITY_WATCH_TV)[0] == "automation.test" - assert scripts_with_entity(hass, ENTITY_WATCH_TV)[0] == "script.test" - - assert issue_registry.async_get_issue(DOMAIN, "deprecated_switches") - assert issue_registry.async_get_issue( - DOMAIN, "deprecated_switches_switch.guest_room_watch_tv_automation.test" - ) - assert issue_registry.async_get_issue( - DOMAIN, "deprecated_switches_switch.guest_room_watch_tv_script.test" - ) - - assert len(issue_registry.issues) == 3 From 34f20fce36fd2ff8acbe4c558e8b8758ee69efb3 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Sun, 9 Jun 2024 15:52:34 +0200 Subject: [PATCH 0409/1445] Bump incomfort backend library to v0.6.0 (#119207) --- homeassistant/components/incomfort/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/incomfort/manifest.json b/homeassistant/components/incomfort/manifest.json index 8ef57047cce..2dd7491c5bb 100644 --- a/homeassistant/components/incomfort/manifest.json +++ b/homeassistant/components/incomfort/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/incomfort", "iot_class": "local_polling", "loggers": ["incomfortclient"], - "requirements": ["incomfort-client==0.5.0"] + "requirements": ["incomfort-client==0.6.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 7acc4348952..dec2a787834 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1152,7 +1152,7 @@ ihcsdk==2.8.5 imgw_pib==1.0.4 # homeassistant.components.incomfort -incomfort-client==0.5.0 +incomfort-client==0.6.0 # homeassistant.components.influxdb influxdb-client==1.24.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6c8d765f9e9..a7db391fee0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -939,7 +939,7 @@ ifaddr==0.2.0 imgw_pib==1.0.4 # homeassistant.components.incomfort -incomfort-client==0.5.0 +incomfort-client==0.6.0 # homeassistant.components.influxdb influxdb-client==1.24.0 From c9911e4dd488c5e14a35415d3b38fb544a5c5a00 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sun, 9 Jun 2024 15:56:26 +0200 Subject: [PATCH 0410/1445] Rework UniFi tests to not use runtime data (#119202) --- homeassistant/components/unifi/config_flow.py | 6 +- tests/common.py | 6 +- tests/components/unifi/conftest.py | 64 ++++++++------ tests/components/unifi/test_button.py | 18 ++-- tests/components/unifi/test_device_tracker.py | 64 +++++++------- tests/components/unifi/test_hub.py | 22 ++--- tests/components/unifi/test_image.py | 16 ++-- tests/components/unifi/test_init.py | 4 +- tests/components/unifi/test_sensor.py | 78 ++++++++--------- tests/components/unifi/test_switch.py | 86 ++++++++++--------- tests/components/unifi/test_update.py | 12 +-- 11 files changed, 196 insertions(+), 180 deletions(-) diff --git a/homeassistant/components/unifi/config_flow.py b/homeassistant/components/unifi/config_flow.py index e703f393d68..e93b59b0673 100644 --- a/homeassistant/components/unifi/config_flow.py +++ b/homeassistant/components/unifi/config_flow.py @@ -21,6 +21,7 @@ import voluptuous as vol from homeassistant.components import ssdp from homeassistant.config_entries import ( ConfigEntry, + ConfigEntryState, ConfigFlow, ConfigFlowResult, OptionsFlow, @@ -163,7 +164,10 @@ class UnifiFlowHandler(ConfigFlow, domain=UNIFI_DOMAIN): config_entry = self.reauth_config_entry abort_reason = "reauth_successful" - if config_entry: + if ( + config_entry is not None + and config_entry.state is not ConfigEntryState.NOT_LOADED + ): hub = config_entry.runtime_data if hub and hub.available: diff --git a/tests/common.py b/tests/common.py index 21e810be1e8..732970e108b 100644 --- a/tests/common.py +++ b/tests/common.py @@ -39,7 +39,7 @@ from homeassistant.components.device_automation import ( # noqa: F401 _async_get_device_automation_capabilities as async_get_device_automation_capabilities, ) from homeassistant.config import async_process_component_config -from homeassistant.config_entries import ConfigEntry, ConfigFlow, _DataT +from homeassistant.config_entries import ConfigEntry, ConfigFlow from homeassistant.const import ( DEVICE_DEFAULT_NAME, EVENT_HOMEASSISTANT_CLOSE, @@ -972,11 +972,9 @@ class MockToggleEntity(entity.ToggleEntity): return None -class MockConfigEntry(config_entries.ConfigEntry[_DataT]): +class MockConfigEntry(config_entries.ConfigEntry): """Helper for creating config entries that adds some defaults.""" - runtime_data: _DataT - def __init__( self, *, diff --git a/tests/components/unifi/conftest.py b/tests/components/unifi/conftest.py index 316be2bea47..b11c17b3df7 100644 --- a/tests/components/unifi/conftest.py +++ b/tests/components/unifi/conftest.py @@ -7,10 +7,12 @@ from collections.abc import Callable from datetime import timedelta from types import MappingProxyType from typing import Any -from unittest.mock import patch +from unittest.mock import AsyncMock, patch from aiounifi.models.message import MessageKey +import orjson import pytest +from typing_extensions import Generator from homeassistant.components.unifi import STORAGE_KEY, STORAGE_VERSION from homeassistant.components.unifi.const import CONF_SITE_ID, DOMAIN as UNIFI_DOMAIN @@ -307,31 +309,33 @@ class WebsocketStateManager(asyncio.Event): Prepares disconnect and reconnect flows. """ - def __init__(self, hass: HomeAssistant, aioclient_mock: AiohttpClientMocker): + def __init__( + self, hass: HomeAssistant, aioclient_mock: AiohttpClientMocker + ) -> None: """Store hass object and initialize asyncio.Event.""" self.hass = hass self.aioclient_mock = aioclient_mock super().__init__() - async def disconnect(self): + async def waiter(self, input: Callable[[bytes], None]) -> None: + """Consume message_handler new_data callback.""" + await self.wait() + + async def disconnect(self) -> None: """Mark future as done to make 'await self.api.start_websocket' return.""" self.set() await self.hass.async_block_till_done() - async def reconnect(self, fail=False): + async def reconnect(self, fail: bool = False) -> None: """Set up new future to make 'await self.api.start_websocket' block. Mock api calls done by 'await self.api.login'. Fail will make 'await self.api.start_websocket' return immediately. """ - hub = self.hass.config_entries.async_get_entry( - DEFAULT_CONFIG_ENTRY_ID - ).runtime_data - self.aioclient_mock.get( - f"https://{hub.config.host}:1234", status=302 - ) # Check UniFi OS + # Check UniFi OS + self.aioclient_mock.get(f"https://{DEFAULT_HOST}:1234", status=302) self.aioclient_mock.post( - f"https://{hub.config.host}:1234/api/login", + f"https://{DEFAULT_HOST}:1234/api/login", json={"data": "login successful", "meta": {"rc": "ok"}}, headers={"content-type": CONTENT_TYPE_JSON}, ) @@ -343,36 +347,42 @@ class WebsocketStateManager(asyncio.Event): await self.hass.async_block_till_done() -@pytest.fixture(autouse=True) -def websocket_mock(hass: HomeAssistant, aioclient_mock: AiohttpClientMocker): - """Mock 'await self.api.start_websocket' in 'UniFiController.start_websocket'.""" +@pytest.fixture(autouse=True, name="_mock_websocket") +def fixture_aiounifi_websocket_method() -> Generator[AsyncMock]: + """Mock aiounifi websocket context manager.""" + with patch("aiounifi.controller.Connectivity.websocket") as ws_mock: + yield ws_mock + + +@pytest.fixture(autouse=True, name="mock_websocket_state") +def fixture_aiounifi_websocket_state( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, _mock_websocket: AsyncMock +) -> WebsocketStateManager: + """Provide a state manager for UniFi websocket.""" websocket_state_manager = WebsocketStateManager(hass, aioclient_mock) - with patch("aiounifi.Controller.start_websocket") as ws_mock: - ws_mock.side_effect = websocket_state_manager.wait - yield websocket_state_manager + _mock_websocket.side_effect = websocket_state_manager.waiter + return websocket_state_manager -@pytest.fixture(autouse=True) -def mock_unifi_websocket(hass): +@pytest.fixture(name="mock_websocket_message") +def fixture_aiounifi_websocket_message(_mock_websocket: AsyncMock): """No real websocket allowed.""" def make_websocket_call( *, message: MessageKey | None = None, data: list[dict] | dict | None = None, - ): + ) -> None: """Generate a websocket call.""" - hub = hass.config_entries.async_get_entry(DEFAULT_CONFIG_ENTRY_ID).runtime_data + message_handler = _mock_websocket.call_args[0][0] + if data and not message: - hub.api.messages.handler(data) + message_handler(orjson.dumps(data)) elif data and message: if not isinstance(data, list): data = [data] - hub.api.messages.handler( - { - "meta": {"message": message.value}, - "data": data, - } + message_handler( + orjson.dumps({"meta": {"message": message.value}, "data": data}) ) else: raise NotImplementedError diff --git a/tests/components/unifi/test_button.py b/tests/components/unifi/test_button.py index 08e9b52a2ca..b58d01e7724 100644 --- a/tests/components/unifi/test_button.py +++ b/tests/components/unifi/test_button.py @@ -123,7 +123,7 @@ async def _test_button_entity( hass: HomeAssistant, entity_registry: er.EntityRegistry, aioclient_mock: AiohttpClientMocker, - websocket_mock, + mock_websocket_state, config_entry: ConfigEntry, entity_count: int, entity_id: str, @@ -164,11 +164,11 @@ async def _test_button_entity( # Availability signalling # Controller disconnects - await websocket_mock.disconnect() + await mock_websocket_state.disconnect() assert hass.states.get(entity_id).state == STATE_UNAVAILABLE # Controller reconnects - await websocket_mock.reconnect() + await mock_websocket_state.reconnect() assert hass.states.get(entity_id).state != STATE_UNAVAILABLE @@ -218,8 +218,8 @@ async def test_device_button_entities( hass: HomeAssistant, entity_registry: er.EntityRegistry, aioclient_mock: AiohttpClientMocker, - config_entry_setup, - websocket_mock, + config_entry_setup: ConfigEntry, + mock_websocket_state, entity_count: int, entity_id: str, unique_id: str, @@ -233,7 +233,7 @@ async def test_device_button_entities( hass, entity_registry, aioclient_mock, - websocket_mock, + mock_websocket_state, config_entry_setup, entity_count, entity_id, @@ -279,8 +279,8 @@ async def test_wlan_button_entities( hass: HomeAssistant, entity_registry: er.EntityRegistry, aioclient_mock: AiohttpClientMocker, - config_entry_setup, - websocket_mock, + config_entry_setup: ConfigEntry, + mock_websocket_state, entity_count: int, entity_id: str, unique_id: str, @@ -308,7 +308,7 @@ async def test_wlan_button_entities( hass, entity_registry, aioclient_mock, - websocket_mock, + mock_websocket_state, config_entry_setup, entity_count, entity_id, diff --git a/tests/components/unifi/test_device_tracker.py b/tests/components/unifi/test_device_tracker.py index 0a3aaff581d..c8149b75fe0 100644 --- a/tests/components/unifi/test_device_tracker.py +++ b/tests/components/unifi/test_device_tracker.py @@ -49,7 +49,7 @@ from tests.common import async_fire_time_changed @pytest.mark.usefixtures("mock_device_registry") async def test_tracked_wireless_clients( hass: HomeAssistant, - mock_unifi_websocket, + mock_websocket_message, config_entry_setup: ConfigEntry, client_payload: list[dict[str, Any]], ) -> None: @@ -60,7 +60,7 @@ async def test_tracked_wireless_clients( # Updated timestamp marks client as home client = client_payload[0] client["last_seen"] = dt_util.as_timestamp(dt_util.utcnow()) - mock_unifi_websocket(message=MessageKey.CLIENT, data=client) + mock_websocket_message(message=MessageKey.CLIENT, data=client) await hass.async_block_till_done() assert hass.states.get("device_tracker.client").state == STATE_HOME @@ -78,7 +78,7 @@ async def test_tracked_wireless_clients( assert hass.states.get("device_tracker.client").state == STATE_NOT_HOME # Same timestamp doesn't explicitly mark client as away - mock_unifi_websocket(message=MessageKey.CLIENT, data=client) + mock_websocket_message(message=MessageKey.CLIENT, data=client) await hass.async_block_till_done() assert hass.states.get("device_tracker.client").state == STATE_HOME @@ -146,7 +146,7 @@ async def test_tracked_wireless_clients( @pytest.mark.usefixtures("config_entry_setup") @pytest.mark.usefixtures("mock_device_registry") async def test_tracked_clients( - hass: HomeAssistant, mock_unifi_websocket, client_payload: list[dict[str, Any]] + hass: HomeAssistant, mock_websocket_message, client_payload: list[dict[str, Any]] ) -> None: """Test the update_items function with some clients.""" assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 5 @@ -170,7 +170,7 @@ async def test_tracked_clients( client_1 = client_payload[0] client_1["last_seen"] = dt_util.as_timestamp(dt_util.utcnow()) - mock_unifi_websocket(message=MessageKey.CLIENT, data=client_1) + mock_websocket_message(message=MessageKey.CLIENT, data=client_1) await hass.async_block_till_done() assert hass.states.get("device_tracker.client_1").state == STATE_HOME @@ -196,7 +196,7 @@ async def test_tracked_clients( async def test_tracked_wireless_clients_event_source( hass: HomeAssistant, freezer: FrozenDateTimeFactory, - mock_unifi_websocket, + mock_websocket_message, config_entry_setup: ConfigEntry, client_payload: list[dict[str, Any]], ) -> None: @@ -226,7 +226,7 @@ async def test_tracked_wireless_clients_event_source( ), "_id": "5ea331fa30c49e00f90ddc1a", } - mock_unifi_websocket(message=MessageKey.EVENT, data=event) + mock_websocket_message(message=MessageKey.EVENT, data=event) await hass.async_block_till_done() assert hass.states.get("device_tracker.client").state == STATE_HOME @@ -249,7 +249,7 @@ async def test_tracked_wireless_clients_event_source( ), "_id": "5ea32ff730c49e00f90dca1a", } - mock_unifi_websocket(message=MessageKey.EVENT, data=event) + mock_websocket_message(message=MessageKey.EVENT, data=event) await hass.async_block_till_done() assert hass.states.get("device_tracker.client").state == STATE_HOME @@ -275,7 +275,7 @@ async def test_tracked_wireless_clients_event_source( # New data client["last_seen"] = dt_util.as_timestamp(dt_util.utcnow()) - mock_unifi_websocket(message=MessageKey.CLIENT, data=client) + mock_websocket_message(message=MessageKey.CLIENT, data=client) await hass.async_block_till_done() assert hass.states.get("device_tracker.client").state == STATE_HOME @@ -298,7 +298,7 @@ async def test_tracked_wireless_clients_event_source( ), "_id": "5ea32ff730c49e00f90dca1a", } - mock_unifi_websocket(message=MessageKey.EVENT, data=event) + mock_websocket_message(message=MessageKey.EVENT, data=event) await hass.async_block_till_done() assert hass.states.get("device_tracker.client").state == STATE_HOME @@ -361,7 +361,7 @@ async def test_tracked_wireless_clients_event_source( async def test_tracked_devices( hass: HomeAssistant, freezer: FrozenDateTimeFactory, - mock_unifi_websocket, + mock_websocket_message, device_payload: list[dict[str, Any]], ) -> None: """Test the update_items function with some devices.""" @@ -375,7 +375,7 @@ async def test_tracked_devices( device_2 = device_payload[1] device_2["state"] = 1 device_2["next_interval"] = 50 - mock_unifi_websocket(message=MessageKey.DEVICE, data=[device_1, device_2]) + mock_websocket_message(message=MessageKey.DEVICE, data=[device_1, device_2]) await hass.async_block_till_done() assert hass.states.get("device_tracker.device_1").state == STATE_HOME @@ -392,7 +392,7 @@ async def test_tracked_devices( # Disabled device is unavailable device_1["disabled"] = True - mock_unifi_websocket(message=MessageKey.DEVICE, data=device_1) + mock_websocket_message(message=MessageKey.DEVICE, data=device_1) await hass.async_block_till_done() assert hass.states.get("device_tracker.device_1").state == STATE_UNAVAILABLE @@ -422,7 +422,7 @@ async def test_tracked_devices( @pytest.mark.usefixtures("config_entry_setup") @pytest.mark.usefixtures("mock_device_registry") async def test_remove_clients( - hass: HomeAssistant, mock_unifi_websocket, client_payload: list[dict[str, Any]] + hass: HomeAssistant, mock_websocket_message, client_payload: list[dict[str, Any]] ) -> None: """Test the remove_items function with some clients.""" assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 2 @@ -430,7 +430,7 @@ async def test_remove_clients( assert hass.states.get("device_tracker.client_2") # Remove client - mock_unifi_websocket(message=MessageKey.CLIENT_REMOVED, data=client_payload[0]) + mock_websocket_message(message=MessageKey.CLIENT_REMOVED, data=client_payload[0]) await hass.async_block_till_done() await hass.async_block_till_done() @@ -479,19 +479,19 @@ async def test_remove_clients( ) @pytest.mark.usefixtures("config_entry_setup") @pytest.mark.usefixtures("mock_device_registry") -async def test_hub_state_change(hass: HomeAssistant, websocket_mock) -> None: +async def test_hub_state_change(hass: HomeAssistant, mock_websocket_state) -> None: """Verify entities state reflect on hub connection becoming unavailable.""" assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 2 assert hass.states.get("device_tracker.client").state == STATE_NOT_HOME assert hass.states.get("device_tracker.device").state == STATE_HOME # Controller unavailable - await websocket_mock.disconnect() + await mock_websocket_state.disconnect() assert hass.states.get("device_tracker.client").state == STATE_UNAVAILABLE assert hass.states.get("device_tracker.device").state == STATE_UNAVAILABLE # Controller available - await websocket_mock.reconnect() + await mock_websocket_state.reconnect() assert hass.states.get("device_tracker.client").state == STATE_NOT_HOME assert hass.states.get("device_tracker.device").state == STATE_HOME @@ -707,7 +707,7 @@ async def test_option_track_devices( @pytest.mark.usefixtures("mock_device_registry") async def test_option_ssid_filter( hass: HomeAssistant, - mock_unifi_websocket, + mock_websocket_message, config_entry_factory: Callable[[], ConfigEntry], client_payload: list[dict[str, Any]], ) -> None: @@ -753,12 +753,12 @@ async def test_option_ssid_filter( # Roams to SSID outside of filter client = client_payload[0] client["essid"] = "other_ssid" - mock_unifi_websocket(message=MessageKey.CLIENT, data=client) + mock_websocket_message(message=MessageKey.CLIENT, data=client) # Data update while SSID filter is in effect shouldn't create the client client_on_ssid2 = client_payload[1] client_on_ssid2["last_seen"] = dt_util.as_timestamp(dt_util.utcnow()) - mock_unifi_websocket(message=MessageKey.CLIENT, data=client_on_ssid2) + mock_websocket_message(message=MessageKey.CLIENT, data=client_on_ssid2) await hass.async_block_till_done() new_time = dt_util.utcnow() + timedelta( @@ -782,7 +782,7 @@ async def test_option_ssid_filter( client["last_seen"] += 1 client_on_ssid2["last_seen"] += 1 - mock_unifi_websocket(message=MessageKey.CLIENT, data=[client, client_on_ssid2]) + mock_websocket_message(message=MessageKey.CLIENT, data=[client, client_on_ssid2]) await hass.async_block_till_done() assert hass.states.get("device_tracker.client").state == STATE_HOME @@ -801,7 +801,7 @@ async def test_option_ssid_filter( assert hass.states.get("device_tracker.client").state == STATE_NOT_HOME client_on_ssid2["last_seen"] += 1 - mock_unifi_websocket(message=MessageKey.CLIENT, data=client_on_ssid2) + mock_websocket_message(message=MessageKey.CLIENT, data=client_on_ssid2) await hass.async_block_till_done() # Client won't go away until after next update @@ -809,7 +809,7 @@ async def test_option_ssid_filter( # Trigger update to get client marked as away client_on_ssid2["last_seen"] += 1 - mock_unifi_websocket(message=MessageKey.CLIENT, data=client_on_ssid2) + mock_websocket_message(message=MessageKey.CLIENT, data=client_on_ssid2) await hass.async_block_till_done() new_time += timedelta( @@ -825,7 +825,7 @@ async def test_option_ssid_filter( @pytest.mark.usefixtures("mock_device_registry") async def test_wireless_client_go_wired_issue( hass: HomeAssistant, - mock_unifi_websocket, + mock_websocket_message, config_entry_factory: Callable[[], ConfigEntry], client_payload: list[dict[str, Any]], ) -> None: @@ -855,7 +855,7 @@ async def test_wireless_client_go_wired_issue( client = client_payload[0] client["last_seen"] = dt_util.as_timestamp(dt_util.utcnow()) client["is_wired"] = True - mock_unifi_websocket(message=MessageKey.CLIENT, data=client) + mock_websocket_message(message=MessageKey.CLIENT, data=client) await hass.async_block_till_done() # Wired bug fix keeps client marked as wireless @@ -876,7 +876,7 @@ async def test_wireless_client_go_wired_issue( # Try to mark client as connected client["last_seen"] += 1 - mock_unifi_websocket(message=MessageKey.CLIENT, data=client) + mock_websocket_message(message=MessageKey.CLIENT, data=client) await hass.async_block_till_done() # Make sure it don't go online again until wired bug disappears @@ -886,7 +886,7 @@ async def test_wireless_client_go_wired_issue( # Make client wireless client["last_seen"] += 1 client["is_wired"] = False - mock_unifi_websocket(message=MessageKey.CLIENT, data=client) + mock_websocket_message(message=MessageKey.CLIENT, data=client) await hass.async_block_till_done() # Client is no longer affected by wired bug and can be marked online @@ -898,7 +898,7 @@ async def test_wireless_client_go_wired_issue( @pytest.mark.usefixtures("mock_device_registry") async def test_option_ignore_wired_bug( hass: HomeAssistant, - mock_unifi_websocket, + mock_websocket_message, config_entry_factory: Callable[[], ConfigEntry], client_payload: list[dict[str, Any]], ) -> None: @@ -925,7 +925,7 @@ async def test_option_ignore_wired_bug( # Trigger wired bug client = client_payload[0] client["is_wired"] = True - mock_unifi_websocket(message=MessageKey.CLIENT, data=client) + mock_websocket_message(message=MessageKey.CLIENT, data=client) await hass.async_block_till_done() # Wired bug in effect @@ -946,7 +946,7 @@ async def test_option_ignore_wired_bug( # Mark client as connected again client["last_seen"] += 1 - mock_unifi_websocket(message=MessageKey.CLIENT, data=client) + mock_websocket_message(message=MessageKey.CLIENT, data=client) await hass.async_block_till_done() # Ignoring wired bug allows client to go home again even while affected @@ -956,7 +956,7 @@ async def test_option_ignore_wired_bug( # Make client wireless client["last_seen"] += 1 client["is_wired"] = False - mock_unifi_websocket(message=MessageKey.CLIENT, data=client) + mock_websocket_message(message=MessageKey.CLIENT, data=client) await hass.async_block_till_done() # Client is wireless and still connected diff --git a/tests/components/unifi/test_hub.py b/tests/components/unifi/test_hub.py index 932c95af4f9..312ad5cef93 100644 --- a/tests/components/unifi/test_hub.py +++ b/tests/components/unifi/test_hub.py @@ -80,7 +80,7 @@ async def test_reset_fails( async def test_connection_state_signalling( hass: HomeAssistant, mock_device_registry, - websocket_mock, + mock_websocket_state, config_entry_factory: Callable[[], ConfigEntry], client_payload: list[dict[str, Any]], ) -> None: @@ -99,17 +99,19 @@ async def test_connection_state_signalling( # Controller is connected assert hass.states.get("device_tracker.client").state == "home" - await websocket_mock.disconnect() + await mock_websocket_state.disconnect() # Controller is disconnected assert hass.states.get("device_tracker.client").state == "unavailable" - await websocket_mock.reconnect() + await mock_websocket_state.reconnect() # Controller is once again connected assert hass.states.get("device_tracker.client").state == "home" async def test_reconnect_mechanism( - aioclient_mock: AiohttpClientMocker, websocket_mock, config_entry_setup: ConfigEntry + aioclient_mock: AiohttpClientMocker, + mock_websocket_state, + config_entry_setup: ConfigEntry, ) -> None: """Verify reconnect prints only on first reconnection try.""" aioclient_mock.clear_requests() @@ -118,13 +120,13 @@ async def test_reconnect_mechanism( status=HTTPStatus.BAD_GATEWAY, ) - await websocket_mock.disconnect() + await mock_websocket_state.disconnect() assert aioclient_mock.call_count == 0 - await websocket_mock.reconnect(fail=True) + await mock_websocket_state.reconnect(fail=True) assert aioclient_mock.call_count == 1 - await websocket_mock.reconnect(fail=True) + await mock_websocket_state.reconnect(fail=True) assert aioclient_mock.call_count == 2 @@ -138,7 +140,7 @@ async def test_reconnect_mechanism( ], ) @pytest.mark.usefixtures("config_entry_setup") -async def test_reconnect_mechanism_exceptions(websocket_mock, exception) -> None: +async def test_reconnect_mechanism_exceptions(mock_websocket_state, exception) -> None: """Verify async_reconnect calls expected methods.""" with ( patch("aiounifi.Controller.login", side_effect=exception), @@ -146,9 +148,9 @@ async def test_reconnect_mechanism_exceptions(websocket_mock, exception) -> None "homeassistant.components.unifi.hub.hub.UnifiWebsocket.reconnect" ) as mock_reconnect, ): - await websocket_mock.disconnect() + await mock_websocket_state.disconnect() - await websocket_mock.reconnect() + await mock_websocket_state.reconnect() mock_reconnect.assert_called_once() diff --git a/tests/components/unifi/test_image.py b/tests/components/unifi/test_image.py index e92dcdd4d69..75d2f02900d 100644 --- a/tests/components/unifi/test_image.py +++ b/tests/components/unifi/test_image.py @@ -63,8 +63,8 @@ async def test_wlan_qr_code( entity_registry: er.EntityRegistry, hass_client: ClientSessionGenerator, snapshot: SnapshotAssertion, - mock_unifi_websocket, - websocket_mock, + mock_websocket_message, + mock_websocket_state, ) -> None: """Test the update_clients function when no clients are found.""" assert len(hass.states.async_entity_ids(IMAGE_DOMAIN)) == 0 @@ -96,7 +96,7 @@ async def test_wlan_qr_code( assert body == snapshot # Update state object - same password - no change to state - mock_unifi_websocket(message=MessageKey.WLAN_CONF_UPDATED, data=WLAN) + mock_websocket_message(message=MessageKey.WLAN_CONF_UPDATED, data=WLAN) await hass.async_block_till_done() image_state_2 = hass.states.get("image.ssid_1_qr_code") assert image_state_1.state == image_state_2.state @@ -104,7 +104,7 @@ async def test_wlan_qr_code( # Update state object - changed password - new state data = deepcopy(WLAN) data["x_passphrase"] = "new password" - mock_unifi_websocket(message=MessageKey.WLAN_CONF_UPDATED, data=data) + mock_websocket_message(message=MessageKey.WLAN_CONF_UPDATED, data=data) await hass.async_block_till_done() image_state_3 = hass.states.get("image.ssid_1_qr_code") assert image_state_1.state != image_state_3.state @@ -119,22 +119,22 @@ async def test_wlan_qr_code( # Availability signalling # Controller disconnects - await websocket_mock.disconnect() + await mock_websocket_state.disconnect() assert hass.states.get("image.ssid_1_qr_code").state == STATE_UNAVAILABLE # Controller reconnects - await websocket_mock.reconnect() + await mock_websocket_state.reconnect() assert hass.states.get("image.ssid_1_qr_code").state != STATE_UNAVAILABLE # WLAN gets disabled wlan_1 = deepcopy(WLAN) wlan_1["enabled"] = False - mock_unifi_websocket(message=MessageKey.WLAN_CONF_UPDATED, data=wlan_1) + mock_websocket_message(message=MessageKey.WLAN_CONF_UPDATED, data=wlan_1) await hass.async_block_till_done() assert hass.states.get("image.ssid_1_qr_code").state == STATE_UNAVAILABLE # WLAN gets re-enabled wlan_1["enabled"] = True - mock_unifi_websocket(message=MessageKey.WLAN_CONF_UPDATED, data=wlan_1) + mock_websocket_message(message=MessageKey.WLAN_CONF_UPDATED, data=wlan_1) await hass.async_block_till_done() assert hass.states.get("image.ssid_1_qr_code").state != STATE_UNAVAILABLE diff --git a/tests/components/unifi/test_init.py b/tests/components/unifi/test_init.py index 914f272e118..7cd203ab8fd 100644 --- a/tests/components/unifi/test_init.py +++ b/tests/components/unifi/test_init.py @@ -176,7 +176,7 @@ async def test_remove_config_entry_device( config_entry_factory: Callable[[], ConfigEntry], client_payload: list[dict[str, Any]], device_payload: list[dict[str, Any]], - mock_unifi_websocket, + mock_websocket_message, hass_ws_client: WebSocketGenerator, ) -> None: """Verify removing a device manually.""" @@ -206,7 +206,7 @@ async def test_remove_config_entry_device( ) # Remove a client from Unifi API - mock_unifi_websocket(message=MessageKey.CLIENT_REMOVED, data=[client_payload[1]]) + mock_websocket_message(message=MessageKey.CLIENT_REMOVED, data=[client_payload[1]]) await hass.async_block_till_done() # Try to remove an inactive client from UI: allowed diff --git a/tests/components/unifi/test_sensor.py b/tests/components/unifi/test_sensor.py index c8f9e9fb17e..3131eefbbee 100644 --- a/tests/components/unifi/test_sensor.py +++ b/tests/components/unifi/test_sensor.py @@ -355,7 +355,7 @@ async def test_no_clients(hass: HomeAssistant) -> None: ) async def test_bandwidth_sensors( hass: HomeAssistant, - mock_unifi_websocket, + mock_websocket_message, config_entry_options: MappingProxyType[str, Any], config_entry_setup: ConfigEntry, client_payload: list[dict[str, Any]], @@ -391,7 +391,7 @@ async def test_bandwidth_sensors( wireless_client["rx_bytes-r"] = 3456000000 wireless_client["tx_bytes-r"] = 7891000000 - mock_unifi_websocket(message=MessageKey.CLIENT, data=wireless_client) + mock_websocket_message(message=MessageKey.CLIENT, data=wireless_client) await hass.async_block_till_done() assert hass.states.get("sensor.wireless_client_rx").state == "3456.0" @@ -402,7 +402,7 @@ async def test_bandwidth_sensors( new_time = dt_util.utcnow() wireless_client["last_seen"] = dt_util.as_timestamp(new_time) - mock_unifi_websocket(message=MessageKey.CLIENT, data=wireless_client) + mock_websocket_message(message=MessageKey.CLIENT, data=wireless_client) await hass.async_block_till_done() with freeze_time(new_time): @@ -490,7 +490,7 @@ async def test_uptime_sensors( hass: HomeAssistant, entity_registry: er.EntityRegistry, freezer: FrozenDateTimeFactory, - mock_unifi_websocket, + mock_websocket_message, config_entry_options: MappingProxyType[str, Any], config_entry_factory: Callable[[], ConfigEntry], client_payload: list[dict[str, Any]], @@ -516,7 +516,7 @@ async def test_uptime_sensors( uptime_client["uptime"] = event_uptime now = datetime(2021, 1, 1, 1, 1, 4, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.now", return_value=now): - mock_unifi_websocket(message=MessageKey.CLIENT, data=uptime_client) + mock_websocket_message(message=MessageKey.CLIENT, data=uptime_client) await hass.async_block_till_done() assert hass.states.get("sensor.client1_uptime").state == "2021-01-01T01:00:00+00:00" @@ -526,7 +526,7 @@ async def test_uptime_sensors( uptime_client["uptime"] = new_uptime now = datetime(2021, 2, 1, 1, 1, 0, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.now", return_value=now): - mock_unifi_websocket(message=MessageKey.CLIENT, data=uptime_client) + mock_websocket_message(message=MessageKey.CLIENT, data=uptime_client) await hass.async_block_till_done() assert hass.states.get("sensor.client1_uptime").state == "2021-02-01T01:00:00+00:00" @@ -583,7 +583,7 @@ async def test_uptime_sensors( @pytest.mark.usefixtures("config_entry_setup") @pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_remove_sensors( - hass: HomeAssistant, mock_unifi_websocket, client_payload: list[dict[str, Any]] + hass: HomeAssistant, mock_websocket_message, client_payload: list[dict[str, Any]] ) -> None: """Verify removing of clients work as expected.""" assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 6 @@ -595,7 +595,7 @@ async def test_remove_sensors( assert hass.states.get("sensor.wireless_client_uptime") # Remove wired client - mock_unifi_websocket(message=MessageKey.CLIENT_REMOVED, data=client_payload[0]) + mock_websocket_message(message=MessageKey.CLIENT_REMOVED, data=client_payload[0]) await hass.async_block_till_done() assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 3 @@ -612,8 +612,8 @@ async def test_remove_sensors( async def test_poe_port_switches( hass: HomeAssistant, entity_registry: er.EntityRegistry, - mock_unifi_websocket, - websocket_mock, + mock_websocket_message, + mock_websocket_state, ) -> None: """Test the update_items function with some clients.""" assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 2 @@ -642,34 +642,34 @@ async def test_poe_port_switches( # Update state object device_1 = deepcopy(DEVICE_1) device_1["port_table"][0]["poe_power"] = "5.12" - mock_unifi_websocket(message=MessageKey.DEVICE, data=device_1) + mock_websocket_message(message=MessageKey.DEVICE, data=device_1) await hass.async_block_till_done() assert hass.states.get("sensor.mock_name_port_1_poe_power").state == "5.12" # PoE is disabled device_1 = deepcopy(DEVICE_1) device_1["port_table"][0]["poe_mode"] = "off" - mock_unifi_websocket(message=MessageKey.DEVICE, data=device_1) + mock_websocket_message(message=MessageKey.DEVICE, data=device_1) await hass.async_block_till_done() assert hass.states.get("sensor.mock_name_port_1_poe_power").state == "0" # Availability signalling # Controller disconnects - await websocket_mock.disconnect() + await mock_websocket_state.disconnect() assert ( hass.states.get("sensor.mock_name_port_1_poe_power").state == STATE_UNAVAILABLE ) # Controller reconnects - await websocket_mock.reconnect() + await mock_websocket_state.reconnect() assert ( hass.states.get("sensor.mock_name_port_1_poe_power").state != STATE_UNAVAILABLE ) # Device gets disabled device_1["disabled"] = True - mock_unifi_websocket(message=MessageKey.DEVICE, data=device_1) + mock_websocket_message(message=MessageKey.DEVICE, data=device_1) await hass.async_block_till_done() assert ( hass.states.get("sensor.mock_name_port_1_poe_power").state == STATE_UNAVAILABLE @@ -677,7 +677,7 @@ async def test_poe_port_switches( # Device gets re-enabled device_1["disabled"] = False - mock_unifi_websocket(message=MessageKey.DEVICE, data=device_1) + mock_websocket_message(message=MessageKey.DEVICE, data=device_1) await hass.async_block_till_done() assert hass.states.get("sensor.mock_name_port_1_poe_power") @@ -686,8 +686,8 @@ async def test_poe_port_switches( async def test_wlan_client_sensors( hass: HomeAssistant, entity_registry: er.EntityRegistry, - mock_unifi_websocket, - websocket_mock, + mock_websocket_message, + mock_websocket_state, config_entry_factory: Callable[[], ConfigEntry], client_payload: list[dict[str, Any]], ) -> None: @@ -730,10 +730,10 @@ async def test_wlan_client_sensors( # Verify state update - increasing number wireless_client_1 = client_payload[0] wireless_client_1["essid"] = "SSID 1" - mock_unifi_websocket(message=MessageKey.CLIENT, data=wireless_client_1) + mock_websocket_message(message=MessageKey.CLIENT, data=wireless_client_1) wireless_client_2 = client_payload[1] wireless_client_2["essid"] = "SSID 1" - mock_unifi_websocket(message=MessageKey.CLIENT, data=wireless_client_2) + mock_websocket_message(message=MessageKey.CLIENT, data=wireless_client_2) await hass.async_block_till_done() ssid_1 = hass.states.get("sensor.ssid_1") @@ -748,7 +748,7 @@ async def test_wlan_client_sensors( # Verify state update - decreasing number wireless_client_1["essid"] = "SSID" - mock_unifi_websocket(message=MessageKey.CLIENT, data=wireless_client_1) + mock_websocket_message(message=MessageKey.CLIENT, data=wireless_client_1) async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL) await hass.async_block_till_done() @@ -759,7 +759,7 @@ async def test_wlan_client_sensors( # Verify state update - decreasing number wireless_client_2["last_seen"] = 0 - mock_unifi_websocket(message=MessageKey.CLIENT, data=wireless_client_2) + mock_websocket_message(message=MessageKey.CLIENT, data=wireless_client_2) async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL) await hass.async_block_till_done() @@ -770,23 +770,23 @@ async def test_wlan_client_sensors( # Availability signalling # Controller disconnects - await websocket_mock.disconnect() + await mock_websocket_state.disconnect() assert hass.states.get("sensor.ssid_1").state == STATE_UNAVAILABLE # Controller reconnects - await websocket_mock.reconnect() + await mock_websocket_state.reconnect() assert hass.states.get("sensor.ssid_1").state == "0" # WLAN gets disabled wlan_1 = deepcopy(WLAN) wlan_1["enabled"] = False - mock_unifi_websocket(message=MessageKey.WLAN_CONF_UPDATED, data=wlan_1) + mock_websocket_message(message=MessageKey.WLAN_CONF_UPDATED, data=wlan_1) await hass.async_block_till_done() assert hass.states.get("sensor.ssid_1").state == STATE_UNAVAILABLE # WLAN gets re-enabled wlan_1["enabled"] = True - mock_unifi_websocket(message=MessageKey.WLAN_CONF_UPDATED, data=wlan_1) + mock_websocket_message(message=MessageKey.WLAN_CONF_UPDATED, data=wlan_1) await hass.async_block_till_done() assert hass.states.get("sensor.ssid_1").state == "0" @@ -828,7 +828,7 @@ async def test_wlan_client_sensors( async def test_outlet_power_readings( hass: HomeAssistant, entity_registry: er.EntityRegistry, - mock_unifi_websocket, + mock_websocket_message, device_payload: list[dict[str, Any]], entity_id: str, expected_unique_id: str, @@ -852,7 +852,7 @@ async def test_outlet_power_readings( updated_device_data = deepcopy(device_payload[0]) updated_device_data.update(changed_data) - mock_unifi_websocket(message=MessageKey.DEVICE, data=updated_device_data) + mock_websocket_message(message=MessageKey.DEVICE, data=updated_device_data) await hass.async_block_till_done() sensor_data = hass.states.get(f"sensor.{entity_id}") @@ -887,7 +887,7 @@ async def test_outlet_power_readings( async def test_device_uptime( hass: HomeAssistant, entity_registry: er.EntityRegistry, - mock_unifi_websocket, + mock_websocket_message, config_entry_factory: Callable[[], ConfigEntry], device_payload: list[dict[str, Any]], ) -> None: @@ -909,7 +909,7 @@ async def test_device_uptime( device["uptime"] = 64 now = datetime(2021, 1, 1, 1, 1, 4, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.now", return_value=now): - mock_unifi_websocket(message=MessageKey.DEVICE, data=device) + mock_websocket_message(message=MessageKey.DEVICE, data=device) assert hass.states.get("sensor.device_uptime").state == "2021-01-01T01:00:00+00:00" @@ -919,7 +919,7 @@ async def test_device_uptime( device["uptime"] = 60 now = datetime(2021, 2, 1, 1, 1, 0, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.now", return_value=now): - mock_unifi_websocket(message=MessageKey.DEVICE, data=device) + mock_websocket_message(message=MessageKey.DEVICE, data=device) assert hass.states.get("sensor.device_uptime").state == "2021-02-01T01:00:00+00:00" @@ -955,7 +955,7 @@ async def test_device_uptime( async def test_device_temperature( hass: HomeAssistant, entity_registry: er.EntityRegistry, - mock_unifi_websocket, + mock_websocket_message, device_payload: list[dict[str, Any]], ) -> None: """Verify that temperature sensors are working as expected.""" @@ -969,7 +969,7 @@ async def test_device_temperature( # Verify new event change temperature device = device_payload[0] device["general_temperature"] = 60 - mock_unifi_websocket(message=MessageKey.DEVICE, data=device) + mock_websocket_message(message=MessageKey.DEVICE, data=device) assert hass.states.get("sensor.device_temperature").state == "60" @@ -1004,7 +1004,7 @@ async def test_device_temperature( async def test_device_state( hass: HomeAssistant, entity_registry: er.EntityRegistry, - mock_unifi_websocket, + mock_websocket_message, device_payload: list[dict[str, Any]], ) -> None: """Verify that state sensors are working as expected.""" @@ -1017,7 +1017,7 @@ async def test_device_state( device = device_payload[0] for i in list(map(int, DeviceState)): device["state"] = i - mock_unifi_websocket(message=MessageKey.DEVICE, data=device) + mock_websocket_message(message=MessageKey.DEVICE, data=device) assert hass.states.get("sensor.device_state").state == DEVICE_STATES[i] @@ -1041,7 +1041,7 @@ async def test_device_state( async def test_device_system_stats( hass: HomeAssistant, entity_registry: er.EntityRegistry, - mock_unifi_websocket, + mock_websocket_message, device_payload: list[dict[str, Any]], ) -> None: """Verify that device stats sensors are working as expected.""" @@ -1065,7 +1065,7 @@ async def test_device_system_stats( # Verify new event change system-stats device = device_payload[0] device["system-stats"] = {"cpu": 7.7, "mem": 33.3, "uptime": 7316} - mock_unifi_websocket(message=MessageKey.DEVICE, data=device) + mock_websocket_message(message=MessageKey.DEVICE, data=device) assert hass.states.get("sensor.device_cpu_utilization").state == "7.7" assert hass.states.get("sensor.device_memory_utilization").state == "33.3" @@ -1138,7 +1138,7 @@ async def test_device_system_stats( async def test_bandwidth_port_sensors( hass: HomeAssistant, entity_registry: er.EntityRegistry, - mock_unifi_websocket, + mock_websocket_message, config_entry_setup: ConfigEntry, config_entry_options: MappingProxyType[str, Any], device_payload, @@ -1206,7 +1206,7 @@ async def test_bandwidth_port_sensors( device_1["port_table"][0]["rx_bytes-r"] = 3456000000 device_1["port_table"][0]["tx_bytes-r"] = 7891000000 - mock_unifi_websocket(message=MessageKey.DEVICE, data=device_1) + mock_websocket_message(message=MessageKey.DEVICE, data=device_1) await hass.async_block_till_done() assert hass.states.get("sensor.mock_name_port_1_rx").state == "27648.00000" diff --git a/tests/components/unifi/test_switch.py b/tests/components/unifi/test_switch.py index 4d5661a48ba..851f0107c39 100644 --- a/tests/components/unifi/test_switch.py +++ b/tests/components/unifi/test_switch.py @@ -886,14 +886,14 @@ async def test_switches( @pytest.mark.parametrize("dpi_app_payload", [DPI_APPS]) @pytest.mark.parametrize("dpi_group_payload", [DPI_GROUPS]) @pytest.mark.usefixtures("config_entry_setup") -async def test_remove_switches(hass: HomeAssistant, mock_unifi_websocket) -> None: +async def test_remove_switches(hass: HomeAssistant, mock_websocket_message) -> None: """Test the update_items function with some clients.""" assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 2 assert hass.states.get("switch.block_client_2") is not None assert hass.states.get("switch.block_media_streaming") is not None - mock_unifi_websocket(message=MessageKey.CLIENT_REMOVED, data=[UNBLOCKED]) + mock_websocket_message(message=MessageKey.CLIENT_REMOVED, data=[UNBLOCKED]) await hass.async_block_till_done() assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 1 @@ -901,7 +901,7 @@ async def test_remove_switches(hass: HomeAssistant, mock_unifi_websocket) -> Non assert hass.states.get("switch.block_client_2") is None assert hass.states.get("switch.block_media_streaming") is not None - mock_unifi_websocket(data=DPI_GROUP_REMOVED_EVENT) + mock_websocket_message(data=DPI_GROUP_REMOVED_EVENT) await hass.async_block_till_done() assert hass.states.get("switch.block_media_streaming") is None @@ -923,7 +923,7 @@ async def test_remove_switches(hass: HomeAssistant, mock_unifi_websocket) -> Non async def test_block_switches( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, - mock_unifi_websocket, + mock_websocket_message, config_entry_setup, ) -> None: """Test the update_items function with some clients.""" @@ -939,7 +939,9 @@ async def test_block_switches( assert unblocked is not None assert unblocked.state == "on" - mock_unifi_websocket(message=MessageKey.EVENT, data=EVENT_BLOCKED_CLIENT_UNBLOCKED) + mock_websocket_message( + message=MessageKey.EVENT, data=EVENT_BLOCKED_CLIENT_UNBLOCKED + ) await hass.async_block_till_done() assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 2 @@ -947,7 +949,7 @@ async def test_block_switches( assert blocked is not None assert blocked.state == "on" - mock_unifi_websocket(message=MessageKey.EVENT, data=EVENT_BLOCKED_CLIENT_BLOCKED) + mock_websocket_message(message=MessageKey.EVENT, data=EVENT_BLOCKED_CLIENT_BLOCKED) await hass.async_block_till_done() assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 2 @@ -984,7 +986,7 @@ async def test_block_switches( @pytest.mark.parametrize("dpi_group_payload", [DPI_GROUPS]) @pytest.mark.usefixtures("config_entry_setup") async def test_dpi_switches( - hass: HomeAssistant, mock_unifi_websocket, websocket_mock + hass: HomeAssistant, mock_websocket_message, mock_websocket_state ) -> None: """Test the update_items function with some clients.""" assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 1 @@ -994,7 +996,7 @@ async def test_dpi_switches( assert dpi_switch.state == STATE_ON assert dpi_switch.attributes["icon"] == "mdi:network" - mock_unifi_websocket(data=DPI_APP_DISABLED_EVENT) + mock_websocket_message(data=DPI_APP_DISABLED_EVENT) await hass.async_block_till_done() assert hass.states.get("switch.block_media_streaming").state == STATE_OFF @@ -1002,15 +1004,15 @@ async def test_dpi_switches( # Availability signalling # Controller disconnects - await websocket_mock.disconnect() + await mock_websocket_state.disconnect() assert hass.states.get("switch.block_media_streaming").state == STATE_UNAVAILABLE # Controller reconnects - await websocket_mock.reconnect() + await mock_websocket_state.reconnect() assert hass.states.get("switch.block_media_streaming").state == STATE_OFF # Remove app - mock_unifi_websocket(data=DPI_GROUP_REMOVE_APP) + mock_websocket_message(data=DPI_GROUP_REMOVE_APP) await hass.async_block_till_done() assert hass.states.get("switch.block_media_streaming") is None @@ -1021,7 +1023,7 @@ async def test_dpi_switches( @pytest.mark.parametrize("dpi_group_payload", [DPI_GROUPS]) @pytest.mark.usefixtures("config_entry_setup") async def test_dpi_switches_add_second_app( - hass: HomeAssistant, mock_unifi_websocket + hass: HomeAssistant, mock_websocket_message ) -> None: """Test the update_items function with some clients.""" assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 1 @@ -1036,7 +1038,7 @@ async def test_dpi_switches_add_second_app( "site_id": "name", "_id": "61783e89c1773a18c0c61f00", } - mock_unifi_websocket(message=MessageKey.DPI_APP_ADDED, data=second_app_event) + mock_websocket_message(message=MessageKey.DPI_APP_ADDED, data=second_app_event) await hass.async_block_till_done() assert hass.states.get("switch.block_media_streaming").state == STATE_ON @@ -1047,7 +1049,7 @@ async def test_dpi_switches_add_second_app( "site_id": "name", "dpiapp_ids": ["5f976f62e3c58f018ec7e17d", "61783e89c1773a18c0c61f00"], } - mock_unifi_websocket( + mock_websocket_message( message=MessageKey.DPI_GROUP_UPDATED, data=add_second_app_to_group ) await hass.async_block_till_done() @@ -1063,7 +1065,7 @@ async def test_dpi_switches_add_second_app( "site_id": "name", "_id": "61783e89c1773a18c0c61f00", } - mock_unifi_websocket( + mock_websocket_message( message=MessageKey.DPI_APP_UPDATED, data=second_app_event_enabled ) await hass.async_block_till_done() @@ -1082,10 +1084,10 @@ async def test_dpi_switches_add_second_app( async def test_outlet_switches( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, - mock_unifi_websocket, + mock_websocket_message, config_entry_setup, device_payload, - websocket_mock, + mock_websocket_state, entity_id: str, outlet_index: int, expected_switches: int, @@ -1104,7 +1106,7 @@ async def test_outlet_switches( # Update state object device_1 = deepcopy(device_payload[0]) device_1["outlet_table"][outlet_index - 1]["relay_state"] = False - mock_unifi_websocket(message=MessageKey.DEVICE, data=device_1) + mock_websocket_message(message=MessageKey.DEVICE, data=device_1) await hass.async_block_till_done() assert hass.states.get(f"switch.{entity_id}").state == STATE_OFF @@ -1149,22 +1151,22 @@ async def test_outlet_switches( # Availability signalling # Controller disconnects - await websocket_mock.disconnect() + await mock_websocket_state.disconnect() assert hass.states.get(f"switch.{entity_id}").state == STATE_UNAVAILABLE # Controller reconnects - await websocket_mock.reconnect() + await mock_websocket_state.reconnect() assert hass.states.get(f"switch.{entity_id}").state == STATE_OFF # Device gets disabled device_1["disabled"] = True - mock_unifi_websocket(message=MessageKey.DEVICE, data=device_1) + mock_websocket_message(message=MessageKey.DEVICE, data=device_1) await hass.async_block_till_done() assert hass.states.get(f"switch.{entity_id}").state == STATE_UNAVAILABLE # Device gets re-enabled device_1["disabled"] = False - mock_unifi_websocket(message=MessageKey.DEVICE, data=device_1) + mock_websocket_message(message=MessageKey.DEVICE, data=device_1) await hass.async_block_till_done() assert hass.states.get(f"switch.{entity_id}").state == STATE_OFF @@ -1191,13 +1193,13 @@ async def test_outlet_switches( ) @pytest.mark.usefixtures("config_entry_setup") async def test_new_client_discovered_on_block_control( - hass: HomeAssistant, mock_unifi_websocket + hass: HomeAssistant, mock_websocket_message ) -> None: """Test if 2nd update has a new client.""" assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 0 assert hass.states.get("switch.block_client_1") is None - mock_unifi_websocket(message=MessageKey.CLIENT, data=BLOCKED) + mock_websocket_message(message=MessageKey.CLIENT, data=BLOCKED) await hass.async_block_till_done() assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 1 @@ -1281,8 +1283,8 @@ async def test_poe_port_switches( hass: HomeAssistant, entity_registry: er.EntityRegistry, aioclient_mock: AiohttpClientMocker, - mock_unifi_websocket, - websocket_mock, + mock_websocket_message, + mock_websocket_state, config_entry_setup, device_payload, ) -> None: @@ -1319,7 +1321,7 @@ async def test_poe_port_switches( # Update state object device_1 = deepcopy(device_payload[0]) device_1["port_table"][0]["poe_mode"] = "off" - mock_unifi_websocket(message=MessageKey.DEVICE, data=device_1) + mock_websocket_message(message=MessageKey.DEVICE, data=device_1) await hass.async_block_till_done() assert hass.states.get("switch.mock_name_port_1_poe").state == STATE_OFF @@ -1369,22 +1371,22 @@ async def test_poe_port_switches( # Availability signalling # Controller disconnects - await websocket_mock.disconnect() + await mock_websocket_state.disconnect() assert hass.states.get("switch.mock_name_port_1_poe").state == STATE_UNAVAILABLE # Controller reconnects - await websocket_mock.reconnect() + await mock_websocket_state.reconnect() assert hass.states.get("switch.mock_name_port_1_poe").state == STATE_OFF # Device gets disabled device_1["disabled"] = True - mock_unifi_websocket(message=MessageKey.DEVICE, data=device_1) + mock_websocket_message(message=MessageKey.DEVICE, data=device_1) await hass.async_block_till_done() assert hass.states.get("switch.mock_name_port_1_poe").state == STATE_UNAVAILABLE # Device gets re-enabled device_1["disabled"] = False - mock_unifi_websocket(message=MessageKey.DEVICE, data=device_1) + mock_websocket_message(message=MessageKey.DEVICE, data=device_1) await hass.async_block_till_done() assert hass.states.get("switch.mock_name_port_1_poe").state == STATE_OFF @@ -1394,8 +1396,8 @@ async def test_wlan_switches( hass: HomeAssistant, entity_registry: er.EntityRegistry, aioclient_mock: AiohttpClientMocker, - mock_unifi_websocket, - websocket_mock, + mock_websocket_message, + mock_websocket_state, config_entry_setup, wlan_payload, ) -> None: @@ -1417,7 +1419,7 @@ async def test_wlan_switches( # Update state object wlan = deepcopy(wlan_payload[0]) wlan["enabled"] = False - mock_unifi_websocket(message=MessageKey.WLAN_CONF_UPDATED, data=wlan) + mock_websocket_message(message=MessageKey.WLAN_CONF_UPDATED, data=wlan) await hass.async_block_till_done() assert hass.states.get("switch.ssid_1").state == STATE_OFF @@ -1450,11 +1452,11 @@ async def test_wlan_switches( # Availability signalling # Controller disconnects - await websocket_mock.disconnect() + await mock_websocket_state.disconnect() assert hass.states.get("switch.ssid_1").state == STATE_UNAVAILABLE # Controller reconnects - await websocket_mock.reconnect() + await mock_websocket_state.reconnect() assert hass.states.get("switch.ssid_1").state == STATE_OFF @@ -1481,8 +1483,8 @@ async def test_port_forwarding_switches( hass: HomeAssistant, entity_registry: er.EntityRegistry, aioclient_mock: AiohttpClientMocker, - mock_unifi_websocket, - websocket_mock, + mock_websocket_message, + mock_websocket_state, config_entry_setup, port_forward_payload, ) -> None: @@ -1503,7 +1505,7 @@ async def test_port_forwarding_switches( # Update state object data = port_forward_payload[0].copy() data["enabled"] = False - mock_unifi_websocket(message=MessageKey.PORT_FORWARD_UPDATED, data=data) + mock_websocket_message(message=MessageKey.PORT_FORWARD_UPDATED, data=data) await hass.async_block_till_done() assert hass.states.get("switch.unifi_network_plex").state == STATE_OFF @@ -1538,15 +1540,15 @@ async def test_port_forwarding_switches( # Availability signalling # Controller disconnects - await websocket_mock.disconnect() + await mock_websocket_state.disconnect() assert hass.states.get("switch.unifi_network_plex").state == STATE_UNAVAILABLE # Controller reconnects - await websocket_mock.reconnect() + await mock_websocket_state.reconnect() assert hass.states.get("switch.unifi_network_plex").state == STATE_OFF # Remove entity on deleted message - mock_unifi_websocket( + mock_websocket_message( message=MessageKey.PORT_FORWARD_DELETED, data=port_forward_payload[0] ) await hass.async_block_till_done() diff --git a/tests/components/unifi/test_update.py b/tests/components/unifi/test_update.py index 5f9039aa48e..c44b2993a8b 100644 --- a/tests/components/unifi/test_update.py +++ b/tests/components/unifi/test_update.py @@ -61,7 +61,7 @@ DEVICE_2 = { @pytest.mark.parametrize("device_payload", [[DEVICE_1, DEVICE_2]]) @pytest.mark.usefixtures("config_entry_setup") -async def test_device_updates(hass: HomeAssistant, mock_unifi_websocket) -> None: +async def test_device_updates(hass: HomeAssistant, mock_websocket_message) -> None: """Test the update_items function with some devices.""" assert len(hass.states.async_entity_ids(UPDATE_DOMAIN)) == 2 @@ -95,7 +95,7 @@ async def test_device_updates(hass: HomeAssistant, mock_unifi_websocket) -> None device_1 = deepcopy(DEVICE_1) device_1["state"] = 4 - mock_unifi_websocket(message=MessageKey.DEVICE, data=device_1) + mock_websocket_message(message=MessageKey.DEVICE, data=device_1) await hass.async_block_till_done() device_1_state = hass.states.get("update.device_1") @@ -110,7 +110,7 @@ async def test_device_updates(hass: HomeAssistant, mock_unifi_websocket) -> None device_1["version"] = "4.3.17.11279" device_1["upgradable"] = False del device_1["upgrade_to_firmware"] - mock_unifi_websocket(message=MessageKey.DEVICE, data=device_1) + mock_websocket_message(message=MessageKey.DEVICE, data=device_1) await hass.async_block_till_done() device_1_state = hass.states.get("update.device_1") @@ -173,15 +173,15 @@ async def test_install( @pytest.mark.parametrize("device_payload", [[DEVICE_1]]) @pytest.mark.usefixtures("config_entry_setup") -async def test_hub_state_change(hass: HomeAssistant, websocket_mock) -> None: +async def test_hub_state_change(hass: HomeAssistant, mock_websocket_state) -> None: """Verify entities state reflect on hub becoming unavailable.""" assert len(hass.states.async_entity_ids(UPDATE_DOMAIN)) == 1 assert hass.states.get("update.device_1").state == STATE_ON # Controller unavailable - await websocket_mock.disconnect() + await mock_websocket_state.disconnect() assert hass.states.get("update.device_1").state == STATE_UNAVAILABLE # Controller available - await websocket_mock.reconnect() + await mock_websocket_state.reconnect() assert hass.states.get("update.device_1").state == STATE_ON From b26f613d0651879c8e76b6ec641b27ff5d1c801c Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sun, 9 Jun 2024 16:01:19 +0200 Subject: [PATCH 0411/1445] Add config flow to MPD (#117907) --- homeassistant/components/mpd/__init__.py | 23 ++- homeassistant/components/mpd/config_flow.py | 101 ++++++++++ homeassistant/components/mpd/const.py | 7 + homeassistant/components/mpd/media_player.py | 83 ++++++-- homeassistant/components/mpd/strings.json | 33 ++++ requirements_test_all.txt | 3 + tests/components/mpd/__init__.py | 1 + tests/components/mpd/conftest.py | 43 +++++ tests/components/mpd/test_config_flow.py | 191 +++++++++++++++++++ 9 files changed, 469 insertions(+), 16 deletions(-) create mode 100644 homeassistant/components/mpd/config_flow.py create mode 100644 homeassistant/components/mpd/const.py create mode 100644 homeassistant/components/mpd/strings.json create mode 100644 tests/components/mpd/__init__.py create mode 100644 tests/components/mpd/conftest.py create mode 100644 tests/components/mpd/test_config_flow.py diff --git a/homeassistant/components/mpd/__init__.py b/homeassistant/components/mpd/__init__.py index bf917ff19aa..01ea159cf02 100644 --- a/homeassistant/components/mpd/__init__.py +++ b/homeassistant/components/mpd/__init__.py @@ -1 +1,22 @@ -"""The mpd component.""" +"""The Music Player Daemon integration.""" + +from __future__ import annotations + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant + +PLATFORMS: list[Platform] = [Platform.MEDIA_PLAYER] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Music Player Daemon from a config entry.""" + + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/mpd/config_flow.py b/homeassistant/components/mpd/config_flow.py new file mode 100644 index 00000000000..619fb8936e2 --- /dev/null +++ b/homeassistant/components/mpd/config_flow.py @@ -0,0 +1,101 @@ +"""Music Player Daemon config flow.""" + +from asyncio import timeout +from contextlib import suppress +from socket import gaierror +from typing import Any + +import mpd +from mpd.asyncio import MPDClient +import voluptuous as vol + +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult +from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_PORT + +from .const import DOMAIN, LOGGER + +SCHEMA = vol.Schema( + { + vol.Required(CONF_HOST): str, + vol.Optional(CONF_PASSWORD): str, + vol.Optional(CONF_PORT, default=6600): int, + } +) + + +class MPDConfigFlow(ConfigFlow, domain=DOMAIN): + """Music Player Daemon config flow.""" + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle a flow initiated by the user.""" + errors = {} + if user_input: + self._async_abort_entries_match({CONF_HOST: user_input[CONF_HOST]}) + client = MPDClient() + client.timeout = 30 + client.idletimeout = 10 + try: + async with timeout(35): + await client.connect(user_input[CONF_HOST], user_input[CONF_PORT]) + if CONF_PASSWORD in user_input: + await client.password(user_input[CONF_PASSWORD]) + with suppress(mpd.ConnectionError): + client.disconnect() + except ( + TimeoutError, + gaierror, + mpd.ConnectionError, + OSError, + ): + errors["base"] = "cannot_connect" + except Exception: # noqa: BLE001 + LOGGER.exception("Unknown exception") + errors["base"] = "unknown" + else: + return self.async_create_entry( + title="Music Player Daemon", + data=user_input, + ) + + return self.async_show_form( + step_id="user", + data_schema=SCHEMA, + errors=errors, + ) + + async def async_step_import( + self, import_config: dict[str, Any] + ) -> ConfigFlowResult: + """Attempt to import the existing configuration.""" + self._async_abort_entries_match({CONF_HOST: import_config[CONF_HOST]}) + client = MPDClient() + client.timeout = 30 + client.idletimeout = 10 + try: + async with timeout(35): + await client.connect(import_config[CONF_HOST], import_config[CONF_PORT]) + if CONF_PASSWORD in import_config: + await client.password(import_config[CONF_PASSWORD]) + with suppress(mpd.ConnectionError): + client.disconnect() + except ( + TimeoutError, + gaierror, + mpd.ConnectionError, + OSError, + ): + return self.async_abort(reason="cannot_connect") + except Exception: # noqa: BLE001 + LOGGER.exception("Unknown exception") + return self.async_abort(reason="unknown") + + return self.async_create_entry( + title=import_config.get(CONF_NAME, "Music Player Daemon"), + data={ + CONF_HOST: import_config[CONF_HOST], + CONF_PORT: import_config[CONF_PORT], + CONF_PASSWORD: import_config.get(CONF_PASSWORD), + }, + ) diff --git a/homeassistant/components/mpd/const.py b/homeassistant/components/mpd/const.py new file mode 100644 index 00000000000..0aed3bb8106 --- /dev/null +++ b/homeassistant/components/mpd/const.py @@ -0,0 +1,7 @@ +"""Constants for the MPD integration.""" + +import logging + +DOMAIN = "mpd" + +LOGGER = logging.getLogger(__package__) diff --git a/homeassistant/components/mpd/media_player.py b/homeassistant/components/mpd/media_player.py index 7f69b7bf914..f0df2cdbbe2 100644 --- a/homeassistant/components/mpd/media_player.py +++ b/homeassistant/components/mpd/media_player.py @@ -26,15 +26,18 @@ from homeassistant.components.media_player import ( RepeatMode, async_process_play_media_url, ) +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_PORT -from homeassistant.core import HomeAssistant +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant +from homeassistant.data_entry_flow import FlowResultType import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util import Throttle import homeassistant.util.dt as dt_util -_LOGGER = logging.getLogger(__name__) +from .const import DOMAIN, LOGGER DEFAULT_NAME = "MPD" DEFAULT_PORT = 6600 @@ -74,13 +77,63 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the MPD platform.""" - host = config.get(CONF_HOST) - port = config.get(CONF_PORT) - name = config.get(CONF_NAME) - password = config.get(CONF_PASSWORD) - entity = MpdDevice(host, port, password, name) - async_add_entities([entity], True) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=config, + ) + if ( + result["type"] is FlowResultType.CREATE_ENTRY + or result["reason"] == "single_instance_allowed" + ): + async_create_issue( + hass, + HOMEASSISTANT_DOMAIN, + f"deprecated_yaml_{DOMAIN}", + breaks_in_ha_version="2024.12.0", + is_fixable=False, + issue_domain=DOMAIN, + severity=IssueSeverity.WARNING, + translation_key="deprecated_yaml", + translation_placeholders={ + "domain": DOMAIN, + "integration_title": "Music Player Daemon", + }, + ) + return + async_create_issue( + hass, + DOMAIN, + f"deprecated_yaml_import_issue_{result['reason']}", + breaks_in_ha_version="2024.12.0", + is_fixable=False, + issue_domain=DOMAIN, + severity=IssueSeverity.WARNING, + translation_key=f"deprecated_yaml_import_issue_{result['reason']}", + translation_placeholders={ + "domain": DOMAIN, + "integration_title": "Music Player Daemon", + }, + ) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up media player from config_entry.""" + + async_add_entities( + [ + MpdDevice( + entry.data[CONF_HOST], + entry.data[CONF_PORT], + entry.data.get(CONF_PASSWORD), + entry.title, + ) + ], + True, + ) class MpdDevice(MediaPlayerEntity): @@ -148,7 +201,7 @@ class MpdDevice(MediaPlayerEntity): log_level = logging.DEBUG if self._is_available is not False: log_level = logging.WARNING - _LOGGER.log( + LOGGER.log( log_level, "Error connecting to '%s': %s", self.server, error ) self._is_available = False @@ -181,7 +234,7 @@ class MpdDevice(MediaPlayerEntity): await self._update_playlists() except (mpd.ConnectionError, ValueError) as error: - _LOGGER.debug("Error updating status: %s", error) + LOGGER.debug("Error updating status: %s", error) @property def available(self) -> bool: @@ -340,7 +393,7 @@ class MpdDevice(MediaPlayerEntity): response = await self._client.readpicture(file) except mpd.CommandError as error: if error.errno is not mpd.FailureResponseCode.NO_EXIST: - _LOGGER.warning( + LOGGER.warning( "Retrieving artwork through `readpicture` command failed: %s", error, ) @@ -352,7 +405,7 @@ class MpdDevice(MediaPlayerEntity): response = await self._client.albumart(file) except mpd.CommandError as error: if error.errno is not mpd.FailureResponseCode.NO_EXIST: - _LOGGER.warning( + LOGGER.warning( "Retrieving artwork through `albumart` command failed: %s", error, ) @@ -412,7 +465,7 @@ class MpdDevice(MediaPlayerEntity): self._playlists.append(playlist_data["playlist"]) except mpd.CommandError as error: self._playlists = None - _LOGGER.warning("Playlists could not be updated: %s:", error) + LOGGER.warning("Playlists could not be updated: %s:", error) async def async_set_volume_level(self, volume: float) -> None: """Set volume of media player.""" @@ -489,12 +542,12 @@ class MpdDevice(MediaPlayerEntity): media_id = async_process_play_media_url(self.hass, play_item.url) if media_type == MediaType.PLAYLIST: - _LOGGER.debug("Playing playlist: %s", media_id) + LOGGER.debug("Playing playlist: %s", media_id) if media_id in self._playlists: self._currentplaylist = media_id else: self._currentplaylist = None - _LOGGER.warning("Unknown playlist name %s", media_id) + LOGGER.warning("Unknown playlist name %s", media_id) await self._client.clear() await self._client.load(media_id) await self._client.play() diff --git a/homeassistant/components/mpd/strings.json b/homeassistant/components/mpd/strings.json new file mode 100644 index 00000000000..fc922ab128a --- /dev/null +++ b/homeassistant/components/mpd/strings.json @@ -0,0 +1,33 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "[%key:common::config_flow::data::host%]", + "password": "[%key:common::config_flow::data::password%]", + "port": "[%key:common::config_flow::data::port%]" + }, + "data_description": { + "host": "The hostname or IP address of your Music Player Daemon instance." + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + }, + "issues": { + "deprecated_yaml_import_issue_cannot_connect": { + "title": "The {integration_title} YAML configuration import cannot connect to daemon", + "description": "Configuring {integration_title} using YAML is being removed but there was a connection error importing your YAML configuration.\n\nPlease make sure {integration_title} is turned on, and restart Home Assistant to try importing again. Otherwise, please remove the YAML from your configuration and add the integration manually." + }, + "deprecated_yaml_import_issue_unknown": { + "title": "The {integration_title} YAML configuration could not be imported", + "description": "Configuring {integration_title} using YAML is being removed but there was an unknown error importing your YAML configuration.\n\nPlease make sure {integration_title} is turned on, and restart Home Assistant to try importing again. Otherwise, please remove the YAML from your configuration and add the integration manually." + } + } +} diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a7db391fee0..2a04171e636 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1780,6 +1780,9 @@ python-matter-server==6.1.0 # homeassistant.components.xiaomi_miio python-miio==0.5.12 +# homeassistant.components.mpd +python-mpd2==3.1.1 + # homeassistant.components.mystrom python-mystrom==2.2.0 diff --git a/tests/components/mpd/__init__.py b/tests/components/mpd/__init__.py new file mode 100644 index 00000000000..f5ad1301c14 --- /dev/null +++ b/tests/components/mpd/__init__.py @@ -0,0 +1 @@ +"""Tests for the Music Player Daemon integration.""" diff --git a/tests/components/mpd/conftest.py b/tests/components/mpd/conftest.py new file mode 100644 index 00000000000..818f085decc --- /dev/null +++ b/tests/components/mpd/conftest.py @@ -0,0 +1,43 @@ +"""Fixtures for Music Player Daemon integration tests.""" + +from collections.abc import Generator +from unittest.mock import AsyncMock, patch + +import pytest + +from homeassistant.components.mpd.const import DOMAIN +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT + +from tests.common import MockConfigEntry + + +@pytest.fixture +def mock_config_entry() -> MockConfigEntry: + """Return the default mocked config entry.""" + return MockConfigEntry( + title="Music Player Daemon", + domain=DOMAIN, + data={CONF_HOST: "192.168.0.1", CONF_PORT: 6600, CONF_PASSWORD: "test123"}, + ) + + +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock, None, None]: + """Mock setting up a config entry.""" + with patch( + "homeassistant.components.mpd.async_setup_entry", return_value=True + ) as mock_setup_entry: + yield mock_setup_entry + + +@pytest.fixture +def mock_mpd_client() -> Generator[AsyncMock, None, None]: + """Return a mock for Music Player Daemon client.""" + + with patch( + "homeassistant.components.mpd.config_flow.MPDClient", + autospec=True, + ) as mpd_client: + client = mpd_client.return_value + client.password = AsyncMock() + yield client diff --git a/tests/components/mpd/test_config_flow.py b/tests/components/mpd/test_config_flow.py new file mode 100644 index 00000000000..d17bef60446 --- /dev/null +++ b/tests/components/mpd/test_config_flow.py @@ -0,0 +1,191 @@ +"""Tests for the Music Player Daemon config flow.""" + +from socket import gaierror +from unittest.mock import AsyncMock + +import mpd +import pytest + +from homeassistant.components.mpd.const import DOMAIN +from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER +from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_PORT +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +from tests.common import MockConfigEntry + + +async def test_full_flow( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_mpd_client: AsyncMock, +) -> None: + """Test the happy flow.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] is FlowResultType.FORM + assert not result["errors"] + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_HOST: "192.168.0.1", CONF_PORT: 6600, CONF_PASSWORD: "test123"}, + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "Music Player Daemon" + assert result["data"] == { + CONF_HOST: "192.168.0.1", + CONF_PORT: 6600, + CONF_PASSWORD: "test123", + } + assert len(mock_setup_entry.mock_calls) == 1 + + +@pytest.mark.parametrize( + ("exception", "error"), + [ + (TimeoutError, "cannot_connect"), + (gaierror, "cannot_connect"), + (mpd.ConnectionError, "cannot_connect"), + (OSError, "cannot_connect"), + (Exception, "unknown"), + ], +) +async def test_errors( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_mpd_client: AsyncMock, + exception: Exception, + error: str, +) -> None: + """Test we handle errors correctly.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] is FlowResultType.FORM + + mock_mpd_client.password.side_effect = exception + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_HOST: "192.168.0.1", CONF_PORT: 6600, CONF_PASSWORD: "test123"}, + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"base": error} + + mock_mpd_client.password.side_effect = None + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_HOST: "192.168.0.1", CONF_PORT: 6600, CONF_PASSWORD: "test123"}, + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.CREATE_ENTRY + + +async def test_existing_entry( + hass: HomeAssistant, mock_config_entry: MockConfigEntry +) -> None: + """Test we abort if an entry already exists.""" + mock_config_entry.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] is FlowResultType.FORM + assert not result["errors"] + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_HOST: "192.168.0.1", CONF_PORT: 6600, CONF_PASSWORD: "test123"}, + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +async def test_full_import_flow( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_mpd_client: AsyncMock, +) -> None: + """Test the happy flow.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data={ + CONF_HOST: "192.168.0.1", + CONF_PORT: 6600, + CONF_PASSWORD: "test123", + CONF_NAME: "My PC", + }, + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "My PC" + assert result["data"] == { + CONF_HOST: "192.168.0.1", + CONF_PORT: 6600, + CONF_PASSWORD: "test123", + } + assert len(mock_setup_entry.mock_calls) == 1 + + +@pytest.mark.parametrize( + ("exception", "error"), + [ + (TimeoutError, "cannot_connect"), + (gaierror, "cannot_connect"), + (mpd.ConnectionError, "cannot_connect"), + (OSError, "cannot_connect"), + (Exception, "unknown"), + ], +) +async def test_import_errors( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_mpd_client: AsyncMock, + exception: Exception, + error: str, +) -> None: + """Test we handle errors correctly.""" + mock_mpd_client.password.side_effect = exception + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data={ + CONF_HOST: "192.168.0.1", + CONF_PORT: 6600, + CONF_PASSWORD: "test123", + CONF_NAME: "My PC", + }, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == error + + +async def test_existing_entry_import( + hass: HomeAssistant, mock_config_entry: MockConfigEntry +) -> None: + """Test we abort if an entry already exists.""" + mock_config_entry.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data={ + CONF_HOST: "192.168.0.1", + CONF_PORT: 6600, + CONF_PASSWORD: "test123", + CONF_NAME: "My PC", + }, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" From 04a5a1d18be52bfe354e4109d2f35b9dae2020c4 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sun, 9 Jun 2024 16:02:58 +0200 Subject: [PATCH 0412/1445] Improve demo config flow and add tests (#118481) Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> --- homeassistant/components/demo/__init__.py | 9 +- homeassistant/components/demo/config_flow.py | 3 + tests/components/demo/conftest.py | 14 ++- tests/components/demo/test_config_flow.py | 92 ++++++++++++++++++++ 4 files changed, 109 insertions(+), 9 deletions(-) create mode 100644 tests/components/demo/test_config_flow.py diff --git a/homeassistant/components/demo/__init__.py b/homeassistant/components/demo/__init__.py index 738f6af38dd..371b783b653 100644 --- a/homeassistant/components/demo/__init__.py +++ b/homeassistant/components/demo/__init__.py @@ -65,12 +65,11 @@ CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN) async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the demo environment.""" - if not hass.config_entries.async_entries(DOMAIN): - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data={} - ) + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data={} ) + ) if DOMAIN not in config: return True diff --git a/homeassistant/components/demo/config_flow.py b/homeassistant/components/demo/config_flow.py index cc57ed9a460..468d9cb042b 100644 --- a/homeassistant/components/demo/config_flow.py +++ b/homeassistant/components/demo/config_flow.py @@ -39,6 +39,9 @@ class DemoConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_import(self, import_info: dict[str, Any]) -> ConfigFlowResult: """Set the config entry up from yaml.""" + if self._async_current_entries(): + return self.async_abort(reason="single_instance_allowed") + return self.async_create_entry(title="Demo", data=import_info) diff --git a/tests/components/demo/conftest.py b/tests/components/demo/conftest.py index 731a33360d7..56aabac0280 100644 --- a/tests/components/demo/conftest.py +++ b/tests/components/demo/conftest.py @@ -22,10 +22,16 @@ async def setup_homeassistant(hass: HomeAssistant): @pytest.fixture -async def disable_platforms(hass: HomeAssistant) -> None: +def disable_platforms(hass: HomeAssistant) -> None: """Disable platforms to speed up tests.""" - with patch( - "homeassistant.components.demo.COMPONENTS_WITH_CONFIG_ENTRY_DEMO_PLATFORM", - [], + with ( + patch( + "homeassistant.components.demo.COMPONENTS_WITH_CONFIG_ENTRY_DEMO_PLATFORM", + [], + ), + patch( + "homeassistant.components.demo.COMPONENTS_WITH_DEMO_PLATFORM", + [], + ), ): yield diff --git a/tests/components/demo/test_config_flow.py b/tests/components/demo/test_config_flow.py new file mode 100644 index 00000000000..a0b687e422a --- /dev/null +++ b/tests/components/demo/test_config_flow.py @@ -0,0 +1,92 @@ +"""Test the Demo config flow.""" + +from unittest.mock import patch + +import pytest + +from homeassistant import config_entries, setup +from homeassistant.components.demo import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +from tests.common import MockConfigEntry + + +@pytest.mark.usefixtures("disable_platforms") +async def test_import(hass: HomeAssistant) -> None: + """Test that we can import a config entry.""" + with patch("homeassistant.components.demo.async_setup_entry", return_value=True): + assert await setup.async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + await hass.async_block_till_done() + + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + entry = hass.config_entries.async_entries(DOMAIN)[0] + assert entry.data == {} + + +@pytest.mark.usefixtures("disable_platforms") +async def test_import_once(hass: HomeAssistant) -> None: + """Test that we don't create multiple config entries.""" + with patch( + "homeassistant.components.demo.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={}, + ) + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "Demo" + assert result["data"] == {} + assert result["options"] == {} + mock_setup_entry.assert_called_once() + + # Test importing again doesn't create a 2nd entry + with patch("homeassistant.components.demo.async_setup_entry") as mock_setup_entry: + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={}, + ) + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "single_instance_allowed" + mock_setup_entry.assert_not_called() + + +@pytest.mark.usefixtures("disable_platforms") +async def test_options_flow(hass: HomeAssistant) -> None: + """Test config flow options.""" + config_entry = MockConfigEntry(domain=DOMAIN) + config_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "options_1" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={"bool": True, "constant": "Constant Value", "int": 15}, + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "options_2" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], user_input={} + ) + assert result["type"] is FlowResultType.CREATE_ENTRY + assert config_entry.options == { + "bool": True, + "constant": "Constant Value", + "int": 15, + "multi": ["default"], + "select": "default", + "string": "Default", + } + + await hass.async_block_till_done() + assert await hass.config_entries.async_unload(config_entry.entry_id) + await hass.async_block_till_done() From b4c9b3f1091ae553b0d9f30fbb04f6c389cae2a0 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sun, 9 Jun 2024 16:36:36 +0200 Subject: [PATCH 0413/1445] Create DWD device with unique_id instead of entry_id (#116498) Co-authored-by: Matthias Alphart --- .../dwd_weather_warnings/__init__.py | 6 ++- .../components/dwd_weather_warnings/sensor.py | 19 +++++----- .../dwd_weather_warnings/test_init.py | 38 ++++++++++++++++++- 3 files changed, 51 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/dwd_weather_warnings/__init__.py b/homeassistant/components/dwd_weather_warnings/__init__.py index f71b81d862b..7a56299a35b 100644 --- a/homeassistant/components/dwd_weather_warnings/__init__.py +++ b/homeassistant/components/dwd_weather_warnings/__init__.py @@ -3,8 +3,9 @@ from __future__ import annotations from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr -from .const import PLATFORMS +from .const import DOMAIN, PLATFORMS from .coordinator import DwdWeatherWarningsConfigEntry, DwdWeatherWarningsCoordinator @@ -12,6 +13,9 @@ async def async_setup_entry( hass: HomeAssistant, entry: DwdWeatherWarningsConfigEntry ) -> bool: """Set up a config entry.""" + device_registry = dr.async_get(hass) + if device_registry.async_get_device(identifiers={(DOMAIN, entry.entry_id)}): + device_registry.async_clear_config_entry(entry.entry_id) coordinator = DwdWeatherWarningsCoordinator(hass) await coordinator.async_config_entry_first_refresh() diff --git a/homeassistant/components/dwd_weather_warnings/sensor.py b/homeassistant/components/dwd_weather_warnings/sensor.py index 4f1b64a5b44..c6aa5727b74 100644 --- a/homeassistant/components/dwd_weather_warnings/sensor.py +++ b/homeassistant/components/dwd_weather_warnings/sensor.py @@ -36,7 +36,6 @@ from .const import ( ATTR_REGION_NAME, ATTR_WARNING_COUNT, CURRENT_WARNING_SENSOR, - DEFAULT_NAME, DOMAIN, ) from .coordinator import DwdWeatherWarningsConfigEntry, DwdWeatherWarningsCoordinator @@ -61,12 +60,12 @@ async def async_setup_entry( """Set up entities from config entry.""" coordinator = entry.runtime_data + unique_id = entry.unique_id + assert unique_id + async_add_entities( - [ - DwdWeatherWarningsSensor(coordinator, entry, description) - for description in SENSOR_TYPES - ], - True, + DwdWeatherWarningsSensor(coordinator, description, unique_id) + for description in SENSOR_TYPES ) @@ -81,18 +80,18 @@ class DwdWeatherWarningsSensor( def __init__( self, coordinator: DwdWeatherWarningsCoordinator, - entry: DwdWeatherWarningsConfigEntry, description: SensorEntityDescription, + unique_id: str, ) -> None: """Initialize a DWD-Weather-Warnings sensor.""" super().__init__(coordinator) self.entity_description = description - self._attr_unique_id = f"{entry.unique_id}-{description.key}" + self._attr_unique_id = f"{unique_id}-{description.key}" self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, entry.entry_id)}, - name=f"{DEFAULT_NAME} {entry.title}", + identifiers={(DOMAIN, unique_id)}, + name=coordinator.api.warncell_name, entry_type=DeviceEntryType.SERVICE, ) diff --git a/tests/components/dwd_weather_warnings/test_init.py b/tests/components/dwd_weather_warnings/test_init.py index e5b82d0c453..54f57ead77c 100644 --- a/tests/components/dwd_weather_warnings/test_init.py +++ b/tests/components/dwd_weather_warnings/test_init.py @@ -12,7 +12,8 @@ from homeassistant.components.dwd_weather_warnings.coordinator import ( from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE, STATE_HOME from homeassistant.core import HomeAssistant -from homeassistant.helpers import entity_registry as er +from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers.device_registry import DeviceEntryType from . import init_integration @@ -36,6 +37,41 @@ async def test_load_unload_entry( assert entry.state is ConfigEntryState.NOT_LOADED +async def test_removing_old_device( + hass: HomeAssistant, + mock_identifier_entry: MockConfigEntry, + mock_dwdwfsapi: MagicMock, + device_registry: dr.DeviceRegistry, +) -> None: + """Test removing old device when reloading the integration.""" + + mock_identifier_entry.add_to_hass(hass) + + device_registry.async_get_or_create( + identifiers={(DOMAIN, mock_identifier_entry.entry_id)}, + config_entry_id=mock_identifier_entry.entry_id, + entry_type=DeviceEntryType.SERVICE, + name="test", + ) + + assert ( + device_registry.async_get_device( + identifiers={(DOMAIN, mock_identifier_entry.entry_id)} + ) + is not None + ) + + await hass.config_entries.async_setup(mock_identifier_entry.entry_id) + await hass.async_block_till_done() + + assert ( + device_registry.async_get_device( + identifiers={(DOMAIN, mock_identifier_entry.entry_id)} + ) + is None + ) + + async def test_load_invalid_registry_entry( hass: HomeAssistant, mock_tracker_entry: MockConfigEntry ) -> None: From 361c46d4911914468d6f49b838ef52de0d74e4bc Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Sun, 9 Jun 2024 17:48:17 +0200 Subject: [PATCH 0414/1445] Bump incomfort backend client to v0.6.1 (#119209) --- homeassistant/components/incomfort/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/incomfort/manifest.json b/homeassistant/components/incomfort/manifest.json index 2dd7491c5bb..99567de0b36 100644 --- a/homeassistant/components/incomfort/manifest.json +++ b/homeassistant/components/incomfort/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/incomfort", "iot_class": "local_polling", "loggers": ["incomfortclient"], - "requirements": ["incomfort-client==0.6.0"] + "requirements": ["incomfort-client==0.6.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index dec2a787834..859e7ad7d77 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1152,7 +1152,7 @@ ihcsdk==2.8.5 imgw_pib==1.0.4 # homeassistant.components.incomfort -incomfort-client==0.6.0 +incomfort-client==0.6.1 # homeassistant.components.influxdb influxdb-client==1.24.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2a04171e636..335d5e390b0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -939,7 +939,7 @@ ifaddr==0.2.0 imgw_pib==1.0.4 # homeassistant.components.incomfort -incomfort-client==0.6.0 +incomfort-client==0.6.1 # homeassistant.components.influxdb influxdb-client==1.24.0 From 93fa9e778b6a584f17529402eb39167f2ee51cdb Mon Sep 17 00:00:00 2001 From: Kevin Stillhammer Date: Sun, 9 Jun 2024 18:13:32 +0200 Subject: [PATCH 0415/1445] Add reconfigure step for google_travel_time (#115178) * Add reconfigure step for google_travel_time * Do not allow to change name * Duplicate tests for reconfigure * Use link for description in strings.json * Try except else * Extend existing config flow tests --- .../google_travel_time/config_flow.py | 68 ++++- .../google_travel_time/strings.json | 11 +- tests/components/google_travel_time/const.py | 6 + .../google_travel_time/test_config_flow.py | 240 +++++++++++++++++- 4 files changed, 300 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/google_travel_time/config_flow.py b/homeassistant/components/google_travel_time/config_flow.py index 424ad56b9d4..d8ba7643bc9 100644 --- a/homeassistant/components/google_travel_time/config_flow.py +++ b/homeassistant/components/google_travel_time/config_flow.py @@ -2,6 +2,8 @@ from __future__ import annotations +from typing import TYPE_CHECKING, Any + import voluptuous as vol from homeassistant.config_entries import ( @@ -49,6 +51,20 @@ from .const import ( ) from .helpers import InvalidApiKeyException, UnknownException, validate_config_entry +RECONFIGURE_SCHEMA = vol.Schema( + { + vol.Required(CONF_API_KEY): cv.string, + vol.Required(CONF_DESTINATION): cv.string, + vol.Required(CONF_ORIGIN): cv.string, + } +) + +CONFIG_SCHEMA = RECONFIGURE_SCHEMA.extend( + { + vol.Required(CONF_NAME, default=DEFAULT_NAME): cv.string, + } +) + OPTIONS_SCHEMA = vol.Schema( { vol.Required(CONF_MODE): SelectSelector( @@ -190,29 +206,61 @@ class GoogleTravelTimeConfigFlow(ConfigFlow, domain=DOMAIN): user_input[CONF_ORIGIN], user_input[CONF_DESTINATION], ) + except InvalidApiKeyException: + errors["base"] = "invalid_auth" + except TimeoutError: + errors["base"] = "timeout_connect" + except UnknownException: + errors["base"] = "cannot_connect" + else: return self.async_create_entry( title=user_input.get(CONF_NAME, DEFAULT_NAME), data=user_input, options=default_options(self.hass), ) + + return self.async_show_form( + step_id="user", + data_schema=self.add_suggested_values_to_schema(CONFIG_SCHEMA, user_input), + errors=errors, + ) + + async def async_step_reconfigure( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle reconfiguration.""" + entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) + if TYPE_CHECKING: + assert entry + + errors = {} + user_input = user_input or {} + if user_input: + try: + await self.hass.async_add_executor_job( + validate_config_entry, + self.hass, + user_input[CONF_API_KEY], + user_input[CONF_ORIGIN], + user_input[CONF_DESTINATION], + ) except InvalidApiKeyException: errors["base"] = "invalid_auth" except TimeoutError: errors["base"] = "timeout_connect" except UnknownException: errors["base"] = "cannot_connect" + else: + return self.async_update_reload_and_abort( + entry, + data=user_input, + reason="reconfigure_successful", + ) return self.async_show_form( - step_id="user", - data_schema=vol.Schema( - { - vol.Required( - CONF_NAME, default=user_input.get(CONF_NAME, DEFAULT_NAME) - ): cv.string, - vol.Required(CONF_API_KEY): cv.string, - vol.Required(CONF_DESTINATION): cv.string, - vol.Required(CONF_ORIGIN): cv.string, - } + step_id="reconfigure", + data_schema=self.add_suggested_values_to_schema( + RECONFIGURE_SCHEMA, entry.data.copy() ), errors=errors, ) diff --git a/homeassistant/components/google_travel_time/strings.json b/homeassistant/components/google_travel_time/strings.json index 2c7840b23d8..765cfc9c4b6 100644 --- a/homeassistant/components/google_travel_time/strings.json +++ b/homeassistant/components/google_travel_time/strings.json @@ -10,6 +10,14 @@ "origin": "Origin", "destination": "Destination" } + }, + "reconfigure": { + "description": "[%key:component::google_travel_time::config::step::user::description%]", + "data": { + "api_key": "[%key:common::config_flow::data::api_key%]", + "origin": "[%key:component::google_travel_time::config::step::user::data::origin%]", + "destination": "[%key:component::google_travel_time::config::step::user::data::destination%]" + } } }, "error": { @@ -18,7 +26,8 @@ "timeout_connect": "[%key:common::config_flow::error::timeout_connect%]" }, "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_location%]" + "already_configured": "[%key:common::config_flow::abort::already_configured_location%]", + "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]" } }, "options": { diff --git a/tests/components/google_travel_time/const.py b/tests/components/google_travel_time/const.py index 77e99ffbf68..29cf32b8e29 100644 --- a/tests/components/google_travel_time/const.py +++ b/tests/components/google_travel_time/const.py @@ -11,3 +11,9 @@ MOCK_CONFIG = { CONF_ORIGIN: "location1", CONF_DESTINATION: "location2", } + +RECONFIGURE_CONFIG = { + CONF_API_KEY: "api_key2", + CONF_ORIGIN: "location3", + CONF_DESTINATION: "location4", +} diff --git a/tests/components/google_travel_time/test_config_flow.py b/tests/components/google_travel_time/test_config_flow.py index 6e73bfd8d23..e9b383a0120 100644 --- a/tests/components/google_travel_time/test_config_flow.py +++ b/tests/components/google_travel_time/test_config_flow.py @@ -1,5 +1,7 @@ """Test the Google Maps Travel Time config flow.""" +from unittest.mock import patch + import pytest from homeassistant import config_entries @@ -25,7 +27,58 @@ from homeassistant.const import CONF_API_KEY, CONF_LANGUAGE, CONF_MODE, CONF_NAM from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType -from .const import MOCK_CONFIG +from .const import MOCK_CONFIG, RECONFIGURE_CONFIG + + +async def assert_common_reconfigure_steps( + hass: HomeAssistant, reconfigure_result: config_entries.ConfigFlowResult +) -> None: + """Step through and assert the happy case reconfigure flow.""" + with ( + patch("homeassistant.components.google_travel_time.helpers.Client"), + patch( + "homeassistant.components.google_travel_time.helpers.distance_matrix", + return_value=None, + ), + ): + reconfigure_successful_result = await hass.config_entries.flow.async_configure( + reconfigure_result["flow_id"], + RECONFIGURE_CONFIG, + ) + assert reconfigure_successful_result["type"] is FlowResultType.ABORT + assert reconfigure_successful_result["reason"] == "reconfigure_successful" + await hass.async_block_till_done() + + entry = hass.config_entries.async_entries(DOMAIN)[0] + assert entry.data == RECONFIGURE_CONFIG + + +async def assert_common_create_steps( + hass: HomeAssistant, user_step_result: config_entries.ConfigFlowResult +) -> None: + """Step through and assert the happy case create flow.""" + with ( + patch("homeassistant.components.google_travel_time.helpers.Client"), + patch( + "homeassistant.components.google_travel_time.helpers.distance_matrix", + return_value=None, + ), + ): + create_result = await hass.config_entries.flow.async_configure( + user_step_result["flow_id"], + MOCK_CONFIG, + ) + assert create_result["type"] is FlowResultType.CREATE_ENTRY + await hass.async_block_till_done() + + entry = hass.config_entries.async_entries(DOMAIN)[0] + assert entry.title == DEFAULT_NAME + assert entry.data == { + CONF_NAME: DEFAULT_NAME, + CONF_API_KEY: "api_key", + CONF_ORIGIN: "location1", + CONF_DESTINATION: "location2", + } @pytest.mark.usefixtures("validate_config_entry", "bypass_setup") @@ -37,19 +90,7 @@ async def test_minimum_fields(hass: HomeAssistant) -> None: assert result["type"] is FlowResultType.FORM assert result["errors"] == {} - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - MOCK_CONFIG, - ) - - assert result2["type"] is FlowResultType.CREATE_ENTRY - assert result2["title"] == DEFAULT_NAME - assert result2["data"] == { - CONF_NAME: DEFAULT_NAME, - CONF_API_KEY: "api_key", - CONF_ORIGIN: "location1", - CONF_DESTINATION: "location2", - } + await assert_common_create_steps(hass, result) @pytest.mark.usefixtures("invalidate_config_entry") @@ -67,6 +108,7 @@ async def test_invalid_config_entry(hass: HomeAssistant) -> None: assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} + await assert_common_create_steps(hass, result2) @pytest.mark.usefixtures("invalid_api_key") @@ -84,6 +126,7 @@ async def test_invalid_api_key(hass: HomeAssistant) -> None: assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} + await assert_common_create_steps(hass, result2) @pytest.mark.usefixtures("transport_error") @@ -101,6 +144,7 @@ async def test_transport_error(hass: HomeAssistant) -> None: assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} + await assert_common_create_steps(hass, result2) @pytest.mark.usefixtures("timeout") @@ -118,6 +162,7 @@ async def test_timeout(hass: HomeAssistant) -> None: assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "timeout_connect"} + await assert_common_create_steps(hass, result2) async def test_malformed_api_key(hass: HomeAssistant) -> None: @@ -136,6 +181,173 @@ async def test_malformed_api_key(hass: HomeAssistant) -> None: assert result2["errors"] == {"base": "invalid_auth"} +@pytest.mark.parametrize( + ("data", "options"), + [ + ( + MOCK_CONFIG, + { + CONF_MODE: "driving", + CONF_UNITS: UNITS_IMPERIAL, + }, + ) + ], +) +@pytest.mark.usefixtures("validate_config_entry", "bypass_setup") +async def test_reconfigure(hass: HomeAssistant, mock_config) -> None: + """Test reconfigure flow.""" + reconfigure_result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_RECONFIGURE, + "entry_id": mock_config.entry_id, + }, + ) + assert reconfigure_result["type"] is FlowResultType.FORM + assert reconfigure_result["step_id"] == "reconfigure" + + await assert_common_reconfigure_steps(hass, reconfigure_result) + + +@pytest.mark.parametrize( + ("data", "options"), + [ + ( + MOCK_CONFIG, + { + CONF_MODE: "driving", + CONF_UNITS: UNITS_IMPERIAL, + }, + ) + ], +) +@pytest.mark.usefixtures("invalidate_config_entry") +async def test_reconfigure_invalid_config_entry( + hass: HomeAssistant, mock_config +) -> None: + """Test we get the form.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_RECONFIGURE, + "entry_id": mock_config.entry_id, + }, + ) + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {} + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + RECONFIGURE_CONFIG, + ) + + assert result2["type"] is FlowResultType.FORM + assert result2["errors"] == {"base": "cannot_connect"} + + await assert_common_reconfigure_steps(hass, result2) + + +@pytest.mark.parametrize( + ("data", "options"), + [ + ( + MOCK_CONFIG, + { + CONF_MODE: "driving", + CONF_UNITS: UNITS_IMPERIAL, + }, + ) + ], +) +@pytest.mark.usefixtures("invalid_api_key") +async def test_reconfigure_invalid_api_key(hass: HomeAssistant, mock_config) -> None: + """Test we get the form.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_RECONFIGURE, + "entry_id": mock_config.entry_id, + }, + ) + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {} + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + RECONFIGURE_CONFIG, + ) + + assert result2["type"] is FlowResultType.FORM + assert result2["errors"] == {"base": "invalid_auth"} + await assert_common_reconfigure_steps(hass, result2) + + +@pytest.mark.parametrize( + ("data", "options"), + [ + ( + MOCK_CONFIG, + { + CONF_MODE: "driving", + CONF_UNITS: UNITS_IMPERIAL, + }, + ) + ], +) +@pytest.mark.usefixtures("transport_error") +async def test_reconfigure_transport_error(hass: HomeAssistant, mock_config) -> None: + """Test we get the form.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_RECONFIGURE, + "entry_id": mock_config.entry_id, + }, + ) + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {} + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + RECONFIGURE_CONFIG, + ) + + assert result2["type"] is FlowResultType.FORM + assert result2["errors"] == {"base": "cannot_connect"} + await assert_common_reconfigure_steps(hass, result2) + + +@pytest.mark.parametrize( + ("data", "options"), + [ + ( + MOCK_CONFIG, + { + CONF_MODE: "driving", + CONF_UNITS: UNITS_IMPERIAL, + }, + ) + ], +) +@pytest.mark.usefixtures("timeout") +async def test_reconfigure_timeout(hass: HomeAssistant, mock_config) -> None: + """Test we get the form.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_RECONFIGURE, + "entry_id": mock_config.entry_id, + }, + ) + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {} + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + RECONFIGURE_CONFIG, + ) + + assert result2["type"] is FlowResultType.FORM + assert result2["errors"] == {"base": "timeout_connect"} + await assert_common_reconfigure_steps(hass, result2) + + @pytest.mark.parametrize( ("data", "options"), [ From 09ba9547eddb49063a980e60e4b1234f653d318d Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 9 Jun 2024 18:14:46 +0200 Subject: [PATCH 0416/1445] Fix envisalink alarm (#119212) --- .../envisalink/alarm_control_panel.py | 39 +++---------------- 1 file changed, 6 insertions(+), 33 deletions(-) diff --git a/homeassistant/components/envisalink/alarm_control_panel.py b/homeassistant/components/envisalink/alarm_control_panel.py index 119608bbb2a..b962621edea 100644 --- a/homeassistant/components/envisalink/alarm_control_panel.py +++ b/homeassistant/components/envisalink/alarm_control_panel.py @@ -116,8 +116,9 @@ class EnvisalinkAlarm(EnvisalinkDevice, AlarmControlPanelEntity): ): """Initialize the alarm panel.""" self._partition_number = partition_number - self._code = code self._panic_type = panic_type + self._alarm_control_panel_option_default_code = code + self._attr_code_format = CodeFormat.NUMBER _LOGGER.debug("Setting up alarm: %s", alarm_name) super().__init__(alarm_name, info, controller) @@ -141,13 +142,6 @@ class EnvisalinkAlarm(EnvisalinkDevice, AlarmControlPanelEntity): if partition is None or int(partition) == self._partition_number: self.async_write_ha_state() - @property - def code_format(self) -> CodeFormat | None: - """Regex for code format or None if no code is required.""" - if self._code: - return None - return CodeFormat.NUMBER - @property def state(self) -> str: """Return the state of the device.""" @@ -169,34 +163,15 @@ class EnvisalinkAlarm(EnvisalinkDevice, AlarmControlPanelEntity): async def async_alarm_disarm(self, code: str | None = None) -> None: """Send disarm command.""" - if code: - self.hass.data[DATA_EVL].disarm_partition(str(code), self._partition_number) - else: - self.hass.data[DATA_EVL].disarm_partition( - str(self._code), self._partition_number - ) + self.hass.data[DATA_EVL].disarm_partition(code, self._partition_number) async def async_alarm_arm_home(self, code: str | None = None) -> None: """Send arm home command.""" - if code: - self.hass.data[DATA_EVL].arm_stay_partition( - str(code), self._partition_number - ) - else: - self.hass.data[DATA_EVL].arm_stay_partition( - str(self._code), self._partition_number - ) + self.hass.data[DATA_EVL].arm_stay_partition(code, self._partition_number) async def async_alarm_arm_away(self, code: str | None = None) -> None: """Send arm away command.""" - if code: - self.hass.data[DATA_EVL].arm_away_partition( - str(code), self._partition_number - ) - else: - self.hass.data[DATA_EVL].arm_away_partition( - str(self._code), self._partition_number - ) + self.hass.data[DATA_EVL].arm_away_partition(code, self._partition_number) async def async_alarm_trigger(self, code: str | None = None) -> None: """Alarm trigger command. Will be used to trigger a panic alarm.""" @@ -204,9 +179,7 @@ class EnvisalinkAlarm(EnvisalinkDevice, AlarmControlPanelEntity): async def async_alarm_arm_night(self, code: str | None = None) -> None: """Send arm night command.""" - self.hass.data[DATA_EVL].arm_night_partition( - str(code) if code else str(self._code), self._partition_number - ) + self.hass.data[DATA_EVL].arm_night_partition(code, self._partition_number) @callback def async_alarm_keypress(self, keypress=None): From 30e11ed068604e9d30469ceccf70786e1ecb66ef Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Sun, 9 Jun 2024 18:26:33 +0200 Subject: [PATCH 0417/1445] Update links between config entry and device on sensor change in integral (#119213) --- .../components/integration/__init__.py | 11 ++++ tests/components/integration/test_init.py | 62 ++++++++++++++++++- 2 files changed, 72 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/integration/__init__.py b/homeassistant/components/integration/__init__.py index 4a8d4baa3f2..effa0c4df55 100644 --- a/homeassistant/components/integration/__init__.py +++ b/homeassistant/components/integration/__init__.py @@ -5,6 +5,7 @@ from __future__ import annotations from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: @@ -16,6 +17,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def config_entry_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: """Update listener, called when the config entry options are changed.""" + # Remove device link for entry, the source device may have changed. + # The link will be recreated after load. + device_registry = dr.async_get(hass) + devices = device_registry.devices.get_devices_for_config_entry_id(entry.entry_id) + + for device in devices: + device_registry.async_update_device( + device.id, remove_config_entry_id=entry.entry_id + ) + await hass.config_entries.async_reload(entry.entry_id) diff --git a/tests/components/integration/test_init.py b/tests/components/integration/test_init.py index e6ff2a8efb8..2ed32c7645c 100644 --- a/tests/components/integration/test_init.py +++ b/tests/components/integration/test_init.py @@ -4,7 +4,7 @@ import pytest from homeassistant.components.integration.const import DOMAIN from homeassistant.core import HomeAssistant -from homeassistant.helpers import entity_registry as er +from homeassistant.helpers import device_registry as dr, entity_registry as er from tests.common import MockConfigEntry @@ -61,3 +61,63 @@ async def test_setup_and_remove_config_entry( # Check the state and entity registry entry are removed assert hass.states.get(integration_entity_id) is None assert entity_registry.async_get(integration_entity_id) is None + + +@pytest.mark.parametrize("platform", ["sensor"]) +async def test_entry_changed(hass: HomeAssistant, platform) -> None: + """Test reconfiguring.""" + + device_registry = dr.async_get(hass) + entity_registry = er.async_get(hass) + + def _create_mock_entity(domain: str, name: str) -> er.RegistryEntry: + config_entry = MockConfigEntry( + data={}, + domain="test", + title=f"{name}", + ) + config_entry.add_to_hass(hass) + device_entry = device_registry.async_get_or_create( + identifiers={("test", name)}, config_entry_id=config_entry.entry_id + ) + return entity_registry.async_get_or_create( + domain, "test", name, suggested_object_id=name, device_id=device_entry.id + ) + + def _get_device_config_entries(entry: er.RegistryEntry) -> set[str]: + assert entry.device_id + device = device_registry.async_get(entry.device_id) + assert device + return device.config_entries + + # Set up entities, with backing devices and config entries + input_entry = _create_mock_entity("sensor", "input") + valid_entry = _create_mock_entity("sensor", "valid") + + # Setup the config entry + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={ + "method": "left", + "name": "My integration", + "source": "sensor.input", + "unit_time": "min", + }, + title="My integration", + ) + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.entry_id in _get_device_config_entries(input_entry) + assert config_entry.entry_id not in _get_device_config_entries(valid_entry) + + hass.config_entries.async_update_entry( + config_entry, options={**config_entry.options, "source": "sensor.valid"} + ) + await hass.async_block_till_done() + + # Check that the config entry association has updated + assert config_entry.entry_id not in _get_device_config_entries(input_entry) + assert config_entry.entry_id in _get_device_config_entries(valid_entry) From 38ab121db53d2c72a0e4a3585ed8c657259a1999 Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Mon, 10 Jun 2024 02:30:36 +1000 Subject: [PATCH 0418/1445] Add cabin overheat protection entity to Teslemetry (#118449) * test_cabin_overheat_protection * Fix snapshot * Translate error * Review Feedback --- .../components/teslemetry/climate.py | 152 +++++++++++++- .../components/teslemetry/strings.json | 6 + .../teslemetry/fixtures/vehicle_data.json | 2 +- .../teslemetry/fixtures/vehicle_data_alt.json | 2 +- .../teslemetry/snapshots/test_climate.ambr | 196 ++++++++++++++++++ .../snapshots/test_diagnostics.ambr | 2 +- tests/components/teslemetry/test_climate.py | 99 +++++++++ 7 files changed, 449 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/teslemetry/climate.py b/homeassistant/components/teslemetry/climate.py index f32aca26636..a70dc5a360a 100644 --- a/homeassistant/components/teslemetry/climate.py +++ b/homeassistant/components/teslemetry/climate.py @@ -2,9 +2,10 @@ from __future__ import annotations +from itertools import chain from typing import Any, cast -from tesla_fleet_api.const import Scope +from tesla_fleet_api.const import CabinOverheatProtectionTemp, Scope from homeassistant.components.climate import ( ATTR_HVAC_MODE, @@ -12,12 +13,18 @@ from homeassistant.components.climate import ( ClimateEntityFeature, HVACMode, ) -from homeassistant.const import ATTR_TEMPERATURE, PRECISION_HALVES, UnitOfTemperature +from homeassistant.const import ( + ATTR_TEMPERATURE, + PRECISION_HALVES, + PRECISION_WHOLE, + UnitOfTemperature, +) from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ServiceValidationError from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import TeslemetryConfigEntry -from .const import TeslemetryClimateSide +from .const import DOMAIN, TeslemetryClimateSide from .entity import TeslemetryVehicleEntity from .models import TeslemetryVehicleData @@ -33,15 +40,25 @@ async def async_setup_entry( """Set up the Teslemetry Climate platform from a config entry.""" async_add_entities( - TeslemetryClimateEntity( - vehicle, TeslemetryClimateSide.DRIVER, entry.runtime_data.scopes + chain( + ( + TeslemetryClimateEntity( + vehicle, TeslemetryClimateSide.DRIVER, entry.runtime_data.scopes + ) + for vehicle in entry.runtime_data.vehicles + ), + ( + TeslemetryCabinOverheatProtectionEntity( + vehicle, entry.runtime_data.scopes + ) + for vehicle in entry.runtime_data.vehicles + ), ) - for vehicle in entry.runtime_data.vehicles ) class TeslemetryClimateEntity(TeslemetryVehicleEntity, ClimateEntity): - """Vehicle Location Climate Class.""" + """Telemetry vehicle climate entity.""" _attr_precision = PRECISION_HALVES @@ -153,3 +170,124 @@ class TeslemetryClimateEntity(TeslemetryVehicleEntity, ClimateEntity): else: self._attr_hvac_mode = HVACMode.HEAT_COOL self.async_write_ha_state() + + +COP_MODES = { + "Off": HVACMode.OFF, + "On": HVACMode.COOL, + "FanOnly": HVACMode.FAN_ONLY, +} + +COP_LEVELS = { + "Low": 30, + "Medium": 35, + "High": 40, +} + + +class TeslemetryCabinOverheatProtectionEntity(TeslemetryVehicleEntity, ClimateEntity): + """Telemetry vehicle cabin overheat protection entity.""" + + _attr_precision = PRECISION_WHOLE + _attr_target_temperature_step = 5 + _attr_min_temp = 30 + _attr_max_temp = 40 + _attr_temperature_unit = UnitOfTemperature.CELSIUS + _attr_hvac_modes = list(COP_MODES.values()) + _enable_turn_on_off_backwards_compatibility = False + _attr_entity_registry_enabled_default = False + + def __init__( + self, + data: TeslemetryVehicleData, + scopes: Scope, + ) -> None: + """Initialize the climate.""" + + super().__init__(data, "climate_state_cabin_overheat_protection") + + # Supported Features + self._attr_supported_features = ( + ClimateEntityFeature.TURN_ON | ClimateEntityFeature.TURN_OFF + ) + if self.get("vehicle_config_cop_user_set_temp_supported"): + self._attr_supported_features |= ClimateEntityFeature.TARGET_TEMPERATURE + + # Scopes + self.scoped = Scope.VEHICLE_CMDS in scopes + if not self.scoped: + self._attr_supported_features = ClimateEntityFeature(0) + + def _async_update_attrs(self) -> None: + """Update the attributes of the entity.""" + + if (state := self.get("climate_state_cabin_overheat_protection")) is None: + self._attr_hvac_mode = None + else: + self._attr_hvac_mode = COP_MODES.get(state) + + if (level := self.get("climate_state_cop_activation_temperature")) is None: + self._attr_target_temperature = None + else: + self._attr_target_temperature = COP_LEVELS.get(level) + + self._attr_current_temperature = self.get("climate_state_inside_temp") + + async def async_turn_on(self) -> None: + """Set the climate state to on.""" + await self.async_set_hvac_mode(HVACMode.COOL) + + async def async_turn_off(self) -> None: + """Set the climate state to off.""" + await self.async_set_hvac_mode(HVACMode.OFF) + + async def async_set_temperature(self, **kwargs: Any) -> None: + """Set the climate temperature.""" + self.raise_for_scope() + + if not (temp := kwargs.get(ATTR_TEMPERATURE)): + return + + if temp == 30: + cop_mode = CabinOverheatProtectionTemp.LOW + elif temp == 35: + cop_mode = CabinOverheatProtectionTemp.MEDIUM + elif temp == 40: + cop_mode = CabinOverheatProtectionTemp.HIGH + else: + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="invalid_cop_temp", + ) + + await self.wake_up_if_asleep() + await self.handle_command(self.api.set_cop_temp(cop_mode)) + self._attr_target_temperature = temp + + if mode := kwargs.get(ATTR_HVAC_MODE): + await self._async_set_cop(mode) + + self.async_write_ha_state() + + async def _async_set_cop(self, hvac_mode: HVACMode) -> None: + if hvac_mode == HVACMode.OFF: + await self.handle_command( + self.api.set_cabin_overheat_protection(on=False, fan_only=False) + ) + elif hvac_mode == HVACMode.COOL: + await self.handle_command( + self.api.set_cabin_overheat_protection(on=True, fan_only=False) + ) + elif hvac_mode == HVACMode.FAN_ONLY: + await self.handle_command( + self.api.set_cabin_overheat_protection(on=True, fan_only=True) + ) + + self._attr_hvac_mode = hvac_mode + + async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: + """Set the climate mode and state.""" + self.raise_for_scope() + await self.wake_up_if_asleep() + await self._async_set_cop(hvac_mode) + self.async_write_ha_state() diff --git a/homeassistant/components/teslemetry/strings.json b/homeassistant/components/teslemetry/strings.json index b1b794404f4..d3740db9760 100644 --- a/homeassistant/components/teslemetry/strings.json +++ b/homeassistant/components/teslemetry/strings.json @@ -117,6 +117,9 @@ } }, "climate": { + "climate_state_cabin_overheat_protection": { + "name": "Cabin overheat protection" + }, "driver_temp": { "name": "[%key:component::climate::title%]", "state_attributes": { @@ -464,6 +467,9 @@ "exceptions": { "no_cable": { "message": "Charge cable will lock automatically when connected" + }, + "invalid_cop_temp": { + "message": "Cabin overheat protection does not support that temperature" } } } diff --git a/tests/components/teslemetry/fixtures/vehicle_data.json b/tests/components/teslemetry/fixtures/vehicle_data.json index 50022d7f4e9..6c787df4897 100644 --- a/tests/components/teslemetry/fixtures/vehicle_data.json +++ b/tests/components/teslemetry/fixtures/vehicle_data.json @@ -148,7 +148,7 @@ "car_special_type": "base", "car_type": "model3", "charge_port_type": "CCS", - "cop_user_set_temp_supported": false, + "cop_user_set_temp_supported": true, "dashcam_clip_save_supported": true, "default_charge_to_max": false, "driver_assist": "TeslaAP3", diff --git a/tests/components/teslemetry/fixtures/vehicle_data_alt.json b/tests/components/teslemetry/fixtures/vehicle_data_alt.json index 46f65e90760..76416982eba 100644 --- a/tests/components/teslemetry/fixtures/vehicle_data_alt.json +++ b/tests/components/teslemetry/fixtures/vehicle_data_alt.json @@ -81,7 +81,7 @@ "cabin_overheat_protection": "Off", "cabin_overheat_protection_actively_cooling": false, "climate_keeper_mode": "off", - "cop_activation_temperature": "High", + "cop_activation_temperature": "Low", "defrost_mode": 0, "driver_temp_setting": 22, "fan_status": 0, diff --git a/tests/components/teslemetry/snapshots/test_climate.ambr b/tests/components/teslemetry/snapshots/test_climate.ambr index b25baf239c9..b65796fe10e 100644 --- a/tests/components/teslemetry/snapshots/test_climate.ambr +++ b/tests/components/teslemetry/snapshots/test_climate.ambr @@ -1,4 +1,70 @@ # serializer version: 1 +# name: test_climate[climate.test_cabin_overheat_protection-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'hvac_modes': list([ + , + , + , + ]), + 'max_temp': 40, + 'min_temp': 30, + 'target_temp_step': 5, + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'climate', + 'entity_category': None, + 'entity_id': 'climate.test_cabin_overheat_protection', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Cabin overheat protection', + 'platform': 'teslemetry', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': 'climate_state_cabin_overheat_protection', + 'unique_id': 'LRWXF7EK4KC700000-climate_state_cabin_overheat_protection', + 'unit_of_measurement': None, + }) +# --- +# name: test_climate[climate.test_cabin_overheat_protection-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'current_temperature': 30, + 'friendly_name': 'Test Cabin overheat protection', + 'hvac_modes': list([ + , + , + , + ]), + 'max_temp': 40, + 'min_temp': 30, + 'supported_features': , + 'target_temp_step': 5, + 'temperature': 40, + }), + 'context': , + 'entity_id': 'climate.test_cabin_overheat_protection', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'cool', + }) +# --- # name: test_climate[climate.test_climate-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -74,6 +140,71 @@ 'state': 'heat_cool', }) # --- +# name: test_climate_alt[climate.test_cabin_overheat_protection-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'hvac_modes': list([ + , + , + , + ]), + 'max_temp': 40, + 'min_temp': 30, + 'target_temp_step': 5, + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'climate', + 'entity_category': None, + 'entity_id': 'climate.test_cabin_overheat_protection', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Cabin overheat protection', + 'platform': 'teslemetry', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': 'climate_state_cabin_overheat_protection', + 'unique_id': 'LRWXF7EK4KC700000-climate_state_cabin_overheat_protection', + 'unit_of_measurement': None, + }) +# --- +# name: test_climate_alt[climate.test_cabin_overheat_protection-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'current_temperature': 30, + 'friendly_name': 'Test Cabin overheat protection', + 'hvac_modes': list([ + , + , + , + ]), + 'max_temp': 40, + 'min_temp': 30, + 'supported_features': , + 'target_temp_step': 5, + }), + 'context': , + 'entity_id': 'climate.test_cabin_overheat_protection', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- # name: test_climate_alt[climate.test_climate-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -149,6 +280,71 @@ 'state': 'off', }) # --- +# name: test_climate_offline[climate.test_cabin_overheat_protection-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'hvac_modes': list([ + , + , + , + ]), + 'max_temp': 40, + 'min_temp': 30, + 'target_temp_step': 5, + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'climate', + 'entity_category': None, + 'entity_id': 'climate.test_cabin_overheat_protection', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Cabin overheat protection', + 'platform': 'teslemetry', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': 'climate_state_cabin_overheat_protection', + 'unique_id': 'LRWXF7EK4KC700000-climate_state_cabin_overheat_protection', + 'unit_of_measurement': None, + }) +# --- +# name: test_climate_offline[climate.test_cabin_overheat_protection-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'current_temperature': None, + 'friendly_name': 'Test Cabin overheat protection', + 'hvac_modes': list([ + , + , + , + ]), + 'max_temp': 40, + 'min_temp': 30, + 'supported_features': , + 'target_temp_step': 5, + }), + 'context': , + 'entity_id': 'climate.test_cabin_overheat_protection', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- # name: test_climate_offline[climate.test_climate-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/teslemetry/snapshots/test_diagnostics.ambr b/tests/components/teslemetry/snapshots/test_diagnostics.ambr index d7348d66d07..d13c4f48068 100644 --- a/tests/components/teslemetry/snapshots/test_diagnostics.ambr +++ b/tests/components/teslemetry/snapshots/test_diagnostics.ambr @@ -307,7 +307,7 @@ 'vehicle_config_car_special_type': 'base', 'vehicle_config_car_type': 'model3', 'vehicle_config_charge_port_type': 'CCS', - 'vehicle_config_cop_user_set_temp_supported': False, + 'vehicle_config_cop_user_set_temp_supported': True, 'vehicle_config_dashcam_clip_save_supported': True, 'vehicle_config_default_charge_to_max': False, 'vehicle_config_driver_assist': 'TeslaAP3', diff --git a/tests/components/teslemetry/test_climate.py b/tests/components/teslemetry/test_climate.py index 0e21533083c..1ea21554659 100644 --- a/tests/components/teslemetry/test_climate.py +++ b/tests/components/teslemetry/test_climate.py @@ -10,11 +10,14 @@ from tesla_fleet_api.exceptions import InvalidCommand, VehicleOffline from homeassistant.components.climate import ( ATTR_HVAC_MODE, ATTR_PRESET_MODE, + ATTR_TARGET_TEMP_HIGH, + ATTR_TARGET_TEMP_LOW, ATTR_TEMPERATURE, DOMAIN as CLIMATE_DOMAIN, SERVICE_SET_HVAC_MODE, SERVICE_SET_PRESET_MODE, SERVICE_SET_TEMPERATURE, + SERVICE_TURN_OFF, SERVICE_TURN_ON, HVACMode, ) @@ -37,6 +40,7 @@ from .const import ( from tests.common import async_fire_time_changed +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_climate( hass: HomeAssistant, snapshot: SnapshotAssertion, @@ -108,7 +112,100 @@ async def test_climate( state = hass.states.get(entity_id) assert state.state == HVACMode.OFF + entity_id = "climate.test_cabin_overheat_protection" + # Turn On and Set Low + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_TEMPERATURE, + { + ATTR_ENTITY_ID: [entity_id], + ATTR_TEMPERATURE: 30, + ATTR_HVAC_MODE: HVACMode.FAN_ONLY, + }, + blocking=True, + ) + state = hass.states.get(entity_id) + assert state.attributes[ATTR_TEMPERATURE] == 30 + assert state.state == HVACMode.FAN_ONLY + + # Set Temp Medium + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_TEMPERATURE, + { + ATTR_ENTITY_ID: [entity_id], + ATTR_TEMPERATURE: 35, + }, + blocking=True, + ) + state = hass.states.get(entity_id) + assert state.attributes[ATTR_TEMPERATURE] == 35 + + # Set Temp High + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_TEMPERATURE, + { + ATTR_ENTITY_ID: [entity_id], + ATTR_TEMPERATURE: 40, + }, + blocking=True, + ) + state = hass.states.get(entity_id) + assert state.attributes[ATTR_TEMPERATURE] == 40 + + # Turn Off + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: [entity_id]}, + blocking=True, + ) + state = hass.states.get(entity_id) + assert state.state == HVACMode.OFF + + # Turn On + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: [entity_id]}, + blocking=True, + ) + state = hass.states.get(entity_id) + assert state.state == HVACMode.COOL + + # Set Temp do nothing + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_TEMPERATURE, + { + ATTR_ENTITY_ID: [entity_id], + ATTR_TARGET_TEMP_HIGH: 30, + ATTR_TARGET_TEMP_LOW: 30, + }, + blocking=True, + ) + state = hass.states.get(entity_id) + assert state.attributes[ATTR_TEMPERATURE] == 40 + assert state.state == HVACMode.COOL + + # pytest raises ServiceValidationError + with pytest.raises( + ServiceValidationError, + match="Cabin overheat protection does not support that temperature", + ) as error: + # Invalid Temp + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_TEMPERATURE, + {ATTR_ENTITY_ID: [entity_id], ATTR_TEMPERATURE: 25}, + blocking=True, + ) + assert error + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_climate_alt( hass: HomeAssistant, snapshot: SnapshotAssertion, @@ -122,6 +219,7 @@ async def test_climate_alt( assert_entities(hass, entry.entry_id, entity_registry, snapshot) +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_climate_offline( hass: HomeAssistant, snapshot: SnapshotAssertion, @@ -204,6 +302,7 @@ async def test_ignored_error( mock_on.assert_called_once() +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_asleep_or_offline( hass: HomeAssistant, mock_vehicle_data, From 909df675e0ffe25ecfb2405ac442762bfdb0deae Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 9 Jun 2024 11:32:33 -0500 Subject: [PATCH 0419/1445] Use a listcomp for history results (#119188) --- .../components/recorder/history/modern.py | 30 +++++++++++-------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/recorder/history/modern.py b/homeassistant/components/recorder/history/modern.py index 96347a1f57b..b6acb6601ff 100644 --- a/homeassistant/components/recorder/history/modern.py +++ b/homeassistant/components/recorder/history/modern.py @@ -782,24 +782,30 @@ def _sorted_states_to_dict( if compressed_state_format: # Compressed state format uses the timestamp directly ent_results.extend( - { - attr_state: (prev_state := state), - attr_time: row[last_updated_ts_idx], - } - for row in group - if (state := row[state_idx]) != prev_state + [ + { + attr_state: (prev_state := state), + attr_time: row[last_updated_ts_idx], + } + for row in group + if (state := row[state_idx]) != prev_state + ] ) continue # Non-compressed state format returns an ISO formatted string _utc_from_timestamp = dt_util.utc_from_timestamp ent_results.extend( - { - attr_state: (prev_state := state), - attr_time: _utc_from_timestamp(row[last_updated_ts_idx]).isoformat(), - } - for row in group - if (state := row[state_idx]) != prev_state + [ + { + attr_state: (prev_state := state), + attr_time: _utc_from_timestamp( + row[last_updated_ts_idx] + ).isoformat(), + } + for row in group + if (state := row[state_idx]) != prev_state + ] ) if descending: From 7065c0993d2f5fba193c504b3549299b30913f9a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 9 Jun 2024 11:33:10 -0500 Subject: [PATCH 0420/1445] Reduce overhead to reduce statistics (#119187) --- .../components/recorder/statistics.py | 47 +++++++++---------- 1 file changed, 23 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index 7b5c6811e29..84c82f35264 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -948,7 +948,8 @@ def reduce_day_ts_factory() -> ( ] ): """Return functions to match same day and day start end.""" - _boundries: tuple[float, float] = (0, 0) + _lower_bound: float = 0 + _upper_bound: float = 0 # We have to recreate _local_from_timestamp in the closure in case the timezone changes _local_from_timestamp = partial( @@ -957,10 +958,10 @@ def reduce_day_ts_factory() -> ( def _same_day_ts(time1: float, time2: float) -> bool: """Return True if time1 and time2 are in the same date.""" - nonlocal _boundries - if not _boundries[0] <= time1 < _boundries[1]: - _boundries = _day_start_end_ts_cached(time1) - return _boundries[0] <= time2 < _boundries[1] + nonlocal _lower_bound, _upper_bound + if not _lower_bound <= time1 < _upper_bound: + _lower_bound, _upper_bound = _day_start_end_ts_cached(time1) + return _lower_bound <= time2 < _upper_bound def _day_start_end_ts(time: float) -> tuple[float, float]: """Return the start and end of the period (day) time is within.""" @@ -968,8 +969,8 @@ def reduce_day_ts_factory() -> ( hour=0, minute=0, second=0, microsecond=0 ) return ( - start_local.astimezone(dt_util.UTC).timestamp(), - (start_local + timedelta(days=1)).astimezone(dt_util.UTC).timestamp(), + start_local.timestamp(), + (start_local + timedelta(days=1)).timestamp(), ) # We create _day_start_end_ts_cached in the closure in case the timezone changes @@ -996,7 +997,8 @@ def reduce_week_ts_factory() -> ( ] ): """Return functions to match same week and week start end.""" - _boundries: tuple[float, float] = (0, 0) + _lower_bound: float = 0 + _upper_bound: float = 0 # We have to recreate _local_from_timestamp in the closure in case the timezone changes _local_from_timestamp = partial( @@ -1005,21 +1007,20 @@ def reduce_week_ts_factory() -> ( def _same_week_ts(time1: float, time2: float) -> bool: """Return True if time1 and time2 are in the same year and week.""" - nonlocal _boundries - if not _boundries[0] <= time1 < _boundries[1]: - _boundries = _week_start_end_ts_cached(time1) - return _boundries[0] <= time2 < _boundries[1] + nonlocal _lower_bound, _upper_bound + if not _lower_bound <= time1 < _upper_bound: + _lower_bound, _upper_bound = _week_start_end_ts_cached(time1) + return _lower_bound <= time2 < _upper_bound def _week_start_end_ts(time: float) -> tuple[float, float]: """Return the start and end of the period (week) time is within.""" - nonlocal _boundries time_local = _local_from_timestamp(time) start_local = time_local.replace( hour=0, minute=0, second=0, microsecond=0 ) - timedelta(days=time_local.weekday()) return ( - start_local.astimezone(dt_util.UTC).timestamp(), - (start_local + timedelta(days=7)).astimezone(dt_util.UTC).timestamp(), + start_local.timestamp(), + (start_local + timedelta(days=7)).timestamp(), ) # We create _week_start_end_ts_cached in the closure in case the timezone changes @@ -1054,7 +1055,8 @@ def reduce_month_ts_factory() -> ( ] ): """Return functions to match same month and month start end.""" - _boundries: tuple[float, float] = (0, 0) + _lower_bound: float = 0 + _upper_bound: float = 0 # We have to recreate _local_from_timestamp in the closure in case the timezone changes _local_from_timestamp = partial( @@ -1063,10 +1065,10 @@ def reduce_month_ts_factory() -> ( def _same_month_ts(time1: float, time2: float) -> bool: """Return True if time1 and time2 are in the same year and month.""" - nonlocal _boundries - if not _boundries[0] <= time1 < _boundries[1]: - _boundries = _month_start_end_ts_cached(time1) - return _boundries[0] <= time2 < _boundries[1] + nonlocal _lower_bound, _upper_bound + if not _lower_bound <= time1 < _upper_bound: + _lower_bound, _upper_bound = _month_start_end_ts_cached(time1) + return _lower_bound <= time2 < _upper_bound def _month_start_end_ts(time: float) -> tuple[float, float]: """Return the start and end of the period (month) time is within.""" @@ -1074,10 +1076,7 @@ def reduce_month_ts_factory() -> ( day=1, hour=0, minute=0, second=0, microsecond=0 ) end_local = _find_month_end_time(start_local) - return ( - start_local.astimezone(dt_util.UTC).timestamp(), - end_local.astimezone(dt_util.UTC).timestamp(), - ) + return (start_local.timestamp(), end_local.timestamp()) # We create _month_start_end_ts_cached in the closure in case the timezone changes _month_start_end_ts_cached = lru_cache(maxsize=6)(_month_start_end_ts) From 4ca38f227a8694c998ca94e7fd09a2621411bdab Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Sun, 9 Jun 2024 19:21:37 +0200 Subject: [PATCH 0421/1445] Fix - Remove unneeded assert in teslemetry test (#119219) Remove unneded assert in teslemetry test --- tests/components/teslemetry/test_climate.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/components/teslemetry/test_climate.py b/tests/components/teslemetry/test_climate.py index 1ea21554659..a737fc9f376 100644 --- a/tests/components/teslemetry/test_climate.py +++ b/tests/components/teslemetry/test_climate.py @@ -194,7 +194,7 @@ async def test_climate( with pytest.raises( ServiceValidationError, match="Cabin overheat protection does not support that temperature", - ) as error: + ): # Invalid Temp await hass.services.async_call( CLIMATE_DOMAIN, @@ -202,7 +202,6 @@ async def test_climate( {ATTR_ENTITY_ID: [entity_id], ATTR_TEMPERATURE: 25}, blocking=True, ) - assert error @pytest.mark.usefixtures("entity_registry_enabled_by_default") From c03f9d264ef400f24fa25d373d901a8cbeb9fe1c Mon Sep 17 00:00:00 2001 From: Jake Martin Date: Sun, 9 Jun 2024 19:24:33 +0100 Subject: [PATCH 0422/1445] Bump monzopy to 1.3.0 (#119225) --- homeassistant/components/monzo/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/monzo/manifest.json b/homeassistant/components/monzo/manifest.json index 0737852eff1..8b816457004 100644 --- a/homeassistant/components/monzo/manifest.json +++ b/homeassistant/components/monzo/manifest.json @@ -6,5 +6,5 @@ "dependencies": ["application_credentials"], "documentation": "https://www.home-assistant.io/integrations/monzo", "iot_class": "cloud_polling", - "requirements": ["monzopy==1.2.0"] + "requirements": ["monzopy==1.3.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 859e7ad7d77..c3eb350b521 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1341,7 +1341,7 @@ moat-ble==0.1.1 moehlenhoff-alpha2==1.3.0 # homeassistant.components.monzo -monzopy==1.2.0 +monzopy==1.3.0 # homeassistant.components.mopeka mopeka-iot-ble==0.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 335d5e390b0..4f1e667e1d2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1086,7 +1086,7 @@ moat-ble==0.1.1 moehlenhoff-alpha2==1.3.0 # homeassistant.components.monzo -monzopy==1.2.0 +monzopy==1.3.0 # homeassistant.components.mopeka mopeka-iot-ble==0.7.0 From 06c1c435d1bd3f4f1e56eccfc841de24e9c902f3 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sun, 9 Jun 2024 21:50:13 +0200 Subject: [PATCH 0423/1445] Improve type hints in ambient_station tests (#119230) --- tests/components/ambient_station/conftest.py | 33 +++++++++++++------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/tests/components/ambient_station/conftest.py b/tests/components/ambient_station/conftest.py index adbd6777727..e4f067108a5 100644 --- a/tests/components/ambient_station/conftest.py +++ b/tests/components/ambient_station/conftest.py @@ -1,24 +1,31 @@ """Define test fixtures for Ambient PWS.""" -import json +from typing import Any from unittest.mock import AsyncMock, Mock, patch import pytest +from typing_extensions import Generator from homeassistant.components.ambient_station.const import CONF_APP_KEY, DOMAIN from homeassistant.const import CONF_API_KEY +from homeassistant.core import HomeAssistant +from homeassistant.util.json import JsonArrayType, JsonObjectType -from tests.common import MockConfigEntry, load_fixture +from tests.common import ( + MockConfigEntry, + load_json_array_fixture, + load_json_object_fixture, +) @pytest.fixture(name="api") -def api_fixture(hass, data_devices): +def api_fixture(data_devices: JsonArrayType) -> Mock: """Define a mock API object.""" return Mock(get_devices=AsyncMock(return_value=data_devices)) @pytest.fixture(name="config") -def config_fixture(hass): +def config_fixture() -> dict[str, Any]: """Define a config entry data fixture.""" return { CONF_API_KEY: "12345abcde12345abcde", @@ -27,7 +34,9 @@ def config_fixture(hass): @pytest.fixture(name="config_entry") -def config_entry_fixture(hass, config): +def config_entry_fixture( + hass: HomeAssistant, config: dict[str, Any] +) -> MockConfigEntry: """Define a config entry fixture.""" entry = MockConfigEntry( domain=DOMAIN, @@ -39,19 +48,19 @@ def config_entry_fixture(hass, config): @pytest.fixture(name="data_devices", scope="package") -def data_devices_fixture(): +def data_devices_fixture() -> JsonArrayType: """Define devices data.""" - return json.loads(load_fixture("devices.json", "ambient_station")) + return load_json_array_fixture("devices.json", "ambient_station") @pytest.fixture(name="data_station", scope="package") -def data_station_fixture(): +def data_station_fixture() -> JsonObjectType: """Define station data.""" - return json.loads(load_fixture("station_data.json", "ambient_station")) + return load_json_object_fixture("station_data.json", "ambient_station") @pytest.fixture(name="mock_aioambient") -async def mock_aioambient_fixture(api): +def mock_aioambient_fixture(api: Mock) -> Generator[None]: """Define a fixture to patch aioambient.""" with ( patch( @@ -64,7 +73,9 @@ async def mock_aioambient_fixture(api): @pytest.fixture(name="setup_config_entry") -async def setup_config_entry_fixture(hass, config_entry, mock_aioambient): +async def setup_config_entry_fixture( + hass: HomeAssistant, config_entry: MockConfigEntry, mock_aioambient: None +) -> None: """Define a fixture to set up ambient_station.""" assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() From 7dfaa05793aa9a5f7b264aba12932c750fed9095 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sun, 9 Jun 2024 22:04:40 +0200 Subject: [PATCH 0424/1445] Improve type hints in amberelectric tests (#119229) --- .../amberelectric/test_binary_sensor.py | 15 ++++--- tests/components/amberelectric/test_sensor.py | 44 +++++++++---------- 2 files changed, 30 insertions(+), 29 deletions(-) diff --git a/tests/components/amberelectric/test_binary_sensor.py b/tests/components/amberelectric/test_binary_sensor.py index 92877c57c61..1e5eb572e07 100644 --- a/tests/components/amberelectric/test_binary_sensor.py +++ b/tests/components/amberelectric/test_binary_sensor.py @@ -28,7 +28,7 @@ MOCK_API_TOKEN = "psk_0000000000000000" @pytest.fixture -async def setup_no_spike(hass) -> AsyncGenerator: +async def setup_no_spike(hass: HomeAssistant) -> AsyncGenerator[Mock]: """Set up general channel.""" MockConfigEntry( domain="amberelectric", @@ -51,7 +51,7 @@ async def setup_no_spike(hass) -> AsyncGenerator: @pytest.fixture -async def setup_potential_spike(hass) -> AsyncGenerator: +async def setup_potential_spike(hass: HomeAssistant) -> AsyncGenerator[Mock]: """Set up general channel.""" MockConfigEntry( domain="amberelectric", @@ -80,7 +80,7 @@ async def setup_potential_spike(hass) -> AsyncGenerator: @pytest.fixture -async def setup_spike(hass) -> AsyncGenerator: +async def setup_spike(hass: HomeAssistant) -> AsyncGenerator[Mock]: """Set up general channel.""" MockConfigEntry( domain="amberelectric", @@ -108,7 +108,8 @@ async def setup_spike(hass) -> AsyncGenerator: yield mock_update.return_value -def test_no_spike_sensor(hass: HomeAssistant, setup_no_spike) -> None: +@pytest.mark.usefixtures("setup_no_spike") +def test_no_spike_sensor(hass: HomeAssistant) -> None: """Testing the creation of the Amber renewables sensor.""" assert len(hass.states.async_all()) == 5 sensor = hass.states.get("binary_sensor.mock_title_price_spike") @@ -118,7 +119,8 @@ def test_no_spike_sensor(hass: HomeAssistant, setup_no_spike) -> None: assert sensor.attributes["spike_status"] == "none" -def test_potential_spike_sensor(hass: HomeAssistant, setup_potential_spike) -> None: +@pytest.mark.usefixtures("setup_potential_spike") +def test_potential_spike_sensor(hass: HomeAssistant) -> None: """Testing the creation of the Amber renewables sensor.""" assert len(hass.states.async_all()) == 5 sensor = hass.states.get("binary_sensor.mock_title_price_spike") @@ -128,7 +130,8 @@ def test_potential_spike_sensor(hass: HomeAssistant, setup_potential_spike) -> N assert sensor.attributes["spike_status"] == "potential" -def test_spike_sensor(hass: HomeAssistant, setup_spike) -> None: +@pytest.mark.usefixtures("setup_spike") +def test_spike_sensor(hass: HomeAssistant) -> None: """Testing the creation of the Amber renewables sensor.""" assert len(hass.states.async_all()) == 5 sensor = hass.states.get("binary_sensor.mock_title_price_spike") diff --git a/tests/components/amberelectric/test_sensor.py b/tests/components/amberelectric/test_sensor.py index c2d4886bbe9..3c0910f0afc 100644 --- a/tests/components/amberelectric/test_sensor.py +++ b/tests/components/amberelectric/test_sensor.py @@ -31,7 +31,7 @@ MOCK_API_TOKEN = "psk_0000000000000000" @pytest.fixture -async def setup_general(hass) -> AsyncGenerator: +async def setup_general(hass: HomeAssistant) -> AsyncGenerator[Mock]: """Set up general channel.""" MockConfigEntry( domain="amberelectric", @@ -54,7 +54,9 @@ async def setup_general(hass) -> AsyncGenerator: @pytest.fixture -async def setup_general_and_controlled_load(hass) -> AsyncGenerator: +async def setup_general_and_controlled_load( + hass: HomeAssistant, +) -> AsyncGenerator[Mock]: """Set up general channel and controller load channel.""" MockConfigEntry( domain="amberelectric", @@ -78,7 +80,7 @@ async def setup_general_and_controlled_load(hass) -> AsyncGenerator: @pytest.fixture -async def setup_general_and_feed_in(hass) -> AsyncGenerator: +async def setup_general_and_feed_in(hass: HomeAssistant) -> AsyncGenerator[Mock]: """Set up general channel and feed in channel.""" MockConfigEntry( domain="amberelectric", @@ -138,9 +140,8 @@ async def test_general_price_sensor(hass: HomeAssistant, setup_general: Mock) -> assert attributes.get("range_max") == 0.12 -async def test_general_and_controlled_load_price_sensor( - hass: HomeAssistant, setup_general_and_controlled_load: Mock -) -> None: +@pytest.mark.usefixtures("setup_general_and_controlled_load") +async def test_general_and_controlled_load_price_sensor(hass: HomeAssistant) -> None: """Test the Controlled Price sensor.""" assert len(hass.states.async_all()) == 8 price = hass.states.get("sensor.mock_title_controlled_load_price") @@ -161,9 +162,8 @@ async def test_general_and_controlled_load_price_sensor( assert attributes["attribution"] == "Data provided by Amber Electric" -async def test_general_and_feed_in_price_sensor( - hass: HomeAssistant, setup_general_and_feed_in: Mock -) -> None: +@pytest.mark.usefixtures("setup_general_and_feed_in") +async def test_general_and_feed_in_price_sensor(hass: HomeAssistant) -> None: """Test the Feed In sensor.""" assert len(hass.states.async_all()) == 8 price = hass.states.get("sensor.mock_title_feed_in_price") @@ -227,9 +227,8 @@ async def test_general_forecast_sensor( assert first_forecast.get("range_max") == 0.12 -async def test_controlled_load_forecast_sensor( - hass: HomeAssistant, setup_general_and_controlled_load: Mock -) -> None: +@pytest.mark.usefixtures("setup_general_and_controlled_load") +async def test_controlled_load_forecast_sensor(hass: HomeAssistant) -> None: """Test the Controlled Load Forecast sensor.""" assert len(hass.states.async_all()) == 8 price = hass.states.get("sensor.mock_title_controlled_load_forecast") @@ -252,9 +251,8 @@ async def test_controlled_load_forecast_sensor( assert first_forecast["descriptor"] == "very_low" -async def test_feed_in_forecast_sensor( - hass: HomeAssistant, setup_general_and_feed_in: Mock -) -> None: +@pytest.mark.usefixtures("setup_general_and_feed_in") +async def test_feed_in_forecast_sensor(hass: HomeAssistant) -> None: """Test the Feed In Forecast sensor.""" assert len(hass.states.async_all()) == 8 price = hass.states.get("sensor.mock_title_feed_in_forecast") @@ -277,7 +275,8 @@ async def test_feed_in_forecast_sensor( assert first_forecast["descriptor"] == "very_low" -def test_renewable_sensor(hass: HomeAssistant, setup_general) -> None: +@pytest.mark.usefixtures("setup_general") +def test_renewable_sensor(hass: HomeAssistant) -> None: """Testing the creation of the Amber renewables sensor.""" assert len(hass.states.async_all()) == 5 sensor = hass.states.get("sensor.mock_title_renewables") @@ -285,9 +284,8 @@ def test_renewable_sensor(hass: HomeAssistant, setup_general) -> None: assert sensor.state == "51" -def test_general_price_descriptor_descriptor_sensor( - hass: HomeAssistant, setup_general: Mock -) -> None: +@pytest.mark.usefixtures("setup_general") +def test_general_price_descriptor_descriptor_sensor(hass: HomeAssistant) -> None: """Test the General Price Descriptor sensor.""" assert len(hass.states.async_all()) == 5 price = hass.states.get("sensor.mock_title_general_price_descriptor") @@ -295,8 +293,9 @@ def test_general_price_descriptor_descriptor_sensor( assert price.state == "extremely_low" +@pytest.mark.usefixtures("setup_general_and_controlled_load") def test_general_and_controlled_load_price_descriptor_sensor( - hass: HomeAssistant, setup_general_and_controlled_load: Mock + hass: HomeAssistant, ) -> None: """Test the Controlled Price Descriptor sensor.""" assert len(hass.states.async_all()) == 8 @@ -305,9 +304,8 @@ def test_general_and_controlled_load_price_descriptor_sensor( assert price.state == "extremely_low" -def test_general_and_feed_in_price_descriptor_sensor( - hass: HomeAssistant, setup_general_and_feed_in: Mock -) -> None: +@pytest.mark.usefixtures("setup_general_and_feed_in") +def test_general_and_feed_in_price_descriptor_sensor(hass: HomeAssistant) -> None: """Test the Feed In Price Descriptor sensor.""" assert len(hass.states.async_all()) == 8 price = hass.states.get("sensor.mock_title_feed_in_price_descriptor") From 325352e1977c5f696e02783757da65da50ef5253 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sun, 9 Jun 2024 22:07:36 +0200 Subject: [PATCH 0425/1445] Fixture cleanup in UniFi tests (#119227) * Make sure all mock_device_registry are used with usefixtuers * Make sure to use name with fixtures and rename functions to start with fixture_ * Streamline config_entry_setup * Type all *_payload * Mark @pytest.mark.usefixtures("mock_default_requests") * Clean up unnecessary newlines after docstring --- tests/components/unifi/conftest.py | 46 ++++----- tests/components/unifi/test_config_flow.py | 33 +++---- tests/components/unifi/test_device_tracker.py | 1 - tests/components/unifi/test_hub.py | 2 +- tests/components/unifi/test_sensor.py | 4 +- tests/components/unifi/test_services.py | 40 +++----- tests/components/unifi/test_switch.py | 95 ++++++++----------- tests/components/unifi/test_update.py | 11 ++- 8 files changed, 95 insertions(+), 137 deletions(-) diff --git a/tests/components/unifi/conftest.py b/tests/components/unifi/conftest.py index b11c17b3df7..cbb570088c6 100644 --- a/tests/components/unifi/conftest.py +++ b/tests/components/unifi/conftest.py @@ -54,8 +54,8 @@ CONTROLLER_HOST = { } -@pytest.fixture(autouse=True) -def mock_discovery(): +@pytest.fixture(autouse=True, name="mock_discovery") +def fixture_discovery(): """No real network traffic allowed.""" with patch( "homeassistant.components.unifi.config_flow._async_discover_unifi", @@ -64,8 +64,8 @@ def mock_discovery(): yield mock -@pytest.fixture -def mock_device_registry(hass, device_registry: dr.DeviceRegistry): +@pytest.fixture(name="mock_device_registry") +def fixture_device_registry(hass, device_registry: dr.DeviceRegistry): """Mock device registry.""" config_entry = MockConfigEntry(domain="something_else") config_entry.add_to_hass(hass) @@ -93,7 +93,7 @@ def mock_device_registry(hass, device_registry: dr.DeviceRegistry): @pytest.fixture(name="config_entry") -def config_entry_fixture( +def fixture_config_entry( hass: HomeAssistant, config_entry_data: MappingProxyType[str, Any], config_entry_options: MappingProxyType[str, Any], @@ -111,7 +111,7 @@ def config_entry_fixture( @pytest.fixture(name="config_entry_data") -def config_entry_data_fixture() -> MappingProxyType[str, Any]: +def fixture_config_entry_data() -> MappingProxyType[str, Any]: """Define a config entry data fixture.""" return { CONF_HOST: DEFAULT_HOST, @@ -124,7 +124,7 @@ def config_entry_data_fixture() -> MappingProxyType[str, Any]: @pytest.fixture(name="config_entry_options") -def config_entry_options_fixture() -> MappingProxyType[str, Any]: +def fixture_config_entry_options() -> MappingProxyType[str, Any]: """Define a config entry options fixture.""" return {} @@ -133,13 +133,13 @@ def config_entry_options_fixture() -> MappingProxyType[str, Any]: @pytest.fixture(name="known_wireless_clients") -def known_wireless_clients_fixture() -> list[str]: +def fixture_known_wireless_clients() -> list[str]: """Known previously observed wireless clients.""" return [] -@pytest.fixture(autouse=True) -def mock_wireless_client_storage(hass_storage, known_wireless_clients: list[str]): +@pytest.fixture(autouse=True, name="mock_wireless_client_storage") +def fixture_wireless_client_storage(hass_storage, known_wireless_clients: list[str]): """Mock the known wireless storage.""" data: dict[str, list[str]] = ( {"wireless_clients": known_wireless_clients} if known_wireless_clients else {} @@ -151,7 +151,7 @@ def mock_wireless_client_storage(hass_storage, known_wireless_clients: list[str] @pytest.fixture(name="mock_requests") -def request_fixture( +def fixture_request( aioclient_mock: AiohttpClientMocker, client_payload: list[dict[str, Any]], clients_all_payload: list[dict[str, Any]], @@ -198,49 +198,49 @@ def request_fixture( @pytest.fixture(name="client_payload") -def client_data_fixture() -> list[dict[str, Any]]: +def fixture_client_data() -> list[dict[str, Any]]: """Client data.""" return [] @pytest.fixture(name="clients_all_payload") -def clients_all_data_fixture() -> list[dict[str, Any]]: +def fixture_clients_all_data() -> list[dict[str, Any]]: """Clients all data.""" return [] @pytest.fixture(name="device_payload") -def device_data_fixture() -> list[dict[str, Any]]: +def fixture_device_data() -> list[dict[str, Any]]: """Device data.""" return [] @pytest.fixture(name="dpi_app_payload") -def dpi_app_data_fixture() -> list[dict[str, Any]]: +def fixture_dpi_app_data() -> list[dict[str, Any]]: """DPI app data.""" return [] @pytest.fixture(name="dpi_group_payload") -def dpi_group_data_fixture() -> list[dict[str, Any]]: +def fixture_dpi_group_data() -> list[dict[str, Any]]: """DPI group data.""" return [] @pytest.fixture(name="port_forward_payload") -def port_forward_data_fixture() -> list[dict[str, Any]]: +def fixture_port_forward_data() -> list[dict[str, Any]]: """Port forward data.""" return [] @pytest.fixture(name="site_payload") -def site_data_fixture() -> list[dict[str, Any]]: +def fixture_site_data() -> list[dict[str, Any]]: """Site data.""" return [{"desc": "Site name", "name": "site_id", "role": "admin", "_id": "1"}] @pytest.fixture(name="system_information_payload") -def system_information_data_fixture() -> list[dict[str, Any]]: +def fixture_system_information_data() -> list[dict[str, Any]]: """System information data.""" return [ { @@ -262,13 +262,13 @@ def system_information_data_fixture() -> list[dict[str, Any]]: @pytest.fixture(name="wlan_payload") -def wlan_data_fixture() -> list[dict[str, Any]]: +def fixture_wlan_data() -> list[dict[str, Any]]: """WLAN data.""" return [] @pytest.fixture(name="mock_default_requests") -def default_requests_fixture( +def fixture_default_requests( mock_requests: Callable[[str, str], None], ) -> None: """Mock UniFi requests responses with default host and site.""" @@ -276,7 +276,7 @@ def default_requests_fixture( @pytest.fixture(name="config_entry_factory") -async def config_entry_factory_fixture( +async def fixture_config_entry_factory( hass: HomeAssistant, config_entry: ConfigEntry, mock_requests: Callable[[str, str], None], @@ -293,7 +293,7 @@ async def config_entry_factory_fixture( @pytest.fixture(name="config_entry_setup") -async def config_entry_setup_fixture( +async def fixture_config_entry_setup( hass: HomeAssistant, config_entry_factory: Callable[[], ConfigEntry] ) -> ConfigEntry: """Fixture providing a set up instance of UniFi network integration.""" diff --git a/tests/components/unifi/test_config_flow.py b/tests/components/unifi/test_config_flow.py index 7abf45dd16f..7b37437cd1d 100644 --- a/tests/components/unifi/test_config_flow.py +++ b/tests/components/unifi/test_config_flow.py @@ -95,9 +95,8 @@ DPI_GROUPS = [ ] -async def test_flow_works( - hass: HomeAssistant, mock_discovery, mock_default_requests: None -) -> None: +@pytest.mark.usefixtures("mock_default_requests") +async def test_flow_works(hass: HomeAssistant, mock_discovery) -> None: """Test config flow.""" mock_discovery.return_value = "1" result = await hass.config_entries.flow.async_init( @@ -165,9 +164,8 @@ async def test_flow_works_negative_discovery( ] ], ) -async def test_flow_multiple_sites( - hass: HomeAssistant, mock_default_requests: None -) -> None: +@pytest.mark.usefixtures("mock_default_requests") +async def test_flow_multiple_sites(hass: HomeAssistant) -> None: """Test config flow works when finding multiple sites.""" result = await hass.config_entries.flow.async_init( UNIFI_DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -193,9 +191,8 @@ async def test_flow_multiple_sites( assert result["data_schema"]({"site": "2"}) -async def test_flow_raise_already_configured( - hass: HomeAssistant, config_entry_setup: ConfigEntry -) -> None: +@pytest.mark.usefixtures("config_entry_setup") +async def test_flow_raise_already_configured(hass: HomeAssistant) -> None: """Test config flow aborts since a connected config entry already exists.""" result = await hass.config_entries.flow.async_init( UNIFI_DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -219,9 +216,8 @@ async def test_flow_raise_already_configured( assert result["reason"] == "already_configured" -async def test_flow_aborts_configuration_updated( - hass: HomeAssistant, config_entry_setup: ConfigEntry -) -> None: +@pytest.mark.usefixtures("config_entry_setup") +async def test_flow_aborts_configuration_updated(hass: HomeAssistant) -> None: """Test config flow aborts since a connected config entry already exists.""" result = await hass.config_entries.flow.async_init( UNIFI_DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -249,9 +245,8 @@ async def test_flow_aborts_configuration_updated( assert result["reason"] == "configuration_updated" -async def test_flow_fails_user_credentials_faulty( - hass: HomeAssistant, mock_default_requests: None -) -> None: +@pytest.mark.usefixtures("mock_default_requests") +async def test_flow_fails_user_credentials_faulty(hass: HomeAssistant) -> None: """Test config flow.""" result = await hass.config_entries.flow.async_init( UNIFI_DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -276,9 +271,8 @@ async def test_flow_fails_user_credentials_faulty( assert result["errors"] == {"base": "faulty_credentials"} -async def test_flow_fails_hub_unavailable( - hass: HomeAssistant, mock_default_requests: None -) -> None: +@pytest.mark.usefixtures("mock_default_requests") +async def test_flow_fails_hub_unavailable(hass: HomeAssistant) -> None: """Test config flow.""" result = await hass.config_entries.flow.async_init( UNIFI_DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -465,7 +459,6 @@ async def test_simple_option_flow( async def test_form_ssdp(hass: HomeAssistant) -> None: """Test we get the form with ssdp source.""" - result = await hass.config_entries.flow.async_init( UNIFI_DOMAIN, context={"source": config_entries.SOURCE_SSDP}, @@ -507,7 +500,6 @@ async def test_form_ssdp_aborts_if_host_already_exists( hass: HomeAssistant, config_entry: ConfigEntry ) -> None: """Test we abort if the host is already configured.""" - result = await hass.config_entries.flow.async_init( UNIFI_DOMAIN, context={"source": config_entries.SOURCE_SSDP}, @@ -551,7 +543,6 @@ async def test_form_ssdp_aborts_if_serial_already_exists( async def test_form_ssdp_gets_form_with_ignored_entry(hass: HomeAssistant) -> None: """Test we can still setup if there is an ignored never configured entry.""" - entry = MockConfigEntry( domain=UNIFI_DOMAIN, data={"not_controller_key": None}, diff --git a/tests/components/unifi/test_device_tracker.py b/tests/components/unifi/test_device_tracker.py index c8149b75fe0..3f3913ad0b3 100644 --- a/tests/components/unifi/test_device_tracker.py +++ b/tests/components/unifi/test_device_tracker.py @@ -545,7 +545,6 @@ async def test_option_track_clients( hass: HomeAssistant, config_entry_setup: ConfigEntry ) -> None: """Test the tracking of clients can be turned off.""" - assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 3 assert hass.states.get("device_tracker.wireless_client") assert hass.states.get("device_tracker.wired_client") diff --git a/tests/components/unifi/test_hub.py b/tests/components/unifi/test_hub.py index 312ad5cef93..0d75a83c5f5 100644 --- a/tests/components/unifi/test_hub.py +++ b/tests/components/unifi/test_hub.py @@ -77,9 +77,9 @@ async def test_reset_fails( assert config_entry_setup.state is ConfigEntryState.LOADED +@pytest.mark.usefixtures("mock_device_registry") async def test_connection_state_signalling( hass: HomeAssistant, - mock_device_registry, mock_websocket_state, config_entry_factory: Callable[[], ConfigEntry], client_payload: list[dict[str, Any]], diff --git a/tests/components/unifi/test_sensor.py b/tests/components/unifi/test_sensor.py index 3131eefbbee..735df53b0c5 100644 --- a/tests/components/unifi/test_sensor.py +++ b/tests/components/unifi/test_sensor.py @@ -1045,7 +1045,6 @@ async def test_device_system_stats( device_payload: list[dict[str, Any]], ) -> None: """Verify that device stats sensors are working as expected.""" - assert len(hass.states.async_all()) == 8 assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 4 @@ -1134,14 +1133,13 @@ async def test_device_system_stats( ] ], ) -@pytest.mark.usefixtures("config_entry_setup") async def test_bandwidth_port_sensors( hass: HomeAssistant, entity_registry: er.EntityRegistry, mock_websocket_message, config_entry_setup: ConfigEntry, config_entry_options: MappingProxyType[str, Any], - device_payload, + device_payload: list[dict[str, Any]], ) -> None: """Verify that port bandwidth sensors are working as expected.""" assert len(hass.states.async_all()) == 5 diff --git a/tests/components/unifi/test_services.py b/tests/components/unifi/test_services.py index 210d52d1fb9..a85d4494d4a 100644 --- a/tests/components/unifi/test_services.py +++ b/tests/components/unifi/test_services.py @@ -29,16 +29,14 @@ async def test_reconnect_client( client_payload: list[dict[str, Any]], ) -> None: """Verify call to reconnect client is performed as expected.""" - config_entry = config_entry_setup - aioclient_mock.clear_requests() aioclient_mock.post( - f"https://{config_entry.data[CONF_HOST]}:1234" - f"/api/s/{config_entry.data[CONF_SITE_ID]}/cmd/stamgr", + f"https://{config_entry_setup.data[CONF_HOST]}:1234" + f"/api/s/{config_entry_setup.data[CONF_SITE_ID]}/cmd/stamgr", ) device_entry = device_registry.async_get_or_create( - config_entry_id=config_entry.entry_id, + config_entry_id=config_entry_setup.entry_id, connections={(dr.CONNECTION_NETWORK_MAC, client_payload[0]["mac"])}, ) @@ -74,12 +72,10 @@ async def test_reconnect_device_without_mac( config_entry_setup: ConfigEntry, ) -> None: """Verify no call is made if device does not have a known mac.""" - config_entry = config_entry_setup - aioclient_mock.clear_requests() device_entry = device_registry.async_get_or_create( - config_entry_id=config_entry.entry_id, + config_entry_id=config_entry_setup.entry_id, connections={("other connection", "not mac")}, ) @@ -103,16 +99,14 @@ async def test_reconnect_client_hub_unavailable( client_payload: list[dict[str, Any]], ) -> None: """Verify no call is made if hub is unavailable.""" - config_entry = config_entry_setup - aioclient_mock.clear_requests() aioclient_mock.post( - f"https://{config_entry.data[CONF_HOST]}:1234" - f"/api/s/{config_entry.data[CONF_SITE_ID]}/cmd/stamgr", + f"https://{config_entry_setup.data[CONF_HOST]}:1234" + f"/api/s/{config_entry_setup.data[CONF_SITE_ID]}/cmd/stamgr", ) device_entry = device_registry.async_get_or_create( - config_entry_id=config_entry.entry_id, + config_entry_id=config_entry_setup.entry_id, connections={(dr.CONNECTION_NETWORK_MAC, client_payload[0]["mac"])}, ) @@ -136,12 +130,9 @@ async def test_reconnect_client_unknown_mac( config_entry_setup: ConfigEntry, ) -> None: """Verify no call is made if trying to reconnect a mac unknown to hub.""" - config_entry = config_entry_setup - aioclient_mock.clear_requests() - device_entry = device_registry.async_get_or_create( - config_entry_id=config_entry.entry_id, + config_entry_id=config_entry_setup.entry_id, connections={(dr.CONNECTION_NETWORK_MAC, "mac unknown to hub")}, ) @@ -165,12 +156,9 @@ async def test_reconnect_wired_client( client_payload: list[dict[str, Any]], ) -> None: """Verify no call is made if client is wired.""" - config_entry = config_entry_setup - aioclient_mock.clear_requests() - device_entry = device_registry.async_get_or_create( - config_entry_id=config_entry.entry_id, + config_entry_id=config_entry_setup.entry_id, connections={(dr.CONNECTION_NETWORK_MAC, client_payload[0]["mac"])}, ) @@ -219,12 +207,10 @@ async def test_remove_clients( config_entry_setup: ConfigEntry, ) -> None: """Verify removing different variations of clients work.""" - config_entry = config_entry_setup - aioclient_mock.clear_requests() aioclient_mock.post( - f"https://{config_entry.data[CONF_HOST]}:1234" - f"/api/s/{config_entry.data[CONF_SITE_ID]}/cmd/stamgr", + f"https://{config_entry_setup.data[CONF_HOST]}:1234" + f"/api/s/{config_entry_setup.data[CONF_SITE_ID]}/cmd/stamgr", ) await hass.services.async_call(UNIFI_DOMAIN, SERVICE_REMOVE_CLIENTS, blocking=True) @@ -233,7 +219,7 @@ async def test_remove_clients( "macs": ["00:00:00:00:00:00", "00:00:00:00:00:01"], } - assert await hass.config_entries.async_unload(config_entry.entry_id) + assert await hass.config_entries.async_unload(config_entry_setup.entry_id) @pytest.mark.parametrize( @@ -254,7 +240,6 @@ async def test_remove_clients_hub_unavailable( ) -> None: """Verify no call is made if UniFi Network is unavailable.""" aioclient_mock.clear_requests() - with patch( "homeassistant.components.unifi.UnifiHub.available", new_callable=PropertyMock ) as ws_mock: @@ -283,6 +268,5 @@ async def test_remove_clients_no_call_on_empty_list( ) -> None: """Verify no call is made if no fitting client has been added to the list.""" aioclient_mock.clear_requests() - await hass.services.async_call(UNIFI_DOMAIN, SERVICE_REMOVE_CLIENTS, blocking=True) assert aioclient_mock.call_count == 0 diff --git a/tests/components/unifi/test_switch.py b/tests/components/unifi/test_switch.py index 851f0107c39..3f2e82be7d2 100644 --- a/tests/components/unifi/test_switch.py +++ b/tests/components/unifi/test_switch.py @@ -1,7 +1,9 @@ """UniFi Network switch platform tests.""" +from collections.abc import Callable from copy import deepcopy from datetime import timedelta +from typing import Any from aiounifi.models.message import MessageKey import pytest @@ -20,7 +22,7 @@ from homeassistant.components.unifi.const import ( CONF_TRACK_DEVICES, DOMAIN as UNIFI_DOMAIN, ) -from homeassistant.config_entries import RELOAD_AFTER_UPDATE_DELAY +from homeassistant.config_entries import RELOAD_AFTER_UPDATE_DELAY, ConfigEntry from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_ENTITY_ID, @@ -800,11 +802,9 @@ async def test_switches( hass: HomeAssistant, entity_registry: er.EntityRegistry, aioclient_mock: AiohttpClientMocker, - config_entry_setup, + config_entry_setup: ConfigEntry, ) -> None: """Test the update_items function with some clients.""" - config_entry = config_entry_setup - assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 3 switch_4 = hass.states.get("switch.poe_client_4") @@ -831,8 +831,8 @@ async def test_switches( # Block and unblock client aioclient_mock.clear_requests() aioclient_mock.post( - f"https://{config_entry.data[CONF_HOST]}:1234" - f"/api/s/{config_entry.data[CONF_SITE_ID]}/cmd/stamgr", + f"https://{config_entry_setup.data[CONF_HOST]}:1234" + f"/api/s/{config_entry_setup.data[CONF_SITE_ID]}/cmd/stamgr", ) await hass.services.async_call( @@ -856,8 +856,8 @@ async def test_switches( # Enable and disable DPI aioclient_mock.clear_requests() aioclient_mock.put( - f"https://{config_entry.data[CONF_HOST]}:1234" - f"/api/s/{config_entry.data[CONF_SITE_ID]}/rest/dpiapp/{DPI_APPS[0]['_id']}", + f"https://{config_entry_setup.data[CONF_HOST]}:1234" + f"/api/s/{config_entry_setup.data[CONF_SITE_ID]}/rest/dpiapp/{DPI_APPS[0]['_id']}", ) await hass.services.async_call( @@ -924,11 +924,9 @@ async def test_block_switches( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, mock_websocket_message, - config_entry_setup, + config_entry_setup: ConfigEntry, ) -> None: """Test the update_items function with some clients.""" - config_entry = config_entry_setup - assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 2 blocked = hass.states.get("switch.block_client_1") @@ -959,8 +957,8 @@ async def test_block_switches( aioclient_mock.clear_requests() aioclient_mock.post( - f"https://{config_entry.data[CONF_HOST]}:1234" - f"/api/s/{config_entry.data[CONF_SITE_ID]}/cmd/stamgr", + f"https://{config_entry_setup.data[CONF_HOST]}:1234" + f"/api/s/{config_entry_setup.data[CONF_SITE_ID]}/cmd/stamgr", ) await hass.services.async_call( @@ -1085,16 +1083,14 @@ async def test_outlet_switches( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, mock_websocket_message, - config_entry_setup, - device_payload, + config_entry_setup: ConfigEntry, + device_payload: list[dict[str, Any]], mock_websocket_state, entity_id: str, outlet_index: int, expected_switches: int, ) -> None: """Test the outlet entities.""" - config_entry = config_entry_setup - assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == expected_switches # Validate state object @@ -1114,8 +1110,8 @@ async def test_outlet_switches( device_id = device_payload[0]["device_id"] aioclient_mock.clear_requests() aioclient_mock.put( - f"https://{config_entry.data[CONF_HOST]}:1234" - f"/api/s/{config_entry.data[CONF_SITE_ID]}/rest/device/{device_id}", + f"https://{config_entry_setup.data[CONF_HOST]}:1234" + f"/api/s/{config_entry_setup.data[CONF_SITE_ID]}/rest/device/{device_id}", ) await hass.services.async_call( @@ -1171,11 +1167,11 @@ async def test_outlet_switches( assert hass.states.get(f"switch.{entity_id}").state == STATE_OFF # Unload config entry - await hass.config_entries.async_unload(config_entry.entry_id) + await hass.config_entries.async_unload(config_entry_setup.entry_id) assert hass.states.get(f"switch.{entity_id}").state == STATE_UNAVAILABLE # Remove config entry - await hass.config_entries.async_remove(config_entry.entry_id) + await hass.config_entries.async_remove(config_entry_setup.entry_id) await hass.async_block_till_done() assert hass.states.get(f"switch.{entity_id}") is None @@ -1211,16 +1207,14 @@ async def test_new_client_discovered_on_block_control( ) @pytest.mark.parametrize("clients_all_payload", [[BLOCKED, UNBLOCKED]]) async def test_option_block_clients( - hass: HomeAssistant, config_entry_setup, clients_all_payload + hass: HomeAssistant, config_entry_setup: ConfigEntry, clients_all_payload ) -> None: """Test the changes to option reflects accordingly.""" - config_entry = config_entry_setup - assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 1 # Add a second switch hass.config_entries.async_update_entry( - config_entry, + config_entry_setup, options={ CONF_BLOCK_CLIENT: [ clients_all_payload[0]["mac"], @@ -1233,24 +1227,21 @@ async def test_option_block_clients( # Remove the second switch again hass.config_entries.async_update_entry( - config_entry, - options={CONF_BLOCK_CLIENT: [clients_all_payload[0]["mac"]]}, + config_entry_setup, options={CONF_BLOCK_CLIENT: [clients_all_payload[0]["mac"]]} ) await hass.async_block_till_done() assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 1 # Enable one and remove the other one hass.config_entries.async_update_entry( - config_entry, - options={CONF_BLOCK_CLIENT: [clients_all_payload[1]["mac"]]}, + config_entry_setup, options={CONF_BLOCK_CLIENT: [clients_all_payload[1]["mac"]]} ) await hass.async_block_till_done() assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 0 # Remove one hass.config_entries.async_update_entry( - config_entry, - options={CONF_BLOCK_CLIENT: []}, + config_entry_setup, options={CONF_BLOCK_CLIENT: []} ) await hass.async_block_till_done() assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 0 @@ -1263,16 +1254,15 @@ async def test_option_block_clients( @pytest.mark.parametrize("client_payload", [[CLIENT_1]]) @pytest.mark.parametrize("dpi_app_payload", [DPI_APPS]) @pytest.mark.parametrize("dpi_group_payload", [DPI_GROUPS]) -async def test_option_remove_switches(hass: HomeAssistant, config_entry_setup) -> None: +async def test_option_remove_switches( + hass: HomeAssistant, config_entry_setup: ConfigEntry +) -> None: """Test removal of DPI switch when options updated.""" - config_entry = config_entry_setup - assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 1 # Disable DPI Switches hass.config_entries.async_update_entry( - config_entry, - options={CONF_DPI_RESTRICTIONS: False}, + config_entry_setup, options={CONF_DPI_RESTRICTIONS: False} ) await hass.async_block_till_done() assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 0 @@ -1285,12 +1275,10 @@ async def test_poe_port_switches( aioclient_mock: AiohttpClientMocker, mock_websocket_message, mock_websocket_state, - config_entry_setup, - device_payload, + config_entry_setup: ConfigEntry, + device_payload: list[dict[str, Any]], ) -> None: """Test PoE port entities work.""" - config_entry = config_entry_setup - assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 0 ent_reg_entry = entity_registry.async_get("switch.mock_name_port_1_poe") @@ -1328,8 +1316,8 @@ async def test_poe_port_switches( # Turn off PoE aioclient_mock.clear_requests() aioclient_mock.put( - f"https://{config_entry.data[CONF_HOST]}:1234" - f"/api/s/{config_entry.data[CONF_SITE_ID]}/rest/device/mock-id", + f"https://{config_entry_setup.data[CONF_HOST]}:1234" + f"/api/s/{config_entry_setup.data[CONF_SITE_ID]}/rest/device/mock-id", ) await hass.services.async_call( @@ -1398,12 +1386,10 @@ async def test_wlan_switches( aioclient_mock: AiohttpClientMocker, mock_websocket_message, mock_websocket_state, - config_entry_setup, - wlan_payload, + config_entry_setup: ConfigEntry, + wlan_payload: list[dict[str, Any]], ) -> None: """Test control of UniFi WLAN availability.""" - config_entry = config_entry_setup - assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 1 ent_reg_entry = entity_registry.async_get("switch.ssid_1") @@ -1426,8 +1412,8 @@ async def test_wlan_switches( # Disable WLAN aioclient_mock.clear_requests() aioclient_mock.put( - f"https://{config_entry.data[CONF_HOST]}:1234" - f"/api/s/{config_entry.data[CONF_SITE_ID]}/rest/wlanconf/{wlan['_id']}", + f"https://{config_entry_setup.data[CONF_HOST]}:1234" + f"/api/s/{config_entry_setup.data[CONF_SITE_ID]}/rest/wlanconf/{wlan['_id']}", ) await hass.services.async_call( @@ -1485,11 +1471,10 @@ async def test_port_forwarding_switches( aioclient_mock: AiohttpClientMocker, mock_websocket_message, mock_websocket_state, - config_entry_setup, - port_forward_payload, + config_entry_setup: ConfigEntry, + port_forward_payload: list[dict[str, Any]], ) -> None: """Test control of UniFi port forwarding.""" - config_entry = config_entry_setup assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 1 ent_reg_entry = entity_registry.async_get("switch.unifi_network_plex") @@ -1512,8 +1497,8 @@ async def test_port_forwarding_switches( # Disable port forward aioclient_mock.clear_requests() aioclient_mock.put( - f"https://{config_entry.data[CONF_HOST]}:1234" - f"/api/s/{config_entry.data[CONF_SITE_ID]}/rest/portforward/{data['_id']}", + f"https://{config_entry_setup.data[CONF_HOST]}:1234" + f"/api/s/{config_entry_setup.data[CONF_SITE_ID]}/rest/portforward/{data['_id']}", ) await hass.services.async_call( @@ -1594,8 +1579,8 @@ async def test_port_forwarding_switches( async def test_updating_unique_id( hass: HomeAssistant, entity_registry: er.EntityRegistry, - config_entry_factory, - config_entry, + config_entry_factory: Callable[[], ConfigEntry], + config_entry: ConfigEntry, device_payload, ) -> None: """Verify outlet control and poe control unique ID update works.""" diff --git a/tests/components/unifi/test_update.py b/tests/components/unifi/test_update.py index c44b2993a8b..3b1de6c4456 100644 --- a/tests/components/unifi/test_update.py +++ b/tests/components/unifi/test_update.py @@ -16,6 +16,7 @@ from homeassistant.components.update import ( UpdateDeviceClass, UpdateEntityFeature, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_ENTITY_ID, @@ -138,18 +139,18 @@ async def test_not_admin(hass: HomeAssistant) -> None: @pytest.mark.parametrize("device_payload", [[DEVICE_1]]) async def test_install( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, config_entry_setup + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, + config_entry_setup: ConfigEntry, ) -> None: """Test the device update install call.""" - config_entry = config_entry_setup - assert len(hass.states.async_entity_ids(UPDATE_DOMAIN)) == 1 device_state = hass.states.get("update.device_1") assert device_state.state == STATE_ON url = ( - f"https://{config_entry.data[CONF_HOST]}:1234" - f"/api/s/{config_entry.data[CONF_SITE_ID]}/cmd/devmgr" + f"https://{config_entry_setup.data[CONF_HOST]}:1234" + f"/api/s/{config_entry_setup.data[CONF_SITE_ID]}/cmd/devmgr" ) aioclient_mock.clear_requests() aioclient_mock.post(url) From 9b41fa5f258de7cd04ad4cdbfbc497aeb95d673c Mon Sep 17 00:00:00 2001 From: David Knowles Date: Sun, 9 Jun 2024 16:56:19 -0400 Subject: [PATCH 0426/1445] Bump pyschlage to 2024.6.0 (#119233) --- homeassistant/components/schlage/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/schlage/manifest.json b/homeassistant/components/schlage/manifest.json index 23b36ddae0b..c6dfc443bb8 100644 --- a/homeassistant/components/schlage/manifest.json +++ b/homeassistant/components/schlage/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/schlage", "iot_class": "cloud_polling", - "requirements": ["pyschlage==2024.2.0"] + "requirements": ["pyschlage==2024.6.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index c3eb350b521..a6d918507c4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2135,7 +2135,7 @@ pysabnzbd==1.1.1 pysaj==0.0.16 # homeassistant.components.schlage -pyschlage==2024.2.0 +pyschlage==2024.6.0 # homeassistant.components.sensibo pysensibo==1.0.36 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4f1e667e1d2..d1426bbd0be 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1677,7 +1677,7 @@ pyrympro==0.0.8 pysabnzbd==1.1.1 # homeassistant.components.schlage -pyschlage==2024.2.0 +pyschlage==2024.6.0 # homeassistant.components.sensibo pysensibo==1.0.36 From 0c585e1836c2882e342d5d591cd4590273e3c812 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Sun, 9 Jun 2024 22:57:12 +0200 Subject: [PATCH 0427/1445] Bump reolink-aio to 0.9.2 (#119236) --- homeassistant/components/reolink/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/reolink/manifest.json b/homeassistant/components/reolink/manifest.json index 36bc8731925..ba4d88578f1 100644 --- a/homeassistant/components/reolink/manifest.json +++ b/homeassistant/components/reolink/manifest.json @@ -18,5 +18,5 @@ "documentation": "https://www.home-assistant.io/integrations/reolink", "iot_class": "local_push", "loggers": ["reolink_aio"], - "requirements": ["reolink-aio==0.9.1"] + "requirements": ["reolink-aio==0.9.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index a6d918507c4..d96b59f5ccc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2460,7 +2460,7 @@ renault-api==0.2.3 renson-endura-delta==1.7.1 # homeassistant.components.reolink -reolink-aio==0.9.1 +reolink-aio==0.9.2 # homeassistant.components.idteck_prox rfk101py==0.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d1426bbd0be..7f8feec04f1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1924,7 +1924,7 @@ renault-api==0.2.3 renson-endura-delta==1.7.1 # homeassistant.components.reolink -reolink-aio==0.9.1 +reolink-aio==0.9.2 # homeassistant.components.rflink rflink==0.0.66 From 39820caa1ab6d50ffd1bb1936870018c276260de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ethem=20Cem=20=C3=96zkan?= Date: Sun, 9 Jun 2024 22:58:49 +0200 Subject: [PATCH 0428/1445] Bump python-roborock to 2.3.0 (#119228) --- homeassistant/components/roborock/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/roborock/manifest.json b/homeassistant/components/roborock/manifest.json index 3fd6dd7d782..42c0f9ba347 100644 --- a/homeassistant/components/roborock/manifest.json +++ b/homeassistant/components/roborock/manifest.json @@ -7,7 +7,7 @@ "iot_class": "local_polling", "loggers": ["roborock"], "requirements": [ - "python-roborock==2.2.3", + "python-roborock==2.3.0", "vacuum-map-parser-roborock==0.1.2" ] } diff --git a/requirements_all.txt b/requirements_all.txt index d96b59f5ccc..bf50af47ad3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2312,7 +2312,7 @@ python-rabbitair==0.0.8 python-ripple-api==0.0.3 # homeassistant.components.roborock -python-roborock==2.2.3 +python-roborock==2.3.0 # homeassistant.components.smarttub python-smarttub==0.0.36 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7f8feec04f1..41a3cf07b5c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1806,7 +1806,7 @@ python-qbittorrent==0.4.3 python-rabbitair==0.0.8 # homeassistant.components.roborock -python-roborock==2.2.3 +python-roborock==2.3.0 # homeassistant.components.smarttub python-smarttub==0.0.36 From b70a33a7184f9227dfca197f7b30be5c978904a0 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Sun, 9 Jun 2024 23:02:11 +0200 Subject: [PATCH 0429/1445] Add Reolink manual record switch (#119232) Add manual record switch --- homeassistant/components/reolink/icons.json | 3 +++ homeassistant/components/reolink/strings.json | 3 +++ homeassistant/components/reolink/switch.py | 9 +++++++++ 3 files changed, 15 insertions(+) diff --git a/homeassistant/components/reolink/icons.json b/homeassistant/components/reolink/icons.json index a06293abf9a..a4620bd95d5 100644 --- a/homeassistant/components/reolink/icons.json +++ b/homeassistant/components/reolink/icons.json @@ -264,6 +264,9 @@ "record": { "default": "mdi:record-rec" }, + "manual_record": { + "default": "mdi:record-rec" + }, "buzzer": { "default": "mdi:room-service" }, diff --git a/homeassistant/components/reolink/strings.json b/homeassistant/components/reolink/strings.json index 799e7f2cac5..aa141818ec6 100644 --- a/homeassistant/components/reolink/strings.json +++ b/homeassistant/components/reolink/strings.json @@ -544,6 +544,9 @@ "record": { "name": "Record" }, + "manual_record": { + "name": "Manual record" + }, "buzzer": { "name": "Buzzer on event" }, diff --git a/homeassistant/components/reolink/switch.py b/homeassistant/components/reolink/switch.py index a672afe745e..f1a8de09509 100644 --- a/homeassistant/components/reolink/switch.py +++ b/homeassistant/components/reolink/switch.py @@ -146,6 +146,15 @@ SWITCH_ENTITIES = ( value=lambda api, ch: api.recording_enabled(ch), method=lambda api, ch, value: api.set_recording(ch, value), ), + ReolinkSwitchEntityDescription( + key="manual_record", + cmd_key="GetManualRec", + translation_key="manual_record", + entity_category=EntityCategory.CONFIG, + supported=lambda api, ch: api.supported(ch, "manual_record"), + value=lambda api, ch: api.manual_record_enabled(ch), + method=lambda api, ch, value: api.set_manual_record(ch, value), + ), ReolinkSwitchEntityDescription( key="buzzer", cmd_key="GetBuzzerAlarmV20", From d657feafa6b39e3b14ca2e525551d64fbab637bf Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 9 Jun 2024 18:25:39 -0500 Subject: [PATCH 0430/1445] Switch unifiprotect lib to use uiprotect (#119243) --- homeassistant/components/unifiprotect/__init__.py | 10 +++++----- .../components/unifiprotect/binary_sensor.py | 4 ++-- homeassistant/components/unifiprotect/button.py | 2 +- homeassistant/components/unifiprotect/camera.py | 4 ++-- homeassistant/components/unifiprotect/config_flow.py | 6 +++--- homeassistant/components/unifiprotect/const.py | 2 +- homeassistant/components/unifiprotect/data.py | 10 +++++----- homeassistant/components/unifiprotect/diagnostics.py | 2 +- homeassistant/components/unifiprotect/entity.py | 2 +- homeassistant/components/unifiprotect/light.py | 2 +- homeassistant/components/unifiprotect/lock.py | 2 +- homeassistant/components/unifiprotect/manifest.json | 4 ++-- .../components/unifiprotect/media_player.py | 4 ++-- .../components/unifiprotect/media_source.py | 12 +++--------- homeassistant/components/unifiprotect/migrate.py | 4 ++-- homeassistant/components/unifiprotect/models.py | 2 +- homeassistant/components/unifiprotect/number.py | 2 +- homeassistant/components/unifiprotect/repairs.py | 6 +++--- homeassistant/components/unifiprotect/select.py | 4 ++-- homeassistant/components/unifiprotect/sensor.py | 2 +- homeassistant/components/unifiprotect/services.py | 6 +++--- homeassistant/components/unifiprotect/switch.py | 2 +- homeassistant/components/unifiprotect/text.py | 2 +- homeassistant/components/unifiprotect/utils.py | 6 +++--- homeassistant/components/unifiprotect/views.py | 4 ++-- requirements_all.txt | 6 +++--- requirements_test_all.txt | 6 +++--- tests/components/unifiprotect/conftest.py | 4 ++-- tests/components/unifiprotect/test_binary_sensor.py | 4 ++-- tests/components/unifiprotect/test_button.py | 2 +- tests/components/unifiprotect/test_camera.py | 4 ++-- tests/components/unifiprotect/test_config_flow.py | 4 ++-- tests/components/unifiprotect/test_diagnostics.py | 2 +- tests/components/unifiprotect/test_init.py | 4 ++-- tests/components/unifiprotect/test_light.py | 4 ++-- tests/components/unifiprotect/test_lock.py | 2 +- tests/components/unifiprotect/test_media_player.py | 4 ++-- tests/components/unifiprotect/test_media_source.py | 4 ++-- tests/components/unifiprotect/test_migrate.py | 2 +- tests/components/unifiprotect/test_number.py | 2 +- tests/components/unifiprotect/test_recorder.py | 2 +- tests/components/unifiprotect/test_repairs.py | 2 +- tests/components/unifiprotect/test_select.py | 4 ++-- tests/components/unifiprotect/test_sensor.py | 11 ++--------- tests/components/unifiprotect/test_services.py | 6 +++--- tests/components/unifiprotect/test_switch.py | 2 +- tests/components/unifiprotect/test_text.py | 2 +- tests/components/unifiprotect/test_views.py | 4 ++-- tests/components/unifiprotect/utils.py | 8 ++++---- 49 files changed, 94 insertions(+), 107 deletions(-) diff --git a/homeassistant/components/unifiprotect/__init__.py b/homeassistant/components/unifiprotect/__init__.py index d85f91be860..0f41011361d 100644 --- a/homeassistant/components/unifiprotect/__init__.py +++ b/homeassistant/components/unifiprotect/__init__.py @@ -6,14 +6,14 @@ from datetime import timedelta import logging from aiohttp.client_exceptions import ServerDisconnectedError -from pyunifiprotect.data import Bootstrap -from pyunifiprotect.data.types import FirmwareReleaseChannel -from pyunifiprotect.exceptions import ClientError, NotAuthorized +from uiprotect.data import Bootstrap +from uiprotect.data.types import FirmwareReleaseChannel +from uiprotect.exceptions import ClientError, NotAuthorized -# Import the test_util.anonymize module from the pyunifiprotect package +# Import the test_util.anonymize module from the uiprotect package # in __init__ to ensure it gets imported in the executor since the # diagnostics module will not be imported in the executor. -from pyunifiprotect.test_util.anonymize import anonymize_data # noqa: F401 +from uiprotect.test_util.anonymize import anonymize_data # noqa: F401 from homeassistant.config_entries import ConfigEntry from homeassistant.const import EVENT_HOMEASSISTANT_STOP diff --git a/homeassistant/components/unifiprotect/binary_sensor.py b/homeassistant/components/unifiprotect/binary_sensor.py index f779fc7a1ad..b6aaed8f975 100644 --- a/homeassistant/components/unifiprotect/binary_sensor.py +++ b/homeassistant/components/unifiprotect/binary_sensor.py @@ -6,7 +6,7 @@ import dataclasses import logging from typing import Any -from pyunifiprotect.data import ( +from uiprotect.data import ( NVR, Camera, Light, @@ -16,7 +16,7 @@ from pyunifiprotect.data import ( ProtectModelWithId, Sensor, ) -from pyunifiprotect.data.nvr import UOSDisk +from uiprotect.data.nvr import UOSDisk from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, diff --git a/homeassistant/components/unifiprotect/button.py b/homeassistant/components/unifiprotect/button.py index db27306aedf..0db05a6cdc9 100644 --- a/homeassistant/components/unifiprotect/button.py +++ b/homeassistant/components/unifiprotect/button.py @@ -6,7 +6,7 @@ from dataclasses import dataclass import logging from typing import Final -from pyunifiprotect.data import ProtectAdoptableDeviceModel, ProtectModelWithId +from uiprotect.data import ProtectAdoptableDeviceModel, ProtectModelWithId from homeassistant.components.button import ( ButtonDeviceClass, diff --git a/homeassistant/components/unifiprotect/camera.py b/homeassistant/components/unifiprotect/camera.py index 7a73c94c535..04ac2a823a3 100644 --- a/homeassistant/components/unifiprotect/camera.py +++ b/homeassistant/components/unifiprotect/camera.py @@ -5,7 +5,8 @@ from __future__ import annotations import logging from typing import Any, cast -from pyunifiprotect.data import ( +from typing_extensions import Generator +from uiprotect.data import ( Camera as UFPCamera, CameraChannel, ModelType, @@ -13,7 +14,6 @@ from pyunifiprotect.data import ( ProtectModelWithId, StateType, ) -from typing_extensions import Generator from homeassistant.components.camera import Camera, CameraEntityFeature from homeassistant.config_entries import ConfigEntry diff --git a/homeassistant/components/unifiprotect/config_flow.py b/homeassistant/components/unifiprotect/config_flow.py index 19561a6003d..284b7003485 100644 --- a/homeassistant/components/unifiprotect/config_flow.py +++ b/homeassistant/components/unifiprotect/config_flow.py @@ -8,9 +8,9 @@ from pathlib import Path from typing import Any from aiohttp import CookieJar -from pyunifiprotect import ProtectApiClient -from pyunifiprotect.data import NVR -from pyunifiprotect.exceptions import ClientError, NotAuthorized +from uiprotect import ProtectApiClient +from uiprotect.data import NVR +from uiprotect.exceptions import ClientError, NotAuthorized from unifi_discovery import async_console_is_alive import voluptuous as vol diff --git a/homeassistant/components/unifiprotect/const.py b/homeassistant/components/unifiprotect/const.py index 39be5f0e7cb..f51a58aadc7 100644 --- a/homeassistant/components/unifiprotect/const.py +++ b/homeassistant/components/unifiprotect/const.py @@ -1,6 +1,6 @@ """Constant definitions for UniFi Protect Integration.""" -from pyunifiprotect.data import ModelType, Version +from uiprotect.data import ModelType, Version from homeassistant.const import Platform diff --git a/homeassistant/components/unifiprotect/data.py b/homeassistant/components/unifiprotect/data.py index 52d40d9e89e..5ca9b5aaeb7 100644 --- a/homeassistant/components/unifiprotect/data.py +++ b/homeassistant/components/unifiprotect/data.py @@ -8,8 +8,9 @@ from functools import partial import logging from typing import Any, cast -from pyunifiprotect import ProtectApiClient -from pyunifiprotect.data import ( +from typing_extensions import Generator +from uiprotect import ProtectApiClient +from uiprotect.data import ( NVR, Bootstrap, Camera, @@ -20,9 +21,8 @@ from pyunifiprotect.data import ( ProtectAdoptableDeviceModel, WSSubscriptionMessage, ) -from pyunifiprotect.exceptions import ClientError, NotAuthorized -from pyunifiprotect.utils import log_event -from typing_extensions import Generator +from uiprotect.exceptions import ClientError, NotAuthorized +from uiprotect.utils import log_event from homeassistant.config_entries import ConfigEntry from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback diff --git a/homeassistant/components/unifiprotect/diagnostics.py b/homeassistant/components/unifiprotect/diagnostics.py index b85870a08c5..ac651f6138d 100644 --- a/homeassistant/components/unifiprotect/diagnostics.py +++ b/homeassistant/components/unifiprotect/diagnostics.py @@ -4,7 +4,7 @@ from __future__ import annotations from typing import Any, cast -from pyunifiprotect.test_util.anonymize import anonymize_data +from uiprotect.test_util.anonymize import anonymize_data from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant diff --git a/homeassistant/components/unifiprotect/entity.py b/homeassistant/components/unifiprotect/entity.py index 49478ce0582..766c93949bd 100644 --- a/homeassistant/components/unifiprotect/entity.py +++ b/homeassistant/components/unifiprotect/entity.py @@ -6,7 +6,7 @@ from collections.abc import Callable, Sequence import logging from typing import TYPE_CHECKING, Any -from pyunifiprotect.data import ( +from uiprotect.data import ( NVR, Camera, Chime, diff --git a/homeassistant/components/unifiprotect/light.py b/homeassistant/components/unifiprotect/light.py index 3ce236b3e23..18e611f2307 100644 --- a/homeassistant/components/unifiprotect/light.py +++ b/homeassistant/components/unifiprotect/light.py @@ -5,7 +5,7 @@ from __future__ import annotations import logging from typing import Any -from pyunifiprotect.data import ( +from uiprotect.data import ( Light, ModelType, ProtectAdoptableDeviceModel, diff --git a/homeassistant/components/unifiprotect/lock.py b/homeassistant/components/unifiprotect/lock.py index c54f9b316ff..6bb1dd7b4ee 100644 --- a/homeassistant/components/unifiprotect/lock.py +++ b/homeassistant/components/unifiprotect/lock.py @@ -5,7 +5,7 @@ from __future__ import annotations import logging from typing import Any, cast -from pyunifiprotect.data import ( +from uiprotect.data import ( Doorlock, LockStatusType, ModelType, diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index a09db1cf01a..9cb6ceb7cb9 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -39,8 +39,8 @@ "documentation": "https://www.home-assistant.io/integrations/unifiprotect", "integration_type": "hub", "iot_class": "local_push", - "loggers": ["pyunifiprotect", "unifi_discovery"], - "requirements": ["pyunifiprotect==5.1.2", "unifi-discovery==1.1.8"], + "loggers": ["uiprotect", "unifi_discovery"], + "requirements": ["uiprotect==0.3.9", "unifi-discovery==1.1.8"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/homeassistant/components/unifiprotect/media_player.py b/homeassistant/components/unifiprotect/media_player.py index 50fec39e9cb..eb17137842b 100644 --- a/homeassistant/components/unifiprotect/media_player.py +++ b/homeassistant/components/unifiprotect/media_player.py @@ -5,14 +5,14 @@ from __future__ import annotations import logging from typing import Any, cast -from pyunifiprotect.data import ( +from uiprotect.data import ( Camera, ModelType, ProtectAdoptableDeviceModel, ProtectModelWithId, StateType, ) -from pyunifiprotect.exceptions import StreamError +from uiprotect.exceptions import StreamError from homeassistant.components import media_source from homeassistant.components.media_player import ( diff --git a/homeassistant/components/unifiprotect/media_source.py b/homeassistant/components/unifiprotect/media_source.py index 0ff27f562ea..1a67efcfd03 100644 --- a/homeassistant/components/unifiprotect/media_source.py +++ b/homeassistant/components/unifiprotect/media_source.py @@ -7,15 +7,9 @@ from datetime import date, datetime, timedelta from enum import Enum from typing import Any, NoReturn, cast -from pyunifiprotect.data import ( - Camera, - Event, - EventType, - ModelType, - SmartDetectObjectType, -) -from pyunifiprotect.exceptions import NvrError -from pyunifiprotect.utils import from_js_time +from uiprotect.data import Camera, Event, EventType, ModelType, SmartDetectObjectType +from uiprotect.exceptions import NvrError +from uiprotect.utils import from_js_time from yarl import URL from homeassistant.components.camera import CameraImageView diff --git a/homeassistant/components/unifiprotect/migrate.py b/homeassistant/components/unifiprotect/migrate.py index cfc8cff7618..a95341f497a 100644 --- a/homeassistant/components/unifiprotect/migrate.py +++ b/homeassistant/components/unifiprotect/migrate.py @@ -6,8 +6,8 @@ from itertools import chain import logging from typing import TypedDict -from pyunifiprotect import ProtectApiClient -from pyunifiprotect.data import Bootstrap +from uiprotect import ProtectApiClient +from uiprotect.data import Bootstrap from homeassistant.components.automation import automations_with_entity from homeassistant.components.script import scripts_with_entity diff --git a/homeassistant/components/unifiprotect/models.py b/homeassistant/components/unifiprotect/models.py index a9c79556135..d2ab31d672d 100644 --- a/homeassistant/components/unifiprotect/models.py +++ b/homeassistant/components/unifiprotect/models.py @@ -8,7 +8,7 @@ from enum import Enum import logging from typing import TYPE_CHECKING, Any, Generic, TypeVar -from pyunifiprotect.data import NVR, Event, ProtectAdoptableDeviceModel +from uiprotect.data import NVR, Event, ProtectAdoptableDeviceModel from homeassistant.helpers.entity import EntityDescription diff --git a/homeassistant/components/unifiprotect/number.py b/homeassistant/components/unifiprotect/number.py index 49c629ac42f..ceb8614e77e 100644 --- a/homeassistant/components/unifiprotect/number.py +++ b/homeassistant/components/unifiprotect/number.py @@ -7,7 +7,7 @@ from datetime import timedelta import logging from typing import Any -from pyunifiprotect.data import ( +from uiprotect.data import ( Camera, Doorlock, Light, diff --git a/homeassistant/components/unifiprotect/repairs.py b/homeassistant/components/unifiprotect/repairs.py index baf08c9b5cf..3cc8967ea0d 100644 --- a/homeassistant/components/unifiprotect/repairs.py +++ b/homeassistant/components/unifiprotect/repairs.py @@ -4,9 +4,9 @@ from __future__ import annotations from typing import cast -from pyunifiprotect import ProtectApiClient -from pyunifiprotect.data import Bootstrap, Camera, ModelType -from pyunifiprotect.data.types import FirmwareReleaseChannel +from uiprotect import ProtectApiClient +from uiprotect.data import Bootstrap, Camera, ModelType +from uiprotect.data.types import FirmwareReleaseChannel import voluptuous as vol from homeassistant import data_entry_flow diff --git a/homeassistant/components/unifiprotect/select.py b/homeassistant/components/unifiprotect/select.py index 6ba90948fca..f4a9d58e346 100644 --- a/homeassistant/components/unifiprotect/select.py +++ b/homeassistant/components/unifiprotect/select.py @@ -8,8 +8,8 @@ from enum import Enum import logging from typing import Any, Final -from pyunifiprotect.api import ProtectApiClient -from pyunifiprotect.data import ( +from uiprotect.api import ProtectApiClient +from uiprotect.data import ( Camera, ChimeType, DoorbellMessageType, diff --git a/homeassistant/components/unifiprotect/sensor.py b/homeassistant/components/unifiprotect/sensor.py index 63c9e11c660..00849c095f0 100644 --- a/homeassistant/components/unifiprotect/sensor.py +++ b/homeassistant/components/unifiprotect/sensor.py @@ -7,7 +7,7 @@ from datetime import datetime import logging from typing import Any, cast -from pyunifiprotect.data import ( +from uiprotect.data import ( NVR, Camera, Light, diff --git a/homeassistant/components/unifiprotect/services.py b/homeassistant/components/unifiprotect/services.py index 8c62664f55b..c5c2ffc8bfe 100644 --- a/homeassistant/components/unifiprotect/services.py +++ b/homeassistant/components/unifiprotect/services.py @@ -7,9 +7,9 @@ import functools from typing import Any, cast from pydantic import ValidationError -from pyunifiprotect.api import ProtectApiClient -from pyunifiprotect.data import Camera, Chime -from pyunifiprotect.exceptions import ClientError +from uiprotect.api import ProtectApiClient +from uiprotect.data import Camera, Chime +from uiprotect.exceptions import ClientError import voluptuous as vol from homeassistant.components.binary_sensor import BinarySensorDeviceClass diff --git a/homeassistant/components/unifiprotect/switch.py b/homeassistant/components/unifiprotect/switch.py index bd7cfa4d2a2..d17b208de12 100644 --- a/homeassistant/components/unifiprotect/switch.py +++ b/homeassistant/components/unifiprotect/switch.py @@ -6,7 +6,7 @@ from dataclasses import dataclass import logging from typing import Any -from pyunifiprotect.data import ( +from uiprotect.data import ( NVR, Camera, ProtectAdoptableDeviceModel, diff --git a/homeassistant/components/unifiprotect/text.py b/homeassistant/components/unifiprotect/text.py index 584bd511ee5..05e6712fa65 100644 --- a/homeassistant/components/unifiprotect/text.py +++ b/homeassistant/components/unifiprotect/text.py @@ -5,7 +5,7 @@ from __future__ import annotations from dataclasses import dataclass from typing import Any -from pyunifiprotect.data import ( +from uiprotect.data import ( Camera, DoorbellMessageType, ProtectAdoptableDeviceModel, diff --git a/homeassistant/components/unifiprotect/utils.py b/homeassistant/components/unifiprotect/utils.py index 4f422a846a3..5a0809ef9ac 100644 --- a/homeassistant/components/unifiprotect/utils.py +++ b/homeassistant/components/unifiprotect/utils.py @@ -10,8 +10,9 @@ import socket from typing import Any from aiohttp import CookieJar -from pyunifiprotect import ProtectApiClient -from pyunifiprotect.data import ( +from typing_extensions import Generator +from uiprotect import ProtectApiClient +from uiprotect.data import ( Bootstrap, CameraChannel, Light, @@ -19,7 +20,6 @@ from pyunifiprotect.data import ( LightModeType, ProtectAdoptableDeviceModel, ) -from typing_extensions import Generator from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( diff --git a/homeassistant/components/unifiprotect/views.py b/homeassistant/components/unifiprotect/views.py index 0f9bff63689..b359fd5d948 100644 --- a/homeassistant/components/unifiprotect/views.py +++ b/homeassistant/components/unifiprotect/views.py @@ -9,8 +9,8 @@ from typing import Any from urllib.parse import urlencode from aiohttp import web -from pyunifiprotect.data import Camera, Event -from pyunifiprotect.exceptions import ClientError +from uiprotect.data import Camera, Event +from uiprotect.exceptions import ClientError from homeassistant.components.http import HomeAssistantView from homeassistant.core import HomeAssistant, callback diff --git a/requirements_all.txt b/requirements_all.txt index bf50af47ad3..a295bc816f5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2363,9 +2363,6 @@ pytrydan==0.7.0 # homeassistant.components.usb pyudev==0.24.1 -# homeassistant.components.unifiprotect -pyunifiprotect==5.1.2 - # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 @@ -2787,6 +2784,9 @@ twitchAPI==4.0.0 # homeassistant.components.ukraine_alarm uasiren==0.0.1 +# homeassistant.components.unifiprotect +uiprotect==0.3.9 + # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 41a3cf07b5c..df739ab4b26 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1848,9 +1848,6 @@ pytrydan==0.7.0 # homeassistant.components.usb pyudev==0.24.1 -# homeassistant.components.unifiprotect -pyunifiprotect==5.1.2 - # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 @@ -2167,6 +2164,9 @@ twitchAPI==4.0.0 # homeassistant.components.ukraine_alarm uasiren==0.0.1 +# homeassistant.components.unifiprotect +uiprotect==0.3.9 + # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 diff --git a/tests/components/unifiprotect/conftest.py b/tests/components/unifiprotect/conftest.py index 5b3f9653d75..9eb1ea312c6 100644 --- a/tests/components/unifiprotect/conftest.py +++ b/tests/components/unifiprotect/conftest.py @@ -13,8 +13,8 @@ from typing import Any from unittest.mock import AsyncMock, Mock, patch import pytest -from pyunifiprotect import ProtectApiClient -from pyunifiprotect.data import ( +from uiprotect import ProtectApiClient +from uiprotect.data import ( NVR, Bootstrap, Camera, diff --git a/tests/components/unifiprotect/test_binary_sensor.py b/tests/components/unifiprotect/test_binary_sensor.py index 81ed02869b8..dbe8f72b244 100644 --- a/tests/components/unifiprotect/test_binary_sensor.py +++ b/tests/components/unifiprotect/test_binary_sensor.py @@ -5,8 +5,8 @@ from __future__ import annotations from datetime import datetime, timedelta from unittest.mock import Mock -from pyunifiprotect.data import Camera, Event, EventType, Light, MountType, Sensor -from pyunifiprotect.data.nvr import EventMetadata +from uiprotect.data import Camera, Event, EventType, Light, MountType, Sensor +from uiprotect.data.nvr import EventMetadata from homeassistant.components.binary_sensor import BinarySensorDeviceClass from homeassistant.components.unifiprotect.binary_sensor import ( diff --git a/tests/components/unifiprotect/test_button.py b/tests/components/unifiprotect/test_button.py index a38a29b5999..3a283093179 100644 --- a/tests/components/unifiprotect/test_button.py +++ b/tests/components/unifiprotect/test_button.py @@ -4,7 +4,7 @@ from __future__ import annotations from unittest.mock import AsyncMock, Mock -from pyunifiprotect.data.devices import Camera, Chime, Doorlock +from uiprotect.data.devices import Camera, Chime, Doorlock from homeassistant.components.unifiprotect.const import DEFAULT_ATTRIBUTION from homeassistant.const import ATTR_ATTRIBUTION, ATTR_ENTITY_ID, Platform diff --git a/tests/components/unifiprotect/test_camera.py b/tests/components/unifiprotect/test_camera.py index d374f61c2b0..444898fbd85 100644 --- a/tests/components/unifiprotect/test_camera.py +++ b/tests/components/unifiprotect/test_camera.py @@ -4,8 +4,8 @@ from __future__ import annotations from unittest.mock import AsyncMock, Mock -from pyunifiprotect.data import Camera as ProtectCamera, CameraChannel, StateType -from pyunifiprotect.exceptions import NvrError +from uiprotect.data import Camera as ProtectCamera, CameraChannel, StateType +from uiprotect.exceptions import NvrError from homeassistant.components.camera import ( CameraEntityFeature, diff --git a/tests/components/unifiprotect/test_config_flow.py b/tests/components/unifiprotect/test_config_flow.py index 845766809b2..5d02e1cf098 100644 --- a/tests/components/unifiprotect/test_config_flow.py +++ b/tests/components/unifiprotect/test_config_flow.py @@ -7,8 +7,8 @@ import socket from unittest.mock import patch import pytest -from pyunifiprotect import NotAuthorized, NvrError, ProtectApiClient -from pyunifiprotect.data import NVR, Bootstrap, CloudAccount +from uiprotect import NotAuthorized, NvrError, ProtectApiClient +from uiprotect.data import NVR, Bootstrap, CloudAccount from homeassistant import config_entries from homeassistant.components import dhcp, ssdp diff --git a/tests/components/unifiprotect/test_diagnostics.py b/tests/components/unifiprotect/test_diagnostics.py index b13c069b37c..fd882929e96 100644 --- a/tests/components/unifiprotect/test_diagnostics.py +++ b/tests/components/unifiprotect/test_diagnostics.py @@ -1,6 +1,6 @@ """Test UniFi Protect diagnostics.""" -from pyunifiprotect.data import NVR, Light +from uiprotect.data import NVR, Light from homeassistant.components.unifiprotect.const import CONF_ALLOW_EA from homeassistant.core import HomeAssistant diff --git a/tests/components/unifiprotect/test_init.py b/tests/components/unifiprotect/test_init.py index 9bb2141631b..3b75afaace8 100644 --- a/tests/components/unifiprotect/test_init.py +++ b/tests/components/unifiprotect/test_init.py @@ -4,8 +4,8 @@ from __future__ import annotations from unittest.mock import AsyncMock, patch -from pyunifiprotect import NotAuthorized, NvrError, ProtectApiClient -from pyunifiprotect.data import NVR, Bootstrap, CloudAccount, Light +from uiprotect import NotAuthorized, NvrError, ProtectApiClient +from uiprotect.data import NVR, Bootstrap, CloudAccount, Light from homeassistant.components.unifiprotect.const import ( AUTH_RETRIES, diff --git a/tests/components/unifiprotect/test_light.py b/tests/components/unifiprotect/test_light.py index 57867a3c7e9..bb0b6992e4e 100644 --- a/tests/components/unifiprotect/test_light.py +++ b/tests/components/unifiprotect/test_light.py @@ -4,8 +4,8 @@ from __future__ import annotations from unittest.mock import AsyncMock, Mock -from pyunifiprotect.data import Light -from pyunifiprotect.data.types import LEDLevel +from uiprotect.data import Light +from uiprotect.data.types import LEDLevel from homeassistant.components.light import ATTR_BRIGHTNESS from homeassistant.components.unifiprotect.const import DEFAULT_ATTRIBUTION diff --git a/tests/components/unifiprotect/test_lock.py b/tests/components/unifiprotect/test_lock.py index 6785ea2a4f6..62a1cb9ff46 100644 --- a/tests/components/unifiprotect/test_lock.py +++ b/tests/components/unifiprotect/test_lock.py @@ -4,7 +4,7 @@ from __future__ import annotations from unittest.mock import AsyncMock, Mock -from pyunifiprotect.data import Doorlock, LockStatusType +from uiprotect.data import Doorlock, LockStatusType from homeassistant.components.unifiprotect.const import DEFAULT_ATTRIBUTION from homeassistant.const import ( diff --git a/tests/components/unifiprotect/test_media_player.py b/tests/components/unifiprotect/test_media_player.py index 1558d11fbbe..642a3a1e372 100644 --- a/tests/components/unifiprotect/test_media_player.py +++ b/tests/components/unifiprotect/test_media_player.py @@ -5,8 +5,8 @@ from __future__ import annotations from unittest.mock import AsyncMock, Mock, patch import pytest -from pyunifiprotect.data import Camera -from pyunifiprotect.exceptions import StreamError +from uiprotect.data import Camera +from uiprotect.exceptions import StreamError from homeassistant.components.media_player import ( ATTR_MEDIA_CONTENT_TYPE, diff --git a/tests/components/unifiprotect/test_media_source.py b/tests/components/unifiprotect/test_media_source.py index 7e51031128e..2cdebeafb04 100644 --- a/tests/components/unifiprotect/test_media_source.py +++ b/tests/components/unifiprotect/test_media_source.py @@ -5,7 +5,7 @@ from ipaddress import IPv4Address from unittest.mock import AsyncMock, Mock, patch import pytest -from pyunifiprotect.data import ( +from uiprotect.data import ( Bootstrap, Camera, Event, @@ -13,7 +13,7 @@ from pyunifiprotect.data import ( Permission, SmartDetectObjectType, ) -from pyunifiprotect.exceptions import NvrError +from uiprotect.exceptions import NvrError from homeassistant.components.media_player import BrowseError, MediaClass from homeassistant.components.media_source import MediaSourceItem diff --git a/tests/components/unifiprotect/test_migrate.py b/tests/components/unifiprotect/test_migrate.py index 8fdf113f9db..4e1bf8bd418 100644 --- a/tests/components/unifiprotect/test_migrate.py +++ b/tests/components/unifiprotect/test_migrate.py @@ -4,7 +4,7 @@ from __future__ import annotations from unittest.mock import patch -from pyunifiprotect.data import Camera +from uiprotect.data import Camera from homeassistant.components.automation import DOMAIN as AUTOMATION_DOMAIN from homeassistant.components.repairs.issue_handler import ( diff --git a/tests/components/unifiprotect/test_number.py b/tests/components/unifiprotect/test_number.py index 3050992457c..77a409551b1 100644 --- a/tests/components/unifiprotect/test_number.py +++ b/tests/components/unifiprotect/test_number.py @@ -6,7 +6,7 @@ from datetime import timedelta from unittest.mock import AsyncMock, Mock import pytest -from pyunifiprotect.data import Camera, Doorlock, IRLEDMode, Light +from uiprotect.data import Camera, Doorlock, IRLEDMode, Light from homeassistant.components.unifiprotect.const import DEFAULT_ATTRIBUTION from homeassistant.components.unifiprotect.number import ( diff --git a/tests/components/unifiprotect/test_recorder.py b/tests/components/unifiprotect/test_recorder.py index 3e1a8599ea7..94c93413de5 100644 --- a/tests/components/unifiprotect/test_recorder.py +++ b/tests/components/unifiprotect/test_recorder.py @@ -5,7 +5,7 @@ from __future__ import annotations from datetime import datetime, timedelta from unittest.mock import Mock -from pyunifiprotect.data import Camera, Event, EventType +from uiprotect.data import Camera, Event, EventType from homeassistant.components.recorder import Recorder from homeassistant.components.recorder.history import get_significant_states diff --git a/tests/components/unifiprotect/test_repairs.py b/tests/components/unifiprotect/test_repairs.py index f4be3164fd5..7d76550f7c7 100644 --- a/tests/components/unifiprotect/test_repairs.py +++ b/tests/components/unifiprotect/test_repairs.py @@ -6,7 +6,7 @@ from copy import copy, deepcopy from http import HTTPStatus from unittest.mock import AsyncMock, Mock -from pyunifiprotect.data import Camera, CloudAccount, ModelType, Version +from uiprotect.data import Camera, CloudAccount, ModelType, Version from homeassistant.components.repairs.issue_handler import ( async_process_repairs_platforms, diff --git a/tests/components/unifiprotect/test_select.py b/tests/components/unifiprotect/test_select.py index 4ac82f45173..8795af57214 100644 --- a/tests/components/unifiprotect/test_select.py +++ b/tests/components/unifiprotect/test_select.py @@ -5,7 +5,7 @@ from __future__ import annotations from copy import copy from unittest.mock import AsyncMock, Mock -from pyunifiprotect.data import ( +from uiprotect.data import ( Camera, DoorbellMessageType, IRLEDMode, @@ -17,7 +17,7 @@ from pyunifiprotect.data import ( RecordingMode, Viewer, ) -from pyunifiprotect.data.nvr import DoorbellMessage +from uiprotect.data.nvr import DoorbellMessage from homeassistant.components.select import ATTR_OPTIONS from homeassistant.components.unifiprotect.const import DEFAULT_ATTRIBUTION diff --git a/tests/components/unifiprotect/test_sensor.py b/tests/components/unifiprotect/test_sensor.py index 5e70238519d..1ba3641ba36 100644 --- a/tests/components/unifiprotect/test_sensor.py +++ b/tests/components/unifiprotect/test_sensor.py @@ -6,15 +6,8 @@ from datetime import datetime, timedelta from unittest.mock import Mock import pytest -from pyunifiprotect.data import ( - NVR, - Camera, - Event, - EventType, - Sensor, - SmartDetectObjectType, -) -from pyunifiprotect.data.nvr import EventMetadata, LicensePlateMetadata +from uiprotect.data import NVR, Camera, Event, EventType, Sensor, SmartDetectObjectType +from uiprotect.data.nvr import EventMetadata, LicensePlateMetadata from homeassistant.components.unifiprotect.const import DEFAULT_ATTRIBUTION from homeassistant.components.unifiprotect.sensor import ( diff --git a/tests/components/unifiprotect/test_services.py b/tests/components/unifiprotect/test_services.py index 98decab9e4a..919af53ef10 100644 --- a/tests/components/unifiprotect/test_services.py +++ b/tests/components/unifiprotect/test_services.py @@ -5,9 +5,9 @@ from __future__ import annotations from unittest.mock import AsyncMock, Mock import pytest -from pyunifiprotect.data import Camera, Chime, Color, Light, ModelType -from pyunifiprotect.data.devices import CameraZone -from pyunifiprotect.exceptions import BadRequest +from uiprotect.data import Camera, Chime, Color, Light, ModelType +from uiprotect.data.devices import CameraZone +from uiprotect.exceptions import BadRequest from homeassistant.components.unifiprotect.const import ATTR_MESSAGE, DOMAIN from homeassistant.components.unifiprotect.services import ( diff --git a/tests/components/unifiprotect/test_switch.py b/tests/components/unifiprotect/test_switch.py index e421937632c..16e471c2e7a 100644 --- a/tests/components/unifiprotect/test_switch.py +++ b/tests/components/unifiprotect/test_switch.py @@ -5,7 +5,7 @@ from __future__ import annotations from unittest.mock import AsyncMock, Mock import pytest -from pyunifiprotect.data import Camera, Light, Permission, RecordingMode, VideoMode +from uiprotect.data import Camera, Light, Permission, RecordingMode, VideoMode from homeassistant.components.unifiprotect.const import DEFAULT_ATTRIBUTION from homeassistant.components.unifiprotect.switch import ( diff --git a/tests/components/unifiprotect/test_text.py b/tests/components/unifiprotect/test_text.py index be2ae93203a..3ca11744abb 100644 --- a/tests/components/unifiprotect/test_text.py +++ b/tests/components/unifiprotect/test_text.py @@ -4,7 +4,7 @@ from __future__ import annotations from unittest.mock import AsyncMock, Mock -from pyunifiprotect.data import Camera, DoorbellMessageType, LCDMessage +from uiprotect.data import Camera, DoorbellMessageType, LCDMessage from homeassistant.components.unifiprotect.const import DEFAULT_ATTRIBUTION from homeassistant.components.unifiprotect.text import CAMERA diff --git a/tests/components/unifiprotect/test_views.py b/tests/components/unifiprotect/test_views.py index f7930e5ff9a..6d190eb4dd6 100644 --- a/tests/components/unifiprotect/test_views.py +++ b/tests/components/unifiprotect/test_views.py @@ -6,8 +6,8 @@ from unittest.mock import AsyncMock, Mock from aiohttp import ClientResponse import pytest -from pyunifiprotect.data import Camera, Event, EventType -from pyunifiprotect.exceptions import ClientError +from uiprotect.data import Camera, Event, EventType +from uiprotect.exceptions import ClientError from homeassistant.components.unifiprotect.views import ( async_generate_event_video_url, diff --git a/tests/components/unifiprotect/utils.py b/tests/components/unifiprotect/utils.py index 1ade39dafca..ab3aefaa09d 100644 --- a/tests/components/unifiprotect/utils.py +++ b/tests/components/unifiprotect/utils.py @@ -8,8 +8,8 @@ from datetime import timedelta from typing import Any from unittest.mock import Mock -from pyunifiprotect import ProtectApiClient -from pyunifiprotect.data import ( +from uiprotect import ProtectApiClient +from uiprotect.data import ( Bootstrap, Camera, Event, @@ -18,8 +18,8 @@ from pyunifiprotect.data import ( ProtectAdoptableDeviceModel, WSSubscriptionMessage, ) -from pyunifiprotect.data.bootstrap import ProtectDeviceRef -from pyunifiprotect.test_util.anonymize import random_hex +from uiprotect.data.bootstrap import ProtectDeviceRef +from uiprotect.test_util.anonymize import random_hex from homeassistant.const import Platform from homeassistant.core import HomeAssistant, split_entity_id From be22214a3340eae38663eb477a4c5d214a655baa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ab=C3=ADlio=20Costa?= Date: Mon, 10 Jun 2024 02:02:38 +0100 Subject: [PATCH 0431/1445] Fix wrong arg name in Idasen Desk config flow (#119247) --- homeassistant/components/idasen_desk/config_flow.py | 2 +- tests/components/idasen_desk/test_config_flow.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/idasen_desk/config_flow.py b/homeassistant/components/idasen_desk/config_flow.py index b7c14089656..782d4988a3c 100644 --- a/homeassistant/components/idasen_desk/config_flow.py +++ b/homeassistant/components/idasen_desk/config_flow.py @@ -64,7 +64,7 @@ class IdasenDeskConfigFlow(ConfigFlow, domain=DOMAIN): desk = Desk(None, monitor_height=False) try: - await desk.connect(discovery_info.device, auto_reconnect=False) + await desk.connect(discovery_info.device, retry=False) except AuthFailedError: errors["base"] = "auth_failed" except TimeoutError: diff --git a/tests/components/idasen_desk/test_config_flow.py b/tests/components/idasen_desk/test_config_flow.py index a861dc5f5e2..c27cdea58aa 100644 --- a/tests/components/idasen_desk/test_config_flow.py +++ b/tests/components/idasen_desk/test_config_flow.py @@ -305,4 +305,4 @@ async def test_bluetooth_step_success(hass: HomeAssistant) -> None: } assert result2["result"].unique_id == IDASEN_DISCOVERY_INFO.address assert len(mock_setup_entry.mock_calls) == 1 - desk_connect.assert_called_with(ANY, auto_reconnect=False) + desk_connect.assert_called_with(ANY, retry=False) From 4376e0931af46c5ebf6200593c1bd504c8a80b62 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Mon, 10 Jun 2024 07:46:07 +0200 Subject: [PATCH 0432/1445] Add boiler entity state translations for incomfort water_heater entities (#119211) --- .../components/incomfort/strings.json | 41 +++++++++++++++++++ .../components/incomfort/water_heater.py | 1 + .../snapshots/test_water_heater.ambr | 2 +- 3 files changed, 43 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/incomfort/strings.json b/homeassistant/components/incomfort/strings.json index d4c01e4d0ed..67a736d5408 100644 --- a/homeassistant/components/incomfort/strings.json +++ b/homeassistant/components/incomfort/strings.json @@ -63,6 +63,47 @@ "tap_temperature": { "name": "Tap temperature" } + }, + "water_heater": { + "boiler": { + "state": { + "unknown": "Unknown", + "opentherm": "OpenTherm", + "boiler_ext": "Boiler external", + "frost": "Frost", + "central_heating_rf": "Central heating rf", + "tapwater_int": "Tapwater internal", + "sensor_test": "Sensor test", + "central_heating": "Central heating", + "standby": "Standby", + "postrun_boyler": "Postrun boiler", + "service": "Service", + "tapwater": "Tapwater", + "postrun_ch": "Postrun central heating", + "boiler_int": "Boiler internal", + "buffer": "Buffer", + "sensor_fault_after_self_check_e0": "Sensor fault after self check", + "cv_temperature_too_high_e1": "Temperature too high", + "s1_and_s2_interchanged_e2": "S1 and S2 interchanged", + "no_flame_signal_e4": "No flame signal", + "poor_flame_signal_e5": "Poor flame signal", + "flame_detection_fault_e6": "Flame detection fault", + "incorrect_fan_speed_e8": "Incorrect fan speed", + "sensor_fault_s1_e10": "Sensor fault S1", + "sensor_fault_s1_e11": "[%key:component::incomfort::entity::water_heater::boiler::state::sensor_fault_s1_e10%]", + "sensor_fault_s1_e12": "[%key:component::incomfort::entity::water_heater::boiler::state::sensor_fault_s1_e10%]", + "sensor_fault_s1_e13": "[%key:component::incomfort::entity::water_heater::boiler::state::sensor_fault_s1_e10%]", + "sensor_fault_s1_e14": "[%key:component::incomfort::entity::water_heater::boiler::state::sensor_fault_s1_e10%]", + "sensor_fault_s2_e20": "Sensor fault S2", + "sensor_fault_s2_e21": "[%key:component::incomfort::entity::water_heater::boiler::state::sensor_fault_s2_e20%]", + "sensor_fault_s2_e22": "[%key:component::incomfort::entity::water_heater::boiler::state::sensor_fault_s2_e20%]", + "sensor_fault_s2_e23": "[%key:component::incomfort::entity::water_heater::boiler::state::sensor_fault_s2_e20%]", + "sensor_fault_s2_e24": "[%key:component::incomfort::entity::water_heater::boiler::state::sensor_fault_s2_e20%]", + "shortcut_outside_sensor_temperature_e27": "Shortcut outside sensor temperature", + "gas_valve_relay_faulty_e29": "Gas valve relay faulty", + "gas_valve_relay_faulty_e30": "[%key:component::incomfort::entity::water_heater::boiler::state::gas_valve_relay_faulty_e29%]" + } + } } } } diff --git a/homeassistant/components/incomfort/water_heater.py b/homeassistant/components/incomfort/water_heater.py index f652cc21c8f..2295ce514b3 100644 --- a/homeassistant/components/incomfort/water_heater.py +++ b/homeassistant/components/incomfort/water_heater.py @@ -39,6 +39,7 @@ class IncomfortWaterHeater(IncomfortBoilerEntity, WaterHeaterEntity): _attr_max_temp = 80.0 _attr_name = None _attr_temperature_unit = UnitOfTemperature.CELSIUS + _attr_translation_key = "boiler" def __init__( self, coordinator: InComfortDataCoordinator, heater: InComfortHeater diff --git a/tests/components/incomfort/snapshots/test_water_heater.ambr b/tests/components/incomfort/snapshots/test_water_heater.ambr index 7e277da99f1..4b6bd8e9751 100644 --- a/tests/components/incomfort/snapshots/test_water_heater.ambr +++ b/tests/components/incomfort/snapshots/test_water_heater.ambr @@ -30,7 +30,7 @@ 'platform': 'incomfort', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'boiler', 'unique_id': 'c0ffeec0ffee', 'unit_of_measurement': None, }) From 8a0cc55278b0f265822760557b2230c073cee14f Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Mon, 10 Jun 2024 07:47:16 +0200 Subject: [PATCH 0433/1445] Always provide a currentArmLevel in Google assistant (#119238) --- .../components/google_assistant/trait.py | 32 +++++++++++-------- .../components/google_assistant/test_trait.py | 5 ++- 2 files changed, 23 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index e39634a5dd6..3d1daea9810 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -1586,6 +1586,17 @@ class ArmDisArmTrait(_Trait): if features & required_feature != 0 ] + def _default_arm_state(self): + states = self._supported_states() + + if STATE_ALARM_TRIGGERED in states: + states.remove(STATE_ALARM_TRIGGERED) + + if len(states) != 1: + raise SmartHomeError(ERR_NOT_SUPPORTED, "ArmLevel missing") + + return states[0] + def sync_attributes(self): """Return ArmDisarm attributes for a sync request.""" response = {} @@ -1609,10 +1620,13 @@ class ArmDisArmTrait(_Trait): def query_attributes(self): """Return ArmDisarm query attributes.""" armed_state = self.state.attributes.get("next_state", self.state.state) - response = {"isArmed": armed_state in self.state_to_service} - if response["isArmed"]: - response.update({"currentArmLevel": armed_state}) - return response + + if armed_state in self.state_to_service: + return {"isArmed": True, "currentArmLevel": armed_state} + return { + "isArmed": False, + "currentArmLevel": self._default_arm_state(), + } async def execute(self, command, data, params, challenge): """Execute an ArmDisarm command.""" @@ -1620,15 +1634,7 @@ class ArmDisArmTrait(_Trait): # If no arm level given, we can only arm it if there is # only one supported arm type. We never default to triggered. if not (arm_level := params.get("armLevel")): - states = self._supported_states() - - if STATE_ALARM_TRIGGERED in states: - states.remove(STATE_ALARM_TRIGGERED) - - if len(states) != 1: - raise SmartHomeError(ERR_NOT_SUPPORTED, "ArmLevel missing") - - arm_level = states[0] + arm_level = self._default_arm_state() if self.state.state == arm_level: raise SmartHomeError(ERR_ALREADY_ARMED, "System is already armed") diff --git a/tests/components/google_assistant/test_trait.py b/tests/components/google_assistant/test_trait.py index 4d5f438831a..d91d12b7074 100644 --- a/tests/components/google_assistant/test_trait.py +++ b/tests/components/google_assistant/test_trait.py @@ -1931,7 +1931,10 @@ async def test_arm_disarm_disarm(hass: HomeAssistant) -> None: } } - assert trt.query_attributes() == {"isArmed": False} + assert trt.query_attributes() == { + "currentArmLevel": "armed_custom_bypass", + "isArmed": False, + } assert trt.can_execute(trait.COMMAND_ARMDISARM, {"arm": False}) From 159503b8d37fbc46a353287416a640ad07c2cced Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Mon, 10 Jun 2024 15:48:09 +1000 Subject: [PATCH 0434/1445] Add model to Teslemetry Wall Connectors (#119251) --- homeassistant/components/teslemetry/entity.py | 9 +++++++++ tests/components/teslemetry/fixtures/site_info.json | 6 ++++-- .../teslemetry/snapshots/test_diagnostics.ambr | 6 ++++-- tests/components/teslemetry/snapshots/test_init.ambr | 4 ++-- 4 files changed, 19 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/teslemetry/entity.py b/homeassistant/components/teslemetry/entity.py index 82b06918f7d..dd6e6e575c2 100644 --- a/homeassistant/components/teslemetry/entity.py +++ b/homeassistant/components/teslemetry/entity.py @@ -211,6 +211,14 @@ class TeslemetryWallConnectorEntity( """Initialize common aspects of a Teslemetry entity.""" self.din = din self._attr_unique_id = f"{data.id}-{din}-{key}" + + # Find the model from the info coordinator + model: str | None = None + for wc in data.info_coordinator.data.get("components_wall_connectors", []): + if wc["din"] == din: + model = wc.get("part_name") + break + self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, din)}, manufacturer="Tesla", @@ -218,6 +226,7 @@ class TeslemetryWallConnectorEntity( name="Wall Connector", via_device=(DOMAIN, str(data.id)), serial_number=din.split("-")[-1], + model=model, ) super().__init__(data.live_coordinator, data.api, key) diff --git a/tests/components/teslemetry/fixtures/site_info.json b/tests/components/teslemetry/fixtures/site_info.json index f581707ff14..60958bbabbb 100644 --- a/tests/components/teslemetry/fixtures/site_info.json +++ b/tests/components/teslemetry/fixtures/site_info.json @@ -82,12 +82,14 @@ "wall_connectors": [ { "device_id": "123abc", - "din": "abc123", + "din": "abd-123", + "part_name": "Gen 3 Wall Connector", "is_active": true }, { "device_id": "234bcd", - "din": "bcd234", + "din": "bcd-234", + "part_name": "Gen 3 Wall Connector", "is_active": true } ], diff --git a/tests/components/teslemetry/snapshots/test_diagnostics.ambr b/tests/components/teslemetry/snapshots/test_diagnostics.ambr index d13c4f48068..4a942daa508 100644 --- a/tests/components/teslemetry/snapshots/test_diagnostics.ambr +++ b/tests/components/teslemetry/snapshots/test_diagnostics.ambr @@ -76,13 +76,15 @@ 'components_wall_connectors': list([ dict({ 'device_id': '123abc', - 'din': 'abc123', + 'din': 'abd-123', 'is_active': True, + 'part_name': 'Gen 3 Wall Connector', }), dict({ 'device_id': '234bcd', - 'din': 'bcd234', + 'din': 'bcd-234', 'is_active': True, + 'part_name': 'Gen 3 Wall Connector', }), ]), 'components_wifi_commissioning_enabled': True, diff --git a/tests/components/teslemetry/snapshots/test_init.ambr b/tests/components/teslemetry/snapshots/test_init.ambr index 74c3ac011a5..434e9025ac7 100644 --- a/tests/components/teslemetry/snapshots/test_init.ambr +++ b/tests/components/teslemetry/snapshots/test_init.ambr @@ -80,7 +80,7 @@ 'labels': set({ }), 'manufacturer': 'Tesla', - 'model': None, + 'model': 'Gen 3 Wall Connector', 'name': 'Wall Connector', 'name_by_user': None, 'serial_number': '123', @@ -110,7 +110,7 @@ 'labels': set({ }), 'manufacturer': 'Tesla', - 'model': None, + 'model': 'Gen 3 Wall Connector', 'name': 'Wall Connector', 'name_by_user': None, 'serial_number': '234', From 8b5627b1be7066e89d81c51be97f10e7650a8399 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 10 Jun 2024 08:08:52 +0200 Subject: [PATCH 0435/1445] Temporary pin CI to Python 3.12.3 (#119261) --- .github/workflows/builder.yml | 2 +- .github/workflows/ci.yaml | 4 ++-- .github/workflows/translations.yml | 2 +- .github/workflows/wheels.yml | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml index 80c32d47c1c..58d9c5a5d28 100644 --- a/.github/workflows/builder.yml +++ b/.github/workflows/builder.yml @@ -10,7 +10,7 @@ on: env: BUILD_TYPE: core - DEFAULT_PYTHON: "3.12" + DEFAULT_PYTHON: "3.12.3" PIP_TIMEOUT: 60 UV_HTTP_TIMEOUT: 60 UV_SYSTEM_PYTHON: "true" diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index f2ffd03f1a8..fd4aaeed526 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -37,8 +37,8 @@ env: UV_CACHE_VERSION: 1 MYPY_CACHE_VERSION: 8 HA_SHORT_VERSION: "2024.7" - DEFAULT_PYTHON: "3.12" - ALL_PYTHON_VERSIONS: "['3.12']" + DEFAULT_PYTHON: "3.12.3" + ALL_PYTHON_VERSIONS: "['3.12.3']" # 10.3 is the oldest supported version # - 10.3.32 is the version currently shipped with Synology (as of 17 Feb 2022) # 10.6 is the current long-term-support diff --git a/.github/workflows/translations.yml b/.github/workflows/translations.yml index f487292e79a..92c4c845e7d 100644 --- a/.github/workflows/translations.yml +++ b/.github/workflows/translations.yml @@ -10,7 +10,7 @@ on: - "**strings.json" env: - DEFAULT_PYTHON: "3.11" + DEFAULT_PYTHON: "3.12.3" jobs: upload: diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index fc169619325..13f5177bd7e 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -17,7 +17,7 @@ on: - "script/gen_requirements_all.py" env: - DEFAULT_PYTHON: "3.12" + DEFAULT_PYTHON: "3.12.3" concurrency: group: ${{ github.workflow }}-${{ github.ref_name}} From 8b6fbd5b3f2d42f169edb321fbffc423bf614cf6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 10 Jun 2024 01:17:29 -0500 Subject: [PATCH 0436/1445] Fix climate on/off in nexia (#119254) --- homeassistant/components/nexia/climate.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/nexia/climate.py b/homeassistant/components/nexia/climate.py index 78c0bc88ef7..7d09f710828 100644 --- a/homeassistant/components/nexia/climate.py +++ b/homeassistant/components/nexia/climate.py @@ -388,12 +388,12 @@ class NexiaZone(NexiaThermostatZoneEntity, ClimateEntity): async def async_turn_off(self) -> None: """Turn off the zone.""" - await self.async_set_hvac_mode(OPERATION_MODE_OFF) + await self.async_set_hvac_mode(HVACMode.OFF) self._signal_zone_update() async def async_turn_on(self) -> None: """Turn on the zone.""" - await self.async_set_hvac_mode(OPERATION_MODE_AUTO) + await self.async_set_hvac_mode(HVACMode.AUTO) self._signal_zone_update() async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: From f04654588385d2c50ff65be7dcc33d25a5995ef7 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 9 Jun 2024 23:18:50 -0700 Subject: [PATCH 0437/1445] Fix nest to cancel event listener on config entry unload (#119257) --- homeassistant/components/nest/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/nest/__init__.py b/homeassistant/components/nest/__init__.py index 96231390119..bdec44a3c85 100644 --- a/homeassistant/components/nest/__init__.py +++ b/homeassistant/components/nest/__init__.py @@ -224,7 +224,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Close connection when hass stops.""" subscriber.stop_async() - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, on_hass_stop) + entry.async_on_unload( + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, on_hass_stop) + ) hass.data[DOMAIN][entry.entry_id] = { DATA_SUBSCRIBER: subscriber, From f6c6b3cf6c7922695fbd2fccd9674a3203df27b9 Mon Sep 17 00:00:00 2001 From: wittypluck Date: Mon, 10 Jun 2024 08:25:39 +0200 Subject: [PATCH 0438/1445] Fix Glances v4 network and container issues (glances-api 0.8.0) (#119226) --- homeassistant/components/glances/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/glances/manifest.json b/homeassistant/components/glances/manifest.json index 68101583b48..e129a375df2 100644 --- a/homeassistant/components/glances/manifest.json +++ b/homeassistant/components/glances/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/glances", "iot_class": "local_polling", "loggers": ["glances_api"], - "requirements": ["glances-api==0.7.0"] + "requirements": ["glances-api==0.8.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index a295bc816f5..aa9d807db63 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -958,7 +958,7 @@ gios==4.0.0 gitterpy==0.1.7 # homeassistant.components.glances -glances-api==0.7.0 +glances-api==0.8.0 # homeassistant.components.goalzero goalzero==0.2.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index df739ab4b26..2438c137861 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -787,7 +787,7 @@ getmac==0.9.4 gios==4.0.0 # homeassistant.components.glances -glances-api==0.7.0 +glances-api==0.8.0 # homeassistant.components.goalzero goalzero==0.2.2 From 42e2c2b3e96c1a4e9c89cf8c7da6f64329f991fc Mon Sep 17 00:00:00 2001 From: Kevin Stillhammer Date: Mon, 10 Jun 2024 08:26:24 +0200 Subject: [PATCH 0439/1445] google_travel_time: Merge user_input validation (#119221) --- .../google_travel_time/config_flow.py | 60 +++++++++---------- .../google_travel_time/test_config_flow.py | 24 ++++---- 2 files changed, 40 insertions(+), 44 deletions(-) diff --git a/homeassistant/components/google_travel_time/config_flow.py b/homeassistant/components/google_travel_time/config_flow.py index d8ba7643bc9..0b493d7eeeb 100644 --- a/homeassistant/components/google_travel_time/config_flow.py +++ b/homeassistant/components/google_travel_time/config_flow.py @@ -180,6 +180,28 @@ class GoogleOptionsFlow(OptionsFlow): ) +async def validate_input( + hass: HomeAssistant, user_input: dict[str, Any] +) -> dict[str, str] | None: + """Validate the user input allows us to connect.""" + try: + await hass.async_add_executor_job( + validate_config_entry, + hass, + user_input[CONF_API_KEY], + user_input[CONF_ORIGIN], + user_input[CONF_DESTINATION], + ) + except InvalidApiKeyException: + return {"base": "invalid_auth"} + except TimeoutError: + return {"base": "timeout_connect"} + except UnknownException: + return {"base": "cannot_connect"} + + return None + + class GoogleTravelTimeConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Google Maps Travel Time.""" @@ -195,24 +217,11 @@ class GoogleTravelTimeConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_user(self, user_input=None) -> ConfigFlowResult: """Handle the initial step.""" - errors = {} + errors: dict[str, str] | None = None user_input = user_input or {} if user_input: - try: - await self.hass.async_add_executor_job( - validate_config_entry, - self.hass, - user_input[CONF_API_KEY], - user_input[CONF_ORIGIN], - user_input[CONF_DESTINATION], - ) - except InvalidApiKeyException: - errors["base"] = "invalid_auth" - except TimeoutError: - errors["base"] = "timeout_connect" - except UnknownException: - errors["base"] = "cannot_connect" - else: + errors = await validate_input(self.hass, user_input) + if not errors: return self.async_create_entry( title=user_input.get(CONF_NAME, DEFAULT_NAME), data=user_input, @@ -233,24 +242,11 @@ class GoogleTravelTimeConfigFlow(ConfigFlow, domain=DOMAIN): if TYPE_CHECKING: assert entry - errors = {} + errors: dict[str, str] | None = None user_input = user_input or {} if user_input: - try: - await self.hass.async_add_executor_job( - validate_config_entry, - self.hass, - user_input[CONF_API_KEY], - user_input[CONF_ORIGIN], - user_input[CONF_DESTINATION], - ) - except InvalidApiKeyException: - errors["base"] = "invalid_auth" - except TimeoutError: - errors["base"] = "timeout_connect" - except UnknownException: - errors["base"] = "cannot_connect" - else: + errors = await validate_input(self.hass, user_input) + if not errors: return self.async_update_reload_and_abort( entry, data=user_input, diff --git a/tests/components/google_travel_time/test_config_flow.py b/tests/components/google_travel_time/test_config_flow.py index e9b383a0120..270b82272d8 100644 --- a/tests/components/google_travel_time/test_config_flow.py +++ b/tests/components/google_travel_time/test_config_flow.py @@ -88,7 +88,7 @@ async def test_minimum_fields(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] is FlowResultType.FORM - assert result["errors"] == {} + assert result["errors"] is None await assert_common_create_steps(hass, result) @@ -100,7 +100,7 @@ async def test_invalid_config_entry(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] is FlowResultType.FORM - assert result["errors"] == {} + assert result["errors"] is None result2 = await hass.config_entries.flow.async_configure( result["flow_id"], MOCK_CONFIG, @@ -118,7 +118,7 @@ async def test_invalid_api_key(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] is FlowResultType.FORM - assert result["errors"] == {} + assert result["errors"] is None result2 = await hass.config_entries.flow.async_configure( result["flow_id"], MOCK_CONFIG, @@ -136,7 +136,7 @@ async def test_transport_error(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] is FlowResultType.FORM - assert result["errors"] == {} + assert result["errors"] is None result2 = await hass.config_entries.flow.async_configure( result["flow_id"], MOCK_CONFIG, @@ -154,7 +154,7 @@ async def test_timeout(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] is FlowResultType.FORM - assert result["errors"] == {} + assert result["errors"] is None result2 = await hass.config_entries.flow.async_configure( result["flow_id"], MOCK_CONFIG, @@ -171,7 +171,7 @@ async def test_malformed_api_key(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] is FlowResultType.FORM - assert result["errors"] == {} + assert result["errors"] is None result2 = await hass.config_entries.flow.async_configure( result["flow_id"], MOCK_CONFIG, @@ -234,7 +234,7 @@ async def test_reconfigure_invalid_config_entry( }, ) assert result["type"] is FlowResultType.FORM - assert result["errors"] == {} + assert result["errors"] is None result2 = await hass.config_entries.flow.async_configure( result["flow_id"], RECONFIGURE_CONFIG, @@ -269,7 +269,7 @@ async def test_reconfigure_invalid_api_key(hass: HomeAssistant, mock_config) -> }, ) assert result["type"] is FlowResultType.FORM - assert result["errors"] == {} + assert result["errors"] is None result2 = await hass.config_entries.flow.async_configure( result["flow_id"], RECONFIGURE_CONFIG, @@ -303,7 +303,7 @@ async def test_reconfigure_transport_error(hass: HomeAssistant, mock_config) -> }, ) assert result["type"] is FlowResultType.FORM - assert result["errors"] == {} + assert result["errors"] is None result2 = await hass.config_entries.flow.async_configure( result["flow_id"], RECONFIGURE_CONFIG, @@ -337,7 +337,7 @@ async def test_reconfigure_timeout(hass: HomeAssistant, mock_config) -> None: }, ) assert result["type"] is FlowResultType.FORM - assert result["errors"] == {} + assert result["errors"] is None result2 = await hass.config_entries.flow.async_configure( result["flow_id"], RECONFIGURE_CONFIG, @@ -615,7 +615,7 @@ async def test_dupe(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] is FlowResultType.FORM - assert result["errors"] == {} + assert result["errors"] is None result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -633,7 +633,7 @@ async def test_dupe(hass: HomeAssistant) -> None: ) assert result["type"] is FlowResultType.FORM - assert result["errors"] == {} + assert result["errors"] is None result2 = await hass.config_entries.flow.async_configure( result["flow_id"], From edc7c58bbae48d08ae316ca784bd3f44227ae850 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 9 Jun 2024 23:35:54 -0700 Subject: [PATCH 0440/1445] Bump google-nest-sdm to 4.0.5 (#119255) --- homeassistant/components/nest/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nest/manifest.json b/homeassistant/components/nest/manifest.json index 5a975bb19ec..d3ba571e65a 100644 --- a/homeassistant/components/nest/manifest.json +++ b/homeassistant/components/nest/manifest.json @@ -20,5 +20,5 @@ "iot_class": "cloud_push", "loggers": ["google_nest_sdm"], "quality_scale": "platinum", - "requirements": ["google-nest-sdm==4.0.4"] + "requirements": ["google-nest-sdm==4.0.5"] } diff --git a/requirements_all.txt b/requirements_all.txt index aa9d807db63..1d628f304d8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -980,7 +980,7 @@ google-cloud-texttospeech==2.12.3 google-generativeai==0.6.0 # homeassistant.components.nest -google-nest-sdm==4.0.4 +google-nest-sdm==4.0.5 # homeassistant.components.google_travel_time googlemaps==2.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2438c137861..edacda3e149 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -806,7 +806,7 @@ google-cloud-pubsub==2.13.11 google-generativeai==0.6.0 # homeassistant.components.nest -google-nest-sdm==4.0.4 +google-nest-sdm==4.0.5 # homeassistant.components.google_travel_time googlemaps==2.5.1 From d4baad62ef119c1927fa02039829295f9c1f7fca Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 10 Jun 2024 01:36:36 -0500 Subject: [PATCH 0441/1445] Bump uiprotect to 0.4.0 (#119256) --- homeassistant/components/unifiprotect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index 9cb6ceb7cb9..ba6319ab0ba 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -40,7 +40,7 @@ "integration_type": "hub", "iot_class": "local_push", "loggers": ["uiprotect", "unifi_discovery"], - "requirements": ["uiprotect==0.3.9", "unifi-discovery==1.1.8"], + "requirements": ["uiprotect==0.4.0", "unifi-discovery==1.1.8"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/requirements_all.txt b/requirements_all.txt index 1d628f304d8..ebc8c35df35 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2785,7 +2785,7 @@ twitchAPI==4.0.0 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==0.3.9 +uiprotect==0.4.0 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index edacda3e149..7410f83f04f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2165,7 +2165,7 @@ twitchAPI==4.0.0 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==0.3.9 +uiprotect==0.4.0 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 From 733e563500a61b839923af099e3fba9e6afd7fb5 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 10 Jun 2024 08:39:24 +0200 Subject: [PATCH 0442/1445] Improve type hints in blackbird tests (#119262) --- .../components/blackbird/test_media_player.py | 60 +++++++++++++------ 1 file changed, 43 insertions(+), 17 deletions(-) diff --git a/tests/components/blackbird/test_media_player.py b/tests/components/blackbird/test_media_player.py index 3b0465ef208..ec5a37f72ad 100644 --- a/tests/components/blackbird/test_media_player.py +++ b/tests/components/blackbird/test_media_player.py @@ -12,7 +12,10 @@ from homeassistant.components.blackbird.media_player import ( PLATFORM_SCHEMA, setup_platform, ) -from homeassistant.components.media_player import MediaPlayerEntityFeature +from homeassistant.components.media_player import ( + MediaPlayerEntity, + MediaPlayerEntityFeature, +) from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant @@ -166,13 +169,13 @@ def test_invalid_schemas() -> None: @pytest.fixture -def mock_blackbird(): +def mock_blackbird() -> MockBlackbird: """Return a mock blackbird instance.""" return MockBlackbird() @pytest.fixture -async def setup_blackbird(hass, mock_blackbird): +async def setup_blackbird(hass: HomeAssistant, mock_blackbird: MockBlackbird) -> None: """Set up blackbird.""" with mock.patch( "homeassistant.components.blackbird.media_player.get_blackbird", @@ -198,7 +201,9 @@ async def setup_blackbird(hass, mock_blackbird): @pytest.fixture -def media_player_entity(hass, setup_blackbird): +def media_player_entity( + hass: HomeAssistant, setup_blackbird: None +) -> MediaPlayerEntity: """Return the media player entity.""" media_player = hass.data[DATA_BLACKBIRD]["/dev/ttyUSB0-3"] media_player.hass = hass @@ -206,7 +211,8 @@ def media_player_entity(hass, setup_blackbird): return media_player -async def test_setup_platform(hass: HomeAssistant, setup_blackbird) -> None: +@pytest.mark.usefixtures("setup_blackbird") +async def test_setup_platform(hass: HomeAssistant) -> None: """Test setting up platform.""" # One service must be registered assert hass.services.has_service(DOMAIN, SERVICE_SETALLZONES) @@ -215,7 +221,9 @@ async def test_setup_platform(hass: HomeAssistant, setup_blackbird) -> None: async def test_setallzones_service_call_with_entity_id( - hass: HomeAssistant, media_player_entity, mock_blackbird + hass: HomeAssistant, + media_player_entity: MediaPlayerEntity, + mock_blackbird: MockBlackbird, ) -> None: """Test set all zone source service call with entity id.""" await hass.async_add_executor_job(media_player_entity.update) @@ -238,7 +246,9 @@ async def test_setallzones_service_call_with_entity_id( async def test_setallzones_service_call_without_entity_id( - mock_blackbird, hass: HomeAssistant, media_player_entity + mock_blackbird: MockBlackbird, + hass: HomeAssistant, + media_player_entity: MediaPlayerEntity, ) -> None: """Test set all zone source service call without entity id.""" await hass.async_add_executor_job(media_player_entity.update) @@ -257,7 +267,9 @@ async def test_setallzones_service_call_without_entity_id( assert media_player_entity.source == "three" -async def test_update(hass: HomeAssistant, media_player_entity) -> None: +async def test_update( + hass: HomeAssistant, media_player_entity: MediaPlayerEntity +) -> None: """Test updating values from blackbird.""" assert media_player_entity.state is None assert media_player_entity.source is None @@ -268,12 +280,16 @@ async def test_update(hass: HomeAssistant, media_player_entity) -> None: assert media_player_entity.source == "one" -async def test_name(media_player_entity) -> None: +async def test_name(media_player_entity: MediaPlayerEntity) -> None: """Test name property.""" assert media_player_entity.name == "Zone name" -async def test_state(hass: HomeAssistant, media_player_entity, mock_blackbird) -> None: +async def test_state( + hass: HomeAssistant, + media_player_entity: MediaPlayerEntity, + mock_blackbird: MockBlackbird, +) -> None: """Test state property.""" assert media_player_entity.state is None @@ -285,7 +301,7 @@ async def test_state(hass: HomeAssistant, media_player_entity, mock_blackbird) - assert media_player_entity.state == STATE_OFF -async def test_supported_features(media_player_entity) -> None: +async def test_supported_features(media_player_entity: MediaPlayerEntity) -> None: """Test supported features property.""" assert ( media_player_entity.supported_features @@ -295,28 +311,34 @@ async def test_supported_features(media_player_entity) -> None: ) -async def test_source(hass: HomeAssistant, media_player_entity) -> None: +async def test_source( + hass: HomeAssistant, media_player_entity: MediaPlayerEntity +) -> None: """Test source property.""" assert media_player_entity.source is None await hass.async_add_executor_job(media_player_entity.update) assert media_player_entity.source == "one" -async def test_media_title(hass: HomeAssistant, media_player_entity) -> None: +async def test_media_title( + hass: HomeAssistant, media_player_entity: MediaPlayerEntity +) -> None: """Test media title property.""" assert media_player_entity.media_title is None await hass.async_add_executor_job(media_player_entity.update) assert media_player_entity.media_title == "one" -async def test_source_list(media_player_entity) -> None: +async def test_source_list(media_player_entity: MediaPlayerEntity) -> None: """Test source list property.""" # Note, the list is sorted! assert media_player_entity.source_list == ["one", "two", "three"] async def test_select_source( - hass: HomeAssistant, media_player_entity, mock_blackbird + hass: HomeAssistant, + media_player_entity: MediaPlayerEntity, + mock_blackbird: MockBlackbird, ) -> None: """Test source selection methods.""" await hass.async_add_executor_job(media_player_entity.update) @@ -336,7 +358,9 @@ async def test_select_source( async def test_turn_on( - hass: HomeAssistant, media_player_entity, mock_blackbird + hass: HomeAssistant, + media_player_entity: MediaPlayerEntity, + mock_blackbird: MockBlackbird, ) -> None: """Testing turning on the zone.""" mock_blackbird.zones[3].power = False @@ -350,7 +374,9 @@ async def test_turn_on( async def test_turn_off( - hass: HomeAssistant, media_player_entity, mock_blackbird + hass: HomeAssistant, + media_player_entity: MediaPlayerEntity, + mock_blackbird: MockBlackbird, ) -> None: """Testing turning off the zone.""" mock_blackbird.zones[3].power = True From 731df892c6e8dd9c42f5defafdfc2bbbee7011dd Mon Sep 17 00:00:00 2001 From: Angel Nunez Mencias Date: Mon, 10 Jun 2024 08:41:22 +0200 Subject: [PATCH 0443/1445] Fixes crashes when receiving malformed decoded payloads (#119216) Co-authored-by: Jan Bouwhuis --- homeassistant/components/thethingsnetwork/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/thethingsnetwork/manifest.json b/homeassistant/components/thethingsnetwork/manifest.json index b8b1dbd7e1d..bc132d171f2 100644 --- a/homeassistant/components/thethingsnetwork/manifest.json +++ b/homeassistant/components/thethingsnetwork/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/thethingsnetwork", "integration_type": "hub", "iot_class": "cloud_polling", - "requirements": ["ttn_client==0.0.4"] + "requirements": ["ttn_client==1.0.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index ebc8c35df35..cd382d9b1c7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2767,7 +2767,7 @@ transmission-rpc==7.0.3 ttls==1.5.1 # homeassistant.components.thethingsnetwork -ttn_client==0.0.4 +ttn_client==1.0.0 # homeassistant.components.tuya tuya-device-sharing-sdk==0.1.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7410f83f04f..e84a8a345ed 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2147,7 +2147,7 @@ transmission-rpc==7.0.3 ttls==1.5.1 # homeassistant.components.thethingsnetwork -ttn_client==0.0.4 +ttn_client==1.0.0 # homeassistant.components.tuya tuya-device-sharing-sdk==0.1.9 From 0873322af7a892b524adea55054acd0eea6d2a8f Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Mon, 10 Jun 2024 07:47:08 +0100 Subject: [PATCH 0444/1445] Moves V2C from hass.data to config_entry.runtime_data (#119165) Co-authored-by: Paulus Schoutsen --- homeassistant/components/v2c/__init__.py | 13 ++++++------- homeassistant/components/v2c/binary_sensor.py | 7 +++---- homeassistant/components/v2c/diagnostics.py | 8 +++----- homeassistant/components/v2c/number.py | 7 +++---- homeassistant/components/v2c/sensor.py | 7 +++---- homeassistant/components/v2c/switch.py | 7 +++---- tests/components/v2c/conftest.py | 2 +- 7 files changed, 22 insertions(+), 29 deletions(-) diff --git a/homeassistant/components/v2c/__init__.py b/homeassistant/components/v2c/__init__.py index b80163742cb..0c07891df72 100644 --- a/homeassistant/components/v2c/__init__.py +++ b/homeassistant/components/v2c/__init__.py @@ -9,7 +9,6 @@ from homeassistant.const import CONF_HOST, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers.httpx_client import get_async_client -from .const import DOMAIN from .coordinator import V2CUpdateCoordinator PLATFORMS: list[Platform] = [ @@ -20,7 +19,10 @@ PLATFORMS: list[Platform] = [ ] -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +type V2CConfigEntry = ConfigEntry[V2CUpdateCoordinator] + + +async def async_setup_entry(hass: HomeAssistant, entry: V2CConfigEntry) -> bool: """Set up V2C from a config entry.""" host = entry.data[CONF_HOST] @@ -29,7 +31,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_config_entry_first_refresh() - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator + entry.runtime_data = coordinator if coordinator.data.ID and entry.unique_id != coordinator.data.ID: hass.config_entries.async_update_entry(entry, unique_id=coordinator.data.ID) @@ -41,7 +43,4 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): - hass.data[DOMAIN].pop(entry.entry_id) - - return unload_ok + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/v2c/binary_sensor.py b/homeassistant/components/v2c/binary_sensor.py index 203cc9f3396..28ad3665996 100644 --- a/homeassistant/components/v2c/binary_sensor.py +++ b/homeassistant/components/v2c/binary_sensor.py @@ -12,11 +12,10 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntity, BinarySensorEntityDescription, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN +from . import V2CConfigEntry from .coordinator import V2CUpdateCoordinator from .entity import V2CBaseEntity @@ -51,11 +50,11 @@ TRYDAN_SENSORS = ( async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: V2CConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up V2C binary sensor platform.""" - coordinator: V2CUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + coordinator = config_entry.runtime_data async_add_entities( V2CBinarySensorBaseEntity(coordinator, description, config_entry.entry_id) diff --git a/homeassistant/components/v2c/diagnostics.py b/homeassistant/components/v2c/diagnostics.py index 9f9df8723e0..289d585b164 100644 --- a/homeassistant/components/v2c/diagnostics.py +++ b/homeassistant/components/v2c/diagnostics.py @@ -5,21 +5,19 @@ from __future__ import annotations from typing import TYPE_CHECKING, Any from homeassistant.components.diagnostics import async_redact_data -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant -from .const import DOMAIN -from .coordinator import V2CUpdateCoordinator +from . import V2CConfigEntry TO_REDACT = {CONF_HOST, "title"} async def async_get_config_entry_diagnostics( - hass: HomeAssistant, entry: ConfigEntry + hass: HomeAssistant, entry: V2CConfigEntry ) -> dict[str, Any]: """Return diagnostics for a config entry.""" - coordinator: V2CUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + coordinator = entry.runtime_data if TYPE_CHECKING: assert coordinator.evse diff --git a/homeassistant/components/v2c/number.py b/homeassistant/components/v2c/number.py index 376509c4780..2ff70226132 100644 --- a/homeassistant/components/v2c/number.py +++ b/homeassistant/components/v2c/number.py @@ -13,11 +13,10 @@ from homeassistant.components.number import ( NumberEntity, NumberEntityDescription, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN +from . import V2CConfigEntry from .coordinator import V2CUpdateCoordinator from .entity import V2CBaseEntity @@ -48,11 +47,11 @@ TRYDAN_NUMBER_SETTINGS = ( async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: V2CConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up V2C Trydan number platform.""" - coordinator: V2CUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + coordinator = config_entry.runtime_data async_add_entities( V2CSettingsNumberEntity(coordinator, description, config_entry.entry_id) diff --git a/homeassistant/components/v2c/sensor.py b/homeassistant/components/v2c/sensor.py index 0c59993ac0e..fc0cc0bfaa8 100644 --- a/homeassistant/components/v2c/sensor.py +++ b/homeassistant/components/v2c/sensor.py @@ -15,13 +15,12 @@ from homeassistant.components.sensor import ( SensorEntityDescription, SensorStateClass, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import UnitOfEnergy, UnitOfPower, UnitOfTime from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType -from .const import DOMAIN +from . import V2CConfigEntry from .coordinator import V2CUpdateCoordinator from .entity import V2CBaseEntity @@ -106,11 +105,11 @@ TRYDAN_SENSORS = ( async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: V2CConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up V2C sensor platform.""" - coordinator: V2CUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + coordinator = config_entry.runtime_data async_add_entities( V2CSensorBaseEntity(coordinator, description, config_entry.entry_id) diff --git a/homeassistant/components/v2c/switch.py b/homeassistant/components/v2c/switch.py index 0974a712153..cd89e954275 100644 --- a/homeassistant/components/v2c/switch.py +++ b/homeassistant/components/v2c/switch.py @@ -17,11 +17,10 @@ from pytrydan.models.trydan import ( ) from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN +from . import V2CConfigEntry from .coordinator import V2CUpdateCoordinator from .entity import V2CBaseEntity @@ -80,11 +79,11 @@ TRYDAN_SWITCHES = ( async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: V2CConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up V2C switch platform.""" - coordinator: V2CUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + coordinator = config_entry.runtime_data async_add_entities( V2CSwitchEntity(coordinator, description, config_entry.entry_id) diff --git a/tests/components/v2c/conftest.py b/tests/components/v2c/conftest.py index 9cc3e4ed9e2..1803298be28 100644 --- a/tests/components/v2c/conftest.py +++ b/tests/components/v2c/conftest.py @@ -6,7 +6,7 @@ import pytest from pytrydan.models.trydan import TrydanData from typing_extensions import Generator -from homeassistant.components.v2c import DOMAIN +from homeassistant.components.v2c.const import DOMAIN from homeassistant.const import CONF_HOST from homeassistant.helpers.json import json_dumps From ea3097f84c2a0ff2b12eaf763e85c21f0aa21893 Mon Sep 17 00:00:00 2001 From: Austin Drummond Date: Mon, 10 Jun 2024 02:48:11 -0400 Subject: [PATCH 0445/1445] Fix control 4 on os 2 (#119104) --- homeassistant/components/control4/__init__.py | 7 ++++++- homeassistant/components/control4/media_player.py | 11 ++++++++--- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/control4/__init__.py b/homeassistant/components/control4/__init__.py index 86a13de1ac8..c9a6eab5c62 100644 --- a/homeassistant/components/control4/__init__.py +++ b/homeassistant/components/control4/__init__.py @@ -120,7 +120,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: director_all_items = json.loads(director_all_items) entry_data[CONF_DIRECTOR_ALL_ITEMS] = director_all_items - entry_data[CONF_UI_CONFIGURATION] = json.loads(await director.getUiConfiguration()) + # Check if OS version is 3 or higher to get UI configuration + entry_data[CONF_UI_CONFIGURATION] = None + if int(entry_data[CONF_DIRECTOR_SW_VERSION].split(".")[0]) >= 3: + entry_data[CONF_UI_CONFIGURATION] = json.loads( + await director.getUiConfiguration() + ) # Load options from config entry entry_data[CONF_SCAN_INTERVAL] = entry.options.get( diff --git a/homeassistant/components/control4/media_player.py b/homeassistant/components/control4/media_player.py index 99d8c27face..72aa44faaed 100644 --- a/homeassistant/components/control4/media_player.py +++ b/homeassistant/components/control4/media_player.py @@ -81,11 +81,18 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up Control4 rooms from a config entry.""" + entry_data = hass.data[DOMAIN][entry.entry_id] + ui_config = entry_data[CONF_UI_CONFIGURATION] + + # OS 2 will not have a ui_configuration + if not ui_config: + _LOGGER.debug("No UI Configuration found for Control4") + return + all_rooms = await get_rooms(hass, entry) if not all_rooms: return - entry_data = hass.data[DOMAIN][entry.entry_id] scan_interval = entry_data[CONF_SCAN_INTERVAL] _LOGGER.debug("Scan interval = %s", scan_interval) @@ -119,8 +126,6 @@ async def async_setup_entry( if "parentId" in item and k > 1 } - ui_config = entry_data[CONF_UI_CONFIGURATION] - entity_list = [] for room in all_rooms: room_id = room["id"] From 2d2f5de191e70bbf9fbcb3eb68a40188924d62a4 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 10 Jun 2024 08:49:18 +0200 Subject: [PATCH 0446/1445] Improve type hints in blueprint tests (#119263) --- tests/components/blueprint/test_models.py | 30 +++++++++++-------- .../blueprint/test_websocket_api.py | 22 ++++++-------- 2 files changed, 27 insertions(+), 25 deletions(-) diff --git a/tests/components/blueprint/test_models.py b/tests/components/blueprint/test_models.py index 1b84d4abcbe..45e35474e4c 100644 --- a/tests/components/blueprint/test_models.py +++ b/tests/components/blueprint/test_models.py @@ -11,7 +11,7 @@ from homeassistant.util.yaml import Input @pytest.fixture -def blueprint_1(): +def blueprint_1() -> models.Blueprint: """Blueprint fixture.""" return models.Blueprint( { @@ -61,7 +61,7 @@ def blueprint_2(request: pytest.FixtureRequest) -> models.Blueprint: @pytest.fixture -def domain_bps(hass): +def domain_bps(hass: HomeAssistant) -> models.DomainBlueprints: """Domain blueprints fixture.""" return models.DomainBlueprints( hass, "automation", logging.getLogger(__name__), None, AsyncMock() @@ -92,7 +92,7 @@ def test_blueprint_model_init() -> None: ) -def test_blueprint_properties(blueprint_1) -> None: +def test_blueprint_properties(blueprint_1: models.Blueprint) -> None: """Test properties.""" assert blueprint_1.metadata == { "name": "Hello", @@ -147,7 +147,7 @@ def test_blueprint_validate() -> None: ).validate() == ["Requires at least Home Assistant 100000.0.0"] -def test_blueprint_inputs(blueprint_2) -> None: +def test_blueprint_inputs(blueprint_2: models.Blueprint) -> None: """Test blueprint inputs.""" inputs = models.BlueprintInputs( blueprint_2, @@ -167,7 +167,7 @@ def test_blueprint_inputs(blueprint_2) -> None: } -def test_blueprint_inputs_validation(blueprint_1) -> None: +def test_blueprint_inputs_validation(blueprint_1: models.Blueprint) -> None: """Test blueprint input validation.""" inputs = models.BlueprintInputs( blueprint_1, @@ -177,7 +177,7 @@ def test_blueprint_inputs_validation(blueprint_1) -> None: inputs.validate() -def test_blueprint_inputs_default(blueprint_2) -> None: +def test_blueprint_inputs_default(blueprint_2: models.Blueprint) -> None: """Test blueprint inputs.""" inputs = models.BlueprintInputs( blueprint_2, @@ -192,7 +192,7 @@ def test_blueprint_inputs_default(blueprint_2) -> None: assert inputs.async_substitute() == {"example": 1, "example-default": "test"} -def test_blueprint_inputs_override_default(blueprint_2) -> None: +def test_blueprint_inputs_override_default(blueprint_2: models.Blueprint) -> None: """Test blueprint inputs.""" inputs = models.BlueprintInputs( blueprint_2, @@ -216,7 +216,7 @@ def test_blueprint_inputs_override_default(blueprint_2) -> None: async def test_domain_blueprints_get_blueprint_errors( - hass: HomeAssistant, domain_bps + hass: HomeAssistant, domain_bps: models.DomainBlueprints ) -> None: """Test domain blueprints.""" assert hass.data["blueprint"]["automation"] is domain_bps @@ -236,7 +236,7 @@ async def test_domain_blueprints_get_blueprint_errors( await domain_bps.async_get_blueprint("non-existing-path") -async def test_domain_blueprints_caching(domain_bps) -> None: +async def test_domain_blueprints_caching(domain_bps: models.DomainBlueprints) -> None: """Test domain blueprints cache blueprints.""" obj = object() with patch.object(domain_bps, "_load_blueprint", return_value=obj): @@ -253,7 +253,9 @@ async def test_domain_blueprints_caching(domain_bps) -> None: assert await domain_bps.async_get_blueprint("something") is obj_2 -async def test_domain_blueprints_inputs_from_config(domain_bps, blueprint_1) -> None: +async def test_domain_blueprints_inputs_from_config( + domain_bps: models.DomainBlueprints, blueprint_1: models.Blueprint +) -> None: """Test DomainBlueprints.async_inputs_from_config.""" with pytest.raises(errors.InvalidBlueprintInputs): await domain_bps.async_inputs_from_config({"not-referencing": "use_blueprint"}) @@ -274,7 +276,9 @@ async def test_domain_blueprints_inputs_from_config(domain_bps, blueprint_1) -> assert inputs.inputs == {"test-input": None} -async def test_domain_blueprints_add_blueprint(domain_bps, blueprint_1) -> None: +async def test_domain_blueprints_add_blueprint( + domain_bps: models.DomainBlueprints, blueprint_1: models.Blueprint +) -> None: """Test DomainBlueprints.async_add_blueprint.""" with patch.object(domain_bps, "_create_file") as create_file_mock: await domain_bps.async_add_blueprint(blueprint_1, "something.yaml") @@ -286,7 +290,9 @@ async def test_domain_blueprints_add_blueprint(domain_bps, blueprint_1) -> None: assert not mock_load.mock_calls -async def test_inputs_from_config_nonexisting_blueprint(domain_bps) -> None: +async def test_inputs_from_config_nonexisting_blueprint( + domain_bps: models.DomainBlueprints, +) -> None: """Test referring non-existing blueprint.""" with pytest.raises(errors.FailedToLoad): await domain_bps.async_inputs_from_config( diff --git a/tests/components/blueprint/test_websocket_api.py b/tests/components/blueprint/test_websocket_api.py index 93d97dfd036..21387f7763c 100644 --- a/tests/components/blueprint/test_websocket_api.py +++ b/tests/components/blueprint/test_websocket_api.py @@ -1,6 +1,7 @@ """Test websocket API.""" from pathlib import Path +from typing import Any from unittest.mock import Mock, patch import pytest @@ -15,19 +16,23 @@ from tests.typing import WebSocketGenerator @pytest.fixture -def automation_config(): +def automation_config() -> dict[str, Any]: """Automation config.""" return {} @pytest.fixture -def script_config(): +def script_config() -> dict[str, Any]: """Script config.""" return {} @pytest.fixture(autouse=True) -async def setup_bp(hass, automation_config, script_config): +async def setup_bp( + hass: HomeAssistant, + automation_config: dict[str, Any], + script_config: dict[str, Any], +) -> None: """Fixture to set up the blueprint component.""" assert await async_setup_component(hass, "blueprint", {}) @@ -135,11 +140,11 @@ async def test_import_blueprint( } +@pytest.mark.usefixtures("setup_bp") async def test_import_blueprint_update( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, hass_ws_client: WebSocketGenerator, - setup_bp, ) -> None: """Test importing blueprints.""" raw_data = Path( @@ -182,7 +187,6 @@ async def test_import_blueprint_update( async def test_save_blueprint( hass: HomeAssistant, - aioclient_mock: AiohttpClientMocker, hass_ws_client: WebSocketGenerator, ) -> None: """Test saving blueprints.""" @@ -236,7 +240,6 @@ async def test_save_blueprint( async def test_save_existing_file( hass: HomeAssistant, - aioclient_mock: AiohttpClientMocker, hass_ws_client: WebSocketGenerator, ) -> None: """Test saving blueprints.""" @@ -262,7 +265,6 @@ async def test_save_existing_file( async def test_save_existing_file_override( hass: HomeAssistant, - aioclient_mock: AiohttpClientMocker, hass_ws_client: WebSocketGenerator, ) -> None: """Test saving blueprints.""" @@ -298,7 +300,6 @@ async def test_save_existing_file_override( async def test_save_file_error( hass: HomeAssistant, - aioclient_mock: AiohttpClientMocker, hass_ws_client: WebSocketGenerator, ) -> None: """Test saving blueprints with OS error.""" @@ -323,7 +324,6 @@ async def test_save_file_error( async def test_save_invalid_blueprint( hass: HomeAssistant, - aioclient_mock: AiohttpClientMocker, hass_ws_client: WebSocketGenerator, ) -> None: """Test saving invalid blueprints.""" @@ -352,7 +352,6 @@ async def test_save_invalid_blueprint( async def test_delete_blueprint( hass: HomeAssistant, - aioclient_mock: AiohttpClientMocker, hass_ws_client: WebSocketGenerator, ) -> None: """Test deleting blueprints.""" @@ -377,7 +376,6 @@ async def test_delete_blueprint( async def test_delete_non_exist_file_blueprint( hass: HomeAssistant, - aioclient_mock: AiohttpClientMocker, hass_ws_client: WebSocketGenerator, ) -> None: """Test deleting non existing blueprints.""" @@ -417,7 +415,6 @@ async def test_delete_non_exist_file_blueprint( ) async def test_delete_blueprint_in_use_by_automation( hass: HomeAssistant, - aioclient_mock: AiohttpClientMocker, hass_ws_client: WebSocketGenerator, ) -> None: """Test deleting a blueprint which is in use.""" @@ -463,7 +460,6 @@ async def test_delete_blueprint_in_use_by_automation( ) async def test_delete_blueprint_in_use_by_script( hass: HomeAssistant, - aioclient_mock: AiohttpClientMocker, hass_ws_client: WebSocketGenerator, ) -> None: """Test deleting a blueprint which is in use.""" From d9362a2f2faf29542c01601710eba76ea078cdaa Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 10 Jun 2024 08:49:43 +0200 Subject: [PATCH 0447/1445] Improve type hints in axis tests (#119260) * Improve type hints in axis tests * A couple more * One more * Improve light * Improve hub * Improve config-flow --- tests/components/axis/conftest.py | 12 ++--- tests/components/axis/test_binary_sensor.py | 8 +-- tests/components/axis/test_camera.py | 12 ++--- tests/components/axis/test_config_flow.py | 54 +++++++++++-------- tests/components/axis/test_diagnostics.py | 3 +- tests/components/axis/test_hub.py | 57 +++++++++++++-------- tests/components/axis/test_init.py | 14 +++-- tests/components/axis/test_light.py | 7 +-- tests/components/axis/test_switch.py | 5 +- 9 files changed, 101 insertions(+), 71 deletions(-) diff --git a/tests/components/axis/conftest.py b/tests/components/axis/conftest.py index eba0af91393..b306e25c434 100644 --- a/tests/components/axis/conftest.py +++ b/tests/components/axis/conftest.py @@ -111,10 +111,10 @@ def config_entry_options_fixture() -> MappingProxyType[str, Any]: @pytest.fixture(name="mock_vapix_requests") def default_request_fixture( - respx_mock: respx, + respx_mock: respx.MockRouter, port_management_payload: dict[str, Any], - param_properties_payload: dict[str, Any], - param_ports_payload: dict[str, Any], + param_properties_payload: str, + param_ports_payload: str, mqtt_status_code: int, ) -> Callable[[str], None]: """Mock default Vapix requests responses.""" @@ -230,19 +230,19 @@ def io_port_management_data_fixture() -> dict[str, Any]: @pytest.fixture(name="param_properties_payload") -def param_properties_data_fixture() -> dict[str, Any]: +def param_properties_data_fixture() -> str: """Property parameter data.""" return PROPERTIES_RESPONSE @pytest.fixture(name="param_ports_payload") -def param_ports_data_fixture() -> dict[str, Any]: +def param_ports_data_fixture() -> str: """Property parameter data.""" return PORTS_RESPONSE @pytest.fixture(name="mqtt_status_code") -def mqtt_status_code_fixture(): +def mqtt_status_code_fixture() -> int: """Property parameter data.""" return 200 diff --git a/tests/components/axis/test_binary_sensor.py b/tests/components/axis/test_binary_sensor.py index dd7674d7d3f..99a530724e3 100644 --- a/tests/components/axis/test_binary_sensor.py +++ b/tests/components/axis/test_binary_sensor.py @@ -1,6 +1,7 @@ """Axis binary sensor platform tests.""" from collections.abc import Callable +from typing import Any import pytest @@ -8,7 +9,6 @@ from homeassistant.components.binary_sensor import ( DOMAIN as BINARY_SENSOR_DOMAIN, BinarySensorDeviceClass, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant @@ -173,12 +173,12 @@ from .const import NAME ), ], ) +@pytest.mark.usefixtures("setup_config_entry") async def test_binary_sensors( hass: HomeAssistant, - setup_config_entry: ConfigEntry, mock_rtsp_event: Callable[[str, str, str, str, str, str], None], event: dict[str, str], - entity: dict[str, str], + entity: dict[str, Any], ) -> None: """Test that sensors are loaded properly.""" mock_rtsp_event(**event) @@ -225,9 +225,9 @@ async def test_binary_sensors( }, ], ) +@pytest.mark.usefixtures("setup_config_entry") async def test_unsupported_events( hass: HomeAssistant, - setup_config_entry: ConfigEntry, mock_rtsp_event: Callable[[str, str, str, str, str, str], None], event: dict[str, str], ) -> None: diff --git a/tests/components/axis/test_camera.py b/tests/components/axis/test_camera.py index e184f2014b3..7d26cc7a3bc 100644 --- a/tests/components/axis/test_camera.py +++ b/tests/components/axis/test_camera.py @@ -30,7 +30,8 @@ async def test_platform_manually_configured(hass: HomeAssistant) -> None: assert AXIS_DOMAIN not in hass.data -async def test_camera(hass: HomeAssistant, setup_config_entry: ConfigEntry) -> None: +@pytest.mark.usefixtures("setup_config_entry") +async def test_camera(hass: HomeAssistant) -> None: """Test that Axis camera platform is loaded properly.""" assert len(hass.states.async_entity_ids(CAMERA_DOMAIN)) == 1 @@ -50,9 +51,8 @@ async def test_camera(hass: HomeAssistant, setup_config_entry: ConfigEntry) -> N @pytest.mark.parametrize("config_entry_options", [{CONF_STREAM_PROFILE: "profile_1"}]) -async def test_camera_with_stream_profile( - hass: HomeAssistant, setup_config_entry: ConfigEntry -) -> None: +@pytest.mark.usefixtures("setup_config_entry") +async def test_camera_with_stream_profile(hass: HomeAssistant) -> None: """Test that Axis camera entity is using the correct path with stream profike.""" assert len(hass.states.async_entity_ids(CAMERA_DOMAIN)) == 1 @@ -74,7 +74,7 @@ async def test_camera_with_stream_profile( ) -property_data = f"""root.Properties.API.HTTP.Version=3 +PROPERTY_DATA = f"""root.Properties.API.HTTP.Version=3 root.Properties.API.Metadata.Metadata=yes root.Properties.API.Metadata.Version=1.0 root.Properties.EmbeddedDevelopment.Version=2.16 @@ -85,7 +85,7 @@ root.Properties.System.SerialNumber={MAC} """ -@pytest.mark.parametrize("param_properties_payload", [property_data]) +@pytest.mark.parametrize("param_properties_payload", [PROPERTY_DATA]) async def test_camera_disabled( hass: HomeAssistant, prepare_config_entry: Callable[[], ConfigEntry] ) -> None: diff --git a/tests/components/axis/test_config_flow.py b/tests/components/axis/test_config_flow.py index 68dca3539c5..055c74cc9a5 100644 --- a/tests/components/axis/test_config_flow.py +++ b/tests/components/axis/test_config_flow.py @@ -1,7 +1,8 @@ """Test Axis config flow.""" +from collections.abc import Callable from ipaddress import ip_address -from unittest.mock import patch +from unittest.mock import AsyncMock, patch import pytest @@ -22,6 +23,7 @@ from homeassistant.config_entries import ( SOURCE_SSDP, SOURCE_USER, SOURCE_ZEROCONF, + ConfigEntry, ) from homeassistant.const import ( CONF_HOST, @@ -33,7 +35,7 @@ from homeassistant.const import ( CONF_USERNAME, ) from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import FlowResultType +from homeassistant.data_entry_flow import BaseServiceInfo, FlowResultType from homeassistant.helpers import device_registry as dr from .const import DEFAULT_HOST, MAC, MODEL, NAME @@ -44,16 +46,17 @@ DHCP_FORMATTED_MAC = dr.format_mac(MAC).replace(":", "") @pytest.fixture(name="mock_config_entry") -async def mock_config_entry_fixture(hass, config_entry, mock_setup_entry): +async def mock_config_entry_fixture( + hass: HomeAssistant, config_entry: MockConfigEntry, mock_setup_entry: AsyncMock +) -> MockConfigEntry: """Mock config entry and setup entry.""" assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() return config_entry -async def test_flow_manual_configuration( - hass: HomeAssistant, setup_default_vapix_requests, mock_setup_entry -) -> None: +@pytest.mark.usefixtures("setup_default_vapix_requests", "mock_setup_entry") +async def test_flow_manual_configuration(hass: HomeAssistant) -> None: """Test that config flow works.""" MockConfigEntry(domain=AXIS_DOMAIN, source=SOURCE_IGNORE).add_to_hass(hass) @@ -89,7 +92,9 @@ async def test_flow_manual_configuration( async def test_manual_configuration_update_configuration( - hass: HomeAssistant, mock_config_entry, mock_vapix_requests + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_vapix_requests: Callable[[str], None], ) -> None: """Test that config flow fails on already configured device.""" assert mock_config_entry.data[CONF_HOST] == "1.2.3.4" @@ -173,8 +178,9 @@ async def test_flow_fails_cannot_connect(hass: HomeAssistant) -> None: assert result["errors"] == {"base": "cannot_connect"} +@pytest.mark.usefixtures("setup_default_vapix_requests", "mock_setup_entry") async def test_flow_create_entry_multiple_existing_entries_of_same_model( - hass: HomeAssistant, setup_default_vapix_requests, mock_setup_entry + hass: HomeAssistant, ) -> None: """Test that create entry can generate a name with other entries.""" entry = MockConfigEntry( @@ -222,7 +228,9 @@ async def test_flow_create_entry_multiple_existing_entries_of_same_model( async def test_reauth_flow_update_configuration( - hass: HomeAssistant, mock_config_entry, mock_vapix_requests + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_vapix_requests: Callable[[str], None], ) -> None: """Test that config flow fails on already configured device.""" assert mock_config_entry.data[CONF_HOST] == "1.2.3.4" @@ -261,7 +269,9 @@ async def test_reauth_flow_update_configuration( async def test_reconfiguration_flow_update_configuration( - hass: HomeAssistant, mock_config_entry, mock_vapix_requests + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_vapix_requests: Callable[[str], None], ) -> None: """Test that config flow reconfiguration updates configured device.""" assert mock_config_entry.data[CONF_HOST] == "1.2.3.4" @@ -362,12 +372,11 @@ async def test_reconfiguration_flow_update_configuration( ), ], ) +@pytest.mark.usefixtures("setup_default_vapix_requests", "mock_setup_entry") async def test_discovery_flow( hass: HomeAssistant, - setup_default_vapix_requests, source: str, - discovery_info: dict, - mock_setup_entry, + discovery_info: BaseServiceInfo, ) -> None: """Test the different discovery flows for new devices work.""" result = await hass.config_entries.flow.async_init( @@ -445,7 +454,10 @@ async def test_discovery_flow( ], ) async def test_discovered_device_already_configured( - hass: HomeAssistant, mock_config_entry, source: str, discovery_info: dict + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + source: str, + discovery_info: BaseServiceInfo, ) -> None: """Test that discovery doesn't setup already configured devices.""" assert mock_config_entry.data[CONF_HOST] == DEFAULT_HOST @@ -501,10 +513,10 @@ async def test_discovered_device_already_configured( ) async def test_discovery_flow_updated_configuration( hass: HomeAssistant, - mock_config_entry, - mock_vapix_requests, + mock_config_entry: MockConfigEntry, + mock_vapix_requests: Callable[[str], None], source: str, - discovery_info: dict, + discovery_info: BaseServiceInfo, expected_port: int, ) -> None: """Test that discovery flow update configuration with new parameters.""" @@ -573,7 +585,7 @@ async def test_discovery_flow_updated_configuration( ], ) async def test_discovery_flow_ignore_non_axis_device( - hass: HomeAssistant, source: str, discovery_info: dict + hass: HomeAssistant, source: str, discovery_info: BaseServiceInfo ) -> None: """Test that discovery flow ignores devices with non Axis OUI.""" result = await hass.config_entries.flow.async_init( @@ -622,7 +634,7 @@ async def test_discovery_flow_ignore_non_axis_device( ], ) async def test_discovery_flow_ignore_link_local_address( - hass: HomeAssistant, source: str, discovery_info: dict + hass: HomeAssistant, source: str, discovery_info: BaseServiceInfo ) -> None: """Test that discovery flow ignores devices with link local addresses.""" result = await hass.config_entries.flow.async_init( @@ -633,7 +645,9 @@ async def test_discovery_flow_ignore_link_local_address( assert result["reason"] == "link_local_address" -async def test_option_flow(hass: HomeAssistant, setup_config_entry) -> None: +async def test_option_flow( + hass: HomeAssistant, setup_config_entry: ConfigEntry +) -> None: """Test config flow options.""" assert CONF_STREAM_PROFILE not in setup_config_entry.options assert CONF_VIDEO_SOURCE not in setup_config_entry.options diff --git a/tests/components/axis/test_diagnostics.py b/tests/components/axis/test_diagnostics.py index 026e1ae4d22..c3e1faf4277 100644 --- a/tests/components/axis/test_diagnostics.py +++ b/tests/components/axis/test_diagnostics.py @@ -3,6 +3,7 @@ import pytest from syrupy import SnapshotAssertion +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from .const import API_DISCOVERY_BASIC_DEVICE_INFO @@ -15,7 +16,7 @@ from tests.typing import ClientSessionGenerator async def test_entry_diagnostics( hass: HomeAssistant, hass_client: ClientSessionGenerator, - setup_config_entry, + setup_config_entry: ConfigEntry, snapshot: SnapshotAssertion, ) -> None: """Test config entry diagnostics.""" diff --git a/tests/components/axis/test_hub.py b/tests/components/axis/test_hub.py index c208f767bfc..fb0a28bb262 100644 --- a/tests/components/axis/test_hub.py +++ b/tests/components/axis/test_hub.py @@ -1,16 +1,20 @@ """Test Axis device.""" +from collections.abc import Callable from ipaddress import ip_address +from types import MappingProxyType +from typing import Any from unittest import mock -from unittest.mock import ANY, Mock, call, patch +from unittest.mock import ANY, AsyncMock, Mock, call, patch import axis as axislib import pytest +from typing_extensions import Generator from homeassistant.components import axis, zeroconf from homeassistant.components.axis.const import DOMAIN as AXIS_DOMAIN from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN -from homeassistant.config_entries import SOURCE_ZEROCONF +from homeassistant.config_entries import SOURCE_ZEROCONF, ConfigEntry from homeassistant.const import ( CONF_HOST, CONF_MODEL, @@ -35,7 +39,7 @@ from tests.typing import MqttMockHAClient @pytest.fixture(name="forward_entry_setups") -def hass_mock_forward_entry_setup(hass): +def hass_mock_forward_entry_setup(hass: HomeAssistant) -> Generator[AsyncMock]: """Mock async_forward_entry_setups.""" with patch.object( hass.config_entries, "async_forward_entry_setups" @@ -44,10 +48,9 @@ def hass_mock_forward_entry_setup(hass): async def test_device_setup( - hass: HomeAssistant, - forward_entry_setups, - config_entry_data, - setup_config_entry, + forward_entry_setups: AsyncMock, + config_entry_data: MappingProxyType[str, Any], + setup_config_entry: ConfigEntry, device_registry: dr.DeviceRegistry, ) -> None: """Successful setup.""" @@ -75,7 +78,7 @@ async def test_device_setup( @pytest.mark.parametrize("api_discovery_items", [API_DISCOVERY_BASIC_DEVICE_INFO]) -async def test_device_info(hass: HomeAssistant, setup_config_entry) -> None: +async def test_device_info(setup_config_entry: ConfigEntry) -> None: """Verify other path of device information works.""" hub = setup_config_entry.runtime_data @@ -86,8 +89,9 @@ async def test_device_info(hass: HomeAssistant, setup_config_entry) -> None: @pytest.mark.parametrize("api_discovery_items", [API_DISCOVERY_MQTT]) +@pytest.mark.usefixtures("setup_config_entry") async def test_device_support_mqtt( - hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_config_entry + hass: HomeAssistant, mqtt_mock: MqttMockHAClient ) -> None: """Successful setup.""" mqtt_call = call(f"axis/{MAC}/#", mock.ANY, 0, "utf-8", ANY) @@ -111,16 +115,17 @@ async def test_device_support_mqtt( @pytest.mark.parametrize("api_discovery_items", [API_DISCOVERY_MQTT]) @pytest.mark.parametrize("mqtt_status_code", [401]) -async def test_device_support_mqtt_low_privilege( - hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_config_entry -) -> None: +@pytest.mark.usefixtures("setup_config_entry") +async def test_device_support_mqtt_low_privilege(mqtt_mock: MqttMockHAClient) -> None: """Successful setup.""" mqtt_call = call(f"{MAC}/#", mock.ANY, 0, "utf-8") assert mqtt_call not in mqtt_mock.async_subscribe.call_args_list async def test_update_address( - hass: HomeAssistant, setup_config_entry, mock_vapix_requests + hass: HomeAssistant, + setup_config_entry: ConfigEntry, + mock_vapix_requests: Callable[[str], None], ) -> None: """Test update address works.""" hub = setup_config_entry.runtime_data @@ -145,8 +150,11 @@ async def test_update_address( assert hub.api.config.host == "2.3.4.5" +@pytest.mark.usefixtures("setup_config_entry") async def test_device_unavailable( - hass: HomeAssistant, setup_config_entry, mock_rtsp_event, mock_rtsp_signal_state + hass: HomeAssistant, + mock_rtsp_event: Callable[[str, str, str, str, str, str], None], + mock_rtsp_signal_state: Callable[[bool], None], ) -> None: """Successful setup.""" # Provide an entity that can be used to verify connection state on @@ -179,8 +187,9 @@ async def test_device_unavailable( assert hass.states.get(f"{BINARY_SENSOR_DOMAIN}.{NAME}_sound_1").state == STATE_OFF +@pytest.mark.usefixtures("setup_default_vapix_requests") async def test_device_not_accessible( - hass: HomeAssistant, config_entry, setup_default_vapix_requests + hass: HomeAssistant, config_entry: ConfigEntry ) -> None: """Failed setup schedules a retry of setup.""" with patch.object(axis, "get_axis_api", side_effect=axis.errors.CannotConnect): @@ -189,8 +198,9 @@ async def test_device_not_accessible( assert hass.data[AXIS_DOMAIN] == {} +@pytest.mark.usefixtures("setup_default_vapix_requests") async def test_device_trigger_reauth_flow( - hass: HomeAssistant, config_entry, setup_default_vapix_requests + hass: HomeAssistant, config_entry: ConfigEntry ) -> None: """Failed authentication trigger a reauthentication flow.""" with ( @@ -205,8 +215,9 @@ async def test_device_trigger_reauth_flow( assert hass.data[AXIS_DOMAIN] == {} +@pytest.mark.usefixtures("setup_default_vapix_requests") async def test_device_unknown_error( - hass: HomeAssistant, config_entry, setup_default_vapix_requests + hass: HomeAssistant, config_entry: ConfigEntry ) -> None: """Unknown errors are handled.""" with patch.object(axis, "get_axis_api", side_effect=Exception): @@ -215,7 +226,7 @@ async def test_device_unknown_error( assert hass.data[AXIS_DOMAIN] == {} -async def test_shutdown(config_entry_data) -> None: +async def test_shutdown(config_entry_data: MappingProxyType[str, Any]) -> None: """Successful shutdown.""" hass = Mock() entry = Mock() @@ -230,7 +241,9 @@ async def test_shutdown(config_entry_data) -> None: assert len(axis_device.api.stream.stop.mock_calls) == 1 -async def test_get_device_fails(hass: HomeAssistant, config_entry_data) -> None: +async def test_get_device_fails( + hass: HomeAssistant, config_entry_data: MappingProxyType[str, Any] +) -> None: """Device unauthorized yields authentication required error.""" with ( patch( @@ -242,7 +255,7 @@ async def test_get_device_fails(hass: HomeAssistant, config_entry_data) -> None: async def test_get_device_device_unavailable( - hass: HomeAssistant, config_entry_data + hass: HomeAssistant, config_entry_data: MappingProxyType[str, Any] ) -> None: """Device unavailable yields cannot connect error.""" with ( @@ -252,7 +265,9 @@ async def test_get_device_device_unavailable( await axis.hub.get_axis_api(hass, config_entry_data) -async def test_get_device_unknown_error(hass: HomeAssistant, config_entry_data) -> None: +async def test_get_device_unknown_error( + hass: HomeAssistant, config_entry_data: MappingProxyType[str, Any] +) -> None: """Device yield unknown error.""" with ( patch("axis.interfaces.vapix.Vapix.request", side_effect=axislib.AxisException), diff --git a/tests/components/axis/test_init.py b/tests/components/axis/test_init.py index 607508b985a..e4dc7cd1eef 100644 --- a/tests/components/axis/test_init.py +++ b/tests/components/axis/test_init.py @@ -5,16 +5,18 @@ from unittest.mock import AsyncMock, Mock, patch import pytest from homeassistant.components import axis -from homeassistant.config_entries import ConfigEntryState +from homeassistant.config_entries import ConfigEntry, ConfigEntryState from homeassistant.core import HomeAssistant -async def test_setup_entry(hass: HomeAssistant, setup_config_entry) -> None: +async def test_setup_entry(setup_config_entry: ConfigEntry) -> None: """Test successful setup of entry.""" assert setup_config_entry.state is ConfigEntryState.LOADED -async def test_setup_entry_fails(hass: HomeAssistant, config_entry) -> None: +async def test_setup_entry_fails( + hass: HomeAssistant, config_entry: ConfigEntry +) -> None: """Test successful setup of entry.""" mock_device = Mock() mock_device.async_setup = AsyncMock(return_value=False) @@ -27,7 +29,9 @@ async def test_setup_entry_fails(hass: HomeAssistant, config_entry) -> None: assert config_entry.state is ConfigEntryState.SETUP_ERROR -async def test_unload_entry(hass: HomeAssistant, setup_config_entry) -> None: +async def test_unload_entry( + hass: HomeAssistant, setup_config_entry: ConfigEntry +) -> None: """Test successful unload of entry.""" assert setup_config_entry.state is ConfigEntryState.LOADED @@ -36,7 +40,7 @@ async def test_unload_entry(hass: HomeAssistant, setup_config_entry) -> None: @pytest.mark.parametrize("config_entry_version", [1]) -async def test_migrate_entry(hass: HomeAssistant, config_entry) -> None: +async def test_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> None: """Test successful migration of entry data.""" assert config_entry.version == 1 diff --git a/tests/components/axis/test_light.py b/tests/components/axis/test_light.py index 5cde6b74fc4..a5ae66afee0 100644 --- a/tests/components/axis/test_light.py +++ b/tests/components/axis/test_light.py @@ -9,7 +9,6 @@ import pytest import respx from homeassistant.components.light import ATTR_BRIGHTNESS, DOMAIN as LIGHT_DOMAIN -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_ENTITY_ID, SERVICE_TURN_OFF, @@ -70,9 +69,9 @@ def light_control_fixture(light_control_items: list[dict[str, Any]]) -> None: @pytest.mark.parametrize("api_discovery_items", [API_DISCOVERY_LIGHT_CONTROL]) @pytest.mark.parametrize("light_control_items", [[]]) +@pytest.mark.usefixtures("setup_config_entry") async def test_no_light_entity_without_light_control_representation( hass: HomeAssistant, - setup_config_entry: ConfigEntry, mock_rtsp_event: Callable[[str, str, str, str, str, str], None], ) -> None: """Verify no lights entities get created without light control representation.""" @@ -89,12 +88,10 @@ async def test_no_light_entity_without_light_control_representation( @pytest.mark.parametrize("api_discovery_items", [API_DISCOVERY_LIGHT_CONTROL]) +@pytest.mark.usefixtures("setup_config_entry") async def test_lights( hass: HomeAssistant, - respx_mock: respx, - setup_config_entry: ConfigEntry, mock_rtsp_event: Callable[[str, str, str, str, str, str], None], - api_discovery_items: dict[str, Any], ) -> None: """Test that lights are loaded properly.""" # Add light diff --git a/tests/components/axis/test_switch.py b/tests/components/axis/test_switch.py index b9202d42e25..479830783b1 100644 --- a/tests/components/axis/test_switch.py +++ b/tests/components/axis/test_switch.py @@ -7,7 +7,6 @@ from axis.models.api import CONTEXT import pytest from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_ENTITY_ID, SERVICE_TURN_OFF, @@ -31,9 +30,9 @@ root.IOPort.I1.Output.Active=open @pytest.mark.parametrize("param_ports_payload", [PORT_DATA]) +@pytest.mark.usefixtures("setup_config_entry") async def test_switches_with_port_cgi( hass: HomeAssistant, - setup_config_entry: ConfigEntry, mock_rtsp_event: Callable[[str, str, str, str, str, str], None], ) -> None: """Test that switches are loaded properly using port.cgi.""" @@ -116,9 +115,9 @@ PORT_MANAGEMENT_RESPONSE = { @pytest.mark.parametrize("api_discovery_items", [API_DISCOVERY_PORT_MANAGEMENT]) @pytest.mark.parametrize("port_management_payload", [PORT_MANAGEMENT_RESPONSE]) +@pytest.mark.usefixtures("setup_config_entry") async def test_switches_with_port_management( hass: HomeAssistant, - setup_config_entry: ConfigEntry, mock_rtsp_event: Callable[[str, str, str, str, str, str], None], ) -> None: """Test that switches are loaded properly using port management.""" From 1ebc1685f7a6bdcb07c0d457229c13c555c2230b Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 10 Jun 2024 09:30:00 +0200 Subject: [PATCH 0448/1445] Improve type hints in camera tests (#119264) --- tests/components/camera/conftest.py | 15 +- tests/components/camera/test_init.py | 166 +++++++++---------- tests/components/camera/test_media_source.py | 27 +-- 3 files changed, 104 insertions(+), 104 deletions(-) diff --git a/tests/components/camera/conftest.py b/tests/components/camera/conftest.py index ee8c5df7d65..524b56c2303 100644 --- a/tests/components/camera/conftest.py +++ b/tests/components/camera/conftest.py @@ -3,6 +3,7 @@ from unittest.mock import PropertyMock, patch import pytest +from typing_extensions import AsyncGenerator, Generator from homeassistant.components import camera from homeassistant.components.camera.const import StreamType @@ -15,13 +16,13 @@ from .common import WEBRTC_ANSWER @pytest.fixture(autouse=True) -async def setup_homeassistant(hass: HomeAssistant): +async def setup_homeassistant(hass: HomeAssistant) -> None: """Set up the homeassistant integration.""" await async_setup_component(hass, "homeassistant", {}) @pytest.fixture(autouse=True) -async def camera_only() -> None: +def camera_only() -> Generator[None]: """Enable only the camera platform.""" with patch( "homeassistant.components.demo.COMPONENTS_WITH_CONFIG_ENTRY_DEMO_PLATFORM", @@ -31,7 +32,7 @@ async def camera_only() -> None: @pytest.fixture(name="mock_camera") -async def mock_camera_fixture(hass): +async def mock_camera_fixture(hass: HomeAssistant) -> AsyncGenerator[None]: """Initialize a demo camera platform.""" assert await async_setup_component( hass, "camera", {camera.DOMAIN: {"platform": "demo"}} @@ -46,7 +47,7 @@ async def mock_camera_fixture(hass): @pytest.fixture(name="mock_camera_hls") -async def mock_camera_hls_fixture(mock_camera): +def mock_camera_hls_fixture(mock_camera: None) -> Generator[None]: """Initialize a demo camera platform with HLS.""" with patch( "homeassistant.components.camera.Camera.frontend_stream_type", @@ -56,7 +57,7 @@ async def mock_camera_hls_fixture(mock_camera): @pytest.fixture(name="mock_camera_web_rtc") -async def mock_camera_web_rtc_fixture(hass): +async def mock_camera_web_rtc_fixture(hass: HomeAssistant) -> AsyncGenerator[None]: """Initialize a demo camera platform with WebRTC.""" assert await async_setup_component( hass, "camera", {camera.DOMAIN: {"platform": "demo"}} @@ -77,7 +78,7 @@ async def mock_camera_web_rtc_fixture(hass): @pytest.fixture(name="mock_camera_with_device") -async def mock_camera_with_device_fixture(): +def mock_camera_with_device_fixture() -> Generator[None]: """Initialize a demo camera platform with a device.""" dev_info = DeviceInfo( identifiers={("camera", "test_unique_id")}, @@ -103,7 +104,7 @@ async def mock_camera_with_device_fixture(): @pytest.fixture(name="mock_camera_with_no_name") -async def mock_camera_with_no_name_fixture(mock_camera_with_device): +def mock_camera_with_no_name_fixture(mock_camera_with_device: None) -> Generator[None]: """Initialize a demo camera platform with a device and no name.""" with patch( "homeassistant.components.camera.Camera._attr_name", diff --git a/tests/components/camera/test_init.py b/tests/components/camera/test_init.py index 0520908f210..669c3594648 100644 --- a/tests/components/camera/test_init.py +++ b/tests/components/camera/test_init.py @@ -6,6 +6,7 @@ from types import ModuleType from unittest.mock import AsyncMock, Mock, PropertyMock, mock_open, patch import pytest +from typing_extensions import Generator from homeassistant.components import camera from homeassistant.components.camera.const import ( @@ -41,7 +42,7 @@ WEBRTC_OFFER = "v=0\r\n" @pytest.fixture(name="mock_stream") -def mock_stream_fixture(hass): +def mock_stream_fixture(hass: HomeAssistant) -> None: """Initialize a demo camera platform with streaming.""" assert hass.loop.run_until_complete( async_setup_component(hass, "stream", {"stream": {}}) @@ -49,7 +50,7 @@ def mock_stream_fixture(hass): @pytest.fixture(name="image_mock_url") -async def image_mock_url_fixture(hass): +async def image_mock_url_fixture(hass: HomeAssistant) -> None: """Fixture for get_image tests.""" await async_setup_component( hass, camera.DOMAIN, {camera.DOMAIN: {"platform": "demo"}} @@ -58,7 +59,7 @@ async def image_mock_url_fixture(hass): @pytest.fixture(name="mock_stream_source") -async def mock_stream_source_fixture(): +def mock_stream_source_fixture() -> Generator[AsyncMock]: """Fixture to create an RTSP stream source.""" with patch( "homeassistant.components.camera.Camera.stream_source", @@ -68,7 +69,7 @@ async def mock_stream_source_fixture(): @pytest.fixture(name="mock_hls_stream_source") -async def mock_hls_stream_source_fixture(): +async def mock_hls_stream_source_fixture() -> Generator[AsyncMock]: """Fixture to create an HLS stream source.""" with patch( "homeassistant.components.camera.Camera.stream_source", @@ -85,7 +86,7 @@ async def provide_web_rtc_answer(stream_source: str, offer: str, stream_id: str) @pytest.fixture(name="mock_rtsp_to_web_rtc") -async def mock_rtsp_to_web_rtc_fixture(hass): +def mock_rtsp_to_web_rtc_fixture(hass: HomeAssistant) -> Generator[Mock]: """Fixture that registers a mock rtsp to web_rtc provider.""" mock_provider = Mock(side_effect=provide_web_rtc_answer) unsub = camera.async_register_rtsp_to_web_rtc_provider( @@ -95,7 +96,8 @@ async def mock_rtsp_to_web_rtc_fixture(hass): unsub() -async def test_get_image_from_camera(hass: HomeAssistant, image_mock_url) -> None: +@pytest.mark.usefixtures("image_mock_url") +async def test_get_image_from_camera(hass: HomeAssistant) -> None: """Grab an image from camera entity.""" with patch( @@ -109,9 +111,8 @@ async def test_get_image_from_camera(hass: HomeAssistant, image_mock_url) -> Non assert image.content == b"Test" -async def test_get_image_from_camera_with_width_height( - hass: HomeAssistant, image_mock_url -) -> None: +@pytest.mark.usefixtures("image_mock_url") +async def test_get_image_from_camera_with_width_height(hass: HomeAssistant) -> None: """Grab an image from camera entity with width and height.""" turbo_jpeg = mock_turbo_jpeg( @@ -136,8 +137,9 @@ async def test_get_image_from_camera_with_width_height( assert image.content == b"Test" +@pytest.mark.usefixtures("image_mock_url") async def test_get_image_from_camera_with_width_height_scaled( - hass: HomeAssistant, image_mock_url + hass: HomeAssistant, ) -> None: """Grab an image from camera entity with width and height and scale it.""" @@ -164,9 +166,8 @@ async def test_get_image_from_camera_with_width_height_scaled( assert image.content == EMPTY_8_6_JPEG -async def test_get_image_from_camera_not_jpeg( - hass: HomeAssistant, image_mock_url -) -> None: +@pytest.mark.usefixtures("image_mock_url") +async def test_get_image_from_camera_not_jpeg(hass: HomeAssistant) -> None: """Grab an image from camera entity that we cannot scale.""" turbo_jpeg = mock_turbo_jpeg( @@ -192,8 +193,9 @@ async def test_get_image_from_camera_not_jpeg( assert image.content == b"png" +@pytest.mark.usefixtures("mock_camera") async def test_get_stream_source_from_camera( - hass: HomeAssistant, mock_camera, mock_stream_source + hass: HomeAssistant, mock_stream_source: AsyncMock ) -> None: """Fetch stream source from camera entity.""" @@ -203,9 +205,8 @@ async def test_get_stream_source_from_camera( assert stream_source == STREAM_SOURCE -async def test_get_image_without_exists_camera( - hass: HomeAssistant, image_mock_url -) -> None: +@pytest.mark.usefixtures("image_mock_url") +async def test_get_image_without_exists_camera(hass: HomeAssistant) -> None: """Try to get image without exists camera.""" with ( patch( @@ -217,7 +218,8 @@ async def test_get_image_without_exists_camera( await camera.async_get_image(hass, "camera.demo_camera") -async def test_get_image_with_timeout(hass: HomeAssistant, image_mock_url) -> None: +@pytest.mark.usefixtures("image_mock_url") +async def test_get_image_with_timeout(hass: HomeAssistant) -> None: """Try to get image with timeout.""" with ( patch( @@ -229,7 +231,8 @@ async def test_get_image_with_timeout(hass: HomeAssistant, image_mock_url) -> No await camera.async_get_image(hass, "camera.demo_camera") -async def test_get_image_fails(hass: HomeAssistant, image_mock_url) -> None: +@pytest.mark.usefixtures("image_mock_url") +async def test_get_image_fails(hass: HomeAssistant) -> None: """Try to get image with timeout.""" with ( patch( @@ -241,7 +244,8 @@ async def test_get_image_fails(hass: HomeAssistant, image_mock_url) -> None: await camera.async_get_image(hass, "camera.demo_camera") -async def test_snapshot_service(hass: HomeAssistant, mock_camera) -> None: +@pytest.mark.usefixtures("mock_camera") +async def test_snapshot_service(hass: HomeAssistant) -> None: """Test snapshot service.""" mopen = mock_open() @@ -268,9 +272,8 @@ async def test_snapshot_service(hass: HomeAssistant, mock_camera) -> None: assert mock_write.mock_calls[0][1][0] == b"Test" -async def test_snapshot_service_not_allowed_path( - hass: HomeAssistant, mock_camera -) -> None: +@pytest.mark.usefixtures("mock_camera") +async def test_snapshot_service_not_allowed_path(hass: HomeAssistant) -> None: """Test snapshot service with a not allowed path.""" mopen = mock_open() @@ -292,8 +295,9 @@ async def test_snapshot_service_not_allowed_path( ) +@pytest.mark.usefixtures("mock_camera", "mock_stream") async def test_websocket_stream_no_source( - hass: HomeAssistant, hass_ws_client: WebSocketGenerator, mock_camera, mock_stream + hass: HomeAssistant, hass_ws_client: WebSocketGenerator ) -> None: """Test camera/stream websocket command with camera with no source.""" await async_setup_component(hass, "camera", {}) @@ -311,8 +315,9 @@ async def test_websocket_stream_no_source( assert not msg["success"] +@pytest.mark.usefixtures("mock_camera", "mock_stream") async def test_websocket_camera_stream( - hass: HomeAssistant, hass_ws_client: WebSocketGenerator, mock_camera, mock_stream + hass: HomeAssistant, hass_ws_client: WebSocketGenerator ) -> None: """Test camera/stream websocket command.""" await async_setup_component(hass, "camera", {}) @@ -342,8 +347,9 @@ async def test_websocket_camera_stream( assert msg["result"]["url"][-13:] == "playlist.m3u8" +@pytest.mark.usefixtures("mock_camera") async def test_websocket_get_prefs( - hass: HomeAssistant, hass_ws_client: WebSocketGenerator, mock_camera + hass: HomeAssistant, hass_ws_client: WebSocketGenerator ) -> None: """Test get camera preferences websocket command.""" await async_setup_component(hass, "camera", {}) @@ -359,8 +365,9 @@ async def test_websocket_get_prefs( assert msg["success"] +@pytest.mark.usefixtures("mock_camera") async def test_websocket_update_preload_prefs( - hass: HomeAssistant, hass_ws_client: WebSocketGenerator, mock_camera + hass: HomeAssistant, hass_ws_client: WebSocketGenerator ) -> None: """Test updating camera preferences.""" @@ -396,11 +403,11 @@ async def test_websocket_update_preload_prefs( assert msg["result"][PREF_PRELOAD_STREAM] is True +@pytest.mark.usefixtures("mock_camera") async def test_websocket_update_orientation_prefs( hass: HomeAssistant, hass_ws_client: WebSocketGenerator, entity_registry: er.EntityRegistry, - mock_camera, ) -> None: """Test updating camera preferences.""" await async_setup_component(hass, "homeassistant", {}) @@ -454,9 +461,8 @@ async def test_websocket_update_orientation_prefs( assert msg["result"]["orientation"] == camera.Orientation.ROTATE_180 -async def test_play_stream_service_no_source( - hass: HomeAssistant, mock_camera, mock_stream -) -> None: +@pytest.mark.usefixtures("mock_camera", "mock_stream") +async def test_play_stream_service_no_source(hass: HomeAssistant) -> None: """Test camera play_stream service.""" data = { ATTR_ENTITY_ID: "camera.demo_camera", @@ -469,9 +475,8 @@ async def test_play_stream_service_no_source( ) -async def test_handle_play_stream_service( - hass: HomeAssistant, mock_camera, mock_stream -) -> None: +@pytest.mark.usefixtures("mock_camera", "mock_stream") +async def test_handle_play_stream_service(hass: HomeAssistant) -> None: """Test camera play_stream service.""" await async_process_ha_core_config( hass, @@ -502,7 +507,8 @@ async def test_handle_play_stream_service( assert mock_request_stream.called -async def test_no_preload_stream(hass: HomeAssistant, mock_stream) -> None: +@pytest.mark.usefixtures("mock_stream") +async def test_no_preload_stream(hass: HomeAssistant) -> None: """Test camera preload preference.""" demo_settings = camera.DynamicStreamSettings() with ( @@ -525,7 +531,8 @@ async def test_no_preload_stream(hass: HomeAssistant, mock_stream) -> None: assert not mock_request_stream.called -async def test_preload_stream(hass: HomeAssistant, mock_stream) -> None: +@pytest.mark.usefixtures("mock_stream") +async def test_preload_stream(hass: HomeAssistant) -> None: """Test camera preload preference.""" demo_settings = camera.DynamicStreamSettings(preload_stream=True) with ( @@ -549,7 +556,8 @@ async def test_preload_stream(hass: HomeAssistant, mock_stream) -> None: assert mock_create_stream.called -async def test_record_service_invalid_path(hass: HomeAssistant, mock_camera) -> None: +@pytest.mark.usefixtures("mock_camera") +async def test_record_service_invalid_path(hass: HomeAssistant) -> None: """Test record service with invalid path.""" with ( patch.object(hass.config, "is_allowed_path", return_value=False), @@ -567,7 +575,8 @@ async def test_record_service_invalid_path(hass: HomeAssistant, mock_camera) -> ) -async def test_record_service(hass: HomeAssistant, mock_camera, mock_stream) -> None: +@pytest.mark.usefixtures("mock_camera", "mock_stream") +async def test_record_service(hass: HomeAssistant) -> None: """Test record service.""" with ( patch( @@ -591,9 +600,8 @@ async def test_record_service(hass: HomeAssistant, mock_camera, mock_stream) -> assert mock_record.called -async def test_camera_proxy_stream( - hass: HomeAssistant, mock_camera, hass_client: ClientSessionGenerator -) -> None: +@pytest.mark.usefixtures("mock_camera") +async def test_camera_proxy_stream(hass_client: ClientSessionGenerator) -> None: """Test record service.""" client = await hass_client() @@ -611,10 +619,9 @@ async def test_camera_proxy_stream( assert response.status == HTTPStatus.BAD_GATEWAY +@pytest.mark.usefixtures("mock_camera_web_rtc") async def test_websocket_web_rtc_offer( - hass: HomeAssistant, - hass_ws_client: WebSocketGenerator, - mock_camera_web_rtc, + hass: HomeAssistant, hass_ws_client: WebSocketGenerator ) -> None: """Test initiating a WebRTC stream with offer and answer.""" client = await hass_ws_client(hass) @@ -634,10 +641,9 @@ async def test_websocket_web_rtc_offer( assert response["result"]["answer"] == WEBRTC_ANSWER +@pytest.mark.usefixtures("mock_camera_web_rtc") async def test_websocket_web_rtc_offer_invalid_entity( - hass: HomeAssistant, - hass_ws_client: WebSocketGenerator, - mock_camera_web_rtc, + hass: HomeAssistant, hass_ws_client: WebSocketGenerator ) -> None: """Test WebRTC with a camera entity that does not exist.""" client = await hass_ws_client(hass) @@ -656,10 +662,9 @@ async def test_websocket_web_rtc_offer_invalid_entity( assert not response["success"] +@pytest.mark.usefixtures("mock_camera_web_rtc") async def test_websocket_web_rtc_offer_missing_offer( - hass: HomeAssistant, - hass_ws_client: WebSocketGenerator, - mock_camera_web_rtc, + hass: HomeAssistant, hass_ws_client: WebSocketGenerator ) -> None: """Test WebRTC stream with missing required fields.""" client = await hass_ws_client(hass) @@ -678,10 +683,9 @@ async def test_websocket_web_rtc_offer_missing_offer( assert response["error"]["code"] == "invalid_format" +@pytest.mark.usefixtures("mock_camera_web_rtc") async def test_websocket_web_rtc_offer_failure( - hass: HomeAssistant, - hass_ws_client: WebSocketGenerator, - mock_camera_web_rtc, + hass: HomeAssistant, hass_ws_client: WebSocketGenerator ) -> None: """Test WebRTC stream that fails handling the offer.""" client = await hass_ws_client(hass) @@ -707,10 +711,9 @@ async def test_websocket_web_rtc_offer_failure( assert response["error"]["message"] == "offer failed" +@pytest.mark.usefixtures("mock_camera_web_rtc") async def test_websocket_web_rtc_offer_timeout( - hass: HomeAssistant, - hass_ws_client: WebSocketGenerator, - mock_camera_web_rtc, + hass: HomeAssistant, hass_ws_client: WebSocketGenerator ) -> None: """Test WebRTC stream with timeout handling the offer.""" client = await hass_ws_client(hass) @@ -736,10 +739,9 @@ async def test_websocket_web_rtc_offer_timeout( assert response["error"]["message"] == "Timeout handling WebRTC offer" +@pytest.mark.usefixtures("mock_camera") async def test_websocket_web_rtc_offer_invalid_stream_type( - hass: HomeAssistant, - hass_ws_client: WebSocketGenerator, - mock_camera, + hass: HomeAssistant, hass_ws_client: WebSocketGenerator ) -> None: """Test WebRTC initiating for a camera with a different stream_type.""" client = await hass_ws_client(hass) @@ -759,17 +761,17 @@ async def test_websocket_web_rtc_offer_invalid_stream_type( assert response["error"]["code"] == "web_rtc_offer_failed" -async def test_state_streaming( - hass: HomeAssistant, hass_ws_client: WebSocketGenerator, mock_camera -) -> None: +@pytest.mark.usefixtures("mock_camera") +async def test_state_streaming(hass: HomeAssistant) -> None: """Camera state.""" demo_camera = hass.states.get("camera.demo_camera") assert demo_camera is not None assert demo_camera.state == camera.STATE_STREAMING +@pytest.mark.usefixtures("mock_camera", "mock_stream") async def test_stream_unavailable( - hass: HomeAssistant, hass_ws_client: WebSocketGenerator, mock_camera, mock_stream + hass: HomeAssistant, hass_ws_client: WebSocketGenerator ) -> None: """Camera state.""" await async_setup_component(hass, "camera", {}) @@ -820,12 +822,11 @@ async def test_stream_unavailable( assert demo_camera.state == camera.STATE_STREAMING +@pytest.mark.usefixtures("mock_camera", "mock_stream_source") async def test_rtsp_to_web_rtc_offer( hass: HomeAssistant, hass_ws_client: WebSocketGenerator, - mock_camera, - mock_stream_source, - mock_rtsp_to_web_rtc, + mock_rtsp_to_web_rtc: Mock, ) -> None: """Test creating a web_rtc offer from an rstp provider.""" client = await hass_ws_client(hass) @@ -848,12 +849,13 @@ async def test_rtsp_to_web_rtc_offer( assert mock_rtsp_to_web_rtc.called +@pytest.mark.usefixtures( + "mock_camera", + "mock_hls_stream_source", # Not an RTSP stream source + "mock_rtsp_to_web_rtc", +) async def test_unsupported_rtsp_to_web_rtc_stream_type( - hass: HomeAssistant, - hass_ws_client: WebSocketGenerator, - mock_camera, - mock_hls_stream_source, # Not an RTSP stream source - mock_rtsp_to_web_rtc, + hass: HomeAssistant, hass_ws_client: WebSocketGenerator ) -> None: """Test rtsp-to-webrtc is not registered for non-RTSP streams.""" client = await hass_ws_client(hass) @@ -873,11 +875,9 @@ async def test_unsupported_rtsp_to_web_rtc_stream_type( assert not response["success"] +@pytest.mark.usefixtures("mock_camera", "mock_stream_source") async def test_rtsp_to_web_rtc_provider_unregistered( - hass: HomeAssistant, - hass_ws_client: WebSocketGenerator, - mock_camera, - mock_stream_source, + hass: HomeAssistant, hass_ws_client: WebSocketGenerator ) -> None: """Test creating a web_rtc offer from an rstp provider.""" mock_provider = Mock(side_effect=provide_web_rtc_answer) @@ -924,11 +924,9 @@ async def test_rtsp_to_web_rtc_provider_unregistered( assert not mock_provider.called +@pytest.mark.usefixtures("mock_camera", "mock_stream_source") async def test_rtsp_to_web_rtc_offer_not_accepted( - hass: HomeAssistant, - hass_ws_client: WebSocketGenerator, - mock_camera, - mock_stream_source, + hass: HomeAssistant, hass_ws_client: WebSocketGenerator ) -> None: """Test a provider that can't satisfy the rtsp to webrtc offer.""" @@ -962,10 +960,9 @@ async def test_rtsp_to_web_rtc_offer_not_accepted( unsub() +@pytest.mark.usefixtures("mock_camera") async def test_use_stream_for_stills( - hass: HomeAssistant, - hass_client: ClientSessionGenerator, - mock_camera, + hass: HomeAssistant, hass_client: ClientSessionGenerator ) -> None: """Test that the component can grab images from stream.""" @@ -1080,9 +1077,8 @@ def test_deprecated_supported_features_ints(caplog: pytest.LogCaptureFixture) -> assert "is using deprecated supported features values" not in caplog.text -async def test_entity_picture_url_changes_on_token_update( - hass: HomeAssistant, mock_camera -) -> None: +@pytest.mark.usefixtures("mock_camera") +async def test_entity_picture_url_changes_on_token_update(hass: HomeAssistant) -> None: """Test the token is rotated and entity entity picture cache is cleared.""" await async_setup_component(hass, "camera", {}) await hass.async_block_till_done() diff --git a/tests/components/camera/test_media_source.py b/tests/components/camera/test_media_source.py index 3dd0399a710..0780ecc2a9c 100644 --- a/tests/components/camera/test_media_source.py +++ b/tests/components/camera/test_media_source.py @@ -12,14 +12,13 @@ from homeassistant.setup import async_setup_component @pytest.fixture(autouse=True) -async def setup_media_source(hass): +async def setup_media_source(hass: HomeAssistant) -> None: """Set up media source.""" assert await async_setup_component(hass, "media_source", {}) -async def test_device_with_device( - hass: HomeAssistant, mock_camera_with_device, mock_camera -) -> None: +@pytest.mark.usefixtures("mock_camera_with_device", "mock_camera") +async def test_device_with_device(hass: HomeAssistant) -> None: """Test browsing when camera has a device and a name.""" item = await media_source.async_browse_media(hass, "media-source://camera") assert item.not_shown == 2 @@ -27,9 +26,8 @@ async def test_device_with_device( assert item.children[0].title == "Test Camera Device Demo camera without stream" -async def test_device_with_no_name( - hass: HomeAssistant, mock_camera_with_no_name, mock_camera -) -> None: +@pytest.mark.usefixtures("mock_camera_with_no_name", "mock_camera") +async def test_device_with_no_name(hass: HomeAssistant) -> None: """Test browsing when camera has device and name == None.""" item = await media_source.async_browse_media(hass, "media-source://camera") assert item.not_shown == 2 @@ -37,7 +35,8 @@ async def test_device_with_no_name( assert item.children[0].title == "Test Camera Device Demo camera without stream" -async def test_browsing_hls(hass: HomeAssistant, mock_camera_hls) -> None: +@pytest.mark.usefixtures("mock_camera_hls") +async def test_browsing_hls(hass: HomeAssistant) -> None: """Test browsing HLS camera media source.""" item = await media_source.async_browse_media(hass, "media-source://camera") assert item is not None @@ -54,7 +53,8 @@ async def test_browsing_hls(hass: HomeAssistant, mock_camera_hls) -> None: assert item.children[0].media_content_type == FORMAT_CONTENT_TYPE["hls"] -async def test_browsing_mjpeg(hass: HomeAssistant, mock_camera) -> None: +@pytest.mark.usefixtures("mock_camera") +async def test_browsing_mjpeg(hass: HomeAssistant) -> None: """Test browsing MJPEG camera media source.""" item = await media_source.async_browse_media(hass, "media-source://camera") assert item is not None @@ -65,7 +65,8 @@ async def test_browsing_mjpeg(hass: HomeAssistant, mock_camera) -> None: assert item.children[0].title == "Demo camera without stream" -async def test_browsing_web_rtc(hass: HomeAssistant, mock_camera_web_rtc) -> None: +@pytest.mark.usefixtures("mock_camera_web_rtc") +async def test_browsing_web_rtc(hass: HomeAssistant) -> None: """Test browsing WebRTC camera media source.""" # 3 cameras: # one only supports WebRTC (no stream source) @@ -90,7 +91,8 @@ async def test_browsing_web_rtc(hass: HomeAssistant, mock_camera_web_rtc) -> Non assert item.children[0].media_content_type == FORMAT_CONTENT_TYPE["hls"] -async def test_resolving(hass: HomeAssistant, mock_camera_hls) -> None: +@pytest.mark.usefixtures("mock_camera_hls") +async def test_resolving(hass: HomeAssistant) -> None: """Test resolving.""" # Adding stream enables HLS camera hass.config.components.add("stream") @@ -107,7 +109,8 @@ async def test_resolving(hass: HomeAssistant, mock_camera_hls) -> None: assert item.mime_type == FORMAT_CONTENT_TYPE["hls"] -async def test_resolving_errors(hass: HomeAssistant, mock_camera_hls) -> None: +@pytest.mark.usefixtures("mock_camera_hls") +async def test_resolving_errors(hass: HomeAssistant) -> None: """Test resolving.""" with pytest.raises(media_source.Unresolvable) as exc_info: From a5cde4b32bc229f41dd5601988dbcb1681dd1974 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 10 Jun 2024 09:40:54 +0200 Subject: [PATCH 0449/1445] Use device_registry fixture in webostv tests (#119269) --- .../components/webostv/test_device_trigger.py | 24 +++++++++++-------- tests/components/webostv/test_trigger.py | 10 ++++---- 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/tests/components/webostv/test_device_trigger.py b/tests/components/webostv/test_device_trigger.py index 1349c0670e4..29c75d4440b 100644 --- a/tests/components/webostv/test_device_trigger.py +++ b/tests/components/webostv/test_device_trigger.py @@ -11,7 +11,7 @@ from homeassistant.components.webostv import DOMAIN, device_trigger from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.device_registry import async_get as get_dev_reg +from homeassistant.helpers import device_registry as dr from homeassistant.setup import async_setup_component from . import setup_webostv @@ -20,12 +20,13 @@ from .const import ENTITY_ID, FAKE_UUID from tests.common import MockConfigEntry, async_get_device_automations -async def test_get_triggers(hass: HomeAssistant, client) -> None: +async def test_get_triggers( + hass: HomeAssistant, device_registry: dr.DeviceRegistry, client +) -> None: """Test we get the expected triggers.""" await setup_webostv(hass) - device_reg = get_dev_reg(hass) - device = device_reg.async_get_device(identifiers={(DOMAIN, FAKE_UUID)}) + device = device_registry.async_get_device(identifiers={(DOMAIN, FAKE_UUID)}) turn_on_trigger = { "platform": "device", @@ -42,13 +43,15 @@ async def test_get_triggers(hass: HomeAssistant, client) -> None: async def test_if_fires_on_turn_on_request( - hass: HomeAssistant, calls: list[ServiceCall], client + hass: HomeAssistant, + calls: list[ServiceCall], + device_registry: dr.DeviceRegistry, + client, ) -> None: """Test for turn_on and turn_off triggers firing.""" await setup_webostv(hass) - device_reg = get_dev_reg(hass) - device = device_reg.async_get_device(identifiers={(DOMAIN, FAKE_UUID)}) + device = device_registry.async_get_device(identifiers={(DOMAIN, FAKE_UUID)}) assert await async_setup_component( hass, @@ -101,7 +104,9 @@ async def test_if_fires_on_turn_on_request( assert calls[1].data["id"] == 0 -async def test_failure_scenarios(hass: HomeAssistant, client) -> None: +async def test_failure_scenarios( + hass: HomeAssistant, device_registry: dr.DeviceRegistry, client +) -> None: """Test failure scenarios.""" await setup_webostv(hass) @@ -125,9 +130,8 @@ async def test_failure_scenarios(hass: HomeAssistant, client) -> None: entry = MockConfigEntry(domain="fake", state=ConfigEntryState.LOADED, data={}) entry.add_to_hass(hass) - device_reg = get_dev_reg(hass) - device = device_reg.async_get_or_create( + device = device_registry.async_get_or_create( config_entry_id=entry.entry_id, identifiers={("fake", "fake")} ) diff --git a/tests/components/webostv/test_trigger.py b/tests/components/webostv/test_trigger.py index 05fde697752..918666cf4bf 100644 --- a/tests/components/webostv/test_trigger.py +++ b/tests/components/webostv/test_trigger.py @@ -9,7 +9,7 @@ from homeassistant.components.webostv import DOMAIN from homeassistant.const import SERVICE_RELOAD from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.device_registry import async_get as get_dev_reg +from homeassistant.helpers import device_registry as dr from homeassistant.setup import async_setup_component from . import setup_webostv @@ -19,13 +19,15 @@ from tests.common import MockEntity, MockEntityPlatform async def test_webostv_turn_on_trigger_device_id( - hass: HomeAssistant, calls: list[ServiceCall], client + hass: HomeAssistant, + calls: list[ServiceCall], + device_registry: dr.DeviceRegistry, + client, ) -> None: """Test for turn_on triggers by device_id firing.""" await setup_webostv(hass) - device_reg = get_dev_reg(hass) - device = device_reg.async_get_device(identifiers={(DOMAIN, FAKE_UUID)}) + device = device_registry.async_get_device(identifiers={(DOMAIN, FAKE_UUID)}) assert await async_setup_component( hass, From 42f3dd636f57c1f503c95ca9c24787fc4601d8d9 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 10 Jun 2024 10:05:47 +0200 Subject: [PATCH 0450/1445] Use service_calls fixture in bthome tests (#119268) --- tests/components/bthome/test_device_trigger.py | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/tests/components/bthome/test_device_trigger.py b/tests/components/bthome/test_device_trigger.py index 7022726412a..251fb52bda6 100644 --- a/tests/components/bthome/test_device_trigger.py +++ b/tests/components/bthome/test_device_trigger.py @@ -1,7 +1,5 @@ """Test BTHome BLE events.""" -import pytest - from homeassistant.components import automation from homeassistant.components.bluetooth.const import DOMAIN as BLUETOOTH_DOMAIN from homeassistant.components.bthome.const import CONF_SUBTYPE, DOMAIN @@ -20,7 +18,6 @@ from tests.common import ( MockConfigEntry, async_capture_events, async_get_device_automations, - async_mock_service, ) from tests.components.bluetooth import inject_bluetooth_service_info_bleak @@ -31,12 +28,6 @@ def get_device_id(mac: str) -> tuple[str, str]: return (BLUETOOTH_DOMAIN, mac) -@pytest.fixture -def calls(hass: HomeAssistant) -> list[ServiceCall]: - """Track calls to a mock service.""" - return async_mock_service(hass, "test", "automation") - - async def _async_setup_bthome_device(hass, mac: str): config_entry = MockConfigEntry( domain=DOMAIN, @@ -230,7 +221,7 @@ async def test_get_triggers_for_invalid_device_id(hass: HomeAssistant) -> None: async def test_if_fires_on_motion_detected( - hass: HomeAssistant, calls: list[ServiceCall] + hass: HomeAssistant, service_calls: list[ServiceCall] ) -> None: """Test for motion event trigger firing.""" mac = "DE:70:E8:B2:39:0C" @@ -278,8 +269,8 @@ async def test_if_fires_on_motion_detected( ) await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["some"] == "test_trigger_button_long_press" + assert len(service_calls) == 1 + assert service_calls[0].data["some"] == "test_trigger_button_long_press" assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() From e114e6f8627cbb3d325b479d8c7ce65e9b818896 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Mon, 10 Jun 2024 10:07:38 +0200 Subject: [PATCH 0451/1445] Improve incomfort boiler state strings (#119270) --- homeassistant/components/incomfort/strings.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/incomfort/strings.json b/homeassistant/components/incomfort/strings.json index 67a736d5408..f74dd4f3202 100644 --- a/homeassistant/components/incomfort/strings.json +++ b/homeassistant/components/incomfort/strings.json @@ -70,16 +70,16 @@ "unknown": "Unknown", "opentherm": "OpenTherm", "boiler_ext": "Boiler external", - "frost": "Frost", + "frost": "Frost protection", "central_heating_rf": "Central heating rf", - "tapwater_int": "Tapwater internal", + "tapwater_int": "Tap water internal", "sensor_test": "Sensor test", "central_heating": "Central heating", - "standby": "Standby", - "postrun_boyler": "Postrun boiler", + "standby": "Stand-by", + "postrun_boyler": "Post run boiler", "service": "Service", - "tapwater": "Tapwater", - "postrun_ch": "Postrun central heating", + "tapwater": "Tap water", + "postrun_ch": "Post run central heating", "boiler_int": "Boiler internal", "buffer": "Buffer", "sensor_fault_after_self_check_e0": "Sensor fault after self check", From e818de1da87aa6e87f3ec7abd3d7d597659d8dad Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 10 Jun 2024 10:44:00 +0200 Subject: [PATCH 0452/1445] Use service_calls fixture in scaffold (#119266) --- .../tests/test_device_condition.py | 23 +++++-------------- .../tests/test_device_trigger.py | 23 +++++-------------- 2 files changed, 12 insertions(+), 34 deletions(-) diff --git a/script/scaffold/templates/device_condition/tests/test_device_condition.py b/script/scaffold/templates/device_condition/tests/test_device_condition.py index ad6d527bd65..5a0e7122571 100644 --- a/script/scaffold/templates/device_condition/tests/test_device_condition.py +++ b/script/scaffold/templates/device_condition/tests/test_device_condition.py @@ -2,7 +2,6 @@ from __future__ import annotations -import pytest from pytest_unordered import unordered from homeassistant.components import automation @@ -13,17 +12,7 @@ from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.setup import async_setup_component -from tests.common import ( - MockConfigEntry, - async_get_device_automations, - async_mock_service, -) - - -@pytest.fixture -def calls(hass: HomeAssistant) -> list[ServiceCall]: - """Track calls to a mock service.""" - return async_mock_service(hass, "test", "automation") +from tests.common import MockConfigEntry, async_get_device_automations async def test_get_conditions( @@ -63,7 +52,7 @@ async def test_get_conditions( assert conditions == unordered(expected_conditions) -async def test_if_state(hass: HomeAssistant, calls: list[ServiceCall]) -> None: +async def test_if_state(hass: HomeAssistant, service_calls: list[ServiceCall]) -> None: """Test for turn_on and turn_off conditions.""" hass.states.async_set("NEW_DOMAIN.entity", STATE_ON) @@ -114,12 +103,12 @@ async def test_if_state(hass: HomeAssistant, calls: list[ServiceCall]) -> None: hass.bus.async_fire("test_event1") hass.bus.async_fire("test_event2") await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["some"] == "is_on - event - test_event1" + assert len(service_calls) == 1 + assert service_calls[0].data["some"] == "is_on - event - test_event1" hass.states.async_set("NEW_DOMAIN.entity", STATE_OFF) hass.bus.async_fire("test_event1") hass.bus.async_fire("test_event2") await hass.async_block_till_done() - assert len(calls) == 2 - assert calls[1].data["some"] == "is_off - event - test_event2" + assert len(service_calls) == 2 + assert service_calls[1].data["some"] == "is_off - event - test_event2" diff --git a/script/scaffold/templates/device_trigger/tests/test_device_trigger.py b/script/scaffold/templates/device_trigger/tests/test_device_trigger.py index 54b202c978c..7e4f88261bc 100644 --- a/script/scaffold/templates/device_trigger/tests/test_device_trigger.py +++ b/script/scaffold/templates/device_trigger/tests/test_device_trigger.py @@ -1,6 +1,5 @@ """The tests for NEW_NAME device triggers.""" -import pytest from pytest_unordered import unordered from homeassistant.components import automation @@ -11,17 +10,7 @@ from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.setup import async_setup_component -from tests.common import ( - MockConfigEntry, - async_get_device_automations, - async_mock_service, -) - - -@pytest.fixture -def calls(hass: HomeAssistant) -> list[ServiceCall]: - """Track calls to a mock service.""" - return async_mock_service(hass, "test", "automation") +from tests.common import MockConfigEntry, async_get_device_automations async def test_get_triggers( @@ -62,7 +51,7 @@ async def test_get_triggers( async def test_if_fires_on_state_change( - hass: HomeAssistant, calls: list[ServiceCall] + hass: HomeAssistant, service_calls: list[ServiceCall] ) -> None: """Test for turn_on and turn_off triggers firing.""" hass.states.async_set("NEW_DOMAIN.entity", STATE_OFF) @@ -119,15 +108,15 @@ async def test_if_fires_on_state_change( # Fake that the entity is turning on. hass.states.async_set("NEW_DOMAIN.entity", STATE_ON) await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data[ + assert len(service_calls) == 1 + assert service_calls[0].data[ "some" ] == "turn_on - device - {} - off - on - None - 0".format("NEW_DOMAIN.entity") # Fake that the entity is turning off. hass.states.async_set("NEW_DOMAIN.entity", STATE_OFF) await hass.async_block_till_done() - assert len(calls) == 2 - assert calls[1].data[ + assert len(service_calls) == 2 + assert service_calls[1].data[ "some" ] == "turn_off - device - {} - on - off - None - 0".format("NEW_DOMAIN.entity") From b8e57f617413246ecf1fb3aee05e9e928268dfbf Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 10 Jun 2024 12:52:34 +0200 Subject: [PATCH 0453/1445] Use relative imports in tests [a-i] (#119280) --- tests/components/airgradient/test_init.py | 3 ++- tests/components/airthings_ble/test_sensor.py | 3 ++- tests/components/alarm_control_panel/test_device_action.py | 3 ++- tests/components/analytics_insights/test_config_flow.py | 3 ++- tests/components/analytics_insights/test_init.py | 3 ++- tests/components/aosmith/test_config_flow.py | 3 ++- tests/components/apcupsd/test_diagnostics.py | 3 ++- tests/components/aquacell/conftest.py | 3 ++- tests/components/aquacell/test_config_flow.py | 3 ++- tests/components/aquacell/test_init.py | 3 ++- tests/components/aquacell/test_sensor.py | 3 ++- tests/components/aurora/test_config_flow.py | 3 ++- tests/components/bmw_connected_drive/test_sensor.py | 2 +- tests/components/bthome/test_device_trigger.py | 2 +- tests/components/co2signal/conftest.py | 3 ++- tests/components/cover/test_device_action.py | 3 ++- tests/components/cover/test_device_condition.py | 3 ++- tests/components/cover/test_device_trigger.py | 3 ++- tests/components/cover/test_init.py | 3 ++- tests/components/date/test_init.py | 3 ++- tests/components/datetime/test_init.py | 3 ++- tests/components/discovergy/conftest.py | 3 ++- tests/components/ecobee/test_climate.py | 4 ++-- tests/components/ecobee/test_switch.py | 3 +-- tests/components/fan/test_init.py | 3 ++- tests/components/flexit_bacnet/test_binary_sensor.py | 3 ++- tests/components/flexit_bacnet/test_climate.py | 3 ++- tests/components/flexit_bacnet/test_init.py | 3 ++- tests/components/flexit_bacnet/test_number.py | 3 ++- tests/components/flexit_bacnet/test_sensor.py | 3 ++- tests/components/flexit_bacnet/test_switch.py | 3 ++- tests/components/geo_json_events/test_geo_location.py | 5 +++-- tests/components/geo_json_events/test_init.py | 3 ++- tests/components/hue/conftest.py | 3 ++- tests/components/ipma/test_config_flow.py | 2 +- 35 files changed, 67 insertions(+), 38 deletions(-) diff --git a/tests/components/airgradient/test_init.py b/tests/components/airgradient/test_init.py index 463cb47f144..273f425f4fc 100644 --- a/tests/components/airgradient/test_init.py +++ b/tests/components/airgradient/test_init.py @@ -8,8 +8,9 @@ from homeassistant.components.airgradient import DOMAIN from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr +from . import setup_integration + from tests.common import MockConfigEntry -from tests.components.airgradient import setup_integration async def test_device_info( diff --git a/tests/components/airthings_ble/test_sensor.py b/tests/components/airthings_ble/test_sensor.py index abbc373ab2e..a8acdf7ec7b 100644 --- a/tests/components/airthings_ble/test_sensor.py +++ b/tests/components/airthings_ble/test_sensor.py @@ -7,7 +7,7 @@ from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er -from tests.components.airthings_ble import ( +from . import ( CO2_V1, CO2_V2, HUMIDITY_V2, @@ -21,6 +21,7 @@ from tests.components.airthings_ble import ( create_entry, patch_airthings_device_update, ) + from tests.components.bluetooth import inject_bluetooth_service_info _LOGGER = logging.getLogger(__name__) diff --git a/tests/components/alarm_control_panel/test_device_action.py b/tests/components/alarm_control_panel/test_device_action.py index 5d142ab277b..04c0e3b045b 100644 --- a/tests/components/alarm_control_panel/test_device_action.py +++ b/tests/components/alarm_control_panel/test_device_action.py @@ -24,13 +24,14 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.setup import async_setup_component +from .common import MockAlarm + from tests.common import ( MockConfigEntry, async_get_device_automation_capabilities, async_get_device_automations, setup_test_component_platform, ) -from tests.components.alarm_control_panel.common import MockAlarm @pytest.fixture(autouse=True, name="stub_blueprint_populate") diff --git a/tests/components/analytics_insights/test_config_flow.py b/tests/components/analytics_insights/test_config_flow.py index 6bfd0e798ce..0c9d4c074f8 100644 --- a/tests/components/analytics_insights/test_config_flow.py +++ b/tests/components/analytics_insights/test_config_flow.py @@ -15,8 +15,9 @@ from homeassistant.config_entries import SOURCE_USER from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from . import setup_integration + from tests.common import MockConfigEntry -from tests.components.analytics_insights import setup_integration @pytest.mark.parametrize( diff --git a/tests/components/analytics_insights/test_init.py b/tests/components/analytics_insights/test_init.py index 8543a02c025..b75266b45ca 100644 --- a/tests/components/analytics_insights/test_init.py +++ b/tests/components/analytics_insights/test_init.py @@ -8,8 +8,9 @@ from homeassistant.components.analytics_insights.const import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant +from . import setup_integration + from tests.common import MockConfigEntry -from tests.components.analytics_insights import setup_integration async def test_load_unload_entry( diff --git a/tests/components/aosmith/test_config_flow.py b/tests/components/aosmith/test_config_flow.py index 991d4129392..0027986f3d1 100644 --- a/tests/components/aosmith/test_config_flow.py +++ b/tests/components/aosmith/test_config_flow.py @@ -18,8 +18,9 @@ from homeassistant.const import CONF_EMAIL, CONF_PASSWORD from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from .conftest import FIXTURE_USER_INPUT + from tests.common import MockConfigEntry, async_fire_time_changed -from tests.components.aosmith.conftest import FIXTURE_USER_INPUT async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: diff --git a/tests/components/apcupsd/test_diagnostics.py b/tests/components/apcupsd/test_diagnostics.py index 5dfce28a989..67946a928f8 100644 --- a/tests/components/apcupsd/test_diagnostics.py +++ b/tests/components/apcupsd/test_diagnostics.py @@ -4,7 +4,8 @@ from syrupy.assertion import SnapshotAssertion from homeassistant.core import HomeAssistant -from tests.components.apcupsd import async_init_integration +from . import async_init_integration + from tests.components.diagnostics import get_diagnostics_for_config_entry from tests.typing import ClientSessionGenerator diff --git a/tests/components/aquacell/conftest.py b/tests/components/aquacell/conftest.py index 0d0949aee2a..db27f51dc03 100644 --- a/tests/components/aquacell/conftest.py +++ b/tests/components/aquacell/conftest.py @@ -13,8 +13,9 @@ from homeassistant.components.aquacell.const import ( ) from homeassistant.const import CONF_EMAIL +from . import TEST_CONFIG_ENTRY + from tests.common import MockConfigEntry, load_json_array_fixture -from tests.components.aquacell import TEST_CONFIG_ENTRY @pytest.fixture diff --git a/tests/components/aquacell/test_config_flow.py b/tests/components/aquacell/test_config_flow.py index 7e348c47c78..b6bcb82293c 100644 --- a/tests/components/aquacell/test_config_flow.py +++ b/tests/components/aquacell/test_config_flow.py @@ -11,8 +11,9 @@ from homeassistant.const import CONF_EMAIL, CONF_PASSWORD from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from . import TEST_CONFIG_ENTRY, TEST_USER_INPUT + from tests.common import MockConfigEntry -from tests.components.aquacell import TEST_CONFIG_ENTRY, TEST_USER_INPUT async def test_config_flow_already_configured(hass: HomeAssistant) -> None: diff --git a/tests/components/aquacell/test_init.py b/tests/components/aquacell/test_init.py index 215b50719be..a70d077e180 100644 --- a/tests/components/aquacell/test_init.py +++ b/tests/components/aquacell/test_init.py @@ -16,8 +16,9 @@ from homeassistant.components.aquacell.const import ( from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant +from . import setup_integration + from tests.common import MockConfigEntry -from tests.components.aquacell import setup_integration async def test_load_unload_entry( diff --git a/tests/components/aquacell/test_sensor.py b/tests/components/aquacell/test_sensor.py index 8c52c3caa1f..0c59dcc40e9 100644 --- a/tests/components/aquacell/test_sensor.py +++ b/tests/components/aquacell/test_sensor.py @@ -9,8 +9,9 @@ from syrupy import SnapshotAssertion from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er +from . import setup_integration + from tests.common import MockConfigEntry, snapshot_platform -from tests.components.aquacell import setup_integration async def test_sensors( diff --git a/tests/components/aurora/test_config_flow.py b/tests/components/aurora/test_config_flow.py index e521ba32884..710f4d607d2 100644 --- a/tests/components/aurora/test_config_flow.py +++ b/tests/components/aurora/test_config_flow.py @@ -11,8 +11,9 @@ from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from . import setup_integration + from tests.common import MockConfigEntry -from tests.components.aurora import setup_integration DATA = { CONF_LATITUDE: -10, diff --git a/tests/components/bmw_connected_drive/test_sensor.py b/tests/components/bmw_connected_drive/test_sensor.py index c89df2caa7a..6607bed280d 100644 --- a/tests/components/bmw_connected_drive/test_sensor.py +++ b/tests/components/bmw_connected_drive/test_sensor.py @@ -7,7 +7,7 @@ from syrupy.assertion import SnapshotAssertion from homeassistant.components.bmw_connected_drive import DOMAIN as BMW_DOMAIN from homeassistant.components.bmw_connected_drive.sensor import SENSOR_TYPES -from homeassistant.components.sensor.const import SensorDeviceClass +from homeassistant.components.sensor import SensorDeviceClass from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er diff --git a/tests/components/bthome/test_device_trigger.py b/tests/components/bthome/test_device_trigger.py index 251fb52bda6..496f191c434 100644 --- a/tests/components/bthome/test_device_trigger.py +++ b/tests/components/bthome/test_device_trigger.py @@ -1,7 +1,7 @@ """Test BTHome BLE events.""" from homeassistant.components import automation -from homeassistant.components.bluetooth.const import DOMAIN as BLUETOOTH_DOMAIN +from homeassistant.components.bluetooth import DOMAIN as BLUETOOTH_DOMAIN from homeassistant.components.bthome.const import CONF_SUBTYPE, DOMAIN from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_PLATFORM, CONF_TYPE diff --git a/tests/components/co2signal/conftest.py b/tests/components/co2signal/conftest.py index 8d71672dcac..04ab6db7464 100644 --- a/tests/components/co2signal/conftest.py +++ b/tests/components/co2signal/conftest.py @@ -10,8 +10,9 @@ from homeassistant.const import CONF_API_KEY from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component +from . import VALID_RESPONSE + from tests.common import MockConfigEntry -from tests.components.co2signal import VALID_RESPONSE @pytest.fixture(name="electricity_maps") diff --git a/tests/components/cover/test_device_action.py b/tests/components/cover/test_device_action.py index e70e8d3a70f..d38f02d9c6e 100644 --- a/tests/components/cover/test_device_action.py +++ b/tests/components/cover/test_device_action.py @@ -12,6 +12,8 @@ from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.entity_registry import RegistryEntryHider from homeassistant.setup import async_setup_component +from .common import MockCover + from tests.common import ( MockConfigEntry, async_get_device_automation_capabilities, @@ -19,7 +21,6 @@ from tests.common import ( async_mock_service, setup_test_component_platform, ) -from tests.components.cover.common import MockCover @pytest.fixture(autouse=True, name="stub_blueprint_populate") diff --git a/tests/components/cover/test_device_condition.py b/tests/components/cover/test_device_condition.py index f1e31004cdc..9e5e5db1862 100644 --- a/tests/components/cover/test_device_condition.py +++ b/tests/components/cover/test_device_condition.py @@ -20,6 +20,8 @@ from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.entity_registry import RegistryEntryHider from homeassistant.setup import async_setup_component +from .common import MockCover + from tests.common import ( MockConfigEntry, async_get_device_automation_capabilities, @@ -27,7 +29,6 @@ from tests.common import ( async_mock_service, setup_test_component_platform, ) -from tests.components.cover.common import MockCover @pytest.fixture(autouse=True, name="stub_blueprint_populate") diff --git a/tests/components/cover/test_device_trigger.py b/tests/components/cover/test_device_trigger.py index 61a443f28ac..1ad84e52c0c 100644 --- a/tests/components/cover/test_device_trigger.py +++ b/tests/components/cover/test_device_trigger.py @@ -22,6 +22,8 @@ from homeassistant.helpers.entity_registry import RegistryEntryHider from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util +from .common import MockCover + from tests.common import ( MockConfigEntry, async_fire_time_changed, @@ -30,7 +32,6 @@ from tests.common import ( async_mock_service, setup_test_component_platform, ) -from tests.components.cover.common import MockCover @pytest.fixture(autouse=True, name="stub_blueprint_populate") diff --git a/tests/components/cover/test_init.py b/tests/components/cover/test_init.py index 5ccd948cc6b..7da6c6efe21 100644 --- a/tests/components/cover/test_init.py +++ b/tests/components/cover/test_init.py @@ -17,12 +17,13 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component +from .common import MockCover + from tests.common import ( help_test_all, import_and_test_deprecated_constant_enum, setup_test_component_platform, ) -from tests.components.cover.common import MockCover async def test_services( diff --git a/tests/components/date/test_init.py b/tests/components/date/test_init.py index a6c517c7b9e..c7d2949d326 100644 --- a/tests/components/date/test_init.py +++ b/tests/components/date/test_init.py @@ -12,8 +12,9 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component +from .common import MockDateEntity + from tests.common import setup_test_component_platform -from tests.components.date.common import MockDateEntity async def test_date(hass: HomeAssistant) -> None: diff --git a/tests/components/datetime/test_init.py b/tests/components/datetime/test_init.py index ca866ec4364..6d90bbf746d 100644 --- a/tests/components/datetime/test_init.py +++ b/tests/components/datetime/test_init.py @@ -10,8 +10,9 @@ from homeassistant.const import ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, CONF_PLATFOR from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component +from .common import MockDateTimeEntity + from tests.common import setup_test_component_platform -from tests.components.datetime.common import MockDateTimeEntity DEFAULT_VALUE = datetime(2020, 1, 1, 12, 0, 0, tzinfo=UTC) diff --git a/tests/components/discovergy/conftest.py b/tests/components/discovergy/conftest.py index 0d0e68c487a..056f763c3e2 100644 --- a/tests/components/discovergy/conftest.py +++ b/tests/components/discovergy/conftest.py @@ -11,8 +11,9 @@ from homeassistant.const import CONF_EMAIL, CONF_PASSWORD from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component +from .const import GET_METERS, LAST_READING, LAST_READING_GAS + from tests.common import MockConfigEntry -from tests.components.discovergy.const import GET_METERS, LAST_READING, LAST_READING_GAS def _meter_last_reading(meter_id: str) -> Reading: diff --git a/tests/components/ecobee/test_climate.py b/tests/components/ecobee/test_climate.py index 46ca77025cc..35dd931d284 100644 --- a/tests/components/ecobee/test_climate.py +++ b/tests/components/ecobee/test_climate.py @@ -18,8 +18,8 @@ from homeassistant.components.ecobee.climate import ( from homeassistant.const import ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, STATE_OFF from homeassistant.core import HomeAssistant -from tests.components.ecobee import GENERIC_THERMOSTAT_INFO_WITH_HEATPUMP -from tests.components.ecobee.common import setup_platform +from . import GENERIC_THERMOSTAT_INFO_WITH_HEATPUMP +from .common import setup_platform ENTITY_ID = "climate.ecobee" diff --git a/tests/components/ecobee/test_switch.py b/tests/components/ecobee/test_switch.py index 383abf9644c..94b7296dcf5 100644 --- a/tests/components/ecobee/test_switch.py +++ b/tests/components/ecobee/test_switch.py @@ -12,10 +12,9 @@ from homeassistant.components.switch import DOMAIN, SERVICE_TURN_OFF, SERVICE_TU from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import HomeAssistant +from . import GENERIC_THERMOSTAT_INFO_WITH_HEATPUMP from .common import setup_platform -from tests.components.ecobee import GENERIC_THERMOSTAT_INFO_WITH_HEATPUMP - VENTILATOR_20MIN_ID = "switch.ecobee_ventilator_20m_timer" THERMOSTAT_ID = 0 diff --git a/tests/components/fan/test_init.py b/tests/components/fan/test_init.py index 2f1b583d7f2..04f594b959c 100644 --- a/tests/components/fan/test_init.py +++ b/tests/components/fan/test_init.py @@ -16,12 +16,13 @@ from homeassistant.core import HomeAssistant import homeassistant.helpers.entity_registry as er from homeassistant.setup import async_setup_component +from .common import MockFan + from tests.common import ( help_test_all, import_and_test_deprecated_constant_enum, setup_test_component_platform, ) -from tests.components.fan.common import MockFan class BaseFan(FanEntity): diff --git a/tests/components/flexit_bacnet/test_binary_sensor.py b/tests/components/flexit_bacnet/test_binary_sensor.py index 96efefc45ec..ceb9853acac 100644 --- a/tests/components/flexit_bacnet/test_binary_sensor.py +++ b/tests/components/flexit_bacnet/test_binary_sensor.py @@ -8,8 +8,9 @@ from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er +from . import setup_with_selected_platforms + from tests.common import MockConfigEntry, snapshot_platform -from tests.components.flexit_bacnet import setup_with_selected_platforms async def test_binary_sensors( diff --git a/tests/components/flexit_bacnet/test_climate.py b/tests/components/flexit_bacnet/test_climate.py index 7f5a20499ce..7b0546f60ea 100644 --- a/tests/components/flexit_bacnet/test_climate.py +++ b/tests/components/flexit_bacnet/test_climate.py @@ -8,8 +8,9 @@ from homeassistant.const import Platform from homeassistant.core import HomeAssistant import homeassistant.helpers.entity_registry as er +from . import setup_with_selected_platforms + from tests.common import MockConfigEntry, snapshot_platform -from tests.components.flexit_bacnet import setup_with_selected_platforms async def test_climate_entity( diff --git a/tests/components/flexit_bacnet/test_init.py b/tests/components/flexit_bacnet/test_init.py index 4ff52a3bcfc..4cae562c1be 100644 --- a/tests/components/flexit_bacnet/test_init.py +++ b/tests/components/flexit_bacnet/test_init.py @@ -7,8 +7,9 @@ from homeassistant.config_entries import ConfigEntryState from homeassistant.const import Platform from homeassistant.core import HomeAssistant +from . import setup_with_selected_platforms + from tests.common import MockConfigEntry -from tests.components.flexit_bacnet import setup_with_selected_platforms async def test_loading_and_unloading_config_entry( diff --git a/tests/components/flexit_bacnet/test_number.py b/tests/components/flexit_bacnet/test_number.py index 921977d0d63..c2f8026b1cd 100644 --- a/tests/components/flexit_bacnet/test_number.py +++ b/tests/components/flexit_bacnet/test_number.py @@ -13,8 +13,9 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er +from . import setup_with_selected_platforms + from tests.common import MockConfigEntry, snapshot_platform -from tests.components.flexit_bacnet import setup_with_selected_platforms ENTITY_ID = "number.device_name_fireplace_supply_fan_setpoint" diff --git a/tests/components/flexit_bacnet/test_sensor.py b/tests/components/flexit_bacnet/test_sensor.py index 566d3d318f1..ef1269ee7b2 100644 --- a/tests/components/flexit_bacnet/test_sensor.py +++ b/tests/components/flexit_bacnet/test_sensor.py @@ -8,8 +8,9 @@ from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er +from . import setup_with_selected_platforms + from tests.common import MockConfigEntry, snapshot_platform -from tests.components.flexit_bacnet import setup_with_selected_platforms async def test_sensors( diff --git a/tests/components/flexit_bacnet/test_switch.py b/tests/components/flexit_bacnet/test_switch.py index 00ca1997f77..8ce0bf11977 100644 --- a/tests/components/flexit_bacnet/test_switch.py +++ b/tests/components/flexit_bacnet/test_switch.py @@ -16,8 +16,9 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er +from . import setup_with_selected_platforms + from tests.common import MockConfigEntry, snapshot_platform -from tests.components.flexit_bacnet import setup_with_selected_platforms ENTITY_ID = "switch.device_name_electric_heater" diff --git a/tests/components/geo_json_events/test_geo_location.py b/tests/components/geo_json_events/test_geo_location.py index 365c4ca27bc..173ba201888 100644 --- a/tests/components/geo_json_events/test_geo_location.py +++ b/tests/components/geo_json_events/test_geo_location.py @@ -28,9 +28,10 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.util import dt as dt_util +from . import _generate_mock_feed_entry +from .conftest import URL + from tests.common import MockConfigEntry, async_fire_time_changed -from tests.components.geo_json_events import _generate_mock_feed_entry -from tests.components.geo_json_events.conftest import URL CONFIG_LEGACY = { GEO_LOCATION_DOMAIN: [ diff --git a/tests/components/geo_json_events/test_init.py b/tests/components/geo_json_events/test_init.py index 278586ba2e3..e90e663d8b6 100644 --- a/tests/components/geo_json_events/test_init.py +++ b/tests/components/geo_json_events/test_init.py @@ -7,8 +7,9 @@ from homeassistant.components.geo_location import DOMAIN as GEO_LOCATION_DOMAIN from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er +from . import _generate_mock_feed_entry + from tests.common import MockConfigEntry -from tests.components.geo_json_events import _generate_mock_feed_entry async def test_component_unload_config_entry( diff --git a/tests/components/hue/conftest.py b/tests/components/hue/conftest.py index dd27a657e2a..e824e8cb149 100644 --- a/tests/components/hue/conftest.py +++ b/tests/components/hue/conftest.py @@ -19,13 +19,14 @@ from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.setup import async_setup_component +from .const import FAKE_BRIDGE, FAKE_BRIDGE_DEVICE + from tests.common import ( MockConfigEntry, async_mock_service, load_fixture, mock_device_registry, ) -from tests.components.hue.const import FAKE_BRIDGE, FAKE_BRIDGE_DEVICE @pytest.fixture(autouse=True) diff --git a/tests/components/ipma/test_config_flow.py b/tests/components/ipma/test_config_flow.py index b007534e09f..38bb1dbf126 100644 --- a/tests/components/ipma/test_config_flow.py +++ b/tests/components/ipma/test_config_flow.py @@ -12,7 +12,7 @@ from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType -from tests.components.ipma import MockLocation +from . import MockLocation @pytest.fixture(name="ipma_setup", autouse=True) From 8cbfc5a58b79c0cb9b0333ce2771ea396fce95ef Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 10 Jun 2024 13:09:51 +0200 Subject: [PATCH 0454/1445] Use service_calls fixture in arcam_fmj tests (#119274) --- .../arcam_fmj/test_device_trigger.py | 28 ++++++------------- tests/conftest.py | 24 ++++++++++++++-- 2 files changed, 30 insertions(+), 22 deletions(-) diff --git a/tests/components/arcam_fmj/test_device_trigger.py b/tests/components/arcam_fmj/test_device_trigger.py index da01f00d8a5..eb5cf1d7892 100644 --- a/tests/components/arcam_fmj/test_device_trigger.py +++ b/tests/components/arcam_fmj/test_device_trigger.py @@ -9,11 +9,7 @@ from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.setup import async_setup_component -from tests.common import ( - MockConfigEntry, - async_get_device_automations, - async_mock_service, -) +from tests.common import MockConfigEntry, async_get_device_automations @pytest.fixture(autouse=True, name="stub_blueprint_populate") @@ -21,12 +17,6 @@ def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None: """Stub copying the blueprints to the config folder.""" -@pytest.fixture -def calls(hass: HomeAssistant) -> list[ServiceCall]: - """Track calls to a mock service.""" - return async_mock_service(hass, "test", "automation") - - async def test_get_triggers( hass: HomeAssistant, device_registry: dr.DeviceRegistry, @@ -69,7 +59,7 @@ async def test_get_triggers( async def test_if_fires_on_turn_on_request( hass: HomeAssistant, entity_registry: er.EntityRegistry, - calls: list[ServiceCall], + service_calls: list[ServiceCall], player_setup, state, ) -> None: @@ -111,15 +101,15 @@ async def test_if_fires_on_turn_on_request( ) await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["some"] == player_setup - assert calls[0].data["id"] == 0 + assert len(service_calls) == 2 + assert service_calls[1].data["some"] == player_setup + assert service_calls[1].data["id"] == 0 async def test_if_fires_on_turn_on_request_legacy( hass: HomeAssistant, entity_registry: er.EntityRegistry, - calls: list[ServiceCall], + service_calls: list[ServiceCall], player_setup, state, ) -> None: @@ -161,6 +151,6 @@ async def test_if_fires_on_turn_on_request_legacy( ) await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["some"] == player_setup - assert calls[0].data["id"] == 0 + assert len(service_calls) == 2 + assert service_calls[1].data["some"] == player_setup + assert service_calls[1].data["id"] == 0 diff --git a/tests/conftest.py b/tests/conftest.py index 78fb6835abe..dee98ecd3b8 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -51,7 +51,13 @@ from homeassistant.components.websocket_api.http import URL from homeassistant.config import YAML_CONFIG_FILE from homeassistant.config_entries import ConfigEntries, ConfigEntry, ConfigEntryState from homeassistant.const import HASSIO_USER_NAME -from homeassistant.core import CoreState, HassJob, HomeAssistant, ServiceCall +from homeassistant.core import ( + CoreState, + HassJob, + HomeAssistant, + ServiceCall, + ServiceResponse, +) from homeassistant.helpers import ( area_registry as ar, category_registry as cr, @@ -1776,18 +1782,30 @@ def label_registry(hass: HomeAssistant) -> lr.LabelRegistry: @pytest.fixture -def service_calls() -> Generator[None, None, list[ServiceCall]]: +def service_calls(hass: HomeAssistant) -> Generator[None, None, list[ServiceCall]]: """Track all service calls.""" calls = [] + _original_async_call = hass.services.async_call + async def _async_call( self, domain: str, service: str, service_data: dict[str, Any] | None = None, **kwargs: Any, - ): + ) -> ServiceResponse: calls.append(ServiceCall(domain, service, service_data)) + try: + return await _original_async_call( + domain, + service, + service_data, + **kwargs, + ) + except ha.ServiceNotFound: + _LOGGER.debug("Ignoring unknown service call to %s.%s", domain, service) + return None with patch("homeassistant.core.ServiceRegistry.async_call", _async_call): yield calls From 94720fd0155a8361621e9db18b67acbae0e56cae Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 10 Jun 2024 13:31:29 +0200 Subject: [PATCH 0455/1445] Fix root-import pylint warning in dlna_dmr tests (#119286) --- .../components/dlna_dmr/test_media_player.py | 219 +++++++++--------- 1 file changed, 107 insertions(+), 112 deletions(-) diff --git a/tests/components/dlna_dmr/test_media_player.py b/tests/components/dlna_dmr/test_media_player.py index 224046dcef5..ad67530e605 100644 --- a/tests/components/dlna_dmr/test_media_player.py +++ b/tests/components/dlna_dmr/test_media_player.py @@ -20,7 +20,7 @@ from didl_lite import didl_lite import pytest from homeassistant import const as ha_const -from homeassistant.components import ssdp +from homeassistant.components import media_player as mp, ssdp from homeassistant.components.dlna_dmr.const import ( CONF_BROWSE_UNFILTERED, CONF_CALLBACK_URL_OVERRIDE, @@ -31,13 +31,10 @@ from homeassistant.components.dlna_dmr.const import ( from homeassistant.components.dlna_dmr.data import EventListenAddr from homeassistant.components.dlna_dmr.media_player import DlnaDmrEntity from homeassistant.components.media_player import ( - ATTR_TO_PROPERTY, - DOMAIN as MP_DOMAIN, MediaPlayerEntityFeature, MediaPlayerState, MediaType, RepeatMode, - const as mp_const, ) from homeassistant.components.media_source import DOMAIN as MS_DOMAIN, PlayMedia from homeassistant.const import ( @@ -551,56 +548,56 @@ async def test_attributes( """Test attributes of a connected DlnaDmrEntity.""" # Check attributes come directly from the device attrs = await get_attrs(hass, mock_entity_id) - assert attrs[mp_const.ATTR_MEDIA_VOLUME_LEVEL] is dmr_device_mock.volume_level - assert attrs[mp_const.ATTR_MEDIA_VOLUME_MUTED] is dmr_device_mock.is_volume_muted - assert attrs[mp_const.ATTR_MEDIA_DURATION] is dmr_device_mock.media_duration - assert attrs[mp_const.ATTR_MEDIA_POSITION] is dmr_device_mock.media_position + assert attrs[mp.ATTR_MEDIA_VOLUME_LEVEL] is dmr_device_mock.volume_level + assert attrs[mp.ATTR_MEDIA_VOLUME_MUTED] is dmr_device_mock.is_volume_muted + assert attrs[mp.ATTR_MEDIA_DURATION] is dmr_device_mock.media_duration + assert attrs[mp.ATTR_MEDIA_POSITION] is dmr_device_mock.media_position assert ( - attrs[mp_const.ATTR_MEDIA_POSITION_UPDATED_AT] + attrs[mp.ATTR_MEDIA_POSITION_UPDATED_AT] is dmr_device_mock.media_position_updated_at ) - assert attrs[mp_const.ATTR_MEDIA_CONTENT_ID] is dmr_device_mock.current_track_uri - assert attrs[mp_const.ATTR_MEDIA_ARTIST] is dmr_device_mock.media_artist - assert attrs[mp_const.ATTR_MEDIA_ALBUM_NAME] is dmr_device_mock.media_album_name - assert attrs[mp_const.ATTR_MEDIA_ALBUM_ARTIST] is dmr_device_mock.media_album_artist - assert attrs[mp_const.ATTR_MEDIA_TRACK] is dmr_device_mock.media_track_number - assert attrs[mp_const.ATTR_MEDIA_SERIES_TITLE] is dmr_device_mock.media_series_title - assert attrs[mp_const.ATTR_MEDIA_SEASON] is dmr_device_mock.media_season_number - assert attrs[mp_const.ATTR_MEDIA_EPISODE] is dmr_device_mock.media_episode_number - assert attrs[mp_const.ATTR_MEDIA_CHANNEL] is dmr_device_mock.media_channel_name - assert attrs[mp_const.ATTR_SOUND_MODE_LIST] is dmr_device_mock.preset_names + assert attrs[mp.ATTR_MEDIA_CONTENT_ID] is dmr_device_mock.current_track_uri + assert attrs[mp.ATTR_MEDIA_ARTIST] is dmr_device_mock.media_artist + assert attrs[mp.ATTR_MEDIA_ALBUM_NAME] is dmr_device_mock.media_album_name + assert attrs[mp.ATTR_MEDIA_ALBUM_ARTIST] is dmr_device_mock.media_album_artist + assert attrs[mp.ATTR_MEDIA_TRACK] is dmr_device_mock.media_track_number + assert attrs[mp.ATTR_MEDIA_SERIES_TITLE] is dmr_device_mock.media_series_title + assert attrs[mp.ATTR_MEDIA_SEASON] is dmr_device_mock.media_season_number + assert attrs[mp.ATTR_MEDIA_EPISODE] is dmr_device_mock.media_episode_number + assert attrs[mp.ATTR_MEDIA_CHANNEL] is dmr_device_mock.media_channel_name + assert attrs[mp.ATTR_SOUND_MODE_LIST] is dmr_device_mock.preset_names # Entity picture is cached, won't correspond to remote image assert isinstance(attrs[ha_const.ATTR_ENTITY_PICTURE], str) # media_title depends on what is available - assert attrs[mp_const.ATTR_MEDIA_TITLE] is dmr_device_mock.media_program_title + assert attrs[mp.ATTR_MEDIA_TITLE] is dmr_device_mock.media_program_title dmr_device_mock.media_program_title = None attrs = await get_attrs(hass, mock_entity_id) - assert attrs[mp_const.ATTR_MEDIA_TITLE] is dmr_device_mock.media_title + assert attrs[mp.ATTR_MEDIA_TITLE] is dmr_device_mock.media_title # media_content_type is mapped from UPnP class to MediaPlayer type dmr_device_mock.media_class = "object.item.audioItem.musicTrack" attrs = await get_attrs(hass, mock_entity_id) - assert attrs[mp_const.ATTR_MEDIA_CONTENT_TYPE] == MediaType.MUSIC + assert attrs[mp.ATTR_MEDIA_CONTENT_TYPE] == MediaType.MUSIC dmr_device_mock.media_class = "object.item.videoItem.movie" attrs = await get_attrs(hass, mock_entity_id) - assert attrs[mp_const.ATTR_MEDIA_CONTENT_TYPE] == MediaType.MOVIE + assert attrs[mp.ATTR_MEDIA_CONTENT_TYPE] == MediaType.MOVIE dmr_device_mock.media_class = "object.item.videoItem.videoBroadcast" attrs = await get_attrs(hass, mock_entity_id) - assert attrs[mp_const.ATTR_MEDIA_CONTENT_TYPE] == MediaType.TVSHOW + assert attrs[mp.ATTR_MEDIA_CONTENT_TYPE] == MediaType.TVSHOW # media_season & media_episode have a special case dmr_device_mock.media_season_number = "0" dmr_device_mock.media_episode_number = "123" attrs = await get_attrs(hass, mock_entity_id) - assert attrs[mp_const.ATTR_MEDIA_SEASON] == "1" - assert attrs[mp_const.ATTR_MEDIA_EPISODE] == "23" + assert attrs[mp.ATTR_MEDIA_SEASON] == "1" + assert attrs[mp.ATTR_MEDIA_EPISODE] == "23" dmr_device_mock.media_season_number = "0" dmr_device_mock.media_episode_number = "S1E23" # Unexpected and not parsed attrs = await get_attrs(hass, mock_entity_id) - assert attrs[mp_const.ATTR_MEDIA_SEASON] == "0" - assert attrs[mp_const.ATTR_MEDIA_EPISODE] == "S1E23" + assert attrs[mp.ATTR_MEDIA_SEASON] == "0" + assert attrs[mp.ATTR_MEDIA_EPISODE] == "S1E23" # shuffle and repeat is based on device's play mode for play_mode, shuffle, repeat in [ @@ -614,13 +611,13 @@ async def test_attributes( ]: dmr_device_mock.play_mode = play_mode attrs = await get_attrs(hass, mock_entity_id) - assert attrs[mp_const.ATTR_MEDIA_SHUFFLE] is shuffle - assert attrs[mp_const.ATTR_MEDIA_REPEAT] == repeat + assert attrs[mp.ATTR_MEDIA_SHUFFLE] is shuffle + assert attrs[mp.ATTR_MEDIA_REPEAT] == repeat for bad_play_mode in [None, PlayMode.VENDOR_DEFINED]: dmr_device_mock.play_mode = bad_play_mode attrs = await get_attrs(hass, mock_entity_id) - assert mp_const.ATTR_MEDIA_SHUFFLE not in attrs - assert mp_const.ATTR_MEDIA_REPEAT not in attrs + assert mp.ATTR_MEDIA_SHUFFLE not in attrs + assert mp.ATTR_MEDIA_REPEAT not in attrs async def test_services( @@ -629,65 +626,65 @@ async def test_services( """Test service calls of a connected DlnaDmrEntity.""" # Check interface methods interact directly with the device await hass.services.async_call( - MP_DOMAIN, + mp.DOMAIN, ha_const.SERVICE_VOLUME_SET, - {ATTR_ENTITY_ID: mock_entity_id, mp_const.ATTR_MEDIA_VOLUME_LEVEL: 0.80}, + {ATTR_ENTITY_ID: mock_entity_id, mp.ATTR_MEDIA_VOLUME_LEVEL: 0.80}, blocking=True, ) dmr_device_mock.async_set_volume_level.assert_awaited_once_with(0.80) await hass.services.async_call( - MP_DOMAIN, + mp.DOMAIN, ha_const.SERVICE_VOLUME_MUTE, - {ATTR_ENTITY_ID: mock_entity_id, mp_const.ATTR_MEDIA_VOLUME_MUTED: True}, + {ATTR_ENTITY_ID: mock_entity_id, mp.ATTR_MEDIA_VOLUME_MUTED: True}, blocking=True, ) dmr_device_mock.async_mute_volume.assert_awaited_once_with(True) await hass.services.async_call( - MP_DOMAIN, + mp.DOMAIN, ha_const.SERVICE_MEDIA_PAUSE, {ATTR_ENTITY_ID: mock_entity_id}, blocking=True, ) dmr_device_mock.async_pause.assert_awaited_once_with() await hass.services.async_call( - MP_DOMAIN, + mp.DOMAIN, ha_const.SERVICE_MEDIA_PLAY, {ATTR_ENTITY_ID: mock_entity_id}, blocking=True, ) dmr_device_mock.async_pause.assert_awaited_once_with() await hass.services.async_call( - MP_DOMAIN, + mp.DOMAIN, ha_const.SERVICE_MEDIA_STOP, {ATTR_ENTITY_ID: mock_entity_id}, blocking=True, ) dmr_device_mock.async_stop.assert_awaited_once_with() await hass.services.async_call( - MP_DOMAIN, + mp.DOMAIN, ha_const.SERVICE_MEDIA_NEXT_TRACK, {ATTR_ENTITY_ID: mock_entity_id}, blocking=True, ) dmr_device_mock.async_next.assert_awaited_once_with() await hass.services.async_call( - MP_DOMAIN, + mp.DOMAIN, ha_const.SERVICE_MEDIA_PREVIOUS_TRACK, {ATTR_ENTITY_ID: mock_entity_id}, blocking=True, ) dmr_device_mock.async_previous.assert_awaited_once_with() await hass.services.async_call( - MP_DOMAIN, + mp.DOMAIN, ha_const.SERVICE_MEDIA_SEEK, - {ATTR_ENTITY_ID: mock_entity_id, mp_const.ATTR_MEDIA_SEEK_POSITION: 33}, + {ATTR_ENTITY_ID: mock_entity_id, mp.ATTR_MEDIA_SEEK_POSITION: 33}, blocking=True, ) dmr_device_mock.async_seek_rel_time.assert_awaited_once_with(timedelta(seconds=33)) await hass.services.async_call( - MP_DOMAIN, - mp_const.SERVICE_SELECT_SOUND_MODE, - {ATTR_ENTITY_ID: mock_entity_id, mp_const.ATTR_SOUND_MODE: "Default"}, + mp.DOMAIN, + mp.SERVICE_SELECT_SOUND_MODE, + {ATTR_ENTITY_ID: mock_entity_id, mp.ATTR_SOUND_MODE: "Default"}, blocking=True, ) dmr_device_mock.async_select_preset.assert_awaited_once_with("Default") @@ -701,15 +698,15 @@ async def test_play_media_stopped( dmr_device_mock.can_stop = True dmr_device_mock.transport_state = TransportState.STOPPED await hass.services.async_call( - MP_DOMAIN, - mp_const.SERVICE_PLAY_MEDIA, + mp.DOMAIN, + mp.SERVICE_PLAY_MEDIA, { ATTR_ENTITY_ID: mock_entity_id, - mp_const.ATTR_MEDIA_CONTENT_TYPE: MediaType.MUSIC, - mp_const.ATTR_MEDIA_CONTENT_ID: ( + mp.ATTR_MEDIA_CONTENT_TYPE: MediaType.MUSIC, + mp.ATTR_MEDIA_CONTENT_ID: ( "http://198.51.100.20:8200/MediaItems/17621.mp3" ), - mp_const.ATTR_MEDIA_ENQUEUE: False, + mp.ATTR_MEDIA_ENQUEUE: False, }, blocking=True, ) @@ -735,15 +732,15 @@ async def test_play_media_playing( dmr_device_mock.can_stop = False dmr_device_mock.transport_state = TransportState.PLAYING await hass.services.async_call( - MP_DOMAIN, - mp_const.SERVICE_PLAY_MEDIA, + mp.DOMAIN, + mp.SERVICE_PLAY_MEDIA, { ATTR_ENTITY_ID: mock_entity_id, - mp_const.ATTR_MEDIA_CONTENT_TYPE: MediaType.MUSIC, - mp_const.ATTR_MEDIA_CONTENT_ID: ( + mp.ATTR_MEDIA_CONTENT_TYPE: MediaType.MUSIC, + mp.ATTR_MEDIA_CONTENT_ID: ( "http://198.51.100.20:8200/MediaItems/17621.mp3" ), - mp_const.ATTR_MEDIA_ENQUEUE: False, + mp.ATTR_MEDIA_ENQUEUE: False, }, blocking=True, ) @@ -770,16 +767,16 @@ async def test_play_media_no_autoplay( dmr_device_mock.can_stop = True dmr_device_mock.transport_state = TransportState.STOPPED await hass.services.async_call( - MP_DOMAIN, - mp_const.SERVICE_PLAY_MEDIA, + mp.DOMAIN, + mp.SERVICE_PLAY_MEDIA, { ATTR_ENTITY_ID: mock_entity_id, - mp_const.ATTR_MEDIA_CONTENT_TYPE: MediaType.MUSIC, - mp_const.ATTR_MEDIA_CONTENT_ID: ( + mp.ATTR_MEDIA_CONTENT_TYPE: MediaType.MUSIC, + mp.ATTR_MEDIA_CONTENT_ID: ( "http://198.51.100.20:8200/MediaItems/17621.mp3" ), - mp_const.ATTR_MEDIA_ENQUEUE: False, - mp_const.ATTR_MEDIA_EXTRA: {"autoplay": False}, + mp.ATTR_MEDIA_ENQUEUE: False, + mp.ATTR_MEDIA_EXTRA: {"autoplay": False}, }, blocking=True, ) @@ -803,16 +800,16 @@ async def test_play_media_metadata( ) -> None: """Test play_media constructs useful metadata from user params.""" await hass.services.async_call( - MP_DOMAIN, - mp_const.SERVICE_PLAY_MEDIA, + mp.DOMAIN, + mp.SERVICE_PLAY_MEDIA, { ATTR_ENTITY_ID: mock_entity_id, - mp_const.ATTR_MEDIA_CONTENT_TYPE: MediaType.MUSIC, - mp_const.ATTR_MEDIA_CONTENT_ID: ( + mp.ATTR_MEDIA_CONTENT_TYPE: MediaType.MUSIC, + mp.ATTR_MEDIA_CONTENT_ID: ( "http://198.51.100.20:8200/MediaItems/17621.mp3" ), - mp_const.ATTR_MEDIA_ENQUEUE: False, - mp_const.ATTR_MEDIA_EXTRA: { + mp.ATTR_MEDIA_ENQUEUE: False, + mp.ATTR_MEDIA_EXTRA: { "title": "Mock song", "thumb": "http://198.51.100.20:8200/MediaItems/17621.jpg", "metadata": {"artist": "Mock artist", "album": "Mock album"}, @@ -835,16 +832,14 @@ async def test_play_media_metadata( # Check again for a different media type dmr_device_mock.construct_play_media_metadata.reset_mock() await hass.services.async_call( - MP_DOMAIN, - mp_const.SERVICE_PLAY_MEDIA, + mp.DOMAIN, + mp.SERVICE_PLAY_MEDIA, { ATTR_ENTITY_ID: mock_entity_id, - mp_const.ATTR_MEDIA_CONTENT_TYPE: MediaType.TVSHOW, - mp_const.ATTR_MEDIA_CONTENT_ID: ( - "http://198.51.100.20:8200/MediaItems/123.mkv" - ), - mp_const.ATTR_MEDIA_ENQUEUE: False, - mp_const.ATTR_MEDIA_EXTRA: { + mp.ATTR_MEDIA_CONTENT_TYPE: MediaType.TVSHOW, + mp.ATTR_MEDIA_CONTENT_ID: ("http://198.51.100.20:8200/MediaItems/123.mkv"), + mp.ATTR_MEDIA_ENQUEUE: False, + mp.ATTR_MEDIA_EXTRA: { "title": "Mock show", "metadata": {"season": 1, "episode": 12}, }, @@ -870,12 +865,12 @@ async def test_play_media_local_source( await hass.async_block_till_done() await hass.services.async_call( - MP_DOMAIN, - mp_const.SERVICE_PLAY_MEDIA, + mp.DOMAIN, + mp.SERVICE_PLAY_MEDIA, { ATTR_ENTITY_ID: mock_entity_id, - mp_const.ATTR_MEDIA_CONTENT_TYPE: "video/mp4", - mp_const.ATTR_MEDIA_CONTENT_ID: ( + mp.ATTR_MEDIA_CONTENT_TYPE: "video/mp4", + mp.ATTR_MEDIA_CONTENT_ID: ( "media-source://media_source/local/Epic Sax Guy 10 Hours.mp4" ), }, @@ -927,12 +922,12 @@ async def test_play_media_didl_metadata( return_value=play_media, ): await hass.services.async_call( - MP_DOMAIN, - mp_const.SERVICE_PLAY_MEDIA, + mp.DOMAIN, + mp.SERVICE_PLAY_MEDIA, { ATTR_ENTITY_ID: mock_entity_id, - mp_const.ATTR_MEDIA_CONTENT_TYPE: "video/mp4", - mp_const.ATTR_MEDIA_CONTENT_ID: ( + mp.ATTR_MEDIA_CONTENT_TYPE: "video/mp4", + mp.ATTR_MEDIA_CONTENT_ID: ( "media-source://media_source/local/Epic Sax Guy 10 Hours.mp4" ), }, @@ -968,9 +963,9 @@ async def test_shuffle_repeat_modes( ]: dmr_device_mock.play_mode = init_mode await hass.services.async_call( - MP_DOMAIN, + mp.DOMAIN, ha_const.SERVICE_SHUFFLE_SET, - {ATTR_ENTITY_ID: mock_entity_id, mp_const.ATTR_MEDIA_SHUFFLE: shuffle_set}, + {ATTR_ENTITY_ID: mock_entity_id, mp.ATTR_MEDIA_SHUFFLE: shuffle_set}, blocking=True, ) dmr_device_mock.async_set_play_mode.assert_awaited_with(expect_mode) @@ -995,9 +990,9 @@ async def test_shuffle_repeat_modes( ]: dmr_device_mock.play_mode = init_mode await hass.services.async_call( - MP_DOMAIN, + mp.DOMAIN, ha_const.SERVICE_REPEAT_SET, - {ATTR_ENTITY_ID: mock_entity_id, mp_const.ATTR_MEDIA_REPEAT: repeat_set}, + {ATTR_ENTITY_ID: mock_entity_id, mp.ATTR_MEDIA_REPEAT: repeat_set}, blocking=True, ) dmr_device_mock.async_set_play_mode.assert_awaited_with(expect_mode) @@ -1009,9 +1004,9 @@ async def test_shuffle_repeat_modes( dmr_device_mock.valid_play_modes = {PlayMode.SHUFFLE, PlayMode.RANDOM} await get_attrs(hass, mock_entity_id) await hass.services.async_call( - MP_DOMAIN, + mp.DOMAIN, ha_const.SERVICE_SHUFFLE_SET, - {ATTR_ENTITY_ID: mock_entity_id, mp_const.ATTR_MEDIA_SHUFFLE: False}, + {ATTR_ENTITY_ID: mock_entity_id, mp.ATTR_MEDIA_SHUFFLE: False}, blocking=True, ) dmr_device_mock.async_set_play_mode.assert_not_awaited() @@ -1023,11 +1018,11 @@ async def test_shuffle_repeat_modes( dmr_device_mock.valid_play_modes = {PlayMode.REPEAT_ONE, PlayMode.REPEAT_ALL} await get_attrs(hass, mock_entity_id) await hass.services.async_call( - MP_DOMAIN, + mp.DOMAIN, ha_const.SERVICE_REPEAT_SET, { ATTR_ENTITY_ID: mock_entity_id, - mp_const.ATTR_MEDIA_REPEAT: RepeatMode.OFF, + mp.ATTR_MEDIA_REPEAT: RepeatMode.OFF, }, blocking=True, ) @@ -1322,40 +1317,40 @@ async def test_unavailable_device( # Check attributes are unavailable attrs = mock_state.attributes - for attr in ATTR_TO_PROPERTY: + for attr in mp.ATTR_TO_PROPERTY: assert attr not in attrs assert attrs[ha_const.ATTR_FRIENDLY_NAME] == MOCK_DEVICE_NAME assert attrs[ha_const.ATTR_SUPPORTED_FEATURES] == 0 - assert mp_const.ATTR_SOUND_MODE_LIST not in attrs + assert mp.ATTR_SOUND_MODE_LIST not in attrs # Check service calls do nothing SERVICES: list[tuple[str, dict]] = [ - (ha_const.SERVICE_VOLUME_SET, {mp_const.ATTR_MEDIA_VOLUME_LEVEL: 0.80}), - (ha_const.SERVICE_VOLUME_MUTE, {mp_const.ATTR_MEDIA_VOLUME_MUTED: True}), + (ha_const.SERVICE_VOLUME_SET, {mp.ATTR_MEDIA_VOLUME_LEVEL: 0.80}), + (ha_const.SERVICE_VOLUME_MUTE, {mp.ATTR_MEDIA_VOLUME_MUTED: True}), (ha_const.SERVICE_MEDIA_PAUSE, {}), (ha_const.SERVICE_MEDIA_PLAY, {}), (ha_const.SERVICE_MEDIA_STOP, {}), (ha_const.SERVICE_MEDIA_NEXT_TRACK, {}), (ha_const.SERVICE_MEDIA_PREVIOUS_TRACK, {}), - (ha_const.SERVICE_MEDIA_SEEK, {mp_const.ATTR_MEDIA_SEEK_POSITION: 33}), + (ha_const.SERVICE_MEDIA_SEEK, {mp.ATTR_MEDIA_SEEK_POSITION: 33}), ( - mp_const.SERVICE_PLAY_MEDIA, + mp.SERVICE_PLAY_MEDIA, { - mp_const.ATTR_MEDIA_CONTENT_TYPE: MediaType.MUSIC, - mp_const.ATTR_MEDIA_CONTENT_ID: ( + mp.ATTR_MEDIA_CONTENT_TYPE: MediaType.MUSIC, + mp.ATTR_MEDIA_CONTENT_ID: ( "http://198.51.100.20:8200/MediaItems/17621.mp3" ), - mp_const.ATTR_MEDIA_ENQUEUE: False, + mp.ATTR_MEDIA_ENQUEUE: False, }, ), - (mp_const.SERVICE_SELECT_SOUND_MODE, {mp_const.ATTR_SOUND_MODE: "Default"}), - (ha_const.SERVICE_SHUFFLE_SET, {mp_const.ATTR_MEDIA_SHUFFLE: True}), - (ha_const.SERVICE_REPEAT_SET, {mp_const.ATTR_MEDIA_REPEAT: "all"}), + (mp.SERVICE_SELECT_SOUND_MODE, {mp.ATTR_SOUND_MODE: "Default"}), + (ha_const.SERVICE_SHUFFLE_SET, {mp.ATTR_MEDIA_SHUFFLE: True}), + (ha_const.SERVICE_REPEAT_SET, {mp.ATTR_MEDIA_REPEAT: "all"}), ] for service, data in SERVICES: await hass.services.async_call( - MP_DOMAIN, + mp.DOMAIN, service, {ATTR_ENTITY_ID: mock_entity_id, **data}, blocking=True, @@ -1980,9 +1975,9 @@ async def test_become_unavailable( # Interface service calls should flag that the device is unavailable, but # not disconnect it immediately await hass.services.async_call( - MP_DOMAIN, + mp.DOMAIN, ha_const.SERVICE_VOLUME_SET, - {ATTR_ENTITY_ID: mock_entity_id, mp_const.ATTR_MEDIA_VOLUME_LEVEL: 0.80}, + {ATTR_ENTITY_ID: mock_entity_id, mp.ATTR_MEDIA_VOLUME_LEVEL: 0.80}, blocking=True, ) @@ -2003,9 +1998,9 @@ async def test_become_unavailable( dmr_device_mock.async_update.side_effect = UpnpConnectionError await hass.services.async_call( - MP_DOMAIN, + mp.DOMAIN, ha_const.SERVICE_VOLUME_SET, - {ATTR_ENTITY_ID: mock_entity_id, mp_const.ATTR_MEDIA_VOLUME_LEVEL: 0.80}, + {ATTR_ENTITY_ID: mock_entity_id, mp.ATTR_MEDIA_VOLUME_LEVEL: 0.80}, blocking=True, ) await async_update_entity(hass, mock_entity_id) @@ -2083,10 +2078,10 @@ async def test_disappearing_device( directly to skip the availability check. """ # Retrieve entity directly. - entity: DlnaDmrEntity = hass.data[MP_DOMAIN].get_entity(mock_disconnected_entity_id) + entity: DlnaDmrEntity = hass.data[mp.DOMAIN].get_entity(mock_disconnected_entity_id) # Test attribute access - for attr in ATTR_TO_PROPERTY: + for attr in mp.ATTR_TO_PROPERTY: value = getattr(entity, attr) assert value is None @@ -2456,7 +2451,7 @@ async def test_udn_upnp_connection_added_if_missing( # Cause connection attempts to fail before adding entity ent_reg = async_get_er(hass) entry = ent_reg.async_get_or_create( - MP_DOMAIN, + mp.DOMAIN, DOMAIN, MOCK_DEVICE_UDN, config_entry=config_entry_mock, From ac588ddc758255a528939d70875bf0b1ce0a722a Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 10 Jun 2024 13:32:31 +0200 Subject: [PATCH 0456/1445] Use relative imports in tests [j-r] (#119282) --- tests/components/lastfm/conftest.py | 9 ++------- tests/components/light/test_device_condition.py | 3 ++- tests/components/light/test_init.py | 3 ++- tests/components/linear_garage_door/test_init.py | 3 ++- tests/components/matrix/test_commands.py | 2 +- tests/components/matrix/test_login.py | 7 +------ tests/components/matrix/test_rooms.py | 4 +--- tests/components/matrix/test_send_message.py | 2 +- tests/components/media_extractor/__init__.py | 5 +++-- tests/components/media_extractor/conftest.py | 5 +++-- tests/components/media_extractor/test_init.py | 10 +++------- tests/components/melissa/test_climate.py | 2 +- tests/components/melissa/test_init.py | 2 +- tests/components/netatmo/test_binary_sensor.py | 3 ++- tests/components/number/conftest.py | 2 +- tests/components/number/test_init.py | 3 ++- tests/components/opensky/test_config_flow.py | 3 ++- tests/components/opensky/test_init.py | 3 ++- tests/components/opensky/test_sensor.py | 3 ++- tests/components/overkiz/conftest.py | 10 +++------- tests/components/recorder/test_purge_v32_schema.py | 4 ++-- tests/components/roborock/test_image.py | 3 ++- tests/components/roborock/test_vacuum.py | 3 ++- tests/components/rova/test_init.py | 3 ++- 24 files changed, 45 insertions(+), 52 deletions(-) diff --git a/tests/components/lastfm/conftest.py b/tests/components/lastfm/conftest.py index e17a1ccfa8a..361bb401521 100644 --- a/tests/components/lastfm/conftest.py +++ b/tests/components/lastfm/conftest.py @@ -11,14 +11,9 @@ from homeassistant.const import CONF_API_KEY from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component +from . import API_KEY, USERNAME_1, USERNAME_2, MockNetwork, MockUser + from tests.common import MockConfigEntry -from tests.components.lastfm import ( - API_KEY, - USERNAME_1, - USERNAME_2, - MockNetwork, - MockUser, -) type ComponentSetup = Callable[[MockConfigEntry, MockUser], Awaitable[None]] diff --git a/tests/components/light/test_device_condition.py b/tests/components/light/test_device_condition.py index 01b735bd5af..cef3ef788cb 100644 --- a/tests/components/light/test_device_condition.py +++ b/tests/components/light/test_device_condition.py @@ -16,6 +16,8 @@ from homeassistant.helpers.entity_registry import RegistryEntryHider from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util +from .common import MockLight + from tests.common import ( MockConfigEntry, async_get_device_automation_capabilities, @@ -23,7 +25,6 @@ from tests.common import ( async_mock_service, setup_test_component_platform, ) -from tests.components.light.common import MockLight @pytest.fixture(autouse=True, name="stub_blueprint_populate") diff --git a/tests/components/light/test_init.py b/tests/components/light/test_init.py index 6832b5812e2..a01d70d328c 100644 --- a/tests/components/light/test_init.py +++ b/tests/components/light/test_init.py @@ -22,13 +22,14 @@ from homeassistant.exceptions import HomeAssistantError, Unauthorized from homeassistant.setup import async_setup_component import homeassistant.util.color as color_util +from .common import MockLight + from tests.common import ( MockEntityPlatform, MockUser, async_mock_service, setup_test_component_platform, ) -from tests.components.light.common import MockLight orig_Profiles = light.Profiles diff --git a/tests/components/linear_garage_door/test_init.py b/tests/components/linear_garage_door/test_init.py index 92ff832be87..640264eb207 100644 --- a/tests/components/linear_garage_door/test_init.py +++ b/tests/components/linear_garage_door/test_init.py @@ -8,8 +8,9 @@ import pytest from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant +from . import setup_integration + from tests.common import MockConfigEntry -from tests.components.linear_garage_door import setup_integration async def test_unload_entry( diff --git a/tests/components/matrix/test_commands.py b/tests/components/matrix/test_commands.py index 17d92760fa0..8539252ad66 100644 --- a/tests/components/matrix/test_commands.py +++ b/tests/components/matrix/test_commands.py @@ -11,7 +11,7 @@ import pytest from homeassistant.components.matrix import MatrixBot, RoomID from homeassistant.core import Event, HomeAssistant -from tests.components.matrix.conftest import ( +from .conftest import ( MOCK_EXPRESSION_COMMANDS, MOCK_WORD_COMMANDS, TEST_MXID, diff --git a/tests/components/matrix/test_login.py b/tests/components/matrix/test_login.py index caf74576d4e..ad9bf660402 100644 --- a/tests/components/matrix/test_login.py +++ b/tests/components/matrix/test_login.py @@ -6,12 +6,7 @@ import pytest from homeassistant.components.matrix import MatrixBot from homeassistant.exceptions import ConfigEntryAuthFailed, HomeAssistantError -from tests.components.matrix.conftest import ( - TEST_DEVICE_ID, - TEST_MXID, - TEST_PASSWORD, - TEST_TOKEN, -) +from .conftest import TEST_DEVICE_ID, TEST_MXID, TEST_PASSWORD, TEST_TOKEN @dataclass diff --git a/tests/components/matrix/test_rooms.py b/tests/components/matrix/test_rooms.py index 66d1afbf532..e8e94224066 100644 --- a/tests/components/matrix/test_rooms.py +++ b/tests/components/matrix/test_rooms.py @@ -9,9 +9,7 @@ from homeassistant.const import EVENT_HOMEASSISTANT_START from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component -from .conftest import MOCK_CONFIG_DATA - -from tests.components.matrix.conftest import TEST_BAD_ROOM, TEST_JOINABLE_ROOMS +from .conftest import MOCK_CONFIG_DATA, TEST_BAD_ROOM, TEST_JOINABLE_ROOMS async def test_join( diff --git a/tests/components/matrix/test_send_message.py b/tests/components/matrix/test_send_message.py index 58c0573a22e..cdea2270cf9 100644 --- a/tests/components/matrix/test_send_message.py +++ b/tests/components/matrix/test_send_message.py @@ -12,7 +12,7 @@ from homeassistant.components.matrix.const import FORMAT_HTML, SERVICE_SEND_MESS from homeassistant.components.notify import ATTR_DATA, ATTR_MESSAGE, ATTR_TARGET from homeassistant.core import HomeAssistant -from tests.components.matrix.conftest import TEST_BAD_ROOM, TEST_JOINABLE_ROOMS +from .conftest import TEST_BAD_ROOM, TEST_JOINABLE_ROOMS async def test_send_message( diff --git a/tests/components/media_extractor/__init__.py b/tests/components/media_extractor/__init__.py index 79130f1ea4b..631bdc19ed7 100644 --- a/tests/components/media_extractor/__init__.py +++ b/tests/components/media_extractor/__init__.py @@ -2,8 +2,7 @@ from typing import Any -from tests.common import load_json_object_fixture -from tests.components.media_extractor.const import ( +from .const import ( AUDIO_QUERY, NO_FORMATS_RESPONSE, SOUNDCLOUD_TRACK, @@ -12,6 +11,8 @@ from tests.components.media_extractor.const import ( YOUTUBE_VIDEO, ) +from tests.common import load_json_object_fixture + def _get_base_fixture(url: str) -> str: return { diff --git a/tests/components/media_extractor/conftest.py b/tests/components/media_extractor/conftest.py index 91cff851ab0..1d198681f3f 100644 --- a/tests/components/media_extractor/conftest.py +++ b/tests/components/media_extractor/conftest.py @@ -10,9 +10,10 @@ from homeassistant.components.media_extractor import DOMAIN from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.setup import async_setup_component +from . import MockYoutubeDL +from .const import AUDIO_QUERY + from tests.common import async_mock_service -from tests.components.media_extractor import MockYoutubeDL -from tests.components.media_extractor.const import AUDIO_QUERY @pytest.fixture(autouse=True) diff --git a/tests/components/media_extractor/test_init.py b/tests/components/media_extractor/test_init.py index ee74eb4660b..8c8a1407ccc 100644 --- a/tests/components/media_extractor/test_init.py +++ b/tests/components/media_extractor/test_init.py @@ -19,14 +19,10 @@ from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import async_setup_component +from . import YOUTUBE_EMPTY_PLAYLIST, YOUTUBE_PLAYLIST, YOUTUBE_VIDEO, MockYoutubeDL +from .const import NO_FORMATS_RESPONSE, SOUNDCLOUD_TRACK + from tests.common import load_json_object_fixture -from tests.components.media_extractor import ( - YOUTUBE_EMPTY_PLAYLIST, - YOUTUBE_PLAYLIST, - YOUTUBE_VIDEO, - MockYoutubeDL, -) -from tests.components.media_extractor.const import NO_FORMATS_RESPONSE, SOUNDCLOUD_TRACK async def test_play_media_service_is_registered(hass: HomeAssistant) -> None: diff --git a/tests/components/melissa/test_climate.py b/tests/components/melissa/test_climate.py index ff59f925961..ceb14faf8fb 100644 --- a/tests/components/melissa/test_climate.py +++ b/tests/components/melissa/test_climate.py @@ -12,7 +12,7 @@ from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE from homeassistant.core import HomeAssistant import homeassistant.helpers.entity_registry as er -from tests.components.melissa import setup_integration +from . import setup_integration async def test_setup_platform( diff --git a/tests/components/melissa/test_init.py b/tests/components/melissa/test_init.py index d809f42e409..2eebc012fe1 100644 --- a/tests/components/melissa/test_init.py +++ b/tests/components/melissa/test_init.py @@ -2,7 +2,7 @@ from homeassistant.core import HomeAssistant -from tests.components.melissa import setup_integration +from . import setup_integration async def test_setup(hass: HomeAssistant, mock_melissa) -> None: diff --git a/tests/components/netatmo/test_binary_sensor.py b/tests/components/netatmo/test_binary_sensor.py index 53aea461fde..7b841ba204e 100644 --- a/tests/components/netatmo/test_binary_sensor.py +++ b/tests/components/netatmo/test_binary_sensor.py @@ -9,8 +9,9 @@ from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er +from .common import snapshot_platform_entities + from tests.common import MockConfigEntry -from tests.components.netatmo.common import snapshot_platform_entities @pytest.mark.usefixtures("entity_registry_enabled_by_default") diff --git a/tests/components/number/conftest.py b/tests/components/number/conftest.py index a84ab03611b..49b492821ab 100644 --- a/tests/components/number/conftest.py +++ b/tests/components/number/conftest.py @@ -2,7 +2,7 @@ import pytest -from tests.components.number.common import MockNumberEntity +from .common import MockNumberEntity UNIQUE_NUMBER = "unique_number" diff --git a/tests/components/number/test_init.py b/tests/components/number/test_init.py index 9fe9322c731..dbdbab31d63 100644 --- a/tests/components/number/test_init.py +++ b/tests/components/number/test_init.py @@ -43,6 +43,8 @@ from homeassistant.helpers.restore_state import STORAGE_KEY as RESTORE_STATE_KEY from homeassistant.setup import async_setup_component from homeassistant.util.unit_system import METRIC_SYSTEM, US_CUSTOMARY_SYSTEM +from . import common + from tests.common import ( MockConfigEntry, MockModule, @@ -54,7 +56,6 @@ from tests.common import ( mock_restore_cache_with_extra_data, setup_test_component_platform, ) -from tests.components.number import common TEST_DOMAIN = "test" diff --git a/tests/components/opensky/test_config_flow.py b/tests/components/opensky/test_config_flow.py index e30d5ad8475..b99c264f205 100644 --- a/tests/components/opensky/test_config_flow.py +++ b/tests/components/opensky/test_config_flow.py @@ -22,8 +22,9 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from . import setup_integration + from tests.common import MockConfigEntry -from tests.components.opensky import setup_integration async def test_full_user_flow(hass: HomeAssistant, mock_setup_entry) -> None: diff --git a/tests/components/opensky/test_init.py b/tests/components/opensky/test_init.py index f5acf7479a2..cc53bc1de14 100644 --- a/tests/components/opensky/test_init.py +++ b/tests/components/opensky/test_init.py @@ -10,8 +10,9 @@ from python_opensky.exceptions import OpenSkyUnauthenticatedError from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant +from . import setup_integration + from tests.common import MockConfigEntry -from tests.components.opensky import setup_integration async def test_load_unload_entry( diff --git a/tests/components/opensky/test_sensor.py b/tests/components/opensky/test_sensor.py index 0c84762dd50..937540a42c1 100644 --- a/tests/components/opensky/test_sensor.py +++ b/tests/components/opensky/test_sensor.py @@ -14,12 +14,13 @@ from homeassistant.components.opensky.const import ( ) from homeassistant.core import Event, HomeAssistant +from . import setup_integration + from tests.common import ( MockConfigEntry, async_fire_time_changed, load_json_object_fixture, ) -from tests.components.opensky import setup_integration async def test_sensor( diff --git a/tests/components/overkiz/conftest.py b/tests/components/overkiz/conftest.py index ea021ccef1e..8ab26e3587b 100644 --- a/tests/components/overkiz/conftest.py +++ b/tests/components/overkiz/conftest.py @@ -8,14 +8,10 @@ from typing_extensions import Generator from homeassistant.components.overkiz.const import DOMAIN from homeassistant.core import HomeAssistant +from . import load_setup_fixture +from .test_config_flow import TEST_EMAIL, TEST_GATEWAY_ID, TEST_PASSWORD, TEST_SERVER + from tests.common import MockConfigEntry -from tests.components.overkiz import load_setup_fixture -from tests.components.overkiz.test_config_flow import ( - TEST_EMAIL, - TEST_GATEWAY_ID, - TEST_PASSWORD, - TEST_SERVER, -) MOCK_SETUP_RESPONSE = Mock(devices=[], gateways=[]) diff --git a/tests/components/recorder/test_purge_v32_schema.py b/tests/components/recorder/test_purge_v32_schema.py index e5bd0eae060..fb636cfa9dc 100644 --- a/tests/components/recorder/test_purge_v32_schema.py +++ b/tests/components/recorder/test_purge_v32_schema.py @@ -34,8 +34,7 @@ from .common import ( async_wait_recording_done, old_db_schema, ) - -from tests.components.recorder.db_schema_32 import ( +from .db_schema_32 import ( EventData, Events, RecorderRuns, @@ -44,6 +43,7 @@ from tests.components.recorder.db_schema_32 import ( StatisticsRuns, StatisticsShortTerm, ) + from tests.typing import RecorderInstanceGenerator diff --git a/tests/components/roborock/test_image.py b/tests/components/roborock/test_image.py index bc45c6dec05..c884baef123 100644 --- a/tests/components/roborock/test_image.py +++ b/tests/components/roborock/test_image.py @@ -13,8 +13,9 @@ from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util +from .mock_data import MAP_DATA, PROP + from tests.common import MockConfigEntry, async_fire_time_changed -from tests.components.roborock.mock_data import MAP_DATA, PROP from tests.typing import ClientSessionGenerator diff --git a/tests/components/roborock/test_vacuum.py b/tests/components/roborock/test_vacuum.py index ea1075726ba..15a64cbecf3 100644 --- a/tests/components/roborock/test_vacuum.py +++ b/tests/components/roborock/test_vacuum.py @@ -27,8 +27,9 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component +from .mock_data import PROP + from tests.common import MockConfigEntry -from tests.components.roborock.mock_data import PROP ENTITY_ID = "vacuum.roborock_s7_maxv" DEVICE_ID = "abc123" diff --git a/tests/components/rova/test_init.py b/tests/components/rova/test_init.py index e522d5bfb12..2190e2f8ce3 100644 --- a/tests/components/rova/test_init.py +++ b/tests/components/rova/test_init.py @@ -12,8 +12,9 @@ from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, issue_registry as ir +from . import setup_with_selected_platforms + from tests.common import MockConfigEntry -from tests.components.rova import setup_with_selected_platforms async def test_reload( From 2e3c3789d3e2462cf387c401f0b08179cccce21b Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 10 Jun 2024 13:33:15 +0200 Subject: [PATCH 0457/1445] Use relative imports in tests [s-z] (#119283) --- tests/components/sanix/test_init.py | 3 ++- tests/components/select/conftest.py | 2 +- tests/components/sensor/test_device_condition.py | 3 ++- tests/components/sensor/test_device_trigger.py | 3 ++- tests/components/sensor/test_init.py | 3 ++- tests/components/sensor/test_recorder.py | 3 ++- tests/components/seventeentrack/test_services.py | 5 +++-- tests/components/streamlabswater/test_binary_sensor.py | 3 ++- tests/components/streamlabswater/test_sensor.py | 3 ++- tests/components/text/test_init.py | 3 ++- tests/components/time/test_init.py | 3 ++- tests/components/trend/test_init.py | 3 ++- tests/components/twitch/test_config_flow.py | 2 +- tests/components/update/test_device_trigger.py | 3 ++- tests/components/update/test_recorder.py | 3 ++- tests/components/withings/conftest.py | 5 +++-- tests/components/withings/test_calendar.py | 3 +-- tests/components/withings/test_diagnostics.py | 3 ++- tests/components/youtube/conftest.py | 3 ++- tests/components/zha/test_base.py | 2 +- 20 files changed, 38 insertions(+), 23 deletions(-) diff --git a/tests/components/sanix/test_init.py b/tests/components/sanix/test_init.py index 467737628fe..af3a3615669 100644 --- a/tests/components/sanix/test_init.py +++ b/tests/components/sanix/test_init.py @@ -7,8 +7,9 @@ from unittest.mock import AsyncMock from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant +from . import setup_integration + from tests.common import MockConfigEntry -from tests.components.sanix import setup_integration async def test_load_unload_entry( diff --git a/tests/components/select/conftest.py b/tests/components/select/conftest.py index 700749f9aba..6e789f88573 100644 --- a/tests/components/select/conftest.py +++ b/tests/components/select/conftest.py @@ -2,7 +2,7 @@ import pytest -from tests.components.select.common import MockSelectEntity +from .common import MockSelectEntity @pytest.fixture diff --git a/tests/components/sensor/test_device_condition.py b/tests/components/sensor/test_device_condition.py index 02eaa2c9739..dc81ec696f8 100644 --- a/tests/components/sensor/test_device_condition.py +++ b/tests/components/sensor/test_device_condition.py @@ -21,6 +21,8 @@ from homeassistant.helpers.entity_registry import RegistryEntryHider from homeassistant.setup import async_setup_component from homeassistant.util.json import load_json +from .common import UNITS_OF_MEASUREMENT, MockSensor + from tests.common import ( MockConfigEntry, async_get_device_automation_capabilities, @@ -28,7 +30,6 @@ from tests.common import ( async_mock_service, setup_test_component_platform, ) -from tests.components.sensor.common import UNITS_OF_MEASUREMENT, MockSensor @pytest.fixture(autouse=True, name="stub_blueprint_populate") diff --git a/tests/components/sensor/test_device_trigger.py b/tests/components/sensor/test_device_trigger.py index c98fe1e3a52..922a83709f7 100644 --- a/tests/components/sensor/test_device_trigger.py +++ b/tests/components/sensor/test_device_trigger.py @@ -24,6 +24,8 @@ from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util from homeassistant.util.json import load_json +from .common import UNITS_OF_MEASUREMENT, MockSensor + from tests.common import ( MockConfigEntry, async_fire_time_changed, @@ -32,7 +34,6 @@ from tests.common import ( async_mock_service, setup_test_component_platform, ) -from tests.components.sensor.common import UNITS_OF_MEASUREMENT, MockSensor @pytest.fixture(autouse=True, name="stub_blueprint_populate") diff --git a/tests/components/sensor/test_init.py b/tests/components/sensor/test_init.py index 9a1af587a0a..0aa0ff3de85 100644 --- a/tests/components/sensor/test_init.py +++ b/tests/components/sensor/test_init.py @@ -51,6 +51,8 @@ from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util from homeassistant.util.unit_system import METRIC_SYSTEM, US_CUSTOMARY_SYSTEM +from .common import MockRestoreSensor, MockSensor + from tests.common import ( MockConfigEntry, MockEntityPlatform, @@ -65,7 +67,6 @@ from tests.common import ( mock_restore_cache_with_extra_data, setup_test_component_platform, ) -from tests.components.sensor.common import MockRestoreSensor, MockSensor TEST_DOMAIN = "test" diff --git a/tests/components/sensor/test_recorder.py b/tests/components/sensor/test_recorder.py index ea02674a8d1..3762b3f083a 100644 --- a/tests/components/sensor/test_recorder.py +++ b/tests/components/sensor/test_recorder.py @@ -41,6 +41,8 @@ from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util from homeassistant.util.unit_system import METRIC_SYSTEM, US_CUSTOMARY_SYSTEM +from .common import MockSensor + from tests.common import setup_test_component_platform from tests.components.recorder.common import ( assert_dict_of_states_equal_without_context_and_last_changed, @@ -50,7 +52,6 @@ from tests.components.recorder.common import ( do_adhoc_statistics, statistics_during_period, ) -from tests.components.sensor.common import MockSensor from tests.typing import RecorderInstanceGenerator, WebSocketGenerator BATTERY_SENSOR_ATTRIBUTES = { diff --git a/tests/components/seventeentrack/test_services.py b/tests/components/seventeentrack/test_services.py index cbd7132bf67..148286d66d4 100644 --- a/tests/components/seventeentrack/test_services.py +++ b/tests/components/seventeentrack/test_services.py @@ -7,9 +7,10 @@ from syrupy import SnapshotAssertion from homeassistant.components.seventeentrack import DOMAIN, SERVICE_GET_PACKAGES from homeassistant.core import HomeAssistant, SupportsResponse +from . import init_integration +from .conftest import get_package + from tests.common import MockConfigEntry -from tests.components.seventeentrack import init_integration -from tests.components.seventeentrack.conftest import get_package async def test_get_packages_from_list( diff --git a/tests/components/streamlabswater/test_binary_sensor.py b/tests/components/streamlabswater/test_binary_sensor.py index 7c9351c5e69..7beb088d498 100644 --- a/tests/components/streamlabswater/test_binary_sensor.py +++ b/tests/components/streamlabswater/test_binary_sensor.py @@ -8,8 +8,9 @@ from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er +from . import setup_integration + from tests.common import MockConfigEntry -from tests.components.streamlabswater import setup_integration async def test_all_entities( diff --git a/tests/components/streamlabswater/test_sensor.py b/tests/components/streamlabswater/test_sensor.py index f27b61d724b..6afb71f3fd7 100644 --- a/tests/components/streamlabswater/test_sensor.py +++ b/tests/components/streamlabswater/test_sensor.py @@ -8,8 +8,9 @@ from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er +from . import setup_integration + from tests.common import MockConfigEntry -from tests.components.streamlabswater import setup_integration async def test_all_entities( diff --git a/tests/components/text/test_init.py b/tests/components/text/test_init.py index deacf029ced..8e20af6cb7a 100644 --- a/tests/components/text/test_init.py +++ b/tests/components/text/test_init.py @@ -20,12 +20,13 @@ from homeassistant.core import HomeAssistant, ServiceCall, State from homeassistant.helpers.restore_state import STORAGE_KEY as RESTORE_STATE_KEY from homeassistant.setup import async_setup_component +from .common import MockRestoreText, MockTextEntity + from tests.common import ( async_mock_restore_state_shutdown_restart, mock_restore_cache_with_extra_data, setup_test_component_platform, ) -from tests.components.text.common import MockRestoreText, MockTextEntity async def test_text_default(hass: HomeAssistant) -> None: diff --git a/tests/components/time/test_init.py b/tests/components/time/test_init.py index 0f0dbe05e5b..f616570f956 100644 --- a/tests/components/time/test_init.py +++ b/tests/components/time/test_init.py @@ -12,8 +12,9 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component +from .common import MockTimeEntity + from tests.common import setup_test_component_platform -from tests.components.time.common import MockTimeEntity async def test_date(hass: HomeAssistant) -> None: diff --git a/tests/components/trend/test_init.py b/tests/components/trend/test_init.py index c926d1cb771..eea76025d65 100644 --- a/tests/components/trend/test_init.py +++ b/tests/components/trend/test_init.py @@ -4,8 +4,9 @@ from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er +from .conftest import ComponentSetup + from tests.common import MockConfigEntry -from tests.components.trend.conftest import ComponentSetup async def test_setup_and_remove_config_entry( diff --git a/tests/components/twitch/test_config_flow.py b/tests/components/twitch/test_config_flow.py index 7d677df1adb..6935943a4d3 100644 --- a/tests/components/twitch/test_config_flow.py +++ b/tests/components/twitch/test_config_flow.py @@ -16,9 +16,9 @@ from homeassistant.data_entry_flow import FlowResult, FlowResultType from homeassistant.helpers import config_entry_oauth2_flow from . import get_generator, setup_integration +from .conftest import CLIENT_ID, TITLE from tests.common import MockConfigEntry -from tests.components.twitch.conftest import CLIENT_ID, TITLE from tests.typing import ClientSessionGenerator diff --git a/tests/components/update/test_device_trigger.py b/tests/components/update/test_device_trigger.py index 6ece4f818d1..69719d4453b 100644 --- a/tests/components/update/test_device_trigger.py +++ b/tests/components/update/test_device_trigger.py @@ -14,6 +14,8 @@ from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util +from .common import MockUpdateEntity + from tests.common import ( MockConfigEntry, async_fire_time_changed, @@ -22,7 +24,6 @@ from tests.common import ( async_mock_service, setup_test_component_platform, ) -from tests.components.update.common import MockUpdateEntity @pytest.fixture(autouse=True, name="stub_blueprint_populate") diff --git a/tests/components/update/test_recorder.py b/tests/components/update/test_recorder.py index da63518009e..0bd209ce1c2 100644 --- a/tests/components/update/test_recorder.py +++ b/tests/components/update/test_recorder.py @@ -17,9 +17,10 @@ from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util +from .common import MockUpdateEntity + from tests.common import async_fire_time_changed, setup_test_component_platform from tests.components.recorder.common import async_wait_recording_done -from tests.components.update.common import MockUpdateEntity async def test_exclude_attributes( diff --git a/tests/components/withings/conftest.py b/tests/components/withings/conftest.py index 66dd65efccb..dfb0658b64a 100644 --- a/tests/components/withings/conftest.py +++ b/tests/components/withings/conftest.py @@ -16,8 +16,7 @@ from homeassistant.components.withings.const import DOMAIN from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component -from tests.common import MockConfigEntry, load_json_array_fixture -from tests.components.withings import ( +from . import ( load_activity_fixture, load_goals_fixture, load_measurements_fixture, @@ -25,6 +24,8 @@ from tests.components.withings import ( load_workout_fixture, ) +from tests.common import MockConfigEntry, load_json_array_fixture + CLIENT_ID = "1234" CLIENT_SECRET = "5678" SCOPES = [ diff --git a/tests/components/withings/test_calendar.py b/tests/components/withings/test_calendar.py index 060a1baa54d..c04a93ba43d 100644 --- a/tests/components/withings/test_calendar.py +++ b/tests/components/withings/test_calendar.py @@ -9,10 +9,9 @@ from syrupy.assertion import SnapshotAssertion from homeassistant.core import HomeAssistant -from . import load_workout_fixture +from . import load_workout_fixture, setup_integration from tests.common import MockConfigEntry, async_fire_time_changed -from tests.components.withings import setup_integration from tests.typing import ClientSessionGenerator diff --git a/tests/components/withings/test_diagnostics.py b/tests/components/withings/test_diagnostics.py index d607584df7b..51f54b2ab17 100644 --- a/tests/components/withings/test_diagnostics.py +++ b/tests/components/withings/test_diagnostics.py @@ -7,9 +7,10 @@ from syrupy import SnapshotAssertion from homeassistant.core import HomeAssistant +from . import prepare_webhook_setup, setup_integration + from tests.common import MockConfigEntry from tests.components.diagnostics import get_diagnostics_for_config_entry -from tests.components.withings import prepare_webhook_setup, setup_integration from tests.typing import ClientSessionGenerator diff --git a/tests/components/youtube/conftest.py b/tests/components/youtube/conftest.py index 0673efd42b5..7f1caef47b5 100644 --- a/tests/components/youtube/conftest.py +++ b/tests/components/youtube/conftest.py @@ -15,8 +15,9 @@ from homeassistant.components.youtube.const import DOMAIN from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component +from . import MockYouTube + from tests.common import MockConfigEntry -from tests.components.youtube import MockYouTube from tests.test_util.aiohttp import AiohttpClientMocker type ComponentSetup = Callable[[], Awaitable[MockYouTube]] diff --git a/tests/components/zha/test_base.py b/tests/components/zha/test_base.py index ee5293d16b9..203df2ffda5 100644 --- a/tests/components/zha/test_base.py +++ b/tests/components/zha/test_base.py @@ -2,7 +2,7 @@ from homeassistant.components.zha.core.cluster_handlers import parse_and_log_command -from tests.components.zha.test_cluster_handlers import ( # noqa: F401 +from .test_cluster_handlers import ( # noqa: F401 endpoint, poll_control_ch, zigpy_coordinator_device, From 960d1289efdd56899d11e7f13aeb99b065b6f28a Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 10 Jun 2024 13:49:44 +0200 Subject: [PATCH 0458/1445] Avoid references to websocket_api.const in core and other components (#119285) --- .../components/assist_pipeline/pipeline.py | 8 +++----- .../components/assist_pipeline/websocket_api.py | 4 ++-- homeassistant/components/auth/__init__.py | 2 +- homeassistant/components/automation/__init__.py | 2 +- homeassistant/components/cloud/http_api.py | 6 +++--- homeassistant/components/config/auth.py | 2 +- .../components/config/config_entries.py | 4 +--- .../components/config/device_registry.py | 2 +- .../components/config/entity_registry.py | 4 ++-- homeassistant/components/conversation/http.py | 4 +--- .../components/device_automation/__init__.py | 2 +- .../components/homeassistant/exposed_entities.py | 2 +- homeassistant/components/insteon/api/device.py | 2 +- homeassistant/components/knx/websocket.py | 2 +- homeassistant/components/logger/websocket_api.py | 2 +- homeassistant/components/script/__init__.py | 2 +- .../components/shopping_list/__init__.py | 4 ++-- homeassistant/components/template/config_flow.py | 2 +- homeassistant/components/thread/websocket_api.py | 16 +++++----------- homeassistant/components/tts/__init__.py | 4 ++-- homeassistant/components/update/__init__.py | 4 ++-- homeassistant/components/wake_word/__init__.py | 4 ++-- .../components/websocket_api/__init__.py | 2 ++ homeassistant/components/zha/websocket_api.py | 10 +++++----- homeassistant/helpers/collection.py | 16 ++++++---------- 25 files changed, 49 insertions(+), 63 deletions(-) diff --git a/homeassistant/components/assist_pipeline/pipeline.py b/homeassistant/components/assist_pipeline/pipeline.py index 4bc008d895b..1471af2ea41 100644 --- a/homeassistant/components/assist_pipeline/pipeline.py +++ b/homeassistant/components/assist_pipeline/pipeline.py @@ -1648,9 +1648,7 @@ class PipelineStorageCollectionWebsocket( try: await super().ws_delete_item(hass, connection, msg) except PipelinePreferred as exc: - connection.send_error( - msg["id"], websocket_api.const.ERR_NOT_ALLOWED, str(exc) - ) + connection.send_error(msg["id"], websocket_api.ERR_NOT_ALLOWED, str(exc)) @callback def ws_get_item( @@ -1664,7 +1662,7 @@ class PipelineStorageCollectionWebsocket( if item_id not in self.storage_collection.data: connection.send_error( msg["id"], - websocket_api.const.ERR_NOT_FOUND, + websocket_api.ERR_NOT_FOUND, f"Unable to find {self.item_id_key} {item_id}", ) return @@ -1695,7 +1693,7 @@ class PipelineStorageCollectionWebsocket( self.storage_collection.async_set_preferred_item(msg[self.item_id_key]) except ItemNotFound: connection.send_error( - msg["id"], websocket_api.const.ERR_NOT_FOUND, "unknown item" + msg["id"], websocket_api.ERR_NOT_FOUND, "unknown item" ) return connection.send_result(msg["id"]) diff --git a/homeassistant/components/assist_pipeline/websocket_api.py b/homeassistant/components/assist_pipeline/websocket_api.py index 56effd50a3e..18464810525 100644 --- a/homeassistant/components/assist_pipeline/websocket_api.py +++ b/homeassistant/components/assist_pipeline/websocket_api.py @@ -353,7 +353,7 @@ def websocket_get_run( if pipeline_id not in pipeline_data.pipeline_debug: connection.send_error( msg["id"], - websocket_api.const.ERR_NOT_FOUND, + websocket_api.ERR_NOT_FOUND, f"pipeline_id {pipeline_id} not found", ) return @@ -363,7 +363,7 @@ def websocket_get_run( if pipeline_run_id not in pipeline_debug: connection.send_error( msg["id"], - websocket_api.const.ERR_NOT_FOUND, + websocket_api.ERR_NOT_FOUND, f"pipeline_run_id {pipeline_run_id} not found", ) return diff --git a/homeassistant/components/auth/__init__.py b/homeassistant/components/auth/__init__.py index 6e4bbac8b63..cef7af4df92 100644 --- a/homeassistant/components/auth/__init__.py +++ b/homeassistant/components/auth/__init__.py @@ -544,7 +544,7 @@ async def websocket_create_long_lived_access_token( try: access_token = hass.auth.async_create_access_token(refresh_token) except InvalidAuthError as exc: - connection.send_error(msg["id"], websocket_api.const.ERR_UNAUTHORIZED, str(exc)) + connection.send_error(msg["id"], websocket_api.ERR_UNAUTHORIZED, str(exc)) return connection.send_result(msg["id"], access_token) diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 977008df1f8..deb3613d668 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -1208,7 +1208,7 @@ def websocket_config( if automation is None: connection.send_error( - msg["id"], websocket_api.const.ERR_NOT_FOUND, "Entity not found" + msg["id"], websocket_api.ERR_NOT_FOUND, "Entity not found" ) return diff --git a/homeassistant/components/cloud/http_api.py b/homeassistant/components/cloud/http_api.py index 757bd27e212..bd2860b19df 100644 --- a/homeassistant/components/cloud/http_api.py +++ b/homeassistant/components/cloud/http_api.py @@ -642,7 +642,7 @@ async def google_assistant_get( if not state: connection.send_error( msg["id"], - websocket_api.const.ERR_NOT_FOUND, + websocket_api.ERR_NOT_FOUND, f"{entity_id} unknown", ) return @@ -651,7 +651,7 @@ async def google_assistant_get( if entity_id in CLOUD_NEVER_EXPOSED_ENTITIES or not entity.is_supported(): connection.send_error( msg["id"], - websocket_api.const.ERR_NOT_SUPPORTED, + websocket_api.ERR_NOT_SUPPORTED, f"{entity_id} not supported by Google assistant", ) return @@ -755,7 +755,7 @@ async def alexa_get( ): connection.send_error( msg["id"], - websocket_api.const.ERR_NOT_SUPPORTED, + websocket_api.ERR_NOT_SUPPORTED, f"{entity_id} not supported by Alexa", ) return diff --git a/homeassistant/components/config/auth.py b/homeassistant/components/config/auth.py index 266c06d6ee8..1b3fa71d7ea 100644 --- a/homeassistant/components/config/auth.py +++ b/homeassistant/components/config/auth.py @@ -121,7 +121,7 @@ async def websocket_update( if not (user := await hass.auth.async_get_user(msg.pop("user_id"))): connection.send_message( websocket_api.error_message( - msg["id"], websocket_api.const.ERR_NOT_FOUND, "User not found" + msg["id"], websocket_api.ERR_NOT_FOUND, "User not found" ) ) return diff --git a/homeassistant/components/config/config_entries.py b/homeassistant/components/config/config_entries.py index 8eb4eb22fb5..b16701f8bd0 100644 --- a/homeassistant/components/config/config_entries.py +++ b/homeassistant/components/config/config_entries.py @@ -311,9 +311,7 @@ def send_entry_not_found( connection: websocket_api.ActiveConnection, msg_id: int ) -> None: """Send Config entry not found error.""" - connection.send_error( - msg_id, websocket_api.const.ERR_NOT_FOUND, "Config entry not found" - ) + connection.send_error(msg_id, websocket_api.ERR_NOT_FOUND, "Config entry not found") def get_entry( diff --git a/homeassistant/components/config/device_registry.py b/homeassistant/components/config/device_registry.py index 2cc05978267..a5d506e5a8d 100644 --- a/homeassistant/components/config/device_registry.py +++ b/homeassistant/components/config/device_registry.py @@ -42,7 +42,7 @@ def websocket_list_devices( registry = dr.async_get(hass) # Build start of response message msg_json_prefix = ( - f'{{"id":{msg["id"]},"type": "{websocket_api.const.TYPE_RESULT}",' + f'{{"id":{msg["id"]},"type": "{websocket_api.TYPE_RESULT}",' f'"success":true,"result": [' ).encode() # Concatenate cached entity registry item JSON serializations diff --git a/homeassistant/components/config/entity_registry.py b/homeassistant/components/config/entity_registry.py index 7cdec324340..bf7a9087d56 100644 --- a/homeassistant/components/config/entity_registry.py +++ b/homeassistant/components/config/entity_registry.py @@ -43,7 +43,7 @@ def websocket_list_entities( registry = er.async_get(hass) # Build start of response message msg_json_prefix = ( - f'{{"id":{msg["id"]},"type": "{websocket_api.const.TYPE_RESULT}",' + f'{{"id":{msg["id"]},"type": "{websocket_api.TYPE_RESULT}",' '"success":true,"result": [' ).encode() # Concatenate cached entity registry item JSON serializations @@ -74,7 +74,7 @@ def websocket_list_entities_for_display( registry = er.async_get(hass) # Build start of response message msg_json_prefix = ( - f'{{"id":{msg["id"]},"type":"{websocket_api.const.TYPE_RESULT}","success":true,' + f'{{"id":{msg["id"]},"type":"{websocket_api.TYPE_RESULT}","success":true,' f'"result":{{"entity_categories":{_ENTITY_CATEGORIES_JSON},"entities":[' ).encode() # Concatenate cached entity registry item JSON serializations diff --git a/homeassistant/components/conversation/http.py b/homeassistant/components/conversation/http.py index e0821e14738..591298cbac1 100644 --- a/homeassistant/components/conversation/http.py +++ b/homeassistant/components/conversation/http.py @@ -94,9 +94,7 @@ async def websocket_prepare( agent = async_get_agent(hass, msg.get("agent_id")) if agent is None: - connection.send_error( - msg["id"], websocket_api.const.ERR_NOT_FOUND, "Agent not found" - ) + connection.send_error(msg["id"], websocket_api.ERR_NOT_FOUND, "Agent not found") return await agent.async_prepare(msg.get("language")) diff --git a/homeassistant/components/device_automation/__init__.py b/homeassistant/components/device_automation/__init__.py index b79c9e56a95..567b8fcc2d2 100644 --- a/homeassistant/components/device_automation/__init__.py +++ b/homeassistant/components/device_automation/__init__.py @@ -369,7 +369,7 @@ def handle_device_errors( await func(hass, connection, msg) except DeviceNotFound: connection.send_error( - msg["id"], websocket_api.const.ERR_NOT_FOUND, "Device not found" + msg["id"], websocket_api.ERR_NOT_FOUND, "Device not found" ) return with_error_handling diff --git a/homeassistant/components/homeassistant/exposed_entities.py b/homeassistant/components/homeassistant/exposed_entities.py index 82848b0e273..68632223045 100644 --- a/homeassistant/components/homeassistant/exposed_entities.py +++ b/homeassistant/components/homeassistant/exposed_entities.py @@ -417,7 +417,7 @@ def ws_expose_entity( None, ): connection.send_error( - msg["id"], websocket_api.const.ERR_NOT_ALLOWED, f"can't expose '{blocked}'" + msg["id"], websocket_api.ERR_NOT_ALLOWED, f"can't expose '{blocked}'" ) return diff --git a/homeassistant/components/insteon/api/device.py b/homeassistant/components/insteon/api/device.py index e8bd08bc4ee..ff688eef40c 100644 --- a/homeassistant/components/insteon/api/device.py +++ b/homeassistant/components/insteon/api/device.py @@ -65,7 +65,7 @@ async def async_device_name(dev_registry, address): def notify_device_not_found(connection, msg, text): """Notify the caller that the device was not found.""" connection.send_message( - websocket_api.error_message(msg[ID], websocket_api.const.ERR_NOT_FOUND, text) + websocket_api.error_message(msg[ID], websocket_api.ERR_NOT_FOUND, text) ) diff --git a/homeassistant/components/knx/websocket.py b/homeassistant/components/knx/websocket.py index f6869902793..dc5b5e483be 100644 --- a/homeassistant/components/knx/websocket.py +++ b/homeassistant/components/knx/websocket.py @@ -131,7 +131,7 @@ async def ws_project_file_process( except (ValueError, XknxProjectException) as err: # ValueError could raise from file_upload integration connection.send_error( - msg["id"], websocket_api.const.ERR_HOME_ASSISTANT_ERROR, str(err) + msg["id"], websocket_api.ERR_HOME_ASSISTANT_ERROR, str(err) ) return diff --git a/homeassistant/components/logger/websocket_api.py b/homeassistant/components/logger/websocket_api.py index fafc2d3eedb..6d34b10bd34 100644 --- a/homeassistant/components/logger/websocket_api.py +++ b/homeassistant/components/logger/websocket_api.py @@ -65,7 +65,7 @@ async def handle_integration_log_level( await async_get_integration(hass, msg["integration"]) except IntegrationNotFound: connection.send_error( - msg["id"], websocket_api.const.ERR_NOT_FOUND, "Integration not found" + msg["id"], websocket_api.ERR_NOT_FOUND, "Integration not found" ) return await async_get_domain_config(hass).settings.async_update( diff --git a/homeassistant/components/script/__init__.py b/homeassistant/components/script/__init__.py index 65cea1e2e4c..f19a48fea33 100644 --- a/homeassistant/components/script/__init__.py +++ b/homeassistant/components/script/__init__.py @@ -717,7 +717,7 @@ def websocket_config( if script is None: connection.send_error( - msg["id"], websocket_api.const.ERR_NOT_FOUND, "Entity not found" + msg["id"], websocket_api.ERR_NOT_FOUND, "Entity not found" ) return diff --git a/homeassistant/components/shopping_list/__init__.py b/homeassistant/components/shopping_list/__init__.py index 1176192bdcd..20d3078228c 100644 --- a/homeassistant/components/shopping_list/__init__.py +++ b/homeassistant/components/shopping_list/__init__.py @@ -582,12 +582,12 @@ def websocket_handle_reorder( except NoMatchingShoppingListItem: connection.send_error( msg_id, - websocket_api.const.ERR_NOT_FOUND, + websocket_api.ERR_NOT_FOUND, "One or more item id(s) not found.", ) return except vol.Invalid as err: - connection.send_error(msg_id, websocket_api.const.ERR_INVALID_FORMAT, f"{err}") + connection.send_error(msg_id, websocket_api.ERR_INVALID_FORMAT, f"{err}") return connection.send_result(msg_id) diff --git a/homeassistant/components/template/config_flow.py b/homeassistant/components/template/config_flow.py index 5d0cb99826f..8a5ecca5b4b 100644 --- a/homeassistant/components/template/config_flow.py +++ b/homeassistant/components/template/config_flow.py @@ -344,7 +344,7 @@ def ws_start_preview( connection.send_message( { "id": msg["id"], - "type": websocket_api.const.TYPE_RESULT, + "type": websocket_api.TYPE_RESULT, "success": False, "error": {"code": "invalid_user_input", "message": errors}, } diff --git a/homeassistant/components/thread/websocket_api.py b/homeassistant/components/thread/websocket_api.py index 687c4067caf..d436a5ffb72 100644 --- a/homeassistant/components/thread/websocket_api.py +++ b/homeassistant/components/thread/websocket_api.py @@ -44,9 +44,7 @@ async def ws_add_dataset( try: await dataset_store.async_add_dataset(hass, source, tlv) except TLVError as exc: - connection.send_error( - msg["id"], websocket_api.const.ERR_INVALID_FORMAT, str(exc) - ) + connection.send_error(msg["id"], websocket_api.ERR_INVALID_FORMAT, str(exc)) return connection.send_result(msg["id"]) @@ -94,9 +92,7 @@ async def ws_set_preferred_dataset( try: store.preferred_dataset = dataset_id except KeyError: - connection.send_error( - msg["id"], websocket_api.const.ERR_NOT_FOUND, "unknown dataset" - ) + connection.send_error(msg["id"], websocket_api.ERR_NOT_FOUND, "unknown dataset") return connection.send_result(msg["id"]) @@ -120,10 +116,10 @@ async def ws_delete_dataset( try: store.async_delete(dataset_id) except KeyError as exc: - connection.send_error(msg["id"], websocket_api.const.ERR_NOT_FOUND, str(exc)) + connection.send_error(msg["id"], websocket_api.ERR_NOT_FOUND, str(exc)) return except dataset_store.DatasetPreferredError as exc: - connection.send_error(msg["id"], websocket_api.const.ERR_NOT_ALLOWED, str(exc)) + connection.send_error(msg["id"], websocket_api.ERR_NOT_ALLOWED, str(exc)) return connection.send_result(msg["id"]) @@ -145,9 +141,7 @@ async def ws_get_dataset( store = await dataset_store.async_get_store(hass) if not (dataset := store.async_get(dataset_id)): - connection.send_error( - msg["id"], websocket_api.const.ERR_NOT_FOUND, "unknown dataset" - ) + connection.send_error(msg["id"], websocket_api.ERR_NOT_FOUND, "unknown dataset") return connection.send_result(msg["id"], {"tlv": dataset.tlv}) diff --git a/homeassistant/components/tts/__init__.py b/homeassistant/components/tts/__init__.py index 3055bf46ca7..15cd10552ed 100644 --- a/homeassistant/components/tts/__init__.py +++ b/homeassistant/components/tts/__init__.py @@ -1114,7 +1114,7 @@ def websocket_get_engine( if not provider: connection.send_error( msg["id"], - websocket_api.const.ERR_NOT_FOUND, + websocket_api.ERR_NOT_FOUND, f"tts engine {engine_id} not found", ) return @@ -1149,7 +1149,7 @@ def websocket_list_engine_voices( if not engine_instance: connection.send_error( msg["id"], - websocket_api.const.ERR_NOT_FOUND, + websocket_api.ERR_NOT_FOUND, f"tts engine {engine_id} not found", ) return diff --git a/homeassistant/components/update/__init__.py b/homeassistant/components/update/__init__.py index 57d63c92ede..352237bf201 100644 --- a/homeassistant/components/update/__init__.py +++ b/homeassistant/components/update/__init__.py @@ -495,14 +495,14 @@ async def websocket_release_notes( if entity is None: connection.send_error( - msg["id"], websocket_api.const.ERR_NOT_FOUND, "Entity not found" + msg["id"], websocket_api.ERR_NOT_FOUND, "Entity not found" ) return if UpdateEntityFeature.RELEASE_NOTES not in entity.supported_features_compat: connection.send_error( msg["id"], - websocket_api.const.ERR_NOT_SUPPORTED, + websocket_api.ERR_NOT_SUPPORTED, "Entity does not support release notes", ) return diff --git a/homeassistant/components/wake_word/__init__.py b/homeassistant/components/wake_word/__init__.py index f05a61e34dc..5ce592aacd8 100644 --- a/homeassistant/components/wake_word/__init__.py +++ b/homeassistant/components/wake_word/__init__.py @@ -147,7 +147,7 @@ async def websocket_entity_info( if entity is None: connection.send_error( - msg["id"], websocket_api.const.ERR_NOT_FOUND, "Entity not found" + msg["id"], websocket_api.ERR_NOT_FOUND, "Entity not found" ) return @@ -156,7 +156,7 @@ async def websocket_entity_info( wake_words = await entity.get_supported_wake_words() except TimeoutError: connection.send_error( - msg["id"], websocket_api.const.ERR_TIMEOUT, "Timeout fetching wake words" + msg["id"], websocket_api.ERR_TIMEOUT, "Timeout fetching wake words" ) return diff --git a/homeassistant/components/websocket_api/__init__.py b/homeassistant/components/websocket_api/__init__.py index aad161eba34..d8427bff10e 100644 --- a/homeassistant/components/websocket_api/__init__.py +++ b/homeassistant/components/websocket_api/__init__.py @@ -16,6 +16,7 @@ from .connection import ActiveConnection, current_connection # noqa: F401 from .const import ( # noqa: F401 ERR_HOME_ASSISTANT_ERROR, ERR_INVALID_FORMAT, + ERR_NOT_ALLOWED, ERR_NOT_FOUND, ERR_NOT_SUPPORTED, ERR_SERVICE_VALIDATION_ERROR, @@ -24,6 +25,7 @@ from .const import ( # noqa: F401 ERR_UNAUTHORIZED, ERR_UNKNOWN_COMMAND, ERR_UNKNOWN_ERROR, + TYPE_RESULT, AsyncWebSocketCommandHandler, WebSocketCommandHandler, ) diff --git a/homeassistant/components/zha/websocket_api.py b/homeassistant/components/zha/websocket_api.py index 70be438bf24..1a51a06243e 100644 --- a/homeassistant/components/zha/websocket_api.py +++ b/homeassistant/components/zha/websocket_api.py @@ -443,7 +443,7 @@ async def websocket_get_device( if not (zha_device := zha_gateway.devices.get(ieee)): connection.send_message( websocket_api.error_message( - msg[ID], websocket_api.const.ERR_NOT_FOUND, "ZHA Device not found" + msg[ID], websocket_api.ERR_NOT_FOUND, "ZHA Device not found" ) ) return @@ -470,7 +470,7 @@ async def websocket_get_group( if not (zha_group := zha_gateway.groups.get(group_id)): connection.send_message( websocket_api.error_message( - msg[ID], websocket_api.const.ERR_NOT_FOUND, "ZHA Group not found" + msg[ID], websocket_api.ERR_NOT_FOUND, "ZHA Group not found" ) ) return @@ -548,7 +548,7 @@ async def websocket_add_group_members( if not (zha_group := zha_gateway.groups.get(group_id)): connection.send_message( websocket_api.error_message( - msg[ID], websocket_api.const.ERR_NOT_FOUND, "ZHA Group not found" + msg[ID], websocket_api.ERR_NOT_FOUND, "ZHA Group not found" ) ) return @@ -578,7 +578,7 @@ async def websocket_remove_group_members( if not (zha_group := zha_gateway.groups.get(group_id)): connection.send_message( websocket_api.error_message( - msg[ID], websocket_api.const.ERR_NOT_FOUND, "ZHA Group not found" + msg[ID], websocket_api.ERR_NOT_FOUND, "ZHA Group not found" ) ) return @@ -1214,7 +1214,7 @@ async def websocket_restore_network_backup( try: await application_controller.backups.restore_backup(backup) except ValueError as err: - connection.send_error(msg[ID], websocket_api.const.ERR_INVALID_FORMAT, str(err)) + connection.send_error(msg[ID], websocket_api.ERR_INVALID_FORMAT, str(err)) else: connection.send_result(msg[ID]) diff --git a/homeassistant/helpers/collection.py b/homeassistant/helpers/collection.py index c69295ed1b1..bf65b47f451 100644 --- a/homeassistant/helpers/collection.py +++ b/homeassistant/helpers/collection.py @@ -619,13 +619,11 @@ class StorageCollectionWebsocket[_StorageCollectionT: StorageCollection]: except vol.Invalid as err: connection.send_error( msg["id"], - websocket_api.const.ERR_INVALID_FORMAT, + websocket_api.ERR_INVALID_FORMAT, humanize_error(data, err), ) except ValueError as err: - connection.send_error( - msg["id"], websocket_api.const.ERR_INVALID_FORMAT, str(err) - ) + connection.send_error(msg["id"], websocket_api.ERR_INVALID_FORMAT, str(err)) async def ws_update_item( self, hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict @@ -642,19 +640,17 @@ class StorageCollectionWebsocket[_StorageCollectionT: StorageCollection]: except ItemNotFound: connection.send_error( msg["id"], - websocket_api.const.ERR_NOT_FOUND, + websocket_api.ERR_NOT_FOUND, f"Unable to find {self.item_id_key} {item_id}", ) except vol.Invalid as err: connection.send_error( msg["id"], - websocket_api.const.ERR_INVALID_FORMAT, + websocket_api.ERR_INVALID_FORMAT, humanize_error(data, err), ) except ValueError as err: - connection.send_error( - msg_id, websocket_api.const.ERR_INVALID_FORMAT, str(err) - ) + connection.send_error(msg_id, websocket_api.ERR_INVALID_FORMAT, str(err)) async def ws_delete_item( self, hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict @@ -665,7 +661,7 @@ class StorageCollectionWebsocket[_StorageCollectionT: StorageCollection]: except ItemNotFound: connection.send_error( msg["id"], - websocket_api.const.ERR_NOT_FOUND, + websocket_api.ERR_NOT_FOUND, f"Unable to find {self.item_id_key} {msg[self.item_id_key]}", ) From 80b2b05bd8b92e45b35fc24636bc3bab07624b8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Cl=C3=A9ment?= Date: Mon, 10 Jun 2024 14:27:20 +0200 Subject: [PATCH 0459/1445] Change qBittorrent lib to qbittorrentapi (#113394) * Change qBittorrent lib to qbittorrentapi * Fix tests * Convert qbittorrent service to new lib * Add missing translation key * Catch APIConnectionError in service call * Replace type ignore by Any typing * Remove last type: ignore * Use lib type for torrent_filter * Change import format * Fix remaining Any type --------- Co-authored-by: Erik Montnemery --- .../components/qbittorrent/__init__.py | 12 +++-- .../components/qbittorrent/config_flow.py | 7 ++- .../components/qbittorrent/coordinator.py | 51 ++++++++++++------- .../components/qbittorrent/helpers.py | 29 ++++++----- .../components/qbittorrent/manifest.json | 2 +- .../components/qbittorrent/sensor.py | 48 +++++++++-------- .../components/qbittorrent/strings.json | 3 ++ requirements_all.txt | 6 +-- requirements_test_all.txt | 6 +-- .../qbittorrent/test_config_flow.py | 20 +++++--- 10 files changed, 109 insertions(+), 75 deletions(-) diff --git a/homeassistant/components/qbittorrent/__init__.py b/homeassistant/components/qbittorrent/__init__.py index 84f080c4d49..fb781dd1a0c 100644 --- a/homeassistant/components/qbittorrent/__init__.py +++ b/homeassistant/components/qbittorrent/__init__.py @@ -3,8 +3,7 @@ import logging from typing import Any -from qbittorrent.client import LoginRequired -from requests.exceptions import RequestException +from qbittorrentapi import APIConnectionError, Forbidden403Error, LoginFailed from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -118,10 +117,13 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b config_entry.data[CONF_PASSWORD], config_entry.data[CONF_VERIFY_SSL], ) - except LoginRequired as err: + except LoginFailed as err: raise ConfigEntryNotReady("Invalid credentials") from err - except RequestException as err: - raise ConfigEntryNotReady("Failed to connect") from err + except Forbidden403Error as err: + raise ConfigEntryNotReady("Fail to log in, banned user ?") from err + except APIConnectionError as exc: + raise ConfigEntryNotReady("Fail to connect to qBittorrent") from exc + coordinator = QBittorrentDataCoordinator(hass, client) await coordinator.async_config_entry_first_refresh() diff --git a/homeassistant/components/qbittorrent/config_flow.py b/homeassistant/components/qbittorrent/config_flow.py index c17c842529b..fb9bde4805f 100644 --- a/homeassistant/components/qbittorrent/config_flow.py +++ b/homeassistant/components/qbittorrent/config_flow.py @@ -5,8 +5,7 @@ from __future__ import annotations import logging from typing import Any -from qbittorrent.client import LoginRequired -from requests.exceptions import RequestException +from qbittorrentapi import APIConnectionError, Forbidden403Error, LoginFailed import voluptuous as vol from homeassistant.config_entries import ConfigFlow, ConfigFlowResult @@ -46,9 +45,9 @@ class QbittorrentConfigFlow(ConfigFlow, domain=DOMAIN): user_input[CONF_PASSWORD], user_input[CONF_VERIFY_SSL], ) - except LoginRequired: + except (LoginFailed, Forbidden403Error): errors = {"base": "invalid_auth"} - except RequestException: + except APIConnectionError: errors = {"base": "cannot_connect"} else: return self.async_create_entry(title=DEFAULT_NAME, data=user_input) diff --git a/homeassistant/components/qbittorrent/coordinator.py b/homeassistant/components/qbittorrent/coordinator.py index 850bcf15ca2..0ef36d2a954 100644 --- a/homeassistant/components/qbittorrent/coordinator.py +++ b/homeassistant/components/qbittorrent/coordinator.py @@ -4,10 +4,16 @@ from __future__ import annotations from datetime import timedelta import logging -from typing import Any -from qbittorrent import Client -from qbittorrent.client import LoginRequired +from qbittorrentapi import ( + APIConnectionError, + Client, + Forbidden403Error, + LoginFailed, + SyncMainDataDictionary, + TorrentInfoList, +) +from qbittorrentapi.torrents import TorrentStatusesT from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError @@ -18,8 +24,8 @@ from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -class QBittorrentDataCoordinator(DataUpdateCoordinator[dict[str, Any]]): - """Coordinator for updating qBittorrent data.""" +class QBittorrentDataCoordinator(DataUpdateCoordinator[SyncMainDataDictionary]): + """Coordinator for updating QBittorrent data.""" def __init__(self, hass: HomeAssistant, client: Client) -> None: """Initialize coordinator.""" @@ -39,22 +45,31 @@ class QBittorrentDataCoordinator(DataUpdateCoordinator[dict[str, Any]]): update_interval=timedelta(seconds=30), ) - async def _async_update_data(self) -> dict[str, Any]: - """Async method to update QBittorrent data.""" + async def _async_update_data(self) -> SyncMainDataDictionary: try: - return await self.hass.async_add_executor_job(self.client.sync_main_data) - except LoginRequired as exc: - raise HomeAssistantError(str(exc)) from exc - - async def get_torrents(self, torrent_filter: str) -> list[dict[str, Any]]: - """Async method to get QBittorrent torrents.""" - try: - torrents = await self.hass.async_add_executor_job( - lambda: self.client.torrents(filter=torrent_filter) - ) - except LoginRequired as exc: + return await self.hass.async_add_executor_job(self.client.sync_maindata) + except (LoginFailed, Forbidden403Error) as exc: raise HomeAssistantError( translation_domain=DOMAIN, translation_key="login_error" ) from exc + except APIConnectionError as exc: + raise HomeAssistantError( + translation_domain=DOMAIN, translation_key="cannot_connect" + ) from exc + + async def get_torrents(self, torrent_filter: TorrentStatusesT) -> TorrentInfoList: + """Async method to get QBittorrent torrents.""" + try: + torrents = await self.hass.async_add_executor_job( + lambda: self.client.torrents_info(torrent_filter) + ) + except (LoginFailed, Forbidden403Error) as exc: + raise HomeAssistantError( + translation_domain=DOMAIN, translation_key="login_error" + ) from exc + except APIConnectionError as exc: + raise HomeAssistantError( + translation_domain=DOMAIN, translation_key="cannot_connect" + ) from exc return torrents diff --git a/homeassistant/components/qbittorrent/helpers.py b/homeassistant/components/qbittorrent/helpers.py index bbe53765f8b..fac0a6033fa 100644 --- a/homeassistant/components/qbittorrent/helpers.py +++ b/homeassistant/components/qbittorrent/helpers.py @@ -1,17 +1,18 @@ """Helper functions for qBittorrent.""" from datetime import UTC, datetime -from typing import Any +from typing import Any, cast -from qbittorrent.client import Client +from qbittorrentapi import Client, TorrentDictionary, TorrentInfoList def setup_client(url: str, username: str, password: str, verify_ssl: bool) -> Client: """Create a qBittorrent client.""" - client = Client(url, verify=verify_ssl) - client.login(username, password) - # Get an arbitrary attribute to test if connection succeeds - client.get_alternative_speed_status() + + client = Client( + url, username=username, password=password, VERIFY_WEBUI_CERTIFICATE=verify_ssl + ) + client.auth_log_in(username, password) return client @@ -31,23 +32,24 @@ def format_unix_timestamp(timestamp) -> str: return dt_object.isoformat() -def format_progress(torrent) -> str: +def format_progress(torrent: TorrentDictionary) -> str: """Format the progress of a torrent.""" - progress = torrent["progress"] - progress = float(progress) * 100 + progress = cast(float, torrent["progress"]) * 100 return f"{progress:.2f}" -def format_torrents(torrents: list[dict[str, Any]]) -> dict[str, dict[str, Any]]: +def format_torrents( + torrents: TorrentInfoList, +) -> dict[str, dict[str, Any]]: """Format a list of torrents.""" value = {} for torrent in torrents: - value[torrent["name"]] = format_torrent(torrent) + value[str(torrent["name"])] = format_torrent(torrent) return value -def format_torrent(torrent) -> dict[str, Any]: +def format_torrent(torrent: TorrentDictionary) -> dict[str, Any]: """Format a single torrent.""" value = {} value["id"] = torrent["hash"] @@ -55,6 +57,7 @@ def format_torrent(torrent) -> dict[str, Any]: value["percent_done"] = format_progress(torrent) value["status"] = torrent["state"] value["eta"] = seconds_to_hhmmss(torrent["eta"]) - value["ratio"] = "{:.2f}".format(float(torrent["ratio"])) + ratio = cast(float, torrent["ratio"]) + value["ratio"] = f"{ratio:.2f}" return value diff --git a/homeassistant/components/qbittorrent/manifest.json b/homeassistant/components/qbittorrent/manifest.json index fb51f177081..bd9897aa6ba 100644 --- a/homeassistant/components/qbittorrent/manifest.json +++ b/homeassistant/components/qbittorrent/manifest.json @@ -7,5 +7,5 @@ "integration_type": "service", "iot_class": "local_polling", "loggers": ["qbittorrent"], - "requirements": ["python-qbittorrent==0.4.3"] + "requirements": ["qbittorrent-api==2024.2.59"] } diff --git a/homeassistant/components/qbittorrent/sensor.py b/homeassistant/components/qbittorrent/sensor.py index 84eac7d28cf..cd65fb766e4 100644 --- a/homeassistant/components/qbittorrent/sensor.py +++ b/homeassistant/components/qbittorrent/sensor.py @@ -2,9 +2,10 @@ from __future__ import annotations -from collections.abc import Callable +from collections.abc import Callable, Mapping from dataclasses import dataclass import logging +from typing import Any, cast from homeassistant.components.sensor import ( SensorDeviceClass, @@ -35,8 +36,9 @@ SENSOR_TYPE_INACTIVE_TORRENTS = "inactive_torrents" def get_state(coordinator: QBittorrentDataCoordinator) -> str: """Get current download/upload state.""" - upload = coordinator.data["server_state"]["up_info_speed"] - download = coordinator.data["server_state"]["dl_info_speed"] + server_state = cast(Mapping, coordinator.data.get("server_state")) + upload = cast(int, server_state.get("up_info_speed")) + download = cast(int, server_state.get("dl_info_speed")) if upload > 0 and download > 0: return STATE_UP_DOWN @@ -47,6 +49,18 @@ def get_state(coordinator: QBittorrentDataCoordinator) -> str: return STATE_IDLE +def get_dl(coordinator: QBittorrentDataCoordinator) -> int: + """Get current download speed.""" + server_state = cast(Mapping, coordinator.data.get("server_state")) + return cast(int, server_state.get("dl_info_speed")) + + +def get_up(coordinator: QBittorrentDataCoordinator) -> int: + """Get current upload speed.""" + server_state = cast(Mapping[str, Any], coordinator.data.get("server_state")) + return cast(int, server_state.get("up_info_speed")) + + @dataclass(frozen=True, kw_only=True) class QBittorrentSensorEntityDescription(SensorEntityDescription): """Entity description class for qBittorent sensors.""" @@ -69,9 +83,7 @@ SENSOR_TYPES: tuple[QBittorrentSensorEntityDescription, ...] = ( native_unit_of_measurement=UnitOfDataRate.BYTES_PER_SECOND, suggested_display_precision=2, suggested_unit_of_measurement=UnitOfDataRate.MEGABYTES_PER_SECOND, - value_fn=lambda coordinator: float( - coordinator.data["server_state"]["dl_info_speed"] - ), + value_fn=get_dl, ), QBittorrentSensorEntityDescription( key=SENSOR_TYPE_UPLOAD_SPEED, @@ -80,9 +92,7 @@ SENSOR_TYPES: tuple[QBittorrentSensorEntityDescription, ...] = ( native_unit_of_measurement=UnitOfDataRate.BYTES_PER_SECOND, suggested_display_precision=2, suggested_unit_of_measurement=UnitOfDataRate.MEGABYTES_PER_SECOND, - value_fn=lambda coordinator: float( - coordinator.data["server_state"]["up_info_speed"] - ), + value_fn=get_up, ), QBittorrentSensorEntityDescription( key=SENSOR_TYPE_ALL_TORRENTS, @@ -165,16 +175,12 @@ def count_torrents_in_states( ) -> int: """Count the number of torrents in specified states.""" # When torrents are not in the returned data, there are none, return 0. - if "torrents" not in coordinator.data: + try: + torrents = cast(Mapping[str, Mapping], coordinator.data.get("torrents")) + if not states: + return len(torrents) + return len( + [torrent for torrent in torrents.values() if torrent.get("state") in states] + ) + except AttributeError: return 0 - - if not states: - return len(coordinator.data["torrents"]) - - return len( - [ - torrent - for torrent in coordinator.data["torrents"].values() - if torrent["state"] in states - ] - ) diff --git a/homeassistant/components/qbittorrent/strings.json b/homeassistant/components/qbittorrent/strings.json index 5376e929429..948e9dca8e9 100644 --- a/homeassistant/components/qbittorrent/strings.json +++ b/homeassistant/components/qbittorrent/strings.json @@ -84,6 +84,9 @@ }, "login_error": { "message": "A login error occured. Please check you username and password." + }, + "cannot_connect": { + "message": "Can't connect to QBittorrent, please check your configuration." } } } diff --git a/requirements_all.txt b/requirements_all.txt index cd382d9b1c7..730f4c32633 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2302,9 +2302,6 @@ python-otbr-api==2.6.0 # homeassistant.components.picnic python-picnic-api==1.1.0 -# homeassistant.components.qbittorrent -python-qbittorrent==0.4.3 - # homeassistant.components.rabbitair python-rabbitair==0.0.8 @@ -2420,6 +2417,9 @@ pyzbar==0.1.7 # homeassistant.components.zerproc pyzerproc==0.4.8 +# homeassistant.components.qbittorrent +qbittorrent-api==2024.2.59 + # homeassistant.components.qingping qingping-ble==0.10.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e84a8a345ed..a7fc37a5473 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1799,9 +1799,6 @@ python-otbr-api==2.6.0 # homeassistant.components.picnic python-picnic-api==1.1.0 -# homeassistant.components.qbittorrent -python-qbittorrent==0.4.3 - # homeassistant.components.rabbitair python-rabbitair==0.0.8 @@ -1893,6 +1890,9 @@ pyyardian==1.1.1 # homeassistant.components.zerproc pyzerproc==0.4.8 +# homeassistant.components.qbittorrent +qbittorrent-api==2024.2.59 + # homeassistant.components.qingping qingping-ble==0.10.0 diff --git a/tests/components/qbittorrent/test_config_flow.py b/tests/components/qbittorrent/test_config_flow.py index c52762f24d3..abf64713f50 100644 --- a/tests/components/qbittorrent/test_config_flow.py +++ b/tests/components/qbittorrent/test_config_flow.py @@ -59,8 +59,7 @@ async def test_flow_user(hass: HomeAssistant, mock_api: requests_mock.Mocker) -> # Test flow with wrong creds, fail with invalid_auth with requests_mock.Mocker() as mock: - mock.get(f"{USER_INPUT[CONF_URL]}/api/v2/transfer/speedLimitsMode") - mock.get(f"{USER_INPUT[CONF_URL]}/api/v2/app/preferences", status_code=403) + mock.head(USER_INPUT[CONF_URL]) mock.post( f"{USER_INPUT[CONF_URL]}/api/v2/auth/login", text="Wrong username/password", @@ -74,11 +73,18 @@ async def test_flow_user(hass: HomeAssistant, mock_api: requests_mock.Mocker) -> assert result["errors"] == {"base": "invalid_auth"} # Test flow with proper input, succeed - result = await hass.config_entries.flow.async_configure( - result["flow_id"], USER_INPUT - ) - await hass.async_block_till_done() - assert result["type"] is FlowResultType.CREATE_ENTRY + with requests_mock.Mocker() as mock: + mock.head(USER_INPUT[CONF_URL]) + mock.post( + f"{USER_INPUT[CONF_URL]}/api/v2/auth/login", + text="Ok.", + ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], USER_INPUT + ) + await hass.async_block_till_done() + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["data"] == { CONF_URL: "http://localhost:8080", CONF_USERNAME: "user", From bedff291657b9c44575f0535058d89451308c451 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 10 Jun 2024 14:55:28 +0200 Subject: [PATCH 0460/1445] Fix persistence on OpenWeatherMap raised repair issue (#119289) --- homeassistant/components/openweathermap/repairs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/openweathermap/repairs.py b/homeassistant/components/openweathermap/repairs.py index 0f411a45405..c54484e1e1e 100644 --- a/homeassistant/components/openweathermap/repairs.py +++ b/homeassistant/components/openweathermap/repairs.py @@ -73,7 +73,7 @@ def async_create_issue(hass: HomeAssistant, entry_id: str) -> None: domain=DOMAIN, issue_id=_get_issue_id(entry_id), is_fixable=True, - is_persistent=True, + is_persistent=False, severity=ir.IssueSeverity.WARNING, learn_more_url="https://www.home-assistant.io/integrations/openweathermap/", translation_key="deprecated_v25", From 9c120675656455177a326525bf181d863b789c0b Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 10 Jun 2024 14:55:47 +0200 Subject: [PATCH 0461/1445] Don't run tests if lint-ruff-format fails (#119291) --- .github/workflows/ci.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index fd4aaeed526..499319ff99f 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -746,6 +746,7 @@ jobs: - hassfest - lint-other - lint-ruff + - lint-ruff-format - mypy - prepare-pytest-full strategy: @@ -863,6 +864,7 @@ jobs: - hassfest - lint-other - lint-ruff + - lint-ruff-format - mypy strategy: fail-fast: false @@ -986,6 +988,7 @@ jobs: - hassfest - lint-other - lint-ruff + - lint-ruff-format - mypy strategy: fail-fast: false @@ -1128,6 +1131,7 @@ jobs: - hassfest - lint-other - lint-ruff + - lint-ruff-format - mypy strategy: fail-fast: false From 6733f86c6182d42f1478709aeb07fd15be20ebc6 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 10 Jun 2024 14:57:34 +0200 Subject: [PATCH 0462/1445] Use service_calls fixture in helper tests (#119275) Co-authored-by: Franck Nijhof --- tests/helpers/test_condition.py | 185 +++++++++++++++++--------------- tests/helpers/test_trigger.py | 52 ++++----- 2 files changed, 127 insertions(+), 110 deletions(-) diff --git a/tests/helpers/test_condition.py b/tests/helpers/test_condition.py index ce114058453..31f813469cc 100644 --- a/tests/helpers/test_condition.py +++ b/tests/helpers/test_condition.py @@ -30,16 +30,9 @@ from homeassistant.helpers.template import Template from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.common import async_mock_service from tests.typing import WebSocketGenerator -@pytest.fixture -def calls(hass: HomeAssistant) -> list[ServiceCall]: - """Track calls to a mock service.""" - return async_mock_service(hass, "test", "automation") - - def assert_element(trace_element, expected_element, path): """Assert a trace element is as expected. @@ -2215,7 +2208,9 @@ async def assert_automation_condition_trace(hass_ws_client, automation_id, expec async def test_if_action_before_sunrise_no_offset( - hass: HomeAssistant, hass_ws_client: WebSocketGenerator, calls: list[ServiceCall] + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + service_calls: list[ServiceCall], ) -> None: """Test if action was before sunrise. @@ -2241,7 +2236,7 @@ async def test_if_action_before_sunrise_no_offset( with freeze_time(now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 await assert_automation_condition_trace( hass_ws_client, "sun", @@ -2253,7 +2248,7 @@ async def test_if_action_before_sunrise_no_offset( with freeze_time(now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 await assert_automation_condition_trace( hass_ws_client, "sun", @@ -2265,7 +2260,7 @@ async def test_if_action_before_sunrise_no_offset( with freeze_time(now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert len(calls) == 2 + assert len(service_calls) == 2 await assert_automation_condition_trace( hass_ws_client, "sun", @@ -2277,7 +2272,7 @@ async def test_if_action_before_sunrise_no_offset( with freeze_time(now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert len(calls) == 2 + assert len(service_calls) == 2 await assert_automation_condition_trace( hass_ws_client, "sun", @@ -2286,7 +2281,9 @@ async def test_if_action_before_sunrise_no_offset( async def test_if_action_after_sunrise_no_offset( - hass: HomeAssistant, hass_ws_client: WebSocketGenerator, calls: list[ServiceCall] + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + service_calls: list[ServiceCall], ) -> None: """Test if action was after sunrise. @@ -2312,7 +2309,7 @@ async def test_if_action_after_sunrise_no_offset( with freeze_time(now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 await assert_automation_condition_trace( hass_ws_client, "sun", @@ -2324,7 +2321,7 @@ async def test_if_action_after_sunrise_no_offset( with freeze_time(now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 await assert_automation_condition_trace( hass_ws_client, "sun", @@ -2336,7 +2333,7 @@ async def test_if_action_after_sunrise_no_offset( with freeze_time(now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 await assert_automation_condition_trace( hass_ws_client, "sun", @@ -2348,7 +2345,7 @@ async def test_if_action_after_sunrise_no_offset( with freeze_time(now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert len(calls) == 2 + assert len(service_calls) == 2 await assert_automation_condition_trace( hass_ws_client, "sun", @@ -2357,7 +2354,9 @@ async def test_if_action_after_sunrise_no_offset( async def test_if_action_before_sunrise_with_offset( - hass: HomeAssistant, hass_ws_client: WebSocketGenerator, calls: list[ServiceCall] + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + service_calls: list[ServiceCall], ) -> None: """Test if action was before sunrise with offset. @@ -2387,7 +2386,7 @@ async def test_if_action_before_sunrise_with_offset( with freeze_time(now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 await assert_automation_condition_trace( hass_ws_client, "sun", @@ -2399,7 +2398,7 @@ async def test_if_action_before_sunrise_with_offset( with freeze_time(now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 await assert_automation_condition_trace( hass_ws_client, "sun", @@ -2411,7 +2410,7 @@ async def test_if_action_before_sunrise_with_offset( with freeze_time(now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 await assert_automation_condition_trace( hass_ws_client, "sun", @@ -2423,7 +2422,7 @@ async def test_if_action_before_sunrise_with_offset( with freeze_time(now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 await assert_automation_condition_trace( hass_ws_client, "sun", @@ -2435,7 +2434,7 @@ async def test_if_action_before_sunrise_with_offset( with freeze_time(now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert len(calls) == 2 + assert len(service_calls) == 2 await assert_automation_condition_trace( hass_ws_client, "sun", @@ -2447,7 +2446,7 @@ async def test_if_action_before_sunrise_with_offset( with freeze_time(now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert len(calls) == 2 + assert len(service_calls) == 2 await assert_automation_condition_trace( hass_ws_client, "sun", @@ -2459,7 +2458,7 @@ async def test_if_action_before_sunrise_with_offset( with freeze_time(now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert len(calls) == 2 + assert len(service_calls) == 2 await assert_automation_condition_trace( hass_ws_client, "sun", @@ -2471,7 +2470,7 @@ async def test_if_action_before_sunrise_with_offset( with freeze_time(now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert len(calls) == 2 + assert len(service_calls) == 2 await assert_automation_condition_trace( hass_ws_client, "sun", @@ -2480,7 +2479,9 @@ async def test_if_action_before_sunrise_with_offset( async def test_if_action_before_sunset_with_offset( - hass: HomeAssistant, hass_ws_client: WebSocketGenerator, calls: list[ServiceCall] + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + service_calls: list[ServiceCall], ) -> None: """Test if action was before sunset with offset. @@ -2510,7 +2511,7 @@ async def test_if_action_before_sunset_with_offset( with freeze_time(now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 await assert_automation_condition_trace( hass_ws_client, "sun", @@ -2522,7 +2523,7 @@ async def test_if_action_before_sunset_with_offset( with freeze_time(now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 await assert_automation_condition_trace( hass_ws_client, "sun", @@ -2534,7 +2535,7 @@ async def test_if_action_before_sunset_with_offset( with freeze_time(now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert len(calls) == 2 + assert len(service_calls) == 2 await assert_automation_condition_trace( hass_ws_client, "sun", @@ -2546,7 +2547,7 @@ async def test_if_action_before_sunset_with_offset( with freeze_time(now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert len(calls) == 3 + assert len(service_calls) == 3 await assert_automation_condition_trace( hass_ws_client, "sun", @@ -2558,7 +2559,7 @@ async def test_if_action_before_sunset_with_offset( with freeze_time(now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert len(calls) == 4 + assert len(service_calls) == 4 await assert_automation_condition_trace( hass_ws_client, "sun", @@ -2570,7 +2571,7 @@ async def test_if_action_before_sunset_with_offset( with freeze_time(now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert len(calls) == 5 + assert len(service_calls) == 5 await assert_automation_condition_trace( hass_ws_client, "sun", @@ -2582,7 +2583,7 @@ async def test_if_action_before_sunset_with_offset( with freeze_time(now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert len(calls) == 6 + assert len(service_calls) == 6 await assert_automation_condition_trace( hass_ws_client, "sun", @@ -2594,7 +2595,7 @@ async def test_if_action_before_sunset_with_offset( with freeze_time(now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert len(calls) == 6 + assert len(service_calls) == 6 await assert_automation_condition_trace( hass_ws_client, "sun", @@ -2603,7 +2604,9 @@ async def test_if_action_before_sunset_with_offset( async def test_if_action_after_sunrise_with_offset( - hass: HomeAssistant, hass_ws_client: WebSocketGenerator, calls: list[ServiceCall] + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + service_calls: list[ServiceCall], ) -> None: """Test if action was after sunrise with offset. @@ -2633,7 +2636,7 @@ async def test_if_action_after_sunrise_with_offset( with freeze_time(now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 await assert_automation_condition_trace( hass_ws_client, "sun", @@ -2645,7 +2648,7 @@ async def test_if_action_after_sunrise_with_offset( with freeze_time(now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 await assert_automation_condition_trace( hass_ws_client, "sun", @@ -2657,7 +2660,7 @@ async def test_if_action_after_sunrise_with_offset( with freeze_time(now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 await assert_automation_condition_trace( hass_ws_client, "sun", @@ -2669,7 +2672,7 @@ async def test_if_action_after_sunrise_with_offset( with freeze_time(now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 await assert_automation_condition_trace( hass_ws_client, "sun", @@ -2681,7 +2684,7 @@ async def test_if_action_after_sunrise_with_offset( with freeze_time(now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert len(calls) == 2 + assert len(service_calls) == 2 await assert_automation_condition_trace( hass_ws_client, "sun", @@ -2693,7 +2696,7 @@ async def test_if_action_after_sunrise_with_offset( with freeze_time(now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert len(calls) == 3 + assert len(service_calls) == 3 await assert_automation_condition_trace( hass_ws_client, "sun", @@ -2705,7 +2708,7 @@ async def test_if_action_after_sunrise_with_offset( with freeze_time(now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert len(calls) == 4 + assert len(service_calls) == 4 await assert_automation_condition_trace( hass_ws_client, "sun", @@ -2717,7 +2720,7 @@ async def test_if_action_after_sunrise_with_offset( with freeze_time(now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert len(calls) == 5 + assert len(service_calls) == 5 await assert_automation_condition_trace( hass_ws_client, "sun", @@ -2729,7 +2732,7 @@ async def test_if_action_after_sunrise_with_offset( with freeze_time(now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert len(calls) == 6 + assert len(service_calls) == 6 await assert_automation_condition_trace( hass_ws_client, "sun", @@ -2741,7 +2744,7 @@ async def test_if_action_after_sunrise_with_offset( with freeze_time(now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert len(calls) == 6 + assert len(service_calls) == 6 await assert_automation_condition_trace( hass_ws_client, "sun", @@ -2750,7 +2753,9 @@ async def test_if_action_after_sunrise_with_offset( async def test_if_action_after_sunset_with_offset( - hass: HomeAssistant, hass_ws_client: WebSocketGenerator, calls: list[ServiceCall] + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + service_calls: list[ServiceCall], ) -> None: """Test if action was after sunset with offset. @@ -2780,7 +2785,7 @@ async def test_if_action_after_sunset_with_offset( with freeze_time(now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 await assert_automation_condition_trace( hass_ws_client, "sun", @@ -2792,7 +2797,7 @@ async def test_if_action_after_sunset_with_offset( with freeze_time(now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 await assert_automation_condition_trace( hass_ws_client, "sun", @@ -2804,7 +2809,7 @@ async def test_if_action_after_sunset_with_offset( with freeze_time(now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert len(calls) == 2 + assert len(service_calls) == 2 await assert_automation_condition_trace( hass_ws_client, "sun", @@ -2816,7 +2821,7 @@ async def test_if_action_after_sunset_with_offset( with freeze_time(now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert len(calls) == 2 + assert len(service_calls) == 2 await assert_automation_condition_trace( hass_ws_client, "sun", @@ -2825,7 +2830,9 @@ async def test_if_action_after_sunset_with_offset( async def test_if_action_after_and_before_during( - hass: HomeAssistant, hass_ws_client: WebSocketGenerator, calls: list[ServiceCall] + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + service_calls: list[ServiceCall], ) -> None: """Test if action was after sunrise and before sunset. @@ -2855,7 +2862,7 @@ async def test_if_action_after_and_before_during( with freeze_time(now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 await assert_automation_condition_trace( hass_ws_client, "sun", @@ -2871,7 +2878,7 @@ async def test_if_action_after_and_before_during( with freeze_time(now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 await assert_automation_condition_trace( hass_ws_client, "sun", @@ -2883,7 +2890,7 @@ async def test_if_action_after_and_before_during( with freeze_time(now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 await assert_automation_condition_trace( hass_ws_client, "sun", @@ -2899,7 +2906,7 @@ async def test_if_action_after_and_before_during( with freeze_time(now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert len(calls) == 2 + assert len(service_calls) == 2 await assert_automation_condition_trace( hass_ws_client, "sun", @@ -2915,7 +2922,7 @@ async def test_if_action_after_and_before_during( with freeze_time(now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert len(calls) == 3 + assert len(service_calls) == 3 await assert_automation_condition_trace( hass_ws_client, "sun", @@ -2928,7 +2935,9 @@ async def test_if_action_after_and_before_during( async def test_if_action_before_or_after_during( - hass: HomeAssistant, hass_ws_client: WebSocketGenerator, calls: list[ServiceCall] + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + service_calls: list[ServiceCall], ) -> None: """Test if action was before sunrise or after sunset. @@ -2958,7 +2967,7 @@ async def test_if_action_before_or_after_during( with freeze_time(now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 await assert_automation_condition_trace( hass_ws_client, "sun", @@ -2974,7 +2983,7 @@ async def test_if_action_before_or_after_during( with freeze_time(now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert len(calls) == 2 + assert len(service_calls) == 2 await assert_automation_condition_trace( hass_ws_client, "sun", @@ -2990,7 +2999,7 @@ async def test_if_action_before_or_after_during( with freeze_time(now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert len(calls) == 2 + assert len(service_calls) == 2 await assert_automation_condition_trace( hass_ws_client, "sun", @@ -3006,7 +3015,7 @@ async def test_if_action_before_or_after_during( with freeze_time(now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert len(calls) == 2 + assert len(service_calls) == 2 await assert_automation_condition_trace( hass_ws_client, "sun", @@ -3022,7 +3031,7 @@ async def test_if_action_before_or_after_during( with freeze_time(now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert len(calls) == 3 + assert len(service_calls) == 3 await assert_automation_condition_trace( hass_ws_client, "sun", @@ -3038,7 +3047,7 @@ async def test_if_action_before_or_after_during( with freeze_time(now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert len(calls) == 4 + assert len(service_calls) == 4 await assert_automation_condition_trace( hass_ws_client, "sun", @@ -3051,7 +3060,9 @@ async def test_if_action_before_or_after_during( async def test_if_action_before_sunrise_no_offset_kotzebue( - hass: HomeAssistant, hass_ws_client: WebSocketGenerator, calls: list[ServiceCall] + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + service_calls: list[ServiceCall], ) -> None: """Test if action was before sunrise. @@ -3083,7 +3094,7 @@ async def test_if_action_before_sunrise_no_offset_kotzebue( with freeze_time(now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 await assert_automation_condition_trace( hass_ws_client, "sun", @@ -3095,7 +3106,7 @@ async def test_if_action_before_sunrise_no_offset_kotzebue( with freeze_time(now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 await assert_automation_condition_trace( hass_ws_client, "sun", @@ -3107,7 +3118,7 @@ async def test_if_action_before_sunrise_no_offset_kotzebue( with freeze_time(now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert len(calls) == 2 + assert len(service_calls) == 2 await assert_automation_condition_trace( hass_ws_client, "sun", @@ -3119,7 +3130,7 @@ async def test_if_action_before_sunrise_no_offset_kotzebue( with freeze_time(now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert len(calls) == 2 + assert len(service_calls) == 2 await assert_automation_condition_trace( hass_ws_client, "sun", @@ -3128,7 +3139,9 @@ async def test_if_action_before_sunrise_no_offset_kotzebue( async def test_if_action_after_sunrise_no_offset_kotzebue( - hass: HomeAssistant, hass_ws_client: WebSocketGenerator, calls: list[ServiceCall] + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + service_calls: list[ServiceCall], ) -> None: """Test if action was after sunrise. @@ -3160,7 +3173,7 @@ async def test_if_action_after_sunrise_no_offset_kotzebue( with freeze_time(now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 await assert_automation_condition_trace( hass_ws_client, "sun", @@ -3172,7 +3185,7 @@ async def test_if_action_after_sunrise_no_offset_kotzebue( with freeze_time(now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 await assert_automation_condition_trace( hass_ws_client, "sun", @@ -3184,7 +3197,7 @@ async def test_if_action_after_sunrise_no_offset_kotzebue( with freeze_time(now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 await assert_automation_condition_trace( hass_ws_client, "sun", @@ -3196,7 +3209,7 @@ async def test_if_action_after_sunrise_no_offset_kotzebue( with freeze_time(now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert len(calls) == 2 + assert len(service_calls) == 2 await assert_automation_condition_trace( hass_ws_client, "sun", @@ -3205,7 +3218,9 @@ async def test_if_action_after_sunrise_no_offset_kotzebue( async def test_if_action_before_sunset_no_offset_kotzebue( - hass: HomeAssistant, hass_ws_client: WebSocketGenerator, calls: list[ServiceCall] + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + service_calls: list[ServiceCall], ) -> None: """Test if action was before sunrise. @@ -3237,7 +3252,7 @@ async def test_if_action_before_sunset_no_offset_kotzebue( with freeze_time(now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 await assert_automation_condition_trace( hass_ws_client, "sun", @@ -3249,7 +3264,7 @@ async def test_if_action_before_sunset_no_offset_kotzebue( with freeze_time(now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 await assert_automation_condition_trace( hass_ws_client, "sun", @@ -3261,7 +3276,7 @@ async def test_if_action_before_sunset_no_offset_kotzebue( with freeze_time(now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert len(calls) == 2 + assert len(service_calls) == 2 await assert_automation_condition_trace( hass_ws_client, "sun", @@ -3273,7 +3288,7 @@ async def test_if_action_before_sunset_no_offset_kotzebue( with freeze_time(now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert len(calls) == 2 + assert len(service_calls) == 2 await assert_automation_condition_trace( hass_ws_client, "sun", @@ -3282,7 +3297,9 @@ async def test_if_action_before_sunset_no_offset_kotzebue( async def test_if_action_after_sunset_no_offset_kotzebue( - hass: HomeAssistant, hass_ws_client: WebSocketGenerator, calls: list[ServiceCall] + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + service_calls: list[ServiceCall], ) -> None: """Test if action was after sunrise. @@ -3314,7 +3331,7 @@ async def test_if_action_after_sunset_no_offset_kotzebue( with freeze_time(now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 await assert_automation_condition_trace( hass_ws_client, "sun", @@ -3326,7 +3343,7 @@ async def test_if_action_after_sunset_no_offset_kotzebue( with freeze_time(now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 await assert_automation_condition_trace( hass_ws_client, "sun", @@ -3338,7 +3355,7 @@ async def test_if_action_after_sunset_no_offset_kotzebue( with freeze_time(now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 await assert_automation_condition_trace( hass_ws_client, "sun", @@ -3350,7 +3367,7 @@ async def test_if_action_after_sunset_no_offset_kotzebue( with freeze_time(now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert len(calls) == 2 + assert len(service_calls) == 2 await assert_automation_condition_trace( hass_ws_client, "sun", diff --git a/tests/helpers/test_trigger.py b/tests/helpers/test_trigger.py index 0ab02b8c4dc..e8322c7e660 100644 --- a/tests/helpers/test_trigger.py +++ b/tests/helpers/test_trigger.py @@ -15,14 +15,6 @@ from homeassistant.helpers.trigger import ( ) from homeassistant.setup import async_setup_component -from tests.common import async_mock_service - - -@pytest.fixture -def calls(hass): - """Track calls to a mock service.""" - return async_mock_service(hass, "test", "automation") - async def test_bad_trigger_platform(hass: HomeAssistant) -> None: """Test bad trigger platform.""" @@ -45,7 +37,9 @@ async def test_trigger_variables(hass: HomeAssistant) -> None: """Test trigger variables.""" -async def test_if_fires_on_event(hass: HomeAssistant, calls) -> None: +async def test_if_fires_on_event( + hass: HomeAssistant, service_calls: list[ServiceCall] +) -> None: """Test the firing of events.""" assert await async_setup_component( hass, @@ -70,12 +64,12 @@ async def test_if_fires_on_event(hass: HomeAssistant, calls) -> None: hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["hello"] == "Paulus + test_event" + assert len(service_calls) == 1 + assert service_calls[0].data["hello"] == "Paulus + test_event" async def test_if_disabled_trigger_not_firing( - hass: HomeAssistant, calls: list[ServiceCall] + hass: HomeAssistant, service_calls: list[ServiceCall] ) -> None: """Test disabled triggers don't fire.""" assert await async_setup_component( @@ -103,15 +97,15 @@ async def test_if_disabled_trigger_not_firing( hass.bus.async_fire("disabled_trigger_event") await hass.async_block_till_done() - assert not calls + assert not service_calls hass.bus.async_fire("enabled_trigger_event") await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 async def test_trigger_enabled_templates( - hass: HomeAssistant, calls: list[ServiceCall] + hass: HomeAssistant, service_calls: list[ServiceCall] ) -> None: """Test triggers enabled by template.""" assert await async_setup_component( @@ -150,23 +144,25 @@ async def test_trigger_enabled_templates( hass.bus.async_fire("falsy_template_trigger_event") await hass.async_block_till_done() - assert not calls + assert not service_calls hass.bus.async_fire("falsy_trigger_event") await hass.async_block_till_done() - assert not calls + assert not service_calls hass.bus.async_fire("truthy_template_trigger_event") await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 hass.bus.async_fire("truthy_trigger_event") await hass.async_block_till_done() - assert len(calls) == 2 + assert len(service_calls) == 2 async def test_trigger_enabled_template_limited( - hass: HomeAssistant, calls: list[ServiceCall], caplog: pytest.LogCaptureFixture + hass: HomeAssistant, + service_calls: list[ServiceCall], + caplog: pytest.LogCaptureFixture, ) -> None: """Test triggers enabled invalid template.""" assert await async_setup_component( @@ -190,12 +186,14 @@ async def test_trigger_enabled_template_limited( hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert not calls + assert not service_calls assert "Error rendering enabled template" in caplog.text async def test_trigger_alias( - hass: HomeAssistant, calls: list[ServiceCall], caplog: pytest.LogCaptureFixture + hass: HomeAssistant, + service_calls: list[ServiceCall], + caplog: pytest.LogCaptureFixture, ) -> None: """Test triggers support aliases.""" assert await async_setup_component( @@ -220,8 +218,8 @@ async def test_trigger_alias( hass.bus.async_fire("trigger_event") await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["alias"] == "My event" + assert len(service_calls) == 1 + assert service_calls[0].data["alias"] == "My event" assert ( "Automation trigger 'My event' triggered by event 'trigger_event'" in caplog.text @@ -229,7 +227,9 @@ async def test_trigger_alias( async def test_async_initialize_triggers( - hass: HomeAssistant, calls: list[ServiceCall], caplog: pytest.LogCaptureFixture + hass: HomeAssistant, + service_calls: list[ServiceCall], + caplog: pytest.LogCaptureFixture, ) -> None: """Test async_initialize_triggers with different action types.""" @@ -287,7 +287,7 @@ async def test_async_initialize_triggers( unsub() -async def test_pluggable_action(hass: HomeAssistant, calls: list[ServiceCall]): +async def test_pluggable_action(hass: HomeAssistant, service_calls: list[ServiceCall]): """Test normal behavior of pluggable actions.""" update_1 = MagicMock() update_2 = MagicMock() From 94b9ae14c9821b3a3564b4227f0944efa4fd0180 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 10 Jun 2024 15:00:05 +0200 Subject: [PATCH 0463/1445] Use Registry fixture in zwave_js tests (#119277) --- tests/components/zwave_js/test_button.py | 5 +- .../zwave_js/test_device_trigger.py | 232 +++++++++++------- tests/components/zwave_js/test_init.py | 2 +- tests/components/zwave_js/test_services.py | 114 +++++---- tests/components/zwave_js/test_trigger.py | 29 ++- tests/components/zwave_js/test_update.py | 5 +- 6 files changed, 232 insertions(+), 155 deletions(-) diff --git a/tests/components/zwave_js/test_button.py b/tests/components/zwave_js/test_button.py index e1a1c6d665a..b0c06668926 100644 --- a/tests/components/zwave_js/test_button.py +++ b/tests/components/zwave_js/test_button.py @@ -7,11 +7,12 @@ from homeassistant.components.zwave_js.const import DOMAIN, SERVICE_REFRESH_VALU from homeassistant.components.zwave_js.helpers import get_valueless_base_unique_id from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity_registry import async_get +from homeassistant.helpers import entity_registry as er async def test_ping_entity( hass: HomeAssistant, + entity_registry: er.EntityRegistry, client, climate_radio_thermostat_ct100_plus_different_endpoints, integration, @@ -56,7 +57,7 @@ async def test_ping_entity( node = driver.controller.nodes[1] assert node.is_controller_node assert ( - async_get(hass).async_get_entity_id( + entity_registry.async_get_entity_id( DOMAIN, "sensor", f"{get_valueless_base_unique_id(driver, node)}.ping" ) is None diff --git a/tests/components/zwave_js/test_device_trigger.py b/tests/components/zwave_js/test_device_trigger.py index e739393471e..0fa228288ec 100644 --- a/tests/components/zwave_js/test_device_trigger.py +++ b/tests/components/zwave_js/test_device_trigger.py @@ -21,9 +21,11 @@ from homeassistant.components.zwave_js.helpers import ( ) from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.device_registry import async_get as async_get_dev_reg -from homeassistant.helpers.entity_registry import async_get as async_get_ent_reg +from homeassistant.helpers import ( + config_validation as cv, + device_registry as dr, + entity_registry as er, +) from homeassistant.setup import async_setup_component from tests.common import async_get_device_automations, async_mock_service @@ -35,10 +37,11 @@ def calls(hass: HomeAssistant) -> list[ServiceCall]: return async_mock_service(hass, "test", "automation") -async def test_no_controller_triggers(hass: HomeAssistant, client, integration) -> None: +async def test_no_controller_triggers( + hass: HomeAssistant, device_registry: dr.DeviceRegistry, client, integration +) -> None: """Test that we do not get triggers for the controller.""" - dev_reg = async_get_dev_reg(hass) - device = dev_reg.async_get_device( + device = device_registry.async_get_device( identifiers={get_device_id(client.driver, client.driver.controller.nodes[1])} ) assert device @@ -51,11 +54,14 @@ async def test_no_controller_triggers(hass: HomeAssistant, client, integration) async def test_get_notification_notification_triggers( - hass: HomeAssistant, client, lock_schlage_be469, integration + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + client, + lock_schlage_be469, + integration, ) -> None: """Test we get the expected triggers from a zwave_js device with the Notification CC.""" - dev_reg = async_get_dev_reg(hass) - device = dev_reg.async_get_device( + device = device_registry.async_get_device( identifiers={get_device_id(client.driver, lock_schlage_be469)} ) assert device @@ -75,6 +81,7 @@ async def test_get_notification_notification_triggers( async def test_if_notification_notification_fires( hass: HomeAssistant, + device_registry: dr.DeviceRegistry, client, lock_schlage_be469, integration, @@ -82,8 +89,7 @@ async def test_if_notification_notification_fires( ) -> None: """Test for event.notification.notification trigger firing.""" node: Node = lock_schlage_be469 - dev_reg = async_get_dev_reg(hass) - device = dev_reg.async_get_device( + device = device_registry.async_get_device( identifiers={get_device_id(client.driver, lock_schlage_be469)} ) assert device @@ -174,11 +180,14 @@ async def test_if_notification_notification_fires( async def test_get_trigger_capabilities_notification_notification( - hass: HomeAssistant, client, lock_schlage_be469, integration + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + client, + lock_schlage_be469, + integration, ) -> None: """Test we get the expected capabilities from a notification.notification trigger.""" - dev_reg = async_get_dev_reg(hass) - device = dev_reg.async_get_device( + device = device_registry.async_get_device( identifiers={get_device_id(client.driver, lock_schlage_be469)} ) assert device @@ -208,6 +217,7 @@ async def test_get_trigger_capabilities_notification_notification( async def test_if_entry_control_notification_fires( hass: HomeAssistant, + device_registry: dr.DeviceRegistry, client, lock_schlage_be469, integration, @@ -215,8 +225,7 @@ async def test_if_entry_control_notification_fires( ) -> None: """Test for notification.entry_control trigger firing.""" node: Node = lock_schlage_be469 - dev_reg = async_get_dev_reg(hass) - device = dev_reg.async_get_device( + device = device_registry.async_get_device( identifiers={get_device_id(client.driver, lock_schlage_be469)} ) assert device @@ -306,11 +315,14 @@ async def test_if_entry_control_notification_fires( async def test_get_trigger_capabilities_entry_control_notification( - hass: HomeAssistant, client, lock_schlage_be469, integration + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + client, + lock_schlage_be469, + integration, ) -> None: """Test we get the expected capabilities from a notification.entry_control trigger.""" - dev_reg = async_get_dev_reg(hass) - device = dev_reg.async_get_device( + device = device_registry.async_get_device( identifiers={get_device_id(client.driver, lock_schlage_be469)} ) assert device @@ -337,19 +349,22 @@ async def test_get_trigger_capabilities_entry_control_notification( async def test_get_node_status_triggers( - hass: HomeAssistant, client, lock_schlage_be469, integration + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, + client, + lock_schlage_be469, + integration, ) -> None: """Test we get the expected triggers from a device with node status sensor enabled.""" - dev_reg = async_get_dev_reg(hass) - device = dev_reg.async_get_device( + device = device_registry.async_get_device( identifiers={get_device_id(client.driver, lock_schlage_be469)} ) assert device - ent_reg = async_get_ent_reg(hass) entity_id = async_get_node_status_sensor_entity_id( - hass, device.id, ent_reg, dev_reg + hass, device.id, entity_registry, device_registry ) - entity = ent_reg.async_update_entity(entity_id, disabled_by=None) + entity = entity_registry.async_update_entity(entity_id, disabled_by=None) await hass.config_entries.async_reload(integration.entry_id) await hass.async_block_till_done() @@ -369,6 +384,8 @@ async def test_get_node_status_triggers( async def test_if_node_status_change_fires( hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, client, lock_schlage_be469, integration, @@ -376,16 +393,14 @@ async def test_if_node_status_change_fires( ) -> None: """Test for node_status trigger firing.""" node: Node = lock_schlage_be469 - dev_reg = async_get_dev_reg(hass) - device = dev_reg.async_get_device( + device = device_registry.async_get_device( identifiers={get_device_id(client.driver, lock_schlage_be469)} ) assert device - ent_reg = async_get_ent_reg(hass) entity_id = async_get_node_status_sensor_entity_id( - hass, device.id, ent_reg, dev_reg + hass, device.id, entity_registry, device_registry ) - entity = ent_reg.async_update_entity(entity_id, disabled_by=None) + entity = entity_registry.async_update_entity(entity_id, disabled_by=None) await hass.config_entries.async_reload(integration.entry_id) await hass.async_block_till_done() @@ -452,6 +467,8 @@ async def test_if_node_status_change_fires( async def test_if_node_status_change_fires_legacy( hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, client, lock_schlage_be469, integration, @@ -459,16 +476,14 @@ async def test_if_node_status_change_fires_legacy( ) -> None: """Test for node_status trigger firing.""" node: Node = lock_schlage_be469 - dev_reg = async_get_dev_reg(hass) - device = dev_reg.async_get_device( + device = device_registry.async_get_device( {get_device_id(client.driver, lock_schlage_be469)} ) assert device - ent_reg = async_get_ent_reg(hass) entity_id = async_get_node_status_sensor_entity_id( - hass, device.id, ent_reg, dev_reg + hass, device.id, entity_registry, device_registry ) - ent_reg.async_update_entity(entity_id, disabled_by=None) + entity_registry.async_update_entity(entity_id, disabled_by=None) await hass.config_entries.async_reload(integration.entry_id) await hass.async_block_till_done() @@ -534,19 +549,22 @@ async def test_if_node_status_change_fires_legacy( async def test_get_trigger_capabilities_node_status( - hass: HomeAssistant, client, lock_schlage_be469, integration + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, + client, + lock_schlage_be469, + integration, ) -> None: """Test we get the expected capabilities from a node_status trigger.""" - dev_reg = async_get_dev_reg(hass) - device = dev_reg.async_get_device( + device = device_registry.async_get_device( identifiers={get_device_id(client.driver, lock_schlage_be469)} ) assert device - ent_reg = async_get_ent_reg(hass) entity_id = async_get_node_status_sensor_entity_id( - hass, device.id, ent_reg, dev_reg + hass, device.id, entity_registry, device_registry ) - ent_reg.async_update_entity(entity_id, disabled_by=None) + entity_registry.async_update_entity(entity_id, disabled_by=None) await hass.config_entries.async_reload(integration.entry_id) await hass.async_block_till_done() @@ -592,11 +610,14 @@ async def test_get_trigger_capabilities_node_status( async def test_get_basic_value_notification_triggers( - hass: HomeAssistant, client, ge_in_wall_dimmer_switch, integration + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + client, + ge_in_wall_dimmer_switch, + integration, ) -> None: """Test we get the expected triggers from a zwave_js device with the Basic CC.""" - dev_reg = async_get_dev_reg(hass) - device = dev_reg.async_get_device( + device = device_registry.async_get_device( identifiers={get_device_id(client.driver, ge_in_wall_dimmer_switch)} ) assert device @@ -620,6 +641,7 @@ async def test_get_basic_value_notification_triggers( async def test_if_basic_value_notification_fires( hass: HomeAssistant, + device_registry: dr.DeviceRegistry, client, ge_in_wall_dimmer_switch, integration, @@ -627,8 +649,7 @@ async def test_if_basic_value_notification_fires( ) -> None: """Test for event.value_notification.basic trigger firing.""" node: Node = ge_in_wall_dimmer_switch - dev_reg = async_get_dev_reg(hass) - device = dev_reg.async_get_device( + device = device_registry.async_get_device( identifiers={get_device_id(client.driver, ge_in_wall_dimmer_switch)} ) assert device @@ -733,11 +754,14 @@ async def test_if_basic_value_notification_fires( async def test_get_trigger_capabilities_basic_value_notification( - hass: HomeAssistant, client, ge_in_wall_dimmer_switch, integration + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + client, + ge_in_wall_dimmer_switch, + integration, ) -> None: """Test we get the expected capabilities from a value_notification.basic trigger.""" - dev_reg = async_get_dev_reg(hass) - device = dev_reg.async_get_device( + device = device_registry.async_get_device( identifiers={get_device_id(client.driver, ge_in_wall_dimmer_switch)} ) assert device @@ -771,11 +795,14 @@ async def test_get_trigger_capabilities_basic_value_notification( async def test_get_central_scene_value_notification_triggers( - hass: HomeAssistant, client, wallmote_central_scene, integration + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + client, + wallmote_central_scene, + integration, ) -> None: """Test we get the expected triggers from a zwave_js device with the Central Scene CC.""" - dev_reg = async_get_dev_reg(hass) - device = dev_reg.async_get_device( + device = device_registry.async_get_device( identifiers={get_device_id(client.driver, wallmote_central_scene)} ) assert device @@ -799,6 +826,7 @@ async def test_get_central_scene_value_notification_triggers( async def test_if_central_scene_value_notification_fires( hass: HomeAssistant, + device_registry: dr.DeviceRegistry, client, wallmote_central_scene, integration, @@ -806,8 +834,7 @@ async def test_if_central_scene_value_notification_fires( ) -> None: """Test for event.value_notification.central_scene trigger firing.""" node: Node = wallmote_central_scene - dev_reg = async_get_dev_reg(hass) - device = dev_reg.async_get_device( + device = device_registry.async_get_device( identifiers={get_device_id(client.driver, wallmote_central_scene)} ) assert device @@ -918,11 +945,14 @@ async def test_if_central_scene_value_notification_fires( async def test_get_trigger_capabilities_central_scene_value_notification( - hass: HomeAssistant, client, wallmote_central_scene, integration + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + client, + wallmote_central_scene, + integration, ) -> None: """Test we get the expected capabilities from a value_notification.central_scene trigger.""" - dev_reg = async_get_dev_reg(hass) - device = dev_reg.async_get_device( + device = device_registry.async_get_device( identifiers={get_device_id(client.driver, wallmote_central_scene)} ) assert device @@ -955,11 +985,14 @@ async def test_get_trigger_capabilities_central_scene_value_notification( async def test_get_scene_activation_value_notification_triggers( - hass: HomeAssistant, client, hank_binary_switch, integration + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + client, + hank_binary_switch, + integration, ) -> None: """Test we get the expected triggers from a zwave_js device with the SceneActivation CC.""" - dev_reg = async_get_dev_reg(hass) - device = dev_reg.async_get_device( + device = device_registry.async_get_device( identifiers={get_device_id(client.driver, hank_binary_switch)} ) assert device @@ -983,6 +1016,7 @@ async def test_get_scene_activation_value_notification_triggers( async def test_if_scene_activation_value_notification_fires( hass: HomeAssistant, + device_registry: dr.DeviceRegistry, client, hank_binary_switch, integration, @@ -990,8 +1024,7 @@ async def test_if_scene_activation_value_notification_fires( ) -> None: """Test for event.value_notification.scene_activation trigger firing.""" node: Node = hank_binary_switch - dev_reg = async_get_dev_reg(hass) - device = dev_reg.async_get_device( + device = device_registry.async_get_device( identifiers={get_device_id(client.driver, hank_binary_switch)} ) assert device @@ -1096,11 +1129,14 @@ async def test_if_scene_activation_value_notification_fires( async def test_get_trigger_capabilities_scene_activation_value_notification( - hass: HomeAssistant, client, hank_binary_switch, integration + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + client, + hank_binary_switch, + integration, ) -> None: """Test we get the expected capabilities from a value_notification.scene_activation trigger.""" - dev_reg = async_get_dev_reg(hass) - device = dev_reg.async_get_device( + device = device_registry.async_get_device( identifiers={get_device_id(client.driver, hank_binary_switch)} ) assert device @@ -1134,11 +1170,14 @@ async def test_get_trigger_capabilities_scene_activation_value_notification( async def test_get_value_updated_value_triggers( - hass: HomeAssistant, client, lock_schlage_be469, integration + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + client, + lock_schlage_be469, + integration, ) -> None: """Test we get the zwave_js.value_updated.value trigger from a zwave_js device.""" - dev_reg = async_get_dev_reg(hass) - device = dev_reg.async_get_device( + device = device_registry.async_get_device( identifiers={get_device_id(client.driver, lock_schlage_be469)} ) assert device @@ -1157,6 +1196,7 @@ async def test_get_value_updated_value_triggers( async def test_if_value_updated_value_fires( hass: HomeAssistant, + device_registry: dr.DeviceRegistry, client, lock_schlage_be469, integration, @@ -1164,8 +1204,7 @@ async def test_if_value_updated_value_fires( ) -> None: """Test for zwave_js.value_updated.value trigger firing.""" node: Node = lock_schlage_be469 - dev_reg = async_get_dev_reg(hass) - device = dev_reg.async_get_device( + device = device_registry.async_get_device( identifiers={get_device_id(client.driver, lock_schlage_be469)} ) assert device @@ -1253,6 +1292,7 @@ async def test_if_value_updated_value_fires( async def test_value_updated_value_no_driver( hass: HomeAssistant, + device_registry: dr.DeviceRegistry, client, lock_schlage_be469, integration, @@ -1260,8 +1300,7 @@ async def test_value_updated_value_no_driver( ) -> None: """Test zwave_js.value_updated.value trigger with missing driver.""" node: Node = lock_schlage_be469 - dev_reg = async_get_dev_reg(hass) - device = dev_reg.async_get_device( + device = device_registry.async_get_device( identifiers={get_device_id(client.driver, lock_schlage_be469)} ) assert device @@ -1327,11 +1366,14 @@ async def test_value_updated_value_no_driver( async def test_get_trigger_capabilities_value_updated_value( - hass: HomeAssistant, client, lock_schlage_be469, integration + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + client, + lock_schlage_be469, + integration, ) -> None: """Test we get the expected capabilities from a zwave_js.value_updated.value trigger.""" - dev_reg = async_get_dev_reg(hass) - device = dev_reg.async_get_device( + device = device_registry.async_get_device( identifiers={get_device_id(client.driver, lock_schlage_be469)} ) assert device @@ -1378,11 +1420,14 @@ async def test_get_trigger_capabilities_value_updated_value( async def test_get_value_updated_config_parameter_triggers( - hass: HomeAssistant, client, lock_schlage_be469, integration + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + client, + lock_schlage_be469, + integration, ) -> None: """Test we get the zwave_js.value_updated.config_parameter trigger from a zwave_js device.""" - dev_reg = async_get_dev_reg(hass) - device = dev_reg.async_get_device( + device = device_registry.async_get_device( identifiers={get_device_id(client.driver, lock_schlage_be469)} ) assert device @@ -1406,6 +1451,7 @@ async def test_get_value_updated_config_parameter_triggers( async def test_if_value_updated_config_parameter_fires( hass: HomeAssistant, + device_registry: dr.DeviceRegistry, client, lock_schlage_be469, integration, @@ -1413,8 +1459,7 @@ async def test_if_value_updated_config_parameter_fires( ) -> None: """Test for zwave_js.value_updated.config_parameter trigger firing.""" node: Node = lock_schlage_be469 - dev_reg = async_get_dev_reg(hass) - device = dev_reg.async_get_device( + device = device_registry.async_get_device( identifiers={get_device_id(client.driver, lock_schlage_be469)} ) assert device @@ -1480,11 +1525,14 @@ async def test_if_value_updated_config_parameter_fires( async def test_get_trigger_capabilities_value_updated_config_parameter_range( - hass: HomeAssistant, client, lock_schlage_be469, integration + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + client, + lock_schlage_be469, + integration, ) -> None: """Test we get the expected capabilities from a range zwave_js.value_updated.config_parameter trigger.""" - dev_reg = async_get_dev_reg(hass) - device = dev_reg.async_get_device( + device = device_registry.async_get_device( identifiers={get_device_id(client.driver, lock_schlage_be469)} ) assert device @@ -1525,11 +1573,14 @@ async def test_get_trigger_capabilities_value_updated_config_parameter_range( async def test_get_trigger_capabilities_value_updated_config_parameter_enumerated( - hass: HomeAssistant, client, lock_schlage_be469, integration + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + client, + lock_schlage_be469, + integration, ) -> None: """Test we get the expected capabilities from an enumerated zwave_js.value_updated.config_parameter trigger.""" - dev_reg = async_get_dev_reg(hass) - device = dev_reg.async_get_device( + device = device_registry.async_get_device( identifiers={get_device_id(client.driver, lock_schlage_be469)} ) assert device @@ -1568,7 +1619,11 @@ async def test_get_trigger_capabilities_value_updated_config_parameter_enumerate async def test_failure_scenarios( - hass: HomeAssistant, client, hank_binary_switch, integration + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + client, + hank_binary_switch, + integration, ) -> None: """Test failure scenarios.""" with pytest.raises(HomeAssistantError): @@ -1584,8 +1639,7 @@ async def test_failure_scenarios( {}, ) - dev_reg = async_get_dev_reg(hass) - device = dev_reg.async_get_device( + device = device_registry.async_get_device( identifiers={get_device_id(client.driver, hank_binary_switch)} ) assert device diff --git a/tests/components/zwave_js/test_init.py b/tests/components/zwave_js/test_init.py index 8c9c05a124e..51aeee72c1d 100644 --- a/tests/components/zwave_js/test_init.py +++ b/tests/components/zwave_js/test_init.py @@ -368,6 +368,7 @@ async def test_existing_node_not_ready( async def test_existing_node_not_replaced_when_not_ready( hass: HomeAssistant, + area_registry: ar.AreaRegistry, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, zp3111, @@ -375,7 +376,6 @@ async def test_existing_node_not_replaced_when_not_ready( zp3111_state, client, integration, - area_registry: ar.AreaRegistry, ) -> None: """Test when a node added event with a non-ready node is received. diff --git a/tests/components/zwave_js/test_services.py b/tests/components/zwave_js/test_services.py index 5462bcf9946..ec13d0262f8 100644 --- a/tests/components/zwave_js/test_services.py +++ b/tests/components/zwave_js/test_services.py @@ -41,9 +41,11 @@ from homeassistant.components.zwave_js.helpers import get_device_id from homeassistant.const import ATTR_AREA_ID, ATTR_DEVICE_ID, ATTR_ENTITY_ID from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.area_registry import async_get as async_get_area_reg -from homeassistant.helpers.device_registry import async_get as async_get_dev_reg -from homeassistant.helpers.entity_registry import async_get as async_get_ent_reg +from homeassistant.helpers import ( + area_registry as ar, + device_registry as dr, + entity_registry as er, +) from homeassistant.setup import async_setup_component from .common import ( @@ -61,6 +63,9 @@ from tests.common import MockConfigEntry async def test_set_config_parameter( hass: HomeAssistant, + area_registry: ar.AreaRegistry, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, client, multisensor_6, aeotec_zw164_siren, @@ -68,9 +73,7 @@ async def test_set_config_parameter( caplog: pytest.LogCaptureFixture, ) -> None: """Test the set_config_parameter service.""" - dev_reg = async_get_dev_reg(hass) - ent_reg = async_get_ent_reg(hass) - entity_entry = ent_reg.async_get(AIR_TEMPERATURE_SENSOR) + entity_entry = entity_registry.async_get(AIR_TEMPERATURE_SENSOR) # Test setting config parameter by property and property_key await hass.services.async_call( @@ -179,9 +182,8 @@ async def test_set_config_parameter( client.async_send_command_no_wait.reset_mock() # Test using area ID - area_reg = async_get_area_reg(hass) - area = area_reg.async_get_or_create("test") - ent_reg.async_update_entity(entity_entry.entity_id, area_id=area.id) + area = area_registry.async_get_or_create("test") + entity_registry.async_update_entity(entity_entry.entity_id, area_id=area.id) await hass.services.async_call( DOMAIN, SERVICE_SET_CONFIG_PARAMETER, @@ -345,16 +347,16 @@ async def test_set_config_parameter( non_zwave_js_config_entry = MockConfigEntry(entry_id="fake_entry_id") non_zwave_js_config_entry.add_to_hass(hass) - non_zwave_js_device = dev_reg.async_get_or_create( + non_zwave_js_device = device_registry.async_get_or_create( config_entry_id=non_zwave_js_config_entry.entry_id, identifiers={("test", "test")}, ) - zwave_js_device_with_invalid_node_id = dev_reg.async_get_or_create( + zwave_js_device_with_invalid_node_id = device_registry.async_get_or_create( config_entry_id=integration.entry_id, identifiers={(DOMAIN, "500-500")} ) - non_zwave_js_entity = ent_reg.async_get_or_create( + non_zwave_js_entity = entity_registry.async_get_or_create( "test", "sensor", "test_sensor", @@ -601,11 +603,15 @@ async def test_set_config_parameter_gather( async def test_bulk_set_config_parameters( - hass: HomeAssistant, client, multisensor_6, integration + hass: HomeAssistant, + area_registry: ar.AreaRegistry, + device_registry: dr.DeviceRegistry, + client, + multisensor_6, + integration, ) -> None: """Test the bulk_set_partial_config_parameters service.""" - dev_reg = async_get_dev_reg(hass) - device = dev_reg.async_get_device( + device = device_registry.async_get_device( identifiers={get_device_id(client.driver, multisensor_6)} ) assert device @@ -636,9 +642,8 @@ async def test_bulk_set_config_parameters( client.async_send_command_no_wait.reset_mock() # Test using area ID - area_reg = async_get_area_reg(hass) - area = area_reg.async_get_or_create("test") - dev_reg.async_update_device(device.id, area_id=area.id) + area = area_registry.async_get_or_create("test") + device_registry.async_update_device(device.id, area_id=area.id) await hass.services.async_call( DOMAIN, SERVICE_BULK_SET_PARTIAL_CONFIG_PARAMETERS, @@ -968,11 +973,15 @@ async def test_refresh_value( async def test_set_value( - hass: HomeAssistant, client, climate_danfoss_lc_13, integration + hass: HomeAssistant, + area_registry: ar.AreaRegistry, + device_registry: dr.DeviceRegistry, + client, + climate_danfoss_lc_13, + integration, ) -> None: """Test set_value service.""" - dev_reg = async_get_dev_reg(hass) - device = dev_reg.async_get_device( + device = device_registry.async_get_device( identifiers={get_device_id(client.driver, climate_danfoss_lc_13)} ) assert device @@ -1030,9 +1039,8 @@ async def test_set_value( client.async_send_command.reset_mock() # Test using area ID - area_reg = async_get_area_reg(hass) - area = area_reg.async_get_or_create("test") - dev_reg.async_update_device(device.id, area_id=area.id) + area = area_registry.async_get_or_create("test") + device_registry.async_update_device(device.id, area_id=area.id) await hass.services.async_call( DOMAIN, SERVICE_SET_VALUE, @@ -1254,6 +1262,8 @@ async def test_set_value_gather( async def test_multicast_set_value( hass: HomeAssistant, + area_registry: ar.AreaRegistry, + device_registry: dr.DeviceRegistry, client, climate_danfoss_lc_13, climate_eurotronic_spirit_z, @@ -1327,19 +1337,17 @@ async def test_multicast_set_value( client.async_send_command.reset_mock() # Test using area ID - dev_reg = async_get_dev_reg(hass) - device_eurotronic = dev_reg.async_get_device( + device_eurotronic = device_registry.async_get_device( identifiers={get_device_id(client.driver, climate_eurotronic_spirit_z)} ) assert device_eurotronic - device_danfoss = dev_reg.async_get_device( + device_danfoss = device_registry.async_get_device( identifiers={get_device_id(client.driver, climate_danfoss_lc_13)} ) assert device_danfoss - area_reg = async_get_area_reg(hass) - area = area_reg.async_get_or_create("test") - dev_reg.async_update_device(device_eurotronic.id, area_id=area.id) - dev_reg.async_update_device(device_danfoss.id, area_id=area.id) + area = area_registry.async_get_or_create("test") + device_registry.async_update_device(device_eurotronic.id, area_id=area.id) + device_registry.async_update_device(device_danfoss.id, area_id=area.id) await hass.services.async_call( DOMAIN, SERVICE_MULTICAST_SET_VALUE, @@ -1646,14 +1654,15 @@ async def test_multicast_set_value_string( async def test_ping( hass: HomeAssistant, + area_registry: ar.AreaRegistry, + device_registry: dr.DeviceRegistry, client, climate_danfoss_lc_13, climate_radio_thermostat_ct100_plus_different_endpoints, integration, ) -> None: """Test ping service.""" - dev_reg = async_get_dev_reg(hass) - device_radio_thermostat = dev_reg.async_get_device( + device_radio_thermostat = device_registry.async_get_device( identifiers={ get_device_id( client.driver, climate_radio_thermostat_ct100_plus_different_endpoints @@ -1661,7 +1670,7 @@ async def test_ping( } ) assert device_radio_thermostat - device_danfoss = dev_reg.async_get_device( + device_danfoss = device_registry.async_get_device( identifiers={get_device_id(client.driver, climate_danfoss_lc_13)} ) assert device_danfoss @@ -1721,10 +1730,9 @@ async def test_ping( client.async_send_command.reset_mock() # Test successful ping call with area - area_reg = async_get_area_reg(hass) - area = area_reg.async_get_or_create("test") - dev_reg.async_update_device(device_radio_thermostat.id, area_id=area.id) - dev_reg.async_update_device(device_danfoss.id, area_id=area.id) + area = area_registry.async_get_or_create("test") + device_registry.async_update_device(device_radio_thermostat.id, area_id=area.id) + device_registry.async_update_device(device_danfoss.id, area_id=area.id) await hass.services.async_call( DOMAIN, SERVICE_PING, @@ -1803,14 +1811,15 @@ async def test_ping( async def test_invoke_cc_api( hass: HomeAssistant, + area_registry: ar.AreaRegistry, + device_registry: dr.DeviceRegistry, client, climate_danfoss_lc_13, climate_radio_thermostat_ct100_plus_different_endpoints, integration, ) -> None: """Test invoke_cc_api service.""" - dev_reg = async_get_dev_reg(hass) - device_radio_thermostat = dev_reg.async_get_device( + device_radio_thermostat = device_registry.async_get_device( identifiers={ get_device_id( client.driver, climate_radio_thermostat_ct100_plus_different_endpoints @@ -1818,7 +1827,7 @@ async def test_invoke_cc_api( } ) assert device_radio_thermostat - device_danfoss = dev_reg.async_get_device( + device_danfoss = device_registry.async_get_device( identifiers={get_device_id(client.driver, climate_danfoss_lc_13)} ) assert device_danfoss @@ -1868,9 +1877,8 @@ async def test_invoke_cc_api( client.async_send_command_no_wait.reset_mock() # Test successful invoke_cc_api call without an endpoint (include area) - area_reg = async_get_area_reg(hass) - area = area_reg.async_get_or_create("test") - dev_reg.async_update_device(device_danfoss.id, area_id=area.id) + area = area_registry.async_get_or_create("test") + device_registry.async_update_device(device_danfoss.id, area_id=area.id) client.async_send_command.return_value = {"response": True} client.async_send_command_no_wait.return_value = {"response": True} @@ -1969,22 +1977,26 @@ async def test_invoke_cc_api( async def test_refresh_notifications( - hass: HomeAssistant, client, zen_31, multisensor_6, integration + hass: HomeAssistant, + area_registry: ar.AreaRegistry, + device_registry: dr.DeviceRegistry, + client, + zen_31, + multisensor_6, + integration, ) -> None: """Test refresh_notifications service.""" - dev_reg = async_get_dev_reg(hass) - zen_31_device = dev_reg.async_get_device( + zen_31_device = device_registry.async_get_device( identifiers={get_device_id(client.driver, zen_31)} ) assert zen_31_device - multisensor_6_device = dev_reg.async_get_device( + multisensor_6_device = device_registry.async_get_device( identifiers={get_device_id(client.driver, multisensor_6)} ) assert multisensor_6_device - area_reg = async_get_area_reg(hass) - area = area_reg.async_get_or_create("test") - dev_reg.async_update_device(zen_31_device.id, area_id=area.id) + area = area_registry.async_get_or_create("test") + device_registry.async_update_device(zen_31_device.id, area_id=area.id) # Test successful refresh_notifications call client.async_send_command.return_value = {"response": True} diff --git a/tests/components/zwave_js/test_trigger.py b/tests/components/zwave_js/test_trigger.py index 23c97913400..5822afe7b9f 100644 --- a/tests/components/zwave_js/test_trigger.py +++ b/tests/components/zwave_js/test_trigger.py @@ -20,7 +20,7 @@ from homeassistant.components.zwave_js.triggers.trigger_helpers import ( ) from homeassistant.const import CONF_PLATFORM, SERVICE_RELOAD from homeassistant.core import HomeAssistant -from homeassistant.helpers.device_registry import async_get as async_get_dev_reg +from homeassistant.helpers import device_registry as dr from homeassistant.setup import async_setup_component from .common import SCHLAGE_BE469_LOCK_ENTITY @@ -29,13 +29,16 @@ from tests.common import async_capture_events async def test_zwave_js_value_updated( - hass: HomeAssistant, client, lock_schlage_be469, integration + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + client, + lock_schlage_be469, + integration, ) -> None: """Test for zwave_js.value_updated automation trigger.""" trigger_type = f"{DOMAIN}.value_updated" node: Node = lock_schlage_be469 - dev_reg = async_get_dev_reg(hass) - device = dev_reg.async_get_device( + device = device_registry.async_get_device( identifiers={get_device_id(client.driver, lock_schlage_be469)} ) assert device @@ -453,13 +456,16 @@ async def test_zwave_js_value_updated_bypass_dynamic_validation_no_driver( async def test_zwave_js_event( - hass: HomeAssistant, client, lock_schlage_be469, integration + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + client, + lock_schlage_be469, + integration, ) -> None: """Test for zwave_js.event automation trigger.""" trigger_type = f"{DOMAIN}.event" node: Node = lock_schlage_be469 - dev_reg = async_get_dev_reg(hass) - device = dev_reg.async_get_device( + device = device_registry.async_get_device( identifiers={get_device_id(client.driver, lock_schlage_be469)} ) assert device @@ -1009,11 +1015,14 @@ async def test_invalid_trigger_configs(hass: HomeAssistant) -> None: async def test_zwave_js_trigger_config_entry_unloaded( - hass: HomeAssistant, client, lock_schlage_be469, integration + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + client, + lock_schlage_be469, + integration, ) -> None: """Test zwave_js triggers bypass dynamic validation when needed.""" - dev_reg = async_get_dev_reg(hass) - device = dev_reg.async_get_device( + device = device_registry.async_get_device( identifiers={get_device_id(client.driver, lock_schlage_be469)} ) assert device diff --git a/tests/components/zwave_js/test_update.py b/tests/components/zwave_js/test_update.py index 338d1511fc3..abdceb155f7 100644 --- a/tests/components/zwave_js/test_update.py +++ b/tests/components/zwave_js/test_update.py @@ -25,7 +25,7 @@ from homeassistant.components.zwave_js.helpers import get_valueless_base_unique_ from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON, STATE_UNKNOWN from homeassistant.core import CoreState, HomeAssistant, State from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.entity_registry import async_get +from homeassistant.helpers import entity_registry as er from homeassistant.util import dt as dt_util from tests.common import ( @@ -113,6 +113,7 @@ FIRMWARE_UPDATES = { async def test_update_entity_states( hass: HomeAssistant, + entity_registry: er.EntityRegistry, client, climate_radio_thermostat_ct100_plus_different_endpoints, integration, @@ -194,7 +195,7 @@ async def test_update_entity_states( node = driver.controller.nodes[1] assert node.is_controller_node assert ( - async_get(hass).async_get_entity_id( + entity_registry.async_get_entity_id( DOMAIN, "sensor", f"{get_valueless_base_unique_id(driver, node)}.firmware_update", From fbaba3753b6c106017ea29d8768206c897967bc0 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 10 Jun 2024 15:14:49 +0200 Subject: [PATCH 0464/1445] Fix root-import pylint warning in components (#119294) * Fix root-import pylint warning in components * Adjust * Adjust --- tests/components/camera/test_init.py | 2 +- tests/components/config/test_core.py | 2 +- .../components/device_automation/test_init.py | 2 +- tests/components/diagnostics/test_init.py | 2 +- tests/components/ecovacs/test_button.py | 2 +- tests/components/ecovacs/test_event.py | 2 +- tests/components/ecovacs/test_lawn_mower.py | 4 +- tests/components/ecovacs/test_number.py | 2 +- tests/components/ecovacs/test_switch.py | 2 +- tests/components/flexit_bacnet/test_number.py | 7 ++- tests/components/frontend/test_init.py | 2 +- tests/components/generic/test_camera.py | 4 +- tests/components/group/test_event.py | 7 ++- .../test_silabs_multiprotocol_addon.py | 2 +- .../homeassistant_yellow/test_config_flow.py | 2 +- .../homematicip_cloud/test_button.py | 3 +- tests/components/huawei_lte/test_select.py | 6 +- tests/components/image_upload/test_init.py | 6 +- tests/components/imap/test_diagnostics.py | 2 +- tests/components/imap/test_init.py | 2 +- tests/components/kitchen_sink/test_notify.py | 2 +- .../components/logbook/test_websocket_api.py | 2 +- tests/components/logger/test_websocket_api.py | 20 +++--- tests/components/matter/test_climate.py | 3 +- tests/components/media_player/test_init.py | 2 +- tests/components/modbus/test_climate.py | 4 +- tests/components/nest/test_camera.py | 2 +- .../persistent_notification/test_init.py | 2 +- tests/components/ping/conftest.py | 5 +- tests/components/plex/test_browse_media.py | 2 +- tests/components/plugwise/test_climate.py | 2 +- tests/components/roku/test_media_player.py | 2 +- tests/components/rtsp_to_webrtc/test_init.py | 2 +- tests/components/shopping_list/test_init.py | 2 +- tests/components/smartthings/test_climate.py | 2 +- tests/components/smhi/test_weather.py | 6 +- tests/components/sonos/test_media_browser.py | 3 +- tests/components/sonos/test_media_player.py | 6 +- tests/components/sql/test_config_flow.py | 2 +- .../components/trafikverket_train/__init__.py | 4 +- tests/components/vallox/test_date.py | 2 +- tests/components/vallox/test_number.py | 2 +- tests/components/vallox/test_switch.py | 2 +- tests/components/weatherkit/test_weather.py | 2 +- .../xiaomi_ble/test_device_trigger.py | 2 +- .../yale_smart_alarm/test_button.py | 3 +- tests/components/zha/test_websocket_api.py | 62 ++++++++++--------- 47 files changed, 107 insertions(+), 106 deletions(-) diff --git a/tests/components/camera/test_init.py b/tests/components/camera/test_init.py index 669c3594648..7da6cd91a7a 100644 --- a/tests/components/camera/test_init.py +++ b/tests/components/camera/test_init.py @@ -14,7 +14,7 @@ from homeassistant.components.camera.const import ( PREF_ORIENTATION, PREF_PRELOAD_STREAM, ) -from homeassistant.components.websocket_api.const import TYPE_RESULT +from homeassistant.components.websocket_api import TYPE_RESULT from homeassistant.config import async_process_ha_core_config from homeassistant.const import ( ATTR_ENTITY_ID, diff --git a/tests/components/config/test_core.py b/tests/components/config/test_core.py index 29cbdd9b83e..b351493dac7 100644 --- a/tests/components/config/test_core.py +++ b/tests/components/config/test_core.py @@ -8,7 +8,7 @@ import pytest from homeassistant.bootstrap import async_setup_component from homeassistant.components import config from homeassistant.components.config import core -from homeassistant.components.websocket_api.const import TYPE_RESULT +from homeassistant.components.websocket_api import TYPE_RESULT from homeassistant.const import ( CONF_UNIT_SYSTEM, CONF_UNIT_SYSTEM_IMPERIAL, diff --git a/tests/components/device_automation/test_init.py b/tests/components/device_automation/test_init.py index fa6a3e840a9..7d68a944de1 100644 --- a/tests/components/device_automation/test_init.py +++ b/tests/components/device_automation/test_init.py @@ -13,7 +13,7 @@ from homeassistant.components.device_automation import ( InvalidDeviceAutomationConfig, toggle_entity, ) -from homeassistant.components.websocket_api.const import TYPE_RESULT +from homeassistant.components.websocket_api import TYPE_RESULT from homeassistant.config_entries import ConfigEntryState from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant, ServiceCall diff --git a/tests/components/diagnostics/test_init.py b/tests/components/diagnostics/test_init.py index 1189cc6a65d..40a8f5ab744 100644 --- a/tests/components/diagnostics/test_init.py +++ b/tests/components/diagnostics/test_init.py @@ -5,7 +5,7 @@ from unittest.mock import AsyncMock, Mock, patch import pytest -from homeassistant.components.websocket_api.const import TYPE_RESULT +from homeassistant.components.websocket_api import TYPE_RESULT from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import async_get from homeassistant.helpers.system_info import async_get_system_info diff --git a/tests/components/ecovacs/test_button.py b/tests/components/ecovacs/test_button.py index 277983eb0c5..82a75654b58 100644 --- a/tests/components/ecovacs/test_button.py +++ b/tests/components/ecovacs/test_button.py @@ -7,7 +7,7 @@ from deebot_client.events import LifeSpan import pytest from syrupy import SnapshotAssertion -from homeassistant.components.button.const import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS +from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS from homeassistant.components.ecovacs.const import DOMAIN from homeassistant.components.ecovacs.controller import EcovacsController from homeassistant.const import ATTR_ENTITY_ID, STATE_UNKNOWN, Platform diff --git a/tests/components/ecovacs/test_event.py b/tests/components/ecovacs/test_event.py index 104a3bfc69e..1ee3efbf64d 100644 --- a/tests/components/ecovacs/test_event.py +++ b/tests/components/ecovacs/test_event.py @@ -10,7 +10,7 @@ from syrupy import SnapshotAssertion from homeassistant.components.ecovacs.const import DOMAIN from homeassistant.components.ecovacs.controller import EcovacsController -from homeassistant.components.event.const import ATTR_EVENT_TYPE +from homeassistant.components.event import ATTR_EVENT_TYPE from homeassistant.const import STATE_UNKNOWN, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er diff --git a/tests/components/ecovacs/test_lawn_mower.py b/tests/components/ecovacs/test_lawn_mower.py index 563e6aecbb0..cd49374d4c2 100644 --- a/tests/components/ecovacs/test_lawn_mower.py +++ b/tests/components/ecovacs/test_lawn_mower.py @@ -14,12 +14,10 @@ from homeassistant.components.ecovacs.const import DOMAIN from homeassistant.components.ecovacs.controller import EcovacsController from homeassistant.components.lawn_mower import ( DOMAIN as PLATFORM_DOMAIN, - LawnMowerActivity, -) -from homeassistant.components.lawn_mower.const import ( SERVICE_DOCK, SERVICE_PAUSE, SERVICE_START_MOWING, + LawnMowerActivity, ) from homeassistant.const import ATTR_ENTITY_ID, STATE_UNKNOWN, Platform from homeassistant.core import HomeAssistant diff --git a/tests/components/ecovacs/test_number.py b/tests/components/ecovacs/test_number.py index 6d8941506b5..0b758fa6860 100644 --- a/tests/components/ecovacs/test_number.py +++ b/tests/components/ecovacs/test_number.py @@ -11,7 +11,7 @@ from syrupy import SnapshotAssertion from homeassistant.components.ecovacs.const import DOMAIN from homeassistant.components.ecovacs.controller import EcovacsController -from homeassistant.components.number.const import ( +from homeassistant.components.number import ( ATTR_VALUE, DOMAIN as PLATFORM_DOMAIN, SERVICE_SET_VALUE, diff --git a/tests/components/ecovacs/test_switch.py b/tests/components/ecovacs/test_switch.py index fee348149ee..2e3feb36586 100644 --- a/tests/components/ecovacs/test_switch.py +++ b/tests/components/ecovacs/test_switch.py @@ -32,7 +32,7 @@ from syrupy import SnapshotAssertion from homeassistant.components.ecovacs.const import DOMAIN from homeassistant.components.ecovacs.controller import EcovacsController -from homeassistant.components.switch.const import DOMAIN as PLATFORM_DOMAIN +from homeassistant.components.switch import DOMAIN as PLATFORM_DOMAIN from homeassistant.const import ( ATTR_ENTITY_ID, SERVICE_TURN_OFF, diff --git a/tests/components/flexit_bacnet/test_number.py b/tests/components/flexit_bacnet/test_number.py index c2f8026b1cd..ad49908fa96 100644 --- a/tests/components/flexit_bacnet/test_number.py +++ b/tests/components/flexit_bacnet/test_number.py @@ -6,8 +6,11 @@ from flexit_bacnet import DecodingError import pytest from syrupy.assertion import SnapshotAssertion -from homeassistant.components.number import DOMAIN as NUMBER_DOMAIN -from homeassistant.components.number.const import ATTR_VALUE, SERVICE_SET_VALUE +from homeassistant.components.number import ( + ATTR_VALUE, + DOMAIN as NUMBER_DOMAIN, + SERVICE_SET_VALUE, +) from homeassistant.const import ATTR_ENTITY_ID, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError diff --git a/tests/components/frontend/test_init.py b/tests/components/frontend/test_init.py index f7ef7da6d1b..084db2a27d5 100644 --- a/tests/components/frontend/test_init.py +++ b/tests/components/frontend/test_init.py @@ -21,7 +21,7 @@ from homeassistant.components.frontend import ( async_register_built_in_panel, async_remove_panel, ) -from homeassistant.components.websocket_api.const import TYPE_RESULT +from homeassistant.components.websocket_api import TYPE_RESULT from homeassistant.core import HomeAssistant from homeassistant.loader import async_get_integration from homeassistant.setup import async_setup_component diff --git a/tests/components/generic/test_camera.py b/tests/components/generic/test_camera.py index 41a97384e27..72a7c32ba25 100644 --- a/tests/components/generic/test_camera.py +++ b/tests/components/generic/test_camera.py @@ -25,8 +25,8 @@ from homeassistant.components.generic.const import ( CONF_STREAM_SOURCE, DOMAIN, ) -from homeassistant.components.stream.const import CONF_RTSP_TRANSPORT -from homeassistant.components.websocket_api.const import TYPE_RESULT +from homeassistant.components.stream import CONF_RTSP_TRANSPORT +from homeassistant.components.websocket_api import TYPE_RESULT from homeassistant.const import ( CONF_AUTHENTICATION, CONF_NAME, diff --git a/tests/components/group/test_event.py b/tests/components/group/test_event.py index f82cc8f314b..1428fbeb8ad 100644 --- a/tests/components/group/test_event.py +++ b/tests/components/group/test_event.py @@ -2,8 +2,11 @@ from pytest_unordered import unordered -from homeassistant.components.event import DOMAIN as EVENT_DOMAIN -from homeassistant.components.event.const import ATTR_EVENT_TYPE, ATTR_EVENT_TYPES +from homeassistant.components.event import ( + ATTR_EVENT_TYPE, + ATTR_EVENT_TYPES, + DOMAIN as EVENT_DOMAIN, +) from homeassistant.components.group import DOMAIN from homeassistant.const import ( ATTR_DEVICE_CLASS, diff --git a/tests/components/homeassistant_hardware/test_silabs_multiprotocol_addon.py b/tests/components/homeassistant_hardware/test_silabs_multiprotocol_addon.py index c7e469b5bbb..63c1ea5a9a4 100644 --- a/tests/components/homeassistant_hardware/test_silabs_multiprotocol_addon.py +++ b/tests/components/homeassistant_hardware/test_silabs_multiprotocol_addon.py @@ -11,7 +11,7 @@ from typing_extensions import Generator from homeassistant.components.hassio import AddonError, AddonInfo, AddonState, HassIO from homeassistant.components.hassio.handler import HassioAPIError from homeassistant.components.homeassistant_hardware import silabs_multiprotocol_addon -from homeassistant.components.zha.core.const import DOMAIN as ZHA_DOMAIN +from homeassistant.components.zha import DOMAIN as ZHA_DOMAIN from homeassistant.config_entries import ConfigEntry, ConfigFlow from homeassistant.const import EVENT_COMPONENT_LOADED from homeassistant.core import HomeAssistant, callback diff --git a/tests/components/homeassistant_yellow/test_config_flow.py b/tests/components/homeassistant_yellow/test_config_flow.py index 34946f20b05..4ae04180a64 100644 --- a/tests/components/homeassistant_yellow/test_config_flow.py +++ b/tests/components/homeassistant_yellow/test_config_flow.py @@ -7,7 +7,7 @@ from typing_extensions import Generator from homeassistant.components.hassio import DOMAIN as HASSIO_DOMAIN from homeassistant.components.homeassistant_yellow.const import DOMAIN -from homeassistant.components.zha.core.const import DOMAIN as ZHA_DOMAIN +from homeassistant.components.zha import DOMAIN as ZHA_DOMAIN from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType from homeassistant.setup import async_setup_component diff --git a/tests/components/homematicip_cloud/test_button.py b/tests/components/homematicip_cloud/test_button.py index 5135c0ec48a..0b5e81dd703 100644 --- a/tests/components/homematicip_cloud/test_button.py +++ b/tests/components/homematicip_cloud/test_button.py @@ -2,8 +2,7 @@ from freezegun.api import FrozenDateTimeFactory -from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN -from homeassistant.components.button.const import SERVICE_PRESS +from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS from homeassistant.const import ATTR_ENTITY_ID, STATE_UNKNOWN from homeassistant.core import HomeAssistant from homeassistant.util import dt as dt_util diff --git a/tests/components/huawei_lte/test_select.py b/tests/components/huawei_lte/test_select.py index f6c8d34c4a0..85a0fcfdf0c 100644 --- a/tests/components/huawei_lte/test_select.py +++ b/tests/components/huawei_lte/test_select.py @@ -5,8 +5,10 @@ from unittest.mock import MagicMock, patch from huawei_lte_api.enums.net import LTEBandEnum, NetworkBandEnum, NetworkModeEnum from homeassistant.components.huawei_lte.const import DOMAIN -from homeassistant.components.select import SERVICE_SELECT_OPTION -from homeassistant.components.select.const import DOMAIN as SELECT_DOMAIN +from homeassistant.components.select import ( + DOMAIN as SELECT_DOMAIN, + SERVICE_SELECT_OPTION, +) from homeassistant.const import ATTR_ENTITY_ID, ATTR_OPTION, CONF_URL from homeassistant.core import HomeAssistant diff --git a/tests/components/image_upload/test_init.py b/tests/components/image_upload/test_init.py index c364fab4a23..d404f1f841e 100644 --- a/tests/components/image_upload/test_init.py +++ b/tests/components/image_upload/test_init.py @@ -7,7 +7,7 @@ from unittest.mock import patch from aiohttp import ClientSession, ClientWebSocketResponse from freezegun.api import FrozenDateTimeFactory -from homeassistant.components.websocket_api import const as ws_const +from homeassistant.components.websocket_api import TYPE_RESULT from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util @@ -77,7 +77,7 @@ async def test_upload_image( msg = await ws_client.receive_json() assert msg["id"] == 6 - assert msg["type"] == ws_const.TYPE_RESULT + assert msg["type"] == TYPE_RESULT assert msg["success"] assert msg["result"] == [item] @@ -88,7 +88,7 @@ async def test_upload_image( msg = await ws_client.receive_json() assert msg["id"] == 7 - assert msg["type"] == ws_const.TYPE_RESULT + assert msg["type"] == TYPE_RESULT assert msg["success"] # Ensure removed from disk diff --git a/tests/components/imap/test_diagnostics.py b/tests/components/imap/test_diagnostics.py index 721e09352f2..23450104aed 100644 --- a/tests/components/imap/test_diagnostics.py +++ b/tests/components/imap/test_diagnostics.py @@ -7,7 +7,7 @@ from unittest.mock import MagicMock import pytest from homeassistant.components import imap -from homeassistant.components.sensor.const import SensorStateClass +from homeassistant.components.sensor import SensorStateClass from homeassistant.core import HomeAssistant from homeassistant.util import dt as dt_util diff --git a/tests/components/imap/test_init.py b/tests/components/imap/test_init.py index fe10770fc64..40c3ce013e4 100644 --- a/tests/components/imap/test_init.py +++ b/tests/components/imap/test_init.py @@ -11,7 +11,7 @@ import pytest from homeassistant.components.imap import DOMAIN from homeassistant.components.imap.const import CONF_CHARSET from homeassistant.components.imap.errors import InvalidAuth, InvalidFolder -from homeassistant.components.sensor.const import SensorStateClass +from homeassistant.components.sensor import SensorStateClass from homeassistant.const import STATE_UNAVAILABLE from homeassistant.core import HomeAssistant from homeassistant.exceptions import ServiceValidationError diff --git a/tests/components/kitchen_sink/test_notify.py b/tests/components/kitchen_sink/test_notify.py index 25fdc61a019..df025087b6b 100644 --- a/tests/components/kitchen_sink/test_notify.py +++ b/tests/components/kitchen_sink/test_notify.py @@ -8,10 +8,10 @@ from typing_extensions import AsyncGenerator from homeassistant.components.kitchen_sink import DOMAIN from homeassistant.components.notify import ( + ATTR_MESSAGE, DOMAIN as NOTIFY_DOMAIN, SERVICE_SEND_MESSAGE, ) -from homeassistant.components.notify.const import ATTR_MESSAGE from homeassistant.const import ATTR_ENTITY_ID, STATE_UNKNOWN, Platform from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component diff --git a/tests/components/logbook/test_websocket_api.py b/tests/components/logbook/test_websocket_api.py index bd11c87f4df..ac653737614 100644 --- a/tests/components/logbook/test_websocket_api.py +++ b/tests/components/logbook/test_websocket_api.py @@ -15,7 +15,7 @@ from homeassistant.components.logbook import websocket_api from homeassistant.components.recorder import Recorder from homeassistant.components.recorder.util import get_instance from homeassistant.components.script import EVENT_SCRIPT_STARTED -from homeassistant.components.websocket_api.const import TYPE_RESULT +from homeassistant.components.websocket_api import TYPE_RESULT from homeassistant.const import ( ATTR_DOMAIN, ATTR_ENTITY_ID, diff --git a/tests/components/logger/test_websocket_api.py b/tests/components/logger/test_websocket_api.py index c2fcc7f208e..5bc280535f9 100644 --- a/tests/components/logger/test_websocket_api.py +++ b/tests/components/logger/test_websocket_api.py @@ -5,7 +5,7 @@ from unittest.mock import patch from homeassistant import loader from homeassistant.components.logger.helpers import async_get_domain_config -from homeassistant.components.websocket_api import const +from homeassistant.components.websocket_api import TYPE_RESULT from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component @@ -28,7 +28,7 @@ async def test_integration_log_info( msg = await websocket_client.receive_json() assert msg["id"] == 7 - assert msg["type"] == const.TYPE_RESULT + assert msg["type"] == TYPE_RESULT assert msg["success"] assert {"domain": "http", "level": logging.DEBUG} in msg["result"] assert {"domain": "websocket_api", "level": logging.DEBUG} in msg["result"] @@ -51,7 +51,7 @@ async def test_integration_log_level_logger_not_loaded( msg = await websocket_client.receive_json() assert msg["id"] == 7 - assert msg["type"] == const.TYPE_RESULT + assert msg["type"] == TYPE_RESULT assert not msg["success"] @@ -74,7 +74,7 @@ async def test_integration_log_level( msg = await websocket_client.receive_json() assert msg["id"] == 7 - assert msg["type"] == const.TYPE_RESULT + assert msg["type"] == TYPE_RESULT assert msg["success"] assert async_get_domain_config(hass).overrides == { @@ -124,7 +124,7 @@ async def test_custom_integration_log_level( msg = await websocket_client.receive_json() assert msg["id"] == 7 - assert msg["type"] == const.TYPE_RESULT + assert msg["type"] == TYPE_RESULT assert msg["success"] assert async_get_domain_config(hass).overrides == { @@ -153,7 +153,7 @@ async def test_integration_log_level_unknown_integration( msg = await websocket_client.receive_json() assert msg["id"] == 7 - assert msg["type"] == const.TYPE_RESULT + assert msg["type"] == TYPE_RESULT assert not msg["success"] @@ -180,7 +180,7 @@ async def test_module_log_level( msg = await websocket_client.receive_json() assert msg["id"] == 7 - assert msg["type"] == const.TYPE_RESULT + assert msg["type"] == TYPE_RESULT assert msg["success"] assert async_get_domain_config(hass).overrides == { @@ -216,7 +216,7 @@ async def test_module_log_level_override( msg = await websocket_client.receive_json() assert msg["id"] == 6 - assert msg["type"] == const.TYPE_RESULT + assert msg["type"] == TYPE_RESULT assert msg["success"] assert async_get_domain_config(hass).overrides == { @@ -235,7 +235,7 @@ async def test_module_log_level_override( msg = await websocket_client.receive_json() assert msg["id"] == 7 - assert msg["type"] == const.TYPE_RESULT + assert msg["type"] == TYPE_RESULT assert msg["success"] assert async_get_domain_config(hass).overrides == { @@ -254,7 +254,7 @@ async def test_module_log_level_override( msg = await websocket_client.receive_json() assert msg["id"] == 8 - assert msg["type"] == const.TYPE_RESULT + assert msg["type"] == TYPE_RESULT assert msg["success"] assert async_get_domain_config(hass).overrides == { diff --git a/tests/components/matter/test_climate.py b/tests/components/matter/test_climate.py index 2b3ae922fb2..2150c733700 100644 --- a/tests/components/matter/test_climate.py +++ b/tests/components/matter/test_climate.py @@ -7,8 +7,7 @@ from matter_server.client.models.node import MatterNode from matter_server.common.helpers.util import create_attribute_path_from_attribute import pytest -from homeassistant.components.climate import HVACAction, HVACMode -from homeassistant.components.climate.const import ClimateEntityFeature +from homeassistant.components.climate import ClimateEntityFeature, HVACAction, HVACMode from homeassistant.core import HomeAssistant from .common import ( diff --git a/tests/components/media_player/test_init.py b/tests/components/media_player/test_init.py index 436a9e3d05f..11898edfc36 100644 --- a/tests/components/media_player/test_init.py +++ b/tests/components/media_player/test_init.py @@ -13,7 +13,7 @@ from homeassistant.components.media_player import ( MediaPlayerEntity, MediaPlayerEntityFeature, ) -from homeassistant.components.websocket_api.const import TYPE_RESULT +from homeassistant.components.websocket_api import TYPE_RESULT from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component diff --git a/tests/components/modbus/test_climate.py b/tests/components/modbus/test_climate.py index 94778cdcbd2..a52285b22d7 100644 --- a/tests/components/modbus/test_climate.py +++ b/tests/components/modbus/test_climate.py @@ -2,14 +2,14 @@ import pytest -from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( ATTR_FAN_MODE, ATTR_FAN_MODES, ATTR_HVAC_MODE, ATTR_HVAC_MODES, ATTR_SWING_MODE, ATTR_SWING_MODES, + DOMAIN as CLIMATE_DOMAIN, FAN_AUTO, FAN_DIFFUSE, FAN_FOCUS, diff --git a/tests/components/nest/test_camera.py b/tests/components/nest/test_camera.py index d005355410f..8db86f5d8c1 100644 --- a/tests/components/nest/test_camera.py +++ b/tests/components/nest/test_camera.py @@ -16,7 +16,7 @@ import pytest from homeassistant.components import camera from homeassistant.components.camera import STATE_IDLE, STATE_STREAMING, StreamType from homeassistant.components.nest.const import DOMAIN -from homeassistant.components.websocket_api.const import TYPE_RESULT +from homeassistant.components.websocket_api import TYPE_RESULT from homeassistant.const import ATTR_FRIENDLY_NAME from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er diff --git a/tests/components/persistent_notification/test_init.py b/tests/components/persistent_notification/test_init.py index 3e99e268231..956183d8420 100644 --- a/tests/components/persistent_notification/test_init.py +++ b/tests/components/persistent_notification/test_init.py @@ -1,7 +1,7 @@ """The tests for the persistent notification component.""" import homeassistant.components.persistent_notification as pn -from homeassistant.components.websocket_api.const import TYPE_RESULT +from homeassistant.components.websocket_api import TYPE_RESULT from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component diff --git a/tests/components/ping/conftest.py b/tests/components/ping/conftest.py index 9bbbc9e6e32..fced110f1c5 100644 --- a/tests/components/ping/conftest.py +++ b/tests/components/ping/conftest.py @@ -5,9 +5,8 @@ from unittest.mock import patch from icmplib import Host import pytest -from homeassistant.components.device_tracker.const import CONF_CONSIDER_HOME -from homeassistant.components.ping import DOMAIN -from homeassistant.components.ping.const import CONF_PING_COUNT +from homeassistant.components.device_tracker import CONF_CONSIDER_HOME +from homeassistant.components.ping import CONF_PING_COUNT, DOMAIN from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant diff --git a/tests/components/plex/test_browse_media.py b/tests/components/plex/test_browse_media.py index 11eb73ad608..470caead14c 100644 --- a/tests/components/plex/test_browse_media.py +++ b/tests/components/plex/test_browse_media.py @@ -11,7 +11,7 @@ from homeassistant.components.media_player import ( ATTR_MEDIA_CONTENT_TYPE, ) from homeassistant.components.plex.const import CONF_SERVER_IDENTIFIER, PLEX_URI_SCHEME -from homeassistant.components.websocket_api.const import ERR_UNKNOWN_ERROR, TYPE_RESULT +from homeassistant.components.websocket_api import ERR_UNKNOWN_ERROR, TYPE_RESULT from homeassistant.core import HomeAssistant from .const import DEFAULT_DATA diff --git a/tests/components/plugwise/test_climate.py b/tests/components/plugwise/test_climate.py index 8041d2778ef..5cdc468a957 100644 --- a/tests/components/plugwise/test_climate.py +++ b/tests/components/plugwise/test_climate.py @@ -6,7 +6,7 @@ from unittest.mock import MagicMock, patch from plugwise.exceptions import PlugwiseError import pytest -from homeassistant.components.climate.const import HVACMode +from homeassistant.components.climate import HVACMode from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.util.dt import utcnow diff --git a/tests/components/roku/test_media_player.py b/tests/components/roku/test_media_player.py index c749419b24a..9aff8f581d7 100644 --- a/tests/components/roku/test_media_player.py +++ b/tests/components/roku/test_media_player.py @@ -36,7 +36,7 @@ from homeassistant.components.roku.const import ( SERVICE_SEARCH, ) from homeassistant.components.stream import FORMAT_CONTENT_TYPE, HLS_PROVIDER -from homeassistant.components.websocket_api.const import TYPE_RESULT +from homeassistant.components.websocket_api import TYPE_RESULT from homeassistant.config import async_process_ha_core_config from homeassistant.const import ( ATTR_ENTITY_ID, diff --git a/tests/components/rtsp_to_webrtc/test_init.py b/tests/components/rtsp_to_webrtc/test_init.py index 27656dd10c7..3071c3d9d08 100644 --- a/tests/components/rtsp_to_webrtc/test_init.py +++ b/tests/components/rtsp_to_webrtc/test_init.py @@ -11,7 +11,7 @@ import pytest import rtsp_to_webrtc from homeassistant.components.rtsp_to_webrtc import CONF_STUN_SERVER, DOMAIN -from homeassistant.components.websocket_api.const import TYPE_RESULT +from homeassistant.components.websocket_api import TYPE_RESULT from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component diff --git a/tests/components/shopping_list/test_init.py b/tests/components/shopping_list/test_init.py index c28ea66a32b..4e758764e3d 100644 --- a/tests/components/shopping_list/test_init.py +++ b/tests/components/shopping_list/test_init.py @@ -15,7 +15,7 @@ from homeassistant.components.shopping_list.const import ( SERVICE_REMOVE_ITEM, SERVICE_SORT, ) -from homeassistant.components.websocket_api.const import ( +from homeassistant.components.websocket_api import ( ERR_INVALID_FORMAT, ERR_NOT_FOUND, TYPE_RESULT, diff --git a/tests/components/smartthings/test_climate.py b/tests/components/smartthings/test_climate.py index b5fcc9f7647..c97f18e97d9 100644 --- a/tests/components/smartthings/test_climate.py +++ b/tests/components/smartthings/test_climate.py @@ -17,6 +17,7 @@ from homeassistant.components.climate import ( ATTR_HVAC_MODE, ATTR_HVAC_MODES, ATTR_PRESET_MODE, + ATTR_SWING_MODE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, DOMAIN as CLIMATE_DOMAIN, @@ -29,7 +30,6 @@ from homeassistant.components.climate import ( HVACAction, HVACMode, ) -from homeassistant.components.climate.const import ATTR_SWING_MODE from homeassistant.components.smartthings import climate from homeassistant.components.smartthings.const import DOMAIN from homeassistant.const import ( diff --git a/tests/components/smhi/test_weather.py b/tests/components/smhi/test_weather.py index 0794148915c..6c15ec53236 100644 --- a/tests/components/smhi/test_weather.py +++ b/tests/components/smhi/test_weather.py @@ -13,21 +13,19 @@ from homeassistant.components.smhi.weather import CONDITION_CLASSES, RETRY_TIMEO from homeassistant.components.weather import ( ATTR_CONDITION_CLEAR_NIGHT, ATTR_FORECAST_CONDITION, + ATTR_WEATHER_CLOUD_COVERAGE, ATTR_WEATHER_HUMIDITY, ATTR_WEATHER_PRESSURE, ATTR_WEATHER_TEMPERATURE, ATTR_WEATHER_VISIBILITY, ATTR_WEATHER_WIND_BEARING, + ATTR_WEATHER_WIND_GUST_SPEED, ATTR_WEATHER_WIND_SPEED, ATTR_WEATHER_WIND_SPEED_UNIT, DOMAIN as WEATHER_DOMAIN, LEGACY_SERVICE_GET_FORECAST, SERVICE_GET_FORECASTS, ) -from homeassistant.components.weather.const import ( - ATTR_WEATHER_CLOUD_COVERAGE, - ATTR_WEATHER_WIND_GUST_SPEED, -) from homeassistant.const import ATTR_ATTRIBUTION, STATE_UNKNOWN, UnitOfSpeed from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er diff --git a/tests/components/sonos/test_media_browser.py b/tests/components/sonos/test_media_browser.py index 4f6c2f53d8b..6e03935f7f6 100644 --- a/tests/components/sonos/test_media_browser.py +++ b/tests/components/sonos/test_media_browser.py @@ -4,8 +4,7 @@ from functools import partial from syrupy import SnapshotAssertion -from homeassistant.components.media_player.browse_media import BrowseMedia -from homeassistant.components.media_player.const import MediaClass, MediaType +from homeassistant.components.media_player import BrowseMedia, MediaClass, MediaType from homeassistant.components.sonos.media_browser import ( build_item_response, get_thumbnail_url_full, diff --git a/tests/components/sonos/test_media_player.py b/tests/components/sonos/test_media_player.py index 9fb8444a696..2be9aa5f823 100644 --- a/tests/components/sonos/test_media_player.py +++ b/tests/components/sonos/test_media_player.py @@ -6,13 +6,11 @@ from typing import Any import pytest from homeassistant.components.media_player import ( + ATTR_MEDIA_ENQUEUE, DOMAIN as MP_DOMAIN, SERVICE_PLAY_MEDIA, - MediaPlayerEnqueue, -) -from homeassistant.components.media_player.const import ( - ATTR_MEDIA_ENQUEUE, SERVICE_SELECT_SOURCE, + MediaPlayerEnqueue, ) from homeassistant.components.sonos.const import SOURCE_LINEIN, SOURCE_TV from homeassistant.components.sonos.media_player import LONG_SERVICE_TIMEOUT diff --git a/tests/components/sql/test_config_flow.py b/tests/components/sql/test_config_flow.py index 93cde0bccdd..cb990e454b7 100644 --- a/tests/components/sql/test_config_flow.py +++ b/tests/components/sql/test_config_flow.py @@ -8,7 +8,7 @@ from sqlalchemy.exc import SQLAlchemyError from homeassistant import config_entries from homeassistant.components.recorder import Recorder -from homeassistant.components.sensor.const import SensorDeviceClass, SensorStateClass +from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass from homeassistant.components.sql.const import DOMAIN from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType diff --git a/tests/components/trafikverket_train/__init__.py b/tests/components/trafikverket_train/__init__.py index 632f082c73b..f5e60eae535 100644 --- a/tests/components/trafikverket_train/__init__.py +++ b/tests/components/trafikverket_train/__init__.py @@ -2,12 +2,12 @@ from __future__ import annotations -from homeassistant.components.trafikverket_ferry.const import ( +from homeassistant.components.trafikverket_train.const import ( + CONF_FILTER_PRODUCT, CONF_FROM, CONF_TIME, CONF_TO, ) -from homeassistant.components.trafikverket_train.const import CONF_FILTER_PRODUCT from homeassistant.const import CONF_API_KEY, CONF_NAME, CONF_WEEKDAY, WEEKDAYS ENTRY_CONFIG = { diff --git a/tests/components/vallox/test_date.py b/tests/components/vallox/test_date.py index 1572e9b205c..bd4e1487bd5 100644 --- a/tests/components/vallox/test_date.py +++ b/tests/components/vallox/test_date.py @@ -4,7 +4,7 @@ from datetime import date from vallox_websocket_api import MetricData -from homeassistant.components.date.const import DOMAIN as DATE_DOMAIN, SERVICE_SET_VALUE +from homeassistant.components.date import DOMAIN as DATE_DOMAIN, SERVICE_SET_VALUE from homeassistant.const import ATTR_DATE, ATTR_ENTITY_ID from homeassistant.core import HomeAssistant diff --git a/tests/components/vallox/test_number.py b/tests/components/vallox/test_number.py index 2e440c5e304..1f8b05f21d8 100644 --- a/tests/components/vallox/test_number.py +++ b/tests/components/vallox/test_number.py @@ -2,7 +2,7 @@ import pytest -from homeassistant.components.number.const import ( +from homeassistant.components.number import ( ATTR_VALUE, DOMAIN as NUMBER_DOMAIN, SERVICE_SET_VALUE, diff --git a/tests/components/vallox/test_switch.py b/tests/components/vallox/test_switch.py index 294d4b00385..61290ea89ce 100644 --- a/tests/components/vallox/test_switch.py +++ b/tests/components/vallox/test_switch.py @@ -2,7 +2,7 @@ import pytest -from homeassistant.components.switch.const import DOMAIN as SWITCH_DOMAIN +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON from homeassistant.core import HomeAssistant diff --git a/tests/components/weatherkit/test_weather.py b/tests/components/weatherkit/test_weather.py index 3b3a9a50d7f..be949efffb8 100644 --- a/tests/components/weatherkit/test_weather.py +++ b/tests/components/weatherkit/test_weather.py @@ -18,8 +18,8 @@ from homeassistant.components.weather import ( DOMAIN as WEATHER_DOMAIN, LEGACY_SERVICE_GET_FORECAST, SERVICE_GET_FORECASTS, + WeatherEntityFeature, ) -from homeassistant.components.weather.const import WeatherEntityFeature from homeassistant.components.weatherkit.const import ATTRIBUTION from homeassistant.const import ATTR_ATTRIBUTION, ATTR_SUPPORTED_FEATURES from homeassistant.core import HomeAssistant diff --git a/tests/components/xiaomi_ble/test_device_trigger.py b/tests/components/xiaomi_ble/test_device_trigger.py index f1414146f22..7b4624d1025 100644 --- a/tests/components/xiaomi_ble/test_device_trigger.py +++ b/tests/components/xiaomi_ble/test_device_trigger.py @@ -3,7 +3,7 @@ import pytest from homeassistant.components import automation -from homeassistant.components.bluetooth.const import DOMAIN as BLUETOOTH_DOMAIN +from homeassistant.components.bluetooth import DOMAIN as BLUETOOTH_DOMAIN from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.xiaomi_ble.const import CONF_SUBTYPE, DOMAIN from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_PLATFORM, CONF_TYPE diff --git a/tests/components/yale_smart_alarm/test_button.py b/tests/components/yale_smart_alarm/test_button.py index e6fed9d94ae..ad6074345d3 100644 --- a/tests/components/yale_smart_alarm/test_button.py +++ b/tests/components/yale_smart_alarm/test_button.py @@ -9,8 +9,7 @@ import pytest from syrupy.assertion import SnapshotAssertion from yalesmartalarmclient.exceptions import UnknownError -from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN -from homeassistant.components.button.const import SERVICE_PRESS +from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS from homeassistant.const import ATTR_ENTITY_ID, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError diff --git a/tests/components/zha/test_websocket_api.py b/tests/components/zha/test_websocket_api.py index 85d849958a4..80b9f6accd0 100644 --- a/tests/components/zha/test_websocket_api.py +++ b/tests/components/zha/test_websocket_api.py @@ -19,7 +19,11 @@ from zigpy.zcl.clusters import general, security from zigpy.zcl.clusters.general import Groups import zigpy.zdo.types as zdo_types -from homeassistant.components.websocket_api import const +from homeassistant.components.websocket_api import ( + ERR_INVALID_FORMAT, + ERR_NOT_FOUND, + TYPE_RESULT, +) from homeassistant.components.zha import DOMAIN from homeassistant.components.zha.core.const import ( ATTR_CLUSTER_ID, @@ -336,9 +340,9 @@ async def test_device_not_found(zha_client) -> None: ) msg = await zha_client.receive_json() assert msg["id"] == 6 - assert msg["type"] == const.TYPE_RESULT + assert msg["type"] == TYPE_RESULT assert not msg["success"] - assert msg["error"]["code"] == const.ERR_NOT_FOUND + assert msg["error"]["code"] == ERR_NOT_FOUND async def test_list_groups(zha_client) -> None: @@ -347,7 +351,7 @@ async def test_list_groups(zha_client) -> None: msg = await zha_client.receive_json() assert msg["id"] == 7 - assert msg["type"] == const.TYPE_RESULT + assert msg["type"] == TYPE_RESULT groups = msg["result"] assert len(groups) == 1 @@ -364,7 +368,7 @@ async def test_get_group(zha_client) -> None: msg = await zha_client.receive_json() assert msg["id"] == 8 - assert msg["type"] == const.TYPE_RESULT + assert msg["type"] == TYPE_RESULT group = msg["result"] assert group is not None @@ -380,9 +384,9 @@ async def test_get_group_not_found(zha_client) -> None: msg = await zha_client.receive_json() assert msg["id"] == 9 - assert msg["type"] == const.TYPE_RESULT + assert msg["type"] == TYPE_RESULT assert not msg["success"] - assert msg["error"]["code"] == const.ERR_NOT_FOUND + assert msg["error"]["code"] == ERR_NOT_FOUND async def test_list_groupable_devices( @@ -397,7 +401,7 @@ async def test_list_groupable_devices( msg = await zha_client.receive_json() assert msg["id"] == 10 - assert msg["type"] == const.TYPE_RESULT + assert msg["type"] == TYPE_RESULT device_endpoints = msg["result"] assert len(device_endpoints) == 1 @@ -427,7 +431,7 @@ async def test_list_groupable_devices( msg = await zha_client.receive_json() assert msg["id"] == 11 - assert msg["type"] == const.TYPE_RESULT + assert msg["type"] == TYPE_RESULT device_endpoints = msg["result"] assert len(device_endpoints) == 0 @@ -439,7 +443,7 @@ async def test_add_group(zha_client) -> None: msg = await zha_client.receive_json() assert msg["id"] == 12 - assert msg["type"] == const.TYPE_RESULT + assert msg["type"] == TYPE_RESULT added_group = msg["result"] @@ -450,7 +454,7 @@ async def test_add_group(zha_client) -> None: msg = await zha_client.receive_json() assert msg["id"] == 13 - assert msg["type"] == const.TYPE_RESULT + assert msg["type"] == TYPE_RESULT groups = msg["result"] assert len(groups) == 2 @@ -466,7 +470,7 @@ async def test_remove_group(zha_client) -> None: msg = await zha_client.receive_json() assert msg["id"] == 14 - assert msg["type"] == const.TYPE_RESULT + assert msg["type"] == TYPE_RESULT groups = msg["result"] assert len(groups) == 1 @@ -477,7 +481,7 @@ async def test_remove_group(zha_client) -> None: msg = await zha_client.receive_json() assert msg["id"] == 15 - assert msg["type"] == const.TYPE_RESULT + assert msg["type"] == TYPE_RESULT groups_remaining = msg["result"] assert len(groups_remaining) == 0 @@ -486,7 +490,7 @@ async def test_remove_group(zha_client) -> None: msg = await zha_client.receive_json() assert msg["id"] == 16 - assert msg["type"] == const.TYPE_RESULT + assert msg["type"] == TYPE_RESULT groups = msg["result"] assert len(groups) == 0 @@ -710,14 +714,14 @@ async def test_ws_permit_with_qr_code( ) msg_type = None - while msg_type != const.TYPE_RESULT: + while msg_type != TYPE_RESULT: # There will be logging events coming over the websocket # as well so we want to ignore those msg = await zha_client.receive_json() msg_type = msg["type"] assert msg["id"] == 14 - assert msg["type"] == const.TYPE_RESULT + assert msg["type"] == TYPE_RESULT assert msg["success"] assert app_controller.permit.await_count == 0 @@ -739,7 +743,7 @@ async def test_ws_permit_with_install_code_fail( msg = await zha_client.receive_json() assert msg["id"] == 14 - assert msg["type"] == const.TYPE_RESULT + assert msg["type"] == TYPE_RESULT assert msg["success"] is False assert app_controller.permit.await_count == 0 @@ -773,14 +777,14 @@ async def test_ws_permit_ha12( ) msg_type = None - while msg_type != const.TYPE_RESULT: + while msg_type != TYPE_RESULT: # There will be logging events coming over the websocket # as well so we want to ignore those msg = await zha_client.receive_json() msg_type = msg["type"] assert msg["id"] == 14 - assert msg["type"] == const.TYPE_RESULT + assert msg["type"] == TYPE_RESULT assert msg["success"] assert app_controller.permit.await_count == 1 @@ -800,7 +804,7 @@ async def test_get_network_settings( msg = await zha_client.receive_json() assert msg["id"] == 6 - assert msg["type"] == const.TYPE_RESULT + assert msg["type"] == TYPE_RESULT assert msg["success"] assert "radio_type" in msg["result"] assert "network_info" in msg["result"]["settings"] @@ -818,7 +822,7 @@ async def test_list_network_backups( msg = await zha_client.receive_json() assert msg["id"] == 6 - assert msg["type"] == const.TYPE_RESULT + assert msg["type"] == TYPE_RESULT assert msg["success"] assert "network_info" in msg["result"][0] @@ -834,7 +838,7 @@ async def test_create_network_backup( assert len(app_controller.backups.backups) == 1 assert msg["id"] == 6 - assert msg["type"] == const.TYPE_RESULT + assert msg["type"] == TYPE_RESULT assert msg["success"] assert "backup" in msg["result"] and "is_complete" in msg["result"] @@ -860,7 +864,7 @@ async def test_restore_network_backup_success( assert "ezsp" not in backup.network_info.stack_specific assert msg["id"] == 6 - assert msg["type"] == const.TYPE_RESULT + assert msg["type"] == TYPE_RESULT assert msg["success"] @@ -892,7 +896,7 @@ async def test_restore_network_backup_force_write_eui64( ) assert msg["id"] == 6 - assert msg["type"] == const.TYPE_RESULT + assert msg["type"] == TYPE_RESULT assert msg["success"] @@ -915,9 +919,9 @@ async def test_restore_network_backup_failure( p.assert_called_once_with("a backup") assert msg["id"] == 6 - assert msg["type"] == const.TYPE_RESULT + assert msg["type"] == TYPE_RESULT assert not msg["success"] - assert msg["error"]["code"] == const.ERR_INVALID_FORMAT + assert msg["error"]["code"] == ERR_INVALID_FORMAT @pytest.mark.parametrize("new_channel", ["auto", 15]) @@ -940,7 +944,7 @@ async def test_websocket_change_channel( msg = await zha_client.receive_json() assert msg["id"] == 6 - assert msg["type"] == const.TYPE_RESULT + assert msg["type"] == TYPE_RESULT assert msg["success"] change_channel_mock.assert_has_calls([call(ANY, new_channel)]) @@ -973,7 +977,7 @@ async def test_websocket_bind_unbind_devices( msg = await zha_client.receive_json() assert msg["id"] == 27 - assert msg["type"] == const.TYPE_RESULT + assert msg["type"] == TYPE_RESULT assert msg["success"] assert binding_operation_mock.mock_calls == [ call( @@ -1027,7 +1031,7 @@ async def test_websocket_bind_unbind_group( msg = await zha_client.receive_json() assert msg["id"] == 27 - assert msg["type"] == const.TYPE_RESULT + assert msg["type"] == TYPE_RESULT assert msg["success"] if command_type == "bind": assert bind_mock.mock_calls == [call(test_group_id, ANY)] From c896458fcf75102c88ecc90ffa61a7b9f632336e Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 10 Jun 2024 15:55:08 +0200 Subject: [PATCH 0465/1445] Fix namespace-import pylint warning in components (#119299) --- .../components/bayesian/test_binary_sensor.py | 8 +- .../components/bthome/test_device_trigger.py | 43 +++---- tests/components/buienradar/test_camera.py | 4 +- tests/components/buienradar/test_sensor.py | 7 +- .../components/config/test_entity_registry.py | 7 +- tests/components/deconz/test_binary_sensor.py | 7 +- tests/components/deconz/test_services.py | 5 +- tests/components/diagnostics/test_init.py | 9 +- .../components/dlna_dmr/test_media_player.py | 107 ++++++++---------- tests/components/fritz/test_image.py | 4 +- tests/components/greeneye_monitor/conftest.py | 13 +-- .../greeneye_monitor/test_sensor.py | 12 +- tests/components/hassio/test_init.py | 15 +-- tests/components/hue/test_sensor_v1.py | 11 +- .../kostal_plenticore/test_number.py | 16 +-- tests/components/metoffice/test_sensor.py | 28 +++-- tests/components/metoffice/test_weather.py | 34 +++--- tests/components/octoprint/test_servics.py | 20 ++-- tests/components/plugwise/test_sensor.py | 10 +- .../samsungtv/test_device_trigger.py | 21 ++-- tests/components/smhi/test_init.py | 12 +- tests/components/template/test_button.py | 10 +- tests/components/template/test_image.py | 9 +- tests/components/template/test_number.py | 7 +- tests/components/template/test_select.py | 7 +- tests/components/tomorrowio/test_sensor.py | 4 +- tests/components/tomorrowio/test_weather.py | 3 +- tests/components/uvc/test_camera.py | 17 +-- .../xiaomi_ble/test_device_trigger.py | 78 +++++++------ tests/components/zha/test_diagnostics.py | 8 +- 30 files changed, 273 insertions(+), 263 deletions(-) diff --git a/tests/components/bayesian/test_binary_sensor.py b/tests/components/bayesian/test_binary_sensor.py index 8dedce0c297..aaade6da2f4 100644 --- a/tests/components/bayesian/test_binary_sensor.py +++ b/tests/components/bayesian/test_binary_sensor.py @@ -20,15 +20,16 @@ from homeassistant.const import ( STATE_UNKNOWN, ) from homeassistant.core import Context, HomeAssistant, callback -from homeassistant.helpers import issue_registry as ir -from homeassistant.helpers.entity_registry import async_get as async_get_entities +from homeassistant.helpers import entity_registry as er, issue_registry as ir from homeassistant.helpers.event import async_track_state_change_event from homeassistant.setup import async_setup_component from tests.common import get_fixture_path -async def test_load_values_when_added_to_hass(hass: HomeAssistant) -> None: +async def test_load_values_when_added_to_hass( + hass: HomeAssistant, entity_registry: er.EntityRegistry +) -> None: """Test that sensor initializes with observations of relevant entities.""" config = { @@ -57,7 +58,6 @@ async def test_load_values_when_added_to_hass(hass: HomeAssistant) -> None: assert await async_setup_component(hass, "binary_sensor", config) await hass.async_block_till_done() - entity_registry = async_get_entities(hass) assert ( entity_registry.entities["binary_sensor.test_binary"].unique_id == "bayesian-3b4c9563-5e84-4167-8fe7-8f507e796d72" diff --git a/tests/components/bthome/test_device_trigger.py b/tests/components/bthome/test_device_trigger.py index 496f191c434..f847ffb9c0a 100644 --- a/tests/components/bthome/test_device_trigger.py +++ b/tests/components/bthome/test_device_trigger.py @@ -6,10 +6,7 @@ from homeassistant.components.bthome.const import CONF_SUBTYPE, DOMAIN from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_PLATFORM, CONF_TYPE from homeassistant.core import HomeAssistant, ServiceCall, callback -from homeassistant.helpers.device_registry import ( - CONNECTION_NETWORK_MAC, - async_get as async_get_dev_reg, -) +from homeassistant.helpers import device_registry as dr from homeassistant.setup import async_setup_component from . import make_bthome_v2_adv @@ -87,7 +84,9 @@ async def test_event_rotate_dimmer(hass: HomeAssistant) -> None: await hass.async_block_till_done() -async def test_get_triggers_button(hass: HomeAssistant) -> None: +async def test_get_triggers_button( + hass: HomeAssistant, device_registry: dr.DeviceRegistry +) -> None: """Test that we get the expected triggers from a BTHome BLE sensor.""" mac = "A4:C1:38:8D:18:B2" entry = await _async_setup_bthome_device(hass, mac) @@ -103,8 +102,7 @@ async def test_get_triggers_button(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert len(events) == 1 - dev_reg = async_get_dev_reg(hass) - device = dev_reg.async_get_device(identifiers={get_device_id(mac)}) + device = device_registry.async_get_device(identifiers={get_device_id(mac)}) assert device expected_trigger = { CONF_PLATFORM: "device", @@ -123,7 +121,9 @@ async def test_get_triggers_button(hass: HomeAssistant) -> None: await hass.async_block_till_done() -async def test_get_triggers_dimmer(hass: HomeAssistant) -> None: +async def test_get_triggers_dimmer( + hass: HomeAssistant, device_registry: dr.DeviceRegistry +) -> None: """Test that we get the expected triggers from a BTHome BLE sensor.""" mac = "A4:C1:38:8D:18:B2" entry = await _async_setup_bthome_device(hass, mac) @@ -139,8 +139,7 @@ async def test_get_triggers_dimmer(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert len(events) == 1 - dev_reg = async_get_dev_reg(hass) - device = dev_reg.async_get_device(identifiers={get_device_id(mac)}) + device = device_registry.async_get_device(identifiers={get_device_id(mac)}) assert device expected_trigger = { CONF_PLATFORM: "device", @@ -159,7 +158,9 @@ async def test_get_triggers_dimmer(hass: HomeAssistant) -> None: await hass.async_block_till_done() -async def test_get_triggers_for_invalid_bthome_ble_device(hass: HomeAssistant) -> None: +async def test_get_triggers_for_invalid_bthome_ble_device( + hass: HomeAssistant, device_registry: dr.DeviceRegistry +) -> None: """Test that we don't get triggers for an invalid device.""" mac = "A4:C1:38:8D:18:B2" entry = await _async_setup_bthome_device(hass, mac) @@ -175,8 +176,7 @@ async def test_get_triggers_for_invalid_bthome_ble_device(hass: HomeAssistant) - await hass.async_block_till_done() assert len(events) == 0 - dev_reg = async_get_dev_reg(hass) - invalid_device = dev_reg.async_get_or_create( + invalid_device = device_registry.async_get_or_create( config_entry_id=entry.entry_id, identifiers={(DOMAIN, "invdevmac")}, ) @@ -190,7 +190,9 @@ async def test_get_triggers_for_invalid_bthome_ble_device(hass: HomeAssistant) - await hass.async_block_till_done() -async def test_get_triggers_for_invalid_device_id(hass: HomeAssistant) -> None: +async def test_get_triggers_for_invalid_device_id( + hass: HomeAssistant, device_registry: dr.DeviceRegistry +) -> None: """Test that we don't get triggers when using an invalid device_id.""" mac = "DE:70:E8:B2:39:0C" entry = await _async_setup_bthome_device(hass, mac) @@ -204,11 +206,9 @@ async def test_get_triggers_for_invalid_device_id(hass: HomeAssistant) -> None: # wait for the event await hass.async_block_till_done() - dev_reg = async_get_dev_reg(hass) - - invalid_device = dev_reg.async_get_or_create( + invalid_device = device_registry.async_get_or_create( config_entry_id=entry.entry_id, - connections={(CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, ) assert invalid_device triggers = await async_get_device_automations( @@ -221,7 +221,9 @@ async def test_get_triggers_for_invalid_device_id(hass: HomeAssistant) -> None: async def test_if_fires_on_motion_detected( - hass: HomeAssistant, service_calls: list[ServiceCall] + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + service_calls: list[ServiceCall], ) -> None: """Test for motion event trigger firing.""" mac = "DE:70:E8:B2:39:0C" @@ -236,8 +238,7 @@ async def test_if_fires_on_motion_detected( # # wait for the event await hass.async_block_till_done() - dev_reg = async_get_dev_reg(hass) - device = dev_reg.async_get_device(identifiers={get_device_id(mac)}) + device = device_registry.async_get_device(identifiers={get_device_id(mac)}) device_id = device.id assert await async_setup_component( diff --git a/tests/components/buienradar/test_camera.py b/tests/components/buienradar/test_camera.py index 799fa37c7e3..9ef986b094c 100644 --- a/tests/components/buienradar/test_camera.py +++ b/tests/components/buienradar/test_camera.py @@ -10,7 +10,7 @@ from aiohttp.client_exceptions import ClientResponseError from homeassistant.components.buienradar.const import CONF_DELTA, DOMAIN from homeassistant.const import CONF_COUNTRY_CODE, CONF_LATITUDE, CONF_LONGITUDE from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity_registry import async_get +from homeassistant.helpers import entity_registry as er from homeassistant.util import dt as dt_util from tests.common import MockConfigEntry @@ -32,7 +32,7 @@ def radar_map_url(country_code: str = "NL") -> str: async def _setup_config_entry(hass, entry): - entity_registry = async_get(hass) + entity_registry = er.async_get(hass) entity_registry.async_get_or_create( domain="camera", platform="buienradar", diff --git a/tests/components/buienradar/test_sensor.py b/tests/components/buienradar/test_sensor.py index ea5ef74f72e..09121a885c0 100644 --- a/tests/components/buienradar/test_sensor.py +++ b/tests/components/buienradar/test_sensor.py @@ -5,7 +5,7 @@ from http import HTTPStatus from homeassistant.components.buienradar.const import DOMAIN from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity_registry import async_get +from homeassistant.helpers import entity_registry as er from tests.common import MockConfigEntry from tests.test_util.aiohttp import AiohttpClientMocker @@ -18,7 +18,9 @@ TEST_CFG_DATA = {CONF_LATITUDE: TEST_LATITUDE, CONF_LONGITUDE: TEST_LONGITUDE} async def test_smoke_test_setup_component( - aioclient_mock: AiohttpClientMocker, hass: HomeAssistant + aioclient_mock: AiohttpClientMocker, + hass: HomeAssistant, + entity_registry: er.EntityRegistry, ) -> None: """Smoke test for successfully set-up with default config.""" aioclient_mock.get( @@ -28,7 +30,6 @@ async def test_smoke_test_setup_component( mock_entry.add_to_hass(hass) - entity_registry = async_get(hass) for cond in CONDITIONS: entity_registry.async_get_or_create( domain="sensor", diff --git a/tests/components/config/test_entity_registry.py b/tests/components/config/test_entity_registry.py index d61d9d7f892..813ec654abb 100644 --- a/tests/components/config/test_entity_registry.py +++ b/tests/components/config/test_entity_registry.py @@ -6,13 +6,12 @@ from pytest_unordered import unordered from homeassistant.components.config import entity_registry from homeassistant.const import ATTR_ICON, EntityCategory from homeassistant.core import HomeAssistant -from homeassistant.helpers import device_registry as dr +from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.device_registry import DeviceEntryDisabler from homeassistant.helpers.entity_registry import ( RegistryEntry, RegistryEntryDisabler, RegistryEntryHider, - async_get as async_get_entity_registry, ) from tests.common import ( @@ -863,6 +862,7 @@ async def test_enable_entity_disabled_device( hass: HomeAssistant, client: MockHAClientWebSocket, device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, ) -> None: """Test enabling entity of disabled device.""" entity_id = "test_domain.test_platform_1234" @@ -889,8 +889,7 @@ async def test_enable_entity_disabled_device( state = hass.states.get(entity_id) assert state is None - entity_reg = async_get_entity_registry(hass) - entity_entry = entity_reg.async_get(entity_id) + entity_entry = entity_registry.async_get(entity_id) assert entity_entry.config_entry_id == config_entry.entry_id assert entity_entry.device_id == device.id assert entity_entry.disabled_by == RegistryEntryDisabler.DEVICE diff --git a/tests/components/deconz/test_binary_sensor.py b/tests/components/deconz/test_binary_sensor.py index 9fd57926f44..6ab5f2f5477 100644 --- a/tests/components/deconz/test_binary_sensor.py +++ b/tests/components/deconz/test_binary_sensor.py @@ -21,7 +21,6 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er -from homeassistant.helpers.entity_registry import async_entries_for_config_entry from .test_gateway import ( DECONZ_WEB_REQUEST, @@ -687,7 +686,8 @@ async def test_add_new_binary_sensor_ignored_load_entities_on_service_call( assert not hass.states.get("binary_sensor.presence_sensor") assert ( - len(async_entries_for_config_entry(entity_registry, config_entry.entry_id)) == 0 + len(er.async_entries_for_config_entry(entity_registry, config_entry.entry_id)) + == 0 ) aioclient_mock.clear_requests() @@ -738,7 +738,8 @@ async def test_add_new_binary_sensor_ignored_load_entities_on_options_change( assert not hass.states.get("binary_sensor.presence_sensor") assert ( - len(async_entries_for_config_entry(entity_registry, config_entry.entry_id)) == 0 + len(er.async_entries_for_config_entry(entity_registry, config_entry.entry_id)) + == 0 ) aioclient_mock.clear_requests() diff --git a/tests/components/deconz/test_services.py b/tests/components/deconz/test_services.py index 9c5c21bc0ff..de061fc4e8c 100644 --- a/tests/components/deconz/test_services.py +++ b/tests/components/deconz/test_services.py @@ -22,7 +22,6 @@ from homeassistant.components.deconz.services import ( from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er -from homeassistant.helpers.entity_registry import async_entries_for_config_entry from .test_gateway import ( BRIDGEID, @@ -368,7 +367,7 @@ async def test_remove_orphaned_entries_service( ) assert ( - len(async_entries_for_config_entry(entity_registry, config_entry.entry_id)) + len(er.async_entries_for_config_entry(entity_registry, config_entry.entry_id)) == 3 # Light, switch battery and orphan ) @@ -391,6 +390,6 @@ async def test_remove_orphaned_entries_service( ) assert ( - len(async_entries_for_config_entry(entity_registry, config_entry.entry_id)) + len(er.async_entries_for_config_entry(entity_registry, config_entry.entry_id)) == 2 # Light and switch battery ) diff --git a/tests/components/diagnostics/test_init.py b/tests/components/diagnostics/test_init.py index 40a8f5ab744..eeb4f420225 100644 --- a/tests/components/diagnostics/test_init.py +++ b/tests/components/diagnostics/test_init.py @@ -7,7 +7,7 @@ import pytest from homeassistant.components.websocket_api import TYPE_RESULT from homeassistant.core import HomeAssistant -from homeassistant.helpers.device_registry import async_get +from homeassistant.helpers import device_registry as dr from homeassistant.helpers.system_info import async_get_system_info from homeassistant.loader import async_get_integration from homeassistant.setup import async_setup_component @@ -82,7 +82,9 @@ async def test_websocket( @pytest.mark.usefixtures("enable_custom_integrations") async def test_download_diagnostics( - hass: HomeAssistant, hass_client: ClientSessionGenerator + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + device_registry: dr.DeviceRegistry, ) -> None: """Test download diagnostics.""" config_entry = MockConfigEntry(domain="fake_integration") @@ -178,8 +180,7 @@ async def test_download_diagnostics( "data": {"config_entry": "info"}, } - dev_reg = async_get(hass) - device = dev_reg.async_get_or_create( + device = device_registry.async_get_or_create( config_entry_id=config_entry.entry_id, identifiers={("test", "test")} ) diff --git a/tests/components/dlna_dmr/test_media_player.py b/tests/components/dlna_dmr/test_media_player.py index ad67530e605..9a60ce244dc 100644 --- a/tests/components/dlna_dmr/test_media_player.py +++ b/tests/components/dlna_dmr/test_media_player.py @@ -45,16 +45,8 @@ from homeassistant.const import ( CONF_URL, ) from homeassistant.core import CoreState, HomeAssistant -from homeassistant.helpers.device_registry import ( - CONNECTION_NETWORK_MAC, - CONNECTION_UPNP, - async_get as async_get_dr, -) +from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.entity_component import async_update_entity -from homeassistant.helpers.entity_registry import ( - async_entries_for_config_entry, - async_get as async_get_er, -) from homeassistant.setup import async_setup_component from .conftest import ( @@ -80,7 +72,8 @@ async def setup_mock_component(hass: HomeAssistant, mock_entry: MockConfigEntry) assert await hass.config_entries.async_setup(mock_entry.entry_id) is True await hass.async_block_till_done() - entries = async_entries_for_config_entry(async_get_er(hass), mock_entry.entry_id) + entity_registry = er.async_get(hass) + entries = er.async_entries_for_config_entry(entity_registry, mock_entry.entry_id) assert len(entries) == 1 return entries[0].entity_id @@ -345,6 +338,7 @@ async def test_setup_entry_with_options( async def test_setup_entry_mac_address( hass: HomeAssistant, + device_registry: dr.DeviceRegistry, domain_data_mock: Mock, config_entry_mock: MockConfigEntry, ssdp_scanner_mock: Mock, @@ -356,17 +350,17 @@ async def test_setup_entry_mac_address( await async_update_entity(hass, mock_entity_id) await hass.async_block_till_done() # Check the device registry connections for MAC address - dev_reg = async_get_dr(hass) - device = dev_reg.async_get_device( - connections={(CONNECTION_UPNP, MOCK_DEVICE_UDN)}, + device = device_registry.async_get_device( + connections={(dr.CONNECTION_UPNP, MOCK_DEVICE_UDN)}, identifiers=set(), ) assert device is not None - assert (CONNECTION_NETWORK_MAC, MOCK_MAC_ADDRESS) in device.connections + assert (dr.CONNECTION_NETWORK_MAC, MOCK_MAC_ADDRESS) in device.connections async def test_setup_entry_no_mac_address( hass: HomeAssistant, + device_registry: dr.DeviceRegistry, domain_data_mock: Mock, config_entry_mock_no_mac: MockConfigEntry, ssdp_scanner_mock: Mock, @@ -378,13 +372,12 @@ async def test_setup_entry_no_mac_address( await async_update_entity(hass, mock_entity_id) await hass.async_block_till_done() # Check the device registry connections does not include the MAC address - dev_reg = async_get_dr(hass) - device = dev_reg.async_get_device( - connections={(CONNECTION_UPNP, MOCK_DEVICE_UDN)}, + device = device_registry.async_get_device( + connections={(dr.CONNECTION_UPNP, MOCK_DEVICE_UDN)}, identifiers=set(), ) assert device is not None - assert (CONNECTION_NETWORK_MAC, MOCK_MAC_ADDRESS) not in device.connections + assert (dr.CONNECTION_NETWORK_MAC, MOCK_MAC_ADDRESS) not in device.connections async def test_event_subscribe_failure( @@ -445,15 +438,17 @@ async def test_event_subscribe_rejected( async def test_available_device( - hass: HomeAssistant, dmr_device_mock: Mock, mock_entity_id: str + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + dmr_device_mock: Mock, + mock_entity_id: str, ) -> None: """Test a DlnaDmrEntity with a connected DmrDevice.""" # Check hass device information is filled in await async_update_entity(hass, mock_entity_id) await hass.async_block_till_done() - dev_reg = async_get_dr(hass) - device = dev_reg.async_get_device( - connections={(CONNECTION_UPNP, MOCK_DEVICE_UDN)}, + device = device_registry.async_get_device( + connections={(dr.CONNECTION_UPNP, MOCK_DEVICE_UDN)}, identifiers=set(), ) assert device is not None @@ -1260,6 +1255,7 @@ async def test_playback_update_state( ) async def test_unavailable_device( hass: HomeAssistant, + device_registry: dr.DeviceRegistry, domain_data_mock: Mock, ssdp_scanner_mock: Mock, config_entry_mock: MockConfigEntry, @@ -1357,9 +1353,8 @@ async def test_unavailable_device( ) # Check hass device information has not been filled in yet - dev_reg = async_get_dr(hass) - device = dev_reg.async_get_device( - connections={(CONNECTION_UPNP, MOCK_DEVICE_UDN)}, + device = device_registry.async_get_device( + connections={(dr.CONNECTION_UPNP, MOCK_DEVICE_UDN)}, identifiers=set(), ) assert device is not None @@ -1387,6 +1382,7 @@ async def test_unavailable_device( ) async def test_become_available( hass: HomeAssistant, + device_registry: dr.DeviceRegistry, domain_data_mock: Mock, ssdp_scanner_mock: Mock, config_entry_mock: MockConfigEntry, @@ -1404,9 +1400,8 @@ async def test_become_available( assert mock_state.state == ha_const.STATE_UNAVAILABLE # Check hass device information has not been filled in yet - dev_reg = async_get_dr(hass) - device = dev_reg.async_get_device( - connections={(CONNECTION_UPNP, MOCK_DEVICE_UDN)}, + device = device_registry.async_get_device( + connections={(dr.CONNECTION_UPNP, MOCK_DEVICE_UDN)}, identifiers=set(), ) assert device is not None @@ -1446,9 +1441,8 @@ async def test_become_available( assert mock_state is not None assert mock_state.state == MediaPlayerState.IDLE # Check hass device information is now filled in - dev_reg = async_get_dr(hass) - device = dev_reg.async_get_device( - connections={(CONNECTION_UPNP, MOCK_DEVICE_UDN)}, + device = device_registry.async_get_device( + connections={(dr.CONNECTION_UPNP, MOCK_DEVICE_UDN)}, identifiers=set(), ) assert device is not None @@ -2281,6 +2275,7 @@ async def test_config_update_poll_availability( async def test_config_update_mac_address( hass: HomeAssistant, + device_registry: dr.DeviceRegistry, domain_data_mock: Mock, config_entry_mock_no_mac: MockConfigEntry, ssdp_scanner_mock: Mock, @@ -2293,13 +2288,12 @@ async def test_config_update_mac_address( domain_data_mock.upnp_factory.async_create_device.reset_mock() # Check the device registry connections does not include the MAC address - dev_reg = async_get_dr(hass) - device = dev_reg.async_get_device( - connections={(CONNECTION_UPNP, MOCK_DEVICE_UDN)}, + device = device_registry.async_get_device( + connections={(dr.CONNECTION_UPNP, MOCK_DEVICE_UDN)}, identifiers=set(), ) assert device is not None - assert (CONNECTION_NETWORK_MAC, MOCK_MAC_ADDRESS) not in device.connections + assert (dr.CONNECTION_NETWORK_MAC, MOCK_MAC_ADDRESS) not in device.connections # MAC address discovered and set by config flow hass.config_entries.async_update_entry( @@ -2314,12 +2308,12 @@ async def test_config_update_mac_address( await hass.async_block_till_done() # Device registry connections should now include the MAC address - device = dev_reg.async_get_device( - connections={(CONNECTION_UPNP, MOCK_DEVICE_UDN)}, + device = device_registry.async_get_device( + connections={(dr.CONNECTION_UPNP, MOCK_DEVICE_UDN)}, identifiers=set(), ) assert device is not None - assert (CONNECTION_NETWORK_MAC, MOCK_MAC_ADDRESS) in device.connections + assert (dr.CONNECTION_NETWORK_MAC, MOCK_MAC_ADDRESS) in device.connections @pytest.mark.parametrize( @@ -2328,6 +2322,8 @@ async def test_config_update_mac_address( ) async def test_connections_restored( hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, domain_data_mock: Mock, ssdp_scanner_mock: Mock, config_entry_mock: MockConfigEntry, @@ -2345,9 +2341,8 @@ async def test_connections_restored( assert mock_state.state == ha_const.STATE_UNAVAILABLE # Check hass device information has not been filled in yet - dev_reg = async_get_dr(hass) - device = dev_reg.async_get_device( - connections={(CONNECTION_UPNP, MOCK_DEVICE_UDN)}, + device = device_registry.async_get_device( + connections={(dr.CONNECTION_UPNP, MOCK_DEVICE_UDN)}, identifiers=set(), ) assert device is not None @@ -2387,9 +2382,8 @@ async def test_connections_restored( assert mock_state is not None assert mock_state.state == MediaPlayerState.IDLE # Check hass device information is now filled in - dev_reg = async_get_dr(hass) - device = dev_reg.async_get_device( - connections={(CONNECTION_UPNP, MOCK_DEVICE_UDN)}, + device = device_registry.async_get_device( + connections={(dr.CONNECTION_UPNP, MOCK_DEVICE_UDN)}, identifiers=set(), ) assert device is not None @@ -2410,17 +2404,15 @@ async def test_connections_restored( dmr_device_mock.async_unsubscribe_services.assert_awaited_once() # Check hass device information has not been filled in yet - dev_reg = async_get_dr(hass) - device = dev_reg.async_get_device( - connections={(CONNECTION_UPNP, MOCK_DEVICE_UDN)}, + device = device_registry.async_get_device( + connections={(dr.CONNECTION_UPNP, MOCK_DEVICE_UDN)}, identifiers=set(), ) assert device is not None assert device.connections == previous_connections # Verify the entity remains linked to the device - ent_reg = async_get_er(hass) - entry = ent_reg.async_get(mock_entity_id) + entry = entity_registry.async_get(mock_entity_id) assert entry is not None assert entry.device_id == device.id @@ -2435,6 +2427,8 @@ async def test_connections_restored( async def test_udn_upnp_connection_added_if_missing( hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, domain_data_mock: Mock, ssdp_scanner_mock: Mock, config_entry_mock: MockConfigEntry, @@ -2449,8 +2443,7 @@ async def test_udn_upnp_connection_added_if_missing( config_entry_mock.add_to_hass(hass) # Cause connection attempts to fail before adding entity - ent_reg = async_get_er(hass) - entry = ent_reg.async_get_or_create( + entry = entity_registry.async_get_or_create( mp.DOMAIN, DOMAIN, MOCK_DEVICE_UDN, @@ -2458,14 +2451,13 @@ async def test_udn_upnp_connection_added_if_missing( ) mock_entity_id = entry.entity_id - dev_reg = async_get_dr(hass) - device = dev_reg.async_get_or_create( + device = device_registry.async_get_or_create( config_entry_id=config_entry_mock.entry_id, - connections={(CONNECTION_NETWORK_MAC, MOCK_MAC_ADDRESS)}, + connections={(dr.CONNECTION_NETWORK_MAC, MOCK_MAC_ADDRESS)}, identifiers=set(), ) - ent_reg.async_update_entity(mock_entity_id, device_id=device.id) + entity_registry.async_update_entity(mock_entity_id, device_id=device.id) domain_data_mock.upnp_factory.async_create_device.side_effect = UpnpConnectionError assert await hass.config_entries.async_setup(config_entry_mock.entry_id) is True @@ -2476,7 +2468,6 @@ async def test_udn_upnp_connection_added_if_missing( assert mock_state.state == ha_const.STATE_UNAVAILABLE # Check hass device information has not been filled in yet - dev_reg = async_get_dr(hass) - device = dev_reg.async_get(device.id) + device = device_registry.async_get(device.id) assert device is not None - assert (CONNECTION_UPNP, MOCK_DEVICE_UDN) in device.connections + assert (dr.CONNECTION_UPNP, MOCK_DEVICE_UDN) in device.connections diff --git a/tests/components/fritz/test_image.py b/tests/components/fritz/test_image.py index a22ab76fdb6..9097aab1762 100644 --- a/tests/components/fritz/test_image.py +++ b/tests/components/fritz/test_image.py @@ -13,7 +13,7 @@ from homeassistant.components.image import DOMAIN as IMAGE_DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.const import STATE_UNKNOWN, Platform from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity_registry import async_get as async_get_entity_registry +from homeassistant.helpers import entity_registry as er from homeassistant.util.dt import utcnow from .const import MOCK_FB_SERVICES, MOCK_USER_DATA @@ -89,6 +89,7 @@ GUEST_WIFI_DISABLED: dict[str, dict] = { async def test_image_entity( hass: HomeAssistant, hass_client: ClientSessionGenerator, + entity_registry: er.EntityRegistry, snapshot: SnapshotAssertion, fc_class_mock, fh_class_mock, @@ -122,7 +123,6 @@ async def test_image_entity( "friendly_name": "Mock Title GuestWifi", } - entity_registry = async_get_entity_registry(hass) entity_entry = entity_registry.async_get("image.mock_title_guestwifi") assert entity_entry.unique_id == "1c_ed_6f_12_34_11_guestwifi_qr_code" diff --git a/tests/components/greeneye_monitor/conftest.py b/tests/components/greeneye_monitor/conftest.py index add823237c5..975a0119313 100644 --- a/tests/components/greeneye_monitor/conftest.py +++ b/tests/components/greeneye_monitor/conftest.py @@ -10,10 +10,7 @@ from homeassistant.components.greeneye_monitor import DOMAIN from homeassistant.components.sensor import SensorDeviceClass from homeassistant.const import UnitOfElectricPotential, UnitOfPower from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity_registry import ( - RegistryEntry, - async_get as get_entity_registry, -) +from homeassistant.helpers import entity_registry as er from .common import add_listeners @@ -82,15 +79,15 @@ def assert_sensor_registered( sensor_type: str, number: int, name: str, -) -> RegistryEntry: +) -> er.RegistryEntry: """Assert that a sensor entity of a given type was registered properly.""" - registry = get_entity_registry(hass) + entity_registry = er.async_get(hass) unique_id = f"{serial_number}-{sensor_type}-{number}" - entity_id = registry.async_get_entity_id("sensor", DOMAIN, unique_id) + entity_id = entity_registry.async_get_entity_id("sensor", DOMAIN, unique_id) assert entity_id is not None - sensor = registry.async_get(entity_id) + sensor = entity_registry.async_get(entity_id) assert sensor assert sensor.unique_id == unique_id assert sensor.original_name == name diff --git a/tests/components/greeneye_monitor/test_sensor.py b/tests/components/greeneye_monitor/test_sensor.py index 35d515a4877..cd4243f4f6d 100644 --- a/tests/components/greeneye_monitor/test_sensor.py +++ b/tests/components/greeneye_monitor/test_sensor.py @@ -8,10 +8,7 @@ from homeassistant.components.greeneye_monitor.sensor import ( ) from homeassistant.const import STATE_UNKNOWN from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity_registry import ( - RegistryEntryDisabler, - async_get as get_entity_registry, -) +from homeassistant.helpers import entity_registry as er from .common import ( MULTI_MONITOR_CONFIG, @@ -27,7 +24,7 @@ from .conftest import assert_sensor_state async def test_sensor_does_not_exist_before_monitor_connected( - hass: HomeAssistant, monitors: AsyncMock + hass: HomeAssistant, entity_registry: er.EntityRegistry, monitors: AsyncMock ) -> None: """Test that a sensor does not exist before its monitor is connected.""" # The sensor base class handles connecting the monitor, so we test this with a single voltage sensor for ease @@ -35,7 +32,6 @@ async def test_sensor_does_not_exist_before_monitor_connected( hass, SINGLE_MONITOR_CONFIG_VOLTAGE_SENSORS ) - entity_registry = get_entity_registry(hass) assert entity_registry.async_get("sensor.voltage_1") is None @@ -204,8 +200,8 @@ async def test_multi_monitor_sensors(hass: HomeAssistant, monitors: AsyncMock) - async def disable_entity(hass: HomeAssistant, entity_id: str) -> None: """Disable the given entity.""" - entity_registry = get_entity_registry(hass) + entity_registry = er.async_get(hass) entity_registry.async_update_entity( - entity_id, disabled_by=RegistryEntryDisabler.USER + entity_id, disabled_by=er.RegistryEntryDisabler.USER ) await hass.async_block_till_done() diff --git a/tests/components/hassio/test_init.py b/tests/components/hassio/test_init.py index 2971bdbb675..0246b557ee4 100644 --- a/tests/components/hassio/test_init.py +++ b/tests/components/hassio/test_init.py @@ -24,7 +24,7 @@ from homeassistant.components.hassio.const import REQUEST_REFRESH_DELAY from homeassistant.components.hassio.handler import HassioAPIError from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.core import HomeAssistant -from homeassistant.helpers.device_registry import async_get +from homeassistant.helpers import device_registry as dr from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util @@ -773,9 +773,10 @@ async def test_migration_off_hassio(hass: HomeAssistant) -> None: assert hass.config_entries.async_entries(DOMAIN) == [] -async def test_device_registry_calls(hass: HomeAssistant) -> None: +async def test_device_registry_calls( + hass: HomeAssistant, device_registry: dr.DeviceRegistry +) -> None: """Test device registry entries for hassio.""" - dev_reg = async_get(hass) supervisor_mock_data = { "version": "1.0.0", "version_latest": "1.0.0", @@ -829,7 +830,7 @@ async def test_device_registry_calls(hass: HomeAssistant) -> None: config_entry.add_to_hass(hass) assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done(wait_background_tasks=True) - assert len(dev_reg.devices) == 6 + assert len(device_registry.devices) == 6 supervisor_mock_data = { "version": "1.0.0", @@ -863,11 +864,11 @@ async def test_device_registry_calls(hass: HomeAssistant) -> None: ): async_fire_time_changed(hass, dt_util.now() + timedelta(hours=1)) await hass.async_block_till_done(wait_background_tasks=True) - assert len(dev_reg.devices) == 5 + assert len(device_registry.devices) == 5 async_fire_time_changed(hass, dt_util.now() + timedelta(hours=2)) await hass.async_block_till_done(wait_background_tasks=True) - assert len(dev_reg.devices) == 5 + assert len(device_registry.devices) == 5 supervisor_mock_data = { "version": "1.0.0", @@ -921,7 +922,7 @@ async def test_device_registry_calls(hass: HomeAssistant) -> None: ): async_fire_time_changed(hass, dt_util.now() + timedelta(hours=3)) await hass.async_block_till_done() - assert len(dev_reg.devices) == 5 + assert len(device_registry.devices) == 5 async def test_coordinator_updates( diff --git a/tests/components/hue/test_sensor_v1.py b/tests/components/hue/test_sensor_v1.py index 6e620ded365..b1ef94f8ed0 100644 --- a/tests/components/hue/test_sensor_v1.py +++ b/tests/components/hue/test_sensor_v1.py @@ -10,7 +10,7 @@ from homeassistant.components.hue.const import ATTR_HUE_EVENT from homeassistant.components.hue.v1 import sensor_base from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity_registry import async_get +from homeassistant.helpers import entity_registry as er from .conftest import create_mock_bridge, setup_platform @@ -314,7 +314,9 @@ async def test_sensors_with_multiple_bridges( assert len(hass.states.async_all()) == 10 -async def test_sensors(hass: HomeAssistant, mock_bridge_v1) -> None: +async def test_sensors( + hass: HomeAssistant, entity_registry: er.EntityRegistry, mock_bridge_v1 +) -> None: """Test the update_items function with some sensors.""" mock_bridge_v1.mock_sensor_responses.append(SENSOR_RESPONSE) await setup_platform(hass, mock_bridge_v1, ["binary_sensor", "sensor"]) @@ -351,9 +353,10 @@ async def test_sensors(hass: HomeAssistant, mock_bridge_v1) -> None: assert battery_remote_1.state == "100" assert battery_remote_1.name == "Hue dimmer switch 1 battery level" - ent_reg = async_get(hass) assert ( - ent_reg.async_get("sensor.hue_dimmer_switch_1_battery_level").entity_category + entity_registry.async_get( + "sensor.hue_dimmer_switch_1_battery_level" + ).entity_category == EntityCategory.DIAGNOSTIC ) diff --git a/tests/components/kostal_plenticore/test_number.py b/tests/components/kostal_plenticore/test_number.py index bb401898de5..9d94c6f9951 100644 --- a/tests/components/kostal_plenticore/test_number.py +++ b/tests/components/kostal_plenticore/test_number.py @@ -16,7 +16,7 @@ from homeassistant.components.number import ( ) from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity_registry import async_get +from homeassistant.helpers import entity_registry as er from homeassistant.util import dt as dt_util from tests.common import MockConfigEntry, async_fire_time_changed @@ -95,6 +95,7 @@ def mock_get_setting_values(mock_plenticore_client: ApiClient) -> list: @pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_setup_all_entries( hass: HomeAssistant, + entity_registry: er.EntityRegistry, mock_config_entry: MockConfigEntry, mock_plenticore_client: ApiClient, mock_get_setting_values: list, @@ -106,14 +107,16 @@ async def test_setup_all_entries( assert await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() - ent_reg = async_get(hass) - assert ent_reg.async_get("number.scb_battery_min_soc") is not None - assert ent_reg.async_get("number.scb_battery_min_home_consumption") is not None + assert entity_registry.async_get("number.scb_battery_min_soc") is not None + assert ( + entity_registry.async_get("number.scb_battery_min_home_consumption") is not None + ) @pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_setup_no_entries( hass: HomeAssistant, + entity_registry: er.EntityRegistry, mock_config_entry: MockConfigEntry, mock_plenticore_client: ApiClient, mock_get_setting_values: list, @@ -140,9 +143,8 @@ async def test_setup_no_entries( assert await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() - ent_reg = async_get(hass) - assert ent_reg.async_get("number.scb_battery_min_soc") is None - assert ent_reg.async_get("number.scb_battery_min_home_consumption") is None + assert entity_registry.async_get("number.scb_battery_min_soc") is None + assert entity_registry.async_get("number.scb_battery_min_home_consumption") is None @pytest.mark.usefixtures("entity_registry_enabled_by_default") diff --git a/tests/components/metoffice/test_sensor.py b/tests/components/metoffice/test_sensor.py index 6bddd1d2596..db84e85075e 100644 --- a/tests/components/metoffice/test_sensor.py +++ b/tests/components/metoffice/test_sensor.py @@ -8,7 +8,7 @@ import requests_mock from homeassistant.components.metoffice.const import ATTRIBUTION, DOMAIN from homeassistant.core import HomeAssistant -from homeassistant.helpers.device_registry import async_get as get_dev_reg +from homeassistant.helpers import device_registry as dr from .const import ( DEVICE_KEY_KINGSLYNN, @@ -27,7 +27,9 @@ from tests.common import MockConfigEntry, load_fixture @pytest.mark.freeze_time(datetime.datetime(2020, 4, 25, 12, tzinfo=datetime.UTC)) async def test_one_sensor_site_running( - hass: HomeAssistant, requests_mock: requests_mock.Mocker + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + requests_mock: requests_mock.Mocker, ) -> None: """Test the Met Office sensor platform.""" # all metoffice test data encapsulated in here @@ -54,9 +56,10 @@ async def test_one_sensor_site_running( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - dev_reg = get_dev_reg(hass) - assert len(dev_reg.devices) == 1 - device_wavertree = dev_reg.async_get_device(identifiers=DEVICE_KEY_WAVERTREE) + assert len(device_registry.devices) == 1 + device_wavertree = device_registry.async_get_device( + identifiers=DEVICE_KEY_WAVERTREE + ) assert device_wavertree.name == "Met Office Wavertree" running_sensor_ids = hass.states.async_entity_ids("sensor") @@ -75,7 +78,9 @@ async def test_one_sensor_site_running( @pytest.mark.freeze_time(datetime.datetime(2020, 4, 25, 12, tzinfo=datetime.UTC)) async def test_two_sensor_sites_running( - hass: HomeAssistant, requests_mock: requests_mock.Mocker + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + requests_mock: requests_mock.Mocker, ) -> None: """Test we handle two sets of sensors running for two different sites.""" @@ -115,11 +120,14 @@ async def test_two_sensor_sites_running( await hass.config_entries.async_setup(entry2.entry_id) await hass.async_block_till_done() - dev_reg = get_dev_reg(hass) - assert len(dev_reg.devices) == 2 - device_kingslynn = dev_reg.async_get_device(identifiers=DEVICE_KEY_KINGSLYNN) + assert len(device_registry.devices) == 2 + device_kingslynn = device_registry.async_get_device( + identifiers=DEVICE_KEY_KINGSLYNN + ) assert device_kingslynn.name == "Met Office King's Lynn" - device_wavertree = dev_reg.async_get_device(identifiers=DEVICE_KEY_WAVERTREE) + device_wavertree = device_registry.async_get_device( + identifiers=DEVICE_KEY_WAVERTREE + ) assert device_wavertree.name == "Met Office Wavertree" running_sensor_ids = hass.states.async_entity_ids("sensor") diff --git a/tests/components/metoffice/test_weather.py b/tests/components/metoffice/test_weather.py index 64a85897738..64e6ef65ec2 100644 --- a/tests/components/metoffice/test_weather.py +++ b/tests/components/metoffice/test_weather.py @@ -19,8 +19,7 @@ from homeassistant.components.weather import ( ) from homeassistant.const import STATE_UNAVAILABLE from homeassistant.core import HomeAssistant -from homeassistant.helpers import entity_registry as er -from homeassistant.helpers.device_registry import async_get as get_dev_reg +from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.util import utcnow from .const import ( @@ -73,7 +72,9 @@ async def wavertree_data(requests_mock: requests_mock.Mocker) -> dict[str, _Matc @pytest.mark.freeze_time(datetime.datetime(2020, 4, 25, 12, tzinfo=datetime.UTC)) async def test_site_cannot_connect( - hass: HomeAssistant, requests_mock: requests_mock.Mocker + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + requests_mock: requests_mock.Mocker, ) -> None: """Test we handle cannot connect error.""" @@ -89,8 +90,7 @@ async def test_site_cannot_connect( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - dev_reg = get_dev_reg(hass) - assert len(dev_reg.devices) == 0 + assert len(device_registry.devices) == 0 assert hass.states.get("weather.met_office_wavertree_3hourly") is None assert hass.states.get("weather.met_office_wavertree_daily") is None @@ -103,7 +103,6 @@ async def test_site_cannot_connect( @pytest.mark.freeze_time(datetime.datetime(2020, 4, 25, 12, tzinfo=datetime.UTC)) async def test_site_cannot_update( hass: HomeAssistant, - entity_registry: er.EntityRegistry, requests_mock: requests_mock.Mocker, wavertree_data, ) -> None: @@ -134,7 +133,7 @@ async def test_site_cannot_update( @pytest.mark.freeze_time(datetime.datetime(2020, 4, 25, 12, tzinfo=datetime.UTC)) async def test_one_weather_site_running( hass: HomeAssistant, - entity_registry: er.EntityRegistry, + device_registry: dr.DeviceRegistry, requests_mock: requests_mock.Mocker, wavertree_data, ) -> None: @@ -148,9 +147,10 @@ async def test_one_weather_site_running( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - dev_reg = get_dev_reg(hass) - assert len(dev_reg.devices) == 1 - device_wavertree = dev_reg.async_get_device(identifiers=DEVICE_KEY_WAVERTREE) + assert len(device_registry.devices) == 1 + device_wavertree = device_registry.async_get_device( + identifiers=DEVICE_KEY_WAVERTREE + ) assert device_wavertree.name == "Met Office Wavertree" # Wavertree daily weather platform expected results @@ -167,7 +167,7 @@ async def test_one_weather_site_running( @pytest.mark.freeze_time(datetime.datetime(2020, 4, 25, 12, tzinfo=datetime.UTC)) async def test_two_weather_sites_running( hass: HomeAssistant, - entity_registry: er.EntityRegistry, + device_registry: dr.DeviceRegistry, requests_mock: requests_mock.Mocker, wavertree_data, ) -> None: @@ -199,11 +199,14 @@ async def test_two_weather_sites_running( await hass.config_entries.async_setup(entry2.entry_id) await hass.async_block_till_done() - dev_reg = get_dev_reg(hass) - assert len(dev_reg.devices) == 2 - device_kingslynn = dev_reg.async_get_device(identifiers=DEVICE_KEY_KINGSLYNN) + assert len(device_registry.devices) == 2 + device_kingslynn = device_registry.async_get_device( + identifiers=DEVICE_KEY_KINGSLYNN + ) assert device_kingslynn.name == "Met Office King's Lynn" - device_wavertree = dev_reg.async_get_device(identifiers=DEVICE_KEY_WAVERTREE) + device_wavertree = device_registry.async_get_device( + identifiers=DEVICE_KEY_WAVERTREE + ) assert device_wavertree.name == "Met Office Wavertree" # Wavertree daily weather platform expected results @@ -371,7 +374,6 @@ async def test_legacy_config_entry_is_removed( async def test_forecast_subscription( hass: HomeAssistant, hass_ws_client: WebSocketGenerator, - entity_registry: er.EntityRegistry, freezer: FrozenDateTimeFactory, snapshot: SnapshotAssertion, no_sensor, diff --git a/tests/components/octoprint/test_servics.py b/tests/components/octoprint/test_servics.py index 2b5a89970e8..21a4ede8845 100644 --- a/tests/components/octoprint/test_servics.py +++ b/tests/components/octoprint/test_servics.py @@ -8,20 +8,19 @@ from homeassistant.components.octoprint.const import ( SERVICE_CONNECT, ) from homeassistant.const import ATTR_DEVICE_ID, CONF_PORT, CONF_PROFILE_NAME -from homeassistant.helpers.device_registry import ( - async_entries_for_config_entry, - async_get as async_get_dev_reg, -) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr from . import init_integration -async def test_connect_default(hass) -> None: +async def test_connect_default( + hass: HomeAssistant, device_registry: dr.DeviceRegistry +) -> None: """Test the connect to printer service.""" await init_integration(hass, "sensor") - dev_reg = async_get_dev_reg(hass) - device = async_entries_for_config_entry(dev_reg, "uuid")[0] + device = dr.async_entries_for_config_entry(device_registry, "uuid")[0] # Test pausing the printer when it is printing with patch("pyoctoprintapi.OctoprintClient.connect") as connect_command: @@ -40,12 +39,13 @@ async def test_connect_default(hass) -> None: ) -async def test_connect_all_arguments(hass) -> None: +async def test_connect_all_arguments( + hass: HomeAssistant, device_registry: dr.DeviceRegistry +) -> None: """Test the connect to printer service.""" await init_integration(hass, "sensor") - dev_reg = async_get_dev_reg(hass) - device = async_entries_for_config_entry(dev_reg, "uuid")[0] + device = dr.async_entries_for_config_entry(device_registry, "uuid")[0] # Test pausing the printer when it is printing with patch("pyoctoprintapi.OctoprintClient.connect") as connect_command: diff --git a/tests/components/plugwise/test_sensor.py b/tests/components/plugwise/test_sensor.py index 53de5f8c64a..9a20a37824d 100644 --- a/tests/components/plugwise/test_sensor.py +++ b/tests/components/plugwise/test_sensor.py @@ -5,8 +5,8 @@ from unittest.mock import MagicMock from homeassistant.components.plugwise.const import DOMAIN from homeassistant.const import Platform from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity_component import async_update_entity -from homeassistant.helpers.entity_registry import async_get from tests.common import MockConfigEntry @@ -49,13 +49,13 @@ async def test_adam_climate_sensor_entity_2( async def test_unique_id_migration_humidity( hass: HomeAssistant, + entity_registry: er.EntityRegistry, mock_smile_adam_4: MagicMock, mock_config_entry: MockConfigEntry, ) -> None: """Test unique ID migration of -relative_humidity to -humidity.""" mock_config_entry.add_to_hass(hass) - entity_registry = async_get(hass) # Entry to migrate entity_registry.async_get_or_create( Platform.SENSOR, @@ -136,7 +136,10 @@ async def test_p1_dsmr_sensor_entities( async def test_p1_3ph_dsmr_sensor_entities( - hass: HomeAssistant, mock_smile_p1_2: MagicMock, init_integration: MockConfigEntry + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + mock_smile_p1_2: MagicMock, + init_integration: MockConfigEntry, ) -> None: """Test creation of power related sensor entities.""" state = hass.states.get("sensor.p1_electricity_phase_one_consumed") @@ -155,7 +158,6 @@ async def test_p1_3ph_dsmr_sensor_entities( state = hass.states.get(entity_id) assert not state - entity_registry = async_get(hass) entity_registry.async_update_entity(entity_id=entity_id, disabled_by=None) await hass.async_block_till_done() diff --git a/tests/components/samsungtv/test_device_trigger.py b/tests/components/samsungtv/test_device_trigger.py index 19e7f3ca88a..e16ea718cbb 100644 --- a/tests/components/samsungtv/test_device_trigger.py +++ b/tests/components/samsungtv/test_device_trigger.py @@ -11,7 +11,7 @@ from homeassistant.components.samsungtv import DOMAIN, device_trigger from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.device_registry import async_get as get_dev_reg +from homeassistant.helpers import device_registry as dr from homeassistant.setup import async_setup_component from . import setup_samsungtv_entry @@ -21,12 +21,13 @@ from tests.common import MockConfigEntry, async_get_device_automations @pytest.mark.usefixtures("remoteencws", "rest_api") -async def test_get_triggers(hass: HomeAssistant) -> None: +async def test_get_triggers( + hass: HomeAssistant, device_registry: dr.DeviceRegistry +) -> None: """Test we get the expected triggers.""" await setup_samsungtv_entry(hass, MOCK_ENTRYDATA_ENCRYPTED_WS) - device_reg = get_dev_reg(hass) - device = device_reg.async_get_device(identifiers={(DOMAIN, "any")}) + device = device_registry.async_get_device(identifiers={(DOMAIN, "any")}) turn_on_trigger = { "platform": "device", @@ -44,14 +45,13 @@ async def test_get_triggers(hass: HomeAssistant) -> None: @pytest.mark.usefixtures("remoteencws", "rest_api") async def test_if_fires_on_turn_on_request( - hass: HomeAssistant, calls: list[ServiceCall] + hass: HomeAssistant, device_registry: dr.DeviceRegistry, calls: list[ServiceCall] ) -> None: """Test for turn_on and turn_off triggers firing.""" await setup_samsungtv_entry(hass, MOCK_ENTRYDATA_ENCRYPTED_WS) entity_id = "media_player.fake" - device_reg = get_dev_reg(hass) - device = device_reg.async_get_device(identifiers={(DOMAIN, "any")}) + device = device_registry.async_get_device(identifiers={(DOMAIN, "any")}) assert await async_setup_component( hass, @@ -103,7 +103,9 @@ async def test_if_fires_on_turn_on_request( @pytest.mark.usefixtures("remoteencws", "rest_api") -async def test_failure_scenarios(hass: HomeAssistant) -> None: +async def test_failure_scenarios( + hass: HomeAssistant, device_registry: dr.DeviceRegistry +) -> None: """Test failure scenarios.""" await setup_samsungtv_entry(hass, MOCK_ENTRYDATA_ENCRYPTED_WS) @@ -127,9 +129,8 @@ async def test_failure_scenarios(hass: HomeAssistant) -> None: entry = MockConfigEntry(domain="fake", state=ConfigEntryState.LOADED, data={}) entry.add_to_hass(hass) - device_reg = get_dev_reg(hass) - device = device_reg.async_get_or_create( + device = device_registry.async_get_or_create( config_entry_id=entry.entry_id, identifiers={("fake", "fake")} ) diff --git a/tests/components/smhi/test_init.py b/tests/components/smhi/test_init.py index aefbccb64ec..cfb386c8f6f 100644 --- a/tests/components/smhi/test_init.py +++ b/tests/components/smhi/test_init.py @@ -6,7 +6,7 @@ from smhi.smhi_lib import APIURL_TEMPLATE from homeassistant.components.smhi.const import DOMAIN from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity_registry import async_get +from homeassistant.helpers import entity_registry as er from . import ENTITY_ID, TEST_CONFIG, TEST_CONFIG_MIGRATE @@ -57,7 +57,10 @@ async def test_remove_entry( async def test_migrate_entry( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, api_response: str + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + aioclient_mock: AiohttpClientMocker, + api_response: str, ) -> None: """Test migrate entry data.""" uri = APIURL_TEMPLATE.format( @@ -68,8 +71,7 @@ async def test_migrate_entry( entry.add_to_hass(hass) assert entry.version == 1 - entity_reg = async_get(hass) - entity = entity_reg.async_get_or_create( + entity = entity_registry.async_get_or_create( domain="weather", config_entry=entry, original_name="Weather", @@ -87,7 +89,7 @@ async def test_migrate_entry( assert entry.version == 2 assert entry.unique_id == "17.84197-17.84197" - entity_get = entity_reg.async_get(entity.entity_id) + entity_get = entity_registry.async_get(entity.entity_id) assert entity_get.unique_id == "17.84197, 17.84197" diff --git a/tests/components/template/test_button.py b/tests/components/template/test_button.py index 989ca8e1287..c861c7874d4 100644 --- a/tests/components/template/test_button.py +++ b/tests/components/template/test_button.py @@ -15,7 +15,7 @@ from homeassistant.const import ( STATE_UNKNOWN, ) from homeassistant.core import HomeAssistant, ServiceCall -from homeassistant.helpers.entity_registry import async_get +from homeassistant.helpers import entity_registry as er from tests.common import assert_setup_component @@ -62,7 +62,10 @@ async def test_missing_required_keys(hass: HomeAssistant) -> None: async def test_all_optional_config( - hass: HomeAssistant, freezer: FrozenDateTimeFactory, calls: list[ServiceCall] + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + freezer: FrozenDateTimeFactory, + calls: list[ServiceCall], ) -> None: """Test: including all optional templates is ok.""" with assert_setup_component(1, "template"): @@ -124,8 +127,7 @@ async def test_all_optional_config( _TEST_OPTIONS_BUTTON, ) - er = async_get(hass) - assert er.async_get_entity_id("button", "template", "test-test") + assert entity_registry.async_get_entity_id("button", "template", "test-test") async def test_name_template(hass: HomeAssistant) -> None: diff --git a/tests/components/template/test_image.py b/tests/components/template/test_image.py index 6162276fcec..bda9e2530ca 100644 --- a/tests/components/template/test_image.py +++ b/tests/components/template/test_image.py @@ -17,7 +17,7 @@ from homeassistant.components.input_text import ( ) from homeassistant.const import ATTR_ENTITY_PICTURE, CONF_ENTITY_ID, STATE_UNKNOWN from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity_registry import async_get +from homeassistant.helpers import entity_registry as er from homeassistant.util import dt as dt_util from tests.common import assert_setup_component @@ -211,7 +211,9 @@ async def test_missing_required_keys(hass: HomeAssistant) -> None: assert hass.states.async_all("image") == [] -async def test_unique_id(hass: HomeAssistant) -> None: +async def test_unique_id( + hass: HomeAssistant, entity_registry: er.EntityRegistry +) -> None: """Test unique_id configuration.""" with assert_setup_component(1, "template"): assert await setup.async_setup_component( @@ -232,8 +234,7 @@ async def test_unique_id(hass: HomeAssistant) -> None: await hass.async_start() await hass.async_block_till_done() - ent_reg = async_get(hass) - entry = ent_reg.async_get(_TEST_IMAGE) + entry = entity_registry.async_get(_TEST_IMAGE) assert entry assert entry.unique_id == "b-a" diff --git a/tests/components/template/test_number.py b/tests/components/template/test_number.py index d715a6aed0b..bf04151fd36 100644 --- a/tests/components/template/test_number.py +++ b/tests/components/template/test_number.py @@ -16,7 +16,7 @@ from homeassistant.components.number import ( ) from homeassistant.const import ATTR_ICON, CONF_ENTITY_ID, STATE_UNKNOWN from homeassistant.core import Context, HomeAssistant, ServiceCall -from homeassistant.helpers.entity_registry import async_get +from homeassistant.helpers import entity_registry as er from tests.common import assert_setup_component, async_capture_events @@ -128,7 +128,7 @@ async def test_all_optional_config(hass: HomeAssistant) -> None: async def test_templates_with_entities( - hass: HomeAssistant, calls: list[ServiceCall] + hass: HomeAssistant, entity_registry: er.EntityRegistry, calls: list[ServiceCall] ) -> None: """Test templates with values from other entities.""" with assert_setup_component(4, "input_number"): @@ -208,8 +208,7 @@ async def test_templates_with_entities( await hass.async_start() await hass.async_block_till_done() - ent_reg = async_get(hass) - entry = ent_reg.async_get(_TEST_NUMBER) + entry = entity_registry.async_get(_TEST_NUMBER) assert entry assert entry.unique_id == "b-a" diff --git a/tests/components/template/test_select.py b/tests/components/template/test_select.py index 5f6561d3953..4106abdd469 100644 --- a/tests/components/template/test_select.py +++ b/tests/components/template/test_select.py @@ -16,7 +16,7 @@ from homeassistant.components.select import ( ) from homeassistant.const import ATTR_ICON, CONF_ENTITY_ID, STATE_UNKNOWN from homeassistant.core import Context, HomeAssistant, ServiceCall -from homeassistant.helpers.entity_registry import async_get +from homeassistant.helpers import entity_registry as er from tests.common import assert_setup_component, async_capture_events @@ -133,7 +133,7 @@ async def test_missing_required_keys(hass: HomeAssistant) -> None: async def test_templates_with_entities( - hass: HomeAssistant, calls: list[ServiceCall] + hass: HomeAssistant, entity_registry: er.EntityRegistry, calls: list[ServiceCall] ) -> None: """Test templates with values from other entities.""" with assert_setup_component(1, "input_select"): @@ -189,8 +189,7 @@ async def test_templates_with_entities( await hass.async_start() await hass.async_block_till_done() - ent_reg = async_get(hass) - entry = ent_reg.async_get(_TEST_SELECT) + entry = entity_registry.async_get(_TEST_SELECT) assert entry assert entry.unique_id == "b-a" diff --git a/tests/components/tomorrowio/test_sensor.py b/tests/components/tomorrowio/test_sensor.py index b0e2fba3123..43b0e33aed4 100644 --- a/tests/components/tomorrowio/test_sensor.py +++ b/tests/components/tomorrowio/test_sensor.py @@ -24,7 +24,7 @@ from homeassistant.components.tomorrowio.sensor import TomorrowioSensorEntityDes from homeassistant.config_entries import RELOAD_AFTER_UPDATE_DELAY, SOURCE_USER from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME from homeassistant.core import HomeAssistant, State, callback -from homeassistant.helpers.entity_registry import async_get +from homeassistant.helpers import entity_registry as er from homeassistant.util import dt as dt_util from homeassistant.util.unit_system import US_CUSTOMARY_SYSTEM @@ -103,7 +103,7 @@ V4_FIELDS = [ @callback def _enable_entity(hass: HomeAssistant, entity_name: str) -> None: """Enable disabled entity.""" - ent_reg = async_get(hass) + ent_reg = er.async_get(hass) entry = ent_reg.async_get(entity_name) updated_entry = ent_reg.async_update_entity(entry.entity_id, disabled_by=None) assert updated_entry != entry diff --git a/tests/components/tomorrowio/test_weather.py b/tests/components/tomorrowio/test_weather.py index 88a8d0d0c89..09f871896d3 100644 --- a/tests/components/tomorrowio/test_weather.py +++ b/tests/components/tomorrowio/test_weather.py @@ -43,7 +43,6 @@ from homeassistant.config_entries import RELOAD_AFTER_UPDATE_DELAY, SOURCE_USER from homeassistant.const import ATTR_ATTRIBUTION, ATTR_FRIENDLY_NAME, CONF_NAME from homeassistant.core import HomeAssistant, State, callback from homeassistant.helpers import entity_registry as er -from homeassistant.helpers.entity_registry import async_get from homeassistant.util import dt as dt_util from .const import API_V4_ENTRY_DATA @@ -55,7 +54,7 @@ from tests.typing import WebSocketGenerator @callback def _enable_entity(hass: HomeAssistant, entity_name: str) -> None: """Enable disabled entity.""" - ent_reg = async_get(hass) + ent_reg = er.async_get(hass) entry = ent_reg.async_get(entity_name) updated_entry = ent_reg.async_update_entity(entry.entity_id, disabled_by=None) assert updated_entry != entry diff --git a/tests/components/uvc/test_camera.py b/tests/components/uvc/test_camera.py index 522448ecfc4..5ce8baf9919 100644 --- a/tests/components/uvc/test_camera.py +++ b/tests/components/uvc/test_camera.py @@ -18,7 +18,7 @@ from homeassistant.components.camera import ( ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.entity_registry import async_get as async_get_entity_registry +from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow @@ -111,7 +111,9 @@ def camera_v313_fixture(): yield camera -async def test_setup_full_config(hass: HomeAssistant, mock_remote, camera_info) -> None: +async def test_setup_full_config( + hass: HomeAssistant, entity_registry: er.EntityRegistry, mock_remote, camera_info +) -> None: """Test the setup with full configuration.""" config = { "platform": "uvc", @@ -153,7 +155,6 @@ async def test_setup_full_config(hass: HomeAssistant, mock_remote, camera_info) assert state assert state.name == "Back" - entity_registry = async_get_entity_registry(hass) entity_entry = entity_registry.async_get("camera.front") assert entity_entry.unique_id == "id1" @@ -163,7 +164,9 @@ async def test_setup_full_config(hass: HomeAssistant, mock_remote, camera_info) assert entity_entry.unique_id == "id2" -async def test_setup_partial_config(hass: HomeAssistant, mock_remote) -> None: +async def test_setup_partial_config( + hass: HomeAssistant, entity_registry: er.EntityRegistry, mock_remote +) -> None: """Test the setup with partial configuration.""" config = {"platform": "uvc", "nvr": "foo", "key": "secret"} @@ -187,7 +190,6 @@ async def test_setup_partial_config(hass: HomeAssistant, mock_remote) -> None: assert state assert state.name == "Back" - entity_registry = async_get_entity_registry(hass) entity_entry = entity_registry.async_get("camera.front") assert entity_entry.unique_id == "id1" @@ -197,7 +199,9 @@ async def test_setup_partial_config(hass: HomeAssistant, mock_remote) -> None: assert entity_entry.unique_id == "id2" -async def test_setup_partial_config_v31x(hass: HomeAssistant, mock_remote) -> None: +async def test_setup_partial_config_v31x( + hass: HomeAssistant, entity_registry: er.EntityRegistry, mock_remote +) -> None: """Test the setup with a v3.1.x server.""" config = {"platform": "uvc", "nvr": "foo", "key": "secret"} mock_remote.return_value.server_version = (3, 1, 3) @@ -222,7 +226,6 @@ async def test_setup_partial_config_v31x(hass: HomeAssistant, mock_remote) -> No assert state assert state.name == "Back" - entity_registry = async_get_entity_registry(hass) entity_entry = entity_registry.async_get("camera.front") assert entity_entry.unique_id == "one" diff --git a/tests/components/xiaomi_ble/test_device_trigger.py b/tests/components/xiaomi_ble/test_device_trigger.py index 7b4624d1025..404eb6a4258 100644 --- a/tests/components/xiaomi_ble/test_device_trigger.py +++ b/tests/components/xiaomi_ble/test_device_trigger.py @@ -8,10 +8,7 @@ from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.xiaomi_ble.const import CONF_SUBTYPE, DOMAIN from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_PLATFORM, CONF_TYPE from homeassistant.core import HomeAssistant, ServiceCall, callback -from homeassistant.helpers.device_registry import ( - CONNECTION_NETWORK_MAC, - async_get as async_get_dev_reg, -) +from homeassistant.helpers import device_registry as dr from homeassistant.setup import async_setup_component from . import make_advertisement @@ -176,7 +173,9 @@ async def test_event_dimmer_rotate(hass: HomeAssistant) -> None: await hass.async_block_till_done() -async def test_get_triggers_button(hass: HomeAssistant) -> None: +async def test_get_triggers_button( + hass: HomeAssistant, device_registry: dr.DeviceRegistry +) -> None: """Test that we get the expected triggers from a Xiaomi BLE button sensor.""" mac = "54:EF:44:E3:9C:BC" data = {"bindkey": "5b51a7c91cde6707c9ef18dfda143a58"} @@ -196,8 +195,7 @@ async def test_get_triggers_button(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert len(events) == 1 - dev_reg = async_get_dev_reg(hass) - device = dev_reg.async_get_device(identifiers={get_device_id(mac)}) + device = device_registry.async_get_device(identifiers={get_device_id(mac)}) assert device expected_trigger = { CONF_PLATFORM: "device", @@ -216,7 +214,9 @@ async def test_get_triggers_button(hass: HomeAssistant) -> None: await hass.async_block_till_done() -async def test_get_triggers_double_button(hass: HomeAssistant) -> None: +async def test_get_triggers_double_button( + hass: HomeAssistant, device_registry: dr.DeviceRegistry +) -> None: """Test that we get the expected triggers from a Xiaomi BLE switch with 2 buttons.""" mac = "DC:ED:83:87:12:73" data = {"bindkey": "b93eb3787eabda352edd94b667f5d5a9"} @@ -236,8 +236,7 @@ async def test_get_triggers_double_button(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert len(events) == 1 - dev_reg = async_get_dev_reg(hass) - device = dev_reg.async_get_device(identifiers={get_device_id(mac)}) + device = device_registry.async_get_device(identifiers={get_device_id(mac)}) assert device expected_trigger = { CONF_PLATFORM: "device", @@ -256,7 +255,9 @@ async def test_get_triggers_double_button(hass: HomeAssistant) -> None: await hass.async_block_till_done() -async def test_get_triggers_lock(hass: HomeAssistant) -> None: +async def test_get_triggers_lock( + hass: HomeAssistant, device_registry: dr.DeviceRegistry +) -> None: """Test that we get the expected triggers from a Xiaomi BLE lock with fingerprint scanner.""" mac = "98:0C:33:A3:04:3D" data = {"bindkey": "54d84797cb77f9538b224b305c877d1e"} @@ -277,8 +278,7 @@ async def test_get_triggers_lock(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert len(events) == 1 - dev_reg = async_get_dev_reg(hass) - device = dev_reg.async_get_device(identifiers={get_device_id(mac)}) + device = device_registry.async_get_device(identifiers={get_device_id(mac)}) assert device expected_trigger = { CONF_PLATFORM: "device", @@ -297,7 +297,9 @@ async def test_get_triggers_lock(hass: HomeAssistant) -> None: await hass.async_block_till_done() -async def test_get_triggers_motion(hass: HomeAssistant) -> None: +async def test_get_triggers_motion( + hass: HomeAssistant, device_registry: dr.DeviceRegistry +) -> None: """Test that we get the expected triggers from a Xiaomi BLE motion sensor.""" mac = "DE:70:E8:B2:39:0C" entry = await _async_setup_xiaomi_device(hass, mac) @@ -313,8 +315,7 @@ async def test_get_triggers_motion(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert len(events) == 1 - dev_reg = async_get_dev_reg(hass) - device = dev_reg.async_get_device(identifiers={get_device_id(mac)}) + device = device_registry.async_get_device(identifiers={get_device_id(mac)}) assert device expected_trigger = { CONF_PLATFORM: "device", @@ -333,7 +334,9 @@ async def test_get_triggers_motion(hass: HomeAssistant) -> None: await hass.async_block_till_done() -async def test_get_triggers_for_invalid_xiami_ble_device(hass: HomeAssistant) -> None: +async def test_get_triggers_for_invalid_xiami_ble_device( + hass: HomeAssistant, device_registry: dr.DeviceRegistry +) -> None: """Test that we don't get triggers for an device that does not emit events.""" mac = "C4:7C:8D:6A:3E:7A" entry = await _async_setup_xiaomi_device(hass, mac) @@ -349,8 +352,7 @@ async def test_get_triggers_for_invalid_xiami_ble_device(hass: HomeAssistant) -> await hass.async_block_till_done() assert len(events) == 0 - dev_reg = async_get_dev_reg(hass) - invalid_device = dev_reg.async_get_or_create( + invalid_device = device_registry.async_get_or_create( config_entry_id=entry.entry_id, identifiers={(DOMAIN, "invdevmac")}, ) @@ -364,7 +366,9 @@ async def test_get_triggers_for_invalid_xiami_ble_device(hass: HomeAssistant) -> await hass.async_block_till_done() -async def test_get_triggers_for_invalid_device_id(hass: HomeAssistant) -> None: +async def test_get_triggers_for_invalid_device_id( + hass: HomeAssistant, device_registry: dr.DeviceRegistry +) -> None: """Test that we don't get triggers when using an invalid device_id.""" mac = "DE:70:E8:B2:39:0C" entry = await _async_setup_xiaomi_device(hass, mac) @@ -378,11 +382,9 @@ async def test_get_triggers_for_invalid_device_id(hass: HomeAssistant) -> None: # wait for the event await hass.async_block_till_done() - dev_reg = async_get_dev_reg(hass) - - invalid_device = dev_reg.async_get_or_create( + invalid_device = device_registry.async_get_or_create( config_entry_id=entry.entry_id, - connections={(CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, ) assert invalid_device triggers = await async_get_device_automations( @@ -395,7 +397,7 @@ async def test_get_triggers_for_invalid_device_id(hass: HomeAssistant) -> None: async def test_if_fires_on_button_press( - hass: HomeAssistant, calls: list[ServiceCall] + hass: HomeAssistant, device_registry: dr.DeviceRegistry, calls: list[ServiceCall] ) -> None: """Test for button press event trigger firing.""" mac = "54:EF:44:E3:9C:BC" @@ -414,8 +416,7 @@ async def test_if_fires_on_button_press( # wait for the device being created await hass.async_block_till_done() - dev_reg = async_get_dev_reg(hass) - device = dev_reg.async_get_device(identifiers={get_device_id(mac)}) + device = device_registry.async_get_device(identifiers={get_device_id(mac)}) device_id = device.id assert await async_setup_component( @@ -457,7 +458,7 @@ async def test_if_fires_on_button_press( async def test_if_fires_on_double_button_long_press( - hass: HomeAssistant, calls: list[ServiceCall] + hass: HomeAssistant, device_registry: dr.DeviceRegistry, calls: list[ServiceCall] ) -> None: """Test for button press event trigger firing.""" mac = "DC:ED:83:87:12:73" @@ -476,8 +477,7 @@ async def test_if_fires_on_double_button_long_press( # wait for the device being created await hass.async_block_till_done() - dev_reg = async_get_dev_reg(hass) - device = dev_reg.async_get_device(identifiers={get_device_id(mac)}) + device = device_registry.async_get_device(identifiers={get_device_id(mac)}) device_id = device.id assert await async_setup_component( @@ -519,7 +519,7 @@ async def test_if_fires_on_double_button_long_press( async def test_if_fires_on_motion_detected( - hass: HomeAssistant, calls: list[ServiceCall] + hass: HomeAssistant, device_registry: dr.DeviceRegistry, calls: list[ServiceCall] ) -> None: """Test for motion event trigger firing.""" mac = "DE:70:E8:B2:39:0C" @@ -534,8 +534,7 @@ async def test_if_fires_on_motion_detected( # wait for the device being created await hass.async_block_till_done() - dev_reg = async_get_dev_reg(hass) - device = dev_reg.async_get_device(identifiers={get_device_id(mac)}) + device = device_registry.async_get_device(identifiers={get_device_id(mac)}) device_id = device.id assert await async_setup_component( @@ -575,6 +574,7 @@ async def test_if_fires_on_motion_detected( async def test_automation_with_invalid_trigger_type( hass: HomeAssistant, + device_registry: dr.DeviceRegistry, caplog: pytest.LogCaptureFixture, ) -> None: """Test for automation with invalid trigger type.""" @@ -590,8 +590,7 @@ async def test_automation_with_invalid_trigger_type( # wait for the event await hass.async_block_till_done() - dev_reg = async_get_dev_reg(hass) - device = dev_reg.async_get_device(identifiers={get_device_id(mac)}) + device = device_registry.async_get_device(identifiers={get_device_id(mac)}) device_id = device.id assert await async_setup_component( @@ -624,6 +623,7 @@ async def test_automation_with_invalid_trigger_type( async def test_automation_with_invalid_trigger_event_property( hass: HomeAssistant, + device_registry: dr.DeviceRegistry, caplog: pytest.LogCaptureFixture, ) -> None: """Test for automation with invalid trigger event property.""" @@ -639,8 +639,7 @@ async def test_automation_with_invalid_trigger_event_property( # wait for the event await hass.async_block_till_done() - dev_reg = async_get_dev_reg(hass) - device = dev_reg.async_get_device(identifiers={get_device_id(mac)}) + device = device_registry.async_get_device(identifiers={get_device_id(mac)}) device_id = device.id assert await async_setup_component( @@ -675,7 +674,7 @@ async def test_automation_with_invalid_trigger_event_property( async def test_triggers_for_invalid__model( - hass: HomeAssistant, calls: list[ServiceCall] + hass: HomeAssistant, device_registry: dr.DeviceRegistry, calls: list[ServiceCall] ) -> None: """Test invalid model doesn't return triggers.""" mac = "DE:70:E8:B2:39:0C" @@ -691,8 +690,7 @@ async def test_triggers_for_invalid__model( await hass.async_block_till_done() # modify model to invalid model - dev_reg = async_get_dev_reg(hass) - invalid_model = dev_reg.async_get_or_create( + invalid_model = device_registry.async_get_or_create( config_entry_id=entry.entry_id, identifiers={(DOMAIN, mac)}, model="invalid model", diff --git a/tests/components/zha/test_diagnostics.py b/tests/components/zha/test_diagnostics.py index 50b07b70e8d..4bb30a5fc8c 100644 --- a/tests/components/zha/test_diagnostics.py +++ b/tests/components/zha/test_diagnostics.py @@ -12,7 +12,7 @@ from homeassistant.components.zha.core.helpers import get_zha_gateway from homeassistant.components.zha.diagnostics import KEYS_TO_REDACT from homeassistant.const import Platform from homeassistant.core import HomeAssistant -from homeassistant.helpers.device_registry import async_get +from homeassistant.helpers import device_registry as dr from .conftest import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_PROFILE, SIG_EP_TYPE @@ -104,6 +104,7 @@ async def test_diagnostics_for_config_entry( async def test_diagnostics_for_device( hass: HomeAssistant, hass_client: ClientSessionGenerator, + device_registry: dr.DeviceRegistry, config_entry: MockConfigEntry, zha_device_joined, zigpy_device, @@ -126,8 +127,9 @@ async def test_diagnostics_for_device( } ) - dev_reg = async_get(hass) - device = dev_reg.async_get_device(identifiers={("zha", str(zha_device.ieee))}) + device = device_registry.async_get_device( + identifiers={("zha", str(zha_device.ieee))} + ) assert device diagnostics_data = await get_diagnostics_for_device( hass, hass_client, config_entry, device From 52379ad7cbe0ec1f4e4c510decf92d23eb403cc3 Mon Sep 17 00:00:00 2001 From: chammp <57918757+chammp@users.noreply.github.com> Date: Mon, 10 Jun 2024 16:54:17 +0200 Subject: [PATCH 0466/1445] Add code_format_template to template locks (#106947) * Add code_format to template locks * Replace code_format with code_format_template * Add test case for template eval to None * Apply suggestion to not call super() Co-authored-by: Erik Montnemery * Add more negative tests * Handle template render errors * Better error message * Add custom test lock config for code format * Add type hints from upstream --------- Co-authored-by: Erik Montnemery Co-authored-by: Franck Nijhof --- homeassistant/components/template/lock.py | 64 +++- .../components/template/strings.json | 5 + tests/components/template/test_lock.py | 280 +++++++++++++++++- 3 files changed, 342 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/template/lock.py b/homeassistant/components/template/lock.py index 3f9df4818fd..8259a6c12f0 100644 --- a/homeassistant/components/template/lock.py +++ b/homeassistant/components/template/lock.py @@ -14,6 +14,7 @@ from homeassistant.components.lock import ( LockEntity, ) from homeassistant.const import ( + ATTR_CODE, CONF_NAME, CONF_OPTIMISTIC, CONF_UNIQUE_ID, @@ -23,7 +24,7 @@ from homeassistant.const import ( STATE_UNLOCKED, ) from homeassistant.core import HomeAssistant, callback -from homeassistant.exceptions import TemplateError +from homeassistant.exceptions import ServiceValidationError, TemplateError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.script import Script @@ -36,6 +37,7 @@ from .template_entity import ( rewrite_common_legacy_to_modern_conf, ) +CONF_CODE_FORMAT_TEMPLATE = "code_format_template" CONF_LOCK = "lock" CONF_UNLOCK = "unlock" @@ -48,6 +50,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( vol.Required(CONF_LOCK): cv.SCRIPT_SCHEMA, vol.Required(CONF_UNLOCK): cv.SCRIPT_SCHEMA, vol.Required(CONF_VALUE_TEMPLATE): cv.template, + vol.Optional(CONF_CODE_FORMAT_TEMPLATE): cv.template, vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, vol.Optional(CONF_UNIQUE_ID): cv.string, } @@ -90,6 +93,9 @@ class TemplateLock(TemplateEntity, LockEntity): self._state_template = config.get(CONF_VALUE_TEMPLATE) self._command_lock = Script(hass, config[CONF_LOCK], name, DOMAIN) self._command_unlock = Script(hass, config[CONF_UNLOCK], name, DOMAIN) + self._code_format_template = config.get(CONF_CODE_FORMAT_TEMPLATE) + self._code_format = None + self._code_format_template_error = None self._optimistic = config.get(CONF_OPTIMISTIC) self._attr_assumed_state = bool(self._optimistic) @@ -115,6 +121,7 @@ class TemplateLock(TemplateEntity, LockEntity): @callback def _update_state(self, result): + """Update the state from the template.""" super()._update_state(result) if isinstance(result, TemplateError): self._state = None @@ -130,24 +137,75 @@ class TemplateLock(TemplateEntity, LockEntity): self._state = None + @property + def code_format(self) -> str | None: + """Regex for code format or None if no code is required.""" + return self._code_format + @callback def _async_setup_templates(self) -> None: """Set up templates.""" self.add_template_attribute( "_state", self._state_template, None, self._update_state ) + if self._code_format_template: + self.add_template_attribute( + "_code_format_template", + self._code_format_template, + None, + self._update_code_format, + ) super()._async_setup_templates() + @callback + def _update_code_format(self, render: str | TemplateError | None): + """Update code format from the template.""" + if isinstance(render, TemplateError): + self._code_format = None + self._code_format_template_error = render + elif render in (None, "None", ""): + self._code_format = None + self._code_format_template_error = None + else: + self._code_format = render + self._code_format_template_error = None + async def async_lock(self, **kwargs: Any) -> None: """Lock the device.""" + self._raise_template_error_if_available() + if self._optimistic: self._state = True self.async_write_ha_state() - await self.async_run_script(self._command_lock, context=self._context) + + tpl_vars = {ATTR_CODE: kwargs.get(ATTR_CODE) if kwargs else None} + + await self.async_run_script( + self._command_lock, run_variables=tpl_vars, context=self._context + ) async def async_unlock(self, **kwargs: Any) -> None: """Unlock the device.""" + self._raise_template_error_if_available() + if self._optimistic: self._state = False self.async_write_ha_state() - await self.async_run_script(self._command_unlock, context=self._context) + + tpl_vars = {ATTR_CODE: kwargs.get(ATTR_CODE) if kwargs else None} + + await self.async_run_script( + self._command_unlock, run_variables=tpl_vars, context=self._context + ) + + def _raise_template_error_if_available(self): + if self._code_format_template_error is not None: + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="code_format_template_error", + translation_placeholders={ + "entity_id": self.entity_id, + "code_format_template": self._code_format_template.template, + "cause": str(self._code_format_template_error), + }, + ) diff --git a/homeassistant/components/template/strings.json b/homeassistant/components/template/strings.json index 6122f4c9db5..f5958ec550e 100644 --- a/homeassistant/components/template/strings.json +++ b/homeassistant/components/template/strings.json @@ -153,5 +153,10 @@ "name": "[%key:common::action::reload%]", "description": "Reloads template entities from the YAML-configuration." } + }, + "exceptions": { + "code_format_template_error": { + "message": "Error evaluating code format template \"{code_format_template}\" for {entity_id}: {cause}" + } } } diff --git a/tests/components/template/test_lock.py b/tests/components/template/test_lock.py index 67e7c5bc965..f4e81cbfd63 100644 --- a/tests/components/template/test_lock.py +++ b/tests/components/template/test_lock.py @@ -4,7 +4,13 @@ import pytest from homeassistant import setup from homeassistant.components import lock -from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON, STATE_UNAVAILABLE +from homeassistant.const import ( + ATTR_CODE, + ATTR_ENTITY_ID, + STATE_OFF, + STATE_ON, + STATE_UNAVAILABLE, +) from homeassistant.core import HomeAssistant, ServiceCall OPTIMISTIC_LOCK_CONFIG = { @@ -25,6 +31,26 @@ OPTIMISTIC_LOCK_CONFIG = { }, } +OPTIMISTIC_CODED_LOCK_CONFIG = { + "platform": "template", + "lock": { + "service": "test.automation", + "data_template": { + "action": "lock", + "caller": "{{ this.entity_id }}", + "code": "{{ code }}", + }, + }, + "unlock": { + "service": "test.automation", + "data_template": { + "action": "unlock", + "caller": "{{ this.entity_id }}", + "code": "{{ code }}", + }, + }, +} + @pytest.mark.parametrize(("count", "domain"), [(1, lock.DOMAIN)]) @pytest.mark.parametrize( @@ -138,10 +164,24 @@ async def test_template_state_boolean_off(hass: HomeAssistant, start_ha) -> None }, } }, + { + lock.DOMAIN: { + **OPTIMISTIC_LOCK_CONFIG, + "value_template": "{{ 1 == 1 }}", + "code_format_template": "{{ rubbish }", + } + }, + { + lock.DOMAIN: { + **OPTIMISTIC_LOCK_CONFIG, + "value_template": "{{ 1 == 1 }}", + "code_format_template": "{% if rubbish %}", + } + }, ], ) async def test_template_syntax_error(hass: HomeAssistant, start_ha) -> None: - """Test templating syntax error.""" + """Test templating syntax errors don't create entities.""" assert hass.states.async_all("lock") == [] @@ -192,7 +232,9 @@ async def test_lock_action( assert state.state == lock.STATE_UNLOCKED await hass.services.async_call( - lock.DOMAIN, lock.SERVICE_LOCK, {ATTR_ENTITY_ID: "lock.template_lock"} + lock.DOMAIN, + lock.SERVICE_LOCK, + {ATTR_ENTITY_ID: "lock.template_lock"}, ) await hass.async_block_till_done() @@ -225,7 +267,9 @@ async def test_unlock_action( assert state.state == lock.STATE_LOCKED await hass.services.async_call( - lock.DOMAIN, lock.SERVICE_UNLOCK, {ATTR_ENTITY_ID: "lock.template_lock"} + lock.DOMAIN, + lock.SERVICE_UNLOCK, + {ATTR_ENTITY_ID: "lock.template_lock"}, ) await hass.async_block_till_done() @@ -234,6 +278,234 @@ async def test_unlock_action( assert calls[0].data["caller"] == "lock.template_lock" +@pytest.mark.parametrize(("count", "domain"), [(1, lock.DOMAIN)]) +@pytest.mark.parametrize( + "config", + [ + { + lock.DOMAIN: { + **OPTIMISTIC_CODED_LOCK_CONFIG, + "value_template": "{{ states.switch.test_state.state }}", + "code_format_template": "{{ '.+' }}", + } + }, + ], +) +async def test_lock_action_with_code( + hass: HomeAssistant, start_ha, calls: list[ServiceCall] +) -> None: + """Test lock action with defined code format and supplied lock code.""" + await setup.async_setup_component(hass, "switch", {}) + hass.states.async_set("switch.test_state", STATE_OFF) + await hass.async_block_till_done() + + state = hass.states.get("lock.template_lock") + assert state.state == lock.STATE_UNLOCKED + + await hass.services.async_call( + lock.DOMAIN, + lock.SERVICE_LOCK, + {ATTR_ENTITY_ID: "lock.template_lock", ATTR_CODE: "LOCK_CODE"}, + ) + await hass.async_block_till_done() + + assert len(calls) == 1 + assert calls[0].data["action"] == "lock" + assert calls[0].data["caller"] == "lock.template_lock" + assert calls[0].data["code"] == "LOCK_CODE" + + +@pytest.mark.parametrize(("count", "domain"), [(1, lock.DOMAIN)]) +@pytest.mark.parametrize( + "config", + [ + { + lock.DOMAIN: { + **OPTIMISTIC_CODED_LOCK_CONFIG, + "value_template": "{{ states.switch.test_state.state }}", + "code_format_template": "{{ '.+' }}", + } + }, + ], +) +async def test_unlock_action_with_code( + hass: HomeAssistant, start_ha, calls: list[ServiceCall] +) -> None: + """Test unlock action with code format and supplied unlock code.""" + await setup.async_setup_component(hass, "switch", {}) + hass.states.async_set("switch.test_state", STATE_ON) + await hass.async_block_till_done() + + state = hass.states.get("lock.template_lock") + assert state.state == lock.STATE_LOCKED + + await hass.services.async_call( + lock.DOMAIN, + lock.SERVICE_UNLOCK, + {ATTR_ENTITY_ID: "lock.template_lock", ATTR_CODE: "UNLOCK_CODE"}, + ) + await hass.async_block_till_done() + + assert len(calls) == 1 + assert calls[0].data["action"] == "unlock" + assert calls[0].data["caller"] == "lock.template_lock" + assert calls[0].data["code"] == "UNLOCK_CODE" + + +@pytest.mark.parametrize(("count", "domain"), [(1, lock.DOMAIN)]) +@pytest.mark.parametrize( + "config", + [ + { + lock.DOMAIN: { + **OPTIMISTIC_LOCK_CONFIG, + "value_template": "{{ 1 == 1 }}", + "code_format_template": "{{ '\\\\d+' }}", + } + }, + ], +) +@pytest.mark.parametrize( + "test_action", + [ + lock.SERVICE_LOCK, + lock.SERVICE_UNLOCK, + ], +) +async def test_lock_actions_fail_with_invalid_code( + hass: HomeAssistant, start_ha, calls: list[ServiceCall], test_action +) -> None: + """Test invalid lock codes.""" + await hass.services.async_call( + lock.DOMAIN, + test_action, + {ATTR_ENTITY_ID: "lock.template_lock", ATTR_CODE: "non-number-value"}, + ) + await hass.services.async_call( + lock.DOMAIN, + test_action, + {ATTR_ENTITY_ID: "lock.template_lock"}, + ) + await hass.async_block_till_done() + + assert len(calls) == 0 + + +@pytest.mark.parametrize(("count", "domain"), [(1, lock.DOMAIN)]) +@pytest.mark.parametrize( + "config", + [ + { + lock.DOMAIN: { + **OPTIMISTIC_LOCK_CONFIG, + "value_template": "{{ 1 == 1 }}", + "code_format_template": "{{ 1/0 }}", + } + }, + ], +) +async def test_lock_actions_dont_execute_with_code_template_rendering_error( + hass: HomeAssistant, start_ha, calls: list[ServiceCall] +) -> None: + """Test lock code format rendering fails block lock/unlock actions.""" + await hass.services.async_call( + lock.DOMAIN, + lock.SERVICE_LOCK, + {ATTR_ENTITY_ID: "lock.template_lock"}, + ) + await hass.services.async_call( + lock.DOMAIN, + lock.SERVICE_UNLOCK, + {ATTR_ENTITY_ID: "lock.template_lock", ATTR_CODE: "any-value"}, + ) + await hass.async_block_till_done() + + assert len(calls) == 0 + + +@pytest.mark.parametrize(("count", "domain"), [(1, lock.DOMAIN)]) +@pytest.mark.parametrize("action", [lock.SERVICE_LOCK, lock.SERVICE_UNLOCK]) +@pytest.mark.parametrize( + "config", + [ + { + lock.DOMAIN: { + **OPTIMISTIC_CODED_LOCK_CONFIG, + "value_template": "{{ states.switch.test_state.state }}", + "code_format_template": "{{ None }}", + } + }, + ], +) +async def test_actions_with_none_as_codeformat_ignores_code( + hass: HomeAssistant, action, start_ha, calls: list[ServiceCall] +) -> None: + """Test lock actions with supplied lock code.""" + await setup.async_setup_component(hass, "switch", {}) + hass.states.async_set("switch.test_state", STATE_OFF) + await hass.async_block_till_done() + + state = hass.states.get("lock.template_lock") + assert state.state == lock.STATE_UNLOCKED + + await hass.services.async_call( + lock.DOMAIN, + action, + {ATTR_ENTITY_ID: "lock.template_lock", ATTR_CODE: "any code"}, + ) + await hass.async_block_till_done() + + assert len(calls) == 1 + assert calls[0].data["action"] == action + assert calls[0].data["caller"] == "lock.template_lock" + assert calls[0].data["code"] == "any code" + + +@pytest.mark.parametrize(("count", "domain"), [(1, lock.DOMAIN)]) +@pytest.mark.parametrize("action", [lock.SERVICE_LOCK, lock.SERVICE_UNLOCK]) +@pytest.mark.parametrize( + "config", + [ + { + lock.DOMAIN: { + **OPTIMISTIC_LOCK_CONFIG, + "value_template": "{{ states.switch.test_state.state }}", + "code_format_template": "[12]{1", + } + }, + ], +) +async def test_actions_with_invalid_regexp_as_codeformat_never_execute( + hass: HomeAssistant, action, start_ha, calls: list[ServiceCall] +) -> None: + """Test lock actions don't execute with invalid regexp.""" + await setup.async_setup_component(hass, "switch", {}) + hass.states.async_set("switch.test_state", STATE_OFF) + await hass.async_block_till_done() + + state = hass.states.get("lock.template_lock") + assert state.state == lock.STATE_UNLOCKED + + await hass.services.async_call( + lock.DOMAIN, + action, + {ATTR_ENTITY_ID: "lock.template_lock", ATTR_CODE: "1"}, + ) + await hass.services.async_call( + lock.DOMAIN, + action, + {ATTR_ENTITY_ID: "lock.template_lock", ATTR_CODE: "x"}, + ) + await hass.services.async_call( + lock.DOMAIN, + action, + {ATTR_ENTITY_ID: "lock.template_lock"}, + ) + await hass.async_block_till_done() + + assert len(calls) == 0 + + @pytest.mark.parametrize(("count", "domain"), [(1, lock.DOMAIN)]) @pytest.mark.parametrize( "config", From 30fab7b807553399f746740759e7c8b2a51258bd Mon Sep 17 00:00:00 2001 From: William Taylor Date: Tue, 11 Jun 2024 01:16:36 +1000 Subject: [PATCH 0467/1445] Add support for animal detection in unifiprotect (#116290) Co-authored-by: J. Nick Koston --- .../components/unifiprotect/binary_sensor.py | 18 ++++++++++++++++++ .../components/unifiprotect/switch.py | 11 +++++++++++ tests/components/unifiprotect/conftest.py | 1 + .../unifiprotect/test_binary_sensor.py | 8 ++++---- tests/components/unifiprotect/test_switch.py | 17 +++++++++-------- 5 files changed, 43 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/unifiprotect/binary_sensor.py b/homeassistant/components/unifiprotect/binary_sensor.py index b6aaed8f975..7e66f5efb28 100644 --- a/homeassistant/components/unifiprotect/binary_sensor.py +++ b/homeassistant/components/unifiprotect/binary_sensor.py @@ -175,6 +175,15 @@ CAMERA_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( ufp_value="is_vehicle_detection_on", ufp_perm=PermRequired.NO_WRITE, ), + ProtectBinaryEntityDescription( + key="smart_animal", + name="Detections: Animal", + icon="mdi:paw", + entity_category=EntityCategory.DIAGNOSTIC, + ufp_required_field="can_detect_animal", + ufp_value="is_animal_detection_on", + ufp_perm=PermRequired.NO_WRITE, + ), ProtectBinaryEntityDescription( key="smart_package", name="Detections: Package", @@ -453,6 +462,15 @@ EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = ( ufp_enabled="is_vehicle_detection_on", ufp_event_obj="last_vehicle_detect_event", ), + ProtectBinaryEventEntityDescription( + key="smart_obj_animal", + name="Animal Detected", + icon="mdi:paw", + ufp_value="is_animal_currently_detected", + ufp_required_field="can_detect_animal", + ufp_enabled="is_animal_detection_on", + ufp_event_obj="last_animal_detect_event", + ), ProtectBinaryEventEntityDescription( key="smart_obj_package", name="Package Detected", diff --git a/homeassistant/components/unifiprotect/switch.py b/homeassistant/components/unifiprotect/switch.py index d17b208de12..50953e2b8fe 100644 --- a/homeassistant/components/unifiprotect/switch.py +++ b/homeassistant/components/unifiprotect/switch.py @@ -179,6 +179,17 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( ufp_set_method="set_vehicle_detection", ufp_perm=PermRequired.WRITE, ), + ProtectSwitchEntityDescription( + key="smart_animal", + name="Detections: Animal", + icon="mdi:paw", + entity_category=EntityCategory.CONFIG, + ufp_required_field="can_detect_animal", + ufp_value="is_animal_detection_on", + ufp_enabled="is_recording_enabled", + ufp_set_method="set_animal_detection", + ufp_perm=PermRequired.WRITE, + ), ProtectSwitchEntityDescription( key="smart_package", name="Detections: Package", diff --git a/tests/components/unifiprotect/conftest.py b/tests/components/unifiprotect/conftest.py index 9eb1ea312c6..02a1ce3f421 100644 --- a/tests/components/unifiprotect/conftest.py +++ b/tests/components/unifiprotect/conftest.py @@ -216,6 +216,7 @@ def doorbell_fixture(camera: Camera, fixed_now: datetime): doorbell.feature_flags.smart_detect_types = [ SmartDetectObjectType.PERSON, SmartDetectObjectType.VEHICLE, + SmartDetectObjectType.ANIMAL, ] doorbell.has_speaker = True doorbell.feature_flags.has_hdr = True diff --git a/tests/components/unifiprotect/test_binary_sensor.py b/tests/components/unifiprotect/test_binary_sensor.py index dbe8f72b244..b23fd529233 100644 --- a/tests/components/unifiprotect/test_binary_sensor.py +++ b/tests/components/unifiprotect/test_binary_sensor.py @@ -50,11 +50,11 @@ async def test_binary_sensor_camera_remove( ufp.api.bootstrap.nvr.system_info.ustorage = None await init_entry(hass, ufp, [doorbell, unadopted_camera]) - assert_entity_counts(hass, Platform.BINARY_SENSOR, 7, 7) + assert_entity_counts(hass, Platform.BINARY_SENSOR, 8, 8) await remove_entities(hass, ufp, [doorbell, unadopted_camera]) assert_entity_counts(hass, Platform.BINARY_SENSOR, 0, 0) await adopt_devices(hass, ufp, [doorbell, unadopted_camera]) - assert_entity_counts(hass, Platform.BINARY_SENSOR, 7, 7) + assert_entity_counts(hass, Platform.BINARY_SENSOR, 8, 8) async def test_binary_sensor_light_remove( @@ -122,7 +122,7 @@ async def test_binary_sensor_setup_camera_all( ufp.api.bootstrap.nvr.system_info.ustorage = None await init_entry(hass, ufp, [doorbell, unadopted_camera]) - assert_entity_counts(hass, Platform.BINARY_SENSOR, 7, 7) + assert_entity_counts(hass, Platform.BINARY_SENSOR, 8, 8) description = EVENT_SENSORS[0] unique_id, entity_id = ids_from_device_description( @@ -274,7 +274,7 @@ async def test_binary_sensor_update_motion( """Test binary_sensor motion entity.""" await init_entry(hass, ufp, [doorbell, unadopted_camera]) - assert_entity_counts(hass, Platform.BINARY_SENSOR, 13, 13) + assert_entity_counts(hass, Platform.BINARY_SENSOR, 14, 14) _, entity_id = ids_from_device_description( Platform.BINARY_SENSOR, doorbell, EVENT_SENSORS[1] diff --git a/tests/components/unifiprotect/test_switch.py b/tests/components/unifiprotect/test_switch.py index 16e471c2e7a..e03ab81833b 100644 --- a/tests/components/unifiprotect/test_switch.py +++ b/tests/components/unifiprotect/test_switch.py @@ -43,6 +43,7 @@ CAMERA_SWITCHES_BASIC = [ or d.name == "Detections: Motion" or d.name == "Detections: Person" or d.name == "Detections: Vehicle" + or d.name == "Detections: Animal" ] CAMERA_SWITCHES_NO_EXTRA = [ d @@ -58,11 +59,11 @@ async def test_switch_camera_remove( ufp.api.bootstrap.nvr.system_info.ustorage = None await init_entry(hass, ufp, [doorbell, unadopted_camera]) - assert_entity_counts(hass, Platform.SWITCH, 15, 13) + assert_entity_counts(hass, Platform.SWITCH, 16, 14) await remove_entities(hass, ufp, [doorbell, unadopted_camera]) assert_entity_counts(hass, Platform.SWITCH, 2, 2) await adopt_devices(hass, ufp, [doorbell, unadopted_camera]) - assert_entity_counts(hass, Platform.SWITCH, 15, 13) + assert_entity_counts(hass, Platform.SWITCH, 16, 14) async def test_switch_light_remove( @@ -174,7 +175,7 @@ async def test_switch_setup_camera_all( """Test switch entity setup for camera devices (all enabled feature flags).""" await init_entry(hass, ufp, [doorbell]) - assert_entity_counts(hass, Platform.SWITCH, 15, 13) + assert_entity_counts(hass, Platform.SWITCH, 16, 14) for description in CAMERA_SWITCHES_BASIC: unique_id, entity_id = ids_from_device_description( @@ -294,7 +295,7 @@ async def test_switch_camera_ssh( """Tests SSH switch for cameras.""" await init_entry(hass, ufp, [doorbell]) - assert_entity_counts(hass, Platform.SWITCH, 15, 13) + assert_entity_counts(hass, Platform.SWITCH, 16, 14) description = CAMERA_SWITCHES[0] @@ -327,7 +328,7 @@ async def test_switch_camera_simple( """Tests all simple switches for cameras.""" await init_entry(hass, ufp, [doorbell]) - assert_entity_counts(hass, Platform.SWITCH, 15, 13) + assert_entity_counts(hass, Platform.SWITCH, 16, 14) assert description.ufp_set_method is not None @@ -356,7 +357,7 @@ async def test_switch_camera_highfps( """Tests High FPS switch for cameras.""" await init_entry(hass, ufp, [doorbell]) - assert_entity_counts(hass, Platform.SWITCH, 15, 13) + assert_entity_counts(hass, Platform.SWITCH, 16, 14) description = CAMERA_SWITCHES[3] @@ -387,7 +388,7 @@ async def test_switch_camera_privacy( previous_record = doorbell.recording_settings.mode = RecordingMode.DETECTIONS await init_entry(hass, ufp, [doorbell]) - assert_entity_counts(hass, Platform.SWITCH, 15, 13) + assert_entity_counts(hass, Platform.SWITCH, 16, 14) description = PRIVACY_MODE_SWITCH @@ -439,7 +440,7 @@ async def test_switch_camera_privacy_already_on( doorbell.add_privacy_zone() await init_entry(hass, ufp, [doorbell]) - assert_entity_counts(hass, Platform.SWITCH, 15, 13) + assert_entity_counts(hass, Platform.SWITCH, 16, 14) description = PRIVACY_MODE_SWITCH From 5f9455e0fdcf32c6f4c4028c899cdee9d4e3ebf6 Mon Sep 17 00:00:00 2001 From: tronikos Date: Mon, 10 Jun 2024 08:33:12 -0700 Subject: [PATCH 0468/1445] Log errors in Intent.async_handle (#119182) * Log errors in Intent.async_handle * log exception stack trace * Update homeassistant/helpers/intent.py --------- Co-authored-by: Paulus Schoutsen --- homeassistant/helpers/intent.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/helpers/intent.py b/homeassistant/helpers/intent.py index d7c0f90e2f9..8af5dba29f5 100644 --- a/homeassistant/helpers/intent.py +++ b/homeassistant/helpers/intent.py @@ -140,6 +140,7 @@ async def async_handle( except IntentError: raise # bubble up intent related errors except Exception as err: + _LOGGER.exception("Error handling %s", intent_type) raise IntentUnexpectedError(f"Error handling {intent_type}") from err return result From 404ff9fd692b7065275f70eb3dc86c0c640e72a2 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Tue, 11 Jun 2024 00:57:25 +0900 Subject: [PATCH 0469/1445] bump aiobotocore to 2.13.0 (#119297) bump aiobotocore --- homeassistant/components/aws/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/aws/manifest.json b/homeassistant/components/aws/manifest.json index 470ccc0e409..afc1b4c6c64 100644 --- a/homeassistant/components/aws/manifest.json +++ b/homeassistant/components/aws/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/aws", "iot_class": "cloud_push", "loggers": ["aiobotocore", "botocore"], - "requirements": ["aiobotocore==2.12.1"] + "requirements": ["aiobotocore==2.13.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 730f4c32633..2c16bd5ac19 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -210,7 +210,7 @@ aioazuredevops==2.0.0 aiobafi6==0.9.0 # homeassistant.components.aws -aiobotocore==2.12.1 +aiobotocore==2.13.0 # homeassistant.components.comelit aiocomelit==0.9.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a7fc37a5473..d62035c5431 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -189,7 +189,7 @@ aioazuredevops==2.0.0 aiobafi6==0.9.0 # homeassistant.components.aws -aiobotocore==2.12.1 +aiobotocore==2.13.0 # homeassistant.components.comelit aiocomelit==0.9.0 From d74d418c069c7a43a90cc30043edc09e3b3d9630 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 10 Jun 2024 11:58:26 -0500 Subject: [PATCH 0470/1445] Bump uiprotect to 0.4.1 (#119308) --- homeassistant/components/unifiprotect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index ba6319ab0ba..00a96483f70 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -40,7 +40,7 @@ "integration_type": "hub", "iot_class": "local_push", "loggers": ["uiprotect", "unifi_discovery"], - "requirements": ["uiprotect==0.4.0", "unifi-discovery==1.1.8"], + "requirements": ["uiprotect==0.4.1", "unifi-discovery==1.1.8"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/requirements_all.txt b/requirements_all.txt index 2c16bd5ac19..c84760b1a07 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2785,7 +2785,7 @@ twitchAPI==4.0.0 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==0.4.0 +uiprotect==0.4.1 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d62035c5431..81cab2c4617 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2165,7 +2165,7 @@ twitchAPI==4.0.0 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==0.4.0 +uiprotect==0.4.1 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 From d6bcb1c5fd30acea8c08dc6e2c88078eba8d947a Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Mon, 10 Jun 2024 19:23:12 +0200 Subject: [PATCH 0471/1445] Add HVACAction to incomfort climate devices (#119315) * Add HVACAction to incomfort climate devices * Use IDLE state when not heating --- homeassistant/components/incomfort/climate.py | 9 +++++++++ tests/components/incomfort/snapshots/test_climate.ambr | 1 + 2 files changed, 10 insertions(+) diff --git a/homeassistant/components/incomfort/climate.py b/homeassistant/components/incomfort/climate.py index 7e5cbd08f18..c55c9410f87 100644 --- a/homeassistant/components/incomfort/climate.py +++ b/homeassistant/components/incomfort/climate.py @@ -9,6 +9,7 @@ from incomfortclient import Heater as InComfortHeater, Room as InComfortRoom from homeassistant.components.climate import ( ClimateEntity, ClimateEntityFeature, + HVACAction, HVACMode, ) from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature @@ -56,6 +57,7 @@ class InComfortClimate(IncomfortEntity, ClimateEntity): """Initialize the climate device.""" super().__init__(coordinator) + self._heater = heater self._room = room self._attr_unique_id = f"{heater.serial_no}_{room.room_no}" @@ -75,6 +77,13 @@ class InComfortClimate(IncomfortEntity, ClimateEntity): """Return the current temperature.""" return self._room.room_temp + @property + def hvac_action(self) -> HVACAction | None: + """Return the actual current HVAC action.""" + if self._heater.is_burning and self._heater.is_pumping: + return HVACAction.HEATING + return HVACAction.IDLE + @property def target_temperature(self) -> float | None: """Return the temperature we try to reach.""" diff --git a/tests/components/incomfort/snapshots/test_climate.ambr b/tests/components/incomfort/snapshots/test_climate.ambr index b9a86d26139..05b2d4878d0 100644 --- a/tests/components/incomfort/snapshots/test_climate.ambr +++ b/tests/components/incomfort/snapshots/test_climate.ambr @@ -43,6 +43,7 @@ 'attributes': ReadOnlyDict({ 'current_temperature': 21.4, 'friendly_name': 'Thermostat 1', + 'hvac_action': , 'hvac_modes': list([ , ]), From b7f74532dc6d600cfc4c58b8a805e0eea640d35a Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Mon, 10 Jun 2024 19:30:12 +0200 Subject: [PATCH 0472/1445] Fix incomfort water heater return translated fault code (#119311) --- homeassistant/components/incomfort/water_heater.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/homeassistant/components/incomfort/water_heater.py b/homeassistant/components/incomfort/water_heater.py index 2295ce514b3..1c1e5d2fc8e 100644 --- a/homeassistant/components/incomfort/water_heater.py +++ b/homeassistant/components/incomfort/water_heater.py @@ -68,9 +68,6 @@ class IncomfortWaterHeater(IncomfortBoilerEntity, WaterHeaterEntity): return max(self._heater.heater_temp, self._heater.tap_temp) @property - def current_operation(self) -> str: + def current_operation(self) -> str | None: """Return the current operation mode.""" - if self._heater.is_failed: - return f"Fault code: {self._heater.fault_code}" - return self._heater.display_text From 42b984ee4f84242374a1af63e096ab4b26a88b82 Mon Sep 17 00:00:00 2001 From: Josef Zweck <24647999+zweckj@users.noreply.github.com> Date: Mon, 10 Jun 2024 19:59:39 +0200 Subject: [PATCH 0473/1445] Migrate lamarzocco to lmcloud 1.1 (#113935) * migrate to 1.1 * bump to 1.1.1 * fix newlines docstring * cleanup entity_description fns * strict generics * restructure import * tweaks to generics * tweaks to generics * removed exceptions * move initialization, websocket clean shutdown * get rid of duplicate entry addign * bump lmcloud * re-add calendar, auto on/off switches * use asdict for diagnostics * change number generator * use name as entry title * also migrate title * don't migrate title * remove generics for now * satisfy mypy * add s * adapt * migrate entry.runtime_data * remove auto/onoff * add issue on wrong gw firmware * silence mypy * remove breaks in ha version * parametrize issue test * Update update.py Co-authored-by: Joost Lekkerkerker * Update test_config_flow.py Co-authored-by: Joost Lekkerkerker * regen snapshots * mapping steam level * remove commented code * fix typo * coderabbitai availability tweak * remove microsecond moving * additonal schedule for coverage * be more specific on date offset * keep mappings the same * config_entry imports sharpened * remove unneccessary testcase, clenup date moving * remove superfluous calendar testcase from diag * guard against future version downgrade * use new entry for downgrade test * switch to lmcloud 1.1.11 * revert runtimedata * revert runtimedata * version to helper * conistent Generator * generator from typing_extensions --------- Co-authored-by: Joost Lekkerkerker --- .../components/lamarzocco/__init__.py | 148 ++++++- .../components/lamarzocco/binary_sensor.py | 12 +- homeassistant/components/lamarzocco/button.py | 8 +- .../components/lamarzocco/calendar.py | 64 ++- .../components/lamarzocco/config_flow.py | 53 ++- homeassistant/components/lamarzocco/const.py | 4 +- .../components/lamarzocco/coordinator.py | 174 ++++---- .../components/lamarzocco/diagnostics.py | 44 +- homeassistant/components/lamarzocco/entity.py | 38 +- .../components/lamarzocco/icons.json | 10 +- .../components/lamarzocco/manifest.json | 2 +- homeassistant/components/lamarzocco/number.py | 163 ++++---- homeassistant/components/lamarzocco/select.py | 74 +++- homeassistant/components/lamarzocco/sensor.py | 28 +- .../components/lamarzocco/strings.json | 11 +- homeassistant/components/lamarzocco/switch.py | 40 +- homeassistant/components/lamarzocco/update.py | 26 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/lamarzocco/__init__.py | 23 +- tests/components/lamarzocco/conftest.py | 143 ++++--- .../lamarzocco/fixtures/config.json | 77 ++-- .../lamarzocco/fixtures/current_status.json | 59 --- .../lamarzocco/fixtures/schedule.json | 44 -- .../lamarzocco/snapshots/test_calendar.ambr | 314 +++++++++++---- .../snapshots/test_diagnostics.ambr | 376 +++++------------- .../lamarzocco/snapshots/test_number.ambr | 124 +++--- .../lamarzocco/snapshots/test_select.ambr | 14 +- .../lamarzocco/snapshots/test_sensor.ambr | 8 +- .../lamarzocco/snapshots/test_switch.ambr | 192 +-------- .../lamarzocco/snapshots/test_update.ambr | 8 +- .../lamarzocco/test_binary_sensor.py | 31 +- tests/components/lamarzocco/test_calendar.py | 68 ++-- .../components/lamarzocco/test_config_flow.py | 236 ++++++----- tests/components/lamarzocco/test_init.py | 159 +++++++- tests/components/lamarzocco/test_number.py | 212 ++++++---- tests/components/lamarzocco/test_select.py | 22 +- tests/components/lamarzocco/test_switch.py | 57 +-- tests/components/lamarzocco/test_update.py | 8 +- 39 files changed, 1579 insertions(+), 1499 deletions(-) delete mode 100644 tests/components/lamarzocco/fixtures/current_status.json delete mode 100644 tests/components/lamarzocco/fixtures/schedule.json diff --git a/homeassistant/components/lamarzocco/__init__.py b/homeassistant/components/lamarzocco/__init__.py index d2a7bbb6216..e6bb3b1d3ae 100644 --- a/homeassistant/components/lamarzocco/__init__.py +++ b/homeassistant/components/lamarzocco/__init__.py @@ -1,10 +1,31 @@ """The La Marzocco integration.""" -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import Platform -from homeassistant.core import HomeAssistant +import logging -from .const import DOMAIN +from lmcloud.client_bluetooth import LaMarzoccoBluetoothClient +from lmcloud.client_cloud import LaMarzoccoCloudClient +from lmcloud.client_local import LaMarzoccoLocalClient +from lmcloud.const import BT_MODEL_PREFIXES, FirmwareType +from lmcloud.exceptions import AuthFail, RequestNotSuccessful +from packaging import version + +from homeassistant.components.bluetooth import async_discovered_service_info +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + CONF_HOST, + CONF_MAC, + CONF_MODEL, + CONF_NAME, + CONF_PASSWORD, + CONF_TOKEN, + CONF_USERNAME, + Platform, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import issue_registry as ir +from homeassistant.helpers.httpx_client import get_async_client + +from .const import CONF_USE_BLUETOOTH, DOMAIN from .coordinator import LaMarzoccoUpdateCoordinator PLATFORMS = [ @@ -18,15 +39,89 @@ PLATFORMS = [ Platform.UPDATE, ] +_LOGGER = logging.getLogger(__name__) + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up La Marzocco as config entry.""" - coordinator = LaMarzoccoUpdateCoordinator(hass) + assert entry.unique_id + serial = entry.unique_id + cloud_client = LaMarzoccoCloudClient( + username=entry.data[CONF_USERNAME], + password=entry.data[CONF_PASSWORD], + ) + + # initialize local API + local_client: LaMarzoccoLocalClient | None = None + if (host := entry.data.get(CONF_HOST)) is not None: + _LOGGER.debug("Initializing local API") + local_client = LaMarzoccoLocalClient( + host=host, + local_bearer=entry.data[CONF_TOKEN], + client=get_async_client(hass), + ) + + # initialize Bluetooth + bluetooth_client: LaMarzoccoBluetoothClient | None = None + if entry.options.get(CONF_USE_BLUETOOTH, True): + + def bluetooth_configured() -> bool: + return entry.data.get(CONF_MAC, "") and entry.data.get(CONF_NAME, "") + + if not bluetooth_configured(): + for discovery_info in async_discovered_service_info(hass): + if ( + (name := discovery_info.name) + and name.startswith(BT_MODEL_PREFIXES) + and name.split("_")[1] == serial + ): + _LOGGER.debug("Found Bluetooth device, configuring with Bluetooth") + # found a device, add MAC address to config entry + hass.config_entries.async_update_entry( + entry, + data={ + **entry.data, + CONF_MAC: discovery_info.address, + CONF_NAME: discovery_info.name, + }, + ) + break + + if bluetooth_configured(): + _LOGGER.debug("Initializing Bluetooth device") + bluetooth_client = LaMarzoccoBluetoothClient( + username=entry.data[CONF_USERNAME], + serial_number=serial, + token=entry.data[CONF_TOKEN], + address_or_ble_device=entry.data[CONF_MAC], + ) + + coordinator = LaMarzoccoUpdateCoordinator( + hass=hass, + local_client=local_client, + cloud_client=cloud_client, + bluetooth_client=bluetooth_client, + ) + + await coordinator.async_setup() await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator + gateway_version = coordinator.device.firmware[FirmwareType.GATEWAY].current_version + if version.parse(gateway_version) < version.parse("v3.5-rc5"): + # incompatible gateway firmware, create an issue + ir.async_create_issue( + hass, + DOMAIN, + "unsupported_gateway_firmware", + is_fixable=False, + severity=ir.IssueSeverity.ERROR, + translation_key="unsupported_gateway_firmware", + translation_placeholders={"gateway_version": gateway_version}, + ) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: @@ -39,10 +134,51 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) return unload_ok + + +async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Migrate config entry.""" + if entry.version > 2: + # guard against downgrade from a future version + return False + + if entry.version == 1: + cloud_client = LaMarzoccoCloudClient( + username=entry.data[CONF_USERNAME], + password=entry.data[CONF_PASSWORD], + ) + try: + fleet = await cloud_client.get_customer_fleet() + except (AuthFail, RequestNotSuccessful) as exc: + _LOGGER.error("Migration failed with error %s", exc) + return False + + assert entry.unique_id is not None + device = fleet[entry.unique_id] + v2_data = { + CONF_USERNAME: entry.data[CONF_USERNAME], + CONF_PASSWORD: entry.data[CONF_PASSWORD], + CONF_MODEL: device.model, + CONF_NAME: device.name, + CONF_TOKEN: device.communication_key, + } + + if CONF_HOST in entry.data: + v2_data[CONF_HOST] = entry.data[CONF_HOST] + + if CONF_MAC in entry.data: + v2_data[CONF_MAC] = entry.data[CONF_MAC] + + hass.config_entries.async_update_entry( + entry, + data=v2_data, + version=2, + ) + _LOGGER.debug("Migrated La Marzocco config entry to version 2") + return True diff --git a/homeassistant/components/lamarzocco/binary_sensor.py b/homeassistant/components/lamarzocco/binary_sensor.py index 0eb28fa9558..86b18888fc5 100644 --- a/homeassistant/components/lamarzocco/binary_sensor.py +++ b/homeassistant/components/lamarzocco/binary_sensor.py @@ -3,7 +3,7 @@ from collections.abc import Callable from dataclasses import dataclass -from lmcloud import LMCloud as LaMarzoccoClient +from lmcloud.models import LaMarzoccoMachineConfig from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, @@ -26,7 +26,7 @@ class LaMarzoccoBinarySensorEntityDescription( ): """Description of a La Marzocco binary sensor.""" - is_on_fn: Callable[[LaMarzoccoClient], bool] + is_on_fn: Callable[[LaMarzoccoMachineConfig], bool] ENTITIES: tuple[LaMarzoccoBinarySensorEntityDescription, ...] = ( @@ -34,7 +34,7 @@ ENTITIES: tuple[LaMarzoccoBinarySensorEntityDescription, ...] = ( key="water_tank", translation_key="water_tank", device_class=BinarySensorDeviceClass.PROBLEM, - is_on_fn=lambda lm: not lm.current_status.get("water_reservoir_contact"), + is_on_fn=lambda config: not config.water_contact, entity_category=EntityCategory.DIAGNOSTIC, supported_fn=lambda coordinator: coordinator.local_connection_configured, ), @@ -42,8 +42,8 @@ ENTITIES: tuple[LaMarzoccoBinarySensorEntityDescription, ...] = ( key="brew_active", translation_key="brew_active", device_class=BinarySensorDeviceClass.RUNNING, - is_on_fn=lambda lm: bool(lm.current_status.get("brew_active")), - available_fn=lambda lm: lm.websocket_connected, + is_on_fn=lambda config: config.brew_active, + available_fn=lambda device: device.websocket_connected, entity_category=EntityCategory.DIAGNOSTIC, ), ) @@ -72,4 +72,4 @@ class LaMarzoccoBinarySensorEntity(LaMarzoccoEntity, BinarySensorEntity): @property def is_on(self) -> bool: """Return true if the binary sensor is on.""" - return self.entity_description.is_on_fn(self.coordinator.lm) + return self.entity_description.is_on_fn(self.coordinator.device.config) diff --git a/homeassistant/components/lamarzocco/button.py b/homeassistant/components/lamarzocco/button.py index 68bae5feeb9..ec0477647d8 100644 --- a/homeassistant/components/lamarzocco/button.py +++ b/homeassistant/components/lamarzocco/button.py @@ -4,7 +4,7 @@ from collections.abc import Callable, Coroutine from dataclasses import dataclass from typing import Any -from lmcloud import LMCloud as LaMarzoccoClient +from lmcloud.lm_machine import LaMarzoccoMachine from homeassistant.components.button import ButtonEntity, ButtonEntityDescription from homeassistant.config_entries import ConfigEntry @@ -22,14 +22,14 @@ class LaMarzoccoButtonEntityDescription( ): """Description of a La Marzocco button.""" - press_fn: Callable[[LaMarzoccoClient], Coroutine[Any, Any, None]] + press_fn: Callable[[LaMarzoccoMachine], Coroutine[Any, Any, None]] ENTITIES: tuple[LaMarzoccoButtonEntityDescription, ...] = ( LaMarzoccoButtonEntityDescription( key="start_backflush", translation_key="start_backflush", - press_fn=lambda lm: lm.start_backflush(), + press_fn=lambda machine: machine.start_backflush(), ), ) @@ -56,4 +56,4 @@ class LaMarzoccoButtonEntity(LaMarzoccoEntity, ButtonEntity): async def async_press(self) -> None: """Press button.""" - await self.entity_description.press_fn(self.coordinator.lm) + await self.entity_description.press_fn(self.coordinator.device) diff --git a/homeassistant/components/lamarzocco/calendar.py b/homeassistant/components/lamarzocco/calendar.py index 2a08a90a1b2..b3a8774a1cf 100644 --- a/homeassistant/components/lamarzocco/calendar.py +++ b/homeassistant/components/lamarzocco/calendar.py @@ -3,6 +3,8 @@ from collections.abc import Iterator from datetime import datetime, timedelta +from lmcloud.models import LaMarzoccoWakeUpSleepEntry + from homeassistant.components.calendar import CalendarEntity, CalendarEvent from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -10,10 +12,21 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import dt as dt_util from .const import DOMAIN +from .coordinator import LaMarzoccoUpdateCoordinator from .entity import LaMarzoccoBaseEntity CALENDAR_KEY = "auto_on_off_schedule" +DAY_OF_WEEK = [ + "monday", + "tuesday", + "wednesday", + "thursday", + "friday", + "saturday", + "sunday", +] + async def async_setup_entry( hass: HomeAssistant, @@ -23,7 +36,10 @@ async def async_setup_entry( """Set up switch entities and services.""" coordinator = hass.data[DOMAIN][config_entry.entry_id] - async_add_entities([LaMarzoccoCalendarEntity(coordinator, CALENDAR_KEY)]) + async_add_entities( + LaMarzoccoCalendarEntity(coordinator, CALENDAR_KEY, wake_up_sleep_entry) + for wake_up_sleep_entry in coordinator.device.config.wake_up_sleep_entries.values() + ) class LaMarzoccoCalendarEntity(LaMarzoccoBaseEntity, CalendarEntity): @@ -31,6 +47,17 @@ class LaMarzoccoCalendarEntity(LaMarzoccoBaseEntity, CalendarEntity): _attr_translation_key = CALENDAR_KEY + def __init__( + self, + coordinator: LaMarzoccoUpdateCoordinator, + key: str, + wake_up_sleep_entry: LaMarzoccoWakeUpSleepEntry, + ) -> None: + """Set up calendar.""" + super().__init__(coordinator, f"{key}_{wake_up_sleep_entry.entry_id}") + self.wake_up_sleep_entry = wake_up_sleep_entry + self._attr_translation_placeholders = {"id": wake_up_sleep_entry.entry_id} + @property def event(self) -> CalendarEvent | None: """Return the next upcoming event.""" @@ -85,29 +112,36 @@ class LaMarzoccoCalendarEntity(LaMarzoccoBaseEntity, CalendarEntity): """Return calendar event for a given weekday.""" # check first if auto/on off is turned on in general - # because could still be on for that day but disabled - if self.coordinator.lm.current_status["global_auto"] != "Enabled": + if not self.wake_up_sleep_entry.enabled: return None # parse the schedule for the day - schedule_day = self.coordinator.lm.schedule[date.weekday()] - if schedule_day["enable"] == "Disabled": + + if DAY_OF_WEEK[date.weekday()] not in self.wake_up_sleep_entry.days: return None - hour_on, minute_on = schedule_day["on"].split(":") - hour_off, minute_off = schedule_day["off"].split(":") + + hour_on, minute_on = self.wake_up_sleep_entry.time_on.split(":") + hour_off, minute_off = self.wake_up_sleep_entry.time_off.split(":") + + # if off time is 24:00, then it means the off time is the next day + # only for legacy schedules + day_offset = 0 + if hour_off == "24": + day_offset = 1 + hour_off = "0" + + end_date = date.replace( + hour=int(hour_off), + minute=int(minute_off), + ) + end_date += timedelta(days=day_offset) + return CalendarEvent( start=date.replace( hour=int(hour_on), minute=int(minute_on), - second=0, - microsecond=0, - ), - end=date.replace( - hour=int(hour_off), - minute=int(minute_off), - second=0, - microsecond=0, ), + end=end_date, summary=f"Machine {self.coordinator.config_entry.title} on", description="Machine is scheduled to turn on at the start time and off at the end time", ) diff --git a/homeassistant/components/lamarzocco/config_flow.py b/homeassistant/components/lamarzocco/config_flow.py index 3cacdae1749..b4fed615733 100644 --- a/homeassistant/components/lamarzocco/config_flow.py +++ b/homeassistant/components/lamarzocco/config_flow.py @@ -4,8 +4,10 @@ from collections.abc import Mapping import logging from typing import Any -from lmcloud import LMCloud as LaMarzoccoClient +from lmcloud.client_cloud import LaMarzoccoCloudClient +from lmcloud.client_local import LaMarzoccoLocalClient from lmcloud.exceptions import AuthFail, RequestNotSuccessful +from lmcloud.models import LaMarzoccoDeviceInfo import voluptuous as vol from homeassistant.components.bluetooth import BluetoothServiceInfo @@ -19,12 +21,15 @@ from homeassistant.config_entries import ( from homeassistant.const import ( CONF_HOST, CONF_MAC, + CONF_MODEL, CONF_NAME, CONF_PASSWORD, + CONF_TOKEN, CONF_USERNAME, ) from homeassistant.core import callback from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.httpx_client import get_async_client from homeassistant.helpers.selector import ( SelectOptionDict, SelectSelector, @@ -32,7 +37,9 @@ from homeassistant.helpers.selector import ( SelectSelectorMode, ) -from .const import CONF_MACHINE, CONF_USE_BLUETOOTH, DOMAIN +from .const import CONF_USE_BLUETOOTH, DOMAIN + +CONF_MACHINE = "machine" _LOGGER = logging.getLogger(__name__) @@ -40,12 +47,14 @@ _LOGGER = logging.getLogger(__name__) class LmConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for La Marzocco.""" + VERSION = 2 + def __init__(self) -> None: """Initialize the config flow.""" self.reauth_entry: ConfigEntry | None = None self._config: dict[str, Any] = {} - self._machines: list[tuple[str, str]] = [] + self._fleet: dict[str, LaMarzoccoDeviceInfo] = {} self._discovered: dict[str, str] = {} async def async_step_user( @@ -65,9 +74,12 @@ class LmConfigFlow(ConfigFlow, domain=DOMAIN): **self._discovered, } - lm = LaMarzoccoClient() + cloud_client = LaMarzoccoCloudClient( + username=data[CONF_USERNAME], + password=data[CONF_PASSWORD], + ) try: - self._machines = await lm.get_all_machines(data) + self._fleet = await cloud_client.get_customer_fleet() except AuthFail: _LOGGER.debug("Server rejected login credentials") errors["base"] = "invalid_auth" @@ -75,7 +87,7 @@ class LmConfigFlow(ConfigFlow, domain=DOMAIN): _LOGGER.error("Error connecting to server: %s", exc) errors["base"] = "cannot_connect" else: - if not self._machines: + if not self._fleet: errors["base"] = "no_machines" if not errors: @@ -88,8 +100,7 @@ class LmConfigFlow(ConfigFlow, domain=DOMAIN): ) return self.async_abort(reason="reauth_successful") if self._discovered: - serials = [machine[0] for machine in self._machines] - if self._discovered[CONF_MACHINE] not in serials: + if self._discovered[CONF_MACHINE] not in self._fleet: errors["base"] = "machine_not_found" else: self._config = data @@ -128,28 +139,36 @@ class LmConfigFlow(ConfigFlow, domain=DOMAIN): else: serial_number = self._discovered[CONF_MACHINE] + selected_device = self._fleet[serial_number] + # validate local connection if host is provided if user_input.get(CONF_HOST): - lm = LaMarzoccoClient() - if not await lm.check_local_connection( - credentials=self._config, + if not await LaMarzoccoLocalClient.validate_connection( + client=get_async_client(self.hass), host=user_input[CONF_HOST], - serial=serial_number, + token=selected_device.communication_key, ): errors[CONF_HOST] = "cannot_connect" + else: + self._config[CONF_HOST] = user_input[CONF_HOST] if not errors: return self.async_create_entry( - title=serial_number, - data=self._config | user_input, + title=selected_device.name, + data={ + **self._config, + CONF_NAME: selected_device.name, + CONF_MODEL: selected_device.model, + CONF_TOKEN: selected_device.communication_key, + }, ) machine_options = [ SelectOptionDict( - value=serial_number, - label=f"{model_name} ({serial_number})", + value=device.serial_number, + label=f"{device.model} ({device.serial_number})", ) - for serial_number, model_name in self._machines + for device in self._fleet.values() ] machine_selection_schema = vol.Schema( diff --git a/homeassistant/components/lamarzocco/const.py b/homeassistant/components/lamarzocco/const.py index 87878ea5089..57db84f94da 100644 --- a/homeassistant/components/lamarzocco/const.py +++ b/homeassistant/components/lamarzocco/const.py @@ -4,6 +4,4 @@ from typing import Final DOMAIN: Final = "lamarzocco" -CONF_MACHINE: Final = "machine" - -CONF_USE_BLUETOOTH = "use_bluetooth" +CONF_USE_BLUETOOTH: Final = "use_bluetooth" diff --git a/homeassistant/components/lamarzocco/coordinator.py b/homeassistant/components/lamarzocco/coordinator.py index c26e981208d..2c78a925ca4 100644 --- a/homeassistant/components/lamarzocco/coordinator.py +++ b/homeassistant/components/lamarzocco/coordinator.py @@ -3,133 +3,108 @@ from collections.abc import Callable, Coroutine from datetime import timedelta import logging +from time import time +from typing import Any -from bleak.backends.device import BLEDevice -from lmcloud import LMCloud as LaMarzoccoClient -from lmcloud.const import BT_MODEL_NAMES +from lmcloud.client_bluetooth import LaMarzoccoBluetoothClient +from lmcloud.client_cloud import LaMarzoccoCloudClient +from lmcloud.client_local import LaMarzoccoLocalClient from lmcloud.exceptions import AuthFail, RequestNotSuccessful +from lmcloud.lm_machine import LaMarzoccoMachine -from homeassistant.components.bluetooth import ( - async_ble_device_from_address, - async_discovered_service_info, -) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, CONF_USERNAME +from homeassistant.const import CONF_MODEL, CONF_NAME, EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed -from homeassistant.helpers.httpx_client import get_async_client from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from .const import CONF_MACHINE, CONF_USE_BLUETOOTH, DOMAIN +from .const import DOMAIN SCAN_INTERVAL = timedelta(seconds=30) +FIRMWARE_UPDATE_INTERVAL = 3600 +STATISTICS_UPDATE_INTERVAL = 300 _LOGGER = logging.getLogger(__name__) -NAME_PREFIXES = tuple(BT_MODEL_NAMES) - - class LaMarzoccoUpdateCoordinator(DataUpdateCoordinator[None]): """Class to handle fetching data from the La Marzocco API centrally.""" config_entry: ConfigEntry - def __init__(self, hass: HomeAssistant) -> None: + def __init__( + self, + hass: HomeAssistant, + cloud_client: LaMarzoccoCloudClient, + local_client: LaMarzoccoLocalClient | None, + bluetooth_client: LaMarzoccoBluetoothClient | None, + ) -> None: """Initialize coordinator.""" super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=SCAN_INTERVAL) - self.lm = LaMarzoccoClient( - callback_websocket_notify=self.async_update_listeners, - ) - self.local_connection_configured = ( - self.config_entry.data.get(CONF_HOST) is not None - ) - self._use_bluetooth = False + self.local_connection_configured = local_client is not None - async def _async_update_data(self) -> None: - """Fetch data from API endpoint.""" - - if not self.lm.initialized: - await self._async_init_client() - - await self._async_handle_request( - self.lm.update_local_machine_status, force_update=True + assert self.config_entry.unique_id + self.device = LaMarzoccoMachine( + model=self.config_entry.data[CONF_MODEL], + serial_number=self.config_entry.unique_id, + name=self.config_entry.data[CONF_NAME], + cloud_client=cloud_client, + local_client=local_client, + bluetooth_client=bluetooth_client, ) - _LOGGER.debug("Current status: %s", str(self.lm.current_status)) + self._last_firmware_data_update: float | None = None + self._last_statistics_data_update: float | None = None + self._local_client = local_client - async def _async_init_client(self) -> None: - """Initialize the La Marzocco Client.""" - - # Initialize cloud API - _LOGGER.debug("Initializing Cloud API") - await self._async_handle_request( - self.lm.init_cloud_api, - credentials=self.config_entry.data, - machine_serial=self.config_entry.data[CONF_MACHINE], - ) - _LOGGER.debug("Model name: %s", self.lm.model_name) - - # initialize local API - if (host := self.config_entry.data.get(CONF_HOST)) is not None: - _LOGGER.debug("Initializing local API") - await self.lm.init_local_api( - host=host, - client=get_async_client(self.hass), - ) - - _LOGGER.debug("Init WebSocket in Background Task") + async def async_setup(self) -> None: + """Set up the coordinator.""" + if self._local_client is not None: + _LOGGER.debug("Init WebSocket in background task") self.config_entry.async_create_background_task( hass=self.hass, - target=self.lm.lm_local_api.websocket_connect( - callback=self.lm.on_websocket_message_received, - use_sigterm_handler=False, + target=self.device.websocket_connect( + notify_callback=lambda: self.async_set_updated_data(None) ), name="lm_websocket_task", ) - # initialize Bluetooth - if self.config_entry.options.get(CONF_USE_BLUETOOTH, True): + async def websocket_close(_: Any | None = None) -> None: + if ( + self._local_client is not None + and self._local_client.websocket is not None + and self._local_client.websocket.open + ): + self._local_client.terminating = True + await self._local_client.websocket.close() - def bluetooth_configured() -> bool: - return self.config_entry.data.get( - CONF_MAC, "" - ) and self.config_entry.data.get(CONF_NAME, "") - - if not bluetooth_configured(): - machine = self.config_entry.data[CONF_MACHINE] - for discovery_info in async_discovered_service_info(self.hass): - if ( - (name := discovery_info.name) - and name.startswith(NAME_PREFIXES) - and name.split("_")[1] == machine - ): - _LOGGER.debug( - "Found Bluetooth device, configuring with Bluetooth" - ) - # found a device, add MAC address to config entry - self.hass.config_entries.async_update_entry( - self.config_entry, - data={ - **self.config_entry.data, - CONF_MAC: discovery_info.address, - CONF_NAME: discovery_info.name, - }, - ) - break - - if bluetooth_configured(): - # config entry contains BT config - _LOGGER.debug("Initializing with known Bluetooth device") - await self.lm.init_bluetooth_with_known_device( - self.config_entry.data[CONF_USERNAME], - self.config_entry.data.get(CONF_MAC, ""), - self.config_entry.data.get(CONF_NAME, ""), + self.config_entry.async_on_unload( + self.hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, websocket_close ) - self._use_bluetooth = True + ) + self.config_entry.async_on_unload(websocket_close) - self.lm.initialized = True + async def _async_update_data(self) -> None: + """Fetch data from API endpoint.""" + await self._async_handle_request(self.device.get_config) + + if ( + self._last_firmware_data_update is None + or (self._last_firmware_data_update + FIRMWARE_UPDATE_INTERVAL) < time() + ): + await self._async_handle_request(self.device.get_firmware) + self._last_firmware_data_update = time() + + if ( + self._last_statistics_data_update is None + or (self._last_statistics_data_update + STATISTICS_UPDATE_INTERVAL) < time() + ): + await self._async_handle_request(self.device.get_statistics) + self._last_statistics_data_update = time() + + _LOGGER.debug("Current status: %s", str(self.device.config)) async def _async_handle_request[**_P]( self, @@ -137,9 +112,8 @@ class LaMarzoccoUpdateCoordinator(DataUpdateCoordinator[None]): *args: _P.args, **kwargs: _P.kwargs, ) -> None: - """Handle a request to the API.""" try: - await func(*args, **kwargs) + await func() except AuthFail as ex: msg = "Authentication failed." _LOGGER.debug(msg, exc_info=True) @@ -147,15 +121,3 @@ class LaMarzoccoUpdateCoordinator(DataUpdateCoordinator[None]): except RequestNotSuccessful as ex: _LOGGER.debug(ex, exc_info=True) raise UpdateFailed(f"Querying API failed. Error: {ex}") from ex - - def async_get_ble_device(self) -> BLEDevice | None: - """Get a Bleak Client for the machine.""" - # according to HA best practices, we should not reuse the same client - # get a new BLE device from hass and init a new Bleak Client with it - if not self._use_bluetooth: - return None - - return async_ble_device_from_address( - self.hass, - self.lm.lm_bluetooth.address, - ) diff --git a/homeassistant/components/lamarzocco/diagnostics.py b/homeassistant/components/lamarzocco/diagnostics.py index 648d1357a35..04aed25defe 100644 --- a/homeassistant/components/lamarzocco/diagnostics.py +++ b/homeassistant/components/lamarzocco/diagnostics.py @@ -2,7 +2,10 @@ from __future__ import annotations -from typing import Any +from dataclasses import asdict +from typing import Any, TypedDict + +from lmcloud.const import FirmwareType from homeassistant.components.diagnostics import async_redact_data from homeassistant.config_entries import ConfigEntry @@ -13,31 +16,30 @@ from .coordinator import LaMarzoccoUpdateCoordinator TO_REDACT = { "serial_number", - "machine_sn", } +class DiagnosticsData(TypedDict): + """Diagnostic data for La Marzocco.""" + + model: str + config: dict[str, Any] + firmware: list[dict[FirmwareType, dict[str, Any]]] + statistics: dict[str, Any] + + async def async_get_config_entry_diagnostics( - hass: HomeAssistant, entry: ConfigEntry + hass: HomeAssistant, config_entry: ConfigEntry ) -> dict[str, Any]: """Return diagnostics for a config entry.""" - coordinator: LaMarzoccoUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + coordinator: LaMarzoccoUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + device = coordinator.device # collect all data sources - data = {} - data["current_status"] = coordinator.lm.current_status - data["machine_info"] = coordinator.lm.machine_info - data["config"] = coordinator.lm.config - data["statistics"] = {"stats": coordinator.lm.statistics} # wrap to satisfy mypy + diagnostics_data = DiagnosticsData( + model=device.model, + config=asdict(device.config), + firmware=[{key: asdict(firmware)} for key, firmware in device.firmware.items()], + statistics=asdict(device.statistics), + ) - # build a firmware section - data["firmware"] = { - "machine": { - "version": coordinator.lm.firmware_version, - "latest_version": coordinator.lm.latest_firmware_version, - }, - "gateway": { - "version": coordinator.lm.gateway_version, - "latest_version": coordinator.lm.latest_gateway_version, - }, - } - return async_redact_data(data, TO_REDACT) + return async_redact_data(diagnostics_data, TO_REDACT) diff --git a/homeassistant/components/lamarzocco/entity.py b/homeassistant/components/lamarzocco/entity.py index 4cb9d4a580a..9cc2ce8ef6b 100644 --- a/homeassistant/components/lamarzocco/entity.py +++ b/homeassistant/components/lamarzocco/entity.py @@ -3,7 +3,8 @@ from collections.abc import Callable from dataclasses import dataclass -from lmcloud import LMCloud as LaMarzoccoClient +from lmcloud.const import FirmwareType +from lmcloud.lm_machine import LaMarzoccoMachine from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity import EntityDescription @@ -17,11 +18,13 @@ from .coordinator import LaMarzoccoUpdateCoordinator class LaMarzoccoEntityDescription(EntityDescription): """Description for all LM entities.""" - available_fn: Callable[[LaMarzoccoClient], bool] = lambda _: True + available_fn: Callable[[LaMarzoccoMachine], bool] = lambda _: True supported_fn: Callable[[LaMarzoccoUpdateCoordinator], bool] = lambda _: True -class LaMarzoccoBaseEntity(CoordinatorEntity[LaMarzoccoUpdateCoordinator]): +class LaMarzoccoBaseEntity( + CoordinatorEntity[LaMarzoccoUpdateCoordinator], +): """Common elements for all entities.""" _attr_has_entity_name = True @@ -33,15 +36,15 @@ class LaMarzoccoBaseEntity(CoordinatorEntity[LaMarzoccoUpdateCoordinator]): ) -> None: """Initialize the entity.""" super().__init__(coordinator) - lm = coordinator.lm - self._attr_unique_id = f"{lm.serial_number}_{key}" + device = coordinator.device + self._attr_unique_id = f"{device.serial_number}_{key}" self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, lm.serial_number)}, - name=lm.machine_name, + identifiers={(DOMAIN, device.serial_number)}, + name=device.name, manufacturer="La Marzocco", - model=lm.true_model_name, - serial_number=lm.serial_number, - sw_version=lm.firmware_version, + model=device.full_model_name, + serial_number=device.serial_number, + sw_version=device.firmware[FirmwareType.MACHINE].current_version, ) @@ -50,19 +53,18 @@ class LaMarzoccoEntity(LaMarzoccoBaseEntity): entity_description: LaMarzoccoEntityDescription + @property + def available(self) -> bool: + """Return True if entity is available.""" + if super().available: + return self.entity_description.available_fn(self.coordinator.device) + return False + def __init__( self, coordinator: LaMarzoccoUpdateCoordinator, entity_description: LaMarzoccoEntityDescription, ) -> None: """Initialize the entity.""" - super().__init__(coordinator, entity_description.key) self.entity_description = entity_description - - @property - def available(self) -> bool: - """Return True if entity is available.""" - return super().available and self.entity_description.available_fn( - self.coordinator.lm - ) diff --git a/homeassistant/components/lamarzocco/icons.json b/homeassistant/components/lamarzocco/icons.json index 727d3c66009..965ee7e3c3f 100644 --- a/homeassistant/components/lamarzocco/icons.json +++ b/homeassistant/components/lamarzocco/icons.json @@ -26,10 +26,7 @@ "default": "mdi:thermometer-water" }, "dose": { - "default": "mdi:weight-kilogram" - }, - "steam_temp": { - "default": "mdi:thermometer-water" + "default": "mdi:cup-water" }, "prebrew_off": { "default": "mdi:water-off" @@ -40,6 +37,9 @@ "preinfusion_off": { "default": "mdi:water" }, + "steam_temp": { + "default": "mdi:thermometer-water" + }, "tea_water_duration": { "default": "mdi:timer-sand" } @@ -58,7 +58,7 @@ "state": { "disabled": "mdi:water-pump-off", "prebrew": "mdi:water-pump", - "preinfusion": "mdi:water-pump" + "typeb": "mdi:water-pump" } } }, diff --git a/homeassistant/components/lamarzocco/manifest.json b/homeassistant/components/lamarzocco/manifest.json index ec6068e1988..7714b13d12b 100644 --- a/homeassistant/components/lamarzocco/manifest.json +++ b/homeassistant/components/lamarzocco/manifest.json @@ -22,5 +22,5 @@ "integration_type": "device", "iot_class": "cloud_polling", "loggers": ["lmcloud"], - "requirements": ["lmcloud==0.4.35"] + "requirements": ["lmcloud==1.1.11"] } diff --git a/homeassistant/components/lamarzocco/number.py b/homeassistant/components/lamarzocco/number.py index af5256bc77b..89bb5e75dd2 100644 --- a/homeassistant/components/lamarzocco/number.py +++ b/homeassistant/components/lamarzocco/number.py @@ -4,8 +4,15 @@ from collections.abc import Callable, Coroutine from dataclasses import dataclass from typing import Any -from lmcloud import LMCloud as LaMarzoccoClient -from lmcloud.const import KEYS_PER_MODEL, LaMarzoccoModel +from lmcloud.const import ( + KEYS_PER_MODEL, + BoilerType, + MachineModel, + PhysicalKey, + PrebrewMode, +) +from lmcloud.lm_machine import LaMarzoccoMachine +from lmcloud.models import LaMarzoccoMachineConfig from homeassistant.components.number import ( NumberDeviceClass, @@ -35,10 +42,8 @@ class LaMarzoccoNumberEntityDescription( ): """Description of a La Marzocco number entity.""" - native_value_fn: Callable[[LaMarzoccoClient], float | int] - set_value_fn: Callable[ - [LaMarzoccoUpdateCoordinator, float | int], Coroutine[Any, Any, bool] - ] + native_value_fn: Callable[[LaMarzoccoMachineConfig], float | int] + set_value_fn: Callable[[LaMarzoccoMachine, float | int], Coroutine[Any, Any, bool]] @dataclass(frozen=True, kw_only=True) @@ -48,9 +53,9 @@ class LaMarzoccoKeyNumberEntityDescription( ): """Description of an La Marzocco number entity with keys.""" - native_value_fn: Callable[[LaMarzoccoClient, int], float | int] + native_value_fn: Callable[[LaMarzoccoMachineConfig, PhysicalKey], float | int] set_value_fn: Callable[ - [LaMarzoccoClient, float | int, int], Coroutine[Any, Any, bool] + [LaMarzoccoMachine, float | int, PhysicalKey], Coroutine[Any, Any, bool] ] @@ -63,10 +68,10 @@ ENTITIES: tuple[LaMarzoccoNumberEntityDescription, ...] = ( native_step=PRECISION_TENTHS, native_min_value=85, native_max_value=104, - set_value_fn=lambda coordinator, temp: coordinator.lm.set_coffee_temp( - temp, coordinator.async_get_ble_device() - ), - native_value_fn=lambda lm: lm.current_status["coffee_set_temp"], + set_value_fn=lambda machine, temp: machine.set_temp(BoilerType.COFFEE, temp), + native_value_fn=lambda config: config.boilers[ + BoilerType.COFFEE + ].target_temperature, ), LaMarzoccoNumberEntityDescription( key="steam_temp", @@ -76,14 +81,14 @@ ENTITIES: tuple[LaMarzoccoNumberEntityDescription, ...] = ( native_step=PRECISION_WHOLE, native_min_value=126, native_max_value=131, - set_value_fn=lambda coordinator, temp: coordinator.lm.set_steam_temp( - int(temp), coordinator.async_get_ble_device() - ), - native_value_fn=lambda lm: lm.current_status["steam_set_temp"], - supported_fn=lambda coordinator: coordinator.lm.model_name + set_value_fn=lambda machine, temp: machine.set_temp(BoilerType.STEAM, temp), + native_value_fn=lambda config: config.boilers[ + BoilerType.STEAM + ].target_temperature, + supported_fn=lambda coordinator: coordinator.device.model in ( - LaMarzoccoModel.GS3_AV, - LaMarzoccoModel.GS3_MP, + MachineModel.GS3_AV, + MachineModel.GS3_MP, ), ), LaMarzoccoNumberEntityDescription( @@ -94,54 +99,17 @@ ENTITIES: tuple[LaMarzoccoNumberEntityDescription, ...] = ( native_step=PRECISION_WHOLE, native_min_value=0, native_max_value=30, - set_value_fn=lambda coordinator, value: coordinator.lm.set_dose_hot_water( - value=int(value) - ), - native_value_fn=lambda lm: lm.current_status["dose_hot_water"], - supported_fn=lambda coordinator: coordinator.lm.model_name + set_value_fn=lambda machine, value: machine.set_dose_tea_water(int(value)), + native_value_fn=lambda config: config.dose_hot_water, + supported_fn=lambda coordinator: coordinator.device.model in ( - LaMarzoccoModel.GS3_AV, - LaMarzoccoModel.GS3_MP, + MachineModel.GS3_AV, + MachineModel.GS3_MP, ), ), ) -async def _set_prebrew_on( - lm: LaMarzoccoClient, - value: float, - key: int, -) -> bool: - return await lm.configure_prebrew( - on_time=int(value * 1000), - off_time=int(lm.current_status[f"prebrewing_toff_k{key}"] * 1000), - key=key, - ) - - -async def _set_prebrew_off( - lm: LaMarzoccoClient, - value: float, - key: int, -) -> bool: - return await lm.configure_prebrew( - on_time=int(lm.current_status[f"prebrewing_ton_k{key}"] * 1000), - off_time=int(value * 1000), - key=key, - ) - - -async def _set_preinfusion( - lm: LaMarzoccoClient, - value: float, - key: int, -) -> bool: - return await lm.configure_prebrew( - off_time=int(value * 1000), - key=key, - ) - - KEY_ENTITIES: tuple[LaMarzoccoKeyNumberEntityDescription, ...] = ( LaMarzoccoKeyNumberEntityDescription( key="prebrew_off", @@ -152,11 +120,14 @@ KEY_ENTITIES: tuple[LaMarzoccoKeyNumberEntityDescription, ...] = ( native_min_value=1, native_max_value=10, entity_category=EntityCategory.CONFIG, - set_value_fn=_set_prebrew_off, - native_value_fn=lambda lm, key: lm.current_status[f"prebrewing_ton_k{key}"], - available_fn=lambda lm: lm.current_status["enable_prebrewing"], - supported_fn=lambda coordinator: coordinator.lm.model_name - != LaMarzoccoModel.GS3_MP, + set_value_fn=lambda machine, value, key: machine.set_prebrew_time( + prebrew_off_time=value, key=key + ), + native_value_fn=lambda config, key: config.prebrew_configuration[key].off_time, + available_fn=lambda device: len(device.config.prebrew_configuration) > 0 + and device.config.prebrew_mode == PrebrewMode.PREBREW, + supported_fn=lambda coordinator: coordinator.device.model + != MachineModel.GS3_MP, ), LaMarzoccoKeyNumberEntityDescription( key="prebrew_on", @@ -167,11 +138,14 @@ KEY_ENTITIES: tuple[LaMarzoccoKeyNumberEntityDescription, ...] = ( native_min_value=2, native_max_value=10, entity_category=EntityCategory.CONFIG, - set_value_fn=_set_prebrew_on, - native_value_fn=lambda lm, key: lm.current_status[f"prebrewing_toff_k{key}"], - available_fn=lambda lm: lm.current_status["enable_prebrewing"], - supported_fn=lambda coordinator: coordinator.lm.model_name - != LaMarzoccoModel.GS3_MP, + set_value_fn=lambda machine, value, key: machine.set_prebrew_time( + prebrew_on_time=value, key=key + ), + native_value_fn=lambda config, key: config.prebrew_configuration[key].off_time, + available_fn=lambda device: len(device.config.prebrew_configuration) > 0 + and device.config.prebrew_mode == PrebrewMode.PREBREW, + supported_fn=lambda coordinator: coordinator.device.model + != MachineModel.GS3_MP, ), LaMarzoccoKeyNumberEntityDescription( key="preinfusion_off", @@ -182,11 +156,16 @@ KEY_ENTITIES: tuple[LaMarzoccoKeyNumberEntityDescription, ...] = ( native_min_value=2, native_max_value=29, entity_category=EntityCategory.CONFIG, - set_value_fn=_set_preinfusion, - native_value_fn=lambda lm, key: lm.current_status[f"preinfusion_k{key}"], - available_fn=lambda lm: lm.current_status["enable_preinfusion"], - supported_fn=lambda coordinator: coordinator.lm.model_name - != LaMarzoccoModel.GS3_MP, + set_value_fn=lambda machine, value, key: machine.set_preinfusion_time( + preinfusion_time=value, key=key + ), + native_value_fn=lambda config, key: config.prebrew_configuration[ + key + ].preinfusion_time, + available_fn=lambda device: len(device.config.prebrew_configuration) > 0 + and device.config.prebrew_mode == PrebrewMode.PREINFUSION, + supported_fn=lambda coordinator: coordinator.device.model + != MachineModel.GS3_MP, ), LaMarzoccoKeyNumberEntityDescription( key="dose", @@ -196,10 +175,12 @@ KEY_ENTITIES: tuple[LaMarzoccoKeyNumberEntityDescription, ...] = ( native_min_value=0, native_max_value=999, entity_category=EntityCategory.CONFIG, - set_value_fn=lambda lm, ticks, key: lm.set_dose(key=key, value=int(ticks)), - native_value_fn=lambda lm, key: lm.current_status[f"dose_k{key}"], - supported_fn=lambda coordinator: coordinator.lm.model_name - == LaMarzoccoModel.GS3_AV, + set_value_fn=lambda machine, ticks, key: machine.set_dose( + dose=int(ticks), key=key + ), + native_value_fn=lambda config, key: config.doses[key], + supported_fn=lambda coordinator: coordinator.device.model + == MachineModel.GS3_AV, ), ) @@ -211,7 +192,6 @@ async def async_setup_entry( ) -> None: """Set up number entities.""" coordinator = hass.data[DOMAIN][config_entry.entry_id] - entities: list[NumberEntity] = [ LaMarzoccoNumberEntity(coordinator, description) for description in ENTITIES @@ -220,12 +200,11 @@ async def async_setup_entry( for description in KEY_ENTITIES: if description.supported_fn(coordinator): - num_keys = KEYS_PER_MODEL[coordinator.lm.model_name] + num_keys = KEYS_PER_MODEL[MachineModel(coordinator.device.model)] entities.extend( LaMarzoccoKeyNumberEntity(coordinator, description, key) for key in range(min(num_keys, 1), num_keys + 1) ) - async_add_entities(entities) @@ -237,12 +216,13 @@ class LaMarzoccoNumberEntity(LaMarzoccoEntity, NumberEntity): @property def native_value(self) -> float: """Return the current value.""" - return self.entity_description.native_value_fn(self.coordinator.lm) + return self.entity_description.native_value_fn(self.coordinator.device.config) async def async_set_native_value(self, value: float) -> None: """Set the value.""" - await self.entity_description.set_value_fn(self.coordinator, value) - self.async_write_ha_state() + if value != self.native_value: + await self.entity_description.set_value_fn(self.coordinator.device, value) + self.async_write_ha_state() class LaMarzoccoKeyNumberEntity(LaMarzoccoEntity, NumberEntity): @@ -273,12 +253,13 @@ class LaMarzoccoKeyNumberEntity(LaMarzoccoEntity, NumberEntity): def native_value(self) -> float: """Return the current value.""" return self.entity_description.native_value_fn( - self.coordinator.lm, self.pyhsical_key + self.coordinator.device.config, PhysicalKey(self.pyhsical_key) ) async def async_set_native_value(self, value: float) -> None: """Set the value.""" - await self.entity_description.set_value_fn( - self.coordinator.lm, value, self.pyhsical_key - ) - self.async_write_ha_state() + if value != self.native_value: + await self.entity_description.set_value_fn( + self.coordinator.device, value, PhysicalKey(self.pyhsical_key) + ) + self.async_write_ha_state() diff --git a/homeassistant/components/lamarzocco/select.py b/homeassistant/components/lamarzocco/select.py index f063f8e6336..4e202db7c7c 100644 --- a/homeassistant/components/lamarzocco/select.py +++ b/homeassistant/components/lamarzocco/select.py @@ -4,18 +4,43 @@ from collections.abc import Callable, Coroutine from dataclasses import dataclass from typing import Any -from lmcloud import LMCloud as LaMarzoccoClient -from lmcloud.const import LaMarzoccoModel +from lmcloud.const import MachineModel, PrebrewMode, SteamLevel +from lmcloud.lm_machine import LaMarzoccoMachine +from lmcloud.models import LaMarzoccoMachineConfig from homeassistant.components.select import SelectEntity, SelectEntityDescription from homeassistant.config_entries import ConfigEntry +from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN -from .coordinator import LaMarzoccoUpdateCoordinator from .entity import LaMarzoccoEntity, LaMarzoccoEntityDescription +STEAM_LEVEL_HA_TO_LM = { + "1": SteamLevel.LEVEL_1, + "2": SteamLevel.LEVEL_2, + "3": SteamLevel.LEVEL_3, +} + +STEAM_LEVEL_LM_TO_HA = { + SteamLevel.LEVEL_1: "1", + SteamLevel.LEVEL_2: "2", + SteamLevel.LEVEL_3: "3", +} + +PREBREW_MODE_HA_TO_LM = { + "disabled": PrebrewMode.DISABLED, + "prebrew": PrebrewMode.PREBREW, + "preinfusion": PrebrewMode.PREINFUSION, +} + +PREBREW_MODE_LM_TO_HA = { + PrebrewMode.DISABLED: "disabled", + PrebrewMode.PREBREW: "prebrew", + PrebrewMode.PREINFUSION: "preinfusion", +} + @dataclass(frozen=True, kw_only=True) class LaMarzoccoSelectEntityDescription( @@ -24,10 +49,8 @@ class LaMarzoccoSelectEntityDescription( ): """Description of a La Marzocco select entity.""" - current_option_fn: Callable[[LaMarzoccoClient], str] - select_option_fn: Callable[ - [LaMarzoccoUpdateCoordinator, str], Coroutine[Any, Any, bool] - ] + current_option_fn: Callable[[LaMarzoccoMachineConfig], str] + select_option_fn: Callable[[LaMarzoccoMachine, str], Coroutine[Any, Any, bool]] ENTITIES: tuple[LaMarzoccoSelectEntityDescription, ...] = ( @@ -35,25 +58,27 @@ ENTITIES: tuple[LaMarzoccoSelectEntityDescription, ...] = ( key="steam_temp_select", translation_key="steam_temp_select", options=["1", "2", "3"], - select_option_fn=lambda coordinator, option: coordinator.lm.set_steam_level( - int(option), coordinator.async_get_ble_device() + select_option_fn=lambda machine, option: machine.set_steam_level( + STEAM_LEVEL_HA_TO_LM[option] ), - current_option_fn=lambda lm: lm.current_status["steam_level_set"], - supported_fn=lambda coordinator: coordinator.lm.model_name - == LaMarzoccoModel.LINEA_MICRA, + current_option_fn=lambda config: STEAM_LEVEL_LM_TO_HA[config.steam_level], + supported_fn=lambda coordinator: coordinator.device.model + == MachineModel.LINEA_MICRA, ), LaMarzoccoSelectEntityDescription( key="prebrew_infusion_select", translation_key="prebrew_infusion_select", + entity_category=EntityCategory.CONFIG, options=["disabled", "prebrew", "preinfusion"], - select_option_fn=lambda coordinator, - option: coordinator.lm.select_pre_brew_infusion_mode(option.capitalize()), - current_option_fn=lambda lm: lm.pre_brew_infusion_mode.lower(), - supported_fn=lambda coordinator: coordinator.lm.model_name + select_option_fn=lambda machine, option: machine.set_prebrew_mode( + PREBREW_MODE_HA_TO_LM[option] + ), + current_option_fn=lambda config: PREBREW_MODE_LM_TO_HA[config.prebrew_mode], + supported_fn=lambda coordinator: coordinator.device.model in ( - LaMarzoccoModel.GS3_AV, - LaMarzoccoModel.LINEA_MICRA, - LaMarzoccoModel.LINEA_MINI, + MachineModel.GS3_AV, + MachineModel.LINEA_MICRA, + MachineModel.LINEA_MINI, ), ), ) @@ -82,9 +107,14 @@ class LaMarzoccoSelectEntity(LaMarzoccoEntity, SelectEntity): @property def current_option(self) -> str: """Return the current selected option.""" - return str(self.entity_description.current_option_fn(self.coordinator.lm)) + return str( + self.entity_description.current_option_fn(self.coordinator.device.config) + ) async def async_select_option(self, option: str) -> None: """Change the selected option.""" - await self.entity_description.select_option_fn(self.coordinator, option) - self.async_write_ha_state() + if option != self.current_option: + await self.entity_description.select_option_fn( + self.coordinator.device, option + ) + self.async_write_ha_state() diff --git a/homeassistant/components/lamarzocco/sensor.py b/homeassistant/components/lamarzocco/sensor.py index ea5a5e184e1..723661451c5 100644 --- a/homeassistant/components/lamarzocco/sensor.py +++ b/homeassistant/components/lamarzocco/sensor.py @@ -3,7 +3,8 @@ from collections.abc import Callable from dataclasses import dataclass -from lmcloud import LMCloud as LaMarzoccoClient +from lmcloud.const import BoilerType, PhysicalKey +from lmcloud.lm_machine import LaMarzoccoMachine from homeassistant.components.sensor import ( SensorDeviceClass, @@ -22,12 +23,11 @@ from .entity import LaMarzoccoEntity, LaMarzoccoEntityDescription @dataclass(frozen=True, kw_only=True) class LaMarzoccoSensorEntityDescription( - LaMarzoccoEntityDescription, - SensorEntityDescription, + LaMarzoccoEntityDescription, SensorEntityDescription ): """Description of a La Marzocco sensor.""" - value_fn: Callable[[LaMarzoccoClient], float | int] + value_fn: Callable[[LaMarzoccoMachine], float | int] ENTITIES: tuple[LaMarzoccoSensorEntityDescription, ...] = ( @@ -36,7 +36,8 @@ ENTITIES: tuple[LaMarzoccoSensorEntityDescription, ...] = ( translation_key="drink_stats_coffee", native_unit_of_measurement="drinks", state_class=SensorStateClass.TOTAL_INCREASING, - value_fn=lambda lm: lm.current_status.get("drinks_k1", 0), + value_fn=lambda device: device.statistics.drink_stats.get(PhysicalKey.A, 0), + available_fn=lambda device: len(device.statistics.drink_stats) > 0, entity_category=EntityCategory.DIAGNOSTIC, ), LaMarzoccoSensorEntityDescription( @@ -44,7 +45,8 @@ ENTITIES: tuple[LaMarzoccoSensorEntityDescription, ...] = ( translation_key="drink_stats_flushing", native_unit_of_measurement="drinks", state_class=SensorStateClass.TOTAL_INCREASING, - value_fn=lambda lm: lm.current_status.get("total_flushing", 0), + value_fn=lambda device: device.statistics.total_flushes, + available_fn=lambda device: len(device.statistics.drink_stats) > 0, entity_category=EntityCategory.DIAGNOSTIC, ), LaMarzoccoSensorEntityDescription( @@ -53,8 +55,8 @@ ENTITIES: tuple[LaMarzoccoSensorEntityDescription, ...] = ( native_unit_of_measurement=UnitOfTime.SECONDS, state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.DURATION, - value_fn=lambda lm: lm.current_status.get("brew_active_duration", 0), - available_fn=lambda lm: lm.websocket_connected, + value_fn=lambda device: device.config.brew_active_duration, + available_fn=lambda device: device.websocket_connected, entity_category=EntityCategory.DIAGNOSTIC, supported_fn=lambda coordinator: coordinator.local_connection_configured, ), @@ -65,7 +67,9 @@ ENTITIES: tuple[LaMarzoccoSensorEntityDescription, ...] = ( suggested_display_precision=1, state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.TEMPERATURE, - value_fn=lambda lm: lm.current_status.get("coffee_temp", 0), + value_fn=lambda device: device.config.boilers[ + BoilerType.COFFEE + ].current_temperature, ), LaMarzoccoSensorEntityDescription( key="current_temp_steam", @@ -74,7 +78,9 @@ ENTITIES: tuple[LaMarzoccoSensorEntityDescription, ...] = ( suggested_display_precision=1, state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.TEMPERATURE, - value_fn=lambda lm: lm.current_status.get("steam_temp", 0), + value_fn=lambda device: device.config.boilers[ + BoilerType.STEAM + ].current_temperature, ), ) @@ -102,4 +108,4 @@ class LaMarzoccoSensorEntity(LaMarzoccoEntity, SensorEntity): @property def native_value(self) -> int | float: """State of the sensor.""" - return self.entity_description.value_fn(self.coordinator.lm) + return self.entity_description.value_fn(self.coordinator.device) diff --git a/homeassistant/components/lamarzocco/strings.json b/homeassistant/components/lamarzocco/strings.json index 03ce2eb93e8..744f4a0d63f 100644 --- a/homeassistant/components/lamarzocco/strings.json +++ b/homeassistant/components/lamarzocco/strings.json @@ -68,7 +68,7 @@ }, "calendar": { "auto_on_off_schedule": { - "name": "Auto on/off schedule" + "name": "Auto on/off schedule ({id})" } }, "number": { @@ -139,9 +139,6 @@ } }, "switch": { - "auto_on_off": { - "name": "Auto on/off" - }, "steam_boiler": { "name": "Steam boiler" } @@ -154,5 +151,11 @@ "name": "Gateway firmware" } } + }, + "issues": { + "unsupported_gateway_firmware": { + "title": "Unsupported gateway firmware", + "description": "Gateway firmware {gateway_version} is no longer supported by this integration, please update." + } } } diff --git a/homeassistant/components/lamarzocco/switch.py b/homeassistant/components/lamarzocco/switch.py index dd647bf4582..0c5939e6d59 100644 --- a/homeassistant/components/lamarzocco/switch.py +++ b/homeassistant/components/lamarzocco/switch.py @@ -4,14 +4,16 @@ from collections.abc import Callable, Coroutine from dataclasses import dataclass from typing import Any +from lmcloud.const import BoilerType +from lmcloud.lm_machine import LaMarzoccoMachine +from lmcloud.models import LaMarzoccoMachineConfig + from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription from homeassistant.config_entries import ConfigEntry -from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN -from .coordinator import LaMarzoccoUpdateCoordinator from .entity import LaMarzoccoEntity, LaMarzoccoEntityDescription @@ -22,8 +24,8 @@ class LaMarzoccoSwitchEntityDescription( ): """Description of a La Marzocco Switch.""" - control_fn: Callable[[LaMarzoccoUpdateCoordinator, bool], Coroutine[Any, Any, bool]] - is_on_fn: Callable[[LaMarzoccoUpdateCoordinator], bool] + control_fn: Callable[[LaMarzoccoMachine, bool], Coroutine[Any, Any, bool]] + is_on_fn: Callable[[LaMarzoccoMachineConfig], bool] ENTITIES: tuple[LaMarzoccoSwitchEntityDescription, ...] = ( @@ -31,30 +33,14 @@ ENTITIES: tuple[LaMarzoccoSwitchEntityDescription, ...] = ( key="main", translation_key="main", name=None, - control_fn=lambda coordinator, state: coordinator.lm.set_power( - state, coordinator.async_get_ble_device() - ), - is_on_fn=lambda coordinator: coordinator.lm.current_status["power"], - ), - LaMarzoccoSwitchEntityDescription( - key="auto_on_off", - translation_key="auto_on_off", - control_fn=lambda coordinator, state: coordinator.lm.set_auto_on_off_global( - state - ), - is_on_fn=lambda coordinator: coordinator.lm.current_status["global_auto"] - == "Enabled", - entity_category=EntityCategory.CONFIG, + control_fn=lambda machine, state: machine.set_power(state), + is_on_fn=lambda config: config.turned_on, ), LaMarzoccoSwitchEntityDescription( key="steam_boiler_enable", translation_key="steam_boiler", - control_fn=lambda coordinator, state: coordinator.lm.set_steam( - state, coordinator.async_get_ble_device() - ), - is_on_fn=lambda coordinator: coordinator.lm.current_status[ - "steam_boiler_enable" - ], + control_fn=lambda machine, state: machine.set_steam(state), + is_on_fn=lambda config: config.boilers[BoilerType.STEAM].enabled, ), ) @@ -81,15 +67,15 @@ class LaMarzoccoSwitchEntity(LaMarzoccoEntity, SwitchEntity): async def async_turn_on(self, **kwargs: Any) -> None: """Turn device on.""" - await self.entity_description.control_fn(self.coordinator, True) + await self.entity_description.control_fn(self.coordinator.device, True) self.async_write_ha_state() async def async_turn_off(self, **kwargs: Any) -> None: """Turn device off.""" - await self.entity_description.control_fn(self.coordinator, False) + await self.entity_description.control_fn(self.coordinator.device, False) self.async_write_ha_state() @property def is_on(self) -> bool: """Return true if device is on.""" - return self.entity_description.is_on_fn(self.coordinator) + return self.entity_description.is_on_fn(self.coordinator.device.config) diff --git a/homeassistant/components/lamarzocco/update.py b/homeassistant/components/lamarzocco/update.py index cc3e665725b..f8891b30bf8 100644 --- a/homeassistant/components/lamarzocco/update.py +++ b/homeassistant/components/lamarzocco/update.py @@ -1,11 +1,9 @@ """Support for La Marzocco update entities.""" -from collections.abc import Callable from dataclasses import dataclass from typing import Any -from lmcloud import LMCloud as LaMarzoccoClient -from lmcloud.const import LaMarzoccoUpdateableComponent +from lmcloud.const import FirmwareType from homeassistant.components.update import ( UpdateDeviceClass, @@ -30,9 +28,7 @@ class LaMarzoccoUpdateEntityDescription( ): """Description of a La Marzocco update entities.""" - current_fw_fn: Callable[[LaMarzoccoClient], str] - latest_fw_fn: Callable[[LaMarzoccoClient], str] - component: LaMarzoccoUpdateableComponent + component: FirmwareType ENTITIES: tuple[LaMarzoccoUpdateEntityDescription, ...] = ( @@ -40,18 +36,14 @@ ENTITIES: tuple[LaMarzoccoUpdateEntityDescription, ...] = ( key="machine_firmware", translation_key="machine_firmware", device_class=UpdateDeviceClass.FIRMWARE, - current_fw_fn=lambda lm: lm.firmware_version, - latest_fw_fn=lambda lm: lm.latest_firmware_version, - component=LaMarzoccoUpdateableComponent.MACHINE, + component=FirmwareType.MACHINE, entity_category=EntityCategory.DIAGNOSTIC, ), LaMarzoccoUpdateEntityDescription( key="gateway_firmware", translation_key="gateway_firmware", device_class=UpdateDeviceClass.FIRMWARE, - current_fw_fn=lambda lm: lm.gateway_version, - latest_fw_fn=lambda lm: lm.latest_gateway_version, - component=LaMarzoccoUpdateableComponent.GATEWAY, + component=FirmwareType.GATEWAY, entity_category=EntityCategory.DIAGNOSTIC, ), ) @@ -81,12 +73,16 @@ class LaMarzoccoUpdateEntity(LaMarzoccoEntity, UpdateEntity): @property def installed_version(self) -> str | None: """Return the current firmware version.""" - return self.entity_description.current_fw_fn(self.coordinator.lm) + return self.coordinator.device.firmware[ + self.entity_description.component + ].current_version @property def latest_version(self) -> str: """Return the latest firmware version.""" - return self.entity_description.latest_fw_fn(self.coordinator.lm) + return self.coordinator.device.firmware[ + self.entity_description.component + ].latest_version async def async_install( self, version: str | None, backup: bool, **kwargs: Any @@ -94,7 +90,7 @@ class LaMarzoccoUpdateEntity(LaMarzoccoEntity, UpdateEntity): """Install an update.""" self._attr_in_progress = True self.async_write_ha_state() - success = await self.coordinator.lm.update_firmware( + success = await self.coordinator.device.update_firmware( self.entity_description.component ) if not success: diff --git a/requirements_all.txt b/requirements_all.txt index c84760b1a07..0403cd555f0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1260,7 +1260,7 @@ linear-garage-door==0.2.9 linode-api==4.1.9b1 # homeassistant.components.lamarzocco -lmcloud==0.4.35 +lmcloud==1.1.11 # homeassistant.components.google_maps locationsharinglib==5.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 81cab2c4617..fe147205686 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1017,7 +1017,7 @@ libsoundtouch==0.8 linear-garage-door==0.2.9 # homeassistant.components.lamarzocco -lmcloud==0.4.35 +lmcloud==1.1.11 # homeassistant.components.logi_circle logi-circle==0.2.3 diff --git a/tests/components/lamarzocco/__init__.py b/tests/components/lamarzocco/__init__.py index ed4d2e0990e..4d274d10baa 100644 --- a/tests/components/lamarzocco/__init__.py +++ b/tests/components/lamarzocco/__init__.py @@ -1,6 +1,6 @@ """Mock inputs for tests.""" -from lmcloud.const import LaMarzoccoModel +from lmcloud.const import MachineModel from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant @@ -18,31 +18,34 @@ PASSWORD_SELECTION = { USER_INPUT = PASSWORD_SELECTION | {CONF_USERNAME: "username"} -MODEL_DICT = { - LaMarzoccoModel.GS3_AV: ("GS01234", "GS3 AV"), - LaMarzoccoModel.GS3_MP: ("GS01234", "GS3 MP"), - LaMarzoccoModel.LINEA_MICRA: ("MR01234", "Linea Micra"), - LaMarzoccoModel.LINEA_MINI: ("LM01234", "Linea Mini"), +SERIAL_DICT = { + MachineModel.GS3_AV: "GS01234", + MachineModel.GS3_MP: "GS01234", + MachineModel.LINEA_MICRA: "MR01234", + MachineModel.LINEA_MINI: "LM01234", } +WAKE_UP_SLEEP_ENTRY_IDS = ["Os2OswX", "aXFz5bJ"] + async def async_init_integration( hass: HomeAssistant, mock_config_entry: MockConfigEntry ) -> None: """Set up the La Marzocco integration for testing.""" + mock_config_entry.add_to_hass(hass) await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() def get_bluetooth_service_info( - model: LaMarzoccoModel, serial: str + model: MachineModel, serial: str ) -> BluetoothServiceInfo: """Return a mocked BluetoothServiceInfo.""" - if model in (LaMarzoccoModel.GS3_AV, LaMarzoccoModel.GS3_MP): + if model in (MachineModel.GS3_AV, MachineModel.GS3_MP): name = f"GS3_{serial}" - elif model == LaMarzoccoModel.LINEA_MINI: + elif model == MachineModel.LINEA_MINI: name = f"MINI_{serial}" - elif model == LaMarzoccoModel.LINEA_MICRA: + elif model == MachineModel.LINEA_MICRA: name = f"MICRA_{serial}" return BluetoothServiceInfo( name=name, diff --git a/tests/components/lamarzocco/conftest.py b/tests/components/lamarzocco/conftest.py index 49aa20e3a46..6741ac0797c 100644 --- a/tests/components/lamarzocco/conftest.py +++ b/tests/components/lamarzocco/conftest.py @@ -1,22 +1,23 @@ """Lamarzocco session fixtures.""" +from collections.abc import Callable +import json from unittest.mock import MagicMock, patch -from lmcloud.const import LaMarzoccoModel +from bleak.backends.device import BLEDevice +from lmcloud.const import FirmwareType, MachineModel, SteamLevel +from lmcloud.lm_machine import LaMarzoccoMachine +from lmcloud.models import LaMarzoccoDeviceInfo import pytest from typing_extensions import Generator -from homeassistant.components.lamarzocco.const import CONF_MACHINE, DOMAIN -from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME +from homeassistant.components.lamarzocco.const import DOMAIN +from homeassistant.const import CONF_HOST, CONF_MODEL, CONF_NAME, CONF_TOKEN from homeassistant.core import HomeAssistant -from . import MODEL_DICT, USER_INPUT, async_init_integration +from . import SERIAL_DICT, USER_INPUT, async_init_integration -from tests.common import ( - MockConfigEntry, - load_json_array_fixture, - load_json_object_fixture, -) +from tests.common import MockConfigEntry, load_fixture, load_json_object_fixture @pytest.fixture @@ -27,12 +28,13 @@ def mock_config_entry( entry = MockConfigEntry( title="My LaMarzocco", domain=DOMAIN, + version=2, data=USER_INPUT | { - CONF_MACHINE: mock_lamarzocco.serial_number, + CONF_MODEL: mock_lamarzocco.model, CONF_HOST: "host", - CONF_NAME: "name", - CONF_MAC: "mac", + CONF_TOKEN: "token", + CONF_NAME: "GS3", }, unique_id=mock_lamarzocco.serial_number, ) @@ -44,77 +46,96 @@ def mock_config_entry( async def init_integration( hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_lamarzocco: MagicMock ) -> MockConfigEntry: - """Set up the LaMetric integration for testing.""" + """Set up the La Marzocco integration for testing.""" await async_init_integration(hass, mock_config_entry) return mock_config_entry @pytest.fixture -def device_fixture() -> LaMarzoccoModel: +def device_fixture() -> MachineModel: """Return the device fixture for a specific device.""" - return LaMarzoccoModel.GS3_AV + return MachineModel.GS3_AV @pytest.fixture -def mock_lamarzocco(device_fixture: LaMarzoccoModel) -> Generator[MagicMock]: - """Return a mocked LM client.""" - model_name = device_fixture +def mock_device_info() -> LaMarzoccoDeviceInfo: + """Return a mocked La Marzocco device info.""" + return LaMarzoccoDeviceInfo( + model=MachineModel.GS3_AV, + serial_number="GS01234", + name="GS3", + communication_key="token", + ) - (serial_number, true_model_name) = MODEL_DICT[model_name] + +@pytest.fixture +def mock_cloud_client( + mock_device_info: LaMarzoccoDeviceInfo, +) -> Generator[MagicMock]: + """Return a mocked LM cloud client.""" + with ( + patch( + "homeassistant.components.lamarzocco.config_flow.LaMarzoccoCloudClient", + autospec=True, + ) as cloud_client, + patch( + "homeassistant.components.lamarzocco.LaMarzoccoCloudClient", + new=cloud_client, + ), + ): + client = cloud_client.return_value + client.get_customer_fleet.return_value = { + mock_device_info.serial_number: mock_device_info + } + yield client + + +@pytest.fixture +def mock_lamarzocco(device_fixture: MachineModel) -> Generator[MagicMock]: + """Return a mocked LM client.""" + model = device_fixture + + serial_number = SERIAL_DICT[model] + + dummy_machine = LaMarzoccoMachine( + model=model, + serial_number=serial_number, + name=serial_number, + ) + config = load_json_object_fixture("config.json", DOMAIN) + statistics = json.loads(load_fixture("statistics.json", DOMAIN)) + + dummy_machine.parse_config(config) + dummy_machine.parse_statistics(statistics) with ( patch( - "homeassistant.components.lamarzocco.coordinator.LaMarzoccoClient", + "homeassistant.components.lamarzocco.coordinator.LaMarzoccoMachine", autospec=True, ) as lamarzocco_mock, - patch( - "homeassistant.components.lamarzocco.config_flow.LaMarzoccoClient", - new=lamarzocco_mock, - ), ): lamarzocco = lamarzocco_mock.return_value - lamarzocco.machine_info = { - "machine_name": serial_number, - "serial_number": serial_number, - } + lamarzocco.name = dummy_machine.name + lamarzocco.model = dummy_machine.model + lamarzocco.serial_number = dummy_machine.serial_number + lamarzocco.full_model_name = dummy_machine.full_model_name + lamarzocco.config = dummy_machine.config + lamarzocco.statistics = dummy_machine.statistics + lamarzocco.firmware = dummy_machine.firmware + lamarzocco.steam_level = SteamLevel.LEVEL_1 - lamarzocco.model_name = model_name - lamarzocco.true_model_name = true_model_name - lamarzocco.machine_name = serial_number - lamarzocco.serial_number = serial_number - - lamarzocco.firmware_version = "1.1" - lamarzocco.latest_firmware_version = "1.2" - lamarzocco.gateway_version = "v2.2-rc0" - lamarzocco.latest_gateway_version = "v3.1-rc4" - lamarzocco.update_firmware.return_value = True - - lamarzocco.current_status = load_json_object_fixture( - "current_status.json", DOMAIN - ) - lamarzocco.config = load_json_object_fixture("config.json", DOMAIN) - lamarzocco.statistics = load_json_array_fixture("statistics.json", DOMAIN) - lamarzocco.schedule = load_json_array_fixture("schedule.json", DOMAIN) - - lamarzocco.get_all_machines.return_value = [ - (serial_number, model_name), - ] - lamarzocco.check_local_connection.return_value = True - lamarzocco.initialized = False - lamarzocco.websocket_connected = True + lamarzocco.firmware[FirmwareType.GATEWAY].latest_version = "v3.5-rc3" + lamarzocco.firmware[FirmwareType.MACHINE].latest_version = "1.55" async def websocket_connect_mock( - callback: MagicMock, use_sigterm_handler: MagicMock + notify_callback: Callable | None, ) -> None: """Mock the websocket connect method.""" return None - lamarzocco.lm_local_api.websocket_connect = websocket_connect_mock - - lamarzocco.lm_bluetooth = MagicMock() - lamarzocco.lm_bluetooth.address = "AA:BB:CC:DD:EE:FF" + lamarzocco.websocket_connect = websocket_connect_mock yield lamarzocco @@ -133,3 +154,11 @@ def remove_local_connection( @pytest.fixture(autouse=True) def mock_bluetooth(enable_bluetooth: None) -> None: """Auto mock bluetooth.""" + + +@pytest.fixture +def mock_ble_device() -> BLEDevice: + """Return a mock BLE device.""" + return BLEDevice( + "00:00:00:00:00:00", "GS_GS01234", details={"path": "path"}, rssi=50 + ) diff --git a/tests/components/lamarzocco/fixtures/config.json b/tests/components/lamarzocco/fixtures/config.json index 60d11b0d470..ea6e2ee76b8 100644 --- a/tests/components/lamarzocco/fixtures/config.json +++ b/tests/components/lamarzocco/fixtures/config.json @@ -13,11 +13,16 @@ "schedulingType": "weeklyScheduling" } ], - "machine_sn": "GS01234", + "machine_sn": "Sn01239157", "machine_hw": "2", "isPlumbedIn": true, "isBackFlushEnabled": false, "standByTime": 0, + "smartStandBy": { + "enabled": true, + "minutes": 10, + "mode": "LastBrewing" + }, "tankStatus": true, "groupCapabilities": [ { @@ -121,58 +126,32 @@ } ] }, - "weeklySchedulingConfig": { - "enabled": true, - "monday": { + "wakeUpSleepEntries": [ + { + "days": [ + "monday", + "tuesday", + "wednesday", + "thursday", + "friday", + "saturday", + "sunday" + ], "enabled": true, - "h_on": 6, - "h_off": 16, - "m_on": 0, - "m_off": 0 + "id": "Os2OswX", + "steam": true, + "timeOff": "24:0", + "timeOn": "22:0" }, - "tuesday": { + { + "days": ["sunday"], "enabled": true, - "h_on": 6, - "h_off": 16, - "m_on": 0, - "m_off": 0 - }, - "wednesday": { - "enabled": true, - "h_on": 6, - "h_off": 16, - "m_on": 0, - "m_off": 0 - }, - "thursday": { - "enabled": true, - "h_on": 6, - "h_off": 16, - "m_on": 0, - "m_off": 0 - }, - "friday": { - "enabled": true, - "h_on": 6, - "h_off": 16, - "m_on": 0, - "m_off": 0 - }, - "saturday": { - "enabled": true, - "h_on": 6, - "h_off": 16, - "m_on": 0, - "m_off": 0 - }, - "sunday": { - "enabled": true, - "h_on": 6, - "h_off": 16, - "m_on": 0, - "m_off": 0 + "id": "aXFz5bJ", + "steam": true, + "timeOff": "7:30", + "timeOn": "7:0" } - }, + ], "clock": "1901-07-08T10:29:00", "firmwareVersions": [ { diff --git a/tests/components/lamarzocco/fixtures/current_status.json b/tests/components/lamarzocco/fixtures/current_status.json deleted file mode 100644 index f99c3d5c331..00000000000 --- a/tests/components/lamarzocco/fixtures/current_status.json +++ /dev/null @@ -1,59 +0,0 @@ -{ - "power": true, - "global_auto": "Enabled", - "enable_prebrewing": true, - "coffee_boiler_on": true, - "steam_boiler_on": true, - "enable_preinfusion": false, - "steam_boiler_enable": true, - "steam_temp": 113, - "steam_set_temp": 128, - "steam_level_set": 3, - "coffee_temp": 93, - "coffee_set_temp": 95, - "water_reservoir_contact": true, - "brew_active": false, - "drinks_k1": 13, - "drinks_k2": 2, - "drinks_k3": 42, - "drinks_k4": 34, - "total_flushing": 69, - "mon_auto": "Disabled", - "mon_on_time": "00:00", - "mon_off_time": "00:00", - "tue_auto": "Disabled", - "tue_on_time": "00:00", - "tue_off_time": "00:00", - "wed_auto": "Disabled", - "wed_on_time": "00:00", - "wed_off_time": "00:00", - "thu_auto": "Disabled", - "thu_on_time": "00:00", - "thu_off_time": "00:00", - "fri_auto": "Disabled", - "fri_on_time": "00:00", - "fri_off_time": "00:00", - "sat_auto": "Disabled", - "sat_on_time": "00:00", - "sat_off_time": "00:00", - "sun_auto": "Disabled", - "sun_on_time": "00:00", - "sun_off_time": "00:00", - "dose_k1": 1023, - "dose_k2": 1023, - "dose_k3": 1023, - "dose_k4": 1023, - "dose_hot_water": 1023, - "prebrewing_ton_k1": 3, - "prebrewing_toff_k1": 5, - "prebrewing_ton_k2": 3, - "prebrewing_toff_k2": 5, - "prebrewing_ton_k3": 3, - "prebrewing_toff_k3": 5, - "prebrewing_ton_k4": 3, - "prebrewing_toff_k4": 5, - "preinfusion_k1": 4, - "preinfusion_k2": 4, - "preinfusion_k3": 4, - "preinfusion_k4": 4 -} diff --git a/tests/components/lamarzocco/fixtures/schedule.json b/tests/components/lamarzocco/fixtures/schedule.json deleted file mode 100644 index 62550caaa0b..00000000000 --- a/tests/components/lamarzocco/fixtures/schedule.json +++ /dev/null @@ -1,44 +0,0 @@ -[ - { - "day": "MONDAY", - "enable": "Disabled", - "on": "00:00", - "off": "00:00" - }, - { - "day": "TUESDAY", - "enable": "Disabled", - "on": "00:00", - "off": "00:00" - }, - { - "day": "WEDNESDAY", - "enable": "Enabled", - "on": "08:00", - "off": "13:00" - }, - { - "day": "THURSDAY", - "enable": "Disabled", - "on": "00:00", - "off": "00:00" - }, - { - "day": "FRIDAY", - "enable": "Enabled", - "on": "06:00", - "off": "09:00" - }, - { - "day": "SATURDAY", - "enable": "Enabled", - "on": "10:00", - "off": "23:00" - }, - { - "day": "SUNDAY", - "enable": "Disabled", - "on": "00:00", - "off": "00:00" - } -] diff --git a/tests/components/lamarzocco/snapshots/test_calendar.ambr b/tests/components/lamarzocco/snapshots/test_calendar.ambr index 676c0f1b2ad..2fd5dab846a 100644 --- a/tests/components/lamarzocco/snapshots/test_calendar.ambr +++ b/tests/components/lamarzocco/snapshots/test_calendar.ambr @@ -1,7 +1,7 @@ # serializer version: 1 # name: test_calendar_edge_cases[start_date0-end_date0] dict({ - 'calendar.gs01234_auto_on_off_schedule': dict({ + 'calendar.gs01234_auto_on_off_schedule_axfz5bj': dict({ 'events': list([ dict({ 'description': 'Machine is scheduled to turn on at the start time and off at the end time', @@ -15,7 +15,7 @@ # --- # name: test_calendar_edge_cases[start_date1-end_date1] dict({ - 'calendar.gs01234_auto_on_off_schedule': dict({ + 'calendar.gs01234_auto_on_off_schedule_axfz5bj': dict({ 'events': list([ dict({ 'description': 'Machine is scheduled to turn on at the start time and off at the end time', @@ -29,7 +29,7 @@ # --- # name: test_calendar_edge_cases[start_date2-end_date2] dict({ - 'calendar.gs01234_auto_on_off_schedule': dict({ + 'calendar.gs01234_auto_on_off_schedule_axfz5bj': dict({ 'events': list([ dict({ 'description': 'Machine is scheduled to turn on at the start time and off at the end time', @@ -43,7 +43,7 @@ # --- # name: test_calendar_edge_cases[start_date3-end_date3] dict({ - 'calendar.gs01234_auto_on_off_schedule': dict({ + 'calendar.gs01234_auto_on_off_schedule_axfz5bj': dict({ 'events': list([ dict({ 'description': 'Machine is scheduled to turn on at the start time and off at the end time', @@ -57,7 +57,7 @@ # --- # name: test_calendar_edge_cases[start_date4-end_date4] dict({ - 'calendar.gs01234_auto_on_off_schedule': dict({ + 'calendar.gs01234_auto_on_off_schedule_axfz5bj': dict({ 'events': list([ ]), }), @@ -65,7 +65,7 @@ # --- # name: test_calendar_edge_cases[start_date5-end_date5] dict({ - 'calendar.gs01234_auto_on_off_schedule': dict({ + 'calendar.gs01234_auto_on_off_schedule_axfz5bj': dict({ 'events': list([ dict({ 'description': 'Machine is scheduled to turn on at the start time and off at the end time', @@ -83,26 +83,7 @@ }), }) # --- -# name: test_calendar_events - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'all_day': False, - 'description': 'Machine is scheduled to turn on at the start time and off at the end time', - 'end_time': '2024-01-13 23:00:00', - 'friendly_name': 'GS01234 Auto on/off schedule', - 'location': '', - 'message': 'Machine My LaMarzocco on', - 'start_time': '2024-01-13 10:00:00', - }), - 'context': , - 'entity_id': 'calendar.gs01234_auto_on_off_schedule', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- -# name: test_calendar_events.1 +# name: test_calendar_events[entry.GS01234_auto_on_off_schedule_axfz5bj] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -114,7 +95,7 @@ 'disabled_by': None, 'domain': 'calendar', 'entity_category': None, - 'entity_id': 'calendar.gs01234_auto_on_off_schedule', + 'entity_id': 'calendar.gs01234_auto_on_off_schedule_axfz5bj', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -126,86 +107,267 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Auto on/off schedule', + 'original_name': 'Auto on/off schedule (aXFz5bJ)', 'platform': 'lamarzocco', 'previous_unique_id': None, 'supported_features': 0, 'translation_key': 'auto_on_off_schedule', - 'unique_id': 'GS01234_auto_on_off_schedule', + 'unique_id': 'GS01234_auto_on_off_schedule_aXFz5bJ', 'unit_of_measurement': None, }) # --- -# name: test_calendar_events.2 +# name: test_calendar_events[entry.GS01234_auto_on_off_schedule_os2oswx] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'calendar', + 'entity_category': None, + 'entity_id': 'calendar.gs01234_auto_on_off_schedule_os2oswx', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Auto on/off schedule (Os2OswX)', + 'platform': 'lamarzocco', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'auto_on_off_schedule', + 'unique_id': 'GS01234_auto_on_off_schedule_Os2OswX', + 'unit_of_measurement': None, + }) +# --- +# name: test_calendar_events[events.GS01234_auto_on_off_schedule_axfz5bj] dict({ - 'calendar.gs01234_auto_on_off_schedule': dict({ + 'calendar.gs01234_auto_on_off_schedule_axfz5bj': dict({ 'events': list([ dict({ 'description': 'Machine is scheduled to turn on at the start time and off at the end time', - 'end': '2024-01-13T23:00:00-08:00', - 'start': '2024-01-13T10:00:00-08:00', + 'end': '2024-01-14T07:30:00-08:00', + 'start': '2024-01-14T07:00:00-08:00', 'summary': 'Machine My LaMarzocco on', }), dict({ 'description': 'Machine is scheduled to turn on at the start time and off at the end time', - 'end': '2024-01-17T13:00:00-08:00', - 'start': '2024-01-17T08:00:00-08:00', + 'end': '2024-01-21T07:30:00-08:00', + 'start': '2024-01-21T07:00:00-08:00', 'summary': 'Machine My LaMarzocco on', }), dict({ 'description': 'Machine is scheduled to turn on at the start time and off at the end time', - 'end': '2024-01-19T09:00:00-08:00', - 'start': '2024-01-19T06:00:00-08:00', + 'end': '2024-01-28T07:30:00-08:00', + 'start': '2024-01-28T07:00:00-08:00', 'summary': 'Machine My LaMarzocco on', }), dict({ 'description': 'Machine is scheduled to turn on at the start time and off at the end time', - 'end': '2024-01-20T23:00:00-08:00', - 'start': '2024-01-20T10:00:00-08:00', - 'summary': 'Machine My LaMarzocco on', - }), - dict({ - 'description': 'Machine is scheduled to turn on at the start time and off at the end time', - 'end': '2024-01-24T13:00:00-08:00', - 'start': '2024-01-24T08:00:00-08:00', - 'summary': 'Machine My LaMarzocco on', - }), - dict({ - 'description': 'Machine is scheduled to turn on at the start time and off at the end time', - 'end': '2024-01-26T09:00:00-08:00', - 'start': '2024-01-26T06:00:00-08:00', - 'summary': 'Machine My LaMarzocco on', - }), - dict({ - 'description': 'Machine is scheduled to turn on at the start time and off at the end time', - 'end': '2024-01-27T23:00:00-08:00', - 'start': '2024-01-27T10:00:00-08:00', - 'summary': 'Machine My LaMarzocco on', - }), - dict({ - 'description': 'Machine is scheduled to turn on at the start time and off at the end time', - 'end': '2024-01-31T13:00:00-08:00', - 'start': '2024-01-31T08:00:00-08:00', - 'summary': 'Machine My LaMarzocco on', - }), - dict({ - 'description': 'Machine is scheduled to turn on at the start time and off at the end time', - 'end': '2024-02-02T09:00:00-08:00', - 'start': '2024-02-02T06:00:00-08:00', - 'summary': 'Machine My LaMarzocco on', - }), - dict({ - 'description': 'Machine is scheduled to turn on at the start time and off at the end time', - 'end': '2024-02-03T23:00:00-08:00', - 'start': '2024-02-03T10:00:00-08:00', + 'end': '2024-02-04T07:30:00-08:00', + 'start': '2024-02-04T07:00:00-08:00', 'summary': 'Machine My LaMarzocco on', }), ]), }), }) # --- +# name: test_calendar_events[events.GS01234_auto_on_off_schedule_os2oswx] + dict({ + 'calendar.gs01234_auto_on_off_schedule_os2oswx': dict({ + 'events': list([ + dict({ + 'description': 'Machine is scheduled to turn on at the start time and off at the end time', + 'end': '2024-01-13T00:00:00-08:00', + 'start': '2024-01-12T22:00:00-08:00', + 'summary': 'Machine My LaMarzocco on', + }), + dict({ + 'description': 'Machine is scheduled to turn on at the start time and off at the end time', + 'end': '2024-01-14T00:00:00-08:00', + 'start': '2024-01-13T22:00:00-08:00', + 'summary': 'Machine My LaMarzocco on', + }), + dict({ + 'description': 'Machine is scheduled to turn on at the start time and off at the end time', + 'end': '2024-01-15T00:00:00-08:00', + 'start': '2024-01-14T22:00:00-08:00', + 'summary': 'Machine My LaMarzocco on', + }), + dict({ + 'description': 'Machine is scheduled to turn on at the start time and off at the end time', + 'end': '2024-01-16T00:00:00-08:00', + 'start': '2024-01-15T22:00:00-08:00', + 'summary': 'Machine My LaMarzocco on', + }), + dict({ + 'description': 'Machine is scheduled to turn on at the start time and off at the end time', + 'end': '2024-01-17T00:00:00-08:00', + 'start': '2024-01-16T22:00:00-08:00', + 'summary': 'Machine My LaMarzocco on', + }), + dict({ + 'description': 'Machine is scheduled to turn on at the start time and off at the end time', + 'end': '2024-01-18T00:00:00-08:00', + 'start': '2024-01-17T22:00:00-08:00', + 'summary': 'Machine My LaMarzocco on', + }), + dict({ + 'description': 'Machine is scheduled to turn on at the start time and off at the end time', + 'end': '2024-01-19T00:00:00-08:00', + 'start': '2024-01-18T22:00:00-08:00', + 'summary': 'Machine My LaMarzocco on', + }), + dict({ + 'description': 'Machine is scheduled to turn on at the start time and off at the end time', + 'end': '2024-01-20T00:00:00-08:00', + 'start': '2024-01-19T22:00:00-08:00', + 'summary': 'Machine My LaMarzocco on', + }), + dict({ + 'description': 'Machine is scheduled to turn on at the start time and off at the end time', + 'end': '2024-01-21T00:00:00-08:00', + 'start': '2024-01-20T22:00:00-08:00', + 'summary': 'Machine My LaMarzocco on', + }), + dict({ + 'description': 'Machine is scheduled to turn on at the start time and off at the end time', + 'end': '2024-01-22T00:00:00-08:00', + 'start': '2024-01-21T22:00:00-08:00', + 'summary': 'Machine My LaMarzocco on', + }), + dict({ + 'description': 'Machine is scheduled to turn on at the start time and off at the end time', + 'end': '2024-01-23T00:00:00-08:00', + 'start': '2024-01-22T22:00:00-08:00', + 'summary': 'Machine My LaMarzocco on', + }), + dict({ + 'description': 'Machine is scheduled to turn on at the start time and off at the end time', + 'end': '2024-01-24T00:00:00-08:00', + 'start': '2024-01-23T22:00:00-08:00', + 'summary': 'Machine My LaMarzocco on', + }), + dict({ + 'description': 'Machine is scheduled to turn on at the start time and off at the end time', + 'end': '2024-01-25T00:00:00-08:00', + 'start': '2024-01-24T22:00:00-08:00', + 'summary': 'Machine My LaMarzocco on', + }), + dict({ + 'description': 'Machine is scheduled to turn on at the start time and off at the end time', + 'end': '2024-01-26T00:00:00-08:00', + 'start': '2024-01-25T22:00:00-08:00', + 'summary': 'Machine My LaMarzocco on', + }), + dict({ + 'description': 'Machine is scheduled to turn on at the start time and off at the end time', + 'end': '2024-01-27T00:00:00-08:00', + 'start': '2024-01-26T22:00:00-08:00', + 'summary': 'Machine My LaMarzocco on', + }), + dict({ + 'description': 'Machine is scheduled to turn on at the start time and off at the end time', + 'end': '2024-01-28T00:00:00-08:00', + 'start': '2024-01-27T22:00:00-08:00', + 'summary': 'Machine My LaMarzocco on', + }), + dict({ + 'description': 'Machine is scheduled to turn on at the start time and off at the end time', + 'end': '2024-01-29T00:00:00-08:00', + 'start': '2024-01-28T22:00:00-08:00', + 'summary': 'Machine My LaMarzocco on', + }), + dict({ + 'description': 'Machine is scheduled to turn on at the start time and off at the end time', + 'end': '2024-01-30T00:00:00-08:00', + 'start': '2024-01-29T22:00:00-08:00', + 'summary': 'Machine My LaMarzocco on', + }), + dict({ + 'description': 'Machine is scheduled to turn on at the start time and off at the end time', + 'end': '2024-01-31T00:00:00-08:00', + 'start': '2024-01-30T22:00:00-08:00', + 'summary': 'Machine My LaMarzocco on', + }), + dict({ + 'description': 'Machine is scheduled to turn on at the start time and off at the end time', + 'end': '2024-02-01T00:00:00-08:00', + 'start': '2024-01-31T22:00:00-08:00', + 'summary': 'Machine My LaMarzocco on', + }), + dict({ + 'description': 'Machine is scheduled to turn on at the start time and off at the end time', + 'end': '2024-02-02T00:00:00-08:00', + 'start': '2024-02-01T22:00:00-08:00', + 'summary': 'Machine My LaMarzocco on', + }), + dict({ + 'description': 'Machine is scheduled to turn on at the start time and off at the end time', + 'end': '2024-02-03T00:00:00-08:00', + 'start': '2024-02-02T22:00:00-08:00', + 'summary': 'Machine My LaMarzocco on', + }), + dict({ + 'description': 'Machine is scheduled to turn on at the start time and off at the end time', + 'end': '2024-02-04T00:00:00-08:00', + 'start': '2024-02-03T22:00:00-08:00', + 'summary': 'Machine My LaMarzocco on', + }), + ]), + }), + }) +# --- +# name: test_calendar_events[state.GS01234_auto_on_off_schedule_axfz5bj] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'all_day': False, + 'description': 'Machine is scheduled to turn on at the start time and off at the end time', + 'end_time': '2024-01-14 07:30:00', + 'friendly_name': 'GS01234 Auto on/off schedule (aXFz5bJ)', + 'location': '', + 'message': 'Machine My LaMarzocco on', + 'start_time': '2024-01-14 07:00:00', + }), + 'context': , + 'entity_id': 'calendar.gs01234_auto_on_off_schedule_axfz5bj', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_calendar_events[state.GS01234_auto_on_off_schedule_os2oswx] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'all_day': False, + 'description': 'Machine is scheduled to turn on at the start time and off at the end time', + 'end_time': '2024-01-13 00:00:00', + 'friendly_name': 'GS01234 Auto on/off schedule (Os2OswX)', + 'location': '', + 'message': 'Machine My LaMarzocco on', + 'start_time': '2024-01-12 22:00:00', + }), + 'context': , + 'entity_id': 'calendar.gs01234_auto_on_off_schedule_os2oswx', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- # name: test_no_calendar_events_global_disable dict({ - 'calendar.gs01234_auto_on_off_schedule': dict({ + 'calendar.gs01234_auto_on_off_schedule_os2oswx': dict({ 'events': list([ ]), }), diff --git a/tests/components/lamarzocco/snapshots/test_diagnostics.ambr b/tests/components/lamarzocco/snapshots/test_diagnostics.ambr index ec44100fe1e..29512f0b7b0 100644 --- a/tests/components/lamarzocco/snapshots/test_diagnostics.ambr +++ b/tests/components/lamarzocco/snapshots/test_diagnostics.ambr @@ -2,297 +2,107 @@ # name: test_diagnostics dict({ 'config': dict({ - 'boilerTargetTemperature': dict({ - 'CoffeeBoiler1': 95, - 'SteamBoiler': 123.9000015258789, - }), - 'boilers': list([ - dict({ - 'current': 123.80000305175781, - 'id': 'SteamBoiler', - 'isEnabled': True, - 'target': 123.9000015258789, + 'boilers': dict({ + 'CoffeeBoiler1': dict({ + 'current_temperature': 96.5, + 'enabled': True, + 'target_temperature': 95, }), - dict({ - 'current': 96.5, - 'id': 'CoffeeBoiler1', - 'isEnabled': True, - 'target': 95, - }), - ]), - 'clock': '1901-07-08T10:29:00', - 'firmwareVersions': list([ - dict({ - 'fw_version': '1.40', - 'name': 'machine_firmware', - }), - dict({ - 'fw_version': 'v3.1-rc4', - 'name': 'gateway_firmware', - }), - ]), - 'groupCapabilities': list([ - dict({ - 'capabilities': dict({ - 'boilerId': 'CoffeeBoiler1', - 'groupNumber': 'Group1', - 'groupType': 'AV_Group', - 'hasFlowmeter': True, - 'hasScale': False, - 'numberOfDoses': 4, - }), - 'doseMode': dict({ - 'brewingType': 'PulsesType', - 'groupNumber': 'Group1', - }), - 'doses': list([ - dict({ - 'doseIndex': 'DoseA', - 'doseType': 'PulsesType', - 'groupNumber': 'Group1', - 'stopTarget': 135, - }), - dict({ - 'doseIndex': 'DoseB', - 'doseType': 'PulsesType', - 'groupNumber': 'Group1', - 'stopTarget': 97, - }), - dict({ - 'doseIndex': 'DoseC', - 'doseType': 'PulsesType', - 'groupNumber': 'Group1', - 'stopTarget': 108, - }), - dict({ - 'doseIndex': 'DoseD', - 'doseType': 'PulsesType', - 'groupNumber': 'Group1', - 'stopTarget': 121, - }), - ]), - }), - ]), - 'isBackFlushEnabled': False, - 'isPlumbedIn': True, - 'machineCapabilities': list([ - dict({ - 'coffeeBoilersNumber': 1, - 'family': 'GS3AV', - 'groupsNumber': 1, - 'hasCupWarmer': False, - 'machineModes': list([ - 'BrewingMode', - 'StandBy', - ]), - 'schedulingType': 'weeklyScheduling', - 'steamBoilersNumber': 1, - 'teaDosesNumber': 1, - }), - ]), - 'machineMode': 'BrewingMode', - 'machine_hw': '2', - 'machine_sn': '**REDACTED**', - 'preinfusionMode': dict({ - 'Group1': dict({ - 'groupNumber': 'Group1', - 'preinfusionStyle': 'PreinfusionByDoseType', + 'SteamBoiler': dict({ + 'current_temperature': 123.80000305175781, + 'enabled': True, + 'target_temperature': 123.9000015258789, }), }), - 'preinfusionModesAvailable': list([ - 'ByDoseType', - ]), - 'preinfusionSettings': dict({ - 'Group1': list([ - dict({ - 'doseType': 'DoseA', - 'groupNumber': 'Group1', - 'preWetHoldTime': 1, - 'preWetTime': 0.5, - }), - dict({ - 'doseType': 'DoseB', - 'groupNumber': 'Group1', - 'preWetHoldTime': 1, - 'preWetTime': 0.5, - }), - dict({ - 'doseType': 'DoseC', - 'groupNumber': 'Group1', - 'preWetHoldTime': 3.299999952316284, - 'preWetTime': 3.299999952316284, - }), - dict({ - 'doseType': 'DoseD', - 'groupNumber': 'Group1', - 'preWetHoldTime': 2, - 'preWetTime': 2, - }), - ]), - 'mode': 'TypeB', - }), - 'standByTime': 0, - 'tankStatus': True, - 'teaDoses': dict({ - 'DoseA': dict({ - 'doseIndex': 'DoseA', - 'stopTarget': 8, - }), - }), - 'version': 'v1', - 'weeklySchedulingConfig': dict({ - 'enabled': True, - 'friday': dict({ - 'enabled': True, - 'h_off': 16, - 'h_on': 6, - 'm_off': 0, - 'm_on': 0, - }), - 'monday': dict({ - 'enabled': True, - 'h_off': 16, - 'h_on': 6, - 'm_off': 0, - 'm_on': 0, - }), - 'saturday': dict({ - 'enabled': True, - 'h_off': 16, - 'h_on': 6, - 'm_off': 0, - 'm_on': 0, - }), - 'sunday': dict({ - 'enabled': True, - 'h_off': 16, - 'h_on': 6, - 'm_off': 0, - 'm_on': 0, - }), - 'thursday': dict({ - 'enabled': True, - 'h_off': 16, - 'h_on': 6, - 'm_off': 0, - 'm_on': 0, - }), - 'tuesday': dict({ - 'enabled': True, - 'h_off': 16, - 'h_on': 6, - 'm_off': 0, - 'm_on': 0, - }), - 'wednesday': dict({ - 'enabled': True, - 'h_off': 16, - 'h_on': 6, - 'm_off': 0, - 'm_on': 0, - }), - }), - }), - 'current_status': dict({ 'brew_active': False, - 'coffee_boiler_on': True, - 'coffee_set_temp': 95, - 'coffee_temp': 93, - 'dose_hot_water': 1023, - 'dose_k1': 1023, - 'dose_k2': 1023, - 'dose_k3': 1023, - 'dose_k4': 1023, - 'drinks_k1': 13, - 'drinks_k2': 2, - 'drinks_k3': 42, - 'drinks_k4': 34, - 'enable_prebrewing': True, - 'enable_preinfusion': False, - 'fri_auto': 'Disabled', - 'fri_off_time': '00:00', - 'fri_on_time': '00:00', - 'global_auto': 'Enabled', - 'mon_auto': 'Disabled', - 'mon_off_time': '00:00', - 'mon_on_time': '00:00', - 'power': True, - 'prebrewing_toff_k1': 5, - 'prebrewing_toff_k2': 5, - 'prebrewing_toff_k3': 5, - 'prebrewing_toff_k4': 5, - 'prebrewing_ton_k1': 3, - 'prebrewing_ton_k2': 3, - 'prebrewing_ton_k3': 3, - 'prebrewing_ton_k4': 3, - 'preinfusion_k1': 4, - 'preinfusion_k2': 4, - 'preinfusion_k3': 4, - 'preinfusion_k4': 4, - 'sat_auto': 'Disabled', - 'sat_off_time': '00:00', - 'sat_on_time': '00:00', - 'steam_boiler_enable': True, - 'steam_boiler_on': True, - 'steam_level_set': 3, - 'steam_set_temp': 128, - 'steam_temp': 113, - 'sun_auto': 'Disabled', - 'sun_off_time': '00:00', - 'sun_on_time': '00:00', - 'thu_auto': 'Disabled', - 'thu_off_time': '00:00', - 'thu_on_time': '00:00', - 'total_flushing': 69, - 'tue_auto': 'Disabled', - 'tue_off_time': '00:00', - 'tue_on_time': '00:00', - 'water_reservoir_contact': True, - 'wed_auto': 'Disabled', - 'wed_off_time': '00:00', - 'wed_on_time': '00:00', - }), - 'firmware': dict({ - 'gateway': dict({ - 'latest_version': 'v3.1-rc4', - 'version': 'v2.2-rc0', + 'brew_active_duration': 0, + 'dose_hot_water': 8, + 'doses': dict({ + '1': 135, + '2': 97, + '3': 108, + '4': 121, }), - 'machine': dict({ - 'latest_version': '1.2', - 'version': '1.1', + 'plumbed_in': True, + 'prebrew_configuration': dict({ + '1': dict({ + 'off_time': 1, + 'on_time': 0.5, + }), + '2': dict({ + 'off_time': 1, + 'on_time': 0.5, + }), + '3': dict({ + 'off_time': 3.299999952316284, + 'on_time': 3.299999952316284, + }), + '4': dict({ + 'off_time': 2, + 'on_time': 2, + }), }), + 'prebrew_mode': 'TypeB', + 'smart_standby': dict({ + 'enabled': True, + 'minutes': 10, + 'mode': 'LastBrewing', + }), + 'turned_on': True, + 'wake_up_sleep_entries': dict({ + 'Os2OswX': dict({ + 'days': list([ + 'monday', + 'tuesday', + 'wednesday', + 'thursday', + 'friday', + 'saturday', + 'sunday', + ]), + 'enabled': True, + 'entry_id': 'Os2OswX', + 'steam': True, + 'time_off': '24:0', + 'time_on': '22:0', + }), + 'aXFz5bJ': dict({ + 'days': list([ + 'sunday', + ]), + 'enabled': True, + 'entry_id': 'aXFz5bJ', + 'steam': True, + 'time_off': '7:30', + 'time_on': '7:0', + }), + }), + 'water_contact': True, }), - 'machine_info': dict({ - 'machine_name': 'GS01234', - 'serial_number': '**REDACTED**', - }), + 'firmware': list([ + dict({ + 'machine': dict({ + 'current_version': '1.40', + 'latest_version': '1.55', + }), + }), + dict({ + 'gateway': dict({ + 'current_version': 'v3.1-rc4', + 'latest_version': 'v3.5-rc3', + }), + }), + ]), + 'model': 'GS3 AV', 'statistics': dict({ - 'stats': list([ - dict({ - 'coffeeType': 0, - 'count': 1047, - }), - dict({ - 'coffeeType': 1, - 'count': 560, - }), - dict({ - 'coffeeType': 2, - 'count': 468, - }), - dict({ - 'coffeeType': 3, - 'count': 312, - }), - dict({ - 'coffeeType': 4, - 'count': 2252, - }), - dict({ - 'coffeeType': -1, - 'count': 1740, - }), - ]), + 'continous': 2252, + 'drink_stats': dict({ + '1': 1047, + '2': 560, + '3': 468, + '4': 312, + }), + 'total_flushes': 1740, }), }) # --- diff --git a/tests/components/lamarzocco/snapshots/test_number.ambr b/tests/components/lamarzocco/snapshots/test_number.ambr index da35bf718f6..8265e7d7646 100644 --- a/tests/components/lamarzocco/snapshots/test_number.ambr +++ b/tests/components/lamarzocco/snapshots/test_number.ambr @@ -56,7 +56,7 @@ 'unit_of_measurement': , }) # --- -# name: test_gs3_exclusive[steam_target_temperature-131-set_steam_temp-kwargs0-GS3 AV] +# name: test_gs3_exclusive[steam_target_temperature-131-set_temp-kwargs0-GS3 AV] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'temperature', @@ -72,10 +72,10 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '128', + 'state': '123.900001525879', }) # --- -# name: test_gs3_exclusive[steam_target_temperature-131-set_steam_temp-kwargs0-GS3 AV].1 +# name: test_gs3_exclusive[steam_target_temperature-131-set_temp-kwargs0-GS3 AV].1 EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -113,7 +113,7 @@ 'unit_of_measurement': , }) # --- -# name: test_gs3_exclusive[steam_target_temperature-131-set_steam_temp-kwargs0-GS3 MP] +# name: test_gs3_exclusive[steam_target_temperature-131-set_temp-kwargs0-GS3 MP] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'temperature', @@ -129,10 +129,10 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '128', + 'state': '123.900001525879', }) # --- -# name: test_gs3_exclusive[steam_target_temperature-131-set_steam_temp-kwargs0-GS3 MP].1 +# name: test_gs3_exclusive[steam_target_temperature-131-set_temp-kwargs0-GS3 MP].1 EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -170,7 +170,7 @@ 'unit_of_measurement': , }) # --- -# name: test_gs3_exclusive[tea_water_duration-15-set_dose_hot_water-kwargs1-GS3 AV] +# name: test_gs3_exclusive[tea_water_duration-15-set_dose_tea_water-kwargs1-GS3 AV] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'duration', @@ -186,10 +186,10 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '1023', + 'state': '8', }) # --- -# name: test_gs3_exclusive[tea_water_duration-15-set_dose_hot_water-kwargs1-GS3 AV].1 +# name: test_gs3_exclusive[tea_water_duration-15-set_dose_tea_water-kwargs1-GS3 AV].1 EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -227,7 +227,7 @@ 'unit_of_measurement': , }) # --- -# name: test_gs3_exclusive[tea_water_duration-15-set_dose_hot_water-kwargs1-GS3 MP] +# name: test_gs3_exclusive[tea_water_duration-15-set_dose_tea_water-kwargs1-GS3 MP] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'duration', @@ -243,10 +243,10 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '1023', + 'state': '8', }) # --- -# name: test_gs3_exclusive[tea_water_duration-15-set_dose_hot_water-kwargs1-GS3 MP].1 +# name: test_gs3_exclusive[tea_water_duration-15-set_dose_tea_water-kwargs1-GS3 MP].1 EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -284,7 +284,7 @@ 'unit_of_measurement': , }) # --- -# name: test_pre_brew_infusion_key_numbers[dose-6-set_dose-kwargs3-GS3 AV][GS01234_dose_key_1-state] +# name: test_pre_brew_infusion_key_numbers[dose-6-Disabled-set_dose-kwargs3-GS3 AV][GS01234_dose_key_1-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'GS01234 Dose Key 1', @@ -299,10 +299,10 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '1023', + 'state': '135', }) # --- -# name: test_pre_brew_infusion_key_numbers[dose-6-set_dose-kwargs3-GS3 AV][GS01234_dose_key_2-state] +# name: test_pre_brew_infusion_key_numbers[dose-6-Disabled-set_dose-kwargs3-GS3 AV][GS01234_dose_key_2-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'GS01234 Dose Key 2', @@ -317,10 +317,10 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '1023', + 'state': '97', }) # --- -# name: test_pre_brew_infusion_key_numbers[dose-6-set_dose-kwargs3-GS3 AV][GS01234_dose_key_3-state] +# name: test_pre_brew_infusion_key_numbers[dose-6-Disabled-set_dose-kwargs3-GS3 AV][GS01234_dose_key_3-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'GS01234 Dose Key 3', @@ -335,10 +335,10 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '1023', + 'state': '108', }) # --- -# name: test_pre_brew_infusion_key_numbers[dose-6-set_dose-kwargs3-GS3 AV][GS01234_dose_key_4-state] +# name: test_pre_brew_infusion_key_numbers[dose-6-Disabled-set_dose-kwargs3-GS3 AV][GS01234_dose_key_4-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'GS01234 Dose Key 4', @@ -353,10 +353,10 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '1023', + 'state': '121', }) # --- -# name: test_pre_brew_infusion_key_numbers[prebrew_off_time-6-configure_prebrew-kwargs0-GS3 AV][GS01234_prebrew_off_time_key_1-state] +# name: test_pre_brew_infusion_key_numbers[prebrew_off_time-6-Enabled-set_prebrew_time-kwargs0-GS3 AV][GS01234_prebrew_off_time_key_1-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'duration', @@ -372,10 +372,10 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '3', + 'state': '1', }) # --- -# name: test_pre_brew_infusion_key_numbers[prebrew_off_time-6-configure_prebrew-kwargs0-GS3 AV][GS01234_prebrew_off_time_key_2-state] +# name: test_pre_brew_infusion_key_numbers[prebrew_off_time-6-Enabled-set_prebrew_time-kwargs0-GS3 AV][GS01234_prebrew_off_time_key_2-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'duration', @@ -391,10 +391,10 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '3', + 'state': '1', }) # --- -# name: test_pre_brew_infusion_key_numbers[prebrew_off_time-6-configure_prebrew-kwargs0-GS3 AV][GS01234_prebrew_off_time_key_3-state] +# name: test_pre_brew_infusion_key_numbers[prebrew_off_time-6-Enabled-set_prebrew_time-kwargs0-GS3 AV][GS01234_prebrew_off_time_key_3-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'duration', @@ -410,10 +410,10 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '3', + 'state': '3.29999995231628', }) # --- -# name: test_pre_brew_infusion_key_numbers[prebrew_off_time-6-configure_prebrew-kwargs0-GS3 AV][GS01234_prebrew_off_time_key_4-state] +# name: test_pre_brew_infusion_key_numbers[prebrew_off_time-6-Enabled-set_prebrew_time-kwargs0-GS3 AV][GS01234_prebrew_off_time_key_4-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'duration', @@ -429,10 +429,10 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '3', + 'state': '2', }) # --- -# name: test_pre_brew_infusion_key_numbers[prebrew_on_time-6-configure_prebrew-kwargs1-GS3 AV][GS01234_prebrew_on_time_key_1-state] +# name: test_pre_brew_infusion_key_numbers[prebrew_on_time-6-Enabled-set_prebrew_time-kwargs1-GS3 AV][GS01234_prebrew_on_time_key_1-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'duration', @@ -448,10 +448,10 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '5', + 'state': '1', }) # --- -# name: test_pre_brew_infusion_key_numbers[prebrew_on_time-6-configure_prebrew-kwargs1-GS3 AV][GS01234_prebrew_on_time_key_2-state] +# name: test_pre_brew_infusion_key_numbers[prebrew_on_time-6-Enabled-set_prebrew_time-kwargs1-GS3 AV][GS01234_prebrew_on_time_key_2-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'duration', @@ -467,10 +467,10 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '5', + 'state': '1', }) # --- -# name: test_pre_brew_infusion_key_numbers[prebrew_on_time-6-configure_prebrew-kwargs1-GS3 AV][GS01234_prebrew_on_time_key_3-state] +# name: test_pre_brew_infusion_key_numbers[prebrew_on_time-6-Enabled-set_prebrew_time-kwargs1-GS3 AV][GS01234_prebrew_on_time_key_3-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'duration', @@ -486,10 +486,10 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '5', + 'state': '3.29999995231628', }) # --- -# name: test_pre_brew_infusion_key_numbers[prebrew_on_time-6-configure_prebrew-kwargs1-GS3 AV][GS01234_prebrew_on_time_key_4-state] +# name: test_pre_brew_infusion_key_numbers[prebrew_on_time-6-Enabled-set_prebrew_time-kwargs1-GS3 AV][GS01234_prebrew_on_time_key_4-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'duration', @@ -505,10 +505,10 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '5', + 'state': '2', }) # --- -# name: test_pre_brew_infusion_key_numbers[preinfusion_time-7-configure_prebrew-kwargs2-GS3 AV][GS01234_preinfusion_time_key_1-state] +# name: test_pre_brew_infusion_key_numbers[preinfusion_time-7-TypeB-set_preinfusion_time-kwargs2-GS3 AV][GS01234_preinfusion_time_key_1-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'duration', @@ -524,10 +524,10 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unavailable', + 'state': '1', }) # --- -# name: test_pre_brew_infusion_key_numbers[preinfusion_time-7-configure_prebrew-kwargs2-GS3 AV][GS01234_preinfusion_time_key_2-state] +# name: test_pre_brew_infusion_key_numbers[preinfusion_time-7-TypeB-set_preinfusion_time-kwargs2-GS3 AV][GS01234_preinfusion_time_key_2-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'duration', @@ -543,10 +543,10 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unavailable', + 'state': '1', }) # --- -# name: test_pre_brew_infusion_key_numbers[preinfusion_time-7-configure_prebrew-kwargs2-GS3 AV][GS01234_preinfusion_time_key_3-state] +# name: test_pre_brew_infusion_key_numbers[preinfusion_time-7-TypeB-set_preinfusion_time-kwargs2-GS3 AV][GS01234_preinfusion_time_key_3-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'duration', @@ -562,10 +562,10 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unavailable', + 'state': '3.29999995231628', }) # --- -# name: test_pre_brew_infusion_key_numbers[preinfusion_time-7-configure_prebrew-kwargs2-GS3 AV][GS01234_preinfusion_time_key_4-state] +# name: test_pre_brew_infusion_key_numbers[preinfusion_time-7-TypeB-set_preinfusion_time-kwargs2-GS3 AV][GS01234_preinfusion_time_key_4-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'duration', @@ -581,10 +581,10 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unavailable', + 'state': '2', }) # --- -# name: test_pre_brew_infusion_numbers[prebrew_off_time-6-kwargs0-Linea Mini] +# name: test_pre_brew_infusion_numbers[prebrew_off_time-set_prebrew_time-Enabled-6-kwargs0-Linea Mini] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'duration', @@ -600,10 +600,10 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '3', + 'state': '1', }) # --- -# name: test_pre_brew_infusion_numbers[prebrew_off_time-6-kwargs0-Linea Mini].1 +# name: test_pre_brew_infusion_numbers[prebrew_off_time-set_prebrew_time-Enabled-6-kwargs0-Linea Mini].1 EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -641,7 +641,7 @@ 'unit_of_measurement': , }) # --- -# name: test_pre_brew_infusion_numbers[prebrew_off_time-6-kwargs0-Micra] +# name: test_pre_brew_infusion_numbers[prebrew_off_time-set_prebrew_time-Enabled-6-kwargs0-Micra] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'duration', @@ -657,10 +657,10 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '3', + 'state': '1', }) # --- -# name: test_pre_brew_infusion_numbers[prebrew_off_time-6-kwargs0-Micra].1 +# name: test_pre_brew_infusion_numbers[prebrew_off_time-set_prebrew_time-Enabled-6-kwargs0-Micra].1 EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -698,7 +698,7 @@ 'unit_of_measurement': , }) # --- -# name: test_pre_brew_infusion_numbers[prebrew_on_time-6-kwargs1-Linea Mini] +# name: test_pre_brew_infusion_numbers[prebrew_on_time-set_prebrew_time-Enabled-6-kwargs1-Linea Mini] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'duration', @@ -714,10 +714,10 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '5', + 'state': '1', }) # --- -# name: test_pre_brew_infusion_numbers[prebrew_on_time-6-kwargs1-Linea Mini].1 +# name: test_pre_brew_infusion_numbers[prebrew_on_time-set_prebrew_time-Enabled-6-kwargs1-Linea Mini].1 EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -755,7 +755,7 @@ 'unit_of_measurement': , }) # --- -# name: test_pre_brew_infusion_numbers[prebrew_on_time-6-kwargs1-Micra] +# name: test_pre_brew_infusion_numbers[prebrew_on_time-set_prebrew_time-Enabled-6-kwargs1-Micra] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'duration', @@ -771,10 +771,10 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '5', + 'state': '1', }) # --- -# name: test_pre_brew_infusion_numbers[prebrew_on_time-6-kwargs1-Micra].1 +# name: test_pre_brew_infusion_numbers[prebrew_on_time-set_prebrew_time-Enabled-6-kwargs1-Micra].1 EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -812,7 +812,7 @@ 'unit_of_measurement': , }) # --- -# name: test_pre_brew_infusion_numbers[preinfusion_time-7-kwargs2-Linea Mini] +# name: test_pre_brew_infusion_numbers[preinfusion_time-set_preinfusion_time-TypeB-7-kwargs2-Linea Mini] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'duration', @@ -828,10 +828,10 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unavailable', + 'state': '1', }) # --- -# name: test_pre_brew_infusion_numbers[preinfusion_time-7-kwargs2-Linea Mini].1 +# name: test_pre_brew_infusion_numbers[preinfusion_time-set_preinfusion_time-TypeB-7-kwargs2-Linea Mini].1 EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -869,7 +869,7 @@ 'unit_of_measurement': , }) # --- -# name: test_pre_brew_infusion_numbers[preinfusion_time-7-kwargs2-Micra] +# name: test_pre_brew_infusion_numbers[preinfusion_time-set_preinfusion_time-TypeB-7-kwargs2-Micra] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'duration', @@ -885,10 +885,10 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unavailable', + 'state': '1', }) # --- -# name: test_pre_brew_infusion_numbers[preinfusion_time-7-kwargs2-Micra].1 +# name: test_pre_brew_infusion_numbers[preinfusion_time-set_preinfusion_time-TypeB-7-kwargs2-Micra].1 EntityRegistryEntrySnapshot({ 'aliases': set({ }), diff --git a/tests/components/lamarzocco/snapshots/test_select.ambr b/tests/components/lamarzocco/snapshots/test_select.ambr index 1ee5ae7115f..be56af2b092 100644 --- a/tests/components/lamarzocco/snapshots/test_select.ambr +++ b/tests/components/lamarzocco/snapshots/test_select.ambr @@ -14,7 +14,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': 'preinfusion', }) # --- # name: test_pre_brew_infusion_select[GS3 AV].1 @@ -34,7 +34,7 @@ 'device_id': , 'disabled_by': None, 'domain': 'select', - 'entity_category': None, + 'entity_category': , 'entity_id': 'select.gs01234_prebrew_infusion_mode', 'has_entity_name': True, 'hidden_by': None, @@ -71,7 +71,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': 'preinfusion', }) # --- # name: test_pre_brew_infusion_select[Linea Mini].1 @@ -91,7 +91,7 @@ 'device_id': , 'disabled_by': None, 'domain': 'select', - 'entity_category': None, + 'entity_category': , 'entity_id': 'select.lm01234_prebrew_infusion_mode', 'has_entity_name': True, 'hidden_by': None, @@ -128,7 +128,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': 'preinfusion', }) # --- # name: test_pre_brew_infusion_select[Micra].1 @@ -148,7 +148,7 @@ 'device_id': , 'disabled_by': None, 'domain': 'select', - 'entity_category': None, + 'entity_category': , 'entity_id': 'select.mr01234_prebrew_infusion_mode', 'has_entity_name': True, 'hidden_by': None, @@ -185,7 +185,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '3', + 'state': '1', }) # --- # name: test_steam_boiler_level[Micra].1 diff --git a/tests/components/lamarzocco/snapshots/test_sensor.ambr b/tests/components/lamarzocco/snapshots/test_sensor.ambr index 71422b8b850..2237a8416e1 100644 --- a/tests/components/lamarzocco/snapshots/test_sensor.ambr +++ b/tests/components/lamarzocco/snapshots/test_sensor.ambr @@ -50,7 +50,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '93', + 'state': '96.5', }) # --- # name: test_sensors[GS01234_current_steam_temperature-entry] @@ -104,7 +104,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '113', + 'state': '123.800003051758', }) # --- # name: test_sensors[GS01234_shot_timer-entry] @@ -205,7 +205,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '13', + 'state': '1047', }) # --- # name: test_sensors[GS01234_total_flushes_made-entry] @@ -255,6 +255,6 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '69', + 'state': '1740', }) # --- diff --git a/tests/components/lamarzocco/snapshots/test_switch.ambr b/tests/components/lamarzocco/snapshots/test_switch.ambr index 59053c5c478..00205f48c21 100644 --- a/tests/components/lamarzocco/snapshots/test_switch.ambr +++ b/tests/components/lamarzocco/snapshots/test_switch.ambr @@ -20,16 +20,16 @@ 'labels': set({ }), 'manufacturer': 'La Marzocco', - 'model': 'GS3 AV', + 'model': , 'name': 'GS01234', 'name_by_user': None, 'serial_number': 'GS01234', 'suggested_area': None, - 'sw_version': '1.1', + 'sw_version': '1.40', 'via_device_id': None, }) # --- -# name: test_switches[-set_power-args_on0-args_off0] +# name: test_switches[-set_power] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'GS01234', @@ -42,7 +42,7 @@ 'state': 'on', }) # --- -# name: test_switches[-set_power-args_on0-args_off0].1 +# name: test_switches[-set_power].1 EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -75,141 +75,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_switches[-set_power-kwargs_on0-kwargs_off0] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'GS01234', - 'icon': 'mdi:power', - }), - 'context': , - 'entity_id': 'switch.gs01234', - 'last_changed': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_switches[-set_power-kwargs_on0-kwargs_off0].1 - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': None, - 'entity_id': 'switch.gs01234', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': 'mdi:power', - 'original_name': None, - 'platform': 'lamarzocco', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': 'GS01234_main', - 'unit_of_measurement': None, - }) -# --- -# name: test_switches[_auto_on_off-set_auto_on_off_global-args_on1-args_off1] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'GS01234 Auto on/off', - }), - 'context': , - 'entity_id': 'switch.gs01234_auto_on_off', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_switches[_auto_on_off-set_auto_on_off_global-args_on1-args_off1].1 - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.gs01234_auto_on_off', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Auto on/off', - 'platform': 'lamarzocco', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'auto_on_off', - 'unique_id': 'GS01234_auto_on_off', - 'unit_of_measurement': None, - }) -# --- -# name: test_switches[_auto_on_off-set_auto_on_off_global-kwargs_on1-kwargs_off1] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'GS01234 Auto on/off', - 'icon': 'mdi:alarm', - }), - 'context': , - 'entity_id': 'switch.gs01234_auto_on_off', - 'last_changed': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_switches[_auto_on_off-set_auto_on_off_global-kwargs_on1-kwargs_off1].1 - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.gs01234_auto_on_off', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': 'mdi:alarm', - 'original_name': 'Auto on/off', - 'platform': 'lamarzocco', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'auto_on_off', - 'unique_id': 'GS01234_auto_on_off', - 'unit_of_measurement': None, - }) -# --- -# name: test_switches[_steam_boiler-set_steam-args_on2-args_off2] +# name: test_switches[_steam_boiler-set_steam] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'GS01234 Steam boiler', @@ -222,53 +88,7 @@ 'state': 'on', }) # --- -# name: test_switches[_steam_boiler-set_steam-args_on2-args_off2].1 - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': None, - 'entity_id': 'switch.gs01234_steam_boiler', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Steam boiler', - 'platform': 'lamarzocco', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'steam_boiler', - 'unique_id': 'GS01234_steam_boiler_enable', - 'unit_of_measurement': None, - }) -# --- -# name: test_switches[_steam_boiler-set_steam-kwargs_on2-kwargs_off2] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'GS01234 Steam boiler', - 'icon': 'mdi:water-boiler', - }), - 'context': , - 'entity_id': 'switch.gs01234_steam_boiler', - 'last_changed': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_switches[_steam_boiler-set_steam-kwargs_on2-kwargs_off2].1 +# name: test_switches[_steam_boiler-set_steam].1 EntityRegistryEntrySnapshot({ 'aliases': set({ }), diff --git a/tests/components/lamarzocco/snapshots/test_update.ambr b/tests/components/lamarzocco/snapshots/test_update.ambr index 811b1a6f598..4ab8e35ffd0 100644 --- a/tests/components/lamarzocco/snapshots/test_update.ambr +++ b/tests/components/lamarzocco/snapshots/test_update.ambr @@ -7,8 +7,8 @@ 'entity_picture': 'https://brands.home-assistant.io/_/lamarzocco/icon.png', 'friendly_name': 'GS01234 Gateway firmware', 'in_progress': False, - 'installed_version': 'v2.2-rc0', - 'latest_version': 'v3.1-rc4', + 'installed_version': 'v3.1-rc4', + 'latest_version': 'v3.5-rc3', 'release_summary': None, 'release_url': None, 'skipped_version': None, @@ -64,8 +64,8 @@ 'entity_picture': 'https://brands.home-assistant.io/_/lamarzocco/icon.png', 'friendly_name': 'GS01234 Machine firmware', 'in_progress': False, - 'installed_version': '1.1', - 'latest_version': '1.2', + 'installed_version': '1.40', + 'latest_version': '1.55', 'release_summary': None, 'release_url': None, 'skipped_version': None, diff --git a/tests/components/lamarzocco/test_binary_sensor.py b/tests/components/lamarzocco/test_binary_sensor.py index bb1e16f09a5..36acde91a68 100644 --- a/tests/components/lamarzocco/test_binary_sensor.py +++ b/tests/components/lamarzocco/test_binary_sensor.py @@ -1,7 +1,10 @@ """Tests for La Marzocco binary sensors.""" +from datetime import timedelta from unittest.mock import MagicMock +from freezegun.api import FrozenDateTimeFactory +from lmcloud.exceptions import RequestNotSuccessful import pytest from syrupy import SnapshotAssertion @@ -11,7 +14,7 @@ from homeassistant.helpers import entity_registry as er from . import async_init_integration -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, async_fire_time_changed BINARY_SENSORS = ( "brewing_active", @@ -70,3 +73,29 @@ async def test_brew_active_unavailable( ) assert state assert state.state == STATE_UNAVAILABLE + + +async def test_sensor_going_unavailable( + hass: HomeAssistant, + mock_lamarzocco: MagicMock, + mock_config_entry: MockConfigEntry, + freezer: FrozenDateTimeFactory, +) -> None: + """Test sensor is going unavailable after an unsuccessful update.""" + brewing_active_sensor = ( + f"binary_sensor.{mock_lamarzocco.serial_number}_brewing_active" + ) + await async_init_integration(hass, mock_config_entry) + + state = hass.states.get(brewing_active_sensor) + assert state + assert state.state != STATE_UNAVAILABLE + + mock_lamarzocco.get_config.side_effect = RequestNotSuccessful("") + freezer.tick(timedelta(minutes=10)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + state = hass.states.get(brewing_active_sensor) + assert state + assert state.state == STATE_UNAVAILABLE diff --git a/tests/components/lamarzocco/test_calendar.py b/tests/components/lamarzocco/test_calendar.py index d26faa615e6..dd590a20db1 100644 --- a/tests/components/lamarzocco/test_calendar.py +++ b/tests/components/lamarzocco/test_calendar.py @@ -18,7 +18,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.util import dt as dt_util -from . import async_init_integration +from . import WAKE_UP_SLEEP_ENTRY_IDS, async_init_integration from tests.common import MockConfigEntry @@ -40,27 +40,37 @@ async def test_calendar_events( serial_number = mock_lamarzocco.serial_number - state = hass.states.get(f"calendar.{serial_number}_auto_on_off_schedule") - assert state - assert state == snapshot + for identifier in WAKE_UP_SLEEP_ENTRY_IDS: + identifier = identifier.lower() + state = hass.states.get( + f"calendar.{serial_number}_auto_on_off_schedule_{identifier}" + ) + assert state + assert state == snapshot( + name=f"state.{serial_number}_auto_on_off_schedule_{identifier}" + ) - entry = entity_registry.async_get(state.entity_id) - assert entry - assert entry == snapshot + entry = entity_registry.async_get(state.entity_id) + assert entry + assert entry == snapshot( + name=f"entry.{serial_number}_auto_on_off_schedule_{identifier}" + ) - events = await hass.services.async_call( - CALENDAR_DOMAIN, - SERVICE_GET_EVENTS, - { - ATTR_ENTITY_ID: f"calendar.{serial_number}_auto_on_off_schedule", - EVENT_START_DATETIME: test_time, - EVENT_END_DATETIME: test_time + timedelta(days=23), - }, - blocking=True, - return_response=True, - ) + events = await hass.services.async_call( + CALENDAR_DOMAIN, + SERVICE_GET_EVENTS, + { + ATTR_ENTITY_ID: f"calendar.{serial_number}_auto_on_off_schedule_{identifier}", + EVENT_START_DATETIME: test_time, + EVENT_END_DATETIME: test_time + timedelta(days=23), + }, + blocking=True, + return_response=True, + ) - assert events == snapshot + assert events == snapshot( + name=f"events.{serial_number}_auto_on_off_schedule_{identifier}" + ) @pytest.mark.parametrize( @@ -89,21 +99,13 @@ async def test_calendar_edge_cases( start_date = start_date.replace(tzinfo=dt_util.get_default_time_zone()) end_date = end_date.replace(tzinfo=dt_util.get_default_time_zone()) - # set schedule to be only on Sunday, 07:00 - 07:30 - mock_lamarzocco.schedule[2]["enable"] = "Disabled" - mock_lamarzocco.schedule[4]["enable"] = "Disabled" - mock_lamarzocco.schedule[5]["enable"] = "Disabled" - mock_lamarzocco.schedule[6]["enable"] = "Enabled" - mock_lamarzocco.schedule[6]["on"] = "07:00" - mock_lamarzocco.schedule[6]["off"] = "07:30" - await async_init_integration(hass, mock_config_entry) events = await hass.services.async_call( CALENDAR_DOMAIN, SERVICE_GET_EVENTS, { - ATTR_ENTITY_ID: f"calendar.{mock_lamarzocco.serial_number}_auto_on_off_schedule", + ATTR_ENTITY_ID: f"calendar.{mock_lamarzocco.serial_number}_auto_on_off_schedule_{WAKE_UP_SLEEP_ENTRY_IDS[1].lower()}", EVENT_START_DATETIME: start_date, EVENT_END_DATETIME: end_date, }, @@ -123,7 +125,9 @@ async def test_no_calendar_events_global_disable( ) -> None: """Assert no events when global auto on/off is disabled.""" - mock_lamarzocco.current_status["global_auto"] = "Disabled" + wake_up_sleep_entry_id = WAKE_UP_SLEEP_ENTRY_IDS[0] + + mock_lamarzocco.config.wake_up_sleep_entries[wake_up_sleep_entry_id].enabled = False test_time = datetime(2024, 1, 12, 11, tzinfo=dt_util.get_default_time_zone()) freezer.move_to(test_time) @@ -131,14 +135,16 @@ async def test_no_calendar_events_global_disable( serial_number = mock_lamarzocco.serial_number - state = hass.states.get(f"calendar.{serial_number}_auto_on_off_schedule") + state = hass.states.get( + f"calendar.{serial_number}_auto_on_off_schedule_{wake_up_sleep_entry_id.lower()}" + ) assert state events = await hass.services.async_call( CALENDAR_DOMAIN, SERVICE_GET_EVENTS, { - ATTR_ENTITY_ID: f"calendar.{serial_number}_auto_on_off_schedule", + ATTR_ENTITY_ID: f"calendar.{serial_number}_auto_on_off_schedule_{wake_up_sleep_entry_id.lower()}", EVENT_START_DATETIME: test_time, EVENT_END_DATETIME: test_time + timedelta(days=23), }, diff --git a/tests/components/lamarzocco/test_config_flow.py b/tests/components/lamarzocco/test_config_flow.py index 14f794000d8..92ecd0a13f4 100644 --- a/tests/components/lamarzocco/test_config_flow.py +++ b/tests/components/lamarzocco/test_config_flow.py @@ -1,17 +1,26 @@ """Test the La Marzocco config flow.""" -from unittest.mock import MagicMock +from unittest.mock import MagicMock, patch from lmcloud.exceptions import AuthFail, RequestNotSuccessful +from lmcloud.models import LaMarzoccoDeviceInfo -from homeassistant import config_entries -from homeassistant.components.lamarzocco.const import ( - CONF_MACHINE, - CONF_USE_BLUETOOTH, - DOMAIN, +from homeassistant.components.lamarzocco.config_flow import CONF_MACHINE +from homeassistant.components.lamarzocco.const import CONF_USE_BLUETOOTH, DOMAIN +from homeassistant.config_entries import ( + SOURCE_BLUETOOTH, + SOURCE_REAUTH, + SOURCE_USER, + ConfigEntryState, +) +from homeassistant.const import ( + CONF_HOST, + CONF_MAC, + CONF_MODEL, + CONF_NAME, + CONF_PASSWORD, + CONF_TOKEN, ) -from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState -from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, CONF_PASSWORD from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResult, FlowResultType @@ -21,7 +30,7 @@ from tests.common import MockConfigEntry async def __do_successful_user_step( - hass: HomeAssistant, result: FlowResult + hass: HomeAssistant, result: FlowResult, mock_cloud_client: MagicMock ) -> FlowResult: """Successfully configure the user step.""" result2 = await hass.config_entries.flow.async_configure( @@ -36,51 +45,63 @@ async def __do_successful_user_step( async def __do_sucessful_machine_selection_step( - hass: HomeAssistant, result2: FlowResult, mock_lamarzocco: MagicMock + hass: HomeAssistant, result2: FlowResult, mock_device_info: LaMarzoccoDeviceInfo ) -> None: """Successfully configure the machine selection step.""" - result3 = await hass.config_entries.flow.async_configure( - result2["flow_id"], - { - CONF_HOST: "192.168.1.1", - CONF_MACHINE: mock_lamarzocco.serial_number, - }, - ) + + with patch( + "homeassistant.components.lamarzocco.config_flow.LaMarzoccoLocalClient.validate_connection", + return_value=True, + ): + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + { + CONF_HOST: "192.168.1.1", + CONF_MACHINE: mock_device_info.serial_number, + }, + ) await hass.async_block_till_done() assert result3["type"] is FlowResultType.CREATE_ENTRY - assert result3["title"] == mock_lamarzocco.serial_number + assert result3["title"] == "GS3" assert result3["data"] == { **USER_INPUT, CONF_HOST: "192.168.1.1", - CONF_MACHINE: mock_lamarzocco.serial_number, + CONF_MODEL: mock_device_info.model, + CONF_NAME: mock_device_info.name, + CONF_TOKEN: mock_device_info.communication_key, } -async def test_form(hass: HomeAssistant, mock_lamarzocco: MagicMock) -> None: +async def test_form( + hass: HomeAssistant, + mock_cloud_client: MagicMock, + mock_device_info: LaMarzoccoDeviceInfo, +) -> None: """Test we get the form.""" result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": SOURCE_USER} ) assert result["type"] is FlowResultType.FORM assert result["errors"] == {} assert result["step_id"] == "user" - result2 = await __do_successful_user_step(hass, result) - await __do_sucessful_machine_selection_step(hass, result2, mock_lamarzocco) - - assert len(mock_lamarzocco.check_local_connection.mock_calls) == 1 + result2 = await __do_successful_user_step(hass, result, mock_cloud_client) + await __do_sucessful_machine_selection_step(hass, result2, mock_device_info) async def test_form_abort_already_configured( hass: HomeAssistant, - mock_lamarzocco: MagicMock, + mock_cloud_client: MagicMock, + mock_device_info: LaMarzoccoDeviceInfo, mock_config_entry: MockConfigEntry, ) -> None: """Test we abort if already configured.""" + mock_config_entry.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": SOURCE_USER} ) assert result["type"] is FlowResultType.FORM assert result["errors"] == {} @@ -98,7 +119,7 @@ async def test_form_abort_already_configured( result2["flow_id"], { CONF_HOST: "192.168.1.1", - CONF_MACHINE: mock_lamarzocco.serial_number, + CONF_MACHINE: mock_device_info.serial_number, }, ) await hass.async_block_till_done() @@ -108,13 +129,15 @@ async def test_form_abort_already_configured( async def test_form_invalid_auth( - hass: HomeAssistant, mock_lamarzocco: MagicMock + hass: HomeAssistant, + mock_device_info: LaMarzoccoDeviceInfo, + mock_cloud_client: MagicMock, ) -> None: """Test invalid auth error.""" - mock_lamarzocco.get_all_machines.side_effect = AuthFail("") + mock_cloud_client.get_customer_fleet.side_effect = AuthFail("") result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": SOURCE_USER} ) result2 = await hass.config_entries.flow.async_configure( @@ -124,20 +147,22 @@ async def test_form_invalid_auth( assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} - assert len(mock_lamarzocco.get_all_machines.mock_calls) == 1 + assert len(mock_cloud_client.get_customer_fleet.mock_calls) == 1 # test recovery from failure - mock_lamarzocco.get_all_machines.side_effect = None - result2 = await __do_successful_user_step(hass, result) - await __do_sucessful_machine_selection_step(hass, result2, mock_lamarzocco) + mock_cloud_client.get_customer_fleet.side_effect = None + result2 = await __do_successful_user_step(hass, result, mock_cloud_client) + await __do_sucessful_machine_selection_step(hass, result2, mock_device_info) async def test_form_invalid_host( - hass: HomeAssistant, mock_lamarzocco: MagicMock + hass: HomeAssistant, + mock_cloud_client: MagicMock, + mock_device_info: LaMarzoccoDeviceInfo, ) -> None: """Test invalid auth error.""" result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": SOURCE_USER} ) assert result["type"] is FlowResultType.FORM assert result["errors"] == {} @@ -148,38 +173,41 @@ async def test_form_invalid_host( ) await hass.async_block_till_done() - mock_lamarzocco.check_local_connection.return_value = False - assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "machine_selection" - result3 = await hass.config_entries.flow.async_configure( - result2["flow_id"], - { - CONF_HOST: "192.168.1.1", - CONF_MACHINE: mock_lamarzocco.serial_number, - }, - ) + with patch( + "homeassistant.components.lamarzocco.config_flow.LaMarzoccoLocalClient.validate_connection", + return_value=False, + ): + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + { + CONF_HOST: "192.168.1.1", + CONF_MACHINE: mock_device_info.serial_number, + }, + ) await hass.async_block_till_done() assert result3["type"] is FlowResultType.FORM assert result3["errors"] == {"host": "cannot_connect"} - assert len(mock_lamarzocco.get_all_machines.mock_calls) == 1 + assert len(mock_cloud_client.get_customer_fleet.mock_calls) == 1 # test recovery from failure - mock_lamarzocco.check_local_connection.return_value = True - await __do_sucessful_machine_selection_step(hass, result2, mock_lamarzocco) + await __do_sucessful_machine_selection_step(hass, result2, mock_device_info) async def test_form_cannot_connect( - hass: HomeAssistant, mock_lamarzocco: MagicMock + hass: HomeAssistant, + mock_cloud_client: MagicMock, + mock_device_info: LaMarzoccoDeviceInfo, ) -> None: """Test cannot connect error.""" - mock_lamarzocco.get_all_machines.return_value = [] + mock_cloud_client.get_customer_fleet.return_value = {} result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": SOURCE_USER} ) result2 = await hass.config_entries.flow.async_configure( @@ -189,9 +217,9 @@ async def test_form_cannot_connect( assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "no_machines"} - assert len(mock_lamarzocco.get_all_machines.mock_calls) == 1 + assert len(mock_cloud_client.get_customer_fleet.mock_calls) == 1 - mock_lamarzocco.get_all_machines.side_effect = RequestNotSuccessful("") + mock_cloud_client.get_customer_fleet.side_effect = RequestNotSuccessful("") result2 = await hass.config_entries.flow.async_configure( result["flow_id"], USER_INPUT, @@ -199,21 +227,26 @@ async def test_form_cannot_connect( assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} - assert len(mock_lamarzocco.get_all_machines.mock_calls) == 2 + assert len(mock_cloud_client.get_customer_fleet.mock_calls) == 2 # test recovery from failure - mock_lamarzocco.get_all_machines.side_effect = None - mock_lamarzocco.get_all_machines.return_value = [ - (mock_lamarzocco.serial_number, mock_lamarzocco.model_name) - ] - result2 = await __do_successful_user_step(hass, result) - await __do_sucessful_machine_selection_step(hass, result2, mock_lamarzocco) + mock_cloud_client.get_customer_fleet.side_effect = None + mock_cloud_client.get_customer_fleet.return_value = { + mock_device_info.serial_number: mock_device_info + } + result2 = await __do_successful_user_step(hass, result, mock_cloud_client) + await __do_sucessful_machine_selection_step(hass, result2, mock_device_info) async def test_reauth_flow( - hass: HomeAssistant, mock_lamarzocco: MagicMock, mock_config_entry: MockConfigEntry + hass: HomeAssistant, + mock_cloud_client: MagicMock, + mock_config_entry: MockConfigEntry, ) -> None: """Test that the reauth flow.""" + + mock_config_entry.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( DOMAIN, context={ @@ -235,19 +268,21 @@ async def test_reauth_flow( assert result2["type"] is FlowResultType.ABORT await hass.async_block_till_done() assert result2["reason"] == "reauth_successful" - assert len(mock_lamarzocco.get_all_machines.mock_calls) == 1 + assert len(mock_cloud_client.get_customer_fleet.mock_calls) == 1 assert mock_config_entry.data[CONF_PASSWORD] == "new_password" async def test_bluetooth_discovery( - hass: HomeAssistant, mock_lamarzocco: MagicMock + hass: HomeAssistant, + mock_lamarzocco: MagicMock, + mock_cloud_client: MagicMock, ) -> None: """Test bluetooth discovery.""" service_info = get_bluetooth_service_info( - mock_lamarzocco.model_name, mock_lamarzocco.serial_number + mock_lamarzocco.model, mock_lamarzocco.serial_number ) result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_BLUETOOTH}, data=service_info + DOMAIN, context={"source": SOURCE_BLUETOOTH}, data=service_info ) assert result["type"] is FlowResultType.FORM @@ -260,82 +295,95 @@ async def test_bluetooth_discovery( assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "machine_selection" - assert len(mock_lamarzocco.get_all_machines.mock_calls) == 1 - result3 = await hass.config_entries.flow.async_configure( - result2["flow_id"], - { - CONF_HOST: "192.168.1.1", - }, - ) + assert len(mock_cloud_client.get_customer_fleet.mock_calls) == 1 + with patch( + "homeassistant.components.lamarzocco.config_flow.LaMarzoccoLocalClient.validate_connection", + return_value=True, + ): + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + { + CONF_HOST: "192.168.1.1", + }, + ) await hass.async_block_till_done() assert result3["type"] is FlowResultType.CREATE_ENTRY - assert result3["title"] == mock_lamarzocco.serial_number + assert result3["title"] == "GS3" assert result3["data"] == { **USER_INPUT, CONF_HOST: "192.168.1.1", CONF_MACHINE: mock_lamarzocco.serial_number, - CONF_NAME: service_info.name, + CONF_NAME: "GS3", CONF_MAC: "aa:bb:cc:dd:ee:ff", + CONF_MODEL: mock_lamarzocco.model, + CONF_TOKEN: "token", } - assert len(mock_lamarzocco.check_local_connection.mock_calls) == 1 - async def test_bluetooth_discovery_errors( - hass: HomeAssistant, mock_lamarzocco: MagicMock + hass: HomeAssistant, + mock_lamarzocco: MagicMock, + mock_cloud_client: MagicMock, + mock_device_info: LaMarzoccoDeviceInfo, ) -> None: """Test bluetooth discovery errors.""" service_info = get_bluetooth_service_info( - mock_lamarzocco.model_name, mock_lamarzocco.serial_number + mock_lamarzocco.model, mock_lamarzocco.serial_number ) result = await hass.config_entries.flow.async_init( DOMAIN, - context={"source": config_entries.SOURCE_BLUETOOTH}, + context={"source": SOURCE_BLUETOOTH}, data=service_info, ) assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" - mock_lamarzocco.get_all_machines.return_value = [("GS98765", "GS3 MP")] + mock_cloud_client.get_customer_fleet.return_value = {"GS98765", ""} result2 = await hass.config_entries.flow.async_configure( result["flow_id"], USER_INPUT, ) assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "machine_not_found"} - assert len(mock_lamarzocco.get_all_machines.mock_calls) == 1 + assert len(mock_cloud_client.get_customer_fleet.mock_calls) == 1 - mock_lamarzocco.get_all_machines.return_value = [ - (mock_lamarzocco.serial_number, mock_lamarzocco.model_name) - ] + mock_cloud_client.get_customer_fleet.return_value = { + mock_device_info.serial_number: mock_device_info + } result2 = await hass.config_entries.flow.async_configure( result["flow_id"], USER_INPUT, ) assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "machine_selection" - assert len(mock_lamarzocco.get_all_machines.mock_calls) == 2 + assert len(mock_cloud_client.get_customer_fleet.mock_calls) == 2 - result3 = await hass.config_entries.flow.async_configure( - result2["flow_id"], - { - CONF_HOST: "192.168.1.1", - }, - ) + with patch( + "homeassistant.components.lamarzocco.config_flow.LaMarzoccoLocalClient.validate_connection", + return_value=True, + ): + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + { + CONF_HOST: "192.168.1.1", + }, + ) await hass.async_block_till_done() assert result3["type"] is FlowResultType.CREATE_ENTRY - assert result3["title"] == mock_lamarzocco.serial_number + assert result3["title"] == "GS3" assert result3["data"] == { **USER_INPUT, CONF_HOST: "192.168.1.1", CONF_MACHINE: mock_lamarzocco.serial_number, - CONF_NAME: service_info.name, + CONF_NAME: "GS3", CONF_MAC: "aa:bb:cc:dd:ee:ff", + CONF_MODEL: mock_lamarzocco.model, + CONF_TOKEN: "token", } diff --git a/tests/components/lamarzocco/test_init.py b/tests/components/lamarzocco/test_init.py index a4bc25f64af..2c812f79438 100644 --- a/tests/components/lamarzocco/test_init.py +++ b/tests/components/lamarzocco/test_init.py @@ -1,15 +1,19 @@ """Test initialization of lamarzocco.""" -from unittest.mock import MagicMock, patch +from unittest.mock import AsyncMock, MagicMock, patch +from lmcloud.const import FirmwareType from lmcloud.exceptions import AuthFail, RequestNotSuccessful +import pytest +from homeassistant.components.lamarzocco.config_flow import CONF_MACHINE from homeassistant.components.lamarzocco.const import DOMAIN from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState -from homeassistant.const import CONF_MAC, CONF_NAME +from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant +from homeassistant.helpers import issue_registry as ir -from . import async_init_integration, get_bluetooth_service_info +from . import USER_INPUT, async_init_integration, get_bluetooth_service_info from tests.common import MockConfigEntry @@ -20,7 +24,9 @@ async def test_load_unload_config_entry( mock_lamarzocco: MagicMock, ) -> None: """Test loading and unloading the integration.""" - await async_init_integration(hass, mock_config_entry) + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() assert mock_config_entry.state is ConfigEntryState.LOADED @@ -36,11 +42,13 @@ async def test_config_entry_not_ready( mock_lamarzocco: MagicMock, ) -> None: """Test the La Marzocco configuration entry not ready.""" - mock_lamarzocco.update_local_machine_status.side_effect = RequestNotSuccessful("") + mock_lamarzocco.get_config.side_effect = RequestNotSuccessful("") - await async_init_integration(hass, mock_config_entry) + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() - assert len(mock_lamarzocco.update_local_machine_status.mock_calls) == 1 + assert len(mock_lamarzocco.get_config.mock_calls) == 1 assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY @@ -50,11 +58,13 @@ async def test_invalid_auth( mock_lamarzocco: MagicMock, ) -> None: """Test auth error during setup.""" - mock_lamarzocco.update_local_machine_status.side_effect = AuthFail("") - await async_init_integration(hass, mock_config_entry) + mock_lamarzocco.get_config.side_effect = AuthFail("") + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR - assert len(mock_lamarzocco.update_local_machine_status.mock_calls) == 1 + assert len(mock_lamarzocco.get_config.mock_calls) == 1 flows = hass.config_entries.flow.async_progress() assert len(flows) == 1 @@ -68,27 +78,132 @@ async def test_invalid_auth( assert flow["context"].get("entry_id") == mock_config_entry.entry_id +async def test_v1_migration( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_cloud_client: MagicMock, + mock_lamarzocco: MagicMock, +) -> None: + """Test v1 -> v2 Migration.""" + entry_v1 = MockConfigEntry( + domain=DOMAIN, + version=1, + unique_id=mock_lamarzocco.serial_number, + data={ + **USER_INPUT, + CONF_HOST: "host", + CONF_MACHINE: mock_lamarzocco.serial_number, + CONF_MAC: "aa:bb:cc:dd:ee:ff", + }, + ) + + entry_v1.add_to_hass(hass) + await hass.config_entries.async_setup(entry_v1.entry_id) + await hass.async_block_till_done() + + assert entry_v1.version == 2 + assert dict(entry_v1.data) == dict(mock_config_entry.data) | { + CONF_MAC: "aa:bb:cc:dd:ee:ff" + } + + +async def test_migration_errors( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_cloud_client: MagicMock, + mock_lamarzocco: MagicMock, +) -> None: + """Test errors during migration.""" + + mock_cloud_client.get_customer_fleet.side_effect = RequestNotSuccessful("Error") + + entry_v1 = MockConfigEntry( + domain=DOMAIN, + version=1, + unique_id=mock_lamarzocco.serial_number, + data={ + **USER_INPUT, + CONF_MACHINE: mock_lamarzocco.serial_number, + }, + ) + entry_v1.add_to_hass(hass) + + assert not await hass.config_entries.async_setup(entry_v1.entry_id) + assert entry_v1.state is ConfigEntryState.MIGRATION_ERROR + + +async def test_config_flow_entry_migration_downgrade( + hass: HomeAssistant, +) -> None: + """Test that config entry fails setup if the version is from the future.""" + entry = MockConfigEntry(domain=DOMAIN, version=3) + entry.add_to_hass(hass) + + assert not await hass.config_entries.async_setup(entry.entry_id) + + async def test_bluetooth_is_set_from_discovery( hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_lamarzocco: MagicMock, ) -> None: - """Assert we're not searching for a new BT device when we already found one previously.""" - - # remove the bluetooth configuration from entry - data = mock_config_entry.data.copy() - del data[CONF_NAME] - del data[CONF_MAC] - hass.config_entries.async_update_entry(mock_config_entry, data=data) + """Check we can fill a device from discovery info.""" service_info = get_bluetooth_service_info( - mock_lamarzocco.model_name, mock_lamarzocco.serial_number + mock_lamarzocco.model, mock_lamarzocco.serial_number ) - with patch( - "homeassistant.components.lamarzocco.coordinator.async_discovered_service_info", - return_value=[service_info], + with ( + patch( + "homeassistant.components.lamarzocco.async_discovered_service_info", + return_value=[service_info], + ) as discovery, + patch( + "homeassistant.components.lamarzocco.coordinator.LaMarzoccoMachine" + ) as init_device, ): await async_init_integration(hass, mock_config_entry) - mock_lamarzocco.init_bluetooth_with_known_device.assert_called_once() + discovery.assert_called_once() + init_device.assert_called_once() + _, kwargs = init_device.call_args + assert kwargs["bluetooth_client"] is not None assert mock_config_entry.data[CONF_NAME] == service_info.name assert mock_config_entry.data[CONF_MAC] == service_info.address + + +async def test_websocket_closed_on_unload( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_lamarzocco: MagicMock, +) -> None: + """Test the websocket is closed on unload.""" + with patch( + "homeassistant.components.lamarzocco.LaMarzoccoLocalClient", + autospec=True, + ) as local_client: + client = local_client.return_value + client.websocket = AsyncMock() + client.websocket.connected = True + await async_init_integration(hass, mock_config_entry) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + client.websocket.close.assert_called_once() + + +@pytest.mark.parametrize( + ("version", "issue_exists"), [("v3.5-rc6", False), ("v3.3-rc4", True)] +) +async def test_gateway_version_issue( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_lamarzocco: MagicMock, + version: str, + issue_exists: bool, +) -> None: + """Make sure we get the issue for certain gateway firmware versions.""" + mock_lamarzocco.firmware[FirmwareType.GATEWAY].current_version = version + + await async_init_integration(hass, mock_config_entry) + + issue_registry = ir.async_get(hass) + issue = issue_registry.async_get_issue(DOMAIN, "unsupported_gateway_firmware") + assert (issue is not None) == issue_exists diff --git a/tests/components/lamarzocco/test_number.py b/tests/components/lamarzocco/test_number.py index 8cba3d2387d..288c78c26dd 100644 --- a/tests/components/lamarzocco/test_number.py +++ b/tests/components/lamarzocco/test_number.py @@ -2,7 +2,13 @@ from unittest.mock import MagicMock -from lmcloud.const import KEYS_PER_MODEL, LaMarzoccoModel +from lmcloud.const import ( + KEYS_PER_MODEL, + BoilerType, + MachineModel, + PhysicalKey, + PrebrewMode, +) import pytest from syrupy import SnapshotAssertion @@ -15,17 +21,22 @@ from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er -pytestmark = pytest.mark.usefixtures("init_integration") +from . import async_init_integration + +from tests.common import MockConfigEntry async def test_coffee_boiler( hass: HomeAssistant, mock_lamarzocco: MagicMock, + mock_config_entry: MockConfigEntry, entity_registry: er.EntityRegistry, device_registry: dr.DeviceRegistry, snapshot: SnapshotAssertion, ) -> None: """Test the La Marzocco coffee temperature Number.""" + + await async_init_integration(hass, mock_config_entry) serial_number = mock_lamarzocco.serial_number state = hass.states.get(f"number.{serial_number}_coffee_target_temperature") @@ -47,35 +58,34 @@ async def test_coffee_boiler( SERVICE_SET_VALUE, { ATTR_ENTITY_ID: f"number.{serial_number}_coffee_target_temperature", - ATTR_VALUE: 95, + ATTR_VALUE: 94, }, blocking=True, ) - assert len(mock_lamarzocco.set_coffee_temp.mock_calls) == 1 - mock_lamarzocco.set_coffee_temp.assert_called_once_with( - temperature=95, ble_device=None + assert len(mock_lamarzocco.set_temp.mock_calls) == 1 + mock_lamarzocco.set_temp.assert_called_once_with( + boiler=BoilerType.COFFEE, temperature=94 ) -@pytest.mark.parametrize( - "device_fixture", [LaMarzoccoModel.GS3_AV, LaMarzoccoModel.GS3_MP] -) +@pytest.mark.parametrize("device_fixture", [MachineModel.GS3_AV, MachineModel.GS3_MP]) @pytest.mark.parametrize( ("entity_name", "value", "func_name", "kwargs"), [ ( "steam_target_temperature", 131, - "set_steam_temp", - {"temperature": 131, "ble_device": None}, + "set_temp", + {"boiler": BoilerType.STEAM, "temperature": 131}, ), - ("tea_water_duration", 15, "set_dose_hot_water", {"value": 15}), + ("tea_water_duration", 15, "set_dose_tea_water", {"dose": 15}), ], ) async def test_gs3_exclusive( hass: HomeAssistant, mock_lamarzocco: MagicMock, + mock_config_entry: MockConfigEntry, entity_registry: er.EntityRegistry, device_registry: dr.DeviceRegistry, snapshot: SnapshotAssertion, @@ -85,7 +95,7 @@ async def test_gs3_exclusive( kwargs: dict[str, float], ) -> None: """Test exclusive entities for GS3 AV/MP.""" - + await async_init_integration(hass, mock_config_entry) serial_number = mock_lamarzocco.serial_number func = getattr(mock_lamarzocco, func_name) @@ -118,14 +128,15 @@ async def test_gs3_exclusive( @pytest.mark.parametrize( - "device_fixture", [LaMarzoccoModel.LINEA_MICRA, LaMarzoccoModel.LINEA_MINI] + "device_fixture", [MachineModel.LINEA_MICRA, MachineModel.LINEA_MINI] ) async def test_gs3_exclusive_none( hass: HomeAssistant, mock_lamarzocco: MagicMock, + mock_config_entry: MockConfigEntry, ) -> None: """Ensure GS3 exclusive is None for unsupported models.""" - + await async_init_integration(hass, mock_config_entry) ENTITIES = ("steam_target_temperature", "tea_water_duration") serial_number = mock_lamarzocco.serial_number @@ -135,29 +146,50 @@ async def test_gs3_exclusive_none( @pytest.mark.parametrize( - "device_fixture", [LaMarzoccoModel.LINEA_MICRA, LaMarzoccoModel.LINEA_MINI] + "device_fixture", [MachineModel.LINEA_MICRA, MachineModel.LINEA_MINI] ) @pytest.mark.parametrize( - ("entity_name", "value", "kwargs"), + ("entity_name", "function_name", "prebrew_mode", "value", "kwargs"), [ - ("prebrew_off_time", 6, {"on_time": 3000, "off_time": 6000, "key": 1}), - ("prebrew_on_time", 6, {"on_time": 6000, "off_time": 5000, "key": 1}), - ("preinfusion_time", 7, {"off_time": 7000, "key": 1}), + ( + "prebrew_off_time", + "set_prebrew_time", + PrebrewMode.PREBREW, + 6, + {"prebrew_off_time": 6.0, "key": PhysicalKey.A}, + ), + ( + "prebrew_on_time", + "set_prebrew_time", + PrebrewMode.PREBREW, + 6, + {"prebrew_on_time": 6.0, "key": PhysicalKey.A}, + ), + ( + "preinfusion_time", + "set_preinfusion_time", + PrebrewMode.PREINFUSION, + 7, + {"preinfusion_time": 7.0, "key": PhysicalKey.A}, + ), ], ) async def test_pre_brew_infusion_numbers( hass: HomeAssistant, mock_lamarzocco: MagicMock, + mock_config_entry: MockConfigEntry, entity_registry: er.EntityRegistry, - device_registry: dr.DeviceRegistry, snapshot: SnapshotAssertion, entity_name: str, + function_name: str, + prebrew_mode: PrebrewMode, value: float, kwargs: dict[str, float], ) -> None: """Test the La Marzocco prebrew/-infusion sensors.""" - mock_lamarzocco.current_status["enable_preinfusion"] = True + mock_lamarzocco.config.prebrew_mode = prebrew_mode + await async_init_integration(hass, mock_config_entry) serial_number = mock_lamarzocco.serial_number @@ -168,12 +200,8 @@ async def test_pre_brew_infusion_numbers( entry = entity_registry.async_get(state.entity_id) assert entry - assert entry.device_id assert entry == snapshot - device = device_registry.async_get(entry.device_id) - assert device - # service call await hass.services.async_call( NUMBER_DOMAIN, @@ -185,43 +213,97 @@ async def test_pre_brew_infusion_numbers( blocking=True, ) - assert len(mock_lamarzocco.configure_prebrew.mock_calls) == 1 - mock_lamarzocco.configure_prebrew.assert_called_once_with(**kwargs) + function = getattr(mock_lamarzocco, function_name) + function.assert_called_once_with(**kwargs) -@pytest.mark.parametrize("device_fixture", [LaMarzoccoModel.GS3_AV]) +@pytest.mark.parametrize( + "device_fixture", [MachineModel.LINEA_MICRA, MachineModel.LINEA_MINI] +) +@pytest.mark.parametrize( + ("prebrew_mode", "entity", "unavailable"), + [ + ( + PrebrewMode.PREBREW, + ("prebrew_off_time", "prebrew_on_time"), + ("preinfusion_time",), + ), + ( + PrebrewMode.PREINFUSION, + ("preinfusion_time",), + ("prebrew_off_time", "prebrew_on_time"), + ), + ], +) +async def test_pre_brew_infusion_numbers_unavailable( + hass: HomeAssistant, + mock_lamarzocco: MagicMock, + mock_config_entry: MockConfigEntry, + prebrew_mode: PrebrewMode, + entity: tuple[str, ...], + unavailable: tuple[str, ...], +) -> None: + """Test entities are unavailable depending on selected state.""" + + mock_lamarzocco.config.prebrew_mode = prebrew_mode + await async_init_integration(hass, mock_config_entry) + + serial_number = mock_lamarzocco.serial_number + for entity_name in entity: + state = hass.states.get(f"number.{serial_number}_{entity_name}") + assert state + assert state.state != STATE_UNAVAILABLE + + for entity_name in unavailable: + state = hass.states.get(f"number.{serial_number}_{entity_name}") + assert state + assert state.state == STATE_UNAVAILABLE + + +@pytest.mark.parametrize("device_fixture", [MachineModel.GS3_AV]) @pytest.mark.usefixtures("entity_registry_enabled_by_default") @pytest.mark.parametrize( - ("entity_name", "value", "function_name", "kwargs"), + ("entity_name", "value", "prebrew_mode", "function_name", "kwargs"), [ ( "prebrew_off_time", 6, - "configure_prebrew", - {"on_time": 3000, "off_time": 6000}, + PrebrewMode.PREBREW, + "set_prebrew_time", + {"prebrew_off_time": 6.0}, ), ( "prebrew_on_time", 6, - "configure_prebrew", - {"on_time": 6000, "off_time": 5000}, + PrebrewMode.PREBREW, + "set_prebrew_time", + {"prebrew_on_time": 6.0}, ), - ("preinfusion_time", 7, "configure_prebrew", {"off_time": 7000}), - ("dose", 6, "set_dose", {"value": 6}), + ( + "preinfusion_time", + 7, + PrebrewMode.PREINFUSION, + "set_preinfusion_time", + {"preinfusion_time": 7.0}, + ), + ("dose", 6, PrebrewMode.DISABLED, "set_dose", {"dose": 6}), ], ) async def test_pre_brew_infusion_key_numbers( hass: HomeAssistant, mock_lamarzocco: MagicMock, + mock_config_entry: MockConfigEntry, snapshot: SnapshotAssertion, entity_name: str, value: float, + prebrew_mode: PrebrewMode, function_name: str, kwargs: dict[str, float], ) -> None: """Test the La Marzocco number sensors for GS3AV model.""" - mock_lamarzocco.current_status["enable_preinfusion"] = True + mock_lamarzocco.config.prebrew_mode = prebrew_mode + await async_init_integration(hass, mock_config_entry) serial_number = mock_lamarzocco.serial_number @@ -230,7 +312,7 @@ async def test_pre_brew_infusion_key_numbers( state = hass.states.get(f"number.{serial_number}_{entity_name}") assert state is None - for key in range(1, KEYS_PER_MODEL[mock_lamarzocco.model_name] + 1): + for key in PhysicalKey: state = hass.states.get(f"number.{serial_number}_{entity_name}_key_{key}") assert state assert state == snapshot(name=f"{serial_number}_{entity_name}_key_{key}-state") @@ -248,17 +330,18 @@ async def test_pre_brew_infusion_key_numbers( kwargs["key"] = key - assert len(func.mock_calls) == key + assert len(func.mock_calls) == key.value func.assert_called_with(**kwargs) -@pytest.mark.parametrize("device_fixture", [LaMarzoccoModel.GS3_AV]) +@pytest.mark.parametrize("device_fixture", [MachineModel.GS3_AV]) async def test_disabled_entites( hass: HomeAssistant, mock_lamarzocco: MagicMock, + mock_config_entry: MockConfigEntry, ) -> None: """Test the La Marzocco prebrew/-infusion sensors for GS3AV model.""" - + await async_init_integration(hass, mock_config_entry) ENTITIES = ( "prebrew_off_time", "prebrew_on_time", @@ -269,21 +352,22 @@ async def test_disabled_entites( serial_number = mock_lamarzocco.serial_number for entity_name in ENTITIES: - for key in range(1, KEYS_PER_MODEL[mock_lamarzocco.model_name] + 1): + for key in PhysicalKey: state = hass.states.get(f"number.{serial_number}_{entity_name}_key_{key}") assert state is None @pytest.mark.parametrize( "device_fixture", - [LaMarzoccoModel.GS3_MP, LaMarzoccoModel.LINEA_MICRA, LaMarzoccoModel.LINEA_MINI], + [MachineModel.GS3_MP, MachineModel.LINEA_MICRA, MachineModel.LINEA_MINI], ) -async def test_not_existing_key_entites( +async def test_not_existing_key_entities( hass: HomeAssistant, mock_lamarzocco: MagicMock, + mock_config_entry: MockConfigEntry, ) -> None: """Assert not existing key entities.""" - + await async_init_integration(hass, mock_config_entry) serial_number = mock_lamarzocco.serial_number for entity in ( @@ -292,42 +376,6 @@ async def test_not_existing_key_entites( "preinfusion_time", "set_dose", ): - for key in range(1, KEYS_PER_MODEL[LaMarzoccoModel.GS3_AV] + 1): + for key in range(1, KEYS_PER_MODEL[MachineModel.GS3_AV] + 1): state = hass.states.get(f"number.{serial_number}_{entity}_key_{key}") assert state is None - - -@pytest.mark.parametrize( - "device_fixture", - [LaMarzoccoModel.GS3_MP], -) -async def test_not_existing_entites( - hass: HomeAssistant, - mock_lamarzocco: MagicMock, -) -> None: - """Assert not existing entities.""" - - serial_number = mock_lamarzocco.serial_number - - for entity in ( - "prebrew_off_time", - "prebrew_on_time", - "preinfusion_time", - "set_dose", - ): - state = hass.states.get(f"number.{serial_number}_{entity}") - assert state is None - - -@pytest.mark.parametrize("device_fixture", [LaMarzoccoModel.LINEA_MICRA]) -async def test_not_settable_entites( - hass: HomeAssistant, - mock_lamarzocco: MagicMock, -) -> None: - """Assert not settable causes error.""" - - serial_number = mock_lamarzocco.serial_number - - state = hass.states.get(f"number.{serial_number}_preinfusion_time") - assert state - assert state.state == STATE_UNAVAILABLE diff --git a/tests/components/lamarzocco/test_select.py b/tests/components/lamarzocco/test_select.py index 497a95f6d0d..e3521b473bd 100644 --- a/tests/components/lamarzocco/test_select.py +++ b/tests/components/lamarzocco/test_select.py @@ -2,7 +2,7 @@ from unittest.mock import MagicMock -from lmcloud.const import LaMarzoccoModel +from lmcloud.const import MachineModel, PrebrewMode, SteamLevel import pytest from syrupy import SnapshotAssertion @@ -18,7 +18,7 @@ from homeassistant.helpers import entity_registry as er pytestmark = pytest.mark.usefixtures("init_integration") -@pytest.mark.parametrize("device_fixture", [LaMarzoccoModel.LINEA_MICRA]) +@pytest.mark.parametrize("device_fixture", [MachineModel.LINEA_MICRA]) async def test_steam_boiler_level( hass: HomeAssistant, entity_registry: er.EntityRegistry, @@ -44,18 +44,17 @@ async def test_steam_boiler_level( SERVICE_SELECT_OPTION, { ATTR_ENTITY_ID: f"select.{serial_number}_steam_level", - ATTR_OPTION: "1", + ATTR_OPTION: "2", }, blocking=True, ) - assert len(mock_lamarzocco.set_steam_level.mock_calls) == 1 - mock_lamarzocco.set_steam_level.assert_called_once_with(1, None) + mock_lamarzocco.set_steam_level.assert_called_once_with(level=SteamLevel.LEVEL_2) @pytest.mark.parametrize( "device_fixture", - [LaMarzoccoModel.GS3_AV, LaMarzoccoModel.GS3_MP, LaMarzoccoModel.LINEA_MINI], + [MachineModel.GS3_AV, MachineModel.GS3_MP, MachineModel.LINEA_MINI], ) async def test_steam_boiler_level_none( hass: HomeAssistant, @@ -70,7 +69,7 @@ async def test_steam_boiler_level_none( @pytest.mark.parametrize( "device_fixture", - [LaMarzoccoModel.LINEA_MICRA, LaMarzoccoModel.GS3_AV, LaMarzoccoModel.LINEA_MINI], + [MachineModel.LINEA_MICRA, MachineModel.GS3_AV, MachineModel.LINEA_MINI], ) async def test_pre_brew_infusion_select( hass: HomeAssistant, @@ -97,20 +96,17 @@ async def test_pre_brew_infusion_select( SERVICE_SELECT_OPTION, { ATTR_ENTITY_ID: f"select.{serial_number}_prebrew_infusion_mode", - ATTR_OPTION: "preinfusion", + ATTR_OPTION: "prebrew", }, blocking=True, ) - assert len(mock_lamarzocco.select_pre_brew_infusion_mode.mock_calls) == 1 - mock_lamarzocco.select_pre_brew_infusion_mode.assert_called_once_with( - mode="Preinfusion" - ) + mock_lamarzocco.set_prebrew_mode.assert_called_once_with(mode=PrebrewMode.PREBREW) @pytest.mark.parametrize( "device_fixture", - [LaMarzoccoModel.GS3_MP], + [MachineModel.GS3_MP], ) async def test_pre_brew_infusion_select_none( hass: HomeAssistant, diff --git a/tests/components/lamarzocco/test_switch.py b/tests/components/lamarzocco/test_switch.py index e1924f0a8ca..19950a0c21e 100644 --- a/tests/components/lamarzocco/test_switch.py +++ b/tests/components/lamarzocco/test_switch.py @@ -5,7 +5,6 @@ from unittest.mock import MagicMock import pytest from syrupy import SnapshotAssertion -from homeassistant.components.lamarzocco.const import DOMAIN from homeassistant.components.switch import ( DOMAIN as SWITCH_DOMAIN, SERVICE_TURN_OFF, @@ -15,35 +14,39 @@ from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er -from tests.common import MockConfigEntry +from . import async_init_integration -pytestmark = pytest.mark.usefixtures("init_integration") +from tests.common import MockConfigEntry @pytest.mark.parametrize( - ("entity_name", "method_name", "args_on", "args_off"), + ( + "entity_name", + "method_name", + ), [ - ("", "set_power", (True, None), (False, None)), ( - "_auto_on_off", - "set_auto_on_off_global", - (True,), - (False,), + "", + "set_power", + ), + ( + "_steam_boiler", + "set_steam", ), - ("_steam_boiler", "set_steam", (True, None), (False, None)), ], ) async def test_switches( hass: HomeAssistant, mock_lamarzocco: MagicMock, + mock_config_entry: MockConfigEntry, entity_registry: er.EntityRegistry, snapshot: SnapshotAssertion, entity_name: str, method_name: str, - args_on: tuple, - args_off: tuple, ) -> None: """Test the La Marzocco switches.""" + await async_init_integration(hass, mock_config_entry) + serial_number = mock_lamarzocco.serial_number control_fn = getattr(mock_lamarzocco, method_name) @@ -66,7 +69,7 @@ async def test_switches( ) assert len(control_fn.mock_calls) == 1 - control_fn.assert_called_once_with(*args_off) + control_fn.assert_called_once_with(False) await hass.services.async_call( SWITCH_DOMAIN, @@ -78,18 +81,21 @@ async def test_switches( ) assert len(control_fn.mock_calls) == 2 - control_fn.assert_called_with(*args_on) + control_fn.assert_called_with(True) async def test_device( hass: HomeAssistant, mock_lamarzocco: MagicMock, + mock_config_entry: MockConfigEntry, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, snapshot: SnapshotAssertion, ) -> None: """Test the device for one switch.""" + await async_init_integration(hass, mock_config_entry) + state = hass.states.get(f"switch.{mock_lamarzocco.serial_number}") assert state @@ -100,26 +106,3 @@ async def test_device( device = device_registry.async_get(entry.device_id) assert device assert device == snapshot - - -async def test_call_without_bluetooth_works( - hass: HomeAssistant, - mock_lamarzocco: MagicMock, - mock_config_entry: MockConfigEntry, -) -> None: - """Test that if not using bluetooth, the switch still works.""" - serial_number = mock_lamarzocco.serial_number - coordinator = hass.data[DOMAIN][mock_config_entry.entry_id] - coordinator._use_bluetooth = False - - await hass.services.async_call( - SWITCH_DOMAIN, - SERVICE_TURN_OFF, - { - ATTR_ENTITY_ID: f"switch.{serial_number}_steam_boiler", - }, - blocking=True, - ) - - assert len(mock_lamarzocco.set_steam.mock_calls) == 1 - mock_lamarzocco.set_steam.assert_called_once_with(False, None) diff --git a/tests/components/lamarzocco/test_update.py b/tests/components/lamarzocco/test_update.py index 3b1323d1c73..02330daf794 100644 --- a/tests/components/lamarzocco/test_update.py +++ b/tests/components/lamarzocco/test_update.py @@ -2,7 +2,7 @@ from unittest.mock import MagicMock -from lmcloud.const import LaMarzoccoUpdateableComponent +from lmcloud.const import FirmwareType import pytest from syrupy import SnapshotAssertion @@ -18,8 +18,8 @@ pytestmark = pytest.mark.usefixtures("init_integration") @pytest.mark.parametrize( ("entity_name", "component"), [ - ("machine_firmware", LaMarzoccoUpdateableComponent.MACHINE), - ("gateway_firmware", LaMarzoccoUpdateableComponent.GATEWAY), + ("machine_firmware", FirmwareType.MACHINE), + ("gateway_firmware", FirmwareType.GATEWAY), ], ) async def test_update_entites( @@ -28,7 +28,7 @@ async def test_update_entites( entity_registry: er.EntityRegistry, snapshot: SnapshotAssertion, entity_name: str, - component: LaMarzoccoUpdateableComponent, + component: FirmwareType, ) -> None: """Test the La Marzocco update entities.""" From 508564ece2ffc114c7837718d196f0ab7ec12f41 Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Mon, 10 Jun 2024 20:09:39 +0200 Subject: [PATCH 0474/1445] Add more debug logging to Ping integration (#119318) --- homeassistant/components/ping/helpers.py | 31 +++++++++++++++--------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/ping/helpers.py b/homeassistant/components/ping/helpers.py index f1fd8518d42..7f1696d2ed9 100644 --- a/homeassistant/components/ping/helpers.py +++ b/homeassistant/components/ping/helpers.py @@ -9,6 +9,7 @@ from typing import TYPE_CHECKING, Any from icmplib import NameLookupError, async_ping from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import UpdateFailed from .const import ICMP_TIMEOUT, PING_TIMEOUT @@ -58,9 +59,16 @@ class PingDataICMPLib(PingData): timeout=ICMP_TIMEOUT, privileged=self._privileged, ) - except NameLookupError: + except NameLookupError as err: self.is_alive = False - return + raise UpdateFailed(f"Error resolving host: {self.ip_address}") from err + + _LOGGER.debug( + "async_ping returned: reachable=%s sent=%i received=%s", + data.is_alive, + data.packets_sent, + data.packets_received, + ) self.is_alive = data.is_alive if not self.is_alive: @@ -94,6 +102,10 @@ class PingDataSubProcess(PingData): async def async_ping(self) -> dict[str, Any] | None: """Send ICMP echo request and return details if success.""" + _LOGGER.debug( + "Pinging %s with: `%s`", self.ip_address, " ".join(self._ping_cmd) + ) + pinger = await asyncio.create_subprocess_exec( *self._ping_cmd, stdin=None, @@ -140,20 +152,17 @@ class PingDataSubProcess(PingData): if TYPE_CHECKING: assert match is not None rtt_min, rtt_avg, rtt_max, rtt_mdev = match.groups() - except TimeoutError: - _LOGGER.exception( - "Timed out running command: `%s`, after: %ss", - self._ping_cmd, - self._count + PING_TIMEOUT, - ) + except TimeoutError as err: if pinger: with suppress(TypeError): await pinger.kill() # type: ignore[func-returns-value] del pinger - return None - except AttributeError: - return None + raise UpdateFailed( + f"Timed out running command: `{self._ping_cmd}`, after: {self._count + PING_TIMEOUT}s" + ) from err + except AttributeError as err: + raise UpdateFailed from err return {"min": rtt_min, "avg": rtt_avg, "max": rtt_max, "mdev": rtt_mdev} async def async_update(self) -> None: From ac08cd1201320f6b18a919415a94e8bb0af46281 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 10 Jun 2024 20:20:25 +0200 Subject: [PATCH 0475/1445] Revert SamsungTV migration (#119234) --- homeassistant/components/samsungtv/__init__.py | 11 +---------- tests/components/samsungtv/test_init.py | 6 +++++- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/samsungtv/__init__.py b/homeassistant/components/samsungtv/__init__.py index f49ae276665..992c86d5d7e 100644 --- a/homeassistant/components/samsungtv/__init__.py +++ b/homeassistant/components/samsungtv/__init__.py @@ -297,16 +297,7 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> if version == 2: if minor_version < 2: # Cleanup invalid MAC addresses - see #103512 - dev_reg = dr.async_get(hass) - for device in dr.async_entries_for_config_entry( - dev_reg, config_entry.entry_id - ): - new_connections = device.connections.copy() - new_connections.discard((dr.CONNECTION_NETWORK_MAC, "none")) - if new_connections != device.connections: - dev_reg.async_update_device( - device.id, new_connections=new_connections - ) + # Reverted due to device registry collisions - see #119082 / #119249 minor_version = 2 hass.config_entries.async_update_entry(config_entry, minor_version=2) diff --git a/tests/components/samsungtv/test_init.py b/tests/components/samsungtv/test_init.py index 4efcf62c1dd..479664d4ec0 100644 --- a/tests/components/samsungtv/test_init.py +++ b/tests/components/samsungtv/test_init.py @@ -220,10 +220,14 @@ async def test_incorrectly_formatted_mac_fixed(hass: HomeAssistant) -> None: @pytest.mark.usefixtures("remotews", "rest_api") +@pytest.mark.xfail async def test_cleanup_mac( hass: HomeAssistant, device_registry: dr.DeviceRegistry, snapshot: SnapshotAssertion ) -> None: - """Test for `none` mac cleanup #103512.""" + """Test for `none` mac cleanup #103512. + + Reverted due to device registry collisions in #119249 / #119082 + """ entry = MockConfigEntry( domain=SAMSUNGTV_DOMAIN, data=MOCK_ENTRY_WS_WITH_MAC, From f75cc1da243b55ca636ab1d8eaa609a27bb037ac Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 10 Jun 2024 20:22:04 +0200 Subject: [PATCH 0476/1445] Update frontend to 20240610.0 (#119320) --- 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 27322b423d0..d3d19375105 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==20240605.0"] + "requirements": ["home-assistant-frontend==20240610.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 05086aadd4b..ef4cb7773cb 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -32,7 +32,7 @@ habluetooth==3.1.1 hass-nabucasa==0.81.1 hassil==1.7.1 home-assistant-bluetooth==1.12.1 -home-assistant-frontend==20240605.0 +home-assistant-frontend==20240610.0 home-assistant-intents==2024.6.5 httpx==0.27.0 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 0403cd555f0..14ede992a85 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1090,7 +1090,7 @@ hole==0.8.0 holidays==0.50 # homeassistant.components.frontend -home-assistant-frontend==20240605.0 +home-assistant-frontend==20240610.0 # homeassistant.components.conversation home-assistant-intents==2024.6.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fe147205686..aad6dc6124a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -892,7 +892,7 @@ hole==0.8.0 holidays==0.50 # homeassistant.components.frontend -home-assistant-frontend==20240605.0 +home-assistant-frontend==20240610.0 # homeassistant.components.conversation home-assistant-intents==2024.6.5 From aa419686cb0cd5d4550bdc7082ba20ab5c25aa2f Mon Sep 17 00:00:00 2001 From: karwosts <32912880+karwosts@users.noreply.github.com> Date: Mon, 10 Jun 2024 11:23:21 -0700 Subject: [PATCH 0477/1445] Fix statistic_during_period wrongly prioritizing ST statistics over LT (#115291) * Fix statistic_during_period wrongly prioritizing ST statistics over LT * comment * start of a test * more testcases * fix sts insertion range * update from review * remove unneeded comments * update logic * min/mean/max testing --- .../components/recorder/statistics.py | 20 +- .../components/recorder/test_websocket_api.py | 328 ++++++++++++++++++ 2 files changed, 343 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index 84c82f35264..8d077e19344 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -1262,6 +1262,7 @@ def _get_oldest_sum_statistic( main_start_time: datetime | None, tail_start_time: datetime | None, oldest_stat: datetime | None, + oldest_5_min_stat: datetime | None, tail_only: bool, metadata_id: int, ) -> float | None: @@ -1306,6 +1307,15 @@ def _get_oldest_sum_statistic( if ( head_start_time is not None + and oldest_5_min_stat is not None + and ( + # If we want stats older than the short term purge window, don't lookup + # the oldest sum in the short term table, as it would be prioritized + # over older LongTermStats. + (oldest_stat is None) + or (oldest_5_min_stat < oldest_stat) + or (oldest_5_min_stat <= head_start_time) + ) and ( oldest_sum := _get_oldest_sum_statistic_in_sub_period( session, head_start_time, StatisticsShortTerm, metadata_id @@ -1477,12 +1487,11 @@ def statistic_during_period( tail_end_time: datetime | None = None if end_time is None: tail_start_time = now.replace(minute=0, second=0, microsecond=0) + elif tail_only: + tail_start_time = start_time + tail_end_time = end_time elif end_time.minute: - tail_start_time = ( - start_time - if tail_only - else end_time.replace(minute=0, second=0, microsecond=0) - ) + tail_start_time = end_time.replace(minute=0, second=0, microsecond=0) tail_end_time = end_time # Calculate the main period @@ -1517,6 +1526,7 @@ def statistic_during_period( main_start_time, tail_start_time, oldest_stat, + oldest_5_min_stat, tail_only, metadata_id, ) diff --git a/tests/components/recorder/test_websocket_api.py b/tests/components/recorder/test_websocket_api.py index 3d35aafb2b3..0dd9241776d 100644 --- a/tests/components/recorder/test_websocket_api.py +++ b/tests/components/recorder/test_websocket_api.py @@ -794,6 +794,334 @@ async def test_statistic_during_period_hole( } +@pytest.mark.freeze_time(datetime.datetime(2022, 10, 21, 6, 31, tzinfo=datetime.UTC)) +async def test_statistic_during_period_partial_overlap( + recorder_mock: Recorder, + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, +) -> None: + """Test statistic_during_period.""" + now = dt_util.utcnow() + + await async_recorder_block_till_done(hass) + client = await hass_ws_client() + + zero = now + start = zero.replace(hour=0, minute=0, second=0, microsecond=0) + + # Sum shall be tracking a hypothetical sensor that is 0 at midnight, and grows by 1 per minute. + # The test will have 4 hours of LTS-only data (0:00-3:59:59), followed by 2 hours of overlapping STS/LTS (4:00-5:59:59), followed by 30 minutes of STS only (6:00-6:29:59) + # similar to how a real recorder might look after purging STS. + + # The datapoint at i=0 (start = 0:00) will be 60 as that is the growth during the hour starting at the start period + imported_stats_hours = [ + { + "start": (start + timedelta(hours=i)), + "min": i * 60, + "max": i * 60 + 60, + "mean": i * 60 + 30, + "sum": (i + 1) * 60, + } + for i in range(6) + ] + + # The datapoint at i=0 (start = 4:00) would be the sensor's value at t=4:05, or 245 + imported_stats_5min = [ + { + "start": (start + timedelta(hours=4, minutes=5 * i)), + "min": 4 * 60 + i * 5, + "max": 4 * 60 + i * 5 + 5, + "mean": 4 * 60 + i * 5 + 2.5, + "sum": 4 * 60 + (i + 1) * 5, + } + for i in range(30) + ] + + assert imported_stats_hours[-1]["sum"] == 360 + assert imported_stats_hours[-1]["start"] == start.replace( + hour=5, minute=0, second=0, microsecond=0 + ) + assert imported_stats_5min[-1]["sum"] == 390 + assert imported_stats_5min[-1]["start"] == start.replace( + hour=6, minute=25, second=0, microsecond=0 + ) + + statId = "sensor.test_overlapping" + imported_metadata = { + "has_mean": False, + "has_sum": True, + "name": "Total imported energy overlapping", + "source": "recorder", + "statistic_id": statId, + "unit_of_measurement": "kWh", + } + + recorder.get_instance(hass).async_import_statistics( + imported_metadata, + imported_stats_hours, + Statistics, + ) + recorder.get_instance(hass).async_import_statistics( + imported_metadata, + imported_stats_5min, + StatisticsShortTerm, + ) + await async_wait_recording_done(hass) + + metadata = get_metadata(hass, statistic_ids={statId}) + metadata_id = metadata[statId][0] + run_cache = get_short_term_statistics_run_cache(hass) + # Verify the import of the short term statistics + # also updates the run cache + assert run_cache.get_latest_ids({metadata_id}) is not None + + # Get all the stats, should consider all hours and 5mins + await client.send_json_auto_id( + { + "type": "recorder/statistic_during_period", + "statistic_id": statId, + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == { + "change": 390, + "max": 390, + "min": 0, + "mean": 195, + } + + async def assert_stat_during_fixed(client, start_time, end_time, expect): + json = { + "type": "recorder/statistic_during_period", + "types": list(expect.keys()), + "statistic_id": statId, + "fixed_period": {}, + } + if start_time: + json["fixed_period"]["start_time"] = start_time.isoformat() + if end_time: + json["fixed_period"]["end_time"] = end_time.isoformat() + + await client.send_json_auto_id(json) + response = await client.receive_json() + assert response["success"] + assert response["result"] == expect + + # One hours worth of growth in LTS-only + start_time = start.replace(hour=1) + end_time = start.replace(hour=2) + await assert_stat_during_fixed( + client, start_time, end_time, {"change": 60, "min": 60, "max": 120, "mean": 90} + ) + + # Five minutes of growth in STS-only + start_time = start.replace(hour=6, minute=15) + end_time = start.replace(hour=6, minute=20) + await assert_stat_during_fixed( + client, + start_time, + end_time, + { + "change": 5, + "min": 6 * 60 + 15, + "max": 6 * 60 + 20, + "mean": 6 * 60 + (15 + 20) / 2, + }, + ) + + # Six minutes of growth in STS-only + start_time = start.replace(hour=6, minute=14) + end_time = start.replace(hour=6, minute=20) + await assert_stat_during_fixed( + client, + start_time, + end_time, + { + "change": 5, + "min": 6 * 60 + 15, + "max": 6 * 60 + 20, + "mean": 6 * 60 + (15 + 20) / 2, + }, + ) + + # Six minutes of growth in STS-only + # 5-minute Change includes start times exactly on or before a statistics start, but end times are not counted unless they are greater than start. + start_time = start.replace(hour=6, minute=15) + end_time = start.replace(hour=6, minute=21) + await assert_stat_during_fixed( + client, + start_time, + end_time, + { + "change": 10, + "min": 6 * 60 + 15, + "max": 6 * 60 + 25, + "mean": 6 * 60 + (15 + 25) / 2, + }, + ) + + # Five minutes of growth in overlapping LTS+STS + start_time = start.replace(hour=5, minute=15) + end_time = start.replace(hour=5, minute=20) + await assert_stat_during_fixed( + client, + start_time, + end_time, + { + "change": 5, + "min": 5 * 60 + 15, + "max": 5 * 60 + 20, + "mean": 5 * 60 + (15 + 20) / 2, + }, + ) + + # Five minutes of growth in overlapping LTS+STS (start of hour) + start_time = start.replace(hour=5, minute=0) + end_time = start.replace(hour=5, minute=5) + await assert_stat_during_fixed( + client, + start_time, + end_time, + {"change": 5, "min": 5 * 60, "max": 5 * 60 + 5, "mean": 5 * 60 + (5) / 2}, + ) + + # Five minutes of growth in overlapping LTS+STS (end of hour) + start_time = start.replace(hour=4, minute=55) + end_time = start.replace(hour=5, minute=0) + await assert_stat_during_fixed( + client, + start_time, + end_time, + { + "change": 5, + "min": 4 * 60 + 55, + "max": 5 * 60, + "mean": 4 * 60 + (55 + 60) / 2, + }, + ) + + # Five minutes of growth in STS-only, with a minute offset. Despite that this does not cover the full period, result is still 5 + start_time = start.replace(hour=6, minute=16) + end_time = start.replace(hour=6, minute=21) + await assert_stat_during_fixed( + client, + start_time, + end_time, + { + "change": 5, + "min": 6 * 60 + 20, + "max": 6 * 60 + 25, + "mean": 6 * 60 + (20 + 25) / 2, + }, + ) + + # 7 minutes of growth in STS-only, spanning two intervals + start_time = start.replace(hour=6, minute=14) + end_time = start.replace(hour=6, minute=21) + await assert_stat_during_fixed( + client, + start_time, + end_time, + { + "change": 10, + "min": 6 * 60 + 15, + "max": 6 * 60 + 25, + "mean": 6 * 60 + (15 + 25) / 2, + }, + ) + + # One hours worth of growth in LTS-only, with arbitrary minute offsets + # Since this does not fully cover the hour, result is None? + start_time = start.replace(hour=1, minute=40) + end_time = start.replace(hour=2, minute=12) + await assert_stat_during_fixed( + client, + start_time, + end_time, + {"change": None, "min": None, "max": None, "mean": None}, + ) + + # One hours worth of growth in LTS-only, with arbitrary minute offsets, covering a whole 1-hour period + start_time = start.replace(hour=1, minute=40) + end_time = start.replace(hour=3, minute=12) + await assert_stat_during_fixed( + client, + start_time, + end_time, + {"change": 60, "min": 120, "max": 180, "mean": 150}, + ) + + # 90 minutes of growth in window overlapping LTS+STS/STS-only (4:41 - 6:11) + start_time = start.replace(hour=4, minute=41) + end_time = start_time + timedelta(minutes=90) + await assert_stat_during_fixed( + client, + start_time, + end_time, + { + "change": 90, + "min": 4 * 60 + 45, + "max": 4 * 60 + 45 + 90, + "mean": 4 * 60 + 45 + 45, + }, + ) + + # 4 hours of growth in overlapping LTS-only/LTS+STS (2:01-6:01) + start_time = start.replace(hour=2, minute=1) + end_time = start_time + timedelta(minutes=240) + # 60 from LTS (3:00-3:59), 125 from STS (25 intervals) (4:00-6:01) + await assert_stat_during_fixed( + client, + start_time, + end_time, + {"change": 185, "min": 3 * 60, "max": 3 * 60 + 185, "mean": 3 * 60 + 185 / 2}, + ) + + # 4 hours of growth in overlapping LTS-only/LTS+STS (1:31-5:31) + start_time = start.replace(hour=1, minute=31) + end_time = start_time + timedelta(minutes=240) + # 120 from LTS (2:00-3:59), 95 from STS (19 intervals) 4:00-5:31 + await assert_stat_during_fixed( + client, + start_time, + end_time, + {"change": 215, "min": 2 * 60, "max": 2 * 60 + 215, "mean": 2 * 60 + 215 / 2}, + ) + + # 5 hours of growth, start time only (1:31-end) + start_time = start.replace(hour=1, minute=31) + end_time = None + # will be actually 2:00 - end + await assert_stat_during_fixed( + client, + start_time, + end_time, + {"change": 4 * 60 + 30, "min": 120, "max": 390, "mean": (390 + 120) / 2}, + ) + + # 5 hours of growth, end_time_only (0:00-5:00) + start_time = None + end_time = start.replace(hour=5) + await assert_stat_during_fixed( + client, + start_time, + end_time, + {"change": 5 * 60, "min": 0, "max": 5 * 60, "mean": (5 * 60) / 2}, + ) + + # 5 hours 1 minute of growth, end_time_only (0:00-5:01) + start_time = None + end_time = start.replace(hour=5, minute=1) + # 4 hours LTS, 1 hour and 5 minutes STS (4:00-5:01) + await assert_stat_during_fixed( + client, + start_time, + end_time, + {"change": 5 * 60 + 5, "min": 0, "max": 5 * 60 + 5, "mean": (5 * 60 + 5) / 2}, + ) + + @pytest.mark.freeze_time(datetime.datetime(2022, 10, 21, 7, 25, tzinfo=datetime.UTC)) @pytest.mark.parametrize( ("calendar_period", "start_time", "end_time"), From eb6af2238c60ee41058c0d23b592cb9b9d4cd803 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 10 Jun 2024 20:25:34 +0200 Subject: [PATCH 0478/1445] Improve type hints in registry helper tests (#119302) --- tests/helpers/test_category_registry.py | 2 +- tests/helpers/test_device_registry.py | 12 +++---- tests/helpers/test_entity_registry.py | 24 ++++++-------- tests/helpers/test_floor_registry.py | 32 +++++++------------ tests/helpers/test_label_registry.py | 32 +++++++------------ .../test_normalized_name_base_registry.py | 10 +++--- 6 files changed, 45 insertions(+), 67 deletions(-) diff --git a/tests/helpers/test_category_registry.py b/tests/helpers/test_category_registry.py index 1800b3babe9..1317750ebec 100644 --- a/tests/helpers/test_category_registry.py +++ b/tests/helpers/test_category_registry.py @@ -340,7 +340,7 @@ async def test_load_categories( @pytest.mark.parametrize("load_registries", [False]) async def test_loading_categories_from_storage( - hass: HomeAssistant, hass_storage: Any + hass: HomeAssistant, hass_storage: dict[str, Any] ) -> None: """Test loading stored categories on start.""" hass_storage[cr.STORAGE_KEY] = { diff --git a/tests/helpers/test_device_registry.py b/tests/helpers/test_device_registry.py index da99f176a3c..3ad45d630df 100644 --- a/tests/helpers/test_device_registry.py +++ b/tests/helpers/test_device_registry.py @@ -534,7 +534,7 @@ async def test_migration_1_3_to_1_5( hass: HomeAssistant, hass_storage: dict[str, Any], mock_config_entry: MockConfigEntry, -): +) -> None: """Test migration from version 1.3 to 1.5.""" hass_storage[dr.STORAGE_KEY] = { "version": 1, @@ -659,7 +659,7 @@ async def test_migration_1_4_to_1_5( hass: HomeAssistant, hass_storage: dict[str, Any], mock_config_entry: MockConfigEntry, -): +) -> None: """Test migration from version 1.4 to 1.5.""" hass_storage[dr.STORAGE_KEY] = { "version": 1, @@ -1219,7 +1219,7 @@ async def test_format_mac( config_entry_id=mock_config_entry.entry_id, connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, ) - for mac in ["123456ABCDEF", "123456abcdef", "12:34:56:ab:cd:ef", "1234.56ab.cdef"]: + for mac in ("123456ABCDEF", "123456abcdef", "12:34:56:ab:cd:ef", "1234.56ab.cdef"): test_entry = device_registry.async_get_or_create( config_entry_id=mock_config_entry.entry_id, connections={(dr.CONNECTION_NETWORK_MAC, mac)}, @@ -1230,14 +1230,14 @@ async def test_format_mac( } # This should not raise - for invalid in [ + for invalid in ( "invalid_mac", "123456ABCDEFG", # 1 extra char "12:34:56:ab:cdef", # not enough : "12:34:56:ab:cd:e:f", # too many : "1234.56abcdef", # not enough . "123.456.abc.def", # too many . - ]: + ): invalid_mac_entry = device_registry.async_get_or_create( config_entry_id=mock_config_entry.entry_id, connections={(dr.CONNECTION_NETWORK_MAC, invalid)}, @@ -2235,7 +2235,7 @@ async def test_device_info_configuration_url_validation( hass: HomeAssistant, device_registry: dr.DeviceRegistry, configuration_url: str | URL | None, - expectation, + expectation: AbstractContextManager, ) -> None: """Test configuration URL of device info is properly validated.""" config_entry_1 = MockConfigEntry() diff --git a/tests/helpers/test_entity_registry.py b/tests/helpers/test_entity_registry.py index 4256707b7b1..4dc8d79be3f 100644 --- a/tests/helpers/test_entity_registry.py +++ b/tests/helpers/test_entity_registry.py @@ -30,7 +30,7 @@ from tests.common import ( YAML__OPEN_PATH = "homeassistant.util.yaml.loader.open" -async def test_get(entity_registry: er.EntityRegistry): +async def test_get(entity_registry: er.EntityRegistry) -> None: """Test we can get an item.""" entry = entity_registry.async_get_or_create("light", "hue", "1234") @@ -620,7 +620,7 @@ async def test_removing_config_entry_id( async def test_deleted_entity_removing_config_entry_id( entity_registry: er.EntityRegistry, -): +) -> None: """Test that we update config entry id in registry on deleted entity.""" mock_config = MockConfigEntry(domain="light", entry_id="mock-id-1") @@ -1528,9 +1528,7 @@ def test_entity_registry_items() -> None: assert entities.get_entry(entry2.id) is None -async def test_disabled_by_str_not_allowed( - hass: HomeAssistant, entity_registry: er.EntityRegistry -) -> None: +async def test_disabled_by_str_not_allowed(entity_registry: er.EntityRegistry) -> None: """Test we need to pass disabled by type.""" with pytest.raises(ValueError): entity_registry.async_get_or_create( @@ -1545,7 +1543,7 @@ async def test_disabled_by_str_not_allowed( async def test_entity_category_str_not_allowed( - hass: HomeAssistant, entity_registry: er.EntityRegistry + entity_registry: er.EntityRegistry, ) -> None: """Test we need to pass entity category type.""" with pytest.raises(ValueError): @@ -1574,9 +1572,7 @@ async def test_hidden_by_str_not_allowed(entity_registry: er.EntityRegistry) -> ) -async def test_unique_id_non_hashable( - hass: HomeAssistant, entity_registry: er.EntityRegistry -) -> None: +async def test_unique_id_non_hashable(entity_registry: er.EntityRegistry) -> None: """Test unique_id which is not hashable.""" with pytest.raises(TypeError): entity_registry.async_get_or_create("light", "hue", ["not", "valid"]) @@ -1587,9 +1583,7 @@ async def test_unique_id_non_hashable( async def test_unique_id_non_string( - hass: HomeAssistant, - entity_registry: er.EntityRegistry, - caplog: pytest.LogCaptureFixture, + entity_registry: er.EntityRegistry, caplog: pytest.LogCaptureFixture ) -> None: """Test unique_id which is not a string.""" entity_registry.async_get_or_create("light", "hue", 1234) @@ -1683,7 +1677,7 @@ async def test_restore_entity( hass: HomeAssistant, entity_registry: er.EntityRegistry, freezer: FrozenDateTimeFactory, -): +) -> None: """Make sure entity registry id is stable and entity_id is reused if possible.""" update_events = async_capture_events(hass, er.EVENT_ENTITY_REGISTRY_UPDATED) config_entry = MockConfigEntry(domain="light") @@ -1777,7 +1771,7 @@ async def test_restore_entity( async def test_async_migrate_entry_delete_self( hass: HomeAssistant, entity_registry: er.EntityRegistry -): +) -> None: """Test async_migrate_entry.""" config_entry1 = MockConfigEntry(domain="test1") config_entry2 = MockConfigEntry(domain="test2") @@ -1812,7 +1806,7 @@ async def test_async_migrate_entry_delete_self( async def test_async_migrate_entry_delete_other( hass: HomeAssistant, entity_registry: er.EntityRegistry -): +) -> None: """Test async_migrate_entry.""" config_entry1 = MockConfigEntry(domain="test1") config_entry2 = MockConfigEntry(domain="test2") diff --git a/tests/helpers/test_floor_registry.py b/tests/helpers/test_floor_registry.py index 95381e82389..3b07563fd11 100644 --- a/tests/helpers/test_floor_registry.py +++ b/tests/helpers/test_floor_registry.py @@ -8,14 +8,6 @@ import pytest from homeassistant.core import HomeAssistant from homeassistant.helpers import area_registry as ar, floor_registry as fr -from homeassistant.helpers.floor_registry import ( - EVENT_FLOOR_REGISTRY_UPDATED, - STORAGE_KEY, - STORAGE_VERSION_MAJOR, - FloorRegistry, - async_get, - async_load, -) from tests.common import async_capture_events, flush_store @@ -30,7 +22,7 @@ async def test_create_floor( hass: HomeAssistant, floor_registry: fr.FloorRegistry ) -> None: """Make sure that we can create floors.""" - update_events = async_capture_events(hass, EVENT_FLOOR_REGISTRY_UPDATED) + update_events = async_capture_events(hass, fr.EVENT_FLOOR_REGISTRY_UPDATED) floor = floor_registry.async_create( name="First floor", icon="mdi:home-floor-1", @@ -59,7 +51,7 @@ async def test_create_floor_with_name_already_in_use( hass: HomeAssistant, floor_registry: fr.FloorRegistry ) -> None: """Make sure that we can't create a floor with a name already in use.""" - update_events = async_capture_events(hass, EVENT_FLOOR_REGISTRY_UPDATED) + update_events = async_capture_events(hass, fr.EVENT_FLOOR_REGISTRY_UPDATED) floor_registry.async_create("First floor") with pytest.raises( @@ -75,7 +67,7 @@ async def test_create_floor_with_name_already_in_use( async def test_create_floor_with_id_already_in_use( - hass: HomeAssistant, floor_registry: fr.FloorRegistry + floor_registry: fr.FloorRegistry, ) -> None: """Make sure that we can't create an floor with an id already in use.""" floor = floor_registry.async_create("First") @@ -92,7 +84,7 @@ async def test_delete_floor( hass: HomeAssistant, floor_registry: fr.FloorRegistry ) -> None: """Make sure that we can delete a floor.""" - update_events = async_capture_events(hass, EVENT_FLOOR_REGISTRY_UPDATED) + update_events = async_capture_events(hass, fr.EVENT_FLOOR_REGISTRY_UPDATED) floor = floor_registry.async_create("First floor") assert len(floor_registry.floors) == 1 @@ -127,7 +119,7 @@ async def test_update_floor( hass: HomeAssistant, floor_registry: fr.FloorRegistry ) -> None: """Make sure that we can update floors.""" - update_events = async_capture_events(hass, EVENT_FLOOR_REGISTRY_UPDATED) + update_events = async_capture_events(hass, fr.EVENT_FLOOR_REGISTRY_UPDATED) floor = floor_registry.async_create("First floor") assert len(floor_registry.floors) == 1 @@ -171,7 +163,7 @@ async def test_update_floor_with_same_data( hass: HomeAssistant, floor_registry: fr.FloorRegistry ) -> None: """Make sure that we can reapply the same data to a floor and it won't update.""" - update_events = async_capture_events(hass, EVENT_FLOOR_REGISTRY_UPDATED) + update_events = async_capture_events(hass, fr.EVENT_FLOOR_REGISTRY_UPDATED) floor = floor_registry.async_create( "First floor", icon="mdi:home-floor-1", @@ -262,7 +254,7 @@ async def test_load_floors( assert len(floor_registry.floors) == 2 - registry2 = FloorRegistry(hass) + registry2 = fr.FloorRegistry(hass) await flush_store(floor_registry._store) await registry2.async_load() @@ -288,11 +280,11 @@ async def test_load_floors( @pytest.mark.parametrize("load_registries", [False]) async def test_loading_floors_from_storage( - hass: HomeAssistant, hass_storage: Any + hass: HomeAssistant, hass_storage: dict[str, Any] ) -> None: """Test loading stored floors on start.""" - hass_storage[STORAGE_KEY] = { - "version": STORAGE_VERSION_MAJOR, + hass_storage[fr.STORAGE_KEY] = { + "version": fr.STORAGE_VERSION_MAJOR, "data": { "floors": [ { @@ -306,8 +298,8 @@ async def test_loading_floors_from_storage( }, } - await async_load(hass) - registry = async_get(hass) + await fr.async_load(hass) + registry = fr.async_get(hass) assert len(registry.floors) == 1 diff --git a/tests/helpers/test_label_registry.py b/tests/helpers/test_label_registry.py index af53ef51f98..445319a4b62 100644 --- a/tests/helpers/test_label_registry.py +++ b/tests/helpers/test_label_registry.py @@ -12,14 +12,6 @@ from homeassistant.helpers import ( entity_registry as er, label_registry as lr, ) -from homeassistant.helpers.label_registry import ( - EVENT_LABEL_REGISTRY_UPDATED, - STORAGE_KEY, - STORAGE_VERSION_MAJOR, - LabelRegistry, - async_get, - async_load, -) from tests.common import MockConfigEntry, async_capture_events, flush_store @@ -34,7 +26,7 @@ async def test_create_label( hass: HomeAssistant, label_registry: lr.LabelRegistry ) -> None: """Make sure that we can create labels.""" - update_events = async_capture_events(hass, EVENT_LABEL_REGISTRY_UPDATED) + update_events = async_capture_events(hass, lr.EVENT_LABEL_REGISTRY_UPDATED) label = label_registry.async_create( name="My Label", color="#FF0000", @@ -63,7 +55,7 @@ async def test_create_label_with_name_already_in_use( hass: HomeAssistant, label_registry: lr.LabelRegistry ) -> None: """Make sure that we can't create a label with a ID already in use.""" - update_events = async_capture_events(hass, EVENT_LABEL_REGISTRY_UPDATED) + update_events = async_capture_events(hass, lr.EVENT_LABEL_REGISTRY_UPDATED) label_registry.async_create("mock") with pytest.raises( @@ -95,7 +87,7 @@ async def test_delete_label( hass: HomeAssistant, label_registry: lr.LabelRegistry ) -> None: """Make sure that we can delete a label.""" - update_events = async_capture_events(hass, EVENT_LABEL_REGISTRY_UPDATED) + update_events = async_capture_events(hass, lr.EVENT_LABEL_REGISTRY_UPDATED) label = label_registry.async_create("Label") assert len(label_registry.labels) == 1 @@ -130,7 +122,7 @@ async def test_update_label( hass: HomeAssistant, label_registry: lr.LabelRegistry ) -> None: """Make sure that we can update labels.""" - update_events = async_capture_events(hass, EVENT_LABEL_REGISTRY_UPDATED) + update_events = async_capture_events(hass, lr.EVENT_LABEL_REGISTRY_UPDATED) label = label_registry.async_create("Mock") assert len(label_registry.labels) == 1 @@ -174,7 +166,7 @@ async def test_update_label_with_same_data( hass: HomeAssistant, label_registry: lr.LabelRegistry ) -> None: """Make sure that we can reapply the same data to the label and it won't update.""" - update_events = async_capture_events(hass, EVENT_LABEL_REGISTRY_UPDATED) + update_events = async_capture_events(hass, lr.EVENT_LABEL_REGISTRY_UPDATED) label = label_registry.async_create( "mock", color="#FFFFFF", @@ -202,7 +194,7 @@ async def test_update_label_with_same_data( async def test_update_label_with_same_name_change_case( - hass: HomeAssistant, label_registry: lr.LabelRegistry + label_registry: lr.LabelRegistry, ) -> None: """Make sure that we can reapply the same name with a different case to the label.""" label = label_registry.async_create("mock") @@ -268,7 +260,7 @@ async def test_load_labels( assert len(label_registry.labels) == 2 - registry2 = LabelRegistry(hass) + registry2 = lr.LabelRegistry(hass) await flush_store(label_registry._store) await registry2.async_load() @@ -293,11 +285,11 @@ async def test_load_labels( @pytest.mark.parametrize("load_registries", [False]) async def test_loading_label_from_storage( - hass: HomeAssistant, hass_storage: Any + hass: HomeAssistant, hass_storage: dict[str, Any] ) -> None: """Test loading stored labels on start.""" - hass_storage[STORAGE_KEY] = { - "version": STORAGE_VERSION_MAJOR, + hass_storage[lr.STORAGE_KEY] = { + "version": lr.STORAGE_VERSION_MAJOR, "data": { "labels": [ { @@ -311,8 +303,8 @@ async def test_loading_label_from_storage( }, } - await async_load(hass) - registry = async_get(hass) + await lr.async_load(hass) + registry = lr.async_get(hass) assert len(registry.labels) == 1 diff --git a/tests/helpers/test_normalized_name_base_registry.py b/tests/helpers/test_normalized_name_base_registry.py index 71f5c94285a..9783e64eeff 100644 --- a/tests/helpers/test_normalized_name_base_registry.py +++ b/tests/helpers/test_normalized_name_base_registry.py @@ -10,12 +10,12 @@ from homeassistant.helpers.normalized_name_base_registry import ( @pytest.fixture -def registry_items(): +def registry_items() -> NormalizedNameBaseRegistryItems: """Fixture for registry items.""" return NormalizedNameBaseRegistryItems[NormalizedNameBaseRegistryEntry]() -def test_normalize_name(): +def test_normalize_name() -> None: """Test normalize_name.""" assert normalize_name("Hello World") == "helloworld" assert normalize_name("HELLO WORLD") == "helloworld" @@ -24,7 +24,7 @@ def test_normalize_name(): def test_registry_items( registry_items: NormalizedNameBaseRegistryItems[NormalizedNameBaseRegistryEntry], -): +) -> None: """Test registry items.""" entry = NormalizedNameBaseRegistryEntry( name="Hello World", normalized_name="helloworld" @@ -46,12 +46,12 @@ def test_registry_items( # test delete entry del registry_items["key"] assert "key" not in registry_items - assert list(registry_items.values()) == [] + assert not registry_items.values() def test_key_already_in_use( registry_items: NormalizedNameBaseRegistryItems[NormalizedNameBaseRegistryEntry], -): +) -> None: """Test key already in use.""" entry = NormalizedNameBaseRegistryEntry( name="Hello World", normalized_name="helloworld" From 612743077a8216cd1b9b9ddaaf3224c927115d35 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 10 Jun 2024 13:26:52 -0500 Subject: [PATCH 0479/1445] Improve workday test coverage (#119259) --- .../components/workday/test_binary_sensor.py | 44 ++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/tests/components/workday/test_binary_sensor.py b/tests/components/workday/test_binary_sensor.py index 9aa4dd6b5b4..e973a9f9c28 100644 --- a/tests/components/workday/test_binary_sensor.py +++ b/tests/components/workday/test_binary_sensor.py @@ -10,6 +10,7 @@ from homeassistant.components.workday.binary_sensor import SERVICE_CHECK_DATE from homeassistant.components.workday.const import DOMAIN from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component +from homeassistant.util import dt as dt_util from homeassistant.util.dt import UTC from . import ( @@ -144,18 +145,59 @@ async def test_setup_add_holiday( assert state.state == "off" +@pytest.mark.parametrize( + "time_zone", ["Asia/Tokyo", "Europe/Berlin", "America/Chicago", "US/Hawaii"] +) async def test_setup_no_country_weekend( hass: HomeAssistant, freezer: FrozenDateTimeFactory, + time_zone: str, ) -> None: """Test setup shows weekend as non-workday with no country.""" - freezer.move_to(datetime(2020, 2, 23, 12, tzinfo=UTC)) # Sunday + await hass.config.async_set_time_zone(time_zone) + zone = await dt_util.async_get_time_zone(time_zone) + freezer.move_to(datetime(2020, 2, 22, 0, 1, 1, tzinfo=zone)) # Saturday await init_integration(hass, TEST_CONFIG_NO_COUNTRY) state = hass.states.get("binary_sensor.workday_sensor") assert state is not None assert state.state == "off" + freezer.move_to(datetime(2020, 2, 24, 23, 59, 59, tzinfo=zone)) # Monday + async_fire_time_changed(hass) + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.workday_sensor") + assert state is not None + assert state.state == "on" + + +@pytest.mark.parametrize( + "time_zone", ["Asia/Tokyo", "Europe/Berlin", "America/Chicago", "US/Hawaii"] +) +async def test_setup_no_country_weekday( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + time_zone: str, +) -> None: + """Test setup shows a weekday as a workday with no country.""" + await hass.config.async_set_time_zone(time_zone) + zone = await dt_util.async_get_time_zone(time_zone) + freezer.move_to(datetime(2020, 2, 21, 23, 59, 59, tzinfo=zone)) # Friday + await init_integration(hass, TEST_CONFIG_NO_COUNTRY) + + state = hass.states.get("binary_sensor.workday_sensor") + assert state is not None + assert state.state == "on" + + freezer.move_to(datetime(2020, 2, 22, 23, 59, 59, tzinfo=zone)) # Saturday + async_fire_time_changed(hass) + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.workday_sensor") + assert state is not None + assert state.state == "off" + async def test_setup_remove_holiday( hass: HomeAssistant, From 650cf13bcaea194f58763c282159c3eeb61608ca Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 10 Jun 2024 20:31:19 +0200 Subject: [PATCH 0480/1445] Improve type hints in aiohttp_client helper tests (#119300) --- tests/helpers/test_aiohttp_client.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/tests/helpers/test_aiohttp_client.py b/tests/helpers/test_aiohttp_client.py index 69015c80305..7dd34fd2c64 100644 --- a/tests/helpers/test_aiohttp_client.py +++ b/tests/helpers/test_aiohttp_client.py @@ -3,9 +3,10 @@ from unittest.mock import Mock, patch import aiohttp +from aiohttp.test_utils import TestClient import pytest -from homeassistant.components.mjpeg.const import ( +from homeassistant.components.mjpeg import ( CONF_MJPEG_URL, CONF_STILL_IMAGE_URL, DOMAIN as MJPEG_DOMAIN, @@ -28,10 +29,13 @@ from tests.common import ( mock_integration, ) from tests.test_util.aiohttp import AiohttpClientMocker +from tests.typing import ClientSessionGenerator @pytest.fixture(name="camera_client") -def camera_client_fixture(hass, hass_client): +async def camera_client_fixture( + hass: HomeAssistant, hass_client: ClientSessionGenerator +) -> TestClient: """Fixture to fetch camera streams.""" mock_config_entry = MockConfigEntry( title="MJPEG Camera", @@ -46,12 +50,10 @@ def camera_client_fixture(hass, hass_client): }, ) mock_config_entry.add_to_hass(hass) - hass.loop.run_until_complete( - hass.config_entries.async_setup(mock_config_entry.entry_id) - ) - hass.loop.run_until_complete(hass.async_block_till_done()) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() - return hass.loop.run_until_complete(hass_client()) + return await hass_client() async def test_get_clientsession_with_ssl(hass: HomeAssistant) -> None: @@ -253,7 +255,7 @@ async def test_warning_close_session_custom( async def test_async_aiohttp_proxy_stream( - aioclient_mock: AiohttpClientMocker, camera_client + aioclient_mock: AiohttpClientMocker, camera_client: TestClient ) -> None: """Test that it fetches the given url.""" aioclient_mock.get("http://example.com/mjpeg_stream", content=b"Frame1Frame2Frame3") @@ -267,7 +269,7 @@ async def test_async_aiohttp_proxy_stream( async def test_async_aiohttp_proxy_stream_timeout( - aioclient_mock: AiohttpClientMocker, camera_client + aioclient_mock: AiohttpClientMocker, camera_client: TestClient ) -> None: """Test that it fetches the given url.""" aioclient_mock.get("http://example.com/mjpeg_stream", exc=TimeoutError()) @@ -277,7 +279,7 @@ async def test_async_aiohttp_proxy_stream_timeout( async def test_async_aiohttp_proxy_stream_client_err( - aioclient_mock: AiohttpClientMocker, camera_client + aioclient_mock: AiohttpClientMocker, camera_client: TestClient ) -> None: """Test that it fetches the given url.""" aioclient_mock.get("http://example.com/mjpeg_stream", exc=aiohttp.ClientError()) From 8d3e3faf0201e8d2fe1b07ca8760a4afdec68caf Mon Sep 17 00:00:00 2001 From: Cyr-ius <1258123+cyr-ius@users.noreply.github.com> Date: Mon, 10 Jun 2024 20:46:29 +0200 Subject: [PATCH 0481/1445] Use runtime_data in Husqvarna Automower (#119309) --- .../components/husqvarna_automower/__init__.py | 17 ++++++++--------- .../husqvarna_automower/binary_sensor.py | 9 +++++---- .../husqvarna_automower/device_tracker.py | 9 +++++---- .../husqvarna_automower/diagnostics.py | 6 +++--- .../husqvarna_automower/lawn_mower.py | 9 +++++---- .../components/husqvarna_automower/number.py | 16 ++++++++-------- .../components/husqvarna_automower/select.py | 9 +++++---- .../components/husqvarna_automower/sensor.py | 9 +++++---- .../components/husqvarna_automower/switch.py | 16 ++++++++-------- 9 files changed, 52 insertions(+), 48 deletions(-) diff --git a/homeassistant/components/husqvarna_automower/__init__.py b/homeassistant/components/husqvarna_automower/__init__.py index e4211e1078e..e62badd7e7c 100644 --- a/homeassistant/components/husqvarna_automower/__init__.py +++ b/homeassistant/components/husqvarna_automower/__init__.py @@ -12,7 +12,6 @@ from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import aiohttp_client, config_entry_oauth2_flow from . import api -from .const import DOMAIN from .coordinator import AutomowerDataUpdateCoordinator _LOGGER = logging.getLogger(__name__) @@ -27,8 +26,10 @@ PLATFORMS: list[Platform] = [ Platform.SWITCH, ] +type AutomowerConfigEntry = ConfigEntry[AutomowerDataUpdateCoordinator] -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + +async def async_setup_entry(hass: HomeAssistant, entry: AutomowerConfigEntry) -> bool: """Set up this integration using UI.""" implementation = ( await config_entry_oauth2_flow.async_get_config_entry_implementation( @@ -47,16 +48,17 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if 400 <= err.status < 500: raise ConfigEntryAuthFailed from err raise ConfigEntryNotReady from err + coordinator = AutomowerDataUpdateCoordinator(hass, automower_api, entry) await coordinator.async_config_entry_first_refresh() + entry.runtime_data = coordinator + entry.async_create_background_task( hass, coordinator.client_listen(hass, entry, automower_api), "websocket_task", ) - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator - if "amc:api" not in entry.data["token"]["scope"]: # We raise ConfigEntryAuthFailed here because the websocket can't be used # without the scope. So only polling would be possible. @@ -66,9 +68,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: AutomowerConfigEntry) -> bool: """Handle unload of an entry.""" - unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - if unload_ok: - hass.data[DOMAIN].pop(entry.entry_id) - return unload_ok + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/husqvarna_automower/binary_sensor.py b/homeassistant/components/husqvarna_automower/binary_sensor.py index e8e64e7ffc7..922f7deb99b 100644 --- a/homeassistant/components/husqvarna_automower/binary_sensor.py +++ b/homeassistant/components/husqvarna_automower/binary_sensor.py @@ -11,11 +11,10 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntity, BinarySensorEntityDescription, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN +from . import AutomowerConfigEntry from .coordinator import AutomowerDataUpdateCoordinator from .entity import AutomowerBaseEntity @@ -49,10 +48,12 @@ BINARY_SENSOR_TYPES: tuple[AutomowerBinarySensorEntityDescription, ...] = ( async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, + entry: AutomowerConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up binary sensor platform.""" - coordinator: AutomowerDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + coordinator = entry.runtime_data async_add_entities( AutomowerBinarySensorEntity(mower_id, coordinator, description) for mower_id in coordinator.data diff --git a/homeassistant/components/husqvarna_automower/device_tracker.py b/homeassistant/components/husqvarna_automower/device_tracker.py index 780d1da76fb..74ad624a515 100644 --- a/homeassistant/components/husqvarna_automower/device_tracker.py +++ b/homeassistant/components/husqvarna_automower/device_tracker.py @@ -3,20 +3,21 @@ from typing import TYPE_CHECKING from homeassistant.components.device_tracker import SourceType, TrackerEntity -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN +from . import AutomowerConfigEntry from .coordinator import AutomowerDataUpdateCoordinator from .entity import AutomowerBaseEntity async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, + entry: AutomowerConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up device tracker platform.""" - coordinator: AutomowerDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + coordinator = entry.runtime_data async_add_entities( AutomowerDeviceTrackerEntity(mower_id, coordinator) for mower_id in coordinator.data diff --git a/homeassistant/components/husqvarna_automower/diagnostics.py b/homeassistant/components/husqvarna_automower/diagnostics.py index f5677d4cb4b..658f6f94445 100644 --- a/homeassistant/components/husqvarna_automower/diagnostics.py +++ b/homeassistant/components/husqvarna_automower/diagnostics.py @@ -11,8 +11,8 @@ from homeassistant.const import CONF_ACCESS_TOKEN from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceEntry +from . import AutomowerConfigEntry from .const import DOMAIN -from .coordinator import AutomowerDataUpdateCoordinator CONF_REFRESH_TOKEN = "refresh_token" POSITIONS = "positions" @@ -33,10 +33,10 @@ async def async_get_config_entry_diagnostics( async def async_get_device_diagnostics( - hass: HomeAssistant, entry: ConfigEntry, device: DeviceEntry + hass: HomeAssistant, entry: AutomowerConfigEntry, device: DeviceEntry ) -> dict[str, Any]: """Return diagnostics for a device entry.""" - coordinator: AutomowerDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + coordinator = entry.runtime_data for identifier in device.identifiers: if identifier[0] == DOMAIN: if ( diff --git a/homeassistant/components/husqvarna_automower/lawn_mower.py b/homeassistant/components/husqvarna_automower/lawn_mower.py index 8ba9136364a..50333076308 100644 --- a/homeassistant/components/husqvarna_automower/lawn_mower.py +++ b/homeassistant/components/husqvarna_automower/lawn_mower.py @@ -10,12 +10,11 @@ from homeassistant.components.lawn_mower import ( LawnMowerEntity, LawnMowerEntityFeature, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN +from . import AutomowerConfigEntry from .coordinator import AutomowerDataUpdateCoordinator from .entity import AutomowerControlEntity @@ -42,10 +41,12 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, + entry: AutomowerConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up lawn mower platform.""" - coordinator: AutomowerDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + coordinator = entry.runtime_data async_add_entities( AutomowerLawnMowerEntity(mower_id, coordinator) for mower_id in coordinator.data ) diff --git a/homeassistant/components/husqvarna_automower/number.py b/homeassistant/components/husqvarna_automower/number.py index 72c1d360da9..f6d55389195 100644 --- a/homeassistant/components/husqvarna_automower/number.py +++ b/homeassistant/components/husqvarna_automower/number.py @@ -11,14 +11,14 @@ from aioautomower.model import MowerAttributes, WorkArea from aioautomower.session import AutomowerSession from homeassistant.components.number import NumberEntity, NumberEntityDescription -from homeassistant.config_entries import ConfigEntry from homeassistant.const import PERCENTAGE, EntityCategory, Platform from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN, EXECUTION_TIME_DELAY +from . import AutomowerConfigEntry +from .const import EXECUTION_TIME_DELAY from .coordinator import AutomowerDataUpdateCoordinator from .entity import AutomowerControlEntity @@ -111,10 +111,12 @@ WORK_AREA_NUMBER_TYPES: tuple[AutomowerWorkAreaNumberEntityDescription, ...] = ( async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, + entry: AutomowerConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up number platform.""" - coordinator: AutomowerDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + coordinator = entry.runtime_data entities: list[NumberEntity] = [] for mower_id in coordinator.data: @@ -227,7 +229,7 @@ class AutomowerWorkAreaNumberEntity(AutomowerControlEntity, NumberEntity): def async_remove_entities( hass: HomeAssistant, coordinator: AutomowerDataUpdateCoordinator, - config_entry: ConfigEntry, + entry: AutomowerConfigEntry, mower_id: str, ) -> None: """Remove deleted work areas from Home Assistant.""" @@ -238,9 +240,7 @@ def async_remove_entities( for work_area_id in _work_areas: uid = f"{mower_id}_{work_area_id}_cutting_height_work_area" active_work_areas.add(uid) - for entity_entry in er.async_entries_for_config_entry( - entity_reg, config_entry.entry_id - ): + for entity_entry in er.async_entries_for_config_entry(entity_reg, entry.entry_id): if ( entity_entry.domain == Platform.NUMBER and (split := entity_entry.unique_id.split("_"))[0] == mower_id diff --git a/homeassistant/components/husqvarna_automower/select.py b/homeassistant/components/husqvarna_automower/select.py index 1baa90e2799..b647407581f 100644 --- a/homeassistant/components/husqvarna_automower/select.py +++ b/homeassistant/components/husqvarna_automower/select.py @@ -7,13 +7,12 @@ from aioautomower.exceptions import ApiException from aioautomower.model import HeadlightModes from homeassistant.components.select import SelectEntity -from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN +from . import AutomowerConfigEntry from .coordinator import AutomowerDataUpdateCoordinator from .entity import AutomowerControlEntity @@ -29,10 +28,12 @@ HEADLIGHT_MODES: list = [ async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, + entry: AutomowerConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up select platform.""" - coordinator: AutomowerDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + coordinator = entry.runtime_data async_add_entities( AutomowerSelectEntity(mower_id, coordinator) for mower_id in coordinator.data diff --git a/homeassistant/components/husqvarna_automower/sensor.py b/homeassistant/components/husqvarna_automower/sensor.py index 0ece16f8e83..4cc3bcf5e57 100644 --- a/homeassistant/components/husqvarna_automower/sensor.py +++ b/homeassistant/components/husqvarna_automower/sensor.py @@ -13,13 +13,12 @@ from homeassistant.components.sensor import ( SensorEntityDescription, SensorStateClass, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import PERCENTAGE, EntityCategory, UnitOfLength, UnitOfTime from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType -from .const import DOMAIN +from . import AutomowerConfigEntry from .coordinator import AutomowerDataUpdateCoordinator from .entity import AutomowerBaseEntity @@ -319,10 +318,12 @@ SENSOR_TYPES: tuple[AutomowerSensorEntityDescription, ...] = ( async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, + entry: AutomowerConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up sensor platform.""" - coordinator: AutomowerDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + coordinator = entry.runtime_data async_add_entities( AutomowerSensorEntity(mower_id, coordinator, description) for mower_id in coordinator.data diff --git a/homeassistant/components/husqvarna_automower/switch.py b/homeassistant/components/husqvarna_automower/switch.py index 4964c50eee5..fed2d3cfedc 100644 --- a/homeassistant/components/husqvarna_automower/switch.py +++ b/homeassistant/components/husqvarna_automower/switch.py @@ -14,14 +14,14 @@ from aioautomower.model import ( ) from homeassistant.components.switch import SwitchEntity -from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN, EXECUTION_TIME_DELAY +from . import AutomowerConfigEntry +from .const import EXECUTION_TIME_DELAY from .coordinator import AutomowerDataUpdateCoordinator from .entity import AutomowerControlEntity @@ -44,10 +44,12 @@ ERROR_STATES = [ async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, + entry: AutomowerConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up switch platform.""" - coordinator: AutomowerDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + coordinator = entry.runtime_data entities: list[SwitchEntity] = [] entities.extend( AutomowerScheduleSwitchEntity(mower_id, coordinator) @@ -196,7 +198,7 @@ class AutomowerStayOutZoneSwitchEntity(AutomowerControlEntity, SwitchEntity): def async_remove_entities( hass: HomeAssistant, coordinator: AutomowerDataUpdateCoordinator, - config_entry: ConfigEntry, + entry: AutomowerConfigEntry, mower_id: str, ) -> None: """Remove deleted stay-out-zones from Home Assistant.""" @@ -207,9 +209,7 @@ def async_remove_entities( for zones_uid in _zones.zones: uid = f"{mower_id}_{zones_uid}_stay_out_zones" active_zones.add(uid) - for entity_entry in er.async_entries_for_config_entry( - entity_reg, config_entry.entry_id - ): + for entity_entry in er.async_entries_for_config_entry(entity_reg, entry.entry_id): if ( entity_entry.domain == Platform.SWITCH and (split := entity_entry.unique_id.split("_"))[0] == mower_id From 53d5a65f2c4af910594d2e329e9616b1029df089 Mon Sep 17 00:00:00 2001 From: osohotwateriot <102795312+osohotwateriot@users.noreply.github.com> Date: Mon, 10 Jun 2024 21:48:41 +0300 Subject: [PATCH 0482/1445] Add OSO Energy temperature sensors (#119301) --- homeassistant/components/osoenergy/sensor.py | 35 ++++++++++++++++++- .../components/osoenergy/strings.json | 12 +++++++ 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/osoenergy/sensor.py b/homeassistant/components/osoenergy/sensor.py index 772c3c0a69e..40ec33e3e02 100644 --- a/homeassistant/components/osoenergy/sensor.py +++ b/homeassistant/components/osoenergy/sensor.py @@ -13,7 +13,12 @@ from homeassistant.components.sensor import ( SensorStateClass, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import UnitOfEnergy, UnitOfPower, UnitOfVolume +from homeassistant.const import ( + UnitOfEnergy, + UnitOfPower, + UnitOfTemperature, + UnitOfVolume, +) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType @@ -101,6 +106,34 @@ SENSOR_TYPES: dict[str, OSOEnergySensorEntityDescription] = { native_unit_of_measurement=UnitOfVolume.LITERS, value_fn=lambda entity_data: entity_data.state, ), + "temperature_top": OSOEnergySensorEntityDescription( + key="temperature_top", + translation_key="temperature_top", + device_class=SensorDeviceClass.TEMPERATURE, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + value_fn=lambda entity_data: entity_data.state, + ), + "temperature_mid": OSOEnergySensorEntityDescription( + key="temperature_mid", + translation_key="temperature_mid", + device_class=SensorDeviceClass.TEMPERATURE, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + value_fn=lambda entity_data: entity_data.state, + ), + "temperature_low": OSOEnergySensorEntityDescription( + key="temperature_low", + translation_key="temperature_low", + device_class=SensorDeviceClass.TEMPERATURE, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + value_fn=lambda entity_data: entity_data.state, + ), + "temperature_one": OSOEnergySensorEntityDescription( + key="temperature_one", + translation_key="temperature_one", + device_class=SensorDeviceClass.TEMPERATURE, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + value_fn=lambda entity_data: entity_data.state, + ), } diff --git a/homeassistant/components/osoenergy/strings.json b/homeassistant/components/osoenergy/strings.json index 27e7d295785..a7963bfa436 100644 --- a/homeassistant/components/osoenergy/strings.json +++ b/homeassistant/components/osoenergy/strings.json @@ -77,6 +77,18 @@ }, "profile": { "name": "Profile local" + }, + "temperature_top": { + "name": "Temperature top" + }, + "temperature_mid": { + "name": "Temperature middle" + }, + "temperature_low": { + "name": "Temperature bottom" + }, + "temperature_one": { + "name": "Temperature one" } } } From 51d78c3c250e5c4de859ee978d7a7eae51df7fa8 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Mon, 10 Jun 2024 20:57:36 +0200 Subject: [PATCH 0483/1445] Improve incomfort binary sensors (#119292) * Improve incomfort binary_sensor, add is_burning, is_pumping and is_tapping * Update test snapshot * Use helper for fault code label name * Update tests * Remove extra state attribute * Make default Fault `none` to supprt localization * Update snapshot --- .../components/incomfort/binary_sensor.py | 30 +- .../components/incomfort/strings.json | 18 +- tests/components/incomfort/conftest.py | 35 +- .../snapshots/test_binary_sensor.ambr | 1604 ++++++++++++++++- .../snapshots/test_water_heater.ambr | 2 +- .../incomfort/test_binary_sensor.py | 32 + 6 files changed, 1698 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/incomfort/binary_sensor.py b/homeassistant/components/incomfort/binary_sensor.py index 9a2ec9414eb..a94e1fac504 100644 --- a/homeassistant/components/incomfort/binary_sensor.py +++ b/homeassistant/components/incomfort/binary_sensor.py @@ -26,7 +26,7 @@ class IncomfortBinarySensorEntityDescription(BinarySensorEntityDescription): """Describes Incomfort binary sensor entity.""" value_key: str - extra_state_attributes_fn: Callable[[dict[str, Any]], dict[str, Any]] + extra_state_attributes_fn: Callable[[dict[str, Any]], dict[str, Any]] | None = None SENSOR_TYPES: tuple[IncomfortBinarySensorEntityDescription, ...] = ( @@ -35,7 +35,27 @@ SENSOR_TYPES: tuple[IncomfortBinarySensorEntityDescription, ...] = ( translation_key="fault", device_class=BinarySensorDeviceClass.PROBLEM, value_key="is_failed", - extra_state_attributes_fn=lambda status: {"fault_code": status["fault_code"]}, + extra_state_attributes_fn=lambda status: { + "fault_code": status["fault_code"] or "none", + }, + ), + IncomfortBinarySensorEntityDescription( + key="is_pumping", + translation_key="is_pumping", + device_class=BinarySensorDeviceClass.RUNNING, + value_key="is_pumping", + ), + IncomfortBinarySensorEntityDescription( + key="is_burning", + translation_key="is_burning", + device_class=BinarySensorDeviceClass.RUNNING, + value_key="is_burning", + ), + IncomfortBinarySensorEntityDescription( + key="is_tapping", + translation_key="is_tapping", + device_class=BinarySensorDeviceClass.RUNNING, + value_key="is_tapping", ), ) @@ -77,6 +97,8 @@ class IncomfortBinarySensor(IncomfortBoilerEntity, BinarySensorEntity): return self._heater.status[self.entity_description.value_key] @property - def extra_state_attributes(self) -> dict[str, Any]: + def extra_state_attributes(self) -> dict[str, Any] | None: """Return the device state attributes.""" - return self.entity_description.extra_state_attributes_fn(self._heater.status) + if (attributes_fn := self.entity_description.extra_state_attributes_fn) is None: + return None + return attributes_fn(self._heater.status) diff --git a/homeassistant/components/incomfort/strings.json b/homeassistant/components/incomfort/strings.json index f74dd4f3202..a2bb874142b 100644 --- a/homeassistant/components/incomfort/strings.json +++ b/homeassistant/components/incomfort/strings.json @@ -56,7 +56,23 @@ "entity": { "binary_sensor": { "fault": { - "name": "Fault" + "name": "Fault", + "state_attributes": { + "fault_code": { + "state": { + "none": "None" + } + } + } + }, + "is_burning": { + "name": "Burner" + }, + "is_pumping": { + "name": "Pump" + }, + "is_tapping": { + "name": "Hot water tap" } }, "sensor": { diff --git a/tests/components/incomfort/conftest.py b/tests/components/incomfort/conftest.py index d3675b4abea..64885e38b65 100644 --- a/tests/components/incomfort/conftest.py +++ b/tests/components/incomfort/conftest.py @@ -3,6 +3,7 @@ from typing import Any from unittest.mock import AsyncMock, MagicMock, patch +from incomfortclient import DisplayCode import pytest from typing_extensions import Generator @@ -18,6 +19,23 @@ MOCK_CONFIG = { "password": "verysecret", } +MOCK_HEATER_STATUS = { + "display_code": DisplayCode(126), + "display_text": "standby", + "fault_code": None, + "is_burning": False, + "is_failed": False, + "is_pumping": False, + "is_tapping": False, + "heater_temp": 35.34, + "tap_temp": 30.21, + "pressure": 1.86, + "serial_no": "c0ffeec0ffee", + "nodenr": 249, + "rf_message_rssi": 30, + "rfstatus_cntr": 0, +} + @pytest.fixture def mock_setup_entry() -> Generator[AsyncMock]: @@ -48,22 +66,7 @@ def mock_config_entry( @pytest.fixture def mock_heater_status() -> dict[str, Any]: """Mock heater status.""" - return { - "display_code": 126, - "display_text": "standby", - "fault_code": None, - "is_burning": False, - "is_failed": False, - "is_pumping": False, - "is_tapping": False, - "heater_temp": 35.34, - "tap_temp": 30.21, - "pressure": 1.86, - "serial_no": "c0ffeec0ffee", - "nodenr": 249, - "rf_message_rssi": 30, - "rfstatus_cntr": 0, - } + return dict(MOCK_HEATER_STATUS) @pytest.fixture diff --git a/tests/components/incomfort/snapshots/test_binary_sensor.ambr b/tests/components/incomfort/snapshots/test_binary_sensor.ambr index 633f3fdf01c..565abcaa26f 100644 --- a/tests/components/incomfort/snapshots/test_binary_sensor.ambr +++ b/tests/components/incomfort/snapshots/test_binary_sensor.ambr @@ -1,4 +1,1371 @@ # serializer version: 1 +# name: test_setup_binary_sensors_alt[is_burning][binary_sensor.boiler_burner-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.boiler_burner', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Burner', + 'platform': 'incomfort', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'is_burning', + 'unique_id': 'c0ffeec0ffee_is_burning', + 'unit_of_measurement': None, + }) +# --- +# name: test_setup_binary_sensors_alt[is_burning][binary_sensor.boiler_burner-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'running', + 'friendly_name': 'Boiler Burner', + }), + 'context': , + 'entity_id': 'binary_sensor.boiler_burner', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_setup_binary_sensors_alt[is_burning][binary_sensor.boiler_fault-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.boiler_fault', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Fault', + 'platform': 'incomfort', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'fault', + 'unique_id': 'c0ffeec0ffee_failed', + 'unit_of_measurement': None, + }) +# --- +# name: test_setup_binary_sensors_alt[is_burning][binary_sensor.boiler_fault-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'problem', + 'fault_code': 'none', + 'friendly_name': 'Boiler Fault', + }), + 'context': , + 'entity_id': 'binary_sensor.boiler_fault', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_setup_binary_sensors_alt[is_burning][binary_sensor.boiler_hot_water_tap-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.boiler_hot_water_tap', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Hot water tap', + 'platform': 'incomfort', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'is_tapping', + 'unique_id': 'c0ffeec0ffee_is_tapping', + 'unit_of_measurement': None, + }) +# --- +# name: test_setup_binary_sensors_alt[is_burning][binary_sensor.boiler_hot_water_tap-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'running', + 'friendly_name': 'Boiler Hot water tap', + }), + 'context': , + 'entity_id': 'binary_sensor.boiler_hot_water_tap', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_setup_binary_sensors_alt[is_burning][binary_sensor.boiler_pump-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.boiler_pump', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Pump', + 'platform': 'incomfort', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'is_pumping', + 'unique_id': 'c0ffeec0ffee_is_pumping', + 'unit_of_measurement': None, + }) +# --- +# name: test_setup_binary_sensors_alt[is_burning][binary_sensor.boiler_pump-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'running', + 'friendly_name': 'Boiler Pump', + }), + 'context': , + 'entity_id': 'binary_sensor.boiler_pump', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_setup_binary_sensors_alt[is_burning][binary_sensor.boiler_running-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.boiler_running', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Running', + 'platform': 'incomfort', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'is_pumping', + 'unique_id': 'c0ffeec0ffee_is_pumping', + 'unit_of_measurement': None, + }) +# --- +# name: test_setup_binary_sensors_alt[is_burning][binary_sensor.boiler_running-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'running', + 'friendly_name': 'Boiler Running', + }), + 'context': , + 'entity_id': 'binary_sensor.boiler_running', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_setup_binary_sensors_alt[is_burning][binary_sensor.boiler_running_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.boiler_running_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Running', + 'platform': 'incomfort', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'is_burning', + 'unique_id': 'c0ffeec0ffee_is_burning', + 'unit_of_measurement': None, + }) +# --- +# name: test_setup_binary_sensors_alt[is_burning][binary_sensor.boiler_running_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'running', + 'friendly_name': 'Boiler Running', + }), + 'context': , + 'entity_id': 'binary_sensor.boiler_running_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_setup_binary_sensors_alt[is_burning][binary_sensor.boiler_running_3-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.boiler_running_3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Running', + 'platform': 'incomfort', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'is_tapping', + 'unique_id': 'c0ffeec0ffee_is_tapping', + 'unit_of_measurement': None, + }) +# --- +# name: test_setup_binary_sensors_alt[is_burning][binary_sensor.boiler_running_3-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'running', + 'friendly_name': 'Boiler Running', + }), + 'context': , + 'entity_id': 'binary_sensor.boiler_running_3', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_setup_binary_sensors_alt[is_failed][binary_sensor.boiler_burner-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.boiler_burner', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Burner', + 'platform': 'incomfort', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'is_burning', + 'unique_id': 'c0ffeec0ffee_is_burning', + 'unit_of_measurement': None, + }) +# --- +# name: test_setup_binary_sensors_alt[is_failed][binary_sensor.boiler_burner-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'running', + 'friendly_name': 'Boiler Burner', + }), + 'context': , + 'entity_id': 'binary_sensor.boiler_burner', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_setup_binary_sensors_alt[is_failed][binary_sensor.boiler_fault-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.boiler_fault', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Fault', + 'platform': 'incomfort', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'fault', + 'unique_id': 'c0ffeec0ffee_failed', + 'unit_of_measurement': None, + }) +# --- +# name: test_setup_binary_sensors_alt[is_failed][binary_sensor.boiler_fault-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'problem', + 'fault_code': , + 'friendly_name': 'Boiler Fault', + }), + 'context': , + 'entity_id': 'binary_sensor.boiler_fault', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_setup_binary_sensors_alt[is_failed][binary_sensor.boiler_hot_water_tap-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.boiler_hot_water_tap', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Hot water tap', + 'platform': 'incomfort', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'is_tapping', + 'unique_id': 'c0ffeec0ffee_is_tapping', + 'unit_of_measurement': None, + }) +# --- +# name: test_setup_binary_sensors_alt[is_failed][binary_sensor.boiler_hot_water_tap-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'running', + 'friendly_name': 'Boiler Hot water tap', + }), + 'context': , + 'entity_id': 'binary_sensor.boiler_hot_water_tap', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_setup_binary_sensors_alt[is_failed][binary_sensor.boiler_pump-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.boiler_pump', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Pump', + 'platform': 'incomfort', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'is_pumping', + 'unique_id': 'c0ffeec0ffee_is_pumping', + 'unit_of_measurement': None, + }) +# --- +# name: test_setup_binary_sensors_alt[is_failed][binary_sensor.boiler_pump-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'running', + 'friendly_name': 'Boiler Pump', + }), + 'context': , + 'entity_id': 'binary_sensor.boiler_pump', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_setup_binary_sensors_alt[is_failed][binary_sensor.boiler_running-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.boiler_running', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Running', + 'platform': 'incomfort', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'is_pumping', + 'unique_id': 'c0ffeec0ffee_is_pumping', + 'unit_of_measurement': None, + }) +# --- +# name: test_setup_binary_sensors_alt[is_failed][binary_sensor.boiler_running-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'running', + 'friendly_name': 'Boiler Running', + }), + 'context': , + 'entity_id': 'binary_sensor.boiler_running', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_setup_binary_sensors_alt[is_failed][binary_sensor.boiler_running_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.boiler_running_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Running', + 'platform': 'incomfort', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'is_burning', + 'unique_id': 'c0ffeec0ffee_is_burning', + 'unit_of_measurement': None, + }) +# --- +# name: test_setup_binary_sensors_alt[is_failed][binary_sensor.boiler_running_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'running', + 'friendly_name': 'Boiler Running', + }), + 'context': , + 'entity_id': 'binary_sensor.boiler_running_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_setup_binary_sensors_alt[is_failed][binary_sensor.boiler_running_3-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.boiler_running_3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Running', + 'platform': 'incomfort', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'is_tapping', + 'unique_id': 'c0ffeec0ffee_is_tapping', + 'unit_of_measurement': None, + }) +# --- +# name: test_setup_binary_sensors_alt[is_failed][binary_sensor.boiler_running_3-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'running', + 'friendly_name': 'Boiler Running', + }), + 'context': , + 'entity_id': 'binary_sensor.boiler_running_3', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_setup_binary_sensors_alt[is_pumping][binary_sensor.boiler_burner-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.boiler_burner', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Burner', + 'platform': 'incomfort', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'is_burning', + 'unique_id': 'c0ffeec0ffee_is_burning', + 'unit_of_measurement': None, + }) +# --- +# name: test_setup_binary_sensors_alt[is_pumping][binary_sensor.boiler_burner-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'running', + 'friendly_name': 'Boiler Burner', + }), + 'context': , + 'entity_id': 'binary_sensor.boiler_burner', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_setup_binary_sensors_alt[is_pumping][binary_sensor.boiler_fault-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.boiler_fault', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Fault', + 'platform': 'incomfort', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'fault', + 'unique_id': 'c0ffeec0ffee_failed', + 'unit_of_measurement': None, + }) +# --- +# name: test_setup_binary_sensors_alt[is_pumping][binary_sensor.boiler_fault-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'problem', + 'fault_code': 'none', + 'friendly_name': 'Boiler Fault', + }), + 'context': , + 'entity_id': 'binary_sensor.boiler_fault', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_setup_binary_sensors_alt[is_pumping][binary_sensor.boiler_hot_water_tap-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.boiler_hot_water_tap', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Hot water tap', + 'platform': 'incomfort', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'is_tapping', + 'unique_id': 'c0ffeec0ffee_is_tapping', + 'unit_of_measurement': None, + }) +# --- +# name: test_setup_binary_sensors_alt[is_pumping][binary_sensor.boiler_hot_water_tap-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'running', + 'friendly_name': 'Boiler Hot water tap', + }), + 'context': , + 'entity_id': 'binary_sensor.boiler_hot_water_tap', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_setup_binary_sensors_alt[is_pumping][binary_sensor.boiler_pump-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.boiler_pump', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Pump', + 'platform': 'incomfort', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'is_pumping', + 'unique_id': 'c0ffeec0ffee_is_pumping', + 'unit_of_measurement': None, + }) +# --- +# name: test_setup_binary_sensors_alt[is_pumping][binary_sensor.boiler_pump-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'running', + 'friendly_name': 'Boiler Pump', + }), + 'context': , + 'entity_id': 'binary_sensor.boiler_pump', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_setup_binary_sensors_alt[is_pumping][binary_sensor.boiler_running-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.boiler_running', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Running', + 'platform': 'incomfort', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'is_pumping', + 'unique_id': 'c0ffeec0ffee_is_pumping', + 'unit_of_measurement': None, + }) +# --- +# name: test_setup_binary_sensors_alt[is_pumping][binary_sensor.boiler_running-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'running', + 'friendly_name': 'Boiler Running', + }), + 'context': , + 'entity_id': 'binary_sensor.boiler_running', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_setup_binary_sensors_alt[is_pumping][binary_sensor.boiler_running_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.boiler_running_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Running', + 'platform': 'incomfort', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'is_burning', + 'unique_id': 'c0ffeec0ffee_is_burning', + 'unit_of_measurement': None, + }) +# --- +# name: test_setup_binary_sensors_alt[is_pumping][binary_sensor.boiler_running_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'running', + 'friendly_name': 'Boiler Running', + }), + 'context': , + 'entity_id': 'binary_sensor.boiler_running_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_setup_binary_sensors_alt[is_pumping][binary_sensor.boiler_running_3-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.boiler_running_3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Running', + 'platform': 'incomfort', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'is_tapping', + 'unique_id': 'c0ffeec0ffee_is_tapping', + 'unit_of_measurement': None, + }) +# --- +# name: test_setup_binary_sensors_alt[is_pumping][binary_sensor.boiler_running_3-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'running', + 'friendly_name': 'Boiler Running', + }), + 'context': , + 'entity_id': 'binary_sensor.boiler_running_3', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_setup_binary_sensors_alt[is_tapping][binary_sensor.boiler_burner-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.boiler_burner', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Burner', + 'platform': 'incomfort', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'is_burning', + 'unique_id': 'c0ffeec0ffee_is_burning', + 'unit_of_measurement': None, + }) +# --- +# name: test_setup_binary_sensors_alt[is_tapping][binary_sensor.boiler_burner-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'running', + 'friendly_name': 'Boiler Burner', + }), + 'context': , + 'entity_id': 'binary_sensor.boiler_burner', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_setup_binary_sensors_alt[is_tapping][binary_sensor.boiler_fault-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.boiler_fault', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Fault', + 'platform': 'incomfort', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'fault', + 'unique_id': 'c0ffeec0ffee_failed', + 'unit_of_measurement': None, + }) +# --- +# name: test_setup_binary_sensors_alt[is_tapping][binary_sensor.boiler_fault-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'problem', + 'fault_code': 'none', + 'friendly_name': 'Boiler Fault', + }), + 'context': , + 'entity_id': 'binary_sensor.boiler_fault', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_setup_binary_sensors_alt[is_tapping][binary_sensor.boiler_hot_water_tap-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.boiler_hot_water_tap', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Hot water tap', + 'platform': 'incomfort', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'is_tapping', + 'unique_id': 'c0ffeec0ffee_is_tapping', + 'unit_of_measurement': None, + }) +# --- +# name: test_setup_binary_sensors_alt[is_tapping][binary_sensor.boiler_hot_water_tap-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'running', + 'friendly_name': 'Boiler Hot water tap', + }), + 'context': , + 'entity_id': 'binary_sensor.boiler_hot_water_tap', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_setup_binary_sensors_alt[is_tapping][binary_sensor.boiler_pump-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.boiler_pump', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Pump', + 'platform': 'incomfort', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'is_pumping', + 'unique_id': 'c0ffeec0ffee_is_pumping', + 'unit_of_measurement': None, + }) +# --- +# name: test_setup_binary_sensors_alt[is_tapping][binary_sensor.boiler_pump-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'running', + 'friendly_name': 'Boiler Pump', + }), + 'context': , + 'entity_id': 'binary_sensor.boiler_pump', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_setup_binary_sensors_alt[is_tapping][binary_sensor.boiler_running-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.boiler_running', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Running', + 'platform': 'incomfort', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'is_pumping', + 'unique_id': 'c0ffeec0ffee_is_pumping', + 'unit_of_measurement': None, + }) +# --- +# name: test_setup_binary_sensors_alt[is_tapping][binary_sensor.boiler_running-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'running', + 'friendly_name': 'Boiler Running', + }), + 'context': , + 'entity_id': 'binary_sensor.boiler_running', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_setup_binary_sensors_alt[is_tapping][binary_sensor.boiler_running_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.boiler_running_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Running', + 'platform': 'incomfort', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'is_burning', + 'unique_id': 'c0ffeec0ffee_is_burning', + 'unit_of_measurement': None, + }) +# --- +# name: test_setup_binary_sensors_alt[is_tapping][binary_sensor.boiler_running_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'running', + 'friendly_name': 'Boiler Running', + }), + 'context': , + 'entity_id': 'binary_sensor.boiler_running_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_setup_binary_sensors_alt[is_tapping][binary_sensor.boiler_running_3-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.boiler_running_3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Running', + 'platform': 'incomfort', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'is_tapping', + 'unique_id': 'c0ffeec0ffee_is_tapping', + 'unit_of_measurement': None, + }) +# --- +# name: test_setup_binary_sensors_alt[is_tapping][binary_sensor.boiler_running_3-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'running', + 'friendly_name': 'Boiler Running', + }), + 'context': , + 'entity_id': 'binary_sensor.boiler_running_3', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_setup_platform[binary_sensor.boiler_burner-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.boiler_burner', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Burner', + 'platform': 'incomfort', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'is_burning', + 'unique_id': 'c0ffeec0ffee_is_burning', + 'unit_of_measurement': None, + }) +# --- +# name: test_setup_platform[binary_sensor.boiler_burner-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'running', + 'friendly_name': 'Boiler Burner', + }), + 'context': , + 'entity_id': 'binary_sensor.boiler_burner', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- # name: test_setup_platform[binary_sensor.boiler_fault-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -36,7 +1403,7 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'problem', - 'fault_code': None, + 'fault_code': 'none', 'friendly_name': 'Boiler Fault', }), 'context': , @@ -47,3 +1414,238 @@ 'state': 'off', }) # --- +# name: test_setup_platform[binary_sensor.boiler_hot_water_tap-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.boiler_hot_water_tap', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Hot water tap', + 'platform': 'incomfort', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'is_tapping', + 'unique_id': 'c0ffeec0ffee_is_tapping', + 'unit_of_measurement': None, + }) +# --- +# name: test_setup_platform[binary_sensor.boiler_hot_water_tap-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'running', + 'friendly_name': 'Boiler Hot water tap', + }), + 'context': , + 'entity_id': 'binary_sensor.boiler_hot_water_tap', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_setup_platform[binary_sensor.boiler_pump-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.boiler_pump', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Pump', + 'platform': 'incomfort', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'is_pumping', + 'unique_id': 'c0ffeec0ffee_is_pumping', + 'unit_of_measurement': None, + }) +# --- +# name: test_setup_platform[binary_sensor.boiler_pump-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'running', + 'friendly_name': 'Boiler Pump', + }), + 'context': , + 'entity_id': 'binary_sensor.boiler_pump', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_setup_platform[binary_sensor.boiler_running-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.boiler_running', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Running', + 'platform': 'incomfort', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'is_pumping', + 'unique_id': 'c0ffeec0ffee_is_pumping', + 'unit_of_measurement': None, + }) +# --- +# name: test_setup_platform[binary_sensor.boiler_running-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'running', + 'friendly_name': 'Boiler Running', + }), + 'context': , + 'entity_id': 'binary_sensor.boiler_running', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_setup_platform[binary_sensor.boiler_running_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.boiler_running_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Running', + 'platform': 'incomfort', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'is_burning', + 'unique_id': 'c0ffeec0ffee_is_burning', + 'unit_of_measurement': None, + }) +# --- +# name: test_setup_platform[binary_sensor.boiler_running_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'running', + 'friendly_name': 'Boiler Running', + }), + 'context': , + 'entity_id': 'binary_sensor.boiler_running_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_setup_platform[binary_sensor.boiler_running_3-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.boiler_running_3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Running', + 'platform': 'incomfort', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'is_tapping', + 'unique_id': 'c0ffeec0ffee_is_tapping', + 'unit_of_measurement': None, + }) +# --- +# name: test_setup_platform[binary_sensor.boiler_running_3-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'running', + 'friendly_name': 'Boiler Running', + }), + 'context': , + 'entity_id': 'binary_sensor.boiler_running_3', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- diff --git a/tests/components/incomfort/snapshots/test_water_heater.ambr b/tests/components/incomfort/snapshots/test_water_heater.ambr index 4b6bd8e9751..3ec87c49f3e 100644 --- a/tests/components/incomfort/snapshots/test_water_heater.ambr +++ b/tests/components/incomfort/snapshots/test_water_heater.ambr @@ -39,7 +39,7 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'current_temperature': 35.3, - 'display_code': 126, + 'display_code': , 'display_text': 'standby', 'friendly_name': 'Boiler', 'icon': 'mdi:thermometer-lines', diff --git a/tests/components/incomfort/test_binary_sensor.py b/tests/components/incomfort/test_binary_sensor.py index 3a50a08d9d1..c724cf4b7b2 100644 --- a/tests/components/incomfort/test_binary_sensor.py +++ b/tests/components/incomfort/test_binary_sensor.py @@ -2,6 +2,8 @@ from unittest.mock import MagicMock, patch +from incomfortclient import FaultCode +import pytest from syrupy import SnapshotAssertion from homeassistant.config_entries import ConfigEntry @@ -9,6 +11,8 @@ from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er +from .conftest import MOCK_HEATER_STATUS + from tests.common import snapshot_platform @@ -23,3 +27,31 @@ async def test_setup_platform( """Test the incomfort entities are set up correctly.""" await hass.config_entries.async_setup(mock_config_entry.entry_id) await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + +@pytest.mark.parametrize( + "mock_heater_status", + [ + MOCK_HEATER_STATUS + | { + "is_failed": True, + "display_code": None, + "fault_code": FaultCode.CV_TEMPERATURE_TOO_HIGH_E1, + }, + MOCK_HEATER_STATUS | {"is_pumping": True}, + MOCK_HEATER_STATUS | {"is_burning": True}, + MOCK_HEATER_STATUS | {"is_tapping": True}, + ], + ids=["is_failed", "is_pumping", "is_burning", "is_tapping"], +) +@patch("homeassistant.components.incomfort.PLATFORMS", [Platform.BINARY_SENSOR]) +async def test_setup_binary_sensors_alt( + hass: HomeAssistant, + mock_incomfort: MagicMock, + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, + mock_config_entry: ConfigEntry, +) -> None: + """Test the incomfort heater .""" + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) From 04c8a5574a2041885e072562315f5f55317e659e Mon Sep 17 00:00:00 2001 From: Quentin <39061148+LapsTimeOFF@users.noreply.github.com> Date: Mon, 10 Jun 2024 20:58:15 +0200 Subject: [PATCH 0484/1445] Fix elgato light color detection (#119177) --- homeassistant/components/elgato/light.py | 10 +++++++++- tests/components/elgato/fixtures/light-strip/info.json | 2 +- tests/components/elgato/snapshots/test_light.ambr | 4 ++-- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/elgato/light.py b/homeassistant/components/elgato/light.py index 2cd3d611bf5..339bed97f6f 100644 --- a/homeassistant/components/elgato/light.py +++ b/homeassistant/components/elgato/light.py @@ -59,7 +59,15 @@ class ElgatoLight(ElgatoEntity, LightEntity): self._attr_unique_id = coordinator.data.info.serial_number # Elgato Light supporting color, have a different temperature range - if self.coordinator.data.settings.power_on_hue is not None: + if ( + self.coordinator.data.info.product_name + in ( + "Elgato Light Strip", + "Elgato Light Strip Pro", + ) + or self.coordinator.data.settings.power_on_hue + or self.coordinator.data.state.hue is not None + ): self._attr_supported_color_modes = {ColorMode.COLOR_TEMP, ColorMode.HS} self._attr_min_mireds = 153 self._attr_max_mireds = 285 diff --git a/tests/components/elgato/fixtures/light-strip/info.json b/tests/components/elgato/fixtures/light-strip/info.json index e2a816df26e..a8c3200e4b9 100644 --- a/tests/components/elgato/fixtures/light-strip/info.json +++ b/tests/components/elgato/fixtures/light-strip/info.json @@ -1,5 +1,5 @@ { - "productName": "Elgato Key Light", + "productName": "Elgato Light Strip", "hardwareBoardType": 53, "firmwareBuildNumber": 192, "firmwareVersion": "1.0.3", diff --git a/tests/components/elgato/snapshots/test_light.ambr b/tests/components/elgato/snapshots/test_light.ambr index 6ef773a7304..e2f663d294b 100644 --- a/tests/components/elgato/snapshots/test_light.ambr +++ b/tests/components/elgato/snapshots/test_light.ambr @@ -218,7 +218,7 @@ 'labels': set({ }), 'manufacturer': 'Elgato', - 'model': 'Elgato Key Light', + 'model': 'Elgato Light Strip', 'name': 'Frenck', 'name_by_user': None, 'serial_number': 'CN11A1A00001', @@ -333,7 +333,7 @@ 'labels': set({ }), 'manufacturer': 'Elgato', - 'model': 'Elgato Key Light', + 'model': 'Elgato Light Strip', 'name': 'Frenck', 'name_by_user': None, 'serial_number': 'CN11A1A00001', From 4a9ebd9af186bb76fd2a05cbe64a2d8b7c79e580 Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Tue, 11 Jun 2024 05:12:09 +1000 Subject: [PATCH 0485/1445] Refactor helpers and bump Teslemetry (#119246) --- .../components/teslemetry/__init__.py | 1 + homeassistant/components/teslemetry/button.py | 3 +- .../components/teslemetry/climate.py | 17 ++--- homeassistant/components/teslemetry/cover.py | 19 +++--- homeassistant/components/teslemetry/entity.py | 57 ++--------------- .../components/teslemetry/helpers.py | 63 +++++++++++++++++++ homeassistant/components/teslemetry/lock.py | 7 ++- .../components/teslemetry/manifest.json | 2 +- .../components/teslemetry/media_player.py | 11 ++-- homeassistant/components/teslemetry/number.py | 5 +- homeassistant/components/teslemetry/select.py | 13 ++-- homeassistant/components/teslemetry/switch.py | 13 ++-- homeassistant/components/teslemetry/update.py | 3 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../teslemetry/snapshots/test_init.ambr | 2 +- tests/components/teslemetry/test_climate.py | 2 +- 17 files changed, 126 insertions(+), 96 deletions(-) create mode 100644 homeassistant/components/teslemetry/helpers.py diff --git a/homeassistant/components/teslemetry/__init__.py b/homeassistant/components/teslemetry/__init__.py index 16d32736165..387ebd1039e 100644 --- a/homeassistant/components/teslemetry/__init__.py +++ b/homeassistant/components/teslemetry/__init__.py @@ -102,6 +102,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: TeslemetryConfigEntry) - manufacturer="Tesla", configuration_url="https://teslemetry.com/console", name=product.get("site_name", "Energy Site"), + serial_number=str(site_id), ) energysites.append( diff --git a/homeassistant/components/teslemetry/button.py b/homeassistant/components/teslemetry/button.py index 433279f21da..011879525b8 100644 --- a/homeassistant/components/teslemetry/button.py +++ b/homeassistant/components/teslemetry/button.py @@ -14,6 +14,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import TeslemetryConfigEntry from .entity import TeslemetryVehicleEntity +from .helpers import handle_vehicle_command from .models import TeslemetryVehicleData @@ -84,4 +85,4 @@ class TeslemetryButtonEntity(TeslemetryVehicleEntity, ButtonEntity): """Press the button.""" await self.wake_up_if_asleep() if self.entity_description.func: - await self.handle_command(self.entity_description.func(self)) + await handle_vehicle_command(self.entity_description.func(self)) diff --git a/homeassistant/components/teslemetry/climate.py b/homeassistant/components/teslemetry/climate.py index a70dc5a360a..1158822f960 100644 --- a/homeassistant/components/teslemetry/climate.py +++ b/homeassistant/components/teslemetry/climate.py @@ -26,6 +26,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import TeslemetryConfigEntry from .const import DOMAIN, TeslemetryClimateSide from .entity import TeslemetryVehicleEntity +from .helpers import handle_vehicle_command from .models import TeslemetryVehicleData DEFAULT_MIN_TEMP = 15 @@ -114,7 +115,7 @@ class TeslemetryClimateEntity(TeslemetryVehicleEntity, ClimateEntity): self.raise_for_scope() await self.wake_up_if_asleep() - await self.handle_command(self.api.auto_conditioning_start()) + await handle_vehicle_command(self.api.auto_conditioning_start()) self._attr_hvac_mode = HVACMode.HEAT_COOL self.async_write_ha_state() @@ -124,7 +125,7 @@ class TeslemetryClimateEntity(TeslemetryVehicleEntity, ClimateEntity): self.raise_for_scope() await self.wake_up_if_asleep() - await self.handle_command(self.api.auto_conditioning_stop()) + await handle_vehicle_command(self.api.auto_conditioning_stop()) self._attr_hvac_mode = HVACMode.OFF self._attr_preset_mode = self._attr_preset_modes[0] @@ -135,7 +136,7 @@ class TeslemetryClimateEntity(TeslemetryVehicleEntity, ClimateEntity): if temp := kwargs.get(ATTR_TEMPERATURE): await self.wake_up_if_asleep() - await self.handle_command( + await handle_vehicle_command( self.api.set_temps( driver_temp=temp, passenger_temp=temp, @@ -159,7 +160,7 @@ class TeslemetryClimateEntity(TeslemetryVehicleEntity, ClimateEntity): async def async_set_preset_mode(self, preset_mode: str) -> None: """Set the climate preset mode.""" await self.wake_up_if_asleep() - await self.handle_command( + await handle_vehicle_command( self.api.set_climate_keeper_mode( climate_keeper_mode=self._attr_preset_modes.index(preset_mode) ) @@ -261,7 +262,7 @@ class TeslemetryCabinOverheatProtectionEntity(TeslemetryVehicleEntity, ClimateEn ) await self.wake_up_if_asleep() - await self.handle_command(self.api.set_cop_temp(cop_mode)) + await handle_vehicle_command(self.api.set_cop_temp(cop_mode)) self._attr_target_temperature = temp if mode := kwargs.get(ATTR_HVAC_MODE): @@ -271,15 +272,15 @@ class TeslemetryCabinOverheatProtectionEntity(TeslemetryVehicleEntity, ClimateEn async def _async_set_cop(self, hvac_mode: HVACMode) -> None: if hvac_mode == HVACMode.OFF: - await self.handle_command( + await handle_vehicle_command( self.api.set_cabin_overheat_protection(on=False, fan_only=False) ) elif hvac_mode == HVACMode.COOL: - await self.handle_command( + await handle_vehicle_command( self.api.set_cabin_overheat_protection(on=True, fan_only=False) ) elif hvac_mode == HVACMode.FAN_ONLY: - await self.handle_command( + await handle_vehicle_command( self.api.set_cabin_overheat_protection(on=True, fan_only=True) ) diff --git a/homeassistant/components/teslemetry/cover.py b/homeassistant/components/teslemetry/cover.py index 6c08dff6c96..4fbbb5fdb2b 100644 --- a/homeassistant/components/teslemetry/cover.py +++ b/homeassistant/components/teslemetry/cover.py @@ -17,6 +17,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import TeslemetryConfigEntry from .entity import TeslemetryVehicleEntity +from .helpers import handle_vehicle_command from .models import TeslemetryVehicleData OPEN = 1 @@ -88,7 +89,9 @@ class TeslemetryWindowEntity(TeslemetryVehicleEntity, CoverEntity): """Vent windows.""" self.raise_for_scope() await self.wake_up_if_asleep() - await self.handle_command(self.api.window_control(command=WindowCommand.VENT)) + await handle_vehicle_command( + self.api.window_control(command=WindowCommand.VENT) + ) self._attr_is_closed = False self.async_write_ha_state() @@ -96,7 +99,9 @@ class TeslemetryWindowEntity(TeslemetryVehicleEntity, CoverEntity): """Close windows.""" self.raise_for_scope() await self.wake_up_if_asleep() - await self.handle_command(self.api.window_control(command=WindowCommand.CLOSE)) + await handle_vehicle_command( + self.api.window_control(command=WindowCommand.CLOSE) + ) self._attr_is_closed = True self.async_write_ha_state() @@ -127,7 +132,7 @@ class TeslemetryChargePortEntity(TeslemetryVehicleEntity, CoverEntity): """Open charge port.""" self.raise_for_scope() await self.wake_up_if_asleep() - await self.handle_command(self.api.charge_port_door_open()) + await handle_vehicle_command(self.api.charge_port_door_open()) self._attr_is_closed = False self.async_write_ha_state() @@ -135,7 +140,7 @@ class TeslemetryChargePortEntity(TeslemetryVehicleEntity, CoverEntity): """Close charge port.""" self.raise_for_scope() await self.wake_up_if_asleep() - await self.handle_command(self.api.charge_port_door_close()) + await handle_vehicle_command(self.api.charge_port_door_close()) self._attr_is_closed = True self.async_write_ha_state() @@ -162,7 +167,7 @@ class TeslemetryFrontTrunkEntity(TeslemetryVehicleEntity, CoverEntity): """Open front trunk.""" self.raise_for_scope() await self.wake_up_if_asleep() - await self.handle_command(self.api.actuate_trunk(Trunk.FRONT)) + await handle_vehicle_command(self.api.actuate_trunk(Trunk.FRONT)) self._attr_is_closed = False self.async_write_ha_state() @@ -198,7 +203,7 @@ class TeslemetryRearTrunkEntity(TeslemetryVehicleEntity, CoverEntity): if self.is_closed is not False: self.raise_for_scope() await self.wake_up_if_asleep() - await self.handle_command(self.api.actuate_trunk(Trunk.REAR)) + await handle_vehicle_command(self.api.actuate_trunk(Trunk.REAR)) self._attr_is_closed = False self.async_write_ha_state() @@ -207,6 +212,6 @@ class TeslemetryRearTrunkEntity(TeslemetryVehicleEntity, CoverEntity): if self.is_closed is not True: self.raise_for_scope() await self.wake_up_if_asleep() - await self.handle_command(self.api.actuate_trunk(Trunk.REAR)) + await handle_vehicle_command(self.api.actuate_trunk(Trunk.REAR)) self._attr_is_closed = True self.async_write_ha_state() diff --git a/homeassistant/components/teslemetry/entity.py b/homeassistant/components/teslemetry/entity.py index dd6e6e575c2..74c1fdd52b1 100644 --- a/homeassistant/components/teslemetry/entity.py +++ b/homeassistant/components/teslemetry/entity.py @@ -1,22 +1,21 @@ """Teslemetry parent entity class.""" from abc import abstractmethod -import asyncio from typing import Any from tesla_fleet_api import EnergySpecific, VehicleSpecific -from tesla_fleet_api.exceptions import TeslaFleetError -from homeassistant.exceptions import HomeAssistantError, ServiceValidationError +from homeassistant.exceptions import ServiceValidationError from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import DOMAIN, LOGGER, TeslemetryState +from .const import DOMAIN from .coordinator import ( TeslemetryEnergySiteInfoCoordinator, TeslemetryEnergySiteLiveCoordinator, TeslemetryVehicleDataCoordinator, ) +from .helpers import wake_up_vehicle from .models import TeslemetryEnergyData, TeslemetryVehicleData @@ -76,15 +75,6 @@ class TeslemetryEntity( """Return True if a specific value is in coordinator data.""" return self.key in self.coordinator.data - async def handle_command(self, command) -> dict[str, Any]: - """Handle a command.""" - try: - result = await command - except TeslaFleetError as e: - raise HomeAssistantError(f"Teslemetry command failed, {e.message}") from e - LOGGER.debug("Command result: %s", result) - return result - def _handle_coordinator_update(self) -> None: """Handle updated data from the coordinator.""" self._async_update_attrs() @@ -113,7 +103,7 @@ class TeslemetryVehicleEntity(TeslemetryEntity): """Initialize common aspects of a Teslemetry entity.""" self._attr_unique_id = f"{data.vin}-{key}" - self._wakelock = data.wakelock + self.vehicle = data self._attr_device_info = data.device super().__init__(data.coordinator, data.api, key) @@ -125,44 +115,7 @@ class TeslemetryVehicleEntity(TeslemetryEntity): async def wake_up_if_asleep(self) -> None: """Wake up the vehicle if its asleep.""" - async with self._wakelock: - times = 0 - while self.coordinator.data["state"] != TeslemetryState.ONLINE: - try: - if times == 0: - cmd = await self.api.wake_up() - else: - cmd = await self.api.vehicle() - state = cmd["response"]["state"] - except TeslaFleetError as e: - raise HomeAssistantError(str(e)) from e - self.coordinator.data["state"] = state - if state != TeslemetryState.ONLINE: - times += 1 - if times >= 4: # Give up after 30 seconds total - raise HomeAssistantError("Could not wake up vehicle") - await asyncio.sleep(times * 5) - - async def handle_command(self, command) -> dict[str, Any]: - """Handle a vehicle command.""" - result = await super().handle_command(command) - if (response := result.get("response")) is None: - if error := result.get("error"): - # No response with error - raise HomeAssistantError(error) - # No response without error (unexpected) - raise HomeAssistantError(f"Unknown response: {response}") - if (result := response.get("result")) is not True: - if reason := response.get("reason"): - if reason in ("already_set", "not_charging", "requested"): - # Reason is acceptable - return result - # Result of false with reason - raise HomeAssistantError(reason) - # Result of false without reason (unexpected) - raise HomeAssistantError("Command failed with no reason") - # Response with result of true - return result + await wake_up_vehicle(self.vehicle) class TeslemetryEnergyLiveEntity(TeslemetryEntity): diff --git a/homeassistant/components/teslemetry/helpers.py b/homeassistant/components/teslemetry/helpers.py new file mode 100644 index 00000000000..a8cfa1051f1 --- /dev/null +++ b/homeassistant/components/teslemetry/helpers.py @@ -0,0 +1,63 @@ +"""Teslemetry helper functions.""" + +import asyncio +from typing import Any + +from tesla_fleet_api.exceptions import TeslaFleetError + +from homeassistant.exceptions import HomeAssistantError + +from .const import LOGGER, TeslemetryState + + +async def wake_up_vehicle(vehicle) -> None: + """Wake up a vehicle.""" + async with vehicle.wakelock: + times = 0 + while vehicle.coordinator.data["state"] != TeslemetryState.ONLINE: + try: + if times == 0: + cmd = await vehicle.api.wake_up() + else: + cmd = await vehicle.api.vehicle() + state = cmd["response"]["state"] + except TeslaFleetError as e: + raise HomeAssistantError(str(e)) from e + vehicle.coordinator.data["state"] = state + if state != TeslemetryState.ONLINE: + times += 1 + if times >= 4: # Give up after 30 seconds total + raise HomeAssistantError("Could not wake up vehicle") + await asyncio.sleep(times * 5) + + +async def handle_command(command) -> dict[str, Any]: + """Handle a command.""" + try: + result = await command + except TeslaFleetError as e: + raise HomeAssistantError(f"Teslemetry command failed, {e.message}") from e + LOGGER.debug("Command result: %s", result) + return result + + +async def handle_vehicle_command(command) -> dict[str, Any]: + """Handle a vehicle command.""" + result = await handle_command(command) + if (response := result.get("response")) is None: + if error := result.get("error"): + # No response with error + raise HomeAssistantError(error) + # No response without error (unexpected) + raise HomeAssistantError(f"Unknown response: {response}") + if (result := response.get("result")) is not True: + if reason := response.get("reason"): + if reason in ("already_set", "not_charging", "requested"): + # Reason is acceptable + return result + # Result of false with reason + raise HomeAssistantError(reason) + # Result of false without reason (unexpected) + raise HomeAssistantError("Command failed with no reason") + # Response with result of true + return result diff --git a/homeassistant/components/teslemetry/lock.py b/homeassistant/components/teslemetry/lock.py index d40d389bfb9..2201b898d66 100644 --- a/homeassistant/components/teslemetry/lock.py +++ b/homeassistant/components/teslemetry/lock.py @@ -14,6 +14,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import TeslemetryConfigEntry from .const import DOMAIN from .entity import TeslemetryVehicleEntity +from .helpers import handle_vehicle_command from .models import TeslemetryVehicleData ENGAGED = "Engaged" @@ -52,7 +53,7 @@ class TeslemetryVehicleLockEntity(TeslemetryVehicleEntity, LockEntity): """Lock the doors.""" self.raise_for_scope() await self.wake_up_if_asleep() - await self.handle_command(self.api.door_lock()) + await handle_vehicle_command(self.api.door_lock()) self._attr_is_locked = True self.async_write_ha_state() @@ -60,7 +61,7 @@ class TeslemetryVehicleLockEntity(TeslemetryVehicleEntity, LockEntity): """Unlock the doors.""" self.raise_for_scope() await self.wake_up_if_asleep() - await self.handle_command(self.api.door_unlock()) + await handle_vehicle_command(self.api.door_unlock()) self._attr_is_locked = False self.async_write_ha_state() @@ -95,6 +96,6 @@ class TeslemetryCableLockEntity(TeslemetryVehicleEntity, LockEntity): """Unlock charge cable lock.""" self.raise_for_scope() await self.wake_up_if_asleep() - await self.handle_command(self.api.charge_port_door_open()) + await handle_vehicle_command(self.api.charge_port_door_open()) self._attr_is_locked = False self.async_write_ha_state() diff --git a/homeassistant/components/teslemetry/manifest.json b/homeassistant/components/teslemetry/manifest.json index 14ac4a315d4..36a655b3b11 100644 --- a/homeassistant/components/teslemetry/manifest.json +++ b/homeassistant/components/teslemetry/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/teslemetry", "iot_class": "cloud_polling", "loggers": ["tesla-fleet-api"], - "requirements": ["tesla-fleet-api==0.5.12"] + "requirements": ["tesla-fleet-api==0.6.1"] } diff --git a/homeassistant/components/teslemetry/media_player.py b/homeassistant/components/teslemetry/media_player.py index 0f8533109ae..31c58e9505b 100644 --- a/homeassistant/components/teslemetry/media_player.py +++ b/homeassistant/components/teslemetry/media_player.py @@ -15,6 +15,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import TeslemetryConfigEntry from .entity import TeslemetryVehicleEntity +from .helpers import handle_vehicle_command from .models import TeslemetryVehicleData STATES = { @@ -114,7 +115,7 @@ class TeslemetryMediaEntity(TeslemetryVehicleEntity, MediaPlayerEntity): """Set volume level, range 0..1.""" self.raise_for_scope() await self.wake_up_if_asleep() - await self.handle_command( + await handle_vehicle_command( self.api.adjust_volume(int(volume * self._volume_max)) ) self._attr_volume_level = volume @@ -125,7 +126,7 @@ class TeslemetryMediaEntity(TeslemetryVehicleEntity, MediaPlayerEntity): if self.state != MediaPlayerState.PLAYING: self.raise_for_scope() await self.wake_up_if_asleep() - await self.handle_command(self.api.media_toggle_playback()) + await handle_vehicle_command(self.api.media_toggle_playback()) self._attr_state = MediaPlayerState.PLAYING self.async_write_ha_state() @@ -134,7 +135,7 @@ class TeslemetryMediaEntity(TeslemetryVehicleEntity, MediaPlayerEntity): if self.state == MediaPlayerState.PLAYING: self.raise_for_scope() await self.wake_up_if_asleep() - await self.handle_command(self.api.media_toggle_playback()) + await handle_vehicle_command(self.api.media_toggle_playback()) self._attr_state = MediaPlayerState.PAUSED self.async_write_ha_state() @@ -142,10 +143,10 @@ class TeslemetryMediaEntity(TeslemetryVehicleEntity, MediaPlayerEntity): """Send next track command.""" self.raise_for_scope() await self.wake_up_if_asleep() - await self.handle_command(self.api.media_next_track()) + await handle_vehicle_command(self.api.media_next_track()) async def async_media_previous_track(self) -> None: """Send previous track command.""" self.raise_for_scope() await self.wake_up_if_asleep() - await self.handle_command(self.api.media_prev_track()) + await handle_vehicle_command(self.api.media_prev_track()) diff --git a/homeassistant/components/teslemetry/number.py b/homeassistant/components/teslemetry/number.py index 592c20c3e4a..258fc5c5559 100644 --- a/homeassistant/components/teslemetry/number.py +++ b/homeassistant/components/teslemetry/number.py @@ -23,6 +23,7 @@ from homeassistant.helpers.icon import icon_for_battery_level from . import TeslemetryConfigEntry from .entity import TeslemetryEnergyInfoEntity, TeslemetryVehicleEntity +from .helpers import handle_command, handle_vehicle_command from .models import TeslemetryEnergyData, TeslemetryVehicleData @@ -163,7 +164,7 @@ class TeslemetryVehicleNumberEntity(TeslemetryVehicleEntity, NumberEntity): value = int(value) self.raise_for_scope() await self.wake_up_if_asleep() - await self.handle_command(self.entity_description.func(self.api, value)) + await handle_vehicle_command(self.entity_description.func(self.api, value)) self._attr_native_value = value self.async_write_ha_state() @@ -198,6 +199,6 @@ class TeslemetryEnergyInfoNumberSensorEntity(TeslemetryEnergyInfoEntity, NumberE """Set new value.""" value = int(value) self.raise_for_scope() - await self.handle_command(self.entity_description.func(self.api, value)) + await handle_command(self.entity_description.func(self.api, value)) self._attr_native_value = value self.async_write_ha_state() diff --git a/homeassistant/components/teslemetry/select.py b/homeassistant/components/teslemetry/select.py index c9c8cb1ec20..10d925ad94d 100644 --- a/homeassistant/components/teslemetry/select.py +++ b/homeassistant/components/teslemetry/select.py @@ -14,6 +14,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import TeslemetryConfigEntry from .entity import TeslemetryEnergyInfoEntity, TeslemetryVehicleEntity +from .helpers import handle_command, handle_vehicle_command from .models import TeslemetryEnergyData, TeslemetryVehicleData OFF = "off" @@ -146,8 +147,8 @@ class TeslemetrySeatHeaterSelectEntity(TeslemetryVehicleEntity, SelectEntity): level = self._attr_options.index(option) # AC must be on to turn on seat heater if level and not self.get("climate_state_is_climate_on"): - await self.handle_command(self.api.auto_conditioning_start()) - await self.handle_command( + await handle_vehicle_command(self.api.auto_conditioning_start()) + await handle_vehicle_command( self.api.remote_seat_heater_request(self.entity_description.position, level) ) self._attr_current_option = option @@ -191,8 +192,8 @@ class TeslemetryWheelHeaterSelectEntity(TeslemetryVehicleEntity, SelectEntity): level = self._attr_options.index(option) # AC must be on to turn on steering wheel heater if level and not self.get("climate_state_is_climate_on"): - await self.handle_command(self.api.auto_conditioning_start()) - await self.handle_command( + await handle_vehicle_command(self.api.auto_conditioning_start()) + await handle_vehicle_command( self.api.remote_steering_wheel_heat_level_request(level) ) self._attr_current_option = option @@ -224,7 +225,7 @@ class TeslemetryOperationSelectEntity(TeslemetryEnergyInfoEntity, SelectEntity): async def async_select_option(self, option: str) -> None: """Change the selected option.""" self.raise_for_scope() - await self.handle_command(self.api.operation(option)) + await handle_command(self.api.operation(option)) self._attr_current_option = option self.async_write_ha_state() @@ -254,7 +255,7 @@ class TeslemetryExportRuleSelectEntity(TeslemetryEnergyInfoEntity, SelectEntity) async def async_select_option(self, option: str) -> None: """Change the selected option.""" self.raise_for_scope() - await self.handle_command( + await handle_command( self.api.grid_import_export(customer_preferred_export_rule=option) ) self._attr_current_option = option diff --git a/homeassistant/components/teslemetry/switch.py b/homeassistant/components/teslemetry/switch.py index d7d5095db90..e23d34f242a 100644 --- a/homeassistant/components/teslemetry/switch.py +++ b/homeassistant/components/teslemetry/switch.py @@ -19,6 +19,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import TeslemetryConfigEntry from .entity import TeslemetryEnergyInfoEntity, TeslemetryVehicleEntity +from .helpers import handle_command, handle_vehicle_command from .models import TeslemetryEnergyData, TeslemetryVehicleData @@ -156,7 +157,7 @@ class TeslemetryVehicleSwitchEntity(TeslemetryVehicleEntity, TeslemetrySwitchEnt """Turn on the Switch.""" self.raise_for_scope() await self.wake_up_if_asleep() - await self.handle_command(self.entity_description.on_func(self.api)) + await handle_vehicle_command(self.entity_description.on_func(self.api)) self._attr_is_on = True self.async_write_ha_state() @@ -164,7 +165,7 @@ class TeslemetryVehicleSwitchEntity(TeslemetryVehicleEntity, TeslemetrySwitchEnt """Turn off the Switch.""" self.raise_for_scope() await self.wake_up_if_asleep() - await self.handle_command(self.entity_description.off_func(self.api)) + await handle_vehicle_command(self.entity_description.off_func(self.api)) self._attr_is_on = False self.async_write_ha_state() @@ -205,7 +206,7 @@ class TeslemetryChargeFromGridSwitchEntity( async def async_turn_on(self, **kwargs: Any) -> None: """Turn on the Switch.""" self.raise_for_scope() - await self.handle_command( + await handle_command( self.api.grid_import_export( disallow_charge_from_grid_with_solar_installed=False ) @@ -216,7 +217,7 @@ class TeslemetryChargeFromGridSwitchEntity( async def async_turn_off(self, **kwargs: Any) -> None: """Turn off the Switch.""" self.raise_for_scope() - await self.handle_command( + await handle_command( self.api.grid_import_export( disallow_charge_from_grid_with_solar_installed=True ) @@ -247,13 +248,13 @@ class TeslemetryStormModeSwitchEntity( async def async_turn_on(self, **kwargs: Any) -> None: """Turn on the Switch.""" self.raise_for_scope() - await self.handle_command(self.api.storm_mode(enabled=True)) + await handle_command(self.api.storm_mode(enabled=True)) self._attr_is_on = True self.async_write_ha_state() async def async_turn_off(self, **kwargs: Any) -> None: """Turn off the Switch.""" self.raise_for_scope() - await self.handle_command(self.api.storm_mode(enabled=False)) + await handle_command(self.api.storm_mode(enabled=False)) self._attr_is_on = False self.async_write_ha_state() diff --git a/homeassistant/components/teslemetry/update.py b/homeassistant/components/teslemetry/update.py index 89393700c1f..74ecec8020d 100644 --- a/homeassistant/components/teslemetry/update.py +++ b/homeassistant/components/teslemetry/update.py @@ -12,6 +12,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import TeslemetryConfigEntry from .entity import TeslemetryVehicleEntity +from .helpers import handle_vehicle_command from .models import TeslemetryVehicleData AVAILABLE = "available" @@ -102,6 +103,6 @@ class TeslemetryUpdateEntity(TeslemetryVehicleEntity, UpdateEntity): """Install an update.""" self.raise_for_scope() await self.wake_up_if_asleep() - await self.handle_command(self.api.schedule_software_update(offset_sec=60)) + await handle_vehicle_command(self.api.schedule_software_update(offset_sec=60)) self._attr_in_progress = True self.async_write_ha_state() diff --git a/requirements_all.txt b/requirements_all.txt index 14ede992a85..367fe4de0f4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2707,7 +2707,7 @@ temperusb==1.6.1 # tensorflow==2.5.0 # homeassistant.components.teslemetry -tesla-fleet-api==0.5.12 +tesla-fleet-api==0.6.1 # homeassistant.components.powerwall tesla-powerwall==0.5.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index aad6dc6124a..21b41dc1c28 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2105,7 +2105,7 @@ temescal==0.5 temperusb==1.6.1 # homeassistant.components.teslemetry -tesla-fleet-api==0.5.12 +tesla-fleet-api==0.6.1 # homeassistant.components.powerwall tesla-powerwall==0.5.2 diff --git a/tests/components/teslemetry/snapshots/test_init.ambr b/tests/components/teslemetry/snapshots/test_init.ambr index 434e9025ac7..951e4557bdd 100644 --- a/tests/components/teslemetry/snapshots/test_init.ambr +++ b/tests/components/teslemetry/snapshots/test_init.ambr @@ -23,7 +23,7 @@ 'model': 'Powerwall 2, Tesla Backup Gateway 2', 'name': 'Energy Site', 'name_by_user': None, - 'serial_number': None, + 'serial_number': '123456', 'suggested_area': None, 'sw_version': None, 'via_device_id': None, diff --git a/tests/components/teslemetry/test_climate.py b/tests/components/teslemetry/test_climate.py index a737fc9f376..250413396c1 100644 --- a/tests/components/teslemetry/test_climate.py +++ b/tests/components/teslemetry/test_climate.py @@ -343,7 +343,7 @@ async def test_asleep_or_offline( mock_wake_up.return_value = WAKE_UP_ASLEEP mock_vehicle.return_value = WAKE_UP_ASLEEP with ( - patch("homeassistant.components.teslemetry.entity.asyncio.sleep"), + patch("homeassistant.components.teslemetry.helpers.asyncio.sleep"), pytest.raises(HomeAssistantError) as error, ): await hass.services.async_call( From 6184fd26d384c96553de8bb62388ae95dec8bf92 Mon Sep 17 00:00:00 2001 From: Sid <27780930+autinerd@users.noreply.github.com> Date: Mon, 10 Jun 2024 21:13:12 +0200 Subject: [PATCH 0486/1445] Add options flow to enigma2 (#115795) Co-authored-by: Franck Nijhof --- .../components/enigma2/config_flow.py | 48 +++++++++++++++++-- homeassistant/components/enigma2/strings.json | 14 ++++++ tests/components/enigma2/conftest.py | 11 +++++ tests/components/enigma2/test_config_flow.py | 33 +++++++++++++ 4 files changed, 103 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/enigma2/config_flow.py b/homeassistant/components/enigma2/config_flow.py index b628d10b91a..b9ae6ffbebf 100644 --- a/homeassistant/components/enigma2/config_flow.py +++ b/homeassistant/components/enigma2/config_flow.py @@ -1,6 +1,6 @@ """Config flow for Enigma2.""" -from typing import Any +from typing import Any, cast from aiohttp.client_exceptions import ClientError from openwebif.api import OpenWebIfDevice @@ -8,7 +8,12 @@ from openwebif.error import InvalidAuthError import voluptuous as vol from yarl import URL -from homeassistant.config_entries import SOURCE_USER, ConfigFlow, ConfigFlowResult +from homeassistant.config_entries import ( + SOURCE_USER, + ConfigEntry, + ConfigFlow, + ConfigFlowResult, +) from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, @@ -17,10 +22,15 @@ from homeassistant.const import ( CONF_USERNAME, CONF_VERIFY_SSL, ) -from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, callback from homeassistant.helpers import selector from homeassistant.helpers.aiohttp_client import async_create_clientsession from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue +from homeassistant.helpers.schema_config_entry_flow import ( + SchemaCommonFlowHandler, + SchemaFlowFormStep, + SchemaOptionsFlowHandler, +) from .const import ( CONF_DEEP_STANDBY, @@ -55,6 +65,32 @@ CONFIG_SCHEMA = vol.Schema( ) +async def get_options_schema(handler: SchemaCommonFlowHandler) -> vol.Schema: + """Get the options schema.""" + hass = handler.parent_handler.hass + entry = cast(SchemaOptionsFlowHandler, handler.parent_handler).config_entry + device: OpenWebIfDevice = hass.data[DOMAIN][entry.entry_id] + bouquets = [b[1] for b in (await device.get_all_bouquets())["bouquets"]] + + return vol.Schema( + { + vol.Optional(CONF_DEEP_STANDBY): selector.BooleanSelector(), + vol.Optional(CONF_SOURCE_BOUQUET): selector.SelectSelector( + selector.SelectSelectorConfig( + options=bouquets, + mode=selector.SelectSelectorMode.DROPDOWN, + ) + ), + vol.Optional(CONF_USE_CHANNEL_ICON): selector.BooleanSelector(), + } + ) + + +OPTIONS_FLOW = { + "init": SchemaFlowFormStep(get_options_schema), +} + + class Enigma2ConfigFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a config flow for Enigma2.""" @@ -163,3 +199,9 @@ class Enigma2ConfigFlowHandler(ConfigFlow, domain=DOMAIN): return self.async_create_entry( data=data, title=data[CONF_HOST], options=options ) + + @staticmethod + @callback + def async_get_options_flow(config_entry: ConfigEntry) -> SchemaOptionsFlowHandler: + """Get the options flow for this handler.""" + return SchemaOptionsFlowHandler(config_entry, OPTIONS_FLOW) diff --git a/homeassistant/components/enigma2/strings.json b/homeassistant/components/enigma2/strings.json index ddeb59ea6d5..f74806b60a2 100644 --- a/homeassistant/components/enigma2/strings.json +++ b/homeassistant/components/enigma2/strings.json @@ -26,6 +26,20 @@ "unknown": "[%key:common::config_flow::error::unknown%]" } }, + "options": { + "step": { + "init": { + "data": { + "deep_standby": "Turn off to Deep Standby", + "source_bouquet": "Bouquet to use as media source", + "use_channel_icon": "Show channel icon as media image" + }, + "data_description": { + "deep_standby": "Turn off the device to Deep Standby (shutdown) instead of standby mode." + } + } + } + }, "issues": { "deprecated_yaml_import_issue_unknown": { "title": "The Enigma2 YAML configuration import failed", diff --git a/tests/components/enigma2/conftest.py b/tests/components/enigma2/conftest.py index 9bbbda895bd..f879fb327d7 100644 --- a/tests/components/enigma2/conftest.py +++ b/tests/components/enigma2/conftest.py @@ -86,5 +86,16 @@ class MockDevice: """Get mock about endpoint.""" return await self._call_api("/api/about") + async def get_all_bouquets(self) -> dict: + """Get all bouquets.""" + return { + "bouquets": [ + [ + '1:7:1:0:0:0:0:0:0:0:FROM BOUQUET "userbouquet.favourites.tv" ORDER BY bouquet', + "Favourites (TV)", + ] + ] + } + async def close(self): """Mock close.""" diff --git a/tests/components/enigma2/test_config_flow.py b/tests/components/enigma2/test_config_flow.py index b4bcb29f0ac..a1074ed9e34 100644 --- a/tests/components/enigma2/test_config_flow.py +++ b/tests/components/enigma2/test_config_flow.py @@ -23,6 +23,8 @@ from .conftest import ( MockDevice, ) +from tests.common import MockConfigEntry + @pytest.fixture async def user_flow(hass: HomeAssistant) -> str: @@ -164,3 +166,34 @@ async def test_form_import_errors( assert issue.issue_domain == DOMAIN assert result["type"] is FlowResultType.ABORT assert result["reason"] == error_type + + +async def test_options_flow(hass: HomeAssistant, user_flow: str): + """Test the form options.""" + + with patch( + "openwebif.api.OpenWebIfDevice.__new__", + return_value=MockDevice(), + ): + entry = MockConfigEntry(domain=DOMAIN, data=TEST_FULL, options={}, entry_id="1") + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state is config_entries.ConfigEntryState.LOADED + + result = await hass.config_entries.options.async_init(entry.entry_id) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "init" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], user_input={"source_bouquet": "Favourites (TV)"} + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert entry.options == {"source_bouquet": "Favourites (TV)"} + + await hass.async_block_till_done() + + assert entry.state is config_entries.ConfigEntryState.LOADED From b30a924e0388b14bf994009edfc1239a1c1659a6 Mon Sep 17 00:00:00 2001 From: Bas Brussee <68892092+basbruss@users.noreply.github.com> Date: Mon, 10 Jun 2024 21:16:51 +0200 Subject: [PATCH 0487/1445] Add price service call to Tibber (#117366) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Daniel Hjelseth Høyer Co-authored-by: Jason R. Coombs --- homeassistant/components/tibber/__init__.py | 4 + homeassistant/components/tibber/icons.json | 5 + homeassistant/components/tibber/services.py | 106 ++++++++ homeassistant/components/tibber/services.yaml | 12 + homeassistant/components/tibber/strings.json | 16 ++ tests/components/tibber/test_services.py | 254 ++++++++++++++++++ 6 files changed, 397 insertions(+) create mode 100644 homeassistant/components/tibber/icons.json create mode 100644 homeassistant/components/tibber/services.py create mode 100644 homeassistant/components/tibber/services.yaml create mode 100644 tests/components/tibber/test_services.py diff --git a/homeassistant/components/tibber/__init__.py b/homeassistant/components/tibber/__init__.py index 49633707ed6..51d6f0560f1 100644 --- a/homeassistant/components/tibber/__init__.py +++ b/homeassistant/components/tibber/__init__.py @@ -21,6 +21,7 @@ from homeassistant.helpers.typing import ConfigType from homeassistant.util import dt as dt_util from .const import DATA_HASS_CONFIG, DOMAIN +from .services import async_setup_services PLATFORMS = [Platform.NOTIFY, Platform.SENSOR] @@ -33,6 +34,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Tibber component.""" hass.data[DATA_HASS_CONFIG] = config + + async_setup_services(hass) + return True diff --git a/homeassistant/components/tibber/icons.json b/homeassistant/components/tibber/icons.json new file mode 100644 index 00000000000..c6cdd9b0e25 --- /dev/null +++ b/homeassistant/components/tibber/icons.json @@ -0,0 +1,5 @@ +{ + "services": { + "get_prices": "mdi:cash" + } +} diff --git a/homeassistant/components/tibber/services.py b/homeassistant/components/tibber/services.py new file mode 100644 index 00000000000..82353bb78d7 --- /dev/null +++ b/homeassistant/components/tibber/services.py @@ -0,0 +1,106 @@ +"""Services for Tibber integration.""" + +from __future__ import annotations + +import datetime as dt +from datetime import date, datetime +from functools import partial +from typing import Any, Final + +import voluptuous as vol + +from homeassistant.core import ( + HomeAssistant, + ServiceCall, + ServiceResponse, + SupportsResponse, + callback, +) +from homeassistant.exceptions import ServiceValidationError +from homeassistant.util import dt as dt_util + +from .const import DOMAIN + +PRICE_SERVICE_NAME = "get_prices" +ATTR_START: Final = "start" +ATTR_END: Final = "end" + +SERVICE_SCHEMA: Final = vol.Schema( + { + vol.Optional(ATTR_START): str, + vol.Optional(ATTR_END): str, + } +) + + +async def __get_prices(call: ServiceCall, *, hass: HomeAssistant) -> ServiceResponse: + tibber_connection = hass.data[DOMAIN] + + start = __get_date(call.data.get(ATTR_START), "start") + end = __get_date(call.data.get(ATTR_END), "end") + + if start >= end: + end = start + dt.timedelta(days=1) + + tibber_prices: dict[str, Any] = {} + + for tibber_home in tibber_connection.get_homes(only_active=True): + home_nickname = tibber_home.name + + price_info = tibber_home.info["viewer"]["home"]["currentSubscription"][ + "priceInfo" + ] + price_data = [ + { + "start_time": dt.datetime.fromisoformat(price["startsAt"]), + "price": price["total"], + "level": price["level"], + } + for key in ("today", "tomorrow") + for price in price_info[key] + ] + + selected_data = [ + price + for price in price_data + if price["start_time"].replace(tzinfo=None) >= start + and price["start_time"].replace(tzinfo=None) < end + ] + tibber_prices[home_nickname] = selected_data + + return {"prices": tibber_prices} + + +def __get_date(date_input: str | None, mode: str | None) -> date | datetime: + """Get date.""" + if not date_input: + if mode == "end": + increment = dt.timedelta(days=1) + else: + increment = dt.timedelta() + return datetime.fromisoformat(dt_util.now().date().isoformat()) + increment + + if value := dt_util.parse_datetime(date_input): + return value + + raise ServiceValidationError( + "Invalid datetime provided.", + translation_domain=DOMAIN, + translation_key="invalid_date", + translation_placeholders={ + "date": date_input, + }, + ) + + +@callback +def async_setup_services(hass: HomeAssistant) -> None: + """Set up services for Tibber integration.""" + + hass.services.async_register( + DOMAIN, + PRICE_SERVICE_NAME, + partial(__get_prices, hass=hass), + schema=SERVICE_SCHEMA, + supports_response=SupportsResponse.ONLY, + ) diff --git a/homeassistant/components/tibber/services.yaml b/homeassistant/components/tibber/services.yaml new file mode 100644 index 00000000000..0a4413aa54e --- /dev/null +++ b/homeassistant/components/tibber/services.yaml @@ -0,0 +1,12 @@ +get_prices: + fields: + start: + required: false + example: "2024-01-01 00:00:00" + selector: + datetime: + end: + required: false + example: "2024-01-01 23:00:00" + selector: + datetime: diff --git a/homeassistant/components/tibber/strings.json b/homeassistant/components/tibber/strings.json index 7647dcb9e9a..00a9efe342a 100644 --- a/homeassistant/components/tibber/strings.json +++ b/homeassistant/components/tibber/strings.json @@ -84,6 +84,22 @@ } } }, + "services": { + "get_prices": { + "name": "Get enegry prices", + "description": "Get hourly energy prices from Tibber", + "fields": { + "start": { + "name": "Start", + "description": "Specifies the date and time from which to retrieve prices. Defaults to today if omitted." + }, + "end": { + "name": "End", + "description": "Specifies the date and time until which to retrieve prices. Defaults to the last hour of today if omitted." + } + } + } + }, "config": { "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" diff --git a/tests/components/tibber/test_services.py b/tests/components/tibber/test_services.py new file mode 100644 index 00000000000..fe437e421d7 --- /dev/null +++ b/tests/components/tibber/test_services.py @@ -0,0 +1,254 @@ +"""Test service for Tibber integration.""" + +import asyncio +import datetime as dt +from unittest.mock import MagicMock + +import pytest + +from homeassistant.components.tibber.const import DOMAIN +from homeassistant.components.tibber.services import PRICE_SERVICE_NAME, __get_prices +from homeassistant.core import ServiceCall +from homeassistant.exceptions import ServiceValidationError + + +def generate_mock_home_data(): + """Create mock data from the tibber connection.""" + today = remove_microseconds(dt.datetime.now()) + tomorrow = remove_microseconds(today + dt.timedelta(days=1)) + mock_homes = [ + MagicMock( + name="first_home", + info={ + "viewer": { + "home": { + "currentSubscription": { + "priceInfo": { + "today": [ + { + "startsAt": today.isoformat(), + "total": 0.46914, + "level": "VERY_EXPENSIVE", + }, + { + "startsAt": ( + today + dt.timedelta(hours=1) + ).isoformat(), + "total": 0.46914, + "level": "VERY_EXPENSIVE", + }, + ], + "tomorrow": [ + { + "startsAt": tomorrow.isoformat(), + "total": 0.46914, + "level": "VERY_EXPENSIVE", + }, + { + "startsAt": ( + tomorrow + dt.timedelta(hours=1) + ).isoformat(), + "total": 0.46914, + "level": "VERY_EXPENSIVE", + }, + ], + } + } + } + } + }, + ), + MagicMock( + name="second_home", + info={ + "viewer": { + "home": { + "currentSubscription": { + "priceInfo": { + "today": [ + { + "startsAt": today.isoformat(), + "total": 0.46914, + "level": "VERY_EXPENSIVE", + }, + { + "startsAt": ( + today + dt.timedelta(hours=1) + ).isoformat(), + "total": 0.46914, + "level": "VERY_EXPENSIVE", + }, + ], + "tomorrow": [ + { + "startsAt": tomorrow.isoformat(), + "total": 0.46914, + "level": "VERY_EXPENSIVE", + }, + { + "startsAt": ( + tomorrow + dt.timedelta(hours=1) + ).isoformat(), + "total": 0.46914, + "level": "VERY_EXPENSIVE", + }, + ], + } + } + } + } + }, + ), + ] + mock_homes[0].name = "first_home" + mock_homes[1].name = "second_home" + return mock_homes + + +def create_mock_tibber_connection(): + """Create a mock tibber connection.""" + tibber_connection = MagicMock() + tibber_connection.get_homes.return_value = generate_mock_home_data() + return tibber_connection + + +def create_mock_hass(): + """Create a mock hass object.""" + mock_hass = MagicMock + mock_hass.data = {"tibber": create_mock_tibber_connection()} + return mock_hass + + +def remove_microseconds(dt): + """Remove microseconds from a datetime object.""" + return dt.replace(microsecond=0) + + +async def test_get_prices(): + """Test __get_prices with mock data.""" + today = remove_microseconds(dt.datetime.now()) + tomorrow = remove_microseconds(dt.datetime.now() + dt.timedelta(days=1)) + call = ServiceCall( + DOMAIN, + PRICE_SERVICE_NAME, + {"start": today.date().isoformat(), "end": tomorrow.date().isoformat()}, + ) + + result = await __get_prices(call, hass=create_mock_hass()) + + assert result == { + "prices": { + "first_home": [ + { + "start_time": today, + "price": 0.46914, + "level": "VERY_EXPENSIVE", + }, + { + "start_time": today + dt.timedelta(hours=1), + "price": 0.46914, + "level": "VERY_EXPENSIVE", + }, + ], + "second_home": [ + { + "start_time": today, + "price": 0.46914, + "level": "VERY_EXPENSIVE", + }, + { + "start_time": today + dt.timedelta(hours=1), + "price": 0.46914, + "level": "VERY_EXPENSIVE", + }, + ], + } + } + + +async def test_get_prices_no_input(): + """Test __get_prices with no input.""" + today = remove_microseconds(dt.datetime.now()) + call = ServiceCall(DOMAIN, PRICE_SERVICE_NAME, {}) + + result = await __get_prices(call, hass=create_mock_hass()) + + assert result == { + "prices": { + "first_home": [ + { + "start_time": today, + "price": 0.46914, + "level": "VERY_EXPENSIVE", + }, + { + "start_time": today + dt.timedelta(hours=1), + "price": 0.46914, + "level": "VERY_EXPENSIVE", + }, + ], + "second_home": [ + { + "start_time": today, + "price": 0.46914, + "level": "VERY_EXPENSIVE", + }, + { + "start_time": today + dt.timedelta(hours=1), + "price": 0.46914, + "level": "VERY_EXPENSIVE", + }, + ], + } + } + + +async def test_get_prices_start_tomorrow(): + """Test __get_prices with start date tomorrow.""" + tomorrow = remove_microseconds(dt.datetime.now() + dt.timedelta(days=1)) + call = ServiceCall( + DOMAIN, PRICE_SERVICE_NAME, {"start": tomorrow.date().isoformat()} + ) + + result = await __get_prices(call, hass=create_mock_hass()) + + assert result == { + "prices": { + "first_home": [ + { + "start_time": tomorrow, + "price": 0.46914, + "level": "VERY_EXPENSIVE", + }, + { + "start_time": tomorrow + dt.timedelta(hours=1), + "price": 0.46914, + "level": "VERY_EXPENSIVE", + }, + ], + "second_home": [ + { + "start_time": tomorrow, + "price": 0.46914, + "level": "VERY_EXPENSIVE", + }, + { + "start_time": tomorrow + dt.timedelta(hours=1), + "price": 0.46914, + "level": "VERY_EXPENSIVE", + }, + ], + } + } + + +async def test_get_prices_invalid_input(): + """Test __get_prices with invalid input.""" + + call = ServiceCall(DOMAIN, PRICE_SERVICE_NAME, {"start": "test"}) + task = asyncio.create_task(__get_prices(call, hass=create_mock_hass())) + + with pytest.raises(ServiceValidationError) as excinfo: + await task + + assert "Invalid datetime provided." in str(excinfo.value) From 632f136c026fecbefd3611a892211fb2c94e9aa1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Mon, 10 Jun 2024 21:18:48 +0200 Subject: [PATCH 0488/1445] Update Airzone Cloud to v0.5.2 and add fan speeds to Zones (#119314) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update aioairzone-cloud to v0.5.2 * airzone_cloud: climate: add zone fan speeds support Signed-off-by: Álvaro Fernández Rojas --- .../components/airzone_cloud/climate.py | 102 +++++++++--------- .../components/airzone_cloud/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 51 insertions(+), 57 deletions(-) diff --git a/homeassistant/components/airzone_cloud/climate.py b/homeassistant/components/airzone_cloud/climate.py index 277bafba498..80f8af36a15 100644 --- a/homeassistant/components/airzone_cloud/climate.py +++ b/homeassistant/components/airzone_cloud/climate.py @@ -193,6 +193,12 @@ class AirzoneClimate(AirzoneEntity, ClimateEntity): ClimateEntityFeature.TARGET_TEMPERATURE_RANGE ) + if ( + self.get_airzone_value(AZD_SPEED) is not None + and self.get_airzone_value(AZD_SPEEDS) is not None + ): + self._initialize_fan_speeds() + @callback def _handle_coordinator_update(self) -> None: """Update attributes when the coordinator updates.""" @@ -207,6 +213,8 @@ class AirzoneClimate(AirzoneEntity, ClimateEntity): self._attr_hvac_action = HVAC_ACTION_LIB_TO_HASS[ self.get_airzone_value(AZD_ACTION) ] + if self.supported_features & ClimateEntityFeature.FAN_MODE: + self._attr_fan_mode = self._speeds.get(self.get_airzone_value(AZD_SPEED)) if self.get_airzone_value(AZD_POWER): self._attr_hvac_mode = HVAC_MODE_LIB_TO_HASS[ self.get_airzone_value(AZD_MODE) @@ -234,6 +242,37 @@ class AirzoneDeviceClimate(AirzoneClimate): | ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TURN_ON ) + _speeds: dict[int, str] + _speeds_reverse: dict[str, int] + + def _initialize_fan_speeds(self) -> None: + """Initialize fan speeds.""" + azd_speeds: dict[int, int] = self.get_airzone_value(AZD_SPEEDS) + max_speed = max(azd_speeds) + + fan_speeds: dict[int, str] + if speeds_map := FAN_SPEED_MAPS.get(max_speed): + fan_speeds = speeds_map + else: + fan_speeds = {} + + for speed in azd_speeds: + if speed != 0: + fan_speeds[speed] = f"{int(round((speed * 100) / max_speed, 0))}%" + + if 0 in azd_speeds: + fan_speeds = FAN_SPEED_AUTO | fan_speeds + + self._speeds = {} + for key, value in fan_speeds.items(): + _key = azd_speeds.get(key) + if _key is not None: + self._speeds[_key] = value + + self._speeds_reverse = {v: k for k, v in self._speeds.items()} + self._attr_fan_modes = list(self._speeds_reverse) + + self._attr_supported_features |= ClimateEntityFeature.FAN_MODE async def async_turn_on(self) -> None: """Turn the entity on.""" @@ -253,6 +292,15 @@ class AirzoneDeviceClimate(AirzoneClimate): } await self._async_update_params(params) + async def async_set_fan_mode(self, fan_mode: str) -> None: + """Set new fan mode.""" + params: dict[str, Any] = { + API_SPEED_CONF: { + API_VALUE: self._speeds_reverse.get(fan_mode), + } + } + await self._async_update_params(params) + async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" params: dict[str, Any] = {} @@ -341,9 +389,6 @@ class AirzoneDeviceGroupClimate(AirzoneClimate): class AirzoneAidooClimate(AirzoneAidooEntity, AirzoneDeviceClimate): """Define an Airzone Cloud Aidoo climate.""" - _speeds: dict[int, str] - _speeds_reverse: dict[str, int] - def __init__( self, coordinator: AirzoneUpdateCoordinator, @@ -355,52 +400,9 @@ class AirzoneAidooClimate(AirzoneAidooEntity, AirzoneDeviceClimate): self._attr_unique_id = aidoo_id self._init_attributes() - if ( - self.get_airzone_value(AZD_SPEED) is not None - and self.get_airzone_value(AZD_SPEEDS) is not None - ): - self._initialize_fan_speeds() self._async_update_attrs() - def _initialize_fan_speeds(self) -> None: - """Initialize Aidoo fan speeds.""" - azd_speeds: dict[int, int] = self.get_airzone_value(AZD_SPEEDS) - max_speed = max(azd_speeds) - - fan_speeds: dict[int, str] - if speeds_map := FAN_SPEED_MAPS.get(max_speed): - fan_speeds = speeds_map - else: - fan_speeds = {} - - for speed in azd_speeds: - if speed != 0: - fan_speeds[speed] = f"{int(round((speed * 100) / max_speed, 0))}%" - - if 0 in azd_speeds: - fan_speeds = FAN_SPEED_AUTO | fan_speeds - - self._speeds = {} - for key, value in fan_speeds.items(): - _key = azd_speeds.get(key) - if _key is not None: - self._speeds[_key] = value - - self._speeds_reverse = {v: k for k, v in self._speeds.items()} - self._attr_fan_modes = list(self._speeds_reverse) - - self._attr_supported_features |= ClimateEntityFeature.FAN_MODE - - async def async_set_fan_mode(self, fan_mode: str) -> None: - """Set Aidoo fan mode.""" - params: dict[str, Any] = { - API_SPEED_CONF: { - API_VALUE: self._speeds_reverse.get(fan_mode), - } - } - await self._async_update_params(params) - async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set hvac mode.""" params: dict[str, Any] = {} @@ -418,14 +420,6 @@ class AirzoneAidooClimate(AirzoneAidooEntity, AirzoneDeviceClimate): } await self._async_update_params(params) - @callback - def _async_update_attrs(self) -> None: - """Update Aidoo climate attributes.""" - super()._async_update_attrs() - - if self.supported_features & ClimateEntityFeature.FAN_MODE: - self._attr_fan_mode = self._speeds.get(self.get_airzone_value(AZD_SPEED)) - class AirzoneGroupClimate(AirzoneGroupEntity, AirzoneDeviceGroupClimate): """Define an Airzone Cloud Group climate.""" diff --git a/homeassistant/components/airzone_cloud/manifest.json b/homeassistant/components/airzone_cloud/manifest.json index 366f8214bc1..ca024d0e1a3 100644 --- a/homeassistant/components/airzone_cloud/manifest.json +++ b/homeassistant/components/airzone_cloud/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/airzone_cloud", "iot_class": "cloud_push", "loggers": ["aioairzone_cloud"], - "requirements": ["aioairzone-cloud==0.5.1"] + "requirements": ["aioairzone-cloud==0.5.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 367fe4de0f4..94e824342b3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -179,7 +179,7 @@ aio-georss-gdacs==0.9 aioairq==0.3.2 # homeassistant.components.airzone_cloud -aioairzone-cloud==0.5.1 +aioairzone-cloud==0.5.2 # homeassistant.components.airzone aioairzone==0.7.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 21b41dc1c28..005cd2ae77c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -158,7 +158,7 @@ aio-georss-gdacs==0.9 aioairq==0.3.2 # homeassistant.components.airzone_cloud -aioairzone-cloud==0.5.1 +aioairzone-cloud==0.5.2 # homeassistant.components.airzone aioairzone==0.7.6 From def9d5b1011e674f7d6f0661f141088863800d31 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 10 Jun 2024 21:44:55 +0200 Subject: [PATCH 0489/1445] Fix statistic_during_period after core restart (#119323) --- .../components/recorder/statistics.py | 25 +++++++++++++++++-- .../components/recorder/test_websocket_api.py | 18 +++++++++++-- 2 files changed, 39 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index 8d077e19344..691fc58c609 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -1244,7 +1244,7 @@ def _first_statistic( table: type[StatisticsBase], metadata_id: int, ) -> datetime | None: - """Return the data of the oldest statistic row for a given metadata id.""" + """Return the date of the oldest statistic row for a given metadata id.""" stmt = lambda_stmt( lambda: select(table.start_ts) .filter(table.metadata_id == metadata_id) @@ -1256,6 +1256,23 @@ def _first_statistic( return None +def _last_statistic( + session: Session, + table: type[StatisticsBase], + metadata_id: int, +) -> datetime | None: + """Return the date of the newest statistic row for a given metadata id.""" + stmt = lambda_stmt( + lambda: select(table.start_ts) + .filter(table.metadata_id == metadata_id) + .order_by(table.start_ts.desc()) + .limit(1) + ) + if stats := cast(Sequence[Row], execute_stmt_lambda_element(session, stmt)): + return dt_util.utc_from_timestamp(stats[0].start_ts) + return None + + def _get_oldest_sum_statistic( session: Session, head_start_time: datetime | None, @@ -1486,7 +1503,11 @@ def statistic_during_period( tail_start_time: datetime | None = None tail_end_time: datetime | None = None if end_time is None: - tail_start_time = now.replace(minute=0, second=0, microsecond=0) + tail_start_time = _last_statistic(session, Statistics, metadata_id) + if tail_start_time: + tail_start_time += Statistics.duration + else: + tail_start_time = now.replace(minute=0, second=0, microsecond=0) elif tail_only: tail_start_time = start_time tail_end_time = end_time diff --git a/tests/components/recorder/test_websocket_api.py b/tests/components/recorder/test_websocket_api.py index 0dd9241776d..d915eeeeeb6 100644 --- a/tests/components/recorder/test_websocket_api.py +++ b/tests/components/recorder/test_websocket_api.py @@ -7,6 +7,7 @@ import threading from unittest.mock import ANY, patch from freezegun import freeze_time +from freezegun.api import FrozenDateTimeFactory import pytest from homeassistant.components import recorder @@ -794,17 +795,30 @@ async def test_statistic_during_period_hole( } -@pytest.mark.freeze_time(datetime.datetime(2022, 10, 21, 6, 31, tzinfo=datetime.UTC)) +@pytest.mark.parametrize( + "frozen_time", + [ + # This is the normal case, all statistics runs are available + datetime.datetime(2022, 10, 21, 6, 31, tzinfo=datetime.UTC), + # Statistic only available up until 6:25, this can happen if + # core has been shut down for an hour + datetime.datetime(2022, 10, 21, 7, 31, tzinfo=datetime.UTC), + ], +) async def test_statistic_during_period_partial_overlap( recorder_mock: Recorder, hass: HomeAssistant, hass_ws_client: WebSocketGenerator, + freezer: FrozenDateTimeFactory, + frozen_time: datetime, ) -> None: """Test statistic_during_period.""" + client = await hass_ws_client() + + freezer.move_to(frozen_time) now = dt_util.utcnow() await async_recorder_block_till_done(hass) - client = await hass_ws_client() zero = now start = zero.replace(hour=0, minute=0, second=0, microsecond=0) From 8855289d9cb131fd488e1e48e8aec4c3bdc312af Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 10 Jun 2024 14:50:11 -0500 Subject: [PATCH 0490/1445] Migrate august to use yalexs 6.0.0 (#119321) --- homeassistant/components/august/__init__.py | 470 +----------------- .../components/august/binary_sensor.py | 2 +- homeassistant/components/august/const.py | 11 - homeassistant/components/august/data.py | 65 +++ homeassistant/components/august/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/august/conftest.py | 2 +- tests/components/august/mocks.py | 4 +- 9 files changed, 91 insertions(+), 469 deletions(-) create mode 100644 homeassistant/components/august/data.py diff --git a/homeassistant/components/august/__init__.py b/homeassistant/components/august/__init__.py index c21bfbc1042..cc4070c0d53 100644 --- a/homeassistant/components/august/__init__.py +++ b/homeassistant/components/august/__init__.py @@ -2,54 +2,25 @@ from __future__ import annotations -import asyncio -from collections.abc import Callable, Coroutine, Iterable, ValuesView -from datetime import datetime -from itertools import chain -import logging -from typing import Any, cast +from typing import cast -from aiohttp import ClientError, ClientResponseError +from aiohttp import ClientResponseError from path import Path -from yalexs.activity import ActivityTypes -from yalexs.const import DEFAULT_BRAND -from yalexs.doorbell import Doorbell, DoorbellDetail from yalexs.exceptions import AugustApiAIOHTTPError -from yalexs.lock import Lock, LockDetail -from yalexs.manager.activity import ActivityStream -from yalexs.manager.const import CONF_BRAND from yalexs.manager.exceptions import CannotConnect, InvalidAuth, RequireValidation from yalexs.manager.gateway import Config as YaleXSConfig -from yalexs.manager.subscriber import SubscriberMixin -from yalexs.pubnub_activity import activities_from_pubnub_message -from yalexs.pubnub_async import AugustPubNub, async_create_pubnub -from yalexs_ble import YaleXSBLEDiscovery -from homeassistant.config_entries import SOURCE_INTEGRATION_DISCOVERY, ConfigEntry +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, EVENT_HOMEASSISTANT_STOP -from homeassistant.core import Event, HomeAssistant, callback -from homeassistant.exceptions import ( - ConfigEntryAuthFailed, - ConfigEntryNotReady, - HomeAssistantError, -) -from homeassistant.helpers import device_registry as dr, discovery_flow -from homeassistant.util.async_ import create_eager_task +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady +from homeassistant.helpers import device_registry as dr -from .const import DOMAIN, MIN_TIME_BETWEEN_DETAIL_UPDATES, PLATFORMS +from .const import DOMAIN, PLATFORMS +from .data import AugustData from .gateway import AugustGateway from .util import async_create_august_clientsession -_LOGGER = logging.getLogger(__name__) - -API_CACHED_ATTRS = { - "door_state", - "door_state_datetime", - "lock_status", - "lock_status_datetime", -} -YALEXS_BLE_DOMAIN = "yalexs_ble" - type AugustConfigEntry = ConfigEntry[AugustData] @@ -73,437 +44,34 @@ async def async_unload_entry(hass: HomeAssistant, entry: AugustConfigEntry) -> b async def async_setup_august( - hass: HomeAssistant, config_entry: AugustConfigEntry, august_gateway: AugustGateway + hass: HomeAssistant, entry: AugustConfigEntry, august_gateway: AugustGateway ) -> bool: """Set up the August component.""" - config = cast(YaleXSConfig, config_entry.data) + config = cast(YaleXSConfig, entry.data) await august_gateway.async_setup(config) - if CONF_PASSWORD in config_entry.data: + if CONF_PASSWORD in entry.data: # We no longer need to store passwords since we do not # support YAML anymore - config_data = config_entry.data.copy() + config_data = entry.data.copy() del config_data[CONF_PASSWORD] - hass.config_entries.async_update_entry(config_entry, data=config_data) + hass.config_entries.async_update_entry(entry, data=config_data) await august_gateway.async_authenticate() await august_gateway.async_refresh_access_token_if_needed() - data = config_entry.runtime_data = AugustData(hass, config_entry, august_gateway) + data = entry.runtime_data = AugustData(hass, entry, august_gateway) + entry.async_on_unload( + hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, data.async_stop) + ) + entry.async_on_unload(data.async_stop) await data.async_setup() - await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True -@callback -def _async_trigger_ble_lock_discovery( - hass: HomeAssistant, locks_with_offline_keys: list[LockDetail] -) -> None: - """Update keys for the yalexs-ble integration if available.""" - for lock_detail in locks_with_offline_keys: - discovery_flow.async_create_flow( - hass, - YALEXS_BLE_DOMAIN, - context={"source": SOURCE_INTEGRATION_DISCOVERY}, - data=YaleXSBLEDiscovery( - { - "name": lock_detail.device_name, - "address": lock_detail.mac_address, - "serial": lock_detail.serial_number, - "key": lock_detail.offline_key, - "slot": lock_detail.offline_slot, - } - ), - ) - - -class AugustData(SubscriberMixin): - """August data object.""" - - def __init__( - self, - hass: HomeAssistant, - config_entry: ConfigEntry, - august_gateway: AugustGateway, - ) -> None: - """Init August data object.""" - super().__init__(MIN_TIME_BETWEEN_DETAIL_UPDATES) - self._config_entry = config_entry - self._hass = hass - self._august_gateway = august_gateway - self.activity_stream: ActivityStream = None - self._api = august_gateway.api - self._device_detail_by_id: dict[str, LockDetail | DoorbellDetail] = {} - self._doorbells_by_id: dict[str, Doorbell] = {} - self._locks_by_id: dict[str, Lock] = {} - self._house_ids: set[str] = set() - self._pubnub_unsub: Callable[[], Coroutine[Any, Any, None]] | None = None - - @property - def brand(self) -> str: - """Brand of the device.""" - return self._config_entry.data.get(CONF_BRAND, DEFAULT_BRAND) - - async def async_setup(self) -> None: - """Async setup of august device data and activities.""" - token = self._august_gateway.access_token - # This used to be a gather but it was less reliable with august's recent api changes. - user_data = await self._api.async_get_user(token) - locks: list[Lock] = await self._api.async_get_operable_locks(token) or [] - doorbells: list[Doorbell] = await self._api.async_get_doorbells(token) or [] - self._doorbells_by_id = {device.device_id: device for device in doorbells} - self._locks_by_id = {device.device_id: device for device in locks} - self._house_ids = {device.house_id for device in chain(locks, doorbells)} - - await self._async_refresh_device_detail_by_ids( - [device.device_id for device in chain(locks, doorbells)] - ) - - # We remove all devices that we are missing - # detail as we cannot determine if they are usable. - # This also allows us to avoid checking for - # detail being None all over the place - self._remove_inoperative_locks() - self._remove_inoperative_doorbells() - - pubnub = AugustPubNub() - for device in self._device_detail_by_id.values(): - pubnub.register_device(device) - - self.activity_stream = ActivityStream( - self._api, self._august_gateway, self._house_ids, pubnub - ) - self._config_entry.async_on_unload( - self._hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, self.async_stop) - ) - self._config_entry.async_on_unload(self.async_stop) - await self.activity_stream.async_setup() - - pubnub.subscribe(self.async_pubnub_message) - self._pubnub_unsub = async_create_pubnub( - user_data["UserID"], - pubnub, - self.brand, - ) - - if self._locks_by_id: - # Do not prevent setup as the sync can timeout - # but it is not a fatal error as the lock - # will recover automatically when it comes back online. - self._config_entry.async_create_background_task( - self._hass, self._async_initial_sync(), "august-initial-sync" - ) - - async def _async_initial_sync(self) -> None: - """Attempt to request an initial sync.""" - # We don't care if this fails because we only want to wake - # locks that are actually online anyways and they will be - # awake when they come back online - for result in await asyncio.gather( - *[ - create_eager_task( - self.async_status_async( - device_id, bool(detail.bridge and detail.bridge.hyper_bridge) - ) - ) - for device_id, detail in self._device_detail_by_id.items() - if device_id in self._locks_by_id - ], - return_exceptions=True, - ): - if isinstance(result, Exception) and not isinstance( - result, (TimeoutError, ClientResponseError, CannotConnect) - ): - _LOGGER.warning( - "Unexpected exception during initial sync: %s", - result, - exc_info=result, - ) - - @callback - def async_pubnub_message( - self, device_id: str, date_time: datetime, message: dict[str, Any] - ) -> None: - """Process a pubnub message.""" - device = self.get_device_detail(device_id) - activities = activities_from_pubnub_message(device, date_time, message) - activity_stream = self.activity_stream - if activities and activity_stream.async_process_newer_device_activities( - activities - ): - self.async_signal_device_id_update(device.device_id) - activity_stream.async_schedule_house_id_refresh(device.house_id) - - async def async_stop(self, event: Event | None = None) -> None: - """Stop the subscriptions.""" - if self._pubnub_unsub: - await self._pubnub_unsub() - self.activity_stream.async_stop() - - @property - def doorbells(self) -> ValuesView[Doorbell]: - """Return a list of py-august Doorbell objects.""" - return self._doorbells_by_id.values() - - @property - def locks(self) -> ValuesView[Lock]: - """Return a list of py-august Lock objects.""" - return self._locks_by_id.values() - - def get_device_detail(self, device_id: str) -> DoorbellDetail | LockDetail: - """Return the py-august LockDetail or DoorbellDetail object for a device.""" - return self._device_detail_by_id[device_id] - - async def _async_refresh(self, time: datetime) -> None: - await self._async_refresh_device_detail_by_ids(self._subscriptions.keys()) - - async def _async_refresh_device_detail_by_ids( - self, device_ids_list: Iterable[str] - ) -> None: - """Refresh each device in sequence. - - This used to be a gather but it was less reliable with august's - recent api changes. - - The august api has been timing out for some devices so - we want the ones that it isn't timing out for to keep working. - """ - for device_id in device_ids_list: - try: - await self._async_refresh_device_detail_by_id(device_id) - except TimeoutError: - _LOGGER.warning( - "Timed out calling august api during refresh of device: %s", - device_id, - ) - except (ClientResponseError, CannotConnect) as err: - _LOGGER.warning( - "Error from august api during refresh of device: %s", - device_id, - exc_info=err, - ) - - async def refresh_camera_by_id(self, device_id: str) -> None: - """Re-fetch doorbell/camera data from API.""" - await self._async_update_device_detail( - self._doorbells_by_id[device_id], - self._api.async_get_doorbell_detail, - ) - - async def _async_refresh_device_detail_by_id(self, device_id: str) -> None: - if device_id in self._locks_by_id: - if self.activity_stream and self.activity_stream.pubnub.connected: - saved_attrs = _save_live_attrs(self._device_detail_by_id[device_id]) - await self._async_update_device_detail( - self._locks_by_id[device_id], self._api.async_get_lock_detail - ) - if self.activity_stream and self.activity_stream.pubnub.connected: - _restore_live_attrs(self._device_detail_by_id[device_id], saved_attrs) - # keypads are always attached to locks - if ( - device_id in self._device_detail_by_id - and self._device_detail_by_id[device_id].keypad is not None - ): - keypad = self._device_detail_by_id[device_id].keypad - self._device_detail_by_id[keypad.device_id] = keypad - elif device_id in self._doorbells_by_id: - await self._async_update_device_detail( - self._doorbells_by_id[device_id], - self._api.async_get_doorbell_detail, - ) - _LOGGER.debug( - "async_signal_device_id_update (from detail updates): %s", device_id - ) - self.async_signal_device_id_update(device_id) - - async def _async_update_device_detail( - self, - device: Doorbell | Lock, - api_call: Callable[ - [str, str], Coroutine[Any, Any, DoorbellDetail | LockDetail] - ], - ) -> None: - device_id = device.device_id - device_name = device.device_name - _LOGGER.debug("Started retrieving detail for %s (%s)", device_name, device_id) - - try: - detail = await api_call(self._august_gateway.access_token, device_id) - except ClientError as ex: - _LOGGER.error( - "Request error trying to retrieve %s details for %s. %s", - device_id, - device_name, - ex, - ) - _LOGGER.debug("Completed retrieving detail for %s (%s)", device_name, device_id) - # If the key changes after startup we need to trigger a - # discovery to keep it up to date - if isinstance(detail, LockDetail) and detail.offline_key: - _async_trigger_ble_lock_discovery(self._hass, [detail]) - - self._device_detail_by_id[device_id] = detail - - def get_device(self, device_id: str) -> Doorbell | Lock | None: - """Get a device by id.""" - return self._locks_by_id.get(device_id) or self._doorbells_by_id.get(device_id) - - def _get_device_name(self, device_id: str) -> str | None: - """Return doorbell or lock name as August has it stored.""" - if device := self.get_device(device_id): - return device.device_name - return None - - async def async_lock(self, device_id: str) -> list[ActivityTypes]: - """Lock the device.""" - return await self._async_call_api_op_requires_bridge( - device_id, - self._api.async_lock_return_activities, - self._august_gateway.access_token, - device_id, - ) - - async def async_status_async(self, device_id: str, hyper_bridge: bool) -> str: - """Request status of the device but do not wait for a response since it will come via pubnub.""" - return await self._async_call_api_op_requires_bridge( - device_id, - self._api.async_status_async, - self._august_gateway.access_token, - device_id, - hyper_bridge, - ) - - async def async_lock_async(self, device_id: str, hyper_bridge: bool) -> str: - """Lock the device but do not wait for a response since it will come via pubnub.""" - return await self._async_call_api_op_requires_bridge( - device_id, - self._api.async_lock_async, - self._august_gateway.access_token, - device_id, - hyper_bridge, - ) - - async def async_unlatch(self, device_id: str) -> list[ActivityTypes]: - """Open/unlatch the device.""" - return await self._async_call_api_op_requires_bridge( - device_id, - self._api.async_unlatch_return_activities, - self._august_gateway.access_token, - device_id, - ) - - async def async_unlatch_async(self, device_id: str, hyper_bridge: bool) -> str: - """Open/unlatch the device but do not wait for a response since it will come via pubnub.""" - return await self._async_call_api_op_requires_bridge( - device_id, - self._api.async_unlatch_async, - self._august_gateway.access_token, - device_id, - hyper_bridge, - ) - - async def async_unlock(self, device_id: str) -> list[ActivityTypes]: - """Unlock the device.""" - return await self._async_call_api_op_requires_bridge( - device_id, - self._api.async_unlock_return_activities, - self._august_gateway.access_token, - device_id, - ) - - async def async_unlock_async(self, device_id: str, hyper_bridge: bool) -> str: - """Unlock the device but do not wait for a response since it will come via pubnub.""" - return await self._async_call_api_op_requires_bridge( - device_id, - self._api.async_unlock_async, - self._august_gateway.access_token, - device_id, - hyper_bridge, - ) - - async def _async_call_api_op_requires_bridge[**_P, _R]( - self, - device_id: str, - func: Callable[_P, Coroutine[Any, Any, _R]], - *args: _P.args, - **kwargs: _P.kwargs, - ) -> _R: - """Call an API that requires the bridge to be online and will change the device state.""" - try: - ret = await func(*args, **kwargs) - except AugustApiAIOHTTPError as err: - device_name = self._get_device_name(device_id) - if device_name is None: - device_name = f"DeviceID: {device_id}" - raise HomeAssistantError(f"{device_name}: {err}") from err - - return ret - - def _remove_inoperative_doorbells(self) -> None: - for doorbell in list(self.doorbells): - device_id = doorbell.device_id - if self._device_detail_by_id.get(device_id): - continue - _LOGGER.info( - ( - "The doorbell %s could not be setup because the system could not" - " fetch details about the doorbell" - ), - doorbell.device_name, - ) - del self._doorbells_by_id[device_id] - - def _remove_inoperative_locks(self) -> None: - # Remove non-operative locks as there must - # be a bridge (August Connect) for them to - # be usable - for lock in list(self.locks): - device_id = lock.device_id - lock_detail = self._device_detail_by_id.get(device_id) - if lock_detail is None: - _LOGGER.info( - ( - "The lock %s could not be setup because the system could not" - " fetch details about the lock" - ), - lock.device_name, - ) - elif lock_detail.bridge is None: - _LOGGER.info( - ( - "The lock %s could not be setup because it does not have a" - " bridge (Connect)" - ), - lock.device_name, - ) - del self._device_detail_by_id[device_id] - # Bridge may come back online later so we still add the device since we will - # have a pubnub subscription to tell use when it recovers - else: - continue - del self._locks_by_id[device_id] - - -def _save_live_attrs(lock_detail: DoorbellDetail | LockDetail) -> dict[str, Any]: - """Store the attributes that the lock detail api may have an invalid cache for. - - Since we are connected to pubnub we may have more current data - then the api so we want to restore the most current data after - updating battery state etc. - """ - return {attr: getattr(lock_detail, attr) for attr in API_CACHED_ATTRS} - - -def _restore_live_attrs( - lock_detail: DoorbellDetail | LockDetail, attrs: dict[str, Any] -) -> None: - """Restore the non-cache attributes after a cached update.""" - for attr, value in attrs.items(): - setattr(lock_detail, attr, value) - - async def async_remove_config_entry_device( hass: HomeAssistant, config_entry: AugustConfigEntry, device_entry: dr.DeviceEntry ) -> bool: diff --git a/homeassistant/components/august/binary_sensor.py b/homeassistant/components/august/binary_sensor.py index baf78bbd445..8671032f32d 100644 --- a/homeassistant/components/august/binary_sensor.py +++ b/homeassistant/components/august/binary_sensor.py @@ -15,6 +15,7 @@ from yalexs.activity import ( ) from yalexs.doorbell import Doorbell, DoorbellDetail from yalexs.lock import Lock, LockDetail, LockDoorStatus +from yalexs.manager.const import ACTIVITY_UPDATE_INTERVAL from yalexs.util import update_lock_detail_from_activity from homeassistant.components.binary_sensor import ( @@ -28,7 +29,6 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_call_later from . import AugustConfigEntry, AugustData -from .const import ACTIVITY_UPDATE_INTERVAL from .entity import AugustEntityMixin _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/august/const.py b/homeassistant/components/august/const.py index 6aa033c62b2..7d7ff1854ed 100644 --- a/homeassistant/components/august/const.py +++ b/homeassistant/components/august/const.py @@ -1,7 +1,5 @@ """Constants for August devices.""" -from datetime import timedelta - from homeassistant.const import Platform DEFAULT_TIMEOUT = 25 @@ -37,15 +35,6 @@ ATTR_OPERATION_KEYPAD = "keypad" ATTR_OPERATION_MANUAL = "manual" ATTR_OPERATION_TAG = "tag" -# Limit battery, online, and hardware updates to hourly -# in order to reduce the number of api requests and -# avoid hitting rate limits -MIN_TIME_BETWEEN_DETAIL_UPDATES = timedelta(hours=24) - -# Activity needs to be checked more frequently as the -# doorbell motion and rings are included here -ACTIVITY_UPDATE_INTERVAL = timedelta(seconds=10) - LOGIN_METHODS = ["phone", "email"] DEFAULT_LOGIN_METHOD = "email" diff --git a/homeassistant/components/august/data.py b/homeassistant/components/august/data.py new file mode 100644 index 00000000000..59c37dfd2b1 --- /dev/null +++ b/homeassistant/components/august/data.py @@ -0,0 +1,65 @@ +"""Support for August devices.""" + +from __future__ import annotations + +from yalexs.const import DEFAULT_BRAND +from yalexs.lock import LockDetail +from yalexs.manager.const import CONF_BRAND +from yalexs.manager.data import YaleXSData +from yalexs_ble import YaleXSBLEDiscovery + +from homeassistant.config_entries import SOURCE_INTEGRATION_DISCOVERY, ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import discovery_flow + +from .gateway import AugustGateway + +YALEXS_BLE_DOMAIN = "yalexs_ble" + + +@callback +def _async_trigger_ble_lock_discovery( + hass: HomeAssistant, locks_with_offline_keys: list[LockDetail] +) -> None: + """Update keys for the yalexs-ble integration if available.""" + for lock_detail in locks_with_offline_keys: + discovery_flow.async_create_flow( + hass, + YALEXS_BLE_DOMAIN, + context={"source": SOURCE_INTEGRATION_DISCOVERY}, + data=YaleXSBLEDiscovery( + { + "name": lock_detail.device_name, + "address": lock_detail.mac_address, + "serial": lock_detail.serial_number, + "key": lock_detail.offline_key, + "slot": lock_detail.offline_slot, + } + ), + ) + + +class AugustData(YaleXSData): + """August data object.""" + + def __init__( + self, + hass: HomeAssistant, + config_entry: ConfigEntry, + august_gateway: AugustGateway, + ) -> None: + """Init August data object.""" + self._hass = hass + self._config_entry = config_entry + super().__init__(august_gateway, HomeAssistantError) + + @property + def brand(self) -> str: + """Brand of the device.""" + return self._config_entry.data.get(CONF_BRAND, DEFAULT_BRAND) + + @callback + def async_offline_key_discovered(self, detail: LockDetail) -> None: + """Handle offline key discovery.""" + _async_trigger_ble_lock_discovery(self._hass, [detail]) diff --git a/homeassistant/components/august/manifest.json b/homeassistant/components/august/manifest.json index 179e85de7f0..d4bad52c339 100644 --- a/homeassistant/components/august/manifest.json +++ b/homeassistant/components/august/manifest.json @@ -28,5 +28,5 @@ "documentation": "https://www.home-assistant.io/integrations/august", "iot_class": "cloud_push", "loggers": ["pubnub", "yalexs"], - "requirements": ["yalexs==5.2.0", "yalexs-ble==2.4.2"] + "requirements": ["yalexs==6.0.0", "yalexs-ble==2.4.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 94e824342b3..ae801a82aae 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2933,7 +2933,7 @@ yalesmartalarmclient==0.3.9 yalexs-ble==2.4.2 # homeassistant.components.august -yalexs==5.2.0 +yalexs==6.0.0 # homeassistant.components.yeelight yeelight==0.7.14 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 005cd2ae77c..ffc20577cfa 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2292,7 +2292,7 @@ yalesmartalarmclient==0.3.9 yalexs-ble==2.4.2 # homeassistant.components.august -yalexs==5.2.0 +yalexs==6.0.0 # homeassistant.components.yeelight yeelight==0.7.14 diff --git a/tests/components/august/conftest.py b/tests/components/august/conftest.py index 8640ffeecd4..052cde7d2a2 100644 --- a/tests/components/august/conftest.py +++ b/tests/components/august/conftest.py @@ -9,6 +9,6 @@ import pytest def mock_discovery_fixture(): """Mock discovery to avoid loading the whole bluetooth stack.""" with patch( - "homeassistant.components.august.discovery_flow.async_create_flow" + "homeassistant.components.august.data.discovery_flow.async_create_flow" ) as mock_discovery: yield mock_discovery diff --git a/tests/components/august/mocks.py b/tests/components/august/mocks.py index b8d394fa067..2b9b401e107 100644 --- a/tests/components/august/mocks.py +++ b/tests/components/august/mocks.py @@ -78,10 +78,10 @@ async def _mock_setup_august( entry.add_to_hass(hass) with ( patch( - "homeassistant.components.august.async_create_pubnub", + "yalexs.manager.data.async_create_pubnub", return_value=AsyncMock(), ), - patch("homeassistant.components.august.AugustPubNub", return_value=pubnub_mock), + patch("yalexs.manager.data.AugustPubNub", return_value=pubnub_mock), ): assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() From 68a9f3a0487a00f7273947056d13e075cb5b6d48 Mon Sep 17 00:00:00 2001 From: swcloudgenie <45437888+swcloudgenie@users.noreply.github.com> Date: Mon, 10 Jun 2024 16:40:24 -0500 Subject: [PATCH 0491/1445] Fix AladdinConnect OAuth domain (#119336) fix aladdin connect oauth domain --- homeassistant/components/aladdin_connect/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/aladdin_connect/const.py b/homeassistant/components/aladdin_connect/const.py index 0fe60724154..a87147c8f09 100644 --- a/homeassistant/components/aladdin_connect/const.py +++ b/homeassistant/components/aladdin_connect/const.py @@ -2,5 +2,5 @@ DOMAIN = "aladdin_connect" -OAUTH2_AUTHORIZE = "https://app.aladdinconnect.com/login.html" +OAUTH2_AUTHORIZE = "https://app.aladdinconnect.net/login.html" OAUTH2_TOKEN = "https://twdvzuefzh.execute-api.us-east-2.amazonaws.com/v1/oauth2/token" From ea6729ab5fd1cb08f7cb92d609f1f92fa889b991 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Mon, 10 Jun 2024 23:43:30 +0200 Subject: [PATCH 0492/1445] Fix enigma2 option flow (#119335) --- homeassistant/components/enigma2/config_flow.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/enigma2/config_flow.py b/homeassistant/components/enigma2/config_flow.py index b9ae6ffbebf..0d640d0a478 100644 --- a/homeassistant/components/enigma2/config_flow.py +++ b/homeassistant/components/enigma2/config_flow.py @@ -67,9 +67,8 @@ CONFIG_SCHEMA = vol.Schema( async def get_options_schema(handler: SchemaCommonFlowHandler) -> vol.Schema: """Get the options schema.""" - hass = handler.parent_handler.hass entry = cast(SchemaOptionsFlowHandler, handler.parent_handler).config_entry - device: OpenWebIfDevice = hass.data[DOMAIN][entry.entry_id] + device: OpenWebIfDevice = entry.runtime_data bouquets = [b[1] for b in (await device.get_all_bouquets())["bouquets"]] return vol.Schema( From 0149698002898f1d76aed672f18dc88ac4274019 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 10 Jun 2024 20:03:23 -0500 Subject: [PATCH 0493/1445] Bump uiprotect to 0.10.1 (#119327) Co-authored-by: Jan Bouwhuis --- homeassistant/components/unifiprotect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index 00a96483f70..dd04332daa7 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -40,7 +40,7 @@ "integration_type": "hub", "iot_class": "local_push", "loggers": ["uiprotect", "unifi_discovery"], - "requirements": ["uiprotect==0.4.1", "unifi-discovery==1.1.8"], + "requirements": ["uiprotect==0.10.1", "unifi-discovery==1.1.8"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/requirements_all.txt b/requirements_all.txt index ae801a82aae..95e1cb4b4a4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2785,7 +2785,7 @@ twitchAPI==4.0.0 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==0.4.1 +uiprotect==0.10.1 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ffc20577cfa..ca784a18bef 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2165,7 +2165,7 @@ twitchAPI==4.0.0 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==0.4.1 +uiprotect==0.10.1 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 From 3308f07d4b459cfeb9e64607a47ccafe08c14af4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 10 Jun 2024 20:22:59 -0500 Subject: [PATCH 0494/1445] Speed up generating large stats results (#119210) * Speed up generating large stats results * naming * fix type * fix type * tweak * tweak * delete unused code --- .../components/recorder/statistics.py | 191 +++++++++++------- 1 file changed, 116 insertions(+), 75 deletions(-) diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index 691fc58c609..8b434fcdf3a 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -241,7 +241,8 @@ def _get_statistic_to_display_unit_converter( statistic_unit: str | None, state_unit: str | None, requested_units: dict[str, str] | None, -) -> Callable[[float | None], float | None] | None: + allow_none: bool = True, +) -> Callable[[float | None], float | None] | Callable[[float], float] | None: """Prepare a converter from the statistics unit to display unit.""" if (converter := STATISTIC_UNIT_TO_UNIT_CONVERTER.get(statistic_unit)) is None: return None @@ -260,9 +261,11 @@ def _get_statistic_to_display_unit_converter( if display_unit == statistic_unit: return None - return converter.converter_factory_allow_none( - from_unit=statistic_unit, to_unit=display_unit - ) + if allow_none: + return converter.converter_factory_allow_none( + from_unit=statistic_unit, to_unit=display_unit + ) + return converter.converter_factory(from_unit=statistic_unit, to_unit=display_unit) def _get_display_to_statistic_unit_converter( @@ -1760,13 +1763,11 @@ def _statistics_during_period_with_session( result = _sorted_statistics_to_dict( hass, - session, stats, statistic_ids, metadata, True, table, - start_time, units, types, ) @@ -1878,14 +1879,12 @@ def _get_last_statistics( # Return statistics combined with metadata return _sorted_statistics_to_dict( hass, - session, stats, statistic_ids, metadata, convert_units, table, None, - None, types, ) @@ -1993,14 +1992,12 @@ def get_latest_short_term_statistics_with_session( # Return statistics combined with metadata return _sorted_statistics_to_dict( hass, - session, stats, statistic_ids, metadata, False, StatisticsShortTerm, None, - None, types, ) @@ -2047,42 +2044,119 @@ def _statistics_at_time( return cast(Sequence[Row], execute_stmt_lambda_element(session, stmt)) -def _fast_build_sum_list( - stats_list: list[Row], +def _build_sum_converted_stats( + db_rows: list[Row], + table_duration_seconds: float, + start_ts_idx: int, + sum_idx: int, + convert: Callable[[float | None], float | None] | Callable[[float], float], +) -> list[StatisticsRow]: + """Build a list of sum statistics.""" + return [ + { + "start": (start_ts := db_row[start_ts_idx]), + "end": start_ts + table_duration_seconds, + "sum": None if (v := db_row[sum_idx]) is None else convert(v), + } + for db_row in db_rows + ] + + +def _build_sum_stats( + db_rows: list[Row], table_duration_seconds: float, - convert: Callable | None, start_ts_idx: int, sum_idx: int, ) -> list[StatisticsRow]: """Build a list of sum statistics.""" - if convert: - return [ - { - "start": (start_ts := db_state[start_ts_idx]), - "end": start_ts + table_duration_seconds, - "sum": convert(db_state[sum_idx]), - } - for db_state in stats_list - ] return [ { - "start": (start_ts := db_state[start_ts_idx]), + "start": (start_ts := db_row[start_ts_idx]), "end": start_ts + table_duration_seconds, - "sum": db_state[sum_idx], + "sum": db_row[sum_idx], } - for db_state in stats_list + for db_row in db_rows ] -def _sorted_statistics_to_dict( # noqa: C901 +def _build_stats( + db_rows: list[Row], + table_duration_seconds: float, + start_ts_idx: int, + mean_idx: int | None, + min_idx: int | None, + max_idx: int | None, + last_reset_ts_idx: int | None, + state_idx: int | None, + sum_idx: int | None, +) -> list[StatisticsRow]: + """Build a list of statistics without unit conversion.""" + result: list[StatisticsRow] = [] + ent_results_append = result.append + for db_row in db_rows: + row: StatisticsRow = { + "start": (start_ts := db_row[start_ts_idx]), + "end": start_ts + table_duration_seconds, + } + if last_reset_ts_idx is not None: + row["last_reset"] = db_row[last_reset_ts_idx] + if mean_idx is not None: + row["mean"] = db_row[mean_idx] + if min_idx is not None: + row["min"] = db_row[min_idx] + if max_idx is not None: + row["max"] = db_row[max_idx] + if state_idx is not None: + row["state"] = db_row[state_idx] + if sum_idx is not None: + row["sum"] = db_row[sum_idx] + ent_results_append(row) + return result + + +def _build_converted_stats( + db_rows: list[Row], + table_duration_seconds: float, + start_ts_idx: int, + mean_idx: int | None, + min_idx: int | None, + max_idx: int | None, + last_reset_ts_idx: int | None, + state_idx: int | None, + sum_idx: int | None, + convert: Callable[[float | None], float | None] | Callable[[float], float], +) -> list[StatisticsRow]: + """Build a list of statistics with unit conversion.""" + result: list[StatisticsRow] = [] + ent_results_append = result.append + for db_row in db_rows: + row: StatisticsRow = { + "start": (start_ts := db_row[start_ts_idx]), + "end": start_ts + table_duration_seconds, + } + if last_reset_ts_idx is not None: + row["last_reset"] = db_row[last_reset_ts_idx] + if mean_idx is not None: + row["mean"] = None if (v := db_row[mean_idx]) is None else convert(v) + if min_idx is not None: + row["min"] = None if (v := db_row[min_idx]) is None else convert(v) + if max_idx is not None: + row["max"] = None if (v := db_row[max_idx]) is None else convert(v) + if state_idx is not None: + row["state"] = None if (v := db_row[state_idx]) is None else convert(v) + if sum_idx is not None: + row["sum"] = None if (v := db_row[sum_idx]) is None else convert(v) + ent_results_append(row) + return result + + +def _sorted_statistics_to_dict( hass: HomeAssistant, - session: Session, stats: Sequence[Row[Any]], statistic_ids: set[str] | None, _metadata: dict[str, tuple[int, StatisticMetaData]], convert_units: bool, table: type[StatisticsBase], - start_time: datetime | None, units: dict[str, str] | None, types: set[Literal["last_reset", "max", "mean", "min", "state", "sum"]], ) -> dict[str, list[StatisticsRow]]: @@ -2120,19 +2194,23 @@ def _sorted_statistics_to_dict( # noqa: C901 state_idx = field_map["state"] if "state" in types else None sum_idx = field_map["sum"] if "sum" in types else None sum_only = len(types) == 1 and sum_idx is not None + row_idxes = (mean_idx, min_idx, max_idx, last_reset_ts_idx, state_idx, sum_idx) # Append all statistic entries, and optionally do unit conversion table_duration_seconds = table.duration.total_seconds() - for meta_id, stats_list in stats_by_meta_id.items(): + for meta_id, db_rows in stats_by_meta_id.items(): metadata_by_id = metadata[meta_id] statistic_id = metadata_by_id["statistic_id"] if convert_units: state_unit = unit = metadata_by_id["unit_of_measurement"] if state := hass.states.get(statistic_id): state_unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - convert = _get_statistic_to_display_unit_converter(unit, state_unit, units) + convert = _get_statistic_to_display_unit_converter( + unit, state_unit, units, allow_none=False + ) else: convert = None + build_args = (db_rows, table_duration_seconds, start_ts_idx) if sum_only: # This function is extremely flexible and can handle all types of # statistics, but in practice we only ever use a few combinations. @@ -2140,53 +2218,16 @@ def _sorted_statistics_to_dict( # noqa: C901 # For energy, we only need sum statistics, so we can optimize # this path to avoid the overhead of the more generic function. assert sum_idx is not None - result[statistic_id] = _fast_build_sum_list( - stats_list, - table_duration_seconds, - convert, - start_ts_idx, - sum_idx, - ) - continue - - ent_results_append = result[statistic_id].append - # - # The below loop is a red hot path for energy, and every - # optimization counts in here. - # - # Specifically, we want to avoid function calls, - # attribute lookups, and dict lookups as much as possible. - # - for db_state in stats_list: - row: StatisticsRow = { - "start": (start_ts := db_state[start_ts_idx]), - "end": start_ts + table_duration_seconds, - } - if last_reset_ts_idx is not None: - row["last_reset"] = db_state[last_reset_ts_idx] if convert: - if mean_idx is not None: - row["mean"] = convert(db_state[mean_idx]) - if min_idx is not None: - row["min"] = convert(db_state[min_idx]) - if max_idx is not None: - row["max"] = convert(db_state[max_idx]) - if state_idx is not None: - row["state"] = convert(db_state[state_idx]) - if sum_idx is not None: - row["sum"] = convert(db_state[sum_idx]) + _stats = _build_sum_converted_stats(*build_args, sum_idx, convert) else: - if mean_idx is not None: - row["mean"] = db_state[mean_idx] - if min_idx is not None: - row["min"] = db_state[min_idx] - if max_idx is not None: - row["max"] = db_state[max_idx] - if state_idx is not None: - row["state"] = db_state[state_idx] - if sum_idx is not None: - row["sum"] = db_state[sum_idx] - ent_results_append(row) + _stats = _build_sum_stats(*build_args, sum_idx) + elif convert: + _stats = _build_converted_stats(*build_args, *row_idxes, convert) + else: + _stats = _build_stats(*build_args, *row_idxes) + + result[statistic_id] = _stats return result From 9bb9792607c5ec6cae1550cfa2f6f10ec8ceb427 Mon Sep 17 00:00:00 2001 From: Jake Martin Date: Tue, 11 Jun 2024 03:11:07 +0100 Subject: [PATCH 0495/1445] Move runtime_data deletion after unload (#119224) * Move runtime_data deletion after unload. Doing this before unload means we can't use, eg. the coordinator, during teardown. * Re-order config entry on unload * Add test --------- Co-authored-by: Paulus Schoutsen --- homeassistant/config_entries.py | 12 ++++++------ tests/test_config_entries.py | 15 +++++++++++---- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index eac7f5f25ab..1ca6e99f262 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -803,13 +803,13 @@ class ConfigEntry(Generic[_DataT]): assert isinstance(result, bool) # Only adjust state if we unloaded the component - if domain_is_integration: - if result: - self._async_set_state(hass, ConfigEntryState.NOT_LOADED, None) - if hasattr(self, "runtime_data"): - object.__delattr__(self, "runtime_data") - + if domain_is_integration and result: await self._async_process_on_unload(hass) + if hasattr(self, "runtime_data"): + object.__delattr__(self, "runtime_data") + + self._async_set_state(hass, ConfigEntryState.NOT_LOADED, None) + except Exception as exc: _LOGGER.exception( "Error unloading entry %s for %s", self.title, integration.domain diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 010d322775e..5c2bf8b205b 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -1726,16 +1726,23 @@ async def test_entry_unload_succeed( hass: HomeAssistant, manager: config_entries.ConfigEntries ) -> None: """Test that we can unload an entry.""" + unloads_called = [] + + async def verify_runtime_data(*args): + """Verify runtime data.""" + assert entry.runtime_data == 2 + unloads_called.append(args) + return True + entry = MockConfigEntry(domain="comp", state=config_entries.ConfigEntryState.LOADED) entry.add_to_hass(hass) + entry.async_on_unload(verify_runtime_data) entry.runtime_data = 2 - async_unload_entry = AsyncMock(return_value=True) - - mock_integration(hass, MockModule("comp", async_unload_entry=async_unload_entry)) + mock_integration(hass, MockModule("comp", async_unload_entry=verify_runtime_data)) assert await manager.async_unload(entry.entry_id) - assert len(async_unload_entry.mock_calls) == 1 + assert len(unloads_called) == 2 assert entry.state is config_entries.ConfigEntryState.NOT_LOADED assert not hasattr(entry, "runtime_data") From f02383e10db401a73927de5176ee436af8d533b6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 10 Jun 2024 22:50:44 -0500 Subject: [PATCH 0496/1445] Bump uiprotect to 0.13.0 (#119344) --- homeassistant/components/unifiprotect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index dd04332daa7..8bbd3738222 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -40,7 +40,7 @@ "integration_type": "hub", "iot_class": "local_push", "loggers": ["uiprotect", "unifi_discovery"], - "requirements": ["uiprotect==0.10.1", "unifi-discovery==1.1.8"], + "requirements": ["uiprotect==0.13.0", "unifi-discovery==1.1.8"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/requirements_all.txt b/requirements_all.txt index 95e1cb4b4a4..131f7e56a61 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2785,7 +2785,7 @@ twitchAPI==4.0.0 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==0.10.1 +uiprotect==0.13.0 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ca784a18bef..12d7754b372 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2165,7 +2165,7 @@ twitchAPI==4.0.0 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==0.10.1 +uiprotect==0.13.0 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 From 958a4562759c04f47744c6e29699e997af5e3d8f Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Tue, 11 Jun 2024 06:41:29 +0200 Subject: [PATCH 0497/1445] Allow source sensor to be changed in threshold helper (#119157) * Allow source sensor to be changed in threshold helper * Make sure old device link is removed on entry change * Add test case for changed association --- .../components/threshold/__init__.py | 12 ++++ .../components/threshold/config_flow.py | 6 +- .../components/threshold/test_config_flow.py | 1 + tests/components/threshold/test_init.py | 64 ++++++++++++++++++- 4 files changed, 79 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/threshold/__init__.py b/homeassistant/components/threshold/__init__.py index 2ca1410a890..fb9e7145951 100644 --- a/homeassistant/components/threshold/__init__.py +++ b/homeassistant/components/threshold/__init__.py @@ -3,6 +3,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: @@ -18,6 +19,17 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def config_entry_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: """Update listener, called when the config entry options are changed.""" + + # Remove device link for entry, the source device may have changed. + # The link will be recreated after load. + device_registry = dr.async_get(hass) + devices = device_registry.devices.get_devices_for_config_entry_id(entry.entry_id) + + for device in devices: + device_registry.async_update_device( + device.id, remove_config_entry_id=entry.entry_id + ) + await hass.config_entries.async_reload(entry.entry_id) diff --git a/homeassistant/components/threshold/config_flow.py b/homeassistant/components/threshold/config_flow.py index a8e330cab38..08a4a18fca7 100644 --- a/homeassistant/components/threshold/config_flow.py +++ b/homeassistant/components/threshold/config_flow.py @@ -48,15 +48,15 @@ OPTIONS_SCHEMA = vol.Schema( mode=selector.NumberSelectorMode.BOX, step="any" ), ), + vol.Required(CONF_ENTITY_ID): selector.EntitySelector( + selector.EntitySelectorConfig(domain=SENSOR_DOMAIN) + ), } ) CONFIG_SCHEMA = vol.Schema( { vol.Required(CONF_NAME): selector.TextSelector(), - vol.Required(CONF_ENTITY_ID): selector.EntitySelector( - selector.EntitySelectorConfig(domain=SENSOR_DOMAIN) - ), } ).extend(OPTIONS_SCHEMA.schema) diff --git a/tests/components/threshold/test_config_flow.py b/tests/components/threshold/test_config_flow.py index 88c970d5c2c..ddf870b7a0a 100644 --- a/tests/components/threshold/test_config_flow.py +++ b/tests/components/threshold/test_config_flow.py @@ -129,6 +129,7 @@ async def test_options(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={ + "entity_id": input_sensor, "hysteresis": 0.0, "upper": 20.0, }, diff --git a/tests/components/threshold/test_init.py b/tests/components/threshold/test_init.py index 02726d5a121..d1fda706911 100644 --- a/tests/components/threshold/test_init.py +++ b/tests/components/threshold/test_init.py @@ -4,7 +4,7 @@ import pytest from homeassistant.components.threshold.const import DOMAIN from homeassistant.core import HomeAssistant -from homeassistant.helpers import entity_registry as er +from homeassistant.helpers import device_registry as dr, entity_registry as er from tests.common import MockConfigEntry @@ -44,6 +44,7 @@ async def test_setup_and_remove_config_entry( # Check the platform is setup correctly state = hass.states.get(threshold_entity_id) + assert state assert state.state == "on" assert state.attributes["entity_id"] == input_sensor assert state.attributes["hysteresis"] == 0.0 @@ -60,3 +61,64 @@ async def test_setup_and_remove_config_entry( # Check the state and entity registry entry are removed assert hass.states.get(threshold_entity_id) is None assert entity_registry.async_get(threshold_entity_id) is None + + +@pytest.mark.parametrize("platform", ["sensor"]) +async def test_entry_changed(hass: HomeAssistant, platform) -> None: + """Test reconfiguring.""" + + device_registry = dr.async_get(hass) + entity_registry = er.async_get(hass) + + def _create_mock_entity(domain: str, name: str) -> er.RegistryEntry: + config_entry = MockConfigEntry( + data={}, + domain="test", + title=f"{name}", + ) + config_entry.add_to_hass(hass) + device_entry = device_registry.async_get_or_create( + identifiers={("test", name)}, config_entry_id=config_entry.entry_id + ) + return entity_registry.async_get_or_create( + domain, "test", name, suggested_object_id=name, device_id=device_entry.id + ) + + def _get_device_config_entries(entry: er.RegistryEntry) -> set[str]: + assert entry.device_id + device = device_registry.async_get(entry.device_id) + assert device + return device.config_entries + + # Set up entities, with backing devices and config entries + run1_entry = _create_mock_entity("sensor", "initial") + run2_entry = _create_mock_entity("sensor", "changed") + + # Setup the config entry + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={ + "entity_id": "sensor.initial", + "hysteresis": 0.0, + "lower": -2.0, + "name": "My threshold", + "upper": None, + }, + title="My integration", + ) + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.entry_id in _get_device_config_entries(run1_entry) + assert config_entry.entry_id not in _get_device_config_entries(run2_entry) + + hass.config_entries.async_update_entry( + config_entry, options={**config_entry.options, "entity_id": "sensor.changed"} + ) + await hass.async_block_till_done() + + # Check that the config entry association has updated + assert config_entry.entry_id not in _get_device_config_entries(run1_entry) + assert config_entry.entry_id in _get_device_config_entries(run2_entry) From dd6cfdf731350bb993f430b9d5e9c44ec9b5dadd Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Tue, 11 Jun 2024 06:55:05 +0200 Subject: [PATCH 0498/1445] Bump incomfort backend client to v0.6.2 (#119330) --- homeassistant/components/incomfort/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/incomfort/manifest.json b/homeassistant/components/incomfort/manifest.json index 99567de0b36..c0b536dabe5 100644 --- a/homeassistant/components/incomfort/manifest.json +++ b/homeassistant/components/incomfort/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/incomfort", "iot_class": "local_polling", "loggers": ["incomfortclient"], - "requirements": ["incomfort-client==0.6.1"] + "requirements": ["incomfort-client==0.6.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 131f7e56a61..1009692a2b9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1152,7 +1152,7 @@ ihcsdk==2.8.5 imgw_pib==1.0.4 # homeassistant.components.incomfort -incomfort-client==0.6.1 +incomfort-client==0.6.2 # homeassistant.components.influxdb influxdb-client==1.24.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 12d7754b372..e3781a5ece7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -939,7 +939,7 @@ ifaddr==0.2.0 imgw_pib==1.0.4 # homeassistant.components.incomfort -incomfort-client==0.6.1 +incomfort-client==0.6.2 # homeassistant.components.influxdb influxdb-client==1.24.0 From cceb0d8b4762cdad3bc76d1385dd55663366e372 Mon Sep 17 00:00:00 2001 From: middlingphys <38708390+middlingphys@users.noreply.github.com> Date: Tue, 11 Jun 2024 13:56:31 +0900 Subject: [PATCH 0499/1445] Fix typo in Ecovacs integration (#119346) --- homeassistant/components/ecovacs/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/ecovacs/strings.json b/homeassistant/components/ecovacs/strings.json index 25fd9b1b978..68218e63d4e 100644 --- a/homeassistant/components/ecovacs/strings.json +++ b/homeassistant/components/ecovacs/strings.json @@ -228,7 +228,7 @@ "message": "Params are required for the command: {command}" }, "vacuum_raw_get_positions_not_supported": { - "message": "Getting the positions of the charges and the device itself is not supported" + "message": "Getting the positions of the chargers and the device itself is not supported" } }, "issues": { From 35347929ca7fa1b601b49bd237a22a3259059fc4 Mon Sep 17 00:00:00 2001 From: Ruben Bokobza Date: Tue, 11 Jun 2024 08:04:25 +0300 Subject: [PATCH 0500/1445] Bump pyElectra to 1.2.1 (#118958) --- .strict-typing | 1 - homeassistant/components/electrasmart/manifest.json | 2 +- mypy.ini | 10 ---------- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- script/hassfest/requirements.py | 1 - 6 files changed, 3 insertions(+), 15 deletions(-) diff --git a/.strict-typing b/.strict-typing index 313dda48649..86fbf3c3563 100644 --- a/.strict-typing +++ b/.strict-typing @@ -163,7 +163,6 @@ homeassistant.components.easyenergy.* homeassistant.components.ecovacs.* homeassistant.components.ecowitt.* homeassistant.components.efergy.* -homeassistant.components.electrasmart.* homeassistant.components.electric_kiwi.* homeassistant.components.elgato.* homeassistant.components.elkm1.* diff --git a/homeassistant/components/electrasmart/manifest.json b/homeassistant/components/electrasmart/manifest.json index 405d9ee688a..e00b818e2a6 100644 --- a/homeassistant/components/electrasmart/manifest.json +++ b/homeassistant/components/electrasmart/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/electrasmart", "iot_class": "cloud_polling", - "requirements": ["pyElectra==1.2.0"] + "requirements": ["pyElectra==1.2.1"] } diff --git a/mypy.ini b/mypy.ini index 4e4d9cc624b..ac3945872a1 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1393,16 +1393,6 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true -[mypy-homeassistant.components.electrasmart.*] -check_untyped_defs = true -disallow_incomplete_defs = true -disallow_subclassing_any = true -disallow_untyped_calls = true -disallow_untyped_decorators = true -disallow_untyped_defs = true -warn_return_any = true -warn_unreachable = true - [mypy-homeassistant.components.electric_kiwi.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/requirements_all.txt b/requirements_all.txt index 1009692a2b9..89d6e0fbd23 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1673,7 +1673,7 @@ pyControl4==1.1.0 pyDuotecno==2024.5.1 # homeassistant.components.electrasmart -pyElectra==1.2.0 +pyElectra==1.2.1 # homeassistant.components.emby pyEmby==1.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e3781a5ece7..253b1a71284 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1332,7 +1332,7 @@ pyControl4==1.1.0 pyDuotecno==2024.5.1 # homeassistant.components.electrasmart -pyElectra==1.2.0 +pyElectra==1.2.1 # homeassistant.components.rfxtrx pyRFXtrx==0.31.1 diff --git a/script/hassfest/requirements.py b/script/hassfest/requirements.py index f9a8ec2db92..d35d96121c5 100644 --- a/script/hassfest/requirements.py +++ b/script/hassfest/requirements.py @@ -30,7 +30,6 @@ PIP_VERSION_RANGE_SEPARATOR = re.compile(r"^(==|>=|<=|~=|!=|<|>|===)?(.*)$") IGNORE_STANDARD_LIBRARY_VIOLATIONS = { # Integrations which have standard library requirements. - "electrasmart", "slide", "suez_water", } From 013c1175707c48f016f1e7886dae58ada3199755 Mon Sep 17 00:00:00 2001 From: Ishima Date: Tue, 11 Jun 2024 07:06:25 +0200 Subject: [PATCH 0501/1445] Add Xiaomi Air Purifier Pro H EU (zhimi.airpurifier.vb2) (#119149) --- homeassistant/components/xiaomi_miio/const.py | 2 ++ homeassistant/components/xiaomi_miio/select.py | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/homeassistant/components/xiaomi_miio/const.py b/homeassistant/components/xiaomi_miio/const.py index d643602531d..24b494f3d08 100644 --- a/homeassistant/components/xiaomi_miio/const.py +++ b/homeassistant/components/xiaomi_miio/const.py @@ -67,6 +67,7 @@ MODEL_AIRPURIFIER_MA1 = "zhimi.airpurifier.ma1" MODEL_AIRPURIFIER_MA2 = "zhimi.airpurifier.ma2" MODEL_AIRPURIFIER_PRO = "zhimi.airpurifier.v6" MODEL_AIRPURIFIER_PROH = "zhimi.airpurifier.va1" +MODEL_AIRPURIFIER_PROH_EU = "zhimi.airpurifier.vb2" MODEL_AIRPURIFIER_PRO_V7 = "zhimi.airpurifier.v7" MODEL_AIRPURIFIER_SA1 = "zhimi.airpurifier.sa1" MODEL_AIRPURIFIER_SA2 = "zhimi.airpurifier.sa2" @@ -125,6 +126,7 @@ MODELS_PURIFIER_MIOT = [ MODEL_AIRPURIFIER_3C, MODEL_AIRPURIFIER_3H, MODEL_AIRPURIFIER_PROH, + MODEL_AIRPURIFIER_PROH_EU, MODEL_AIRPURIFIER_4_LITE_RMA1, MODEL_AIRPURIFIER_4_LITE_RMB1, MODEL_AIRPURIFIER_4, diff --git a/homeassistant/components/xiaomi_miio/select.py b/homeassistant/components/xiaomi_miio/select.py index c1eb18e885f..b785adef15a 100644 --- a/homeassistant/components/xiaomi_miio/select.py +++ b/homeassistant/components/xiaomi_miio/select.py @@ -54,6 +54,7 @@ from .const import ( MODEL_AIRPURIFIER_M2, MODEL_AIRPURIFIER_MA2, MODEL_AIRPURIFIER_PROH, + MODEL_AIRPURIFIER_PROH_EU, MODEL_AIRPURIFIER_ZA1, MODEL_FAN_SA1, MODEL_FAN_V2, @@ -137,6 +138,9 @@ MODEL_TO_ATTR_MAP: dict[str, list] = { MODEL_AIRPURIFIER_PROH: [ AttributeEnumMapping(ATTR_LED_BRIGHTNESS, AirpurifierMiotLedBrightness) ], + MODEL_AIRPURIFIER_PROH_EU: [ + AttributeEnumMapping(ATTR_LED_BRIGHTNESS, AirpurifierMiotLedBrightness) + ], MODEL_FAN_SA1: [AttributeEnumMapping(ATTR_LED_BRIGHTNESS, FanLedBrightness)], MODEL_FAN_V2: [AttributeEnumMapping(ATTR_LED_BRIGHTNESS, FanLedBrightness)], MODEL_FAN_V3: [AttributeEnumMapping(ATTR_LED_BRIGHTNESS, FanLedBrightness)], From 8942088419f135da05c8ff3b592eccb8d861cefb Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Tue, 11 Jun 2024 07:07:52 +0200 Subject: [PATCH 0502/1445] Customize incomfort binary sensor icons (#119331) --- homeassistant/components/incomfort/icons.json | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 homeassistant/components/incomfort/icons.json diff --git a/homeassistant/components/incomfort/icons.json b/homeassistant/components/incomfort/icons.json new file mode 100644 index 00000000000..eb93ed9a319 --- /dev/null +++ b/homeassistant/components/incomfort/icons.json @@ -0,0 +1,24 @@ +{ + "entity": { + "binary_sensor": { + "is_burning": { + "state": { + "off": "mdi:fire-off", + "on": "mdi:fire" + } + }, + "is_pumping": { + "state": { + "off": "mdi:pump-off", + "on": "mdi:pump" + } + }, + "is_tapping": { + "state": { + "off": "mdi:water-pump-off", + "on": "mdi:water-pump" + } + } + } + } +} From cdd9f19cf9376134c2a8aada70e2b275c8f33e3f Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 11 Jun 2024 17:32:40 +1200 Subject: [PATCH 0503/1445] Bump aioesphomeapi to 24.6.0 (#119348) --- homeassistant/components/esphome/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index 37d2e7092e3..de855e15d4c 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -16,7 +16,7 @@ "loggers": ["aioesphomeapi", "noiseprotocol", "bleak_esphome"], "mqtt": ["esphome/discover/#"], "requirements": [ - "aioesphomeapi==24.5.0", + "aioesphomeapi==24.6.0", "esphome-dashboard-api==1.2.3", "bleak-esphome==1.0.0" ], diff --git a/requirements_all.txt b/requirements_all.txt index 89d6e0fbd23..b73060a23d1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -240,7 +240,7 @@ aioelectricitymaps==0.4.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==24.5.0 +aioesphomeapi==24.6.0 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 253b1a71284..7de35382dcd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -219,7 +219,7 @@ aioelectricitymaps==0.4.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==24.5.0 +aioesphomeapi==24.6.0 # homeassistant.components.flo aioflo==2021.11.0 From 0ea9581cfc3a7c151540dd7e29cc0a421828d9e5 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 11 Jun 2024 01:49:14 -0400 Subject: [PATCH 0504/1445] OpenAI to respect custom conversation IDs (#119307) --- .../openai_conversation/conversation.py | 18 ++++++++- .../openai_conversation/test_conversation.py | 39 +++++++++++++++++++ 2 files changed, 55 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/openai_conversation/conversation.py b/homeassistant/components/openai_conversation/conversation.py index d5e566678f1..d0b3ef8f895 100644 --- a/homeassistant/components/openai_conversation/conversation.py +++ b/homeassistant/components/openai_conversation/conversation.py @@ -141,11 +141,25 @@ class OpenAIConversationEntity( ) tools = [_format_tool(tool) for tool in llm_api.tools] - if user_input.conversation_id in self.history: + if user_input.conversation_id is None: + conversation_id = ulid.ulid_now() + messages = [] + + elif user_input.conversation_id in self.history: conversation_id = user_input.conversation_id messages = self.history[conversation_id] + else: - conversation_id = ulid.ulid_now() + # Conversation IDs are ULIDs. We generate a new one if not provided. + # If an old OLID is passed in, we will generate a new one to indicate + # a new conversation was started. If the user picks their own, they + # want to track a conversation and we respect it. + try: + ulid.ulid_to_bytes(user_input.conversation_id) + conversation_id = ulid.ulid_now() + except ValueError: + conversation_id = user_input.conversation_id + messages = [] if ( diff --git a/tests/components/openai_conversation/test_conversation.py b/tests/components/openai_conversation/test_conversation.py index 002b2df186b..5ca54611c91 100644 --- a/tests/components/openai_conversation/test_conversation.py +++ b/tests/components/openai_conversation/test_conversation.py @@ -22,6 +22,7 @@ from homeassistant.core import Context, HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import intent, llm from homeassistant.setup import async_setup_component +from homeassistant.util import ulid from tests.common import MockConfigEntry @@ -497,3 +498,41 @@ async def test_unknown_hass_api( ) assert result == snapshot + + +@patch( + "openai.resources.chat.completions.AsyncCompletions.create", + new_callable=AsyncMock, +) +async def test_conversation_id( + mock_create, + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_init_component, +) -> None: + """Test conversation ID is honored.""" + result = await conversation.async_converse( + hass, "hello", None, None, agent_id=mock_config_entry.entry_id + ) + + conversation_id = result.conversation_id + + result = await conversation.async_converse( + hass, "hello", conversation_id, None, agent_id=mock_config_entry.entry_id + ) + + assert result.conversation_id == conversation_id + + unknown_id = ulid.ulid() + + result = await conversation.async_converse( + hass, "hello", unknown_id, None, agent_id=mock_config_entry.entry_id + ) + + assert result.conversation_id != unknown_id + + result = await conversation.async_converse( + hass, "hello", "koala", None, agent_id=mock_config_entry.entry_id + ) + + assert result.conversation_id == "koala" From ecad1bef7e4d18f7f0565d5902ea3cb24284bd7e Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 11 Jun 2024 07:57:45 +0200 Subject: [PATCH 0505/1445] Avoid cross-domain imports in scrape tests (#119351) --- tests/components/scrape/test_sensor.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/components/scrape/test_sensor.py b/tests/components/scrape/test_sensor.py index 5b339b6a315..d1f2a22d036 100644 --- a/tests/components/scrape/test_sensor.py +++ b/tests/components/scrape/test_sensor.py @@ -8,8 +8,6 @@ from unittest.mock import patch from freezegun.api import FrozenDateTimeFactory import pytest -from homeassistant.components.rest.const import DEFAULT_METHOD -from homeassistant.components.rest.data import DEFAULT_TIMEOUT from homeassistant.components.scrape.const import ( CONF_ENCODING, CONF_INDEX, @@ -584,9 +582,9 @@ async def test_templates_with_yaml(hass: HomeAssistant) -> None: [ { CONF_RESOURCE: "https://www.home-assistant.io", - CONF_METHOD: DEFAULT_METHOD, + CONF_METHOD: "GET", CONF_VERIFY_SSL: DEFAULT_VERIFY_SSL, - CONF_TIMEOUT: DEFAULT_TIMEOUT, + CONF_TIMEOUT: 10, CONF_ENCODING: DEFAULT_ENCODING, SENSOR_DOMAIN: [ { From 4320445c3021933dead382411418830f264ce497 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 11 Jun 2024 07:59:46 +0200 Subject: [PATCH 0506/1445] Use absolute import in roborock tests (#119353) --- tests/components/roborock/test_config_flow.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/components/roborock/test_config_flow.py b/tests/components/roborock/test_config_flow.py index fc097dd73ae..5134ef7eea2 100644 --- a/tests/components/roborock/test_config_flow.py +++ b/tests/components/roborock/test_config_flow.py @@ -18,9 +18,10 @@ from homeassistant.const import CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType -from ...common import MockConfigEntry from .mock_data import MOCK_CONFIG, USER_DATA, USER_EMAIL +from tests.common import MockConfigEntry + async def test_config_flow_success( hass: HomeAssistant, From a3ac0af56d0f61eeedcb77ea9bdef188a332f14d Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 11 Jun 2024 08:01:52 +0200 Subject: [PATCH 0507/1445] Ignore some pylint errors in component tests (#119352) --- tests/components/forked_daapd/test_browse_media.py | 4 ++-- tests/components/ibeacon/test_device_tracker.py | 4 +++- tests/components/ibeacon/test_sensor.py | 4 +++- tests/components/zha/test_repairs.py | 2 +- 4 files changed, 9 insertions(+), 5 deletions(-) diff --git a/tests/components/forked_daapd/test_browse_media.py b/tests/components/forked_daapd/test_browse_media.py index 29923c9f9e9..805bcac3976 100644 --- a/tests/components/forked_daapd/test_browse_media.py +++ b/tests/components/forked_daapd/test_browse_media.py @@ -12,10 +12,10 @@ from homeassistant.components.forked_daapd.browse_media import ( is_owntone_media_content_id, ) from homeassistant.components.media_player import BrowseMedia, MediaClass, MediaType -from homeassistant.components.spotify.const import ( +from homeassistant.components.spotify.const import ( # pylint: disable=hass-component-root-import MEDIA_PLAYER_PREFIX as SPOTIFY_MEDIA_PLAYER_PREFIX, ) -from homeassistant.components.websocket_api.const import TYPE_RESULT +from homeassistant.components.websocket_api import TYPE_RESULT from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component diff --git a/tests/components/ibeacon/test_device_tracker.py b/tests/components/ibeacon/test_device_tracker.py index e34cc480cb0..dcc21b5bfc9 100644 --- a/tests/components/ibeacon/test_device_tracker.py +++ b/tests/components/ibeacon/test_device_tracker.py @@ -11,7 +11,9 @@ from homeassistant.components.bluetooth import ( async_ble_device_from_address, async_last_service_info, ) -from homeassistant.components.bluetooth.const import UNAVAILABLE_TRACK_SECONDS +from homeassistant.components.bluetooth.const import ( # pylint: disable=hass-component-root-import + UNAVAILABLE_TRACK_SECONDS, +) from homeassistant.components.ibeacon.const import ( DOMAIN, UNAVAILABLE_TIMEOUT, diff --git a/tests/components/ibeacon/test_sensor.py b/tests/components/ibeacon/test_sensor.py index f4dba57bced..e2ddf1dd7bc 100644 --- a/tests/components/ibeacon/test_sensor.py +++ b/tests/components/ibeacon/test_sensor.py @@ -4,7 +4,9 @@ from datetime import timedelta import pytest -from homeassistant.components.bluetooth.const import UNAVAILABLE_TRACK_SECONDS +from homeassistant.components.bluetooth.const import ( # pylint: disable=hass-component-root-import + UNAVAILABLE_TRACK_SECONDS, +) from homeassistant.components.ibeacon.const import DOMAIN, UPDATE_INTERVAL from homeassistant.components.sensor import ATTR_STATE_CLASS from homeassistant.const import ( diff --git a/tests/components/zha/test_repairs.py b/tests/components/zha/test_repairs.py index abb9dc6dc9e..c093fe266bd 100644 --- a/tests/components/zha/test_repairs.py +++ b/tests/components/zha/test_repairs.py @@ -12,7 +12,7 @@ from zigpy.application import ControllerApplication import zigpy.backups from zigpy.exceptions import NetworkSettingsInconsistent -from homeassistant.components.homeassistant_sky_connect.const import ( +from homeassistant.components.homeassistant_sky_connect.const import ( # pylint: disable=hass-component-root-import DOMAIN as SKYCONNECT_DOMAIN, ) from homeassistant.components.repairs import DOMAIN as REPAIRS_DOMAIN From 08eb8232e5df65fe35a9b1418354cdbc9ccb6b6f Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 11 Jun 2024 08:08:47 +0200 Subject: [PATCH 0508/1445] Fix namespace-import pylint warning in shelly tests (#119349) --- tests/components/shelly/__init__.py | 10 +-- tests/components/shelly/conftest.py | 8 +- tests/components/shelly/test_binary_sensor.py | 16 ++-- tests/components/shelly/test_climate.py | 20 ++--- tests/components/shelly/test_coordinator.py | 39 +++++----- .../components/shelly/test_device_trigger.py | 75 ++++++++++--------- tests/components/shelly/test_init.py | 8 +- tests/components/shelly/test_logbook.py | 15 ++-- tests/components/shelly/test_number.py | 8 +- tests/components/shelly/test_sensor.py | 37 ++++----- tests/components/shelly/test_switch.py | 8 +- tests/components/shelly/test_update.py | 8 +- 12 files changed, 119 insertions(+), 133 deletions(-) diff --git a/tests/components/shelly/__init__.py b/tests/components/shelly/__init__.py index 348b1115a6f..4631a17969e 100644 --- a/tests/components/shelly/__init__.py +++ b/tests/components/shelly/__init__.py @@ -20,12 +20,12 @@ from homeassistant.components.shelly.const import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er from homeassistant.helpers.device_registry import ( CONNECTION_NETWORK_MAC, DeviceRegistry, format_mac, ) -from homeassistant.helpers.entity_registry import async_get from tests.common import MockConfigEntry, async_fire_time_changed @@ -113,7 +113,7 @@ def register_entity( capabilities: Mapping[str, Any] | None = None, ) -> str: """Register enabled entity, return entity_id.""" - entity_registry = async_get(hass) + entity_registry = er.async_get(hass) entity_registry.async_get_or_create( domain, DOMAIN, @@ -132,7 +132,7 @@ def get_entity( unique_id: str, ) -> str | None: """Get Shelly entity.""" - entity_registry = async_get(hass) + entity_registry = er.async_get(hass) return entity_registry.async_get_entity_id( domain, DOMAIN, f"{MOCK_MAC}-{unique_id}" ) @@ -145,9 +145,9 @@ def get_entity_state(hass: HomeAssistant, entity_id: str) -> str: return entity.state -def register_device(device_reg: DeviceRegistry, config_entry: ConfigEntry) -> None: +def register_device(device_registry: DeviceRegistry, config_entry: ConfigEntry) -> None: """Register Shelly device.""" - device_reg.async_get_or_create( + device_registry.async_get_or_create( config_entry_id=config_entry.entry_id, connections={(CONNECTION_NETWORK_MAC, format_mac(MOCK_MAC))}, ) diff --git a/tests/components/shelly/conftest.py b/tests/components/shelly/conftest.py index 23ed1f306b1..6099a16d52e 100644 --- a/tests/components/shelly/conftest.py +++ b/tests/components/shelly/conftest.py @@ -15,7 +15,7 @@ from homeassistant.core import HomeAssistant, ServiceCall from . import MOCK_MAC -from tests.common import async_capture_events, async_mock_service, mock_device_registry +from tests.common import async_capture_events, async_mock_service MOCK_SETTINGS = { "name": "Test name", @@ -286,12 +286,6 @@ def mock_ws_server(): yield -@pytest.fixture -def device_reg(hass: HomeAssistant): - """Return an empty, loaded, registry.""" - return mock_device_registry(hass) - - @pytest.fixture def calls(hass: HomeAssistant) -> list[ServiceCall]: """Track calls to a mock service.""" diff --git a/tests/components/shelly/test_binary_sensor.py b/tests/components/shelly/test_binary_sensor.py index 524bc1e8ffc..026a7041863 100644 --- a/tests/components/shelly/test_binary_sensor.py +++ b/tests/components/shelly/test_binary_sensor.py @@ -162,12 +162,12 @@ async def test_block_sleeping_binary_sensor( async def test_block_restored_sleeping_binary_sensor( hass: HomeAssistant, mock_block_device: Mock, - device_reg: DeviceRegistry, + device_registry: DeviceRegistry, monkeypatch: pytest.MonkeyPatch, ) -> None: """Test block restored sleeping binary sensor.""" entry = await init_integration(hass, 1, sleep_period=1000, skip_setup=True) - register_device(device_reg, entry) + register_device(device_registry, entry) entity_id = register_entity( hass, BINARY_SENSOR_DOMAIN, "test_name_motion", "sensor_0-motion", entry ) @@ -189,12 +189,12 @@ async def test_block_restored_sleeping_binary_sensor( async def test_block_restored_sleeping_binary_sensor_no_last_state( hass: HomeAssistant, mock_block_device: Mock, - device_reg: DeviceRegistry, + device_registry: DeviceRegistry, monkeypatch: pytest.MonkeyPatch, ) -> None: """Test block restored sleeping binary sensor missing last state.""" entry = await init_integration(hass, 1, sleep_period=1000, skip_setup=True) - register_device(device_reg, entry) + register_device(device_registry, entry) entity_id = register_entity( hass, BINARY_SENSOR_DOMAIN, "test_name_motion", "sensor_0-motion", entry ) @@ -297,12 +297,12 @@ async def test_rpc_sleeping_binary_sensor( async def test_rpc_restored_sleeping_binary_sensor( hass: HomeAssistant, mock_rpc_device: Mock, - device_reg: DeviceRegistry, + device_registry: DeviceRegistry, monkeypatch: pytest.MonkeyPatch, ) -> None: """Test RPC restored binary sensor.""" entry = await init_integration(hass, 2, sleep_period=1000, skip_setup=True) - register_device(device_reg, entry) + register_device(device_registry, entry) entity_id = register_entity( hass, BINARY_SENSOR_DOMAIN, "test_name_cloud", "cloud-cloud", entry ) @@ -326,12 +326,12 @@ async def test_rpc_restored_sleeping_binary_sensor( async def test_rpc_restored_sleeping_binary_sensor_no_last_state( hass: HomeAssistant, mock_rpc_device: Mock, - device_reg: DeviceRegistry, + device_registry: DeviceRegistry, monkeypatch: pytest.MonkeyPatch, ) -> None: """Test RPC restored sleeping binary sensor missing last state.""" entry = await init_integration(hass, 2, sleep_period=1000, skip_setup=True) - register_device(device_reg, entry) + register_device(device_registry, entry) entity_id = register_entity( hass, BINARY_SENSOR_DOMAIN, "test_name_cloud", "cloud-cloud", entry ) diff --git a/tests/components/shelly/test_climate.py b/tests/components/shelly/test_climate.py index aac14c24288..ed4ceea0306 100644 --- a/tests/components/shelly/test_climate.py +++ b/tests/components/shelly/test_climate.py @@ -244,7 +244,7 @@ async def test_climate_set_preset_mode( async def test_block_restored_climate( hass: HomeAssistant, mock_block_device: Mock, - device_reg: DeviceRegistry, + device_registry: DeviceRegistry, monkeypatch: pytest.MonkeyPatch, ) -> None: """Test block restored climate.""" @@ -253,7 +253,7 @@ async def test_block_restored_climate( monkeypatch.setattr(mock_block_device.blocks[DEVICE_BLOCK_ID], "valveError", 0) monkeypatch.delattr(mock_block_device.blocks[EMETER_BLOCK_ID], "targetTemp") entry = await init_integration(hass, 1, sleep_period=1000, skip_setup=True) - register_device(device_reg, entry) + register_device(device_registry, entry) entity_id = register_entity( hass, CLIMATE_DOMAIN, @@ -310,7 +310,7 @@ async def test_block_restored_climate( async def test_block_restored_climate_us_customery( hass: HomeAssistant, mock_block_device: Mock, - device_reg: DeviceRegistry, + device_registry: DeviceRegistry, monkeypatch: pytest.MonkeyPatch, ) -> None: """Test block restored climate with US CUSTOMATY unit system.""" @@ -320,7 +320,7 @@ async def test_block_restored_climate_us_customery( monkeypatch.setattr(mock_block_device.blocks[DEVICE_BLOCK_ID], "valveError", 0) monkeypatch.delattr(mock_block_device.blocks[EMETER_BLOCK_ID], "targetTemp") entry = await init_integration(hass, 1, sleep_period=1000, skip_setup=True) - register_device(device_reg, entry) + register_device(device_registry, entry) entity_id = register_entity( hass, CLIMATE_DOMAIN, @@ -382,14 +382,14 @@ async def test_block_restored_climate_us_customery( async def test_block_restored_climate_unavailable( hass: HomeAssistant, mock_block_device: Mock, - device_reg: DeviceRegistry, + device_registry: DeviceRegistry, monkeypatch: pytest.MonkeyPatch, ) -> None: """Test block restored climate unavailable state.""" monkeypatch.delattr(mock_block_device.blocks[DEVICE_BLOCK_ID], "targetTemp") monkeypatch.setattr(mock_block_device.blocks[DEVICE_BLOCK_ID], "valveError", 0) entry = await init_integration(hass, 1, sleep_period=1000, skip_setup=True) - register_device(device_reg, entry) + register_device(device_registry, entry) entity_id = register_entity( hass, CLIMATE_DOMAIN, @@ -409,14 +409,14 @@ async def test_block_restored_climate_unavailable( async def test_block_restored_climate_set_preset_before_online( hass: HomeAssistant, mock_block_device: Mock, - device_reg: DeviceRegistry, + device_registry: DeviceRegistry, monkeypatch: pytest.MonkeyPatch, ) -> None: """Test block restored climate set preset before device is online.""" monkeypatch.delattr(mock_block_device.blocks[DEVICE_BLOCK_ID], "targetTemp") monkeypatch.setattr(mock_block_device.blocks[DEVICE_BLOCK_ID], "valveError", 0) entry = await init_integration(hass, 1, sleep_period=1000, skip_setup=True) - register_device(device_reg, entry) + register_device(device_registry, entry) entity_id = register_entity( hass, CLIMATE_DOMAIN, @@ -510,14 +510,14 @@ async def test_block_set_mode_auth_error( async def test_block_restored_climate_auth_error( hass: HomeAssistant, mock_block_device: Mock, - device_reg: DeviceRegistry, + device_registry: DeviceRegistry, monkeypatch: pytest.MonkeyPatch, ) -> None: """Test block restored climate with authentication error during init.""" monkeypatch.delattr(mock_block_device.blocks[DEVICE_BLOCK_ID], "targetTemp") monkeypatch.setattr(mock_block_device.blocks[DEVICE_BLOCK_ID], "valveError", 0) entry = await init_integration(hass, 1, sleep_period=1000, skip_setup=True) - register_device(device_reg, entry) + register_device(device_registry, entry) entity_id = register_entity( hass, CLIMATE_DOMAIN, diff --git a/tests/components/shelly/test_coordinator.py b/tests/components/shelly/test_coordinator.py index cd750e53f0b..895d18cd7e1 100644 --- a/tests/components/shelly/test_coordinator.py +++ b/tests/components/shelly/test_coordinator.py @@ -27,14 +27,7 @@ from homeassistant.components.shelly.const import ( from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState from homeassistant.const import ATTR_DEVICE_ID, STATE_ON, STATE_UNAVAILABLE from homeassistant.core import Event, HomeAssistant, State -from homeassistant.helpers import issue_registry as ir -from homeassistant.helpers.device_registry import ( - CONNECTION_NETWORK_MAC, - DeviceRegistry, - async_entries_for_config_entry, - async_get as async_get_dev_reg, - format_mac, -) +from homeassistant.helpers import device_registry as dr, issue_registry as ir from . import ( MOCK_MAC, @@ -343,6 +336,7 @@ async def test_block_device_push_updates_failure( async def test_block_button_click_event( hass: HomeAssistant, + device_registry: dr.DeviceRegistry, mock_block_device: Mock, events: list[Event], monkeypatch: pytest.MonkeyPatch, @@ -360,8 +354,7 @@ async def test_block_button_click_event( mock_block_device.mock_online() await hass.async_block_till_done() - dev_reg = async_get_dev_reg(hass) - device = async_entries_for_config_entry(dev_reg, entry.entry_id)[0] + device = dr.async_entries_for_config_entry(device_registry, entry.entry_id)[0] # Generate button click event mock_block_device.mock_update() @@ -508,6 +501,7 @@ async def test_rpc_connection_error_during_unload( async def test_rpc_click_event( hass: HomeAssistant, + device_registry: dr.DeviceRegistry, mock_rpc_device: Mock, events: list[Event], monkeypatch: pytest.MonkeyPatch, @@ -515,8 +509,7 @@ async def test_rpc_click_event( """Test RPC click event.""" entry = await init_integration(hass, 2) - dev_reg = async_get_dev_reg(hass) - device = async_entries_for_config_entry(dev_reg, entry.entry_id)[0] + device = dr.async_entries_for_config_entry(device_registry, entry.entry_id)[0] # Generate config change from switch to light inject_rpc_device_event( @@ -805,21 +798,23 @@ async def test_rpc_polling_disconnected( async def test_rpc_update_entry_fw_ver( - hass: HomeAssistant, mock_rpc_device: Mock, monkeypatch: pytest.MonkeyPatch + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + mock_rpc_device: Mock, + monkeypatch: pytest.MonkeyPatch, ) -> None: """Test RPC update entry firmware version.""" monkeypatch.setitem(mock_rpc_device.status["sys"], "wakeup_period", 600) entry = await init_integration(hass, 2, sleep_period=600) - dev_reg = async_get_dev_reg(hass) # Make device online mock_rpc_device.mock_online() await hass.async_block_till_done(wait_background_tasks=True) assert entry.unique_id - device = dev_reg.async_get_device( + device = device_registry.async_get_device( identifiers={(DOMAIN, entry.entry_id)}, - connections={(CONNECTION_NETWORK_MAC, format_mac(entry.unique_id))}, + connections={(dr.CONNECTION_NETWORK_MAC, dr.format_mac(entry.unique_id))}, ) assert device assert device.sw_version == "some fw string" @@ -829,9 +824,9 @@ async def test_rpc_update_entry_fw_ver( mock_rpc_device.mock_update() await hass.async_block_till_done() - device = dev_reg.async_get_device( + device = device_registry.async_get_device( identifiers={(DOMAIN, entry.entry_id)}, - connections={(CONNECTION_NETWORK_MAC, format_mac(entry.unique_id))}, + connections={(dr.CONNECTION_NETWORK_MAC, dr.format_mac(entry.unique_id))}, ) assert device assert device.sw_version == "99.0.0" @@ -859,16 +854,16 @@ async def test_rpc_runs_connected_events_when_initialized( async def test_block_sleeping_device_connection_error( hass: HomeAssistant, + device_registry: dr.DeviceRegistry, freezer: FrozenDateTimeFactory, mock_block_device: Mock, - device_reg: DeviceRegistry, monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture, ) -> None: """Test block sleeping device connection error during initialize.""" sleep_period = 1000 entry = await init_integration(hass, 1, sleep_period=sleep_period, skip_setup=True) - register_device(device_reg, entry) + register_device(device_registry, entry) entity_id = register_entity( hass, BINARY_SENSOR_DOMAIN, "test_name_motion", "sensor_0-motion", entry ) @@ -904,16 +899,16 @@ async def test_block_sleeping_device_connection_error( async def test_rpc_sleeping_device_connection_error( hass: HomeAssistant, + device_registry: dr.DeviceRegistry, freezer: FrozenDateTimeFactory, mock_rpc_device: Mock, - device_reg: DeviceRegistry, monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture, ) -> None: """Test RPC sleeping device connection error during initialize.""" sleep_period = 1000 entry = await init_integration(hass, 2, sleep_period=1000, skip_setup=True) - register_device(device_reg, entry) + register_device(device_registry, entry) entity_id = register_entity( hass, BINARY_SENSOR_DOMAIN, "test_name_cloud", "cloud-cloud", entry ) diff --git a/tests/components/shelly/test_device_trigger.py b/tests/components/shelly/test_device_trigger.py index 42ea13aec24..fc860a4df46 100644 --- a/tests/components/shelly/test_device_trigger.py +++ b/tests/components/shelly/test_device_trigger.py @@ -20,12 +20,7 @@ from homeassistant.components.shelly.const import ( ) from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_PLATFORM, CONF_TYPE from homeassistant.core import HomeAssistant, ServiceCall -from homeassistant.helpers.device_registry import ( - CONNECTION_NETWORK_MAC, - DeviceRegistry, - async_entries_for_config_entry, - async_get as async_get_dev_reg, -) +from homeassistant.helpers import device_registry as dr from homeassistant.setup import async_setup_component from . import init_integration @@ -44,6 +39,7 @@ from tests.common import MockConfigEntry, async_get_device_automations ) async def test_get_triggers_block_device( hass: HomeAssistant, + device_registry: dr.DeviceRegistry, mock_block_device: Mock, monkeypatch: pytest.MonkeyPatch, button_type: str, @@ -59,8 +55,7 @@ async def test_get_triggers_block_device( ], ) entry = await init_integration(hass, 1) - dev_reg = async_get_dev_reg(hass) - device = async_entries_for_config_entry(dev_reg, entry.entry_id)[0] + device = dr.async_entries_for_config_entry(device_registry, entry.entry_id)[0] expected_triggers = [] if is_valid: @@ -84,12 +79,11 @@ async def test_get_triggers_block_device( async def test_get_triggers_rpc_device( - hass: HomeAssistant, mock_rpc_device: Mock + hass: HomeAssistant, device_registry: dr.DeviceRegistry, mock_rpc_device: Mock ) -> None: """Test we get the expected triggers from a shelly RPC device.""" entry = await init_integration(hass, 2) - dev_reg = async_get_dev_reg(hass) - device = async_entries_for_config_entry(dev_reg, entry.entry_id)[0] + device = dr.async_entries_for_config_entry(device_registry, entry.entry_id)[0] expected_triggers = [ { @@ -118,12 +112,11 @@ async def test_get_triggers_rpc_device( async def test_get_triggers_button( - hass: HomeAssistant, mock_block_device: Mock + hass: HomeAssistant, device_registry: dr.DeviceRegistry, mock_block_device: Mock ) -> None: """Test we get the expected triggers from a shelly button.""" entry = await init_integration(hass, 1, model=MODEL_BUTTON1) - dev_reg = async_get_dev_reg(hass) - device = async_entries_for_config_entry(dev_reg, entry.entry_id)[0] + device = dr.async_entries_for_config_entry(device_registry, entry.entry_id)[0] expected_triggers = [ { @@ -145,13 +138,15 @@ async def test_get_triggers_button( async def test_get_triggers_non_initialized_devices( - hass: HomeAssistant, mock_block_device: Mock, monkeypatch: pytest.MonkeyPatch + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + mock_block_device: Mock, + monkeypatch: pytest.MonkeyPatch, ) -> None: """Test we get the empty triggers for non-initialized devices.""" monkeypatch.setattr(mock_block_device, "initialized", False) entry = await init_integration(hass, 1) - dev_reg = async_get_dev_reg(hass) - device = async_entries_for_config_entry(dev_reg, entry.entry_id)[0] + device = dr.async_entries_for_config_entry(device_registry, entry.entry_id)[0] expected_triggers = [] @@ -163,15 +158,15 @@ async def test_get_triggers_non_initialized_devices( async def test_get_triggers_for_invalid_device_id( - hass: HomeAssistant, device_reg: DeviceRegistry, mock_block_device: Mock + hass: HomeAssistant, device_registry: dr.DeviceRegistry, mock_block_device: Mock ) -> None: """Test error raised for invalid shelly device_id.""" await init_integration(hass, 1) config_entry = MockConfigEntry(domain=DOMAIN, data={}) config_entry.add_to_hass(hass) - invalid_device = device_reg.async_get_or_create( + invalid_device = device_registry.async_get_or_create( config_entry_id=config_entry.entry_id, - connections={(CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, ) with pytest.raises(InvalidDeviceAutomationConfig): @@ -181,12 +176,14 @@ async def test_get_triggers_for_invalid_device_id( async def test_if_fires_on_click_event_block_device( - hass: HomeAssistant, calls: list[ServiceCall], mock_block_device: Mock + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + calls: list[ServiceCall], + mock_block_device: Mock, ) -> None: """Test for click_event trigger firing for block device.""" entry = await init_integration(hass, 1) - dev_reg = async_get_dev_reg(hass) - device = async_entries_for_config_entry(dev_reg, entry.entry_id)[0] + device = dr.async_entries_for_config_entry(device_registry, entry.entry_id)[0] assert await async_setup_component( hass, @@ -223,12 +220,14 @@ async def test_if_fires_on_click_event_block_device( async def test_if_fires_on_click_event_rpc_device( - hass: HomeAssistant, calls: list[ServiceCall], mock_rpc_device: Mock + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + calls: list[ServiceCall], + mock_rpc_device: Mock, ) -> None: """Test for click_event trigger firing for rpc device.""" entry = await init_integration(hass, 2) - dev_reg = async_get_dev_reg(hass) - device = async_entries_for_config_entry(dev_reg, entry.entry_id)[0] + device = dr.async_entries_for_config_entry(device_registry, entry.entry_id)[0] assert await async_setup_component( hass, @@ -266,6 +265,7 @@ async def test_if_fires_on_click_event_rpc_device( async def test_validate_trigger_block_device_not_ready( hass: HomeAssistant, + device_registry: dr.DeviceRegistry, calls: list[ServiceCall], mock_block_device: Mock, monkeypatch: pytest.MonkeyPatch, @@ -273,8 +273,7 @@ async def test_validate_trigger_block_device_not_ready( """Test validate trigger config when block device is not ready.""" monkeypatch.setattr(mock_block_device, "initialized", False) entry = await init_integration(hass, 1) - dev_reg = async_get_dev_reg(hass) - device = async_entries_for_config_entry(dev_reg, entry.entry_id)[0] + device = dr.async_entries_for_config_entry(device_registry, entry.entry_id)[0] assert await async_setup_component( hass, @@ -311,6 +310,7 @@ async def test_validate_trigger_block_device_not_ready( async def test_validate_trigger_rpc_device_not_ready( hass: HomeAssistant, + device_registry: dr.DeviceRegistry, calls: list[ServiceCall], mock_rpc_device: Mock, monkeypatch: pytest.MonkeyPatch, @@ -318,8 +318,7 @@ async def test_validate_trigger_rpc_device_not_ready( """Test validate trigger config when RPC device is not ready.""" monkeypatch.setattr(mock_rpc_device, "initialized", False) entry = await init_integration(hass, 2) - dev_reg = async_get_dev_reg(hass) - device = async_entries_for_config_entry(dev_reg, entry.entry_id)[0] + device = dr.async_entries_for_config_entry(device_registry, entry.entry_id)[0] assert await async_setup_component( hass, @@ -355,12 +354,14 @@ async def test_validate_trigger_rpc_device_not_ready( async def test_validate_trigger_invalid_triggers( - hass: HomeAssistant, mock_block_device: Mock, caplog: pytest.LogCaptureFixture + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + mock_block_device: Mock, + caplog: pytest.LogCaptureFixture, ) -> None: """Test for click_event with invalid triggers.""" entry = await init_integration(hass, 1) - dev_reg = async_get_dev_reg(hass) - device = async_entries_for_config_entry(dev_reg, entry.entry_id)[0] + device = dr.async_entries_for_config_entry(device_registry, entry.entry_id)[0] assert await async_setup_component( hass, @@ -389,6 +390,7 @@ async def test_validate_trigger_invalid_triggers( async def test_rpc_no_runtime_data( hass: HomeAssistant, + device_registry: dr.DeviceRegistry, calls: list[ServiceCall], mock_rpc_device: Mock, monkeypatch: pytest.MonkeyPatch, @@ -396,8 +398,7 @@ async def test_rpc_no_runtime_data( """Test the device trigger for the RPC device when there is no runtime_data in the entry.""" entry = await init_integration(hass, 2) monkeypatch.delattr(entry, "runtime_data") - dev_reg = async_get_dev_reg(hass) - device = async_entries_for_config_entry(dev_reg, entry.entry_id)[0] + device = dr.async_entries_for_config_entry(device_registry, entry.entry_id)[0] assert await async_setup_component( hass, @@ -434,6 +435,7 @@ async def test_rpc_no_runtime_data( async def test_block_no_runtime_data( hass: HomeAssistant, + device_registry: dr.DeviceRegistry, calls: list[ServiceCall], mock_block_device: Mock, monkeypatch: pytest.MonkeyPatch, @@ -441,8 +443,7 @@ async def test_block_no_runtime_data( """Test the device trigger for the block device when there is no runtime_data in the entry.""" entry = await init_integration(hass, 1) monkeypatch.delattr(entry, "runtime_data") - dev_reg = async_get_dev_reg(hass) - device = async_entries_for_config_entry(dev_reg, entry.entry_id)[0] + device = dr.async_entries_for_config_entry(device_registry, entry.entry_id)[0] assert await async_setup_component( hass, diff --git a/tests/components/shelly/test_init.py b/tests/components/shelly/test_init.py index 61ec8ce6779..05d306c76ff 100644 --- a/tests/components/shelly/test_init.py +++ b/tests/components/shelly/test_init.py @@ -117,13 +117,13 @@ async def test_shared_device_mac( gen: int, mock_block_device: Mock, mock_rpc_device: Mock, - device_reg: DeviceRegistry, + device_registry: DeviceRegistry, caplog: pytest.LogCaptureFixture, ) -> None: """Test first time shared device with another domain.""" config_entry = MockConfigEntry(domain="test", data={}, unique_id="some_id") config_entry.add_to_hass(hass) - device_reg.async_get_or_create( + device_registry.async_get_or_create( config_entry_id=config_entry.entry_id, connections={(CONNECTION_NETWORK_MAC, format_mac(MOCK_MAC))}, ) @@ -243,13 +243,13 @@ async def test_sleeping_block_device_online( device_sleep: int, mock_block_device: Mock, monkeypatch: pytest.MonkeyPatch, - device_reg: DeviceRegistry, + device_registry: DeviceRegistry, caplog: pytest.LogCaptureFixture, ) -> None: """Test sleeping block device online.""" config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id="shelly") config_entry.add_to_hass(hass) - device_reg.async_get_or_create( + device_registry.async_get_or_create( config_entry_id=config_entry.entry_id, connections={(CONNECTION_NETWORK_MAC, format_mac(MOCK_MAC))}, ) diff --git a/tests/components/shelly/test_logbook.py b/tests/components/shelly/test_logbook.py index cd1714d6b26..8962b26544b 100644 --- a/tests/components/shelly/test_logbook.py +++ b/tests/components/shelly/test_logbook.py @@ -11,10 +11,7 @@ from homeassistant.components.shelly.const import ( ) from homeassistant.const import ATTR_DEVICE_ID from homeassistant.core import HomeAssistant -from homeassistant.helpers.device_registry import ( - async_entries_for_config_entry, - async_get as async_get_dev_reg, -) +from homeassistant.helpers import device_registry as dr from homeassistant.setup import async_setup_component from . import init_integration @@ -23,12 +20,11 @@ from tests.components.logbook.common import MockRow, mock_humanify async def test_humanify_shelly_click_event_block_device( - hass: HomeAssistant, mock_block_device: Mock + hass: HomeAssistant, device_registry: dr.DeviceRegistry, mock_block_device: Mock ) -> None: """Test humanifying Shelly click event for block device.""" entry = await init_integration(hass, 1) - dev_reg = async_get_dev_reg(hass) - device = async_entries_for_config_entry(dev_reg, entry.entry_id)[0] + device = dr.async_entries_for_config_entry(device_registry, entry.entry_id)[0] hass.config.components.add("recorder") assert await async_setup_component(hass, "logbook", {}) @@ -74,12 +70,11 @@ async def test_humanify_shelly_click_event_block_device( async def test_humanify_shelly_click_event_rpc_device( - hass: HomeAssistant, mock_rpc_device: Mock + hass: HomeAssistant, device_registry: dr.DeviceRegistry, mock_rpc_device: Mock ) -> None: """Test humanifying Shelly click event for rpc device.""" entry = await init_integration(hass, 2) - dev_reg = async_get_dev_reg(hass) - device = async_entries_for_config_entry(dev_reg, entry.entry_id)[0] + device = dr.async_entries_for_config_entry(device_registry, entry.entry_id)[0] hass.config.components.add("recorder") assert await async_setup_component(hass, "logbook", {}) diff --git a/tests/components/shelly/test_number.py b/tests/components/shelly/test_number.py index a5f64409d09..3f0f3ae8686 100644 --- a/tests/components/shelly/test_number.py +++ b/tests/components/shelly/test_number.py @@ -61,12 +61,12 @@ async def test_block_number_update( async def test_block_restored_number( hass: HomeAssistant, mock_block_device: Mock, - device_reg: DeviceRegistry, + device_registry: DeviceRegistry, monkeypatch: pytest.MonkeyPatch, ) -> None: """Test block restored number.""" entry = await init_integration(hass, 1, sleep_period=1000, skip_setup=True) - register_device(device_reg, entry) + register_device(device_registry, entry) capabilities = { "min": 0, "max": 100, @@ -107,12 +107,12 @@ async def test_block_restored_number( async def test_block_restored_number_no_last_state( hass: HomeAssistant, mock_block_device: Mock, - device_reg: DeviceRegistry, + device_registry: DeviceRegistry, monkeypatch: pytest.MonkeyPatch, ) -> None: """Test block restored number missing last state.""" entry = await init_integration(hass, 1, sleep_period=1000, skip_setup=True) - register_device(device_reg, entry) + register_device(device_registry, entry) capabilities = { "min": 0, "max": 100, diff --git a/tests/components/shelly/test_sensor.py b/tests/components/shelly/test_sensor.py index 33008287b98..036a5e0d70e 100644 --- a/tests/components/shelly/test_sensor.py +++ b/tests/components/shelly/test_sensor.py @@ -28,7 +28,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, State from homeassistant.helpers.device_registry import DeviceRegistry -from homeassistant.helpers.entity_registry import EntityRegistry, async_get +from homeassistant.helpers.entity_registry import EntityRegistry from homeassistant.setup import async_setup_component from . import ( @@ -182,12 +182,12 @@ async def test_block_sleeping_sensor( async def test_block_restored_sleeping_sensor( hass: HomeAssistant, mock_block_device: Mock, - device_reg: DeviceRegistry, + device_registry: DeviceRegistry, monkeypatch: pytest.MonkeyPatch, ) -> None: """Test block restored sleeping sensor.""" entry = await init_integration(hass, 1, sleep_period=1000, skip_setup=True) - register_device(device_reg, entry) + register_device(device_registry, entry) entity_id = register_entity( hass, SENSOR_DOMAIN, "test_name_temperature", "sensor_0-temp", entry ) @@ -215,12 +215,12 @@ async def test_block_restored_sleeping_sensor( async def test_block_restored_sleeping_sensor_no_last_state( hass: HomeAssistant, mock_block_device: Mock, - device_reg: DeviceRegistry, + device_registry: DeviceRegistry, monkeypatch: pytest.MonkeyPatch, ) -> None: """Test block restored sleeping sensor missing last state.""" entry = await init_integration(hass, 1, sleep_period=1000, skip_setup=True) - register_device(device_reg, entry) + register_device(device_registry, entry) entity_id = register_entity( hass, SENSOR_DOMAIN, "test_name_temperature", "sensor_0-temp", entry ) @@ -282,12 +282,12 @@ async def test_block_sensor_removal( async def test_block_not_matched_restored_sleeping_sensor( hass: HomeAssistant, mock_block_device: Mock, - device_reg: DeviceRegistry, + device_registry: DeviceRegistry, monkeypatch: pytest.MonkeyPatch, ) -> None: """Test block not matched to restored sleeping sensor.""" entry = await init_integration(hass, 1, sleep_period=1000, skip_setup=True) - register_device(device_reg, entry) + register_device(device_registry, entry) entity_id = register_entity( hass, SENSOR_DOMAIN, "test_name_temperature", "sensor_0-temp", entry ) @@ -443,7 +443,7 @@ async def test_rpc_polling_sensor( async def test_rpc_sleeping_sensor( hass: HomeAssistant, mock_rpc_device: Mock, - device_reg: DeviceRegistry, + device_registry: DeviceRegistry, monkeypatch: pytest.MonkeyPatch, ) -> None: """Test RPC online sleeping sensor.""" @@ -477,12 +477,12 @@ async def test_rpc_sleeping_sensor( async def test_rpc_restored_sleeping_sensor( hass: HomeAssistant, mock_rpc_device: Mock, - device_reg: DeviceRegistry, + device_registry: DeviceRegistry, monkeypatch: pytest.MonkeyPatch, ) -> None: """Test RPC restored sensor.""" entry = await init_integration(hass, 2, sleep_period=1000, skip_setup=True) - register_device(device_reg, entry) + register_device(device_registry, entry) entity_id = register_entity( hass, SENSOR_DOMAIN, @@ -515,12 +515,12 @@ async def test_rpc_restored_sleeping_sensor( async def test_rpc_restored_sleeping_sensor_no_last_state( hass: HomeAssistant, mock_rpc_device: Mock, - device_reg: DeviceRegistry, + device_registry: DeviceRegistry, monkeypatch: pytest.MonkeyPatch, ) -> None: """Test RPC restored sensor missing last state.""" entry = await init_integration(hass, 2, sleep_period=1000, skip_setup=True) - register_device(device_reg, entry) + register_device(device_registry, entry) entity_id = register_entity( hass, SENSOR_DOMAIN, @@ -549,16 +549,17 @@ async def test_rpc_restored_sleeping_sensor_no_last_state( @pytest.mark.usefixtures("entity_registry_enabled_by_default") -async def test_rpc_em1_sensors(hass: HomeAssistant, mock_rpc_device: Mock) -> None: +async def test_rpc_em1_sensors( + hass: HomeAssistant, entity_registry: EntityRegistry, mock_rpc_device: Mock +) -> None: """Test RPC sensors for EM1 component.""" - registry = async_get(hass) await init_integration(hass, 2) state = hass.states.get("sensor.test_name_em0_power") assert state assert state.state == "85.3" - entry = registry.async_get("sensor.test_name_em0_power") + entry = entity_registry.async_get("sensor.test_name_em0_power") assert entry assert entry.unique_id == "123456789ABC-em1:0-power_em1" @@ -566,7 +567,7 @@ async def test_rpc_em1_sensors(hass: HomeAssistant, mock_rpc_device: Mock) -> No assert state assert state.state == "123.3" - entry = registry.async_get("sensor.test_name_em1_power") + entry = entity_registry.async_get("sensor.test_name_em1_power") assert entry assert entry.unique_id == "123456789ABC-em1:1-power_em1" @@ -574,7 +575,7 @@ async def test_rpc_em1_sensors(hass: HomeAssistant, mock_rpc_device: Mock) -> No assert state assert state.state == "123.4564" - entry = registry.async_get("sensor.test_name_em0_total_active_energy") + entry = entity_registry.async_get("sensor.test_name_em0_total_active_energy") assert entry assert entry.unique_id == "123456789ABC-em1data:0-total_act_energy" @@ -582,7 +583,7 @@ async def test_rpc_em1_sensors(hass: HomeAssistant, mock_rpc_device: Mock) -> No assert state assert state.state == "987.6543" - entry = registry.async_get("sensor.test_name_em1_total_active_energy") + entry = entity_registry.async_get("sensor.test_name_em1_total_active_energy") assert entry assert entry.unique_id == "123456789ABC-em1data:1-total_act_energy" diff --git a/tests/components/shelly/test_switch.py b/tests/components/shelly/test_switch.py index ac75e6dd96f..daaf03b081b 100644 --- a/tests/components/shelly/test_switch.py +++ b/tests/components/shelly/test_switch.py @@ -115,14 +115,14 @@ async def test_block_restored_motion_switch( hass: HomeAssistant, model: str, mock_block_device: Mock, - device_reg: DeviceRegistry, + device_registry: DeviceRegistry, monkeypatch: pytest.MonkeyPatch, ) -> None: """Test block restored motion active switch.""" entry = await init_integration( hass, 1, sleep_period=1000, model=model, skip_setup=True ) - register_device(device_reg, entry) + register_device(device_registry, entry) entity_id = register_entity( hass, SWITCH_DOMAIN, @@ -151,14 +151,14 @@ async def test_block_restored_motion_switch_no_last_state( hass: HomeAssistant, model: str, mock_block_device: Mock, - device_reg: DeviceRegistry, + device_registry: DeviceRegistry, monkeypatch: pytest.MonkeyPatch, ) -> None: """Test block restored motion active switch missing last state.""" entry = await init_integration( hass, 1, sleep_period=1000, model=model, skip_setup=True ) - register_device(device_reg, entry) + register_device(device_registry, entry) entity_id = register_entity( hass, SWITCH_DOMAIN, diff --git a/tests/components/shelly/test_update.py b/tests/components/shelly/test_update.py index 9b779da093e..8448c116815 100644 --- a/tests/components/shelly/test_update.py +++ b/tests/components/shelly/test_update.py @@ -379,12 +379,12 @@ async def test_rpc_sleeping_update( async def test_rpc_restored_sleeping_update( hass: HomeAssistant, mock_rpc_device: Mock, - device_reg: DeviceRegistry, + device_registry: DeviceRegistry, monkeypatch: pytest.MonkeyPatch, ) -> None: """Test RPC restored update entity.""" entry = await init_integration(hass, 2, sleep_period=1000, skip_setup=True) - register_device(device_reg, entry) + register_device(device_registry, entry) entity_id = register_entity( hass, UPDATE_DOMAIN, @@ -429,7 +429,7 @@ async def test_rpc_restored_sleeping_update( async def test_rpc_restored_sleeping_update_no_last_state( hass: HomeAssistant, mock_rpc_device: Mock, - device_reg: DeviceRegistry, + device_registry: DeviceRegistry, monkeypatch: pytest.MonkeyPatch, ) -> None: """Test RPC restored update entity missing last state.""" @@ -442,7 +442,7 @@ async def test_rpc_restored_sleeping_update_no_last_state( }, ) entry = await init_integration(hass, 2, sleep_period=1000, skip_setup=True) - register_device(device_reg, entry) + register_device(device_registry, entry) entity_id = register_entity( hass, UPDATE_DOMAIN, From 511547c29ad4157e5a5bcf03bc2248ba749467db Mon Sep 17 00:00:00 2001 From: kaareseras Date: Tue, 11 Jun 2024 09:18:06 +0200 Subject: [PATCH 0509/1445] Fix Azure data explorer (#119089) Co-authored-by: Robert Resch --- .../azure_data_explorer/__init__.py | 9 ++-- .../components/azure_data_explorer/client.py | 41 ++++++++++++------- .../azure_data_explorer/config_flow.py | 5 ++- .../components/azure_data_explorer/const.py | 2 +- .../azure_data_explorer/strings.json | 14 ++++--- tests/components/azure_data_explorer/const.py | 8 ++-- 6 files changed, 47 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/azure_data_explorer/__init__.py b/homeassistant/components/azure_data_explorer/__init__.py index 62718d6938e..319f7e4389b 100644 --- a/homeassistant/components/azure_data_explorer/__init__.py +++ b/homeassistant/components/azure_data_explorer/__init__.py @@ -62,13 +62,12 @@ async def async_setup(hass: HomeAssistant, yaml_config: ConfigType) -> bool: Adds an empty filter to hass data. Tries to get a filter from yaml, if present set to hass data. - If config is empty after getting the filter, return, otherwise emit - deprecated warning and pass the rest to the config flow. """ - hass.data.setdefault(DOMAIN, {DATA_FILTER: {}}) + hass.data.setdefault(DOMAIN, {DATA_FILTER: FILTER_SCHEMA({})}) if DOMAIN in yaml_config: - hass.data[DOMAIN][DATA_FILTER] = yaml_config[DOMAIN][CONF_FILTER] + hass.data[DOMAIN][DATA_FILTER] = yaml_config[DOMAIN].pop(CONF_FILTER) + return True @@ -207,6 +206,6 @@ class AzureDataExplorer: if "\n" in state.state: return None, dropped + 1 - json_event = str(json.dumps(obj=state, cls=JSONEncoder).encode("utf-8")) + json_event = json.dumps(obj=state, cls=JSONEncoder) return (json_event, dropped) diff --git a/homeassistant/components/azure_data_explorer/client.py b/homeassistant/components/azure_data_explorer/client.py index 40528bc6a6f..88609ff8e10 100644 --- a/homeassistant/components/azure_data_explorer/client.py +++ b/homeassistant/components/azure_data_explorer/client.py @@ -23,7 +23,7 @@ from .const import ( CONF_APP_REG_ID, CONF_APP_REG_SECRET, CONF_AUTHORITY_ID, - CONF_USE_FREE, + CONF_USE_QUEUED_CLIENT, ) _LOGGER = logging.getLogger(__name__) @@ -35,7 +35,6 @@ class AzureDataExplorerClient: def __init__(self, data: Mapping[str, Any]) -> None: """Create the right class.""" - self._cluster_ingest_uri = data[CONF_ADX_CLUSTER_INGEST_URI] self._database = data[CONF_ADX_DATABASE_NAME] self._table = data[CONF_ADX_TABLE_NAME] self._ingestion_properties = IngestionProperties( @@ -45,24 +44,36 @@ class AzureDataExplorerClient: ingestion_mapping_reference="ha_json_mapping", ) - # Create cLient for ingesting and querying data - kcsb = KustoConnectionStringBuilder.with_aad_application_key_authentication( - self._cluster_ingest_uri, - data[CONF_APP_REG_ID], - data[CONF_APP_REG_SECRET], - data[CONF_AUTHORITY_ID], + # Create client for ingesting data + kcsb_ingest = ( + KustoConnectionStringBuilder.with_aad_application_key_authentication( + data[CONF_ADX_CLUSTER_INGEST_URI], + data[CONF_APP_REG_ID], + data[CONF_APP_REG_SECRET], + data[CONF_AUTHORITY_ID], + ) ) - if data[CONF_USE_FREE] is True: - # Queded is the only option supported on free tear of ADX - self.write_client = QueuedIngestClient(kcsb) - else: - self.write_client = ManagedStreamingIngestClient.from_dm_kcsb(kcsb) + # Create client for querying data + kcsb_query = ( + KustoConnectionStringBuilder.with_aad_application_key_authentication( + data[CONF_ADX_CLUSTER_INGEST_URI].replace("ingest-", ""), + data[CONF_APP_REG_ID], + data[CONF_APP_REG_SECRET], + data[CONF_AUTHORITY_ID], + ) + ) - self.query_client = KustoClient(kcsb) + if data[CONF_USE_QUEUED_CLIENT] is True: + # Queded is the only option supported on free tear of ADX + self.write_client = QueuedIngestClient(kcsb_ingest) + else: + self.write_client = ManagedStreamingIngestClient.from_dm_kcsb(kcsb_ingest) + + self.query_client = KustoClient(kcsb_query) def test_connection(self) -> None: - """Test connection, will throw Exception when it cannot connect.""" + """Test connection, will throw Exception if it cannot connect.""" query = f"{self._table} | take 1" diff --git a/homeassistant/components/azure_data_explorer/config_flow.py b/homeassistant/components/azure_data_explorer/config_flow.py index d8390246b41..4ffb5ea7cf7 100644 --- a/homeassistant/components/azure_data_explorer/config_flow.py +++ b/homeassistant/components/azure_data_explorer/config_flow.py @@ -10,6 +10,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.config_entries import ConfigFlowResult +from homeassistant.helpers.selector import BooleanSelector from . import AzureDataExplorerClient from .const import ( @@ -19,7 +20,7 @@ from .const import ( CONF_APP_REG_ID, CONF_APP_REG_SECRET, CONF_AUTHORITY_ID, - CONF_USE_FREE, + CONF_USE_QUEUED_CLIENT, DEFAULT_OPTIONS, DOMAIN, ) @@ -34,7 +35,7 @@ STEP_USER_DATA_SCHEMA = vol.Schema( vol.Required(CONF_APP_REG_ID): str, vol.Required(CONF_APP_REG_SECRET): str, vol.Required(CONF_AUTHORITY_ID): str, - vol.Optional(CONF_USE_FREE, default=False): bool, + vol.Required(CONF_USE_QUEUED_CLIENT, default=False): BooleanSelector(), } ) diff --git a/homeassistant/components/azure_data_explorer/const.py b/homeassistant/components/azure_data_explorer/const.py index ca98110597a..a88a6b8b94f 100644 --- a/homeassistant/components/azure_data_explorer/const.py +++ b/homeassistant/components/azure_data_explorer/const.py @@ -17,7 +17,7 @@ CONF_AUTHORITY_ID = "authority_id" CONF_SEND_INTERVAL = "send_interval" CONF_MAX_DELAY = "max_delay" CONF_FILTER = DATA_FILTER = "filter" -CONF_USE_FREE = "use_queued_ingestion" +CONF_USE_QUEUED_CLIENT = "use_queued_ingestion" DATA_HUB = "hub" STEP_USER = "user" diff --git a/homeassistant/components/azure_data_explorer/strings.json b/homeassistant/components/azure_data_explorer/strings.json index 64005872579..c8ec158a844 100644 --- a/homeassistant/components/azure_data_explorer/strings.json +++ b/homeassistant/components/azure_data_explorer/strings.json @@ -3,15 +3,19 @@ "step": { "user": { "title": "Setup your Azure Data Explorer integration", - "description": "Enter connection details.", + "description": "Enter connection details", "data": { - "cluster_ingest_uri": "Cluster ingest URI", - "database": "Database name", - "table": "Table name", + "cluster_ingest_uri": "Cluster Ingest URI", + "authority_id": "Authority ID", "client_id": "Client ID", "client_secret": "Client secret", - "authority_id": "Authority ID", + "database": "Database name", + "table": "Table name", "use_queued_ingestion": "Use queued ingestion" + }, + "data_description": { + "cluster_ingest_uri": "Ingest-URI of the cluster", + "use_queued_ingestion": "Must be enabled when using ADX free cluster" } } }, diff --git a/tests/components/azure_data_explorer/const.py b/tests/components/azure_data_explorer/const.py index d29f4d5ba93..d20be1584a1 100644 --- a/tests/components/azure_data_explorer/const.py +++ b/tests/components/azure_data_explorer/const.py @@ -8,7 +8,7 @@ from homeassistant.components.azure_data_explorer.const import ( CONF_APP_REG_SECRET, CONF_AUTHORITY_ID, CONF_SEND_INTERVAL, - CONF_USE_FREE, + CONF_USE_QUEUED_CLIENT, ) AZURE_DATA_EXPLORER_PATH = "homeassistant.components.azure_data_explorer" @@ -29,7 +29,7 @@ BASE_CONFIG_URI = { } BASIC_OPTIONS = { - CONF_USE_FREE: False, + CONF_USE_QUEUED_CLIENT: False, CONF_SEND_INTERVAL: 5, } @@ -39,10 +39,10 @@ BASE_CONFIG_FULL = BASE_CONFIG | BASIC_OPTIONS | BASE_CONFIG_URI BASE_CONFIG_IMPORT = { CONF_ADX_CLUSTER_INGEST_URI: "https://cluster.region.kusto.windows.net", - CONF_USE_FREE: False, + CONF_USE_QUEUED_CLIENT: False, CONF_SEND_INTERVAL: 5, } -FREE_OPTIONS = {CONF_USE_FREE: True, CONF_SEND_INTERVAL: 5} +FREE_OPTIONS = {CONF_USE_QUEUED_CLIENT: True, CONF_SEND_INTERVAL: 5} BASE_CONFIG_FREE = BASE_CONFIG | FREE_OPTIONS From b84ea1edeb2b348af9e55b2ca152e6ceb7420d6d Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Tue, 11 Jun 2024 09:22:55 +0200 Subject: [PATCH 0510/1445] Bump `imgw-pib` backend library to version 1.0.5 (#119360) Co-authored-by: Maciej Bieniek <478555+bieniu@users.noreply.github.com> --- homeassistant/components/imgw_pib/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/imgw_pib/manifest.json b/homeassistant/components/imgw_pib/manifest.json index fe714691f13..08946a802f1 100644 --- a/homeassistant/components/imgw_pib/manifest.json +++ b/homeassistant/components/imgw_pib/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/imgw_pib", "iot_class": "cloud_polling", "quality_scale": "platinum", - "requirements": ["imgw_pib==1.0.4"] + "requirements": ["imgw_pib==1.0.5"] } diff --git a/requirements_all.txt b/requirements_all.txt index b73060a23d1..6929009ced0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1149,7 +1149,7 @@ iglo==1.2.7 ihcsdk==2.8.5 # homeassistant.components.imgw_pib -imgw_pib==1.0.4 +imgw_pib==1.0.5 # homeassistant.components.incomfort incomfort-client==0.6.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7de35382dcd..fe280ef080d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -936,7 +936,7 @@ idasen-ha==2.5.3 ifaddr==0.2.0 # homeassistant.components.imgw_pib -imgw_pib==1.0.4 +imgw_pib==1.0.5 # homeassistant.components.incomfort incomfort-client==0.6.2 From fc915dc1bf341b27f90925df5749278789218822 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Tue, 11 Jun 2024 09:26:44 +0200 Subject: [PATCH 0511/1445] Calculate attributes when entity information available in Group sensor (#119021) --- homeassistant/components/group/sensor.py | 32 +++++++++++++++- tests/components/group/test_sensor.py | 49 ++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/group/sensor.py b/homeassistant/components/group/sensor.py index 203b1b3fc8e..2e6c321be1e 100644 --- a/homeassistant/components/group/sensor.py +++ b/homeassistant/components/group/sensor.py @@ -36,7 +36,14 @@ from homeassistant.const import ( STATE_UNAVAILABLE, STATE_UNKNOWN, ) -from homeassistant.core import HomeAssistant, State, callback +from homeassistant.core import ( + CALLBACK_TYPE, + Event, + EventStateChangedData, + HomeAssistant, + State, + callback, +) from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv, entity_registry as er from homeassistant.helpers.entity import ( @@ -45,6 +52,7 @@ from homeassistant.helpers.entity import ( get_unit_of_measurement, ) from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.event import async_track_state_change_event from homeassistant.helpers.issue_registry import ( IssueSeverity, async_create_issue, @@ -329,6 +337,7 @@ class SensorGroup(GroupEntity, SensorEntity): self._native_unit_of_measurement = unit_of_measurement self._valid_units: set[str | None] = set() self._can_convert: bool = False + self.calculate_attributes_later: CALLBACK_TYPE | None = None self._attr_name = name if name == DEFAULT_NAME: self._attr_name = f"{DEFAULT_NAME} {sensor_type}".capitalize() @@ -345,13 +354,32 @@ class SensorGroup(GroupEntity, SensorEntity): async def async_added_to_hass(self) -> None: """When added to hass.""" + for entity_id in self._entity_ids: + if self.hass.states.get(entity_id) is None: + self.calculate_attributes_later = async_track_state_change_event( + self.hass, self._entity_ids, self.calculate_state_attributes + ) + break + if not self.calculate_attributes_later: + await self.calculate_state_attributes() + await super().async_added_to_hass() + + async def calculate_state_attributes( + self, event: Event[EventStateChangedData] | None = None + ) -> None: + """Calculate state attributes.""" + for entity_id in self._entity_ids: + if self.hass.states.get(entity_id) is None: + return + if self.calculate_attributes_later: + self.calculate_attributes_later() + self.calculate_attributes_later = None self._attr_state_class = self._calculate_state_class(self._state_class) self._attr_device_class = self._calculate_device_class(self._device_class) self._attr_native_unit_of_measurement = self._calculate_unit_of_measurement( self._native_unit_of_measurement ) self._valid_units = self._get_valid_units() - await super().async_added_to_hass() @callback def async_update_group_state(self) -> None: diff --git a/tests/components/group/test_sensor.py b/tests/components/group/test_sensor.py index c5331aa2f60..db642506361 100644 --- a/tests/components/group/test_sensor.py +++ b/tests/components/group/test_sensor.py @@ -763,3 +763,52 @@ async def test_last_sensor(hass: HomeAssistant) -> None: state = hass.states.get("sensor.test_last") assert str(float(value)) == state.state assert entity_id == state.attributes.get("last_entity_id") + + +async def test_sensors_attributes_added_when_entity_info_available( + hass: HomeAssistant, +) -> None: + """Test the sensor calculate attributes once all entities attributes are available.""" + config = { + SENSOR_DOMAIN: { + "platform": GROUP_DOMAIN, + "name": DEFAULT_NAME, + "type": "sum", + "entities": ["sensor.test_1", "sensor.test_2", "sensor.test_3"], + "unique_id": "very_unique_id", + } + } + + entity_ids = config["sensor"]["entities"] + + assert await async_setup_component(hass, "sensor", config) + await hass.async_block_till_done() + + state = hass.states.get("sensor.sensor_group_sum") + + assert state.state == STATE_UNAVAILABLE + assert state.attributes.get(ATTR_ENTITY_ID) is None + assert state.attributes.get(ATTR_DEVICE_CLASS) is None + assert state.attributes.get(ATTR_STATE_CLASS) is None + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None + + for entity_id, value in dict(zip(entity_ids, VALUES, strict=False)).items(): + hass.states.async_set( + entity_id, + value, + { + ATTR_DEVICE_CLASS: SensorDeviceClass.VOLUME, + ATTR_STATE_CLASS: SensorStateClass.TOTAL, + ATTR_UNIT_OF_MEASUREMENT: "L", + }, + ) + await hass.async_block_till_done() + + state = hass.states.get("sensor.sensor_group_sum") + + assert float(state.state) == pytest.approx(float(SUM_VALUE)) + assert state.attributes.get(ATTR_ENTITY_ID) == entity_ids + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.VOLUME + assert state.attributes.get(ATTR_ICON) is None + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.TOTAL + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "L" From a3356f4ee6602ed8179829acd3d04bc0b5825070 Mon Sep 17 00:00:00 2001 From: Jirka Date: Tue, 11 Jun 2024 09:36:12 +0200 Subject: [PATCH 0512/1445] Fix typo in Tibber service description (#119354) --- homeassistant/components/tibber/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/tibber/strings.json b/homeassistant/components/tibber/strings.json index 00a9efe342a..8d73d435c8c 100644 --- a/homeassistant/components/tibber/strings.json +++ b/homeassistant/components/tibber/strings.json @@ -86,7 +86,7 @@ }, "services": { "get_prices": { - "name": "Get enegry prices", + "name": "Get energy prices", "description": "Get hourly energy prices from Tibber", "fields": { "start": { From 18f30d2ee9b306f093126b894a676e312a3d4b4c Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 11 Jun 2024 11:57:54 +0200 Subject: [PATCH 0513/1445] Fix pointless-string-statement pylint warning in emulated_hue tests (#119368) --- tests/components/emulated_hue/test_upnp.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/components/emulated_hue/test_upnp.py b/tests/components/emulated_hue/test_upnp.py index c1469b29bf4..3522f7e8047 100644 --- a/tests/components/emulated_hue/test_upnp.py +++ b/tests/components/emulated_hue/test_upnp.py @@ -27,7 +27,7 @@ BRIDGE_SERVER_PORT = get_test_instance_port() class MockTransport: """Mock asyncio transport.""" - def __init__(self): + def __init__(self) -> None: """Create a place to store the sends.""" self.sends = [] @@ -63,7 +63,7 @@ def hue_client( yield client -async def setup_hue(hass): +async def setup_hue(hass: HomeAssistant) -> None: """Set up the emulated_hue integration.""" with patch( "homeassistant.components.emulated_hue.async_create_upnp_datagram_endpoint" @@ -82,7 +82,7 @@ def test_upnp_discovery_basic() -> None: mock_transport = MockTransport() upnp_responder_protocol.transport = mock_transport - """Original request emitted by the Hue Bridge v1 app.""" + # Original request emitted by the Hue Bridge v1 app. request = """M-SEARCH * HTTP/1.1 HOST:239.255.255.250:1900 ST:ssdp:all @@ -114,7 +114,7 @@ def test_upnp_discovery_rootdevice() -> None: mock_transport = MockTransport() upnp_responder_protocol.transport = mock_transport - """Original request emitted by Busch-Jaeger free@home SysAP.""" + # Original request emitted by Busch-Jaeger free@home SysAP. request = """M-SEARCH * HTTP/1.1 HOST: 239.255.255.250:1900 MAN: "ssdp:discover" @@ -146,7 +146,7 @@ def test_upnp_no_response() -> None: mock_transport = MockTransport() upnp_responder_protocol.transport = mock_transport - """Original request emitted by the Hue Bridge v1 app.""" + # Original request emitted by the Hue Bridge v1 app. request = """INVALID * HTTP/1.1 HOST:239.255.255.250:1900 ST:ssdp:all @@ -158,7 +158,7 @@ MX:3 upnp_responder_protocol.datagram_received(encoded_request, 1234) - assert mock_transport.sends == [] + assert not mock_transport.sends async def test_description_xml(hass: HomeAssistant, hue_client) -> None: From 572700a326b80cfa48df0687182c3f35d44bcbee Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 11 Jun 2024 12:57:43 +0200 Subject: [PATCH 0514/1445] Ignore c-extension-no-member pylint warnings in tests (#119378) --- tests/components/bluetooth/test_init.py | 2 +- tests/components/stream/test_hls.py | 1 + tests/components/stream/test_worker.py | 2 ++ tests/conftest.py | 3 ++- 4 files changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/components/bluetooth/test_init.py b/tests/components/bluetooth/test_init.py index f132a6aa150..bd38c9cfbae 100644 --- a/tests/components/bluetooth/test_init.py +++ b/tests/components/bluetooth/test_init.py @@ -129,7 +129,7 @@ async def test_setup_and_stop_passive( assert init_kwargs == { "adapter": "hci0", - "bluez": scanner.PASSIVE_SCANNER_ARGS, + "bluez": scanner.PASSIVE_SCANNER_ARGS, # pylint: disable=c-extension-no-member "scanning_mode": "passive", "detection_callback": ANY, } diff --git a/tests/components/stream/test_hls.py b/tests/components/stream/test_hls.py index 4b2d2a3cd61..6d0b1e12ab8 100644 --- a/tests/components/stream/test_hls.py +++ b/tests/components/stream/test_hls.py @@ -309,6 +309,7 @@ async def test_stream_retries( def av_open_side_effect(*args, **kwargs): hass.loop.call_soon_threadsafe(futures.pop().set_result, None) + # pylint: disable-next=c-extension-no-member raise av.error.InvalidDataError(-2, "error") with ( diff --git a/tests/components/stream/test_worker.py b/tests/components/stream/test_worker.py index c8f3f22196f..2cb90c5ee9a 100644 --- a/tests/components/stream/test_worker.py +++ b/tests/components/stream/test_worker.py @@ -342,6 +342,7 @@ async def test_stream_open_fails(hass: HomeAssistant) -> None: ) stream.add_provider(HLS_PROVIDER) with patch("av.open") as av_open: + # pylint: disable-next=c-extension-no-member av_open.side_effect = av.error.InvalidDataError(-2, "error") with pytest.raises(StreamWorkerError): run_worker(hass, stream, STREAM_SOURCE) @@ -770,6 +771,7 @@ async def test_worker_log( stream.add_provider(HLS_PROVIDER) with patch("av.open") as av_open: + # pylint: disable-next=c-extension-no-member av_open.side_effect = av.error.InvalidDataError(-2, "error") with pytest.raises(StreamWorkerError) as err: run_worker(hass, stream, stream_url) diff --git a/tests/conftest.py b/tests/conftest.py index dee98ecd3b8..01607484d70 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1686,10 +1686,11 @@ def mock_bleak_scanner_start() -> Generator[MagicMock]: # We need to drop the stop method from the object since we patched # out start and this fixture will expire before the stop method is called # when EVENT_HOMEASSISTANT_STOP is fired. + # pylint: disable-next=c-extension-no-member bluetooth_scanner.OriginalBleakScanner.stop = AsyncMock() # type: ignore[assignment] with ( patch.object( - bluetooth_scanner.OriginalBleakScanner, + bluetooth_scanner.OriginalBleakScanner, # pylint: disable=c-extension-no-member "start", ) as mock_bleak_scanner_start, patch.object(bluetooth_scanner, "HaScanner"), From 904b89df808d7a53cb5c70c67e0a43a905399a7d Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 11 Jun 2024 13:48:12 +0200 Subject: [PATCH 0515/1445] Allow importing typing helper in core files (#119377) * Allow importing typing helper in core files * Really fix the circular import * Update test --- homeassistant/core.py | 26 +++++++++++----------- homeassistant/helpers/deprecation.py | 28 +++++++++++++++++++++--- homeassistant/helpers/typing.py | 32 +++++++++++++++------------- homeassistant/loader.py | 16 ++++++-------- tests/helpers/test_deprecation.py | 11 +++++++--- 5 files changed, 69 insertions(+), 44 deletions(-) diff --git a/homeassistant/core.py b/homeassistant/core.py index 7aa823dc042..108248c9e83 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -96,6 +96,7 @@ from .helpers.deprecation import ( dir_with_deprecated_constants, ) from .helpers.json import json_bytes, json_fragment +from .helpers.typing import UNDEFINED, UndefinedType from .util import dt as dt_util, location from .util.async_ import ( cancelling, @@ -131,8 +132,6 @@ FINAL_WRITE_STAGE_SHUTDOWN_TIMEOUT = 60 CLOSE_STAGE_SHUTDOWN_TIMEOUT = 30 -# Internal; not helpers.typing.UNDEFINED due to circular dependency -_UNDEF: dict[Any, Any] = {} _SENTINEL = object() _DataT = TypeVar("_DataT", bound=Mapping[str, Any], default=Mapping[str, Any]) type CALLBACK_TYPE = Callable[[], None] @@ -3035,11 +3034,10 @@ class Config: unit_system: str | None = None, location_name: str | None = None, time_zone: str | None = None, - # pylint: disable=dangerous-default-value # _UNDEFs not modified - external_url: str | dict[Any, Any] | None = _UNDEF, - internal_url: str | dict[Any, Any] | None = _UNDEF, + external_url: str | UndefinedType | None = UNDEFINED, + internal_url: str | UndefinedType | None = UNDEFINED, currency: str | None = None, - country: str | dict[Any, Any] | None = _UNDEF, + country: str | UndefinedType | None = UNDEFINED, language: str | None = None, ) -> None: """Update the configuration from a dictionary.""" @@ -3059,14 +3057,14 @@ class Config: self.location_name = location_name if time_zone is not None: await self.async_set_time_zone(time_zone) - if external_url is not _UNDEF: - self.external_url = cast(str | None, external_url) - if internal_url is not _UNDEF: - self.internal_url = cast(str | None, internal_url) + if external_url is not UNDEFINED: + self.external_url = external_url + if internal_url is not UNDEFINED: + self.internal_url = internal_url if currency is not None: self.currency = currency - if country is not _UNDEF: - self.country = cast(str | None, country) + if country is not UNDEFINED: + self.country = country if language is not None: self.language = language @@ -3112,8 +3110,8 @@ class Config: unit_system=data.get("unit_system_v2"), location_name=data.get("location_name"), time_zone=data.get("time_zone"), - external_url=data.get("external_url", _UNDEF), - internal_url=data.get("internal_url", _UNDEF), + external_url=data.get("external_url", UNDEFINED), + internal_url=data.get("internal_url", UNDEFINED), currency=data.get("currency"), country=data.get("country"), language=data.get("language"), diff --git a/homeassistant/helpers/deprecation.py b/homeassistant/helpers/deprecation.py index 82ff136332b..65e8f4ef97e 100644 --- a/homeassistant/helpers/deprecation.py +++ b/homeassistant/helpers/deprecation.py @@ -242,6 +242,26 @@ class DeprecatedAlias(NamedTuple): breaks_in_ha_version: str | None +class DeferredDeprecatedAlias: + """Deprecated alias with deferred evaluation of the value.""" + + def __init__( + self, + value_fn: Callable[[], Any], + replacement: str, + breaks_in_ha_version: str | None, + ) -> None: + """Initialize.""" + self.breaks_in_ha_version = breaks_in_ha_version + self.replacement = replacement + self._value_fn = value_fn + + @functools.cached_property + def value(self) -> Any: + """Return the value.""" + return self._value_fn() + + _PREFIX_DEPRECATED = "_DEPRECATED_" @@ -266,7 +286,7 @@ def check_if_deprecated_constant(name: str, module_globals: dict[str, Any]) -> A f"{deprecated_const.enum.__class__.__name__}.{deprecated_const.enum.name}" ) breaks_in_ha_version = deprecated_const.breaks_in_ha_version - elif isinstance(deprecated_const, DeprecatedAlias): + elif isinstance(deprecated_const, (DeprecatedAlias, DeferredDeprecatedAlias)): description = "alias" value = deprecated_const.value replacement = deprecated_const.replacement @@ -274,8 +294,10 @@ def check_if_deprecated_constant(name: str, module_globals: dict[str, Any]) -> A if value is None or replacement is None: msg = ( - f"Value of {_PREFIX_DEPRECATED}{name} is an instance of {type(deprecated_const)} " - "but an instance of DeprecatedConstant or DeprecatedConstantEnum is required" + f"Value of {_PREFIX_DEPRECATED}{name} is an instance of " + f"{type(deprecated_const)} but an instance of DeprecatedAlias, " + "DeferredDeprecatedAlias, DeprecatedConstant or DeprecatedConstantEnum " + "is required" ) logging.getLogger(module_name).debug(msg) diff --git a/homeassistant/helpers/typing.py b/homeassistant/helpers/typing.py index 13c54862b8d..3cdd9ec9250 100644 --- a/homeassistant/helpers/typing.py +++ b/homeassistant/helpers/typing.py @@ -5,10 +5,8 @@ from enum import Enum from functools import partial from typing import Any, Never -import homeassistant.core - from .deprecation import ( - DeprecatedAlias, + DeferredDeprecatedAlias, all_with_deprecated_constants, check_if_deprecated_constant, dir_with_deprecated_constants, @@ -35,23 +33,27 @@ class UndefinedType(Enum): UNDEFINED = UndefinedType._singleton # noqa: SLF001 +def _deprecated_typing_helper(attr: str) -> DeferredDeprecatedAlias: + """Help to make a DeferredDeprecatedAlias.""" + + def value_fn() -> Any: + # pylint: disable-next=import-outside-toplevel + import homeassistant.core + + return getattr(homeassistant.core, attr) + + return DeferredDeprecatedAlias(value_fn, f"homeassistant.core.{attr}", "2025.5") + + # The following types should not used and # are not present in the core code base. # They are kept in order not to break custom integrations # that may rely on them. # Deprecated as of 2024.5 use types from homeassistant.core instead. -_DEPRECATED_ContextType = DeprecatedAlias( - homeassistant.core.Context, "homeassistant.core.Context", "2025.5" -) -_DEPRECATED_EventType = DeprecatedAlias( - homeassistant.core.Event, "homeassistant.core.Event", "2025.5" -) -_DEPRECATED_HomeAssistantType = DeprecatedAlias( - homeassistant.core.HomeAssistant, "homeassistant.core.HomeAssistant", "2025.5" -) -_DEPRECATED_ServiceCallType = DeprecatedAlias( - homeassistant.core.ServiceCall, "homeassistant.core.ServiceCall", "2025.5" -) +_DEPRECATED_ContextType = _deprecated_typing_helper("Context") +_DEPRECATED_EventType = _deprecated_typing_helper("Event") +_DEPRECATED_HomeAssistantType = _deprecated_typing_helper("HomeAssistant") +_DEPRECATED_ServiceCallType = _deprecated_typing_helper("ServiceCall") # These can be removed if no deprecated constant are in this module anymore __getattr__ = partial(check_if_deprecated_constant, module_globals=globals()) diff --git a/homeassistant/loader.py b/homeassistant/loader.py index 542f9d4f009..9afad610420 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -40,6 +40,7 @@ from .generated.ssdp import SSDP from .generated.usb import USB from .generated.zeroconf import HOMEKIT, ZEROCONF from .helpers.json import json_bytes, json_fragment +from .helpers.typing import UNDEFINED from .util.hass_dict import HassKey from .util.json import JSON_DECODE_EXCEPTIONS, json_loads @@ -129,9 +130,6 @@ IMPORT_EVENT_LOOP_WARNING = ( "experience issues with Home Assistant" ) -_UNDEF = object() # Internal; not helpers.typing.UNDEFINED due to circular dependency - - MOVED_ZEROCONF_PROPS = ("macaddress", "model", "manufacturer") @@ -1322,7 +1320,7 @@ def async_get_loaded_integration(hass: HomeAssistant, domain: str) -> Integratio Raises IntegrationNotLoaded if the integration is not loaded. """ cache = hass.data[DATA_INTEGRATIONS] - int_or_fut = cache.get(domain, _UNDEF) + int_or_fut = cache.get(domain, UNDEFINED) # Integration is never subclassed, so we can check for type if type(int_or_fut) is Integration: return int_or_fut @@ -1332,7 +1330,7 @@ def async_get_loaded_integration(hass: HomeAssistant, domain: str) -> Integratio async def async_get_integration(hass: HomeAssistant, domain: str) -> Integration: """Get integration.""" cache = hass.data[DATA_INTEGRATIONS] - if type(int_or_fut := cache.get(domain, _UNDEF)) is Integration: + if type(int_or_fut := cache.get(domain, UNDEFINED)) is Integration: return int_or_fut integrations_or_excs = await async_get_integrations(hass, [domain]) int_or_exc = integrations_or_excs[domain] @@ -1350,11 +1348,11 @@ async def async_get_integrations( needed: dict[str, asyncio.Future[None]] = {} in_progress: dict[str, asyncio.Future[None]] = {} for domain in domains: - int_or_fut = cache.get(domain, _UNDEF) + int_or_fut = cache.get(domain, UNDEFINED) # Integration is never subclassed, so we can check for type if type(int_or_fut) is Integration: results[domain] = int_or_fut - elif int_or_fut is not _UNDEF: + elif int_or_fut is not UNDEFINED: in_progress[domain] = cast(asyncio.Future[None], int_or_fut) elif "." in domain: results[domain] = ValueError(f"Invalid domain {domain}") @@ -1364,10 +1362,10 @@ async def async_get_integrations( if in_progress: await asyncio.wait(in_progress.values()) for domain in in_progress: - # When we have waited and it's _UNDEF, it doesn't exist + # When we have waited and it's UNDEFINED, it doesn't exist # We don't cache that it doesn't exist, or else people can't fix it # and then restart, because their config will never be valid. - if (int_or_fut := cache.get(domain, _UNDEF)) is _UNDEF: + if (int_or_fut := cache.get(domain, UNDEFINED)) is UNDEFINED: results[domain] = IntegrationNotFound(domain) else: results[domain] = cast(Integration, int_or_fut) diff --git a/tests/helpers/test_deprecation.py b/tests/helpers/test_deprecation.py index fed48c5735b..b48e70eff82 100644 --- a/tests/helpers/test_deprecation.py +++ b/tests/helpers/test_deprecation.py @@ -483,14 +483,19 @@ def test_check_if_deprecated_constant_integration_not_found( def test_test_check_if_deprecated_constant_invalid( caplog: pytest.LogCaptureFixture, ) -> None: - """Test check_if_deprecated_constant will raise an attribute error and create an log entry on an invalid deprecation type.""" + """Test check_if_deprecated_constant error handling. + + Test check_if_deprecated_constant raises an attribute error and creates a log entry + on an invalid deprecation type. + """ module_name = "homeassistant.components.hue.light" module_globals = {"__name__": module_name, "_DEPRECATED_TEST_CONSTANT": 1} name = "TEST_CONSTANT" excepted_msg = ( - f"Value of _DEPRECATED_{name} is an instance of " - "but an instance of DeprecatedConstant or DeprecatedConstantEnum is required" + f"Value of _DEPRECATED_{name} is an instance of but an instance " + "of DeprecatedAlias, DeferredDeprecatedAlias, DeprecatedConstant or " + "DeprecatedConstantEnum is required" ) with pytest.raises(AttributeError, match=excepted_msg): From 27fe00125d429bcc71d3db9f34fbb863620af015 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 11 Jun 2024 14:01:23 +0200 Subject: [PATCH 0516/1445] Fix typo in auth (#119388) --- homeassistant/auth/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/auth/__init__.py b/homeassistant/auth/__init__.py index 0b749766263..c39657b6147 100644 --- a/homeassistant/auth/__init__.py +++ b/homeassistant/auth/__init__.py @@ -53,7 +53,7 @@ async def auth_manager_from_config( ) -> AuthManager: """Initialize an auth manager from config. - CORE_CONFIG_SCHEMA will make sure do duplicated auth providers or + CORE_CONFIG_SCHEMA will make sure no duplicated auth providers or mfa modules exist in configs. """ store = auth_store.AuthStore(hass) From f9cf7598da7baa6aafa7b73b5371c9d9f46f3ba3 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 11 Jun 2024 14:13:12 +0200 Subject: [PATCH 0517/1445] Fix missing checks in core config test (#119387) --- tests/components/config/test_core.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/components/config/test_core.py b/tests/components/config/test_core.py index b351493dac7..366a3d31b9b 100644 --- a/tests/components/config/test_core.py +++ b/tests/components/config/test_core.py @@ -165,6 +165,8 @@ async def test_websocket_core_update(hass: HomeAssistant, client) -> None: assert hass.config.external_url == "https://www.example.com" assert hass.config.internal_url == "http://example.local" assert hass.config.currency == "USD" + assert hass.config.country == "SE" + assert hass.config.language == "sv" assert len(mock_set_tz.mock_calls) == 1 assert mock_set_tz.mock_calls[0][1][0] == dt_util.get_time_zone("America/New_York") From 43db0e457cbe0b6e466f8503a1f3bc45c4ba6905 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 11 Jun 2024 14:56:53 +0200 Subject: [PATCH 0518/1445] Fix pylint warnings in xiaomi tests (#119373) --- tests/components/xiaomi/test_device_tracker.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/components/xiaomi/test_device_tracker.py b/tests/components/xiaomi/test_device_tracker.py index 1b1d898add1..975e666af68 100644 --- a/tests/components/xiaomi/test_device_tracker.py +++ b/tests/components/xiaomi/test_device_tracker.py @@ -28,7 +28,7 @@ def mocked_requests(*args, **kwargs): class MockResponse: """Class to represent a mocked response.""" - def __init__(self, json_data, status_code): + def __init__(self, json_data, status_code) -> None: """Initialize the mock response class.""" self.json_data = json_data self.status_code = status_code @@ -48,6 +48,7 @@ def mocked_requests(*args, **kwargs): raise requests.HTTPError(self.status_code) data = kwargs.get("data") + # pylint: disable-next=global-statement global FIRST_CALL # noqa: PLW0603 if data and data.get("username", None) == INVALID_USERNAME: From 2c7022950c84d7c5a4571c70b8bf133e1e180d98 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 11 Jun 2024 14:57:50 +0200 Subject: [PATCH 0519/1445] Fix import-outside-toplevel pylint warnings in tests (#119389) --- .../components/arcam_fmj/test_media_player.py | 12 +- tests/components/dsmr/test_mbus_migration.py | 21 +-- tests/components/dsmr/test_sensor.py | 131 ++++-------------- tests/components/izone/test_config_flow.py | 3 +- tests/components/litterrobot/test_sensor.py | 2 +- tests/components/mqtt/test_init.py | 3 +- tests/components/recorder/db_schema_0.py | 3 +- tests/components/recorder/test_util.py | 4 +- tests/components/spc/test_init.py | 5 +- tests/components/sun/test_init.py | 5 +- tests/components/upb/test_config_flow.py | 2 +- tests/components/v2c/test_sensor.py | 3 +- 12 files changed, 52 insertions(+), 142 deletions(-) diff --git a/tests/components/arcam_fmj/test_media_player.py b/tests/components/arcam_fmj/test_media_player.py index 0baa8ba6870..1fa67691895 100644 --- a/tests/components/arcam_fmj/test_media_player.py +++ b/tests/components/arcam_fmj/test_media_player.py @@ -6,6 +6,12 @@ from unittest.mock import ANY, PropertyMock, patch from arcam.fmj import ConnectionFailed, DecodeMode2CH, DecodeModeMCH, SourceCodes import pytest +from homeassistant.components.arcam_fmj.const import ( + SIGNAL_CLIENT_DATA, + SIGNAL_CLIENT_STARTED, + SIGNAL_CLIENT_STOPPED, +) +from homeassistant.components.arcam_fmj.media_player import ArcamFmj from homeassistant.components.homeassistant import ( DOMAIN as HA_DOMAIN, SERVICE_UPDATE_ENTITY, @@ -338,7 +344,6 @@ async def test_media_artist(player, state, source, dls, artist) -> None: ) async def test_media_title(player, state, source, channel, title) -> None: """Test media title.""" - from homeassistant.components.arcam_fmj.media_player import ArcamFmj state.get_source.return_value = source with patch.object( @@ -354,11 +359,6 @@ async def test_media_title(player, state, source, channel, title) -> None: async def test_added_to_hass(player, state) -> None: """Test addition to hass.""" - from homeassistant.components.arcam_fmj.const import ( - SIGNAL_CLIENT_DATA, - SIGNAL_CLIENT_STARTED, - SIGNAL_CLIENT_STOPPED, - ) with patch( "homeassistant.components.arcam_fmj.media_player.async_dispatcher_connect" diff --git a/tests/components/dsmr/test_mbus_migration.py b/tests/components/dsmr/test_mbus_migration.py index 429128c48bb..284a0001b89 100644 --- a/tests/components/dsmr/test_mbus_migration.py +++ b/tests/components/dsmr/test_mbus_migration.py @@ -3,6 +3,13 @@ import datetime from decimal import Decimal +from dsmr_parser.obis_references import ( + BELGIUM_MBUS1_DEVICE_TYPE, + BELGIUM_MBUS1_EQUIPMENT_IDENTIFIER, + BELGIUM_MBUS1_METER_READING2, +) +from dsmr_parser.objects import CosemObject, MBusObject + from homeassistant.components.dsmr.const import DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.core import HomeAssistant @@ -20,13 +27,6 @@ async def test_migrate_gas_to_mbus( """Test migration of unique_id.""" (connection_factory, transport, protocol) = dsmr_connection_fixture - from dsmr_parser.obis_references import ( - BELGIUM_MBUS1_DEVICE_TYPE, - BELGIUM_MBUS1_EQUIPMENT_IDENTIFIER, - BELGIUM_MBUS1_METER_READING2, - ) - from dsmr_parser.objects import CosemObject, MBusObject - mock_entry = MockConfigEntry( domain=DOMAIN, unique_id="/dev/ttyUSB0", @@ -118,13 +118,6 @@ async def test_migrate_gas_to_mbus_exists( """Test migration of unique_id.""" (connection_factory, transport, protocol) = dsmr_connection_fixture - from dsmr_parser.obis_references import ( - BELGIUM_MBUS1_DEVICE_TYPE, - BELGIUM_MBUS1_EQUIPMENT_IDENTIFIER, - BELGIUM_MBUS1_METER_READING2, - ) - from dsmr_parser.objects import CosemObject, MBusObject - mock_entry = MockConfigEntry( domain=DOMAIN, unique_id="/dev/ttyUSB0", diff --git a/tests/components/dsmr/test_sensor.py b/tests/components/dsmr/test_sensor.py index 7a38e3010d8..e014fdb68f2 100644 --- a/tests/components/dsmr/test_sensor.py +++ b/tests/components/dsmr/test_sensor.py @@ -11,6 +11,33 @@ from decimal import Decimal from itertools import chain, repeat from unittest.mock import DEFAULT, MagicMock +from dsmr_parser.obis_references import ( + BELGIUM_CURRENT_AVERAGE_DEMAND, + BELGIUM_MAXIMUM_DEMAND_MONTH, + BELGIUM_MBUS1_DEVICE_TYPE, + BELGIUM_MBUS1_EQUIPMENT_IDENTIFIER, + BELGIUM_MBUS1_METER_READING1, + BELGIUM_MBUS1_METER_READING2, + BELGIUM_MBUS2_DEVICE_TYPE, + BELGIUM_MBUS2_EQUIPMENT_IDENTIFIER, + BELGIUM_MBUS2_METER_READING1, + BELGIUM_MBUS2_METER_READING2, + BELGIUM_MBUS3_DEVICE_TYPE, + BELGIUM_MBUS3_EQUIPMENT_IDENTIFIER, + BELGIUM_MBUS3_METER_READING1, + BELGIUM_MBUS3_METER_READING2, + BELGIUM_MBUS4_DEVICE_TYPE, + BELGIUM_MBUS4_EQUIPMENT_IDENTIFIER, + BELGIUM_MBUS4_METER_READING1, + BELGIUM_MBUS4_METER_READING2, + CURRENT_ELECTRICITY_USAGE, + ELECTRICITY_ACTIVE_TARIFF, + ELECTRICITY_EXPORTED_TOTAL, + ELECTRICITY_IMPORTED_TOTAL, + GAS_METER_READING, + HOURLY_GAS_METER_READING, +) +from dsmr_parser.objects import CosemObject, MBusObject import pytest from homeassistant.components.sensor import ( @@ -41,13 +68,6 @@ async def test_default_setup( """Test the default setup.""" (connection_factory, transport, protocol) = dsmr_connection_fixture - from dsmr_parser.obis_references import ( - CURRENT_ELECTRICITY_USAGE, - ELECTRICITY_ACTIVE_TARIFF, - GAS_METER_READING, - ) - from dsmr_parser.objects import CosemObject, MBusObject - entry_data = { "port": "/dev/ttyUSB0", "dsmr_version": "2.2", @@ -176,12 +196,6 @@ async def test_setup_only_energy( """Test the default setup.""" (connection_factory, transport, protocol) = dsmr_connection_fixture - from dsmr_parser.obis_references import ( - CURRENT_ELECTRICITY_USAGE, - ELECTRICITY_ACTIVE_TARIFF, - ) - from dsmr_parser.objects import CosemObject - entry_data = { "port": "/dev/ttyUSB0", "dsmr_version": "2.2", @@ -230,12 +244,6 @@ async def test_v4_meter(hass: HomeAssistant, dsmr_connection_fixture) -> None: """Test if v4 meter is correctly parsed.""" (connection_factory, transport, protocol) = dsmr_connection_fixture - from dsmr_parser.obis_references import ( - ELECTRICITY_ACTIVE_TARIFF, - HOURLY_GAS_METER_READING, - ) - from dsmr_parser.objects import CosemObject, MBusObject - entry_data = { "port": "/dev/ttyUSB0", "dsmr_version": "4", @@ -316,12 +324,6 @@ async def test_v5_meter( """Test if v5 meter is correctly parsed.""" (connection_factory, transport, protocol) = dsmr_connection_fixture - from dsmr_parser.obis_references import ( - ELECTRICITY_ACTIVE_TARIFF, - HOURLY_GAS_METER_READING, - ) - from dsmr_parser.objects import CosemObject, MBusObject - entry_data = { "port": "/dev/ttyUSB0", "dsmr_version": "5", @@ -388,13 +390,6 @@ async def test_luxembourg_meter(hass: HomeAssistant, dsmr_connection_fixture) -> """Test if v5 meter is correctly parsed.""" (connection_factory, transport, protocol) = dsmr_connection_fixture - from dsmr_parser.obis_references import ( - ELECTRICITY_EXPORTED_TOTAL, - ELECTRICITY_IMPORTED_TOTAL, - HOURLY_GAS_METER_READING, - ) - from dsmr_parser.objects import CosemObject, MBusObject - entry_data = { "port": "/dev/ttyUSB0", "dsmr_version": "5L", @@ -477,25 +472,6 @@ async def test_belgian_meter(hass: HomeAssistant, dsmr_connection_fixture) -> No """Test if Belgian meter is correctly parsed.""" (connection_factory, transport, protocol) = dsmr_connection_fixture - from dsmr_parser.obis_references import ( - BELGIUM_CURRENT_AVERAGE_DEMAND, - BELGIUM_MAXIMUM_DEMAND_MONTH, - BELGIUM_MBUS1_DEVICE_TYPE, - BELGIUM_MBUS1_EQUIPMENT_IDENTIFIER, - BELGIUM_MBUS1_METER_READING2, - BELGIUM_MBUS2_DEVICE_TYPE, - BELGIUM_MBUS2_EQUIPMENT_IDENTIFIER, - BELGIUM_MBUS2_METER_READING1, - BELGIUM_MBUS3_DEVICE_TYPE, - BELGIUM_MBUS3_EQUIPMENT_IDENTIFIER, - BELGIUM_MBUS3_METER_READING2, - BELGIUM_MBUS4_DEVICE_TYPE, - BELGIUM_MBUS4_EQUIPMENT_IDENTIFIER, - BELGIUM_MBUS4_METER_READING1, - ELECTRICITY_ACTIVE_TARIFF, - ) - from dsmr_parser.objects import CosemObject, MBusObject - entry_data = { "port": "/dev/ttyUSB0", "dsmr_version": "5B", @@ -679,22 +655,6 @@ async def test_belgian_meter_alt(hass: HomeAssistant, dsmr_connection_fixture) - """Test if Belgian meter is correctly parsed.""" (connection_factory, transport, protocol) = dsmr_connection_fixture - from dsmr_parser.obis_references import ( - BELGIUM_MBUS1_DEVICE_TYPE, - BELGIUM_MBUS1_EQUIPMENT_IDENTIFIER, - BELGIUM_MBUS1_METER_READING1, - BELGIUM_MBUS2_DEVICE_TYPE, - BELGIUM_MBUS2_EQUIPMENT_IDENTIFIER, - BELGIUM_MBUS2_METER_READING2, - BELGIUM_MBUS3_DEVICE_TYPE, - BELGIUM_MBUS3_EQUIPMENT_IDENTIFIER, - BELGIUM_MBUS3_METER_READING1, - BELGIUM_MBUS4_DEVICE_TYPE, - BELGIUM_MBUS4_EQUIPMENT_IDENTIFIER, - BELGIUM_MBUS4_METER_READING2, - ) - from dsmr_parser.objects import CosemObject, MBusObject - entry_data = { "port": "/dev/ttyUSB0", "dsmr_version": "5B", @@ -842,20 +802,6 @@ async def test_belgian_meter_mbus(hass: HomeAssistant, dsmr_connection_fixture) """Test if Belgian meter is correctly parsed.""" (connection_factory, transport, protocol) = dsmr_connection_fixture - from dsmr_parser.obis_references import ( - BELGIUM_MBUS1_DEVICE_TYPE, - BELGIUM_MBUS1_EQUIPMENT_IDENTIFIER, - BELGIUM_MBUS2_DEVICE_TYPE, - BELGIUM_MBUS2_EQUIPMENT_IDENTIFIER, - BELGIUM_MBUS3_DEVICE_TYPE, - BELGIUM_MBUS3_EQUIPMENT_IDENTIFIER, - BELGIUM_MBUS3_METER_READING2, - BELGIUM_MBUS4_DEVICE_TYPE, - BELGIUM_MBUS4_METER_READING1, - ELECTRICITY_ACTIVE_TARIFF, - ) - from dsmr_parser.objects import CosemObject, MBusObject - entry_data = { "port": "/dev/ttyUSB0", "dsmr_version": "5B", @@ -963,9 +909,6 @@ async def test_belgian_meter_low(hass: HomeAssistant, dsmr_connection_fixture) - """Test if Belgian meter is correctly parsed.""" (connection_factory, transport, protocol) = dsmr_connection_fixture - from dsmr_parser.obis_references import ELECTRICITY_ACTIVE_TARIFF - from dsmr_parser.objects import CosemObject - entry_data = { "port": "/dev/ttyUSB0", "dsmr_version": "5B", @@ -1012,12 +955,6 @@ async def test_swedish_meter(hass: HomeAssistant, dsmr_connection_fixture) -> No """Test if v5 meter is correctly parsed.""" (connection_factory, transport, protocol) = dsmr_connection_fixture - from dsmr_parser.obis_references import ( - ELECTRICITY_EXPORTED_TOTAL, - ELECTRICITY_IMPORTED_TOTAL, - ) - from dsmr_parser.objects import CosemObject - entry_data = { "port": "/dev/ttyUSB0", "dsmr_version": "5S", @@ -1084,12 +1021,6 @@ async def test_easymeter(hass: HomeAssistant, dsmr_connection_fixture) -> None: """Test if Q3D meter is correctly parsed.""" (connection_factory, transport, protocol) = dsmr_connection_fixture - from dsmr_parser.obis_references import ( - ELECTRICITY_EXPORTED_TOTAL, - ELECTRICITY_IMPORTED_TOTAL, - ) - from dsmr_parser.objects import CosemObject - entry_data = { "port": "/dev/ttyUSB0", "dsmr_version": "Q3D", @@ -1248,11 +1179,6 @@ async def test_connection_errors_retry( @patch("homeassistant.components.dsmr.sensor.DEFAULT_RECONNECT_INTERVAL", 0) async def test_reconnect(hass: HomeAssistant, dsmr_connection_fixture) -> None: """If transport disconnects, the connection should be retried.""" - from dsmr_parser.obis_references import ( - CURRENT_ELECTRICITY_USAGE, - ELECTRICITY_ACTIVE_TARIFF, - ) - from dsmr_parser.objects import CosemObject (connection_factory, transport, protocol) = dsmr_connection_fixture @@ -1334,9 +1260,6 @@ async def test_gas_meter_providing_energy_reading( """Test that gas providing energy readings use the correct device class.""" (connection_factory, transport, protocol) = dsmr_connection_fixture - from dsmr_parser.obis_references import GAS_METER_READING - from dsmr_parser.objects import MBusObject - entry_data = { "port": "/dev/ttyUSB0", "dsmr_version": "2.2", diff --git a/tests/components/izone/test_config_flow.py b/tests/components/izone/test_config_flow.py index 9f668e1ec62..6591e402ec2 100644 --- a/tests/components/izone/test_config_flow.py +++ b/tests/components/izone/test_config_flow.py @@ -8,6 +8,7 @@ from homeassistant import config_entries from homeassistant.components.izone.const import DISPATCH_CONTROLLER_DISCOVERED, IZONE from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers.dispatcher import async_dispatcher_send @pytest.fixture @@ -20,8 +21,6 @@ def mock_disco(): def _mock_start_discovery(hass, mock_disco): - from homeassistant.helpers.dispatcher import async_dispatcher_send - def do_disovered(*args): async_dispatcher_send(hass, DISPATCH_CONTROLLER_DISCOVERED, True) return mock_disco diff --git a/tests/components/litterrobot/test_sensor.py b/tests/components/litterrobot/test_sensor.py index 8d1f2b68e05..360d13096a7 100644 --- a/tests/components/litterrobot/test_sensor.py +++ b/tests/components/litterrobot/test_sensor.py @@ -4,6 +4,7 @@ from unittest.mock import MagicMock import pytest +from homeassistant.components.litterrobot.sensor import icon_for_gauge_level from homeassistant.components.sensor import DOMAIN as PLATFORM_DOMAIN, SensorDeviceClass from homeassistant.const import PERCENTAGE, STATE_UNKNOWN, UnitOfMass from homeassistant.core import HomeAssistant @@ -47,7 +48,6 @@ async def test_sleep_time_sensor_with_sleep_disabled( async def test_gauge_icon() -> None: """Test icon generator for gauge sensor.""" - from homeassistant.components.litterrobot.sensor import icon_for_gauge_level GAUGE_EMPTY = "mdi:gauge-empty" GAUGE_LOW = "mdi:gauge-low" diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index a780fce83c0..144b2f9cf45 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -12,6 +12,7 @@ import time from typing import Any, TypedDict from unittest.mock import ANY, MagicMock, Mock, call, mock_open, patch +import certifi from freezegun.api import FrozenDateTimeFactory import paho.mqtt.client as paho_mqtt import pytest @@ -2479,8 +2480,6 @@ async def test_setup_uses_certificate_on_certificate_set_to_auto_and_insecure( assert calls - import certifi - expected_certificate = certifi.where() assert calls[0][0] == expected_certificate diff --git a/tests/components/recorder/db_schema_0.py b/tests/components/recorder/db_schema_0.py index 9062de01b59..12336dcc96a 100644 --- a/tests/components/recorder/db_schema_0.py +++ b/tests/components/recorder/db_schema_0.py @@ -19,6 +19,7 @@ from sqlalchemy import ( distinct, ) from sqlalchemy.orm import declarative_base +from sqlalchemy.orm.session import Session from homeassistant.core import Event, EventOrigin, State, split_entity_id from homeassistant.helpers.json import JSONEncoder @@ -141,8 +142,6 @@ class RecorderRuns(Base): # type: ignore[valid-type,misc] Specify point_in_time if you want to know which existed at that point in time inside the run. """ - from sqlalchemy.orm.session import Session - session = Session.object_session(self) assert session is not None, "RecorderRuns need to be persisted" diff --git a/tests/components/recorder/test_util.py b/tests/components/recorder/test_util.py index 974e401264e..d72978c57bb 100644 --- a/tests/components/recorder/test_util.py +++ b/tests/components/recorder/test_util.py @@ -10,7 +10,7 @@ from unittest.mock import MagicMock, Mock, patch import pytest from sqlalchemy import lambda_stmt, text from sqlalchemy.engine.result import ChunkedIteratorResult -from sqlalchemy.exc import SQLAlchemyError +from sqlalchemy.exc import OperationalError, SQLAlchemyError from sqlalchemy.sql.elements import TextClause from sqlalchemy.sql.lambdas import StatementLambdaElement @@ -73,7 +73,6 @@ async def test_session_scope_not_setup( async def test_recorder_bad_execute(hass: HomeAssistant, setup_recorder: None) -> None: """Bad execute, retry 3 times.""" - from sqlalchemy.exc import SQLAlchemyError def to_native(validate_entity_id=True): """Raise exception.""" @@ -854,7 +853,6 @@ async def test_write_lock_db( tmp_path: Path, ) -> None: """Test database write lock.""" - from sqlalchemy.exc import OperationalError # Use file DB, in memory DB cannot do write locks. config = { diff --git a/tests/components/spc/test_init.py b/tests/components/spc/test_init.py index 92c3282dd23..3dfea94a4bd 100644 --- a/tests/components/spc/test_init.py +++ b/tests/components/spc/test_init.py @@ -2,6 +2,9 @@ from unittest.mock import Mock, PropertyMock, patch +import pyspcwebgw +from pyspcwebgw.const import AreaMode + from homeassistant.bootstrap import async_setup_component from homeassistant.components.spc import DATA_API from homeassistant.const import STATE_ALARM_ARMED_AWAY, STATE_ALARM_DISARMED @@ -32,8 +35,6 @@ async def test_invalid_device_config(hass: HomeAssistant, monkeypatch) -> None: async def test_update_alarm_device(hass: HomeAssistant) -> None: """Test that alarm panel state changes on incoming websocket data.""" - import pyspcwebgw - from pyspcwebgw.const import AreaMode config = {"spc": {"api_url": "http://localhost/", "ws_url": "ws://localhost/"}} diff --git a/tests/components/sun/test_init.py b/tests/components/sun/test_init.py index 48a214274c9..a30076d6d3c 100644 --- a/tests/components/sun/test_init.py +++ b/tests/components/sun/test_init.py @@ -3,6 +3,8 @@ from datetime import datetime, timedelta from unittest.mock import patch +from astral import LocationInfo +import astral.sun from freezegun import freeze_time import pytest @@ -25,9 +27,6 @@ async def test_setting_rising(hass: HomeAssistant) -> None: await hass.async_block_till_done() state = hass.states.get(entity.ENTITY_ID) - from astral import LocationInfo - import astral.sun - utc_today = utc_now.date() location = LocationInfo( diff --git a/tests/components/upb/test_config_flow.py b/tests/components/upb/test_config_flow.py index 5eaed2e3a24..d5d6d70bb68 100644 --- a/tests/components/upb/test_config_flow.py +++ b/tests/components/upb/test_config_flow.py @@ -1,5 +1,6 @@ """Test the UPB Control config flow.""" +from asyncio import TimeoutError from unittest.mock import MagicMock, PropertyMock, patch from homeassistant import config_entries @@ -84,7 +85,6 @@ async def test_form_user_with_tcp_upb(hass: HomeAssistant) -> None: async def test_form_cannot_connect(hass: HomeAssistant) -> None: """Test we handle cannot connect error.""" - from asyncio import TimeoutError with patch( "homeassistant.components.upb.config_flow.asyncio.timeout", diff --git a/tests/components/v2c/test_sensor.py b/tests/components/v2c/test_sensor.py index b48a173821c..9e7e3800767 100644 --- a/tests/components/v2c/test_sensor.py +++ b/tests/components/v2c/test_sensor.py @@ -5,6 +5,7 @@ from unittest.mock import AsyncMock, patch import pytest from syrupy import SnapshotAssertion +from homeassistant.components.v2c.sensor import _METER_ERROR_OPTIONS from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -27,8 +28,6 @@ async def test_sensor( await init_integration(hass, mock_config_entry) await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) - from homeassistant.components.v2c.sensor import _METER_ERROR_OPTIONS - assert [ "no_error", "communication", From d376371c2578a754e7fa8ba20e20e05f26edd496 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 11 Jun 2024 14:59:49 +0200 Subject: [PATCH 0520/1445] Fix pylint warnings in testing config custom components (#119370) --- .../custom_components/test/image_processing.py | 12 +++++++++--- .../testing_config/custom_components/test/light.py | 13 ++++++++++--- tests/testing_config/custom_components/test/lock.py | 11 +++++++++-- .../testing_config/custom_components/test/remote.py | 11 +++++++++-- .../testing_config/custom_components/test/switch.py | 11 +++++++++-- .../custom_components/test/weather.py | 1 + .../custom_components/test_embedded/__init__.py | 5 ++++- .../custom_components/test_embedded/switch.py | 11 +++++++++-- .../test_integration_platform/__init__.py | 5 ++++- .../custom_components/test_package/__init__.py | 5 ++++- .../test_package_loaded_executor/__init__.py | 5 ++++- .../test_package_loaded_loop/__init__.py | 5 ++++- .../test_package_raises_cancelled_error/__init__.py | 5 ++++- .../__init__.py | 8 ++++++-- .../custom_components/test_standalone.py | 5 ++++- 15 files changed, 90 insertions(+), 23 deletions(-) diff --git a/tests/testing_config/custom_components/test/image_processing.py b/tests/testing_config/custom_components/test/image_processing.py index 343c60a78fe..fe22325c3e0 100644 --- a/tests/testing_config/custom_components/test/image_processing.py +++ b/tests/testing_config/custom_components/test/image_processing.py @@ -1,11 +1,17 @@ """Provide a mock image processing.""" from homeassistant.components.image_processing import ImageProcessingEntity +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType async def async_setup_platform( - hass, config, async_add_entities_callback, discovery_info=None -): + hass: HomeAssistant, + config: ConfigType, + async_add_entities_callback: AddEntitiesCallback, + discovery_info: DiscoveryInfoType | None = None, +) -> None: """Set up the test image_processing platform.""" async_add_entities_callback([TestImageProcessing("camera.demo_camera", "Test")]) @@ -13,7 +19,7 @@ async def async_setup_platform( class TestImageProcessing(ImageProcessingEntity): """Test image processing entity.""" - def __init__(self, camera_entity, name): + def __init__(self, camera_entity, name) -> None: """Initialize test image processing.""" self._name = name self._camera = camera_entity diff --git a/tests/testing_config/custom_components/test/light.py b/tests/testing_config/custom_components/test/light.py index 4cd49fec606..6422bb4fccb 100644 --- a/tests/testing_config/custom_components/test/light.py +++ b/tests/testing_config/custom_components/test/light.py @@ -5,6 +5,9 @@ Call init before using it in your tests to ensure clean test data. from homeassistant.components.light import ColorMode, LightEntity from homeassistant.const import STATE_OFF, STATE_ON +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from tests.common import MockToggleEntity @@ -13,6 +16,7 @@ ENTITIES = [] def init(empty=False): """Initialize the platform with entities.""" + # pylint: disable-next=global-statement global ENTITIES # noqa: PLW0603 ENTITIES = ( @@ -27,8 +31,11 @@ def init(empty=False): async def async_setup_platform( - hass, config, async_add_entities_callback, discovery_info=None -): + hass: HomeAssistant, + config: ConfigType, + async_add_entities_callback: AddEntitiesCallback, + discovery_info: DiscoveryInfoType | None = None, +) -> None: """Return mock entities.""" async_add_entities_callback(ENTITIES) @@ -64,7 +71,7 @@ class MockLight(MockToggleEntity, LightEntity): state, unique_id=None, supported_color_modes: set[ColorMode] | None = None, - ): + ) -> None: """Initialize the mock light.""" super().__init__(name, state, unique_id) if supported_color_modes is None: diff --git a/tests/testing_config/custom_components/test/lock.py b/tests/testing_config/custom_components/test/lock.py index e97d3f8de22..0c24e1b5b41 100644 --- a/tests/testing_config/custom_components/test/lock.py +++ b/tests/testing_config/custom_components/test/lock.py @@ -4,6 +4,9 @@ Call init before using it in your tests to ensure clean test data. """ from homeassistant.components.lock import LockEntity, LockEntityFeature +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from tests.common import MockEntity @@ -12,6 +15,7 @@ ENTITIES = {} def init(empty=False): """Initialize the platform with entities.""" + # pylint: disable-next=global-statement global ENTITIES # noqa: PLW0603 ENTITIES = ( @@ -35,8 +39,11 @@ def init(empty=False): async def async_setup_platform( - hass, config, async_add_entities_callback, discovery_info=None -): + hass: HomeAssistant, + config: ConfigType, + async_add_entities_callback: AddEntitiesCallback, + discovery_info: DiscoveryInfoType | None = None, +) -> None: """Return mock entities.""" async_add_entities_callback(list(ENTITIES.values())) diff --git a/tests/testing_config/custom_components/test/remote.py b/tests/testing_config/custom_components/test/remote.py index 3226c93310c..6d3f2ec955d 100644 --- a/tests/testing_config/custom_components/test/remote.py +++ b/tests/testing_config/custom_components/test/remote.py @@ -5,6 +5,9 @@ Call init before using it in your tests to ensure clean test data. from homeassistant.components.remote import RemoteEntity from homeassistant.const import STATE_OFF, STATE_ON +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from tests.common import MockToggleEntity @@ -13,6 +16,7 @@ ENTITIES = [] def init(empty=False): """Initialize the platform with entities.""" + # pylint: disable-next=global-statement global ENTITIES # noqa: PLW0603 ENTITIES = ( @@ -27,8 +31,11 @@ def init(empty=False): async def async_setup_platform( - hass, config, async_add_entities_callback, discovery_info=None -): + hass: HomeAssistant, + config: ConfigType, + async_add_entities_callback: AddEntitiesCallback, + discovery_info: DiscoveryInfoType | None = None, +) -> None: """Return mock entities.""" async_add_entities_callback(ENTITIES) diff --git a/tests/testing_config/custom_components/test/switch.py b/tests/testing_config/custom_components/test/switch.py index b06db33746f..9099040e2b6 100644 --- a/tests/testing_config/custom_components/test/switch.py +++ b/tests/testing_config/custom_components/test/switch.py @@ -1,8 +1,15 @@ """Stub switch platform for translation tests.""" +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType + async def async_setup_platform( - hass, config, async_add_entities_callback, discovery_info=None -): + hass: HomeAssistant, + config: ConfigType, + async_add_entities_callback: AddEntitiesCallback, + discovery_info: DiscoveryInfoType | None = None, +) -> None: """Stub setup for translation tests.""" async_add_entities_callback([]) diff --git a/tests/testing_config/custom_components/test/weather.py b/tests/testing_config/custom_components/test/weather.py index b051531b9e8..cef0584e4e0 100644 --- a/tests/testing_config/custom_components/test/weather.py +++ b/tests/testing_config/custom_components/test/weather.py @@ -33,6 +33,7 @@ ENTITIES = [] def init(empty=False): """Initialize the platform with entities.""" + # pylint: disable-next=global-statement global ENTITIES # noqa: PLW0603 ENTITIES = [] if empty else [MockWeather()] diff --git a/tests/testing_config/custom_components/test_embedded/__init__.py b/tests/testing_config/custom_components/test_embedded/__init__.py index b83493817fd..b3fe1be4d74 100644 --- a/tests/testing_config/custom_components/test_embedded/__init__.py +++ b/tests/testing_config/custom_components/test_embedded/__init__.py @@ -1,8 +1,11 @@ """Component with embedded platforms.""" +from homeassistant.core import HomeAssistant +from homeassistant.helpers.typing import ConfigType + DOMAIN = "test_embedded" -async def async_setup(hass, config): +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Mock config.""" return True diff --git a/tests/testing_config/custom_components/test_embedded/switch.py b/tests/testing_config/custom_components/test_embedded/switch.py index 46dac4419a6..f287f5ee547 100644 --- a/tests/testing_config/custom_components/test_embedded/switch.py +++ b/tests/testing_config/custom_components/test_embedded/switch.py @@ -1,7 +1,14 @@ """Switch platform for the embedded component.""" +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType + async def async_setup_platform( - hass, config, async_add_entities_callback, discovery_info=None -): + hass: HomeAssistant, + config: ConfigType, + async_add_entities_callback: AddEntitiesCallback, + discovery_info: DiscoveryInfoType | None = None, +) -> None: """Find and return test switches.""" diff --git a/tests/testing_config/custom_components/test_integration_platform/__init__.py b/tests/testing_config/custom_components/test_integration_platform/__init__.py index 220beb05367..8c3929398a1 100644 --- a/tests/testing_config/custom_components/test_integration_platform/__init__.py +++ b/tests/testing_config/custom_components/test_integration_platform/__init__.py @@ -1,10 +1,13 @@ """Provide a mock package component.""" +from homeassistant.core import HomeAssistant +from homeassistant.helpers.typing import ConfigType + from .const import TEST # noqa: F401 DOMAIN = "test_integration_platform" -async def async_setup(hass, config): +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Mock a successful setup.""" return True diff --git a/tests/testing_config/custom_components/test_package/__init__.py b/tests/testing_config/custom_components/test_package/__init__.py index 50e132e2c07..33b04428ba4 100644 --- a/tests/testing_config/custom_components/test_package/__init__.py +++ b/tests/testing_config/custom_components/test_package/__init__.py @@ -1,10 +1,13 @@ """Provide a mock package component.""" +from homeassistant.core import HomeAssistant +from homeassistant.helpers.typing import ConfigType + from .const import TEST # noqa: F401 DOMAIN = "test_package" -async def async_setup(hass, config): +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Mock a successful setup.""" return True diff --git a/tests/testing_config/custom_components/test_package_loaded_executor/__init__.py b/tests/testing_config/custom_components/test_package_loaded_executor/__init__.py index 50e132e2c07..33b04428ba4 100644 --- a/tests/testing_config/custom_components/test_package_loaded_executor/__init__.py +++ b/tests/testing_config/custom_components/test_package_loaded_executor/__init__.py @@ -1,10 +1,13 @@ """Provide a mock package component.""" +from homeassistant.core import HomeAssistant +from homeassistant.helpers.typing import ConfigType + from .const import TEST # noqa: F401 DOMAIN = "test_package" -async def async_setup(hass, config): +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Mock a successful setup.""" return True diff --git a/tests/testing_config/custom_components/test_package_loaded_loop/__init__.py b/tests/testing_config/custom_components/test_package_loaded_loop/__init__.py index b9080a2048a..28eb409ba2b 100644 --- a/tests/testing_config/custom_components/test_package_loaded_loop/__init__.py +++ b/tests/testing_config/custom_components/test_package_loaded_loop/__init__.py @@ -1,8 +1,11 @@ """Provide a mock package component.""" +from homeassistant.core import HomeAssistant +from homeassistant.helpers.typing import ConfigType + from .const import TEST # noqa: F401 -async def async_setup(hass, config): +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Mock a successful setup.""" return True diff --git a/tests/testing_config/custom_components/test_package_raises_cancelled_error/__init__.py b/tests/testing_config/custom_components/test_package_raises_cancelled_error/__init__.py index 37d3becb2d3..2bdf421c9b0 100644 --- a/tests/testing_config/custom_components/test_package_raises_cancelled_error/__init__.py +++ b/tests/testing_config/custom_components/test_package_raises_cancelled_error/__init__.py @@ -2,8 +2,11 @@ import asyncio +from homeassistant.core import HomeAssistant +from homeassistant.helpers.typing import ConfigType -async def async_setup(hass, config): + +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Mock a successful setup.""" asyncio.current_task().cancel() await asyncio.sleep(0) diff --git a/tests/testing_config/custom_components/test_package_raises_cancelled_error_config_entry/__init__.py b/tests/testing_config/custom_components/test_package_raises_cancelled_error_config_entry/__init__.py index 55ce19865c6..caceba1d1da 100644 --- a/tests/testing_config/custom_components/test_package_raises_cancelled_error_config_entry/__init__.py +++ b/tests/testing_config/custom_components/test_package_raises_cancelled_error_config_entry/__init__.py @@ -2,13 +2,17 @@ import asyncio +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.typing import ConfigType -async def async_setup(hass, config): + +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Mock a successful setup.""" return True -async def async_setup_entry(hass, entry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: """Mock an unsuccessful entry setup.""" asyncio.current_task().cancel() await asyncio.sleep(0) diff --git a/tests/testing_config/custom_components/test_standalone.py b/tests/testing_config/custom_components/test_standalone.py index 0b7ce8033e5..7d4c713d3c2 100644 --- a/tests/testing_config/custom_components/test_standalone.py +++ b/tests/testing_config/custom_components/test_standalone.py @@ -1,8 +1,11 @@ """Provide a mock standalone component.""" +from homeassistant.core import HomeAssistant +from homeassistant.helpers.typing import ConfigType + DOMAIN = "test_standalone" -async def async_setup(hass, config): +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Mock a successful setup.""" return True From 5abdc83b2e520f750457b0dbcc6792c21f6c5744 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 11 Jun 2024 15:00:23 +0200 Subject: [PATCH 0521/1445] Fix non-parent-init-called pylint warning in google_assistant tests (#119367) --- tests/components/google_assistant/__init__.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/components/google_assistant/__init__.py b/tests/components/google_assistant/__init__.py index 73dc109f7e6..6be58f50469 100644 --- a/tests/components/google_assistant/__init__.py +++ b/tests/components/google_assistant/__init__.py @@ -2,7 +2,8 @@ from unittest.mock import MagicMock -from homeassistant.components.google_assistant import helpers, http +from homeassistant.components.google_assistant import http +from homeassistant.core import HomeAssistant def mock_google_config_store(agent_user_ids=None): @@ -24,14 +25,14 @@ class MockConfig(http.GoogleConfig): agent_user_ids=None, enabled=True, entity_config=None, - hass=None, + hass: HomeAssistant | None = None, secure_devices_pin=None, should_2fa=None, should_expose=None, should_report_state=False, - ): + ) -> None: """Initialize config.""" - helpers.AbstractConfig.__init__(self, hass) + super().__init__(hass, None) self._enabled = enabled self._entity_config = entity_config or {} self._secure_devices_pin = secure_devices_pin From d9b3ee35a059fe830329a45e62c36da763dc02d2 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 11 Jun 2024 15:01:14 +0200 Subject: [PATCH 0522/1445] Fix typo in pylint plugin (#119362) --- pylint/plugins/hass_enforce_type_hints.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index 72cbf2ee04a..feda93fc7fa 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -3118,7 +3118,7 @@ class HassTypeHintChecker(BaseChecker): "Used when method return type is incorrect", ), "W7433": ( - "Argument %s is of type %s and could be move to " + "Argument %s is of type %s and could be moved to " "`@pytest.mark.usefixtures` decorator in %s", "hass-consider-usefixtures-decorator", "Used when an argument type is None and could be a fixture", From 1974ea4fdd0f3c1831602755291874b62bd1df8a Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 11 Jun 2024 15:01:54 +0200 Subject: [PATCH 0523/1445] Improve type hints in yaml util tests (#119358) --- tests/util/yaml/test_init.py | 121 +++++++++++++++++------------------ 1 file changed, 60 insertions(+), 61 deletions(-) diff --git a/tests/util/yaml/test_init.py b/tests/util/yaml/test_init.py index b900bd9dbce..6ea3f1437af 100644 --- a/tests/util/yaml/test_init.py +++ b/tests/util/yaml/test_init.py @@ -23,7 +23,7 @@ from tests.common import extract_stack_to_frame, get_test_config_dir, patch_yaml @pytest.fixture(params=["enable_c_loader", "disable_c_loader"]) -def try_both_loaders(request): +def try_both_loaders(request: pytest.FixtureRequest) -> Generator[None]: """Disable the yaml c loader.""" if request.param != "disable_c_loader": yield @@ -40,7 +40,7 @@ def try_both_loaders(request): @pytest.fixture(params=["enable_c_dumper", "disable_c_dumper"]) -def try_both_dumpers(request): +def try_both_dumpers(request: pytest.FixtureRequest) -> Generator[None]: """Disable the yaml c dumper.""" if request.param != "disable_c_dumper": yield @@ -56,7 +56,8 @@ def try_both_dumpers(request): importlib.reload(yaml_loader) -def test_simple_list(try_both_loaders) -> None: +@pytest.mark.usefixtures("try_both_loaders") +def test_simple_list() -> None: """Test simple list.""" conf = "config:\n - simple\n - list" with io.StringIO(conf) as file: @@ -64,7 +65,8 @@ def test_simple_list(try_both_loaders) -> None: assert doc["config"] == ["simple", "list"] -def test_simple_dict(try_both_loaders) -> None: +@pytest.mark.usefixtures("try_both_loaders") +def test_simple_dict() -> None: """Test simple dict.""" conf = "key: value" with io.StringIO(conf) as file: @@ -73,20 +75,23 @@ def test_simple_dict(try_both_loaders) -> None: @pytest.mark.parametrize("hass_config_yaml", ["message:\n {{ states.state }}"]) -def test_unhashable_key(try_both_loaders, mock_hass_config_yaml: None) -> None: +@pytest.mark.usefixtures("try_both_loaders", "mock_hass_config_yaml") +def test_unhashable_key() -> None: """Test an unhashable key.""" with pytest.raises(HomeAssistantError): load_yaml_config_file(YAML_CONFIG_FILE) @pytest.mark.parametrize("hass_config_yaml", ["a: a\nnokeyhere"]) -def test_no_key(try_both_loaders, mock_hass_config_yaml: None) -> None: +@pytest.mark.usefixtures("try_both_loaders", "mock_hass_config_yaml") +def test_no_key() -> None: """Test item without a key.""" with pytest.raises(HomeAssistantError): yaml.load_yaml(YAML_CONFIG_FILE) -def test_environment_variable(try_both_loaders) -> None: +@pytest.mark.usefixtures("try_both_loaders") +def test_environment_variable() -> None: """Test config file with environment variable.""" os.environ["PASSWORD"] = "secret_password" conf = "password: !env_var PASSWORD" @@ -96,7 +101,8 @@ def test_environment_variable(try_both_loaders) -> None: del os.environ["PASSWORD"] -def test_environment_variable_default(try_both_loaders) -> None: +@pytest.mark.usefixtures("try_both_loaders") +def test_environment_variable_default() -> None: """Test config file with default value for environment variable.""" conf = "password: !env_var PASSWORD secret_password" with io.StringIO(conf) as file: @@ -104,7 +110,8 @@ def test_environment_variable_default(try_both_loaders) -> None: assert doc["password"] == "secret_password" -def test_invalid_environment_variable(try_both_loaders) -> None: +@pytest.mark.usefixtures("try_both_loaders") +def test_invalid_environment_variable() -> None: """Test config file with no environment variable sat.""" conf = "password: !env_var PASSWORD" with pytest.raises(HomeAssistantError), io.StringIO(conf) as file: @@ -119,9 +126,8 @@ def test_invalid_environment_variable(try_both_loaders) -> None: ({"test.yaml": "123"}, 123), ], ) -def test_include_yaml( - try_both_loaders, mock_hass_config_yaml: None, value: Any -) -> None: +@pytest.mark.usefixtures("try_both_loaders", "mock_hass_config_yaml") +def test_include_yaml(value: Any) -> None: """Test include yaml.""" conf = "key: !include test.yaml" with io.StringIO(conf) as file: @@ -138,9 +144,8 @@ def test_include_yaml( ({"/test/one.yaml": "1", "/test/two.yaml": None}, [1]), ], ) -def test_include_dir_list( - mock_walk, try_both_loaders, mock_hass_config_yaml: None, value: Any -) -> None: +@pytest.mark.usefixtures("try_both_loaders", "mock_hass_config_yaml") +def test_include_dir_list(mock_walk: Mock, value: Any) -> None: """Test include dir list yaml.""" mock_walk.return_value = [["/test", [], ["two.yaml", "one.yaml"]]] @@ -161,9 +166,8 @@ def test_include_dir_list( } ], ) -def test_include_dir_list_recursive( - mock_walk, try_both_loaders, mock_hass_config_yaml: None -) -> None: +@pytest.mark.usefixtures("try_both_loaders", "mock_hass_config_yaml") +def test_include_dir_list_recursive(mock_walk: Mock) -> None: """Test include dir recursive list yaml.""" mock_walk.return_value = [ ["/test", ["tmp2", ".ignore", "ignore"], ["zero.yaml"]], @@ -198,9 +202,8 @@ def test_include_dir_list_recursive( ), ], ) -def test_include_dir_named( - mock_walk, try_both_loaders, mock_hass_config_yaml: None, value: Any -) -> None: +@pytest.mark.usefixtures("try_both_loaders", "mock_hass_config_yaml") +def test_include_dir_named(mock_walk: Mock, value: Any) -> None: """Test include dir named yaml.""" mock_walk.return_value = [ ["/test", [], ["first.yaml", "second.yaml", "secrets.yaml"]] @@ -223,9 +226,8 @@ def test_include_dir_named( } ], ) -def test_include_dir_named_recursive( - mock_walk, try_both_loaders, mock_hass_config_yaml: None -) -> None: +@pytest.mark.usefixtures("try_both_loaders", "mock_hass_config_yaml") +def test_include_dir_named_recursive(mock_walk: Mock) -> None: """Test include dir named yaml.""" mock_walk.return_value = [ ["/test", ["tmp2", ".ignore", "ignore"], ["first.yaml"]], @@ -261,9 +263,8 @@ def test_include_dir_named_recursive( ), ], ) -def test_include_dir_merge_list( - mock_walk, try_both_loaders, mock_hass_config_yaml: None, value: Any -) -> None: +@pytest.mark.usefixtures("try_both_loaders", "mock_hass_config_yaml") +def test_include_dir_merge_list(mock_walk: Mock, value: Any) -> None: """Test include dir merge list yaml.""" mock_walk.return_value = [["/test", [], ["first.yaml", "second.yaml"]]] @@ -284,9 +285,8 @@ def test_include_dir_merge_list( } ], ) -def test_include_dir_merge_list_recursive( - mock_walk, try_both_loaders, mock_hass_config_yaml: None -) -> None: +@pytest.mark.usefixtures("try_both_loaders", "mock_hass_config_yaml") +def test_include_dir_merge_list_recursive(mock_walk: Mock) -> None: """Test include dir merge list yaml.""" mock_walk.return_value = [ ["/test", ["tmp2", ".ignore", "ignore"], ["first.yaml"]], @@ -330,9 +330,8 @@ def test_include_dir_merge_list_recursive( ), ], ) -def test_include_dir_merge_named( - mock_walk, try_both_loaders, mock_hass_config_yaml: None, value: Any -) -> None: +@pytest.mark.usefixtures("try_both_loaders", "mock_hass_config_yaml") +def test_include_dir_merge_named(mock_walk: Mock, value: Any) -> None: """Test include dir merge named yaml.""" mock_walk.return_value = [["/test", [], ["first.yaml", "second.yaml"]]] @@ -353,9 +352,8 @@ def test_include_dir_merge_named( } ], ) -def test_include_dir_merge_named_recursive( - mock_walk, try_both_loaders, mock_hass_config_yaml: None -) -> None: +@pytest.mark.usefixtures("try_both_loaders", "mock_hass_config_yaml") +def test_include_dir_merge_named_recursive(mock_walk: Mock) -> None: """Test include dir merge named yaml.""" mock_walk.return_value = [ ["/test", ["tmp2", ".ignore", "ignore"], ["first.yaml"]], @@ -378,19 +376,22 @@ def test_include_dir_merge_named_recursive( @patch("homeassistant.util.yaml.loader.open", create=True) -def test_load_yaml_encoding_error(mock_open, try_both_loaders) -> None: +@pytest.mark.usefixtures("try_both_loaders") +def test_load_yaml_encoding_error(mock_open: Mock) -> None: """Test raising a UnicodeDecodeError.""" mock_open.side_effect = UnicodeDecodeError("", b"", 1, 0, "") with pytest.raises(HomeAssistantError): yaml_loader.load_yaml("test") -def test_dump(try_both_dumpers) -> None: +@pytest.mark.usefixtures("try_both_dumpers") +def test_dump() -> None: """The that the dump method returns empty None values.""" assert yaml.dump({"a": None, "b": "b"}) == "a:\nb: b\n" -def test_dump_unicode(try_both_dumpers) -> None: +@pytest.mark.usefixtures("try_both_dumpers") +def test_dump_unicode() -> None: """The that the dump method returns empty None values.""" assert yaml.dump({"a": None, "b": "привет"}) == "a:\nb: привет\n" @@ -535,18 +536,16 @@ class TestSecrets(unittest.TestCase): @pytest.mark.parametrize("hass_config_yaml", ['key: [1, "2", 3]']) -def test_representing_yaml_loaded_data( - try_both_dumpers, mock_hass_config_yaml: None -) -> None: +@pytest.mark.usefixtures("try_both_dumpers", "mock_hass_config_yaml") +def test_representing_yaml_loaded_data() -> None: """Test we can represent YAML loaded data.""" data = load_yaml_config_file(YAML_CONFIG_FILE) assert yaml.dump(data) == "key:\n- 1\n- '2'\n- 3\n" @pytest.mark.parametrize("hass_config_yaml", ["key: thing1\nkey: thing2"]) -def test_duplicate_key( - caplog: pytest.LogCaptureFixture, try_both_loaders, mock_hass_config_yaml: None -) -> None: +@pytest.mark.usefixtures("try_both_loaders", "mock_hass_config_yaml") +def test_duplicate_key(caplog: pytest.LogCaptureFixture) -> None: """Test duplicate dict keys.""" load_yaml_config_file(YAML_CONFIG_FILE) assert "contains duplicate key" in caplog.text @@ -556,9 +555,8 @@ def test_duplicate_key( "hass_config_yaml_files", [{YAML_CONFIG_FILE: "key: !secret a", yaml.SECRET_YAML: "a: 1\nb: !secret a"}], ) -def test_no_recursive_secrets( - caplog: pytest.LogCaptureFixture, try_both_loaders, mock_hass_config_yaml: None -) -> None: +@pytest.mark.usefixtures("try_both_loaders", "mock_hass_config_yaml") +def test_no_recursive_secrets() -> None: """Test that loading of secrets from the secrets file fails correctly.""" with pytest.raises(HomeAssistantError) as e: load_yaml_config_file(YAML_CONFIG_FILE) @@ -577,7 +575,8 @@ def test_input_class() -> None: assert len({yaml_input, yaml_input2}) == 1 -def test_input(try_both_loaders, try_both_dumpers) -> None: +@pytest.mark.usefixtures("try_both_loaders", "try_both_dumpers") +def test_input() -> None: """Test loading inputs.""" data = {"hello": yaml.Input("test_name")} assert yaml.parse_yaml(yaml.dump(data)) == data @@ -592,9 +591,8 @@ def test_c_loader_is_available_in_ci() -> None: assert yaml.loader.HAS_C_LOADER is True -async def test_loading_actual_file_with_syntax_error( - hass: HomeAssistant, try_both_loaders -) -> None: +@pytest.mark.usefixtures("try_both_loaders") +async def test_loading_actual_file_with_syntax_error(hass: HomeAssistant) -> None: """Test loading a real file with syntax errors.""" fixture_path = pathlib.Path(__file__).parent.joinpath("fixtures", "bad.yaml.txt") with pytest.raises(HomeAssistantError): @@ -646,11 +644,10 @@ def mock_integration_frame() -> Generator[Mock]: ), ], ) +@pytest.mark.usefixtures("mock_integration_frame") async def test_deprecated_loaders( - hass: HomeAssistant, - mock_integration_frame: Mock, caplog: pytest.LogCaptureFixture, - loader_class, + loader_class: type, message: str, ) -> None: """Test instantiating the deprecated yaml loaders logs a warning.""" @@ -662,7 +659,8 @@ async def test_deprecated_loaders( assert (f"Detected that integration 'hue' uses deprecated {message}") in caplog.text -def test_string_annotated(try_both_loaders) -> None: +@pytest.mark.usefixtures("try_both_loaders") +def test_string_annotated() -> None: """Test strings are annotated with file + line.""" conf = ( "key1: str\n" @@ -695,7 +693,8 @@ def test_string_annotated(try_both_loaders) -> None: assert getattr(value, "__line__", None) == expected_annotations[key][1][1] -def test_string_used_as_vol_schema(try_both_loaders) -> None: +@pytest.mark.usefixtures("try_both_loaders") +def test_string_used_as_vol_schema() -> None: """Test the subclassed strings can be used in voluptuous schemas.""" conf = "wanted_data:\n key_1: value_1\n key_2: value_2\n" with io.StringIO(conf) as file: @@ -715,15 +714,15 @@ def test_string_used_as_vol_schema(try_both_loaders) -> None: @pytest.mark.parametrize( ("hass_config_yaml", "expected_data"), [("", {}), ("bla:", {"bla": None})] ) -def test_load_yaml_dict( - try_both_loaders, mock_hass_config_yaml: None, expected_data: Any -) -> None: +@pytest.mark.usefixtures("try_both_loaders", "mock_hass_config_yaml") +def test_load_yaml_dict(expected_data: Any) -> None: """Test item without a key.""" assert yaml.load_yaml_dict(YAML_CONFIG_FILE) == expected_data @pytest.mark.parametrize("hass_config_yaml", ["abc", "123", "[]"]) -def test_load_yaml_dict_fail(try_both_loaders, mock_hass_config_yaml: None) -> None: +@pytest.mark.usefixtures("try_both_loaders", "mock_hass_config_yaml") +def test_load_yaml_dict_fail() -> None: """Test item without a key.""" with pytest.raises(yaml_loader.YamlTypeError): yaml_loader.load_yaml_dict(YAML_CONFIG_FILE) From e57bac6da82af462c6638159fc196a0bdccae680 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 11 Jun 2024 15:03:03 +0200 Subject: [PATCH 0524/1445] Fix confusing-with-statement pylint warnings (#119364) --- tests/components/dsmr/test_config_flow.py | 9 +++++---- tests/components/rfxtrx/test_config_flow.py | 9 +++++---- tests/components/usb/test_init.py | 9 +++++---- 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/tests/components/dsmr/test_config_flow.py b/tests/components/dsmr/test_config_flow.py index 791797f7dcd..711b29f4ae0 100644 --- a/tests/components/dsmr/test_config_flow.py +++ b/tests/components/dsmr/test_config_flow.py @@ -519,16 +519,17 @@ def test_get_serial_by_id_no_dir() -> None: def test_get_serial_by_id() -> None: """Test serial by id conversion.""" - p1 = patch("os.path.isdir", MagicMock(return_value=True)) - p2 = patch("os.scandir") def _realpath(path): if path is sentinel.matched_link: return sentinel.path return sentinel.serial_link_path - p3 = patch("os.path.realpath", side_effect=_realpath) - with p1 as is_dir_mock, p2 as scan_mock, p3: + with ( + patch("os.path.isdir", MagicMock(return_value=True)) as is_dir_mock, + patch("os.scandir") as scan_mock, + patch("os.path.realpath", side_effect=_realpath), + ): res = config_flow.get_serial_by_id(sentinel.path) assert res is sentinel.path assert is_dir_mock.call_count == 1 diff --git a/tests/components/rfxtrx/test_config_flow.py b/tests/components/rfxtrx/test_config_flow.py index fd1cfbb09fd..b61440c31b6 100644 --- a/tests/components/rfxtrx/test_config_flow.py +++ b/tests/components/rfxtrx/test_config_flow.py @@ -900,16 +900,17 @@ def test_get_serial_by_id_no_dir() -> None: def test_get_serial_by_id() -> None: """Test serial by id conversion.""" - p1 = patch("os.path.isdir", MagicMock(return_value=True)) - p2 = patch("os.scandir") def _realpath(path): if path is sentinel.matched_link: return sentinel.path return sentinel.serial_link_path - p3 = patch("os.path.realpath", side_effect=_realpath) - with p1 as is_dir_mock, p2 as scan_mock, p3: + with ( + patch("os.path.isdir", MagicMock(return_value=True)) as is_dir_mock, + patch("os.scandir") as scan_mock, + patch("os.path.realpath", side_effect=_realpath), + ): res = config_flow.get_serial_by_id(sentinel.path) assert res is sentinel.path assert is_dir_mock.call_count == 1 diff --git a/tests/components/usb/test_init.py b/tests/components/usb/test_init.py index c3f7817527c..ce7484a811c 100644 --- a/tests/components/usb/test_init.py +++ b/tests/components/usb/test_init.py @@ -771,16 +771,17 @@ def test_get_serial_by_id_no_dir() -> None: def test_get_serial_by_id() -> None: """Test serial by id conversion.""" - p1 = patch("os.path.isdir", MagicMock(return_value=True)) - p2 = patch("os.scandir") def _realpath(path): if path is sentinel.matched_link: return sentinel.path return sentinel.serial_link_path - p3 = patch("os.path.realpath", side_effect=_realpath) - with p1 as is_dir_mock, p2 as scan_mock, p3: + with ( + patch("os.path.isdir", MagicMock(return_value=True)) as is_dir_mock, + patch("os.scandir") as scan_mock, + patch("os.path.realpath", side_effect=_realpath), + ): res = usb.get_serial_by_id(sentinel.path) assert res is sentinel.path assert is_dir_mock.call_count == 1 From 65befcf5d41ea0c668a3d1329af9e32835778ee1 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 11 Jun 2024 15:04:00 +0200 Subject: [PATCH 0525/1445] Fix import pylint warning in core tests (#119359) --- tests/common.py | 27 ++++++++++++------------ tests/components/conftest.py | 40 +++++++++++++++++++++--------------- tests/test_backports.py | 2 +- tests/test_block_async_io.py | 2 +- tests/test_config_entries.py | 3 +-- tests/test_const.py | 2 +- tests/test_loader.py | 5 ++++- 7 files changed, 46 insertions(+), 35 deletions(-) diff --git a/tests/common.py b/tests/common.py index 732970e108b..9faf7e10712 100644 --- a/tests/common.py +++ b/tests/common.py @@ -71,7 +71,6 @@ from homeassistant.helpers import ( issue_registry as ir, label_registry as lr, recorder as recorder_helper, - restore_state, restore_state as rs, storage, translation, @@ -100,7 +99,7 @@ import homeassistant.util.ulid as ulid_util from homeassistant.util.unit_system import METRIC_SYSTEM import homeassistant.util.yaml.loader as yaml_loader -from tests.testing_config.custom_components.test_constant_deprecation import ( +from .testing_config.custom_components.test_constant_deprecation import ( import_deprecated_constant, ) @@ -1133,6 +1132,7 @@ def init_recorder_component(hass, add_config=None, db_url="sqlite://"): """Initialize the recorder.""" # Local import to avoid processing recorder and SQLite modules when running a # testcase which does not use the recorder. + # pylint: disable-next=import-outside-toplevel from homeassistant.components import recorder config = dict(add_config) if add_config else {} @@ -1154,8 +1154,8 @@ def init_recorder_component(hass, add_config=None, db_url="sqlite://"): def mock_restore_cache(hass: HomeAssistant, states: Sequence[State]) -> None: """Mock the DATA_RESTORE_CACHE.""" - key = restore_state.DATA_RESTORE_STATE - data = restore_state.RestoreStateData(hass) + key = rs.DATA_RESTORE_STATE + data = rs.RestoreStateData(hass) now = dt_util.utcnow() last_states = {} @@ -1167,14 +1167,14 @@ def mock_restore_cache(hass: HomeAssistant, states: Sequence[State]) -> None: json.dumps(restored_state["attributes"], cls=JSONEncoder) ), } - last_states[state.entity_id] = restore_state.StoredState.from_dict( + last_states[state.entity_id] = rs.StoredState.from_dict( {"state": restored_state, "last_seen": now} ) data.last_states = last_states _LOGGER.debug("Restore cache: %s", data.last_states) assert len(data.last_states) == len(states), f"Duplicate entity_id? {states}" - restore_state.async_get.cache_clear() + rs.async_get.cache_clear() hass.data[key] = data @@ -1182,8 +1182,8 @@ def mock_restore_cache_with_extra_data( hass: HomeAssistant, states: Sequence[tuple[State, Mapping[str, Any]]] ) -> None: """Mock the DATA_RESTORE_CACHE.""" - key = restore_state.DATA_RESTORE_STATE - data = restore_state.RestoreStateData(hass) + key = rs.DATA_RESTORE_STATE + data = rs.RestoreStateData(hass) now = dt_util.utcnow() last_states = {} @@ -1195,22 +1195,22 @@ def mock_restore_cache_with_extra_data( json.dumps(restored_state["attributes"], cls=JSONEncoder) ), } - last_states[state.entity_id] = restore_state.StoredState.from_dict( + last_states[state.entity_id] = rs.StoredState.from_dict( {"state": restored_state, "extra_data": extra_data, "last_seen": now} ) data.last_states = last_states _LOGGER.debug("Restore cache: %s", data.last_states) assert len(data.last_states) == len(states), f"Duplicate entity_id? {states}" - restore_state.async_get.cache_clear() + rs.async_get.cache_clear() hass.data[key] = data async def async_mock_restore_state_shutdown_restart( hass: HomeAssistant, -) -> restore_state.RestoreStateData: +) -> rs.RestoreStateData: """Mock shutting down and saving restore state and restoring.""" - data = restore_state.async_get(hass) + data = rs.async_get(hass) await data.async_dump_states() await async_mock_load_restore_state_from_storage(hass) return data @@ -1223,7 +1223,7 @@ async def async_mock_load_restore_state_from_storage( hass_storage must already be mocked. """ - await restore_state.async_get(hass).async_load() + await rs.async_get(hass).async_load() class MockEntity(entity.Entity): @@ -1571,6 +1571,7 @@ def async_get_persistent_notifications( def async_mock_cloud_connection_status(hass: HomeAssistant, connected: bool) -> None: """Mock a signal the cloud disconnected.""" + # pylint: disable-next=import-outside-toplevel from homeassistant.components.cloud import ( SIGNAL_CLOUD_CONNECTION_STATE, CloudConnectionState, diff --git a/tests/components/conftest.py b/tests/components/conftest.py index e44479873d8..42746525a0d 100644 --- a/tests/components/conftest.py +++ b/tests/components/conftest.py @@ -54,7 +54,8 @@ def entity_registry_enabled_by_default() -> Generator[None]: @pytest.fixture(name="stub_blueprint_populate") def stub_blueprint_populate_fixture() -> Generator[None]: """Stub copying the blueprints to the config folder.""" - from tests.components.blueprint.common import stub_blueprint_populate_fixture_helper + # pylint: disable-next=import-outside-toplevel + from .blueprint.common import stub_blueprint_populate_fixture_helper yield from stub_blueprint_populate_fixture_helper() @@ -63,7 +64,8 @@ def stub_blueprint_populate_fixture() -> Generator[None]: @pytest.fixture(name="mock_tts_get_cache_files") def mock_tts_get_cache_files_fixture() -> Generator[MagicMock]: """Mock the list TTS cache function.""" - from tests.components.tts.common import mock_tts_get_cache_files_fixture_helper + # pylint: disable-next=import-outside-toplevel + from .tts.common import mock_tts_get_cache_files_fixture_helper yield from mock_tts_get_cache_files_fixture_helper() @@ -73,7 +75,8 @@ def mock_tts_init_cache_dir_fixture( init_tts_cache_dir_side_effect: Any, ) -> Generator[MagicMock]: """Mock the TTS cache dir in memory.""" - from tests.components.tts.common import mock_tts_init_cache_dir_fixture_helper + # pylint: disable-next=import-outside-toplevel + from .tts.common import mock_tts_init_cache_dir_fixture_helper yield from mock_tts_init_cache_dir_fixture_helper(init_tts_cache_dir_side_effect) @@ -81,9 +84,8 @@ def mock_tts_init_cache_dir_fixture( @pytest.fixture(name="init_tts_cache_dir_side_effect") def init_tts_cache_dir_side_effect_fixture() -> Any: """Return the cache dir.""" - from tests.components.tts.common import ( - init_tts_cache_dir_side_effect_fixture_helper, - ) + # pylint: disable-next=import-outside-toplevel + from .tts.common import init_tts_cache_dir_side_effect_fixture_helper return init_tts_cache_dir_side_effect_fixture_helper() @@ -96,7 +98,8 @@ def mock_tts_cache_dir_fixture( request: pytest.FixtureRequest, ) -> Generator[Path]: """Mock the TTS cache dir with empty dir.""" - from tests.components.tts.common import mock_tts_cache_dir_fixture_helper + # pylint: disable-next=import-outside-toplevel + from .tts.common import mock_tts_cache_dir_fixture_helper yield from mock_tts_cache_dir_fixture_helper( tmp_path, mock_tts_init_cache_dir, mock_tts_get_cache_files, request @@ -106,7 +109,8 @@ def mock_tts_cache_dir_fixture( @pytest.fixture(name="tts_mutagen_mock") def tts_mutagen_mock_fixture() -> Generator[MagicMock]: """Mock writing tags.""" - from tests.components.tts.common import tts_mutagen_mock_fixture_helper + # pylint: disable-next=import-outside-toplevel + from .tts.common import tts_mutagen_mock_fixture_helper yield from tts_mutagen_mock_fixture_helper() @@ -114,9 +118,8 @@ def tts_mutagen_mock_fixture() -> Generator[MagicMock]: @pytest.fixture(name="mock_conversation_agent") def mock_conversation_agent_fixture(hass: HomeAssistant) -> MockAgent: """Mock a conversation agent.""" - from tests.components.conversation.common import ( - mock_conversation_agent_fixture_helper, - ) + # pylint: disable-next=import-outside-toplevel + from .conversation.common import mock_conversation_agent_fixture_helper return mock_conversation_agent_fixture_helper(hass) @@ -133,7 +136,8 @@ def prevent_ffmpeg_subprocess() -> Generator[None]: @pytest.fixture def mock_light_entities() -> list[MockLight]: """Return mocked light entities.""" - from tests.components.light.common import MockLight + # pylint: disable-next=import-outside-toplevel + from .light.common import MockLight return [ MockLight("Ceiling", STATE_ON), @@ -145,7 +149,8 @@ def mock_light_entities() -> list[MockLight]: @pytest.fixture def mock_sensor_entities() -> dict[str, MockSensor]: """Return mocked sensor entities.""" - from tests.components.sensor.common import get_mock_sensor_entities + # pylint: disable-next=import-outside-toplevel + from .sensor.common import get_mock_sensor_entities return get_mock_sensor_entities() @@ -153,7 +158,8 @@ def mock_sensor_entities() -> dict[str, MockSensor]: @pytest.fixture def mock_switch_entities() -> list[MockSwitch]: """Return mocked toggle entities.""" - from tests.components.switch.common import get_mock_switch_entities + # pylint: disable-next=import-outside-toplevel + from .switch.common import get_mock_switch_entities return get_mock_switch_entities() @@ -161,7 +167,8 @@ def mock_switch_entities() -> list[MockSwitch]: @pytest.fixture def mock_legacy_device_scanner() -> MockScanner: """Return mocked legacy device scanner entity.""" - from tests.components.device_tracker.common import MockScanner + # pylint: disable-next=import-outside-toplevel + from .device_tracker.common import MockScanner return MockScanner() @@ -169,6 +176,7 @@ def mock_legacy_device_scanner() -> MockScanner: @pytest.fixture def mock_legacy_device_tracker_setup() -> Callable[[HomeAssistant, MockScanner], None]: """Return setup callable for legacy device tracker setup.""" - from tests.components.device_tracker.common import mock_legacy_device_tracker_setup + # pylint: disable-next=import-outside-toplevel + from .device_tracker.common import mock_legacy_device_tracker_setup return mock_legacy_device_tracker_setup diff --git a/tests/test_backports.py b/tests/test_backports.py index 09c11da37cb..4df0a9e3f57 100644 --- a/tests/test_backports.py +++ b/tests/test_backports.py @@ -14,7 +14,7 @@ from homeassistant.backports import ( functools as backports_functools, ) -from tests.common import import_and_test_deprecated_alias +from .common import import_and_test_deprecated_alias @pytest.mark.parametrize( diff --git a/tests/test_block_async_io.py b/tests/test_block_async_io.py index 5a1e38d78cd..eab8033e37d 100644 --- a/tests/test_block_async_io.py +++ b/tests/test_block_async_io.py @@ -14,7 +14,7 @@ import pytest from homeassistant import block_async_io from homeassistant.core import HomeAssistant -from tests.common import extract_stack_to_frame +from .common import extract_stack_to_frame async def test_protect_loop_debugger_sleep(caplog: pytest.LogCaptureFixture) -> None: diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 5c2bf8b205b..0208b33169c 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -44,13 +44,12 @@ from .common import ( MockPlatform, async_capture_events, async_fire_time_changed, + async_get_persistent_notifications, mock_config_flow, mock_integration, mock_platform, ) -from tests.common import async_get_persistent_notifications - @pytest.fixture(autouse=True) def mock_handlers() -> Generator[None]: diff --git a/tests/test_const.py b/tests/test_const.py index 63b01388dd7..a6a2387b091 100644 --- a/tests/test_const.py +++ b/tests/test_const.py @@ -7,7 +7,7 @@ import pytest from homeassistant import const from homeassistant.components import sensor -from tests.common import ( +from .common import ( help_test_all, import_and_test_deprecated_constant, import_and_test_deprecated_constant_enum, diff --git a/tests/test_loader.py b/tests/test_loader.py index 328b55ddf80..b195de6006b 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -131,6 +131,7 @@ async def test_custom_component_name( assert platform.__package__ == "custom_components.test" # Test custom components is mounted + # pylint: disable-next=import-outside-toplevel from custom_components.test_package import TEST assert TEST == 5 @@ -1247,14 +1248,16 @@ def test_import_executor_default(hass: HomeAssistant) -> None: assert built_in_comp.import_executor is True -async def test_config_folder_not_in_path(hass): +async def test_config_folder_not_in_path() -> None: """Test that config folder is not in path.""" # Verify that we are unable to import this file from top level with pytest.raises(ImportError): + # pylint: disable-next=import-outside-toplevel import check_config_not_in_path # noqa: F401 # Verify that we are able to load the file with absolute path + # pylint: disable-next=import-outside-toplevel,hass-relative-import import tests.testing_config.check_config_not_in_path # noqa: F401 From 9af13e54c1c473dce0fabe326d4bd00594447ba0 Mon Sep 17 00:00:00 2001 From: Jafar Atili Date: Tue, 11 Jun 2024 16:05:53 +0300 Subject: [PATCH 0526/1445] Bump pyElectra to 1.2.3 (#119369) --- .strict-typing | 1 + homeassistant/components/electrasmart/manifest.json | 2 +- mypy.ini | 10 ++++++++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 14 insertions(+), 3 deletions(-) diff --git a/.strict-typing b/.strict-typing index 86fbf3c3563..313dda48649 100644 --- a/.strict-typing +++ b/.strict-typing @@ -163,6 +163,7 @@ homeassistant.components.easyenergy.* homeassistant.components.ecovacs.* homeassistant.components.ecowitt.* homeassistant.components.efergy.* +homeassistant.components.electrasmart.* homeassistant.components.electric_kiwi.* homeassistant.components.elgato.* homeassistant.components.elkm1.* diff --git a/homeassistant/components/electrasmart/manifest.json b/homeassistant/components/electrasmart/manifest.json index e00b818e2a6..f19aeb3d947 100644 --- a/homeassistant/components/electrasmart/manifest.json +++ b/homeassistant/components/electrasmart/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/electrasmart", "iot_class": "cloud_polling", - "requirements": ["pyElectra==1.2.1"] + "requirements": ["pyElectra==1.2.3"] } diff --git a/mypy.ini b/mypy.ini index ac3945872a1..4e4d9cc624b 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1393,6 +1393,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.electrasmart.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.electric_kiwi.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/requirements_all.txt b/requirements_all.txt index 6929009ced0..be95c4f5120 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1673,7 +1673,7 @@ pyControl4==1.1.0 pyDuotecno==2024.5.1 # homeassistant.components.electrasmart -pyElectra==1.2.1 +pyElectra==1.2.3 # homeassistant.components.emby pyEmby==1.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fe280ef080d..8e2a2fa48aa 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1332,7 +1332,7 @@ pyControl4==1.1.0 pyDuotecno==2024.5.1 # homeassistant.components.electrasmart -pyElectra==1.2.1 +pyElectra==1.2.3 # homeassistant.components.rfxtrx pyRFXtrx==0.31.1 From 2e9f63ced64f3910e289c5e28aba9b47978cc7ba Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 11 Jun 2024 15:06:16 +0200 Subject: [PATCH 0527/1445] Fix use-maxsplit-arg pylint warnings in tests (#119366) --- tests/components/vizio/const.py | 3 +-- tests/components/whirlpool/test_sensor.py | 6 +++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/tests/components/vizio/const.py b/tests/components/vizio/const.py index 1f35cc16385..3e7b0c83c70 100644 --- a/tests/components/vizio/const.py +++ b/tests/components/vizio/const.py @@ -196,8 +196,7 @@ MOCK_INCLUDE_NO_APPS = { VIZIO_ZEROCONF_SERVICE_TYPE = "_viziocast._tcp.local." ZEROCONF_NAME = f"{NAME}.{VIZIO_ZEROCONF_SERVICE_TYPE}" -ZEROCONF_HOST = HOST.split(":")[0] -ZEROCONF_PORT = HOST.split(":")[1] +ZEROCONF_HOST, ZEROCONF_PORT = HOST.split(":", maxsplit=2) MOCK_ZEROCONF_SERVICE_INFO = zeroconf.ZeroconfServiceInfo( ip_address=ip_address(ZEROCONF_HOST), diff --git a/tests/components/whirlpool/test_sensor.py b/tests/components/whirlpool/test_sensor.py index fc509f264c5..6af88c8a9f3 100644 --- a/tests/components/whirlpool/test_sensor.py +++ b/tests/components/whirlpool/test_sensor.py @@ -81,7 +81,7 @@ async def test_dryer_sensor_values( state = await update_sensor_state(hass, entity_id, mock_instance) assert state is not None - state_id = f"{entity_id.split('_')[0]}_end_time" + state_id = f"{entity_id.split('_', maxsplit=1)[0]}_end_time" state = hass.states.get(state_id) assert state.state == thetimestamp.isoformat() @@ -151,11 +151,11 @@ async def test_washer_sensor_values( state = await update_sensor_state(hass, entity_id, mock_instance) assert state is not None - state_id = f"{entity_id.split('_')[0]}_end_time" + state_id = f"{entity_id.split('_', maxsplit=1)[0]}_end_time" state = hass.states.get(state_id) assert state.state == thetimestamp.isoformat() - state_id = f"{entity_id.split('_')[0]}_detergent_level" + state_id = f"{entity_id.split('_', maxsplit=1)[0]}_detergent_level" entry = entity_registry.async_get(state_id) assert entry assert entry.disabled From e14146d7c915d244d591eb65d42424f347dedc1a Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 11 Jun 2024 15:06:44 +0200 Subject: [PATCH 0528/1445] Fix consider-using-with pylint warnings in matrix tests (#119365) --- tests/components/matrix/conftest.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/components/matrix/conftest.py b/tests/components/matrix/conftest.py index f65deea8dad..bb5448a8a09 100644 --- a/tests/components/matrix/conftest.py +++ b/tests/components/matrix/conftest.py @@ -24,6 +24,7 @@ from nio import ( ) from PIL import Image import pytest +from typing_extensions import Generator from homeassistant.components.matrix import ( CONF_COMMANDS, @@ -305,9 +306,9 @@ def command_events(hass: HomeAssistant): @pytest.fixture -def image_path(tmp_path: Path): +def image_path(tmp_path: Path) -> Generator[tempfile._TemporaryFileWrapper]: """Provide the Path to a mock image.""" image = Image.new("RGBA", size=(50, 50), color=(256, 0, 0)) - image_file = tempfile.NamedTemporaryFile(dir=tmp_path) - image.save(image_file, "PNG") - return image_file + with tempfile.NamedTemporaryFile(dir=tmp_path) as image_file: + image.save(image_file, "PNG") + yield image_file From 18722aeccb79b4125043046be92c6a679e69bb46 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 11 Jun 2024 15:07:40 +0200 Subject: [PATCH 0529/1445] Improve type hints and fix pylint warnings in util tests (#119355) --- tests/util/test_json.py | 2 +- tests/util/test_location.py | 13 ++++++++----- tests/util/test_unit_system.py | 5 ++--- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/tests/util/test_json.py b/tests/util/test_json.py index 3eccb524538..c973ed1a91c 100644 --- a/tests/util/test_json.py +++ b/tests/util/test_json.py @@ -159,7 +159,7 @@ async def test_deprecated_save_json( assert "should be updated to use homeassistant.helpers.json module" in caplog.text -async def test_loading_derived_class(): +async def test_loading_derived_class() -> None: """Test loading data from classes derived from str.""" class MyStr(str): diff --git a/tests/util/test_location.py b/tests/util/test_location.py index b9252c33e9d..3af3ad2765a 100644 --- a/tests/util/test_location.py +++ b/tests/util/test_location.py @@ -5,6 +5,7 @@ from unittest.mock import Mock, patch import aiohttp import pytest +from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.util.location as location_util @@ -28,13 +29,13 @@ DISTANCE_MILES = 3632.78 @pytest.fixture -async def session(hass): +async def session(hass: HomeAssistant) -> aiohttp.ClientSession: """Return aioclient session.""" return async_get_clientsession(hass) @pytest.fixture -async def raising_session(): +async def raising_session() -> Mock: """Return an aioclient session that only fails.""" return Mock(get=Mock(side_effect=aiohttp.ClientError)) @@ -76,7 +77,7 @@ def test_get_miles() -> None: async def test_detect_location_info_whoami( - aioclient_mock: AiohttpClientMocker, session + aioclient_mock: AiohttpClientMocker, session: aiohttp.ClientSession ) -> None: """Test detect location info using services.home-assistant.io/whoami.""" aioclient_mock.get(location_util.WHOAMI_URL, text=load_fixture("whoami.json")) @@ -99,7 +100,9 @@ async def test_detect_location_info_whoami( assert info.use_metric -async def test_dev_url(aioclient_mock: AiohttpClientMocker, session) -> None: +async def test_dev_url( + aioclient_mock: AiohttpClientMocker, session: aiohttp.ClientSession +) -> None: """Test usage of dev URL.""" aioclient_mock.get(location_util.WHOAMI_URL_DEV, text=load_fixture("whoami.json")) with patch("homeassistant.util.location.HA_VERSION", "1.0.dev0"): @@ -110,7 +113,7 @@ async def test_dev_url(aioclient_mock: AiohttpClientMocker, session) -> None: assert info.currency == "XXX" -async def test_whoami_query_raises(raising_session) -> None: +async def test_whoami_query_raises(raising_session: Mock) -> None: """Test whoami query when the request to API fails.""" info = await location_util._get_whoami(raising_session) assert info is None diff --git a/tests/util/test_unit_system.py b/tests/util/test_unit_system.py index 0fa11762490..033631563f4 100644 --- a/tests/util/test_unit_system.py +++ b/tests/util/test_unit_system.py @@ -4,8 +4,7 @@ from __future__ import annotations import pytest -from homeassistant.components.sensor import SensorDeviceClass -from homeassistant.components.sensor.const import DEVICE_CLASS_UNITS +from homeassistant.components.sensor import DEVICE_CLASS_UNITS, SensorDeviceClass from homeassistant.const import ( ACCUMULATED_PRECIPITATION, LENGTH, @@ -23,7 +22,7 @@ from homeassistant.const import ( UnitOfVolumetricFlux, ) from homeassistant.exceptions import HomeAssistantError -from homeassistant.util.unit_system import ( +from homeassistant.util.unit_system import ( # pylint: disable=hass-deprecated-import _CONF_UNIT_SYSTEM_IMPERIAL, _CONF_UNIT_SYSTEM_METRIC, _CONF_UNIT_SYSTEM_US_CUSTOMARY, From 8c27214dc9e771bbd7bd43b54d9bb21f2ff4acd0 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 11 Jun 2024 15:12:20 +0200 Subject: [PATCH 0530/1445] Use statistic tables' duration attribute instead of magic numbers (#119356) --- .../components/recorder/statistics.py | 26 +++++++++++-------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index 8b434fcdf3a..e1178dea2a9 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -395,7 +395,7 @@ def _compile_hourly_statistics(session: Session, start: datetime) -> None: """ start_time = start.replace(minute=0) start_time_ts = start_time.timestamp() - end_time = start_time + timedelta(hours=1) + end_time = start_time + Statistics.duration end_time_ts = end_time.timestamp() # Compute last hour's average, min, max @@ -463,7 +463,9 @@ def compile_missing_statistics(instance: Recorder) -> bool: ) as session: # Find the newest statistics run, if any if last_run := session.query(func.max(StatisticsRuns.start)).scalar(): - start = max(start, process_timestamp(last_run) + timedelta(minutes=5)) + start = max( + start, process_timestamp(last_run) + StatisticsShortTerm.duration + ) periods_without_commit = 0 while start < last_period: @@ -532,7 +534,7 @@ def _compile_statistics( returns a set of modified statistic_ids if any were modified. """ assert start.tzinfo == dt_util.UTC, "start must be in UTC" - end = start + timedelta(minutes=5) + end = start + StatisticsShortTerm.duration statistics_meta_manager = instance.statistics_meta_manager modified_statistic_ids: set[str] = set() @@ -1477,7 +1479,7 @@ def statistic_during_period( tail_only = ( start_time is not None and end_time is not None - and end_time - start_time < timedelta(hours=1) + and end_time - start_time < Statistics.duration ) # Calculate the head period @@ -1487,20 +1489,22 @@ def statistic_during_period( not tail_only and oldest_stat is not None and oldest_5_min_stat is not None - and oldest_5_min_stat - oldest_stat < timedelta(hours=1) + and oldest_5_min_stat - oldest_stat < Statistics.duration and (start_time is None or start_time < oldest_5_min_stat) ): # To improve accuracy of averaged for statistics which were added within # recorder's retention period. head_start_time = oldest_5_min_stat - head_end_time = oldest_5_min_stat.replace( - minute=0, second=0, microsecond=0 - ) + timedelta(hours=1) + head_end_time = ( + oldest_5_min_stat.replace(minute=0, second=0, microsecond=0) + + Statistics.duration + ) elif not tail_only and start_time is not None and start_time.minute: head_start_time = start_time - head_end_time = start_time.replace( - minute=0, second=0, microsecond=0 - ) + timedelta(hours=1) + head_end_time = ( + start_time.replace(minute=0, second=0, microsecond=0) + + Statistics.duration + ) # Calculate the tail period tail_start_time: datetime | None = None From 6df7c34aa290616159ab16f59778bfc73b81f5e7 Mon Sep 17 00:00:00 2001 From: Sebastian Schneider Date: Tue, 11 Jun 2024 15:22:49 +0200 Subject: [PATCH 0531/1445] Add switch to Tuya thermostat: child_lock (#113052) --- homeassistant/components/tuya/switch.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/homeassistant/components/tuya/switch.py b/homeassistant/components/tuya/switch.py index b33852870a8..0f893aecb42 100644 --- a/homeassistant/components/tuya/switch.py +++ b/homeassistant/components/tuya/switch.py @@ -520,6 +520,15 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { entity_category=EntityCategory.CONFIG, ), ), + # Thermostat + # https://developer.tuya.com/en/docs/iot/f?id=K9gf45ld5l0t9 + "wk": ( + SwitchEntityDescription( + key=DPCode.CHILD_LOCK, + translation_key="child_lock", + entity_category=EntityCategory.CONFIG, + ), + ), # Thermostatic Radiator Valve # Not documented "wkf": ( From 68a84c365db9817de16eb7701ad25a3e3638b9e8 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 11 Jun 2024 16:09:02 +0200 Subject: [PATCH 0532/1445] Fix incorrect constants in streamlabswater tests (#119399) --- tests/components/streamlabswater/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/components/streamlabswater/__init__.py b/tests/components/streamlabswater/__init__.py index f8776708887..c0f6cbf2bde 100644 --- a/tests/components/streamlabswater/__init__.py +++ b/tests/components/streamlabswater/__init__.py @@ -1,7 +1,7 @@ """Tests for the StreamLabs integration.""" from homeassistant.core import HomeAssistant -from homeassistant.util.unit_system import IMPERIAL_SYSTEM +from homeassistant.util.unit_system import US_CUSTOMARY_SYSTEM from tests.common import MockConfigEntry @@ -10,7 +10,7 @@ async def setup_integration(hass: HomeAssistant, config_entry: MockConfigEntry) """Fixture for setting up the component.""" config_entry.add_to_hass(hass) - hass.config.units = IMPERIAL_SYSTEM + hass.config.units = US_CUSTOMARY_SYSTEM await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() From 50356aa877c510fe0dc45bf7ae9c6e1981148c27 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 11 Jun 2024 16:09:53 +0200 Subject: [PATCH 0533/1445] Drop use of deprecated constant in zha tests (#119397) --- tests/components/zha/test_sensor.py | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/tests/components/zha/test_sensor.py b/tests/components/zha/test_sensor.py index 8a9c59c587c..86868ef65c2 100644 --- a/tests/components/zha/test_sensor.py +++ b/tests/components/zha/test_sensor.py @@ -1,5 +1,6 @@ """Test ZHA sensor.""" +from collections.abc import Callable from datetime import timedelta import math from typing import Any @@ -23,8 +24,6 @@ from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_UNIT_OF_MEASUREMENT, CONF_UNIT_SYSTEM, - CONF_UNIT_SYSTEM_IMPERIAL, - CONF_UNIT_SYSTEM_METRIC, LIGHT_LUX, PERCENTAGE, STATE_UNAVAILABLE, @@ -633,10 +632,10 @@ def assert_state(hass: HomeAssistant, entity_id, state, unit_of_measurement): @pytest.fixture -def hass_ms(hass: HomeAssistant): +def hass_ms(hass: HomeAssistant) -> Callable[[str], HomeAssistant]: """Hass instance with measurement system.""" - async def _hass_ms(meas_sys): + async def _hass_ms(meas_sys: str) -> HomeAssistant: await config_util.async_process_ha_core_config( hass, {CONF_UNIT_SYSTEM: meas_sys} ) @@ -688,11 +687,11 @@ def core_rs(hass_storage: dict[str, Any]): ) async def test_temp_uom( hass: HomeAssistant, - uom, - raw_temp, - expected, - restore, - hass_ms, + uom: UnitOfTemperature, + raw_temp: int, + expected: int, + restore: bool, + hass_ms: Callable[[str], HomeAssistant], core_rs, zigpy_device_mock, zha_device_restored, @@ -704,11 +703,7 @@ async def test_temp_uom( core_rs(entity_id, uom, state=(expected - 2)) await async_mock_load_restore_state_from_storage(hass) - hass = await hass_ms( - CONF_UNIT_SYSTEM_METRIC - if uom == UnitOfTemperature.CELSIUS - else CONF_UNIT_SYSTEM_IMPERIAL - ) + hass = await hass_ms("metric" if uom == UnitOfTemperature.CELSIUS else "imperial") zigpy_device = zigpy_device_mock( { From 4bca0ad9560d768c473b7f398590e5e74a88abb3 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 11 Jun 2024 16:10:17 +0200 Subject: [PATCH 0534/1445] Fix incorrect constants in google_travel_time tests (#119395) --- tests/components/google_travel_time/test_sensor.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/components/google_travel_time/test_sensor.py b/tests/components/google_travel_time/test_sensor.py index a7fb263d4c9..57f3d7a0b98 100644 --- a/tests/components/google_travel_time/test_sensor.py +++ b/tests/components/google_travel_time/test_sensor.py @@ -9,8 +9,9 @@ from homeassistant.components.google_travel_time.const import ( CONF_ARRIVAL_TIME, CONF_DEPARTURE_TIME, DOMAIN, + UNITS_IMPERIAL, + UNITS_METRIC, ) -from homeassistant.const import CONF_UNIT_SYSTEM_IMPERIAL, CONF_UNIT_SYSTEM_METRIC from homeassistant.core import HomeAssistant from homeassistant.util.unit_system import ( METRIC_SYSTEM, @@ -208,8 +209,8 @@ async def test_sensor_arrival_time_custom_timestamp(hass: HomeAssistant) -> None @pytest.mark.parametrize( ("unit_system", "expected_unit_option"), [ - (METRIC_SYSTEM, CONF_UNIT_SYSTEM_METRIC), - (US_CUSTOMARY_SYSTEM, CONF_UNIT_SYSTEM_IMPERIAL), + (METRIC_SYSTEM, UNITS_METRIC), + (US_CUSTOMARY_SYSTEM, UNITS_IMPERIAL), ], ) async def test_sensor_unit_system( From fce4fc663e2190c06824c5d6bd867006bd941c82 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 11 Jun 2024 16:10:34 +0200 Subject: [PATCH 0535/1445] Fix import-outside-toplevel pylint warnings in core tests (#119394) --- tests/conftest.py | 6 ++---- tests/helpers/test_frame.py | 2 ++ tests/helpers/test_sun.py | 10 ++-------- 3 files changed, 6 insertions(+), 12 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 01607484d70..4e720bc0bd2 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -26,6 +26,7 @@ from aiohttp.test_utils import ( ) from aiohttp.typedefs import JSONDecoder from aiohttp.web import Application +import bcrypt import freezegun import multidict import pytest @@ -70,6 +71,7 @@ from homeassistant.helpers import ( recorder as recorder_helper, ) from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.translation import _TranslationsCacheData from homeassistant.helpers.typing import ConfigType from homeassistant.setup import BASE_PLATFORMS, async_setup_component from homeassistant.util import location @@ -394,8 +396,6 @@ def reset_hass_threading_local_object() -> Generator[None]: @pytest.fixture(scope="session", autouse=True) def bcrypt_cost() -> Generator[None]: """Run with reduced rounds during tests, to speed up uses.""" - import bcrypt - gensalt_orig = bcrypt.gensalt def gensalt_mock(rounds=12, prefix=b"2b"): @@ -1174,8 +1174,6 @@ def mock_get_source_ip() -> Generator[_patch]: @pytest.fixture(autouse=True, scope="session") def translations_once() -> Generator[_patch]: """Only load translations once per session.""" - from homeassistant.helpers.translation import _TranslationsCacheData - cache = _TranslationsCacheData({}, {}) patcher = patch( "homeassistant.helpers.translation._TranslationsCacheData", diff --git a/tests/helpers/test_frame.py b/tests/helpers/test_frame.py index e6251963d36..b0b4a0be6ee 100644 --- a/tests/helpers/test_frame.py +++ b/tests/helpers/test_frame.py @@ -36,6 +36,7 @@ async def test_extract_frame_resolve_module( hass: HomeAssistant, enable_custom_integrations ) -> None: """Test extracting the current frame from integration context.""" + # pylint: disable-next=import-outside-toplevel from custom_components.test_integration_frame import call_get_integration_frame integration_frame = call_get_integration_frame() @@ -53,6 +54,7 @@ async def test_get_integration_logger_resolve_module( hass: HomeAssistant, enable_custom_integrations ) -> None: """Test getting the logger from integration context.""" + # pylint: disable-next=import-outside-toplevel from custom_components.test_integration_frame import call_get_integration_logger logger = call_get_integration_logger(__name__) diff --git a/tests/helpers/test_sun.py b/tests/helpers/test_sun.py index da436d799aa..54c26997422 100644 --- a/tests/helpers/test_sun.py +++ b/tests/helpers/test_sun.py @@ -2,6 +2,8 @@ from datetime import datetime, timedelta +from astral import LocationInfo +import astral.sun from freezegun import freeze_time import pytest @@ -14,8 +16,6 @@ import homeassistant.util.dt as dt_util def test_next_events(hass: HomeAssistant) -> None: """Test retrieving next sun events.""" utc_now = datetime(2016, 11, 1, 8, 0, 0, tzinfo=dt_util.UTC) - from astral import LocationInfo - import astral.sun utc_today = utc_now.date() @@ -89,8 +89,6 @@ def test_next_events(hass: HomeAssistant) -> None: def test_date_events(hass: HomeAssistant) -> None: """Test retrieving next sun events.""" utc_now = datetime(2016, 11, 1, 8, 0, 0, tzinfo=dt_util.UTC) - from astral import LocationInfo - import astral.sun utc_today = utc_now.date() @@ -116,8 +114,6 @@ def test_date_events(hass: HomeAssistant) -> None: def test_date_events_default_date(hass: HomeAssistant) -> None: """Test retrieving next sun events.""" utc_now = datetime(2016, 11, 1, 8, 0, 0, tzinfo=dt_util.UTC) - from astral import LocationInfo - import astral.sun utc_today = utc_now.date() @@ -144,8 +140,6 @@ def test_date_events_default_date(hass: HomeAssistant) -> None: def test_date_events_accepts_datetime(hass: HomeAssistant) -> None: """Test retrieving next sun events.""" utc_now = datetime(2016, 11, 1, 8, 0, 0, tzinfo=dt_util.UTC) - from astral import LocationInfo - import astral.sun utc_today = utc_now.date() From bdf69c2e5b442f54d479e18bc07e782383cc76cb Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 11 Jun 2024 16:11:10 +0200 Subject: [PATCH 0536/1445] Remove deprecated imports in config tests (#119393) --- tests/components/config/test_core.py | 9 ++----- tests/test_config.py | 40 +++++++++++++--------------- 2 files changed, 20 insertions(+), 29 deletions(-) diff --git a/tests/components/config/test_core.py b/tests/components/config/test_core.py index 366a3d31b9b..3ee3e3334ea 100644 --- a/tests/components/config/test_core.py +++ b/tests/components/config/test_core.py @@ -9,11 +9,6 @@ from homeassistant.bootstrap import async_setup_component from homeassistant.components import config from homeassistant.components.config import core from homeassistant.components.websocket_api import TYPE_RESULT -from homeassistant.const import ( - CONF_UNIT_SYSTEM, - CONF_UNIT_SYSTEM_IMPERIAL, - CONF_UNIT_SYSTEM_METRIC, -) from homeassistant.core import HomeAssistant from homeassistant.util import dt as dt_util, location from homeassistant.util.unit_system import US_CUSTOMARY_SYSTEM @@ -140,7 +135,7 @@ async def test_websocket_core_update(hass: HomeAssistant, client) -> None: "longitude": 50, "elevation": 25, "location_name": "Huis", - CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_IMPERIAL, + "unit_system": "imperial", "time_zone": "America/New_York", "external_url": "https://www.example.com", "internal_url": "http://example.local", @@ -181,7 +176,7 @@ async def test_websocket_core_update(hass: HomeAssistant, client) -> None: { "id": 6, "type": "config/core/update", - CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_METRIC, + "unit_system": "metric", "update_units": True, } ) diff --git a/tests/test_config.py b/tests/test_config.py index 7f6183de2e3..1178b41398c 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -16,7 +16,7 @@ import voluptuous as vol from voluptuous import Invalid, MultipleInvalid import yaml -from homeassistant import config, loader +from homeassistant import loader import homeassistant.config as config_util from homeassistant.const import ( ATTR_ASSUMED_STATE, @@ -27,9 +27,6 @@ from homeassistant.const import ( CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, - CONF_UNIT_SYSTEM, - CONF_UNIT_SYSTEM_IMPERIAL, - CONF_UNIT_SYSTEM_METRIC, __version__, ) from homeassistant.core import ( @@ -49,7 +46,6 @@ from homeassistant.helpers.typing import ConfigType from homeassistant.loader import Integration, async_get_integration from homeassistant.setup import async_setup_component from homeassistant.util.unit_system import ( - _CONF_UNIT_SYSTEM_US_CUSTOMARY, METRIC_SYSTEM, US_CUSTOMARY_SYSTEM, UnitSystem, @@ -511,7 +507,7 @@ async def test_create_default_config_returns_none_if_write_error( def test_core_config_schema() -> None: """Test core config schema.""" for value in ( - {CONF_UNIT_SYSTEM: "K"}, + {"unit_system": "K"}, {"time_zone": "non-exist"}, {"latitude": "91"}, {"longitude": -181}, @@ -534,7 +530,7 @@ def test_core_config_schema() -> None: "longitude": "123.45", "external_url": "https://www.example.com", "internal_url": "http://example.local", - CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_METRIC, + "unit_system": "metric", "currency": "USD", "customize": {"sensor.temperature": {"hidden": True}}, "country": "SE", @@ -850,7 +846,7 @@ async def test_loading_configuration(hass: HomeAssistant) -> None: "longitude": 50, "elevation": 25, "name": "Huis", - CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_IMPERIAL, + "unit_system": "imperial", "time_zone": "America/New_York", "allowlist_external_dirs": "/etc", "external_url": "https://www.example.com", @@ -982,7 +978,7 @@ async def test_loading_configuration_from_packages(hass: HomeAssistant) -> None: "longitude": -1, "elevation": 500, "name": "Huis", - CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_METRIC, + "unit_system": "metric", "time_zone": "Europe/Madrid", "external_url": "https://www.example.com", "internal_url": "http://example.local", @@ -1006,7 +1002,7 @@ async def test_loading_configuration_from_packages(hass: HomeAssistant) -> None: "longitude": -1, "elevation": 500, "name": "Huis", - CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_METRIC, + "unit_system": "metric", "time_zone": "Europe/Madrid", "packages": {"empty_package": None}, }, @@ -1016,9 +1012,9 @@ async def test_loading_configuration_from_packages(hass: HomeAssistant) -> None: @pytest.mark.parametrize( ("unit_system_name", "expected_unit_system"), [ - (CONF_UNIT_SYSTEM_METRIC, METRIC_SYSTEM), - (CONF_UNIT_SYSTEM_IMPERIAL, US_CUSTOMARY_SYSTEM), - (_CONF_UNIT_SYSTEM_US_CUSTOMARY, US_CUSTOMARY_SYSTEM), + ("metric", METRIC_SYSTEM), + ("imperial", US_CUSTOMARY_SYSTEM), + ("us_customary", US_CUSTOMARY_SYSTEM), ], ) async def test_loading_configuration_unit_system( @@ -1295,7 +1291,7 @@ async def test_merge_customize(hass: HomeAssistant) -> None: "longitude": 50, "elevation": 25, "name": "Huis", - CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_IMPERIAL, + "unit_system": "imperial", "time_zone": "GMT", "customize": {"a.a": {"friendly_name": "A"}}, "packages": { @@ -1314,7 +1310,7 @@ async def test_auth_provider_config(hass: HomeAssistant) -> None: "longitude": 50, "elevation": 25, "name": "Huis", - CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_IMPERIAL, + "unit_system": "imperial", "time_zone": "GMT", CONF_AUTH_PROVIDERS: [ {"type": "homeassistant"}, @@ -1341,7 +1337,7 @@ async def test_auth_provider_config_default(hass: HomeAssistant) -> None: "longitude": 50, "elevation": 25, "name": "Huis", - CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_IMPERIAL, + "unit_system": "imperial", "time_zone": "GMT", } if hasattr(hass, "auth"): @@ -1361,7 +1357,7 @@ async def test_disallowed_auth_provider_config(hass: HomeAssistant) -> None: "longitude": 50, "elevation": 25, "name": "Huis", - CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_IMPERIAL, + "unit_system": "imperial", "time_zone": "GMT", CONF_AUTH_PROVIDERS: [ { @@ -1387,7 +1383,7 @@ async def test_disallowed_duplicated_auth_provider_config(hass: HomeAssistant) - "longitude": 50, "elevation": 25, "name": "Huis", - CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_IMPERIAL, + "unit_system": "imperial", "time_zone": "GMT", CONF_AUTH_PROVIDERS: [{"type": "homeassistant"}, {"type": "homeassistant"}], } @@ -1402,7 +1398,7 @@ async def test_disallowed_auth_mfa_module_config(hass: HomeAssistant) -> None: "longitude": 50, "elevation": 25, "name": "Huis", - CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_IMPERIAL, + "unit_system": "imperial", "time_zone": "GMT", CONF_AUTH_MFA_MODULES: [ { @@ -1424,7 +1420,7 @@ async def test_disallowed_duplicated_auth_mfa_module_config( "longitude": 50, "elevation": 25, "name": "Huis", - CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_IMPERIAL, + "unit_system": "imperial", "time_zone": "GMT", CONF_AUTH_MFA_MODULES: [{"type": "totp"}, {"type": "totp"}], } @@ -2459,7 +2455,7 @@ async def test_loading_platforms_gathers(hass: HomeAssistant) -> None: _load_platform, ): light_task = hass.async_create_task( - config.async_process_component_config( + config_util.async_process_component_config( hass, { "light": [ @@ -2472,7 +2468,7 @@ async def test_loading_platforms_gathers(hass: HomeAssistant) -> None: eager_start=True, ) sensor_task = hass.async_create_task( - config.async_process_component_config( + config_util.async_process_component_config( hass, { "sensor": [ From ea571a69979cbd861976cf87f2345033ff04b1c2 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 11 Jun 2024 16:18:16 +0200 Subject: [PATCH 0537/1445] Fix unnecessary-dunder-call pylint warnings in tests (#119379) --- tests/common.py | 2 +- tests/components/google_assistant_sdk/test_init.py | 1 + tests/components/google_assistant_sdk/test_notify.py | 1 + tests/components/usb/test_init.py | 1 + 4 files changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/common.py b/tests/common.py index 9faf7e10712..3f1dea4b720 100644 --- a/tests/common.py +++ b/tests/common.py @@ -1680,7 +1680,7 @@ def import_and_test_deprecated_alias( def help_test_all(module: ModuleType) -> None: """Test module.__all__ is correctly set.""" assert set(module.__all__) == { - itm for itm in module.__dir__() if not itm.startswith("_") + itm for itm in dir(module) if not itm.startswith("_") } diff --git a/tests/components/google_assistant_sdk/test_init.py b/tests/components/google_assistant_sdk/test_init.py index 11b3fbaa03f..f986497ed29 100644 --- a/tests/components/google_assistant_sdk/test_init.py +++ b/tests/components/google_assistant_sdk/test_init.py @@ -149,6 +149,7 @@ async def test_send_text_command( mock_text_assistant.assert_called_once_with( ExpectedCredentials(), expected_language_code, audio_out=False ) + # pylint:disable-next=unnecessary-dunder-call mock_text_assistant.assert_has_calls([call().__enter__().assist(command)]) diff --git a/tests/components/google_assistant_sdk/test_notify.py b/tests/components/google_assistant_sdk/test_notify.py index 0ffdc3c5660..266846b17e1 100644 --- a/tests/components/google_assistant_sdk/test_notify.py +++ b/tests/components/google_assistant_sdk/test_notify.py @@ -50,6 +50,7 @@ async def test_broadcast_no_targets( mock_text_assistant.assert_called_once_with( ExpectedCredentials(), language_code, audio_out=False ) + # pylint:disable-next=unnecessary-dunder-call mock_text_assistant.assert_has_calls([call().__enter__().assist(expected_command)]) diff --git a/tests/components/usb/test_init.py b/tests/components/usb/test_init.py index ce7484a811c..effc63bf8aa 100644 --- a/tests/components/usb/test_init.py +++ b/tests/components/usb/test_init.py @@ -108,6 +108,7 @@ async def test_observer_discovery( hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) await hass.async_block_till_done() + # pylint:disable-next=unnecessary-dunder-call assert mock_observer.mock_calls == [call.start(), call.__bool__(), call.stop()] From 8620bef5b041db29f1658963ac26e7d344c8cbe9 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 11 Jun 2024 16:31:19 +0200 Subject: [PATCH 0538/1445] Support shared keys starting with period in services.yaml (#118789) --- homeassistant/components/light/services.yaml | 361 ++++++++++--------- homeassistant/helpers/service.py | 15 +- script/hassfest/services.py | 5 +- tests/common.py | 7 +- tests/helpers/test_service.py | 55 +++ 5 files changed, 262 insertions(+), 181 deletions(-) diff --git a/homeassistant/components/light/services.yaml b/homeassistant/components/light/services.yaml index 4f9f4e03b89..6183d2a49df 100644 --- a/homeassistant/components/light/services.yaml +++ b/homeassistant/components/light/services.yaml @@ -1,4 +1,184 @@ # Describes the format for available light services +.brightness_support: &brightness_support + attribute: + supported_color_modes: + - light.ColorMode.BRIGHTNESS + - light.ColorMode.COLOR_TEMP + - light.ColorMode.HS + - light.ColorMode.XY + - light.ColorMode.RGB + - light.ColorMode.RGBW + - light.ColorMode.RGBWW + +.color_support: &color_support + attribute: + supported_color_modes: + - light.ColorMode.HS + - light.ColorMode.XY + - light.ColorMode.RGB + - light.ColorMode.RGBW + - light.ColorMode.RGBWW + +.color_temp_support: &color_temp_support + attribute: + supported_color_modes: + - light.ColorMode.COLOR_TEMP + - light.ColorMode.HS + - light.ColorMode.XY + - light.ColorMode.RGB + - light.ColorMode.RGBW + - light.ColorMode.RGBWW + +.named_colors: &named_colors + - "homeassistant" + - "aliceblue" + - "antiquewhite" + - "aqua" + - "aquamarine" + - "azure" + - "beige" + - "bisque" + # Black is omitted from this list as nonsensical for lights + - "blanchedalmond" + - "blue" + - "blueviolet" + - "brown" + - "burlywood" + - "cadetblue" + - "chartreuse" + - "chocolate" + - "coral" + - "cornflowerblue" + - "cornsilk" + - "crimson" + - "cyan" + - "darkblue" + - "darkcyan" + - "darkgoldenrod" + - "darkgray" + - "darkgreen" + - "darkgrey" + - "darkkhaki" + - "darkmagenta" + - "darkolivegreen" + - "darkorange" + - "darkorchid" + - "darkred" + - "darksalmon" + - "darkseagreen" + - "darkslateblue" + - "darkslategray" + - "darkslategrey" + - "darkturquoise" + - "darkviolet" + - "deeppink" + - "deepskyblue" + - "dimgray" + - "dimgrey" + - "dodgerblue" + - "firebrick" + - "floralwhite" + - "forestgreen" + - "fuchsia" + - "gainsboro" + - "ghostwhite" + - "gold" + - "goldenrod" + - "gray" + - "green" + - "greenyellow" + - "grey" + - "honeydew" + - "hotpink" + - "indianred" + - "indigo" + - "ivory" + - "khaki" + - "lavender" + - "lavenderblush" + - "lawngreen" + - "lemonchiffon" + - "lightblue" + - "lightcoral" + - "lightcyan" + - "lightgoldenrodyellow" + - "lightgray" + - "lightgreen" + - "lightgrey" + - "lightpink" + - "lightsalmon" + - "lightseagreen" + - "lightskyblue" + - "lightslategray" + - "lightslategrey" + - "lightsteelblue" + - "lightyellow" + - "lime" + - "limegreen" + - "linen" + - "magenta" + - "maroon" + - "mediumaquamarine" + - "mediumblue" + - "mediumorchid" + - "mediumpurple" + - "mediumseagreen" + - "mediumslateblue" + - "mediumspringgreen" + - "mediumturquoise" + - "mediumvioletred" + - "midnightblue" + - "mintcream" + - "mistyrose" + - "moccasin" + - "navajowhite" + - "navy" + - "navyblue" + - "oldlace" + - "olive" + - "olivedrab" + - "orange" + - "orangered" + - "orchid" + - "palegoldenrod" + - "palegreen" + - "paleturquoise" + - "palevioletred" + - "papayawhip" + - "peachpuff" + - "peru" + - "pink" + - "plum" + - "powderblue" + - "purple" + - "red" + - "rosybrown" + - "royalblue" + - "saddlebrown" + - "salmon" + - "sandybrown" + - "seagreen" + - "seashell" + - "sienna" + - "silver" + - "skyblue" + - "slateblue" + - "slategray" + - "slategrey" + - "snow" + - "springgreen" + - "steelblue" + - "tan" + - "teal" + - "thistle" + - "tomato" + - "turquoise" + - "violet" + - "wheat" + - "white" + - "whitesmoke" + - "yellow" + - "yellowgreen" turn_on: target: @@ -15,14 +195,7 @@ turn_on: max: 300 unit_of_measurement: seconds rgb_color: &rgb_color - filter: &color_support - attribute: - supported_color_modes: - - light.ColorMode.HS - - light.ColorMode.XY - - light.ColorMode.RGB - - light.ColorMode.RGBW - - light.ColorMode.RGBWW + filter: *color_support example: "[255, 100, 100]" selector: color_rgb: @@ -44,156 +217,7 @@ turn_on: selector: select: translation_key: color_name - options: &named_colors - - "homeassistant" - - "aliceblue" - - "antiquewhite" - - "aqua" - - "aquamarine" - - "azure" - - "beige" - - "bisque" - # Black is omitted from this list as nonsensical for lights - - "blanchedalmond" - - "blue" - - "blueviolet" - - "brown" - - "burlywood" - - "cadetblue" - - "chartreuse" - - "chocolate" - - "coral" - - "cornflowerblue" - - "cornsilk" - - "crimson" - - "cyan" - - "darkblue" - - "darkcyan" - - "darkgoldenrod" - - "darkgray" - - "darkgreen" - - "darkgrey" - - "darkkhaki" - - "darkmagenta" - - "darkolivegreen" - - "darkorange" - - "darkorchid" - - "darkred" - - "darksalmon" - - "darkseagreen" - - "darkslateblue" - - "darkslategray" - - "darkslategrey" - - "darkturquoise" - - "darkviolet" - - "deeppink" - - "deepskyblue" - - "dimgray" - - "dimgrey" - - "dodgerblue" - - "firebrick" - - "floralwhite" - - "forestgreen" - - "fuchsia" - - "gainsboro" - - "ghostwhite" - - "gold" - - "goldenrod" - - "gray" - - "green" - - "greenyellow" - - "grey" - - "honeydew" - - "hotpink" - - "indianred" - - "indigo" - - "ivory" - - "khaki" - - "lavender" - - "lavenderblush" - - "lawngreen" - - "lemonchiffon" - - "lightblue" - - "lightcoral" - - "lightcyan" - - "lightgoldenrodyellow" - - "lightgray" - - "lightgreen" - - "lightgrey" - - "lightpink" - - "lightsalmon" - - "lightseagreen" - - "lightskyblue" - - "lightslategray" - - "lightslategrey" - - "lightsteelblue" - - "lightyellow" - - "lime" - - "limegreen" - - "linen" - - "magenta" - - "maroon" - - "mediumaquamarine" - - "mediumblue" - - "mediumorchid" - - "mediumpurple" - - "mediumseagreen" - - "mediumslateblue" - - "mediumspringgreen" - - "mediumturquoise" - - "mediumvioletred" - - "midnightblue" - - "mintcream" - - "mistyrose" - - "moccasin" - - "navajowhite" - - "navy" - - "navyblue" - - "oldlace" - - "olive" - - "olivedrab" - - "orange" - - "orangered" - - "orchid" - - "palegoldenrod" - - "palegreen" - - "paleturquoise" - - "palevioletred" - - "papayawhip" - - "peachpuff" - - "peru" - - "pink" - - "plum" - - "powderblue" - - "purple" - - "red" - - "rosybrown" - - "royalblue" - - "saddlebrown" - - "salmon" - - "sandybrown" - - "seagreen" - - "seashell" - - "sienna" - - "silver" - - "skyblue" - - "slateblue" - - "slategray" - - "slategrey" - - "snow" - - "springgreen" - - "steelblue" - - "tan" - - "teal" - - "thistle" - - "tomato" - - "turquoise" - - "violet" - - "wheat" - - "white" - - "whitesmoke" - - "yellow" - - "yellowgreen" + options: *named_colors hs_color: &hs_color filter: *color_support advanced: true @@ -207,15 +231,7 @@ turn_on: selector: object: color_temp: &color_temp - filter: &color_temp_support - attribute: - supported_color_modes: - - light.ColorMode.COLOR_TEMP - - light.ColorMode.HS - - light.ColorMode.XY - - light.ColorMode.RGB - - light.ColorMode.RGBW - - light.ColorMode.RGBWW + filter: *color_temp_support advanced: true selector: color_temp: @@ -230,16 +246,7 @@ turn_on: min: 2000 max: 6500 brightness: &brightness - filter: &brightness_support - attribute: - supported_color_modes: - - light.ColorMode.BRIGHTNESS - - light.ColorMode.COLOR_TEMP - - light.ColorMode.HS - - light.ColorMode.XY - - light.ColorMode.RGB - - light.ColorMode.RGBW - - light.ColorMode.RGBWW + filter: *brightness_support advanced: true selector: number: diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index 3a828ada9c2..a9959902084 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -187,7 +187,20 @@ _SERVICE_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) -_SERVICES_SCHEMA = vol.Schema({cv.slug: vol.Any(None, _SERVICE_SCHEMA)}) + +def starts_with_dot(key: str) -> str: + """Check if key starts with dot.""" + if not key.startswith("."): + raise vol.Invalid("Key does not start with .") + return key + + +_SERVICES_SCHEMA = vol.Schema( + { + vol.Remove(vol.All(str, starts_with_dot)): object, + cv.slug: vol.Any(None, _SERVICE_SCHEMA), + } +) class ServiceParams(TypedDict): diff --git a/script/hassfest/services.py b/script/hassfest/services.py index c962d84e6e1..ea4503d5410 100644 --- a/script/hassfest/services.py +++ b/script/hassfest/services.py @@ -78,7 +78,10 @@ CUSTOM_INTEGRATION_SERVICE_SCHEMA = vol.Any( ) CORE_INTEGRATION_SERVICES_SCHEMA = vol.Schema( - {cv.slug: CORE_INTEGRATION_SERVICE_SCHEMA} + { + vol.Remove(vol.All(str, service.starts_with_dot)): object, + cv.slug: CORE_INTEGRATION_SERVICE_SCHEMA, + } ) CUSTOM_INTEGRATION_SERVICES_SCHEMA = vol.Schema( {cv.slug: CUSTOM_INTEGRATION_SERVICE_SCHEMA} diff --git a/tests/common.py b/tests/common.py index 3f1dea4b720..cf5469e1cd2 100644 --- a/tests/common.py +++ b/tests/common.py @@ -1432,7 +1432,10 @@ def mock_config_flow(domain: str, config_flow: type[ConfigFlow]) -> None: def mock_integration( - hass: HomeAssistant, module: MockModule, built_in: bool = True + hass: HomeAssistant, + module: MockModule, + built_in: bool = True, + top_level_files: set[str] | None = None, ) -> loader.Integration: """Mock an integration.""" integration = loader.Integration( @@ -1442,7 +1445,7 @@ def mock_integration( else f"{loader.PACKAGE_CUSTOM_COMPONENTS}.{module.DOMAIN}", pathlib.Path(""), module.mock_manifest(), - set(), + top_level_files, ) def mock_import_platform(platform_name: str) -> NoReturn: diff --git a/tests/helpers/test_service.py b/tests/helpers/test_service.py index c9d92c2f25a..60fe87db9d2 100644 --- a/tests/helpers/test_service.py +++ b/tests/helpers/test_service.py @@ -3,6 +3,7 @@ import asyncio from collections.abc import Iterable from copy import deepcopy +import io from typing import Any from unittest.mock import AsyncMock, Mock, patch @@ -43,13 +44,16 @@ from homeassistant.helpers import ( import homeassistant.helpers.config_validation as cv from homeassistant.loader import async_get_integration from homeassistant.setup import async_setup_component +from homeassistant.util.yaml.loader import parse_yaml from tests.common import ( MockEntity, + MockModule, MockUser, async_mock_service, mock_area_registry, mock_device_registry, + mock_integration, mock_registry, ) @@ -916,6 +920,57 @@ async def test_async_get_all_descriptions(hass: HomeAssistant) -> None: assert await service.async_get_all_descriptions(hass) is descriptions +async def test_async_get_all_descriptions_dot_keys(hass: HomeAssistant) -> None: + """Test async_get_all_descriptions with keys starting with a period.""" + service_descriptions = """ + .anchor: &anchor + selector: + text: + test_service: + fields: + test: *anchor + """ + + domain = "test_domain" + + hass.services.async_register(domain, "test_service", lambda call: None) + mock_integration(hass, MockModule(domain), top_level_files={"services.yaml"}) + assert await async_setup_component(hass, domain, {}) + + def load_yaml(fname, secrets=None): + with io.StringIO(service_descriptions) as file: + return parse_yaml(file) + + with ( + patch( + "homeassistant.helpers.service._load_services_files", + side_effect=service._load_services_files, + ) as proxy_load_services_files, + patch( + "homeassistant.util.yaml.loader.load_yaml", + side_effect=load_yaml, + ) as mock_load_yaml, + ): + descriptions = await service.async_get_all_descriptions(hass) + + mock_load_yaml.assert_called_once_with("services.yaml", None) + assert proxy_load_services_files.mock_calls[0][1][1] == unordered( + [ + await async_get_integration(hass, domain), + ] + ) + + assert descriptions == { + "test_domain": { + "test_service": { + "description": "", + "fields": {"test": {"selector": {"text": None}}}, + "name": "", + } + } + } + + async def test_async_get_all_descriptions_failing_integration( hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: From 5531e547458983980b9c08f3f47c16aa8b352d36 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 11 Jun 2024 16:37:07 +0200 Subject: [PATCH 0539/1445] Ignore no-name-in-module warnings in tests (#119401) --- tests/components/bluetooth/test_advertisement_tracker.py | 1 + tests/components/bluetooth/test_base_scanner.py | 2 ++ tests/components/bluetooth/test_manager.py | 2 ++ tests/components/private_ble_device/test_device_tracker.py | 1 + tests/components/private_ble_device/test_sensor.py | 1 + tests/components/samsungtv/conftest.py | 2 +- 6 files changed, 8 insertions(+), 1 deletion(-) diff --git a/tests/components/bluetooth/test_advertisement_tracker.py b/tests/components/bluetooth/test_advertisement_tracker.py index 85feca83f00..57fd8354148 100644 --- a/tests/components/bluetooth/test_advertisement_tracker.py +++ b/tests/components/bluetooth/test_advertisement_tracker.py @@ -3,6 +3,7 @@ from datetime import timedelta import time +# pylint: disable-next=no-name-in-module from habluetooth.advertisement_tracker import ADVERTISING_TIMES_NEEDED import pytest diff --git a/tests/components/bluetooth/test_base_scanner.py b/tests/components/bluetooth/test_base_scanner.py index efd9708a167..abfbbaa15ab 100644 --- a/tests/components/bluetooth/test_base_scanner.py +++ b/tests/components/bluetooth/test_base_scanner.py @@ -9,6 +9,8 @@ from unittest.mock import patch from bleak.backends.device import BLEDevice from bleak.backends.scanner import AdvertisementData + +# pylint: disable-next=no-name-in-module from habluetooth.advertisement_tracker import TRACKER_BUFFERING_WOBBLE_SECONDS import pytest diff --git a/tests/components/bluetooth/test_manager.py b/tests/components/bluetooth/test_manager.py index 6a607838d36..4bff7cbe94d 100644 --- a/tests/components/bluetooth/test_manager.py +++ b/tests/components/bluetooth/test_manager.py @@ -7,6 +7,8 @@ from unittest.mock import patch from bleak.backends.scanner import AdvertisementData, BLEDevice from bluetooth_adapters import AdvertisementHistory + +# pylint: disable-next=no-name-in-module from habluetooth.advertisement_tracker import TRACKER_BUFFERING_WOBBLE_SECONDS import pytest from typing_extensions import Generator diff --git a/tests/components/private_ble_device/test_device_tracker.py b/tests/components/private_ble_device/test_device_tracker.py index 8fd1f694d84..02b0dd14df8 100644 --- a/tests/components/private_ble_device/test_device_tracker.py +++ b/tests/components/private_ble_device/test_device_tracker.py @@ -2,6 +2,7 @@ import time +# pylint: disable-next=no-name-in-module from habluetooth.advertisement_tracker import ADVERTISING_TIMES_NEEDED import pytest diff --git a/tests/components/private_ble_device/test_sensor.py b/tests/components/private_ble_device/test_sensor.py index b1ee10286e0..cb40fc4f0c2 100644 --- a/tests/components/private_ble_device/test_sensor.py +++ b/tests/components/private_ble_device/test_sensor.py @@ -1,5 +1,6 @@ """Tests for sensors.""" +# pylint: disable-next=no-name-in-module from habluetooth.advertisement_tracker import ADVERTISING_TIMES_NEEDED import pytest diff --git a/tests/components/samsungtv/conftest.py b/tests/components/samsungtv/conftest.py index 8518fc4c586..8d38adad06d 100644 --- a/tests/components/samsungtv/conftest.py +++ b/tests/components/samsungtv/conftest.py @@ -4,7 +4,7 @@ from __future__ import annotations from collections.abc import Awaitable, Callable from datetime import datetime -from socket import AddressFamily +from socket import AddressFamily # pylint: disable=no-name-in-module from typing import Any from unittest.mock import AsyncMock, Mock, patch From 7384140a60ade1393cf3c4ad8454477641aba681 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 11 Jun 2024 17:20:23 +0200 Subject: [PATCH 0540/1445] Fix pointless-exception-statement warning in tests (#119402) --- tests/common.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/common.py b/tests/common.py index cf5469e1cd2..1e53242c961 100644 --- a/tests/common.py +++ b/tests/common.py @@ -600,7 +600,7 @@ def mock_state_change_event( def mock_component(hass: HomeAssistant, component: str) -> None: """Mock a component is setup.""" if component in hass.config.components: - AssertionError(f"Integration {component} is already setup") + raise AssertionError(f"Integration {component} is already setup") hass.config.components.add(component) From 6bb9011db3735d0f3a4c9bcc5d302c90f7d7e9ec Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 11 Jun 2024 17:57:58 +0200 Subject: [PATCH 0541/1445] Fix use-implicit-booleaness-not-len warnings in tests (#119407) --- tests/components/alexa/test_smart_home.py | 2 +- tests/components/unifiprotect/test_services.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index 43d92f1a533..d502dce7d01 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -5643,6 +5643,6 @@ async def test_alexa_config( with patch.object(test_config, "_auth", AsyncMock()): test_config._auth.async_invalidate_access_token = MagicMock() test_config.async_invalidate_access_token() - assert len(test_config._auth.async_invalidate_access_token.mock_calls) + assert len(test_config._auth.async_invalidate_access_token.mock_calls) == 1 await test_config.async_accept_grant("grant_code") test_config._auth.async_do_auth.assert_called_once_with("grant_code") diff --git a/tests/components/unifiprotect/test_services.py b/tests/components/unifiprotect/test_services.py index 919af53ef10..0a90a2d5667 100644 --- a/tests/components/unifiprotect/test_services.py +++ b/tests/components/unifiprotect/test_services.py @@ -233,4 +233,4 @@ async def test_remove_privacy_zone( blocking=True, ) ufp.api.update_device.assert_called() - assert not len(doorbell.privacy_zones) + assert not doorbell.privacy_zones From 73882716898b94f82a002ddf4c60092ba6e05776 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 11 Jun 2024 17:58:40 +0200 Subject: [PATCH 0542/1445] Fix unspecified-encoding warnings in tests (#119405) --- tests/common.py | 2 +- tests/components/blueprint/test_importer.py | 4 ++-- .../blueprint/test_websocket_api.py | 6 +++--- .../color_extractor/test_service.py | 2 +- tests/components/folder/test_sensor.py | 2 +- .../components/google_assistant/test_http.py | 2 +- tests/components/kira/test_init.py | 2 +- tests/components/recorder/common.py | 2 +- .../recorder/test_statistics_v23_migration.py | 2 +- tests/helpers/test_storage.py | 4 ++-- tests/test_block_async_io.py | 6 +++--- tests/test_config.py | 19 +++++++++++-------- tests/test_core.py | 2 +- tests/util/test_file.py | 6 +++--- tests/util/test_json.py | 2 +- 15 files changed, 33 insertions(+), 30 deletions(-) diff --git a/tests/common.py b/tests/common.py index 1e53242c961..2606b510430 100644 --- a/tests/common.py +++ b/tests/common.py @@ -555,7 +555,7 @@ def get_fixture_path(filename: str, integration: str | None = None) -> pathlib.P @lru_cache def load_fixture(filename: str, integration: str | None = None) -> str: """Load a fixture.""" - return get_fixture_path(filename, integration).read_text() + return get_fixture_path(filename, integration).read_text(encoding="utf8") def load_json_value_fixture( diff --git a/tests/components/blueprint/test_importer.py b/tests/components/blueprint/test_importer.py index 2b1d697fce5..f135bbf23b8 100644 --- a/tests/components/blueprint/test_importer.py +++ b/tests/components/blueprint/test_importer.py @@ -138,7 +138,7 @@ async def test_fetch_blueprint_from_github_url( "https://raw.githubusercontent.com/balloob/home-assistant-config/main/blueprints/automation/motion_light.yaml", text=Path( hass.config.path("blueprints/automation/test_event_service.yaml") - ).read_text(), + ).read_text(encoding="utf8"), ) imported_blueprint = await importer.fetch_blueprint_from_url(hass, url) @@ -181,7 +181,7 @@ async def test_fetch_blueprint_from_website_url( "https://www.home-assistant.io/blueprints/awesome.yaml", text=Path( hass.config.path("blueprints/automation/test_event_service.yaml") - ).read_text(), + ).read_text(encoding="utf8"), ) url = "https://www.home-assistant.io/blueprints/awesome.yaml" diff --git a/tests/components/blueprint/test_websocket_api.py b/tests/components/blueprint/test_websocket_api.py index 21387f7763c..4052e7c3316 100644 --- a/tests/components/blueprint/test_websocket_api.py +++ b/tests/components/blueprint/test_websocket_api.py @@ -100,7 +100,7 @@ async def test_import_blueprint( """Test importing blueprints.""" raw_data = Path( hass.config.path("blueprints/automation/test_event_service.yaml") - ).read_text() + ).read_text(encoding="utf8") aioclient_mock.get( "https://raw.githubusercontent.com/balloob/home-assistant-config/main/blueprints/automation/motion_light.yaml", @@ -149,7 +149,7 @@ async def test_import_blueprint_update( """Test importing blueprints.""" raw_data = Path( hass.config.path("blueprints/automation/in_folder/in_folder_blueprint.yaml") - ).read_text() + ).read_text(encoding="utf8") aioclient_mock.get( "https://raw.githubusercontent.com/in_folder/home-assistant-config/main/blueprints/automation/in_folder_blueprint.yaml", @@ -192,7 +192,7 @@ async def test_save_blueprint( """Test saving blueprints.""" raw_data = Path( hass.config.path("blueprints/automation/test_event_service.yaml") - ).read_text() + ).read_text(encoding="utf8") with patch("pathlib.Path.write_text") as write_mock: client = await hass_ws_client(hass) diff --git a/tests/components/color_extractor/test_service.py b/tests/components/color_extractor/test_service.py index 941a0710067..7b603420bdf 100644 --- a/tests/components/color_extractor/test_service.py +++ b/tests/components/color_extractor/test_service.py @@ -243,7 +243,7 @@ def _get_file_mock(file_path): """Convert file to BytesIO for testing due to PIL UnidentifiedImageError.""" _file = None - with open(file_path) as file_handler: + with open(file_path, encoding="utf8") as file_handler: _file = io.BytesIO(file_handler.read()) _file.name = "color_extractor.jpg" diff --git a/tests/components/folder/test_sensor.py b/tests/components/folder/test_sensor.py index ad0969c6a0f..e71f1b3addc 100644 --- a/tests/components/folder/test_sensor.py +++ b/tests/components/folder/test_sensor.py @@ -15,7 +15,7 @@ TEST_FILE = os.path.join(TEST_DIR, TEST_TXT) def create_file(path): """Create a test file.""" - with open(path, "w") as test_file: + with open(path, "w", encoding="utf8") as test_file: test_file.write("test") diff --git a/tests/components/google_assistant/test_http.py b/tests/components/google_assistant/test_http.py index 416d569b286..b041f69828f 100644 --- a/tests/components/google_assistant/test_http.py +++ b/tests/components/google_assistant/test_http.py @@ -655,7 +655,7 @@ async def test_async_get_users( ) path = hass.config.config_dir / ".storage" / GoogleConfigStore._STORAGE_KEY os.makedirs(os.path.dirname(path), exist_ok=True) - with open(path, "w") as f: + with open(path, "w", encoding="utf8") as f: f.write(store_data) assert await async_get_users(hass) == expected_users diff --git a/tests/components/kira/test_init.py b/tests/components/kira/test_init.py index a200c25d2a3..e57519667ce 100644 --- a/tests/components/kira/test_init.py +++ b/tests/components/kira/test_init.py @@ -79,7 +79,7 @@ async def test_kira_creates_codes(work_dir) -> None: async def test_load_codes(work_dir) -> None: """Kira should ignore invalid codes.""" code_path = os.path.join(work_dir, "codes.yaml") - with open(code_path, "w") as code_file: + with open(code_path, "w", encoding="utf8") as code_file: code_file.write(KIRA_CODES) res = kira.load_codes(code_path) assert len(res) == 1, "Expected exactly 1 valid Kira code" diff --git a/tests/components/recorder/common.py b/tests/components/recorder/common.py index 2ded3513a7e..c72b1ac830b 100644 --- a/tests/components/recorder/common.py +++ b/tests/components/recorder/common.py @@ -138,7 +138,7 @@ async def async_recorder_block_till_done(hass: HomeAssistant) -> None: def corrupt_db_file(test_db_file): """Corrupt an sqlite3 database file.""" - with open(test_db_file, "w+") as fhandle: + with open(test_db_file, "w+", encoding="utf8") as fhandle: fhandle.seek(200) fhandle.write("I am a corrupt db" * 100) diff --git a/tests/components/recorder/test_statistics_v23_migration.py b/tests/components/recorder/test_statistics_v23_migration.py index ac48f0d0994..af784692612 100644 --- a/tests/components/recorder/test_statistics_v23_migration.py +++ b/tests/components/recorder/test_statistics_v23_migration.py @@ -556,7 +556,7 @@ def test_delete_duplicates_non_identical( isotime = dt_util.utcnow().isoformat() backup_file_name = f".storage/deleted_statistics.{isotime}.json" - with open(hass.config.path(backup_file_name)) as backup_file: + with open(hass.config.path(backup_file_name), encoding="utf8") as backup_file: backup = json.load(backup_file) assert backup == [ diff --git a/tests/helpers/test_storage.py b/tests/helpers/test_storage.py index 577e81d1a44..651c7ce5cbc 100644 --- a/tests/helpers/test_storage.py +++ b/tests/helpers/test_storage.py @@ -684,7 +684,7 @@ async def test_loading_corrupt_core_file( assert data == {"hello": "world"} def _corrupt_store(): - with open(store_file, "w") as f: + with open(store_file, "w", encoding="utf8") as f: f.write("corrupt") await hass.async_add_executor_job(_corrupt_store) @@ -745,7 +745,7 @@ async def test_loading_corrupt_file_known_domain( assert data == {"hello": "world"} def _corrupt_store(): - with open(store_file, "w") as f: + with open(store_file, "w", encoding="utf8") as f: f.write('{"valid":"json"}..with..corrupt') await hass.async_add_executor_job(_corrupt_store) diff --git a/tests/test_block_async_io.py b/tests/test_block_async_io.py index eab8033e37d..d011bdccdbe 100644 --- a/tests/test_block_async_io.py +++ b/tests/test_block_async_io.py @@ -208,7 +208,7 @@ async def test_protect_loop_open(caplog: pytest.LogCaptureFixture) -> None: """Test open of a file in /proc is not reported.""" block_async_io.enable() with contextlib.suppress(FileNotFoundError): - open("/proc/does_not_exist").close() + open("/proc/does_not_exist", encoding="utf8").close() assert "Detected blocking call to open with args" not in caplog.text @@ -216,7 +216,7 @@ async def test_protect_open(caplog: pytest.LogCaptureFixture) -> None: """Test opening a file in the event loop logs.""" block_async_io.enable() with contextlib.suppress(FileNotFoundError): - open("/config/data_not_exist").close() + open("/config/data_not_exist", encoding="utf8").close() assert "Detected blocking call to open with args" in caplog.text @@ -233,7 +233,7 @@ async def test_protect_open_path(path: Any, caplog: pytest.LogCaptureFixture) -> """Test opening a file by path in the event loop logs.""" block_async_io.enable() with contextlib.suppress(FileNotFoundError): - open(path).close() + open(path, encoding="utf8").close() assert "Detected blocking call to open with args" in caplog.text diff --git a/tests/test_config.py b/tests/test_config.py index 1178b41398c..27ef8059fd8 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -74,7 +74,7 @@ SAFE_MODE_PATH = os.path.join(CONFIG_DIR, config_util.SAFE_MODE_FILENAME) def create_file(path): """Create an empty file.""" - with open(path, "w"): + with open(path, "w", encoding="utf8"): pass @@ -414,7 +414,7 @@ async def test_ensure_config_exists_uses_existing_config(hass: HomeAssistant) -> create_file(YAML_PATH) await config_util.async_ensure_config_exists(hass) - with open(YAML_PATH) as fp: + with open(YAML_PATH, encoding="utf8") as fp: content = fp.read() # File created with create_file are empty @@ -427,7 +427,7 @@ async def test_ensure_existing_files_is_not_overwritten(hass: HomeAssistant) -> await config_util.async_create_default_config(hass) - with open(SECRET_PATH) as fp: + with open(SECRET_PATH, encoding="utf8") as fp: content = fp.read() # File created with create_file are empty @@ -443,7 +443,7 @@ def test_load_yaml_config_converts_empty_files_to_dict() -> None: def test_load_yaml_config_raises_error_if_not_dict() -> None: """Test error raised when YAML file is not a dict.""" - with open(YAML_PATH, "w") as fp: + with open(YAML_PATH, "w", encoding="utf8") as fp: fp.write("5") with pytest.raises(HomeAssistantError): @@ -452,7 +452,7 @@ def test_load_yaml_config_raises_error_if_not_dict() -> None: def test_load_yaml_config_raises_error_if_malformed_yaml() -> None: """Test error raised if invalid YAML.""" - with open(YAML_PATH, "w") as fp: + with open(YAML_PATH, "w", encoding="utf8") as fp: fp.write(":-") with pytest.raises(HomeAssistantError): @@ -461,7 +461,7 @@ def test_load_yaml_config_raises_error_if_malformed_yaml() -> None: def test_load_yaml_config_raises_error_if_unsafe_yaml() -> None: """Test error raised if unsafe YAML.""" - with open(YAML_PATH, "w") as fp: + with open(YAML_PATH, "w", encoding="utf8") as fp: fp.write("- !!python/object/apply:os.system []") with ( @@ -474,7 +474,10 @@ def test_load_yaml_config_raises_error_if_unsafe_yaml() -> None: # Here we validate that the test above is a good test # since previously the syntax was not valid - with open(YAML_PATH) as fp, patch.object(os, "system") as system_mock: + with ( + open(YAML_PATH, encoding="utf8") as fp, + patch.object(os, "system") as system_mock, + ): list(yaml.unsafe_load_all(fp)) assert len(system_mock.mock_calls) == 1 @@ -482,7 +485,7 @@ def test_load_yaml_config_raises_error_if_unsafe_yaml() -> None: def test_load_yaml_config_preserves_key_order() -> None: """Test removal of library.""" - with open(YAML_PATH, "w") as fp: + with open(YAML_PATH, "w", encoding="utf8") as fp: fp.write("hello: 2\n") fp.write("world: 1\n") diff --git a/tests/test_core.py b/tests/test_core.py index f8e96640fd1..71e6cb3f3b6 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -1997,7 +1997,7 @@ async def test_config_is_allowed_path() -> None: config.allowlist_external_dirs = {os.path.realpath(tmp_dir)} test_file = os.path.join(tmp_dir, "test.jpg") - with open(test_file, "w") as tmp_file: + with open(test_file, "w", encoding="utf8") as tmp_file: tmp_file.write("test") valid = [test_file, tmp_dir, os.path.join(tmp_dir, "notfound321")] diff --git a/tests/util/test_file.py b/tests/util/test_file.py index 2371998b1b9..efa3c1ab0d9 100644 --- a/tests/util/test_file.py +++ b/tests/util/test_file.py @@ -17,17 +17,17 @@ def test_write_utf8_file_atomic_private(tmpdir: py.path.local, func) -> None: test_file = Path(test_dir / "test.json") func(test_file, '{"some":"data"}', False) - with open(test_file) as fh: + with open(test_file, encoding="utf8") as fh: assert fh.read() == '{"some":"data"}' assert os.stat(test_file).st_mode & 0o777 == 0o644 func(test_file, '{"some":"data"}', True) - with open(test_file) as fh: + with open(test_file, encoding="utf8") as fh: assert fh.read() == '{"some":"data"}' assert os.stat(test_file).st_mode & 0o777 == 0o600 func(test_file, b'{"some":"data"}', True, mode="wb") - with open(test_file) as fh: + with open(test_file, encoding="utf8") as fh: assert fh.read() == '{"some":"data"}' assert os.stat(test_file).st_mode & 0o777 == 0o600 diff --git a/tests/util/test_json.py b/tests/util/test_json.py index c973ed1a91c..3a314bb5a1b 100644 --- a/tests/util/test_json.py +++ b/tests/util/test_json.py @@ -25,7 +25,7 @@ TEST_BAD_SERIALIED = "THIS IS NOT JSON\n" def test_load_bad_data(tmp_path: Path) -> None: """Test error from trying to load unserializable data.""" fname = tmp_path / "test5.json" - with open(fname, "w") as fh: + with open(fname, "w", encoding="utf8") as fh: fh.write(TEST_BAD_SERIALIED) with pytest.raises(HomeAssistantError, match=re.escape(str(fname))) as err: load_json(fname) From 9e8f9abbf76261b0e9fe7ad9bc71edac884ee2b1 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 11 Jun 2024 17:59:54 +0200 Subject: [PATCH 0543/1445] Ignore misplaced-bare-raise warnings in tests (#119403) --- tests/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index 4e720bc0bd2..1d0ad3d47b3 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -898,7 +898,7 @@ def fail_on_log_exception( return def log_exception(format_err, *args): - raise + raise # pylint: disable=misplaced-bare-raise monkeypatch.setattr("homeassistant.util.logging.log_exception", log_exception) From a0abd537c67dc283d26a272e8063ccb6c6234285 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 11 Jun 2024 18:06:30 +0200 Subject: [PATCH 0544/1445] Adjust nacl import in tests (#119392) --- tests/components/mobile_app/test_http_api.py | 17 ++------- tests/components/mobile_app/test_webhook.py | 35 ++----------------- .../owntracks/test_device_tracker.py | 34 ++++++------------ 3 files changed, 15 insertions(+), 71 deletions(-) diff --git a/tests/components/mobile_app/test_http_api.py b/tests/components/mobile_app/test_http_api.py index d080b7a5106..b333f91d985 100644 --- a/tests/components/mobile_app/test_http_api.py +++ b/tests/components/mobile_app/test_http_api.py @@ -5,7 +5,8 @@ from http import HTTPStatus import json from unittest.mock import patch -import pytest +from nacl.encoding import Base64Encoder +from nacl.secret import SecretBox from homeassistant.components.mobile_app.const import CONF_SECRET, DOMAIN from homeassistant.const import CONF_WEBHOOK_ID @@ -66,13 +67,6 @@ async def test_registration_encryption( hass: HomeAssistant, hass_client: ClientSessionGenerator ) -> None: """Test that registrations happen.""" - try: - from nacl.encoding import Base64Encoder - from nacl.secret import SecretBox - except (ImportError, OSError): - pytest.skip("libnacl/libsodium is not installed") - return - await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) api_client = await hass_client() @@ -111,13 +105,6 @@ async def test_registration_encryption_legacy( hass: HomeAssistant, hass_client: ClientSessionGenerator ) -> None: """Test that registrations happen.""" - try: - from nacl.encoding import Base64Encoder - from nacl.secret import SecretBox - except (ImportError, OSError): - pytest.skip("libnacl/libsodium is not installed") - return - await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) api_client = await hass_client() diff --git a/tests/components/mobile_app/test_webhook.py b/tests/components/mobile_app/test_webhook.py index 9f521cafd38..ca5c9936409 100644 --- a/tests/components/mobile_app/test_webhook.py +++ b/tests/components/mobile_app/test_webhook.py @@ -2,8 +2,11 @@ from binascii import unhexlify from http import HTTPStatus +import json from unittest.mock import ANY, patch +from nacl.encoding import Base64Encoder +from nacl.secret import SecretBox import pytest from homeassistant.components.camera import CameraEntityFeature @@ -35,14 +38,6 @@ async def homeassistant(hass): def encrypt_payload(secret_key, payload, encode_json=True): """Return a encrypted payload given a key and dictionary of data.""" - try: - from nacl.encoding import Base64Encoder - from nacl.secret import SecretBox - except (ImportError, OSError): - pytest.skip("libnacl/libsodium is not installed") - - import json - prepped_key = unhexlify(secret_key) if encode_json: @@ -56,14 +51,6 @@ def encrypt_payload(secret_key, payload, encode_json=True): def encrypt_payload_legacy(secret_key, payload, encode_json=True): """Return a encrypted payload given a key and dictionary of data.""" - try: - from nacl.encoding import Base64Encoder - from nacl.secret import SecretBox - except (ImportError, OSError): - pytest.skip("libnacl/libsodium is not installed") - - import json - keylen = SecretBox.KEY_SIZE prepped_key = secret_key.encode("utf-8") prepped_key = prepped_key[:keylen] @@ -80,14 +67,6 @@ def encrypt_payload_legacy(secret_key, payload, encode_json=True): def decrypt_payload(secret_key, encrypted_data): """Return a decrypted payload given a key and a string of encrypted data.""" - try: - from nacl.encoding import Base64Encoder - from nacl.secret import SecretBox - except (ImportError, OSError): - pytest.skip("libnacl/libsodium is not installed") - - import json - prepped_key = unhexlify(secret_key) decrypted_data = SecretBox(prepped_key).decrypt( @@ -100,14 +79,6 @@ def decrypt_payload(secret_key, encrypted_data): def decrypt_payload_legacy(secret_key, encrypted_data): """Return a decrypted payload given a key and a string of encrypted data.""" - try: - from nacl.encoding import Base64Encoder - from nacl.secret import SecretBox - except (ImportError, OSError): - pytest.skip("libnacl/libsodium is not installed") - - import json - keylen = SecretBox.KEY_SIZE prepped_key = secret_key.encode("utf-8") prepped_key = prepped_key[:keylen] diff --git a/tests/components/owntracks/test_device_tracker.py b/tests/components/owntracks/test_device_tracker.py index 80e76a5e7b4..16ce8223845 100644 --- a/tests/components/owntracks/test_device_tracker.py +++ b/tests/components/owntracks/test_device_tracker.py @@ -1,8 +1,12 @@ """The tests for the Owntracks device tracker.""" +import base64 import json +import pickle from unittest.mock import patch +from nacl.encoding import Base64Encoder +from nacl.secret import SecretBox import pytest from homeassistant.components import owntracks @@ -1330,23 +1334,14 @@ def generate_ciphers(secret): # PyNaCl ciphertext generation will fail if the module # cannot be imported. However, the test for decryption # also relies on this library and won't be run without it. - import base64 - import pickle + keylen = SecretBox.KEY_SIZE + key = secret.encode("utf-8") + key = key[:keylen] + key = key.ljust(keylen, b"\0") - try: - from nacl.encoding import Base64Encoder - from nacl.secret import SecretBox + msg = json.dumps(DEFAULT_LOCATION_MESSAGE).encode("utf-8") - keylen = SecretBox.KEY_SIZE - key = secret.encode("utf-8") - key = key[:keylen] - key = key.ljust(keylen, b"\0") - - msg = json.dumps(DEFAULT_LOCATION_MESSAGE).encode("utf-8") - - ctxt = SecretBox(key).encrypt(msg, encoder=Base64Encoder).decode("utf-8") - except (ImportError, OSError): - ctxt = "" + ctxt = SecretBox(key).encrypt(msg, encoder=Base64Encoder).decode("utf-8") mctxt = base64.b64encode( pickle.dumps( @@ -1381,9 +1376,6 @@ def mock_cipher(): def mock_decrypt(ciphertext, key): """Decrypt/unpickle.""" - import base64 - import pickle - (mkey, plaintext) = pickle.loads(base64.b64decode(ciphertext)) if key != mkey: raise ValueError @@ -1504,12 +1496,6 @@ async def test_encrypted_payload_no_topic_key(hass: HomeAssistant, setup_comp) - async def test_encrypted_payload_libsodium(hass: HomeAssistant, setup_comp) -> None: """Test sending encrypted message payload.""" - try: - import nacl # noqa: F401 - except (ImportError, OSError): - pytest.skip("PyNaCl/libsodium is not installed") - return - await setup_owntracks(hass, {CONF_SECRET: TEST_SECRET_KEY}) await send_message(hass, LOCATION_TOPIC, ENCRYPTED_LOCATION_MESSAGE) From c907912dd1f112c792db1491c08ef624bd24eb38 Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Tue, 11 Jun 2024 17:08:58 +0100 Subject: [PATCH 0545/1445] Restructure and setup dedicated coordinator for Azure DevOps (#119199) --- .../components/azure_devops/__init__.py | 96 +++------------ .../components/azure_devops/coordinator.py | 116 ++++++++++++++++++ homeassistant/components/azure_devops/data.py | 15 +++ .../components/azure_devops/entity.py | 28 +++++ .../components/azure_devops/sensor.py | 22 ++-- tests/components/azure_devops/conftest.py | 9 +- tests/components/azure_devops/test_init.py | 17 ++- 7 files changed, 208 insertions(+), 95 deletions(-) create mode 100644 homeassistant/components/azure_devops/coordinator.py create mode 100644 homeassistant/components/azure_devops/data.py create mode 100644 homeassistant/components/azure_devops/entity.py diff --git a/homeassistant/components/azure_devops/__init__.py b/homeassistant/components/azure_devops/__init__.py index 27f7f790637..a6e531879b7 100644 --- a/homeassistant/components/azure_devops/__init__.py +++ b/homeassistant/components/azure_devops/__init__.py @@ -2,83 +2,45 @@ from __future__ import annotations -from datetime import timedelta import logging -from typing import Final - -from aioazuredevops.builds import DevOpsBuild -from aioazuredevops.client import DevOpsClient -import aiohttp from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryAuthFailed -from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo -from homeassistant.helpers.update_coordinator import ( - CoordinatorEntity, - DataUpdateCoordinator, - UpdateFailed, -) -from .const import CONF_ORG, CONF_PAT, CONF_PROJECT, DOMAIN +from .const import CONF_PAT, CONF_PROJECT, DOMAIN +from .coordinator import AzureDevOpsDataUpdateCoordinator _LOGGER = logging.getLogger(__name__) PLATFORMS = [Platform.SENSOR] -BUILDS_QUERY: Final = "?queryOrder=queueTimeDescending&maxBuildsPerDefinition=1" - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Azure DevOps from a config entry.""" - aiohttp_session = async_get_clientsession(hass) - client = DevOpsClient(session=aiohttp_session) - if entry.data.get(CONF_PAT) is not None: - await client.authorize(entry.data[CONF_PAT], entry.data[CONF_ORG]) - if not client.authorized: - raise ConfigEntryAuthFailed( - "Could not authorize with Azure DevOps. You will need to update your" - " token" - ) - - project = await client.get_project( - entry.data[CONF_ORG], - entry.data[CONF_PROJECT], - ) - - async def async_update_data() -> list[DevOpsBuild]: - """Fetch data from Azure DevOps.""" - - try: - builds = await client.get_builds( - entry.data[CONF_ORG], - entry.data[CONF_PROJECT], - BUILDS_QUERY, - ) - except aiohttp.ClientError as exception: - raise UpdateFailed from exception - - if builds is None: - raise UpdateFailed("No builds found") - - return builds - - coordinator = DataUpdateCoordinator( + # Create the data update coordinator + coordinator = AzureDevOpsDataUpdateCoordinator( hass, _LOGGER, - name=f"{DOMAIN}_coordinator", - update_method=async_update_data, - update_interval=timedelta(seconds=300), + entry=entry, ) + # Store the coordinator in hass data + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][entry.entry_id] = coordinator + + # If a personal access token is set, authorize the client + if entry.data.get(CONF_PAT) is not None: + await coordinator.authorize(entry.data[CONF_PAT]) + + # Set the project for the coordinator + coordinator.project = await coordinator.get_project(entry.data[CONF_PROJECT]) + + # Fetch initial data so we have data when entities subscribe await coordinator.async_config_entry_first_refresh() - hass.data.setdefault(DOMAIN, {}) - hass.data[DOMAIN][entry.entry_id] = coordinator, project - + # Set up platforms await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True @@ -89,25 +51,5 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: del hass.data[DOMAIN][entry.entry_id] + return unload_ok - - -class AzureDevOpsEntity(CoordinatorEntity[DataUpdateCoordinator[list[DevOpsBuild]]]): - """Defines a base Azure DevOps entity.""" - - _attr_has_entity_name = True - - def __init__( - self, - coordinator: DataUpdateCoordinator[list[DevOpsBuild]], - organization: str, - project_name: str, - ) -> None: - """Initialize the Azure DevOps entity.""" - super().__init__(coordinator) - self._attr_device_info = DeviceInfo( - entry_type=DeviceEntryType.SERVICE, - identifiers={(DOMAIN, organization, project_name)}, # type: ignore[arg-type] - manufacturer=organization, - name=project_name, - ) diff --git a/homeassistant/components/azure_devops/coordinator.py b/homeassistant/components/azure_devops/coordinator.py new file mode 100644 index 00000000000..ba0528de282 --- /dev/null +++ b/homeassistant/components/azure_devops/coordinator.py @@ -0,0 +1,116 @@ +"""Define the Azure DevOps DataUpdateCoordinator.""" + +from collections.abc import Callable +from datetime import timedelta +import logging +from typing import Final + +from aioazuredevops.builds import DevOpsBuild +from aioazuredevops.client import DevOpsClient +from aioazuredevops.core import DevOpsProject +import aiohttp + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import CONF_ORG, DOMAIN +from .data import AzureDevOpsData + +BUILDS_QUERY: Final = "?queryOrder=queueTimeDescending&maxBuildsPerDefinition=1" + + +def ado_exception_none_handler(func: Callable) -> Callable: + """Handle exceptions or None to always return a value or raise.""" + + async def handler(*args, **kwargs): + try: + response = await func(*args, **kwargs) + except aiohttp.ClientError as exception: + raise UpdateFailed from exception + + if response is None: + raise UpdateFailed("No data returned from Azure DevOps") + + return response + + return handler + + +class AzureDevOpsDataUpdateCoordinator(DataUpdateCoordinator[AzureDevOpsData]): + """Class to manage and fetch Azure DevOps data.""" + + client: DevOpsClient + organization: str + project: DevOpsProject + + def __init__( + self, + hass: HomeAssistant, + logger: logging.Logger, + *, + entry: ConfigEntry, + ) -> None: + """Initialize global Azure DevOps data updater.""" + self.title = entry.title + + super().__init__( + hass=hass, + logger=logger, + name=DOMAIN, + update_interval=timedelta(seconds=300), + ) + + self.client = DevOpsClient(session=async_get_clientsession(hass)) + self.organization = entry.data[CONF_ORG] + + @ado_exception_none_handler + async def authorize( + self, + personal_access_token: str, + ) -> bool: + """Authorize with Azure DevOps.""" + await self.client.authorize( + personal_access_token, + self.organization, + ) + if not self.client.authorized: + raise ConfigEntryAuthFailed( + "Could not authorize with Azure DevOps. You will need to update your" + " token" + ) + + return True + + @ado_exception_none_handler + async def get_project( + self, + project: str, + ) -> DevOpsProject | None: + """Get the project.""" + return await self.client.get_project( + self.organization, + project, + ) + + @ado_exception_none_handler + async def _get_builds(self, project_name: str) -> list[DevOpsBuild] | None: + """Get the builds.""" + return await self.client.get_builds( + self.organization, + project_name, + BUILDS_QUERY, + ) + + async def _async_update_data(self) -> AzureDevOpsData: + """Fetch data from Azure DevOps.""" + # Get the builds from the project + builds = await self._get_builds(self.project.name) + + return AzureDevOpsData( + organization=self.organization, + project=self.project, + builds=builds, + ) diff --git a/homeassistant/components/azure_devops/data.py b/homeassistant/components/azure_devops/data.py new file mode 100644 index 00000000000..6cbd6eb3bc1 --- /dev/null +++ b/homeassistant/components/azure_devops/data.py @@ -0,0 +1,15 @@ +"""Data classes for Azure DevOps integration.""" + +from dataclasses import dataclass + +from aioazuredevops.builds import DevOpsBuild +from aioazuredevops.core import DevOpsProject + + +@dataclass(frozen=True, kw_only=True) +class AzureDevOpsData: + """Class describing Azure DevOps data.""" + + organization: str + project: DevOpsProject + builds: list[DevOpsBuild] diff --git a/homeassistant/components/azure_devops/entity.py b/homeassistant/components/azure_devops/entity.py new file mode 100644 index 00000000000..0a4a94d4b32 --- /dev/null +++ b/homeassistant/components/azure_devops/entity.py @@ -0,0 +1,28 @@ +"""Base entity for Azure DevOps.""" + +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN +from .coordinator import AzureDevOpsDataUpdateCoordinator + + +class AzureDevOpsEntity(CoordinatorEntity[AzureDevOpsDataUpdateCoordinator]): + """Defines a base Azure DevOps entity.""" + + _attr_has_entity_name = True + + def __init__( + self, + coordinator: AzureDevOpsDataUpdateCoordinator, + ) -> None: + """Initialize the Azure DevOps entity.""" + super().__init__(coordinator) + self._attr_device_info = DeviceInfo( + entry_type=DeviceEntryType.SERVICE, + identifiers={ + (DOMAIN, coordinator.data.organization, coordinator.data.project.name) # type: ignore[arg-type] + }, + manufacturer=coordinator.data.organization, + name=coordinator.data.project.name, + ) diff --git a/homeassistant/components/azure_devops/sensor.py b/homeassistant/components/azure_devops/sensor.py index b1d975f0a70..7b2a1a15adf 100644 --- a/homeassistant/components/azure_devops/sensor.py +++ b/homeassistant/components/azure_devops/sensor.py @@ -19,11 +19,11 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from homeassistant.util import dt as dt_util -from . import AzureDevOpsEntity -from .const import CONF_ORG, DOMAIN +from .const import DOMAIN +from .coordinator import AzureDevOpsDataUpdateCoordinator +from .entity import AzureDevOpsEntity _LOGGER = logging.getLogger(__name__) @@ -132,15 +132,13 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up Azure DevOps sensor based on a config entry.""" - coordinator, project = hass.data[DOMAIN][entry.entry_id] - initial_builds: list[DevOpsBuild] = coordinator.data + coordinator = hass.data[DOMAIN][entry.entry_id] + initial_builds: list[DevOpsBuild] = coordinator.data.builds async_add_entities( AzureDevOpsBuildSensor( coordinator, description, - entry.data[CONF_ORG], - project.name, key, ) for description in BASE_BUILD_SENSOR_DESCRIPTIONS @@ -156,17 +154,15 @@ class AzureDevOpsBuildSensor(AzureDevOpsEntity, SensorEntity): def __init__( self, - coordinator: DataUpdateCoordinator[list[DevOpsBuild]], + coordinator: AzureDevOpsDataUpdateCoordinator, description: AzureDevOpsBuildSensorEntityDescription, - organization: str, - project_name: str, item_key: int, ) -> None: """Initialize.""" - super().__init__(coordinator, organization, project_name) + super().__init__(coordinator) self.entity_description = description self.item_key = item_key - self._attr_unique_id = f"{organization}_{self.build.project.project_id}_{self.build.definition.build_id}_{description.key}" + self._attr_unique_id = f"{self.coordinator.data.organization}_{self.build.project.project_id}_{self.build.definition.build_id}_{description.key}" self._attr_translation_placeholders = { "definition_name": self.build.definition.name } @@ -174,7 +170,7 @@ class AzureDevOpsBuildSensor(AzureDevOpsEntity, SensorEntity): @property def build(self) -> DevOpsBuild: """Return the build.""" - return self.coordinator.data[self.item_key] + return self.coordinator.data.builds[self.item_key] @property def native_value(self) -> datetime | StateType: diff --git a/tests/components/azure_devops/conftest.py b/tests/components/azure_devops/conftest.py index 29569da2c90..97e113bbb39 100644 --- a/tests/components/azure_devops/conftest.py +++ b/tests/components/azure_devops/conftest.py @@ -1,9 +1,9 @@ """Test fixtures for Azure DevOps.""" +from collections.abc import AsyncGenerator, Generator from unittest.mock import AsyncMock, MagicMock, patch import pytest -from typing_extensions import AsyncGenerator, Generator from homeassistant.components.azure_devops.const import DOMAIN @@ -18,7 +18,8 @@ async def mock_devops_client() -> AsyncGenerator[MagicMock]: with ( patch( - "homeassistant.components.azure_devops.DevOpsClient", autospec=True + "homeassistant.components.azure_devops.coordinator.DevOpsClient", + autospec=True, ) as mock_client, patch( "homeassistant.components.azure_devops.config_flow.DevOpsClient", @@ -54,5 +55,5 @@ def mock_setup_entry() -> Generator[AsyncMock]: with patch( "homeassistant.components.azure_devops.async_setup_entry", return_value=True, - ) as mock_setup_entry: - yield mock_setup_entry + ) as mock_entry: + yield mock_entry diff --git a/tests/components/azure_devops/test_init.py b/tests/components/azure_devops/test_init.py index 240edee82d7..a7655042f25 100644 --- a/tests/components/azure_devops/test_init.py +++ b/tests/components/azure_devops/test_init.py @@ -48,7 +48,22 @@ async def test_auth_failed( assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR -async def test_update_failed( +async def test_update_failed_project( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_devops_client: MagicMock, +) -> None: + """Test a failed update entry.""" + mock_devops_client.get_project.side_effect = aiohttp.ClientError + + await setup_integration(hass, mock_config_entry) + + assert mock_devops_client.get_project.call_count == 1 + + assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR + + +async def test_update_failed_builds( hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_devops_client: MagicMock, From e6df0be072b78961e6929a69154386ef79c962d4 Mon Sep 17 00:00:00 2001 From: Douglas Krahmer Date: Tue, 11 Jun 2024 09:09:57 -0700 Subject: [PATCH 0546/1445] Add support for Tuya non-standard contact sensors (#115557) Co-authored-by: Franck Nijhof --- homeassistant/components/tuya/binary_sensor.py | 4 ++++ homeassistant/components/tuya/const.py | 1 + homeassistant/components/tuya/sensor.py | 8 ++++++++ 3 files changed, 13 insertions(+) diff --git a/homeassistant/components/tuya/binary_sensor.py b/homeassistant/components/tuya/binary_sensor.py index b992c24d07d..2d6d9b478c8 100644 --- a/homeassistant/components/tuya/binary_sensor.py +++ b/homeassistant/components/tuya/binary_sensor.py @@ -190,6 +190,10 @@ BINARY_SENSORS: dict[str, tuple[TuyaBinarySensorEntityDescription, ...]] = { key=DPCode.DOORCONTACT_STATE, device_class=BinarySensorDeviceClass.DOOR, ), + TuyaBinarySensorEntityDescription( + key=DPCode.SWITCH, # Used by non-standard contact sensor implementations + device_class=BinarySensorDeviceClass.DOOR, + ), TAMPER_BINARY_SENSOR, ), # Access Control diff --git a/homeassistant/components/tuya/const.py b/homeassistant/components/tuya/const.py index a9c53d807bc..d731a93f858 100644 --- a/homeassistant/components/tuya/const.py +++ b/homeassistant/components/tuya/const.py @@ -113,6 +113,7 @@ class DPCode(StrEnum): BASIC_OSD = "basic_osd" BASIC_PRIVATE = "basic_private" BASIC_WDR = "basic_wdr" + BATTERY = "battery" # Used by non-standard contact sensor implementations BATTERY_PERCENTAGE = "battery_percentage" # Battery percentage BATTERY_STATE = "battery_state" # Battery state BATTERY_VALUE = "battery_value" # Battery value diff --git a/homeassistant/components/tuya/sensor.py b/homeassistant/components/tuya/sensor.py index 9382059471d..cd487a31d97 100644 --- a/homeassistant/components/tuya/sensor.py +++ b/homeassistant/components/tuya/sensor.py @@ -55,6 +55,14 @@ BATTERY_SENSORS: tuple[TuyaSensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, ), + TuyaSensorEntityDescription( + key=DPCode.BATTERY, # Used by non-standard contact sensor implementations + translation_key="battery", + native_unit_of_measurement=PERCENTAGE, + device_class=SensorDeviceClass.BATTERY, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + ), TuyaSensorEntityDescription( key=DPCode.BATTERY_STATE, translation_key="battery_state", From 4f28f3a5fc211e3979c56743dad1ccdd41675ea1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 11 Jun 2024 13:58:05 -0500 Subject: [PATCH 0547/1445] Fix incorrect key name in unifiprotect options strings (#119417) --- homeassistant/components/unifiprotect/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/unifiprotect/strings.json b/homeassistant/components/unifiprotect/strings.json index b83d514f836..bac7eaa5bf3 100644 --- a/homeassistant/components/unifiprotect/strings.json +++ b/homeassistant/components/unifiprotect/strings.json @@ -55,7 +55,7 @@ "all_updates": "Realtime metrics (WARNING: Greatly increases CPU usage)", "override_connection_host": "Override Connection Host", "max_media": "Max number of event to load for Media Browser (increases RAM usage)", - "allow_ea": "Allow Early Access versions of Protect (WARNING: Will mark your integration as unsupported)" + "allow_ea_channel": "Allow Early Access versions of Protect (WARNING: Will mark your integration as unsupported)" } } } From 400b8a836199c96f70217ce425823eed12f6bb83 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 11 Jun 2024 13:59:28 -0500 Subject: [PATCH 0548/1445] Bump uiprotect to 1.0.0 (#119415) --- homeassistant/components/unifiprotect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index 8bbd3738222..b88eed6f39a 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -40,7 +40,7 @@ "integration_type": "hub", "iot_class": "local_push", "loggers": ["uiprotect", "unifi_discovery"], - "requirements": ["uiprotect==0.13.0", "unifi-discovery==1.1.8"], + "requirements": ["uiprotect==1.0.0", "unifi-discovery==1.1.8"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/requirements_all.txt b/requirements_all.txt index be95c4f5120..56dd4e5f947 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2785,7 +2785,7 @@ twitchAPI==4.0.0 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==0.13.0 +uiprotect==1.0.0 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8e2a2fa48aa..99ef1d54fa7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2165,7 +2165,7 @@ twitchAPI==4.0.0 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==0.13.0 +uiprotect==1.0.0 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 From bce8f2a25a530cbf0dc90c51664be143a177da7a Mon Sep 17 00:00:00 2001 From: Josef Zweck <24647999+zweckj@users.noreply.github.com> Date: Tue, 11 Jun 2024 22:27:47 +0200 Subject: [PATCH 0549/1445] Migrate lamarzocco to entry.runtime_data (#119425) migrate lamarzocco to entry.runtime_data --- homeassistant/components/lamarzocco/__init__.py | 13 +++++-------- .../components/lamarzocco/binary_sensor.py | 7 +++---- homeassistant/components/lamarzocco/button.py | 7 +++---- homeassistant/components/lamarzocco/calendar.py | 7 +++---- homeassistant/components/lamarzocco/diagnostics.py | 9 ++++----- homeassistant/components/lamarzocco/number.py | 7 +++---- homeassistant/components/lamarzocco/select.py | 7 +++---- homeassistant/components/lamarzocco/sensor.py | 7 +++---- homeassistant/components/lamarzocco/switch.py | 7 +++---- homeassistant/components/lamarzocco/update.py | 7 +++---- 10 files changed, 33 insertions(+), 45 deletions(-) diff --git a/homeassistant/components/lamarzocco/__init__.py b/homeassistant/components/lamarzocco/__init__.py index e6bb3b1d3ae..9c66fdd1b60 100644 --- a/homeassistant/components/lamarzocco/__init__.py +++ b/homeassistant/components/lamarzocco/__init__.py @@ -41,8 +41,10 @@ PLATFORMS = [ _LOGGER = logging.getLogger(__name__) +type LaMarzoccoConfigEntry = ConfigEntry[LaMarzoccoUpdateCoordinator] -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + +async def async_setup_entry(hass: HomeAssistant, entry: LaMarzoccoConfigEntry) -> bool: """Set up La Marzocco as config entry.""" assert entry.unique_id @@ -107,7 +109,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_setup() await coordinator.async_config_entry_first_refresh() - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator + entry.runtime_data = coordinator gateway_version = coordinator.device.firmware[FirmwareType.GATEWAY].current_version if version.parse(gateway_version) < version.parse("v3.5-rc5"): @@ -134,12 +136,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - - if unload_ok: - hass.data[DOMAIN].pop(entry.entry_id) - - return unload_ok + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/lamarzocco/binary_sensor.py b/homeassistant/components/lamarzocco/binary_sensor.py index 86b18888fc5..2ad72ea4087 100644 --- a/homeassistant/components/lamarzocco/binary_sensor.py +++ b/homeassistant/components/lamarzocco/binary_sensor.py @@ -10,12 +10,11 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntity, BinarySensorEntityDescription, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN +from . import LaMarzoccoConfigEntry from .entity import LaMarzoccoEntity, LaMarzoccoEntityDescription @@ -51,11 +50,11 @@ ENTITIES: tuple[LaMarzoccoBinarySensorEntityDescription, ...] = ( async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + entry: LaMarzoccoConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up binary sensor entities.""" - coordinator = hass.data[DOMAIN][config_entry.entry_id] + coordinator = entry.runtime_data async_add_entities( LaMarzoccoBinarySensorEntity(coordinator, description) diff --git a/homeassistant/components/lamarzocco/button.py b/homeassistant/components/lamarzocco/button.py index ec0477647d8..c261630836e 100644 --- a/homeassistant/components/lamarzocco/button.py +++ b/homeassistant/components/lamarzocco/button.py @@ -7,11 +7,10 @@ from typing import Any from lmcloud.lm_machine import LaMarzoccoMachine from homeassistant.components.button import ButtonEntity, ButtonEntityDescription -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN +from . import LaMarzoccoConfigEntry from .entity import LaMarzoccoEntity, LaMarzoccoEntityDescription @@ -36,12 +35,12 @@ ENTITIES: tuple[LaMarzoccoButtonEntityDescription, ...] = ( async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + entry: LaMarzoccoConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up button entities.""" - coordinator = hass.data[DOMAIN][config_entry.entry_id] + coordinator = entry.runtime_data async_add_entities( LaMarzoccoButtonEntity(coordinator, description) for description in ENTITIES diff --git a/homeassistant/components/lamarzocco/calendar.py b/homeassistant/components/lamarzocco/calendar.py index b3a8774a1cf..8b3240ff7a1 100644 --- a/homeassistant/components/lamarzocco/calendar.py +++ b/homeassistant/components/lamarzocco/calendar.py @@ -6,12 +6,11 @@ from datetime import datetime, timedelta from lmcloud.models import LaMarzoccoWakeUpSleepEntry from homeassistant.components.calendar import CalendarEntity, CalendarEvent -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import dt as dt_util -from .const import DOMAIN +from . import LaMarzoccoConfigEntry from .coordinator import LaMarzoccoUpdateCoordinator from .entity import LaMarzoccoBaseEntity @@ -30,12 +29,12 @@ DAY_OF_WEEK = [ async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + entry: LaMarzoccoConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up switch entities and services.""" - coordinator = hass.data[DOMAIN][config_entry.entry_id] + coordinator = entry.runtime_data async_add_entities( LaMarzoccoCalendarEntity(coordinator, CALENDAR_KEY, wake_up_sleep_entry) for wake_up_sleep_entry in coordinator.device.config.wake_up_sleep_entries.values() diff --git a/homeassistant/components/lamarzocco/diagnostics.py b/homeassistant/components/lamarzocco/diagnostics.py index 04aed25defe..4293fdca615 100644 --- a/homeassistant/components/lamarzocco/diagnostics.py +++ b/homeassistant/components/lamarzocco/diagnostics.py @@ -8,11 +8,9 @@ from typing import Any, TypedDict from lmcloud.const import FirmwareType from homeassistant.components.diagnostics import async_redact_data -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from .const import DOMAIN -from .coordinator import LaMarzoccoUpdateCoordinator +from . import LaMarzoccoConfigEntry TO_REDACT = { "serial_number", @@ -29,10 +27,11 @@ class DiagnosticsData(TypedDict): async def async_get_config_entry_diagnostics( - hass: HomeAssistant, config_entry: ConfigEntry + hass: HomeAssistant, + entry: LaMarzoccoConfigEntry, ) -> dict[str, Any]: """Return diagnostics for a config entry.""" - coordinator: LaMarzoccoUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + coordinator = entry.runtime_data device = coordinator.device # collect all data sources diagnostics_data = DiagnosticsData( diff --git a/homeassistant/components/lamarzocco/number.py b/homeassistant/components/lamarzocco/number.py index 89bb5e75dd2..69e5b42c116 100644 --- a/homeassistant/components/lamarzocco/number.py +++ b/homeassistant/components/lamarzocco/number.py @@ -19,7 +19,6 @@ from homeassistant.components.number import ( NumberEntity, NumberEntityDescription, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( PRECISION_TENTHS, PRECISION_WHOLE, @@ -30,7 +29,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN +from . import LaMarzoccoConfigEntry from .coordinator import LaMarzoccoUpdateCoordinator from .entity import LaMarzoccoEntity, LaMarzoccoEntityDescription @@ -187,11 +186,11 @@ KEY_ENTITIES: tuple[LaMarzoccoKeyNumberEntityDescription, ...] = ( async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + entry: LaMarzoccoConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up number entities.""" - coordinator = hass.data[DOMAIN][config_entry.entry_id] + coordinator = entry.runtime_data entities: list[NumberEntity] = [ LaMarzoccoNumberEntity(coordinator, description) for description in ENTITIES diff --git a/homeassistant/components/lamarzocco/select.py b/homeassistant/components/lamarzocco/select.py index 4e202db7c7c..5bff815fb95 100644 --- a/homeassistant/components/lamarzocco/select.py +++ b/homeassistant/components/lamarzocco/select.py @@ -9,12 +9,11 @@ from lmcloud.lm_machine import LaMarzoccoMachine from lmcloud.models import LaMarzoccoMachineConfig from homeassistant.components.select import SelectEntity, SelectEntityDescription -from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN +from . import LaMarzoccoConfigEntry from .entity import LaMarzoccoEntity, LaMarzoccoEntityDescription STEAM_LEVEL_HA_TO_LM = { @@ -86,11 +85,11 @@ ENTITIES: tuple[LaMarzoccoSelectEntityDescription, ...] = ( async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + entry: LaMarzoccoConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up select entities.""" - coordinator = hass.data[DOMAIN][config_entry.entry_id] + coordinator = entry.runtime_data async_add_entities( LaMarzoccoSelectEntity(coordinator, description) diff --git a/homeassistant/components/lamarzocco/sensor.py b/homeassistant/components/lamarzocco/sensor.py index 723661451c5..c43ea0f99bc 100644 --- a/homeassistant/components/lamarzocco/sensor.py +++ b/homeassistant/components/lamarzocco/sensor.py @@ -12,12 +12,11 @@ from homeassistant.components.sensor import ( SensorEntityDescription, SensorStateClass, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory, UnitOfTemperature, UnitOfTime from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN +from . import LaMarzoccoConfigEntry from .entity import LaMarzoccoEntity, LaMarzoccoEntityDescription @@ -87,11 +86,11 @@ ENTITIES: tuple[LaMarzoccoSensorEntityDescription, ...] = ( async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + entry: LaMarzoccoConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up sensor entities.""" - coordinator = hass.data[DOMAIN][config_entry.entry_id] + coordinator = entry.runtime_data async_add_entities( LaMarzoccoSensorEntity(coordinator, description) diff --git a/homeassistant/components/lamarzocco/switch.py b/homeassistant/components/lamarzocco/switch.py index 0c5939e6d59..1661917fcbc 100644 --- a/homeassistant/components/lamarzocco/switch.py +++ b/homeassistant/components/lamarzocco/switch.py @@ -9,11 +9,10 @@ from lmcloud.lm_machine import LaMarzoccoMachine from lmcloud.models import LaMarzoccoMachineConfig from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN +from . import LaMarzoccoConfigEntry from .entity import LaMarzoccoEntity, LaMarzoccoEntityDescription @@ -47,12 +46,12 @@ ENTITIES: tuple[LaMarzoccoSwitchEntityDescription, ...] = ( async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + entry: LaMarzoccoConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up switch entities and services.""" - coordinator = hass.data[DOMAIN][config_entry.entry_id] + coordinator = entry.runtime_data async_add_entities( LaMarzoccoSwitchEntity(coordinator, description) for description in ENTITIES diff --git a/homeassistant/components/lamarzocco/update.py b/homeassistant/components/lamarzocco/update.py index f8891b30bf8..342a3e09071 100644 --- a/homeassistant/components/lamarzocco/update.py +++ b/homeassistant/components/lamarzocco/update.py @@ -11,13 +11,12 @@ from homeassistant.components.update import ( UpdateEntityDescription, UpdateEntityFeature, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN +from . import LaMarzoccoConfigEntry from .entity import LaMarzoccoEntity, LaMarzoccoEntityDescription @@ -51,12 +50,12 @@ ENTITIES: tuple[LaMarzoccoUpdateEntityDescription, ...] = ( async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + entry: LaMarzoccoConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Create update entities.""" - coordinator = hass.data[DOMAIN][config_entry.entry_id] + coordinator = entry.runtime_data async_add_entities( LaMarzoccoUpdateEntity(coordinator, description) for description in ENTITIES From 34cfa0fd0e3bdd76d5acbda5ebbb9d5b9cf420de Mon Sep 17 00:00:00 2001 From: MJJ Date: Wed, 12 Jun 2024 00:01:11 +0200 Subject: [PATCH 0550/1445] Bump buieradar to 1.0.6 (#119433) --- homeassistant/components/buienradar/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/buienradar/manifest.json b/homeassistant/components/buienradar/manifest.json index 4885f45032c..5b08f5c631a 100644 --- a/homeassistant/components/buienradar/manifest.json +++ b/homeassistant/components/buienradar/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/buienradar", "iot_class": "cloud_polling", "loggers": ["buienradar", "vincenty"], - "requirements": ["buienradar==1.0.5"] + "requirements": ["buienradar==1.0.6"] } diff --git a/requirements_all.txt b/requirements_all.txt index 56dd4e5f947..aec599d0581 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -637,7 +637,7 @@ bthomehub5-devicelist==0.1.1 btsmarthub-devicelist==0.2.3 # homeassistant.components.buienradar -buienradar==1.0.5 +buienradar==1.0.6 # homeassistant.components.dhcp cached_ipaddress==0.3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 99ef1d54fa7..a7309a7fa4f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -539,7 +539,7 @@ brunt==1.2.0 bthome-ble==3.9.1 # homeassistant.components.buienradar -buienradar==1.0.5 +buienradar==1.0.6 # homeassistant.components.dhcp cached_ipaddress==0.3.0 From 35417649cd1206f7e582cbe1095602fc7d969993 Mon Sep 17 00:00:00 2001 From: Sebastian Goscik Date: Wed, 12 Jun 2024 00:20:00 +0100 Subject: [PATCH 0551/1445] Bump uiprotect to v1.0.1 (#119436) --- homeassistant/components/unifiprotect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index b88eed6f39a..5674fcb07a1 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -40,7 +40,7 @@ "integration_type": "hub", "iot_class": "local_push", "loggers": ["uiprotect", "unifi_discovery"], - "requirements": ["uiprotect==1.0.0", "unifi-discovery==1.1.8"], + "requirements": ["uiprotect==1.0.1", "unifi-discovery==1.1.8"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/requirements_all.txt b/requirements_all.txt index aec599d0581..2f0b65db341 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2785,7 +2785,7 @@ twitchAPI==4.0.0 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==1.0.0 +uiprotect==1.0.1 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a7309a7fa4f..ad854b2629c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2165,7 +2165,7 @@ twitchAPI==4.0.0 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==1.0.0 +uiprotect==1.0.1 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 From 47d39938052dfe358848caac6995939688b54f1e Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Wed, 12 Jun 2024 10:08:41 +0200 Subject: [PATCH 0552/1445] Add loggers to gardena bluetooth (#119460) --- homeassistant/components/gardena_bluetooth/manifest.json | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/gardena_bluetooth/manifest.json b/homeassistant/components/gardena_bluetooth/manifest.json index 1e3ef156d72..4812def7dde 100644 --- a/homeassistant/components/gardena_bluetooth/manifest.json +++ b/homeassistant/components/gardena_bluetooth/manifest.json @@ -13,5 +13,6 @@ "dependencies": ["bluetooth_adapters"], "documentation": "https://www.home-assistant.io/integrations/gardena_bluetooth", "iot_class": "local_polling", + "loggers": ["bleak", "bleak_esphome", "gardena_bluetooth"], "requirements": ["gardena-bluetooth==1.4.2"] } From 0c79eeabdf05c7970e20b74ce8a92f33bf9af8bc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 12 Jun 2024 03:10:40 -0500 Subject: [PATCH 0553/1445] Bump uiprotect to 1.1.0 (#119449) --- homeassistant/components/unifiprotect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index 5674fcb07a1..5c1d252ce48 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -40,7 +40,7 @@ "integration_type": "hub", "iot_class": "local_push", "loggers": ["uiprotect", "unifi_discovery"], - "requirements": ["uiprotect==1.0.1", "unifi-discovery==1.1.8"], + "requirements": ["uiprotect==1.1.0", "unifi-discovery==1.1.8"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/requirements_all.txt b/requirements_all.txt index 2f0b65db341..74df113ae97 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2785,7 +2785,7 @@ twitchAPI==4.0.0 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==1.0.1 +uiprotect==1.1.0 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ad854b2629c..a508c8ff21e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2165,7 +2165,7 @@ twitchAPI==4.0.0 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==1.0.1 +uiprotect==1.1.0 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 From 2a7e78a80fedfb74c7e1b7b9aedb8d9729a50011 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 12 Jun 2024 12:21:41 +0200 Subject: [PATCH 0554/1445] Ignore broad-exception-raised pylint warnings in tests (#119468) --- tests/components/bluetooth/test_passive_update_processor.py | 2 ++ tests/components/notify/test_legacy.py | 2 +- tests/components/profiler/test_init.py | 2 +- tests/components/roon/test_config_flow.py | 2 +- tests/components/system_health/test_init.py | 2 +- tests/components/system_log/test_init.py | 2 +- tests/helpers/test_dispatcher.py | 2 ++ tests/test_config_entries.py | 2 +- tests/test_core.py | 4 ++-- tests/test_runner.py | 3 ++- tests/test_setup.py | 5 +++-- tests/util/test_logging.py | 1 + 12 files changed, 18 insertions(+), 11 deletions(-) diff --git a/tests/components/bluetooth/test_passive_update_processor.py b/tests/components/bluetooth/test_passive_update_processor.py index 24cf344a31c..8e1163c0bdb 100644 --- a/tests/components/bluetooth/test_passive_update_processor.py +++ b/tests/components/bluetooth/test_passive_update_processor.py @@ -583,6 +583,7 @@ async def test_exception_from_update_method( nonlocal run_count run_count += 1 if run_count == 2: + # pylint: disable-next=broad-exception-raised raise Exception("Test exception") return GENERIC_PASSIVE_BLUETOOTH_DATA_UPDATE @@ -1417,6 +1418,7 @@ async def test_exception_from_coordinator_update_method( nonlocal run_count run_count += 1 if run_count == 2: + # pylint: disable-next=broad-exception-raised raise Exception("Test exception") return {"test": "data"} diff --git a/tests/components/notify/test_legacy.py b/tests/components/notify/test_legacy.py index cc2192461ae..d6478c358bf 100644 --- a/tests/components/notify/test_legacy.py +++ b/tests/components/notify/test_legacy.py @@ -261,7 +261,7 @@ async def test_platform_setup_with_error( async def async_get_service(hass, config, discovery_info=None): """Return None for an invalid notify service.""" - raise Exception("Setup error") + raise Exception("Setup error") # pylint: disable=broad-exception-raised mock_notify_platform( hass, tmp_path, "testnotify", async_get_service=async_get_service diff --git a/tests/components/profiler/test_init.py b/tests/components/profiler/test_init.py index ba605049e72..2eca84b43fe 100644 --- a/tests/components/profiler/test_init.py +++ b/tests/components/profiler/test_init.py @@ -181,7 +181,7 @@ async def test_dump_log_object( def __repr__(self): if self.fail: - raise Exception("failed") + raise Exception("failed") # pylint: disable=broad-exception-raised return "" obj1 = DumpLogDummy(False) diff --git a/tests/components/roon/test_config_flow.py b/tests/components/roon/test_config_flow.py index 6f83331d1c7..9822c88fa48 100644 --- a/tests/components/roon/test_config_flow.py +++ b/tests/components/roon/test_config_flow.py @@ -48,7 +48,7 @@ class RoonApiMockException(RoonApiMock): @property def token(self): """Throw exception.""" - raise Exception + raise Exception # pylint: disable=broad-exception-raised class RoonDiscoveryMock: diff --git a/tests/components/system_health/test_init.py b/tests/components/system_health/test_init.py index e677b7d1d34..e51ab8fab99 100644 --- a/tests/components/system_health/test_init.py +++ b/tests/components/system_health/test_init.py @@ -110,7 +110,7 @@ async def test_info_endpoint_register_callback_exc( """Test that the info endpoint requires auth.""" async def mock_info(hass): - raise Exception("TEST ERROR") + raise Exception("TEST ERROR") # pylint: disable=broad-exception-raised async_register_info(hass, "lovelace", mock_info) assert await async_setup_component(hass, "system_health", {}) diff --git a/tests/components/system_log/test_init.py b/tests/components/system_log/test_init.py index e9a50f62cee..0e301720aeb 100644 --- a/tests/components/system_log/test_init.py +++ b/tests/components/system_log/test_init.py @@ -35,7 +35,7 @@ async def get_error_log(hass_ws_client): def _generate_and_log_exception(exception, log): try: - raise Exception(exception) + raise Exception(exception) # pylint: disable=broad-exception-raised except Exception: _LOGGER.exception(log) diff --git a/tests/helpers/test_dispatcher.py b/tests/helpers/test_dispatcher.py index 89d05407fbd..c2c8663f47c 100644 --- a/tests/helpers/test_dispatcher.py +++ b/tests/helpers/test_dispatcher.py @@ -188,6 +188,7 @@ async def test_callback_exception_gets_logged( @callback def bad_handler(*args): """Record calls.""" + # pylint: disable-next=broad-exception-raised raise Exception("This is a bad message callback") # wrap in partial to test message logging. @@ -208,6 +209,7 @@ async def test_coro_exception_gets_logged( async def bad_async_handler(*args): """Record calls.""" + # pylint: disable-next=broad-exception-raised raise Exception("This is a bad message in a coro") # wrap in partial to test message logging. diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 0208b33169c..2d946dd1fce 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -576,7 +576,7 @@ async def test_remove_entry_raises( async def mock_unload_entry(hass, entry): """Mock unload entry function.""" - raise Exception("BROKEN") + raise Exception("BROKEN") # pylint: disable=broad-exception-raised mock_integration(hass, MockModule("comp", async_unload_entry=mock_unload_entry)) diff --git a/tests/test_core.py b/tests/test_core.py index 71e6cb3f3b6..7787a9a3769 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -423,11 +423,11 @@ async def test_async_get_hass_can_be_called(hass: HomeAssistant) -> None: try: if ha.async_get_hass() is hass: return True - raise Exception + raise Exception # pylint: disable=broad-exception-raised except HomeAssistantError: return False - raise Exception + raise Exception # pylint: disable=broad-exception-raised # Test scheduling a coroutine which calls async_get_hass via hass.async_create_task async def _async_create_task() -> None: diff --git a/tests/test_runner.py b/tests/test_runner.py index a4bec12bc0d..90678454adf 100644 --- a/tests/test_runner.py +++ b/tests/test_runner.py @@ -104,7 +104,7 @@ def test_run_does_not_block_forever_with_shielded_task( try: await asyncio.sleep(2) except asyncio.CancelledError: - raise Exception + raise Exception # pylint: disable=broad-exception-raised async def async_shielded(*_): try: @@ -141,6 +141,7 @@ async def test_unhandled_exception_traceback( async def _unhandled_exception(): raised.set() + # pylint: disable-next=broad-exception-raised raise Exception("This is unhandled") try: diff --git a/tests/test_setup.py b/tests/test_setup.py index 27d4b32d32f..f15fe72603e 100644 --- a/tests/test_setup.py +++ b/tests/test_setup.py @@ -328,7 +328,7 @@ async def test_component_exception_setup(hass: HomeAssistant) -> None: def exception_setup(hass, config): """Raise exception.""" - raise Exception("fail!") + raise Exception("fail!") # pylint: disable=broad-exception-raised mock_integration(hass, MockModule("comp", setup=exception_setup)) @@ -342,7 +342,7 @@ async def test_component_base_exception_setup(hass: HomeAssistant) -> None: def exception_setup(hass, config): """Raise exception.""" - raise BaseException("fail!") + raise BaseException("fail!") # pylint: disable=broad-exception-raised mock_integration(hass, MockModule("comp", setup=exception_setup)) @@ -362,6 +362,7 @@ async def test_component_setup_with_validation_and_dependency( """Test that config is passed in.""" if config.get("comp_a", {}).get("valid", False): return True + # pylint: disable-next=broad-exception-raised raise Exception(f"Config not passed in: {config}") platform = MockPlatform() diff --git a/tests/util/test_logging.py b/tests/util/test_logging.py index 8e7106475a2..4667dbcbec8 100644 --- a/tests/util/test_logging.py +++ b/tests/util/test_logging.py @@ -80,6 +80,7 @@ async def test_async_create_catching_coro( """Test exception logging of wrapped coroutine.""" async def job(): + # pylint: disable-next=broad-exception-raised raise Exception("This is a bad coroutine") hass.async_create_task(logging_util.async_create_catching_coro(job())) From 7d631c28a6b740fe264aaed5b713d872963f156a Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 12 Jun 2024 12:22:31 +0200 Subject: [PATCH 0555/1445] Ignore attribute-defined-outside-init pylint warnings in tests (#119470) --- tests/components/bluetooth/test_models.py | 1 + tests/components/harmony/conftest.py | 1 + tests/components/network/test_init.py | 1 + tests/components/sonos/conftest.py | 2 ++ tests/components/yeelight/__init__.py | 1 + tests/helpers/test_entity.py | 1 + tests/helpers/test_entity_platform.py | 2 ++ tests/helpers/test_schema_config_entry_flow.py | 2 ++ tests/test_data_entry_flow.py | 2 ++ 9 files changed, 13 insertions(+) diff --git a/tests/components/bluetooth/test_models.py b/tests/components/bluetooth/test_models.py index 820fa734f73..d36741b4d5d 100644 --- a/tests/components/bluetooth/test_models.py +++ b/tests/components/bluetooth/test_models.py @@ -107,6 +107,7 @@ async def test_wrapped_bleak_client_local_adapter_only(hass: HomeAssistant) -> N "00:00:00:00:00:01", "hci0", ) + # pylint: disable-next=attribute-defined-outside-init scanner.connectable = True cancel = manager.async_register_scanner(scanner) inject_advertisement_with_source( diff --git a/tests/components/harmony/conftest.py b/tests/components/harmony/conftest.py index 97449749667..1e6bbd7a3c3 100644 --- a/tests/components/harmony/conftest.py +++ b/tests/components/harmony/conftest.py @@ -50,6 +50,7 @@ class FakeHarmonyClient: self, ip_address: str = "", callbacks: ClientCallbackType = MagicMock() ): """Initialize FakeHarmonyClient class to capture callbacks.""" + # pylint: disable=attribute-defined-outside-init self._activity_name = "Watch TV" self.close = AsyncMock() self.send_commands = AsyncMock() diff --git a/tests/components/network/test_init.py b/tests/components/network/test_init.py index e57b3242e8c..57a12868d0a 100644 --- a/tests/components/network/test_init.py +++ b/tests/components/network/test_init.py @@ -36,6 +36,7 @@ def _mock_cond_socket(sockname): class CondMockSock(MagicMock): def connect(self, addr): """Mock connect that stores addr.""" + # pylint: disable-next=attribute-defined-outside-init self._addr = addr[0] def getsockname(self): diff --git a/tests/components/sonos/conftest.py b/tests/components/sonos/conftest.py index bfece59ff9c..478443fff76 100644 --- a/tests/components/sonos/conftest.py +++ b/tests/components/sonos/conftest.py @@ -467,6 +467,7 @@ def music_library_fixture( def alarm_clock_fixture(): """Create alarmClock fixture.""" alarm_clock = SonosMockService("AlarmClock") + # pylint: disable-next=attribute-defined-outside-init alarm_clock.ListAlarms = Mock() alarm_clock.ListAlarms.return_value = { "CurrentAlarmListVersion": "RINCON_test:14", @@ -484,6 +485,7 @@ def alarm_clock_fixture(): def alarm_clock_fixture_extended(): """Create alarmClock fixture.""" alarm_clock = SonosMockService("AlarmClock") + # pylint: disable-next=attribute-defined-outside-init alarm_clock.ListAlarms = Mock() alarm_clock.ListAlarms.return_value = { "CurrentAlarmListVersion": "RINCON_test:15", diff --git a/tests/components/yeelight/__init__.py b/tests/components/yeelight/__init__.py index 6c940b0b229..8dc2acef416 100644 --- a/tests/components/yeelight/__init__.py +++ b/tests/components/yeelight/__init__.py @@ -132,6 +132,7 @@ class MockAsyncBulb: def _mocked_bulb(cannot_connect=False): + # pylint: disable=attribute-defined-outside-init bulb = MockAsyncBulb(MODEL, BulbType.Color, cannot_connect) type(bulb).async_get_properties = AsyncMock( side_effect=BulbException if cannot_connect else None diff --git a/tests/helpers/test_entity.py b/tests/helpers/test_entity.py index d105ffad791..a8524d73a5d 100644 --- a/tests/helpers/test_entity.py +++ b/tests/helpers/test_entity.py @@ -107,6 +107,7 @@ async def test_async_update_support(hass: HomeAssistant) -> None: """Async update.""" async_update.append(1) + # pylint: disable-next=attribute-defined-outside-init ent.async_update = async_update_func await ent.async_update_ha_state(True) diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py index 55b5d98fd30..986c3e5493e 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -94,8 +94,10 @@ async def test_polling_check_works_if_entity_add_fails( return self.hass.data is not None working_poll_ent = MockEntityNeedsSelfHassInShouldPoll(should_poll=True) + # pylint: disable-next=attribute-defined-outside-init working_poll_ent.async_update = AsyncMock() broken_poll_ent = MockEntityNeedsSelfHassInShouldPoll(should_poll=True) + # pylint: disable-next=attribute-defined-outside-init broken_poll_ent.async_update = AsyncMock(side_effect=Exception("Broken")) await component.async_add_entities( diff --git a/tests/helpers/test_schema_config_entry_flow.py b/tests/helpers/test_schema_config_entry_flow.py index 4db56a91c11..877e3762d3b 100644 --- a/tests/helpers/test_schema_config_entry_flow.py +++ b/tests/helpers/test_schema_config_entry_flow.py @@ -68,7 +68,9 @@ def manager_fixture(): return result mgr = FlowManager(None) + # pylint: disable-next=attribute-defined-outside-init mgr.mock_created_entries = entries + # pylint: disable-next=attribute-defined-outside-init mgr.mock_reg_handler = handlers.register return mgr diff --git a/tests/test_data_entry_flow.py b/tests/test_data_entry_flow.py index 312e2be7602..11e36e8f718 100644 --- a/tests/test_data_entry_flow.py +++ b/tests/test_data_entry_flow.py @@ -47,7 +47,9 @@ def manager(): return result mgr = FlowManager(None) + # pylint: disable-next=attribute-defined-outside-init mgr.mock_created_entries = entries + # pylint: disable-next=attribute-defined-outside-init mgr.mock_reg_handler = handlers.register return mgr From 8323266960657d3f93901fe0843b330f498d55f6 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 12 Jun 2024 12:23:24 +0200 Subject: [PATCH 0556/1445] Use pytest.mark.parametrize in airthings_ble tests (#119461) --- .../airthings_ble/test_config_flow.py | 30 ++++++++++--------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/tests/components/airthings_ble/test_config_flow.py b/tests/components/airthings_ble/test_config_flow.py index f6a7098785b..79ae46500dd 100644 --- a/tests/components/airthings_ble/test_config_flow.py +++ b/tests/components/airthings_ble/test_config_flow.py @@ -4,6 +4,7 @@ from unittest.mock import patch from airthings_ble import AirthingsDevice, AirthingsDeviceType from bleak import BleakError +import pytest from homeassistant.components.airthings_ble.const import DOMAIN from homeassistant.config_entries import SOURCE_BLUETOOTH, SOURCE_USER @@ -71,24 +72,25 @@ async def test_bluetooth_discovery_no_BLEDevice(hass: HomeAssistant) -> None: assert result["reason"] == "cannot_connect" +@pytest.mark.parametrize( + ("exc", "reason"), [(Exception(), "unknown"), (BleakError(), "cannot_connect")] +) async def test_bluetooth_discovery_airthings_ble_update_failed( - hass: HomeAssistant, + hass: HomeAssistant, exc: Exception, reason: str ) -> None: """Test discovery via bluetooth but there's an exception from airthings-ble.""" - for loop in [(Exception(), "unknown"), (BleakError(), "cannot_connect")]: - exc, reason = loop - with ( - patch_async_ble_device_from_address(WAVE_SERVICE_INFO), - patch_airthings_ble(side_effect=exc), - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_BLUETOOTH}, - data=WAVE_SERVICE_INFO, - ) + with ( + patch_async_ble_device_from_address(WAVE_SERVICE_INFO), + patch_airthings_ble(side_effect=exc), + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_BLUETOOTH}, + data=WAVE_SERVICE_INFO, + ) - assert result["type"] is FlowResultType.ABORT - assert result["reason"] == reason + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == reason async def test_bluetooth_discovery_already_setup(hass: HomeAssistant) -> None: From d69e62c0965d005aa8f0ad0a6645d3faf2e8bbbb Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 12 Jun 2024 12:24:16 +0200 Subject: [PATCH 0557/1445] Ignore undefined-loop-variable pylint warnings in zha tests (#119476) --- tests/components/zha/test_cluster_handlers.py | 1 + tests/components/zha/test_registries.py | 1 + 2 files changed, 2 insertions(+) diff --git a/tests/components/zha/test_cluster_handlers.py b/tests/components/zha/test_cluster_handlers.py index d09883c38e3..f89c47b79a2 100644 --- a/tests/components/zha/test_cluster_handlers.py +++ b/tests/components/zha/test_cluster_handlers.py @@ -368,6 +368,7 @@ def test_cluster_handler_registry() -> None: all_quirk_ids[cluster_id] = {None} all_quirk_ids[cluster_id].add(quirk_id) + # pylint: disable-next=undefined-loop-variable del quirk, model_quirk_list, manufacturer for ( diff --git a/tests/components/zha/test_registries.py b/tests/components/zha/test_registries.py index 279975a260f..18253186cf1 100644 --- a/tests/components/zha/test_registries.py +++ b/tests/components/zha/test_registries.py @@ -576,6 +576,7 @@ def test_quirk_classes() -> None: quirk_id = getattr(quirk, ATTR_QUIRK_ID, None) if quirk_id is not None and quirk_id not in all_quirk_ids: all_quirk_ids.append(quirk_id) + # pylint: disable-next=undefined-loop-variable del quirk, model_quirk_list, manufacturer # validate all quirk IDs used in component match rules From c70cfbb535b9980468b8d96bf5664025ddebb614 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 12 Jun 2024 12:25:29 +0200 Subject: [PATCH 0558/1445] Fix arguments-renamed pylint warning in tests (#119473) --- tests/common.py | 6 +++--- tests/components/config/test_config_entries.py | 2 +- tests/components/intent/test_init.py | 11 ++++++----- tests/components/nest/common.py | 4 ++-- tests/helpers/test_llm.py | 4 +--- tests/test_config_entries.py | 2 +- tests/test_data_entry_flow.py | 2 +- 7 files changed, 15 insertions(+), 16 deletions(-) diff --git a/tests/common.py b/tests/common.py index 2606b510430..ff1fea9cbda 100644 --- a/tests/common.py +++ b/tests/common.py @@ -413,10 +413,10 @@ def async_mock_intent(hass, intent_typ): class MockIntentHandler(intent.IntentHandler): intent_type = intent_typ - async def async_handle(self, intent): + async def async_handle(self, intent_obj): """Handle the intent.""" - intents.append(intent) - return intent.create_response() + intents.append(intent_obj) + return intent_obj.create_response() intent.async_register(hass, MockIntentHandler()) diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py index 17cc7d8c6de..95ff87c2beb 100644 --- a/tests/components/config/test_config_entries.py +++ b/tests/components/config/test_config_entries.py @@ -705,7 +705,7 @@ async def test_get_progress_index( class TestFlow(core_ce.ConfigFlow): VERSION = 5 - async def async_step_hassio(self, info): + async def async_step_hassio(self, discovery_info): return await self.async_step_account() async def async_step_account(self, user_input=None): diff --git a/tests/components/intent/test_init.py b/tests/components/intent/test_init.py index 95d1ee78538..09128681b9e 100644 --- a/tests/components/intent/test_init.py +++ b/tests/components/intent/test_init.py @@ -29,15 +29,16 @@ async def test_http_handle_intent( intent_type = "OrderBeer" - async def async_handle(self, intent): + async def async_handle(self, intent_obj): """Handle the intent.""" - assert intent.context.user_id == hass_admin_user.id - response = intent.create_response() + assert intent_obj.context.user_id == hass_admin_user.id + response = intent_obj.create_response() response.async_set_speech( - "I've ordered a {}!".format(intent.slots["type"]["value"]) + "I've ordered a {}!".format(intent_obj.slots["type"]["value"]) ) response.async_set_card( - "Beer ordered", "You chose a {}.".format(intent.slots["type"]["value"]) + "Beer ordered", + "You chose a {}.".format(intent_obj.slots["type"]["value"]), ) return response diff --git a/tests/components/nest/common.py b/tests/components/nest/common.py index d4eec5ae592..693fcae5b87 100644 --- a/tests/components/nest/common.py +++ b/tests/components/nest/common.py @@ -97,9 +97,9 @@ class FakeSubscriber(GoogleNestSubscriber): """Initialize Fake Subscriber.""" self._device_manager = DeviceManager() - def set_update_callback(self, callback: Callable[[EventMessage], Awaitable[None]]): + def set_update_callback(self, target: Callable[[EventMessage], Awaitable[None]]): """Capture the callback set by Home Assistant.""" - self._device_manager.set_update_callback(callback) + self._device_manager.set_update_callback(target) async def create_subscription(self): """Create the subscription.""" diff --git a/tests/helpers/test_llm.py b/tests/helpers/test_llm.py index 6ac17a2fe0e..17a0ef0e73e 100644 --- a/tests/helpers/test_llm.py +++ b/tests/helpers/test_llm.py @@ -49,9 +49,7 @@ async def test_register_api(hass: HomeAssistant, llm_context: llm.LLMContext) -> """Test registering an llm api.""" class MyAPI(llm.API): - async def async_get_api_instance( - self, tool_context: llm.ToolInput - ) -> llm.APIInstance: + async def async_get_api_instance(self, _: llm.ToolInput) -> llm.APIInstance: """Return a list of tools.""" return llm.APIInstance(self, "", [], llm_context) diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 2d946dd1fce..d410cb4568a 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -4879,7 +4879,7 @@ async def test_preview_not_supported( VERSION = 1 - async def async_step_user(self, data): + async def async_step_user(self, user_input): """Mock Reauth.""" return self.async_show_form(step_id="user_confirm") diff --git a/tests/test_data_entry_flow.py b/tests/test_data_entry_flow.py index 11e36e8f718..cc12ae42b67 100644 --- a/tests/test_data_entry_flow.py +++ b/tests/test_data_entry_flow.py @@ -260,7 +260,7 @@ async def test_finish_callback_change_result_type(hass: HomeAssistant) -> None: ) class FlowManager(data_entry_flow.FlowManager): - async def async_create_flow(self, handler_name, *, context, data): + async def async_create_flow(self, handler_key, *, context, data): """Create a test flow.""" return TestFlow() From 10b32e6a245b789c8cadb04d4710c6022aacdd91 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Wed, 12 Jun 2024 06:27:44 -0400 Subject: [PATCH 0559/1445] Store runtime data inside the config entry in Dremel 3D Printer (#119441) * Store runtime data inside the config entry in Dremel 3D Printer * add typing for config entry --- .../components/dremel_3d_printer/__init__.py | 20 +++++++++---------- .../dremel_3d_printer/binary_sensor.py | 9 +++------ .../components/dremel_3d_printer/button.py | 8 +++----- .../components/dremel_3d_printer/camera.py | 9 +++------ .../dremel_3d_printer/coordinator.py | 4 +++- .../components/dremel_3d_printer/sensor.py | 11 +++++----- 6 files changed, 26 insertions(+), 35 deletions(-) diff --git a/homeassistant/components/dremel_3d_printer/__init__.py b/homeassistant/components/dremel_3d_printer/__init__.py index 76cd63a3a1d..632c42d9b54 100644 --- a/homeassistant/components/dremel_3d_printer/__init__.py +++ b/homeassistant/components/dremel_3d_printer/__init__.py @@ -5,18 +5,19 @@ from __future__ import annotations from dremel3dpy import Dremel3DPrinter from requests.exceptions import ConnectTimeout, HTTPError -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady -from .const import CAMERA_MODEL, DOMAIN -from .coordinator import Dremel3DPrinterDataUpdateCoordinator +from .const import CAMERA_MODEL +from .coordinator import Dremel3DPrinterDataUpdateCoordinator, DremelConfigEntry PLATFORMS = [Platform.BINARY_SENSOR, Platform.BUTTON, Platform.CAMERA, Platform.SENSOR] -async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: +async def async_setup_entry( + hass: HomeAssistant, config_entry: DremelConfigEntry +) -> bool: """Set up Dremel 3D Printer from a config entry.""" try: api = await hass.async_add_executor_job( @@ -30,7 +31,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b coordinator = Dremel3DPrinterDataUpdateCoordinator(hass, api) await coordinator.async_config_entry_first_refresh() - hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = coordinator + config_entry.runtime_data = coordinator platforms = list(PLATFORMS) if api.get_model() != CAMERA_MODEL: platforms.remove(Platform.CAMERA) @@ -38,12 +39,9 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: DremelConfigEntry) -> bool: """Unload Dremel config entry.""" platforms = list(PLATFORMS) - api: Dremel3DPrinter = hass.data[DOMAIN][entry.entry_id].api - if api.get_model() != CAMERA_MODEL: + if entry.runtime_data.api.get_model() != CAMERA_MODEL: platforms.remove(Platform.CAMERA) - if unload_ok := await hass.config_entries.async_unload_platforms(entry, platforms): - hass.data[DOMAIN].pop(entry.entry_id) - return unload_ok + return await hass.config_entries.async_unload_platforms(entry, platforms) diff --git a/homeassistant/components/dremel_3d_printer/binary_sensor.py b/homeassistant/components/dremel_3d_printer/binary_sensor.py index e6df0ebcf6e..972945a84bb 100644 --- a/homeassistant/components/dremel_3d_printer/binary_sensor.py +++ b/homeassistant/components/dremel_3d_printer/binary_sensor.py @@ -12,11 +12,10 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntity, BinarySensorEntityDescription, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN +from .coordinator import DremelConfigEntry from .entity import Dremel3DPrinterEntity @@ -43,14 +42,12 @@ BINARY_SENSOR_TYPES: tuple[Dremel3DPrinterBinarySensorEntityDescription, ...] = async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: DremelConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up the available Dremel binary sensors.""" - coordinator = hass.data[DOMAIN][config_entry.entry_id] - async_add_entities( - Dremel3DPrinterBinarySensor(coordinator, description) + Dremel3DPrinterBinarySensor(config_entry.runtime_data, description) for description in BINARY_SENSOR_TYPES ) diff --git a/homeassistant/components/dremel_3d_printer/button.py b/homeassistant/components/dremel_3d_printer/button.py index d92263b6a15..f91c1b0ea51 100644 --- a/homeassistant/components/dremel_3d_printer/button.py +++ b/homeassistant/components/dremel_3d_printer/button.py @@ -8,12 +8,11 @@ from dataclasses import dataclass from dremel3dpy import Dremel3DPrinter from homeassistant.components.button import ButtonEntity, ButtonEntityDescription -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN +from .coordinator import DremelConfigEntry from .entity import Dremel3DPrinterEntity @@ -45,13 +44,12 @@ BUTTON_TYPES: tuple[Dremel3DPrinterButtonEntityDescription, ...] = ( async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: DremelConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up Dremel 3D Printer control buttons.""" - coordinator = hass.data[DOMAIN][config_entry.entry_id] async_add_entities( - Dremel3DPrinterButtonEntity(coordinator, description) + Dremel3DPrinterButtonEntity(config_entry.runtime_data, description) for description in BUTTON_TYPES ) diff --git a/homeassistant/components/dremel_3d_printer/camera.py b/homeassistant/components/dremel_3d_printer/camera.py index dc663844c9c..f4293915a25 100644 --- a/homeassistant/components/dremel_3d_printer/camera.py +++ b/homeassistant/components/dremel_3d_printer/camera.py @@ -4,12 +4,10 @@ from __future__ import annotations from homeassistant.components.camera import CameraEntityDescription from homeassistant.components.mjpeg import MjpegCamera -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import Dremel3DPrinterDataUpdateCoordinator -from .const import DOMAIN +from .coordinator import Dremel3DPrinterDataUpdateCoordinator, DremelConfigEntry from .entity import Dremel3DPrinterEntity CAMERA_TYPE = CameraEntityDescription( @@ -20,12 +18,11 @@ CAMERA_TYPE = CameraEntityDescription( async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: DremelConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up a MJPEG IP Camera for the 3D45 Model. The 3D20 and 3D40 models don't have built in cameras.""" - coordinator = hass.data[DOMAIN][config_entry.entry_id] - async_add_entities([Dremel3D45Camera(coordinator, CAMERA_TYPE)]) + async_add_entities([Dremel3D45Camera(config_entry.runtime_data, CAMERA_TYPE)]) class Dremel3D45Camera(Dremel3DPrinterEntity, MjpegCamera): diff --git a/homeassistant/components/dremel_3d_printer/coordinator.py b/homeassistant/components/dremel_3d_printer/coordinator.py index 81e0053fd77..3323569c05f 100644 --- a/homeassistant/components/dremel_3d_printer/coordinator.py +++ b/homeassistant/components/dremel_3d_printer/coordinator.py @@ -10,11 +10,13 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda from .const import DOMAIN, LOGGER +type DremelConfigEntry = ConfigEntry[Dremel3DPrinterDataUpdateCoordinator] + class Dremel3DPrinterDataUpdateCoordinator(DataUpdateCoordinator[None]): """Class to manage fetching Dremel 3D Printer data.""" - config_entry: ConfigEntry + config_entry: DremelConfigEntry def __init__(self, hass: HomeAssistant, api: Dremel3DPrinter) -> None: """Initialize Dremel 3D Printer data update coordinator.""" diff --git a/homeassistant/components/dremel_3d_printer/sensor.py b/homeassistant/components/dremel_3d_printer/sensor.py index bda2bb537fd..002a5fc4adb 100644 --- a/homeassistant/components/dremel_3d_printer/sensor.py +++ b/homeassistant/components/dremel_3d_printer/sensor.py @@ -14,7 +14,6 @@ from homeassistant.components.sensor import ( SensorEntityDescription, SensorStateClass, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( PERCENTAGE, EntityCategory, @@ -28,7 +27,8 @@ from homeassistant.helpers.typing import StateType from homeassistant.util.dt import utcnow from homeassistant.util.variance import ignore_variance -from .const import ATTR_EXTRUDER, ATTR_PLATFORM, DOMAIN +from .const import ATTR_EXTRUDER, ATTR_PLATFORM +from .coordinator import DremelConfigEntry from .entity import Dremel3DPrinterEntity @@ -234,14 +234,13 @@ SENSOR_TYPES: tuple[Dremel3DPrinterSensorEntityDescription, ...] = ( async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: DremelConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up the available Dremel 3D Printer sensors.""" - coordinator = hass.data[DOMAIN][config_entry.entry_id] - async_add_entities( - Dremel3DPrinterSensor(coordinator, description) for description in SENSOR_TYPES + Dremel3DPrinterSensor(config_entry.runtime_data, description) + for description in SENSOR_TYPES ) From abb8c58b87047c4bd3b7e6b6ad24644b0fed4ef1 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 12 Jun 2024 12:35:01 +0200 Subject: [PATCH 0560/1445] Fix consider-using-tuple pylint warnings in core tests (#119463) --- .../alarm_control_panel/test_device_action.py | 2 +- .../test_device_condition.py | 2 +- .../test_device_trigger.py | 2 +- .../binary_sensor/test_device_condition.py | 2 +- .../binary_sensor/test_device_trigger.py | 2 +- tests/components/button/test_device_action.py | 2 +- .../components/button/test_device_trigger.py | 2 +- tests/components/climate/common.py | 8 +++---- .../components/climate/test_device_action.py | 2 +- .../climate/test_device_condition.py | 2 +- .../components/climate/test_device_trigger.py | 8 +++---- tests/components/cover/test_device_action.py | 2 +- .../components/cover/test_device_condition.py | 2 +- tests/components/cover/test_device_trigger.py | 2 +- .../device_tracker/test_device_condition.py | 4 ++-- .../device_tracker/test_device_trigger.py | 4 ++-- tests/components/fan/common.py | 22 +++++++++---------- tests/components/fan/test_device_action.py | 4 ++-- tests/components/fan/test_device_condition.py | 4 ++-- tests/components/fan/test_device_trigger.py | 4 ++-- tests/components/group/common.py | 4 ++-- tests/components/group/test_init.py | 4 ++-- .../humidifier/test_device_condition.py | 2 +- tests/components/light/common.py | 12 +++++----- tests/components/light/test_device_action.py | 6 ++--- .../components/light/test_device_condition.py | 4 ++-- tests/components/light/test_device_trigger.py | 4 ++-- tests/components/lock/test_device_action.py | 2 +- .../components/lock/test_device_condition.py | 8 +++---- tests/components/lock/test_device_trigger.py | 8 +++---- tests/components/remote/test_device_action.py | 4 ++-- .../remote/test_device_condition.py | 4 ++-- .../components/remote/test_device_trigger.py | 4 ++-- tests/components/select/test_device_action.py | 8 +++---- .../select/test_device_condition.py | 2 +- .../components/select/test_device_trigger.py | 2 +- .../sensor/test_device_condition.py | 2 +- .../components/sensor/test_device_trigger.py | 2 +- tests/components/sensor/test_recorder.py | 18 +++++++-------- tests/components/switch/test_device_action.py | 4 ++-- .../switch/test_device_condition.py | 4 ++-- .../components/switch/test_device_trigger.py | 4 ++-- tests/components/vacuum/test_device_action.py | 4 ++-- .../vacuum/test_device_condition.py | 4 ++-- .../components/vacuum/test_device_trigger.py | 4 ++-- tests/components/water_heater/common.py | 4 ++-- .../water_heater/test_device_action.py | 4 ++-- tests/helpers/test_config_validation.py | 8 +++---- tests/helpers/test_entity_platform.py | 2 +- tests/test_config.py | 16 +++++++------- tests/test_core.py | 16 +++++++------- 51 files changed, 128 insertions(+), 128 deletions(-) diff --git a/tests/components/alarm_control_panel/test_device_action.py b/tests/components/alarm_control_panel/test_device_action.py index 04c0e3b045b..9c5aaffd733 100644 --- a/tests/components/alarm_control_panel/test_device_action.py +++ b/tests/components/alarm_control_panel/test_device_action.py @@ -174,7 +174,7 @@ async def test_get_actions_hidden_auxiliary( "entity_id": entity_entry.id, "metadata": {"secondary": True}, } - for action in ["disarm", "arm_away"] + for action in ("disarm", "arm_away") ] actions = await async_get_device_automations( hass, DeviceAutomationType.ACTION, device_entry.id diff --git a/tests/components/alarm_control_panel/test_device_condition.py b/tests/components/alarm_control_panel/test_device_condition.py index 9f8f56ccb6f..da1d77f50a3 100644 --- a/tests/components/alarm_control_panel/test_device_condition.py +++ b/tests/components/alarm_control_panel/test_device_condition.py @@ -167,7 +167,7 @@ async def test_get_conditions_hidden_auxiliary( "entity_id": entity_entry.id, "metadata": {"secondary": True}, } - for condition in ["is_disarmed", "is_triggered"] + for condition in ("is_disarmed", "is_triggered") ] conditions = await async_get_device_automations( hass, DeviceAutomationType.CONDITION, device_entry.id diff --git a/tests/components/alarm_control_panel/test_device_trigger.py b/tests/components/alarm_control_panel/test_device_trigger.py index 6be15cca097..46eba314dc1 100644 --- a/tests/components/alarm_control_panel/test_device_trigger.py +++ b/tests/components/alarm_control_panel/test_device_trigger.py @@ -162,7 +162,7 @@ async def test_get_triggers_hidden_auxiliary( "entity_id": entry.id, "metadata": {"secondary": True}, } - for trigger in ["triggered", "disarmed", "arming"] + for trigger in ("triggered", "disarmed", "arming") ] triggers = await async_get_device_automations( hass, DeviceAutomationType.TRIGGER, device_entry.id diff --git a/tests/components/binary_sensor/test_device_condition.py b/tests/components/binary_sensor/test_device_condition.py index 7d7b4f62c87..c2bd29fad36 100644 --- a/tests/components/binary_sensor/test_device_condition.py +++ b/tests/components/binary_sensor/test_device_condition.py @@ -122,7 +122,7 @@ async def test_get_conditions_hidden_auxiliary( "entity_id": entity_entry.id, "metadata": {"secondary": True}, } - for condition in ["is_on", "is_off"] + for condition in ("is_on", "is_off") ] conditions = await async_get_device_automations( hass, DeviceAutomationType.CONDITION, device_entry.id diff --git a/tests/components/binary_sensor/test_device_trigger.py b/tests/components/binary_sensor/test_device_trigger.py index 2ecd17fd0d1..f91a336061d 100644 --- a/tests/components/binary_sensor/test_device_trigger.py +++ b/tests/components/binary_sensor/test_device_trigger.py @@ -122,7 +122,7 @@ async def test_get_triggers_hidden_auxiliary( "entity_id": entry.id, "metadata": {"secondary": True}, } - for trigger in ["turned_on", "turned_off"] + for trigger in ("turned_on", "turned_off") ] triggers = await async_get_device_automations( hass, DeviceAutomationType.TRIGGER, device_entry.id diff --git a/tests/components/button/test_device_action.py b/tests/components/button/test_device_action.py index c3ba03b60e6..837a433c87c 100644 --- a/tests/components/button/test_device_action.py +++ b/tests/components/button/test_device_action.py @@ -88,7 +88,7 @@ async def test_get_actions_hidden_auxiliary( "entity_id": entity_entry.id, "metadata": {"secondary": True}, } - for action in ["press"] + for action in ("press",) ] actions = await async_get_device_automations( hass, DeviceAutomationType.ACTION, device_entry.id diff --git a/tests/components/button/test_device_trigger.py b/tests/components/button/test_device_trigger.py index 1d9a84b0e8f..dee8045a71f 100644 --- a/tests/components/button/test_device_trigger.py +++ b/tests/components/button/test_device_trigger.py @@ -97,7 +97,7 @@ async def test_get_triggers_hidden_auxiliary( "entity_id": entity_entry.id, "metadata": {"secondary": True}, } - for trigger in ["pressed"] + for trigger in ("pressed",) ] triggers = await async_get_device_automations( hass, DeviceAutomationType.TRIGGER, device_entry.id diff --git a/tests/components/climate/common.py b/tests/components/climate/common.py index 20f6bfd880d..c890d3a7bb5 100644 --- a/tests/components/climate/common.py +++ b/tests/components/climate/common.py @@ -86,13 +86,13 @@ async def async_set_temperature( """Set new target temperature.""" kwargs = { key: value - for key, value in [ + for key, value in ( (ATTR_TEMPERATURE, temperature), (ATTR_TARGET_TEMP_HIGH, target_temp_high), (ATTR_TARGET_TEMP_LOW, target_temp_low), (ATTR_ENTITY_ID, entity_id), (ATTR_HVAC_MODE, hvac_mode), - ] + ) if value is not None } _LOGGER.debug("set_temperature start data=%s", kwargs) @@ -113,13 +113,13 @@ def set_temperature( """Set new target temperature.""" kwargs = { key: value - for key, value in [ + for key, value in ( (ATTR_TEMPERATURE, temperature), (ATTR_TARGET_TEMP_HIGH, target_temp_high), (ATTR_TARGET_TEMP_LOW, target_temp_low), (ATTR_ENTITY_ID, entity_id), (ATTR_HVAC_MODE, hvac_mode), - ] + ) if value is not None } _LOGGER.debug("set_temperature start data=%s", kwargs) diff --git a/tests/components/climate/test_device_action.py b/tests/components/climate/test_device_action.py index 850f8b6c843..361aeaec867 100644 --- a/tests/components/climate/test_device_action.py +++ b/tests/components/climate/test_device_action.py @@ -136,7 +136,7 @@ async def test_get_actions_hidden_auxiliary( "entity_id": entity_entry.id, "metadata": {"secondary": True}, } - for action in ["set_hvac_mode"] + for action in ("set_hvac_mode",) ] actions = await async_get_device_automations( hass, DeviceAutomationType.ACTION, device_entry.id diff --git a/tests/components/climate/test_device_condition.py b/tests/components/climate/test_device_condition.py index 01513bcc506..0961bd3dc73 100644 --- a/tests/components/climate/test_device_condition.py +++ b/tests/components/climate/test_device_condition.py @@ -139,7 +139,7 @@ async def test_get_conditions_hidden_auxiliary( "entity_id": entity_entry.id, "metadata": {"secondary": True}, } - for condition in ["is_hvac_mode"] + for condition in ("is_hvac_mode",) ] conditions = await async_get_device_automations( hass, DeviceAutomationType.CONDITION, device_entry.id diff --git a/tests/components/climate/test_device_trigger.py b/tests/components/climate/test_device_trigger.py index 094c743f2b3..e8e5b577bf4 100644 --- a/tests/components/climate/test_device_trigger.py +++ b/tests/components/climate/test_device_trigger.py @@ -74,11 +74,11 @@ async def test_get_triggers( "entity_id": entity_entry.id, "metadata": {"secondary": False}, } - for trigger in [ + for trigger in ( "hvac_mode_changed", "current_temperature_changed", "current_humidity_changed", - ] + ) ] triggers = await async_get_device_automations( hass, DeviceAutomationType.TRIGGER, device_entry.id @@ -135,11 +135,11 @@ async def test_get_triggers_hidden_auxiliary( "entity_id": entity_entry.id, "metadata": {"secondary": True}, } - for trigger in [ + for trigger in ( "hvac_mode_changed", "current_temperature_changed", "current_humidity_changed", - ] + ) ] triggers = await async_get_device_automations( hass, DeviceAutomationType.TRIGGER, device_entry.id diff --git a/tests/components/cover/test_device_action.py b/tests/components/cover/test_device_action.py index d38f02d9c6e..db9e75bcaef 100644 --- a/tests/components/cover/test_device_action.py +++ b/tests/components/cover/test_device_action.py @@ -136,7 +136,7 @@ async def test_get_actions_hidden_auxiliary( "entity_id": entity_entry.id, "metadata": {"secondary": True}, } - for action in ["close"] + for action in ("close",) ] actions = await async_get_device_automations( hass, DeviceAutomationType.ACTION, device_entry.id diff --git a/tests/components/cover/test_device_condition.py b/tests/components/cover/test_device_condition.py index 9e5e5db1862..545bdd6587e 100644 --- a/tests/components/cover/test_device_condition.py +++ b/tests/components/cover/test_device_condition.py @@ -165,7 +165,7 @@ async def test_get_conditions_hidden_auxiliary( "entity_id": entity_entry.id, "metadata": {"secondary": True}, } - for condition in ["is_open", "is_closed", "is_opening", "is_closing"] + for condition in ("is_open", "is_closed", "is_opening", "is_closing") ] conditions = await async_get_device_automations( hass, DeviceAutomationType.CONDITION, device_entry.id diff --git a/tests/components/cover/test_device_trigger.py b/tests/components/cover/test_device_trigger.py index 1ad84e52c0c..419eea05f9f 100644 --- a/tests/components/cover/test_device_trigger.py +++ b/tests/components/cover/test_device_trigger.py @@ -166,7 +166,7 @@ async def test_get_triggers_hidden_auxiliary( "entity_id": entity_entry.id, "metadata": {"secondary": True}, } - for trigger in ["opened", "closed", "opening", "closing"] + for trigger in ("opened", "closed", "opening", "closing") ] triggers = await async_get_device_automations( hass, DeviceAutomationType.TRIGGER, device_entry.id diff --git a/tests/components/device_tracker/test_device_condition.py b/tests/components/device_tracker/test_device_condition.py index 3147f7ee2fd..6ea4ed7a372 100644 --- a/tests/components/device_tracker/test_device_condition.py +++ b/tests/components/device_tracker/test_device_condition.py @@ -54,7 +54,7 @@ async def test_get_conditions( "entity_id": entity_entry.id, "metadata": {"secondary": False}, } - for condition in ["is_not_home", "is_home"] + for condition in ("is_not_home", "is_home") ] conditions = await async_get_device_automations( hass, DeviceAutomationType.CONDITION, device_entry.id @@ -102,7 +102,7 @@ async def test_get_conditions_hidden_auxiliary( "entity_id": entity_entry.id, "metadata": {"secondary": True}, } - for condition in ["is_not_home", "is_home"] + for condition in ("is_not_home", "is_home") ] conditions = await async_get_device_automations( hass, DeviceAutomationType.CONDITION, device_entry.id diff --git a/tests/components/device_tracker/test_device_trigger.py b/tests/components/device_tracker/test_device_trigger.py index 0a74c009ee3..8932eb15997 100644 --- a/tests/components/device_tracker/test_device_trigger.py +++ b/tests/components/device_tracker/test_device_trigger.py @@ -85,7 +85,7 @@ async def test_get_triggers( "entity_id": entity_entry.id, "metadata": {"secondary": False}, } - for trigger in ["leaves", "enters"] + for trigger in ("leaves", "enters") ] triggers = await async_get_device_automations( hass, DeviceAutomationType.TRIGGER, device_entry.id @@ -133,7 +133,7 @@ async def test_get_triggers_hidden_auxiliary( "entity_id": entity_entry.id, "metadata": {"secondary": True}, } - for trigger in ["leaves", "enters"] + for trigger in ("leaves", "enters") ] triggers = await async_get_device_automations( hass, DeviceAutomationType.TRIGGER, device_entry.id diff --git a/tests/components/fan/common.py b/tests/components/fan/common.py index fbc7c7bb1bb..74939342fac 100644 --- a/tests/components/fan/common.py +++ b/tests/components/fan/common.py @@ -38,11 +38,11 @@ async def async_turn_on( """Turn all or specified fan on.""" data = { key: value - for key, value in [ + for key, value in ( (ATTR_ENTITY_ID, entity_id), (ATTR_PERCENTAGE, percentage), (ATTR_PRESET_MODE, preset_mode), - ] + ) if value is not None } @@ -64,10 +64,10 @@ async def async_oscillate( """Set oscillation on all or specified fan.""" data = { key: value - for key, value in [ + for key, value in ( (ATTR_ENTITY_ID, entity_id), (ATTR_OSCILLATING, should_oscillate), - ] + ) if value is not None } @@ -81,7 +81,7 @@ async def async_set_preset_mode( """Set preset mode for all or specified fan.""" data = { key: value - for key, value in [(ATTR_ENTITY_ID, entity_id), (ATTR_PRESET_MODE, preset_mode)] + for key, value in ((ATTR_ENTITY_ID, entity_id), (ATTR_PRESET_MODE, preset_mode)) if value is not None } @@ -95,7 +95,7 @@ async def async_set_percentage( """Set percentage for all or specified fan.""" data = { key: value - for key, value in [(ATTR_ENTITY_ID, entity_id), (ATTR_PERCENTAGE, percentage)] + for key, value in ((ATTR_ENTITY_ID, entity_id), (ATTR_PERCENTAGE, percentage)) if value is not None } @@ -109,10 +109,10 @@ async def async_increase_speed( """Increase speed for all or specified fan.""" data = { key: value - for key, value in [ + for key, value in ( (ATTR_ENTITY_ID, entity_id), (ATTR_PERCENTAGE_STEP, percentage_step), - ] + ) if value is not None } @@ -126,10 +126,10 @@ async def async_decrease_speed( """Decrease speed for all or specified fan.""" data = { key: value - for key, value in [ + for key, value in ( (ATTR_ENTITY_ID, entity_id), (ATTR_PERCENTAGE_STEP, percentage_step), - ] + ) if value is not None } @@ -143,7 +143,7 @@ async def async_set_direction( """Set direction for all or specified fan.""" data = { key: value - for key, value in [(ATTR_ENTITY_ID, entity_id), (ATTR_DIRECTION, direction)] + for key, value in ((ATTR_ENTITY_ID, entity_id), (ATTR_DIRECTION, direction)) if value is not None } diff --git a/tests/components/fan/test_device_action.py b/tests/components/fan/test_device_action.py index 96e02ab5592..647e45374ac 100644 --- a/tests/components/fan/test_device_action.py +++ b/tests/components/fan/test_device_action.py @@ -48,7 +48,7 @@ async def test_get_actions( "entity_id": entity_entry.id, "metadata": {"secondary": False}, } - for action in ["turn_on", "turn_off", "toggle"] + for action in ("turn_on", "turn_off", "toggle") ] actions = await async_get_device_automations( hass, DeviceAutomationType.ACTION, device_entry.id @@ -96,7 +96,7 @@ async def test_get_actions_hidden_auxiliary( "entity_id": entity_entry.id, "metadata": {"secondary": True}, } - for action in ["turn_on", "turn_off", "toggle"] + for action in ("turn_on", "turn_off", "toggle") ] actions = await async_get_device_automations( hass, DeviceAutomationType.ACTION, device_entry.id diff --git a/tests/components/fan/test_device_condition.py b/tests/components/fan/test_device_condition.py index d442d91c9dd..9f9bde1a680 100644 --- a/tests/components/fan/test_device_condition.py +++ b/tests/components/fan/test_device_condition.py @@ -54,7 +54,7 @@ async def test_get_conditions( "entity_id": entity_entry.id, "metadata": {"secondary": False}, } - for condition in ["is_off", "is_on"] + for condition in ("is_off", "is_on") ] conditions = await async_get_device_automations( hass, DeviceAutomationType.CONDITION, device_entry.id @@ -102,7 +102,7 @@ async def test_get_conditions_hidden_auxiliary( "entity_id": entity_entry.id, "metadata": {"secondary": True}, } - for condition in ["is_off", "is_on"] + for condition in ("is_off", "is_on") ] conditions = await async_get_device_automations( hass, DeviceAutomationType.CONDITION, device_entry.id diff --git a/tests/components/fan/test_device_trigger.py b/tests/components/fan/test_device_trigger.py index 445193b27d4..38f39376592 100644 --- a/tests/components/fan/test_device_trigger.py +++ b/tests/components/fan/test_device_trigger.py @@ -59,7 +59,7 @@ async def test_get_triggers( "entity_id": entity_entry.id, "metadata": {"secondary": False}, } - for trigger in ["turned_off", "turned_on", "changed_states"] + for trigger in ("turned_off", "turned_on", "changed_states") ] triggers = await async_get_device_automations( hass, DeviceAutomationType.TRIGGER, device_entry.id @@ -107,7 +107,7 @@ async def test_get_triggers_hidden_auxiliary( "entity_id": entity_entry.id, "metadata": {"secondary": True}, } - for trigger in ["turned_off", "turned_on", "changed_states"] + for trigger in ("turned_off", "turned_on", "changed_states") ] triggers = await async_get_device_automations( hass, DeviceAutomationType.TRIGGER, device_entry.id diff --git a/tests/components/group/common.py b/tests/components/group/common.py index 395fc990930..86fe537a776 100644 --- a/tests/components/group/common.py +++ b/tests/components/group/common.py @@ -64,13 +64,13 @@ def async_set_group( """Create/Update a group.""" data = { key: value - for key, value in [ + for key, value in ( (ATTR_OBJECT_ID, object_id), (ATTR_NAME, name), (ATTR_ENTITIES, entity_ids), (ATTR_ICON, icon), (ATTR_ADD_ENTITIES, add), - ] + ) if value is not None } diff --git a/tests/components/group/test_init.py b/tests/components/group/test_init.py index e2e618002ac..7434de74f63 100644 --- a/tests/components/group/test_init.py +++ b/tests/components/group/test_init.py @@ -1236,7 +1236,7 @@ async def test_group_mixed_domains_on(hass: HomeAssistant) -> None: hass.states.async_set("binary_sensor.alexander_garage_side_door_open", "on") hass.states.async_set("cover.small_garage_door", "open") - for domain in ["lock", "binary_sensor", "cover"]: + for domain in ("lock", "binary_sensor", "cover"): assert await async_setup_component(hass, domain, {}) assert await async_setup_component( hass, @@ -1261,7 +1261,7 @@ async def test_group_mixed_domains_off(hass: HomeAssistant) -> None: hass.states.async_set("binary_sensor.alexander_garage_side_door_open", "off") hass.states.async_set("cover.small_garage_door", "closed") - for domain in ["lock", "binary_sensor", "cover"]: + for domain in ("lock", "binary_sensor", "cover"): assert await async_setup_component(hass, domain, {}) assert await async_setup_component( hass, diff --git a/tests/components/humidifier/test_device_condition.py b/tests/components/humidifier/test_device_condition.py index e9b84a1b515..4f4d21adcba 100644 --- a/tests/components/humidifier/test_device_condition.py +++ b/tests/components/humidifier/test_device_condition.py @@ -141,7 +141,7 @@ async def test_get_conditions_hidden_auxiliary( "entity_id": entity_entry.id, "metadata": {"secondary": True}, } - for condition in ["is_off", "is_on"] + for condition in ("is_off", "is_on") ] conditions = await async_get_device_automations( hass, DeviceAutomationType.CONDITION, device_entry.id diff --git a/tests/components/light/common.py b/tests/components/light/common.py index 26c4d18706d..fd9557b05b2 100644 --- a/tests/components/light/common.py +++ b/tests/components/light/common.py @@ -101,7 +101,7 @@ async def async_turn_on( """Turn all or specified light on.""" data = { key: value - for key, value in [ + for key, value in ( (ATTR_ENTITY_ID, entity_id), (ATTR_PROFILE, profile), (ATTR_TRANSITION, transition), @@ -118,7 +118,7 @@ async def async_turn_on( (ATTR_EFFECT, effect), (ATTR_COLOR_NAME, color_name), (ATTR_WHITE, white), - ] + ) if value is not None } @@ -135,11 +135,11 @@ async def async_turn_off(hass, entity_id=ENTITY_MATCH_ALL, transition=None, flas """Turn all or specified light off.""" data = { key: value - for key, value in [ + for key, value in ( (ATTR_ENTITY_ID, entity_id), (ATTR_TRANSITION, transition), (ATTR_FLASH, flash), - ] + ) if value is not None } @@ -202,7 +202,7 @@ async def async_toggle( """Turn all or specified light on.""" data = { key: value - for key, value in [ + for key, value in ( (ATTR_ENTITY_ID, entity_id), (ATTR_PROFILE, profile), (ATTR_TRANSITION, transition), @@ -216,7 +216,7 @@ async def async_toggle( (ATTR_FLASH, flash), (ATTR_EFFECT, effect), (ATTR_COLOR_NAME, color_name), - ] + ) if value is not None } diff --git a/tests/components/light/test_device_action.py b/tests/components/light/test_device_action.py index 1013942f96b..8848ce19621 100644 --- a/tests/components/light/test_device_action.py +++ b/tests/components/light/test_device_action.py @@ -66,14 +66,14 @@ async def test_get_actions( "entity_id": entity_entry.id, "metadata": {"secondary": False}, } - for action in [ + for action in ( "brightness_decrease", "brightness_increase", "flash", "turn_off", "turn_on", "toggle", - ] + ) ] actions = await async_get_device_automations( hass, DeviceAutomationType.ACTION, device_entry.id @@ -123,7 +123,7 @@ async def test_get_actions_hidden_auxiliary( "entity_id": entity_entry.id, "metadata": {"secondary": True}, } - for action in ["turn_on", "turn_off", "toggle"] + for action in ("turn_on", "turn_off", "toggle") ] actions = await async_get_device_automations( hass, DeviceAutomationType.ACTION, device_entry.id diff --git a/tests/components/light/test_device_condition.py b/tests/components/light/test_device_condition.py index cef3ef788cb..11dea49ea60 100644 --- a/tests/components/light/test_device_condition.py +++ b/tests/components/light/test_device_condition.py @@ -62,7 +62,7 @@ async def test_get_conditions( "entity_id": entity_entry.id, "metadata": {"secondary": False}, } - for condition in ["is_off", "is_on"] + for condition in ("is_off", "is_on") ] conditions = await async_get_device_automations( hass, DeviceAutomationType.CONDITION, device_entry.id @@ -110,7 +110,7 @@ async def test_get_conditions_hidden_auxiliary( "entity_id": entity_entry.id, "metadata": {"secondary": True}, } - for condition in ["is_off", "is_on"] + for condition in ("is_off", "is_on") ] conditions = await async_get_device_automations( hass, DeviceAutomationType.CONDITION, device_entry.id diff --git a/tests/components/light/test_device_trigger.py b/tests/components/light/test_device_trigger.py index b61b69fef25..ab3babd1b64 100644 --- a/tests/components/light/test_device_trigger.py +++ b/tests/components/light/test_device_trigger.py @@ -67,7 +67,7 @@ async def test_get_triggers( "entity_id": entity_entry.id, "metadata": {"secondary": False}, } - for trigger in ["changed_states", "turned_off", "turned_on"] + for trigger in ("changed_states", "turned_off", "turned_on") ] triggers = await async_get_device_automations( hass, DeviceAutomationType.TRIGGER, device_entry.id @@ -115,7 +115,7 @@ async def test_get_triggers_hidden_auxiliary( "entity_id": entity_entry.id, "metadata": {"secondary": True}, } - for trigger in ["changed_states", "turned_off", "turned_on"] + for trigger in ("changed_states", "turned_off", "turned_on") ] triggers = await async_get_device_automations( hass, DeviceAutomationType.TRIGGER, device_entry.id diff --git a/tests/components/lock/test_device_action.py b/tests/components/lock/test_device_action.py index 3b46117ccd2..e77e7edd005 100644 --- a/tests/components/lock/test_device_action.py +++ b/tests/components/lock/test_device_action.py @@ -129,7 +129,7 @@ async def test_get_actions_hidden_auxiliary( "entity_id": entity_entry.id, "metadata": {"secondary": True}, } - for action in ["lock", "unlock"] + for action in ("lock", "unlock") ] actions = await async_get_device_automations( hass, DeviceAutomationType.ACTION, device_entry.id diff --git a/tests/components/lock/test_device_condition.py b/tests/components/lock/test_device_condition.py index ce7ce773999..97afe9fb759 100644 --- a/tests/components/lock/test_device_condition.py +++ b/tests/components/lock/test_device_condition.py @@ -63,7 +63,7 @@ async def test_get_conditions( "entity_id": entity_entry.id, "metadata": {"secondary": False}, } - for condition in [ + for condition in ( "is_locked", "is_unlocked", "is_unlocking", @@ -71,7 +71,7 @@ async def test_get_conditions( "is_jammed", "is_open", "is_opening", - ] + ) ] conditions = await async_get_device_automations( hass, DeviceAutomationType.CONDITION, device_entry.id @@ -119,7 +119,7 @@ async def test_get_conditions_hidden_auxiliary( "entity_id": entity_entry.id, "metadata": {"secondary": True}, } - for condition in [ + for condition in ( "is_locked", "is_unlocked", "is_unlocking", @@ -127,7 +127,7 @@ async def test_get_conditions_hidden_auxiliary( "is_jammed", "is_open", "is_opening", - ] + ) ] conditions = await async_get_device_automations( hass, DeviceAutomationType.CONDITION, device_entry.id diff --git a/tests/components/lock/test_device_trigger.py b/tests/components/lock/test_device_trigger.py index 800b2ea756e..3cbfbb1a04c 100644 --- a/tests/components/lock/test_device_trigger.py +++ b/tests/components/lock/test_device_trigger.py @@ -72,7 +72,7 @@ async def test_get_triggers( "entity_id": entity_entry.id, "metadata": {"secondary": False}, } - for trigger in [ + for trigger in ( "locked", "unlocked", "unlocking", @@ -80,7 +80,7 @@ async def test_get_triggers( "jammed", "open", "opening", - ] + ) ] triggers = await async_get_device_automations( hass, DeviceAutomationType.TRIGGER, device_entry.id @@ -129,7 +129,7 @@ async def test_get_triggers_hidden_auxiliary( "entity_id": entity_entry.id, "metadata": {"secondary": True}, } - for trigger in [ + for trigger in ( "locked", "unlocked", "unlocking", @@ -137,7 +137,7 @@ async def test_get_triggers_hidden_auxiliary( "jammed", "open", "opening", - ] + ) ] triggers = await async_get_device_automations( hass, DeviceAutomationType.TRIGGER, device_entry.id diff --git a/tests/components/remote/test_device_action.py b/tests/components/remote/test_device_action.py index e228810149c..a6e890937b5 100644 --- a/tests/components/remote/test_device_action.py +++ b/tests/components/remote/test_device_action.py @@ -53,7 +53,7 @@ async def test_get_actions( "entity_id": entity_entry.id, "metadata": {"secondary": False}, } - for action in ["turn_off", "turn_on", "toggle"] + for action in ("turn_off", "turn_on", "toggle") ] actions = await async_get_device_automations( hass, DeviceAutomationType.ACTION, device_entry.id @@ -101,7 +101,7 @@ async def test_get_actions_hidden_auxiliary( "entity_id": entity_entry.id, "metadata": {"secondary": True}, } - for action in ["turn_off", "turn_on", "toggle"] + for action in ("turn_off", "turn_on", "toggle") ] actions = await async_get_device_automations( hass, DeviceAutomationType.ACTION, device_entry.id diff --git a/tests/components/remote/test_device_condition.py b/tests/components/remote/test_device_condition.py index e0c5f6d862b..d13a0480355 100644 --- a/tests/components/remote/test_device_condition.py +++ b/tests/components/remote/test_device_condition.py @@ -59,7 +59,7 @@ async def test_get_conditions( "entity_id": entity_entry.id, "metadata": {"secondary": False}, } - for condition in ["is_off", "is_on"] + for condition in ("is_off", "is_on") ] conditions = await async_get_device_automations( hass, DeviceAutomationType.CONDITION, device_entry.id @@ -107,7 +107,7 @@ async def test_get_conditions_hidden_auxiliary( "entity_id": entity_entry.id, "metadata": {"secondary": True}, } - for condition in ["is_off", "is_on"] + for condition in ("is_off", "is_on") ] conditions = await async_get_device_automations( hass, DeviceAutomationType.CONDITION, device_entry.id diff --git a/tests/components/remote/test_device_trigger.py b/tests/components/remote/test_device_trigger.py index 7e8f91a91dc..8a1a0c318d7 100644 --- a/tests/components/remote/test_device_trigger.py +++ b/tests/components/remote/test_device_trigger.py @@ -59,7 +59,7 @@ async def test_get_triggers( "entity_id": entity_entry.id, "metadata": {"secondary": False}, } - for trigger in ["changed_states", "turned_off", "turned_on"] + for trigger in ("changed_states", "turned_off", "turned_on") ] triggers = await async_get_device_automations( hass, DeviceAutomationType.TRIGGER, device_entry.id @@ -107,7 +107,7 @@ async def test_get_triggers_hidden_auxiliary( "entity_id": entity_entry.id, "metadata": {"secondary": True}, } - for trigger in ["changed_states", "turned_off", "turned_on"] + for trigger in ("changed_states", "turned_off", "turned_on") ] triggers = await async_get_device_automations( hass, DeviceAutomationType.TRIGGER, device_entry.id diff --git a/tests/components/select/test_device_action.py b/tests/components/select/test_device_action.py index c83e2585d5b..0ffb860179d 100644 --- a/tests/components/select/test_device_action.py +++ b/tests/components/select/test_device_action.py @@ -47,13 +47,13 @@ async def test_get_actions( "entity_id": entity_entry.id, "metadata": {"secondary": False}, } - for action in [ + for action in ( "select_first", "select_last", "select_next", "select_option", "select_previous", - ] + ) ] actions = await async_get_device_automations( hass, DeviceAutomationType.ACTION, device_entry.id @@ -101,13 +101,13 @@ async def test_get_actions_hidden_auxiliary( "entity_id": entity_entry.id, "metadata": {"secondary": True}, } - for action in [ + for action in ( "select_first", "select_last", "select_next", "select_option", "select_previous", - ] + ) ] actions = await async_get_device_automations( hass, DeviceAutomationType.ACTION, device_entry.id diff --git a/tests/components/select/test_device_condition.py b/tests/components/select/test_device_condition.py index 526ad678c19..e60df688658 100644 --- a/tests/components/select/test_device_condition.py +++ b/tests/components/select/test_device_condition.py @@ -105,7 +105,7 @@ async def test_get_conditions_hidden_auxiliary( "entity_id": entity_entry.id, "metadata": {"secondary": True}, } - for condition in ["selected_option"] + for condition in ("selected_option",) ] conditions = await async_get_device_automations( hass, DeviceAutomationType.CONDITION, device_entry.id diff --git a/tests/components/select/test_device_trigger.py b/tests/components/select/test_device_trigger.py index 8370a060bcd..c7a55c56202 100644 --- a/tests/components/select/test_device_trigger.py +++ b/tests/components/select/test_device_trigger.py @@ -105,7 +105,7 @@ async def test_get_triggers_hidden_auxiliary( "entity_id": entity_entry.id, "metadata": {"secondary": True}, } - for trigger in ["current_option_changed"] + for trigger in ("current_option_changed",) ] triggers = await async_get_device_automations( hass, DeviceAutomationType.TRIGGER, device_entry.id diff --git a/tests/components/sensor/test_device_condition.py b/tests/components/sensor/test_device_condition.py index dc81ec696f8..5f0646db8db 100644 --- a/tests/components/sensor/test_device_condition.py +++ b/tests/components/sensor/test_device_condition.py @@ -171,7 +171,7 @@ async def test_get_conditions_hidden_auxiliary( "entity_id": entity_entry.id, "metadata": {"secondary": True}, } - for condition in ["is_value"] + for condition in ("is_value",) ] conditions = await async_get_device_automations( hass, DeviceAutomationType.CONDITION, device_entry.id diff --git a/tests/components/sensor/test_device_trigger.py b/tests/components/sensor/test_device_trigger.py index 922a83709f7..71c844e428a 100644 --- a/tests/components/sensor/test_device_trigger.py +++ b/tests/components/sensor/test_device_trigger.py @@ -173,7 +173,7 @@ async def test_get_triggers_hidden_auxiliary( "entity_id": entity_entry.id, "metadata": {"secondary": True}, } - for trigger in ["value"] + for trigger in ("value",) ] triggers = await async_get_device_automations( hass, DeviceAutomationType.TRIGGER, device_entry.id diff --git a/tests/components/sensor/test_recorder.py b/tests/components/sensor/test_recorder.py index 3762b3f083a..0abe5e56e44 100644 --- a/tests/components/sensor/test_recorder.py +++ b/tests/components/sensor/test_recorder.py @@ -2413,7 +2413,7 @@ async def test_list_statistic_ids( "unit_class": unit_class, }, ] - for stat_type in ["mean", "sum", "dogs"]: + for stat_type in ("mean", "sum", "dogs"): statistic_ids = await async_list_statistic_ids(hass, statistic_type=stat_type) if statistic_type == stat_type: assert statistic_ids == [ @@ -3887,12 +3887,12 @@ async def test_compile_statistics_hourly_daily_monthly_summary( start = zero end = zero + timedelta(minutes=5) for i in range(24): - for entity_id in [ + for entity_id in ( "sensor.test1", "sensor.test2", "sensor.test3", "sensor.test4", - ]: + ): expected_average = ( expected_averages[entity_id][i] if entity_id in expected_averages @@ -3936,12 +3936,12 @@ async def test_compile_statistics_hourly_daily_monthly_summary( start = zero end = zero + timedelta(hours=1) for i in range(2): - for entity_id in [ + for entity_id in ( "sensor.test1", "sensor.test2", "sensor.test3", "sensor.test4", - ]: + ): expected_average = ( mean(expected_averages[entity_id][i * 12 : (i + 1) * 12]) if entity_id in expected_averages @@ -3993,12 +3993,12 @@ async def test_compile_statistics_hourly_daily_monthly_summary( start = dt_util.parse_datetime("2021-08-31T06:00:00+00:00") end = start + timedelta(days=1) for i in range(2): - for entity_id in [ + for entity_id in ( "sensor.test1", "sensor.test2", "sensor.test3", "sensor.test4", - ]: + ): expected_average = ( mean(expected_averages[entity_id][i * 12 : (i + 1) * 12]) if entity_id in expected_averages @@ -4050,12 +4050,12 @@ async def test_compile_statistics_hourly_daily_monthly_summary( start = dt_util.parse_datetime("2021-08-01T06:00:00+00:00") end = dt_util.parse_datetime("2021-09-01T06:00:00+00:00") for i in range(2): - for entity_id in [ + for entity_id in ( "sensor.test1", "sensor.test2", "sensor.test3", "sensor.test4", - ]: + ): expected_average = ( mean(expected_averages[entity_id][i * 12 : (i + 1) * 12]) if entity_id in expected_averages diff --git a/tests/components/switch/test_device_action.py b/tests/components/switch/test_device_action.py index ed3ff6f55ac..0b41ce7992d 100644 --- a/tests/components/switch/test_device_action.py +++ b/tests/components/switch/test_device_action.py @@ -54,7 +54,7 @@ async def test_get_actions( "entity_id": entity_entry.id, "metadata": {"secondary": False}, } - for action in ["turn_off", "turn_on", "toggle"] + for action in ("turn_off", "turn_on", "toggle") ] actions = await async_get_device_automations( hass, DeviceAutomationType.ACTION, device_entry.id @@ -102,7 +102,7 @@ async def test_get_actions_hidden_auxiliary( "entity_id": entity_entry.id, "metadata": {"secondary": True}, } - for action in ["turn_off", "turn_on", "toggle"] + for action in ("turn_off", "turn_on", "toggle") ] actions = await async_get_device_automations( hass, DeviceAutomationType.ACTION, device_entry.id diff --git a/tests/components/switch/test_device_condition.py b/tests/components/switch/test_device_condition.py index 43a91b8628a..2ba2c6adb5c 100644 --- a/tests/components/switch/test_device_condition.py +++ b/tests/components/switch/test_device_condition.py @@ -59,7 +59,7 @@ async def test_get_conditions( "entity_id": entity_entry.id, "metadata": {"secondary": False}, } - for condition in ["is_off", "is_on"] + for condition in ("is_off", "is_on") ] conditions = await async_get_device_automations( hass, DeviceAutomationType.CONDITION, device_entry.id @@ -107,7 +107,7 @@ async def test_get_conditions_hidden_auxiliary( "entity_id": entity_entry.id, "metadata": {"secondary": True}, } - for condition in ["is_off", "is_on"] + for condition in ("is_off", "is_on") ] conditions = await async_get_device_automations( hass, DeviceAutomationType.CONDITION, device_entry.id diff --git a/tests/components/switch/test_device_trigger.py b/tests/components/switch/test_device_trigger.py index 96479ba1900..092b7a964bb 100644 --- a/tests/components/switch/test_device_trigger.py +++ b/tests/components/switch/test_device_trigger.py @@ -59,7 +59,7 @@ async def test_get_triggers( "entity_id": entity_entry.id, "metadata": {"secondary": False}, } - for trigger in ["changed_states", "turned_off", "turned_on"] + for trigger in ("changed_states", "turned_off", "turned_on") ] triggers = await async_get_device_automations( hass, DeviceAutomationType.TRIGGER, device_entry.id @@ -107,7 +107,7 @@ async def test_get_triggers_hidden_auxiliary( "entity_id": entity_entry.id, "metadata": {"secondary": True}, } - for trigger in ["changed_states", "turned_off", "turned_on"] + for trigger in ("changed_states", "turned_off", "turned_on") ] triggers = await async_get_device_automations( hass, DeviceAutomationType.TRIGGER, device_entry.id diff --git a/tests/components/vacuum/test_device_action.py b/tests/components/vacuum/test_device_action.py index fec2ca1bf12..08459e05571 100644 --- a/tests/components/vacuum/test_device_action.py +++ b/tests/components/vacuum/test_device_action.py @@ -47,7 +47,7 @@ async def test_get_actions( "entity_id": entity_entry.id, "metadata": {"secondary": False}, } - for action in ["clean", "dock"] + for action in ("clean", "dock") ] actions = await async_get_device_automations( hass, DeviceAutomationType.ACTION, device_entry.id @@ -95,7 +95,7 @@ async def test_get_actions_hidden_auxiliary( "entity_id": entity_entry.id, "metadata": {"secondary": True}, } - for action in ["clean", "dock"] + for action in ("clean", "dock") ] actions = await async_get_device_automations( hass, DeviceAutomationType.ACTION, device_entry.id diff --git a/tests/components/vacuum/test_device_condition.py b/tests/components/vacuum/test_device_condition.py index 1a5a5ed38e0..5cc222a1833 100644 --- a/tests/components/vacuum/test_device_condition.py +++ b/tests/components/vacuum/test_device_condition.py @@ -59,7 +59,7 @@ async def test_get_conditions( "entity_id": entity_entry.id, "metadata": {"secondary": False}, } - for condition in ["is_cleaning", "is_docked"] + for condition in ("is_cleaning", "is_docked") ] conditions = await async_get_device_automations( hass, DeviceAutomationType.CONDITION, device_entry.id @@ -107,7 +107,7 @@ async def test_get_conditions_hidden_auxiliary( "entity_id": entity_entry.id, "metadata": {"secondary": True}, } - for condition in ["is_cleaning", "is_docked"] + for condition in ("is_cleaning", "is_docked") ] conditions = await async_get_device_automations( hass, DeviceAutomationType.CONDITION, device_entry.id diff --git a/tests/components/vacuum/test_device_trigger.py b/tests/components/vacuum/test_device_trigger.py index 648059e3c8f..56e351a6446 100644 --- a/tests/components/vacuum/test_device_trigger.py +++ b/tests/components/vacuum/test_device_trigger.py @@ -59,7 +59,7 @@ async def test_get_triggers( "entity_id": entity_entry.id, "metadata": {"secondary": False}, } - for trigger in ["cleaning", "docked"] + for trigger in ("cleaning", "docked") ] triggers = await async_get_device_automations( hass, DeviceAutomationType.TRIGGER, device_entry.id @@ -107,7 +107,7 @@ async def test_get_triggers_hidden_auxiliary( "entity_id": entity_entry.id, "metadata": {"secondary": True}, } - for trigger in ["cleaning", "docked"] + for trigger in ("cleaning", "docked") ] triggers = await async_get_device_automations( hass, DeviceAutomationType.TRIGGER, device_entry.id diff --git a/tests/components/water_heater/common.py b/tests/components/water_heater/common.py index 9e47af4a19f..e0a8075f4cc 100644 --- a/tests/components/water_heater/common.py +++ b/tests/components/water_heater/common.py @@ -35,11 +35,11 @@ async def async_set_temperature( """Set new target temperature.""" kwargs = { key: value - for key, value in [ + for key, value in ( (ATTR_TEMPERATURE, temperature), (ATTR_ENTITY_ID, entity_id), (ATTR_OPERATION_MODE, operation_mode), - ] + ) if value is not None } _LOGGER.debug("set_temperature start data=%s", kwargs) diff --git a/tests/components/water_heater/test_device_action.py b/tests/components/water_heater/test_device_action.py index e08721d3e10..943aa3373a0 100644 --- a/tests/components/water_heater/test_device_action.py +++ b/tests/components/water_heater/test_device_action.py @@ -47,7 +47,7 @@ async def test_get_actions( "entity_id": entity_entry.id, "metadata": {"secondary": False}, } - for action in ["turn_on", "turn_off"] + for action in ("turn_on", "turn_off") ] actions = await async_get_device_automations( hass, DeviceAutomationType.ACTION, device_entry.id @@ -95,7 +95,7 @@ async def test_get_actions_hidden_auxiliary( "entity_id": entity_entry.id, "metadata": {"secondary": True}, } - for action in ["turn_on", "turn_off"] + for action in ("turn_on", "turn_off") ] actions = await async_get_device_automations( hass, DeviceAutomationType.ACTION, device_entry.id diff --git a/tests/helpers/test_config_validation.py b/tests/helpers/test_config_validation.py index f7c6a9bc99a..163a33db988 100644 --- a/tests/helpers/test_config_validation.py +++ b/tests/helpers/test_config_validation.py @@ -768,7 +768,7 @@ def test_date() -> None: """Test date validation.""" schema = vol.Schema(cv.date) - for value in ["Not a date", "23:42", "2016-11-23T18:59:08"]: + for value in ("Not a date", "23:42", "2016-11-23T18:59:08"): with pytest.raises(vol.Invalid): schema(value) @@ -780,7 +780,7 @@ def test_time() -> None: """Test date validation.""" schema = vol.Schema(cv.time) - for value in ["Not a time", "2016-11-23", "2016-11-23T18:59:08"]: + for value in ("Not a time", "2016-11-23", "2016-11-23T18:59:08"): with pytest.raises(vol.Invalid): schema(value) @@ -792,7 +792,7 @@ def test_time() -> None: def test_datetime() -> None: """Test date time validation.""" schema = vol.Schema(cv.datetime) - for value in [date.today(), "Wrong DateTime"]: + for value in (date.today(), "Wrong DateTime"): with pytest.raises(vol.MultipleInvalid): schema(value) @@ -1307,7 +1307,7 @@ def test_uuid4_hex(caplog: pytest.LogCaptureFixture) -> None: """Test uuid validation.""" schema = vol.Schema(cv.uuid4_hex) - for value in ["Not a hex string", "0", 0]: + for value in ("Not a hex string", "0", 0): with pytest.raises(vol.Invalid): schema(value) diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py index 986c3e5493e..56ddcd9a6c9 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -615,7 +615,7 @@ async def test_async_remove_with_platform_update_finishes(hass: HomeAssistant) - # Add, remove, and make sure no updates # cause the entity to reappear after removal and # that we can add another entity with the same entity_id - for entity in [entity1, entity2]: + for entity in (entity1, entity2): update_called = asyncio.Event() update_done = asyncio.Event() await component.async_add_entities([entity]) diff --git a/tests/test_config.py b/tests/test_config.py index 27ef8059fd8..9a44333e20c 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -192,13 +192,13 @@ async def mock_non_adr_0007_integration_with_docs(hass: HomeAssistant) -> None: async def mock_adr_0007_integrations(hass: HomeAssistant) -> list[Integration]: """Mock ADR-0007 compliant integrations.""" integrations = [] - for domain in [ + for domain in ( "adr_0007_1", "adr_0007_2", "adr_0007_3", "adr_0007_4", "adr_0007_5", - ]: + ): adr_0007_config_schema = vol.Schema( { domain: vol.Schema( @@ -225,13 +225,13 @@ async def mock_adr_0007_integrations_with_docs( ) -> list[Integration]: """Mock ADR-0007 compliant integrations.""" integrations = [] - for domain in [ + for domain in ( "adr_0007_1", "adr_0007_2", "adr_0007_3", "adr_0007_4", "adr_0007_5", - ]: + ): adr_0007_config_schema = vol.Schema( { domain: vol.Schema( @@ -293,10 +293,10 @@ async def mock_custom_validator_integrations(hass: HomeAssistant) -> list[Integr Mock(async_validate_config=gen_async_validate_config(domain)), ) - for domain, exception in [ + for domain, exception in ( ("custom_validator_bad_1", HomeAssistantError("broken")), ("custom_validator_bad_2", ValueError("broken")), - ]: + ): integrations.append(mock_integration(hass, MockModule(domain))) mock_platform( hass, @@ -352,10 +352,10 @@ async def mock_custom_validator_integrations_with_docs( Mock(async_validate_config=gen_async_validate_config(domain)), ) - for domain, exception in [ + for domain, exception in ( ("custom_validator_bad_1", HomeAssistantError("broken")), ("custom_validator_bad_2", ValueError("broken")), - ]: + ): integrations.append( mock_integration( hass, diff --git a/tests/test_core.py b/tests/test_core.py index 7787a9a3769..541affc729b 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -2230,7 +2230,7 @@ async def test_async_run_job_starts_coro_eagerly(hass: HomeAssistant) -> None: def test_valid_entity_id() -> None: """Test valid entity ID.""" - for invalid in [ + for invalid in ( "_light.kitchen", ".kitchen", ".light.kitchen", @@ -2243,10 +2243,10 @@ def test_valid_entity_id() -> None: "Light.kitchen", "light.Kitchen", "lightkitchen", - ]: + ): assert not ha.valid_entity_id(invalid), invalid - for valid in [ + for valid in ( "1.a", "1light.kitchen", "a.1", @@ -2255,13 +2255,13 @@ def test_valid_entity_id() -> None: "light.1kitchen", "light.kitchen", "light.something_yoo", - ]: + ): assert ha.valid_entity_id(valid), valid def test_valid_domain() -> None: """Test valid domain.""" - for invalid in [ + for invalid in ( "_light", ".kitchen", ".light.kitchen", @@ -2272,16 +2272,16 @@ def test_valid_domain() -> None: "light.kitchen_yo_", "light.kitchen.", "Light", - ]: + ): assert not ha.valid_domain(invalid), invalid - for valid in [ + for valid in ( "1", "1light", "a", "input_boolean", "light", - ]: + ): assert ha.valid_domain(valid), valid From 20817955afacf29047c50da8c166d3c477a5b7a6 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 12 Jun 2024 12:35:55 +0200 Subject: [PATCH 0561/1445] Fix bad-chained-comparison pylint warning in tests (#119477) --- tests/helpers/test_template.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index fd19ef019c2..6b75ff384b6 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -2812,7 +2812,7 @@ def test_version(hass: HomeAssistant) -> None: "{{ version('2099.9.9') < '2099.9.10' }}", hass, ).async_render() - assert filter_result == function_result is True + assert filter_result is function_result is True filter_result = template.Template( "{{ '2099.9.9' | version == '2099.9.9' }}", @@ -2822,7 +2822,7 @@ def test_version(hass: HomeAssistant) -> None: "{{ version('2099.9.9') == '2099.9.9' }}", hass, ).async_render() - assert filter_result == function_result is True + assert filter_result is function_result is True with pytest.raises(TemplateError): template.Template( From ade936e6d5088c4a4d809111417fb3c7080825d5 Mon Sep 17 00:00:00 2001 From: Erwin Douna Date: Wed, 12 Jun 2024 12:47:47 +0200 Subject: [PATCH 0562/1445] Revert Use integration fallback configuration for tado water heater fallback (#119466) --- homeassistant/components/tado/climate.py | 26 ++++++--- homeassistant/components/tado/helper.py | 31 ----------- homeassistant/components/tado/water_heater.py | 12 ++--- tests/components/tado/test_helper.py | 54 ------------------- 4 files changed, 25 insertions(+), 98 deletions(-) delete mode 100644 homeassistant/components/tado/helper.py delete mode 100644 tests/components/tado/test_helper.py diff --git a/homeassistant/components/tado/climate.py b/homeassistant/components/tado/climate.py index 487bc519a26..6d298a80e79 100644 --- a/homeassistant/components/tado/climate.py +++ b/homeassistant/components/tado/climate.py @@ -36,6 +36,8 @@ from .const import ( CONST_MODE_OFF, CONST_MODE_SMART_SCHEDULE, CONST_OVERLAY_MANUAL, + CONST_OVERLAY_TADO_DEFAULT, + CONST_OVERLAY_TADO_MODE, CONST_OVERLAY_TADO_OPTIONS, CONST_OVERLAY_TIMER, DATA, @@ -65,7 +67,6 @@ from .const import ( TYPE_HEATING, ) from .entity import TadoZoneEntity -from .helper import decide_overlay_mode _LOGGER = logging.getLogger(__name__) @@ -597,12 +598,23 @@ class TadoClimate(TadoZoneEntity, ClimateEntity): self._tado.reset_zone_overlay(self.zone_id) return - overlay_mode = decide_overlay_mode( - tado=self._tado, - duration=duration, - overlay_mode=overlay_mode, - zone_id=self.zone_id, - ) + # If user gave duration then overlay mode needs to be timer + if duration: + overlay_mode = CONST_OVERLAY_TIMER + # If no duration or timer set to fallback setting + if overlay_mode is None: + overlay_mode = ( + self._tado.fallback + if self._tado.fallback is not None + else CONST_OVERLAY_TADO_MODE + ) + # If default is Tado default then look it up + if overlay_mode == CONST_OVERLAY_TADO_DEFAULT: + overlay_mode = ( + self._tado_zone_data.default_overlay_termination_type + if self._tado_zone_data.default_overlay_termination_type is not None + else CONST_OVERLAY_TADO_MODE + ) # If we ended up with a timer but no duration, set a default duration if overlay_mode == CONST_OVERLAY_TIMER and duration is None: duration = ( diff --git a/homeassistant/components/tado/helper.py b/homeassistant/components/tado/helper.py deleted file mode 100644 index fee23aef64a..00000000000 --- a/homeassistant/components/tado/helper.py +++ /dev/null @@ -1,31 +0,0 @@ -"""Helper methods for Tado.""" - -from . import TadoConnector -from .const import ( - CONST_OVERLAY_TADO_DEFAULT, - CONST_OVERLAY_TADO_MODE, - CONST_OVERLAY_TIMER, -) - - -def decide_overlay_mode( - tado: TadoConnector, - duration: int | None, - zone_id: int, - overlay_mode: str | None = None, -) -> str: - """Return correct overlay mode based on the action and defaults.""" - # If user gave duration then overlay mode needs to be timer - if duration: - return CONST_OVERLAY_TIMER - # If no duration or timer set to fallback setting - if overlay_mode is None: - overlay_mode = tado.fallback or CONST_OVERLAY_TADO_MODE - # If default is Tado default then look it up - if overlay_mode == CONST_OVERLAY_TADO_DEFAULT: - overlay_mode = ( - tado.data["zone"][zone_id].default_overlay_termination_type - or CONST_OVERLAY_TADO_MODE - ) - - return overlay_mode diff --git a/homeassistant/components/tado/water_heater.py b/homeassistant/components/tado/water_heater.py index 9b449dd43cc..f1257f097eb 100644 --- a/homeassistant/components/tado/water_heater.py +++ b/homeassistant/components/tado/water_heater.py @@ -32,7 +32,6 @@ from .const import ( TYPE_HOT_WATER, ) from .entity import TadoZoneEntity -from .helper import decide_overlay_mode _LOGGER = logging.getLogger(__name__) @@ -278,11 +277,12 @@ class TadoWaterHeater(TadoZoneEntity, WaterHeaterEntity): self._tado.set_zone_off(self.zone_id, CONST_OVERLAY_MANUAL, TYPE_HOT_WATER) return - overlay_mode = decide_overlay_mode( - tado=self._tado, - duration=duration, - zone_id=self.zone_id, - ) + overlay_mode = CONST_OVERLAY_MANUAL + if duration: + overlay_mode = CONST_OVERLAY_TIMER + elif self._tado.fallback: + # Fallback to Smart Schedule at next Schedule switch if we have fallback enabled + overlay_mode = CONST_OVERLAY_TADO_MODE _LOGGER.debug( "Switching to %s for zone %s (%d) with temperature %s", diff --git a/tests/components/tado/test_helper.py b/tests/components/tado/test_helper.py deleted file mode 100644 index ff85dfce944..00000000000 --- a/tests/components/tado/test_helper.py +++ /dev/null @@ -1,54 +0,0 @@ -"""Helper method tests.""" - -from unittest.mock import patch - -from homeassistant.components.tado import TadoConnector -from homeassistant.components.tado.const import ( - CONST_OVERLAY_MANUAL, - CONST_OVERLAY_TADO_DEFAULT, - CONST_OVERLAY_TADO_MODE, - CONST_OVERLAY_TIMER, -) -from homeassistant.components.tado.helper import decide_overlay_mode -from homeassistant.core import HomeAssistant - - -def dummy_tado_connector(hass: HomeAssistant, fallback) -> TadoConnector: - """Return dummy tado connector.""" - return TadoConnector(hass, username="dummy", password="dummy", fallback=fallback) - - -async def test_overlay_mode_duration_set(hass: HomeAssistant) -> None: - """Test overlay method selection when duration is set.""" - tado = dummy_tado_connector(hass=hass, fallback=CONST_OVERLAY_TADO_MODE) - overlay_mode = decide_overlay_mode(tado=tado, duration="01:00:00", zone_id=1) - # Must select TIMER overlay - assert overlay_mode == CONST_OVERLAY_TIMER - - -async def test_overlay_mode_next_time_block_fallback(hass: HomeAssistant) -> None: - """Test overlay method selection when duration is not set.""" - integration_fallback = CONST_OVERLAY_TADO_MODE - tado = dummy_tado_connector(hass=hass, fallback=integration_fallback) - overlay_mode = decide_overlay_mode(tado=tado, duration=None, zone_id=1) - # Must fallback to integration wide setting - assert overlay_mode == integration_fallback - - -async def test_overlay_mode_tado_default_fallback(hass: HomeAssistant) -> None: - """Test overlay method selection when tado default is selected.""" - integration_fallback = CONST_OVERLAY_TADO_DEFAULT - zone_fallback = CONST_OVERLAY_MANUAL - tado = dummy_tado_connector(hass=hass, fallback=integration_fallback) - - class MockZoneData: - def __init__(self) -> None: - self.default_overlay_termination_type = zone_fallback - - zone_id = 1 - - zone_data = {"zone": {zone_id: MockZoneData()}} - with patch.dict(tado.data, zone_data): - overlay_mode = decide_overlay_mode(tado=tado, duration=None, zone_id=zone_id) - # Must fallback to zone setting - assert overlay_mode == zone_fallback From 35b13e355b8d4e21e2f83184582ff892db66234f Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Wed, 12 Jun 2024 06:48:55 -0400 Subject: [PATCH 0563/1445] Store runtime data inside the config entry in Google Sheets (#119438) --- .../components/google_sheets/__init__.py | 29 ++++++++++--------- .../components/google_sheets/config_flow.py | 5 ++-- tests/components/google_sheets/test_init.py | 2 +- 3 files changed, 19 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/google_sheets/__init__.py b/homeassistant/components/google_sheets/__init__.py index 713a801257d..fc104cc5c22 100644 --- a/homeassistant/components/google_sheets/__init__.py +++ b/homeassistant/components/google_sheets/__init__.py @@ -29,6 +29,8 @@ from homeassistant.helpers.selector import ConfigEntrySelector from .const import DEFAULT_ACCESS, DOMAIN +type GoogleSheetsConfigEntry = ConfigEntry[OAuth2Session] + DATA = "data" DATA_CONFIG_ENTRY = "config_entry" WORKSHEET = "worksheet" @@ -44,7 +46,9 @@ SHEET_SERVICE_SCHEMA = vol.All( ) -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_setup_entry( + hass: HomeAssistant, entry: GoogleSheetsConfigEntry +) -> bool: """Set up Google Sheets from a config entry.""" implementation = await async_get_config_entry_implementation(hass, entry) session = OAuth2Session(hass, entry, implementation) @@ -61,21 +65,22 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if not async_entry_has_scopes(hass, entry): raise ConfigEntryAuthFailed("Required scopes are not present, reauth required") - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = session + entry.runtime_data = session await async_setup_service(hass) return True -def async_entry_has_scopes(hass: HomeAssistant, entry: ConfigEntry) -> bool: +def async_entry_has_scopes(hass: HomeAssistant, entry: GoogleSheetsConfigEntry) -> bool: """Verify that the config entry desired scope is present in the oauth token.""" return DEFAULT_ACCESS in entry.data.get(CONF_TOKEN, {}).get("scope", "").split(" ") -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_unload_entry( + hass: HomeAssistant, entry: GoogleSheetsConfigEntry +) -> bool: """Unload a config entry.""" - hass.data[DOMAIN].pop(entry.entry_id) loaded_entries = [ entry for entry in hass.config_entries.async_entries(DOMAIN) @@ -91,11 +96,9 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_setup_service(hass: HomeAssistant) -> None: """Add the services for Google Sheets.""" - def _append_to_sheet(call: ServiceCall, entry: ConfigEntry) -> None: + def _append_to_sheet(call: ServiceCall, entry: GoogleSheetsConfigEntry) -> None: """Run append in the executor.""" - service = Client( - Credentials(entry.data[CONF_TOKEN][CONF_ACCESS_TOKEN]) # type: ignore[no-untyped-call] - ) + service = Client(Credentials(entry.data[CONF_TOKEN][CONF_ACCESS_TOKEN])) # type: ignore[no-untyped-call] try: sheet = service.open_by_key(entry.unique_id) except RefreshError: @@ -117,14 +120,12 @@ async def async_setup_service(hass: HomeAssistant) -> None: async def append_to_sheet(call: ServiceCall) -> None: """Append new line of data to a Google Sheets document.""" - entry: ConfigEntry | None = hass.config_entries.async_get_entry( + entry: GoogleSheetsConfigEntry | None = hass.config_entries.async_get_entry( call.data[DATA_CONFIG_ENTRY] ) - if not entry: + if not entry or not hasattr(entry, "runtime_data"): raise ValueError(f"Invalid config entry: {call.data[DATA_CONFIG_ENTRY]}") - if not (session := hass.data[DOMAIN].get(entry.entry_id)): - raise ValueError(f"Config entry not loaded: {call.data[DATA_CONFIG_ENTRY]}") - await session.async_ensure_token_valid() + await entry.runtime_data.async_ensure_token_valid() await hass.async_add_executor_job(_append_to_sheet, call, entry) hass.services.async_register( diff --git a/homeassistant/components/google_sheets/config_flow.py b/homeassistant/components/google_sheets/config_flow.py index ab0c084c317..4008d42f52d 100644 --- a/homeassistant/components/google_sheets/config_flow.py +++ b/homeassistant/components/google_sheets/config_flow.py @@ -9,10 +9,11 @@ from typing import Any from google.oauth2.credentials import Credentials from gspread import Client, GSpreadException -from homeassistant.config_entries import ConfigEntry, ConfigFlowResult +from homeassistant.config_entries import ConfigFlowResult from homeassistant.const import CONF_ACCESS_TOKEN, CONF_TOKEN from homeassistant.helpers import config_entry_oauth2_flow +from . import GoogleSheetsConfigEntry from .const import DEFAULT_ACCESS, DEFAULT_NAME, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -25,7 +26,7 @@ class OAuth2FlowHandler( DOMAIN = DOMAIN - reauth_entry: ConfigEntry | None = None + reauth_entry: GoogleSheetsConfigEntry | None = None @property def logger(self) -> logging.Logger: diff --git a/tests/components/google_sheets/test_init.py b/tests/components/google_sheets/test_init.py index 0842debc38d..014e89349e2 100644 --- a/tests/components/google_sheets/test_init.py +++ b/tests/components/google_sheets/test_init.py @@ -294,7 +294,7 @@ async def test_append_sheet_invalid_config_entry( await hass.async_block_till_done() assert config_entry2.state is ConfigEntryState.NOT_LOADED - with pytest.raises(ValueError, match="Config entry not loaded"): + with pytest.raises(ValueError, match="Invalid config entry"): await hass.services.async_call( DOMAIN, "append_sheet", From e6b2a9b5c4b6b3d7ea55415e9204af7c33d6cbcf Mon Sep 17 00:00:00 2001 From: David Bonnes Date: Wed, 12 Jun 2024 12:45:03 +0100 Subject: [PATCH 0564/1445] Remove redundant logging from evohome (#119482) remove redundant logging --- homeassistant/components/evohome/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/evohome/__init__.py b/homeassistant/components/evohome/__init__.py index 782e4c4e674..13673caebb3 100644 --- a/homeassistant/components/evohome/__init__.py +++ b/homeassistant/components/evohome/__init__.py @@ -185,7 +185,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: } _config = { SZ_LOCATION_INFO: loc_info, - GWS: [{SZ_GATEWAY_INFO: gwy_info, TCS: loc_config[GWS][0][TCS]}], + GWS: [{SZ_GATEWAY_INFO: gwy_info}], } _LOGGER.debug("Config = %s", _config) From 8ca0de1d204be09d3116ced549f46792b471064d Mon Sep 17 00:00:00 2001 From: AlCalzone Date: Wed, 12 Jun 2024 13:48:47 +0200 Subject: [PATCH 0565/1445] Forward Z-Wave JS `node found` event to frontend (#118866) * forward Z-Wave `node found` event to frontend * add tests --- homeassistant/components/zwave_js/api.py | 26 ++++++++++++++++ tests/components/zwave_js/test_api.py | 38 ++++++++++++++++++++++++ 2 files changed, 64 insertions(+) diff --git a/homeassistant/components/zwave_js/api.py b/homeassistant/components/zwave_js/api.py index 463e665fa86..fee828c9fd8 100644 --- a/homeassistant/components/zwave_js/api.py +++ b/homeassistant/components/zwave_js/api.py @@ -772,6 +772,18 @@ async def websocket_add_node( ) ) + @callback + def node_found(event: dict) -> None: + node = event["node"] + node_details = { + "node_id": node["nodeId"], + } + connection.send_message( + websocket_api.event_message( + msg[ID], {"event": "node found", "node": node_details} + ) + ) + @callback def node_added(event: dict) -> None: node = event["node"] @@ -815,6 +827,7 @@ async def websocket_add_node( controller.on("inclusion stopped", forward_event), controller.on("validate dsk and enter pin", forward_dsk), controller.on("grant security classes", forward_requested_grant), + controller.on("node found", node_found), controller.on("node added", node_added), async_dispatcher_connect( hass, EVENT_DEVICE_ADDED_TO_REGISTRY, device_registered @@ -1296,6 +1309,18 @@ async def websocket_replace_failed_node( ) ) + @callback + def node_found(event: dict) -> None: + node = event["node"] + node_details = { + "node_id": node["nodeId"], + } + connection.send_message( + websocket_api.event_message( + msg[ID], {"event": "node found", "node": node_details} + ) + ) + @callback def node_added(event: dict) -> None: node = event["node"] @@ -1352,6 +1377,7 @@ async def websocket_replace_failed_node( controller.on("validate dsk and enter pin", forward_dsk), controller.on("grant security classes", forward_requested_grant), controller.on("node removed", node_removed), + controller.on("node found", node_found), controller.on("node added", node_added), async_dispatcher_connect( hass, EVENT_DEVICE_ADDED_TO_REGISTRY, device_registered diff --git a/tests/components/zwave_js/test_api.py b/tests/components/zwave_js/test_api.py index 23501e18745..0437f9d9085 100644 --- a/tests/components/zwave_js/test_api.py +++ b/tests/components/zwave_js/test_api.py @@ -532,6 +532,25 @@ async def test_add_node( msg = await ws_client.receive_json() assert msg["event"]["event"] == "inclusion started" + event = Event( + type="node found", + data={ + "source": "controller", + "event": "node found", + "node": { + "nodeId": 67, + }, + }, + ) + client.driver.receive_event(event) + + msg = await ws_client.receive_json() + assert msg["event"]["event"] == "node found" + node_details = { + "node_id": 67, + } + assert msg["event"]["node"] == node_details + event = Event( type="grant security classes", data={ @@ -1811,6 +1830,25 @@ async def test_replace_failed_node( msg = await ws_client.receive_json() assert msg["event"]["event"] == "inclusion started" + event = Event( + type="node found", + data={ + "source": "controller", + "event": "node found", + "node": { + "nodeId": 67, + }, + }, + ) + client.driver.receive_event(event) + + msg = await ws_client.receive_json() + assert msg["event"]["event"] == "node found" + node_details = { + "node_id": 67, + } + assert msg["event"]["node"] == node_details + event = Event( type="grant security classes", data={ From 171707e8b781a503257e4af72926bfba92ab789a Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Wed, 12 Jun 2024 14:10:02 +0200 Subject: [PATCH 0566/1445] Translation support for device automation extra fields (#115892) * Translation support for device trigger extra fields * Move extra_fields translations to backend --- homeassistant/components/alarm_control_panel/strings.json | 4 ++++ homeassistant/components/binary_sensor/strings.json | 3 +++ homeassistant/components/climate/strings.json | 8 ++++++++ homeassistant/components/cover/strings.json | 6 ++++++ homeassistant/components/device_tracker/strings.json | 3 +++ homeassistant/components/humidifier/strings.json | 7 +++++++ homeassistant/components/light/strings.json | 4 ++++ homeassistant/components/lock/strings.json | 3 +++ homeassistant/components/media_player/strings.json | 3 +++ homeassistant/components/mobile_app/strings.json | 4 ++++ homeassistant/components/number/strings.json | 3 +++ homeassistant/components/select/strings.json | 7 +++++++ homeassistant/components/sensor/strings.json | 5 +++++ homeassistant/components/text/strings.json | 3 +++ homeassistant/components/vacuum/strings.json | 3 +++ homeassistant/strings.json | 8 ++++++++ script/hassfest/translations.py | 4 ++++ 17 files changed, 78 insertions(+) diff --git a/homeassistant/components/alarm_control_panel/strings.json b/homeassistant/components/alarm_control_panel/strings.json index deaab6d75ee..6dac4d069a1 100644 --- a/homeassistant/components/alarm_control_panel/strings.json +++ b/homeassistant/components/alarm_control_panel/strings.json @@ -17,6 +17,10 @@ "is_armed_night": "{entity_name} is armed night", "is_armed_vacation": "{entity_name} is armed vacation" }, + "extra_fields": { + "code": "Code", + "for": "[%key:common::device_automation::extra_fields::for%]" + }, "trigger_type": { "triggered": "{entity_name} triggered", "disarmed": "{entity_name} disarmed", diff --git a/homeassistant/components/binary_sensor/strings.json b/homeassistant/components/binary_sensor/strings.json index 29e40c8b336..162cf139a1d 100644 --- a/homeassistant/components/binary_sensor/strings.json +++ b/homeassistant/components/binary_sensor/strings.json @@ -55,6 +55,9 @@ "is_on": "[%key:common::device_automation::condition_type::is_on%]", "is_off": "[%key:common::device_automation::condition_type::is_off%]" }, + "extra_fields": { + "for": "[%key:common::device_automation::extra_fields::for%]" + }, "trigger_type": { "bat_low": "{entity_name} battery low", "not_bat_low": "{entity_name} battery normal", diff --git a/homeassistant/components/climate/strings.json b/homeassistant/components/climate/strings.json index c31d22ccbeb..2a7fea9136c 100644 --- a/homeassistant/components/climate/strings.json +++ b/homeassistant/components/climate/strings.json @@ -13,6 +13,14 @@ "action_type": { "set_hvac_mode": "Change HVAC mode on {entity_name}", "set_preset_mode": "Change preset on {entity_name}" + }, + "extra_fields": { + "above": "[%key:common::device_automation::extra_fields::above%]", + "below": "[%key:common::device_automation::extra_fields::below%]", + "for": "[%key:common::device_automation::extra_fields::for%]", + "to": "[%key:common::device_automation::extra_fields::to%]", + "preset_mode": "Preset mode", + "hvac_mode": "HVAC mode" } }, "entity_component": { diff --git a/homeassistant/components/cover/strings.json b/homeassistant/components/cover/strings.json index 979835fcfd2..0afef8a200f 100644 --- a/homeassistant/components/cover/strings.json +++ b/homeassistant/components/cover/strings.json @@ -18,6 +18,12 @@ "is_position": "Current {entity_name} position is", "is_tilt_position": "Current {entity_name} tilt position is" }, + "extra_fields": { + "above": "[%key:common::device_automation::extra_fields::above%]", + "below": "[%key:common::device_automation::extra_fields::below%]", + "for": "[%key:common::device_automation::extra_fields::for%]", + "position": "Position" + }, "trigger_type": { "opened": "{entity_name} opened", "closed": "{entity_name} closed", diff --git a/homeassistant/components/device_tracker/strings.json b/homeassistant/components/device_tracker/strings.json index 44c43219b82..d6e36d92300 100644 --- a/homeassistant/components/device_tracker/strings.json +++ b/homeassistant/components/device_tracker/strings.json @@ -5,6 +5,9 @@ "is_home": "{entity_name} is home", "is_not_home": "{entity_name} is not home" }, + "extra_fields": { + "zone": "[%key:common::device_automation::extra_fields::zone%]" + }, "trigger_type": { "enters": "{entity_name} enters a zone", "leaves": "{entity_name} leaves a zone" diff --git a/homeassistant/components/humidifier/strings.json b/homeassistant/components/humidifier/strings.json index cb59dd04bdd..0416f4a68a6 100644 --- a/homeassistant/components/humidifier/strings.json +++ b/homeassistant/components/humidifier/strings.json @@ -18,6 +18,13 @@ "toggle": "[%key:common::device_automation::action_type::toggle%]", "turn_on": "[%key:common::device_automation::action_type::turn_on%]", "turn_off": "[%key:common::device_automation::action_type::turn_off%]" + }, + "extra_fields": { + "above": "[%key:common::device_automation::extra_fields::above%]", + "below": "[%key:common::device_automation::extra_fields::below%]", + "for": "[%key:common::device_automation::extra_fields::for%]", + "mode": "Mode", + "humidity": "Humidity" } }, "entity_component": { diff --git a/homeassistant/components/light/strings.json b/homeassistant/components/light/strings.json index f17044d4d74..76156404991 100644 --- a/homeassistant/components/light/strings.json +++ b/homeassistant/components/light/strings.json @@ -53,6 +53,10 @@ "changed_states": "[%key:common::device_automation::trigger_type::changed_states%]", "turned_on": "[%key:common::device_automation::trigger_type::turned_on%]", "turned_off": "[%key:common::device_automation::trigger_type::turned_off%]" + }, + "extra_fields": { + "brightness_pct": "Brightness", + "flash": "Flash" } }, "entity_component": { diff --git a/homeassistant/components/lock/strings.json b/homeassistant/components/lock/strings.json index 3b36171bf94..fd8636acf97 100644 --- a/homeassistant/components/lock/strings.json +++ b/homeassistant/components/lock/strings.json @@ -15,6 +15,9 @@ "locked": "{entity_name} locked", "unlocked": "{entity_name} unlocked", "open": "{entity_name} opened" + }, + "extra_fields": { + "for": "[%key:common::device_automation::extra_fields::for%]" } }, "entity_component": { diff --git a/homeassistant/components/media_player/strings.json b/homeassistant/components/media_player/strings.json index bcf594a2675..ff246e420ce 100644 --- a/homeassistant/components/media_player/strings.json +++ b/homeassistant/components/media_player/strings.json @@ -17,6 +17,9 @@ "paused": "{entity_name} is paused", "playing": "{entity_name} starts playing", "changed_states": "[%key:common::device_automation::trigger_type::changed_states%]" + }, + "extra_fields": { + "for": "[%key:common::device_automation::extra_fields::for%]" } }, "entity_component": { diff --git a/homeassistant/components/mobile_app/strings.json b/homeassistant/components/mobile_app/strings.json index 9e388ebc76c..3d3e0767312 100644 --- a/homeassistant/components/mobile_app/strings.json +++ b/homeassistant/components/mobile_app/strings.json @@ -13,6 +13,10 @@ "device_automation": { "action_type": { "notify": "Send a notification" + }, + "extra_fields": { + "message": "Message", + "title": "Title" } } } diff --git a/homeassistant/components/number/strings.json b/homeassistant/components/number/strings.json index 502b2b4affd..d6932286469 100644 --- a/homeassistant/components/number/strings.json +++ b/homeassistant/components/number/strings.json @@ -3,6 +3,9 @@ "device_automation": { "action_type": { "set_value": "Set value for {entity_name}" + }, + "extra_fields": { + "value": "[%key:common::device_automation::extra_fields::value%]" } }, "entity_component": { diff --git a/homeassistant/components/select/strings.json b/homeassistant/components/select/strings.json index 9c9d1136b99..02c1765133a 100644 --- a/homeassistant/components/select/strings.json +++ b/homeassistant/components/select/strings.json @@ -13,6 +13,13 @@ }, "condition_type": { "selected_option": "Current {entity_name} selected option" + }, + "extra_fields": { + "for": "[%key:common::device_automation::extra_fields::for%]", + "to": "[%key:common::device_automation::extra_fields::to%]", + "cycle": "Cycle", + "from": "From", + "option": "Option" } }, "entity_component": { diff --git a/homeassistant/components/sensor/strings.json b/homeassistant/components/sensor/strings.json index fad1086c034..101b32f373f 100644 --- a/homeassistant/components/sensor/strings.json +++ b/homeassistant/components/sensor/strings.json @@ -98,6 +98,11 @@ "water": "{entity_name} water changes", "weight": "{entity_name} weight changes", "wind_speed": "{entity_name} wind speed changes" + }, + "extra_fields": { + "above": "[%key:common::device_automation::extra_fields::above%]", + "below": "[%key:common::device_automation::extra_fields::below%]", + "for": "[%key:common::device_automation::extra_fields::for%]" } }, "entity_component": { diff --git a/homeassistant/components/text/strings.json b/homeassistant/components/text/strings.json index 82cab559d0e..1389d5aa500 100644 --- a/homeassistant/components/text/strings.json +++ b/homeassistant/components/text/strings.json @@ -3,6 +3,9 @@ "device_automation": { "action_type": { "set_value": "Set value for {entity_name}" + }, + "extra_fields": { + "value": "[%key:common::device_automation::extra_fields::value%]" } }, "entity_component": { diff --git a/homeassistant/components/vacuum/strings.json b/homeassistant/components/vacuum/strings.json index 673c76b7f8d..1efaf87e748 100644 --- a/homeassistant/components/vacuum/strings.json +++ b/homeassistant/components/vacuum/strings.json @@ -12,6 +12,9 @@ "action_type": { "clean": "Let {entity_name} clean", "dock": "Let {entity_name} return to the dock" + }, + "extra_fields": { + "for": "[%key:common::device_automation::extra_fields::for%]" } }, "entity_component": { diff --git a/homeassistant/strings.json b/homeassistant/strings.json index b31e83394bb..fca55353aa0 100644 --- a/homeassistant/strings.json +++ b/homeassistant/strings.json @@ -9,6 +9,14 @@ "is_on": "{entity_name} is on", "is_off": "{entity_name} is off" }, + "extra_fields": { + "above": "Above", + "below": "Below", + "for": "Duration", + "to": "To", + "value": "Value", + "zone": "Zone" + }, "trigger_type": { "changed_states": "{entity_name} turned on or off", "turned_on": "{entity_name} turned on", diff --git a/script/hassfest/translations.py b/script/hassfest/translations.py index c508f4ee36e..04ea85ca5d5 100644 --- a/script/hassfest/translations.py +++ b/script/hassfest/translations.py @@ -284,6 +284,10 @@ def gen_strings_schema(config: Config, integration: Integration) -> vol.Schema: vol.Optional("condition_type"): {str: translation_value_validator}, vol.Optional("trigger_type"): {str: translation_value_validator}, vol.Optional("trigger_subtype"): {str: translation_value_validator}, + vol.Optional("extra_fields"): {str: translation_value_validator}, + vol.Optional("extra_fields_descriptions"): { + str: translation_value_validator + }, }, vol.Optional("system_health"): { vol.Optional("info"): cv.schema_with_slug_keys( From 3a4b46208f6b6d80cb867267763eef19b287e756 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 12 Jun 2024 14:49:03 +0200 Subject: [PATCH 0567/1445] Migrate AirGradient to runtime_data (#119491) * Migrate AirGradient to runtime_data * Migrate AirGradient to runtime_data --- .../components/airgradient/__init__.py | 26 +++++++++++++------ .../components/airgradient/coordinator.py | 9 +++++-- .../components/airgradient/select.py | 16 +++++------- .../components/airgradient/sensor.py | 11 ++++---- 4 files changed, 37 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/airgradient/__init__.py b/homeassistant/components/airgradient/__init__.py index da3edcf0453..91ee0a440a6 100644 --- a/homeassistant/components/airgradient/__init__.py +++ b/homeassistant/components/airgradient/__init__.py @@ -2,6 +2,8 @@ from __future__ import annotations +from dataclasses import dataclass + from airgradient import AirGradientClient from homeassistant.config_entries import ConfigEntry @@ -16,6 +18,17 @@ from .coordinator import AirGradientConfigCoordinator, AirGradientMeasurementCoo PLATFORMS: list[Platform] = [Platform.SELECT, Platform.SENSOR] +@dataclass +class AirGradientData: + """AirGradient data class.""" + + measurement: AirGradientMeasurementCoordinator + config: AirGradientConfigCoordinator + + +type AirGradientConfigEntry = ConfigEntry[AirGradientData] + + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Airgradient from a config entry.""" @@ -39,10 +52,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: sw_version=measurement_coordinator.data.firmware_version, ) - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = { - "measurement": measurement_coordinator, - "config": config_coordinator, - } + entry.runtime_data = AirGradientData( + measurement=measurement_coordinator, + config=config_coordinator, + ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) @@ -51,7 +64,4 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): - hass.data[DOMAIN].pop(entry.entry_id) - - return unload_ok + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/airgradient/coordinator.py b/homeassistant/components/airgradient/coordinator.py index 90aded9a4ba..fbc1505f9c3 100644 --- a/homeassistant/components/airgradient/coordinator.py +++ b/homeassistant/components/airgradient/coordinator.py @@ -1,21 +1,26 @@ """Define an object to manage fetching AirGradient data.""" +from __future__ import annotations + from datetime import timedelta +from typing import TYPE_CHECKING from airgradient import AirGradientClient, AirGradientError, Config, Measures -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import LOGGER +if TYPE_CHECKING: + from . import AirGradientConfigEntry + class AirGradientCoordinator[_DataT](DataUpdateCoordinator[_DataT]): """Class to manage fetching AirGradient data.""" _update_interval: timedelta - config_entry: ConfigEntry + config_entry: AirGradientConfigEntry def __init__(self, hass: HomeAssistant, client: AirGradientClient) -> None: """Initialize coordinator.""" diff --git a/homeassistant/components/airgradient/select.py b/homeassistant/components/airgradient/select.py index 7a82d3b8a46..7880e55de19 100644 --- a/homeassistant/components/airgradient/select.py +++ b/homeassistant/components/airgradient/select.py @@ -7,14 +7,14 @@ from airgradient import AirGradientClient, Config from airgradient.models import ConfigurationControl, TemperatureUnit from homeassistant.components.select import SelectEntity, SelectEntityDescription -from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant from homeassistant.exceptions import ServiceValidationError from homeassistant.helpers.entity_platform import AddEntitiesCallback +from . import AirGradientConfigEntry from .const import DOMAIN -from .coordinator import AirGradientConfigCoordinator, AirGradientMeasurementCoordinator +from .coordinator import AirGradientConfigCoordinator from .entity import AirGradientEntity @@ -56,16 +56,14 @@ PROTECTED_SELECT_TYPES: tuple[AirGradientSelectEntityDescription, ...] = ( async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, + entry: AirGradientConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up AirGradient select entities based on a config entry.""" - config_coordinator: AirGradientConfigCoordinator = hass.data[DOMAIN][ - entry.entry_id - ]["config"] - measurement_coordinator: AirGradientMeasurementCoordinator = hass.data[DOMAIN][ - entry.entry_id - ]["measurement"] + config_coordinator = entry.runtime_data.config + measurement_coordinator = entry.runtime_data.measurement entities = [AirGradientSelect(config_coordinator, CONFIG_CONTROL_ENTITY)] diff --git a/homeassistant/components/airgradient/sensor.py b/homeassistant/components/airgradient/sensor.py index f21f13b80ab..6123d4289f9 100644 --- a/homeassistant/components/airgradient/sensor.py +++ b/homeassistant/components/airgradient/sensor.py @@ -11,7 +11,6 @@ from homeassistant.components.sensor import ( SensorEntityDescription, SensorStateClass, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONCENTRATION_PARTS_PER_MILLION, @@ -24,7 +23,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType -from .const import DOMAIN +from . import AirGradientConfigEntry from .coordinator import AirGradientMeasurementCoordinator from .entity import AirGradientEntity @@ -127,13 +126,13 @@ SENSOR_TYPES: tuple[AirGradientSensorEntityDescription, ...] = ( async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, + entry: AirGradientConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up AirGradient sensor entities based on a config entry.""" - coordinator: AirGradientMeasurementCoordinator = hass.data[DOMAIN][entry.entry_id][ - "measurement" - ] + coordinator = entry.runtime_data.measurement listener: Callable[[], None] | None = None not_setup: set[AirGradientSensorEntityDescription] = set(SENSOR_TYPES) From 2ca580898d49a867b6622de1f7c5b21f0e254837 Mon Sep 17 00:00:00 2001 From: tronikos Date: Wed, 12 Jun 2024 05:50:34 -0700 Subject: [PATCH 0568/1445] Fix typo in Camera.turn_on (#119386) --- homeassistant/components/camera/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index f8e8e6bf22b..4d2ba00900f 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -698,11 +698,11 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): await self.hass.async_add_executor_job(self.turn_off) def turn_on(self) -> None: - """Turn off camera.""" + """Turn on camera.""" raise NotImplementedError async def async_turn_on(self) -> None: - """Turn off camera.""" + """Turn on camera.""" await self.hass.async_add_executor_job(self.turn_on) def enable_motion_detection(self) -> None: From e065c7096999f43977365db74406378c430d4105 Mon Sep 17 00:00:00 2001 From: Rami Mosleh Date: Wed, 12 Jun 2024 16:38:35 +0300 Subject: [PATCH 0569/1445] Store transmission coordinator in runtime_data (#119502) store transmission coordinator in runtime_data --- .../components/transmission/__init__.py | 27 ++++++++++++------- .../components/transmission/sensor.py | 8 +++--- .../components/transmission/switch.py | 11 +++----- tests/components/transmission/test_init.py | 1 - 4 files changed, 23 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/transmission/__init__.py b/homeassistant/components/transmission/__init__.py index 681b4438099..06f27a1e605 100644 --- a/homeassistant/components/transmission/__init__.py +++ b/homeassistant/components/transmission/__init__.py @@ -15,7 +15,7 @@ from transmission_rpc.error import ( ) import voluptuous as vol -from homeassistant.config_entries import ConfigEntry +from homeassistant.config_entries import ConfigEntry, ConfigEntryState from homeassistant.const import ( CONF_HOST, CONF_ID, @@ -102,8 +102,12 @@ SERVICE_STOP_TORRENT_SCHEMA = vol.All( ) ) +type TransmissionConfigEntry = ConfigEntry[TransmissionDataUpdateCoordinator] -async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: + +async def async_setup_entry( + hass: HomeAssistant, config_entry: TransmissionConfigEntry +) -> bool: """Set up the Transmission Component.""" @callback @@ -135,7 +139,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b await hass.async_add_executor_job(coordinator.init_torrent_list) await coordinator.async_config_entry_first_refresh() - hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = coordinator + config_entry.runtime_data = coordinator await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) @@ -204,13 +208,16 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> if unload_ok := await hass.config_entries.async_unload_platforms( config_entry, PLATFORMS ): - hass.data[DOMAIN].pop(config_entry.entry_id) - - if not hass.data[DOMAIN]: - hass.services.async_remove(DOMAIN, SERVICE_ADD_TORRENT) - hass.services.async_remove(DOMAIN, SERVICE_REMOVE_TORRENT) - hass.services.async_remove(DOMAIN, SERVICE_START_TORRENT) - hass.services.async_remove(DOMAIN, SERVICE_STOP_TORRENT) + loaded_entries = [ + entry + for entry in hass.config_entries.async_entries(DOMAIN) + if entry.state == ConfigEntryState.LOADED + ] + if len(loaded_entries) == 1: + hass.services.async_remove(DOMAIN, SERVICE_ADD_TORRENT) + hass.services.async_remove(DOMAIN, SERVICE_REMOVE_TORRENT) + hass.services.async_remove(DOMAIN, SERVICE_START_TORRENT) + hass.services.async_remove(DOMAIN, SERVICE_STOP_TORRENT) return unload_ok diff --git a/homeassistant/components/transmission/sensor.py b/homeassistant/components/transmission/sensor.py index 9ee42045aab..737520adb5f 100644 --- a/homeassistant/components/transmission/sensor.py +++ b/homeassistant/components/transmission/sensor.py @@ -14,7 +14,6 @@ from homeassistant.components.sensor import ( SensorEntity, SensorEntityDescription, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_IDLE, UnitOfDataRate from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo @@ -22,6 +21,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity +from . import TransmissionConfigEntry from .const import ( DOMAIN, STATE_ATTR_TORRENT_INFO, @@ -134,14 +134,12 @@ SENSOR_TYPES: tuple[TransmissionSensorEntityDescription, ...] = ( async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: TransmissionConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Transmission sensors.""" - coordinator: TransmissionDataUpdateCoordinator = hass.data[DOMAIN][ - config_entry.entry_id - ] + coordinator = config_entry.runtime_data async_add_entities( TransmissionSensor(coordinator, description) for description in SENSOR_TYPES diff --git a/homeassistant/components/transmission/switch.py b/homeassistant/components/transmission/switch.py index 8e79d8246e0..d88f794cb10 100644 --- a/homeassistant/components/transmission/switch.py +++ b/homeassistant/components/transmission/switch.py @@ -2,21 +2,18 @@ from collections.abc import Callable from dataclasses import dataclass -import logging from typing import Any from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity +from . import TransmissionConfigEntry from .const import DOMAIN from .coordinator import TransmissionDataUpdateCoordinator -_LOGGING = logging.getLogger(__name__) - @dataclass(frozen=True, kw_only=True) class TransmissionSwitchEntityDescription(SwitchEntityDescription): @@ -47,14 +44,12 @@ SWITCH_TYPES: tuple[TransmissionSwitchEntityDescription, ...] = ( async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: TransmissionConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Transmission switch.""" - coordinator: TransmissionDataUpdateCoordinator = hass.data[DOMAIN][ - config_entry.entry_id - ] + coordinator = config_entry.runtime_data async_add_entities( TransmissionSwitch(coordinator, description) for description in SWITCH_TYPES diff --git a/tests/components/transmission/test_init.py b/tests/components/transmission/test_init.py index 307576ffdea..38d941c3779 100644 --- a/tests/components/transmission/test_init.py +++ b/tests/components/transmission/test_init.py @@ -119,7 +119,6 @@ async def test_unload_entry(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert entry.state is ConfigEntryState.NOT_LOADED - assert not hass.data[DOMAIN] @pytest.mark.parametrize( From fb1b0058eee8d4a83daaa81b5e7d925a1cdf7c08 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 12 Jun 2024 15:50:27 +0200 Subject: [PATCH 0570/1445] Fix consider-using-tuple pylint warnings in component tests (#119464) * Fix consider-using-tuple pylint warnings in component tests * Apply su Co-authored-by: Michael <35783820+mib1185@users.noreply.github.com> --------- Co-authored-by: Michael <35783820+mib1185@users.noreply.github.com> --- tests/components/alexa/test_capabilities.py | 6 ++-- tests/components/alexa/test_entities.py | 6 ++-- tests/components/dlna_dmr/test_config_flow.py | 4 +-- .../components/dlna_dmr/test_media_player.py | 18 ++++++------ tests/components/ecobee/test_climate.py | 13 ++------- tests/components/filter/test_sensor.py | 6 ++-- tests/components/google/test_calendar.py | 4 +-- tests/components/hddtemp/test_sensor.py | 4 +-- .../test_silabs_multiprotocol_addon.py | 4 +-- tests/components/hue/test_light_v2.py | 4 +-- .../husqvarna_automower/test_binary_sensor.py | 4 +-- .../husqvarna_automower/test_lawn_mower.py | 4 +-- .../husqvarna_automower/test_select.py | 4 +-- .../husqvarna_automower/test_sensor.py | 4 +-- .../husqvarna_automower/test_switch.py | 4 +-- tests/components/iaqualink/test_init.py | 2 +- tests/components/influxdb/test_sensor.py | 4 +-- tests/components/insteon/mock_devices.py | 4 +-- tests/components/insteon/test_api_aldb.py | 4 +-- .../components/insteon/test_api_properties.py | 2 +- tests/components/integration/test_sensor.py | 6 ++-- tests/components/knx/test_interface_device.py | 16 +++++------ tests/components/kodi/test_device_trigger.py | 2 +- tests/components/lcn/test_device_trigger.py | 4 +-- tests/components/local_calendar/conftest.py | 2 +- .../lutron_caseta/test_device_trigger.py | 4 +-- tests/components/mailbox/test_init.py | 2 +- .../media_player/test_device_condition.py | 8 +++--- tests/components/microsoft/test_tts.py | 6 ++-- tests/components/modbus/test_binary_sensor.py | 2 +- tests/components/mqtt/test_humidifier.py | 4 +-- tests/components/mqtt/test_water_heater.py | 2 +- tests/components/number/test_device_action.py | 2 +- .../openai_conversation/test_conversation.py | 4 +-- tests/components/panel_iframe/test_init.py | 2 +- tests/components/plant/test_init.py | 2 +- tests/components/plex/conftest.py | 2 +- tests/components/plex/test_init.py | 2 +- tests/components/proximity/test_init.py | 2 +- tests/components/rest_command/test_init.py | 4 +-- .../components/rfxtrx/test_device_trigger.py | 4 +-- tests/components/ruuvitag_ble/test_sensor.py | 4 +-- tests/components/sensirion_ble/test_sensor.py | 4 +-- .../components/shelly/test_device_trigger.py | 8 +++--- .../template/test_alarm_control_panel.py | 8 +++--- tests/components/template/test_cover.py | 12 ++++---- tests/components/template/test_fan.py | 28 +++++++++---------- tests/components/template/test_weather.py | 24 ++++++++-------- tests/components/tessie/test_button.py | 4 +-- tests/components/tessie/test_cover.py | 4 +-- tests/components/text/test_device_action.py | 2 +- tests/components/trend/test_binary_sensor.py | 10 +++---- .../components/update/test_device_trigger.py | 4 +-- tests/components/vulcan/test_config_flow.py | 4 +-- tests/components/xiaomi_miio/test_vacuum.py | 6 ++-- tests/components/zha/test_cluster_handlers.py | 2 +- tests/components/zha/test_device_action.py | 8 +++--- tests/components/zha/test_fan.py | 6 ++-- 58 files changed, 158 insertions(+), 167 deletions(-) diff --git a/tests/components/alexa/test_capabilities.py b/tests/components/alexa/test_capabilities.py index 7efc851a9c5..15a4bd6d9a1 100644 --- a/tests/components/alexa/test_capabilities.py +++ b/tests/components/alexa/test_capabilities.py @@ -817,7 +817,7 @@ async def test_report_climate_state(hass: HomeAssistant) -> None: {"value": 34.0, "scale": "CELSIUS"}, ) - for off_modes in [HVACMode.OFF]: + for off_modes in (HVACMode.OFF,): hass.states.async_set( "climate.downstairs", off_modes, @@ -954,7 +954,7 @@ async def test_report_on_off_climate_state(hass: HomeAssistant) -> None: {"value": 34.0, "scale": "CELSIUS"}, ) - for off_modes in [HVACMode.OFF]: + for off_modes in (HVACMode.OFF,): hass.states.async_set( "climate.onoff", off_modes, @@ -1002,7 +1002,7 @@ async def test_report_water_heater_state(hass: HomeAssistant) -> None: {"value": 34.0, "scale": "CELSIUS"}, ) - for off_mode in [STATE_OFF]: + for off_mode in (STATE_OFF,): hass.states.async_set( "water_heater.boyler", off_mode, diff --git a/tests/components/alexa/test_entities.py b/tests/components/alexa/test_entities.py index 9ec490c4f83..6998b2acc97 100644 --- a/tests/components/alexa/test_entities.py +++ b/tests/components/alexa/test_entities.py @@ -130,7 +130,7 @@ async def test_serialize_discovery_partly_fails( } assert all( entity in endpoint_ids - for entity in ["switch#bla", "fan#bla", "humidifier#bla", "sensor#bla"] + for entity in ("switch#bla", "fan#bla", "humidifier#bla", "sensor#bla") ) # Simulate fetching the interfaces fails for fan entity @@ -147,7 +147,7 @@ async def test_serialize_discovery_partly_fails( } assert all( entity in endpoint_ids - for entity in ["switch#bla", "humidifier#bla", "sensor#bla"] + for entity in ("switch#bla", "humidifier#bla", "sensor#bla") ) assert "Unable to serialize fan.bla for discovery" in caplog.text caplog.clear() @@ -166,7 +166,7 @@ async def test_serialize_discovery_partly_fails( } assert all( entity in endpoint_ids - for entity in ["switch#bla", "humidifier#bla", "fan#bla"] + for entity in ("switch#bla", "humidifier#bla", "fan#bla") ) assert "Unable to serialize sensor.bla for discovery" in caplog.text caplog.clear() diff --git a/tests/components/dlna_dmr/test_config_flow.py b/tests/components/dlna_dmr/test_config_flow.py index 55cf20859d3..765d65ff0b9 100644 --- a/tests/components/dlna_dmr/test_config_flow.py +++ b/tests/components/dlna_dmr/test_config_flow.py @@ -598,12 +598,12 @@ async def test_ssdp_ignore_device(hass: HomeAssistant) -> None: assert result["type"] is FlowResultType.ABORT assert result["reason"] == "alternative_integration" - for manufacturer, model in [ + for manufacturer, model in ( ("XBMC Foundation", "Kodi"), ("Samsung", "Smart TV"), ("LG Electronics.", "LG TV"), ("Royal Philips Electronics", "Philips TV DMR"), - ]: + ): discovery = dataclasses.replace(MOCK_DISCOVERY) discovery.upnp = dict(discovery.upnp) discovery.upnp[ssdp.ATTR_UPNP_MANUFACTURER] = manufacturer diff --git a/tests/components/dlna_dmr/test_media_player.py b/tests/components/dlna_dmr/test_media_player.py index 9a60ce244dc..d202994f988 100644 --- a/tests/components/dlna_dmr/test_media_player.py +++ b/tests/components/dlna_dmr/test_media_player.py @@ -458,7 +458,7 @@ async def test_available_device( assert device.name == "device_name" # Check entity state gets updated when device changes state - for dev_state, ent_state in [ + for dev_state, ent_state in ( (None, MediaPlayerState.ON), (TransportState.STOPPED, MediaPlayerState.IDLE), (TransportState.PLAYING, MediaPlayerState.PLAYING), @@ -468,7 +468,7 @@ async def test_available_device( (TransportState.RECORDING, MediaPlayerState.IDLE), (TransportState.NO_MEDIA_PRESENT, MediaPlayerState.IDLE), (TransportState.VENDOR_DEFINED, ha_const.STATE_UNKNOWN), - ]: + ): dmr_device_mock.profile_device.available = True dmr_device_mock.transport_state = dev_state await async_update_entity(hass, mock_entity_id) @@ -595,7 +595,7 @@ async def test_attributes( assert attrs[mp.ATTR_MEDIA_EPISODE] == "S1E23" # shuffle and repeat is based on device's play mode - for play_mode, shuffle, repeat in [ + for play_mode, shuffle, repeat in ( (PlayMode.NORMAL, False, RepeatMode.OFF), (PlayMode.SHUFFLE, True, RepeatMode.OFF), (PlayMode.REPEAT_ONE, False, RepeatMode.ONE), @@ -603,12 +603,12 @@ async def test_attributes( (PlayMode.RANDOM, True, RepeatMode.ALL), (PlayMode.DIRECT_1, False, RepeatMode.OFF), (PlayMode.INTRO, False, RepeatMode.OFF), - ]: + ): dmr_device_mock.play_mode = play_mode attrs = await get_attrs(hass, mock_entity_id) assert attrs[mp.ATTR_MEDIA_SHUFFLE] is shuffle assert attrs[mp.ATTR_MEDIA_REPEAT] == repeat - for bad_play_mode in [None, PlayMode.VENDOR_DEFINED]: + for bad_play_mode in (None, PlayMode.VENDOR_DEFINED): dmr_device_mock.play_mode = bad_play_mode attrs = await get_attrs(hass, mock_entity_id) assert mp.ATTR_MEDIA_SHUFFLE not in attrs @@ -944,7 +944,7 @@ async def test_shuffle_repeat_modes( """Test setting repeat and shuffle modes.""" # Test shuffle with all variations of existing play mode dmr_device_mock.valid_play_modes = {mode.value for mode in PlayMode} - for init_mode, shuffle_set, expect_mode in [ + for init_mode, shuffle_set, expect_mode in ( (PlayMode.NORMAL, False, PlayMode.NORMAL), (PlayMode.SHUFFLE, False, PlayMode.NORMAL), (PlayMode.REPEAT_ONE, False, PlayMode.REPEAT_ONE), @@ -955,7 +955,7 @@ async def test_shuffle_repeat_modes( (PlayMode.REPEAT_ONE, True, PlayMode.RANDOM), (PlayMode.REPEAT_ALL, True, PlayMode.RANDOM), (PlayMode.RANDOM, True, PlayMode.RANDOM), - ]: + ): dmr_device_mock.play_mode = init_mode await hass.services.async_call( mp.DOMAIN, @@ -966,7 +966,7 @@ async def test_shuffle_repeat_modes( dmr_device_mock.async_set_play_mode.assert_awaited_with(expect_mode) # Test repeat with all variations of existing play mode - for init_mode, repeat_set, expect_mode in [ + for init_mode, repeat_set, expect_mode in ( (PlayMode.NORMAL, RepeatMode.OFF, PlayMode.NORMAL), (PlayMode.SHUFFLE, RepeatMode.OFF, PlayMode.SHUFFLE), (PlayMode.REPEAT_ONE, RepeatMode.OFF, PlayMode.NORMAL), @@ -982,7 +982,7 @@ async def test_shuffle_repeat_modes( (PlayMode.REPEAT_ONE, RepeatMode.ALL, PlayMode.REPEAT_ALL), (PlayMode.REPEAT_ALL, RepeatMode.ALL, PlayMode.REPEAT_ALL), (PlayMode.RANDOM, RepeatMode.ALL, PlayMode.RANDOM), - ]: + ): dmr_device_mock.play_mode = init_mode await hass.services.async_call( mp.DOMAIN, diff --git a/tests/components/ecobee/test_climate.py b/tests/components/ecobee/test_climate.py index 35dd931d284..ae53132fe46 100644 --- a/tests/components/ecobee/test_climate.py +++ b/tests/components/ecobee/test_climate.py @@ -363,13 +363,10 @@ async def test_hold_preference(ecobee_fixture, thermostat) -> None: """Test hold preference.""" ecobee_fixture["settings"]["holdAction"] = "indefinite" assert thermostat.hold_preference() == "indefinite" - for action in ["useEndTime2hour", "useEndTime4hour"]: + for action in ("useEndTime2hour", "useEndTime4hour"): ecobee_fixture["settings"]["holdAction"] = action assert thermostat.hold_preference() == "holdHours" - for action in [ - "nextPeriod", - "askMe", - ]: + for action in ("nextPeriod", "askMe"): ecobee_fixture["settings"]["holdAction"] = action assert thermostat.hold_preference() == "nextTransition" @@ -380,11 +377,7 @@ def test_hold_hours(ecobee_fixture, thermostat) -> None: assert thermostat.hold_hours() == 2 ecobee_fixture["settings"]["holdAction"] = "useEndTime4hour" assert thermostat.hold_hours() == 4 - for action in [ - "nextPeriod", - "indefinite", - "askMe", - ]: + for action in ("nextPeriod", "indefinite", "askMe"): ecobee_fixture["settings"]["holdAction"] = action assert thermostat.hold_hours() is None diff --git a/tests/components/filter/test_sensor.py b/tests/components/filter/test_sensor.py index 67370bbcedc..0ece61708f2 100644 --- a/tests/components/filter/test_sensor.py +++ b/tests/components/filter/test_sensor.py @@ -390,7 +390,7 @@ def test_initial_outlier(values: list[State]) -> None: """Test issue #13363.""" filt = OutlierFilter(window_size=3, precision=2, entity=None, radius=4.0) out = State("sensor.test_monitored", "4000") - for state in [out, *values]: + for state in (out, *values): filtered = filt.filter_state(state) assert filtered.state == 21 @@ -399,7 +399,7 @@ def test_unknown_state_outlier(values: list[State]) -> None: """Test issue #32395.""" filt = OutlierFilter(window_size=3, precision=2, entity=None, radius=4.0) out = State("sensor.test_monitored", "unknown") - for state in [out, *values, out]: + for state in (out, *values, out): try: filtered = filt.filter_state(state) except ValueError: @@ -419,7 +419,7 @@ def test_lowpass(values: list[State]) -> None: """Test if lowpass filter works.""" filt = LowPassFilter(window_size=10, precision=2, entity=None, time_constant=10) out = State("sensor.test_monitored", "unknown") - for state in [out, *values, out]: + for state in (out, *values, out): try: filtered = filt.filter_state(state) except ValueError: diff --git a/tests/components/google/test_calendar.py b/tests/components/google/test_calendar.py index a5c65412c15..8e934925f46 100644 --- a/tests/components/google/test_calendar.py +++ b/tests/components/google/test_calendar.py @@ -485,7 +485,7 @@ async def test_http_api_event( assert response.status == HTTPStatus.OK events = await response.json() assert len(events) == 1 - assert {k: events[0].get(k) for k in ["summary", "start", "end"]} == { + assert {k: events[0].get(k) for k in ("summary", "start", "end")} == { "summary": TEST_EVENT["summary"], "start": {"dateTime": "2022-03-27T15:05:00+03:00"}, "end": {"dateTime": "2022-03-27T15:10:00+03:00"}, @@ -513,7 +513,7 @@ async def test_http_api_all_day_event( assert response.status == HTTPStatus.OK events = await response.json() assert len(events) == 1 - assert {k: events[0].get(k) for k in ["summary", "start", "end"]} == { + assert {k: events[0].get(k) for k in ("summary", "start", "end")} == { "summary": TEST_EVENT["summary"], "start": {"date": "2022-03-27"}, "end": {"date": "2022-03-28"}, diff --git a/tests/components/hddtemp/test_sensor.py b/tests/components/hddtemp/test_sensor.py index eac6d4c4053..f1851f959f0 100644 --- a/tests/components/hddtemp/test_sensor.py +++ b/tests/components/hddtemp/test_sensor.py @@ -158,11 +158,11 @@ async def test_hddtemp_multiple_disks(hass: HomeAssistant, telnetmock) -> None: assert await async_setup_component(hass, "sensor", VALID_CONFIG_MULTIPLE_DISKS) await hass.async_block_till_done() - for sensor in [ + for sensor in ( "sensor.hd_temperature_dev_sda1", "sensor.hd_temperature_dev_sdb1", "sensor.hd_temperature_dev_sdc1", - ]: + ): state = hass.states.get(sensor) reference = REFERENCE[state.attributes.get("device")] diff --git a/tests/components/homeassistant_hardware/test_silabs_multiprotocol_addon.py b/tests/components/homeassistant_hardware/test_silabs_multiprotocol_addon.py index 63c1ea5a9a4..267bded2970 100644 --- a/tests/components/homeassistant_hardware/test_silabs_multiprotocol_addon.py +++ b/tests/components/homeassistant_hardware/test_silabs_multiprotocol_addon.py @@ -584,7 +584,7 @@ async def test_option_flow_addon_installed_same_device_reconfigure_expected_user config_entry.add_to_hass(hass) mock_multiprotocol_platforms = {} - for domain in ["otbr", "zha"]: + for domain in ("otbr", "zha"): mock_multiprotocol_platform = MockMultiprotocolPlatform() mock_multiprotocol_platforms[domain] = mock_multiprotocol_platform mock_multiprotocol_platform.channel = configured_channel @@ -619,7 +619,7 @@ async def test_option_flow_addon_installed_same_device_reconfigure_expected_user result = await hass.config_entries.options.async_configure(result["flow_id"], {}) assert result["type"] is FlowResultType.CREATE_ENTRY - for domain in ["otbr", "zha"]: + for domain in ("otbr", "zha"): assert mock_multiprotocol_platforms[domain].change_channel_calls == [(14, 300)] assert multipan_manager._channel == 14 diff --git a/tests/components/hue/test_light_v2.py b/tests/components/hue/test_light_v2.py index d8d0f4b6e66..1f25649fdaa 100644 --- a/tests/components/hue/test_light_v2.py +++ b/tests/components/hue/test_light_v2.py @@ -455,11 +455,11 @@ async def test_grouped_lights( assert mock_bridge_v2.mock_requests[0]["json"]["dynamics"]["duration"] == 200 # Now generate update events by emitting the json we've sent as incoming events - for light_id in [ + for light_id in ( "02cba059-9c2c-4d45-97e4-4f79b1bfbaa1", "b3fe71ef-d0ef-48de-9355-d9e604377df0", "8015b17f-8336-415b-966a-b364bd082397", - ]: + ): event = { "id": light_id, "type": "light", diff --git a/tests/components/husqvarna_automower/test_binary_sensor.py b/tests/components/husqvarna_automower/test_binary_sensor.py index 29e626f99cb..fceaeee2321 100644 --- a/tests/components/husqvarna_automower/test_binary_sensor.py +++ b/tests/components/husqvarna_automower/test_binary_sensor.py @@ -45,11 +45,11 @@ async def test_binary_sensor_states( assert state is not None assert state.state == "off" - for activity, entity in [ + for activity, entity in ( (MowerActivities.CHARGING, "test_mower_1_charging"), (MowerActivities.LEAVING, "test_mower_1_leaving_dock"), (MowerActivities.GOING_HOME, "test_mower_1_returning_to_dock"), - ]: + ): values[TEST_MOWER_ID].mower.activity = activity mock_automower_client.get_status.return_value = values freezer.tick(SCAN_INTERVAL) diff --git a/tests/components/husqvarna_automower/test_lawn_mower.py b/tests/components/husqvarna_automower/test_lawn_mower.py index f01f4afd401..849339e4d96 100644 --- a/tests/components/husqvarna_automower/test_lawn_mower.py +++ b/tests/components/husqvarna_automower/test_lawn_mower.py @@ -38,11 +38,11 @@ async def test_lawn_mower_states( assert state is not None assert state.state == LawnMowerActivity.DOCKED - for activity, state, expected_state in [ + for activity, state, expected_state in ( ("UNKNOWN", "PAUSED", LawnMowerActivity.PAUSED), ("MOWING", "NOT_APPLICABLE", LawnMowerActivity.MOWING), ("NOT_APPLICABLE", "ERROR", LawnMowerActivity.ERROR), - ]: + ): values[TEST_MOWER_ID].mower.activity = activity values[TEST_MOWER_ID].mower.state = state mock_automower_client.get_status.return_value = values diff --git a/tests/components/husqvarna_automower/test_select.py b/tests/components/husqvarna_automower/test_select.py index fea2ca08742..2728bb5e672 100644 --- a/tests/components/husqvarna_automower/test_select.py +++ b/tests/components/husqvarna_automower/test_select.py @@ -38,14 +38,14 @@ async def test_select_states( assert state is not None assert state.state == "evening_only" - for state, expected_state in [ + for state, expected_state in ( ( HeadlightModes.ALWAYS_OFF, "always_off", ), (HeadlightModes.ALWAYS_ON, "always_on"), (HeadlightModes.EVENING_AND_NIGHT, "evening_and_night"), - ]: + ): values[TEST_MOWER_ID].settings.headlight.mode = state mock_automower_client.get_status.return_value = values freezer.tick(SCAN_INTERVAL) diff --git a/tests/components/husqvarna_automower/test_sensor.py b/tests/components/husqvarna_automower/test_sensor.py index 9eea901c93c..8f30a3dcb04 100644 --- a/tests/components/husqvarna_automower/test_sensor.py +++ b/tests/components/husqvarna_automower/test_sensor.py @@ -131,10 +131,10 @@ async def test_error_sensor( ) await setup_integration(hass, mock_config_entry) - for state, expected_state in [ + for state, expected_state in ( (None, "no_error"), ("can_error", "can_error"), - ]: + ): values[TEST_MOWER_ID].mower.error_key = state mock_automower_client.get_status.return_value = values freezer.tick(SCAN_INTERVAL) diff --git a/tests/components/husqvarna_automower/test_switch.py b/tests/components/husqvarna_automower/test_switch.py index a6e91e35544..de18f9081ea 100644 --- a/tests/components/husqvarna_automower/test_switch.py +++ b/tests/components/husqvarna_automower/test_switch.py @@ -41,10 +41,10 @@ async def test_switch_states( ) await setup_integration(hass, mock_config_entry) - for state, restricted_reson, expected_state in [ + for state, restricted_reson, expected_state in ( (MowerStates.RESTRICTED, RestrictedReasons.NOT_APPLICABLE, "off"), (MowerStates.IN_OPERATION, RestrictedReasons.NONE, "on"), - ]: + ): values[TEST_MOWER_ID].mower.state = state values[TEST_MOWER_ID].planner.restricted_reason = restricted_reson mock_automower_client.get_status.return_value = values diff --git a/tests/components/iaqualink/test_init.py b/tests/components/iaqualink/test_init.py index d450ced1fd7..8e157b8d1e3 100644 --- a/tests/components/iaqualink/test_init.py +++ b/tests/components/iaqualink/test_init.py @@ -346,7 +346,7 @@ async def test_entity_assumed_and_available( light = get_aqualink_device( system, name="aux_1", cls=IaquaLightSwitch, data={"state": "1"} ) - devices = {d.name: d for d in [light]} + devices = {light.name: light} system.get_devices = AsyncMock(return_value=devices) system.update = AsyncMock() diff --git a/tests/components/influxdb/test_sensor.py b/tests/components/influxdb/test_sensor.py index 08c92923bd3..48cae2a3ae6 100644 --- a/tests/components/influxdb/test_sensor.py +++ b/tests/components/influxdb/test_sensor.py @@ -111,7 +111,7 @@ def _make_v1_resultset(*args): def _make_v1_databases_resultset(): """Create a mock V1 'show databases' resultset.""" - for name in [DEFAULT_DATABASE, "db2"]: + for name in (DEFAULT_DATABASE, "db2"): yield {"name": name} @@ -129,7 +129,7 @@ def _make_v2_resultset(*args): def _make_v2_buckets_resultset(): """Create a mock V2 'buckets()' resultset.""" - records = [Record({"name": name}) for name in [DEFAULT_BUCKET, "bucket2"]] + records = [Record({"name": name}) for name in (DEFAULT_BUCKET, "bucket2")] return [Table(records)] diff --git a/tests/components/insteon/mock_devices.py b/tests/components/insteon/mock_devices.py index dea9fb4e34f..6b5f5cf5e09 100644 --- a/tests/components/insteon/mock_devices.py +++ b/tests/components/insteon/mock_devices.py @@ -85,7 +85,7 @@ class MockDevices: ) for device in [ - self._devices[addr] for addr in [addr1, addr2, addr3, addr4, addr5] + self._devices[addr] for addr in (addr1, addr2, addr3, addr4, addr5) ]: device.async_read_config = AsyncMock() device.aldb.async_write = AsyncMock() @@ -105,7 +105,7 @@ class MockDevices: ) for device in [ - self._devices[addr] for addr in [addr2, addr3, addr4, addr5] + self._devices[addr] for addr in (addr2, addr3, addr4, addr5) ]: device.async_status = AsyncMock() self._devices[addr1].async_status = AsyncMock(side_effect=AttributeError) diff --git a/tests/components/insteon/test_api_aldb.py b/tests/components/insteon/test_api_aldb.py index c919e7a9d22..4376628d9a4 100644 --- a/tests/components/insteon/test_api_aldb.py +++ b/tests/components/insteon/test_api_aldb.py @@ -303,7 +303,7 @@ async def test_bad_address( record = _aldb_dict(0) ws_id = 0 - for call in ["get", "write", "load", "reset", "add_default_links", "notify"]: + for call in ("get", "write", "load", "reset", "add_default_links", "notify"): ws_id += 1 await ws_client.send_json( { @@ -316,7 +316,7 @@ async def test_bad_address( assert not msg["success"] assert msg["error"]["message"] == INSTEON_DEVICE_NOT_FOUND - for call in ["change", "create"]: + for call in ("change", "create"): ws_id += 1 await ws_client.send_json( { diff --git a/tests/components/insteon/test_api_properties.py b/tests/components/insteon/test_api_properties.py index 74ef759006c..aee35cb8994 100644 --- a/tests/components/insteon/test_api_properties.py +++ b/tests/components/insteon/test_api_properties.py @@ -491,7 +491,7 @@ async def test_bad_address( ) ws_id = 0 - for call in ["get", "write", "load", "reset"]: + for call in ("get", "write", "load", "reset"): ws_id += 1 params = { ID: ws_id, diff --git a/tests/components/integration/test_sensor.py b/tests/components/integration/test_sensor.py index 3fc779423ac..5bc87717440 100644 --- a/tests/components/integration/test_sensor.py +++ b/tests/components/integration/test_sensor.py @@ -326,7 +326,7 @@ async def test_trapezoidal(hass: HomeAssistant) -> None: start_time = dt_util.utcnow() with freeze_time(start_time) as freezer: # Testing a power sensor with non-monotonic intervals and values - for time, value in [(20, 10), (30, 30), (40, 5), (50, 0)]: + for time, value in ((20, 10), (30, 30), (40, 5), (50, 0)): freezer.move_to(start_time + timedelta(minutes=time)) hass.states.async_set( entity_id, @@ -365,7 +365,7 @@ async def test_left(hass: HomeAssistant) -> None: await hass.async_block_till_done() # Testing a power sensor with non-monotonic intervals and values - for time, value in [(20, 10), (30, 30), (40, 5), (50, 0)]: + for time, value in ((20, 10), (30, 30), (40, 5), (50, 0)): now = dt_util.utcnow() + timedelta(minutes=time) with freeze_time(now): hass.states.async_set( @@ -405,7 +405,7 @@ async def test_right(hass: HomeAssistant) -> None: await hass.async_block_till_done() # Testing a power sensor with non-monotonic intervals and values - for time, value in [(20, 10), (30, 30), (40, 5), (50, 0)]: + for time, value in ((20, 10), (30, 30), (40, 5), (50, 0)): now = dt_util.utcnow() + timedelta(minutes=time) with freeze_time(now): hass.states.async_set( diff --git a/tests/components/knx/test_interface_device.py b/tests/components/knx/test_interface_device.py index c857022750c..6cf5d8026b9 100644 --- a/tests/components/knx/test_interface_device.py +++ b/tests/components/knx/test_interface_device.py @@ -22,7 +22,7 @@ async def test_diagnostic_entities( """Test diagnostic entities.""" await knx.setup_integration({}) - for entity_id in [ + for entity_id in ( "sensor.knx_interface_individual_address", "sensor.knx_interface_connection_established", "sensor.knx_interface_connection_type", @@ -31,14 +31,14 @@ async def test_diagnostic_entities( "sensor.knx_interface_outgoing_telegrams", "sensor.knx_interface_outgoing_telegram_errors", "sensor.knx_interface_telegrams", - ]: + ): entity = entity_registry.async_get(entity_id) assert entity.entity_category is EntityCategory.DIAGNOSTIC - for entity_id in [ + for entity_id in ( "sensor.knx_interface_incoming_telegrams", "sensor.knx_interface_outgoing_telegrams", - ]: + ): entity = entity_registry.async_get(entity_id) assert entity.disabled is True @@ -54,14 +54,14 @@ async def test_diagnostic_entities( assert len(events) == 3 # 5 polled sensors - 2 disabled events.clear() - for entity_id, test_state in [ + for entity_id, test_state in ( ("sensor.knx_interface_individual_address", "0.0.0"), ("sensor.knx_interface_connection_type", "Tunnel TCP"), # skipping connected_since timestamp ("sensor.knx_interface_incoming_telegram_errors", "1"), ("sensor.knx_interface_outgoing_telegram_errors", "2"), ("sensor.knx_interface_telegrams", "31"), - ]: + ): assert hass.states.get(entity_id).state == test_state await knx.xknx.connection_manager.connection_state_changed( @@ -85,14 +85,14 @@ async def test_diagnostic_entities( await hass.async_block_till_done() assert len(events) == 6 # all diagnostic sensors - counters are reset on connect - for entity_id, test_state in [ + for entity_id, test_state in ( ("sensor.knx_interface_individual_address", "1.1.1"), ("sensor.knx_interface_connection_type", "Tunnel UDP"), # skipping connected_since timestamp ("sensor.knx_interface_incoming_telegram_errors", "0"), ("sensor.knx_interface_outgoing_telegram_errors", "0"), ("sensor.knx_interface_telegrams", "0"), - ]: + ): assert hass.states.get(entity_id).state == test_state diff --git a/tests/components/kodi/test_device_trigger.py b/tests/components/kodi/test_device_trigger.py index d3ee4c7c301..d3de349018e 100644 --- a/tests/components/kodi/test_device_trigger.py +++ b/tests/components/kodi/test_device_trigger.py @@ -61,7 +61,7 @@ async def test_get_triggers( "entity_id": entity_entry.id, "metadata": {"secondary": False}, } - for trigger in ["turn_off", "turn_on"] + for trigger in ("turn_off", "turn_on") ] # Test triggers are either kodi specific triggers or media_player entity triggers diff --git a/tests/components/lcn/test_device_trigger.py b/tests/components/lcn/test_device_trigger.py index 7f26e528b7c..67bd7568254 100644 --- a/tests/components/lcn/test_device_trigger.py +++ b/tests/components/lcn/test_device_trigger.py @@ -34,13 +34,13 @@ async def test_get_triggers_module_device( CONF_DEVICE_ID: device.id, "metadata": {}, } - for trigger in [ + for trigger in ( "transmitter", "transponder", "fingerprint", "codelock", "send_keys", - ] + ) ] triggers = await async_get_device_automations( diff --git a/tests/components/local_calendar/conftest.py b/tests/components/local_calendar/conftest.py index 8d50036bbbe..6d2c38544a5 100644 --- a/tests/components/local_calendar/conftest.py +++ b/tests/components/local_calendar/conftest.py @@ -129,7 +129,7 @@ def event_fields(data: dict[str, str]) -> dict[str, str]: """Filter event API response to minimum fields.""" return { k: data[k] - for k in ["summary", "start", "end", "recurrence_id", "location"] + for k in ("summary", "start", "end", "recurrence_id", "location") if data.get(k) } diff --git a/tests/components/lutron_caseta/test_device_trigger.py b/tests/components/lutron_caseta/test_device_trigger.py index dc746be3ba6..208dd36cccd 100644 --- a/tests/components/lutron_caseta/test_device_trigger.py +++ b/tests/components/lutron_caseta/test_device_trigger.py @@ -148,7 +148,7 @@ async def test_get_triggers(hass: HomeAssistant) -> None: CONF_TYPE: "press", "metadata": {}, } - for subtype in ["on", "stop", "off", "raise", "lower"] + for subtype in ("on", "stop", "off", "raise", "lower") ] expected_triggers += [ { @@ -159,7 +159,7 @@ async def test_get_triggers(hass: HomeAssistant) -> None: CONF_TYPE: "release", "metadata": {}, } - for subtype in ["on", "stop", "off", "raise", "lower"] + for subtype in ("on", "stop", "off", "raise", "lower") ] triggers = await async_get_device_automations( diff --git a/tests/components/mailbox/test_init.py b/tests/components/mailbox/test_init.py index 296a4fbfa6b..31e831c3bae 100644 --- a/tests/components/mailbox/test_init.py +++ b/tests/components/mailbox/test_init.py @@ -164,7 +164,7 @@ async def test_delete_from_mailbox(mock_http_client: TestClient) -> None: msgsha1 = sha1(msgtxt1.encode("utf-8")).hexdigest() msgsha2 = sha1(msgtxt2.encode("utf-8")).hexdigest() - for msg in [msgsha1, msgsha2]: + for msg in (msgsha1, msgsha2): url = f"/api/mailbox/delete/TestMailbox/{msg}" req = await mock_http_client.delete(url) assert req.status == HTTPStatus.OK diff --git a/tests/components/media_player/test_device_condition.py b/tests/components/media_player/test_device_condition.py index 292d8e81db4..186cd674b39 100644 --- a/tests/components/media_player/test_device_condition.py +++ b/tests/components/media_player/test_device_condition.py @@ -62,14 +62,14 @@ async def test_get_conditions( "entity_id": entity_entry.id, "metadata": {"secondary": False}, } - for condition in [ + for condition in ( "is_buffering", "is_off", "is_on", "is_idle", "is_paused", "is_playing", - ] + ) ] conditions = await async_get_device_automations( hass, DeviceAutomationType.CONDITION, device_entry.id @@ -117,14 +117,14 @@ async def test_get_conditions_hidden_auxiliary( "entity_id": entity_entry.id, "metadata": {"secondary": True}, } - for condition in [ + for condition in ( "is_buffering", "is_off", "is_on", "is_idle", "is_paused", "is_playing", - ] + ) ] conditions = await async_get_device_automations( hass, DeviceAutomationType.CONDITION, device_entry.id diff --git a/tests/components/microsoft/test_tts.py b/tests/components/microsoft/test_tts.py index 94d77955f52..082def901c5 100644 --- a/tests/components/microsoft/test_tts.py +++ b/tests/components/microsoft/test_tts.py @@ -302,17 +302,17 @@ async def test_service_say_fa_ir_service( def test_supported_languages() -> None: """Test list of supported languages.""" - for lang in ["en-us", "fa-ir", "en-gb"]: + for lang in ("en-us", "fa-ir", "en-gb"): assert lang in SUPPORTED_LANGUAGES assert "en-US" not in SUPPORTED_LANGUAGES - for lang in [ + for lang in ( "en", "en-uk", "english", "english (united states)", "jennyneural", "en-us-jennyneural", - ]: + ): assert lang not in {s.lower() for s in SUPPORTED_LANGUAGES} assert len(SUPPORTED_LANGUAGES) > 100 diff --git a/tests/components/modbus/test_binary_sensor.py b/tests/components/modbus/test_binary_sensor.py index 7ae933998cf..6aae0e7feae 100644 --- a/tests/components/modbus/test_binary_sensor.py +++ b/tests/components/modbus/test_binary_sensor.py @@ -291,7 +291,7 @@ async def test_config_virtual_binary_sensor(hass: HomeAssistant, mock_modbus) -> """Run config test for binary sensor.""" assert SENSOR_DOMAIN in hass.config.components - for addon in ["", " 1", " 2", " 3"]: + for addon in ("", " 1", " 2", " 3"): entity_id = f"{SENSOR_DOMAIN}.{TEST_ENTITY_NAME}{addon}".replace(" ", "_") assert hass.states.get(entity_id) is not None diff --git a/tests/components/mqtt/test_humidifier.py b/tests/components/mqtt/test_humidifier.py index c29250bff82..4e8918d330e 100644 --- a/tests/components/mqtt/test_humidifier.py +++ b/tests/components/mqtt/test_humidifier.py @@ -106,7 +106,7 @@ async def async_set_mode( """Set mode for all or specified humidifier.""" data = { key: value - for key, value in [(ATTR_ENTITY_ID, entity_id), (ATTR_MODE, mode)] + for key, value in ((ATTR_ENTITY_ID, entity_id), (ATTR_MODE, mode)) if value is not None } @@ -119,7 +119,7 @@ async def async_set_humidity( """Set target humidity for all or specified humidifier.""" data = { key: value - for key, value in [(ATTR_ENTITY_ID, entity_id), (ATTR_HUMIDITY, humidity)] + for key, value in ((ATTR_ENTITY_ID, entity_id), (ATTR_HUMIDITY, humidity)) if value is not None } diff --git a/tests/components/mqtt/test_water_heater.py b/tests/components/mqtt/test_water_heater.py index 8cba3fb9f67..a80ab59657f 100644 --- a/tests/components/mqtt/test_water_heater.py +++ b/tests/components/mqtt/test_water_heater.py @@ -1286,7 +1286,7 @@ async def test_skipped_async_ha_write_state( }, ), ) - for value_template in ["value_template", "mode_state_template"] + for value_template in ("value_template", "mode_state_template") ], ids=["value_template", "mode_state_template"], ) diff --git a/tests/components/number/test_device_action.py b/tests/components/number/test_device_action.py index 92a7cefd467..ffebd62fcbf 100644 --- a/tests/components/number/test_device_action.py +++ b/tests/components/number/test_device_action.py @@ -100,7 +100,7 @@ async def test_get_actions_hidden_auxiliary( "entity_id": entity_entry.id, "metadata": {"secondary": True}, } - for action in ["set_value"] + for action in ("set_value",) ] actions = await async_get_device_automations( hass, DeviceAutomationType.ACTION, device_entry.id diff --git a/tests/components/openai_conversation/test_conversation.py b/tests/components/openai_conversation/test_conversation.py index 5ca54611c91..1008482847c 100644 --- a/tests/components/openai_conversation/test_conversation.py +++ b/tests/components/openai_conversation/test_conversation.py @@ -429,7 +429,7 @@ async def test_assist_api_tools_conversion( mock_init_component, ) -> None: """Test that we are able to convert actual tools from Assist API.""" - for component in [ + for component in ( "intent", "todo", "light", @@ -440,7 +440,7 @@ async def test_assist_api_tools_conversion( "vacuum", "cover", "weather", - ]: + ): assert await async_setup_component(hass, component, {}) agent_id = mock_config_entry_with_assist.entry_id diff --git a/tests/components/panel_iframe/test_init.py b/tests/components/panel_iframe/test_init.py index a585cd523ec..74e1b642df5 100644 --- a/tests/components/panel_iframe/test_init.py +++ b/tests/components/panel_iframe/test_init.py @@ -104,7 +104,7 @@ async def test_import_config( }, ] - for url_path in ["api", "ftp", "router", "weather"]: + for url_path in ("api", "ftp", "router", "weather"): await client.send_json_auto_id( {"type": "lovelace/config", "url_path": url_path} ) diff --git a/tests/components/plant/test_init.py b/tests/components/plant/test_init.py index 0f79ade2df5..8a728665ce2 100644 --- a/tests/components/plant/test_init.py +++ b/tests/components/plant/test_init.py @@ -153,7 +153,7 @@ async def test_load_from_db(recorder_mock: Recorder, hass: HomeAssistant) -> Non is enabled via plant.ENABLE_LOAD_HISTORY. """ plant_name = "wise_plant" - for value in [20, 30, 10]: + for value in (20, 30, 10): hass.states.async_set( BRIGHTNESS_ENTITY, value, {ATTR_UNIT_OF_MEASUREMENT: "Lux"} ) diff --git a/tests/components/plex/conftest.py b/tests/components/plex/conftest.py index 8c2b1434f17..40b61dfb17a 100644 --- a/tests/components/plex/conftest.py +++ b/tests/components/plex/conftest.py @@ -482,7 +482,7 @@ def mock_plex_calls( url = plex_server_url(entry) - for server in [url, PLEX_DIRECT_URL]: + for server in (url, PLEX_DIRECT_URL): requests_mock.get(server, text=plex_server_default) requests_mock.get(f"{server}/accounts", text=plex_server_accounts) diff --git a/tests/components/plex/test_init.py b/tests/components/plex/test_init.py index a14c65daa43..51a6a56ccdb 100644 --- a/tests/components/plex/test_init.py +++ b/tests/components/plex/test_init.py @@ -255,7 +255,7 @@ async def test_setup_when_certificate_changed( # Test with success new_url = PLEX_DIRECT_URL requests_mock.get("https://plex.tv/api/v2/resources", text=plextv_resources) - for resource_url in [new_url, "http://1.2.3.4:32400"]: + for resource_url in (new_url, "http://1.2.3.4:32400"): requests_mock.get(resource_url, text=plex_server_default) requests_mock.get(f"{new_url}/accounts", text=plex_server_accounts) requests_mock.get(f"{new_url}/library", text=empty_library) diff --git a/tests/components/proximity/test_init.py b/tests/components/proximity/test_init.py index 8fa9e4a1ce1..6c2b54cae29 100644 --- a/tests/components/proximity/test_init.py +++ b/tests/components/proximity/test_init.py @@ -483,7 +483,7 @@ async def test_device_trackers_in_zone(hass: HomeAssistant) -> None: state = hass.states.get("sensor.home_nearest_device") assert state.state == "test1, test2" - for device in ["test1", "test2"]: + for device in ("test1", "test2"): entity_base_name = f"sensor.home_{device}" state = hass.states.get(f"{entity_base_name}_distance") assert state.state == "0" diff --git a/tests/components/rest_command/test_init.py b/tests/components/rest_command/test_init.py index 4429fe4011e..97ef29dfaca 100644 --- a/tests/components/rest_command/test_init.py +++ b/tests/components/rest_command/test_init.py @@ -215,7 +215,7 @@ async def test_rest_command_headers( # provide post request data aioclient_mock.post(TEST_URL, content=b"success") - for test_service in [ + for test_service in ( "no_headers_test", "content_type_test", "headers_test", @@ -223,7 +223,7 @@ async def test_rest_command_headers( "headers_and_content_type_override_test", "headers_template_test", "headers_and_content_type_override_template_test", - ]: + ): await hass.services.async_call(DOMAIN, test_service, {}, blocking=True) await hass.async_block_till_done() diff --git a/tests/components/rfxtrx/test_device_trigger.py b/tests/components/rfxtrx/test_device_trigger.py index 629ff897eb7..38f7cccc072 100644 --- a/tests/components/rfxtrx/test_device_trigger.py +++ b/tests/components/rfxtrx/test_device_trigger.py @@ -65,7 +65,7 @@ async def setup_entry(hass, devices): EVENT_LIGHTING_1, [ {"type": "command", "subtype": subtype} - for subtype in [ + for subtype in ( "Off", "On", "Dim", @@ -74,7 +74,7 @@ async def setup_entry(hass, devices): "All/group On", "Chime", "Illegal command", - ] + ) ], ) ], diff --git a/tests/components/ruuvitag_ble/test_sensor.py b/tests/components/ruuvitag_ble/test_sensor.py index c33e0453c53..14826a692a6 100644 --- a/tests/components/ruuvitag_ble/test_sensor.py +++ b/tests/components/ruuvitag_ble/test_sensor.py @@ -32,12 +32,12 @@ async def test_sensors(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert len(hass.states.async_all()) >= 4 - for sensor, value, unit, state_class in [ + for sensor, value, unit, state_class in ( ("temperature", "7.2", "°C", "measurement"), ("humidity", "61.84", "%", "measurement"), ("pressure", "1013.54", "hPa", "measurement"), ("voltage", "2395", "mV", "measurement"), - ]: + ): state = hass.states.get(f"sensor.{CONFIGURED_PREFIX}_{sensor}") assert state is not None assert state.state == value diff --git a/tests/components/sensirion_ble/test_sensor.py b/tests/components/sensirion_ble/test_sensor.py index 35e13a4133c..10dcb91ed22 100644 --- a/tests/components/sensirion_ble/test_sensor.py +++ b/tests/components/sensirion_ble/test_sensor.py @@ -29,11 +29,11 @@ async def test_sensors(enable_bluetooth: None, hass: HomeAssistant) -> None: await hass.async_block_till_done() assert len(hass.states.async_all()) >= 3 - for sensor, value, unit, state_class in [ + for sensor, value, unit, state_class in ( ("carbon_dioxide", "724", "ppm", "measurement"), ("humidity", "27.8", "%", "measurement"), ("temperature", "20.1", "°C", "measurement"), - ]: + ): state = hass.states.get(f"sensor.{CONFIGURED_PREFIX}_{sensor}") assert state is not None assert state.state == value diff --git a/tests/components/shelly/test_device_trigger.py b/tests/components/shelly/test_device_trigger.py index fc860a4df46..d47cca17460 100644 --- a/tests/components/shelly/test_device_trigger.py +++ b/tests/components/shelly/test_device_trigger.py @@ -68,7 +68,7 @@ async def test_get_triggers_block_device( CONF_SUBTYPE: "button1", "metadata": {}, } - for type_ in ["single", "long"] + for type_ in ("single", "long") ] triggers = await async_get_device_automations( @@ -94,14 +94,14 @@ async def test_get_triggers_rpc_device( CONF_SUBTYPE: "button1", "metadata": {}, } - for trigger_type in [ + for trigger_type in ( "btn_down", "btn_up", "single_push", "double_push", "triple_push", "long_push", - ] + ) ] triggers = await async_get_device_automations( @@ -127,7 +127,7 @@ async def test_get_triggers_button( CONF_SUBTYPE: "button", "metadata": {}, } - for trigger_type in ["single", "double", "triple", "long"] + for trigger_type in ("single", "double", "triple", "long") ] triggers = await async_get_device_automations( diff --git a/tests/components/template/test_alarm_control_panel.py b/tests/components/template/test_alarm_control_panel.py index a24650c678c..6a2a95a64eb 100644 --- a/tests/components/template/test_alarm_control_panel.py +++ b/tests/components/template/test_alarm_control_panel.py @@ -103,7 +103,7 @@ TEMPLATE_ALARM_CONFIG = { async def test_template_state_text(hass: HomeAssistant, start_ha) -> None: """Test the state text of a template.""" - for set_state in [ + for set_state in ( STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_NIGHT, @@ -113,7 +113,7 @@ async def test_template_state_text(hass: HomeAssistant, start_ha) -> None: STATE_ALARM_DISARMED, STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED, - ]: + ): hass.states.async_set(PANEL_NAME, set_state) await hass.async_block_till_done() state = hass.states.get(TEMPLATE_NAME) @@ -144,7 +144,7 @@ async def test_optimistic_states(hass: HomeAssistant, start_ha) -> None: await hass.async_block_till_done() assert state.state == "unknown" - for service, set_state in [ + for service, set_state in ( ("alarm_arm_away", STATE_ALARM_ARMED_AWAY), ("alarm_arm_home", STATE_ALARM_ARMED_HOME), ("alarm_arm_night", STATE_ALARM_ARMED_NIGHT), @@ -152,7 +152,7 @@ async def test_optimistic_states(hass: HomeAssistant, start_ha) -> None: ("alarm_arm_custom_bypass", STATE_ALARM_ARMED_CUSTOM_BYPASS), ("alarm_disarm", STATE_ALARM_DISARMED), ("alarm_trigger", STATE_ALARM_TRIGGERED), - ]: + ): await hass.services.async_call( ALARM_DOMAIN, service, diff --git a/tests/components/template/test_cover.py b/tests/components/template/test_cover.py index 0b3c221113f..2674b9697ed 100644 --- a/tests/components/template/test_cover.py +++ b/tests/components/template/test_cover.py @@ -267,11 +267,11 @@ async def test_template_position( hass.states.async_set("cover.test", STATE_OPEN) attrs = {} - for set_state, pos, test_state in [ + for set_state, pos, test_state in ( (STATE_CLOSED, 42, STATE_OPEN), (STATE_OPEN, 0.0, STATE_CLOSED), (STATE_CLOSED, None, STATE_UNKNOWN), - ]: + ): attrs["position"] = pos hass.states.async_set("cover.test", set_state, attributes=attrs) await hass.async_block_till_done() @@ -704,12 +704,12 @@ async def test_set_position_optimistic( state = hass.states.get("cover.test_template_cover") assert state.attributes.get("current_position") == 42.0 - for service, test_state in [ + for service, test_state in ( (SERVICE_CLOSE_COVER, STATE_CLOSED), (SERVICE_OPEN_COVER, STATE_OPEN), (SERVICE_TOGGLE, STATE_CLOSED), (SERVICE_TOGGLE, STATE_OPEN), - ]: + ): await hass.services.async_call( DOMAIN, service, {ATTR_ENTITY_ID: ENTITY_COVER}, blocking=True ) @@ -753,12 +753,12 @@ async def test_set_tilt_position_optimistic( state = hass.states.get("cover.test_template_cover") assert state.attributes.get("current_tilt_position") == 42.0 - for service, pos in [ + for service, pos in ( (SERVICE_CLOSE_COVER_TILT, 0.0), (SERVICE_OPEN_COVER_TILT, 100.0), (SERVICE_TOGGLE_COVER_TILT, 0.0), (SERVICE_TOGGLE_COVER_TILT, 100.0), - ]: + ): await hass.services.async_call( DOMAIN, service, {ATTR_ENTITY_ID: ENTITY_COVER}, blocking=True ) diff --git a/tests/components/template/test_fan.py b/tests/components/template/test_fan.py index b3023c8db0b..82ad4ede91c 100644 --- a/tests/components/template/test_fan.py +++ b/tests/components/template/test_fan.py @@ -157,13 +157,13 @@ async def test_templates_with_entities(hass: HomeAssistant, start_ha) -> None: hass.states.async_set(_PERCENTAGE_INPUT_NUMBER, 66) hass.states.async_set(_OSC_INPUT, "True") - for set_state, set_value, value in [ + for set_state, set_value, value in ( (_DIRECTION_INPUT_SELECT, DIRECTION_FORWARD, 66), (_PERCENTAGE_INPUT_NUMBER, 33, 33), (_PERCENTAGE_INPUT_NUMBER, 66, 66), (_PERCENTAGE_INPUT_NUMBER, 100, 100), (_PERCENTAGE_INPUT_NUMBER, "dog", 0), - ]: + ): hass.states.async_set(set_state, set_value) await hass.async_block_till_done() _verify(hass, STATE_ON, value, True, DIRECTION_FORWARD, None) @@ -266,7 +266,7 @@ async def test_availability_template_with_entities( hass: HomeAssistant, start_ha ) -> None: """Test availability tempalates with values from other entities.""" - for state, test_assert in [(STATE_ON, True), (STATE_OFF, False)]: + for state, test_assert in ((STATE_ON, True), (STATE_OFF, False)): hass.states.async_set(_STATE_AVAILABILITY_BOOLEAN, state) await hass.async_block_till_done() assert (hass.states.get(_TEST_FAN).state != STATE_UNAVAILABLE) == test_assert @@ -426,7 +426,7 @@ async def test_set_osc(hass: HomeAssistant, calls: list[ServiceCall]) -> None: await common.async_turn_on(hass, _TEST_FAN) expected_calls += 1 - for state in [True, False]: + for state in (True, False): await common.async_oscillate(hass, _TEST_FAN, state) assert hass.states.get(_OSC_INPUT).state == str(state) _verify(hass, STATE_ON, 0, state, None, None) @@ -444,7 +444,7 @@ async def test_set_direction(hass: HomeAssistant, calls: list[ServiceCall]) -> N await common.async_turn_on(hass, _TEST_FAN) expected_calls += 1 - for cmd in [DIRECTION_FORWARD, DIRECTION_REVERSE]: + for cmd in (DIRECTION_FORWARD, DIRECTION_REVERSE): await common.async_set_direction(hass, _TEST_FAN, cmd) assert hass.states.get(_DIRECTION_INPUT_SELECT).state == cmd _verify(hass, STATE_ON, 0, None, cmd, None) @@ -462,7 +462,7 @@ async def test_set_invalid_direction( await _register_components(hass) await common.async_turn_on(hass, _TEST_FAN) - for cmd in [DIRECTION_FORWARD, "invalid"]: + for cmd in (DIRECTION_FORWARD, "invalid"): await common.async_set_direction(hass, _TEST_FAN, cmd) assert hass.states.get(_DIRECTION_INPUT_SELECT).state == DIRECTION_FORWARD _verify(hass, STATE_ON, 0, None, DIRECTION_FORWARD, None) @@ -475,11 +475,11 @@ async def test_preset_modes(hass: HomeAssistant, calls: list[ServiceCall]) -> No ) await common.async_turn_on(hass, _TEST_FAN) - for extra, state, expected_calls in [ + for extra, state, expected_calls in ( ("auto", "auto", 2), ("smart", "smart", 3), ("invalid", "smart", 3), - ]: + ): if extra != state: with pytest.raises(NotValidPresetModeError): await common.async_set_preset_mode(hass, _TEST_FAN, extra) @@ -502,11 +502,11 @@ async def test_set_percentage(hass: HomeAssistant, calls: list[ServiceCall]) -> await common.async_turn_on(hass, _TEST_FAN) expected_calls += 1 - for state, value in [ + for state, value in ( (STATE_ON, 100), (STATE_ON, 66), (STATE_ON, 0), - ]: + ): await common.async_set_percentage(hass, _TEST_FAN, value) assert int(float(hass.states.get(_PERCENTAGE_INPUT_NUMBER).state)) == value _verify(hass, state, value, None, None, None) @@ -528,13 +528,13 @@ async def test_increase_decrease_speed( await _register_components(hass, speed_count=3) await common.async_turn_on(hass, _TEST_FAN) - for func, extra, state, value in [ + for func, extra, state, value in ( (common.async_set_percentage, 100, STATE_ON, 100), (common.async_decrease_speed, None, STATE_ON, 66), (common.async_decrease_speed, None, STATE_ON, 33), (common.async_decrease_speed, None, STATE_ON, 0), (common.async_increase_speed, None, STATE_ON, 33), - ]: + ): await func(hass, _TEST_FAN, extra) assert int(float(hass.states.get(_PERCENTAGE_INPUT_NUMBER).state)) == value _verify(hass, state, value, None, None, None) @@ -658,13 +658,13 @@ async def test_increase_decrease_speed_default_speed_count( await _register_components(hass) await common.async_turn_on(hass, _TEST_FAN) - for func, extra, state, value in [ + for func, extra, state, value in ( (common.async_set_percentage, 100, STATE_ON, 100), (common.async_decrease_speed, None, STATE_ON, 99), (common.async_decrease_speed, None, STATE_ON, 98), (common.async_decrease_speed, 31, STATE_ON, 67), (common.async_decrease_speed, None, STATE_ON, 66), - ]: + ): await func(hass, _TEST_FAN, extra) assert int(float(hass.states.get(_PERCENTAGE_INPUT_NUMBER).state)) == value _verify(hass, state, value, None, None, None) diff --git a/tests/components/template/test_weather.py b/tests/components/template/test_weather.py index e457f2e263b..b365d5d2890 100644 --- a/tests/components/template/test_weather.py +++ b/tests/components/template/test_weather.py @@ -68,7 +68,7 @@ ATTR_FORECAST = "forecast" ) async def test_template_state_text(hass: HomeAssistant, start_ha) -> None: """Test the state text of a template.""" - for attr, v_attr, value in [ + for attr, v_attr, value in ( ( "sensor.attribution", ATTR_ATTRIBUTION, @@ -85,7 +85,7 @@ async def test_template_state_text(hass: HomeAssistant, start_ha) -> None: ("sensor.cloud_coverage", ATTR_WEATHER_CLOUD_COVERAGE, 75), ("sensor.dew_point", ATTR_WEATHER_DEW_POINT, 2.2), ("sensor.apparent_temperature", ATTR_WEATHER_APPARENT_TEMPERATURE, 25), - ]: + ): hass.states.async_set(attr, value) await hass.async_block_till_done() state = hass.states.get("weather.test") @@ -125,10 +125,10 @@ async def test_forecasts( hass: HomeAssistant, start_ha, snapshot: SnapshotAssertion, service: str ) -> None: """Test forecast service.""" - for attr, _v_attr, value in [ + for attr, _v_attr, value in ( ("sensor.temperature", ATTR_WEATHER_TEMPERATURE, 22.3), ("sensor.humidity", ATTR_WEATHER_HUMIDITY, 60), - ]: + ): hass.states.async_set(attr, value) await hass.async_block_till_done() @@ -254,10 +254,10 @@ async def test_forecast_invalid( expected: dict[str, Any], ) -> None: """Test invalid forecasts.""" - for attr, _v_attr, value in [ + for attr, _v_attr, value in ( ("sensor.temperature", ATTR_WEATHER_TEMPERATURE, 22.3), ("sensor.humidity", ATTR_WEATHER_HUMIDITY, 60), - ]: + ): hass.states.async_set(attr, value) await hass.async_block_till_done() @@ -337,10 +337,10 @@ async def test_forecast_invalid_is_daytime_missing_in_twice_daily( expected: dict[str, Any], ) -> None: """Test forecast service invalid when is_daytime missing in twice_daily forecast.""" - for attr, _v_attr, value in [ + for attr, _v_attr, value in ( ("sensor.temperature", ATTR_WEATHER_TEMPERATURE, 22.3), ("sensor.humidity", ATTR_WEATHER_HUMIDITY, 60), - ]: + ): hass.states.async_set(attr, value) await hass.async_block_till_done() @@ -406,10 +406,10 @@ async def test_forecast_invalid_datetime_missing( expected: dict[str, Any], ) -> None: """Test forecast service invalid when datetime missing.""" - for attr, _v_attr, value in [ + for attr, _v_attr, value in ( ("sensor.temperature", ATTR_WEATHER_TEMPERATURE, 22.3), ("sensor.humidity", ATTR_WEATHER_HUMIDITY, 60), - ]: + ): hass.states.async_set(attr, value) await hass.async_block_till_done() @@ -472,10 +472,10 @@ async def test_forecast_format_error( hass: HomeAssistant, start_ha, caplog: pytest.LogCaptureFixture, service: str ) -> None: """Test forecast service invalid on incorrect format.""" - for attr, _v_attr, value in [ + for attr, _v_attr, value in ( ("sensor.temperature", ATTR_WEATHER_TEMPERATURE, 22.3), ("sensor.humidity", ATTR_WEATHER_HUMIDITY, 60), - ]: + ): hass.states.async_set(attr, value) await hass.async_block_till_done() diff --git a/tests/components/tessie/test_button.py b/tests/components/tessie/test_button.py index fa6c8358ae6..c9cfca3288a 100644 --- a/tests/components/tessie/test_button.py +++ b/tests/components/tessie/test_button.py @@ -21,14 +21,14 @@ async def test_buttons( assert_entities(hass, entry.entry_id, entity_registry, snapshot) - for entity_id, func in [ + for entity_id, func in ( ("button.test_wake", "wake"), ("button.test_flash_lights", "flash_lights"), ("button.test_honk_horn", "honk"), ("button.test_homelink", "trigger_homelink"), ("button.test_keyless_driving", "enable_keyless_driving"), ("button.test_play_fart", "boombox"), - ]: + ): with patch( f"homeassistant.components.tessie.button.{func}", ) as mock_press: diff --git a/tests/components/tessie/test_cover.py b/tests/components/tessie/test_cover.py index b0e3d770ced..b731add10f8 100644 --- a/tests/components/tessie/test_cover.py +++ b/tests/components/tessie/test_cover.py @@ -37,12 +37,12 @@ async def test_covers( assert_entities(hass, entry.entry_id, entity_registry, snapshot) - for entity_id, openfunc, closefunc in [ + for entity_id, openfunc, closefunc in ( ("cover.test_vent_windows", "vent_windows", "close_windows"), ("cover.test_charge_port_door", "open_unlock_charge_port", "close_charge_port"), ("cover.test_frunk", "open_front_trunk", False), ("cover.test_trunk", "open_close_rear_trunk", "open_close_rear_trunk"), - ]: + ): # Test open windows if openfunc: with patch( diff --git a/tests/components/text/test_device_action.py b/tests/components/text/test_device_action.py index 29e030b034e..5766e5dce2a 100644 --- a/tests/components/text/test_device_action.py +++ b/tests/components/text/test_device_action.py @@ -100,7 +100,7 @@ async def test_get_actions_hidden_auxiliary( "entity_id": entity_entry.id, "metadata": {"secondary": True}, } - for action in ["set_value"] + for action in ("set_value",) ] actions = await async_get_device_automations( hass, DeviceAutomationType.ACTION, device_entry.id diff --git a/tests/components/trend/test_binary_sensor.py b/tests/components/trend/test_binary_sensor.py index d8d02755044..23d5a5357a7 100644 --- a/tests/components/trend/test_binary_sensor.py +++ b/tests/components/trend/test_binary_sensor.py @@ -197,7 +197,7 @@ async def test_max_samples( }, ) - for val in [0, 1, 2, 3, 2, 1]: + for val in (0, 1, 2, 3, 2, 1): hass.states.async_set("sensor.test_state", val) await hass.async_block_till_done() @@ -212,7 +212,7 @@ async def test_non_numeric( """Test for non-numeric sensor.""" await setup_component({"entity_id": "sensor.test_state"}) - for val in ["Non", "Numeric"]: + for val in ("Non", "Numeric"): hass.states.async_set("sensor.test_state", val) await hass.async_block_till_done() @@ -230,7 +230,7 @@ async def test_missing_attribute( }, ) - for val in [1, 2]: + for val in (1, 2): hass.states.async_set("sensor.test_state", "State", {"attr": val}) await hass.async_block_till_done() @@ -311,7 +311,7 @@ async def test_restore_state( assert hass.states.get("binary_sensor.test_trend_sensor").state == restored_state # add not enough samples to trigger calculation - for val in [10, 20, 30, 40]: + for val in (10, 20, 30, 40): freezer.tick(timedelta(seconds=2)) hass.states.async_set("sensor.test_state", val) await hass.async_block_till_done() @@ -320,7 +320,7 @@ async def test_restore_state( assert hass.states.get("binary_sensor.test_trend_sensor").state == restored_state # add more samples to trigger calculation - for val in [50, 60, 70, 80]: + for val in (50, 60, 70, 80): freezer.tick(timedelta(seconds=2)) hass.states.async_set("sensor.test_state", val) await hass.async_block_till_done() diff --git a/tests/components/update/test_device_trigger.py b/tests/components/update/test_device_trigger.py index 69719d4453b..fa9af863f56 100644 --- a/tests/components/update/test_device_trigger.py +++ b/tests/components/update/test_device_trigger.py @@ -61,7 +61,7 @@ async def test_get_triggers( "entity_id": entity_entry.id, "metadata": {"secondary": False}, } - for trigger in ["changed_states", "turned_off", "turned_on"] + for trigger in ("changed_states", "turned_off", "turned_on") ] triggers = await async_get_device_automations( hass, DeviceAutomationType.TRIGGER, device_entry.id @@ -109,7 +109,7 @@ async def test_get_triggers_hidden_auxiliary( "entity_id": entity_entry.id, "metadata": {"secondary": True}, } - for trigger in ["changed_states", "turned_off", "turned_on"] + for trigger in ("changed_states", "turned_off", "turned_on") ] triggers = await async_get_device_automations( hass, DeviceAutomationType.TRIGGER, device_entry.id diff --git a/tests/components/vulcan/test_config_flow.py b/tests/components/vulcan/test_config_flow.py index 01c6bf3edaf..3311f3c71b2 100644 --- a/tests/components/vulcan/test_config_flow.py +++ b/tests/components/vulcan/test_config_flow.py @@ -89,10 +89,10 @@ async def test_config_flow_auth_success_with_multiple_students( mock_account.return_value = fake_account mock_student.return_value = [ Student.load(student) - for student in [ + for student in ( load_fixture("fake_student_1.json", "vulcan"), load_fixture("fake_student_2.json", "vulcan"), - ] + ) ] result = await hass.config_entries.flow.async_init( const.DOMAIN, context={"source": config_entries.SOURCE_USER} diff --git a/tests/components/xiaomi_miio/test_vacuum.py b/tests/components/xiaomi_miio/test_vacuum.py index 6a65c1b7b9a..462145d16ab 100644 --- a/tests/components/xiaomi_miio/test_vacuum.py +++ b/tests/components/xiaomi_miio/test_vacuum.py @@ -57,8 +57,6 @@ from . import TEST_MAC from tests.common import MockConfigEntry, async_fire_time_changed -# pylint: disable=consider-using-tuple - # calls made when device status is requested STATUS_CALLS = [ mock.call.status(), @@ -423,7 +421,7 @@ async def test_xiaomi_vacuum_services( "segments": ["1", "2"], }, "segment_clean", - mock.call(segments=[int(i) for i in ["1", "2"]]), + mock.call(segments=[int(i) for i in ("1", "2")]), ), ( SERVICE_CLEAN_SEGMENT, @@ -495,7 +493,7 @@ async def test_xiaomi_vacuum_fanspeeds( state = hass.states.get(entity_id) assert state.attributes.get(ATTR_FAN_SPEED) == "Silent" fanspeeds = state.attributes.get(ATTR_FAN_SPEED_LIST) - for speed in ["Silent", "Standard", "Medium", "Turbo"]: + for speed in ("Silent", "Standard", "Medium", "Turbo"): assert speed in fanspeeds # Set speed service: diff --git a/tests/components/zha/test_cluster_handlers.py b/tests/components/zha/test_cluster_handlers.py index f89c47b79a2..0f9929d0a97 100644 --- a/tests/components/zha/test_cluster_handlers.py +++ b/tests/components/zha/test_cluster_handlers.py @@ -586,7 +586,7 @@ async def test_ep_cluster_handlers_configure(cluster_handler) -> None: await endpoint.async_configure() await endpoint.async_initialize(mock.sentinel.from_cache) - for ch in [*claimed.values(), *client_handlers.values()]: + for ch in (*claimed.values(), *client_handlers.values()): assert ch.async_initialize.call_count == 1 assert ch.async_initialize.await_count == 1 assert ch.async_initialize.call_args[0][0] is mock.sentinel.from_cache diff --git a/tests/components/zha/test_device_action.py b/tests/components/zha/test_device_action.py index 53f4e10ad19..13e9d789191 100644 --- a/tests/components/zha/test_device_action.py +++ b/tests/components/zha/test_device_action.py @@ -149,19 +149,19 @@ async def test_get_actions( "entity_id": entity_id, "metadata": {"secondary": True}, } - for action in [ + for action in ( "select_first", "select_last", "select_next", "select_option", "select_previous", - ] - for entity_id in [ + ) + for entity_id in ( siren_level_select.id, siren_tone_select.id, strobe_level_select.id, strobe_select.id, - ] + ) ] ) diff --git a/tests/components/zha/test_fan.py b/tests/components/zha/test_fan.py index 5ed7c7bfeed..095f505876e 100644 --- a/tests/components/zha/test_fan.py +++ b/tests/components/zha/test_fan.py @@ -238,7 +238,7 @@ async def async_turn_on(hass, entity_id, percentage=None): """Turn fan on.""" data = { key: value - for key, value in [(ATTR_ENTITY_ID, entity_id), (ATTR_PERCENTAGE, percentage)] + for key, value in ((ATTR_ENTITY_ID, entity_id), (ATTR_PERCENTAGE, percentage)) if value is not None } @@ -256,7 +256,7 @@ async def async_set_percentage(hass, entity_id, percentage=None): """Set percentage for specified fan.""" data = { key: value - for key, value in [(ATTR_ENTITY_ID, entity_id), (ATTR_PERCENTAGE, percentage)] + for key, value in ((ATTR_ENTITY_ID, entity_id), (ATTR_PERCENTAGE, percentage)) if value is not None } @@ -269,7 +269,7 @@ async def async_set_preset_mode(hass, entity_id, preset_mode=None): """Set preset_mode for specified fan.""" data = { key: value - for key, value in [(ATTR_ENTITY_ID, entity_id), (ATTR_PRESET_MODE, preset_mode)] + for key, value in ((ATTR_ENTITY_ID, entity_id), (ATTR_PRESET_MODE, preset_mode)) if value is not None } From 4962895f196326e54967ae09ada7aef107dcb0a9 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 12 Jun 2024 16:27:56 +0200 Subject: [PATCH 0571/1445] Fix consider-using-enumerate warnings in tests (#119506) --- tests/components/knx/test_telegrams.py | 4 ++-- tests/components/modbus/test_sensor.py | 8 ++++---- tests/components/plant/test_init.py | 8 ++++---- tests/components/sensor/test_recorder.py | 18 +++++++++--------- tests/components/statistics/test_sensor.py | 4 ++-- 5 files changed, 21 insertions(+), 21 deletions(-) diff --git a/tests/components/knx/test_telegrams.py b/tests/components/knx/test_telegrams.py index 4d72a9583a1..2eda718f5ac 100644 --- a/tests/components/knx/test_telegrams.py +++ b/tests/components/knx/test_telegrams.py @@ -51,8 +51,8 @@ MOCK_TELEGRAMS = [ def assert_telegram_history(telegrams: list[TelegramDict]) -> bool: """Assert that the mock telegrams are equal to the given telegrams. Omitting timestamp.""" assert len(telegrams) == len(MOCK_TELEGRAMS) - for index in range(len(telegrams)): - test_telegram = copy(telegrams[index]) # don't modify the original + for index, value in enumerate(telegrams): + test_telegram = copy(value) # don't modify the original comp_telegram = MOCK_TELEGRAMS[index] assert datetime.fromisoformat(test_telegram["timestamp"]) if isinstance(test_telegram["payload"], tuple): diff --git a/tests/components/modbus/test_sensor.py b/tests/components/modbus/test_sensor.py index 71cb64cc1b6..20ff558fce6 100644 --- a/tests/components/modbus/test_sensor.py +++ b/tests/components/modbus/test_sensor.py @@ -901,7 +901,7 @@ async def test_virtual_sensor( hass: HomeAssistant, entity_registry: er.EntityRegistry, mock_do_cycle, expected ) -> None: """Run test for sensor.""" - for i in range(len(expected)): + for i, expected_value in enumerate(expected): entity_id = f"{SENSOR_DOMAIN}.{TEST_ENTITY_NAME}".replace(" ", "_") unique_id = f"{SLAVE_UNIQUE_ID}" if i: @@ -909,7 +909,7 @@ async def test_virtual_sensor( unique_id = f"{unique_id}_{i}" entry = entity_registry.async_get(entity_id) state = hass.states.get(entity_id).state - assert state == expected[i] + assert state == expected_value assert entry.unique_id == unique_id @@ -1071,12 +1071,12 @@ async def test_virtual_swap_sensor( hass: HomeAssistant, mock_do_cycle, expected ) -> None: """Run test for sensor.""" - for i in range(len(expected)): + for i, expected_value in enumerate(expected): entity_id = f"{SENSOR_DOMAIN}.{TEST_ENTITY_NAME}".replace(" ", "_") if i: entity_id = f"{entity_id}_{i}" state = hass.states.get(entity_id).state - assert state == expected[i] + assert state == expected_value @pytest.mark.parametrize( diff --git a/tests/components/plant/test_init.py b/tests/components/plant/test_init.py index 8a728665ce2..97286a28cde 100644 --- a/tests/components/plant/test_init.py +++ b/tests/components/plant/test_init.py @@ -204,8 +204,8 @@ def test_daily_history_one_day(hass: HomeAssistant) -> None: """Test storing data for the same day.""" dh = plant.DailyHistory(3) values = [-2, 10, 0, 5, 20] - for i in range(len(values)): - dh.add_measurement(values[i]) + for i, value in enumerate(values): + dh.add_measurement(value) max_value = max(values[0 : i + 1]) assert len(dh._days) == 1 assert dh.max == max_value @@ -222,6 +222,6 @@ def test_daily_history_multiple_days(hass: HomeAssistant) -> None: values = [10, 1, 7, 3] max_values = [10, 10, 10, 7] - for i in range(len(days)): - dh.add_measurement(values[i], days[i]) + for i, value in enumerate(days): + dh.add_measurement(values[i], value) assert max_values[i] == dh.max diff --git a/tests/components/sensor/test_recorder.py b/tests/components/sensor/test_recorder.py index 0abe5e56e44..896742d87c3 100644 --- a/tests/components/sensor/test_recorder.py +++ b/tests/components/sensor/test_recorder.py @@ -4786,10 +4786,10 @@ async def test_validate_statistics_unit_change_no_conversion( with session_scope(hass=hass, read_only=True) as session: db_states = list(session.query(StatisticsMeta)) assert len(db_states) == len(expected_result) - for i in range(len(db_states)): - assert db_states[i].statistic_id == expected_result[i]["statistic_id"] + for i, db_state in enumerate(db_states): + assert db_state.statistic_id == expected_result[i]["statistic_id"] assert ( - db_states[i].unit_of_measurement + db_state.unit_of_measurement == expected_result[i]["unit_of_measurement"] ) @@ -4920,10 +4920,10 @@ async def test_validate_statistics_unit_change_equivalent_units( with session_scope(hass=hass, read_only=True) as session: db_states = list(session.query(StatisticsMeta)) assert len(db_states) == len(expected_result) - for i in range(len(db_states)): - assert db_states[i].statistic_id == expected_result[i]["statistic_id"] + for i, db_state in enumerate(db_states): + assert db_state.statistic_id == expected_result[i]["statistic_id"] assert ( - db_states[i].unit_of_measurement + db_state.unit_of_measurement == expected_result[i]["unit_of_measurement"] ) @@ -5005,10 +5005,10 @@ async def test_validate_statistics_unit_change_equivalent_units_2( with session_scope(hass=hass, read_only=True) as session: db_states = list(session.query(StatisticsMeta)) assert len(db_states) == len(expected_result) - for i in range(len(db_states)): - assert db_states[i].statistic_id == expected_result[i]["statistic_id"] + for i, db_state in enumerate(db_states): + assert db_state.statistic_id == expected_result[i]["statistic_id"] assert ( - db_states[i].unit_of_measurement + db_state.unit_of_measurement == expected_result[i]["unit_of_measurement"] ) diff --git a/tests/components/statistics/test_sensor.py b/tests/components/statistics/test_sensor.py index 6508ccd608e..5a716fd8ce8 100644 --- a/tests/components/statistics/test_sensor.py +++ b/tests/components/statistics/test_sensor.py @@ -1314,13 +1314,13 @@ async def test_state_characteristics(hass: HomeAssistant) -> None: # With all values in buffer - for i in range(len(VALUES_NUMERIC)): + for i, value in enumerate(VALUES_NUMERIC): current_time += timedelta(minutes=1) freezer.move_to(current_time) async_fire_time_changed(hass, current_time) hass.states.async_set( "sensor.test_monitored", - str(VALUES_NUMERIC[i]), + str(value), {ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS}, ) hass.states.async_set( From cb39d2d16be96177ec59fc36ebaa5a21402b4252 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 12 Jun 2024 16:40:26 +0200 Subject: [PATCH 0572/1445] Ignore existing fixme pylint warnings in tests (#119500) Co-authored-by: Robert Resch --- tests/components/emulated_hue/test_hue_api.py | 2 ++ tests/components/energy/test_sensor.py | 1 + tests/components/zha/test_climate.py | 1 + tests/components/zha/test_light.py | 1 + 4 files changed, 5 insertions(+) diff --git a/tests/components/emulated_hue/test_hue_api.py b/tests/components/emulated_hue/test_hue_api.py index a0409a83901..4edd52b812d 100644 --- a/tests/components/emulated_hue/test_hue_api.py +++ b/tests/components/emulated_hue/test_hue_api.py @@ -1651,6 +1651,7 @@ async def test_only_change_contrast(hass: HomeAssistant, hass_hue, hue_client) - ) # Check that only setting the contrast will also turn on the light. + # pylint: disable-next=fixme # TODO: It should be noted that a real Hue hub will not allow to change the brightness if the underlying entity is off. # giving the error: [{"error":{"type":201,"address":"/lights/20/state/bri","description":"parameter, bri, is not modifiable. Device is set to off."}}] # emulated_hue however will always turn on the light. @@ -1664,6 +1665,7 @@ async def test_only_change_hue_or_saturation( ) -> None: """Test setting either the hue or the saturation but not both.""" + # pylint: disable-next=fixme # TODO: The handling of this appears wrong, as setting only one will set the other to 0. # The return values also appear wrong. diff --git a/tests/components/energy/test_sensor.py b/tests/components/energy/test_sensor.py index b9aca285829..0439ac2c028 100644 --- a/tests/components/energy/test_sensor.py +++ b/tests/components/energy/test_sensor.py @@ -87,6 +87,7 @@ async def test_cost_sensor_no_states( "data": energy_data, } await setup_integration(hass) + # pylint: disable-next=fixme # TODO: No states, should the cost entity refuse to setup? diff --git a/tests/components/zha/test_climate.py b/tests/components/zha/test_climate.py index cac5ef66937..32ef08fcd96 100644 --- a/tests/components/zha/test_climate.py +++ b/tests/components/zha/test_climate.py @@ -1458,6 +1458,7 @@ async def test_set_moes_operation_mode( [ (0, PRESET_AWAY), (1, PRESET_SCHEDULE), + # pylint: disable-next=fixme # (2, PRESET_NONE), # TODO: why does this not work? (4, PRESET_ECO), (5, PRESET_BOOST), diff --git a/tests/components/zha/test_light.py b/tests/components/zha/test_light.py index e2c13ed9a29..5d50d708ed6 100644 --- a/tests/components/zha/test_light.py +++ b/tests/components/zha/test_light.py @@ -454,6 +454,7 @@ async def test_light_initialization( assert entity_id is not None + # pylint: disable-next=fixme # TODO ensure hue and saturation are properly set on startup From 99b349fa2c4c5a153e8dcc909f92dff0f7f6384f Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 12 Jun 2024 16:44:29 +0200 Subject: [PATCH 0573/1445] Fix consider-using-dict-items warnings in tests (#119497) --- .../components/google_assistant/test_trait.py | 6 +-- tests/components/google_wifi/test_sensor.py | 42 +++++++++---------- tests/components/metoffice/test_weather.py | 4 +- .../components/netatmo/test_device_trigger.py | 6 +-- tests/components/nexia/test_binary_sensor.py | 4 +- tests/components/nexia/test_climate.py | 4 +- tests/components/nexia/test_number.py | 4 +- tests/components/nexia/test_scene.py | 6 +-- tests/components/nexia/test_sensor.py | 18 ++++---- tests/components/number/test_init.py | 7 +--- tests/components/switch_as_x/test_init.py | 19 ++++----- .../components/template/test_binary_sensor.py | 4 +- tests/components/template/test_sensor.py | 4 +- 13 files changed, 60 insertions(+), 68 deletions(-) diff --git a/tests/components/google_assistant/test_trait.py b/tests/components/google_assistant/test_trait.py index d91d12b7074..038b16d0cfc 100644 --- a/tests/components/google_assistant/test_trait.py +++ b/tests/components/google_assistant/test_trait.py @@ -3986,7 +3986,7 @@ async def test_sensorstate( ), } - for sensor_type in sensor_types: + for sensor_type, item in sensor_types.items(): assert helpers.get_google_type(sensor.DOMAIN, None) is not None assert trait.SensorStateTrait.supported(sensor.DOMAIN, None, sensor_type, None) @@ -4002,8 +4002,8 @@ async def test_sensorstate( BASIC_CONFIG, ) - name = sensor_types[sensor_type][0] - unit = sensor_types[sensor_type][1] + name = item[0] + unit = item[1] if sensor_type == sensor.SensorDeviceClass.AQI: assert trt.sync_attributes() == { diff --git a/tests/components/google_wifi/test_sensor.py b/tests/components/google_wifi/test_sensor.py index fcc5603fdc5..c7df2b4e822 100644 --- a/tests/components/google_wifi/test_sensor.py +++ b/tests/components/google_wifi/test_sensor.py @@ -94,8 +94,8 @@ def setup_api(hass, data, requests_mock): "units": desc.native_unit_of_measurement, "icon": desc.icon, } - for name in sensor_dict: - sensor = sensor_dict[name]["sensor"] + for value in sensor_dict.values(): + sensor = value["sensor"] sensor.hass = hass return api, sensor_dict @@ -111,9 +111,9 @@ def fake_delay(hass, ha_delay): def test_name(requests_mock: requests_mock.Mocker) -> None: """Test the name.""" api, sensor_dict = setup_api(None, MOCK_DATA, requests_mock) - for name in sensor_dict: - sensor = sensor_dict[name]["sensor"] - test_name = sensor_dict[name]["name"] + for value in sensor_dict.values(): + sensor = value["sensor"] + test_name = value["name"] assert test_name == sensor.name @@ -122,17 +122,17 @@ def test_unit_of_measurement( ) -> None: """Test the unit of measurement.""" api, sensor_dict = setup_api(hass, MOCK_DATA, requests_mock) - for name in sensor_dict: - sensor = sensor_dict[name]["sensor"] - assert sensor_dict[name]["units"] == sensor.unit_of_measurement + for value in sensor_dict.values(): + sensor = value["sensor"] + assert value["units"] == sensor.unit_of_measurement def test_icon(requests_mock: requests_mock.Mocker) -> None: """Test the icon.""" api, sensor_dict = setup_api(None, MOCK_DATA, requests_mock) - for name in sensor_dict: - sensor = sensor_dict[name]["sensor"] - assert sensor_dict[name]["icon"] == sensor.icon + for value in sensor_dict.values(): + sensor = value["sensor"] + assert value["icon"] == sensor.icon def test_state(hass: HomeAssistant, requests_mock: requests_mock.Mocker) -> None: @@ -140,8 +140,8 @@ def test_state(hass: HomeAssistant, requests_mock: requests_mock.Mocker) -> None api, sensor_dict = setup_api(hass, MOCK_DATA, requests_mock) now = datetime(1970, month=1, day=1) with patch("homeassistant.util.dt.now", return_value=now): - for name in sensor_dict: - sensor = sensor_dict[name]["sensor"] + for name, value in sensor_dict.items(): + sensor = value["sensor"] fake_delay(hass, 2) sensor.update() if name == google_wifi.ATTR_LAST_RESTART: @@ -159,8 +159,8 @@ def test_update_when_value_is_none( ) -> None: """Test state gets updated to unknown when sensor returns no data.""" api, sensor_dict = setup_api(hass, None, requests_mock) - for name in sensor_dict: - sensor = sensor_dict[name]["sensor"] + for value in sensor_dict.values(): + sensor = value["sensor"] fake_delay(hass, 2) sensor.update() assert sensor.state is None @@ -173,8 +173,8 @@ def test_update_when_value_changed( api, sensor_dict = setup_api(hass, MOCK_DATA_NEXT, requests_mock) now = datetime(1970, month=1, day=1) with patch("homeassistant.util.dt.now", return_value=now): - for name in sensor_dict: - sensor = sensor_dict[name]["sensor"] + for name, value in sensor_dict.items(): + sensor = value["sensor"] fake_delay(hass, 2) sensor.update() if name == google_wifi.ATTR_LAST_RESTART: @@ -198,8 +198,8 @@ def test_when_api_data_missing( api, sensor_dict = setup_api(hass, MOCK_DATA_MISSING, requests_mock) now = datetime(1970, month=1, day=1) with patch("homeassistant.util.dt.now", return_value=now): - for name in sensor_dict: - sensor = sensor_dict[name]["sensor"] + for value in sensor_dict.values(): + sensor = value["sensor"] fake_delay(hass, 2) sensor.update() assert sensor.state is None @@ -214,8 +214,8 @@ def test_update_when_unavailable( "google_wifi.GoogleWifiAPI.update", side_effect=update_side_effect(hass, requests_mock), ) - for name in sensor_dict: - sensor = sensor_dict[name]["sensor"] + for value in sensor_dict.values(): + sensor = value["sensor"] sensor.update() assert sensor.state is None diff --git a/tests/components/metoffice/test_weather.py b/tests/components/metoffice/test_weather.py index 64e6ef65ec2..c931222d1d6 100644 --- a/tests/components/metoffice/test_weather.py +++ b/tests/components/metoffice/test_weather.py @@ -94,8 +94,8 @@ async def test_site_cannot_connect( assert hass.states.get("weather.met_office_wavertree_3hourly") is None assert hass.states.get("weather.met_office_wavertree_daily") is None - for sensor_id in WAVERTREE_SENSOR_RESULTS: - sensor_name, _ = WAVERTREE_SENSOR_RESULTS[sensor_id] + for sensor in WAVERTREE_SENSOR_RESULTS.values(): + sensor_name = sensor[0] sensor = hass.states.get(f"sensor.wavertree_{sensor_name}") assert sensor is None diff --git a/tests/components/netatmo/test_device_trigger.py b/tests/components/netatmo/test_device_trigger.py index fac3cedff75..ad1e9bd8cb9 100644 --- a/tests/components/netatmo/test_device_trigger.py +++ b/tests/components/netatmo/test_device_trigger.py @@ -266,13 +266,11 @@ async def test_if_fires_on_event_legacy( ("platform", "camera_type", "event_type", "sub_type"), [ ("climate", "Smart Valve", trigger, subtype) - for trigger in SUBTYPES - for subtype in SUBTYPES[trigger] + for trigger, subtype in SUBTYPES.items() ] + [ ("climate", "Smart Thermostat", trigger, subtype) - for trigger in SUBTYPES - for subtype in SUBTYPES[trigger] + for trigger, subtype in SUBTYPES.items() ], ) async def test_if_fires_on_event_with_subtype( diff --git a/tests/components/nexia/test_binary_sensor.py b/tests/components/nexia/test_binary_sensor.py index e175afe6214..0abb709f6aa 100644 --- a/tests/components/nexia/test_binary_sensor.py +++ b/tests/components/nexia/test_binary_sensor.py @@ -20,7 +20,7 @@ async def test_create_binary_sensors(hass: HomeAssistant) -> None: # Only test for a subset of attributes in case # HA changes the implementation and a new one appears assert all( - state.attributes[key] == expected_attributes[key] for key in expected_attributes + state.attributes[key] == value for key, value in expected_attributes.items() ) state = hass.states.get("binary_sensor.downstairs_east_wing_blower_active") @@ -32,5 +32,5 @@ async def test_create_binary_sensors(hass: HomeAssistant) -> None: # Only test for a subset of attributes in case # HA changes the implementation and a new one appears assert all( - state.attributes[key] == expected_attributes[key] for key in expected_attributes + state.attributes[key] == value for key, value in expected_attributes.items() ) diff --git a/tests/components/nexia/test_climate.py b/tests/components/nexia/test_climate.py index 900838547f2..1d248e5ec5f 100644 --- a/tests/components/nexia/test_climate.py +++ b/tests/components/nexia/test_climate.py @@ -39,7 +39,7 @@ async def test_climate_zones(hass: HomeAssistant) -> None: # Only test for a subset of attributes in case # HA changes the implementation and a new one appears assert all( - state.attributes[key] == expected_attributes[key] for key in expected_attributes + state.attributes[key] == value for key, value in expected_attributes.items() ) state = hass.states.get("climate.kitchen") @@ -72,5 +72,5 @@ async def test_climate_zones(hass: HomeAssistant) -> None: # Only test for a subset of attributes in case # HA changes the implementation and a new one appears assert all( - state.attributes[key] == expected_attributes[key] for key in expected_attributes + state.attributes[key] == value for key, value in expected_attributes.items() ) diff --git a/tests/components/nexia/test_number.py b/tests/components/nexia/test_number.py index 7f4c5f92ab6..ee621912807 100644 --- a/tests/components/nexia/test_number.py +++ b/tests/components/nexia/test_number.py @@ -26,7 +26,7 @@ async def test_create_fan_speed_number_entities(hass: HomeAssistant) -> None: # Only test for a subset of attributes in case # HA changes the implementation and a new one appears assert all( - state.attributes[key] == expected_attributes[key] for key in expected_attributes + state.attributes[key] == value for key, value in expected_attributes.items() ) state = hass.states.get("number.downstairs_east_wing_fan_speed") @@ -40,7 +40,7 @@ async def test_create_fan_speed_number_entities(hass: HomeAssistant) -> None: # Only test for a subset of attributes in case # HA changes the implementation and a new one appears assert all( - state.attributes[key] == expected_attributes[key] for key in expected_attributes + state.attributes[key] == value for key, value in expected_attributes.items() ) diff --git a/tests/components/nexia/test_scene.py b/tests/components/nexia/test_scene.py index 20f214fff27..5d9ae30c7e1 100644 --- a/tests/components/nexia/test_scene.py +++ b/tests/components/nexia/test_scene.py @@ -35,7 +35,7 @@ async def test_automation_scenes(hass: HomeAssistant) -> None: # Only test for a subset of attributes in case # HA changes the implementation and a new one appears assert all( - state.attributes[key] == expected_attributes[key] for key in expected_attributes + state.attributes[key] == value for key, value in expected_attributes.items() ) state = hass.states.get("scene.power_outage") @@ -55,7 +55,7 @@ async def test_automation_scenes(hass: HomeAssistant) -> None: # Only test for a subset of attributes in case # HA changes the implementation and a new one appears assert all( - state.attributes[key] == expected_attributes[key] for key in expected_attributes + state.attributes[key] == value for key, value in expected_attributes.items() ) state = hass.states.get("scene.power_restored") @@ -73,5 +73,5 @@ async def test_automation_scenes(hass: HomeAssistant) -> None: # Only test for a subset of attributes in case # HA changes the implementation and a new one appears assert all( - state.attributes[key] == expected_attributes[key] for key in expected_attributes + state.attributes[key] == value for key, value in expected_attributes.items() ) diff --git a/tests/components/nexia/test_sensor.py b/tests/components/nexia/test_sensor.py index 1f595da43d1..ec9ed256617 100644 --- a/tests/components/nexia/test_sensor.py +++ b/tests/components/nexia/test_sensor.py @@ -23,7 +23,7 @@ async def test_create_sensors(hass: HomeAssistant) -> None: # Only test for a subset of attributes in case # HA changes the implementation and a new one appears assert all( - state.attributes[key] == expected_attributes[key] for key in expected_attributes + state.attributes[key] == value for key, value in expected_attributes.items() ) state = hass.states.get("sensor.nick_office_zone_setpoint_status") @@ -35,7 +35,7 @@ async def test_create_sensors(hass: HomeAssistant) -> None: # Only test for a subset of attributes in case # HA changes the implementation and a new one appears assert all( - state.attributes[key] == expected_attributes[key] for key in expected_attributes + state.attributes[key] == value for key, value in expected_attributes.items() ) state = hass.states.get("sensor.nick_office_zone_status") @@ -48,7 +48,7 @@ async def test_create_sensors(hass: HomeAssistant) -> None: # Only test for a subset of attributes in case # HA changes the implementation and a new one appears assert all( - state.attributes[key] == expected_attributes[key] for key in expected_attributes + state.attributes[key] == value for key, value in expected_attributes.items() ) state = hass.states.get("sensor.master_suite_air_cleaner_mode") @@ -61,7 +61,7 @@ async def test_create_sensors(hass: HomeAssistant) -> None: # Only test for a subset of attributes in case # HA changes the implementation and a new one appears assert all( - state.attributes[key] == expected_attributes[key] for key in expected_attributes + state.attributes[key] == value for key, value in expected_attributes.items() ) state = hass.states.get("sensor.master_suite_current_compressor_speed") @@ -75,7 +75,7 @@ async def test_create_sensors(hass: HomeAssistant) -> None: # Only test for a subset of attributes in case # HA changes the implementation and a new one appears assert all( - state.attributes[key] == expected_attributes[key] for key in expected_attributes + state.attributes[key] == value for key, value in expected_attributes.items() ) state = hass.states.get("sensor.master_suite_outdoor_temperature") @@ -90,7 +90,7 @@ async def test_create_sensors(hass: HomeAssistant) -> None: # Only test for a subset of attributes in case # HA changes the implementation and a new one appears assert all( - state.attributes[key] == expected_attributes[key] for key in expected_attributes + state.attributes[key] == value for key, value in expected_attributes.items() ) state = hass.states.get("sensor.master_suite_humidity") @@ -105,7 +105,7 @@ async def test_create_sensors(hass: HomeAssistant) -> None: # Only test for a subset of attributes in case # HA changes the implementation and a new one appears assert all( - state.attributes[key] == expected_attributes[key] for key in expected_attributes + state.attributes[key] == value for key, value in expected_attributes.items() ) state = hass.states.get("sensor.master_suite_requested_compressor_speed") @@ -119,7 +119,7 @@ async def test_create_sensors(hass: HomeAssistant) -> None: # Only test for a subset of attributes in case # HA changes the implementation and a new one appears assert all( - state.attributes[key] == expected_attributes[key] for key in expected_attributes + state.attributes[key] == value for key, value in expected_attributes.items() ) state = hass.states.get("sensor.master_suite_system_status") @@ -132,5 +132,5 @@ async def test_create_sensors(hass: HomeAssistant) -> None: # Only test for a subset of attributes in case # HA changes the implementation and a new one appears assert all( - state.attributes[key] == expected_attributes[key] for key in expected_attributes + state.attributes[key] == value for key, value in expected_attributes.items() ) diff --git a/tests/components/number/test_init.py b/tests/components/number/test_init.py index dbdbab31d63..6f74a3126c0 100644 --- a/tests/components/number/test_init.py +++ b/tests/components/number/test_init.py @@ -846,13 +846,10 @@ def test_device_classes_aligned() -> None: assert hasattr(NumberDeviceClass, device_class.name) assert getattr(NumberDeviceClass, device_class.name).value == device_class.value - for device_class in SENSOR_DEVICE_CLASS_UNITS: + for device_class, unit in SENSOR_DEVICE_CLASS_UNITS.items(): if device_class in NON_NUMERIC_DEVICE_CLASSES: continue - assert ( - SENSOR_DEVICE_CLASS_UNITS[device_class] - == NUMBER_DEVICE_CLASS_UNITS[device_class] - ) + assert unit == NUMBER_DEVICE_CLASS_UNITS[device_class] class MockFlow(ConfigFlow): diff --git a/tests/components/switch_as_x/test_init.py b/tests/components/switch_as_x/test_init.py index b1ebbbb9322..3889a43f741 100644 --- a/tests/components/switch_as_x/test_init.py +++ b/tests/components/switch_as_x/test_init.py @@ -782,8 +782,8 @@ async def test_import_expose_settings_1( expose_settings = exposed_entities.async_get_entity_settings( hass, entity_entry.entity_id ) - for assistant in EXPOSE_SETTINGS: - assert expose_settings[assistant]["should_expose"] == EXPOSE_SETTINGS[assistant] + for assistant, settings in EXPOSE_SETTINGS.items(): + assert expose_settings[assistant]["should_expose"] == settings # Check the switch is no longer exposed expose_settings = exposed_entities.async_get_entity_settings( @@ -856,18 +856,15 @@ async def test_import_expose_settings_2( expose_settings = exposed_entities.async_get_entity_settings( hass, entity_entry.entity_id ) - for assistant in EXPOSE_SETTINGS: - assert ( - expose_settings[assistant]["should_expose"] - is not EXPOSE_SETTINGS[assistant] - ) + for assistant, settings in EXPOSE_SETTINGS.items(): + assert expose_settings[assistant]["should_expose"] is not settings # Check the switch settings were not modified expose_settings = exposed_entities.async_get_entity_settings( hass, switch_entity_entry.entity_id ) - for assistant in EXPOSE_SETTINGS: - assert expose_settings[assistant]["should_expose"] == EXPOSE_SETTINGS[assistant] + for assistant, settings in EXPOSE_SETTINGS.items(): + assert expose_settings[assistant]["should_expose"] == settings @pytest.mark.parametrize("target_domain", PLATFORMS_TO_TEST) @@ -922,8 +919,8 @@ async def test_restore_expose_settings( expose_settings = exposed_entities.async_get_entity_settings( hass, switch_entity_entry.entity_id ) - for assistant in EXPOSE_SETTINGS: - assert expose_settings[assistant]["should_expose"] == EXPOSE_SETTINGS[assistant] + for assistant, settings in EXPOSE_SETTINGS.items(): + assert expose_settings[assistant]["should_expose"] == settings @pytest.mark.parametrize("target_domain", PLATFORMS_TO_TEST) diff --git a/tests/components/template/test_binary_sensor.py b/tests/components/template/test_binary_sensor.py index 452f926dca5..63d9b338eaa 100644 --- a/tests/components/template/test_binary_sensor.py +++ b/tests/components/template/test_binary_sensor.py @@ -1273,9 +1273,9 @@ async def test_trigger_entity_restore_state( state = hass.states.get("binary_sensor.test") assert state.state == initial_state - for attr in restored_attributes: + for attr, value in restored_attributes.items(): if attr in initial_attributes: - assert state.attributes[attr] == restored_attributes[attr] + assert state.attributes[attr] == value else: assert attr not in state.attributes assert "another" not in state.attributes diff --git a/tests/components/template/test_sensor.py b/tests/components/template/test_sensor.py index fdcc0587a73..54e53f5257e 100644 --- a/tests/components/template/test_sensor.py +++ b/tests/components/template/test_sensor.py @@ -1828,9 +1828,9 @@ async def test_trigger_entity_restore_state( state = hass.states.get("sensor.test") assert state.state == initial_state - for attr in restored_attributes: + for attr, value in restored_attributes.items(): if attr in initial_attributes: - assert state.attributes[attr] == restored_attributes[attr] + assert state.attributes[attr] == value else: assert attr not in state.attributes assert "another" not in state.attributes From 420ee782ff317b85802882b04eb8646164762891 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 12 Jun 2024 16:45:21 +0200 Subject: [PATCH 0574/1445] Migrate Airtouch4 to runtime_data (#119493) --- homeassistant/components/airtouch4/__init__.py | 17 ++++++----------- homeassistant/components/airtouch4/climate.py | 6 +++--- 2 files changed, 9 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/airtouch4/__init__.py b/homeassistant/components/airtouch4/__init__.py index 5f63fe023dc..1a4c87a940c 100644 --- a/homeassistant/components/airtouch4/__init__.py +++ b/homeassistant/components/airtouch4/__init__.py @@ -7,15 +7,15 @@ from homeassistant.const import CONF_HOST, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady -from .const import DOMAIN from .coordinator import AirtouchDataUpdateCoordinator PLATFORMS = [Platform.CLIMATE] +type AirTouch4ConfigEntry = ConfigEntry[AirtouchDataUpdateCoordinator] -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + +async def async_setup_entry(hass: HomeAssistant, entry: AirTouch4ConfigEntry) -> bool: """Set up AirTouch4 from a config entry.""" - hass.data.setdefault(DOMAIN, {}) host = entry.data[CONF_HOST] airtouch = AirTouch(host) await airtouch.UpdateInfo() @@ -24,18 +24,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: raise ConfigEntryNotReady coordinator = AirtouchDataUpdateCoordinator(hass, airtouch) await coordinator.async_config_entry_first_refresh() - hass.data[DOMAIN][entry.entry_id] = coordinator + entry.runtime_data = coordinator await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: AirTouch4ConfigEntry) -> bool: """Unload a config entry.""" - unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - - if unload_ok: - hass.data[DOMAIN].pop(entry.entry_id) - - return unload_ok + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/airtouch4/climate.py b/homeassistant/components/airtouch4/climate.py index 3fdace0f553..29fd2bc4bed 100644 --- a/homeassistant/components/airtouch4/climate.py +++ b/homeassistant/components/airtouch4/climate.py @@ -16,13 +16,13 @@ from homeassistant.components.climate import ( ClimateEntityFeature, HVACMode, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity +from . import AirTouch4ConfigEntry from .const import DOMAIN AT_TO_HA_STATE = { @@ -63,11 +63,11 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: AirTouch4ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Airtouch 4.""" - coordinator = hass.data[DOMAIN][config_entry.entry_id] + coordinator = config_entry.runtime_data info = coordinator.data entities: list[ClimateEntity] = [ AirtouchGroup(coordinator, group["group_number"], info) From dc3ade655833a3b0efc99c34e43667facbc7d54c Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Wed, 12 Jun 2024 10:49:40 -0400 Subject: [PATCH 0575/1445] Store runtime data inside the config entry in Google Mail (#119439) --- .../components/google_mail/__init__.py | 12 ++++++------ .../components/google_mail/config_flow.py | 5 +++-- homeassistant/components/google_mail/sensor.py | 11 +++++------ .../components/google_mail/services.py | 17 +++++++++++------ 4 files changed, 25 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/google_mail/__init__.py b/homeassistant/components/google_mail/__init__.py index 441ecd3841f..7fae5f18da5 100644 --- a/homeassistant/components/google_mail/__init__.py +++ b/homeassistant/components/google_mail/__init__.py @@ -16,6 +16,8 @@ from .api import AsyncConfigEntryAuth from .const import DATA_AUTH, DATA_HASS_CONFIG, DOMAIN from .services import async_setup_services +type GoogleMailConfigEntry = ConfigEntry[AsyncConfigEntryAuth] + PLATFORMS = [Platform.NOTIFY, Platform.SENSOR] CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN) @@ -28,13 +30,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: return True -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: GoogleMailConfigEntry) -> bool: """Set up Google Mail from a config entry.""" implementation = await async_get_config_entry_implementation(hass, entry) session = OAuth2Session(hass, entry, implementation) auth = AsyncConfigEntryAuth(hass, session) await auth.check_and_refresh_token() - hass.data[DOMAIN][entry.entry_id] = auth + entry.runtime_data = auth hass.async_create_task( discovery.async_load_platform( @@ -55,10 +57,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: GoogleMailConfigEntry) -> bool: """Unload a config entry.""" - if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): - hass.data[DOMAIN].pop(entry.entry_id) loaded_entries = [ entry for entry in hass.config_entries.async_entries(DOMAIN) @@ -68,4 +68,4 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: for service_name in hass.services.async_services_for_domain(DOMAIN): hass.services.async_remove(DOMAIN, service_name) - return unload_ok + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/google_mail/config_flow.py b/homeassistant/components/google_mail/config_flow.py index 5b5c760628b..5c81f7d49f5 100644 --- a/homeassistant/components/google_mail/config_flow.py +++ b/homeassistant/components/google_mail/config_flow.py @@ -9,10 +9,11 @@ from typing import Any, cast from google.oauth2.credentials import Credentials from googleapiclient.discovery import build -from homeassistant.config_entries import ConfigEntry, ConfigFlowResult +from homeassistant.config_entries import ConfigFlowResult from homeassistant.const import CONF_ACCESS_TOKEN, CONF_TOKEN from homeassistant.helpers import config_entry_oauth2_flow +from . import GoogleMailConfigEntry from .const import DEFAULT_ACCESS, DOMAIN @@ -23,7 +24,7 @@ class OAuth2FlowHandler( DOMAIN = DOMAIN - reauth_entry: ConfigEntry | None = None + reauth_entry: GoogleMailConfigEntry | None = None @property def logger(self) -> logging.Logger: diff --git a/homeassistant/components/google_mail/sensor.py b/homeassistant/components/google_mail/sensor.py index 1de72632de1..c832104d719 100644 --- a/homeassistant/components/google_mail/sensor.py +++ b/homeassistant/components/google_mail/sensor.py @@ -11,11 +11,10 @@ from homeassistant.components.sensor import ( SensorEntity, SensorEntityDescription, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN +from . import GoogleMailConfigEntry from .entity import GoogleMailEntity SCAN_INTERVAL = timedelta(minutes=15) @@ -28,12 +27,12 @@ SENSOR_TYPE = SensorEntityDescription( async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, + entry: GoogleMailConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Google Mail sensor.""" - async_add_entities( - [GoogleMailSensor(hass.data[DOMAIN][entry.entry_id], SENSOR_TYPE)], True - ) + async_add_entities([GoogleMailSensor(entry.runtime_data, SENSOR_TYPE)], True) class GoogleMailSensor(GoogleMailEntity, SensorEntity): diff --git a/homeassistant/components/google_mail/services.py b/homeassistant/components/google_mail/services.py index e07e2be2101..2a81f7e6c51 100644 --- a/homeassistant/components/google_mail/services.py +++ b/homeassistant/components/google_mail/services.py @@ -3,16 +3,15 @@ from __future__ import annotations from datetime import datetime, timedelta +from typing import TYPE_CHECKING from googleapiclient.http import HttpRequest import voluptuous as vol -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.helpers import config_validation as cv from homeassistant.helpers.service import async_extract_config_entry_ids -from .api import AsyncConfigEntryAuth from .const import ( ATTR_ENABLED, ATTR_END, @@ -26,6 +25,9 @@ from .const import ( DOMAIN, ) +if TYPE_CHECKING: + from . import GoogleMailConfigEntry + SERVICE_SET_VACATION = "set_vacation" SERVICE_VACATION_SCHEMA = vol.All( @@ -47,7 +49,9 @@ SERVICE_VACATION_SCHEMA = vol.All( async def async_setup_services(hass: HomeAssistant) -> None: """Set up services for Google Mail integration.""" - async def extract_gmail_config_entries(call: ServiceCall) -> list[ConfigEntry]: + async def extract_gmail_config_entries( + call: ServiceCall, + ) -> list[GoogleMailConfigEntry]: return [ entry for entry_id in await async_extract_config_entry_ids(hass, call) @@ -57,10 +61,11 @@ async def async_setup_services(hass: HomeAssistant) -> None: async def gmail_service(call: ServiceCall) -> None: """Call Google Mail service.""" - auth: AsyncConfigEntryAuth for entry in await extract_gmail_config_entries(call): - if not (auth := hass.data[DOMAIN].get(entry.entry_id)): - raise ValueError(f"Config entry not loaded: {entry.entry_id}") + try: + auth = entry.runtime_data + except AttributeError as ex: + raise ValueError(f"Config entry not loaded: {entry.entry_id}") from ex service = await auth.get_resource() _settings = { From 5b91ea45502d7dfc36e71dd5c1dacb007a95de6b Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Wed, 12 Jun 2024 10:52:18 -0400 Subject: [PATCH 0576/1445] Store runtime data inside the config entry in Goalzero (#119440) --- homeassistant/components/goalzero/__init__.py | 17 ++++++----------- .../components/goalzero/binary_sensor.py | 12 +++++------- .../components/goalzero/coordinator.py | 4 +++- homeassistant/components/goalzero/sensor.py | 13 +++++-------- homeassistant/components/goalzero/switch.py | 13 +++++-------- tests/components/goalzero/__init__.py | 3 +-- 6 files changed, 25 insertions(+), 37 deletions(-) diff --git a/homeassistant/components/goalzero/__init__.py b/homeassistant/components/goalzero/__init__.py index 60b0338c258..6698d1efc99 100644 --- a/homeassistant/components/goalzero/__init__.py +++ b/homeassistant/components/goalzero/__init__.py @@ -6,20 +6,18 @@ from typing import TYPE_CHECKING from goalzero import Yeti, exceptions -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.device_registry import format_mac -from .const import DOMAIN -from .coordinator import GoalZeroDataUpdateCoordinator +from .coordinator import GoalZeroConfigEntry, GoalZeroDataUpdateCoordinator PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR, Platform.SWITCH] -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: GoalZeroConfigEntry) -> bool: """Set up Goal Zero Yeti from a config entry.""" mac = entry.unique_id @@ -38,16 +36,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: except exceptions.ConnectError as ex: raise ConfigEntryNotReady(f"Failed to connect to device: {ex}") from ex - coordinator = GoalZeroDataUpdateCoordinator(hass, api) - await coordinator.async_config_entry_first_refresh() - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator + entry.runtime_data = GoalZeroDataUpdateCoordinator(hass, api) + await entry.runtime_data.async_config_entry_first_refresh() await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: GoalZeroConfigEntry) -> bool: """Unload a config entry.""" - if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): - hass.data[DOMAIN].pop(entry.entry_id) - return unload_ok + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/goalzero/binary_sensor.py b/homeassistant/components/goalzero/binary_sensor.py index eec8773db30..6bd061879eb 100644 --- a/homeassistant/components/goalzero/binary_sensor.py +++ b/homeassistant/components/goalzero/binary_sensor.py @@ -9,12 +9,11 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntity, BinarySensorEntityDescription, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN +from .coordinator import GoalZeroConfigEntry from .entity import GoalZeroEntity PARALLEL_UPDATES = 0 @@ -43,14 +42,13 @@ BINARY_SENSOR_TYPES: tuple[BinarySensorEntityDescription, ...] = ( async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, + entry: GoalZeroConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Goal Zero Yeti sensor.""" async_add_entities( - GoalZeroBinarySensor( - hass.data[DOMAIN][entry.entry_id], - description, - ) + GoalZeroBinarySensor(entry.runtime_data, description) for description in BINARY_SENSOR_TYPES ) diff --git a/homeassistant/components/goalzero/coordinator.py b/homeassistant/components/goalzero/coordinator.py index 61c3a8dba29..3c7cd967482 100644 --- a/homeassistant/components/goalzero/coordinator.py +++ b/homeassistant/components/goalzero/coordinator.py @@ -10,11 +10,13 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda from .const import DOMAIN, LOGGER +type GoalZeroConfigEntry = ConfigEntry[GoalZeroDataUpdateCoordinator] + class GoalZeroDataUpdateCoordinator(DataUpdateCoordinator[None]): """Data update coordinator for the Goal zero integration.""" - config_entry: ConfigEntry + config_entry: GoalZeroConfigEntry def __init__(self, hass: HomeAssistant, api: Yeti) -> None: """Initialize the coordinator.""" diff --git a/homeassistant/components/goalzero/sensor.py b/homeassistant/components/goalzero/sensor.py index 86f8bc9455b..f565c216745 100644 --- a/homeassistant/components/goalzero/sensor.py +++ b/homeassistant/components/goalzero/sensor.py @@ -10,7 +10,6 @@ from homeassistant.components.sensor import ( SensorEntityDescription, SensorStateClass, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( PERCENTAGE, SIGNAL_STRENGTH_DECIBELS, @@ -26,7 +25,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType -from .const import DOMAIN +from .coordinator import GoalZeroConfigEntry from .entity import GoalZeroEntity SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( @@ -130,15 +129,13 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, + entry: GoalZeroConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Goal Zero Yeti sensor.""" async_add_entities( - GoalZeroSensor( - hass.data[DOMAIN][entry.entry_id], - description, - ) - for description in SENSOR_TYPES + GoalZeroSensor(entry.runtime_data, description) for description in SENSOR_TYPES ) diff --git a/homeassistant/components/goalzero/switch.py b/homeassistant/components/goalzero/switch.py index 9c0aee03b83..daff4ee5fec 100644 --- a/homeassistant/components/goalzero/switch.py +++ b/homeassistant/components/goalzero/switch.py @@ -5,11 +5,10 @@ from __future__ import annotations from typing import Any, cast from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN +from .coordinator import GoalZeroConfigEntry from .entity import GoalZeroEntity SWITCH_TYPES: tuple[SwitchEntityDescription, ...] = ( @@ -29,15 +28,13 @@ SWITCH_TYPES: tuple[SwitchEntityDescription, ...] = ( async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, + entry: GoalZeroConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Goal Zero Yeti switch.""" async_add_entities( - GoalZeroSwitch( - hass.data[DOMAIN][entry.entry_id], - description, - ) - for description in SWITCH_TYPES + GoalZeroSwitch(entry.runtime_data, description) for description in SWITCH_TYPES ) diff --git a/tests/components/goalzero/__init__.py b/tests/components/goalzero/__init__.py index d2e990ca122..30a7c92510e 100644 --- a/tests/components/goalzero/__init__.py +++ b/tests/components/goalzero/__init__.py @@ -3,8 +3,7 @@ from unittest.mock import AsyncMock, patch from homeassistant.components import dhcp -from homeassistant.components.goalzero import DOMAIN -from homeassistant.components.goalzero.const import DEFAULT_NAME +from homeassistant.components.goalzero.const import DEFAULT_NAME, DOMAIN from homeassistant.const import CONF_HOST, CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import format_mac From cd928d5571b60312e2e899a50a0acf61cff6ec5a Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Wed, 12 Jun 2024 17:39:44 +0200 Subject: [PATCH 0577/1445] Support reconfigure flow in Brother integration (#117298) * Add reconfigure flow * Improve config flow * Check if it is the same printer * Improve description * Add tests * Improve strings * Add missing reconfigure_successful string * Improve test names and comments * Format * Mock unload entry * Use add_suggested_values_to_schema() * Do not abort when another device's IP has been used * Remove unnecessary code * Suggested changes --------- Co-authored-by: Maciej Bieniek <478555+bieniu@users.noreply.github.com> --- .../components/brother/config_flow.py | 104 ++++++++++-- homeassistant/components/brother/strings.json | 15 +- tests/components/brother/conftest.py | 11 +- tests/components/brother/test_config_flow.py | 159 +++++++++++++++++- 4 files changed, 266 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/brother/config_flow.py b/homeassistant/components/brother/config_flow.py index 2b711186fff..4536cb9c4d5 100644 --- a/homeassistant/components/brother/config_flow.py +++ b/homeassistant/components/brother/config_flow.py @@ -2,15 +2,16 @@ from __future__ import annotations -from typing import Any +from typing import TYPE_CHECKING, Any from brother import Brother, SnmpError, UnsupportedModelError import voluptuous as vol from homeassistant.components import zeroconf from homeassistant.components.snmp import async_get_snmp_engine -from homeassistant.config_entries import ConfigFlow, ConfigFlowResult +from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_HOST, CONF_TYPE +from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.util.network import is_host_valid @@ -18,10 +19,29 @@ from .const import DOMAIN, PRINTER_TYPES DATA_SCHEMA = vol.Schema( { - vol.Required(CONF_HOST, default=""): str, + vol.Required(CONF_HOST): str, vol.Optional(CONF_TYPE, default="laser"): vol.In(PRINTER_TYPES), } ) +RECONFIGURE_SCHEMA = vol.Schema({vol.Required(CONF_HOST): str}) + + +async def validate_input( + hass: HomeAssistant, user_input: dict[str, Any], expected_mac: str | None = None +) -> tuple[str, str]: + """Validate the user input.""" + if not is_host_valid(user_input[CONF_HOST]): + raise InvalidHost + + snmp_engine = await async_get_snmp_engine(hass) + + brother = await Brother.create(user_input[CONF_HOST], snmp_engine=snmp_engine) + await brother.async_update() + + if expected_mac is not None and brother.serial.lower() != expected_mac: + raise AnotherDevice + + return (brother.model, brother.serial) class BrotherConfigFlow(ConfigFlow, domain=DOMAIN): @@ -33,6 +53,7 @@ class BrotherConfigFlow(ConfigFlow, domain=DOMAIN): """Initialize.""" self.brother: Brother self.host: str | None = None + self.entry: ConfigEntry | None = None async def async_step_user( self, user_input: dict[str, Any] | None = None @@ -42,21 +63,7 @@ class BrotherConfigFlow(ConfigFlow, domain=DOMAIN): if user_input is not None: try: - if not is_host_valid(user_input[CONF_HOST]): - raise InvalidHost - - snmp_engine = await async_get_snmp_engine(self.hass) - - brother = await Brother.create( - user_input[CONF_HOST], snmp_engine=snmp_engine - ) - await brother.async_update() - - await self.async_set_unique_id(brother.serial.lower()) - self._abort_if_unique_id_configured() - - title = f"{brother.model} {brother.serial}" - return self.async_create_entry(title=title, data=user_input) + model, serial = await validate_input(self.hass, user_input) except InvalidHost: errors[CONF_HOST] = "wrong_host" except (ConnectionError, TimeoutError): @@ -65,6 +72,12 @@ class BrotherConfigFlow(ConfigFlow, domain=DOMAIN): errors["base"] = "snmp_error" except UnsupportedModelError: return self.async_abort(reason="unsupported_model") + else: + await self.async_set_unique_id(serial.lower()) + self._abort_if_unique_id_configured() + + title = f"{model} {serial}" + return self.async_create_entry(title=title, data=user_input) return self.async_show_form( step_id="user", data_schema=DATA_SCHEMA, errors=errors @@ -127,6 +140,61 @@ class BrotherConfigFlow(ConfigFlow, domain=DOMAIN): }, ) + async def async_step_reconfigure( + self, _: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle a reconfiguration flow initialized by the user.""" + entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) + + if TYPE_CHECKING: + assert entry is not None + + self.entry = entry + + return await self.async_step_reconfigure_confirm() + + async def async_step_reconfigure_confirm( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle a reconfiguration flow initialized by the user.""" + errors = {} + + if TYPE_CHECKING: + assert self.entry is not None + + if user_input is not None: + try: + await validate_input(self.hass, user_input, self.entry.unique_id) + except InvalidHost: + errors[CONF_HOST] = "wrong_host" + except (ConnectionError, TimeoutError): + errors["base"] = "cannot_connect" + except SnmpError: + errors["base"] = "snmp_error" + except AnotherDevice: + errors["base"] = "another_device" + else: + self.hass.config_entries.async_update_entry( + self.entry, + data=self.entry.data | {CONF_HOST: user_input[CONF_HOST]}, + ) + await self.hass.config_entries.async_reload(self.entry.entry_id) + return self.async_abort(reason="reconfigure_successful") + + return self.async_show_form( + step_id="reconfigure_confirm", + data_schema=self.add_suggested_values_to_schema( + data_schema=RECONFIGURE_SCHEMA, + suggested_values=self.entry.data | (user_input or {}), + ), + description_placeholders={"printer_name": self.entry.title}, + errors=errors, + ) + class InvalidHost(HomeAssistantError): """Error to indicate that hostname/IP address is invalid.""" + + +class AnotherDevice(HomeAssistantError): + """Error to indicate that hostname/IP address belongs to another device.""" diff --git a/homeassistant/components/brother/strings.json b/homeassistant/components/brother/strings.json index 0d8f4f4eedf..d7f8f4a1b89 100644 --- a/homeassistant/components/brother/strings.json +++ b/homeassistant/components/brother/strings.json @@ -17,16 +17,27 @@ "data": { "type": "[%key:component::brother::config::step::user::data::type%]" } + }, + "reconfigure_confirm": { + "description": "Update configuration for {printer_name}.", + "data": { + "host": "[%key:common::config_flow::data::host%]" + }, + "data_description": { + "host": "[%key:component::brother::config::step::user::data_description::host%]" + } } }, "error": { "wrong_host": "Invalid hostname or IP address.", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", - "snmp_error": "SNMP server turned off or printer not supported." + "snmp_error": "SNMP server turned off or printer not supported.", + "another_device": "The IP address or hostname of another Brother printer was used." }, "abort": { "unsupported_model": "This printer model is not supported.", - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]" } }, "entity": { diff --git a/tests/components/brother/conftest.py b/tests/components/brother/conftest.py index 66f92f5907d..5fadca5314d 100644 --- a/tests/components/brother/conftest.py +++ b/tests/components/brother/conftest.py @@ -87,7 +87,16 @@ def mock_setup_entry() -> Generator[AsyncMock]: @pytest.fixture -def mock_brother_client() -> Generator[AsyncMock]: +def mock_unload_entry() -> Generator[AsyncMock, None, None]: + """Override async_unload_entry.""" + with patch( + "homeassistant.components.brother.async_unload_entry", return_value=True + ) as mock_unload_entry: + yield mock_unload_entry + + +@pytest.fixture +def mock_brother_client() -> Generator[AsyncMock, None, None]: """Mock Brother client.""" with ( patch("homeassistant.components.brother.Brother", autospec=True) as mock_client, diff --git a/tests/components/brother/test_config_flow.py b/tests/components/brother/test_config_flow.py index 3a9aff48e90..ac7af4cc912 100644 --- a/tests/components/brother/test_config_flow.py +++ b/tests/components/brother/test_config_flow.py @@ -8,7 +8,11 @@ import pytest from homeassistant.components import zeroconf from homeassistant.components.brother.const import DOMAIN -from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF +from homeassistant.config_entries import ( + SOURCE_RECONFIGURE, + SOURCE_USER, + SOURCE_ZEROCONF, +) from homeassistant.const import CONF_HOST, CONF_TYPE from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -19,7 +23,7 @@ from tests.common import MockConfigEntry CONFIG = {CONF_HOST: "127.0.0.1", CONF_TYPE: "laser"} -pytestmark = pytest.mark.usefixtures("mock_setup_entry") +pytestmark = pytest.mark.usefixtures("mock_setup_entry", "mock_unload_entry") async def test_show_form(hass: HomeAssistant) -> None: @@ -248,3 +252,154 @@ async def test_zeroconf_confirm_create_entry( assert result["title"] == "HL-L2340DW 0123456789" assert result["data"][CONF_HOST] == "127.0.0.1" assert result["data"][CONF_TYPE] == "laser" + + +async def test_reconfigure_successful( + hass: HomeAssistant, + mock_brother_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test starting a reconfigure flow.""" + await init_integration(hass, mock_config_entry) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": SOURCE_RECONFIGURE, + "entry_id": mock_config_entry.entry_id, + }, + data=mock_config_entry.data, + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reconfigure_confirm" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_HOST: "10.10.10.10"}, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reconfigure_successful" + assert mock_config_entry.data == { + CONF_HOST: "10.10.10.10", + CONF_TYPE: "laser", + } + + +@pytest.mark.parametrize( + ("exc", "base_error"), + [ + (ConnectionError, "cannot_connect"), + (TimeoutError, "cannot_connect"), + (SnmpError("error"), "snmp_error"), + ], +) +async def test_reconfigure_not_successful( + hass: HomeAssistant, + exc: Exception, + base_error: str, + mock_brother_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test starting a reconfigure flow but no connection found.""" + await init_integration(hass, mock_config_entry) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": SOURCE_RECONFIGURE, + "entry_id": mock_config_entry.entry_id, + }, + data=mock_config_entry.data, + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reconfigure_confirm" + + mock_brother_client.async_update.side_effect = exc + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_HOST: "10.10.10.10"}, + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reconfigure_confirm" + assert result["errors"] == {"base": base_error} + + mock_brother_client.async_update.side_effect = None + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_HOST: "10.10.10.10"}, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reconfigure_successful" + assert mock_config_entry.data == { + CONF_HOST: "10.10.10.10", + CONF_TYPE: "laser", + } + + +async def test_reconfigure_invalid_hostname( + hass: HomeAssistant, + mock_brother_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test starting a reconfigure flow but no connection found.""" + await init_integration(hass, mock_config_entry) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": SOURCE_RECONFIGURE, + "entry_id": mock_config_entry.entry_id, + }, + data=mock_config_entry.data, + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reconfigure_confirm" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_HOST: "invalid/hostname"}, + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reconfigure_confirm" + assert result["errors"] == {CONF_HOST: "wrong_host"} + + +async def test_reconfigure_not_the_same_device( + hass: HomeAssistant, + mock_brother_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test starting the reconfiguration process, but with a different printer.""" + await init_integration(hass, mock_config_entry) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": SOURCE_RECONFIGURE, + "entry_id": mock_config_entry.entry_id, + }, + data=mock_config_entry.data, + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reconfigure_confirm" + + mock_brother_client.serial = "9876543210" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_HOST: "10.10.10.10"}, + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reconfigure_confirm" + assert result["errors"] == {"base": "another_device"} From b953ff73c07f2b5bf60debc878ae5c6074c70bd6 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 12 Jun 2024 17:42:51 +0200 Subject: [PATCH 0578/1445] Migrate Airzone cloud to runtime_data (#119495) --- .../components/airzone_cloud/__init__.py | 15 ++++++++++----- .../components/airzone_cloud/binary_sensor.py | 9 +++++---- homeassistant/components/airzone_cloud/climate.py | 9 +++++---- .../components/airzone_cloud/diagnostics.py | 8 +++----- homeassistant/components/airzone_cloud/select.py | 9 +++++---- homeassistant/components/airzone_cloud/sensor.py | 9 +++++---- .../components/airzone_cloud/water_heater.py | 9 +++++---- .../components/airzone_cloud/test_diagnostics.py | 1 - tests/components/airzone_cloud/util.py | 2 +- 9 files changed, 39 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/airzone_cloud/__init__.py b/homeassistant/components/airzone_cloud/__init__.py index e53c01e0f81..b1d7900f2e8 100644 --- a/homeassistant/components/airzone_cloud/__init__.py +++ b/homeassistant/components/airzone_cloud/__init__.py @@ -10,7 +10,6 @@ from homeassistant.const import CONF_ID, CONF_PASSWORD, CONF_USERNAME, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import aiohttp_client -from .const import DOMAIN from .coordinator import AirzoneUpdateCoordinator PLATFORMS: list[Platform] = [ @@ -21,8 +20,12 @@ PLATFORMS: list[Platform] = [ Platform.WATER_HEATER, ] +type AirzoneCloudConfigEntry = ConfigEntry[AirzoneUpdateCoordinator] -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + +async def async_setup_entry( + hass: HomeAssistant, entry: AirzoneCloudConfigEntry +) -> bool: """Set up Airzone Cloud from a config entry.""" options = ConnectionOptions( entry.data[CONF_USERNAME], @@ -41,18 +44,20 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: coordinator = AirzoneUpdateCoordinator(hass, airzone) await coordinator.async_config_entry_first_refresh() - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator + entry.runtime_data = coordinator await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_unload_entry( + hass: HomeAssistant, entry: AirzoneCloudConfigEntry +) -> bool: """Unload a config entry.""" if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): - coordinator: AirzoneUpdateCoordinator = hass.data[DOMAIN].pop(entry.entry_id) + coordinator = entry.runtime_data await coordinator.airzone.logout() return unload_ok diff --git a/homeassistant/components/airzone_cloud/binary_sensor.py b/homeassistant/components/airzone_cloud/binary_sensor.py index 9266ee3445e..f235d9b06d0 100644 --- a/homeassistant/components/airzone_cloud/binary_sensor.py +++ b/homeassistant/components/airzone_cloud/binary_sensor.py @@ -21,12 +21,11 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntity, BinarySensorEntityDescription, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN +from . import AirzoneCloudConfigEntry from .coordinator import AirzoneUpdateCoordinator from .entity import ( AirzoneAidooEntity, @@ -94,10 +93,12 @@ ZONE_BINARY_SENSOR_TYPES: Final[tuple[AirzoneBinarySensorEntityDescription, ...] async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, + entry: AirzoneCloudConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Add Airzone Cloud binary sensors from a config_entry.""" - coordinator: AirzoneUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + coordinator = entry.runtime_data binary_sensors: list[AirzoneBinarySensor] = [ AirzoneAidooBinarySensor( diff --git a/homeassistant/components/airzone_cloud/climate.py b/homeassistant/components/airzone_cloud/climate.py index 80f8af36a15..3658c073795 100644 --- a/homeassistant/components/airzone_cloud/climate.py +++ b/homeassistant/components/airzone_cloud/climate.py @@ -53,13 +53,12 @@ from homeassistant.components.climate import ( HVACAction, HVACMode, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN +from . import AirzoneCloudConfigEntry from .coordinator import AirzoneUpdateCoordinator from .entity import ( AirzoneAidooEntity, @@ -119,10 +118,12 @@ HVAC_MODE_HASS_TO_LIB: Final[dict[HVACMode, OperationMode]] = { async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, + entry: AirzoneCloudConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Add Airzone climate from a config_entry.""" - coordinator: AirzoneUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + coordinator = entry.runtime_data entities: list[AirzoneClimate] = [] diff --git a/homeassistant/components/airzone_cloud/diagnostics.py b/homeassistant/components/airzone_cloud/diagnostics.py index 372455a4597..516a8fcb165 100644 --- a/homeassistant/components/airzone_cloud/diagnostics.py +++ b/homeassistant/components/airzone_cloud/diagnostics.py @@ -22,12 +22,10 @@ from aioairzone_cloud.const import ( ) from homeassistant.components.diagnostics.util import async_redact_data -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant -from .const import DOMAIN -from .coordinator import AirzoneUpdateCoordinator +from . import AirzoneCloudConfigEntry TO_REDACT_API = [ API_CITY, @@ -137,10 +135,10 @@ def redact_all( async def async_get_config_entry_diagnostics( - hass: HomeAssistant, config_entry: ConfigEntry + hass: HomeAssistant, config_entry: AirzoneCloudConfigEntry ) -> dict[str, Any]: """Return diagnostics for a config entry.""" - coordinator: AirzoneUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + coordinator = config_entry.runtime_data raw_data = coordinator.airzone.raw_data() ids = gather_ids(raw_data) diff --git a/homeassistant/components/airzone_cloud/select.py b/homeassistant/components/airzone_cloud/select.py index c5c9f664503..9bc0bdd1f5b 100644 --- a/homeassistant/components/airzone_cloud/select.py +++ b/homeassistant/components/airzone_cloud/select.py @@ -14,12 +14,11 @@ from aioairzone_cloud.const import ( ) from homeassistant.components.select import SelectEntity, SelectEntityDescription -from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN +from . import AirzoneCloudConfigEntry from .coordinator import AirzoneUpdateCoordinator from .entity import AirzoneEntity, AirzoneZoneEntity @@ -52,10 +51,12 @@ ZONE_SELECT_TYPES: Final[tuple[AirzoneSelectDescription, ...]] = ( async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, + entry: AirzoneCloudConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Add Airzone Cloud select from a config_entry.""" - coordinator: AirzoneUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + coordinator = entry.runtime_data # Zones async_add_entities( diff --git a/homeassistant/components/airzone_cloud/sensor.py b/homeassistant/components/airzone_cloud/sensor.py index febbbcc7ef6..f5dc2d7f9eb 100644 --- a/homeassistant/components/airzone_cloud/sensor.py +++ b/homeassistant/components/airzone_cloud/sensor.py @@ -23,7 +23,6 @@ from homeassistant.components.sensor import ( SensorEntityDescription, SensorStateClass, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, PERCENTAGE, @@ -34,7 +33,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN +from . import AirzoneCloudConfigEntry from .coordinator import AirzoneUpdateCoordinator from .entity import ( AirzoneAidooEntity, @@ -103,10 +102,12 @@ ZONE_SENSOR_TYPES: Final[tuple[SensorEntityDescription, ...]] = ( async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, + entry: AirzoneCloudConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Add Airzone Cloud sensors from a config_entry.""" - coordinator: AirzoneUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + coordinator = entry.runtime_data # Aidoos sensors: list[AirzoneSensor] = [ diff --git a/homeassistant/components/airzone_cloud/water_heater.py b/homeassistant/components/airzone_cloud/water_heater.py index fd1c772b38a..51228ae6b90 100644 --- a/homeassistant/components/airzone_cloud/water_heater.py +++ b/homeassistant/components/airzone_cloud/water_heater.py @@ -27,12 +27,11 @@ from homeassistant.components.water_heater import ( WaterHeaterEntity, WaterHeaterEntityFeature, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, STATE_OFF, UnitOfTemperature from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN +from . import AirzoneCloudConfigEntry from .coordinator import AirzoneUpdateCoordinator from .entity import AirzoneHotWaterEntity @@ -68,10 +67,12 @@ OPERATION_MODE_TO_DHW_PARAMS: Final[dict[str, dict[str, Any]]] = { async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, + entry: AirzoneCloudConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Add Airzone Cloud Water Heater from a config_entry.""" - coordinator: AirzoneUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + coordinator = entry.runtime_data async_add_entities( AirzoneWaterHeater( diff --git a/tests/components/airzone_cloud/test_diagnostics.py b/tests/components/airzone_cloud/test_diagnostics.py index 2b2e3f33105..254dba16b09 100644 --- a/tests/components/airzone_cloud/test_diagnostics.py +++ b/tests/components/airzone_cloud/test_diagnostics.py @@ -104,7 +104,6 @@ async def test_config_entry_diagnostics( ) -> None: """Test config entry diagnostics.""" await async_init_integration(hass) - assert hass.data[DOMAIN] config_entry = hass.config_entries.async_entries(DOMAIN)[0] with patch( diff --git a/tests/components/airzone_cloud/util.py b/tests/components/airzone_cloud/util.py index 0583fad7c0e..dfd59199a8a 100644 --- a/tests/components/airzone_cloud/util.py +++ b/tests/components/airzone_cloud/util.py @@ -91,7 +91,7 @@ from aioairzone_cloud.const import ( from aioairzone_cloud.device import Device from aioairzone_cloud.webserver import WebServer -from homeassistant.components.airzone_cloud import DOMAIN +from homeassistant.components.airzone_cloud.const import DOMAIN from homeassistant.const import CONF_ID, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant From 4766f48f47160fe29797a3b11acfab2cf51f6132 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 12 Jun 2024 17:44:03 +0200 Subject: [PATCH 0579/1445] Migrate Airzone to runtime_data (#119494) --- homeassistant/components/airzone/__init__.py | 16 +++++++--------- .../components/airzone/binary_sensor.py | 8 +++++--- homeassistant/components/airzone/climate.py | 9 ++++++--- homeassistant/components/airzone/diagnostics.py | 8 +++----- homeassistant/components/airzone/entity.py | 3 ++- homeassistant/components/airzone/select.py | 8 +++++--- homeassistant/components/airzone/sensor.py | 9 ++++++--- homeassistant/components/airzone/water_heater.py | 9 ++++++--- tests/components/airzone/test_diagnostics.py | 1 - tests/components/airzone/util.py | 2 +- 10 files changed, 41 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/airzone/__init__.py b/homeassistant/components/airzone/__init__.py index 1a65b92c3f4..754dfe90dce 100644 --- a/homeassistant/components/airzone/__init__.py +++ b/homeassistant/components/airzone/__init__.py @@ -17,7 +17,6 @@ from homeassistant.helpers import ( entity_registry as er, ) -from .const import DOMAIN from .coordinator import AirzoneUpdateCoordinator PLATFORMS: list[Platform] = [ @@ -30,10 +29,12 @@ PLATFORMS: list[Platform] = [ _LOGGER = logging.getLogger(__name__) +type AirzoneConfigEntry = ConfigEntry[AirzoneUpdateCoordinator] + async def _async_migrate_unique_ids( hass: HomeAssistant, - entry: ConfigEntry, + entry: AirzoneConfigEntry, coordinator: AirzoneUpdateCoordinator, ) -> None: """Migrate entities when the mac address gets discovered.""" @@ -71,7 +72,7 @@ async def _async_migrate_unique_ids( await er.async_migrate_entries(hass, entry.entry_id, _async_migrator) -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: AirzoneConfigEntry) -> bool: """Set up Airzone from a config entry.""" options = ConnectionOptions( entry.data[CONF_HOST], @@ -84,16 +85,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_config_entry_first_refresh() await _async_migrate_unique_ids(hass, entry, coordinator) - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator + entry.runtime_data = coordinator await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: AirzoneConfigEntry) -> bool: """Unload a config entry.""" - if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): - hass.data[DOMAIN].pop(entry.entry_id) - - return unload_ok + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/airzone/binary_sensor.py b/homeassistant/components/airzone/binary_sensor.py index e25751f2a47..20878c08b82 100644 --- a/homeassistant/components/airzone/binary_sensor.py +++ b/homeassistant/components/airzone/binary_sensor.py @@ -25,7 +25,7 @@ from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN +from . import AirzoneConfigEntry from .coordinator import AirzoneUpdateCoordinator from .entity import AirzoneEntity, AirzoneSystemEntity, AirzoneZoneEntity @@ -75,10 +75,12 @@ ZONE_BINARY_SENSOR_TYPES: Final[tuple[AirzoneBinarySensorEntityDescription, ...] async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, + entry: AirzoneConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Add Airzone binary sensors from a config_entry.""" - coordinator = hass.data[DOMAIN][entry.entry_id] + coordinator = entry.runtime_data binary_sensors: list[AirzoneBinarySensor] = [ AirzoneSystemBinarySensor( diff --git a/homeassistant/components/airzone/climate.py b/homeassistant/components/airzone/climate.py index f5b42c4ccbd..33c84b67501 100644 --- a/homeassistant/components/airzone/climate.py +++ b/homeassistant/components/airzone/climate.py @@ -50,7 +50,8 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import API_TEMPERATURE_STEP, DOMAIN, TEMP_UNIT_LIB_TO_HASS +from . import AirzoneConfigEntry +from .const import API_TEMPERATURE_STEP, TEMP_UNIT_LIB_TO_HASS from .coordinator import AirzoneUpdateCoordinator from .entity import AirzoneZoneEntity @@ -97,10 +98,12 @@ HVAC_MODE_HASS_TO_LIB: Final[dict[HVACMode, OperationMode]] = { async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, + entry: AirzoneConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Add Airzone sensors from a config_entry.""" - coordinator = hass.data[DOMAIN][entry.entry_id] + coordinator = entry.runtime_data async_add_entities( AirzoneClimate( coordinator, diff --git a/homeassistant/components/airzone/diagnostics.py b/homeassistant/components/airzone/diagnostics.py index 8c75302d692..6c75b750eaf 100644 --- a/homeassistant/components/airzone/diagnostics.py +++ b/homeassistant/components/airzone/diagnostics.py @@ -7,12 +7,10 @@ from typing import Any from aioairzone.const import API_MAC, AZD_MAC from homeassistant.components.diagnostics.util import async_redact_data -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_UNIQUE_ID from homeassistant.core import HomeAssistant -from .const import DOMAIN -from .coordinator import AirzoneUpdateCoordinator +from . import AirzoneConfigEntry TO_REDACT_API = [ API_MAC, @@ -28,10 +26,10 @@ TO_REDACT_COORD = [ async def async_get_config_entry_diagnostics( - hass: HomeAssistant, config_entry: ConfigEntry + hass: HomeAssistant, config_entry: AirzoneConfigEntry ) -> dict[str, Any]: """Return diagnostics for a config entry.""" - coordinator: AirzoneUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + coordinator = config_entry.runtime_data return { "api_data": async_redact_data(coordinator.airzone.raw_data(), TO_REDACT_API), diff --git a/homeassistant/components/airzone/entity.py b/homeassistant/components/airzone/entity.py index b360db61897..61f79eabf52 100644 --- a/homeassistant/components/airzone/entity.py +++ b/homeassistant/components/airzone/entity.py @@ -31,6 +31,7 @@ from homeassistant.helpers import device_registry as dr from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity +from . import AirzoneConfigEntry from .const import DOMAIN, MANUFACTURER from .coordinator import AirzoneUpdateCoordinator @@ -53,7 +54,7 @@ class AirzoneSystemEntity(AirzoneEntity): def __init__( self, coordinator: AirzoneUpdateCoordinator, - entry: ConfigEntry, + entry: AirzoneConfigEntry, system_data: dict[str, Any], ) -> None: """Initialize.""" diff --git a/homeassistant/components/airzone/select.py b/homeassistant/components/airzone/select.py index 6e92394bb05..8ffe86851b8 100644 --- a/homeassistant/components/airzone/select.py +++ b/homeassistant/components/airzone/select.py @@ -22,7 +22,7 @@ from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN +from . import AirzoneConfigEntry from .coordinator import AirzoneUpdateCoordinator from .entity import AirzoneEntity, AirzoneZoneEntity @@ -79,10 +79,12 @@ ZONE_SELECT_TYPES: Final[tuple[AirzoneSelectDescription, ...]] = ( async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, + entry: AirzoneConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Add Airzone sensors from a config_entry.""" - coordinator = hass.data[DOMAIN][entry.entry_id] + coordinator = entry.runtime_data async_add_entities( AirzoneZoneSelect( diff --git a/homeassistant/components/airzone/sensor.py b/homeassistant/components/airzone/sensor.py index e2f9eabc6f6..7cba0dc515c 100644 --- a/homeassistant/components/airzone/sensor.py +++ b/homeassistant/components/airzone/sensor.py @@ -30,7 +30,8 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN, TEMP_UNIT_LIB_TO_HASS +from . import AirzoneConfigEntry +from .const import TEMP_UNIT_LIB_TO_HASS from .coordinator import AirzoneUpdateCoordinator from .entity import ( AirzoneEntity, @@ -77,10 +78,12 @@ ZONE_SENSOR_TYPES: Final[tuple[SensorEntityDescription, ...]] = ( async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, + entry: AirzoneConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Add Airzone sensors from a config_entry.""" - coordinator = hass.data[DOMAIN][entry.entry_id] + coordinator = entry.runtime_data sensors: list[AirzoneSensor] = [ AirzoneZoneSensor( diff --git a/homeassistant/components/airzone/water_heater.py b/homeassistant/components/airzone/water_heater.py index 4e502776185..ed1c2069c27 100644 --- a/homeassistant/components/airzone/water_heater.py +++ b/homeassistant/components/airzone/water_heater.py @@ -30,7 +30,8 @@ from homeassistant.const import ATTR_TEMPERATURE, STATE_OFF from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN, TEMP_UNIT_LIB_TO_HASS +from . import AirzoneConfigEntry +from .const import TEMP_UNIT_LIB_TO_HASS from .coordinator import AirzoneUpdateCoordinator from .entity import AirzoneHotWaterEntity @@ -56,10 +57,12 @@ OPERATION_MODE_TO_DHW_PARAMS: Final[dict[str, dict[str, Any]]] = { async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, + entry: AirzoneConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Add Airzone sensors from a config_entry.""" - coordinator = hass.data[DOMAIN][entry.entry_id] + coordinator = entry.runtime_data if AZD_HOT_WATER in coordinator.data: async_add_entities([AirzoneWaterHeater(coordinator, entry)]) diff --git a/tests/components/airzone/test_diagnostics.py b/tests/components/airzone/test_diagnostics.py index b64f346f27e..6a03b9f1985 100644 --- a/tests/components/airzone/test_diagnostics.py +++ b/tests/components/airzone/test_diagnostics.py @@ -26,7 +26,6 @@ async def test_config_entry_diagnostics( ) -> None: """Test config entry diagnostics.""" await async_init_integration(hass) - assert hass.data[DOMAIN] config_entry = hass.config_entries.async_entries(DOMAIN)[0] with patch( diff --git a/tests/components/airzone/util.py b/tests/components/airzone/util.py index c5c2d5972d4..6e3e0eccc8f 100644 --- a/tests/components/airzone/util.py +++ b/tests/components/airzone/util.py @@ -55,7 +55,7 @@ from aioairzone.const import ( API_ZONE_ID, ) -from homeassistant.components.airzone import DOMAIN +from homeassistant.components.airzone.const import DOMAIN from homeassistant.const import CONF_HOST, CONF_ID, CONF_PORT from homeassistant.core import HomeAssistant From 3f188b7e27e15459667aff3c7a1adf0b270b3918 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 12 Jun 2024 10:49:18 -0500 Subject: [PATCH 0580/1445] Migrate unifiprotect to use entry.runtime_data (#119507) --- .../components/unifiprotect/__init__.py | 18 +++--- .../components/unifiprotect/binary_sensor.py | 9 ++- .../components/unifiprotect/button.py | 7 +- .../components/unifiprotect/camera.py | 13 ++-- homeassistant/components/unifiprotect/data.py | 64 +++++++++++++++---- .../components/unifiprotect/diagnostics.py | 8 +-- .../components/unifiprotect/light.py | 9 ++- homeassistant/components/unifiprotect/lock.py | 9 ++- .../components/unifiprotect/media_player.py | 9 ++- .../components/unifiprotect/media_source.py | 16 ++--- .../components/unifiprotect/migrate.py | 10 +-- .../components/unifiprotect/number.py | 9 ++- .../components/unifiprotect/repairs.py | 8 +-- .../components/unifiprotect/select.py | 9 ++- .../components/unifiprotect/sensor.py | 9 ++- .../components/unifiprotect/switch.py | 9 ++- homeassistant/components/unifiprotect/text.py | 9 ++- .../components/unifiprotect/utils.py | 10 +-- .../components/unifiprotect/views.py | 20 ++---- .../components/unifiprotect/test_services.py | 24 +++++++ tests/components/unifiprotect/test_views.py | 19 ++++++ 21 files changed, 181 insertions(+), 117 deletions(-) diff --git a/homeassistant/components/unifiprotect/__init__.py b/homeassistant/components/unifiprotect/__init__.py index 0f41011361d..38e45798789 100644 --- a/homeassistant/components/unifiprotect/__init__.py +++ b/homeassistant/components/unifiprotect/__init__.py @@ -15,7 +15,6 @@ from uiprotect.exceptions import ClientError, NotAuthorized # diagnostics module will not be imported in the executor. from uiprotect.test_util.anonymize import anonymize_data # noqa: F401 -from homeassistant.config_entries import ConfigEntry from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady @@ -37,7 +36,7 @@ from .const import ( OUTDATED_LOG_MESSAGE, PLATFORMS, ) -from .data import ProtectData, async_ufp_instance_for_config_entry_ids +from .data import ProtectData, UFPConfigEntry, async_ufp_instance_for_config_entry_ids from .discovery import async_start_discovery from .migrate import async_migrate_data from .services import async_cleanup_services, async_setup_services @@ -62,7 +61,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: return True -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: UFPConfigEntry) -> bool: """Set up the UniFi Protect config entries.""" protect = async_create_api_client(hass, entry) _LOGGER.debug("Connect to UniFi Protect") @@ -107,7 +106,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if entry.unique_id is None: hass.config_entries.async_update_entry(entry, unique_id=nvr_info.mac) - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = data_service + entry.runtime_data = data_service entry.async_on_unload(entry.add_update_listener(_async_options_updated)) entry.async_on_unload( hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, data_service.async_stop) @@ -160,7 +159,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def _async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: UFPConfigEntry, data_service: ProtectData, bootstrap: Bootstrap, ) -> None: @@ -176,25 +175,24 @@ async def _async_setup_entry( hass.http.register_view(VideoProxyView(hass)) -async def _async_options_updated(hass: HomeAssistant, entry: ConfigEntry) -> None: +async def _async_options_updated(hass: HomeAssistant, entry: UFPConfigEntry) -> None: """Update options.""" await hass.config_entries.async_reload(entry.entry_id) -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: UFPConfigEntry) -> bool: """Unload UniFi Protect config entry.""" if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): - data: ProtectData = hass.data[DOMAIN][entry.entry_id] + data = entry.runtime_data await data.async_stop() - hass.data[DOMAIN].pop(entry.entry_id) async_cleanup_services(hass) return bool(unload_ok) async def async_remove_config_entry_device( - hass: HomeAssistant, config_entry: ConfigEntry, device_entry: dr.DeviceEntry + hass: HomeAssistant, config_entry: UFPConfigEntry, device_entry: dr.DeviceEntry ) -> bool: """Remove ufp config entry from a device.""" unifi_macs = { diff --git a/homeassistant/components/unifiprotect/binary_sensor.py b/homeassistant/components/unifiprotect/binary_sensor.py index 7e66f5efb28..c97197fea5e 100644 --- a/homeassistant/components/unifiprotect/binary_sensor.py +++ b/homeassistant/components/unifiprotect/binary_sensor.py @@ -23,14 +23,13 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntity, BinarySensorEntityDescription, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DISPATCH_ADOPT, DOMAIN -from .data import ProtectData +from .const import DISPATCH_ADOPT +from .data import ProtectData, UFPConfigEntry from .entity import ( EventEntityMixin, ProtectDeviceEntity, @@ -614,11 +613,11 @@ DISK_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: UFPConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up binary sensors for UniFi Protect integration.""" - data: ProtectData = hass.data[DOMAIN][entry.entry_id] + data = entry.runtime_data @callback def _add_new_device(device: ProtectAdoptableDeviceModel) -> None: diff --git a/homeassistant/components/unifiprotect/button.py b/homeassistant/components/unifiprotect/button.py index 0db05a6cdc9..009f9b275dc 100644 --- a/homeassistant/components/unifiprotect/button.py +++ b/homeassistant/components/unifiprotect/button.py @@ -13,7 +13,6 @@ from homeassistant.components.button import ( ButtonEntity, ButtonEntityDescription, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_registry as er @@ -21,7 +20,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DEVICES_THAT_ADOPT, DISPATCH_ADD, DISPATCH_ADOPT, DOMAIN -from .data import ProtectData +from .data import ProtectData, UFPConfigEntry from .entity import ProtectDeviceEntity, async_all_device_entities from .models import PermRequired, ProtectSetableKeysMixin, T from .utils import async_dispatch_id as _ufpd @@ -108,11 +107,11 @@ def _async_remove_adopt_button( async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: UFPConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Discover devices on a UniFi Protect NVR.""" - data: ProtectData = hass.data[DOMAIN][entry.entry_id] + data = entry.runtime_data @callback def _add_new_device(device: ProtectAdoptableDeviceModel) -> None: diff --git a/homeassistant/components/unifiprotect/camera.py b/homeassistant/components/unifiprotect/camera.py index 04ac2a823a3..5a703dc5458 100644 --- a/homeassistant/components/unifiprotect/camera.py +++ b/homeassistant/components/unifiprotect/camera.py @@ -16,7 +16,6 @@ from uiprotect.data import ( ) from homeassistant.components.camera import Camera, CameraEntityFeature -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import issue_registry as ir from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -33,7 +32,7 @@ from .const import ( DISPATCH_CHANNELS, DOMAIN, ) -from .data import ProtectData +from .data import ProtectData, UFPConfigEntry from .entity import ProtectDeviceEntity from .utils import async_dispatch_id as _ufpd, get_camera_base_name @@ -42,7 +41,7 @@ _LOGGER = logging.getLogger(__name__) @callback def _create_rtsp_repair( - hass: HomeAssistant, entry: ConfigEntry, data: ProtectData, camera: UFPCamera + hass: HomeAssistant, entry: UFPConfigEntry, data: ProtectData, camera: UFPCamera ) -> None: edit_key = "readonly" if camera.can_write(data.api.bootstrap.auth_user): @@ -68,7 +67,7 @@ def _create_rtsp_repair( @callback def _get_camera_channels( hass: HomeAssistant, - entry: ConfigEntry, + entry: UFPConfigEntry, data: ProtectData, ufp_device: UFPCamera | None = None, ) -> Generator[tuple[UFPCamera, CameraChannel, bool]]: @@ -108,7 +107,7 @@ def _get_camera_channels( def _async_camera_entities( hass: HomeAssistant, - entry: ConfigEntry, + entry: UFPConfigEntry, data: ProtectData, ufp_device: UFPCamera | None = None, ) -> list[ProtectDeviceEntity]: @@ -146,11 +145,11 @@ def _async_camera_entities( async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: UFPConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Discover cameras on a UniFi Protect NVR.""" - data: ProtectData = hass.data[DOMAIN][entry.entry_id] + data = entry.runtime_data @callback def _add_new_device(device: ProtectAdoptableDeviceModel) -> None: diff --git a/homeassistant/components/unifiprotect/data.py b/homeassistant/components/unifiprotect/data.py index 5ca9b5aaeb7..4e63ff01bc7 100644 --- a/homeassistant/components/unifiprotect/data.py +++ b/homeassistant/components/unifiprotect/data.py @@ -45,16 +45,15 @@ from .utils import async_dispatch_id as _ufpd, async_get_devices_by_type _LOGGER = logging.getLogger(__name__) type ProtectDeviceType = ProtectAdoptableDeviceModel | NVR +type UFPConfigEntry = ConfigEntry[ProtectData] @callback -def async_last_update_was_successful(hass: HomeAssistant, entry: ConfigEntry) -> bool: +def async_last_update_was_successful( + hass: HomeAssistant, entry: UFPConfigEntry +) -> bool: """Check if the last update was successful for a config entry.""" - return bool( - DOMAIN in hass.data - and entry.entry_id in hass.data[DOMAIN] - and hass.data[DOMAIN][entry.entry_id].last_update_success - ) + return hasattr(entry, "runtime_data") and entry.runtime_data.last_update_success class ProtectData: @@ -65,7 +64,7 @@ class ProtectData: hass: HomeAssistant, protect: ProtectApiClient, update_interval: timedelta, - entry: ConfigEntry, + entry: UFPConfigEntry, ) -> None: """Initialize an subscriber.""" super().__init__() @@ -316,9 +315,50 @@ def async_ufp_instance_for_config_entry_ids( hass: HomeAssistant, config_entry_ids: set[str] ) -> ProtectApiClient | None: """Find the UFP instance for the config entry ids.""" - domain_data = hass.data[DOMAIN] - for config_entry_id in config_entry_ids: - if config_entry_id in domain_data: - protect_data: ProtectData = domain_data[config_entry_id] - return protect_data.api + return next( + iter( + entry.runtime_data.api + for entry_id in config_entry_ids + if (entry := hass.config_entries.async_get_entry(entry_id)) + ), + None, + ) + + +@callback +def async_get_ufp_entries(hass: HomeAssistant) -> list[UFPConfigEntry]: + """Get all the UFP entries.""" + return cast( + list[UFPConfigEntry], + [ + entry + for entry in hass.config_entries.async_entries( + DOMAIN, include_ignore=True, include_disabled=True + ) + if hasattr(entry, "runtime_data") + ], + ) + + +@callback +def async_get_data_for_nvr_id(hass: HomeAssistant, nvr_id: str) -> ProtectData | None: + """Find the ProtectData instance for the NVR id.""" + return next( + iter( + entry.runtime_data + for entry in async_get_ufp_entries(hass) + if entry.runtime_data.api.bootstrap.nvr.id == nvr_id + ), + None, + ) + + +@callback +def async_get_data_for_entry_id( + hass: HomeAssistant, entry_id: str +) -> ProtectData | None: + """Find the ProtectData instance for a config entry id.""" + if entry := hass.config_entries.async_get_entry(entry_id): + entry = cast(UFPConfigEntry, entry) + return entry.runtime_data return None diff --git a/homeassistant/components/unifiprotect/diagnostics.py b/homeassistant/components/unifiprotect/diagnostics.py index ac651f6138d..b72f35db0b5 100644 --- a/homeassistant/components/unifiprotect/diagnostics.py +++ b/homeassistant/components/unifiprotect/diagnostics.py @@ -6,18 +6,16 @@ from typing import Any, cast from uiprotect.test_util.anonymize import anonymize_data -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from .const import DOMAIN -from .data import ProtectData +from .data import UFPConfigEntry async def async_get_config_entry_diagnostics( - hass: HomeAssistant, config_entry: ConfigEntry + hass: HomeAssistant, config_entry: UFPConfigEntry ) -> dict[str, Any]: """Return diagnostics for a config entry.""" - data: ProtectData = hass.data[DOMAIN][config_entry.entry_id] + data = config_entry.runtime_data bootstrap = cast(dict[str, Any], anonymize_data(data.api.bootstrap.unifi_dict())) return {"bootstrap": bootstrap, "options": dict(config_entry.options)} diff --git a/homeassistant/components/unifiprotect/light.py b/homeassistant/components/unifiprotect/light.py index 18e611f2307..e119a4a59d5 100644 --- a/homeassistant/components/unifiprotect/light.py +++ b/homeassistant/components/unifiprotect/light.py @@ -13,13 +13,12 @@ from uiprotect.data import ( ) from homeassistant.components.light import ATTR_BRIGHTNESS, ColorMode, LightEntity -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DISPATCH_ADOPT, DOMAIN -from .data import ProtectData +from .const import DISPATCH_ADOPT +from .data import UFPConfigEntry from .entity import ProtectDeviceEntity from .utils import async_dispatch_id as _ufpd @@ -28,11 +27,11 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: UFPConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up lights for UniFi Protect integration.""" - data: ProtectData = hass.data[DOMAIN][entry.entry_id] + data = entry.runtime_data @callback def _add_new_device(device: ProtectAdoptableDeviceModel) -> None: diff --git a/homeassistant/components/unifiprotect/lock.py b/homeassistant/components/unifiprotect/lock.py index 6bb1dd7b4ee..4deeafa0782 100644 --- a/homeassistant/components/unifiprotect/lock.py +++ b/homeassistant/components/unifiprotect/lock.py @@ -14,13 +14,12 @@ from uiprotect.data import ( ) from homeassistant.components.lock import LockEntity, LockEntityDescription -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DISPATCH_ADOPT, DOMAIN -from .data import ProtectData +from .const import DISPATCH_ADOPT +from .data import ProtectData, UFPConfigEntry from .entity import ProtectDeviceEntity from .utils import async_dispatch_id as _ufpd @@ -29,11 +28,11 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: UFPConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up locks on a UniFi Protect NVR.""" - data: ProtectData = hass.data[DOMAIN][entry.entry_id] + data = entry.runtime_data @callback def _add_new_device(device: ProtectAdoptableDeviceModel) -> None: diff --git a/homeassistant/components/unifiprotect/media_player.py b/homeassistant/components/unifiprotect/media_player.py index eb17137842b..f3761b5c18a 100644 --- a/homeassistant/components/unifiprotect/media_player.py +++ b/homeassistant/components/unifiprotect/media_player.py @@ -25,14 +25,13 @@ from homeassistant.components.media_player import ( MediaType, async_process_play_media_url, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DISPATCH_ADOPT, DOMAIN -from .data import ProtectData +from .const import DISPATCH_ADOPT +from .data import ProtectData, UFPConfigEntry from .entity import ProtectDeviceEntity from .utils import async_dispatch_id as _ufpd @@ -41,11 +40,11 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: UFPConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Discover cameras with speakers on a UniFi Protect NVR.""" - data: ProtectData = hass.data[DOMAIN][entry.entry_id] + data = entry.runtime_data @callback def _add_new_device(device: ProtectAdoptableDeviceModel) -> None: diff --git a/homeassistant/components/unifiprotect/media_source.py b/homeassistant/components/unifiprotect/media_source.py index 1a67efcfd03..9d94c3ecda7 100644 --- a/homeassistant/components/unifiprotect/media_source.py +++ b/homeassistant/components/unifiprotect/media_source.py @@ -26,7 +26,7 @@ from homeassistant.helpers import entity_registry as er from homeassistant.util import dt as dt_util from .const import DOMAIN -from .data import ProtectData +from .data import ProtectData, async_get_ufp_entries from .views import async_generate_event_video_url, async_generate_thumbnail_url VIDEO_FORMAT = "video/mp4" @@ -89,13 +89,13 @@ def get_ufp_event(event_type: SimpleEventType) -> set[EventType]: async def async_get_media_source(hass: HomeAssistant) -> MediaSource: """Set up UniFi Protect media source.""" - - data_sources: dict[str, ProtectData] = {} - for data in hass.data.get(DOMAIN, {}).values(): - if isinstance(data, ProtectData): - data_sources[data.api.bootstrap.nvr.id] = data - - return ProtectMediaSource(hass, data_sources) + return ProtectMediaSource( + hass, + { + entry.runtime_data.api.bootstrap.nvr.id: entry.runtime_data + for entry in async_get_ufp_entries(hass) + }, + ) @callback diff --git a/homeassistant/components/unifiprotect/migrate.py b/homeassistant/components/unifiprotect/migrate.py index a95341f497a..e469b684518 100644 --- a/homeassistant/components/unifiprotect/migrate.py +++ b/homeassistant/components/unifiprotect/migrate.py @@ -11,13 +11,13 @@ from uiprotect.data import Bootstrap from homeassistant.components.automation import automations_with_entity from homeassistant.components.script import scripts_with_entity -from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_registry as er, issue_registry as ir from homeassistant.helpers.issue_registry import IssueSeverity from .const import DOMAIN +from .data import UFPConfigEntry _LOGGER = logging.getLogger(__name__) @@ -38,7 +38,7 @@ class EntityUsage(TypedDict): @callback def check_if_used( - hass: HomeAssistant, entry: ConfigEntry, entities: dict[str, EntityRef] + hass: HomeAssistant, entry: UFPConfigEntry, entities: dict[str, EntityRef] ) -> dict[str, EntityUsage]: """Check for usages of entities and return them.""" @@ -67,7 +67,7 @@ def check_if_used( @callback def create_repair_if_used( hass: HomeAssistant, - entry: ConfigEntry, + entry: UFPConfigEntry, breaks_in: str, entities: dict[str, EntityRef], ) -> None: @@ -101,7 +101,7 @@ def create_repair_if_used( async def async_migrate_data( hass: HomeAssistant, - entry: ConfigEntry, + entry: UFPConfigEntry, protect: ProtectApiClient, bootstrap: Bootstrap, ) -> None: @@ -113,7 +113,7 @@ async def async_migrate_data( @callback -def async_deprecate_hdr_package(hass: HomeAssistant, entry: ConfigEntry) -> None: +def async_deprecate_hdr_package(hass: HomeAssistant, entry: UFPConfigEntry) -> None: """Check for usages of hdr_mode switch and package sensor and raise repair if it is used. UniFi Protect v3.0.22 changed how HDR works so it is no longer a simple on/off toggle. There is diff --git a/homeassistant/components/unifiprotect/number.py b/homeassistant/components/unifiprotect/number.py index ceb8614e77e..2a8137f50f7 100644 --- a/homeassistant/components/unifiprotect/number.py +++ b/homeassistant/components/unifiprotect/number.py @@ -16,14 +16,13 @@ from uiprotect.data import ( ) from homeassistant.components.number import NumberEntity, NumberEntityDescription -from homeassistant.config_entries import ConfigEntry from homeassistant.const import PERCENTAGE, EntityCategory, UnitOfTime from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DISPATCH_ADOPT, DOMAIN -from .data import ProtectData +from .const import DISPATCH_ADOPT +from .data import ProtectData, UFPConfigEntry from .entity import ProtectDeviceEntity, async_all_device_entities from .models import PermRequired, ProtectSetableKeysMixin, T from .utils import async_dispatch_id as _ufpd @@ -220,11 +219,11 @@ CHIME_NUMBERS: tuple[ProtectNumberEntityDescription, ...] = ( async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: UFPConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up number entities for UniFi Protect integration.""" - data: ProtectData = hass.data[DOMAIN][entry.entry_id] + data = entry.runtime_data @callback def _add_new_device(device: ProtectAdoptableDeviceModel) -> None: diff --git a/homeassistant/components/unifiprotect/repairs.py b/homeassistant/components/unifiprotect/repairs.py index 3cc8967ea0d..0e505f87391 100644 --- a/homeassistant/components/unifiprotect/repairs.py +++ b/homeassistant/components/unifiprotect/repairs.py @@ -11,11 +11,11 @@ import voluptuous as vol from homeassistant import data_entry_flow from homeassistant.components.repairs import ConfirmRepairFlow, RepairsFlow -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import issue_registry as ir from .const import CONF_ALLOW_EA +from .data import UFPConfigEntry from .utils import async_create_api_client @@ -23,9 +23,9 @@ class ProtectRepair(RepairsFlow): """Handler for an issue fixing flow.""" _api: ProtectApiClient - _entry: ConfigEntry + _entry: UFPConfigEntry - def __init__(self, *, api: ProtectApiClient, entry: ConfigEntry) -> None: + def __init__(self, *, api: ProtectApiClient, entry: UFPConfigEntry) -> None: """Create flow.""" self._api = api @@ -128,7 +128,7 @@ class RTSPRepair(ProtectRepair): self, *, api: ProtectApiClient, - entry: ConfigEntry, + entry: UFPConfigEntry, camera_id: str, ) -> None: """Create flow.""" diff --git a/homeassistant/components/unifiprotect/select.py b/homeassistant/components/unifiprotect/select.py index f4a9d58e346..5ba557a8af6 100644 --- a/homeassistant/components/unifiprotect/select.py +++ b/homeassistant/components/unifiprotect/select.py @@ -27,14 +27,13 @@ from uiprotect.data import ( ) from homeassistant.components.select import SelectEntity, SelectEntityDescription -from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DISPATCH_ADOPT, DOMAIN, TYPE_EMPTY_VALUE -from .data import ProtectData +from .const import DISPATCH_ADOPT, TYPE_EMPTY_VALUE +from .data import ProtectData, UFPConfigEntry from .entity import ProtectDeviceEntity, async_all_device_entities from .models import PermRequired, ProtectSetableKeysMixin, T from .utils import async_dispatch_id as _ufpd, async_get_light_motion_current @@ -322,10 +321,10 @@ VIEWER_SELECTS: tuple[ProtectSelectEntityDescription, ...] = ( async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, entry: UFPConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up number entities for UniFi Protect integration.""" - data: ProtectData = hass.data[DOMAIN][entry.entry_id] + data = entry.runtime_data @callback def _add_new_device(device: ProtectAdoptableDeviceModel) -> None: diff --git a/homeassistant/components/unifiprotect/sensor.py b/homeassistant/components/unifiprotect/sensor.py index 00849c095f0..a69e9d48293 100644 --- a/homeassistant/components/unifiprotect/sensor.py +++ b/homeassistant/components/unifiprotect/sensor.py @@ -24,7 +24,6 @@ from homeassistant.components.sensor import ( SensorEntityDescription, SensorStateClass, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( LIGHT_LUX, PERCENTAGE, @@ -40,8 +39,8 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DISPATCH_ADOPT, DOMAIN -from .data import ProtectData +from .const import DISPATCH_ADOPT +from .data import ProtectData, UFPConfigEntry from .entity import ( EventEntityMixin, ProtectDeviceEntity, @@ -612,11 +611,11 @@ VIEWER_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: UFPConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up sensors for UniFi Protect integration.""" - data: ProtectData = hass.data[DOMAIN][entry.entry_id] + data = entry.runtime_data @callback def _add_new_device(device: ProtectAdoptableDeviceModel) -> None: diff --git a/homeassistant/components/unifiprotect/switch.py b/homeassistant/components/unifiprotect/switch.py index 50953e2b8fe..d13c49af882 100644 --- a/homeassistant/components/unifiprotect/switch.py +++ b/homeassistant/components/unifiprotect/switch.py @@ -16,15 +16,14 @@ from uiprotect.data import ( ) from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription -from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity -from .const import DISPATCH_ADOPT, DOMAIN -from .data import ProtectData +from .const import DISPATCH_ADOPT +from .data import ProtectData, UFPConfigEntry from .entity import ProtectDeviceEntity, ProtectNVREntity, async_all_device_entities from .models import PermRequired, ProtectSetableKeysMixin, T from .utils import async_dispatch_id as _ufpd @@ -459,11 +458,11 @@ NVR_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: UFPConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up sensors for UniFi Protect integration.""" - data: ProtectData = hass.data[DOMAIN][entry.entry_id] + data = entry.runtime_data @callback def _add_new_device(device: ProtectAdoptableDeviceModel) -> None: diff --git a/homeassistant/components/unifiprotect/text.py b/homeassistant/components/unifiprotect/text.py index 05e6712fa65..c267419bd6d 100644 --- a/homeassistant/components/unifiprotect/text.py +++ b/homeassistant/components/unifiprotect/text.py @@ -13,14 +13,13 @@ from uiprotect.data import ( ) from homeassistant.components.text import TextEntity, TextEntityDescription -from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DISPATCH_ADOPT, DOMAIN -from .data import ProtectData +from .const import DISPATCH_ADOPT +from .data import ProtectData, UFPConfigEntry from .entity import ProtectDeviceEntity, async_all_device_entities from .models import PermRequired, ProtectSetableKeysMixin, T from .utils import async_dispatch_id as _ufpd @@ -56,11 +55,11 @@ CAMERA: tuple[ProtectTextEntityDescription, ...] = ( async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: UFPConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up sensors for UniFi Protect integration.""" - data: ProtectData = hass.data[DOMAIN][entry.entry_id] + data = entry.runtime_data @callback def _add_new_device(device: ProtectAdoptableDeviceModel) -> None: diff --git a/homeassistant/components/unifiprotect/utils.py b/homeassistant/components/unifiprotect/utils.py index 5a0809ef9ac..ad4c99379c8 100644 --- a/homeassistant/components/unifiprotect/utils.py +++ b/homeassistant/components/unifiprotect/utils.py @@ -7,7 +7,7 @@ import contextlib from enum import Enum from pathlib import Path import socket -from typing import Any +from typing import TYPE_CHECKING, Any from aiohttp import CookieJar from typing_extensions import Generator @@ -21,7 +21,6 @@ from uiprotect.data import ( ProtectAdoptableDeviceModel, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, @@ -41,6 +40,9 @@ from .const import ( ModelType, ) +if TYPE_CHECKING: + from .data import UFPConfigEntry + _SENTINEL = object() @@ -122,7 +124,7 @@ def async_get_light_motion_current(obj: Light) -> str: @callback -def async_dispatch_id(entry: ConfigEntry, dispatch: str) -> str: +def async_dispatch_id(entry: UFPConfigEntry, dispatch: str) -> str: """Generate entry specific dispatch ID.""" return f"{DOMAIN}.{entry.entry_id}.{dispatch}" @@ -130,7 +132,7 @@ def async_dispatch_id(entry: ConfigEntry, dispatch: str) -> str: @callback def async_create_api_client( - hass: HomeAssistant, entry: ConfigEntry + hass: HomeAssistant, entry: UFPConfigEntry ) -> ProtectApiClient: """Create ProtectApiClient from config entry.""" diff --git a/homeassistant/components/unifiprotect/views.py b/homeassistant/components/unifiprotect/views.py index b359fd5d948..00128492c67 100644 --- a/homeassistant/components/unifiprotect/views.py +++ b/homeassistant/components/unifiprotect/views.py @@ -16,8 +16,7 @@ from homeassistant.components.http import HomeAssistantView from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import device_registry as dr, entity_registry as er -from .const import DOMAIN -from .data import ProtectData +from .data import ProtectData, async_get_data_for_entry_id, async_get_data_for_nvr_id _LOGGER = logging.getLogger(__name__) @@ -99,18 +98,13 @@ class ProtectProxyView(HomeAssistantView): def __init__(self, hass: HomeAssistant) -> None: """Initialize a thumbnail proxy view.""" self.hass = hass - self.data = hass.data[DOMAIN] - def _get_data_or_404(self, nvr_id: str) -> ProtectData | web.Response: - all_data: list[ProtectData] = [] - - for entry_id, data in self.data.items(): - if isinstance(data, ProtectData): - if nvr_id == entry_id: - return data - if data.api.bootstrap.nvr.id == nvr_id: - return data - all_data.append(data) + def _get_data_or_404(self, nvr_id_or_entry_id: str) -> ProtectData | web.Response: + if data := ( + async_get_data_for_nvr_id(self.hass, nvr_id_or_entry_id) + or async_get_data_for_entry_id(self.hass, nvr_id_or_entry_id) + ): + return data return _404("Invalid NVR ID") diff --git a/tests/components/unifiprotect/test_services.py b/tests/components/unifiprotect/test_services.py index 0a90a2d5667..b468c2de9a8 100644 --- a/tests/components/unifiprotect/test_services.py +++ b/tests/components/unifiprotect/test_services.py @@ -17,6 +17,7 @@ from homeassistant.components.unifiprotect.services import ( SERVICE_SET_CHIME_PAIRED, SERVICE_SET_DEFAULT_DOORBELL_TEXT, ) +from homeassistant.config_entries import ConfigEntryDisabler from homeassistant.const import ATTR_DEVICE_ID, ATTR_ENTITY_ID, ATTR_NAME from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError @@ -142,6 +143,29 @@ async def test_set_default_doorbell_text( nvr.set_default_doorbell_message.assert_called_once_with("Test Message") +async def test_add_doorbell_text_disabled_config_entry( + hass: HomeAssistant, device: dr.DeviceEntry, ufp: MockUFPFixture +) -> None: + """Test add_doorbell_text service.""" + nvr = ufp.api.bootstrap.nvr + nvr.__fields__["add_custom_doorbell_message"] = Mock(final=False) + nvr.add_custom_doorbell_message = AsyncMock() + + await hass.config_entries.async_set_disabled_by( + ufp.entry.entry_id, ConfigEntryDisabler.USER + ) + await hass.async_block_till_done() + + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + DOMAIN, + SERVICE_ADD_DOORBELL_TEXT, + {ATTR_DEVICE_ID: device.id, ATTR_MESSAGE: "Test Message"}, + blocking=True, + ) + assert not nvr.add_custom_doorbell_message.called + + async def test_set_chime_paired_doorbells( hass: HomeAssistant, entity_registry: er.EntityRegistry, diff --git a/tests/components/unifiprotect/test_views.py b/tests/components/unifiprotect/test_views.py index 6d190eb4dd6..2b80a41b16f 100644 --- a/tests/components/unifiprotect/test_views.py +++ b/tests/components/unifiprotect/test_views.py @@ -149,6 +149,25 @@ async def test_thumbnail_entry_id( ufp.api.get_event_thumbnail.assert_called_with("test_id", width=None, height=None) +async def test_thumbnail_invalid_entry_entry_id( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + ufp: MockUFPFixture, + camera: Camera, +) -> None: + """Test invalid config entry ID in URL.""" + + ufp.api.get_event_thumbnail = AsyncMock(return_value=b"testtest") + + await init_entry(hass, ufp, [camera]) + url = async_generate_thumbnail_url("test_id", "invalid") + + http_client = await hass_client() + response = cast(ClientResponse, await http_client.get(url)) + + assert response.status == 404 + + async def test_video_bad_event( hass: HomeAssistant, ufp: MockUFPFixture, From c3c3a705facf284ea1624f6b7383f78780be9794 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 12 Jun 2024 17:51:08 +0200 Subject: [PATCH 0581/1445] Fix attribute-defined-outside-init pylint warnings in tests (#119471) --- tests/components/flic/test_binary_sensor.py | 1 + tests/components/hdmi_cec/__init__.py | 1 + tests/components/refoss/__init__.py | 1 + tests/components/universal/test_media_player.py | 1 + tests/components/yeelight/__init__.py | 1 + 5 files changed, 5 insertions(+) diff --git a/tests/components/flic/test_binary_sensor.py b/tests/components/flic/test_binary_sensor.py index d2584e4f5a2..44db1d6ea1b 100644 --- a/tests/components/flic/test_binary_sensor.py +++ b/tests/components/flic/test_binary_sensor.py @@ -12,6 +12,7 @@ class _MockFlicClient: self.addresses = button_addresses self.get_info_callback = None self.scan_wizard = None + self.channel = None def close(self): pass diff --git a/tests/components/hdmi_cec/__init__.py b/tests/components/hdmi_cec/__init__.py index 31e09489d4a..5cf8ed18b6a 100644 --- a/tests/components/hdmi_cec/__init__.py +++ b/tests/components/hdmi_cec/__init__.py @@ -21,6 +21,7 @@ class MockHDMIDevice: self.turn_off = Mock() self.send_command = Mock() self.async_send_command = AsyncMock() + self._update = None def __getattr__(self, name): """Get attribute from `_values` if not explicitly set.""" diff --git a/tests/components/refoss/__init__.py b/tests/components/refoss/__init__.py index 51c4261b954..1a3e02dac62 100644 --- a/tests/components/refoss/__init__.py +++ b/tests/components/refoss/__init__.py @@ -22,6 +22,7 @@ class FakeDiscovery: self.mock_devices = {"abc": build_device_mock()} self.last_mock_infos = {} self._listeners = [] + self.sock = None def add_listener(self, listener: Listener) -> None: """Add an event listener.""" diff --git a/tests/components/universal/test_media_player.py b/tests/components/universal/test_media_player.py index 008f7aa5162..6869e025b33 100644 --- a/tests/components/universal/test_media_player.py +++ b/tests/components/universal/test_media_player.py @@ -70,6 +70,7 @@ class MockMediaPlayer(media_player.MediaPlayerEntity): self._media_image_url = None self._shuffle = False self._sound_mode = None + self._repeat = None self.service_calls = { "turn_on": async_mock_service( diff --git a/tests/components/yeelight/__init__.py b/tests/components/yeelight/__init__.py index 8dc2acef416..2de064cf567 100644 --- a/tests/components/yeelight/__init__.py +++ b/tests/components/yeelight/__init__.py @@ -115,6 +115,7 @@ class MockAsyncBulb: self.bulb_type = bulb_type self._async_callback = None self._cannot_connect = cannot_connect + self.capabilities = None async def async_listen(self, callback): """Mock the listener.""" From aaa674955c13e78117e38175b83780abfa771b2b Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Wed, 12 Jun 2024 11:52:10 -0400 Subject: [PATCH 0582/1445] Store runtime data inside the config entry in Dlink (#119442) --- homeassistant/components/dlink/__init__.py | 14 +++++++------- homeassistant/components/dlink/entity.py | 10 ++++------ homeassistant/components/dlink/switch.py | 13 ++++++------- tests/components/dlink/test_switch.py | 2 +- 4 files changed, 18 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/dlink/__init__.py b/homeassistant/components/dlink/__init__.py index 80260643223..212fe2e9e21 100644 --- a/homeassistant/components/dlink/__init__.py +++ b/homeassistant/components/dlink/__init__.py @@ -9,13 +9,15 @@ from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, Platfor from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady -from .const import CONF_USE_LEGACY_PROTOCOL, DOMAIN +from .const import CONF_USE_LEGACY_PROTOCOL from .data import SmartPlugData +type DLinkConfigEntry = ConfigEntry[SmartPlugData] + PLATFORMS = [Platform.SWITCH] -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: DLinkConfigEntry) -> bool: """Set up D-Link Power Plug from a config entry.""" smartplug = await hass.async_add_executor_job( SmartPlug, @@ -27,14 +29,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if not smartplug.authenticated and smartplug.use_legacy_protocol: raise ConfigEntryNotReady("Cannot connect/authenticate") - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = SmartPlugData(smartplug) + entry.runtime_data = SmartPlugData(smartplug) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: DLinkConfigEntry) -> bool: """Unload a config entry.""" - if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): - hass.data[DOMAIN].pop(entry.entry_id) - return unload_ok + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/dlink/entity.py b/homeassistant/components/dlink/entity.py index 2a9ac0e6c12..228dfd168a5 100644 --- a/homeassistant/components/dlink/entity.py +++ b/homeassistant/components/dlink/entity.py @@ -2,14 +2,13 @@ from __future__ import annotations -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_CONNECTIONS from homeassistant.helpers import device_registry as dr from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity import Entity, EntityDescription +from . import DLinkConfigEntry from .const import ATTRIBUTION, DOMAIN, MANUFACTURER -from .data import SmartPlugData class DLinkEntity(Entity): @@ -20,18 +19,17 @@ class DLinkEntity(Entity): def __init__( self, - config_entry: ConfigEntry, - data: SmartPlugData, + config_entry: DLinkConfigEntry, description: EntityDescription, ) -> None: """Initialize a D-Link Power Plug entity.""" - self.data = data + self.data = config_entry.runtime_data self.entity_description = description self._attr_unique_id = f"{config_entry.entry_id}_{description.key}" self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, config_entry.entry_id)}, manufacturer=MANUFACTURER, - model=data.smartplug.model_name, + model=self.data.smartplug.model_name, name=config_entry.title, ) if config_entry.unique_id: diff --git a/homeassistant/components/dlink/switch.py b/homeassistant/components/dlink/switch.py index 36bfe4fb391..54322cc6875 100644 --- a/homeassistant/components/dlink/switch.py +++ b/homeassistant/components/dlink/switch.py @@ -6,12 +6,12 @@ from datetime import timedelta from typing import Any from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import ATTR_TOTAL_CONSUMPTION, DOMAIN +from . import DLinkConfigEntry +from .const import ATTR_TOTAL_CONSUMPTION from .entity import DLinkEntity SCAN_INTERVAL = timedelta(minutes=2) @@ -22,13 +22,12 @@ SWITCH_TYPE = SwitchEntityDescription( async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, + entry: DLinkConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up the D-Link Power Plug switch.""" - async_add_entities( - [SmartPlugSwitch(entry, hass.data[DOMAIN][entry.entry_id], SWITCH_TYPE)], - True, - ) + async_add_entities([SmartPlugSwitch(entry, SWITCH_TYPE)], True) class SmartPlugSwitch(DLinkEntity, SwitchEntity): diff --git a/tests/components/dlink/test_switch.py b/tests/components/dlink/test_switch.py index d070158d9fb..0460a6a918f 100644 --- a/tests/components/dlink/test_switch.py +++ b/tests/components/dlink/test_switch.py @@ -2,7 +2,7 @@ from unittest.mock import patch -from homeassistant.components.dlink import DOMAIN +from homeassistant.components.dlink.const import DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.const import ( ATTR_ENTITY_ID, From 3d1165519d4185b7213cff39f9f6f06bf4afb48d Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 12 Jun 2024 17:53:42 +0200 Subject: [PATCH 0583/1445] Fix broad-exception-raised in component tests (#119467) --- tests/components/derivative/test_config_flow.py | 2 +- tests/components/group/test_config_flow.py | 2 +- .../homeassistant_hardware/test_silabs_multiprotocol_addon.py | 2 +- tests/components/integration/test_config_flow.py | 2 +- tests/components/min_max/test_config_flow.py | 2 +- tests/components/template/test_config_flow.py | 2 +- tests/components/threshold/test_config_flow.py | 2 +- tests/components/tod/test_config_flow.py | 2 +- tests/components/utility_meter/test_config_flow.py | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/components/derivative/test_config_flow.py b/tests/components/derivative/test_config_flow.py index 3db0227c2a6..d111df76ece 100644 --- a/tests/components/derivative/test_config_flow.py +++ b/tests/components/derivative/test_config_flow.py @@ -71,7 +71,7 @@ def get_suggested(schema, key): return None return k.description["suggested_value"] # Wanted key absent from schema - raise Exception + raise KeyError("Wanted key absent from schema") @pytest.mark.parametrize("platform", ["sensor"]) diff --git a/tests/components/group/test_config_flow.py b/tests/components/group/test_config_flow.py index 3aea9d21f0c..c6ee4ae5a87 100644 --- a/tests/components/group/test_config_flow.py +++ b/tests/components/group/test_config_flow.py @@ -205,7 +205,7 @@ def get_suggested(schema, key): return None return k.description["suggested_value"] # Wanted key absent from schema - raise Exception + raise KeyError("Wanted key absent from schema") @pytest.mark.parametrize( diff --git a/tests/components/homeassistant_hardware/test_silabs_multiprotocol_addon.py b/tests/components/homeassistant_hardware/test_silabs_multiprotocol_addon.py index 267bded2970..1df8fa86cf9 100644 --- a/tests/components/homeassistant_hardware/test_silabs_multiprotocol_addon.py +++ b/tests/components/homeassistant_hardware/test_silabs_multiprotocol_addon.py @@ -164,7 +164,7 @@ def get_suggested(schema, key): return None return k.description["suggested_value"] # Wanted key absent from schema - raise Exception + raise KeyError("Wanted key absent from schema") @patch( diff --git a/tests/components/integration/test_config_flow.py b/tests/components/integration/test_config_flow.py index 0f724158362..f8387d85174 100644 --- a/tests/components/integration/test_config_flow.py +++ b/tests/components/integration/test_config_flow.py @@ -75,7 +75,7 @@ def get_suggested(schema, key): return None return k.description["suggested_value"] # Wanted key absent from schema - raise Exception + raise KeyError("Wanted key absent from schema") @pytest.mark.parametrize("platform", ["sensor"]) diff --git a/tests/components/min_max/test_config_flow.py b/tests/components/min_max/test_config_flow.py index 4a408524d09..93f8426e428 100644 --- a/tests/components/min_max/test_config_flow.py +++ b/tests/components/min_max/test_config_flow.py @@ -63,7 +63,7 @@ def get_suggested(schema, key): return None return k.description["suggested_value"] # Wanted key absent from schema - raise Exception + raise KeyError("Wanted key absent from schema") @pytest.mark.parametrize("platform", ["sensor"]) diff --git a/tests/components/template/test_config_flow.py b/tests/components/template/test_config_flow.py index 8c5dda401dd..591fe877cc2 100644 --- a/tests/components/template/test_config_flow.py +++ b/tests/components/template/test_config_flow.py @@ -130,7 +130,7 @@ def get_suggested(schema, key): return None return k.description["suggested_value"] # Wanted key absent from schema - raise Exception + raise KeyError("Wanted key absent from schema") @pytest.mark.parametrize( diff --git a/tests/components/threshold/test_config_flow.py b/tests/components/threshold/test_config_flow.py index ddf870b7a0a..e337c5c41c5 100644 --- a/tests/components/threshold/test_config_flow.py +++ b/tests/components/threshold/test_config_flow.py @@ -93,7 +93,7 @@ def get_suggested(schema, key): return None return k.description["suggested_value"] # Wanted key absent from schema - raise Exception + raise KeyError("Wanted key absent from schema") async def test_options(hass: HomeAssistant) -> None: diff --git a/tests/components/tod/test_config_flow.py b/tests/components/tod/test_config_flow.py index 15c0229c653..81f10061774 100644 --- a/tests/components/tod/test_config_flow.py +++ b/tests/components/tod/test_config_flow.py @@ -63,7 +63,7 @@ def get_suggested(schema, key): return None return k.description["suggested_value"] # Wanted key absent from schema - raise Exception + raise KeyError("Wanted key absent from schema") @pytest.mark.freeze_time("2022-03-16 17:37:00", tz_offset=-7) diff --git a/tests/components/utility_meter/test_config_flow.py b/tests/components/utility_meter/test_config_flow.py index 8aa4afe43b9..eccc1d3e12d 100644 --- a/tests/components/utility_meter/test_config_flow.py +++ b/tests/components/utility_meter/test_config_flow.py @@ -261,7 +261,7 @@ def get_suggested(schema, key): return None return k.description["suggested_value"] # Wanted key absent from schema - raise Exception + raise KeyError("Wanted key absent from schema") async def test_options(hass: HomeAssistant) -> None: From a0c445764c3349da5150171b46be1290f4c53c94 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 12 Jun 2024 17:54:38 +0200 Subject: [PATCH 0584/1445] Ignore super-init-not-called pylint warnings in tests (#119474) --- tests/components/baf/__init__.py | 1 + tests/components/devolo_home_control/mocks.py | 18 +++++++++--------- tests/components/nest/common.py | 2 +- tests/components/plex/test_config_flow.py | 2 +- tests/components/plex/test_init.py | 2 +- 5 files changed, 13 insertions(+), 12 deletions(-) diff --git a/tests/components/baf/__init__.py b/tests/components/baf/__init__.py index 09288c4a874..f1074a87cee 100644 --- a/tests/components/baf/__init__.py +++ b/tests/components/baf/__init__.py @@ -11,6 +11,7 @@ MOCK_NAME = "Living Room Fan" class MockBAFDevice(Device): """A simple mock for a BAF Device.""" + # pylint: disable-next=super-init-not-called def __init__(self, async_wait_available_side_effect=None): """Init simple mock.""" self._async_wait_available_side_effect = async_wait_available_side_effect diff --git a/tests/components/devolo_home_control/mocks.py b/tests/components/devolo_home_control/mocks.py index 422a24c3be0..02823871e0f 100644 --- a/tests/components/devolo_home_control/mocks.py +++ b/tests/components/devolo_home_control/mocks.py @@ -25,7 +25,7 @@ from devolo_home_control_api.publisher.publisher import Publisher class BinarySensorPropertyMock(BinarySensorProperty): """devolo Home Control binary sensor mock.""" - def __init__(self, **kwargs: Any) -> None: + def __init__(self, **kwargs: Any) -> None: # pylint: disable=super-init-not-called """Initialize the mock.""" self._logger = MagicMock() self.element_uid = "Test" @@ -38,7 +38,7 @@ class BinarySensorPropertyMock(BinarySensorProperty): class BinarySwitchPropertyMock(BinarySwitchProperty): """devolo Home Control binary sensor mock.""" - def __init__(self, **kwargs: Any) -> None: + def __init__(self, **kwargs: Any) -> None: # pylint: disable=super-init-not-called """Initialize the mock.""" self._logger = MagicMock() self.element_uid = "Test" @@ -48,7 +48,7 @@ class BinarySwitchPropertyMock(BinarySwitchProperty): class ConsumptionPropertyMock(ConsumptionProperty): """devolo Home Control binary sensor mock.""" - def __init__(self, **kwargs: Any) -> None: + def __init__(self, **kwargs: Any) -> None: # pylint: disable=super-init-not-called """Initialize the mock.""" self._logger = MagicMock() self.element_uid = "devolo.Meter:Test" @@ -61,7 +61,7 @@ class ConsumptionPropertyMock(ConsumptionProperty): class MultiLevelSensorPropertyMock(MultiLevelSensorProperty): """devolo Home Control multi level sensor mock.""" - def __init__(self, **kwargs: Any) -> None: + def __init__(self, **kwargs: Any) -> None: # pylint: disable=super-init-not-called """Initialize the mock.""" self.element_uid = "Test" self.sensor_type = "temperature" @@ -73,7 +73,7 @@ class MultiLevelSensorPropertyMock(MultiLevelSensorProperty): class MultiLevelSwitchPropertyMock(MultiLevelSwitchProperty): """devolo Home Control multi level switch mock.""" - def __init__(self, **kwargs: Any) -> None: + def __init__(self, **kwargs: Any) -> None: # pylint: disable=super-init-not-called """Initialize the mock.""" self.element_uid = "Test" self.min = 4 @@ -85,7 +85,7 @@ class MultiLevelSwitchPropertyMock(MultiLevelSwitchProperty): class SirenPropertyMock(MultiLevelSwitchProperty): """devolo Home Control siren mock.""" - def __init__(self, **kwargs: Any) -> None: + def __init__(self, **kwargs: Any) -> None: # pylint: disable=super-init-not-called """Initialize the mock.""" self.element_uid = "Test" self.max = 0 @@ -98,7 +98,7 @@ class SirenPropertyMock(MultiLevelSwitchProperty): class SettingsMock(SettingsProperty): """devolo Home Control settings mock.""" - def __init__(self, **kwargs: Any) -> None: + def __init__(self, **kwargs: Any) -> None: # pylint: disable=super-init-not-called """Initialize the mock.""" self._logger = MagicMock() self.name = "Test" @@ -109,7 +109,7 @@ class SettingsMock(SettingsProperty): class DeviceMock(Zwave): """devolo Home Control device mock.""" - def __init__(self) -> None: + def __init__(self) -> None: # pylint: disable=super-init-not-called """Initialize the mock.""" self.status = 0 self.brand = "devolo" @@ -250,7 +250,7 @@ class SwitchMock(DeviceMock): class HomeControlMock(HomeControl): """devolo Home Control gateway mock.""" - def __init__(self, **kwargs: Any) -> None: + def __init__(self, **kwargs: Any) -> None: # pylint: disable=super-init-not-called """Initialize the mock.""" self.devices = {} self.publisher = MagicMock() diff --git a/tests/components/nest/common.py b/tests/components/nest/common.py index 693fcae5b87..bbaa92b7b28 100644 --- a/tests/components/nest/common.py +++ b/tests/components/nest/common.py @@ -93,7 +93,7 @@ class FakeSubscriber(GoogleNestSubscriber): stop_calls = 0 - def __init__(self): + def __init__(self): # pylint: disable=super-init-not-called """Initialize Fake Subscriber.""" self._device_manager = DeviceManager() diff --git a/tests/components/plex/test_config_flow.py b/tests/components/plex/test_config_flow.py index a47ea275ddb..08733a7dd17 100644 --- a/tests/components/plex/test_config_flow.py +++ b/tests/components/plex/test_config_flow.py @@ -537,7 +537,7 @@ async def test_manual_config(hass: HomeAssistant, mock_plex_calls) -> None: class WrongCertValidaitionException(requests.exceptions.SSLError): """Mock the exception showing an unmatched error.""" - def __init__(self): + def __init__(self): # pylint: disable=super-init-not-called self.__context__ = ssl.SSLCertVerificationError( "some random message that doesn't match" ) diff --git a/tests/components/plex/test_init.py b/tests/components/plex/test_init.py index 51a6a56ccdb..f718e6c86ad 100644 --- a/tests/components/plex/test_init.py +++ b/tests/components/plex/test_init.py @@ -209,7 +209,7 @@ async def test_setup_when_certificate_changed( class WrongCertHostnameException(requests.exceptions.SSLError): """Mock the exception showing a mismatched hostname.""" - def __init__(self): + def __init__(self): # pylint: disable=super-init-not-called self.__context__ = ssl.SSLCertVerificationError( f"hostname '{old_domain}' doesn't match" ) From 0489d0b396a1780aed320112d012e19d952f69d6 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 12 Jun 2024 17:56:52 +0200 Subject: [PATCH 0585/1445] Fix attribute-defined-outside-init pylint warning in anova tests (#119472) --- tests/components/anova/conftest.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/components/anova/conftest.py b/tests/components/anova/conftest.py index 92f3c8ce6a7..e652893d474 100644 --- a/tests/components/anova/conftest.py +++ b/tests/components/anova/conftest.py @@ -78,7 +78,9 @@ class MockedAnovaWebsocketHandler(AnovaWebsocketHandler): self.ws = MockedAnovaWebsocketStream(self.connect_messages) await self.message_listener() self.ws = MockedAnovaWebsocketStream(self.post_connect_messages) - self.fut = asyncio.ensure_future(self.message_listener()) + # RUF006 ignored as it replicates the parent library + # https://github.com/Lash-L/anova_wifi/issues/35 + asyncio.ensure_future(self.message_listener()) # noqa: RUF006 def anova_api_mock( From 44901bdcd1df8340541689a935ebccfa3251951e Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 12 Jun 2024 17:57:27 +0200 Subject: [PATCH 0586/1445] Fix deprecated-typing-alias pylint warnings in zha tests (#119453) --- tests/components/zha/test_registries.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/tests/components/zha/test_registries.py b/tests/components/zha/test_registries.py index 18253186cf1..2b1c0dcc561 100644 --- a/tests/components/zha/test_registries.py +++ b/tests/components/zha/test_registries.py @@ -2,20 +2,18 @@ from __future__ import annotations -import typing from unittest import mock import pytest +from typing_extensions import Generator import zigpy.quirks as zigpy_quirks from homeassistant.components.zha.binary_sensor import IASZone from homeassistant.components.zha.core import registries from homeassistant.components.zha.core.const import ATTR_QUIRK_ID +from homeassistant.components.zha.entity import ZhaEntity from homeassistant.helpers import entity_registry as er -if typing.TYPE_CHECKING: - from homeassistant.components.zha.core.entity import ZhaEntity - MANUFACTURER = "mock manufacturer" MODEL = "mock model" QUIRK_CLASS = "mock.test.quirk.class" @@ -532,7 +530,7 @@ def test_multi_sensor_match( } -def iter_all_rules() -> typing.Iterable[registries.MatchRule, list[type[ZhaEntity]]]: +def iter_all_rules() -> Generator[tuple[registries.MatchRule, list[type[ZhaEntity]]]]: """Iterate over all match rules and their corresponding entities.""" for rules in registries.ZHA_ENTITIES._strict_registry.values(): From 0f0c2f055339dedab0fc47c93ebd7118322efa82 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 12 Jun 2024 17:58:58 +0200 Subject: [PATCH 0587/1445] Fix redefined-argument-from-local pylint warning in tests (#119475) --- tests/components/mqtt/test_util.py | 4 +- tests/components/netatmo/test_init.py | 16 +-- tests/components/recorder/test_init.py | 4 +- tests/components/sensor/test_recorder.py | 115 +++++++++----------- tests/components/vicare/test_config_flow.py | 2 +- 5 files changed, 67 insertions(+), 74 deletions(-) diff --git a/tests/components/mqtt/test_util.py b/tests/components/mqtt/test_util.py index c485e8a9c27..290f561e1ad 100644 --- a/tests/components/mqtt/test_util.py +++ b/tests/components/mqtt/test_util.py @@ -129,8 +129,8 @@ async def test_return_default_get_file_path( with patch( "homeassistant.components.mqtt.util.TEMP_DIR_NAME", f"home-assistant-mqtt-other-{getrandbits(10):03x}", - ) as mock_temp_dir: - tempdir = Path(tempfile.gettempdir()) / mock_temp_dir + ) as temp_dir_name: + tempdir = Path(tempfile.gettempdir()) / temp_dir_name assert await hass.async_add_executor_job(_get_file_path, tempdir) diff --git a/tests/components/netatmo/test_init.py b/tests/components/netatmo/test_init.py index 8d8dfae9eeb..5fdf4f8ea35 100644 --- a/tests/components/netatmo/test_init.py +++ b/tests/components/netatmo/test_init.py @@ -87,8 +87,8 @@ async def test_setup_component( assert hass.config_entries.async_entries(DOMAIN) assert len(hass.states.async_all()) > 0 - for config_entry in hass.config_entries.async_entries("netatmo"): - await hass.config_entries.async_remove(config_entry.entry_id) + for entry in hass.config_entries.async_entries("netatmo"): + await hass.config_entries.async_remove(entry.entry_id) await hass.async_block_till_done() assert len(hass.states.async_all()) == 0 @@ -160,8 +160,8 @@ async def test_setup_component_with_webhook( await simulate_webhook(hass, webhook_id, FAKE_WEBHOOK) assert hass.states.get(climate_entity_livingroom).state == "heat" - for config_entry in hass.config_entries.async_entries("netatmo"): - await hass.config_entries.async_remove(config_entry.entry_id) + for entry in hass.config_entries.async_entries("netatmo"): + await hass.config_entries.async_remove(entry.entry_id) await hass.async_block_till_done() assert len(hass.states.async_all()) == 0 @@ -246,8 +246,8 @@ async def test_setup_with_cloud( await hass.async_block_till_done() assert hass.config_entries.async_entries(DOMAIN) - for config_entry in hass.config_entries.async_entries("netatmo"): - await hass.config_entries.async_remove(config_entry.entry_id) + for entry in hass.config_entries.async_entries("netatmo"): + await hass.config_entries.async_remove(entry.entry_id) fake_delete_cloudhook.assert_called_once() await hass.async_block_till_done() @@ -479,8 +479,8 @@ async def test_setup_component_invalid_token( notifications = async_get_persistent_notifications(hass) assert len(notifications) > 0 - for config_entry in hass.config_entries.async_entries("netatmo"): - await hass.config_entries.async_remove(config_entry.entry_id) + for entry in hass.config_entries.async_entries("netatmo"): + await hass.config_entries.async_remove(entry.entry_id) async def test_devices( diff --git a/tests/components/recorder/test_init.py b/tests/components/recorder/test_init.py index bb449cf279a..300d338fcb3 100644 --- a/tests/components/recorder/test_init.py +++ b/tests/components/recorder/test_init.py @@ -694,7 +694,7 @@ async def test_saving_event_exclude_event_type( await async_wait_recording_done(hass) - def _get_events(hass: HomeAssistant, event_types: list[str]) -> list[Event]: + def _get_events(hass: HomeAssistant, event_type_list: list[str]) -> list[Event]: with session_scope(hass=hass, read_only=True) as session: events = [] for event, event_data, event_types in ( @@ -703,7 +703,7 @@ async def test_saving_event_exclude_event_type( EventTypes, (Events.event_type_id == EventTypes.event_type_id) ) .outerjoin(EventData, Events.data_id == EventData.data_id) - .where(EventTypes.event_type.in_(event_types)) + .where(EventTypes.event_type.in_(event_type_list)) ): event = cast(Events, event) event_data = cast(EventData, event_data) diff --git a/tests/components/sensor/test_recorder.py b/tests/components/sensor/test_recorder.py index 896742d87c3..62cb66d2053 100644 --- a/tests/components/sensor/test_recorder.py +++ b/tests/components/sensor/test_recorder.py @@ -3742,69 +3742,62 @@ async def test_compile_statistics_hourly_daily_monthly_summary( "sensor.test4": None, } start = zero - with freeze_time(start) as freezer: - for i in range(24): - seq = [-10, 15, 30] - # test1 has same value in every period - four, _states = await async_record_states( - hass, freezer, start, "sensor.test1", attributes, seq + for i in range(24): + seq = [-10, 15, 30] + # test1 has same value in every period + four, _states = await async_record_states( + hass, freezer, start, "sensor.test1", attributes, seq + ) + states["sensor.test1"] += _states["sensor.test1"] + last_state = last_states["sensor.test1"] + expected_minima["sensor.test1"].append(_min(seq, last_state)) + expected_maxima["sensor.test1"].append(_max(seq, last_state)) + expected_averages["sensor.test1"].append(_weighted_average(seq, i, last_state)) + last_states["sensor.test1"] = seq[-1] + # test2 values change: min/max at the last state + seq = [-10 * (i + 1), 15 * (i + 1), 30 * (i + 1)] + four, _states = await async_record_states( + hass, freezer, start, "sensor.test2", attributes, seq + ) + states["sensor.test2"] += _states["sensor.test2"] + last_state = last_states["sensor.test2"] + expected_minima["sensor.test2"].append(_min(seq, last_state)) + expected_maxima["sensor.test2"].append(_max(seq, last_state)) + expected_averages["sensor.test2"].append(_weighted_average(seq, i, last_state)) + last_states["sensor.test2"] = seq[-1] + # test3 values change: min/max at the first state + seq = [-10 * (23 - i + 1), 15 * (23 - i + 1), 30 * (23 - i + 1)] + four, _states = await async_record_states( + hass, freezer, start, "sensor.test3", attributes, seq + ) + states["sensor.test3"] += _states["sensor.test3"] + last_state = last_states["sensor.test3"] + expected_minima["sensor.test3"].append(_min(seq, last_state)) + expected_maxima["sensor.test3"].append(_max(seq, last_state)) + expected_averages["sensor.test3"].append(_weighted_average(seq, i, last_state)) + last_states["sensor.test3"] = seq[-1] + # test4 values grow + seq = [i, i + 0.5, i + 0.75] + start_meter = start + for j in range(len(seq)): + _states = await async_record_meter_state( + hass, + freezer, + start_meter, + "sensor.test4", + sum_attributes, + seq[j : j + 1], ) - states["sensor.test1"] += _states["sensor.test1"] - last_state = last_states["sensor.test1"] - expected_minima["sensor.test1"].append(_min(seq, last_state)) - expected_maxima["sensor.test1"].append(_max(seq, last_state)) - expected_averages["sensor.test1"].append( - _weighted_average(seq, i, last_state) - ) - last_states["sensor.test1"] = seq[-1] - # test2 values change: min/max at the last state - seq = [-10 * (i + 1), 15 * (i + 1), 30 * (i + 1)] - four, _states = await async_record_states( - hass, freezer, start, "sensor.test2", attributes, seq - ) - states["sensor.test2"] += _states["sensor.test2"] - last_state = last_states["sensor.test2"] - expected_minima["sensor.test2"].append(_min(seq, last_state)) - expected_maxima["sensor.test2"].append(_max(seq, last_state)) - expected_averages["sensor.test2"].append( - _weighted_average(seq, i, last_state) - ) - last_states["sensor.test2"] = seq[-1] - # test3 values change: min/max at the first state - seq = [-10 * (23 - i + 1), 15 * (23 - i + 1), 30 * (23 - i + 1)] - four, _states = await async_record_states( - hass, freezer, start, "sensor.test3", attributes, seq - ) - states["sensor.test3"] += _states["sensor.test3"] - last_state = last_states["sensor.test3"] - expected_minima["sensor.test3"].append(_min(seq, last_state)) - expected_maxima["sensor.test3"].append(_max(seq, last_state)) - expected_averages["sensor.test3"].append( - _weighted_average(seq, i, last_state) - ) - last_states["sensor.test3"] = seq[-1] - # test4 values grow - seq = [i, i + 0.5, i + 0.75] - start_meter = start - for j in range(len(seq)): - _states = await async_record_meter_state( - hass, - freezer, - start_meter, - "sensor.test4", - sum_attributes, - seq[j : j + 1], - ) - start_meter += timedelta(minutes=1) - states["sensor.test4"] += _states["sensor.test4"] - last_state = last_states["sensor.test4"] - expected_states["sensor.test4"].append(seq[-1]) - expected_sums["sensor.test4"].append( - _sum(seq, last_state, expected_sums["sensor.test4"]) - ) - last_states["sensor.test4"] = seq[-1] + start_meter += timedelta(minutes=1) + states["sensor.test4"] += _states["sensor.test4"] + last_state = last_states["sensor.test4"] + expected_states["sensor.test4"].append(seq[-1]) + expected_sums["sensor.test4"].append( + _sum(seq, last_state, expected_sums["sensor.test4"]) + ) + last_states["sensor.test4"] = seq[-1] - start += timedelta(minutes=5) + start += timedelta(minutes=5) await async_wait_recording_done(hass) hist = history.get_significant_states( hass, diff --git a/tests/components/vicare/test_config_flow.py b/tests/components/vicare/test_config_flow.py index edef1606572..b823bb72dc9 100644 --- a/tests/components/vicare/test_config_flow.py +++ b/tests/components/vicare/test_config_flow.py @@ -81,7 +81,7 @@ async def test_user_create_entry( with patch( f"{MODULE}.config_flow.vicare_login", return_value=None, - ) as mock_setup_entry: + ): result = await hass.config_entries.flow.async_configure( result["flow_id"], VALID_CONFIG, From db3029dc5ff3b465917ae54389b9d31b221a321d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 12 Jun 2024 11:07:20 -0500 Subject: [PATCH 0588/1445] Remove unreachable sensor code in unifiprotect (#119501) --- .../components/unifiprotect/sensor.py | 56 ++++++++----------- tests/components/unifiprotect/test_sensor.py | 4 +- 2 files changed, 26 insertions(+), 34 deletions(-) diff --git a/homeassistant/components/unifiprotect/sensor.py b/homeassistant/components/unifiprotect/sensor.py index a69e9d48293..95b01710b9b 100644 --- a/homeassistant/components/unifiprotect/sensor.py +++ b/homeassistant/components/unifiprotect/sensor.py @@ -520,7 +520,7 @@ NVR_DISABLED_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( ), ) -EVENT_SENSORS: tuple[ProtectSensorEventEntityDescription, ...] = ( +LICENSE_PLATE_EVENT_SENSORS: tuple[ProtectSensorEventEntityDescription, ...] = ( ProtectSensorEventEntityDescription( key="smart_obj_licenseplate", name="License Plate Detected", @@ -678,11 +678,11 @@ def _async_event_entities( if not device.feature_flags.has_smart_detect: continue - for event_desc in EVENT_SENSORS: + for event_desc in LICENSE_PLATE_EVENT_SENSORS: if not event_desc.has_required(device): continue - entities.append(ProtectEventSensor(data, device, event_desc)) + entities.append(ProtectLicensePlateEventSensor(data, device, event_desc)) _LOGGER.debug( "Adding sensor entity %s for %s", description.name, @@ -750,35 +750,6 @@ class ProtectEventSensor(EventEntityMixin, SensorEntity): entity_description: ProtectSensorEventEntityDescription - @callback - def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None: - # do not call ProtectDeviceSensor method since we want event to get value here - EventEntityMixin._async_update_device_from_protect(self, device) # noqa: SLF001 - event = self._event - entity_description = self.entity_description - is_on = entity_description.get_is_on(self.device, self._event) - is_license_plate = ( - entity_description.ufp_event_obj == "last_license_plate_detect_event" - ) - if ( - not is_on - or event is None - or ( - is_license_plate - and (event.metadata is None or event.metadata.license_plate is None) - ) - ): - self._attr_native_value = OBJECT_TYPE_NONE - self._event = None - self._attr_extra_state_attributes = {} - return - - if is_license_plate: - # type verified above - self._attr_native_value = event.metadata.license_plate.name # type: ignore[union-attr] - else: - self._attr_native_value = event.smart_detect_types[0].value - @callback def _async_get_state_attrs(self) -> tuple[Any, ...]: """Retrieve data that goes into the current state of the entity. @@ -792,3 +763,24 @@ class ProtectEventSensor(EventEntityMixin, SensorEntity): self._attr_native_value, self._attr_extra_state_attributes, ) + + +class ProtectLicensePlateEventSensor(ProtectEventSensor): + """A UniFi Protect license plate sensor.""" + + @callback + def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None: + super()._async_update_device_from_protect(device) + event = self._event + entity_description = self.entity_description + if ( + event is None + or (event.metadata is None or event.metadata.license_plate is None) + or not entity_description.get_is_on(self.device, event) + ): + self._attr_native_value = OBJECT_TYPE_NONE + self._event = None + self._attr_extra_state_attributes = {} + return + + self._attr_native_value = event.metadata.license_plate.name diff --git a/tests/components/unifiprotect/test_sensor.py b/tests/components/unifiprotect/test_sensor.py index 1ba3641ba36..72915936a70 100644 --- a/tests/components/unifiprotect/test_sensor.py +++ b/tests/components/unifiprotect/test_sensor.py @@ -14,7 +14,7 @@ from homeassistant.components.unifiprotect.sensor import ( ALL_DEVICES_SENSORS, CAMERA_DISABLED_SENSORS, CAMERA_SENSORS, - EVENT_SENSORS, + LICENSE_PLATE_EVENT_SENSORS, MOTION_TRIP_SENSORS, NVR_DISABLED_SENSORS, NVR_SENSORS, @@ -514,7 +514,7 @@ async def test_camera_update_licenseplate( assert_entity_counts(hass, Platform.SENSOR, 23, 13) _, entity_id = ids_from_device_description( - Platform.SENSOR, camera, EVENT_SENSORS[0] + Platform.SENSOR, camera, LICENSE_PLATE_EVENT_SENSORS[0] ) event_metadata = EventMetadata( From 2f5f372f6385928e12f9ec600d6b9c5b857fd39b Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 12 Jun 2024 18:08:01 +0200 Subject: [PATCH 0589/1445] Remove pointless TODO in recorder tests (#119490) --- tests/components/recorder/test_websocket_api.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/components/recorder/test_websocket_api.py b/tests/components/recorder/test_websocket_api.py index d915eeeeeb6..cc187a1e6ad 100644 --- a/tests/components/recorder/test_websocket_api.py +++ b/tests/components/recorder/test_websocket_api.py @@ -2816,7 +2816,7 @@ async def test_import_statistics( }, ] } - statistic_ids = list_statistic_ids(hass) # TODO + statistic_ids = list_statistic_ids(hass) assert statistic_ids == [ { "display_unit_of_measurement": "kWh", @@ -3034,7 +3034,7 @@ async def test_adjust_sum_statistics_energy( }, ] } - statistic_ids = list_statistic_ids(hass) # TODO + statistic_ids = list_statistic_ids(hass) assert statistic_ids == [ { "display_unit_of_measurement": "kWh", @@ -3227,7 +3227,7 @@ async def test_adjust_sum_statistics_gas( }, ] } - statistic_ids = list_statistic_ids(hass) # TODO + statistic_ids = list_statistic_ids(hass) assert statistic_ids == [ { "display_unit_of_measurement": "m³", From b92372c4ca1ed7d14b863faafed67a2ea380e5e6 Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Wed, 12 Jun 2024 18:08:44 +0200 Subject: [PATCH 0590/1445] Partially revert "Add more debug logging to Ping integration" (#119487) --- homeassistant/components/ping/helpers.py | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/ping/helpers.py b/homeassistant/components/ping/helpers.py index 7f1696d2ed9..82ebf4532da 100644 --- a/homeassistant/components/ping/helpers.py +++ b/homeassistant/components/ping/helpers.py @@ -9,7 +9,6 @@ from typing import TYPE_CHECKING, Any from icmplib import NameLookupError, async_ping from homeassistant.core import HomeAssistant -from homeassistant.helpers.update_coordinator import UpdateFailed from .const import ICMP_TIMEOUT, PING_TIMEOUT @@ -59,9 +58,10 @@ class PingDataICMPLib(PingData): timeout=ICMP_TIMEOUT, privileged=self._privileged, ) - except NameLookupError as err: + except NameLookupError: + _LOGGER.debug("Error resolving host: %s", self.ip_address) self.is_alive = False - raise UpdateFailed(f"Error resolving host: {self.ip_address}") from err + return _LOGGER.debug( "async_ping returned: reachable=%s sent=%i received=%s", @@ -152,17 +152,22 @@ class PingDataSubProcess(PingData): if TYPE_CHECKING: assert match is not None rtt_min, rtt_avg, rtt_max, rtt_mdev = match.groups() - except TimeoutError as err: + except TimeoutError: + _LOGGER.debug( + "Timed out running command: `%s`, after: %s", + " ".join(self._ping_cmd), + self._count + PING_TIMEOUT, + ) + if pinger: with suppress(TypeError): await pinger.kill() # type: ignore[func-returns-value] del pinger - raise UpdateFailed( - f"Timed out running command: `{self._ping_cmd}`, after: {self._count + PING_TIMEOUT}s" - ) from err + return None except AttributeError as err: - raise UpdateFailed from err + _LOGGER.debug("Error matching ping output: %s", err) + return None return {"min": rtt_min, "avg": rtt_avg, "max": rtt_max, "mdev": rtt_mdev} async def async_update(self) -> None: From 4fb8202de18f84933d084d97f7897c6cf610e848 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 12 Jun 2024 11:11:59 -0500 Subject: [PATCH 0591/1445] Refactor adding entities to unifiprotect (#119512) --- .../components/unifiprotect/binary_sensor.py | 23 ++- .../components/unifiprotect/button.py | 17 ++- .../components/unifiprotect/entity.py | 139 ++++++++---------- .../components/unifiprotect/number.py | 23 +-- .../components/unifiprotect/select.py | 27 ++-- .../components/unifiprotect/sensor.py | 24 +-- .../components/unifiprotect/switch.py | 32 ++-- homeassistant/components/unifiprotect/text.py | 14 +- 8 files changed, 145 insertions(+), 154 deletions(-) diff --git a/homeassistant/components/unifiprotect/binary_sensor.py b/homeassistant/components/unifiprotect/binary_sensor.py index c97197fea5e..f42d2d09211 100644 --- a/homeassistant/components/unifiprotect/binary_sensor.py +++ b/homeassistant/components/unifiprotect/binary_sensor.py @@ -2,6 +2,7 @@ from __future__ import annotations +from collections.abc import Sequence import dataclasses import logging from typing import Any @@ -610,6 +611,14 @@ DISK_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( ), ) +_MODEL_DESCRIPTIONS: dict[ModelType, Sequence[ProtectRequiredKeysMixin]] = { + ModelType.CAMERA: CAMERA_SENSORS, + ModelType.LIGHT: LIGHT_SENSORS, + ModelType.SENSOR: SENSE_SENSORS, + ModelType.DOORLOCK: DOORLOCK_SENSORS, + ModelType.VIEWPORT: VIEWER_SENSORS, +} + async def async_setup_entry( hass: HomeAssistant, @@ -624,11 +633,7 @@ async def async_setup_entry( entities: list[ProtectDeviceEntity] = async_all_device_entities( data, ProtectDeviceBinarySensor, - camera_descs=CAMERA_SENSORS, - light_descs=LIGHT_SENSORS, - sense_descs=SENSE_SENSORS, - lock_descs=DOORLOCK_SENSORS, - viewer_descs=VIEWER_SENSORS, + model_descriptions=_MODEL_DESCRIPTIONS, ufp_device=device, ) if device.is_adopted and isinstance(device, Camera): @@ -640,13 +645,7 @@ async def async_setup_entry( ) entities: list[ProtectDeviceEntity] = async_all_device_entities( - data, - ProtectDeviceBinarySensor, - camera_descs=CAMERA_SENSORS, - light_descs=LIGHT_SENSORS, - sense_descs=SENSE_SENSORS, - lock_descs=DOORLOCK_SENSORS, - viewer_descs=VIEWER_SENSORS, + data, ProtectDeviceBinarySensor, model_descriptions=_MODEL_DESCRIPTIONS ) entities += _async_event_entities(data) entities += _async_nvr_entities(data) diff --git a/homeassistant/components/unifiprotect/button.py b/homeassistant/components/unifiprotect/button.py index 009f9b275dc..98d226e9e76 100644 --- a/homeassistant/components/unifiprotect/button.py +++ b/homeassistant/components/unifiprotect/button.py @@ -2,11 +2,12 @@ from __future__ import annotations +from collections.abc import Sequence from dataclasses import dataclass import logging from typing import Final -from uiprotect.data import ProtectAdoptableDeviceModel, ProtectModelWithId +from uiprotect.data import ModelType, ProtectAdoptableDeviceModel, ProtectModelWithId from homeassistant.components.button import ( ButtonDeviceClass, @@ -22,7 +23,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DEVICES_THAT_ADOPT, DISPATCH_ADD, DISPATCH_ADOPT, DOMAIN from .data import ProtectData, UFPConfigEntry from .entity import ProtectDeviceEntity, async_all_device_entities -from .models import PermRequired, ProtectSetableKeysMixin, T +from .models import PermRequired, ProtectRequiredKeysMixin, ProtectSetableKeysMixin, T from .utils import async_dispatch_id as _ufpd _LOGGER = logging.getLogger(__name__) @@ -94,6 +95,12 @@ CHIME_BUTTONS: tuple[ProtectButtonEntityDescription, ...] = ( ) +_MODEL_DESCRIPTIONS: dict[ModelType, Sequence[ProtectRequiredKeysMixin]] = { + ModelType.CHIME: CHIME_BUTTONS, + ModelType.SENSOR: SENSOR_BUTTONS, +} + + @callback def _async_remove_adopt_button( hass: HomeAssistant, device: ProtectAdoptableDeviceModel @@ -120,8 +127,7 @@ async def async_setup_entry( ProtectButton, all_descs=ALL_DEVICE_BUTTONS, unadopted_descs=[ADOPT_BUTTON], - chime_descs=CHIME_BUTTONS, - sense_descs=SENSOR_BUTTONS, + model_descriptions=_MODEL_DESCRIPTIONS, ufp_device=device, ) async_add_entities(entities) @@ -155,8 +161,7 @@ async def async_setup_entry( ProtectButton, all_descs=ALL_DEVICE_BUTTONS, unadopted_descs=[ADOPT_BUTTON], - chime_descs=CHIME_BUTTONS, - sense_descs=SENSOR_BUTTONS, + model_descriptions=_MODEL_DESCRIPTIONS, ) async_add_entities(entities) diff --git a/homeassistant/components/unifiprotect/entity.py b/homeassistant/components/unifiprotect/entity.py index 766c93949bd..137f8c532ee 100644 --- a/homeassistant/components/unifiprotect/entity.py +++ b/homeassistant/components/unifiprotect/entity.py @@ -8,17 +8,11 @@ from typing import TYPE_CHECKING, Any from uiprotect.data import ( NVR, - Camera, - Chime, - Doorlock, Event, - Light, ModelType, ProtectAdoptableDeviceModel, ProtectModelWithId, - Sensor, StateType, - Viewer, ) from homeassistant.core import callback @@ -46,7 +40,7 @@ def _async_device_entities( klass: type[ProtectDeviceEntity], model_type: ModelType, descs: Sequence[ProtectRequiredKeysMixin], - unadopted_descs: Sequence[ProtectRequiredKeysMixin], + unadopted_descs: Sequence[ProtectRequiredKeysMixin] | None = None, ufp_device: ProtectAdoptableDeviceModel | None = None, ) -> list[ProtectDeviceEntity]: if not descs and not unadopted_descs: @@ -58,37 +52,36 @@ def _async_device_entities( if ufp_device is not None else data.get_by_types({model_type}, ignore_unadopted=False) ) + auth_user = data.api.bootstrap.auth_user for device in devices: if TYPE_CHECKING: - assert isinstance(device, (Camera, Light, Sensor, Viewer, Doorlock, Chime)) + assert isinstance(device, ProtectAdoptableDeviceModel) if not device.is_adopted_by_us: - for description in unadopted_descs: - entities.append( - klass( - data, - device=device, - description=description, + if unadopted_descs: + for description in unadopted_descs: + entities.append( + klass( + data, + device=device, + description=description, + ) + ) + _LOGGER.debug( + "Adding %s entity %s for %s", + klass.__name__, + description.name, + device.display_name, ) - ) - _LOGGER.debug( - "Adding %s entity %s for %s", - klass.__name__, - description.name, - device.display_name, - ) continue - can_write = device.can_write(data.api.bootstrap.auth_user) + can_write = device.can_write(auth_user) for description in descs: - if description.ufp_perm is not None: - if description.ufp_perm is PermRequired.WRITE and not can_write: + if (perms := description.ufp_perm) is not None: + if perms is PermRequired.WRITE and not can_write: continue - if description.ufp_perm is PermRequired.NO_WRITE and can_write: + if perms is PermRequired.NO_WRITE and can_write: continue - if ( - description.ufp_perm is PermRequired.DELETE - and not device.can_delete(data.api.bootstrap.auth_user) - ): + if perms is PermRequired.DELETE and not device.can_delete(auth_user): continue if not description.has_required(device): @@ -111,70 +104,54 @@ def _async_device_entities( return entities +_ALL_MODEL_TYPES = ( + ModelType.CAMERA, + ModelType.LIGHT, + ModelType.SENSOR, + ModelType.VIEWPORT, + ModelType.DOORLOCK, + ModelType.CHIME, +) + + +@callback +def _combine_model_descs( + model_type: ModelType, + model_descriptions: dict[ModelType, Sequence[ProtectRequiredKeysMixin]] | None, + all_descs: Sequence[ProtectRequiredKeysMixin] | None, +) -> list[ProtectRequiredKeysMixin]: + """Combine all the descriptions with descriptions a model type.""" + descs: list[ProtectRequiredKeysMixin] = list(all_descs) if all_descs else [] + if model_descriptions and (model_descs := model_descriptions.get(model_type)): + descs.extend(model_descs) + return descs + + @callback def async_all_device_entities( data: ProtectData, klass: type[ProtectDeviceEntity], - camera_descs: Sequence[ProtectRequiredKeysMixin] | None = None, - light_descs: Sequence[ProtectRequiredKeysMixin] | None = None, - sense_descs: Sequence[ProtectRequiredKeysMixin] | None = None, - viewer_descs: Sequence[ProtectRequiredKeysMixin] | None = None, - lock_descs: Sequence[ProtectRequiredKeysMixin] | None = None, - chime_descs: Sequence[ProtectRequiredKeysMixin] | None = None, + model_descriptions: dict[ModelType, Sequence[ProtectRequiredKeysMixin]] + | None = None, all_descs: Sequence[ProtectRequiredKeysMixin] | None = None, - unadopted_descs: Sequence[ProtectRequiredKeysMixin] | None = None, + unadopted_descs: list[ProtectRequiredKeysMixin] | None = None, ufp_device: ProtectAdoptableDeviceModel | None = None, ) -> list[ProtectDeviceEntity]: """Generate a list of all the device entities.""" - all_descs = list(all_descs or []) - unadopted_descs = list(unadopted_descs or []) - camera_descs = list(camera_descs or []) + all_descs - light_descs = list(light_descs or []) + all_descs - sense_descs = list(sense_descs or []) + all_descs - viewer_descs = list(viewer_descs or []) + all_descs - lock_descs = list(lock_descs or []) + all_descs - chime_descs = list(chime_descs or []) + all_descs - if ufp_device is None: - return ( - _async_device_entities( - data, klass, ModelType.CAMERA, camera_descs, unadopted_descs + entities: list[ProtectDeviceEntity] = [] + for model_type in _ALL_MODEL_TYPES: + descs = _combine_model_descs(model_type, model_descriptions, all_descs) + entities.extend( + _async_device_entities(data, klass, model_type, descs, unadopted_descs) ) - + _async_device_entities( - data, klass, ModelType.LIGHT, light_descs, unadopted_descs - ) - + _async_device_entities( - data, klass, ModelType.SENSOR, sense_descs, unadopted_descs - ) - + _async_device_entities( - data, klass, ModelType.VIEWPORT, viewer_descs, unadopted_descs - ) - + _async_device_entities( - data, klass, ModelType.DOORLOCK, lock_descs, unadopted_descs - ) - + _async_device_entities( - data, klass, ModelType.CHIME, chime_descs, unadopted_descs - ) - ) + return entities - descs = [] - if ufp_device.model is ModelType.CAMERA: - descs = camera_descs - elif ufp_device.model is ModelType.LIGHT: - descs = light_descs - elif ufp_device.model is ModelType.SENSOR: - descs = sense_descs - elif ufp_device.model is ModelType.VIEWPORT: - descs = viewer_descs - elif ufp_device.model is ModelType.DOORLOCK: - descs = lock_descs - elif ufp_device.model is ModelType.CHIME: - descs = chime_descs - - if not descs and not unadopted_descs or ufp_device.model is None: - return [] + device_model_type = ufp_device.model + assert device_model_type is not None + descs = _combine_model_descs(device_model_type, model_descriptions, all_descs) return _async_device_entities( - data, klass, ufp_device.model, descs, unadopted_descs, ufp_device + data, klass, device_model_type, descs, unadopted_descs, ufp_device ) diff --git a/homeassistant/components/unifiprotect/number.py b/homeassistant/components/unifiprotect/number.py index 2a8137f50f7..05d07203191 100644 --- a/homeassistant/components/unifiprotect/number.py +++ b/homeassistant/components/unifiprotect/number.py @@ -2,6 +2,7 @@ from __future__ import annotations +from collections.abc import Sequence from dataclasses import dataclass from datetime import timedelta import logging @@ -11,6 +12,7 @@ from uiprotect.data import ( Camera, Doorlock, Light, + ModelType, ProtectAdoptableDeviceModel, ProtectModelWithId, ) @@ -24,7 +26,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DISPATCH_ADOPT from .data import ProtectData, UFPConfigEntry from .entity import ProtectDeviceEntity, async_all_device_entities -from .models import PermRequired, ProtectSetableKeysMixin, T +from .models import PermRequired, ProtectRequiredKeysMixin, ProtectSetableKeysMixin, T from .utils import async_dispatch_id as _ufpd _LOGGER = logging.getLogger(__name__) @@ -215,6 +217,13 @@ CHIME_NUMBERS: tuple[ProtectNumberEntityDescription, ...] = ( ufp_perm=PermRequired.WRITE, ), ) +_MODEL_DESCRIPTIONS: dict[ModelType, Sequence[ProtectRequiredKeysMixin]] = { + ModelType.CAMERA: CAMERA_NUMBERS, + ModelType.LIGHT: LIGHT_NUMBERS, + ModelType.SENSOR: SENSE_NUMBERS, + ModelType.DOORLOCK: DOORLOCK_NUMBERS, + ModelType.CHIME: CHIME_NUMBERS, +} async def async_setup_entry( @@ -230,11 +239,7 @@ async def async_setup_entry( entities = async_all_device_entities( data, ProtectNumbers, - camera_descs=CAMERA_NUMBERS, - light_descs=LIGHT_NUMBERS, - sense_descs=SENSE_NUMBERS, - lock_descs=DOORLOCK_NUMBERS, - chime_descs=CHIME_NUMBERS, + model_descriptions=_MODEL_DESCRIPTIONS, ufp_device=device, ) async_add_entities(entities) @@ -246,11 +251,7 @@ async def async_setup_entry( entities: list[ProtectDeviceEntity] = async_all_device_entities( data, ProtectNumbers, - camera_descs=CAMERA_NUMBERS, - light_descs=LIGHT_NUMBERS, - sense_descs=SENSE_NUMBERS, - lock_descs=DOORLOCK_NUMBERS, - chime_descs=CHIME_NUMBERS, + model_descriptions=_MODEL_DESCRIPTIONS, ) async_add_entities(entities) diff --git a/homeassistant/components/unifiprotect/select.py b/homeassistant/components/unifiprotect/select.py index 5ba557a8af6..678d0007347 100644 --- a/homeassistant/components/unifiprotect/select.py +++ b/homeassistant/components/unifiprotect/select.py @@ -2,7 +2,7 @@ from __future__ import annotations -from collections.abc import Callable +from collections.abc import Callable, Sequence from dataclasses import dataclass from enum import Enum import logging @@ -18,6 +18,7 @@ from uiprotect.data import ( Light, LightModeEnableType, LightModeType, + ModelType, MountType, ProtectAdoptableDeviceModel, ProtectModelWithId, @@ -35,7 +36,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DISPATCH_ADOPT, TYPE_EMPTY_VALUE from .data import ProtectData, UFPConfigEntry from .entity import ProtectDeviceEntity, async_all_device_entities -from .models import PermRequired, ProtectSetableKeysMixin, T +from .models import PermRequired, ProtectRequiredKeysMixin, ProtectSetableKeysMixin, T from .utils import async_dispatch_id as _ufpd, async_get_light_motion_current _LOGGER = logging.getLogger(__name__) @@ -319,6 +320,14 @@ VIEWER_SELECTS: tuple[ProtectSelectEntityDescription, ...] = ( ), ) +_MODEL_DESCRIPTIONS: dict[ModelType, Sequence[ProtectRequiredKeysMixin]] = { + ModelType.CAMERA: CAMERA_SELECTS, + ModelType.LIGHT: LIGHT_SELECTS, + ModelType.SENSOR: SENSE_SELECTS, + ModelType.VIEWPORT: VIEWER_SELECTS, + ModelType.DOORLOCK: DOORLOCK_SELECTS, +} + async def async_setup_entry( hass: HomeAssistant, entry: UFPConfigEntry, async_add_entities: AddEntitiesCallback @@ -331,11 +340,7 @@ async def async_setup_entry( entities = async_all_device_entities( data, ProtectSelects, - camera_descs=CAMERA_SELECTS, - light_descs=LIGHT_SELECTS, - sense_descs=SENSE_SELECTS, - viewer_descs=VIEWER_SELECTS, - lock_descs=DOORLOCK_SELECTS, + model_descriptions=_MODEL_DESCRIPTIONS, ufp_device=device, ) async_add_entities(entities) @@ -345,13 +350,7 @@ async def async_setup_entry( ) entities: list[ProtectDeviceEntity] = async_all_device_entities( - data, - ProtectSelects, - camera_descs=CAMERA_SELECTS, - light_descs=LIGHT_SELECTS, - sense_descs=SENSE_SELECTS, - viewer_descs=VIEWER_SELECTS, - lock_descs=DOORLOCK_SELECTS, + data, ProtectSelects, model_descriptions=_MODEL_DESCRIPTIONS ) async_add_entities(entities) diff --git a/homeassistant/components/unifiprotect/sensor.py b/homeassistant/components/unifiprotect/sensor.py index 95b01710b9b..7624a659d38 100644 --- a/homeassistant/components/unifiprotect/sensor.py +++ b/homeassistant/components/unifiprotect/sensor.py @@ -2,6 +2,7 @@ from __future__ import annotations +from collections.abc import Sequence from dataclasses import dataclass from datetime import datetime import logging @@ -608,6 +609,15 @@ VIEWER_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( ), ) +_MODEL_DESCRIPTIONS: dict[ModelType, Sequence[ProtectRequiredKeysMixin]] = { + ModelType.CAMERA: CAMERA_SENSORS + CAMERA_DISABLED_SENSORS, + ModelType.SENSOR: SENSE_SENSORS, + ModelType.LIGHT: LIGHT_SENSORS, + ModelType.DOORLOCK: DOORLOCK_SENSORS, + ModelType.CHIME: CHIME_SENSORS, + ModelType.VIEWPORT: VIEWER_SENSORS, +} + async def async_setup_entry( hass: HomeAssistant, @@ -623,12 +633,7 @@ async def async_setup_entry( data, ProtectDeviceSensor, all_descs=ALL_DEVICES_SENSORS, - camera_descs=CAMERA_SENSORS + CAMERA_DISABLED_SENSORS, - sense_descs=SENSE_SENSORS, - light_descs=LIGHT_SENSORS, - lock_descs=DOORLOCK_SENSORS, - chime_descs=CHIME_SENSORS, - viewer_descs=VIEWER_SENSORS, + model_descriptions=_MODEL_DESCRIPTIONS, ufp_device=device, ) if device.is_adopted_by_us and isinstance(device, Camera): @@ -643,12 +648,7 @@ async def async_setup_entry( data, ProtectDeviceSensor, all_descs=ALL_DEVICES_SENSORS, - camera_descs=CAMERA_SENSORS + CAMERA_DISABLED_SENSORS, - sense_descs=SENSE_SENSORS, - light_descs=LIGHT_SENSORS, - lock_descs=DOORLOCK_SENSORS, - chime_descs=CHIME_SENSORS, - viewer_descs=VIEWER_SENSORS, + model_descriptions=_MODEL_DESCRIPTIONS, ) entities += _async_event_entities(data) entities += _async_nvr_entities(data) diff --git a/homeassistant/components/unifiprotect/switch.py b/homeassistant/components/unifiprotect/switch.py index d13c49af882..fafa9d1f90d 100644 --- a/homeassistant/components/unifiprotect/switch.py +++ b/homeassistant/components/unifiprotect/switch.py @@ -2,6 +2,7 @@ from __future__ import annotations +from collections.abc import Sequence from dataclasses import dataclass import logging from typing import Any @@ -9,6 +10,7 @@ from typing import Any from uiprotect.data import ( NVR, Camera, + ModelType, ProtectAdoptableDeviceModel, ProtectModelWithId, RecordingMode, @@ -25,7 +27,7 @@ from homeassistant.helpers.restore_state import RestoreEntity from .const import DISPATCH_ADOPT from .data import ProtectData, UFPConfigEntry from .entity import ProtectDeviceEntity, ProtectNVREntity, async_all_device_entities -from .models import PermRequired, ProtectSetableKeysMixin, T +from .models import PermRequired, ProtectRequiredKeysMixin, ProtectSetableKeysMixin, T from .utils import async_dispatch_id as _ufpd _LOGGER = logging.getLogger(__name__) @@ -455,6 +457,18 @@ NVR_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( ), ) +_MODEL_DESCRIPTIONS: dict[ModelType, Sequence[ProtectRequiredKeysMixin]] = { + ModelType.CAMERA: CAMERA_SWITCHES, + ModelType.LIGHT: LIGHT_SWITCHES, + ModelType.SENSOR: SENSE_SWITCHES, + ModelType.DOORLOCK: DOORLOCK_SWITCHES, + ModelType.VIEWPORT: VIEWER_SWITCHES, +} + +_PRIVACY_MODEL_DESCRIPTIONS: dict[ModelType, Sequence[ProtectRequiredKeysMixin]] = { + ModelType.CAMERA: [PRIVACY_MODE_SWITCH] +} + async def async_setup_entry( hass: HomeAssistant, @@ -469,17 +483,13 @@ async def async_setup_entry( entities = async_all_device_entities( data, ProtectSwitch, - camera_descs=CAMERA_SWITCHES, - light_descs=LIGHT_SWITCHES, - sense_descs=SENSE_SWITCHES, - lock_descs=DOORLOCK_SWITCHES, - viewer_descs=VIEWER_SWITCHES, + model_descriptions=_MODEL_DESCRIPTIONS, ufp_device=device, ) entities += async_all_device_entities( data, ProtectPrivacyModeSwitch, - camera_descs=[PRIVACY_MODE_SWITCH], + model_descriptions=_PRIVACY_MODEL_DESCRIPTIONS, ufp_device=device, ) async_add_entities(entities) @@ -491,16 +501,12 @@ async def async_setup_entry( entities: list[ProtectDeviceEntity] = async_all_device_entities( data, ProtectSwitch, - camera_descs=CAMERA_SWITCHES, - light_descs=LIGHT_SWITCHES, - sense_descs=SENSE_SWITCHES, - lock_descs=DOORLOCK_SWITCHES, - viewer_descs=VIEWER_SWITCHES, + model_descriptions=_MODEL_DESCRIPTIONS, ) entities += async_all_device_entities( data, ProtectPrivacyModeSwitch, - camera_descs=[PRIVACY_MODE_SWITCH], + model_descriptions=_PRIVACY_MODEL_DESCRIPTIONS, ) if ( diff --git a/homeassistant/components/unifiprotect/text.py b/homeassistant/components/unifiprotect/text.py index c267419bd6d..5fc11546fae 100644 --- a/homeassistant/components/unifiprotect/text.py +++ b/homeassistant/components/unifiprotect/text.py @@ -2,12 +2,14 @@ from __future__ import annotations +from collections.abc import Sequence from dataclasses import dataclass from typing import Any from uiprotect.data import ( Camera, DoorbellMessageType, + ModelType, ProtectAdoptableDeviceModel, ProtectModelWithId, ) @@ -21,7 +23,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DISPATCH_ADOPT from .data import ProtectData, UFPConfigEntry from .entity import ProtectDeviceEntity, async_all_device_entities -from .models import PermRequired, ProtectSetableKeysMixin, T +from .models import PermRequired, ProtectRequiredKeysMixin, ProtectSetableKeysMixin, T from .utils import async_dispatch_id as _ufpd @@ -52,6 +54,10 @@ CAMERA: tuple[ProtectTextEntityDescription, ...] = ( ), ) +_MODEL_DESCRIPTIONS: dict[ModelType, Sequence[ProtectRequiredKeysMixin]] = { + ModelType.CAMERA: CAMERA, +} + async def async_setup_entry( hass: HomeAssistant, @@ -66,7 +72,7 @@ async def async_setup_entry( entities = async_all_device_entities( data, ProtectDeviceText, - camera_descs=CAMERA, + model_descriptions=_MODEL_DESCRIPTIONS, ufp_device=device, ) async_add_entities(entities) @@ -76,9 +82,7 @@ async def async_setup_entry( ) entities: list[ProtectDeviceEntity] = async_all_device_entities( - data, - ProtectDeviceText, - camera_descs=CAMERA, + data, ProtectDeviceText, model_descriptions=_MODEL_DESCRIPTIONS ) async_add_entities(entities) From 707e422a3167eec2f7a5d4aa765c181c80874085 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 12 Jun 2024 18:20:31 +0200 Subject: [PATCH 0592/1445] Add UniFi sensor for number of clients connected to a device (#119509) Co-authored-by: Kim de Vos --- homeassistant/components/unifi/sensor.py | 35 ++++++++ tests/components/unifi/test_sensor.py | 107 +++++++++++++++++++++++ 2 files changed, 142 insertions(+) diff --git a/homeassistant/components/unifi/sensor.py b/homeassistant/components/unifi/sensor.py index 3fd179f5676..ba1da7ea6c8 100644 --- a/homeassistant/components/unifi/sensor.py +++ b/homeassistant/components/unifi/sensor.py @@ -108,6 +108,27 @@ def async_wlan_client_value_fn(hub: UnifiHub, wlan: Wlan) -> int: ) +@callback +def async_device_clients_value_fn(hub: UnifiHub, device: Device) -> int: + """Calculate the amount of clients connected to a device.""" + + return len( + [ + client.mac + for client in hub.api.clients.values() + if ( + ( + client.access_point_mac != "" + and client.access_point_mac == device.mac + ) + or (client.access_point_mac == "" and client.switch_mac == device.mac) + ) + and dt_util.utcnow() - dt_util.utc_from_timestamp(client.last_seen or 0) + < hub.config.option_detection_time + ] + ) + + @callback def async_device_uptime_value_fn(hub: UnifiHub, device: Device) -> datetime | None: """Calculate the approximate time the device started (based on uptime returned from API, in seconds).""" @@ -302,6 +323,20 @@ ENTITY_DESCRIPTIONS: tuple[UnifiSensorEntityDescription, ...] = ( unique_id_fn=lambda hub, obj_id: f"wlan_clients-{obj_id}", value_fn=async_wlan_client_value_fn, ), + UnifiSensorEntityDescription[Devices, Device]( + key="Device clients", + entity_category=EntityCategory.DIAGNOSTIC, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + api_handler_fn=lambda api: api.devices, + available_fn=async_device_available_fn, + device_info_fn=async_device_device_info_fn, + name_fn=lambda device: "Clients", + object_fn=lambda api, obj_id: api.devices[obj_id], + should_poll=True, + unique_id_fn=lambda hub, obj_id: f"device_clients-{obj_id}", + value_fn=async_device_clients_value_fn, + ), UnifiSensorEntityDescription[Outlets, Outlet]( key="Outlet power metering", device_class=SensorDeviceClass.POWER, diff --git a/tests/components/unifi/test_sensor.py b/tests/components/unifi/test_sensor.py index 735df53b0c5..802166068b2 100644 --- a/tests/components/unifi/test_sensor.py +++ b/tests/components/unifi/test_sensor.py @@ -1225,3 +1225,110 @@ async def test_bandwidth_port_sensors( assert hass.states.get("sensor.mock_name_port_1_tx") is None assert hass.states.get("sensor.mock_name_port_2_rx") is None assert hass.states.get("sensor.mock_name_port_2_tx") is None + + +@pytest.mark.parametrize( + "device_payload", + [ + [ + { + "device_id": "mock-id1", + "mac": "01:00:00:00:00:00", + "model": "US16P150", + "name": "Wired Device", + "state": 1, + "version": "4.0.42.10433", + }, + { + "device_id": "mock-id2", + "mac": "02:00:00:00:00:00", + "model": "US16P150", + "name": "Wireless Device", + "state": 1, + "version": "4.0.42.10433", + }, + ] + ], +) +async def test_device_client_sensors( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + config_entry_factory, + mock_websocket_message, + client_payload, +) -> None: + """Verify that WLAN client sensors are working as expected.""" + client_payload += [ + { + "hostname": "Wired client 1", + "is_wired": True, + "mac": "00:00:00:00:00:01", + "oui": "Producer", + "sw_mac": "01:00:00:00:00:00", + "last_seen": dt_util.as_timestamp(dt_util.utcnow()), + }, + { + "hostname": "Wired client 2", + "is_wired": True, + "mac": "00:00:00:00:00:02", + "oui": "Producer", + "sw_mac": "01:00:00:00:00:00", + "last_seen": dt_util.as_timestamp(dt_util.utcnow()), + }, + { + "is_wired": False, + "mac": "00:00:00:00:00:03", + "name": "Wireless client 1", + "oui": "Producer", + "ap_mac": "02:00:00:00:00:00", + "sw_mac": "01:00:00:00:00:00", + "last_seen": dt_util.as_timestamp(dt_util.utcnow()), + }, + ] + await config_entry_factory() + + assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 4 + + ent_reg_entry = entity_registry.async_get("sensor.wired_device_clients") + assert ent_reg_entry.disabled_by == RegistryEntryDisabler.INTEGRATION + assert ent_reg_entry.entity_category is EntityCategory.DIAGNOSTIC + assert ent_reg_entry.unique_id == "device_clients-01:00:00:00:00:00" + + ent_reg_entry = entity_registry.async_get("sensor.wireless_device_clients") + assert ent_reg_entry.disabled_by == RegistryEntryDisabler.INTEGRATION + assert ent_reg_entry.entity_category is EntityCategory.DIAGNOSTIC + assert ent_reg_entry.unique_id == "device_clients-02:00:00:00:00:00" + + # Enable entity + entity_registry.async_update_entity( + entity_id="sensor.wired_device_clients", disabled_by=None + ) + entity_registry.async_update_entity( + entity_id="sensor.wireless_device_clients", disabled_by=None + ) + + await hass.async_block_till_done() + + async_fire_time_changed( + hass, + dt_util.utcnow() + timedelta(seconds=RELOAD_AFTER_UPDATE_DELAY + 1), + ) + await hass.async_block_till_done() + + # Validate state object + assert len(hass.states.async_all()) == 13 + assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 6 + + assert hass.states.get("sensor.wired_device_clients").state == "2" + assert hass.states.get("sensor.wireless_device_clients").state == "1" + + # Verify state update - decreasing number + wireless_client_1 = client_payload[2] + wireless_client_1["last_seen"] = 0 + mock_websocket_message(message=MessageKey.CLIENT, data=wireless_client_1) + + async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL) + await hass.async_block_till_done() + + assert hass.states.get("sensor.wired_device_clients").state == "2" + assert hass.states.get("sensor.wireless_device_clients").state == "0" From 7f7128adbf37670b434a3a89a01f2567ed9e6f6c Mon Sep 17 00:00:00 2001 From: Caius-Bonus <123886836+Caius-Bonus@users.noreply.github.com> Date: Wed, 12 Jun 2024 18:48:37 +0200 Subject: [PATCH 0593/1445] Add Danfoss Ally thermostat and derivatives to ZHA (#86907) * zha integration: Add danfoss specific clusters and attributes; add thermostat.pi_heating_demand and thermostat_ui.keypad_lockout * zha integration: fix Danfoss thermostat viewing direction not working because of use of bitmap8 instead of enum8 * ZHA Integration: add missing ThermostatChannelSensor * ZHA integration: format using black * zha integration: fix flake8 issues * ZHA danfoss: Add MinHeatSetpointLimit, MaxHeatSetpointLimit, add reporting and read config for danfoss and keypad_lockout. * ZHA danfoss: fix mypy complaining about type of _attr_entity_category * ZHA danfoss: ruff fix * fix tests * pylint: disable-next=hass-invalid-inheritance * fix pylint tests * refactoring * remove scheduled setpoint * remove scheduled setpoint in manufacturer specific * refactor * fix tests * change cluster ids * remove custom clusters * code quality * match clusters in manufacturerspecific on quirk class * fix comment * fix match on quirk in manufacturerspecific.py * correctly extend cluster handlers in manufacturerspecific.py and remove workaround for illegal use of attribute updated signals in climate.py * fix style * allow non-danfoss thermostats to work in manufacturerspecific.py * correct order of init of parent and subclasses in manufacturerspecific.py * improve entity names * fix pylint * explicitly state changing size of tuple * ignore tuple size change error * really ignore error * initial * fix tests * match on specific name and quirk name * don't restructure file as it is out of scope * move back * remove unnecessary change * fix tests * fix tests * remove code duplication * reduce code duplication * empty line * remove unused variable * end file on newline * comply with recent PRs * correctly initialize all attributes * comply with recent PRs * make class variables private * forgot one reference * swap 2 lines for consistency * reorder 2 lines * fix tests * align with recent PR * store cluster handlers in only one place * edit tests * use correct device for quirk id * change quirk id * fix tests * even if there is a quirk id, it doesn't have to have a specific cluster handler * add tests * use quirk id for manufacturer specific cluster handlers * use quirk_ids instead of quirks_classes * rename quirk_id * rename quirk_id * forgot to rename here * rename id * add tests * fix tests * fix tests * use quirk ids from zha_quirks * use quirk id from zha_quirks * wrong translation * sync changes with ZCL branch * sync * style * merge error * move bitmapSensor * merge error * merge error * watch the capitals * fix entity categories * more decapitalization * translate BitmapSensor * translate all enums * translate all enums * don't convert camelcase to snakecase * don't change enums at all * remove comments * fix bitmaps and add enum for algorithm scale factor * improve readability if bitmapsensor * fix capitals * better setpoint response time * feedback * lowercase every enum to adhere to the translation_key standard * remove enum state translations and use enums from quirks * correctly capitalize OrientationEnum * bump zha dependencies; this will have to be done in a separate PR, but this aids review * accidentally removed enum * tests * comment * Migrate reporting and ZCL attribute config out of `__init__` * hvac.py shouldn't be changed in this pull request * change wording comment * I forgot I changed the size of the tuple. --------- Co-authored-by: puddly <32534428+puddly@users.noreply.github.com> --- homeassistant/components/zha/binary_sensor.py | 42 ++++++ .../zha/core/cluster_handlers/hvac.py | 2 +- .../cluster_handlers/manufacturerspecific.py | 71 +++++++++- homeassistant/components/zha/number.py | 80 ++++++++++- homeassistant/components/zha/select.py | 110 ++++++++++++++- homeassistant/components/zha/sensor.py | 128 +++++++++++++++++ homeassistant/components/zha/strings.json | 129 ++++++++++++++++++ homeassistant/components/zha/switch.py | 95 ++++++++++++- tests/components/zha/test_sensor.py | 59 ++++++++ 9 files changed, 711 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/zha/binary_sensor.py b/homeassistant/components/zha/binary_sensor.py index 6ffb6d6f909..bdd2fd03ca0 100644 --- a/homeassistant/components/zha/binary_sensor.py +++ b/homeassistant/components/zha/binary_sensor.py @@ -5,6 +5,7 @@ from __future__ import annotations import functools import logging +from zhaquirks.quirk_ids import DANFOSS_ALLY_THERMOSTAT from zigpy.quirks.v2 import BinarySensorMetadata import zigpy.types as t from zigpy.zcl.clusters.general import OnOff @@ -27,6 +28,7 @@ from .core.const import ( CLUSTER_HANDLER_HUE_OCCUPANCY, CLUSTER_HANDLER_OCCUPANCY, CLUSTER_HANDLER_ON_OFF, + CLUSTER_HANDLER_THERMOSTAT, CLUSTER_HANDLER_ZONE, ENTITY_METADATA, SIGNAL_ADD_ENTITIES, @@ -337,3 +339,43 @@ class AqaraE1CurtainMotorOpenedByHandBinarySensor(BinarySensor): _attribute_name = "hand_open" _attr_translation_key = "hand_open" _attr_entity_category = EntityCategory.DIAGNOSTIC + + +@CONFIG_DIAGNOSTIC_MATCH( + cluster_handler_names=CLUSTER_HANDLER_THERMOSTAT, + quirk_ids={DANFOSS_ALLY_THERMOSTAT}, +) +class DanfossMountingModeActive(BinarySensor): + """Danfoss TRV proprietary attribute exposing whether in mounting mode.""" + + _unique_id_suffix = "mounting_mode_active" + _attribute_name = "mounting_mode_active" + _attr_translation_key: str = "mounting_mode_active" + _attr_device_class: BinarySensorDeviceClass = BinarySensorDeviceClass.OPENING + _attr_entity_category = EntityCategory.DIAGNOSTIC + + +@MULTI_MATCH( + cluster_handler_names=CLUSTER_HANDLER_THERMOSTAT, + quirk_ids={DANFOSS_ALLY_THERMOSTAT}, +) +class DanfossHeatRequired(BinarySensor): + """Danfoss TRV proprietary attribute exposing whether heat is required.""" + + _unique_id_suffix = "heat_required" + _attribute_name = "heat_required" + _attr_translation_key: str = "heat_required" + + +@CONFIG_DIAGNOSTIC_MATCH( + cluster_handler_names=CLUSTER_HANDLER_THERMOSTAT, + quirk_ids={DANFOSS_ALLY_THERMOSTAT}, +) +class DanfossPreheatStatus(BinarySensor): + """Danfoss TRV proprietary attribute exposing whether in pre-heating mode.""" + + _unique_id_suffix = "preheat_status" + _attribute_name = "preheat_status" + _attr_translation_key: str = "preheat_status" + _attr_entity_registry_enabled_default = False + _attr_entity_category = EntityCategory.DIAGNOSTIC diff --git a/homeassistant/components/zha/core/cluster_handlers/hvac.py b/homeassistant/components/zha/core/cluster_handlers/hvac.py index d455ade4e66..1230549832b 100644 --- a/homeassistant/components/zha/core/cluster_handlers/hvac.py +++ b/homeassistant/components/zha/core/cluster_handlers/hvac.py @@ -90,7 +90,7 @@ class PumpClusterHandler(ClusterHandler): class ThermostatClusterHandler(ClusterHandler): """Thermostat cluster handler.""" - REPORT_CONFIG = ( + REPORT_CONFIG: tuple[AttrReportConfig, ...] = ( AttrReportConfig( attr=Thermostat.AttributeDefs.local_temperature.name, config=REPORT_CONFIG_CLIMATE, diff --git a/homeassistant/components/zha/core/cluster_handlers/manufacturerspecific.py b/homeassistant/components/zha/core/cluster_handlers/manufacturerspecific.py index dc8af821724..9d5d68d2c7e 100644 --- a/homeassistant/components/zha/core/cluster_handlers/manufacturerspecific.py +++ b/homeassistant/components/zha/core/cluster_handlers/manufacturerspecific.py @@ -6,8 +6,13 @@ import logging from typing import TYPE_CHECKING, Any from zhaquirks.inovelli.types import AllLEDEffectType, SingleLEDEffectType -from zhaquirks.quirk_ids import TUYA_PLUG_MANUFACTURER, XIAOMI_AQARA_VIBRATION_AQ1 +from zhaquirks.quirk_ids import ( + DANFOSS_ALLY_THERMOSTAT, + TUYA_PLUG_MANUFACTURER, + XIAOMI_AQARA_VIBRATION_AQ1, +) import zigpy.zcl +from zigpy.zcl import clusters from zigpy.zcl.clusters.closures import DoorLock from homeassistant.core import callback @@ -27,6 +32,8 @@ from ..const import ( ) from . import AttrReportConfig, ClientClusterHandler, ClusterHandler from .general import MultistateInputClusterHandler +from .homeautomation import DiagnosticClusterHandler +from .hvac import ThermostatClusterHandler, UserInterfaceClusterHandler if TYPE_CHECKING: from ..endpoint import Endpoint @@ -444,3 +451,65 @@ class SonoffPresenceSenorClusterHandler(ClusterHandler): super().__init__(cluster, endpoint) if self.cluster.endpoint.model == "SNZB-06P": self.ZCL_INIT_ATTRS = {"last_illumination_state": True} + + +@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register( + clusters.hvac.Thermostat.cluster_id, DANFOSS_ALLY_THERMOSTAT +) +class DanfossThermostatClusterHandler(ThermostatClusterHandler): + """Thermostat cluster handler for the Danfoss TRV and derivatives.""" + + REPORT_CONFIG = ( + *ThermostatClusterHandler.REPORT_CONFIG, + AttrReportConfig(attr="open_window_detection", config=REPORT_CONFIG_DEFAULT), + AttrReportConfig(attr="heat_required", config=REPORT_CONFIG_ASAP), + AttrReportConfig(attr="mounting_mode_active", config=REPORT_CONFIG_DEFAULT), + AttrReportConfig(attr="load_estimate", config=REPORT_CONFIG_DEFAULT), + AttrReportConfig(attr="adaptation_run_status", config=REPORT_CONFIG_DEFAULT), + AttrReportConfig(attr="preheat_status", config=REPORT_CONFIG_DEFAULT), + AttrReportConfig(attr="preheat_time", config=REPORT_CONFIG_DEFAULT), + ) + + ZCL_INIT_ATTRS = { + **ThermostatClusterHandler.ZCL_INIT_ATTRS, + "external_open_window_detected": True, + "window_open_feature": True, + "exercise_day_of_week": True, + "exercise_trigger_time": True, + "mounting_mode_control": False, # Can change + "orientation": True, + "external_measured_room_sensor": False, # Can change + "radiator_covered": True, + "heat_available": True, + "load_balancing_enable": True, + "load_room_mean": False, # Can change + "control_algorithm_scale_factor": True, + "regulation_setpoint_offset": True, + "adaptation_run_control": True, + "adaptation_run_settings": True, + } + + +@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register( + clusters.hvac.UserInterface.cluster_id, DANFOSS_ALLY_THERMOSTAT +) +class DanfossUserInterfaceClusterHandler(UserInterfaceClusterHandler): + """Interface cluster handler for the Danfoss TRV and derivatives.""" + + ZCL_INIT_ATTRS = { + **UserInterfaceClusterHandler.ZCL_INIT_ATTRS, + "viewing_direction": True, + } + + +@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register( + clusters.homeautomation.Diagnostic.cluster_id, DANFOSS_ALLY_THERMOSTAT +) +class DanfossDiagnosticClusterHandler(DiagnosticClusterHandler): + """Diagnostic cluster handler for the Danfoss TRV and derivatives.""" + + REPORT_CONFIG = ( + *DiagnosticClusterHandler.REPORT_CONFIG, + AttrReportConfig(attr="sw_error_code", config=REPORT_CONFIG_DEFAULT), + AttrReportConfig(attr="motor_step_counter", config=REPORT_CONFIG_DEFAULT), + ) diff --git a/homeassistant/components/zha/number.py b/homeassistant/components/zha/number.py index 8af2fe178c8..9320b4494a4 100644 --- a/homeassistant/components/zha/number.py +++ b/homeassistant/components/zha/number.py @@ -6,12 +6,19 @@ import functools import logging from typing import TYPE_CHECKING, Any, Self +from zhaquirks.quirk_ids import DANFOSS_ALLY_THERMOSTAT from zigpy.quirks.v2 import NumberMetadata from zigpy.zcl.clusters.hvac import Thermostat from homeassistant.components.number import NumberDeviceClass, NumberEntity, NumberMode from homeassistant.config_entries import ConfigEntry -from homeassistant.const import EntityCategory, Platform, UnitOfMass, UnitOfTemperature +from homeassistant.const import ( + EntityCategory, + Platform, + UnitOfMass, + UnitOfTemperature, + UnitOfTime, +) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -1073,3 +1080,74 @@ class MinHeatSetpointLimit(ZCLHeatSetpointLimitEntity): _attr_entity_category = EntityCategory.CONFIG _max_source = Thermostat.AttributeDefs.max_heat_setpoint_limit.name + + +@CONFIG_DIAGNOSTIC_MATCH( + cluster_handler_names=CLUSTER_HANDLER_THERMOSTAT, + quirk_ids={DANFOSS_ALLY_THERMOSTAT}, +) +# pylint: disable-next=hass-invalid-inheritance # needs fixing +class DanfossExerciseTriggerTime(ZHANumberConfigurationEntity): + """Danfoss proprietary attribute to set the time to exercise the valve.""" + + _unique_id_suffix = "exercise_trigger_time" + _attribute_name: str = "exercise_trigger_time" + _attr_translation_key: str = "exercise_trigger_time" + _attr_native_min_value: int = 0 + _attr_native_max_value: int = 1439 + _attr_mode: NumberMode = NumberMode.BOX + _attr_native_unit_of_measurement: str = UnitOfTime.MINUTES + _attr_icon: str = "mdi:clock" + + +@CONFIG_DIAGNOSTIC_MATCH( + cluster_handler_names=CLUSTER_HANDLER_THERMOSTAT, + quirk_ids={DANFOSS_ALLY_THERMOSTAT}, +) +# pylint: disable-next=hass-invalid-inheritance # needs fixing +class DanfossExternalMeasuredRoomSensor(ZCLTemperatureEntity): + """Danfoss proprietary attribute to communicate the value of the external temperature sensor.""" + + _unique_id_suffix = "external_measured_room_sensor" + _attribute_name: str = "external_measured_room_sensor" + _attr_translation_key: str = "external_temperature_sensor" + _attr_native_min_value: float = -80 + _attr_native_max_value: float = 35 + _attr_icon: str = "mdi:thermometer" + + +@CONFIG_DIAGNOSTIC_MATCH( + cluster_handler_names=CLUSTER_HANDLER_THERMOSTAT, + quirk_ids={DANFOSS_ALLY_THERMOSTAT}, +) +# pylint: disable-next=hass-invalid-inheritance # needs fixing +class DanfossLoadRoomMean(ZHANumberConfigurationEntity): + """Danfoss proprietary attribute to set a value for the load.""" + + _unique_id_suffix = "load_room_mean" + _attribute_name: str = "load_room_mean" + _attr_translation_key: str = "load_room_mean" + _attr_native_min_value: int = -8000 + _attr_native_max_value: int = 2000 + _attr_mode: NumberMode = NumberMode.BOX + _attr_icon: str = "mdi:scale-balance" + + +@CONFIG_DIAGNOSTIC_MATCH( + cluster_handler_names=CLUSTER_HANDLER_THERMOSTAT, + quirk_ids={DANFOSS_ALLY_THERMOSTAT}, +) +# pylint: disable-next=hass-invalid-inheritance # needs fixing +class DanfossRegulationSetpointOffset(ZHANumberConfigurationEntity): + """Danfoss proprietary attribute to set the regulation setpoint offset.""" + + _unique_id_suffix = "regulation_setpoint_offset" + _attribute_name: str = "regulation_setpoint_offset" + _attr_translation_key: str = "regulation_setpoint_offset" + _attr_mode: NumberMode = NumberMode.BOX + _attr_native_unit_of_measurement: str = UnitOfTemperature.CELSIUS + _attr_icon: str = "mdi:thermostat" + _attr_native_min_value: float = -2.5 + _attr_native_max_value: float = 2.5 + _attr_native_step: float = 0.1 + _attr_multiplier = 1 / 10 diff --git a/homeassistant/components/zha/select.py b/homeassistant/components/zha/select.py index 98d5debd999..026a85fbfdc 100644 --- a/homeassistant/components/zha/select.py +++ b/homeassistant/components/zha/select.py @@ -7,7 +7,12 @@ import functools import logging from typing import TYPE_CHECKING, Any, Self -from zhaquirks.quirk_ids import TUYA_PLUG_MANUFACTURER, TUYA_PLUG_ONOFF +from zhaquirks.danfoss import thermostat as danfoss_thermostat +from zhaquirks.quirk_ids import ( + DANFOSS_ALLY_THERMOSTAT, + TUYA_PLUG_MANUFACTURER, + TUYA_PLUG_ONOFF, +) from zhaquirks.xiaomi.aqara.magnet_ac01 import OppleCluster as MagnetAC01OppleCluster from zhaquirks.xiaomi.aqara.switch_acn047 import OppleCluster as T2RelayOppleCluster from zigpy import types @@ -29,6 +34,7 @@ from .core.const import ( CLUSTER_HANDLER_INOVELLI, CLUSTER_HANDLER_OCCUPANCY, CLUSTER_HANDLER_ON_OFF, + CLUSTER_HANDLER_THERMOSTAT, ENTITY_METADATA, SIGNAL_ADD_ENTITIES, SIGNAL_ATTR_UPDATED, @@ -688,3 +694,105 @@ class KeypadLockout(ZCLEnumSelectEntity): _attribute_name: str = "keypad_lockout" _enum = KeypadLockoutEnum _attr_translation_key: str = "keypad_lockout" + + +@CONFIG_DIAGNOSTIC_MATCH( + cluster_handler_names=CLUSTER_HANDLER_THERMOSTAT, + quirk_ids={DANFOSS_ALLY_THERMOSTAT}, +) +class DanfossExerciseDayOfTheWeek(ZCLEnumSelectEntity): + """Danfoss proprietary attribute for setting the day of the week for exercising.""" + + _unique_id_suffix = "exercise_day_of_week" + _attribute_name = "exercise_day_of_week" + _attr_translation_key: str = "exercise_day_of_week" + _enum = danfoss_thermostat.DanfossExerciseDayOfTheWeekEnum + _attr_icon: str = "mdi:wrench-clock" + + +class DanfossOrientationEnum(types.enum8): + """Vertical or Horizontal.""" + + Horizontal = 0x00 + Vertical = 0x01 + + +@CONFIG_DIAGNOSTIC_MATCH( + cluster_handler_names=CLUSTER_HANDLER_THERMOSTAT, + quirk_ids={DANFOSS_ALLY_THERMOSTAT}, +) +class DanfossOrientation(ZCLEnumSelectEntity): + """Danfoss proprietary attribute for setting the orientation of the valve. + + Needed for biasing the internal temperature sensor. + This is implemented as an enum here, but is a boolean on the device. + """ + + _unique_id_suffix = "orientation" + _attribute_name = "orientation" + _attr_translation_key: str = "valve_orientation" + _enum = DanfossOrientationEnum + + +@CONFIG_DIAGNOSTIC_MATCH( + cluster_handler_names=CLUSTER_HANDLER_THERMOSTAT, + quirk_ids={DANFOSS_ALLY_THERMOSTAT}, +) +class DanfossAdaptationRunControl(ZCLEnumSelectEntity): + """Danfoss proprietary attribute for controlling the current adaptation run.""" + + _unique_id_suffix = "adaptation_run_control" + _attribute_name = "adaptation_run_control" + _attr_translation_key: str = "adaptation_run_command" + _enum = danfoss_thermostat.DanfossAdaptationRunControlEnum + + +class DanfossControlAlgorithmScaleFactorEnum(types.enum8): + """The time scale factor for changing the opening of the valve. + + Not all values are given, therefore there are some extrapolated values with a margin of error of about 5 minutes. + This is implemented as an enum here, but is a number on the device. + """ + + quick_5min = 0x01 + + quick_10min = 0x02 # extrapolated + quick_15min = 0x03 # extrapolated + quick_25min = 0x04 # extrapolated + + moderate_30min = 0x05 + + moderate_40min = 0x06 # extrapolated + moderate_50min = 0x07 # extrapolated + moderate_60min = 0x08 # extrapolated + moderate_70min = 0x09 # extrapolated + + slow_80min = 0x0A + + quick_open_disabled = 0x11 # not sure what it does; also requires lower 4 bits to be in [1, 10] I assume + + +@CONFIG_DIAGNOSTIC_MATCH( + cluster_handler_names=CLUSTER_HANDLER_THERMOSTAT, + quirk_ids={DANFOSS_ALLY_THERMOSTAT}, +) +class DanfossControlAlgorithmScaleFactor(ZCLEnumSelectEntity): + """Danfoss proprietary attribute for setting the scale factor of the setpoint filter time constant.""" + + _unique_id_suffix = "control_algorithm_scale_factor" + _attribute_name = "control_algorithm_scale_factor" + _attr_translation_key: str = "setpoint_response_time" + _enum = DanfossControlAlgorithmScaleFactorEnum + + +@CONFIG_DIAGNOSTIC_MATCH( + cluster_handler_names="thermostat_ui", + quirk_ids={DANFOSS_ALLY_THERMOSTAT}, +) +class DanfossViewingDirection(ZCLEnumSelectEntity): + """Danfoss proprietary attribute for setting the viewing direction of the screen.""" + + _unique_id_suffix = "viewing_direction" + _attribute_name = "viewing_direction" + _attr_translation_key: str = "viewing_direction" + _enum = danfoss_thermostat.DanfossViewingDirectionEnum diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index 9e98060667a..99d950dc06a 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -12,6 +12,8 @@ import numbers import random from typing import TYPE_CHECKING, Any, Self +from zhaquirks.danfoss import thermostat as danfoss_thermostat +from zhaquirks.quirk_ids import DANFOSS_ALLY_THERMOSTAT from zigpy import types from zigpy.quirks.v2 import ZCLEnumMetadata, ZCLSensorMetadata from zigpy.state import Counter, State @@ -1499,3 +1501,129 @@ class AqaraCurtainHookStateSensor(EnumSensor): _attr_translation_key: str = "hooks_state" _attr_icon: str = "mdi:hook" _attr_entity_category = EntityCategory.DIAGNOSTIC + + +# pylint: disable-next=hass-invalid-inheritance # needs fixing +class BitMapSensor(Sensor): + """A sensor with only state attributes. + + The sensor value will be an aggregate of the state attributes. + """ + + _bitmap: types.bitmap8 | types.bitmap16 + + def formatter(self, _value: int) -> str: + """Summary of all attributes.""" + binary_state_attributes = [ + key for (key, elem) in self.extra_state_attributes.items() if elem + ] + + return "something" if binary_state_attributes else "nothing" + + @property + def extra_state_attributes(self) -> dict[str, Any]: + """Bitmap.""" + value = self._cluster_handler.cluster.get(self._attribute_name) + + state_attr = {} + + for bit in list(self._bitmap): + if value is None: + state_attr[bit.name] = False + else: + state_attr[bit.name] = bit in self._bitmap(value) + + return state_attr + + +@MULTI_MATCH( + cluster_handler_names=CLUSTER_HANDLER_THERMOSTAT, + quirk_ids={DANFOSS_ALLY_THERMOSTAT}, +) +# pylint: disable-next=hass-invalid-inheritance # needs fixing +class DanfossOpenWindowDetection(EnumSensor): + """Danfoss proprietary attribute. + + Sensor that displays whether the TRV detects an open window using the temperature sensor. + """ + + _unique_id_suffix = "open_window_detection" + _attribute_name = "open_window_detection" + _attr_translation_key: str = "open_window_detected" + _attr_icon: str = "mdi:window-open" + _enum = danfoss_thermostat.DanfossOpenWindowDetectionEnum + + +@CONFIG_DIAGNOSTIC_MATCH( + cluster_handler_names=CLUSTER_HANDLER_THERMOSTAT, + quirk_ids={DANFOSS_ALLY_THERMOSTAT}, +) +# pylint: disable-next=hass-invalid-inheritance # needs fixing +class DanfossLoadEstimate(Sensor): + """Danfoss proprietary attribute for communicating its estimate of the radiator load.""" + + _unique_id_suffix = "load_estimate" + _attribute_name = "load_estimate" + _attr_translation_key: str = "load_estimate" + _attr_icon: str = "mdi:scale-balance" + _attr_entity_category = EntityCategory.DIAGNOSTIC + + +@CONFIG_DIAGNOSTIC_MATCH( + cluster_handler_names=CLUSTER_HANDLER_THERMOSTAT, + quirk_ids={DANFOSS_ALLY_THERMOSTAT}, +) +# pylint: disable-next=hass-invalid-inheritance # needs fixing +class DanfossAdaptationRunStatus(BitMapSensor): + """Danfoss proprietary attribute for showing the status of the adaptation run.""" + + _unique_id_suffix = "adaptation_run_status" + _attribute_name = "adaptation_run_status" + _attr_translation_key: str = "adaptation_run_status" + _attr_entity_category = EntityCategory.DIAGNOSTIC + _bitmap = danfoss_thermostat.DanfossAdaptationRunStatusBitmap + + +@CONFIG_DIAGNOSTIC_MATCH( + cluster_handler_names=CLUSTER_HANDLER_THERMOSTAT, + quirk_ids={DANFOSS_ALLY_THERMOSTAT}, +) +# pylint: disable-next=hass-invalid-inheritance # needs fixing +class DanfossPreheatTime(Sensor): + """Danfoss proprietary attribute for communicating the time when it starts pre-heating.""" + + _unique_id_suffix = "preheat_time" + _attribute_name = "preheat_time" + _attr_translation_key: str = "preheat_time" + _attr_icon: str = "mdi:radiator" + _attr_entity_registry_enabled_default = False + _attr_entity_category = EntityCategory.DIAGNOSTIC + + +@CONFIG_DIAGNOSTIC_MATCH( + cluster_handler_names="diagnostic", + quirk_ids={DANFOSS_ALLY_THERMOSTAT}, +) +# pylint: disable-next=hass-invalid-inheritance # needs fixing +class DanfossSoftwareErrorCode(BitMapSensor): + """Danfoss proprietary attribute for communicating the error code.""" + + _unique_id_suffix = "sw_error_code" + _attribute_name = "sw_error_code" + _attr_translation_key: str = "software_error" + _attr_entity_category = EntityCategory.DIAGNOSTIC + _bitmap = danfoss_thermostat.DanfossSoftwareErrorCodeBitmap + + +@CONFIG_DIAGNOSTIC_MATCH( + cluster_handler_names="diagnostic", + quirk_ids={DANFOSS_ALLY_THERMOSTAT}, +) +# pylint: disable-next=hass-invalid-inheritance # needs fixing +class DanfossMotorStepCounter(Sensor): + """Danfoss proprietary attribute for communicating the motor step counter.""" + + _unique_id_suffix = "motor_step_counter" + _attribute_name = "motor_step_counter" + _attr_translation_key: str = "motor_stepcount" + _attr_entity_category = EntityCategory.DIAGNOSTIC diff --git a/homeassistant/components/zha/strings.json b/homeassistant/components/zha/strings.json index 3db54712dee..04cef23b2df 100644 --- a/homeassistant/components/zha/strings.json +++ b/homeassistant/components/zha/strings.json @@ -569,6 +569,15 @@ }, "hand_open": { "name": "Opened by hand" + }, + "mounting_mode_active": { + "name": "Mounting mode active" + }, + "heat_required": { + "name": "Heat required" + }, + "preheat_status": { + "name": "Pre-heat status" } }, "button": { @@ -739,6 +748,18 @@ }, "min_heat_setpoint_limit": { "name": "Min heat setpoint limit" + }, + "exercise_trigger_time": { + "name": "Exercise start time" + }, + "external_temperature_sensor": { + "name": "External temperature sensor" + }, + "load_room_mean": { + "name": "Load room mean" + }, + "regulation_setpoint_offset": { + "name": "Regulation setpoint offset" } }, "select": { @@ -810,6 +831,21 @@ }, "keypad_lockout": { "name": "Keypad lockout" + }, + "exercise_day_of_week": { + "name": "Exercise day of the week" + }, + "valve_orientation": { + "name": "Valve orientation" + }, + "adaptation_run_command": { + "name": "Adaptation run command" + }, + "viewing_direction": { + "name": "Viewing direction" + }, + "setpoint_response_time": { + "name": "Setpoint response time" } }, "sensor": { @@ -908,6 +944,78 @@ }, "hooks_state": { "name": "Hooks state" + }, + "open_window_detected": { + "name": "Open window detected" + }, + "load_estimate": { + "name": "Load estimate" + }, + "adaptation_run_status": { + "name": "Adaptation run status", + "state": { + "nothing": "Idle", + "something": "State" + }, + "state_attributes": { + "in_progress": { + "name": "In progress" + }, + "run_successful": { + "name": "Run successful" + }, + "valve_characteristic_lost": { + "name": "Valve characteristic lost" + } + } + }, + "preheat_time": { + "name": "Pre-heat time" + }, + "software_error": { + "name": "Software error", + "state": { + "nothing": "Good", + "something": "Error" + }, + "state_attributes": { + "top_pcb_sensor_error": { + "name": "Top PCB sensor error" + }, + "side_pcb_sensor_error": { + "name": "Side PCB sensor error" + }, + "non_volatile_memory_error": { + "name": "Non-volatile memory error" + }, + "unknown_hw_error": { + "name": "Unknown HW error" + }, + "motor_error": { + "name": "Motor error" + }, + "invalid_internal_communication": { + "name": "Invalid internal communication" + }, + "invalid_clock_information": { + "name": "Invalid clock information" + }, + "radio_communication_error": { + "name": "Radio communication error" + }, + "encoder_jammed": { + "name": "Encoder jammed" + }, + "low_battery": { + "name": "Low battery" + }, + "critical_low_battery": { + "name": "Critical low battery" + } + } + }, + "motor_stepcount": { + "name": "Motor stepcount" } }, "switch": { @@ -991,6 +1099,27 @@ }, "buzzer_manual_alarm": { "name": "Buzzer manual alarm" + }, + "external_window_sensor": { + "name": "External window sensor" + }, + "use_internal_window_detection": { + "name": "Use internal window detection" + }, + "mounting_mode": { + "name": "Mounting mode" + }, + "prioritize_external_temperature_sensor": { + "name": "Prioritize external temperature sensor" + }, + "heat_available": { + "name": "Heat available" + }, + "use_load_balancing": { + "name": "Use load balancing" + }, + "adaptation_run_enabled": { + "name": "Adaptation run enabled" } } } diff --git a/homeassistant/components/zha/switch.py b/homeassistant/components/zha/switch.py index 14da2344cd4..f07d3d4c8e3 100644 --- a/homeassistant/components/zha/switch.py +++ b/homeassistant/components/zha/switch.py @@ -6,7 +6,7 @@ import functools import logging from typing import TYPE_CHECKING, Any, Self -from zhaquirks.quirk_ids import TUYA_PLUG_ONOFF +from zhaquirks.quirk_ids import DANFOSS_ALLY_THERMOSTAT, TUYA_PLUG_ONOFF from zigpy.quirks.v2 import SwitchMetadata from zigpy.zcl.clusters.closures import ConfigStatus, WindowCovering, WindowCoveringMode from zigpy.zcl.clusters.general import OnOff @@ -25,6 +25,7 @@ from .core.const import ( CLUSTER_HANDLER_COVER, CLUSTER_HANDLER_INOVELLI, CLUSTER_HANDLER_ON_OFF, + CLUSTER_HANDLER_THERMOSTAT, ENTITY_METADATA, SIGNAL_ADD_ENTITIES, SIGNAL_ATTR_UPDATED, @@ -716,3 +717,95 @@ class AqaraE1CurtainMotorHooksLockedSwitch(ZHASwitchConfigurationEntity): _unique_id_suffix = "hooks_lock" _attribute_name = "hooks_lock" _attr_translation_key = "hooks_locked" + + +@CONFIG_DIAGNOSTIC_MATCH( + cluster_handler_names=CLUSTER_HANDLER_THERMOSTAT, + quirk_ids={DANFOSS_ALLY_THERMOSTAT}, +) +class DanfossExternalOpenWindowDetected(ZHASwitchConfigurationEntity): + """Danfoss proprietary attribute for communicating an open window.""" + + _unique_id_suffix = "external_open_window_detected" + _attribute_name: str = "external_open_window_detected" + _attr_translation_key: str = "external_window_sensor" + _attr_icon: str = "mdi:window-open" + + +@CONFIG_DIAGNOSTIC_MATCH( + cluster_handler_names=CLUSTER_HANDLER_THERMOSTAT, + quirk_ids={DANFOSS_ALLY_THERMOSTAT}, +) +class DanfossWindowOpenFeature(ZHASwitchConfigurationEntity): + """Danfoss proprietary attribute enabling open window detection.""" + + _unique_id_suffix = "window_open_feature" + _attribute_name: str = "window_open_feature" + _attr_translation_key: str = "use_internal_window_detection" + _attr_icon: str = "mdi:window-open" + + +@CONFIG_DIAGNOSTIC_MATCH( + cluster_handler_names=CLUSTER_HANDLER_THERMOSTAT, + quirk_ids={DANFOSS_ALLY_THERMOSTAT}, +) +class DanfossMountingModeControl(ZHASwitchConfigurationEntity): + """Danfoss proprietary attribute for switching to mounting mode.""" + + _unique_id_suffix = "mounting_mode_control" + _attribute_name: str = "mounting_mode_control" + _attr_translation_key: str = "mounting_mode" + + +@CONFIG_DIAGNOSTIC_MATCH( + cluster_handler_names=CLUSTER_HANDLER_THERMOSTAT, + quirk_ids={DANFOSS_ALLY_THERMOSTAT}, +) +class DanfossRadiatorCovered(ZHASwitchConfigurationEntity): + """Danfoss proprietary attribute for communicating full usage of the external temperature sensor.""" + + _unique_id_suffix = "radiator_covered" + _attribute_name: str = "radiator_covered" + _attr_translation_key: str = "prioritize_external_temperature_sensor" + _attr_icon: str = "mdi:thermometer" + + +@CONFIG_DIAGNOSTIC_MATCH( + cluster_handler_names=CLUSTER_HANDLER_THERMOSTAT, + quirk_ids={DANFOSS_ALLY_THERMOSTAT}, +) +class DanfossHeatAvailable(ZHASwitchConfigurationEntity): + """Danfoss proprietary attribute for communicating available heat.""" + + _unique_id_suffix = "heat_available" + _attribute_name: str = "heat_available" + _attr_translation_key: str = "heat_available" + _attr_icon: str = "mdi:water-boiler" + + +@CONFIG_DIAGNOSTIC_MATCH( + cluster_handler_names=CLUSTER_HANDLER_THERMOSTAT, + quirk_ids={DANFOSS_ALLY_THERMOSTAT}, +) +class DanfossLoadBalancingEnable(ZHASwitchConfigurationEntity): + """Danfoss proprietary attribute for enabling load balancing.""" + + _unique_id_suffix = "load_balancing_enable" + _attribute_name: str = "load_balancing_enable" + _attr_translation_key: str = "use_load_balancing" + _attr_icon: str = "mdi:scale-balance" + + +@CONFIG_DIAGNOSTIC_MATCH( + cluster_handler_names=CLUSTER_HANDLER_THERMOSTAT, + quirk_ids={DANFOSS_ALLY_THERMOSTAT}, +) +class DanfossAdaptationRunSettings(ZHASwitchConfigurationEntity): + """Danfoss proprietary attribute for enabling daily adaptation run. + + Actually a bitmap, but only the first bit is used. + """ + + _unique_id_suffix = "adaptation_run_settings" + _attribute_name: str = "adaptation_run_settings" + _attr_translation_key: str = "adaptation_run_enabled" diff --git a/tests/components/zha/test_sensor.py b/tests/components/zha/test_sensor.py index 86868ef65c2..8443c4ced07 100644 --- a/tests/components/zha/test_sensor.py +++ b/tests/components/zha/test_sensor.py @@ -7,6 +7,7 @@ from typing import Any from unittest.mock import MagicMock, patch import pytest +from zhaquirks.danfoss import thermostat as danfoss_thermostat import zigpy.profiles.zha from zigpy.quirks import CustomCluster from zigpy.quirks.v2 import CustomDeviceV2, add_to_registry_v2 @@ -1316,3 +1317,61 @@ async def test_device_counter_sensors( state = hass.states.get(entity_id) assert state is not None assert state.state == "2" + + +@pytest.fixture +async def zigpy_device_danfoss_thermostat( + hass: HomeAssistant, zigpy_device_mock, zha_device_joined_restored +): + """Device tracker zigpy danfoss thermostat device.""" + + zigpy_device = zigpy_device_mock( + { + 1: { + SIG_EP_INPUT: [ + general.Basic.cluster_id, + general.PowerConfiguration.cluster_id, + general.Identify.cluster_id, + general.Time.cluster_id, + general.PollControl.cluster_id, + Thermostat.cluster_id, + hvac.UserInterface.cluster_id, + homeautomation.Diagnostic.cluster_id, + ], + SIG_EP_OUTPUT: [general.Basic.cluster_id, general.Ota.cluster_id], + SIG_EP_TYPE: zigpy.profiles.zha.DeviceType.THERMOSTAT, + } + }, + manufacturer="Danfoss", + model="eTRV0100", + ) + + zha_device = await zha_device_joined_restored(zigpy_device) + return zha_device, zigpy_device + + +async def test_danfoss_thermostat_sw_error( + hass: HomeAssistant, zigpy_device_danfoss_thermostat +) -> None: + """Test quirks defined thermostat.""" + + zha_device, zigpy_device = zigpy_device_danfoss_thermostat + + entity_id = find_entity_id( + Platform.SENSOR, zha_device, hass, qualifier="software_error" + ) + assert entity_id is not None + + cluster = zigpy_device.endpoints[1].diagnostic + + await send_attributes_report( + hass, + cluster, + { + danfoss_thermostat.DanfossDiagnosticCluster.AttributeDefs.sw_error_code.id: 0x0001 + }, + ) + + hass_state = hass.states.get(entity_id) + assert hass_state.state == "something" + assert hass_state.attributes["Top_pcb_sensor_error"] From 8f7686082c9040992c8b0c077ea0d77a81afa996 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 12 Jun 2024 12:02:53 -0500 Subject: [PATCH 0594/1445] Refactor unifiprotect media_source to remove type ignores (#119516) --- .../components/unifiprotect/media_source.py | 41 +++++++++---------- 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/unifiprotect/media_source.py b/homeassistant/components/unifiprotect/media_source.py index 9d94c3ecda7..9165b574b2d 100644 --- a/homeassistant/components/unifiprotect/media_source.py +++ b/homeassistant/components/unifiprotect/media_source.py @@ -543,21 +543,20 @@ class ProtectMediaSource(MediaSource): return source now = dt_util.now() - - args = { - "data": data, - "start": now - timedelta(days=days), - "end": now, - "reserve": True, - "event_types": get_ufp_event(event_type), - } - camera: Camera | None = None + event_camera_id: str | None = None if camera_id != "all": camera = data.api.bootstrap.cameras.get(camera_id) - args["camera_id"] = camera_id + event_camera_id = camera_id - events = await self._build_events(**args) # type: ignore[arg-type] + events = await self._build_events( + data=data, + start=now - timedelta(days=days), + end=now, + camera_id=event_camera_id, + event_types=get_ufp_event(event_type), + reserve=True, + ) source.children = events source.title = self._breadcrumb( data, @@ -674,21 +673,21 @@ class ProtectMediaSource(MediaSource): else: end_dt = start_dt + timedelta(hours=24) - args = { - "data": data, - "start": start_dt, - "end": end_dt, - "reserve": False, - "event_types": get_ufp_event(event_type), - } - camera: Camera | None = None + event_camera_id: str | None = None if camera_id != "all": camera = data.api.bootstrap.cameras.get(camera_id) - args["camera_id"] = camera_id + event_camera_id = camera_id title = f"{start.strftime('%B %Y')} > {title}" - events = await self._build_events(**args) # type: ignore[arg-type] + events = await self._build_events( + data=data, + start=start_dt, + end=end_dt, + camera_id=event_camera_id, + reserve=False, + event_types=get_ufp_event(event_type), + ) source.children = events source.title = self._breadcrumb( data, From ae3134d875c16b02860de5734f50aff6a91e4ae7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 12 Jun 2024 12:03:12 -0500 Subject: [PATCH 0595/1445] Simplify unifiprotect device removal code (#119517) --- homeassistant/components/unifiprotect/__init__.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/unifiprotect/__init__.py b/homeassistant/components/unifiprotect/__init__.py index 38e45798789..eab4cc29737 100644 --- a/homeassistant/components/unifiprotect/__init__.py +++ b/homeassistant/components/unifiprotect/__init__.py @@ -36,7 +36,7 @@ from .const import ( OUTDATED_LOG_MESSAGE, PLATFORMS, ) -from .data import ProtectData, UFPConfigEntry, async_ufp_instance_for_config_entry_ids +from .data import ProtectData, UFPConfigEntry from .discovery import async_start_discovery from .migrate import async_migrate_data from .services import async_cleanup_services, async_setup_services @@ -200,8 +200,7 @@ async def async_remove_config_entry_device( for connection in device_entry.connections if connection[0] == dr.CONNECTION_NETWORK_MAC } - api = async_ufp_instance_for_config_entry_ids(hass, {config_entry.entry_id}) - assert api is not None + api = config_entry.runtime_data.api if api.bootstrap.nvr.mac in unifi_macs: return False for device in async_get_devices(api.bootstrap, DEVICES_THAT_ADOPT): From a4c34fe207017e3738c60e3b8a4bd22146490e63 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 12 Jun 2024 20:14:59 +0200 Subject: [PATCH 0596/1445] Fix typo in lovelace (#119523) --- homeassistant/components/lovelace/websocket.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/lovelace/websocket.py b/homeassistant/components/lovelace/websocket.py index 3049ae38542..2aa55efafbd 100644 --- a/homeassistant/components/lovelace/websocket.py +++ b/homeassistant/components/lovelace/websocket.py @@ -88,7 +88,7 @@ async def websocket_lovelace_config( msg: dict[str, Any], config: LovelaceStorage, ) -> json_fragment: - """Send Lovelace UI config over WebSocket configuration.""" + """Send Lovelace UI config over WebSocket connection.""" return await config.async_json(msg["force"]) From 2661581d4e2475db37981cbb484f0f76cbc9183c Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 12 Jun 2024 20:37:38 +0200 Subject: [PATCH 0597/1445] Fix typos in collection helper (#119524) --- homeassistant/helpers/collection.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/helpers/collection.py b/homeassistant/helpers/collection.py index bf65b47f451..1b63d95864a 100644 --- a/homeassistant/helpers/collection.py +++ b/homeassistant/helpers/collection.py @@ -609,7 +609,7 @@ class StorageCollectionWebsocket[_StorageCollectionT: StorageCollection]: async def ws_create_item( self, hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict ) -> None: - """Create a item.""" + """Create an item.""" try: data = dict(msg) data.pop("id") @@ -628,7 +628,7 @@ class StorageCollectionWebsocket[_StorageCollectionT: StorageCollection]: async def ws_update_item( self, hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict ) -> None: - """Update a item.""" + """Update an item.""" data = dict(msg) msg_id = data.pop("id") item_id = data.pop(self.item_id_key) @@ -655,7 +655,7 @@ class StorageCollectionWebsocket[_StorageCollectionT: StorageCollection]: async def ws_delete_item( self, hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict ) -> None: - """Delete a item.""" + """Delete an item.""" try: await self.storage_collection.async_delete_item(msg[self.item_id_key]) except ItemNotFound: From a586e7fb7282a3777f4ecf00f8841936c9e47221 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 12 Jun 2024 15:23:18 -0500 Subject: [PATCH 0598/1445] Remove useless delegation in unifiprotect (#119514) --- .../components/unifiprotect/binary_sensor.py | 9 +++-- .../components/unifiprotect/button.py | 29 +++++++------- .../components/unifiprotect/camera.py | 8 +--- .../components/unifiprotect/entity.py | 38 +++++++++---------- homeassistant/components/unifiprotect/lock.py | 10 ++--- .../components/unifiprotect/number.py | 25 ++++++------ .../components/unifiprotect/select.py | 22 +++++------ .../components/unifiprotect/sensor.py | 7 ++-- .../components/unifiprotect/switch.py | 2 +- homeassistant/components/unifiprotect/text.py | 21 +++++----- 10 files changed, 83 insertions(+), 88 deletions(-) diff --git a/homeassistant/components/unifiprotect/binary_sensor.py b/homeassistant/components/unifiprotect/binary_sensor.py index f42d2d09211..396894c997a 100644 --- a/homeassistant/components/unifiprotect/binary_sensor.py +++ b/homeassistant/components/unifiprotect/binary_sensor.py @@ -32,6 +32,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DISPATCH_ADOPT from .data import ProtectData, UFPConfigEntry from .entity import ( + BaseProtectEntity, EventEntityMixin, ProtectDeviceEntity, ProtectNVREntity, @@ -630,7 +631,7 @@ async def async_setup_entry( @callback def _add_new_device(device: ProtectAdoptableDeviceModel) -> None: - entities: list[ProtectDeviceEntity] = async_all_device_entities( + entities = async_all_device_entities( data, ProtectDeviceBinarySensor, model_descriptions=_MODEL_DESCRIPTIONS, @@ -644,7 +645,7 @@ async def async_setup_entry( async_dispatcher_connect(hass, _ufpd(entry, DISPATCH_ADOPT), _add_new_device) ) - entities: list[ProtectDeviceEntity] = async_all_device_entities( + entities = async_all_device_entities( data, ProtectDeviceBinarySensor, model_descriptions=_MODEL_DESCRIPTIONS ) entities += _async_event_entities(data) @@ -679,8 +680,8 @@ def _async_event_entities( @callback def _async_nvr_entities( data: ProtectData, -) -> list[ProtectDeviceEntity]: - entities: list[ProtectDeviceEntity] = [] +) -> list[BaseProtectEntity]: + entities: list[BaseProtectEntity] = [] device = data.api.bootstrap.nvr if device.system_info.ustorage is None: return entities diff --git a/homeassistant/components/unifiprotect/button.py b/homeassistant/components/unifiprotect/button.py index 98d226e9e76..a1b1ec21f6a 100644 --- a/homeassistant/components/unifiprotect/button.py +++ b/homeassistant/components/unifiprotect/button.py @@ -138,14 +138,14 @@ async def async_setup_entry( if not device.can_adopt or not device.can_create(data.api.bootstrap.auth_user): _LOGGER.debug("Device is not adoptable: %s", device.id) return - - entities = async_all_device_entities( - data, - ProtectButton, - unadopted_descs=[ADOPT_BUTTON], - ufp_device=device, + async_add_entities( + async_all_device_entities( + data, + ProtectButton, + unadopted_descs=[ADOPT_BUTTON], + ufp_device=device, + ) ) - async_add_entities(entities) entry.async_on_unload( async_dispatcher_connect(hass, _ufpd(entry, DISPATCH_ADOPT), _add_new_device) @@ -156,14 +156,15 @@ async def async_setup_entry( ) ) - entities: list[ProtectDeviceEntity] = async_all_device_entities( - data, - ProtectButton, - all_descs=ALL_DEVICE_BUTTONS, - unadopted_descs=[ADOPT_BUTTON], - model_descriptions=_MODEL_DESCRIPTIONS, + async_add_entities( + async_all_device_entities( + data, + ProtectButton, + all_descs=ALL_DEVICE_BUTTONS, + unadopted_descs=[ADOPT_BUTTON], + model_descriptions=_MODEL_DESCRIPTIONS, + ) ) - async_add_entities(entities) for device in data.get_by_types(DEVICES_THAT_ADOPT): _async_remove_adopt_button(hass, device) diff --git a/homeassistant/components/unifiprotect/camera.py b/homeassistant/components/unifiprotect/camera.py index 5a703dc5458..dc41310ab3f 100644 --- a/homeassistant/components/unifiprotect/camera.py +++ b/homeassistant/components/unifiprotect/camera.py @@ -155,9 +155,7 @@ async def async_setup_entry( def _add_new_device(device: ProtectAdoptableDeviceModel) -> None: if not isinstance(device, UFPCamera): return - - entities = _async_camera_entities(hass, entry, data, ufp_device=device) - async_add_entities(entities) + async_add_entities(_async_camera_entities(hass, entry, data, ufp_device=device)) entry.async_on_unload( async_dispatcher_connect(hass, _ufpd(entry, DISPATCH_ADOPT), _add_new_device) @@ -165,9 +163,7 @@ async def async_setup_entry( entry.async_on_unload( async_dispatcher_connect(hass, _ufpd(entry, DISPATCH_CHANNELS), _add_new_device) ) - - entities = _async_camera_entities(hass, entry, data) - async_add_entities(entities) + async_add_entities(_async_camera_entities(hass, entry, data)) class ProtectCamera(ProtectDeviceEntity, Camera): diff --git a/homeassistant/components/unifiprotect/entity.py b/homeassistant/components/unifiprotect/entity.py index 137f8c532ee..a41aadfcd89 100644 --- a/homeassistant/components/unifiprotect/entity.py +++ b/homeassistant/components/unifiprotect/entity.py @@ -37,16 +37,16 @@ _LOGGER = logging.getLogger(__name__) @callback def _async_device_entities( data: ProtectData, - klass: type[ProtectDeviceEntity], + klass: type[BaseProtectEntity], model_type: ModelType, descs: Sequence[ProtectRequiredKeysMixin], unadopted_descs: Sequence[ProtectRequiredKeysMixin] | None = None, ufp_device: ProtectAdoptableDeviceModel | None = None, -) -> list[ProtectDeviceEntity]: +) -> list[BaseProtectEntity]: if not descs and not unadopted_descs: return [] - entities: list[ProtectDeviceEntity] = [] + entities: list[BaseProtectEntity] = [] devices = ( [ufp_device] if ufp_device is not None @@ -130,16 +130,16 @@ def _combine_model_descs( @callback def async_all_device_entities( data: ProtectData, - klass: type[ProtectDeviceEntity], + klass: type[BaseProtectEntity], model_descriptions: dict[ModelType, Sequence[ProtectRequiredKeysMixin]] | None = None, all_descs: Sequence[ProtectRequiredKeysMixin] | None = None, unadopted_descs: list[ProtectRequiredKeysMixin] | None = None, ufp_device: ProtectAdoptableDeviceModel | None = None, -) -> list[ProtectDeviceEntity]: +) -> list[BaseProtectEntity]: """Generate a list of all the device entities.""" if ufp_device is None: - entities: list[ProtectDeviceEntity] = [] + entities: list[BaseProtectEntity] = [] for model_type in _ALL_MODEL_TYPES: descs = _combine_model_descs(model_type, model_descriptions, all_descs) entities.extend( @@ -155,17 +155,17 @@ def async_all_device_entities( ) -class ProtectDeviceEntity(Entity): +class BaseProtectEntity(Entity): """Base class for UniFi protect entities.""" - device: ProtectAdoptableDeviceModel + device: ProtectAdoptableDeviceModel | NVR _attr_should_poll = False def __init__( self, data: ProtectData, - device: ProtectAdoptableDeviceModel, + device: ProtectAdoptableDeviceModel | NVR, description: EntityDescription | None = None, ) -> None: """Initialize the entity.""" @@ -275,20 +275,16 @@ class ProtectDeviceEntity(Entity): ) -class ProtectNVREntity(ProtectDeviceEntity): +class ProtectDeviceEntity(BaseProtectEntity): + """Base class for UniFi protect entities.""" + + device: ProtectAdoptableDeviceModel + + +class ProtectNVREntity(BaseProtectEntity): """Base class for unifi protect entities.""" - # separate subclass on purpose - device: NVR # type: ignore[assignment] - - def __init__( - self, - entry: ProtectData, - device: NVR, - description: EntityDescription | None = None, - ) -> None: - """Initialize the entity.""" - super().__init__(entry, device, description) # type: ignore[arg-type] + device: NVR @callback def _async_set_device_info(self) -> None: diff --git a/homeassistant/components/unifiprotect/lock.py b/homeassistant/components/unifiprotect/lock.py index 4deeafa0782..7ffa3c6bfc5 100644 --- a/homeassistant/components/unifiprotect/lock.py +++ b/homeassistant/components/unifiprotect/lock.py @@ -43,12 +43,10 @@ async def async_setup_entry( async_dispatcher_connect(hass, _ufpd(entry, DISPATCH_ADOPT), _add_new_device) ) - entities = [] - for device in data.get_by_types({ModelType.DOORLOCK}): - device = cast(Doorlock, device) - entities.append(ProtectLock(data, device)) - - async_add_entities(entities) + async_add_entities( + ProtectLock(data, cast(Doorlock, device)) + for device in data.get_by_types({ModelType.DOORLOCK}) + ) class ProtectLock(ProtectDeviceEntity, LockEntity): diff --git a/homeassistant/components/unifiprotect/number.py b/homeassistant/components/unifiprotect/number.py index 05d07203191..08e07536f87 100644 --- a/homeassistant/components/unifiprotect/number.py +++ b/homeassistant/components/unifiprotect/number.py @@ -236,26 +236,27 @@ async def async_setup_entry( @callback def _add_new_device(device: ProtectAdoptableDeviceModel) -> None: - entities = async_all_device_entities( - data, - ProtectNumbers, - model_descriptions=_MODEL_DESCRIPTIONS, - ufp_device=device, + async_add_entities( + async_all_device_entities( + data, + ProtectNumbers, + model_descriptions=_MODEL_DESCRIPTIONS, + ufp_device=device, + ) ) - async_add_entities(entities) entry.async_on_unload( async_dispatcher_connect(hass, _ufpd(entry, DISPATCH_ADOPT), _add_new_device) ) - entities: list[ProtectDeviceEntity] = async_all_device_entities( - data, - ProtectNumbers, - model_descriptions=_MODEL_DESCRIPTIONS, + async_add_entities( + async_all_device_entities( + data, + ProtectNumbers, + model_descriptions=_MODEL_DESCRIPTIONS, + ) ) - async_add_entities(entities) - class ProtectNumbers(ProtectDeviceEntity, NumberEntity): """A UniFi Protect Number Entity.""" diff --git a/homeassistant/components/unifiprotect/select.py b/homeassistant/components/unifiprotect/select.py index 678d0007347..57e0c806c69 100644 --- a/homeassistant/components/unifiprotect/select.py +++ b/homeassistant/components/unifiprotect/select.py @@ -337,24 +337,24 @@ async def async_setup_entry( @callback def _add_new_device(device: ProtectAdoptableDeviceModel) -> None: - entities = async_all_device_entities( - data, - ProtectSelects, - model_descriptions=_MODEL_DESCRIPTIONS, - ufp_device=device, + async_add_entities( + async_all_device_entities( + data, + ProtectSelects, + model_descriptions=_MODEL_DESCRIPTIONS, + ufp_device=device, + ) ) - async_add_entities(entities) entry.async_on_unload( async_dispatcher_connect(hass, _ufpd(entry, DISPATCH_ADOPT), _add_new_device) ) - - entities: list[ProtectDeviceEntity] = async_all_device_entities( - data, ProtectSelects, model_descriptions=_MODEL_DESCRIPTIONS + async_add_entities( + async_all_device_entities( + data, ProtectSelects, model_descriptions=_MODEL_DESCRIPTIONS + ) ) - async_add_entities(entities) - class ProtectSelects(ProtectDeviceEntity, SelectEntity): """A UniFi Protect Select Entity.""" diff --git a/homeassistant/components/unifiprotect/sensor.py b/homeassistant/components/unifiprotect/sensor.py index 7624a659d38..26103d21bb5 100644 --- a/homeassistant/components/unifiprotect/sensor.py +++ b/homeassistant/components/unifiprotect/sensor.py @@ -43,6 +43,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DISPATCH_ADOPT from .data import ProtectData, UFPConfigEntry from .entity import ( + BaseProtectEntity, EventEntityMixin, ProtectDeviceEntity, ProtectNVREntity, @@ -644,7 +645,7 @@ async def async_setup_entry( async_dispatcher_connect(hass, _ufpd(entry, DISPATCH_ADOPT), _add_new_device) ) - entities: list[ProtectDeviceEntity] = async_all_device_entities( + entities = async_all_device_entities( data, ProtectDeviceSensor, all_descs=ALL_DEVICES_SENSORS, @@ -695,8 +696,8 @@ def _async_event_entities( @callback def _async_nvr_entities( data: ProtectData, -) -> list[ProtectDeviceEntity]: - entities: list[ProtectDeviceEntity] = [] +) -> list[BaseProtectEntity]: + entities: list[BaseProtectEntity] = [] device = data.api.bootstrap.nvr for description in NVR_SENSORS + NVR_DISABLED_SENSORS: entities.append(ProtectNVRSensor(data, device, description)) diff --git a/homeassistant/components/unifiprotect/switch.py b/homeassistant/components/unifiprotect/switch.py index fafa9d1f90d..3dd8bc2dbda 100644 --- a/homeassistant/components/unifiprotect/switch.py +++ b/homeassistant/components/unifiprotect/switch.py @@ -498,7 +498,7 @@ async def async_setup_entry( async_dispatcher_connect(hass, _ufpd(entry, DISPATCH_ADOPT), _add_new_device) ) - entities: list[ProtectDeviceEntity] = async_all_device_entities( + entities = async_all_device_entities( data, ProtectSwitch, model_descriptions=_MODEL_DESCRIPTIONS, diff --git a/homeassistant/components/unifiprotect/text.py b/homeassistant/components/unifiprotect/text.py index 5fc11546fae..30c54d4c15c 100644 --- a/homeassistant/components/unifiprotect/text.py +++ b/homeassistant/components/unifiprotect/text.py @@ -69,24 +69,25 @@ async def async_setup_entry( @callback def _add_new_device(device: ProtectAdoptableDeviceModel) -> None: - entities = async_all_device_entities( - data, - ProtectDeviceText, - model_descriptions=_MODEL_DESCRIPTIONS, - ufp_device=device, + async_add_entities( + async_all_device_entities( + data, + ProtectDeviceText, + model_descriptions=_MODEL_DESCRIPTIONS, + ufp_device=device, + ) ) - async_add_entities(entities) entry.async_on_unload( async_dispatcher_connect(hass, _ufpd(entry, DISPATCH_ADOPT), _add_new_device) ) - entities: list[ProtectDeviceEntity] = async_all_device_entities( - data, ProtectDeviceText, model_descriptions=_MODEL_DESCRIPTIONS + async_add_entities( + async_all_device_entities( + data, ProtectDeviceText, model_descriptions=_MODEL_DESCRIPTIONS + ) ) - async_add_entities(entities) - class ProtectDeviceText(ProtectDeviceEntity, TextEntity): """A Ubiquiti UniFi Protect Sensor.""" From 541c9410068feb39f9d199ddb5cefea367d40546 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Wed, 12 Jun 2024 22:25:49 +0200 Subject: [PATCH 0599/1445] Add state icons to incomfort water_heater entities (#119527) --- homeassistant/components/incomfort/icons.json | 41 +++++++++++++++++++ .../components/incomfort/water_heater.py | 5 --- .../snapshots/test_water_heater.ambr | 3 +- 3 files changed, 42 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/incomfort/icons.json b/homeassistant/components/incomfort/icons.json index eb93ed9a319..6e33ac75eee 100644 --- a/homeassistant/components/incomfort/icons.json +++ b/homeassistant/components/incomfort/icons.json @@ -19,6 +19,47 @@ "on": "mdi:water-pump" } } + }, + "water_heater": { + "boiler": { + "state": { + "unknown": "mdi:water-boiler-alert", + "opentherm": "mdi:radiator", + "boiler_ext": "mdi:water-boiler", + "frost": "mdi:snowflake-thermometer", + "central_heating_rf": "mdi:radiator", + "tapwater_int": "mdi:faucet", + "sensor_test": "mdi:thermometer-check", + "central_heating": "mdi:radiator", + "standby": "mdi:water-boiler-off", + "postrun_boyler": "mdi:water-boiler-auto", + "service": "mdi:progress-wrench", + "tapwater": "mdi:faucet", + "postrun_ch": "mdi:radiator-disabled", + "boiler_int": "mdi:water-boiler", + "buffer": "mdi:water-boiler-auto", + "sensor_fault_after_self_check_e0": "mdi:thermometer-alert", + "cv_temperature_too_high_e1": "mdi:thermometer-alert", + "s1_and_s2_interchanged_e2": "mdi:thermometer-alert", + "no_flame_signal_e4": "mdi:fire-alert", + "poor_flame_signal_e5": "mdi:fire-alert", + "flame_detection_fault_e6": "mdi:fire-alert", + "incorrect_fan_speed_e8": "mdi:water-boiler-alert", + "sensor_fault_s1_e10": "mdi:water-boiler-alert", + "sensor_fault_s1_e11": "mdi:water-boiler-alert", + "sensor_fault_s1_e12": "mdi:water-boiler-alert", + "sensor_fault_s1_e13": "mdi:water-boiler-alert", + "sensor_fault_s1_e14": "mdi:water-boiler-alert", + "sensor_fault_s2_e20": "mdi:water-boiler-alert", + "sensor_fault_s2_e21": "mdi:water-boiler-alert", + "sensor_fault_s2_e22": "mdi:water-boiler-alert", + "sensor_fault_s2_e23": "mdi:water-boiler-alert", + "sensor_fault_s2_e24": "mdi:water-boiler-alert", + "shortcut_outside_sensor_temperature_e27": "mdi:thermometer-alert", + "gas_valve_relay_faulty_e29": "mdi:water-boiler-alert", + "gas_valve_relay_faulty_e30": "mdi:water-boiler-alert" + } + } } } } diff --git a/homeassistant/components/incomfort/water_heater.py b/homeassistant/components/incomfort/water_heater.py index 1c1e5d2fc8e..28424069d1c 100644 --- a/homeassistant/components/incomfort/water_heater.py +++ b/homeassistant/components/incomfort/water_heater.py @@ -48,11 +48,6 @@ class IncomfortWaterHeater(IncomfortBoilerEntity, WaterHeaterEntity): super().__init__(coordinator, heater) self._attr_unique_id = heater.serial_no - @property - def icon(self) -> str: - """Return the icon of the water_heater device.""" - return "mdi:thermometer-lines" - @property def extra_state_attributes(self) -> dict[str, Any]: """Return the device state attributes.""" diff --git a/tests/components/incomfort/snapshots/test_water_heater.ambr b/tests/components/incomfort/snapshots/test_water_heater.ambr index 3ec87c49f3e..06b0d0c1e52 100644 --- a/tests/components/incomfort/snapshots/test_water_heater.ambr +++ b/tests/components/incomfort/snapshots/test_water_heater.ambr @@ -25,7 +25,7 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:thermometer-lines', + 'original_icon': None, 'original_name': None, 'platform': 'incomfort', 'previous_unique_id': None, @@ -42,7 +42,6 @@ 'display_code': , 'display_text': 'standby', 'friendly_name': 'Boiler', - 'icon': 'mdi:thermometer-lines', 'is_burning': False, 'max_temp': 80.0, 'min_temp': 30.0, From f7326d3baf82f55459a81d7e0419c8a8c4db704d Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 12 Jun 2024 22:33:40 +0200 Subject: [PATCH 0600/1445] Ignore too-many-nested-blocks warning in zha tests (#119479) --- tests/components/zha/test_cluster_handlers.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/components/zha/test_cluster_handlers.py b/tests/components/zha/test_cluster_handlers.py index 0f9929d0a97..655a36a2492 100644 --- a/tests/components/zha/test_cluster_handlers.py +++ b/tests/components/zha/test_cluster_handlers.py @@ -347,6 +347,7 @@ def test_cluster_handler_registry() -> None: all_quirk_ids = {} for cluster_id in CLUSTERS_BY_ID: all_quirk_ids[cluster_id] = {None} + # pylint: disable-next=too-many-nested-blocks for manufacturer in zigpy_quirks._DEVICE_REGISTRY.registry.values(): for model_quirk_list in manufacturer.values(): for quirk in model_quirk_list: From e3e80c83b760b9b79cf5e62cc0e1d0a9ac3b5add Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 12 Jun 2024 22:38:11 +0200 Subject: [PATCH 0601/1445] Fix contextmanager-generator-missing-cleanup warning in tests (#119478) --- tests/common.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/tests/common.py b/tests/common.py index ff1fea9cbda..5cb82cef3ba 100644 --- a/tests/common.py +++ b/tests/common.py @@ -195,9 +195,11 @@ def get_test_home_assistant() -> Generator[HomeAssistant]: threading.Thread(name="LoopThread", target=run_loop, daemon=False).start() - yield hass - loop.run_until_complete(context_manager.__aexit__(None, None, None)) - loop.close() + try: + yield hass + finally: + loop.run_until_complete(context_manager.__aexit__(None, None, None)) + loop.close() class StoreWithoutWriteLoad[_T: (Mapping[str, Any] | Sequence[Any])](storage.Store[_T]): @@ -359,10 +361,11 @@ async def async_test_home_assistant( hass.bus.async_listen_once(EVENT_HOMEASSISTANT_CLOSE, clear_instance) - yield hass - - # Restore timezone, it is set when creating the hass object - dt_util.set_default_time_zone(orig_tz) + try: + yield hass + finally: + # Restore timezone, it is set when creating the hass object + dt_util.set_default_time_zone(orig_tz) def async_mock_service( From 39f3a294dc56a432d9a6a2f24c25ebaadc2658f9 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Wed, 12 Jun 2024 22:44:50 +0200 Subject: [PATCH 0602/1445] Device automation extra fields translation for Z-Wave-JS (#119529) --- .../components/zwave_js/strings.json | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/homeassistant/components/zwave_js/strings.json b/homeassistant/components/zwave_js/strings.json index 9e2317ba728..7c65f1804b1 100644 --- a/homeassistant/components/zwave_js/strings.json +++ b/homeassistant/components/zwave_js/strings.json @@ -90,6 +90,27 @@ "state.node_status": "Node status changed", "zwave_js.value_updated.config_parameter": "Value change on config parameter {subtype}", "zwave_js.value_updated.value": "Value change on a Z-Wave JS Value" + }, + "extra_fields": { + "code_slot": "Code slot", + "command_class": "Command class", + "data_type": "Data type", + "endpoint": "Endpoint", + "event": "Event", + "event_label": "Event label", + "event_type": "Event type", + "for": "[%key:common::device_automation::extra_fields::for%]", + "from": "From", + "label": "Label", + "property": "Property", + "property_key": "Property key", + "refresh_all_values": "Refresh all values", + "status": "Status", + "to": "[%key:common::device_automation::extra_fields::to%]", + "type.": "Type", + "usercode": "Usercode", + "value": "Value", + "wait_for_result": "Wait for result" } }, "entity": { From 929dd9f5dac9d807f4a11a9a7d765c21ada1282e Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Wed, 12 Jun 2024 22:45:10 +0200 Subject: [PATCH 0603/1445] Device automation extra fields translation for LCN (#119519) --- homeassistant/components/lcn/strings.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/homeassistant/components/lcn/strings.json b/homeassistant/components/lcn/strings.json index e441832926b..3bab17cbbcd 100644 --- a/homeassistant/components/lcn/strings.json +++ b/homeassistant/components/lcn/strings.json @@ -6,6 +6,12 @@ "fingerprint": "Fingerprint code received", "codelock": "Code lock code received", "send_keys": "Send keys received" + }, + "extra_fields": { + "action": "Action", + "code": "Code", + "key": "Key", + "level": "Level" } }, "services": { From 51891ff8e24818eaec66ed5fe98b3b74bd73992d Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 12 Jun 2024 22:45:41 +0200 Subject: [PATCH 0604/1445] Fix typo in google_assistant (#119522) --- homeassistant/components/google_assistant/trait.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index 3d1daea9810..a640e3a52af 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -257,7 +257,7 @@ def _google_temp_unit(units): def _next_selected(items: list[str], selected: str | None) -> str | None: - """Return the next item in a item list starting at given value. + """Return the next item in an item list starting at given value. If selected is missing in items, None is returned """ From 532f6d1d97bff30b8705e0842b80347e0afb3d5d Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Wed, 12 Jun 2024 23:13:12 +0200 Subject: [PATCH 0605/1445] Return override target temp for incomfort climate (#119528) --- homeassistant/components/incomfort/climate.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/incomfort/climate.py b/homeassistant/components/incomfort/climate.py index c55c9410f87..dc08ce8a6c0 100644 --- a/homeassistant/components/incomfort/climate.py +++ b/homeassistant/components/incomfort/climate.py @@ -86,8 +86,12 @@ class InComfortClimate(IncomfortEntity, ClimateEntity): @property def target_temperature(self) -> float | None: - """Return the temperature we try to reach.""" - return self._room.setpoint + """Return the (override)temperature we try to reach. + + As we set the override, we report back the override. The actual set point is + is returned at a later time. + """ + return self._room.override async def async_set_temperature(self, **kwargs: Any) -> None: """Set a new target temperature for this zone.""" From fd83b9a7c6ae24156874903ece4c5bc2e557f4ff Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Wed, 12 Jun 2024 23:34:01 +0200 Subject: [PATCH 0606/1445] Add missing attribute translations to water heater entity component (#119531) --- .../components/water_heater/strings.json | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/homeassistant/components/water_heater/strings.json b/homeassistant/components/water_heater/strings.json index 956cfe76b63..741b277d84d 100644 --- a/homeassistant/components/water_heater/strings.json +++ b/homeassistant/components/water_heater/strings.json @@ -18,6 +18,24 @@ "performance": "Performance" }, "state_attributes": { + "current_operation": { + "name": "Current operation" + }, + "current_temperature": { + "name": "Current temperature" + }, + "max_temp": { + "name": "Max target temperature" + }, + "min_temp": { + "name": "Min target temperature" + }, + "target_temp_high": { + "name": "Upper target temperature" + }, + "target_temp_low": { + "name": "Lower target temperature" + }, "away_mode": { "name": "Away mode", "state": { From 4e121fcbe8bad583052a26b2a33e3bb88122aa40 Mon Sep 17 00:00:00 2001 From: Josef Zweck <24647999+zweckj@users.noreply.github.com> Date: Wed, 12 Jun 2024 23:35:51 +0200 Subject: [PATCH 0607/1445] Remove steam temp sensor for Linea Mini (#119423) --- homeassistant/components/lamarzocco/sensor.py | 4 +++- tests/components/lamarzocco/test_sensor.py | 15 +++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/lamarzocco/sensor.py b/homeassistant/components/lamarzocco/sensor.py index c43ea0f99bc..225f0a43c5c 100644 --- a/homeassistant/components/lamarzocco/sensor.py +++ b/homeassistant/components/lamarzocco/sensor.py @@ -3,7 +3,7 @@ from collections.abc import Callable from dataclasses import dataclass -from lmcloud.const import BoilerType, PhysicalKey +from lmcloud.const import BoilerType, MachineModel, PhysicalKey from lmcloud.lm_machine import LaMarzoccoMachine from homeassistant.components.sensor import ( @@ -80,6 +80,8 @@ ENTITIES: tuple[LaMarzoccoSensorEntityDescription, ...] = ( value_fn=lambda device: device.config.boilers[ BoilerType.STEAM ].current_temperature, + supported_fn=lambda coordinator: coordinator.device.model + != MachineModel.LINEA_MINI, ), ) diff --git a/tests/components/lamarzocco/test_sensor.py b/tests/components/lamarzocco/test_sensor.py index b5f551309b6..1ce56724fa3 100644 --- a/tests/components/lamarzocco/test_sensor.py +++ b/tests/components/lamarzocco/test_sensor.py @@ -2,6 +2,7 @@ from unittest.mock import MagicMock +from lmcloud.const import MachineModel import pytest from syrupy import SnapshotAssertion @@ -71,3 +72,17 @@ async def test_shot_timer_unavailable( state = hass.states.get(f"sensor.{mock_lamarzocco.serial_number}_shot_timer") assert state assert state.state == STATE_UNAVAILABLE + + +@pytest.mark.parametrize("device_fixture", [MachineModel.LINEA_MINI]) +async def test_no_steam_linea_mini( + hass: HomeAssistant, + mock_lamarzocco: MagicMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Ensure Linea Mini has no steam temp.""" + await async_init_integration(hass, mock_config_entry) + + serial_number = mock_lamarzocco.serial_number + state = hass.states.get(f"sensor.{serial_number}_current_temp_steam") + assert state is None From dbd3147c9b5fa4c05bf9280133d3fa8824a9d934 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 12 Jun 2024 20:06:11 -0500 Subject: [PATCH 0608/1445] Remove `async_late_forward_entry_setups` and instead implicitly hold the lock (#119088) * Refactor config entry forwards to implictly obtain the lock instead of explictly This is a bit of a tradeoff to not need async_late_forward_entry_setups The downside is we can no longer detect non-awaited plastform setups as we will always implicitly obtain the lock instead of explictly. Note, this PR is incomplete and is only for discussion purposes at this point * preen * cover * cover * restore check for non-awaited platform setup * cleanup * fix missing word * make non-awaited test safer --- .../components/ambient_station/__init__.py | 2 +- .../components/cert_expiry/__init__.py | 2 +- .../components/esphome/entry_data.py | 11 +- homeassistant/components/esphome/manager.py | 2 +- homeassistant/components/mqtt/__init__.py | 4 +- homeassistant/components/mqtt/discovery.py | 2 +- homeassistant/components/mqtt/util.py | 12 +- .../components/shelly/coordinator.py | 4 +- homeassistant/components/zwave_js/__init__.py | 2 +- homeassistant/config_entries.py | 116 +++--- .../components/assist_pipeline/test_select.py | 6 +- tests/components/hue/conftest.py | 2 +- tests/components/hue/test_light_v1.py | 2 +- tests/components/hue/test_sensor_v2.py | 4 +- .../mobile_app/test_device_tracker.py | 2 +- tests/components/smartthings/conftest.py | 2 +- tests/ignore_uncaught_exceptions.py | 6 + tests/test_config_entries.py | 388 ++++++++++++++---- 18 files changed, 381 insertions(+), 188 deletions(-) diff --git a/homeassistant/components/ambient_station/__init__.py b/homeassistant/components/ambient_station/__init__.py index aded84427a5..d0b04e53e67 100644 --- a/homeassistant/components/ambient_station/__init__.py +++ b/homeassistant/components/ambient_station/__init__.py @@ -182,7 +182,7 @@ class AmbientStation: # already been done): if not self._entry_setup_complete: self._hass.async_create_task( - self._hass.config_entries.async_late_forward_entry_setups( + self._hass.config_entries.async_forward_entry_setups( self._entry, PLATFORMS ), eager_start=True, diff --git a/homeassistant/components/cert_expiry/__init__.py b/homeassistant/components/cert_expiry/__init__.py index 2a59b10588f..bc6ae29ee8e 100644 --- a/homeassistant/components/cert_expiry/__init__.py +++ b/homeassistant/components/cert_expiry/__init__.py @@ -28,7 +28,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: CertExpiryConfigEntry) - async def _async_finish_startup(_: HomeAssistant) -> None: await coordinator.async_refresh() - await hass.config_entries.async_late_forward_entry_setups(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) async_at_started(hass, _async_finish_startup) return True diff --git a/homeassistant/components/esphome/entry_data.py b/homeassistant/components/esphome/entry_data.py index c45a6dcf253..494669ae839 100644 --- a/homeassistant/components/esphome/entry_data.py +++ b/homeassistant/components/esphome/entry_data.py @@ -248,16 +248,10 @@ class RuntimeEntryData: hass: HomeAssistant, entry: ConfigEntry, platforms: set[Platform], - late: bool, ) -> None: async with self.platform_load_lock: if needed := platforms - self.loaded_platforms: - if late: - await hass.config_entries.async_late_forward_entry_setups( - entry, needed - ) - else: - await hass.config_entries.async_forward_entry_setups(entry, needed) + await hass.config_entries.async_forward_entry_setups(entry, needed) self.loaded_platforms |= needed async def async_update_static_infos( @@ -266,7 +260,6 @@ class RuntimeEntryData: entry: ConfigEntry, infos: list[EntityInfo], mac: str, - late: bool = False, ) -> None: """Distribute an update of static infos to all platforms.""" # First, load all platforms @@ -296,7 +289,7 @@ class RuntimeEntryData: ): ent_reg.async_update_entity(old_entry, new_unique_id=new_unique_id) - await self._ensure_platforms_loaded(hass, entry, needed_platforms, late) + await self._ensure_platforms_loaded(hass, entry, needed_platforms) # Make a dict of the EntityInfo by type and send # them to the listeners for each specific EntityInfo type diff --git a/homeassistant/components/esphome/manager.py b/homeassistant/components/esphome/manager.py index 09a751eb72e..f191c36c574 100644 --- a/homeassistant/components/esphome/manager.py +++ b/homeassistant/components/esphome/manager.py @@ -491,7 +491,7 @@ class ESPHomeManager: entry_data.async_update_device_state() await entry_data.async_update_static_infos( - hass, entry, entity_infos, device_info.mac_address, late=True + hass, entry, entity_infos, device_info.mac_address ) _setup_services(hass, entry_data, services) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 687e1b14247..ea520e88366 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -379,9 +379,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: new_config: list[ConfigType] = config_yaml.get(DOMAIN, []) platforms_used = platforms_from_config(new_config) new_platforms = platforms_used - mqtt_data.platforms_loaded - await async_forward_entry_setup_and_setup_discovery( - hass, entry, new_platforms, late=True - ) + await async_forward_entry_setup_and_setup_discovery(hass, entry, new_platforms) # Check the schema before continuing reload await async_check_config_schema(hass, config_yaml) diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py index 2ee7dffc18f..0d93af26a57 100644 --- a/homeassistant/components/mqtt/discovery.py +++ b/homeassistant/components/mqtt/discovery.py @@ -211,7 +211,7 @@ async def async_start( # noqa: C901 async with platform_setup_lock.setdefault(component, asyncio.Lock()): if component not in mqtt_data.platforms_loaded: await async_forward_entry_setup_and_setup_discovery( - hass, config_entry, {component}, late=True + hass, config_entry, {component} ) _async_add_component(discovery_payload) diff --git a/homeassistant/components/mqtt/util.py b/homeassistant/components/mqtt/util.py index 747a2c43f76..256bad71ba6 100644 --- a/homeassistant/components/mqtt/util.py +++ b/homeassistant/components/mqtt/util.py @@ -72,11 +72,13 @@ async def async_forward_entry_setup_and_setup_discovery( tasks.append(create_eager_task(tag.async_setup_entry(hass, config_entry))) if new_entity_platforms := (new_platforms - {"tag", "device_automation"}): - if late: - coro = hass.config_entries.async_late_forward_entry_setups - else: - coro = hass.config_entries.async_forward_entry_setups - tasks.append(create_eager_task(coro(config_entry, new_entity_platforms))) + tasks.append( + create_eager_task( + hass.config_entries.async_forward_entry_setups( + config_entry, new_entity_platforms + ) + ) + ) if not tasks: return await asyncio.gather(*tasks) diff --git a/homeassistant/components/shelly/coordinator.py b/homeassistant/components/shelly/coordinator.py index 3415f1b22db..5bb05d48d62 100644 --- a/homeassistant/components/shelly/coordinator.py +++ b/homeassistant/components/shelly/coordinator.py @@ -200,9 +200,7 @@ class ShellyCoordinatorBase[_DeviceT: BlockDevice | RpcDevice]( self.hass.config_entries.async_update_entry(self.entry, data=data) # Resume platform setup - await self.hass.config_entries.async_late_forward_entry_setups( - self.entry, platforms - ) + await self.hass.config_entries.async_forward_entry_setups(self.entry, platforms) return True diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index 4b0cc4ac7a9..2b10f415bb7 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -330,7 +330,7 @@ class DriverEvents: """Set up platform if needed.""" if platform not in self.platform_setup_tasks: self.platform_setup_tasks[platform] = self.hass.async_create_task( - self.hass.config_entries.async_late_forward_entry_setups( + self.hass.config_entries.async_forward_entry_setups( self.config_entry, [platform] ) ) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 1ca6e99f262..fdcf4ad7604 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -1170,18 +1170,13 @@ class FlowCancelledError(Exception): """Error to indicate that a flow has been cancelled.""" -def _report_non_locked_platform_forwards(entry: ConfigEntry) -> None: - """Report non awaited and non-locked platform forwards.""" +def _report_non_awaited_platform_forwards(entry: ConfigEntry, what: str) -> None: + """Report non awaited platform forwards.""" report( - f"calls async_forward_entry_setup after the entry for " - f"integration, {entry.domain} with title: {entry.title} " - f"and entry_id: {entry.entry_id}, has been set up, " - "without holding the setup lock that prevents the config " - "entry from being set up multiple times. " - "Instead await hass.config_entries.async_forward_entry_setup " - "during setup of the config entry or call " - "hass.config_entries.async_late_forward_entry_setups " - "in a tracked task. " + f"calls {what} for integration {entry.domain} with " + f"title: {entry.title} and entry_id: {entry.entry_id}, " + f"during setup without awaiting {what}, which can cause " + "the setup lock to be released before the setup is done. " "This will stop working in Home Assistant 2025.1", error_if_integration=False, error_if_core=False, @@ -2041,9 +2036,6 @@ class ConfigEntries: before the entry is set up. This ensures that the config entry cannot be unloaded before all platforms are loaded. - If platforms must be loaded late (after the config entry is setup), - use async_late_forward_entry_setup instead. - This method is more efficient than async_forward_entry_setup as it can load multiple platforms at once and does not require a separate import executor job for each platform. @@ -2052,14 +2044,32 @@ class ConfigEntries: if not integration.platforms_are_loaded(platforms): with async_pause_setup(self.hass, SetupPhases.WAIT_IMPORT_PLATFORMS): await integration.async_get_platforms(platforms) - if non_locked_platform_forwards := not entry.setup_lock.locked(): - _report_non_locked_platform_forwards(entry) + + if not entry.setup_lock.locked(): + async with entry.setup_lock: + if entry.state is not ConfigEntryState.LOADED: + raise OperationNotAllowed( + f"The config entry {entry.title} ({entry.domain}) with entry_id" + f" {entry.entry_id} cannot forward setup for {platforms} because it" + f" is not loaded in the {entry.state} state" + ) + await self._async_forward_entry_setups_locked(entry, platforms) + else: + await self._async_forward_entry_setups_locked(entry, platforms) + # If the lock was held when we stated, and it was released during + # the platform setup, it means they did not await the setup call. + if not entry.setup_lock.locked(): + _report_non_awaited_platform_forwards( + entry, "async_forward_entry_setups" + ) + + async def _async_forward_entry_setups_locked( + self, entry: ConfigEntry, platforms: Iterable[Platform | str] + ) -> None: await asyncio.gather( *( create_eager_task( - self._async_forward_entry_setup( - entry, platform, False, non_locked_platform_forwards - ), + self._async_forward_entry_setup(entry, platform, False), name=( f"config entry forward setup {entry.title} " f"{entry.domain} {entry.entry_id} {platform}" @@ -2070,25 +2080,6 @@ class ConfigEntries: ) ) - async def async_late_forward_entry_setups( - self, entry: ConfigEntry, platforms: Iterable[Platform | str] - ) -> None: - """Forward the setup of an entry to platforms after setup. - - If platforms must be loaded late (after the config entry is setup), - use this method instead of async_forward_entry_setups as it holds - the setup lock until the platforms are loaded to ensure that the - config entry cannot be unloaded while platforms are loaded. - """ - async with entry.setup_lock: - if entry.state is not ConfigEntryState.LOADED: - raise OperationNotAllowed( - f"The config entry {entry.title} ({entry.domain}) with entry_id" - f" {entry.entry_id} cannot forward setup for {platforms} " - f"because it is not loaded in the {entry.state} state" - ) - await self.async_forward_entry_setups(entry, platforms) - async def async_forward_entry_setup( self, entry: ConfigEntry, domain: Platform | str ) -> bool: @@ -2103,32 +2094,37 @@ class ConfigEntries: Instead, await async_forward_entry_setups as it can load multiple platforms at once and is more efficient since it does not require a separate import executor job for each platform. - - If platforms must be loaded late (after the config entry is setup), - use async_late_forward_entry_setup instead. """ - if non_locked_platform_forwards := not entry.setup_lock.locked(): - _report_non_locked_platform_forwards(entry) - else: - report( - "calls async_forward_entry_setup for " - f"integration, {entry.domain} with title: {entry.title} " - f"and entry_id: {entry.entry_id}, which is deprecated and " - "will stop working in Home Assistant 2025.6, " - "await async_forward_entry_setups instead", - error_if_core=False, - error_if_integration=False, - ) - return await self._async_forward_entry_setup( - entry, domain, True, non_locked_platform_forwards + report( + "calls async_forward_entry_setup for " + f"integration, {entry.domain} with title: {entry.title} " + f"and entry_id: {entry.entry_id}, which is deprecated and " + "will stop working in Home Assistant 2025.6, " + "await async_forward_entry_setups instead", + error_if_core=False, + error_if_integration=False, ) + if not entry.setup_lock.locked(): + async with entry.setup_lock: + if entry.state is not ConfigEntryState.LOADED: + raise OperationNotAllowed( + f"The config entry {entry.title} ({entry.domain}) with entry_id" + f" {entry.entry_id} cannot forward setup for {domain} because it" + f" is not loaded in the {entry.state} state" + ) + return await self._async_forward_entry_setup(entry, domain, True) + result = await self._async_forward_entry_setup(entry, domain, True) + # If the lock was held when we stated, and it was released during + # the platform setup, it means they did not await the setup call. + if not entry.setup_lock.locked(): + _report_non_awaited_platform_forwards(entry, "async_forward_entry_setup") + return result async def _async_forward_entry_setup( self, entry: ConfigEntry, domain: Platform | str, preload_platform: bool, - non_locked_platform_forwards: bool, ) -> bool: """Forward the setup of an entry to a different component.""" # Setup Component if not set up yet @@ -2152,12 +2148,6 @@ class ConfigEntries: integration = loader.async_get_loaded_integration(self.hass, domain) await entry.async_setup(self.hass, integration=integration) - - # Check again after setup to make sure the lock - # is still there because it could have been released - # unless we already reported it. - if not non_locked_platform_forwards and not entry.setup_lock.locked(): - _report_non_locked_platform_forwards(entry) return True async def async_unload_platforms( @@ -2221,7 +2211,7 @@ class ConfigEntries: # The component was not loaded. if entry.domain not in self.hass.config.components: return False - return entry.state == ConfigEntryState.LOADED + return entry.state is ConfigEntryState.LOADED @callback diff --git a/tests/components/assist_pipeline/test_select.py b/tests/components/assist_pipeline/test_select.py index 35f1e015d5d..9fb02e228d8 100644 --- a/tests/components/assist_pipeline/test_select.py +++ b/tests/components/assist_pipeline/test_select.py @@ -53,7 +53,7 @@ async def init_select(hass: HomeAssistant, init_components) -> ConfigEntry: domain="assist_pipeline", state=ConfigEntryState.LOADED ) config_entry.add_to_hass(hass) - await hass.config_entries.async_late_forward_entry_setups(config_entry, ["select"]) + await hass.config_entries.async_forward_entry_setups(config_entry, ["select"]) return config_entry @@ -161,7 +161,7 @@ async def test_select_entity_changing_pipelines( # Reload config entry to test selected option persists assert await hass.config_entries.async_forward_entry_unload(config_entry, "select") - await hass.config_entries.async_late_forward_entry_setups(config_entry, ["select"]) + await hass.config_entries.async_forward_entry_setups(config_entry, ["select"]) state = hass.states.get("select.assist_pipeline_test_prefix_pipeline") assert state is not None @@ -209,7 +209,7 @@ async def test_select_entity_changing_vad_sensitivity( # Reload config entry to test selected option persists assert await hass.config_entries.async_forward_entry_unload(config_entry, "select") - await hass.config_entries.async_late_forward_entry_setups(config_entry, ["select"]) + await hass.config_entries.async_forward_entry_setups(config_entry, ["select"]) state = hass.states.get("select.assist_pipeline_test_vad_sensitivity") assert state is not None diff --git a/tests/components/hue/conftest.py b/tests/components/hue/conftest.py index e824e8cb149..fca950d6b7a 100644 --- a/tests/components/hue/conftest.py +++ b/tests/components/hue/conftest.py @@ -278,7 +278,7 @@ async def setup_platform( await hass.async_block_till_done() config_entry.mock_state(hass, ConfigEntryState.LOADED) - await hass.config_entries.async_late_forward_entry_setups(config_entry, platforms) + await hass.config_entries.async_forward_entry_setups(config_entry, platforms) # and make sure it completes before going further await hass.async_block_till_done() diff --git a/tests/components/hue/test_light_v1.py b/tests/components/hue/test_light_v1.py index 3172e834954..21b35e6d5e8 100644 --- a/tests/components/hue/test_light_v1.py +++ b/tests/components/hue/test_light_v1.py @@ -186,7 +186,7 @@ async def setup_bridge(hass: HomeAssistant, mock_bridge_v1): config_entry.mock_state(hass, ConfigEntryState.LOADED) mock_bridge_v1.config_entry = config_entry hass.data[hue.DOMAIN] = {config_entry.entry_id: mock_bridge_v1} - await hass.config_entries.async_late_forward_entry_setups(config_entry, ["light"]) + await hass.config_entries.async_forward_entry_setups(config_entry, ["light"]) # To flush out the service call to update the group await hass.async_block_till_done() diff --git a/tests/components/hue/test_sensor_v2.py b/tests/components/hue/test_sensor_v2.py index ae02c775191..beb86de505b 100644 --- a/tests/components/hue/test_sensor_v2.py +++ b/tests/components/hue/test_sensor_v2.py @@ -75,7 +75,7 @@ async def test_enable_sensor( assert await async_setup_component(hass, hue.DOMAIN, {}) is True await hass.async_block_till_done() - await hass.config_entries.async_late_forward_entry_setups( + await hass.config_entries.async_forward_entry_setups( mock_config_entry_v2, ["sensor"] ) @@ -95,7 +95,7 @@ async def test_enable_sensor( # reload platform and check if entity is correctly there await hass.config_entries.async_forward_entry_unload(mock_config_entry_v2, "sensor") - await hass.config_entries.async_late_forward_entry_setups( + await hass.config_entries.async_forward_entry_setups( mock_config_entry_v2, ["sensor"] ) await hass.async_block_till_done() diff --git a/tests/components/mobile_app/test_device_tracker.py b/tests/components/mobile_app/test_device_tracker.py index 52abe75f966..e3e2ce3227a 100644 --- a/tests/components/mobile_app/test_device_tracker.py +++ b/tests/components/mobile_app/test_device_tracker.py @@ -104,7 +104,7 @@ async def test_restoring_location( # mobile app doesn't support unloading, so we just reload device tracker await hass.config_entries.async_forward_entry_unload(config_entry, "device_tracker") - await hass.config_entries.async_late_forward_entry_setups( + await hass.config_entries.async_forward_entry_setups( config_entry, ["device_tracker"] ) await hass.async_block_till_done() diff --git a/tests/components/smartthings/conftest.py b/tests/components/smartthings/conftest.py index abe7657021c..baef9d9fa82 100644 --- a/tests/components/smartthings/conftest.py +++ b/tests/components/smartthings/conftest.py @@ -71,7 +71,7 @@ async def setup_platform(hass, platform: str, *, devices=None, scenes=None): hass.data[DOMAIN] = {DATA_BROKERS: {config_entry.entry_id: broker}} config_entry.mock_state(hass, ConfigEntryState.LOADED) - await hass.config_entries.async_late_forward_entry_setups(config_entry, [platform]) + await hass.config_entries.async_forward_entry_setups(config_entry, [platform]) await hass.async_block_till_done() return config_entry diff --git a/tests/ignore_uncaught_exceptions.py b/tests/ignore_uncaught_exceptions.py index 7be10571222..c8388207af4 100644 --- a/tests/ignore_uncaught_exceptions.py +++ b/tests/ignore_uncaught_exceptions.py @@ -13,6 +13,12 @@ IGNORE_UNCAUGHT_EXCEPTIONS = [ "tests.helpers.test_event", "test_track_point_in_time_repr", ), + ( + # This test explicitly throws an uncaught exception + # and should not be removed. + "tests.test_config_entries", + "test_config_entry_unloaded_during_platform_setups", + ), ( # This test explicitly throws an uncaught exception # and should not be removed. diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index d410cb4568a..b23b247b7a3 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -35,6 +35,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from homeassistant.setup import async_set_domains_to_be_loaded, async_setup_component +from homeassistant.util.async_ import create_eager_task import homeassistant.util.dt as dt_util from .common import ( @@ -971,7 +972,7 @@ async def test_forward_entry_sets_up_component(hass: HomeAssistant) -> None: ) with patch.object(integration, "async_get_platforms") as mock_async_get_platforms: - await hass.config_entries.async_late_forward_entry_setups(entry, ["forwarded"]) + await hass.config_entries.async_forward_entry_setups(entry, ["forwarded"]) mock_async_get_platforms.assert_called_once_with(["forwarded"]) assert len(mock_original_setup_entry.mock_calls) == 0 @@ -1001,7 +1002,7 @@ async def test_forward_entry_does_not_setup_entry_if_setup_fails( ) with patch.object(integration, "async_get_platforms"): - await hass.config_entries.async_late_forward_entry_setups(entry, ["forwarded"]) + await hass.config_entries.async_forward_entry_setups(entry, ["forwarded"]) assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 0 @@ -1028,23 +1029,7 @@ async def test_async_forward_entry_setup_deprecated( ), ) - with patch.object(integration, "async_get_platforms"): - await hass.config_entries.async_forward_entry_setup(entry, "forwarded") - assert len(mock_setup.mock_calls) == 1 - assert len(mock_setup_entry.mock_calls) == 0 entry_id = entry.entry_id - assert ( - "Detected code that calls async_forward_entry_setup after the entry " - "for integration, original with title: Mock Title and entry_id: " - f"{entry_id}, has been set up, without holding the setup lock that " - "prevents the config entry from being set up multiple times. " - "Instead await hass.config_entries.async_forward_entry_setup " - "during setup of the config entry or call " - "hass.config_entries.async_late_forward_entry_setups " - "in a tracked task. This will stop working in Home Assistant " - "2025.1. Please report this issue." - ) in caplog.text - caplog.clear() with patch.object(integration, "async_get_platforms"): async with entry.setup_lock: @@ -5553,77 +5538,7 @@ async def test_raise_wrong_exception_in_forwarded_platform( ) -async def test_non_awaited_async_forward_entry_setups( - hass: HomeAssistant, - manager: config_entries.ConfigEntries, - caplog: pytest.LogCaptureFixture, -) -> None: - """Test async_forward_entry_setups not being awaited.""" - - async def mock_setup_entry( - hass: HomeAssistant, entry: config_entries.ConfigEntry - ) -> bool: - """Mock setting up entry.""" - # Call async_forward_entry_setups without awaiting it - # This is not allowed and will raise a warning - hass.async_create_task( - hass.config_entries.async_forward_entry_setups(entry, ["light"]) - ) - return True - - async def mock_unload_entry( - hass: HomeAssistant, entry: config_entries.ConfigEntry - ) -> bool: - """Mock unloading an entry.""" - result = await hass.config_entries.async_unload_platforms(entry, ["light"]) - assert result - return result - - mock_remove_entry = AsyncMock(return_value=None) - - async def mock_setup_entry_platform( - hass: HomeAssistant, - entry: config_entries.ConfigEntry, - async_add_entities: AddEntitiesCallback, - ) -> None: - """Mock setting up platform.""" - await asyncio.sleep(0) - - mock_integration( - hass, - MockModule( - "test", - async_setup_entry=mock_setup_entry, - async_unload_entry=mock_unload_entry, - async_remove_entry=mock_remove_entry, - ), - ) - mock_platform( - hass, "test.light", MockPlatform(async_setup_entry=mock_setup_entry_platform) - ) - mock_platform(hass, "test.config_flow", None) - - entry = MockConfigEntry(domain="test", entry_id="test2") - entry.add_to_manager(manager) - - # Setup entry - await manager.async_setup(entry.entry_id) - await hass.async_block_till_done() - - assert ( - "Detected code that calls async_forward_entry_setup after the " - "entry for integration, test with title: Mock Title and entry_id:" - " test2, has been set up, without holding the setup lock that " - "prevents the config entry from being set up multiple times. " - "Instead await hass.config_entries.async_forward_entry_setup " - "during setup of the config entry or call " - "hass.config_entries.async_late_forward_entry_setups " - "in a tracked task. This will stop working in Home Assistant" - " 2025.1. Please report this issue." - ) in caplog.text - - -async def test_config_entry_unloaded_during_platform_setup( +async def test_config_entry_unloaded_during_platform_setups( hass: HomeAssistant, manager: config_entries.ConfigEntries, caplog: pytest.LogCaptureFixture, @@ -5636,12 +5551,12 @@ async def test_config_entry_unloaded_during_platform_setup( ) -> bool: """Mock setting up entry.""" - # Call async_late_forward_entry_setups in a non-tracked task + # Call async_forward_entry_setups in a non-tracked task # so we can unload the config entry during the setup def _late_setup(): nonlocal task task = asyncio.create_task( - hass.config_entries.async_late_forward_entry_setups(entry, ["light"]) + hass.config_entries.async_forward_entry_setups(entry, ["light"]) ) hass.loop.call_soon(_late_setup) @@ -5695,3 +5610,294 @@ async def test_config_entry_unloaded_during_platform_setup( "entry_id test2 cannot forward setup for ['light'] because it is " "not loaded in the ConfigEntryState.NOT_LOADED state" ) in caplog.text + + +async def test_non_awaited_async_forward_entry_setups( + hass: HomeAssistant, + manager: config_entries.ConfigEntries, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test async_forward_entry_setups not being awaited.""" + forward_event = asyncio.Event() + task: asyncio.Task | None = None + + async def mock_setup_entry( + hass: HomeAssistant, entry: config_entries.ConfigEntry + ) -> bool: + """Mock setting up entry.""" + # Call async_forward_entry_setups without awaiting it + # This is not allowed and will raise a warning + nonlocal task + task = create_eager_task( + hass.config_entries.async_forward_entry_setups(entry, ["light"]) + ) + return True + + async def mock_unload_entry( + hass: HomeAssistant, entry: config_entries.ConfigEntry + ) -> bool: + """Mock unloading an entry.""" + result = await hass.config_entries.async_unload_platforms(entry, ["light"]) + assert result + return result + + mock_remove_entry = AsyncMock(return_value=None) + + async def mock_setup_entry_platform( + hass: HomeAssistant, + entry: config_entries.ConfigEntry, + async_add_entities: AddEntitiesCallback, + ) -> None: + """Mock setting up platform.""" + await forward_event.wait() + + mock_integration( + hass, + MockModule( + "test", + async_setup_entry=mock_setup_entry, + async_unload_entry=mock_unload_entry, + async_remove_entry=mock_remove_entry, + ), + ) + mock_platform( + hass, "test.light", MockPlatform(async_setup_entry=mock_setup_entry_platform) + ) + mock_platform(hass, "test.config_flow", None) + + entry = MockConfigEntry(domain="test", entry_id="test2") + entry.add_to_manager(manager) + + # Setup entry + await manager.async_setup(entry.entry_id) + await hass.async_block_till_done() + forward_event.set() + await hass.async_block_till_done() + await task + + assert ( + "Detected code that calls async_forward_entry_setups for integration " + "test with title: Mock Title and entry_id: test2, during setup without " + "awaiting async_forward_entry_setups, which can cause the setup lock " + "to be released before the setup is done. This will stop working in " + "Home Assistant 2025.1. Please report this issue." + ) in caplog.text + + +async def test_non_awaited_async_forward_entry_setup( + hass: HomeAssistant, + manager: config_entries.ConfigEntries, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test async_forward_entry_setup not being awaited.""" + forward_event = asyncio.Event() + task: asyncio.Task | None = None + + async def mock_setup_entry( + hass: HomeAssistant, entry: config_entries.ConfigEntry + ) -> bool: + """Mock setting up entry.""" + # Call async_forward_entry_setup without awaiting it + # This is not allowed and will raise a warning + nonlocal task + task = create_eager_task( + hass.config_entries.async_forward_entry_setup(entry, "light") + ) + return True + + async def mock_unload_entry( + hass: HomeAssistant, entry: config_entries.ConfigEntry + ) -> bool: + """Mock unloading an entry.""" + result = await hass.config_entries.async_unload_platforms(entry, ["light"]) + assert result + return result + + mock_remove_entry = AsyncMock(return_value=None) + + async def mock_setup_entry_platform( + hass: HomeAssistant, + entry: config_entries.ConfigEntry, + async_add_entities: AddEntitiesCallback, + ) -> None: + """Mock setting up platform.""" + await forward_event.wait() + + mock_integration( + hass, + MockModule( + "test", + async_setup_entry=mock_setup_entry, + async_unload_entry=mock_unload_entry, + async_remove_entry=mock_remove_entry, + ), + ) + mock_platform( + hass, "test.light", MockPlatform(async_setup_entry=mock_setup_entry_platform) + ) + mock_platform(hass, "test.config_flow", None) + + entry = MockConfigEntry(domain="test", entry_id="test2") + entry.add_to_manager(manager) + + # Setup entry + await manager.async_setup(entry.entry_id) + await hass.async_block_till_done() + forward_event.set() + await hass.async_block_till_done() + await task + + assert ( + "Detected code that calls async_forward_entry_setup for integration " + "test with title: Mock Title and entry_id: test2, during setup without " + "awaiting async_forward_entry_setup, which can cause the setup lock " + "to be released before the setup is done. This will stop working in " + "Home Assistant 2025.1. Please report this issue." + ) in caplog.text + + +async def test_config_entry_unloaded_during_platform_setup( + hass: HomeAssistant, + manager: config_entries.ConfigEntries, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test async_forward_entry_setup not being awaited.""" + task = None + + async def mock_setup_entry( + hass: HomeAssistant, entry: config_entries.ConfigEntry + ) -> bool: + """Mock setting up entry.""" + + # Call async_forward_entry_setup in a non-tracked task + # so we can unload the config entry during the setup + def _late_setup(): + nonlocal task + task = asyncio.create_task( + hass.config_entries.async_forward_entry_setup(entry, "light") + ) + + hass.loop.call_soon(_late_setup) + return True + + async def mock_unload_entry( + hass: HomeAssistant, entry: config_entries.ConfigEntry + ) -> bool: + """Mock unloading an entry.""" + result = await hass.config_entries.async_unload_platforms(entry, ["light"]) + assert result + return result + + mock_remove_entry = AsyncMock(return_value=None) + + async def mock_setup_entry_platform( + hass: HomeAssistant, + entry: config_entries.ConfigEntry, + async_add_entities: AddEntitiesCallback, + ) -> None: + """Mock setting up platform.""" + await asyncio.sleep(0) + await asyncio.sleep(0) + + mock_integration( + hass, + MockModule( + "test", + async_setup_entry=mock_setup_entry, + async_unload_entry=mock_unload_entry, + async_remove_entry=mock_remove_entry, + ), + ) + mock_platform( + hass, "test.light", MockPlatform(async_setup_entry=mock_setup_entry_platform) + ) + mock_platform(hass, "test.config_flow", None) + + entry = MockConfigEntry(domain="test", entry_id="test2") + entry.add_to_manager(manager) + + # Setup entry + await manager.async_setup(entry.entry_id) + await hass.async_block_till_done() + await manager.async_unload(entry.entry_id) + await hass.async_block_till_done() + del task + + assert ( + "OperationNotAllowed: The config entry Mock Title (test) with " + "entry_id test2 cannot forward setup for light because it is " + "not loaded in the ConfigEntryState.NOT_LOADED state" + ) in caplog.text + + +async def test_config_entry_late_platform_setup( + hass: HomeAssistant, + manager: config_entries.ConfigEntries, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test async_forward_entry_setup not being awaited.""" + task = None + + async def mock_setup_entry( + hass: HomeAssistant, entry: config_entries.ConfigEntry + ) -> bool: + """Mock setting up entry.""" + + # Call async_forward_entry_setup in a non-tracked task + # so we can unload the config entry during the setup + def _late_setup(): + nonlocal task + task = asyncio.create_task( + hass.config_entries.async_forward_entry_setup(entry, "light") + ) + + hass.loop.call_soon(_late_setup) + return True + + async def mock_unload_entry( + hass: HomeAssistant, entry: config_entries.ConfigEntry + ) -> bool: + """Mock unloading an entry.""" + result = await hass.config_entries.async_unload_platforms(entry, ["light"]) + assert result + return result + + mock_remove_entry = AsyncMock(return_value=None) + + async def mock_setup_entry_platform( + hass: HomeAssistant, + entry: config_entries.ConfigEntry, + async_add_entities: AddEntitiesCallback, + ) -> None: + """Mock setting up platform.""" + await asyncio.sleep(0) + await asyncio.sleep(0) + + mock_integration( + hass, + MockModule( + "test", + async_setup_entry=mock_setup_entry, + async_unload_entry=mock_unload_entry, + async_remove_entry=mock_remove_entry, + ), + ) + mock_platform( + hass, "test.light", MockPlatform(async_setup_entry=mock_setup_entry_platform) + ) + mock_platform(hass, "test.config_flow", None) + + entry = MockConfigEntry(domain="test", entry_id="test2") + entry.add_to_manager(manager) + + # Setup entry + await manager.async_setup(entry.entry_id) + await hass.async_block_till_done() + await task + await hass.async_block_till_done() + + assert ( + "OperationNotAllowed: The config entry Mock Title (test) with " + "entry_id test2 cannot forward setup for light because it is " + "not loaded in the ConfigEntryState.NOT_LOADED state" + ) not in caplog.text From dda6ccccd22c6019f62228637f0d075f8bc65e14 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 13 Jun 2024 08:32:55 +0200 Subject: [PATCH 0609/1445] Fix dangerous-default-value in nest tests (#119561) * Fix dangerous-default-value in nest tests * Adjust * Adjust --- tests/components/nest/test_device_trigger.py | 6 +++++- tests/components/nest/test_events.py | 4 ++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/tests/components/nest/test_device_trigger.py b/tests/components/nest/test_device_trigger.py index 759fb56d213..1820096d2a6 100644 --- a/tests/components/nest/test_device_trigger.py +++ b/tests/components/nest/test_device_trigger.py @@ -1,5 +1,7 @@ """The tests for Nest device triggers.""" +from typing import Any + from google_nest_sdm.event import EventMessage import pytest from pytest_unordered import unordered @@ -30,7 +32,9 @@ def platforms() -> list[str]: return ["camera"] -def make_camera(device_id, name=DEVICE_NAME, traits={}): +def make_camera( + device_id, name: str = DEVICE_NAME, *, traits: dict[str, Any] +) -> dict[str, Any]: """Create a nest camera.""" traits = traits.copy() traits.update( diff --git a/tests/components/nest/test_events.py b/tests/components/nest/test_events.py index f817378aea1..08cf9f775b7 100644 --- a/tests/components/nest/test_events.py +++ b/tests/components/nest/test_events.py @@ -53,7 +53,7 @@ def device_traits() -> list[str]: @pytest.fixture(autouse=True) def device( - device_type: str, device_traits: dict[str, Any], create_device: CreateDevice + device_type: str, device_traits: list[str], create_device: CreateDevice ) -> None: """Fixture to create a device under test.""" return create_device.create( @@ -70,7 +70,7 @@ def event_view(d: Mapping[str, Any]) -> Mapping[str, Any]: return {key: value for key, value in d.items() if key in EVENT_KEYS} -def create_device_traits(event_traits=[]): +def create_device_traits(event_traits: list[str]) -> dict[str, Any]: """Create fake traits for a device.""" result = { "sdm.devices.traits.Info": { From 669569ca49a89d6f2def4a511453e9ec9ffc17c8 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 13 Jun 2024 08:35:05 +0200 Subject: [PATCH 0610/1445] Fix dangerous-default-value in zha tests (#119560) --- tests/components/zha/conftest.py | 4 ++-- tests/components/zha/test_binary_sensor.py | 6 ++++-- tests/components/zha/test_light.py | 8 +++++--- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/tests/components/zha/conftest.py b/tests/components/zha/conftest.py index 97388fd17cc..e75a84406d6 100644 --- a/tests/components/zha/conftest.py +++ b/tests/components/zha/conftest.py @@ -520,10 +520,10 @@ def network_backup() -> zigpy.backups.NetworkBackup: @pytest.fixture -def core_rs(hass_storage: dict[str, Any]): +def core_rs(hass_storage: dict[str, Any]) -> Callable[[str, Any, dict[str, Any]], None]: """Core.restore_state fixture.""" - def _storage(entity_id, state, attributes={}): + def _storage(entity_id: str, state: str, attributes: dict[str, Any]) -> None: now = dt_util.utcnow().isoformat() hass_storage[restore_state.STORAGE_KEY] = { diff --git a/tests/components/zha/test_binary_sensor.py b/tests/components/zha/test_binary_sensor.py index bd9262a41ce..8276223926d 100644 --- a/tests/components/zha/test_binary_sensor.py +++ b/tests/components/zha/test_binary_sensor.py @@ -1,5 +1,7 @@ """Test ZHA binary sensor.""" +from collections.abc import Callable +from typing import Any from unittest.mock import patch import pytest @@ -158,9 +160,9 @@ async def test_binary_sensor( async def test_onoff_binary_sensor_restore_state( hass: HomeAssistant, zigpy_device_mock, - core_rs, + core_rs: Callable[[str, Any, dict[str, Any]], None], zha_device_restored, - restored_state, + restored_state: str, ) -> None: """Test ZHA OnOff binary_sensor restores last state from HA.""" diff --git a/tests/components/zha/test_light.py b/tests/components/zha/test_light.py index 5d50d708ed6..fda5971cbf7 100644 --- a/tests/components/zha/test_light.py +++ b/tests/components/zha/test_light.py @@ -1,6 +1,8 @@ """Test ZHA light.""" +from collections.abc import Callable from datetime import timedelta +from typing import Any from unittest.mock import AsyncMock, call, patch, sentinel import pytest @@ -1962,10 +1964,10 @@ async def test_group_member_assume_state( async def test_restore_light_state( hass: HomeAssistant, zigpy_device_mock, - core_rs, + core_rs: Callable[[str, Any, dict[str, Any]], None], zha_device_restored, - restored_state, - expected_state, + restored_state: str, + expected_state: dict[str, Any], ) -> None: """Test ZHA light restores without throwing an error when attributes are None.""" From d52ce03aa4b41d2931c671d4a1a0eb4227bd0685 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 13 Jun 2024 01:52:01 -0500 Subject: [PATCH 0611/1445] Ensure asyncio blocking checks are undone after tests run (#119542) * Ensure asyncio blocking checks are undone after tests run * no reason to ever enable twice * we are patching objects, make it more generic * make sure bootstrap unblocks as well * move disable to tests only * re-protect * Update tests/test_block_async_io.py Co-authored-by: Erik Montnemery * Revert "Update tests/test_block_async_io.py" This reverts commit 2d46028e21b4095479302629a201c3cfc811b2c2. * tweak name * fixture only * Update tests/conftest.py * Update tests/conftest.py * Apply suggestions from code review --------- Co-authored-by: Erik Montnemery --- homeassistant/block_async_io.py | 168 +++++++++++++++++++++++--------- tests/conftest.py | 14 +++ tests/test_block_async_io.py | 49 ++++++++-- tests/test_bootstrap.py | 5 + 4 files changed, 184 insertions(+), 52 deletions(-) diff --git a/homeassistant/block_async_io.py b/homeassistant/block_async_io.py index 2dc94fa456a..5b8ba535b5a 100644 --- a/homeassistant/block_async_io.py +++ b/homeassistant/block_async_io.py @@ -1,7 +1,9 @@ """Block blocking calls being done in asyncio.""" import builtins +from collections.abc import Callable from contextlib import suppress +from dataclasses import dataclass import glob from http.client import HTTPConnection import importlib @@ -46,53 +48,131 @@ def _check_sleep_call_allowed(mapped_args: dict[str, Any]) -> bool: return False +@dataclass(slots=True, frozen=True) +class BlockingCall: + """Class to hold information about a blocking call.""" + + original_func: Callable + object: object + function: str + check_allowed: Callable[[dict[str, Any]], bool] | None + strict: bool + strict_core: bool + skip_for_tests: bool + + +_BLOCKING_CALLS: tuple[BlockingCall, ...] = ( + BlockingCall( + original_func=HTTPConnection.putrequest, + object=HTTPConnection, + function="putrequest", + check_allowed=None, + strict=True, + strict_core=True, + skip_for_tests=False, + ), + BlockingCall( + original_func=time.sleep, + object=time, + function="sleep", + check_allowed=_check_sleep_call_allowed, + strict=True, + strict_core=True, + skip_for_tests=False, + ), + BlockingCall( + original_func=glob.glob, + object=glob, + function="glob", + check_allowed=None, + strict=False, + strict_core=False, + skip_for_tests=False, + ), + BlockingCall( + original_func=glob.iglob, + object=glob, + function="iglob", + check_allowed=None, + strict=False, + strict_core=False, + skip_for_tests=False, + ), + BlockingCall( + original_func=os.walk, + object=os, + function="walk", + check_allowed=None, + strict=False, + strict_core=False, + skip_for_tests=False, + ), + BlockingCall( + original_func=os.listdir, + object=os, + function="listdir", + check_allowed=None, + strict=False, + strict_core=False, + skip_for_tests=True, + ), + BlockingCall( + original_func=os.scandir, + object=os, + function="scandir", + check_allowed=None, + strict=False, + strict_core=False, + skip_for_tests=True, + ), + BlockingCall( + original_func=builtins.open, + object=builtins, + function="open", + check_allowed=_check_file_allowed, + strict=False, + strict_core=False, + skip_for_tests=True, + ), + BlockingCall( + original_func=importlib.import_module, + object=importlib, + function="import_module", + check_allowed=_check_import_call_allowed, + strict=False, + strict_core=False, + skip_for_tests=True, + ), +) + + +@dataclass(slots=True) +class BlockedCalls: + """Class to track which calls are blocked.""" + + calls: set[BlockingCall] + + +_BLOCKED_CALLS = BlockedCalls(set()) + + def enable() -> None: """Enable the detection of blocking calls in the event loop.""" + calls = _BLOCKED_CALLS.calls + if calls: + raise RuntimeError("Blocking call detection is already enabled") + loop_thread_id = threading.get_ident() - # Prevent urllib3 and requests doing I/O in event loop - HTTPConnection.putrequest = protect_loop( # type: ignore[method-assign] - HTTPConnection.putrequest, loop_thread_id=loop_thread_id - ) + for blocking_call in _BLOCKING_CALLS: + if _IN_TESTS and blocking_call.skip_for_tests: + continue - # Prevent sleeping in event loop. - time.sleep = protect_loop( - time.sleep, - check_allowed=_check_sleep_call_allowed, - loop_thread_id=loop_thread_id, - ) - - glob.glob = protect_loop( - glob.glob, strict_core=False, strict=False, loop_thread_id=loop_thread_id - ) - glob.iglob = protect_loop( - glob.iglob, strict_core=False, strict=False, loop_thread_id=loop_thread_id - ) - os.walk = protect_loop( - os.walk, strict_core=False, strict=False, loop_thread_id=loop_thread_id - ) - - if not _IN_TESTS: - # Prevent files being opened inside the event loop - os.listdir = protect_loop( # type: ignore[assignment] - os.listdir, strict_core=False, strict=False, loop_thread_id=loop_thread_id - ) - os.scandir = protect_loop( # type: ignore[assignment] - os.scandir, strict_core=False, strict=False, loop_thread_id=loop_thread_id - ) - - builtins.open = protect_loop( # type: ignore[assignment] - builtins.open, - strict_core=False, - strict=False, - check_allowed=_check_file_allowed, - loop_thread_id=loop_thread_id, - ) - # unittest uses `importlib.import_module` to do mocking - # so we cannot protect it if we are running tests - importlib.import_module = protect_loop( - importlib.import_module, - strict_core=False, - strict=False, - check_allowed=_check_import_call_allowed, + protected_function = protect_loop( + blocking_call.original_func, + strict=blocking_call.strict, + strict_core=blocking_call.strict_core, + check_allowed=blocking_call.check_allowed, loop_thread_id=loop_thread_id, ) + setattr(blocking_call.object, blocking_call.function, protected_function) + calls.add(blocking_call) diff --git a/tests/conftest.py b/tests/conftest.py index 1d0ad3d47b3..0bef1a7b06a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -35,6 +35,8 @@ import requests_mock from syrupy.assertion import SnapshotAssertion from typing_extensions import AsyncGenerator, Generator +from homeassistant import block_async_io + # Setup patching if dt_util time functions before any other Home Assistant imports from . import patch_time # noqa: F401, isort:skip @@ -1814,3 +1816,15 @@ def service_calls(hass: HomeAssistant) -> Generator[None, None, list[ServiceCall def snapshot(snapshot: SnapshotAssertion) -> SnapshotAssertion: """Return snapshot assertion fixture with the Home Assistant extension.""" return snapshot.use_extension(HomeAssistantSnapshotExtension) + + +@pytest.fixture +def disable_block_async_io() -> Generator[Any, Any, None]: + """Fixture to disable the loop protection from block_async_io.""" + yield + calls = block_async_io._BLOCKED_CALLS.calls + for blocking_call in calls: + setattr( + blocking_call.object, blocking_call.function, blocking_call.original_func + ) + calls.clear() diff --git a/tests/test_block_async_io.py b/tests/test_block_async_io.py index d011bdccdbe..d823f8c6912 100644 --- a/tests/test_block_async_io.py +++ b/tests/test_block_async_io.py @@ -17,6 +17,11 @@ from homeassistant.core import HomeAssistant from .common import extract_stack_to_frame +@pytest.fixture(autouse=True) +def disable_block_async_io(disable_block_async_io): + """Disable the loop protection from block_async_io after each test.""" + + async def test_protect_loop_debugger_sleep(caplog: pytest.LogCaptureFixture) -> None: """Test time.sleep injected by the debugger is not reported.""" block_async_io.enable() @@ -214,13 +219,25 @@ async def test_protect_loop_open(caplog: pytest.LogCaptureFixture) -> None: async def test_protect_open(caplog: pytest.LogCaptureFixture) -> None: """Test opening a file in the event loop logs.""" - block_async_io.enable() + with patch.object(block_async_io, "_IN_TESTS", False): + block_async_io.enable() with contextlib.suppress(FileNotFoundError): open("/config/data_not_exist", encoding="utf8").close() assert "Detected blocking call to open with args" in caplog.text +async def test_enable_multiple_times(caplog: pytest.LogCaptureFixture) -> None: + """Test trying to enable multiple times.""" + with patch.object(block_async_io, "_IN_TESTS", False): + block_async_io.enable() + + with pytest.raises( + RuntimeError, match="Blocking call detection is already enabled" + ): + block_async_io.enable() + + @pytest.mark.parametrize( "path", [ @@ -231,7 +248,8 @@ async def test_protect_open(caplog: pytest.LogCaptureFixture) -> None: ) async def test_protect_open_path(path: Any, caplog: pytest.LogCaptureFixture) -> None: """Test opening a file by path in the event loop logs.""" - block_async_io.enable() + with patch.object(block_async_io, "_IN_TESTS", False): + block_async_io.enable() with contextlib.suppress(FileNotFoundError): open(path, encoding="utf8").close() @@ -242,7 +260,8 @@ async def test_protect_loop_glob( hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: """Test glob calls in the loop are logged.""" - block_async_io.enable() + with patch.object(block_async_io, "_IN_TESTS", False): + block_async_io.enable() glob.glob("/dev/null") assert "Detected blocking call to glob with args" in caplog.text caplog.clear() @@ -254,7 +273,8 @@ async def test_protect_loop_iglob( hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: """Test iglob calls in the loop are logged.""" - block_async_io.enable() + with patch.object(block_async_io, "_IN_TESTS", False): + block_async_io.enable() glob.iglob("/dev/null") assert "Detected blocking call to iglob with args" in caplog.text caplog.clear() @@ -266,7 +286,8 @@ async def test_protect_loop_scandir( hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: """Test glob calls in the loop are logged.""" - block_async_io.enable() + with patch.object(block_async_io, "_IN_TESTS", False): + block_async_io.enable() with contextlib.suppress(FileNotFoundError): os.scandir("/path/that/does/not/exists") assert "Detected blocking call to scandir with args" in caplog.text @@ -280,7 +301,8 @@ async def test_protect_loop_listdir( hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: """Test listdir calls in the loop are logged.""" - block_async_io.enable() + with patch.object(block_async_io, "_IN_TESTS", False): + block_async_io.enable() with contextlib.suppress(FileNotFoundError): os.listdir("/path/that/does/not/exists") assert "Detected blocking call to listdir with args" in caplog.text @@ -293,8 +315,9 @@ async def test_protect_loop_listdir( async def test_protect_loop_walk( hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: - """Test glob calls in the loop are logged.""" - block_async_io.enable() + """Test os.walk calls in the loop are logged.""" + with patch.object(block_async_io, "_IN_TESTS", False): + block_async_io.enable() with contextlib.suppress(FileNotFoundError): os.walk("/path/that/does/not/exists") assert "Detected blocking call to walk with args" in caplog.text @@ -302,3 +325,13 @@ async def test_protect_loop_walk( with contextlib.suppress(FileNotFoundError): await hass.async_add_executor_job(os.walk, "/path/that/does/not/exists") assert "Detected blocking call to walk with args" not in caplog.text + + +async def test_open_calls_ignored_in_tests(caplog: pytest.LogCaptureFixture) -> None: + """Test opening a file in tests is ignored.""" + assert block_async_io._IN_TESTS + block_async_io.enable() + with contextlib.suppress(FileNotFoundError): + open("/config/data_not_exist", encoding="utf8").close() + + assert "Detected blocking call to open with args" not in caplog.text diff --git a/tests/test_bootstrap.py b/tests/test_bootstrap.py index 9e04421a58a..225720fb604 100644 --- a/tests/test_bootstrap.py +++ b/tests/test_bootstrap.py @@ -55,6 +55,11 @@ async def apply_stop_hass(stop_hass: None) -> None: """Make sure all hass are stopped.""" +@pytest.fixture(autouse=True) +def disable_block_async_io(disable_block_async_io): + """Disable the loop protection from block_async_io after each test.""" + + @pytest.fixture(scope="module", autouse=True) def mock_http_start_stop() -> Generator[None]: """Mock HTTP start and stop.""" From 0a727aba4afa0c82d1602f82b54bd580d5797300 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 13 Jun 2024 08:55:50 +0200 Subject: [PATCH 0612/1445] Bump dawidd6/action-download-artifact from 5 to 6 (#119565) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/builder.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml index 58d9c5a5d28..f0d15ea76d3 100644 --- a/.github/workflows/builder.yml +++ b/.github/workflows/builder.yml @@ -94,7 +94,7 @@ jobs: - name: Download nightly wheels of frontend if: needs.init.outputs.channel == 'dev' - uses: dawidd6/action-download-artifact@v5 + uses: dawidd6/action-download-artifact@v6 with: github_token: ${{secrets.GITHUB_TOKEN}} repo: home-assistant/frontend @@ -105,7 +105,7 @@ jobs: - name: Download nightly wheels of intents if: needs.init.outputs.channel == 'dev' - uses: dawidd6/action-download-artifact@v5 + uses: dawidd6/action-download-artifact@v6 with: github_token: ${{secrets.GITHUB_TOKEN}} repo: home-assistant/intents-package From 4af3879fc2d67d8e60dce2f7cf7bfb10ce305a68 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 13 Jun 2024 08:56:04 +0200 Subject: [PATCH 0613/1445] Bump github/codeql-action from 3.25.8 to 3.25.9 (#119567) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/codeql.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 0ad7747347d..7c36b8b7981 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -24,11 +24,11 @@ jobs: uses: actions/checkout@v4.1.6 - name: Initialize CodeQL - uses: github/codeql-action/init@v3.25.8 + uses: github/codeql-action/init@v3.25.9 with: languages: python - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3.25.8 + uses: github/codeql-action/analyze@v3.25.9 with: category: "/language:python" From 610f21c4a63203623c11150b1f81c63d8d1f2866 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 13 Jun 2024 08:56:14 +0200 Subject: [PATCH 0614/1445] Fix unnecessary-lambda warnings in tests (#119563) --- tests/components/home_connect/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/components/home_connect/conftest.py b/tests/components/home_connect/conftest.py index 895782454fc..f4c19320826 100644 --- a/tests/components/home_connect/conftest.py +++ b/tests/components/home_connect/conftest.py @@ -98,7 +98,7 @@ def mock_bypass_throttle(): """Fixture to bypass the throttle decorator in __init__.""" with patch( "homeassistant.components.home_connect.update_all_devices", - side_effect=lambda x, y: bypass_throttle(x, y), + side_effect=bypass_throttle, ): yield From cad616316205311492d836c748f1a32da1500fec Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Thu, 13 Jun 2024 02:57:28 -0400 Subject: [PATCH 0615/1445] Store runtime data inside the config entry in Tautulli (#119552) --- homeassistant/components/tautulli/__init__.py | 16 ++++++++-------- .../components/tautulli/coordinator.py | 7 +++++-- homeassistant/components/tautulli/sensor.py | 18 ++++++++++-------- 3 files changed, 23 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/tautulli/__init__.py b/homeassistant/components/tautulli/__init__.py index b7fcf48cfdb..7d3efa4f283 100644 --- a/homeassistant/components/tautulli/__init__.py +++ b/homeassistant/components/tautulli/__init__.py @@ -16,9 +16,10 @@ from .const import DEFAULT_NAME, DOMAIN from .coordinator import TautulliDataUpdateCoordinator PLATFORMS = [Platform.SENSOR] +type TautulliConfigEntry = ConfigEntry[TautulliDataUpdateCoordinator] -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: TautulliConfigEntry) -> bool: """Set up Tautulli from a config entry.""" host_configuration = PyTautulliHostConfiguration( api_token=entry.data[CONF_API_KEY], @@ -29,19 +30,18 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: host_configuration=host_configuration, session=async_get_clientsession(hass, entry.data[CONF_VERIFY_SSL]), ) - coordinator = TautulliDataUpdateCoordinator(hass, host_configuration, api_client) - await coordinator.async_config_entry_first_refresh() - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator + entry.runtime_data = TautulliDataUpdateCoordinator( + hass, host_configuration, api_client + ) + await entry.runtime_data.async_config_entry_first_refresh() await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: TautulliConfigEntry) -> bool: """Unload a config entry.""" - if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): - hass.data[DOMAIN].pop(entry.entry_id) - return unload_ok + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) class TautulliEntity(CoordinatorEntity[TautulliDataUpdateCoordinator]): diff --git a/homeassistant/components/tautulli/coordinator.py b/homeassistant/components/tautulli/coordinator.py index be7dfce4e3a..f392ab8df03 100644 --- a/homeassistant/components/tautulli/coordinator.py +++ b/homeassistant/components/tautulli/coordinator.py @@ -4,6 +4,7 @@ from __future__ import annotations import asyncio from datetime import timedelta +from typing import TYPE_CHECKING from pytautulli import ( PyTautulli, @@ -17,18 +18,20 @@ from pytautulli.exceptions import ( ) from pytautulli.models.host_configuration import PyTautulliHostConfiguration -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import DOMAIN, LOGGER +if TYPE_CHECKING: + from . import TautulliConfigEntry + class TautulliDataUpdateCoordinator(DataUpdateCoordinator[None]): """Data update coordinator for the Tautulli integration.""" - config_entry: ConfigEntry + config_entry: TautulliConfigEntry def __init__( self, diff --git a/homeassistant/components/tautulli/sensor.py b/homeassistant/components/tautulli/sensor.py index f0d274bbe12..26b7c602de8 100644 --- a/homeassistant/components/tautulli/sensor.py +++ b/homeassistant/components/tautulli/sensor.py @@ -19,14 +19,14 @@ from homeassistant.components.sensor import ( SensorEntityDescription, SensorStateClass, ) -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import PERCENTAGE, EntityCategory, UnitOfInformation from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import EntityDescription from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType -from . import TautulliEntity +from . import TautulliConfigEntry, TautulliEntity from .const import ATTR_TOP_USER, DOMAIN from .coordinator import TautulliDataUpdateCoordinator @@ -210,26 +210,28 @@ async def async_setup_platform( async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, + entry: TautulliConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up Tautulli sensor.""" - coordinator: TautulliDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + data = entry.runtime_data entities: list[TautulliSensor | TautulliSessionSensor] = [ TautulliSensor( - coordinator, + data, description, ) for description in SENSOR_TYPES ] - if coordinator.users: + if data.users: entities.extend( TautulliSessionSensor( - coordinator, + data, description, user, ) for description in SESSION_SENSOR_TYPES - for user in coordinator.users + for user in data.users if user.username != "Local" ) async_add_entities(entities) From 08403df20e58d71d07d9fe88f4ef9c36fe143a79 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 13 Jun 2024 09:19:26 +0200 Subject: [PATCH 0616/1445] Bump actions/checkout from 4.1.6 to 4.1.7 (#119566) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/builder.yml | 12 +++++------ .github/workflows/ci.yaml | 34 +++++++++++++++--------------- .github/workflows/codeql.yml | 2 +- .github/workflows/translations.yml | 2 +- .github/workflows/wheels.yml | 6 +++--- 5 files changed, 28 insertions(+), 28 deletions(-) diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml index f0d15ea76d3..304a077b808 100644 --- a/.github/workflows/builder.yml +++ b/.github/workflows/builder.yml @@ -27,7 +27,7 @@ jobs: publish: ${{ steps.version.outputs.publish }} steps: - name: Checkout the repository - uses: actions/checkout@v4.1.6 + uses: actions/checkout@v4.1.7 with: fetch-depth: 0 @@ -90,7 +90,7 @@ jobs: arch: ${{ fromJson(needs.init.outputs.architectures) }} steps: - name: Checkout the repository - uses: actions/checkout@v4.1.6 + uses: actions/checkout@v4.1.7 - name: Download nightly wheels of frontend if: needs.init.outputs.channel == 'dev' @@ -242,7 +242,7 @@ jobs: - green steps: - name: Checkout the repository - uses: actions/checkout@v4.1.6 + uses: actions/checkout@v4.1.7 - name: Set build additional args run: | @@ -279,7 +279,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout the repository - uses: actions/checkout@v4.1.6 + uses: actions/checkout@v4.1.7 - name: Initialize git uses: home-assistant/actions/helpers/git-init@master @@ -320,7 +320,7 @@ jobs: registry: ["ghcr.io/home-assistant", "docker.io/homeassistant"] steps: - name: Checkout the repository - uses: actions/checkout@v4.1.6 + uses: actions/checkout@v4.1.7 - name: Install Cosign uses: sigstore/cosign-installer@v3.5.0 @@ -450,7 +450,7 @@ jobs: if: github.repository_owner == 'home-assistant' && needs.init.outputs.publish == 'true' steps: - name: Checkout the repository - uses: actions/checkout@v4.1.6 + uses: actions/checkout@v4.1.7 - name: Set up Python ${{ env.DEFAULT_PYTHON }} uses: actions/setup-python@v5.1.0 diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 499319ff99f..912ca464ef0 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -89,7 +89,7 @@ jobs: runs-on: ubuntu-22.04 steps: - name: Check out code from GitHub - uses: actions/checkout@v4.1.6 + uses: actions/checkout@v4.1.7 - name: Generate partial Python venv restore key id: generate_python_cache_key run: | @@ -226,7 +226,7 @@ jobs: - info steps: - name: Check out code from GitHub - uses: actions/checkout@v4.1.6 + uses: actions/checkout@v4.1.7 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python uses: actions/setup-python@v5.1.0 @@ -272,7 +272,7 @@ jobs: - pre-commit steps: - name: Check out code from GitHub - uses: actions/checkout@v4.1.6 + uses: actions/checkout@v4.1.7 - name: Set up Python ${{ env.DEFAULT_PYTHON }} uses: actions/setup-python@v5.1.0 id: python @@ -312,7 +312,7 @@ jobs: - pre-commit steps: - name: Check out code from GitHub - uses: actions/checkout@v4.1.6 + uses: actions/checkout@v4.1.7 - name: Set up Python ${{ env.DEFAULT_PYTHON }} uses: actions/setup-python@v5.1.0 id: python @@ -351,7 +351,7 @@ jobs: - pre-commit steps: - name: Check out code from GitHub - uses: actions/checkout@v4.1.6 + uses: actions/checkout@v4.1.7 - name: Set up Python ${{ env.DEFAULT_PYTHON }} uses: actions/setup-python@v5.1.0 id: python @@ -445,7 +445,7 @@ jobs: python-version: ${{ fromJSON(needs.info.outputs.python_versions) }} steps: - name: Check out code from GitHub - uses: actions/checkout@v4.1.6 + uses: actions/checkout@v4.1.7 - name: Set up Python ${{ matrix.python-version }} id: python uses: actions/setup-python@v5.1.0 @@ -522,7 +522,7 @@ jobs: - base steps: - name: Check out code from GitHub - uses: actions/checkout@v4.1.6 + uses: actions/checkout@v4.1.7 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python uses: actions/setup-python@v5.1.0 @@ -554,7 +554,7 @@ jobs: - base steps: - name: Check out code from GitHub - uses: actions/checkout@v4.1.6 + uses: actions/checkout@v4.1.7 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python uses: actions/setup-python@v5.1.0 @@ -587,7 +587,7 @@ jobs: - base steps: - name: Check out code from GitHub - uses: actions/checkout@v4.1.6 + uses: actions/checkout@v4.1.7 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python uses: actions/setup-python@v5.1.0 @@ -631,7 +631,7 @@ jobs: - base steps: - name: Check out code from GitHub - uses: actions/checkout@v4.1.6 + uses: actions/checkout@v4.1.7 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python uses: actions/setup-python@v5.1.0 @@ -704,7 +704,7 @@ jobs: ffmpeg \ libgammu-dev - name: Check out code from GitHub - uses: actions/checkout@v4.1.6 + uses: actions/checkout@v4.1.7 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python uses: actions/setup-python@v5.1.0 @@ -766,7 +766,7 @@ jobs: ffmpeg \ libgammu-dev - name: Check out code from GitHub - uses: actions/checkout@v4.1.6 + uses: actions/checkout@v4.1.7 - name: Set up Python ${{ matrix.python-version }} id: python uses: actions/setup-python@v5.1.0 @@ -883,7 +883,7 @@ jobs: ffmpeg \ libmariadb-dev-compat - name: Check out code from GitHub - uses: actions/checkout@v4.1.6 + uses: actions/checkout@v4.1.7 - name: Set up Python ${{ matrix.python-version }} id: python uses: actions/setup-python@v5.1.0 @@ -1007,7 +1007,7 @@ jobs: ffmpeg \ postgresql-server-dev-14 - name: Check out code from GitHub - uses: actions/checkout@v4.1.6 + uses: actions/checkout@v4.1.7 - name: Set up Python ${{ matrix.python-version }} id: python uses: actions/setup-python@v5.1.0 @@ -1102,7 +1102,7 @@ jobs: timeout-minutes: 10 steps: - name: Check out code from GitHub - uses: actions/checkout@v4.1.6 + uses: actions/checkout@v4.1.7 - name: Download all coverage artifacts uses: actions/download-artifact@v4.1.7 with: @@ -1150,7 +1150,7 @@ jobs: ffmpeg \ libgammu-dev - name: Check out code from GitHub - uses: actions/checkout@v4.1.6 + uses: actions/checkout@v4.1.7 - name: Set up Python ${{ matrix.python-version }} id: python uses: actions/setup-python@v5.1.0 @@ -1237,7 +1237,7 @@ jobs: timeout-minutes: 10 steps: - name: Check out code from GitHub - uses: actions/checkout@v4.1.6 + uses: actions/checkout@v4.1.7 - name: Download all coverage artifacts uses: actions/download-artifact@v4.1.7 with: diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 7c36b8b7981..09f30a2a96d 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -21,7 +21,7 @@ jobs: steps: - name: Check out code from GitHub - uses: actions/checkout@v4.1.6 + uses: actions/checkout@v4.1.7 - name: Initialize CodeQL uses: github/codeql-action/init@v3.25.9 diff --git a/.github/workflows/translations.yml b/.github/workflows/translations.yml index 92c4c845e7d..69e1792f926 100644 --- a/.github/workflows/translations.yml +++ b/.github/workflows/translations.yml @@ -19,7 +19,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout the repository - uses: actions/checkout@v4.1.6 + uses: actions/checkout@v4.1.7 - name: Set up Python ${{ env.DEFAULT_PYTHON }} uses: actions/setup-python@v5.1.0 diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 13f5177bd7e..e1c2700cba9 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -32,7 +32,7 @@ jobs: architectures: ${{ steps.info.outputs.architectures }} steps: - name: Checkout the repository - uses: actions/checkout@v4.1.6 + uses: actions/checkout@v4.1.7 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python @@ -118,7 +118,7 @@ jobs: arch: ${{ fromJson(needs.init.outputs.architectures) }} steps: - name: Checkout the repository - uses: actions/checkout@v4.1.6 + uses: actions/checkout@v4.1.7 - name: Download env_file uses: actions/download-artifact@v4.1.7 @@ -156,7 +156,7 @@ jobs: arch: ${{ fromJson(needs.init.outputs.architectures) }} steps: - name: Checkout the repository - uses: actions/checkout@v4.1.6 + uses: actions/checkout@v4.1.7 - name: Download env_file uses: actions/download-artifact@v4.1.7 From a06f09831206b4f7ef861ac3214296b10b3fb0a1 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 13 Jun 2024 09:20:53 +0200 Subject: [PATCH 0617/1445] Fix dangerous-default-value warnings in switchbot tests (#119575) --- tests/components/switchbot/__init__.py | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/tests/components/switchbot/__init__.py b/tests/components/switchbot/__init__.py index c824a16d952..b2a8445546e 100644 --- a/tests/components/switchbot/__init__.py +++ b/tests/components/switchbot/__init__.py @@ -36,19 +36,13 @@ def patch_async_setup_entry(return_value=True): ) -async def init_integration( - hass: HomeAssistant, - *, - data: dict = ENTRY_CONFIG, - skip_entry_setup: bool = False, -) -> MockConfigEntry: +async def init_integration(hass: HomeAssistant) -> MockConfigEntry: """Set up the Switchbot integration in Home Assistant.""" - entry = MockConfigEntry(domain=DOMAIN, data=data) + entry = MockConfigEntry(domain=DOMAIN, data=ENTRY_CONFIG) entry.add_to_hass(hass) - if not skip_entry_setup: - await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() return entry From 92d150ff57ccd4df411b5cf0effcf879912b04d3 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 13 Jun 2024 09:21:59 +0200 Subject: [PATCH 0618/1445] Fix dangerous-default-value warnings in integration tests (#119574) --- tests/components/integration/test_sensor.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/tests/components/integration/test_sensor.py b/tests/components/integration/test_sensor.py index 5bc87717440..500d567dca4 100644 --- a/tests/components/integration/test_sensor.py +++ b/tests/components/integration/test_sensor.py @@ -1,6 +1,7 @@ """The tests for the integration sensor platform.""" from datetime import timedelta +from typing import Any from freezegun import freeze_time import pytest @@ -33,6 +34,8 @@ from tests.common import ( mock_restore_cache_with_extra_data, ) +DEFAULT_MAX_SUB_INTERVAL = {"minutes": 1} + @pytest.mark.parametrize("method", ["trapezoidal", "left", "right"]) async def test_state(hass: HomeAssistant, method) -> None: @@ -752,7 +755,7 @@ async def test_device_id( assert integration_entity.device_id == source_entity.device_id -def _integral_sensor_config(max_sub_interval: dict[str, int] | None = {"minutes": 1}): +def _integral_sensor_config(max_sub_interval: dict[str, int] | None) -> dict[str, Any]: sensor = { "platform": "integration", "name": "integration", @@ -765,7 +768,7 @@ def _integral_sensor_config(max_sub_interval: dict[str, int] | None = {"minutes" async def _setup_integral_sensor( - hass: HomeAssistant, max_sub_interval: dict[str, int] | None = {"minutes": 1} + hass: HomeAssistant, max_sub_interval: dict[str, int] | None ) -> None: await async_setup_component( hass, "sensor", _integral_sensor_config(max_sub_interval=max_sub_interval) @@ -775,7 +778,9 @@ async def _setup_integral_sensor( async def _update_source_sensor(hass: HomeAssistant, value: int | str) -> None: hass.states.async_set( - _integral_sensor_config()["sensor"]["source"], + _integral_sensor_config(max_sub_interval=DEFAULT_MAX_SUB_INTERVAL)["sensor"][ + "source" + ], value, {ATTR_UNIT_OF_MEASUREMENT: UnitOfPower.KILO_WATT}, force_update=True, @@ -790,7 +795,7 @@ async def test_on_valid_source_expect_update_on_time( start_time = dt_util.utcnow() with freeze_time(start_time) as freezer: - await _setup_integral_sensor(hass) + await _setup_integral_sensor(hass, max_sub_interval=DEFAULT_MAX_SUB_INTERVAL) await _update_source_sensor(hass, 100) state_before_max_sub_interval_exceeded = hass.states.get("sensor.integration") @@ -816,7 +821,7 @@ async def test_on_unvailable_source_expect_no_update_on_time( start_time = dt_util.utcnow() with freeze_time(start_time) as freezer: - await _setup_integral_sensor(hass) + await _setup_integral_sensor(hass, max_sub_interval=DEFAULT_MAX_SUB_INTERVAL) await _update_source_sensor(hass, 100) freezer.tick(61) async_fire_time_changed(hass, dt_util.now()) @@ -843,7 +848,7 @@ async def test_on_statechanges_source_expect_no_update_on_time( start_time = dt_util.utcnow() with freeze_time(start_time) as freezer: - await _setup_integral_sensor(hass) + await _setup_integral_sensor(hass, max_sub_interval=DEFAULT_MAX_SUB_INTERVAL) await _update_source_sensor(hass, 100) freezer.tick(30) From b5d16bb3ca35bc2a2a357056d5e69cebb0c7b0b3 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 13 Jun 2024 09:27:51 +0200 Subject: [PATCH 0619/1445] Fix dangerous-default-value warnings in version tests (#119577) --- tests/components/version/common.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/components/version/common.py b/tests/components/version/common.py index cd9469d08a1..5cecdf3d26f 100644 --- a/tests/components/version/common.py +++ b/tests/components/version/common.py @@ -42,13 +42,12 @@ async def mock_get_version_update( hass: HomeAssistant, freezer: FrozenDateTimeFactory, version: str = MOCK_VERSION, - data: dict[str, Any] = MOCK_VERSION_DATA, side_effect: Exception | None = None, ) -> None: """Mock getting version.""" with patch( "pyhaversion.HaVersion.get_version", - return_value=(version, data), + return_value=(version, MOCK_VERSION_DATA), side_effect=side_effect, ): freezer.tick(UPDATE_COORDINATOR_UPDATE_INTERVAL) From cadb6317bf4fee684462b2d3893e8ca6e579c468 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 13 Jun 2024 09:28:11 +0200 Subject: [PATCH 0620/1445] Fix dangerous-default-value warnings in canary tests (#119578) --- tests/components/canary/__init__.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/components/canary/__init__.py b/tests/components/canary/__init__.py index 8aed2fa1337..13c4b84ab94 100644 --- a/tests/components/canary/__init__.py +++ b/tests/components/canary/__init__.py @@ -54,12 +54,10 @@ def _patch_async_setup_entry(return_value=True): async def init_integration( hass: HomeAssistant, *, - data: dict = ENTRY_CONFIG, - options: dict = ENTRY_OPTIONS, skip_entry_setup: bool = False, ) -> MockConfigEntry: """Set up the Canary integration in Home Assistant.""" - entry = MockConfigEntry(domain=DOMAIN, data=data, options=options) + entry = MockConfigEntry(domain=DOMAIN, data=ENTRY_CONFIG, options=ENTRY_OPTIONS) entry.add_to_hass(hass) if not skip_entry_setup: From b2be3e0a9b71d27760834ae6e687d24096a8dd7d Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 13 Jun 2024 09:29:04 +0200 Subject: [PATCH 0621/1445] Fix dangerous-default-value warnings in automation tests (#119576) --- tests/components/automation/test_blueprint.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/tests/components/automation/test_blueprint.py b/tests/components/automation/test_blueprint.py index 7e29c134462..ee3fa631d00 100644 --- a/tests/components/automation/test_blueprint.py +++ b/tests/components/automation/test_blueprint.py @@ -4,6 +4,7 @@ import asyncio import contextlib from datetime import timedelta import pathlib +from typing import Any from unittest.mock import patch import pytest @@ -56,12 +57,12 @@ async def test_notify_leaving_zone( connections={(dr.CONNECTION_NETWORK_MAC, "00:00:00:00:00:01")}, ) - def set_person_state(state, extra={}): + def set_person_state(state: str, extra: dict[str, Any]) -> None: hass.states.async_set( "person.test_person", state, {"friendly_name": "Paulus", **extra} ) - set_person_state("School") + set_person_state("School", {}) assert await async_setup_component( hass, "zone", {"zone": {"name": "School", "latitude": 1, "longitude": 2}} @@ -92,7 +93,7 @@ async def test_notify_leaving_zone( "homeassistant.components.mobile_app.device_action.async_call_action_from_config" ) as mock_call_action: # Leaving zone to no zone - set_person_state("not_home") + set_person_state("not_home", {}) await hass.async_block_till_done() assert len(mock_call_action.mock_calls) == 1 @@ -108,13 +109,13 @@ async def test_notify_leaving_zone( assert message_tpl.async_render(variables) == "Paulus has left School" # Should not increase when we go to another zone - set_person_state("bla") + set_person_state("bla", {}) await hass.async_block_till_done() assert len(mock_call_action.mock_calls) == 1 # Should not increase when we go into the zone - set_person_state("School") + set_person_state("School", {}) await hass.async_block_till_done() assert len(mock_call_action.mock_calls) == 1 @@ -126,7 +127,7 @@ async def test_notify_leaving_zone( assert len(mock_call_action.mock_calls) == 1 # Should increase when leaving zone for another zone - set_person_state("Just Outside School") + set_person_state("Just Outside School", {}) await hass.async_block_till_done() assert len(mock_call_action.mock_calls) == 2 From c02ac5e538228221d2209cfab16e0085b34d0e0c Mon Sep 17 00:00:00 2001 From: William Grant Date: Thu, 13 Jun 2024 17:29:57 +1000 Subject: [PATCH 0622/1445] Classify more ecowitt power supply sensors as diagnostics (#119555) --- homeassistant/components/ecowitt/binary_sensor.py | 5 ++++- homeassistant/components/ecowitt/sensor.py | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/ecowitt/binary_sensor.py b/homeassistant/components/ecowitt/binary_sensor.py index f73467288a2..1ef2956d84b 100644 --- a/homeassistant/components/ecowitt/binary_sensor.py +++ b/homeassistant/components/ecowitt/binary_sensor.py @@ -11,6 +11,7 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry +from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -22,7 +23,9 @@ ECOWITT_BINARYSENSORS_MAPPING: Final = { key="LEAK", device_class=BinarySensorDeviceClass.MOISTURE ), EcoWittSensorTypes.BATTERY_BINARY: BinarySensorEntityDescription( - key="BATTERY", device_class=BinarySensorDeviceClass.BATTERY + key="BATTERY", + device_class=BinarySensorDeviceClass.BATTERY, + entity_category=EntityCategory.DIAGNOSTIC, ), } diff --git a/homeassistant/components/ecowitt/sensor.py b/homeassistant/components/ecowitt/sensor.py index dccb3747c60..6845fb64d4c 100644 --- a/homeassistant/components/ecowitt/sensor.py +++ b/homeassistant/components/ecowitt/sensor.py @@ -125,6 +125,7 @@ ECOWITT_SENSORS_MAPPING: Final = { device_class=SensorDeviceClass.VOLTAGE, native_unit_of_measurement=UnitOfElectricPotential.VOLT, state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, ), EcoWittSensorTypes.LIGHTNING_COUNT: SensorEntityDescription( key="LIGHTNING_COUNT", From 440771bdea3eddd5bf2687aa918348d4919f96db Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Thu, 13 Jun 2024 09:30:53 +0200 Subject: [PATCH 0623/1445] Fix error for Reolink snapshot streams (#119572) --- homeassistant/components/reolink/camera.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/reolink/camera.py b/homeassistant/components/reolink/camera.py index a2c396e7ef5..4adac1a96d8 100644 --- a/homeassistant/components/reolink/camera.py +++ b/homeassistant/components/reolink/camera.py @@ -116,7 +116,6 @@ async def async_setup_entry( class ReolinkCamera(ReolinkChannelCoordinatorEntity, Camera): """An implementation of a Reolink IP camera.""" - _attr_supported_features: CameraEntityFeature = CameraEntityFeature.STREAM entity_description: ReolinkCameraEntityDescription def __init__( @@ -130,6 +129,9 @@ class ReolinkCamera(ReolinkChannelCoordinatorEntity, Camera): ReolinkChannelCoordinatorEntity.__init__(self, reolink_data, channel) Camera.__init__(self) + if "snapshots" not in entity_description.stream: + self._attr_supported_features = CameraEntityFeature.STREAM + if self._host.api.model in DUAL_LENS_MODELS: self._attr_translation_key = ( f"{entity_description.translation_key}_lens_{self._channel}" From 55f8a36572367652bc097c52a0260228226b8cae Mon Sep 17 00:00:00 2001 From: Matrix Date: Thu, 13 Jun 2024 15:31:29 +0800 Subject: [PATCH 0624/1445] Improve code readability (#119558) --- homeassistant/components/yolink/switch.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/yolink/switch.py b/homeassistant/components/yolink/switch.py index 2e31100bf3c..c999f04d90d 100644 --- a/homeassistant/components/yolink/switch.py +++ b/homeassistant/components/yolink/switch.py @@ -70,18 +70,22 @@ DEVICE_TYPES: tuple[YoLinkSwitchEntityDescription, ...] = ( translation_key="plug_1", device_class=SwitchDeviceClass.OUTLET, exists_fn=lambda device: device.device_type == ATTR_DEVICE_MULTI_OUTLET, - plug_index_fn=lambda device: 1 - if device.device_model_name.startswith(DEV_MODEL_MULTI_OUTLET_YS6801) - else 0, + plug_index_fn=lambda device: ( + 1 + if device.device_model_name.startswith(DEV_MODEL_MULTI_OUTLET_YS6801) + else 0 + ), ), YoLinkSwitchEntityDescription( key="multi_outlet_plug_2", translation_key="plug_2", device_class=SwitchDeviceClass.OUTLET, exists_fn=lambda device: device.device_type == ATTR_DEVICE_MULTI_OUTLET, - plug_index_fn=lambda device: 2 - if device.device_model_name.startswith(DEV_MODEL_MULTI_OUTLET_YS6801) - else 1, + plug_index_fn=lambda device: ( + 2 + if device.device_model_name.startswith(DEV_MODEL_MULTI_OUTLET_YS6801) + else 1 + ), ), YoLinkSwitchEntityDescription( key="multi_outlet_plug_3", From d211af75ef2f584c95c29628fa85eadd5ebc990c Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 13 Jun 2024 10:30:44 +0200 Subject: [PATCH 0625/1445] Fix dangerous-default-value warnings in cloud tests (#119585) --- tests/components/cloud/__init__.py | 2 +- tests/components/cloud/conftest.py | 2 +- tests/components/cloud/test_client.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/components/cloud/__init__.py b/tests/components/cloud/__init__.py index 2b4a95a61d9..d527cbbeec2 100644 --- a/tests/components/cloud/__init__.py +++ b/tests/components/cloud/__init__.py @@ -69,7 +69,7 @@ async def mock_cloud(hass, config=None): await cloud_inst.initialize() -def mock_cloud_prefs(hass, prefs={}): +def mock_cloud_prefs(hass, prefs): """Fixture for cloud component.""" prefs_to_set = { const.PREF_ALEXA_SETTINGS_VERSION: cloud_prefs.ALEXA_SETTINGS_VERSION, diff --git a/tests/components/cloud/conftest.py b/tests/components/cloud/conftest.py index 617492c0416..ebd9ea6663e 100644 --- a/tests/components/cloud/conftest.py +++ b/tests/components/cloud/conftest.py @@ -203,7 +203,7 @@ def mock_user_data(): def mock_cloud_fixture(hass): """Fixture for cloud component.""" hass.loop.run_until_complete(mock_cloud(hass)) - return mock_cloud_prefs(hass) + return mock_cloud_prefs(hass, {}) @pytest.fixture diff --git a/tests/components/cloud/test_client.py b/tests/components/cloud/test_client.py index ecc98cf5579..7c04373c261 100644 --- a/tests/components/cloud/test_client.py +++ b/tests/components/cloud/test_client.py @@ -117,7 +117,7 @@ async def test_handler_google_actions(hass: HomeAssistant) -> None: }, ) - mock_cloud_prefs(hass) + mock_cloud_prefs(hass, {}) cloud = hass.data["cloud"] reqid = "5711642932632160983" From f5b86154b486496b6d442c44fceba9e79fa2bc59 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Thu, 13 Jun 2024 11:49:20 +0200 Subject: [PATCH 0626/1445] Bump deebot-client to 8.0.0 (#119515) Co-authored-by: Franck Nijhof --- .../components/ecovacs/binary_sensor.py | 12 ++---- homeassistant/components/ecovacs/button.py | 15 ++----- .../components/ecovacs/controller.py | 14 +++---- .../components/ecovacs/diagnostics.py | 4 +- homeassistant/components/ecovacs/entity.py | 14 +++---- homeassistant/components/ecovacs/event.py | 8 ++-- homeassistant/components/ecovacs/image.py | 14 +++---- .../components/ecovacs/lawn_mower.py | 13 +++--- .../components/ecovacs/manifest.json | 2 +- homeassistant/components/ecovacs/number.py | 7 +--- homeassistant/components/ecovacs/select.py | 13 ++---- homeassistant/components/ecovacs/sensor.py | 22 +++------- homeassistant/components/ecovacs/switch.py | 41 ++++++------------- homeassistant/components/ecovacs/util.py | 5 +-- homeassistant/components/ecovacs/vacuum.py | 39 ++++++++++-------- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../components/ecovacs/test_binary_sensor.py | 3 +- tests/components/ecovacs/test_button.py | 3 +- tests/components/ecovacs/test_event.py | 3 +- tests/components/ecovacs/test_init.py | 3 +- tests/components/ecovacs/test_lawn_mower.py | 5 +-- tests/components/ecovacs/test_number.py | 5 +-- tests/components/ecovacs/test_select.py | 5 +-- tests/components/ecovacs/test_sensor.py | 3 +- tests/components/ecovacs/test_switch.py | 3 +- 26 files changed, 100 insertions(+), 160 deletions(-) diff --git a/homeassistant/components/ecovacs/binary_sensor.py b/homeassistant/components/ecovacs/binary_sensor.py index f6e3e34aaa4..d755d01a4ae 100644 --- a/homeassistant/components/ecovacs/binary_sensor.py +++ b/homeassistant/components/ecovacs/binary_sensor.py @@ -4,7 +4,7 @@ from collections.abc import Callable from dataclasses import dataclass from typing import Generic -from deebot_client.capabilities import CapabilityEvent, VacuumCapabilities +from deebot_client.capabilities import CapabilityEvent from deebot_client.events.water_info import WaterInfoEvent from homeassistant.components.binary_sensor import ( @@ -16,12 +16,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import EcovacsConfigEntry -from .entity import ( - CapabilityDevice, - EcovacsCapabilityEntityDescription, - EcovacsDescriptionEntity, - EventT, -) +from .entity import EcovacsCapabilityEntityDescription, EcovacsDescriptionEntity, EventT from .util import get_supported_entitites @@ -38,7 +33,6 @@ class EcovacsBinarySensorEntityDescription( ENTITY_DESCRIPTIONS: tuple[EcovacsBinarySensorEntityDescription, ...] = ( EcovacsBinarySensorEntityDescription[WaterInfoEvent]( - device_capabilities=VacuumCapabilities, capability_fn=lambda caps: caps.water, value_fn=lambda e: e.mop_attached, key="water_mop_attached", @@ -62,7 +56,7 @@ async def async_setup_entry( class EcovacsBinarySensor( - EcovacsDescriptionEntity[CapabilityDevice, CapabilityEvent[EventT]], + EcovacsDescriptionEntity[CapabilityEvent[EventT]], BinarySensorEntity, ): """Ecovacs binary sensor.""" diff --git a/homeassistant/components/ecovacs/button.py b/homeassistant/components/ecovacs/button.py index 14fd54df5a0..5d76b38bed8 100644 --- a/homeassistant/components/ecovacs/button.py +++ b/homeassistant/components/ecovacs/button.py @@ -2,12 +2,7 @@ from dataclasses import dataclass -from deebot_client.capabilities import ( - Capabilities, - CapabilityExecute, - CapabilityLifeSpan, - VacuumCapabilities, -) +from deebot_client.capabilities import CapabilityExecute, CapabilityLifeSpan from deebot_client.events import LifeSpan from homeassistant.components.button import ButtonEntity, ButtonEntityDescription @@ -18,7 +13,6 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import EcovacsConfigEntry from .const import SUPPORTED_LIFESPANS from .entity import ( - CapabilityDevice, EcovacsCapabilityEntityDescription, EcovacsDescriptionEntity, EcovacsEntity, @@ -43,7 +37,6 @@ class EcovacsLifespanButtonEntityDescription(ButtonEntityDescription): ENTITY_DESCRIPTIONS: tuple[EcovacsButtonEntityDescription, ...] = ( EcovacsButtonEntityDescription( - device_capabilities=VacuumCapabilities, capability_fn=lambda caps: caps.map.relocation if caps.map else None, key="relocate", translation_key="relocate", @@ -77,7 +70,7 @@ async def async_setup_entry( EcovacsResetLifespanButtonEntity( device, device.capabilities.life_span, description ) - for device in controller.devices(Capabilities) + for device in controller.devices for description in LIFESPAN_ENTITY_DESCRIPTIONS if description.component in device.capabilities.life_span.types ) @@ -85,7 +78,7 @@ async def async_setup_entry( class EcovacsButtonEntity( - EcovacsDescriptionEntity[CapabilityDevice, CapabilityExecute], + EcovacsDescriptionEntity[CapabilityExecute], ButtonEntity, ): """Ecovacs button entity.""" @@ -98,7 +91,7 @@ class EcovacsButtonEntity( class EcovacsResetLifespanButtonEntity( - EcovacsDescriptionEntity[Capabilities, CapabilityLifeSpan], + EcovacsDescriptionEntity[CapabilityLifeSpan], ButtonEntity, ): """Ecovacs reset lifespan button entity.""" diff --git a/homeassistant/components/ecovacs/controller.py b/homeassistant/components/ecovacs/controller.py index 3e2d2ebdd9a..0bef2e8fdd7 100644 --- a/homeassistant/components/ecovacs/controller.py +++ b/homeassistant/components/ecovacs/controller.py @@ -9,7 +9,6 @@ from typing import Any from deebot_client.api_client import ApiClient from deebot_client.authentication import Authenticator, create_rest_config -from deebot_client.capabilities import Capabilities from deebot_client.const import UNDEFINED, UndefinedType from deebot_client.device import Device from deebot_client.exceptions import DeebotError, InvalidAuthenticationError @@ -18,10 +17,9 @@ from deebot_client.mqtt_client import MqttClient, create_mqtt_config from deebot_client.util import md5 from deebot_client.util.continents import get_continent from sucks import EcoVacsAPI, VacBot -from typing_extensions import Generator from homeassistant.const import CONF_COUNTRY, CONF_PASSWORD, CONF_USERNAME -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryError, ConfigEntryNotReady from homeassistant.helpers import aiohttp_client from homeassistant.util.ssl import get_default_no_verify_context @@ -119,12 +117,10 @@ class EcovacsController: await self._mqtt.disconnect() await self._authenticator.teardown() - @callback - def devices(self, capability: type[Capabilities]) -> Generator[Device]: - """Return generator for devices with a specific capability.""" - for device in self._devices: - if isinstance(device.capabilities, capability): - yield device + @property + def devices(self) -> list[Device]: + """Return devices.""" + return self._devices @property def legacy_devices(self) -> list[VacBot]: diff --git a/homeassistant/components/ecovacs/diagnostics.py b/homeassistant/components/ecovacs/diagnostics.py index 50b59b90860..22a55d9c6ab 100644 --- a/homeassistant/components/ecovacs/diagnostics.py +++ b/homeassistant/components/ecovacs/diagnostics.py @@ -4,8 +4,6 @@ from __future__ import annotations from typing import Any -from deebot_client.capabilities import Capabilities - from homeassistant.components.diagnostics import async_redact_data from homeassistant.const import CONF_NAME, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant @@ -34,7 +32,7 @@ async def async_get_config_entry_diagnostics( diag["devices"] = [ async_redact_data(device.device_info, REDACT_DEVICE) - for device in controller.devices(Capabilities) + for device in controller.devices ] diag["legacy_devices"] = [ async_redact_data(device.vacuum, REDACT_DEVICE) diff --git a/homeassistant/components/ecovacs/entity.py b/homeassistant/components/ecovacs/entity.py index 4497f82d964..c038c54497a 100644 --- a/homeassistant/components/ecovacs/entity.py +++ b/homeassistant/components/ecovacs/entity.py @@ -18,11 +18,10 @@ from homeassistant.helpers.entity import Entity, EntityDescription from .const import DOMAIN CapabilityEntity = TypeVar("CapabilityEntity") -CapabilityDevice = TypeVar("CapabilityDevice", bound=Capabilities) EventT = TypeVar("EventT", bound=Event) -class EcovacsEntity(Entity, Generic[CapabilityDevice, CapabilityEntity]): +class EcovacsEntity(Entity, Generic[CapabilityEntity]): """Ecovacs entity.""" _attr_should_poll = False @@ -31,7 +30,7 @@ class EcovacsEntity(Entity, Generic[CapabilityDevice, CapabilityEntity]): def __init__( self, - device: Device[CapabilityDevice], + device: Device, capability: CapabilityEntity, **kwargs: Any, ) -> None: @@ -97,12 +96,12 @@ class EcovacsEntity(Entity, Generic[CapabilityDevice, CapabilityEntity]): self._device.events.request_refresh(event_type) -class EcovacsDescriptionEntity(EcovacsEntity[CapabilityDevice, CapabilityEntity]): +class EcovacsDescriptionEntity(EcovacsEntity[CapabilityEntity]): """Ecovacs entity.""" def __init__( self, - device: Device[CapabilityDevice], + device: Device, capability: CapabilityEntity, entity_description: EntityDescription, **kwargs: Any, @@ -115,9 +114,8 @@ class EcovacsDescriptionEntity(EcovacsEntity[CapabilityDevice, CapabilityEntity] @dataclass(kw_only=True, frozen=True) class EcovacsCapabilityEntityDescription( EntityDescription, - Generic[CapabilityDevice, CapabilityEntity], + Generic[CapabilityEntity], ): """Ecovacs entity description.""" - device_capabilities: type[CapabilityDevice] - capability_fn: Callable[[CapabilityDevice], CapabilityEntity | None] + capability_fn: Callable[[Capabilities], CapabilityEntity | None] diff --git a/homeassistant/components/ecovacs/event.py b/homeassistant/components/ecovacs/event.py index 9e4dde00b54..3249b466c77 100644 --- a/homeassistant/components/ecovacs/event.py +++ b/homeassistant/components/ecovacs/event.py @@ -1,6 +1,6 @@ """Event module.""" -from deebot_client.capabilities import Capabilities, CapabilityEvent +from deebot_client.capabilities import CapabilityEvent from deebot_client.device import Device from deebot_client.events import CleanJobStatus, ReportStatsEvent @@ -22,12 +22,12 @@ async def async_setup_entry( """Add entities for passed config_entry in HA.""" controller = config_entry.runtime_data async_add_entities( - EcovacsLastJobEventEntity(device) for device in controller.devices(Capabilities) + EcovacsLastJobEventEntity(device) for device in controller.devices ) class EcovacsLastJobEventEntity( - EcovacsEntity[Capabilities, CapabilityEvent[ReportStatsEvent]], + EcovacsEntity[CapabilityEvent[ReportStatsEvent]], EventEntity, ): """Ecovacs last job event entity.""" @@ -39,7 +39,7 @@ class EcovacsLastJobEventEntity( event_types=["finished", "finished_with_warnings", "manually_stopped"], ) - def __init__(self, device: Device[Capabilities]) -> None: + def __init__(self, device: Device) -> None: """Initialize entity.""" super().__init__(device, device.capabilities.stats.report) diff --git a/homeassistant/components/ecovacs/image.py b/homeassistant/components/ecovacs/image.py index 1e94dc856ee..d8b69084cec 100644 --- a/homeassistant/components/ecovacs/image.py +++ b/homeassistant/components/ecovacs/image.py @@ -1,6 +1,6 @@ """Ecovacs image entities.""" -from deebot_client.capabilities import CapabilityMap, VacuumCapabilities +from deebot_client.capabilities import CapabilityMap from deebot_client.device import Device from deebot_client.events.map import CachedMapInfoEvent, MapChangedEvent @@ -20,18 +20,18 @@ async def async_setup_entry( ) -> None: """Add entities for passed config_entry in HA.""" controller = config_entry.runtime_data - entities = [] - for device in controller.devices(VacuumCapabilities): - capabilities: VacuumCapabilities = device.capabilities - if caps := capabilities.map: - entities.append(EcovacsMap(device, caps, hass)) + entities = [ + EcovacsMap(device, caps, hass) + for device in controller.devices + if (caps := device.capabilities.map) + ] if entities: async_add_entities(entities) class EcovacsMap( - EcovacsEntity[VacuumCapabilities, CapabilityMap], + EcovacsEntity[CapabilityMap], ImageEntity, ): """Ecovacs map.""" diff --git a/homeassistant/components/ecovacs/lawn_mower.py b/homeassistant/components/ecovacs/lawn_mower.py index 2561fe22217..a1dc8acf3a2 100644 --- a/homeassistant/components/ecovacs/lawn_mower.py +++ b/homeassistant/components/ecovacs/lawn_mower.py @@ -4,7 +4,7 @@ from __future__ import annotations import logging -from deebot_client.capabilities import MowerCapabilities +from deebot_client.capabilities import Capabilities, DeviceType from deebot_client.device import Device from deebot_client.events import StateEvent from deebot_client.models import CleanAction, State @@ -42,14 +42,16 @@ async def async_setup_entry( """Set up the Ecovacs mowers.""" controller = config_entry.runtime_data mowers: list[EcovacsMower] = [ - EcovacsMower(device) for device in controller.devices(MowerCapabilities) + EcovacsMower(device) + for device in controller.devices + if device.capabilities.device_type is DeviceType.MOWER ] _LOGGER.debug("Adding Ecovacs Mowers to Home Assistant: %s", mowers) async_add_entities(mowers) class EcovacsMower( - EcovacsEntity[MowerCapabilities, MowerCapabilities], + EcovacsEntity[Capabilities], LawnMowerEntity, ): """Ecovacs Mower.""" @@ -62,10 +64,9 @@ class EcovacsMower( entity_description = LawnMowerEntityEntityDescription(key="mower", name=None) - def __init__(self, device: Device[MowerCapabilities]) -> None: + def __init__(self, device: Device) -> None: """Initialize the mower.""" - capabilities = device.capabilities - super().__init__(device, capabilities) + super().__init__(device, device.capabilities) async def async_added_to_hass(self) -> None: """Set up the event listeners now that hass is ready.""" diff --git a/homeassistant/components/ecovacs/manifest.json b/homeassistant/components/ecovacs/manifest.json index 66dd07cf431..d14291576ff 100644 --- a/homeassistant/components/ecovacs/manifest.json +++ b/homeassistant/components/ecovacs/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/ecovacs", "iot_class": "cloud_push", "loggers": ["sleekxmppfs", "sucks", "deebot_client"], - "requirements": ["py-sucks==0.9.10", "deebot-client==7.3.0"] + "requirements": ["py-sucks==0.9.10", "deebot-client==8.0.0"] } diff --git a/homeassistant/components/ecovacs/number.py b/homeassistant/components/ecovacs/number.py index bd8ce50aadb..bfe840dad42 100644 --- a/homeassistant/components/ecovacs/number.py +++ b/homeassistant/components/ecovacs/number.py @@ -6,7 +6,7 @@ from collections.abc import Callable from dataclasses import dataclass from typing import Generic -from deebot_client.capabilities import Capabilities, CapabilitySet, VacuumCapabilities +from deebot_client.capabilities import CapabilitySet from deebot_client.events import CleanCountEvent, VolumeEvent from homeassistant.components.number import NumberEntity, NumberEntityDescription @@ -16,7 +16,6 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import EcovacsConfigEntry from .entity import ( - CapabilityDevice, EcovacsCapabilityEntityDescription, EcovacsDescriptionEntity, EcovacsEntity, @@ -39,7 +38,6 @@ class EcovacsNumberEntityDescription( ENTITY_DESCRIPTIONS: tuple[EcovacsNumberEntityDescription, ...] = ( EcovacsNumberEntityDescription[VolumeEvent]( - device_capabilities=Capabilities, capability_fn=lambda caps: caps.settings.volume, value_fn=lambda e: e.volume, native_max_value_fn=lambda e: e.maximum, @@ -52,7 +50,6 @@ ENTITY_DESCRIPTIONS: tuple[EcovacsNumberEntityDescription, ...] = ( native_step=1.0, ), EcovacsNumberEntityDescription[CleanCountEvent]( - device_capabilities=VacuumCapabilities, capability_fn=lambda caps: caps.clean.count, value_fn=lambda e: e.count, key="clean_count", @@ -81,7 +78,7 @@ async def async_setup_entry( class EcovacsNumberEntity( - EcovacsDescriptionEntity[CapabilityDevice, CapabilitySet[EventT, int]], + EcovacsDescriptionEntity[CapabilitySet[EventT, int]], NumberEntity, ): """Ecovacs number entity.""" diff --git a/homeassistant/components/ecovacs/select.py b/homeassistant/components/ecovacs/select.py index 4caa6327bb3..c8b01a0f83a 100644 --- a/homeassistant/components/ecovacs/select.py +++ b/homeassistant/components/ecovacs/select.py @@ -4,7 +4,7 @@ from collections.abc import Callable from dataclasses import dataclass from typing import Any, Generic -from deebot_client.capabilities import CapabilitySetTypes, VacuumCapabilities +from deebot_client.capabilities import CapabilitySetTypes from deebot_client.device import Device from deebot_client.events import WaterInfoEvent, WorkModeEvent @@ -14,12 +14,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import EcovacsConfigEntry -from .entity import ( - CapabilityDevice, - EcovacsCapabilityEntityDescription, - EcovacsDescriptionEntity, - EventT, -) +from .entity import EcovacsCapabilityEntityDescription, EcovacsDescriptionEntity, EventT from .util import get_name_key, get_supported_entitites @@ -37,7 +32,6 @@ class EcovacsSelectEntityDescription( ENTITY_DESCRIPTIONS: tuple[EcovacsSelectEntityDescription, ...] = ( EcovacsSelectEntityDescription[WaterInfoEvent]( - device_capabilities=VacuumCapabilities, capability_fn=lambda caps: caps.water, current_option_fn=lambda e: get_name_key(e.amount), options_fn=lambda water: [get_name_key(amount) for amount in water.types], @@ -46,7 +40,6 @@ ENTITY_DESCRIPTIONS: tuple[EcovacsSelectEntityDescription, ...] = ( entity_category=EntityCategory.CONFIG, ), EcovacsSelectEntityDescription[WorkModeEvent]( - device_capabilities=VacuumCapabilities, capability_fn=lambda caps: caps.clean.work_mode, current_option_fn=lambda e: get_name_key(e.mode), options_fn=lambda cap: [get_name_key(mode) for mode in cap.types], @@ -73,7 +66,7 @@ async def async_setup_entry( class EcovacsSelectEntity( - EcovacsDescriptionEntity[CapabilityDevice, CapabilitySetTypes[EventT, str]], + EcovacsDescriptionEntity[CapabilitySetTypes[EventT, str]], SelectEntity, ): """Ecovacs select entity.""" diff --git a/homeassistant/components/ecovacs/sensor.py b/homeassistant/components/ecovacs/sensor.py index e9229781827..256198693fb 100644 --- a/homeassistant/components/ecovacs/sensor.py +++ b/homeassistant/components/ecovacs/sensor.py @@ -6,7 +6,7 @@ from collections.abc import Callable from dataclasses import dataclass from typing import Generic -from deebot_client.capabilities import Capabilities, CapabilityEvent, CapabilityLifeSpan +from deebot_client.capabilities import CapabilityEvent, CapabilityLifeSpan from deebot_client.events import ( BatteryEvent, ErrorEvent, @@ -39,7 +39,6 @@ from homeassistant.helpers.typing import StateType from . import EcovacsConfigEntry from .const import SUPPORTED_LIFESPANS from .entity import ( - CapabilityDevice, EcovacsCapabilityEntityDescription, EcovacsDescriptionEntity, EcovacsEntity, @@ -63,7 +62,6 @@ ENTITY_DESCRIPTIONS: tuple[EcovacsSensorEntityDescription, ...] = ( # Stats EcovacsSensorEntityDescription[StatsEvent]( key="stats_area", - device_capabilities=Capabilities, capability_fn=lambda caps: caps.stats.clean, value_fn=lambda e: e.area, translation_key="stats_area", @@ -71,7 +69,6 @@ ENTITY_DESCRIPTIONS: tuple[EcovacsSensorEntityDescription, ...] = ( ), EcovacsSensorEntityDescription[StatsEvent]( key="stats_time", - device_capabilities=Capabilities, capability_fn=lambda caps: caps.stats.clean, value_fn=lambda e: e.time, translation_key="stats_time", @@ -81,7 +78,6 @@ ENTITY_DESCRIPTIONS: tuple[EcovacsSensorEntityDescription, ...] = ( ), # TotalStats EcovacsSensorEntityDescription[TotalStatsEvent]( - device_capabilities=Capabilities, capability_fn=lambda caps: caps.stats.total, value_fn=lambda e: e.area, key="total_stats_area", @@ -90,7 +86,6 @@ ENTITY_DESCRIPTIONS: tuple[EcovacsSensorEntityDescription, ...] = ( state_class=SensorStateClass.TOTAL_INCREASING, ), EcovacsSensorEntityDescription[TotalStatsEvent]( - device_capabilities=Capabilities, capability_fn=lambda caps: caps.stats.total, value_fn=lambda e: e.time, key="total_stats_time", @@ -101,7 +96,6 @@ ENTITY_DESCRIPTIONS: tuple[EcovacsSensorEntityDescription, ...] = ( state_class=SensorStateClass.TOTAL_INCREASING, ), EcovacsSensorEntityDescription[TotalStatsEvent]( - device_capabilities=Capabilities, capability_fn=lambda caps: caps.stats.total, value_fn=lambda e: e.cleanings, key="total_stats_cleanings", @@ -109,7 +103,6 @@ ENTITY_DESCRIPTIONS: tuple[EcovacsSensorEntityDescription, ...] = ( state_class=SensorStateClass.TOTAL_INCREASING, ), EcovacsSensorEntityDescription[BatteryEvent]( - device_capabilities=Capabilities, capability_fn=lambda caps: caps.battery, value_fn=lambda e: e.value, key=ATTR_BATTERY_LEVEL, @@ -118,7 +111,6 @@ ENTITY_DESCRIPTIONS: tuple[EcovacsSensorEntityDescription, ...] = ( entity_category=EntityCategory.DIAGNOSTIC, ), EcovacsSensorEntityDescription[NetworkInfoEvent]( - device_capabilities=Capabilities, capability_fn=lambda caps: caps.network, value_fn=lambda e: e.ip, key="network_ip", @@ -127,7 +119,6 @@ ENTITY_DESCRIPTIONS: tuple[EcovacsSensorEntityDescription, ...] = ( entity_category=EntityCategory.DIAGNOSTIC, ), EcovacsSensorEntityDescription[NetworkInfoEvent]( - device_capabilities=Capabilities, capability_fn=lambda caps: caps.network, value_fn=lambda e: e.rssi, key="network_rssi", @@ -136,7 +127,6 @@ ENTITY_DESCRIPTIONS: tuple[EcovacsSensorEntityDescription, ...] = ( entity_category=EntityCategory.DIAGNOSTIC, ), EcovacsSensorEntityDescription[NetworkInfoEvent]( - device_capabilities=Capabilities, capability_fn=lambda caps: caps.network, value_fn=lambda e: e.ssid, key="network_ssid", @@ -181,13 +171,13 @@ async def async_setup_entry( ) entities.extend( EcovacsLifespanSensor(device, device.capabilities.life_span, description) - for device in controller.devices(Capabilities) + for device in controller.devices for description in LIFESPAN_ENTITY_DESCRIPTIONS if description.component in device.capabilities.life_span.types ) entities.extend( EcovacsErrorSensor(device, capability) - for device in controller.devices(Capabilities) + for device in controller.devices if (capability := device.capabilities.error) ) @@ -195,7 +185,7 @@ async def async_setup_entry( class EcovacsSensor( - EcovacsDescriptionEntity[CapabilityDevice, CapabilityEvent], + EcovacsDescriptionEntity[CapabilityEvent], SensorEntity, ): """Ecovacs sensor.""" @@ -218,7 +208,7 @@ class EcovacsSensor( class EcovacsLifespanSensor( - EcovacsDescriptionEntity[Capabilities, CapabilityLifeSpan], + EcovacsDescriptionEntity[CapabilityLifeSpan], SensorEntity, ): """Lifespan sensor.""" @@ -238,7 +228,7 @@ class EcovacsLifespanSensor( class EcovacsErrorSensor( - EcovacsEntity[Capabilities, CapabilityEvent[ErrorEvent]], + EcovacsEntity[CapabilityEvent[ErrorEvent]], SensorEntity, ): """Error sensor.""" diff --git a/homeassistant/components/ecovacs/switch.py b/homeassistant/components/ecovacs/switch.py index 25ecb53e278..872981b5c28 100644 --- a/homeassistant/components/ecovacs/switch.py +++ b/homeassistant/components/ecovacs/switch.py @@ -3,11 +3,7 @@ from dataclasses import dataclass from typing import Any -from deebot_client.capabilities import ( - Capabilities, - CapabilitySetEnable, - VacuumCapabilities, -) +from deebot_client.capabilities import CapabilitySetEnable from deebot_client.events import EnableEvent from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription @@ -17,7 +13,6 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import EcovacsConfigEntry from .entity import ( - CapabilityDevice, EcovacsCapabilityEntityDescription, EcovacsDescriptionEntity, EcovacsEntity, @@ -28,86 +23,76 @@ from .util import get_supported_entitites @dataclass(kw_only=True, frozen=True) class EcovacsSwitchEntityDescription( SwitchEntityDescription, - EcovacsCapabilityEntityDescription[CapabilityDevice, CapabilitySetEnable], + EcovacsCapabilityEntityDescription[CapabilitySetEnable], ): """Ecovacs switch entity description.""" ENTITY_DESCRIPTIONS: tuple[EcovacsSwitchEntityDescription, ...] = ( - EcovacsSwitchEntityDescription[Capabilities]( - device_capabilities=Capabilities, + EcovacsSwitchEntityDescription( capability_fn=lambda c: c.settings.advanced_mode, key="advanced_mode", translation_key="advanced_mode", entity_registry_enabled_default=False, entity_category=EntityCategory.CONFIG, ), - EcovacsSwitchEntityDescription[VacuumCapabilities]( - device_capabilities=VacuumCapabilities, + EcovacsSwitchEntityDescription( capability_fn=lambda c: c.clean.continuous, key="continuous_cleaning", translation_key="continuous_cleaning", entity_registry_enabled_default=False, entity_category=EntityCategory.CONFIG, ), - EcovacsSwitchEntityDescription[VacuumCapabilities]( - device_capabilities=VacuumCapabilities, + EcovacsSwitchEntityDescription( capability_fn=lambda c: c.settings.carpet_auto_fan_boost, key="carpet_auto_fan_boost", translation_key="carpet_auto_fan_boost", entity_registry_enabled_default=False, entity_category=EntityCategory.CONFIG, ), - EcovacsSwitchEntityDescription[VacuumCapabilities]( - device_capabilities=VacuumCapabilities, + EcovacsSwitchEntityDescription( capability_fn=lambda c: c.clean.preference, key="clean_preference", translation_key="clean_preference", entity_registry_enabled_default=False, entity_category=EntityCategory.CONFIG, ), - EcovacsSwitchEntityDescription[Capabilities]( - device_capabilities=Capabilities, + EcovacsSwitchEntityDescription( capability_fn=lambda c: c.settings.true_detect, key="true_detect", translation_key="true_detect", entity_registry_enabled_default=False, entity_category=EntityCategory.CONFIG, ), - EcovacsSwitchEntityDescription[Capabilities]( - device_capabilities=Capabilities, + EcovacsSwitchEntityDescription( capability_fn=lambda c: c.settings.border_switch, key="border_switch", translation_key="border_switch", entity_registry_enabled_default=False, entity_category=EntityCategory.CONFIG, ), - EcovacsSwitchEntityDescription[Capabilities]( - device_capabilities=Capabilities, + EcovacsSwitchEntityDescription( capability_fn=lambda c: c.settings.child_lock, key="child_lock", translation_key="child_lock", entity_registry_enabled_default=False, entity_category=EntityCategory.CONFIG, ), - EcovacsSwitchEntityDescription[Capabilities]( - device_capabilities=Capabilities, + EcovacsSwitchEntityDescription( capability_fn=lambda c: c.settings.moveup_warning, key="move_up_warning", translation_key="move_up_warning", entity_registry_enabled_default=False, entity_category=EntityCategory.CONFIG, ), - EcovacsSwitchEntityDescription[Capabilities]( - device_capabilities=Capabilities, + EcovacsSwitchEntityDescription( capability_fn=lambda c: c.settings.cross_map_border_warning, key="cross_map_border_warning", translation_key="cross_map_border_warning", entity_registry_enabled_default=False, entity_category=EntityCategory.CONFIG, ), - EcovacsSwitchEntityDescription[Capabilities]( - device_capabilities=Capabilities, + EcovacsSwitchEntityDescription( capability_fn=lambda c: c.settings.safe_protect, key="safe_protect", translation_key="safe_protect", @@ -132,7 +117,7 @@ async def async_setup_entry( class EcovacsSwitchEntity( - EcovacsDescriptionEntity[CapabilityDevice, CapabilitySetEnable], + EcovacsDescriptionEntity[CapabilitySetEnable], SwitchEntity, ): """Ecovacs switch entity.""" diff --git a/homeassistant/components/ecovacs/util.py b/homeassistant/components/ecovacs/util.py index 9d692bbbb8f..a4894de8968 100644 --- a/homeassistant/components/ecovacs/util.py +++ b/homeassistant/components/ecovacs/util.py @@ -7,8 +7,6 @@ import random import string from typing import TYPE_CHECKING -from deebot_client.capabilities import Capabilities - from homeassistant.core import HomeAssistant, callback from homeassistant.util import slugify @@ -40,9 +38,8 @@ def get_supported_entitites( """Return all supported entities for all devices.""" return [ entity_class(device, capability, description) - for device in controller.devices(Capabilities) + for device in controller.devices for description in descriptions - if isinstance(device.capabilities, description.device_capabilities) if (capability := description.capability_fn(device.capabilities)) ] diff --git a/homeassistant/components/ecovacs/vacuum.py b/homeassistant/components/ecovacs/vacuum.py index e637eb14fd6..401274609d8 100644 --- a/homeassistant/components/ecovacs/vacuum.py +++ b/homeassistant/components/ecovacs/vacuum.py @@ -4,9 +4,9 @@ from __future__ import annotations from collections.abc import Mapping import logging -from typing import Any +from typing import TYPE_CHECKING, Any -from deebot_client.capabilities import VacuumCapabilities +from deebot_client.capabilities import Capabilities, DeviceType from deebot_client.device import Device from deebot_client.events import BatteryEvent, FanSpeedEvent, RoomsEvent, StateEvent from deebot_client.models import CleanAction, CleanMode, Room, State @@ -52,7 +52,9 @@ async def async_setup_entry( controller = config_entry.runtime_data vacuums: list[EcovacsVacuum | EcovacsLegacyVacuum] = [ - EcovacsVacuum(device) for device in controller.devices(VacuumCapabilities) + EcovacsVacuum(device) + for device in controller.devices + if device.capabilities.device_type is DeviceType.VACUUM ] for device in controller.legacy_devices: await hass.async_add_executor_job(device.connect_and_wait_until_ready) @@ -232,7 +234,7 @@ _ATTR_ROOMS = "rooms" class EcovacsVacuum( - EcovacsEntity[VacuumCapabilities, VacuumCapabilities], + EcovacsEntity[Capabilities], StateVacuumEntity, ): """Ecovacs vacuum.""" @@ -243,7 +245,6 @@ class EcovacsVacuum( VacuumEntityFeature.PAUSE | VacuumEntityFeature.STOP | VacuumEntityFeature.RETURN_HOME - | VacuumEntityFeature.FAN_SPEED | VacuumEntityFeature.BATTERY | VacuumEntityFeature.SEND_COMMAND | VacuumEntityFeature.LOCATE @@ -255,16 +256,17 @@ class EcovacsVacuum( key="vacuum", translation_key="vacuum", name=None ) - def __init__(self, device: Device[VacuumCapabilities]) -> None: + def __init__(self, device: Device) -> None: """Initialize the vacuum.""" - capabilities = device.capabilities - super().__init__(device, capabilities) + super().__init__(device, device.capabilities) self._rooms: list[Room] = [] - self._attr_fan_speed_list = [ - get_name_key(level) for level in capabilities.fan_speed.types - ] + if fan_speed := self._capability.fan_speed: + self._attr_supported_features |= VacuumEntityFeature.FAN_SPEED + self._attr_fan_speed_list = [ + get_name_key(level) for level in fan_speed.types + ] async def async_added_to_hass(self) -> None: """Set up the event listeners now that hass is ready.""" @@ -274,10 +276,6 @@ class EcovacsVacuum( self._attr_battery_level = event.value self.async_write_ha_state() - async def on_fan_speed(event: FanSpeedEvent) -> None: - self._attr_fan_speed = get_name_key(event.speed) - self.async_write_ha_state() - async def on_rooms(event: RoomsEvent) -> None: self._rooms = event.rooms self.async_write_ha_state() @@ -287,9 +285,16 @@ class EcovacsVacuum( self.async_write_ha_state() self._subscribe(self._capability.battery.event, on_battery) - self._subscribe(self._capability.fan_speed.event, on_fan_speed) self._subscribe(self._capability.state.event, on_status) + if self._capability.fan_speed: + + async def on_fan_speed(event: FanSpeedEvent) -> None: + self._attr_fan_speed = get_name_key(event.speed) + self.async_write_ha_state() + + self._subscribe(self._capability.fan_speed.event, on_fan_speed) + if map_caps := self._capability.map: self._subscribe(map_caps.rooms.event, on_rooms) @@ -319,6 +324,8 @@ class EcovacsVacuum( async def async_set_fan_speed(self, fan_speed: str, **kwargs: Any) -> None: """Set fan speed.""" + if TYPE_CHECKING: + assert self._capability.fan_speed await self._device.execute_command(self._capability.fan_speed.set(fan_speed)) async def async_return_to_base(self, **kwargs: Any) -> None: diff --git a/requirements_all.txt b/requirements_all.txt index 74df113ae97..88644b6b602 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -706,7 +706,7 @@ debugpy==1.8.1 # decora==0.6 # homeassistant.components.ecovacs -deebot-client==7.3.0 +deebot-client==8.0.0 # homeassistant.components.ihc # homeassistant.components.namecheapdns diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a508c8ff21e..b84951b56b9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -584,7 +584,7 @@ dbus-fast==2.21.3 debugpy==1.8.1 # homeassistant.components.ecovacs -deebot-client==7.3.0 +deebot-client==8.0.0 # homeassistant.components.ihc # homeassistant.components.namecheapdns diff --git a/tests/components/ecovacs/test_binary_sensor.py b/tests/components/ecovacs/test_binary_sensor.py index 697e57c6def..b57f67e948e 100644 --- a/tests/components/ecovacs/test_binary_sensor.py +++ b/tests/components/ecovacs/test_binary_sensor.py @@ -1,6 +1,5 @@ """Tests for Ecovacs binary sensors.""" -from deebot_client.capabilities import Capabilities from deebot_client.events import WaterAmount, WaterInfoEvent import pytest from syrupy import SnapshotAssertion @@ -38,7 +37,7 @@ async def test_mop_attached( assert entity_entry == snapshot(name=f"{entity_id}-entity_entry") assert entity_entry.device_id - device = next(controller.devices(Capabilities)) + device = controller.devices[0] assert (device_entry := device_registry.async_get(entity_entry.device_id)) assert device_entry.identifiers == {(DOMAIN, device.device_info["did"])} diff --git a/tests/components/ecovacs/test_button.py b/tests/components/ecovacs/test_button.py index 82a75654b58..08d53f3e93d 100644 --- a/tests/components/ecovacs/test_button.py +++ b/tests/components/ecovacs/test_button.py @@ -1,6 +1,5 @@ """Tests for Ecovacs sensors.""" -from deebot_client.capabilities import Capabilities from deebot_client.command import Command from deebot_client.commands.json import ResetLifeSpan, SetRelocationState from deebot_client.events import LifeSpan @@ -74,7 +73,7 @@ async def test_buttons( ) -> None: """Test that sensor entity snapshots match.""" assert hass.states.async_entity_ids() == [e[0] for e in entities] - device = next(controller.devices(Capabilities)) + device = controller.devices[0] for entity_id, command in entities: assert (state := hass.states.get(entity_id)), f"State of {entity_id} is missing" assert state.state == STATE_UNKNOWN diff --git a/tests/components/ecovacs/test_event.py b/tests/components/ecovacs/test_event.py index 1ee3efbf64d..03fb79e083f 100644 --- a/tests/components/ecovacs/test_event.py +++ b/tests/components/ecovacs/test_event.py @@ -2,7 +2,6 @@ from datetime import timedelta -from deebot_client.capabilities import Capabilities from deebot_client.events import CleanJobStatus, ReportStatsEvent from freezegun.api import FrozenDateTimeFactory import pytest @@ -44,7 +43,7 @@ async def test_last_job( assert entity_entry == snapshot(name=f"{entity_id}-entity_entry") assert entity_entry.device_id - device = next(controller.devices(Capabilities)) + device = controller.devices[0] assert (device_entry := device_registry.async_get(entity_entry.device_id)) assert device_entry.identifiers == {(DOMAIN, device.device_info["did"])} diff --git a/tests/components/ecovacs/test_init.py b/tests/components/ecovacs/test_init.py index 752276015d3..27d00a2d023 100644 --- a/tests/components/ecovacs/test_init.py +++ b/tests/components/ecovacs/test_init.py @@ -3,7 +3,6 @@ from typing import Any from unittest.mock import AsyncMock, Mock, patch -from deebot_client.capabilities import Capabilities from deebot_client.exceptions import DeebotError, InvalidAuthenticationError import pytest from syrupy import SnapshotAssertion @@ -121,7 +120,7 @@ async def test_devices_in_dr( snapshot: SnapshotAssertion, ) -> None: """Test all devices are in the device registry.""" - for device in controller.devices(Capabilities): + for device in controller.devices: assert ( device_entry := device_registry.async_get_device( identifiers={(DOMAIN, device.device_info["did"])} diff --git a/tests/components/ecovacs/test_lawn_mower.py b/tests/components/ecovacs/test_lawn_mower.py index cd49374d4c2..2c0abd0a49e 100644 --- a/tests/components/ecovacs/test_lawn_mower.py +++ b/tests/components/ecovacs/test_lawn_mower.py @@ -2,7 +2,6 @@ from dataclasses import dataclass -from deebot_client.capabilities import MowerCapabilities from deebot_client.command import Command from deebot_client.commands.json import Charge, CleanV2 from deebot_client.events import StateEvent @@ -56,7 +55,7 @@ async def test_lawn_mower( assert entity_entry == snapshot(name=f"{entity_id}-entity_entry") assert entity_entry.device_id - device = next(controller.devices(MowerCapabilities)) + device = controller.devices[0] assert (device_entry := device_registry.async_get(entity_entry.device_id)) assert device_entry.identifiers == {(DOMAIN, device.device_info["did"])} @@ -104,7 +103,7 @@ async def test_mover_services( tests: list[MowerTestCase], ) -> None: """Test mover services.""" - device = next(controller.devices(MowerCapabilities)) + device = controller.devices[0] for test in tests: device._execute_command.reset_mock() diff --git a/tests/components/ecovacs/test_number.py b/tests/components/ecovacs/test_number.py index 0b758fa6860..d444d6510a8 100644 --- a/tests/components/ecovacs/test_number.py +++ b/tests/components/ecovacs/test_number.py @@ -2,7 +2,6 @@ from dataclasses import dataclass -from deebot_client.capabilities import Capabilities from deebot_client.command import Command from deebot_client.commands.json import SetVolume from deebot_client.events import Event, VolumeEvent @@ -66,7 +65,7 @@ async def test_number_entities( tests: list[NumberTestCase], ) -> None: """Test that number entity snapshots match.""" - device = next(controller.devices(Capabilities)) + device = controller.devices[0] event_bus = device.events assert sorted(hass.states.async_entity_ids()) == sorted( @@ -131,7 +130,7 @@ async def test_volume_maximum( controller: EcovacsController, ) -> None: """Test volume maximum.""" - device = next(controller.devices(Capabilities)) + device = controller.devices[0] event_bus = device.events entity_id = "number.ozmo_950_volume" assert (state := hass.states.get(entity_id)) diff --git a/tests/components/ecovacs/test_select.py b/tests/components/ecovacs/test_select.py index b7e9435b416..02a6b5ebfa4 100644 --- a/tests/components/ecovacs/test_select.py +++ b/tests/components/ecovacs/test_select.py @@ -1,6 +1,5 @@ """Tests for Ecovacs select entities.""" -from deebot_client.capabilities import Capabilities from deebot_client.command import Command from deebot_client.commands.json import SetWaterInfo from deebot_client.event_bus import EventBus @@ -64,7 +63,7 @@ async def test_selects( assert (state := hass.states.get(entity_id)), f"State of {entity_id} is missing" assert state.state == STATE_UNKNOWN - device = next(controller.devices(Capabilities)) + device = controller.devices[0] await notify_events(hass, device.events) for entity_id in entity_ids: assert (state := hass.states.get(entity_id)), f"State of {entity_id} is missing" @@ -100,7 +99,7 @@ async def test_selects_change( command: Command, ) -> None: """Test that changing select entities works.""" - device = next(controller.devices(Capabilities)) + device = controller.devices[0] await notify_events(hass, device.events) assert (state := hass.states.get(entity_id)), f"State of {entity_id} is missing" diff --git a/tests/components/ecovacs/test_sensor.py b/tests/components/ecovacs/test_sensor.py index 5b8bf18e1d8..005d10bffbd 100644 --- a/tests/components/ecovacs/test_sensor.py +++ b/tests/components/ecovacs/test_sensor.py @@ -1,6 +1,5 @@ """Tests for Ecovacs sensors.""" -from deebot_client.capabilities import Capabilities from deebot_client.event_bus import EventBus from deebot_client.events import ( BatteryEvent, @@ -103,7 +102,7 @@ async def test_sensors( assert (state := hass.states.get(entity_id)), f"State of {entity_id} is missing" assert state.state == STATE_UNKNOWN - device = next(controller.devices(Capabilities)) + device = controller.devices[0] await notify_events(hass, device.events) for entity_id in entity_ids: assert (state := hass.states.get(entity_id)), f"State of {entity_id} is missing" diff --git a/tests/components/ecovacs/test_switch.py b/tests/components/ecovacs/test_switch.py index 2e3feb36586..b14cafeaba4 100644 --- a/tests/components/ecovacs/test_switch.py +++ b/tests/components/ecovacs/test_switch.py @@ -2,7 +2,6 @@ from dataclasses import dataclass -from deebot_client.capabilities import Capabilities from deebot_client.command import Command from deebot_client.commands.json import ( SetAdvancedMode, @@ -140,7 +139,7 @@ async def test_switch_entities( tests: list[SwitchTestCase], ) -> None: """Test switch entities.""" - device = next(controller.devices(Capabilities)) + device = controller.devices[0] event_bus = device.events assert hass.states.async_entity_ids() == [test.entity_id for test in tests] From 030fe6df4a422161355f731ae5998a4c954da603 Mon Sep 17 00:00:00 2001 From: Rami Mosleh Date: Thu, 13 Jun 2024 12:53:32 +0300 Subject: [PATCH 0627/1445] Store Mikrotik coordinator in runtime_data (#119594) --- homeassistant/components/mikrotik/__init__.py | 15 +++++++-------- .../components/mikrotik/device_tracker.py | 9 +++------ tests/components/mikrotik/test_init.py | 2 -- 3 files changed, 10 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/mikrotik/__init__.py b/homeassistant/components/mikrotik/__init__.py index 8e5911677af..9f2b40bf1c8 100644 --- a/homeassistant/components/mikrotik/__init__.py +++ b/homeassistant/components/mikrotik/__init__.py @@ -14,8 +14,12 @@ CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) PLATFORMS = [Platform.DEVICE_TRACKER] +type MikrotikConfigEntry = ConfigEntry[MikrotikDataUpdateCoordinator] -async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: + +async def async_setup_entry( + hass: HomeAssistant, config_entry: MikrotikConfigEntry +) -> bool: """Set up the Mikrotik component.""" try: api = await hass.async_add_executor_job(get_api, dict(config_entry.data)) @@ -28,7 +32,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b await hass.async_add_executor_job(coordinator.api.get_hub_details) await coordinator.async_config_entry_first_refresh() - hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = coordinator + config_entry.runtime_data = coordinator await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) @@ -47,9 +51,4 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Unload a config entry.""" - if unload_ok := await hass.config_entries.async_unload_platforms( - config_entry, PLATFORMS - ): - hass.data[DOMAIN].pop(config_entry.entry_id) - - return unload_ok + return await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS) diff --git a/homeassistant/components/mikrotik/device_tracker.py b/homeassistant/components/mikrotik/device_tracker.py index 073db547b4c..aa19da01369 100644 --- a/homeassistant/components/mikrotik/device_tracker.py +++ b/homeassistant/components/mikrotik/device_tracker.py @@ -9,26 +9,23 @@ from homeassistant.components.device_tracker import ( ScannerEntity, SourceType, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity import homeassistant.util.dt as dt_util -from .const import DOMAIN +from . import MikrotikConfigEntry from .coordinator import Device, MikrotikDataUpdateCoordinator async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: MikrotikConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up device tracker for Mikrotik component.""" - coordinator: MikrotikDataUpdateCoordinator = hass.data[DOMAIN][ - config_entry.entry_id - ] + coordinator = config_entry.runtime_data tracked: dict[str, MikrotikDataUpdateCoordinatorTracker] = {} diff --git a/tests/components/mikrotik/test_init.py b/tests/components/mikrotik/test_init.py index cc6a737e75a..97245480300 100644 --- a/tests/components/mikrotik/test_init.py +++ b/tests/components/mikrotik/test_init.py @@ -6,7 +6,6 @@ from librouteros.exceptions import ConnectionClosed, LibRouterosError import pytest from homeassistant.components import mikrotik -from homeassistant.components.mikrotik.const import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant @@ -84,4 +83,3 @@ async def test_unload_entry(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert entry.state is ConfigEntryState.NOT_LOADED - assert entry.entry_id not in hass.data[DOMAIN] From 40d9d22e76c1660401082a5449e755183d5dfa3a Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 13 Jun 2024 11:55:37 +0200 Subject: [PATCH 0628/1445] Fix dangerous-default-value warnings in deconz tests (#119599) --- tests/components/deconz/test_gateway.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/components/deconz/test_gateway.py b/tests/components/deconz/test_gateway.py index 610aea3b01b..b00a5cc1f05 100644 --- a/tests/components/deconz/test_gateway.py +++ b/tests/components/deconz/test_gateway.py @@ -1,6 +1,7 @@ """Test deCONZ gateway.""" from copy import deepcopy +from typing import Any from unittest.mock import patch import pydeconz @@ -44,6 +45,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.typing import UNDEFINED, UndefinedType from tests.common import MockConfigEntry from tests.test_util.aiohttp import AiohttpClientMocker @@ -105,12 +107,10 @@ def mock_deconz_put_request(aioclient_mock, config, path): async def setup_deconz_integration( - hass, - aioclient_mock=None, + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker | None = None, *, - config=ENTRY_CONFIG, - options=ENTRY_OPTIONS, - get_state_response=DECONZ_WEB_REQUEST, + options: dict[str, Any] | UndefinedType = UNDEFINED, entry_id="1", unique_id=BRIDGEID, source=SOURCE_USER, @@ -119,15 +119,15 @@ async def setup_deconz_integration( config_entry = MockConfigEntry( domain=DECONZ_DOMAIN, source=source, - data=deepcopy(config), - options=deepcopy(options), + data=deepcopy(ENTRY_CONFIG), + options=deepcopy(ENTRY_OPTIONS if options is UNDEFINED else options), entry_id=entry_id, unique_id=unique_id, ) config_entry.add_to_hass(hass) if aioclient_mock: - mock_deconz_request(aioclient_mock, config, get_state_response) + mock_deconz_request(aioclient_mock, ENTRY_CONFIG, DECONZ_WEB_REQUEST) await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() From 315e5f1d9597ae22a6d9545caeda7263ad542cad Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 13 Jun 2024 11:55:56 +0200 Subject: [PATCH 0629/1445] Fix import-outside-toplevel pylint warnings in zha tests (#119451) --- tests/components/zha/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/components/zha/conftest.py b/tests/components/zha/conftest.py index e75a84406d6..326c3cfcd76 100644 --- a/tests/components/zha/conftest.py +++ b/tests/components/zha/conftest.py @@ -63,7 +63,7 @@ def globally_load_quirks(): run. """ - import zhaquirks + import zhaquirks # pylint: disable=import-outside-toplevel zhaquirks.setup() From 27c08bcb5efbe705438c2ff6751f49ea789815f9 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 13 Jun 2024 11:57:45 +0200 Subject: [PATCH 0630/1445] Fix dangerous-default-value warnings in lastfm tests (#119601) --- tests/components/lastfm/__init__.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/tests/components/lastfm/__init__.py b/tests/components/lastfm/__init__.py index 8f133607c8d..9fe946f8dff 100644 --- a/tests/components/lastfm/__init__.py +++ b/tests/components/lastfm/__init__.py @@ -6,6 +6,7 @@ from pylast import PyLastError, Track from homeassistant.components.lastfm.const import CONF_MAIN_USER, CONF_USERS from homeassistant.const import CONF_API_KEY +from homeassistant.helpers.typing import UNDEFINED, UndefinedType API_KEY = "asdasdasdasdasd" USERNAME_1 = "testaccount1" @@ -52,16 +53,16 @@ class MockUser: username: str = USERNAME_1, now_playing_result: Track | None = None, thrown_error: Exception | None = None, - friends: list = [], - recent_tracks: list[Track] = [], - top_tracks: list[Track] = [], + friends: list | UndefinedType = UNDEFINED, + recent_tracks: list[Track] | UndefinedType = UNDEFINED, + top_tracks: list[Track] | UndefinedType = UNDEFINED, ) -> None: """Initialize the mock.""" self._now_playing_result = now_playing_result self._thrown_error = thrown_error - self._friends = friends - self._recent_tracks = recent_tracks - self._top_tracks = top_tracks + self._friends = [] if friends is UNDEFINED else friends + self._recent_tracks = [] if recent_tracks is UNDEFINED else recent_tracks + self._top_tracks = [] if top_tracks is UNDEFINED else top_tracks self.name = username def get_name(self, capitalized: bool) -> str: From 887109046389b7c71927af9b2235a3d704263ae9 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 13 Jun 2024 13:53:17 +0200 Subject: [PATCH 0631/1445] Fix dangerous-default-value warnings in fronius tests (#119600) --- tests/components/fronius/__init__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/components/fronius/__init__.py b/tests/components/fronius/__init__.py index 6cefae734a0..2109d4a6692 100644 --- a/tests/components/fronius/__init__.py +++ b/tests/components/fronius/__init__.py @@ -11,6 +11,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.typing import UNDEFINED, UndefinedType from tests.common import MockConfigEntry, async_fire_time_changed, load_fixture from tests.test_util.aiohttp import AiohttpClientMocker @@ -64,7 +65,7 @@ def mock_responses( aioclient_mock: AiohttpClientMocker, host: str = MOCK_HOST, fixture_set: str = "symo", - inverter_ids: list[str | int] = [1], + inverter_ids: list[str | int] | UndefinedType = UNDEFINED, night: bool = False, override_data: dict[str, list[tuple[list[str], Any]]] | None = None, # {filename: [([list of nested keys], patch_value)]} @@ -78,7 +79,7 @@ def mock_responses( f"{host}/solar_api/GetAPIVersion.cgi", text=_load(f"{fixture_set}/GetAPIVersion.json", "fronius"), ) - for inverter_id in inverter_ids: + for inverter_id in [1] if inverter_ids is UNDEFINED else inverter_ids: aioclient_mock.get( f"{host}/solar_api/v1/GetInverterRealtimeData.cgi?Scope=Device&" f"DeviceId={inverter_id}&DataCollection=CommonInverterData", From 9f322b20d119c74277ba238e8eab27753a737a78 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 13 Jun 2024 16:04:32 +0200 Subject: [PATCH 0632/1445] Use send_json_auto_id in some collection tests (#119570) Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> --- tests/helpers/test_collection.py | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/tests/helpers/test_collection.py b/tests/helpers/test_collection.py index 6d2764afb16..4be372efe9c 100644 --- a/tests/helpers/test_collection.py +++ b/tests/helpers/test_collection.py @@ -450,9 +450,8 @@ async def test_storage_collection_websocket( client = await hass_ws_client(hass) # Create invalid - await client.send_json( + await client.send_json_auto_id( { - "id": 1, "type": "test_item/collection/create", "name": 1, # Forgot to add immutable_string @@ -464,9 +463,8 @@ async def test_storage_collection_websocket( assert len(changes) == 0 # Create - await client.send_json( + await client.send_json_auto_id( { - "id": 2, "type": "test_item/collection/create", "name": "Initial Name", "immutable_string": "no-changes", @@ -483,7 +481,7 @@ async def test_storage_collection_websocket( assert changes[0] == (collection.CHANGE_ADDED, "initial_name", response["result"]) # List - await client.send_json({"id": 3, "type": "test_item/collection/list"}) + await client.send_json_auto_id({"type": "test_item/collection/list"}) response = await client.receive_json() assert response["success"] assert response["result"] == [ @@ -496,9 +494,8 @@ async def test_storage_collection_websocket( assert len(changes) == 1 # Update invalid data - await client.send_json( + await client.send_json_auto_id( { - "id": 4, "type": "test_item/collection/update", "test_item_id": "initial_name", "immutable_string": "no-changes", @@ -510,9 +507,8 @@ async def test_storage_collection_websocket( assert len(changes) == 1 # Update invalid item - await client.send_json( + await client.send_json_auto_id( { - "id": 5, "type": "test_item/collection/update", "test_item_id": "non-existing", "name": "Updated name", @@ -524,9 +520,8 @@ async def test_storage_collection_websocket( assert len(changes) == 1 # Update - await client.send_json( + await client.send_json_auto_id( { - "id": 6, "type": "test_item/collection/update", "test_item_id": "initial_name", "name": "Updated name", @@ -543,8 +538,8 @@ async def test_storage_collection_websocket( assert changes[1] == (collection.CHANGE_UPDATED, "initial_name", response["result"]) # Delete invalid ID - await client.send_json( - {"id": 7, "type": "test_item/collection/update", "test_item_id": "non-existing"} + await client.send_json_auto_id( + {"type": "test_item/collection/update", "test_item_id": "non-existing"} ) response = await client.receive_json() assert not response["success"] @@ -552,8 +547,8 @@ async def test_storage_collection_websocket( assert len(changes) == 2 # Delete - await client.send_json( - {"id": 8, "type": "test_item/collection/delete", "test_item_id": "initial_name"} + await client.send_json_auto_id( + {"type": "test_item/collection/delete", "test_item_id": "initial_name"} ) response = await client.receive_json() assert response["success"] From e34c42c0a90832b4117fe3c30f58e7002f5128c3 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 13 Jun 2024 16:47:53 +0200 Subject: [PATCH 0633/1445] Fix dangerous-default-value warnings in greeneye_monitor tests (#119581) --- tests/components/greeneye_monitor/conftest.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/components/greeneye_monitor/conftest.py b/tests/components/greeneye_monitor/conftest.py index 975a0119313..ad8a98ce3fe 100644 --- a/tests/components/greeneye_monitor/conftest.py +++ b/tests/components/greeneye_monitor/conftest.py @@ -19,13 +19,15 @@ def assert_sensor_state( hass: HomeAssistant, entity_id: str, expected_state: str, - attributes: dict[str, Any] = {}, + attributes: dict[str, Any] | None = None, ) -> None: """Assert that the given entity has the expected state and at least the provided attributes.""" state = hass.states.get(entity_id) assert state actual_state = state.state assert actual_state == expected_state + if not attributes: + return for key, value in attributes.items(): assert key in state.attributes assert state.attributes[key] == value From 27ee204e2f6820f54c450dfdded830eb43ac8f94 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 13 Jun 2024 16:51:45 +0200 Subject: [PATCH 0634/1445] Fix dangerous-default-value warnings in mqtt tests (#119584) --- tests/components/mqtt/test_siren.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/components/mqtt/test_siren.py b/tests/components/mqtt/test_siren.py index bb4b103225e..28b88e2793d 100644 --- a/tests/components/mqtt/test_siren.py +++ b/tests/components/mqtt/test_siren.py @@ -61,8 +61,8 @@ DEFAULT_CONFIG = { async def async_turn_on( hass: HomeAssistant, - entity_id: str = ENTITY_MATCH_ALL, - parameters: dict[str, Any] = {}, + entity_id: str, + parameters: dict[str, Any], ) -> None: """Turn all or specified siren on.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} @@ -144,7 +144,7 @@ async def test_sending_mqtt_commands_and_optimistic( assert state.state == STATE_OFF assert state.attributes.get(ATTR_ASSUMED_STATE) - await async_turn_on(hass, entity_id="siren.test") + await async_turn_on(hass, entity_id="siren.test", parameters={}) mqtt_mock.async_publish.assert_called_once_with( "command-topic", '{"state":"beer on"}', 2, False From 23edbf7bbff5d180f73d287e3b96698527589e2e Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 13 Jun 2024 16:53:00 +0200 Subject: [PATCH 0635/1445] Fix dangerous-default-value warnings in subaru tests (#119604) --- tests/components/subaru/conftest.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/tests/components/subaru/conftest.py b/tests/components/subaru/conftest.py index 307199d43ac..f769eba252c 100644 --- a/tests/components/subaru/conftest.py +++ b/tests/components/subaru/conftest.py @@ -1,6 +1,7 @@ """Common functions needed to setup tests for Subaru component.""" from datetime import timedelta +from typing import Any from unittest.mock import patch import pytest @@ -29,6 +30,8 @@ from homeassistant.const import ( CONF_PIN, CONF_USERNAME, ) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.typing import UNDEFINED, UndefinedType from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util @@ -104,15 +107,18 @@ def advance_time_to_next_fetch(hass): async def setup_subaru_config_entry( - hass, + hass: HomeAssistant, config_entry, - vehicle_list=[TEST_VIN_2_EV], - vehicle_data=VEHICLE_DATA[TEST_VIN_2_EV], - vehicle_status=VEHICLE_STATUS_EV, + vehicle_list: list[str] | UndefinedType = UNDEFINED, + vehicle_data: dict[str, Any] | UndefinedType = UNDEFINED, + vehicle_status: dict[str, Any] | UndefinedType = UNDEFINED, connect_effect=None, fetch_effect=None, ): """Run async_setup with API mocks in place.""" + if vehicle_data is UNDEFINED: + vehicle_data = VEHICLE_DATA[TEST_VIN_2_EV] + with ( patch( MOCK_API_CONNECT, @@ -121,7 +127,7 @@ async def setup_subaru_config_entry( ), patch( MOCK_API_GET_VEHICLES, - return_value=vehicle_list, + return_value=[TEST_VIN_2_EV] if vehicle_list is UNDEFINED else vehicle_list, ), patch( MOCK_API_VIN_TO_NAME, @@ -161,7 +167,9 @@ async def setup_subaru_config_entry( ), patch( MOCK_API_GET_DATA, - return_value=vehicle_status, + return_value=VEHICLE_STATUS_EV + if vehicle_status is UNDEFINED + else vehicle_status, ), patch( MOCK_API_UPDATE, From f2ce510484011732c144762365c984fb4a46af9f Mon Sep 17 00:00:00 2001 From: Rami Mosleh Date: Thu, 13 Jun 2024 17:54:40 +0300 Subject: [PATCH 0636/1445] Store islamic prayer times coordinator in runtime_data (#119612) --- .../islamic_prayer_times/__init__.py | 24 +++++++++++-------- .../components/islamic_prayer_times/sensor.py | 11 ++++----- 2 files changed, 18 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/islamic_prayer_times/__init__.py b/homeassistant/components/islamic_prayer_times/__init__.py index 15e165d2f48..089afc88564 100644 --- a/homeassistant/components/islamic_prayer_times/__init__.py +++ b/homeassistant/components/islamic_prayer_times/__init__.py @@ -18,8 +18,12 @@ CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) _LOGGER = logging.getLogger(__name__) +type IslamicPrayerTimesConfigEntry = ConfigEntry[IslamicPrayerDataUpdateCoordinator] -async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: + +async def async_setup_entry( + hass: HomeAssistant, config_entry: IslamicPrayerTimesConfigEntry +) -> bool: """Set up the Islamic Prayer Component.""" @callback @@ -37,7 +41,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b coordinator = IslamicPrayerDataUpdateCoordinator(hass) await coordinator.async_config_entry_first_refresh() - hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = coordinator + config_entry.runtime_data = coordinator config_entry.async_on_unload( config_entry.add_update_listener(async_options_updated) ) @@ -72,24 +76,24 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> return True -async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: +async def async_unload_entry( + hass: HomeAssistant, config_entry: IslamicPrayerTimesConfigEntry +) -> bool: """Unload Islamic Prayer entry from config_entry.""" if unload_ok := await hass.config_entries.async_unload_platforms( config_entry, PLATFORMS ): - coordinator: IslamicPrayerDataUpdateCoordinator = hass.data[DOMAIN].pop( - config_entry.entry_id - ) + coordinator = config_entry.runtime_data if coordinator.event_unsub: coordinator.event_unsub() - if not hass.data[DOMAIN]: - del hass.data[DOMAIN] return unload_ok -async def async_options_updated(hass: HomeAssistant, entry: ConfigEntry) -> None: +async def async_options_updated( + hass: HomeAssistant, entry: IslamicPrayerTimesConfigEntry +) -> None: """Triggered by config entry options updates.""" - coordinator: IslamicPrayerDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + coordinator = entry.runtime_data if coordinator.event_unsub: coordinator.event_unsub() await coordinator.async_request_refresh() diff --git a/homeassistant/components/islamic_prayer_times/sensor.py b/homeassistant/components/islamic_prayer_times/sensor.py index eb042d83c49..c46b629d2d8 100644 --- a/homeassistant/components/islamic_prayer_times/sensor.py +++ b/homeassistant/components/islamic_prayer_times/sensor.py @@ -7,14 +7,14 @@ from homeassistant.components.sensor import ( SensorEntity, SensorEntityDescription, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity -from . import IslamicPrayerDataUpdateCoordinator +from . import IslamicPrayerTimesConfigEntry from .const import DOMAIN, NAME +from .coordinator import IslamicPrayerDataUpdateCoordinator SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( @@ -50,15 +50,12 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: IslamicPrayerTimesConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Islamic prayer times sensor platform.""" - coordinator: IslamicPrayerDataUpdateCoordinator = hass.data[DOMAIN][ - config_entry.entry_id - ] - + coordinator = config_entry.runtime_data async_add_entities( IslamicPrayerTimeSensor(coordinator, description) for description in SENSOR_TYPES From 2a061f58ebd5931bc9f5495f526c65c13f3a66f1 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 13 Jun 2024 16:55:06 +0200 Subject: [PATCH 0637/1445] Fix dangerous-default-value warnings in tessie tests (#119605) --- tests/components/tessie/common.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/components/tessie/common.py b/tests/components/tessie/common.py index d4fc002ba25..c19f6f65201 100644 --- a/tests/components/tessie/common.py +++ b/tests/components/tessie/common.py @@ -12,6 +12,7 @@ from homeassistant.components.tessie.const import DOMAIN, TessieStatus from homeassistant.const import CONF_ACCESS_TOKEN, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.typing import UNDEFINED, UndefinedType from tests.common import MockConfigEntry, load_json_object_fixture @@ -48,7 +49,7 @@ ERROR_CONNECTION = ClientConnectionError() async def setup_platform( - hass: HomeAssistant, platforms: list[Platform] = PLATFORMS + hass: HomeAssistant, platforms: list[Platform] | UndefinedType = UNDEFINED ) -> MockConfigEntry: """Set up the Tessie platform.""" @@ -58,7 +59,10 @@ async def setup_platform( ) mock_entry.add_to_hass(hass) - with patch("homeassistant.components.tessie.PLATFORMS", platforms): + with patch( + "homeassistant.components.tessie.PLATFORMS", + PLATFORMS if platforms is UNDEFINED else platforms, + ): await hass.config_entries.async_setup(mock_entry.entry_id) await hass.async_block_till_done() From 349ac5461682d24f2393751ebdf37f8124d26ed3 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 13 Jun 2024 16:55:48 +0200 Subject: [PATCH 0638/1445] Fix dangerous-default-value warnings in auth tests (#119597) --- tests/components/auth/__init__.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/tests/components/auth/__init__.py b/tests/components/auth/__init__.py index 18904cb2710..7b48855493e 100644 --- a/tests/components/auth/__init__.py +++ b/tests/components/auth/__init__.py @@ -4,6 +4,7 @@ from typing import Any from homeassistant import auth from homeassistant.core import HomeAssistant +from homeassistant.helpers.typing import UNDEFINED, UndefinedType from homeassistant.setup import async_setup_component from tests.common import ensure_auth_manager_loaded @@ -26,14 +27,16 @@ EMPTY_CONFIG = [] async def async_setup_auth( hass: HomeAssistant, aiohttp_client: ClientSessionGenerator, - provider_configs: list[dict[str, Any]] = BASE_CONFIG, - module_configs=EMPTY_CONFIG, + provider_configs: list[dict[str, Any]] | UndefinedType = UNDEFINED, + module_configs: list[dict[str, Any]] | UndefinedType = UNDEFINED, setup_api: bool = False, custom_ip: str | None = None, ): """Set up authentication and create an HTTP client.""" hass.auth = await auth.auth_manager_from_config( - hass, provider_configs, module_configs + hass, + BASE_CONFIG if provider_configs is UNDEFINED else provider_configs, + EMPTY_CONFIG if module_configs is UNDEFINED else module_configs, ) ensure_auth_manager_loaded(hass.auth) await async_setup_component(hass, "auth", {}) From 3b8337985ce3c5ff4cd011fe5207b7dfa1fad2d7 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 13 Jun 2024 16:56:22 +0200 Subject: [PATCH 0639/1445] Fix dangerous-default-value warnings in environment_canada tests (#119586) --- .../environment_canada/test_config_flow.py | 24 ++++++------------- 1 file changed, 7 insertions(+), 17 deletions(-) diff --git a/tests/components/environment_canada/test_config_flow.py b/tests/components/environment_canada/test_config_flow.py index 3571c74cdcc..f2c35ab4295 100644 --- a/tests/components/environment_canada/test_config_flow.py +++ b/tests/components/environment_canada/test_config_flow.py @@ -23,26 +23,16 @@ FAKE_CONFIG = { FAKE_TITLE = "Universal title!" -def mocked_ec( - station_id=FAKE_CONFIG[CONF_STATION], - lat=FAKE_CONFIG[CONF_LATITUDE], - lon=FAKE_CONFIG[CONF_LONGITUDE], - lang=FAKE_CONFIG[CONF_LANGUAGE], - update=None, - metadata={"location": FAKE_TITLE}, -): +def mocked_ec(): """Mock the env_canada library.""" ec_mock = MagicMock() - ec_mock.station_id = station_id - ec_mock.lat = lat - ec_mock.lon = lon - ec_mock.language = lang - ec_mock.metadata = metadata + ec_mock.station_id = FAKE_CONFIG[CONF_STATION] + ec_mock.lat = FAKE_CONFIG[CONF_LATITUDE] + ec_mock.lon = FAKE_CONFIG[CONF_LONGITUDE] + ec_mock.language = FAKE_CONFIG[CONF_LANGUAGE] + ec_mock.metadata = {"location": FAKE_TITLE} - if update: - ec_mock.update = update - else: - ec_mock.update = AsyncMock() + ec_mock.update = AsyncMock() return patch( "homeassistant.components.environment_canada.config_flow.ECWeather", From 50fe29ccc1191e0624c618304fc6efb4abf92d5c Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 13 Jun 2024 16:57:20 +0200 Subject: [PATCH 0640/1445] Fix attribute-defined-outside-init in harmony tests (#119614) --- tests/components/harmony/conftest.py | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/tests/components/harmony/conftest.py b/tests/components/harmony/conftest.py index 1e6bbd7a3c3..fb4be73aa72 100644 --- a/tests/components/harmony/conftest.py +++ b/tests/components/harmony/conftest.py @@ -4,6 +4,7 @@ from unittest.mock import AsyncMock, MagicMock, PropertyMock, patch from aioharmony.const import ClientCallbackType import pytest +from typing_extensions import Generator from homeassistant.components.harmony.const import ACTIVITY_POWER_OFF, DOMAIN from homeassistant.const import CONF_HOST, CONF_NAME @@ -46,21 +47,17 @@ IDS_TO_DEVICES = { class FakeHarmonyClient: """FakeHarmonyClient to mock away network calls.""" - def initialize( - self, ip_address: str = "", callbacks: ClientCallbackType = MagicMock() - ): + _callbacks: ClientCallbackType + + def __init__(self) -> None: """Initialize FakeHarmonyClient class to capture callbacks.""" - # pylint: disable=attribute-defined-outside-init self._activity_name = "Watch TV" self.close = AsyncMock() self.send_commands = AsyncMock() self.change_channel = AsyncMock() self.sync = AsyncMock() - self._callbacks = callbacks self.fw_version = "123.456" - return self - async def connect(self): """Connect and call the appropriate callbacks.""" self._callbacks.connect(None) @@ -152,20 +149,27 @@ class FakeHarmonyClient: @pytest.fixture -def harmony_client(): +def harmony_client() -> FakeHarmonyClient: """Create the FakeHarmonyClient instance.""" return FakeHarmonyClient() @pytest.fixture -def mock_hc(harmony_client): +def mock_hc(harmony_client: FakeHarmonyClient) -> Generator[None]: """Patch the real HarmonyClient with initialization side effect.""" + def _on_create_instance( + ip_address: str, callbacks: ClientCallbackType + ) -> FakeHarmonyClient: + """Set client callbacks on instance creation.""" + harmony_client._callbacks = callbacks + return harmony_client + with patch( "homeassistant.components.harmony.data.HarmonyClient", - side_effect=harmony_client.initialize, - ) as fake: - yield fake + side_effect=_on_create_instance, + ): + yield @pytest.fixture From bb2883a5a831c824bc9ff0d1369516f2c8d99a59 Mon Sep 17 00:00:00 2001 From: Rami Mosleh Date: Thu, 13 Jun 2024 17:58:05 +0300 Subject: [PATCH 0641/1445] Store imap coordinator in runtime_data (#119611) --- homeassistant/components/imap/__init__.py | 27 +++++++++----------- homeassistant/components/imap/diagnostics.py | 10 +++----- homeassistant/components/imap/sensor.py | 17 +++++------- 3 files changed, 22 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/imap/__init__.py b/homeassistant/components/imap/__init__.py index f39a78925c1..f62edf1451f 100644 --- a/homeassistant/components/imap/__init__.py +++ b/homeassistant/components/imap/__init__.py @@ -4,12 +4,11 @@ from __future__ import annotations import asyncio import logging -from typing import TYPE_CHECKING from aioimaplib import IMAP4_SSL, AioImapException, Response import voluptuous as vol -from homeassistant.config_entries import ConfigEntry +from homeassistant.config_entries import ConfigEntry, ConfigEntryState from homeassistant.const import EVENT_HOMEASSISTANT_STOP, Platform from homeassistant.core import ( HomeAssistant, @@ -29,6 +28,7 @@ from homeassistant.helpers.typing import ConfigType from .const import CONF_ENABLE_PUSH, DOMAIN from .coordinator import ( + ImapDataUpdateCoordinator, ImapMessage, ImapPollingDataUpdateCoordinator, ImapPushDataUpdateCoordinator, @@ -65,17 +65,18 @@ SERVICE_MOVE_SCHEMA = _SERVICE_UID_SCHEMA.extend( SERVICE_DELETE_SCHEMA = _SERVICE_UID_SCHEMA SERVICE_FETCH_TEXT_SCHEMA = _SERVICE_UID_SCHEMA +type ImapConfigEntry = ConfigEntry[ImapDataUpdateCoordinator] + async def async_get_imap_client(hass: HomeAssistant, entry_id: str) -> IMAP4_SSL: """Get IMAP client and connect.""" - if hass.data[DOMAIN].get(entry_id) is None: + if (entry := hass.config_entries.async_get_entry(entry_id)) is None or ( + entry.state is not ConfigEntryState.LOADED + ): raise ServiceValidationError( translation_domain=DOMAIN, translation_key="invalid_entry", ) - entry = hass.config_entries.async_get_entry(entry_id) - if TYPE_CHECKING: - assert entry is not None try: client = await connect_to_server(entry.data) except InvalidAuth as exc: @@ -235,7 +236,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: return True -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: ImapConfigEntry) -> bool: """Set up imap from a config entry.""" try: imap_client: IMAP4_SSL = await connect_to_server(dict(entry.data)) @@ -255,12 +256,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: else: coordinator_class = ImapPollingDataUpdateCoordinator - coordinator: ImapPushDataUpdateCoordinator | ImapPollingDataUpdateCoordinator = ( - coordinator_class(hass, imap_client, entry) - ) + coordinator: ImapDataUpdateCoordinator = coordinator_class(hass, imap_client, entry) await coordinator.async_config_entry_first_refresh() - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator + entry.runtime_data = coordinator entry.async_on_unload( hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, coordinator.shutdown) @@ -271,11 +270,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: ImapConfigEntry) -> bool: """Unload a config entry.""" if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): - coordinator: ( - ImapPushDataUpdateCoordinator | ImapPollingDataUpdateCoordinator - ) = hass.data[DOMAIN].pop(entry.entry_id) + coordinator = entry.runtime_data await coordinator.shutdown() return unload_ok diff --git a/homeassistant/components/imap/diagnostics.py b/homeassistant/components/imap/diagnostics.py index 8afe3e327ba..d402053520a 100644 --- a/homeassistant/components/imap/diagnostics.py +++ b/homeassistant/components/imap/diagnostics.py @@ -5,18 +5,16 @@ from __future__ import annotations from typing import Any from homeassistant.components.diagnostics import async_redact_data -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant, callback -from .const import DOMAIN -from .coordinator import ImapDataUpdateCoordinator +from . import ImapConfigEntry REDACT_CONFIG = {CONF_PASSWORD, CONF_USERNAME} async def async_get_config_entry_diagnostics( - hass: HomeAssistant, entry: ConfigEntry + hass: HomeAssistant, entry: ImapConfigEntry ) -> dict[str, Any]: """Return diagnostics for a config entry.""" return _async_get_diagnostics(hass, entry) @@ -25,11 +23,11 @@ async def async_get_config_entry_diagnostics( @callback def _async_get_diagnostics( hass: HomeAssistant, - entry: ConfigEntry, + entry: ImapConfigEntry, ) -> dict[str, Any]: """Return diagnostics for a config entry.""" redacted_config = async_redact_data(entry.data, REDACT_CONFIG) - coordinator: ImapDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + coordinator = entry.runtime_data return { "config": redacted_config, diff --git a/homeassistant/components/imap/sensor.py b/homeassistant/components/imap/sensor.py index 0a9070d7a5e..625af9ce6a1 100644 --- a/homeassistant/components/imap/sensor.py +++ b/homeassistant/components/imap/sensor.py @@ -7,15 +7,15 @@ from homeassistant.components.sensor import ( SensorEntityDescription, SensorStateClass, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity -from . import ImapPollingDataUpdateCoordinator, ImapPushDataUpdateCoordinator +from . import ImapConfigEntry from .const import DOMAIN +from .coordinator import ImapDataUpdateCoordinator IMAP_MAIL_COUNT_DESCRIPTION = SensorEntityDescription( key="imap_mail_count", @@ -27,27 +27,22 @@ IMAP_MAIL_COUNT_DESCRIPTION = SensorEntityDescription( async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, entry: ImapConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up the Imap sensor.""" - coordinator: ImapPushDataUpdateCoordinator | ImapPollingDataUpdateCoordinator = ( - hass.data[DOMAIN][entry.entry_id] - ) + coordinator = entry.runtime_data async_add_entities([ImapSensor(coordinator, IMAP_MAIL_COUNT_DESCRIPTION)]) -class ImapSensor( - CoordinatorEntity[ImapPushDataUpdateCoordinator | ImapPollingDataUpdateCoordinator], - SensorEntity, -): +class ImapSensor(CoordinatorEntity[ImapDataUpdateCoordinator], SensorEntity): """Representation of an IMAP sensor.""" _attr_has_entity_name = True def __init__( self, - coordinator: ImapPushDataUpdateCoordinator | ImapPollingDataUpdateCoordinator, + coordinator: ImapDataUpdateCoordinator, description: SensorEntityDescription, ) -> None: """Initialize the sensor.""" From ca8d3e0c83bc1366f57830e3cdaafd6190a2b0c3 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 13 Jun 2024 16:58:41 +0200 Subject: [PATCH 0642/1445] Ignore unnecessary-lambda warnings in tests (#119564) --- tests/common.py | 2 + .../components/bayesian/test_binary_sensor.py | 10 +++- tests/components/demo/test_update.py | 2 + tests/components/pilight/test_init.py | 2 + tests/components/rainforest_raven/__init__.py | 1 + .../components/universal/test_media_player.py | 5 +- tests/components/update/test_init.py | 10 +++- tests/helpers/test_event.py | 52 ++++++++++++++++--- 8 files changed, 71 insertions(+), 13 deletions(-) diff --git a/tests/common.py b/tests/common.py index 5cb82cef3ba..ec7b5ca46b7 100644 --- a/tests/common.py +++ b/tests/common.py @@ -818,6 +818,7 @@ class MockModule: if setup: # We run this in executor, wrap it in function + # pylint: disable-next=unnecessary-lambda self.setup = lambda *args: setup(*args) if async_setup is not None: @@ -875,6 +876,7 @@ class MockPlatform: if setup_platform is not None: # We run this in executor, wrap it in function + # pylint: disable-next=unnecessary-lambda self.setup_platform = lambda *args: setup_platform(*args) if async_setup_platform is not None: diff --git a/tests/components/bayesian/test_binary_sensor.py b/tests/components/bayesian/test_binary_sensor.py index aaade6da2f4..e4f646572cb 100644 --- a/tests/components/bayesian/test_binary_sensor.py +++ b/tests/components/bayesian/test_binary_sensor.py @@ -1012,7 +1012,10 @@ async def test_template_triggers(hass: HomeAssistant) -> None: events = [] async_track_state_change_event( - hass, "binary_sensor.test_binary", callback(lambda event: events.append(event)) + hass, + "binary_sensor.test_binary", + # pylint: disable-next=unnecessary-lambda + callback(lambda event: events.append(event)), ) context = Context() @@ -1051,7 +1054,10 @@ async def test_state_triggers(hass: HomeAssistant) -> None: events = [] async_track_state_change_event( - hass, "binary_sensor.test_binary", callback(lambda event: events.append(event)) + hass, + "binary_sensor.test_binary", + # pylint: disable-next=unnecessary-lambda + callback(lambda event: events.append(event)), ) context = Context() diff --git a/tests/components/demo/test_update.py b/tests/components/demo/test_update.py index d8af9c21c75..e8fe909541c 100644 --- a/tests/components/demo/test_update.py +++ b/tests/components/demo/test_update.py @@ -134,6 +134,7 @@ async def test_update_with_progress(hass: HomeAssistant) -> None: async_track_state_change_event( hass, "update.demo_update_with_progress", + # pylint: disable-next=unnecessary-lambda callback(lambda event: events.append(event)), ) @@ -170,6 +171,7 @@ async def test_update_with_progress_raising(hass: HomeAssistant) -> None: events = [] async_track_state_change_event( hass, + # pylint: disable-next=unnecessary-lambda "update.demo_update_with_progress", callback(lambda event: events.append(event)), ) diff --git a/tests/components/pilight/test_init.py b/tests/components/pilight/test_init.py index 7c7c39d5616..c48135f59eb 100644 --- a/tests/components/pilight/test_init.py +++ b/tests/components/pilight/test_init.py @@ -362,6 +362,7 @@ async def test_call_rate_delay_throttle_enabled(hass: HomeAssistant) -> None: delay = 5.0 limit = pilight.CallRateDelayThrottle(hass, delay) + # pylint: disable-next=unnecessary-lambda action = limit.limited(lambda x: runs.append(x)) for i in range(3): @@ -385,6 +386,7 @@ def test_call_rate_delay_throttle_disabled(hass: HomeAssistant) -> None: runs = [] limit = pilight.CallRateDelayThrottle(hass, 0.0) + # pylint: disable-next=unnecessary-lambda action = limit.limited(lambda x: runs.append(x)) for i in range(3): diff --git a/tests/components/rainforest_raven/__init__.py b/tests/components/rainforest_raven/__init__.py index eb3cb4efcc2..9d40652b42d 100644 --- a/tests/components/rainforest_raven/__init__.py +++ b/tests/components/rainforest_raven/__init__.py @@ -27,6 +27,7 @@ def create_mock_device() -> AsyncMock: device.get_device_info.return_value = DEVICE_INFO device.get_instantaneous_demand.return_value = DEMAND device.get_meter_list.return_value = METER_LIST + # pylint: disable-next=unnecessary-lambda device.get_meter_info.side_effect = lambda meter: METER_INFO.get(meter) device.get_network_info.return_value = NETWORK_INFO diff --git a/tests/components/universal/test_media_player.py b/tests/components/universal/test_media_player.py index 6869e025b33..814fa34a125 100644 --- a/tests/components/universal/test_media_player.py +++ b/tests/components/universal/test_media_player.py @@ -1271,7 +1271,10 @@ async def test_master_state_with_template(hass: HomeAssistant) -> None: events = [] async_track_state_change_event( - hass, "media_player.tv", callback(lambda event: events.append(event)) + hass, + "media_player.tv", + # pylint: disable-next=unnecessary-lambda + callback(lambda event: events.append(event)), ) context = Context() diff --git a/tests/components/update/test_init.py b/tests/components/update/test_init.py index c03559d76d0..b37abc2263a 100644 --- a/tests/components/update/test_init.py +++ b/tests/components/update/test_init.py @@ -586,7 +586,10 @@ async def test_entity_without_progress_support( events = [] async_track_state_change_event( - hass, "update.update_available", callback(lambda event: events.append(event)) + hass, + "update.update_available", + # pylint: disable-next=unnecessary-lambda + callback(lambda event: events.append(event)), ) await hass.services.async_call( @@ -624,7 +627,10 @@ async def test_entity_without_progress_support_raising( events = [] async_track_state_change_event( - hass, "update.update_available", callback(lambda event: events.append(event)) + hass, + "update.update_available", + # pylint: disable-next=unnecessary-lambda + callback(lambda event: events.append(event)), ) with ( diff --git a/tests/helpers/test_event.py b/tests/helpers/test_event.py index a4cffe9a732..edce36218e8 100644 --- a/tests/helpers/test_event.py +++ b/tests/helpers/test_event.py @@ -61,7 +61,10 @@ async def test_track_point_in_time(hass: HomeAssistant) -> None: runs = [] async_track_point_in_utc_time( - hass, callback(lambda x: runs.append(x)), birthday_paulus + hass, + # pylint: disable-next=unnecessary-lambda + callback(lambda x: runs.append(x)), + birthday_paulus, ) async_fire_time_changed(hass, before_birthday) @@ -78,7 +81,10 @@ async def test_track_point_in_time(hass: HomeAssistant) -> None: assert len(runs) == 1 async_track_point_in_utc_time( - hass, callback(lambda x: runs.append(x)), birthday_paulus + hass, + # pylint: disable-next=unnecessary-lambda + callback(lambda x: runs.append(x)), + birthday_paulus, ) async_fire_time_changed(hass, after_birthday) @@ -86,7 +92,10 @@ async def test_track_point_in_time(hass: HomeAssistant) -> None: assert len(runs) == 2 unsub = async_track_point_in_time( - hass, callback(lambda x: runs.append(x)), birthday_paulus + hass, + # pylint: disable-next=unnecessary-lambda + callback(lambda x: runs.append(x)), + birthday_paulus, ) unsub() @@ -107,6 +116,7 @@ async def test_track_point_in_time_drift_rearm(hass: HomeAssistant) -> None: async_track_point_in_utc_time( hass, + # pylint: disable-next=unnecessary-lambda callback(lambda x: specific_runs.append(x)), time_that_will_not_match_right_away, ) @@ -3546,7 +3556,10 @@ async def test_track_time_interval(hass: HomeAssistant) -> None: utc_now = dt_util.utcnow() unsub = async_track_time_interval( - hass, callback(lambda x: specific_runs.append(x)), timedelta(seconds=10) + hass, + # pylint: disable-next=unnecessary-lambda + callback(lambda x: specific_runs.append(x)), + timedelta(seconds=10), ) async_fire_time_changed(hass, utc_now + timedelta(seconds=5)) @@ -3578,6 +3591,7 @@ async def test_track_time_interval_name(hass: HomeAssistant) -> None: unique_string = "xZ13" unsub = async_track_time_interval( hass, + # pylint: disable-next=unnecessary-lambda callback(lambda x: specific_runs.append(x)), timedelta(seconds=10), name=unique_string, @@ -3808,12 +3822,20 @@ async def test_async_track_time_change( ) freezer.move_to(time_that_will_not_match_right_away) - unsub = async_track_time_change(hass, callback(lambda x: none_runs.append(x))) + unsub = async_track_time_change( + hass, + # pylint: disable-next=unnecessary-lambda + callback(lambda x: none_runs.append(x)), + ) unsub_utc = async_track_utc_time_change( - hass, callback(lambda x: specific_runs.append(x)), second=[0, 30] + hass, + # pylint: disable-next=unnecessary-lambda + callback(lambda x: specific_runs.append(x)), + second=[0, 30], ) unsub_wildcard = async_track_time_change( hass, + # pylint: disable-next=unnecessary-lambda callback(lambda x: wildcard_runs.append(x)), second="*", minute="*", @@ -3872,7 +3894,11 @@ async def test_periodic_task_minute( freezer.move_to(time_that_will_not_match_right_away) unsub = async_track_utc_time_change( - hass, callback(lambda x: specific_runs.append(x)), minute="/5", second=0 + hass, + # pylint: disable-next=unnecessary-lambda + callback(lambda x: specific_runs.append(x)), + minute="/5", + second=0, ) async_fire_time_changed( @@ -3918,6 +3944,7 @@ async def test_periodic_task_hour( unsub = async_track_utc_time_change( hass, + # pylint: disable-next=unnecessary-lambda callback(lambda x: specific_runs.append(x)), hour="/2", minute=0, @@ -3971,7 +3998,10 @@ async def test_periodic_task_wrong_input(hass: HomeAssistant) -> None: with pytest.raises(ValueError): async_track_utc_time_change( - hass, callback(lambda x: specific_runs.append(x)), hour="/two" + hass, + # pylint: disable-next=unnecessary-lambda + callback(lambda x: specific_runs.append(x)), + hour="/two", ) async_fire_time_changed( @@ -3995,6 +4025,7 @@ async def test_periodic_task_clock_rollback( unsub = async_track_utc_time_change( hass, + # pylint: disable-next=unnecessary-lambda callback(lambda x: specific_runs.append(x)), hour="/2", minute=0, @@ -4064,6 +4095,7 @@ async def test_periodic_task_duplicate_time( unsub = async_track_utc_time_change( hass, + # pylint: disable-next=unnecessary-lambda callback(lambda x: specific_runs.append(x)), hour="/2", minute=0, @@ -4109,6 +4141,7 @@ async def test_periodic_task_entering_dst( unsub = async_track_time_change( hass, + # pylint: disable-next=unnecessary-lambda callback(lambda x: specific_runs.append(x)), hour=2, minute=30, @@ -4160,6 +4193,7 @@ async def test_periodic_task_entering_dst_2( unsub = async_track_time_change( hass, + # pylint: disable-next=unnecessary-lambda callback(lambda x: specific_runs.append(x)), second=list(range(59)), ) @@ -4210,6 +4244,7 @@ async def test_periodic_task_leaving_dst( unsub = async_track_time_change( hass, + # pylint: disable-next=unnecessary-lambda callback(lambda x: specific_runs.append(x)), hour=2, minute=30, @@ -4285,6 +4320,7 @@ async def test_periodic_task_leaving_dst_2( unsub = async_track_time_change( hass, + # pylint: disable-next=unnecessary-lambda callback(lambda x: specific_runs.append(x)), minute=30, second=0, From 384fa53cc0a1b08ecdb6570410be8496bc287380 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 13 Jun 2024 16:59:05 +0200 Subject: [PATCH 0643/1445] Fix dangerous-default-value warnings in panasonic_viera tests (#119602) --- tests/components/panasonic_viera/conftest.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/components/panasonic_viera/conftest.py b/tests/components/panasonic_viera/conftest.py index e30c0f41e92..8871da106e3 100644 --- a/tests/components/panasonic_viera/conftest.py +++ b/tests/components/panasonic_viera/conftest.py @@ -21,6 +21,7 @@ from homeassistant.components.panasonic_viera.const import ( ) from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT from homeassistant.core import HomeAssistant +from homeassistant.helpers.typing import UNDEFINED, UndefinedType from tests.common import MockConfigEntry @@ -54,7 +55,7 @@ def get_mock_remote( encrypted=False, app_id=None, encryption_key=None, - device_info=MOCK_DEVICE_INFO, + device_info: UndefinedType | None = UNDEFINED, ): """Return a mock remote.""" mock_remote = Mock() @@ -78,7 +79,9 @@ def get_mock_remote( mock_remote.authorize_pin_code = authorize_pin_code - mock_remote.get_device_info = Mock(return_value=device_info) + mock_remote.get_device_info = Mock( + return_value=MOCK_DEVICE_INFO if device_info is UNDEFINED else device_info + ) mock_remote.send_key = Mock() From 49b28cca621b12fd7f3c636241f96a53719f1632 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 13 Jun 2024 16:59:40 +0200 Subject: [PATCH 0644/1445] Fix consider-using-with warnings in core tests (#119606) --- tests/test_block_async_io.py | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/tests/test_block_async_io.py b/tests/test_block_async_io.py index d823f8c6912..20089cf15b9 100644 --- a/tests/test_block_async_io.py +++ b/tests/test_block_async_io.py @@ -212,8 +212,11 @@ async def test_protect_loop_importlib_import_module_in_integration( async def test_protect_loop_open(caplog: pytest.LogCaptureFixture) -> None: """Test open of a file in /proc is not reported.""" block_async_io.enable() - with contextlib.suppress(FileNotFoundError): - open("/proc/does_not_exist", encoding="utf8").close() + with ( + contextlib.suppress(FileNotFoundError), + open("/proc/does_not_exist", encoding="utf8"), + ): + pass assert "Detected blocking call to open with args" not in caplog.text @@ -221,8 +224,11 @@ async def test_protect_open(caplog: pytest.LogCaptureFixture) -> None: """Test opening a file in the event loop logs.""" with patch.object(block_async_io, "_IN_TESTS", False): block_async_io.enable() - with contextlib.suppress(FileNotFoundError): - open("/config/data_not_exist", encoding="utf8").close() + with ( + contextlib.suppress(FileNotFoundError), + open("/config/data_not_exist", encoding="utf8"), + ): + pass assert "Detected blocking call to open with args" in caplog.text @@ -250,8 +256,8 @@ async def test_protect_open_path(path: Any, caplog: pytest.LogCaptureFixture) -> """Test opening a file by path in the event loop logs.""" with patch.object(block_async_io, "_IN_TESTS", False): block_async_io.enable() - with contextlib.suppress(FileNotFoundError): - open(path, encoding="utf8").close() + with contextlib.suppress(FileNotFoundError), open(path, encoding="utf8"): + pass assert "Detected blocking call to open with args" in caplog.text @@ -331,7 +337,10 @@ async def test_open_calls_ignored_in_tests(caplog: pytest.LogCaptureFixture) -> """Test opening a file in tests is ignored.""" assert block_async_io._IN_TESTS block_async_io.enable() - with contextlib.suppress(FileNotFoundError): - open("/config/data_not_exist", encoding="utf8").close() + with ( + contextlib.suppress(FileNotFoundError), + open("/config/data_not_exist", encoding="utf8"), + ): + pass assert "Detected blocking call to open with args" not in caplog.text From 97e19cb61cd438e00f88753d19358c039612c04d Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 13 Jun 2024 17:00:18 +0200 Subject: [PATCH 0645/1445] Fix dangerous-default-value warnings in cloudflare tests (#119598) --- tests/components/cloudflare/__init__.py | 26 +++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/tests/components/cloudflare/__init__.py b/tests/components/cloudflare/__init__.py index ce9c6844f5a..5e1529a9da8 100644 --- a/tests/components/cloudflare/__init__.py +++ b/tests/components/cloudflare/__init__.py @@ -2,12 +2,15 @@ from __future__ import annotations +from typing import Any from unittest.mock import AsyncMock, patch import pycfdns from homeassistant.components.cloudflare.const import CONF_RECORDS, DOMAIN from homeassistant.const import CONF_API_TOKEN, CONF_ZONE +from homeassistant.core import HomeAssistant +from homeassistant.helpers.typing import UNDEFINED, UndefinedType from tests.common import MockConfigEntry @@ -54,18 +57,18 @@ MOCK_ZONE_RECORDS: list[pycfdns.RecordModel] = [ async def init_integration( - hass, + hass: HomeAssistant, *, - data: dict = ENTRY_CONFIG, - options: dict = ENTRY_OPTIONS, + data: dict[str, Any] | UndefinedType = UNDEFINED, + options: dict[str, Any] | UndefinedType = UNDEFINED, unique_id: str = MOCK_ZONE["name"], skip_setup: bool = False, ) -> MockConfigEntry: """Set up the Cloudflare integration in Home Assistant.""" entry = MockConfigEntry( domain=DOMAIN, - data=data, - options=options, + data=ENTRY_CONFIG if data is UNDEFINED else data, + options=ENTRY_OPTIONS if options is UNDEFINED else options, unique_id=unique_id, ) entry.add_to_hass(hass) @@ -77,11 +80,18 @@ async def init_integration( return entry -def _get_mock_client(zone: str = MOCK_ZONE, records: list = MOCK_ZONE_RECORDS): +def _get_mock_client( + zone: pycfdns.ZoneModel | UndefinedType = UNDEFINED, + records: list[pycfdns.RecordModel] | UndefinedType = UNDEFINED, +): client: pycfdns.Client = AsyncMock() - client.list_zones = AsyncMock(return_value=[zone]) - client.list_dns_records = AsyncMock(return_value=records) + client.list_zones = AsyncMock( + return_value=[MOCK_ZONE if zone is UNDEFINED else zone] + ) + client.list_dns_records = AsyncMock( + return_value=MOCK_ZONE_RECORDS if records is UNDEFINED else records + ) client.update_dns_record = AsyncMock(return_value=None) return client From 1440ad26c800a34f686ceb5d704a689ac667c5fd Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 13 Jun 2024 17:01:52 +0200 Subject: [PATCH 0646/1445] Fix dangerous-default-value warnings in plex tests (#119603) --- tests/components/plex/helpers.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/components/plex/helpers.py b/tests/components/plex/helpers.py index 00d0a4539c1..4828b972d9d 100644 --- a/tests/components/plex/helpers.py +++ b/tests/components/plex/helpers.py @@ -1,9 +1,11 @@ """Helper methods for Plex tests.""" from datetime import timedelta +from typing import Any from plexwebsocket import SIGNAL_CONNECTION_STATE, STATE_CONNECTED +from homeassistant.helpers.typing import UNDEFINED, UndefinedType import homeassistant.util.dt as dt_util from tests.common import async_fire_time_changed @@ -27,10 +29,14 @@ def websocket_connected(mock_websocket): callback(SIGNAL_CONNECTION_STATE, STATE_CONNECTED, None) -def trigger_plex_update(mock_websocket, msgtype="playing", payload=UPDATE_PAYLOAD): +def trigger_plex_update( + mock_websocket, + msgtype="playing", + payload: dict[str, Any] | UndefinedType = UNDEFINED, +): """Call the websocket callback method with a Plex update.""" callback = mock_websocket.call_args[0][1] - callback(msgtype, payload, None) + callback(msgtype, UPDATE_PAYLOAD if payload is UNDEFINED else payload, None) async def wait_for_debouncer(hass): From 382eb1e3b20645b985a3b3bd6659bc7519d034cf Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 13 Jun 2024 17:02:26 +0200 Subject: [PATCH 0647/1445] Fix dangerous-default-value warnings in rituals_perfume_genie tests (#119590) --- tests/components/rituals_perfume_genie/common.py | 2 +- tests/components/rituals_perfume_genie/test_init.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/components/rituals_perfume_genie/common.py b/tests/components/rituals_perfume_genie/common.py index f2a54ca5def..044582c5735 100644 --- a/tests/components/rituals_perfume_genie/common.py +++ b/tests/components/rituals_perfume_genie/common.py @@ -85,7 +85,7 @@ def mock_diffuser_v2_no_battery_no_cartridge() -> MagicMock: async def init_integration( hass: HomeAssistant, mock_config_entry: MockConfigEntry, - mock_diffusers: list[MagicMock] = [mock_diffuser(hublot="lot123")], + mock_diffusers: list[MagicMock], ) -> None: """Initialize the Rituals Perfume Genie integration with the given Config Entry and Diffuser list.""" mock_config_entry.add_to_hass(hass) diff --git a/tests/components/rituals_perfume_genie/test_init.py b/tests/components/rituals_perfume_genie/test_init.py index d1001d1ad93..435e762a646 100644 --- a/tests/components/rituals_perfume_genie/test_init.py +++ b/tests/components/rituals_perfume_genie/test_init.py @@ -12,6 +12,7 @@ from homeassistant.helpers import entity_registry as er from .common import ( init_integration, mock_config_entry, + mock_diffuser, mock_diffuser_v1_battery_cartridge, ) @@ -31,7 +32,7 @@ async def test_config_entry_not_ready(hass: HomeAssistant) -> None: async def test_config_entry_unload(hass: HomeAssistant) -> None: """Test the Rituals Perfume Genie configuration entry setup and unloading.""" config_entry = mock_config_entry(unique_id="id_123_unload") - await init_integration(hass, config_entry) + await init_integration(hass, config_entry, [mock_diffuser(hublot="lot123")]) await hass.config_entries.async_unload(config_entry.entry_id) await hass.async_block_till_done() From 6901c24ab7b1d5913f3b4a92f17afa3aaa189a8c Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 13 Jun 2024 17:03:16 +0200 Subject: [PATCH 0648/1445] Fix dangerous-default-value warnings in aussie broadband tests (#119596) --- tests/components/aussie_broadband/common.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/tests/components/aussie_broadband/common.py b/tests/components/aussie_broadband/common.py index 1c992d116d1..a2bc79a42a6 100644 --- a/tests/components/aussie_broadband/common.py +++ b/tests/components/aussie_broadband/common.py @@ -1,12 +1,15 @@ """Aussie Broadband common helpers for tests.""" +from typing import Any from unittest.mock import patch from homeassistant.components.aussie_broadband.const import ( CONF_SERVICES, DOMAIN as AUSSIE_BROADBAND_DOMAIN, ) -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers.typing import UNDEFINED, UndefinedType from tests.common import MockConfigEntry @@ -38,7 +41,11 @@ FAKE_DATA = { async def setup_platform( - hass, platforms=[], side_effect=None, usage={}, usage_effect=None + hass: HomeAssistant, + platforms: list[Platform] | UndefinedType = UNDEFINED, + side_effect=None, + usage: dict[str, Any] | UndefinedType = UNDEFINED, + usage_effect=None, ): """Set up the Aussie Broadband platform.""" mock_entry = MockConfigEntry( @@ -51,7 +58,10 @@ async def setup_platform( mock_entry.add_to_hass(hass) with ( - patch("homeassistant.components.aussie_broadband.PLATFORMS", platforms), + patch( + "homeassistant.components.aussie_broadband.PLATFORMS", + [] if platforms is UNDEFINED else platforms, + ), patch("aussiebb.asyncio.AussieBB.__init__", return_value=None), patch( "aussiebb.asyncio.AussieBB.login", @@ -65,7 +75,7 @@ async def setup_platform( ), patch( "aussiebb.asyncio.AussieBB.get_usage", - return_value=usage, + return_value={} if usage is UNDEFINED else usage, side_effect=usage_effect, ), ): From 835d422a906f529f3dd98cb7bc3706624de50fd2 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 13 Jun 2024 17:06:12 +0200 Subject: [PATCH 0649/1445] Fix dangerous-default-value warnings in control4 tests (#119592) --- tests/components/control4/test_config_flow.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/tests/components/control4/test_config_flow.py b/tests/components/control4/test_config_flow.py index d1faf2da6c6..9a1b392f61c 100644 --- a/tests/components/control4/test_config_flow.py +++ b/tests/components/control4/test_config_flow.py @@ -20,25 +20,23 @@ from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry -def _get_mock_c4_account( - getAccountControllers={ +def _get_mock_c4_account(): + c4_account_mock = AsyncMock(C4Account) + + c4_account_mock.getAccountControllers.return_value = { "controllerCommonName": "control4_model_00AA00AA00AA", "href": "https://apis.control4.com/account/v3/rest/accounts/000000", "name": "Name", - }, - getDirectorBearerToken={"token": "token"}, -): - c4_account_mock = AsyncMock(C4Account) + } - c4_account_mock.getAccountControllers.return_value = getAccountControllers - c4_account_mock.getDirectorBearerToken.return_value = getDirectorBearerToken + c4_account_mock.getDirectorBearerToken.return_value = {"token": "token"} return c4_account_mock -def _get_mock_c4_director(getAllItemInfo={}): +def _get_mock_c4_director(): c4_director_mock = AsyncMock(C4Director) - c4_director_mock.getAllItemInfo.return_value = getAllItemInfo + c4_director_mock.getAllItemInfo.return_value = {} return c4_director_mock From 75e0aee8fcf0c2fba2e62c3b6b11f392faabda5a Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 13 Jun 2024 17:08:40 +0200 Subject: [PATCH 0650/1445] Fix dangerous-default-value warnings in homematicip_cloud tests (#119583) --- tests/components/homematicip_cloud/helper.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/components/homematicip_cloud/helper.py b/tests/components/homematicip_cloud/helper.py index 4632b9107af..f82880d3fa8 100644 --- a/tests/components/homematicip_cloud/helper.py +++ b/tests/components/homematicip_cloud/helper.py @@ -84,7 +84,7 @@ class HomeFactory: self.hmip_config_entry = hmip_config_entry async def async_get_mock_hap( - self, test_devices=[], test_groups=[] + self, test_devices=None, test_groups=None ) -> HomematicipHAP: """Create a mocked homematic access point.""" home_name = self.hmip_config_entry.data["name"] @@ -130,7 +130,9 @@ class HomeTemplate(Home): _typeGroupMap = TYPE_GROUP_MAP _typeSecurityEventMap = TYPE_SECURITY_EVENT_MAP - def __init__(self, connection=None, home_name="", test_devices=[], test_groups=[]): + def __init__( + self, connection=None, home_name="", test_devices=None, test_groups=None + ): """Init template with connection.""" super().__init__(connection=connection) self.name = home_name From ed52ff3076108183a6f8e3f9a028cdea6f65e242 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 13 Jun 2024 17:09:26 +0200 Subject: [PATCH 0651/1445] Fix dangerous-default-value warnings in ezviz tests (#119589) --- tests/components/ezviz/__init__.py | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/tests/components/ezviz/__init__.py b/tests/components/ezviz/__init__.py index 7872cf37b68..9fc297be099 100644 --- a/tests/components/ezviz/__init__.py +++ b/tests/components/ezviz/__init__.py @@ -90,19 +90,12 @@ def _patch_async_setup_entry(return_value=True): ) -async def init_integration( - hass: HomeAssistant, - *, - data: dict = ENTRY_CONFIG, - options: dict = ENTRY_OPTIONS, - skip_entry_setup: bool = False, -) -> MockConfigEntry: +async def init_integration(hass: HomeAssistant) -> MockConfigEntry: """Set up the EZVIZ integration in Home Assistant.""" - entry = MockConfigEntry(domain=DOMAIN, data=data, options=options) + entry = MockConfigEntry(domain=DOMAIN, data=ENTRY_CONFIG, options=ENTRY_OPTIONS) entry.add_to_hass(hass) - if not skip_entry_setup: - await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() return entry From a80a372c1c86078f754fc939376b73b001fcc536 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 13 Jun 2024 17:10:00 +0200 Subject: [PATCH 0652/1445] Fix dangerous-default-value warnings in nzbget tests (#119580) --- tests/components/nzbget/__init__.py | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/tests/components/nzbget/__init__.py b/tests/components/nzbget/__init__.py index d3216b62ef3..d8fa2f87233 100644 --- a/tests/components/nzbget/__init__.py +++ b/tests/components/nzbget/__init__.py @@ -59,14 +59,9 @@ MOCK_HISTORY = [ ] -async def init_integration( - hass, - *, - data: dict = ENTRY_CONFIG, - options: dict = ENTRY_OPTIONS, -) -> MockConfigEntry: +async def init_integration(hass) -> MockConfigEntry: """Set up the NZBGet integration in Home Assistant.""" - entry = MockConfigEntry(domain=DOMAIN, data=data, options=options) + entry = MockConfigEntry(domain=DOMAIN, data=ENTRY_CONFIG, options=ENTRY_OPTIONS) entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) @@ -82,17 +77,17 @@ def _patch_async_setup_entry(return_value=True): ) -def _patch_history(return_value=MOCK_HISTORY): +def _patch_history(): return patch( "homeassistant.components.nzbget.coordinator.NZBGetAPI.history", - return_value=return_value, + return_value=MOCK_HISTORY, ) -def _patch_status(return_value=MOCK_STATUS): +def _patch_status(): return patch( "homeassistant.components.nzbget.coordinator.NZBGetAPI.status", - return_value=return_value, + return_value=MOCK_STATUS, ) From 8e1103050cdc1476179d469827eea40c4f7820ef Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 13 Jun 2024 17:10:37 +0200 Subject: [PATCH 0653/1445] Fix dangerous-default-value warnings in core tests (#119568) --- tests/common.py | 4 ++-- tests/test_util/aiohttp.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/common.py b/tests/common.py index ec7b5ca46b7..24fb6cf458f 100644 --- a/tests/common.py +++ b/tests/common.py @@ -693,9 +693,9 @@ def mock_device_registry( class MockGroup(auth_models.Group): """Mock a group in Home Assistant.""" - def __init__(self, id=None, name="Mock Group", policy=system_policies.ADMIN_POLICY): + def __init__(self, id=None, name="Mock Group"): """Mock a group.""" - kwargs = {"name": name, "policy": policy} + kwargs = {"name": name, "policy": system_policies.ADMIN_POLICY} if id is not None: kwargs["id"] = id diff --git a/tests/test_util/aiohttp.py b/tests/test_util/aiohttp.py index 742b111143f..b4b8cfa4b6d 100644 --- a/tests/test_util/aiohttp.py +++ b/tests/test_util/aiohttp.py @@ -54,7 +54,7 @@ class AiohttpClientMocker: content=None, json=None, params=None, - headers={}, + headers=None, exc=None, cookies=None, side_effect=None, From 6d31991021b8f637dcec878e73d4c8deb4852ddb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 13 Jun 2024 11:44:29 -0500 Subject: [PATCH 0654/1445] Reduce duplicate code in unifiprotect (#119624) --- .../components/unifiprotect/binary_sensor.py | 11 ++----- .../components/unifiprotect/button.py | 6 ++-- .../components/unifiprotect/camera.py | 15 +++------- homeassistant/components/unifiprotect/data.py | 24 +++++++++++++-- .../components/unifiprotect/light.py | 8 +---- homeassistant/components/unifiprotect/lock.py | 7 +---- .../components/unifiprotect/media_player.py | 21 ++++--------- .../components/unifiprotect/media_source.py | 5 ++-- .../components/unifiprotect/number.py | 8 +---- .../components/unifiprotect/select.py | 9 ++---- .../components/unifiprotect/sensor.py | 30 +++++++------------ .../components/unifiprotect/switch.py | 8 +---- homeassistant/components/unifiprotect/text.py | 8 +---- 13 files changed, 56 insertions(+), 104 deletions(-) diff --git a/homeassistant/components/unifiprotect/binary_sensor.py b/homeassistant/components/unifiprotect/binary_sensor.py index 396894c997a..349b4f9b266 100644 --- a/homeassistant/components/unifiprotect/binary_sensor.py +++ b/homeassistant/components/unifiprotect/binary_sensor.py @@ -26,10 +26,8 @@ from homeassistant.components.binary_sensor import ( ) from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DISPATCH_ADOPT from .data import ProtectData, UFPConfigEntry from .entity import ( BaseProtectEntity, @@ -39,7 +37,6 @@ from .entity import ( async_all_device_entities, ) from .models import PermRequired, ProtectEventMixin, ProtectRequiredKeysMixin -from .utils import async_dispatch_id as _ufpd _LOGGER = logging.getLogger(__name__) _KEY_DOOR = "door" @@ -641,9 +638,7 @@ async def async_setup_entry( entities += _async_event_entities(data, ufp_device=device) async_add_entities(entities) - entry.async_on_unload( - async_dispatcher_connect(hass, _ufpd(entry, DISPATCH_ADOPT), _add_new_device) - ) + data.async_subscribe_adopt(_add_new_device) entities = async_all_device_entities( data, ProtectDeviceBinarySensor, model_descriptions=_MODEL_DESCRIPTIONS @@ -660,9 +655,7 @@ def _async_event_entities( ufp_device: ProtectAdoptableDeviceModel | None = None, ) -> list[ProtectDeviceEntity]: entities: list[ProtectDeviceEntity] = [] - devices = ( - data.get_by_types({ModelType.CAMERA}) if ufp_device is None else [ufp_device] - ) + devices = data.get_cameras() if ufp_device is None else [ufp_device] for device in devices: for description in EVENT_SENSORS: if not description.has_required(device): diff --git a/homeassistant/components/unifiprotect/button.py b/homeassistant/components/unifiprotect/button.py index a1b1ec21f6a..265367a9272 100644 --- a/homeassistant/components/unifiprotect/button.py +++ b/homeassistant/components/unifiprotect/button.py @@ -20,7 +20,7 @@ from homeassistant.helpers import entity_registry as er from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DEVICES_THAT_ADOPT, DISPATCH_ADD, DISPATCH_ADOPT, DOMAIN +from .const import DEVICES_THAT_ADOPT, DISPATCH_ADD, DOMAIN from .data import ProtectData, UFPConfigEntry from .entity import ProtectDeviceEntity, async_all_device_entities from .models import PermRequired, ProtectRequiredKeysMixin, ProtectSetableKeysMixin, T @@ -147,9 +147,7 @@ async def async_setup_entry( ) ) - entry.async_on_unload( - async_dispatcher_connect(hass, _ufpd(entry, DISPATCH_ADOPT), _add_new_device) - ) + data.async_subscribe_adopt(_add_new_device) entry.async_on_unload( async_dispatcher_connect( hass, _ufpd(entry, DISPATCH_ADD), _async_add_unadopted_device diff --git a/homeassistant/components/unifiprotect/camera.py b/homeassistant/components/unifiprotect/camera.py index dc41310ab3f..5f077d3a62e 100644 --- a/homeassistant/components/unifiprotect/camera.py +++ b/homeassistant/components/unifiprotect/camera.py @@ -3,13 +3,12 @@ from __future__ import annotations import logging -from typing import Any, cast +from typing import Any from typing_extensions import Generator from uiprotect.data import ( Camera as UFPCamera, CameraChannel, - ModelType, ProtectAdoptableDeviceModel, ProtectModelWithId, StateType, @@ -28,7 +27,6 @@ from .const import ( ATTR_FPS, ATTR_HEIGHT, ATTR_WIDTH, - DISPATCH_ADOPT, DISPATCH_CHANNELS, DOMAIN, ) @@ -73,11 +71,8 @@ def _get_camera_channels( ) -> Generator[tuple[UFPCamera, CameraChannel, bool]]: """Get all the camera channels.""" - devices = ( - data.get_by_types({ModelType.CAMERA}) if ufp_device is None else [ufp_device] - ) - for camera in devices: - camera = cast(UFPCamera, camera) + cameras = data.get_cameras() if ufp_device is None else [ufp_device] + for camera in cameras: if not camera.channels: if ufp_device is None: # only warn on startup @@ -157,9 +152,7 @@ async def async_setup_entry( return async_add_entities(_async_camera_entities(hass, entry, data, ufp_device=device)) - entry.async_on_unload( - async_dispatcher_connect(hass, _ufpd(entry, DISPATCH_ADOPT), _add_new_device) - ) + data.async_subscribe_adopt(_add_new_device) entry.async_on_unload( async_dispatcher_connect(hass, _ufpd(entry, DISPATCH_CHANNELS), _add_new_device) ) diff --git a/homeassistant/components/unifiprotect/data.py b/homeassistant/components/unifiprotect/data.py index 4e63ff01bc7..59e98cfb9a0 100644 --- a/homeassistant/components/unifiprotect/data.py +++ b/homeassistant/components/unifiprotect/data.py @@ -27,7 +27,10 @@ from uiprotect.utils import log_event from homeassistant.config_entries import ConfigEntry from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.helpers import device_registry as dr -from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) from homeassistant.helpers.event import async_track_time_interval from .const import ( @@ -81,6 +84,7 @@ class ProtectData: self.last_update_success = False self.api = protect + self._adopt_signal = _ufpd(self._entry, DISPATCH_ADOPT) @property def disable_stream(self) -> bool: @@ -92,6 +96,15 @@ class ProtectData: """Max number of events to load at once.""" return self._entry.options.get(CONF_MAX_MEDIA, DEFAULT_MAX_MEDIA) + @callback + def async_subscribe_adopt( + self, add_callback: Callable[[ProtectAdoptableDeviceModel], None] + ) -> None: + """Add an callback for on device adopt.""" + self._entry.async_on_unload( + async_dispatcher_connect(self._hass, self._adopt_signal, add_callback) + ) + def get_by_types( self, device_types: Iterable[ModelType], ignore_unadopted: bool = True ) -> Generator[ProtectAdoptableDeviceModel]: @@ -105,6 +118,12 @@ class ProtectData: continue yield device + def get_cameras(self, ignore_unadopted: bool = True) -> Generator[Camera]: + """Get all cameras.""" + return cast( + Generator[Camera], self.get_by_types({ModelType.CAMERA}, ignore_unadopted) + ) + async def async_setup(self) -> None: """Subscribe and do the refresh.""" self._unsub_websocket = self.api.subscribe_websocket( @@ -206,8 +225,7 @@ class ProtectData: "Doorbell messages updated. Updating devices with LCD screens" ) self.api.bootstrap.nvr.update_all_messages() - for camera in self.get_by_types({ModelType.CAMERA}): - camera = cast(Camera, camera) + for camera in self.get_cameras(): if camera.feature_flags.has_lcd_screen: self._async_signal_device_update(camera) diff --git a/homeassistant/components/unifiprotect/light.py b/homeassistant/components/unifiprotect/light.py index e119a4a59d5..e8a51c357a0 100644 --- a/homeassistant/components/unifiprotect/light.py +++ b/homeassistant/components/unifiprotect/light.py @@ -14,13 +14,10 @@ from uiprotect.data import ( from homeassistant.components.light import ATTR_BRIGHTNESS, ColorMode, LightEntity from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DISPATCH_ADOPT from .data import UFPConfigEntry from .entity import ProtectDeviceEntity -from .utils import async_dispatch_id as _ufpd _LOGGER = logging.getLogger(__name__) @@ -40,10 +37,7 @@ async def async_setup_entry( ): async_add_entities([ProtectLight(data, device)]) - entry.async_on_unload( - async_dispatcher_connect(hass, _ufpd(entry, DISPATCH_ADOPT), _add_new_device) - ) - + data.async_subscribe_adopt(_add_new_device) async_add_entities( ProtectLight(data, device) for device in data.get_by_types({ModelType.LIGHT}) diff --git a/homeassistant/components/unifiprotect/lock.py b/homeassistant/components/unifiprotect/lock.py index 7ffa3c6bfc5..4f5dfe43ce2 100644 --- a/homeassistant/components/unifiprotect/lock.py +++ b/homeassistant/components/unifiprotect/lock.py @@ -15,13 +15,10 @@ from uiprotect.data import ( from homeassistant.components.lock import LockEntity, LockEntityDescription from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DISPATCH_ADOPT from .data import ProtectData, UFPConfigEntry from .entity import ProtectDeviceEntity -from .utils import async_dispatch_id as _ufpd _LOGGER = logging.getLogger(__name__) @@ -39,9 +36,7 @@ async def async_setup_entry( if isinstance(device, Doorlock): async_add_entities([ProtectLock(data, device)]) - entry.async_on_unload( - async_dispatcher_connect(hass, _ufpd(entry, DISPATCH_ADOPT), _add_new_device) - ) + data.async_subscribe_adopt(_add_new_device) async_add_entities( ProtectLock(data, cast(Doorlock, device)) diff --git a/homeassistant/components/unifiprotect/media_player.py b/homeassistant/components/unifiprotect/media_player.py index f3761b5c18a..55a85155d89 100644 --- a/homeassistant/components/unifiprotect/media_player.py +++ b/homeassistant/components/unifiprotect/media_player.py @@ -3,11 +3,10 @@ from __future__ import annotations import logging -from typing import Any, cast +from typing import Any from uiprotect.data import ( Camera, - ModelType, ProtectAdoptableDeviceModel, ProtectModelWithId, StateType, @@ -27,13 +26,10 @@ from homeassistant.components.media_player import ( ) from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DISPATCH_ADOPT from .data import ProtectData, UFPConfigEntry from .entity import ProtectDeviceEntity -from .utils import async_dispatch_id as _ufpd _LOGGER = logging.getLogger(__name__) @@ -53,18 +49,13 @@ async def async_setup_entry( ): async_add_entities([ProtectMediaPlayer(data, device)]) - entry.async_on_unload( - async_dispatcher_connect(hass, _ufpd(entry, DISPATCH_ADOPT), _add_new_device) + data.async_subscribe_adopt(_add_new_device) + async_add_entities( + ProtectMediaPlayer(data, device) + for device in data.get_cameras() + if device.has_speaker or device.has_removable_speaker ) - entities = [] - for device in data.get_by_types({ModelType.CAMERA}): - device = cast(Camera, device) - if device.has_speaker or device.has_removable_speaker: - entities.append(ProtectMediaPlayer(data, device)) - - async_add_entities(entities) - class ProtectMediaPlayer(ProtectDeviceEntity, MediaPlayerEntity): """A Ubiquiti UniFi Protect Speaker.""" diff --git a/homeassistant/components/unifiprotect/media_source.py b/homeassistant/components/unifiprotect/media_source.py index 9165b574b2d..d6acb876c94 100644 --- a/homeassistant/components/unifiprotect/media_source.py +++ b/homeassistant/components/unifiprotect/media_source.py @@ -7,7 +7,7 @@ from datetime import date, datetime, timedelta from enum import Enum from typing import Any, NoReturn, cast -from uiprotect.data import Camera, Event, EventType, ModelType, SmartDetectObjectType +from uiprotect.data import Camera, Event, EventType, SmartDetectObjectType from uiprotect.exceptions import NvrError from uiprotect.utils import from_js_time from yarl import URL @@ -848,8 +848,7 @@ class ProtectMediaSource(MediaSource): cameras: list[BrowseMediaSource] = [await self._build_camera(data, "all")] - for camera in data.get_by_types({ModelType.CAMERA}): - camera = cast(Camera, camera) + for camera in data.get_cameras(): if not camera.can_read_media(data.api.bootstrap.auth_user): continue cameras.append(await self._build_camera(data, camera.id)) diff --git a/homeassistant/components/unifiprotect/number.py b/homeassistant/components/unifiprotect/number.py index 08e07536f87..c3d0bb8b6b9 100644 --- a/homeassistant/components/unifiprotect/number.py +++ b/homeassistant/components/unifiprotect/number.py @@ -20,14 +20,11 @@ from uiprotect.data import ( from homeassistant.components.number import NumberEntity, NumberEntityDescription from homeassistant.const import PERCENTAGE, EntityCategory, UnitOfTime from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DISPATCH_ADOPT from .data import ProtectData, UFPConfigEntry from .entity import ProtectDeviceEntity, async_all_device_entities from .models import PermRequired, ProtectRequiredKeysMixin, ProtectSetableKeysMixin, T -from .utils import async_dispatch_id as _ufpd _LOGGER = logging.getLogger(__name__) @@ -245,10 +242,7 @@ async def async_setup_entry( ) ) - entry.async_on_unload( - async_dispatcher_connect(hass, _ufpd(entry, DISPATCH_ADOPT), _add_new_device) - ) - + data.async_subscribe_adopt(_add_new_device) async_add_entities( async_all_device_entities( data, diff --git a/homeassistant/components/unifiprotect/select.py b/homeassistant/components/unifiprotect/select.py index 57e0c806c69..b253e5a9d18 100644 --- a/homeassistant/components/unifiprotect/select.py +++ b/homeassistant/components/unifiprotect/select.py @@ -30,14 +30,13 @@ from uiprotect.data import ( from homeassistant.components.select import SelectEntity, SelectEntityDescription from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DISPATCH_ADOPT, TYPE_EMPTY_VALUE +from .const import TYPE_EMPTY_VALUE from .data import ProtectData, UFPConfigEntry from .entity import ProtectDeviceEntity, async_all_device_entities from .models import PermRequired, ProtectRequiredKeysMixin, ProtectSetableKeysMixin, T -from .utils import async_dispatch_id as _ufpd, async_get_light_motion_current +from .utils import async_get_light_motion_current _LOGGER = logging.getLogger(__name__) _KEY_LIGHT_MOTION = "light_motion" @@ -346,9 +345,7 @@ async def async_setup_entry( ) ) - entry.async_on_unload( - async_dispatcher_connect(hass, _ufpd(entry, DISPATCH_ADOPT), _add_new_device) - ) + data.async_subscribe_adopt(_add_new_device) async_add_entities( async_all_device_entities( data, ProtectSelects, model_descriptions=_MODEL_DESCRIPTIONS diff --git a/homeassistant/components/unifiprotect/sensor.py b/homeassistant/components/unifiprotect/sensor.py index 26103d21bb5..754bf3bc82b 100644 --- a/homeassistant/components/unifiprotect/sensor.py +++ b/homeassistant/components/unifiprotect/sensor.py @@ -6,7 +6,7 @@ from collections.abc import Sequence from dataclasses import dataclass from datetime import datetime import logging -from typing import Any, cast +from typing import Any from uiprotect.data import ( NVR, @@ -37,10 +37,8 @@ from homeassistant.const import ( UnitOfTime, ) from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DISPATCH_ADOPT from .data import ProtectData, UFPConfigEntry from .entity import ( BaseProtectEntity, @@ -50,7 +48,7 @@ from .entity import ( async_all_device_entities, ) from .models import PermRequired, ProtectEventMixin, ProtectRequiredKeysMixin, T -from .utils import async_dispatch_id as _ufpd, async_get_light_motion_current +from .utils import async_get_light_motion_current _LOGGER = logging.getLogger(__name__) OBJECT_TYPE_NONE = "none" @@ -641,10 +639,7 @@ async def async_setup_entry( entities += _async_event_entities(data, ufp_device=device) async_add_entities(entities) - entry.async_on_unload( - async_dispatcher_connect(hass, _ufpd(entry, DISPATCH_ADOPT), _add_new_device) - ) - + data.async_subscribe_adopt(_add_new_device) entities = async_all_device_entities( data, ProtectDeviceSensor, @@ -663,31 +658,28 @@ def _async_event_entities( ufp_device: Camera | None = None, ) -> list[ProtectDeviceEntity]: entities: list[ProtectDeviceEntity] = [] - devices = ( - data.get_by_types({ModelType.CAMERA}) if ufp_device is None else [ufp_device] - ) - for device in devices: - device = cast(Camera, device) + cameras = data.get_cameras() if ufp_device is None else [ufp_device] + for camera in cameras: for description in MOTION_TRIP_SENSORS: - entities.append(ProtectDeviceSensor(data, device, description)) + entities.append(ProtectDeviceSensor(data, camera, description)) _LOGGER.debug( "Adding trip sensor entity %s for %s", description.name, - device.display_name, + camera.display_name, ) - if not device.feature_flags.has_smart_detect: + if not camera.feature_flags.has_smart_detect: continue for event_desc in LICENSE_PLATE_EVENT_SENSORS: - if not event_desc.has_required(device): + if not event_desc.has_required(camera): continue - entities.append(ProtectLicensePlateEventSensor(data, device, event_desc)) + entities.append(ProtectLicensePlateEventSensor(data, camera, event_desc)) _LOGGER.debug( "Adding sensor entity %s for %s", description.name, - device.display_name, + camera.display_name, ) return entities diff --git a/homeassistant/components/unifiprotect/switch.py b/homeassistant/components/unifiprotect/switch.py index 3dd8bc2dbda..8a66b285021 100644 --- a/homeassistant/components/unifiprotect/switch.py +++ b/homeassistant/components/unifiprotect/switch.py @@ -20,15 +20,12 @@ from uiprotect.data import ( from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity -from .const import DISPATCH_ADOPT from .data import ProtectData, UFPConfigEntry from .entity import ProtectDeviceEntity, ProtectNVREntity, async_all_device_entities from .models import PermRequired, ProtectRequiredKeysMixin, ProtectSetableKeysMixin, T -from .utils import async_dispatch_id as _ufpd _LOGGER = logging.getLogger(__name__) ATTR_PREV_MIC = "prev_mic_level" @@ -494,10 +491,7 @@ async def async_setup_entry( ) async_add_entities(entities) - entry.async_on_unload( - async_dispatcher_connect(hass, _ufpd(entry, DISPATCH_ADOPT), _add_new_device) - ) - + data.async_subscribe_adopt(_add_new_device) entities = async_all_device_entities( data, ProtectSwitch, diff --git a/homeassistant/components/unifiprotect/text.py b/homeassistant/components/unifiprotect/text.py index 30c54d4c15c..acd28a31794 100644 --- a/homeassistant/components/unifiprotect/text.py +++ b/homeassistant/components/unifiprotect/text.py @@ -17,14 +17,11 @@ from uiprotect.data import ( from homeassistant.components.text import TextEntity, TextEntityDescription from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DISPATCH_ADOPT from .data import ProtectData, UFPConfigEntry from .entity import ProtectDeviceEntity, async_all_device_entities from .models import PermRequired, ProtectRequiredKeysMixin, ProtectSetableKeysMixin, T -from .utils import async_dispatch_id as _ufpd @dataclass(frozen=True, kw_only=True) @@ -78,10 +75,7 @@ async def async_setup_entry( ) ) - entry.async_on_unload( - async_dispatcher_connect(hass, _ufpd(entry, DISPATCH_ADOPT), _add_new_device) - ) - + data.async_subscribe_adopt(_add_new_device) async_add_entities( async_all_device_entities( data, ProtectDeviceText, model_descriptions=_MODEL_DESCRIPTIONS From fc6dd7ce7dbd5926aacbb85718d01f7dce12361b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 13 Jun 2024 12:28:42 -0500 Subject: [PATCH 0655/1445] Bump uiprotect to 1.2.1 (#119620) * Bump uiprotect to 1.2.0 changelog: https://github.com/uilibs/uiprotect/compare/v1.1.0...v1.2.0 * bump --- homeassistant/components/unifiprotect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index 5c1d252ce48..f7b3a4bde70 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -40,7 +40,7 @@ "integration_type": "hub", "iot_class": "local_push", "loggers": ["uiprotect", "unifi_discovery"], - "requirements": ["uiprotect==1.1.0", "unifi-discovery==1.1.8"], + "requirements": ["uiprotect==1.2.1", "unifi-discovery==1.1.8"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/requirements_all.txt b/requirements_all.txt index 88644b6b602..d7e4dc67dd7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2785,7 +2785,7 @@ twitchAPI==4.0.0 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==1.1.0 +uiprotect==1.2.1 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b84951b56b9..8beb4420fd9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2165,7 +2165,7 @@ twitchAPI==4.0.0 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==1.1.0 +uiprotect==1.2.1 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 From b4a77f834195f7e3e876d67d93470a362a555265 Mon Sep 17 00:00:00 2001 From: Thomas55555 <59625598+Thomas55555@users.noreply.github.com> Date: Thu, 13 Jun 2024 19:58:04 +0200 Subject: [PATCH 0656/1445] Bump aioautomower to 2024.6.0 (#119625) --- homeassistant/components/husqvarna_automower/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/husqvarna_automower/fixtures/mower.json | 5 ++++- .../husqvarna_automower/snapshots/test_diagnostics.ambr | 3 +++ 5 files changed, 10 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/husqvarna_automower/manifest.json b/homeassistant/components/husqvarna_automower/manifest.json index 64cb3d9e92c..1f36d9c8acc 100644 --- a/homeassistant/components/husqvarna_automower/manifest.json +++ b/homeassistant/components/husqvarna_automower/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/husqvarna_automower", "iot_class": "cloud_push", "loggers": ["aioautomower"], - "requirements": ["aioautomower==2024.5.1"] + "requirements": ["aioautomower==2024.6.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index d7e4dc67dd7..40f25b8608d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -201,7 +201,7 @@ aioaseko==0.1.1 aioasuswrt==1.4.0 # homeassistant.components.husqvarna_automower -aioautomower==2024.5.1 +aioautomower==2024.6.0 # homeassistant.components.azure_devops aioazuredevops==2.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8beb4420fd9..586bbf32872 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -180,7 +180,7 @@ aioaseko==0.1.1 aioasuswrt==1.4.0 # homeassistant.components.husqvarna_automower -aioautomower==2024.5.1 +aioautomower==2024.6.0 # homeassistant.components.azure_devops aioazuredevops==2.0.0 diff --git a/tests/components/husqvarna_automower/fixtures/mower.json b/tests/components/husqvarna_automower/fixtures/mower.json index f2be7bfdcb9..a5cae68f47c 100644 --- a/tests/components/husqvarna_automower/fixtures/mower.json +++ b/tests/components/husqvarna_automower/fixtures/mower.json @@ -21,9 +21,12 @@ "mower": { "mode": "MAIN_AREA", "activity": "PARKED_IN_CS", + "inactiveReason": "NONE", "state": "RESTRICTED", + "workAreaId": 123456, "errorCode": 0, - "errorCodeTimestamp": 0 + "errorCodeTimestamp": 0, + "isErrorConfirmable": false }, "calendar": { "tasks": [ diff --git a/tests/components/husqvarna_automower/snapshots/test_diagnostics.ambr b/tests/components/husqvarna_automower/snapshots/test_diagnostics.ambr index 7d2ac04791e..d8cd748c793 100644 --- a/tests/components/husqvarna_automower/snapshots/test_diagnostics.ambr +++ b/tests/components/husqvarna_automower/snapshots/test_diagnostics.ambr @@ -64,8 +64,11 @@ 'error_datetime': None, 'error_datetime_naive': None, 'error_key': None, + 'inactive_reason': 'NONE', + 'is_error_confirmable': False, 'mode': 'MAIN_AREA', 'state': 'RESTRICTED', + 'work_area_id': 123456, }), 'planner': dict({ 'next_start_datetime': '2023-06-05T19:00:00+00:00', From b8851f2f3c4da10a411b8a70082819fb36cadf19 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Thu, 13 Jun 2024 21:27:30 +0200 Subject: [PATCH 0657/1445] Cleanup Reolink firmware update entity (#119239) --- homeassistant/components/reolink/__init__.py | 39 ++++++------ homeassistant/components/reolink/entity.py | 37 ++++------- homeassistant/components/reolink/update.py | 67 ++++++++++++++------ tests/components/reolink/conftest.py | 1 + tests/components/reolink/test_init.py | 29 ++++----- 5 files changed, 94 insertions(+), 79 deletions(-) diff --git a/homeassistant/components/reolink/__init__.py b/homeassistant/components/reolink/__init__.py index 9807739b790..64058caba78 100644 --- a/homeassistant/components/reolink/__init__.py +++ b/homeassistant/components/reolink/__init__.py @@ -6,11 +6,9 @@ import asyncio from dataclasses import dataclass from datetime import timedelta import logging -from typing import Literal from reolink_aio.api import RETRY_ATTEMPTS from reolink_aio.exceptions import CredentialsInvalidError, ReolinkError -from reolink_aio.software_version import NewSoftwareVersion from homeassistant.config_entries import ConfigEntry from homeassistant.const import EVENT_HOMEASSISTANT_STOP, Platform @@ -47,9 +45,7 @@ class ReolinkData: host: ReolinkHost device_coordinator: DataUpdateCoordinator[None] - firmware_coordinator: DataUpdateCoordinator[ - str | Literal[False] | NewSoftwareVersion - ] + firmware_coordinator: DataUpdateCoordinator[None] async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: @@ -93,16 +89,11 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b async with asyncio.timeout(host.api.timeout * (RETRY_ATTEMPTS + 2)): await host.renew() - async def async_check_firmware_update() -> ( - str | Literal[False] | NewSoftwareVersion - ): + async def async_check_firmware_update() -> None: """Check for firmware updates.""" - if not host.api.supported(None, "update"): - return False - async with asyncio.timeout(host.api.timeout * (RETRY_ATTEMPTS + 2)): try: - return await host.api.check_new_firmware() + await host.api.check_new_firmware() except ReolinkError as err: if starting: _LOGGER.debug( @@ -110,7 +101,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b "from %s, possibly internet access is blocked", host.api.nvr_name, ) - return False + return raise UpdateFailed( f"Error checking Reolink firmware update from {host.api.nvr_name}, " @@ -151,13 +142,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b ) cleanup_disconnected_cams(hass, config_entry.entry_id, host) - - # Can be remove in HA 2024.6.0 - entity_reg = er.async_get(hass) - entities = er.async_entries_for_config_entry(entity_reg, config_entry.entry_id) - for entity in entities: - if entity.domain == "light" and entity.unique_id.endswith("ir_lights"): - entity_reg.async_remove(entity.entity_id) + migrate_entity_ids(hass, config_entry.entry_id, host) await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) @@ -234,3 +219,17 @@ def cleanup_disconnected_cams( # clean device registry and associated entities device_reg.async_remove_device(device.id) + + +def migrate_entity_ids( + hass: HomeAssistant, config_entry_id: str, host: ReolinkHost +) -> None: + """Migrate entity IDs if needed.""" + entity_reg = er.async_get(hass) + entities = er.async_entries_for_config_entry(entity_reg, config_entry_id) + for entity in entities: + # Can be remove in HA 2025.1.0 + if entity.domain == "update" and entity.unique_id == host.unique_id: + entity_reg.async_update_entity( + entity.entity_id, new_unique_id=f"{host.unique_id}_firmware" + ) diff --git a/homeassistant/components/reolink/entity.py b/homeassistant/components/reolink/entity.py index 309e5b54fe0..f722944a2fc 100644 --- a/homeassistant/components/reolink/entity.py +++ b/homeassistant/components/reolink/entity.py @@ -34,22 +34,28 @@ class ReolinkHostEntityDescription(EntityDescription): supported: Callable[[Host], bool] = lambda api: True -class ReolinkBaseCoordinatorEntity[_DataT]( - CoordinatorEntity[DataUpdateCoordinator[_DataT]] -): - """Parent class for Reolink entities.""" +class ReolinkHostCoordinatorEntity(CoordinatorEntity[DataUpdateCoordinator[None]]): + """Parent class for entities that control the Reolink NVR itself, without a channel. + + A camera connected directly to HomeAssistant without using a NVR is in the reolink API + basically a NVR with a single channel that has the camera connected to that channel. + """ _attr_has_entity_name = True + entity_description: ReolinkHostEntityDescription | ReolinkChannelEntityDescription def __init__( self, reolink_data: ReolinkData, - coordinator: DataUpdateCoordinator[_DataT], + coordinator: DataUpdateCoordinator[None] | None = None, ) -> None: - """Initialize ReolinkBaseCoordinatorEntity.""" + """Initialize ReolinkHostCoordinatorEntity.""" + if coordinator is None: + coordinator = reolink_data.device_coordinator super().__init__(coordinator) self._host = reolink_data.host + self._attr_unique_id = f"{self._host.unique_id}_{self.entity_description.key}" http_s = "https" if self._host.api.use_https else "http" self._conf_url = f"{http_s}://{self._host.api.host}:{self._host.api.port}" @@ -70,22 +76,6 @@ class ReolinkBaseCoordinatorEntity[_DataT]( """Return True if entity is available.""" return self._host.api.session_active and super().available - -class ReolinkHostCoordinatorEntity(ReolinkBaseCoordinatorEntity[None]): - """Parent class for entities that control the Reolink NVR itself, without a channel. - - A camera connected directly to HomeAssistant without using a NVR is in the reolink API - basically a NVR with a single channel that has the camera connected to that channel. - """ - - entity_description: ReolinkHostEntityDescription | ReolinkChannelEntityDescription - - def __init__(self, reolink_data: ReolinkData) -> None: - """Initialize ReolinkHostCoordinatorEntity.""" - super().__init__(reolink_data, reolink_data.device_coordinator) - - self._attr_unique_id = f"{self._host.unique_id}_{self.entity_description.key}" - async def async_added_to_hass(self) -> None: """Entity created.""" await super().async_added_to_hass() @@ -116,9 +106,10 @@ class ReolinkChannelCoordinatorEntity(ReolinkHostCoordinatorEntity): self, reolink_data: ReolinkData, channel: int, + coordinator: DataUpdateCoordinator[None] | None = None, ) -> None: """Initialize ReolinkChannelCoordinatorEntity for a hardware camera connected to a channel of the NVR.""" - super().__init__(reolink_data) + super().__init__(reolink_data, coordinator) self._channel = channel self._attr_unique_id = ( diff --git a/homeassistant/components/reolink/update.py b/homeassistant/components/reolink/update.py index 41933ae2efc..2adbd225cef 100644 --- a/homeassistant/components/reolink/update.py +++ b/homeassistant/components/reolink/update.py @@ -2,9 +2,9 @@ from __future__ import annotations +from dataclasses import dataclass from datetime import datetime -import logging -from typing import Any, Literal +from typing import Any from reolink_aio.exceptions import ReolinkError from reolink_aio.software_version import NewSoftwareVersion @@ -12,6 +12,7 @@ from reolink_aio.software_version import NewSoftwareVersion from homeassistant.components.update import ( UpdateDeviceClass, UpdateEntity, + UpdateEntityDescription, UpdateEntityFeature, ) from homeassistant.config_entries import ConfigEntry @@ -22,13 +23,28 @@ from homeassistant.helpers.event import async_call_later from . import ReolinkData from .const import DOMAIN -from .entity import ReolinkBaseCoordinatorEntity - -LOGGER = logging.getLogger(__name__) +from .entity import ReolinkHostCoordinatorEntity, ReolinkHostEntityDescription POLL_AFTER_INSTALL = 120 +@dataclass(frozen=True, kw_only=True) +class ReolinkHostUpdateEntityDescription( + UpdateEntityDescription, + ReolinkHostEntityDescription, +): + """A class that describes host update entities.""" + + +HOST_UPDATE_ENTITIES = ( + ReolinkHostUpdateEntityDescription( + key="firmware", + supported=lambda api: api.supported(None, "firmware"), + device_class=UpdateDeviceClass.FIRMWARE, + ), +) + + async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, @@ -36,26 +52,32 @@ async def async_setup_entry( ) -> None: """Set up update entities for Reolink component.""" reolink_data: ReolinkData = hass.data[DOMAIN][config_entry.entry_id] - async_add_entities([ReolinkUpdateEntity(reolink_data)]) + + entities: list[ReolinkHostUpdateEntity] = [ + ReolinkHostUpdateEntity(reolink_data, entity_description) + for entity_description in HOST_UPDATE_ENTITIES + if entity_description.supported(reolink_data.host.api) + ] + async_add_entities(entities) -class ReolinkUpdateEntity( - ReolinkBaseCoordinatorEntity[str | Literal[False] | NewSoftwareVersion], +class ReolinkHostUpdateEntity( + ReolinkHostCoordinatorEntity, UpdateEntity, ): - """Update entity for a Netgear device.""" + """Update entity class for Reolink Host.""" - _attr_device_class = UpdateDeviceClass.FIRMWARE + entity_description: ReolinkHostUpdateEntityDescription _attr_release_url = "https://reolink.com/download-center/" def __init__( self, reolink_data: ReolinkData, + entity_description: ReolinkHostUpdateEntityDescription, ) -> None: - """Initialize a Netgear device.""" + """Initialize Reolink update entity.""" + self.entity_description = entity_description super().__init__(reolink_data, reolink_data.firmware_coordinator) - - self._attr_unique_id = f"{self._host.unique_id}" self._cancel_update: CALLBACK_TYPE | None = None @property @@ -66,32 +88,35 @@ class ReolinkUpdateEntity( @property def latest_version(self) -> str | None: """Latest version available for install.""" - if not self.coordinator.data: + new_firmware = self._host.api.firmware_update_available() + if not new_firmware: return self.installed_version - if isinstance(self.coordinator.data, str): - return self.coordinator.data + if isinstance(new_firmware, str): + return new_firmware - return self.coordinator.data.version_string + return new_firmware.version_string @property def supported_features(self) -> UpdateEntityFeature: """Flag supported features.""" supported_features = UpdateEntityFeature.INSTALL - if isinstance(self.coordinator.data, NewSoftwareVersion): + new_firmware = self._host.api.firmware_update_available() + if isinstance(new_firmware, NewSoftwareVersion): supported_features |= UpdateEntityFeature.RELEASE_NOTES return supported_features async def async_release_notes(self) -> str | None: """Return the release notes.""" - if not isinstance(self.coordinator.data, NewSoftwareVersion): + new_firmware = self._host.api.firmware_update_available() + if not isinstance(new_firmware, NewSoftwareVersion): return None return ( "If the install button fails, download this" - f" [firmware zip file]({self.coordinator.data.download_url})." + f" [firmware zip file]({new_firmware.download_url})." " Then, follow the installation guide (PDF in the zip file).\n\n" - f"## Release notes\n\n{self.coordinator.data.release_notes}" + f"## Release notes\n\n{new_firmware.release_notes}" ) async def async_install( diff --git a/tests/components/reolink/conftest.py b/tests/components/reolink/conftest.py index d997b57bb52..9b7dd481c9d 100644 --- a/tests/components/reolink/conftest.py +++ b/tests/components/reolink/conftest.py @@ -86,6 +86,7 @@ def reolink_connect_class() -> Generator[MagicMock]: host_mock.camera_name.return_value = TEST_NVR_NAME host_mock.camera_sw_version.return_value = "v1.1.0.0.0.0000" host_mock.camera_uid.return_value = TEST_UID + host_mock.firmware_update_available.return_value = False host_mock.session_active = True host_mock.timeout = 60 host_mock.renewtimer.return_value = 600 diff --git a/tests/components/reolink/test_init.py b/tests/components/reolink/test_init.py index 40b12b65f43..3cca1831a28 100644 --- a/tests/components/reolink/test_init.py +++ b/tests/components/reolink/test_init.py @@ -178,40 +178,39 @@ async def test_cleanup_disconnected_cams( assert sorted(device_models) == sorted(expected_models) -async def test_cleanup_deprecated_entities( +async def test_migrate_entity_ids( hass: HomeAssistant, config_entry: MockConfigEntry, reolink_connect: MagicMock, entity_registry: er.EntityRegistry, ) -> None: - """Test deprecated ir_lights light entity is cleaned.""" + """Test entity ids that need to be migrated.""" reolink_connect.channels = [0] - ir_id = f"{TEST_MAC}_0_ir_lights" + original_id = f"{TEST_MAC}" + new_id = f"{TEST_MAC}_firmware" + domain = Platform.UPDATE entity_registry.async_get_or_create( - domain=Platform.LIGHT, + domain=domain, platform=const.DOMAIN, - unique_id=ir_id, + unique_id=original_id, config_entry=config_entry, - suggested_object_id=ir_id, + suggested_object_id=original_id, disabled_by=None, ) - assert entity_registry.async_get_entity_id(Platform.LIGHT, const.DOMAIN, ir_id) - assert ( - entity_registry.async_get_entity_id(Platform.SWITCH, const.DOMAIN, ir_id) - is None - ) + assert entity_registry.async_get_entity_id(domain, const.DOMAIN, original_id) + assert entity_registry.async_get_entity_id(domain, const.DOMAIN, new_id) is None - # setup CH 0 and NVR switch entities/device - with patch("homeassistant.components.reolink.PLATFORMS", [Platform.SWITCH]): + # setup CH 0 and host entities/device + with patch("homeassistant.components.reolink.PLATFORMS", [domain]): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() assert ( - entity_registry.async_get_entity_id(Platform.LIGHT, const.DOMAIN, ir_id) is None + entity_registry.async_get_entity_id(domain, const.DOMAIN, original_id) is None ) - assert entity_registry.async_get_entity_id(Platform.SWITCH, const.DOMAIN, ir_id) + assert entity_registry.async_get_entity_id(domain, const.DOMAIN, new_id) async def test_no_repair_issue( From 72c62571318d4fdad8e253b340ab27069a6da343 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Thu, 13 Jun 2024 21:34:58 +0200 Subject: [PATCH 0658/1445] Update frontend to 20240610.1 (#119634) --- 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 d3d19375105..1b17601a2f6 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==20240610.0"] + "requirements": ["home-assistant-frontend==20240610.1"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index ef4cb7773cb..8f7958bdc4c 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -32,7 +32,7 @@ habluetooth==3.1.1 hass-nabucasa==0.81.1 hassil==1.7.1 home-assistant-bluetooth==1.12.1 -home-assistant-frontend==20240610.0 +home-assistant-frontend==20240610.1 home-assistant-intents==2024.6.5 httpx==0.27.0 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 40f25b8608d..8bfbce89514 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1090,7 +1090,7 @@ hole==0.8.0 holidays==0.50 # homeassistant.components.frontend -home-assistant-frontend==20240610.0 +home-assistant-frontend==20240610.1 # homeassistant.components.conversation home-assistant-intents==2024.6.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 586bbf32872..d62837452b3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -892,7 +892,7 @@ hole==0.8.0 holidays==0.50 # homeassistant.components.frontend -home-assistant-frontend==20240610.0 +home-assistant-frontend==20240610.1 # homeassistant.components.conversation home-assistant-intents==2024.6.5 From 40b98b70b0d4aab36140d37deb170fbcd78cb6df Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Thu, 13 Jun 2024 22:36:03 +0300 Subject: [PATCH 0659/1445] Wait for background tasks in Shelly tests (#119636) --- tests/components/shelly/test_config_flow.py | 2 +- tests/components/shelly/test_coordinator.py | 2 +- tests/components/shelly/test_init.py | 6 +++--- tests/components/shelly/test_number.py | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/components/shelly/test_config_flow.py b/tests/components/shelly/test_config_flow.py index f6467215faa..a26c6eac405 100644 --- a/tests/components/shelly/test_config_flow.py +++ b/tests/components/shelly/test_config_flow.py @@ -1125,7 +1125,7 @@ async def test_zeroconf_sleeping_device_not_triggers_refresh( await hass.async_block_till_done() mock_rpc_device.mock_online() - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert "online, resuming setup" in caplog.text assert len(mock_rpc_device.initialize.mock_calls) == 1 diff --git a/tests/components/shelly/test_coordinator.py b/tests/components/shelly/test_coordinator.py index 895d18cd7e1..1e0af115c9e 100644 --- a/tests/components/shelly/test_coordinator.py +++ b/tests/components/shelly/test_coordinator.py @@ -352,7 +352,7 @@ async def test_block_button_click_event( # Make device online mock_block_device.mock_online() - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) device = dr.async_entries_for_config_entry(device_registry, entry.entry_id)[0] diff --git a/tests/components/shelly/test_init.py b/tests/components/shelly/test_init.py index 05d306c76ff..998d56fc6cc 100644 --- a/tests/components/shelly/test_init.py +++ b/tests/components/shelly/test_init.py @@ -263,7 +263,7 @@ async def test_sleeping_block_device_online( assert "will resume when device is online" in caplog.text mock_block_device.mock_online() - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert "online, resuming setup" in caplog.text assert entry.data["sleep_period"] == device_sleep @@ -284,7 +284,7 @@ async def test_sleeping_rpc_device_online( assert "will resume when device is online" in caplog.text mock_rpc_device.mock_online() - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert "online, resuming setup" in caplog.text assert entry.data["sleep_period"] == device_sleep @@ -302,7 +302,7 @@ async def test_sleeping_rpc_device_online_new_firmware( mutate_rpc_device_status(monkeypatch, mock_rpc_device, "sys", "wakeup_period", 1500) mock_rpc_device.mock_online() - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert "online, resuming setup" in caplog.text assert entry.data["sleep_period"] == 1500 diff --git a/tests/components/shelly/test_number.py b/tests/components/shelly/test_number.py index 3f0f3ae8686..ff453b3251c 100644 --- a/tests/components/shelly/test_number.py +++ b/tests/components/shelly/test_number.py @@ -188,7 +188,7 @@ async def test_block_set_value_connection_error( # Make device online mock_block_device.mock_online() - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) with pytest.raises(HomeAssistantError): await hass.services.async_call( From 7bbd28d38542a0ab672a32ee2d200301499025e0 Mon Sep 17 00:00:00 2001 From: "Mr. Bubbles" Date: Thu, 13 Jun 2024 22:52:19 +0200 Subject: [PATCH 0660/1445] Migrate library to PyLoadAPI 1.1.0 in pyLoad integration (#116053) * Migrate pyLoad integration to externa API library * Add const to .coveragerc * raise update failed when cookie expired * fix exceptions * Add tests * bump to PyLoadAPI 1.1.0 * remove unreachable code * fix tests * Improve logging and exception handling - Modify manifest.json to update logger configuration. - Improve error messages for authentication failures in sensor.py. - Simplify and rename pytest fixtures in conftest.py. - Update test cases in test_sensor.py to check for log entries and remove unnecessary code. * remove exception translations --- .coveragerc | 1 - CODEOWNERS | 2 + homeassistant/components/pyload/const.py | 7 + homeassistant/components/pyload/manifest.json | 7 +- homeassistant/components/pyload/sensor.py | 124 ++++++++---------- homeassistant/generated/integrations.json | 2 +- requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/pyload/__init__.py | 1 + tests/components/pyload/conftest.py | 74 +++++++++++ .../pyload/snapshots/test_sensor.ambr | 16 +++ tests/components/pyload/test_sensor.py | 84 ++++++++++++ 12 files changed, 249 insertions(+), 75 deletions(-) create mode 100644 homeassistant/components/pyload/const.py create mode 100644 tests/components/pyload/__init__.py create mode 100644 tests/components/pyload/conftest.py create mode 100644 tests/components/pyload/snapshots/test_sensor.ambr create mode 100644 tests/components/pyload/test_sensor.py diff --git a/.coveragerc b/.coveragerc index fefd9205b05..bba6eb584c5 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1061,7 +1061,6 @@ omit = homeassistant/components/pushbullet/sensor.py homeassistant/components/pushover/notify.py homeassistant/components/pushsafer/notify.py - homeassistant/components/pyload/sensor.py homeassistant/components/qbittorrent/__init__.py homeassistant/components/qbittorrent/coordinator.py homeassistant/components/qbittorrent/sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index f3a33c394ca..fa8db6628ce 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1100,6 +1100,8 @@ build.json @home-assistant/supervisor /tests/components/pvoutput/ @frenck /homeassistant/components/pvpc_hourly_pricing/ @azogue /tests/components/pvpc_hourly_pricing/ @azogue +/homeassistant/components/pyload/ @tr4nt0r +/tests/components/pyload/ @tr4nt0r /homeassistant/components/qbittorrent/ @geoffreylagaisse @finder39 /tests/components/qbittorrent/ @geoffreylagaisse @finder39 /homeassistant/components/qingping/ @bdraco diff --git a/homeassistant/components/pyload/const.py b/homeassistant/components/pyload/const.py new file mode 100644 index 00000000000..a7d155d8b33 --- /dev/null +++ b/homeassistant/components/pyload/const.py @@ -0,0 +1,7 @@ +"""Constants for the pyLoad integration.""" + +DOMAIN = "pyload" + +DEFAULT_HOST = "localhost" +DEFAULT_NAME = "pyLoad" +DEFAULT_PORT = 8000 diff --git a/homeassistant/components/pyload/manifest.json b/homeassistant/components/pyload/manifest.json index 6cb641f6ead..90d750ff9b8 100644 --- a/homeassistant/components/pyload/manifest.json +++ b/homeassistant/components/pyload/manifest.json @@ -1,7 +1,10 @@ { "domain": "pyload", "name": "pyLoad", - "codeowners": [], + "codeowners": ["@tr4nt0r"], "documentation": "https://www.home-assistant.io/integrations/pyload", - "iot_class": "local_polling" + "integration_type": "service", + "iot_class": "local_polling", + "loggers": ["pyloadapi"], + "requirements": ["PyLoadAPI==1.1.0"] } diff --git a/homeassistant/components/pyload/sensor.py b/homeassistant/components/pyload/sensor.py index b7d4d1f461b..c21e74b18a7 100644 --- a/homeassistant/components/pyload/sensor.py +++ b/homeassistant/components/pyload/sensor.py @@ -5,7 +5,10 @@ from __future__ import annotations from datetime import timedelta import logging -import requests +from aiohttp import CookieJar +from pyloadapi.api import PyLoadAPI +from pyloadapi.exceptions import CannotConnect, InvalidAuth, ParserError +from pyloadapi.types import StatusServerResponse import voluptuous as vol from homeassistant.components.sensor import ( @@ -22,22 +25,22 @@ from homeassistant.const import ( CONF_PORT, CONF_SSL, CONF_USERNAME, - CONTENT_TYPE_JSON, UnitOfDataRate, ) from homeassistant.core import HomeAssistant +from homeassistant.exceptions import PlatformNotReady +from homeassistant.helpers.aiohttp_client import async_create_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from homeassistant.util import Throttle +from homeassistant.helpers.update_coordinator import UpdateFailed + +from .const import DEFAULT_HOST, DEFAULT_NAME, DEFAULT_PORT _LOGGER = logging.getLogger(__name__) -DEFAULT_HOST = "localhost" -DEFAULT_NAME = "pyLoad" -DEFAULT_PORT = 8000 -MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=15) +SCAN_INTERVAL = timedelta(seconds=15) SENSOR_TYPES = { "speed": SensorEntityDescription( @@ -63,10 +66,10 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -def setup_platform( +async def async_setup_platform( hass: HomeAssistant, config: ConfigType, - add_entities: AddEntitiesCallback, + async_add_entities: AddEntitiesCallback, discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the pyLoad sensors.""" @@ -77,16 +80,26 @@ def setup_platform( username = config.get(CONF_USERNAME) password = config.get(CONF_PASSWORD) monitored_types = config[CONF_MONITORED_VARIABLES] - url = f"{protocol}://{host}:{port}/api/" + url = f"{protocol}://{host}:{port}/" + session = async_create_clientsession( + hass, + verify_ssl=False, + cookie_jar=CookieJar(unsafe=True), + ) + pyloadapi = PyLoadAPI(session, api_url=url, username=username, password=password) try: - pyloadapi = PyLoadAPI(api_url=url, username=username, password=password) - except ( - requests.exceptions.ConnectionError, - requests.exceptions.HTTPError, - ) as conn_err: - _LOGGER.error("Error setting up pyLoad API: %s", conn_err) - return + await pyloadapi.login() + except CannotConnect as conn_err: + raise PlatformNotReady( + "Unable to connect and retrieve data from pyLoad API" + ) from conn_err + except ParserError as e: + raise PlatformNotReady("Unable to parse data from pyLoad API") from e + except InvalidAuth as e: + raise PlatformNotReady( + f"Authentication failed for {config[CONF_USERNAME]}, check your login credentials" + ) from e devices = [] for ng_type in monitored_types: @@ -95,7 +108,7 @@ def setup_platform( ) devices.append(new_sensor) - add_entities(devices, True) + async_add_entities(devices, True) class PyLoadSensor(SensorEntity): @@ -109,64 +122,33 @@ class PyLoadSensor(SensorEntity): self.type = sensor_type.key self.api = api self.entity_description = sensor_type + self.data: StatusServerResponse - def update(self) -> None: + async def async_update(self) -> None: """Update state of sensor.""" try: - self.api.update() - except requests.exceptions.ConnectionError: - # Error calling the API, already logged in api.update() - return + self.data = await self.api.get_status() + except InvalidAuth: + _LOGGER.info("Authentication failed, trying to reauthenticate") + try: + await self.api.login() + except InvalidAuth as e: + raise PlatformNotReady( + f"Authentication failed for {self.api.username}, check your login credentials" + ) from e + else: + raise UpdateFailed( + "Unable to retrieve data due to cookie expiration but re-authentication was successful." + ) + except CannotConnect as e: + raise UpdateFailed( + "Unable to connect and retrieve data from pyLoad API" + ) from e + except ParserError as e: + raise UpdateFailed("Unable to parse data from pyLoad API") from e - if self.api.status is None: - _LOGGER.debug( - "Update of %s requested, but no status is available", self.name - ) - return - - if (value := self.api.status.get(self.type)) is None: - _LOGGER.warning("Unable to locate value for %s", self.type) - return + value = getattr(self.data, self.type) if "speed" in self.type and value > 0: # Convert download rate from Bytes/s to MBytes/s self._attr_native_value = round(value / 2**20, 2) - else: - self._attr_native_value = value - - -class PyLoadAPI: - """Simple wrapper for pyLoad's API.""" - - def __init__(self, api_url, username=None, password=None): - """Initialize pyLoad API and set headers needed later.""" - self.api_url = api_url - self.status = None - self.headers = {"Content-Type": CONTENT_TYPE_JSON} - - if username is not None and password is not None: - self.payload = {"username": username, "password": password} - self.login = requests.post(f"{api_url}login", data=self.payload, timeout=5) - self.update() - - def post(self): - """Send a POST request and return the response as a dict.""" - try: - response = requests.post( - f"{self.api_url}statusServer", - cookies=self.login.cookies, - headers=self.headers, - timeout=5, - ) - response.raise_for_status() - _LOGGER.debug("JSON Response: %s", response.json()) - return response.json() - - except requests.exceptions.ConnectionError as conn_exc: - _LOGGER.error("Failed to update pyLoad status. Error: %s", conn_exc) - raise - - @Throttle(MIN_TIME_BETWEEN_UPDATES) - def update(self): - """Update cached response.""" - self.status = self.post() diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 7f2f4292748..425702562d0 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -4775,7 +4775,7 @@ }, "pyload": { "name": "pyLoad", - "integration_type": "hub", + "integration_type": "service", "config_flow": false, "iot_class": "local_polling" }, diff --git a/requirements_all.txt b/requirements_all.txt index 8bfbce89514..b7fccc8d6f2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -59,6 +59,9 @@ PyFlume==0.6.5 # homeassistant.components.fronius PyFronius==0.7.3 +# homeassistant.components.pyload +PyLoadAPI==1.1.0 + # homeassistant.components.mvglive PyMVGLive==1.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d62837452b3..be404d99447 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -50,6 +50,9 @@ PyFlume==0.6.5 # homeassistant.components.fronius PyFronius==0.7.3 +# homeassistant.components.pyload +PyLoadAPI==1.1.0 + # homeassistant.components.met_eireann PyMetEireann==2021.8.0 diff --git a/tests/components/pyload/__init__.py b/tests/components/pyload/__init__.py new file mode 100644 index 00000000000..5ba1e4f9337 --- /dev/null +++ b/tests/components/pyload/__init__.py @@ -0,0 +1 @@ +"""Tests for the pyLoad component.""" diff --git a/tests/components/pyload/conftest.py b/tests/components/pyload/conftest.py new file mode 100644 index 00000000000..31f251c6e85 --- /dev/null +++ b/tests/components/pyload/conftest.py @@ -0,0 +1,74 @@ +"""Fixtures for pyLoad integration tests.""" + +from collections.abc import Generator +from unittest.mock import AsyncMock, patch + +from pyloadapi.types import LoginResponse, StatusServerResponse +import pytest + +from homeassistant.const import ( + CONF_HOST, + CONF_MONITORED_VARIABLES, + CONF_NAME, + CONF_PASSWORD, + CONF_PLATFORM, + CONF_PORT, + CONF_SSL, + CONF_USERNAME, +) +from homeassistant.helpers.typing import ConfigType + + +@pytest.fixture +def pyload_config() -> ConfigType: + """Mock pyload configuration entry.""" + return { + "sensor": { + CONF_PLATFORM: "pyload", + CONF_HOST: "localhost", + CONF_PORT: 8000, + CONF_USERNAME: "username", + CONF_PASSWORD: "password", + CONF_SSL: True, + CONF_MONITORED_VARIABLES: ["speed"], + CONF_NAME: "pyload", + } + } + + +@pytest.fixture +def mock_pyloadapi() -> Generator[AsyncMock, None, None]: + """Mock PyLoadAPI.""" + with ( + patch( + "homeassistant.components.pyload.sensor.PyLoadAPI", + autospec=True, + ) as mock_client, + ): + client = mock_client.return_value + client.username = "username" + client.login.return_value = LoginResponse.from_dict( + { + "_permanent": True, + "authenticated": True, + "id": 2, + "name": "username", + "role": 0, + "perms": 0, + "template": "default", + "_flashes": [["message", "Logged in successfully"]], + } + ) + client.get_status.return_value = StatusServerResponse.from_dict( + { + "pause": False, + "active": 1, + "queue": 6, + "total": 37, + "speed": 5405963.0, + "download": True, + "reconnect": False, + "captcha": False, + } + ) + yield client diff --git a/tests/components/pyload/snapshots/test_sensor.ambr b/tests/components/pyload/snapshots/test_sensor.ambr new file mode 100644 index 00000000000..384a59b78b2 --- /dev/null +++ b/tests/components/pyload/snapshots/test_sensor.ambr @@ -0,0 +1,16 @@ +# serializer version: 1 +# name: test_setup + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'data_rate', + 'friendly_name': 'pyload Speed', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.pyload_speed', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '5.16', + }) +# --- diff --git a/tests/components/pyload/test_sensor.py b/tests/components/pyload/test_sensor.py new file mode 100644 index 00000000000..54f15deb313 --- /dev/null +++ b/tests/components/pyload/test_sensor.py @@ -0,0 +1,84 @@ +"""Tests for the pyLoad Sensors.""" + +from unittest.mock import AsyncMock + +from pyloadapi.exceptions import CannotConnect, InvalidAuth, ParserError +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.sensor import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.helpers.typing import ConfigType +from homeassistant.setup import async_setup_component + + +@pytest.mark.usefixtures("mock_pyloadapi") +async def test_setup( + hass: HomeAssistant, + pyload_config: ConfigType, + snapshot: SnapshotAssertion, +) -> None: + """Test setup of the pyload sensor platform.""" + + assert await async_setup_component(hass, DOMAIN, pyload_config) + await hass.async_block_till_done() + + result = hass.states.get("sensor.pyload_speed") + assert result == snapshot + + +@pytest.mark.parametrize( + ("exception", "expected_exception"), + [ + (CannotConnect, "Unable to connect and retrieve data from pyLoad API"), + (ParserError, "Unable to parse data from pyLoad API"), + ( + InvalidAuth, + "Authentication failed for username, check your login credentials", + ), + ], +) +async def test_setup_exceptions( + hass: HomeAssistant, + pyload_config: ConfigType, + mock_pyloadapi: AsyncMock, + exception: Exception, + expected_exception: str, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test exceptions during setup up pyLoad platform.""" + + mock_pyloadapi.login.side_effect = exception + + assert await async_setup_component(hass, DOMAIN, pyload_config) + await hass.async_block_till_done() + + assert len(hass.states.async_all(DOMAIN)) == 0 + assert expected_exception in caplog.text + + +@pytest.mark.parametrize( + ("exception", "expected_exception"), + [ + (CannotConnect, "UpdateFailed"), + (ParserError, "UpdateFailed"), + (InvalidAuth, "UpdateFailed"), + ], +) +async def test_sensor_update_exceptions( + hass: HomeAssistant, + pyload_config: ConfigType, + mock_pyloadapi: AsyncMock, + exception: Exception, + expected_exception: str, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test exceptions during update of pyLoad sensor.""" + + mock_pyloadapi.get_status.side_effect = exception + + assert await async_setup_component(hass, DOMAIN, pyload_config) + await hass.async_block_till_done() + + assert len(hass.states.async_all(DOMAIN)) == 0 + assert expected_exception in caplog.text From de27f24a4c9c6f1b3e225b4f3d30758a5120222f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 13 Jun 2024 16:17:11 -0500 Subject: [PATCH 0661/1445] Use the existing api client for unifiprotect repairs if available (#119640) Co-authored-by: TheJulianJES --- homeassistant/components/unifiprotect/data.py | 4 +- .../components/unifiprotect/repairs.py | 42 +++++++------ tests/components/unifiprotect/test_repairs.py | 61 +++++++++++++++++++ 3 files changed, 88 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/unifiprotect/data.py b/homeassistant/components/unifiprotect/data.py index 59e98cfb9a0..97f3a4129ae 100644 --- a/homeassistant/components/unifiprotect/data.py +++ b/homeassistant/components/unifiprotect/data.py @@ -376,7 +376,9 @@ def async_get_data_for_entry_id( hass: HomeAssistant, entry_id: str ) -> ProtectData | None: """Find the ProtectData instance for a config entry id.""" - if entry := hass.config_entries.async_get_entry(entry_id): + if (entry := hass.config_entries.async_get_entry(entry_id)) and hasattr( + entry, "runtime_data" + ): entry = cast(UFPConfigEntry, entry) return entry.runtime_data return None diff --git a/homeassistant/components/unifiprotect/repairs.py b/homeassistant/components/unifiprotect/repairs.py index 0e505f87391..020da0a03f6 100644 --- a/homeassistant/components/unifiprotect/repairs.py +++ b/homeassistant/components/unifiprotect/repairs.py @@ -11,11 +11,12 @@ import voluptuous as vol from homeassistant import data_entry_flow from homeassistant.components.repairs import ConfirmRepairFlow, RepairsFlow +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import issue_registry as ir from .const import CONF_ALLOW_EA -from .data import UFPConfigEntry +from .data import UFPConfigEntry, async_get_data_for_entry_id from .utils import async_create_api_client @@ -219,29 +220,34 @@ class RTSPRepair(ProtectRepair): ) +@callback +def _async_get_or_create_api_client( + hass: HomeAssistant, entry: ConfigEntry +) -> ProtectApiClient: + """Get or create an API client.""" + if data := async_get_data_for_entry_id(hass, entry.entry_id): + return data.api + return async_create_api_client(hass, entry) + + async def async_create_fix_flow( hass: HomeAssistant, issue_id: str, data: dict[str, str | int | float | None] | None, ) -> RepairsFlow: """Create flow.""" - if data is not None and issue_id == "ea_channel_warning": - entry_id = cast(str, data["entry_id"]) - if (entry := hass.config_entries.async_get_entry(entry_id)) is not None: - api = async_create_api_client(hass, entry) + if ( + data is not None + and "entry_id" in data + and (entry := hass.config_entries.async_get_entry(cast(str, data["entry_id"]))) + ): + api = _async_get_or_create_api_client(hass, entry) + if issue_id == "ea_channel_warning": return EAConfirmRepair(api=api, entry=entry) - - elif data is not None and issue_id == "cloud_user": - entry_id = cast(str, data["entry_id"]) - if (entry := hass.config_entries.async_get_entry(entry_id)) is not None: - api = async_create_api_client(hass, entry) + if issue_id == "cloud_user": return CloudAccountRepair(api=api, entry=entry) - - elif data is not None and issue_id.startswith("rtsp_disabled_"): - entry_id = cast(str, data["entry_id"]) - camera_id = cast(str, data["camera_id"]) - if (entry := hass.config_entries.async_get_entry(entry_id)) is not None: - api = async_create_api_client(hass, entry) - return RTSPRepair(api=api, entry=entry, camera_id=camera_id) - + if issue_id.startswith("rtsp_disabled_"): + return RTSPRepair( + api=api, entry=entry, camera_id=cast(str, data["camera_id"]) + ) return ConfirmRepairFlow() diff --git a/tests/components/unifiprotect/test_repairs.py b/tests/components/unifiprotect/test_repairs.py index 7d76550f7c7..51ffd4d23cb 100644 --- a/tests/components/unifiprotect/test_repairs.py +++ b/tests/components/unifiprotect/test_repairs.py @@ -357,3 +357,64 @@ async def test_rtsp_writable_fix( ufp.api.update_device.assert_called_with( ModelType.CAMERA, doorbell.id, {"channels": channels} ) + + +async def test_rtsp_writable_fix_when_not_setup( + hass: HomeAssistant, + ufp: MockUFPFixture, + doorbell: Camera, + hass_client: ClientSessionGenerator, + hass_ws_client: WebSocketGenerator, +) -> None: + """Test RTSP disabled warning if the integration is no longer set up.""" + + for channel in doorbell.channels: + channel.is_rtsp_enabled = False + + await init_entry(hass, ufp, [doorbell]) + await async_process_repairs_platforms(hass) + ws_client = await hass_ws_client(hass) + client = await hass_client() + + new_doorbell = deepcopy(doorbell) + new_doorbell.channels[0].is_rtsp_enabled = True + ufp.api.get_camera = AsyncMock(side_effect=[doorbell, new_doorbell]) + ufp.api.update_device = AsyncMock() + issue_id = f"rtsp_disabled_{doorbell.id}" + + await ws_client.send_json({"id": 1, "type": "repairs/list_issues"}) + msg = await ws_client.receive_json() + + assert msg["success"] + assert len(msg["result"]["issues"]) > 0 + issue = None + for i in msg["result"]["issues"]: + if i["issue_id"] == issue_id: + issue = i + assert issue is not None + + # Unload the integration to ensure the fix flow still works + # if the integration is no longer set up + await hass.config_entries.async_unload(ufp.entry.entry_id) + await hass.async_block_till_done() + + url = RepairsFlowIndexView.url + resp = await client.post(url, json={"handler": DOMAIN, "issue_id": issue_id}) + assert resp.status == HTTPStatus.OK + data = await resp.json() + + flow_id = data["flow_id"] + assert data["step_id"] == "start" + + url = RepairsFlowResourceView.url.format(flow_id=flow_id) + resp = await client.post(url) + assert resp.status == HTTPStatus.OK + data = await resp.json() + + assert data["type"] == "create_entry" + + channels = doorbell.unifi_dict()["channels"] + channels[0]["isRtspEnabled"] = True + ufp.api.update_device.assert_called_with( + ModelType.CAMERA, doorbell.id, {"channels": channels} + ) From 0c3a5ae5da7b4bfd670c9469e2ef7355e1026b89 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 13 Jun 2024 16:17:31 -0500 Subject: [PATCH 0662/1445] Dispatch unifiprotect websocket messages based on model (#119633) --- homeassistant/components/unifiprotect/data.py | 63 ++++++++++--------- .../unifiprotect/test_binary_sensor.py | 14 +++-- .../unifiprotect/test_media_source.py | 23 +++++++ .../components/unifiprotect/test_recorder.py | 3 +- tests/components/unifiprotect/test_sensor.py | 12 +++- tests/components/unifiprotect/test_views.py | 12 +++- 6 files changed, 91 insertions(+), 36 deletions(-) diff --git a/homeassistant/components/unifiprotect/data.py b/homeassistant/components/unifiprotect/data.py index 97f3a4129ae..59a5242273a 100644 --- a/homeassistant/components/unifiprotect/data.py +++ b/homeassistant/components/unifiprotect/data.py @@ -6,7 +6,7 @@ from collections.abc import Callable, Iterable from datetime import datetime, timedelta from functools import partial import logging -from typing import Any, cast +from typing import TYPE_CHECKING, Any, cast from typing_extensions import Generator from uiprotect import ProtectApiClient @@ -16,7 +16,6 @@ from uiprotect.data import ( Camera, Event, EventType, - Liveview, ModelType, ProtectAdoptableDeviceModel, WSSubscriptionMessage, @@ -231,41 +230,49 @@ class ProtectData: @callback def _async_process_ws_message(self, message: WSSubscriptionMessage) -> None: - if message.new_obj is None: + """Process a message from the websocket.""" + if (new_obj := message.new_obj) is None: if isinstance(message.old_obj, ProtectAdoptableDeviceModel): self._async_remove_device(message.old_obj) return - obj = message.new_obj - if isinstance(obj, (ProtectAdoptableDeviceModel, NVR)): - if message.old_obj is None and isinstance(obj, ProtectAdoptableDeviceModel): - self._async_add_device(obj) - elif getattr(obj, "is_adopted_by_us", True): - self._async_update_device(obj, message.changed_data) - - # trigger updates for camera that the event references - elif isinstance(obj, Event): + model_type = new_obj.model + if model_type is ModelType.EVENT: + if TYPE_CHECKING: + assert isinstance(new_obj, Event) if _LOGGER.isEnabledFor(logging.DEBUG): - log_event(obj) - if obj.type is EventType.DEVICE_ADOPTED: - if obj.metadata is not None and obj.metadata.device_id is not None: - device = self.api.bootstrap.get_device_from_id( - obj.metadata.device_id - ) - if device is not None: - self._async_add_device(device) - elif obj.camera is not None: - self._async_signal_device_update(obj.camera) - elif obj.light is not None: - self._async_signal_device_update(obj.light) - elif obj.sensor is not None: - self._async_signal_device_update(obj.sensor) - # alert user viewport needs restart so voice clients can get new options - elif len(self.api.bootstrap.viewers) > 0 and isinstance(obj, Liveview): + log_event(new_obj) + if ( + (new_obj.type is EventType.DEVICE_ADOPTED) + and (metadata := new_obj.metadata) + and (device_id := metadata.device_id) + and (device := self.api.bootstrap.get_device_from_id(device_id)) + ): + self._async_add_device(device) + elif camera := new_obj.camera: + self._async_signal_device_update(camera) + elif light := new_obj.light: + self._async_signal_device_update(light) + elif sensor := new_obj.sensor: + self._async_signal_device_update(sensor) + return + + if model_type is ModelType.LIVEVIEW and len(self.api.bootstrap.viewers) > 0: + # alert user viewport needs restart so voice clients can get new options _LOGGER.warning( "Liveviews updated. Restart Home Assistant to update Viewport select" " options" ) + return + + if message.old_obj is None and isinstance(new_obj, ProtectAdoptableDeviceModel): + self._async_add_device(new_obj) + return + + if getattr(new_obj, "is_adopted_by_us", True) and hasattr(new_obj, "mac"): + if TYPE_CHECKING: + assert isinstance(new_obj, (ProtectAdoptableDeviceModel, NVR)) + self._async_update_device(new_obj, message.changed_data) @callback def _async_process_updates(self, updates: Bootstrap | None) -> None: diff --git a/tests/components/unifiprotect/test_binary_sensor.py b/tests/components/unifiprotect/test_binary_sensor.py index b23fd529233..3231c233ca3 100644 --- a/tests/components/unifiprotect/test_binary_sensor.py +++ b/tests/components/unifiprotect/test_binary_sensor.py @@ -5,7 +5,7 @@ from __future__ import annotations from datetime import datetime, timedelta from unittest.mock import Mock -from uiprotect.data import Camera, Event, EventType, Light, MountType, Sensor +from uiprotect.data import Camera, Event, EventType, Light, ModelType, MountType, Sensor from uiprotect.data.nvr import EventMetadata from homeassistant.components.binary_sensor import BinarySensorDeviceClass @@ -281,6 +281,7 @@ async def test_binary_sensor_update_motion( ) event = Event( + model=ModelType.EVENT, id="test_event_id", type=EventType.MOTION, start=fixed_now - timedelta(seconds=1), @@ -289,19 +290,21 @@ async def test_binary_sensor_update_motion( smart_detect_types=[], smart_detect_event_ids=[], camera_id=doorbell.id, + api=ufp.api, ) new_camera = doorbell.copy() new_camera.is_motion_detected = True new_camera.last_motion_event_id = event.id - mock_msg = Mock() - mock_msg.changed_data = {} - mock_msg.new_obj = new_camera - ufp.api.bootstrap.cameras = {new_camera.id: new_camera} ufp.api.bootstrap.events = {event.id: event} + + mock_msg = Mock() + mock_msg.changed_data = {} + mock_msg.new_obj = event ufp.ws_msg(mock_msg) + await hass.async_block_till_done() state = hass.states.get(entity_id) @@ -325,6 +328,7 @@ async def test_binary_sensor_update_light_motion( event_metadata = EventMetadata(light_id=light.id) event = Event( + model=ModelType.EVENT, id="test_event_id", type=EventType.MOTION_LIGHT, start=fixed_now - timedelta(seconds=1), diff --git a/tests/components/unifiprotect/test_media_source.py b/tests/components/unifiprotect/test_media_source.py index 2cdebeafb04..60cd3150884 100644 --- a/tests/components/unifiprotect/test_media_source.py +++ b/tests/components/unifiprotect/test_media_source.py @@ -10,6 +10,7 @@ from uiprotect.data import ( Camera, Event, EventType, + ModelType, Permission, SmartDetectObjectType, ) @@ -72,6 +73,7 @@ async def test_resolve_media_thumbnail( await init_entry(hass, ufp, [doorbell], regenerate_ids=False) event = Event( + model=ModelType.EVENT, id="test_event_id", type=EventType.MOTION, start=fixed_now - timedelta(seconds=20), @@ -103,6 +105,7 @@ async def test_resolve_media_event( await init_entry(hass, ufp, [doorbell], regenerate_ids=False) event = Event( + model=ModelType.EVENT, id="test_event_id", type=EventType.MOTION, start=fixed_now - timedelta(seconds=20), @@ -172,6 +175,7 @@ async def test_browse_media_event_ongoing( await init_entry(hass, ufp, [doorbell], regenerate_ids=False) event = Event( + model=ModelType.EVENT, id="test_event_id", type=EventType.MOTION, start=fixed_now - timedelta(seconds=20), @@ -591,6 +595,7 @@ async def test_browse_media_recent( await init_entry(hass, ufp, [doorbell], regenerate_ids=False) event = Event( + model=ModelType.EVENT, id="test_event_id", type=EventType.MOTION, start=fixed_now - timedelta(seconds=20), @@ -628,6 +633,7 @@ async def test_browse_media_recent_truncated( await init_entry(hass, ufp, [doorbell], regenerate_ids=False) event = Event( + model=ModelType.EVENT, id="test_event_id", type=EventType.MOTION, start=fixed_now - timedelta(seconds=20), @@ -660,6 +666,7 @@ async def test_browse_media_recent_truncated( [ ( Event( + model=ModelType.EVENT, id="test_event_id", type=EventType.RING, start=datetime(1000, 1, 1, 0, 0, 0), @@ -673,6 +680,7 @@ async def test_browse_media_recent_truncated( ), ( Event( + model=ModelType.EVENT, id="test_event_id", type=EventType.MOTION, start=datetime(1000, 1, 1, 0, 0, 0), @@ -686,6 +694,7 @@ async def test_browse_media_recent_truncated( ), ( Event( + model=ModelType.EVENT, id="test_event_id", type=EventType.SMART_DETECT, start=datetime(1000, 1, 1, 0, 0, 0), @@ -708,6 +717,7 @@ async def test_browse_media_recent_truncated( ), ( Event( + model=ModelType.EVENT, id="test_event_id", type=EventType.SMART_DETECT, start=datetime(1000, 1, 1, 0, 0, 0), @@ -721,6 +731,7 @@ async def test_browse_media_recent_truncated( ), ( Event( + model=ModelType.EVENT, id="test_event_id", type=EventType.SMART_DETECT, start=datetime(1000, 1, 1, 0, 0, 0), @@ -734,6 +745,7 @@ async def test_browse_media_recent_truncated( ), ( Event( + model=ModelType.EVENT, id="test_event_id", type=EventType.SMART_DETECT, start=datetime(1000, 1, 1, 0, 0, 0), @@ -757,6 +769,7 @@ async def test_browse_media_recent_truncated( ), ( Event( + model=ModelType.EVENT, id="test_event_id", type=EventType.SMART_DETECT, start=datetime(1000, 1, 1, 0, 0, 0), @@ -786,6 +799,7 @@ async def test_browse_media_recent_truncated( ), ( Event( + model=ModelType.EVENT, id="test_event_id", type=EventType.SMART_DETECT, start=datetime(1000, 1, 1, 0, 0, 0), @@ -820,6 +834,7 @@ async def test_browse_media_recent_truncated( ), ( Event( + model=ModelType.EVENT, id="test_event_id", type=EventType.SMART_DETECT, start=datetime(1000, 1, 1, 0, 0, 0), @@ -852,6 +867,7 @@ async def test_browse_media_recent_truncated( ), ( Event( + model=ModelType.EVENT, id="test_event_id", type=EventType.SMART_AUDIO_DETECT, start=datetime(1000, 1, 1, 0, 0, 0), @@ -906,6 +922,7 @@ async def test_browse_media_eventthumb( await init_entry(hass, ufp, [doorbell], regenerate_ids=False) event = Event( + model=ModelType.EVENT, id="test_event_id", type=EventType.SMART_DETECT, start=fixed_now - timedelta(seconds=20), @@ -969,6 +986,7 @@ async def test_browse_media_browse_day( await init_entry(hass, ufp, [doorbell], regenerate_ids=False) event = Event( + model=ModelType.EVENT, id="test_event_id", type=EventType.MOTION, start=fixed_now - timedelta(seconds=20), @@ -1010,6 +1028,7 @@ async def test_browse_media_browse_whole_month( await init_entry(hass, ufp, [doorbell], regenerate_ids=False) event = Event( + model=ModelType.EVENT, id="test_event_id", type=EventType.MOTION, start=fixed_now - timedelta(seconds=20), @@ -1052,6 +1071,7 @@ async def test_browse_media_browse_whole_month_december( await init_entry(hass, ufp, [doorbell], regenerate_ids=False) event1 = Event( + model=ModelType.EVENT, id="test_event_id", type=EventType.SMART_DETECT, start=fixed_now - timedelta(seconds=3663), @@ -1063,6 +1083,7 @@ async def test_browse_media_browse_whole_month_december( ) event1._api = ufp.api event2 = Event( + model=ModelType.EVENT, id="test_event_id2", type=EventType.MOTION, start=fixed_now - timedelta(seconds=20), @@ -1074,6 +1095,7 @@ async def test_browse_media_browse_whole_month_december( ) event2._api = ufp.api event3 = Event( + model=ModelType.EVENT, id="test_event_id3", type=EventType.MOTION, start=fixed_now - timedelta(seconds=20), @@ -1085,6 +1107,7 @@ async def test_browse_media_browse_whole_month_december( ) event3._api = ufp.api event4 = Event( + model=ModelType.EVENT, id="test_event_id4", type=EventType.MOTION, start=fixed_now - timedelta(seconds=20), diff --git a/tests/components/unifiprotect/test_recorder.py b/tests/components/unifiprotect/test_recorder.py index 94c93413de5..fe102c2fdbc 100644 --- a/tests/components/unifiprotect/test_recorder.py +++ b/tests/components/unifiprotect/test_recorder.py @@ -5,7 +5,7 @@ from __future__ import annotations from datetime import datetime, timedelta from unittest.mock import Mock -from uiprotect.data import Camera, Event, EventType +from uiprotect.data import Camera, Event, EventType, ModelType from homeassistant.components.recorder import Recorder from homeassistant.components.recorder.history import get_significant_states @@ -40,6 +40,7 @@ async def test_exclude_attributes( ) event = Event( + model=ModelType.EVENT, id="test_event_id", type=EventType.MOTION, start=fixed_now - timedelta(seconds=1), diff --git a/tests/components/unifiprotect/test_sensor.py b/tests/components/unifiprotect/test_sensor.py index 72915936a70..1a1374390ae 100644 --- a/tests/components/unifiprotect/test_sensor.py +++ b/tests/components/unifiprotect/test_sensor.py @@ -6,7 +6,15 @@ from datetime import datetime, timedelta from unittest.mock import Mock import pytest -from uiprotect.data import NVR, Camera, Event, EventType, Sensor, SmartDetectObjectType +from uiprotect.data import ( + NVR, + Camera, + Event, + EventType, + ModelType, + Sensor, + SmartDetectObjectType, +) from uiprotect.data.nvr import EventMetadata, LicensePlateMetadata from homeassistant.components.unifiprotect.const import DEFAULT_ATTRIBUTION @@ -438,6 +446,7 @@ async def test_sensor_update_alarm( event_metadata = EventMetadata(sensor_id=sensor_all.id, alarm_type="smoke") event = Event( + model=ModelType.EVENT, id="test_event_id", type=EventType.SENSOR_ALARM, start=fixed_now - timedelta(seconds=1), @@ -521,6 +530,7 @@ async def test_camera_update_licenseplate( license_plate=LicensePlateMetadata(name="ABCD1234", confidence_level=95) ) event = Event( + model=ModelType.EVENT, id="test_event_id", type=EventType.SMART_DETECT, start=fixed_now - timedelta(seconds=1), diff --git a/tests/components/unifiprotect/test_views.py b/tests/components/unifiprotect/test_views.py index 2b80a41b16f..fed0a98552d 100644 --- a/tests/components/unifiprotect/test_views.py +++ b/tests/components/unifiprotect/test_views.py @@ -6,7 +6,7 @@ from unittest.mock import AsyncMock, Mock from aiohttp import ClientResponse import pytest -from uiprotect.data import Camera, Event, EventType +from uiprotect.data import Camera, Event, EventType, ModelType from uiprotect.exceptions import ClientError from homeassistant.components.unifiprotect.views import ( @@ -179,6 +179,7 @@ async def test_video_bad_event( await init_entry(hass, ufp, [camera]) event = Event( + model=ModelType.EVENT, api=ufp.api, camera_id="test_id", start=fixed_now - timedelta(seconds=30), @@ -205,6 +206,7 @@ async def test_video_bad_event_ongoing( await init_entry(hass, ufp, [camera]) event = Event( + model=ModelType.EVENT, api=ufp.api, camera_id=camera.id, start=fixed_now - timedelta(seconds=30), @@ -232,6 +234,7 @@ async def test_video_bad_perms( await init_entry(hass, ufp, [camera]) event = Event( + model=ModelType.EVENT, api=ufp.api, camera_id=camera.id, start=fixed_now - timedelta(seconds=30), @@ -260,6 +263,7 @@ async def test_video_bad_nvr_id( await init_entry(hass, ufp, [camera]) event = Event( + model=ModelType.EVENT, api=ufp.api, camera_id=camera.id, start=fixed_now - timedelta(seconds=30), @@ -294,6 +298,7 @@ async def test_video_bad_camera_id( await init_entry(hass, ufp, [camera]) event = Event( + model=ModelType.EVENT, api=ufp.api, camera_id=camera.id, start=fixed_now - timedelta(seconds=30), @@ -328,6 +333,7 @@ async def test_video_bad_camera_perms( await init_entry(hass, ufp, [camera]) event = Event( + model=ModelType.EVENT, api=ufp.api, camera_id=camera.id, start=fixed_now - timedelta(seconds=30), @@ -368,6 +374,7 @@ async def test_video_bad_params( event_start = fixed_now - timedelta(seconds=30) event = Event( + model=ModelType.EVENT, api=ufp.api, camera_id=camera.id, start=event_start, @@ -405,6 +412,7 @@ async def test_video_bad_video( event_start = fixed_now - timedelta(seconds=30) event = Event( + model=ModelType.EVENT, api=ufp.api, camera_id=camera.id, start=event_start, @@ -447,6 +455,7 @@ async def test_video( event_start = fixed_now - timedelta(seconds=30) event = Event( + model=ModelType.EVENT, api=ufp.api, camera_id=camera.id, start=event_start, @@ -490,6 +499,7 @@ async def test_video_entity_id( event_start = fixed_now - timedelta(seconds=30) event = Event( + model=ModelType.EVENT, api=ufp.api, camera_id=camera.id, start=event_start, From 09aa9cf84239d93965f02a9792db3b63a2ea70e6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 13 Jun 2024 16:31:39 -0500 Subject: [PATCH 0663/1445] Soften unifiprotect EA channel message (#119641) --- homeassistant/components/unifiprotect/__init__.py | 6 +++++- homeassistant/components/unifiprotect/strings.json | 2 +- tests/components/unifiprotect/test_repairs.py | 6 +++--- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/unifiprotect/__init__.py b/homeassistant/components/unifiprotect/__init__.py index eab4cc29737..e1e5f977c3d 100644 --- a/homeassistant/components/unifiprotect/__init__.py +++ b/homeassistant/components/unifiprotect/__init__.py @@ -53,6 +53,10 @@ SCAN_INTERVAL = timedelta(seconds=DEFAULT_SCAN_INTERVAL) CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN) +EARLY_ACCESS_URL = ( + "https://www.home-assistant.io/integrations/unifiprotect#software-support" +) + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the UniFi Protect.""" @@ -122,7 +126,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: UFPConfigEntry) -> bool: "ea_channel_warning", is_fixable=True, is_persistent=True, - learn_more_url="https://www.home-assistant.io/integrations/unifiprotect#about-unifi-early-access", + learn_more_url=EARLY_ACCESS_URL, severity=IssueSeverity.WARNING, translation_key="ea_channel_warning", translation_placeholders={"version": str(nvr_info.version)}, diff --git a/homeassistant/components/unifiprotect/strings.json b/homeassistant/components/unifiprotect/strings.json index bac7eaa5bf3..54023a1768f 100644 --- a/homeassistant/components/unifiprotect/strings.json +++ b/homeassistant/components/unifiprotect/strings.json @@ -67,7 +67,7 @@ "step": { "start": { "title": "UniFi Protect Early Access enabled", - "description": "You are either running an Early Access version of UniFi Protect (v{version}) or opt-ed into a release channel that is not the Official Release Channel. [Home Assistant does not support Early Access versions](https://www.home-assistant.io/integrations/unifiprotect#about-unifi-early-access), so you should immediately switch to the Official Release Channel. Accidentally upgrading to an Early Access version can break your UniFi Protect integration.\n\nBy submitting this form, you have switched back to the Official Release Channel or agree to run an unsupported version of UniFi Protect, which may break your Home Assistant integration at any time." + "description": "You are either running an Early Access version of UniFi Protect (v{version}) or opt-ed into a release channel that is not the Official Release Channel.\n\nAs these Early Access releases may not be tested yet, using it may cause the UniFi Protect integration to behave unexpectedly. [Read more about Early Access and Home Assistant]({learn_more}).\n\nSubmit to dismiss this message." }, "confirm": { "title": "[%key:component::unifiprotect::issues::ea_channel_warning::fix_flow::step::start::title%]", diff --git a/tests/components/unifiprotect/test_repairs.py b/tests/components/unifiprotect/test_repairs.py index 51ffd4d23cb..bdfcd6ff475 100644 --- a/tests/components/unifiprotect/test_repairs.py +++ b/tests/components/unifiprotect/test_repairs.py @@ -61,7 +61,7 @@ async def test_ea_warning_ignore( flow_id = data["flow_id"] assert data["description_placeholders"] == { - "learn_more": "https://www.home-assistant.io/integrations/unifiprotect#about-unifi-early-access", + "learn_more": "https://www.home-assistant.io/integrations/unifiprotect#software-support", "version": str(version), } assert data["step_id"] == "start" @@ -73,7 +73,7 @@ async def test_ea_warning_ignore( flow_id = data["flow_id"] assert data["description_placeholders"] == { - "learn_more": "https://www.home-assistant.io/integrations/unifiprotect#about-unifi-early-access", + "learn_more": "https://www.home-assistant.io/integrations/unifiprotect#software-support", "version": str(version), } assert data["step_id"] == "confirm" @@ -123,7 +123,7 @@ async def test_ea_warning_fix( flow_id = data["flow_id"] assert data["description_placeholders"] == { - "learn_more": "https://www.home-assistant.io/integrations/unifiprotect#about-unifi-early-access", + "learn_more": "https://www.home-assistant.io/integrations/unifiprotect#software-support", "version": str(version), } assert data["step_id"] == "start" From cd80b9b3187c866fd6bdd01bb3a009046e200788 Mon Sep 17 00:00:00 2001 From: dougiteixeira <31328123+dougiteixeira@users.noreply.github.com> Date: Thu, 13 Jun 2024 18:44:13 -0300 Subject: [PATCH 0664/1445] Remove obsolete device links in Utility Meter helper (#119328) * Make sure we update the links between the devices and config entries when the changes source device --- .../components/utility_meter/__init__.py | 45 ++++++---- tests/components/utility_meter/test_init.py | 89 ++++++++++++++++++- 2 files changed, 118 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/utility_meter/__init__.py b/homeassistant/components/utility_meter/__init__.py index 4bacde32367..c579a684406 100644 --- a/homeassistant/components/utility_meter/__init__.py +++ b/homeassistant/components/utility_meter/__init__.py @@ -191,6 +191,11 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Utility Meter from a config entry.""" + + await async_remove_stale_device_links( + hass, entry.entry_id, entry.options[CONF_SOURCE_SENSOR] + ) + entity_registry = er.async_get(hass) hass.data[DATA_UTILITY][entry.entry_id] = { "source": entry.options[CONF_SOURCE_SENSOR], @@ -230,23 +235,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def config_entry_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: """Update listener, called when the config entry options are changed.""" - old_source = hass.data[DATA_UTILITY][entry.entry_id]["source"] + await hass.config_entries.async_reload(entry.entry_id) - if old_source == entry.options[CONF_SOURCE_SENSOR]: - return - - entity_registry = er.async_get(hass) - device_registry = dr.async_get(hass) - - old_source_entity = entity_registry.async_get(old_source) - if not old_source_entity or not old_source_entity.device_id: - return - - device_registry.async_update_device( - old_source_entity.device_id, remove_config_entry_id=entry.entry_id - ) - async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" @@ -275,3 +266,27 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> _LOGGER.info("Migration to version %s successful", config_entry.version) return True + + +async def async_remove_stale_device_links( + hass: HomeAssistant, entry_id: str, entity_id: str +) -> None: + """Remove device link for entry, the source device may have changed.""" + + device_registry = dr.async_get(hass) + entity_registry = er.async_get(hass) + + # Resolve source entity device + current_device_id = None + if ((source_entity := entity_registry.async_get(entity_id)) is not None) and ( + source_entity.device_id is not None + ): + current_device_id = source_entity.device_id + + devices_in_entry = device_registry.devices.get_devices_for_config_entry_id(entry_id) + + # Removes all devices from the config entry that are not the same as the current device + for device in devices_in_entry: + if device.id == current_device_id: + continue + device_registry.async_update_device(device.id, remove_config_entry_id=entry_id) diff --git a/tests/components/utility_meter/test_init.py b/tests/components/utility_meter/test_init.py index 5e000076fdc..77d223454ec 100644 --- a/tests/components/utility_meter/test_init.py +++ b/tests/components/utility_meter/test_init.py @@ -24,7 +24,7 @@ from homeassistant.const import ( UnitOfEnergy, ) from homeassistant.core import HomeAssistant, State -from homeassistant.helpers import entity_registry as er +from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util @@ -442,3 +442,90 @@ async def test_setup_and_remove_config_entry( # Check the state and entity registry entry are removed assert len(hass.states.async_all()) == 0 assert len(entity_registry.entities) == 0 + + +async def test_device_cleaning(hass: HomeAssistant) -> None: + """Test for source entity device for Utility Meter.""" + device_registry = dr.async_get(hass) + entity_registry = er.async_get(hass) + + # Source entity device config entry + source_config_entry = MockConfigEntry() + source_config_entry.add_to_hass(hass) + + # Device entry of the source entity + source_device1_entry = device_registry.async_get_or_create( + config_entry_id=source_config_entry.entry_id, + identifiers={("sensor", "identifier_test1")}, + connections={("mac", "30:31:32:33:34:01")}, + ) + + # Source entity registry + source_entity = entity_registry.async_get_or_create( + "sensor", + "test", + "source", + config_entry=source_config_entry, + device_id=source_device1_entry.id, + ) + await hass.async_block_till_done() + assert entity_registry.async_get("sensor.test_source") is not None + + # Configure the configuration entry for Utility Meter + utility_meter_config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={ + "cycle": "monthly", + "delta_values": False, + "name": "Meter", + "net_consumption": False, + "offset": 0, + "periodically_resetting": True, + "source": "sensor.test_source", + "tariffs": [], + }, + title="Meter", + ) + utility_meter_config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(utility_meter_config_entry.entry_id) + await hass.async_block_till_done() + + # Confirm the link between the source entity device and the meter sensor + utility_meter_entity = entity_registry.async_get("sensor.meter") + assert utility_meter_entity is not None + assert utility_meter_entity.device_id == source_entity.device_id + + # Device entry incorrectly linked to Utility Meter config entry + device_registry.async_get_or_create( + config_entry_id=utility_meter_config_entry.entry_id, + identifiers={("sensor", "identifier_test2")}, + connections={("mac", "30:31:32:33:34:02")}, + ) + device_registry.async_get_or_create( + config_entry_id=utility_meter_config_entry.entry_id, + identifiers={("sensor", "identifier_test3")}, + connections={("mac", "30:31:32:33:34:03")}, + ) + await hass.async_block_till_done() + + # Before reloading the config entry, two devices are expected to be linked + devices_before_reload = device_registry.devices.get_devices_for_config_entry_id( + utility_meter_config_entry.entry_id + ) + assert len(devices_before_reload) == 3 + + # Config entry reload + await hass.config_entries.async_reload(utility_meter_config_entry.entry_id) + await hass.async_block_till_done() + + # Confirm the link between the source entity device and the meter sensor after reload + utility_meter_entity = entity_registry.async_get("sensor.meter") + assert utility_meter_entity is not None + assert utility_meter_entity.device_id == source_entity.device_id + + # After reloading the config entry, only one linked device is expected + devices_after_reload = device_registry.devices.get_devices_for_config_entry_id( + utility_meter_config_entry.entry_id + ) + assert len(devices_after_reload) == 1 From a992654a8b7247f2d4c6ce33501fdcbdd13890dd Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Fri, 14 Jun 2024 00:47:38 +0200 Subject: [PATCH 0665/1445] Fix blocking IO calls in mqtt client setup (#119647) --- homeassistant/components/mqtt/async_client.py | 2 +- homeassistant/components/mqtt/client.py | 31 +++++++++++++++---- homeassistant/components/mqtt/config_flow.py | 4 ++- 3 files changed, 29 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/mqtt/async_client.py b/homeassistant/components/mqtt/async_client.py index c0b847f35a1..882e910d7e8 100644 --- a/homeassistant/components/mqtt/async_client.py +++ b/homeassistant/components/mqtt/async_client.py @@ -44,7 +44,7 @@ class AsyncMQTTClient(MQTTClient): that is not needed since we are running in an async event loop. """ - def async_setup(self) -> None: + def setup(self) -> None: """Set up the client. All the threading locks are replaced with NullLock diff --git a/homeassistant/components/mqtt/client.py b/homeassistant/components/mqtt/client.py index 13f33c44047..ace2293e7a6 100644 --- a/homeassistant/components/mqtt/client.py +++ b/homeassistant/components/mqtt/client.py @@ -277,9 +277,22 @@ class Subscription: class MqttClientSetup: """Helper class to setup the paho mqtt client from config.""" - def __init__(self, config: ConfigType) -> None: - """Initialize the MQTT client setup helper.""" + _client: AsyncMQTTClient + def __init__(self, config: ConfigType) -> None: + """Initialize the MQTT client setup helper. + + self.setup must be run in an executor job. + """ + + self._config = config + + def setup(self) -> None: + """Set up the MQTT client. + + The setup of the MQTT client should be run in an executor job, + because it accesses files, so it does IO. + """ # We don't import on the top because some integrations # should be able to optionally rely on MQTT. import paho.mqtt.client as mqtt # pylint: disable=import-outside-toplevel @@ -287,6 +300,7 @@ class MqttClientSetup: # pylint: disable-next=import-outside-toplevel from .async_client import AsyncMQTTClient + config = self._config if (protocol := config.get(CONF_PROTOCOL, DEFAULT_PROTOCOL)) == PROTOCOL_31: proto = mqtt.MQTTv31 elif protocol == PROTOCOL_5: @@ -298,11 +312,14 @@ class MqttClientSetup: # PAHO MQTT relies on the MQTT server to generate random client IDs. # However, that feature is not mandatory so we generate our own. client_id = mqtt.base62(uuid.uuid4().int, padding=22) - transport = config.get(CONF_TRANSPORT, DEFAULT_TRANSPORT) + transport: str = config.get(CONF_TRANSPORT, DEFAULT_TRANSPORT) self._client = AsyncMQTTClient( - client_id, protocol=proto, transport=transport, reconnect_on_failure=False + client_id, + protocol=proto, + transport=transport, + reconnect_on_failure=False, ) - self._client.async_setup() + self._client.setup() # Enable logging self._client.enable_logger() @@ -544,7 +561,9 @@ class MQTT: self.hass, "homeassistant.components.mqtt.async_client" ) - mqttc = MqttClientSetup(self.conf).client + mqttc_setup = MqttClientSetup(self.conf) + await self.hass.async_add_executor_job(mqttc_setup.setup) + mqttc = mqttc_setup.client # on_socket_unregister_write and _async_on_socket_close # are only ever called in the event loop mqttc.on_socket_close = self._async_on_socket_close diff --git a/homeassistant/components/mqtt/config_flow.py b/homeassistant/components/mqtt/config_flow.py index 2c5d921e1db..17dfc6512b3 100644 --- a/homeassistant/components/mqtt/config_flow.py +++ b/homeassistant/components/mqtt/config_flow.py @@ -834,7 +834,9 @@ def try_connection( # should be able to optionally rely on MQTT. import paho.mqtt.client as mqtt # pylint: disable=import-outside-toplevel - client = MqttClientSetup(user_input).client + mqtt_client_setup = MqttClientSetup(user_input) + mqtt_client_setup.setup() + client = mqtt_client_setup.client result: queue.Queue[bool] = queue.Queue(maxsize=1) From 87ddb02828ad871d4c4815b84c8b4a2d86df8b9c Mon Sep 17 00:00:00 2001 From: tronikos Date: Thu, 13 Jun 2024 17:41:37 -0700 Subject: [PATCH 0666/1445] Bump python-fullykiosk to 0.0.13 (#119544) Co-authored-by: Robert Resch --- homeassistant/components/fully_kiosk/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/fully_kiosk/manifest.json b/homeassistant/components/fully_kiosk/manifest.json index b5dadf14184..8d9ba85a058 100644 --- a/homeassistant/components/fully_kiosk/manifest.json +++ b/homeassistant/components/fully_kiosk/manifest.json @@ -12,5 +12,5 @@ "documentation": "https://www.home-assistant.io/integrations/fully_kiosk", "iot_class": "local_polling", "mqtt": ["fully/deviceInfo/+"], - "requirements": ["python-fullykiosk==0.0.12"] + "requirements": ["python-fullykiosk==0.0.13"] } diff --git a/requirements_all.txt b/requirements_all.txt index b7fccc8d6f2..898563714ed 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2245,7 +2245,7 @@ python-etherscan-api==0.0.3 python-family-hub-local==0.0.2 # homeassistant.components.fully_kiosk -python-fullykiosk==0.0.12 +python-fullykiosk==0.0.13 # homeassistant.components.sms # python-gammu==3.2.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index be404d99447..ab19663746f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1757,7 +1757,7 @@ python-bsblan==0.5.18 python-ecobee-api==0.2.18 # homeassistant.components.fully_kiosk -python-fullykiosk==0.0.12 +python-fullykiosk==0.0.13 # homeassistant.components.sms # python-gammu==3.2.4 From efa7240ac50f1aa1967cc31fd32eb6367a022631 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Fri, 14 Jun 2024 04:27:40 +0300 Subject: [PATCH 0667/1445] Use single list for Shelly non-sleeping platforms (#119540) --- homeassistant/components/shelly/__init__.py | 33 +++++++-------------- 1 file changed, 10 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index cc1ea5e81a6..aae0d560810 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -54,9 +54,10 @@ from .utils import ( get_ws_context, ) -BLOCK_PLATFORMS: Final = [ +PLATFORMS: Final = [ Platform.BINARY_SENSOR, Platform.BUTTON, + Platform.CLIMATE, Platform.COVER, Platform.EVENT, Platform.LIGHT, @@ -72,17 +73,6 @@ BLOCK_SLEEPING_PLATFORMS: Final = [ Platform.SENSOR, Platform.SWITCH, ] -RPC_PLATFORMS: Final = [ - Platform.BINARY_SENSOR, - Platform.BUTTON, - Platform.CLIMATE, - Platform.COVER, - Platform.EVENT, - Platform.LIGHT, - Platform.SENSOR, - Platform.SWITCH, - Platform.UPDATE, -] RPC_SLEEPING_PLATFORMS: Final = [ Platform.BINARY_SENSOR, Platform.SENSOR, @@ -194,7 +184,7 @@ async def _async_setup_block_entry( shelly_entry_data.block = ShellyBlockCoordinator(hass, entry, device) shelly_entry_data.block.async_setup() shelly_entry_data.rest = ShellyRestCoordinator(hass, device, entry) - await hass.config_entries.async_forward_entry_setups(entry, BLOCK_PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) elif sleep_period is None or device_entry is None: # Need to get sleep info or first time sleeping device setup, wait for device LOGGER.debug( @@ -264,7 +254,7 @@ async def _async_setup_rpc_entry(hass: HomeAssistant, entry: ShellyConfigEntry) shelly_entry_data.rpc = ShellyRpcCoordinator(hass, entry, device) shelly_entry_data.rpc.async_setup() shelly_entry_data.rpc_poll = ShellyRpcPollingCoordinator(hass, entry, device) - await hass.config_entries.async_forward_entry_setups(entry, RPC_PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) elif sleep_period is None or device_entry is None: # Need to get sleep info or first time sleeping device setup, wait for device LOGGER.debug( @@ -290,12 +280,12 @@ async def _async_setup_rpc_entry(hass: HomeAssistant, entry: ShellyConfigEntry) async def async_unload_entry(hass: HomeAssistant, entry: ShellyConfigEntry) -> bool: """Unload a config entry.""" shelly_entry_data = entry.runtime_data - - platforms = RPC_SLEEPING_PLATFORMS - if not entry.data.get(CONF_SLEEP_PERIOD): - platforms = RPC_PLATFORMS + platforms = PLATFORMS if get_device_entry_gen(entry) in RPC_GENERATIONS: + if entry.data.get(CONF_SLEEP_PERIOD): + platforms = RPC_SLEEPING_PLATFORMS + if unload_ok := await hass.config_entries.async_unload_platforms( entry, platforms ): @@ -312,11 +302,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ShellyConfigEntry) -> b hass, DOMAIN, PUSH_UPDATE_ISSUE_ID.format(unique=entry.unique_id) ) - platforms = BLOCK_SLEEPING_PLATFORMS - - if not entry.data.get(CONF_SLEEP_PERIOD): - shelly_entry_data.rest = None - platforms = BLOCK_PLATFORMS + if entry.data.get(CONF_SLEEP_PERIOD): + platforms = BLOCK_SLEEPING_PLATFORMS if unload_ok := await hass.config_entries.async_unload_platforms(entry, platforms): if shelly_entry_data.block: From b11d832fb129f5f5632bf01553fa6ac9f7a83de3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 13 Jun 2024 21:01:25 -0500 Subject: [PATCH 0668/1445] Bump uiprotect to 1.4.1 (#119653) --- homeassistant/components/unifiprotect/manifest.json | 2 +- homeassistant/components/unifiprotect/utils.py | 6 ++---- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 5 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index f7b3a4bde70..57589c44f85 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -40,7 +40,7 @@ "integration_type": "hub", "iot_class": "local_push", "loggers": ["uiprotect", "unifi_discovery"], - "requirements": ["uiprotect==1.2.1", "unifi-discovery==1.1.8"], + "requirements": ["uiprotect==1.4.1", "unifi-discovery==1.1.8"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/homeassistant/components/unifiprotect/utils.py b/homeassistant/components/unifiprotect/utils.py index ad4c99379c8..c509558c9c2 100644 --- a/homeassistant/components/unifiprotect/utils.py +++ b/homeassistant/components/unifiprotect/utils.py @@ -92,10 +92,8 @@ def async_get_devices_by_type( bootstrap: Bootstrap, device_type: ModelType ) -> dict[str, ProtectAdoptableDeviceModel]: """Get devices by type.""" - - devices: dict[str, ProtectAdoptableDeviceModel] = getattr( - bootstrap, f"{device_type.value}s" - ) + devices: dict[str, ProtectAdoptableDeviceModel] + devices = getattr(bootstrap, device_type.devices_key) return devices diff --git a/requirements_all.txt b/requirements_all.txt index 898563714ed..93eaa28e108 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2788,7 +2788,7 @@ twitchAPI==4.0.0 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==1.2.1 +uiprotect==1.4.1 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ab19663746f..de1b04e9f33 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2168,7 +2168,7 @@ twitchAPI==4.0.0 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==1.2.1 +uiprotect==1.4.1 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 From 95b9e15306e051dfdbb2e2b09348c3829790ed9d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 13 Jun 2024 23:34:55 -0500 Subject: [PATCH 0669/1445] Bump uiprotect to 1.6.0 (#119661) --- homeassistant/components/unifiprotect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index 57589c44f85..181f87b4469 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -40,7 +40,7 @@ "integration_type": "hub", "iot_class": "local_push", "loggers": ["uiprotect", "unifi_discovery"], - "requirements": ["uiprotect==1.4.1", "unifi-discovery==1.1.8"], + "requirements": ["uiprotect==1.6.0", "unifi-discovery==1.1.8"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/requirements_all.txt b/requirements_all.txt index 93eaa28e108..62f2ed4be16 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2788,7 +2788,7 @@ twitchAPI==4.0.0 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==1.4.1 +uiprotect==1.6.0 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index de1b04e9f33..f897506d6b6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2168,7 +2168,7 @@ twitchAPI==4.0.0 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==1.4.1 +uiprotect==1.6.0 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 From 097844aca6d95f9044af0b9c3dc3bdbb5903c0ae Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Fri, 14 Jun 2024 07:18:57 +0200 Subject: [PATCH 0670/1445] Allow arm levels be in order for google assistant (#119645) --- .../components/google_assistant/trait.py | 13 ++++++----- .../components/google_assistant/test_trait.py | 22 ++++++++++++++----- 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index a640e3a52af..05d18f1e45b 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -1553,19 +1553,20 @@ class ArmDisArmTrait(_Trait): state_to_service = { STATE_ALARM_ARMED_HOME: SERVICE_ALARM_ARM_HOME, - STATE_ALARM_ARMED_AWAY: SERVICE_ALARM_ARM_AWAY, STATE_ALARM_ARMED_NIGHT: SERVICE_ALARM_ARM_NIGHT, + STATE_ALARM_ARMED_AWAY: SERVICE_ALARM_ARM_AWAY, STATE_ALARM_ARMED_CUSTOM_BYPASS: SERVICE_ALARM_ARM_CUSTOM_BYPASS, STATE_ALARM_TRIGGERED: SERVICE_ALARM_TRIGGER, } state_to_support = { STATE_ALARM_ARMED_HOME: AlarmControlPanelEntityFeature.ARM_HOME, - STATE_ALARM_ARMED_AWAY: AlarmControlPanelEntityFeature.ARM_AWAY, STATE_ALARM_ARMED_NIGHT: AlarmControlPanelEntityFeature.ARM_NIGHT, + STATE_ALARM_ARMED_AWAY: AlarmControlPanelEntityFeature.ARM_AWAY, STATE_ALARM_ARMED_CUSTOM_BYPASS: AlarmControlPanelEntityFeature.ARM_CUSTOM_BYPASS, STATE_ALARM_TRIGGERED: AlarmControlPanelEntityFeature.TRIGGER, } + """The list of states to support in increasing security state.""" @staticmethod def supported(domain, features, device_class, _): @@ -1592,7 +1593,7 @@ class ArmDisArmTrait(_Trait): if STATE_ALARM_TRIGGERED in states: states.remove(STATE_ALARM_TRIGGERED) - if len(states) != 1: + if not states: raise SmartHomeError(ERR_NOT_SUPPORTED, "ArmLevel missing") return states[0] @@ -1614,7 +1615,7 @@ class ArmDisArmTrait(_Trait): } levels.append(level) - response["availableArmLevels"] = {"levels": levels, "ordered": False} + response["availableArmLevels"] = {"levels": levels, "ordered": True} return response def query_attributes(self): @@ -1631,8 +1632,8 @@ class ArmDisArmTrait(_Trait): async def execute(self, command, data, params, challenge): """Execute an ArmDisarm command.""" if params["arm"] and not params.get("cancel"): - # If no arm level given, we can only arm it if there is - # only one supported arm type. We never default to triggered. + # If no arm level given, we we arm the first supported + # level in state_to_support. if not (arm_level := params.get("armLevel")): arm_level = self._default_arm_state() diff --git a/tests/components/google_assistant/test_trait.py b/tests/components/google_assistant/test_trait.py index 038b16d0cfc..63a34c01dac 100644 --- a/tests/components/google_assistant/test_trait.py +++ b/tests/components/google_assistant/test_trait.py @@ -1763,7 +1763,7 @@ async def test_arm_disarm_arm_away(hass: HomeAssistant) -> None: ], }, ], - "ordered": False, + "ordered": True, } } @@ -1905,7 +1905,8 @@ async def test_arm_disarm_disarm(hass: HomeAssistant) -> None: { alarm_control_panel.ATTR_CODE_ARM_REQUIRED: True, ATTR_SUPPORTED_FEATURES: AlarmControlPanelEntityFeature.TRIGGER - | AlarmControlPanelEntityFeature.ARM_CUSTOM_BYPASS, + | AlarmControlPanelEntityFeature.ARM_HOME + | AlarmControlPanelEntityFeature.ARM_AWAY, }, ), PIN_CONFIG, @@ -1914,10 +1915,19 @@ async def test_arm_disarm_disarm(hass: HomeAssistant) -> None: "availableArmLevels": { "levels": [ { - "level_name": "armed_custom_bypass", + "level_name": "armed_home", "level_values": [ { - "level_synonym": ["armed custom bypass", "custom"], + "level_synonym": ["armed home", "home"], + "lang": "en", + } + ], + }, + { + "level_name": "armed_away", + "level_values": [ + { + "level_synonym": ["armed away", "away"], "lang": "en", } ], @@ -1927,12 +1937,12 @@ async def test_arm_disarm_disarm(hass: HomeAssistant) -> None: "level_values": [{"level_synonym": ["triggered"], "lang": "en"}], }, ], - "ordered": False, + "ordered": True, } } assert trt.query_attributes() == { - "currentArmLevel": "armed_custom_bypass", + "currentArmLevel": "armed_home", "isArmed": False, } From 3336bdb4026f87e4d9676acba682c85033f2ac14 Mon Sep 17 00:00:00 2001 From: Matrix Date: Fri, 14 Jun 2024 13:31:02 +0800 Subject: [PATCH 0671/1445] Fix Yolink device incorrect state (#119658) fix device incorrect state --- homeassistant/components/yolink/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/yolink/__init__.py b/homeassistant/components/yolink/__init__.py index fec678ce435..004c5a70cc1 100644 --- a/homeassistant/components/yolink/__init__.py +++ b/homeassistant/components/yolink/__init__.py @@ -66,9 +66,12 @@ class YoLinkHomeMessageListener(MessageListener): device_coordinators = entry_data.device_coordinators if not device_coordinators: return - device_coordinator = device_coordinators.get(device.device_id) + device_coordinator: YoLinkCoordinator = device_coordinators.get( + device.device_id + ) if device_coordinator is None: return + device_coordinator.dev_online = True device_coordinator.async_set_updated_data(msg_data) # handling events if ( From 9e146a51c203f94bb74ac105353678acb2802e92 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Fri, 14 Jun 2024 07:46:24 +0200 Subject: [PATCH 0672/1445] Fix group enabled platforms are preloaded if they have alternative states (#119621) --- homeassistant/components/group/manifest.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/group/manifest.json b/homeassistant/components/group/manifest.json index d86fc4ba622..a2045f370b1 100644 --- a/homeassistant/components/group/manifest.json +++ b/homeassistant/components/group/manifest.json @@ -4,7 +4,10 @@ "after_dependencies": [ "alarm_control_panel", "climate", + "cover", "device_tracker", + "lock", + "media_player", "person", "plant", "vacuum", From 471e2a17a2846f63ab975eb46186cc38a97ce22a Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 14 Jun 2024 08:00:36 +0200 Subject: [PATCH 0673/1445] Improve error messages when config entry is in wrong state (#119591) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Improve error messages when config entry is in wrong state * Apply suggestions from code review Co-authored-by: Joakim Sørensen * Adjust tests --------- Co-authored-by: Joakim Sørensen --- homeassistant/config_entries.py | 26 ++++++++++++++------------ tests/test_config_entries.py | 14 ++++++++------ 2 files changed, 22 insertions(+), 18 deletions(-) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index fdcf4ad7604..c8d671e1fe1 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -1812,9 +1812,9 @@ class ConfigEntries: if entry.state is not ConfigEntryState.NOT_LOADED: raise OperationNotAllowed( - f"The config entry {entry.title} ({entry.domain}) with entry_id" - f" {entry.entry_id} cannot be set up because it is already loaded" - f" in the {entry.state} state" + f"The config entry '{entry.title}' ({entry.domain}) with entry_id" + f" '{entry.entry_id}' cannot be set up because it is in state " + f"{entry.state}, but needs to be in the {ConfigEntryState.NOT_LOADED} state" ) # Setup Component if not set up yet @@ -1844,9 +1844,9 @@ class ConfigEntries: if not entry.state.recoverable: raise OperationNotAllowed( - f"The config entry {entry.title} ({entry.domain}) with entry_id" - f" {entry.entry_id} cannot be unloaded because it is not in a" - f" recoverable state ({entry.state})" + f"The config entry '{entry.title}' ({entry.domain}) with entry_id" + f" '{entry.entry_id}' cannot be unloaded because it is in the non" + f" recoverable state {entry.state}" ) if _lock: @@ -2049,9 +2049,10 @@ class ConfigEntries: async with entry.setup_lock: if entry.state is not ConfigEntryState.LOADED: raise OperationNotAllowed( - f"The config entry {entry.title} ({entry.domain}) with entry_id" - f" {entry.entry_id} cannot forward setup for {platforms} because it" - f" is not loaded in the {entry.state} state" + f"The config entry '{entry.title}' ({entry.domain}) with " + f"entry_id '{entry.entry_id}' cannot forward setup for " + f"{platforms} because it is in state {entry.state}, but needs " + f"to be in the {ConfigEntryState.LOADED} state" ) await self._async_forward_entry_setups_locked(entry, platforms) else: @@ -2108,9 +2109,10 @@ class ConfigEntries: async with entry.setup_lock: if entry.state is not ConfigEntryState.LOADED: raise OperationNotAllowed( - f"The config entry {entry.title} ({entry.domain}) with entry_id" - f" {entry.entry_id} cannot forward setup for {domain} because it" - f" is not loaded in the {entry.state} state" + f"The config entry '{entry.title}' ({entry.domain}) with " + f"entry_id '{entry.entry_id}' cannot forward setup for " + f"{domain} because it is in state {entry.state}, but needs " + f"to be in the {ConfigEntryState.LOADED} state" ) return await self._async_forward_entry_setup(entry, domain, True) result = await self._async_forward_entry_setup(entry, domain, True) diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index b23b247b7a3..cba7ad8f215 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -5606,9 +5606,10 @@ async def test_config_entry_unloaded_during_platform_setups( del task assert ( - "OperationNotAllowed: The config entry Mock Title (test) with " - "entry_id test2 cannot forward setup for ['light'] because it is " - "not loaded in the ConfigEntryState.NOT_LOADED state" + "OperationNotAllowed: The config entry 'Mock Title' (test) with " + "entry_id 'test2' cannot forward setup for ['light'] because it is " + "in state ConfigEntryState.NOT_LOADED, but needs to be in the " + "ConfigEntryState.LOADED state" ) in caplog.text @@ -5824,9 +5825,10 @@ async def test_config_entry_unloaded_during_platform_setup( del task assert ( - "OperationNotAllowed: The config entry Mock Title (test) with " - "entry_id test2 cannot forward setup for light because it is " - "not loaded in the ConfigEntryState.NOT_LOADED state" + "OperationNotAllowed: The config entry 'Mock Title' (test) with " + "entry_id 'test2' cannot forward setup for light because it is " + "in state ConfigEntryState.NOT_LOADED, but needs to be in the " + "ConfigEntryState.LOADED state" ) in caplog.text From 26e21bb3569a5badb9e522891bdc96a036293c02 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 14 Jun 2024 08:25:35 +0200 Subject: [PATCH 0674/1445] Adjust incorrect unnecessary-lambda pylint disable statement in demo tests (#119666) --- tests/components/demo/test_update.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/components/demo/test_update.py b/tests/components/demo/test_update.py index e8fe909541c..0a8886a085d 100644 --- a/tests/components/demo/test_update.py +++ b/tests/components/demo/test_update.py @@ -171,8 +171,8 @@ async def test_update_with_progress_raising(hass: HomeAssistant) -> None: events = [] async_track_state_change_event( hass, - # pylint: disable-next=unnecessary-lambda "update.demo_update_with_progress", + # pylint: disable-next=unnecessary-lambda callback(lambda event: events.append(event)), ) From 38a6e666a71b78f10e89fdb2dec34d5afe56949d Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 14 Jun 2024 08:26:45 +0200 Subject: [PATCH 0675/1445] Add missing return type to some test functions (#119665) --- tests/auth/providers/test_legacy_api_password.py | 2 +- tests/common.py | 2 +- tests/components/bluetooth/test_wrappers.py | 2 +- tests/components/enigma2/test_config_flow.py | 2 +- tests/components/homematicip_cloud/helper.py | 2 +- tests/components/knx/conftest.py | 2 +- tests/components/light/common.py | 2 +- tests/components/logbook/common.py | 2 +- tests/components/reddit/test_sensor.py | 4 ++-- tests/components/sonos/conftest.py | 2 +- tests/components/youtube/__init__.py | 2 +- tests/helpers/test_entity.py | 4 ++-- tests/helpers/test_template.py | 2 +- tests/helpers/test_translation.py | 4 ++-- tests/helpers/test_trigger.py | 4 +++- 15 files changed, 20 insertions(+), 18 deletions(-) diff --git a/tests/auth/providers/test_legacy_api_password.py b/tests/auth/providers/test_legacy_api_password.py index c8d32fbc59a..a9ef03fd27b 100644 --- a/tests/auth/providers/test_legacy_api_password.py +++ b/tests/auth/providers/test_legacy_api_password.py @@ -79,7 +79,7 @@ async def test_login_flow_works(hass: HomeAssistant, manager) -> None: async def test_create_repair_issue( hass: HomeAssistant, issue_registry: ir.IssueRegistry -): +) -> None: """Test legacy api password auth provider creates a reapir issue.""" hass.auth = await auth.auth_manager_from_config(hass, [CONFIG], []) ensure_auth_manager_loaded(hass.auth) diff --git a/tests/common.py b/tests/common.py index 24fb6cf458f..114e683fbfa 100644 --- a/tests/common.py +++ b/tests/common.py @@ -901,7 +901,7 @@ class MockEntityPlatform(entity_platform.EntityPlatform): platform=None, scan_interval=timedelta(seconds=15), entity_namespace=None, - ): + ) -> None: """Initialize a mock entity platform.""" if logger is None: logger = logging.getLogger("homeassistant.helpers.entity_platform") diff --git a/tests/components/bluetooth/test_wrappers.py b/tests/components/bluetooth/test_wrappers.py index 9c537079db7..0c5645b3f71 100644 --- a/tests/components/bluetooth/test_wrappers.py +++ b/tests/components/bluetooth/test_wrappers.py @@ -72,7 +72,7 @@ class FakeScanner(BaseHaRemoteScanner): class BaseFakeBleakClient: """Base class for fake bleak clients.""" - def __init__(self, address_or_ble_device: BLEDevice | str, **kwargs): + def __init__(self, address_or_ble_device: BLEDevice | str, **kwargs) -> None: """Initialize the fake bleak client.""" self._device_path = "/dev/test" self._device = address_or_ble_device diff --git a/tests/components/enigma2/test_config_flow.py b/tests/components/enigma2/test_config_flow.py index a1074ed9e34..74721ce0993 100644 --- a/tests/components/enigma2/test_config_flow.py +++ b/tests/components/enigma2/test_config_flow.py @@ -168,7 +168,7 @@ async def test_form_import_errors( assert result["reason"] == error_type -async def test_options_flow(hass: HomeAssistant, user_flow: str): +async def test_options_flow(hass: HomeAssistant, user_flow: str) -> None: """Test the form options.""" with patch( diff --git a/tests/components/homematicip_cloud/helper.py b/tests/components/homematicip_cloud/helper.py index f82880d3fa8..e7d7350f98e 100644 --- a/tests/components/homematicip_cloud/helper.py +++ b/tests/components/homematicip_cloud/helper.py @@ -77,7 +77,7 @@ class HomeFactory: hass: HomeAssistant, mock_connection, hmip_config_entry: config_entries.ConfigEntry, - ): + ) -> None: """Initialize the Factory.""" self.hass = hass self.mock_connection = mock_connection diff --git a/tests/components/knx/conftest.py b/tests/components/knx/conftest.py index 5cdeb0d8adb..cd7146b565b 100644 --- a/tests/components/knx/conftest.py +++ b/tests/components/knx/conftest.py @@ -44,7 +44,7 @@ class KNXTestKit: INDIVIDUAL_ADDRESS = "1.2.3" - def __init__(self, hass: HomeAssistant, mock_config_entry: MockConfigEntry): + def __init__(self, hass: HomeAssistant, mock_config_entry: MockConfigEntry) -> None: """Init KNX test helper class.""" self.hass: HomeAssistant = hass self.mock_config_entry: MockConfigEntry = mock_config_entry diff --git a/tests/components/light/common.py b/tests/components/light/common.py index fd9557b05b2..7c33c40ab63 100644 --- a/tests/components/light/common.py +++ b/tests/components/light/common.py @@ -254,7 +254,7 @@ class MockLight(MockToggleEntity, LightEntity): state, unique_id=None, supported_color_modes: set[ColorMode] | None = None, - ): + ) -> None: """Initialize the mock light.""" super().__init__(name, state, unique_id) if supported_color_modes is None: diff --git a/tests/components/logbook/common.py b/tests/components/logbook/common.py index 67b83a19768..67f12955581 100644 --- a/tests/components/logbook/common.py +++ b/tests/components/logbook/common.py @@ -27,7 +27,7 @@ class MockRow: event_type: str, data: dict[str, Any] | None = None, context: Context | None = None, - ): + ) -> None: """Init the fake row.""" self.event_type = event_type self.event_data = json.dumps(data, cls=JSONEncoder) diff --git a/tests/components/reddit/test_sensor.py b/tests/components/reddit/test_sensor.py index 92ee282e9c8..52dac07d621 100644 --- a/tests/components/reddit/test_sensor.py +++ b/tests/components/reddit/test_sensor.py @@ -111,7 +111,7 @@ class MockPraw: username: str, password: str, user_agent: str, - ): + ) -> None: """Add mock data for API return.""" self._data = MOCK_RESULTS @@ -123,7 +123,7 @@ class MockPraw: class MockSubreddit: """Mock class for a subreddit instance.""" - def __init__(self, subreddit: str, data): + def __init__(self, subreddit: str, data) -> None: """Add mock data for API return.""" self._subreddit = subreddit self._data = data diff --git a/tests/components/sonos/conftest.py b/tests/components/sonos/conftest.py index 478443fff76..c7f5cfb7223 100644 --- a/tests/components/sonos/conftest.py +++ b/tests/components/sonos/conftest.py @@ -323,7 +323,7 @@ class MockMusicServiceItem: parent_id: str, item_class: str, album_art_uri: None | str = None, - ): + ) -> None: """Initialize the mock item.""" self.title = title self.item_id = item_id diff --git a/tests/components/youtube/__init__.py b/tests/components/youtube/__init__.py index 8f6da97481a..1b559f0f1c4 100644 --- a/tests/components/youtube/__init__.py +++ b/tests/components/youtube/__init__.py @@ -19,7 +19,7 @@ class MockYouTube: channel_fixture: str = "youtube/get_channel.json", playlist_items_fixture: str = "youtube/get_playlist_items.json", subscriptions_fixture: str = "youtube/get_subscriptions.json", - ): + ) -> None: """Initialize mock service.""" self._channel_fixture = channel_fixture self._playlist_items_fixture = playlist_items_fixture diff --git a/tests/helpers/test_entity.py b/tests/helpers/test_entity.py index a8524d73a5d..cc53bca8e4d 100644 --- a/tests/helpers/test_entity.py +++ b/tests/helpers/test_entity.py @@ -1873,7 +1873,7 @@ async def test_change_entity_id( assert len(ent.remove_calls) == 2 -def test_entity_description_as_dataclass(snapshot: SnapshotAssertion): +def test_entity_description_as_dataclass(snapshot: SnapshotAssertion) -> None: """Test EntityDescription behaves like a dataclass.""" obj = entity.EntityDescription("blah", device_class="test") @@ -1888,7 +1888,7 @@ def test_entity_description_as_dataclass(snapshot: SnapshotAssertion): assert repr(obj) == snapshot -def test_extending_entity_description(snapshot: SnapshotAssertion): +def test_extending_entity_description(snapshot: SnapshotAssertion) -> None: """Test extending entity descriptions.""" @dataclasses.dataclass(frozen=True) diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index 6b75ff384b6..0547ddf8823 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -2067,7 +2067,7 @@ def test_states_function(hass: HomeAssistant) -> None: async def test_state_translated( hass: HomeAssistant, entity_registry: er.EntityRegistry -): +) -> None: """Test state_translated method.""" assert await async_setup_component( hass, diff --git a/tests/helpers/test_translation.py b/tests/helpers/test_translation.py index dfe96562a4a..da81016e153 100644 --- a/tests/helpers/test_translation.py +++ b/tests/helpers/test_translation.py @@ -549,7 +549,7 @@ async def test_get_cached_translations( } -async def test_setup(hass: HomeAssistant): +async def test_setup(hass: HomeAssistant) -> None: """Test the setup load listeners helper.""" translation.async_setup(hass) @@ -577,7 +577,7 @@ async def test_setup(hass: HomeAssistant): mock.assert_not_called() -async def test_translate_state(hass: HomeAssistant): +async def test_translate_state(hass: HomeAssistant) -> None: """Test the state translation helper.""" result = translation.async_translate_state( hass, "unavailable", "binary_sensor", "platform", "translation_key", None diff --git a/tests/helpers/test_trigger.py b/tests/helpers/test_trigger.py index e8322c7e660..0bd5da0707c 100644 --- a/tests/helpers/test_trigger.py +++ b/tests/helpers/test_trigger.py @@ -287,7 +287,9 @@ async def test_async_initialize_triggers( unsub() -async def test_pluggable_action(hass: HomeAssistant, service_calls: list[ServiceCall]): +async def test_pluggable_action( + hass: HomeAssistant, service_calls: list[ServiceCall] +) -> None: """Test normal behavior of pluggable actions.""" update_1 = MagicMock() update_2 = MagicMock() From e6b73013672fd56f9f1d3999f77170d951bf8bbe Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 14 Jun 2024 01:27:50 -0500 Subject: [PATCH 0676/1445] Fix blocking I/O in CachingStaticResource (#119663) --- homeassistant/components/http/static.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/http/static.py b/homeassistant/components/http/static.py index b7bb9d4f3a8..a7280fb9b2f 100644 --- a/homeassistant/components/http/static.py +++ b/homeassistant/components/http/static.py @@ -80,4 +80,4 @@ class CachingStaticResource(StaticResource): }, ) - return await super()._handle(request) + raise HTTPForbidden if filepath is None else HTTPNotFound From 4b29c354535752aa35b3f5141a7552f0ebdddd03 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 14 Jun 2024 08:28:47 +0200 Subject: [PATCH 0677/1445] Tweak logging statements in tests (#119664) --- tests/components/litejet/test_trigger.py | 4 +- tests/components/system_log/test_init.py | 70 ++++++++++++------------ 2 files changed, 37 insertions(+), 37 deletions(-) diff --git a/tests/components/litejet/test_trigger.py b/tests/components/litejet/test_trigger.py index 96dc3c78487..216084c26bc 100644 --- a/tests/components/litejet/test_trigger.py +++ b/tests/components/litejet/test_trigger.py @@ -72,10 +72,10 @@ async def simulate_time(hass, mock_litejet, delta): "homeassistant.helpers.condition.dt_util.utcnow", return_value=mock_litejet.start_time + delta, ): - _LOGGER.info("now=%s", dt_util.utcnow()) + _LOGGER.info("*** now=%s", dt_util.utcnow()) async_fire_time_changed_exact(hass, mock_litejet.start_time + delta) await hass.async_block_till_done() - _LOGGER.info("done with now=%s", dt_util.utcnow()) + _LOGGER.info("*** done with now=%s", dt_util.utcnow()) async def setup_automation(hass, trigger): diff --git a/tests/components/system_log/test_init.py b/tests/components/system_log/test_init.py index 0e301720aeb..94d3a1dd400 100644 --- a/tests/components/system_log/test_init.py +++ b/tests/components/system_log/test_init.py @@ -109,7 +109,7 @@ async def test_normal_logs( await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) await hass.async_block_till_done() _LOGGER.debug("debug") - _LOGGER.info("info") + _LOGGER.info("Info") # Assert done by get_error_log logs = await get_error_log(hass_ws_client) @@ -133,10 +133,10 @@ async def test_warning(hass: HomeAssistant, hass_ws_client: WebSocketGenerator) """Test that warning are logged and retrieved correctly.""" await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) await hass.async_block_till_done() - _LOGGER.warning("warning message") + _LOGGER.warning("Warning message") log = find_log(await get_error_log(hass_ws_client), "WARNING") - assert_log(log, "", "warning message", "WARNING") + assert_log(log, "", "Warning message", "WARNING") async def test_warning_good_format( @@ -145,11 +145,11 @@ async def test_warning_good_format( """Test that warning with good format arguments are logged and retrieved correctly.""" await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) await hass.async_block_till_done() - _LOGGER.warning("warning message: %s", "test") + _LOGGER.warning("Warning message: %s", "test") await hass.async_block_till_done() log = find_log(await get_error_log(hass_ws_client), "WARNING") - assert_log(log, "", "warning message: test", "WARNING") + assert_log(log, "", "Warning message: test", "WARNING") async def test_warning_missing_format_args( @@ -158,11 +158,11 @@ async def test_warning_missing_format_args( """Test that warning with missing format arguments are logged and retrieved correctly.""" await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) await hass.async_block_till_done() - _LOGGER.warning("warning message missing a format arg %s") + _LOGGER.warning("Warning message missing a format arg %s") await hass.async_block_till_done() log = find_log(await get_error_log(hass_ws_client), "WARNING") - assert_log(log, "", ["warning message missing a format arg %s"], "WARNING") + assert_log(log, "", ["Warning message missing a format arg %s"], "WARNING") async def test_error(hass: HomeAssistant, hass_ws_client: WebSocketGenerator) -> None: @@ -170,10 +170,10 @@ async def test_error(hass: HomeAssistant, hass_ws_client: WebSocketGenerator) -> await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) await hass.async_block_till_done() - _LOGGER.error("error message") + _LOGGER.error("Error message") log = find_log(await get_error_log(hass_ws_client), "ERROR") - assert_log(log, "", "error message", "ERROR") + assert_log(log, "", "Error message", "ERROR") async def test_config_not_fire_event(hass: HomeAssistant) -> None: @@ -200,17 +200,17 @@ async def test_error_posted_as_event(hass: HomeAssistant) -> None: watcher = await async_setup_system_log( hass, {"system_log": {"max_entries": 2, "fire_event": True}} ) - wait_empty = watcher.add_watcher("error message") + wait_empty = watcher.add_watcher("Error message") events = async_capture_events(hass, system_log.EVENT_SYSTEM_LOG) - _LOGGER.error("error message") + _LOGGER.error("Error message") await wait_empty await hass.async_block_till_done() await hass.async_block_till_done() assert len(events) == 1 - assert_log(events[0].data, "", "error message", "ERROR") + assert_log(events[0].data, "", "Error message", "ERROR") async def test_critical( @@ -220,10 +220,10 @@ async def test_critical( await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) await hass.async_block_till_done() - _LOGGER.critical("critical message") + _LOGGER.critical("Critical message") log = find_log(await get_error_log(hass_ws_client), "CRITICAL") - assert_log(log, "", "critical message", "CRITICAL") + assert_log(log, "", "Critical message", "CRITICAL") async def test_remove_older_logs( @@ -232,18 +232,18 @@ async def test_remove_older_logs( """Test that older logs are rotated out.""" await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) await hass.async_block_till_done() - _LOGGER.error("error message 1") - _LOGGER.error("error message 2") - _LOGGER.error("error message 3") + _LOGGER.error("Error message 1") + _LOGGER.error("Error message 2") + _LOGGER.error("Error message 3") await hass.async_block_till_done() log = await get_error_log(hass_ws_client) - assert_log(log[0], "", "error message 3", "ERROR") - assert_log(log[1], "", "error message 2", "ERROR") + assert_log(log[0], "", "Error message 3", "ERROR") + assert_log(log[1], "", "Error message 2", "ERROR") def log_msg(nr=2): """Log an error at same line.""" - _LOGGER.error("error message %s", nr) + _LOGGER.error("Error message %s", nr) async def test_dedupe_logs( @@ -252,19 +252,19 @@ async def test_dedupe_logs( """Test that duplicate log entries are dedupe.""" await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) await hass.async_block_till_done() - _LOGGER.error("error message 1") + _LOGGER.error("Error message 1") log_msg() log_msg("2-2") - _LOGGER.error("error message 3") + _LOGGER.error("Error message 3") log = await get_error_log(hass_ws_client) - assert_log(log[0], "", "error message 3", "ERROR") + assert_log(log[0], "", "Error message 3", "ERROR") assert log[1]["count"] == 2 - assert_log(log[1], "", ["error message 2", "error message 2-2"], "ERROR") + assert_log(log[1], "", ["Error message 2", "Error message 2-2"], "ERROR") log_msg() log = await get_error_log(hass_ws_client) - assert_log(log[0], "", ["error message 2", "error message 2-2"], "ERROR") + assert_log(log[0], "", ["Error message 2", "Error message 2-2"], "ERROR") assert log[0]["timestamp"] > log[0]["first_occurred"] log_msg("2-3") @@ -277,11 +277,11 @@ async def test_dedupe_logs( log[0], "", [ - "error message 2-2", - "error message 2-3", - "error message 2-4", - "error message 2-5", - "error message 2-6", + "Error message 2-2", + "Error message 2-3", + "Error message 2-4", + "Error message 2-5", + "Error message 2-6", ], "ERROR", ) @@ -293,7 +293,7 @@ async def test_clear_logs( """Test that the log can be cleared via a service call.""" await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) await hass.async_block_till_done() - _LOGGER.error("error message") + _LOGGER.error("Error message") await hass.services.async_call(system_log.DOMAIN, system_log.SERVICE_CLEAR, {}) await hass.async_block_till_done() @@ -354,7 +354,7 @@ async def test_unknown_path( await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) await hass.async_block_till_done() _LOGGER.findCaller = MagicMock(return_value=("unknown_path", 0, None, None)) - _LOGGER.error("error message") + _LOGGER.error("Error message") log = (await get_error_log(hass_ws_client))[0] assert log["source"] == ["unknown_path", 0] @@ -385,8 +385,8 @@ async def async_log_error_from_test_path(hass, path, watcher): return_value=logger_frame, ), ): - wait_empty = watcher.add_watcher("error message") - _LOGGER.error("error message") + wait_empty = watcher.add_watcher("Error message") + _LOGGER.error("Error message") await wait_empty @@ -444,7 +444,7 @@ async def test_raise_during_log_capture( raise_during_repr = RaisesDuringRepr() - _LOGGER.error("raise during repr: %s", raise_during_repr) + _LOGGER.error("Raise during repr: %s", raise_during_repr) log = find_log(await get_error_log(hass_ws_client), "ERROR") assert log is not None assert_log(log, "", "Bad logger message: repr error", "ERROR") From 1d62056d9b18b636bfbd1828c595f654537ba657 Mon Sep 17 00:00:00 2001 From: mletenay Date: Fri, 14 Jun 2024 08:29:32 +0200 Subject: [PATCH 0678/1445] Bump goodwe to 0.3.6 (#119646) --- homeassistant/components/goodwe/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/goodwe/manifest.json b/homeassistant/components/goodwe/manifest.json index 8506d1fd6af..41e0ed91f6a 100644 --- a/homeassistant/components/goodwe/manifest.json +++ b/homeassistant/components/goodwe/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/goodwe", "iot_class": "local_polling", "loggers": ["goodwe"], - "requirements": ["goodwe==0.3.5"] + "requirements": ["goodwe==0.3.6"] } diff --git a/requirements_all.txt b/requirements_all.txt index 62f2ed4be16..d38673f02ed 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -967,7 +967,7 @@ glances-api==0.8.0 goalzero==0.2.2 # homeassistant.components.goodwe -goodwe==0.3.5 +goodwe==0.3.6 # homeassistant.components.google_mail # homeassistant.components.google_tasks diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f897506d6b6..cb9dd30599d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -796,7 +796,7 @@ glances-api==0.8.0 goalzero==0.2.2 # homeassistant.components.goodwe -goodwe==0.3.5 +goodwe==0.3.6 # homeassistant.components.google_mail # homeassistant.components.google_tasks From f3ce562847106406821aa02022c1919b0a24f4fd Mon Sep 17 00:00:00 2001 From: Rami Mosleh Date: Fri, 14 Jun 2024 09:39:04 +0300 Subject: [PATCH 0679/1445] Store Glances coordinator in runtime_data (#119607) --- homeassistant/components/glances/__init__.py | 16 ++++++++-------- homeassistant/components/glances/sensor.py | 7 +++---- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/glances/__init__.py b/homeassistant/components/glances/__init__.py index 437882e0135..f83b39d1cf9 100644 --- a/homeassistant/components/glances/__init__.py +++ b/homeassistant/components/glances/__init__.py @@ -40,8 +40,12 @@ CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) _LOGGER = logging.getLogger(__name__) +type GlancesConfigEntry = ConfigEntry[GlancesDataUpdateCoordinator] -async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: + +async def async_setup_entry( + hass: HomeAssistant, config_entry: GlancesConfigEntry +) -> bool: """Set up Glances from config entry.""" try: api = await get_api(hass, dict(config_entry.data)) @@ -54,20 +58,16 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b coordinator = GlancesDataUpdateCoordinator(hass, config_entry, api) await coordinator.async_config_entry_first_refresh() - hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = coordinator + config_entry.runtime_data = coordinator await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: GlancesConfigEntry) -> bool: """Unload a config entry.""" - if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): - hass.data[DOMAIN].pop(entry.entry_id) - if not hass.data[DOMAIN]: - del hass.data[DOMAIN] - return unload_ok + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) async def get_api(hass: HomeAssistant, entry_data: dict[str, Any]) -> Glances: diff --git a/homeassistant/components/glances/sensor.py b/homeassistant/components/glances/sensor.py index c5706757725..a1cb8e47b9d 100644 --- a/homeassistant/components/glances/sensor.py +++ b/homeassistant/components/glances/sensor.py @@ -10,7 +10,6 @@ from homeassistant.components.sensor import ( SensorEntityDescription, SensorStateClass, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( PERCENTAGE, REVOLUTIONS_PER_MINUTE, @@ -23,7 +22,7 @@ from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity -from . import GlancesDataUpdateCoordinator +from . import GlancesConfigEntry, GlancesDataUpdateCoordinator from .const import CPU_ICON, DOMAIN @@ -288,12 +287,12 @@ SENSOR_TYPES = { async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: GlancesConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Glances sensors.""" - coordinator: GlancesDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + coordinator = config_entry.runtime_data entities: list[GlancesSensor] = [] for sensor_type, sensors in coordinator.data.items(): From 9f41133bbc044ba1cedafbff08a723709c545cf7 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 14 Jun 2024 08:42:01 +0200 Subject: [PATCH 0680/1445] Add missing argument type to core tests (#119667) --- tests/conftest.py | 6 +++--- tests/helpers/test_entity.py | 2 +- tests/helpers/test_frame.py | 10 ++++------ tests/test_core.py | 4 +++- tests/test_data_entry_flow.py | 4 +++- tests/test_loader.py | 12 ++++++++---- tests/test_setup.py | 2 +- 7 files changed, 23 insertions(+), 17 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 0bef1a7b06a..b2b0eb3487c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -743,7 +743,7 @@ async def hass_supervisor_user( @pytest.fixture async def hass_supervisor_access_token( hass: HomeAssistant, - hass_supervisor_user, + hass_supervisor_user: MockUser, local_auth: homeassistant.HassAuthProvider, ) -> str: """Return a Home Assistant Supervisor access token.""" @@ -836,7 +836,7 @@ def current_request_with_host(current_request: MagicMock) -> None: @pytest.fixture def hass_ws_client( aiohttp_client: ClientSessionGenerator, - hass_access_token: str | None, + hass_access_token: str, hass: HomeAssistant, socket_enabled: None, ) -> WebSocketGenerator: @@ -1372,7 +1372,7 @@ def hass_recorder( enable_migrate_context_ids: bool, enable_migrate_event_type_ids: bool, enable_migrate_entity_ids: bool, - hass_storage, + hass_storage: dict[str, Any], ) -> Generator[Callable[..., HomeAssistant]]: """Home Assistant fixture with in-memory recorder.""" # pylint: disable-next=import-outside-toplevel diff --git a/tests/helpers/test_entity.py b/tests/helpers/test_entity.py index cc53bca8e4d..9d2c9a66a5b 100644 --- a/tests/helpers/test_entity.py +++ b/tests/helpers/test_entity.py @@ -1601,7 +1601,7 @@ async def test_translation_key(hass: HomeAssistant) -> None: assert mock_entity2.translation_key == "from_entity_description" -async def test_repr(hass) -> None: +async def test_repr(hass: HomeAssistant) -> None: """Test Entity.__repr__.""" class MyEntity(MockEntity): diff --git a/tests/helpers/test_frame.py b/tests/helpers/test_frame.py index b0b4a0be6ee..b3fbb0faaf4 100644 --- a/tests/helpers/test_frame.py +++ b/tests/helpers/test_frame.py @@ -32,9 +32,8 @@ async def test_get_integration_logger( assert logger.name == "homeassistant.components.hue" -async def test_extract_frame_resolve_module( - hass: HomeAssistant, enable_custom_integrations -) -> None: +@pytest.mark.usefixtures("enable_custom_integrations") +async def test_extract_frame_resolve_module(hass: HomeAssistant) -> None: """Test extracting the current frame from integration context.""" # pylint: disable-next=import-outside-toplevel from custom_components.test_integration_frame import call_get_integration_frame @@ -50,9 +49,8 @@ async def test_extract_frame_resolve_module( ) -async def test_get_integration_logger_resolve_module( - hass: HomeAssistant, enable_custom_integrations -) -> None: +@pytest.mark.usefixtures("enable_custom_integrations") +async def test_get_integration_logger_resolve_module(hass: HomeAssistant) -> None: """Test getting the logger from integration context.""" # pylint: disable-next=import-outside-toplevel from custom_components.test_integration_frame import call_get_integration_logger diff --git a/tests/test_core.py b/tests/test_core.py index 541affc729b..8be2599f454 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -647,7 +647,9 @@ async def test_stage_shutdown_timeouts(hass: HomeAssistant) -> None: assert hass.state is CoreState.stopped -async def test_stage_shutdown_generic_error(hass: HomeAssistant, caplog) -> None: +async def test_stage_shutdown_generic_error( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: """Simulate a shutdown, test that a generic error at the final stage doesn't prevent it.""" task = asyncio.Future() diff --git a/tests/test_data_entry_flow.py b/tests/test_data_entry_flow.py index cc12ae42b67..c02d909733a 100644 --- a/tests/test_data_entry_flow.py +++ b/tests/test_data_entry_flow.py @@ -556,7 +556,9 @@ async def test_show_progress_hidden_from_frontend(hass: HomeAssistant, manager) assert async_show_progress_done_called -async def test_show_progress_legacy(hass: HomeAssistant, manager, caplog) -> None: +async def test_show_progress_legacy( + hass: HomeAssistant, manager, caplog: pytest.LogCaptureFixture +) -> None: """Test show progress logic. This tests the deprecated version where the config flow is responsible for diff --git a/tests/test_loader.py b/tests/test_loader.py index b195de6006b..8cda75e0321 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -1125,7 +1125,10 @@ CUSTOM_ISSUE_TRACKER = "https://blablabla.com" ], ) async def test_async_get_issue_tracker( - hass, domain: str | None, module: str | None, issue_tracker: str | None + hass: HomeAssistant, + domain: str | None, + module: str | None, + issue_tracker: str | None, ) -> None: """Test async_get_issue_tracker.""" mock_integration(hass, MockModule("bla_built_in")) @@ -1187,7 +1190,7 @@ async def test_async_get_issue_tracker( ], ) async def test_async_get_issue_tracker_no_hass( - hass, domain: str | None, module: str | None, issue_tracker: str + hass: HomeAssistant, domain: str | None, module: str | None, issue_tracker: str ) -> None: """Test async_get_issue_tracker.""" mock_integration(hass, MockModule("bla_built_in")) @@ -1220,7 +1223,7 @@ REPORT_CUSTOM_UNKNOWN = "report it to the custom integration author" ], ) async def test_async_suggest_report_issue( - hass, domain: str | None, module: str | None, report_issue: str + hass: HomeAssistant, domain: str | None, module: str | None, report_issue: str ) -> None: """Test async_suggest_report_issue.""" mock_integration(hass, MockModule("bla_built_in")) @@ -1952,7 +1955,8 @@ async def test_integration_warnings( assert "configured to to import its code in the event loop" in caplog.text -async def test_has_services(hass: HomeAssistant, enable_custom_integrations) -> None: +@pytest.mark.usefixtures("enable_custom_integrations") +async def test_has_services(hass: HomeAssistant) -> None: """Test has_services.""" integration = await loader.async_get_integration(hass, "test") assert integration.has_services is False diff --git a/tests/test_setup.py b/tests/test_setup.py index f15fe72603e..910a46d3c73 100644 --- a/tests/test_setup.py +++ b/tests/test_setup.py @@ -1057,7 +1057,7 @@ async def test_async_start_setup_simple_integration_end_to_end( } -async def test_async_get_setup_timings(hass) -> None: +async def test_async_get_setup_timings(hass: HomeAssistant) -> None: """Test we can get the setup timings from the setup time data.""" setup_time = setup._setup_times(hass) # Mock setup time data From 9082dc2a799734f7dfa7eb7cbcb8e99210796b80 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 14 Jun 2024 01:43:51 -0500 Subject: [PATCH 0681/1445] Reduce recorder overhead when entity filter is empty (#119631) --- homeassistant/components/recorder/__init__.py | 8 ++++---- homeassistant/components/recorder/core.py | 7 +++++-- homeassistant/components/recorder/purge.py | 8 +++++--- homeassistant/components/sensor/recorder.py | 8 +++++--- 4 files changed, 19 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index a5a49e7df60..f5e72912224 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -127,15 +127,15 @@ def is_entity_recorded(hass: HomeAssistant, entity_id: str) -> bool: Async friendly. """ - if DATA_INSTANCE not in hass.data: - return False - return hass.data[DATA_INSTANCE].entity_filter(entity_id) + instance = get_instance(hass) + return instance.entity_filter is None or instance.entity_filter(entity_id) async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the recorder.""" conf = config[DOMAIN] - entity_filter = convert_include_exclude_filter(conf).get_filter() + _filter = convert_include_exclude_filter(conf) + entity_filter = None if _filter.empty_filter else _filter.get_filter() auto_purge = conf[CONF_AUTO_PURGE] auto_repack = conf[CONF_AUTO_REPACK] keep_days = conf[CONF_PURGE_KEEP_DAYS] diff --git a/homeassistant/components/recorder/core.py b/homeassistant/components/recorder/core.py index 890cc2e1a8f..a5eecf42f22 100644 --- a/homeassistant/components/recorder/core.py +++ b/homeassistant/components/recorder/core.py @@ -178,7 +178,7 @@ class Recorder(threading.Thread): uri: str, db_max_retries: int, db_retry_wait: int, - entity_filter: Callable[[str], bool], + entity_filter: Callable[[str], bool] | None, exclude_event_types: set[EventType[Any] | str], ) -> None: """Initialize the recorder.""" @@ -318,7 +318,10 @@ class Recorder(threading.Thread): if event.event_type in exclude_event_types: return - if (entity_id := event.data.get(ATTR_ENTITY_ID)) is None: + if ( + entity_filter is None + or (entity_id := event.data.get(ATTR_ENTITY_ID)) is None + ): queue_put(event) return diff --git a/homeassistant/components/recorder/purge.py b/homeassistant/components/recorder/purge.py index 2d161571511..d28e7e2a547 100644 --- a/homeassistant/components/recorder/purge.py +++ b/homeassistant/components/recorder/purge.py @@ -645,7 +645,7 @@ def _purge_filtered_data(instance: Recorder, session: Session) -> bool: for (metadata_id, entity_id) in session.query( StatesMeta.metadata_id, StatesMeta.entity_id ).all() - if not entity_filter(entity_id) + if entity_filter and not entity_filter(entity_id) ] if excluded_metadata_ids: has_more_states_to_purge = _purge_filtered_states( @@ -765,7 +765,9 @@ def _purge_filtered_events( @retryable_database_job("purge_entity_data") def purge_entity_data( - instance: Recorder, entity_filter: Callable[[str], bool], purge_before: datetime + instance: Recorder, + entity_filter: Callable[[str], bool] | None, + purge_before: datetime, ) -> bool: """Purge states and events of specified entities.""" database_engine = instance.database_engine @@ -777,7 +779,7 @@ def purge_entity_data( for (metadata_id, entity_id) in session.query( StatesMeta.metadata_id, StatesMeta.entity_id ).all() - if entity_filter(entity_id) + if entity_filter and entity_filter(entity_id) ] _LOGGER.debug("Purging entity data for %s", selected_metadata_ids) if not selected_metadata_ids: diff --git a/homeassistant/components/sensor/recorder.py b/homeassistant/components/sensor/recorder.py index 26bb4f4376b..940592d7b08 100644 --- a/homeassistant/components/sensor/recorder.py +++ b/homeassistant/components/sensor/recorder.py @@ -80,6 +80,7 @@ def _get_sensor_states(hass: HomeAssistant) -> list[State]: # We check for state class first before calling the filter # function as the filter function is much more expensive # than checking the state class + entity_filter = instance.entity_filter return [ state for state in hass.states.all(DOMAIN) @@ -88,7 +89,7 @@ def _get_sensor_states(hass: HomeAssistant) -> list[State]: type(state_class) is SensorStateClass or try_parse_enum(SensorStateClass, state_class) ) - and instance.entity_filter(state.entity_id) + and (not entity_filter or entity_filter(state.entity_id)) ] @@ -680,6 +681,7 @@ def validate_statistics( sensor_entity_ids = {i.entity_id for i in sensor_states} sensor_statistic_ids = set(metadatas) instance = get_instance(hass) + entity_filter = instance.entity_filter for state in sensor_states: entity_id = state.entity_id @@ -689,7 +691,7 @@ def validate_statistics( state_unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) if metadata := metadatas.get(entity_id): - if not instance.entity_filter(state.entity_id): + if entity_filter and not entity_filter(state.entity_id): # Sensor was previously recorded, but no longer is validation_result[entity_id].append( statistics.ValidationIssue( @@ -739,7 +741,7 @@ def validate_statistics( ) ) elif state_class is not None: - if not instance.entity_filter(state.entity_id): + if entity_filter and not entity_filter(state.entity_id): # Sensor is not recorded validation_result[entity_id].append( statistics.ValidationIssue( From 003f2168202cc872cff31c46de6520e0f8581adc Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 14 Jun 2024 08:54:37 +0200 Subject: [PATCH 0682/1445] Rename collection.CollectionChangeSet to collection.CollectionChange (#119532) --- .../components/assist_pipeline/select.py | 2 +- homeassistant/helpers/collection.py | 64 +++++++++---------- tests/helpers/test_collection.py | 20 +++--- 3 files changed, 41 insertions(+), 45 deletions(-) diff --git a/homeassistant/components/assist_pipeline/select.py b/homeassistant/components/assist_pipeline/select.py index 43ed003f65d..5d011424e6e 100644 --- a/homeassistant/components/assist_pipeline/select.py +++ b/homeassistant/components/assist_pipeline/select.py @@ -109,7 +109,7 @@ class AssistPipelineSelect(SelectEntity, restore_state.RestoreEntity): self.async_write_ha_state() async def _pipelines_updated( - self, change_sets: Iterable[collection.CollectionChangeSet] + self, change_set: Iterable[collection.CollectionChange] ) -> None: """Handle pipeline update.""" self._update_options() diff --git a/homeassistant/helpers/collection.py b/homeassistant/helpers/collection.py index 1b63d95864a..4691bc804fd 100644 --- a/homeassistant/helpers/collection.py +++ b/homeassistant/helpers/collection.py @@ -39,8 +39,8 @@ _EntityT = TypeVar("_EntityT", bound=Entity, default=Entity) @dataclass(slots=True) -class CollectionChangeSet: - """Class to represent a change set. +class CollectionChange: + """Class to represent an item in a change set. change_type: One of CHANGE_* item_id: The id of the item @@ -64,7 +64,7 @@ type ChangeListener = Callable[ Awaitable[None], ] -type ChangeSetListener = Callable[[Iterable[CollectionChangeSet]], Awaitable[None]] +type ChangeSetListener = Callable[[Iterable[CollectionChange]], Awaitable[None]] class CollectionError(HomeAssistantError): @@ -163,16 +163,16 @@ class ObservableCollection[_ItemT](ABC): self.change_set_listeners.append(listener) return partial(self.change_set_listeners.remove, listener) - async def notify_changes(self, change_sets: Iterable[CollectionChangeSet]) -> None: + async def notify_changes(self, change_set: Iterable[CollectionChange]) -> None: """Notify listeners of a change.""" await asyncio.gather( *( - listener(change_set.change_type, change_set.item_id, change_set.item) + listener(change.change_type, change.item_id, change.item) for listener in self.listeners - for change_set in change_sets + for change in change_set ), *( - change_set_listener(change_sets) + change_set_listener(change_set) for change_set_listener in self.change_set_listeners ), ) @@ -201,7 +201,7 @@ class YamlCollection(ObservableCollection[dict]): """Load the YAML collection. Overrides existing data.""" old_ids = set(self.data) - change_sets = [] + change_set = [] for item in data: item_id = item[CONF_ID] @@ -216,15 +216,15 @@ class YamlCollection(ObservableCollection[dict]): event = CHANGE_ADDED self.data[item_id] = item - change_sets.append(CollectionChangeSet(event, item_id, item)) + change_set.append(CollectionChange(event, item_id, item)) - change_sets.extend( - CollectionChangeSet(CHANGE_REMOVED, item_id, self.data.pop(item_id)) + change_set.extend( + CollectionChange(CHANGE_REMOVED, item_id, self.data.pop(item_id)) for item_id in old_ids ) - if change_sets: - await self.notify_changes(change_sets) + if change_set: + await self.notify_changes(change_set) class SerializedStorageCollection(TypedDict): @@ -273,7 +273,7 @@ class StorageCollection[_ItemT, _StoreT: SerializedStorageCollection]( await self.notify_changes( [ - CollectionChangeSet(CHANGE_ADDED, item[CONF_ID], item) + CollectionChange(CHANGE_ADDED, item[CONF_ID], item) for item in raw_storage["items"] ] ) @@ -313,7 +313,7 @@ class StorageCollection[_ItemT, _StoreT: SerializedStorageCollection]( item = self._create_item(item_id, validated_data) self.data[item_id] = item self._async_schedule_save() - await self.notify_changes([CollectionChangeSet(CHANGE_ADDED, item_id, item)]) + await self.notify_changes([CollectionChange(CHANGE_ADDED, item_id, item)]) return item async def async_update_item(self, item_id: str, updates: dict) -> _ItemT: @@ -331,9 +331,7 @@ class StorageCollection[_ItemT, _StoreT: SerializedStorageCollection]( self.data[item_id] = updated self._async_schedule_save() - await self.notify_changes( - [CollectionChangeSet(CHANGE_UPDATED, item_id, updated)] - ) + await self.notify_changes([CollectionChange(CHANGE_UPDATED, item_id, updated)]) return self.data[item_id] @@ -345,7 +343,7 @@ class StorageCollection[_ItemT, _StoreT: SerializedStorageCollection]( item = self.data.pop(item_id) self._async_schedule_save() - await self.notify_changes([CollectionChangeSet(CHANGE_REMOVED, item_id, item)]) + await self.notify_changes([CollectionChange(CHANGE_REMOVED, item_id, item)]) @callback def _async_schedule_save(self) -> None: @@ -398,7 +396,7 @@ class IDLessCollection(YamlCollection): """Load the collection. Overrides existing data.""" await self.notify_changes( [ - CollectionChangeSet(CHANGE_REMOVED, item_id, item) + CollectionChange(CHANGE_REMOVED, item_id, item) for item_id, item in list(self.data.items()) ] ) @@ -413,7 +411,7 @@ class IDLessCollection(YamlCollection): await self.notify_changes( [ - CollectionChangeSet(CHANGE_ADDED, item_id, item) + CollectionChange(CHANGE_ADDED, item_id, item) for item_id, item in self.data.items() ] ) @@ -444,14 +442,14 @@ class _CollectionLifeCycle(Generic[_EntityT]): self.entities.pop(item_id, None) @callback - def _add_entity(self, change_set: CollectionChangeSet) -> CollectionEntity: + def _add_entity(self, change_set: CollectionChange) -> CollectionEntity: item_id = change_set.item_id entity = self.collection.create_entity(self.entity_class, change_set.item) self.entities[item_id] = entity entity.async_on_remove(partial(self._entity_removed, item_id)) return entity - async def _remove_entity(self, change_set: CollectionChangeSet) -> None: + async def _remove_entity(self, change_set: CollectionChange) -> None: item_id = change_set.item_id ent_reg = self.ent_reg entities = self.entities @@ -464,29 +462,27 @@ class _CollectionLifeCycle(Generic[_EntityT]): # the entity registry event handled by Entity._async_registry_updated entities.pop(item_id, None) - async def _update_entity(self, change_set: CollectionChangeSet) -> None: + async def _update_entity(self, change_set: CollectionChange) -> None: if entity := self.entities.get(change_set.item_id): await entity.async_update_config(change_set.item) - async def _collection_changed( - self, change_sets: Iterable[CollectionChangeSet] - ) -> None: + async def _collection_changed(self, change_set: Iterable[CollectionChange]) -> None: """Handle a collection change.""" # Create a new bucket every time we have a different change type # to ensure operations happen in order. We only group # the same change type. new_entities: list[CollectionEntity] = [] coros: list[Coroutine[Any, Any, CollectionEntity | None]] = [] - grouped: Iterable[CollectionChangeSet] - for _, grouped in groupby(change_sets, _GROUP_BY_KEY): - for change_set in grouped: - change_type = change_set.change_type + grouped: Iterable[CollectionChange] + for _, grouped in groupby(change_set, _GROUP_BY_KEY): + for change in grouped: + change_type = change.change_type if change_type == CHANGE_ADDED: - new_entities.append(self._add_entity(change_set)) + new_entities.append(self._add_entity(change)) elif change_type == CHANGE_REMOVED: - coros.append(self._remove_entity(change_set)) + coros.append(self._remove_entity(change)) elif change_type == CHANGE_UPDATED: - coros.append(self._update_entity(change_set)) + coros.append(self._update_entity(change)) if coros: await asyncio.gather(*coros) diff --git a/tests/helpers/test_collection.py b/tests/helpers/test_collection.py index 4be372efe9c..dc9ac21e246 100644 --- a/tests/helpers/test_collection.py +++ b/tests/helpers/test_collection.py @@ -124,7 +124,7 @@ async def test_observable_collection() -> None: changes = track_changes(coll) await coll.notify_changes( - [collection.CollectionChangeSet("mock_type", "mock_id", {"mock": "item"})] + [collection.CollectionChange("mock_type", "mock_id", {"mock": "item"})] ) assert len(changes) == 1 assert changes[0] == ("mock_type", "mock_id", {"mock": "item"}) @@ -263,7 +263,7 @@ async def test_attach_entity_component_collection(hass: HomeAssistant) -> None: await coll.notify_changes( [ - collection.CollectionChangeSet( + collection.CollectionChange( collection.CHANGE_ADDED, "mock_id", {"id": "mock_id", "state": "initial", "name": "Mock 1"}, @@ -276,7 +276,7 @@ async def test_attach_entity_component_collection(hass: HomeAssistant) -> None: await coll.notify_changes( [ - collection.CollectionChangeSet( + collection.CollectionChange( collection.CHANGE_UPDATED, "mock_id", {"id": "mock_id", "state": "second", "name": "Mock 1 updated"}, @@ -288,7 +288,7 @@ async def test_attach_entity_component_collection(hass: HomeAssistant) -> None: assert hass.states.get("test.mock_1").state == "second" await coll.notify_changes( - [collection.CollectionChangeSet(collection.CHANGE_REMOVED, "mock_id", None)], + [collection.CollectionChange(collection.CHANGE_REMOVED, "mock_id", None)], ) assert hass.states.get("test.mock_1") is None @@ -331,7 +331,7 @@ async def test_entity_component_collection_abort( await coll.notify_changes( [ - collection.CollectionChangeSet( + collection.CollectionChange( collection.CHANGE_ADDED, "mock_id", {"id": "mock_id", "state": "initial", "name": "Mock 1"}, @@ -343,7 +343,7 @@ async def test_entity_component_collection_abort( await coll.notify_changes( [ - collection.CollectionChangeSet( + collection.CollectionChange( collection.CHANGE_UPDATED, "mock_id", {"id": "mock_id", "state": "second", "name": "Mock 1 updated"}, @@ -355,7 +355,7 @@ async def test_entity_component_collection_abort( assert len(async_update_config_calls) == 0 await coll.notify_changes( - [collection.CollectionChangeSet(collection.CHANGE_REMOVED, "mock_id", None)], + [collection.CollectionChange(collection.CHANGE_REMOVED, "mock_id", None)], ) assert hass.states.get("test.mock_1") is None @@ -395,7 +395,7 @@ async def test_entity_component_collection_entity_removed( await coll.notify_changes( [ - collection.CollectionChangeSet( + collection.CollectionChange( collection.CHANGE_ADDED, "mock_id", {"id": "mock_id", "state": "initial", "name": "Mock 1"}, @@ -413,7 +413,7 @@ async def test_entity_component_collection_entity_removed( await coll.notify_changes( [ - collection.CollectionChangeSet( + collection.CollectionChange( collection.CHANGE_UPDATED, "mock_id", {"id": "mock_id", "state": "second", "name": "Mock 1 updated"}, @@ -425,7 +425,7 @@ async def test_entity_component_collection_entity_removed( assert len(async_update_config_calls) == 0 await coll.notify_changes( - [collection.CollectionChangeSet(collection.CHANGE_REMOVED, "mock_id", None)], + [collection.CollectionChange(collection.CHANGE_REMOVED, "mock_id", None)], ) assert hass.states.get("test.mock_1") is None From 83b97d321888e0d8c03d8fa1f88f1a1968e5b3f1 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 14 Jun 2024 09:25:26 +0200 Subject: [PATCH 0683/1445] Add missing argument type hints to recorder tests (#119672) --- tests/components/recorder/test_history.py | 4 +++- tests/components/recorder/test_history_db_schema_32.py | 4 +++- tests/components/recorder/test_history_db_schema_42.py | 4 +++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/tests/components/recorder/test_history.py b/tests/components/recorder/test_history.py index 05542cbecb5..af846353467 100644 --- a/tests/components/recorder/test_history.py +++ b/tests/components/recorder/test_history.py @@ -806,7 +806,9 @@ async def test_get_significant_states_only_minimal_response( assert len(hist["sensor.test"]) == 3 -def record_states(hass) -> tuple[datetime, datetime, dict[str, list[State]]]: +def record_states( + hass: HomeAssistant, +) -> tuple[datetime, datetime, dict[str, list[State]]]: """Record some test states. We inject a bunch of state updates from media player, zone and diff --git a/tests/components/recorder/test_history_db_schema_32.py b/tests/components/recorder/test_history_db_schema_32.py index b778a3ff6a3..8a3e6a58ab3 100644 --- a/tests/components/recorder/test_history_db_schema_32.py +++ b/tests/components/recorder/test_history_db_schema_32.py @@ -568,7 +568,9 @@ async def test_get_significant_states_only( ) -def record_states(hass) -> tuple[datetime, datetime, dict[str, list[State]]]: +def record_states( + hass: HomeAssistant, +) -> tuple[datetime, datetime, dict[str, list[State]]]: """Record some test states. We inject a bunch of state updates from media player, zone and diff --git a/tests/components/recorder/test_history_db_schema_42.py b/tests/components/recorder/test_history_db_schema_42.py index 04490b88a28..083d4c0930e 100644 --- a/tests/components/recorder/test_history_db_schema_42.py +++ b/tests/components/recorder/test_history_db_schema_42.py @@ -808,7 +808,9 @@ async def test_get_significant_states_only_minimal_response( assert len(hist["sensor.test"]) == 3 -def record_states(hass) -> tuple[datetime, datetime, dict[str, list[State]]]: +def record_states( + hass: HomeAssistant, +) -> tuple[datetime, datetime, dict[str, list[State]]]: """Record some test states. We inject a bunch of state updates from media player, zone and From 3e9d25f81d321511c02b6c459bc9ac6c598e60b9 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 14 Jun 2024 09:26:46 +0200 Subject: [PATCH 0684/1445] Add missing argument type hints to component tests (#119671) --- tests/components/accuweather/__init__.py | 3 ++- tests/components/airly/__init__.py | 6 +++++- .../test_passive_update_coordinator.py | 8 +++++++- tests/components/bthome/test_device_trigger.py | 2 +- tests/components/device_tracker/common.py | 6 ++++-- tests/components/dexcom/__init__.py | 3 ++- tests/components/dlna_dms/conftest.py | 2 +- tests/components/esphome/conftest.py | 4 ++-- tests/components/fan/common.py | 17 +++++++++-------- tests/components/freedompro/conftest.py | 5 +++-- tests/components/gios/__init__.py | 3 ++- tests/components/imgw_pib/__init__.py | 6 +++++- tests/components/kodi/__init__.py | 3 ++- tests/components/litejet/__init__.py | 3 ++- tests/components/loqed/test_init.py | 4 ++-- tests/components/lutron_caseta/__init__.py | 3 ++- tests/components/met/__init__.py | 5 ++++- tests/components/met_eireann/__init__.py | 3 ++- tests/components/motioneye/test_media_source.py | 2 +- tests/components/nam/__init__.py | 5 ++++- tests/components/nest/test_camera.py | 3 ++- tests/components/nest/test_media_source.py | 2 +- tests/components/nightscout/__init__.py | 7 ++++--- tests/components/nina/test_init.py | 2 +- tests/components/nzbget/__init__.py | 3 ++- tests/components/octoprint/__init__.py | 8 +++++--- tests/components/onvif/__init__.py | 3 ++- .../components/owntracks/test_device_tracker.py | 4 +++- tests/components/plex/conftest.py | 3 ++- tests/components/plex/test_init.py | 2 +- tests/components/powerwall/mocks.py | 6 +++++- tests/components/rfxtrx/conftest.py | 3 ++- tests/components/rtsp_to_webrtc/conftest.py | 2 +- tests/components/sia/test_config_flow.py | 2 +- tests/components/smartthings/conftest.py | 4 +++- tests/components/stream/test_hls.py | 2 +- tests/components/system_log/test_init.py | 5 ++++- tests/components/unifi/conftest.py | 6 ++++-- tests/components/v2c/__init__.py | 6 +++++- tests/components/valve/test_init.py | 2 +- tests/components/voip/conftest.py | 2 +- tests/components/ws66i/test_media_player.py | 4 ++-- .../xiaomi_ble/test_device_trigger.py | 4 +++- tests/components/zha/common.py | 5 ++++- tests/components/zha/conftest.py | 9 ++++++--- tests/components/zha/test_discover.py | 4 ++-- tests/components/zha/test_light.py | 6 +++++- 47 files changed, 135 insertions(+), 67 deletions(-) diff --git a/tests/components/accuweather/__init__.py b/tests/components/accuweather/__init__.py index 21cdb2ac558..0e5313ceb94 100644 --- a/tests/components/accuweather/__init__.py +++ b/tests/components/accuweather/__init__.py @@ -1,11 +1,12 @@ """Tests for AccuWeather.""" from homeassistant.components.accuweather.const import DOMAIN +from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry -async def init_integration(hass) -> MockConfigEntry: +async def init_integration(hass: HomeAssistant) -> MockConfigEntry: """Set up the AccuWeather integration in Home Assistant.""" entry = MockConfigEntry( domain=DOMAIN, diff --git a/tests/components/airly/__init__.py b/tests/components/airly/__init__.py index 2e2ec23e4e3..c87c41b5162 100644 --- a/tests/components/airly/__init__.py +++ b/tests/components/airly/__init__.py @@ -1,8 +1,10 @@ """Tests for Airly.""" from homeassistant.components.airly.const import DOMAIN +from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry, load_fixture +from tests.test_util.aiohttp import AiohttpClientMocker API_NEAREST_URL = "https://airapi.airly.eu/v2/measurements/nearest?lat=123.000000&lng=456.000000&maxDistanceKM=5.000000" API_POINT_URL = ( @@ -14,7 +16,9 @@ HEADERS = { } -async def init_integration(hass, aioclient_mock) -> MockConfigEntry: +async def init_integration( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> MockConfigEntry: """Set up the Airly integration in Home Assistant.""" entry = MockConfigEntry( domain=DOMAIN, diff --git a/tests/components/bluetooth/test_passive_update_coordinator.py b/tests/components/bluetooth/test_passive_update_coordinator.py index 53a18e88683..9b668b97177 100644 --- a/tests/components/bluetooth/test_passive_update_coordinator.py +++ b/tests/components/bluetooth/test_passive_update_coordinator.py @@ -52,7 +52,13 @@ GENERIC_BLUETOOTH_SERVICE_INFO = BluetoothServiceInfo( class MyCoordinator(PassiveBluetoothDataUpdateCoordinator): """An example coordinator that subclasses PassiveBluetoothDataUpdateCoordinator.""" - def __init__(self, hass, logger, device_id, mode) -> None: + def __init__( + self, + hass: HomeAssistant, + logger: logging.Logger, + device_id: str, + mode: BluetoothScanningMode, + ) -> None: """Initialize the coordinator.""" super().__init__(hass, logger, device_id, mode) self.data: dict[str, Any] = {} diff --git a/tests/components/bthome/test_device_trigger.py b/tests/components/bthome/test_device_trigger.py index f847ffb9c0a..459654826f9 100644 --- a/tests/components/bthome/test_device_trigger.py +++ b/tests/components/bthome/test_device_trigger.py @@ -25,7 +25,7 @@ def get_device_id(mac: str) -> tuple[str, str]: return (BLUETOOTH_DOMAIN, mac) -async def _async_setup_bthome_device(hass, mac: str): +async def _async_setup_bthome_device(hass: HomeAssistant, mac: str) -> MockConfigEntry: config_entry = MockConfigEntry( domain=DOMAIN, unique_id=mac, diff --git a/tests/components/device_tracker/common.py b/tests/components/device_tracker/common.py index a17556cfbaa..d30db984a66 100644 --- a/tests/components/device_tracker/common.py +++ b/tests/components/device_tracker/common.py @@ -20,7 +20,7 @@ from homeassistant.components.device_tracker import ( SourceType, ) from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.typing import GPSType +from homeassistant.helpers.typing import ConfigType, GPSType from homeassistant.loader import bind_hass from tests.common import MockPlatform, mock_platform @@ -143,7 +143,9 @@ def mock_legacy_device_tracker_setup( ) -> None: """Mock legacy device tracker platform setup.""" - async def _async_get_scanner(hass, config) -> MockScanner: + async def _async_get_scanner( + hass: HomeAssistant, config: ConfigType + ) -> MockScanner: """Return the test scanner.""" return legacy_device_scanner diff --git a/tests/components/dexcom/__init__.py b/tests/components/dexcom/__init__.py index e9ca303765b..adc9c56049a 100644 --- a/tests/components/dexcom/__init__.py +++ b/tests/components/dexcom/__init__.py @@ -7,6 +7,7 @@ from pydexcom import GlucoseReading from homeassistant.components.dexcom.const import CONF_SERVER, DOMAIN, SERVER_US from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry, load_fixture @@ -19,7 +20,7 @@ CONFIG = { GLUCOSE_READING = GlucoseReading(json.loads(load_fixture("data.json", "dexcom"))) -async def init_integration(hass) -> MockConfigEntry: +async def init_integration(hass: HomeAssistant) -> MockConfigEntry: """Set up the Dexcom integration in Home Assistant.""" entry = MockConfigEntry( domain=DOMAIN, diff --git a/tests/components/dlna_dms/conftest.py b/tests/components/dlna_dms/conftest.py index c1bee224c5a..1fa56f4bc24 100644 --- a/tests/components/dlna_dms/conftest.py +++ b/tests/components/dlna_dms/conftest.py @@ -38,7 +38,7 @@ NEW_DEVICE_LOCATION: Final = "http://192.88.99.7" + "/dmr_description.xml" @pytest.fixture -async def setup_media_source(hass) -> None: +async def setup_media_source(hass: HomeAssistant) -> None: """Set up media source.""" assert await async_setup_component(hass, "media_source", {}) diff --git a/tests/components/esphome/conftest.py b/tests/components/esphome/conftest.py index f1fae38e0e3..43edca54158 100644 --- a/tests/components/esphome/conftest.py +++ b/tests/components/esphome/conftest.py @@ -52,7 +52,7 @@ def esphome_mock_async_zeroconf(mock_async_zeroconf: MagicMock) -> None: @pytest.fixture(autouse=True) -async def load_homeassistant(hass) -> None: +async def load_homeassistant(hass: HomeAssistant) -> None: """Load the homeassistant integration.""" assert await async_setup_component(hass, "homeassistant", {}) @@ -63,7 +63,7 @@ def mock_tts(mock_tts_cache_dir: Path) -> None: @pytest.fixture -def mock_config_entry(hass) -> MockConfigEntry: +def mock_config_entry(hass: HomeAssistant) -> MockConfigEntry: """Return the default mocked config entry.""" config_entry = MockConfigEntry( title="ESPHome Device", diff --git a/tests/components/fan/common.py b/tests/components/fan/common.py index 74939342fac..0b4243e4144 100644 --- a/tests/components/fan/common.py +++ b/tests/components/fan/common.py @@ -25,12 +25,13 @@ from homeassistant.const import ( SERVICE_TURN_OFF, SERVICE_TURN_ON, ) +from homeassistant.core import HomeAssistant from tests.common import MockEntity async def async_turn_on( - hass, + hass: HomeAssistant, entity_id=ENTITY_MATCH_ALL, percentage: int | None = None, preset_mode: str | None = None, @@ -50,7 +51,7 @@ async def async_turn_on( await hass.async_block_till_done() -async def async_turn_off(hass, entity_id=ENTITY_MATCH_ALL) -> None: +async def async_turn_off(hass: HomeAssistant, entity_id=ENTITY_MATCH_ALL) -> None: """Turn all or specified fan off.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} @@ -59,7 +60,7 @@ async def async_turn_off(hass, entity_id=ENTITY_MATCH_ALL) -> None: async def async_oscillate( - hass, entity_id=ENTITY_MATCH_ALL, should_oscillate: bool = True + hass: HomeAssistant, entity_id=ENTITY_MATCH_ALL, should_oscillate: bool = True ) -> None: """Set oscillation on all or specified fan.""" data = { @@ -76,7 +77,7 @@ async def async_oscillate( async def async_set_preset_mode( - hass, entity_id=ENTITY_MATCH_ALL, preset_mode: str | None = None + hass: HomeAssistant, entity_id=ENTITY_MATCH_ALL, preset_mode: str | None = None ) -> None: """Set preset mode for all or specified fan.""" data = { @@ -90,7 +91,7 @@ async def async_set_preset_mode( async def async_set_percentage( - hass, entity_id=ENTITY_MATCH_ALL, percentage: int | None = None + hass: HomeAssistant, entity_id=ENTITY_MATCH_ALL, percentage: int | None = None ) -> None: """Set percentage for all or specified fan.""" data = { @@ -104,7 +105,7 @@ async def async_set_percentage( async def async_increase_speed( - hass, entity_id=ENTITY_MATCH_ALL, percentage_step: int | None = None + hass: HomeAssistant, entity_id=ENTITY_MATCH_ALL, percentage_step: int | None = None ) -> None: """Increase speed for all or specified fan.""" data = { @@ -121,7 +122,7 @@ async def async_increase_speed( async def async_decrease_speed( - hass, entity_id=ENTITY_MATCH_ALL, percentage_step: int | None = None + hass: HomeAssistant, entity_id=ENTITY_MATCH_ALL, percentage_step: int | None = None ) -> None: """Decrease speed for all or specified fan.""" data = { @@ -138,7 +139,7 @@ async def async_decrease_speed( async def async_set_direction( - hass, entity_id=ENTITY_MATCH_ALL, direction: str | None = None + hass: HomeAssistant, entity_id=ENTITY_MATCH_ALL, direction: str | None = None ) -> None: """Set direction for all or specified fan.""" data = { diff --git a/tests/components/freedompro/conftest.py b/tests/components/freedompro/conftest.py index daafc7e8dc7..91eecc24f27 100644 --- a/tests/components/freedompro/conftest.py +++ b/tests/components/freedompro/conftest.py @@ -10,6 +10,7 @@ import pytest from typing_extensions import Generator from homeassistant.components.freedompro.const import DOMAIN +from homeassistant.core import HomeAssistant from .const import DEVICES, DEVICES_STATE @@ -45,7 +46,7 @@ def mock_freedompro(): @pytest.fixture -async def init_integration(hass) -> MockConfigEntry: +async def init_integration(hass: HomeAssistant) -> MockConfigEntry: """Set up the Freedompro integration in Home Assistant.""" entry = MockConfigEntry( domain=DOMAIN, @@ -64,7 +65,7 @@ async def init_integration(hass) -> MockConfigEntry: @pytest.fixture -async def init_integration_no_state(hass) -> MockConfigEntry: +async def init_integration_no_state(hass: HomeAssistant) -> MockConfigEntry: """Set up the Freedompro integration in Home Assistant without state.""" entry = MockConfigEntry( domain=DOMAIN, diff --git a/tests/components/gios/__init__.py b/tests/components/gios/__init__.py index 435b3209199..07dbd6502b4 100644 --- a/tests/components/gios/__init__.py +++ b/tests/components/gios/__init__.py @@ -4,6 +4,7 @@ import json from unittest.mock import patch from homeassistant.components.gios.const import DOMAIN +from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry, load_fixture @@ -14,7 +15,7 @@ STATIONS = [ async def init_integration( - hass, incomplete_data=False, invalid_indexes=False + hass: HomeAssistant, incomplete_data=False, invalid_indexes=False ) -> MockConfigEntry: """Set up the GIOS integration in Home Assistant.""" entry = MockConfigEntry( diff --git a/tests/components/imgw_pib/__init__.py b/tests/components/imgw_pib/__init__.py index c684b596949..adea1c40925 100644 --- a/tests/components/imgw_pib/__init__.py +++ b/tests/components/imgw_pib/__init__.py @@ -1,9 +1,13 @@ """Tests for the IMGW-PIB integration.""" +from homeassistant.core import HomeAssistant + from tests.common import MockConfigEntry -async def init_integration(hass, config_entry: MockConfigEntry) -> MockConfigEntry: +async def init_integration( + hass: HomeAssistant, config_entry: MockConfigEntry +) -> MockConfigEntry: """Set up the IMGW-PIB integration in Home Assistant.""" config_entry.add_to_hass(hass) diff --git a/tests/components/kodi/__init__.py b/tests/components/kodi/__init__.py index d55a67ba235..f78207be404 100644 --- a/tests/components/kodi/__init__.py +++ b/tests/components/kodi/__init__.py @@ -11,13 +11,14 @@ from homeassistant.const import ( CONF_SSL, CONF_USERNAME, ) +from homeassistant.core import HomeAssistant from .util import MockConnection from tests.common import MockConfigEntry -async def init_integration(hass) -> MockConfigEntry: +async def init_integration(hass: HomeAssistant) -> MockConfigEntry: """Set up the Kodi integration in Home Assistant.""" entry_data = { CONF_NAME: "name", diff --git a/tests/components/litejet/__init__.py b/tests/components/litejet/__init__.py index 3116d9e810d..bf992836043 100644 --- a/tests/components/litejet/__init__.py +++ b/tests/components/litejet/__init__.py @@ -3,13 +3,14 @@ from homeassistant.components import scene, switch from homeassistant.components.litejet import DOMAIN from homeassistant.const import CONF_PORT +from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from tests.common import MockConfigEntry async def async_init_integration( - hass, use_switch=False, use_scene=False + hass: HomeAssistant, use_switch: bool = False, use_scene: bool = False ) -> MockConfigEntry: """Set up the LiteJet integration in Home Assistant.""" diff --git a/tests/components/loqed/test_init.py b/tests/components/loqed/test_init.py index ed38b63fdb1..e6bff2203a9 100644 --- a/tests/components/loqed/test_init.py +++ b/tests/components/loqed/test_init.py @@ -144,7 +144,7 @@ async def test_setup_cloudhook_from_entry_in_bridge( async def test_unload_entry( - hass, integration: MockConfigEntry, lock: loqed.Lock + hass: HomeAssistant, integration: MockConfigEntry, lock: loqed.Lock ) -> None: """Test successful unload of entry.""" @@ -157,7 +157,7 @@ async def test_unload_entry( async def test_unload_entry_fails( - hass, integration: MockConfigEntry, lock: loqed.Lock + hass: HomeAssistant, integration: MockConfigEntry, lock: loqed.Lock ) -> None: """Test unsuccessful unload of entry.""" lock.deleteWebhook = AsyncMock(side_effect=Exception) diff --git a/tests/components/lutron_caseta/__init__.py b/tests/components/lutron_caseta/__init__.py index cc785f71e19..9b25e2a0164 100644 --- a/tests/components/lutron_caseta/__init__.py +++ b/tests/components/lutron_caseta/__init__.py @@ -9,6 +9,7 @@ from homeassistant.components.lutron_caseta.const import ( CONF_KEYFILE, ) from homeassistant.const import CONF_HOST +from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry @@ -83,7 +84,7 @@ _LEAP_DEVICE_TYPES = { } -async def async_setup_integration(hass, mock_bridge) -> MockConfigEntry: +async def async_setup_integration(hass: HomeAssistant, mock_bridge) -> MockConfigEntry: """Set up a mock bridge.""" mock_entry = MockConfigEntry(domain=DOMAIN, data=ENTRY_MOCK_DATA) mock_entry.add_to_hass(hass) diff --git a/tests/components/met/__init__.py b/tests/components/met/__init__.py index 8ea5ce605f0..6556c96bff9 100644 --- a/tests/components/met/__init__.py +++ b/tests/components/met/__init__.py @@ -4,11 +4,14 @@ from unittest.mock import patch from homeassistant.components.met.const import CONF_TRACK_HOME, DOMAIN from homeassistant.const import CONF_ELEVATION, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME +from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry -async def init_integration(hass, track_home=False) -> MockConfigEntry: +async def init_integration( + hass: HomeAssistant, track_home: bool = False +) -> MockConfigEntry: """Set up the Met integration in Home Assistant.""" entry_data = { CONF_NAME: "test", diff --git a/tests/components/met_eireann/__init__.py b/tests/components/met_eireann/__init__.py index 86c3090b0ca..c38f197691a 100644 --- a/tests/components/met_eireann/__init__.py +++ b/tests/components/met_eireann/__init__.py @@ -4,11 +4,12 @@ from unittest.mock import patch from homeassistant.components.met_eireann.const import DOMAIN from homeassistant.const import CONF_ELEVATION, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME +from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry -async def init_integration(hass) -> MockConfigEntry: +async def init_integration(hass: HomeAssistant) -> MockConfigEntry: """Set up the Met Éireann integration in Home Assistant.""" entry_data = { CONF_NAME: "test", diff --git a/tests/components/motioneye/test_media_source.py b/tests/components/motioneye/test_media_source.py index f895ed7fcb2..f8a750d50da 100644 --- a/tests/components/motioneye/test_media_source.py +++ b/tests/components/motioneye/test_media_source.py @@ -74,7 +74,7 @@ _LOGGER = logging.getLogger(__name__) @pytest.fixture(autouse=True) -async def setup_media_source(hass) -> None: +async def setup_media_source(hass: HomeAssistant) -> None: """Set up media source.""" assert await async_setup_component(hass, "media_source", {}) diff --git a/tests/components/nam/__init__.py b/tests/components/nam/__init__.py index 9b254de452c..e7560f8f7ce 100644 --- a/tests/components/nam/__init__.py +++ b/tests/components/nam/__init__.py @@ -3,6 +3,7 @@ from unittest.mock import AsyncMock, Mock, patch from homeassistant.components.nam.const import DOMAIN +from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry, load_json_object_fixture @@ -12,7 +13,9 @@ INCOMPLETE_NAM_DATA = { } -async def init_integration(hass, co2_sensor=True) -> MockConfigEntry: +async def init_integration( + hass: HomeAssistant, co2_sensor: bool = True +) -> MockConfigEntry: """Set up the Nettigo Air Monitor integration in Home Assistant.""" entry = MockConfigEntry( domain=DOMAIN, diff --git a/tests/components/nest/test_camera.py b/tests/components/nest/test_camera.py index 8db86f5d8c1..1838c18b6d4 100644 --- a/tests/components/nest/test_camera.py +++ b/tests/components/nest/test_camera.py @@ -12,6 +12,7 @@ import aiohttp from freezegun import freeze_time from google_nest_sdm.event import EventMessage import pytest +from typing_extensions import Generator from homeassistant.components import camera from homeassistant.components.camera import STATE_IDLE, STATE_STREAMING, StreamType @@ -149,7 +150,7 @@ def make_stream_url_response( @pytest.fixture -async def mock_create_stream(hass) -> Mock: +async def mock_create_stream(hass: HomeAssistant) -> Generator[AsyncMock]: """Fixture to mock out the create stream call.""" assert await async_setup_component(hass, "stream", {}) with patch( diff --git a/tests/components/nest/test_media_source.py b/tests/components/nest/test_media_source.py index bbc08229d37..f4fb8bdb623 100644 --- a/tests/components/nest/test_media_source.py +++ b/tests/components/nest/test_media_source.py @@ -95,7 +95,7 @@ def platforms() -> list[str]: @pytest.fixture(autouse=True) -async def setup_components(hass) -> None: +async def setup_components(hass: HomeAssistant) -> None: """Fixture to initialize the integration.""" await async_setup_component(hass, "media_source", {}) diff --git a/tests/components/nightscout/__init__.py b/tests/components/nightscout/__init__.py index da421d5bba9..551ecffbed1 100644 --- a/tests/components/nightscout/__init__.py +++ b/tests/components/nightscout/__init__.py @@ -8,6 +8,7 @@ from py_nightscout.models import SGV, ServerStatus from homeassistant.components.nightscout.const import DOMAIN from homeassistant.const import CONF_URL +from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry @@ -30,7 +31,7 @@ SERVER_STATUS_STATUS_ONLY = ServerStatus.new_from_json_dict( ) -async def init_integration(hass) -> MockConfigEntry: +async def init_integration(hass: HomeAssistant) -> MockConfigEntry: """Set up the Nightscout integration in Home Assistant.""" entry = MockConfigEntry( domain=DOMAIN, @@ -53,7 +54,7 @@ async def init_integration(hass) -> MockConfigEntry: return entry -async def init_integration_unavailable(hass) -> MockConfigEntry: +async def init_integration_unavailable(hass: HomeAssistant) -> MockConfigEntry: """Set up the Nightscout integration in Home Assistant.""" entry = MockConfigEntry( domain=DOMAIN, @@ -76,7 +77,7 @@ async def init_integration_unavailable(hass) -> MockConfigEntry: return entry -async def init_integration_empty_response(hass) -> MockConfigEntry: +async def init_integration_empty_response(hass: HomeAssistant) -> MockConfigEntry: """Set up the Nightscout integration in Home Assistant.""" entry = MockConfigEntry( domain=DOMAIN, diff --git a/tests/components/nina/test_init.py b/tests/components/nina/test_init.py index 5a6b9ab07dd..620b01fdeb8 100644 --- a/tests/components/nina/test_init.py +++ b/tests/components/nina/test_init.py @@ -22,7 +22,7 @@ ENTRY_DATA: dict[str, Any] = { } -async def init_integration(hass) -> MockConfigEntry: +async def init_integration(hass: HomeAssistant) -> MockConfigEntry: """Set up the NINA integration in Home Assistant.""" with patch( diff --git a/tests/components/nzbget/__init__.py b/tests/components/nzbget/__init__.py index d8fa2f87233..e91f6e35e08 100644 --- a/tests/components/nzbget/__init__.py +++ b/tests/components/nzbget/__init__.py @@ -13,6 +13,7 @@ from homeassistant.const import ( CONF_USERNAME, CONF_VERIFY_SSL, ) +from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry @@ -59,7 +60,7 @@ MOCK_HISTORY = [ ] -async def init_integration(hass) -> MockConfigEntry: +async def init_integration(hass: HomeAssistant) -> MockConfigEntry: """Set up the NZBGet integration in Home Assistant.""" entry = MockConfigEntry(domain=DOMAIN, data=ENTRY_CONFIG, options=ENTRY_OPTIONS) entry.add_to_hass(hass) diff --git a/tests/components/octoprint/__init__.py b/tests/components/octoprint/__init__.py index 0a35d0a2267..dd3eda0e81f 100644 --- a/tests/components/octoprint/__init__.py +++ b/tests/components/octoprint/__init__.py @@ -14,6 +14,8 @@ from pyoctoprintapi import ( from homeassistant.components.octoprint import DOMAIN from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant from homeassistant.helpers.typing import UNDEFINED, UndefinedType from tests.common import MockConfigEntry @@ -33,11 +35,11 @@ DEFAULT_PRINTER = { async def init_integration( - hass, - platform, + hass: HomeAssistant, + platform: Platform, printer: dict[str, Any] | UndefinedType | None = UNDEFINED, job: dict[str, Any] | None = None, -): +) -> None: """Set up the octoprint integration in Home Assistant.""" printer_info: OctoprintPrinterInfo | None = None if printer is UNDEFINED: diff --git a/tests/components/onvif/__init__.py b/tests/components/onvif/__init__.py index 1e7c3273ced..0857dfef798 100644 --- a/tests/components/onvif/__init__.py +++ b/tests/components/onvif/__init__.py @@ -18,6 +18,7 @@ from homeassistant.components.onvif.models import ( WebHookManagerState, ) from homeassistant.const import HTTP_DIGEST_AUTHENTICATION +from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry @@ -158,7 +159,7 @@ def setup_mock_device(mock_device, capabilities=None): async def setup_onvif_integration( - hass, + hass: HomeAssistant, config=None, options=None, unique_id=MAC, diff --git a/tests/components/owntracks/test_device_tracker.py b/tests/components/owntracks/test_device_tracker.py index 16ce8223845..8246a7f51ac 100644 --- a/tests/components/owntracks/test_device_tracker.py +++ b/tests/components/owntracks/test_device_tracker.py @@ -290,7 +290,9 @@ BAD_JSON_SUFFIX = "** and it ends here ^^" @pytest.fixture def setup_comp( - hass, mock_device_tracker_conf: list[Device], mqtt_mock: MqttMockHAClient + hass: HomeAssistant, + mock_device_tracker_conf: list[Device], + mqtt_mock: MqttMockHAClient, ): """Initialize components.""" hass.loop.run_until_complete(async_setup_component(hass, "device_tracker", {})) diff --git a/tests/components/plex/conftest.py b/tests/components/plex/conftest.py index 40b61dfb17a..a061d9c1105 100644 --- a/tests/components/plex/conftest.py +++ b/tests/components/plex/conftest.py @@ -8,6 +8,7 @@ from typing_extensions import Generator from homeassistant.components.plex.const import DOMAIN, PLEX_SERVER_CONFIG, SERVERS from homeassistant.const import CONF_URL +from homeassistant.core import HomeAssistant from .const import DEFAULT_DATA, DEFAULT_OPTIONS, PLEX_DIRECT_URL from .helpers import websocket_connected @@ -546,7 +547,7 @@ def mock_plex_calls( @pytest.fixture def setup_plex_server( - hass, + hass: HomeAssistant, entry, livetv_sessions, mock_websocket, diff --git a/tests/components/plex/test_init.py b/tests/components/plex/test_init.py index f718e6c86ad..15af78faf65 100644 --- a/tests/components/plex/test_init.py +++ b/tests/components/plex/test_init.py @@ -270,7 +270,7 @@ async def test_setup_when_certificate_changed( assert old_entry.data[const.PLEX_SERVER_CONFIG][CONF_URL] == new_url -async def test_tokenless_server(hass, entry, setup_plex_server) -> None: +async def test_tokenless_server(hass: HomeAssistant, entry, setup_plex_server) -> None: """Test setup with a server with token auth disabled.""" TOKENLESS_DATA = copy.deepcopy(DEFAULT_DATA) TOKENLESS_DATA[const.PLEX_SERVER_CONFIG].pop(CONF_TOKEN, None) diff --git a/tests/components/powerwall/mocks.py b/tests/components/powerwall/mocks.py index 10b070a0db7..e43ccee16f1 100644 --- a/tests/components/powerwall/mocks.py +++ b/tests/components/powerwall/mocks.py @@ -16,12 +16,16 @@ from tesla_powerwall import ( SiteMasterResponse, ) +from homeassistant.core import HomeAssistant + from tests.common import load_fixture MOCK_GATEWAY_DIN = "111-0----2-000000000FFA" -async def _mock_powerwall_with_fixtures(hass, empty_meters: bool = False) -> MagicMock: +async def _mock_powerwall_with_fixtures( + hass: HomeAssistant, empty_meters: bool = False +) -> MagicMock: """Mock data used to build powerwall state.""" async with asyncio.TaskGroup() as tg: meters_file = "meters_empty.json" if empty_meters else "meters.json" diff --git a/tests/components/rfxtrx/conftest.py b/tests/components/rfxtrx/conftest.py index 5e0223173f9..88450638d6c 100644 --- a/tests/components/rfxtrx/conftest.py +++ b/tests/components/rfxtrx/conftest.py @@ -10,6 +10,7 @@ from RFXtrx import Connect, RFXtrxTransport from homeassistant.components import rfxtrx from homeassistant.components.rfxtrx import DOMAIN +from homeassistant.core import HomeAssistant from homeassistant.util.dt import utcnow from tests.common import MockConfigEntry, async_fire_time_changed @@ -37,7 +38,7 @@ def create_rfx_test_cfg( async def setup_rfx_test_cfg( - hass, + hass: HomeAssistant, device="abcd", automatic_add=False, devices: dict[str, dict] | None = None, diff --git a/tests/components/rtsp_to_webrtc/conftest.py b/tests/components/rtsp_to_webrtc/conftest.py index cdb7a9d0cfc..6e790b4ff00 100644 --- a/tests/components/rtsp_to_webrtc/conftest.py +++ b/tests/components/rtsp_to_webrtc/conftest.py @@ -39,7 +39,7 @@ async def webrtc_server() -> None: @pytest.fixture -async def mock_camera(hass) -> AsyncGenerator[None]: +async def mock_camera(hass: HomeAssistant) -> AsyncGenerator[None]: """Initialize a demo camera platform.""" assert await async_setup_component( hass, "camera", {camera.DOMAIN: {"platform": "demo"}} diff --git a/tests/components/sia/test_config_flow.py b/tests/components/sia/test_config_flow.py index 36f2292bdea..95de53d7fbe 100644 --- a/tests/components/sia/test_config_flow.py +++ b/tests/components/sia/test_config_flow.py @@ -139,7 +139,7 @@ async def entry_with_additional_account_config(hass, flow_at_add_account_step): ) -async def setup_sia(hass, config_entry: MockConfigEntry): +async def setup_sia(hass: HomeAssistant, config_entry: MockConfigEntry): """Add mock config to HASS.""" assert await async_setup_component(hass, DOMAIN, {}) config_entry.add_to_hass(hass) diff --git a/tests/components/smartthings/conftest.py b/tests/components/smartthings/conftest.py index baef9d9fa82..17e2c781989 100644 --- a/tests/components/smartthings/conftest.py +++ b/tests/components/smartthings/conftest.py @@ -55,7 +55,9 @@ from tests.components.light.conftest import mock_light_profiles # noqa: F401 COMPONENT_PREFIX = "homeassistant.components.smartthings." -async def setup_platform(hass, platform: str, *, devices=None, scenes=None): +async def setup_platform( + hass: HomeAssistant, platform: str, *, devices=None, scenes=None +): """Set up the SmartThings platform and prerequisites.""" hass.config.components.add(DOMAIN) config_entry = MockConfigEntry( diff --git a/tests/components/stream/test_hls.py b/tests/components/stream/test_hls.py index 6d0b1e12ab8..ce66848a2b1 100644 --- a/tests/components/stream/test_hls.py +++ b/tests/components/stream/test_hls.py @@ -46,7 +46,7 @@ HLS_CONFIG = { @pytest.fixture -async def setup_component(hass) -> None: +async def setup_component(hass: HomeAssistant) -> None: """Test fixture to setup the stream component.""" await async_setup_component(hass, "stream", HLS_CONFIG) diff --git a/tests/components/system_log/test_init.py b/tests/components/system_log/test_init.py index 94d3a1dd400..918d995fab9 100644 --- a/tests/components/system_log/test_init.py +++ b/tests/components/system_log/test_init.py @@ -13,6 +13,7 @@ from unittest.mock import MagicMock, patch from homeassistant.bootstrap import async_setup_component from homeassistant.components import system_log from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.typing import ConfigType from tests.common import async_capture_events from tests.typing import WebSocketGenerator @@ -89,7 +90,9 @@ class WatchLogErrorHandler(system_log.LogErrorHandler): self.watch_event.set() -async def async_setup_system_log(hass, config) -> WatchLogErrorHandler: +async def async_setup_system_log( + hass: HomeAssistant, config: ConfigType +) -> WatchLogErrorHandler: """Set up the system_log component.""" WatchLogErrorHandler.instances = [] with patch( diff --git a/tests/components/unifi/conftest.py b/tests/components/unifi/conftest.py index cbb570088c6..4a7d86eea38 100644 --- a/tests/components/unifi/conftest.py +++ b/tests/components/unifi/conftest.py @@ -65,7 +65,7 @@ def fixture_discovery(): @pytest.fixture(name="mock_device_registry") -def fixture_device_registry(hass, device_registry: dr.DeviceRegistry): +def fixture_device_registry(hass: HomeAssistant, device_registry: dr.DeviceRegistry): """Mock device registry.""" config_entry = MockConfigEntry(domain="something_else") config_entry.add_to_hass(hass) @@ -139,7 +139,9 @@ def fixture_known_wireless_clients() -> list[str]: @pytest.fixture(autouse=True, name="mock_wireless_client_storage") -def fixture_wireless_client_storage(hass_storage, known_wireless_clients: list[str]): +def fixture_wireless_client_storage( + hass_storage: dict[str, Any], known_wireless_clients: list[str] +): """Mock the known wireless storage.""" data: dict[str, list[str]] = ( {"wireless_clients": known_wireless_clients} if known_wireless_clients else {} diff --git a/tests/components/v2c/__init__.py b/tests/components/v2c/__init__.py index 6cb6662b850..02f8ade6179 100644 --- a/tests/components/v2c/__init__.py +++ b/tests/components/v2c/__init__.py @@ -1,9 +1,13 @@ """Tests for the V2C integration.""" +from homeassistant.core import HomeAssistant + from tests.common import MockConfigEntry -async def init_integration(hass, config_entry: MockConfigEntry) -> MockConfigEntry: +async def init_integration( + hass: HomeAssistant, config_entry: MockConfigEntry +) -> MockConfigEntry: """Set up the V2C integration in Home Assistant.""" config_entry.add_to_hass(hass) diff --git a/tests/components/valve/test_init.py b/tests/components/valve/test_init.py index 704f690f2f8..3ef3b1ff4b0 100644 --- a/tests/components/valve/test_init.py +++ b/tests/components/valve/test_init.py @@ -131,7 +131,7 @@ def config_flow_fixture(hass: HomeAssistant) -> Generator[None]: @pytest.fixture -def mock_config_entry(hass) -> tuple[MockConfigEntry, list[ValveEntity]]: +def mock_config_entry(hass: HomeAssistant) -> tuple[MockConfigEntry, list[ValveEntity]]: """Mock a config entry which sets up a couple of valve entities.""" entities = [ MockBinaryValveEntity( diff --git a/tests/components/voip/conftest.py b/tests/components/voip/conftest.py index bcd9becbc5a..b039a49e0f0 100644 --- a/tests/components/voip/conftest.py +++ b/tests/components/voip/conftest.py @@ -17,7 +17,7 @@ from tests.common import MockConfigEntry @pytest.fixture(autouse=True) -async def load_homeassistant(hass) -> None: +async def load_homeassistant(hass: HomeAssistant) -> None: """Load the homeassistant integration.""" assert await async_setup_component(hass, "homeassistant", {}) diff --git a/tests/components/ws66i/test_media_player.py b/tests/components/ws66i/test_media_player.py index 2784d74d292..a66e79bf9e0 100644 --- a/tests/components/ws66i/test_media_player.py +++ b/tests/components/ws66i/test_media_player.py @@ -138,7 +138,7 @@ async def test_setup_success(hass: HomeAssistant) -> None: assert hass.states.get(ZONE_1_ID) is not None -async def _setup_ws66i(hass, ws66i) -> MockConfigEntry: +async def _setup_ws66i(hass: HomeAssistant, ws66i) -> MockConfigEntry: config_entry = MockConfigEntry( domain=DOMAIN, data=MOCK_CONFIG, options=MOCK_DEFAULT_OPTIONS ) @@ -154,7 +154,7 @@ async def _setup_ws66i(hass, ws66i) -> MockConfigEntry: return config_entry -async def _setup_ws66i_with_options(hass, ws66i) -> MockConfigEntry: +async def _setup_ws66i_with_options(hass: HomeAssistant, ws66i) -> MockConfigEntry: config_entry = MockConfigEntry( domain=DOMAIN, data=MOCK_CONFIG, options=MOCK_OPTIONS ) diff --git a/tests/components/xiaomi_ble/test_device_trigger.py b/tests/components/xiaomi_ble/test_device_trigger.py index 404eb6a4258..87a4d340d8c 100644 --- a/tests/components/xiaomi_ble/test_device_trigger.py +++ b/tests/components/xiaomi_ble/test_device_trigger.py @@ -35,7 +35,9 @@ def calls(hass: HomeAssistant) -> list[ServiceCall]: return async_mock_service(hass, "test", "automation") -async def _async_setup_xiaomi_device(hass, mac: str, data: Any | None = None): +async def _async_setup_xiaomi_device( + hass: HomeAssistant, mac: str, data: Any | None = None +): config_entry = MockConfigEntry(domain=DOMAIN, unique_id=mac, data=data) config_entry.add_to_hass(hass) diff --git a/tests/components/zha/common.py b/tests/components/zha/common.py index addf1e24ea9..a8bec33a23a 100644 --- a/tests/components/zha/common.py +++ b/tests/components/zha/common.py @@ -14,6 +14,7 @@ from homeassistant.components.zha.core.helpers import ( async_get_zha_config_value, get_zha_gateway, ) +from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er import homeassistant.util.dt as dt_util @@ -102,7 +103,9 @@ def send_attribute_report(hass, cluster, attrid, value): return send_attributes_report(hass, cluster, {attrid: value}) -async def send_attributes_report(hass, cluster: zigpy.zcl.Cluster, attributes: dict): +async def send_attributes_report( + hass: HomeAssistant, cluster: zigpy.zcl.Cluster, attributes: dict +): """Cause the sensor to receive an attribute report from the network. This is to simulate the normal device communication that happens when a diff --git a/tests/components/zha/conftest.py b/tests/components/zha/conftest.py index 326c3cfcd76..410eaceda76 100644 --- a/tests/components/zha/conftest.py +++ b/tests/components/zha/conftest.py @@ -29,6 +29,7 @@ import homeassistant.components.zha.core.const as zha_const import homeassistant.components.zha.core.device as zha_core_device from homeassistant.components.zha.core.gateway import ZHAGateway from homeassistant.components.zha.core.helpers import get_zha_gateway +from homeassistant.core import HomeAssistant from homeassistant.helpers import restore_state from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util @@ -198,7 +199,7 @@ async def zigpy_app_controller(): @pytest.fixture(name="config_entry") -async def config_entry_fixture(hass) -> MockConfigEntry: +async def config_entry_fixture() -> MockConfigEntry: """Fixture representing a config entry.""" return MockConfigEntry( version=3, @@ -243,7 +244,9 @@ def mock_zigpy_connect( @pytest.fixture def setup_zha( - hass, config_entry: MockConfigEntry, mock_zigpy_connect: ControllerApplication + hass: HomeAssistant, + config_entry: MockConfigEntry, + mock_zigpy_connect: ControllerApplication, ): """Set up ZHA component.""" zha_config = {zha_const.CONF_ENABLE_QUIRKS: False} @@ -395,7 +398,7 @@ def zha_device_joined_restored(request: pytest.FixtureRequest): @pytest.fixture def zha_device_mock( - hass, config_entry, zigpy_device_mock + hass: HomeAssistant, config_entry, zigpy_device_mock ) -> Callable[..., zha_core_device.ZHADevice]: """Return a ZHA Device factory.""" diff --git a/tests/components/zha/test_discover.py b/tests/components/zha/test_discover.py index de30bc44b87..c59acc3395f 100644 --- a/tests/components/zha/test_discover.py +++ b/tests/components/zha/test_discover.py @@ -493,7 +493,7 @@ async def test_group_probe_cleanup_called( async def test_quirks_v2_entity_discovery( - hass, + hass: HomeAssistant, zigpy_device_mock, zha_device_joined, ) -> None: @@ -561,7 +561,7 @@ async def test_quirks_v2_entity_discovery( async def test_quirks_v2_entity_discovery_e1_curtain( - hass, + hass: HomeAssistant, zigpy_device_mock, zha_device_joined, ) -> None: diff --git a/tests/components/zha/test_light.py b/tests/components/zha/test_light.py index fda5971cbf7..a9d32362863 100644 --- a/tests/components/zha/test_light.py +++ b/tests/components/zha/test_light.py @@ -1466,7 +1466,11 @@ async def async_test_off_from_hass(hass, cluster, entity_id): async def async_test_level_on_off_from_hass( - hass, on_off_cluster, level_cluster, entity_id, expected_default_transition: int = 0 + hass: HomeAssistant, + on_off_cluster, + level_cluster, + entity_id, + expected_default_transition: int = 0, ): """Test on off functionality from hass.""" From 453564fd03fc406d4fb6132d50abcd1af6d8dee5 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 14 Jun 2024 09:27:18 +0200 Subject: [PATCH 0685/1445] Force full CI on all root test files (#119673) --- .core_files.yaml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/.core_files.yaml b/.core_files.yaml index f59b84ddbf1..067a6a2b41d 100644 --- a/.core_files.yaml +++ b/.core_files.yaml @@ -120,24 +120,20 @@ tests: &tests - pylint/** - requirements_test_pre_commit.txt - requirements_test.txt + - tests/*.py - tests/auth/** - tests/backports/** - - tests/common.py - tests/components/history/** - tests/components/logbook/** - tests/components/recorder/** - tests/components/sensor/** - - tests/conftest.py - tests/hassfest/** - tests/helpers/** - - tests/ignore_uncaught_exceptions.py - tests/mock/** - tests/pylint/** - tests/scripts/** - - tests/syrupy.py - tests/test_util/** - tests/testing_config/** - - tests/typing.py - tests/util/** other: &other From fb801946bbeb43d3c35c1b46a31c8a397b54dd50 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 14 Jun 2024 09:27:54 +0200 Subject: [PATCH 0686/1445] Bump github/codeql-action from 3.25.9 to 3.25.10 (#119669) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/codeql.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 09f30a2a96d..641f349408a 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -24,11 +24,11 @@ jobs: uses: actions/checkout@v4.1.7 - name: Initialize CodeQL - uses: github/codeql-action/init@v3.25.9 + uses: github/codeql-action/init@v3.25.10 with: languages: python - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3.25.9 + uses: github/codeql-action/analyze@v3.25.10 with: category: "/language:python" From b80f7185b2e8e3a04727a59032a517cb87c9ce19 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 14 Jun 2024 09:28:17 +0200 Subject: [PATCH 0687/1445] Bump codecov/codecov-action from 4.4.1 to 4.5.0 (#119668) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 912ca464ef0..1dc1c5af289 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -1109,7 +1109,7 @@ jobs: pattern: coverage-* - name: Upload coverage to Codecov if: needs.info.outputs.test_full_suite == 'true' - uses: codecov/codecov-action@v4.4.1 + uses: codecov/codecov-action@v4.5.0 with: fail_ci_if_error: true flags: full-suite @@ -1244,7 +1244,7 @@ jobs: pattern: coverage-* - name: Upload coverage to Codecov if: needs.info.outputs.test_full_suite == 'false' - uses: codecov/codecov-action@v4.4.1 + uses: codecov/codecov-action@v4.5.0 with: fail_ci_if_error: true token: ${{ secrets.CODECOV_TOKEN }} From 01be5d5f6be6229cd8b8255ecdda2528b5ae9d9c Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 14 Jun 2024 13:32:42 +0200 Subject: [PATCH 0688/1445] Move fixtures to decorators in core tests (#119675) --- tests/components/sensirion_ble/test_sensor.py | 5 +- .../helpers/test_config_entry_oauth2_flow.py | 19 ++--- tests/helpers/test_translation.py | 17 ++--- tests/scripts/test_check_config.py | 35 ++++----- tests/test_bootstrap.py | 24 +++---- tests/test_config.py | 5 +- tests/test_loader.py | 71 ++++++++----------- tests/test_setup.py | 10 ++- tests/test_test_fixtures.py | 3 +- 9 files changed, 80 insertions(+), 109 deletions(-) diff --git a/tests/components/sensirion_ble/test_sensor.py b/tests/components/sensirion_ble/test_sensor.py index 10dcb91ed22..cc95303a4ee 100644 --- a/tests/components/sensirion_ble/test_sensor.py +++ b/tests/components/sensirion_ble/test_sensor.py @@ -2,6 +2,8 @@ from __future__ import annotations +import pytest + from homeassistant.components.sensirion_ble.const import DOMAIN from homeassistant.components.sensor import ATTR_STATE_CLASS from homeassistant.const import ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT @@ -13,7 +15,8 @@ from tests.common import MockConfigEntry from tests.components.bluetooth import inject_bluetooth_service_info -async def test_sensors(enable_bluetooth: None, hass: HomeAssistant) -> None: +@pytest.mark.usefixtures("enable_bluetooth") +async def test_sensors(hass: HomeAssistant) -> None: """Test the Sensirion BLE sensors.""" entry = MockConfigEntry(domain=DOMAIN, unique_id=SENSIRION_SERVICE_INFO.address) entry.add_to_hass(hass) diff --git a/tests/helpers/test_config_entry_oauth2_flow.py b/tests/helpers/test_config_entry_oauth2_flow.py index a9e69f542f3..18e1712f764 100644 --- a/tests/helpers/test_config_entry_oauth2_flow.py +++ b/tests/helpers/test_config_entry_oauth2_flow.py @@ -133,8 +133,9 @@ async def test_missing_credentials_for_domain( assert result["reason"] == "missing_credentials" +@pytest.mark.usefixtures("current_request_with_host") async def test_abort_if_authorization_timeout( - hass: HomeAssistant, flow_handler, local_impl, current_request_with_host: None + hass: HomeAssistant, flow_handler, local_impl ) -> None: """Check timeout generating authorization url.""" flow_handler.async_register_implementation(hass, local_impl) @@ -152,8 +153,9 @@ async def test_abort_if_authorization_timeout( assert result["reason"] == "authorize_url_timeout" +@pytest.mark.usefixtures("current_request_with_host") async def test_abort_if_no_url_available( - hass: HomeAssistant, flow_handler, local_impl, current_request_with_host: None + hass: HomeAssistant, flow_handler, local_impl ) -> None: """Check no_url_available generating authorization url.""" flow_handler.async_register_implementation(hass, local_impl) @@ -171,13 +173,13 @@ async def test_abort_if_no_url_available( @pytest.mark.parametrize("expires_in_dict", [{}, {"expires_in": "badnumber"}]) +@pytest.mark.usefixtures("current_request_with_host") async def test_abort_if_oauth_error( hass: HomeAssistant, flow_handler, local_impl, hass_client_no_auth: ClientSessionGenerator, aioclient_mock: AiohttpClientMocker, - current_request_with_host: None, expires_in_dict: dict[str, str], ) -> None: """Check bad oauth token.""" @@ -234,13 +236,12 @@ async def test_abort_if_oauth_error( assert result["reason"] == "oauth_error" +@pytest.mark.usefixtures("current_request_with_host") async def test_abort_if_oauth_rejected( hass: HomeAssistant, flow_handler, local_impl, hass_client_no_auth: ClientSessionGenerator, - aioclient_mock: AiohttpClientMocker, - current_request_with_host: None, ) -> None: """Check bad oauth token.""" flow_handler.async_register_implementation(hass, local_impl) @@ -289,13 +290,13 @@ async def test_abort_if_oauth_rejected( assert result["description_placeholders"] == {"error": "access_denied"} +@pytest.mark.usefixtures("current_request_with_host") async def test_abort_on_oauth_timeout_error( hass: HomeAssistant, flow_handler, local_impl, hass_client_no_auth: ClientSessionGenerator, aioclient_mock: AiohttpClientMocker, - current_request_with_host: None, ) -> None: """Check timeout during oauth token exchange.""" flow_handler.async_register_implementation(hass, local_impl) @@ -423,13 +424,13 @@ async def test_abort_discovered_multiple( ), ], ) +@pytest.mark.usefixtures("current_request_with_host") async def test_abort_if_oauth_token_error( hass: HomeAssistant, flow_handler, local_impl, hass_client_no_auth: ClientSessionGenerator, aioclient_mock: AiohttpClientMocker, - current_request_with_host: None, status_code: HTTPStatus, error_body: dict[str, Any], error_reason: str, @@ -487,13 +488,13 @@ async def test_abort_if_oauth_token_error( assert error_log in caplog.text +@pytest.mark.usefixtures("current_request_with_host") async def test_abort_if_oauth_token_closing_error( hass: HomeAssistant, flow_handler, local_impl, hass_client_no_auth: ClientSessionGenerator, aioclient_mock: AiohttpClientMocker, - current_request_with_host: None, caplog: pytest.LogCaptureFixture, ) -> None: """Check error when obtaining an oauth token.""" @@ -573,13 +574,13 @@ async def test_abort_discovered_existing_entries( assert result["reason"] == "already_configured" +@pytest.mark.usefixtures("current_request_with_host") async def test_full_flow( hass: HomeAssistant, flow_handler, local_impl, hass_client_no_auth: ClientSessionGenerator, aioclient_mock: AiohttpClientMocker, - current_request_with_host: None, ) -> None: """Check full flow.""" flow_handler.async_register_implementation(hass, local_impl) diff --git a/tests/helpers/test_translation.py b/tests/helpers/test_translation.py index da81016e153..73cd243a0c6 100644 --- a/tests/helpers/test_translation.py +++ b/tests/helpers/test_translation.py @@ -126,9 +126,9 @@ def test_load_translations_files_by_language( ), ], ) +@pytest.mark.usefixtures("enable_custom_integrations") async def test_load_translations_files_invalid_localized_placeholders( hass: HomeAssistant, - enable_custom_integrations: None, caplog: pytest.LogCaptureFixture, language: str, expected_translation: dict, @@ -151,9 +151,8 @@ async def test_load_translations_files_invalid_localized_placeholders( ) -async def test_get_translations( - hass: HomeAssistant, mock_config_flows, enable_custom_integrations: None -) -> None: +@pytest.mark.usefixtures("enable_custom_integrations") +async def test_get_translations(hass: HomeAssistant, mock_config_flows) -> None: """Test the get translations helper.""" translations = await translation.async_get_translations(hass, "en", "entity") assert translations == {} @@ -484,18 +483,16 @@ async def test_caching(hass: HomeAssistant) -> None: assert len(mock_build.mock_calls) > 1 -async def test_custom_component_translations( - hass: HomeAssistant, enable_custom_integrations: None -) -> None: +@pytest.mark.usefixtures("enable_custom_integrations") +async def test_custom_component_translations(hass: HomeAssistant) -> None: """Test getting translation from custom components.""" hass.config.components.add("test_embedded") hass.config.components.add("test_package") assert await translation.async_get_translations(hass, "en", "state") == {} -async def test_get_cached_translations( - hass: HomeAssistant, mock_config_flows, enable_custom_integrations: None -) -> None: +@pytest.mark.usefixtures("enable_custom_integrations") +async def test_get_cached_translations(hass: HomeAssistant, mock_config_flows) -> None: """Test the get cached translations helper.""" translations = await translation.async_get_translations(hass, "en", "entity") assert translations == {} diff --git a/tests/scripts/test_check_config.py b/tests/scripts/test_check_config.py index 8838e9c3b31..7e3c1abbb22 100644 --- a/tests/scripts/test_check_config.py +++ b/tests/scripts/test_check_config.py @@ -1,6 +1,5 @@ """Test check_config script.""" -from asyncio import AbstractEventLoop import logging from unittest.mock import patch @@ -56,9 +55,8 @@ def normalize_yaml_files(check_dict): @pytest.mark.parametrize("hass_config_yaml", [BAD_CORE_CONFIG]) -def test_bad_core_config( - mock_is_file: None, event_loop: AbstractEventLoop, mock_hass_config_yaml: None -) -> None: +@pytest.mark.usefixtures("mock_is_file", "event_loop", "mock_hass_config_yaml") +def test_bad_core_config() -> None: """Test a bad core config setup.""" res = check_config.check(get_test_config_dir()) assert res["except"].keys() == {"homeassistant"} @@ -67,9 +65,8 @@ def test_bad_core_config( @pytest.mark.parametrize("hass_config_yaml", [BASE_CONFIG + "light:\n platform: demo"]) -def test_config_platform_valid( - mock_is_file: None, event_loop: AbstractEventLoop, mock_hass_config_yaml: None -) -> None: +@pytest.mark.usefixtures("mock_is_file", "event_loop", "mock_hass_config_yaml") +def test_config_platform_valid() -> None: """Test a valid platform setup.""" res = check_config.check(get_test_config_dir()) assert res["components"].keys() == {"homeassistant", "light"} @@ -99,13 +96,8 @@ def test_config_platform_valid( ), ], ) -def test_component_platform_not_found( - mock_is_file: None, - event_loop: AbstractEventLoop, - mock_hass_config_yaml: None, - platforms: set[str], - error: str, -) -> None: +@pytest.mark.usefixtures("mock_is_file", "event_loop", "mock_hass_config_yaml") +def test_component_platform_not_found(platforms: set[str], error: str) -> None: """Test errors if component or platform not found.""" # Make sure they don't exist res = check_config.check(get_test_config_dir()) @@ -129,9 +121,8 @@ def test_component_platform_not_found( } ], ) -def test_secrets( - mock_is_file: None, event_loop: AbstractEventLoop, mock_hass_config_yaml: None -) -> None: +@pytest.mark.usefixtures("mock_is_file", "event_loop", "mock_hass_config_yaml") +def test_secrets() -> None: """Test secrets config checking method.""" res = check_config.check(get_test_config_dir(), True) @@ -160,9 +151,8 @@ def test_secrets( @pytest.mark.parametrize( "hass_config_yaml", [BASE_CONFIG + ' packages:\n p1:\n group: ["a"]'] ) -def test_package_invalid( - mock_is_file: None, event_loop: AbstractEventLoop, mock_hass_config_yaml: None -) -> None: +@pytest.mark.usefixtures("mock_is_file", "event_loop", "mock_hass_config_yaml") +def test_package_invalid() -> None: """Test an invalid package.""" res = check_config.check(get_test_config_dir()) @@ -178,9 +168,8 @@ def test_package_invalid( @pytest.mark.parametrize( "hass_config_yaml", [BASE_CONFIG + "automation: !include no.yaml"] ) -def test_bootstrap_error( - event_loop: AbstractEventLoop, mock_hass_config_yaml: None -) -> None: +@pytest.mark.usefixtures("event_loop", "mock_hass_config_yaml") +def test_bootstrap_error() -> None: """Test a valid platform setup.""" res = check_config.check(get_test_config_dir(YAML_CONFIG_FILE)) err = res["except"].pop(check_config.ERROR_STR) diff --git a/tests/test_bootstrap.py b/tests/test_bootstrap.py index 225720fb604..110a41e4216 100644 --- a/tests/test_bootstrap.py +++ b/tests/test_bootstrap.py @@ -139,8 +139,8 @@ async def test_config_does_not_turn_off_debug(hass: HomeAssistant) -> None: @pytest.mark.parametrize("hass_config", [{"frontend": {}}]) +@pytest.mark.usefixtures("mock_hass_config") async def test_asyncio_debug_on_turns_hass_debug_on( - mock_hass_config: None, mock_enable_logging: Mock, mock_is_virtual_env: Mock, mock_mount_local_lib_path: AsyncMock, @@ -632,8 +632,8 @@ def mock_ensure_config_exists() -> Generator[AsyncMock]: @pytest.mark.parametrize("hass_config", [{"browser": {}, "frontend": {}}]) +@pytest.mark.usefixtures("mock_hass_config") async def test_setup_hass( - mock_hass_config: None, mock_enable_logging: Mock, mock_is_virtual_env: Mock, mock_mount_local_lib_path: AsyncMock, @@ -685,8 +685,8 @@ async def test_setup_hass( @pytest.mark.parametrize("hass_config", [{"browser": {}, "frontend": {}}]) +@pytest.mark.usefixtures("mock_hass_config") async def test_setup_hass_takes_longer_than_log_slow_startup( - mock_hass_config: None, mock_enable_logging: Mock, mock_is_virtual_env: Mock, mock_mount_local_lib_path: AsyncMock, @@ -815,8 +815,8 @@ async def test_setup_hass_recovery_mode( assert len(browser_setup.mock_calls) == 0 +@pytest.mark.usefixtures("mock_hass_config") async def test_setup_hass_safe_mode( - mock_hass_config: None, mock_enable_logging: Mock, mock_is_virtual_env: Mock, mock_mount_local_lib_path: AsyncMock, @@ -850,8 +850,8 @@ async def test_setup_hass_safe_mode( assert "Starting in safe mode" in caplog.text +@pytest.mark.usefixtures("mock_hass_config") async def test_setup_hass_recovery_mode_and_safe_mode( - mock_hass_config: None, mock_enable_logging: Mock, mock_is_virtual_env: Mock, mock_mount_local_lib_path: AsyncMock, @@ -886,8 +886,8 @@ async def test_setup_hass_recovery_mode_and_safe_mode( @pytest.mark.parametrize("hass_config", [{"homeassistant": {"non-existing": 1}}]) +@pytest.mark.usefixtures("mock_hass_config") async def test_setup_hass_invalid_core_config( - mock_hass_config: None, mock_enable_logging: Mock, mock_is_virtual_env: Mock, mock_mount_local_lib_path: AsyncMock, @@ -925,8 +925,8 @@ async def test_setup_hass_invalid_core_config( } ], ) +@pytest.mark.usefixtures("mock_hass_config") async def test_setup_recovery_mode_if_no_frontend( - mock_hass_config: None, mock_enable_logging: Mock, mock_is_virtual_env: Mock, mock_mount_local_lib_path: AsyncMock, @@ -1372,10 +1372,9 @@ async def test_bootstrap_does_not_preload_stage_1_integrations() -> None: @pytest.mark.parametrize("load_registries", [False]) +@pytest.mark.usefixtures("enable_custom_integrations") async def test_cancellation_does_not_leak_upward_from_async_setup( - hass: HomeAssistant, - caplog: pytest.LogCaptureFixture, - enable_custom_integrations: None, + hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: """Test setting up an integration that raises asyncio.CancelledError.""" await bootstrap.async_setup_multi_components( @@ -1390,10 +1389,9 @@ async def test_cancellation_does_not_leak_upward_from_async_setup( @pytest.mark.parametrize("load_registries", [False]) +@pytest.mark.usefixtures("enable_custom_integrations") async def test_cancellation_does_not_leak_upward_from_async_setup_entry( - hass: HomeAssistant, - caplog: pytest.LogCaptureFixture, - enable_custom_integrations: None, + hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: """Test setting up an integration that raises asyncio.CancelledError.""" entry = MockConfigEntry( diff --git a/tests/test_config.py b/tests/test_config.py index 9a44333e20c..73e14fee10a 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -1071,9 +1071,8 @@ async def test_check_ha_config_file_wrong(mock_check, hass: HomeAssistant) -> No } ], ) -async def test_async_hass_config_yaml_merge( - merge_log_err, hass: HomeAssistant, mock_hass_config: None -) -> None: +@pytest.mark.usefixtures("mock_hass_config") +async def test_async_hass_config_yaml_merge(merge_log_err, hass: HomeAssistant) -> None: """Test merge during async config reload.""" conf = await config_util.async_hass_config_yaml(hass) diff --git a/tests/test_loader.py b/tests/test_loader.py index 8cda75e0321..a45bec516f6 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -106,9 +106,8 @@ async def test_helpers_wrapper(hass: HomeAssistant) -> None: assert result == ["hello"] -async def test_custom_component_name( - hass: HomeAssistant, enable_custom_integrations: None -) -> None: +@pytest.mark.usefixtures("enable_custom_integrations") +async def test_custom_component_name(hass: HomeAssistant) -> None: """Test the name attribute of custom components.""" with pytest.raises(loader.IntegrationNotFound): await loader.async_get_integration(hass, "test_standalone") @@ -137,10 +136,9 @@ async def test_custom_component_name( assert TEST == 5 +@pytest.mark.usefixtures("enable_custom_integrations") async def test_log_warning_custom_component( - hass: HomeAssistant, - caplog: pytest.LogCaptureFixture, - enable_custom_integrations: None, + hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: """Test that we log a warning when loading a custom component.""" @@ -151,10 +149,9 @@ async def test_log_warning_custom_component( assert "We found a custom integration test " in caplog.text +@pytest.mark.usefixtures("enable_custom_integrations") async def test_custom_integration_version_not_valid( - hass: HomeAssistant, - caplog: pytest.LogCaptureFixture, - enable_custom_integrations: None, + hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: """Test that we log a warning when custom integrations have a invalid version.""" with pytest.raises(loader.IntegrationNotFound): @@ -180,10 +177,10 @@ async def test_custom_integration_version_not_valid( loader.BlockedIntegration(AwesomeVersion("2.0.0"), "breaks Home Assistant"), ], ) +@pytest.mark.usefixtures("enable_custom_integrations") async def test_custom_integration_version_blocked( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, - enable_custom_integrations: None, blocked_versions, ) -> None: """Test that we log a warning when custom integrations have a blocked version.""" @@ -207,10 +204,10 @@ async def test_custom_integration_version_blocked( loader.BlockedIntegration(AwesomeVersion("1.0.0"), "breaks Home Assistant"), ], ) +@pytest.mark.usefixtures("enable_custom_integrations") async def test_custom_integration_version_not_blocked( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, - enable_custom_integrations: None, blocked_versions, ) -> None: """Test that we log a warning when custom integrations have a blocked version.""" @@ -493,9 +490,8 @@ async def test_async_get_platforms_caches_failures_when_component_loaded( assert integration.get_platform_cached("light") is None -async def test_get_integration_legacy( - hass: HomeAssistant, enable_custom_integrations: None -) -> None: +@pytest.mark.usefixtures("enable_custom_integrations") +async def test_get_integration_legacy(hass: HomeAssistant) -> None: """Test resolving integration.""" integration = await loader.async_get_integration(hass, "test_embedded") assert integration.get_component().DOMAIN == "test_embedded" @@ -503,9 +499,8 @@ async def test_get_integration_legacy( assert integration.get_platform_cached("switch") is not None -async def test_get_integration_custom_component( - hass: HomeAssistant, enable_custom_integrations: None -) -> None: +@pytest.mark.usefixtures("enable_custom_integrations") +async def test_get_integration_custom_component(hass: HomeAssistant) -> None: """Test resolving integration.""" integration = await loader.async_get_integration(hass, "test_package") @@ -802,9 +797,8 @@ def _get_test_integration_with_usb_matcher(hass, name, config_flow): ) -async def test_get_custom_components( - hass: HomeAssistant, enable_custom_integrations: None -) -> None: +@pytest.mark.usefixtures("enable_custom_integrations") +async def test_get_custom_components(hass: HomeAssistant) -> None: """Verify that custom components are cached.""" test_1_integration = _get_test_integration(hass, "test_1", False) test_2_integration = _get_test_integration(hass, "test_2", True) @@ -1000,9 +994,8 @@ async def test_get_mqtt(hass: HomeAssistant) -> None: assert mqtt["test_2"] == ["test_2/discovery"] -async def test_import_platform_executor( - hass: HomeAssistant, enable_custom_integrations: None -) -> None: +@pytest.mark.usefixtures("enable_custom_integrations") +async def test_import_platform_executor(hass: HomeAssistant) -> None: """Test import a platform in the executor.""" integration = await loader.async_get_integration( hass, "test_package_loaded_executor" @@ -1342,10 +1335,9 @@ async def test_async_get_component_preloads_config_and_config_flow( } +@pytest.mark.usefixtures("enable_custom_integrations") async def test_async_get_component_loads_loop_if_already_in_sys_modules( - hass: HomeAssistant, - caplog: pytest.LogCaptureFixture, - enable_custom_integrations: None, + hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: """Verify async_get_component does not create an executor job if the module is already in sys.modules.""" integration = await loader.async_get_integration( @@ -1407,10 +1399,8 @@ async def test_async_get_component_loads_loop_if_already_in_sys_modules( assert module is module_mock -async def test_async_get_component_concurrent_loads( - hass: HomeAssistant, - enable_custom_integrations: None, -) -> None: +@pytest.mark.usefixtures("enable_custom_integrations") +async def test_async_get_component_concurrent_loads(hass: HomeAssistant) -> None: """Verify async_get_component waits if the first load if called again when still in progress.""" integration = await loader.async_get_integration( hass, "test_package_loaded_executor" @@ -1720,9 +1710,8 @@ async def test_async_get_platform_raises_after_import_failure( assert "loaded_executor=False" not in caplog.text -async def test_platforms_exists( - hass: HomeAssistant, enable_custom_integrations: None -) -> None: +@pytest.mark.usefixtures("enable_custom_integrations") +async def test_platforms_exists(hass: HomeAssistant) -> None: """Test platforms_exists.""" original_os_listdir = os.listdir @@ -1778,10 +1767,9 @@ async def test_platforms_exists( assert integration.platforms_are_loaded(["other"]) is False +@pytest.mark.usefixtures("enable_custom_integrations") async def test_async_get_platforms_loads_loop_if_already_in_sys_modules( - hass: HomeAssistant, - caplog: pytest.LogCaptureFixture, - enable_custom_integrations: None, + hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: """Verify async_get_platforms does not create an executor job. @@ -1881,10 +1869,8 @@ async def test_async_get_platforms_loads_loop_if_already_in_sys_modules( assert integration.get_platform_cached("light") is light_module_mock -async def test_async_get_platforms_concurrent_loads( - hass: HomeAssistant, - enable_custom_integrations: None, -) -> None: +@pytest.mark.usefixtures("enable_custom_integrations") +async def test_async_get_platforms_concurrent_loads(hass: HomeAssistant) -> None: """Verify async_get_platforms waits if the first load if called again. Case is for when when a second load is called @@ -1945,10 +1931,9 @@ async def test_async_get_platforms_concurrent_loads( assert integration.get_platform_cached("button") is button_module_mock +@pytest.mark.usefixtures("enable_custom_integrations") async def test_integration_warnings( - hass: HomeAssistant, - enable_custom_integrations: None, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: """Test integration warnings.""" await loader.async_get_integration(hass, "test_package_loaded_loop") diff --git a/tests/test_setup.py b/tests/test_setup.py index 910a46d3c73..92367b84ab7 100644 --- a/tests/test_setup.py +++ b/tests/test_setup.py @@ -1177,19 +1177,17 @@ async def test_loading_component_loads_translations(hass: HomeAssistant) -> None assert translation.async_translations_loaded(hass, {"comp"}) is True -async def test_importing_integration_in_executor( - hass: HomeAssistant, enable_custom_integrations: None -) -> None: +@pytest.mark.usefixtures("enable_custom_integrations") +async def test_importing_integration_in_executor(hass: HomeAssistant) -> None: """Test we can import an integration in an executor.""" assert await setup.async_setup_component(hass, "test_package_loaded_executor", {}) assert await setup.async_setup_component(hass, "test_package_loaded_executor", {}) await hass.async_block_till_done() +@pytest.mark.usefixtures("enable_custom_integrations") async def test_async_prepare_setup_platform( - hass: HomeAssistant, - enable_custom_integrations: None, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: """Test we can prepare a platform setup.""" integration = await loader.async_get_integration(hass, "test") diff --git a/tests/test_test_fixtures.py b/tests/test_test_fixtures.py index b3ce068289b..78f66ceb549 100644 --- a/tests/test_test_fixtures.py +++ b/tests/test_test_fixtures.py @@ -20,7 +20,8 @@ def test_sockets_disabled() -> None: socket.socket() -def test_sockets_enabled(socket_enabled: None) -> None: +@pytest.mark.usefixtures("socket_enabled") +def test_sockets_enabled() -> None: """Test we can't connect to an address different from 127.0.0.1.""" mysocket = socket.socket() with pytest.raises(pytest_socket.SocketConnectBlockedError): From da64f61083a4f204b4e7294562d1f5f12e235615 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Fri, 14 Jun 2024 14:12:55 +0200 Subject: [PATCH 0689/1445] Add firmware update entities for Reolink IPC channel cameras (#119637) --- homeassistant/components/reolink/__init__.py | 2 +- homeassistant/components/reolink/host.py | 1 + homeassistant/components/reolink/update.py | 139 ++++++++++++++++++- 3 files changed, 136 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/reolink/__init__.py b/homeassistant/components/reolink/__init__.py index 64058caba78..e9b1d7e8c37 100644 --- a/homeassistant/components/reolink/__init__.py +++ b/homeassistant/components/reolink/__init__.py @@ -93,7 +93,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b """Check for firmware updates.""" async with asyncio.timeout(host.api.timeout * (RETRY_ATTEMPTS + 2)): try: - await host.api.check_new_firmware() + await host.api.check_new_firmware(host.firmware_ch_list) except ReolinkError as err: if starting: _LOGGER.debug( diff --git a/homeassistant/components/reolink/host.py b/homeassistant/components/reolink/host.py index e557eb1d60e..83f366005f9 100644 --- a/homeassistant/components/reolink/host.py +++ b/homeassistant/components/reolink/host.py @@ -77,6 +77,7 @@ class ReolinkHost: self._update_cmd: defaultdict[str, defaultdict[int | None, int]] = defaultdict( lambda: defaultdict(int) ) + self.firmware_ch_list: list[int | None] = [] self.webhook_id: str | None = None self._onvif_push_supported: bool = True diff --git a/homeassistant/components/reolink/update.py b/homeassistant/components/reolink/update.py index 2adbd225cef..da3dafe0130 100644 --- a/homeassistant/components/reolink/update.py +++ b/homeassistant/components/reolink/update.py @@ -23,11 +23,24 @@ from homeassistant.helpers.event import async_call_later from . import ReolinkData from .const import DOMAIN -from .entity import ReolinkHostCoordinatorEntity, ReolinkHostEntityDescription +from .entity import ( + ReolinkChannelCoordinatorEntity, + ReolinkChannelEntityDescription, + ReolinkHostCoordinatorEntity, + ReolinkHostEntityDescription, +) POLL_AFTER_INSTALL = 120 +@dataclass(frozen=True, kw_only=True) +class ReolinkUpdateEntityDescription( + UpdateEntityDescription, + ReolinkChannelEntityDescription, +): + """A class that describes update entities.""" + + @dataclass(frozen=True, kw_only=True) class ReolinkHostUpdateEntityDescription( UpdateEntityDescription, @@ -36,6 +49,14 @@ class ReolinkHostUpdateEntityDescription( """A class that describes host update entities.""" +UPDATE_ENTITIES = ( + ReolinkUpdateEntityDescription( + key="firmware", + supported=lambda api, ch: api.supported(ch, "firmware"), + device_class=UpdateDeviceClass.FIRMWARE, + ), +) + HOST_UPDATE_ENTITIES = ( ReolinkHostUpdateEntityDescription( key="firmware", @@ -53,14 +74,115 @@ async def async_setup_entry( """Set up update entities for Reolink component.""" reolink_data: ReolinkData = hass.data[DOMAIN][config_entry.entry_id] - entities: list[ReolinkHostUpdateEntity] = [ - ReolinkHostUpdateEntity(reolink_data, entity_description) - for entity_description in HOST_UPDATE_ENTITIES - if entity_description.supported(reolink_data.host.api) + entities: list[ReolinkUpdateEntity | ReolinkHostUpdateEntity] = [ + ReolinkUpdateEntity(reolink_data, channel, entity_description) + for entity_description in UPDATE_ENTITIES + for channel in reolink_data.host.api.channels + if entity_description.supported(reolink_data.host.api, channel) ] + entities.extend( + [ + ReolinkHostUpdateEntity(reolink_data, entity_description) + for entity_description in HOST_UPDATE_ENTITIES + if entity_description.supported(reolink_data.host.api) + ] + ) async_add_entities(entities) +class ReolinkUpdateEntity( + ReolinkChannelCoordinatorEntity, + UpdateEntity, +): + """Base update entity class for Reolink IP cameras.""" + + entity_description: ReolinkUpdateEntityDescription + _attr_release_url = "https://reolink.com/download-center/" + + def __init__( + self, + reolink_data: ReolinkData, + channel: int, + entity_description: ReolinkUpdateEntityDescription, + ) -> None: + """Initialize Reolink update entity.""" + self.entity_description = entity_description + super().__init__(reolink_data, channel, reolink_data.firmware_coordinator) + self._cancel_update: CALLBACK_TYPE | None = None + + @property + def installed_version(self) -> str | None: + """Version currently in use.""" + return self._host.api.camera_sw_version(self._channel) + + @property + def latest_version(self) -> str | None: + """Latest version available for install.""" + new_firmware = self._host.api.firmware_update_available(self._channel) + if not new_firmware: + return self.installed_version + + if isinstance(new_firmware, str): + return new_firmware + + return new_firmware.version_string + + @property + def supported_features(self) -> UpdateEntityFeature: + """Flag supported features.""" + supported_features = UpdateEntityFeature.INSTALL + new_firmware = self._host.api.firmware_update_available(self._channel) + if isinstance(new_firmware, NewSoftwareVersion): + supported_features |= UpdateEntityFeature.RELEASE_NOTES + return supported_features + + async def async_release_notes(self) -> str | None: + """Return the release notes.""" + new_firmware = self._host.api.firmware_update_available(self._channel) + if not isinstance(new_firmware, NewSoftwareVersion): + return None + + return ( + "If the install button fails, download this" + f" [firmware zip file]({new_firmware.download_url})." + " Then, follow the installation guide (PDF in the zip file).\n\n" + f"## Release notes\n\n{new_firmware.release_notes}" + ) + + async def async_install( + self, version: str | None, backup: bool, **kwargs: Any + ) -> None: + """Install the latest firmware version.""" + try: + await self._host.api.update_firmware(self._channel) + except ReolinkError as err: + raise HomeAssistantError( + f"Error trying to update Reolink firmware: {err}" + ) from err + finally: + self.async_write_ha_state() + self._cancel_update = async_call_later( + self.hass, POLL_AFTER_INSTALL, self._async_update_future + ) + + async def _async_update_future(self, now: datetime | None = None) -> None: + """Request update.""" + await self.async_update() + + async def async_added_to_hass(self) -> None: + """Entity created.""" + await super().async_added_to_hass() + self._host.firmware_ch_list.append(self._channel) + + async def async_will_remove_from_hass(self) -> None: + """Entity removed.""" + await super().async_will_remove_from_hass() + if self._channel in self._host.firmware_ch_list: + self._host.firmware_ch_list.remove(self._channel) + if self._cancel_update is not None: + self._cancel_update() + + class ReolinkHostUpdateEntity( ReolinkHostCoordinatorEntity, UpdateEntity, @@ -139,8 +261,15 @@ class ReolinkHostUpdateEntity( """Request update.""" await self.async_update() + async def async_added_to_hass(self) -> None: + """Entity created.""" + await super().async_added_to_hass() + self._host.firmware_ch_list.append(None) + async def async_will_remove_from_hass(self) -> None: """Entity removed.""" await super().async_will_remove_from_hass() + if None in self._host.firmware_ch_list: + self._host.firmware_ch_list.remove(None) if self._cancel_update is not None: self._cancel_update() From 10a2fd7cb66c2d39c2652c9b85f78e89a0dfc7a9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 14 Jun 2024 10:56:26 -0500 Subject: [PATCH 0690/1445] Bump uiprotect to 1.7.1 (#119694) changelog: https://github.com/uilibs/uiprotect/compare/v1.6.0...v1.7.0 --- homeassistant/components/unifiprotect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index 181f87b4469..4a9822811ef 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -40,7 +40,7 @@ "integration_type": "hub", "iot_class": "local_push", "loggers": ["uiprotect", "unifi_discovery"], - "requirements": ["uiprotect==1.6.0", "unifi-discovery==1.1.8"], + "requirements": ["uiprotect==1.7.1", "unifi-discovery==1.1.8"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/requirements_all.txt b/requirements_all.txt index d38673f02ed..6421a1798d5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2788,7 +2788,7 @@ twitchAPI==4.0.0 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==1.6.0 +uiprotect==1.7.1 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cb9dd30599d..97aa3dbf0fb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2168,7 +2168,7 @@ twitchAPI==4.0.0 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==1.6.0 +uiprotect==1.7.1 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 From 6e322c310b79be29f169a7435669c0d70d9167d6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 14 Jun 2024 13:16:49 -0500 Subject: [PATCH 0691/1445] Split binary sensor classes in unifiprotect (#119696) * Split binary sensor classes in unifiprotect There were two types of binary sensors, ones that can change device_class at run-time (re-mountable ones), and ones that cannot. Instead of having branching in the class, split the class * tweak order to match name --- .../components/unifiprotect/binary_sensor.py | 182 ++++++++++-------- .../unifiprotect/test_binary_sensor.py | 9 +- 2 files changed, 101 insertions(+), 90 deletions(-) diff --git a/homeassistant/components/unifiprotect/binary_sensor.py b/homeassistant/components/unifiprotect/binary_sensor.py index 349b4f9b266..74710427318 100644 --- a/homeassistant/components/unifiprotect/binary_sensor.py +++ b/homeassistant/components/unifiprotect/binary_sensor.py @@ -332,7 +332,9 @@ LIGHT_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( ), ) -SENSE_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( +# The mountable sensors can be remounted at run-time which +# means they can change their device class at run-time. +MOUNTABLE_SENSE_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( ProtectBinaryEntityDescription( key=_KEY_DOOR, name="Contact", @@ -340,6 +342,9 @@ SENSE_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( ufp_value="is_opened", ufp_enabled="is_contact_sensor_enabled", ), +) + +SENSE_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( ProtectBinaryEntityDescription( key="leak", name="Leak", @@ -617,80 +622,9 @@ _MODEL_DESCRIPTIONS: dict[ModelType, Sequence[ProtectRequiredKeysMixin]] = { ModelType.VIEWPORT: VIEWER_SENSORS, } - -async def async_setup_entry( - hass: HomeAssistant, - entry: UFPConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up binary sensors for UniFi Protect integration.""" - data = entry.runtime_data - - @callback - def _add_new_device(device: ProtectAdoptableDeviceModel) -> None: - entities = async_all_device_entities( - data, - ProtectDeviceBinarySensor, - model_descriptions=_MODEL_DESCRIPTIONS, - ufp_device=device, - ) - if device.is_adopted and isinstance(device, Camera): - entities += _async_event_entities(data, ufp_device=device) - async_add_entities(entities) - - data.async_subscribe_adopt(_add_new_device) - - entities = async_all_device_entities( - data, ProtectDeviceBinarySensor, model_descriptions=_MODEL_DESCRIPTIONS - ) - entities += _async_event_entities(data) - entities += _async_nvr_entities(data) - - async_add_entities(entities) - - -@callback -def _async_event_entities( - data: ProtectData, - ufp_device: ProtectAdoptableDeviceModel | None = None, -) -> list[ProtectDeviceEntity]: - entities: list[ProtectDeviceEntity] = [] - devices = data.get_cameras() if ufp_device is None else [ufp_device] - for device in devices: - for description in EVENT_SENSORS: - if not description.has_required(device): - continue - entities.append(ProtectEventBinarySensor(data, device, description)) - _LOGGER.debug( - "Adding binary sensor entity %s for %s", - description.name, - device.display_name, - ) - - return entities - - -@callback -def _async_nvr_entities( - data: ProtectData, -) -> list[BaseProtectEntity]: - entities: list[BaseProtectEntity] = [] - device = data.api.bootstrap.nvr - if device.system_info.ustorage is None: - return entities - - for disk in device.system_info.ustorage.disks: - for description in DISK_SENSORS: - if not disk.has_disk: - continue - - entities.append(ProtectDiskBinarySensor(data, device, description, disk)) - _LOGGER.debug( - "Adding binary sensor entity %s", - f"{disk.type} {disk.slot}", - ) - - return entities +_MOUNTABLE_MODEL_DESCRIPTIONS: dict[ModelType, Sequence[ProtectRequiredKeysMixin]] = { + ModelType.SENSOR: MOUNTABLE_SENSE_SENSORS, +} class ProtectDeviceBinarySensor(ProtectDeviceEntity, BinarySensorEntity): @@ -702,16 +636,7 @@ class ProtectDeviceBinarySensor(ProtectDeviceEntity, BinarySensorEntity): @callback def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None: super()._async_update_device_from_protect(device) - entity_description = self.entity_description - updated_device = self.device - self._attr_is_on = entity_description.get_ufp_value(updated_device) - # UP Sense can be any of the 3 contact sensor device classes - if entity_description.key == _KEY_DOOR and isinstance(updated_device, Sensor): - self._attr_device_class = MOUNT_DEVICE_CLASS_MAP.get( - updated_device.mount_type, BinarySensorDeviceClass.DOOR - ) - else: - self._attr_device_class = self.entity_description.device_class + self._attr_is_on = self.entity_description.get_ufp_value(self.device) @callback def _async_get_state_attrs(self) -> tuple[Any, ...]: @@ -720,7 +645,30 @@ class ProtectDeviceBinarySensor(ProtectDeviceEntity, BinarySensorEntity): Called before and after updating entity and state is only written if there is a change. """ + return (self._attr_available, self._attr_is_on) + +class MountableProtectDeviceBinarySensor(ProtectDeviceBinarySensor): + """A UniFi Protect Device Binary Sensor that can change device class at runtime.""" + + device: Sensor + + @callback + def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None: + super()._async_update_device_from_protect(device) + updated_device = self.device + # UP Sense can be any of the 3 contact sensor device classes + self._attr_device_class = MOUNT_DEVICE_CLASS_MAP.get( + updated_device.mount_type, BinarySensorDeviceClass.DOOR + ) + + @callback + def _async_get_state_attrs(self) -> tuple[Any, ...]: + """Retrieve data that goes into the current state of the entity. + + Called before and after updating entity and state is only written if there + is a change. + """ return (self._attr_available, self._attr_is_on, self._attr_device_class) @@ -805,3 +753,67 @@ class ProtectEventBinarySensor(EventEntityMixin, BinarySensorEntity): self._attr_is_on, self._attr_extra_state_attributes, ) + + +MODEL_DESCRIPTIONS_WITH_CLASS = ( + (_MODEL_DESCRIPTIONS, ProtectDeviceBinarySensor), + (_MOUNTABLE_MODEL_DESCRIPTIONS, MountableProtectDeviceBinarySensor), +) + + +@callback +def _async_event_entities( + data: ProtectData, + ufp_device: ProtectAdoptableDeviceModel | None = None, +) -> list[ProtectDeviceEntity]: + return [ + ProtectEventBinarySensor(data, device, description) + for device in (data.get_cameras() if ufp_device is None else [ufp_device]) + for description in EVENT_SENSORS + if description.has_required(device) + ] + + +@callback +def _async_nvr_entities( + data: ProtectData, +) -> list[BaseProtectEntity]: + device = data.api.bootstrap.nvr + if (ustorage := device.system_info.ustorage) is None: + return [] + return [ + ProtectDiskBinarySensor(data, device, description, disk) + for disk in ustorage.disks + for description in DISK_SENSORS + if disk.has_disk + ] + + +async def async_setup_entry( + hass: HomeAssistant, + entry: UFPConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up binary sensors for UniFi Protect integration.""" + data = entry.runtime_data + + @callback + def _add_new_device(device: ProtectAdoptableDeviceModel) -> None: + entities: list[BaseProtectEntity] = [] + for model_descriptions, klass in MODEL_DESCRIPTIONS_WITH_CLASS: + entities += async_all_device_entities( + data, klass, model_descriptions=model_descriptions, ufp_device=device + ) + if device.is_adopted and isinstance(device, Camera): + entities += _async_event_entities(data, ufp_device=device) + async_add_entities(entities) + + data.async_subscribe_adopt(_add_new_device) + entities: list[BaseProtectEntity] = [] + for model_descriptions, klass in MODEL_DESCRIPTIONS_WITH_CLASS: + entities += async_all_device_entities( + data, klass, model_descriptions=model_descriptions + ) + entities += _async_event_entities(data) + entities += _async_nvr_entities(data) + async_add_entities(entities) diff --git a/tests/components/unifiprotect/test_binary_sensor.py b/tests/components/unifiprotect/test_binary_sensor.py index 3231c233ca3..4674ec289ca 100644 --- a/tests/components/unifiprotect/test_binary_sensor.py +++ b/tests/components/unifiprotect/test_binary_sensor.py @@ -13,6 +13,7 @@ from homeassistant.components.unifiprotect.binary_sensor import ( CAMERA_SENSORS, EVENT_SENSORS, LIGHT_SENSORS, + MOUNTABLE_SENSE_SENSORS, SENSE_SENSORS, ) from homeassistant.components.unifiprotect.const import ( @@ -40,7 +41,7 @@ from .utils import ( ) LIGHT_SENSOR_WRITE = LIGHT_SENSORS[:2] -SENSE_SENSORS_WRITE = SENSE_SENSORS[:4] +SENSE_SENSORS_WRITE = SENSE_SENSORS[:3] async def test_binary_sensor_camera_remove( @@ -209,7 +210,6 @@ async def test_binary_sensor_setup_sensor( assert_entity_counts(hass, Platform.BINARY_SENSOR, 11, 11) expected = [ - STATE_OFF, STATE_UNAVAILABLE, STATE_OFF, STATE_OFF, @@ -243,7 +243,6 @@ async def test_binary_sensor_setup_sensor_leak( assert_entity_counts(hass, Platform.BINARY_SENSOR, 11, 11) expected = [ - STATE_UNAVAILABLE, STATE_OFF, STATE_OFF, STATE_UNAVAILABLE, @@ -367,7 +366,7 @@ async def test_binary_sensor_update_mount_type_window( assert_entity_counts(hass, Platform.BINARY_SENSOR, 11, 11) _, entity_id = ids_from_device_description( - Platform.BINARY_SENSOR, sensor_all, SENSE_SENSORS_WRITE[0] + Platform.BINARY_SENSOR, sensor_all, MOUNTABLE_SENSE_SENSORS[0] ) state = hass.states.get(entity_id) @@ -399,7 +398,7 @@ async def test_binary_sensor_update_mount_type_garage( assert_entity_counts(hass, Platform.BINARY_SENSOR, 11, 11) _, entity_id = ids_from_device_description( - Platform.BINARY_SENSOR, sensor_all, SENSE_SENSORS_WRITE[0] + Platform.BINARY_SENSOR, sensor_all, MOUNTABLE_SENSE_SENSORS[0] ) state = hass.states.get(entity_id) From d2bcd5d1fbb0bb06ac29deefb2f4d06291bb5610 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 14 Jun 2024 13:22:12 -0500 Subject: [PATCH 0692/1445] Refactor unifiprotect switch to match other platforms (#119698) - Use _attr_is_on for nvr entities - implement _async_get_state_attrs for nvr entities - define MODEL_DESCRIPTIONS_WITH_CLASS --- .../components/unifiprotect/switch.py | 125 +++++++++--------- 1 file changed, 61 insertions(+), 64 deletions(-) diff --git a/homeassistant/components/unifiprotect/switch.py b/homeassistant/components/unifiprotect/switch.py index 8a66b285021..7690dc5d62f 100644 --- a/homeassistant/components/unifiprotect/switch.py +++ b/homeassistant/components/unifiprotect/switch.py @@ -24,7 +24,12 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity from .data import ProtectData, UFPConfigEntry -from .entity import ProtectDeviceEntity, ProtectNVREntity, async_all_device_entities +from .entity import ( + BaseProtectEntity, + ProtectDeviceEntity, + ProtectNVREntity, + async_all_device_entities, +) from .models import PermRequired, ProtectRequiredKeysMixin, ProtectSetableKeysMixin, T _LOGGER = logging.getLogger(__name__) @@ -467,55 +472,6 @@ _PRIVACY_MODEL_DESCRIPTIONS: dict[ModelType, Sequence[ProtectRequiredKeysMixin]] } -async def async_setup_entry( - hass: HomeAssistant, - entry: UFPConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up sensors for UniFi Protect integration.""" - data = entry.runtime_data - - @callback - def _add_new_device(device: ProtectAdoptableDeviceModel) -> None: - entities = async_all_device_entities( - data, - ProtectSwitch, - model_descriptions=_MODEL_DESCRIPTIONS, - ufp_device=device, - ) - entities += async_all_device_entities( - data, - ProtectPrivacyModeSwitch, - model_descriptions=_PRIVACY_MODEL_DESCRIPTIONS, - ufp_device=device, - ) - async_add_entities(entities) - - data.async_subscribe_adopt(_add_new_device) - entities = async_all_device_entities( - data, - ProtectSwitch, - model_descriptions=_MODEL_DESCRIPTIONS, - ) - entities += async_all_device_entities( - data, - ProtectPrivacyModeSwitch, - model_descriptions=_PRIVACY_MODEL_DESCRIPTIONS, - ) - - if ( - data.api.bootstrap.nvr.can_write(data.api.bootstrap.auth_user) - and data.api.bootstrap.nvr.is_insights_enabled is not None - ): - for switch in NVR_SWITCHES: - entities.append( - ProtectNVRSwitch( - data, device=data.api.bootstrap.nvr, description=switch - ) - ) - async_add_entities(entities) - - class ProtectSwitch(ProtectDeviceEntity, SwitchEntity): """A UniFi Protect Switch.""" @@ -551,7 +507,6 @@ class ProtectSwitch(ProtectDeviceEntity, SwitchEntity): Called before and after updating entity and state is only written if there is a change. """ - return (self._attr_available, self._attr_is_on) @@ -570,21 +525,27 @@ class ProtectNVRSwitch(ProtectNVREntity, SwitchEntity): super().__init__(data, device, description) self._attr_name = f"{self.device.display_name} {self.entity_description.name}" - @property - def is_on(self) -> bool: - """Return true if device is on.""" - return self.entity_description.get_ufp_value(self.device) is True + def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None: + super()._async_update_device_from_protect(device) + self._attr_is_on = self.entity_description.get_ufp_value(self.device) is True async def async_turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" - await self.entity_description.ufp_set(self.device, True) async def async_turn_off(self, **kwargs: Any) -> None: """Turn the device off.""" - await self.entity_description.ufp_set(self.device, False) + @callback + def _async_get_state_attrs(self) -> tuple[Any, ...]: + """Retrieve data that goes into the current state of the entity. + + Called before and after updating entity and state is only written if there + is a change. + """ + return (self._attr_available, self._attr_is_on) + class ProtectPrivacyModeSwitch(RestoreEntity, ProtectSwitch): """A UniFi Protect Switch.""" @@ -623,21 +584,18 @@ class ProtectPrivacyModeSwitch(RestoreEntity, ProtectSwitch): @callback def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None: super()._async_update_device_from_protect(device) - # do not add extra state attribute on initialize if self.entity_id: self._update_previous_attr() async def async_turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" - self._previous_mic_level = self.device.mic_volume self._previous_record_mode = self.device.recording_settings.mode await self.device.set_privacy(True, 0, RecordingMode.NEVER) async def async_turn_off(self, **kwargs: Any) -> None: """Turn the device off.""" - extra_state = self.extra_state_attributes or {} prev_mic = extra_state.get(ATTR_PREV_MIC, self._previous_mic_level) prev_record = extra_state.get(ATTR_PREV_RECORD, self._previous_record_mode) @@ -646,14 +604,53 @@ class ProtectPrivacyModeSwitch(RestoreEntity, ProtectSwitch): async def async_added_to_hass(self) -> None: """Restore extra state attributes on startp up.""" await super().async_added_to_hass() - if not (last_state := await self.async_get_last_state()): return - - self._previous_mic_level = last_state.attributes.get( + last_attrs = last_state.attributes + self._previous_mic_level = last_attrs.get( ATTR_PREV_MIC, self._previous_mic_level ) - self._previous_record_mode = last_state.attributes.get( + self._previous_record_mode = last_attrs.get( ATTR_PREV_RECORD, self._previous_record_mode ) self._update_previous_attr() + + +MODEL_DESCRIPTIONS_WITH_CLASS = ( + (_MODEL_DESCRIPTIONS, ProtectSwitch), + (_PRIVACY_MODEL_DESCRIPTIONS, ProtectPrivacyModeSwitch), +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: UFPConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up sensors for UniFi Protect integration.""" + data = entry.runtime_data + + @callback + def _add_new_device(device: ProtectAdoptableDeviceModel) -> None: + entities: list[BaseProtectEntity] = [] + for model_descriptions, klass in MODEL_DESCRIPTIONS_WITH_CLASS: + entities += async_all_device_entities( + data, klass, model_descriptions=model_descriptions, ufp_device=device + ) + async_add_entities(entities) + + data.async_subscribe_adopt(_add_new_device) + entities: list[BaseProtectEntity] = [] + for model_descriptions, klass in MODEL_DESCRIPTIONS_WITH_CLASS: + entities += async_all_device_entities( + data, klass, model_descriptions=model_descriptions + ) + + bootstrap = data.api.bootstrap + nvr = bootstrap.nvr + if nvr.can_write(bootstrap.auth_user) and nvr.is_insights_enabled is not None: + entities.extend( + ProtectNVRSwitch(data, device=nvr, description=switch) + for switch in NVR_SWITCHES + ) + async_add_entities(entities) From 6bdfed69100264bf0f76efe12b63afdeaba6e814 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 14 Jun 2024 13:43:40 -0500 Subject: [PATCH 0693/1445] Bump uiprotect to 1.7.2 (#119705) changelog: https://github.com/uilibs/uiprotect/compare/v1.7.1...v1.7.2 --- homeassistant/components/unifiprotect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index 4a9822811ef..ce512ca3f3c 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -40,7 +40,7 @@ "integration_type": "hub", "iot_class": "local_push", "loggers": ["uiprotect", "unifi_discovery"], - "requirements": ["uiprotect==1.7.1", "unifi-discovery==1.1.8"], + "requirements": ["uiprotect==1.7.2", "unifi-discovery==1.1.8"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/requirements_all.txt b/requirements_all.txt index 6421a1798d5..1de4e7bbc52 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2788,7 +2788,7 @@ twitchAPI==4.0.0 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==1.7.1 +uiprotect==1.7.2 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 97aa3dbf0fb..4905155182a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2168,7 +2168,7 @@ twitchAPI==4.0.0 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==1.7.1 +uiprotect==1.7.2 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 From 05cbda0e50cd8c597304451eed1c06a59f818600 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 14 Jun 2024 20:45:27 +0200 Subject: [PATCH 0694/1445] Fix alarm default code in concord232 (#119691) --- homeassistant/components/concord232/alarm_control_panel.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/concord232/alarm_control_panel.py b/homeassistant/components/concord232/alarm_control_panel.py index 2799481ccaa..0256f5aab37 100644 --- a/homeassistant/components/concord232/alarm_control_panel.py +++ b/homeassistant/components/concord232/alarm_control_panel.py @@ -86,6 +86,7 @@ class Concord232Alarm(AlarmControlPanelEntity): self._attr_name = name self._code = code + self._alarm_control_panel_option_default_code = code self._mode = mode self._url = url self._alarm = concord232_client.Client(self._url) From c077c2a972a97b48a5fdc250fd5bdcf2658aa62d Mon Sep 17 00:00:00 2001 From: "Mr. Bubbles" Date: Fri, 14 Jun 2024 20:47:06 +0200 Subject: [PATCH 0695/1445] Fix pyload async_update SensorEntity raising exceptions (#119655) * Fix Sensorentity raising exceptions * Increase test coverage --- homeassistant/components/pyload/sensor.py | 31 +++++++++--------- tests/components/pyload/test_sensor.py | 38 ++++++++++++++++++++--- 2 files changed, 51 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/pyload/sensor.py b/homeassistant/components/pyload/sensor.py index c21e74b18a7..730f0202d5b 100644 --- a/homeassistant/components/pyload/sensor.py +++ b/homeassistant/components/pyload/sensor.py @@ -33,7 +33,6 @@ from homeassistant.helpers.aiohttp_client import async_create_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from homeassistant.helpers.update_coordinator import UpdateFailed from .const import DEFAULT_HOST, DEFAULT_NAME, DEFAULT_PORT @@ -132,20 +131,24 @@ class PyLoadSensor(SensorEntity): _LOGGER.info("Authentication failed, trying to reauthenticate") try: await self.api.login() - except InvalidAuth as e: - raise PlatformNotReady( - f"Authentication failed for {self.api.username}, check your login credentials" - ) from e - else: - raise UpdateFailed( - "Unable to retrieve data due to cookie expiration but re-authentication was successful." + except InvalidAuth: + _LOGGER.error( + "Authentication failed for %s, check your login credentials", + self.api.username, ) - except CannotConnect as e: - raise UpdateFailed( - "Unable to connect and retrieve data from pyLoad API" - ) from e - except ParserError as e: - raise UpdateFailed("Unable to parse data from pyLoad API") from e + return + else: + _LOGGER.info( + "Unable to retrieve data due to cookie expiration " + "but re-authentication was successful" + ) + return + except CannotConnect: + _LOGGER.debug("Unable to connect and retrieve data from pyLoad API") + return + except ParserError: + _LOGGER.error("Unable to parse data from pyLoad API") + return value = getattr(self.data, self.type) diff --git a/tests/components/pyload/test_sensor.py b/tests/components/pyload/test_sensor.py index 54f15deb313..6fd85ba0796 100644 --- a/tests/components/pyload/test_sensor.py +++ b/tests/components/pyload/test_sensor.py @@ -2,15 +2,19 @@ from unittest.mock import AsyncMock +from freezegun.api import FrozenDateTimeFactory from pyloadapi.exceptions import CannotConnect, InvalidAuth, ParserError import pytest from syrupy.assertion import SnapshotAssertion +from homeassistant.components.pyload.sensor import SCAN_INTERVAL from homeassistant.components.sensor import DOMAIN from homeassistant.core import HomeAssistant from homeassistant.helpers.typing import ConfigType from homeassistant.setup import async_setup_component +from tests.common import async_fire_time_changed + @pytest.mark.usefixtures("mock_pyloadapi") async def test_setup( @@ -60,9 +64,9 @@ async def test_setup_exceptions( @pytest.mark.parametrize( ("exception", "expected_exception"), [ - (CannotConnect, "UpdateFailed"), - (ParserError, "UpdateFailed"), - (InvalidAuth, "UpdateFailed"), + (CannotConnect, "Unable to connect and retrieve data from pyLoad API"), + (ParserError, "Unable to parse data from pyLoad API"), + (InvalidAuth, "Authentication failed, trying to reauthenticate"), ], ) async def test_sensor_update_exceptions( @@ -80,5 +84,31 @@ async def test_sensor_update_exceptions( assert await async_setup_component(hass, DOMAIN, pyload_config) await hass.async_block_till_done() - assert len(hass.states.async_all(DOMAIN)) == 0 + assert len(hass.states.async_all(DOMAIN)) == 1 assert expected_exception in caplog.text + + +async def test_sensor_invalid_auth( + hass: HomeAssistant, + pyload_config: ConfigType, + mock_pyloadapi: AsyncMock, + caplog: pytest.LogCaptureFixture, + freezer: FrozenDateTimeFactory, +) -> None: + """Test invalid auth during sensor update.""" + + assert await async_setup_component(hass, DOMAIN, pyload_config) + await hass.async_block_till_done() + assert len(hass.states.async_all(DOMAIN)) == 1 + + mock_pyloadapi.get_status.side_effect = InvalidAuth + mock_pyloadapi.login.side_effect = InvalidAuth + + freezer.tick(SCAN_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert ( + "Authentication failed for username, check your login credentials" + in caplog.text + ) From 6b8bddf6e38f5a3cce493e876b9fd6eb4d2ec65a Mon Sep 17 00:00:00 2001 From: tronikos Date: Fri, 14 Jun 2024 11:47:41 -0700 Subject: [PATCH 0696/1445] Make remaining time of timers available to LLMs (#118696) * Include speech_slots in IntentResponse.as_dict * Populate speech_slots only if available * fix typo * Add test * test all fields * Fix another test --------- Co-authored-by: Paulus Schoutsen --- homeassistant/helpers/intent.py | 2 ++ tests/helpers/test_llm.py | 41 +++++++++++++++++++++++++++++---- 2 files changed, 39 insertions(+), 4 deletions(-) diff --git a/homeassistant/helpers/intent.py b/homeassistant/helpers/intent.py index 8af5dba29f5..b1ddf5eacc7 100644 --- a/homeassistant/helpers/intent.py +++ b/homeassistant/helpers/intent.py @@ -1363,6 +1363,8 @@ class IntentResponse: if self.reprompt: response_dict["reprompt"] = self.reprompt + if self.speech_slots: + response_dict["speech_slots"] = self.speech_slots response_data: dict[str, Any] = {} diff --git a/tests/helpers/test_llm.py b/tests/helpers/test_llm.py index 17a0ef0e73e..e62d9ffdbee 100644 --- a/tests/helpers/test_llm.py +++ b/tests/helpers/test_llm.py @@ -147,8 +147,13 @@ async def test_assist_api( assert test_context.json_fragment # To reproduce an error case in tracing intent_response = intent.IntentResponse("*") - intent_response.matched_states = [State("light.matched", "on")] - intent_response.unmatched_states = [State("light.unmatched", "on")] + intent_response.async_set_states( + [State("light.matched", "on")], [State("light.unmatched", "on")] + ) + intent_response.async_set_speech("Some speech") + intent_response.async_set_card("Card title", "card content") + intent_response.async_set_speech_slots({"hello": 1}) + intent_response.async_set_reprompt("Do it again") tool_input = llm.ToolInput( tool_name="test_intent", tool_args={"area": "kitchen", "floor": "ground_floor"}, @@ -179,8 +184,22 @@ async def test_assist_api( "success": [], "targets": [], }, + "reprompt": { + "plain": { + "extra_data": None, + "reprompt": "Do it again", + }, + }, "response_type": "action_done", - "speech": {}, + "speech": { + "plain": { + "extra_data": None, + "speech": "Some speech", + }, + }, + "speech_slots": { + "hello": 1, + }, } # Call with a device/area/floor @@ -225,7 +244,21 @@ async def test_assist_api( "targets": [], }, "response_type": "action_done", - "speech": {}, + "reprompt": { + "plain": { + "extra_data": None, + "reprompt": "Do it again", + }, + }, + "speech": { + "plain": { + "extra_data": None, + "speech": "Some speech", + }, + }, + "speech_slots": { + "hello": 1, + }, } From f8bf357811cf38a14807011c734230473a3f689a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 14 Jun 2024 14:25:14 -0500 Subject: [PATCH 0697/1445] Remove set default doorbell text service from unifiprotect (#119695) UI has removed this functionality in UI Protect 4.x discovered via https://github.com/uilibs/uiprotect/issues/57 --- .../components/unifiprotect/icons.json | 1 - .../components/unifiprotect/services.py | 13 ------------- .../components/unifiprotect/services.yaml | 12 ------------ .../components/unifiprotect/strings.json | 14 -------------- .../components/unifiprotect/test_services.py | 19 ------------------- 5 files changed, 59 deletions(-) diff --git a/homeassistant/components/unifiprotect/icons.json b/homeassistant/components/unifiprotect/icons.json index b357a892ff4..bb713d4ee79 100644 --- a/homeassistant/components/unifiprotect/icons.json +++ b/homeassistant/components/unifiprotect/icons.json @@ -2,7 +2,6 @@ "services": { "add_doorbell_text": "mdi:message-plus", "remove_doorbell_text": "mdi:message-minus", - "set_default_doorbell_text": "mdi:message-processing", "set_chime_paired_doorbells": "mdi:bell-cog", "remove_privacy_zone": "mdi:eye-minus" } diff --git a/homeassistant/components/unifiprotect/services.py b/homeassistant/components/unifiprotect/services.py index c5c2ffc8bfe..60345ac6403 100644 --- a/homeassistant/components/unifiprotect/services.py +++ b/homeassistant/components/unifiprotect/services.py @@ -32,13 +32,11 @@ SERVICE_ADD_DOORBELL_TEXT = "add_doorbell_text" SERVICE_REMOVE_DOORBELL_TEXT = "remove_doorbell_text" SERVICE_SET_PRIVACY_ZONE = "set_privacy_zone" SERVICE_REMOVE_PRIVACY_ZONE = "remove_privacy_zone" -SERVICE_SET_DEFAULT_DOORBELL_TEXT = "set_default_doorbell_text" SERVICE_SET_CHIME_PAIRED = "set_chime_paired_doorbells" ALL_GLOBAL_SERIVCES = [ SERVICE_ADD_DOORBELL_TEXT, SERVICE_REMOVE_DOORBELL_TEXT, - SERVICE_SET_DEFAULT_DOORBELL_TEXT, SERVICE_SET_CHIME_PAIRED, SERVICE_REMOVE_PRIVACY_ZONE, ] @@ -145,12 +143,6 @@ async def remove_doorbell_text(hass: HomeAssistant, call: ServiceCall) -> None: await _async_service_call_nvr(hass, call, "remove_custom_doorbell_message", message) -async def set_default_doorbell_text(hass: HomeAssistant, call: ServiceCall) -> None: - """Set the default doorbell text message.""" - message: str = call.data[ATTR_MESSAGE] - await _async_service_call_nvr(hass, call, "set_default_doorbell_message", message) - - async def remove_privacy_zone(hass: HomeAssistant, call: ServiceCall) -> None: """Remove privacy zone from camera.""" @@ -231,11 +223,6 @@ def async_setup_services(hass: HomeAssistant) -> None: functools.partial(remove_doorbell_text, hass), DOORBELL_TEXT_SCHEMA, ), - ( - SERVICE_SET_DEFAULT_DOORBELL_TEXT, - functools.partial(set_default_doorbell_text, hass), - DOORBELL_TEXT_SCHEMA, - ), ( SERVICE_SET_CHIME_PAIRED, functools.partial(set_chime_paired_doorbells, hass), diff --git a/homeassistant/components/unifiprotect/services.yaml b/homeassistant/components/unifiprotect/services.yaml index e747b9e7240..192dfd0566f 100644 --- a/homeassistant/components/unifiprotect/services.yaml +++ b/homeassistant/components/unifiprotect/services.yaml @@ -22,18 +22,6 @@ remove_doorbell_text: required: true selector: text: -set_default_doorbell_text: - fields: - device_id: - required: true - selector: - device: - integration: unifiprotect - message: - example: Welcome! - required: true - selector: - text: set_chime_paired_doorbells: fields: device_id: diff --git a/homeassistant/components/unifiprotect/strings.json b/homeassistant/components/unifiprotect/strings.json index 54023a1768f..1435de5011e 100644 --- a/homeassistant/components/unifiprotect/strings.json +++ b/homeassistant/components/unifiprotect/strings.json @@ -168,20 +168,6 @@ } } }, - "set_default_doorbell_text": { - "name": "Set default doorbell text", - "description": "Sets the default doorbell message. This will be the message that is automatically selected when a message \"expires\".", - "fields": { - "device_id": { - "name": "[%key:component::unifiprotect::services::add_doorbell_text::fields::device_id::name%]", - "description": "[%key:component::unifiprotect::services::add_doorbell_text::fields::device_id::description%]" - }, - "message": { - "name": "Default message", - "description": "The default message for your doorbell. Must be less than 30 characters." - } - } - }, "set_chime_paired_doorbells": { "name": "Set chime paired doorbells", "description": "Use to set the paired doorbell(s) with a smart chime.", diff --git a/tests/components/unifiprotect/test_services.py b/tests/components/unifiprotect/test_services.py index b468c2de9a8..6808bacb40c 100644 --- a/tests/components/unifiprotect/test_services.py +++ b/tests/components/unifiprotect/test_services.py @@ -15,7 +15,6 @@ from homeassistant.components.unifiprotect.services import ( SERVICE_REMOVE_DOORBELL_TEXT, SERVICE_REMOVE_PRIVACY_ZONE, SERVICE_SET_CHIME_PAIRED, - SERVICE_SET_DEFAULT_DOORBELL_TEXT, ) from homeassistant.config_entries import ConfigEntryDisabler from homeassistant.const import ATTR_DEVICE_ID, ATTR_ENTITY_ID, ATTR_NAME @@ -125,24 +124,6 @@ async def test_remove_doorbell_text( nvr.remove_custom_doorbell_message.assert_called_once_with("Test Message") -async def test_set_default_doorbell_text( - hass: HomeAssistant, device: dr.DeviceEntry, ufp: MockUFPFixture -) -> None: - """Test set_default_doorbell_text service.""" - - nvr = ufp.api.bootstrap.nvr - nvr.__fields__["set_default_doorbell_message"] = Mock(final=False) - nvr.set_default_doorbell_message = AsyncMock() - - await hass.services.async_call( - DOMAIN, - SERVICE_SET_DEFAULT_DOORBELL_TEXT, - {ATTR_DEVICE_ID: device.id, ATTR_MESSAGE: "Test Message"}, - blocking=True, - ) - nvr.set_default_doorbell_message.assert_called_once_with("Test Message") - - async def test_add_doorbell_text_disabled_config_entry( hass: HomeAssistant, device: dr.DeviceEntry, ufp: MockUFPFixture ) -> None: From c0ff2d866fae518e8e6fd1d0269504653b09e914 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 14 Jun 2024 14:29:18 -0500 Subject: [PATCH 0698/1445] Reduce code needed to check unifiprotect attrs (#119706) * Reduce code needed to check unifiprotect attrs * Apply suggestions from code review * Update homeassistant/components/unifiprotect/manifest.json * Apply suggestions from code review * revert * adjust * tweak * make mypy happy --- .../components/unifiprotect/binary_sensor.py | 51 +++---------------- .../components/unifiprotect/button.py | 2 - .../components/unifiprotect/camera.py | 20 ++------ .../components/unifiprotect/entity.py | 30 +++++------ .../components/unifiprotect/light.py | 11 +--- homeassistant/components/unifiprotect/lock.py | 23 +++------ .../components/unifiprotect/media_player.py | 11 +--- .../components/unifiprotect/number.py | 12 +---- .../components/unifiprotect/select.py | 11 +--- .../components/unifiprotect/sensor.py | 41 +++------------ .../components/unifiprotect/switch.py | 20 +------- homeassistant/components/unifiprotect/text.py | 13 +---- 12 files changed, 49 insertions(+), 196 deletions(-) diff --git a/homeassistant/components/unifiprotect/binary_sensor.py b/homeassistant/components/unifiprotect/binary_sensor.py index 74710427318..4218d3108e5 100644 --- a/homeassistant/components/unifiprotect/binary_sensor.py +++ b/homeassistant/components/unifiprotect/binary_sensor.py @@ -5,7 +5,6 @@ from __future__ import annotations from collections.abc import Sequence import dataclasses import logging -from typing import Any from uiprotect.data import ( NVR, @@ -632,26 +631,23 @@ class ProtectDeviceBinarySensor(ProtectDeviceEntity, BinarySensorEntity): device: Camera | Light | Sensor entity_description: ProtectBinaryEntityDescription + _state_attrs: tuple[str, ...] = ("_attr_available", "_attr_is_on") @callback def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None: super()._async_update_device_from_protect(device) self._attr_is_on = self.entity_description.get_ufp_value(self.device) - @callback - def _async_get_state_attrs(self) -> tuple[Any, ...]: - """Retrieve data that goes into the current state of the entity. - - Called before and after updating entity and state is only written if there - is a change. - """ - return (self._attr_available, self._attr_is_on) - class MountableProtectDeviceBinarySensor(ProtectDeviceBinarySensor): """A UniFi Protect Device Binary Sensor that can change device class at runtime.""" device: Sensor + _state_attrs: tuple[str, ...] = ( + "_attr_available", + "_attr_is_on", + "_attr_device_class", + ) @callback def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None: @@ -662,21 +658,13 @@ class MountableProtectDeviceBinarySensor(ProtectDeviceBinarySensor): updated_device.mount_type, BinarySensorDeviceClass.DOOR ) - @callback - def _async_get_state_attrs(self) -> tuple[Any, ...]: - """Retrieve data that goes into the current state of the entity. - - Called before and after updating entity and state is only written if there - is a change. - """ - return (self._attr_available, self._attr_is_on, self._attr_device_class) - class ProtectDiskBinarySensor(ProtectNVREntity, BinarySensorEntity): """A UniFi Protect NVR Disk Binary Sensor.""" _disk: UOSDisk entity_description: ProtectBinaryEntityDescription + _state_attrs = ("_attr_available", "_attr_is_on") def __init__( self, @@ -715,21 +703,12 @@ class ProtectDiskBinarySensor(ProtectNVREntity, BinarySensorEntity): self._attr_is_on = not self._disk.is_healthy - @callback - def _async_get_state_attrs(self) -> tuple[Any, ...]: - """Retrieve data that goes into the current state of the entity. - - Called before and after updating entity and state is only written if there - is a change. - """ - - return (self._attr_available, self._attr_is_on) - class ProtectEventBinarySensor(EventEntityMixin, BinarySensorEntity): """A UniFi Protect Device Binary Sensor for events.""" entity_description: ProtectBinaryEventEntityDescription + _state_attrs = ("_attr_available", "_attr_is_on", "_attr_extra_state_attributes") @callback def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None: @@ -740,20 +719,6 @@ class ProtectEventBinarySensor(EventEntityMixin, BinarySensorEntity): self._event = None self._attr_extra_state_attributes = {} - @callback - def _async_get_state_attrs(self) -> tuple[Any, ...]: - """Retrieve data that goes into the current state of the entity. - - Called before and after updating entity and state is only written if there - is a change. - """ - - return ( - self._attr_available, - self._attr_is_on, - self._attr_extra_state_attributes, - ) - MODEL_DESCRIPTIONS_WITH_CLASS = ( (_MODEL_DESCRIPTIONS, ProtectDeviceBinarySensor), diff --git a/homeassistant/components/unifiprotect/button.py b/homeassistant/components/unifiprotect/button.py index 265367a9272..7866dd5b183 100644 --- a/homeassistant/components/unifiprotect/button.py +++ b/homeassistant/components/unifiprotect/button.py @@ -186,7 +186,6 @@ class ProtectButton(ProtectDeviceEntity, ButtonEntity): @callback def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None: super()._async_update_device_from_protect(device) - if self.entity_description.key == KEY_ADOPT: device = self.device self._attr_available = device.can_adopt and device.can_create( @@ -195,6 +194,5 @@ class ProtectButton(ProtectDeviceEntity, ButtonEntity): async def async_press(self) -> None: """Press the button.""" - if self.entity_description.ufp_press is not None: await getattr(self.device, self.entity_description.ufp_press)() diff --git a/homeassistant/components/unifiprotect/camera.py b/homeassistant/components/unifiprotect/camera.py index 5f077d3a62e..b4596582cd6 100644 --- a/homeassistant/components/unifiprotect/camera.py +++ b/homeassistant/components/unifiprotect/camera.py @@ -3,7 +3,6 @@ from __future__ import annotations import logging -from typing import Any from typing_extensions import Generator from uiprotect.data import ( @@ -163,6 +162,11 @@ class ProtectCamera(ProtectDeviceEntity, Camera): """A Ubiquiti UniFi Protect Camera.""" device: UFPCamera + _state_attrs = ( + "_attr_available", + "_attr_is_recording", + "_attr_motion_detection_enabled", + ) def __init__( self, @@ -210,20 +214,6 @@ class ProtectCamera(ProtectDeviceEntity, Camera): else: self._attr_supported_features = CameraEntityFeature(0) - @callback - def _async_get_state_attrs(self) -> tuple[Any, ...]: - """Retrieve data that goes into the current state of the entity. - - Called before and after updating entity and state is only written if there - is a change. - """ - - return ( - self._attr_available, - self._attr_is_recording, - self._attr_motion_detection_enabled, - ) - @callback def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None: super()._async_update_device_from_protect(device) diff --git a/homeassistant/components/unifiprotect/entity.py b/homeassistant/components/unifiprotect/entity.py index a41aadfcd89..d1b82dd218f 100644 --- a/homeassistant/components/unifiprotect/entity.py +++ b/homeassistant/components/unifiprotect/entity.py @@ -3,7 +3,9 @@ from __future__ import annotations from collections.abc import Callable, Sequence +from functools import partial import logging +from operator import attrgetter from typing import TYPE_CHECKING, Any from uiprotect.data import ( @@ -161,6 +163,7 @@ class BaseProtectEntity(Entity): device: ProtectAdoptableDeviceModel | NVR _attr_should_poll = False + _state_attrs: tuple[str, ...] = ("_attr_available",) def __init__( self, @@ -194,6 +197,9 @@ class BaseProtectEntity(Entity): self._attr_attribution = DEFAULT_ATTRIBUTION self._async_set_device_info() self._async_update_device_from_protect(device) + self._state_getters = tuple( + partial(attrgetter(attr), self) for attr in self._state_attrs + ) async def async_update(self) -> None: """Update the entity. @@ -233,24 +239,18 @@ class BaseProtectEntity(Entity): and (not async_get_ufp_enabled or async_get_ufp_enabled(device)) ) - @callback - def _async_get_state_attrs(self) -> tuple[Any, ...]: - """Retrieve data that goes into the current state of the entity. - - Called before and after updating entity and state is only written if there - is a change. - """ - - return (self._attr_available,) - @callback def _async_updated_event(self, device: ProtectAdoptableDeviceModel | NVR) -> None: """When device is updated from Protect.""" - - previous_attrs = self._async_get_state_attrs() + previous_attrs = [getter() for getter in self._state_getters] self._async_update_device_from_protect(device) - current_attrs = self._async_get_state_attrs() - if previous_attrs != current_attrs: + changed = False + for idx, getter in enumerate(self._state_getters): + if previous_attrs[idx] != getter(): + changed = True + break + + if changed: if _LOGGER.isEnabledFor(logging.DEBUG): device_name = device.name or "" if hasattr(self, "entity_description") and self.entity_description.name: @@ -261,7 +261,7 @@ class BaseProtectEntity(Entity): device_name, device.mac, previous_attrs, - current_attrs, + tuple((getattr(self, attr)) for attr in self._state_attrs), ) self.async_write_ha_state() diff --git a/homeassistant/components/unifiprotect/light.py b/homeassistant/components/unifiprotect/light.py index e8a51c357a0..651b9c7d3d4 100644 --- a/homeassistant/components/unifiprotect/light.py +++ b/homeassistant/components/unifiprotect/light.py @@ -63,16 +63,7 @@ class ProtectLight(ProtectDeviceEntity, LightEntity): _attr_icon = "mdi:spotlight-beam" _attr_color_mode = ColorMode.BRIGHTNESS _attr_supported_color_modes = {ColorMode.BRIGHTNESS} - - @callback - def _async_get_state_attrs(self) -> tuple[Any, ...]: - """Retrieve data that goes into the current state of the entity. - - Called before and after updating entity and state is only written if there - is a change. - """ - - return (self._attr_available, self._attr_is_on, self._attr_brightness) + _state_attrs = ("_attr_available", "_attr_is_on", "_attr_brightness") @callback def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None: diff --git a/homeassistant/components/unifiprotect/lock.py b/homeassistant/components/unifiprotect/lock.py index 4f5dfe43ce2..52de63cd833 100644 --- a/homeassistant/components/unifiprotect/lock.py +++ b/homeassistant/components/unifiprotect/lock.py @@ -49,6 +49,13 @@ class ProtectLock(ProtectDeviceEntity, LockEntity): device: Doorlock entity_description: LockEntityDescription + _state_attrs = ( + "_attr_available", + "_attr_is_locked", + "_attr_is_locking", + "_attr_is_unlocking", + "_attr_is_jammed", + ) def __init__( self, @@ -64,22 +71,6 @@ class ProtectLock(ProtectDeviceEntity, LockEntity): self._attr_name = f"{self.device.display_name} Lock" - @callback - def _async_get_state_attrs(self) -> tuple[Any, ...]: - """Retrieve data that goes into the current state of the entity. - - Called before and after updating entity and state is only written if there - is a change. - """ - - return ( - self._attr_available, - self._attr_is_locked, - self._attr_is_locking, - self._attr_is_unlocking, - self._attr_is_jammed, - ) - @callback def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None: super()._async_update_device_from_protect(device) diff --git a/homeassistant/components/unifiprotect/media_player.py b/homeassistant/components/unifiprotect/media_player.py index 55a85155d89..dbf5321b3d8 100644 --- a/homeassistant/components/unifiprotect/media_player.py +++ b/homeassistant/components/unifiprotect/media_player.py @@ -69,6 +69,7 @@ class ProtectMediaPlayer(ProtectDeviceEntity, MediaPlayerEntity): | MediaPlayerEntityFeature.STOP | MediaPlayerEntityFeature.BROWSE_MEDIA ) + _state_attrs = ("_attr_available", "_attr_state", "_attr_volume_level") def __init__( self, @@ -107,16 +108,6 @@ class ProtectMediaPlayer(ProtectDeviceEntity, MediaPlayerEntity): ) self._attr_available = is_connected and updated_device.feature_flags.has_speaker - @callback - def _async_get_state_attrs(self) -> tuple[Any, ...]: - """Retrieve data that goes into the current state of the entity. - - Called before and after updating entity and state is only written if there - is a change. - """ - - return (self._attr_available, self._attr_state, self._attr_volume_level) - async def async_set_volume_level(self, volume: float) -> None: """Set volume level, range 0..1.""" diff --git a/homeassistant/components/unifiprotect/number.py b/homeassistant/components/unifiprotect/number.py index c3d0bb8b6b9..44f965e4796 100644 --- a/homeassistant/components/unifiprotect/number.py +++ b/homeassistant/components/unifiprotect/number.py @@ -6,7 +6,6 @@ from collections.abc import Sequence from dataclasses import dataclass from datetime import timedelta import logging -from typing import Any from uiprotect.data import ( Camera, @@ -257,6 +256,7 @@ class ProtectNumbers(ProtectDeviceEntity, NumberEntity): device: Camera | Light entity_description: ProtectNumberEntityDescription + _state_attrs = ("_attr_available", "_attr_native_value") def __init__( self, @@ -278,13 +278,3 @@ class ProtectNumbers(ProtectDeviceEntity, NumberEntity): async def async_set_native_value(self, value: float) -> None: """Set new value.""" await self.entity_description.ufp_set(self.device, value) - - @callback - def _async_get_state_attrs(self) -> tuple[Any, ...]: - """Retrieve data that goes into the current state of the entity. - - Called before and after updating entity and state is only written if there - is a change. - """ - - return (self._attr_available, self._attr_native_value) diff --git a/homeassistant/components/unifiprotect/select.py b/homeassistant/components/unifiprotect/select.py index b253e5a9d18..2dd52fac774 100644 --- a/homeassistant/components/unifiprotect/select.py +++ b/homeassistant/components/unifiprotect/select.py @@ -358,6 +358,7 @@ class ProtectSelects(ProtectDeviceEntity, SelectEntity): device: Camera | Light | Viewer entity_description: ProtectSelectEntityDescription + _state_attrs = ("_attr_available", "_attr_options", "_attr_current_option") def __init__( self, @@ -418,13 +419,3 @@ class ProtectSelects(ProtectDeviceEntity, SelectEntity): if self.entity_description.ufp_enum_type is not None: unifi_value = self.entity_description.ufp_enum_type(unifi_value) await self.entity_description.ufp_set(self.device, unifi_value) - - @callback - def _async_get_state_attrs(self) -> tuple[Any, ...]: - """Retrieve data that goes into the current state of the entity. - - Called before and after updating entity and state is only written if there - is a change. - """ - - return (self._attr_available, self._attr_options, self._attr_current_option) diff --git a/homeassistant/components/unifiprotect/sensor.py b/homeassistant/components/unifiprotect/sensor.py index 754bf3bc82b..56b7ef7f9a4 100644 --- a/homeassistant/components/unifiprotect/sensor.py +++ b/homeassistant/components/unifiprotect/sensor.py @@ -702,60 +702,33 @@ class ProtectDeviceSensor(ProtectDeviceEntity, SensorEntity): """A Ubiquiti UniFi Protect Sensor.""" entity_description: ProtectSensorEntityDescription + _state_attrs = ("_attr_available", "_attr_native_value") def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None: super()._async_update_device_from_protect(device) self._attr_native_value = self.entity_description.get_ufp_value(self.device) - @callback - def _async_get_state_attrs(self) -> tuple[Any, ...]: - """Retrieve data that goes into the current state of the entity. - - Called before and after updating entity and state is only written if there - is a change. - """ - - return (self._attr_available, self._attr_native_value) - class ProtectNVRSensor(ProtectNVREntity, SensorEntity): """A Ubiquiti UniFi Protect Sensor.""" entity_description: ProtectSensorEntityDescription + _state_attrs = ("_attr_available", "_attr_native_value") def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None: super()._async_update_device_from_protect(device) self._attr_native_value = self.entity_description.get_ufp_value(self.device) - @callback - def _async_get_state_attrs(self) -> tuple[Any, ...]: - """Retrieve data that goes into the current state of the entity. - - Called before and after updating entity and state is only written if there - is a change. - """ - - return (self._attr_available, self._attr_native_value) - class ProtectEventSensor(EventEntityMixin, SensorEntity): """A UniFi Protect Device Sensor with access tokens.""" entity_description: ProtectSensorEventEntityDescription - - @callback - def _async_get_state_attrs(self) -> tuple[Any, ...]: - """Retrieve data that goes into the current state of the entity. - - Called before and after updating entity and state is only written if there - is a change. - """ - - return ( - self._attr_available, - self._attr_native_value, - self._attr_extra_state_attributes, - ) + _state_attrs = ( + "_attr_available", + "_attr_native_value", + "_attr_extra_state_attributes", + ) class ProtectLicensePlateEventSensor(ProtectEventSensor): diff --git a/homeassistant/components/unifiprotect/switch.py b/homeassistant/components/unifiprotect/switch.py index 7690dc5d62f..36c2c497b57 100644 --- a/homeassistant/components/unifiprotect/switch.py +++ b/homeassistant/components/unifiprotect/switch.py @@ -476,6 +476,7 @@ class ProtectSwitch(ProtectDeviceEntity, SwitchEntity): """A UniFi Protect Switch.""" entity_description: ProtectSwitchEntityDescription + _state_attrs = ("_attr_available", "_attr_is_on") def __init__( self, @@ -500,20 +501,12 @@ class ProtectSwitch(ProtectDeviceEntity, SwitchEntity): """Turn the device off.""" await self.entity_description.ufp_set(self.device, False) - @callback - def _async_get_state_attrs(self) -> tuple[Any, ...]: - """Retrieve data that goes into the current state of the entity. - - Called before and after updating entity and state is only written if there - is a change. - """ - return (self._attr_available, self._attr_is_on) - class ProtectNVRSwitch(ProtectNVREntity, SwitchEntity): """A UniFi Protect NVR Switch.""" entity_description: ProtectSwitchEntityDescription + _state_attrs = ("_attr_available", "_attr_is_on") def __init__( self, @@ -537,15 +530,6 @@ class ProtectNVRSwitch(ProtectNVREntity, SwitchEntity): """Turn the device off.""" await self.entity_description.ufp_set(self.device, False) - @callback - def _async_get_state_attrs(self) -> tuple[Any, ...]: - """Retrieve data that goes into the current state of the entity. - - Called before and after updating entity and state is only written if there - is a change. - """ - return (self._attr_available, self._attr_is_on) - class ProtectPrivacyModeSwitch(RestoreEntity, ProtectSwitch): """A UniFi Protect Switch.""" diff --git a/homeassistant/components/unifiprotect/text.py b/homeassistant/components/unifiprotect/text.py index acd28a31794..009e013ee51 100644 --- a/homeassistant/components/unifiprotect/text.py +++ b/homeassistant/components/unifiprotect/text.py @@ -4,7 +4,6 @@ from __future__ import annotations from collections.abc import Sequence from dataclasses import dataclass -from typing import Any from uiprotect.data import ( Camera, @@ -87,6 +86,7 @@ class ProtectDeviceText(ProtectDeviceEntity, TextEntity): """A Ubiquiti UniFi Protect Sensor.""" entity_description: ProtectTextEntityDescription + _state_attrs = ("_attr_available", "_attr_native_value") def __init__( self, @@ -102,17 +102,6 @@ class ProtectDeviceText(ProtectDeviceEntity, TextEntity): super()._async_update_device_from_protect(device) self._attr_native_value = self.entity_description.get_ufp_value(self.device) - @callback - def _async_get_state_attrs(self) -> tuple[Any, ...]: - """Retrieve data that goes into the current state of the entity. - - Called before and after updating entity and state is only written if there - is a change. - """ - - return (self._attr_available, self._attr_native_value) - async def async_set_value(self, value: str) -> None: """Change the value.""" - await self.entity_description.ufp_set(self.device, value) From c2e31e984612bd00ad205044fdae096a3dea778b Mon Sep 17 00:00:00 2001 From: Thomas55555 <59625598+Thomas55555@users.noreply.github.com> Date: Fri, 14 Jun 2024 21:34:47 +0200 Subject: [PATCH 0699/1445] Add work area sensor for Husqvarna Automower (#119704) * Add work area sensor to Husqvarna Automower * fix exist_fn --- .../components/husqvarna_automower/sensor.py | 42 ++++++++++++-- .../husqvarna_automower/strings.json | 6 ++ .../snapshots/test_sensor.ambr | 58 +++++++++++++++++++ 3 files changed, 102 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/husqvarna_automower/sensor.py b/homeassistant/components/husqvarna_automower/sensor.py index 4cc3bcf5e57..146ef17a6e4 100644 --- a/homeassistant/components/husqvarna_automower/sensor.py +++ b/homeassistant/components/husqvarna_automower/sensor.py @@ -4,6 +4,7 @@ from collections.abc import Callable from dataclasses import dataclass from datetime import datetime import logging +from typing import TYPE_CHECKING from aioautomower.model import MowerAttributes, MowerModes, RestrictedReasons @@ -14,7 +15,7 @@ from homeassistant.components.sensor import ( SensorStateClass, ) from homeassistant.const import PERCENTAGE, EntityCategory, UnitOfLength, UnitOfTime -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType @@ -184,11 +185,31 @@ RESTRICTED_REASONS: list = [ ] +@callback +def _get_work_area_names(data: MowerAttributes) -> list[str]: + """Return a list with all work area names.""" + if TYPE_CHECKING: + # Sensor does not get created if it is None + assert data.work_areas is not None + return [data.work_areas[work_area_id].name for work_area_id in data.work_areas] + + +@callback +def _get_current_work_area_name(data: MowerAttributes) -> str: + """Return the name of the current work area.""" + if TYPE_CHECKING: + # Sensor does not get created if values are None + assert data.work_areas is not None + assert data.mower.work_area_id is not None + return data.work_areas[data.mower.work_area_id].name + + @dataclass(frozen=True, kw_only=True) class AutomowerSensorEntityDescription(SensorEntityDescription): """Describes Automower sensor entity.""" exists_fn: Callable[[MowerAttributes], bool] = lambda _: True + option_fn: Callable[[MowerAttributes], list[str] | None] = lambda _: None value_fn: Callable[[MowerAttributes], StateType | datetime] @@ -204,7 +225,7 @@ SENSOR_TYPES: tuple[AutomowerSensorEntityDescription, ...] = ( key="mode", translation_key="mode", device_class=SensorDeviceClass.ENUM, - options=[option.lower() for option in list(MowerModes)], + option_fn=lambda data: [option.lower() for option in list(MowerModes)], value_fn=( lambda data: data.mower.mode.lower() if data.mower.mode != MowerModes.UNKNOWN @@ -302,18 +323,26 @@ SENSOR_TYPES: tuple[AutomowerSensorEntityDescription, ...] = ( key="error", translation_key="error", device_class=SensorDeviceClass.ENUM, + option_fn=lambda data: ERROR_KEY_LIST, value_fn=lambda data: ( "no_error" if data.mower.error_key is None else data.mower.error_key ), - options=ERROR_KEY_LIST, ), AutomowerSensorEntityDescription( key="restricted_reason", translation_key="restricted_reason", device_class=SensorDeviceClass.ENUM, - options=RESTRICTED_REASONS, + option_fn=lambda data: RESTRICTED_REASONS, value_fn=lambda data: data.planner.restricted_reason.lower(), ), + AutomowerSensorEntityDescription( + key="work_area", + translation_key="work_area", + device_class=SensorDeviceClass.ENUM, + exists_fn=lambda data: data.capabilities.work_areas, + option_fn=_get_work_area_names, + value_fn=_get_current_work_area_name, + ), ) @@ -352,3 +381,8 @@ class AutomowerSensorEntity(AutomowerBaseEntity, SensorEntity): def native_value(self) -> StateType | datetime: """Return the state of the sensor.""" return self.entity_description.value_fn(self.mower_attributes) + + @property + def options(self) -> list[str] | None: + """Return the option of the sensor.""" + return self.entity_description.option_fn(self.mower_attributes) diff --git a/homeassistant/components/husqvarna_automower/strings.json b/homeassistant/components/husqvarna_automower/strings.json index bd2ffe6b012..c94a8d0f6d1 100644 --- a/homeassistant/components/husqvarna_automower/strings.json +++ b/homeassistant/components/husqvarna_automower/strings.json @@ -243,6 +243,12 @@ "home": "Home", "demo": "Demo" } + }, + "work_area": { + "name": "Work area", + "state": { + "my_lawn": "My lawn" + } } }, "switch": { diff --git a/tests/components/husqvarna_automower/snapshots/test_sensor.ambr b/tests/components/husqvarna_automower/snapshots/test_sensor.ambr index c43a7d4841a..6cb74ab8814 100644 --- a/tests/components/husqvarna_automower/snapshots/test_sensor.ambr +++ b/tests/components/husqvarna_automower/snapshots/test_sensor.ambr @@ -991,3 +991,61 @@ 'state': '103.000', }) # --- +# name: test_sensor_snapshot[sensor.test_mower_1_work_area-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'Front lawn', + 'Back lawn', + 'my_lawn', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.test_mower_1_work_area', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Work area', + 'platform': 'husqvarna_automower', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'work_area', + 'unique_id': 'c7233734-b219-4287-a173-08e3643f89f0_work_area', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor_snapshot[sensor.test_mower_1_work_area-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Test Mower 1 Work area', + 'options': list([ + 'Front lawn', + 'Back lawn', + 'my_lawn', + ]), + }), + 'context': , + 'entity_id': 'sensor.test_mower_1_work_area', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'Front lawn', + }) +# --- From 2639336ab0caf9a24b8a976bd8738a01f18f24ac Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Fri, 14 Jun 2024 21:38:53 +0200 Subject: [PATCH 0700/1445] Prefer mp4 playback in Reolink (#119630) * If possible use PLAYBACK of mp4 files * bring test_coverage back to 100% * Do not reasign the vod_type multiple times Co-authored-by: Dave T <17680170+davet2001@users.noreply.github.com> * fix indent * add white space * fix tests * Update homeassistant/components/reolink/media_source.py Co-authored-by: Dave T <17680170+davet2001@users.noreply.github.com> --------- Co-authored-by: Dave T <17680170+davet2001@users.noreply.github.com> --- .../components/reolink/media_source.py | 20 +++++++++++--- tests/components/reolink/test_media_source.py | 26 ++++++++++++++++--- 2 files changed, 39 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/reolink/media_source.py b/homeassistant/components/reolink/media_source.py index c22a0fc28e7..c941f5ed055 100644 --- a/homeassistant/components/reolink/media_source.py +++ b/homeassistant/components/reolink/media_source.py @@ -59,19 +59,31 @@ class ReolinkVODMediaSource(MediaSource): data: dict[str, ReolinkData] = self.hass.data[DOMAIN] host = data[config_entry_id].host - vod_type = VodRequestType.RTMP - if host.api.is_nvr: - vod_type = VodRequestType.FLV + def get_vod_type() -> VodRequestType: + if filename.endswith(".mp4"): + return VodRequestType.PLAYBACK + if host.api.is_nvr: + return VodRequestType.FLV + return VodRequestType.RTMP + + vod_type = get_vod_type() mime_type, url = await host.api.get_vod_source( channel, filename, stream_res, vod_type ) if _LOGGER.isEnabledFor(logging.DEBUG): - url_log = f"{url.split('&user=')[0]}&user=xxxxx&password=xxxxx" + url_log = url + if "&user=" in url_log: + url_log = f"{url_log.split('&user=')[0]}&user=xxxxx&password=xxxxx" + elif "&token=" in url_log: + url_log = f"{url_log.split('&token=')[0]}&token=xxxxx" _LOGGER.debug( "Opening VOD stream from %s: %s", host.api.camera_name(channel), url_log ) + if mime_type == "video/mp4": + return PlayMedia(url, mime_type) + stream = create_stream(self.hass, url, {}, DynamicStreamSettings()) stream.add_provider("hls", timeout=3600) stream_url: str = stream.endpoint_url("hls") diff --git a/tests/components/reolink/test_media_source.py b/tests/components/reolink/test_media_source.py index 1eb45945eee..3e3cdd02b46 100644 --- a/tests/components/reolink/test_media_source.py +++ b/tests/components/reolink/test_media_source.py @@ -51,11 +51,14 @@ TEST_DAY2 = 15 TEST_HOUR = 13 TEST_MINUTE = 12 TEST_FILE_NAME = f"{TEST_YEAR}{TEST_MONTH}{TEST_DAY}{TEST_HOUR}{TEST_MINUTE}00" +TEST_FILE_NAME_MP4 = f"{TEST_YEAR}{TEST_MONTH}{TEST_DAY}{TEST_HOUR}{TEST_MINUTE}00.mp4" TEST_STREAM = "main" TEST_CHANNEL = "0" TEST_MIME_TYPE = "application/x-mpegURL" -TEST_URL = "http:test_url" +TEST_MIME_TYPE_MP4 = "video/mp4" +TEST_URL = "http:test_url&user=admin&password=test" +TEST_URL2 = "http:test_url&token=test" @pytest.fixture(autouse=True) @@ -85,18 +88,35 @@ async def test_resolve( """Test resolving Reolink media items.""" assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - - reolink_connect.get_vod_source.return_value = (TEST_MIME_TYPE, TEST_URL) caplog.set_level(logging.DEBUG) file_id = ( f"FILE|{config_entry.entry_id}|{TEST_CHANNEL}|{TEST_STREAM}|{TEST_FILE_NAME}" ) + reolink_connect.get_vod_source.return_value = (TEST_MIME_TYPE, TEST_URL) play_media = await async_resolve_media( hass, f"{URI_SCHEME}{DOMAIN}/{file_id}", None ) + assert play_media.mime_type == TEST_MIME_TYPE + file_id = f"FILE|{config_entry.entry_id}|{TEST_CHANNEL}|{TEST_STREAM}|{TEST_FILE_NAME_MP4}" + reolink_connect.get_vod_source.return_value = (TEST_MIME_TYPE_MP4, TEST_URL2) + + play_media = await async_resolve_media( + hass, f"{URI_SCHEME}{DOMAIN}/{file_id}", None + ) + assert play_media.mime_type == TEST_MIME_TYPE_MP4 + + file_id = ( + f"FILE|{config_entry.entry_id}|{TEST_CHANNEL}|{TEST_STREAM}|{TEST_FILE_NAME}" + ) + reolink_connect.get_vod_source.return_value = (TEST_MIME_TYPE, TEST_URL) + reolink_connect.is_nvr = False + + play_media = await async_resolve_media( + hass, f"{URI_SCHEME}{DOMAIN}/{file_id}", None + ) assert play_media.mime_type == TEST_MIME_TYPE From 8397d6a29f5d5f04babe7c1be74816c56cc40fd3 Mon Sep 17 00:00:00 2001 From: Jay Date: Fri, 14 Jun 2024 14:51:20 -0500 Subject: [PATCH 0701/1445] Envisalink add arming as a state to alarm control panel (#119702) Envisalink Add Arming as a State --- homeassistant/components/envisalink/alarm_control_panel.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/envisalink/alarm_control_panel.py b/homeassistant/components/envisalink/alarm_control_panel.py index b962621edea..d4bbe174f20 100644 --- a/homeassistant/components/envisalink/alarm_control_panel.py +++ b/homeassistant/components/envisalink/alarm_control_panel.py @@ -17,6 +17,7 @@ from homeassistant.const import ( STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_ARMING, STATE_ALARM_DISARMED, STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED, @@ -155,7 +156,9 @@ class EnvisalinkAlarm(EnvisalinkDevice, AlarmControlPanelEntity): state = STATE_ALARM_ARMED_AWAY elif self._info["status"]["armed_stay"]: state = STATE_ALARM_ARMED_HOME - elif self._info["status"]["exit_delay"] or self._info["status"]["entry_delay"]: + elif self._info["status"]["exit_delay"]: + state = STATE_ALARM_ARMING + elif self._info["status"]["entry_delay"]: state = STATE_ALARM_PENDING elif self._info["status"]["alpha"]: state = STATE_ALARM_DISARMED From c75db797d0eca12c755771e0579ce374a3ac7f97 Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Fri, 14 Jun 2024 22:33:38 +0200 Subject: [PATCH 0702/1445] Bump ZHA dependencies (#119713) * Bump bellows to 0.39.1 * Bump zigpy to 0.64.1 --- homeassistant/components/zha/manifest.json | 4 ++-- requirements_all.txt | 4 ++-- requirements_test_all.txt | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 4f72f226fe2..aed0abd3404 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -21,11 +21,11 @@ "universal_silabs_flasher" ], "requirements": [ - "bellows==0.39.0", + "bellows==0.39.1", "pyserial==3.5", "zha-quirks==0.0.116", "zigpy-deconz==0.23.1", - "zigpy==0.64.0", + "zigpy==0.64.1", "zigpy-xbee==0.20.1", "zigpy-zigate==0.12.0", "zigpy-znp==0.12.1", diff --git a/requirements_all.txt b/requirements_all.txt index 1de4e7bbc52..1368f730617 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -553,7 +553,7 @@ beautifulsoup4==4.12.3 # beewi-smartclim==0.0.10 # homeassistant.components.zha -bellows==0.39.0 +bellows==0.39.1 # homeassistant.components.bmw_connected_drive bimmer-connected[china]==0.15.3 @@ -2990,7 +2990,7 @@ zigpy-zigate==0.12.0 zigpy-znp==0.12.1 # homeassistant.components.zha -zigpy==0.64.0 +zigpy==0.64.1 # homeassistant.components.zoneminder zm-py==0.5.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4905155182a..74ebd97eda0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -478,7 +478,7 @@ base36==0.1.1 beautifulsoup4==4.12.3 # homeassistant.components.zha -bellows==0.39.0 +bellows==0.39.1 # homeassistant.components.bmw_connected_drive bimmer-connected[china]==0.15.3 @@ -2337,7 +2337,7 @@ zigpy-zigate==0.12.0 zigpy-znp==0.12.1 # homeassistant.components.zha -zigpy==0.64.0 +zigpy==0.64.1 # homeassistant.components.zwave_js zwave-js-server-python==0.56.0 From f1f82ffbf881987fc20d1c98affb76dec9c6eb07 Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Sat, 15 Jun 2024 04:30:38 +0100 Subject: [PATCH 0703/1445] Update aioazuredevops to 2.1.1 (#119720) * Update aioazuredevops to 2.1.1 * Update tests --- .../components/azure_devops/coordinator.py | 10 +++++----- homeassistant/components/azure_devops/data.py | 8 ++++---- .../components/azure_devops/manifest.json | 2 +- homeassistant/components/azure_devops/sensor.py | 12 ++++++------ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/azure_devops/__init__.py | 17 ++++++++--------- tests/components/azure_devops/conftest.py | 2 +- .../components/azure_devops/test_config_flow.py | 4 ---- 9 files changed, 27 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/azure_devops/coordinator.py b/homeassistant/components/azure_devops/coordinator.py index ba0528de282..d7531c130e9 100644 --- a/homeassistant/components/azure_devops/coordinator.py +++ b/homeassistant/components/azure_devops/coordinator.py @@ -5,9 +5,9 @@ from datetime import timedelta import logging from typing import Final -from aioazuredevops.builds import DevOpsBuild from aioazuredevops.client import DevOpsClient -from aioazuredevops.core import DevOpsProject +from aioazuredevops.models.builds import Build +from aioazuredevops.models.core import Project import aiohttp from homeassistant.config_entries import ConfigEntry @@ -44,7 +44,7 @@ class AzureDevOpsDataUpdateCoordinator(DataUpdateCoordinator[AzureDevOpsData]): client: DevOpsClient organization: str - project: DevOpsProject + project: Project def __init__( self, @@ -88,7 +88,7 @@ class AzureDevOpsDataUpdateCoordinator(DataUpdateCoordinator[AzureDevOpsData]): async def get_project( self, project: str, - ) -> DevOpsProject | None: + ) -> Project | None: """Get the project.""" return await self.client.get_project( self.organization, @@ -96,7 +96,7 @@ class AzureDevOpsDataUpdateCoordinator(DataUpdateCoordinator[AzureDevOpsData]): ) @ado_exception_none_handler - async def _get_builds(self, project_name: str) -> list[DevOpsBuild] | None: + async def _get_builds(self, project_name: str) -> list[Build] | None: """Get the builds.""" return await self.client.get_builds( self.organization, diff --git a/homeassistant/components/azure_devops/data.py b/homeassistant/components/azure_devops/data.py index 6cbd6eb3bc1..6d9e2069b67 100644 --- a/homeassistant/components/azure_devops/data.py +++ b/homeassistant/components/azure_devops/data.py @@ -2,8 +2,8 @@ from dataclasses import dataclass -from aioazuredevops.builds import DevOpsBuild -from aioazuredevops.core import DevOpsProject +from aioazuredevops.models.builds import Build +from aioazuredevops.models.core import Project @dataclass(frozen=True, kw_only=True) @@ -11,5 +11,5 @@ class AzureDevOpsData: """Class describing Azure DevOps data.""" organization: str - project: DevOpsProject - builds: list[DevOpsBuild] + project: Project + builds: list[Build] diff --git a/homeassistant/components/azure_devops/manifest.json b/homeassistant/components/azure_devops/manifest.json index 0d5e5a1c685..48ceee5f9d8 100644 --- a/homeassistant/components/azure_devops/manifest.json +++ b/homeassistant/components/azure_devops/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/azure_devops", "iot_class": "cloud_polling", "loggers": ["aioazuredevops"], - "requirements": ["aioazuredevops==2.0.0"] + "requirements": ["aioazuredevops==2.1.1"] } diff --git a/homeassistant/components/azure_devops/sensor.py b/homeassistant/components/azure_devops/sensor.py index 7b2a1a15adf..7e1e19cc142 100644 --- a/homeassistant/components/azure_devops/sensor.py +++ b/homeassistant/components/azure_devops/sensor.py @@ -8,7 +8,7 @@ from datetime import datetime import logging from typing import Any -from aioazuredevops.builds import DevOpsBuild +from aioazuredevops.models.builds import Build from homeassistant.components.sensor import ( SensorDeviceClass, @@ -32,8 +32,8 @@ _LOGGER = logging.getLogger(__name__) class AzureDevOpsBuildSensorEntityDescription(SensorEntityDescription): """Class describing Azure DevOps base build sensor entities.""" - attr_fn: Callable[[DevOpsBuild], dict[str, Any] | None] = lambda _: None - value_fn: Callable[[DevOpsBuild], datetime | StateType] + attr_fn: Callable[[Build], dict[str, Any] | None] = lambda _: None + value_fn: Callable[[Build], datetime | StateType] BASE_BUILD_SENSOR_DESCRIPTIONS: tuple[AzureDevOpsBuildSensorEntityDescription, ...] = ( @@ -133,7 +133,7 @@ async def async_setup_entry( ) -> None: """Set up Azure DevOps sensor based on a config entry.""" coordinator = hass.data[DOMAIN][entry.entry_id] - initial_builds: list[DevOpsBuild] = coordinator.data.builds + initial_builds: list[Build] = coordinator.data.builds async_add_entities( AzureDevOpsBuildSensor( @@ -162,13 +162,13 @@ class AzureDevOpsBuildSensor(AzureDevOpsEntity, SensorEntity): super().__init__(coordinator) self.entity_description = description self.item_key = item_key - self._attr_unique_id = f"{self.coordinator.data.organization}_{self.build.project.project_id}_{self.build.definition.build_id}_{description.key}" + self._attr_unique_id = f"{self.coordinator.data.organization}_{self.build.project.id}_{self.build.definition.build_id}_{description.key}" self._attr_translation_placeholders = { "definition_name": self.build.definition.name } @property - def build(self) -> DevOpsBuild: + def build(self) -> Build: """Return the build.""" return self.coordinator.data.builds[self.item_key] diff --git a/requirements_all.txt b/requirements_all.txt index 1368f730617..adcc839c94a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -207,7 +207,7 @@ aioasuswrt==1.4.0 aioautomower==2024.6.0 # homeassistant.components.azure_devops -aioazuredevops==2.0.0 +aioazuredevops==2.1.1 # homeassistant.components.baf aiobafi6==0.9.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 74ebd97eda0..6d39899f873 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -186,7 +186,7 @@ aioasuswrt==1.4.0 aioautomower==2024.6.0 # homeassistant.components.azure_devops -aioazuredevops==2.0.0 +aioazuredevops==2.1.1 # homeassistant.components.baf aiobafi6==0.9.0 diff --git a/tests/components/azure_devops/__init__.py b/tests/components/azure_devops/__init__.py index 7c540cd3c6d..d636a6fda6d 100644 --- a/tests/components/azure_devops/__init__.py +++ b/tests/components/azure_devops/__init__.py @@ -2,8 +2,8 @@ from typing import Final -from aioazuredevops.builds import DevOpsBuild, DevOpsBuildDefinition -from aioazuredevops.core import DevOpsProject +from aioazuredevops.models.builds import Build, BuildDefinition +from aioazuredevops.models.core import Project from homeassistant.components.azure_devops.const import CONF_ORG, CONF_PAT, CONF_PROJECT from homeassistant.core import HomeAssistant @@ -28,20 +28,19 @@ FIXTURE_REAUTH_INPUT = { } -DEVOPS_PROJECT = DevOpsProject( - project_id="1234", +DEVOPS_PROJECT = Project( + id="1234", name=PROJECT, description="Test Description", url=f"https://dev.azure.com/{ORGANIZATION}/{PROJECT}", state="wellFormed", revision=1, visibility="private", - last_updated=None, default_team=None, links=None, ) -DEVOPS_BUILD_DEFINITION = DevOpsBuildDefinition( +DEVOPS_BUILD_DEFINITION = BuildDefinition( build_id=9876, name="CI", url=f"https://dev.azure.com/{ORGANIZATION}/{PROJECT}/_apis/build/definitions/1", @@ -51,7 +50,7 @@ DEVOPS_BUILD_DEFINITION = DevOpsBuildDefinition( revision=1, ) -DEVOPS_BUILD = DevOpsBuild( +DEVOPS_BUILD = Build( build_id=5678, build_number="1", status="completed", @@ -68,13 +67,13 @@ DEVOPS_BUILD = DevOpsBuild( links=None, ) -DEVOPS_BUILD_MISSING_DATA = DevOpsBuild( +DEVOPS_BUILD_MISSING_DATA = Build( build_id=6789, definition=DEVOPS_BUILD_DEFINITION, project=DEVOPS_PROJECT, ) -DEVOPS_BUILD_MISSING_PROJECT_DEFINITION = DevOpsBuild( +DEVOPS_BUILD_MISSING_PROJECT_DEFINITION = Build( build_id=9876, ) diff --git a/tests/components/azure_devops/conftest.py b/tests/components/azure_devops/conftest.py index 97e113bbb39..c65adaa4da5 100644 --- a/tests/components/azure_devops/conftest.py +++ b/tests/components/azure_devops/conftest.py @@ -33,7 +33,7 @@ async def mock_devops_client() -> AsyncGenerator[MagicMock]: devops_client.get_project.return_value = DEVOPS_PROJECT devops_client.get_builds.return_value = [DEVOPS_BUILD] devops_client.get_build.return_value = DEVOPS_BUILD - devops_client.get_work_items_ids_all.return_value = None + devops_client.get_work_item_ids.return_value = None devops_client.get_work_items.return_value = None yield devops_client diff --git a/tests/components/azure_devops/test_config_flow.py b/tests/components/azure_devops/test_config_flow.py index acb610a78be..45dc10802b9 100644 --- a/tests/components/azure_devops/test_config_flow.py +++ b/tests/components/azure_devops/test_config_flow.py @@ -2,7 +2,6 @@ from unittest.mock import AsyncMock -from aioazuredevops.core import DevOpsProject import aiohttp from homeassistant import config_entries @@ -218,9 +217,6 @@ async def test_reauth_flow( mock_devops_client.authorize.return_value = True mock_devops_client.authorized = True - mock_devops_client.get_project.return_value = DevOpsProject( - "abcd-abcd-abcd-abcd", FIXTURE_USER_INPUT[CONF_PROJECT] - ) result2 = await hass.config_entries.flow.async_configure( result["flow_id"], From 7a3a57c78eb425fb6e1c7320700eecafa58f2d89 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Sat, 15 Jun 2024 11:24:33 +0200 Subject: [PATCH 0704/1445] Add open state support to matter lock (#119682) --- homeassistant/components/matter/lock.py | 3 +++ tests/components/matter/test_door_lock.py | 8 ++++++++ 2 files changed, 11 insertions(+) diff --git a/homeassistant/components/matter/lock.py b/homeassistant/components/matter/lock.py index e5067efd482..f58ded01013 100644 --- a/homeassistant/components/matter/lock.py +++ b/homeassistant/components/matter/lock.py @@ -168,6 +168,9 @@ class MatterLock(MatterEntity, LockEntity): self._attr_is_jammed = ( door_state is clusters.DoorLock.Enums.DoorStateEnum.kDoorJammed ) + self._attr_is_open = ( + door_state is clusters.DoorLock.Enums.DoorStateEnum.kDoorOpen + ) DISCOVERY_SCHEMAS = [ diff --git a/tests/components/matter/test_door_lock.py b/tests/components/matter/test_door_lock.py index 7f6abeff62b..6e0e0846ad5 100644 --- a/tests/components/matter/test_door_lock.py +++ b/tests/components/matter/test_door_lock.py @@ -9,6 +9,7 @@ import pytest from homeassistant.components.lock import ( STATE_LOCKED, STATE_LOCKING, + STATE_OPEN, STATE_UNLOCKED, STATE_UNLOCKING, LockEntityFeature, @@ -208,3 +209,10 @@ async def test_lock_with_unbolt( command=clusters.DoorLock.Commands.UnlockDoor(), timed_request_timeout_ms=1000, ) + + set_node_attribute(door_lock_with_unbolt, 1, 257, 3, 0) + await trigger_subscription_callback(hass, matter_client) + + state = hass.states.get("lock.mock_door_lock") + assert state + assert state.state == STATE_OPEN From 8c5c7203ea3d7ddf70d95cbd6054b914cf70f143 Mon Sep 17 00:00:00 2001 From: Sid <27780930+autinerd@users.noreply.github.com> Date: Sat, 15 Jun 2024 11:28:10 +0200 Subject: [PATCH 0705/1445] Bump ruff to 0.4.9 (#119721) --- .pre-commit-config.yaml | 2 +- requirements_test_pre_commit.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1d47ba2b3f1..b5f6377ce7b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.4.8 + rev: v0.4.9 hooks: - id: ruff args: diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index 94758f58e32..a7e5c20d86c 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -1,5 +1,5 @@ # Automatically generated from .pre-commit-config.yaml by gen_requirements_all.py, do not edit codespell==2.3.0 -ruff==0.4.8 +ruff==0.4.9 yamllint==1.35.1 From c8e9a3a8f4dfb004b6b453fed231f4859394c4f2 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Sat, 15 Jun 2024 11:31:10 +0200 Subject: [PATCH 0706/1445] Device automation extra fields translation for KNX (#119518) --- .../components/knx/device_trigger.py | 24 ++++++++++++++++--- homeassistant/components/knx/strings.json | 18 +++++++++++++- homeassistant/components/knx/trigger.py | 11 +++------ tests/components/knx/test_device_trigger.py | 20 ++++++++++++---- 4 files changed, 56 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/knx/device_trigger.py b/homeassistant/components/knx/device_trigger.py index 5551aa1d439..ea3cc5faad4 100644 --- a/homeassistant/components/knx/device_trigger.py +++ b/homeassistant/components/knx/device_trigger.py @@ -21,8 +21,12 @@ from .const import DOMAIN from .project import KNXProject from .trigger import ( CONF_KNX_DESTINATION, + CONF_KNX_GROUP_VALUE_READ, + CONF_KNX_GROUP_VALUE_RESPONSE, + CONF_KNX_GROUP_VALUE_WRITE, + CONF_KNX_INCOMING, + CONF_KNX_OUTGOING, PLATFORM_TYPE_TRIGGER_TELEGRAM, - TELEGRAM_TRIGGER_OPTIONS, TELEGRAM_TRIGGER_SCHEMA, TRIGGER_SCHEMA as TRIGGER_TRIGGER_SCHEMA, ) @@ -79,7 +83,21 @@ async def async_get_trigger_capabilities( options=options, ), ), - **TELEGRAM_TRIGGER_OPTIONS, + vol.Optional( + CONF_KNX_GROUP_VALUE_WRITE, default=True + ): selector.BooleanSelector(), + vol.Optional( + CONF_KNX_GROUP_VALUE_RESPONSE, default=True + ): selector.BooleanSelector(), + vol.Optional( + CONF_KNX_GROUP_VALUE_READ, default=True + ): selector.BooleanSelector(), + vol.Optional( + CONF_KNX_INCOMING, default=True + ): selector.BooleanSelector(), + vol.Optional( + CONF_KNX_OUTGOING, default=True + ): selector.BooleanSelector(), } ) } @@ -98,7 +116,7 @@ async def async_attach_trigger( } | {CONF_PLATFORM: PLATFORM_TYPE_TRIGGER_TELEGRAM} try: - TRIGGER_TRIGGER_SCHEMA(trigger_config) + trigger_config = TRIGGER_TRIGGER_SCHEMA(trigger_config) except vol.Invalid as err: raise InvalidDeviceAutomationConfig(f"{err}") from err diff --git a/homeassistant/components/knx/strings.json b/homeassistant/components/knx/strings.json index 39b96dddf8f..d6e1e2f49f0 100644 --- a/homeassistant/components/knx/strings.json +++ b/homeassistant/components/knx/strings.json @@ -296,7 +296,23 @@ }, "device_automation": { "trigger_type": { - "telegram": "Telegram sent or received" + "telegram": "Telegram" + }, + "extra_fields": { + "destination": "Group addresses", + "group_value_write": "GroupValueWrite", + "group_value_read": "GroupValueRead", + "group_value_response": "GroupValueResponse", + "incoming": "Incoming", + "outgoing": "Outgoing" + }, + "extra_fields_descriptions": { + "destination": "The trigger will listen to telegrams sent or received on these group addresses. If no address is selected, the trigger will fire for every group address.", + "group_value_write": "Listen on GroupValueWrite telegrams.", + "group_value_read": "Listen on GroupValueRead telegrams.", + "group_value_response": "Listen on GroupValueResponse telegrams.", + "incoming": "Listen on incoming telegrams.", + "outgoing": "Listen on outgoing telegrams." } }, "services": { diff --git a/homeassistant/components/knx/trigger.py b/homeassistant/components/knx/trigger.py index fff844f35b0..1df1ffd6c3b 100644 --- a/homeassistant/components/knx/trigger.py +++ b/homeassistant/components/knx/trigger.py @@ -31,20 +31,15 @@ CONF_KNX_GROUP_VALUE_RESPONSE: Final = "group_value_response" CONF_KNX_INCOMING: Final = "incoming" CONF_KNX_OUTGOING: Final = "outgoing" -TELEGRAM_TRIGGER_OPTIONS: Final = { + +TELEGRAM_TRIGGER_SCHEMA: Final = { + vol.Optional(CONF_KNX_DESTINATION): vol.All(cv.ensure_list, [ga_validator]), vol.Optional(CONF_KNX_GROUP_VALUE_WRITE, default=True): cv.boolean, vol.Optional(CONF_KNX_GROUP_VALUE_RESPONSE, default=True): cv.boolean, vol.Optional(CONF_KNX_GROUP_VALUE_READ, default=True): cv.boolean, vol.Optional(CONF_KNX_INCOMING, default=True): cv.boolean, vol.Optional(CONF_KNX_OUTGOING, default=True): cv.boolean, } -TELEGRAM_TRIGGER_SCHEMA: Final = { - vol.Optional(CONF_KNX_DESTINATION): vol.All( - cv.ensure_list, - [ga_validator], - ), - **TELEGRAM_TRIGGER_OPTIONS, -} # TRIGGER_SCHEMA is exclusive to triggers, the above are used in device triggers too TRIGGER_SCHEMA = cv.TRIGGER_BASE_SCHEMA.extend( { diff --git a/tests/components/knx/test_device_trigger.py b/tests/components/knx/test_device_trigger.py index 2fd15150503..136dddefaab 100644 --- a/tests/components/knx/test_device_trigger.py +++ b/tests/components/knx/test_device_trigger.py @@ -319,31 +319,41 @@ async def test_get_trigger_capabilities( "name": "group_value_write", "optional": True, "default": True, - "type": "boolean", + "selector": { + "boolean": {}, + }, }, { "name": "group_value_response", "optional": True, "default": True, - "type": "boolean", + "selector": { + "boolean": {}, + }, }, { "name": "group_value_read", "optional": True, "default": True, - "type": "boolean", + "selector": { + "boolean": {}, + }, }, { "name": "incoming", "optional": True, "default": True, - "type": "boolean", + "selector": { + "boolean": {}, + }, }, { "name": "outgoing", "optional": True, "default": True, - "type": "boolean", + "selector": { + "boolean": {}, + }, }, ] From a515562a11e446ad7b33ccbebf0334ab34423338 Mon Sep 17 00:00:00 2001 From: Josef Zweck <24647999+zweckj@users.noreply.github.com> Date: Sat, 15 Jun 2024 11:33:29 +0200 Subject: [PATCH 0707/1445] Bring back auto on off switches to lamarzocco (#119421) * add auto on off switches --- .../components/lamarzocco/strings.json | 3 + homeassistant/components/lamarzocco/switch.py | 55 ++++++++++- .../lamarzocco/snapshots/test_switch.ambr | 92 +++++++++++++++++++ tests/components/lamarzocco/test_switch.py | 54 ++++++++++- 4 files changed, 201 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/lamarzocco/strings.json b/homeassistant/components/lamarzocco/strings.json index 744f4a0d63f..f6b979a30ae 100644 --- a/homeassistant/components/lamarzocco/strings.json +++ b/homeassistant/components/lamarzocco/strings.json @@ -139,6 +139,9 @@ } }, "switch": { + "auto_on_off": { + "name": "Auto on/off ({id})" + }, "steam_boiler": { "name": "Steam boiler" } diff --git a/homeassistant/components/lamarzocco/switch.py b/homeassistant/components/lamarzocco/switch.py index 1661917fcbc..e21cd2f3d94 100644 --- a/homeassistant/components/lamarzocco/switch.py +++ b/homeassistant/components/lamarzocco/switch.py @@ -13,7 +13,8 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import LaMarzoccoConfigEntry -from .entity import LaMarzoccoEntity, LaMarzoccoEntityDescription +from .coordinator import LaMarzoccoUpdateCoordinator +from .entity import LaMarzoccoBaseEntity, LaMarzoccoEntity, LaMarzoccoEntityDescription @dataclass(frozen=True, kw_only=True) @@ -52,12 +53,21 @@ async def async_setup_entry( """Set up switch entities and services.""" coordinator = entry.runtime_data - async_add_entities( + + entities: list[SwitchEntity] = [] + entities.extend( LaMarzoccoSwitchEntity(coordinator, description) for description in ENTITIES if description.supported_fn(coordinator) ) + entities.extend( + LaMarzoccoAutoOnOffSwitchEntity(coordinator, wake_up_sleep_entry_id) + for wake_up_sleep_entry_id in coordinator.device.config.wake_up_sleep_entries + ) + + async_add_entities(entities) + class LaMarzoccoSwitchEntity(LaMarzoccoEntity, SwitchEntity): """Switches representing espresso machine power, prebrew, and auto on/off.""" @@ -78,3 +88,44 @@ class LaMarzoccoSwitchEntity(LaMarzoccoEntity, SwitchEntity): def is_on(self) -> bool: """Return true if device is on.""" return self.entity_description.is_on_fn(self.coordinator.device.config) + + +class LaMarzoccoAutoOnOffSwitchEntity(LaMarzoccoBaseEntity, SwitchEntity): + """Switch representing espresso machine auto on/off.""" + + coordinator: LaMarzoccoUpdateCoordinator + _attr_translation_key = "auto_on_off" + + def __init__( + self, + coordinator: LaMarzoccoUpdateCoordinator, + identifier: str, + ) -> None: + """Initialize the switch.""" + super().__init__(coordinator, f"auto_on_off_{identifier}") + self._identifier = identifier + self._attr_translation_placeholders = {"id": identifier} + + async def _async_enable(self, state: bool) -> None: + """Enable or disable the auto on/off schedule.""" + wake_up_sleep_entry = self.coordinator.device.config.wake_up_sleep_entries[ + self._identifier + ] + wake_up_sleep_entry.enabled = state + await self.coordinator.device.set_wake_up_sleep(wake_up_sleep_entry) + self.async_write_ha_state() + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn switch on.""" + await self._async_enable(True) + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn switch off.""" + await self._async_enable(False) + + @property + def is_on(self) -> bool: + """Return true if switch is on.""" + return self.coordinator.device.config.wake_up_sleep_entries[ + self._identifier + ].enabled diff --git a/tests/components/lamarzocco/snapshots/test_switch.ambr b/tests/components/lamarzocco/snapshots/test_switch.ambr index 00205f48c21..09864be1d5c 100644 --- a/tests/components/lamarzocco/snapshots/test_switch.ambr +++ b/tests/components/lamarzocco/snapshots/test_switch.ambr @@ -1,4 +1,96 @@ # serializer version: 1 +# name: test_auto_on_off_switches[entry.auto_on_off_Os2OswX] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.gs01234_auto_on_off_os2oswx', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Auto on/off (Os2OswX)', + 'platform': 'lamarzocco', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'auto_on_off', + 'unique_id': 'GS01234_auto_on_off_Os2OswX', + 'unit_of_measurement': None, + }) +# --- +# name: test_auto_on_off_switches[entry.auto_on_off_aXFz5bJ] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.gs01234_auto_on_off_axfz5bj', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Auto on/off (aXFz5bJ)', + 'platform': 'lamarzocco', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'auto_on_off', + 'unique_id': 'GS01234_auto_on_off_aXFz5bJ', + 'unit_of_measurement': None, + }) +# --- +# name: test_auto_on_off_switches[state.auto_on_off_Os2OswX] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'GS01234 Auto on/off (Os2OswX)', + }), + 'context': , + 'entity_id': 'switch.gs01234_auto_on_off_os2oswx', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_auto_on_off_switches[state.auto_on_off_aXFz5bJ] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'GS01234 Auto on/off (aXFz5bJ)', + }), + 'context': , + 'entity_id': 'switch.gs01234_auto_on_off_axfz5bj', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- # name: test_device DeviceRegistryEntrySnapshot({ 'area_id': None, diff --git a/tests/components/lamarzocco/test_switch.py b/tests/components/lamarzocco/test_switch.py index 19950a0c21e..4f60b264a1d 100644 --- a/tests/components/lamarzocco/test_switch.py +++ b/tests/components/lamarzocco/test_switch.py @@ -14,7 +14,7 @@ from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er -from . import async_init_integration +from . import WAKE_UP_SLEEP_ENTRY_IDS, async_init_integration from tests.common import MockConfigEntry @@ -106,3 +106,55 @@ async def test_device( device = device_registry.async_get(entry.device_id) assert device assert device == snapshot + + +async def test_auto_on_off_switches( + hass: HomeAssistant, + mock_lamarzocco: MagicMock, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, +) -> None: + """Test the auto on off/switches.""" + + await async_init_integration(hass, mock_config_entry) + + serial_number = mock_lamarzocco.serial_number + + for wake_up_sleep_entry_id in WAKE_UP_SLEEP_ENTRY_IDS: + state = hass.states.get( + f"switch.{serial_number}_auto_on_off_{wake_up_sleep_entry_id}" + ) + assert state + assert state == snapshot(name=f"state.auto_on_off_{wake_up_sleep_entry_id}") + + entry = entity_registry.async_get(state.entity_id) + assert entry + assert entry == snapshot(name=f"entry.auto_on_off_{wake_up_sleep_entry_id}") + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + { + ATTR_ENTITY_ID: f"switch.{serial_number}_auto_on_off_{wake_up_sleep_entry_id}", + }, + blocking=True, + ) + + wake_up_sleep_entry = mock_lamarzocco.config.wake_up_sleep_entries[ + wake_up_sleep_entry_id + ] + wake_up_sleep_entry.enabled = False + + mock_lamarzocco.set_wake_up_sleep.assert_called_with(wake_up_sleep_entry) + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: f"switch.{serial_number}_auto_on_off_{wake_up_sleep_entry_id}", + }, + blocking=True, + ) + wake_up_sleep_entry.enabled = True + mock_lamarzocco.set_wake_up_sleep.assert_called_with(wake_up_sleep_entry) From dac661831e837cb8e22acdfb8ef2347938849254 Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Sat, 15 Jun 2024 20:10:02 +1000 Subject: [PATCH 0708/1445] Add unique IDs to config entries for Teslemetry (#115616) * Add basic UID * Add Unique IDs * Add debug message * Readd debug message * Minor bump config version * Ruff * Rework migration * Fix migration return * Review feedback * Add test for v2 --- .../components/teslemetry/__init__.py | 25 +++- .../components/teslemetry/config_flow.py | 5 +- .../components/teslemetry/strings.json | 3 + tests/components/teslemetry/__init__.py | 3 +- tests/components/teslemetry/const.py | 2 + .../components/teslemetry/test_config_flow.py | 116 ++++++++++++++---- 6 files changed, 128 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/teslemetry/__init__.py b/homeassistant/components/teslemetry/__init__.py index 387ebd1039e..21ea2915884 100644 --- a/homeassistant/components/teslemetry/__init__.py +++ b/homeassistant/components/teslemetry/__init__.py @@ -18,7 +18,7 @@ from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.device_registry import DeviceInfo -from .const import DOMAIN, MODELS +from .const import DOMAIN, LOGGER, MODELS from .coordinator import ( TeslemetryEnergySiteInfoCoordinator, TeslemetryEnergySiteLiveCoordinator, @@ -153,3 +153,26 @@ async def async_setup_entry(hass: HomeAssistant, entry: TeslemetryConfigEntry) - async def async_unload_entry(hass: HomeAssistant, entry: TeslemetryConfigEntry) -> bool: """Unload Teslemetry Config.""" return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + + +async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: + """Migrate config entry.""" + if config_entry.version > 1: + return False + + if config_entry.version == 1 and config_entry.minor_version < 2: + # Add unique_id to existing entry + teslemetry = Teslemetry( + session=async_get_clientsession(hass), + access_token=config_entry.data[CONF_ACCESS_TOKEN], + ) + try: + metadata = await teslemetry.metadata() + except TeslaFleetError as e: + LOGGER.error(e.message) + return False + + hass.config_entries.async_update_entry( + config_entry, unique_id=metadata["uid"], version=1, minor_version=2 + ) + return True diff --git a/homeassistant/components/teslemetry/config_flow.py b/homeassistant/components/teslemetry/config_flow.py index 5fb6ce56aed..73921986f44 100644 --- a/homeassistant/components/teslemetry/config_flow.py +++ b/homeassistant/components/teslemetry/config_flow.py @@ -31,6 +31,7 @@ class TeslemetryConfigFlow(ConfigFlow, domain=DOMAIN): """Config Teslemetry API connection.""" VERSION = 1 + MINOR_VERSION = 2 _entry: ConfigEntry | None = None async def async_auth(self, user_input: Mapping[str, Any]) -> dict[str, str]: @@ -40,7 +41,7 @@ class TeslemetryConfigFlow(ConfigFlow, domain=DOMAIN): access_token=user_input[CONF_ACCESS_TOKEN], ) try: - await teslemetry.test() + metadata = await teslemetry.metadata() except InvalidToken: return {CONF_ACCESS_TOKEN: "invalid_access_token"} except SubscriptionRequired: @@ -50,6 +51,8 @@ class TeslemetryConfigFlow(ConfigFlow, domain=DOMAIN): except TeslaFleetError as e: LOGGER.error(e) return {"base": "unknown"} + + await self.async_set_unique_id(metadata["uid"]) return {} async def async_step_user( diff --git a/homeassistant/components/teslemetry/strings.json b/homeassistant/components/teslemetry/strings.json index d3740db9760..fe45b4ee9e3 100644 --- a/homeassistant/components/teslemetry/strings.json +++ b/homeassistant/components/teslemetry/strings.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Account is already configured" + }, "error": { "invalid_access_token": "[%key:common::config_flow::error::invalid_access_token%]", "subscription_required": "Subscription required, please visit {short_url}", diff --git a/tests/components/teslemetry/__init__.py b/tests/components/teslemetry/__init__.py index daa2c070091..c4fbdaf3fbd 100644 --- a/tests/components/teslemetry/__init__.py +++ b/tests/components/teslemetry/__init__.py @@ -18,8 +18,7 @@ async def setup_platform(hass: HomeAssistant, platforms: list[Platform] | None = """Set up the Teslemetry platform.""" mock_entry = MockConfigEntry( - domain=DOMAIN, - data=CONFIG, + domain=DOMAIN, data=CONFIG, minor_version=2, unique_id="abc-123" ) mock_entry.add_to_hass(hass) diff --git a/tests/components/teslemetry/const.py b/tests/components/teslemetry/const.py index ffb349e4b7e..6a3a657a1b1 100644 --- a/tests/components/teslemetry/const.py +++ b/tests/components/teslemetry/const.py @@ -31,6 +31,7 @@ COMMAND_ERRORS = (COMMAND_REASON, COMMAND_NOREASON, COMMAND_ERROR, COMMAND_NOERR RESPONSE_OK = {"response": {}, "error": None} METADATA = { + "uid": "abc-123", "region": "NA", "scopes": [ "openid", @@ -44,6 +45,7 @@ METADATA = { ], } METADATA_NOSCOPE = { + "uid": "abc-123", "region": "NA", "scopes": ["openid", "offline_access", "vehicle_device_data"], } diff --git a/tests/components/teslemetry/test_config_flow.py b/tests/components/teslemetry/test_config_flow.py index 2f12b202712..fa35142dc07 100644 --- a/tests/components/teslemetry/test_config_flow.py +++ b/tests/components/teslemetry/test_config_flow.py @@ -12,26 +12,18 @@ from tesla_fleet_api.exceptions import ( from homeassistant import config_entries from homeassistant.components.teslemetry.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_ACCESS_TOKEN from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType -from .const import CONFIG +from .const import CONFIG, METADATA from tests.common import MockConfigEntry BAD_CONFIG = {CONF_ACCESS_TOKEN: "bad_access_token"} -@pytest.fixture(autouse=True) -def mock_test(): - """Mock Teslemetry api class.""" - with patch( - "homeassistant.components.teslemetry.Teslemetry.test", return_value=True - ) as mock_test: - yield mock_test - - async def test_form( hass: HomeAssistant, ) -> None: @@ -67,14 +59,16 @@ async def test_form( (TeslaFleetError, {"base": "unknown"}), ], ) -async def test_form_errors(hass: HomeAssistant, side_effect, error, mock_test) -> None: +async def test_form_errors( + hass: HomeAssistant, side_effect, error, mock_metadata +) -> None: """Test errors are handled.""" result1 = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - mock_test.side_effect = side_effect + mock_metadata.side_effect = side_effect result2 = await hass.config_entries.flow.async_configure( result1["flow_id"], CONFIG, @@ -84,7 +78,7 @@ async def test_form_errors(hass: HomeAssistant, side_effect, error, mock_test) - assert result2["errors"] == error # Complete the flow - mock_test.side_effect = None + mock_metadata.side_effect = None result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], CONFIG, @@ -92,12 +86,11 @@ async def test_form_errors(hass: HomeAssistant, side_effect, error, mock_test) - assert result3["type"] is FlowResultType.CREATE_ENTRY -async def test_reauth(hass: HomeAssistant, mock_test) -> None: +async def test_reauth(hass: HomeAssistant, mock_metadata) -> None: """Test reauth flow.""" mock_entry = MockConfigEntry( - domain=DOMAIN, - data=BAD_CONFIG, + domain=DOMAIN, data=BAD_CONFIG, minor_version=2, unique_id="abc-123" ) mock_entry.add_to_hass(hass) @@ -124,7 +117,7 @@ async def test_reauth(hass: HomeAssistant, mock_test) -> None: ) await hass.async_block_till_done() assert len(mock_setup_entry.mock_calls) == 1 - assert len(mock_test.mock_calls) == 1 + assert len(mock_metadata.mock_calls) == 1 assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" @@ -141,14 +134,13 @@ async def test_reauth(hass: HomeAssistant, mock_test) -> None: ], ) async def test_reauth_errors( - hass: HomeAssistant, mock_test, side_effect, error + hass: HomeAssistant, mock_metadata, side_effect, error ) -> None: """Test reauth flows that fail.""" # Start the reauth mock_entry = MockConfigEntry( - domain=DOMAIN, - data=BAD_CONFIG, + domain=DOMAIN, data=BAD_CONFIG, minor_version=2, unique_id="abc-123" ) mock_entry.add_to_hass(hass) @@ -162,7 +154,7 @@ async def test_reauth_errors( data=BAD_CONFIG, ) - mock_test.side_effect = side_effect + mock_metadata.side_effect = side_effect result2 = await hass.config_entries.flow.async_configure( result["flow_id"], BAD_CONFIG, @@ -173,7 +165,7 @@ async def test_reauth_errors( assert result2["errors"] == error # Complete the flow - mock_test.side_effect = None + mock_metadata.side_effect = None result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], CONFIG, @@ -182,3 +174,83 @@ async def test_reauth_errors( assert result3["type"] is FlowResultType.ABORT assert result3["reason"] == "reauth_successful" assert mock_entry.data == CONFIG + + +async def test_unique_id_abort( + hass: HomeAssistant, +) -> None: + """Test duplicate unique ID in config.""" + + result1 = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER}, data=CONFIG + ) + assert result1["type"] is FlowResultType.CREATE_ENTRY + + # Setup a duplicate + result2 = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER}, data=CONFIG + ) + assert result2["type"] is FlowResultType.ABORT + + +async def test_migrate_from_1_1(hass: HomeAssistant, mock_metadata) -> None: + """Test config migration.""" + + mock_entry = MockConfigEntry( + domain=DOMAIN, + version=1, + minor_version=1, + unique_id=None, + data=CONFIG, + ) + + mock_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + + entry = hass.config_entries.async_get_entry(mock_entry.entry_id) + assert entry.version == 1 + assert entry.minor_version == 2 + assert entry.unique_id == METADATA["uid"] + + +async def test_migrate_error_from_1_1(hass: HomeAssistant, mock_metadata) -> None: + """Test config migration handles errors.""" + + mock_metadata.side_effect = TeslaFleetError + + mock_entry = MockConfigEntry( + domain=DOMAIN, + version=1, + minor_version=1, + unique_id=None, + data=CONFIG, + ) + + mock_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + + entry = hass.config_entries.async_get_entry(mock_entry.entry_id) + assert entry.state is ConfigEntryState.MIGRATION_ERROR + + +async def test_migrate_error_from_future(hass: HomeAssistant, mock_metadata) -> None: + """Test a future version isn't migrated.""" + + mock_metadata.side_effect = TeslaFleetError + + mock_entry = MockConfigEntry( + domain=DOMAIN, + version=2, + minor_version=1, + unique_id="abc-123", + data=CONFIG, + ) + + mock_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + + entry = hass.config_entries.async_get_entry(mock_entry.entry_id) + assert entry.state is ConfigEntryState.MIGRATION_ERROR From 8cf1890772329799c19720410d045a0a18f6cc8a Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Sat, 15 Jun 2024 11:50:19 +0100 Subject: [PATCH 0709/1445] Moves diagnostic information from attributes to diagnostic in Utility Meter (#118637) * move diag information from attributes to diagnostic * remove constant attributes --------- Co-authored-by: G Johansson --- .../components/utility_meter/diagnostics.py | 3 +++ homeassistant/components/utility_meter/sensor.py | 6 ------ .../utility_meter/snapshots/test_diagnostics.ambr | 12 ++++++------ tests/components/utility_meter/test_config_flow.py | 2 -- 4 files changed, 9 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/utility_meter/diagnostics.py b/homeassistant/components/utility_meter/diagnostics.py index 57850beb0fb..1ff723f7a89 100644 --- a/homeassistant/components/utility_meter/diagnostics.py +++ b/homeassistant/components/utility_meter/diagnostics.py @@ -26,6 +26,9 @@ async def async_get_config_entry_diagnostics( "entity_id": sensor.entity_id, "extra_attributes": sensor.extra_state_attributes, "last_sensor_data": restored_last_extra_data, + "period": sensor._period, # noqa: SLF001 + "cron": sensor._cron_pattern, # noqa: SLF001 + "source": sensor._sensor_source_id, # noqa: SLF001 } ) diff --git a/homeassistant/components/utility_meter/sensor.py b/homeassistant/components/utility_meter/sensor.py index 96cfccfd211..4a68248f067 100644 --- a/homeassistant/components/utility_meter/sensor.py +++ b/homeassistant/components/utility_meter/sensor.py @@ -57,7 +57,6 @@ import homeassistant.util.dt as dt_util from homeassistant.util.enum import try_parse_enum from .const import ( - ATTR_CRON_PATTERN, ATTR_NEXT_RESET, ATTR_VALUE, BIMONTHLY, @@ -740,15 +739,10 @@ class UtilityMeterSensor(RestoreSensor): def extra_state_attributes(self): """Return the state attributes of the sensor.""" state_attr = { - ATTR_SOURCE_ID: self._sensor_source_id, ATTR_STATUS: PAUSED if self._collecting is None else COLLECTING, ATTR_LAST_PERIOD: str(self._last_period), ATTR_LAST_VALID_STATE: str(self._last_valid_state), } - if self._period is not None: - state_attr[ATTR_PERIOD] = self._period - if self._cron_pattern is not None: - state_attr[ATTR_CRON_PATTERN] = self._cron_pattern if self._tariff is not None: state_attr[ATTR_TARIFF] = self._tariff # last_reset in utility meter was used before last_reset was added for long term diff --git a/tests/components/utility_meter/snapshots/test_diagnostics.ambr b/tests/components/utility_meter/snapshots/test_diagnostics.ambr index 9858973d912..28841854766 100644 --- a/tests/components/utility_meter/snapshots/test_diagnostics.ambr +++ b/tests/components/utility_meter/snapshots/test_diagnostics.ambr @@ -29,36 +29,36 @@ }), 'tariff_sensors': list([ dict({ + 'cron': '0 0 1 * *', 'entity_id': 'sensor.energy_bill_tariff0', 'extra_attributes': dict({ - 'cron pattern': '0 0 1 * *', 'last_period': '0', 'last_reset': '2024-04-05T00:00:00+00:00', 'last_valid_state': 'None', - 'meter_period': 'monthly', 'next_reset': '2024-05-01T00:00:00-07:00', - 'source': 'sensor.input1', 'status': 'collecting', 'tariff': 'tariff0', }), 'last_sensor_data': None, 'name': 'Energy Bill tariff0', + 'period': 'monthly', + 'source': 'sensor.input1', }), dict({ + 'cron': '0 0 1 * *', 'entity_id': 'sensor.energy_bill_tariff1', 'extra_attributes': dict({ - 'cron pattern': '0 0 1 * *', 'last_period': '0', 'last_reset': '2024-04-05T00:00:00+00:00', 'last_valid_state': 'None', - 'meter_period': 'monthly', 'next_reset': '2024-05-01T00:00:00-07:00', - 'source': 'sensor.input1', 'status': 'paused', 'tariff': 'tariff1', }), 'last_sensor_data': None, 'name': 'Energy Bill tariff1', + 'period': 'monthly', + 'source': 'sensor.input1', }), ]), }) diff --git a/tests/components/utility_meter/test_config_flow.py b/tests/components/utility_meter/test_config_flow.py index eccc1d3e12d..560566d7c49 100644 --- a/tests/components/utility_meter/test_config_flow.py +++ b/tests/components/utility_meter/test_config_flow.py @@ -332,8 +332,6 @@ async def test_options(hass: HomeAssistant) -> None: # Check config entry is reloaded with new options await hass.async_block_till_done() - state = hass.states.get("sensor.electricity_meter") - assert state.attributes["source"] == input_sensor2_entity_id async def test_change_device_source( From 7e61ec96e766e08154c10c1baa423d99bc2a81a1 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sat, 15 Jun 2024 13:22:01 +0200 Subject: [PATCH 0710/1445] Make the radius of the home zone configurable (#119385) --- homeassistant/components/config/core.py | 1 + homeassistant/components/zone/__init__.py | 2 +- homeassistant/config.py | 4 ++++ homeassistant/core.py | 21 ++++++++++++++++++++- tests/components/config/test_core.py | 3 +++ tests/test_config.py | 16 +++++++++++++--- tests/test_core.py | 2 ++ 7 files changed, 44 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/config/core.py b/homeassistant/components/config/core.py index 3cfb7c03a40..6f788b1c9f2 100644 --- a/homeassistant/components/config/core.py +++ b/homeassistant/components/config/core.py @@ -61,6 +61,7 @@ class CheckConfigView(HomeAssistantView): vol.Optional("latitude"): cv.latitude, vol.Optional("location_name"): str, vol.Optional("longitude"): cv.longitude, + vol.Optional("radius"): cv.positive_int, vol.Optional("time_zone"): cv.time_zone, vol.Optional("update_units"): bool, vol.Optional("unit_system"): unit_system.validate_unit_system, diff --git a/homeassistant/components/zone/__init__.py b/homeassistant/components/zone/__init__.py index 16784a9e0c3..0fef9961679 100644 --- a/homeassistant/components/zone/__init__.py +++ b/homeassistant/components/zone/__init__.py @@ -302,7 +302,7 @@ def _home_conf(hass: HomeAssistant) -> dict: CONF_NAME: hass.config.location_name, CONF_LATITUDE: hass.config.latitude, CONF_LONGITUDE: hass.config.longitude, - CONF_RADIUS: DEFAULT_RADIUS, + CONF_RADIUS: hass.config.radius, CONF_ICON: ICON_HOME, CONF_PASSIVE: False, } diff --git a/homeassistant/config.py b/homeassistant/config.py index bb3a8fb1cd4..751eaca7376 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -52,6 +52,7 @@ from .const import ( CONF_NAME, CONF_PACKAGES, CONF_PLATFORM, + CONF_RADIUS, CONF_TEMPERATURE_UNIT, CONF_TIME_ZONE, CONF_TYPE, @@ -342,6 +343,7 @@ CORE_CONFIG_SCHEMA = vol.All( CONF_LATITUDE: cv.latitude, CONF_LONGITUDE: cv.longitude, CONF_ELEVATION: vol.Coerce(int), + CONF_RADIUS: cv.positive_int, vol.Remove(CONF_TEMPERATURE_UNIT): cv.temperature_unit, CONF_UNIT_SYSTEM: validate_unit_system, CONF_TIME_ZONE: cv.time_zone, @@ -882,6 +884,7 @@ async def async_process_ha_core_config(hass: HomeAssistant, config: dict) -> Non CONF_CURRENCY, CONF_COUNTRY, CONF_LANGUAGE, + CONF_RADIUS, ) ): hac.config_source = ConfigSource.YAML @@ -898,6 +901,7 @@ async def async_process_ha_core_config(hass: HomeAssistant, config: dict) -> Non (CONF_CURRENCY, "currency"), (CONF_COUNTRY, "country"), (CONF_LANGUAGE, "language"), + (CONF_RADIUS, "radius"), ): if key in config: setattr(hac, attr, config[key]) diff --git a/homeassistant/core.py b/homeassistant/core.py index 108248c9e83..ac287fb2d5f 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -138,7 +138,7 @@ type CALLBACK_TYPE = Callable[[], None] CORE_STORAGE_KEY = "core.config" CORE_STORAGE_VERSION = 1 -CORE_STORAGE_MINOR_VERSION = 3 +CORE_STORAGE_MINOR_VERSION = 4 DOMAIN = "homeassistant" @@ -2835,6 +2835,9 @@ class Config: def __init__(self, hass: HomeAssistant, config_dir: str) -> None: """Initialize a new config object.""" + # pylint: disable-next=import-outside-toplevel + from .components.zone import DEFAULT_RADIUS + self.hass = hass self.latitude: float = 0 @@ -2843,6 +2846,9 @@ class Config: self.elevation: int = 0 """Elevation (always in meters regardless of the unit system).""" + self.radius: int = DEFAULT_RADIUS + """Radius of the Home Zone (always in meters regardless of the unit system).""" + self.debug: bool = False self.location_name: str = "Home" self.time_zone: str = "UTC" @@ -2991,6 +2997,7 @@ class Config: "language": self.language, "safe_mode": self.safe_mode, "debug": self.debug, + "radius": self.radius, } async def async_set_time_zone(self, time_zone_str: str) -> None: @@ -3039,6 +3046,7 @@ class Config: currency: str | None = None, country: str | UndefinedType | None = UNDEFINED, language: str | None = None, + radius: int | None = None, ) -> None: """Update the configuration from a dictionary.""" self.config_source = source @@ -3067,6 +3075,8 @@ class Config: self.country = country if language is not None: self.language = language + if radius is not None: + self.radius = radius async def async_update(self, **kwargs: Any) -> None: """Update the configuration from a dictionary.""" @@ -3115,6 +3125,7 @@ class Config: currency=data.get("currency"), country=data.get("country"), language=data.get("language"), + radius=data["radius"], ) async def _async_store(self) -> None: @@ -3133,6 +3144,7 @@ class Config: "currency": self.currency, "country": self.country, "language": self.language, + "radius": self.radius, } await self._store.async_save(data) @@ -3162,6 +3174,10 @@ class Config: old_data: dict[str, Any], ) -> dict[str, Any]: """Migrate to the new version.""" + + # pylint: disable-next=import-outside-toplevel + from .components.zone import DEFAULT_RADIUS + data = old_data if old_major_version == 1 and old_minor_version < 2: # In 1.2, we remove support for "imperial", replaced by "us_customary" @@ -3198,6 +3214,9 @@ class Config: # pylint: disable-next=broad-except except Exception: _LOGGER.exception("Unexpected error during core config migration") + if old_major_version == 1 and old_minor_version < 4: + # In 1.4, we add the key "radius", initialize it with the default. + data.setdefault("radius", DEFAULT_RADIUS) if old_major_version > 1: raise NotImplementedError diff --git a/tests/components/config/test_core.py b/tests/components/config/test_core.py index 3ee3e3334ea..7d02063b2b9 100644 --- a/tests/components/config/test_core.py +++ b/tests/components/config/test_core.py @@ -120,6 +120,7 @@ async def test_websocket_core_update(hass: HomeAssistant, client) -> None: assert hass.config.currency == "EUR" assert hass.config.country != "SE" assert hass.config.language != "sv" + assert hass.config.radius != 150 with ( patch("homeassistant.util.dt.set_default_time_zone") as mock_set_tz, @@ -142,6 +143,7 @@ async def test_websocket_core_update(hass: HomeAssistant, client) -> None: "currency": "USD", "country": "SE", "language": "sv", + "radius": 150, } ) @@ -162,6 +164,7 @@ async def test_websocket_core_update(hass: HomeAssistant, client) -> None: assert hass.config.currency == "USD" assert hass.config.country == "SE" assert hass.config.language == "sv" + assert hass.config.radius == 150 assert len(mock_set_tz.mock_calls) == 1 assert mock_set_tz.mock_calls[0][1][0] == dt_util.get_time_zone("America/New_York") diff --git a/tests/test_config.py b/tests/test_config.py index 73e14fee10a..8a8cf8f909b 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -522,6 +522,7 @@ def test_core_config_schema() -> None: {"customize": {"entity_id": []}}, {"country": "xx"}, {"language": "xx"}, + {"radius": -10}, ): with pytest.raises(MultipleInvalid): config_util.CORE_CONFIG_SCHEMA(value) @@ -538,6 +539,7 @@ def test_core_config_schema() -> None: "customize": {"sensor.temperature": {"hidden": True}}, "country": "SE", "language": "sv", + "radius": "10", } ) @@ -709,10 +711,11 @@ async def test_loading_configuration_from_storage( "currency": "EUR", "country": "SE", "language": "sv", + "radius": 150, }, "key": "core.config", "version": 1, - "minor_version": 3, + "minor_version": 4, } await config_util.async_process_ha_core_config( hass, {"allowlist_external_dirs": "/etc"} @@ -729,6 +732,7 @@ async def test_loading_configuration_from_storage( assert hass.config.currency == "EUR" assert hass.config.country == "SE" assert hass.config.language == "sv" + assert hass.config.radius == 150 assert len(hass.config.allowlist_external_dirs) == 3 assert "/etc" in hass.config.allowlist_external_dirs assert hass.config.config_source is ConfigSource.STORAGE @@ -798,15 +802,19 @@ async def test_migration_and_updating_configuration( expected_new_core_data["data"]["currency"] = "USD" # 1.1 -> 1.2 store migration with migrated unit system expected_new_core_data["data"]["unit_system_v2"] = "us_customary" - expected_new_core_data["minor_version"] = 3 - # defaults for country and language + # 1.1 -> 1.3 defaults for country and language expected_new_core_data["data"]["country"] = None expected_new_core_data["data"]["language"] = "en" + # 1.1 -> 1.4 defaults for zone radius + expected_new_core_data["data"]["radius"] = 100 + # Bumped minor version + expected_new_core_data["minor_version"] = 4 assert hass_storage["core.config"] == expected_new_core_data assert hass.config.latitude == 50 assert hass.config.currency == "USD" assert hass.config.country is None assert hass.config.language == "en" + assert hass.config.radius == 100 async def test_override_stored_configuration( @@ -860,6 +868,7 @@ async def test_loading_configuration(hass: HomeAssistant) -> None: "currency": "EUR", "country": "SE", "language": "sv", + "radius": 150, }, ) @@ -881,6 +890,7 @@ async def test_loading_configuration(hass: HomeAssistant) -> None: assert hass.config.currency == "EUR" assert hass.config.country == "SE" assert hass.config.language == "sv" + assert hass.config.radius == 150 @pytest.mark.parametrize( diff --git a/tests/test_core.py b/tests/test_core.py index 8be2599f454..4c53e1bbd58 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -1936,6 +1936,7 @@ async def test_config_defaults() -> None: assert config.currency == "EUR" assert config.country is None assert config.language == "en" + assert config.radius == 100 async def test_config_path_with_file() -> None: @@ -1983,6 +1984,7 @@ async def test_config_as_dict() -> None: "language": "en", "safe_mode": False, "debug": False, + "radius": 100, } assert expected == config.as_dict() From 08ef55673620116ff92d3f2867a66e5420c53276 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 15 Jun 2024 14:04:42 +0200 Subject: [PATCH 0711/1445] Ensure workday issues are not persistent (#119732) --- homeassistant/components/workday/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/workday/__init__.py b/homeassistant/components/workday/__init__.py index f25cf41b992..60a0489ec5c 100644 --- a/homeassistant/components/workday/__init__.py +++ b/homeassistant/components/workday/__init__.py @@ -35,7 +35,7 @@ async def _async_validate_country_and_province( DOMAIN, "bad_country", is_fixable=True, - is_persistent=True, + is_persistent=False, severity=IssueSeverity.ERROR, translation_key="bad_country", translation_placeholders={"title": entry.title}, @@ -59,7 +59,7 @@ async def _async_validate_country_and_province( DOMAIN, "bad_province", is_fixable=True, - is_persistent=True, + is_persistent=False, severity=IssueSeverity.ERROR, translation_key="bad_province", translation_placeholders={ From af0f540dd48181172e800a92657a5007aac6b52f Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 15 Jun 2024 14:05:18 +0200 Subject: [PATCH 0712/1445] Ensure UniFi Protect EA warning is not persistent (#119730) --- homeassistant/components/unifiprotect/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/unifiprotect/__init__.py b/homeassistant/components/unifiprotect/__init__.py index e1e5f977c3d..fa20c892850 100644 --- a/homeassistant/components/unifiprotect/__init__.py +++ b/homeassistant/components/unifiprotect/__init__.py @@ -125,7 +125,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: UFPConfigEntry) -> bool: DOMAIN, "ea_channel_warning", is_fixable=True, - is_persistent=True, + is_persistent=False, learn_more_url=EARLY_ACCESS_URL, severity=IssueSeverity.WARNING, translation_key="ea_channel_warning", From b405e2f03e58ecb9daf7d86fbce8f1379c4ce16d Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Sat, 15 Jun 2024 16:50:12 +0200 Subject: [PATCH 0713/1445] Improve logging use of deprecated `schema` option for mqtt vacuum (#119724) --- homeassistant/components/mqtt/vacuum.py | 9 +++++ tests/components/mqtt/test_vacuum.py | 49 +++++++++++++++++++------ 2 files changed, 46 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/mqtt/vacuum.py b/homeassistant/components/mqtt/vacuum.py index fb988751d6b..eac3556a28b 100644 --- a/homeassistant/components/mqtt/vacuum.py +++ b/homeassistant/components/mqtt/vacuum.py @@ -170,6 +170,15 @@ def _fail_legacy_config(discovery: bool) -> Callable[[ConfigType], ConfigType]: ) if discovery: + _LOGGER.warning( + "The `schema` option is deprecated for MQTT %s, but " + "it was used in a discovery payload. Please contact the maintainer " + "of the integration or service that supplies the config, and suggest " + "to remove the option. Got %s at discovery topic %s", + vacuum.DOMAIN, + config, + getattr(config, "discovery_data")["discovery_topic"], + ) return config translation_key = "deprecation_mqtt_schema_vacuum_yaml" diff --git a/tests/components/mqtt/test_vacuum.py b/tests/components/mqtt/test_vacuum.py index 7563752b2d7..0a06759c7e6 100644 --- a/tests/components/mqtt/test_vacuum.py +++ b/tests/components/mqtt/test_vacuum.py @@ -2,6 +2,7 @@ from copy import deepcopy import json +import logging from typing import Any from unittest.mock import patch @@ -12,7 +13,6 @@ from homeassistant.components.mqtt import vacuum as mqttvacuum from homeassistant.components.mqtt.const import CONF_COMMAND_TOPIC, CONF_STATE_TOPIC from homeassistant.components.mqtt.vacuum import ( ALL_SERVICES, - CONF_SCHEMA, MQTT_VACUUM_ATTRIBUTES_BLOCKED, SERVICE_TO_STRING, services_to_strings, @@ -77,7 +77,6 @@ STATE_TOPIC = "vacuum/state" DEFAULT_CONFIG = { mqtt.DOMAIN: { vacuum.DOMAIN: { - CONF_SCHEMA: "state", CONF_NAME: "mqtttest", CONF_COMMAND_TOPIC: COMMAND_TOPIC, mqttvacuum.CONF_SEND_COMMAND_TOPIC: SEND_COMMAND_TOPIC, @@ -88,7 +87,7 @@ DEFAULT_CONFIG = { } } -DEFAULT_CONFIG_2 = {mqtt.DOMAIN: {vacuum.DOMAIN: {"schema": "state", "name": "test"}}} +DEFAULT_CONFIG_2 = {mqtt.DOMAIN: {vacuum.DOMAIN: {"name": "test"}}} CONFIG_ALL_SERVICES = help_custom_config( vacuum.DOMAIN, @@ -103,6 +102,35 @@ CONFIG_ALL_SERVICES = help_custom_config( ) +async def test_warning_schema_option( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test the warning on use of deprecated schema option.""" + await mqtt_mock_entry() + # Send discovery message with deprecated schema option + async_fire_mqtt_message( + hass, + f"homeassistant/{vacuum.DOMAIN}/bla/config", + '{"name": "test", "schema": "state", "o": {"name": "Bla2MQTT", "sw": "0.99", "url":"https://example.com/support"}}', + ) + await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) + + state = hass.states.get("vacuum.test") + assert state is not None + with caplog.at_level(logging.WARNING): + assert ( + "The `schema` option is deprecated for MQTT vacuum, but it was used in a " + "discovery payload. Please contact the maintainer of the integration or " + "service that supplies the config, and suggest to remove the option." + in caplog.text + ) + assert "https://example.com/support" in caplog.text + assert "at discovery topic homeassistant/vacuum/bla/config" in caplog.text + + @pytest.mark.parametrize("hass_config", [DEFAULT_CONFIG]) async def test_default_supported_features( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator @@ -261,7 +289,6 @@ async def test_commands_without_supported_features( "mqtt": { "vacuum": { "name": "test", - "schema": "state", mqttvacuum.CONF_SUPPORTED_FEATURES: services_to_strings( ALL_SERVICES, SERVICE_TO_STRING ), @@ -525,13 +552,11 @@ async def test_discovery_update_attr( mqtt.DOMAIN: { vacuum.DOMAIN: [ { - "schema": "state", "name": "Test 1", "command_topic": "command-topic", "unique_id": "TOTALLY_UNIQUE", }, { - "schema": "state", "name": "Test 2", "command_topic": "command-topic", "unique_id": "TOTALLY_UNIQUE", @@ -554,7 +579,7 @@ async def test_discovery_removal_vacuum( caplog: pytest.LogCaptureFixture, ) -> None: """Test removal of discovered vacuum.""" - data = '{ "schema": "state", "name": "test", "command_topic": "test_topic"}' + data = '{"name": "test", "command_topic": "test_topic"}' await help_test_discovery_removal( hass, mqtt_mock_entry, caplog, vacuum.DOMAIN, data ) @@ -566,8 +591,8 @@ async def test_discovery_update_vacuum( caplog: pytest.LogCaptureFixture, ) -> None: """Test update of discovered vacuum.""" - config1 = {"schema": "state", "name": "Beer", "command_topic": "test_topic"} - config2 = {"schema": "state", "name": "Milk", "command_topic": "test_topic"} + config1 = {"name": "Beer", "command_topic": "test_topic"} + config2 = {"name": "Milk", "command_topic": "test_topic"} await help_test_discovery_update( hass, mqtt_mock_entry, caplog, vacuum.DOMAIN, config1, config2 ) @@ -579,7 +604,7 @@ async def test_discovery_update_unchanged_vacuum( caplog: pytest.LogCaptureFixture, ) -> None: """Test update of discovered vacuum.""" - data1 = '{ "schema": "state", "name": "Beer", "command_topic": "test_topic"}' + data1 = '{"name": "Beer", "command_topic": "test_topic"}' with patch( "homeassistant.components.mqtt.vacuum.MqttStateVacuum.discovery_update" ) as discovery_update: @@ -600,8 +625,8 @@ async def test_discovery_broken( caplog: pytest.LogCaptureFixture, ) -> None: """Test handling of bad discovery message.""" - data1 = '{ "schema": "state", "name": "Beer", "command_topic": "test_topic#"}' - data2 = '{ "schema": "state", "name": "Milk", "command_topic": "test_topic"}' + data1 = '{"name": "Beer", "command_topic": "test_topic#"}' + data2 = '{"name": "Milk", "command_topic": "test_topic"}' await help_test_discovery_broken( hass, mqtt_mock_entry, caplog, vacuum.DOMAIN, data1, data2 ) From 410ef8ce1447f91c87fc0404b92d35cfebf60f6e Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Sat, 15 Jun 2024 12:14:34 -0400 Subject: [PATCH 0714/1445] Store runtime data inside the config entry in Efergy (#119551) * Store runtime data inside the config entry in Efergy * store later --- homeassistant/components/efergy/__init__.py | 12 ++++++------ homeassistant/components/efergy/sensor.py | 11 ++++++----- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/efergy/__init__.py b/homeassistant/components/efergy/__init__.py index 3bfd37392ad..52979e50552 100644 --- a/homeassistant/components/efergy/__init__.py +++ b/homeassistant/components/efergy/__init__.py @@ -16,9 +16,10 @@ from homeassistant.helpers.entity import Entity from .const import DEFAULT_NAME, DOMAIN PLATFORMS = [Platform.SENSOR] +type EfergyConfigEntry = ConfigEntry[Efergy] -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: EfergyConfigEntry) -> bool: """Set up Efergy from a config entry.""" api = Efergy( entry.data[CONF_API_KEY], @@ -36,17 +37,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: "API Key is no longer valid. Please reauthenticate" ) from ex - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = api + entry.runtime_data = api + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: EfergyConfigEntry) -> bool: """Unload a config entry.""" - if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): - hass.data[DOMAIN].pop(entry.entry_id) - return unload_ok + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) class EfergyEntity(Entity): diff --git a/homeassistant/components/efergy/sensor.py b/homeassistant/components/efergy/sensor.py index 59b2799d37b..a03f8f7d012 100644 --- a/homeassistant/components/efergy/sensor.py +++ b/homeassistant/components/efergy/sensor.py @@ -15,14 +15,13 @@ from homeassistant.components.sensor import ( SensorEntityDescription, SensorStateClass, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import UnitOfEnergy, UnitOfPower from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType -from . import EfergyEntity -from .const import CONF_CURRENT_VALUES, DOMAIN, LOGGER +from . import EfergyConfigEntry, EfergyEntity +from .const import CONF_CURRENT_VALUES, LOGGER SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( @@ -106,10 +105,12 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, + entry: EfergyConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up Efergy sensors.""" - api: Efergy = hass.data[DOMAIN][entry.entry_id] + api = entry.runtime_data sensors = [] for description in SENSOR_TYPES: if description.key != CONF_CURRENT_VALUES: From 90650429603cf379f3def8e6ecb8610b93321050 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ethem=20Cem=20=C3=96zkan?= Date: Sat, 15 Jun 2024 18:16:10 +0200 Subject: [PATCH 0715/1445] Revert "Revert Use integration fallback configuration for tado water fallback" (#119526) * Revert "Revert Use integration fallback configuration for tado water heater fallback (#119466)" This reverts commit ade936e6d5088c4a4d809111417fb3c7080825d5. * add decide method for duration * add repair issue to let users know * test module for repairs * Update strings.json Co-authored-by: Franck Nijhof * repair issue should not be persistent * use issue_registery fixture instead of mocking * fix comment * parameterize repair issue created test case --------- Co-authored-by: Franck Nijhof --- homeassistant/components/tado/climate.py | 41 +++------ homeassistant/components/tado/const.py | 2 + homeassistant/components/tado/helper.py | 51 +++++++++++ homeassistant/components/tado/repairs.py | 34 ++++++++ homeassistant/components/tado/strings.json | 4 + homeassistant/components/tado/water_heater.py | 26 ++++-- tests/components/tado/test_helper.py | 87 +++++++++++++++++++ tests/components/tado/test_repairs.py | 64 ++++++++++++++ 8 files changed, 274 insertions(+), 35 deletions(-) create mode 100644 homeassistant/components/tado/helper.py create mode 100644 homeassistant/components/tado/repairs.py create mode 100644 tests/components/tado/test_helper.py create mode 100644 tests/components/tado/test_repairs.py diff --git a/homeassistant/components/tado/climate.py b/homeassistant/components/tado/climate.py index 6d298a80e79..3cb5d7fbce9 100644 --- a/homeassistant/components/tado/climate.py +++ b/homeassistant/components/tado/climate.py @@ -36,10 +36,7 @@ from .const import ( CONST_MODE_OFF, CONST_MODE_SMART_SCHEDULE, CONST_OVERLAY_MANUAL, - CONST_OVERLAY_TADO_DEFAULT, - CONST_OVERLAY_TADO_MODE, CONST_OVERLAY_TADO_OPTIONS, - CONST_OVERLAY_TIMER, DATA, DOMAIN, HA_TERMINATION_DURATION, @@ -67,6 +64,7 @@ from .const import ( TYPE_HEATING, ) from .entity import TadoZoneEntity +from .helper import decide_duration, decide_overlay_mode _LOGGER = logging.getLogger(__name__) @@ -598,31 +596,18 @@ class TadoClimate(TadoZoneEntity, ClimateEntity): self._tado.reset_zone_overlay(self.zone_id) return - # If user gave duration then overlay mode needs to be timer - if duration: - overlay_mode = CONST_OVERLAY_TIMER - # If no duration or timer set to fallback setting - if overlay_mode is None: - overlay_mode = ( - self._tado.fallback - if self._tado.fallback is not None - else CONST_OVERLAY_TADO_MODE - ) - # If default is Tado default then look it up - if overlay_mode == CONST_OVERLAY_TADO_DEFAULT: - overlay_mode = ( - self._tado_zone_data.default_overlay_termination_type - if self._tado_zone_data.default_overlay_termination_type is not None - else CONST_OVERLAY_TADO_MODE - ) - # If we ended up with a timer but no duration, set a default duration - if overlay_mode == CONST_OVERLAY_TIMER and duration is None: - duration = ( - int(self._tado_zone_data.default_overlay_termination_duration) - if self._tado_zone_data.default_overlay_termination_duration is not None - else 3600 - ) - + overlay_mode = decide_overlay_mode( + tado=self._tado, + duration=duration, + overlay_mode=overlay_mode, + zone_id=self.zone_id, + ) + duration = decide_duration( + tado=self._tado, + duration=duration, + zone_id=self.zone_id, + overlay_mode=overlay_mode, + ) _LOGGER.debug( ( "Switching to %s for zone %s (%d) with temperature %s °C and duration" diff --git a/homeassistant/components/tado/const.py b/homeassistant/components/tado/const.py index c62352a6d95..be35bbb8e25 100644 --- a/homeassistant/components/tado/const.py +++ b/homeassistant/components/tado/const.py @@ -212,3 +212,5 @@ SERVICE_ADD_METER_READING = "add_meter_reading" CONF_CONFIG_ENTRY = "config_entry" CONF_READING = "reading" ATTR_MESSAGE = "message" + +WATER_HEATER_FALLBACK_REPAIR = "water_heater_fallback" diff --git a/homeassistant/components/tado/helper.py b/homeassistant/components/tado/helper.py new file mode 100644 index 00000000000..efcd3e7c4ea --- /dev/null +++ b/homeassistant/components/tado/helper.py @@ -0,0 +1,51 @@ +"""Helper methods for Tado.""" + +from . import TadoConnector +from .const import ( + CONST_OVERLAY_TADO_DEFAULT, + CONST_OVERLAY_TADO_MODE, + CONST_OVERLAY_TIMER, +) + + +def decide_overlay_mode( + tado: TadoConnector, + duration: int | None, + zone_id: int, + overlay_mode: str | None = None, +) -> str: + """Return correct overlay mode based on the action and defaults.""" + # If user gave duration then overlay mode needs to be timer + if duration: + return CONST_OVERLAY_TIMER + # If no duration or timer set to fallback setting + if overlay_mode is None: + overlay_mode = tado.fallback or CONST_OVERLAY_TADO_MODE + # If default is Tado default then look it up + if overlay_mode == CONST_OVERLAY_TADO_DEFAULT: + overlay_mode = ( + tado.data["zone"][zone_id].default_overlay_termination_type + or CONST_OVERLAY_TADO_MODE + ) + + return overlay_mode + + +def decide_duration( + tado: TadoConnector, + duration: int | None, + zone_id: int, + overlay_mode: str | None = None, +) -> None | int: + """Return correct duration based on the selected overlay mode/duration and tado config.""" + # If we ended up with a timer but no duration, set a default duration + # If we ended up with a timer but no duration, set a default duration + if overlay_mode == CONST_OVERLAY_TIMER and duration is None: + duration = ( + int(tado.data["zone"][zone_id].default_overlay_termination_duration) + if tado.data["zone"][zone_id].default_overlay_termination_duration + is not None + else 3600 + ) + + return duration diff --git a/homeassistant/components/tado/repairs.py b/homeassistant/components/tado/repairs.py new file mode 100644 index 00000000000..5ffc3c76bf7 --- /dev/null +++ b/homeassistant/components/tado/repairs.py @@ -0,0 +1,34 @@ +"""Repair implementations.""" + +from homeassistant.core import HomeAssistant +from homeassistant.helpers import issue_registry as ir + +from .const import ( + CONST_OVERLAY_MANUAL, + CONST_OVERLAY_TADO_DEFAULT, + DOMAIN, + WATER_HEATER_FALLBACK_REPAIR, +) + + +def manage_water_heater_fallback_issue( + hass: HomeAssistant, + water_heater_entities: list, + integration_overlay_fallback: str | None, +) -> None: + """Notify users about water heater respecting fallback setting.""" + if ( + integration_overlay_fallback + in [CONST_OVERLAY_TADO_DEFAULT, CONST_OVERLAY_MANUAL] + and len(water_heater_entities) > 0 + ): + for water_heater_entity in water_heater_entities: + ir.async_create_issue( + hass=hass, + domain=DOMAIN, + issue_id=f"{WATER_HEATER_FALLBACK_REPAIR}_{water_heater_entity.zone_name}", + is_fixable=False, + is_persistent=False, + severity=ir.IssueSeverity.WARNING, + translation_key=WATER_HEATER_FALLBACK_REPAIR, + ) diff --git a/homeassistant/components/tado/strings.json b/homeassistant/components/tado/strings.json index 51e36fe5355..d992befe112 100644 --- a/homeassistant/components/tado/strings.json +++ b/homeassistant/components/tado/strings.json @@ -165,6 +165,10 @@ "import_failed_invalid_auth": { "title": "Failed to import, invalid credentials", "description": "Failed to import the configuration for the Tado Device Tracker, due to invalid credentials. Please fix the YAML configuration and restart Home Assistant. Alternatively you can use the UI to configure Tado. Don't forget to delete the YAML configuration, once the import is successful." + }, + "water_heater_fallback": { + "title": "Tado Water Heater entities now support fallback options", + "description": "Due to added support for water heaters entities, these entities may use different overlay. Please configure integration entity and tado app water heater zone overlay options." } } } diff --git a/homeassistant/components/tado/water_heater.py b/homeassistant/components/tado/water_heater.py index f1257f097eb..a31b70a8f9a 100644 --- a/homeassistant/components/tado/water_heater.py +++ b/homeassistant/components/tado/water_heater.py @@ -32,6 +32,8 @@ from .const import ( TYPE_HOT_WATER, ) from .entity import TadoZoneEntity +from .helper import decide_duration, decide_overlay_mode +from .repairs import manage_water_heater_fallback_issue _LOGGER = logging.getLogger(__name__) @@ -79,6 +81,12 @@ async def async_setup_entry( async_add_entities(entities, True) + manage_water_heater_fallback_issue( + hass=hass, + water_heater_entities=entities, + integration_overlay_fallback=tado.fallback, + ) + def _generate_entities(tado: TadoConnector) -> list[WaterHeaterEntity]: """Create all water heater entities.""" @@ -277,13 +285,17 @@ class TadoWaterHeater(TadoZoneEntity, WaterHeaterEntity): self._tado.set_zone_off(self.zone_id, CONST_OVERLAY_MANUAL, TYPE_HOT_WATER) return - overlay_mode = CONST_OVERLAY_MANUAL - if duration: - overlay_mode = CONST_OVERLAY_TIMER - elif self._tado.fallback: - # Fallback to Smart Schedule at next Schedule switch if we have fallback enabled - overlay_mode = CONST_OVERLAY_TADO_MODE - + overlay_mode = decide_overlay_mode( + tado=self._tado, + duration=duration, + zone_id=self.zone_id, + ) + duration = decide_duration( + tado=self._tado, + duration=duration, + zone_id=self.zone_id, + overlay_mode=overlay_mode, + ) _LOGGER.debug( "Switching to %s for zone %s (%d) with temperature %s", self._current_tado_hvac_mode, diff --git a/tests/components/tado/test_helper.py b/tests/components/tado/test_helper.py new file mode 100644 index 00000000000..bdd7977f858 --- /dev/null +++ b/tests/components/tado/test_helper.py @@ -0,0 +1,87 @@ +"""Helper method tests.""" + +from unittest.mock import patch + +from homeassistant.components.tado import TadoConnector +from homeassistant.components.tado.const import ( + CONST_OVERLAY_MANUAL, + CONST_OVERLAY_TADO_DEFAULT, + CONST_OVERLAY_TADO_MODE, + CONST_OVERLAY_TIMER, +) +from homeassistant.components.tado.helper import decide_duration, decide_overlay_mode +from homeassistant.core import HomeAssistant + + +def dummy_tado_connector(hass: HomeAssistant, fallback) -> TadoConnector: + """Return dummy tado connector.""" + return TadoConnector(hass, username="dummy", password="dummy", fallback=fallback) + + +async def test_overlay_mode_duration_set(hass: HomeAssistant) -> None: + """Test overlay method selection when duration is set.""" + tado = dummy_tado_connector(hass=hass, fallback=CONST_OVERLAY_TADO_MODE) + overlay_mode = decide_overlay_mode(tado=tado, duration=3600, zone_id=1) + # Must select TIMER overlay + assert overlay_mode == CONST_OVERLAY_TIMER + + +async def test_overlay_mode_next_time_block_fallback(hass: HomeAssistant) -> None: + """Test overlay method selection when duration is not set.""" + integration_fallback = CONST_OVERLAY_TADO_MODE + tado = dummy_tado_connector(hass=hass, fallback=integration_fallback) + overlay_mode = decide_overlay_mode(tado=tado, duration=None, zone_id=1) + # Must fallback to integration wide setting + assert overlay_mode == integration_fallback + + +async def test_overlay_mode_tado_default_fallback(hass: HomeAssistant) -> None: + """Test overlay method selection when tado default is selected.""" + integration_fallback = CONST_OVERLAY_TADO_DEFAULT + zone_fallback = CONST_OVERLAY_MANUAL + tado = dummy_tado_connector(hass=hass, fallback=integration_fallback) + + class MockZoneData: + def __init__(self) -> None: + self.default_overlay_termination_type = zone_fallback + + zone_id = 1 + + zone_data = {"zone": {zone_id: MockZoneData()}} + with patch.dict(tado.data, zone_data): + overlay_mode = decide_overlay_mode(tado=tado, duration=None, zone_id=zone_id) + # Must fallback to zone setting + assert overlay_mode == zone_fallback + + +async def test_duration_enabled_without_tado_default(hass: HomeAssistant) -> None: + """Test duration decide method when overlay is timer and duration is set.""" + overlay = CONST_OVERLAY_TIMER + expected_duration = 600 + tado = dummy_tado_connector(hass=hass, fallback=CONST_OVERLAY_MANUAL) + duration = decide_duration( + tado=tado, duration=expected_duration, overlay_mode=overlay, zone_id=0 + ) + # Should return the same duration value + assert duration == expected_duration + + +async def test_duration_enabled_with_tado_default(hass: HomeAssistant) -> None: + """Test overlay method selection when ended up with timer overlay and None duration.""" + zone_fallback = CONST_OVERLAY_TIMER + expected_duration = 45000 + tado = dummy_tado_connector(hass=hass, fallback=zone_fallback) + + class MockZoneData: + def __init__(self) -> None: + self.default_overlay_termination_duration = expected_duration + + zone_id = 1 + + zone_data = {"zone": {zone_id: MockZoneData()}} + with patch.dict(tado.data, zone_data): + duration = decide_duration( + tado=tado, duration=None, zone_id=zone_id, overlay_mode=zone_fallback + ) + # Must fallback to zone timer setting + assert duration == expected_duration diff --git a/tests/components/tado/test_repairs.py b/tests/components/tado/test_repairs.py new file mode 100644 index 00000000000..2e055884272 --- /dev/null +++ b/tests/components/tado/test_repairs.py @@ -0,0 +1,64 @@ +"""Repair tests.""" + +import pytest + +from homeassistant.components.tado.const import ( + CONST_OVERLAY_MANUAL, + CONST_OVERLAY_TADO_DEFAULT, + CONST_OVERLAY_TADO_MODE, + DOMAIN, + WATER_HEATER_FALLBACK_REPAIR, +) +from homeassistant.components.tado.repairs import manage_water_heater_fallback_issue +from homeassistant.core import HomeAssistant +from homeassistant.helpers import issue_registry as ir + + +class MockWaterHeater: + """Mock Water heater entity.""" + + def __init__(self, zone_name) -> None: + """Init mock entity class.""" + self.zone_name = zone_name + + +async def test_manage_water_heater_fallback_issue_not_created( + hass: HomeAssistant, + issue_registry: ir.IssueRegistry, +) -> None: + """Test water heater fallback issue is not needed.""" + zone_name = "Hot Water" + expected_issue_id = f"{WATER_HEATER_FALLBACK_REPAIR}_{zone_name}" + water_heater_entities = [MockWaterHeater(zone_name)] + manage_water_heater_fallback_issue( + water_heater_entities=water_heater_entities, + integration_overlay_fallback=CONST_OVERLAY_TADO_MODE, + hass=hass, + ) + assert ( + issue_registry.async_get_issue(issue_id=expected_issue_id, domain=DOMAIN) + is None + ) + + +@pytest.mark.parametrize( + "integration_overlay_fallback", [CONST_OVERLAY_TADO_DEFAULT, CONST_OVERLAY_MANUAL] +) +async def test_manage_water_heater_fallback_issue_created( + hass: HomeAssistant, + issue_registry: ir.IssueRegistry, + integration_overlay_fallback: str, +) -> None: + """Test water heater fallback issue created cases.""" + zone_name = "Hot Water" + expected_issue_id = f"{WATER_HEATER_FALLBACK_REPAIR}_{zone_name}" + water_heater_entities = [MockWaterHeater(zone_name)] + manage_water_heater_fallback_issue( + water_heater_entities=water_heater_entities, + integration_overlay_fallback=integration_overlay_fallback, + hass=hass, + ) + assert ( + issue_registry.async_get_issue(issue_id=expected_issue_id, domain=DOMAIN) + is not None + ) From 59ade9cf933cafabc030738d263a35f964233d20 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Sat, 15 Jun 2024 17:47:47 -0500 Subject: [PATCH 0716/1445] Fix model import in Spotify (#119747) * Always import HomeAssistantSpotifyData in spotify.media_browser Relocate HomeAssistantSpotifyData to avoid circular import * Fix moved import * Rename module to 'models' * Adjust docstring --- homeassistant/components/spotify/__init__.py | 12 +----------- .../components/spotify/browse_media.py | 6 ++---- .../components/spotify/media_player.py | 3 ++- homeassistant/components/spotify/models.py | 19 +++++++++++++++++++ 4 files changed, 24 insertions(+), 16 deletions(-) create mode 100644 homeassistant/components/spotify/models.py diff --git a/homeassistant/components/spotify/__init__.py b/homeassistant/components/spotify/__init__.py index 632871ba36e..becf90b04cd 100644 --- a/homeassistant/components/spotify/__init__.py +++ b/homeassistant/components/spotify/__init__.py @@ -2,7 +2,6 @@ from __future__ import annotations -from dataclasses import dataclass from datetime import timedelta from typing import Any @@ -22,6 +21,7 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda from .browse_media import async_browse_media from .const import DOMAIN, LOGGER, SPOTIFY_SCOPES +from .models import HomeAssistantSpotifyData from .util import ( is_spotify_media_type, resolve_spotify_media_type, @@ -39,16 +39,6 @@ __all__ = [ ] -@dataclass -class HomeAssistantSpotifyData: - """Spotify data stored in the Home Assistant data object.""" - - client: Spotify - current_user: dict[str, Any] - devices: DataUpdateCoordinator[list[dict[str, Any]]] - session: OAuth2Session - - type SpotifyConfigEntry = ConfigEntry[HomeAssistantSpotifyData] diff --git a/homeassistant/components/spotify/browse_media.py b/homeassistant/components/spotify/browse_media.py index a1d3d9c804a..cff7cae5ebd 100644 --- a/homeassistant/components/spotify/browse_media.py +++ b/homeassistant/components/spotify/browse_media.py @@ -5,7 +5,7 @@ from __future__ import annotations from enum import StrEnum from functools import partial import logging -from typing import TYPE_CHECKING, Any +from typing import Any from spotipy import Spotify import yarl @@ -20,11 +20,9 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.config_entry_oauth2_flow import OAuth2Session from .const import DOMAIN, MEDIA_PLAYER_PREFIX, MEDIA_TYPE_SHOW, PLAYABLE_MEDIA_TYPES +from .models import HomeAssistantSpotifyData from .util import fetch_image_url -if TYPE_CHECKING: - from . import HomeAssistantSpotifyData - BROWSE_LIMIT = 48 diff --git a/homeassistant/components/spotify/media_player.py b/homeassistant/components/spotify/media_player.py index fe9614374f7..bd1bcdfd43e 100644 --- a/homeassistant/components/spotify/media_player.py +++ b/homeassistant/components/spotify/media_player.py @@ -29,9 +29,10 @@ from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util.dt import utcnow -from . import HomeAssistantSpotifyData, SpotifyConfigEntry +from . import SpotifyConfigEntry from .browse_media import async_browse_media_internal from .const import DOMAIN, MEDIA_PLAYER_PREFIX, PLAYABLE_MEDIA_TYPES, SPOTIFY_SCOPES +from .models import HomeAssistantSpotifyData from .util import fetch_image_url _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/spotify/models.py b/homeassistant/components/spotify/models.py new file mode 100644 index 00000000000..bbec134d89d --- /dev/null +++ b/homeassistant/components/spotify/models.py @@ -0,0 +1,19 @@ +"""Models for use in Spotify integration.""" + +from dataclasses import dataclass +from typing import Any + +from spotipy import Spotify + +from homeassistant.helpers.config_entry_oauth2_flow import OAuth2Session +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + + +@dataclass +class HomeAssistantSpotifyData: + """Spotify data stored in the Home Assistant data object.""" + + client: Spotify + current_user: dict[str, Any] + devices: DataUpdateCoordinator[list[dict[str, Any]]] + session: OAuth2Session From c0a680a80a2d0f43d9a3151bdc75cd3b48c0b332 Mon Sep 17 00:00:00 2001 From: Lode Smets <31108717+lodesmets@users.noreply.github.com> Date: Sun, 16 Jun 2024 00:48:08 +0200 Subject: [PATCH 0717/1445] Fix for Synology DSM shared images (#117695) * Fix for shared images * - FIX: Synology shared photos * - changes after review * Added test * added test * fix test --- .../components/synology_dsm/const.py | 2 ++ .../components/synology_dsm/media_source.py | 21 +++++++++++---- .../synology_dsm/test_media_source.py | 26 ++++++++++++++++--- 3 files changed, 41 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/synology_dsm/const.py b/homeassistant/components/synology_dsm/const.py index 11839caf8be..e6367458578 100644 --- a/homeassistant/components/synology_dsm/const.py +++ b/homeassistant/components/synology_dsm/const.py @@ -46,6 +46,8 @@ DEFAULT_SNAPSHOT_QUALITY = SNAPSHOT_PROFILE_BALANCED ENTITY_UNIT_LOAD = "load" +SHARED_SUFFIX = "_shared" + # Signals SIGNAL_CAMERA_SOURCE_CHANGED = "synology_dsm.camera_stream_source_changed" diff --git a/homeassistant/components/synology_dsm/media_source.py b/homeassistant/components/synology_dsm/media_source.py index 4b0c19b2b55..ace5733c222 100644 --- a/homeassistant/components/synology_dsm/media_source.py +++ b/homeassistant/components/synology_dsm/media_source.py @@ -21,7 +21,7 @@ from homeassistant.components.media_source import ( from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from .const import DOMAIN +from .const import DOMAIN, SHARED_SUFFIX from .models import SynologyDSMData @@ -45,6 +45,7 @@ class SynologyPhotosMediaSourceIdentifier: self.album_id = None self.cache_key = None self.file_name = None + self.is_shared = False if parts: self.unique_id = parts[0] @@ -54,6 +55,9 @@ class SynologyPhotosMediaSourceIdentifier: self.cache_key = parts[2] if len(parts) > 3: self.file_name = parts[3] + if self.file_name.endswith(SHARED_SUFFIX): + self.is_shared = True + self.file_name = self.file_name.removesuffix(SHARED_SUFFIX) class SynologyPhotosMediaSource(MediaSource): @@ -160,10 +164,13 @@ class SynologyPhotosMediaSource(MediaSource): if isinstance(mime_type, str) and mime_type.startswith("image/"): # Force small small thumbnails album_item.thumbnail_size = "sm" + suffix = "" + if album_item.is_shared: + suffix = SHARED_SUFFIX ret.append( BrowseMediaSource( domain=DOMAIN, - identifier=f"{identifier.unique_id}/{identifier.album_id}/{album_item.thumbnail_cache_key}/{album_item.file_name}", + identifier=f"{identifier.unique_id}/{identifier.album_id}/{album_item.thumbnail_cache_key}/{album_item.file_name}{suffix}", media_class=MediaClass.IMAGE, media_content_type=mime_type, title=album_item.file_name, @@ -186,8 +193,11 @@ class SynologyPhotosMediaSource(MediaSource): mime_type, _ = mimetypes.guess_type(identifier.file_name) if not isinstance(mime_type, str): raise Unresolvable("No file extension") + suffix = "" + if identifier.is_shared: + suffix = SHARED_SUFFIX return PlayMedia( - f"/synology_dsm/{identifier.unique_id}/{identifier.cache_key}/{identifier.file_name}", + f"/synology_dsm/{identifier.unique_id}/{identifier.cache_key}/{identifier.file_name}{suffix}", mime_type, ) @@ -223,13 +233,14 @@ class SynologyDsmMediaView(http.HomeAssistantView): # location: {cache_key}/{filename} cache_key, file_name = location.split("/") image_id = int(cache_key.split("_")[0]) + if shared := file_name.endswith(SHARED_SUFFIX): + file_name = file_name.removesuffix(SHARED_SUFFIX) mime_type, _ = mimetypes.guess_type(file_name) if not isinstance(mime_type, str): raise web.HTTPNotFound diskstation: SynologyDSMData = self.hass.data[DOMAIN][source_dir_id] - assert diskstation.api.photos is not None - item = SynoPhotosItem(image_id, "", "", "", cache_key, "", False) + item = SynoPhotosItem(image_id, "", "", "", cache_key, "", shared) try: image = await diskstation.api.photos.download_item(item) except SynologyDSMException as exc: diff --git a/tests/components/synology_dsm/test_media_source.py b/tests/components/synology_dsm/test_media_source.py index 2a792d174f8..433a4b15c23 100644 --- a/tests/components/synology_dsm/test_media_source.py +++ b/tests/components/synology_dsm/test_media_source.py @@ -50,7 +50,8 @@ def dsm_with_photos() -> MagicMock: dsm.photos.get_albums = AsyncMock(return_value=[SynoPhotosAlbum(1, "Album 1", 10)]) dsm.photos.get_items_from_album = AsyncMock( return_value=[ - SynoPhotosItem(10, "", "filename.jpg", 12345, "10_1298753", "sm", False) + SynoPhotosItem(10, "", "filename.jpg", 12345, "10_1298753", "sm", False), + SynoPhotosItem(10, "", "filename.jpg", 12345, "10_1298753", "sm", True), ] ) dsm.photos.get_item_thumbnail_url = AsyncMock( @@ -102,6 +103,11 @@ async def test_resolve_media_bad_identifier( "/synology_dsm/ABC012345/12631_47189/filename.png", "image/png", ), + ( + "ABC012345/12/12631_47189/filename.png_shared", + "/synology_dsm/ABC012345/12631_47189/filename.png_shared", + "image/png", + ), ], ) async def test_resolve_media_success( @@ -333,7 +339,7 @@ async def test_browse_media_get_items_thumbnail_error( result = await source.async_browse_media(item) assert result - assert len(result.children) == 1 + assert len(result.children) == 2 item = result.children[0] assert isinstance(item, BrowseMedia) assert item.thumbnail is None @@ -372,7 +378,7 @@ async def test_browse_media_get_items( result = await source.async_browse_media(item) assert result - assert len(result.children) == 1 + assert len(result.children) == 2 item = result.children[0] assert isinstance(item, BrowseMedia) assert item.identifier == "mocked_syno_dsm_entry/1/10_1298753/filename.jpg" @@ -382,6 +388,15 @@ async def test_browse_media_get_items( assert item.can_play assert not item.can_expand assert item.thumbnail == "http://my.thumbnail.url" + item = result.children[1] + assert isinstance(item, BrowseMedia) + assert item.identifier == "mocked_syno_dsm_entry/1/10_1298753/filename.jpg_shared" + assert item.title == "filename.jpg" + assert item.media_class == MediaClass.IMAGE + assert item.media_content_type == "image/jpeg" + assert item.can_play + assert not item.can_expand + assert item.thumbnail == "http://my.thumbnail.url" @pytest.mark.usefixtures("setup_media_source") @@ -435,3 +450,8 @@ async def test_media_view( request, "mocked_syno_dsm_entry", "10_1298753/filename.jpg" ) assert isinstance(result, web.Response) + with patch.object(tempfile, "tempdir", tmp_path): + result = await view.get( + request, "mocked_syno_dsm_entry", "10_1298753/filename.jpg_shared" + ) + assert isinstance(result, web.Response) From c519e12042997d14426e3c3d73da7cd561badbe8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 15 Jun 2024 21:02:03 -0500 Subject: [PATCH 0718/1445] Cleanup unifiprotect entity model (#119746) * Small cleanups to unifiprotect * Small cleanups to unifiprotect * Small cleanups to unifiprotect * Small cleanups to unifiprotect * tweak * comments * comments * stale docstrings * missed one * remove dead code * remove dead code * remove dead code * remove dead code * cleanup --- .../components/unifiprotect/binary_sensor.py | 16 +-- .../components/unifiprotect/button.py | 4 +- .../components/unifiprotect/camera.py | 10 +- .../components/unifiprotect/entity.py | 49 +++----- .../components/unifiprotect/models.py | 118 ++++++++---------- .../components/unifiprotect/number.py | 7 +- .../components/unifiprotect/select.py | 4 +- .../components/unifiprotect/sensor.py | 6 +- .../components/unifiprotect/switch.py | 16 ++- homeassistant/components/unifiprotect/text.py | 15 +-- 10 files changed, 101 insertions(+), 144 deletions(-) diff --git a/homeassistant/components/unifiprotect/binary_sensor.py b/homeassistant/components/unifiprotect/binary_sensor.py index 4218d3108e5..19ae4504109 100644 --- a/homeassistant/components/unifiprotect/binary_sensor.py +++ b/homeassistant/components/unifiprotect/binary_sensor.py @@ -4,7 +4,6 @@ from __future__ import annotations from collections.abc import Sequence import dataclasses -import logging from uiprotect.data import ( NVR, @@ -35,15 +34,14 @@ from .entity import ( ProtectNVREntity, async_all_device_entities, ) -from .models import PermRequired, ProtectEventMixin, ProtectRequiredKeysMixin +from .models import PermRequired, ProtectEntityDescription, ProtectEventMixin -_LOGGER = logging.getLogger(__name__) _KEY_DOOR = "door" @dataclasses.dataclass(frozen=True, kw_only=True) class ProtectBinaryEntityDescription( - ProtectRequiredKeysMixin, BinarySensorEntityDescription + ProtectEntityDescription, BinarySensorEntityDescription ): """Describes UniFi Protect Binary Sensor entity.""" @@ -613,7 +611,7 @@ DISK_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( ), ) -_MODEL_DESCRIPTIONS: dict[ModelType, Sequence[ProtectRequiredKeysMixin]] = { +_MODEL_DESCRIPTIONS: dict[ModelType, Sequence[ProtectEntityDescription]] = { ModelType.CAMERA: CAMERA_SENSORS, ModelType.LIGHT: LIGHT_SENSORS, ModelType.SENSOR: SENSE_SENSORS, @@ -621,7 +619,7 @@ _MODEL_DESCRIPTIONS: dict[ModelType, Sequence[ProtectRequiredKeysMixin]] = { ModelType.VIEWPORT: VIEWER_SENSORS, } -_MOUNTABLE_MODEL_DESCRIPTIONS: dict[ModelType, Sequence[ProtectRequiredKeysMixin]] = { +_MOUNTABLE_MODEL_DESCRIPTIONS: dict[ModelType, Sequence[ProtectEntityDescription]] = { ModelType.SENSOR: MOUNTABLE_SENSE_SENSORS, } @@ -652,10 +650,9 @@ class MountableProtectDeviceBinarySensor(ProtectDeviceBinarySensor): @callback def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None: super()._async_update_device_from_protect(device) - updated_device = self.device # UP Sense can be any of the 3 contact sensor device classes self._attr_device_class = MOUNT_DEVICE_CLASS_MAP.get( - updated_device.mount_type, BinarySensorDeviceClass.DOOR + self.device.mount_type, BinarySensorDeviceClass.DOOR ) @@ -688,7 +685,6 @@ class ProtectDiskBinarySensor(ProtectNVREntity, BinarySensorEntity): @callback def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None: super()._async_update_device_from_protect(device) - slot = self._disk.slot self._attr_available = False @@ -714,7 +710,7 @@ class ProtectEventBinarySensor(EventEntityMixin, BinarySensorEntity): def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None: super()._async_update_device_from_protect(device) is_on = self.entity_description.get_is_on(self.device, self._event) - self._attr_is_on: bool | None = is_on + self._attr_is_on = is_on if not is_on: self._event = None self._attr_extra_state_attributes = {} diff --git a/homeassistant/components/unifiprotect/button.py b/homeassistant/components/unifiprotect/button.py index 7866dd5b183..8a6c4b38ea5 100644 --- a/homeassistant/components/unifiprotect/button.py +++ b/homeassistant/components/unifiprotect/button.py @@ -23,7 +23,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DEVICES_THAT_ADOPT, DISPATCH_ADD, DOMAIN from .data import ProtectData, UFPConfigEntry from .entity import ProtectDeviceEntity, async_all_device_entities -from .models import PermRequired, ProtectRequiredKeysMixin, ProtectSetableKeysMixin, T +from .models import PermRequired, ProtectEntityDescription, ProtectSetableKeysMixin, T from .utils import async_dispatch_id as _ufpd _LOGGER = logging.getLogger(__name__) @@ -95,7 +95,7 @@ CHIME_BUTTONS: tuple[ProtectButtonEntityDescription, ...] = ( ) -_MODEL_DESCRIPTIONS: dict[ModelType, Sequence[ProtectRequiredKeysMixin]] = { +_MODEL_DESCRIPTIONS: dict[ModelType, Sequence[ProtectEntityDescription]] = { ModelType.CHIME: CHIME_BUTTONS, ModelType.SENSOR: SENSOR_BUTTONS, } diff --git a/homeassistant/components/unifiprotect/camera.py b/homeassistant/components/unifiprotect/camera.py index b4596582cd6..a08e0f03e65 100644 --- a/homeassistant/components/unifiprotect/camera.py +++ b/homeassistant/components/unifiprotect/camera.py @@ -158,6 +158,9 @@ async def async_setup_entry( async_add_entities(_async_camera_entities(hass, entry, data)) +_EMPTY_CAMERA_FEATURES = CameraEntityFeature(0) + + class ProtectCamera(ProtectDeviceEntity, Camera): """A Ubiquiti UniFi Protect Camera.""" @@ -206,13 +209,12 @@ class ProtectCamera(ProtectDeviceEntity, Camera): rtsp_url = channel.rtsps_url if self._secure else channel.rtsp_url # _async_set_stream_source called by __init__ - self._stream_source = ( # pylint: disable=attribute-defined-outside-init - None if disable_stream else rtsp_url - ) + # pylint: disable-next=attribute-defined-outside-init + self._stream_source = None if disable_stream else rtsp_url if self._stream_source: self._attr_supported_features = CameraEntityFeature.STREAM else: - self._attr_supported_features = CameraEntityFeature(0) + self._attr_supported_features = _EMPTY_CAMERA_FEATURES @callback def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None: diff --git a/homeassistant/components/unifiprotect/entity.py b/homeassistant/components/unifiprotect/entity.py index d1b82dd218f..1a89b7c06d1 100644 --- a/homeassistant/components/unifiprotect/entity.py +++ b/homeassistant/components/unifiprotect/entity.py @@ -6,7 +6,7 @@ from collections.abc import Callable, Sequence from functools import partial import logging from operator import attrgetter -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING from uiprotect.data import ( NVR, @@ -31,7 +31,7 @@ from .const import ( DOMAIN, ) from .data import ProtectData -from .models import PermRequired, ProtectEventMixin, ProtectRequiredKeysMixin +from .models import PermRequired, ProtectEntityDescription, ProtectEventMixin _LOGGER = logging.getLogger(__name__) @@ -41,8 +41,8 @@ def _async_device_entities( data: ProtectData, klass: type[BaseProtectEntity], model_type: ModelType, - descs: Sequence[ProtectRequiredKeysMixin], - unadopted_descs: Sequence[ProtectRequiredKeysMixin] | None = None, + descs: Sequence[ProtectEntityDescription], + unadopted_descs: Sequence[ProtectEntityDescription] | None = None, ufp_device: ProtectAdoptableDeviceModel | None = None, ) -> list[BaseProtectEntity]: if not descs and not unadopted_descs: @@ -119,11 +119,11 @@ _ALL_MODEL_TYPES = ( @callback def _combine_model_descs( model_type: ModelType, - model_descriptions: dict[ModelType, Sequence[ProtectRequiredKeysMixin]] | None, - all_descs: Sequence[ProtectRequiredKeysMixin] | None, -) -> list[ProtectRequiredKeysMixin]: + model_descriptions: dict[ModelType, Sequence[ProtectEntityDescription]] | None, + all_descs: Sequence[ProtectEntityDescription] | None, +) -> list[ProtectEntityDescription]: """Combine all the descriptions with descriptions a model type.""" - descs: list[ProtectRequiredKeysMixin] = list(all_descs) if all_descs else [] + descs: list[ProtectEntityDescription] = list(all_descs) if all_descs else [] if model_descriptions and (model_descs := model_descriptions.get(model_type)): descs.extend(model_descs) return descs @@ -133,10 +133,10 @@ def _combine_model_descs( def async_all_device_entities( data: ProtectData, klass: type[BaseProtectEntity], - model_descriptions: dict[ModelType, Sequence[ProtectRequiredKeysMixin]] + model_descriptions: dict[ModelType, Sequence[ProtectEntityDescription]] | None = None, - all_descs: Sequence[ProtectRequiredKeysMixin] | None = None, - unadopted_descs: list[ProtectRequiredKeysMixin] | None = None, + all_descs: Sequence[ProtectEntityDescription] | None = None, + unadopted_descs: list[ProtectEntityDescription] | None = None, ufp_device: ProtectAdoptableDeviceModel | None = None, ) -> list[BaseProtectEntity]: """Generate a list of all the device entities.""" @@ -163,6 +163,7 @@ class BaseProtectEntity(Entity): device: ProtectAdoptableDeviceModel | NVR _attr_should_poll = False + _attr_attribution = DEFAULT_ATTRIBUTION _state_attrs: tuple[str, ...] = ("_attr_available",) def __init__( @@ -191,10 +192,9 @@ class BaseProtectEntity(Entity): else "" ) self._attr_name = f"{self.device.display_name} {name.title()}" - if isinstance(description, ProtectRequiredKeysMixin): + if isinstance(description, ProtectEntityDescription): self._async_get_ufp_enabled = description.get_ufp_enabled - self._attr_attribution = DEFAULT_ATTRIBUTION self._async_set_device_info() self._async_update_device_from_protect(device) self._state_getters = tuple( @@ -301,8 +301,7 @@ class ProtectNVREntity(BaseProtectEntity): @callback def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None: data = self.data - last_update_success = data.last_update_success - if last_update_success: + if last_update_success := data.last_update_success: self.device = data.api.bootstrap.nvr self._attr_available = last_update_success @@ -311,28 +310,18 @@ class ProtectNVREntity(BaseProtectEntity): class EventEntityMixin(ProtectDeviceEntity): """Adds motion event attributes to sensor.""" - _unrecorded_attributes = frozenset({ATTR_EVENT_ID, ATTR_EVENT_SCORE}) - entity_description: ProtectEventMixin - - def __init__( - self, - *args: Any, - **kwarg: Any, - ) -> None: - """Init an sensor that has event thumbnails.""" - super().__init__(*args, **kwarg) - self._event: Event | None = None + _unrecorded_attributes = frozenset({ATTR_EVENT_ID, ATTR_EVENT_SCORE}) + _event: Event | None = None @callback def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None: - event = self.entity_description.get_event_obj(device) - if event is not None: + if (event := self.entity_description.get_event_obj(device)) is None: + self._attr_extra_state_attributes = {} + else: self._attr_extra_state_attributes = { ATTR_EVENT_ID: event.id, ATTR_EVENT_SCORE: event.score, } - else: - self._attr_extra_state_attributes = {} self._event = event super()._async_update_device_from_protect(device) diff --git a/homeassistant/components/unifiprotect/models.py b/homeassistant/components/unifiprotect/models.py index d2ab31d672d..36db9a847c7 100644 --- a/homeassistant/components/unifiprotect/models.py +++ b/homeassistant/components/unifiprotect/models.py @@ -5,8 +5,10 @@ from __future__ import annotations from collections.abc import Callable, Coroutine from dataclasses import dataclass from enum import Enum +from functools import partial import logging -from typing import TYPE_CHECKING, Any, Generic, TypeVar +from operator import attrgetter +from typing import Any, Generic, TypeVar from uiprotect.data import NVR, Event, ProtectAdoptableDeviceModel @@ -19,15 +21,6 @@ _LOGGER = logging.getLogger(__name__) T = TypeVar("T", bound=ProtectAdoptableDeviceModel | NVR) -def split_tuple(value: tuple[str, ...] | str | None) -> tuple[str, ...] | None: - """Split string to tuple.""" - if value is None: - return None - if TYPE_CHECKING: - assert isinstance(value, str) - return tuple(value.split(".")) - - class PermRequired(int, Enum): """Type of permission level required for entity.""" @@ -37,92 +30,83 @@ class PermRequired(int, Enum): @dataclass(frozen=True, kw_only=True) -class ProtectRequiredKeysMixin(EntityDescription, Generic[T]): - """Mixin for required keys.""" +class ProtectEntityDescription(EntityDescription, Generic[T]): + """Base class for protect entity descriptions.""" - # `ufp_required_field`, `ufp_value`, and `ufp_enabled` are defined as - # a `str` in the dataclass, but `__post_init__` converts it to a - # `tuple[str, ...]` to avoid doing it at run time in `get_nested_attr` - # which is usually called millions of times per day. - ufp_required_field: tuple[str, ...] | str | None = None - ufp_value: tuple[str, ...] | str | None = None + ufp_required_field: str | None = None + ufp_value: str | None = None ufp_value_fn: Callable[[T], Any] | None = None - ufp_enabled: tuple[str, ...] | str | None = None + ufp_enabled: str | None = None ufp_perm: PermRequired | None = None - def __post_init__(self) -> None: - """Pre-convert strings to tuples for faster get_nested_attr.""" - object.__setattr__( - self, "ufp_required_field", split_tuple(self.ufp_required_field) - ) - object.__setattr__(self, "ufp_value", split_tuple(self.ufp_value)) - object.__setattr__(self, "ufp_enabled", split_tuple(self.ufp_enabled)) - def get_ufp_value(self, obj: T) -> Any: - """Return value from UniFi Protect device.""" - if (ufp_value := self.ufp_value) is not None: - if TYPE_CHECKING: - # `ufp_value` is defined as a `str` in the dataclass, but - # `__post_init__` converts it to a `tuple[str, ...]` to avoid - # doing it at run time in `get_nested_attr` which is usually called - # millions of times per day. This tells mypy that it's a tuple. - assert isinstance(ufp_value, tuple) - return get_nested_attr(obj, ufp_value) - if (ufp_value_fn := self.ufp_value_fn) is not None: - return ufp_value_fn(obj) + """Return value from UniFi Protect device. - # reminder for future that one is required + May be overridden by ufp_value or ufp_value_fn. + """ + # ufp_value or ufp_value_fn is required, the + # RuntimeError is to catch any issues in the code + # with new descriptions. raise RuntimeError( # pragma: no cover "`ufp_value` or `ufp_value_fn` is required" ) - def get_ufp_enabled(self, obj: T) -> bool: - """Return value from UniFi Protect device.""" - if (ufp_enabled := self.ufp_enabled) is not None: - if TYPE_CHECKING: - # `ufp_enabled` is defined as a `str` in the dataclass, but - # `__post_init__` converts it to a `tuple[str, ...]` to avoid - # doing it at run time in `get_nested_attr` which is usually called - # millions of times per day. This tells mypy that it's a tuple. - assert isinstance(ufp_enabled, tuple) - return bool(get_nested_attr(obj, ufp_enabled)) + def has_required(self, obj: T) -> bool: + """Return if required field is set. + + May be overridden by ufp_required_field. + """ return True - def has_required(self, obj: T) -> bool: - """Return if has required field.""" - if (ufp_required_field := self.ufp_required_field) is None: - return True - if TYPE_CHECKING: - # `ufp_required_field` is defined as a `str` in the dataclass, but - # `__post_init__` converts it to a `tuple[str, ...]` to avoid - # doing it at run time in `get_nested_attr` which is usually called - # millions of times per day. This tells mypy that it's a tuple. - assert isinstance(ufp_required_field, tuple) - return bool(get_nested_attr(obj, ufp_required_field)) + def get_ufp_enabled(self, obj: T) -> bool: + """Return if entity is enabled. + + May be overridden by ufp_enabled. + """ + return True + + def __post_init__(self) -> None: + """Override get_ufp_value, has_required, and get_ufp_enabled if required.""" + _setter = partial(object.__setattr__, self) + if (_ufp_value := self.ufp_value) is not None: + ufp_value = tuple(_ufp_value.split(".")) + _setter("get_ufp_value", partial(get_nested_attr, attrs=ufp_value)) + elif (ufp_value_fn := self.ufp_value_fn) is not None: + _setter("get_ufp_value", ufp_value_fn) + if (_ufp_enabled := self.ufp_enabled) is not None: + ufp_enabled = tuple(_ufp_enabled.split(".")) + _setter("get_ufp_enabled", partial(get_nested_attr, attrs=ufp_enabled)) + if (_ufp_required_field := self.ufp_required_field) is not None: + ufp_required_field = tuple(_ufp_required_field.split(".")) + _setter( + "has_required", + lambda obj: bool(get_nested_attr(obj, ufp_required_field)), + ) @dataclass(frozen=True, kw_only=True) -class ProtectEventMixin(ProtectRequiredKeysMixin[T]): +class ProtectEventMixin(ProtectEntityDescription[T]): """Mixin for events.""" ufp_event_obj: str | None = None def get_event_obj(self, obj: T) -> Event | None: """Return value from UniFi Protect device.""" - - if self.ufp_event_obj is not None: - event: Event | None = getattr(obj, self.ufp_event_obj, None) - return event return None + def __post_init__(self) -> None: + """Override get_event_obj if ufp_event_obj is set.""" + if (_ufp_event_obj := self.ufp_event_obj) is not None: + object.__setattr__(self, "get_event_obj", attrgetter(_ufp_event_obj)) + super().__post_init__() + def get_is_on(self, obj: T, event: Event | None) -> bool: """Return value if event is active.""" - return event is not None and self.get_ufp_value(obj) @dataclass(frozen=True, kw_only=True) -class ProtectSetableKeysMixin(ProtectRequiredKeysMixin[T]): +class ProtectSetableKeysMixin(ProtectEntityDescription[T]): """Mixin for settable values.""" ufp_set_method: str | None = None diff --git a/homeassistant/components/unifiprotect/number.py b/homeassistant/components/unifiprotect/number.py index 44f965e4796..3719dcbd4ac 100644 --- a/homeassistant/components/unifiprotect/number.py +++ b/homeassistant/components/unifiprotect/number.py @@ -5,7 +5,6 @@ from __future__ import annotations from collections.abc import Sequence from dataclasses import dataclass from datetime import timedelta -import logging from uiprotect.data import ( Camera, @@ -23,9 +22,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .data import ProtectData, UFPConfigEntry from .entity import ProtectDeviceEntity, async_all_device_entities -from .models import PermRequired, ProtectRequiredKeysMixin, ProtectSetableKeysMixin, T - -_LOGGER = logging.getLogger(__name__) +from .models import PermRequired, ProtectEntityDescription, ProtectSetableKeysMixin, T @dataclass(frozen=True, kw_only=True) @@ -213,7 +210,7 @@ CHIME_NUMBERS: tuple[ProtectNumberEntityDescription, ...] = ( ufp_perm=PermRequired.WRITE, ), ) -_MODEL_DESCRIPTIONS: dict[ModelType, Sequence[ProtectRequiredKeysMixin]] = { +_MODEL_DESCRIPTIONS: dict[ModelType, Sequence[ProtectEntityDescription]] = { ModelType.CAMERA: CAMERA_NUMBERS, ModelType.LIGHT: LIGHT_NUMBERS, ModelType.SENSOR: SENSE_NUMBERS, diff --git a/homeassistant/components/unifiprotect/select.py b/homeassistant/components/unifiprotect/select.py index 2dd52fac774..d6e0f638d2d 100644 --- a/homeassistant/components/unifiprotect/select.py +++ b/homeassistant/components/unifiprotect/select.py @@ -35,7 +35,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import TYPE_EMPTY_VALUE from .data import ProtectData, UFPConfigEntry from .entity import ProtectDeviceEntity, async_all_device_entities -from .models import PermRequired, ProtectRequiredKeysMixin, ProtectSetableKeysMixin, T +from .models import PermRequired, ProtectEntityDescription, ProtectSetableKeysMixin, T from .utils import async_get_light_motion_current _LOGGER = logging.getLogger(__name__) @@ -319,7 +319,7 @@ VIEWER_SELECTS: tuple[ProtectSelectEntityDescription, ...] = ( ), ) -_MODEL_DESCRIPTIONS: dict[ModelType, Sequence[ProtectRequiredKeysMixin]] = { +_MODEL_DESCRIPTIONS: dict[ModelType, Sequence[ProtectEntityDescription]] = { ModelType.CAMERA: CAMERA_SELECTS, ModelType.LIGHT: LIGHT_SELECTS, ModelType.SENSOR: SENSE_SELECTS, diff --git a/homeassistant/components/unifiprotect/sensor.py b/homeassistant/components/unifiprotect/sensor.py index 56b7ef7f9a4..3e849bc1ad1 100644 --- a/homeassistant/components/unifiprotect/sensor.py +++ b/homeassistant/components/unifiprotect/sensor.py @@ -47,7 +47,7 @@ from .entity import ( ProtectNVREntity, async_all_device_entities, ) -from .models import PermRequired, ProtectEventMixin, ProtectRequiredKeysMixin, T +from .models import PermRequired, ProtectEntityDescription, ProtectEventMixin, T from .utils import async_get_light_motion_current _LOGGER = logging.getLogger(__name__) @@ -56,7 +56,7 @@ OBJECT_TYPE_NONE = "none" @dataclass(frozen=True, kw_only=True) class ProtectSensorEntityDescription( - ProtectRequiredKeysMixin[T], SensorEntityDescription + ProtectEntityDescription[T], SensorEntityDescription ): """Describes UniFi Protect Sensor entity.""" @@ -608,7 +608,7 @@ VIEWER_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( ), ) -_MODEL_DESCRIPTIONS: dict[ModelType, Sequence[ProtectRequiredKeysMixin]] = { +_MODEL_DESCRIPTIONS: dict[ModelType, Sequence[ProtectEntityDescription]] = { ModelType.CAMERA: CAMERA_SENSORS + CAMERA_DISABLED_SENSORS, ModelType.SENSOR: SENSE_SENSORS, ModelType.LIGHT: LIGHT_SENSORS, diff --git a/homeassistant/components/unifiprotect/switch.py b/homeassistant/components/unifiprotect/switch.py index 36c2c497b57..32dc5808005 100644 --- a/homeassistant/components/unifiprotect/switch.py +++ b/homeassistant/components/unifiprotect/switch.py @@ -30,7 +30,7 @@ from .entity import ( ProtectNVREntity, async_all_device_entities, ) -from .models import PermRequired, ProtectRequiredKeysMixin, ProtectSetableKeysMixin, T +from .models import PermRequired, ProtectEntityDescription, ProtectSetableKeysMixin, T _LOGGER = logging.getLogger(__name__) ATTR_PREV_MIC = "prev_mic_level" @@ -459,7 +459,7 @@ NVR_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( ), ) -_MODEL_DESCRIPTIONS: dict[ModelType, Sequence[ProtectRequiredKeysMixin]] = { +_MODEL_DESCRIPTIONS: dict[ModelType, Sequence[ProtectEntityDescription]] = { ModelType.CAMERA: CAMERA_SWITCHES, ModelType.LIGHT: LIGHT_SWITCHES, ModelType.SENSOR: SENSE_SWITCHES, @@ -467,7 +467,7 @@ _MODEL_DESCRIPTIONS: dict[ModelType, Sequence[ProtectRequiredKeysMixin]] = { ModelType.VIEWPORT: VIEWER_SWITCHES, } -_PRIVACY_MODEL_DESCRIPTIONS: dict[ModelType, Sequence[ProtectRequiredKeysMixin]] = { +_PRIVACY_MODEL_DESCRIPTIONS: dict[ModelType, Sequence[ProtectEntityDescription]] = { ModelType.CAMERA: [PRIVACY_MODE_SWITCH] } @@ -487,7 +487,6 @@ class ProtectSwitch(ProtectDeviceEntity, SwitchEntity): """Initialize an UniFi Protect Switch.""" super().__init__(data, device, description) self._attr_name = f"{self.device.display_name} {self.entity_description.name}" - self._switch_type = self.entity_description.key def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None: super()._async_update_device_from_protect(device) @@ -539,21 +538,20 @@ class ProtectPrivacyModeSwitch(RestoreEntity, ProtectSwitch): def __init__( self, data: ProtectData, - device: ProtectAdoptableDeviceModel, + device: Camera, description: ProtectSwitchEntityDescription, ) -> None: """Initialize an UniFi Protect Switch.""" super().__init__(data, device, description) - - if self.device.is_privacy_on: + if device.is_privacy_on: extra_state = self.extra_state_attributes or {} self._previous_mic_level = extra_state.get(ATTR_PREV_MIC, 100) self._previous_record_mode = extra_state.get( ATTR_PREV_RECORD, RecordingMode.ALWAYS ) else: - self._previous_mic_level = self.device.mic_volume - self._previous_record_mode = self.device.recording_settings.mode + self._previous_mic_level = device.mic_volume + self._previous_record_mode = device.recording_settings.mode @callback def _update_previous_attr(self) -> None: diff --git a/homeassistant/components/unifiprotect/text.py b/homeassistant/components/unifiprotect/text.py index 009e013ee51..e01a6b31f11 100644 --- a/homeassistant/components/unifiprotect/text.py +++ b/homeassistant/components/unifiprotect/text.py @@ -18,9 +18,9 @@ from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .data import ProtectData, UFPConfigEntry +from .data import UFPConfigEntry from .entity import ProtectDeviceEntity, async_all_device_entities -from .models import PermRequired, ProtectRequiredKeysMixin, ProtectSetableKeysMixin, T +from .models import PermRequired, ProtectEntityDescription, ProtectSetableKeysMixin, T @dataclass(frozen=True, kw_only=True) @@ -50,7 +50,7 @@ CAMERA: tuple[ProtectTextEntityDescription, ...] = ( ), ) -_MODEL_DESCRIPTIONS: dict[ModelType, Sequence[ProtectRequiredKeysMixin]] = { +_MODEL_DESCRIPTIONS: dict[ModelType, Sequence[ProtectEntityDescription]] = { ModelType.CAMERA: CAMERA, } @@ -88,15 +88,6 @@ class ProtectDeviceText(ProtectDeviceEntity, TextEntity): entity_description: ProtectTextEntityDescription _state_attrs = ("_attr_available", "_attr_native_value") - def __init__( - self, - data: ProtectData, - device: ProtectAdoptableDeviceModel, - description: ProtectTextEntityDescription, - ) -> None: - """Initialize an UniFi Protect sensor.""" - super().__init__(data, device, description) - @callback def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None: super()._async_update_device_from_protect(device) From 3a672642ea004197b857f4bc73a6c058be63780b Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Sun, 16 Jun 2024 14:02:10 +0200 Subject: [PATCH 0719/1445] Reolink extend diagnostic data (#119745) * Add diagnostic info * fix * change order * update tests --- .../components/reolink/diagnostics.py | 4 + homeassistant/components/reolink/host.py | 16 +-- tests/components/reolink/conftest.py | 2 + .../reolink/snapshots/test_diagnostics.ambr | 108 ++++++++++++++++++ 4 files changed, 122 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/reolink/diagnostics.py b/homeassistant/components/reolink/diagnostics.py index 5c13bccf58d..b06ddcd458f 100644 --- a/homeassistant/components/reolink/diagnostics.py +++ b/homeassistant/components/reolink/diagnostics.py @@ -23,7 +23,9 @@ async def async_get_config_entry_diagnostics( for ch in api.channels: IPC_cam[ch] = {} IPC_cam[ch]["model"] = api.camera_model(ch) + IPC_cam[ch]["hardware version"] = api.camera_hardware_version(ch) IPC_cam[ch]["firmware version"] = api.camera_sw_version(ch) + IPC_cam[ch]["encoding main"] = await api.get_encoding(ch) return { "model": api.model, @@ -42,6 +44,8 @@ async def async_get_config_entry_diagnostics( "stream channels": api.stream_channels, "IPC cams": IPC_cam, "capabilities": api.capabilities, + "cmd list": host.update_cmd, + "firmware ch list": host.firmware_ch_list, "api versions": api.checked_api_versions, "abilities": api.abilities, } diff --git a/homeassistant/components/reolink/host.py b/homeassistant/components/reolink/host.py index 83f366005f9..8256ef7f017 100644 --- a/homeassistant/components/reolink/host.py +++ b/homeassistant/components/reolink/host.py @@ -74,7 +74,7 @@ class ReolinkHost: ) self.last_wake: float = 0 - self._update_cmd: defaultdict[str, defaultdict[int | None, int]] = defaultdict( + self.update_cmd: defaultdict[str, defaultdict[int | None, int]] = defaultdict( lambda: defaultdict(int) ) self.firmware_ch_list: list[int | None] = [] @@ -97,16 +97,16 @@ class ReolinkHost: @callback def async_register_update_cmd(self, cmd: str, channel: int | None = None) -> None: """Register the command to update the state.""" - self._update_cmd[cmd][channel] += 1 + self.update_cmd[cmd][channel] += 1 @callback def async_unregister_update_cmd(self, cmd: str, channel: int | None = None) -> None: """Unregister the command to update the state.""" - self._update_cmd[cmd][channel] -= 1 - if not self._update_cmd[cmd][channel]: - del self._update_cmd[cmd][channel] - if not self._update_cmd[cmd]: - del self._update_cmd[cmd] + self.update_cmd[cmd][channel] -= 1 + if not self.update_cmd[cmd][channel]: + del self.update_cmd[cmd][channel] + if not self.update_cmd[cmd]: + del self.update_cmd[cmd] @property def unique_id(self) -> str: @@ -350,7 +350,7 @@ class ReolinkHost: wake = True self.last_wake = time() - await self._api.get_states(cmd_list=self._update_cmd, wake=wake) + await self._api.get_states(cmd_list=self.update_cmd, wake=wake) async def disconnect(self) -> None: """Disconnect from the API, so the connection will be released.""" diff --git a/tests/components/reolink/conftest.py b/tests/components/reolink/conftest.py index 9b7dd481c9d..4fed102b320 100644 --- a/tests/components/reolink/conftest.py +++ b/tests/components/reolink/conftest.py @@ -84,8 +84,10 @@ def reolink_connect_class() -> Generator[MagicMock]: host_mock.model = TEST_HOST_MODEL host_mock.camera_model.return_value = TEST_CAM_MODEL host_mock.camera_name.return_value = TEST_NVR_NAME + host_mock.camera_hardware_version.return_value = "IPC_00001" host_mock.camera_sw_version.return_value = "v1.1.0.0.0.0000" host_mock.camera_uid.return_value = TEST_UID + host_mock.get_encoding.return_value = "h264" host_mock.firmware_update_available.return_value = False host_mock.session_active = True host_mock.timeout = 60 diff --git a/tests/components/reolink/snapshots/test_diagnostics.ambr b/tests/components/reolink/snapshots/test_diagnostics.ambr index 9f70673695c..00363023d14 100644 --- a/tests/components/reolink/snapshots/test_diagnostics.ambr +++ b/tests/components/reolink/snapshots/test_diagnostics.ambr @@ -5,7 +5,9 @@ 'HTTPS': True, 'IPC cams': dict({ '0': dict({ + 'encoding main': 'h264', 'firmware version': 'v1.1.0.0.0.0000', + 'hardware version': 'IPC_00001', 'model': 'RLC-123', }), }), @@ -38,7 +40,113 @@ 'channels': list([ 0, ]), + 'cmd list': dict({ + 'GetAiAlarm': dict({ + '0': 5, + 'null': 5, + }), + 'GetAiCfg': dict({ + '0': 4, + 'null': 4, + }), + 'GetAudioAlarm': dict({ + '0': 1, + 'null': 1, + }), + 'GetAudioCfg': dict({ + '0': 2, + 'null': 2, + }), + 'GetAutoFocus': dict({ + '0': 1, + 'null': 1, + }), + 'GetAutoReply': dict({ + '0': 2, + 'null': 2, + }), + 'GetBatteryInfo': dict({ + '0': 1, + 'null': 1, + }), + 'GetBuzzerAlarmV20': dict({ + '0': 1, + 'null': 2, + }), + 'GetChannelstatus': dict({ + '0': 1, + 'null': 1, + }), + 'GetEmail': dict({ + '0': 1, + 'null': 2, + }), + 'GetEnc': dict({ + '0': 1, + 'null': 1, + }), + 'GetFtp': dict({ + '0': 1, + 'null': 2, + }), + 'GetIrLights': dict({ + '0': 1, + 'null': 1, + }), + 'GetIsp': dict({ + '0': 1, + 'null': 1, + }), + 'GetManualRec': dict({ + '0': 1, + 'null': 1, + }), + 'GetMdAlarm': dict({ + '0': 1, + 'null': 1, + }), + 'GetPirInfo': dict({ + '0': 1, + 'null': 1, + }), + 'GetPowerLed': dict({ + '0': 2, + 'null': 2, + }), + 'GetPtzCurPos': dict({ + '0': 1, + 'null': 1, + }), + 'GetPtzGuard': dict({ + '0': 2, + 'null': 2, + }), + 'GetPtzTraceSection': dict({ + '0': 2, + 'null': 2, + }), + 'GetPush': dict({ + '0': 1, + 'null': 2, + }), + 'GetRec': dict({ + '0': 1, + 'null': 2, + }), + 'GetWhiteLed': dict({ + '0': 3, + 'null': 3, + }), + 'GetZoomFocus': dict({ + '0': 2, + 'null': 2, + }), + }), 'event connection': 'Fast polling', + 'firmware ch list': list([ + 0, + None, + ]), 'firmware version': 'v1.0.0.0.0.0000', 'hardware version': 'IPC_00000', 'model': 'RLN8-410', From b20160a46502f9990b8b738a8152d38472d69aba Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Sun, 16 Jun 2024 16:25:23 +0300 Subject: [PATCH 0720/1445] Cleanup Shelly entry unload (#119748) * Cleanup Shelly entry unload * store platforms on runtime_data --- homeassistant/components/shelly/__init__.py | 78 +++++++++---------- .../components/shelly/coordinator.py | 1 + 2 files changed, 36 insertions(+), 43 deletions(-) diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index aae0d560810..184b7c8bb6b 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -112,8 +112,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ShellyConfigEntry) -> bo ) return False - entry.runtime_data = ShellyEntryData() - if get_device_entry_gen(entry) in RPC_GENERATIONS: return await _async_setup_rpc_entry(hass, entry) @@ -150,7 +148,7 @@ async def _async_setup_block_entry( device_entry = None sleep_period = entry.data.get(CONF_SLEEP_PERIOD) - shelly_entry_data = entry.runtime_data + runtime_data = entry.runtime_data = ShellyEntryData(BLOCK_SLEEPING_PLATFORMS) # Some old firmware have a wrong sleep period hardcoded value. # Following code block will force the right value for affected devices @@ -171,6 +169,7 @@ async def _async_setup_block_entry( if sleep_period == 0: # Not a sleeping device, finish setup LOGGER.debug("Setting up online block device %s", entry.title) + runtime_data.platforms = PLATFORMS try: await device.initialize() if not device.firmware_supported: @@ -181,24 +180,26 @@ async def _async_setup_block_entry( except InvalidAuthError as err: raise ConfigEntryAuthFailed(repr(err)) from err - shelly_entry_data.block = ShellyBlockCoordinator(hass, entry, device) - shelly_entry_data.block.async_setup() - shelly_entry_data.rest = ShellyRestCoordinator(hass, device, entry) - await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + runtime_data.block = ShellyBlockCoordinator(hass, entry, device) + runtime_data.block.async_setup() + runtime_data.rest = ShellyRestCoordinator(hass, device, entry) + await hass.config_entries.async_forward_entry_setups( + entry, runtime_data.platforms + ) elif sleep_period is None or device_entry is None: # Need to get sleep info or first time sleeping device setup, wait for device LOGGER.debug( "Setup for device %s will resume when device is online", entry.title ) - shelly_entry_data.block = ShellyBlockCoordinator(hass, entry, device) - shelly_entry_data.block.async_setup(BLOCK_SLEEPING_PLATFORMS) + runtime_data.block = ShellyBlockCoordinator(hass, entry, device) + runtime_data.block.async_setup(runtime_data.platforms) else: # Restore sensors for sleeping device LOGGER.debug("Setting up offline block device %s", entry.title) - shelly_entry_data.block = ShellyBlockCoordinator(hass, entry, device) - shelly_entry_data.block.async_setup() + runtime_data.block = ShellyBlockCoordinator(hass, entry, device) + runtime_data.block.async_setup() await hass.config_entries.async_forward_entry_setups( - entry, BLOCK_SLEEPING_PLATFORMS + entry, runtime_data.platforms ) ir.async_delete_issue( @@ -236,11 +237,12 @@ async def _async_setup_rpc_entry(hass: HomeAssistant, entry: ShellyConfigEntry) device_entry = None sleep_period = entry.data.get(CONF_SLEEP_PERIOD) - shelly_entry_data = entry.runtime_data + runtime_data = entry.runtime_data = ShellyEntryData(RPC_SLEEPING_PLATFORMS) if sleep_period == 0: # Not a sleeping device, finish setup LOGGER.debug("Setting up online RPC device %s", entry.title) + runtime_data.platforms = PLATFORMS try: await device.initialize() if not device.firmware_supported: @@ -251,24 +253,26 @@ async def _async_setup_rpc_entry(hass: HomeAssistant, entry: ShellyConfigEntry) except InvalidAuthError as err: raise ConfigEntryAuthFailed(repr(err)) from err - shelly_entry_data.rpc = ShellyRpcCoordinator(hass, entry, device) - shelly_entry_data.rpc.async_setup() - shelly_entry_data.rpc_poll = ShellyRpcPollingCoordinator(hass, entry, device) - await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + runtime_data.rpc = ShellyRpcCoordinator(hass, entry, device) + runtime_data.rpc.async_setup() + runtime_data.rpc_poll = ShellyRpcPollingCoordinator(hass, entry, device) + await hass.config_entries.async_forward_entry_setups( + entry, runtime_data.platforms + ) elif sleep_period is None or device_entry is None: # Need to get sleep info or first time sleeping device setup, wait for device LOGGER.debug( "Setup for device %s will resume when device is online", entry.title ) - shelly_entry_data.rpc = ShellyRpcCoordinator(hass, entry, device) - shelly_entry_data.rpc.async_setup(RPC_SLEEPING_PLATFORMS) + runtime_data.rpc = ShellyRpcCoordinator(hass, entry, device) + runtime_data.rpc.async_setup(runtime_data.platforms) else: # Restore sensors for sleeping device LOGGER.debug("Setting up offline RPC device %s", entry.title) - shelly_entry_data.rpc = ShellyRpcCoordinator(hass, entry, device) - shelly_entry_data.rpc.async_setup() + runtime_data.rpc = ShellyRpcCoordinator(hass, entry, device) + runtime_data.rpc.async_setup() await hass.config_entries.async_forward_entry_setups( - entry, RPC_SLEEPING_PLATFORMS + entry, runtime_data.platforms ) ir.async_delete_issue( @@ -279,21 +283,6 @@ async def _async_setup_rpc_entry(hass: HomeAssistant, entry: ShellyConfigEntry) async def async_unload_entry(hass: HomeAssistant, entry: ShellyConfigEntry) -> bool: """Unload a config entry.""" - shelly_entry_data = entry.runtime_data - platforms = PLATFORMS - - if get_device_entry_gen(entry) in RPC_GENERATIONS: - if entry.data.get(CONF_SLEEP_PERIOD): - platforms = RPC_SLEEPING_PLATFORMS - - if unload_ok := await hass.config_entries.async_unload_platforms( - entry, platforms - ): - if shelly_entry_data.rpc: - await shelly_entry_data.rpc.shutdown() - - return unload_ok - # delete push update issue if it exists LOGGER.debug( "Deleting issue %s", PUSH_UPDATE_ISSUE_ID.format(unique=entry.unique_id) @@ -302,11 +291,14 @@ async def async_unload_entry(hass: HomeAssistant, entry: ShellyConfigEntry) -> b hass, DOMAIN, PUSH_UPDATE_ISSUE_ID.format(unique=entry.unique_id) ) - if entry.data.get(CONF_SLEEP_PERIOD): - platforms = BLOCK_SLEEPING_PLATFORMS + runtime_data = entry.runtime_data - if unload_ok := await hass.config_entries.async_unload_platforms(entry, platforms): - if shelly_entry_data.block: - await shelly_entry_data.block.shutdown() + if runtime_data.rpc: + await runtime_data.rpc.shutdown() - return unload_ok + if runtime_data.block: + await runtime_data.block.shutdown() + + return await hass.config_entries.async_unload_platforms( + entry, runtime_data.platforms + ) diff --git a/homeassistant/components/shelly/coordinator.py b/homeassistant/components/shelly/coordinator.py index 5bb05d48d62..f15eca51413 100644 --- a/homeassistant/components/shelly/coordinator.py +++ b/homeassistant/components/shelly/coordinator.py @@ -72,6 +72,7 @@ from .utils import ( class ShellyEntryData: """Class for sharing data within a given config entry.""" + platforms: list[Platform] block: ShellyBlockCoordinator | None = None rest: ShellyRestCoordinator | None = None rpc: ShellyRpcCoordinator | None = None From 59ca5b04fa756952aed4a822605ec0435a1cb49c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 16 Jun 2024 09:00:14 -0500 Subject: [PATCH 0721/1445] Migrate unifiprotect to use has_entity_name (#119759) --- .../components/unifiprotect/binary_sensor.py | 108 ++++++++-------- .../components/unifiprotect/button.py | 24 ++-- .../components/unifiprotect/camera.py | 4 +- .../components/unifiprotect/entity.py | 18 +-- homeassistant/components/unifiprotect/lock.py | 20 +-- .../components/unifiprotect/media_player.py | 26 ++-- .../components/unifiprotect/number.py | 18 +-- .../components/unifiprotect/select.py | 21 ++-- .../components/unifiprotect/sensor.py | 90 ++++++------- .../components/unifiprotect/switch.py | 119 +++++++----------- .../components/unifiprotect/utils.py | 2 +- tests/components/unifiprotect/test_switch.py | 18 +-- 12 files changed, 198 insertions(+), 270 deletions(-) diff --git a/homeassistant/components/unifiprotect/binary_sensor.py b/homeassistant/components/unifiprotect/binary_sensor.py index 19ae4504109..e57826fd2f3 100644 --- a/homeassistant/components/unifiprotect/binary_sensor.py +++ b/homeassistant/components/unifiprotect/binary_sensor.py @@ -63,13 +63,13 @@ MOUNT_DEVICE_CLASS_MAP = { CAMERA_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( ProtectBinaryEntityDescription( key="dark", - name="Is Dark", + name="Is dark", icon="mdi:brightness-6", ufp_value="is_dark", ), ProtectBinaryEntityDescription( key="ssh", - name="SSH Enabled", + name="SSH enabled", icon="mdi:lock", entity_registry_enabled_default=False, entity_category=EntityCategory.DIAGNOSTIC, @@ -78,7 +78,7 @@ CAMERA_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( ), ProtectBinaryEntityDescription( key="status_light", - name="Status Light On", + name="Status light on", icon="mdi:led-on", entity_category=EntityCategory.DIAGNOSTIC, ufp_required_field="feature_flags.has_led_status", @@ -87,7 +87,7 @@ CAMERA_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( ), ProtectBinaryEntityDescription( key="hdr_mode", - name="HDR Mode", + name="HDR mode", icon="mdi:brightness-7", entity_category=EntityCategory.DIAGNOSTIC, ufp_required_field="feature_flags.has_hdr", @@ -105,7 +105,7 @@ CAMERA_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( ), ProtectBinaryEntityDescription( key="system_sounds", - name="System Sounds", + name="System sounds", icon="mdi:speaker", entity_category=EntityCategory.DIAGNOSTIC, ufp_required_field="has_speaker", @@ -115,7 +115,7 @@ CAMERA_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( ), ProtectBinaryEntityDescription( key="osd_name", - name="Overlay: Show Name", + name="Overlay: show name", icon="mdi:fullscreen", entity_category=EntityCategory.DIAGNOSTIC, ufp_value="osd_settings.is_name_enabled", @@ -123,7 +123,7 @@ CAMERA_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( ), ProtectBinaryEntityDescription( key="osd_date", - name="Overlay: Show Date", + name="Overlay: show date", icon="mdi:fullscreen", entity_category=EntityCategory.DIAGNOSTIC, ufp_value="osd_settings.is_date_enabled", @@ -131,7 +131,7 @@ CAMERA_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( ), ProtectBinaryEntityDescription( key="osd_logo", - name="Overlay: Show Logo", + name="Overlay: show logo", icon="mdi:fullscreen", entity_category=EntityCategory.DIAGNOSTIC, ufp_value="osd_settings.is_logo_enabled", @@ -139,7 +139,7 @@ CAMERA_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( ), ProtectBinaryEntityDescription( key="osd_bitrate", - name="Overlay: Show Bitrate", + name="Overlay: show bitrate", icon="mdi:fullscreen", entity_category=EntityCategory.DIAGNOSTIC, ufp_value="osd_settings.is_debug_enabled", @@ -147,14 +147,14 @@ CAMERA_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( ), ProtectBinaryEntityDescription( key="motion_enabled", - name="Detections: Motion", + name="Detections: motion", icon="mdi:run-fast", ufp_value="recording_settings.enable_motion_detection", ufp_perm=PermRequired.NO_WRITE, ), ProtectBinaryEntityDescription( key="smart_person", - name="Detections: Person", + name="Detections: person", icon="mdi:walk", entity_category=EntityCategory.DIAGNOSTIC, ufp_required_field="can_detect_person", @@ -163,7 +163,7 @@ CAMERA_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( ), ProtectBinaryEntityDescription( key="smart_vehicle", - name="Detections: Vehicle", + name="Detections: vehicle", icon="mdi:car", entity_category=EntityCategory.DIAGNOSTIC, ufp_required_field="can_detect_vehicle", @@ -172,7 +172,7 @@ CAMERA_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( ), ProtectBinaryEntityDescription( key="smart_animal", - name="Detections: Animal", + name="Detections: animal", icon="mdi:paw", entity_category=EntityCategory.DIAGNOSTIC, ufp_required_field="can_detect_animal", @@ -181,7 +181,7 @@ CAMERA_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( ), ProtectBinaryEntityDescription( key="smart_package", - name="Detections: Package", + name="Detections: package", icon="mdi:package-variant-closed", entity_category=EntityCategory.DIAGNOSTIC, ufp_required_field="can_detect_package", @@ -190,7 +190,7 @@ CAMERA_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( ), ProtectBinaryEntityDescription( key="smart_licenseplate", - name="Detections: License Plate", + name="Detections: license plate", icon="mdi:car", entity_category=EntityCategory.DIAGNOSTIC, ufp_required_field="can_detect_license_plate", @@ -199,7 +199,7 @@ CAMERA_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( ), ProtectBinaryEntityDescription( key="smart_smoke", - name="Detections: Smoke", + name="Detections: smoke", icon="mdi:fire", entity_category=EntityCategory.DIAGNOSTIC, ufp_required_field="can_detect_smoke", @@ -217,7 +217,7 @@ CAMERA_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( ), ProtectBinaryEntityDescription( key="smart_siren", - name="Detections: Siren", + name="Detections: siren", icon="mdi:alarm-bell", entity_category=EntityCategory.DIAGNOSTIC, ufp_required_field="can_detect_siren", @@ -226,7 +226,7 @@ CAMERA_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( ), ProtectBinaryEntityDescription( key="smart_baby_cry", - name="Detections: Baby Cry", + name="Detections: baby cry", icon="mdi:cradle", entity_category=EntityCategory.DIAGNOSTIC, ufp_required_field="can_detect_baby_cry", @@ -235,7 +235,7 @@ CAMERA_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( ), ProtectBinaryEntityDescription( key="smart_speak", - name="Detections: Speaking", + name="Detections: speaking", icon="mdi:account-voice", entity_category=EntityCategory.DIAGNOSTIC, ufp_required_field="can_detect_speaking", @@ -244,7 +244,7 @@ CAMERA_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( ), ProtectBinaryEntityDescription( key="smart_bark", - name="Detections: Barking", + name="Detections: barking", icon="mdi:dog", entity_category=EntityCategory.DIAGNOSTIC, ufp_required_field="can_detect_bark", @@ -253,7 +253,7 @@ CAMERA_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( ), ProtectBinaryEntityDescription( key="smart_car_alarm", - name="Detections: Car Alarm", + name="Detections: car alarm", icon="mdi:car", entity_category=EntityCategory.DIAGNOSTIC, ufp_required_field="can_detect_car_alarm", @@ -262,7 +262,7 @@ CAMERA_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( ), ProtectBinaryEntityDescription( key="smart_car_horn", - name="Detections: Car Horn", + name="Detections: car horn", icon="mdi:bugle", entity_category=EntityCategory.DIAGNOSTIC, ufp_required_field="can_detect_car_horn", @@ -271,7 +271,7 @@ CAMERA_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( ), ProtectBinaryEntityDescription( key="smart_glass_break", - name="Detections: Glass Break", + name="Detections: glass break", icon="mdi:glass-fragile", entity_category=EntityCategory.DIAGNOSTIC, ufp_required_field="can_detect_glass_break", @@ -280,7 +280,7 @@ CAMERA_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( ), ProtectBinaryEntityDescription( key="track_person", - name="Tracking: Person", + name="Tracking: person", icon="mdi:walk", entity_category=EntityCategory.DIAGNOSTIC, ufp_required_field="is_ptz", @@ -292,19 +292,19 @@ CAMERA_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( LIGHT_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( ProtectBinaryEntityDescription( key="dark", - name="Is Dark", + name="Is dark", icon="mdi:brightness-6", ufp_value="is_dark", ), ProtectBinaryEntityDescription( key="motion", - name="Motion Detected", + name="Motion detected", device_class=BinarySensorDeviceClass.MOTION, ufp_value="is_pir_motion_detected", ), ProtectBinaryEntityDescription( key="light", - name="Flood Light", + name="Flood light", icon="mdi:spotlight-beam", entity_category=EntityCategory.DIAGNOSTIC, ufp_value="is_light_on", @@ -312,7 +312,7 @@ LIGHT_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( ), ProtectBinaryEntityDescription( key="ssh", - name="SSH Enabled", + name="SSH enabled", icon="mdi:lock", entity_registry_enabled_default=False, entity_category=EntityCategory.DIAGNOSTIC, @@ -321,7 +321,7 @@ LIGHT_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( ), ProtectBinaryEntityDescription( key="status_light", - name="Status Light On", + name="Status light on", icon="mdi:led-on", entity_category=EntityCategory.DIAGNOSTIC, ufp_value="light_device_settings.is_indicator_enabled", @@ -358,20 +358,20 @@ SENSE_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( ), ProtectBinaryEntityDescription( key="motion", - name="Motion Detected", + name="Motion detected", device_class=BinarySensorDeviceClass.MOTION, ufp_value="is_motion_detected", ufp_enabled="is_motion_sensor_enabled", ), ProtectBinaryEntityDescription( key="tampering", - name="Tampering Detected", + name="Tampering detected", device_class=BinarySensorDeviceClass.TAMPER, ufp_value="is_tampering_detected", ), ProtectBinaryEntityDescription( key="status_light", - name="Status Light On", + name="Status light on", icon="mdi:led-on", entity_category=EntityCategory.DIAGNOSTIC, ufp_value="led_settings.is_enabled", @@ -379,7 +379,7 @@ SENSE_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( ), ProtectBinaryEntityDescription( key="motion_enabled", - name="Motion Detection", + name="Motion detection", icon="mdi:walk", entity_category=EntityCategory.DIAGNOSTIC, ufp_value="motion_settings.is_enabled", @@ -387,7 +387,7 @@ SENSE_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( ), ProtectBinaryEntityDescription( key="temperature", - name="Temperature Sensor", + name="Temperature sensor", icon="mdi:thermometer", entity_category=EntityCategory.DIAGNOSTIC, ufp_value="temperature_settings.is_enabled", @@ -395,7 +395,7 @@ SENSE_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( ), ProtectBinaryEntityDescription( key="humidity", - name="Humidity Sensor", + name="Humidity sensor", icon="mdi:water-percent", entity_category=EntityCategory.DIAGNOSTIC, ufp_value="humidity_settings.is_enabled", @@ -403,7 +403,7 @@ SENSE_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( ), ProtectBinaryEntityDescription( key="light", - name="Light Sensor", + name="Light sensor", icon="mdi:brightness-5", entity_category=EntityCategory.DIAGNOSTIC, ufp_value="light_settings.is_enabled", @@ -411,7 +411,7 @@ SENSE_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( ), ProtectBinaryEntityDescription( key="alarm", - name="Alarm Sound Detection", + name="Alarm sound detection", entity_category=EntityCategory.DIAGNOSTIC, ufp_value="alarm_settings.is_enabled", ufp_perm=PermRequired.NO_WRITE, @@ -438,7 +438,7 @@ EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = ( ), ProtectBinaryEventEntityDescription( key="smart_obj_any", - name="Object Detected", + name="Object detected", icon="mdi:eye", ufp_value="is_smart_currently_detected", ufp_required_field="feature_flags.has_smart_detect", @@ -446,7 +446,7 @@ EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = ( ), ProtectBinaryEventEntityDescription( key="smart_obj_person", - name="Person Detected", + name="Person detected", icon="mdi:walk", ufp_value="is_person_currently_detected", ufp_required_field="can_detect_person", @@ -455,7 +455,7 @@ EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = ( ), ProtectBinaryEventEntityDescription( key="smart_obj_vehicle", - name="Vehicle Detected", + name="Vehicle detected", icon="mdi:car", ufp_value="is_vehicle_currently_detected", ufp_required_field="can_detect_vehicle", @@ -464,7 +464,7 @@ EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = ( ), ProtectBinaryEventEntityDescription( key="smart_obj_animal", - name="Animal Detected", + name="Animal detected", icon="mdi:paw", ufp_value="is_animal_currently_detected", ufp_required_field="can_detect_animal", @@ -473,7 +473,7 @@ EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = ( ), ProtectBinaryEventEntityDescription( key="smart_obj_package", - name="Package Detected", + name="Package detected", icon="mdi:package-variant-closed", ufp_value="is_package_currently_detected", entity_registry_enabled_default=False, @@ -483,7 +483,7 @@ EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = ( ), ProtectBinaryEventEntityDescription( key="smart_audio_any", - name="Audio Object Detected", + name="Audio object detected", icon="mdi:eye", ufp_value="is_audio_currently_detected", ufp_required_field="feature_flags.has_smart_detect", @@ -491,7 +491,7 @@ EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = ( ), ProtectBinaryEventEntityDescription( key="smart_audio_smoke", - name="Smoke Alarm Detected", + name="Smoke alarm detected", icon="mdi:fire", ufp_value="is_smoke_currently_detected", ufp_required_field="can_detect_smoke", @@ -500,7 +500,7 @@ EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = ( ), ProtectBinaryEventEntityDescription( key="smart_audio_cmonx", - name="CO Alarm Detected", + name="CO alarm detected", icon="mdi:molecule-co", ufp_value="is_cmonx_currently_detected", ufp_required_field="can_detect_co", @@ -509,7 +509,7 @@ EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = ( ), ProtectBinaryEventEntityDescription( key="smart_audio_siren", - name="Siren Detected", + name="Siren detected", icon="mdi:alarm-bell", ufp_value="is_siren_currently_detected", ufp_required_field="can_detect_siren", @@ -518,7 +518,7 @@ EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = ( ), ProtectBinaryEventEntityDescription( key="smart_audio_baby_cry", - name="Baby Cry Detected", + name="Baby cry detected", icon="mdi:cradle", ufp_value="is_baby_cry_currently_detected", ufp_required_field="can_detect_baby_cry", @@ -527,7 +527,7 @@ EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = ( ), ProtectBinaryEventEntityDescription( key="smart_audio_speak", - name="Speaking Detected", + name="Speaking detected", icon="mdi:account-voice", ufp_value="is_speaking_currently_detected", ufp_required_field="can_detect_speaking", @@ -536,7 +536,7 @@ EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = ( ), ProtectBinaryEventEntityDescription( key="smart_audio_bark", - name="Barking Detected", + name="Barking detected", icon="mdi:dog", ufp_value="is_bark_currently_detected", ufp_required_field="can_detect_bark", @@ -545,7 +545,7 @@ EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = ( ), ProtectBinaryEventEntityDescription( key="smart_audio_car_alarm", - name="Car Alarm Detected", + name="Car alarm detected", icon="mdi:car", ufp_value="is_car_alarm_currently_detected", ufp_required_field="can_detect_car_alarm", @@ -554,7 +554,7 @@ EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = ( ), ProtectBinaryEventEntityDescription( key="smart_audio_car_horn", - name="Car Horn Detected", + name="Car horn detected", icon="mdi:bugle", ufp_value="is_car_horn_currently_detected", ufp_required_field="can_detect_car_horn", @@ -563,7 +563,7 @@ EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = ( ), ProtectBinaryEventEntityDescription( key="smart_audio_glass_break", - name="Glass Break Detected", + name="Glass break detected", icon="mdi:glass-fragile", ufp_value="last_glass_break_detect", ufp_required_field="can_detect_glass_break", @@ -582,7 +582,7 @@ DOORLOCK_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( ), ProtectBinaryEntityDescription( key="status_light", - name="Status Light On", + name="Status light on", icon="mdi:led-on", entity_category=EntityCategory.DIAGNOSTIC, ufp_value="led_settings.is_enabled", @@ -593,7 +593,7 @@ DOORLOCK_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( VIEWER_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( ProtectBinaryEntityDescription( key="ssh", - name="SSH Enabled", + name="SSH enabled", icon="mdi:lock", entity_registry_enabled_default=False, entity_category=EntityCategory.DIAGNOSTIC, diff --git a/homeassistant/components/unifiprotect/button.py b/homeassistant/components/unifiprotect/button.py index 8a6c4b38ea5..f0824ad894c 100644 --- a/homeassistant/components/unifiprotect/button.py +++ b/homeassistant/components/unifiprotect/button.py @@ -21,7 +21,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DEVICES_THAT_ADOPT, DISPATCH_ADD, DOMAIN -from .data import ProtectData, UFPConfigEntry +from .data import UFPConfigEntry from .entity import ProtectDeviceEntity, async_all_device_entities from .models import PermRequired, ProtectEntityDescription, ProtectSetableKeysMixin, T from .utils import async_dispatch_id as _ufpd @@ -47,14 +47,14 @@ ALL_DEVICE_BUTTONS: tuple[ProtectButtonEntityDescription, ...] = ( key="reboot", entity_registry_enabled_default=False, device_class=ButtonDeviceClass.RESTART, - name="Reboot Device", + name="Reboot device", ufp_press="reboot", ufp_perm=PermRequired.WRITE, ), ProtectButtonEntityDescription( key="unadopt", entity_registry_enabled_default=False, - name="Unadopt Device", + name="Unadopt device", icon="mdi:delete", ufp_press="unadopt", ufp_perm=PermRequired.DELETE, @@ -63,7 +63,7 @@ ALL_DEVICE_BUTTONS: tuple[ProtectButtonEntityDescription, ...] = ( ADOPT_BUTTON = ProtectButtonEntityDescription[ProtectAdoptableDeviceModel]( key=KEY_ADOPT, - name="Adopt Device", + name="Adopt device", icon="mdi:plus-circle", ufp_press="adopt", ) @@ -71,7 +71,7 @@ ADOPT_BUTTON = ProtectButtonEntityDescription[ProtectAdoptableDeviceModel]( SENSOR_BUTTONS: tuple[ProtectButtonEntityDescription, ...] = ( ProtectButtonEntityDescription( key="clear_tamper", - name="Clear Tamper", + name="Clear tamper", icon="mdi:notification-clear-all", ufp_press="clear_tamper", ufp_perm=PermRequired.WRITE, @@ -81,14 +81,14 @@ SENSOR_BUTTONS: tuple[ProtectButtonEntityDescription, ...] = ( CHIME_BUTTONS: tuple[ProtectButtonEntityDescription, ...] = ( ProtectButtonEntityDescription( key="play", - name="Play Chime", + name="Play chime", device_class=DEVICE_CLASS_CHIME_BUTTON, icon="mdi:play", ufp_press="play", ), ProtectButtonEntityDescription( key="play_buzzer", - name="Play Buzzer", + name="Play buzzer", icon="mdi:play", ufp_press="play_buzzer", ), @@ -173,16 +173,6 @@ class ProtectButton(ProtectDeviceEntity, ButtonEntity): entity_description: ProtectButtonEntityDescription - def __init__( - self, - data: ProtectData, - device: ProtectAdoptableDeviceModel, - description: ProtectButtonEntityDescription, - ) -> None: - """Initialize an UniFi camera.""" - super().__init__(data, device, description) - self._attr_name = f"{self.device.display_name} {self.entity_description.name}" - @callback def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None: super()._async_update_device_from_protect(device) diff --git a/homeassistant/components/unifiprotect/camera.py b/homeassistant/components/unifiprotect/camera.py index a08e0f03e65..67533472ea7 100644 --- a/homeassistant/components/unifiprotect/camera.py +++ b/homeassistant/components/unifiprotect/camera.py @@ -191,10 +191,10 @@ class ProtectCamera(ProtectDeviceEntity, Camera): camera_name = get_camera_base_name(channel) if self._secure: self._attr_unique_id = f"{device.mac}_{channel.id}" - self._attr_name = f"{device.display_name} {camera_name}" + self._attr_name = camera_name else: self._attr_unique_id = f"{device.mac}_{channel.id}_insecure" - self._attr_name = f"{device.display_name} {camera_name} (Insecure)" + self._attr_name = f"{camera_name} (insecure)" # only the default (first) channel is enabled by default self._attr_entity_registry_enabled_default = is_default and secure diff --git a/homeassistant/components/unifiprotect/entity.py b/homeassistant/components/unifiprotect/entity.py index 1a89b7c06d1..a4179e023b3 100644 --- a/homeassistant/components/unifiprotect/entity.py +++ b/homeassistant/components/unifiprotect/entity.py @@ -21,7 +21,6 @@ from homeassistant.core import callback import homeassistant.helpers.device_registry as dr from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity import Entity, EntityDescription -from homeassistant.helpers.typing import UNDEFINED from .const import ( ATTR_EVENT_ID, @@ -165,6 +164,8 @@ class BaseProtectEntity(Entity): _attr_should_poll = False _attr_attribution = DEFAULT_ATTRIBUTION _state_attrs: tuple[str, ...] = ("_attr_available",) + _attr_has_entity_name = True + _async_get_ufp_enabled: Callable[[ProtectAdoptableDeviceModel], bool] | None = None def __init__( self, @@ -174,24 +175,15 @@ class BaseProtectEntity(Entity): ) -> None: """Initialize the entity.""" super().__init__() - self.data: ProtectData = data + self.data = data self.device = device - self._async_get_ufp_enabled: ( - Callable[[ProtectAdoptableDeviceModel], bool] | None - ) = None if description is None: - self._attr_unique_id = f"{self.device.mac}" - self._attr_name = f"{self.device.display_name}" + self._attr_unique_id = self.device.mac + self._attr_name = None else: self.entity_description = description self._attr_unique_id = f"{self.device.mac}_{description.key}" - name = ( - description.name - if description.name and description.name is not UNDEFINED - else "" - ) - self._attr_name = f"{self.device.display_name} {name.title()}" if isinstance(description, ProtectEntityDescription): self._async_get_ufp_enabled = description.get_ufp_enabled diff --git a/homeassistant/components/unifiprotect/lock.py b/homeassistant/components/unifiprotect/lock.py index 52de63cd833..b649813135b 100644 --- a/homeassistant/components/unifiprotect/lock.py +++ b/homeassistant/components/unifiprotect/lock.py @@ -17,7 +17,7 @@ from homeassistant.components.lock import LockEntity, LockEntityDescription from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .data import ProtectData, UFPConfigEntry +from .data import UFPConfigEntry from .entity import ProtectDeviceEntity _LOGGER = logging.getLogger(__name__) @@ -39,7 +39,9 @@ async def async_setup_entry( data.async_subscribe_adopt(_add_new_device) async_add_entities( - ProtectLock(data, cast(Doorlock, device)) + ProtectLock( + data, cast(Doorlock, device), LockEntityDescription(key="lock", name="Lock") + ) for device in data.get_by_types({ModelType.DOORLOCK}) ) @@ -57,20 +59,6 @@ class ProtectLock(ProtectDeviceEntity, LockEntity): "_attr_is_jammed", ) - def __init__( - self, - data: ProtectData, - doorlock: Doorlock, - ) -> None: - """Initialize an UniFi lock.""" - super().__init__( - data, - doorlock, - LockEntityDescription(key="lock"), - ) - - self._attr_name = f"{self.device.display_name} Lock" - @callback def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None: super()._async_update_device_from_protect(device) diff --git a/homeassistant/components/unifiprotect/media_player.py b/homeassistant/components/unifiprotect/media_player.py index dbf5321b3d8..d9b2dad7220 100644 --- a/homeassistant/components/unifiprotect/media_player.py +++ b/homeassistant/components/unifiprotect/media_player.py @@ -28,11 +28,15 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .data import ProtectData, UFPConfigEntry +from .data import UFPConfigEntry from .entity import ProtectDeviceEntity _LOGGER = logging.getLogger(__name__) +_SPEAKER_DESCRIPTION = MediaPlayerEntityDescription( + key="speaker", name="Speaker", device_class=MediaPlayerDeviceClass.SPEAKER +) + async def async_setup_entry( hass: HomeAssistant, @@ -51,7 +55,7 @@ async def async_setup_entry( data.async_subscribe_adopt(_add_new_device) async_add_entities( - ProtectMediaPlayer(data, device) + ProtectMediaPlayer(data, device, _SPEAKER_DESCRIPTION) for device in data.get_cameras() if device.has_speaker or device.has_removable_speaker ) @@ -69,25 +73,9 @@ class ProtectMediaPlayer(ProtectDeviceEntity, MediaPlayerEntity): | MediaPlayerEntityFeature.STOP | MediaPlayerEntityFeature.BROWSE_MEDIA ) + _attr_media_content_type = MediaType.MUSIC _state_attrs = ("_attr_available", "_attr_state", "_attr_volume_level") - def __init__( - self, - data: ProtectData, - camera: Camera, - ) -> None: - """Initialize an UniFi speaker.""" - super().__init__( - data, - camera, - MediaPlayerEntityDescription( - key="speaker", device_class=MediaPlayerDeviceClass.SPEAKER - ), - ) - - self._attr_name = f"{self.device.display_name} Speaker" - self._attr_media_content_type = MediaType.MUSIC - @callback def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None: super()._async_update_device_from_protect(device) diff --git a/homeassistant/components/unifiprotect/number.py b/homeassistant/components/unifiprotect/number.py index 3719dcbd4ac..a0d360af80b 100644 --- a/homeassistant/components/unifiprotect/number.py +++ b/homeassistant/components/unifiprotect/number.py @@ -59,7 +59,7 @@ def _get_chime_duration(obj: Camera) -> int: CAMERA_NUMBERS: tuple[ProtectNumberEntityDescription, ...] = ( ProtectNumberEntityDescription( key="wdr_value", - name="Wide Dynamic Range", + name="Wide dynamic range", icon="mdi:state-machine", entity_category=EntityCategory.CONFIG, ufp_min=0, @@ -72,7 +72,7 @@ CAMERA_NUMBERS: tuple[ProtectNumberEntityDescription, ...] = ( ), ProtectNumberEntityDescription( key="mic_level", - name="Microphone Level", + name="Microphone level", icon="mdi:microphone", entity_category=EntityCategory.CONFIG, native_unit_of_measurement=PERCENTAGE, @@ -87,7 +87,7 @@ CAMERA_NUMBERS: tuple[ProtectNumberEntityDescription, ...] = ( ), ProtectNumberEntityDescription( key="zoom_position", - name="Zoom Level", + name="Zoom level", icon="mdi:magnify-plus-outline", entity_category=EntityCategory.CONFIG, native_unit_of_measurement=PERCENTAGE, @@ -101,7 +101,7 @@ CAMERA_NUMBERS: tuple[ProtectNumberEntityDescription, ...] = ( ), ProtectNumberEntityDescription( key="chime_duration", - name="Chime Duration", + name="Chime duration", icon="mdi:bell", entity_category=EntityCategory.CONFIG, native_unit_of_measurement=UnitOfTime.SECONDS, @@ -116,7 +116,7 @@ CAMERA_NUMBERS: tuple[ProtectNumberEntityDescription, ...] = ( ), ProtectNumberEntityDescription( key="icr_lux", - name="Infrared Custom Lux Trigger", + name="Infrared custom lux trigger", icon="mdi:white-balance-sunny", entity_category=EntityCategory.CONFIG, ufp_min=1, @@ -133,7 +133,7 @@ CAMERA_NUMBERS: tuple[ProtectNumberEntityDescription, ...] = ( LIGHT_NUMBERS: tuple[ProtectNumberEntityDescription, ...] = ( ProtectNumberEntityDescription( key="sensitivity", - name="Motion Sensitivity", + name="Motion sensitivity", icon="mdi:walk", entity_category=EntityCategory.CONFIG, native_unit_of_measurement=PERCENTAGE, @@ -147,7 +147,7 @@ LIGHT_NUMBERS: tuple[ProtectNumberEntityDescription, ...] = ( ), ProtectNumberEntityDescription[Light]( key="duration", - name="Auto-shutoff Duration", + name="Auto-shutoff duration", icon="mdi:camera-timer", entity_category=EntityCategory.CONFIG, native_unit_of_measurement=UnitOfTime.SECONDS, @@ -164,7 +164,7 @@ LIGHT_NUMBERS: tuple[ProtectNumberEntityDescription, ...] = ( SENSE_NUMBERS: tuple[ProtectNumberEntityDescription, ...] = ( ProtectNumberEntityDescription( key="sensitivity", - name="Motion Sensitivity", + name="Motion sensitivity", icon="mdi:walk", entity_category=EntityCategory.CONFIG, native_unit_of_measurement=PERCENTAGE, @@ -181,7 +181,7 @@ SENSE_NUMBERS: tuple[ProtectNumberEntityDescription, ...] = ( DOORLOCK_NUMBERS: tuple[ProtectNumberEntityDescription, ...] = ( ProtectNumberEntityDescription[Doorlock]( key="auto_lock_time", - name="Auto-lock Timeout", + name="Auto-lock timeout", icon="mdi:walk", entity_category=EntityCategory.CONFIG, native_unit_of_measurement=UnitOfTime.SECONDS, diff --git a/homeassistant/components/unifiprotect/select.py b/homeassistant/components/unifiprotect/select.py index d6e0f638d2d..9e742caa9ce 100644 --- a/homeassistant/components/unifiprotect/select.py +++ b/homeassistant/components/unifiprotect/select.py @@ -188,7 +188,7 @@ async def _set_liveview(obj: Viewer, liveview_id: str) -> None: CAMERA_SELECTS: tuple[ProtectSelectEntityDescription, ...] = ( ProtectSelectEntityDescription( key="recording_mode", - name="Recording Mode", + name="Recording mode", icon="mdi:video-outline", entity_category=EntityCategory.CONFIG, ufp_options=DEVICE_RECORDING_MODES, @@ -199,7 +199,7 @@ CAMERA_SELECTS: tuple[ProtectSelectEntityDescription, ...] = ( ), ProtectSelectEntityDescription( key="infrared", - name="Infrared Mode", + name="Infrared mode", icon="mdi:circle-opacity", entity_category=EntityCategory.CONFIG, ufp_required_field="feature_flags.has_led_ir", @@ -211,7 +211,7 @@ CAMERA_SELECTS: tuple[ProtectSelectEntityDescription, ...] = ( ), ProtectSelectEntityDescription[Camera]( key="doorbell_text", - name="Doorbell Text", + name="Doorbell text", icon="mdi:card-text", entity_category=EntityCategory.CONFIG, device_class=DEVICE_CLASS_LCD_MESSAGE, @@ -223,7 +223,7 @@ CAMERA_SELECTS: tuple[ProtectSelectEntityDescription, ...] = ( ), ProtectSelectEntityDescription( key="chime_type", - name="Chime Type", + name="Chime type", icon="mdi:bell", entity_category=EntityCategory.CONFIG, ufp_required_field="feature_flags.has_chime", @@ -235,7 +235,7 @@ CAMERA_SELECTS: tuple[ProtectSelectEntityDescription, ...] = ( ), ProtectSelectEntityDescription( key="hdr_mode", - name="HDR Mode", + name="HDR mode", icon="mdi:brightness-7", entity_category=EntityCategory.CONFIG, ufp_required_field="feature_flags.has_hdr", @@ -249,7 +249,7 @@ CAMERA_SELECTS: tuple[ProtectSelectEntityDescription, ...] = ( LIGHT_SELECTS: tuple[ProtectSelectEntityDescription, ...] = ( ProtectSelectEntityDescription[Light]( key=_KEY_LIGHT_MOTION, - name="Light Mode", + name="Light mode", icon="mdi:spotlight", entity_category=EntityCategory.CONFIG, ufp_options=MOTION_MODE_TO_LIGHT_MODE, @@ -259,7 +259,7 @@ LIGHT_SELECTS: tuple[ProtectSelectEntityDescription, ...] = ( ), ProtectSelectEntityDescription[Light]( key="paired_camera", - name="Paired Camera", + name="Paired camera", icon="mdi:cctv", entity_category=EntityCategory.CONFIG, ufp_value="camera_id", @@ -272,7 +272,7 @@ LIGHT_SELECTS: tuple[ProtectSelectEntityDescription, ...] = ( SENSE_SELECTS: tuple[ProtectSelectEntityDescription, ...] = ( ProtectSelectEntityDescription( key="mount_type", - name="Mount Type", + name="Mount type", icon="mdi:screwdriver", entity_category=EntityCategory.CONFIG, ufp_options=MOUNT_TYPES, @@ -283,7 +283,7 @@ SENSE_SELECTS: tuple[ProtectSelectEntityDescription, ...] = ( ), ProtectSelectEntityDescription[Sensor]( key="paired_camera", - name="Paired Camera", + name="Paired camera", icon="mdi:cctv", entity_category=EntityCategory.CONFIG, ufp_value="camera_id", @@ -296,7 +296,7 @@ SENSE_SELECTS: tuple[ProtectSelectEntityDescription, ...] = ( DOORLOCK_SELECTS: tuple[ProtectSelectEntityDescription, ...] = ( ProtectSelectEntityDescription[Doorlock]( key="paired_camera", - name="Paired Camera", + name="Paired camera", icon="mdi:cctv", entity_category=EntityCategory.CONFIG, ufp_value="camera_id", @@ -369,7 +369,6 @@ class ProtectSelects(ProtectDeviceEntity, SelectEntity): """Initialize the unifi protect select entity.""" self._async_set_options(data, description) super().__init__(data, device, description) - self._attr_name = f"{self.device.display_name} {self.entity_description.name}" @callback def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None: diff --git a/homeassistant/components/unifiprotect/sensor.py b/homeassistant/components/unifiprotect/sensor.py index 3e849bc1ad1..0fcd4f5853d 100644 --- a/homeassistant/components/unifiprotect/sensor.py +++ b/homeassistant/components/unifiprotect/sensor.py @@ -123,7 +123,7 @@ ALL_DEVICES_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( ), ProtectSensorEntityDescription( key="ble_signal", - name="Bluetooth Signal Strength", + name="Bluetooth signal strength", native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, device_class=SensorDeviceClass.SIGNAL_STRENGTH, entity_category=EntityCategory.DIAGNOSTIC, @@ -134,7 +134,7 @@ ALL_DEVICES_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( ), ProtectSensorEntityDescription( key="phy_rate", - name="Link Speed", + name="Link speed", device_class=SensorDeviceClass.DATA_RATE, native_unit_of_measurement=UnitOfDataRate.MEGABITS_PER_SECOND, entity_category=EntityCategory.DIAGNOSTIC, @@ -145,7 +145,7 @@ ALL_DEVICES_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( ), ProtectSensorEntityDescription( key="wifi_signal", - name="WiFi Signal Strength", + name="WiFi signal strength", native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, device_class=SensorDeviceClass.SIGNAL_STRENGTH, entity_registry_enabled_default=False, @@ -159,7 +159,7 @@ ALL_DEVICES_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( CAMERA_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( ProtectSensorEntityDescription( key="oldest_recording", - name="Oldest Recording", + name="Oldest recording", device_class=SensorDeviceClass.TIMESTAMP, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, @@ -167,7 +167,7 @@ CAMERA_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( ), ProtectSensorEntityDescription( key="storage_used", - name="Storage Used", + name="Storage used", native_unit_of_measurement=UnitOfInformation.BYTES, device_class=SensorDeviceClass.DATA_SIZE, entity_category=EntityCategory.DIAGNOSTIC, @@ -176,7 +176,7 @@ CAMERA_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( ), ProtectSensorEntityDescription( key="write_rate", - name="Disk Write Rate", + name="Disk write rate", device_class=SensorDeviceClass.DATA_RATE, native_unit_of_measurement=UnitOfDataRate.BYTES_PER_SECOND, entity_category=EntityCategory.DIAGNOSTIC, @@ -199,7 +199,7 @@ CAMERA_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( ), ProtectSensorEntityDescription( key="doorbell_last_trip_time", - name="Last Doorbell Ring", + name="Last doorbell ring", device_class=SensorDeviceClass.TIMESTAMP, icon="mdi:doorbell-video", ufp_required_field="feature_flags.is_doorbell", @@ -208,7 +208,7 @@ CAMERA_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( ), ProtectSensorEntityDescription( key="lens_type", - name="Lens Type", + name="Lens type", entity_category=EntityCategory.DIAGNOSTIC, icon="mdi:camera-iris", ufp_required_field="has_removable_lens", @@ -216,7 +216,7 @@ CAMERA_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( ), ProtectSensorEntityDescription( key="mic_level", - name="Microphone Level", + name="Microphone level", icon="mdi:microphone", native_unit_of_measurement=PERCENTAGE, entity_category=EntityCategory.DIAGNOSTIC, @@ -227,7 +227,7 @@ CAMERA_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( ), ProtectSensorEntityDescription( key="recording_mode", - name="Recording Mode", + name="Recording mode", icon="mdi:video-outline", entity_category=EntityCategory.DIAGNOSTIC, ufp_value="recording_settings.mode", @@ -235,7 +235,7 @@ CAMERA_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( ), ProtectSensorEntityDescription( key="infrared", - name="Infrared Mode", + name="Infrared mode", icon="mdi:circle-opacity", entity_category=EntityCategory.DIAGNOSTIC, ufp_required_field="feature_flags.has_led_ir", @@ -244,7 +244,7 @@ CAMERA_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( ), ProtectSensorEntityDescription( key="doorbell_text", - name="Doorbell Text", + name="Doorbell text", icon="mdi:card-text", entity_category=EntityCategory.DIAGNOSTIC, ufp_required_field="feature_flags.has_lcd_screen", @@ -253,7 +253,7 @@ CAMERA_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( ), ProtectSensorEntityDescription( key="chime_type", - name="Chime Type", + name="Chime type", icon="mdi:bell", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, @@ -265,7 +265,7 @@ CAMERA_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( CAMERA_DISABLED_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( ProtectSensorEntityDescription( key="stats_rx", - name="Received Data", + name="Received data", native_unit_of_measurement=UnitOfInformation.BYTES, device_class=SensorDeviceClass.DATA_SIZE, entity_registry_enabled_default=False, @@ -275,7 +275,7 @@ CAMERA_DISABLED_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( ), ProtectSensorEntityDescription( key="stats_tx", - name="Transferred Data", + name="Transferred data", native_unit_of_measurement=UnitOfInformation.BYTES, device_class=SensorDeviceClass.DATA_SIZE, entity_registry_enabled_default=False, @@ -288,7 +288,7 @@ CAMERA_DISABLED_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( SENSE_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( ProtectSensorEntityDescription( key="battery_level", - name="Battery Level", + name="Battery level", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, @@ -297,7 +297,7 @@ SENSE_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( ), ProtectSensorEntityDescription( key="light_level", - name="Light Level", + name="Light level", native_unit_of_measurement=LIGHT_LUX, device_class=SensorDeviceClass.ILLUMINANCE, state_class=SensorStateClass.MEASUREMENT, @@ -306,7 +306,7 @@ SENSE_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( ), ProtectSensorEntityDescription( key="humidity_level", - name="Humidity Level", + name="Humidity level", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, state_class=SensorStateClass.MEASUREMENT, @@ -324,34 +324,34 @@ SENSE_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( ), ProtectSensorEntityDescription[Sensor]( key="alarm_sound", - name="Alarm Sound Detected", + name="Alarm sound detected", ufp_value_fn=_get_alarm_sound, ufp_enabled="is_alarm_sensor_enabled", ), ProtectSensorEntityDescription( key="door_last_trip_time", - name="Last Open", + name="Last open", device_class=SensorDeviceClass.TIMESTAMP, ufp_value="open_status_changed_at", entity_registry_enabled_default=False, ), ProtectSensorEntityDescription( key="motion_last_trip_time", - name="Last Motion Detected", + name="Last motion detected", device_class=SensorDeviceClass.TIMESTAMP, ufp_value="motion_detected_at", entity_registry_enabled_default=False, ), ProtectSensorEntityDescription( key="tampering_last_trip_time", - name="Last Tampering Detected", + name="Last tampering detected", device_class=SensorDeviceClass.TIMESTAMP, ufp_value="tampering_detected_at", entity_registry_enabled_default=False, ), ProtectSensorEntityDescription( key="sensitivity", - name="Motion Sensitivity", + name="Motion sensitivity", icon="mdi:walk", native_unit_of_measurement=PERCENTAGE, entity_category=EntityCategory.DIAGNOSTIC, @@ -360,7 +360,7 @@ SENSE_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( ), ProtectSensorEntityDescription( key="mount_type", - name="Mount Type", + name="Mount type", icon="mdi:screwdriver", entity_category=EntityCategory.DIAGNOSTIC, ufp_value="mount_type", @@ -368,7 +368,7 @@ SENSE_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( ), ProtectSensorEntityDescription( key="paired_camera", - name="Paired Camera", + name="Paired camera", icon="mdi:cctv", entity_category=EntityCategory.DIAGNOSTIC, ufp_value="camera.display_name", @@ -379,7 +379,7 @@ SENSE_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( DOORLOCK_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( ProtectSensorEntityDescription( key="battery_level", - name="Battery Level", + name="Battery level", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, @@ -388,7 +388,7 @@ DOORLOCK_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( ), ProtectSensorEntityDescription( key="paired_camera", - name="Paired Camera", + name="Paired camera", icon="mdi:cctv", entity_category=EntityCategory.DIAGNOSTIC, ufp_value="camera.display_name", @@ -407,7 +407,7 @@ NVR_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( ), ProtectSensorEntityDescription( key="storage_utilization", - name="Storage Utilization", + name="Storage utilization", native_unit_of_measurement=PERCENTAGE, icon="mdi:harddisk", entity_category=EntityCategory.DIAGNOSTIC, @@ -417,7 +417,7 @@ NVR_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( ), ProtectSensorEntityDescription( key="record_rotating", - name="Type: Timelapse Video", + name="Type: timelapse video", native_unit_of_measurement=PERCENTAGE, icon="mdi:server", entity_category=EntityCategory.DIAGNOSTIC, @@ -427,7 +427,7 @@ NVR_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( ), ProtectSensorEntityDescription( key="record_timelapse", - name="Type: Continuous Video", + name="Type: continuous video", native_unit_of_measurement=PERCENTAGE, icon="mdi:server", entity_category=EntityCategory.DIAGNOSTIC, @@ -437,7 +437,7 @@ NVR_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( ), ProtectSensorEntityDescription( key="record_detections", - name="Type: Detections Video", + name="Type: detections video", native_unit_of_measurement=PERCENTAGE, icon="mdi:server", entity_category=EntityCategory.DIAGNOSTIC, @@ -447,7 +447,7 @@ NVR_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( ), ProtectSensorEntityDescription( key="resolution_HD", - name="Resolution: HD Video", + name="Resolution: HD video", native_unit_of_measurement=PERCENTAGE, icon="mdi:cctv", entity_category=EntityCategory.DIAGNOSTIC, @@ -457,7 +457,7 @@ NVR_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( ), ProtectSensorEntityDescription( key="resolution_4K", - name="Resolution: 4K Video", + name="Resolution: 4K video", native_unit_of_measurement=PERCENTAGE, icon="mdi:cctv", entity_category=EntityCategory.DIAGNOSTIC, @@ -467,7 +467,7 @@ NVR_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( ), ProtectSensorEntityDescription( key="resolution_free", - name="Resolution: Free Space", + name="Resolution: free space", native_unit_of_measurement=PERCENTAGE, icon="mdi:cctv", entity_category=EntityCategory.DIAGNOSTIC, @@ -477,7 +477,7 @@ NVR_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( ), ProtectSensorEntityDescription[NVR]( key="record_capacity", - name="Recording Capacity", + name="Recording capacity", native_unit_of_measurement=UnitOfTime.SECONDS, icon="mdi:record-rec", entity_category=EntityCategory.DIAGNOSTIC, @@ -489,7 +489,7 @@ NVR_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( NVR_DISABLED_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( ProtectSensorEntityDescription( key="cpu_utilization", - name="CPU Utilization", + name="CPU utilization", native_unit_of_measurement=PERCENTAGE, icon="mdi:speedometer", entity_registry_enabled_default=False, @@ -499,7 +499,7 @@ NVR_DISABLED_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( ), ProtectSensorEntityDescription( key="cpu_temperature", - name="CPU Temperature", + name="CPU temperature", native_unit_of_measurement=UnitOfTemperature.CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, entity_registry_enabled_default=False, @@ -509,7 +509,7 @@ NVR_DISABLED_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( ), ProtectSensorEntityDescription[NVR]( key="memory_utilization", - name="Memory Utilization", + name="Memory utilization", native_unit_of_measurement=PERCENTAGE, icon="mdi:memory", entity_registry_enabled_default=False, @@ -523,7 +523,7 @@ NVR_DISABLED_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( LICENSE_PLATE_EVENT_SENSORS: tuple[ProtectSensorEventEntityDescription, ...] = ( ProtectSensorEventEntityDescription( key="smart_obj_licenseplate", - name="License Plate Detected", + name="License plate detected", icon="mdi:car", translation_key="license_plate", ufp_value="is_license_plate_currently_detected", @@ -536,14 +536,14 @@ LICENSE_PLATE_EVENT_SENSORS: tuple[ProtectSensorEventEntityDescription, ...] = ( LIGHT_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( ProtectSensorEntityDescription( key="motion_last_trip_time", - name="Last Motion Detected", + name="Last motion detected", device_class=SensorDeviceClass.TIMESTAMP, ufp_value="last_motion", entity_registry_enabled_default=False, ), ProtectSensorEntityDescription( key="sensitivity", - name="Motion Sensitivity", + name="Motion sensitivity", icon="mdi:walk", native_unit_of_measurement=PERCENTAGE, entity_category=EntityCategory.DIAGNOSTIC, @@ -552,7 +552,7 @@ LIGHT_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( ), ProtectSensorEntityDescription[Light]( key="light_motion", - name="Light Mode", + name="Light mode", icon="mdi:spotlight", entity_category=EntityCategory.DIAGNOSTIC, ufp_value_fn=async_get_light_motion_current, @@ -560,7 +560,7 @@ LIGHT_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( ), ProtectSensorEntityDescription( key="paired_camera", - name="Paired Camera", + name="Paired camera", icon="mdi:cctv", entity_category=EntityCategory.DIAGNOSTIC, ufp_value="camera.display_name", @@ -571,7 +571,7 @@ LIGHT_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( MOTION_TRIP_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( ProtectSensorEntityDescription( key="motion_last_trip_time", - name="Last Motion Detected", + name="Last motion detected", device_class=SensorDeviceClass.TIMESTAMP, ufp_value="last_motion", entity_registry_enabled_default=False, @@ -581,7 +581,7 @@ MOTION_TRIP_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( CHIME_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( ProtectSensorEntityDescription( key="last_ring", - name="Last Ring", + name="Last ring", device_class=SensorDeviceClass.TIMESTAMP, icon="mdi:bell", ufp_value="last_ring", diff --git a/homeassistant/components/unifiprotect/switch.py b/homeassistant/components/unifiprotect/switch.py index 32dc5808005..104c8f4af86 100644 --- a/homeassistant/components/unifiprotect/switch.py +++ b/homeassistant/components/unifiprotect/switch.py @@ -4,11 +4,11 @@ from __future__ import annotations from collections.abc import Sequence from dataclasses import dataclass +from functools import partial import logging from typing import Any from uiprotect.data import ( - NVR, Camera, ModelType, ProtectAdoptableDeviceModel, @@ -54,7 +54,7 @@ async def _set_highfps(obj: Camera, value: bool) -> None: CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( ProtectSwitchEntityDescription( key="ssh", - name="SSH Enabled", + name="SSH enabled", icon="mdi:lock", entity_registry_enabled_default=False, entity_category=EntityCategory.CONFIG, @@ -64,7 +64,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( ), ProtectSwitchEntityDescription( key="status_light", - name="Status Light On", + name="Status light on", icon="mdi:led-on", entity_category=EntityCategory.CONFIG, ufp_required_field="feature_flags.has_led_status", @@ -74,7 +74,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( ), ProtectSwitchEntityDescription( key="hdr_mode", - name="HDR Mode", + name="HDR mode", icon="mdi:brightness-7", entity_category=EntityCategory.CONFIG, entity_registry_enabled_default=False, @@ -95,7 +95,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( ), ProtectSwitchEntityDescription( key="system_sounds", - name="System Sounds", + name="System sounds", icon="mdi:speaker", entity_category=EntityCategory.CONFIG, ufp_required_field="has_speaker", @@ -106,7 +106,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( ), ProtectSwitchEntityDescription( key="osd_name", - name="Overlay: Show Name", + name="Overlay: show name", icon="mdi:fullscreen", entity_category=EntityCategory.CONFIG, ufp_value="osd_settings.is_name_enabled", @@ -115,7 +115,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( ), ProtectSwitchEntityDescription( key="osd_date", - name="Overlay: Show Date", + name="Overlay: show date", icon="mdi:fullscreen", entity_category=EntityCategory.CONFIG, ufp_value="osd_settings.is_date_enabled", @@ -124,7 +124,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( ), ProtectSwitchEntityDescription( key="osd_logo", - name="Overlay: Show Logo", + name="Overlay: show logo", icon="mdi:fullscreen", entity_category=EntityCategory.CONFIG, ufp_value="osd_settings.is_logo_enabled", @@ -133,7 +133,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( ), ProtectSwitchEntityDescription( key="osd_bitrate", - name="Overlay: Show Nerd Mode", + name="Overlay: show nerd mode", icon="mdi:fullscreen", entity_category=EntityCategory.CONFIG, ufp_value="osd_settings.is_debug_enabled", @@ -142,7 +142,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( ), ProtectSwitchEntityDescription( key="color_night_vision", - name="Color Night Vision", + name="Color night vision", icon="mdi:light-flood-down", entity_category=EntityCategory.CONFIG, ufp_required_field="has_color_night_vision", @@ -152,7 +152,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( ), ProtectSwitchEntityDescription( key="motion", - name="Detections: Motion", + name="Detections: motion", icon="mdi:run-fast", entity_category=EntityCategory.CONFIG, ufp_value="recording_settings.enable_motion_detection", @@ -162,7 +162,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( ), ProtectSwitchEntityDescription( key="smart_person", - name="Detections: Person", + name="Detections: person", icon="mdi:walk", entity_category=EntityCategory.CONFIG, ufp_required_field="can_detect_person", @@ -173,7 +173,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( ), ProtectSwitchEntityDescription( key="smart_vehicle", - name="Detections: Vehicle", + name="Detections: vehicle", icon="mdi:car", entity_category=EntityCategory.CONFIG, ufp_required_field="can_detect_vehicle", @@ -184,7 +184,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( ), ProtectSwitchEntityDescription( key="smart_animal", - name="Detections: Animal", + name="Detections: animal", icon="mdi:paw", entity_category=EntityCategory.CONFIG, ufp_required_field="can_detect_animal", @@ -195,7 +195,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( ), ProtectSwitchEntityDescription( key="smart_package", - name="Detections: Package", + name="Detections: package", icon="mdi:package-variant-closed", entity_category=EntityCategory.CONFIG, ufp_required_field="can_detect_package", @@ -206,7 +206,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( ), ProtectSwitchEntityDescription( key="smart_licenseplate", - name="Detections: License Plate", + name="Detections: license plate", icon="mdi:car", entity_category=EntityCategory.CONFIG, ufp_required_field="can_detect_license_plate", @@ -217,7 +217,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( ), ProtectSwitchEntityDescription( key="smart_smoke", - name="Detections: Smoke", + name="Detections: smoke", icon="mdi:fire", entity_category=EntityCategory.CONFIG, ufp_required_field="can_detect_smoke", @@ -239,7 +239,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( ), ProtectSwitchEntityDescription( key="smart_siren", - name="Detections: Siren", + name="Detections: siren", icon="mdi:alarm-bell", entity_category=EntityCategory.CONFIG, ufp_required_field="can_detect_siren", @@ -250,7 +250,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( ), ProtectSwitchEntityDescription( key="smart_baby_cry", - name="Detections: Baby Cry", + name="Detections: baby cry", icon="mdi:cradle", entity_category=EntityCategory.CONFIG, ufp_required_field="can_detect_baby_cry", @@ -261,7 +261,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( ), ProtectSwitchEntityDescription( key="smart_speak", - name="Detections: Speaking", + name="Detections: speaking", icon="mdi:account-voice", entity_category=EntityCategory.CONFIG, ufp_required_field="can_detect_speaking", @@ -272,7 +272,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( ), ProtectSwitchEntityDescription( key="smart_bark", - name="Detections: Barking", + name="Detections: barking", icon="mdi:dog", entity_category=EntityCategory.CONFIG, ufp_required_field="can_detect_bark", @@ -283,7 +283,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( ), ProtectSwitchEntityDescription( key="smart_car_alarm", - name="Detections: Car Alarm", + name="Detections: car alarm", icon="mdi:car", entity_category=EntityCategory.CONFIG, ufp_required_field="can_detect_car_alarm", @@ -294,7 +294,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( ), ProtectSwitchEntityDescription( key="smart_car_horn", - name="Detections: Car Horn", + name="Detections: car horn", icon="mdi:bugle", entity_category=EntityCategory.CONFIG, ufp_required_field="can_detect_car_horn", @@ -305,7 +305,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( ), ProtectSwitchEntityDescription( key="smart_glass_break", - name="Detections: Glass Break", + name="Detections: glass break", icon="mdi:glass-fragile", entity_category=EntityCategory.CONFIG, ufp_required_field="can_detect_glass_break", @@ -316,7 +316,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( ), ProtectSwitchEntityDescription( key="track_person", - name="Tracking: Person", + name="Tracking: person", icon="mdi:walk", entity_category=EntityCategory.CONFIG, ufp_required_field="is_ptz", @@ -328,7 +328,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( PRIVACY_MODE_SWITCH = ProtectSwitchEntityDescription[Camera]( key="privacy_mode", - name="Privacy Mode", + name="Privacy mode", icon="mdi:eye-settings", entity_category=EntityCategory.CONFIG, ufp_required_field="feature_flags.has_privacy_mask", @@ -339,7 +339,7 @@ PRIVACY_MODE_SWITCH = ProtectSwitchEntityDescription[Camera]( SENSE_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( ProtectSwitchEntityDescription( key="status_light", - name="Status Light On", + name="Status light on", icon="mdi:led-on", entity_category=EntityCategory.CONFIG, ufp_value="led_settings.is_enabled", @@ -348,7 +348,7 @@ SENSE_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( ), ProtectSwitchEntityDescription( key="motion", - name="Motion Detection", + name="Motion detection", icon="mdi:walk", entity_category=EntityCategory.CONFIG, ufp_value="motion_settings.is_enabled", @@ -357,7 +357,7 @@ SENSE_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( ), ProtectSwitchEntityDescription( key="temperature", - name="Temperature Sensor", + name="Temperature sensor", icon="mdi:thermometer", entity_category=EntityCategory.CONFIG, ufp_value="temperature_settings.is_enabled", @@ -366,7 +366,7 @@ SENSE_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( ), ProtectSwitchEntityDescription( key="humidity", - name="Humidity Sensor", + name="Humidity sensor", icon="mdi:water-percent", entity_category=EntityCategory.CONFIG, ufp_value="humidity_settings.is_enabled", @@ -375,7 +375,7 @@ SENSE_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( ), ProtectSwitchEntityDescription( key="light", - name="Light Sensor", + name="Light sensor", icon="mdi:brightness-5", entity_category=EntityCategory.CONFIG, ufp_value="light_settings.is_enabled", @@ -384,7 +384,7 @@ SENSE_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( ), ProtectSwitchEntityDescription( key="alarm", - name="Alarm Sound Detection", + name="Alarm sound detection", entity_category=EntityCategory.CONFIG, ufp_value="alarm_settings.is_enabled", ufp_set_method="set_alarm_status", @@ -396,7 +396,7 @@ SENSE_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( LIGHT_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( ProtectSwitchEntityDescription( key="ssh", - name="SSH Enabled", + name="SSH enabled", icon="mdi:lock", entity_registry_enabled_default=False, entity_category=EntityCategory.CONFIG, @@ -406,7 +406,7 @@ LIGHT_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( ), ProtectSwitchEntityDescription( key="status_light", - name="Status Light On", + name="Status light on", icon="mdi:led-on", entity_category=EntityCategory.CONFIG, ufp_value="light_device_settings.is_indicator_enabled", @@ -418,7 +418,7 @@ LIGHT_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( DOORLOCK_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( ProtectSwitchEntityDescription( key="status_light", - name="Status Light On", + name="Status light on", icon="mdi:led-on", entity_category=EntityCategory.CONFIG, ufp_value="led_settings.is_enabled", @@ -430,7 +430,7 @@ DOORLOCK_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( VIEWER_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( ProtectSwitchEntityDescription( key="ssh", - name="SSH Enabled", + name="SSH enabled", icon="mdi:lock", entity_registry_enabled_default=False, entity_category=EntityCategory.CONFIG, @@ -443,7 +443,7 @@ VIEWER_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( NVR_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( ProtectSwitchEntityDescription( key="analytics_enabled", - name="Analytics Enabled", + name="Analytics enabled", icon="mdi:google-analytics", entity_category=EntityCategory.CONFIG, ufp_value="is_analytics_enabled", @@ -451,7 +451,7 @@ NVR_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( ), ProtectSwitchEntityDescription( key="insights_enabled", - name="Insights Enabled", + name="Insights enabled", icon="mdi:magnify", entity_category=EntityCategory.CONFIG, ufp_value="is_insights_enabled", @@ -467,7 +467,7 @@ _MODEL_DESCRIPTIONS: dict[ModelType, Sequence[ProtectEntityDescription]] = { ModelType.VIEWPORT: VIEWER_SWITCHES, } -_PRIVACY_MODEL_DESCRIPTIONS: dict[ModelType, Sequence[ProtectEntityDescription]] = { +_PRIVACY_DESCRIPTIONS: dict[ModelType, Sequence[ProtectEntityDescription]] = { ModelType.CAMERA: [PRIVACY_MODE_SWITCH] } @@ -478,16 +478,6 @@ class ProtectSwitch(ProtectDeviceEntity, SwitchEntity): entity_description: ProtectSwitchEntityDescription _state_attrs = ("_attr_available", "_attr_is_on") - def __init__( - self, - data: ProtectData, - device: ProtectAdoptableDeviceModel, - description: ProtectSwitchEntityDescription, - ) -> None: - """Initialize an UniFi Protect Switch.""" - super().__init__(data, device, description) - self._attr_name = f"{self.device.display_name} {self.entity_description.name}" - def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None: super()._async_update_device_from_protect(device) self._attr_is_on = self.entity_description.get_ufp_value(self.device) is True @@ -507,16 +497,6 @@ class ProtectNVRSwitch(ProtectNVREntity, SwitchEntity): entity_description: ProtectSwitchEntityDescription _state_attrs = ("_attr_available", "_attr_is_on") - def __init__( - self, - data: ProtectData, - device: NVR, - description: ProtectSwitchEntityDescription, - ) -> None: - """Initialize an UniFi Protect Switch.""" - super().__init__(data, device, description) - self._attr_name = f"{self.device.display_name} {self.entity_description.name}" - def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None: super()._async_update_device_from_protect(device) self._attr_is_on = self.entity_description.get_ufp_value(self.device) is True @@ -598,12 +578,6 @@ class ProtectPrivacyModeSwitch(RestoreEntity, ProtectSwitch): self._update_previous_attr() -MODEL_DESCRIPTIONS_WITH_CLASS = ( - (_MODEL_DESCRIPTIONS, ProtectSwitch), - (_PRIVACY_MODEL_DESCRIPTIONS, ProtectPrivacyModeSwitch), -) - - async def async_setup_entry( hass: HomeAssistant, entry: UFPConfigEntry, @@ -614,20 +588,17 @@ async def async_setup_entry( @callback def _add_new_device(device: ProtectAdoptableDeviceModel) -> None: + _make_entities = partial(async_all_device_entities, data, ufp_device=device) entities: list[BaseProtectEntity] = [] - for model_descriptions, klass in MODEL_DESCRIPTIONS_WITH_CLASS: - entities += async_all_device_entities( - data, klass, model_descriptions=model_descriptions, ufp_device=device - ) + entities += _make_entities(ProtectSwitch, _MODEL_DESCRIPTIONS) + entities += _make_entities(ProtectPrivacyModeSwitch, _PRIVACY_DESCRIPTIONS) async_add_entities(entities) + _make_entities = partial(async_all_device_entities, data) data.async_subscribe_adopt(_add_new_device) entities: list[BaseProtectEntity] = [] - for model_descriptions, klass in MODEL_DESCRIPTIONS_WITH_CLASS: - entities += async_all_device_entities( - data, klass, model_descriptions=model_descriptions - ) - + entities += _make_entities(ProtectSwitch, _MODEL_DESCRIPTIONS) + entities += _make_entities(ProtectPrivacyModeSwitch, _PRIVACY_DESCRIPTIONS) bootstrap = data.api.bootstrap nvr = bootstrap.nvr if nvr.can_write(bootstrap.auth_user) and nvr.is_insights_enabled is not None: diff --git a/homeassistant/components/unifiprotect/utils.py b/homeassistant/components/unifiprotect/utils.py index c509558c9c2..4fb7f6f7661 100644 --- a/homeassistant/components/unifiprotect/utils.py +++ b/homeassistant/components/unifiprotect/utils.py @@ -157,6 +157,6 @@ def get_camera_base_name(channel: CameraChannel) -> str: camera_name = channel.name if channel.name != "Package Camera": - camera_name = f"{channel.name} Resolution Channel" + camera_name = f"{channel.name} resolution channel" return camera_name diff --git a/tests/components/unifiprotect/test_switch.py b/tests/components/unifiprotect/test_switch.py index e03ab81833b..da16475dc1c 100644 --- a/tests/components/unifiprotect/test_switch.py +++ b/tests/components/unifiprotect/test_switch.py @@ -35,20 +35,20 @@ CAMERA_SWITCHES_BASIC = [ for d in CAMERA_SWITCHES if ( not d.name.startswith("Detections:") - and d.name != "SSH Enabled" - and d.name != "Color Night Vision" - and d.name != "Tracking: Person" - and d.name != "HDR Mode" + and d.name != "SSH enabled" + and d.name != "Color night vision" + and d.name != "Tracking: person" + and d.name != "HDR mode" ) - or d.name == "Detections: Motion" - or d.name == "Detections: Person" - or d.name == "Detections: Vehicle" - or d.name == "Detections: Animal" + or d.name == "Detections: motion" + or d.name == "Detections: person" + or d.name == "Detections: vehicle" + or d.name == "Detections: animal" ] CAMERA_SWITCHES_NO_EXTRA = [ d for d in CAMERA_SWITCHES_BASIC - if d.name not in ("High FPS", "Privacy Mode", "HDR Mode") + if d.name not in ("High FPS", "Privacy mode", "HDR mode") ] From 836abe68c7cd1968302f37142ccb5cc22f9781a4 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 16 Jun 2024 13:26:06 -0400 Subject: [PATCH 0722/1445] Track primary integration (#119741) * Track primary integration * Update snapshots * More snapshots updated * Uno mas * Update snapshot --- homeassistant/helpers/device_registry.py | 11 + homeassistant/helpers/entity_platform.py | 1 + .../airgradient/snapshots/test_init.ambr | 1 + .../airgradient/snapshots/test_sensor.ambr | 50 + .../aosmith/snapshots/test_device.ambr | 1 + .../components/config/test_device_registry.py | 3 + .../snapshots/test_init.ambr | 1 + .../ecovacs/snapshots/test_init.ambr | 1 + .../elgato/snapshots/test_button.ambr | 2 + .../elgato/snapshots/test_light.ambr | 3 + .../elgato/snapshots/test_sensor.ambr | 5 + .../elgato/snapshots/test_switch.ambr | 2 + .../energyzero/snapshots/test_sensor.ambr | 6 + .../snapshots/test_diagnostics.ambr | 2 + .../snapshots/test_init.ambr | 1 + .../snapshots/test_init.ambr | 1712 +++++++++-------- .../homewizard/snapshots/test_button.ambr | 1 + .../homewizard/snapshots/test_number.ambr | 2 + .../homewizard/snapshots/test_sensor.ambr | 218 +++ .../homewizard/snapshots/test_switch.ambr | 11 + .../snapshots/test_init.ambr | 1 + .../snapshots/test_sensor.ambr | 58 + .../ista_ecotrend/snapshots/test_init.ambr | 2 + .../kitchen_sink/snapshots/test_switch.ambr | 4 + .../lamarzocco/snapshots/test_switch.ambr | 1 + .../netatmo/snapshots/test_init.ambr | 38 + .../netgear_lte/snapshots/test_init.ambr | 1 + .../ondilo_ico/snapshots/test_init.ambr | 2 + .../onewire/snapshots/test_binary_sensor.ambr | 22 + .../onewire/snapshots/test_sensor.ambr | 22 + .../onewire/snapshots/test_switch.ambr | 22 + .../renault/snapshots/test_binary_sensor.ambr | 8 + .../renault/snapshots/test_button.ambr | 8 + .../snapshots/test_device_tracker.ambr | 8 + .../renault/snapshots/test_select.ambr | 8 + .../renault/snapshots/test_sensor.ambr | 8 + .../components/rova/snapshots/test_init.ambr | 1 + .../sfr_box/snapshots/test_binary_sensor.ambr | 2 + .../sfr_box/snapshots/test_button.ambr | 1 + .../sfr_box/snapshots/test_sensor.ambr | 1 + .../snapshots/test_binary_sensor.ambr | 2 + .../tailwind/snapshots/test_button.ambr | 1 + .../tailwind/snapshots/test_cover.ambr | 2 + .../tailwind/snapshots/test_number.ambr | 1 + .../components/tedee/snapshots/test_init.ambr | 1 + .../components/tedee/snapshots/test_lock.ambr | 2 + .../teslemetry/snapshots/test_init.ambr | 4 + .../twentemilieu/snapshots/test_calendar.ambr | 1 + .../twentemilieu/snapshots/test_sensor.ambr | 5 + .../uptime/snapshots/test_sensor.ambr | 1 + .../components/vesync/snapshots/test_fan.ambr | 9 + .../vesync/snapshots/test_light.ambr | 9 + .../vesync/snapshots/test_sensor.ambr | 9 + .../vesync/snapshots/test_switch.ambr | 9 + .../whois/snapshots/test_sensor.ambr | 9 + .../wled/snapshots/test_binary_sensor.ambr | 1 + .../wled/snapshots/test_button.ambr | 1 + .../wled/snapshots/test_number.ambr | 2 + .../wled/snapshots/test_select.ambr | 4 + .../wled/snapshots/test_switch.ambr | 4 + tests/helpers/test_device_registry.py | 36 + tests/helpers/test_entity_platform.py | 1 + 62 files changed, 1559 insertions(+), 807 deletions(-) diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index 962cd01bf00..324d5ed89a6 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -248,6 +248,7 @@ class DeviceEntry: configuration_url: str | None = attr.ib(default=None) connections: set[tuple[str, str]] = attr.ib(converter=set, factory=set) disabled_by: DeviceEntryDisabler | None = attr.ib(default=None) + primary_integration: str | None = attr.ib(default=None) entry_type: DeviceEntryType | None = attr.ib(default=None) hw_version: str | None = attr.ib(default=None) id: str = attr.ib(factory=uuid_util.random_uuid_hex) @@ -290,6 +291,7 @@ class DeviceEntry: "model": self.model, "name_by_user": self.name_by_user, "name": self.name, + "primary_integration": self.primary_integration, "serial_number": self.serial_number, "sw_version": self.sw_version, "via_device_id": self.via_device_id, @@ -645,6 +647,7 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]): default_name: str | None | UndefinedType = UNDEFINED, # To disable a device if it gets created disabled_by: DeviceEntryDisabler | None | UndefinedType = UNDEFINED, + domain: str | UndefinedType = UNDEFINED, entry_type: DeviceEntryType | None | UndefinedType = UNDEFINED, hw_version: str | None | UndefinedType = UNDEFINED, identifiers: set[tuple[str, str]] | None | UndefinedType = UNDEFINED, @@ -761,7 +764,9 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]): device.id, add_config_entry_id=config_entry_id, configuration_url=configuration_url, + device_info_type=device_info_type, disabled_by=disabled_by, + domain=domain, entry_type=entry_type, hw_version=hw_version, manufacturer=manufacturer, @@ -788,6 +793,8 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]): add_config_entry_id: str | UndefinedType = UNDEFINED, area_id: str | None | UndefinedType = UNDEFINED, configuration_url: str | URL | None | UndefinedType = UNDEFINED, + device_info_type: str | UndefinedType = UNDEFINED, + domain: str | UndefinedType = UNDEFINED, disabled_by: DeviceEntryDisabler | None | UndefinedType = UNDEFINED, entry_type: DeviceEntryType | None | UndefinedType = UNDEFINED, hw_version: str | None | UndefinedType = UNDEFINED, @@ -912,6 +919,10 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]): new_values[attr_name] = value old_values[attr_name] = getattr(old, attr_name) + if device_info_type == "primary" and domain is not UNDEFINED: + new_values["primary_integration"] = domain + old_values["primary_integration"] = old.primary_integration + if old.is_new: new_values["is_new"] = False diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index 4dbe3ac68d8..2fb3c41fbfa 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -771,6 +771,7 @@ class EntityPlatform: try: device = dev_reg.async_get(self.hass).async_get_or_create( config_entry_id=self.config_entry.entry_id, + domain=self.platform_name, **device_info, ) except dev_reg.DeviceInfoError as exc: diff --git a/tests/components/airgradient/snapshots/test_init.ambr b/tests/components/airgradient/snapshots/test_init.ambr index 7109f603c9d..92698023f1c 100644 --- a/tests/components/airgradient/snapshots/test_init.ambr +++ b/tests/components/airgradient/snapshots/test_init.ambr @@ -23,6 +23,7 @@ 'model': 'I-9PSL', 'name': 'Airgradient', 'name_by_user': None, + 'primary_integration': None, 'serial_number': '84fce612f5b8', 'suggested_area': None, 'sw_version': '3.1.1', diff --git a/tests/components/airgradient/snapshots/test_sensor.ambr b/tests/components/airgradient/snapshots/test_sensor.ambr index b9b6be41ff4..6f9297db0d7 100644 --- a/tests/components/airgradient/snapshots/test_sensor.ambr +++ b/tests/components/airgradient/snapshots/test_sensor.ambr @@ -200,6 +200,56 @@ 'state': '270', }) # --- +# name: test_all_entities[sensor.airgradient_pm0_3_count-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.airgradient_pm0_3_count', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'PM0.3 count', + 'platform': 'airgradient', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'pm003_count', + 'unique_id': '84fce612f5b8-pm003', + 'unit_of_measurement': 'particles/dL', + }) +# --- +# name: test_all_entities[sensor.airgradient_pm0_3_count-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Airgradient PM0.3 count', + 'state_class': , + 'unit_of_measurement': 'particles/dL', + }), + 'context': , + 'entity_id': 'sensor.airgradient_pm0_3_count', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '270', + }) +# --- # name: test_all_entities[sensor.airgradient_pm1-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/aosmith/snapshots/test_device.ambr b/tests/components/aosmith/snapshots/test_device.ambr index f6e2625afdb..bee404076cd 100644 --- a/tests/components/aosmith/snapshots/test_device.ambr +++ b/tests/components/aosmith/snapshots/test_device.ambr @@ -23,6 +23,7 @@ 'model': 'HPTS-50 200 202172000', 'name': 'My water heater', 'name_by_user': None, + 'primary_integration': None, 'serial_number': 'serial', 'suggested_area': 'Basement', 'sw_version': '2.14', diff --git a/tests/components/config/test_device_registry.py b/tests/components/config/test_device_registry.py index 3d80b38e8e1..7524de013f6 100644 --- a/tests/components/config/test_device_registry.py +++ b/tests/components/config/test_device_registry.py @@ -70,6 +70,7 @@ async def test_list_devices( "model": "model", "name_by_user": None, "name": None, + "primary_integration": None, "serial_number": None, "sw_version": None, "via_device_id": None, @@ -88,6 +89,7 @@ async def test_list_devices( "model": "model", "name_by_user": None, "name": None, + "primary_integration": None, "serial_number": None, "sw_version": None, "via_device_id": dev1, @@ -119,6 +121,7 @@ async def test_list_devices( "model": "model", "name_by_user": None, "name": None, + "primary_integration": None, "serial_number": None, "sw_version": None, "via_device_id": None, diff --git a/tests/components/devolo_home_network/snapshots/test_init.ambr b/tests/components/devolo_home_network/snapshots/test_init.ambr index b042dfec2f1..1a592d21836 100644 --- a/tests/components/devolo_home_network/snapshots/test_init.ambr +++ b/tests/components/devolo_home_network/snapshots/test_init.ambr @@ -27,6 +27,7 @@ 'model': 'dLAN pro 1200+ WiFi ac', 'name': 'Mock Title', 'name_by_user': None, + 'primary_integration': 'devolo_home_network', 'serial_number': '1234567890', 'suggested_area': None, 'sw_version': '5.6.1', diff --git a/tests/components/ecovacs/snapshots/test_init.ambr b/tests/components/ecovacs/snapshots/test_init.ambr index f47e747b1cf..74b59637dba 100644 --- a/tests/components/ecovacs/snapshots/test_init.ambr +++ b/tests/components/ecovacs/snapshots/test_init.ambr @@ -23,6 +23,7 @@ 'model': 'DEEBOT OZMO 950 Series', 'name': 'Ozmo 950', 'name_by_user': None, + 'primary_integration': 'ecovacs', 'serial_number': 'E1234567890000000001', 'suggested_area': None, 'sw_version': None, diff --git a/tests/components/elgato/snapshots/test_button.ambr b/tests/components/elgato/snapshots/test_button.ambr index e7477540f46..6995e265e1e 100644 --- a/tests/components/elgato/snapshots/test_button.ambr +++ b/tests/components/elgato/snapshots/test_button.ambr @@ -74,6 +74,7 @@ 'model': 'Elgato Key Light Mini', 'name': 'Frenck', 'name_by_user': None, + 'primary_integration': 'elgato', 'serial_number': 'GW24L1A02987', 'suggested_area': None, 'sw_version': '1.0.4 (229)', @@ -155,6 +156,7 @@ 'model': 'Elgato Key Light Mini', 'name': 'Frenck', 'name_by_user': None, + 'primary_integration': 'elgato', 'serial_number': 'GW24L1A02987', 'suggested_area': None, 'sw_version': '1.0.4 (229)', diff --git a/tests/components/elgato/snapshots/test_light.ambr b/tests/components/elgato/snapshots/test_light.ambr index e2f663d294b..9bb26f5efd9 100644 --- a/tests/components/elgato/snapshots/test_light.ambr +++ b/tests/components/elgato/snapshots/test_light.ambr @@ -106,6 +106,7 @@ 'model': 'Elgato Key Light', 'name': 'Frenck', 'name_by_user': None, + 'primary_integration': 'elgato', 'serial_number': 'CN11A1A00001', 'suggested_area': None, 'sw_version': '1.0.3 (192)', @@ -221,6 +222,7 @@ 'model': 'Elgato Light Strip', 'name': 'Frenck', 'name_by_user': None, + 'primary_integration': 'elgato', 'serial_number': 'CN11A1A00001', 'suggested_area': None, 'sw_version': '1.0.3 (192)', @@ -336,6 +338,7 @@ 'model': 'Elgato Light Strip', 'name': 'Frenck', 'name_by_user': None, + 'primary_integration': 'elgato', 'serial_number': 'CN11A1A00001', 'suggested_area': None, 'sw_version': '1.0.3 (192)', diff --git a/tests/components/elgato/snapshots/test_sensor.ambr b/tests/components/elgato/snapshots/test_sensor.ambr index 2b52d6b9f23..aacaf34ef4f 100644 --- a/tests/components/elgato/snapshots/test_sensor.ambr +++ b/tests/components/elgato/snapshots/test_sensor.ambr @@ -81,6 +81,7 @@ 'model': 'Elgato Key Light Mini', 'name': 'Frenck', 'name_by_user': None, + 'primary_integration': 'elgato', 'serial_number': 'GW24L1A02987', 'suggested_area': None, 'sw_version': '1.0.4 (229)', @@ -172,6 +173,7 @@ 'model': 'Elgato Key Light Mini', 'name': 'Frenck', 'name_by_user': None, + 'primary_integration': 'elgato', 'serial_number': 'GW24L1A02987', 'suggested_area': None, 'sw_version': '1.0.4 (229)', @@ -263,6 +265,7 @@ 'model': 'Elgato Key Light Mini', 'name': 'Frenck', 'name_by_user': None, + 'primary_integration': 'elgato', 'serial_number': 'GW24L1A02987', 'suggested_area': None, 'sw_version': '1.0.4 (229)', @@ -351,6 +354,7 @@ 'model': 'Elgato Key Light Mini', 'name': 'Frenck', 'name_by_user': None, + 'primary_integration': 'elgato', 'serial_number': 'GW24L1A02987', 'suggested_area': None, 'sw_version': '1.0.4 (229)', @@ -442,6 +446,7 @@ 'model': 'Elgato Key Light Mini', 'name': 'Frenck', 'name_by_user': None, + 'primary_integration': 'elgato', 'serial_number': 'GW24L1A02987', 'suggested_area': None, 'sw_version': '1.0.4 (229)', diff --git a/tests/components/elgato/snapshots/test_switch.ambr b/tests/components/elgato/snapshots/test_switch.ambr index 41f3a8f3aaf..a501c20e2d7 100644 --- a/tests/components/elgato/snapshots/test_switch.ambr +++ b/tests/components/elgato/snapshots/test_switch.ambr @@ -73,6 +73,7 @@ 'model': 'Elgato Key Light Mini', 'name': 'Frenck', 'name_by_user': None, + 'primary_integration': 'elgato', 'serial_number': 'GW24L1A02987', 'suggested_area': None, 'sw_version': '1.0.4 (229)', @@ -153,6 +154,7 @@ 'model': 'Elgato Key Light Mini', 'name': 'Frenck', 'name_by_user': None, + 'primary_integration': 'elgato', 'serial_number': 'GW24L1A02987', 'suggested_area': None, 'sw_version': '1.0.4 (229)', diff --git a/tests/components/energyzero/snapshots/test_sensor.ambr b/tests/components/energyzero/snapshots/test_sensor.ambr index 23b232379df..2663437ae33 100644 --- a/tests/components/energyzero/snapshots/test_sensor.ambr +++ b/tests/components/energyzero/snapshots/test_sensor.ambr @@ -64,6 +64,7 @@ 'model': None, 'name': 'Energy market price', 'name_by_user': None, + 'primary_integration': 'energyzero', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -138,6 +139,7 @@ 'model': None, 'name': 'Energy market price', 'name_by_user': None, + 'primary_integration': 'energyzero', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -209,6 +211,7 @@ 'model': None, 'name': 'Energy market price', 'name_by_user': None, + 'primary_integration': 'energyzero', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -280,6 +283,7 @@ 'model': None, 'name': 'Energy market price', 'name_by_user': None, + 'primary_integration': 'energyzero', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -351,6 +355,7 @@ 'model': None, 'name': 'Energy market price', 'name_by_user': None, + 'primary_integration': 'energyzero', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -425,6 +430,7 @@ 'model': None, 'name': 'Gas market price', 'name_by_user': None, + 'primary_integration': 'energyzero', 'serial_number': None, 'suggested_area': None, 'sw_version': None, diff --git a/tests/components/enphase_envoy/snapshots/test_diagnostics.ambr b/tests/components/enphase_envoy/snapshots/test_diagnostics.ambr index c2ab51a7dbd..bcbd546c95e 100644 --- a/tests/components/enphase_envoy/snapshots/test_diagnostics.ambr +++ b/tests/components/enphase_envoy/snapshots/test_diagnostics.ambr @@ -48,6 +48,7 @@ 'model': 'Envoy, phases: 3, phase mode: three, net-consumption CT, production CT, storage CT', 'name': 'Envoy <>', 'name_by_user': None, + 'primary_integration': 'enphase_envoy', 'serial_number': '<>', 'suggested_area': None, 'sw_version': '7.1.2', @@ -3772,6 +3773,7 @@ 'model': 'Inverter', 'name': 'Inverter 1', 'name_by_user': None, + 'primary_integration': 'enphase_envoy', 'serial_number': None, 'suggested_area': None, 'sw_version': None, diff --git a/tests/components/gardena_bluetooth/snapshots/test_init.ambr b/tests/components/gardena_bluetooth/snapshots/test_init.ambr index 82e17896d60..2dd7aa2c7de 100644 --- a/tests/components/gardena_bluetooth/snapshots/test_init.ambr +++ b/tests/components/gardena_bluetooth/snapshots/test_init.ambr @@ -23,6 +23,7 @@ 'model': 'Mock Model', 'name': 'Mock Title', 'name_by_user': None, + 'primary_integration': 'gardena_bluetooth', 'serial_number': None, 'suggested_area': None, 'sw_version': '1.2.3', diff --git a/tests/components/homekit_controller/snapshots/test_init.ambr b/tests/components/homekit_controller/snapshots/test_init.ambr index 0507976cd20..34f613ac027 100644 --- a/tests/components/homekit_controller/snapshots/test_init.ambr +++ b/tests/components/homekit_controller/snapshots/test_init.ambr @@ -26,6 +26,7 @@ 'model': 'AP2', 'name': 'Airversa AP2 1808', 'name_by_user': None, + 'primary_integration': 'homekit_controller', 'serial_number': '1234', 'suggested_area': None, 'sw_version': '0.8.16', @@ -622,6 +623,7 @@ 'model': 'T8010', 'name': 'eufy HomeBase2-0AAA', 'name_by_user': None, + 'primary_integration': 'homekit_controller', 'serial_number': 'A0000A000000000A', 'suggested_area': None, 'sw_version': '2.1.6', @@ -695,6 +697,7 @@ 'model': 'T8113', 'name': 'eufyCam2-0000', 'name_by_user': None, + 'primary_integration': 'homekit_controller', 'serial_number': 'A0000A000000000D', 'suggested_area': None, 'sw_version': '1.6.7', @@ -936,6 +939,7 @@ 'model': 'T8113', 'name': 'eufyCam2-000A', 'name_by_user': None, + 'primary_integration': 'homekit_controller', 'serial_number': 'A0000A000000000B', 'suggested_area': None, 'sw_version': '1.6.7', @@ -1177,6 +1181,7 @@ 'model': 'T8113', 'name': 'eufyCam2-000A', 'name_by_user': None, + 'primary_integration': 'homekit_controller', 'serial_number': 'A0000A000000000C', 'suggested_area': None, 'sw_version': '1.6.7', @@ -1422,6 +1427,7 @@ 'model': 'HE1-G01', 'name': 'Aqara-Hub-E1-00A0', 'name_by_user': None, + 'primary_integration': 'homekit_controller', 'serial_number': '00aa00000a0', 'suggested_area': None, 'sw_version': '3.3.0', @@ -1628,6 +1634,7 @@ 'model': 'AS006', 'name': 'Contact Sensor', 'name_by_user': None, + 'primary_integration': 'homekit_controller', 'serial_number': '158d0007c59c6a', 'suggested_area': None, 'sw_version': '0', @@ -1792,6 +1799,7 @@ 'model': 'ZHWA11LM', 'name': 'Aqara Hub-1563', 'name_by_user': None, + 'primary_integration': 'homekit_controller', 'serial_number': '0000000123456789', 'suggested_area': None, 'sw_version': '1.4.7', @@ -2067,6 +2075,7 @@ 'model': 'AR004', 'name': 'Programmable Switch', 'name_by_user': None, + 'primary_integration': 'homekit_controller', 'serial_number': '111a1111a1a111', 'suggested_area': None, 'sw_version': '9', @@ -2190,6 +2199,7 @@ 'model': 'ABC1000', 'name': 'ArloBabyA0', 'name_by_user': None, + 'primary_integration': 'homekit_controller', 'serial_number': '00A0000000000', 'suggested_area': None, 'sw_version': '1.10.931', @@ -2674,6 +2684,7 @@ 'model': 'CS-IWO', 'name': 'InWall Outlet-0394DE', 'name_by_user': None, + 'primary_integration': 'homekit_controller', 'serial_number': '1020301376', 'suggested_area': None, 'sw_version': '1.0.0', @@ -3103,6 +3114,7 @@ 'model': 'REMOTE SENSOR', 'name': 'Basement', 'name_by_user': None, + 'primary_integration': 'homekit_controller', 'serial_number': 'AB3C', 'suggested_area': None, 'sw_version': '1.0.0', @@ -3262,6 +3274,7 @@ 'model': 'ecobee3', 'name': 'HomeW', 'name_by_user': None, + 'primary_integration': 'homekit_controller', 'serial_number': '123456789012', 'suggested_area': None, 'sw_version': '4.2.394', @@ -3716,6 +3729,7 @@ 'model': 'REMOTE SENSOR', 'name': 'Kitchen', 'name_by_user': None, + 'primary_integration': 'homekit_controller', 'serial_number': 'AB1C', 'suggested_area': None, 'sw_version': '1.0.0', @@ -3875,6 +3889,7 @@ 'model': 'REMOTE SENSOR', 'name': 'Porch', 'name_by_user': None, + 'primary_integration': 'homekit_controller', 'serial_number': 'AB2C', 'suggested_area': None, 'sw_version': '1.0.0', @@ -4038,6 +4053,7 @@ 'model': 'ecobee3', 'name': 'HomeW', 'name_by_user': None, + 'primary_integration': 'homekit_controller', 'serial_number': '123456789012', 'suggested_area': None, 'sw_version': '4.2.394', @@ -4496,6 +4512,7 @@ 'model': 'REMOTE SENSOR', 'name': 'Basement', 'name_by_user': None, + 'primary_integration': 'homekit_controller', 'serial_number': 'AB3C', 'suggested_area': None, 'sw_version': '1.0.0', @@ -4610,6 +4627,7 @@ 'model': 'ecobee3', 'name': 'HomeW', 'name_by_user': None, + 'primary_integration': 'homekit_controller', 'serial_number': '123456789012', 'suggested_area': None, 'sw_version': '4.2.394', @@ -4891,6 +4909,7 @@ 'model': 'REMOTE SENSOR', 'name': 'Kitchen', 'name_by_user': None, + 'primary_integration': 'homekit_controller', 'serial_number': 'AB1C', 'suggested_area': None, 'sw_version': '1.0.0', @@ -5050,6 +5069,7 @@ 'model': 'REMOTE SENSOR', 'name': 'Porch', 'name_by_user': None, + 'primary_integration': 'homekit_controller', 'serial_number': 'AB2C', 'suggested_area': None, 'sw_version': '1.0.0', @@ -5213,6 +5233,7 @@ 'model': 'ECB501', 'name': 'My ecobee', 'name_by_user': None, + 'primary_integration': 'homekit_controller', 'serial_number': '123456789016', 'suggested_area': None, 'sw_version': '4.7.340214', @@ -5680,6 +5701,7 @@ 'model': 'ecobee Switch+', 'name': 'Master Fan', 'name_by_user': None, + 'primary_integration': 'homekit_controller', 'serial_number': '111111111111', 'suggested_area': None, 'sw_version': '4.5.130201', @@ -5969,6 +5991,7 @@ 'model': 'Eve Degree 00AAA0000', 'name': 'Eve Degree AA11', 'name_by_user': None, + 'primary_integration': 'homekit_controller', 'serial_number': 'AA00A0A00000', 'suggested_area': None, 'sw_version': '1.2.8', @@ -6325,6 +6348,7 @@ 'model': 'Eve Energy 20EAO8601', 'name': 'Eve Energy 50FF', 'name_by_user': None, + 'primary_integration': 'homekit_controller', 'serial_number': 'AA00A0A00000', 'suggested_area': None, 'sw_version': '1.2.9', @@ -6638,6 +6662,120 @@ # --- # name: test_snapshots[haa_fan] list([ + dict({ + 'device': dict({ + 'area_id': None, + 'config_entries': list([ + 'TestData', + ]), + 'configuration_url': None, + 'connections': list([ + ]), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': '', + 'identifiers': list([ + list([ + 'homekit_controller:accessory-id', + '00:00:00:00:00:00:aid:2', + ]), + ]), + 'is_new': False, + 'labels': list([ + ]), + 'manufacturer': 'José A. Jiménez Campos', + 'model': 'RavenSystem HAA', + 'name': 'HAA-C718B3', + 'name_by_user': None, + 'primary_integration': 'homekit_controller', + 'serial_number': 'C718B3-2', + 'suggested_area': None, + 'sw_version': '5.0.18', + }), + 'entities': list([ + dict({ + 'entry': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': 'TestData', + 'device_class': None, + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.haa_c718b3_identify_2', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'HAA-C718B3 Identify', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00_2_1_7', + 'unit_of_measurement': None, + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'identify', + 'friendly_name': 'HAA-C718B3 Identify', + }), + 'entity_id': 'button.haa_c718b3_identify_2', + 'state': 'unknown', + }), + }), + dict({ + 'entry': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': 'TestData', + 'device_class': None, + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.haa_c718b3', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'HAA-C718B3', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00_2_8', + 'unit_of_measurement': None, + }), + 'state': dict({ + 'attributes': dict({ + 'friendly_name': 'HAA-C718B3', + }), + 'entity_id': 'switch.haa_c718b3', + 'state': 'off', + }), + }), + ]), + }), dict({ 'device': dict({ 'area_id': None, @@ -6663,6 +6801,7 @@ 'model': 'RavenSystem HAA', 'name': 'HAA-C718B3', 'name_by_user': None, + 'primary_integration': 'homekit_controller', 'serial_number': 'C718B3-1', 'suggested_area': None, 'sw_version': '5.0.18', @@ -6839,119 +6978,6 @@ }), ]), }), - dict({ - 'device': dict({ - 'area_id': None, - 'config_entries': list([ - 'TestData', - ]), - 'configuration_url': None, - 'connections': list([ - ]), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': '', - 'identifiers': list([ - list([ - 'homekit_controller:accessory-id', - '00:00:00:00:00:00:aid:2', - ]), - ]), - 'is_new': False, - 'labels': list([ - ]), - 'manufacturer': 'José A. Jiménez Campos', - 'model': 'RavenSystem HAA', - 'name': 'HAA-C718B3', - 'name_by_user': None, - 'serial_number': 'C718B3-2', - 'suggested_area': None, - 'sw_version': '5.0.18', - }), - 'entities': list([ - dict({ - 'entry': dict({ - 'aliases': list([ - ]), - 'area_id': None, - 'capabilities': None, - 'categories': dict({ - }), - 'config_entry_id': 'TestData', - 'device_class': None, - 'disabled_by': None, - 'domain': 'button', - 'entity_category': , - 'entity_id': 'button.haa_c718b3_identify_2', - 'has_entity_name': False, - 'hidden_by': None, - 'icon': None, - 'labels': list([ - ]), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'HAA-C718B3 Identify', - 'platform': 'homekit_controller', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '00:00:00:00:00:00_2_1_7', - 'unit_of_measurement': None, - }), - 'state': dict({ - 'attributes': dict({ - 'device_class': 'identify', - 'friendly_name': 'HAA-C718B3 Identify', - }), - 'entity_id': 'button.haa_c718b3_identify_2', - 'state': 'unknown', - }), - }), - dict({ - 'entry': dict({ - 'aliases': list([ - ]), - 'area_id': None, - 'capabilities': None, - 'categories': dict({ - }), - 'config_entry_id': 'TestData', - 'device_class': None, - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': None, - 'entity_id': 'switch.haa_c718b3', - 'has_entity_name': False, - 'hidden_by': None, - 'icon': None, - 'labels': list([ - ]), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'HAA-C718B3', - 'platform': 'homekit_controller', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '00:00:00:00:00:00_2_8', - 'unit_of_measurement': None, - }), - 'state': dict({ - 'attributes': dict({ - 'friendly_name': 'HAA-C718B3', - }), - 'entity_id': 'switch.haa_c718b3', - 'state': 'off', - }), - }), - ]), - }), ]) # --- # name: test_snapshots[home_assistant_bridge_basic_cover] @@ -6981,6 +7007,7 @@ 'model': 'RYSE Shade', 'name': 'Family Room North', 'name_by_user': None, + 'primary_integration': 'homekit_controller', 'serial_number': 'cover.family_door_north', 'suggested_area': None, 'sw_version': '3.6.2', @@ -7142,6 +7169,7 @@ 'model': 'Bridge', 'name': 'HASS Bridge S6', 'name_by_user': None, + 'primary_integration': 'homekit_controller', 'serial_number': 'homekit.bridge', 'suggested_area': None, 'sw_version': '2024.2.0', @@ -7215,6 +7243,7 @@ 'model': 'RYSE Shade', 'name': 'Kitchen Window', 'name_by_user': None, + 'primary_integration': 'homekit_controller', 'serial_number': 'cover.kitchen_window', 'suggested_area': None, 'sw_version': '3.6.2', @@ -7380,6 +7409,7 @@ 'model': 'Fan', 'name': 'Ceiling Fan', 'name_by_user': None, + 'primary_integration': 'homekit_controller', 'serial_number': 'fan.ceiling_fan', 'suggested_area': None, 'sw_version': '0.104.0.dev0', @@ -7500,6 +7530,7 @@ 'model': 'Bridge', 'name': 'Home Assistant Bridge', 'name_by_user': None, + 'primary_integration': 'homekit_controller', 'serial_number': 'homekit.bridge', 'suggested_area': None, 'sw_version': '0.104.0.dev0', @@ -7573,6 +7604,7 @@ 'model': 'Fan', 'name': 'Living Room Fan', 'name_by_user': None, + 'primary_integration': 'homekit_controller', 'serial_number': 'fan.living_room_fan', 'suggested_area': None, 'sw_version': '0.104.0.dev0', @@ -7698,6 +7730,7 @@ 'model': 'Climate Control', 'name': '89 Living Room', 'name_by_user': None, + 'primary_integration': 'homekit_controller', 'serial_number': 'climate.89_living_room', 'suggested_area': None, 'sw_version': '2024.2.0', @@ -8020,6 +8053,7 @@ 'model': 'Bridge', 'name': 'HASS Bridge S6', 'name_by_user': None, + 'primary_integration': 'homekit_controller', 'serial_number': 'homekit.bridge', 'suggested_area': None, 'sw_version': '2024.2.0', @@ -8097,6 +8131,7 @@ 'model': 'Bridge', 'name': 'HASS Bridge S6', 'name_by_user': None, + 'primary_integration': 'homekit_controller', 'serial_number': 'homekit.bridge', 'suggested_area': None, 'sw_version': '2024.2.0', @@ -8170,6 +8205,7 @@ 'model': '1039102', 'name': 'Laundry Smoke ED78', 'name_by_user': None, + 'primary_integration': 'homekit_controller', 'serial_number': 'light.laundry_smoke_ed78', 'suggested_area': None, 'sw_version': '1.4.84', @@ -8343,6 +8379,7 @@ 'model': 'RYSE Shade', 'name': 'Family Room North', 'name_by_user': None, + 'primary_integration': 'homekit_controller', 'serial_number': 'cover.family_door_north', 'suggested_area': None, 'sw_version': '3.6.2', @@ -8504,6 +8541,7 @@ 'model': 'Bridge', 'name': 'HASS Bridge S6', 'name_by_user': None, + 'primary_integration': 'homekit_controller', 'serial_number': 'homekit.bridge', 'suggested_area': None, 'sw_version': '2024.2.0', @@ -8577,6 +8615,7 @@ 'model': 'RYSE Shade', 'name': 'Kitchen Window', 'name_by_user': None, + 'primary_integration': 'homekit_controller', 'serial_number': 'cover.kitchen_window', 'suggested_area': None, 'sw_version': '3.6.2', @@ -8742,6 +8781,7 @@ 'model': 'Fan', 'name': 'Ceiling Fan', 'name_by_user': None, + 'primary_integration': 'homekit_controller', 'serial_number': 'fan.ceiling_fan', 'suggested_area': None, 'sw_version': '0.104.0.dev0', @@ -8862,6 +8902,7 @@ 'model': 'Bridge', 'name': 'Home Assistant Bridge', 'name_by_user': None, + 'primary_integration': 'homekit_controller', 'serial_number': 'homekit.bridge', 'suggested_area': None, 'sw_version': '0.104.0.dev0', @@ -8935,6 +8976,7 @@ 'model': 'Fan', 'name': 'Living Room Fan', 'name_by_user': None, + 'primary_integration': 'homekit_controller', 'serial_number': 'fan.living_room_fan', 'suggested_area': None, 'sw_version': '0.104.0.dev0', @@ -9061,6 +9103,7 @@ 'model': 'Bridge', 'name': 'Home Assistant Bridge', 'name_by_user': None, + 'primary_integration': 'homekit_controller', 'serial_number': 'homekit.bridge', 'suggested_area': None, 'sw_version': '0.104.0.dev0', @@ -9134,6 +9177,7 @@ 'model': 'Fan', 'name': 'Living Room Fan', 'name_by_user': None, + 'primary_integration': 'homekit_controller', 'serial_number': 'fan.living_room_fan', 'suggested_area': None, 'sw_version': '0.104.0.dev0', @@ -9260,6 +9304,7 @@ 'model': 'Climate Control', 'name': '89 Living Room', 'name_by_user': None, + 'primary_integration': 'homekit_controller', 'serial_number': 'climate.89_living_room', 'suggested_area': None, 'sw_version': '2024.2.0', @@ -9591,6 +9636,7 @@ 'model': 'Bridge', 'name': 'HASS Bridge S6', 'name_by_user': None, + 'primary_integration': 'homekit_controller', 'serial_number': 'homekit.bridge', 'suggested_area': None, 'sw_version': '2024.2.0', @@ -9668,6 +9714,7 @@ 'model': 'Bridge', 'name': 'HASS Bridge S6', 'name_by_user': None, + 'primary_integration': 'homekit_controller', 'serial_number': 'homekit.bridge', 'suggested_area': None, 'sw_version': '2024.2.0', @@ -9741,6 +9788,7 @@ 'model': 'WoHumi', 'name': 'Humidifier 182A', 'name_by_user': None, + 'primary_integration': 'homekit_controller', 'serial_number': 'humidifier.humidifier_182a', 'suggested_area': None, 'sw_version': '2024.2.0', @@ -9921,6 +9969,7 @@ 'model': 'Bridge', 'name': 'HASS Bridge S6', 'name_by_user': None, + 'primary_integration': 'homekit_controller', 'serial_number': 'homekit.bridge', 'suggested_area': None, 'sw_version': '2024.2.0', @@ -9994,6 +10043,7 @@ 'model': 'WoHumi', 'name': 'Humidifier 182A', 'name_by_user': None, + 'primary_integration': 'homekit_controller', 'serial_number': 'humidifier.humidifier_182a', 'suggested_area': None, 'sw_version': '2024.2.0', @@ -10174,6 +10224,7 @@ 'model': 'Bridge', 'name': 'HASS Bridge S6', 'name_by_user': None, + 'primary_integration': 'homekit_controller', 'serial_number': 'homekit.bridge', 'suggested_area': None, 'sw_version': '2024.2.0', @@ -10247,6 +10298,7 @@ 'model': '1039102', 'name': 'Laundry Smoke ED78', 'name_by_user': None, + 'primary_integration': 'homekit_controller', 'serial_number': 'light.laundry_smoke_ed78', 'suggested_area': None, 'sw_version': '1.4.84', @@ -10435,6 +10487,7 @@ 'model': 'Daikin-fwec3a-esp32-homekit-bridge', 'name': 'Air Conditioner', 'name_by_user': None, + 'primary_integration': 'homekit_controller', 'serial_number': '00000001', 'suggested_area': None, 'sw_version': '1.0.0', @@ -10608,414 +10661,6 @@ # --- # name: test_snapshots[hue_bridge] list([ - dict({ - 'device': dict({ - 'area_id': None, - 'config_entries': list([ - 'TestData', - ]), - 'configuration_url': None, - 'connections': list([ - ]), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': '', - 'identifiers': list([ - list([ - 'homekit_controller:accessory-id', - '00:00:00:00:00:00:aid:6623462395276914', - ]), - ]), - 'is_new': False, - 'labels': list([ - ]), - 'manufacturer': 'Philips', - 'model': 'LTW012', - 'name': 'Hue ambiance candle', - 'name_by_user': None, - 'serial_number': '6623462395276914', - 'suggested_area': None, - 'sw_version': '1.46.13', - }), - 'entities': list([ - dict({ - 'entry': dict({ - 'aliases': list([ - ]), - 'area_id': None, - 'capabilities': None, - 'categories': dict({ - }), - 'config_entry_id': 'TestData', - 'device_class': None, - 'disabled_by': None, - 'domain': 'button', - 'entity_category': , - 'entity_id': 'button.hue_ambiance_candle_identify_4', - 'has_entity_name': False, - 'hidden_by': None, - 'icon': None, - 'labels': list([ - ]), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Hue ambiance candle Identify', - 'platform': 'homekit_controller', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '00:00:00:00:00:00_6623462395276914_1_6', - 'unit_of_measurement': None, - }), - 'state': dict({ - 'attributes': dict({ - 'device_class': 'identify', - 'friendly_name': 'Hue ambiance candle Identify', - }), - 'entity_id': 'button.hue_ambiance_candle_identify_4', - 'state': 'unknown', - }), - }), - dict({ - 'entry': dict({ - 'aliases': list([ - ]), - 'area_id': None, - 'capabilities': dict({ - 'max_color_temp_kelvin': 6535, - 'max_mireds': 454, - 'min_color_temp_kelvin': 2202, - 'min_mireds': 153, - 'supported_color_modes': list([ - , - ]), - }), - 'categories': dict({ - }), - 'config_entry_id': 'TestData', - 'device_class': None, - 'disabled_by': None, - 'domain': 'light', - 'entity_category': None, - 'entity_id': 'light.hue_ambiance_candle_4', - 'has_entity_name': False, - 'hidden_by': None, - 'icon': None, - 'labels': list([ - ]), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Hue ambiance candle', - 'platform': 'homekit_controller', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '00:00:00:00:00:00_6623462395276914_2816', - 'unit_of_measurement': None, - }), - 'state': dict({ - 'attributes': dict({ - 'brightness': None, - 'color_mode': None, - 'color_temp': None, - 'color_temp_kelvin': None, - 'friendly_name': 'Hue ambiance candle', - 'hs_color': None, - 'max_color_temp_kelvin': 6535, - 'max_mireds': 454, - 'min_color_temp_kelvin': 2202, - 'min_mireds': 153, - 'rgb_color': None, - 'supported_color_modes': list([ - , - ]), - 'supported_features': , - 'xy_color': None, - }), - 'entity_id': 'light.hue_ambiance_candle_4', - 'state': 'off', - }), - }), - ]), - }), - dict({ - 'device': dict({ - 'area_id': None, - 'config_entries': list([ - 'TestData', - ]), - 'configuration_url': None, - 'connections': list([ - ]), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': '', - 'identifiers': list([ - list([ - 'homekit_controller:accessory-id', - '00:00:00:00:00:00:aid:6623462395276939', - ]), - ]), - 'is_new': False, - 'labels': list([ - ]), - 'manufacturer': 'Philips', - 'model': 'LTW012', - 'name': 'Hue ambiance candle', - 'name_by_user': None, - 'serial_number': '6623462395276939', - 'suggested_area': None, - 'sw_version': '1.46.13', - }), - 'entities': list([ - dict({ - 'entry': dict({ - 'aliases': list([ - ]), - 'area_id': None, - 'capabilities': None, - 'categories': dict({ - }), - 'config_entry_id': 'TestData', - 'device_class': None, - 'disabled_by': None, - 'domain': 'button', - 'entity_category': , - 'entity_id': 'button.hue_ambiance_candle_identify_3', - 'has_entity_name': False, - 'hidden_by': None, - 'icon': None, - 'labels': list([ - ]), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Hue ambiance candle Identify', - 'platform': 'homekit_controller', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '00:00:00:00:00:00_6623462395276939_1_6', - 'unit_of_measurement': None, - }), - 'state': dict({ - 'attributes': dict({ - 'device_class': 'identify', - 'friendly_name': 'Hue ambiance candle Identify', - }), - 'entity_id': 'button.hue_ambiance_candle_identify_3', - 'state': 'unknown', - }), - }), - dict({ - 'entry': dict({ - 'aliases': list([ - ]), - 'area_id': None, - 'capabilities': dict({ - 'max_color_temp_kelvin': 6535, - 'max_mireds': 454, - 'min_color_temp_kelvin': 2202, - 'min_mireds': 153, - 'supported_color_modes': list([ - , - ]), - }), - 'categories': dict({ - }), - 'config_entry_id': 'TestData', - 'device_class': None, - 'disabled_by': None, - 'domain': 'light', - 'entity_category': None, - 'entity_id': 'light.hue_ambiance_candle_3', - 'has_entity_name': False, - 'hidden_by': None, - 'icon': None, - 'labels': list([ - ]), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Hue ambiance candle', - 'platform': 'homekit_controller', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '00:00:00:00:00:00_6623462395276939_2816', - 'unit_of_measurement': None, - }), - 'state': dict({ - 'attributes': dict({ - 'brightness': None, - 'color_mode': None, - 'color_temp': None, - 'color_temp_kelvin': None, - 'friendly_name': 'Hue ambiance candle', - 'hs_color': None, - 'max_color_temp_kelvin': 6535, - 'max_mireds': 454, - 'min_color_temp_kelvin': 2202, - 'min_mireds': 153, - 'rgb_color': None, - 'supported_color_modes': list([ - , - ]), - 'supported_features': , - 'xy_color': None, - }), - 'entity_id': 'light.hue_ambiance_candle_3', - 'state': 'off', - }), - }), - ]), - }), - dict({ - 'device': dict({ - 'area_id': None, - 'config_entries': list([ - 'TestData', - ]), - 'configuration_url': None, - 'connections': list([ - ]), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': '', - 'identifiers': list([ - list([ - 'homekit_controller:accessory-id', - '00:00:00:00:00:00:aid:6623462403113447', - ]), - ]), - 'is_new': False, - 'labels': list([ - ]), - 'manufacturer': 'Philips', - 'model': 'LTW012', - 'name': 'Hue ambiance candle', - 'name_by_user': None, - 'serial_number': '6623462403113447', - 'suggested_area': None, - 'sw_version': '1.46.13', - }), - 'entities': list([ - dict({ - 'entry': dict({ - 'aliases': list([ - ]), - 'area_id': None, - 'capabilities': None, - 'categories': dict({ - }), - 'config_entry_id': 'TestData', - 'device_class': None, - 'disabled_by': None, - 'domain': 'button', - 'entity_category': , - 'entity_id': 'button.hue_ambiance_candle_identify_2', - 'has_entity_name': False, - 'hidden_by': None, - 'icon': None, - 'labels': list([ - ]), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Hue ambiance candle Identify', - 'platform': 'homekit_controller', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '00:00:00:00:00:00_6623462403113447_1_6', - 'unit_of_measurement': None, - }), - 'state': dict({ - 'attributes': dict({ - 'device_class': 'identify', - 'friendly_name': 'Hue ambiance candle Identify', - }), - 'entity_id': 'button.hue_ambiance_candle_identify_2', - 'state': 'unknown', - }), - }), - dict({ - 'entry': dict({ - 'aliases': list([ - ]), - 'area_id': None, - 'capabilities': dict({ - 'max_color_temp_kelvin': 6535, - 'max_mireds': 454, - 'min_color_temp_kelvin': 2202, - 'min_mireds': 153, - 'supported_color_modes': list([ - , - ]), - }), - 'categories': dict({ - }), - 'config_entry_id': 'TestData', - 'device_class': None, - 'disabled_by': None, - 'domain': 'light', - 'entity_category': None, - 'entity_id': 'light.hue_ambiance_candle_2', - 'has_entity_name': False, - 'hidden_by': None, - 'icon': None, - 'labels': list([ - ]), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Hue ambiance candle', - 'platform': 'homekit_controller', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '00:00:00:00:00:00_6623462403113447_2816', - 'unit_of_measurement': None, - }), - 'state': dict({ - 'attributes': dict({ - 'brightness': None, - 'color_mode': None, - 'color_temp': None, - 'color_temp_kelvin': None, - 'friendly_name': 'Hue ambiance candle', - 'hs_color': None, - 'max_color_temp_kelvin': 6535, - 'max_mireds': 454, - 'min_color_temp_kelvin': 2202, - 'min_mireds': 153, - 'rgb_color': None, - 'supported_color_modes': list([ - , - ]), - 'supported_features': , - 'xy_color': None, - }), - 'entity_id': 'light.hue_ambiance_candle_2', - 'state': 'off', - }), - }), - ]), - }), dict({ 'device': dict({ 'area_id': None, @@ -11041,6 +10686,7 @@ 'model': 'LTW012', 'name': 'Hue ambiance candle', 'name_by_user': None, + 'primary_integration': 'homekit_controller', 'serial_number': '6623462403233419', 'suggested_area': None, 'sw_version': '1.46.13', @@ -11167,17 +10813,18 @@ 'identifiers': list([ list([ 'homekit_controller:accessory-id', - '00:00:00:00:00:00:aid:6623462412411853', + '00:00:00:00:00:00:aid:6623462403113447', ]), ]), 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Philips', - 'model': 'LTW013', - 'name': 'Hue ambiance spot', + 'model': 'LTW012', + 'name': 'Hue ambiance candle', 'name_by_user': None, - 'serial_number': '6623462412411853', + 'primary_integration': 'homekit_controller', + 'serial_number': '6623462403113447', 'suggested_area': None, 'sw_version': '1.46.13', }), @@ -11195,7 +10842,7 @@ 'disabled_by': None, 'domain': 'button', 'entity_category': , - 'entity_id': 'button.hue_ambiance_spot_identify_2', + 'entity_id': 'button.hue_ambiance_candle_identify_2', 'has_entity_name': False, 'hidden_by': None, 'icon': None, @@ -11206,20 +10853,20 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Hue ambiance spot Identify', + 'original_name': 'Hue ambiance candle Identify', 'platform': 'homekit_controller', 'previous_unique_id': None, 'supported_features': 0, 'translation_key': None, - 'unique_id': '00:00:00:00:00:00_6623462412411853_1_6', + 'unique_id': '00:00:00:00:00:00_6623462403113447_1_6', 'unit_of_measurement': None, }), 'state': dict({ 'attributes': dict({ 'device_class': 'identify', - 'friendly_name': 'Hue ambiance spot Identify', + 'friendly_name': 'Hue ambiance candle Identify', }), - 'entity_id': 'button.hue_ambiance_spot_identify_2', + 'entity_id': 'button.hue_ambiance_candle_identify_2', 'state': 'unknown', }), }), @@ -11244,7 +10891,7 @@ 'disabled_by': None, 'domain': 'light', 'entity_category': None, - 'entity_id': 'light.hue_ambiance_spot_2', + 'entity_id': 'light.hue_ambiance_candle_2', 'has_entity_name': False, 'hidden_by': None, 'icon': None, @@ -11255,45 +10902,309 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Hue ambiance spot', + 'original_name': 'Hue ambiance candle', 'platform': 'homekit_controller', 'previous_unique_id': None, 'supported_features': 0, 'translation_key': None, - 'unique_id': '00:00:00:00:00:00_6623462412411853_2816', + 'unique_id': '00:00:00:00:00:00_6623462403113447_2816', 'unit_of_measurement': None, }), 'state': dict({ 'attributes': dict({ - 'brightness': 255.0, - 'color_mode': , - 'color_temp': 366, - 'color_temp_kelvin': 2732, - 'friendly_name': 'Hue ambiance spot', - 'hs_color': tuple( - 28.327, - 64.71, - ), + 'brightness': None, + 'color_mode': None, + 'color_temp': None, + 'color_temp_kelvin': None, + 'friendly_name': 'Hue ambiance candle', + 'hs_color': None, 'max_color_temp_kelvin': 6535, 'max_mireds': 454, 'min_color_temp_kelvin': 2202, 'min_mireds': 153, - 'rgb_color': tuple( - 255, - 167, - 89, - ), + 'rgb_color': None, 'supported_color_modes': list([ , ]), 'supported_features': , - 'xy_color': tuple( - 0.524, - 0.387, - ), + 'xy_color': None, }), - 'entity_id': 'light.hue_ambiance_spot_2', - 'state': 'on', + 'entity_id': 'light.hue_ambiance_candle_2', + 'state': 'off', + }), + }), + ]), + }), + dict({ + 'device': dict({ + 'area_id': None, + 'config_entries': list([ + 'TestData', + ]), + 'configuration_url': None, + 'connections': list([ + ]), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': '', + 'identifiers': list([ + list([ + 'homekit_controller:accessory-id', + '00:00:00:00:00:00:aid:6623462395276939', + ]), + ]), + 'is_new': False, + 'labels': list([ + ]), + 'manufacturer': 'Philips', + 'model': 'LTW012', + 'name': 'Hue ambiance candle', + 'name_by_user': None, + 'primary_integration': 'homekit_controller', + 'serial_number': '6623462395276939', + 'suggested_area': None, + 'sw_version': '1.46.13', + }), + 'entities': list([ + dict({ + 'entry': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': 'TestData', + 'device_class': None, + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.hue_ambiance_candle_identify_3', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Hue ambiance candle Identify', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00_6623462395276939_1_6', + 'unit_of_measurement': None, + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'identify', + 'friendly_name': 'Hue ambiance candle Identify', + }), + 'entity_id': 'button.hue_ambiance_candle_identify_3', + 'state': 'unknown', + }), + }), + dict({ + 'entry': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'max_color_temp_kelvin': 6535, + 'max_mireds': 454, + 'min_color_temp_kelvin': 2202, + 'min_mireds': 153, + 'supported_color_modes': list([ + , + ]), + }), + 'categories': dict({ + }), + 'config_entry_id': 'TestData', + 'device_class': None, + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.hue_ambiance_candle_3', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Hue ambiance candle', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00_6623462395276939_2816', + 'unit_of_measurement': None, + }), + 'state': dict({ + 'attributes': dict({ + 'brightness': None, + 'color_mode': None, + 'color_temp': None, + 'color_temp_kelvin': None, + 'friendly_name': 'Hue ambiance candle', + 'hs_color': None, + 'max_color_temp_kelvin': 6535, + 'max_mireds': 454, + 'min_color_temp_kelvin': 2202, + 'min_mireds': 153, + 'rgb_color': None, + 'supported_color_modes': list([ + , + ]), + 'supported_features': , + 'xy_color': None, + }), + 'entity_id': 'light.hue_ambiance_candle_3', + 'state': 'off', + }), + }), + ]), + }), + dict({ + 'device': dict({ + 'area_id': None, + 'config_entries': list([ + 'TestData', + ]), + 'configuration_url': None, + 'connections': list([ + ]), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': '', + 'identifiers': list([ + list([ + 'homekit_controller:accessory-id', + '00:00:00:00:00:00:aid:6623462395276914', + ]), + ]), + 'is_new': False, + 'labels': list([ + ]), + 'manufacturer': 'Philips', + 'model': 'LTW012', + 'name': 'Hue ambiance candle', + 'name_by_user': None, + 'primary_integration': 'homekit_controller', + 'serial_number': '6623462395276914', + 'suggested_area': None, + 'sw_version': '1.46.13', + }), + 'entities': list([ + dict({ + 'entry': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': 'TestData', + 'device_class': None, + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.hue_ambiance_candle_identify_4', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Hue ambiance candle Identify', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00_6623462395276914_1_6', + 'unit_of_measurement': None, + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'identify', + 'friendly_name': 'Hue ambiance candle Identify', + }), + 'entity_id': 'button.hue_ambiance_candle_identify_4', + 'state': 'unknown', + }), + }), + dict({ + 'entry': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'max_color_temp_kelvin': 6535, + 'max_mireds': 454, + 'min_color_temp_kelvin': 2202, + 'min_mireds': 153, + 'supported_color_modes': list([ + , + ]), + }), + 'categories': dict({ + }), + 'config_entry_id': 'TestData', + 'device_class': None, + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.hue_ambiance_candle_4', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Hue ambiance candle', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00_6623462395276914_2816', + 'unit_of_measurement': None, + }), + 'state': dict({ + 'attributes': dict({ + 'brightness': None, + 'color_mode': None, + 'color_temp': None, + 'color_temp_kelvin': None, + 'friendly_name': 'Hue ambiance candle', + 'hs_color': None, + 'max_color_temp_kelvin': 6535, + 'max_mireds': 454, + 'min_color_temp_kelvin': 2202, + 'min_mireds': 153, + 'rgb_color': None, + 'supported_color_modes': list([ + , + ]), + 'supported_features': , + 'xy_color': None, + }), + 'entity_id': 'light.hue_ambiance_candle_4', + 'state': 'off', }), }), ]), @@ -11323,6 +11234,7 @@ 'model': 'LTW013', 'name': 'Hue ambiance spot', 'name_by_user': None, + 'primary_integration': 'homekit_controller', 'serial_number': '6623462412413293', 'suggested_area': None, 'sw_version': '1.46.13', @@ -11444,6 +11356,153 @@ }), ]), }), + dict({ + 'device': dict({ + 'area_id': None, + 'config_entries': list([ + 'TestData', + ]), + 'configuration_url': None, + 'connections': list([ + ]), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': '', + 'identifiers': list([ + list([ + 'homekit_controller:accessory-id', + '00:00:00:00:00:00:aid:6623462412411853', + ]), + ]), + 'is_new': False, + 'labels': list([ + ]), + 'manufacturer': 'Philips', + 'model': 'LTW013', + 'name': 'Hue ambiance spot', + 'name_by_user': None, + 'primary_integration': 'homekit_controller', + 'serial_number': '6623462412411853', + 'suggested_area': None, + 'sw_version': '1.46.13', + }), + 'entities': list([ + dict({ + 'entry': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': 'TestData', + 'device_class': None, + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.hue_ambiance_spot_identify_2', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Hue ambiance spot Identify', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00_6623462412411853_1_6', + 'unit_of_measurement': None, + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'identify', + 'friendly_name': 'Hue ambiance spot Identify', + }), + 'entity_id': 'button.hue_ambiance_spot_identify_2', + 'state': 'unknown', + }), + }), + dict({ + 'entry': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'max_color_temp_kelvin': 6535, + 'max_mireds': 454, + 'min_color_temp_kelvin': 2202, + 'min_mireds': 153, + 'supported_color_modes': list([ + , + ]), + }), + 'categories': dict({ + }), + 'config_entry_id': 'TestData', + 'device_class': None, + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.hue_ambiance_spot_2', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Hue ambiance spot', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00_6623462412411853_2816', + 'unit_of_measurement': None, + }), + 'state': dict({ + 'attributes': dict({ + 'brightness': 255.0, + 'color_mode': , + 'color_temp': 366, + 'color_temp_kelvin': 2732, + 'friendly_name': 'Hue ambiance spot', + 'hs_color': tuple( + 28.327, + 64.71, + ), + 'max_color_temp_kelvin': 6535, + 'max_mireds': 454, + 'min_color_temp_kelvin': 2202, + 'min_mireds': 153, + 'rgb_color': tuple( + 255, + 167, + 89, + ), + 'supported_color_modes': list([ + , + ]), + 'supported_features': , + 'xy_color': tuple( + 0.524, + 0.387, + ), + }), + 'entity_id': 'light.hue_ambiance_spot_2', + 'state': 'on', + }), + }), + ]), + }), dict({ 'device': dict({ 'area_id': None, @@ -11469,6 +11528,7 @@ 'model': 'RWL021', 'name': 'Hue dimmer switch', 'name_by_user': None, + 'primary_integration': 'homekit_controller', 'serial_number': '6623462389072572', 'suggested_area': None, 'sw_version': '45.1.17846', @@ -11784,6 +11844,7 @@ 'model': 'LWB010', 'name': 'Hue white lamp', 'name_by_user': None, + 'primary_integration': 'homekit_controller', 'serial_number': '6623462378982941', 'suggested_area': None, 'sw_version': '1.46.13', @@ -11907,6 +11968,7 @@ 'model': 'LWB010', 'name': 'Hue white lamp', 'name_by_user': None, + 'primary_integration': 'homekit_controller', 'serial_number': '6623462378983942', 'suggested_area': None, 'sw_version': '1.46.13', @@ -12005,129 +12067,6 @@ }), ]), }), - dict({ - 'device': dict({ - 'area_id': None, - 'config_entries': list([ - 'TestData', - ]), - 'configuration_url': None, - 'connections': list([ - ]), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': '', - 'identifiers': list([ - list([ - 'homekit_controller:accessory-id', - '00:00:00:00:00:00:aid:6623462379122122', - ]), - ]), - 'is_new': False, - 'labels': list([ - ]), - 'manufacturer': 'Philips', - 'model': 'LWB010', - 'name': 'Hue white lamp', - 'name_by_user': None, - 'serial_number': '6623462379122122', - 'suggested_area': None, - 'sw_version': '1.46.13', - }), - 'entities': list([ - dict({ - 'entry': dict({ - 'aliases': list([ - ]), - 'area_id': None, - 'capabilities': None, - 'categories': dict({ - }), - 'config_entry_id': 'TestData', - 'device_class': None, - 'disabled_by': None, - 'domain': 'button', - 'entity_category': , - 'entity_id': 'button.hue_white_lamp_identify_4', - 'has_entity_name': False, - 'hidden_by': None, - 'icon': None, - 'labels': list([ - ]), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Hue white lamp Identify', - 'platform': 'homekit_controller', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '00:00:00:00:00:00_6623462379122122_1_6', - 'unit_of_measurement': None, - }), - 'state': dict({ - 'attributes': dict({ - 'device_class': 'identify', - 'friendly_name': 'Hue white lamp Identify', - }), - 'entity_id': 'button.hue_white_lamp_identify_4', - 'state': 'unknown', - }), - }), - dict({ - 'entry': dict({ - 'aliases': list([ - ]), - 'area_id': None, - 'capabilities': dict({ - 'supported_color_modes': list([ - , - ]), - }), - 'categories': dict({ - }), - 'config_entry_id': 'TestData', - 'device_class': None, - 'disabled_by': None, - 'domain': 'light', - 'entity_category': None, - 'entity_id': 'light.hue_white_lamp_4', - 'has_entity_name': False, - 'hidden_by': None, - 'icon': None, - 'labels': list([ - ]), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Hue white lamp', - 'platform': 'homekit_controller', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '00:00:00:00:00:00_6623462379122122_2816', - 'unit_of_measurement': None, - }), - 'state': dict({ - 'attributes': dict({ - 'brightness': None, - 'color_mode': None, - 'friendly_name': 'Hue white lamp', - 'supported_color_modes': list([ - , - ]), - 'supported_features': , - }), - 'entity_id': 'light.hue_white_lamp_4', - 'state': 'off', - }), - }), - ]), - }), dict({ 'device': dict({ 'area_id': None, @@ -12153,6 +12092,7 @@ 'model': 'LWB010', 'name': 'Hue white lamp', 'name_by_user': None, + 'primary_integration': 'homekit_controller', 'serial_number': '6623462379123707', 'suggested_area': None, 'sw_version': '1.46.13', @@ -12266,7 +12206,7 @@ 'identifiers': list([ list([ 'homekit_controller:accessory-id', - '00:00:00:00:00:00:aid:6623462383114163', + '00:00:00:00:00:00:aid:6623462379122122', ]), ]), 'is_new': False, @@ -12276,7 +12216,8 @@ 'model': 'LWB010', 'name': 'Hue white lamp', 'name_by_user': None, - 'serial_number': '6623462383114163', + 'primary_integration': 'homekit_controller', + 'serial_number': '6623462379122122', 'suggested_area': None, 'sw_version': '1.46.13', }), @@ -12294,7 +12235,7 @@ 'disabled_by': None, 'domain': 'button', 'entity_category': , - 'entity_id': 'button.hue_white_lamp_identify_7', + 'entity_id': 'button.hue_white_lamp_identify_4', 'has_entity_name': False, 'hidden_by': None, 'icon': None, @@ -12310,7 +12251,7 @@ 'previous_unique_id': None, 'supported_features': 0, 'translation_key': None, - 'unique_id': '00:00:00:00:00:00_6623462383114163_1_6', + 'unique_id': '00:00:00:00:00:00_6623462379122122_1_6', 'unit_of_measurement': None, }), 'state': dict({ @@ -12318,7 +12259,7 @@ 'device_class': 'identify', 'friendly_name': 'Hue white lamp Identify', }), - 'entity_id': 'button.hue_white_lamp_identify_7', + 'entity_id': 'button.hue_white_lamp_identify_4', 'state': 'unknown', }), }), @@ -12339,7 +12280,7 @@ 'disabled_by': None, 'domain': 'light', 'entity_category': None, - 'entity_id': 'light.hue_white_lamp_7', + 'entity_id': 'light.hue_white_lamp_4', 'has_entity_name': False, 'hidden_by': None, 'icon': None, @@ -12355,7 +12296,7 @@ 'previous_unique_id': None, 'supported_features': 0, 'translation_key': None, - 'unique_id': '00:00:00:00:00:00_6623462383114163_2816', + 'unique_id': '00:00:00:00:00:00_6623462379122122_2816', 'unit_of_measurement': None, }), 'state': dict({ @@ -12368,130 +12309,7 @@ ]), 'supported_features': , }), - 'entity_id': 'light.hue_white_lamp_7', - 'state': 'off', - }), - }), - ]), - }), - dict({ - 'device': dict({ - 'area_id': None, - 'config_entries': list([ - 'TestData', - ]), - 'configuration_url': None, - 'connections': list([ - ]), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': '', - 'identifiers': list([ - list([ - 'homekit_controller:accessory-id', - '00:00:00:00:00:00:aid:6623462383114193', - ]), - ]), - 'is_new': False, - 'labels': list([ - ]), - 'manufacturer': 'Philips', - 'model': 'LWB010', - 'name': 'Hue white lamp', - 'name_by_user': None, - 'serial_number': '6623462383114193', - 'suggested_area': None, - 'sw_version': '1.46.13', - }), - 'entities': list([ - dict({ - 'entry': dict({ - 'aliases': list([ - ]), - 'area_id': None, - 'capabilities': None, - 'categories': dict({ - }), - 'config_entry_id': 'TestData', - 'device_class': None, - 'disabled_by': None, - 'domain': 'button', - 'entity_category': , - 'entity_id': 'button.hue_white_lamp_identify_6', - 'has_entity_name': False, - 'hidden_by': None, - 'icon': None, - 'labels': list([ - ]), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Hue white lamp Identify', - 'platform': 'homekit_controller', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '00:00:00:00:00:00_6623462383114193_1_6', - 'unit_of_measurement': None, - }), - 'state': dict({ - 'attributes': dict({ - 'device_class': 'identify', - 'friendly_name': 'Hue white lamp Identify', - }), - 'entity_id': 'button.hue_white_lamp_identify_6', - 'state': 'unknown', - }), - }), - dict({ - 'entry': dict({ - 'aliases': list([ - ]), - 'area_id': None, - 'capabilities': dict({ - 'supported_color_modes': list([ - , - ]), - }), - 'categories': dict({ - }), - 'config_entry_id': 'TestData', - 'device_class': None, - 'disabled_by': None, - 'domain': 'light', - 'entity_category': None, - 'entity_id': 'light.hue_white_lamp_6', - 'has_entity_name': False, - 'hidden_by': None, - 'icon': None, - 'labels': list([ - ]), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Hue white lamp', - 'platform': 'homekit_controller', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '00:00:00:00:00:00_6623462383114193_2816', - 'unit_of_measurement': None, - }), - 'state': dict({ - 'attributes': dict({ - 'brightness': None, - 'color_mode': None, - 'friendly_name': 'Hue white lamp', - 'supported_color_modes': list([ - , - ]), - 'supported_features': , - }), - 'entity_id': 'light.hue_white_lamp_6', + 'entity_id': 'light.hue_white_lamp_4', 'state': 'off', }), }), @@ -12522,6 +12340,7 @@ 'model': 'LWB010', 'name': 'Hue white lamp', 'name_by_user': None, + 'primary_integration': 'homekit_controller', 'serial_number': '6623462385996792', 'suggested_area': None, 'sw_version': '1.46.13', @@ -12620,6 +12439,254 @@ }), ]), }), + dict({ + 'device': dict({ + 'area_id': None, + 'config_entries': list([ + 'TestData', + ]), + 'configuration_url': None, + 'connections': list([ + ]), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': '', + 'identifiers': list([ + list([ + 'homekit_controller:accessory-id', + '00:00:00:00:00:00:aid:6623462383114193', + ]), + ]), + 'is_new': False, + 'labels': list([ + ]), + 'manufacturer': 'Philips', + 'model': 'LWB010', + 'name': 'Hue white lamp', + 'name_by_user': None, + 'primary_integration': 'homekit_controller', + 'serial_number': '6623462383114193', + 'suggested_area': None, + 'sw_version': '1.46.13', + }), + 'entities': list([ + dict({ + 'entry': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': 'TestData', + 'device_class': None, + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.hue_white_lamp_identify_6', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Hue white lamp Identify', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00_6623462383114193_1_6', + 'unit_of_measurement': None, + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'identify', + 'friendly_name': 'Hue white lamp Identify', + }), + 'entity_id': 'button.hue_white_lamp_identify_6', + 'state': 'unknown', + }), + }), + dict({ + 'entry': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'supported_color_modes': list([ + , + ]), + }), + 'categories': dict({ + }), + 'config_entry_id': 'TestData', + 'device_class': None, + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.hue_white_lamp_6', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Hue white lamp', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00_6623462383114193_2816', + 'unit_of_measurement': None, + }), + 'state': dict({ + 'attributes': dict({ + 'brightness': None, + 'color_mode': None, + 'friendly_name': 'Hue white lamp', + 'supported_color_modes': list([ + , + ]), + 'supported_features': , + }), + 'entity_id': 'light.hue_white_lamp_6', + 'state': 'off', + }), + }), + ]), + }), + dict({ + 'device': dict({ + 'area_id': None, + 'config_entries': list([ + 'TestData', + ]), + 'configuration_url': None, + 'connections': list([ + ]), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': '', + 'identifiers': list([ + list([ + 'homekit_controller:accessory-id', + '00:00:00:00:00:00:aid:6623462383114163', + ]), + ]), + 'is_new': False, + 'labels': list([ + ]), + 'manufacturer': 'Philips', + 'model': 'LWB010', + 'name': 'Hue white lamp', + 'name_by_user': None, + 'primary_integration': 'homekit_controller', + 'serial_number': '6623462383114163', + 'suggested_area': None, + 'sw_version': '1.46.13', + }), + 'entities': list([ + dict({ + 'entry': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': 'TestData', + 'device_class': None, + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.hue_white_lamp_identify_7', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Hue white lamp Identify', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00_6623462383114163_1_6', + 'unit_of_measurement': None, + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'identify', + 'friendly_name': 'Hue white lamp Identify', + }), + 'entity_id': 'button.hue_white_lamp_identify_7', + 'state': 'unknown', + }), + }), + dict({ + 'entry': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'supported_color_modes': list([ + , + ]), + }), + 'categories': dict({ + }), + 'config_entry_id': 'TestData', + 'device_class': None, + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.hue_white_lamp_7', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Hue white lamp', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00_6623462383114163_2816', + 'unit_of_measurement': None, + }), + 'state': dict({ + 'attributes': dict({ + 'brightness': None, + 'color_mode': None, + 'friendly_name': 'Hue white lamp', + 'supported_color_modes': list([ + , + ]), + 'supported_features': , + }), + 'entity_id': 'light.hue_white_lamp_7', + 'state': 'off', + }), + }), + ]), + }), dict({ 'device': dict({ 'area_id': None, @@ -12645,6 +12712,7 @@ 'model': 'BSB002', 'name': 'Philips hue - 482544', 'name_by_user': None, + 'primary_integration': 'homekit_controller', 'serial_number': '123456', 'suggested_area': None, 'sw_version': '1.32.1932126170', @@ -12722,6 +12790,7 @@ 'model': 'LS1', 'name': 'Koogeek-LS1-20833F', 'name_by_user': None, + 'primary_integration': 'homekit_controller', 'serial_number': 'AAAA011111111111', 'suggested_area': None, 'sw_version': '2.2.15', @@ -12864,6 +12933,7 @@ 'model': 'P1EU', 'name': 'Koogeek-P1-A00AA0', 'name_by_user': None, + 'primary_integration': 'homekit_controller', 'serial_number': 'EUCP03190xxxxx48', 'suggested_area': None, 'sw_version': '2.3.7', @@ -13027,6 +13097,7 @@ 'model': 'KH02CN', 'name': 'Koogeek-SW2-187A91', 'name_by_user': None, + 'primary_integration': 'homekit_controller', 'serial_number': 'CNNT061751001372', 'suggested_area': None, 'sw_version': '1.0.3', @@ -13229,6 +13300,7 @@ 'model': 'E30 2B', 'name': 'Lennox', 'name_by_user': None, + 'primary_integration': 'homekit_controller', 'serial_number': 'XXXXXXXX', 'suggested_area': None, 'sw_version': '3.40.XX', @@ -13509,6 +13581,7 @@ 'model': 'OLED55B9PUA', 'name': 'LG webOS TV AF80', 'name_by_user': None, + 'primary_integration': 'homekit_controller', 'serial_number': '999AAAAAA999', 'suggested_area': None, 'sw_version': '04.71.04', @@ -13688,6 +13761,7 @@ 'model': 'PD-FSQN-XX', 'name': 'Caséta® Wireless Fan Speed Control', 'name_by_user': None, + 'primary_integration': 'homekit_controller', 'serial_number': '39024290', 'suggested_area': None, 'sw_version': '001.005', @@ -13808,6 +13882,7 @@ 'model': 'L-BDG2-WH', 'name': 'Smart Bridge 2', 'name_by_user': None, + 'primary_integration': 'homekit_controller', 'serial_number': '12344331', 'suggested_area': None, 'sw_version': '08.08', @@ -13885,6 +13960,7 @@ 'model': 'MSS425F', 'name': 'MSS425F-15cc', 'name_by_user': None, + 'primary_integration': 'homekit_controller', 'serial_number': 'HH41234', 'suggested_area': None, 'sw_version': '4.2.3', @@ -14162,6 +14238,7 @@ 'model': 'MSS565', 'name': 'MSS565-28da', 'name_by_user': None, + 'primary_integration': 'homekit_controller', 'serial_number': 'BB1121', 'suggested_area': None, 'sw_version': '4.1.9', @@ -14289,6 +14366,7 @@ 'model': 'v1', 'name': 'Mysa-85dda9', 'name_by_user': None, + 'primary_integration': 'homekit_controller', 'serial_number': 'AAAAAAA000', 'suggested_area': None, 'sw_version': '2.8.1', @@ -14617,6 +14695,7 @@ 'model': 'NL55', 'name': 'Nanoleaf Strip 3B32', 'name_by_user': None, + 'primary_integration': 'homekit_controller', 'serial_number': 'AAAA011111111111', 'suggested_area': None, 'sw_version': '1.4.40', @@ -14887,6 +14966,7 @@ 'model': 'Netatmo Doorbell', 'name': 'Netatmo-Doorbell-g738658', 'name_by_user': None, + 'primary_integration': 'homekit_controller', 'serial_number': 'g738658', 'suggested_area': None, 'sw_version': '80.0.0', @@ -15179,6 +15259,7 @@ 'model': 'Smart CO Alarm', 'name': 'Smart CO Alarm', 'name_by_user': None, + 'primary_integration': 'homekit_controller', 'serial_number': '1234', 'suggested_area': None, 'sw_version': '1.0.3', @@ -15338,6 +15419,7 @@ 'model': 'Healthy Home Coach', 'name': 'Healthy Home Coach', 'name_by_user': None, + 'primary_integration': 'homekit_controller', 'serial_number': 'AAAAAAAAAAAAA', 'suggested_area': None, 'sw_version': '59', @@ -15639,6 +15721,7 @@ 'model': 'SPK5 Pro', 'name': 'RainMachine-00ce4a', 'name_by_user': None, + 'primary_integration': 'homekit_controller', 'serial_number': '00aa0000aa0a', 'suggested_area': None, 'sw_version': '1.0.4', @@ -16060,6 +16143,7 @@ 'model': 'RYSE Shade', 'name': 'Master Bath South', 'name_by_user': None, + 'primary_integration': 'homekit_controller', 'serial_number': '1.0.0', 'suggested_area': None, 'sw_version': '3.0.8', @@ -16221,6 +16305,7 @@ 'model': 'RYSE SmartBridge', 'name': 'RYSE SmartBridge', 'name_by_user': None, + 'primary_integration': 'homekit_controller', 'serial_number': '0101.3521.0436', 'suggested_area': None, 'sw_version': '1.3.0', @@ -16294,6 +16379,7 @@ 'model': 'RYSE Shade', 'name': 'RYSE SmartShade', 'name_by_user': None, + 'primary_integration': 'homekit_controller', 'serial_number': '', 'suggested_area': None, 'sw_version': '', @@ -16459,6 +16545,7 @@ 'model': 'RYSE Shade', 'name': 'BR Left', 'name_by_user': None, + 'primary_integration': 'homekit_controller', 'serial_number': '1.0.0', 'suggested_area': None, 'sw_version': '3.0.8', @@ -16620,6 +16707,7 @@ 'model': 'RYSE Shade', 'name': 'LR Left', 'name_by_user': None, + 'primary_integration': 'homekit_controller', 'serial_number': '1.0.0', 'suggested_area': None, 'sw_version': '3.0.8', @@ -16781,6 +16869,7 @@ 'model': 'RYSE Shade', 'name': 'LR Right', 'name_by_user': None, + 'primary_integration': 'homekit_controller', 'serial_number': '1.0.0', 'suggested_area': None, 'sw_version': '3.0.8', @@ -16942,6 +17031,7 @@ 'model': 'RYSE SmartBridge', 'name': 'RYSE SmartBridge', 'name_by_user': None, + 'primary_integration': 'homekit_controller', 'serial_number': '0401.3521.0679', 'suggested_area': None, 'sw_version': '1.3.0', @@ -17015,6 +17105,7 @@ 'model': 'RYSE Shade', 'name': 'RZSS', 'name_by_user': None, + 'primary_integration': 'homekit_controller', 'serial_number': '1.0.0', 'suggested_area': None, 'sw_version': '3.0.8', @@ -17180,6 +17271,7 @@ 'model': 'BE479CAM619', 'name': 'SENSE ', 'name_by_user': None, + 'primary_integration': 'homekit_controller', 'serial_number': 'AAAAAAA000', 'suggested_area': None, 'sw_version': '004.027.000', @@ -17298,6 +17390,7 @@ 'model': 'SIMPLEconnect', 'name': 'SIMPLEconnect Fan-06F674', 'name_by_user': None, + 'primary_integration': 'homekit_controller', 'serial_number': '1234567890abcd', 'suggested_area': None, 'sw_version': '', @@ -17473,6 +17566,7 @@ 'model': 'VELUX Gateway', 'name': 'VELUX Gateway', 'name_by_user': None, + 'primary_integration': 'homekit_controller', 'serial_number': 'a1a11a1', 'suggested_area': None, 'sw_version': '70', @@ -17546,6 +17640,7 @@ 'model': 'VELUX Sensor', 'name': 'VELUX Sensor', 'name_by_user': None, + 'primary_integration': 'homekit_controller', 'serial_number': 'a11b111', 'suggested_area': None, 'sw_version': '16', @@ -17754,6 +17849,7 @@ 'model': 'VELUX Window', 'name': 'VELUX Window', 'name_by_user': None, + 'primary_integration': 'homekit_controller', 'serial_number': '1111111a114a111a', 'suggested_area': None, 'sw_version': '48', @@ -17874,6 +17970,7 @@ 'model': 'Flowerbud', 'name': 'VOCOlinc-Flowerbud-0d324b', 'name_by_user': None, + 'primary_integration': 'homekit_controller', 'serial_number': 'AM01121849000327', 'suggested_area': None, 'sw_version': '3.121.2', @@ -18178,6 +18275,7 @@ 'model': 'VP3', 'name': 'VOCOlinc-VP3-123456', 'name_by_user': None, + 'primary_integration': 'homekit_controller', 'serial_number': 'EU0121203xxxxx07', 'suggested_area': None, 'sw_version': '1.101.2', diff --git a/tests/components/homewizard/snapshots/test_button.ambr b/tests/components/homewizard/snapshots/test_button.ambr index 5ab108d344c..47b6a889900 100644 --- a/tests/components/homewizard/snapshots/test_button.ambr +++ b/tests/components/homewizard/snapshots/test_button.ambr @@ -74,6 +74,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', diff --git a/tests/components/homewizard/snapshots/test_number.ambr b/tests/components/homewizard/snapshots/test_number.ambr index a9c9e45098d..ff1f22a4336 100644 --- a/tests/components/homewizard/snapshots/test_number.ambr +++ b/tests/components/homewizard/snapshots/test_number.ambr @@ -83,6 +83,7 @@ 'model': 'HWE-SKT', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.03', @@ -173,6 +174,7 @@ 'model': 'HWE-SKT', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.07', diff --git a/tests/components/homewizard/snapshots/test_sensor.ambr b/tests/components/homewizard/snapshots/test_sensor.ambr index 5e8ddc0d6be..7f402cd7872 100644 --- a/tests/components/homewizard/snapshots/test_sensor.ambr +++ b/tests/components/homewizard/snapshots/test_sensor.ambr @@ -60,6 +60,7 @@ 'model': 'HWE-KWH1', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -145,6 +146,7 @@ 'model': 'HWE-KWH1', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -230,6 +232,7 @@ 'model': 'HWE-KWH1', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -315,6 +318,7 @@ 'model': 'HWE-KWH1', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -400,6 +404,7 @@ 'model': 'HWE-KWH1', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -485,6 +490,7 @@ 'model': 'HWE-KWH1', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -573,6 +579,7 @@ 'model': 'HWE-KWH1', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -658,6 +665,7 @@ 'model': 'HWE-KWH1', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -743,6 +751,7 @@ 'model': 'HWE-KWH1', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -828,6 +837,7 @@ 'model': 'HWE-KWH1', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -908,6 +918,7 @@ 'model': 'HWE-KWH1', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -992,6 +1003,7 @@ 'model': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -1077,6 +1089,7 @@ 'model': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -1162,6 +1175,7 @@ 'model': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -1247,6 +1261,7 @@ 'model': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -1332,6 +1347,7 @@ 'model': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -1417,6 +1433,7 @@ 'model': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -1502,6 +1519,7 @@ 'model': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -1587,6 +1605,7 @@ 'model': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -1672,6 +1691,7 @@ 'model': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -1757,6 +1777,7 @@ 'model': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -1842,6 +1863,7 @@ 'model': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -1927,6 +1949,7 @@ 'model': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -2015,6 +2038,7 @@ 'model': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -2100,6 +2124,7 @@ 'model': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -2185,6 +2210,7 @@ 'model': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -2270,6 +2296,7 @@ 'model': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -2358,6 +2385,7 @@ 'model': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -2446,6 +2474,7 @@ 'model': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -2534,6 +2563,7 @@ 'model': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -2619,6 +2649,7 @@ 'model': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -2704,6 +2735,7 @@ 'model': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -2789,6 +2821,7 @@ 'model': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -2874,6 +2907,7 @@ 'model': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -2959,6 +2993,7 @@ 'model': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -3044,6 +3079,7 @@ 'model': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -3129,6 +3165,7 @@ 'model': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -3209,6 +3246,7 @@ 'model': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -3293,6 +3331,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -3375,6 +3414,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -3460,6 +3500,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -3545,6 +3586,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -3630,6 +3672,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -3710,6 +3753,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -3795,6 +3839,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -3880,6 +3925,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -3965,6 +4011,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -4050,6 +4097,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -4135,6 +4183,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -4220,6 +4269,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -4305,6 +4355,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -4390,6 +4441,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -4475,6 +4527,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -4560,6 +4613,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -4645,6 +4699,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -4725,6 +4780,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -4807,6 +4863,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -4895,6 +4952,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -4975,6 +5033,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -5063,6 +5122,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -5151,6 +5211,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -5239,6 +5300,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -5319,6 +5381,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -5399,6 +5462,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -5493,6 +5557,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -5578,6 +5643,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -5663,6 +5729,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -5748,6 +5815,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -5833,6 +5901,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -5913,6 +5982,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -5993,6 +6063,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -6073,6 +6144,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -6153,6 +6225,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -6233,6 +6306,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -6313,6 +6387,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -6397,6 +6472,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -6477,6 +6553,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -6557,6 +6634,7 @@ 'model': 'HWE-P1', 'name': 'Gas meter', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': 'gas_meter_G001', 'suggested_area': None, 'sw_version': None, @@ -6638,6 +6716,7 @@ 'model': 'HWE-P1', 'name': 'Heat meter', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': 'heat_meter_H001', 'suggested_area': None, 'sw_version': None, @@ -6719,6 +6798,7 @@ 'model': 'HWE-P1', 'name': 'Inlet heat meter', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': 'inlet_heat_meter_IH001', 'suggested_area': None, 'sw_version': None, @@ -6799,6 +6879,7 @@ 'model': 'HWE-P1', 'name': 'Warm water meter', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': 'warm_water_meter_WW001', 'suggested_area': None, 'sw_version': None, @@ -6880,6 +6961,7 @@ 'model': 'HWE-P1', 'name': 'Water meter', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': 'water_meter_W001', 'suggested_area': None, 'sw_version': None, @@ -6965,6 +7047,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -7047,6 +7130,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -7132,6 +7216,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -7217,6 +7302,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -7302,6 +7388,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -7382,6 +7469,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -7467,6 +7555,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -7552,6 +7641,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -7637,6 +7727,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -7722,6 +7813,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -7807,6 +7899,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -7892,6 +7985,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -7977,6 +8071,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -8062,6 +8157,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -8147,6 +8243,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -8232,6 +8329,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -8317,6 +8415,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -8397,6 +8496,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -8479,6 +8579,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -8567,6 +8668,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -8647,6 +8749,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -8735,6 +8838,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -8823,6 +8927,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -8911,6 +9016,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -8991,6 +9097,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -9071,6 +9178,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -9165,6 +9273,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -9250,6 +9359,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -9335,6 +9445,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -9420,6 +9531,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -9505,6 +9617,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -9585,6 +9698,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -9665,6 +9779,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -9745,6 +9860,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -9825,6 +9941,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -9905,6 +10022,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -9985,6 +10103,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -10069,6 +10188,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -10149,6 +10269,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -10229,6 +10350,7 @@ 'model': 'HWE-P1', 'name': 'Gas meter', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': 'gas_meter_\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', 'suggested_area': None, 'sw_version': None, @@ -10310,6 +10432,7 @@ 'model': 'HWE-P1', 'name': 'Heat meter', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': 'heat_meter_\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', 'suggested_area': None, 'sw_version': None, @@ -10391,6 +10514,7 @@ 'model': 'HWE-P1', 'name': 'Inlet heat meter', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': 'inlet_heat_meter_\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', 'suggested_area': None, 'sw_version': None, @@ -10471,6 +10595,7 @@ 'model': 'HWE-P1', 'name': 'Warm water meter', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': 'warm_water_meter_\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', 'suggested_area': None, 'sw_version': None, @@ -10552,6 +10677,7 @@ 'model': 'HWE-P1', 'name': 'Water meter', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': 'water_meter_\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', 'suggested_area': None, 'sw_version': None, @@ -10637,6 +10763,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -10719,6 +10846,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -10804,6 +10932,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -10889,6 +11018,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -10974,6 +11104,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -11059,6 +11190,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -11144,6 +11276,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -11229,6 +11362,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -11314,6 +11448,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -11399,6 +11534,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -11484,6 +11620,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -11569,6 +11706,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -11654,6 +11792,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -11739,6 +11878,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -11824,6 +11964,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -11909,6 +12050,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -11989,6 +12131,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -12077,6 +12220,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -12157,6 +12301,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -12245,6 +12390,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -12333,6 +12479,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -12421,6 +12568,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -12506,6 +12654,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -12591,6 +12740,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -12676,6 +12826,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -12761,6 +12912,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -12841,6 +12993,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -12921,6 +13074,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -13001,6 +13155,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -13081,6 +13236,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -13161,6 +13317,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -13241,6 +13398,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -13325,6 +13483,7 @@ 'model': 'HWE-SKT', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.03', @@ -13410,6 +13569,7 @@ 'model': 'HWE-SKT', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.03', @@ -13495,6 +13655,7 @@ 'model': 'HWE-SKT', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.03', @@ -13583,6 +13744,7 @@ 'model': 'HWE-SKT', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.03', @@ -13671,6 +13833,7 @@ 'model': 'HWE-SKT', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.03', @@ -13751,6 +13914,7 @@ 'model': 'HWE-SKT', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.03', @@ -13835,6 +13999,7 @@ 'model': 'HWE-SKT', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.07', @@ -13920,6 +14085,7 @@ 'model': 'HWE-SKT', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.07', @@ -14005,6 +14171,7 @@ 'model': 'HWE-SKT', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.07', @@ -14090,6 +14257,7 @@ 'model': 'HWE-SKT', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.07', @@ -14175,6 +14343,7 @@ 'model': 'HWE-SKT', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.07', @@ -14260,6 +14429,7 @@ 'model': 'HWE-SKT', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.07', @@ -14348,6 +14518,7 @@ 'model': 'HWE-SKT', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.07', @@ -14433,6 +14604,7 @@ 'model': 'HWE-SKT', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.07', @@ -14521,6 +14693,7 @@ 'model': 'HWE-SKT', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.07', @@ -14606,6 +14779,7 @@ 'model': 'HWE-SKT', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.07', @@ -14691,6 +14865,7 @@ 'model': 'HWE-SKT', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.07', @@ -14771,6 +14946,7 @@ 'model': 'HWE-SKT', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.07', @@ -14855,6 +15031,7 @@ 'model': 'HWE-WTR', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '2.03', @@ -14940,6 +15117,7 @@ 'model': 'HWE-WTR', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '2.03', @@ -15024,6 +15202,7 @@ 'model': 'HWE-WTR', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '2.03', @@ -15104,6 +15283,7 @@ 'model': 'HWE-WTR', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '2.03', @@ -15188,6 +15368,7 @@ 'model': 'SDM230-wifi', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -15273,6 +15454,7 @@ 'model': 'SDM230-wifi', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -15358,6 +15540,7 @@ 'model': 'SDM230-wifi', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -15443,6 +15626,7 @@ 'model': 'SDM230-wifi', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -15528,6 +15712,7 @@ 'model': 'SDM230-wifi', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -15613,6 +15798,7 @@ 'model': 'SDM230-wifi', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -15701,6 +15887,7 @@ 'model': 'SDM230-wifi', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -15786,6 +15973,7 @@ 'model': 'SDM230-wifi', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -15871,6 +16059,7 @@ 'model': 'SDM230-wifi', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -15956,6 +16145,7 @@ 'model': 'SDM230-wifi', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -16036,6 +16226,7 @@ 'model': 'SDM230-wifi', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -16120,6 +16311,7 @@ 'model': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -16205,6 +16397,7 @@ 'model': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -16290,6 +16483,7 @@ 'model': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -16375,6 +16569,7 @@ 'model': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -16460,6 +16655,7 @@ 'model': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -16545,6 +16741,7 @@ 'model': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -16630,6 +16827,7 @@ 'model': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -16715,6 +16913,7 @@ 'model': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -16800,6 +16999,7 @@ 'model': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -16885,6 +17085,7 @@ 'model': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -16970,6 +17171,7 @@ 'model': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -17055,6 +17257,7 @@ 'model': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -17143,6 +17346,7 @@ 'model': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -17228,6 +17432,7 @@ 'model': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -17313,6 +17518,7 @@ 'model': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -17398,6 +17604,7 @@ 'model': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -17486,6 +17693,7 @@ 'model': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -17574,6 +17782,7 @@ 'model': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -17662,6 +17871,7 @@ 'model': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -17747,6 +17957,7 @@ 'model': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -17832,6 +18043,7 @@ 'model': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -17917,6 +18129,7 @@ 'model': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -18002,6 +18215,7 @@ 'model': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -18087,6 +18301,7 @@ 'model': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -18172,6 +18387,7 @@ 'model': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -18257,6 +18473,7 @@ 'model': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -18337,6 +18554,7 @@ 'model': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', diff --git a/tests/components/homewizard/snapshots/test_switch.ambr b/tests/components/homewizard/snapshots/test_switch.ambr index 99a5bcab6cb..2834999a9ba 100644 --- a/tests/components/homewizard/snapshots/test_switch.ambr +++ b/tests/components/homewizard/snapshots/test_switch.ambr @@ -73,6 +73,7 @@ 'model': 'HWE-KWH1', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -153,6 +154,7 @@ 'model': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -234,6 +236,7 @@ 'model': 'HWE-SKT', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.03', @@ -314,6 +317,7 @@ 'model': 'HWE-SKT', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.03', @@ -394,6 +398,7 @@ 'model': 'HWE-SKT', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.03', @@ -475,6 +480,7 @@ 'model': 'HWE-SKT', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.07', @@ -555,6 +561,7 @@ 'model': 'HWE-SKT', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.07', @@ -635,6 +642,7 @@ 'model': 'HWE-SKT', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.07', @@ -715,6 +723,7 @@ 'model': 'HWE-WTR', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '2.03', @@ -795,6 +804,7 @@ 'model': 'SDM230-wifi', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -875,6 +885,7 @@ 'model': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, + 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', diff --git a/tests/components/husqvarna_automower/snapshots/test_init.ambr b/tests/components/husqvarna_automower/snapshots/test_init.ambr index c3a7191b4b9..07cab28b24e 100644 --- a/tests/components/husqvarna_automower/snapshots/test_init.ambr +++ b/tests/components/husqvarna_automower/snapshots/test_init.ambr @@ -23,6 +23,7 @@ 'model': '450XH-TEST', 'name': 'Test Mower 1', 'name_by_user': None, + 'primary_integration': 'husqvarna_automower', 'serial_number': 123, 'suggested_area': 'Garden', 'sw_version': None, diff --git a/tests/components/husqvarna_automower/snapshots/test_sensor.ambr b/tests/components/husqvarna_automower/snapshots/test_sensor.ambr index 6cb74ab8814..0b0d76620d3 100644 --- a/tests/components/husqvarna_automower/snapshots/test_sensor.ambr +++ b/tests/components/husqvarna_automower/snapshots/test_sensor.ambr @@ -551,6 +551,64 @@ 'state': '2023-06-05T19:00:00+00:00', }) # --- +# name: test_sensor_snapshot[sensor.test_mower_1_none-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'Front lawn', + 'Back lawn', + 'my_lawn', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.test_mower_1_none', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': None, + 'platform': 'husqvarna_automower', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'work_area', + 'unique_id': 'c7233734-b219-4287-a173-08e3643f89f0_work_area', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor_snapshot[sensor.test_mower_1_none-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Test Mower 1 None', + 'options': list([ + 'Front lawn', + 'Back lawn', + 'my_lawn', + ]), + }), + 'context': , + 'entity_id': 'sensor.test_mower_1_none', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'Front lawn', + }) +# --- # name: test_sensor_snapshot[sensor.test_mower_1_number_of_charging_cycles-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/ista_ecotrend/snapshots/test_init.ambr b/tests/components/ista_ecotrend/snapshots/test_init.ambr index a9d13510b54..7cc44872071 100644 --- a/tests/components/ista_ecotrend/snapshots/test_init.ambr +++ b/tests/components/ista_ecotrend/snapshots/test_init.ambr @@ -23,6 +23,7 @@ 'model': 'ista EcoTrend', 'name': 'Luxemburger Str. 1', 'name_by_user': None, + 'primary_integration': 'ista_ecotrend', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -53,6 +54,7 @@ 'model': 'ista EcoTrend', 'name': 'Bahnhofsstr. 1A', 'name_by_user': None, + 'primary_integration': 'ista_ecotrend', 'serial_number': None, 'suggested_area': None, 'sw_version': None, diff --git a/tests/components/kitchen_sink/snapshots/test_switch.ambr b/tests/components/kitchen_sink/snapshots/test_switch.ambr index 1cd903a59d6..2f928ddc430 100644 --- a/tests/components/kitchen_sink/snapshots/test_switch.ambr +++ b/tests/components/kitchen_sink/snapshots/test_switch.ambr @@ -69,6 +69,7 @@ 'model': None, 'name': 'Outlet 1', 'name_by_user': None, + 'primary_integration': 'kitchen_sink', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -99,6 +100,7 @@ 'model': None, 'name': 'Power strip with 2 sockets', 'name_by_user': None, + 'primary_integration': None, 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -175,6 +177,7 @@ 'model': None, 'name': 'Outlet 2', 'name_by_user': None, + 'primary_integration': 'kitchen_sink', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -205,6 +208,7 @@ 'model': None, 'name': 'Power strip with 2 sockets', 'name_by_user': None, + 'primary_integration': None, 'serial_number': None, 'suggested_area': None, 'sw_version': None, diff --git a/tests/components/lamarzocco/snapshots/test_switch.ambr b/tests/components/lamarzocco/snapshots/test_switch.ambr index 09864be1d5c..162fade77d6 100644 --- a/tests/components/lamarzocco/snapshots/test_switch.ambr +++ b/tests/components/lamarzocco/snapshots/test_switch.ambr @@ -115,6 +115,7 @@ 'model': , 'name': 'GS01234', 'name_by_user': None, + 'primary_integration': 'lamarzocco', 'serial_number': 'GS01234', 'suggested_area': None, 'sw_version': '1.40', diff --git a/tests/components/netatmo/snapshots/test_init.ambr b/tests/components/netatmo/snapshots/test_init.ambr index 8f4b357fc5f..f844e05e94b 100644 --- a/tests/components/netatmo/snapshots/test_init.ambr +++ b/tests/components/netatmo/snapshots/test_init.ambr @@ -23,6 +23,7 @@ 'model': 'Roller Shutter', 'name': 'Entrance Blinds', 'name_by_user': None, + 'primary_integration': 'netatmo', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -53,6 +54,7 @@ 'model': 'Orientable Shutter', 'name': 'Bubendorff blind', 'name_by_user': None, + 'primary_integration': 'netatmo', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -83,6 +85,7 @@ 'model': '2 wire light switch/dimmer', 'name': 'Unknown 00:11:22:33:00:11:45:fe', 'name_by_user': None, + 'primary_integration': 'netatmo', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -113,6 +116,7 @@ 'model': 'Smarther with Netatmo', 'name': 'Corridor', 'name_by_user': None, + 'primary_integration': 'netatmo', 'serial_number': None, 'suggested_area': 'Corridor', 'sw_version': None, @@ -143,6 +147,7 @@ 'model': 'Connected Energy Meter', 'name': 'Consumption meter', 'name_by_user': None, + 'primary_integration': 'netatmo', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -173,6 +178,7 @@ 'model': 'Light switch/dimmer with neutral', 'name': 'Bathroom light', 'name_by_user': None, + 'primary_integration': 'netatmo', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -203,6 +209,7 @@ 'model': 'Connected Ecometer', 'name': 'Line 1', 'name_by_user': None, + 'primary_integration': 'netatmo', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -233,6 +240,7 @@ 'model': 'Connected Ecometer', 'name': 'Line 2', 'name_by_user': None, + 'primary_integration': 'netatmo', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -263,6 +271,7 @@ 'model': 'Connected Ecometer', 'name': 'Line 3', 'name_by_user': None, + 'primary_integration': 'netatmo', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -293,6 +302,7 @@ 'model': 'Connected Ecometer', 'name': 'Line 4', 'name_by_user': None, + 'primary_integration': 'netatmo', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -323,6 +333,7 @@ 'model': 'Connected Ecometer', 'name': 'Line 5', 'name_by_user': None, + 'primary_integration': 'netatmo', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -353,6 +364,7 @@ 'model': 'Connected Ecometer', 'name': 'Total', 'name_by_user': None, + 'primary_integration': 'netatmo', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -383,6 +395,7 @@ 'model': 'Connected Ecometer', 'name': 'Gas', 'name_by_user': None, + 'primary_integration': 'netatmo', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -413,6 +426,7 @@ 'model': 'Connected Ecometer', 'name': 'Hot water', 'name_by_user': None, + 'primary_integration': 'netatmo', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -443,6 +457,7 @@ 'model': 'Connected Ecometer', 'name': 'Cold water', 'name_by_user': None, + 'primary_integration': 'netatmo', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -473,6 +488,7 @@ 'model': 'Connected Ecometer', 'name': 'Écocompteur', 'name_by_user': None, + 'primary_integration': 'netatmo', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -503,6 +519,7 @@ 'model': 'Smart Indoor Camera', 'name': 'Hall', 'name_by_user': None, + 'primary_integration': 'netatmo', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -533,6 +550,7 @@ 'model': 'Smart Anemometer', 'name': 'Villa Garden', 'name_by_user': None, + 'primary_integration': 'netatmo', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -563,6 +581,7 @@ 'model': 'Smart Outdoor Camera', 'name': 'Front', 'name_by_user': None, + 'primary_integration': 'netatmo', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -593,6 +612,7 @@ 'model': 'Smart Video Doorbell', 'name': 'Netatmo-Doorbell', 'name_by_user': None, + 'primary_integration': 'netatmo', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -623,6 +643,7 @@ 'model': 'Smart Indoor Air Quality Monitor', 'name': 'Kitchen', 'name_by_user': None, + 'primary_integration': 'netatmo', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -653,6 +674,7 @@ 'model': 'Smart Indoor Air Quality Monitor', 'name': 'Livingroom', 'name_by_user': None, + 'primary_integration': 'netatmo', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -683,6 +705,7 @@ 'model': 'Smart Indoor Air Quality Monitor', 'name': 'Baby Bedroom', 'name_by_user': None, + 'primary_integration': 'netatmo', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -713,6 +736,7 @@ 'model': 'Smart Indoor Air Quality Monitor', 'name': 'Bedroom', 'name_by_user': None, + 'primary_integration': 'netatmo', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -743,6 +767,7 @@ 'model': 'Smart Indoor Air Quality Monitor', 'name': 'Parents Bedroom', 'name_by_user': None, + 'primary_integration': 'netatmo', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -773,6 +798,7 @@ 'model': 'Plug', 'name': 'Prise', 'name_by_user': None, + 'primary_integration': 'netatmo', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -803,6 +829,7 @@ 'model': 'Smart Outdoor Module', 'name': 'Villa Outdoor', 'name_by_user': None, + 'primary_integration': 'netatmo', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -833,6 +860,7 @@ 'model': 'Smart Indoor Module', 'name': 'Villa Bedroom', 'name_by_user': None, + 'primary_integration': 'netatmo', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -863,6 +891,7 @@ 'model': 'Smart Indoor Module', 'name': 'Villa Bathroom', 'name_by_user': None, + 'primary_integration': 'netatmo', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -893,6 +922,7 @@ 'model': 'Smart Home Weather station', 'name': 'Villa', 'name_by_user': None, + 'primary_integration': 'netatmo', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -923,6 +953,7 @@ 'model': 'Smart Rain Gauge', 'name': 'Villa Rain', 'name_by_user': None, + 'primary_integration': 'netatmo', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -953,6 +984,7 @@ 'model': 'OpenTherm Modulating Thermostat', 'name': 'Bureau Modulate', 'name_by_user': None, + 'primary_integration': 'netatmo', 'serial_number': None, 'suggested_area': 'Bureau', 'sw_version': None, @@ -983,6 +1015,7 @@ 'model': 'Smart Thermostat', 'name': 'Livingroom', 'name_by_user': None, + 'primary_integration': 'netatmo', 'serial_number': None, 'suggested_area': 'Livingroom', 'sw_version': None, @@ -1013,6 +1046,7 @@ 'model': 'Smart Valve', 'name': 'Valve1', 'name_by_user': None, + 'primary_integration': 'netatmo', 'serial_number': None, 'suggested_area': 'Entrada', 'sw_version': None, @@ -1043,6 +1077,7 @@ 'model': 'Smart Valve', 'name': 'Valve2', 'name_by_user': None, + 'primary_integration': 'netatmo', 'serial_number': None, 'suggested_area': 'Cocina', 'sw_version': None, @@ -1073,6 +1108,7 @@ 'model': 'Climate', 'name': 'MYHOME', 'name_by_user': None, + 'primary_integration': 'netatmo', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -1103,6 +1139,7 @@ 'model': 'Public Weather station', 'name': 'Home avg', 'name_by_user': None, + 'primary_integration': 'netatmo', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -1133,6 +1170,7 @@ 'model': 'Public Weather station', 'name': 'Home max', 'name_by_user': None, + 'primary_integration': 'netatmo', 'serial_number': None, 'suggested_area': None, 'sw_version': None, diff --git a/tests/components/netgear_lte/snapshots/test_init.ambr b/tests/components/netgear_lte/snapshots/test_init.ambr index 8af22f98e02..e51fc937081 100644 --- a/tests/components/netgear_lte/snapshots/test_init.ambr +++ b/tests/components/netgear_lte/snapshots/test_init.ambr @@ -23,6 +23,7 @@ 'model': 'LM1200', 'name': 'Netgear LM1200', 'name_by_user': None, + 'primary_integration': 'netgear_lte', 'serial_number': 'FFFFFFFFFFFFF', 'suggested_area': None, 'sw_version': 'EC25AFFDR07A09M4G', diff --git a/tests/components/ondilo_ico/snapshots/test_init.ambr b/tests/components/ondilo_ico/snapshots/test_init.ambr index c488b1e3c15..0bf4748cfdd 100644 --- a/tests/components/ondilo_ico/snapshots/test_init.ambr +++ b/tests/components/ondilo_ico/snapshots/test_init.ambr @@ -23,6 +23,7 @@ 'model': 'ICO', 'name': 'Pool 1', 'name_by_user': None, + 'primary_integration': 'ondilo_ico', 'serial_number': None, 'suggested_area': None, 'sw_version': '1.7.1-stable', @@ -53,6 +54,7 @@ 'model': 'ICO', 'name': 'Pool 2', 'name_by_user': None, + 'primary_integration': 'ondilo_ico', 'serial_number': None, 'suggested_area': None, 'sw_version': '1.7.1-stable', diff --git a/tests/components/onewire/snapshots/test_binary_sensor.ambr b/tests/components/onewire/snapshots/test_binary_sensor.ambr index 999794ec20d..febb0e50355 100644 --- a/tests/components/onewire/snapshots/test_binary_sensor.ambr +++ b/tests/components/onewire/snapshots/test_binary_sensor.ambr @@ -36,6 +36,7 @@ 'model': 'DS2405', 'name': '05.111111111111', 'name_by_user': None, + 'primary_integration': None, 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -76,6 +77,7 @@ 'model': 'DS18S20', 'name': '10.111111111111', 'name_by_user': None, + 'primary_integration': None, 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -116,6 +118,7 @@ 'model': 'DS2406', 'name': '12.111111111111', 'name_by_user': None, + 'primary_integration': 'onewire', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -256,6 +259,7 @@ 'model': 'DS2423', 'name': '1D.111111111111', 'name_by_user': None, + 'primary_integration': None, 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -296,6 +300,7 @@ 'model': 'DS2409', 'name': '1F.111111111111', 'name_by_user': None, + 'primary_integration': None, 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -324,6 +329,7 @@ 'model': 'DS2423', 'name': '1D.111111111111', 'name_by_user': None, + 'primary_integration': None, 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -364,6 +370,7 @@ 'model': 'DS1822', 'name': '22.111111111111', 'name_by_user': None, + 'primary_integration': None, 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -404,6 +411,7 @@ 'model': 'DS2438', 'name': '26.111111111111', 'name_by_user': None, + 'primary_integration': None, 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -444,6 +452,7 @@ 'model': 'DS18B20', 'name': '28.111111111111', 'name_by_user': None, + 'primary_integration': None, 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -484,6 +493,7 @@ 'model': 'DS18B20', 'name': '28.222222222222', 'name_by_user': None, + 'primary_integration': None, 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -524,6 +534,7 @@ 'model': 'DS18B20', 'name': '28.222222222223', 'name_by_user': None, + 'primary_integration': None, 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -564,6 +575,7 @@ 'model': 'DS2408', 'name': '29.111111111111', 'name_by_user': None, + 'primary_integration': 'onewire', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -956,6 +968,7 @@ 'model': 'DS2760', 'name': '30.111111111111', 'name_by_user': None, + 'primary_integration': None, 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -996,6 +1009,7 @@ 'model': 'DS2413', 'name': '3A.111111111111', 'name_by_user': None, + 'primary_integration': 'onewire', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -1124,6 +1138,7 @@ 'model': 'DS1825', 'name': '3B.111111111111', 'name_by_user': None, + 'primary_integration': None, 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -1164,6 +1179,7 @@ 'model': 'DS28EA00', 'name': '42.111111111111', 'name_by_user': None, + 'primary_integration': None, 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -1204,6 +1220,7 @@ 'model': 'EDS0068', 'name': '7E.111111111111', 'name_by_user': None, + 'primary_integration': None, 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -1244,6 +1261,7 @@ 'model': 'EDS0066', 'name': '7E.222222222222', 'name_by_user': None, + 'primary_integration': None, 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -1284,6 +1302,7 @@ 'model': 'DS2438', 'name': 'A6.111111111111', 'name_by_user': None, + 'primary_integration': None, 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -1324,6 +1343,7 @@ 'model': 'HobbyBoards_EF', 'name': 'EF.111111111111', 'name_by_user': None, + 'primary_integration': None, 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -1364,6 +1384,7 @@ 'model': 'HB_MOISTURE_METER', 'name': 'EF.111111111112', 'name_by_user': None, + 'primary_integration': None, 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -1404,6 +1425,7 @@ 'model': 'HB_HUB', 'name': 'EF.111111111113', 'name_by_user': None, + 'primary_integration': 'onewire', 'serial_number': None, 'suggested_area': None, 'sw_version': None, diff --git a/tests/components/onewire/snapshots/test_sensor.ambr b/tests/components/onewire/snapshots/test_sensor.ambr index 59ed167197d..ffa7dadb6fe 100644 --- a/tests/components/onewire/snapshots/test_sensor.ambr +++ b/tests/components/onewire/snapshots/test_sensor.ambr @@ -36,6 +36,7 @@ 'model': 'DS2405', 'name': '05.111111111111', 'name_by_user': None, + 'primary_integration': None, 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -76,6 +77,7 @@ 'model': 'DS18S20', 'name': '10.111111111111', 'name_by_user': None, + 'primary_integration': 'onewire', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -165,6 +167,7 @@ 'model': 'DS2406', 'name': '12.111111111111', 'name_by_user': None, + 'primary_integration': 'onewire', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -315,6 +318,7 @@ 'model': 'DS2423', 'name': '1D.111111111111', 'name_by_user': None, + 'primary_integration': 'onewire', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -451,6 +455,7 @@ 'model': 'DS2409', 'name': '1F.111111111111', 'name_by_user': None, + 'primary_integration': None, 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -479,6 +484,7 @@ 'model': 'DS2423', 'name': '1D.111111111111', 'name_by_user': None, + 'primary_integration': 'onewire', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -615,6 +621,7 @@ 'model': 'DS1822', 'name': '22.111111111111', 'name_by_user': None, + 'primary_integration': 'onewire', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -704,6 +711,7 @@ 'model': 'DS2438', 'name': '26.111111111111', 'name_by_user': None, + 'primary_integration': 'onewire', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -1283,6 +1291,7 @@ 'model': 'DS18B20', 'name': '28.111111111111', 'name_by_user': None, + 'primary_integration': 'onewire', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -1372,6 +1381,7 @@ 'model': 'DS18B20', 'name': '28.222222222222', 'name_by_user': None, + 'primary_integration': 'onewire', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -1461,6 +1471,7 @@ 'model': 'DS18B20', 'name': '28.222222222223', 'name_by_user': None, + 'primary_integration': 'onewire', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -1550,6 +1561,7 @@ 'model': 'DS2408', 'name': '29.111111111111', 'name_by_user': None, + 'primary_integration': None, 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -1590,6 +1602,7 @@ 'model': 'DS2760', 'name': '30.111111111111', 'name_by_user': None, + 'primary_integration': 'onewire', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -1826,6 +1839,7 @@ 'model': 'DS2413', 'name': '3A.111111111111', 'name_by_user': None, + 'primary_integration': None, 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -1866,6 +1880,7 @@ 'model': 'DS1825', 'name': '3B.111111111111', 'name_by_user': None, + 'primary_integration': 'onewire', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -1955,6 +1970,7 @@ 'model': 'DS28EA00', 'name': '42.111111111111', 'name_by_user': None, + 'primary_integration': 'onewire', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -2044,6 +2060,7 @@ 'model': 'EDS0068', 'name': '7E.111111111111', 'name_by_user': None, + 'primary_integration': 'onewire', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -2280,6 +2297,7 @@ 'model': 'EDS0066', 'name': '7E.222222222222', 'name_by_user': None, + 'primary_integration': 'onewire', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -2418,6 +2436,7 @@ 'model': 'DS2438', 'name': 'A6.111111111111', 'name_by_user': None, + 'primary_integration': 'onewire', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -2997,6 +3016,7 @@ 'model': 'HobbyBoards_EF', 'name': 'EF.111111111111', 'name_by_user': None, + 'primary_integration': 'onewire', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -3184,6 +3204,7 @@ 'model': 'HB_MOISTURE_METER', 'name': 'EF.111111111112', 'name_by_user': None, + 'primary_integration': 'onewire', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -3420,6 +3441,7 @@ 'model': 'HB_HUB', 'name': 'EF.111111111113', 'name_by_user': None, + 'primary_integration': None, 'serial_number': None, 'suggested_area': None, 'sw_version': None, diff --git a/tests/components/onewire/snapshots/test_switch.ambr b/tests/components/onewire/snapshots/test_switch.ambr index 8fd1e2aeef6..5d736bd9c99 100644 --- a/tests/components/onewire/snapshots/test_switch.ambr +++ b/tests/components/onewire/snapshots/test_switch.ambr @@ -36,6 +36,7 @@ 'model': 'DS2405', 'name': '05.111111111111', 'name_by_user': None, + 'primary_integration': 'onewire', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -120,6 +121,7 @@ 'model': 'DS18S20', 'name': '10.111111111111', 'name_by_user': None, + 'primary_integration': None, 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -160,6 +162,7 @@ 'model': 'DS2406', 'name': '12.111111111111', 'name_by_user': None, + 'primary_integration': 'onewire', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -388,6 +391,7 @@ 'model': 'DS2423', 'name': '1D.111111111111', 'name_by_user': None, + 'primary_integration': None, 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -428,6 +432,7 @@ 'model': 'DS2409', 'name': '1F.111111111111', 'name_by_user': None, + 'primary_integration': None, 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -456,6 +461,7 @@ 'model': 'DS2423', 'name': '1D.111111111111', 'name_by_user': None, + 'primary_integration': None, 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -496,6 +502,7 @@ 'model': 'DS1822', 'name': '22.111111111111', 'name_by_user': None, + 'primary_integration': None, 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -536,6 +543,7 @@ 'model': 'DS2438', 'name': '26.111111111111', 'name_by_user': None, + 'primary_integration': 'onewire', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -620,6 +628,7 @@ 'model': 'DS18B20', 'name': '28.111111111111', 'name_by_user': None, + 'primary_integration': None, 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -660,6 +669,7 @@ 'model': 'DS18B20', 'name': '28.222222222222', 'name_by_user': None, + 'primary_integration': None, 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -700,6 +710,7 @@ 'model': 'DS18B20', 'name': '28.222222222223', 'name_by_user': None, + 'primary_integration': None, 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -740,6 +751,7 @@ 'model': 'DS2408', 'name': '29.111111111111', 'name_by_user': None, + 'primary_integration': 'onewire', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -1484,6 +1496,7 @@ 'model': 'DS2760', 'name': '30.111111111111', 'name_by_user': None, + 'primary_integration': None, 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -1524,6 +1537,7 @@ 'model': 'DS2413', 'name': '3A.111111111111', 'name_by_user': None, + 'primary_integration': 'onewire', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -1652,6 +1666,7 @@ 'model': 'DS1825', 'name': '3B.111111111111', 'name_by_user': None, + 'primary_integration': None, 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -1692,6 +1707,7 @@ 'model': 'DS28EA00', 'name': '42.111111111111', 'name_by_user': None, + 'primary_integration': None, 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -1732,6 +1748,7 @@ 'model': 'EDS0068', 'name': '7E.111111111111', 'name_by_user': None, + 'primary_integration': None, 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -1772,6 +1789,7 @@ 'model': 'EDS0066', 'name': '7E.222222222222', 'name_by_user': None, + 'primary_integration': None, 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -1812,6 +1830,7 @@ 'model': 'DS2438', 'name': 'A6.111111111111', 'name_by_user': None, + 'primary_integration': 'onewire', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -1896,6 +1915,7 @@ 'model': 'HobbyBoards_EF', 'name': 'EF.111111111111', 'name_by_user': None, + 'primary_integration': None, 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -1936,6 +1956,7 @@ 'model': 'HB_MOISTURE_METER', 'name': 'EF.111111111112', 'name_by_user': None, + 'primary_integration': 'onewire', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -2328,6 +2349,7 @@ 'model': 'HB_HUB', 'name': 'EF.111111111113', 'name_by_user': None, + 'primary_integration': 'onewire', 'serial_number': None, 'suggested_area': None, 'sw_version': None, diff --git a/tests/components/renault/snapshots/test_binary_sensor.ambr b/tests/components/renault/snapshots/test_binary_sensor.ambr index 7f30faac38e..50833ab681f 100644 --- a/tests/components/renault/snapshots/test_binary_sensor.ambr +++ b/tests/components/renault/snapshots/test_binary_sensor.ambr @@ -24,6 +24,7 @@ 'model': 'Captur ii', 'name': 'REG-NUMBER', 'name_by_user': None, + 'primary_integration': 'renault', 'serial_number': None, 'suggested_area': None, 'sw_version': 'XJB1SU', @@ -322,6 +323,7 @@ 'model': 'Captur ii', 'name': 'REG-NUMBER', 'name_by_user': None, + 'primary_integration': 'renault', 'serial_number': None, 'suggested_area': None, 'sw_version': 'XJB1SU', @@ -706,6 +708,7 @@ 'model': 'Zoe', 'name': 'REG-NUMBER', 'name_by_user': None, + 'primary_integration': 'renault', 'serial_number': None, 'suggested_area': None, 'sw_version': 'X101VE', @@ -874,6 +877,7 @@ 'model': 'Zoe', 'name': 'REG-NUMBER', 'name_by_user': None, + 'primary_integration': 'renault', 'serial_number': None, 'suggested_area': None, 'sw_version': 'X102VE', @@ -1300,6 +1304,7 @@ 'model': 'Captur ii', 'name': 'REG-NUMBER', 'name_by_user': None, + 'primary_integration': 'renault', 'serial_number': None, 'suggested_area': None, 'sw_version': 'XJB1SU', @@ -1598,6 +1603,7 @@ 'model': 'Captur ii', 'name': 'REG-NUMBER', 'name_by_user': None, + 'primary_integration': 'renault', 'serial_number': None, 'suggested_area': None, 'sw_version': 'XJB1SU', @@ -1982,6 +1988,7 @@ 'model': 'Zoe', 'name': 'REG-NUMBER', 'name_by_user': None, + 'primary_integration': 'renault', 'serial_number': None, 'suggested_area': None, 'sw_version': 'X101VE', @@ -2150,6 +2157,7 @@ 'model': 'Zoe', 'name': 'REG-NUMBER', 'name_by_user': None, + 'primary_integration': 'renault', 'serial_number': None, 'suggested_area': None, 'sw_version': 'X102VE', diff --git a/tests/components/renault/snapshots/test_button.ambr b/tests/components/renault/snapshots/test_button.ambr index daef84b5c0a..b23cae4eb03 100644 --- a/tests/components/renault/snapshots/test_button.ambr +++ b/tests/components/renault/snapshots/test_button.ambr @@ -24,6 +24,7 @@ 'model': 'Captur ii', 'name': 'REG-NUMBER', 'name_by_user': None, + 'primary_integration': 'renault', 'serial_number': None, 'suggested_area': None, 'sw_version': 'XJB1SU', @@ -106,6 +107,7 @@ 'model': 'Captur ii', 'name': 'REG-NUMBER', 'name_by_user': None, + 'primary_integration': 'renault', 'serial_number': None, 'suggested_area': None, 'sw_version': 'XJB1SU', @@ -272,6 +274,7 @@ 'model': 'Zoe', 'name': 'REG-NUMBER', 'name_by_user': None, + 'primary_integration': 'renault', 'serial_number': None, 'suggested_area': None, 'sw_version': 'X101VE', @@ -438,6 +441,7 @@ 'model': 'Zoe', 'name': 'REG-NUMBER', 'name_by_user': None, + 'primary_integration': 'renault', 'serial_number': None, 'suggested_area': None, 'sw_version': 'X102VE', @@ -604,6 +608,7 @@ 'model': 'Captur ii', 'name': 'REG-NUMBER', 'name_by_user': None, + 'primary_integration': 'renault', 'serial_number': None, 'suggested_area': None, 'sw_version': 'XJB1SU', @@ -686,6 +691,7 @@ 'model': 'Captur ii', 'name': 'REG-NUMBER', 'name_by_user': None, + 'primary_integration': 'renault', 'serial_number': None, 'suggested_area': None, 'sw_version': 'XJB1SU', @@ -852,6 +858,7 @@ 'model': 'Zoe', 'name': 'REG-NUMBER', 'name_by_user': None, + 'primary_integration': 'renault', 'serial_number': None, 'suggested_area': None, 'sw_version': 'X101VE', @@ -1018,6 +1025,7 @@ 'model': 'Zoe', 'name': 'REG-NUMBER', 'name_by_user': None, + 'primary_integration': 'renault', 'serial_number': None, 'suggested_area': None, 'sw_version': 'X102VE', diff --git a/tests/components/renault/snapshots/test_device_tracker.ambr b/tests/components/renault/snapshots/test_device_tracker.ambr index 8fe1713dc0b..df3db275214 100644 --- a/tests/components/renault/snapshots/test_device_tracker.ambr +++ b/tests/components/renault/snapshots/test_device_tracker.ambr @@ -24,6 +24,7 @@ 'model': 'Captur ii', 'name': 'REG-NUMBER', 'name_by_user': None, + 'primary_integration': 'renault', 'serial_number': None, 'suggested_area': None, 'sw_version': 'XJB1SU', @@ -107,6 +108,7 @@ 'model': 'Captur ii', 'name': 'REG-NUMBER', 'name_by_user': None, + 'primary_integration': 'renault', 'serial_number': None, 'suggested_area': None, 'sw_version': 'XJB1SU', @@ -190,6 +192,7 @@ 'model': 'Zoe', 'name': 'REG-NUMBER', 'name_by_user': None, + 'primary_integration': None, 'serial_number': None, 'suggested_area': None, 'sw_version': 'X101VE', @@ -230,6 +233,7 @@ 'model': 'Zoe', 'name': 'REG-NUMBER', 'name_by_user': None, + 'primary_integration': 'renault', 'serial_number': None, 'suggested_area': None, 'sw_version': 'X102VE', @@ -313,6 +317,7 @@ 'model': 'Captur ii', 'name': 'REG-NUMBER', 'name_by_user': None, + 'primary_integration': 'renault', 'serial_number': None, 'suggested_area': None, 'sw_version': 'XJB1SU', @@ -399,6 +404,7 @@ 'model': 'Captur ii', 'name': 'REG-NUMBER', 'name_by_user': None, + 'primary_integration': 'renault', 'serial_number': None, 'suggested_area': None, 'sw_version': 'XJB1SU', @@ -485,6 +491,7 @@ 'model': 'Zoe', 'name': 'REG-NUMBER', 'name_by_user': None, + 'primary_integration': None, 'serial_number': None, 'suggested_area': None, 'sw_version': 'X101VE', @@ -525,6 +532,7 @@ 'model': 'Zoe', 'name': 'REG-NUMBER', 'name_by_user': None, + 'primary_integration': 'renault', 'serial_number': None, 'suggested_area': None, 'sw_version': 'X102VE', diff --git a/tests/components/renault/snapshots/test_select.ambr b/tests/components/renault/snapshots/test_select.ambr index 0722cb5cab3..d597a2b31f0 100644 --- a/tests/components/renault/snapshots/test_select.ambr +++ b/tests/components/renault/snapshots/test_select.ambr @@ -24,6 +24,7 @@ 'model': 'Captur ii', 'name': 'REG-NUMBER', 'name_by_user': None, + 'primary_integration': None, 'serial_number': None, 'suggested_area': None, 'sw_version': 'XJB1SU', @@ -64,6 +65,7 @@ 'model': 'Captur ii', 'name': 'REG-NUMBER', 'name_by_user': None, + 'primary_integration': 'renault', 'serial_number': None, 'suggested_area': None, 'sw_version': 'XJB1SU', @@ -159,6 +161,7 @@ 'model': 'Zoe', 'name': 'REG-NUMBER', 'name_by_user': None, + 'primary_integration': 'renault', 'serial_number': None, 'suggested_area': None, 'sw_version': 'X101VE', @@ -254,6 +257,7 @@ 'model': 'Zoe', 'name': 'REG-NUMBER', 'name_by_user': None, + 'primary_integration': 'renault', 'serial_number': None, 'suggested_area': None, 'sw_version': 'X102VE', @@ -349,6 +353,7 @@ 'model': 'Captur ii', 'name': 'REG-NUMBER', 'name_by_user': None, + 'primary_integration': None, 'serial_number': None, 'suggested_area': None, 'sw_version': 'XJB1SU', @@ -389,6 +394,7 @@ 'model': 'Captur ii', 'name': 'REG-NUMBER', 'name_by_user': None, + 'primary_integration': 'renault', 'serial_number': None, 'suggested_area': None, 'sw_version': 'XJB1SU', @@ -484,6 +490,7 @@ 'model': 'Zoe', 'name': 'REG-NUMBER', 'name_by_user': None, + 'primary_integration': 'renault', 'serial_number': None, 'suggested_area': None, 'sw_version': 'X101VE', @@ -579,6 +586,7 @@ 'model': 'Zoe', 'name': 'REG-NUMBER', 'name_by_user': None, + 'primary_integration': 'renault', 'serial_number': None, 'suggested_area': None, 'sw_version': 'X102VE', diff --git a/tests/components/renault/snapshots/test_sensor.ambr b/tests/components/renault/snapshots/test_sensor.ambr index 5909c66bc5c..6af7d9cd8d3 100644 --- a/tests/components/renault/snapshots/test_sensor.ambr +++ b/tests/components/renault/snapshots/test_sensor.ambr @@ -24,6 +24,7 @@ 'model': 'Captur ii', 'name': 'REG-NUMBER', 'name_by_user': None, + 'primary_integration': 'renault', 'serial_number': None, 'suggested_area': None, 'sw_version': 'XJB1SU', @@ -332,6 +333,7 @@ 'model': 'Captur ii', 'name': 'REG-NUMBER', 'name_by_user': None, + 'primary_integration': 'renault', 'serial_number': None, 'suggested_area': None, 'sw_version': 'XJB1SU', @@ -1085,6 +1087,7 @@ 'model': 'Zoe', 'name': 'REG-NUMBER', 'name_by_user': None, + 'primary_integration': 'renault', 'serial_number': None, 'suggested_area': None, 'sw_version': 'X101VE', @@ -1834,6 +1837,7 @@ 'model': 'Zoe', 'name': 'REG-NUMBER', 'name_by_user': None, + 'primary_integration': 'renault', 'serial_number': None, 'suggested_area': None, 'sw_version': 'X102VE', @@ -2626,6 +2630,7 @@ 'model': 'Captur ii', 'name': 'REG-NUMBER', 'name_by_user': None, + 'primary_integration': 'renault', 'serial_number': None, 'suggested_area': None, 'sw_version': 'XJB1SU', @@ -2934,6 +2939,7 @@ 'model': 'Captur ii', 'name': 'REG-NUMBER', 'name_by_user': None, + 'primary_integration': 'renault', 'serial_number': None, 'suggested_area': None, 'sw_version': 'XJB1SU', @@ -3687,6 +3693,7 @@ 'model': 'Zoe', 'name': 'REG-NUMBER', 'name_by_user': None, + 'primary_integration': 'renault', 'serial_number': None, 'suggested_area': None, 'sw_version': 'X101VE', @@ -4436,6 +4443,7 @@ 'model': 'Zoe', 'name': 'REG-NUMBER', 'name_by_user': None, + 'primary_integration': 'renault', 'serial_number': None, 'suggested_area': None, 'sw_version': 'X102VE', diff --git a/tests/components/rova/snapshots/test_init.ambr b/tests/components/rova/snapshots/test_init.ambr index 340b0e6d472..9210027221b 100644 --- a/tests/components/rova/snapshots/test_init.ambr +++ b/tests/components/rova/snapshots/test_init.ambr @@ -23,6 +23,7 @@ 'model': None, 'name': '8381BE 13', 'name_by_user': None, + 'primary_integration': 'rova', 'serial_number': None, 'suggested_area': None, 'sw_version': None, diff --git a/tests/components/sfr_box/snapshots/test_binary_sensor.ambr b/tests/components/sfr_box/snapshots/test_binary_sensor.ambr index 7422c1395c3..62a656f9157 100644 --- a/tests/components/sfr_box/snapshots/test_binary_sensor.ambr +++ b/tests/components/sfr_box/snapshots/test_binary_sensor.ambr @@ -24,6 +24,7 @@ 'model': 'NB6VAC-FXC-r0', 'name': 'SFR Box', 'name_by_user': None, + 'primary_integration': None, 'serial_number': None, 'suggested_area': None, 'sw_version': 'NB6VAC-MAIN-R4.0.44k', @@ -150,6 +151,7 @@ 'model': 'NB6VAC-FXC-r0', 'name': 'SFR Box', 'name_by_user': None, + 'primary_integration': None, 'serial_number': None, 'suggested_area': None, 'sw_version': 'NB6VAC-MAIN-R4.0.44k', diff --git a/tests/components/sfr_box/snapshots/test_button.ambr b/tests/components/sfr_box/snapshots/test_button.ambr index 0dfbf187f6d..b786e75910b 100644 --- a/tests/components/sfr_box/snapshots/test_button.ambr +++ b/tests/components/sfr_box/snapshots/test_button.ambr @@ -24,6 +24,7 @@ 'model': 'NB6VAC-FXC-r0', 'name': 'SFR Box', 'name_by_user': None, + 'primary_integration': None, 'serial_number': None, 'suggested_area': None, 'sw_version': 'NB6VAC-MAIN-R4.0.44k', diff --git a/tests/components/sfr_box/snapshots/test_sensor.ambr b/tests/components/sfr_box/snapshots/test_sensor.ambr index 0f39eed9e60..662b765ee74 100644 --- a/tests/components/sfr_box/snapshots/test_sensor.ambr +++ b/tests/components/sfr_box/snapshots/test_sensor.ambr @@ -24,6 +24,7 @@ 'model': 'NB6VAC-FXC-r0', 'name': 'SFR Box', 'name_by_user': None, + 'primary_integration': None, 'serial_number': None, 'suggested_area': None, 'sw_version': 'NB6VAC-MAIN-R4.0.44k', diff --git a/tests/components/tailwind/snapshots/test_binary_sensor.ambr b/tests/components/tailwind/snapshots/test_binary_sensor.ambr index ea2a539363d..f9088e1d5c3 100644 --- a/tests/components/tailwind/snapshots/test_binary_sensor.ambr +++ b/tests/components/tailwind/snapshots/test_binary_sensor.ambr @@ -70,6 +70,7 @@ 'model': 'iQ3', 'name': 'Door 1', 'name_by_user': None, + 'primary_integration': 'tailwind', 'serial_number': None, 'suggested_area': None, 'sw_version': '10.10', @@ -147,6 +148,7 @@ 'model': 'iQ3', 'name': 'Door 2', 'name_by_user': None, + 'primary_integration': 'tailwind', 'serial_number': None, 'suggested_area': None, 'sw_version': '10.10', diff --git a/tests/components/tailwind/snapshots/test_button.ambr b/tests/components/tailwind/snapshots/test_button.ambr index 560d3fe692c..f96032630bc 100644 --- a/tests/components/tailwind/snapshots/test_button.ambr +++ b/tests/components/tailwind/snapshots/test_button.ambr @@ -74,6 +74,7 @@ 'model': 'iQ3', 'name': 'Tailwind iQ3', 'name_by_user': None, + 'primary_integration': None, 'serial_number': None, 'suggested_area': None, 'sw_version': '10.10', diff --git a/tests/components/tailwind/snapshots/test_cover.ambr b/tests/components/tailwind/snapshots/test_cover.ambr index 0ecd172b2ca..98891e649e7 100644 --- a/tests/components/tailwind/snapshots/test_cover.ambr +++ b/tests/components/tailwind/snapshots/test_cover.ambr @@ -71,6 +71,7 @@ 'model': 'iQ3', 'name': 'Door 1', 'name_by_user': None, + 'primary_integration': 'tailwind', 'serial_number': None, 'suggested_area': None, 'sw_version': '10.10', @@ -149,6 +150,7 @@ 'model': 'iQ3', 'name': 'Door 2', 'name_by_user': None, + 'primary_integration': 'tailwind', 'serial_number': None, 'suggested_area': None, 'sw_version': '10.10', diff --git a/tests/components/tailwind/snapshots/test_number.ambr b/tests/components/tailwind/snapshots/test_number.ambr index cbd61d31a6c..1bd01482c0c 100644 --- a/tests/components/tailwind/snapshots/test_number.ambr +++ b/tests/components/tailwind/snapshots/test_number.ambr @@ -83,6 +83,7 @@ 'model': 'iQ3', 'name': 'Tailwind iQ3', 'name_by_user': None, + 'primary_integration': None, 'serial_number': None, 'suggested_area': None, 'sw_version': '10.10', diff --git a/tests/components/tedee/snapshots/test_init.ambr b/tests/components/tedee/snapshots/test_init.ambr index 83ab032dfb4..96284adb338 100644 --- a/tests/components/tedee/snapshots/test_init.ambr +++ b/tests/components/tedee/snapshots/test_init.ambr @@ -23,6 +23,7 @@ 'model': 'Bridge', 'name': 'Bridge-AB1C', 'name_by_user': None, + 'primary_integration': None, 'serial_number': '0000-0000', 'suggested_area': None, 'sw_version': None, diff --git a/tests/components/tedee/snapshots/test_lock.ambr b/tests/components/tedee/snapshots/test_lock.ambr index 8e4fc464479..bf9021b639b 100644 --- a/tests/components/tedee/snapshots/test_lock.ambr +++ b/tests/components/tedee/snapshots/test_lock.ambr @@ -70,6 +70,7 @@ 'model': 'Tedee PRO', 'name': 'Lock-1A2B', 'name_by_user': None, + 'primary_integration': 'tedee', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -147,6 +148,7 @@ 'model': 'Tedee GO', 'name': 'Lock-2C3D', 'name_by_user': None, + 'primary_integration': 'tedee', 'serial_number': None, 'suggested_area': None, 'sw_version': None, diff --git a/tests/components/teslemetry/snapshots/test_init.ambr b/tests/components/teslemetry/snapshots/test_init.ambr index 951e4557bdd..d1656c2260e 100644 --- a/tests/components/teslemetry/snapshots/test_init.ambr +++ b/tests/components/teslemetry/snapshots/test_init.ambr @@ -23,6 +23,7 @@ 'model': 'Powerwall 2, Tesla Backup Gateway 2', 'name': 'Energy Site', 'name_by_user': None, + 'primary_integration': 'teslemetry', 'serial_number': '123456', 'suggested_area': None, 'sw_version': None, @@ -53,6 +54,7 @@ 'model': 'Model X', 'name': 'Test', 'name_by_user': None, + 'primary_integration': 'teslemetry', 'serial_number': 'LRWXF7EK4KC700000', 'suggested_area': None, 'sw_version': None, @@ -83,6 +85,7 @@ 'model': 'Gen 3 Wall Connector', 'name': 'Wall Connector', 'name_by_user': None, + 'primary_integration': 'teslemetry', 'serial_number': '123', 'suggested_area': None, 'sw_version': None, @@ -113,6 +116,7 @@ 'model': 'Gen 3 Wall Connector', 'name': 'Wall Connector', 'name_by_user': None, + 'primary_integration': 'teslemetry', 'serial_number': '234', 'suggested_area': None, 'sw_version': None, diff --git a/tests/components/twentemilieu/snapshots/test_calendar.ambr b/tests/components/twentemilieu/snapshots/test_calendar.ambr index 78b2d56afca..fa24ad644d2 100644 --- a/tests/components/twentemilieu/snapshots/test_calendar.ambr +++ b/tests/components/twentemilieu/snapshots/test_calendar.ambr @@ -101,6 +101,7 @@ 'model': None, 'name': 'Twente Milieu', 'name_by_user': None, + 'primary_integration': 'twentemilieu', 'serial_number': None, 'suggested_area': None, 'sw_version': None, diff --git a/tests/components/twentemilieu/snapshots/test_sensor.ambr b/tests/components/twentemilieu/snapshots/test_sensor.ambr index a0f3b75da57..e943d937fa3 100644 --- a/tests/components/twentemilieu/snapshots/test_sensor.ambr +++ b/tests/components/twentemilieu/snapshots/test_sensor.ambr @@ -70,6 +70,7 @@ 'model': None, 'name': 'Twente Milieu', 'name_by_user': None, + 'primary_integration': 'twentemilieu', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -147,6 +148,7 @@ 'model': None, 'name': 'Twente Milieu', 'name_by_user': None, + 'primary_integration': 'twentemilieu', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -224,6 +226,7 @@ 'model': None, 'name': 'Twente Milieu', 'name_by_user': None, + 'primary_integration': 'twentemilieu', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -301,6 +304,7 @@ 'model': None, 'name': 'Twente Milieu', 'name_by_user': None, + 'primary_integration': 'twentemilieu', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -378,6 +382,7 @@ 'model': None, 'name': 'Twente Milieu', 'name_by_user': None, + 'primary_integration': 'twentemilieu', 'serial_number': None, 'suggested_area': None, 'sw_version': None, diff --git a/tests/components/uptime/snapshots/test_sensor.ambr b/tests/components/uptime/snapshots/test_sensor.ambr index 0e7ae6dceaa..692bfe53ea2 100644 --- a/tests/components/uptime/snapshots/test_sensor.ambr +++ b/tests/components/uptime/snapshots/test_sensor.ambr @@ -63,6 +63,7 @@ 'model': None, 'name': 'Uptime', 'name_by_user': None, + 'primary_integration': 'uptime', 'serial_number': None, 'suggested_area': None, 'sw_version': None, diff --git a/tests/components/vesync/snapshots/test_fan.ambr b/tests/components/vesync/snapshots/test_fan.ambr index 59304e92d9d..159d872a65b 100644 --- a/tests/components/vesync/snapshots/test_fan.ambr +++ b/tests/components/vesync/snapshots/test_fan.ambr @@ -24,6 +24,7 @@ 'model': 'LV-PUR131S', 'name': 'Air Purifier 131s', 'name_by_user': None, + 'primary_integration': 'vesync', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -114,6 +115,7 @@ 'model': 'Core200S', 'name': 'Air Purifier 200s', 'name_by_user': None, + 'primary_integration': 'vesync', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -209,6 +211,7 @@ 'model': 'LAP-C401S-WJP', 'name': 'Air Purifier 400s', 'name_by_user': None, + 'primary_integration': 'vesync', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -306,6 +309,7 @@ 'model': 'LAP-C601S-WUS', 'name': 'Air Purifier 600s', 'name_by_user': None, + 'primary_integration': 'vesync', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -403,6 +407,7 @@ 'model': 'ESL100', 'name': 'Dimmable Light', 'name_by_user': None, + 'primary_integration': 'vesync', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -439,6 +444,7 @@ 'model': 'ESWD16', 'name': 'Dimmer Switch', 'name_by_user': None, + 'primary_integration': 'vesync', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -491,6 +497,7 @@ 'model': 'wifi-switch-1.3', 'name': 'Outlet', 'name_by_user': None, + 'primary_integration': 'vesync', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -527,6 +534,7 @@ 'model': 'ESL100CW', 'name': 'Temperature Light', 'name_by_user': None, + 'primary_integration': 'vesync', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -563,6 +571,7 @@ 'model': 'ESWL01', 'name': 'Wall Switch', 'name_by_user': None, + 'primary_integration': 'vesync', 'serial_number': None, 'suggested_area': None, 'sw_version': None, diff --git a/tests/components/vesync/snapshots/test_light.ambr b/tests/components/vesync/snapshots/test_light.ambr index 9990395a36c..c393453e78c 100644 --- a/tests/components/vesync/snapshots/test_light.ambr +++ b/tests/components/vesync/snapshots/test_light.ambr @@ -24,6 +24,7 @@ 'model': 'LV-PUR131S', 'name': 'Air Purifier 131s', 'name_by_user': None, + 'primary_integration': 'vesync', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -60,6 +61,7 @@ 'model': 'Core200S', 'name': 'Air Purifier 200s', 'name_by_user': None, + 'primary_integration': 'vesync', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -96,6 +98,7 @@ 'model': 'LAP-C401S-WJP', 'name': 'Air Purifier 400s', 'name_by_user': None, + 'primary_integration': 'vesync', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -132,6 +135,7 @@ 'model': 'LAP-C601S-WUS', 'name': 'Air Purifier 600s', 'name_by_user': None, + 'primary_integration': 'vesync', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -168,6 +172,7 @@ 'model': 'ESL100', 'name': 'Dimmable Light', 'name_by_user': None, + 'primary_integration': 'vesync', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -256,6 +261,7 @@ 'model': 'ESWD16', 'name': 'Dimmer Switch', 'name_by_user': None, + 'primary_integration': 'vesync', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -362,6 +368,7 @@ 'model': 'wifi-switch-1.3', 'name': 'Outlet', 'name_by_user': None, + 'primary_integration': 'vesync', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -398,6 +405,7 @@ 'model': 'ESL100CW', 'name': 'Temperature Light', 'name_by_user': None, + 'primary_integration': 'vesync', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -501,6 +509,7 @@ 'model': 'ESWL01', 'name': 'Wall Switch', 'name_by_user': None, + 'primary_integration': 'vesync', 'serial_number': None, 'suggested_area': None, 'sw_version': None, diff --git a/tests/components/vesync/snapshots/test_sensor.ambr b/tests/components/vesync/snapshots/test_sensor.ambr index 268718fb2fe..27c52e5580e 100644 --- a/tests/components/vesync/snapshots/test_sensor.ambr +++ b/tests/components/vesync/snapshots/test_sensor.ambr @@ -24,6 +24,7 @@ 'model': 'LV-PUR131S', 'name': 'Air Purifier 131s', 'name_by_user': None, + 'primary_integration': 'vesync', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -152,6 +153,7 @@ 'model': 'Core200S', 'name': 'Air Purifier 200s', 'name_by_user': None, + 'primary_integration': 'vesync', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -236,6 +238,7 @@ 'model': 'LAP-C401S-WJP', 'name': 'Air Purifier 400s', 'name_by_user': None, + 'primary_integration': 'vesync', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -413,6 +416,7 @@ 'model': 'LAP-C601S-WUS', 'name': 'Air Purifier 600s', 'name_by_user': None, + 'primary_integration': 'vesync', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -590,6 +594,7 @@ 'model': 'ESL100', 'name': 'Dimmable Light', 'name_by_user': None, + 'primary_integration': 'vesync', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -626,6 +631,7 @@ 'model': 'ESWD16', 'name': 'Dimmer Switch', 'name_by_user': None, + 'primary_integration': 'vesync', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -678,6 +684,7 @@ 'model': 'wifi-switch-1.3', 'name': 'Outlet', 'name_by_user': None, + 'primary_integration': 'vesync', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -1008,6 +1015,7 @@ 'model': 'ESL100CW', 'name': 'Temperature Light', 'name_by_user': None, + 'primary_integration': 'vesync', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -1044,6 +1052,7 @@ 'model': 'ESWL01', 'name': 'Wall Switch', 'name_by_user': None, + 'primary_integration': 'vesync', 'serial_number': None, 'suggested_area': None, 'sw_version': None, diff --git a/tests/components/vesync/snapshots/test_switch.ambr b/tests/components/vesync/snapshots/test_switch.ambr index 3df26f74bcf..3b816e70bee 100644 --- a/tests/components/vesync/snapshots/test_switch.ambr +++ b/tests/components/vesync/snapshots/test_switch.ambr @@ -24,6 +24,7 @@ 'model': 'LV-PUR131S', 'name': 'Air Purifier 131s', 'name_by_user': None, + 'primary_integration': 'vesync', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -60,6 +61,7 @@ 'model': 'Core200S', 'name': 'Air Purifier 200s', 'name_by_user': None, + 'primary_integration': 'vesync', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -96,6 +98,7 @@ 'model': 'LAP-C401S-WJP', 'name': 'Air Purifier 400s', 'name_by_user': None, + 'primary_integration': 'vesync', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -132,6 +135,7 @@ 'model': 'LAP-C601S-WUS', 'name': 'Air Purifier 600s', 'name_by_user': None, + 'primary_integration': 'vesync', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -168,6 +172,7 @@ 'model': 'ESL100', 'name': 'Dimmable Light', 'name_by_user': None, + 'primary_integration': 'vesync', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -204,6 +209,7 @@ 'model': 'ESWD16', 'name': 'Dimmer Switch', 'name_by_user': None, + 'primary_integration': 'vesync', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -256,6 +262,7 @@ 'model': 'wifi-switch-1.3', 'name': 'Outlet', 'name_by_user': None, + 'primary_integration': 'vesync', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -336,6 +343,7 @@ 'model': 'ESL100CW', 'name': 'Temperature Light', 'name_by_user': None, + 'primary_integration': 'vesync', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -372,6 +380,7 @@ 'model': 'ESWL01', 'name': 'Wall Switch', 'name_by_user': None, + 'primary_integration': 'vesync', 'serial_number': None, 'suggested_area': None, 'sw_version': None, diff --git a/tests/components/whois/snapshots/test_sensor.ambr b/tests/components/whois/snapshots/test_sensor.ambr index 61762c36e59..409541b6322 100644 --- a/tests/components/whois/snapshots/test_sensor.ambr +++ b/tests/components/whois/snapshots/test_sensor.ambr @@ -69,6 +69,7 @@ 'model': None, 'name': 'home-assistant.io', 'name_by_user': None, + 'primary_integration': 'whois', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -146,6 +147,7 @@ 'model': None, 'name': 'home-assistant.io', 'name_by_user': None, + 'primary_integration': 'whois', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -227,6 +229,7 @@ 'model': None, 'name': 'home-assistant.io', 'name_by_user': None, + 'primary_integration': 'whois', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -304,6 +307,7 @@ 'model': None, 'name': 'home-assistant.io', 'name_by_user': None, + 'primary_integration': 'whois', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -381,6 +385,7 @@ 'model': None, 'name': 'home-assistant.io', 'name_by_user': None, + 'primary_integration': 'whois', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -457,6 +462,7 @@ 'model': None, 'name': 'home-assistant.io', 'name_by_user': None, + 'primary_integration': 'whois', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -533,6 +539,7 @@ 'model': None, 'name': 'home-assistant.io', 'name_by_user': None, + 'primary_integration': 'whois', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -609,6 +616,7 @@ 'model': None, 'name': 'home-assistant.io', 'name_by_user': None, + 'primary_integration': 'whois', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -685,6 +693,7 @@ 'model': None, 'name': 'home-assistant.io', 'name_by_user': None, + 'primary_integration': 'whois', 'serial_number': None, 'suggested_area': None, 'sw_version': None, diff --git a/tests/components/wled/snapshots/test_binary_sensor.ambr b/tests/components/wled/snapshots/test_binary_sensor.ambr index b9a083336d2..ab30bff1729 100644 --- a/tests/components/wled/snapshots/test_binary_sensor.ambr +++ b/tests/components/wled/snapshots/test_binary_sensor.ambr @@ -74,6 +74,7 @@ 'model': 'DIY light', 'name': 'WLED RGB Light', 'name_by_user': None, + 'primary_integration': 'wled', 'serial_number': None, 'suggested_area': None, 'sw_version': '0.8.5', diff --git a/tests/components/wled/snapshots/test_button.ambr b/tests/components/wled/snapshots/test_button.ambr index b489bcc0a71..5fb2ac08be7 100644 --- a/tests/components/wled/snapshots/test_button.ambr +++ b/tests/components/wled/snapshots/test_button.ambr @@ -74,6 +74,7 @@ 'model': 'DIY light', 'name': 'WLED RGB Light', 'name_by_user': None, + 'primary_integration': 'wled', 'serial_number': None, 'suggested_area': None, 'sw_version': '0.8.5', diff --git a/tests/components/wled/snapshots/test_number.ambr b/tests/components/wled/snapshots/test_number.ambr index c3440108148..9c3498372bf 100644 --- a/tests/components/wled/snapshots/test_number.ambr +++ b/tests/components/wled/snapshots/test_number.ambr @@ -82,6 +82,7 @@ 'model': 'DIY light', 'name': 'WLED RGB Light', 'name_by_user': None, + 'primary_integration': 'wled', 'serial_number': None, 'suggested_area': None, 'sw_version': '0.8.5', @@ -171,6 +172,7 @@ 'model': 'DIY light', 'name': 'WLED RGB Light', 'name_by_user': None, + 'primary_integration': 'wled', 'serial_number': None, 'suggested_area': None, 'sw_version': '0.8.5', diff --git a/tests/components/wled/snapshots/test_select.ambr b/tests/components/wled/snapshots/test_select.ambr index 6d64ec43658..41df21c0223 100644 --- a/tests/components/wled/snapshots/test_select.ambr +++ b/tests/components/wled/snapshots/test_select.ambr @@ -84,6 +84,7 @@ 'model': 'DIY light', 'name': 'WLED RGB Light', 'name_by_user': None, + 'primary_integration': 'wled', 'serial_number': None, 'suggested_area': None, 'sw_version': '0.8.5', @@ -269,6 +270,7 @@ 'model': 'DIY light', 'name': 'WLED RGB Light', 'name_by_user': None, + 'primary_integration': 'wled', 'serial_number': None, 'suggested_area': None, 'sw_version': '0.8.5', @@ -358,6 +360,7 @@ 'model': 'DIY light', 'name': 'WLED RGBW Light', 'name_by_user': None, + 'primary_integration': 'wled', 'serial_number': None, 'suggested_area': None, 'sw_version': '0.8.6b4', @@ -447,6 +450,7 @@ 'model': 'DIY light', 'name': 'WLED RGBW Light', 'name_by_user': None, + 'primary_integration': 'wled', 'serial_number': None, 'suggested_area': None, 'sw_version': '0.8.6b4', diff --git a/tests/components/wled/snapshots/test_switch.ambr b/tests/components/wled/snapshots/test_switch.ambr index da69e686f07..4d7a7d59798 100644 --- a/tests/components/wled/snapshots/test_switch.ambr +++ b/tests/components/wled/snapshots/test_switch.ambr @@ -76,6 +76,7 @@ 'model': 'DIY light', 'name': 'WLED RGB Light', 'name_by_user': None, + 'primary_integration': 'wled', 'serial_number': None, 'suggested_area': None, 'sw_version': '0.8.5', @@ -156,6 +157,7 @@ 'model': 'DIY light', 'name': 'WLED RGB Light', 'name_by_user': None, + 'primary_integration': 'wled', 'serial_number': None, 'suggested_area': None, 'sw_version': '0.8.5', @@ -237,6 +239,7 @@ 'model': 'DIY light', 'name': 'WLED RGB Light', 'name_by_user': None, + 'primary_integration': 'wled', 'serial_number': None, 'suggested_area': None, 'sw_version': '0.8.5', @@ -318,6 +321,7 @@ 'model': 'DIY light', 'name': 'WLED RGB Light', 'name_by_user': None, + 'primary_integration': 'wled', 'serial_number': None, 'suggested_area': None, 'sw_version': '0.8.5', diff --git a/tests/helpers/test_device_registry.py b/tests/helpers/test_device_registry.py index 3ad45d630df..ad0df1f9f25 100644 --- a/tests/helpers/test_device_registry.py +++ b/tests/helpers/test_device_registry.py @@ -2628,3 +2628,39 @@ async def test_async_remove_device_thread_safety( await hass.async_add_executor_job( device_registry.async_remove_device, device.id ) + + +async def test_primary_integration( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + mock_config_entry: MockConfigEntry, +) -> None: + """Test the primary integration field.""" + # Update existing + device = device_registry.async_get_or_create( + config_entry_id=mock_config_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + identifiers=set(), + manufacturer="manufacturer", + model="model", + ) + assert device.primary_integration is None + + device = device_registry.async_get_or_create( + config_entry_id=mock_config_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + model="model 2", + domain="test_domain", + ) + assert device.primary_integration == "test_domain" + + # Create new + device = device_registry.async_get_or_create( + config_entry_id=mock_config_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + identifiers=set(), + manufacturer="manufacturer", + model="model", + domain="test_domain", + ) + assert device.primary_integration == "test_domain" diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py index 56ddcd9a6c9..c28a88e8df8 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -1191,6 +1191,7 @@ async def test_device_info_called( assert device.sw_version == "test-sw" assert device.hw_version == "test-hw" assert device.via_device_id == via.id + assert device.primary_integration == config_entry.domain async def test_device_info_not_overrides( From 54e6459a41a197672b14127b4e92a4da74869403 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 16 Jun 2024 13:35:43 -0400 Subject: [PATCH 0723/1445] Speed up getting conversation agent languages (#119554) Speed up getting conversation languages --- homeassistant/components/conversation/__init__.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/conversation/__init__.py b/homeassistant/components/conversation/__init__.py index 2e6c813a551..6441dcab4ca 100644 --- a/homeassistant/components/conversation/__init__.py +++ b/homeassistant/components/conversation/__init__.py @@ -127,7 +127,6 @@ async def async_get_conversation_languages( """ agent_manager = get_agent_manager(hass) entity_component: EntityComponent[ConversationEntity] = hass.data[DOMAIN] - languages: set[str] = set() agents: list[ConversationEntity | AbstractConversationAgent] if agent_id: @@ -136,6 +135,10 @@ async def async_get_conversation_languages( if agent is None: raise ValueError(f"Agent {agent_id} not found") + # Shortcut + if agent.supported_languages == MATCH_ALL: + return MATCH_ALL + agents = [agent] else: @@ -143,11 +146,16 @@ async def async_get_conversation_languages( for info in agent_manager.async_get_agent_info(): agent = agent_manager.async_get_agent(info.id) assert agent is not None + + # Shortcut + if agent.supported_languages == MATCH_ALL: + return MATCH_ALL + agents.append(agent) + languages: set[str] = set() + for agent in agents: - if agent.supported_languages == MATCH_ALL: - return MATCH_ALL for language_tag in agent.supported_languages: languages.add(language_tag) From 03027893ff352411e1a9ddadb9a69e743ca247d8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 16 Jun 2024 13:54:58 -0500 Subject: [PATCH 0724/1445] Fix precision for unifiprotect sensors (#119781) --- .../components/unifiprotect/sensor.py | 22 +++++++++++++------ tests/components/unifiprotect/test_sensor.py | 18 +++++++++++++-- 2 files changed, 31 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/unifiprotect/sensor.py b/homeassistant/components/unifiprotect/sensor.py index 0fcd4f5853d..ea2c84bb128 100644 --- a/homeassistant/components/unifiprotect/sensor.py +++ b/homeassistant/components/unifiprotect/sensor.py @@ -2,9 +2,10 @@ from __future__ import annotations -from collections.abc import Sequence +from collections.abc import Callable, Sequence from dataclasses import dataclass from datetime import datetime +from functools import partial import logging from typing import Any @@ -62,12 +63,19 @@ class ProtectSensorEntityDescription( precision: int | None = None - def get_ufp_value(self, obj: T) -> Any: - """Return value from UniFi Protect device.""" - value = super().get_ufp_value(obj) - if self.precision and value is not None: - return round(value, self.precision) - return value + def __post_init__(self) -> None: + """Ensure values are rounded if precision is set.""" + super().__post_init__() + if precision := self.precision: + object.__setattr__( + self, + "get_ufp_value", + partial(self._rounded_value, precision, self.get_ufp_value), + ) + + def _rounded_value(self, precision: int, getter: Callable[[T], Any], obj: T) -> Any: + """Round value to precision if set.""" + return None if (v := getter(obj)) is None else round(v, precision) @dataclass(frozen=True, kw_only=True) diff --git a/tests/components/unifiprotect/test_sensor.py b/tests/components/unifiprotect/test_sensor.py index 1a1374390ae..a3155376a05 100644 --- a/tests/components/unifiprotect/test_sensor.py +++ b/tests/components/unifiprotect/test_sensor.py @@ -508,10 +508,10 @@ async def test_sensor_update_alarm_with_last_trip_time( assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION -async def test_camera_update_licenseplate( +async def test_camera_update_license_plate( hass: HomeAssistant, ufp: MockUFPFixture, camera: Camera, fixed_now: datetime ) -> None: - """Test sensor motion entity.""" + """Test license plate sensor.""" camera.feature_flags.smart_detect_types.append(SmartDetectObjectType.LICENSE_PLATE) camera.feature_flags.has_smart_detect = True @@ -560,3 +560,17 @@ async def test_camera_update_licenseplate( state = hass.states.get(entity_id) assert state assert state.state == "ABCD1234" + + +async def test_sensor_precision( + hass: HomeAssistant, ufp: MockUFPFixture, sensor_all: Sensor, fixed_now: datetime +) -> None: + """Test sensor precision value is respected.""" + + await init_entry(hass, ufp, [sensor_all]) + assert_entity_counts(hass, Platform.SENSOR, 22, 14) + nvr: NVR = ufp.api.bootstrap.nvr + + _, entity_id = ids_from_device_description(Platform.SENSOR, nvr, NVR_SENSORS[6]) + + assert hass.states.get(entity_id).state == "17.49" From 2713a3fdb144fcf56e4db06e2e157dbda17f12e7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 16 Jun 2024 14:14:41 -0500 Subject: [PATCH 0725/1445] Bump uiprotect to 1.12.0 (#119763) --- homeassistant/components/unifiprotect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index ce512ca3f3c..f54d33984c0 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -40,7 +40,7 @@ "integration_type": "hub", "iot_class": "local_push", "loggers": ["uiprotect", "unifi_discovery"], - "requirements": ["uiprotect==1.7.2", "unifi-discovery==1.1.8"], + "requirements": ["uiprotect==1.12.0", "unifi-discovery==1.1.8"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/requirements_all.txt b/requirements_all.txt index adcc839c94a..75f21390db9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2788,7 +2788,7 @@ twitchAPI==4.0.0 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==1.7.2 +uiprotect==1.12.0 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6d39899f873..e790be1ddd4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2168,7 +2168,7 @@ twitchAPI==4.0.0 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==1.7.2 +uiprotect==1.12.0 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 From 151b3b3b0a86f148d9e609a4330c6505b5b6e732 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 16 Jun 2024 14:14:59 -0500 Subject: [PATCH 0726/1445] Reduce duplicate code in unifiprotect entities (#119779) --- .../components/unifiprotect/sensor.py | 13 ++--- .../components/unifiprotect/switch.py | 53 ++++++++----------- 2 files changed, 26 insertions(+), 40 deletions(-) diff --git a/homeassistant/components/unifiprotect/sensor.py b/homeassistant/components/unifiprotect/sensor.py index ea2c84bb128..78fc24026a3 100644 --- a/homeassistant/components/unifiprotect/sensor.py +++ b/homeassistant/components/unifiprotect/sensor.py @@ -706,8 +706,8 @@ def _async_nvr_entities( return entities -class ProtectDeviceSensor(ProtectDeviceEntity, SensorEntity): - """A Ubiquiti UniFi Protect Sensor.""" +class BaseProtectSensor(BaseProtectEntity, SensorEntity): + """A UniFi Protect Sensor Entity.""" entity_description: ProtectSensorEntityDescription _state_attrs = ("_attr_available", "_attr_native_value") @@ -717,15 +717,12 @@ class ProtectDeviceSensor(ProtectDeviceEntity, SensorEntity): self._attr_native_value = self.entity_description.get_ufp_value(self.device) -class ProtectNVRSensor(ProtectNVREntity, SensorEntity): +class ProtectDeviceSensor(BaseProtectSensor, ProtectDeviceEntity): """A Ubiquiti UniFi Protect Sensor.""" - entity_description: ProtectSensorEntityDescription - _state_attrs = ("_attr_available", "_attr_native_value") - def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None: - super()._async_update_device_from_protect(device) - self._attr_native_value = self.entity_description.get_ufp_value(self.device) +class ProtectNVRSensor(BaseProtectSensor, ProtectNVREntity): + """A Ubiquiti UniFi Protect Sensor.""" class ProtectEventSensor(EventEntityMixin, SensorEntity): diff --git a/homeassistant/components/unifiprotect/switch.py b/homeassistant/components/unifiprotect/switch.py index 104c8f4af86..ca56a602209 100644 --- a/homeassistant/components/unifiprotect/switch.py +++ b/homeassistant/components/unifiprotect/switch.py @@ -472,43 +472,32 @@ _PRIVACY_DESCRIPTIONS: dict[ModelType, Sequence[ProtectEntityDescription]] = { } -class ProtectSwitch(ProtectDeviceEntity, SwitchEntity): +class ProtectBaseSwitch(BaseProtectEntity, SwitchEntity): + """Base class for UniFi Protect Switch.""" + + entity_description: ProtectSwitchEntityDescription + _state_attrs = ("_attr_available", "_attr_is_on") + + def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None: + super()._async_update_device_from_protect(device) + self._attr_is_on = self.entity_description.get_ufp_value(self.device) is True + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the device on.""" + await self.entity_description.ufp_set(self.device, True) + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the device off.""" + await self.entity_description.ufp_set(self.device, False) + + +class ProtectSwitch(ProtectBaseSwitch, ProtectDeviceEntity): """A UniFi Protect Switch.""" - entity_description: ProtectSwitchEntityDescription - _state_attrs = ("_attr_available", "_attr_is_on") - def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None: - super()._async_update_device_from_protect(device) - self._attr_is_on = self.entity_description.get_ufp_value(self.device) is True - - async def async_turn_on(self, **kwargs: Any) -> None: - """Turn the device on.""" - await self.entity_description.ufp_set(self.device, True) - - async def async_turn_off(self, **kwargs: Any) -> None: - """Turn the device off.""" - await self.entity_description.ufp_set(self.device, False) - - -class ProtectNVRSwitch(ProtectNVREntity, SwitchEntity): +class ProtectNVRSwitch(ProtectBaseSwitch, ProtectNVREntity): """A UniFi Protect NVR Switch.""" - entity_description: ProtectSwitchEntityDescription - _state_attrs = ("_attr_available", "_attr_is_on") - - def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None: - super()._async_update_device_from_protect(device) - self._attr_is_on = self.entity_description.get_ufp_value(self.device) is True - - async def async_turn_on(self, **kwargs: Any) -> None: - """Turn the device on.""" - await self.entity_description.ufp_set(self.device, True) - - async def async_turn_off(self, **kwargs: Any) -> None: - """Turn the device off.""" - await self.entity_description.ufp_set(self.device, False) - class ProtectPrivacyModeSwitch(RestoreEntity, ProtectSwitch): """A UniFi Protect Switch.""" From affbc30d0d3e1d20d10f6ee978d90ecf6fe87904 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 16 Jun 2024 14:50:03 -0500 Subject: [PATCH 0727/1445] Move unifiprotect services register to async_setup (#119786) --- homeassistant/components/unifiprotect/__init__.py | 12 ++++-------- homeassistant/components/unifiprotect/data.py | 1 + homeassistant/components/unifiprotect/services.py | 13 ------------- 3 files changed, 5 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/unifiprotect/__init__.py b/homeassistant/components/unifiprotect/__init__.py index fa20c892850..068c5665e6b 100644 --- a/homeassistant/components/unifiprotect/__init__.py +++ b/homeassistant/components/unifiprotect/__init__.py @@ -39,7 +39,7 @@ from .const import ( from .data import ProtectData, UFPConfigEntry from .discovery import async_start_discovery from .migrate import async_migrate_data -from .services import async_cleanup_services, async_setup_services +from .services import async_setup_services from .utils import ( _async_unifi_mac_from_hass, async_create_api_client, @@ -61,6 +61,7 @@ EARLY_ACCESS_URL = ( async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the UniFi Protect.""" # Only start discovery once regardless of how many entries they have + async_setup_services(hass) async_start_discovery(hass) return True @@ -174,7 +175,6 @@ async def _async_setup_entry( raise ConfigEntryNotReady await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) - async_setup_services(hass) hass.http.register_view(ThumbnailProxyView(hass)) hass.http.register_view(VideoProxyView(hass)) @@ -186,13 +186,9 @@ async def _async_options_updated(hass: HomeAssistant, entry: UFPConfigEntry) -> async def async_unload_entry(hass: HomeAssistant, entry: UFPConfigEntry) -> bool: """Unload UniFi Protect config entry.""" - if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): - data = entry.runtime_data - await data.async_stop() - async_cleanup_services(hass) - - return bool(unload_ok) + await entry.runtime_data.async_stop() + return unload_ok async def async_remove_config_entry_device( diff --git a/homeassistant/components/unifiprotect/data.py b/homeassistant/components/unifiprotect/data.py index 59a5242273a..7dcb9768f9a 100644 --- a/homeassistant/components/unifiprotect/data.py +++ b/homeassistant/components/unifiprotect/data.py @@ -345,6 +345,7 @@ def async_ufp_instance_for_config_entry_ids( entry.runtime_data.api for entry_id in config_entry_ids if (entry := hass.config_entries.async_get_entry(entry_id)) + and hasattr(entry, "runtime_data") ), None, ) diff --git a/homeassistant/components/unifiprotect/services.py b/homeassistant/components/unifiprotect/services.py index 60345ac6403..119fe52756c 100644 --- a/homeassistant/components/unifiprotect/services.py +++ b/homeassistant/components/unifiprotect/services.py @@ -13,7 +13,6 @@ from uiprotect.exceptions import ClientError import voluptuous as vol from homeassistant.components.binary_sensor import BinarySensorDeviceClass -from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ATTR_DEVICE_ID, ATTR_NAME, Platform from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.exceptions import HomeAssistantError, ServiceValidationError @@ -238,15 +237,3 @@ def async_setup_services(hass: HomeAssistant) -> None: if hass.services.has_service(DOMAIN, name): continue hass.services.async_register(DOMAIN, name, method, schema=schema) - - -def async_cleanup_services(hass: HomeAssistant) -> None: - """Cleanup global UniFi Protect services (if all config entries unloaded).""" - loaded_entries = [ - entry - for entry in hass.config_entries.async_entries(DOMAIN) - if entry.state == ConfigEntryState.LOADED - ] - if len(loaded_entries) == 1: - for name in ALL_GLOBAL_SERIVCES: - hass.services.async_remove(DOMAIN, name) From 85ca6f15bea31fbd2d74d9d02396ed84403e0ac9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 16 Jun 2024 15:04:28 -0500 Subject: [PATCH 0728/1445] Add some suggested units to unifiprotect sensors (#119790) --- homeassistant/components/unifiprotect/sensor.py | 8 ++++++++ tests/components/unifiprotect/test_sensor.py | 6 +++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/unifiprotect/sensor.py b/homeassistant/components/unifiprotect/sensor.py index 78fc24026a3..e166d532dfb 100644 --- a/homeassistant/components/unifiprotect/sensor.py +++ b/homeassistant/components/unifiprotect/sensor.py @@ -181,6 +181,8 @@ CAMERA_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( entity_category=EntityCategory.DIAGNOSTIC, state_class=SensorStateClass.MEASUREMENT, ufp_value="stats.storage.used", + suggested_unit_of_measurement=UnitOfInformation.MEGABYTES, + suggested_display_precision=2, ), ProtectSensorEntityDescription( key="write_rate", @@ -191,6 +193,8 @@ CAMERA_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, ufp_value="stats.storage.rate_per_second", precision=2, + suggested_unit_of_measurement=UnitOfDataRate.MEGABYTES_PER_SECOND, + suggested_display_precision=2, ), ProtectSensorEntityDescription( key="voltage", @@ -280,6 +284,8 @@ CAMERA_DISABLED_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( entity_category=EntityCategory.DIAGNOSTIC, state_class=SensorStateClass.TOTAL_INCREASING, ufp_value="stats.rx_bytes", + suggested_unit_of_measurement=UnitOfInformation.MEGABYTES, + suggested_display_precision=2, ), ProtectSensorEntityDescription( key="stats_tx", @@ -290,6 +296,8 @@ CAMERA_DISABLED_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( entity_category=EntityCategory.DIAGNOSTIC, state_class=SensorStateClass.TOTAL_INCREASING, ufp_value="stats.tx_bytes", + suggested_unit_of_measurement=UnitOfInformation.MEGABYTES, + suggested_display_precision=2, ), ) diff --git a/tests/components/unifiprotect/test_sensor.py b/tests/components/unifiprotect/test_sensor.py index a3155376a05..ac631ee41a6 100644 --- a/tests/components/unifiprotect/test_sensor.py +++ b/tests/components/unifiprotect/test_sensor.py @@ -327,8 +327,8 @@ async def test_sensor_setup_camera( expected_values = ( fixed_now.replace(microsecond=0).isoformat(), - "100", - "100.0", + "0.0001", + "0.0001", "20.0", ) for index, description in enumerate(CAMERA_SENSORS_WRITE): @@ -348,7 +348,7 @@ async def test_sensor_setup_camera( assert state.state == expected_values[index] assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION - expected_values = ("100", "100") + expected_values = ("0.0001", "0.0001") for index, description in enumerate(CAMERA_DISABLED_SENSORS): unique_id, entity_id = ids_from_device_description( Platform.SENSOR, doorbell, description From 8c613bc869351a9d7f46ae3c850b9159b16a13f1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 16 Jun 2024 17:08:09 -0500 Subject: [PATCH 0729/1445] Cleanup unifiprotect ProtectData object (#119787) --- .../components/unifiprotect/button.py | 7 ++--- .../components/unifiprotect/camera.py | 5 ++-- homeassistant/components/unifiprotect/data.py | 28 +++++++++---------- .../components/unifiprotect/utils.py | 8 ------ 4 files changed, 18 insertions(+), 30 deletions(-) diff --git a/homeassistant/components/unifiprotect/button.py b/homeassistant/components/unifiprotect/button.py index f0824ad894c..6c0ef37e1df 100644 --- a/homeassistant/components/unifiprotect/button.py +++ b/homeassistant/components/unifiprotect/button.py @@ -20,11 +20,10 @@ from homeassistant.helpers import entity_registry as er from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DEVICES_THAT_ADOPT, DISPATCH_ADD, DOMAIN +from .const import DEVICES_THAT_ADOPT, DOMAIN from .data import UFPConfigEntry from .entity import ProtectDeviceEntity, async_all_device_entities from .models import PermRequired, ProtectEntityDescription, ProtectSetableKeysMixin, T -from .utils import async_dispatch_id as _ufpd _LOGGER = logging.getLogger(__name__) @@ -149,9 +148,7 @@ async def async_setup_entry( data.async_subscribe_adopt(_add_new_device) entry.async_on_unload( - async_dispatcher_connect( - hass, _ufpd(entry, DISPATCH_ADD), _async_add_unadopted_device - ) + async_dispatcher_connect(hass, data.add_signal, _async_add_unadopted_device) ) async_add_entities( diff --git a/homeassistant/components/unifiprotect/camera.py b/homeassistant/components/unifiprotect/camera.py index 67533472ea7..2a97aa26823 100644 --- a/homeassistant/components/unifiprotect/camera.py +++ b/homeassistant/components/unifiprotect/camera.py @@ -26,12 +26,11 @@ from .const import ( ATTR_FPS, ATTR_HEIGHT, ATTR_WIDTH, - DISPATCH_CHANNELS, DOMAIN, ) from .data import ProtectData, UFPConfigEntry from .entity import ProtectDeviceEntity -from .utils import async_dispatch_id as _ufpd, get_camera_base_name +from .utils import get_camera_base_name _LOGGER = logging.getLogger(__name__) @@ -153,7 +152,7 @@ async def async_setup_entry( data.async_subscribe_adopt(_add_new_device) entry.async_on_unload( - async_dispatcher_connect(hass, _ufpd(entry, DISPATCH_CHANNELS), _add_new_device) + async_dispatcher_connect(hass, data.channels_signal, _add_new_device) ) async_add_entities(_async_camera_entities(hass, entry, data)) diff --git a/homeassistant/components/unifiprotect/data.py b/homeassistant/components/unifiprotect/data.py index 7dcb9768f9a..2c1f447229a 100644 --- a/homeassistant/components/unifiprotect/data.py +++ b/homeassistant/components/unifiprotect/data.py @@ -43,7 +43,7 @@ from .const import ( DISPATCH_CHANNELS, DOMAIN, ) -from .utils import async_dispatch_id as _ufpd, async_get_devices_by_type +from .utils import async_get_devices_by_type _LOGGER = logging.getLogger(__name__) type ProtectDeviceType = ProtectAdoptableDeviceModel | NVR @@ -58,6 +58,12 @@ def async_last_update_was_successful( return hasattr(entry, "runtime_data") and entry.runtime_data.last_update_success +@callback +def _async_dispatch_id(entry: UFPConfigEntry, dispatch: str) -> str: + """Generate entry specific dispatch ID.""" + return f"{DOMAIN}.{entry.entry_id}.{dispatch}" + + class ProtectData: """Coordinate updates.""" @@ -69,9 +75,6 @@ class ProtectData: entry: UFPConfigEntry, ) -> None: """Initialize an subscriber.""" - super().__init__() - - self._hass = hass self._entry = entry self._hass = hass self._update_interval = update_interval @@ -80,10 +83,11 @@ class ProtectData: self._unsub_interval: CALLBACK_TYPE | None = None self._unsub_websocket: CALLBACK_TYPE | None = None self._auth_failures = 0 - self.last_update_success = False self.api = protect - self._adopt_signal = _ufpd(self._entry, DISPATCH_ADOPT) + self.adopt_signal = _async_dispatch_id(entry, DISPATCH_ADOPT) + self.add_signal = _async_dispatch_id(entry, DISPATCH_ADD) + self.channels_signal = _async_dispatch_id(entry, DISPATCH_CHANNELS) @property def disable_stream(self) -> bool: @@ -101,7 +105,7 @@ class ProtectData: ) -> None: """Add an callback for on device adopt.""" self._entry.async_on_unload( - async_dispatcher_connect(self._hass, self._adopt_signal, add_callback) + async_dispatcher_connect(self._hass, self.adopt_signal, add_callback) ) def get_by_types( @@ -184,12 +188,10 @@ class ProtectData: def _async_add_device(self, device: ProtectAdoptableDeviceModel) -> None: if device.is_adopted_by_us: _LOGGER.debug("Device adopted: %s", device.id) - async_dispatcher_send( - self._hass, _ufpd(self._entry, DISPATCH_ADOPT), device - ) + async_dispatcher_send(self._hass, self.adopt_signal, device) else: _LOGGER.debug("New device detected: %s", device.id) - async_dispatcher_send(self._hass, _ufpd(self._entry, DISPATCH_ADD), device) + async_dispatcher_send(self._hass, self.add_signal, device) @callback def _async_remove_device(self, device: ProtectAdoptableDeviceModel) -> None: @@ -214,9 +216,7 @@ class ProtectData: and "channels" in changed_data ): self._pending_camera_ids.remove(device.id) - async_dispatcher_send( - self._hass, _ufpd(self._entry, DISPATCH_CHANNELS), device - ) + async_dispatcher_send(self._hass, self.channels_signal, device) # trigger update for all Cameras with LCD screens when NVR Doorbell settings updates if "doorbell_settings" in changed_data: diff --git a/homeassistant/components/unifiprotect/utils.py b/homeassistant/components/unifiprotect/utils.py index 4fb7f6f7661..c9dcfa6b37f 100644 --- a/homeassistant/components/unifiprotect/utils.py +++ b/homeassistant/components/unifiprotect/utils.py @@ -36,7 +36,6 @@ from .const import ( CONF_ALL_UPDATES, CONF_OVERRIDE_CHOST, DEVICES_FOR_SUBSCRIBE, - DOMAIN, ModelType, ) @@ -121,13 +120,6 @@ def async_get_light_motion_current(obj: Light) -> str: return obj.light_mode_settings.mode.value -@callback -def async_dispatch_id(entry: UFPConfigEntry, dispatch: str) -> str: - """Generate entry specific dispatch ID.""" - - return f"{DOMAIN}.{entry.entry_id}.{dispatch}" - - @callback def async_create_api_client( hass: HomeAssistant, entry: UFPConfigEntry From 05e690ba0ddf984791ef13b08c92c8959491063f Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Mon, 17 Jun 2024 00:27:07 +0200 Subject: [PATCH 0730/1445] Remove not used group class method (#119798) --- homeassistant/components/group/entity.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/homeassistant/components/group/entity.py b/homeassistant/components/group/entity.py index 489226742ae..785895ff11a 100644 --- a/homeassistant/components/group/entity.py +++ b/homeassistant/components/group/entity.py @@ -3,7 +3,6 @@ from __future__ import annotations from abc import abstractmethod -import asyncio from collections.abc import Callable, Collection, Mapping import logging from typing import Any @@ -262,12 +261,6 @@ class Group(Entity): """Test if any member has an assumed state.""" return self._assumed_state - def update_tracked_entity_ids(self, entity_ids: Collection[str] | None) -> None: - """Update the member entity IDs.""" - asyncio.run_coroutine_threadsafe( - self.async_update_tracked_entity_ids(entity_ids), self.hass.loop - ).result() - async def async_update_tracked_entity_ids( self, entity_ids: Collection[str] | None ) -> None: From 4879c8b72e86cb88d71f00dfcf41ac1af28d4b22 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 16 Jun 2024 17:31:28 -0500 Subject: [PATCH 0731/1445] Increase unifiprotect polling interval to 60s (#119800) --- homeassistant/components/unifiprotect/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/unifiprotect/const.py b/homeassistant/components/unifiprotect/const.py index f51a58aadc7..9839d823585 100644 --- a/homeassistant/components/unifiprotect/const.py +++ b/homeassistant/components/unifiprotect/const.py @@ -35,7 +35,7 @@ CONFIG_OPTIONS = [ DEFAULT_PORT = 443 DEFAULT_ATTRIBUTION = "Powered by UniFi Protect Server" DEFAULT_BRAND = "Ubiquiti" -DEFAULT_SCAN_INTERVAL = 20 +DEFAULT_SCAN_INTERVAL = 60 DEFAULT_VERIFY_SSL = False DEFAULT_MAX_MEDIA = 1000 From fc3fbc68621df7cafc6edbe0e4a10cbcacb410c8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 16 Jun 2024 18:06:45 -0500 Subject: [PATCH 0732/1445] Bump uiprotect to 1.12.1 (#119799) --- homeassistant/components/unifiprotect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index f54d33984c0..3dcd0cd22b6 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -40,7 +40,7 @@ "integration_type": "hub", "iot_class": "local_push", "loggers": ["uiprotect", "unifi_discovery"], - "requirements": ["uiprotect==1.12.0", "unifi-discovery==1.1.8"], + "requirements": ["uiprotect==1.12.1", "unifi-discovery==1.1.8"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/requirements_all.txt b/requirements_all.txt index 75f21390db9..30647e29478 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2788,7 +2788,7 @@ twitchAPI==4.0.0 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==1.12.0 +uiprotect==1.12.1 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e790be1ddd4..ab90ad8ea36 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2168,7 +2168,7 @@ twitchAPI==4.0.0 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==1.12.0 +uiprotect==1.12.1 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 From bd37ce6e9a3ef7321f9be2c0094b70d42b7184e7 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Mon, 17 Jun 2024 05:36:06 +0200 Subject: [PATCH 0733/1445] Remove beat (internet time) from time_date (#119785) --- .../components/time_date/config_flow.py | 2 +- homeassistant/components/time_date/const.py | 1 - homeassistant/components/time_date/sensor.py | 47 ++----------- .../components/time_date/strings.json | 13 ---- tests/components/time_date/test_sensor.py | 68 +------------------ 5 files changed, 7 insertions(+), 124 deletions(-) diff --git a/homeassistant/components/time_date/config_flow.py b/homeassistant/components/time_date/config_flow.py index f65978144c6..9ae98992acb 100644 --- a/homeassistant/components/time_date/config_flow.py +++ b/homeassistant/components/time_date/config_flow.py @@ -35,7 +35,7 @@ USER_SCHEMA = vol.Schema( { vol.Required(CONF_DISPLAY_OPTIONS): SelectSelector( SelectSelectorConfig( - options=[option for option in OPTION_TYPES if option != "beat"], + options=OPTION_TYPES, mode=SelectSelectorMode.DROPDOWN, translation_key="display_options", ) diff --git a/homeassistant/components/time_date/const.py b/homeassistant/components/time_date/const.py index 5d13ec0203c..53656bae181 100644 --- a/homeassistant/components/time_date/const.py +++ b/homeassistant/components/time_date/const.py @@ -18,6 +18,5 @@ OPTION_TYPES = [ "date_time_utc", "date_time_iso", "time_date", - "beat", "time_utc", ] diff --git a/homeassistant/components/time_date/sensor.py b/homeassistant/components/time_date/sensor.py index 57bb87e6ea5..ed999e5a0b2 100644 --- a/homeassistant/components/time_date/sensor.py +++ b/homeassistant/components/time_date/sensor.py @@ -20,11 +20,10 @@ from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_point_in_utc_time -from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType import homeassistant.util.dt as dt_util -from .const import DOMAIN, OPTION_TYPES +from .const import OPTION_TYPES _LOGGER = logging.getLogger(__name__) @@ -51,23 +50,6 @@ async def async_setup_platform( _LOGGER.error("Timezone is not set in Home Assistant configuration") # type: ignore[unreachable] return False - if "beat" in config[CONF_DISPLAY_OPTIONS]: - async_create_issue( - hass, - DOMAIN, - "deprecated_beat", - breaks_in_ha_version="2024.7.0", - is_fixable=False, - severity=IssueSeverity.WARNING, - translation_key="deprecated_beat", - translation_placeholders={ - "config_key": "beat", - "display_options": "display_options", - "integration": DOMAIN, - }, - ) - _LOGGER.warning("'beat': is deprecated and will be removed in version 2024.7") - async_add_entities( [TimeDateSensor(variable) for variable in config[CONF_DISPLAY_OPTIONS]] ) @@ -95,8 +77,7 @@ class TimeDateSensor(SensorEntity): """Initialize the sensor.""" self._attr_translation_key = option_type self.type = option_type - object_id = "internet_time" if option_type == "beat" else option_type - self.entity_id = ENTITY_ID_FORMAT.format(object_id) + self.entity_id = ENTITY_ID_FORMAT.format(option_type) self._attr_unique_id = option_type if entry_id else None self._update_internal_state(dt_util.utcnow()) @@ -169,13 +150,8 @@ class TimeDateSensor(SensorEntity): tomorrow = dt_util.as_local(time_date) + timedelta(days=1) return dt_util.start_of_local_day(tomorrow) - if self.type == "beat": - # Add 1 hour because @0 beats is at 23:00:00 UTC. - timestamp = dt_util.as_timestamp(time_date + timedelta(hours=1)) - interval = 86.4 - else: - timestamp = dt_util.as_timestamp(time_date) - interval = 60 + timestamp = dt_util.as_timestamp(time_date) + interval = 60 delta = interval - (timestamp % interval) next_interval = time_date + timedelta(seconds=delta) @@ -201,21 +177,6 @@ class TimeDateSensor(SensorEntity): self._state = f"{time}, {date}" elif self.type == "time_utc": self._state = time_utc - elif self.type == "beat": - # Calculate Swatch Internet Time. - time_bmt = time_date + timedelta(hours=1) - delta = timedelta( - hours=time_bmt.hour, - minutes=time_bmt.minute, - seconds=time_bmt.second, - microseconds=time_bmt.microsecond, - ) - - # Use integers to better handle rounding. For example, - # int(63763.2/86.4) = 737 but 637632//864 = 738. - beat = int(delta.total_seconds() * 10) // 864 - - self._state = f"@{beat:03d}" elif self.type == "date_time_iso": self._state = dt_util.parse_datetime( f"{date} {time}", raise_on_error=True diff --git a/homeassistant/components/time_date/strings.json b/homeassistant/components/time_date/strings.json index e9efe949b9b..adf37253f90 100644 --- a/homeassistant/components/time_date/strings.json +++ b/homeassistant/components/time_date/strings.json @@ -66,18 +66,5 @@ "name": "[%key:component::time_date::selector::display_options::options::time_utc%]" } } - }, - "issues": { - "deprecated_beat": { - "title": "The `{config_key}` Time & Date sensor is being removed", - "fix_flow": { - "step": { - "confirm": { - "title": "[%key:component::time_date::issues::deprecated_beat::title%]", - "description": "Please remove the `{config_key}` key from the {integration} config entry options and click submit to fix this issue." - } - } - } - } } } diff --git a/tests/components/time_date/test_sensor.py b/tests/components/time_date/test_sensor.py index cbbf9a25d5c..ddeec48b3d2 100644 --- a/tests/components/time_date/test_sensor.py +++ b/tests/components/time_date/test_sensor.py @@ -5,10 +5,9 @@ from unittest.mock import ANY, Mock, patch from freezegun.api import FrozenDateTimeFactory import pytest -from homeassistant.components.time_date.const import DOMAIN, OPTION_TYPES +from homeassistant.components.time_date.const import OPTION_TYPES from homeassistant.core import HomeAssistant -from homeassistant.helpers import event, issue_registry as ir -from homeassistant.setup import async_setup_component +from homeassistant.helpers import event import homeassistant.util.dt as dt_util from . import load_int @@ -25,11 +24,6 @@ from tests.common import async_fire_time_changed dt_util.utc_from_timestamp(45.5), dt_util.utc_from_timestamp(60), ), - ( - "beat", - dt_util.parse_datetime("2020-11-13 00:00:29+01:00"), - dt_util.parse_datetime("2020-11-13 00:01:26.4+01:00"), - ), ( "date_time", dt_util.utc_from_timestamp(1495068899), @@ -83,9 +77,6 @@ async def test_states(hass: HomeAssistant, freezer: FrozenDateTimeFactory) -> No state = hass.states.get("sensor.date_time_utc") assert state.state == "2017-05-18, 00:54" - state = hass.states.get("sensor.internet_time") - assert state.state == "@079" - state = hass.states.get("sensor.date_time_iso") assert state.state == "2017-05-18T00:54:00" @@ -110,9 +101,6 @@ async def test_states(hass: HomeAssistant, freezer: FrozenDateTimeFactory) -> No state = hass.states.get("sensor.date_time_utc") assert state.state == "2020-10-17, 16:42" - state = hass.states.get("sensor.internet_time") - assert state.state == "@738" - state = hass.states.get("sensor.date_time_iso") assert state.state == "2020-10-17T16:42:00" @@ -143,9 +131,6 @@ async def test_states_non_default_timezone( state = hass.states.get("sensor.date_time_utc") assert state.state == "2017-05-18, 00:54" - state = hass.states.get("sensor.internet_time") - assert state.state == "@079" - state = hass.states.get("sensor.date_time_iso") assert state.state == "2017-05-17T20:54:00" @@ -170,9 +155,6 @@ async def test_states_non_default_timezone( state = hass.states.get("sensor.date_time_utc") assert state.state == "2020-10-17, 16:42" - state = hass.states.get("sensor.internet_time") - assert state.state == "@738" - state = hass.states.get("sensor.date_time_iso") assert state.state == "2020-10-17T12:42:00" @@ -195,9 +177,6 @@ async def test_states_non_default_timezone( state = hass.states.get("sensor.date_time_utc") assert state.state == "2020-10-17, 16:42" - state = hass.states.get("sensor.internet_time") - assert state.state == "@738" - state = hass.states.get("sensor.date_time_iso") assert state.state == "2020-10-17T18:42:00" @@ -280,48 +259,5 @@ async def test_icons(hass: HomeAssistant) -> None: assert state.attributes["icon"] == "mdi:calendar-clock" state = hass.states.get("sensor.date_time_utc") assert state.attributes["icon"] == "mdi:calendar-clock" - state = hass.states.get("sensor.internet_time") - assert state.attributes["icon"] == "mdi:clock" state = hass.states.get("sensor.date_time_iso") assert state.attributes["icon"] == "mdi:calendar-clock" - - -@pytest.mark.parametrize( - ( - "display_options", - "expected_warnings", - "expected_issues", - ), - [ - (["time", "date"], [], []), - (["beat"], ["'beat': is deprecated"], ["deprecated_beat"]), - (["time", "beat"], ["'beat': is deprecated"], ["deprecated_beat"]), - ], -) -async def test_deprecation_warning( - hass: HomeAssistant, - caplog: pytest.LogCaptureFixture, - display_options: list[str], - expected_warnings: list[str], - expected_issues: list[str], - issue_registry: ir.IssueRegistry, -) -> None: - """Test deprecation warning for swatch beat.""" - config = { - "sensor": { - "platform": "time_date", - "display_options": display_options, - } - } - - assert await async_setup_component(hass, "sensor", config) - await hass.async_block_till_done() - - warnings = [record for record in caplog.records if record.levelname == "WARNING"] - assert len(warnings) == len(expected_warnings) - for expected_warning in expected_warnings: - assert any(expected_warning in warning.message for warning in warnings) - - assert len(issue_registry.issues) == len(expected_issues) - for expected_issue in expected_issues: - assert (DOMAIN, expected_issue) in issue_registry.issues From f09063d70636ae0f87379ebf78de31272fff2974 Mon Sep 17 00:00:00 2001 From: MarkGodwin <10632972+MarkGodwin@users.noreply.github.com> Date: Mon, 17 Jun 2024 06:36:35 +0100 Subject: [PATCH 0734/1445] Add device trackers to tplink_omada (#115601) * Add device trackers to tplink_omada * tplink_omada - Remove trackers and options flow * Addressed code review feedback * Run linter * Use entity registry fixture --- .../components/tplink_omada/__init__.py | 9 +- .../components/tplink_omada/controller.py | 65 +++------- .../components/tplink_omada/coordinator.py | 64 +++++++++- .../components/tplink_omada/device_tracker.py | 107 ++++++++++++++++ tests/components/tplink_omada/conftest.py | 98 +++++++++++++- .../fixtures/connected-clients.json | 120 ++++++++++++++++++ .../tplink_omada/fixtures/known-clients.json | 67 ++++++++++ .../snapshots/test_device_tracker.ambr | 33 +++++ .../tplink_omada/test_device_tracker.py | 117 +++++++++++++++++ tests/components/tplink_omada/test_switch.py | 28 ++-- 10 files changed, 638 insertions(+), 70 deletions(-) create mode 100644 homeassistant/components/tplink_omada/device_tracker.py create mode 100644 tests/components/tplink_omada/fixtures/connected-clients.json create mode 100644 tests/components/tplink_omada/fixtures/known-clients.json create mode 100644 tests/components/tplink_omada/snapshots/test_device_tracker.ambr create mode 100644 tests/components/tplink_omada/test_device_tracker.py diff --git a/homeassistant/components/tplink_omada/__init__.py b/homeassistant/components/tplink_omada/__init__.py index fa022fcac77..19b3d58dbd4 100644 --- a/homeassistant/components/tplink_omada/__init__.py +++ b/homeassistant/components/tplink_omada/__init__.py @@ -19,7 +19,12 @@ from .config_flow import CONF_SITE, create_omada_client from .const import DOMAIN from .controller import OmadaSiteController -PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.SWITCH, Platform.UPDATE] +PLATFORMS: list[Platform] = [ + Platform.BINARY_SENSOR, + Platform.DEVICE_TRACKER, + Platform.SWITCH, + Platform.UPDATE, +] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: @@ -50,10 +55,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: gateway_coordinator = await controller.get_gateway_coordinator() if gateway_coordinator: await gateway_coordinator.async_config_entry_first_refresh() + await controller.get_clients_coordinator().async_config_entry_first_refresh() hass.data[DOMAIN][entry.entry_id] = controller await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + return True diff --git a/homeassistant/components/tplink_omada/controller.py b/homeassistant/components/tplink_omada/controller.py index c9842f93a5a..d92a6f37e24 100644 --- a/homeassistant/components/tplink_omada/controller.py +++ b/homeassistant/components/tplink_omada/controller.py @@ -1,58 +1,15 @@ """Controller for sharing Omada API coordinators between platforms.""" from tplink_omada_client import OmadaSiteClient -from tplink_omada_client.devices import ( - OmadaGateway, - OmadaSwitch, - OmadaSwitchPortDetails, -) +from tplink_omada_client.devices import OmadaSwitch from homeassistant.core import HomeAssistant -from .coordinator import OmadaCoordinator - -POLL_SWITCH_PORT = 300 -POLL_GATEWAY = 300 - - -class OmadaSwitchPortCoordinator(OmadaCoordinator[OmadaSwitchPortDetails]): # pylint: disable=hass-enforce-coordinator-module - """Coordinator for getting details about ports on a switch.""" - - def __init__( - self, - hass: HomeAssistant, - omada_client: OmadaSiteClient, - network_switch: OmadaSwitch, - ) -> None: - """Initialize my coordinator.""" - super().__init__( - hass, omada_client, f"{network_switch.name} Ports", POLL_SWITCH_PORT - ) - self._network_switch = network_switch - - async def poll_update(self) -> dict[str, OmadaSwitchPortDetails]: - """Poll a switch's current state.""" - ports = await self.omada_client.get_switch_ports(self._network_switch) - return {p.port_id: p for p in ports} - - -class OmadaGatewayCoordinator(OmadaCoordinator[OmadaGateway]): # pylint: disable=hass-enforce-coordinator-module - """Coordinator for getting details about the site's gateway.""" - - def __init__( - self, - hass: HomeAssistant, - omada_client: OmadaSiteClient, - mac: str, - ) -> None: - """Initialize my coordinator.""" - super().__init__(hass, omada_client, "Gateway", POLL_GATEWAY) - self.mac = mac - - async def poll_update(self) -> dict[str, OmadaGateway]: - """Poll a the gateway's current state.""" - gateway = await self.omada_client.get_gateway(self.mac) - return {self.mac: gateway} +from .coordinator import ( + OmadaClientsCoordinator, + OmadaGatewayCoordinator, + OmadaSwitchPortCoordinator, +) class OmadaSiteController: @@ -60,6 +17,7 @@ class OmadaSiteController: _gateway_coordinator: OmadaGatewayCoordinator | None = None _initialized_gateway_coordinator = False + _clients_coordinator: OmadaClientsCoordinator | None = None def __init__(self, hass: HomeAssistant, omada_client: OmadaSiteClient) -> None: """Create the controller.""" @@ -98,3 +56,12 @@ class OmadaSiteController: ) return self._gateway_coordinator + + def get_clients_coordinator(self) -> OmadaClientsCoordinator: + """Get coordinator for site's clients.""" + if not self._clients_coordinator: + self._clients_coordinator = OmadaClientsCoordinator( + self._hass, self._omada_client + ) + + return self._clients_coordinator diff --git a/homeassistant/components/tplink_omada/coordinator.py b/homeassistant/components/tplink_omada/coordinator.py index cfc07b38a49..da0a79ef991 100644 --- a/homeassistant/components/tplink_omada/coordinator.py +++ b/homeassistant/components/tplink_omada/coordinator.py @@ -4,7 +4,9 @@ import asyncio from datetime import timedelta import logging -from tplink_omada_client import OmadaSiteClient +from tplink_omada_client import OmadaSiteClient, OmadaSwitchPortDetails +from tplink_omada_client.clients import OmadaWirelessClient +from tplink_omada_client.devices import OmadaGateway, OmadaSwitch from tplink_omada_client.exceptions import OmadaClientException from homeassistant.core import HomeAssistant @@ -12,6 +14,10 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda _LOGGER = logging.getLogger(__name__) +POLL_SWITCH_PORT = 300 +POLL_GATEWAY = 300 +POLL_CLIENTS = 300 + class OmadaCoordinator[_T](DataUpdateCoordinator[dict[str, _T]]): """Coordinator for synchronizing bulk Omada data.""" @@ -43,3 +49,59 @@ class OmadaCoordinator[_T](DataUpdateCoordinator[dict[str, _T]]): async def poll_update(self) -> dict[str, _T]: """Poll the current data from the controller.""" raise NotImplementedError("Update method not implemented") + + +class OmadaSwitchPortCoordinator(OmadaCoordinator[OmadaSwitchPortDetails]): + """Coordinator for getting details about ports on a switch.""" + + def __init__( + self, + hass: HomeAssistant, + omada_client: OmadaSiteClient, + network_switch: OmadaSwitch, + ) -> None: + """Initialize my coordinator.""" + super().__init__( + hass, omada_client, f"{network_switch.name} Ports", POLL_SWITCH_PORT + ) + self._network_switch = network_switch + + async def poll_update(self) -> dict[str, OmadaSwitchPortDetails]: + """Poll a switch's current state.""" + ports = await self.omada_client.get_switch_ports(self._network_switch) + return {p.port_id: p for p in ports} + + +class OmadaGatewayCoordinator(OmadaCoordinator[OmadaGateway]): + """Coordinator for getting details about the site's gateway.""" + + def __init__( + self, + hass: HomeAssistant, + omada_client: OmadaSiteClient, + mac: str, + ) -> None: + """Initialize my coordinator.""" + super().__init__(hass, omada_client, "Gateway", POLL_GATEWAY) + self.mac = mac + + async def poll_update(self) -> dict[str, OmadaGateway]: + """Poll a the gateway's current state.""" + gateway = await self.omada_client.get_gateway(self.mac) + return {self.mac: gateway} + + +class OmadaClientsCoordinator(OmadaCoordinator[OmadaWirelessClient]): + """Coordinator for getting details about the site's connected clients.""" + + def __init__(self, hass: HomeAssistant, omada_client: OmadaSiteClient) -> None: + """Initialize my coordinator.""" + super().__init__(hass, omada_client, "ClientsList", POLL_CLIENTS) + + async def poll_update(self) -> dict[str, OmadaWirelessClient]: + """Poll the site's current active wi-fi clients.""" + return { + c.mac: c + async for c in self.omada_client.get_connected_clients() + if isinstance(c, OmadaWirelessClient) + } diff --git a/homeassistant/components/tplink_omada/device_tracker.py b/homeassistant/components/tplink_omada/device_tracker.py new file mode 100644 index 00000000000..be734592d11 --- /dev/null +++ b/homeassistant/components/tplink_omada/device_tracker.py @@ -0,0 +1,107 @@ +"""Connected Wi-Fi device scanners for TP-Link Omada access points.""" + +import logging + +from tplink_omada_client.clients import OmadaWirelessClient + +from homeassistant.components.device_tracker import ScannerEntity, SourceType +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .config_flow import CONF_SITE +from .const import DOMAIN +from .controller import OmadaClientsCoordinator, OmadaSiteController + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up device trackers and scanners.""" + + controller: OmadaSiteController = hass.data[DOMAIN][config_entry.entry_id] + + clients_coordinator = controller.get_clients_coordinator() + site_id = config_entry.data[CONF_SITE] + + # Add all known WiFi devices as potentially tracked devices. They will only be + # tracked if the user enables the entity. + async_add_entities( + [ + OmadaClientScannerEntity( + site_id, client.mac, client.name, clients_coordinator + ) + async for client in controller.omada_client.get_known_clients() + if isinstance(client, OmadaWirelessClient) + ] + ) + + +class OmadaClientScannerEntity( + CoordinatorEntity[OmadaClientsCoordinator], ScannerEntity +): + """Entity for a client connected to the Omada network.""" + + _client_details: OmadaWirelessClient | None = None + + def __init__( + self, + site_id: str, + client_id: str, + display_name: str, + coordinator: OmadaClientsCoordinator, + ) -> None: + """Initialize the scanner.""" + super().__init__(coordinator) + self._site_id = site_id + self._client_id = client_id + self._attr_name = display_name + + @property + def source_type(self) -> SourceType: + """Return the source type of the device.""" + return SourceType.ROUTER + + def _do_update(self) -> None: + self._client_details = self.coordinator.data.get(self._client_id) + + async def async_added_to_hass(self) -> None: + """When entity is added to hass.""" + await super().async_added_to_hass() + self._do_update() + + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + self._do_update() + self.async_write_ha_state() + + @property + def ip_address(self) -> str | None: + """Return the primary ip address of the device.""" + return self._client_details.ip if self._client_details else None + + @property + def mac_address(self) -> str | None: + """Return the mac address of the device.""" + return self._client_id + + @property + def hostname(self) -> str | None: + """Return hostname of the device.""" + return self._client_details.host_name if self._client_details else None + + @property + def is_connected(self) -> bool: + """Return true if the device is connected to the network.""" + return self._client_details.is_active if self._client_details else False + + @property + def unique_id(self) -> str | None: + """Return the unique id of the device.""" + return f"scanner_{self._site_id}_{self._client_id}" diff --git a/tests/components/tplink_omada/conftest.py b/tests/components/tplink_omada/conftest.py index 56af55ffd07..085cc32d1aa 100644 --- a/tests/components/tplink_omada/conftest.py +++ b/tests/components/tplink_omada/conftest.py @@ -1,9 +1,16 @@ """Test fixtures for TP-Link Omada integration.""" +from collections.abc import AsyncIterable import json from unittest.mock import AsyncMock, MagicMock, patch import pytest +from tplink_omada_client.clients import ( + OmadaConnectedClient, + OmadaNetworkClient, + OmadaWiredClient, + OmadaWirelessClient, +) from tplink_omada_client.devices import ( OmadaGateway, OmadaListDevice, @@ -49,29 +56,82 @@ def mock_setup_entry() -> Generator[AsyncMock]: @pytest.fixture def mock_omada_site_client() -> Generator[AsyncMock]: """Mock Omada site client.""" - site_client = AsyncMock() + site_client = MagicMock() gateway_data = json.loads(load_fixture("gateway-TL-ER7212PC.json", DOMAIN)) gateway = OmadaGateway(gateway_data) - site_client.get_gateway.return_value = gateway + site_client.get_gateway = AsyncMock(return_value=gateway) switch1_data = json.loads(load_fixture("switch-TL-SG3210XHP-M2.json", DOMAIN)) switch1 = OmadaSwitch(switch1_data) - site_client.get_switches.return_value = [switch1] + site_client.get_switches = AsyncMock(return_value=[switch1]) devices_data = json.loads(load_fixture("devices.json", DOMAIN)) devices = [OmadaListDevice(d) for d in devices_data] - site_client.get_devices.return_value = devices + site_client.get_devices = AsyncMock(return_value=devices) switch1_ports_data = json.loads( load_fixture("switch-ports-TL-SG3210XHP-M2.json", DOMAIN) ) switch1_ports = [OmadaSwitchPortDetails(p) for p in switch1_ports_data] - site_client.get_switch_ports.return_value = switch1_ports + site_client.get_switch_ports = AsyncMock(return_value=switch1_ports) + + async def async_empty() -> AsyncIterable: + for c in []: + yield c + + site_client.get_known_clients.return_value = async_empty() + site_client.get_connected_clients.return_value = async_empty() + return site_client + + +@pytest.fixture +def mock_omada_clients_only_site_client() -> Generator[AsyncMock]: + """Mock Omada site client containing only client connection data.""" + site_client = MagicMock() + + site_client.get_switches = AsyncMock(return_value=[]) + site_client.get_devices = AsyncMock(return_value=[]) + site_client.get_switch_ports = AsyncMock(return_value=[]) + site_client.get_client = AsyncMock(side_effect=_get_mock_client) + + site_client.get_known_clients.side_effect = _get_mock_known_clients + site_client.get_connected_clients.side_effect = _get_mock_connected_clients return site_client +async def _get_mock_known_clients() -> AsyncIterable[OmadaNetworkClient]: + """Mock known clients of the Omada network.""" + known_clients_data = json.loads(load_fixture("known-clients.json", DOMAIN)) + for c in known_clients_data: + if c["wireless"]: + yield OmadaWirelessClient(c) + else: + yield OmadaWiredClient(c) + + +async def _get_mock_connected_clients() -> AsyncIterable[OmadaConnectedClient]: + """Mock connected clients of the Omada network.""" + connected_clients_data = json.loads(load_fixture("connected-clients.json", DOMAIN)) + for c in connected_clients_data: + if c["wireless"]: + yield OmadaWirelessClient(c) + else: + yield OmadaWiredClient(c) + + +def _get_mock_client(mac: str) -> OmadaNetworkClient: + """Mock an Omada client.""" + connected_clients_data = json.loads(load_fixture("connected-clients.json", DOMAIN)) + + for c in connected_clients_data: + if c["mac"] == mac: + if c["wireless"]: + return OmadaWirelessClient(c) + return OmadaWiredClient(c) + + @pytest.fixture def mock_omada_client(mock_omada_site_client: AsyncMock) -> Generator[MagicMock]: """Mock Omada client.""" @@ -85,13 +145,39 @@ def mock_omada_client(mock_omada_site_client: AsyncMock) -> Generator[MagicMock] yield client +@pytest.fixture +def mock_omada_clients_only_client( + mock_omada_clients_only_site_client: AsyncMock, +) -> Generator[MagicMock]: + """Mock Omada client.""" + with patch( + "homeassistant.components.tplink_omada.create_omada_client", + autospec=True, + ) as client_mock: + client = client_mock.return_value + + client.get_site_client.return_value = mock_omada_clients_only_site_client + yield client + + @pytest.fixture async def init_integration( hass: HomeAssistant, - mock_config_entry: MockConfigEntry, mock_omada_client: MagicMock, ) -> MockConfigEntry: """Set up the TP-Link Omada integration for testing.""" + mock_config_entry = MockConfigEntry( + title="Test Omada Controller", + domain=DOMAIN, + data={ + CONF_HOST: "127.0.0.1", + CONF_PASSWORD: "mocked-password", + CONF_USERNAME: "mocked-user", + CONF_VERIFY_SSL: False, + CONF_SITE: "Default", + }, + unique_id="12345", + ) mock_config_entry.add_to_hass(hass) await hass.config_entries.async_setup(mock_config_entry.entry_id) diff --git a/tests/components/tplink_omada/fixtures/connected-clients.json b/tests/components/tplink_omada/fixtures/connected-clients.json new file mode 100644 index 00000000000..3139db7d4df --- /dev/null +++ b/tests/components/tplink_omada/fixtures/connected-clients.json @@ -0,0 +1,120 @@ +[ + { + "mac": "16-32-50-ED-FB-15", + "name": "16-32-50-ED-FB-15", + "deviceType": "unknown", + "ip": "192.168.1.177", + "connectType": 1, + "connectDevType": "ap", + "connectedToWirelessRouter": false, + "wireless": true, + "ssid": "OFFICE_SSID", + "signalLevel": 62, + "healthScore": -1, + "signalRank": 4, + "wifiMode": 4, + "apName": "Office", + "apMac": "E8-48-B8-7E-C7-1A", + "radioId": 0, + "channel": 1, + "rxRate": 65000, + "txRate": 72000, + "powerSave": false, + "rssi": -65, + "snr": 30, + "stackableSwitch": false, + "vid": 0, + "activity": 96, + "trafficDown": 25412800785, + "trafficUp": 1636427981, + "uptime": 621441, + "lastSeen": 1713109713169, + "authStatus": 0, + "guest": false, + "active": true, + "manager": false, + "downPacket": 30179275, + "upPacket": 14288106, + "support5g2": false, + "multiLink": [] + }, + { + "mac": "2E-DC-E1-C4-37-D3", + "name": "Apple", + "deviceType": "unknown", + "ip": "192.168.1.192", + "connectType": 1, + "connectDevType": "ap", + "connectedToWirelessRouter": false, + "wireless": true, + "ssid": "ROAMING_SSID", + "signalLevel": 67, + "healthScore": -1, + "signalRank": 4, + "wifiMode": 5, + "apName": "Spare Room", + "apMac": "C0-C9-E3-4B-AF-0E", + "radioId": 1, + "channel": 44, + "rxRate": 7000, + "txRate": 390000, + "powerSave": false, + "rssi": -63, + "snr": 32, + "stackableSwitch": false, + "vid": 0, + "activity": 0, + "trafficDown": 3327229, + "trafficUp": 746841, + "uptime": 2091, + "lastSeen": 1713109728764, + "authStatus": 0, + "guest": false, + "active": true, + "manager": false, + "downPacket": 5128, + "upPacket": 3611, + "support5g2": false, + "multiLink": [] + }, + { + "mac": "2C-71-FF-ED-34-83", + "name": "Banana", + "hostName": "testhost", + "deviceType": "unknown", + "ip": "192.168.1.102", + "connectType": 1, + "connectDevType": "ap", + "connectedToWirelessRouter": false, + "wireless": true, + "ssid": "ROAMING_SSID", + "signalLevel": 57, + "healthScore": -1, + "signalRank": 3, + "wifiMode": 5, + "apName": "Living Room", + "apMac": "C0-C9-E3-4B-A7-FE", + "radioId": 1, + "channel": 36, + "rxRate": 6000, + "txRate": 390000, + "powerSave": false, + "rssi": -67, + "snr": 28, + "stackableSwitch": false, + "vid": 0, + "activity": 39, + "trafficDown": 407300090, + "trafficUp": 94910187, + "uptime": 621461, + "lastSeen": 1713109729576, + "authStatus": 0, + "guest": false, + "active": true, + "manager": false, + "downPacket": 477858, + "upPacket": 501956, + "support5g2": false, + "multiLink": [] + } +] diff --git a/tests/components/tplink_omada/fixtures/known-clients.json b/tests/components/tplink_omada/fixtures/known-clients.json new file mode 100644 index 00000000000..31d951fab50 --- /dev/null +++ b/tests/components/tplink_omada/fixtures/known-clients.json @@ -0,0 +1,67 @@ +[ + { + "name": "16-32-50-ED-FB-15", + "mac": "16-32-50-ED-FB-15", + "wireless": true, + "guest": false, + "download": 259310931013, + "upload": 43957031162, + "duration": 6832173, + "lastSeen": 1712488285622, + "block": false, + "manager": false, + "lockToAp": false + }, + { + "name": "Banana", + "mac": "2C-71-FF-ED-34-83", + "wireless": true, + "guest": false, + "download": 22093851790, + "upload": 6961197401, + "duration": 16192898, + "lastSeen": 1712488285767, + "block": false, + "manager": false, + "lockToAp": false + }, + { + "name": "Pear", + "mac": "2C-D2-6B-BA-9C-94", + "wireless": true, + "guest": false, + "download": 0, + "upload": 0, + "duration": 23, + "lastSeen": 1713083620997, + "block": false, + "manager": false, + "lockToAp": false + }, + { + "name": "Apple", + "mac": "2E-DC-E1-C4-37-D3", + "wireless": true, + "guest": false, + "download": 1366833567, + "upload": 30126947, + "duration": 60255, + "lastSeen": 1713107649827, + "block": false, + "manager": false, + "lockToAp": false + }, + { + "name": "32-39-24-B1-67-23", + "mac": "32-39-24-B1-67-23", + "wireless": false, + "guest": false, + "download": 1621140542, + "upload": 433306522, + "duration": 60571, + "lastSeen": 1713107438528, + "block": false, + "manager": false, + "lockToAp": false + } +] diff --git a/tests/components/tplink_omada/snapshots/test_device_tracker.ambr b/tests/components/tplink_omada/snapshots/test_device_tracker.ambr new file mode 100644 index 00000000000..8adc4c26f12 --- /dev/null +++ b/tests/components/tplink_omada/snapshots/test_device_tracker.ambr @@ -0,0 +1,33 @@ +# serializer version: 1 +# name: test_device_scanner_created + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Banana', + 'host_name': 'testhost', + 'ip': '192.168.1.102', + 'mac': '2C-71-FF-ED-34-83', + 'source_type': , + }), + 'context': , + 'entity_id': 'device_tracker.banana', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'home', + }) +# --- +# name: test_device_scanner_update_to_away_nulls_properties + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Banana', + 'mac': '2C-71-FF-ED-34-83', + 'source_type': , + }), + 'context': , + 'entity_id': 'device_tracker.banana', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'not_home', + }) +# --- diff --git a/tests/components/tplink_omada/test_device_tracker.py b/tests/components/tplink_omada/test_device_tracker.py new file mode 100644 index 00000000000..199789b87d5 --- /dev/null +++ b/tests/components/tplink_omada/test_device_tracker.py @@ -0,0 +1,117 @@ +"""Tests for TP-Link Omada device tracker entities.""" + +from collections.abc import AsyncIterable +from datetime import timedelta +from unittest.mock import MagicMock + +import pytest +from syrupy.assertion import SnapshotAssertion +from tplink_omada_client.clients import OmadaConnectedClient + +from homeassistant.components.tplink_omada.const import DOMAIN +from homeassistant.components.tplink_omada.coordinator import POLL_CLIENTS +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er +from homeassistant.util.dt import utcnow + +from tests.common import MockConfigEntry, async_fire_time_changed + +UPDATE_INTERVAL = timedelta(seconds=10) +POLL_INTERVAL = timedelta(seconds=POLL_CLIENTS + 10) + +MOCK_ENTRY_DATA = { + "host": "https://fake.omada.host", + "verify_ssl": True, + "site": "SiteId", + "username": "test-username", + "password": "test-password", +} + + +@pytest.fixture +async def init_integration( + hass: HomeAssistant, + mock_omada_clients_only_client: MagicMock, +) -> MockConfigEntry: + """Set up the TP-Link Omada integration for testing.""" + mock_config_entry = MockConfigEntry( + title="Test Omada Controller", + domain=DOMAIN, + data=dict(MOCK_ENTRY_DATA), + unique_id="12345", + ) + mock_config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + return mock_config_entry + + +async def test_device_scanner_created( + hass: HomeAssistant, + init_integration: MockConfigEntry, + snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, +) -> None: + """Test gateway connected switches.""" + + entity_id = "device_tracker.banana" + + updated_entity = entity_registry.async_update_entity(entity_id, disabled_by=None) + assert not updated_entity.disabled + async_fire_time_changed(hass, utcnow() + POLL_INTERVAL) + await hass.async_block_till_done() + + entity = hass.states.get(entity_id) + assert entity is not None + assert entity == snapshot + + +async def test_device_scanner_update_to_away_nulls_properties( + hass: HomeAssistant, + mock_omada_clients_only_site_client: MagicMock, + init_integration: MockConfigEntry, + snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, +) -> None: + """Test gateway connected switches.""" + + entity_id = "device_tracker.banana" + + updated_entity = entity_registry.async_update_entity(entity_id, disabled_by=None) + assert not updated_entity.disabled + async_fire_time_changed(hass, utcnow() + POLL_INTERVAL) + await hass.async_block_till_done() + + entity = hass.states.get(entity_id) + await _setup_client_disconnect( + mock_omada_clients_only_site_client, "2C-71-FF-ED-34-83" + ) + + async_fire_time_changed(hass, utcnow() + (POLL_INTERVAL * 2)) + await hass.async_block_till_done() + + entity = hass.states.get(entity_id) + assert entity is not None + assert entity == snapshot + + mock_omada_clients_only_site_client.get_connected_clients.assert_called_once() + + +async def _setup_client_disconnect( + mock_omada_site_client: MagicMock, + client_mac: str, +): + original_clients = [ + c + async for c in mock_omada_site_client.get_connected_clients() + if c.mac != client_mac + ] + + async def get_filtered_clients() -> AsyncIterable[OmadaConnectedClient]: + for c in original_clients: + yield c + + mock_omada_site_client.get_connected_clients.reset_mock() + mock_omada_site_client.get_connected_clients.side_effect = get_filtered_clients diff --git a/tests/components/tplink_omada/test_switch.py b/tests/components/tplink_omada/test_switch.py index be2c21d02ab..7d83140cc95 100644 --- a/tests/components/tplink_omada/test_switch.py +++ b/tests/components/tplink_omada/test_switch.py @@ -2,7 +2,7 @@ from datetime import timedelta from typing import Any -from unittest.mock import MagicMock +from unittest.mock import AsyncMock, MagicMock from syrupy.assertion import SnapshotAssertion from tplink_omada_client import SwitchPortOverrides @@ -17,7 +17,7 @@ from tplink_omada_client.devices import ( from tplink_omada_client.exceptions import InvalidDevice from homeassistant.components import switch -from homeassistant.components.tplink_omada.controller import POLL_GATEWAY +from homeassistant.components.tplink_omada.coordinator import POLL_GATEWAY from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -34,6 +34,7 @@ async def test_poe_switches( mock_omada_site_client: MagicMock, init_integration: MockConfigEntry, snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, ) -> None: """Test PoE switch.""" poe_switch_mac = "54-AF-97-00-00-01" @@ -44,6 +45,7 @@ async def test_poe_switches( poe_switch_mac, 1, snapshot, + entity_registry, ) await _test_poe_switch( @@ -53,6 +55,7 @@ async def test_poe_switches( poe_switch_mac, 2, snapshot, + entity_registry, ) @@ -84,10 +87,11 @@ async def test_gateway_connect_ipv4_switch( port_status = test_gateway.port_status[3] assert port_status.port_number == 4 - mock_omada_site_client.set_gateway_wan_port_connect_state.reset_mock() - mock_omada_site_client.set_gateway_wan_port_connect_state.return_value = ( - _get_updated_gateway_port_status( - mock_omada_site_client, test_gateway, 3, "internetState", 0 + mock_omada_site_client.set_gateway_wan_port_connect_state = AsyncMock( + return_value=( + _get_updated_gateway_port_status( + mock_omada_site_client, test_gateway, 3, "internetState", 0 + ) ) ) await call_service(hass, "turn_off", entity_id) @@ -136,8 +140,8 @@ async def test_gateway_port_poe_switch( port_config = test_gateway.port_configs[4] assert port_config.port_number == 5 - mock_omada_site_client.set_gateway_port_settings.return_value = ( - OmadaGatewayPortConfig(port_config.raw_data, poe_enabled=False) + mock_omada_site_client.set_gateway_port_settings = AsyncMock( + return_value=(OmadaGatewayPortConfig(port_config.raw_data, poe_enabled=False)) ) await call_service(hass, "turn_off", entity_id) _assert_gateway_poe_set(mock_omada_site_client, test_gateway, False) @@ -239,9 +243,8 @@ async def _test_poe_switch( network_switch_mac: str, port_num: int, snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, ) -> None: - entity_registry = er.async_get(hass) - def assert_update_switch_port( device: OmadaSwitch, switch_port_details: OmadaSwitchPortDetails, @@ -260,9 +263,8 @@ async def _test_poe_switch( entry = entity_registry.async_get(entity_id) assert entry == snapshot - mock_omada_site_client.update_switch_port.reset_mock() - mock_omada_site_client.update_switch_port.return_value = await _update_port_details( - mock_omada_site_client, port_num, False + mock_omada_site_client.update_switch_port = AsyncMock( + return_value=await _update_port_details(mock_omada_site_client, port_num, False) ) await call_service(hass, "turn_off", entity_id) From b1c5845d35e56d0ecab41750a0138295b4487b01 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 17 Jun 2024 02:51:28 -0500 Subject: [PATCH 0735/1445] Bump uiprotect to 1.17.0 (#119802) * Bump uiprotect to 1.16.0 changelog: https://github.com/uilibs/uiprotect/compare/v1.12.1...v1.16.0 * one more bump --- homeassistant/components/unifiprotect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index 3dcd0cd22b6..cde29aa1770 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -40,7 +40,7 @@ "integration_type": "hub", "iot_class": "local_push", "loggers": ["uiprotect", "unifi_discovery"], - "requirements": ["uiprotect==1.12.1", "unifi-discovery==1.1.8"], + "requirements": ["uiprotect==1.17.0", "unifi-discovery==1.1.8"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/requirements_all.txt b/requirements_all.txt index 30647e29478..a8cdae1fed6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2788,7 +2788,7 @@ twitchAPI==4.0.0 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==1.12.1 +uiprotect==1.17.0 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ab90ad8ea36..7ded396a4e9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2168,7 +2168,7 @@ twitchAPI==4.0.0 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==1.12.1 +uiprotect==1.17.0 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 From 75b0acf6b69e94bd5872ff08e8e95788004a3e19 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Mon, 17 Jun 2024 09:52:25 +0200 Subject: [PATCH 0736/1445] Remove YAML import from System monitor (#119782) --- .../components/systemmonitor/config_flow.py | 37 ------- .../components/systemmonitor/sensor.py | 77 +------------ .../systemmonitor/test_config_flow.py | 101 +----------------- tests/components/systemmonitor/test_sensor.py | 53 --------- 4 files changed, 2 insertions(+), 266 deletions(-) diff --git a/homeassistant/components/systemmonitor/config_flow.py b/homeassistant/components/systemmonitor/config_flow.py index 924f63c8d1c..0ff882d89da 100644 --- a/homeassistant/components/systemmonitor/config_flow.py +++ b/homeassistant/components/systemmonitor/config_flow.py @@ -8,11 +8,9 @@ from typing import Any import voluptuous as vol from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN -from homeassistant.components.homeassistant import DOMAIN as HOMEASSISTANT_DOMAIN from homeassistant.config_entries import ConfigFlowResult from homeassistant.core import callback from homeassistant.helpers import entity_registry as er -from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.schema_config_entry_flow import ( SchemaCommonFlowHandler, SchemaConfigFlowHandler, @@ -53,37 +51,6 @@ async def validate_sensor_setup( return {} -async def validate_import_sensor_setup( - handler: SchemaCommonFlowHandler, user_input: dict[str, Any] -) -> dict[str, Any]: - """Validate sensor input.""" - # Standard behavior is to merge the result with the options. - # In this case, we want to add a sub-item so we update the options directly. - sensors: dict[str, list] = handler.options.setdefault(BINARY_SENSOR_DOMAIN, {}) - import_processes: list[str] = user_input["processes"] - processes = sensors.setdefault(CONF_PROCESS, []) - processes.extend(import_processes) - legacy_resources: list[str] = handler.options.setdefault("resources", []) - legacy_resources.extend(user_input["legacy_resources"]) - - async_create_issue( - handler.parent_handler.hass, - HOMEASSISTANT_DOMAIN, - f"deprecated_yaml_{DOMAIN}", - breaks_in_ha_version="2024.7.0", - is_fixable=False, - is_persistent=False, - issue_domain=DOMAIN, - severity=IssueSeverity.WARNING, - translation_key="deprecated_yaml", - translation_placeholders={ - "domain": DOMAIN, - "integration_title": "System Monitor", - }, - ) - return {} - - async def get_sensor_setup_schema(handler: SchemaCommonFlowHandler) -> vol.Schema: """Return process sensor setup schema.""" hass = handler.parent_handler.hass @@ -112,10 +79,6 @@ async def get_suggested_value(handler: SchemaCommonFlowHandler) -> dict[str, Any CONFIG_FLOW = { "user": SchemaFlowFormStep(schema=vol.Schema({})), - "import": SchemaFlowFormStep( - schema=vol.Schema({}), - validate_user_input=validate_import_sensor_setup, - ), } OPTIONS_FLOW = { "init": SchemaFlowFormStep( diff --git a/homeassistant/components/systemmonitor/sensor.py b/homeassistant/components/systemmonitor/sensor.py index 3634820ba30..bad4c3be0b5 100644 --- a/homeassistant/components/systemmonitor/sensor.py +++ b/homeassistant/components/systemmonitor/sensor.py @@ -15,20 +15,15 @@ import time from typing import Any, Literal from psutil import NoSuchProcess -import voluptuous as vol from homeassistant.components.sensor import ( DOMAIN as SENSOR_DOMAIN, - PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, SensorEntityDescription, SensorStateClass, ) -from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import ( - CONF_RESOURCES, - CONF_TYPE, PERCENTAGE, STATE_OFF, STATE_ON, @@ -39,11 +34,10 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_registry as er -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType +from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util import slugify @@ -410,20 +404,6 @@ SENSOR_TYPES: dict[str, SysMonitorSensorEntityDescription] = { } -def check_required_arg(value: Any) -> Any: - """Validate that the required "arg" for the sensor types that need it are set.""" - for sensor in value: - sensor_type = sensor[CONF_TYPE] - sensor_arg = sensor.get(CONF_ARG) - - if sensor_arg is None and SENSOR_TYPES[sensor_type].mandatory_arg: - raise vol.RequiredFieldInvalid( - f"Mandatory 'arg' is missing for sensor type '{sensor_type}'." - ) - - return value - - def check_legacy_resource(resource: str, resources: set[str]) -> bool: """Return True if legacy resource was configured.""" # This function to check legacy resources can be removed @@ -435,23 +415,6 @@ def check_legacy_resource(resource: str, resources: set[str]) -> bool: return False -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_RESOURCES, default={CONF_TYPE: "disk_use"}): vol.All( - cv.ensure_list, - [ - vol.Schema( - { - vol.Required(CONF_TYPE): vol.In(SENSOR_TYPES), - vol.Optional(CONF_ARG): cv.string, - } - ) - ], - check_required_arg, - ) - } -) - IO_COUNTER = { "network_out": 0, "network_in": 1, @@ -463,44 +426,6 @@ IO_COUNTER = { IF_ADDRS_FAMILY = {"ipv4_address": socket.AF_INET, "ipv6_address": socket.AF_INET6} -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigType, - async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the system monitor sensors.""" - processes = [ - resource[CONF_ARG] - for resource in config[CONF_RESOURCES] - if resource[CONF_TYPE] == "process" - ] - legacy_config: list[dict[str, str]] = config[CONF_RESOURCES] - resources = [] - for resource_conf in legacy_config: - if (_type := resource_conf[CONF_TYPE]).startswith("disk_"): - if (arg := resource_conf.get(CONF_ARG)) is None: - resources.append(f"{_type}_/") - continue - resources.append(f"{_type}_{arg}") - continue - resources.append(f"{_type}_{resource_conf.get(CONF_ARG, '')}") - _LOGGER.debug( - "Importing config with processes: %s, resources: %s", processes, resources - ) - - # With removal of the import also cleanup legacy_resources logic in setup_entry - # Also cleanup entry.options["resources"] which is only imported for legacy reasons - - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data={"processes": processes, "legacy_resources": resources}, - ) - ) - - async def async_setup_entry( hass: HomeAssistant, entry: SystemMonitorConfigEntry, diff --git a/tests/components/systemmonitor/test_config_flow.py b/tests/components/systemmonitor/test_config_flow.py index bd98099accc..f5cc46da096 100644 --- a/tests/components/systemmonitor/test_config_flow.py +++ b/tests/components/systemmonitor/test_config_flow.py @@ -5,15 +5,10 @@ from __future__ import annotations from unittest.mock import AsyncMock from homeassistant import config_entries -from homeassistant.components.homeassistant import DOMAIN as HOMEASSISTANT_DOMAIN from homeassistant.components.systemmonitor.const import CONF_PROCESS, DOMAIN from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType -from homeassistant.helpers import ( - device_registry as dr, - entity_registry as er, - issue_registry as ir, -) +from homeassistant.helpers import device_registry as dr, entity_registry as er from tests.common import MockConfigEntry @@ -39,51 +34,6 @@ async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: assert len(mock_setup_entry.mock_calls) == 1 -async def test_import( - hass: HomeAssistant, mock_setup_entry: AsyncMock, issue_registry: ir.IssueRegistry -) -> None: - """Test import.""" - - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={ - "processes": ["systemd", "octave-cli"], - "legacy_resources": [ - "disk_use_percent_/", - "memory_free_", - "network_out_eth0", - "process_systemd", - "process_octave-cli", - ], - }, - ) - await hass.async_block_till_done() - - assert result["type"] is FlowResultType.CREATE_ENTRY - assert result["options"] == { - "binary_sensor": {"process": ["systemd", "octave-cli"]}, - "resources": [ - "disk_use_percent_/", - "memory_free_", - "network_out_eth0", - "process_systemd", - "process_octave-cli", - ], - } - - assert len(mock_setup_entry.mock_calls) == 1 - - issue = issue_registry.async_get_issue( - HOMEASSISTANT_DOMAIN, f"deprecated_yaml_{DOMAIN}" - ) - assert issue.issue_domain == DOMAIN - assert issue.translation_placeholders == { - "domain": DOMAIN, - "integration_title": "System Monitor", - } - - async def test_form_already_configured( hass: HomeAssistant, mock_setup_entry: AsyncMock ) -> None: @@ -111,55 +61,6 @@ async def test_form_already_configured( assert result["reason"] == "already_configured" -async def test_import_already_configured( - hass: HomeAssistant, mock_setup_entry: AsyncMock, issue_registry: ir.IssueRegistry -) -> None: - """Test abort when already configured for import.""" - config_entry = MockConfigEntry( - domain=DOMAIN, - source=config_entries.SOURCE_USER, - options={ - "binary_sensor": [{CONF_PROCESS: "systemd"}], - "resources": [ - "disk_use_percent_/", - "memory_free_", - "network_out_eth0", - "process_systemd", - "process_octave-cli", - ], - }, - ) - config_entry.add_to_hass(hass) - - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={ - "processes": ["systemd", "octave-cli"], - "legacy_resources": [ - "disk_use_percent_/", - "memory_free_", - "network_out_eth0", - "process_systemd", - "process_octave-cli", - ], - }, - ) - await hass.async_block_till_done() - - assert result["type"] is FlowResultType.ABORT - assert result["reason"] == "already_configured" - - issue = issue_registry.async_get_issue( - HOMEASSISTANT_DOMAIN, f"deprecated_yaml_{DOMAIN}" - ) - assert issue.issue_domain == DOMAIN - assert issue.translation_placeholders == { - "domain": DOMAIN, - "integration_title": "System Monitor", - } - - async def test_add_and_remove_processes( hass: HomeAssistant, device_registry: dr.DeviceRegistry, diff --git a/tests/components/systemmonitor/test_sensor.py b/tests/components/systemmonitor/test_sensor.py index 8f0f316b5f8..ce15083da67 100644 --- a/tests/components/systemmonitor/test_sensor.py +++ b/tests/components/systemmonitor/test_sensor.py @@ -17,7 +17,6 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE, STATE_UNKNOWN from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from homeassistant.setup import async_setup_component from .conftest import MockProcess @@ -142,58 +141,6 @@ async def test_sensor_icon( assert get_cpu_icon() == "mdi:cpu-64-bit" -@pytest.mark.usefixtures("entity_registry_enabled_by_default") -async def test_sensor_yaml( - hass: HomeAssistant, - mock_psutil: Mock, - mock_os: Mock, -) -> None: - """Test the sensor imported from YAML.""" - config = { - "sensor": { - "platform": "systemmonitor", - "resources": [ - {"type": "disk_use_percent"}, - {"type": "disk_use_percent", "arg": "/media/share"}, - {"type": "memory_free", "arg": "/"}, - {"type": "network_out", "arg": "eth0"}, - {"type": "process", "arg": "python3"}, - ], - } - } - assert await async_setup_component(hass, "sensor", config) - await hass.async_block_till_done() - memory_sensor = hass.states.get("sensor.system_monitor_memory_free") - assert memory_sensor is not None - assert memory_sensor.state == "40.0" - - process_sensor = hass.states.get("binary_sensor.system_monitor_process_python3") - assert process_sensor is not None - assert process_sensor.state == STATE_ON - - -@pytest.mark.usefixtures("entity_registry_enabled_by_default") -async def test_sensor_yaml_fails_missing_argument( - caplog: pytest.LogCaptureFixture, - hass: HomeAssistant, - mock_psutil: Mock, - mock_os: Mock, -) -> None: - """Test the sensor imported from YAML fails on missing mandatory argument.""" - config = { - "sensor": { - "platform": "systemmonitor", - "resources": [ - {"type": "network_in"}, - ], - } - } - assert await async_setup_component(hass, "sensor", config) - await hass.async_block_till_done() - - assert "Mandatory 'arg' is missing for sensor type 'network_in'" in caplog.text - - async def test_sensor_updating( hass: HomeAssistant, mock_psutil: Mock, From 09b49ee50539df25b866e1769a78790c0ca0dae7 Mon Sep 17 00:00:00 2001 From: 0bmay <57501269+0bmay@users.noreply.github.com> Date: Mon, 17 Jun 2024 01:02:42 -0700 Subject: [PATCH 0737/1445] Bump py-canary to v0.5.4 (#119793) fix gathering data from Canary sensors --- homeassistant/components/canary/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/canary/manifest.json b/homeassistant/components/canary/manifest.json index e6bc52540d5..4d5adf4a32b 100644 --- a/homeassistant/components/canary/manifest.json +++ b/homeassistant/components/canary/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/canary", "iot_class": "cloud_polling", "loggers": ["canary"], - "requirements": ["py-canary==0.5.3"] + "requirements": ["py-canary==0.5.4"] } diff --git a/requirements_all.txt b/requirements_all.txt index a8cdae1fed6..f7693f6daaa 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1625,7 +1625,7 @@ pvo==2.1.1 py-aosmith==1.0.8 # homeassistant.components.canary -py-canary==0.5.3 +py-canary==0.5.4 # homeassistant.components.ccm15 py-ccm15==0.0.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7ded396a4e9..ac7d942324b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1293,7 +1293,7 @@ pvo==2.1.1 py-aosmith==1.0.8 # homeassistant.components.canary -py-canary==0.5.3 +py-canary==0.5.4 # homeassistant.components.ccm15 py-ccm15==0.0.9 From d1d21811fa3e5c1c4853e8fa1335ee6e6db30235 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Mon, 17 Jun 2024 10:04:18 +0200 Subject: [PATCH 0738/1445] Remove YAML import from streamlabswater (#119783) --- .../components/streamlabswater/__init__.py | 66 +---------------- .../components/streamlabswater/config_flow.py | 13 ---- .../components/streamlabswater/strings.json | 10 --- .../streamlabswater/test_config_flow.py | 72 ------------------- 4 files changed, 2 insertions(+), 159 deletions(-) diff --git a/homeassistant/components/streamlabswater/__init__.py b/homeassistant/components/streamlabswater/__init__.py index 46acc443d2e..5eeb40630f8 100644 --- a/homeassistant/components/streamlabswater/__init__.py +++ b/homeassistant/components/streamlabswater/__init__.py @@ -3,17 +3,10 @@ from streamlabswater.streamlabswater import StreamlabsClient import voluptuous as vol -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_API_KEY, Platform -from homeassistant.core import ( - DOMAIN as HOMEASSISTANT_DOMAIN, - HomeAssistant, - ServiceCall, -) -from homeassistant.data_entry_flow import FlowResultType +from homeassistant.core import HomeAssistant, ServiceCall import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue -from homeassistant.helpers.typing import ConfigType from .const import DOMAIN from .coordinator import StreamlabsCoordinator @@ -26,17 +19,6 @@ AWAY_MODE_HOME = "home" CONF_LOCATION_ID = "location_id" ISSUE_PLACEHOLDER = {"url": "/config/integrations/dashboard/add?domain=streamlabswater"} -CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - { - vol.Required(CONF_API_KEY): cv.string, - vol.Optional(CONF_LOCATION_ID): cv.string, - } - ) - }, - extra=vol.ALLOW_EXTRA, -) SET_AWAY_MODE_SCHEMA = vol.Schema( { @@ -48,50 +30,6 @@ SET_AWAY_MODE_SCHEMA = vol.Schema( PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.SENSOR] -async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Set up the streamlabs water integration.""" - - if DOMAIN not in config: - return True - - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data={CONF_API_KEY: config[DOMAIN][CONF_API_KEY]}, - ) - if ( - result["type"] == FlowResultType.CREATE_ENTRY - or result["reason"] == "already_configured" - ): - async_create_issue( - hass, - HOMEASSISTANT_DOMAIN, - f"deprecated_yaml_{DOMAIN}", - breaks_in_ha_version="2024.7.0", - is_fixable=False, - issue_domain=DOMAIN, - severity=IssueSeverity.WARNING, - translation_key="deprecated_yaml", - translation_placeholders={ - "domain": DOMAIN, - "integration_title": "StreamLabs", - }, - ) - else: - async_create_issue( - hass, - DOMAIN, - f"deprecated_yaml_import_issue_{result['reason']}", - breaks_in_ha_version="2024.7.0", - is_fixable=False, - issue_domain=DOMAIN, - severity=IssueSeverity.WARNING, - translation_key=f"deprecated_yaml_import_issue_{result['reason']}", - translation_placeholders=ISSUE_PLACEHOLDER, - ) - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up StreamLabs from a config entry.""" diff --git a/homeassistant/components/streamlabswater/config_flow.py b/homeassistant/components/streamlabswater/config_flow.py index 99352082d68..e931a7cf3ba 100644 --- a/homeassistant/components/streamlabswater/config_flow.py +++ b/homeassistant/components/streamlabswater/config_flow.py @@ -57,19 +57,6 @@ class StreamlabsConfigFlow(ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_import(self, user_input: dict[str, Any]) -> ConfigFlowResult: - """Import the yaml config.""" - self._async_abort_entries_match(user_input) - try: - await validate_input(self.hass, user_input[CONF_API_KEY]) - except CannotConnect: - return self.async_abort(reason="cannot_connect") - except Exception: # noqa: BLE001 - LOGGER.exception("Unexpected exception") - return self.async_abort(reason="unknown") - - return self.async_create_entry(title="Streamlabs", data=user_input) - class CannotConnect(HomeAssistantError): """Error to indicate we cannot connect.""" diff --git a/homeassistant/components/streamlabswater/strings.json b/homeassistant/components/streamlabswater/strings.json index 872a0d1f6ac..2cc543b9f2e 100644 --- a/homeassistant/components/streamlabswater/strings.json +++ b/homeassistant/components/streamlabswater/strings.json @@ -48,15 +48,5 @@ "name": "Yearly usage" } } - }, - "issues": { - "deprecated_yaml_import_issue_cannot_connect": { - "title": "The Streamlabs water YAML configuration import failed", - "description": "Configuring Streamlabs water using YAML is being removed but there was a connection error importing your YAML configuration.\n\nEnsure connection to Streamlabs water works and restart Home Assistant to try again or remove the Streamlabs water YAML configuration from your configuration.yaml file and continue to [set up the integration]({url}) manually." - }, - "deprecated_yaml_import_issue_unknown": { - "title": "The Streamlabs water YAML configuration import failed", - "description": "Configuring Streamlabs water using YAML is being removed but there was an unknown error when trying to import the YAML configuration.\n\nEnsure the imported configuration is correct and remove the Streamlabs water YAML configuration from your configuration.yaml file and continue to [set up the integration]({url}) manually." - } } } diff --git a/tests/components/streamlabswater/test_config_flow.py b/tests/components/streamlabswater/test_config_flow.py index 0cee3b8b088..b8e9bbc1157 100644 --- a/tests/components/streamlabswater/test_config_flow.py +++ b/tests/components/streamlabswater/test_config_flow.py @@ -120,75 +120,3 @@ async def test_form_entry_already_exists(hass: HomeAssistant) -> None: assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" - - -async def test_import(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: - """Test import flow.""" - with patch("homeassistant.components.streamlabswater.config_flow.StreamlabsClient"): - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={CONF_API_KEY: "abc"}, - ) - await hass.async_block_till_done() - - assert result["type"] is FlowResultType.CREATE_ENTRY - assert result["title"] == "Streamlabs" - assert result["data"] == {CONF_API_KEY: "abc"} - assert len(mock_setup_entry.mock_calls) == 1 - - -async def test_import_cannot_connect( - hass: HomeAssistant, mock_setup_entry: AsyncMock -) -> None: - """Test we handle cannot connect error.""" - with patch( - "homeassistant.components.streamlabswater.config_flow.StreamlabsClient.get_locations", - return_value={}, - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={CONF_API_KEY: "abc"}, - ) - await hass.async_block_till_done() - - assert result["type"] is FlowResultType.ABORT - assert result["reason"] == "cannot_connect" - - -async def test_import_unknown(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: - """Test we handle unknown error.""" - with patch( - "homeassistant.components.streamlabswater.config_flow.StreamlabsClient.get_locations", - side_effect=Exception, - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={CONF_API_KEY: "abc"}, - ) - await hass.async_block_till_done() - - assert result["type"] is FlowResultType.ABORT - assert result["reason"] == "unknown" - - -async def test_import_entry_already_exists(hass: HomeAssistant) -> None: - """Test we handle if the entry already exists.""" - - entry = MockConfigEntry( - domain=DOMAIN, - data={CONF_API_KEY: "abc"}, - ) - entry.add_to_hass(hass) - with patch("homeassistant.components.streamlabswater.config_flow.StreamlabsClient"): - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={CONF_API_KEY: "abc"}, - ) - await hass.async_block_till_done() - - assert result["type"] is FlowResultType.ABORT - assert result["reason"] == "already_configured" From 8556f3e7c872188d41eba2e8b33c7893bb27c4e6 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Mon, 17 Jun 2024 10:04:46 +0200 Subject: [PATCH 0739/1445] Remove deprecated speedtest service from Fast.com (#119780) * Remove deprecated speedtest service from Fast.com * Remove not needed tests --- .../components/fastdotcom/__init__.py | 11 --- .../components/fastdotcom/icons.json | 3 - .../components/fastdotcom/services.py | 52 ----------- .../components/fastdotcom/services.yaml | 1 - .../components/fastdotcom/strings.json | 19 ---- tests/components/fastdotcom/test_init.py | 30 ------- tests/components/fastdotcom/test_service.py | 88 ------------------- 7 files changed, 204 deletions(-) delete mode 100644 homeassistant/components/fastdotcom/services.py delete mode 100644 homeassistant/components/fastdotcom/services.yaml delete mode 100644 tests/components/fastdotcom/test_service.py diff --git a/homeassistant/components/fastdotcom/__init__.py b/homeassistant/components/fastdotcom/__init__.py index 4074e9a479d..b9593ec907f 100644 --- a/homeassistant/components/fastdotcom/__init__.py +++ b/homeassistant/components/fastdotcom/__init__.py @@ -6,24 +6,13 @@ import logging from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.start import async_at_started -from homeassistant.helpers.typing import ConfigType from .const import DOMAIN, PLATFORMS from .coordinator import FastdotcomDataUpdateCoordinator -from .services import async_setup_services _LOGGER = logging.getLogger(__name__) -CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN) - - -async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Set up the Fastdotcom component.""" - async_setup_services(hass) - return True - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Fast.com from a config entry.""" diff --git a/homeassistant/components/fastdotcom/icons.json b/homeassistant/components/fastdotcom/icons.json index 5c61065d257..d3679448b81 100644 --- a/homeassistant/components/fastdotcom/icons.json +++ b/homeassistant/components/fastdotcom/icons.json @@ -5,8 +5,5 @@ "default": "mdi:speedometer" } } - }, - "services": { - "speedtest": "mdi:speedometer" } } diff --git a/homeassistant/components/fastdotcom/services.py b/homeassistant/components/fastdotcom/services.py deleted file mode 100644 index 5939a667342..00000000000 --- a/homeassistant/components/fastdotcom/services.py +++ /dev/null @@ -1,52 +0,0 @@ -"""Services for the Fastdotcom integration.""" - -from __future__ import annotations - -from homeassistant.config_entries import ConfigEntryState -from homeassistant.core import HomeAssistant, ServiceCall, callback -from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import issue_registry as ir - -from .const import DOMAIN, SERVICE_NAME -from .coordinator import FastdotcomDataUpdateCoordinator - - -def async_setup_services(hass: HomeAssistant) -> None: - """Set up the service for the Fastdotcom integration.""" - - @callback - def collect_coordinator() -> FastdotcomDataUpdateCoordinator: - """Collect the coordinator Fastdotcom.""" - config_entries = hass.config_entries.async_entries(DOMAIN) - if not config_entries: - raise HomeAssistantError("No Fast.com config entries found") - - for config_entry in config_entries: - if config_entry.state != ConfigEntryState.LOADED: - raise HomeAssistantError(f"{config_entry.title} is not loaded") - coordinator: FastdotcomDataUpdateCoordinator = hass.data[DOMAIN][ - config_entry.entry_id - ] - break - return coordinator - - async def async_perform_service(call: ServiceCall) -> None: - """Perform a service call to manually run Fastdotcom.""" - ir.async_create_issue( - hass, - DOMAIN, - "service_deprecation", - breaks_in_ha_version="2024.7.0", - is_fixable=True, - is_persistent=True, - severity=ir.IssueSeverity.WARNING, - translation_key="service_deprecation", - ) - coordinator = collect_coordinator() - await coordinator.async_request_refresh() - - hass.services.async_register( - DOMAIN, - SERVICE_NAME, - async_perform_service, - ) diff --git a/homeassistant/components/fastdotcom/services.yaml b/homeassistant/components/fastdotcom/services.yaml deleted file mode 100644 index 002b28b4e4d..00000000000 --- a/homeassistant/components/fastdotcom/services.yaml +++ /dev/null @@ -1 +0,0 @@ -speedtest: diff --git a/homeassistant/components/fastdotcom/strings.json b/homeassistant/components/fastdotcom/strings.json index 61a1f686747..36863f1a0a3 100644 --- a/homeassistant/components/fastdotcom/strings.json +++ b/homeassistant/components/fastdotcom/strings.json @@ -15,24 +15,5 @@ "name": "Download" } } - }, - "services": { - "speedtest": { - "name": "Speed test", - "description": "Immediately executes a speed test with Fast.com." - } - }, - "issues": { - "service_deprecation": { - "title": "Fast.com speedtest service is being removed", - "fix_flow": { - "step": { - "confirm": { - "title": "[%key:component::fastdotcom::issues::service_deprecation::title%]", - "description": "Use `homeassistant.update_entity` instead to update the data.\n\nPlease replace this service and adjust your automations and scripts and select **submit** to fix this issue." - } - } - } - } } } diff --git a/tests/components/fastdotcom/test_init.py b/tests/components/fastdotcom/test_init.py index b1be0b53d34..ac7708a3c36 100644 --- a/tests/components/fastdotcom/test_init.py +++ b/tests/components/fastdotcom/test_init.py @@ -10,7 +10,6 @@ from homeassistant.components.fastdotcom.const import DEFAULT_NAME, DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.const import EVENT_HOMEASSISTANT_STARTED, STATE_UNKNOWN from homeassistant.core import CoreState, HomeAssistant -from homeassistant.helpers import issue_registry as ir from tests.common import MockConfigEntry @@ -70,32 +69,3 @@ async def test_delayed_speedtest_during_startup( assert state.state == "5.0" assert config_entry.state is ConfigEntryState.LOADED - - -async def test_service_deprecated( - hass: HomeAssistant, issue_registry: ir.IssueRegistry -) -> None: - """Test deprecated service.""" - config_entry = MockConfigEntry( - domain=DOMAIN, - unique_id="UNIQUE_TEST_ID", - title=DEFAULT_NAME, - ) - config_entry.add_to_hass(hass) - with patch( - "homeassistant.components.fastdotcom.coordinator.fast_com", return_value=5.0 - ): - await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - await hass.services.async_call( - DOMAIN, - "speedtest", - {}, - blocking=True, - ) - await hass.async_block_till_done() - - issue = issue_registry.async_get_issue(DOMAIN, "service_deprecation") - assert issue - assert issue.is_fixable is True - assert issue.translation_key == "service_deprecation" diff --git a/tests/components/fastdotcom/test_service.py b/tests/components/fastdotcom/test_service.py deleted file mode 100644 index 61447d96374..00000000000 --- a/tests/components/fastdotcom/test_service.py +++ /dev/null @@ -1,88 +0,0 @@ -"""Test Fastdotcom service.""" - -from unittest.mock import patch - -import pytest - -from homeassistant.components.fastdotcom.const import DEFAULT_NAME, DOMAIN, SERVICE_NAME -from homeassistant.core import HomeAssistant -from homeassistant.exceptions import HomeAssistantError - -from tests.common import MockConfigEntry - - -async def test_service(hass: HomeAssistant) -> None: - """Test the Fastdotcom service.""" - config_entry = MockConfigEntry( - domain=DOMAIN, - unique_id="UNIQUE_TEST_ID", - title=DEFAULT_NAME, - ) - config_entry.add_to_hass(hass) - - with patch( - "homeassistant.components.fastdotcom.coordinator.fast_com", return_value=0 - ): - await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - - state = hass.states.get("sensor.fast_com_download") - assert state is not None - assert state.state == "0" - - with patch( - "homeassistant.components.fastdotcom.coordinator.fast_com", return_value=5.0 - ): - await hass.services.async_call(DOMAIN, SERVICE_NAME, blocking=True) - - state = hass.states.get("sensor.fast_com_download") - assert state is not None - assert state.state == "5.0" - - -async def test_service_unloaded_entry(hass: HomeAssistant) -> None: - """Test service called when config entry unloaded.""" - config_entry = MockConfigEntry( - domain=DOMAIN, - unique_id="UNIQUE_TEST_ID", - title=DEFAULT_NAME, - ) - config_entry.add_to_hass(hass) - - with patch( - "homeassistant.components.fastdotcom.coordinator.fast_com", return_value=0 - ): - await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - - assert config_entry - await hass.config_entries.async_unload(config_entry.entry_id) - - with pytest.raises(HomeAssistantError) as exc: - await hass.services.async_call(DOMAIN, SERVICE_NAME, blocking=True) - - assert "Fast.com is not loaded" in str(exc) - - -async def test_service_removed_entry(hass: HomeAssistant) -> None: - """Test service called when config entry was removed and HA was not restarted yet.""" - config_entry = MockConfigEntry( - domain=DOMAIN, - unique_id="UNIQUE_TEST_ID", - title=DEFAULT_NAME, - ) - config_entry.add_to_hass(hass) - - with patch( - "homeassistant.components.fastdotcom.coordinator.fast_com", return_value=0 - ): - await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - - assert config_entry - await hass.config_entries.async_remove(config_entry.entry_id) - - with pytest.raises(HomeAssistantError) as exc: - await hass.services.async_call(DOMAIN, SERVICE_NAME, blocking=True) - - assert "No Fast.com config entries found" in str(exc) From 496338fa4ee9b0fa8b60947e127ab7e4a413d7d4 Mon Sep 17 00:00:00 2001 From: Marlon Date: Mon, 17 Jun 2024 10:31:21 +0200 Subject: [PATCH 0740/1445] Add number input for apsystems (#118825) * Add number input for apsystems * Exclude number from apsystems from coverage * Remove unnecessary int-float conversion in apsystems number * Remove unnecessary int-float conversion in apsystems number and redundant and single use variables * Add translation for apsystems number --- .coveragerc | 1 + .../components/apsystems/__init__.py | 2 +- homeassistant/components/apsystems/number.py | 52 +++++++++++++++++++ .../components/apsystems/strings.json | 3 ++ 4 files changed, 57 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/apsystems/number.py diff --git a/.coveragerc b/.coveragerc index bba6eb584c5..390c098418e 100644 --- a/.coveragerc +++ b/.coveragerc @@ -90,6 +90,7 @@ omit = homeassistant/components/apsystems/__init__.py homeassistant/components/apsystems/coordinator.py homeassistant/components/apsystems/entity.py + homeassistant/components/apsystems/number.py homeassistant/components/apsystems/sensor.py homeassistant/components/aqualogic/* homeassistant/components/aquostv/media_player.py diff --git a/homeassistant/components/apsystems/__init__.py b/homeassistant/components/apsystems/__init__.py index 0231d2975d8..2df267dda0b 100644 --- a/homeassistant/components/apsystems/__init__.py +++ b/homeassistant/components/apsystems/__init__.py @@ -12,7 +12,7 @@ from homeassistant.core import HomeAssistant from .coordinator import ApSystemsDataCoordinator -PLATFORMS: list[Platform] = [Platform.SENSOR] +PLATFORMS: list[Platform] = [Platform.NUMBER, Platform.SENSOR] @dataclass diff --git a/homeassistant/components/apsystems/number.py b/homeassistant/components/apsystems/number.py new file mode 100644 index 00000000000..f9b535d7d6a --- /dev/null +++ b/homeassistant/components/apsystems/number.py @@ -0,0 +1,52 @@ +"""The output limit which can be set in the APsystems local API integration.""" + +from __future__ import annotations + +from homeassistant.components.number import NumberDeviceClass, NumberEntity, NumberMode +from homeassistant.const import UnitOfPower +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import DiscoveryInfoType + +from . import ApSystemsConfigEntry, ApSystemsData +from .entity import ApSystemsEntity + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ApSystemsConfigEntry, + add_entities: AddEntitiesCallback, + discovery_info: DiscoveryInfoType | None = None, +) -> None: + """Set up the sensor platform.""" + + add_entities([ApSystemsMaxOutputNumber(config_entry.runtime_data)]) + + +class ApSystemsMaxOutputNumber(ApSystemsEntity, NumberEntity): + """Base sensor to be used with description.""" + + _attr_native_max_value = 800 + _attr_native_min_value = 30 + _attr_native_step = 1 + _attr_device_class = NumberDeviceClass.POWER + _attr_mode = NumberMode.BOX + _attr_native_unit_of_measurement = UnitOfPower.WATT + _attr_translation_key = "max_output" + + def __init__( + self, + data: ApSystemsData, + ) -> None: + """Initialize the sensor.""" + super().__init__(data) + self._api = data.coordinator.api + self._attr_unique_id = f"{data.device_id}_output_limit" + + async def async_update(self) -> None: + """Set the state with the value fetched from the inverter.""" + self._attr_native_value = await self._api.get_max_power() + + async def async_set_native_value(self, value: float) -> None: + """Set the desired output power.""" + self._attr_native_value = await self._api.set_max_power(int(value)) diff --git a/homeassistant/components/apsystems/strings.json b/homeassistant/components/apsystems/strings.json index aa919cd65b1..cfd24675311 100644 --- a/homeassistant/components/apsystems/strings.json +++ b/homeassistant/components/apsystems/strings.json @@ -25,6 +25,9 @@ "today_production": { "name": "Production of today" }, "today_production_p1": { "name": "Production of today from P1" }, "today_production_p2": { "name": "Production of today from P2" } + }, + "number": { + "max_output": { "name": "Max output" } } } } From 75fa0b91d881284d69c448af7162a870674e0767 Mon Sep 17 00:00:00 2001 From: azerty9971 Date: Mon, 17 Jun 2024 10:59:36 +0200 Subject: [PATCH 0741/1445] Add support for Tuya energy data for WKCZ devices (#119635) Add support for energy sensors for WKCZ devices --- homeassistant/components/tuya/sensor.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/homeassistant/components/tuya/sensor.py b/homeassistant/components/tuya/sensor.py index cd487a31d97..b974ccd5eb0 100644 --- a/homeassistant/components/tuya/sensor.py +++ b/homeassistant/components/tuya/sensor.py @@ -221,6 +221,27 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), + TuyaSensorEntityDescription( + key=DPCode.CUR_CURRENT, + translation_key="current", + device_class=SensorDeviceClass.CURRENT, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + ), + TuyaSensorEntityDescription( + key=DPCode.CUR_POWER, + translation_key="power", + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + ), + TuyaSensorEntityDescription( + key=DPCode.CUR_VOLTAGE, + translation_key="voltage", + device_class=SensorDeviceClass.VOLTAGE, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + ), ), # CO Detector # https://developer.tuya.com/en/docs/iot/categorycobj?id=Kaiuz3u1j6q1v From 4e3cc43343c52ea26d6ec07f2356bf6ac8b5053b Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 17 Jun 2024 11:13:34 +0200 Subject: [PATCH 0742/1445] Fix consider-using-tuple warning in tplink_omada tests (#119814) Fix consider-using-tuple in tplink_omada tests --- tests/components/tplink_omada/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/components/tplink_omada/conftest.py b/tests/components/tplink_omada/conftest.py index 085cc32d1aa..c29fcb633e4 100644 --- a/tests/components/tplink_omada/conftest.py +++ b/tests/components/tplink_omada/conftest.py @@ -77,7 +77,7 @@ def mock_omada_site_client() -> Generator[AsyncMock]: site_client.get_switch_ports = AsyncMock(return_value=switch1_ports) async def async_empty() -> AsyncIterable: - for c in []: + for c in (): yield c site_client.get_known_clients.return_value = async_empty() From 1d873115f31c4f6c294ce06ff0a8bb128906912f Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 17 Jun 2024 11:17:35 +0200 Subject: [PATCH 0743/1445] Pin tenacity to 8.3.0 (#119815) --- homeassistant/package_constraints.txt | 3 +++ script/gen_requirements_all.py | 3 +++ 2 files changed, 6 insertions(+) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 8f7958bdc4c..e55f0dd1cf2 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -197,3 +197,6 @@ scapy>=2.5.0 # Only tuf>=4 includes a constraint to <1.0. # https://github.com/theupdateframework/python-tuf/releases/tag/v4.0.0 tuf>=4.0.0 + +# https://github.com/jd/tenacity/issues/471 +tenacity<8.4.0 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 1f2f4bcab66..a12decd5b2c 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -219,6 +219,9 @@ scapy>=2.5.0 # Only tuf>=4 includes a constraint to <1.0. # https://github.com/theupdateframework/python-tuf/releases/tag/v4.0.0 tuf>=4.0.0 + +# https://github.com/jd/tenacity/issues/471 +tenacity<8.4.0 """ GENERATED_MESSAGE = ( From 0ae49036866ef4b3fd9d2b9998bc41fd8df3b879 Mon Sep 17 00:00:00 2001 From: dubstomp <156379311+dubstomp@users.noreply.github.com> Date: Mon, 17 Jun 2024 02:31:18 -0700 Subject: [PATCH 0744/1445] Add Kasa Dimmer to Matter TRANSITION_BLOCKLIST (#119751) --- homeassistant/components/matter/light.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/matter/light.py b/homeassistant/components/matter/light.py index 89400c98989..007bcd1a33a 100644 --- a/homeassistant/components/matter/light.py +++ b/homeassistant/components/matter/light.py @@ -56,6 +56,7 @@ TRANSITION_BLOCKLIST = ( (5010, 769, "3.0", "1.0.0"), (4999, 25057, "1.0", "27.0"), (4448, 36866, "V1", "V1.0.0.5"), + (5009, 514, "1.0", "1.0.0"), ) From e0378f79a450a76af3b41e109ea8ebad03cc5c1a Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 17 Jun 2024 12:16:36 +0200 Subject: [PATCH 0745/1445] Remove create_list from StorageCollectionWebsocket.async_setup (#119508) --- .../components/assist_pipeline/pipeline.py | 3 +- homeassistant/components/lovelace/__init__.py | 22 +++++++---- .../components/lovelace/dashboard.py | 22 +++++++++++ .../components/lovelace/resources.py | 37 ++++++++++++++++++ .../components/lovelace/websocket.py | 38 +++++++++---------- homeassistant/components/person/__init__.py | 37 +++++++++--------- homeassistant/helpers/collection.py | 18 ++++----- tests/components/lovelace/test_resources.py | 34 +++++++++++------ 8 files changed, 142 insertions(+), 69 deletions(-) diff --git a/homeassistant/components/assist_pipeline/pipeline.py b/homeassistant/components/assist_pipeline/pipeline.py index 1471af2ea41..ff360676cf7 100644 --- a/homeassistant/components/assist_pipeline/pipeline.py +++ b/homeassistant/components/assist_pipeline/pipeline.py @@ -1609,11 +1609,10 @@ class PipelineStorageCollectionWebsocket( self, hass: HomeAssistant, *, - create_list: bool = True, create_create: bool = True, ) -> None: """Set up the websocket commands.""" - super().async_setup(hass, create_list=create_list, create_create=create_create) + super().async_setup(hass, create_create=create_create) websocket_api.async_register_command( hass, diff --git a/homeassistant/components/lovelace/__init__.py b/homeassistant/components/lovelace/__init__.py index 60d03717be0..d26e4f1d2d7 100644 --- a/homeassistant/components/lovelace/__init__.py +++ b/homeassistant/components/lovelace/__init__.py @@ -115,6 +115,17 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: reload_resources_service_handler, schema=RESOURCE_RELOAD_SERVICE_SCHEMA, ) + # Register lovelace/resources for backwards compatibility, remove in + # Home Assistant Core 2025.1 + for command in ("lovelace/resources", "lovelace/resources/list"): + websocket_api.async_register_command( + hass, + command, + websocket.websocket_lovelace_resources, + websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( + {"type": command}, + ), + ) else: default_config = dashboard.LovelaceStorage(hass, None) @@ -127,22 +138,19 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: resource_collection = resources.ResourceStorageCollection(hass, default_config) - collection.DictStorageCollectionWebsocket( + resources.ResourceStorageCollectionWebsocket( resource_collection, "lovelace/resources", "resource", RESOURCE_CREATE_FIELDS, RESOURCE_UPDATE_FIELDS, - ).async_setup(hass, create_list=False) + ).async_setup(hass) websocket_api.async_register_command(hass, websocket.websocket_lovelace_config) websocket_api.async_register_command(hass, websocket.websocket_lovelace_save_config) websocket_api.async_register_command( hass, websocket.websocket_lovelace_delete_config ) - websocket_api.async_register_command(hass, websocket.websocket_lovelace_resources) - - websocket_api.async_register_command(hass, websocket.websocket_lovelace_dashboards) hass.data[DOMAIN] = { # We store a dictionary mapping url_path: config. None is the default. @@ -209,13 +217,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: dashboards_collection.async_add_listener(storage_dashboard_changed) await dashboards_collection.async_load() - collection.DictStorageCollectionWebsocket( + dashboard.DashboardsCollectionWebSocket( dashboards_collection, "lovelace/dashboards", "dashboard", STORAGE_DASHBOARD_CREATE_FIELDS, STORAGE_DASHBOARD_UPDATE_FIELDS, - ).async_setup(hass, create_list=False) + ).async_setup(hass) def create_map_dashboard(): hass.async_create_task(_create_map_dashboard(hass)) diff --git a/homeassistant/components/lovelace/dashboard.py b/homeassistant/components/lovelace/dashboard.py index ef2b3075b34..db6db2fa7ef 100644 --- a/homeassistant/components/lovelace/dashboard.py +++ b/homeassistant/components/lovelace/dashboard.py @@ -11,6 +11,7 @@ from typing import Any import voluptuous as vol +from homeassistant.components import websocket_api from homeassistant.components.frontend import DATA_PANELS from homeassistant.const import CONF_FILENAME from homeassistant.core import HomeAssistant, callback @@ -297,3 +298,24 @@ class DashboardsCollection(collection.DictStorageCollection): updated.pop(CONF_ICON) return updated + + +class DashboardsCollectionWebSocket(collection.DictStorageCollectionWebsocket): + """Class to expose storage collection management over websocket.""" + + @callback + def ws_list_item( + self, + hass: HomeAssistant, + connection: websocket_api.ActiveConnection, + msg: dict[str, Any], + ) -> None: + """Send Lovelace UI resources over WebSocket connection.""" + connection.send_result( + msg["id"], + [ + dashboard.config + for dashboard in hass.data[DOMAIN]["dashboards"].values() + if dashboard.config + ], + ) diff --git a/homeassistant/components/lovelace/resources.py b/homeassistant/components/lovelace/resources.py index 2dbbbacabea..c25c81e2c6f 100644 --- a/homeassistant/components/lovelace/resources.py +++ b/homeassistant/components/lovelace/resources.py @@ -8,6 +8,7 @@ import uuid import voluptuous as vol +from homeassistant.components import websocket_api from homeassistant.const import CONF_ID, CONF_RESOURCES, CONF_TYPE from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError @@ -21,6 +22,7 @@ from .const import ( RESOURCE_UPDATE_FIELDS, ) from .dashboard import LovelaceConfig +from .websocket import websocket_lovelace_resources_impl RESOURCE_STORAGE_KEY = f"{DOMAIN}_resources" RESOURCES_STORAGE_VERSION = 1 @@ -125,3 +127,38 @@ class ResourceStorageCollection(collection.DictStorageCollection): update_data[CONF_TYPE] = update_data.pop(CONF_RESOURCE_TYPE_WS) return {**item, **update_data} + + +class ResourceStorageCollectionWebsocket(collection.DictStorageCollectionWebsocket): + """Class to expose storage collection management over websocket.""" + + @callback + def async_setup( + self, + hass: HomeAssistant, + *, + create_create: bool = True, + ) -> None: + """Set up the websocket commands.""" + super().async_setup(hass, create_create=create_create) + + # Register lovelace/resources for backwards compatibility, remove in + # Home Assistant Core 2025.1 + websocket_api.async_register_command( + hass, + self.api_prefix, + self.ws_list_item, + websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( + {vol.Required("type"): f"{self.api_prefix}"} + ), + ) + + @staticmethod + @websocket_api.async_response + async def ws_list_item( + hass: HomeAssistant, + connection: websocket_api.ActiveConnection, + msg: dict[str, Any], + ) -> None: + """Send Lovelace UI resources over WebSocket connection.""" + await websocket_lovelace_resources_impl(hass, connection, msg) diff --git a/homeassistant/components/lovelace/websocket.py b/homeassistant/components/lovelace/websocket.py index 2aa55efafbd..e402ba92f16 100644 --- a/homeassistant/components/lovelace/websocket.py +++ b/homeassistant/components/lovelace/websocket.py @@ -8,7 +8,7 @@ from typing import Any import voluptuous as vol from homeassistant.components import websocket_api -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv from homeassistant.helpers.json import json_fragment @@ -52,14 +52,28 @@ def _handle_errors(func): return send_with_error_handling -@websocket_api.websocket_command({"type": "lovelace/resources"}) @websocket_api.async_response async def websocket_lovelace_resources( hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any], ) -> None: - """Send Lovelace UI resources over WebSocket configuration.""" + """Send Lovelace UI resources over WebSocket connection. + + This function is used in YAML mode. + """ + await websocket_lovelace_resources_impl(hass, connection, msg) + + +async def websocket_lovelace_resources_impl( + hass: HomeAssistant, + connection: websocket_api.ActiveConnection, + msg: dict[str, Any], +) -> None: + """Help send Lovelace UI resources over WebSocket connection. + + This function is called by both Storage and YAML mode WS handlers. + """ resources = hass.data[DOMAIN]["resources"] if hass.config.safe_mode: @@ -129,21 +143,3 @@ async def websocket_lovelace_delete_config( ) -> None: """Delete Lovelace UI configuration.""" await config.async_delete() - - -@websocket_api.websocket_command({"type": "lovelace/dashboards/list"}) -@callback -def websocket_lovelace_dashboards( - hass: HomeAssistant, - connection: websocket_api.ActiveConnection, - msg: dict[str, Any], -) -> None: - """Send Lovelace dashboard configuration.""" - connection.send_result( - msg["id"], - [ - dashboard.config - for dashboard in hass.data[DOMAIN]["dashboards"].values() - if dashboard.config - ], - ) diff --git a/homeassistant/components/person/__init__.py b/homeassistant/components/person/__init__.py index 175a206b38f..55c37f1c36c 100644 --- a/homeassistant/components/person/__init__.py +++ b/homeassistant/components/person/__init__.py @@ -24,7 +24,6 @@ from homeassistant.const import ( ATTR_NAME, CONF_ID, CONF_NAME, - CONF_TYPE, EVENT_HOMEASSISTANT_START, SERVICE_RELOAD, STATE_HOME, @@ -307,6 +306,23 @@ class PersonStorageCollection(collection.DictStorageCollection): raise ValueError("User already taken") +class PersonStorageCollectionWebsocket(collection.DictStorageCollectionWebsocket): + """Class to expose storage collection management over websocket.""" + + def ws_list_item( + self, + hass: HomeAssistant, + connection: websocket_api.ActiveConnection, + msg: dict[str, Any], + ) -> None: + """List persons.""" + yaml, storage, _ = hass.data[DOMAIN] + connection.send_result( + msg[ATTR_ID], + {"storage": storage.async_items(), "config": yaml.async_items()}, + ) + + async def filter_yaml_data(hass: HomeAssistant, persons: list[dict]) -> list[dict]: """Validate YAML data that we can't validate via schema.""" filtered = [] @@ -370,11 +386,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: hass.data[DOMAIN] = (yaml_collection, storage_collection, entity_component) - collection.DictStorageCollectionWebsocket( + PersonStorageCollectionWebsocket( storage_collection, DOMAIN, DOMAIN, CREATE_FIELDS, UPDATE_FIELDS - ).async_setup(hass, create_list=False) - - websocket_api.async_register_command(hass, ws_list_person) + ).async_setup(hass) async def _handle_user_removed(event: Event) -> None: """Handle a user being removed.""" @@ -570,19 +584,6 @@ class Person( self._attr_extra_state_attributes = data -@websocket_api.websocket_command({vol.Required(CONF_TYPE): "person/list"}) -def ws_list_person( - hass: HomeAssistant, - connection: websocket_api.ActiveConnection, - msg: dict[str, Any], -) -> None: - """List persons.""" - yaml, storage, _ = hass.data[DOMAIN] - connection.send_result( - msg[ATTR_ID], {"storage": storage.async_items(), "config": yaml.async_items()} - ) - - def _get_latest(prev: State | None, curr: State) -> State: """Get latest state.""" if prev is None or curr.last_updated > prev.last_updated: diff --git a/homeassistant/helpers/collection.py b/homeassistant/helpers/collection.py index 4691bc804fd..1ce4a9d092b 100644 --- a/homeassistant/helpers/collection.py +++ b/homeassistant/helpers/collection.py @@ -537,19 +537,17 @@ class StorageCollectionWebsocket[_StorageCollectionT: StorageCollection]: self, hass: HomeAssistant, *, - create_list: bool = True, create_create: bool = True, ) -> None: """Set up the websocket commands.""" - if create_list: - websocket_api.async_register_command( - hass, - f"{self.api_prefix}/list", - self.ws_list_item, - websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( - {vol.Required("type"): f"{self.api_prefix}/list"} - ), - ) + websocket_api.async_register_command( + hass, + f"{self.api_prefix}/list", + self.ws_list_item, + websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( + {vol.Required("type"): f"{self.api_prefix}/list"} + ), + ) if create_create: websocket_api.async_register_command( diff --git a/tests/components/lovelace/test_resources.py b/tests/components/lovelace/test_resources.py index d2008ce5d41..bf6b44f0950 100644 --- a/tests/components/lovelace/test_resources.py +++ b/tests/components/lovelace/test_resources.py @@ -5,6 +5,8 @@ from typing import Any from unittest.mock import patch import uuid +import pytest + from homeassistant.components.lovelace import dashboard, resources from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component @@ -17,8 +19,9 @@ RESOURCE_EXAMPLES = [ ] +@pytest.mark.parametrize("list_cmd", ["lovelace/resources", "lovelace/resources/list"]) async def test_yaml_resources( - hass: HomeAssistant, hass_ws_client: WebSocketGenerator + hass: HomeAssistant, hass_ws_client: WebSocketGenerator, list_cmd: str ) -> None: """Test defining resources in configuration.yaml.""" assert await async_setup_component( @@ -28,14 +31,15 @@ async def test_yaml_resources( client = await hass_ws_client(hass) # Fetch data - await client.send_json({"id": 5, "type": "lovelace/resources"}) + await client.send_json({"id": 5, "type": list_cmd}) response = await client.receive_json() assert response["success"] assert response["result"] == RESOURCE_EXAMPLES +@pytest.mark.parametrize("list_cmd", ["lovelace/resources", "lovelace/resources/list"]) async def test_yaml_resources_backwards( - hass: HomeAssistant, hass_ws_client: WebSocketGenerator + hass: HomeAssistant, hass_ws_client: WebSocketGenerator, list_cmd: str ) -> None: """Test defining resources in YAML ll config (legacy).""" with patch( @@ -49,16 +53,18 @@ async def test_yaml_resources_backwards( client = await hass_ws_client(hass) # Fetch data - await client.send_json({"id": 5, "type": "lovelace/resources"}) + await client.send_json({"id": 5, "type": list_cmd}) response = await client.receive_json() assert response["success"] assert response["result"] == RESOURCE_EXAMPLES +@pytest.mark.parametrize("list_cmd", ["lovelace/resources", "lovelace/resources/list"]) async def test_storage_resources( hass: HomeAssistant, hass_ws_client: WebSocketGenerator, hass_storage: dict[str, Any], + list_cmd: str, ) -> None: """Test defining resources in storage config.""" resource_config = [{**item, "id": uuid.uuid4().hex} for item in RESOURCE_EXAMPLES] @@ -72,16 +78,18 @@ async def test_storage_resources( client = await hass_ws_client(hass) # Fetch data - await client.send_json({"id": 5, "type": "lovelace/resources"}) + await client.send_json({"id": 5, "type": list_cmd}) response = await client.receive_json() assert response["success"] assert response["result"] == resource_config +@pytest.mark.parametrize("list_cmd", ["lovelace/resources", "lovelace/resources/list"]) async def test_storage_resources_import( hass: HomeAssistant, hass_ws_client: WebSocketGenerator, hass_storage: dict[str, Any], + list_cmd: str, ) -> None: """Test importing resources from storage config.""" assert await async_setup_component(hass, "lovelace", {}) @@ -94,7 +102,7 @@ async def test_storage_resources_import( client = await hass_ws_client(hass) # Fetch data - await client.send_json({"id": 5, "type": "lovelace/resources"}) + await client.send_json({"id": 5, "type": list_cmd}) response = await client.receive_json() assert response["success"] assert ( @@ -118,7 +126,7 @@ async def test_storage_resources_import( response = await client.receive_json() assert response["success"] - await client.send_json({"id": 7, "type": "lovelace/resources"}) + await client.send_json({"id": 7, "type": list_cmd}) response = await client.receive_json() assert response["success"] @@ -141,7 +149,7 @@ async def test_storage_resources_import( response = await client.receive_json() assert response["success"] - await client.send_json({"id": 9, "type": "lovelace/resources"}) + await client.send_json({"id": 9, "type": list_cmd}) response = await client.receive_json() assert response["success"] @@ -160,7 +168,7 @@ async def test_storage_resources_import( response = await client.receive_json() assert response["success"] - await client.send_json({"id": 11, "type": "lovelace/resources"}) + await client.send_json({"id": 11, "type": list_cmd}) response = await client.receive_json() assert response["success"] @@ -168,10 +176,12 @@ async def test_storage_resources_import( assert first_item["id"] not in (item["id"] for item in response["result"]) +@pytest.mark.parametrize("list_cmd", ["lovelace/resources", "lovelace/resources/list"]) async def test_storage_resources_import_invalid( hass: HomeAssistant, hass_ws_client: WebSocketGenerator, hass_storage: dict[str, Any], + list_cmd: str, ) -> None: """Test importing resources from storage config.""" assert await async_setup_component(hass, "lovelace", {}) @@ -184,7 +194,7 @@ async def test_storage_resources_import_invalid( client = await hass_ws_client(hass) # Fetch data - await client.send_json({"id": 5, "type": "lovelace/resources"}) + await client.send_json({"id": 5, "type": list_cmd}) response = await client.receive_json() assert response["success"] assert response["result"] == [] @@ -194,10 +204,12 @@ async def test_storage_resources_import_invalid( ) +@pytest.mark.parametrize("list_cmd", ["lovelace/resources", "lovelace/resources/list"]) async def test_storage_resources_safe_mode( hass: HomeAssistant, hass_ws_client: WebSocketGenerator, hass_storage: dict[str, Any], + list_cmd: str, ) -> None: """Test defining resources in storage config.""" @@ -213,7 +225,7 @@ async def test_storage_resources_safe_mode( hass.config.safe_mode = True # Fetch data - await client.send_json({"id": 5, "type": "lovelace/resources"}) + await client.send_json({"id": 5, "type": list_cmd}) response = await client.receive_json() assert response["success"] assert response["result"] == [] From 369f9772f2a494e683e2982508b32570dfb35425 Mon Sep 17 00:00:00 2001 From: Tsvi Mostovicz Date: Mon, 17 Jun 2024 13:37:30 +0300 Subject: [PATCH 0746/1445] Fix Jewish Calendar unique id migration (#119683) * Implement correct passing fix * Keep the test as is, as it simulates the current behavior * Last minor change --- homeassistant/components/jewish_calendar/__init__.py | 5 ++++- tests/components/jewish_calendar/test_init.py | 1 - 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/jewish_calendar/__init__.py b/homeassistant/components/jewish_calendar/__init__.py index 8383f9181fc..81fe6cb5377 100644 --- a/homeassistant/components/jewish_calendar/__init__.py +++ b/homeassistant/components/jewish_calendar/__init__.py @@ -72,11 +72,14 @@ def get_unique_prefix( havdalah_offset: int | None, ) -> str: """Create a prefix for unique ids.""" + # location.altitude was unset before 2024.6 when this method + # was used to create the unique id. As such it would always + # use the default altitude of 754. config_properties = [ location.latitude, location.longitude, location.timezone, - location.altitude, + 754, location.diaspora, language, candle_lighting_offset, diff --git a/tests/components/jewish_calendar/test_init.py b/tests/components/jewish_calendar/test_init.py index f052d4e7f46..b8454b41a60 100644 --- a/tests/components/jewish_calendar/test_init.py +++ b/tests/components/jewish_calendar/test_init.py @@ -38,7 +38,6 @@ async def test_import_unique_id_migration(hass: HomeAssistant) -> None: latitude=yaml_conf[DOMAIN][CONF_LATITUDE], longitude=yaml_conf[DOMAIN][CONF_LONGITUDE], timezone=hass.config.time_zone, - altitude=hass.config.elevation, diaspora=DEFAULT_DIASPORA, ) old_prefix = get_unique_prefix(location, DEFAULT_LANGUAGE, 20, 50) From d34be0e8fa3ab2d1c821cb48e1a39fba740a7340 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Mon, 17 Jun 2024 12:58:58 +0200 Subject: [PATCH 0747/1445] Bump reolink-aio to 0.9.3 (#119820) --- homeassistant/components/reolink/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/reolink/manifest.json b/homeassistant/components/reolink/manifest.json index ba4d88578f1..172a43a91b3 100644 --- a/homeassistant/components/reolink/manifest.json +++ b/homeassistant/components/reolink/manifest.json @@ -18,5 +18,5 @@ "documentation": "https://www.home-assistant.io/integrations/reolink", "iot_class": "local_push", "loggers": ["reolink_aio"], - "requirements": ["reolink-aio==0.9.2"] + "requirements": ["reolink-aio==0.9.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index f7693f6daaa..62559a9a84e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2460,7 +2460,7 @@ renault-api==0.2.3 renson-endura-delta==1.7.1 # homeassistant.components.reolink -reolink-aio==0.9.2 +reolink-aio==0.9.3 # homeassistant.components.idteck_prox rfk101py==0.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ac7d942324b..27653bde360 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1924,7 +1924,7 @@ renault-api==0.2.3 renson-endura-delta==1.7.1 # homeassistant.components.reolink -reolink-aio==0.9.2 +reolink-aio==0.9.3 # homeassistant.components.rflink rflink==0.0.66 From cfbc854c846498321d0d0783fe984d930fcb4bdb Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 17 Jun 2024 13:24:10 +0200 Subject: [PATCH 0748/1445] Remove deprecated import swiss public transport import flow (#119813) --- .../swiss_public_transport/config_flow.py | 31 -------- .../swiss_public_transport/sensor.py | 74 +---------------- .../swiss_public_transport/strings.json | 14 ---- .../test_config_flow.py | 79 +------------------ 4 files changed, 5 insertions(+), 193 deletions(-) diff --git a/homeassistant/components/swiss_public_transport/config_flow.py b/homeassistant/components/swiss_public_transport/config_flow.py index 5687e968318..bb852efd211 100644 --- a/homeassistant/components/swiss_public_transport/config_flow.py +++ b/homeassistant/components/swiss_public_transport/config_flow.py @@ -11,7 +11,6 @@ from opendata_transport.exceptions import ( import voluptuous as vol from homeassistant.config_entries import ConfigFlow, ConfigFlowResult -from homeassistant.const import CONF_NAME from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv @@ -69,33 +68,3 @@ class SwissPublicTransportConfigFlow(ConfigFlow, domain=DOMAIN): errors=errors, description_placeholders=PLACEHOLDERS, ) - - async def async_step_import(self, import_input: dict[str, Any]) -> ConfigFlowResult: - """Async import step to set up the connection.""" - await self.async_set_unique_id( - f"{import_input[CONF_START]} {import_input[CONF_DESTINATION]}" - ) - self._abort_if_unique_id_configured() - - session = async_get_clientsession(self.hass) - opendata = OpendataTransport( - import_input[CONF_START], import_input[CONF_DESTINATION], session - ) - try: - await opendata.async_get_data() - except OpendataTransportConnectionError: - return self.async_abort(reason="cannot_connect") - except OpendataTransportError: - return self.async_abort(reason="bad_config") - except Exception: # noqa: BLE001 - _LOGGER.error( - "Unknown error raised by python-opendata-transport for '%s %s', check at http://transport.opendata.ch/examples/stationboard.html if your station names and your parameters are valid", - import_input[CONF_START], - import_input[CONF_DESTINATION], - ) - return self.async_abort(reason="unknown") - - return self.async_create_entry( - title=import_input[CONF_NAME], - data=import_input, - ) diff --git a/homeassistant/components/swiss_public_transport/sensor.py b/homeassistant/components/swiss_public_transport/sensor.py index f477c04f6ec..844797e5dd5 100644 --- a/homeassistant/components/swiss_public_transport/sensor.py +++ b/homeassistant/components/swiss_public_transport/sensor.py @@ -8,48 +8,26 @@ from datetime import datetime, timedelta import logging from typing import TYPE_CHECKING -import voluptuous as vol - from homeassistant import config_entries, core from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, SensorEntityDescription, ) -from homeassistant.config_entries import SOURCE_IMPORT -from homeassistant.const import CONF_NAME, UnitOfTime -from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant, callback -from homeassistant.data_entry_flow import FlowResultType -import homeassistant.helpers.config_validation as cv +from homeassistant.const import UnitOfTime +from homeassistant.core import callback from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType +from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import ( - CONF_DESTINATION, - CONF_START, - DEFAULT_NAME, - DOMAIN, - PLACEHOLDERS, - SENSOR_CONNECTIONS_COUNT, -) +from .const import DOMAIN, SENSOR_CONNECTIONS_COUNT from .coordinator import DataConnection, SwissPublicTransportDataUpdateCoordinator _LOGGER = logging.getLogger(__name__) SCAN_INTERVAL = timedelta(seconds=90) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_DESTINATION): cv.string, - vol.Required(CONF_START): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - } -) - @dataclass(kw_only=True, frozen=True) class SwissPublicTransportSensorEntityDescription(SensorEntityDescription): @@ -118,50 +96,6 @@ async def async_setup_entry( ) -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigType, - async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the sensor platform.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data=config, - ) - if ( - result["type"] == FlowResultType.CREATE_ENTRY - or result["reason"] == "already_configured" - ): - async_create_issue( - hass, - HOMEASSISTANT_DOMAIN, - f"deprecated_yaml_{DOMAIN}", - breaks_in_ha_version="2024.7.0", - is_fixable=False, - issue_domain=DOMAIN, - severity=IssueSeverity.WARNING, - translation_key="deprecated_yaml", - translation_placeholders={ - "domain": DOMAIN, - "integration_title": "Swiss public transport", - }, - ) - else: - async_create_issue( - hass, - DOMAIN, - f"deprecated_yaml_import_issue_{result['reason']}", - breaks_in_ha_version="2024.7.0", - is_fixable=False, - issue_domain=DOMAIN, - severity=IssueSeverity.WARNING, - translation_key=f"deprecated_yaml_import_issue_{result['reason']}", - translation_placeholders=PLACEHOLDERS, - ) - - class SwissPublicTransportSensor( CoordinatorEntity[SwissPublicTransportDataUpdateCoordinator], SensorEntity ): diff --git a/homeassistant/components/swiss_public_transport/strings.json b/homeassistant/components/swiss_public_transport/strings.json index cddc732d3ed..4732bb0f527 100644 --- a/homeassistant/components/swiss_public_transport/strings.json +++ b/homeassistant/components/swiss_public_transport/strings.json @@ -46,19 +46,5 @@ "name": "Delay" } } - }, - "issues": { - "deprecated_yaml_import_issue_cannot_connect": { - "title": "The swiss public transport YAML configuration import cannot connect to server", - "description": "Configuring swiss public transport using YAML is being removed but there was a connection error importing your YAML configuration.\n\nMake sure your Home Assistant can reach the [opendata server]({opendata_url}). In case the server is down, try again later." - }, - "deprecated_yaml_import_issue_bad_config": { - "title": "The swiss public transport YAML configuration import request failed due to bad config", - "description": "Configuring swiss public transport using YAML is being removed but there was bad config imported in your YAML configuration.\n\nCheck the [stationboard]({stationboard_url}) for valid stations." - }, - "deprecated_yaml_import_issue_unknown": { - "title": "The swiss public transport YAML configuration import failed with unknown error raised by python-opendata-transport", - "description": "Configuring swiss public transport using YAML is being removed but there was an unknown error importing your YAML configuration.\n\nCheck your configuration or have a look at the documentation of the integration." - } } } diff --git a/tests/components/swiss_public_transport/test_config_flow.py b/tests/components/swiss_public_transport/test_config_flow.py index 47969cdc9dd..b728c87d4b0 100644 --- a/tests/components/swiss_public_transport/test_config_flow.py +++ b/tests/components/swiss_public_transport/test_config_flow.py @@ -1,6 +1,6 @@ """Test the swiss_public_transport config flow.""" -from unittest.mock import AsyncMock, patch +from unittest.mock import patch from opendata_transport.exceptions import ( OpendataTransportConnectionError, @@ -8,13 +8,11 @@ from opendata_transport.exceptions import ( ) import pytest -from homeassistant import config_entries from homeassistant.components.swiss_public_transport import config_flow from homeassistant.components.swiss_public_transport.const import ( CONF_DESTINATION, CONF_START, ) -from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -126,78 +124,3 @@ async def test_flow_user_init_data_already_configured(hass: HomeAssistant) -> No assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" - - -MOCK_DATA_IMPORT = { - CONF_START: "test_start", - CONF_DESTINATION: "test_destination", - CONF_NAME: "test_name", -} - - -async def test_import( - hass: HomeAssistant, - mock_setup_entry: AsyncMock, -) -> None: - """Test import flow.""" - with patch( - "homeassistant.components.swiss_public_transport.config_flow.OpendataTransport.async_get_data", - autospec=True, - return_value=True, - ): - result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data=MOCK_DATA_IMPORT, - ) - await hass.async_block_till_done() - - assert result["type"] is FlowResultType.CREATE_ENTRY - assert result["data"] == MOCK_DATA_IMPORT - assert len(mock_setup_entry.mock_calls) == 1 - - -@pytest.mark.parametrize( - ("raise_error", "text_error"), - [ - (OpendataTransportConnectionError(), "cannot_connect"), - (OpendataTransportError(), "bad_config"), - (IndexError(), "unknown"), - ], -) -async def test_import_error(hass: HomeAssistant, raise_error, text_error) -> None: - """Test import flow cannot_connect error.""" - with patch( - "homeassistant.components.swiss_public_transport.config_flow.OpendataTransport.async_get_data", - autospec=True, - side_effect=raise_error, - ): - result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data=MOCK_DATA_IMPORT, - ) - await hass.async_block_till_done() - - assert result["type"] is FlowResultType.ABORT - assert result["reason"] == text_error - - -async def test_import_already_configured(hass: HomeAssistant) -> None: - """Test we abort import when entry is already configured.""" - - entry = MockConfigEntry( - domain=config_flow.DOMAIN, - data=MOCK_DATA_IMPORT, - unique_id=f"{MOCK_DATA_IMPORT[CONF_START]} {MOCK_DATA_IMPORT[CONF_DESTINATION]}", - ) - entry.add_to_hass(hass) - - result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data=MOCK_DATA_IMPORT, - ) - - assert result["type"] is FlowResultType.ABORT - assert result["reason"] == "already_configured" From 9f46b582d3efc8a0120657d80bbebe3da34d5350 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 17 Jun 2024 13:33:36 +0200 Subject: [PATCH 0749/1445] Avoid touching internals in Radarr tests (#119821) * Avoid touching internals in Radarr tests * Fix * Fix --- tests/components/radarr/test_calendar.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/tests/components/radarr/test_calendar.py b/tests/components/radarr/test_calendar.py index e82760cadba..ecf8433a445 100644 --- a/tests/components/radarr/test_calendar.py +++ b/tests/components/radarr/test_calendar.py @@ -4,13 +4,12 @@ from datetime import timedelta from freezegun.api import FrozenDateTimeFactory -from homeassistant.components.radarr.const import DOMAIN from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from . import setup_integration +from tests.common import async_fire_time_changed from tests.test_util.aiohttp import AiohttpClientMocker @@ -21,8 +20,7 @@ async def test_calendar( ) -> None: """Test for successfully setting up the Radarr platform.""" freezer.move_to("2021-12-02 00:00:00-08:00") - entry = await setup_integration(hass, aioclient_mock) - coordinator: DataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]["calendar"] + await setup_integration(hass, aioclient_mock) state = hass.states.get("calendar.mock_title") assert state.state == STATE_ON @@ -33,8 +31,9 @@ async def test_calendar( assert state.attributes.get("release_type") == "physicalRelease" assert state.attributes.get("start_time") == "2021-12-02 00:00:00" - freezer.tick(timedelta(hours=16)) - await coordinator.async_refresh() + freezer.tick(timedelta(days=1)) + async_fire_time_changed(hass) + await hass.async_block_till_done() state = hass.states.get("calendar.mock_title") assert state.state == STATE_OFF From dcca749d50d047e3dc0c66c7793ecd73dd03752f Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Mon, 17 Jun 2024 07:47:49 -0400 Subject: [PATCH 0750/1445] Store runtime data inside the config entry in Radarr (#119749) * Store runtime data inside the config entry in Radarr * move entry typing outside constructor --- homeassistant/components/radarr/__init__.py | 46 ++++++++++++------- .../components/radarr/binary_sensor.py | 9 ++-- homeassistant/components/radarr/calendar.py | 10 ++-- .../components/radarr/config_flow.py | 8 ++-- .../components/radarr/coordinator.py | 8 ++-- homeassistant/components/radarr/sensor.py | 11 ++--- 6 files changed, 49 insertions(+), 43 deletions(-) diff --git a/homeassistant/components/radarr/__init__.py b/homeassistant/components/radarr/__init__.py index d3e44e6b7fc..b528e701c71 100644 --- a/homeassistant/components/radarr/__init__.py +++ b/homeassistant/components/radarr/__init__.py @@ -2,7 +2,8 @@ from __future__ import annotations -from typing import Any, cast +from dataclasses import dataclass, fields +from typing import cast from aiopyarr.models.host_configuration import PyArrHostConfiguration from aiopyarr.radarr_client import RadarrClient @@ -34,9 +35,22 @@ from .coordinator import ( ) PLATFORMS = [Platform.BINARY_SENSOR, Platform.CALENDAR, Platform.SENSOR] +type RadarrConfigEntry = ConfigEntry[RadarrData] -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +@dataclass(kw_only=True, slots=True) +class RadarrData: + """Radarr data type.""" + + calendar: CalendarUpdateCoordinator + disk_space: DiskSpaceDataUpdateCoordinator + health: HealthDataUpdateCoordinator + movie: MoviesDataUpdateCoordinator + queue: QueueDataUpdateCoordinator + status: StatusDataUpdateCoordinator + + +async def async_setup_entry(hass: HomeAssistant, entry: RadarrConfigEntry) -> bool: """Set up Radarr from a config entry.""" host_configuration = PyArrHostConfiguration( api_token=entry.data[CONF_API_KEY], @@ -47,27 +61,25 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: host_configuration=host_configuration, session=async_get_clientsession(hass, entry.data[CONF_VERIFY_SSL]), ) - coordinators: dict[str, RadarrDataUpdateCoordinator[Any]] = { - "calendar": CalendarUpdateCoordinator(hass, host_configuration, radarr), - "disk_space": DiskSpaceDataUpdateCoordinator(hass, host_configuration, radarr), - "health": HealthDataUpdateCoordinator(hass, host_configuration, radarr), - "movie": MoviesDataUpdateCoordinator(hass, host_configuration, radarr), - "queue": QueueDataUpdateCoordinator(hass, host_configuration, radarr), - "status": StatusDataUpdateCoordinator(hass, host_configuration, radarr), - } - for coordinator in coordinators.values(): - await coordinator.async_config_entry_first_refresh() - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinators + data = RadarrData( + calendar=CalendarUpdateCoordinator(hass, host_configuration, radarr), + disk_space=DiskSpaceDataUpdateCoordinator(hass, host_configuration, radarr), + health=HealthDataUpdateCoordinator(hass, host_configuration, radarr), + movie=MoviesDataUpdateCoordinator(hass, host_configuration, radarr), + queue=QueueDataUpdateCoordinator(hass, host_configuration, radarr), + status=StatusDataUpdateCoordinator(hass, host_configuration, radarr), + ) + for field in fields(data): + await getattr(data, field.name).async_config_entry_first_refresh() + entry.runtime_data = data await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: RadarrConfigEntry) -> bool: """Unload a config entry.""" - if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): - hass.data[DOMAIN].pop(entry.entry_id) - return unload_ok + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) class RadarrEntity(CoordinatorEntity[RadarrDataUpdateCoordinator[T]]): diff --git a/homeassistant/components/radarr/binary_sensor.py b/homeassistant/components/radarr/binary_sensor.py index 4962ef81614..6c0468cff58 100644 --- a/homeassistant/components/radarr/binary_sensor.py +++ b/homeassistant/components/radarr/binary_sensor.py @@ -9,13 +9,12 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntity, BinarySensorEntityDescription, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import RadarrEntity -from .const import DOMAIN, HEALTH_ISSUES +from . import RadarrConfigEntry, RadarrEntity +from .const import HEALTH_ISSUES BINARY_SENSOR_TYPE = BinarySensorEntityDescription( key="health", @@ -27,11 +26,11 @@ BINARY_SENSOR_TYPE = BinarySensorEntityDescription( async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: RadarrConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up Radarr sensors based on a config entry.""" - coordinator = hass.data[DOMAIN][entry.entry_id]["health"] + coordinator = entry.runtime_data.health async_add_entities([RadarrBinarySensor(coordinator, BINARY_SENSOR_TYPE)]) diff --git a/homeassistant/components/radarr/calendar.py b/homeassistant/components/radarr/calendar.py index ad5e1b8ffd9..4f866123a1a 100644 --- a/homeassistant/components/radarr/calendar.py +++ b/homeassistant/components/radarr/calendar.py @@ -5,13 +5,11 @@ from __future__ import annotations from datetime import datetime from homeassistant.components.calendar import CalendarEntity, CalendarEvent -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity import EntityDescription from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import RadarrEntity -from .const import DOMAIN +from . import RadarrConfigEntry, RadarrEntity from .coordinator import CalendarUpdateCoordinator, RadarrEvent CALENDAR_TYPE = EntityDescription( @@ -21,10 +19,12 @@ CALENDAR_TYPE = EntityDescription( async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, + entry: RadarrConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Radarr calendar entity.""" - coordinator = hass.data[DOMAIN][entry.entry_id]["calendar"] + coordinator = entry.runtime_data.calendar async_add_entities([RadarrCalendarEntity(coordinator, CALENDAR_TYPE)]) diff --git a/homeassistant/components/radarr/config_flow.py b/homeassistant/components/radarr/config_flow.py index 81589c5fe30..3bf0796a9a8 100644 --- a/homeassistant/components/radarr/config_flow.py +++ b/homeassistant/components/radarr/config_flow.py @@ -11,11 +11,12 @@ from aiopyarr.models.host_configuration import PyArrHostConfiguration from aiopyarr.radarr_client import RadarrClient import voluptuous as vol -from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_API_KEY, CONF_URL, CONF_VERIFY_SSL from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession +from . import RadarrConfigEntry from .const import DEFAULT_NAME, DEFAULT_URL, DOMAIN @@ -23,10 +24,7 @@ class RadarrConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Radarr.""" VERSION = 1 - - def __init__(self) -> None: - """Initialize the flow.""" - self.entry: ConfigEntry | None = None + entry: RadarrConfigEntry | None = None async def async_step_reauth(self, _: Mapping[str, Any]) -> ConfigFlowResult: """Handle configuration by re-auth.""" diff --git a/homeassistant/components/radarr/coordinator.py b/homeassistant/components/radarr/coordinator.py index 47a1862b8ae..6e8a3d55d3e 100644 --- a/homeassistant/components/radarr/coordinator.py +++ b/homeassistant/components/radarr/coordinator.py @@ -6,7 +6,7 @@ from abc import ABC, abstractmethod import asyncio from dataclasses import dataclass from datetime import date, datetime, timedelta -from typing import Generic, TypeVar, cast +from typing import TYPE_CHECKING, Generic, TypeVar, cast from aiopyarr import ( Health, @@ -20,13 +20,15 @@ from aiopyarr.models.host_configuration import PyArrHostConfiguration from aiopyarr.radarr_client import RadarrClient from homeassistant.components.calendar import CalendarEvent -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import DEFAULT_MAX_RECORDS, DOMAIN, LOGGER +if TYPE_CHECKING: + from . import RadarrConfigEntry + T = TypeVar("T", bound=SystemStatus | list[RootFolder] | list[Health] | int | None) @@ -45,7 +47,7 @@ class RadarrEvent(CalendarEvent, RadarrEventMixIn): class RadarrDataUpdateCoordinator(DataUpdateCoordinator[T], Generic[T], ABC): """Data update coordinator for the Radarr integration.""" - config_entry: ConfigEntry + config_entry: RadarrConfigEntry _update_interval = timedelta(seconds=30) def __init__( diff --git a/homeassistant/components/radarr/sensor.py b/homeassistant/components/radarr/sensor.py index e6700fb3637..441c44de781 100644 --- a/homeassistant/components/radarr/sensor.py +++ b/homeassistant/components/radarr/sensor.py @@ -15,13 +15,11 @@ from homeassistant.components.sensor import ( SensorEntityDescription, SensorStateClass, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory, UnitOfInformation from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import RadarrEntity -from .const import DOMAIN +from . import RadarrConfigEntry, RadarrEntity from .coordinator import RadarrDataUpdateCoordinator, T @@ -117,16 +115,13 @@ PARALLEL_UPDATES = 1 async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: RadarrConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up Radarr sensors based on a config entry.""" - coordinators: dict[str, RadarrDataUpdateCoordinator[Any]] = hass.data[DOMAIN][ - entry.entry_id - ] entities: list[RadarrSensor[Any]] = [] for coordinator_type, description in SENSOR_TYPES.items(): - coordinator = coordinators[coordinator_type] + coordinator = getattr(entry.runtime_data, coordinator_type) if coordinator_type != "disk_space": entities.append(RadarrSensor(coordinator, description)) else: From 442554c2234b96fc2663927d0c12769582bb02fb Mon Sep 17 00:00:00 2001 From: Alexandre CUER Date: Mon, 17 Jun 2024 13:59:47 +0200 Subject: [PATCH 0751/1445] Migrate Emoncms to external library (#119772) * Migrate Emoncms to external library https://github.com/Open-Building-Management/pyemoncms * Remove the throttle decorator * Remove MIN_TIME_BETWEEN_UPDATES as not used --- .../components/emoncms/manifest.json | 3 +- homeassistant/components/emoncms/sensor.py | 98 +++---------------- requirements_all.txt | 3 + 3 files changed, 19 insertions(+), 85 deletions(-) diff --git a/homeassistant/components/emoncms/manifest.json b/homeassistant/components/emoncms/manifest.json index 02008a90ac9..4b617b0e2f2 100644 --- a/homeassistant/components/emoncms/manifest.json +++ b/homeassistant/components/emoncms/manifest.json @@ -3,5 +3,6 @@ "name": "Emoncms", "codeowners": ["@borpin", "@alexandrecuer"], "documentation": "https://www.home-assistant.io/integrations/emoncms", - "iot_class": "local_polling" + "iot_class": "local_polling", + "requirements": ["pyemoncms==0.0.6"] } diff --git a/homeassistant/components/emoncms/sensor.py b/homeassistant/components/emoncms/sensor.py index cf21cb75847..443cd1bd5d0 100644 --- a/homeassistant/components/emoncms/sensor.py +++ b/homeassistant/components/emoncms/sensor.py @@ -2,12 +2,10 @@ from __future__ import annotations -from datetime import timedelta -from http import HTTPStatus import logging from typing import Any -import requests +from pyemoncms import EmoncmsClient import voluptuous as vol from homeassistant.components.sensor import ( @@ -30,7 +28,6 @@ from homeassistant.helpers import template import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) @@ -48,7 +45,6 @@ CONF_SENSOR_NAMES = "sensor_names" DECIMALS = 2 DEFAULT_UNIT = UnitOfPower.WATT -MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=5) ONLY_INCL_EXCL_NONE = "only_include_exclude_or_none" @@ -72,17 +68,10 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -def get_id( - sensorid: str, feedtag: str, feedname: str, feedid: str, feeduserid: str -) -> str: - """Return unique identifier for feed / sensor.""" - return f"emoncms{sensorid}_{feedtag}_{feedname}_{feedid}_{feeduserid}" - - -def setup_platform( +async def async_setup_platform( hass: HomeAssistant, config: ConfigType, - add_entities: AddEntitiesCallback, + async_add_entities: AddEntitiesCallback, discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the Emoncms sensor.""" @@ -98,16 +87,15 @@ def setup_platform( if value_template is not None: value_template.hass = hass - data = EmonCmsData(hass, url, apikey) + emoncms_client = EmoncmsClient(url, apikey) + elems = await emoncms_client.async_list_feeds() - data.update() - - if data.data is None: + if elems is None: return sensors = [] - for elem in data.data: + for elem in elems: if exclude_feeds is not None and int(elem["id"]) in exclude_feeds: continue @@ -126,7 +114,7 @@ def setup_platform( sensors.append( EmonCmsSensor( hass, - data, + emoncms_client, name, value_template, unit_of_measurement, @@ -134,7 +122,7 @@ def setup_platform( elem, ) ) - add_entities(sensors) + async_add_entities(sensors) class EmonCmsSensor(SensorEntity): @@ -143,7 +131,7 @@ class EmonCmsSensor(SensorEntity): def __init__( self, hass: HomeAssistant, - data: EmonCmsData, + emoncms_client: EmoncmsClient, name: str | None, value_template: template.Template | None, unit_of_measurement: str | None, @@ -161,14 +149,12 @@ class EmonCmsSensor(SensorEntity): self._attr_name = f"EmonCMS{id_for_name} {feed_name}" else: self._attr_name = name - self._identifier = get_id( - sensorid, elem["tag"], elem["name"], elem["id"], elem["userid"] - ) self._hass = hass - self._data = data + self._emoncms_client = emoncms_client self._value_template = value_template self._attr_native_unit_of_measurement = unit_of_measurement self._sensorid = sensorid + self._feed_id = elem["id"] if unit_of_measurement in ("kWh", "Wh"): self._attr_device_class = SensorDeviceClass.ENERGY @@ -221,65 +207,9 @@ class EmonCmsSensor(SensorEntity): elif elem["value"] is not None: self._attr_native_value = round(float(elem["value"]), DECIMALS) - def update(self) -> None: + async def async_update(self) -> None: """Get the latest data and updates the state.""" - self._data.update() - - if self._data.data is None: - return - - elem = next( - ( - elem - for elem in self._data.data - if get_id( - self._sensorid, - elem["tag"], - elem["name"], - elem["id"], - elem["userid"], - ) - == self._identifier - ), - None, - ) - + elem = await self._emoncms_client.async_get_feed_fields(self._feed_id) if elem is None: return - self._update_attributes(elem) - - -class EmonCmsData: - """The class for handling the data retrieval.""" - - def __init__(self, hass: HomeAssistant, url: str, apikey: str) -> None: - """Initialize the data object.""" - self._apikey = apikey - self._url = f"{url}/feed/list.json" - self._hass = hass - self.data: list[dict[str, Any]] | None = None - - @Throttle(MIN_TIME_BETWEEN_UPDATES) - def update(self) -> None: - """Get the latest data from Emoncms.""" - try: - parameters = {"apikey": self._apikey} - req = requests.get( - self._url, params=parameters, allow_redirects=True, timeout=5 - ) - except requests.exceptions.RequestException as exception: - _LOGGER.error(exception) - return - - if req.status_code == HTTPStatus.OK: - self.data = req.json() - else: - _LOGGER.error( - ( - "Please verify if the specified configuration value " - "'%s' is correct! (HTTP Status_code = %d)" - ), - CONF_URL, - req.status_code, - ) diff --git a/requirements_all.txt b/requirements_all.txt index 62559a9a84e..27c50d3fe60 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1826,6 +1826,9 @@ pyefergy==22.5.0 # homeassistant.components.energenie_power_sockets pyegps==0.2.5 +# homeassistant.components.emoncms +pyemoncms==0.0.6 + # homeassistant.components.enphase_envoy pyenphase==1.20.3 From c0a3b507c087a869f2b3662b4a9daed023276fae Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 17 Jun 2024 14:39:07 +0200 Subject: [PATCH 0752/1445] Add tests of frontend.add_extra_js_url (#119826) --- homeassistant/components/frontend/__init__.py | 5 +++- tests/components/frontend/test_init.py | 23 +++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 2f64a019c19..89283b01037 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -322,7 +322,10 @@ def async_remove_panel(hass: HomeAssistant, frontend_url_path: str) -> None: def add_extra_js_url(hass: HomeAssistant, url: str, es5: bool = False) -> None: - """Register extra js or module url to load.""" + """Register extra js or module url to load. + + This function allows custom integrations to register extra js or module. + """ key = DATA_EXTRA_JS_URL_ES5 if es5 else DATA_EXTRA_MODULE_URL hass.data[key].add(url) diff --git a/tests/components/frontend/test_init.py b/tests/components/frontend/test_init.py index 084db2a27d5..610e18ddcff 100644 --- a/tests/components/frontend/test_init.py +++ b/tests/components/frontend/test_init.py @@ -18,6 +18,7 @@ from homeassistant.components.frontend import ( DOMAIN, EVENT_PANELS_UPDATED, THEMES_STORAGE_KEY, + add_extra_js_url, async_register_built_in_panel, async_remove_panel, ) @@ -416,6 +417,17 @@ async def test_extra_js(hass: HomeAssistant, mock_http_client_with_extra_js) -> assert '"/local/my_module.js"' in text assert '"/local/my_es5.js"' in text + # Test dynamically adding extra javascript + add_extra_js_url(hass, "/local/my_module_2.js", False) + add_extra_js_url(hass, "/local/my_es5_2.js", True) + resp = await mock_http_client_with_extra_js.get("") + assert resp.status == 200 + assert "cache-control" not in resp.headers + + text = await resp.text() + assert '"/local/my_module_2.js"' in text + assert '"/local/my_es5_2.js"' in text + # safe mode hass.config.safe_mode = True resp = await mock_http_client_with_extra_js.get("") @@ -426,6 +438,17 @@ async def test_extra_js(hass: HomeAssistant, mock_http_client_with_extra_js) -> assert '"/local/my_module.js"' not in text assert '"/local/my_es5.js"' not in text + # Test dynamically adding extra javascript + add_extra_js_url(hass, "/local/my_module_2.js", False) + add_extra_js_url(hass, "/local/my_es5_2.js", True) + resp = await mock_http_client_with_extra_js.get("") + assert resp.status == 200 + assert "cache-control" not in resp.headers + + text = await resp.text() + assert '"/local/my_module_2.js"' not in text + assert '"/local/my_es5_2.js"' not in text + async def test_get_panels( hass: HomeAssistant, hass_ws_client: WebSocketGenerator, mock_http_client From 8af57487163019097404873967a3aa529bc8e87b Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 17 Jun 2024 15:15:00 +0200 Subject: [PATCH 0753/1445] Add frontend.remove_extra_js_url (#119831) --- homeassistant/components/frontend/__init__.py | 9 ++++ tests/components/frontend/test_init.py | 46 +++++++++++-------- 2 files changed, 35 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 89283b01037..7ff7f76c61c 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -330,6 +330,15 @@ def add_extra_js_url(hass: HomeAssistant, url: str, es5: bool = False) -> None: hass.data[key].add(url) +def remove_extra_js_url(hass: HomeAssistant, url: str, es5: bool = False) -> None: + """Remove extra js or module url to load. + + This function allows custom integrations to remove extra js or module. + """ + key = DATA_EXTRA_JS_URL_ES5 if es5 else DATA_EXTRA_MODULE_URL + hass.data[key].remove(url) + + def add_manifest_json_key(key: str, val: Any) -> None: """Add a keyval to the manifest.json.""" MANIFEST_JSON.update_key(key, val) diff --git a/tests/components/frontend/test_init.py b/tests/components/frontend/test_init.py index 610e18ddcff..81bec28598d 100644 --- a/tests/components/frontend/test_init.py +++ b/tests/components/frontend/test_init.py @@ -21,6 +21,7 @@ from homeassistant.components.frontend import ( add_extra_js_url, async_register_built_in_panel, async_remove_panel, + remove_extra_js_url, ) from homeassistant.components.websocket_api import TYPE_RESULT from homeassistant.core import HomeAssistant @@ -409,43 +410,48 @@ async def test_missing_themes(hass: HomeAssistant, ws_client) -> None: @pytest.mark.usefixtures("mock_onboarded") async def test_extra_js(hass: HomeAssistant, mock_http_client_with_extra_js) -> None: """Test that extra javascript is loaded.""" - resp = await mock_http_client_with_extra_js.get("") - assert resp.status == 200 - assert "cache-control" not in resp.headers - text = await resp.text() + async def get_response(): + resp = await mock_http_client_with_extra_js.get("") + assert resp.status == 200 + assert "cache-control" not in resp.headers + + return await resp.text() + + text = await get_response() assert '"/local/my_module.js"' in text assert '"/local/my_es5.js"' in text - # Test dynamically adding extra javascript + # Test dynamically adding and removing extra javascript add_extra_js_url(hass, "/local/my_module_2.js", False) add_extra_js_url(hass, "/local/my_es5_2.js", True) - resp = await mock_http_client_with_extra_js.get("") - assert resp.status == 200 - assert "cache-control" not in resp.headers - - text = await resp.text() + text = await get_response() assert '"/local/my_module_2.js"' in text assert '"/local/my_es5_2.js"' in text + remove_extra_js_url(hass, "/local/my_module_2.js", False) + remove_extra_js_url(hass, "/local/my_es5_2.js", True) + text = await get_response() + assert '"/local/my_module_2.js"' not in text + assert '"/local/my_es5_2.js"' not in text + + # Remove again should not raise + remove_extra_js_url(hass, "/local/my_module_2.js", False) + remove_extra_js_url(hass, "/local/my_es5_2.js", True) + text = await get_response() + assert '"/local/my_module_2.js"' not in text + assert '"/local/my_es5_2.js"' not in text + # safe mode hass.config.safe_mode = True - resp = await mock_http_client_with_extra_js.get("") - assert resp.status == 200 - assert "cache-control" not in resp.headers - - text = await resp.text() + text = await get_response() assert '"/local/my_module.js"' not in text assert '"/local/my_es5.js"' not in text # Test dynamically adding extra javascript add_extra_js_url(hass, "/local/my_module_2.js", False) add_extra_js_url(hass, "/local/my_es5_2.js", True) - resp = await mock_http_client_with_extra_js.get("") - assert resp.status == 200 - assert "cache-control" not in resp.headers - - text = await resp.text() + text = await get_response() assert '"/local/my_module_2.js"' not in text assert '"/local/my_es5_2.js"' not in text From 71a9ba25dca17a3bd07c46dedcdcfff1ff7228be Mon Sep 17 00:00:00 2001 From: jvmahon Date: Mon, 17 Jun 2024 09:30:59 -0400 Subject: [PATCH 0754/1445] Use "Button" label to name Matter event (#119768) --- homeassistant/components/matter/event.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/matter/event.py b/homeassistant/components/matter/event.py index ea48beef782..ade3452a6cf 100644 --- a/homeassistant/components/matter/event.py +++ b/homeassistant/components/matter/event.py @@ -79,7 +79,7 @@ class MatterEventEntity(MatterEntity, EventEntity): clusters.FixedLabel.Attributes.LabelList ): for label in labels: - if label.label == "Label": + if label.label in ["Label", "Button"]: label_value: str = label.value # in the case the label is only the label id, prettify it a bit if label_value.isnumeric(): From 57308599cd7423c1eeff15bc82c9033af455f195 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 17 Jun 2024 12:05:44 -0500 Subject: [PATCH 0755/1445] Bump aiozoneinfo to 0.2.0 (#119845) --- homeassistant/package_constraints.txt | 2 +- pyproject.toml | 2 +- requirements.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index e55f0dd1cf2..ae1a95fc5b1 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -7,7 +7,7 @@ aiohttp-fast-url-dispatcher==0.3.0 aiohttp-fast-zlib==0.1.0 aiohttp==3.9.5 aiohttp_cors==0.7.0 -aiozoneinfo==0.1.0 +aiozoneinfo==0.2.0 astral==2.2 async-interrupt==1.1.1 async-upnp-client==0.38.3 diff --git a/pyproject.toml b/pyproject.toml index da08e9cee84..cf41b415a91 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,7 +28,7 @@ dependencies = [ "aiohttp_cors==0.7.0", "aiohttp-fast-url-dispatcher==0.3.0", "aiohttp-fast-zlib==0.1.0", - "aiozoneinfo==0.1.0", + "aiozoneinfo==0.2.0", "astral==2.2", "async-interrupt==1.1.1", "attrs==23.2.0", diff --git a/requirements.txt b/requirements.txt index a81815a2651..e08c02510ce 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,7 +8,7 @@ aiohttp==3.9.5 aiohttp_cors==0.7.0 aiohttp-fast-url-dispatcher==0.3.0 aiohttp-fast-zlib==0.1.0 -aiozoneinfo==0.1.0 +aiozoneinfo==0.2.0 astral==2.2 async-interrupt==1.1.1 attrs==23.2.0 From 87c1d5a6a796f876ab5703e65853940566e8e969 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Mon, 17 Jun 2024 19:17:06 +0200 Subject: [PATCH 0756/1445] Remove the switch entity for Shelly Gas Valve (#119817) Co-authored-by: Maciej Bieniek <478555+bieniu@users.noreply.github.com> --- homeassistant/components/shelly/switch.py | 135 +--------------------- homeassistant/components/shelly/valve.py | 5 +- tests/components/shelly/test_switch.py | 106 +---------------- 3 files changed, 10 insertions(+), 236 deletions(-) diff --git a/homeassistant/components/shelly/switch.py b/homeassistant/components/shelly/switch.py index eda61e44d84..09ee133589b 100644 --- a/homeassistant/components/shelly/switch.py +++ b/homeassistant/components/shelly/switch.py @@ -6,38 +6,22 @@ from dataclasses import dataclass from typing import Any, cast from aioshelly.block_device import Block -from aioshelly.const import ( - MODEL_2, - MODEL_25, - MODEL_GAS, - MODEL_WALL_DISPLAY, - RPC_GENERATIONS, -) +from aioshelly.const import MODEL_2, MODEL_25, MODEL_WALL_DISPLAY, RPC_GENERATIONS -from homeassistant.components.automation import automations_with_entity -from homeassistant.components.script import scripts_with_entity -from homeassistant.components.switch import ( - DOMAIN as SWITCH_DOMAIN, - SwitchEntity, - SwitchEntityDescription, -) -from homeassistant.components.valve import DOMAIN as VALVE_DOMAIN +from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription from homeassistant.const import STATE_ON, EntityCategory from homeassistant.core import HomeAssistant, State, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_registry import RegistryEntry -from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.restore_state import RestoreEntity -from .const import CONF_SLEEP_PERIOD, DOMAIN, GAS_VALVE_OPEN_STATES, MOTION_MODELS +from .const import CONF_SLEEP_PERIOD, MOTION_MODELS from .coordinator import ShellyBlockCoordinator, ShellyConfigEntry, ShellyRpcCoordinator from .entity import ( BlockEntityDescription, - ShellyBlockAttributeEntity, ShellyBlockEntity, ShellyRpcEntity, ShellySleepingBlockAttributeEntity, - async_setup_block_attribute_entities, async_setup_entry_attribute_entities, ) from .utils import ( @@ -56,15 +40,6 @@ class BlockSwitchDescription(BlockEntityDescription, SwitchEntityDescription): """Class to describe a BLOCK switch.""" -# This entity description is deprecated and will be removed in Home Assistant 2024.7.0. -GAS_VALVE_SWITCH = BlockSwitchDescription( - key="valve|valve", - name="Valve", - available=lambda block: block.valve not in ("failure", "checking"), - removal_condition=lambda _, block: block.valve in ("not_connected", "unknown"), - entity_registry_enabled_default=False, -) - MOTION_SWITCH = BlockSwitchDescription( key="sensor|motionActive", name="Motion detection", @@ -94,17 +69,6 @@ def async_setup_block_entry( coordinator = config_entry.runtime_data.block assert coordinator - # Add Shelly Gas Valve as a switch - if coordinator.model == MODEL_GAS: - async_setup_block_attribute_entities( - hass, - async_add_entities, - coordinator, - {("valve", "valve"): GAS_VALVE_SWITCH}, - BlockValveSwitch, - ) - return - # Add Shelly Motion as a switch if coordinator.model in MOTION_MODELS: async_setup_entry_attribute_entities( @@ -238,99 +202,6 @@ class BlockSleepingMotionSwitch( self.last_state = last_state -class BlockValveSwitch(ShellyBlockAttributeEntity, SwitchEntity): - """Entity that controls a Gas Valve on Block based Shelly devices. - - This class is deprecated and will be removed in Home Assistant 2024.7.0. - """ - - entity_description: BlockSwitchDescription - _attr_translation_key = "valve_switch" - - def __init__( - self, - coordinator: ShellyBlockCoordinator, - block: Block, - attribute: str, - description: BlockSwitchDescription, - ) -> None: - """Initialize valve.""" - super().__init__(coordinator, block, attribute, description) - self.control_result: dict[str, Any] | None = None - - @property - def is_on(self) -> bool: - """If valve is open.""" - if self.control_result: - return self.control_result["state"] in GAS_VALVE_OPEN_STATES - - return self.attribute_value in GAS_VALVE_OPEN_STATES - - async def async_turn_on(self, **kwargs: Any) -> None: - """Open valve.""" - async_create_issue( - self.hass, - DOMAIN, - "deprecated_valve_switch", - breaks_in_ha_version="2024.7.0", - is_fixable=True, - severity=IssueSeverity.WARNING, - translation_key="deprecated_valve_switch", - translation_placeholders={ - "entity": f"{VALVE_DOMAIN}.{cast(str, self.name).lower().replace(' ', '_')}", - "service": f"{VALVE_DOMAIN}.open_valve", - }, - ) - self.control_result = await self.set_state(go="open") - self.async_write_ha_state() - - async def async_turn_off(self, **kwargs: Any) -> None: - """Close valve.""" - async_create_issue( - self.hass, - DOMAIN, - "deprecated_valve_switch", - breaks_in_ha_version="2024.7.0", - is_fixable=True, - severity=IssueSeverity.WARNING, - translation_key="deprecated_valve_switche", - translation_placeholders={ - "entity": f"{VALVE_DOMAIN}.{cast(str, self.name).lower().replace(' ', '_')}", - "service": f"{VALVE_DOMAIN}.close_valve", - }, - ) - self.control_result = await self.set_state(go="close") - self.async_write_ha_state() - - async def async_added_to_hass(self) -> None: - """Set up a listener when this entity is added to HA.""" - await super().async_added_to_hass() - - entity_automations = automations_with_entity(self.hass, self.entity_id) - entity_scripts = scripts_with_entity(self.hass, self.entity_id) - for item in entity_automations + entity_scripts: - async_create_issue( - self.hass, - DOMAIN, - f"deprecated_valve_{self.entity_id}_{item}", - breaks_in_ha_version="2024.7.0", - is_fixable=True, - severity=IssueSeverity.WARNING, - translation_key="deprecated_valve_switch_entity", - translation_placeholders={ - "entity": f"{SWITCH_DOMAIN}.{cast(str, self.name).lower().replace(' ', '_')}", - "info": item, - }, - ) - - @callback - def _update_callback(self) -> None: - """When device updates, clear control result that overrides state.""" - self.control_result = None - - super()._update_callback() - - class BlockRelaySwitch(ShellyBlockEntity, SwitchEntity): """Entity that controls a relay on Block based Shelly devices.""" diff --git a/homeassistant/components/shelly/valve.py b/homeassistant/components/shelly/valve.py index 83c1f577439..ea6feaabe69 100644 --- a/homeassistant/components/shelly/valve.py +++ b/homeassistant/components/shelly/valve.py @@ -23,7 +23,7 @@ from .entity import ( ShellyBlockAttributeEntity, async_setup_block_attribute_entities, ) -from .utils import get_device_entry_gen +from .utils import async_remove_shelly_entity, get_device_entry_gen @dataclass(kw_only=True, frozen=True) @@ -67,6 +67,9 @@ def async_setup_block_entry( {("valve", "valve"): GAS_VALVE}, BlockShellyValve, ) + # Remove deprecated switch entity for gas valve + unique_id = f"{coordinator.mac}-valve_0-valve" + async_remove_shelly_entity(hass, "switch", unique_id) class BlockShellyValve(ShellyBlockAttributeEntity, ValveEntity): diff --git a/tests/components/shelly/test_switch.py b/tests/components/shelly/test_switch.py index daaf03b081b..637a92a7fbe 100644 --- a/tests/components/shelly/test_switch.py +++ b/tests/components/shelly/test_switch.py @@ -7,10 +7,7 @@ from aioshelly.const import MODEL_GAS from aioshelly.exceptions import DeviceConnectionError, InvalidAuthError, RpcCallError import pytest -from homeassistant.components import automation, script -from homeassistant.components.automation import automations_with_entity from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN -from homeassistant.components.script import scripts_with_entity from homeassistant.components.shelly.const import ( DOMAIN, MODEL_WALL_DISPLAY, @@ -30,8 +27,6 @@ from homeassistant.core import HomeAssistant, State from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.device_registry import DeviceRegistry from homeassistant.helpers.entity_registry import EntityRegistry -import homeassistant.helpers.issue_registry as ir -from homeassistant.setup import async_setup_component from . import get_entity_state, init_integration, register_device, register_entity @@ -388,13 +383,12 @@ async def test_rpc_auth_error( assert flow["context"].get("entry_id") == entry.entry_id -async def test_block_device_gas_valve( +async def test_remove_gas_valve_switch( hass: HomeAssistant, mock_block_device: Mock, entity_registry: EntityRegistry, - monkeypatch: pytest.MonkeyPatch, ) -> None: - """Test block device Shelly Gas with Valve addon.""" + """Test removing deprecated switch entity for Shelly Gas Valve.""" entity_id = register_entity( hass, SWITCH_DOMAIN, @@ -403,41 +397,7 @@ async def test_block_device_gas_valve( ) await init_integration(hass, 1, MODEL_GAS) - entry = entity_registry.async_get(entity_id) - assert entry - assert entry.unique_id == "123456789ABC-valve_0-valve" - - assert hass.states.get(entity_id).state == STATE_OFF # valve is closed - - await hass.services.async_call( - SWITCH_DOMAIN, - SERVICE_TURN_ON, - {ATTR_ENTITY_ID: entity_id}, - blocking=True, - ) - - state = hass.states.get(entity_id) - assert state - assert state.state == STATE_ON # valve is open - - await hass.services.async_call( - SWITCH_DOMAIN, - SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: entity_id}, - blocking=True, - ) - - state = hass.states.get(entity_id) - assert state - assert state.state == STATE_OFF # valve is closed - - monkeypatch.setattr(mock_block_device.blocks[GAS_VALVE_BLOCK_ID], "valve", "opened") - mock_block_device.mock_update() - await hass.async_block_till_done() - - state = hass.states.get(entity_id) - assert state - assert state.state == STATE_ON # valve is open + assert entity_registry.async_get(entity_id) is None async def test_wall_display_relay_mode( @@ -470,63 +430,3 @@ async def test_wall_display_relay_mode( entry = entity_registry.async_get(switch_entity_id) assert entry assert entry.unique_id == "123456789ABC-switch:0" - - -@pytest.mark.usefixtures("entity_registry_enabled_by_default") -async def test_create_issue_valve_switch( - hass: HomeAssistant, - mock_block_device: Mock, - monkeypatch: pytest.MonkeyPatch, - issue_registry: ir.IssueRegistry, -) -> None: - """Test we create an issue when an automation or script is using a deprecated entity.""" - monkeypatch.setitem(mock_block_device.status, "cloud", {"connected": False}) - entity_id = register_entity( - hass, - SWITCH_DOMAIN, - "test_name_valve", - "valve_0-valve", - ) - - assert await async_setup_component( - hass, - automation.DOMAIN, - { - automation.DOMAIN: { - "alias": "test", - "trigger": {"platform": "state", "entity_id": entity_id}, - "action": {"service": "switch.turn_on", "entity_id": entity_id}, - } - }, - ) - assert await async_setup_component( - hass, - script.DOMAIN, - { - script.DOMAIN: { - "test": { - "sequence": [ - { - "service": "switch.turn_on", - "data": {"entity_id": entity_id}, - }, - ], - } - } - }, - ) - - await init_integration(hass, 1, MODEL_GAS) - - assert automations_with_entity(hass, entity_id)[0] == "automation.test" - assert scripts_with_entity(hass, entity_id)[0] == "script.test" - - assert issue_registry.async_get_issue(DOMAIN, "deprecated_valve_switch") - assert issue_registry.async_get_issue( - DOMAIN, "deprecated_valve_switch.test_name_valve_automation.test" - ) - assert issue_registry.async_get_issue( - DOMAIN, "deprecated_valve_switch.test_name_valve_script.test" - ) - - assert len(issue_registry.issues) == 3 From 2560d7aeda47af4f5b653ec2d84c5c7d0ba668d4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 17 Jun 2024 12:17:36 -0500 Subject: [PATCH 0757/1445] Bump uiprotect to 1.18.1 (#119848) changelog: https://github.com/uilibs/uiprotect/compare/v1.17.0...v1.18.1 --- homeassistant/components/unifiprotect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index cde29aa1770..527fa4ef0e6 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -40,7 +40,7 @@ "integration_type": "hub", "iot_class": "local_push", "loggers": ["uiprotect", "unifi_discovery"], - "requirements": ["uiprotect==1.17.0", "unifi-discovery==1.1.8"], + "requirements": ["uiprotect==1.18.1", "unifi-discovery==1.1.8"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/requirements_all.txt b/requirements_all.txt index 27c50d3fe60..82c50ce7c70 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2791,7 +2791,7 @@ twitchAPI==4.0.0 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==1.17.0 +uiprotect==1.18.1 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 27653bde360..514b26ecbe9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2168,7 +2168,7 @@ twitchAPI==4.0.0 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==1.17.0 +uiprotect==1.18.1 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 From e5eef7c6ddc69b2c3b73186d041fd0e0c7e2babb Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 17 Jun 2024 19:17:52 +0200 Subject: [PATCH 0758/1445] Fix Dremel 3D printer tests (#119853) --- .../components/dremel_3d_printer/conftest.py | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/tests/components/dremel_3d_printer/conftest.py b/tests/components/dremel_3d_printer/conftest.py index 0284d8baebf..6490b844dc0 100644 --- a/tests/components/dremel_3d_printer/conftest.py +++ b/tests/components/dremel_3d_printer/conftest.py @@ -32,23 +32,23 @@ def config_entry(hass: HomeAssistant) -> MockConfigEntry: @pytest.fixture def connection() -> None: """Mock Dremel 3D Printer connection.""" - mock = requests_mock.Mocker() - mock.post( - f"http://{HOST}:80/command", - response_list=[ - {"text": load_fixture("dremel_3d_printer/command_1.json")}, - {"text": load_fixture("dremel_3d_printer/command_2.json")}, - {"text": load_fixture("dremel_3d_printer/command_1.json")}, - {"text": load_fixture("dremel_3d_printer/command_2.json")}, - ], - ) + with requests_mock.Mocker() as mock: + mock.post( + f"http://{HOST}:80/command", + response_list=[ + {"text": load_fixture("dremel_3d_printer/command_1.json")}, + {"text": load_fixture("dremel_3d_printer/command_2.json")}, + {"text": load_fixture("dremel_3d_printer/command_1.json")}, + {"text": load_fixture("dremel_3d_printer/command_2.json")}, + ], + ) - mock.post( - f"https://{HOST}:11134/getHomeMessage", - text=load_fixture("dremel_3d_printer/get_home_message.json"), - status_code=HTTPStatus.OK, - ) - mock.start() + mock.post( + f"https://{HOST}:11134/getHomeMessage", + text=load_fixture("dremel_3d_printer/get_home_message.json"), + status_code=HTTPStatus.OK, + ) + yield def patch_async_setup_entry(): From adacdd3a9fbabeb39cf78fec4e8cc608907b197e Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Mon, 17 Jun 2024 13:18:59 -0400 Subject: [PATCH 0759/1445] Run Radarr movie coordinator first refresh in background (#119827) --- homeassistant/components/radarr/__init__.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/radarr/__init__.py b/homeassistant/components/radarr/__init__.py index b528e701c71..1023bf10659 100644 --- a/homeassistant/components/radarr/__init__.py +++ b/homeassistant/components/radarr/__init__.py @@ -70,7 +70,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: RadarrConfigEntry) -> bo status=StatusDataUpdateCoordinator(hass, host_configuration, radarr), ) for field in fields(data): - await getattr(data, field.name).async_config_entry_first_refresh() + coordinator: RadarrDataUpdateCoordinator = getattr(data, field.name) + # Movie update can take a while depending on Radarr database size + if field.name == "movie": + entry.async_create_background_task( + hass, + coordinator.async_config_entry_first_refresh(), + "radarr.movie-coordinator-first-refresh", + ) + continue + await coordinator.async_config_entry_first_refresh() entry.runtime_data = data await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) From 75e8fc0f9c7b1d207fc5bfd631d5c9b5559646d7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 17 Jun 2024 13:34:05 -0500 Subject: [PATCH 0760/1445] Fix homekit_controller haa fixture (#119855) --- .../fixtures/{haa_fan.json => haa_fan1.json} | 87 +-- .../homekit_controller/fixtures/haa_fan2.json | 79 +++ .../snapshots/test_init.ambr | 554 ++++++++++++++---- 3 files changed, 523 insertions(+), 197 deletions(-) rename tests/components/homekit_controller/fixtures/{haa_fan.json => haa_fan1.json} (61%) create mode 100644 tests/components/homekit_controller/fixtures/haa_fan2.json diff --git a/tests/components/homekit_controller/fixtures/haa_fan.json b/tests/components/homekit_controller/fixtures/haa_fan1.json similarity index 61% rename from tests/components/homekit_controller/fixtures/haa_fan.json rename to tests/components/homekit_controller/fixtures/haa_fan1.json index a144a9501ba..7389870e195 100644 --- a/tests/components/homekit_controller/fixtures/haa_fan.json +++ b/tests/components/homekit_controller/fixtures/haa_fan1.json @@ -9,7 +9,7 @@ "hidden": false, "characteristics": [ { - "aid": 2, + "aid": 1, "iid": 2, "type": "00000023-0000-1000-8000-0026BB765291", "perms": ["pr"], @@ -17,7 +17,7 @@ "value": "HAA-C718B3" }, { - "aid": 2, + "aid": 1, "iid": 3, "type": "00000020-0000-1000-8000-0026BB765291", "perms": ["pr"], @@ -33,7 +33,7 @@ "value": "C718B3-1" }, { - "aid": 2, + "aid": 1, "iid": 5, "type": "00000021-0000-1000-8000-0026BB765291", "perms": ["pr"], @@ -41,7 +41,7 @@ "value": "RavenSystem HAA" }, { - "aid": 2, + "aid": 1, "iid": 6, "type": "00000052-0000-1000-8000-0026BB765291", "perms": ["pr"], @@ -49,7 +49,7 @@ "value": "5.0.18" }, { - "aid": 2, + "aid": 1, "iid": 7, "type": "00000014-0000-1000-8000-0026BB765291", "perms": ["pw"], @@ -130,82 +130,5 @@ ] } ] - }, - { - "aid": 2, - "services": [ - { - "iid": 1, - "type": "0000003E-0000-1000-8000-0026BB765291", - "primary": false, - "hidden": false, - "characteristics": [ - { - "aid": 2, - "iid": 2, - "type": "00000023-0000-1000-8000-0026BB765291", - "perms": ["pr"], - "format": "string", - "value": "HAA-C718B3" - }, - { - "aid": 2, - "iid": 3, - "type": "00000020-0000-1000-8000-0026BB765291", - "perms": ["pr"], - "format": "string", - "value": "Jos\u00e9 A. Jim\u00e9nez Campos" - }, - { - "aid": 2, - "iid": 4, - "type": "00000030-0000-1000-8000-0026BB765291", - "perms": ["pr"], - "format": "string", - "value": "C718B3-2" - }, - { - "aid": 2, - "iid": 5, - "type": "00000021-0000-1000-8000-0026BB765291", - "perms": ["pr"], - "format": "string", - "value": "RavenSystem HAA" - }, - { - "aid": 2, - "iid": 6, - "type": "00000052-0000-1000-8000-0026BB765291", - "perms": ["pr"], - "format": "string", - "value": "5.0.18" - }, - { - "aid": 2, - "iid": 7, - "type": "00000014-0000-1000-8000-0026BB765291", - "perms": ["pw"], - "format": "bool" - } - ] - }, - { - "iid": 8, - "type": "00000049-0000-1000-8000-0026BB765291", - "primary": true, - "hidden": false, - "characteristics": [ - { - "aid": 2, - "iid": 9, - "type": "00000025-0000-1000-8000-0026BB765291", - "perms": ["pr", "pw", "ev"], - "ev": true, - "format": "bool", - "value": false - } - ] - } - ] } ] diff --git a/tests/components/homekit_controller/fixtures/haa_fan2.json b/tests/components/homekit_controller/fixtures/haa_fan2.json new file mode 100644 index 00000000000..3cf70c2a85f --- /dev/null +++ b/tests/components/homekit_controller/fixtures/haa_fan2.json @@ -0,0 +1,79 @@ +[ + { + "aid": 1, + "services": [ + { + "iid": 1, + "type": "0000003E-0000-1000-8000-0026BB765291", + "primary": false, + "hidden": false, + "characteristics": [ + { + "aid": 1, + "iid": 2, + "type": "00000023-0000-1000-8000-0026BB765291", + "perms": ["pr"], + "format": "string", + "value": "HAA-C718B3" + }, + { + "aid": 1, + "iid": 3, + "type": "00000020-0000-1000-8000-0026BB765291", + "perms": ["pr"], + "format": "string", + "value": "Jos\u00e9 A. Jim\u00e9nez Campos" + }, + { + "aid": 1, + "iid": 4, + "type": "00000030-0000-1000-8000-0026BB765291", + "perms": ["pr"], + "format": "string", + "value": "C718B3-2" + }, + { + "aid": 1, + "iid": 5, + "type": "00000021-0000-1000-8000-0026BB765291", + "perms": ["pr"], + "format": "string", + "value": "RavenSystem HAA" + }, + { + "aid": 1, + "iid": 6, + "type": "00000052-0000-1000-8000-0026BB765291", + "perms": ["pr"], + "format": "string", + "value": "5.0.18" + }, + { + "aid": 1, + "iid": 7, + "type": "00000014-0000-1000-8000-0026BB765291", + "perms": ["pw"], + "format": "bool" + } + ] + }, + { + "iid": 8, + "type": "00000049-0000-1000-8000-0026BB765291", + "primary": true, + "hidden": false, + "characteristics": [ + { + "aid": 1, + "iid": 9, + "type": "00000025-0000-1000-8000-0026BB765291", + "perms": ["pr", "pw", "ev"], + "ev": true, + "format": "bool", + "value": false + } + ] + } + ] + } +] diff --git a/tests/components/homekit_controller/snapshots/test_init.ambr b/tests/components/homekit_controller/snapshots/test_init.ambr index 34f613ac027..35a2b4937fc 100644 --- a/tests/components/homekit_controller/snapshots/test_init.ambr +++ b/tests/components/homekit_controller/snapshots/test_init.ambr @@ -6660,122 +6660,8 @@ }), ]) # --- -# name: test_snapshots[haa_fan] +# name: test_snapshots[haa_fan1] list([ - dict({ - 'device': dict({ - 'area_id': None, - 'config_entries': list([ - 'TestData', - ]), - 'configuration_url': None, - 'connections': list([ - ]), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': '', - 'identifiers': list([ - list([ - 'homekit_controller:accessory-id', - '00:00:00:00:00:00:aid:2', - ]), - ]), - 'is_new': False, - 'labels': list([ - ]), - 'manufacturer': 'José A. Jiménez Campos', - 'model': 'RavenSystem HAA', - 'name': 'HAA-C718B3', - 'name_by_user': None, - 'primary_integration': 'homekit_controller', - 'serial_number': 'C718B3-2', - 'suggested_area': None, - 'sw_version': '5.0.18', - }), - 'entities': list([ - dict({ - 'entry': dict({ - 'aliases': list([ - ]), - 'area_id': None, - 'capabilities': None, - 'categories': dict({ - }), - 'config_entry_id': 'TestData', - 'device_class': None, - 'disabled_by': None, - 'domain': 'button', - 'entity_category': , - 'entity_id': 'button.haa_c718b3_identify_2', - 'has_entity_name': False, - 'hidden_by': None, - 'icon': None, - 'labels': list([ - ]), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'HAA-C718B3 Identify', - 'platform': 'homekit_controller', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '00:00:00:00:00:00_2_1_7', - 'unit_of_measurement': None, - }), - 'state': dict({ - 'attributes': dict({ - 'device_class': 'identify', - 'friendly_name': 'HAA-C718B3 Identify', - }), - 'entity_id': 'button.haa_c718b3_identify_2', - 'state': 'unknown', - }), - }), - dict({ - 'entry': dict({ - 'aliases': list([ - ]), - 'area_id': None, - 'capabilities': None, - 'categories': dict({ - }), - 'config_entry_id': 'TestData', - 'device_class': None, - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': None, - 'entity_id': 'switch.haa_c718b3', - 'has_entity_name': False, - 'hidden_by': None, - 'icon': None, - 'labels': list([ - ]), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'HAA-C718B3', - 'platform': 'homekit_controller', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '00:00:00:00:00:00_2_8', - 'unit_of_measurement': None, - }), - 'state': dict({ - 'attributes': dict({ - 'friendly_name': 'HAA-C718B3', - }), - 'entity_id': 'switch.haa_c718b3', - 'state': 'off', - }), - }), - ]), - }), dict({ 'device': dict({ 'area_id': None, @@ -6980,6 +6866,444 @@ }), ]) # --- +# name: test_snapshots[haa_fan2] + list([ + dict({ + 'device': dict({ + 'area_id': None, + 'config_entries': list([ + 'TestData', + ]), + 'configuration_url': None, + 'connections': list([ + ]), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': '', + 'identifiers': list([ + list([ + 'homekit_controller:accessory-id', + '00:00:00:00:00:00:aid:1', + ]), + ]), + 'is_new': False, + 'labels': list([ + ]), + 'manufacturer': 'José A. Jiménez Campos', + 'model': 'RavenSystem HAA', + 'name': 'HAA-C718B3', + 'name_by_user': None, + 'primary_integration': 'homekit_controller', + 'serial_number': 'C718B3-2', + 'suggested_area': None, + 'sw_version': '5.0.18', + }), + 'entities': list([ + dict({ + 'entry': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': 'TestData', + 'device_class': None, + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.haa_c718b3_identify', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'HAA-C718B3 Identify', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00_1_1_7', + 'unit_of_measurement': None, + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'identify', + 'friendly_name': 'HAA-C718B3 Identify', + }), + 'entity_id': 'button.haa_c718b3_identify', + 'state': 'unknown', + }), + }), + dict({ + 'entry': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': 'TestData', + 'device_class': None, + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.haa_c718b3', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'HAA-C718B3', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00_1_8', + 'unit_of_measurement': None, + }), + 'state': dict({ + 'attributes': dict({ + 'friendly_name': 'HAA-C718B3', + }), + 'entity_id': 'switch.haa_c718b3', + 'state': 'off', + }), + }), + ]), + }), + ]) +# --- +# name: test_snapshots[haa_fan] + list([ + dict({ + 'device': dict({ + 'area_id': None, + 'config_entries': list([ + 'TestData', + ]), + 'configuration_url': None, + 'connections': list([ + ]), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': '', + 'identifiers': list([ + list([ + 'homekit_controller:accessory-id', + '00:00:00:00:00:00:aid:1', + ]), + ]), + 'is_new': False, + 'labels': list([ + ]), + 'manufacturer': 'José A. Jiménez Campos', + 'model': 'RavenSystem HAA', + 'name': 'HAA-C718B3', + 'name_by_user': None, + 'primary_integration': 'homekit_controller', + 'serial_number': 'C718B3-1', + 'suggested_area': None, + 'sw_version': '5.0.18', + }), + 'entities': list([ + dict({ + 'entry': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': 'TestData', + 'device_class': None, + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.haa_c718b3_identify', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'HAA-C718B3 Identify', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00_1_1_7', + 'unit_of_measurement': None, + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'identify', + 'friendly_name': 'HAA-C718B3 Identify', + }), + 'entity_id': 'button.haa_c718b3_identify', + 'state': 'unknown', + }), + }), + dict({ + 'entry': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': 'TestData', + 'device_class': None, + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.haa_c718b3_setup', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'HAA-C718B3 Setup', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'setup', + 'unique_id': '00:00:00:00:00:00_1_1010_1012', + 'unit_of_measurement': None, + }), + 'state': dict({ + 'attributes': dict({ + 'friendly_name': 'HAA-C718B3 Setup', + }), + 'entity_id': 'button.haa_c718b3_setup', + 'state': 'unknown', + }), + }), + dict({ + 'entry': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': 'TestData', + 'device_class': None, + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.haa_c718b3_update', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'HAA-C718B3 Update', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00_1_1010_1011', + 'unit_of_measurement': None, + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'update', + 'friendly_name': 'HAA-C718B3 Update', + }), + 'entity_id': 'button.haa_c718b3_update', + 'state': 'unknown', + }), + }), + dict({ + 'entry': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'preset_modes': None, + }), + 'categories': dict({ + }), + 'config_entry_id': 'TestData', + 'device_class': None, + 'disabled_by': None, + 'domain': 'fan', + 'entity_category': None, + 'entity_id': 'fan.haa_c718b3', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'HAA-C718B3', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00_1_8', + 'unit_of_measurement': None, + }), + 'state': dict({ + 'attributes': dict({ + 'friendly_name': 'HAA-C718B3', + 'percentage': 66, + 'percentage_step': 33.333333333333336, + 'preset_mode': None, + 'preset_modes': None, + 'supported_features': , + }), + 'entity_id': 'fan.haa_c718b3', + 'state': 'on', + }), + }), + ]), + }), + dict({ + 'device': dict({ + 'area_id': None, + 'config_entries': list([ + 'TestData', + ]), + 'configuration_url': None, + 'connections': list([ + ]), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': '', + 'identifiers': list([ + list([ + 'homekit_controller:accessory-id', + '00:00:00:00:00:00:aid:2', + ]), + ]), + 'is_new': False, + 'labels': list([ + ]), + 'manufacturer': 'José A. Jiménez Campos', + 'model': 'RavenSystem HAA', + 'name': 'HAA-C718B3', + 'name_by_user': None, + 'primary_integration': 'homekit_controller', + 'serial_number': 'C718B3-2', + 'suggested_area': None, + 'sw_version': '5.0.18', + }), + 'entities': list([ + dict({ + 'entry': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': 'TestData', + 'device_class': None, + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.haa_c718b3_identify_2', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'HAA-C718B3 Identify', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00_2_1_7', + 'unit_of_measurement': None, + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'identify', + 'friendly_name': 'HAA-C718B3 Identify', + }), + 'entity_id': 'button.haa_c718b3_identify_2', + 'state': 'unknown', + }), + }), + dict({ + 'entry': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': 'TestData', + 'device_class': None, + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.haa_c718b3', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'HAA-C718B3', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00_2_8', + 'unit_of_measurement': None, + }), + 'state': dict({ + 'attributes': dict({ + 'friendly_name': 'HAA-C718B3', + }), + 'entity_id': 'switch.haa_c718b3', + 'state': 'off', + }), + }), + ]), + }), + ]) +# --- # name: test_snapshots[home_assistant_bridge_basic_cover] list([ dict({ From b6b62487134c375275d739fdaef3423447b6c731 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Mon, 17 Jun 2024 21:13:28 +0200 Subject: [PATCH 0761/1445] Remove legacy get forecast service from Weather (#118664) * Remove legacy get forecast service from Weather * Fix tests * Fix test --------- Co-authored-by: Joost Lekkerkerker --- homeassistant/components/weather/__init__.py | 40 ------------- tests/components/accuweather/test_weather.py | 6 +- tests/components/aemet/test_weather.py | 6 +- tests/components/ipma/test_weather.py | 6 +- tests/components/met_eireann/test_weather.py | 6 +- tests/components/metoffice/test_weather.py | 6 +- tests/components/smhi/test_weather.py | 6 +- tests/components/template/test_weather.py | 19 +----- tests/components/tomorrowio/test_weather.py | 37 +----------- tests/components/weather/test_init.py | 63 +------------------- tests/components/weatherkit/test_weather.py | 11 +--- 11 files changed, 13 insertions(+), 193 deletions(-) diff --git a/homeassistant/components/weather/__init__.py b/homeassistant/components/weather/__init__.py index d7a17ff61e6..b73cbd97654 100644 --- a/homeassistant/components/weather/__init__.py +++ b/homeassistant/components/weather/__init__.py @@ -37,7 +37,6 @@ from homeassistant.helpers.config_validation import ( # noqa: F401 ) from homeassistant.helpers.entity import ABCCachedProperties, Entity, EntityDescription from homeassistant.helpers.entity_component import EntityComponent -import homeassistant.helpers.issue_registry as ir from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, @@ -123,8 +122,6 @@ SCAN_INTERVAL = timedelta(seconds=30) ROUNDING_PRECISION = 2 -LEGACY_SERVICE_GET_FORECAST: Final = "get_forecast" -"""Deprecated: please use SERVICE_GET_FORECASTS.""" SERVICE_GET_FORECASTS: Final = "get_forecasts" _ObservationUpdateCoordinatorT = TypeVar( @@ -204,17 +201,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: component = hass.data[DOMAIN] = EntityComponent[WeatherEntity]( _LOGGER, DOMAIN, hass, SCAN_INTERVAL ) - component.async_register_legacy_entity_service( - LEGACY_SERVICE_GET_FORECAST, - {vol.Required("type"): vol.In(("daily", "hourly", "twice_daily"))}, - async_get_forecast_service, - required_features=[ - WeatherEntityFeature.FORECAST_DAILY, - WeatherEntityFeature.FORECAST_HOURLY, - WeatherEntityFeature.FORECAST_TWICE_DAILY, - ], - supports_response=SupportsResponse.ONLY, - ) component.async_register_entity_service( SERVICE_GET_FORECASTS, {vol.Required("type"): vol.In(("daily", "hourly", "twice_daily"))}, @@ -1012,32 +998,6 @@ def raise_unsupported_forecast(entity_id: str, forecast_type: str) -> None: ) -async def async_get_forecast_service( - weather: WeatherEntity, service_call: ServiceCall -) -> ServiceResponse: - """Get weather forecast. - - Deprecated: please use async_get_forecasts_service. - """ - _LOGGER.warning( - "Detected use of service 'weather.get_forecast'. " - "This is deprecated and will stop working in Home Assistant 2024.6. " - "Use 'weather.get_forecasts' instead which supports multiple entities", - ) - ir.async_create_issue( - weather.hass, - DOMAIN, - "deprecated_service_weather_get_forecast", - breaks_in_ha_version="2024.6.0", - is_fixable=True, - is_persistent=False, - issue_domain=weather.platform.platform_name, - severity=ir.IssueSeverity.WARNING, - translation_key="deprecated_service_weather_get_forecast", - ) - return await async_get_forecasts_service(weather, service_call) - - async def async_get_forecasts_service( weather: WeatherEntity, service_call: ServiceCall ) -> ServiceResponse: diff --git a/tests/components/accuweather/test_weather.py b/tests/components/accuweather/test_weather.py index 1a6201c20a2..a23b09fec29 100644 --- a/tests/components/accuweather/test_weather.py +++ b/tests/components/accuweather/test_weather.py @@ -11,7 +11,6 @@ from homeassistant.components.accuweather.const import UPDATE_INTERVAL_DAILY_FOR from homeassistant.components.weather import ( ATTR_FORECAST_CONDITION, DOMAIN as WEATHER_DOMAIN, - LEGACY_SERVICE_GET_FORECAST, SERVICE_GET_FORECASTS, ) from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE, Platform @@ -109,10 +108,7 @@ async def test_unsupported_condition_icon_data( @pytest.mark.parametrize( ("service"), - [ - SERVICE_GET_FORECASTS, - LEGACY_SERVICE_GET_FORECAST, - ], + [SERVICE_GET_FORECASTS], ) async def test_forecast_service( hass: HomeAssistant, diff --git a/tests/components/aemet/test_weather.py b/tests/components/aemet/test_weather.py index d2f21fbec83..049fd6d18c7 100644 --- a/tests/components/aemet/test_weather.py +++ b/tests/components/aemet/test_weather.py @@ -18,7 +18,6 @@ from homeassistant.components.weather import ( ATTR_WEATHER_WIND_GUST_SPEED, ATTR_WEATHER_WIND_SPEED, DOMAIN as WEATHER_DOMAIN, - LEGACY_SERVICE_GET_FORECAST, SERVICE_GET_FORECASTS, ) from homeassistant.const import ATTR_ATTRIBUTION @@ -56,10 +55,7 @@ async def test_aemet_weather( @pytest.mark.parametrize( ("service"), - [ - SERVICE_GET_FORECASTS, - LEGACY_SERVICE_GET_FORECAST, - ], + [SERVICE_GET_FORECASTS], ) async def test_forecast_service( hass: HomeAssistant, diff --git a/tests/components/ipma/test_weather.py b/tests/components/ipma/test_weather.py index 7150286e4f9..b7ef1347ca5 100644 --- a/tests/components/ipma/test_weather.py +++ b/tests/components/ipma/test_weather.py @@ -15,7 +15,6 @@ from homeassistant.components.weather import ( ATTR_WEATHER_WIND_BEARING, ATTR_WEATHER_WIND_SPEED, DOMAIN as WEATHER_DOMAIN, - LEGACY_SERVICE_GET_FORECAST, SERVICE_GET_FORECASTS, ) from homeassistant.const import STATE_UNKNOWN @@ -101,10 +100,7 @@ async def test_failed_get_observation_forecast(hass: HomeAssistant) -> None: @pytest.mark.parametrize( ("service"), - [ - SERVICE_GET_FORECASTS, - LEGACY_SERVICE_GET_FORECAST, - ], + [SERVICE_GET_FORECASTS], ) async def test_forecast_service( hass: HomeAssistant, diff --git a/tests/components/met_eireann/test_weather.py b/tests/components/met_eireann/test_weather.py index a660c18f7b3..1e385c9a600 100644 --- a/tests/components/met_eireann/test_weather.py +++ b/tests/components/met_eireann/test_weather.py @@ -10,7 +10,6 @@ from homeassistant.components.met_eireann import UPDATE_INTERVAL from homeassistant.components.met_eireann.const import DOMAIN from homeassistant.components.weather import ( DOMAIN as WEATHER_DOMAIN, - LEGACY_SERVICE_GET_FORECAST, SERVICE_GET_FORECASTS, ) from homeassistant.config_entries import ConfigEntry @@ -65,10 +64,7 @@ async def test_weather(hass: HomeAssistant, mock_weather) -> None: @pytest.mark.parametrize( ("service"), - [ - SERVICE_GET_FORECASTS, - LEGACY_SERVICE_GET_FORECAST, - ], + [SERVICE_GET_FORECASTS], ) async def test_forecast_service( hass: HomeAssistant, diff --git a/tests/components/metoffice/test_weather.py b/tests/components/metoffice/test_weather.py index c931222d1d6..5176aff9e7d 100644 --- a/tests/components/metoffice/test_weather.py +++ b/tests/components/metoffice/test_weather.py @@ -14,7 +14,6 @@ from syrupy.assertion import SnapshotAssertion from homeassistant.components.metoffice.const import DEFAULT_SCAN_INTERVAL, DOMAIN from homeassistant.components.weather import ( DOMAIN as WEATHER_DOMAIN, - LEGACY_SERVICE_GET_FORECAST, SERVICE_GET_FORECASTS, ) from homeassistant.const import STATE_UNAVAILABLE @@ -254,10 +253,7 @@ async def test_new_config_entry( @pytest.mark.freeze_time(datetime.datetime(2020, 4, 25, 12, tzinfo=datetime.UTC)) @pytest.mark.parametrize( ("service"), - [ - SERVICE_GET_FORECASTS, - LEGACY_SERVICE_GET_FORECAST, - ], + [SERVICE_GET_FORECASTS], ) async def test_forecast_service( hass: HomeAssistant, diff --git a/tests/components/smhi/test_weather.py b/tests/components/smhi/test_weather.py index 6c15ec53236..1870d7b498a 100644 --- a/tests/components/smhi/test_weather.py +++ b/tests/components/smhi/test_weather.py @@ -23,7 +23,6 @@ from homeassistant.components.weather import ( ATTR_WEATHER_WIND_SPEED, ATTR_WEATHER_WIND_SPEED_UNIT, DOMAIN as WEATHER_DOMAIN, - LEGACY_SERVICE_GET_FORECAST, SERVICE_GET_FORECASTS, ) from homeassistant.const import ATTR_ATTRIBUTION, STATE_UNKNOWN, UnitOfSpeed @@ -489,10 +488,7 @@ async def test_forecast_services_lack_of_data( @pytest.mark.parametrize( ("service"), - [ - SERVICE_GET_FORECASTS, - LEGACY_SERVICE_GET_FORECAST, - ], + [SERVICE_GET_FORECASTS], ) async def test_forecast_service( hass: HomeAssistant, diff --git a/tests/components/template/test_weather.py b/tests/components/template/test_weather.py index b365d5d2890..fd7694cfbed 100644 --- a/tests/components/template/test_weather.py +++ b/tests/components/template/test_weather.py @@ -18,7 +18,6 @@ from homeassistant.components.weather import ( ATTR_WEATHER_WIND_GUST_SPEED, ATTR_WEATHER_WIND_SPEED, DOMAIN as WEATHER_DOMAIN, - LEGACY_SERVICE_GET_FORECAST, SERVICE_GET_FORECASTS, Forecast, ) @@ -96,10 +95,7 @@ async def test_template_state_text(hass: HomeAssistant, start_ha) -> None: @pytest.mark.parametrize( ("service"), - [ - SERVICE_GET_FORECASTS, - LEGACY_SERVICE_GET_FORECAST, - ], + [SERVICE_GET_FORECASTS], ) @pytest.mark.parametrize(("count", "domain"), [(1, WEATHER_DOMAIN)]) @pytest.mark.parametrize( @@ -224,7 +220,6 @@ async def test_forecasts( ("service", "expected"), [ (SERVICE_GET_FORECASTS, {"weather.forecast": {"forecast": []}}), - (LEGACY_SERVICE_GET_FORECAST, {"forecast": []}), ], ) @pytest.mark.parametrize(("count", "domain"), [(1, WEATHER_DOMAIN)]) @@ -308,7 +303,6 @@ async def test_forecast_invalid( ("service", "expected"), [ (SERVICE_GET_FORECASTS, {"weather.forecast": {"forecast": []}}), - (LEGACY_SERVICE_GET_FORECAST, {"forecast": []}), ], ) @pytest.mark.parametrize(("count", "domain"), [(1, WEATHER_DOMAIN)]) @@ -377,7 +371,6 @@ async def test_forecast_invalid_is_daytime_missing_in_twice_daily( ("service", "expected"), [ (SERVICE_GET_FORECASTS, {"weather.forecast": {"forecast": []}}), - (LEGACY_SERVICE_GET_FORECAST, {"forecast": []}), ], ) @pytest.mark.parametrize(("count", "domain"), [(1, WEATHER_DOMAIN)]) @@ -444,10 +437,7 @@ async def test_forecast_invalid_datetime_missing( @pytest.mark.parametrize( ("service"), - [ - SERVICE_GET_FORECASTS, - LEGACY_SERVICE_GET_FORECAST, - ], + [SERVICE_GET_FORECASTS], ) @pytest.mark.parametrize(("count", "domain"), [(1, WEATHER_DOMAIN)]) @pytest.mark.parametrize( @@ -679,10 +669,7 @@ async def test_trigger_action( @pytest.mark.parametrize( ("service"), - [ - SERVICE_GET_FORECASTS, - LEGACY_SERVICE_GET_FORECAST, - ], + [SERVICE_GET_FORECASTS], ) @pytest.mark.parametrize(("count", "domain"), [(1, "template")]) @pytest.mark.parametrize( diff --git a/tests/components/tomorrowio/test_weather.py b/tests/components/tomorrowio/test_weather.py index 09f871896d3..4443c654929 100644 --- a/tests/components/tomorrowio/test_weather.py +++ b/tests/components/tomorrowio/test_weather.py @@ -36,7 +36,6 @@ from homeassistant.components.weather import ( ATTR_WEATHER_WIND_SPEED, ATTR_WEATHER_WIND_SPEED_UNIT, DOMAIN as WEATHER_DOMAIN, - LEGACY_SERVICE_GET_FORECAST, SERVICE_GET_FORECASTS, ) from homeassistant.config_entries import RELOAD_AFTER_UPDATE_DELAY, SOURCE_USER @@ -243,10 +242,7 @@ async def test_v4_weather_legacy_entities(hass: HomeAssistant) -> None: @pytest.mark.parametrize( ("service"), - [ - SERVICE_GET_FORECASTS, - LEGACY_SERVICE_GET_FORECAST, - ], + [SERVICE_GET_FORECASTS], ) @freeze_time(datetime(2021, 3, 6, 23, 59, 59, tzinfo=dt_util.UTC)) async def test_v4_forecast_service( @@ -272,37 +268,6 @@ async def test_v4_forecast_service( assert response == snapshot -async def test_legacy_v4_bad_forecast( - hass: HomeAssistant, - freezer: FrozenDateTimeFactory, - tomorrowio_config_entry_update, - snapshot: SnapshotAssertion, -) -> None: - """Test bad forecast data.""" - freezer.move_to(datetime(2021, 3, 6, 23, 59, 59, tzinfo=dt_util.UTC)) - - weather_state = await _setup(hass, API_V4_ENTRY_DATA) - entity_id = weather_state.entity_id - hourly_forecast = tomorrowio_config_entry_update.return_value["forecasts"]["hourly"] - hourly_forecast[0]["values"]["precipitationProbability"] = "blah" - - # Trigger data refetch - freezer.tick(timedelta(minutes=32) + timedelta(seconds=1)) - await hass.async_block_till_done() - - response = await hass.services.async_call( - WEATHER_DOMAIN, - LEGACY_SERVICE_GET_FORECAST, - { - "entity_id": entity_id, - "type": "hourly", - }, - blocking=True, - return_response=True, - ) - assert response["forecast"][0]["precipitation_probability"] is None - - async def test_v4_bad_forecast( hass: HomeAssistant, freezer: FrozenDateTimeFactory, diff --git a/tests/components/weather/test_init.py b/tests/components/weather/test_init.py index 3343ccd4d9f..78f454b4f95 100644 --- a/tests/components/weather/test_init.py +++ b/tests/components/weather/test_init.py @@ -22,7 +22,6 @@ from homeassistant.components.weather import ( ATTR_WEATHER_WIND_SPEED, ATTR_WEATHER_WIND_SPEED_UNIT, DOMAIN, - LEGACY_SERVICE_GET_FORECAST, ROUNDING_PRECISION, SERVICE_GET_FORECASTS, Forecast, @@ -47,7 +46,6 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er -import homeassistant.helpers.issue_registry as ir from homeassistant.util import dt as dt_util from homeassistant.util.unit_conversion import ( DistanceConverter, @@ -608,7 +606,6 @@ async def test_forecast_twice_daily_missing_is_daytime( ("service"), [ SERVICE_GET_FORECASTS, - LEGACY_SERVICE_GET_FORECAST, ], ) @pytest.mark.parametrize( @@ -681,12 +678,6 @@ async def test_get_forecast( } }, ), - ( - LEGACY_SERVICE_GET_FORECAST, - { - "forecast": [], - }, - ), ], ) async def test_get_forecast_no_forecast( @@ -727,10 +718,7 @@ async def test_get_forecast_no_forecast( @pytest.mark.parametrize( ("service"), - [ - SERVICE_GET_FORECASTS, - LEGACY_SERVICE_GET_FORECAST, - ], + [SERVICE_GET_FORECASTS], ) @pytest.mark.parametrize( ("supported_features", "forecast_types"), @@ -786,52 +774,3 @@ async def test_get_forecast_unsupported( ISSUE_TRACKER = "https://blablabla.com" - - -async def test_issue_deprecated_service_weather_get_forecast( - hass: HomeAssistant, - issue_registry: ir.IssueRegistry, - config_flow_fixture: None, - caplog: pytest.LogCaptureFixture, -) -> None: - """Test the issue is raised on deprecated service weather.get_forecast.""" - - class MockWeatherMock(MockWeatherTest): - """Mock weather class.""" - - async def async_forecast_daily(self) -> list[Forecast] | None: - """Return the forecast_daily.""" - return self.forecast_list - - kwargs = { - "native_temperature": 38, - "native_temperature_unit": UnitOfTemperature.CELSIUS, - "supported_features": WeatherEntityFeature.FORECAST_DAILY, - } - - entity0 = await create_entity(hass, MockWeatherMock, None, **kwargs) - - _ = await hass.services.async_call( - DOMAIN, - LEGACY_SERVICE_GET_FORECAST, - { - "entity_id": entity0.entity_id, - "type": "daily", - }, - blocking=True, - return_response=True, - ) - - issue = issue_registry.async_get_issue( - "weather", "deprecated_service_weather_get_forecast" - ) - assert issue - assert issue.issue_domain == "test" - assert issue.issue_id == "deprecated_service_weather_get_forecast" - assert issue.translation_key == "deprecated_service_weather_get_forecast" - - assert ( - "Detected use of service 'weather.get_forecast'. " - "This is deprecated and will stop working in Home Assistant 2024.6. " - "Use 'weather.get_forecasts' instead which supports multiple entities" - ) in caplog.text diff --git a/tests/components/weatherkit/test_weather.py b/tests/components/weatherkit/test_weather.py index be949efffb8..ba20276c22e 100644 --- a/tests/components/weatherkit/test_weather.py +++ b/tests/components/weatherkit/test_weather.py @@ -16,7 +16,6 @@ from homeassistant.components.weather import ( ATTR_WEATHER_WIND_GUST_SPEED, ATTR_WEATHER_WIND_SPEED, DOMAIN as WEATHER_DOMAIN, - LEGACY_SERVICE_GET_FORECAST, SERVICE_GET_FORECASTS, WeatherEntityFeature, ) @@ -81,10 +80,7 @@ async def test_hourly_forecast_missing(hass: HomeAssistant) -> None: @pytest.mark.parametrize( ("service"), - [ - SERVICE_GET_FORECASTS, - LEGACY_SERVICE_GET_FORECAST, - ], + [SERVICE_GET_FORECASTS], ) async def test_hourly_forecast( hass: HomeAssistant, snapshot: SnapshotAssertion, service: str @@ -107,10 +103,7 @@ async def test_hourly_forecast( @pytest.mark.parametrize( ("service"), - [ - SERVICE_GET_FORECASTS, - LEGACY_SERVICE_GET_FORECAST, - ], + [SERVICE_GET_FORECASTS], ) async def test_daily_forecast( hass: HomeAssistant, snapshot: SnapshotAssertion, service: str From f5dfefb3a6213f712b70c388f02ba5a565840a81 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Mon, 17 Jun 2024 22:17:05 +0200 Subject: [PATCH 0762/1445] Use the humidity value in Shelly Wall Display climate entity (#119830) * Use the humidity value with the climate entity if available * Update tests * Use walrus --------- Co-authored-by: Maciej Bieniek <478555+bieniu@users.noreply.github.com> --- homeassistant/components/shelly/climate.py | 12 ++++++++++ tests/components/shelly/conftest.py | 1 + tests/components/shelly/test_climate.py | 28 ++++++++++++++++++++++ 3 files changed, 41 insertions(+) diff --git a/homeassistant/components/shelly/climate.py b/homeassistant/components/shelly/climate.py index a4dc71f870c..ab1e58583d9 100644 --- a/homeassistant/components/shelly/climate.py +++ b/homeassistant/components/shelly/climate.py @@ -468,6 +468,10 @@ class RpcClimate(ShellyRpcEntity, ClimateEntity): self._attr_hvac_modes = [HVACMode.OFF, HVACMode.COOL] else: self._attr_hvac_modes = [HVACMode.OFF, HVACMode.HEAT] + self._humidity_key: str | None = None + # Check if there is a corresponding humidity key for the thermostat ID + if (humidity_key := f"humidity:{id_}") in self.coordinator.device.status: + self._humidity_key = humidity_key @property def target_temperature(self) -> float | None: @@ -479,6 +483,14 @@ class RpcClimate(ShellyRpcEntity, ClimateEntity): """Return current temperature.""" return cast(float, self.status["current_C"]) + @property + def current_humidity(self) -> float | None: + """Return current humidity.""" + if self._humidity_key is None: + return None + + return cast(float, self.coordinator.device.status[self._humidity_key]["rh"]) + @property def hvac_mode(self) -> HVACMode: """HVAC current mode.""" diff --git a/tests/components/shelly/conftest.py b/tests/components/shelly/conftest.py index 6099a16d52e..8e41cbe060f 100644 --- a/tests/components/shelly/conftest.py +++ b/tests/components/shelly/conftest.py @@ -254,6 +254,7 @@ MOCK_STATUS_RPC = { "current_C": 12.3, "output": True, }, + "humidity:0": {"rh": 44.4}, "sys": { "available_updates": { "beta": {"version": "some_beta_version"}, diff --git a/tests/components/shelly/test_climate.py b/tests/components/shelly/test_climate.py index ed4ceea0306..fea46b1d2d1 100644 --- a/tests/components/shelly/test_climate.py +++ b/tests/components/shelly/test_climate.py @@ -8,6 +8,7 @@ from aioshelly.exceptions import DeviceConnectionError, InvalidAuthError import pytest from homeassistant.components.climate import ( + ATTR_CURRENT_HUMIDITY, ATTR_CURRENT_TEMPERATURE, ATTR_HVAC_ACTION, ATTR_HVAC_MODE, @@ -610,6 +611,7 @@ async def test_rpc_climate_hvac_mode( assert state.attributes[ATTR_TEMPERATURE] == 23 assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 12.3 assert state.attributes[ATTR_HVAC_ACTION] == HVACAction.HEATING + assert state.attributes[ATTR_CURRENT_HUMIDITY] == 44.4 entry = entity_registry.async_get(ENTITY_ID) assert entry @@ -620,6 +622,7 @@ async def test_rpc_climate_hvac_mode( state = hass.states.get(ENTITY_ID) assert state.attributes[ATTR_HVAC_ACTION] == HVACAction.IDLE + assert state.attributes[ATTR_CURRENT_HUMIDITY] == 44.4 monkeypatch.setitem(mock_rpc_device.status["thermostat:0"], "enable", False) await hass.services.async_call( @@ -637,6 +640,31 @@ async def test_rpc_climate_hvac_mode( assert state.state == HVACMode.OFF +async def test_rpc_climate_without_humidity( + hass: HomeAssistant, + entity_registry: EntityRegistry, + mock_rpc_device: Mock, + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Test climate entity without the humidity value.""" + new_status = deepcopy(mock_rpc_device.status) + new_status.pop("humidity:0") + monkeypatch.setattr(mock_rpc_device, "status", new_status) + + await init_integration(hass, 2, model=MODEL_WALL_DISPLAY) + + state = hass.states.get(ENTITY_ID) + assert state.state == HVACMode.HEAT + assert state.attributes[ATTR_TEMPERATURE] == 23 + assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 12.3 + assert state.attributes[ATTR_HVAC_ACTION] == HVACAction.HEATING + assert ATTR_CURRENT_HUMIDITY not in state.attributes + + entry = entity_registry.async_get(ENTITY_ID) + assert entry + assert entry.unique_id == "123456789ABC-thermostat:0" + + async def test_rpc_climate_set_temperature( hass: HomeAssistant, mock_rpc_device: Mock, monkeypatch: pytest.MonkeyPatch ) -> None: From 7410db08fb5953dd8e44a7b61c4eb0be71912ab8 Mon Sep 17 00:00:00 2001 From: Christoph Date: Mon, 17 Jun 2024 23:57:47 +0200 Subject: [PATCH 0763/1445] Bump xiaomi_ble to 0.30.0 (#119859) * bump xiaome_ble to 0.30.0 bump xiaomi_ble to 0.30.0 * bump xiaome_ble to 0.30.0 bump xiaomi_ble to 0.30.0 * bump xiaome_ble to 0.30.0 bump xiaomi_ble to 0.30.0 --- homeassistant/components/xiaomi_ble/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/xiaomi_ble/manifest.json b/homeassistant/components/xiaomi_ble/manifest.json index 2a1d253b603..1e0a09015ee 100644 --- a/homeassistant/components/xiaomi_ble/manifest.json +++ b/homeassistant/components/xiaomi_ble/manifest.json @@ -24,5 +24,5 @@ "dependencies": ["bluetooth_adapters"], "documentation": "https://www.home-assistant.io/integrations/xiaomi_ble", "iot_class": "local_push", - "requirements": ["xiaomi-ble==0.29.0"] + "requirements": ["xiaomi-ble==0.30.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 82c50ce7c70..d440cd3f011 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2912,7 +2912,7 @@ wyoming==1.5.4 xbox-webapi==2.0.11 # homeassistant.components.xiaomi_ble -xiaomi-ble==0.29.0 +xiaomi-ble==0.30.0 # homeassistant.components.knx xknx==2.12.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 514b26ecbe9..c61802c778d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2271,7 +2271,7 @@ wyoming==1.5.4 xbox-webapi==2.0.11 # homeassistant.components.xiaomi_ble -xiaomi-ble==0.29.0 +xiaomi-ble==0.30.0 # homeassistant.components.knx xknx==2.12.2 From a876a55d2f4f395d53f710f1e77976197154b653 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 17 Jun 2024 17:08:43 -0500 Subject: [PATCH 0764/1445] Bump uiprotect to 0.19.0 (#119863) --- homeassistant/components/unifiprotect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index 527fa4ef0e6..9cb62e666dc 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -40,7 +40,7 @@ "integration_type": "hub", "iot_class": "local_push", "loggers": ["uiprotect", "unifi_discovery"], - "requirements": ["uiprotect==1.18.1", "unifi-discovery==1.1.8"], + "requirements": ["uiprotect==1.19.0", "unifi-discovery==1.1.8"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/requirements_all.txt b/requirements_all.txt index d440cd3f011..d4c2c1a628a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2791,7 +2791,7 @@ twitchAPI==4.0.0 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==1.18.1 +uiprotect==1.19.0 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c61802c778d..61a1da9c4fd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2168,7 +2168,7 @@ twitchAPI==4.0.0 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==1.18.1 +uiprotect==1.19.0 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 From ac518516644914f81e4786fafd198a3e9d3b3499 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Mon, 17 Jun 2024 20:53:46 -0400 Subject: [PATCH 0765/1445] Handle general update failure in Sense (#119739) --- homeassistant/components/sense/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sense/__init__.py b/homeassistant/components/sense/__init__.py index 9d909730f5a..88af9fa990b 100644 --- a/homeassistant/components/sense/__init__.py +++ b/homeassistant/components/sense/__init__.py @@ -21,7 +21,7 @@ from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.event import async_track_time_interval -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import ( ACTIVE_UPDATE_RATE, @@ -109,6 +109,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: except (SenseAuthenticationException, SenseMFARequiredException) as err: _LOGGER.warning("Sense authentication expired") raise ConfigEntryAuthFailed(err) from err + except SENSE_CONNECT_EXCEPTIONS as err: + raise UpdateFailed(err) from err trends_coordinator: DataUpdateCoordinator[None] = DataUpdateCoordinator( hass, From faf2a447a42b91e005d9a91416fabc25e2757701 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Mon, 17 Jun 2024 21:43:45 -0400 Subject: [PATCH 0766/1445] Store runtime data inside the config entry in Sense (#119740) --- homeassistant/components/sense/__init__.py | 42 ++++++++++--------- .../components/sense/binary_sensor.py | 25 ++++------- homeassistant/components/sense/const.py | 4 -- homeassistant/components/sense/sensor.py | 21 ++++------ 4 files changed, 37 insertions(+), 55 deletions(-) diff --git a/homeassistant/components/sense/__init__.py b/homeassistant/components/sense/__init__.py index 88af9fa990b..28408c0cb7d 100644 --- a/homeassistant/components/sense/__init__.py +++ b/homeassistant/components/sense/__init__.py @@ -1,7 +1,9 @@ """Support for monitoring a Sense energy sensor.""" +from dataclasses import dataclass from datetime import timedelta import logging +from typing import Any from sense_energy import ( ASyncSenseable, @@ -25,20 +27,16 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda from .const import ( ACTIVE_UPDATE_RATE, - DOMAIN, SENSE_CONNECT_EXCEPTIONS, - SENSE_DATA, SENSE_DEVICE_UPDATE, - SENSE_DEVICES_DATA, - SENSE_DISCOVERED_DEVICES_DATA, SENSE_TIMEOUT_EXCEPTIONS, - SENSE_TRENDS_COORDINATOR, SENSE_WEBSOCKET_EXCEPTIONS, ) _LOGGER = logging.getLogger(__name__) PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR] +type SenseConfigEntry = ConfigEntry[SenseData] class SenseDevicesData: @@ -57,7 +55,17 @@ class SenseDevicesData: return self._data_by_device.get(sense_device_id) -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +@dataclass(kw_only=True, slots=True) +class SenseData: + """Sense data type.""" + + data: ASyncSenseable + device_data: SenseDevicesData + trends: DataUpdateCoordinator[None] + discovered: list[dict[str, Any]] + + +async def async_setup_entry(hass: HomeAssistant, entry: SenseConfigEntry) -> bool: """Set up Sense from a config entry.""" entry_data = entry.data @@ -91,7 +99,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: except SENSE_CONNECT_EXCEPTIONS as err: raise ConfigEntryNotReady(str(err)) from err - sense_devices_data = SenseDevicesData() try: sense_discovered_devices = await gateway.get_discovered_device_data() await gateway.update_realtime() @@ -132,12 +139,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: "sense.trends-coordinator-refresh", ) - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = { - SENSE_DATA: gateway, - SENSE_DEVICES_DATA: sense_devices_data, - SENSE_TRENDS_COORDINATOR: trends_coordinator, - SENSE_DISCOVERED_DEVICES_DATA: sense_discovered_devices, - } + entry.runtime_data = SenseData( + data=gateway, + device_data=SenseDevicesData(), + trends=trends_coordinator, + discovered=sense_discovered_devices, + ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) @@ -152,7 +159,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: data = gateway.get_realtime() if "devices" in data: - sense_devices_data.set_devices_data(data["devices"]) + entry.runtime_data.device_data.set_devices_data(data["devices"]) async_dispatcher_send(hass, f"{SENSE_DEVICE_UPDATE}-{gateway.sense_monitor_id}") remove_update_callback = async_track_time_interval( @@ -173,9 +180,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: SenseConfigEntry) -> bool: """Unload a config entry.""" - unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - if unload_ok: - hass.data[DOMAIN].pop(entry.entry_id) - return unload_ok + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/sense/binary_sensor.py b/homeassistant/components/sense/binary_sensor.py index 7dde4c029b1..5640dd19961 100644 --- a/homeassistant/components/sense/binary_sensor.py +++ b/homeassistant/components/sense/binary_sensor.py @@ -6,40 +6,29 @@ from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, BinarySensorEntity, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_registry as er from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import ( - ATTRIBUTION, - DOMAIN, - MDI_ICONS, - SENSE_DATA, - SENSE_DEVICE_UPDATE, - SENSE_DEVICES_DATA, - SENSE_DISCOVERED_DEVICES_DATA, -) +from . import SenseConfigEntry +from .const import ATTRIBUTION, DOMAIN, MDI_ICONS, SENSE_DEVICE_UPDATE _LOGGER = logging.getLogger(__name__) async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: SenseConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Sense binary sensor.""" - data = hass.data[DOMAIN][config_entry.entry_id][SENSE_DATA] - sense_devices_data = hass.data[DOMAIN][config_entry.entry_id][SENSE_DEVICES_DATA] - sense_monitor_id = data.sense_monitor_id + sense_monitor_id = config_entry.runtime_data.data.sense_monitor_id - sense_devices = hass.data[DOMAIN][config_entry.entry_id][ - SENSE_DISCOVERED_DEVICES_DATA - ] + sense_devices = config_entry.runtime_data.discovered + device_data = config_entry.runtime_data.device_data devices = [ - SenseDevice(sense_devices_data, device, sense_monitor_id) + SenseDevice(device_data, device, sense_monitor_id) for device in sense_devices if device["tags"]["DeviceListAllowed"] == "true" ] diff --git a/homeassistant/components/sense/const.py b/homeassistant/components/sense/const.py index 3ad35ff345d..5e944c18d8d 100644 --- a/homeassistant/components/sense/const.py +++ b/homeassistant/components/sense/const.py @@ -12,11 +12,7 @@ DOMAIN = "sense" DEFAULT_TIMEOUT = 30 ACTIVE_UPDATE_RATE = 60 DEFAULT_NAME = "Sense" -SENSE_DATA = "sense_data" SENSE_DEVICE_UPDATE = "sense_devices_update" -SENSE_DEVICES_DATA = "sense_devices_data" -SENSE_DISCOVERED_DEVICES_DATA = "sense_discovered_devices" -SENSE_TRENDS_COORDINATOR = "sense_trends_coordinator" ACTIVE_NAME = "Energy" ACTIVE_TYPE = "active" diff --git a/homeassistant/components/sense/sensor.py b/homeassistant/components/sense/sensor.py index 199bae43701..129b1262fd0 100644 --- a/homeassistant/components/sense/sensor.py +++ b/homeassistant/components/sense/sensor.py @@ -5,7 +5,6 @@ from homeassistant.components.sensor import ( SensorEntity, SensorStateClass, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( PERCENTAGE, UnitOfElectricPotential, @@ -18,6 +17,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity +from . import SenseConfigEntry from .const import ( ACTIVE_NAME, ACTIVE_TYPE, @@ -34,11 +34,7 @@ from .const import ( PRODUCTION_NAME, PRODUCTION_PCT_ID, PRODUCTION_PCT_NAME, - SENSE_DATA, SENSE_DEVICE_UPDATE, - SENSE_DEVICES_DATA, - SENSE_DISCOVERED_DEVICES_DATA, - SENSE_TRENDS_COORDINATOR, SOLAR_POWERED_ID, SOLAR_POWERED_NAME, TO_GRID_ID, @@ -87,26 +83,23 @@ def sense_to_mdi(sense_icon): async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: SenseConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Sense sensor.""" - base_data = hass.data[DOMAIN][config_entry.entry_id] - data = base_data[SENSE_DATA] - sense_devices_data = base_data[SENSE_DEVICES_DATA] - trends_coordinator = base_data[SENSE_TRENDS_COORDINATOR] + data = config_entry.runtime_data.data + trends_coordinator = config_entry.runtime_data.trends # Request only in case it takes longer # than 60s await trends_coordinator.async_request_refresh() sense_monitor_id = data.sense_monitor_id - sense_devices = hass.data[DOMAIN][config_entry.entry_id][ - SENSE_DISCOVERED_DEVICES_DATA - ] + sense_devices = config_entry.runtime_data.discovered + device_data = config_entry.runtime_data.device_data entities: list[SensorEntity] = [ - SenseEnergyDevice(sense_devices_data, device, sense_monitor_id) + SenseEnergyDevice(device_data, device, sense_monitor_id) for device in sense_devices if device["tags"]["DeviceListAllowed"] == "true" ] From f8711dbfbfcb4df499f3e2e0d7c8e950687edb0e Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 18 Jun 2024 16:31:50 +1200 Subject: [PATCH 0767/1445] Add esphome native device update entities (#119339) Co-authored-by: J. Nick Koston --- .../components/esphome/entry_data.py | 2 + homeassistant/components/esphome/update.py | 91 ++++++++++++- tests/components/esphome/test_update.py | 120 +++++++++++++++++- 3 files changed, 205 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/esphome/entry_data.py b/homeassistant/components/esphome/entry_data.py index 494669ae839..7a491d1863b 100644 --- a/homeassistant/components/esphome/entry_data.py +++ b/homeassistant/components/esphome/entry_data.py @@ -38,6 +38,7 @@ from aioesphomeapi import ( TextInfo, TextSensorInfo, TimeInfo, + UpdateInfo, UserService, ValveInfo, build_unique_id, @@ -82,6 +83,7 @@ INFO_TYPE_TO_PLATFORM: dict[type[EntityInfo], Platform] = { TextInfo: Platform.TEXT, TextSensorInfo: Platform.SENSOR, TimeInfo: Platform.TIME, + UpdateInfo: Platform.UPDATE, ValveInfo: Platform.VALVE, } diff --git a/homeassistant/components/esphome/update.py b/homeassistant/components/esphome/update.py index cbcb3ae1c70..cb3d36dab9d 100644 --- a/homeassistant/components/esphome/update.py +++ b/homeassistant/components/esphome/update.py @@ -5,7 +5,12 @@ from __future__ import annotations import asyncio from typing import Any -from aioesphomeapi import DeviceInfo as ESPHomeDeviceInfo, EntityInfo +from aioesphomeapi import ( + DeviceInfo as ESPHomeDeviceInfo, + EntityInfo, + UpdateInfo, + UpdateState, +) from homeassistant.components.update import ( UpdateDeviceClass, @@ -19,10 +24,17 @@ from homeassistant.helpers import device_registry as dr from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity +from homeassistant.util.enum import try_parse_enum from .coordinator import ESPHomeDashboardCoordinator from .dashboard import async_get_dashboard from .domain_data import DomainData +from .entity import ( + EsphomeEntity, + convert_api_error_ha_error, + esphome_state_property, + platform_async_setup_entry, +) from .entry_data import RuntimeEntryData KEY_UPDATE_LOCK = "esphome_update_lock" @@ -36,6 +48,15 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up ESPHome update based on a config entry.""" + await platform_async_setup_entry( + hass, + entry, + async_add_entities, + info_type=UpdateInfo, + entity_type=ESPHomeUpdateEntity, + state_type=UpdateState, + ) + if (dashboard := async_get_dashboard(hass)) is None: return entry_data = DomainData.get(hass).get_entry_data(entry) @@ -54,7 +75,7 @@ async def async_setup_entry( unsub() unsubs.clear() - async_add_entities([ESPHomeUpdateEntity(entry_data, dashboard)]) + async_add_entities([ESPHomeDashboardUpdateEntity(entry_data, dashboard)]) if entry_data.available and dashboard.last_update_success: _async_setup_update_entity() @@ -66,7 +87,9 @@ async def async_setup_entry( ] -class ESPHomeUpdateEntity(CoordinatorEntity[ESPHomeDashboardCoordinator], UpdateEntity): +class ESPHomeDashboardUpdateEntity( + CoordinatorEntity[ESPHomeDashboardCoordinator], UpdateEntity +): """Defines an ESPHome update entity.""" _attr_has_entity_name = True @@ -179,3 +202,65 @@ class ESPHomeUpdateEntity(CoordinatorEntity[ESPHomeDashboardCoordinator], Update ) finally: await self.coordinator.async_request_refresh() + + +class ESPHomeUpdateEntity(EsphomeEntity[UpdateInfo, UpdateState], UpdateEntity): + """A update implementation for esphome.""" + + _attr_supported_features = ( + UpdateEntityFeature.INSTALL | UpdateEntityFeature.PROGRESS + ) + + @callback + def _on_static_info_update(self, static_info: EntityInfo) -> None: + """Set attrs from static info.""" + super()._on_static_info_update(static_info) + static_info = self._static_info + self._attr_device_class = try_parse_enum( + UpdateDeviceClass, static_info.device_class + ) + + @property + @esphome_state_property + def installed_version(self) -> str | None: + """Return the installed version.""" + return self._state.current_version + + @property + @esphome_state_property + def in_progress(self) -> bool | int | None: + """Return if the update is in progress.""" + if self._state.has_progress: + return int(self._state.progress) + return self._state.in_progress + + @property + @esphome_state_property + def latest_version(self) -> str | None: + """Return the latest version.""" + return self._state.latest_version + + @property + @esphome_state_property + def release_summary(self) -> str | None: + """Return the release summary.""" + return self._state.release_summary + + @property + @esphome_state_property + def release_url(self) -> str | None: + """Return the release URL.""" + return self._state.release_url + + @property + @esphome_state_property + def title(self) -> str | None: + """Return the title of the update.""" + return self._state.title + + @convert_api_error_ha_error + async def async_install( + self, version: str | None, backup: bool, **kwargs: Any + ) -> None: + """Update the current value.""" + self._client.update_command(key=self._key, install=True) diff --git a/tests/components/esphome/test_update.py b/tests/components/esphome/test_update.py index 50ca6104aa4..812bd2f3e18 100644 --- a/tests/components/esphome/test_update.py +++ b/tests/components/esphome/test_update.py @@ -3,12 +3,21 @@ from collections.abc import Awaitable, Callable from unittest.mock import Mock, patch -from aioesphomeapi import APIClient, EntityInfo, EntityState, UserService +from aioesphomeapi import ( + APIClient, + EntityInfo, + EntityState, + UpdateInfo, + UpdateState, + UserService, +) import pytest from homeassistant.components.esphome.dashboard import async_get_dashboard -from homeassistant.components.update import UpdateEntityFeature +from homeassistant.components.update import DOMAIN as UPDATE_DOMAIN, UpdateEntityFeature +from homeassistant.components.update.const import SERVICE_INSTALL from homeassistant.const import ( + ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, STATE_OFF, STATE_ON, @@ -83,7 +92,7 @@ async def test_update_entity( with patch( "homeassistant.components.esphome.update.DomainData.get_entry_data", - return_value=Mock(available=True, device_info=mock_device_info), + return_value=Mock(available=True, device_info=mock_device_info, info={}), ): assert await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() @@ -266,7 +275,7 @@ async def test_update_entity_dashboard_not_available_startup( with ( patch( "homeassistant.components.esphome.update.DomainData.get_entry_data", - return_value=Mock(available=True, device_info=mock_device_info), + return_value=Mock(available=True, device_info=mock_device_info, info={}), ), patch( "esphome_dashboard_api.ESPHomeDashboardAPI.get_devices", @@ -358,7 +367,7 @@ async def test_update_entity_not_present_without_dashboard( """Test ESPHome update entity does not get created if there is no dashboard.""" with patch( "homeassistant.components.esphome.update.DomainData.get_entry_data", - return_value=Mock(available=True, device_info=mock_device_info), + return_value=Mock(available=True, device_info=mock_device_info, info={}), ): assert await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() @@ -408,3 +417,104 @@ async def test_update_becomes_available_at_runtime( # We now know the version so install is enabled features = state.attributes[ATTR_SUPPORTED_FEATURES] assert features is UpdateEntityFeature.INSTALL + + +async def test_generic_device_update_entity( + hass: HomeAssistant, + mock_client: APIClient, + mock_generic_device_entry, +) -> None: + """Test a generic device update entity.""" + entity_info = [ + UpdateInfo( + object_id="myupdate", + key=1, + name="my update", + unique_id="my_update", + ) + ] + states = [ + UpdateState( + key=1, + current_version="2024.6.0", + latest_version="2024.6.0", + title="ESPHome Project", + release_summary="This is a release summary", + release_url="https://esphome.io/changelog", + ) + ] + user_service = [] + await mock_generic_device_entry( + mock_client=mock_client, + entity_info=entity_info, + user_service=user_service, + states=states, + ) + state = hass.states.get("update.test_myupdate") + assert state is not None + assert state.state == STATE_OFF + + +async def test_generic_device_update_entity_has_update( + hass: HomeAssistant, + mock_client: APIClient, + mock_esphome_device: Callable[ + [APIClient, list[EntityInfo], list[UserService], list[EntityState]], + Awaitable[MockESPHomeDevice], + ], +) -> None: + """Test a generic device update entity with an update.""" + entity_info = [ + UpdateInfo( + object_id="myupdate", + key=1, + name="my update", + unique_id="my_update", + ) + ] + states = [ + UpdateState( + key=1, + current_version="2024.6.0", + latest_version="2024.6.1", + title="ESPHome Project", + release_summary="This is a release summary", + release_url="https://esphome.io/changelog", + ) + ] + user_service = [] + mock_device: MockESPHomeDevice = await mock_esphome_device( + mock_client=mock_client, + entity_info=entity_info, + user_service=user_service, + states=states, + ) + state = hass.states.get("update.test_myupdate") + assert state is not None + assert state.state == STATE_ON + + await hass.services.async_call( + UPDATE_DOMAIN, + SERVICE_INSTALL, + {ATTR_ENTITY_ID: "update.test_myupdate"}, + blocking=True, + ) + + mock_device.set_state( + UpdateState( + key=1, + in_progress=True, + has_progress=True, + progress=50, + current_version="2024.6.0", + latest_version="2024.6.1", + title="ESPHome Project", + release_summary="This is a release summary", + release_url="https://esphome.io/changelog", + ) + ) + + state = hass.states.get("update.test_myupdate") + assert state is not None + assert state.state == STATE_ON + assert state.attributes["in_progress"] == 50 From dc553a81a15a074dd96914ceb9972e91414c5036 Mon Sep 17 00:00:00 2001 From: Thomas55555 <59625598+Thomas55555@users.noreply.github.com> Date: Tue, 18 Jun 2024 07:50:05 +0200 Subject: [PATCH 0768/1445] Bump aioautomower to 2024.6.1 (#119871) --- homeassistant/components/husqvarna_automower/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/husqvarna_automower/manifest.json b/homeassistant/components/husqvarna_automower/manifest.json index 1f36d9c8acc..5ca1b500340 100644 --- a/homeassistant/components/husqvarna_automower/manifest.json +++ b/homeassistant/components/husqvarna_automower/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/husqvarna_automower", "iot_class": "cloud_push", "loggers": ["aioautomower"], - "requirements": ["aioautomower==2024.6.0"] + "requirements": ["aioautomower==2024.6.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index d4c2c1a628a..49cf1b84843 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -204,7 +204,7 @@ aioaseko==0.1.1 aioasuswrt==1.4.0 # homeassistant.components.husqvarna_automower -aioautomower==2024.6.0 +aioautomower==2024.6.1 # homeassistant.components.azure_devops aioazuredevops==2.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 61a1da9c4fd..55d2ccabc17 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -183,7 +183,7 @@ aioaseko==0.1.1 aioasuswrt==1.4.0 # homeassistant.components.husqvarna_automower -aioautomower==2024.6.0 +aioautomower==2024.6.1 # homeassistant.components.azure_devops aioazuredevops==2.1.1 From 4be3b531436d94daa1822059f86f1dc40c4ed19c Mon Sep 17 00:00:00 2001 From: Brent Petit Date: Tue, 18 Jun 2024 00:58:00 -0500 Subject: [PATCH 0769/1445] Fix up ecobee windspeed unit (#119870) --- homeassistant/components/ecobee/weather.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/ecobee/weather.py b/homeassistant/components/ecobee/weather.py index b7961f956eb..b6378504c65 100644 --- a/homeassistant/components/ecobee/weather.py +++ b/homeassistant/components/ecobee/weather.py @@ -59,7 +59,7 @@ class EcobeeWeather(WeatherEntity): _attr_native_pressure_unit = UnitOfPressure.HPA _attr_native_temperature_unit = UnitOfTemperature.FAHRENHEIT _attr_native_visibility_unit = UnitOfLength.METERS - _attr_native_wind_speed_unit = UnitOfSpeed.METERS_PER_SECOND + _attr_native_wind_speed_unit = UnitOfSpeed.MILES_PER_HOUR _attr_has_entity_name = True _attr_name = None _attr_supported_features = WeatherEntityFeature.FORECAST_DAILY From eb89ce47ea04578ae60337321d7873dd9c1e882b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 18 Jun 2024 02:08:08 -0400 Subject: [PATCH 0770/1445] Inline primary integration (#119860) --- homeassistant/components/logbook/helpers.py | 2 +- homeassistant/components/unifiprotect/data.py | 2 +- homeassistant/helpers/device_registry.py | 62 +- homeassistant/helpers/entity_platform.py | 1 - .../airgradient/snapshots/test_init.ambr | 1 - .../aosmith/snapshots/test_device.ambr | 1 - .../components/config/test_device_registry.py | 13 +- .../snapshots/test_init.ambr | 1 - .../ecovacs/snapshots/test_init.ambr | 1 - .../elgato/snapshots/test_button.ambr | 2 - .../elgato/snapshots/test_light.ambr | 3 - .../elgato/snapshots/test_sensor.ambr | 5 - .../elgato/snapshots/test_switch.ambr | 2 - .../energyzero/snapshots/test_sensor.ambr | 6 - .../snapshots/test_diagnostics.ambr | 2 - .../snapshots/test_init.ambr | 1 - .../snapshots/test_init.ambr | 1488 ++++++++--------- .../homekit_controller/test_connection.py | 2 +- .../homewizard/snapshots/test_button.ambr | 1 - .../homewizard/snapshots/test_number.ambr | 2 - .../homewizard/snapshots/test_sensor.ambr | 218 --- .../homewizard/snapshots/test_switch.ambr | 11 - .../snapshots/test_init.ambr | 1 - tests/components/hyperion/test_camera.py | 2 +- tests/components/hyperion/test_light.py | 2 +- tests/components/hyperion/test_sensor.py | 2 +- tests/components/hyperion/test_switch.py | 2 +- .../ista_ecotrend/snapshots/test_init.ambr | 2 - .../kitchen_sink/snapshots/test_switch.ambr | 4 - .../lamarzocco/snapshots/test_switch.ambr | 1 - tests/components/lifx/test_migration.py | 6 +- .../components/lutron_caseta/test_logbook.py | 2 +- tests/components/motioneye/test_camera.py | 2 +- tests/components/mqtt/test_discovery.py | 16 +- tests/components/mqtt/test_tag.py | 4 +- .../netatmo/snapshots/test_init.ambr | 38 - .../netgear_lte/snapshots/test_init.ambr | 1 - .../ondilo_ico/snapshots/test_init.ambr | 2 - .../onewire/snapshots/test_binary_sensor.ambr | 22 - .../onewire/snapshots/test_sensor.ambr | 22 - .../onewire/snapshots/test_switch.ambr | 22 - .../renault/snapshots/test_binary_sensor.ambr | 8 - .../renault/snapshots/test_button.ambr | 8 - .../snapshots/test_device_tracker.ambr | 8 - .../renault/snapshots/test_select.ambr | 8 - .../renault/snapshots/test_sensor.ambr | 8 - .../components/rova/snapshots/test_init.ambr | 1 - .../sfr_box/snapshots/test_binary_sensor.ambr | 2 - .../sfr_box/snapshots/test_button.ambr | 1 - .../sfr_box/snapshots/test_sensor.ambr | 1 - .../snapshots/test_binary_sensor.ambr | 2 - .../tailwind/snapshots/test_button.ambr | 1 - .../tailwind/snapshots/test_cover.ambr | 2 - .../tailwind/snapshots/test_number.ambr | 1 - tests/components/tasmota/test_discovery.py | 8 +- .../components/tedee/snapshots/test_init.ambr | 1 - .../components/tedee/snapshots/test_lock.ambr | 2 - .../teslemetry/snapshots/test_init.ambr | 4 - .../twentemilieu/snapshots/test_calendar.ambr | 1 - .../twentemilieu/snapshots/test_sensor.ambr | 5 - .../uptime/snapshots/test_sensor.ambr | 1 - .../components/vesync/snapshots/test_fan.ambr | 9 - .../vesync/snapshots/test_light.ambr | 9 - .../vesync/snapshots/test_sensor.ambr | 9 - .../vesync/snapshots/test_switch.ambr | 9 - .../whois/snapshots/test_sensor.ambr | 9 - .../wled/snapshots/test_binary_sensor.ambr | 1 - .../wled/snapshots/test_button.ambr | 1 - .../wled/snapshots/test_number.ambr | 2 - .../wled/snapshots/test_select.ambr | 4 - .../wled/snapshots/test_switch.ambr | 4 - tests/helpers/test_device_registry.py | 84 +- tests/helpers/test_entity_platform.py | 1 - tests/helpers/test_entity_registry.py | 8 +- 74 files changed, 787 insertions(+), 1416 deletions(-) diff --git a/homeassistant/components/logbook/helpers.py b/homeassistant/components/logbook/helpers.py index 4fa0da9033a..674f1643793 100644 --- a/homeassistant/components/logbook/helpers.py +++ b/homeassistant/components/logbook/helpers.py @@ -58,7 +58,7 @@ def _async_config_entries_for_ids( dev_reg = dr.async_get(hass) for device_id in device_ids: if (device := dev_reg.async_get(device_id)) and device.config_entries: - config_entry_ids |= device.config_entries + config_entry_ids.update(device.config_entries) return config_entry_ids diff --git a/homeassistant/components/unifiprotect/data.py b/homeassistant/components/unifiprotect/data.py index 2c1f447229a..75c850702f3 100644 --- a/homeassistant/components/unifiprotect/data.py +++ b/homeassistant/components/unifiprotect/data.py @@ -337,7 +337,7 @@ class ProtectData: @callback def async_ufp_instance_for_config_entry_ids( - hass: HomeAssistant, config_entry_ids: set[str] + hass: HomeAssistant, config_entry_ids: list[str] ) -> ProtectApiClient | None: """Find the UFP instance for the config entry ids.""" return next( diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index 324d5ed89a6..2a90d885d70 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -244,11 +244,10 @@ class DeviceEntry: """Device Registry Entry.""" area_id: str | None = attr.ib(default=None) - config_entries: set[str] = attr.ib(converter=set, factory=set) + config_entries: list[str] = attr.ib(factory=list) configuration_url: str | None = attr.ib(default=None) connections: set[tuple[str, str]] = attr.ib(converter=set, factory=set) disabled_by: DeviceEntryDisabler | None = attr.ib(default=None) - primary_integration: str | None = attr.ib(default=None) entry_type: DeviceEntryType | None = attr.ib(default=None) hw_version: str | None = attr.ib(default=None) id: str = attr.ib(factory=uuid_util.random_uuid_hex) @@ -279,7 +278,7 @@ class DeviceEntry: return { "area_id": self.area_id, "configuration_url": self.configuration_url, - "config_entries": list(self.config_entries), + "config_entries": self.config_entries, "connections": list(self.connections), "disabled_by": self.disabled_by, "entry_type": self.entry_type, @@ -291,7 +290,6 @@ class DeviceEntry: "model": self.model, "name_by_user": self.name_by_user, "name": self.name, - "primary_integration": self.primary_integration, "serial_number": self.serial_number, "sw_version": self.sw_version, "via_device_id": self.via_device_id, @@ -320,7 +318,7 @@ class DeviceEntry: json_bytes( { "area_id": self.area_id, - "config_entries": list(self.config_entries), + "config_entries": self.config_entries, "configuration_url": self.configuration_url, "connections": list(self.connections), "disabled_by": self.disabled_by, @@ -345,7 +343,7 @@ class DeviceEntry: class DeletedDeviceEntry: """Deleted Device Registry Entry.""" - config_entries: set[str] = attr.ib() + config_entries: list[str] = attr.ib() connections: set[tuple[str, str]] = attr.ib() identifiers: set[tuple[str, str]] = attr.ib() id: str = attr.ib() @@ -360,7 +358,7 @@ class DeletedDeviceEntry: """Create DeviceEntry from DeletedDeviceEntry.""" return DeviceEntry( # type ignores: likely https://github.com/python/mypy/issues/8625 - config_entries={config_entry_id}, # type: ignore[arg-type] + config_entries=[config_entry_id], connections=self.connections & connections, # type: ignore[arg-type] identifiers=self.identifiers & identifiers, # type: ignore[arg-type] id=self.id, @@ -373,7 +371,7 @@ class DeletedDeviceEntry: return json_fragment( json_bytes( { - "config_entries": list(self.config_entries), + "config_entries": self.config_entries, "connections": list(self.connections), "identifiers": list(self.identifiers), "id": self.id, @@ -647,7 +645,6 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]): default_name: str | None | UndefinedType = UNDEFINED, # To disable a device if it gets created disabled_by: DeviceEntryDisabler | None | UndefinedType = UNDEFINED, - domain: str | UndefinedType = UNDEFINED, entry_type: DeviceEntryType | None | UndefinedType = UNDEFINED, hw_version: str | None | UndefinedType = UNDEFINED, identifiers: set[tuple[str, str]] | None | UndefinedType = UNDEFINED, @@ -766,7 +763,6 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]): configuration_url=configuration_url, device_info_type=device_info_type, disabled_by=disabled_by, - domain=domain, entry_type=entry_type, hw_version=hw_version, manufacturer=manufacturer, @@ -794,7 +790,6 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]): area_id: str | None | UndefinedType = UNDEFINED, configuration_url: str | URL | None | UndefinedType = UNDEFINED, device_info_type: str | UndefinedType = UNDEFINED, - domain: str | UndefinedType = UNDEFINED, disabled_by: DeviceEntryDisabler | None | UndefinedType = UNDEFINED, entry_type: DeviceEntryType | None | UndefinedType = UNDEFINED, hw_version: str | None | UndefinedType = UNDEFINED, @@ -858,21 +853,32 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]): area = ar.async_get(self.hass).async_get_or_create(suggested_area) area_id = area.id - if ( - add_config_entry_id is not UNDEFINED - and add_config_entry_id not in old.config_entries - ): - config_entries = old.config_entries | {add_config_entry_id} + if add_config_entry_id is not UNDEFINED: + # primary ones have to be at the start. + if device_info_type == "primary": + # Move entry to first spot + if not config_entries or config_entries[0] != add_config_entry_id: + config_entries = [add_config_entry_id] + [ + entry + for entry in config_entries + if entry != add_config_entry_id + ] + + # Not primary, append + elif add_config_entry_id not in config_entries: + config_entries = [*config_entries, add_config_entry_id] if ( remove_config_entry_id is not UNDEFINED and remove_config_entry_id in config_entries ): - if config_entries == {remove_config_entry_id}: + if config_entries == [remove_config_entry_id]: self.async_remove_device(device_id) return None - config_entries = config_entries - {remove_config_entry_id} + config_entries = [ + entry for entry in config_entries if entry != remove_config_entry_id + ] if config_entries != old.config_entries: new_values["config_entries"] = config_entries @@ -919,10 +925,6 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]): new_values[attr_name] = value old_values[attr_name] = getattr(old, attr_name) - if device_info_type == "primary" and domain is not UNDEFINED: - new_values["primary_integration"] = domain - old_values["primary_integration"] = old.primary_integration - if old.is_new: new_values["is_new"] = False @@ -989,7 +991,7 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]): for device in data["devices"]: devices[device["id"]] = DeviceEntry( area_id=device["area_id"], - config_entries=set(device["config_entries"]), + config_entries=device["config_entries"], configuration_url=device["configuration_url"], # type ignores (if tuple arg was cast): likely https://github.com/python/mypy/issues/8625 connections={ @@ -1024,7 +1026,7 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]): # Introduced in 0.111 for device in data["deleted_devices"]: deleted_devices[device["id"]] = DeletedDeviceEntry( - config_entries=set(device["config_entries"]), + config_entries=device["config_entries"], connections={tuple(conn) for conn in device["connections"]}, identifiers={tuple(iden) for iden in device["identifiers"]}, id=device["id"], @@ -1055,13 +1057,15 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]): config_entries = deleted_device.config_entries if config_entry_id not in config_entries: continue - if config_entries == {config_entry_id}: + if config_entries == [config_entry_id]: # Add a time stamp when the deleted device became orphaned self.deleted_devices[deleted_device.id] = attr.evolve( - deleted_device, orphaned_timestamp=now_time, config_entries=set() + deleted_device, orphaned_timestamp=now_time, config_entries=[] ) else: - config_entries = config_entries - {config_entry_id} + config_entries = [ + entry for entry in config_entries if entry != config_entry_id + ] # No need to reindex here since we currently # do not have a lookup by config entry self.deleted_devices[deleted_device.id] = attr.evolve( @@ -1167,8 +1171,8 @@ def async_config_entry_disabled_by_changed( if device.disabled: # Device already disabled, do not overwrite continue - if len(device.config_entries) > 1 and device.config_entries.intersection( - enabled_config_entries + if len(device.config_entries) > 1 and any( + entry_id in enabled_config_entries for entry_id in device.config_entries ): continue registry.async_update_device( diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index 2fb3c41fbfa..4dbe3ac68d8 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -771,7 +771,6 @@ class EntityPlatform: try: device = dev_reg.async_get(self.hass).async_get_or_create( config_entry_id=self.config_entry.entry_id, - domain=self.platform_name, **device_info, ) except dev_reg.DeviceInfoError as exc: diff --git a/tests/components/airgradient/snapshots/test_init.ambr b/tests/components/airgradient/snapshots/test_init.ambr index 92698023f1c..7109f603c9d 100644 --- a/tests/components/airgradient/snapshots/test_init.ambr +++ b/tests/components/airgradient/snapshots/test_init.ambr @@ -23,7 +23,6 @@ 'model': 'I-9PSL', 'name': 'Airgradient', 'name_by_user': None, - 'primary_integration': None, 'serial_number': '84fce612f5b8', 'suggested_area': None, 'sw_version': '3.1.1', diff --git a/tests/components/aosmith/snapshots/test_device.ambr b/tests/components/aosmith/snapshots/test_device.ambr index bee404076cd..f6e2625afdb 100644 --- a/tests/components/aosmith/snapshots/test_device.ambr +++ b/tests/components/aosmith/snapshots/test_device.ambr @@ -23,7 +23,6 @@ 'model': 'HPTS-50 200 202172000', 'name': 'My water heater', 'name_by_user': None, - 'primary_integration': None, 'serial_number': 'serial', 'suggested_area': 'Basement', 'sw_version': '2.14', diff --git a/tests/components/config/test_device_registry.py b/tests/components/config/test_device_registry.py index 7524de013f6..804cf29979e 100644 --- a/tests/components/config/test_device_registry.py +++ b/tests/components/config/test_device_registry.py @@ -70,7 +70,6 @@ async def test_list_devices( "model": "model", "name_by_user": None, "name": None, - "primary_integration": None, "serial_number": None, "sw_version": None, "via_device_id": None, @@ -89,7 +88,6 @@ async def test_list_devices( "model": "model", "name_by_user": None, "name": None, - "primary_integration": None, "serial_number": None, "sw_version": None, "via_device_id": dev1, @@ -121,7 +119,6 @@ async def test_list_devices( "model": "model", "name_by_user": None, "name": None, - "primary_integration": None, "serial_number": None, "sw_version": None, "via_device_id": None, @@ -277,7 +274,7 @@ async def test_remove_config_entry_from_device( config_entry_id=entry_2.entry_id, connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, ) - assert device_entry.config_entries == {entry_1.entry_id, entry_2.entry_id} + assert device_entry.config_entries == [entry_1.entry_id, entry_2.entry_id] # Try removing a config entry from the device, it should fail because # async_remove_config_entry_device returns False @@ -296,9 +293,9 @@ async def test_remove_config_entry_from_device( assert response["result"]["config_entries"] == [entry_2.entry_id] # Check that the config entry was removed from the device - assert device_registry.async_get(device_entry.id).config_entries == { + assert device_registry.async_get(device_entry.id).config_entries == [ entry_2.entry_id - } + ] # Remove the 2nd config entry response = await ws_client.remove_device(device_entry.id, entry_2.entry_id) @@ -368,11 +365,11 @@ async def test_remove_config_entry_from_device_fails( config_entry_id=entry_3.entry_id, connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, ) - assert device_entry.config_entries == { + assert device_entry.config_entries == [ entry_1.entry_id, entry_2.entry_id, entry_3.entry_id, - } + ] fake_entry_id = "abc123" assert entry_1.entry_id != fake_entry_id diff --git a/tests/components/devolo_home_network/snapshots/test_init.ambr b/tests/components/devolo_home_network/snapshots/test_init.ambr index 1a592d21836..b042dfec2f1 100644 --- a/tests/components/devolo_home_network/snapshots/test_init.ambr +++ b/tests/components/devolo_home_network/snapshots/test_init.ambr @@ -27,7 +27,6 @@ 'model': 'dLAN pro 1200+ WiFi ac', 'name': 'Mock Title', 'name_by_user': None, - 'primary_integration': 'devolo_home_network', 'serial_number': '1234567890', 'suggested_area': None, 'sw_version': '5.6.1', diff --git a/tests/components/ecovacs/snapshots/test_init.ambr b/tests/components/ecovacs/snapshots/test_init.ambr index 74b59637dba..f47e747b1cf 100644 --- a/tests/components/ecovacs/snapshots/test_init.ambr +++ b/tests/components/ecovacs/snapshots/test_init.ambr @@ -23,7 +23,6 @@ 'model': 'DEEBOT OZMO 950 Series', 'name': 'Ozmo 950', 'name_by_user': None, - 'primary_integration': 'ecovacs', 'serial_number': 'E1234567890000000001', 'suggested_area': None, 'sw_version': None, diff --git a/tests/components/elgato/snapshots/test_button.ambr b/tests/components/elgato/snapshots/test_button.ambr index 6995e265e1e..e7477540f46 100644 --- a/tests/components/elgato/snapshots/test_button.ambr +++ b/tests/components/elgato/snapshots/test_button.ambr @@ -74,7 +74,6 @@ 'model': 'Elgato Key Light Mini', 'name': 'Frenck', 'name_by_user': None, - 'primary_integration': 'elgato', 'serial_number': 'GW24L1A02987', 'suggested_area': None, 'sw_version': '1.0.4 (229)', @@ -156,7 +155,6 @@ 'model': 'Elgato Key Light Mini', 'name': 'Frenck', 'name_by_user': None, - 'primary_integration': 'elgato', 'serial_number': 'GW24L1A02987', 'suggested_area': None, 'sw_version': '1.0.4 (229)', diff --git a/tests/components/elgato/snapshots/test_light.ambr b/tests/components/elgato/snapshots/test_light.ambr index 9bb26f5efd9..e2f663d294b 100644 --- a/tests/components/elgato/snapshots/test_light.ambr +++ b/tests/components/elgato/snapshots/test_light.ambr @@ -106,7 +106,6 @@ 'model': 'Elgato Key Light', 'name': 'Frenck', 'name_by_user': None, - 'primary_integration': 'elgato', 'serial_number': 'CN11A1A00001', 'suggested_area': None, 'sw_version': '1.0.3 (192)', @@ -222,7 +221,6 @@ 'model': 'Elgato Light Strip', 'name': 'Frenck', 'name_by_user': None, - 'primary_integration': 'elgato', 'serial_number': 'CN11A1A00001', 'suggested_area': None, 'sw_version': '1.0.3 (192)', @@ -338,7 +336,6 @@ 'model': 'Elgato Light Strip', 'name': 'Frenck', 'name_by_user': None, - 'primary_integration': 'elgato', 'serial_number': 'CN11A1A00001', 'suggested_area': None, 'sw_version': '1.0.3 (192)', diff --git a/tests/components/elgato/snapshots/test_sensor.ambr b/tests/components/elgato/snapshots/test_sensor.ambr index aacaf34ef4f..2b52d6b9f23 100644 --- a/tests/components/elgato/snapshots/test_sensor.ambr +++ b/tests/components/elgato/snapshots/test_sensor.ambr @@ -81,7 +81,6 @@ 'model': 'Elgato Key Light Mini', 'name': 'Frenck', 'name_by_user': None, - 'primary_integration': 'elgato', 'serial_number': 'GW24L1A02987', 'suggested_area': None, 'sw_version': '1.0.4 (229)', @@ -173,7 +172,6 @@ 'model': 'Elgato Key Light Mini', 'name': 'Frenck', 'name_by_user': None, - 'primary_integration': 'elgato', 'serial_number': 'GW24L1A02987', 'suggested_area': None, 'sw_version': '1.0.4 (229)', @@ -265,7 +263,6 @@ 'model': 'Elgato Key Light Mini', 'name': 'Frenck', 'name_by_user': None, - 'primary_integration': 'elgato', 'serial_number': 'GW24L1A02987', 'suggested_area': None, 'sw_version': '1.0.4 (229)', @@ -354,7 +351,6 @@ 'model': 'Elgato Key Light Mini', 'name': 'Frenck', 'name_by_user': None, - 'primary_integration': 'elgato', 'serial_number': 'GW24L1A02987', 'suggested_area': None, 'sw_version': '1.0.4 (229)', @@ -446,7 +442,6 @@ 'model': 'Elgato Key Light Mini', 'name': 'Frenck', 'name_by_user': None, - 'primary_integration': 'elgato', 'serial_number': 'GW24L1A02987', 'suggested_area': None, 'sw_version': '1.0.4 (229)', diff --git a/tests/components/elgato/snapshots/test_switch.ambr b/tests/components/elgato/snapshots/test_switch.ambr index a501c20e2d7..41f3a8f3aaf 100644 --- a/tests/components/elgato/snapshots/test_switch.ambr +++ b/tests/components/elgato/snapshots/test_switch.ambr @@ -73,7 +73,6 @@ 'model': 'Elgato Key Light Mini', 'name': 'Frenck', 'name_by_user': None, - 'primary_integration': 'elgato', 'serial_number': 'GW24L1A02987', 'suggested_area': None, 'sw_version': '1.0.4 (229)', @@ -154,7 +153,6 @@ 'model': 'Elgato Key Light Mini', 'name': 'Frenck', 'name_by_user': None, - 'primary_integration': 'elgato', 'serial_number': 'GW24L1A02987', 'suggested_area': None, 'sw_version': '1.0.4 (229)', diff --git a/tests/components/energyzero/snapshots/test_sensor.ambr b/tests/components/energyzero/snapshots/test_sensor.ambr index 2663437ae33..23b232379df 100644 --- a/tests/components/energyzero/snapshots/test_sensor.ambr +++ b/tests/components/energyzero/snapshots/test_sensor.ambr @@ -64,7 +64,6 @@ 'model': None, 'name': 'Energy market price', 'name_by_user': None, - 'primary_integration': 'energyzero', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -139,7 +138,6 @@ 'model': None, 'name': 'Energy market price', 'name_by_user': None, - 'primary_integration': 'energyzero', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -211,7 +209,6 @@ 'model': None, 'name': 'Energy market price', 'name_by_user': None, - 'primary_integration': 'energyzero', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -283,7 +280,6 @@ 'model': None, 'name': 'Energy market price', 'name_by_user': None, - 'primary_integration': 'energyzero', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -355,7 +351,6 @@ 'model': None, 'name': 'Energy market price', 'name_by_user': None, - 'primary_integration': 'energyzero', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -430,7 +425,6 @@ 'model': None, 'name': 'Gas market price', 'name_by_user': None, - 'primary_integration': 'energyzero', 'serial_number': None, 'suggested_area': None, 'sw_version': None, diff --git a/tests/components/enphase_envoy/snapshots/test_diagnostics.ambr b/tests/components/enphase_envoy/snapshots/test_diagnostics.ambr index bcbd546c95e..c2ab51a7dbd 100644 --- a/tests/components/enphase_envoy/snapshots/test_diagnostics.ambr +++ b/tests/components/enphase_envoy/snapshots/test_diagnostics.ambr @@ -48,7 +48,6 @@ 'model': 'Envoy, phases: 3, phase mode: three, net-consumption CT, production CT, storage CT', 'name': 'Envoy <>', 'name_by_user': None, - 'primary_integration': 'enphase_envoy', 'serial_number': '<>', 'suggested_area': None, 'sw_version': '7.1.2', @@ -3773,7 +3772,6 @@ 'model': 'Inverter', 'name': 'Inverter 1', 'name_by_user': None, - 'primary_integration': 'enphase_envoy', 'serial_number': None, 'suggested_area': None, 'sw_version': None, diff --git a/tests/components/gardena_bluetooth/snapshots/test_init.ambr b/tests/components/gardena_bluetooth/snapshots/test_init.ambr index 2dd7aa2c7de..82e17896d60 100644 --- a/tests/components/gardena_bluetooth/snapshots/test_init.ambr +++ b/tests/components/gardena_bluetooth/snapshots/test_init.ambr @@ -23,7 +23,6 @@ 'model': 'Mock Model', 'name': 'Mock Title', 'name_by_user': None, - 'primary_integration': 'gardena_bluetooth', 'serial_number': None, 'suggested_area': None, 'sw_version': '1.2.3', diff --git a/tests/components/homekit_controller/snapshots/test_init.ambr b/tests/components/homekit_controller/snapshots/test_init.ambr index 35a2b4937fc..c52bf2c3b27 100644 --- a/tests/components/homekit_controller/snapshots/test_init.ambr +++ b/tests/components/homekit_controller/snapshots/test_init.ambr @@ -26,7 +26,6 @@ 'model': 'AP2', 'name': 'Airversa AP2 1808', 'name_by_user': None, - 'primary_integration': 'homekit_controller', 'serial_number': '1234', 'suggested_area': None, 'sw_version': '0.8.16', @@ -623,7 +622,6 @@ 'model': 'T8010', 'name': 'eufy HomeBase2-0AAA', 'name_by_user': None, - 'primary_integration': 'homekit_controller', 'serial_number': 'A0000A000000000A', 'suggested_area': None, 'sw_version': '2.1.6', @@ -697,7 +695,6 @@ 'model': 'T8113', 'name': 'eufyCam2-0000', 'name_by_user': None, - 'primary_integration': 'homekit_controller', 'serial_number': 'A0000A000000000D', 'suggested_area': None, 'sw_version': '1.6.7', @@ -939,7 +936,6 @@ 'model': 'T8113', 'name': 'eufyCam2-000A', 'name_by_user': None, - 'primary_integration': 'homekit_controller', 'serial_number': 'A0000A000000000B', 'suggested_area': None, 'sw_version': '1.6.7', @@ -1181,7 +1177,6 @@ 'model': 'T8113', 'name': 'eufyCam2-000A', 'name_by_user': None, - 'primary_integration': 'homekit_controller', 'serial_number': 'A0000A000000000C', 'suggested_area': None, 'sw_version': '1.6.7', @@ -1427,7 +1422,6 @@ 'model': 'HE1-G01', 'name': 'Aqara-Hub-E1-00A0', 'name_by_user': None, - 'primary_integration': 'homekit_controller', 'serial_number': '00aa00000a0', 'suggested_area': None, 'sw_version': '3.3.0', @@ -1634,7 +1628,6 @@ 'model': 'AS006', 'name': 'Contact Sensor', 'name_by_user': None, - 'primary_integration': 'homekit_controller', 'serial_number': '158d0007c59c6a', 'suggested_area': None, 'sw_version': '0', @@ -1799,7 +1792,6 @@ 'model': 'ZHWA11LM', 'name': 'Aqara Hub-1563', 'name_by_user': None, - 'primary_integration': 'homekit_controller', 'serial_number': '0000000123456789', 'suggested_area': None, 'sw_version': '1.4.7', @@ -2075,7 +2067,6 @@ 'model': 'AR004', 'name': 'Programmable Switch', 'name_by_user': None, - 'primary_integration': 'homekit_controller', 'serial_number': '111a1111a1a111', 'suggested_area': None, 'sw_version': '9', @@ -2199,7 +2190,6 @@ 'model': 'ABC1000', 'name': 'ArloBabyA0', 'name_by_user': None, - 'primary_integration': 'homekit_controller', 'serial_number': '00A0000000000', 'suggested_area': None, 'sw_version': '1.10.931', @@ -2684,7 +2674,6 @@ 'model': 'CS-IWO', 'name': 'InWall Outlet-0394DE', 'name_by_user': None, - 'primary_integration': 'homekit_controller', 'serial_number': '1020301376', 'suggested_area': None, 'sw_version': '1.0.0', @@ -3114,7 +3103,6 @@ 'model': 'REMOTE SENSOR', 'name': 'Basement', 'name_by_user': None, - 'primary_integration': 'homekit_controller', 'serial_number': 'AB3C', 'suggested_area': None, 'sw_version': '1.0.0', @@ -3274,7 +3262,6 @@ 'model': 'ecobee3', 'name': 'HomeW', 'name_by_user': None, - 'primary_integration': 'homekit_controller', 'serial_number': '123456789012', 'suggested_area': None, 'sw_version': '4.2.394', @@ -3729,7 +3716,6 @@ 'model': 'REMOTE SENSOR', 'name': 'Kitchen', 'name_by_user': None, - 'primary_integration': 'homekit_controller', 'serial_number': 'AB1C', 'suggested_area': None, 'sw_version': '1.0.0', @@ -3889,7 +3875,6 @@ 'model': 'REMOTE SENSOR', 'name': 'Porch', 'name_by_user': None, - 'primary_integration': 'homekit_controller', 'serial_number': 'AB2C', 'suggested_area': None, 'sw_version': '1.0.0', @@ -4053,7 +4038,6 @@ 'model': 'ecobee3', 'name': 'HomeW', 'name_by_user': None, - 'primary_integration': 'homekit_controller', 'serial_number': '123456789012', 'suggested_area': None, 'sw_version': '4.2.394', @@ -4512,7 +4496,6 @@ 'model': 'REMOTE SENSOR', 'name': 'Basement', 'name_by_user': None, - 'primary_integration': 'homekit_controller', 'serial_number': 'AB3C', 'suggested_area': None, 'sw_version': '1.0.0', @@ -4627,7 +4610,6 @@ 'model': 'ecobee3', 'name': 'HomeW', 'name_by_user': None, - 'primary_integration': 'homekit_controller', 'serial_number': '123456789012', 'suggested_area': None, 'sw_version': '4.2.394', @@ -4909,7 +4891,6 @@ 'model': 'REMOTE SENSOR', 'name': 'Kitchen', 'name_by_user': None, - 'primary_integration': 'homekit_controller', 'serial_number': 'AB1C', 'suggested_area': None, 'sw_version': '1.0.0', @@ -5069,7 +5050,6 @@ 'model': 'REMOTE SENSOR', 'name': 'Porch', 'name_by_user': None, - 'primary_integration': 'homekit_controller', 'serial_number': 'AB2C', 'suggested_area': None, 'sw_version': '1.0.0', @@ -5233,7 +5213,6 @@ 'model': 'ECB501', 'name': 'My ecobee', 'name_by_user': None, - 'primary_integration': 'homekit_controller', 'serial_number': '123456789016', 'suggested_area': None, 'sw_version': '4.7.340214', @@ -5701,7 +5680,6 @@ 'model': 'ecobee Switch+', 'name': 'Master Fan', 'name_by_user': None, - 'primary_integration': 'homekit_controller', 'serial_number': '111111111111', 'suggested_area': None, 'sw_version': '4.5.130201', @@ -5991,7 +5969,6 @@ 'model': 'Eve Degree 00AAA0000', 'name': 'Eve Degree AA11', 'name_by_user': None, - 'primary_integration': 'homekit_controller', 'serial_number': 'AA00A0A00000', 'suggested_area': None, 'sw_version': '1.2.8', @@ -6348,7 +6325,6 @@ 'model': 'Eve Energy 20EAO8601', 'name': 'Eve Energy 50FF', 'name_by_user': None, - 'primary_integration': 'homekit_controller', 'serial_number': 'AA00A0A00000', 'suggested_area': None, 'sw_version': '1.2.9', @@ -6687,7 +6663,6 @@ 'model': 'RavenSystem HAA', 'name': 'HAA-C718B3', 'name_by_user': None, - 'primary_integration': 'homekit_controller', 'serial_number': 'C718B3-1', 'suggested_area': None, 'sw_version': '5.0.18', @@ -6893,7 +6868,6 @@ 'model': 'RavenSystem HAA', 'name': 'HAA-C718B3', 'name_by_user': None, - 'primary_integration': 'homekit_controller', 'serial_number': 'C718B3-2', 'suggested_area': None, 'sw_version': '5.0.18', @@ -7011,7 +6985,6 @@ 'model': 'RavenSystem HAA', 'name': 'HAA-C718B3', 'name_by_user': None, - 'primary_integration': 'homekit_controller', 'serial_number': 'C718B3-1', 'suggested_area': None, 'sw_version': '5.0.18', @@ -7213,7 +7186,6 @@ 'model': 'RavenSystem HAA', 'name': 'HAA-C718B3', 'name_by_user': None, - 'primary_integration': 'homekit_controller', 'serial_number': 'C718B3-2', 'suggested_area': None, 'sw_version': '5.0.18', @@ -7331,7 +7303,6 @@ 'model': 'RYSE Shade', 'name': 'Family Room North', 'name_by_user': None, - 'primary_integration': 'homekit_controller', 'serial_number': 'cover.family_door_north', 'suggested_area': None, 'sw_version': '3.6.2', @@ -7493,7 +7464,6 @@ 'model': 'Bridge', 'name': 'HASS Bridge S6', 'name_by_user': None, - 'primary_integration': 'homekit_controller', 'serial_number': 'homekit.bridge', 'suggested_area': None, 'sw_version': '2024.2.0', @@ -7567,7 +7537,6 @@ 'model': 'RYSE Shade', 'name': 'Kitchen Window', 'name_by_user': None, - 'primary_integration': 'homekit_controller', 'serial_number': 'cover.kitchen_window', 'suggested_area': None, 'sw_version': '3.6.2', @@ -7733,7 +7702,6 @@ 'model': 'Fan', 'name': 'Ceiling Fan', 'name_by_user': None, - 'primary_integration': 'homekit_controller', 'serial_number': 'fan.ceiling_fan', 'suggested_area': None, 'sw_version': '0.104.0.dev0', @@ -7854,7 +7822,6 @@ 'model': 'Bridge', 'name': 'Home Assistant Bridge', 'name_by_user': None, - 'primary_integration': 'homekit_controller', 'serial_number': 'homekit.bridge', 'suggested_area': None, 'sw_version': '0.104.0.dev0', @@ -7928,7 +7895,6 @@ 'model': 'Fan', 'name': 'Living Room Fan', 'name_by_user': None, - 'primary_integration': 'homekit_controller', 'serial_number': 'fan.living_room_fan', 'suggested_area': None, 'sw_version': '0.104.0.dev0', @@ -8054,7 +8020,6 @@ 'model': 'Climate Control', 'name': '89 Living Room', 'name_by_user': None, - 'primary_integration': 'homekit_controller', 'serial_number': 'climate.89_living_room', 'suggested_area': None, 'sw_version': '2024.2.0', @@ -8377,7 +8342,6 @@ 'model': 'Bridge', 'name': 'HASS Bridge S6', 'name_by_user': None, - 'primary_integration': 'homekit_controller', 'serial_number': 'homekit.bridge', 'suggested_area': None, 'sw_version': '2024.2.0', @@ -8455,7 +8419,6 @@ 'model': 'Bridge', 'name': 'HASS Bridge S6', 'name_by_user': None, - 'primary_integration': 'homekit_controller', 'serial_number': 'homekit.bridge', 'suggested_area': None, 'sw_version': '2024.2.0', @@ -8529,7 +8492,6 @@ 'model': '1039102', 'name': 'Laundry Smoke ED78', 'name_by_user': None, - 'primary_integration': 'homekit_controller', 'serial_number': 'light.laundry_smoke_ed78', 'suggested_area': None, 'sw_version': '1.4.84', @@ -8703,7 +8665,6 @@ 'model': 'RYSE Shade', 'name': 'Family Room North', 'name_by_user': None, - 'primary_integration': 'homekit_controller', 'serial_number': 'cover.family_door_north', 'suggested_area': None, 'sw_version': '3.6.2', @@ -8865,7 +8826,6 @@ 'model': 'Bridge', 'name': 'HASS Bridge S6', 'name_by_user': None, - 'primary_integration': 'homekit_controller', 'serial_number': 'homekit.bridge', 'suggested_area': None, 'sw_version': '2024.2.0', @@ -8939,7 +8899,6 @@ 'model': 'RYSE Shade', 'name': 'Kitchen Window', 'name_by_user': None, - 'primary_integration': 'homekit_controller', 'serial_number': 'cover.kitchen_window', 'suggested_area': None, 'sw_version': '3.6.2', @@ -9105,7 +9064,6 @@ 'model': 'Fan', 'name': 'Ceiling Fan', 'name_by_user': None, - 'primary_integration': 'homekit_controller', 'serial_number': 'fan.ceiling_fan', 'suggested_area': None, 'sw_version': '0.104.0.dev0', @@ -9226,7 +9184,6 @@ 'model': 'Bridge', 'name': 'Home Assistant Bridge', 'name_by_user': None, - 'primary_integration': 'homekit_controller', 'serial_number': 'homekit.bridge', 'suggested_area': None, 'sw_version': '0.104.0.dev0', @@ -9300,7 +9257,6 @@ 'model': 'Fan', 'name': 'Living Room Fan', 'name_by_user': None, - 'primary_integration': 'homekit_controller', 'serial_number': 'fan.living_room_fan', 'suggested_area': None, 'sw_version': '0.104.0.dev0', @@ -9427,7 +9383,6 @@ 'model': 'Bridge', 'name': 'Home Assistant Bridge', 'name_by_user': None, - 'primary_integration': 'homekit_controller', 'serial_number': 'homekit.bridge', 'suggested_area': None, 'sw_version': '0.104.0.dev0', @@ -9501,7 +9456,6 @@ 'model': 'Fan', 'name': 'Living Room Fan', 'name_by_user': None, - 'primary_integration': 'homekit_controller', 'serial_number': 'fan.living_room_fan', 'suggested_area': None, 'sw_version': '0.104.0.dev0', @@ -9628,7 +9582,6 @@ 'model': 'Climate Control', 'name': '89 Living Room', 'name_by_user': None, - 'primary_integration': 'homekit_controller', 'serial_number': 'climate.89_living_room', 'suggested_area': None, 'sw_version': '2024.2.0', @@ -9960,7 +9913,6 @@ 'model': 'Bridge', 'name': 'HASS Bridge S6', 'name_by_user': None, - 'primary_integration': 'homekit_controller', 'serial_number': 'homekit.bridge', 'suggested_area': None, 'sw_version': '2024.2.0', @@ -10038,7 +9990,6 @@ 'model': 'Bridge', 'name': 'HASS Bridge S6', 'name_by_user': None, - 'primary_integration': 'homekit_controller', 'serial_number': 'homekit.bridge', 'suggested_area': None, 'sw_version': '2024.2.0', @@ -10112,7 +10063,6 @@ 'model': 'WoHumi', 'name': 'Humidifier 182A', 'name_by_user': None, - 'primary_integration': 'homekit_controller', 'serial_number': 'humidifier.humidifier_182a', 'suggested_area': None, 'sw_version': '2024.2.0', @@ -10293,7 +10243,6 @@ 'model': 'Bridge', 'name': 'HASS Bridge S6', 'name_by_user': None, - 'primary_integration': 'homekit_controller', 'serial_number': 'homekit.bridge', 'suggested_area': None, 'sw_version': '2024.2.0', @@ -10367,7 +10316,6 @@ 'model': 'WoHumi', 'name': 'Humidifier 182A', 'name_by_user': None, - 'primary_integration': 'homekit_controller', 'serial_number': 'humidifier.humidifier_182a', 'suggested_area': None, 'sw_version': '2024.2.0', @@ -10548,7 +10496,6 @@ 'model': 'Bridge', 'name': 'HASS Bridge S6', 'name_by_user': None, - 'primary_integration': 'homekit_controller', 'serial_number': 'homekit.bridge', 'suggested_area': None, 'sw_version': '2024.2.0', @@ -10622,7 +10569,6 @@ 'model': '1039102', 'name': 'Laundry Smoke ED78', 'name_by_user': None, - 'primary_integration': 'homekit_controller', 'serial_number': 'light.laundry_smoke_ed78', 'suggested_area': None, 'sw_version': '1.4.84', @@ -10811,7 +10757,6 @@ 'model': 'Daikin-fwec3a-esp32-homekit-bridge', 'name': 'Air Conditioner', 'name_by_user': None, - 'primary_integration': 'homekit_controller', 'serial_number': '00000001', 'suggested_area': None, 'sw_version': '1.0.0', @@ -10985,417 +10930,6 @@ # --- # name: test_snapshots[hue_bridge] list([ - dict({ - 'device': dict({ - 'area_id': None, - 'config_entries': list([ - 'TestData', - ]), - 'configuration_url': None, - 'connections': list([ - ]), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': '', - 'identifiers': list([ - list([ - 'homekit_controller:accessory-id', - '00:00:00:00:00:00:aid:6623462403233419', - ]), - ]), - 'is_new': False, - 'labels': list([ - ]), - 'manufacturer': 'Philips', - 'model': 'LTW012', - 'name': 'Hue ambiance candle', - 'name_by_user': None, - 'primary_integration': 'homekit_controller', - 'serial_number': '6623462403233419', - 'suggested_area': None, - 'sw_version': '1.46.13', - }), - 'entities': list([ - dict({ - 'entry': dict({ - 'aliases': list([ - ]), - 'area_id': None, - 'capabilities': None, - 'categories': dict({ - }), - 'config_entry_id': 'TestData', - 'device_class': None, - 'disabled_by': None, - 'domain': 'button', - 'entity_category': , - 'entity_id': 'button.hue_ambiance_candle_identify', - 'has_entity_name': False, - 'hidden_by': None, - 'icon': None, - 'labels': list([ - ]), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Hue ambiance candle Identify', - 'platform': 'homekit_controller', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '00:00:00:00:00:00_6623462403233419_1_6', - 'unit_of_measurement': None, - }), - 'state': dict({ - 'attributes': dict({ - 'device_class': 'identify', - 'friendly_name': 'Hue ambiance candle Identify', - }), - 'entity_id': 'button.hue_ambiance_candle_identify', - 'state': 'unknown', - }), - }), - dict({ - 'entry': dict({ - 'aliases': list([ - ]), - 'area_id': None, - 'capabilities': dict({ - 'max_color_temp_kelvin': 6535, - 'max_mireds': 454, - 'min_color_temp_kelvin': 2202, - 'min_mireds': 153, - 'supported_color_modes': list([ - , - ]), - }), - 'categories': dict({ - }), - 'config_entry_id': 'TestData', - 'device_class': None, - 'disabled_by': None, - 'domain': 'light', - 'entity_category': None, - 'entity_id': 'light.hue_ambiance_candle', - 'has_entity_name': False, - 'hidden_by': None, - 'icon': None, - 'labels': list([ - ]), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Hue ambiance candle', - 'platform': 'homekit_controller', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '00:00:00:00:00:00_6623462403233419_2816', - 'unit_of_measurement': None, - }), - 'state': dict({ - 'attributes': dict({ - 'brightness': None, - 'color_mode': None, - 'color_temp': None, - 'color_temp_kelvin': None, - 'friendly_name': 'Hue ambiance candle', - 'hs_color': None, - 'max_color_temp_kelvin': 6535, - 'max_mireds': 454, - 'min_color_temp_kelvin': 2202, - 'min_mireds': 153, - 'rgb_color': None, - 'supported_color_modes': list([ - , - ]), - 'supported_features': , - 'xy_color': None, - }), - 'entity_id': 'light.hue_ambiance_candle', - 'state': 'off', - }), - }), - ]), - }), - dict({ - 'device': dict({ - 'area_id': None, - 'config_entries': list([ - 'TestData', - ]), - 'configuration_url': None, - 'connections': list([ - ]), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': '', - 'identifiers': list([ - list([ - 'homekit_controller:accessory-id', - '00:00:00:00:00:00:aid:6623462403113447', - ]), - ]), - 'is_new': False, - 'labels': list([ - ]), - 'manufacturer': 'Philips', - 'model': 'LTW012', - 'name': 'Hue ambiance candle', - 'name_by_user': None, - 'primary_integration': 'homekit_controller', - 'serial_number': '6623462403113447', - 'suggested_area': None, - 'sw_version': '1.46.13', - }), - 'entities': list([ - dict({ - 'entry': dict({ - 'aliases': list([ - ]), - 'area_id': None, - 'capabilities': None, - 'categories': dict({ - }), - 'config_entry_id': 'TestData', - 'device_class': None, - 'disabled_by': None, - 'domain': 'button', - 'entity_category': , - 'entity_id': 'button.hue_ambiance_candle_identify_2', - 'has_entity_name': False, - 'hidden_by': None, - 'icon': None, - 'labels': list([ - ]), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Hue ambiance candle Identify', - 'platform': 'homekit_controller', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '00:00:00:00:00:00_6623462403113447_1_6', - 'unit_of_measurement': None, - }), - 'state': dict({ - 'attributes': dict({ - 'device_class': 'identify', - 'friendly_name': 'Hue ambiance candle Identify', - }), - 'entity_id': 'button.hue_ambiance_candle_identify_2', - 'state': 'unknown', - }), - }), - dict({ - 'entry': dict({ - 'aliases': list([ - ]), - 'area_id': None, - 'capabilities': dict({ - 'max_color_temp_kelvin': 6535, - 'max_mireds': 454, - 'min_color_temp_kelvin': 2202, - 'min_mireds': 153, - 'supported_color_modes': list([ - , - ]), - }), - 'categories': dict({ - }), - 'config_entry_id': 'TestData', - 'device_class': None, - 'disabled_by': None, - 'domain': 'light', - 'entity_category': None, - 'entity_id': 'light.hue_ambiance_candle_2', - 'has_entity_name': False, - 'hidden_by': None, - 'icon': None, - 'labels': list([ - ]), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Hue ambiance candle', - 'platform': 'homekit_controller', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '00:00:00:00:00:00_6623462403113447_2816', - 'unit_of_measurement': None, - }), - 'state': dict({ - 'attributes': dict({ - 'brightness': None, - 'color_mode': None, - 'color_temp': None, - 'color_temp_kelvin': None, - 'friendly_name': 'Hue ambiance candle', - 'hs_color': None, - 'max_color_temp_kelvin': 6535, - 'max_mireds': 454, - 'min_color_temp_kelvin': 2202, - 'min_mireds': 153, - 'rgb_color': None, - 'supported_color_modes': list([ - , - ]), - 'supported_features': , - 'xy_color': None, - }), - 'entity_id': 'light.hue_ambiance_candle_2', - 'state': 'off', - }), - }), - ]), - }), - dict({ - 'device': dict({ - 'area_id': None, - 'config_entries': list([ - 'TestData', - ]), - 'configuration_url': None, - 'connections': list([ - ]), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': '', - 'identifiers': list([ - list([ - 'homekit_controller:accessory-id', - '00:00:00:00:00:00:aid:6623462395276939', - ]), - ]), - 'is_new': False, - 'labels': list([ - ]), - 'manufacturer': 'Philips', - 'model': 'LTW012', - 'name': 'Hue ambiance candle', - 'name_by_user': None, - 'primary_integration': 'homekit_controller', - 'serial_number': '6623462395276939', - 'suggested_area': None, - 'sw_version': '1.46.13', - }), - 'entities': list([ - dict({ - 'entry': dict({ - 'aliases': list([ - ]), - 'area_id': None, - 'capabilities': None, - 'categories': dict({ - }), - 'config_entry_id': 'TestData', - 'device_class': None, - 'disabled_by': None, - 'domain': 'button', - 'entity_category': , - 'entity_id': 'button.hue_ambiance_candle_identify_3', - 'has_entity_name': False, - 'hidden_by': None, - 'icon': None, - 'labels': list([ - ]), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Hue ambiance candle Identify', - 'platform': 'homekit_controller', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '00:00:00:00:00:00_6623462395276939_1_6', - 'unit_of_measurement': None, - }), - 'state': dict({ - 'attributes': dict({ - 'device_class': 'identify', - 'friendly_name': 'Hue ambiance candle Identify', - }), - 'entity_id': 'button.hue_ambiance_candle_identify_3', - 'state': 'unknown', - }), - }), - dict({ - 'entry': dict({ - 'aliases': list([ - ]), - 'area_id': None, - 'capabilities': dict({ - 'max_color_temp_kelvin': 6535, - 'max_mireds': 454, - 'min_color_temp_kelvin': 2202, - 'min_mireds': 153, - 'supported_color_modes': list([ - , - ]), - }), - 'categories': dict({ - }), - 'config_entry_id': 'TestData', - 'device_class': None, - 'disabled_by': None, - 'domain': 'light', - 'entity_category': None, - 'entity_id': 'light.hue_ambiance_candle_3', - 'has_entity_name': False, - 'hidden_by': None, - 'icon': None, - 'labels': list([ - ]), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Hue ambiance candle', - 'platform': 'homekit_controller', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '00:00:00:00:00:00_6623462395276939_2816', - 'unit_of_measurement': None, - }), - 'state': dict({ - 'attributes': dict({ - 'brightness': None, - 'color_mode': None, - 'color_temp': None, - 'color_temp_kelvin': None, - 'friendly_name': 'Hue ambiance candle', - 'hs_color': None, - 'max_color_temp_kelvin': 6535, - 'max_mireds': 454, - 'min_color_temp_kelvin': 2202, - 'min_mireds': 153, - 'rgb_color': None, - 'supported_color_modes': list([ - , - ]), - 'supported_features': , - 'xy_color': None, - }), - 'entity_id': 'light.hue_ambiance_candle_3', - 'state': 'off', - }), - }), - ]), - }), dict({ 'device': dict({ 'area_id': None, @@ -11421,7 +10955,6 @@ 'model': 'LTW012', 'name': 'Hue ambiance candle', 'name_by_user': None, - 'primary_integration': 'homekit_controller', 'serial_number': '6623462395276914', 'suggested_area': None, 'sw_version': '1.46.13', @@ -11548,18 +11081,17 @@ 'identifiers': list([ list([ 'homekit_controller:accessory-id', - '00:00:00:00:00:00:aid:6623462412413293', + '00:00:00:00:00:00:aid:6623462395276939', ]), ]), 'is_new': False, 'labels': list([ ]), 'manufacturer': 'Philips', - 'model': 'LTW013', - 'name': 'Hue ambiance spot', + 'model': 'LTW012', + 'name': 'Hue ambiance candle', 'name_by_user': None, - 'primary_integration': 'homekit_controller', - 'serial_number': '6623462412413293', + 'serial_number': '6623462395276939', 'suggested_area': None, 'sw_version': '1.46.13', }), @@ -11577,7 +11109,7 @@ 'disabled_by': None, 'domain': 'button', 'entity_category': , - 'entity_id': 'button.hue_ambiance_spot_identify', + 'entity_id': 'button.hue_ambiance_candle_identify_3', 'has_entity_name': False, 'hidden_by': None, 'icon': None, @@ -11588,20 +11120,20 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Hue ambiance spot Identify', + 'original_name': 'Hue ambiance candle Identify', 'platform': 'homekit_controller', 'previous_unique_id': None, 'supported_features': 0, 'translation_key': None, - 'unique_id': '00:00:00:00:00:00_6623462412413293_1_6', + 'unique_id': '00:00:00:00:00:00_6623462395276939_1_6', 'unit_of_measurement': None, }), 'state': dict({ 'attributes': dict({ 'device_class': 'identify', - 'friendly_name': 'Hue ambiance spot Identify', + 'friendly_name': 'Hue ambiance candle Identify', }), - 'entity_id': 'button.hue_ambiance_spot_identify', + 'entity_id': 'button.hue_ambiance_candle_identify_3', 'state': 'unknown', }), }), @@ -11626,7 +11158,7 @@ 'disabled_by': None, 'domain': 'light', 'entity_category': None, - 'entity_id': 'light.hue_ambiance_spot', + 'entity_id': 'light.hue_ambiance_candle_3', 'has_entity_name': False, 'hidden_by': None, 'icon': None, @@ -11637,45 +11169,307 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Hue ambiance spot', + 'original_name': 'Hue ambiance candle', 'platform': 'homekit_controller', 'previous_unique_id': None, 'supported_features': 0, 'translation_key': None, - 'unique_id': '00:00:00:00:00:00_6623462412413293_2816', + 'unique_id': '00:00:00:00:00:00_6623462395276939_2816', 'unit_of_measurement': None, }), 'state': dict({ 'attributes': dict({ - 'brightness': 255.0, - 'color_mode': , - 'color_temp': 366, - 'color_temp_kelvin': 2732, - 'friendly_name': 'Hue ambiance spot', - 'hs_color': tuple( - 28.327, - 64.71, - ), + 'brightness': None, + 'color_mode': None, + 'color_temp': None, + 'color_temp_kelvin': None, + 'friendly_name': 'Hue ambiance candle', + 'hs_color': None, 'max_color_temp_kelvin': 6535, 'max_mireds': 454, 'min_color_temp_kelvin': 2202, 'min_mireds': 153, - 'rgb_color': tuple( - 255, - 167, - 89, - ), + 'rgb_color': None, 'supported_color_modes': list([ , ]), 'supported_features': , - 'xy_color': tuple( - 0.524, - 0.387, - ), + 'xy_color': None, }), - 'entity_id': 'light.hue_ambiance_spot', - 'state': 'on', + 'entity_id': 'light.hue_ambiance_candle_3', + 'state': 'off', + }), + }), + ]), + }), + dict({ + 'device': dict({ + 'area_id': None, + 'config_entries': list([ + 'TestData', + ]), + 'configuration_url': None, + 'connections': list([ + ]), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': '', + 'identifiers': list([ + list([ + 'homekit_controller:accessory-id', + '00:00:00:00:00:00:aid:6623462403113447', + ]), + ]), + 'is_new': False, + 'labels': list([ + ]), + 'manufacturer': 'Philips', + 'model': 'LTW012', + 'name': 'Hue ambiance candle', + 'name_by_user': None, + 'serial_number': '6623462403113447', + 'suggested_area': None, + 'sw_version': '1.46.13', + }), + 'entities': list([ + dict({ + 'entry': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': 'TestData', + 'device_class': None, + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.hue_ambiance_candle_identify_2', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Hue ambiance candle Identify', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00_6623462403113447_1_6', + 'unit_of_measurement': None, + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'identify', + 'friendly_name': 'Hue ambiance candle Identify', + }), + 'entity_id': 'button.hue_ambiance_candle_identify_2', + 'state': 'unknown', + }), + }), + dict({ + 'entry': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'max_color_temp_kelvin': 6535, + 'max_mireds': 454, + 'min_color_temp_kelvin': 2202, + 'min_mireds': 153, + 'supported_color_modes': list([ + , + ]), + }), + 'categories': dict({ + }), + 'config_entry_id': 'TestData', + 'device_class': None, + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.hue_ambiance_candle_2', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Hue ambiance candle', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00_6623462403113447_2816', + 'unit_of_measurement': None, + }), + 'state': dict({ + 'attributes': dict({ + 'brightness': None, + 'color_mode': None, + 'color_temp': None, + 'color_temp_kelvin': None, + 'friendly_name': 'Hue ambiance candle', + 'hs_color': None, + 'max_color_temp_kelvin': 6535, + 'max_mireds': 454, + 'min_color_temp_kelvin': 2202, + 'min_mireds': 153, + 'rgb_color': None, + 'supported_color_modes': list([ + , + ]), + 'supported_features': , + 'xy_color': None, + }), + 'entity_id': 'light.hue_ambiance_candle_2', + 'state': 'off', + }), + }), + ]), + }), + dict({ + 'device': dict({ + 'area_id': None, + 'config_entries': list([ + 'TestData', + ]), + 'configuration_url': None, + 'connections': list([ + ]), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': '', + 'identifiers': list([ + list([ + 'homekit_controller:accessory-id', + '00:00:00:00:00:00:aid:6623462403233419', + ]), + ]), + 'is_new': False, + 'labels': list([ + ]), + 'manufacturer': 'Philips', + 'model': 'LTW012', + 'name': 'Hue ambiance candle', + 'name_by_user': None, + 'serial_number': '6623462403233419', + 'suggested_area': None, + 'sw_version': '1.46.13', + }), + 'entities': list([ + dict({ + 'entry': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': 'TestData', + 'device_class': None, + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.hue_ambiance_candle_identify', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Hue ambiance candle Identify', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00_6623462403233419_1_6', + 'unit_of_measurement': None, + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'identify', + 'friendly_name': 'Hue ambiance candle Identify', + }), + 'entity_id': 'button.hue_ambiance_candle_identify', + 'state': 'unknown', + }), + }), + dict({ + 'entry': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'max_color_temp_kelvin': 6535, + 'max_mireds': 454, + 'min_color_temp_kelvin': 2202, + 'min_mireds': 153, + 'supported_color_modes': list([ + , + ]), + }), + 'categories': dict({ + }), + 'config_entry_id': 'TestData', + 'device_class': None, + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.hue_ambiance_candle', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Hue ambiance candle', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00_6623462403233419_2816', + 'unit_of_measurement': None, + }), + 'state': dict({ + 'attributes': dict({ + 'brightness': None, + 'color_mode': None, + 'color_temp': None, + 'color_temp_kelvin': None, + 'friendly_name': 'Hue ambiance candle', + 'hs_color': None, + 'max_color_temp_kelvin': 6535, + 'max_mireds': 454, + 'min_color_temp_kelvin': 2202, + 'min_mireds': 153, + 'rgb_color': None, + 'supported_color_modes': list([ + , + ]), + 'supported_features': , + 'xy_color': None, + }), + 'entity_id': 'light.hue_ambiance_candle', + 'state': 'off', }), }), ]), @@ -11705,7 +11499,6 @@ 'model': 'LTW013', 'name': 'Hue ambiance spot', 'name_by_user': None, - 'primary_integration': 'homekit_controller', 'serial_number': '6623462412411853', 'suggested_area': None, 'sw_version': '1.46.13', @@ -11827,6 +11620,152 @@ }), ]), }), + dict({ + 'device': dict({ + 'area_id': None, + 'config_entries': list([ + 'TestData', + ]), + 'configuration_url': None, + 'connections': list([ + ]), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': '', + 'identifiers': list([ + list([ + 'homekit_controller:accessory-id', + '00:00:00:00:00:00:aid:6623462412413293', + ]), + ]), + 'is_new': False, + 'labels': list([ + ]), + 'manufacturer': 'Philips', + 'model': 'LTW013', + 'name': 'Hue ambiance spot', + 'name_by_user': None, + 'serial_number': '6623462412413293', + 'suggested_area': None, + 'sw_version': '1.46.13', + }), + 'entities': list([ + dict({ + 'entry': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': 'TestData', + 'device_class': None, + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.hue_ambiance_spot_identify', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Hue ambiance spot Identify', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00_6623462412413293_1_6', + 'unit_of_measurement': None, + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'identify', + 'friendly_name': 'Hue ambiance spot Identify', + }), + 'entity_id': 'button.hue_ambiance_spot_identify', + 'state': 'unknown', + }), + }), + dict({ + 'entry': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'max_color_temp_kelvin': 6535, + 'max_mireds': 454, + 'min_color_temp_kelvin': 2202, + 'min_mireds': 153, + 'supported_color_modes': list([ + , + ]), + }), + 'categories': dict({ + }), + 'config_entry_id': 'TestData', + 'device_class': None, + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.hue_ambiance_spot', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Hue ambiance spot', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00_6623462412413293_2816', + 'unit_of_measurement': None, + }), + 'state': dict({ + 'attributes': dict({ + 'brightness': 255.0, + 'color_mode': , + 'color_temp': 366, + 'color_temp_kelvin': 2732, + 'friendly_name': 'Hue ambiance spot', + 'hs_color': tuple( + 28.327, + 64.71, + ), + 'max_color_temp_kelvin': 6535, + 'max_mireds': 454, + 'min_color_temp_kelvin': 2202, + 'min_mireds': 153, + 'rgb_color': tuple( + 255, + 167, + 89, + ), + 'supported_color_modes': list([ + , + ]), + 'supported_features': , + 'xy_color': tuple( + 0.524, + 0.387, + ), + }), + 'entity_id': 'light.hue_ambiance_spot', + 'state': 'on', + }), + }), + ]), + }), dict({ 'device': dict({ 'area_id': None, @@ -11852,7 +11791,6 @@ 'model': 'RWL021', 'name': 'Hue dimmer switch', 'name_by_user': None, - 'primary_integration': 'homekit_controller', 'serial_number': '6623462389072572', 'suggested_area': None, 'sw_version': '45.1.17846', @@ -12168,7 +12106,6 @@ 'model': 'LWB010', 'name': 'Hue white lamp', 'name_by_user': None, - 'primary_integration': 'homekit_controller', 'serial_number': '6623462378982941', 'suggested_area': None, 'sw_version': '1.46.13', @@ -12292,7 +12229,6 @@ 'model': 'LWB010', 'name': 'Hue white lamp', 'name_by_user': None, - 'primary_integration': 'homekit_controller', 'serial_number': '6623462378983942', 'suggested_area': None, 'sw_version': '1.46.13', @@ -12391,130 +12327,6 @@ }), ]), }), - dict({ - 'device': dict({ - 'area_id': None, - 'config_entries': list([ - 'TestData', - ]), - 'configuration_url': None, - 'connections': list([ - ]), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': '', - 'identifiers': list([ - list([ - 'homekit_controller:accessory-id', - '00:00:00:00:00:00:aid:6623462379123707', - ]), - ]), - 'is_new': False, - 'labels': list([ - ]), - 'manufacturer': 'Philips', - 'model': 'LWB010', - 'name': 'Hue white lamp', - 'name_by_user': None, - 'primary_integration': 'homekit_controller', - 'serial_number': '6623462379123707', - 'suggested_area': None, - 'sw_version': '1.46.13', - }), - 'entities': list([ - dict({ - 'entry': dict({ - 'aliases': list([ - ]), - 'area_id': None, - 'capabilities': None, - 'categories': dict({ - }), - 'config_entry_id': 'TestData', - 'device_class': None, - 'disabled_by': None, - 'domain': 'button', - 'entity_category': , - 'entity_id': 'button.hue_white_lamp_identify_3', - 'has_entity_name': False, - 'hidden_by': None, - 'icon': None, - 'labels': list([ - ]), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Hue white lamp Identify', - 'platform': 'homekit_controller', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '00:00:00:00:00:00_6623462379123707_1_6', - 'unit_of_measurement': None, - }), - 'state': dict({ - 'attributes': dict({ - 'device_class': 'identify', - 'friendly_name': 'Hue white lamp Identify', - }), - 'entity_id': 'button.hue_white_lamp_identify_3', - 'state': 'unknown', - }), - }), - dict({ - 'entry': dict({ - 'aliases': list([ - ]), - 'area_id': None, - 'capabilities': dict({ - 'supported_color_modes': list([ - , - ]), - }), - 'categories': dict({ - }), - 'config_entry_id': 'TestData', - 'device_class': None, - 'disabled_by': None, - 'domain': 'light', - 'entity_category': None, - 'entity_id': 'light.hue_white_lamp_3', - 'has_entity_name': False, - 'hidden_by': None, - 'icon': None, - 'labels': list([ - ]), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Hue white lamp', - 'platform': 'homekit_controller', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '00:00:00:00:00:00_6623462379123707_2816', - 'unit_of_measurement': None, - }), - 'state': dict({ - 'attributes': dict({ - 'brightness': None, - 'color_mode': None, - 'friendly_name': 'Hue white lamp', - 'supported_color_modes': list([ - , - ]), - 'supported_features': , - }), - 'entity_id': 'light.hue_white_lamp_3', - 'state': 'off', - }), - }), - ]), - }), dict({ 'device': dict({ 'area_id': None, @@ -12540,7 +12352,6 @@ 'model': 'LWB010', 'name': 'Hue white lamp', 'name_by_user': None, - 'primary_integration': 'homekit_controller', 'serial_number': '6623462379122122', 'suggested_area': None, 'sw_version': '1.46.13', @@ -12654,7 +12465,7 @@ 'identifiers': list([ list([ 'homekit_controller:accessory-id', - '00:00:00:00:00:00:aid:6623462385996792', + '00:00:00:00:00:00:aid:6623462379123707', ]), ]), 'is_new': False, @@ -12664,8 +12475,7 @@ 'model': 'LWB010', 'name': 'Hue white lamp', 'name_by_user': None, - 'primary_integration': 'homekit_controller', - 'serial_number': '6623462385996792', + 'serial_number': '6623462379123707', 'suggested_area': None, 'sw_version': '1.46.13', }), @@ -12683,7 +12493,7 @@ 'disabled_by': None, 'domain': 'button', 'entity_category': , - 'entity_id': 'button.hue_white_lamp_identify_5', + 'entity_id': 'button.hue_white_lamp_identify_3', 'has_entity_name': False, 'hidden_by': None, 'icon': None, @@ -12699,7 +12509,7 @@ 'previous_unique_id': None, 'supported_features': 0, 'translation_key': None, - 'unique_id': '00:00:00:00:00:00_6623462385996792_1_6', + 'unique_id': '00:00:00:00:00:00_6623462379123707_1_6', 'unit_of_measurement': None, }), 'state': dict({ @@ -12707,7 +12517,7 @@ 'device_class': 'identify', 'friendly_name': 'Hue white lamp Identify', }), - 'entity_id': 'button.hue_white_lamp_identify_5', + 'entity_id': 'button.hue_white_lamp_identify_3', 'state': 'unknown', }), }), @@ -12728,7 +12538,7 @@ 'disabled_by': None, 'domain': 'light', 'entity_category': None, - 'entity_id': 'light.hue_white_lamp_5', + 'entity_id': 'light.hue_white_lamp_3', 'has_entity_name': False, 'hidden_by': None, 'icon': None, @@ -12744,7 +12554,7 @@ 'previous_unique_id': None, 'supported_features': 0, 'translation_key': None, - 'unique_id': '00:00:00:00:00:00_6623462385996792_2816', + 'unique_id': '00:00:00:00:00:00_6623462379123707_2816', 'unit_of_measurement': None, }), 'state': dict({ @@ -12757,131 +12567,7 @@ ]), 'supported_features': , }), - 'entity_id': 'light.hue_white_lamp_5', - 'state': 'off', - }), - }), - ]), - }), - dict({ - 'device': dict({ - 'area_id': None, - 'config_entries': list([ - 'TestData', - ]), - 'configuration_url': None, - 'connections': list([ - ]), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': '', - 'identifiers': list([ - list([ - 'homekit_controller:accessory-id', - '00:00:00:00:00:00:aid:6623462383114193', - ]), - ]), - 'is_new': False, - 'labels': list([ - ]), - 'manufacturer': 'Philips', - 'model': 'LWB010', - 'name': 'Hue white lamp', - 'name_by_user': None, - 'primary_integration': 'homekit_controller', - 'serial_number': '6623462383114193', - 'suggested_area': None, - 'sw_version': '1.46.13', - }), - 'entities': list([ - dict({ - 'entry': dict({ - 'aliases': list([ - ]), - 'area_id': None, - 'capabilities': None, - 'categories': dict({ - }), - 'config_entry_id': 'TestData', - 'device_class': None, - 'disabled_by': None, - 'domain': 'button', - 'entity_category': , - 'entity_id': 'button.hue_white_lamp_identify_6', - 'has_entity_name': False, - 'hidden_by': None, - 'icon': None, - 'labels': list([ - ]), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Hue white lamp Identify', - 'platform': 'homekit_controller', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '00:00:00:00:00:00_6623462383114193_1_6', - 'unit_of_measurement': None, - }), - 'state': dict({ - 'attributes': dict({ - 'device_class': 'identify', - 'friendly_name': 'Hue white lamp Identify', - }), - 'entity_id': 'button.hue_white_lamp_identify_6', - 'state': 'unknown', - }), - }), - dict({ - 'entry': dict({ - 'aliases': list([ - ]), - 'area_id': None, - 'capabilities': dict({ - 'supported_color_modes': list([ - , - ]), - }), - 'categories': dict({ - }), - 'config_entry_id': 'TestData', - 'device_class': None, - 'disabled_by': None, - 'domain': 'light', - 'entity_category': None, - 'entity_id': 'light.hue_white_lamp_6', - 'has_entity_name': False, - 'hidden_by': None, - 'icon': None, - 'labels': list([ - ]), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Hue white lamp', - 'platform': 'homekit_controller', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '00:00:00:00:00:00_6623462383114193_2816', - 'unit_of_measurement': None, - }), - 'state': dict({ - 'attributes': dict({ - 'brightness': None, - 'color_mode': None, - 'friendly_name': 'Hue white lamp', - 'supported_color_modes': list([ - , - ]), - 'supported_features': , - }), - 'entity_id': 'light.hue_white_lamp_6', + 'entity_id': 'light.hue_white_lamp_3', 'state': 'off', }), }), @@ -12912,7 +12598,6 @@ 'model': 'LWB010', 'name': 'Hue white lamp', 'name_by_user': None, - 'primary_integration': 'homekit_controller', 'serial_number': '6623462383114163', 'suggested_area': None, 'sw_version': '1.46.13', @@ -13011,6 +12696,252 @@ }), ]), }), + dict({ + 'device': dict({ + 'area_id': None, + 'config_entries': list([ + 'TestData', + ]), + 'configuration_url': None, + 'connections': list([ + ]), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': '', + 'identifiers': list([ + list([ + 'homekit_controller:accessory-id', + '00:00:00:00:00:00:aid:6623462383114193', + ]), + ]), + 'is_new': False, + 'labels': list([ + ]), + 'manufacturer': 'Philips', + 'model': 'LWB010', + 'name': 'Hue white lamp', + 'name_by_user': None, + 'serial_number': '6623462383114193', + 'suggested_area': None, + 'sw_version': '1.46.13', + }), + 'entities': list([ + dict({ + 'entry': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': 'TestData', + 'device_class': None, + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.hue_white_lamp_identify_6', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Hue white lamp Identify', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00_6623462383114193_1_6', + 'unit_of_measurement': None, + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'identify', + 'friendly_name': 'Hue white lamp Identify', + }), + 'entity_id': 'button.hue_white_lamp_identify_6', + 'state': 'unknown', + }), + }), + dict({ + 'entry': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'supported_color_modes': list([ + , + ]), + }), + 'categories': dict({ + }), + 'config_entry_id': 'TestData', + 'device_class': None, + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.hue_white_lamp_6', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Hue white lamp', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00_6623462383114193_2816', + 'unit_of_measurement': None, + }), + 'state': dict({ + 'attributes': dict({ + 'brightness': None, + 'color_mode': None, + 'friendly_name': 'Hue white lamp', + 'supported_color_modes': list([ + , + ]), + 'supported_features': , + }), + 'entity_id': 'light.hue_white_lamp_6', + 'state': 'off', + }), + }), + ]), + }), + dict({ + 'device': dict({ + 'area_id': None, + 'config_entries': list([ + 'TestData', + ]), + 'configuration_url': None, + 'connections': list([ + ]), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': '', + 'identifiers': list([ + list([ + 'homekit_controller:accessory-id', + '00:00:00:00:00:00:aid:6623462385996792', + ]), + ]), + 'is_new': False, + 'labels': list([ + ]), + 'manufacturer': 'Philips', + 'model': 'LWB010', + 'name': 'Hue white lamp', + 'name_by_user': None, + 'serial_number': '6623462385996792', + 'suggested_area': None, + 'sw_version': '1.46.13', + }), + 'entities': list([ + dict({ + 'entry': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': 'TestData', + 'device_class': None, + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.hue_white_lamp_identify_5', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Hue white lamp Identify', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00_6623462385996792_1_6', + 'unit_of_measurement': None, + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'identify', + 'friendly_name': 'Hue white lamp Identify', + }), + 'entity_id': 'button.hue_white_lamp_identify_5', + 'state': 'unknown', + }), + }), + dict({ + 'entry': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'supported_color_modes': list([ + , + ]), + }), + 'categories': dict({ + }), + 'config_entry_id': 'TestData', + 'device_class': None, + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.hue_white_lamp_5', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Hue white lamp', + 'platform': 'homekit_controller', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00_6623462385996792_2816', + 'unit_of_measurement': None, + }), + 'state': dict({ + 'attributes': dict({ + 'brightness': None, + 'color_mode': None, + 'friendly_name': 'Hue white lamp', + 'supported_color_modes': list([ + , + ]), + 'supported_features': , + }), + 'entity_id': 'light.hue_white_lamp_5', + 'state': 'off', + }), + }), + ]), + }), dict({ 'device': dict({ 'area_id': None, @@ -13036,7 +12967,6 @@ 'model': 'BSB002', 'name': 'Philips hue - 482544', 'name_by_user': None, - 'primary_integration': 'homekit_controller', 'serial_number': '123456', 'suggested_area': None, 'sw_version': '1.32.1932126170', @@ -13114,7 +13044,6 @@ 'model': 'LS1', 'name': 'Koogeek-LS1-20833F', 'name_by_user': None, - 'primary_integration': 'homekit_controller', 'serial_number': 'AAAA011111111111', 'suggested_area': None, 'sw_version': '2.2.15', @@ -13257,7 +13186,6 @@ 'model': 'P1EU', 'name': 'Koogeek-P1-A00AA0', 'name_by_user': None, - 'primary_integration': 'homekit_controller', 'serial_number': 'EUCP03190xxxxx48', 'suggested_area': None, 'sw_version': '2.3.7', @@ -13421,7 +13349,6 @@ 'model': 'KH02CN', 'name': 'Koogeek-SW2-187A91', 'name_by_user': None, - 'primary_integration': 'homekit_controller', 'serial_number': 'CNNT061751001372', 'suggested_area': None, 'sw_version': '1.0.3', @@ -13624,7 +13551,6 @@ 'model': 'E30 2B', 'name': 'Lennox', 'name_by_user': None, - 'primary_integration': 'homekit_controller', 'serial_number': 'XXXXXXXX', 'suggested_area': None, 'sw_version': '3.40.XX', @@ -13905,7 +13831,6 @@ 'model': 'OLED55B9PUA', 'name': 'LG webOS TV AF80', 'name_by_user': None, - 'primary_integration': 'homekit_controller', 'serial_number': '999AAAAAA999', 'suggested_area': None, 'sw_version': '04.71.04', @@ -14085,7 +14010,6 @@ 'model': 'PD-FSQN-XX', 'name': 'Caséta® Wireless Fan Speed Control', 'name_by_user': None, - 'primary_integration': 'homekit_controller', 'serial_number': '39024290', 'suggested_area': None, 'sw_version': '001.005', @@ -14206,7 +14130,6 @@ 'model': 'L-BDG2-WH', 'name': 'Smart Bridge 2', 'name_by_user': None, - 'primary_integration': 'homekit_controller', 'serial_number': '12344331', 'suggested_area': None, 'sw_version': '08.08', @@ -14284,7 +14207,6 @@ 'model': 'MSS425F', 'name': 'MSS425F-15cc', 'name_by_user': None, - 'primary_integration': 'homekit_controller', 'serial_number': 'HH41234', 'suggested_area': None, 'sw_version': '4.2.3', @@ -14562,7 +14484,6 @@ 'model': 'MSS565', 'name': 'MSS565-28da', 'name_by_user': None, - 'primary_integration': 'homekit_controller', 'serial_number': 'BB1121', 'suggested_area': None, 'sw_version': '4.1.9', @@ -14690,7 +14611,6 @@ 'model': 'v1', 'name': 'Mysa-85dda9', 'name_by_user': None, - 'primary_integration': 'homekit_controller', 'serial_number': 'AAAAAAA000', 'suggested_area': None, 'sw_version': '2.8.1', @@ -15019,7 +14939,6 @@ 'model': 'NL55', 'name': 'Nanoleaf Strip 3B32', 'name_by_user': None, - 'primary_integration': 'homekit_controller', 'serial_number': 'AAAA011111111111', 'suggested_area': None, 'sw_version': '1.4.40', @@ -15290,7 +15209,6 @@ 'model': 'Netatmo Doorbell', 'name': 'Netatmo-Doorbell-g738658', 'name_by_user': None, - 'primary_integration': 'homekit_controller', 'serial_number': 'g738658', 'suggested_area': None, 'sw_version': '80.0.0', @@ -15583,7 +15501,6 @@ 'model': 'Smart CO Alarm', 'name': 'Smart CO Alarm', 'name_by_user': None, - 'primary_integration': 'homekit_controller', 'serial_number': '1234', 'suggested_area': None, 'sw_version': '1.0.3', @@ -15743,7 +15660,6 @@ 'model': 'Healthy Home Coach', 'name': 'Healthy Home Coach', 'name_by_user': None, - 'primary_integration': 'homekit_controller', 'serial_number': 'AAAAAAAAAAAAA', 'suggested_area': None, 'sw_version': '59', @@ -16045,7 +15961,6 @@ 'model': 'SPK5 Pro', 'name': 'RainMachine-00ce4a', 'name_by_user': None, - 'primary_integration': 'homekit_controller', 'serial_number': '00aa0000aa0a', 'suggested_area': None, 'sw_version': '1.0.4', @@ -16467,7 +16382,6 @@ 'model': 'RYSE Shade', 'name': 'Master Bath South', 'name_by_user': None, - 'primary_integration': 'homekit_controller', 'serial_number': '1.0.0', 'suggested_area': None, 'sw_version': '3.0.8', @@ -16629,7 +16543,6 @@ 'model': 'RYSE SmartBridge', 'name': 'RYSE SmartBridge', 'name_by_user': None, - 'primary_integration': 'homekit_controller', 'serial_number': '0101.3521.0436', 'suggested_area': None, 'sw_version': '1.3.0', @@ -16703,7 +16616,6 @@ 'model': 'RYSE Shade', 'name': 'RYSE SmartShade', 'name_by_user': None, - 'primary_integration': 'homekit_controller', 'serial_number': '', 'suggested_area': None, 'sw_version': '', @@ -16869,7 +16781,6 @@ 'model': 'RYSE Shade', 'name': 'BR Left', 'name_by_user': None, - 'primary_integration': 'homekit_controller', 'serial_number': '1.0.0', 'suggested_area': None, 'sw_version': '3.0.8', @@ -17031,7 +16942,6 @@ 'model': 'RYSE Shade', 'name': 'LR Left', 'name_by_user': None, - 'primary_integration': 'homekit_controller', 'serial_number': '1.0.0', 'suggested_area': None, 'sw_version': '3.0.8', @@ -17193,7 +17103,6 @@ 'model': 'RYSE Shade', 'name': 'LR Right', 'name_by_user': None, - 'primary_integration': 'homekit_controller', 'serial_number': '1.0.0', 'suggested_area': None, 'sw_version': '3.0.8', @@ -17355,7 +17264,6 @@ 'model': 'RYSE SmartBridge', 'name': 'RYSE SmartBridge', 'name_by_user': None, - 'primary_integration': 'homekit_controller', 'serial_number': '0401.3521.0679', 'suggested_area': None, 'sw_version': '1.3.0', @@ -17429,7 +17337,6 @@ 'model': 'RYSE Shade', 'name': 'RZSS', 'name_by_user': None, - 'primary_integration': 'homekit_controller', 'serial_number': '1.0.0', 'suggested_area': None, 'sw_version': '3.0.8', @@ -17595,7 +17502,6 @@ 'model': 'BE479CAM619', 'name': 'SENSE ', 'name_by_user': None, - 'primary_integration': 'homekit_controller', 'serial_number': 'AAAAAAA000', 'suggested_area': None, 'sw_version': '004.027.000', @@ -17714,7 +17620,6 @@ 'model': 'SIMPLEconnect', 'name': 'SIMPLEconnect Fan-06F674', 'name_by_user': None, - 'primary_integration': 'homekit_controller', 'serial_number': '1234567890abcd', 'suggested_area': None, 'sw_version': '', @@ -17890,7 +17795,6 @@ 'model': 'VELUX Gateway', 'name': 'VELUX Gateway', 'name_by_user': None, - 'primary_integration': 'homekit_controller', 'serial_number': 'a1a11a1', 'suggested_area': None, 'sw_version': '70', @@ -17964,7 +17868,6 @@ 'model': 'VELUX Sensor', 'name': 'VELUX Sensor', 'name_by_user': None, - 'primary_integration': 'homekit_controller', 'serial_number': 'a11b111', 'suggested_area': None, 'sw_version': '16', @@ -18173,7 +18076,6 @@ 'model': 'VELUX Window', 'name': 'VELUX Window', 'name_by_user': None, - 'primary_integration': 'homekit_controller', 'serial_number': '1111111a114a111a', 'suggested_area': None, 'sw_version': '48', @@ -18294,7 +18196,6 @@ 'model': 'Flowerbud', 'name': 'VOCOlinc-Flowerbud-0d324b', 'name_by_user': None, - 'primary_integration': 'homekit_controller', 'serial_number': 'AM01121849000327', 'suggested_area': None, 'sw_version': '3.121.2', @@ -18599,7 +18500,6 @@ 'model': 'VP3', 'name': 'VOCOlinc-VP3-123456', 'name_by_user': None, - 'primary_integration': 'homekit_controller', 'serial_number': 'EU0121203xxxxx07', 'suggested_area': None, 'sw_version': '1.101.2', diff --git a/tests/components/homekit_controller/test_connection.py b/tests/components/homekit_controller/test_connection.py index 0a77509d675..0f2cdb7c9db 100644 --- a/tests/components/homekit_controller/test_connection.py +++ b/tests/components/homekit_controller/test_connection.py @@ -118,7 +118,7 @@ async def test_migrate_device_id_no_serial_skip_if_other_owner( bridge = device_registry.async_get(bridge.id) assert bridge.identifiers == variant.before - assert bridge.config_entries == {entry.entry_id} + assert bridge.config_entries == [entry.entry_id] @pytest.mark.parametrize("variant", DEVICE_MIGRATION_TESTS) diff --git a/tests/components/homewizard/snapshots/test_button.ambr b/tests/components/homewizard/snapshots/test_button.ambr index 47b6a889900..5ab108d344c 100644 --- a/tests/components/homewizard/snapshots/test_button.ambr +++ b/tests/components/homewizard/snapshots/test_button.ambr @@ -74,7 +74,6 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', diff --git a/tests/components/homewizard/snapshots/test_number.ambr b/tests/components/homewizard/snapshots/test_number.ambr index ff1f22a4336..a9c9e45098d 100644 --- a/tests/components/homewizard/snapshots/test_number.ambr +++ b/tests/components/homewizard/snapshots/test_number.ambr @@ -83,7 +83,6 @@ 'model': 'HWE-SKT', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.03', @@ -174,7 +173,6 @@ 'model': 'HWE-SKT', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.07', diff --git a/tests/components/homewizard/snapshots/test_sensor.ambr b/tests/components/homewizard/snapshots/test_sensor.ambr index 7f402cd7872..5e8ddc0d6be 100644 --- a/tests/components/homewizard/snapshots/test_sensor.ambr +++ b/tests/components/homewizard/snapshots/test_sensor.ambr @@ -60,7 +60,6 @@ 'model': 'HWE-KWH1', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -146,7 +145,6 @@ 'model': 'HWE-KWH1', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -232,7 +230,6 @@ 'model': 'HWE-KWH1', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -318,7 +315,6 @@ 'model': 'HWE-KWH1', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -404,7 +400,6 @@ 'model': 'HWE-KWH1', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -490,7 +485,6 @@ 'model': 'HWE-KWH1', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -579,7 +573,6 @@ 'model': 'HWE-KWH1', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -665,7 +658,6 @@ 'model': 'HWE-KWH1', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -751,7 +743,6 @@ 'model': 'HWE-KWH1', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -837,7 +828,6 @@ 'model': 'HWE-KWH1', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -918,7 +908,6 @@ 'model': 'HWE-KWH1', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -1003,7 +992,6 @@ 'model': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -1089,7 +1077,6 @@ 'model': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -1175,7 +1162,6 @@ 'model': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -1261,7 +1247,6 @@ 'model': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -1347,7 +1332,6 @@ 'model': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -1433,7 +1417,6 @@ 'model': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -1519,7 +1502,6 @@ 'model': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -1605,7 +1587,6 @@ 'model': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -1691,7 +1672,6 @@ 'model': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -1777,7 +1757,6 @@ 'model': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -1863,7 +1842,6 @@ 'model': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -1949,7 +1927,6 @@ 'model': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -2038,7 +2015,6 @@ 'model': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -2124,7 +2100,6 @@ 'model': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -2210,7 +2185,6 @@ 'model': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -2296,7 +2270,6 @@ 'model': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -2385,7 +2358,6 @@ 'model': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -2474,7 +2446,6 @@ 'model': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -2563,7 +2534,6 @@ 'model': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -2649,7 +2619,6 @@ 'model': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -2735,7 +2704,6 @@ 'model': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -2821,7 +2789,6 @@ 'model': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -2907,7 +2874,6 @@ 'model': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -2993,7 +2959,6 @@ 'model': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -3079,7 +3044,6 @@ 'model': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -3165,7 +3129,6 @@ 'model': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -3246,7 +3209,6 @@ 'model': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -3331,7 +3293,6 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -3414,7 +3375,6 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -3500,7 +3460,6 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -3586,7 +3545,6 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -3672,7 +3630,6 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -3753,7 +3710,6 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -3839,7 +3795,6 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -3925,7 +3880,6 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -4011,7 +3965,6 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -4097,7 +4050,6 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -4183,7 +4135,6 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -4269,7 +4220,6 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -4355,7 +4305,6 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -4441,7 +4390,6 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -4527,7 +4475,6 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -4613,7 +4560,6 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -4699,7 +4645,6 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -4780,7 +4725,6 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -4863,7 +4807,6 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -4952,7 +4895,6 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -5033,7 +4975,6 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -5122,7 +5063,6 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -5211,7 +5151,6 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -5300,7 +5239,6 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -5381,7 +5319,6 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -5462,7 +5399,6 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -5557,7 +5493,6 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -5643,7 +5578,6 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -5729,7 +5663,6 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -5815,7 +5748,6 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -5901,7 +5833,6 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -5982,7 +5913,6 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -6063,7 +5993,6 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -6144,7 +6073,6 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -6225,7 +6153,6 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -6306,7 +6233,6 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -6387,7 +6313,6 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -6472,7 +6397,6 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -6553,7 +6477,6 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -6634,7 +6557,6 @@ 'model': 'HWE-P1', 'name': 'Gas meter', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': 'gas_meter_G001', 'suggested_area': None, 'sw_version': None, @@ -6716,7 +6638,6 @@ 'model': 'HWE-P1', 'name': 'Heat meter', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': 'heat_meter_H001', 'suggested_area': None, 'sw_version': None, @@ -6798,7 +6719,6 @@ 'model': 'HWE-P1', 'name': 'Inlet heat meter', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': 'inlet_heat_meter_IH001', 'suggested_area': None, 'sw_version': None, @@ -6879,7 +6799,6 @@ 'model': 'HWE-P1', 'name': 'Warm water meter', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': 'warm_water_meter_WW001', 'suggested_area': None, 'sw_version': None, @@ -6961,7 +6880,6 @@ 'model': 'HWE-P1', 'name': 'Water meter', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': 'water_meter_W001', 'suggested_area': None, 'sw_version': None, @@ -7047,7 +6965,6 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -7130,7 +7047,6 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -7216,7 +7132,6 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -7302,7 +7217,6 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -7388,7 +7302,6 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -7469,7 +7382,6 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -7555,7 +7467,6 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -7641,7 +7552,6 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -7727,7 +7637,6 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -7813,7 +7722,6 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -7899,7 +7807,6 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -7985,7 +7892,6 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -8071,7 +7977,6 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -8157,7 +8062,6 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -8243,7 +8147,6 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -8329,7 +8232,6 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -8415,7 +8317,6 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -8496,7 +8397,6 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -8579,7 +8479,6 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -8668,7 +8567,6 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -8749,7 +8647,6 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -8838,7 +8735,6 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -8927,7 +8823,6 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -9016,7 +8911,6 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -9097,7 +8991,6 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -9178,7 +9071,6 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -9273,7 +9165,6 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -9359,7 +9250,6 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -9445,7 +9335,6 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -9531,7 +9420,6 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -9617,7 +9505,6 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -9698,7 +9585,6 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -9779,7 +9665,6 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -9860,7 +9745,6 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -9941,7 +9825,6 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -10022,7 +9905,6 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -10103,7 +9985,6 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -10188,7 +10069,6 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -10269,7 +10149,6 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -10350,7 +10229,6 @@ 'model': 'HWE-P1', 'name': 'Gas meter', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': 'gas_meter_\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', 'suggested_area': None, 'sw_version': None, @@ -10432,7 +10310,6 @@ 'model': 'HWE-P1', 'name': 'Heat meter', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': 'heat_meter_\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', 'suggested_area': None, 'sw_version': None, @@ -10514,7 +10391,6 @@ 'model': 'HWE-P1', 'name': 'Inlet heat meter', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': 'inlet_heat_meter_\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', 'suggested_area': None, 'sw_version': None, @@ -10595,7 +10471,6 @@ 'model': 'HWE-P1', 'name': 'Warm water meter', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': 'warm_water_meter_\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', 'suggested_area': None, 'sw_version': None, @@ -10677,7 +10552,6 @@ 'model': 'HWE-P1', 'name': 'Water meter', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': 'water_meter_\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', 'suggested_area': None, 'sw_version': None, @@ -10763,7 +10637,6 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -10846,7 +10719,6 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -10932,7 +10804,6 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -11018,7 +10889,6 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -11104,7 +10974,6 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -11190,7 +11059,6 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -11276,7 +11144,6 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -11362,7 +11229,6 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -11448,7 +11314,6 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -11534,7 +11399,6 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -11620,7 +11484,6 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -11706,7 +11569,6 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -11792,7 +11654,6 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -11878,7 +11739,6 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -11964,7 +11824,6 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -12050,7 +11909,6 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -12131,7 +11989,6 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -12220,7 +12077,6 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -12301,7 +12157,6 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -12390,7 +12245,6 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -12479,7 +12333,6 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -12568,7 +12421,6 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -12654,7 +12506,6 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -12740,7 +12591,6 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -12826,7 +12676,6 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -12912,7 +12761,6 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -12993,7 +12841,6 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -13074,7 +12921,6 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -13155,7 +13001,6 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -13236,7 +13081,6 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -13317,7 +13161,6 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -13398,7 +13241,6 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -13483,7 +13325,6 @@ 'model': 'HWE-SKT', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.03', @@ -13569,7 +13410,6 @@ 'model': 'HWE-SKT', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.03', @@ -13655,7 +13495,6 @@ 'model': 'HWE-SKT', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.03', @@ -13744,7 +13583,6 @@ 'model': 'HWE-SKT', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.03', @@ -13833,7 +13671,6 @@ 'model': 'HWE-SKT', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.03', @@ -13914,7 +13751,6 @@ 'model': 'HWE-SKT', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.03', @@ -13999,7 +13835,6 @@ 'model': 'HWE-SKT', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.07', @@ -14085,7 +13920,6 @@ 'model': 'HWE-SKT', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.07', @@ -14171,7 +14005,6 @@ 'model': 'HWE-SKT', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.07', @@ -14257,7 +14090,6 @@ 'model': 'HWE-SKT', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.07', @@ -14343,7 +14175,6 @@ 'model': 'HWE-SKT', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.07', @@ -14429,7 +14260,6 @@ 'model': 'HWE-SKT', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.07', @@ -14518,7 +14348,6 @@ 'model': 'HWE-SKT', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.07', @@ -14604,7 +14433,6 @@ 'model': 'HWE-SKT', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.07', @@ -14693,7 +14521,6 @@ 'model': 'HWE-SKT', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.07', @@ -14779,7 +14606,6 @@ 'model': 'HWE-SKT', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.07', @@ -14865,7 +14691,6 @@ 'model': 'HWE-SKT', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.07', @@ -14946,7 +14771,6 @@ 'model': 'HWE-SKT', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.07', @@ -15031,7 +14855,6 @@ 'model': 'HWE-WTR', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '2.03', @@ -15117,7 +14940,6 @@ 'model': 'HWE-WTR', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '2.03', @@ -15202,7 +15024,6 @@ 'model': 'HWE-WTR', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '2.03', @@ -15283,7 +15104,6 @@ 'model': 'HWE-WTR', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '2.03', @@ -15368,7 +15188,6 @@ 'model': 'SDM230-wifi', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -15454,7 +15273,6 @@ 'model': 'SDM230-wifi', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -15540,7 +15358,6 @@ 'model': 'SDM230-wifi', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -15626,7 +15443,6 @@ 'model': 'SDM230-wifi', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -15712,7 +15528,6 @@ 'model': 'SDM230-wifi', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -15798,7 +15613,6 @@ 'model': 'SDM230-wifi', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -15887,7 +15701,6 @@ 'model': 'SDM230-wifi', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -15973,7 +15786,6 @@ 'model': 'SDM230-wifi', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -16059,7 +15871,6 @@ 'model': 'SDM230-wifi', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -16145,7 +15956,6 @@ 'model': 'SDM230-wifi', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -16226,7 +16036,6 @@ 'model': 'SDM230-wifi', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -16311,7 +16120,6 @@ 'model': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -16397,7 +16205,6 @@ 'model': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -16483,7 +16290,6 @@ 'model': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -16569,7 +16375,6 @@ 'model': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -16655,7 +16460,6 @@ 'model': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -16741,7 +16545,6 @@ 'model': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -16827,7 +16630,6 @@ 'model': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -16913,7 +16715,6 @@ 'model': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -16999,7 +16800,6 @@ 'model': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -17085,7 +16885,6 @@ 'model': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -17171,7 +16970,6 @@ 'model': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -17257,7 +17055,6 @@ 'model': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -17346,7 +17143,6 @@ 'model': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -17432,7 +17228,6 @@ 'model': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -17518,7 +17313,6 @@ 'model': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -17604,7 +17398,6 @@ 'model': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -17693,7 +17486,6 @@ 'model': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -17782,7 +17574,6 @@ 'model': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -17871,7 +17662,6 @@ 'model': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -17957,7 +17747,6 @@ 'model': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -18043,7 +17832,6 @@ 'model': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -18129,7 +17917,6 @@ 'model': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -18215,7 +18002,6 @@ 'model': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -18301,7 +18087,6 @@ 'model': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -18387,7 +18172,6 @@ 'model': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -18473,7 +18257,6 @@ 'model': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -18554,7 +18337,6 @@ 'model': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', diff --git a/tests/components/homewizard/snapshots/test_switch.ambr b/tests/components/homewizard/snapshots/test_switch.ambr index 2834999a9ba..99a5bcab6cb 100644 --- a/tests/components/homewizard/snapshots/test_switch.ambr +++ b/tests/components/homewizard/snapshots/test_switch.ambr @@ -73,7 +73,6 @@ 'model': 'HWE-KWH1', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -154,7 +153,6 @@ 'model': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -236,7 +234,6 @@ 'model': 'HWE-SKT', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.03', @@ -317,7 +314,6 @@ 'model': 'HWE-SKT', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.03', @@ -398,7 +394,6 @@ 'model': 'HWE-SKT', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.03', @@ -480,7 +475,6 @@ 'model': 'HWE-SKT', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.07', @@ -561,7 +555,6 @@ 'model': 'HWE-SKT', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.07', @@ -642,7 +635,6 @@ 'model': 'HWE-SKT', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '4.07', @@ -723,7 +715,6 @@ 'model': 'HWE-WTR', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '2.03', @@ -804,7 +795,6 @@ 'model': 'SDM230-wifi', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -885,7 +875,6 @@ 'model': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, - 'primary_integration': 'homewizard', 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', diff --git a/tests/components/husqvarna_automower/snapshots/test_init.ambr b/tests/components/husqvarna_automower/snapshots/test_init.ambr index 07cab28b24e..c3a7191b4b9 100644 --- a/tests/components/husqvarna_automower/snapshots/test_init.ambr +++ b/tests/components/husqvarna_automower/snapshots/test_init.ambr @@ -23,7 +23,6 @@ 'model': '450XH-TEST', 'name': 'Test Mower 1', 'name_by_user': None, - 'primary_integration': 'husqvarna_automower', 'serial_number': 123, 'suggested_area': 'Garden', 'sw_version': None, diff --git a/tests/components/hyperion/test_camera.py b/tests/components/hyperion/test_camera.py index 0169759f328..41b66f4ad4a 100644 --- a/tests/components/hyperion/test_camera.py +++ b/tests/components/hyperion/test_camera.py @@ -198,7 +198,7 @@ async def test_device_info( device = device_registry.async_get_device(identifiers={(DOMAIN, device_id)}) assert device - assert device.config_entries == {TEST_CONFIG_ENTRY_ID} + assert device.config_entries == [TEST_CONFIG_ENTRY_ID] assert device.identifiers == {(DOMAIN, device_id)} assert device.manufacturer == HYPERION_MANUFACTURER_NAME assert device.model == HYPERION_MODEL_NAME diff --git a/tests/components/hyperion/test_light.py b/tests/components/hyperion/test_light.py index e1e7711e702..b7aef3ac2ac 100644 --- a/tests/components/hyperion/test_light.py +++ b/tests/components/hyperion/test_light.py @@ -803,7 +803,7 @@ async def test_device_info( device = device_registry.async_get_device(identifiers={(DOMAIN, device_id)}) assert device - assert device.config_entries == {TEST_CONFIG_ENTRY_ID} + assert device.config_entries == [TEST_CONFIG_ENTRY_ID] assert device.identifiers == {(DOMAIN, device_id)} assert device.manufacturer == HYPERION_MANUFACTURER_NAME assert device.model == HYPERION_MODEL_NAME diff --git a/tests/components/hyperion/test_sensor.py b/tests/components/hyperion/test_sensor.py index 5ace34eaac0..bc58c07ac7b 100644 --- a/tests/components/hyperion/test_sensor.py +++ b/tests/components/hyperion/test_sensor.py @@ -66,7 +66,7 @@ async def test_device_info( device = device_registry.async_get_device(identifiers={(DOMAIN, device_identifer)}) assert device - assert device.config_entries == {TEST_CONFIG_ENTRY_ID} + assert device.config_entries == [TEST_CONFIG_ENTRY_ID] assert device.identifiers == {(DOMAIN, device_identifer)} assert device.manufacturer == HYPERION_MANUFACTURER_NAME assert device.model == HYPERION_MODEL_NAME diff --git a/tests/components/hyperion/test_switch.py b/tests/components/hyperion/test_switch.py index da458820c81..17a1872f832 100644 --- a/tests/components/hyperion/test_switch.py +++ b/tests/components/hyperion/test_switch.py @@ -170,7 +170,7 @@ async def test_device_info( device = device_registry.async_get_device(identifiers={(DOMAIN, device_identifer)}) assert device - assert device.config_entries == {TEST_CONFIG_ENTRY_ID} + assert device.config_entries == [TEST_CONFIG_ENTRY_ID] assert device.identifiers == {(DOMAIN, device_identifer)} assert device.manufacturer == HYPERION_MANUFACTURER_NAME assert device.model == HYPERION_MODEL_NAME diff --git a/tests/components/ista_ecotrend/snapshots/test_init.ambr b/tests/components/ista_ecotrend/snapshots/test_init.ambr index 7cc44872071..a9d13510b54 100644 --- a/tests/components/ista_ecotrend/snapshots/test_init.ambr +++ b/tests/components/ista_ecotrend/snapshots/test_init.ambr @@ -23,7 +23,6 @@ 'model': 'ista EcoTrend', 'name': 'Luxemburger Str. 1', 'name_by_user': None, - 'primary_integration': 'ista_ecotrend', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -54,7 +53,6 @@ 'model': 'ista EcoTrend', 'name': 'Bahnhofsstr. 1A', 'name_by_user': None, - 'primary_integration': 'ista_ecotrend', 'serial_number': None, 'suggested_area': None, 'sw_version': None, diff --git a/tests/components/kitchen_sink/snapshots/test_switch.ambr b/tests/components/kitchen_sink/snapshots/test_switch.ambr index 2f928ddc430..1cd903a59d6 100644 --- a/tests/components/kitchen_sink/snapshots/test_switch.ambr +++ b/tests/components/kitchen_sink/snapshots/test_switch.ambr @@ -69,7 +69,6 @@ 'model': None, 'name': 'Outlet 1', 'name_by_user': None, - 'primary_integration': 'kitchen_sink', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -100,7 +99,6 @@ 'model': None, 'name': 'Power strip with 2 sockets', 'name_by_user': None, - 'primary_integration': None, 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -177,7 +175,6 @@ 'model': None, 'name': 'Outlet 2', 'name_by_user': None, - 'primary_integration': 'kitchen_sink', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -208,7 +205,6 @@ 'model': None, 'name': 'Power strip with 2 sockets', 'name_by_user': None, - 'primary_integration': None, 'serial_number': None, 'suggested_area': None, 'sw_version': None, diff --git a/tests/components/lamarzocco/snapshots/test_switch.ambr b/tests/components/lamarzocco/snapshots/test_switch.ambr index 162fade77d6..09864be1d5c 100644 --- a/tests/components/lamarzocco/snapshots/test_switch.ambr +++ b/tests/components/lamarzocco/snapshots/test_switch.ambr @@ -115,7 +115,6 @@ 'model': , 'name': 'GS01234', 'name_by_user': None, - 'primary_integration': 'lamarzocco', 'serial_number': 'GS01234', 'suggested_area': None, 'sw_version': '1.40', diff --git a/tests/components/lifx/test_migration.py b/tests/components/lifx/test_migration.py index 0604ee1c8a7..62018790906 100644 --- a/tests/components/lifx/test_migration.py +++ b/tests/components/lifx/test_migration.py @@ -65,7 +65,7 @@ async def test_migration_device_online_end_to_end( assert migrated_entry is not None - assert device.config_entries == {migrated_entry.entry_id} + assert device.config_entries == [migrated_entry.entry_id] assert light_entity_reg.config_entry_id == migrated_entry.entry_id assert er.async_entries_for_config_entry(entity_reg, config_entry) == [] @@ -195,7 +195,7 @@ async def test_migration_device_online_end_to_end_after_downgrade( async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=20)) await hass.async_block_till_done() - assert device.config_entries == {config_entry.entry_id} + assert device.config_entries == [config_entry.entry_id] assert light_entity_reg.config_entry_id == config_entry.entry_id assert er.async_entries_for_config_entry(entity_reg, config_entry) == [] @@ -276,7 +276,7 @@ async def test_migration_device_online_end_to_end_ignores_other_devices( assert new_entry is not None assert legacy_entry is None - assert device.config_entries == {legacy_config_entry.entry_id} + assert device.config_entries == [legacy_config_entry.entry_id] assert light_entity_reg.config_entry_id == legacy_config_entry.entry_id assert ignored_entity_reg.config_entry_id == other_domain_config_entry.entry_id assert garbage_entity_reg.config_entry_id == legacy_config_entry.entry_id diff --git a/tests/components/lutron_caseta/test_logbook.py b/tests/components/lutron_caseta/test_logbook.py index b6e8840c85c..51c96b9d9a9 100644 --- a/tests/components/lutron_caseta/test_logbook.py +++ b/tests/components/lutron_caseta/test_logbook.py @@ -111,7 +111,7 @@ async def test_humanify_lutron_caseta_button_event_integration_not_loaded( await hass.async_block_till_done() for device in device_registry.devices.values(): - if device.config_entries == {config_entry.entry_id}: + if device.config_entries == [config_entry.entry_id]: dr_device_id = device.id break diff --git a/tests/components/motioneye/test_camera.py b/tests/components/motioneye/test_camera.py index 0f3a7d6f904..ccbdc022495 100644 --- a/tests/components/motioneye/test_camera.py +++ b/tests/components/motioneye/test_camera.py @@ -339,7 +339,7 @@ async def test_device_info( device = device_registry.async_get_device(identifiers={device_identifier}) assert device - assert device.config_entries == {TEST_CONFIG_ENTRY_ID} + assert device.config_entries == [TEST_CONFIG_ENTRY_ID] assert device.identifiers == {device_identifier} assert device.manufacturer == MOTIONEYE_MANUFACTURER assert device.model == MOTIONEYE_MANUFACTURER diff --git a/tests/components/mqtt/test_discovery.py b/tests/components/mqtt/test_discovery.py index 020ab4a09a9..911d205269c 100644 --- a/tests/components/mqtt/test_discovery.py +++ b/tests/components/mqtt/test_discovery.py @@ -976,10 +976,10 @@ async def test_cleanup_device_multiple_config_entries( connections={("mac", "12:34:56:AB:CD:EF")} ) assert device_entry is not None - assert device_entry.config_entries == { - mqtt_config_entry.entry_id, + assert device_entry.config_entries == [ config_entry.entry_id, - } + mqtt_config_entry.entry_id, + ] entity_entry = entity_registry.async_get("sensor.none_mqtt_sensor") assert entity_entry is not None @@ -1002,7 +1002,7 @@ async def test_cleanup_device_multiple_config_entries( ) assert device_entry is not None entity_entry = entity_registry.async_get("sensor.none_mqtt_sensor") - assert device_entry.config_entries == {config_entry.entry_id} + assert device_entry.config_entries == [config_entry.entry_id] assert entity_entry is None # Verify state is removed @@ -1070,10 +1070,10 @@ async def test_cleanup_device_multiple_config_entries_mqtt( connections={("mac", "12:34:56:AB:CD:EF")} ) assert device_entry is not None - assert device_entry.config_entries == { - mqtt_config_entry.entry_id, + assert device_entry.config_entries == [ config_entry.entry_id, - } + mqtt_config_entry.entry_id, + ] entity_entry = entity_registry.async_get("sensor.none_mqtt_sensor") assert entity_entry is not None @@ -1094,7 +1094,7 @@ async def test_cleanup_device_multiple_config_entries_mqtt( ) assert device_entry is not None entity_entry = entity_registry.async_get("sensor.none_mqtt_sensor") - assert device_entry.config_entries == {config_entry.entry_id} + assert device_entry.config_entries == [config_entry.entry_id] assert entity_entry is None # Verify state is removed diff --git a/tests/components/mqtt/test_tag.py b/tests/components/mqtt/test_tag.py index 0d0765258f2..e70c06c2c4a 100644 --- a/tests/components/mqtt/test_tag.py +++ b/tests/components/mqtt/test_tag.py @@ -587,7 +587,7 @@ async def test_cleanup_tag( identifiers={("mqtt", "helloworld")} ) assert device_entry1 is not None - assert device_entry1.config_entries == {config_entry.entry_id, mqtt_entry.entry_id} + assert device_entry1.config_entries == [config_entry.entry_id, mqtt_entry.entry_id] device_entry2 = device_registry.async_get_device(identifiers={("mqtt", "hejhopp")}) assert device_entry2 is not None @@ -599,7 +599,7 @@ async def test_cleanup_tag( identifiers={("mqtt", "helloworld")} ) assert device_entry1 is not None - assert device_entry1.config_entries == {mqtt_entry.entry_id} + assert device_entry1.config_entries == [mqtt_entry.entry_id] device_entry2 = device_registry.async_get_device(identifiers={("mqtt", "hejhopp")}) assert device_entry2 is not None mqtt_mock.async_publish.assert_not_called() diff --git a/tests/components/netatmo/snapshots/test_init.ambr b/tests/components/netatmo/snapshots/test_init.ambr index f844e05e94b..8f4b357fc5f 100644 --- a/tests/components/netatmo/snapshots/test_init.ambr +++ b/tests/components/netatmo/snapshots/test_init.ambr @@ -23,7 +23,6 @@ 'model': 'Roller Shutter', 'name': 'Entrance Blinds', 'name_by_user': None, - 'primary_integration': 'netatmo', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -54,7 +53,6 @@ 'model': 'Orientable Shutter', 'name': 'Bubendorff blind', 'name_by_user': None, - 'primary_integration': 'netatmo', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -85,7 +83,6 @@ 'model': '2 wire light switch/dimmer', 'name': 'Unknown 00:11:22:33:00:11:45:fe', 'name_by_user': None, - 'primary_integration': 'netatmo', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -116,7 +113,6 @@ 'model': 'Smarther with Netatmo', 'name': 'Corridor', 'name_by_user': None, - 'primary_integration': 'netatmo', 'serial_number': None, 'suggested_area': 'Corridor', 'sw_version': None, @@ -147,7 +143,6 @@ 'model': 'Connected Energy Meter', 'name': 'Consumption meter', 'name_by_user': None, - 'primary_integration': 'netatmo', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -178,7 +173,6 @@ 'model': 'Light switch/dimmer with neutral', 'name': 'Bathroom light', 'name_by_user': None, - 'primary_integration': 'netatmo', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -209,7 +203,6 @@ 'model': 'Connected Ecometer', 'name': 'Line 1', 'name_by_user': None, - 'primary_integration': 'netatmo', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -240,7 +233,6 @@ 'model': 'Connected Ecometer', 'name': 'Line 2', 'name_by_user': None, - 'primary_integration': 'netatmo', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -271,7 +263,6 @@ 'model': 'Connected Ecometer', 'name': 'Line 3', 'name_by_user': None, - 'primary_integration': 'netatmo', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -302,7 +293,6 @@ 'model': 'Connected Ecometer', 'name': 'Line 4', 'name_by_user': None, - 'primary_integration': 'netatmo', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -333,7 +323,6 @@ 'model': 'Connected Ecometer', 'name': 'Line 5', 'name_by_user': None, - 'primary_integration': 'netatmo', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -364,7 +353,6 @@ 'model': 'Connected Ecometer', 'name': 'Total', 'name_by_user': None, - 'primary_integration': 'netatmo', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -395,7 +383,6 @@ 'model': 'Connected Ecometer', 'name': 'Gas', 'name_by_user': None, - 'primary_integration': 'netatmo', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -426,7 +413,6 @@ 'model': 'Connected Ecometer', 'name': 'Hot water', 'name_by_user': None, - 'primary_integration': 'netatmo', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -457,7 +443,6 @@ 'model': 'Connected Ecometer', 'name': 'Cold water', 'name_by_user': None, - 'primary_integration': 'netatmo', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -488,7 +473,6 @@ 'model': 'Connected Ecometer', 'name': 'Écocompteur', 'name_by_user': None, - 'primary_integration': 'netatmo', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -519,7 +503,6 @@ 'model': 'Smart Indoor Camera', 'name': 'Hall', 'name_by_user': None, - 'primary_integration': 'netatmo', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -550,7 +533,6 @@ 'model': 'Smart Anemometer', 'name': 'Villa Garden', 'name_by_user': None, - 'primary_integration': 'netatmo', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -581,7 +563,6 @@ 'model': 'Smart Outdoor Camera', 'name': 'Front', 'name_by_user': None, - 'primary_integration': 'netatmo', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -612,7 +593,6 @@ 'model': 'Smart Video Doorbell', 'name': 'Netatmo-Doorbell', 'name_by_user': None, - 'primary_integration': 'netatmo', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -643,7 +623,6 @@ 'model': 'Smart Indoor Air Quality Monitor', 'name': 'Kitchen', 'name_by_user': None, - 'primary_integration': 'netatmo', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -674,7 +653,6 @@ 'model': 'Smart Indoor Air Quality Monitor', 'name': 'Livingroom', 'name_by_user': None, - 'primary_integration': 'netatmo', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -705,7 +683,6 @@ 'model': 'Smart Indoor Air Quality Monitor', 'name': 'Baby Bedroom', 'name_by_user': None, - 'primary_integration': 'netatmo', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -736,7 +713,6 @@ 'model': 'Smart Indoor Air Quality Monitor', 'name': 'Bedroom', 'name_by_user': None, - 'primary_integration': 'netatmo', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -767,7 +743,6 @@ 'model': 'Smart Indoor Air Quality Monitor', 'name': 'Parents Bedroom', 'name_by_user': None, - 'primary_integration': 'netatmo', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -798,7 +773,6 @@ 'model': 'Plug', 'name': 'Prise', 'name_by_user': None, - 'primary_integration': 'netatmo', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -829,7 +803,6 @@ 'model': 'Smart Outdoor Module', 'name': 'Villa Outdoor', 'name_by_user': None, - 'primary_integration': 'netatmo', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -860,7 +833,6 @@ 'model': 'Smart Indoor Module', 'name': 'Villa Bedroom', 'name_by_user': None, - 'primary_integration': 'netatmo', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -891,7 +863,6 @@ 'model': 'Smart Indoor Module', 'name': 'Villa Bathroom', 'name_by_user': None, - 'primary_integration': 'netatmo', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -922,7 +893,6 @@ 'model': 'Smart Home Weather station', 'name': 'Villa', 'name_by_user': None, - 'primary_integration': 'netatmo', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -953,7 +923,6 @@ 'model': 'Smart Rain Gauge', 'name': 'Villa Rain', 'name_by_user': None, - 'primary_integration': 'netatmo', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -984,7 +953,6 @@ 'model': 'OpenTherm Modulating Thermostat', 'name': 'Bureau Modulate', 'name_by_user': None, - 'primary_integration': 'netatmo', 'serial_number': None, 'suggested_area': 'Bureau', 'sw_version': None, @@ -1015,7 +983,6 @@ 'model': 'Smart Thermostat', 'name': 'Livingroom', 'name_by_user': None, - 'primary_integration': 'netatmo', 'serial_number': None, 'suggested_area': 'Livingroom', 'sw_version': None, @@ -1046,7 +1013,6 @@ 'model': 'Smart Valve', 'name': 'Valve1', 'name_by_user': None, - 'primary_integration': 'netatmo', 'serial_number': None, 'suggested_area': 'Entrada', 'sw_version': None, @@ -1077,7 +1043,6 @@ 'model': 'Smart Valve', 'name': 'Valve2', 'name_by_user': None, - 'primary_integration': 'netatmo', 'serial_number': None, 'suggested_area': 'Cocina', 'sw_version': None, @@ -1108,7 +1073,6 @@ 'model': 'Climate', 'name': 'MYHOME', 'name_by_user': None, - 'primary_integration': 'netatmo', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -1139,7 +1103,6 @@ 'model': 'Public Weather station', 'name': 'Home avg', 'name_by_user': None, - 'primary_integration': 'netatmo', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -1170,7 +1133,6 @@ 'model': 'Public Weather station', 'name': 'Home max', 'name_by_user': None, - 'primary_integration': 'netatmo', 'serial_number': None, 'suggested_area': None, 'sw_version': None, diff --git a/tests/components/netgear_lte/snapshots/test_init.ambr b/tests/components/netgear_lte/snapshots/test_init.ambr index e51fc937081..8af22f98e02 100644 --- a/tests/components/netgear_lte/snapshots/test_init.ambr +++ b/tests/components/netgear_lte/snapshots/test_init.ambr @@ -23,7 +23,6 @@ 'model': 'LM1200', 'name': 'Netgear LM1200', 'name_by_user': None, - 'primary_integration': 'netgear_lte', 'serial_number': 'FFFFFFFFFFFFF', 'suggested_area': None, 'sw_version': 'EC25AFFDR07A09M4G', diff --git a/tests/components/ondilo_ico/snapshots/test_init.ambr b/tests/components/ondilo_ico/snapshots/test_init.ambr index 0bf4748cfdd..c488b1e3c15 100644 --- a/tests/components/ondilo_ico/snapshots/test_init.ambr +++ b/tests/components/ondilo_ico/snapshots/test_init.ambr @@ -23,7 +23,6 @@ 'model': 'ICO', 'name': 'Pool 1', 'name_by_user': None, - 'primary_integration': 'ondilo_ico', 'serial_number': None, 'suggested_area': None, 'sw_version': '1.7.1-stable', @@ -54,7 +53,6 @@ 'model': 'ICO', 'name': 'Pool 2', 'name_by_user': None, - 'primary_integration': 'ondilo_ico', 'serial_number': None, 'suggested_area': None, 'sw_version': '1.7.1-stable', diff --git a/tests/components/onewire/snapshots/test_binary_sensor.ambr b/tests/components/onewire/snapshots/test_binary_sensor.ambr index febb0e50355..999794ec20d 100644 --- a/tests/components/onewire/snapshots/test_binary_sensor.ambr +++ b/tests/components/onewire/snapshots/test_binary_sensor.ambr @@ -36,7 +36,6 @@ 'model': 'DS2405', 'name': '05.111111111111', 'name_by_user': None, - 'primary_integration': None, 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -77,7 +76,6 @@ 'model': 'DS18S20', 'name': '10.111111111111', 'name_by_user': None, - 'primary_integration': None, 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -118,7 +116,6 @@ 'model': 'DS2406', 'name': '12.111111111111', 'name_by_user': None, - 'primary_integration': 'onewire', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -259,7 +256,6 @@ 'model': 'DS2423', 'name': '1D.111111111111', 'name_by_user': None, - 'primary_integration': None, 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -300,7 +296,6 @@ 'model': 'DS2409', 'name': '1F.111111111111', 'name_by_user': None, - 'primary_integration': None, 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -329,7 +324,6 @@ 'model': 'DS2423', 'name': '1D.111111111111', 'name_by_user': None, - 'primary_integration': None, 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -370,7 +364,6 @@ 'model': 'DS1822', 'name': '22.111111111111', 'name_by_user': None, - 'primary_integration': None, 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -411,7 +404,6 @@ 'model': 'DS2438', 'name': '26.111111111111', 'name_by_user': None, - 'primary_integration': None, 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -452,7 +444,6 @@ 'model': 'DS18B20', 'name': '28.111111111111', 'name_by_user': None, - 'primary_integration': None, 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -493,7 +484,6 @@ 'model': 'DS18B20', 'name': '28.222222222222', 'name_by_user': None, - 'primary_integration': None, 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -534,7 +524,6 @@ 'model': 'DS18B20', 'name': '28.222222222223', 'name_by_user': None, - 'primary_integration': None, 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -575,7 +564,6 @@ 'model': 'DS2408', 'name': '29.111111111111', 'name_by_user': None, - 'primary_integration': 'onewire', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -968,7 +956,6 @@ 'model': 'DS2760', 'name': '30.111111111111', 'name_by_user': None, - 'primary_integration': None, 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -1009,7 +996,6 @@ 'model': 'DS2413', 'name': '3A.111111111111', 'name_by_user': None, - 'primary_integration': 'onewire', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -1138,7 +1124,6 @@ 'model': 'DS1825', 'name': '3B.111111111111', 'name_by_user': None, - 'primary_integration': None, 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -1179,7 +1164,6 @@ 'model': 'DS28EA00', 'name': '42.111111111111', 'name_by_user': None, - 'primary_integration': None, 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -1220,7 +1204,6 @@ 'model': 'EDS0068', 'name': '7E.111111111111', 'name_by_user': None, - 'primary_integration': None, 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -1261,7 +1244,6 @@ 'model': 'EDS0066', 'name': '7E.222222222222', 'name_by_user': None, - 'primary_integration': None, 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -1302,7 +1284,6 @@ 'model': 'DS2438', 'name': 'A6.111111111111', 'name_by_user': None, - 'primary_integration': None, 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -1343,7 +1324,6 @@ 'model': 'HobbyBoards_EF', 'name': 'EF.111111111111', 'name_by_user': None, - 'primary_integration': None, 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -1384,7 +1364,6 @@ 'model': 'HB_MOISTURE_METER', 'name': 'EF.111111111112', 'name_by_user': None, - 'primary_integration': None, 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -1425,7 +1404,6 @@ 'model': 'HB_HUB', 'name': 'EF.111111111113', 'name_by_user': None, - 'primary_integration': 'onewire', 'serial_number': None, 'suggested_area': None, 'sw_version': None, diff --git a/tests/components/onewire/snapshots/test_sensor.ambr b/tests/components/onewire/snapshots/test_sensor.ambr index ffa7dadb6fe..59ed167197d 100644 --- a/tests/components/onewire/snapshots/test_sensor.ambr +++ b/tests/components/onewire/snapshots/test_sensor.ambr @@ -36,7 +36,6 @@ 'model': 'DS2405', 'name': '05.111111111111', 'name_by_user': None, - 'primary_integration': None, 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -77,7 +76,6 @@ 'model': 'DS18S20', 'name': '10.111111111111', 'name_by_user': None, - 'primary_integration': 'onewire', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -167,7 +165,6 @@ 'model': 'DS2406', 'name': '12.111111111111', 'name_by_user': None, - 'primary_integration': 'onewire', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -318,7 +315,6 @@ 'model': 'DS2423', 'name': '1D.111111111111', 'name_by_user': None, - 'primary_integration': 'onewire', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -455,7 +451,6 @@ 'model': 'DS2409', 'name': '1F.111111111111', 'name_by_user': None, - 'primary_integration': None, 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -484,7 +479,6 @@ 'model': 'DS2423', 'name': '1D.111111111111', 'name_by_user': None, - 'primary_integration': 'onewire', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -621,7 +615,6 @@ 'model': 'DS1822', 'name': '22.111111111111', 'name_by_user': None, - 'primary_integration': 'onewire', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -711,7 +704,6 @@ 'model': 'DS2438', 'name': '26.111111111111', 'name_by_user': None, - 'primary_integration': 'onewire', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -1291,7 +1283,6 @@ 'model': 'DS18B20', 'name': '28.111111111111', 'name_by_user': None, - 'primary_integration': 'onewire', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -1381,7 +1372,6 @@ 'model': 'DS18B20', 'name': '28.222222222222', 'name_by_user': None, - 'primary_integration': 'onewire', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -1471,7 +1461,6 @@ 'model': 'DS18B20', 'name': '28.222222222223', 'name_by_user': None, - 'primary_integration': 'onewire', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -1561,7 +1550,6 @@ 'model': 'DS2408', 'name': '29.111111111111', 'name_by_user': None, - 'primary_integration': None, 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -1602,7 +1590,6 @@ 'model': 'DS2760', 'name': '30.111111111111', 'name_by_user': None, - 'primary_integration': 'onewire', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -1839,7 +1826,6 @@ 'model': 'DS2413', 'name': '3A.111111111111', 'name_by_user': None, - 'primary_integration': None, 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -1880,7 +1866,6 @@ 'model': 'DS1825', 'name': '3B.111111111111', 'name_by_user': None, - 'primary_integration': 'onewire', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -1970,7 +1955,6 @@ 'model': 'DS28EA00', 'name': '42.111111111111', 'name_by_user': None, - 'primary_integration': 'onewire', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -2060,7 +2044,6 @@ 'model': 'EDS0068', 'name': '7E.111111111111', 'name_by_user': None, - 'primary_integration': 'onewire', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -2297,7 +2280,6 @@ 'model': 'EDS0066', 'name': '7E.222222222222', 'name_by_user': None, - 'primary_integration': 'onewire', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -2436,7 +2418,6 @@ 'model': 'DS2438', 'name': 'A6.111111111111', 'name_by_user': None, - 'primary_integration': 'onewire', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -3016,7 +2997,6 @@ 'model': 'HobbyBoards_EF', 'name': 'EF.111111111111', 'name_by_user': None, - 'primary_integration': 'onewire', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -3204,7 +3184,6 @@ 'model': 'HB_MOISTURE_METER', 'name': 'EF.111111111112', 'name_by_user': None, - 'primary_integration': 'onewire', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -3441,7 +3420,6 @@ 'model': 'HB_HUB', 'name': 'EF.111111111113', 'name_by_user': None, - 'primary_integration': None, 'serial_number': None, 'suggested_area': None, 'sw_version': None, diff --git a/tests/components/onewire/snapshots/test_switch.ambr b/tests/components/onewire/snapshots/test_switch.ambr index 5d736bd9c99..8fd1e2aeef6 100644 --- a/tests/components/onewire/snapshots/test_switch.ambr +++ b/tests/components/onewire/snapshots/test_switch.ambr @@ -36,7 +36,6 @@ 'model': 'DS2405', 'name': '05.111111111111', 'name_by_user': None, - 'primary_integration': 'onewire', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -121,7 +120,6 @@ 'model': 'DS18S20', 'name': '10.111111111111', 'name_by_user': None, - 'primary_integration': None, 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -162,7 +160,6 @@ 'model': 'DS2406', 'name': '12.111111111111', 'name_by_user': None, - 'primary_integration': 'onewire', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -391,7 +388,6 @@ 'model': 'DS2423', 'name': '1D.111111111111', 'name_by_user': None, - 'primary_integration': None, 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -432,7 +428,6 @@ 'model': 'DS2409', 'name': '1F.111111111111', 'name_by_user': None, - 'primary_integration': None, 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -461,7 +456,6 @@ 'model': 'DS2423', 'name': '1D.111111111111', 'name_by_user': None, - 'primary_integration': None, 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -502,7 +496,6 @@ 'model': 'DS1822', 'name': '22.111111111111', 'name_by_user': None, - 'primary_integration': None, 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -543,7 +536,6 @@ 'model': 'DS2438', 'name': '26.111111111111', 'name_by_user': None, - 'primary_integration': 'onewire', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -628,7 +620,6 @@ 'model': 'DS18B20', 'name': '28.111111111111', 'name_by_user': None, - 'primary_integration': None, 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -669,7 +660,6 @@ 'model': 'DS18B20', 'name': '28.222222222222', 'name_by_user': None, - 'primary_integration': None, 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -710,7 +700,6 @@ 'model': 'DS18B20', 'name': '28.222222222223', 'name_by_user': None, - 'primary_integration': None, 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -751,7 +740,6 @@ 'model': 'DS2408', 'name': '29.111111111111', 'name_by_user': None, - 'primary_integration': 'onewire', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -1496,7 +1484,6 @@ 'model': 'DS2760', 'name': '30.111111111111', 'name_by_user': None, - 'primary_integration': None, 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -1537,7 +1524,6 @@ 'model': 'DS2413', 'name': '3A.111111111111', 'name_by_user': None, - 'primary_integration': 'onewire', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -1666,7 +1652,6 @@ 'model': 'DS1825', 'name': '3B.111111111111', 'name_by_user': None, - 'primary_integration': None, 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -1707,7 +1692,6 @@ 'model': 'DS28EA00', 'name': '42.111111111111', 'name_by_user': None, - 'primary_integration': None, 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -1748,7 +1732,6 @@ 'model': 'EDS0068', 'name': '7E.111111111111', 'name_by_user': None, - 'primary_integration': None, 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -1789,7 +1772,6 @@ 'model': 'EDS0066', 'name': '7E.222222222222', 'name_by_user': None, - 'primary_integration': None, 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -1830,7 +1812,6 @@ 'model': 'DS2438', 'name': 'A6.111111111111', 'name_by_user': None, - 'primary_integration': 'onewire', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -1915,7 +1896,6 @@ 'model': 'HobbyBoards_EF', 'name': 'EF.111111111111', 'name_by_user': None, - 'primary_integration': None, 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -1956,7 +1936,6 @@ 'model': 'HB_MOISTURE_METER', 'name': 'EF.111111111112', 'name_by_user': None, - 'primary_integration': 'onewire', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -2349,7 +2328,6 @@ 'model': 'HB_HUB', 'name': 'EF.111111111113', 'name_by_user': None, - 'primary_integration': 'onewire', 'serial_number': None, 'suggested_area': None, 'sw_version': None, diff --git a/tests/components/renault/snapshots/test_binary_sensor.ambr b/tests/components/renault/snapshots/test_binary_sensor.ambr index 50833ab681f..7f30faac38e 100644 --- a/tests/components/renault/snapshots/test_binary_sensor.ambr +++ b/tests/components/renault/snapshots/test_binary_sensor.ambr @@ -24,7 +24,6 @@ 'model': 'Captur ii', 'name': 'REG-NUMBER', 'name_by_user': None, - 'primary_integration': 'renault', 'serial_number': None, 'suggested_area': None, 'sw_version': 'XJB1SU', @@ -323,7 +322,6 @@ 'model': 'Captur ii', 'name': 'REG-NUMBER', 'name_by_user': None, - 'primary_integration': 'renault', 'serial_number': None, 'suggested_area': None, 'sw_version': 'XJB1SU', @@ -708,7 +706,6 @@ 'model': 'Zoe', 'name': 'REG-NUMBER', 'name_by_user': None, - 'primary_integration': 'renault', 'serial_number': None, 'suggested_area': None, 'sw_version': 'X101VE', @@ -877,7 +874,6 @@ 'model': 'Zoe', 'name': 'REG-NUMBER', 'name_by_user': None, - 'primary_integration': 'renault', 'serial_number': None, 'suggested_area': None, 'sw_version': 'X102VE', @@ -1304,7 +1300,6 @@ 'model': 'Captur ii', 'name': 'REG-NUMBER', 'name_by_user': None, - 'primary_integration': 'renault', 'serial_number': None, 'suggested_area': None, 'sw_version': 'XJB1SU', @@ -1603,7 +1598,6 @@ 'model': 'Captur ii', 'name': 'REG-NUMBER', 'name_by_user': None, - 'primary_integration': 'renault', 'serial_number': None, 'suggested_area': None, 'sw_version': 'XJB1SU', @@ -1988,7 +1982,6 @@ 'model': 'Zoe', 'name': 'REG-NUMBER', 'name_by_user': None, - 'primary_integration': 'renault', 'serial_number': None, 'suggested_area': None, 'sw_version': 'X101VE', @@ -2157,7 +2150,6 @@ 'model': 'Zoe', 'name': 'REG-NUMBER', 'name_by_user': None, - 'primary_integration': 'renault', 'serial_number': None, 'suggested_area': None, 'sw_version': 'X102VE', diff --git a/tests/components/renault/snapshots/test_button.ambr b/tests/components/renault/snapshots/test_button.ambr index b23cae4eb03..daef84b5c0a 100644 --- a/tests/components/renault/snapshots/test_button.ambr +++ b/tests/components/renault/snapshots/test_button.ambr @@ -24,7 +24,6 @@ 'model': 'Captur ii', 'name': 'REG-NUMBER', 'name_by_user': None, - 'primary_integration': 'renault', 'serial_number': None, 'suggested_area': None, 'sw_version': 'XJB1SU', @@ -107,7 +106,6 @@ 'model': 'Captur ii', 'name': 'REG-NUMBER', 'name_by_user': None, - 'primary_integration': 'renault', 'serial_number': None, 'suggested_area': None, 'sw_version': 'XJB1SU', @@ -274,7 +272,6 @@ 'model': 'Zoe', 'name': 'REG-NUMBER', 'name_by_user': None, - 'primary_integration': 'renault', 'serial_number': None, 'suggested_area': None, 'sw_version': 'X101VE', @@ -441,7 +438,6 @@ 'model': 'Zoe', 'name': 'REG-NUMBER', 'name_by_user': None, - 'primary_integration': 'renault', 'serial_number': None, 'suggested_area': None, 'sw_version': 'X102VE', @@ -608,7 +604,6 @@ 'model': 'Captur ii', 'name': 'REG-NUMBER', 'name_by_user': None, - 'primary_integration': 'renault', 'serial_number': None, 'suggested_area': None, 'sw_version': 'XJB1SU', @@ -691,7 +686,6 @@ 'model': 'Captur ii', 'name': 'REG-NUMBER', 'name_by_user': None, - 'primary_integration': 'renault', 'serial_number': None, 'suggested_area': None, 'sw_version': 'XJB1SU', @@ -858,7 +852,6 @@ 'model': 'Zoe', 'name': 'REG-NUMBER', 'name_by_user': None, - 'primary_integration': 'renault', 'serial_number': None, 'suggested_area': None, 'sw_version': 'X101VE', @@ -1025,7 +1018,6 @@ 'model': 'Zoe', 'name': 'REG-NUMBER', 'name_by_user': None, - 'primary_integration': 'renault', 'serial_number': None, 'suggested_area': None, 'sw_version': 'X102VE', diff --git a/tests/components/renault/snapshots/test_device_tracker.ambr b/tests/components/renault/snapshots/test_device_tracker.ambr index df3db275214..8fe1713dc0b 100644 --- a/tests/components/renault/snapshots/test_device_tracker.ambr +++ b/tests/components/renault/snapshots/test_device_tracker.ambr @@ -24,7 +24,6 @@ 'model': 'Captur ii', 'name': 'REG-NUMBER', 'name_by_user': None, - 'primary_integration': 'renault', 'serial_number': None, 'suggested_area': None, 'sw_version': 'XJB1SU', @@ -108,7 +107,6 @@ 'model': 'Captur ii', 'name': 'REG-NUMBER', 'name_by_user': None, - 'primary_integration': 'renault', 'serial_number': None, 'suggested_area': None, 'sw_version': 'XJB1SU', @@ -192,7 +190,6 @@ 'model': 'Zoe', 'name': 'REG-NUMBER', 'name_by_user': None, - 'primary_integration': None, 'serial_number': None, 'suggested_area': None, 'sw_version': 'X101VE', @@ -233,7 +230,6 @@ 'model': 'Zoe', 'name': 'REG-NUMBER', 'name_by_user': None, - 'primary_integration': 'renault', 'serial_number': None, 'suggested_area': None, 'sw_version': 'X102VE', @@ -317,7 +313,6 @@ 'model': 'Captur ii', 'name': 'REG-NUMBER', 'name_by_user': None, - 'primary_integration': 'renault', 'serial_number': None, 'suggested_area': None, 'sw_version': 'XJB1SU', @@ -404,7 +399,6 @@ 'model': 'Captur ii', 'name': 'REG-NUMBER', 'name_by_user': None, - 'primary_integration': 'renault', 'serial_number': None, 'suggested_area': None, 'sw_version': 'XJB1SU', @@ -491,7 +485,6 @@ 'model': 'Zoe', 'name': 'REG-NUMBER', 'name_by_user': None, - 'primary_integration': None, 'serial_number': None, 'suggested_area': None, 'sw_version': 'X101VE', @@ -532,7 +525,6 @@ 'model': 'Zoe', 'name': 'REG-NUMBER', 'name_by_user': None, - 'primary_integration': 'renault', 'serial_number': None, 'suggested_area': None, 'sw_version': 'X102VE', diff --git a/tests/components/renault/snapshots/test_select.ambr b/tests/components/renault/snapshots/test_select.ambr index d597a2b31f0..0722cb5cab3 100644 --- a/tests/components/renault/snapshots/test_select.ambr +++ b/tests/components/renault/snapshots/test_select.ambr @@ -24,7 +24,6 @@ 'model': 'Captur ii', 'name': 'REG-NUMBER', 'name_by_user': None, - 'primary_integration': None, 'serial_number': None, 'suggested_area': None, 'sw_version': 'XJB1SU', @@ -65,7 +64,6 @@ 'model': 'Captur ii', 'name': 'REG-NUMBER', 'name_by_user': None, - 'primary_integration': 'renault', 'serial_number': None, 'suggested_area': None, 'sw_version': 'XJB1SU', @@ -161,7 +159,6 @@ 'model': 'Zoe', 'name': 'REG-NUMBER', 'name_by_user': None, - 'primary_integration': 'renault', 'serial_number': None, 'suggested_area': None, 'sw_version': 'X101VE', @@ -257,7 +254,6 @@ 'model': 'Zoe', 'name': 'REG-NUMBER', 'name_by_user': None, - 'primary_integration': 'renault', 'serial_number': None, 'suggested_area': None, 'sw_version': 'X102VE', @@ -353,7 +349,6 @@ 'model': 'Captur ii', 'name': 'REG-NUMBER', 'name_by_user': None, - 'primary_integration': None, 'serial_number': None, 'suggested_area': None, 'sw_version': 'XJB1SU', @@ -394,7 +389,6 @@ 'model': 'Captur ii', 'name': 'REG-NUMBER', 'name_by_user': None, - 'primary_integration': 'renault', 'serial_number': None, 'suggested_area': None, 'sw_version': 'XJB1SU', @@ -490,7 +484,6 @@ 'model': 'Zoe', 'name': 'REG-NUMBER', 'name_by_user': None, - 'primary_integration': 'renault', 'serial_number': None, 'suggested_area': None, 'sw_version': 'X101VE', @@ -586,7 +579,6 @@ 'model': 'Zoe', 'name': 'REG-NUMBER', 'name_by_user': None, - 'primary_integration': 'renault', 'serial_number': None, 'suggested_area': None, 'sw_version': 'X102VE', diff --git a/tests/components/renault/snapshots/test_sensor.ambr b/tests/components/renault/snapshots/test_sensor.ambr index 6af7d9cd8d3..5909c66bc5c 100644 --- a/tests/components/renault/snapshots/test_sensor.ambr +++ b/tests/components/renault/snapshots/test_sensor.ambr @@ -24,7 +24,6 @@ 'model': 'Captur ii', 'name': 'REG-NUMBER', 'name_by_user': None, - 'primary_integration': 'renault', 'serial_number': None, 'suggested_area': None, 'sw_version': 'XJB1SU', @@ -333,7 +332,6 @@ 'model': 'Captur ii', 'name': 'REG-NUMBER', 'name_by_user': None, - 'primary_integration': 'renault', 'serial_number': None, 'suggested_area': None, 'sw_version': 'XJB1SU', @@ -1087,7 +1085,6 @@ 'model': 'Zoe', 'name': 'REG-NUMBER', 'name_by_user': None, - 'primary_integration': 'renault', 'serial_number': None, 'suggested_area': None, 'sw_version': 'X101VE', @@ -1837,7 +1834,6 @@ 'model': 'Zoe', 'name': 'REG-NUMBER', 'name_by_user': None, - 'primary_integration': 'renault', 'serial_number': None, 'suggested_area': None, 'sw_version': 'X102VE', @@ -2630,7 +2626,6 @@ 'model': 'Captur ii', 'name': 'REG-NUMBER', 'name_by_user': None, - 'primary_integration': 'renault', 'serial_number': None, 'suggested_area': None, 'sw_version': 'XJB1SU', @@ -2939,7 +2934,6 @@ 'model': 'Captur ii', 'name': 'REG-NUMBER', 'name_by_user': None, - 'primary_integration': 'renault', 'serial_number': None, 'suggested_area': None, 'sw_version': 'XJB1SU', @@ -3693,7 +3687,6 @@ 'model': 'Zoe', 'name': 'REG-NUMBER', 'name_by_user': None, - 'primary_integration': 'renault', 'serial_number': None, 'suggested_area': None, 'sw_version': 'X101VE', @@ -4443,7 +4436,6 @@ 'model': 'Zoe', 'name': 'REG-NUMBER', 'name_by_user': None, - 'primary_integration': 'renault', 'serial_number': None, 'suggested_area': None, 'sw_version': 'X102VE', diff --git a/tests/components/rova/snapshots/test_init.ambr b/tests/components/rova/snapshots/test_init.ambr index 9210027221b..340b0e6d472 100644 --- a/tests/components/rova/snapshots/test_init.ambr +++ b/tests/components/rova/snapshots/test_init.ambr @@ -23,7 +23,6 @@ 'model': None, 'name': '8381BE 13', 'name_by_user': None, - 'primary_integration': 'rova', 'serial_number': None, 'suggested_area': None, 'sw_version': None, diff --git a/tests/components/sfr_box/snapshots/test_binary_sensor.ambr b/tests/components/sfr_box/snapshots/test_binary_sensor.ambr index 62a656f9157..7422c1395c3 100644 --- a/tests/components/sfr_box/snapshots/test_binary_sensor.ambr +++ b/tests/components/sfr_box/snapshots/test_binary_sensor.ambr @@ -24,7 +24,6 @@ 'model': 'NB6VAC-FXC-r0', 'name': 'SFR Box', 'name_by_user': None, - 'primary_integration': None, 'serial_number': None, 'suggested_area': None, 'sw_version': 'NB6VAC-MAIN-R4.0.44k', @@ -151,7 +150,6 @@ 'model': 'NB6VAC-FXC-r0', 'name': 'SFR Box', 'name_by_user': None, - 'primary_integration': None, 'serial_number': None, 'suggested_area': None, 'sw_version': 'NB6VAC-MAIN-R4.0.44k', diff --git a/tests/components/sfr_box/snapshots/test_button.ambr b/tests/components/sfr_box/snapshots/test_button.ambr index b786e75910b..0dfbf187f6d 100644 --- a/tests/components/sfr_box/snapshots/test_button.ambr +++ b/tests/components/sfr_box/snapshots/test_button.ambr @@ -24,7 +24,6 @@ 'model': 'NB6VAC-FXC-r0', 'name': 'SFR Box', 'name_by_user': None, - 'primary_integration': None, 'serial_number': None, 'suggested_area': None, 'sw_version': 'NB6VAC-MAIN-R4.0.44k', diff --git a/tests/components/sfr_box/snapshots/test_sensor.ambr b/tests/components/sfr_box/snapshots/test_sensor.ambr index 662b765ee74..0f39eed9e60 100644 --- a/tests/components/sfr_box/snapshots/test_sensor.ambr +++ b/tests/components/sfr_box/snapshots/test_sensor.ambr @@ -24,7 +24,6 @@ 'model': 'NB6VAC-FXC-r0', 'name': 'SFR Box', 'name_by_user': None, - 'primary_integration': None, 'serial_number': None, 'suggested_area': None, 'sw_version': 'NB6VAC-MAIN-R4.0.44k', diff --git a/tests/components/tailwind/snapshots/test_binary_sensor.ambr b/tests/components/tailwind/snapshots/test_binary_sensor.ambr index f9088e1d5c3..ea2a539363d 100644 --- a/tests/components/tailwind/snapshots/test_binary_sensor.ambr +++ b/tests/components/tailwind/snapshots/test_binary_sensor.ambr @@ -70,7 +70,6 @@ 'model': 'iQ3', 'name': 'Door 1', 'name_by_user': None, - 'primary_integration': 'tailwind', 'serial_number': None, 'suggested_area': None, 'sw_version': '10.10', @@ -148,7 +147,6 @@ 'model': 'iQ3', 'name': 'Door 2', 'name_by_user': None, - 'primary_integration': 'tailwind', 'serial_number': None, 'suggested_area': None, 'sw_version': '10.10', diff --git a/tests/components/tailwind/snapshots/test_button.ambr b/tests/components/tailwind/snapshots/test_button.ambr index f96032630bc..560d3fe692c 100644 --- a/tests/components/tailwind/snapshots/test_button.ambr +++ b/tests/components/tailwind/snapshots/test_button.ambr @@ -74,7 +74,6 @@ 'model': 'iQ3', 'name': 'Tailwind iQ3', 'name_by_user': None, - 'primary_integration': None, 'serial_number': None, 'suggested_area': None, 'sw_version': '10.10', diff --git a/tests/components/tailwind/snapshots/test_cover.ambr b/tests/components/tailwind/snapshots/test_cover.ambr index 98891e649e7..0ecd172b2ca 100644 --- a/tests/components/tailwind/snapshots/test_cover.ambr +++ b/tests/components/tailwind/snapshots/test_cover.ambr @@ -71,7 +71,6 @@ 'model': 'iQ3', 'name': 'Door 1', 'name_by_user': None, - 'primary_integration': 'tailwind', 'serial_number': None, 'suggested_area': None, 'sw_version': '10.10', @@ -150,7 +149,6 @@ 'model': 'iQ3', 'name': 'Door 2', 'name_by_user': None, - 'primary_integration': 'tailwind', 'serial_number': None, 'suggested_area': None, 'sw_version': '10.10', diff --git a/tests/components/tailwind/snapshots/test_number.ambr b/tests/components/tailwind/snapshots/test_number.ambr index 1bd01482c0c..cbd61d31a6c 100644 --- a/tests/components/tailwind/snapshots/test_number.ambr +++ b/tests/components/tailwind/snapshots/test_number.ambr @@ -83,7 +83,6 @@ 'model': 'iQ3', 'name': 'Tailwind iQ3', 'name_by_user': None, - 'primary_integration': None, 'serial_number': None, 'suggested_area': None, 'sw_version': '10.10', diff --git a/tests/components/tasmota/test_discovery.py b/tests/components/tasmota/test_discovery.py index 91832f1f2f0..5405e6c417d 100644 --- a/tests/components/tasmota/test_discovery.py +++ b/tests/components/tasmota/test_discovery.py @@ -340,7 +340,7 @@ async def test_device_remove_multiple_config_entries_1( connections={(dr.CONNECTION_NETWORK_MAC, mac)} ) assert device_entry is not None - assert device_entry.config_entries == {tasmota_entry.entry_id, mock_entry.entry_id} + assert device_entry.config_entries == [tasmota_entry.entry_id, mock_entry.entry_id] async_fire_mqtt_message( hass, @@ -354,7 +354,7 @@ async def test_device_remove_multiple_config_entries_1( connections={(dr.CONNECTION_NETWORK_MAC, mac)} ) assert device_entry is not None - assert device_entry.config_entries == {mock_entry.entry_id} + assert device_entry.config_entries == [mock_entry.entry_id] async def test_device_remove_multiple_config_entries_2( @@ -396,7 +396,7 @@ async def test_device_remove_multiple_config_entries_2( connections={(dr.CONNECTION_NETWORK_MAC, mac)} ) assert device_entry is not None - assert device_entry.config_entries == {tasmota_entry.entry_id, mock_entry.entry_id} + assert device_entry.config_entries == [tasmota_entry.entry_id, mock_entry.entry_id] assert other_device_entry.id != device_entry.id # Remove other config entry from the device @@ -410,7 +410,7 @@ async def test_device_remove_multiple_config_entries_2( connections={(dr.CONNECTION_NETWORK_MAC, mac)} ) assert device_entry is not None - assert device_entry.config_entries == {tasmota_entry.entry_id} + assert device_entry.config_entries == [tasmota_entry.entry_id] mqtt_mock.async_publish.assert_not_called() # Remove other config entry from the other device - Tasmota should not do any cleanup diff --git a/tests/components/tedee/snapshots/test_init.ambr b/tests/components/tedee/snapshots/test_init.ambr index 96284adb338..83ab032dfb4 100644 --- a/tests/components/tedee/snapshots/test_init.ambr +++ b/tests/components/tedee/snapshots/test_init.ambr @@ -23,7 +23,6 @@ 'model': 'Bridge', 'name': 'Bridge-AB1C', 'name_by_user': None, - 'primary_integration': None, 'serial_number': '0000-0000', 'suggested_area': None, 'sw_version': None, diff --git a/tests/components/tedee/snapshots/test_lock.ambr b/tests/components/tedee/snapshots/test_lock.ambr index bf9021b639b..8e4fc464479 100644 --- a/tests/components/tedee/snapshots/test_lock.ambr +++ b/tests/components/tedee/snapshots/test_lock.ambr @@ -70,7 +70,6 @@ 'model': 'Tedee PRO', 'name': 'Lock-1A2B', 'name_by_user': None, - 'primary_integration': 'tedee', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -148,7 +147,6 @@ 'model': 'Tedee GO', 'name': 'Lock-2C3D', 'name_by_user': None, - 'primary_integration': 'tedee', 'serial_number': None, 'suggested_area': None, 'sw_version': None, diff --git a/tests/components/teslemetry/snapshots/test_init.ambr b/tests/components/teslemetry/snapshots/test_init.ambr index d1656c2260e..951e4557bdd 100644 --- a/tests/components/teslemetry/snapshots/test_init.ambr +++ b/tests/components/teslemetry/snapshots/test_init.ambr @@ -23,7 +23,6 @@ 'model': 'Powerwall 2, Tesla Backup Gateway 2', 'name': 'Energy Site', 'name_by_user': None, - 'primary_integration': 'teslemetry', 'serial_number': '123456', 'suggested_area': None, 'sw_version': None, @@ -54,7 +53,6 @@ 'model': 'Model X', 'name': 'Test', 'name_by_user': None, - 'primary_integration': 'teslemetry', 'serial_number': 'LRWXF7EK4KC700000', 'suggested_area': None, 'sw_version': None, @@ -85,7 +83,6 @@ 'model': 'Gen 3 Wall Connector', 'name': 'Wall Connector', 'name_by_user': None, - 'primary_integration': 'teslemetry', 'serial_number': '123', 'suggested_area': None, 'sw_version': None, @@ -116,7 +113,6 @@ 'model': 'Gen 3 Wall Connector', 'name': 'Wall Connector', 'name_by_user': None, - 'primary_integration': 'teslemetry', 'serial_number': '234', 'suggested_area': None, 'sw_version': None, diff --git a/tests/components/twentemilieu/snapshots/test_calendar.ambr b/tests/components/twentemilieu/snapshots/test_calendar.ambr index fa24ad644d2..78b2d56afca 100644 --- a/tests/components/twentemilieu/snapshots/test_calendar.ambr +++ b/tests/components/twentemilieu/snapshots/test_calendar.ambr @@ -101,7 +101,6 @@ 'model': None, 'name': 'Twente Milieu', 'name_by_user': None, - 'primary_integration': 'twentemilieu', 'serial_number': None, 'suggested_area': None, 'sw_version': None, diff --git a/tests/components/twentemilieu/snapshots/test_sensor.ambr b/tests/components/twentemilieu/snapshots/test_sensor.ambr index e943d937fa3..a0f3b75da57 100644 --- a/tests/components/twentemilieu/snapshots/test_sensor.ambr +++ b/tests/components/twentemilieu/snapshots/test_sensor.ambr @@ -70,7 +70,6 @@ 'model': None, 'name': 'Twente Milieu', 'name_by_user': None, - 'primary_integration': 'twentemilieu', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -148,7 +147,6 @@ 'model': None, 'name': 'Twente Milieu', 'name_by_user': None, - 'primary_integration': 'twentemilieu', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -226,7 +224,6 @@ 'model': None, 'name': 'Twente Milieu', 'name_by_user': None, - 'primary_integration': 'twentemilieu', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -304,7 +301,6 @@ 'model': None, 'name': 'Twente Milieu', 'name_by_user': None, - 'primary_integration': 'twentemilieu', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -382,7 +378,6 @@ 'model': None, 'name': 'Twente Milieu', 'name_by_user': None, - 'primary_integration': 'twentemilieu', 'serial_number': None, 'suggested_area': None, 'sw_version': None, diff --git a/tests/components/uptime/snapshots/test_sensor.ambr b/tests/components/uptime/snapshots/test_sensor.ambr index 692bfe53ea2..0e7ae6dceaa 100644 --- a/tests/components/uptime/snapshots/test_sensor.ambr +++ b/tests/components/uptime/snapshots/test_sensor.ambr @@ -63,7 +63,6 @@ 'model': None, 'name': 'Uptime', 'name_by_user': None, - 'primary_integration': 'uptime', 'serial_number': None, 'suggested_area': None, 'sw_version': None, diff --git a/tests/components/vesync/snapshots/test_fan.ambr b/tests/components/vesync/snapshots/test_fan.ambr index 159d872a65b..59304e92d9d 100644 --- a/tests/components/vesync/snapshots/test_fan.ambr +++ b/tests/components/vesync/snapshots/test_fan.ambr @@ -24,7 +24,6 @@ 'model': 'LV-PUR131S', 'name': 'Air Purifier 131s', 'name_by_user': None, - 'primary_integration': 'vesync', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -115,7 +114,6 @@ 'model': 'Core200S', 'name': 'Air Purifier 200s', 'name_by_user': None, - 'primary_integration': 'vesync', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -211,7 +209,6 @@ 'model': 'LAP-C401S-WJP', 'name': 'Air Purifier 400s', 'name_by_user': None, - 'primary_integration': 'vesync', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -309,7 +306,6 @@ 'model': 'LAP-C601S-WUS', 'name': 'Air Purifier 600s', 'name_by_user': None, - 'primary_integration': 'vesync', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -407,7 +403,6 @@ 'model': 'ESL100', 'name': 'Dimmable Light', 'name_by_user': None, - 'primary_integration': 'vesync', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -444,7 +439,6 @@ 'model': 'ESWD16', 'name': 'Dimmer Switch', 'name_by_user': None, - 'primary_integration': 'vesync', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -497,7 +491,6 @@ 'model': 'wifi-switch-1.3', 'name': 'Outlet', 'name_by_user': None, - 'primary_integration': 'vesync', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -534,7 +527,6 @@ 'model': 'ESL100CW', 'name': 'Temperature Light', 'name_by_user': None, - 'primary_integration': 'vesync', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -571,7 +563,6 @@ 'model': 'ESWL01', 'name': 'Wall Switch', 'name_by_user': None, - 'primary_integration': 'vesync', 'serial_number': None, 'suggested_area': None, 'sw_version': None, diff --git a/tests/components/vesync/snapshots/test_light.ambr b/tests/components/vesync/snapshots/test_light.ambr index c393453e78c..9990395a36c 100644 --- a/tests/components/vesync/snapshots/test_light.ambr +++ b/tests/components/vesync/snapshots/test_light.ambr @@ -24,7 +24,6 @@ 'model': 'LV-PUR131S', 'name': 'Air Purifier 131s', 'name_by_user': None, - 'primary_integration': 'vesync', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -61,7 +60,6 @@ 'model': 'Core200S', 'name': 'Air Purifier 200s', 'name_by_user': None, - 'primary_integration': 'vesync', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -98,7 +96,6 @@ 'model': 'LAP-C401S-WJP', 'name': 'Air Purifier 400s', 'name_by_user': None, - 'primary_integration': 'vesync', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -135,7 +132,6 @@ 'model': 'LAP-C601S-WUS', 'name': 'Air Purifier 600s', 'name_by_user': None, - 'primary_integration': 'vesync', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -172,7 +168,6 @@ 'model': 'ESL100', 'name': 'Dimmable Light', 'name_by_user': None, - 'primary_integration': 'vesync', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -261,7 +256,6 @@ 'model': 'ESWD16', 'name': 'Dimmer Switch', 'name_by_user': None, - 'primary_integration': 'vesync', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -368,7 +362,6 @@ 'model': 'wifi-switch-1.3', 'name': 'Outlet', 'name_by_user': None, - 'primary_integration': 'vesync', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -405,7 +398,6 @@ 'model': 'ESL100CW', 'name': 'Temperature Light', 'name_by_user': None, - 'primary_integration': 'vesync', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -509,7 +501,6 @@ 'model': 'ESWL01', 'name': 'Wall Switch', 'name_by_user': None, - 'primary_integration': 'vesync', 'serial_number': None, 'suggested_area': None, 'sw_version': None, diff --git a/tests/components/vesync/snapshots/test_sensor.ambr b/tests/components/vesync/snapshots/test_sensor.ambr index 27c52e5580e..268718fb2fe 100644 --- a/tests/components/vesync/snapshots/test_sensor.ambr +++ b/tests/components/vesync/snapshots/test_sensor.ambr @@ -24,7 +24,6 @@ 'model': 'LV-PUR131S', 'name': 'Air Purifier 131s', 'name_by_user': None, - 'primary_integration': 'vesync', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -153,7 +152,6 @@ 'model': 'Core200S', 'name': 'Air Purifier 200s', 'name_by_user': None, - 'primary_integration': 'vesync', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -238,7 +236,6 @@ 'model': 'LAP-C401S-WJP', 'name': 'Air Purifier 400s', 'name_by_user': None, - 'primary_integration': 'vesync', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -416,7 +413,6 @@ 'model': 'LAP-C601S-WUS', 'name': 'Air Purifier 600s', 'name_by_user': None, - 'primary_integration': 'vesync', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -594,7 +590,6 @@ 'model': 'ESL100', 'name': 'Dimmable Light', 'name_by_user': None, - 'primary_integration': 'vesync', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -631,7 +626,6 @@ 'model': 'ESWD16', 'name': 'Dimmer Switch', 'name_by_user': None, - 'primary_integration': 'vesync', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -684,7 +678,6 @@ 'model': 'wifi-switch-1.3', 'name': 'Outlet', 'name_by_user': None, - 'primary_integration': 'vesync', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -1015,7 +1008,6 @@ 'model': 'ESL100CW', 'name': 'Temperature Light', 'name_by_user': None, - 'primary_integration': 'vesync', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -1052,7 +1044,6 @@ 'model': 'ESWL01', 'name': 'Wall Switch', 'name_by_user': None, - 'primary_integration': 'vesync', 'serial_number': None, 'suggested_area': None, 'sw_version': None, diff --git a/tests/components/vesync/snapshots/test_switch.ambr b/tests/components/vesync/snapshots/test_switch.ambr index 3b816e70bee..3df26f74bcf 100644 --- a/tests/components/vesync/snapshots/test_switch.ambr +++ b/tests/components/vesync/snapshots/test_switch.ambr @@ -24,7 +24,6 @@ 'model': 'LV-PUR131S', 'name': 'Air Purifier 131s', 'name_by_user': None, - 'primary_integration': 'vesync', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -61,7 +60,6 @@ 'model': 'Core200S', 'name': 'Air Purifier 200s', 'name_by_user': None, - 'primary_integration': 'vesync', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -98,7 +96,6 @@ 'model': 'LAP-C401S-WJP', 'name': 'Air Purifier 400s', 'name_by_user': None, - 'primary_integration': 'vesync', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -135,7 +132,6 @@ 'model': 'LAP-C601S-WUS', 'name': 'Air Purifier 600s', 'name_by_user': None, - 'primary_integration': 'vesync', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -172,7 +168,6 @@ 'model': 'ESL100', 'name': 'Dimmable Light', 'name_by_user': None, - 'primary_integration': 'vesync', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -209,7 +204,6 @@ 'model': 'ESWD16', 'name': 'Dimmer Switch', 'name_by_user': None, - 'primary_integration': 'vesync', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -262,7 +256,6 @@ 'model': 'wifi-switch-1.3', 'name': 'Outlet', 'name_by_user': None, - 'primary_integration': 'vesync', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -343,7 +336,6 @@ 'model': 'ESL100CW', 'name': 'Temperature Light', 'name_by_user': None, - 'primary_integration': 'vesync', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -380,7 +372,6 @@ 'model': 'ESWL01', 'name': 'Wall Switch', 'name_by_user': None, - 'primary_integration': 'vesync', 'serial_number': None, 'suggested_area': None, 'sw_version': None, diff --git a/tests/components/whois/snapshots/test_sensor.ambr b/tests/components/whois/snapshots/test_sensor.ambr index 409541b6322..61762c36e59 100644 --- a/tests/components/whois/snapshots/test_sensor.ambr +++ b/tests/components/whois/snapshots/test_sensor.ambr @@ -69,7 +69,6 @@ 'model': None, 'name': 'home-assistant.io', 'name_by_user': None, - 'primary_integration': 'whois', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -147,7 +146,6 @@ 'model': None, 'name': 'home-assistant.io', 'name_by_user': None, - 'primary_integration': 'whois', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -229,7 +227,6 @@ 'model': None, 'name': 'home-assistant.io', 'name_by_user': None, - 'primary_integration': 'whois', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -307,7 +304,6 @@ 'model': None, 'name': 'home-assistant.io', 'name_by_user': None, - 'primary_integration': 'whois', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -385,7 +381,6 @@ 'model': None, 'name': 'home-assistant.io', 'name_by_user': None, - 'primary_integration': 'whois', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -462,7 +457,6 @@ 'model': None, 'name': 'home-assistant.io', 'name_by_user': None, - 'primary_integration': 'whois', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -539,7 +533,6 @@ 'model': None, 'name': 'home-assistant.io', 'name_by_user': None, - 'primary_integration': 'whois', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -616,7 +609,6 @@ 'model': None, 'name': 'home-assistant.io', 'name_by_user': None, - 'primary_integration': 'whois', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -693,7 +685,6 @@ 'model': None, 'name': 'home-assistant.io', 'name_by_user': None, - 'primary_integration': 'whois', 'serial_number': None, 'suggested_area': None, 'sw_version': None, diff --git a/tests/components/wled/snapshots/test_binary_sensor.ambr b/tests/components/wled/snapshots/test_binary_sensor.ambr index ab30bff1729..b9a083336d2 100644 --- a/tests/components/wled/snapshots/test_binary_sensor.ambr +++ b/tests/components/wled/snapshots/test_binary_sensor.ambr @@ -74,7 +74,6 @@ 'model': 'DIY light', 'name': 'WLED RGB Light', 'name_by_user': None, - 'primary_integration': 'wled', 'serial_number': None, 'suggested_area': None, 'sw_version': '0.8.5', diff --git a/tests/components/wled/snapshots/test_button.ambr b/tests/components/wled/snapshots/test_button.ambr index 5fb2ac08be7..b489bcc0a71 100644 --- a/tests/components/wled/snapshots/test_button.ambr +++ b/tests/components/wled/snapshots/test_button.ambr @@ -74,7 +74,6 @@ 'model': 'DIY light', 'name': 'WLED RGB Light', 'name_by_user': None, - 'primary_integration': 'wled', 'serial_number': None, 'suggested_area': None, 'sw_version': '0.8.5', diff --git a/tests/components/wled/snapshots/test_number.ambr b/tests/components/wled/snapshots/test_number.ambr index 9c3498372bf..c3440108148 100644 --- a/tests/components/wled/snapshots/test_number.ambr +++ b/tests/components/wled/snapshots/test_number.ambr @@ -82,7 +82,6 @@ 'model': 'DIY light', 'name': 'WLED RGB Light', 'name_by_user': None, - 'primary_integration': 'wled', 'serial_number': None, 'suggested_area': None, 'sw_version': '0.8.5', @@ -172,7 +171,6 @@ 'model': 'DIY light', 'name': 'WLED RGB Light', 'name_by_user': None, - 'primary_integration': 'wled', 'serial_number': None, 'suggested_area': None, 'sw_version': '0.8.5', diff --git a/tests/components/wled/snapshots/test_select.ambr b/tests/components/wled/snapshots/test_select.ambr index 41df21c0223..6d64ec43658 100644 --- a/tests/components/wled/snapshots/test_select.ambr +++ b/tests/components/wled/snapshots/test_select.ambr @@ -84,7 +84,6 @@ 'model': 'DIY light', 'name': 'WLED RGB Light', 'name_by_user': None, - 'primary_integration': 'wled', 'serial_number': None, 'suggested_area': None, 'sw_version': '0.8.5', @@ -270,7 +269,6 @@ 'model': 'DIY light', 'name': 'WLED RGB Light', 'name_by_user': None, - 'primary_integration': 'wled', 'serial_number': None, 'suggested_area': None, 'sw_version': '0.8.5', @@ -360,7 +358,6 @@ 'model': 'DIY light', 'name': 'WLED RGBW Light', 'name_by_user': None, - 'primary_integration': 'wled', 'serial_number': None, 'suggested_area': None, 'sw_version': '0.8.6b4', @@ -450,7 +447,6 @@ 'model': 'DIY light', 'name': 'WLED RGBW Light', 'name_by_user': None, - 'primary_integration': 'wled', 'serial_number': None, 'suggested_area': None, 'sw_version': '0.8.6b4', diff --git a/tests/components/wled/snapshots/test_switch.ambr b/tests/components/wled/snapshots/test_switch.ambr index 4d7a7d59798..da69e686f07 100644 --- a/tests/components/wled/snapshots/test_switch.ambr +++ b/tests/components/wled/snapshots/test_switch.ambr @@ -76,7 +76,6 @@ 'model': 'DIY light', 'name': 'WLED RGB Light', 'name_by_user': None, - 'primary_integration': 'wled', 'serial_number': None, 'suggested_area': None, 'sw_version': '0.8.5', @@ -157,7 +156,6 @@ 'model': 'DIY light', 'name': 'WLED RGB Light', 'name_by_user': None, - 'primary_integration': 'wled', 'serial_number': None, 'suggested_area': None, 'sw_version': '0.8.5', @@ -239,7 +237,6 @@ 'model': 'DIY light', 'name': 'WLED RGB Light', 'name_by_user': None, - 'primary_integration': 'wled', 'serial_number': None, 'suggested_area': None, 'sw_version': '0.8.5', @@ -321,7 +318,6 @@ 'model': 'DIY light', 'name': 'WLED RGB Light', 'name_by_user': None, - 'primary_integration': 'wled', 'serial_number': None, 'suggested_area': None, 'sw_version': '0.8.5', diff --git a/tests/helpers/test_device_registry.py b/tests/helpers/test_device_registry.py index ad0df1f9f25..b141e29f678 100644 --- a/tests/helpers/test_device_registry.py +++ b/tests/helpers/test_device_registry.py @@ -90,7 +90,7 @@ async def test_get_or_create_returns_same_entry( await hass.async_block_till_done() # Only 2 update events. The third entry did not generate any changes. - assert len(update_events) == 2 + assert len(update_events) == 2, update_events assert update_events[0].data == { "action": "create", "device_id": entry.id, @@ -170,7 +170,9 @@ async def test_multiple_config_entries( assert len(device_registry.devices) == 1 assert entry.id == entry2.id assert entry.id == entry3.id - assert entry2.config_entries == {config_entry_1.entry_id, config_entry_2.entry_id} + assert entry2.config_entries == [config_entry_2.entry_id, config_entry_1.entry_id] + # the 3rd get_or_create was a primary update, so that's now first config entry + assert entry3.config_entries == [config_entry_1.entry_id, config_entry_2.entry_id] @pytest.mark.parametrize("load_registries", [False]) @@ -231,7 +233,7 @@ async def test_loading_from_storage( ) assert entry == dr.DeviceEntry( area_id="12345A", - config_entries={mock_config_entry.entry_id}, + config_entries=[mock_config_entry.entry_id], configuration_url="https://example.com/config", connections={("Zigbee", "01.23.45.67.89")}, disabled_by=dr.DeviceEntryDisabler.USER, @@ -248,7 +250,7 @@ async def test_loading_from_storage( suggested_area=None, # Not stored sw_version="version", ) - assert isinstance(entry.config_entries, set) + assert isinstance(entry.config_entries, list) assert isinstance(entry.connections, set) assert isinstance(entry.identifiers, set) @@ -261,7 +263,7 @@ async def test_loading_from_storage( model="model", ) assert entry == dr.DeviceEntry( - config_entries={mock_config_entry.entry_id}, + config_entries=[mock_config_entry.entry_id], connections={("Zigbee", "23.45.67.89.01")}, id="bcdefghijklmn", identifiers={("serial", "3456ABCDEF12")}, @@ -269,7 +271,7 @@ async def test_loading_from_storage( model="model", ) assert entry.id == "bcdefghijklmn" - assert isinstance(entry.config_entries, set) + assert isinstance(entry.config_entries, list) assert isinstance(entry.connections, set) assert isinstance(entry.identifiers, set) @@ -816,7 +818,7 @@ async def test_removing_config_entries( assert len(device_registry.devices) == 2 assert entry.id == entry2.id assert entry.id != entry3.id - assert entry2.config_entries == {config_entry_1.entry_id, config_entry_2.entry_id} + assert entry2.config_entries == [config_entry_2.entry_id, config_entry_1.entry_id] device_registry.async_clear_config_entry(config_entry_1.entry_id) entry = device_registry.async_get_device(identifiers={("bridgeid", "0123")}) @@ -824,7 +826,7 @@ async def test_removing_config_entries( identifiers={("bridgeid", "4567")} ) - assert entry.config_entries == {config_entry_2.entry_id} + assert entry.config_entries == [config_entry_2.entry_id] assert entry3_removed is None await hass.async_block_till_done() @@ -837,7 +839,7 @@ async def test_removing_config_entries( assert update_events[1].data == { "action": "update", "device_id": entry.id, - "changes": {"config_entries": {config_entry_1.entry_id}}, + "changes": {"config_entries": [config_entry_1.entry_id]}, } assert update_events[2].data == { "action": "create", @@ -847,7 +849,7 @@ async def test_removing_config_entries( "action": "update", "device_id": entry.id, "changes": { - "config_entries": {config_entry_1.entry_id, config_entry_2.entry_id} + "config_entries": [config_entry_2.entry_id, config_entry_1.entry_id] }, } assert update_events[4].data == { @@ -892,7 +894,7 @@ async def test_deleted_device_removing_config_entries( assert len(device_registry.deleted_devices) == 0 assert entry.id == entry2.id assert entry.id != entry3.id - assert entry2.config_entries == {config_entry_1.entry_id, config_entry_2.entry_id} + assert entry2.config_entries == [config_entry_2.entry_id, config_entry_1.entry_id] device_registry.async_remove_device(entry.id) device_registry.async_remove_device(entry3.id) @@ -909,7 +911,7 @@ async def test_deleted_device_removing_config_entries( assert update_events[1].data == { "action": "update", "device_id": entry2.id, - "changes": {"config_entries": {config_entry_1.entry_id}}, + "changes": {"config_entries": [config_entry_1.entry_id]}, } assert update_events[2].data == { "action": "create", @@ -1288,7 +1290,7 @@ async def test_update( assert updated_entry != entry assert updated_entry == dr.DeviceEntry( area_id="12345A", - config_entries={mock_config_entry.entry_id}, + config_entries=[mock_config_entry.entry_id], configuration_url="https://example.com/config", connections={("mac", "65:43:21:fe:dc:ba")}, disabled_by=dr.DeviceEntryDisabler.USER, @@ -1497,7 +1499,7 @@ async def test_update_remove_config_entries( assert len(device_registry.devices) == 2 assert entry.id == entry2.id assert entry.id != entry3.id - assert entry2.config_entries == {config_entry_1.entry_id, config_entry_2.entry_id} + assert entry2.config_entries == [config_entry_2.entry_id, config_entry_1.entry_id] updated_entry = device_registry.async_update_device( entry2.id, remove_config_entry_id=config_entry_1.entry_id @@ -1506,7 +1508,7 @@ async def test_update_remove_config_entries( entry3.id, remove_config_entry_id=config_entry_1.entry_id ) - assert updated_entry.config_entries == {config_entry_2.entry_id} + assert updated_entry.config_entries == [config_entry_2.entry_id] assert removed_entry is None removed_entry = device_registry.async_get_device(identifiers={("bridgeid", "4567")}) @@ -1523,7 +1525,7 @@ async def test_update_remove_config_entries( assert update_events[1].data == { "action": "update", "device_id": entry2.id, - "changes": {"config_entries": {config_entry_1.entry_id}}, + "changes": {"config_entries": [config_entry_1.entry_id]}, } assert update_events[2].data == { "action": "create", @@ -1533,7 +1535,7 @@ async def test_update_remove_config_entries( "action": "update", "device_id": entry.id, "changes": { - "config_entries": {config_entry_1.entry_id, config_entry_2.entry_id} + "config_entries": [config_entry_2.entry_id, config_entry_1.entry_id] }, } assert update_events[4].data == { @@ -1766,7 +1768,7 @@ async def test_restore_device( assert len(device_registry.devices) == 2 assert len(device_registry.deleted_devices) == 0 - assert isinstance(entry3.config_entries, set) + assert isinstance(entry3.config_entries, list) assert isinstance(entry3.connections, set) assert isinstance(entry3.identifiers, set) @@ -1898,7 +1900,7 @@ async def test_restore_shared_device( assert len(device_registry.devices) == 1 assert len(device_registry.deleted_devices) == 0 - assert isinstance(entry2.config_entries, set) + assert isinstance(entry2.config_entries, list) assert isinstance(entry2.connections, set) assert isinstance(entry2.identifiers, set) @@ -1916,7 +1918,7 @@ async def test_restore_shared_device( assert len(device_registry.devices) == 1 assert len(device_registry.deleted_devices) == 0 - assert isinstance(entry3.config_entries, set) + assert isinstance(entry3.config_entries, list) assert isinstance(entry3.connections, set) assert isinstance(entry3.identifiers, set) @@ -1932,7 +1934,7 @@ async def test_restore_shared_device( assert len(device_registry.devices) == 1 assert len(device_registry.deleted_devices) == 0 - assert isinstance(entry4.config_entries, set) + assert isinstance(entry4.config_entries, list) assert isinstance(entry4.connections, set) assert isinstance(entry4.identifiers, set) @@ -1947,7 +1949,7 @@ async def test_restore_shared_device( "action": "update", "device_id": entry.id, "changes": { - "config_entries": {config_entry_1.entry_id}, + "config_entries": [config_entry_1.entry_id], "identifiers": {("entry_123", "0123")}, }, } @@ -1971,7 +1973,7 @@ async def test_restore_shared_device( "action": "update", "device_id": entry.id, "changes": { - "config_entries": {config_entry_2.entry_id}, + "config_entries": [config_entry_2.entry_id], "identifiers": {("entry_234", "2345")}, }, } @@ -2628,39 +2630,3 @@ async def test_async_remove_device_thread_safety( await hass.async_add_executor_job( device_registry.async_remove_device, device.id ) - - -async def test_primary_integration( - hass: HomeAssistant, - device_registry: dr.DeviceRegistry, - mock_config_entry: MockConfigEntry, -) -> None: - """Test the primary integration field.""" - # Update existing - device = device_registry.async_get_or_create( - config_entry_id=mock_config_entry.entry_id, - connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, - identifiers=set(), - manufacturer="manufacturer", - model="model", - ) - assert device.primary_integration is None - - device = device_registry.async_get_or_create( - config_entry_id=mock_config_entry.entry_id, - connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, - model="model 2", - domain="test_domain", - ) - assert device.primary_integration == "test_domain" - - # Create new - device = device_registry.async_get_or_create( - config_entry_id=mock_config_entry.entry_id, - connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, - identifiers=set(), - manufacturer="manufacturer", - model="model", - domain="test_domain", - ) - assert device.primary_integration == "test_domain" diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py index c28a88e8df8..56ddcd9a6c9 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -1191,7 +1191,6 @@ async def test_device_info_called( assert device.sw_version == "test-sw" assert device.hw_version == "test-hw" assert device.via_device_id == via.id - assert device.primary_integration == config_entry.domain async def test_device_info_not_overrides( diff --git a/tests/helpers/test_entity_registry.py b/tests/helpers/test_entity_registry.py index 4dc8d79be3f..1390ef3889d 100644 --- a/tests/helpers/test_entity_registry.py +++ b/tests/helpers/test_entity_registry.py @@ -1106,10 +1106,10 @@ async def test_remove_config_entry_from_device_removes_entities( config_entry_id=config_entry_2.entry_id, connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, ) - assert device_entry.config_entries == { + assert device_entry.config_entries == [ config_entry_1.entry_id, config_entry_2.entry_id, - } + ] # Create one entity for each config entry entry_1 = entity_registry.async_get_or_create( @@ -1174,10 +1174,10 @@ async def test_remove_config_entry_from_device_removes_entities_2( config_entry_id=config_entry_2.entry_id, connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, ) - assert device_entry.config_entries == { + assert device_entry.config_entries == [ config_entry_1.entry_id, config_entry_2.entry_id, - } + ] # Create one entity for each config entry entry_1 = entity_registry.async_get_or_create( From faa55de538210554aa1311ea343c618a3fdfa449 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 18 Jun 2024 01:18:31 -0500 Subject: [PATCH 0771/1445] Fix blocking I/O in the event loop when registering static paths (#119629) --- homeassistant/components/dynalite/__init__.py | 5 +- homeassistant/components/dynalite/panel.py | 30 +++--- homeassistant/components/frontend/__init__.py | 17 ++-- homeassistant/components/hassio/__init__.py | 11 ++- homeassistant/components/http/__init__.py | 98 +++++++++++++++---- .../components/insteon/api/__init__.py | 5 +- homeassistant/components/knx/manifest.json | 2 +- homeassistant/components/knx/websocket.py | 13 ++- tests/components/frontend/test_init.py | 21 ++++ tests/components/http/test_static.py | 22 +++++ 10 files changed, 171 insertions(+), 53 deletions(-) diff --git a/homeassistant/components/dynalite/__init__.py b/homeassistant/components/dynalite/__init__.py index 46fcfb267d0..59b8e464bb0 100644 --- a/homeassistant/components/dynalite/__init__.py +++ b/homeassistant/components/dynalite/__init__.py @@ -106,6 +106,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: ), ) + await async_register_dynalite_frontend(hass) + return True @@ -131,9 +133,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: raise ConfigEntryNotReady await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) - - await async_register_dynalite_frontend(hass) - return True diff --git a/homeassistant/components/dynalite/panel.py b/homeassistant/components/dynalite/panel.py index b7020367f74..b62944f63fe 100644 --- a/homeassistant/components/dynalite/panel.py +++ b/homeassistant/components/dynalite/panel.py @@ -5,6 +5,7 @@ import voluptuous as vol from homeassistant.components import panel_custom, websocket_api from homeassistant.components.cover import DEVICE_CLASSES +from homeassistant.components.http import StaticPathConfig from homeassistant.const import CONF_DEFAULT, CONF_HOST, CONF_NAME, CONF_PORT from homeassistant.core import HomeAssistant, callback @@ -98,19 +99,18 @@ async def async_register_dynalite_frontend(hass: HomeAssistant): """Register the Dynalite frontend configuration panel.""" websocket_api.async_register_command(hass, get_dynalite_config) websocket_api.async_register_command(hass, save_dynalite_config) - if DOMAIN not in hass.data.get("frontend_panels", {}): - path = locate_dir() - build_id = get_build_id() - hass.http.register_static_path( - URL_BASE, path, cache_headers=(build_id != "dev") - ) + path = locate_dir() + build_id = get_build_id() + await hass.http.async_register_static_paths( + [StaticPathConfig(URL_BASE, path, cache_headers=(build_id != "dev"))] + ) - await panel_custom.async_register_panel( - hass=hass, - frontend_url_path=DOMAIN, - config_panel_domain=DOMAIN, - webcomponent_name="dynalite-panel", - module_url=f"{URL_BASE}/entrypoint-{build_id}.js", - embed_iframe=True, - require_admin=True, - ) + await panel_custom.async_register_panel( + hass=hass, + frontend_url_path=DOMAIN, + config_panel_domain=DOMAIN, + webcomponent_name="dynalite-panel", + module_url=f"{URL_BASE}/entrypoint-{build_id}.js", + embed_iframe=True, + require_admin=True, + ) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 7ff7f76c61c..2f038e34102 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -15,7 +15,7 @@ import voluptuous as vol from yarl import URL from homeassistant.components import onboarding, websocket_api -from homeassistant.components.http import KEY_HASS, HomeAssistantView +from homeassistant.components.http import KEY_HASS, HomeAssistantView, StaticPathConfig from homeassistant.components.websocket_api.connection import ActiveConnection from homeassistant.config import async_hass_config_yaml from homeassistant.const import ( @@ -378,6 +378,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: is_dev = repo_path is not None root_path = _frontend_root(repo_path) + static_paths_configs: list[StaticPathConfig] = [] + for path, should_cache in ( ("service_worker.js", False), ("robots.txt", False), @@ -386,10 +388,12 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: ("frontend_latest", not is_dev), ("frontend_es5", not is_dev), ): - hass.http.register_static_path(f"/{path}", str(root_path / path), should_cache) + static_paths_configs.append( + StaticPathConfig(f"/{path}", str(root_path / path), should_cache) + ) - hass.http.register_static_path( - "/auth/authorize", str(root_path / "authorize.html"), False + static_paths_configs.append( + StaticPathConfig("/auth/authorize", str(root_path / "authorize.html"), False) ) # https://wicg.github.io/change-password-url/ hass.http.register_redirect( @@ -397,9 +401,10 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: ) local = hass.config.path("www") - if os.path.isdir(local): - hass.http.register_static_path("/local", local, not is_dev) + if await hass.async_add_executor_job(os.path.isdir, local): + static_paths_configs.append(StaticPathConfig("/local", local, not is_dev)) + await hass.http.async_register_static_paths(static_paths_configs) # Shopping list panel was replaced by todo panel in 2023.11 hass.http.register_redirect("/shopping-list", "/todo") diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index 34d15501c48..647c2248d56 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -15,6 +15,7 @@ import voluptuous as vol from homeassistant.auth.const import GROUP_ID_ADMIN from homeassistant.components import panel_custom from homeassistant.components.homeassistant import async_set_stop_handler +from homeassistant.components.http import StaticPathConfig from homeassistant.config_entries import SOURCE_SYSTEM, ConfigEntry from homeassistant.const import ( ATTR_NAME, @@ -350,8 +351,14 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: # This overrides the normal API call that would be forwarded development_repo = config.get(DOMAIN, {}).get(CONF_FRONTEND_REPO) if development_repo is not None: - hass.http.register_static_path( - "/api/hassio/app", os.path.join(development_repo, "hassio/build"), False + await hass.http.async_register_static_paths( + [ + StaticPathConfig( + "/api/hassio/app", + os.path.join(development_repo, "hassio/build"), + False, + ) + ] ) hass.http.register_view(HassIOView(host, websession)) diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index b48e9f9615c..4e62df3a024 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -3,7 +3,10 @@ from __future__ import annotations import asyncio +from collections.abc import Collection +from dataclasses import dataclass import datetime +from functools import partial from ipaddress import IPv4Network, IPv6Network, ip_network import logging import os @@ -29,7 +32,7 @@ from yarl import URL from homeassistant.components.network import async_get_source_ip from homeassistant.const import EVENT_HOMEASSISTANT_STOP, SERVER_PORT -from homeassistant.core import Event, HomeAssistant +from homeassistant.core import Event, HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import storage import homeassistant.helpers.config_validation as cv @@ -134,6 +137,21 @@ HTTP_SCHEMA: Final = vol.All( CONFIG_SCHEMA: Final = vol.Schema({DOMAIN: HTTP_SCHEMA}, extra=vol.ALLOW_EXTRA) +@dataclass(slots=True) +class StaticPathConfig: + """Configuration for a static path.""" + + url_path: str + path: str + cache_headers: bool = True + + +_STATIC_CLASSES = { + True: CachingStaticResource, + False: web.StaticResource, +} + + class ConfData(TypedDict, total=False): """Typed dict for config data.""" @@ -284,6 +302,16 @@ class HomeAssistantApplication(web.Application): ) +async def _serve_file_with_cache_headers( + path: str, request: web.Request +) -> web.FileResponse: + return web.FileResponse(path, headers=CACHE_HEADERS) + + +async def _serve_file(path: str, request: web.Request) -> web.FileResponse: + return web.FileResponse(path) + + class HomeAssistantHTTP: """HTTP server for Home Assistant.""" @@ -403,30 +431,58 @@ class HomeAssistantHTTP: self.app.router.add_route("GET", url, redirect) ) + def _make_static_resources( + self, configs: Collection[StaticPathConfig] + ) -> dict[str, CachingStaticResource | web.StaticResource | None]: + """Create a list of static resources.""" + return { + config.url_path: _STATIC_CLASSES[config.cache_headers]( + config.url_path, config.path + ) + if os.path.isdir(config.path) + else None + for config in configs + } + + async def async_register_static_paths( + self, configs: Collection[StaticPathConfig] + ) -> None: + """Register a folder or file to serve as a static path.""" + resources = await self.hass.async_add_executor_job( + self._make_static_resources, configs + ) + self._async_register_static_paths(configs, resources) + + @callback + def _async_register_static_paths( + self, + configs: Collection[StaticPathConfig], + resources: dict[str, CachingStaticResource | web.StaticResource | None], + ) -> None: + """Register a folders or files to serve as a static path.""" + app = self.app + allow_cors = app[KEY_ALLOW_CONFIGRED_CORS] + for config in configs: + if resource := resources[config.url_path]: + app.router.register_resource(resource) + allow_cors(resource) + + target = ( + _serve_file_with_cache_headers if config.cache_headers else _serve_file + ) + allow_cors( + self.app.router.add_route( + "GET", config.url_path, partial(target, config.path) + ) + ) + def register_static_path( self, url_path: str, path: str, cache_headers: bool = True ) -> None: """Register a folder or file to serve as a static path.""" - if os.path.isdir(path): - if cache_headers: - resource: CachingStaticResource | web.StaticResource = ( - CachingStaticResource(url_path, path) - ) - else: - resource = web.StaticResource(url_path, path) - self.app.router.register_resource(resource) - self.app[KEY_ALLOW_CONFIGRED_CORS](resource) - return - - async def serve_file(request: web.Request) -> web.FileResponse: - """Serve file from disk.""" - if cache_headers: - return web.FileResponse(path, headers=CACHE_HEADERS) - return web.FileResponse(path) - - self.app[KEY_ALLOW_CONFIGRED_CORS]( - self.app.router.add_route("GET", url_path, serve_file) - ) + configs = [StaticPathConfig(url_path, path, cache_headers)] + resources = self._make_static_resources(configs) + self._async_register_static_paths(configs, resources) def _create_ssl_context(self) -> ssl.SSLContext | None: context: ssl.SSLContext | None = None diff --git a/homeassistant/components/insteon/api/__init__.py b/homeassistant/components/insteon/api/__init__.py index 1f671aa1343..b19b1912340 100644 --- a/homeassistant/components/insteon/api/__init__.py +++ b/homeassistant/components/insteon/api/__init__.py @@ -3,6 +3,7 @@ from insteon_frontend import get_build_id, locate_dir from homeassistant.components import panel_custom, websocket_api +from homeassistant.components.http import StaticPathConfig from homeassistant.core import HomeAssistant, callback from ..const import CONF_DEV_PATH, DOMAIN @@ -91,7 +92,9 @@ async def async_register_insteon_frontend(hass: HomeAssistant): is_dev = dev_path is not None path = dev_path if dev_path else locate_dir() build_id = get_build_id(is_dev) - hass.http.register_static_path(URL_BASE, path, cache_headers=not is_dev) + await hass.http.async_register_static_paths( + [StaticPathConfig(URL_BASE, path, cache_headers=not is_dev)] + ) await panel_custom.async_register_panel( hass=hass, diff --git a/homeassistant/components/knx/manifest.json b/homeassistant/components/knx/manifest.json index af0c6b8d01c..3e8986641e7 100644 --- a/homeassistant/components/knx/manifest.json +++ b/homeassistant/components/knx/manifest.json @@ -4,7 +4,7 @@ "after_dependencies": ["panel_custom"], "codeowners": ["@Julius2342", "@farmio", "@marvin-w"], "config_flow": true, - "dependencies": ["file_upload", "websocket_api"], + "dependencies": ["file_upload", "http", "websocket_api"], "documentation": "https://www.home-assistant.io/integrations/knx", "integration_type": "hub", "iot_class": "local_push", diff --git a/homeassistant/components/knx/websocket.py b/homeassistant/components/knx/websocket.py index dc5b5e483be..0ac5a21d333 100644 --- a/homeassistant/components/knx/websocket.py +++ b/homeassistant/components/knx/websocket.py @@ -9,6 +9,7 @@ import voluptuous as vol from xknxproject.exceptions import XknxProjectException from homeassistant.components import panel_custom, websocket_api +from homeassistant.components.http import StaticPathConfig from homeassistant.core import HomeAssistant, callback from .const import DOMAIN @@ -31,10 +32,14 @@ async def register_panel(hass: HomeAssistant) -> None: websocket_api.async_register_command(hass, ws_get_knx_project) if DOMAIN not in hass.data.get("frontend_panels", {}): - hass.http.register_static_path( - URL_BASE, - path=knx_panel.locate_dir(), - cache_headers=knx_panel.is_prod_build, + await hass.http.async_register_static_paths( + [ + StaticPathConfig( + URL_BASE, + path=knx_panel.locate_dir(), + cache_headers=knx_panel.is_prod_build, + ) + ] ) await panel_custom.async_register_panel( hass=hass, diff --git a/tests/components/frontend/test_init.py b/tests/components/frontend/test_init.py index 81bec28598d..b8642aa997d 100644 --- a/tests/components/frontend/test_init.py +++ b/tests/components/frontend/test_init.py @@ -2,6 +2,7 @@ from asyncio import AbstractEventLoop from http import HTTPStatus +from pathlib import Path import re from typing import Any from unittest.mock import patch @@ -787,3 +788,23 @@ async def test_get_icons_for_single_integration( assert msg["type"] == TYPE_RESULT assert msg["success"] assert msg["result"] == {"resources": {"http": {}}} + + +async def test_www_local_dir( + hass: HomeAssistant, tmp_path: Path, hass_client: ClientSessionGenerator +) -> None: + """Test local www folder.""" + hass.config.config_dir = str(tmp_path) + tmp_path_www = tmp_path / "www" + x_txt_file = tmp_path_www / "x.txt" + + def _create_www_and_x_txt(): + tmp_path_www.mkdir() + x_txt_file.write_text("any") + + await hass.async_add_executor_job(_create_www_and_x_txt) + + assert await async_setup_component(hass, "frontend", {}) + client = await hass_client() + resp = await client.get("/local/x.txt") + assert resp.status == HTTPStatus.OK diff --git a/tests/components/http/test_static.py b/tests/components/http/test_static.py index e3cf2f50c15..92e92cdb4a7 100644 --- a/tests/components/http/test_static.py +++ b/tests/components/http/test_static.py @@ -1,11 +1,13 @@ """The tests for http static files.""" +from http import HTTPStatus from pathlib import Path from aiohttp.test_utils import TestClient from aiohttp.web_exceptions import HTTPForbidden import pytest +from homeassistant.components.http import StaticPathConfig from homeassistant.components.http.static import CachingStaticResource, _get_file_path from homeassistant.core import EVENT_HOMEASSISTANT_START, HomeAssistant from homeassistant.helpers.http import KEY_ALLOW_CONFIGRED_CORS @@ -59,3 +61,23 @@ async def test_static_path_blocks_anchors( # changes we still block it. with pytest.raises(HTTPForbidden): _get_file_path(canonical_url, tmp_path) + + +async def test_async_register_static_paths( + hass: HomeAssistant, hass_client: ClientSessionGenerator +) -> None: + """Test registering multiple static paths.""" + assert await async_setup_component(hass, "frontend", {}) + path = str(Path(__file__).parent) + await hass.http.async_register_static_paths( + [ + StaticPathConfig("/something", path), + StaticPathConfig("/something_else", path), + ] + ) + + client = await hass_client() + resp = await client.get("/something/__init__.py") + assert resp.status == HTTPStatus.OK + resp = await client.get("/something_else/__init__.py") + assert resp.status == HTTPStatus.OK From 2555827030a1bcb97bd378ae8b4a41a9bbae7b3e Mon Sep 17 00:00:00 2001 From: dontinelli <73341522+dontinelli@users.noreply.github.com> Date: Tue, 18 Jun 2024 09:06:22 +0200 Subject: [PATCH 0772/1445] Replace Solarlog unmaintained library (#117484) Co-authored-by: Robert Resch --- CODEOWNERS | 4 +- homeassistant/components/solarlog/__init__.py | 48 ++++++++ .../components/solarlog/config_flow.py | 73 ++++++++----- .../components/solarlog/coordinator.py | 41 +++---- .../components/solarlog/manifest.json | 6 +- homeassistant/components/solarlog/sensor.py | 7 +- .../components/solarlog/strings.json | 6 +- requirements_all.txt | 6 +- requirements_test_all.txt | 6 +- tests/components/solarlog/conftest.py | 54 +++++++++ tests/components/solarlog/test_config_flow.py | 103 +++++++++++++----- tests/components/solarlog/test_init.py | 57 ++++++++++ 12 files changed, 320 insertions(+), 91 deletions(-) create mode 100644 tests/components/solarlog/conftest.py create mode 100644 tests/components/solarlog/test_init.py diff --git a/CODEOWNERS b/CODEOWNERS index fa8db6628ce..103c66d3994 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1305,8 +1305,8 @@ build.json @home-assistant/supervisor /homeassistant/components/solaredge/ @frenck @bdraco /tests/components/solaredge/ @frenck @bdraco /homeassistant/components/solaredge_local/ @drobtravels @scheric -/homeassistant/components/solarlog/ @Ernst79 -/tests/components/solarlog/ @Ernst79 +/homeassistant/components/solarlog/ @Ernst79 @dontinelli +/tests/components/solarlog/ @Ernst79 @dontinelli /homeassistant/components/solax/ @squishykid /tests/components/solax/ @squishykid /homeassistant/components/soma/ @ratsept @sebfortier2288 diff --git a/homeassistant/components/solarlog/__init__.py b/homeassistant/components/solarlog/__init__.py index d2a3c50295c..6975a420732 100644 --- a/homeassistant/components/solarlog/__init__.py +++ b/homeassistant/components/solarlog/__init__.py @@ -1,12 +1,17 @@ """Solar-Log integration.""" +import logging + from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er from .const import DOMAIN from .coordinator import SolarlogData +_LOGGER = logging.getLogger(__name__) + PLATFORMS = [Platform.SENSOR] @@ -22,3 +27,46 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + + +async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: + """Migrate old entry.""" + _LOGGER.debug("Migrating from version %s", config_entry.version) + + if config_entry.version > 1: + # This means the user has downgraded from a future version + return False + + if config_entry.version == 1: + if config_entry.minor_version < 2: + # migrate old entity unique id + entity_reg = er.async_get(hass) + entities: list[er.RegistryEntry] = er.async_entries_for_config_entry( + entity_reg, config_entry.entry_id + ) + + for entity in entities: + if "time" in entity.unique_id: + new_uid = entity.unique_id.replace("time", "last_updated") + _LOGGER.debug( + "migrate unique id '%s' to '%s'", entity.unique_id, new_uid + ) + entity_reg.async_update_entity( + entity.entity_id, new_unique_id=new_uid + ) + + # migrate config_entry + new = {**config_entry.data} + new["extended_data"] = False + + hass.config_entries.async_update_entry( + config_entry, data=new, minor_version=2, version=1 + ) + + _LOGGER.debug( + "Migration to version %s.%s successful", + config_entry.version, + config_entry.minor_version, + ) + + return True diff --git a/homeassistant/components/solarlog/config_flow.py b/homeassistant/components/solarlog/config_flow.py index 40343b5ac12..deda2d81779 100644 --- a/homeassistant/components/solarlog/config_flow.py +++ b/homeassistant/components/solarlog/config_flow.py @@ -1,13 +1,14 @@ """Config flow for solarlog integration.""" import logging +from typing import Any from urllib.parse import ParseResult, urlparse -from requests.exceptions import HTTPError, Timeout -from sunwatcher.solarlog.solarlog import SolarLog +from solarlog_cli.solarlog_connector import SolarLogConnector +from solarlog_cli.solarlog_exceptions import SolarLogConnectionError, SolarLogError import voluptuous as vol -from homeassistant.config_entries import ConfigFlow +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_HOST, CONF_NAME from homeassistant.core import HomeAssistant, callback from homeassistant.util import slugify @@ -29,6 +30,7 @@ class SolarLogConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for solarlog.""" VERSION = 1 + MINOR_VERSION = 2 def __init__(self) -> None: """Initialize the config flow.""" @@ -40,37 +42,44 @@ class SolarLogConfigFlow(ConfigFlow, domain=DOMAIN): return True return False + def _parse_url(self, host: str) -> str: + """Return parsed host url.""" + url = urlparse(host, "http") + netloc = url.netloc or url.path + path = url.path if url.netloc else "" + url = ParseResult("http", netloc, path, *url[3:]) + return url.geturl() + async def _test_connection(self, host): """Check if we can connect to the Solar-Log device.""" + solarlog = SolarLogConnector(host) try: - await self.hass.async_add_executor_job(SolarLog, host) - except (OSError, HTTPError, Timeout): - self._errors[CONF_HOST] = "cannot_connect" - _LOGGER.error( - "Could not connect to Solar-Log device at %s, check host ip address", - host, - ) + await solarlog.test_connection() + except SolarLogConnectionError: + self._errors = {CONF_HOST: "cannot_connect"} return False + except SolarLogError: # pylint: disable=broad-except + self._errors = {CONF_HOST: "unknown"} + return False + finally: + solarlog.client.close() + return True - async def async_step_user(self, user_input=None): + async def async_step_user(self, user_input=None) -> ConfigFlowResult: """Step when user initializes a integration.""" self._errors = {} if user_input is not None: # set some defaults in case we need to return to the form - name = slugify(user_input.get(CONF_NAME, DEFAULT_NAME)) - host_entry = user_input.get(CONF_HOST, DEFAULT_HOST) + user_input[CONF_NAME] = slugify(user_input[CONF_NAME]) + user_input[CONF_HOST] = self._parse_url(user_input[CONF_HOST]) - url = urlparse(host_entry, "http") - netloc = url.netloc or url.path - path = url.path if url.netloc else "" - url = ParseResult("http", netloc, path, *url[3:]) - host = url.geturl() - - if self._host_in_configuration_exists(host): + if self._host_in_configuration_exists(user_input[CONF_HOST]): self._errors[CONF_HOST] = "already_configured" - elif await self._test_connection(host): - return self.async_create_entry(title=name, data={CONF_HOST: host}) + elif await self._test_connection(user_input[CONF_HOST]): + return self.async_create_entry( + title=user_input[CONF_NAME], data=user_input + ) else: user_input = {} user_input[CONF_NAME] = DEFAULT_NAME @@ -86,21 +95,25 @@ class SolarLogConfigFlow(ConfigFlow, domain=DOMAIN): vol.Required( CONF_HOST, default=user_input.get(CONF_HOST, DEFAULT_HOST) ): str, + vol.Required("extended_data", default=False): bool, } ), errors=self._errors, ) - async def async_step_import(self, user_input=None): + async def async_step_import(self, user_input: dict[str, Any]) -> ConfigFlowResult: """Import a config entry.""" - host_entry = user_input.get(CONF_HOST, DEFAULT_HOST) - url = urlparse(host_entry, "http") - netloc = url.netloc or url.path - path = url.path if url.netloc else "" - url = ParseResult("http", netloc, path, *url[3:]) - host = url.geturl() + user_input = { + CONF_HOST: DEFAULT_HOST, + CONF_NAME: DEFAULT_NAME, + "extended_data": False, + **user_input, + } - if self._host_in_configuration_exists(host): + user_input[CONF_HOST] = self._parse_url(user_input[CONF_HOST]) + + if self._host_in_configuration_exists(user_input[CONF_HOST]): return self.async_abort(reason="already_configured") + return await self.async_step_user(user_input) diff --git a/homeassistant/components/solarlog/coordinator.py b/homeassistant/components/solarlog/coordinator.py index 6af7c96302d..794e556add5 100644 --- a/homeassistant/components/solarlog/coordinator.py +++ b/homeassistant/components/solarlog/coordinator.py @@ -4,12 +4,16 @@ from datetime import timedelta import logging from urllib.parse import ParseResult, urlparse -from requests.exceptions import HTTPError, Timeout -from sunwatcher.solarlog.solarlog import SolarLog +from solarlog_cli.solarlog_connector import SolarLogConnector +from solarlog_cli.solarlog_exceptions import ( + SolarLogConnectionError, + SolarLogUpdateError, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import update_coordinator _LOGGER = logging.getLogger(__name__) @@ -34,24 +38,23 @@ class SolarlogData(update_coordinator.DataUpdateCoordinator): self.name = entry.title self.host = url.geturl() - async def _async_update_data(self): - """Update the data from the SolarLog device.""" - try: - data = await self.hass.async_add_executor_job(SolarLog, self.host) - except (OSError, Timeout, HTTPError) as err: - raise update_coordinator.UpdateFailed(err) from err + extended_data = entry.data["extended_data"] - if data.time.year == 1999: - raise update_coordinator.UpdateFailed( - "Invalid data returned (can happen after Solarlog restart)." - ) - - self.logger.debug( - ( - "Connection to Solarlog successful. Retrieving latest Solarlog update" - " of %s" - ), - data.time, + self.solarlog = SolarLogConnector( + self.host, extended_data, hass.config.time_zone ) + async def _async_update_data(self): + """Update the data from the SolarLog device.""" + _LOGGER.debug("Start data update") + + try: + data = await self.solarlog.update_data() + except SolarLogConnectionError as err: + raise ConfigEntryNotReady(err) from err + except SolarLogUpdateError as err: + raise update_coordinator.UpdateFailed(err) from err + + _LOGGER.debug("Data successfully updated") + return data diff --git a/homeassistant/components/solarlog/manifest.json b/homeassistant/components/solarlog/manifest.json index 78075123996..0878d652f43 100644 --- a/homeassistant/components/solarlog/manifest.json +++ b/homeassistant/components/solarlog/manifest.json @@ -1,10 +1,10 @@ { "domain": "solarlog", "name": "Solar-Log", - "codeowners": ["@Ernst79"], + "codeowners": ["@Ernst79", "@dontinelli"], "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/solarlog", "iot_class": "local_polling", - "loggers": ["sunwatcher"], - "requirements": ["sunwatcher==0.2.1"] + "loggers": ["solarlog_cli"], + "requirements": ["solarlog_cli==0.1.5"] } diff --git a/homeassistant/components/solarlog/sensor.py b/homeassistant/components/solarlog/sensor.py index dcb4afcb863..0b5d56f1a9e 100644 --- a/homeassistant/components/solarlog/sensor.py +++ b/homeassistant/components/solarlog/sensor.py @@ -21,7 +21,6 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity -from homeassistant.util.dt import as_local from . import SolarlogData from .const import DOMAIN @@ -36,10 +35,9 @@ class SolarLogSensorEntityDescription(SensorEntityDescription): SENSOR_TYPES: tuple[SolarLogSensorEntityDescription, ...] = ( SolarLogSensorEntityDescription( - key="time", + key="last_updated", translation_key="last_update", device_class=SensorDeviceClass.TIMESTAMP, - value=as_local, ), SolarLogSensorEntityDescription( key="power_ac", @@ -231,7 +229,8 @@ class SolarlogSensor(CoordinatorEntity[SolarlogData], SensorEntity): @property def native_value(self): """Return the native sensor value.""" - raw_attr = getattr(self.coordinator.data, self.entity_description.key) + raw_attr = self.coordinator.data.get(self.entity_description.key) + if self.entity_description.value: return self.entity_description.value(raw_attr) return raw_attr diff --git a/homeassistant/components/solarlog/strings.json b/homeassistant/components/solarlog/strings.json index 5f5e2ae7a5f..255f35114c1 100644 --- a/homeassistant/components/solarlog/strings.json +++ b/homeassistant/components/solarlog/strings.json @@ -5,7 +5,8 @@ "title": "Define your Solar-Log connection", "data": { "host": "[%key:common::config_flow::data::host%]", - "name": "The prefix to be used for your Solar-Log sensors" + "name": "The prefix to be used for your Solar-Log sensors", + "extended_data": "Get additional data from Solar-Log. Extended data is only accessible, if no password is set for the Solar-Log. Use at your own risk!" }, "data_description": { "host": "The hostname or IP address of your Solar-Log device." @@ -14,7 +15,8 @@ }, "error": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "unknown": "[%key:common::config_flow::error::unknown%]" }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" diff --git a/requirements_all.txt b/requirements_all.txt index 49cf1b84843..4ecfd25800d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2601,6 +2601,9 @@ soco==0.30.4 # homeassistant.components.solaredge_local solaredge-local==0.2.3 +# homeassistant.components.solarlog +solarlog_cli==0.1.5 + # homeassistant.components.solax solax==3.1.1 @@ -2661,9 +2664,6 @@ stringcase==1.2.0 # homeassistant.components.subaru subarulink==0.7.11 -# homeassistant.components.solarlog -sunwatcher==0.2.1 - # homeassistant.components.sunweg sunweg==3.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 55d2ccabc17..8dcef7f1575 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2020,6 +2020,9 @@ snapcast==2.3.6 # homeassistant.components.sonos soco==0.30.4 +# homeassistant.components.solarlog +solarlog_cli==0.1.5 + # homeassistant.components.solax solax==3.1.1 @@ -2077,9 +2080,6 @@ stringcase==1.2.0 # homeassistant.components.subaru subarulink==0.7.11 -# homeassistant.components.solarlog -sunwatcher==0.2.1 - # homeassistant.components.sunweg sunweg==3.0.1 diff --git a/tests/components/solarlog/conftest.py b/tests/components/solarlog/conftest.py new file mode 100644 index 00000000000..71034828025 --- /dev/null +++ b/tests/components/solarlog/conftest.py @@ -0,0 +1,54 @@ +"""Test helpers.""" + +from collections.abc import Generator +from unittest.mock import AsyncMock, patch + +import pytest + +from homeassistant.core import HomeAssistant + +from tests.common import mock_device_registry, mock_registry + + +@pytest.fixture +def mock_solarlog(): + """Build a fixture for the SolarLog API that connects successfully and returns one device.""" + + mock_solarlog_api = AsyncMock() + with patch( + "homeassistant.components.solarlog.config_flow.SolarLogConnector", + return_value=mock_solarlog_api, + ) as mock_solarlog_api: + mock_solarlog_api.return_value.test_connection.return_value = True + yield mock_solarlog_api + + +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock, None, None]: + """Override async_setup_entry.""" + with patch( + "homeassistant.components.solarlog.async_setup_entry", return_value=True + ) as mock_setup_entry: + yield mock_setup_entry + + +@pytest.fixture(name="test_connect") +def mock_test_connection(): + """Mock a successful _test_connection.""" + with patch( + "homeassistant.components.solarlog.config_flow.SolarLogConfigFlow._test_connection", + return_value=True, + ): + yield + + +@pytest.fixture(name="device_reg") +def device_reg_fixture(hass: HomeAssistant): + """Return an empty, loaded, registry.""" + return mock_device_registry(hass) + + +@pytest.fixture(name="entity_reg") +def entity_reg_fixture(hass: HomeAssistant): + """Return an empty, loaded, registry.""" + return mock_registry(hass) diff --git a/tests/components/solarlog/test_config_flow.py b/tests/components/solarlog/test_config_flow.py index c356a129806..63df582b0e1 100644 --- a/tests/components/solarlog/test_config_flow.py +++ b/tests/components/solarlog/test_config_flow.py @@ -1,8 +1,9 @@ """Test the solarlog config flow.""" -from unittest.mock import patch +from unittest.mock import AsyncMock, patch import pytest +from solarlog_cli.solarlog_exceptions import SolarLogConnectionError, SolarLogError from homeassistant import config_entries from homeassistant.components.solarlog import config_flow @@ -17,7 +18,7 @@ NAME = "Solarlog test 1 2 3" HOST = "http://1.1.1.1" -async def test_form(hass: HomeAssistant) -> None: +async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: """Test we get the form.""" result = await hass.config_entries.flow.async_init( @@ -29,34 +30,22 @@ async def test_form(hass: HomeAssistant) -> None: with ( patch( "homeassistant.components.solarlog.config_flow.SolarLogConfigFlow._test_connection", - return_value={"title": "solarlog test 1 2 3"}, - ), - patch( - "homeassistant.components.solarlog.async_setup_entry", return_value=True, - ) as mock_setup_entry, + ), ): result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], {"host": HOST, "name": NAME} + result["flow_id"], + {CONF_HOST: HOST, CONF_NAME: NAME, "extended_data": False}, ) await hass.async_block_till_done() assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "solarlog_test_1_2_3" - assert result2["data"] == {"host": "http://1.1.1.1"} + assert result2["data"][CONF_HOST] == "http://1.1.1.1" + assert result2["data"]["extended_data"] is False assert len(mock_setup_entry.mock_calls) == 1 -@pytest.fixture(name="test_connect") -def mock_controller(): - """Mock a successful _host_in_configuration_exists.""" - with patch( - "homeassistant.components.solarlog.config_flow.SolarLogConfigFlow._test_connection", - return_value=True, - ): - yield - - def init_config_flow(hass): """Init a configuration flow.""" flow = config_flow.SolarLogConfigFlow() @@ -64,19 +53,75 @@ def init_config_flow(hass): return flow -async def test_user(hass: HomeAssistant, test_connect) -> None: +@pytest.mark.usefixtures("test_connect") +async def test_user( + hass: HomeAssistant, + mock_solarlog: AsyncMock, + mock_setup_entry: AsyncMock, +) -> None: """Test user config.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {} + + # tests with all provided + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_HOST: HOST, CONF_NAME: NAME, "extended_data": False} + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "solarlog_test_1_2_3" + assert result["data"][CONF_HOST] == HOST + assert len(mock_setup_entry.mock_calls) == 1 + + +@pytest.mark.parametrize( + ("exception", "error"), + [ + (SolarLogConnectionError, {CONF_HOST: "cannot_connect"}), + (SolarLogError, {CONF_HOST: "unknown"}), + ], +) +async def test_form_exceptions( + hass: HomeAssistant, + exception: Exception, + error: dict[str, str], + mock_solarlog: AsyncMock, +) -> None: + """Test we can handle Form exceptions.""" flow = init_config_flow(hass) result = await flow.async_step_user() assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" - # tets with all provided - result = await flow.async_step_user({CONF_NAME: NAME, CONF_HOST: HOST}) + mock_solarlog.return_value.test_connection.side_effect = exception + + # tests with connection error + result = await flow.async_step_user( + {CONF_NAME: NAME, CONF_HOST: HOST, "extended_data": False} + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == error + + mock_solarlog.return_value.test_connection.side_effect = None + + # tests with all provided + result = await flow.async_step_user( + {CONF_NAME: NAME, CONF_HOST: HOST, "extended_data": False} + ) + await hass.async_block_till_done() + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "solarlog_test_1_2_3" assert result["data"][CONF_HOST] == HOST + assert result["data"]["extended_data"] is False async def test_import(hass: HomeAssistant, test_connect) -> None: @@ -85,18 +130,24 @@ async def test_import(hass: HomeAssistant, test_connect) -> None: # import with only host result = await flow.async_step_import({CONF_HOST: HOST}) + await hass.async_block_till_done() + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "solarlog" assert result["data"][CONF_HOST] == HOST # import with only name result = await flow.async_step_import({CONF_NAME: NAME}) + await hass.async_block_till_done() + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "solarlog_test_1_2_3" assert result["data"][CONF_HOST] == DEFAULT_HOST # import with host and name result = await flow.async_step_import({CONF_HOST: HOST, CONF_NAME: NAME}) + await hass.async_block_till_done() + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "solarlog_test_1_2_3" assert result["data"][CONF_HOST] == HOST @@ -111,7 +162,7 @@ async def test_abort_if_already_setup(hass: HomeAssistant, test_connect) -> None # Should fail, same HOST different NAME (default) result = await flow.async_step_import( - {CONF_HOST: HOST, CONF_NAME: "solarlog_test_7_8_9"} + {CONF_HOST: HOST, CONF_NAME: "solarlog_test_7_8_9", "extended_data": False} ) assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -123,7 +174,7 @@ async def test_abort_if_already_setup(hass: HomeAssistant, test_connect) -> None # SHOULD pass, diff HOST (without http://), different NAME result = await flow.async_step_import( - {CONF_HOST: "2.2.2.2", CONF_NAME: "solarlog_test_7_8_9"} + {CONF_HOST: "2.2.2.2", CONF_NAME: "solarlog_test_7_8_9", "extended_data": False} ) assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "solarlog_test_7_8_9" @@ -131,8 +182,10 @@ async def test_abort_if_already_setup(hass: HomeAssistant, test_connect) -> None # SHOULD pass, diff HOST, same NAME result = await flow.async_step_import( - {CONF_HOST: "http://2.2.2.2", CONF_NAME: NAME} + {CONF_HOST: "http://2.2.2.2", CONF_NAME: NAME, "extended_data": False} ) + await hass.async_block_till_done() + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "solarlog_test_1_2_3" assert result["data"][CONF_HOST] == "http://2.2.2.2" diff --git a/tests/components/solarlog/test_init.py b/tests/components/solarlog/test_init.py new file mode 100644 index 00000000000..9a8d6cb5bec --- /dev/null +++ b/tests/components/solarlog/test_init.py @@ -0,0 +1,57 @@ +"""Test the initialization.""" + +from homeassistant.components.solarlog.const import DOMAIN +from homeassistant.const import CONF_HOST, Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import DeviceRegistry +from homeassistant.helpers.entity_registry import EntityRegistry + +from .test_config_flow import HOST, NAME + +from tests.common import MockConfigEntry + + +async def test_migrate_config_entry( + hass: HomeAssistant, device_reg: DeviceRegistry, entity_reg: EntityRegistry +) -> None: + """Test successful migration of entry data.""" + entry = MockConfigEntry( + domain=DOMAIN, + title=NAME, + data={ + CONF_HOST: HOST, + }, + version=1, + minor_version=1, + ) + entry.add_to_hass(hass) + + device = device_reg.async_get_or_create( + config_entry_id=entry.entry_id, + identifiers={(DOMAIN, entry.entry_id)}, + manufacturer="Solar-Log", + name="solarlog", + ) + sensor_entity = entity_reg.async_get_or_create( + config_entry=entry, + platform=DOMAIN, + domain=Platform.SENSOR, + unique_id=f"{entry.entry_id}_time", + device_id=device.id, + ) + + assert entry.version == 1 + assert entry.minor_version == 1 + assert sensor_entity.unique_id == f"{entry.entry_id}_time" + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + entity_migrated = entity_reg.async_get(sensor_entity.entity_id) + assert entity_migrated + assert entity_migrated.unique_id == f"{entry.entry_id}_last_updated" + + assert entry.version == 1 + assert entry.minor_version == 2 + assert entry.data[CONF_HOST] == HOST + assert entry.data["extended_data"] is False From d5d906e1488281dc234411a66ccceeca829d37ba Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Tue, 18 Jun 2024 03:12:02 -0400 Subject: [PATCH 0773/1445] Add update coordinator to Netgear LTE (#115474) --- .../components/netgear_lte/__init__.py | 142 +++++------------- .../components/netgear_lte/binary_sensor.py | 13 +- homeassistant/components/netgear_lte/const.py | 2 +- .../components/netgear_lte/coordinator.py | 43 ++++++ .../components/netgear_lte/entity.py | 50 ++---- .../components/netgear_lte/notify.py | 24 +-- .../components/netgear_lte/sensor.py | 28 ++-- .../components/netgear_lte/services.py | 23 +-- .../netgear_lte/test_config_flow.py | 9 +- tests/components/netgear_lte/test_init.py | 26 ++++ 10 files changed, 166 insertions(+), 194 deletions(-) create mode 100644 homeassistant/components/netgear_lte/coordinator.py diff --git a/homeassistant/components/netgear_lte/__init__.py b/homeassistant/components/netgear_lte/__init__.py index c47a5088887..1846d1f7992 100644 --- a/homeassistant/components/netgear_lte/__init__.py +++ b/homeassistant/components/netgear_lte/__init__.py @@ -1,25 +1,17 @@ """Support for Netgear LTE modems.""" -from datetime import timedelta +from typing import Any from aiohttp.cookiejar import CookieJar -import attr import eternalegypt +from eternalegypt.eternalegypt import SMS from homeassistant.config_entries import ConfigEntry, ConfigEntryState -from homeassistant.const import ( - CONF_HOST, - CONF_NAME, - CONF_PASSWORD, - EVENT_HOMEASSISTANT_STOP, - Platform, -) -from homeassistant.core import Event, HomeAssistant +from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, Platform +from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import config_validation as cv, discovery from homeassistant.helpers.aiohttp_client import async_create_clientsession -from homeassistant.helpers.dispatcher import async_dispatcher_send -from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.typing import ConfigType from .const import ( @@ -28,14 +20,12 @@ from .const import ( ATTR_MESSAGE, ATTR_SMS_ID, DATA_HASS_CONFIG, - DISPATCHER_NETGEAR_LTE, + DATA_SESSION, DOMAIN, - LOGGER, ) +from .coordinator import NetgearLTEDataUpdateCoordinator from .services import async_setup_services -SCAN_INTERVAL = timedelta(seconds=10) - EVENT_SMS = "netgear_lte_sms" ALL_SENSORS = [ @@ -65,54 +55,11 @@ PLATFORMS = [ Platform.NOTIFY, Platform.SENSOR, ] +type NetgearLTEConfigEntry = ConfigEntry[NetgearLTEDataUpdateCoordinator] CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN) -@attr.s -class ModemData: - """Class for modem state.""" - - hass = attr.ib() - host = attr.ib() - modem = attr.ib() - - data = attr.ib(init=False, default=None) - connected = attr.ib(init=False, default=True) - - async def async_update(self): - """Call the API to update the data.""" - - try: - self.data = await self.modem.information() - if not self.connected: - LOGGER.warning("Connected to %s", self.host) - self.connected = True - except eternalegypt.Error: - if self.connected: - LOGGER.warning("Lost connection to %s", self.host) - self.connected = False - self.data = None - - async_dispatcher_send(self.hass, DISPATCHER_NETGEAR_LTE) - - -@attr.s -class LTEData: - """Shared state.""" - - websession = attr.ib() - modem_data: dict[str, ModemData] = attr.ib(init=False, factory=dict) - - def get_modem_data(self, config): - """Get modem_data for the host in config.""" - if config[CONF_HOST] is not None: - return self.modem_data.get(config[CONF_HOST]) - if len(self.modem_data) != 1: - return None - return next(iter(self.modem_data.values())) - - async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up Netgear LTE component.""" hass.data[DATA_HASS_CONFIG] = config @@ -120,44 +67,44 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: return True -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: NetgearLTEConfigEntry) -> bool: """Set up Netgear LTE from a config entry.""" host = entry.data[CONF_HOST] password = entry.data[CONF_PASSWORD] - if not (data := hass.data.get(DOMAIN)) or data.websession.closed: - websession = async_create_clientsession(hass, cookie_jar=CookieJar(unsafe=True)) + data: dict[str, Any] = hass.data.setdefault(DOMAIN, {}) + if not (session := data.get(DATA_SESSION)) or session.closed: + session = async_create_clientsession(hass, cookie_jar=CookieJar(unsafe=True)) + modem = eternalegypt.Modem(hostname=host, websession=session) - hass.data[DOMAIN] = LTEData(websession) + try: + await modem.login(password=password) + except eternalegypt.Error as ex: + raise ConfigEntryNotReady("Cannot connect/authenticate") from ex - modem = eternalegypt.Modem(hostname=host, websession=hass.data[DOMAIN].websession) - modem_data = ModemData(hass, host, modem) + def fire_sms_event(sms: SMS) -> None: + """Send an SMS event.""" + data = { + ATTR_HOST: modem.hostname, + ATTR_SMS_ID: sms.id, + ATTR_FROM: sms.sender, + ATTR_MESSAGE: sms.message, + } + hass.bus.async_fire(EVENT_SMS, data) - await _login(hass, modem_data, password) + await modem.add_sms_listener(fire_sms_event) - async def _update(now): - """Periodic update.""" - await modem_data.async_update() + coordinator = NetgearLTEDataUpdateCoordinator(hass, modem) + await coordinator.async_config_entry_first_refresh() + entry.runtime_data = coordinator - update_unsub = async_track_time_interval(hass, _update, SCAN_INTERVAL) - - async def cleanup(event: Event | None = None) -> None: - """Clean up resources.""" - update_unsub() - await modem.logout() - if DOMAIN in hass.data: - del hass.data[DOMAIN].modem_data[modem_data.host] - - entry.async_on_unload(cleanup) - entry.async_on_unload(hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, cleanup)) - - await async_setup_services(hass) + await async_setup_services(hass, modem) await discovery.async_load_platform( hass, Platform.NOTIFY, DOMAIN, - {CONF_HOST: entry.data[CONF_HOST], CONF_NAME: entry.title}, + {CONF_NAME: entry.title, "modem": modem}, hass.data[DATA_HASS_CONFIG], ) @@ -168,7 +115,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: NetgearLTEConfigEntry) -> bool: """Unload a config entry.""" unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) loaded_entries = [ @@ -178,28 +125,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ] if len(loaded_entries) == 1: hass.data.pop(DOMAIN, None) + for service_name in hass.services.async_services()[DOMAIN]: + hass.services.async_remove(DOMAIN, service_name) return unload_ok - - -async def _login(hass: HomeAssistant, modem_data: ModemData, password: str) -> None: - """Log in and complete setup.""" - try: - await modem_data.modem.login(password=password) - except eternalegypt.Error as ex: - raise ConfigEntryNotReady("Cannot connect/authenticate") from ex - - def fire_sms_event(sms): - """Send an SMS event.""" - data = { - ATTR_HOST: modem_data.host, - ATTR_SMS_ID: sms.id, - ATTR_FROM: sms.sender, - ATTR_MESSAGE: sms.message, - } - hass.bus.async_fire(EVENT_SMS, data) - - await modem_data.modem.add_sms_listener(fire_sms_event) - - await modem_data.async_update() - hass.data[DOMAIN].modem_data[modem_data.host] = modem_data diff --git a/homeassistant/components/netgear_lte/binary_sensor.py b/homeassistant/components/netgear_lte/binary_sensor.py index 43a9c1bd260..280d240b90f 100644 --- a/homeassistant/components/netgear_lte/binary_sensor.py +++ b/homeassistant/components/netgear_lte/binary_sensor.py @@ -7,12 +7,11 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntity, BinarySensorEntityDescription, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN +from . import NetgearLTEConfigEntry from .entity import LTEEntity BINARY_SENSORS: tuple[BinarySensorEntityDescription, ...] = ( @@ -38,13 +37,13 @@ BINARY_SENSORS: tuple[BinarySensorEntityDescription, ...] = ( async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, + entry: NetgearLTEConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Netgear LTE binary sensor.""" - modem_data = hass.data[DOMAIN].get_modem_data(entry.data) - async_add_entities( - NetgearLTEBinarySensor(entry, modem_data, sensor) for sensor in BINARY_SENSORS + NetgearLTEBinarySensor(entry, description) for description in BINARY_SENSORS ) @@ -54,4 +53,4 @@ class NetgearLTEBinarySensor(LTEEntity, BinarySensorEntity): @property def is_on(self): """Return true if the binary sensor is on.""" - return getattr(self.modem_data.data, self.entity_description.key) + return getattr(self.coordinator.data, self.entity_description.key) diff --git a/homeassistant/components/netgear_lte/const.py b/homeassistant/components/netgear_lte/const.py index 69a96c289e8..1b8a96319c2 100644 --- a/homeassistant/components/netgear_lte/const.py +++ b/homeassistant/components/netgear_lte/const.py @@ -16,9 +16,9 @@ CONF_NOTIFY: Final = "notify" CONF_SENSOR: Final = "sensor" DATA_HASS_CONFIG = "netgear_lte_hass_config" +DATA_SESSION = "session" # https://kb.netgear.com/31160/How-do-I-change-my-4G-LTE-Modem-s-IP-address-range DEFAULT_HOST = "192.168.5.1" -DISPATCHER_NETGEAR_LTE = "netgear_lte_update" DOMAIN: Final = "netgear_lte" FAILOVER_MODES = ["auto", "wire", "mobile"] diff --git a/homeassistant/components/netgear_lte/coordinator.py b/homeassistant/components/netgear_lte/coordinator.py new file mode 100644 index 00000000000..afd0cb743bf --- /dev/null +++ b/homeassistant/components/netgear_lte/coordinator.py @@ -0,0 +1,43 @@ +"""Data update coordinator for the Netgear LTE integration.""" + +from __future__ import annotations + +from datetime import timedelta +from typing import TYPE_CHECKING + +from eternalegypt.eternalegypt import Error, Information, Modem + +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import DOMAIN, LOGGER + +if TYPE_CHECKING: + from . import NetgearLTEConfigEntry + + +class NetgearLTEDataUpdateCoordinator(DataUpdateCoordinator[Information]): + """Data update coordinator for the Netgear LTE integration.""" + + config_entry: NetgearLTEConfigEntry + + def __init__( + self, + hass: HomeAssistant, + modem: Modem, + ) -> None: + """Initialize the coordinator.""" + super().__init__( + hass=hass, + logger=LOGGER, + name=DOMAIN, + update_interval=timedelta(seconds=10), + ) + self.modem = modem + + async def _async_update_data(self) -> Information: + """Get the latest data.""" + try: + return await self.modem.information() + except Error as ex: + raise UpdateFailed(ex) from ex diff --git a/homeassistant/components/netgear_lte/entity.py b/homeassistant/components/netgear_lte/entity.py index 0ec16ceff9d..3353da6dc77 100644 --- a/homeassistant/components/netgear_lte/entity.py +++ b/homeassistant/components/netgear_lte/entity.py @@ -1,54 +1,36 @@ """Entity representing a Netgear LTE entity.""" -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST from homeassistant.helpers.device_registry import DeviceInfo -from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import Entity, EntityDescription +from homeassistant.helpers.entity import EntityDescription +from homeassistant.helpers.update_coordinator import CoordinatorEntity -from . import ModemData -from .const import DISPATCHER_NETGEAR_LTE, DOMAIN, MANUFACTURER +from . import NetgearLTEConfigEntry +from .const import DOMAIN, MANUFACTURER +from .coordinator import NetgearLTEDataUpdateCoordinator -class LTEEntity(Entity): +class LTEEntity(CoordinatorEntity[NetgearLTEDataUpdateCoordinator]): """Base LTE entity.""" _attr_has_entity_name = True - _attr_should_poll = False def __init__( self, - config_entry: ConfigEntry, - modem_data: ModemData, + entry: NetgearLTEConfigEntry, description: EntityDescription, ) -> None: """Initialize a Netgear LTE entity.""" + super().__init__(entry.runtime_data) self.entity_description = description - self.modem_data = modem_data - self._attr_unique_id = f"{description.key}_{modem_data.data.serial_number}" + data = entry.runtime_data.data + self._attr_unique_id = f"{description.key}_{data.serial_number}" self._attr_device_info = DeviceInfo( - configuration_url=f"http://{config_entry.data[CONF_HOST]}", - identifiers={(DOMAIN, modem_data.data.serial_number)}, + configuration_url=f"http://{entry.data[CONF_HOST]}", + identifiers={(DOMAIN, data.serial_number)}, manufacturer=MANUFACTURER, - model=modem_data.data.items["general.model"], - serial_number=modem_data.data.serial_number, - sw_version=modem_data.data.items["general.fwversion"], - hw_version=modem_data.data.items["general.hwversion"], + model=data.items["general.model"], + serial_number=data.serial_number, + sw_version=data.items["general.fwversion"], + hw_version=data.items["general.hwversion"], ) - - async def async_added_to_hass(self) -> None: - """Register callback.""" - self.async_on_remove( - async_dispatcher_connect( - self.hass, DISPATCHER_NETGEAR_LTE, self.async_write_ha_state - ) - ) - - async def async_update(self) -> None: - """Force update of state.""" - await self.modem_data.async_update() - - @property - def available(self) -> bool: - """Return the availability of the sensor.""" - return self.modem_data.data is not None diff --git a/homeassistant/components/netgear_lte/notify.py b/homeassistant/components/netgear_lte/notify.py index 97ba402dc35..763581b9cad 100644 --- a/homeassistant/components/netgear_lte/notify.py +++ b/homeassistant/components/netgear_lte/notify.py @@ -2,15 +2,17 @@ from __future__ import annotations -import attr +from typing import Any + import eternalegypt +from eternalegypt.eternalegypt import Modem from homeassistant.components.notify import ATTR_TARGET, BaseNotificationService from homeassistant.const import CONF_RECIPIENT from homeassistant.core import HomeAssistant from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from .const import CONF_NOTIFY, DOMAIN, LOGGER +from .const import CONF_NOTIFY, LOGGER async def async_get_service( @@ -22,21 +24,25 @@ async def async_get_service( if discovery_info is None: return None - return NetgearNotifyService(hass, discovery_info) + return NetgearNotifyService(config, discovery_info) -@attr.s class NetgearNotifyService(BaseNotificationService): """Implementation of a notification service.""" - hass = attr.ib() - config = attr.ib() + def __init__( + self, + config: ConfigType, + discovery_info: dict[str, Any], + ) -> None: + """Initialize the service.""" + self.config = config + self.modem: Modem = discovery_info["modem"] async def async_send_message(self, message="", **kwargs): """Send a message to a user.""" - modem_data = self.hass.data[DOMAIN].get_modem_data(self.config) - if not modem_data: + if not self.modem.token: LOGGER.error("Modem not ready") return if not (targets := kwargs.get(ATTR_TARGET)): @@ -50,6 +56,6 @@ class NetgearNotifyService(BaseNotificationService): for target in targets: try: - await modem_data.modem.sms(target, message) + await self.modem.sms(target, message) except eternalegypt.Error: LOGGER.error("Unable to send to %s", target) diff --git a/homeassistant/components/netgear_lte/sensor.py b/homeassistant/components/netgear_lte/sensor.py index 62b4796f068..73e5de7eaeb 100644 --- a/homeassistant/components/netgear_lte/sensor.py +++ b/homeassistant/components/netgear_lte/sensor.py @@ -5,12 +5,13 @@ from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass +from eternalegypt.eternalegypt import Information + from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, SensorEntityDescription, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( PERCENTAGE, SIGNAL_STRENGTH_DECIBELS_MILLIWATT, @@ -21,8 +22,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType -from . import ModemData -from .const import DOMAIN +from . import NetgearLTEConfigEntry from .entity import LTEEntity @@ -30,7 +30,7 @@ from .entity import LTEEntity class NetgearLTESensorEntityDescription(SensorEntityDescription): """Class describing Netgear LTE entities.""" - value_fn: Callable[[ModemData], StateType] | None = None + value_fn: Callable[[Information], StateType] | None = None SENSORS: tuple[NetgearLTESensorEntityDescription, ...] = ( @@ -38,13 +38,13 @@ SENSORS: tuple[NetgearLTESensorEntityDescription, ...] = ( key="sms", translation_key="sms", native_unit_of_measurement="unread", - value_fn=lambda modem_data: sum(1 for x in modem_data.data.sms if x.unread), + value_fn=lambda data: sum(1 for x in data.sms if x.unread), ), NetgearLTESensorEntityDescription( key="sms_total", translation_key="sms_total", native_unit_of_measurement="messages", - value_fn=lambda modem_data: len(modem_data.data.sms), + value_fn=lambda data: len(data.sms), ), NetgearLTESensorEntityDescription( key="usage", @@ -54,7 +54,7 @@ SENSORS: tuple[NetgearLTESensorEntityDescription, ...] = ( native_unit_of_measurement=UnitOfInformation.BYTES, suggested_unit_of_measurement=UnitOfInformation.MEBIBYTES, suggested_display_precision=1, - value_fn=lambda modem_data: modem_data.data.usage, + value_fn=lambda data: data.usage, ), NetgearLTESensorEntityDescription( key="radio_quality", @@ -125,14 +125,12 @@ SENSORS: tuple[NetgearLTESensorEntityDescription, ...] = ( async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, + entry: NetgearLTEConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Netgear LTE sensor.""" - modem_data = hass.data[DOMAIN].get_modem_data(entry.data) - - async_add_entities( - NetgearLTESensor(entry, modem_data, sensor) for sensor in SENSORS - ) + async_add_entities(NetgearLTESensor(entry, description) for description in SENSORS) class NetgearLTESensor(LTEEntity, SensorEntity): @@ -144,5 +142,5 @@ class NetgearLTESensor(LTEEntity, SensorEntity): def native_value(self) -> StateType: """Return the state of the sensor.""" if self.entity_description.value_fn is not None: - return self.entity_description.value_fn(self.modem_data) - return getattr(self.modem_data.data, self.entity_description.key) + return self.entity_description.value_fn(self.coordinator.data) + return getattr(self.coordinator.data, self.entity_description.key) diff --git a/homeassistant/components/netgear_lte/services.py b/homeassistant/components/netgear_lte/services.py index 02000820119..77ed1b91f31 100644 --- a/homeassistant/components/netgear_lte/services.py +++ b/homeassistant/components/netgear_lte/services.py @@ -1,10 +1,8 @@ """Services for the Netgear LTE integration.""" -from typing import TYPE_CHECKING - +from eternalegypt.eternalegypt import Modem import voluptuous as vol -from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.helpers import config_validation as cv @@ -19,9 +17,6 @@ from .const import ( LOGGER, ) -if TYPE_CHECKING: - from . import LTEData, ModemData - SERVICE_DELETE_SMS = "delete_sms" SERVICE_SET_OPTION = "set_option" SERVICE_CONNECT_LTE = "connect_lte" @@ -50,31 +45,29 @@ CONNECT_LTE_SCHEMA = vol.Schema({vol.Optional(ATTR_HOST): cv.string}) DISCONNECT_LTE_SCHEMA = vol.Schema({vol.Optional(ATTR_HOST): cv.string}) -async def async_setup_services(hass: HomeAssistant) -> None: +async def async_setup_services(hass: HomeAssistant, modem: Modem) -> None: """Set up services for Netgear LTE integration.""" async def service_handler(call: ServiceCall) -> None: """Apply a service.""" host = call.data.get(ATTR_HOST) - data: LTEData = hass.data[DOMAIN] - modem_data: ModemData = data.get_modem_data({CONF_HOST: host}) - if not modem_data: + if not modem.token: LOGGER.error("%s: host %s unavailable", call.service, host) return if call.service == SERVICE_DELETE_SMS: for sms_id in call.data[ATTR_SMS_ID]: - await modem_data.modem.delete_sms(sms_id) + await modem.delete_sms(sms_id) elif call.service == SERVICE_SET_OPTION: if failover := call.data.get(ATTR_FAILOVER): - await modem_data.modem.set_failover_mode(failover) + await modem.set_failover_mode(failover) if autoconnect := call.data.get(ATTR_AUTOCONNECT): - await modem_data.modem.set_autoconnect_mode(autoconnect) + await modem.set_autoconnect_mode(autoconnect) elif call.service == SERVICE_CONNECT_LTE: - await modem_data.modem.connect_lte() + await modem.connect_lte() elif call.service == SERVICE_DISCONNECT_LTE: - await modem_data.modem.disconnect_lte() + await modem.disconnect_lte() service_schemas = { SERVICE_DELETE_SMS: DELETE_SMS_SCHEMA, diff --git a/tests/components/netgear_lte/test_config_flow.py b/tests/components/netgear_lte/test_config_flow.py index 16feb88172b..ec649f4def0 100644 --- a/tests/components/netgear_lte/test_config_flow.py +++ b/tests/components/netgear_lte/test_config_flow.py @@ -2,7 +2,6 @@ from unittest.mock import patch -from homeassistant import data_entry_flow from homeassistant.components.netgear_lte.const import DOMAIN from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_SOURCE @@ -25,7 +24,7 @@ async def test_flow_user_form(hass: HomeAssistant, connection: None) -> None: context={CONF_SOURCE: SOURCE_USER}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with _patch_setup(): @@ -33,7 +32,7 @@ async def test_flow_user_form(hass: HomeAssistant, connection: None) -> None: result["flow_id"], user_input=CONF_DATA, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Netgear LM1200" assert result["data"] == CONF_DATA assert result["context"]["unique_id"] == "FFFFFFFFFFFFF" @@ -63,7 +62,7 @@ async def test_flow_user_cannot_connect( data=CONF_DATA, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == "cannot_connect" @@ -78,6 +77,6 @@ async def test_flow_user_unknown_error(hass: HomeAssistant, unknown: None) -> No result["flow_id"], user_input=CONF_DATA, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == "unknown" diff --git a/tests/components/netgear_lte/test_init.py b/tests/components/netgear_lte/test_init.py index ef3109123fa..ca5a22cf259 100644 --- a/tests/components/netgear_lte/test_init.py +++ b/tests/components/netgear_lte/test_init.py @@ -1,14 +1,22 @@ """Test Netgear LTE integration.""" +from datetime import timedelta +from unittest.mock import patch + +from eternalegypt.eternalegypt import Error from syrupy.assertion import SnapshotAssertion from homeassistant.components.netgear_lte.const import DOMAIN from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import STATE_UNAVAILABLE from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr +import homeassistant.util.dt as dt_util from .conftest import CONF_DATA +from tests.common import async_fire_time_changed + async def test_setup_unload(hass: HomeAssistant, setup_integration: None) -> None: """Test setup and unload.""" @@ -43,3 +51,21 @@ async def test_device( await hass.async_block_till_done() device = device_registry.async_get_device(identifiers={(DOMAIN, entry.unique_id)}) assert device == snapshot + + +async def test_update_failed( + hass: HomeAssistant, + entity_registry_enabled_by_default: None, + setup_integration: None, +) -> None: + """Test coordinator throws UpdateFailed after failed update.""" + with patch( + "homeassistant.components.netgear_lte.eternalegypt.Modem.information", + side_effect=Error, + ) as updater: + next_update = dt_util.utcnow() + timedelta(seconds=10) + async_fire_time_changed(hass, next_update) + await hass.async_block_till_done() + updater.assert_called_once() + state = hass.states.get("sensor.netgear_lm1200_radio_quality") + assert state.state == STATE_UNAVAILABLE From 67223b2a2dbe6f9163f9a35ad614c8dfed52f616 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Tue, 18 Jun 2024 03:13:24 -0400 Subject: [PATCH 0774/1445] Store runtime data inside the config entry in Lidarr (#119548) --- homeassistant/components/lidarr/__init__.py | 40 ++++++++++++------- .../components/lidarr/config_flow.py | 5 ++- homeassistant/components/lidarr/sensor.py | 12 ++---- 3 files changed, 32 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/lidarr/__init__.py b/homeassistant/components/lidarr/__init__.py index acfb8f30f30..ee2369a6bc4 100644 --- a/homeassistant/components/lidarr/__init__.py +++ b/homeassistant/components/lidarr/__init__.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import Any +from dataclasses import dataclass, fields from aiopyarr.lidarr_client import LidarrClient from aiopyarr.models.host_configuration import PyArrHostConfiguration @@ -25,10 +25,22 @@ from .coordinator import ( WantedDataUpdateCoordinator, ) +type LidarrConfigEntry = ConfigEntry[LidarrData] + PLATFORMS = [Platform.SENSOR] -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +@dataclass(kw_only=True, slots=True) +class LidarrData: + """Lidarr data type.""" + + disk_space: DiskSpaceDataUpdateCoordinator + queue: QueueDataUpdateCoordinator + status: StatusDataUpdateCoordinator + wanted: WantedDataUpdateCoordinator + + +async def async_setup_entry(hass: HomeAssistant, entry: LidarrConfigEntry) -> bool: """Set up Lidarr from a config entry.""" host_configuration = PyArrHostConfiguration( api_token=entry.data[CONF_API_KEY], @@ -40,31 +52,29 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: session=async_get_clientsession(hass, host_configuration.verify_ssl), request_timeout=60, ) - coordinators: dict[str, LidarrDataUpdateCoordinator[Any]] = { - "disk_space": DiskSpaceDataUpdateCoordinator(hass, host_configuration, lidarr), - "queue": QueueDataUpdateCoordinator(hass, host_configuration, lidarr), - "status": StatusDataUpdateCoordinator(hass, host_configuration, lidarr), - "wanted": WantedDataUpdateCoordinator(hass, host_configuration, lidarr), - } + data = LidarrData( + disk_space=DiskSpaceDataUpdateCoordinator(hass, host_configuration, lidarr), + queue=QueueDataUpdateCoordinator(hass, host_configuration, lidarr), + status=StatusDataUpdateCoordinator(hass, host_configuration, lidarr), + wanted=WantedDataUpdateCoordinator(hass, host_configuration, lidarr), + ) # Temporary, until we add diagnostic entities _version = None - for coordinator in coordinators.values(): + for field in fields(data): + coordinator = getattr(data, field.name) await coordinator.async_config_entry_first_refresh() if isinstance(coordinator, StatusDataUpdateCoordinator): _version = coordinator.data coordinator.system_version = _version - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinators + entry.runtime_data = data await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: LidarrConfigEntry) -> bool: """Unload a config entry.""" - if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): - hass.data[DOMAIN].pop(entry.entry_id) - - return unload_ok + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) class LidarrEntity(CoordinatorEntity[LidarrDataUpdateCoordinator[T]]): diff --git a/homeassistant/components/lidarr/config_flow.py b/homeassistant/components/lidarr/config_flow.py index 379a01375b6..05d6900bb41 100644 --- a/homeassistant/components/lidarr/config_flow.py +++ b/homeassistant/components/lidarr/config_flow.py @@ -10,11 +10,12 @@ from aiopyarr import exceptions from aiopyarr.lidarr_client import LidarrClient import voluptuous as vol -from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_API_KEY, CONF_URL, CONF_VERIFY_SSL from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession +from . import LidarrConfigEntry from .const import DEFAULT_NAME, DOMAIN @@ -25,7 +26,7 @@ class LidarrConfigFlow(ConfigFlow, domain=DOMAIN): def __init__(self) -> None: """Initialize the flow.""" - self.entry: ConfigEntry | None = None + self.entry: LidarrConfigEntry | None = None async def async_step_reauth( self, user_input: Mapping[str, Any] diff --git a/homeassistant/components/lidarr/sensor.py b/homeassistant/components/lidarr/sensor.py index c876aec4623..b50a826a1c7 100644 --- a/homeassistant/components/lidarr/sensor.py +++ b/homeassistant/components/lidarr/sensor.py @@ -14,13 +14,12 @@ from homeassistant.components.sensor import ( SensorEntityDescription, SensorStateClass, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import UnitOfInformation from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import LidarrEntity -from .const import BYTE_SIZES, DOMAIN +from . import LidarrConfigEntry, LidarrEntity +from .const import BYTE_SIZES from .coordinator import LidarrDataUpdateCoordinator, T @@ -106,16 +105,13 @@ SENSOR_TYPES: dict[str, LidarrSensorEntityDescription[Any]] = { async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: LidarrConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up Lidarr sensors based on a config entry.""" - coordinators: dict[str, LidarrDataUpdateCoordinator[Any]] = hass.data[DOMAIN][ - entry.entry_id - ] entities: list[LidarrSensor[Any]] = [] for coordinator_type, description in SENSOR_TYPES.items(): - coordinator = coordinators[coordinator_type] + coordinator = getattr(entry.runtime_data, coordinator_type) if coordinator_type != "disk_space": entities.append(LidarrSensor(coordinator, description)) else: From 6eb9d1e01d0cd7283ed3de22af623b5356f26488 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Tue, 18 Jun 2024 09:29:22 +0200 Subject: [PATCH 0775/1445] Gracefully disconnect MQTT entry if entry is reloaded (#119753) --- homeassistant/components/mqtt/__init__.py | 4 ++-- homeassistant/components/mqtt/client.py | 15 +++++++++++---- tests/components/mqtt/test_init.py | 4 ++++ 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index ea520e88366..f057dab8bc4 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -535,8 +535,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: registry_hooks = mqtt_data.discovery_registry_hooks while registry_hooks: registry_hooks.popitem()[1]() - # Wait for all ACKs and stop the loop - await mqtt_client.async_disconnect() + # Wait for all ACKs, stop the loop and disconnect the client + await mqtt_client.async_disconnect(disconnect_paho_client=True) # Cleanup MQTT client availability hass.data.pop(DATA_MQTT_AVAILABLE, None) diff --git a/homeassistant/components/mqtt/client.py b/homeassistant/components/mqtt/client.py index ace2293e7a6..562fa230bca 100644 --- a/homeassistant/components/mqtt/client.py +++ b/homeassistant/components/mqtt/client.py @@ -803,8 +803,12 @@ class MQTT: await asyncio.sleep(RECONNECT_INTERVAL_SECONDS) - async def async_disconnect(self) -> None: - """Stop the MQTT client.""" + async def async_disconnect(self, disconnect_paho_client: bool = False) -> None: + """Stop the MQTT client. + + We only disconnect grafully if disconnect_paho_client is set, but not + when Home Assistant is shut down. + """ # stop waiting for any pending subscriptions await self._subscribe_debouncer.async_cleanup() @@ -824,7 +828,9 @@ class MQTT: self._should_reconnect = False self._async_cancel_reconnect() # We do not gracefully disconnect to ensure - # the broker publishes the will message + # the broker publishes the will message unless the entry is reloaded + if disconnect_paho_client: + self._mqttc.disconnect() @callback def async_restore_tracked_subscriptions( @@ -1274,7 +1280,8 @@ class MQTT: self._async_connection_result(False) self.connected = False async_dispatcher_send(self.hass, MQTT_CONNECTION_STATE, False) - _LOGGER.warning( + _LOGGER.log( + logging.INFO if result_code == 0 else logging.DEBUG, "Disconnected from MQTT server %s:%s (%s)", self.conf[CONF_BROKER], self.conf.get(CONF_PORT, DEFAULT_PORT), diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index 144b2f9cf45..18310750558 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -4087,6 +4087,7 @@ async def test_link_config_entry( async def test_reload_config_entry( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, + caplog: pytest.LogCaptureFixture, ) -> None: """Test manual entities reloaded and set up correctly.""" await mqtt_mock_entry() @@ -4153,6 +4154,9 @@ async def test_reload_config_entry( assert await hass.config_entries.async_reload(entry.entry_id) assert entry.state is ConfigEntryState.LOADED await hass.async_block_till_done() + # Assert the MQTT client was connected gracefully + with caplog.at_level(logging.INFO): + assert "Disconnected from MQTT server mock-broker:1883" in caplog.text assert (state := hass.states.get("sensor.test_manual1")) is not None assert state.attributes["friendly_name"] == "test_manual1_updated" From 2906fca40ce1f2ba0d1e44ff470d98104d0efc01 Mon Sep 17 00:00:00 2001 From: Thomas Kistler Date: Tue, 18 Jun 2024 01:26:31 -0700 Subject: [PATCH 0776/1445] Update pydrawise to 2024.6.4 (#119868) --- homeassistant/components/hydrawise/manifest.json | 2 +- homeassistant/components/hydrawise/sensor.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/hydrawise/manifest.json b/homeassistant/components/hydrawise/manifest.json index dc6408407e7..b85ddca042e 100644 --- a/homeassistant/components/hydrawise/manifest.json +++ b/homeassistant/components/hydrawise/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/hydrawise", "iot_class": "cloud_polling", "loggers": ["pydrawise"], - "requirements": ["pydrawise==2024.6.3"] + "requirements": ["pydrawise==2024.6.4"] } diff --git a/homeassistant/components/hydrawise/sensor.py b/homeassistant/components/hydrawise/sensor.py index 87dc5e73afe..2497fe8f49d 100644 --- a/homeassistant/components/hydrawise/sensor.py +++ b/homeassistant/components/hydrawise/sensor.py @@ -48,7 +48,7 @@ def _get_zone_daily_active_water_use(sensor: HydrawiseSensor) -> float: return float(daily_water_summary.active_use_by_zone_id.get(sensor.zone.id, 0.0)) -def _get_controller_daily_active_water_use(sensor: HydrawiseSensor) -> float: +def _get_controller_daily_active_water_use(sensor: HydrawiseSensor) -> float | None: """Get active water use for the controller.""" daily_water_summary = sensor.coordinator.data.daily_water_use[sensor.controller.id] return daily_water_summary.total_active_use diff --git a/requirements_all.txt b/requirements_all.txt index 4ecfd25800d..3f4085340d6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1800,7 +1800,7 @@ pydiscovergy==3.0.1 pydoods==1.0.2 # homeassistant.components.hydrawise -pydrawise==2024.6.3 +pydrawise==2024.6.4 # homeassistant.components.android_ip_webcam pydroid-ipcam==2.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8dcef7f1575..5081a168646 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1414,7 +1414,7 @@ pydexcom==0.2.3 pydiscovergy==3.0.1 # homeassistant.components.hydrawise -pydrawise==2024.6.3 +pydrawise==2024.6.4 # homeassistant.components.android_ip_webcam pydroid-ipcam==2.0.0 From 0ff002287763fdeaa55a9d23e5a9f6510a45bab2 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 18 Jun 2024 12:00:24 +0200 Subject: [PATCH 0777/1445] Ignore use-implicit-booleaness-not-comparison pylint warnings in tests (#119876) --- pyproject.toml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index cf41b415a91..bbb5b742dab 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -402,9 +402,10 @@ enable = [ "use-symbolic-message-instead", ] per-file-ignores = [ - # hass-component-root-import: Tests test non-public APIs # redefined-outer-name: Tests reference fixtures in the test function - "/tests/:hass-component-root-import,redefined-outer-name", + # use-implicit-booleaness-not-comparison: Tests need to validate that a list + # or a dict is returned + "/tests/:redefined-outer-name,use-implicit-booleaness-not-comparison", ] [tool.pylint.REPORTS] From 9128dc198ab861963ae2237c5d4b4a356b52e384 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 18 Jun 2024 12:10:11 +0200 Subject: [PATCH 0778/1445] Centralize lidarr device creation (#119822) --- homeassistant/components/lidarr/__init__.py | 22 +++++++++---------- .../components/lidarr/coordinator.py | 1 - 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/lidarr/__init__.py b/homeassistant/components/lidarr/__init__.py index ee2369a6bc4..e7935501650 100644 --- a/homeassistant/components/lidarr/__init__.py +++ b/homeassistant/components/lidarr/__init__.py @@ -10,6 +10,7 @@ from aiopyarr.models.host_configuration import PyArrHostConfiguration from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_API_KEY, CONF_URL, CONF_VERIFY_SSL, Platform from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity import EntityDescription @@ -58,14 +59,18 @@ async def async_setup_entry(hass: HomeAssistant, entry: LidarrConfigEntry) -> bo status=StatusDataUpdateCoordinator(hass, host_configuration, lidarr), wanted=WantedDataUpdateCoordinator(hass, host_configuration, lidarr), ) - # Temporary, until we add diagnostic entities - _version = None for field in fields(data): coordinator = getattr(data, field.name) await coordinator.async_config_entry_first_refresh() - if isinstance(coordinator, StatusDataUpdateCoordinator): - _version = coordinator.data - coordinator.system_version = _version + device_registry = dr.async_get(hass) + device_registry.async_get_or_create( + config_entry_id=entry.entry_id, + configuration_url=entry.data[CONF_URL], + entry_type=DeviceEntryType.SERVICE, + identifiers={(DOMAIN, entry.entry_id)}, + manufacturer=DEFAULT_NAME, + sw_version=data.status.data, + ) entry.runtime_data = data await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) @@ -92,10 +97,5 @@ class LidarrEntity(CoordinatorEntity[LidarrDataUpdateCoordinator[T]]): self.entity_description = description self._attr_unique_id = f"{coordinator.config_entry.entry_id}_{description.key}" self._attr_device_info = DeviceInfo( - configuration_url=coordinator.host_configuration.base_url, - entry_type=DeviceEntryType.SERVICE, - identifiers={(DOMAIN, coordinator.config_entry.entry_id)}, - manufacturer=DEFAULT_NAME, - name=coordinator.config_entry.title, - sw_version=coordinator.system_version, + identifiers={(DOMAIN, coordinator.config_entry.entry_id)} ) diff --git a/homeassistant/components/lidarr/coordinator.py b/homeassistant/components/lidarr/coordinator.py index 8b3116055d4..2f18e4f0ebb 100644 --- a/homeassistant/components/lidarr/coordinator.py +++ b/homeassistant/components/lidarr/coordinator.py @@ -40,7 +40,6 @@ class LidarrDataUpdateCoordinator(DataUpdateCoordinator[T], Generic[T], ABC): ) self.api_client = api_client self.host_configuration = host_configuration - self.system_version: str | None = None async def _async_update_data(self) -> T: """Get the latest data from Lidarr.""" From dc388c76f90a3a0a1148c541a926dbade0f46e3f Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Tue, 18 Jun 2024 06:28:43 -0400 Subject: [PATCH 0779/1445] Store runtime data inside the config entry in Steam (#119881) --- homeassistant/components/steam_online/__init__.py | 12 +++++------- homeassistant/components/steam_online/config_flow.py | 8 ++++---- homeassistant/components/steam_online/coordinator.py | 7 +++++-- homeassistant/components/steam_online/sensor.py | 7 +++---- tests/components/steam_online/__init__.py | 7 +++++-- 5 files changed, 22 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/steam_online/__init__.py b/homeassistant/components/steam_online/__init__.py index 93b4a3eb370..6e45758fb94 100644 --- a/homeassistant/components/steam_online/__init__.py +++ b/homeassistant/components/steam_online/__init__.py @@ -6,24 +6,22 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant -from .const import DOMAIN from .coordinator import SteamDataUpdateCoordinator PLATFORMS = [Platform.SENSOR] +type SteamConfigEntry = ConfigEntry[SteamDataUpdateCoordinator] -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: SteamConfigEntry) -> bool: """Set up Steam from a config entry.""" coordinator = SteamDataUpdateCoordinator(hass) await coordinator.async_config_entry_first_refresh() - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator + entry.runtime_data = coordinator await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: SteamConfigEntry) -> bool: """Unload a config entry.""" - if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): - hass.data[DOMAIN].pop(entry.entry_id) - return unload_ok + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/steam_online/config_flow.py b/homeassistant/components/steam_online/config_flow.py index 3f10b17d805..4b99bf7738d 100644 --- a/homeassistant/components/steam_online/config_flow.py +++ b/homeassistant/components/steam_online/config_flow.py @@ -10,7 +10,6 @@ import voluptuous as vol from homeassistant.config_entries import ( SOURCE_REAUTH, - ConfigEntry, ConfigFlow, ConfigFlowResult, OptionsFlow, @@ -19,6 +18,7 @@ from homeassistant.const import CONF_API_KEY, Platform from homeassistant.core import callback from homeassistant.helpers import config_validation as cv, entity_registry as er +from . import SteamConfigEntry from .const import CONF_ACCOUNT, CONF_ACCOUNTS, DOMAIN, LOGGER, PLACEHOLDERS # To avoid too long request URIs, the amount of ids to request is limited @@ -38,12 +38,12 @@ class SteamFlowHandler(ConfigFlow, domain=DOMAIN): def __init__(self) -> None: """Initialize the flow.""" - self.entry: ConfigEntry | None = None + self.entry: SteamConfigEntry | None = None @staticmethod @callback def async_get_options_flow( - config_entry: ConfigEntry, + config_entry: SteamConfigEntry, ) -> OptionsFlow: """Get the options flow for this handler.""" return SteamOptionsFlowHandler(config_entry) @@ -127,7 +127,7 @@ def _batch_ids(ids: list[str]) -> Iterator[list[str]]: class SteamOptionsFlowHandler(OptionsFlow): """Handle Steam client options.""" - def __init__(self, entry: ConfigEntry) -> None: + def __init__(self, entry: SteamConfigEntry) -> None: """Initialize options flow.""" self.entry = entry self.options = dict(entry.options) diff --git a/homeassistant/components/steam_online/coordinator.py b/homeassistant/components/steam_online/coordinator.py index 847fd297247..6e7bdf4b91c 100644 --- a/homeassistant/components/steam_online/coordinator.py +++ b/homeassistant/components/steam_online/coordinator.py @@ -3,11 +3,11 @@ from __future__ import annotations from datetime import timedelta +from typing import TYPE_CHECKING import steam from steam.api import _interface_method as INTMethod -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_API_KEY from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed @@ -15,13 +15,16 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda from .const import CONF_ACCOUNTS, DOMAIN, LOGGER +if TYPE_CHECKING: + from . import SteamConfigEntry + class SteamDataUpdateCoordinator( DataUpdateCoordinator[dict[str, dict[str, str | int]]] ): """Data update coordinator for the Steam integration.""" - config_entry: ConfigEntry + config_entry: SteamConfigEntry def __init__(self, hass: HomeAssistant) -> None: """Initialize the coordinator.""" diff --git a/homeassistant/components/steam_online/sensor.py b/homeassistant/components/steam_online/sensor.py index 8e8b70eaeb9..058bb386383 100644 --- a/homeassistant/components/steam_online/sensor.py +++ b/homeassistant/components/steam_online/sensor.py @@ -7,15 +7,14 @@ from time import localtime, mktime from typing import cast from homeassistant.components.sensor import SensorEntity, SensorEntityDescription -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from homeassistant.util.dt import utc_from_timestamp +from . import SteamConfigEntry from .const import ( CONF_ACCOUNTS, - DOMAIN, STEAM_API_URL, STEAM_HEADER_IMAGE_FILE, STEAM_ICON_URL, @@ -30,12 +29,12 @@ PARALLEL_UPDATES = 1 async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: SteamConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Steam platform.""" async_add_entities( - SteamSensor(hass.data[DOMAIN][entry.entry_id], account) + SteamSensor(entry.runtime_data, account) for account in entry.options[CONF_ACCOUNTS] ) diff --git a/tests/components/steam_online/__init__.py b/tests/components/steam_online/__init__.py index c7d67509489..d374eb1b917 100644 --- a/tests/components/steam_online/__init__.py +++ b/tests/components/steam_online/__init__.py @@ -7,8 +7,11 @@ import urllib.parse import steam -from homeassistant.components.steam_online import DOMAIN -from homeassistant.components.steam_online.const import CONF_ACCOUNT, CONF_ACCOUNTS +from homeassistant.components.steam_online.const import ( + CONF_ACCOUNT, + CONF_ACCOUNTS, + DOMAIN, +) from homeassistant.const import CONF_API_KEY from homeassistant.core import HomeAssistant From f5fd389512d3fa7a60789df3de7bd9e2316b3b56 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 18 Jun 2024 12:39:36 +0200 Subject: [PATCH 0780/1445] Fix hass-component-root-import warning in esphome tests (#119883) --- tests/components/esphome/test_update.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/components/esphome/test_update.py b/tests/components/esphome/test_update.py index 812bd2f3e18..fc845299142 100644 --- a/tests/components/esphome/test_update.py +++ b/tests/components/esphome/test_update.py @@ -14,8 +14,11 @@ from aioesphomeapi import ( import pytest from homeassistant.components.esphome.dashboard import async_get_dashboard -from homeassistant.components.update import DOMAIN as UPDATE_DOMAIN, UpdateEntityFeature -from homeassistant.components.update.const import SERVICE_INSTALL +from homeassistant.components.update import ( + DOMAIN as UPDATE_DOMAIN, + SERVICE_INSTALL, + UpdateEntityFeature, +) from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, From a1a8d381812d832eb95abf00d245988a0ff80f1d Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 18 Jun 2024 12:40:06 +0200 Subject: [PATCH 0781/1445] Move fixtures to decorators in netgear_lte tests (#119882) --- tests/components/netgear_lte/test_init.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/tests/components/netgear_lte/test_init.py b/tests/components/netgear_lte/test_init.py index ca5a22cf259..1bd3dff1eff 100644 --- a/tests/components/netgear_lte/test_init.py +++ b/tests/components/netgear_lte/test_init.py @@ -4,6 +4,7 @@ from datetime import timedelta from unittest.mock import patch from eternalegypt.eternalegypt import Error +import pytest from syrupy.assertion import SnapshotAssertion from homeassistant.components.netgear_lte.const import DOMAIN @@ -18,7 +19,8 @@ from .conftest import CONF_DATA from tests.common import async_fire_time_changed -async def test_setup_unload(hass: HomeAssistant, setup_integration: None) -> None: +@pytest.mark.usefixtures("setup_integration") +async def test_setup_unload(hass: HomeAssistant) -> None: """Test setup and unload.""" entry = hass.config_entries.async_entries(DOMAIN)[0] assert entry.state is ConfigEntryState.LOADED @@ -31,19 +33,18 @@ async def test_setup_unload(hass: HomeAssistant, setup_integration: None) -> Non assert not hass.data.get(DOMAIN) -async def test_async_setup_entry_not_ready( - hass: HomeAssistant, setup_cannot_connect: None -) -> None: +@pytest.mark.usefixtures("setup_cannot_connect") +async def test_async_setup_entry_not_ready(hass: HomeAssistant) -> None: """Test that it throws ConfigEntryNotReady when exception occurs during setup.""" entry = hass.config_entries.async_entries(DOMAIN)[0] assert len(hass.config_entries.async_entries(DOMAIN)) == 1 assert entry.state is ConfigEntryState.SETUP_RETRY +@pytest.mark.usefixtures("setup_integration") async def test_device( hass: HomeAssistant, device_registry: dr.DeviceRegistry, - setup_integration: None, snapshot: SnapshotAssertion, ) -> None: """Test device info.""" @@ -53,11 +54,8 @@ async def test_device( assert device == snapshot -async def test_update_failed( - hass: HomeAssistant, - entity_registry_enabled_by_default: None, - setup_integration: None, -) -> None: +@pytest.mark.usefixtures("entity_registry_enabled_by_default", "setup_integration") +async def test_update_failed(hass: HomeAssistant) -> None: """Test coordinator throws UpdateFailed after failed update.""" with patch( "homeassistant.components.netgear_lte.eternalegypt.Modem.information", From 6b27e9a745b3609aa0b5a7efd2b760f0e03855d6 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Tue, 18 Jun 2024 07:23:11 -0400 Subject: [PATCH 0782/1445] Store runtime data inside the config entry in Deluge (#119549) --- homeassistant/components/deluge/__init__.py | 12 +++++------- homeassistant/components/deluge/coordinator.py | 10 ++++++---- homeassistant/components/deluge/sensor.py | 12 ++++++------ homeassistant/components/deluge/switch.py | 10 +++++----- 4 files changed, 22 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/deluge/__init__.py b/homeassistant/components/deluge/__init__.py index d2f36bbc28b..62367e81af4 100644 --- a/homeassistant/components/deluge/__init__.py +++ b/homeassistant/components/deluge/__init__.py @@ -26,9 +26,10 @@ from .coordinator import DelugeDataUpdateCoordinator PLATFORMS = [Platform.SENSOR, Platform.SWITCH] _LOGGER = logging.getLogger(__name__) +type DelugeConfigEntry = ConfigEntry[DelugeDataUpdateCoordinator] -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: DelugeConfigEntry) -> bool: """Set up Deluge from a config entry.""" host = entry.data[CONF_HOST] port = entry.data[CONF_PORT] @@ -51,18 +52,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: coordinator = DelugeDataUpdateCoordinator(hass, api, entry) await coordinator.async_config_entry_first_refresh() - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator + entry.runtime_data = coordinator await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: DelugeConfigEntry) -> bool: """Unload a config entry.""" - if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): - hass.data[DOMAIN].pop(entry.entry_id) - - return unload_ok + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) class DelugeEntity(CoordinatorEntity[DelugeDataUpdateCoordinator]): diff --git a/homeassistant/components/deluge/coordinator.py b/homeassistant/components/deluge/coordinator.py index c3dd25609fe..11557561be8 100644 --- a/homeassistant/components/deluge/coordinator.py +++ b/homeassistant/components/deluge/coordinator.py @@ -4,11 +4,10 @@ from __future__ import annotations from datetime import timedelta from ssl import SSLError -from typing import Any +from typing import TYPE_CHECKING, Any from deluge_client.client import DelugeRPCClient, FailedToReconnectException -from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed @@ -16,16 +15,19 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda from .const import DATA_KEYS, LOGGER +if TYPE_CHECKING: + from . import DelugeConfigEntry + class DelugeDataUpdateCoordinator( DataUpdateCoordinator[dict[Platform, dict[str, Any]]] ): """Data update coordinator for the Deluge integration.""" - config_entry: ConfigEntry + config_entry: DelugeConfigEntry def __init__( - self, hass: HomeAssistant, api: DelugeRPCClient, entry: ConfigEntry + self, hass: HomeAssistant, api: DelugeRPCClient, entry: DelugeConfigEntry ) -> None: """Initialize the coordinator.""" super().__init__( diff --git a/homeassistant/components/deluge/sensor.py b/homeassistant/components/deluge/sensor.py index 1b96c60ec45..fd4bf36889c 100644 --- a/homeassistant/components/deluge/sensor.py +++ b/homeassistant/components/deluge/sensor.py @@ -12,14 +12,13 @@ from homeassistant.components.sensor import ( SensorEntityDescription, SensorStateClass, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_IDLE, Platform, UnitOfDataRate from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType -from . import DelugeEntity -from .const import CURRENT_STATUS, DATA_KEYS, DOMAIN, DOWNLOAD_SPEED, UPLOAD_SPEED +from . import DelugeConfigEntry, DelugeEntity +from .const import CURRENT_STATUS, DATA_KEYS, DOWNLOAD_SPEED, UPLOAD_SPEED from .coordinator import DelugeDataUpdateCoordinator @@ -74,12 +73,13 @@ SENSOR_TYPES: tuple[DelugeSensorEntityDescription, ...] = ( async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, + entry: DelugeConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Deluge sensor.""" async_add_entities( - DelugeSensor(hass.data[DOMAIN][entry.entry_id], description) - for description in SENSOR_TYPES + DelugeSensor(entry.runtime_data, description) for description in SENSOR_TYPES ) diff --git a/homeassistant/components/deluge/switch.py b/homeassistant/components/deluge/switch.py index 866f7b4f25b..cfae0244ebd 100644 --- a/homeassistant/components/deluge/switch.py +++ b/homeassistant/components/deluge/switch.py @@ -5,21 +5,21 @@ from __future__ import annotations from typing import Any from homeassistant.components.switch import SwitchEntity -from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import DelugeEntity -from .const import DOMAIN +from . import DelugeConfigEntry, DelugeEntity from .coordinator import DelugeDataUpdateCoordinator async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, + entry: DelugeConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Deluge switch.""" - async_add_entities([DelugeSwitch(hass.data[DOMAIN][entry.entry_id])]) + async_add_entities([DelugeSwitch(entry.runtime_data)]) class DelugeSwitch(DelugeEntity, SwitchEntity): From 041746a50bf3835f5741bd2dd9aff7db02062693 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 18 Jun 2024 13:25:28 +0200 Subject: [PATCH 0783/1445] Improve type hints in data_entry_flow tests (#119877) --- tests/test_data_entry_flow.py | 124 ++++++++++++++++++---------------- 1 file changed, 67 insertions(+), 57 deletions(-) diff --git a/tests/test_data_entry_flow.py b/tests/test_data_entry_flow.py index c02d909733a..782f349f9f2 100644 --- a/tests/test_data_entry_flow.py +++ b/tests/test_data_entry_flow.py @@ -19,42 +19,42 @@ from .common import ( ) +class MockFlowManager(data_entry_flow.FlowManager): + """Test flow manager.""" + + def __init__(self) -> None: + """Initialize the flow manager.""" + super().__init__(None) + self._handlers = Registry() + self.mock_reg_handler = self._handlers.register + self.mock_created_entries = [] + + async def async_create_flow(self, handler_key, *, context, data): + """Test create flow.""" + handler = self._handlers.get(handler_key) + + if handler is None: + raise data_entry_flow.UnknownHandler + + flow = handler() + flow.init_step = context.get("init_step", "init") + return flow + + async def async_finish_flow(self, flow, result): + """Test finish flow.""" + if result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY: + result["source"] = flow.context.get("source") + self.mock_created_entries.append(result) + return result + + @pytest.fixture -def manager(): +def manager() -> MockFlowManager: """Return a flow manager.""" - handlers = Registry() - entries = [] - - class FlowManager(data_entry_flow.FlowManager): - """Test flow manager.""" - - async def async_create_flow(self, handler_key, *, context, data): - """Test create flow.""" - handler = handlers.get(handler_key) - - if handler is None: - raise data_entry_flow.UnknownHandler - - flow = handler() - flow.init_step = context.get("init_step", "init") - return flow - - async def async_finish_flow(self, flow, result): - """Test finish flow.""" - if result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY: - result["source"] = flow.context.get("source") - entries.append(result) - return result - - mgr = FlowManager(None) - # pylint: disable-next=attribute-defined-outside-init - mgr.mock_created_entries = entries - # pylint: disable-next=attribute-defined-outside-init - mgr.mock_reg_handler = handlers.register - return mgr + return MockFlowManager() -async def test_configure_reuses_handler_instance(manager) -> None: +async def test_configure_reuses_handler_instance(manager: MockFlowManager) -> None: """Test that we reuse instances.""" @manager.mock_reg_handler("test") @@ -82,7 +82,7 @@ async def test_configure_reuses_handler_instance(manager) -> None: assert len(manager.mock_created_entries) == 0 -async def test_configure_two_steps(manager: data_entry_flow.FlowManager) -> None: +async def test_configure_two_steps(manager: MockFlowManager) -> None: """Test that we reuse instances.""" @manager.mock_reg_handler("test") @@ -117,7 +117,7 @@ async def test_configure_two_steps(manager: data_entry_flow.FlowManager) -> None assert result["data"] == ["INIT-DATA", "SECOND-DATA"] -async def test_show_form(manager) -> None: +async def test_show_form(manager: MockFlowManager) -> None: """Test that we can show a form.""" schema = vol.Schema({vol.Required("username"): str, vol.Required("password"): str}) @@ -136,7 +136,7 @@ async def test_show_form(manager) -> None: assert form["errors"] == {"username": "Should be unique."} -async def test_abort_removes_instance(manager) -> None: +async def test_abort_removes_instance(manager: MockFlowManager) -> None: """Test that abort removes the flow from progress.""" @manager.mock_reg_handler("test") @@ -158,7 +158,7 @@ async def test_abort_removes_instance(manager) -> None: assert len(manager.mock_created_entries) == 0 -async def test_abort_calls_async_remove(manager) -> None: +async def test_abort_calls_async_remove(manager: MockFlowManager) -> None: """Test abort calling the async_remove FlowHandler method.""" @manager.mock_reg_handler("test") @@ -177,7 +177,7 @@ async def test_abort_calls_async_remove(manager) -> None: async def test_abort_calls_async_remove_with_exception( - manager, caplog: pytest.LogCaptureFixture + manager: MockFlowManager, caplog: pytest.LogCaptureFixture ) -> None: """Test abort calling the async_remove FlowHandler method, with an exception.""" @@ -199,7 +199,7 @@ async def test_abort_calls_async_remove_with_exception( assert len(manager.mock_created_entries) == 0 -async def test_create_saves_data(manager) -> None: +async def test_create_saves_data(manager: MockFlowManager) -> None: """Test creating a config entry.""" @manager.mock_reg_handler("test") @@ -220,7 +220,7 @@ async def test_create_saves_data(manager) -> None: assert entry["source"] is None -async def test_discovery_init_flow(manager) -> None: +async def test_discovery_init_flow(manager: MockFlowManager) -> None: """Test a flow initialized by discovery.""" @manager.mock_reg_handler("test") @@ -290,7 +290,7 @@ async def test_finish_callback_change_result_type(hass: HomeAssistant) -> None: assert result["result"] == 2 -async def test_external_step(hass: HomeAssistant, manager) -> None: +async def test_external_step(hass: HomeAssistant, manager: MockFlowManager) -> None: """Test external step logic.""" manager.hass = hass @@ -340,7 +340,7 @@ async def test_external_step(hass: HomeAssistant, manager) -> None: assert result["title"] == "Hello" -async def test_show_progress(hass: HomeAssistant, manager) -> None: +async def test_show_progress(hass: HomeAssistant, manager: MockFlowManager) -> None: """Test show progress logic.""" manager.hass = hass events = [] @@ -443,7 +443,9 @@ async def test_show_progress(hass: HomeAssistant, manager) -> None: assert result["title"] == "Hello" -async def test_show_progress_error(hass: HomeAssistant, manager) -> None: +async def test_show_progress_error( + hass: HomeAssistant, manager: MockFlowManager +) -> None: """Test show progress logic.""" manager.hass = hass events = [] @@ -506,7 +508,9 @@ async def test_show_progress_error(hass: HomeAssistant, manager) -> None: assert result["reason"] == "error" -async def test_show_progress_hidden_from_frontend(hass: HomeAssistant, manager) -> None: +async def test_show_progress_hidden_from_frontend( + hass: HomeAssistant, manager: MockFlowManager +) -> None: """Test show progress done is not sent to frontend.""" manager.hass = hass async_show_progress_done_called = False @@ -557,7 +561,7 @@ async def test_show_progress_hidden_from_frontend(hass: HomeAssistant, manager) async def test_show_progress_legacy( - hass: HomeAssistant, manager, caplog: pytest.LogCaptureFixture + hass: HomeAssistant, manager: MockFlowManager, caplog: pytest.LogCaptureFixture ) -> None: """Test show progress logic. @@ -659,7 +663,7 @@ async def test_show_progress_legacy( async def test_show_progress_fires_only_when_changed( - hass: HomeAssistant, manager + hass: HomeAssistant, manager: MockFlowManager ) -> None: """Test show progress change logic.""" manager.hass = hass @@ -745,7 +749,7 @@ async def test_show_progress_fires_only_when_changed( ) # change (description placeholder) -async def test_abort_flow_exception(manager) -> None: +async def test_abort_flow_exception(manager: MockFlowManager) -> None: """Test that the AbortFlow exception works.""" @manager.mock_reg_handler("test") @@ -759,7 +763,7 @@ async def test_abort_flow_exception(manager) -> None: assert form["description_placeholders"] == {"placeholder": "yo"} -async def test_init_unknown_flow(manager) -> None: +async def test_init_unknown_flow(manager: MockFlowManager) -> None: """Test that UnknownFlow is raised when async_create_flow returns None.""" with ( @@ -769,7 +773,7 @@ async def test_init_unknown_flow(manager) -> None: await manager.async_init("test") -async def test_async_get_unknown_flow(manager) -> None: +async def test_async_get_unknown_flow(manager: MockFlowManager) -> None: """Test that UnknownFlow is raised when async_get is called with a flow_id that does not exist.""" with pytest.raises(data_entry_flow.UnknownFlow): @@ -777,7 +781,7 @@ async def test_async_get_unknown_flow(manager) -> None: async def test_async_has_matching_flow( - hass: HomeAssistant, manager: data_entry_flow.FlowManager + hass: HomeAssistant, manager: MockFlowManager ) -> None: """Test we can check for matching flows.""" manager.hass = hass @@ -854,7 +858,7 @@ async def test_async_has_matching_flow( async def test_move_to_unknown_step_raises_and_removes_from_in_progress( - manager, + manager: MockFlowManager, ) -> None: """Test that moving to an unknown step raises and removes the flow from in progress.""" @@ -880,7 +884,7 @@ async def test_move_to_unknown_step_raises_and_removes_from_in_progress( ], ) async def test_next_step_unknown_step_raises_and_removes_from_in_progress( - manager, result_type: str, params: dict[str, str] + manager: MockFlowManager, result_type: str, params: dict[str, str] ) -> None: """Test that moving to an unknown step raises and removes the flow from in progress.""" @@ -897,13 +901,17 @@ async def test_next_step_unknown_step_raises_and_removes_from_in_progress( assert manager.async_progress() == [] -async def test_configure_raises_unknown_flow_if_not_in_progress(manager) -> None: +async def test_configure_raises_unknown_flow_if_not_in_progress( + manager: MockFlowManager, +) -> None: """Test configure raises UnknownFlow if the flow is not in progress.""" with pytest.raises(data_entry_flow.UnknownFlow): await manager.async_configure("wrong_flow_id") -async def test_abort_raises_unknown_flow_if_not_in_progress(manager) -> None: +async def test_abort_raises_unknown_flow_if_not_in_progress( + manager: MockFlowManager, +) -> None: """Test abort raises UnknownFlow if the flow is not in progress.""" with pytest.raises(data_entry_flow.UnknownFlow): await manager.async_abort("wrong_flow_id") @@ -913,7 +921,11 @@ async def test_abort_raises_unknown_flow_if_not_in_progress(manager) -> None: "menu_options", [["target1", "target2"], {"target1": "Target 1", "target2": "Target 2"}], ) -async def test_show_menu(hass: HomeAssistant, manager, menu_options) -> None: +async def test_show_menu( + hass: HomeAssistant, + manager: MockFlowManager, + menu_options: list[str] | dict[str, str], +) -> None: """Test show menu.""" manager.hass = hass @@ -952,9 +964,7 @@ async def test_show_menu(hass: HomeAssistant, manager, menu_options) -> None: assert result["step_id"] == "target1" -async def test_find_flows_by_init_data_type( - manager: data_entry_flow.FlowManager, -) -> None: +async def test_find_flows_by_init_data_type(manager: MockFlowManager) -> None: """Test we can find flows by init data type.""" @dataclasses.dataclass From 3046329f4f77baa3817437fda0151c40834d5e36 Mon Sep 17 00:00:00 2001 From: Markus Jacobsen Date: Tue, 18 Jun 2024 14:00:27 +0200 Subject: [PATCH 0784/1445] Add Tidal play_media support to Bang & Olufsen (#119838) Add tidal play_media support --- homeassistant/components/bang_olufsen/const.py | 2 ++ .../components/bang_olufsen/media_player.py | 14 +++++++------- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/bang_olufsen/const.py b/homeassistant/components/bang_olufsen/const.py index 91429d0f9b0..25e7f8e15dc 100644 --- a/homeassistant/components/bang_olufsen/const.py +++ b/homeassistant/components/bang_olufsen/const.py @@ -54,6 +54,7 @@ class BangOlufsenMediaType(StrEnum): FAVOURITE = "favourite" DEEZER = "deezer" RADIO = "radio" + TIDAL = "tidal" TTS = "provider" OVERLAY_TTS = "overlay_tts" @@ -118,6 +119,7 @@ VALID_MEDIA_TYPES: Final[tuple] = ( BangOlufsenMediaType.DEEZER, BangOlufsenMediaType.RADIO, BangOlufsenMediaType.TTS, + BangOlufsenMediaType.TIDAL, BangOlufsenMediaType.OVERLAY_TTS, MediaType.MUSIC, MediaType.URL, diff --git a/homeassistant/components/bang_olufsen/media_player.py b/homeassistant/components/bang_olufsen/media_player.py index 0ce8cd22249..5c214a3fb17 100644 --- a/homeassistant/components/bang_olufsen/media_player.py +++ b/homeassistant/components/bang_olufsen/media_player.py @@ -638,20 +638,20 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity): elif media_type == BangOlufsenMediaType.FAVOURITE: await self._client.activate_preset(id=int(media_id)) - elif media_type == BangOlufsenMediaType.DEEZER: + elif media_type in (BangOlufsenMediaType.DEEZER, BangOlufsenMediaType.TIDAL): try: - if media_id == "flow": + # Play Deezer flow. + if media_id == "flow" and media_type == BangOlufsenMediaType.DEEZER: deezer_id = None if "id" in kwargs[ATTR_MEDIA_EXTRA]: deezer_id = kwargs[ATTR_MEDIA_EXTRA]["id"] - # Play Deezer flow. await self._client.start_deezer_flow( user_flow=UserFlow(user_id=deezer_id) ) - # Play a Deezer playlist or album. + # Play a playlist or album. elif any(match in media_id for match in ("playlist", "album")): start_from = 0 if "start_from" in kwargs[ATTR_MEDIA_EXTRA]: @@ -659,18 +659,18 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity): await self._client.add_to_queue( play_queue_item=PlayQueueItem( - provider=PlayQueueItemType(value="deezer"), + provider=PlayQueueItemType(value=media_type), start_now_from_position=start_from, type="playlist", uri=media_id, ) ) - # Play a Deezer track. + # Play a track. else: await self._client.add_to_queue( play_queue_item=PlayQueueItem( - provider=PlayQueueItemType(value="deezer"), + provider=PlayQueueItemType(value=media_type), start_now_from_position=0, type="track", uri=media_id, From 25b3fe6b64ba86d94416fe1f3a635f34764c98b8 Mon Sep 17 00:00:00 2001 From: Josef Zweck <24647999+zweckj@users.noreply.github.com> Date: Tue, 18 Jun 2024 14:49:04 +0200 Subject: [PATCH 0785/1445] Bump lmcloud to 1.1.13 (#119880) * bump lmcloud to 1.1.12 * update diagnostics * bump to 1.1.13 --- homeassistant/components/lamarzocco/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/lamarzocco/snapshots/test_diagnostics.ambr | 1 + 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/lamarzocco/manifest.json b/homeassistant/components/lamarzocco/manifest.json index 7714b13d12b..73d14250525 100644 --- a/homeassistant/components/lamarzocco/manifest.json +++ b/homeassistant/components/lamarzocco/manifest.json @@ -22,5 +22,5 @@ "integration_type": "device", "iot_class": "cloud_polling", "loggers": ["lmcloud"], - "requirements": ["lmcloud==1.1.11"] + "requirements": ["lmcloud==1.1.13"] } diff --git a/requirements_all.txt b/requirements_all.txt index 3f4085340d6..ca1f8fdfd57 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1263,7 +1263,7 @@ linear-garage-door==0.2.9 linode-api==4.1.9b1 # homeassistant.components.lamarzocco -lmcloud==1.1.11 +lmcloud==1.1.13 # homeassistant.components.google_maps locationsharinglib==5.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5081a168646..669f29a5f4b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1020,7 +1020,7 @@ libsoundtouch==0.8 linear-garage-door==0.2.9 # homeassistant.components.lamarzocco -lmcloud==1.1.11 +lmcloud==1.1.13 # homeassistant.components.logi_circle logi-circle==0.2.3 diff --git a/tests/components/lamarzocco/snapshots/test_diagnostics.ambr b/tests/components/lamarzocco/snapshots/test_diagnostics.ambr index 29512f0b7b0..b185557bd08 100644 --- a/tests/components/lamarzocco/snapshots/test_diagnostics.ambr +++ b/tests/components/lamarzocco/snapshots/test_diagnostics.ambr @@ -2,6 +2,7 @@ # name: test_diagnostics dict({ 'config': dict({ + 'backflush_enabled': False, 'boilers': dict({ 'CoffeeBoiler1': dict({ 'current_temperature': 96.5, From 3c08a02ecfe2d88c634beb63c629d5e62459808d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 18 Jun 2024 09:54:08 -0400 Subject: [PATCH 0786/1445] Update cover intent response (#119756) * Update cover response * Fix intent test --- homeassistant/components/cover/intent.py | 4 ++-- tests/components/cover/test_intent.py | 4 ++-- tests/components/intent/test_init.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/cover/intent.py b/homeassistant/components/cover/intent.py index f347c8cc104..b38f698ac3d 100644 --- a/homeassistant/components/cover/intent.py +++ b/homeassistant/components/cover/intent.py @@ -18,7 +18,7 @@ async def async_setup_intents(hass: HomeAssistant) -> None: INTENT_OPEN_COVER, DOMAIN, SERVICE_OPEN_COVER, - "Opened {}", + "Opening {}", description="Opens a cover", platforms={DOMAIN}, ), @@ -29,7 +29,7 @@ async def async_setup_intents(hass: HomeAssistant) -> None: INTENT_CLOSE_COVER, DOMAIN, SERVICE_CLOSE_COVER, - "Closed {}", + "Closing {}", description="Closes a cover", platforms={DOMAIN}, ), diff --git a/tests/components/cover/test_intent.py b/tests/components/cover/test_intent.py index b1dbe786065..8ee621596db 100644 --- a/tests/components/cover/test_intent.py +++ b/tests/components/cover/test_intent.py @@ -28,7 +28,7 @@ async def test_open_cover_intent(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert response.speech["plain"]["speech"] == "Opened garage door" + assert response.speech["plain"]["speech"] == "Opening garage door" assert len(calls) == 1 call = calls[0] assert call.domain == DOMAIN @@ -51,7 +51,7 @@ async def test_close_cover_intent(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert response.speech["plain"]["speech"] == "Closed garage door" + assert response.speech["plain"]["speech"] == "Closing garage door" assert len(calls) == 1 call = calls[0] assert call.domain == DOMAIN diff --git a/tests/components/intent/test_init.py b/tests/components/intent/test_init.py index 09128681b9e..7288c4855af 100644 --- a/tests/components/intent/test_init.py +++ b/tests/components/intent/test_init.py @@ -91,7 +91,7 @@ async def test_cover_intents_loading(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert response.speech["plain"]["speech"] == "Opened garage door" + assert response.speech["plain"]["speech"] == "Opening garage door" assert len(calls) == 1 call = calls[0] assert call.domain == "cover" From 0ca3f25c5784b5ba2549578689439e87ef6faf17 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 18 Jun 2024 16:15:42 +0200 Subject: [PATCH 0787/1445] Add WS command for subscribing to storage collection changes (#119481) --- homeassistant/helpers/collection.py | 64 +++++- tests/components/lovelace/test_resources.py | 100 ++++++++-- tests/helpers/test_collection.py | 211 ++++++++++++++++++++ 3 files changed, 361 insertions(+), 14 deletions(-) diff --git a/homeassistant/helpers/collection.py b/homeassistant/helpers/collection.py index 1ce4a9d092b..1dd94d85f9a 100644 --- a/homeassistant/helpers/collection.py +++ b/homeassistant/helpers/collection.py @@ -18,7 +18,7 @@ from voluptuous.humanize import humanize_error from homeassistant.components import websocket_api from homeassistant.const import CONF_ID -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.util import slugify @@ -525,6 +525,9 @@ class StorageCollectionWebsocket[_StorageCollectionT: StorageCollection]: self.create_schema = create_schema self.update_schema = update_schema + self._remove_subscription: CALLBACK_TYPE | None = None + self._subscribers: set[tuple[websocket_api.ActiveConnection, int]] = set() + assert self.api_prefix[-1] != "/", "API prefix should not end in /" @property @@ -564,6 +567,15 @@ class StorageCollectionWebsocket[_StorageCollectionT: StorageCollection]: ), ) + websocket_api.async_register_command( + hass, + f"{self.api_prefix}/subscribe", + self._ws_subscribe, + websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( + {vol.Required("type"): f"{self.api_prefix}/subscribe"} + ), + ) + websocket_api.async_register_command( hass, f"{self.api_prefix}/update", @@ -619,6 +631,56 @@ class StorageCollectionWebsocket[_StorageCollectionT: StorageCollection]: except ValueError as err: connection.send_error(msg["id"], websocket_api.ERR_INVALID_FORMAT, str(err)) + @callback + def _ws_subscribe( + self, hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict + ) -> None: + """Subscribe to collection updates.""" + + async def async_change_listener( + change_set: Iterable[CollectionChange], + ) -> None: + json_msg = [ + { + "change_type": change.change_type, + self.item_id_key: change.item_id, + "item": change.item, + } + for change in change_set + ] + for connection, msg_id in self._subscribers: + connection.send_message(websocket_api.event_message(msg_id, json_msg)) + + if not self._subscribers: + self._remove_subscription = ( + self.storage_collection.async_add_change_set_listener( + async_change_listener + ) + ) + + self._subscribers.add((connection, msg["id"])) + + @callback + def cancel_subscription() -> None: + self._subscribers.remove((connection, msg["id"])) + if not self._subscribers and self._remove_subscription: + self._remove_subscription() + self._remove_subscription = None + + connection.subscriptions[msg["id"]] = cancel_subscription + + connection.send_message(websocket_api.result_message(msg["id"])) + + json_msg = [ + { + "change_type": CHANGE_ADDED, + self.item_id_key: item_id, + "item": item, + } + for item_id, item in self.storage_collection.data.items() + ] + connection.send_message(websocket_api.event_message(msg["id"], json_msg)) + async def ws_update_item( self, hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict ) -> None: diff --git a/tests/components/lovelace/test_resources.py b/tests/components/lovelace/test_resources.py index bf6b44f0950..281fb001fc2 100644 --- a/tests/components/lovelace/test_resources.py +++ b/tests/components/lovelace/test_resources.py @@ -2,7 +2,7 @@ import copy from typing import Any -from unittest.mock import patch +from unittest.mock import ANY, patch import uuid import pytest @@ -101,8 +101,43 @@ async def test_storage_resources_import( client = await hass_ws_client(hass) - # Fetch data - await client.send_json({"id": 5, "type": list_cmd}) + # Subscribe + await client.send_json_auto_id({"type": "lovelace/resources/subscribe"}) + response = await client.receive_json() + assert response["success"] + assert response["result"] is None + event_id = response["id"] + + response = await client.receive_json() + assert response["id"] == event_id + assert response["event"] == [] + + # Fetch data - this also loads the resources + await client.send_json_auto_id({"type": list_cmd}) + + response = await client.receive_json() + assert response["id"] == event_id + assert response["event"] == [ + { + "change_type": "added", + "item": { + "id": ANY, + "type": "js", + "url": "/local/bla.js", + }, + "resource_id": ANY, + }, + { + "change_type": "added", + "item": { + "id": ANY, + "type": "css", + "url": "/local/bla.css", + }, + "resource_id": ANY, + }, + ] + response = await client.receive_json() assert response["success"] assert ( @@ -115,18 +150,31 @@ async def test_storage_resources_import( ) # Add a resource - await client.send_json( + await client.send_json_auto_id( { - "id": 6, "type": "lovelace/resources/create", "res_type": "module", "url": "/local/yo.js", } ) + response = await client.receive_json() + assert response["id"] == event_id + assert response["event"] == [ + { + "change_type": "added", + "item": { + "id": ANY, + "type": "module", + "url": "/local/yo.js", + }, + "resource_id": ANY, + } + ] + response = await client.receive_json() assert response["success"] - await client.send_json({"id": 7, "type": list_cmd}) + await client.send_json_auto_id({"type": list_cmd}) response = await client.receive_json() assert response["success"] @@ -137,19 +185,32 @@ async def test_storage_resources_import( # Update a resource first_item = response["result"][0] - await client.send_json( + await client.send_json_auto_id( { - "id": 8, "type": "lovelace/resources/update", "resource_id": first_item["id"], "res_type": "css", "url": "/local/updated.css", } ) + response = await client.receive_json() + assert response["id"] == event_id + assert response["event"] == [ + { + "change_type": "updated", + "item": { + "id": first_item["id"], + "type": "css", + "url": "/local/updated.css", + }, + "resource_id": first_item["id"], + } + ] + response = await client.receive_json() assert response["success"] - await client.send_json({"id": 9, "type": list_cmd}) + await client.send_json_auto_id({"type": list_cmd}) response = await client.receive_json() assert response["success"] @@ -157,18 +218,31 @@ async def test_storage_resources_import( assert first_item["type"] == "css" assert first_item["url"] == "/local/updated.css" - # Delete resources - await client.send_json( + # Delete a resource + await client.send_json_auto_id( { - "id": 10, "type": "lovelace/resources/delete", "resource_id": first_item["id"], } ) + response = await client.receive_json() + assert response["id"] == event_id + assert response["event"] == [ + { + "change_type": "removed", + "item": { + "id": first_item["id"], + "type": "css", + "url": "/local/updated.css", + }, + "resource_id": first_item["id"], + } + ] + response = await client.receive_json() assert response["success"] - await client.send_json({"id": 11, "type": list_cmd}) + await client.send_json_auto_id({"type": list_cmd}) response = await client.receive_json() assert response["success"] diff --git a/tests/helpers/test_collection.py b/tests/helpers/test_collection.py index dc9ac21e246..f4d5b06dae0 100644 --- a/tests/helpers/test_collection.py +++ b/tests/helpers/test_collection.py @@ -563,3 +563,214 @@ async def test_storage_collection_websocket( "name": "Updated name", }, ) + + +async def test_storage_collection_websocket_subscribe( + hass: HomeAssistant, hass_ws_client: WebSocketGenerator +) -> None: + """Test exposing a storage collection via websockets.""" + store = storage.Store(hass, 1, "test-data") + coll = MockStorageCollection(store) + changes = track_changes(coll) + collection.DictStorageCollectionWebsocket( + coll, + "test_item/collection", + "test_item", + {vol.Required("name"): str, vol.Required("immutable_string"): str}, + {vol.Optional("name"): str}, + ).async_setup(hass) + + client = await hass_ws_client(hass) + + # Subscribe + await client.send_json_auto_id({"type": "test_item/collection/subscribe"}) + response = await client.receive_json() + assert response["success"] + assert response["result"] is None + assert len(changes) == 0 + event_id = response["id"] + + response = await client.receive_json() + assert response["id"] == event_id + assert response["event"] == [] + + # Create invalid + await client.send_json_auto_id( + { + "type": "test_item/collection/create", + "name": 1, + # Forgot to add immutable_string + } + ) + response = await client.receive_json() + assert not response["success"] + assert response["error"]["code"] == "invalid_format" + assert len(changes) == 0 + + # Create + await client.send_json_auto_id( + { + "type": "test_item/collection/create", + "name": "Initial Name", + "immutable_string": "no-changes", + } + ) + response = await client.receive_json() + assert response["id"] == event_id + assert response["event"] == [ + { + "change_type": "added", + "item": { + "id": "initial_name", + "immutable_string": "no-changes", + "name": "Initial Name", + }, + "test_item_id": "initial_name", + } + ] + response = await client.receive_json() + assert response["success"] + assert response["result"] == { + "id": "initial_name", + "name": "Initial Name", + "immutable_string": "no-changes", + } + assert len(changes) == 1 + assert changes[0] == (collection.CHANGE_ADDED, "initial_name", response["result"]) + + # Subscribe again + await client.send_json_auto_id({"type": "test_item/collection/subscribe"}) + response = await client.receive_json() + assert response["success"] + assert response["result"] is None + event_id_2 = response["id"] + + response = await client.receive_json() + assert response["id"] == event_id_2 + assert response["event"] == [ + { + "change_type": "added", + "item": { + "id": "initial_name", + "immutable_string": "no-changes", + "name": "Initial Name", + }, + "test_item_id": "initial_name", + }, + ] + + await client.send_json_auto_id( + {"type": "unsubscribe_events", "subscription": event_id_2} + ) + response = await client.receive_json() + assert response["success"] + + # List + await client.send_json_auto_id({"type": "test_item/collection/list"}) + response = await client.receive_json() + assert response["success"] + assert response["result"] == [ + { + "id": "initial_name", + "name": "Initial Name", + "immutable_string": "no-changes", + } + ] + assert len(changes) == 1 + + # Update invalid data + await client.send_json_auto_id( + { + "type": "test_item/collection/update", + "test_item_id": "initial_name", + "immutable_string": "no-changes", + } + ) + response = await client.receive_json() + assert not response["success"] + assert response["error"]["code"] == "invalid_format" + assert len(changes) == 1 + + # Update invalid item + await client.send_json_auto_id( + { + "type": "test_item/collection/update", + "test_item_id": "non-existing", + "name": "Updated name", + } + ) + response = await client.receive_json() + assert not response["success"] + assert response["error"]["code"] == "not_found" + assert len(changes) == 1 + + # Update + await client.send_json_auto_id( + { + "type": "test_item/collection/update", + "test_item_id": "initial_name", + "name": "Updated name", + } + ) + response = await client.receive_json() + assert response["id"] == event_id + assert response["event"] == [ + { + "change_type": "updated", + "item": { + "id": "initial_name", + "immutable_string": "no-changes", + "name": "Updated name", + }, + "test_item_id": "initial_name", + } + ] + response = await client.receive_json() + assert response["success"] + assert response["result"] == { + "id": "initial_name", + "name": "Updated name", + "immutable_string": "no-changes", + } + assert len(changes) == 2 + assert changes[1] == (collection.CHANGE_UPDATED, "initial_name", response["result"]) + + # Delete invalid ID + await client.send_json_auto_id( + {"type": "test_item/collection/update", "test_item_id": "non-existing"} + ) + response = await client.receive_json() + assert not response["success"] + assert response["error"]["code"] == "not_found" + assert len(changes) == 2 + + # Delete + await client.send_json_auto_id( + {"type": "test_item/collection/delete", "test_item_id": "initial_name"} + ) + response = await client.receive_json() + assert response["id"] == event_id + assert response["event"] == [ + { + "change_type": "removed", + "item": { + "id": "initial_name", + "immutable_string": "no-changes", + "name": "Updated name", + }, + "test_item_id": "initial_name", + } + ] + response = await client.receive_json() + assert response["success"] + + assert len(changes) == 3 + assert changes[2] == ( + collection.CHANGE_REMOVED, + "initial_name", + { + "id": "initial_name", + "immutable_string": "no-changes", + "name": "Updated name", + }, + ) From 79403031491176c1b8540b89e04bd071f7d85379 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 18 Jun 2024 16:18:42 +0200 Subject: [PATCH 0788/1445] Add WS command frontend/subscribe_extra_js (#119833) Co-authored-by: Robert Resch --- homeassistant/components/frontend/__init__.py | 60 +++++++++++++++++-- tests/components/frontend/test_init.py | 39 +++++++++++- 2 files changed, 93 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 2f038e34102..5f68ebeac18 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -2,8 +2,8 @@ from __future__ import annotations -from collections.abc import Iterator -from functools import lru_cache +from collections.abc import Callable, Iterator +from functools import lru_cache, partial import logging import os import pathlib @@ -33,6 +33,7 @@ from homeassistant.helpers.storage import Store from homeassistant.helpers.translation import async_get_translations from homeassistant.helpers.typing import ConfigType from homeassistant.loader import async_get_integration, bind_hass +from homeassistant.util.hass_dict import HassKey from .storage import async_setup_frontend_storage @@ -56,6 +57,10 @@ DATA_JS_VERSION = "frontend_js_version" DATA_EXTRA_MODULE_URL = "frontend_extra_module_url" DATA_EXTRA_JS_URL_ES5 = "frontend_extra_js_url_es5" +DATA_WS_SUBSCRIBERS: HassKey[set[tuple[websocket_api.ActiveConnection, int]]] = HassKey( + "frontend_ws_subscribers" +) + THEMES_STORAGE_KEY = f"{DOMAIN}_theme" THEMES_STORAGE_VERSION = 1 THEMES_SAVE_DELAY = 60 @@ -204,17 +209,24 @@ class UrlManager: on hass.data """ - def __init__(self, urls: list[str]) -> None: + def __init__( + self, + on_change: Callable[[str, str], None], + urls: list[str], + ) -> None: """Init the url manager.""" + self._on_change = on_change self.urls = frozenset(urls) def add(self, url: str) -> None: """Add a url to the set.""" self.urls = frozenset([*self.urls, url]) + self._on_change("added", url) def remove(self, url: str) -> None: """Remove a url from the set.""" self.urls = self.urls - {url} + self._on_change("removed", url) class Panel: @@ -363,6 +375,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: websocket_api.async_register_command(hass, websocket_get_themes) websocket_api.async_register_command(hass, websocket_get_translations) websocket_api.async_register_command(hass, websocket_get_version) + websocket_api.async_register_command(hass, websocket_subscribe_extra_js) hass.http.register_view(ManifestJSONView()) conf = config.get(DOMAIN, {}) @@ -420,8 +433,27 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: sidebar_icon="hass:hammer", ) - hass.data[DATA_EXTRA_MODULE_URL] = UrlManager(conf.get(CONF_EXTRA_MODULE_URL, [])) - hass.data[DATA_EXTRA_JS_URL_ES5] = UrlManager(conf.get(CONF_EXTRA_JS_URL_ES5, [])) + @callback + def async_change_listener( + resource_type: str, + change_type: str, + url: str, + ) -> None: + subscribers = hass.data[DATA_WS_SUBSCRIBERS] + json_msg = { + "change_type": change_type, + "item": {"type": resource_type, "url": url}, + } + for connection, msg_id in subscribers: + connection.send_message(websocket_api.event_message(msg_id, json_msg)) + + hass.data[DATA_EXTRA_MODULE_URL] = UrlManager( + partial(async_change_listener, "module"), conf.get(CONF_EXTRA_MODULE_URL, []) + ) + hass.data[DATA_EXTRA_JS_URL_ES5] = UrlManager( + partial(async_change_listener, "es5"), conf.get(CONF_EXTRA_JS_URL_ES5, []) + ) + hass.data[DATA_WS_SUBSCRIBERS] = set() await _async_setup_themes(hass, conf.get(CONF_THEMES)) @@ -783,6 +815,24 @@ async def websocket_get_version( connection.send_result(msg["id"], {"version": frontend}) +@callback +@websocket_api.websocket_command({"type": "frontend/subscribe_extra_js"}) +def websocket_subscribe_extra_js( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: + """Subscribe to URL manager updates.""" + + subscribers = hass.data[DATA_WS_SUBSCRIBERS] + subscribers.add((connection, msg["id"])) + + @callback + def cancel_subscription() -> None: + subscribers.remove((connection, msg["id"])) + + connection.subscriptions[msg["id"]] = cancel_subscription + connection.send_message(websocket_api.result_message(msg["id"])) + + class PanelRespons(TypedDict): """Represent the panel response type.""" diff --git a/tests/components/frontend/test_init.py b/tests/components/frontend/test_init.py index b8642aa997d..a9c24d256e5 100644 --- a/tests/components/frontend/test_init.py +++ b/tests/components/frontend/test_init.py @@ -409,7 +409,11 @@ async def test_missing_themes(hass: HomeAssistant, ws_client) -> None: @pytest.mark.usefixtures("mock_onboarded") -async def test_extra_js(hass: HomeAssistant, mock_http_client_with_extra_js) -> None: +async def test_extra_js( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + mock_http_client_with_extra_js, +) -> None: """Test that extra javascript is loaded.""" async def get_response(): @@ -423,6 +427,13 @@ async def test_extra_js(hass: HomeAssistant, mock_http_client_with_extra_js) -> assert '"/local/my_module.js"' in text assert '"/local/my_es5.js"' in text + client = await hass_ws_client(hass) + await client.send_json_auto_id({"type": "frontend/subscribe_extra_js"}) + msg = await client.receive_json() + + assert msg["success"] is True + subscription_id = msg["id"] + # Test dynamically adding and removing extra javascript add_extra_js_url(hass, "/local/my_module_2.js", False) add_extra_js_url(hass, "/local/my_es5_2.js", True) @@ -430,12 +441,38 @@ async def test_extra_js(hass: HomeAssistant, mock_http_client_with_extra_js) -> assert '"/local/my_module_2.js"' in text assert '"/local/my_es5_2.js"' in text + msg = await client.receive_json() + assert msg["id"] == subscription_id + assert msg["event"] == { + "change_type": "added", + "item": {"type": "module", "url": "/local/my_module_2.js"}, + } + msg = await client.receive_json() + assert msg["id"] == subscription_id + assert msg["event"] == { + "change_type": "added", + "item": {"type": "es5", "url": "/local/my_es5_2.js"}, + } + remove_extra_js_url(hass, "/local/my_module_2.js", False) remove_extra_js_url(hass, "/local/my_es5_2.js", True) text = await get_response() assert '"/local/my_module_2.js"' not in text assert '"/local/my_es5_2.js"' not in text + msg = await client.receive_json() + assert msg["id"] == subscription_id + assert msg["event"] == { + "change_type": "removed", + "item": {"type": "module", "url": "/local/my_module_2.js"}, + } + msg = await client.receive_json() + assert msg["id"] == subscription_id + assert msg["event"] == { + "change_type": "removed", + "item": {"type": "es5", "url": "/local/my_es5_2.js"}, + } + # Remove again should not raise remove_extra_js_url(hass, "/local/my_module_2.js", False) remove_extra_js_url(hass, "/local/my_es5_2.js", True) From e0de436a581e4df970457f70be5087516f97884e Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 18 Jun 2024 19:03:30 +0200 Subject: [PATCH 0789/1445] Add myself as codeowner for Nanoleaf (#119892) --- CODEOWNERS | 4 ++-- homeassistant/components/nanoleaf/manifest.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 103c66d3994..71ac96c05e7 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -910,8 +910,8 @@ build.json @home-assistant/supervisor /tests/components/myuplink/ @pajzo @astrandb /homeassistant/components/nam/ @bieniu /tests/components/nam/ @bieniu -/homeassistant/components/nanoleaf/ @milanmeu -/tests/components/nanoleaf/ @milanmeu +/homeassistant/components/nanoleaf/ @milanmeu @joostlek +/tests/components/nanoleaf/ @milanmeu @joostlek /homeassistant/components/neato/ @Santobert /tests/components/neato/ @Santobert /homeassistant/components/nederlandse_spoorwegen/ @YarmoM diff --git a/homeassistant/components/nanoleaf/manifest.json b/homeassistant/components/nanoleaf/manifest.json index 3afb086d1a6..4b4c026260d 100644 --- a/homeassistant/components/nanoleaf/manifest.json +++ b/homeassistant/components/nanoleaf/manifest.json @@ -1,7 +1,7 @@ { "domain": "nanoleaf", "name": "Nanoleaf", - "codeowners": ["@milanmeu"], + "codeowners": ["@milanmeu", "@joostlek"], "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/nanoleaf", "homekit": { From 407df2aedf7f61f0e9f3ced3b946f41baf64cb3d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 18 Jun 2024 12:08:22 -0500 Subject: [PATCH 0790/1445] Small cleanup to unifiprotect entity descriptions (#119904) --- .../components/unifiprotect/models.py | 30 +++++++------------ 1 file changed, 10 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/unifiprotect/models.py b/homeassistant/components/unifiprotect/models.py index 36db9a847c7..fc24ddaa6e3 100644 --- a/homeassistant/components/unifiprotect/models.py +++ b/homeassistant/components/unifiprotect/models.py @@ -39,43 +39,33 @@ class ProtectEntityDescription(EntityDescription, Generic[T]): ufp_enabled: str | None = None ufp_perm: PermRequired | None = None - def get_ufp_value(self, obj: T) -> Any: - """Return value from UniFi Protect device. + # The below are set in __post_init__ + has_required: Callable[[T], bool] = bool + get_ufp_enabled: Callable[[T], bool] = bool - May be overridden by ufp_value or ufp_value_fn. - """ - # ufp_value or ufp_value_fn is required, the + def get_ufp_value(self, obj: T) -> Any: + """Return value from UniFi Protect device; overridden in __post_init__.""" + # ufp_value or ufp_value_fn are required, the # RuntimeError is to catch any issues in the code # with new descriptions. raise RuntimeError( # pragma: no cover - "`ufp_value` or `ufp_value_fn` is required" + f"`ufp_value` or `ufp_value_fn` is required for {self}" ) - def has_required(self, obj: T) -> bool: - """Return if required field is set. - - May be overridden by ufp_required_field. - """ - return True - - def get_ufp_enabled(self, obj: T) -> bool: - """Return if entity is enabled. - - May be overridden by ufp_enabled. - """ - return True - def __post_init__(self) -> None: """Override get_ufp_value, has_required, and get_ufp_enabled if required.""" _setter = partial(object.__setattr__, self) + if (_ufp_value := self.ufp_value) is not None: ufp_value = tuple(_ufp_value.split(".")) _setter("get_ufp_value", partial(get_nested_attr, attrs=ufp_value)) elif (ufp_value_fn := self.ufp_value_fn) is not None: _setter("get_ufp_value", ufp_value_fn) + if (_ufp_enabled := self.ufp_enabled) is not None: ufp_enabled = tuple(_ufp_enabled.split(".")) _setter("get_ufp_enabled", partial(get_nested_attr, attrs=ufp_enabled)) + if (_ufp_required_field := self.ufp_required_field) is not None: ufp_required_field = tuple(_ufp_required_field.split(".")) _setter( From d2faaa15315636542547f5456dea937e8a4cbb36 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 18 Jun 2024 12:29:26 -0500 Subject: [PATCH 0791/1445] Remove useless function get_ufp_event from unifiprotect (#119906) --- .../components/unifiprotect/media_source.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/unifiprotect/media_source.py b/homeassistant/components/unifiprotect/media_source.py index d6acb876c94..a646c037d62 100644 --- a/homeassistant/components/unifiprotect/media_source.py +++ b/homeassistant/components/unifiprotect/media_source.py @@ -81,12 +81,6 @@ EVENT_NAME_MAP = { } -def get_ufp_event(event_type: SimpleEventType) -> set[EventType]: - """Get UniFi Protect event type from SimpleEventType.""" - - return EVENT_MAP[event_type] - - async def async_get_media_source(hass: HomeAssistant) -> MediaSource: """Set up UniFi Protect media source.""" return ProtectMediaSource( @@ -488,7 +482,7 @@ class ProtectMediaSource(MediaSource): ) -> list[BrowseMediaSource]: """Build media source for a given range of time and event type.""" - event_types = event_types or get_ufp_event(SimpleEventType.ALL) + event_types = event_types or EVENT_MAP[SimpleEventType.ALL] types = list(event_types) sources: list[BrowseMediaSource] = [] events = await data.api.get_events_raw( @@ -554,7 +548,7 @@ class ProtectMediaSource(MediaSource): start=now - timedelta(days=days), end=now, camera_id=event_camera_id, - event_types=get_ufp_event(event_type), + event_types=EVENT_MAP[event_type], reserve=True, ) source.children = events @@ -686,7 +680,7 @@ class ProtectMediaSource(MediaSource): end=end_dt, camera_id=event_camera_id, reserve=False, - event_types=get_ufp_event(event_type), + event_types=EVENT_MAP[event_type], ) source.children = events source.title = self._breadcrumb( From 419dcbf9a1a0ca5903d6d608fc6a7782a654f1a1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 18 Jun 2024 12:44:27 -0500 Subject: [PATCH 0792/1445] Fix typo in KEY_ALLOW_CONFIGRED_CORS (#119905) --- homeassistant/components/http/__init__.py | 6 +++--- homeassistant/components/http/cors.py | 6 +++--- homeassistant/helpers/http.py | 4 ++-- tests/components/http/test_cors.py | 4 ++-- tests/components/http/test_data_validator.py | 4 ++-- tests/components/http/test_static.py | 4 ++-- 6 files changed, 14 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index 4e62df3a024..fae50f97a33 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -37,7 +37,7 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import storage import homeassistant.helpers.config_validation as cv from homeassistant.helpers.http import ( - KEY_ALLOW_CONFIGRED_CORS, + KEY_ALLOW_CONFIGURED_CORS, KEY_AUTHENTICATED, # noqa: F401 KEY_HASS, HomeAssistantView, @@ -427,7 +427,7 @@ class HomeAssistantHTTP: # Should be instance of aiohttp.web_exceptions._HTTPMove. raise redirect_exc(redirect_to) # type: ignore[arg-type,misc] - self.app[KEY_ALLOW_CONFIGRED_CORS]( + self.app[KEY_ALLOW_CONFIGURED_CORS]( self.app.router.add_route("GET", url, redirect) ) @@ -461,7 +461,7 @@ class HomeAssistantHTTP: ) -> None: """Register a folders or files to serve as a static path.""" app = self.app - allow_cors = app[KEY_ALLOW_CONFIGRED_CORS] + allow_cors = app[KEY_ALLOW_CONFIGURED_CORS] for config in configs: if resource := resources[config.url_path]: app.router.register_resource(resource) diff --git a/homeassistant/components/http/cors.py b/homeassistant/components/http/cors.py index d97ac9922a2..69e7c7ea2d5 100644 --- a/homeassistant/components/http/cors.py +++ b/homeassistant/components/http/cors.py @@ -19,7 +19,7 @@ from homeassistant.const import HTTP_HEADER_X_REQUESTED_WITH from homeassistant.core import callback from homeassistant.helpers.http import ( KEY_ALLOW_ALL_CORS, - KEY_ALLOW_CONFIGRED_CORS, + KEY_ALLOW_CONFIGURED_CORS, AllowCorsType, ) @@ -82,6 +82,6 @@ def setup_cors(app: Application, origins: list[str]) -> None: ) if origins: - app[KEY_ALLOW_CONFIGRED_CORS] = cast(AllowCorsType, _allow_cors) + app[KEY_ALLOW_CONFIGURED_CORS] = cast(AllowCorsType, _allow_cors) else: - app[KEY_ALLOW_CONFIGRED_CORS] = lambda _: None + app[KEY_ALLOW_CONFIGURED_CORS] = lambda _: None diff --git a/homeassistant/helpers/http.py b/homeassistant/helpers/http.py index bbe4e26f4e5..22f8e2acbeb 100644 --- a/homeassistant/helpers/http.py +++ b/homeassistant/helpers/http.py @@ -33,7 +33,7 @@ _LOGGER = logging.getLogger(__name__) type AllowCorsType = Callable[[AbstractRoute | AbstractResource], None] KEY_AUTHENTICATED: Final = "ha_authenticated" KEY_ALLOW_ALL_CORS = AppKey[AllowCorsType]("allow_all_cors") -KEY_ALLOW_CONFIGRED_CORS = AppKey[AllowCorsType]("allow_configured_cors") +KEY_ALLOW_CONFIGURED_CORS = AppKey[AllowCorsType]("allow_configured_cors") KEY_HASS: AppKey[HomeAssistant] = AppKey("hass") current_request: ContextVar[Request | None] = ContextVar( @@ -181,7 +181,7 @@ class HomeAssistantView: if self.cors_allowed: allow_cors = app.get(KEY_ALLOW_ALL_CORS) else: - allow_cors = app.get(KEY_ALLOW_CONFIGRED_CORS) + allow_cors = app.get(KEY_ALLOW_CONFIGURED_CORS) if allow_cors: for route in routes: diff --git a/tests/components/http/test_cors.py b/tests/components/http/test_cors.py index 04f5db753c9..1188131cc0f 100644 --- a/tests/components/http/test_cors.py +++ b/tests/components/http/test_cors.py @@ -20,7 +20,7 @@ import pytest from homeassistant.components.http.cors import setup_cors from homeassistant.components.http.view import HomeAssistantView from homeassistant.core import HomeAssistant -from homeassistant.helpers.http import KEY_ALLOW_CONFIGRED_CORS +from homeassistant.helpers.http import KEY_ALLOW_CONFIGURED_CORS from homeassistant.setup import async_setup_component from . import HTTP_HEADER_HA_AUTH @@ -62,7 +62,7 @@ def client( """Fixture to set up a web.Application.""" app = web.Application() setup_cors(app, [TRUSTED_ORIGIN]) - app[KEY_ALLOW_CONFIGRED_CORS](app.router.add_get("/", mock_handler)) + app[KEY_ALLOW_CONFIGURED_CORS](app.router.add_get("/", mock_handler)) return event_loop.run_until_complete(aiohttp_client(app)) diff --git a/tests/components/http/test_data_validator.py b/tests/components/http/test_data_validator.py index 9a4e80052f6..b415e54af04 100644 --- a/tests/components/http/test_data_validator.py +++ b/tests/components/http/test_data_validator.py @@ -8,7 +8,7 @@ import voluptuous as vol from homeassistant.components.http import KEY_HASS, HomeAssistantView from homeassistant.components.http.data_validator import RequestDataValidator -from homeassistant.helpers.http import KEY_ALLOW_CONFIGRED_CORS +from homeassistant.helpers.http import KEY_ALLOW_CONFIGURED_CORS from tests.typing import ClientSessionGenerator @@ -17,7 +17,7 @@ async def get_client(aiohttp_client, validator): """Generate a client that hits a view decorated with validator.""" app = web.Application() app[KEY_HASS] = Mock(is_stopping=False) - app[KEY_ALLOW_CONFIGRED_CORS] = lambda _: None + app[KEY_ALLOW_CONFIGURED_CORS] = lambda _: None class TestView(HomeAssistantView): url = "/" diff --git a/tests/components/http/test_static.py b/tests/components/http/test_static.py index 92e92cdb4a7..3e3f21d5002 100644 --- a/tests/components/http/test_static.py +++ b/tests/components/http/test_static.py @@ -10,7 +10,7 @@ import pytest from homeassistant.components.http import StaticPathConfig from homeassistant.components.http.static import CachingStaticResource, _get_file_path from homeassistant.core import EVENT_HOMEASSISTANT_START, HomeAssistant -from homeassistant.helpers.http import KEY_ALLOW_CONFIGRED_CORS +from homeassistant.helpers.http import KEY_ALLOW_CONFIGURED_CORS from homeassistant.setup import async_setup_component from tests.typing import ClientSessionGenerator @@ -51,7 +51,7 @@ async def test_static_path_blocks_anchors( resource = CachingStaticResource(url, str(tmp_path)) assert resource.canonical == canonical_url app.router.register_resource(resource) - app[KEY_ALLOW_CONFIGRED_CORS](resource) + app[KEY_ALLOW_CONFIGURED_CORS](resource) resp = await mock_http_client.get(canonical_url, allow_redirects=False) assert resp.status == 403 From edb391a0bd7128375c5a70e7f581c85673e66288 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 18 Jun 2024 19:50:07 +0200 Subject: [PATCH 0793/1445] Extract coordinator to separate module in Nanoleaf (#119896) * Extract coordinator to separate module in Nanoleaf * Extract coordinator to separate module in Nanoleaf * Extract coordinator to separate module in Nanoleaf --- .coveragerc | 1 + homeassistant/components/nanoleaf/__init__.py | 32 +++---------------- homeassistant/components/nanoleaf/button.py | 17 +++------- .../components/nanoleaf/coordinator.py | 31 ++++++++++++++++++ homeassistant/components/nanoleaf/entity.py | 16 +++------- homeassistant/components/nanoleaf/light.py | 21 ++++++------ 6 files changed, 55 insertions(+), 63 deletions(-) create mode 100644 homeassistant/components/nanoleaf/coordinator.py diff --git a/.coveragerc b/.coveragerc index 390c098418e..d8d8bbdf80d 100644 --- a/.coveragerc +++ b/.coveragerc @@ -854,6 +854,7 @@ omit = homeassistant/components/nad/media_player.py homeassistant/components/nanoleaf/__init__.py homeassistant/components/nanoleaf/button.py + homeassistant/components/nanoleaf/coordinator.py homeassistant/components/nanoleaf/entity.py homeassistant/components/nanoleaf/light.py homeassistant/components/neato/__init__.py diff --git a/homeassistant/components/nanoleaf/__init__.py b/homeassistant/components/nanoleaf/__init__.py index 9e368353774..c8211969f87 100644 --- a/homeassistant/components/nanoleaf/__init__.py +++ b/homeassistant/components/nanoleaf/__init__.py @@ -4,17 +4,9 @@ from __future__ import annotations import asyncio from dataclasses import dataclass -from datetime import timedelta import logging -from aionanoleaf import ( - EffectsEvent, - InvalidToken, - Nanoleaf, - StateEvent, - TouchEvent, - Unavailable, -) +from aionanoleaf import EffectsEvent, Nanoleaf, StateEvent, TouchEvent from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -25,12 +17,11 @@ from homeassistant.const import ( Platform, ) from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers import device_registry as dr from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import DOMAIN, NANOLEAF_EVENT, TOUCH_GESTURE_TRIGGER_MAP, TOUCH_MODELS +from .coordinator import NanoleafCoordinator _LOGGER = logging.getLogger(__name__) @@ -42,7 +33,7 @@ class NanoleafEntryData: """Class for sharing data within the Nanoleaf integration.""" device: Nanoleaf - coordinator: DataUpdateCoordinator[None] + coordinator: NanoleafCoordinator event_listener: asyncio.Task @@ -52,22 +43,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async_get_clientsession(hass), entry.data[CONF_HOST], entry.data[CONF_TOKEN] ) - async def async_get_state() -> None: - """Get the state of the device.""" - try: - await nanoleaf.get_info() - except Unavailable as err: - raise UpdateFailed from err - except InvalidToken as err: - raise ConfigEntryAuthFailed from err - - coordinator = DataUpdateCoordinator( - hass, - _LOGGER, - name=entry.title, - update_interval=timedelta(minutes=1), - update_method=async_get_state, - ) + coordinator = NanoleafCoordinator(hass, nanoleaf) await coordinator.async_config_entry_first_refresh() diff --git a/homeassistant/components/nanoleaf/button.py b/homeassistant/components/nanoleaf/button.py index 950dc2a591a..dd0cc221fc2 100644 --- a/homeassistant/components/nanoleaf/button.py +++ b/homeassistant/components/nanoleaf/button.py @@ -1,15 +1,12 @@ """Support for Nanoleaf buttons.""" -from aionanoleaf import Nanoleaf - from homeassistant.components.button import ButtonDeviceClass, ButtonEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from . import NanoleafEntryData +from . import NanoleafCoordinator, NanoleafEntryData from .const import DOMAIN from .entity import NanoleafEntity @@ -19,9 +16,7 @@ async def async_setup_entry( ) -> None: """Set up the Nanoleaf button.""" entry_data: NanoleafEntryData = hass.data[DOMAIN][entry.entry_id] - async_add_entities( - [NanoleafIdentifyButton(entry_data.device, entry_data.coordinator)] - ) + async_add_entities([NanoleafIdentifyButton(entry_data.coordinator)]) class NanoleafIdentifyButton(NanoleafEntity, ButtonEntity): @@ -30,12 +25,10 @@ class NanoleafIdentifyButton(NanoleafEntity, ButtonEntity): _attr_entity_category = EntityCategory.CONFIG _attr_device_class = ButtonDeviceClass.IDENTIFY - def __init__( - self, nanoleaf: Nanoleaf, coordinator: DataUpdateCoordinator[None] - ) -> None: + def __init__(self, coordinator: NanoleafCoordinator) -> None: """Initialize the Nanoleaf button.""" - super().__init__(nanoleaf, coordinator) - self._attr_unique_id = f"{nanoleaf.serial_no}_identify" + super().__init__(coordinator) + self._attr_unique_id = f"{self._nanoleaf.serial_no}_identify" async def async_press(self) -> None: """Identify the Nanoleaf.""" diff --git a/homeassistant/components/nanoleaf/coordinator.py b/homeassistant/components/nanoleaf/coordinator.py new file mode 100644 index 00000000000..e080afc492e --- /dev/null +++ b/homeassistant/components/nanoleaf/coordinator.py @@ -0,0 +1,31 @@ +"""Define the Nanoleaf data coordinator.""" + +from datetime import timedelta +import logging + +from aionanoleaf import InvalidToken, Nanoleaf, Unavailable + +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +_LOGGER = logging.getLogger(__name__) + + +class NanoleafCoordinator(DataUpdateCoordinator[None]): + """Class to manage fetching Nanoleaf data.""" + + def __init__(self, hass: HomeAssistant, nanoleaf: Nanoleaf) -> None: + """Initialize the Nanoleaf data coordinator.""" + super().__init__( + hass, _LOGGER, name="Nanoleaf", update_interval=timedelta(minutes=1) + ) + self.nanoleaf = nanoleaf + + async def _async_update_data(self) -> None: + try: + await self.nanoleaf.get_info() + except Unavailable as err: + raise UpdateFailed from err + except InvalidToken as err: + raise ConfigEntryAuthFailed from err diff --git a/homeassistant/components/nanoleaf/entity.py b/homeassistant/components/nanoleaf/entity.py index 73d635a46a1..ffe4a098022 100644 --- a/homeassistant/components/nanoleaf/entity.py +++ b/homeassistant/components/nanoleaf/entity.py @@ -1,27 +1,21 @@ """Base class for Nanoleaf entity.""" -from aionanoleaf import Nanoleaf - from homeassistant.helpers.device_registry import DeviceInfo -from homeassistant.helpers.update_coordinator import ( - CoordinatorEntity, - DataUpdateCoordinator, -) +from homeassistant.helpers.update_coordinator import CoordinatorEntity +from . import NanoleafCoordinator from .const import DOMAIN -class NanoleafEntity(CoordinatorEntity[DataUpdateCoordinator[None]]): +class NanoleafEntity(CoordinatorEntity[NanoleafCoordinator]): """Representation of a Nanoleaf entity.""" _attr_has_entity_name = True - def __init__( - self, nanoleaf: Nanoleaf, coordinator: DataUpdateCoordinator[None] - ) -> None: + def __init__(self, coordinator: NanoleafCoordinator) -> None: """Initialize a Nanoleaf entity.""" super().__init__(coordinator) - self._nanoleaf = nanoleaf + self._nanoleaf = nanoleaf = coordinator.nanoleaf self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, nanoleaf.serial_no)}, manufacturer=nanoleaf.manufacturer, diff --git a/homeassistant/components/nanoleaf/light.py b/homeassistant/components/nanoleaf/light.py index b80048307bb..a02cb30754b 100644 --- a/homeassistant/components/nanoleaf/light.py +++ b/homeassistant/components/nanoleaf/light.py @@ -5,8 +5,6 @@ from __future__ import annotations import math from typing import Any -from aionanoleaf import Nanoleaf - from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, @@ -20,13 +18,12 @@ from homeassistant.components.light import ( from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from homeassistant.util.color import ( color_temperature_kelvin_to_mired as kelvin_to_mired, color_temperature_mired_to_kelvin as mired_to_kelvin, ) -from . import NanoleafEntryData +from . import NanoleafCoordinator, NanoleafEntryData from .const import DOMAIN from .entity import NanoleafEntity @@ -39,7 +36,7 @@ async def async_setup_entry( ) -> None: """Set up the Nanoleaf light.""" entry_data: NanoleafEntryData = hass.data[DOMAIN][entry.entry_id] - async_add_entities([NanoleafLight(entry_data.device, entry_data.coordinator)]) + async_add_entities([NanoleafLight(entry_data.coordinator)]) class NanoleafLight(NanoleafEntity, LightEntity): @@ -50,14 +47,14 @@ class NanoleafLight(NanoleafEntity, LightEntity): _attr_name = None _attr_translation_key = "light" - def __init__( - self, nanoleaf: Nanoleaf, coordinator: DataUpdateCoordinator[None] - ) -> None: + def __init__(self, coordinator: NanoleafCoordinator) -> None: """Initialize the Nanoleaf light.""" - super().__init__(nanoleaf, coordinator) - self._attr_unique_id = nanoleaf.serial_no - self._attr_min_mireds = math.ceil(1000000 / nanoleaf.color_temperature_max) - self._attr_max_mireds = kelvin_to_mired(nanoleaf.color_temperature_min) + super().__init__(coordinator) + self._attr_unique_id = self._nanoleaf.serial_no + self._attr_min_mireds = math.ceil( + 1000000 / self._nanoleaf.color_temperature_max + ) + self._attr_max_mireds = kelvin_to_mired(self._nanoleaf.color_temperature_min) @property def brightness(self) -> int: From 66faeb28d7d3e7a9543bc2b01cf6d35f143e5593 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Tue, 18 Jun 2024 20:01:16 +0200 Subject: [PATCH 0794/1445] Fix late group platform registration (#119789) * Fix late group platform registration * use a callback instead * Run thread safe * Not working domain filter * Also update if a group has nested group's * Only update if the siingle state type key could change * Avoid redundant regisister hooks * Use set, add comment * Revert changes * Keep callback cleanup const * Cleanup after dependencies * Preimport and cleanup excluded domains * Revert test changes as we assume early set up now * Migrate alarm_control_panel * Migrate climate * Migrate cover * Migrate device_tracker * Migrate lock * Migrate media_player * Migrate person * Migrate plant * Migrate vacuum * Migrate water_heater * Remove water_heater group_pre_import * Use Platform enum if possible * Also use platform enum for excluded domains * Set registry to self._registry * move deregistering call back hook to async_added_to_hass * Add comment * Do no pass mutable reference to EXCLUDED_DOMAINS * Remove unneeded type hint --- .../components/air_quality/__init__.py | 1 - homeassistant/components/air_quality/group.py | 20 --- .../alarm_control_panel/__init__.py | 1 - .../components/alarm_control_panel/group.py | 43 ------ homeassistant/components/climate/__init__.py | 1 - homeassistant/components/climate/group.py | 33 ----- homeassistant/components/cover/__init__.py | 1 - homeassistant/components/cover/group.py | 22 --- .../components/device_tracker/__init__.py | 1 - .../components/device_tracker/group.py | 21 --- homeassistant/components/group/__init__.py | 6 +- homeassistant/components/group/entity.py | 13 +- homeassistant/components/group/manifest.json | 12 -- homeassistant/components/group/registry.py | 132 +++++++++++++++++- homeassistant/components/lock/__init__.py | 1 - homeassistant/components/lock/group.py | 39 ------ .../components/media_player/__init__.py | 1 - .../components/media_player/group.py | 37 ----- homeassistant/components/person/__init__.py | 1 - homeassistant/components/person/group.py | 21 --- homeassistant/components/plant/__init__.py | 1 - homeassistant/components/plant/group.py | 21 --- homeassistant/components/sensor/__init__.py | 1 - homeassistant/components/sensor/group.py | 20 --- homeassistant/components/vacuum/__init__.py | 1 - homeassistant/components/vacuum/group.py | 31 ---- .../components/water_heater/__init__.py | 1 - .../components/water_heater/group.py | 42 ------ homeassistant/components/weather/__init__.py | 1 - homeassistant/components/weather/group.py | 20 --- 30 files changed, 140 insertions(+), 406 deletions(-) delete mode 100644 homeassistant/components/air_quality/group.py delete mode 100644 homeassistant/components/alarm_control_panel/group.py delete mode 100644 homeassistant/components/climate/group.py delete mode 100644 homeassistant/components/cover/group.py delete mode 100644 homeassistant/components/device_tracker/group.py delete mode 100644 homeassistant/components/lock/group.py delete mode 100644 homeassistant/components/media_player/group.py delete mode 100644 homeassistant/components/person/group.py delete mode 100644 homeassistant/components/plant/group.py delete mode 100644 homeassistant/components/sensor/group.py delete mode 100644 homeassistant/components/vacuum/group.py delete mode 100644 homeassistant/components/water_heater/group.py delete mode 100644 homeassistant/components/weather/group.py diff --git a/homeassistant/components/air_quality/__init__.py b/homeassistant/components/air_quality/__init__.py index e33fbd34367..78f2616a74d 100644 --- a/homeassistant/components/air_quality/__init__.py +++ b/homeassistant/components/air_quality/__init__.py @@ -17,7 +17,6 @@ from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.typing import ConfigType, StateType -from . import group as group_pre_import # noqa: F401 from .const import DOMAIN _LOGGER: Final = logging.getLogger(__name__) diff --git a/homeassistant/components/air_quality/group.py b/homeassistant/components/air_quality/group.py deleted file mode 100644 index 8dc92ef6d07..00000000000 --- a/homeassistant/components/air_quality/group.py +++ /dev/null @@ -1,20 +0,0 @@ -"""Describe group states.""" - -from __future__ import annotations - -from typing import TYPE_CHECKING - -from homeassistant.core import HomeAssistant, callback - -if TYPE_CHECKING: - from homeassistant.components.group import GroupIntegrationRegistry - -from .const import DOMAIN - - -@callback -def async_describe_on_off_states( - hass: HomeAssistant, registry: GroupIntegrationRegistry -) -> None: - """Describe group on off states.""" - registry.exclude_domain(DOMAIN) diff --git a/homeassistant/components/alarm_control_panel/__init__.py b/homeassistant/components/alarm_control_panel/__init__.py index 48ea72c46d9..f33e168c031 100644 --- a/homeassistant/components/alarm_control_panel/__init__.py +++ b/homeassistant/components/alarm_control_panel/__init__.py @@ -34,7 +34,6 @@ from homeassistant.helpers.entity import Entity, EntityDescription from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.typing import ConfigType -from . import group as group_pre_import # noqa: F401 from .const import ( # noqa: F401 _DEPRECATED_FORMAT_NUMBER, _DEPRECATED_FORMAT_TEXT, diff --git a/homeassistant/components/alarm_control_panel/group.py b/homeassistant/components/alarm_control_panel/group.py deleted file mode 100644 index 5504294c4b9..00000000000 --- a/homeassistant/components/alarm_control_panel/group.py +++ /dev/null @@ -1,43 +0,0 @@ -"""Describe group states.""" - -from __future__ import annotations - -from typing import TYPE_CHECKING - -from homeassistant.const import ( - STATE_ALARM_ARMED_AWAY, - STATE_ALARM_ARMED_CUSTOM_BYPASS, - STATE_ALARM_ARMED_HOME, - STATE_ALARM_ARMED_NIGHT, - STATE_ALARM_ARMED_VACATION, - STATE_ALARM_TRIGGERED, - STATE_OFF, - STATE_ON, -) -from homeassistant.core import HomeAssistant, callback - -from .const import DOMAIN - -if TYPE_CHECKING: - from homeassistant.components.group import GroupIntegrationRegistry - - -@callback -def async_describe_on_off_states( - hass: HomeAssistant, registry: GroupIntegrationRegistry -) -> None: - """Describe group on off states.""" - registry.on_off_states( - DOMAIN, - { - STATE_ON, - STATE_ALARM_ARMED_AWAY, - STATE_ALARM_ARMED_CUSTOM_BYPASS, - STATE_ALARM_ARMED_HOME, - STATE_ALARM_ARMED_NIGHT, - STATE_ALARM_ARMED_VACATION, - STATE_ALARM_TRIGGERED, - }, - STATE_ON, - STATE_OFF, - ) diff --git a/homeassistant/components/climate/__init__.py b/homeassistant/components/climate/__init__.py index 9084a138350..ac6297dc5b6 100644 --- a/homeassistant/components/climate/__init__.py +++ b/homeassistant/components/climate/__init__.py @@ -45,7 +45,6 @@ from homeassistant.helpers.typing import ConfigType from homeassistant.loader import async_get_issue_tracker, async_suggest_report_issue from homeassistant.util.unit_conversion import TemperatureConverter -from . import group as group_pre_import # noqa: F401 from .const import ( # noqa: F401 _DEPRECATED_HVAC_MODE_AUTO, _DEPRECATED_HVAC_MODE_COOL, diff --git a/homeassistant/components/climate/group.py b/homeassistant/components/climate/group.py deleted file mode 100644 index 927bd2768f2..00000000000 --- a/homeassistant/components/climate/group.py +++ /dev/null @@ -1,33 +0,0 @@ -"""Describe group states.""" - -from __future__ import annotations - -from typing import TYPE_CHECKING - -from homeassistant.const import STATE_OFF, STATE_ON -from homeassistant.core import HomeAssistant, callback - -from .const import DOMAIN, HVACMode - -if TYPE_CHECKING: - from homeassistant.components.group import GroupIntegrationRegistry - - -@callback -def async_describe_on_off_states( - hass: HomeAssistant, registry: GroupIntegrationRegistry -) -> None: - """Describe group on off states.""" - registry.on_off_states( - DOMAIN, - { - STATE_ON, - HVACMode.HEAT, - HVACMode.COOL, - HVACMode.HEAT_COOL, - HVACMode.AUTO, - HVACMode.FAN_ONLY, - }, - STATE_ON, - STATE_OFF, - ) diff --git a/homeassistant/components/cover/__init__.py b/homeassistant/components/cover/__init__.py index 9e3184b4822..852c5fd9cae 100644 --- a/homeassistant/components/cover/__init__.py +++ b/homeassistant/components/cover/__init__.py @@ -45,7 +45,6 @@ from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass -from . import group as group_pre_import # noqa: F401 from .const import DOMAIN _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/cover/group.py b/homeassistant/components/cover/group.py deleted file mode 100644 index 8d7b860bc94..00000000000 --- a/homeassistant/components/cover/group.py +++ /dev/null @@ -1,22 +0,0 @@ -"""Describe group states.""" - -from __future__ import annotations - -from typing import TYPE_CHECKING - -from homeassistant.const import STATE_CLOSED, STATE_OPEN -from homeassistant.core import HomeAssistant, callback - -from .const import DOMAIN - -if TYPE_CHECKING: - from homeassistant.components.group import GroupIntegrationRegistry - - -@callback -def async_describe_on_off_states( - hass: HomeAssistant, registry: GroupIntegrationRegistry -) -> None: - """Describe group on off states.""" - # On means open, Off means closed - registry.on_off_states(DOMAIN, {STATE_OPEN}, STATE_OPEN, STATE_CLOSED) diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py index ca78b1cbdc5..92c961eb148 100644 --- a/homeassistant/components/device_tracker/__init__.py +++ b/homeassistant/components/device_tracker/__init__.py @@ -14,7 +14,6 @@ from homeassistant.helpers.deprecation import ( from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass -from . import group as group_pre_import # noqa: F401 from .config_entry import ( # noqa: F401 ScannerEntity, TrackerEntity, diff --git a/homeassistant/components/device_tracker/group.py b/homeassistant/components/device_tracker/group.py deleted file mode 100644 index 8143251e7fa..00000000000 --- a/homeassistant/components/device_tracker/group.py +++ /dev/null @@ -1,21 +0,0 @@ -"""Describe group states.""" - -from __future__ import annotations - -from typing import TYPE_CHECKING - -from homeassistant.const import STATE_HOME, STATE_NOT_HOME -from homeassistant.core import HomeAssistant, callback - -from .const import DOMAIN - -if TYPE_CHECKING: - from homeassistant.components.group import GroupIntegrationRegistry - - -@callback -def async_describe_on_off_states( - hass: HomeAssistant, registry: GroupIntegrationRegistry -) -> None: - """Describe group on off states.""" - registry.on_off_states(DOMAIN, {STATE_HOME}, STATE_HOME, STATE_NOT_HOME) diff --git a/homeassistant/components/group/__init__.py b/homeassistant/components/group/__init__.py index a0f8d2b9a39..f89bf67861d 100644 --- a/homeassistant/components/group/__init__.py +++ b/homeassistant/components/group/__init__.py @@ -265,16 +265,16 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: if ATTR_ADD_ENTITIES in service.data: delta = service.data[ATTR_ADD_ENTITIES] entity_ids = set(group.tracking) | set(delta) - await group.async_update_tracked_entity_ids(entity_ids) + group.async_update_tracked_entity_ids(entity_ids) if ATTR_REMOVE_ENTITIES in service.data: delta = service.data[ATTR_REMOVE_ENTITIES] entity_ids = set(group.tracking) - set(delta) - await group.async_update_tracked_entity_ids(entity_ids) + group.async_update_tracked_entity_ids(entity_ids) if ATTR_ENTITIES in service.data: entity_ids = service.data[ATTR_ENTITIES] - await group.async_update_tracked_entity_ids(entity_ids) + group.async_update_tracked_entity_ids(entity_ids) if ATTR_NAME in service.data: group.set_name(service.data[ATTR_NAME]) diff --git a/homeassistant/components/group/entity.py b/homeassistant/components/group/entity.py index 785895ff11a..1b2db35531f 100644 --- a/homeassistant/components/group/entity.py +++ b/homeassistant/components/group/entity.py @@ -133,6 +133,7 @@ class Group(Entity): tracking: tuple[str, ...] trackable: tuple[str, ...] single_state_type_key: SingleStateType | None + _registry: GroupIntegrationRegistry def __init__( self, @@ -261,7 +262,8 @@ class Group(Entity): """Test if any member has an assumed state.""" return self._assumed_state - async def async_update_tracked_entity_ids( + @callback + def async_update_tracked_entity_ids( self, entity_ids: Collection[str] | None ) -> None: """Update the member entity IDs. @@ -284,7 +286,7 @@ class Group(Entity): self.single_state_type_key = None return - registry: GroupIntegrationRegistry = self.hass.data[REG_KEY] + registry = self._registry excluded_domains = registry.exclude_domains tracking: list[str] = [] @@ -313,7 +315,6 @@ class Group(Entity): registry.state_group_mapping[self.entity_id] = self.single_state_type_key else: self.single_state_type_key = None - self.async_on_remove(self._async_deregister) self.trackable = tuple(trackable) self.tracking = tuple(tracking) @@ -321,7 +322,7 @@ class Group(Entity): @callback def _async_deregister(self) -> None: """Deregister group entity from the registry.""" - registry: GroupIntegrationRegistry = self.hass.data[REG_KEY] + registry = self._registry if self.entity_id in registry.state_group_mapping: registry.state_group_mapping.pop(self.entity_id) @@ -363,8 +364,10 @@ class Group(Entity): async def async_added_to_hass(self) -> None: """Handle addition to Home Assistant.""" + self._registry = self.hass.data[REG_KEY] self._set_tracked(self._entity_ids) self.async_on_remove(start.async_at_start(self.hass, self._async_start)) + self.async_on_remove(self._async_deregister) async def async_will_remove_from_hass(self) -> None: """Handle removal from Home Assistant.""" @@ -405,7 +408,7 @@ class Group(Entity): entity_id = new_state.entity_id domain = new_state.domain state = new_state.state - registry: GroupIntegrationRegistry = self.hass.data[REG_KEY] + registry = self._registry self._assumed[entity_id] = bool(new_state.attributes.get(ATTR_ASSUMED_STATE)) if domain not in registry.on_states_by_domain: diff --git a/homeassistant/components/group/manifest.json b/homeassistant/components/group/manifest.json index a2045f370b1..7ead19414af 100644 --- a/homeassistant/components/group/manifest.json +++ b/homeassistant/components/group/manifest.json @@ -1,18 +1,6 @@ { "domain": "group", "name": "Group", - "after_dependencies": [ - "alarm_control_panel", - "climate", - "cover", - "device_tracker", - "lock", - "media_player", - "person", - "plant", - "vacuum", - "water_heater" - ], "codeowners": ["@home-assistant/core"], "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/group", diff --git a/homeassistant/components/group/registry.py b/homeassistant/components/group/registry.py index 4ce89a4c725..c17a19e24fd 100644 --- a/homeassistant/components/group/registry.py +++ b/homeassistant/components/group/registry.py @@ -8,7 +8,41 @@ from __future__ import annotations from dataclasses import dataclass from typing import Protocol -from homeassistant.const import STATE_OFF, STATE_ON +from homeassistant.components.climate import HVACMode +from homeassistant.components.vacuum import STATE_CLEANING, STATE_ERROR, STATE_RETURNING +from homeassistant.components.water_heater import ( + STATE_ECO, + STATE_ELECTRIC, + STATE_GAS, + STATE_HEAT_PUMP, + STATE_HIGH_DEMAND, + STATE_PERFORMANCE, +) +from homeassistant.const import ( + STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_CUSTOM_BYPASS, + STATE_ALARM_ARMED_HOME, + STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_ARMED_VACATION, + STATE_ALARM_TRIGGERED, + STATE_CLOSED, + STATE_HOME, + STATE_IDLE, + STATE_LOCKED, + STATE_LOCKING, + STATE_NOT_HOME, + STATE_OFF, + STATE_OK, + STATE_ON, + STATE_OPEN, + STATE_OPENING, + STATE_PAUSED, + STATE_PLAYING, + STATE_PROBLEM, + STATE_UNLOCKED, + STATE_UNLOCKING, + Platform, +) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.integration_platform import ( async_process_integration_platforms, @@ -16,6 +50,92 @@ from homeassistant.helpers.integration_platform import ( from .const import DOMAIN, REG_KEY +# EXCLUDED_DOMAINS and ON_OFF_STATES are considered immutable +# in respect that new platforms should not be added. +# The the only maintenance allowed here is +# if existing platforms add new ON or OFF states. +EXCLUDED_DOMAINS: set[Platform | str] = { + Platform.AIR_QUALITY, + Platform.SENSOR, + Platform.WEATHER, +} + +ON_OFF_STATES: dict[Platform | str, tuple[set[str], str, str]] = { + Platform.ALARM_CONTROL_PANEL: ( + { + STATE_ON, + STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_CUSTOM_BYPASS, + STATE_ALARM_ARMED_HOME, + STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_ARMED_VACATION, + STATE_ALARM_TRIGGERED, + }, + STATE_ON, + STATE_OFF, + ), + Platform.CLIMATE: ( + { + STATE_ON, + HVACMode.HEAT, + HVACMode.COOL, + HVACMode.HEAT_COOL, + HVACMode.AUTO, + HVACMode.FAN_ONLY, + }, + STATE_ON, + STATE_OFF, + ), + Platform.COVER: ({STATE_OPEN}, STATE_OPEN, STATE_CLOSED), + Platform.DEVICE_TRACKER: ({STATE_HOME}, STATE_HOME, STATE_NOT_HOME), + Platform.LOCK: ( + { + STATE_LOCKING, + STATE_OPEN, + STATE_OPENING, + STATE_UNLOCKED, + STATE_UNLOCKING, + }, + STATE_UNLOCKED, + STATE_LOCKED, + ), + Platform.MEDIA_PLAYER: ( + { + STATE_ON, + STATE_PAUSED, + STATE_PLAYING, + STATE_IDLE, + }, + STATE_ON, + STATE_OFF, + ), + "person": ({STATE_HOME}, STATE_HOME, STATE_NOT_HOME), + "plant": ({STATE_PROBLEM}, STATE_PROBLEM, STATE_OK), + Platform.VACUUM: ( + { + STATE_ON, + STATE_CLEANING, + STATE_RETURNING, + STATE_ERROR, + }, + STATE_ON, + STATE_OFF, + ), + Platform.WATER_HEATER: ( + { + STATE_ON, + STATE_ECO, + STATE_ELECTRIC, + STATE_PERFORMANCE, + STATE_HIGH_DEMAND, + STATE_HEAT_PUMP, + STATE_GAS, + }, + STATE_ON, + STATE_OFF, + ), +} + async def async_setup(hass: HomeAssistant) -> None: """Set up the Group integration registry of integration platforms.""" @@ -61,8 +181,10 @@ class GroupIntegrationRegistry: self.on_off_mapping: dict[str, str] = {STATE_ON: STATE_OFF} self.off_on_mapping: dict[str, str] = {STATE_OFF: STATE_ON} self.on_states_by_domain: dict[str, set[str]] = {} - self.exclude_domains: set[str] = set() + self.exclude_domains = EXCLUDED_DOMAINS.copy() self.state_group_mapping: dict[str, SingleStateType] = {} + for domain, on_off_states in ON_OFF_STATES.items(): + self.on_off_states(domain, *on_off_states) @callback def exclude_domain(self, domain: str) -> None: @@ -71,7 +193,11 @@ class GroupIntegrationRegistry: @callback def on_off_states( - self, domain: str, on_states: set[str], default_on_state: str, off_state: str + self, + domain: Platform | str, + on_states: set[str], + default_on_state: str, + off_state: str, ) -> None: """Register on and off states for the current domain. diff --git a/homeassistant/components/lock/__init__.py b/homeassistant/components/lock/__init__.py index 55f48fd8d22..21533353ac7 100644 --- a/homeassistant/components/lock/__init__.py +++ b/homeassistant/components/lock/__init__.py @@ -45,7 +45,6 @@ from homeassistant.helpers.entity import Entity, EntityDescription from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.typing import ConfigType, StateType -from . import group as group_pre_import # noqa: F401 from .const import DOMAIN _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/lock/group.py b/homeassistant/components/lock/group.py deleted file mode 100644 index ad5ee15c2bd..00000000000 --- a/homeassistant/components/lock/group.py +++ /dev/null @@ -1,39 +0,0 @@ -"""Describe group states.""" - -from __future__ import annotations - -from typing import TYPE_CHECKING - -from homeassistant.const import ( - STATE_LOCKED, - STATE_LOCKING, - STATE_OPEN, - STATE_OPENING, - STATE_UNLOCKED, - STATE_UNLOCKING, -) -from homeassistant.core import HomeAssistant, callback - -from .const import DOMAIN - -if TYPE_CHECKING: - from homeassistant.components.group import GroupIntegrationRegistry - - -@callback -def async_describe_on_off_states( - hass: HomeAssistant, registry: GroupIntegrationRegistry -) -> None: - """Describe group on off states.""" - registry.on_off_states( - DOMAIN, - { - STATE_LOCKING, - STATE_OPEN, - STATE_OPENING, - STATE_UNLOCKED, - STATE_UNLOCKING, - }, - STATE_UNLOCKED, - STATE_LOCKED, - ) diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index b90de95a489..3679b5f89c5 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -64,7 +64,6 @@ from homeassistant.helpers.network import get_url from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass -from . import group as group_pre_import # noqa: F401 from .browse_media import BrowseMedia, async_process_play_media_url # noqa: F401 from .const import ( # noqa: F401 ATTR_APP_ID, diff --git a/homeassistant/components/media_player/group.py b/homeassistant/components/media_player/group.py deleted file mode 100644 index 1ac5f6aa594..00000000000 --- a/homeassistant/components/media_player/group.py +++ /dev/null @@ -1,37 +0,0 @@ -"""Describe group states.""" - -from __future__ import annotations - -from typing import TYPE_CHECKING - -from homeassistant.const import ( - STATE_IDLE, - STATE_OFF, - STATE_ON, - STATE_PAUSED, - STATE_PLAYING, -) -from homeassistant.core import HomeAssistant, callback - -from .const import DOMAIN - -if TYPE_CHECKING: - from homeassistant.components.group import GroupIntegrationRegistry - - -@callback -def async_describe_on_off_states( - hass: HomeAssistant, registry: GroupIntegrationRegistry -) -> None: - """Describe group on off states.""" - registry.on_off_states( - DOMAIN, - { - STATE_ON, - STATE_PAUSED, - STATE_PLAYING, - STATE_IDLE, - }, - STATE_ON, - STATE_OFF, - ) diff --git a/homeassistant/components/person/__init__.py b/homeassistant/components/person/__init__.py index 55c37f1c36c..0779140a091 100644 --- a/homeassistant/components/person/__init__.py +++ b/homeassistant/components/person/__init__.py @@ -53,7 +53,6 @@ from homeassistant.helpers.storage import Store from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass -from . import group as group_pre_import # noqa: F401 from .const import DOMAIN _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/person/group.py b/homeassistant/components/person/group.py deleted file mode 100644 index 8143251e7fa..00000000000 --- a/homeassistant/components/person/group.py +++ /dev/null @@ -1,21 +0,0 @@ -"""Describe group states.""" - -from __future__ import annotations - -from typing import TYPE_CHECKING - -from homeassistant.const import STATE_HOME, STATE_NOT_HOME -from homeassistant.core import HomeAssistant, callback - -from .const import DOMAIN - -if TYPE_CHECKING: - from homeassistant.components.group import GroupIntegrationRegistry - - -@callback -def async_describe_on_off_states( - hass: HomeAssistant, registry: GroupIntegrationRegistry -) -> None: - """Describe group on off states.""" - registry.on_off_states(DOMAIN, {STATE_HOME}, STATE_HOME, STATE_NOT_HOME) diff --git a/homeassistant/components/plant/__init__.py b/homeassistant/components/plant/__init__.py index 4f35f9eb281..afce1207add 100644 --- a/homeassistant/components/plant/__init__.py +++ b/homeassistant/components/plant/__init__.py @@ -35,7 +35,6 @@ from homeassistant.helpers.event import async_track_state_change_event from homeassistant.helpers.typing import ConfigType from homeassistant.util import dt as dt_util -from . import group as group_pre_import # noqa: F401 from .const import ( ATTR_DICT_OF_UNITS_OF_MEASUREMENT, ATTR_MAX_BRIGHTNESS_HISTORY, diff --git a/homeassistant/components/plant/group.py b/homeassistant/components/plant/group.py deleted file mode 100644 index 93944659e03..00000000000 --- a/homeassistant/components/plant/group.py +++ /dev/null @@ -1,21 +0,0 @@ -"""Describe group states.""" - -from __future__ import annotations - -from typing import TYPE_CHECKING - -from homeassistant.const import STATE_OK, STATE_PROBLEM -from homeassistant.core import HomeAssistant, callback - -from .const import DOMAIN - -if TYPE_CHECKING: - from homeassistant.components.group import GroupIntegrationRegistry - - -@callback -def async_describe_on_off_states( - hass: HomeAssistant, registry: GroupIntegrationRegistry -) -> None: - """Describe group on off states.""" - registry.on_off_states(DOMAIN, {STATE_PROBLEM}, STATE_PROBLEM, STATE_OK) diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index 7e7eaf8aef2..689be1100f6 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -68,7 +68,6 @@ from homeassistant.helpers.typing import UNDEFINED, ConfigType, StateType, Undef from homeassistant.util import dt as dt_util from homeassistant.util.enum import try_parse_enum -from . import group as group_pre_import # noqa: F401 from .const import ( # noqa: F401 _DEPRECATED_STATE_CLASS_MEASUREMENT, _DEPRECATED_STATE_CLASS_TOTAL, diff --git a/homeassistant/components/sensor/group.py b/homeassistant/components/sensor/group.py deleted file mode 100644 index 8dc92ef6d07..00000000000 --- a/homeassistant/components/sensor/group.py +++ /dev/null @@ -1,20 +0,0 @@ -"""Describe group states.""" - -from __future__ import annotations - -from typing import TYPE_CHECKING - -from homeassistant.core import HomeAssistant, callback - -if TYPE_CHECKING: - from homeassistant.components.group import GroupIntegrationRegistry - -from .const import DOMAIN - - -@callback -def async_describe_on_off_states( - hass: HomeAssistant, registry: GroupIntegrationRegistry -) -> None: - """Describe group on off states.""" - registry.exclude_domain(DOMAIN) diff --git a/homeassistant/components/vacuum/__init__.py b/homeassistant/components/vacuum/__init__.py index b50068de149..f68f9a4f082 100644 --- a/homeassistant/components/vacuum/__init__.py +++ b/homeassistant/components/vacuum/__init__.py @@ -34,7 +34,6 @@ from homeassistant.helpers.icon import icon_for_battery_level from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass -from . import group as group_pre_import # noqa: F401 from .const import DOMAIN, STATE_CLEANING, STATE_DOCKED, STATE_ERROR, STATE_RETURNING _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/vacuum/group.py b/homeassistant/components/vacuum/group.py deleted file mode 100644 index 43d77995d1c..00000000000 --- a/homeassistant/components/vacuum/group.py +++ /dev/null @@ -1,31 +0,0 @@ -"""Describe group states.""" - -from __future__ import annotations - -from typing import TYPE_CHECKING - -from homeassistant.const import STATE_OFF, STATE_ON -from homeassistant.core import HomeAssistant, callback - -if TYPE_CHECKING: - from homeassistant.components.group import GroupIntegrationRegistry - -from .const import DOMAIN, STATE_CLEANING, STATE_ERROR, STATE_RETURNING - - -@callback -def async_describe_on_off_states( - hass: HomeAssistant, registry: GroupIntegrationRegistry -) -> None: - """Describe group on off states.""" - registry.on_off_states( - DOMAIN, - { - STATE_ON, - STATE_CLEANING, - STATE_RETURNING, - STATE_ERROR, - }, - STATE_ON, - STATE_OFF, - ) diff --git a/homeassistant/components/water_heater/__init__.py b/homeassistant/components/water_heater/__init__.py index d6871947b77..1623b391e53 100644 --- a/homeassistant/components/water_heater/__init__.py +++ b/homeassistant/components/water_heater/__init__.py @@ -42,7 +42,6 @@ from homeassistant.helpers.temperature import display_temp as show_temp from homeassistant.helpers.typing import ConfigType from homeassistant.util.unit_conversion import TemperatureConverter -from . import group as group_pre_import # noqa: F401 from .const import DOMAIN DEFAULT_MIN_TEMP = 110 diff --git a/homeassistant/components/water_heater/group.py b/homeassistant/components/water_heater/group.py deleted file mode 100644 index c4e415462e4..00000000000 --- a/homeassistant/components/water_heater/group.py +++ /dev/null @@ -1,42 +0,0 @@ -"""Describe group states.""" - -from __future__ import annotations - -from typing import TYPE_CHECKING - -from homeassistant.const import STATE_OFF, STATE_ON -from homeassistant.core import HomeAssistant, callback - -if TYPE_CHECKING: - from homeassistant.components.group import GroupIntegrationRegistry - -from .const import ( - DOMAIN, - STATE_ECO, - STATE_ELECTRIC, - STATE_GAS, - STATE_HEAT_PUMP, - STATE_HIGH_DEMAND, - STATE_PERFORMANCE, -) - - -@callback -def async_describe_on_off_states( - hass: HomeAssistant, registry: GroupIntegrationRegistry -) -> None: - """Describe group on off states.""" - registry.on_off_states( - DOMAIN, - { - STATE_ON, - STATE_ECO, - STATE_ELECTRIC, - STATE_PERFORMANCE, - STATE_HIGH_DEMAND, - STATE_HEAT_PUMP, - STATE_GAS, - }, - STATE_ON, - STATE_OFF, - ) diff --git a/homeassistant/components/weather/__init__.py b/homeassistant/components/weather/__init__.py index b73cbd97654..b3ce52510d2 100644 --- a/homeassistant/components/weather/__init__.py +++ b/homeassistant/components/weather/__init__.py @@ -47,7 +47,6 @@ from homeassistant.util.dt import utcnow from homeassistant.util.json import JsonValueType from homeassistant.util.unit_system import US_CUSTOMARY_SYSTEM -from . import group as group_pre_import # noqa: F401 from .const import ( ATTR_WEATHER_APPARENT_TEMPERATURE, ATTR_WEATHER_CLOUD_COVERAGE, diff --git a/homeassistant/components/weather/group.py b/homeassistant/components/weather/group.py deleted file mode 100644 index 8dc92ef6d07..00000000000 --- a/homeassistant/components/weather/group.py +++ /dev/null @@ -1,20 +0,0 @@ -"""Describe group states.""" - -from __future__ import annotations - -from typing import TYPE_CHECKING - -from homeassistant.core import HomeAssistant, callback - -if TYPE_CHECKING: - from homeassistant.components.group import GroupIntegrationRegistry - -from .const import DOMAIN - - -@callback -def async_describe_on_off_states( - hass: HomeAssistant, registry: GroupIntegrationRegistry -) -> None: - """Describe group on off states.""" - registry.exclude_domain(DOMAIN) From ec9f2f698c88752783bca5864a4033b09020def8 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 18 Jun 2024 20:11:10 +0200 Subject: [PATCH 0795/1445] Add type hints to MockGroup and MockUser in tests (#119897) --- tests/common.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/tests/common.py b/tests/common.py index 114e683fbfa..5050d67b0cb 100644 --- a/tests/common.py +++ b/tests/common.py @@ -693,7 +693,7 @@ def mock_device_registry( class MockGroup(auth_models.Group): """Mock a group in Home Assistant.""" - def __init__(self, id=None, name="Mock Group"): + def __init__(self, id: str | None = None, name: str | None = "Mock Group") -> None: """Mock a group.""" kwargs = {"name": name, "policy": system_policies.ADMIN_POLICY} if id is not None: @@ -701,11 +701,11 @@ class MockGroup(auth_models.Group): super().__init__(**kwargs) - def add_to_hass(self, hass): + def add_to_hass(self, hass: HomeAssistant) -> MockGroup: """Test helper to add entry to hass.""" return self.add_to_auth_manager(hass.auth) - def add_to_auth_manager(self, auth_mgr): + def add_to_auth_manager(self, auth_mgr: auth.AuthManager) -> MockGroup: """Test helper to add entry to hass.""" ensure_auth_manager_loaded(auth_mgr) auth_mgr._store._groups[self.id] = self @@ -717,13 +717,13 @@ class MockUser(auth_models.User): def __init__( self, - id=None, - is_owner=False, - is_active=True, - name="Mock User", - system_generated=False, - groups=None, - ): + id: str | None = None, + is_owner: bool = False, + is_active: bool = True, + name: str | None = "Mock User", + system_generated: bool = False, + groups: list[auth_models.Group] | None = None, + ) -> None: """Initialize mock user.""" kwargs = { "is_owner": is_owner, @@ -737,17 +737,17 @@ class MockUser(auth_models.User): kwargs["id"] = id super().__init__(**kwargs) - def add_to_hass(self, hass): + def add_to_hass(self, hass: HomeAssistant) -> MockUser: """Test helper to add entry to hass.""" return self.add_to_auth_manager(hass.auth) - def add_to_auth_manager(self, auth_mgr): + def add_to_auth_manager(self, auth_mgr: auth.AuthManager) -> MockUser: """Test helper to add entry to hass.""" ensure_auth_manager_loaded(auth_mgr) auth_mgr._store._users[self.id] = self return self - def mock_policy(self, policy): + def mock_policy(self, policy: auth_permissions.PolicyType) -> None: """Mock a policy for a user.""" self.permissions = auth_permissions.PolicyPermissions(policy, self.perm_lookup) @@ -771,7 +771,7 @@ async def register_auth_provider( @callback -def ensure_auth_manager_loaded(auth_mgr): +def ensure_auth_manager_loaded(auth_mgr: auth.AuthManager) -> None: """Ensure an auth manager is considered loaded.""" store = auth_mgr._store if store._users is None: From be4db90c916eadbc2b0ee13e0869baae8aa0076f Mon Sep 17 00:00:00 2001 From: MallocArray Date: Tue, 18 Jun 2024 13:31:33 -0500 Subject: [PATCH 0796/1445] Update airgradient names to NOx index and VOC index (#119152) * Update names to NOx index and VOC index * Fix snapshots * Fix snapshots --------- Co-authored-by: Joostlek --- .../components/airgradient/strings.json | 8 ++-- .../airgradient/snapshots/test_sensor.ambr | 48 +++++++++---------- 2 files changed, 28 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/airgradient/strings.json b/homeassistant/components/airgradient/strings.json index 20322eed33c..a0f6af08132 100644 --- a/homeassistant/components/airgradient/strings.json +++ b/homeassistant/components/airgradient/strings.json @@ -42,19 +42,19 @@ }, "sensor": { "total_volatile_organic_component_index": { - "name": "Total VOC index" + "name": "VOC index" }, "nitrogen_index": { - "name": "Nitrogen index" + "name": "NOx index" }, "pm003_count": { "name": "PM0.3" }, "raw_total_volatile_organic_component": { - "name": "Raw total VOC" + "name": "Raw VOC" }, "raw_nitrogen": { - "name": "Raw nitrogen" + "name": "Raw NOx" } } }, diff --git a/tests/components/airgradient/snapshots/test_sensor.ambr b/tests/components/airgradient/snapshots/test_sensor.ambr index 6f9297db0d7..b0e22e7a9af 100644 --- a/tests/components/airgradient/snapshots/test_sensor.ambr +++ b/tests/components/airgradient/snapshots/test_sensor.ambr @@ -101,7 +101,7 @@ 'state': '48.0', }) # --- -# name: test_all_entities[sensor.airgradient_nitrogen_index-entry] +# name: test_all_entities[sensor.airgradient_nox_index-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -115,7 +115,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.airgradient_nitrogen_index', + 'entity_id': 'sensor.airgradient_nox_index', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -127,7 +127,7 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Nitrogen index', + 'original_name': 'NOx index', 'platform': 'airgradient', 'previous_unique_id': None, 'supported_features': 0, @@ -136,14 +136,14 @@ 'unit_of_measurement': None, }) # --- -# name: test_all_entities[sensor.airgradient_nitrogen_index-state] +# name: test_all_entities[sensor.airgradient_nox_index-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Airgradient Nitrogen index', + 'friendly_name': 'Airgradient NOx index', 'state_class': , }), 'context': , - 'entity_id': 'sensor.airgradient_nitrogen_index', + 'entity_id': 'sensor.airgradient_nox_index', 'last_changed': , 'last_reported': , 'last_updated': , @@ -403,7 +403,7 @@ 'state': '34', }) # --- -# name: test_all_entities[sensor.airgradient_raw_nitrogen-entry] +# name: test_all_entities[sensor.airgradient_raw_nox-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -417,7 +417,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.airgradient_raw_nitrogen', + 'entity_id': 'sensor.airgradient_raw_nox', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -429,7 +429,7 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Raw nitrogen', + 'original_name': 'Raw NOx', 'platform': 'airgradient', 'previous_unique_id': None, 'supported_features': 0, @@ -438,22 +438,22 @@ 'unit_of_measurement': 'ticks', }) # --- -# name: test_all_entities[sensor.airgradient_raw_nitrogen-state] +# name: test_all_entities[sensor.airgradient_raw_nox-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Airgradient Raw nitrogen', + 'friendly_name': 'Airgradient Raw NOx', 'state_class': , 'unit_of_measurement': 'ticks', }), 'context': , - 'entity_id': 'sensor.airgradient_raw_nitrogen', + 'entity_id': 'sensor.airgradient_raw_nox', 'last_changed': , 'last_reported': , 'last_updated': , 'state': '16931', }) # --- -# name: test_all_entities[sensor.airgradient_raw_total_voc-entry] +# name: test_all_entities[sensor.airgradient_raw_voc-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -467,7 +467,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.airgradient_raw_total_voc', + 'entity_id': 'sensor.airgradient_raw_voc', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -479,7 +479,7 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Raw total VOC', + 'original_name': 'Raw VOC', 'platform': 'airgradient', 'previous_unique_id': None, 'supported_features': 0, @@ -488,15 +488,15 @@ 'unit_of_measurement': 'ticks', }) # --- -# name: test_all_entities[sensor.airgradient_raw_total_voc-state] +# name: test_all_entities[sensor.airgradient_raw_voc-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Airgradient Raw total VOC', + 'friendly_name': 'Airgradient Raw VOC', 'state_class': , 'unit_of_measurement': 'ticks', }), 'context': , - 'entity_id': 'sensor.airgradient_raw_total_voc', + 'entity_id': 'sensor.airgradient_raw_voc', 'last_changed': , 'last_reported': , 'last_updated': , @@ -605,7 +605,7 @@ 'state': '27.96', }) # --- -# name: test_all_entities[sensor.airgradient_total_voc_index-entry] +# name: test_all_entities[sensor.airgradient_voc_index-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -619,7 +619,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.airgradient_total_voc_index', + 'entity_id': 'sensor.airgradient_voc_index', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -631,7 +631,7 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Total VOC index', + 'original_name': 'VOC index', 'platform': 'airgradient', 'previous_unique_id': None, 'supported_features': 0, @@ -640,14 +640,14 @@ 'unit_of_measurement': None, }) # --- -# name: test_all_entities[sensor.airgradient_total_voc_index-state] +# name: test_all_entities[sensor.airgradient_voc_index-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Airgradient Total VOC index', + 'friendly_name': 'Airgradient VOC index', 'state_class': , }), 'context': , - 'entity_id': 'sensor.airgradient_total_voc_index', + 'entity_id': 'sensor.airgradient_voc_index', 'last_changed': , 'last_reported': , 'last_updated': , From 484a24512c733362875614e22e56b87172b12692 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 18 Jun 2024 20:51:54 +0200 Subject: [PATCH 0797/1445] Bump airgradient to 0.5.0 (#119911) --- homeassistant/components/airgradient/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/airgradient/manifest.json b/homeassistant/components/airgradient/manifest.json index b9a1e2da54f..d3e5fed74ab 100644 --- a/homeassistant/components/airgradient/manifest.json +++ b/homeassistant/components/airgradient/manifest.json @@ -6,6 +6,6 @@ "documentation": "https://www.home-assistant.io/integrations/airgradient", "integration_type": "device", "iot_class": "local_polling", - "requirements": ["airgradient==0.4.3"], + "requirements": ["airgradient==0.5.0"], "zeroconf": ["_airgradient._tcp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index ca1f8fdfd57..598bdabe0da 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -410,7 +410,7 @@ aiowithings==3.0.1 aioymaps==1.2.2 # homeassistant.components.airgradient -airgradient==0.4.3 +airgradient==0.5.0 # homeassistant.components.airly airly==1.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 669f29a5f4b..90476966221 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -383,7 +383,7 @@ aiowithings==3.0.1 aioymaps==1.2.2 # homeassistant.components.airgradient -airgradient==0.4.3 +airgradient==0.5.0 # homeassistant.components.airly airly==1.1.0 From 0a781b8fa26a97863a0c8519ba407302cfe32ca3 Mon Sep 17 00:00:00 2001 From: Thomas55555 <59625598+Thomas55555@users.noreply.github.com> Date: Tue, 18 Jun 2024 21:24:09 +0200 Subject: [PATCH 0798/1445] Add button platform to Husqvarna Automower (#119856) * Add button platform to Husqvarna Automower * test coverage * adapt to library changes * Address review --- .../husqvarna_automower/__init__.py | 1 + .../components/husqvarna_automower/button.py | 61 ++++++++++ .../husqvarna_automower/strings.json | 10 ++ .../snapshots/test_button.ambr | 47 ++++++++ .../husqvarna_automower/test_button.py | 112 ++++++++++++++++++ 5 files changed, 231 insertions(+) create mode 100644 homeassistant/components/husqvarna_automower/button.py create mode 100644 tests/components/husqvarna_automower/snapshots/test_button.ambr create mode 100644 tests/components/husqvarna_automower/test_button.py diff --git a/homeassistant/components/husqvarna_automower/__init__.py b/homeassistant/components/husqvarna_automower/__init__.py index e62badd7e7c..326a9a010ef 100644 --- a/homeassistant/components/husqvarna_automower/__init__.py +++ b/homeassistant/components/husqvarna_automower/__init__.py @@ -18,6 +18,7 @@ _LOGGER = logging.getLogger(__name__) PLATFORMS: list[Platform] = [ Platform.BINARY_SENSOR, + Platform.BUTTON, Platform.DEVICE_TRACKER, Platform.LAWN_MOWER, Platform.NUMBER, diff --git a/homeassistant/components/husqvarna_automower/button.py b/homeassistant/components/husqvarna_automower/button.py new file mode 100644 index 00000000000..60c05b92a31 --- /dev/null +++ b/homeassistant/components/husqvarna_automower/button.py @@ -0,0 +1,61 @@ +"""Creates a button entity for Husqvarna Automower integration.""" + +import logging + +from aioautomower.exceptions import ApiException + +from homeassistant.components.button import ButtonEntity +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import AutomowerConfigEntry +from .const import DOMAIN +from .coordinator import AutomowerDataUpdateCoordinator +from .entity import AutomowerControlEntity + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: AutomowerConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up button platform.""" + coordinator = entry.runtime_data + async_add_entities( + AutomowerButtonEntity(mower_id, coordinator) for mower_id in coordinator.data + ) + + +class AutomowerButtonEntity(AutomowerControlEntity, ButtonEntity): + """Defining the AutomowerButtonEntity.""" + + _attr_translation_key = "confirm_error" + _attr_entity_registry_enabled_default = False + + def __init__( + self, + mower_id: str, + coordinator: AutomowerDataUpdateCoordinator, + ) -> None: + """Set up button platform.""" + super().__init__(mower_id, coordinator) + self._attr_unique_id = f"{mower_id}_confirm_error" + + @property + def available(self) -> bool: + """Return True if the device and entity is available.""" + return super().available and self.mower_attributes.mower.is_error_confirmable + + async def async_press(self) -> None: + """Handle the button press.""" + try: + await self.coordinator.api.commands.error_confirm(self.mower_id) + except ApiException as exception: + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="command_send_failed", + translation_placeholders={"exception": str(exception)}, + ) from exception diff --git a/homeassistant/components/husqvarna_automower/strings.json b/homeassistant/components/husqvarna_automower/strings.json index c94a8d0f6d1..a403a56cc5e 100644 --- a/homeassistant/components/husqvarna_automower/strings.json +++ b/homeassistant/components/husqvarna_automower/strings.json @@ -42,6 +42,11 @@ "name": "Returning to dock" } }, + "button": { + "confirm_error": { + "name": "Confirm error" + } + }, "number": { "cutting_height": { "name": "Cutting height" @@ -259,5 +264,10 @@ "name": "Avoid {stay_out_zone}" } } + }, + "exceptions": { + "command_send_failed": { + "message": "Failed to send command: {exception}" + } } } diff --git a/tests/components/husqvarna_automower/snapshots/test_button.ambr b/tests/components/husqvarna_automower/snapshots/test_button.ambr new file mode 100644 index 00000000000..ab2cb427f1a --- /dev/null +++ b/tests/components/husqvarna_automower/snapshots/test_button.ambr @@ -0,0 +1,47 @@ +# serializer version: 1 +# name: test_button_snapshot[button.test_mower_1_confirm_error-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': None, + 'entity_id': 'button.test_mower_1_confirm_error', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Confirm error', + 'platform': 'husqvarna_automower', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'confirm_error', + 'unique_id': 'c7233734-b219-4287-a173-08e3643f89f0_confirm_error', + 'unit_of_measurement': None, + }) +# --- +# name: test_button_snapshot[button.test_mower_1_confirm_error-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Mower 1 Confirm error', + }), + 'context': , + 'entity_id': 'button.test_mower_1_confirm_error', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- diff --git a/tests/components/husqvarna_automower/test_button.py b/tests/components/husqvarna_automower/test_button.py new file mode 100644 index 00000000000..6cc465df74b --- /dev/null +++ b/tests/components/husqvarna_automower/test_button.py @@ -0,0 +1,112 @@ +"""Tests for button platform.""" + +import datetime +from unittest.mock import AsyncMock, patch + +from aioautomower.exceptions import ApiException +from aioautomower.utils import mower_list_to_dictionary_dataclass +from freezegun.api import FrozenDateTimeFactory +import pytest +from syrupy import SnapshotAssertion + +from homeassistant.components.button import SERVICE_PRESS +from homeassistant.components.husqvarna_automower.const import DOMAIN +from homeassistant.components.husqvarna_automower.coordinator import SCAN_INTERVAL +from homeassistant.const import ( + ATTR_ENTITY_ID, + STATE_UNAVAILABLE, + STATE_UNKNOWN, + Platform, +) +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import entity_registry as er + +from . import setup_integration +from .const import TEST_MOWER_ID + +from tests.common import ( + MockConfigEntry, + async_fire_time_changed, + load_json_value_fixture, + snapshot_platform, +) + + +@pytest.mark.freeze_time(datetime.datetime(2024, 2, 29, 11, tzinfo=datetime.UTC)) +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_button_states_and_commands( + hass: HomeAssistant, + mock_automower_client: AsyncMock, + mock_config_entry: MockConfigEntry, + freezer: FrozenDateTimeFactory, +) -> None: + """Test button commands.""" + entity_id = "button.test_mower_1_confirm_error" + await setup_integration(hass, mock_config_entry) + state = hass.states.get(entity_id) + assert state.name == "Test Mower 1 Confirm error" + assert state.state == STATE_UNAVAILABLE + + values = mower_list_to_dictionary_dataclass( + load_json_value_fixture("mower.json", DOMAIN) + ) + values[TEST_MOWER_ID].mower.is_error_confirmable = None + mock_automower_client.get_status.return_value = values + freezer.tick(SCAN_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state.state == STATE_UNAVAILABLE + + values[TEST_MOWER_ID].mower.is_error_confirmable = True + mock_automower_client.get_status.return_value = values + freezer.tick(SCAN_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state.state == STATE_UNKNOWN + + await hass.services.async_call( + domain="button", + service=SERVICE_PRESS, + target={ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + mocked_method = getattr(mock_automower_client.commands, "error_confirm") + mocked_method.assert_called_once_with(TEST_MOWER_ID) + await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state.state == "2024-02-29T11:16:00+00:00" + getattr(mock_automower_client.commands, "error_confirm").side_effect = ApiException( + "Test error" + ) + with pytest.raises( + HomeAssistantError, + match="Failed to send command: Test error", + ): + await hass.services.async_call( + domain="button", + service=SERVICE_PRESS, + target={ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_button_snapshot( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + mock_automower_client: AsyncMock, + mock_config_entry: MockConfigEntry, + snapshot: SnapshotAssertion, +) -> None: + """Snapshot tests of the button entities.""" + with patch( + "homeassistant.components.husqvarna_automower.PLATFORMS", + [Platform.BUTTON], + ): + await setup_integration(hass, mock_config_entry) + await snapshot_platform( + hass, entity_registry, snapshot, mock_config_entry.entry_id + ) From 9723b97f4bd42c850f5cb5fbf67ecdfc55769861 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Tue, 18 Jun 2024 22:05:11 +0200 Subject: [PATCH 0799/1445] Bump python-holidays to 0.51 (#119918) --- homeassistant/components/holiday/manifest.json | 2 +- homeassistant/components/workday/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/holiday/manifest.json b/homeassistant/components/holiday/manifest.json index c026c3e6363..cb67039f374 100644 --- a/homeassistant/components/holiday/manifest.json +++ b/homeassistant/components/holiday/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/holiday", "iot_class": "local_polling", - "requirements": ["holidays==0.50", "babel==2.15.0"] + "requirements": ["holidays==0.51", "babel==2.15.0"] } diff --git a/homeassistant/components/workday/manifest.json b/homeassistant/components/workday/manifest.json index 71c26a30e94..1148f46e2d1 100644 --- a/homeassistant/components/workday/manifest.json +++ b/homeassistant/components/workday/manifest.json @@ -7,5 +7,5 @@ "iot_class": "local_polling", "loggers": ["holidays"], "quality_scale": "internal", - "requirements": ["holidays==0.50"] + "requirements": ["holidays==0.51"] } diff --git a/requirements_all.txt b/requirements_all.txt index 598bdabe0da..c8569e26b56 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1090,7 +1090,7 @@ hole==0.8.0 # homeassistant.components.holiday # homeassistant.components.workday -holidays==0.50 +holidays==0.51 # homeassistant.components.frontend home-assistant-frontend==20240610.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 90476966221..c6eb1178748 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -892,7 +892,7 @@ hole==0.8.0 # homeassistant.components.holiday # homeassistant.components.workday -holidays==0.50 +holidays==0.51 # homeassistant.components.frontend home-assistant-frontend==20240610.1 From adcd0cc2a40d7662fde1bde3286d6a3de7e407cf Mon Sep 17 00:00:00 2001 From: HarvsG <11440490+HarvsG@users.noreply.github.com> Date: Tue, 18 Jun 2024 21:24:36 +0100 Subject: [PATCH 0800/1445] DNS IP custom ports for IPv4 (#113993) * squash DNS IP enable port * linting * fix config entries in tests. * fix more config entries * fix parameter order * Add defaults for legacy config entries * test legacy config are not broken * test driven migration * define versions for future proofing * remove defaults as should be covered by migrations in the future * adds config migration * spacing * Review: remove unnecessary statements Co-authored-by: G Johansson * Apply suggestions from code review Co-authored-by: G Johansson * make default ports the same * test migration from future error * linting * Small tweaks --------- Co-authored-by: G Johansson --- homeassistant/components/dnsip/__init__.py | 38 ++++++++- homeassistant/components/dnsip/config_flow.py | 47 ++++++++--- homeassistant/components/dnsip/const.py | 2 + homeassistant/components/dnsip/sensor.py | 12 ++- homeassistant/components/dnsip/strings.json | 10 ++- tests/components/dnsip/test_config_flow.py | 29 ++++++- tests/components/dnsip/test_init.py | 80 ++++++++++++++++++- tests/components/dnsip/test_sensor.py | 50 +++++++++++- 8 files changed, 246 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/dnsip/__init__.py b/homeassistant/components/dnsip/__init__.py index 78309b5f2bf..37e0f60849f 100644 --- a/homeassistant/components/dnsip/__init__.py +++ b/homeassistant/components/dnsip/__init__.py @@ -3,9 +3,10 @@ from __future__ import annotations from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant +from homeassistant.const import CONF_PORT +from homeassistant.core import _LOGGER, HomeAssistant -from .const import PLATFORMS +from .const import CONF_PORT_IPV6, DEFAULT_PORT, PLATFORMS async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: @@ -25,3 +26,36 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload dnsip config entry.""" return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + + +async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: + """Migrate old entry to a newer version.""" + + if config_entry.version > 1: + # This means the user has downgraded from a future version + return False + + if config_entry.version < 2 and config_entry.minor_version < 2: + version = config_entry.version + minor_version = config_entry.minor_version + _LOGGER.debug( + "Migrating configuration from version %s.%s", + version, + minor_version, + ) + + new_options = {**config_entry.options} + new_options[CONF_PORT] = DEFAULT_PORT + new_options[CONF_PORT_IPV6] = DEFAULT_PORT + + hass.config_entries.async_update_entry( + config_entry, options=new_options, minor_version=2 + ) + + _LOGGER.debug( + "Migration to configuration version %s.%s successful", + 1, + 2, + ) + + return True diff --git a/homeassistant/components/dnsip/config_flow.py b/homeassistant/components/dnsip/config_flow.py index 21a29465050..6dda0c03910 100644 --- a/homeassistant/components/dnsip/config_flow.py +++ b/homeassistant/components/dnsip/config_flow.py @@ -16,7 +16,7 @@ from homeassistant.config_entries import ( ConfigFlowResult, OptionsFlowWithConfigEntry, ) -from homeassistant.const import CONF_NAME +from homeassistant.const import CONF_NAME, CONF_PORT from homeassistant.core import callback import homeassistant.helpers.config_validation as cv @@ -25,10 +25,12 @@ from .const import ( CONF_IPV4, CONF_IPV6, CONF_IPV6_V4, + CONF_PORT_IPV6, CONF_RESOLVER, CONF_RESOLVER_IPV6, DEFAULT_HOSTNAME, DEFAULT_NAME, + DEFAULT_PORT, DEFAULT_RESOLVER, DEFAULT_RESOLVER_IPV6, DOMAIN, @@ -42,32 +44,42 @@ DATA_SCHEMA = vol.Schema( DATA_SCHEMA_ADV = vol.Schema( { vol.Required(CONF_HOSTNAME, default=DEFAULT_HOSTNAME): cv.string, - vol.Optional(CONF_RESOLVER, default=DEFAULT_RESOLVER): cv.string, - vol.Optional(CONF_RESOLVER_IPV6, default=DEFAULT_RESOLVER_IPV6): cv.string, + vol.Optional(CONF_RESOLVER): cv.string, + vol.Optional(CONF_PORT): cv.port, + vol.Optional(CONF_RESOLVER_IPV6): cv.string, + vol.Optional(CONF_PORT_IPV6): cv.port, } ) async def async_validate_hostname( - hostname: str, resolver_ipv4: str, resolver_ipv6: str + hostname: str, + resolver_ipv4: str, + resolver_ipv6: str, + port: int, + port_ipv6: int, ) -> dict[str, bool]: """Validate hostname.""" - async def async_check(hostname: str, resolver: str, qtype: str) -> bool: + async def async_check( + hostname: str, resolver: str, qtype: str, port: int = 53 + ) -> bool: """Return if able to resolve hostname.""" result = False with contextlib.suppress(DNSError): result = bool( - await aiodns.DNSResolver(nameservers=[resolver]).query(hostname, qtype) + await aiodns.DNSResolver( + nameservers=[resolver], udp_port=port, tcp_port=port + ).query(hostname, qtype) ) return result result: dict[str, bool] = {} tasks = await asyncio.gather( - async_check(hostname, resolver_ipv4, "A"), - async_check(hostname, resolver_ipv6, "AAAA"), - async_check(hostname, resolver_ipv4, "AAAA"), + async_check(hostname, resolver_ipv4, "A", port=port), + async_check(hostname, resolver_ipv6, "AAAA", port=port_ipv6), + async_check(hostname, resolver_ipv4, "AAAA", port=port), ) result[CONF_IPV4] = tasks[0] @@ -81,6 +93,7 @@ class DnsIPConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for dnsip integration.""" VERSION = 1 + MINOR_VERSION = 2 @staticmethod @callback @@ -102,8 +115,12 @@ class DnsIPConfigFlow(ConfigFlow, domain=DOMAIN): name = DEFAULT_NAME if hostname == DEFAULT_HOSTNAME else hostname resolver = user_input.get(CONF_RESOLVER, DEFAULT_RESOLVER) resolver_ipv6 = user_input.get(CONF_RESOLVER_IPV6, DEFAULT_RESOLVER_IPV6) + port = user_input.get(CONF_PORT, DEFAULT_PORT) + port_ipv6 = user_input.get(CONF_PORT_IPV6, DEFAULT_PORT) - validate = await async_validate_hostname(hostname, resolver, resolver_ipv6) + validate = await async_validate_hostname( + hostname, resolver, resolver_ipv6, port, port_ipv6 + ) set_resolver = resolver if validate[CONF_IPV6]: @@ -129,7 +146,9 @@ class DnsIPConfigFlow(ConfigFlow, domain=DOMAIN): }, options={ CONF_RESOLVER: resolver, + CONF_PORT: port, CONF_RESOLVER_IPV6: set_resolver, + CONF_PORT_IPV6: port_ipv6, }, ) @@ -156,11 +175,15 @@ class DnsIPOptionsFlowHandler(OptionsFlowWithConfigEntry): errors = {} if user_input is not None: resolver = user_input.get(CONF_RESOLVER, DEFAULT_RESOLVER) + port = user_input.get(CONF_PORT, DEFAULT_PORT) resolver_ipv6 = user_input.get(CONF_RESOLVER_IPV6, DEFAULT_RESOLVER_IPV6) + port_ipv6 = user_input.get(CONF_PORT_IPV6, DEFAULT_PORT) validate = await async_validate_hostname( self.config_entry.data[CONF_HOSTNAME], resolver, resolver_ipv6, + port, + port_ipv6, ) if ( @@ -178,7 +201,9 @@ class DnsIPOptionsFlowHandler(OptionsFlowWithConfigEntry): title=self.config_entry.title, data={ CONF_RESOLVER: resolver, + CONF_PORT: port, CONF_RESOLVER_IPV6: resolver_ipv6, + CONF_PORT_IPV6: port_ipv6, }, ) @@ -186,7 +211,9 @@ class DnsIPOptionsFlowHandler(OptionsFlowWithConfigEntry): vol.Schema( { vol.Optional(CONF_RESOLVER): cv.string, + vol.Optional(CONF_PORT): cv.port, vol.Optional(CONF_RESOLVER_IPV6): cv.string, + vol.Optional(CONF_PORT_IPV6): cv.port, } ), self.config_entry.options, diff --git a/homeassistant/components/dnsip/const.py b/homeassistant/components/dnsip/const.py index 41116bde61a..2e81099df34 100644 --- a/homeassistant/components/dnsip/const.py +++ b/homeassistant/components/dnsip/const.py @@ -8,6 +8,7 @@ PLATFORMS = [Platform.SENSOR] CONF_HOSTNAME = "hostname" CONF_RESOLVER = "resolver" CONF_RESOLVER_IPV6 = "resolver_ipv6" +CONF_PORT_IPV6 = "port_ipv6" CONF_IPV4 = "ipv4" CONF_IPV6 = "ipv6" CONF_IPV6_V4 = "ipv6_v4" @@ -16,4 +17,5 @@ DEFAULT_HOSTNAME = "myip.opendns.com" DEFAULT_IPV6 = False DEFAULT_NAME = "myip" DEFAULT_RESOLVER = "208.67.222.222" +DEFAULT_PORT = 53 DEFAULT_RESOLVER_IPV6 = "2620:119:53::53" diff --git a/homeassistant/components/dnsip/sensor.py b/homeassistant/components/dnsip/sensor.py index d3527bda3f2..726198e14cc 100644 --- a/homeassistant/components/dnsip/sensor.py +++ b/homeassistant/components/dnsip/sensor.py @@ -11,7 +11,7 @@ from aiodns.error import DNSError from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_NAME +from homeassistant.const import CONF_NAME, CONF_PORT from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -20,6 +20,7 @@ from .const import ( CONF_HOSTNAME, CONF_IPV4, CONF_IPV6, + CONF_PORT_IPV6, CONF_RESOLVER, CONF_RESOLVER_IPV6, DOMAIN, @@ -53,12 +54,14 @@ async def async_setup_entry( resolver_ipv4 = entry.options[CONF_RESOLVER] resolver_ipv6 = entry.options[CONF_RESOLVER_IPV6] + port_ipv4 = entry.options[CONF_PORT] + port_ipv6 = entry.options[CONF_PORT_IPV6] entities = [] if entry.data[CONF_IPV4]: - entities.append(WanIpSensor(name, hostname, resolver_ipv4, False)) + entities.append(WanIpSensor(name, hostname, resolver_ipv4, False, port_ipv4)) if entry.data[CONF_IPV6]: - entities.append(WanIpSensor(name, hostname, resolver_ipv6, True)) + entities.append(WanIpSensor(name, hostname, resolver_ipv6, True, port_ipv6)) async_add_entities(entities, update_before_add=True) @@ -75,12 +78,13 @@ class WanIpSensor(SensorEntity): hostname: str, resolver: str, ipv6: bool, + port: int, ) -> None: """Initialize the DNS IP sensor.""" self._attr_name = "IPv6" if ipv6 else None self._attr_unique_id = f"{hostname}_{ipv6}" self.hostname = hostname - self.resolver = aiodns.DNSResolver() + self.resolver = aiodns.DNSResolver(tcp_port=port, udp_port=port) self.resolver.nameservers = [resolver] self.querytype = "AAAA" if ipv6 else "A" self._retries = DEFAULT_RETRIES diff --git a/homeassistant/components/dnsip/strings.json b/homeassistant/components/dnsip/strings.json index d402e27287c..d8258a65d6a 100644 --- a/homeassistant/components/dnsip/strings.json +++ b/homeassistant/components/dnsip/strings.json @@ -5,7 +5,9 @@ "data": { "hostname": "The hostname for which to perform the DNS query", "resolver": "Resolver for IPV4 lookup", - "resolver_ipv6": "Resolver for IPV6 lookup" + "port": "Port for IPV4 lookup", + "resolver_ipv6": "Resolver for IPV6 lookup", + "port_ipv6": "Port for IPV6 lookup" } } }, @@ -18,7 +20,9 @@ "init": { "data": { "resolver": "[%key:component::dnsip::config::step::user::data::resolver%]", - "resolver_ipv6": "[%key:component::dnsip::config::step::user::data::resolver_ipv6%]" + "port": "[%key:component::dnsip::config::step::user::data::port%]", + "resolver_ipv6": "[%key:component::dnsip::config::step::user::data::resolver_ipv6%]", + "port_ipv6": "[%key:component::dnsip::config::step::user::data::port_ipv6%]" } } }, @@ -26,7 +30,7 @@ "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" }, "error": { - "invalid_resolver": "Invalid IP address for resolver" + "invalid_resolver": "Invalid IP address or port for resolver" } } } diff --git a/tests/components/dnsip/test_config_flow.py b/tests/components/dnsip/test_config_flow.py index ff089be0e1e..99dc5781d16 100644 --- a/tests/components/dnsip/test_config_flow.py +++ b/tests/components/dnsip/test_config_flow.py @@ -13,12 +13,13 @@ from homeassistant.components.dnsip.const import ( CONF_HOSTNAME, CONF_IPV4, CONF_IPV6, + CONF_PORT_IPV6, CONF_RESOLVER, CONF_RESOLVER_IPV6, DOMAIN, ) from homeassistant.config_entries import ConfigEntryState -from homeassistant.const import CONF_NAME +from homeassistant.const import CONF_NAME, CONF_PORT from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -66,6 +67,8 @@ async def test_form(hass: HomeAssistant) -> None: assert result2["options"] == { "resolver": "208.67.222.222", "resolver_ipv6": "2620:119:53::53", + "port": 53, + "port_ipv6": 53, } assert len(mock_setup_entry.mock_calls) == 1 @@ -96,6 +99,8 @@ async def test_form_adv(hass: HomeAssistant) -> None: CONF_HOSTNAME: "home-assistant.io", CONF_RESOLVER: "8.8.8.8", CONF_RESOLVER_IPV6: "2620:119:53::53", + CONF_PORT: 53, + CONF_PORT_IPV6: 53, }, ) await hass.async_block_till_done() @@ -111,6 +116,8 @@ async def test_form_adv(hass: HomeAssistant) -> None: assert result2["options"] == { "resolver": "8.8.8.8", "resolver_ipv6": "2620:119:53::53", + "port": 53, + "port_ipv6": 53, } assert len(mock_setup_entry.mock_calls) == 1 @@ -152,6 +159,8 @@ async def test_flow_already_exist(hass: HomeAssistant) -> None: options={ CONF_RESOLVER: "208.67.222.222", CONF_RESOLVER_IPV6: "2620:119:53::5", + CONF_PORT: 53, + CONF_PORT_IPV6: 53, }, unique_id="home-assistant.io", ).add_to_hass(hass) @@ -197,6 +206,8 @@ async def test_options_flow(hass: HomeAssistant) -> None: options={ CONF_RESOLVER: "208.67.222.222", CONF_RESOLVER_IPV6: "2620:119:53::5", + CONF_PORT: 53, + CONF_PORT_IPV6: 53, }, ) entry.add_to_hass(hass) @@ -218,6 +229,8 @@ async def test_options_flow(hass: HomeAssistant) -> None: user_input={ CONF_RESOLVER: "8.8.8.8", CONF_RESOLVER_IPV6: "2001:4860:4860::8888", + CONF_PORT: 53, + CONF_PORT_IPV6: 53, }, ) await hass.async_block_till_done() @@ -226,6 +239,8 @@ async def test_options_flow(hass: HomeAssistant) -> None: assert result["data"] == { "resolver": "8.8.8.8", "resolver_ipv6": "2001:4860:4860::8888", + "port": 53, + "port_ipv6": 53, } assert entry.state is ConfigEntryState.LOADED @@ -245,6 +260,8 @@ async def test_options_flow_empty_return(hass: HomeAssistant) -> None: options={ CONF_RESOLVER: "8.8.8.8", CONF_RESOLVER_IPV6: "2620:119:53::1", + CONF_PORT: 53, + CONF_PORT_IPV6: 53, }, ) entry.add_to_hass(hass) @@ -271,6 +288,8 @@ async def test_options_flow_empty_return(hass: HomeAssistant) -> None: assert result["data"] == { "resolver": "208.67.222.222", "resolver_ipv6": "2620:119:53::53", + "port": 53, + "port_ipv6": 53, } entry = hass.config_entries.async_get_entry(entry.entry_id) @@ -283,6 +302,8 @@ async def test_options_flow_empty_return(hass: HomeAssistant) -> None: assert entry.options == { "resolver": "208.67.222.222", "resolver_ipv6": "2620:119:53::53", + "port": 53, + "port_ipv6": 53, } @@ -294,6 +315,8 @@ async def test_options_flow_empty_return(hass: HomeAssistant) -> None: CONF_NAME: "home-assistant.io", CONF_RESOLVER: "208.67.222.222", CONF_RESOLVER_IPV6: "2620:119:53::5", + CONF_PORT: 53, + CONF_PORT_IPV6: 53, CONF_IPV4: True, CONF_IPV6: False, }, @@ -302,6 +325,8 @@ async def test_options_flow_empty_return(hass: HomeAssistant) -> None: CONF_NAME: "home-assistant.io", CONF_RESOLVER: "208.67.222.222", CONF_RESOLVER_IPV6: "2620:119:53::5", + CONF_PORT: 53, + CONF_PORT_IPV6: 53, CONF_IPV4: False, CONF_IPV6: True, }, @@ -334,6 +359,8 @@ async def test_options_error(hass: HomeAssistant, p_input: dict[str, str]) -> No { CONF_RESOLVER: "192.168.200.34", CONF_RESOLVER_IPV6: "2001:4860:4860::8888", + CONF_PORT: 53, + CONF_PORT_IPV6: 53, }, ) await hass.async_block_till_done() diff --git a/tests/components/dnsip/test_init.py b/tests/components/dnsip/test_init.py index 3d816bebe60..ac5da227bde 100644 --- a/tests/components/dnsip/test_init.py +++ b/tests/components/dnsip/test_init.py @@ -8,12 +8,14 @@ from homeassistant.components.dnsip.const import ( CONF_HOSTNAME, CONF_IPV4, CONF_IPV6, + CONF_PORT_IPV6, CONF_RESOLVER, CONF_RESOLVER_IPV6, + DEFAULT_PORT, DOMAIN, ) from homeassistant.config_entries import SOURCE_USER, ConfigEntryState -from homeassistant.const import CONF_NAME +from homeassistant.const import CONF_NAME, CONF_PORT from homeassistant.core import HomeAssistant from . import RetrieveDNS @@ -35,6 +37,8 @@ async def test_load_unload_entry(hass: HomeAssistant) -> None: options={ CONF_RESOLVER: "208.67.222.222", CONF_RESOLVER_IPV6: "2620:119:53::53", + CONF_PORT: 53, + CONF_PORT_IPV6: 53, }, entry_id="1", unique_id="home-assistant.io", @@ -52,3 +56,77 @@ async def test_load_unload_entry(hass: HomeAssistant) -> None: assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() assert entry.state is ConfigEntryState.NOT_LOADED + + +async def test_port_migration( + hass: HomeAssistant, +) -> None: + """Test migration of the config entry from no ports to with ports.""" + + entry = MockConfigEntry( + domain=DOMAIN, + source=SOURCE_USER, + data={ + CONF_HOSTNAME: "home-assistant.io", + CONF_NAME: "home-assistant.io", + CONF_IPV4: True, + CONF_IPV6: True, + }, + options={ + CONF_RESOLVER: "208.67.222.222", + CONF_RESOLVER_IPV6: "2620:119:53::53", + }, + entry_id="1", + unique_id="home-assistant.io", + version=1, + minor_version=1, + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.dnsip.sensor.aiodns.DNSResolver", + return_value=RetrieveDNS(), + ): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert entry.version == 1 + assert entry.minor_version == 2 + assert entry.options[CONF_PORT] == DEFAULT_PORT + assert entry.options[CONF_PORT_IPV6] == DEFAULT_PORT + assert entry.state is ConfigEntryState.LOADED + + +async def test_migrate_error_from_future(hass: HomeAssistant) -> None: + """Test a future version isn't migrated.""" + + entry = MockConfigEntry( + domain=DOMAIN, + source=SOURCE_USER, + data={ + CONF_HOSTNAME: "home-assistant.io", + CONF_NAME: "home-assistant.io", + CONF_IPV4: True, + CONF_IPV6: True, + "some_new_data": "new_value", + }, + options={ + CONF_RESOLVER: "208.67.222.222", + CONF_RESOLVER_IPV6: "2620:119:53::53", + }, + entry_id="1", + unique_id="home-assistant.io", + version=2, + minor_version=1, + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.dnsip.sensor.aiodns.DNSResolver", + return_value=RetrieveDNS(), + ): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + entry = hass.config_entries.async_get_entry(entry.entry_id) + assert entry.state is ConfigEntryState.MIGRATION_ERROR diff --git a/tests/components/dnsip/test_sensor.py b/tests/components/dnsip/test_sensor.py index 0a81804a689..66cb5cc6ad9 100644 --- a/tests/components/dnsip/test_sensor.py +++ b/tests/components/dnsip/test_sensor.py @@ -12,13 +12,14 @@ from homeassistant.components.dnsip.const import ( CONF_HOSTNAME, CONF_IPV4, CONF_IPV6, + CONF_PORT_IPV6, CONF_RESOLVER, CONF_RESOLVER_IPV6, DOMAIN, ) from homeassistant.components.dnsip.sensor import SCAN_INTERVAL from homeassistant.config_entries import SOURCE_USER -from homeassistant.const import CONF_NAME, STATE_UNAVAILABLE +from homeassistant.const import CONF_NAME, CONF_PORT, STATE_UNAVAILABLE from homeassistant.core import HomeAssistant from . import RetrieveDNS @@ -40,6 +41,8 @@ async def test_sensor(hass: HomeAssistant) -> None: options={ CONF_RESOLVER: "208.67.222.222", CONF_RESOLVER_IPV6: "2620:119:53::53", + CONF_PORT: 53, + CONF_PORT_IPV6: 53, }, entry_id="1", unique_id="home-assistant.io", @@ -67,6 +70,49 @@ async def test_sensor(hass: HomeAssistant) -> None: ] +async def test_legacy_sensor(hass: HomeAssistant) -> None: + """Test the DNS IP sensor configured before the addition of ports.""" + entry = MockConfigEntry( + domain=DOMAIN, + source=SOURCE_USER, + data={ + CONF_HOSTNAME: "home-assistant.io", + CONF_NAME: "home-assistant.io", + CONF_IPV4: True, + CONF_IPV6: True, + }, + options={ + CONF_RESOLVER: "208.67.222.222", + CONF_RESOLVER_IPV6: "2620:119:53::53", + }, + entry_id="1", + unique_id="home-assistant.io", + version=1, + minor_version=1, + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.dnsip.sensor.aiodns.DNSResolver", + return_value=RetrieveDNS(), + ): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + state1 = hass.states.get("sensor.home_assistant_io") + state2 = hass.states.get("sensor.home_assistant_io_ipv6") + + assert state1.state == "1.1.1.1" + assert state1.attributes["ip_addresses"] == ["1.1.1.1", "1.2.3.4"] + assert state2.state == "2001:db8::77:dead:beef" + assert state2.attributes["ip_addresses"] == [ + "2001:db8::77:dead:beef", + "2001:db8:66::dead:beef", + "2001:db8:77::dead:beef", + "2001:db8:77::face:b00c", + ] + + async def test_sensor_no_response( hass: HomeAssistant, freezer: FrozenDateTimeFactory ) -> None: @@ -83,6 +129,8 @@ async def test_sensor_no_response( options={ CONF_RESOLVER: "208.67.222.222", CONF_RESOLVER_IPV6: "2620:119:53::53", + CONF_PORT: 53, + CONF_PORT_IPV6: 53, }, entry_id="1", unique_id="home-assistant.io", From 08864959ee81b6e6b09fcdace9dd8490c9b6a595 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Tue, 18 Jun 2024 22:26:10 +0200 Subject: [PATCH 0801/1445] Remove YAML import for Suez Water (#119923) Remove YAML import for suez water --- .../components/suez_water/config_flow.py | 15 ---- homeassistant/components/suez_water/sensor.py | 70 +---------------- .../components/suez_water/strings.json | 14 ---- .../components/suez_water/test_config_flow.py | 77 ------------------- 4 files changed, 4 insertions(+), 172 deletions(-) diff --git a/homeassistant/components/suez_water/config_flow.py b/homeassistant/components/suez_water/config_flow.py index 833981d8ed6..28b211dc808 100644 --- a/homeassistant/components/suez_water/config_flow.py +++ b/homeassistant/components/suez_water/config_flow.py @@ -75,21 +75,6 @@ class SuezWaterConfigFlow(ConfigFlow, domain=DOMAIN): step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors ) - async def async_step_import(self, user_input: dict[str, Any]) -> ConfigFlowResult: - """Import the yaml config.""" - await self.async_set_unique_id(user_input[CONF_USERNAME]) - self._abort_if_unique_id_configured() - try: - await self.hass.async_add_executor_job(validate_input, user_input) - except CannotConnect: - return self.async_abort(reason="cannot_connect") - except InvalidAuth: - return self.async_abort(reason="invalid_auth") - except Exception: - _LOGGER.exception("Unexpected exception") - return self.async_abort(reason="unknown") - return self.async_create_entry(title=user_input[CONF_USERNAME], data=user_input) - class CannotConnect(HomeAssistantError): """Error to indicate we cannot connect.""" diff --git a/homeassistant/components/suez_water/sensor.py b/homeassistant/components/suez_water/sensor.py index f48e78bb153..5b00cbf2dc4 100644 --- a/homeassistant/components/suez_water/sensor.py +++ b/homeassistant/components/suez_water/sensor.py @@ -7,82 +7,20 @@ import logging from pysuez import SuezClient from pysuez.client import PySuezError -import voluptuous as vol -from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, - SensorDeviceClass, - SensorEntity, -) -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, UnitOfVolume -from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant -from homeassistant.data_entry_flow import FlowResultType -import homeassistant.helpers.config_validation as cv +from homeassistant.components.sensor import SensorDeviceClass, SensorEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import UnitOfVolume +from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .const import CONF_COUNTER_ID, DOMAIN _LOGGER = logging.getLogger(__name__) -ISSUE_PLACEHOLDER = {"url": "/config/integrations/dashboard/add?domain=suez_water"} SCAN_INTERVAL = timedelta(hours=12) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Required(CONF_COUNTER_ID): cv.string, - } -) - - -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigType, - async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the sensor platform.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data=config, - ) - if ( - result["type"] == FlowResultType.CREATE_ENTRY - or result["reason"] == "already_configured" - ): - async_create_issue( - hass, - HOMEASSISTANT_DOMAIN, - f"deprecated_yaml_{DOMAIN}", - breaks_in_ha_version="2024.7.0", - is_fixable=False, - issue_domain=DOMAIN, - severity=IssueSeverity.WARNING, - translation_key="deprecated_yaml", - translation_placeholders={ - "domain": DOMAIN, - "integration_title": "Suez Water", - }, - ) - else: - async_create_issue( - hass, - DOMAIN, - f"deprecated_yaml_import_issue_{result['reason']}", - breaks_in_ha_version="2024.7.0", - is_fixable=False, - issue_domain=DOMAIN, - severity=IssueSeverity.WARNING, - translation_key=f"deprecated_yaml_import_issue_{result['reason']}", - translation_placeholders=ISSUE_PLACEHOLDER, - ) - async def async_setup_entry( hass: HomeAssistant, diff --git a/homeassistant/components/suez_water/strings.json b/homeassistant/components/suez_water/strings.json index fd85565d297..f9abd70fc19 100644 --- a/homeassistant/components/suez_water/strings.json +++ b/homeassistant/components/suez_water/strings.json @@ -24,19 +24,5 @@ "name": "Water usage yesterday" } } - }, - "issues": { - "deprecated_yaml_import_issue_invalid_auth": { - "title": "The Suez water YAML configuration import failed", - "description": "Configuring Suez water using YAML is being removed but there was an authentication error importing your YAML configuration.\n\nCorrect the YAML configuration and restart Home Assistant to try again or remove the Suez water YAML configuration from your configuration.yaml file and continue to [set up the integration]({url}) manually." - }, - "deprecated_yaml_import_issue_cannot_connect": { - "title": "The Suez water YAML configuration import failed", - "description": "Configuring Suez water using YAML is being removed but there was a connection error importing your YAML configuration.\n\nEnsure connection to Suez water works and restart Home Assistant to try again or remove the Suez water YAML configuration from your configuration.yaml file and continue to [set up the integration]({url}) manually." - }, - "deprecated_yaml_import_issue_unknown": { - "title": "The Suez water YAML configuration import failed", - "description": "Configuring Suez water using YAML is being removed but there was an unknown error when trying to import the YAML configuration.\n\nEnsure the imported configuration is correct and remove the Suez water YAML configuration from your configuration.yaml file and continue to [set up the integration]({url}) manually." - } } } diff --git a/tests/components/suez_water/test_config_flow.py b/tests/components/suez_water/test_config_flow.py index 1d689ffe0d6..3170a6779f0 100644 --- a/tests/components/suez_water/test_config_flow.py +++ b/tests/components/suez_water/test_config_flow.py @@ -139,80 +139,3 @@ async def test_form_error( assert result["title"] == "test-username" assert result["data"] == MOCK_DATA assert len(mock_setup_entry.mock_calls) == 1 - - -async def test_import(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: - """Test import flow.""" - with patch("homeassistant.components.suez_water.config_flow.SuezClient"): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=MOCK_DATA - ) - await hass.async_block_till_done() - - assert result["type"] is FlowResultType.CREATE_ENTRY - assert result["title"] == "test-username" - assert result["result"].unique_id == "test-username" - assert result["data"] == MOCK_DATA - assert len(mock_setup_entry.mock_calls) == 1 - - -@pytest.mark.parametrize( - ("exception", "reason"), [(PySuezError, "cannot_connect"), (Exception, "unknown")] -) -async def test_import_error( - hass: HomeAssistant, - mock_setup_entry: AsyncMock, - exception: Exception, - reason: str, -) -> None: - """Test we handle errors while importing.""" - - with patch( - "homeassistant.components.suez_water.config_flow.SuezClient", - side_effect=exception, - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=MOCK_DATA - ) - - assert result["type"] is FlowResultType.ABORT - assert result["reason"] == reason - - -async def test_importing_invalid_auth(hass: HomeAssistant) -> None: - """Test we handle invalid auth when importing.""" - - with ( - patch( - "homeassistant.components.suez_water.config_flow.SuezClient.__init__", - return_value=None, - ), - patch( - "homeassistant.components.suez_water.config_flow.SuezClient.check_credentials", - return_value=False, - ), - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=MOCK_DATA - ) - - assert result["type"] is FlowResultType.ABORT - assert result["reason"] == "invalid_auth" - - -async def test_import_already_configured(hass: HomeAssistant) -> None: - """Test we abort import when entry is already configured.""" - - entry = MockConfigEntry( - domain=DOMAIN, - unique_id="test-username", - data=MOCK_DATA, - ) - entry.add_to_hass(hass) - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=MOCK_DATA - ) - - assert result["type"] is FlowResultType.ABORT - assert result["reason"] == "already_configured" From f61347719fff3896397a7b7bf84d86ee642a6b30 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Tue, 18 Jun 2024 23:26:29 +0300 Subject: [PATCH 0802/1445] Allow removal of a Switcher device (#119927) Allow removal of Switcher device --- .../components/switcher_kis/__init__.py | 11 ++++ tests/components/switcher_kis/test_init.py | 60 ++++++++++++++++++- 2 files changed, 69 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/switcher_kis/__init__.py b/homeassistant/components/switcher_kis/__init__.py index 60b3b18b0b0..555ba951041 100644 --- a/homeassistant/components/switcher_kis/__init__.py +++ b/homeassistant/components/switcher_kis/__init__.py @@ -10,7 +10,9 @@ from aioswitcher.device import SwitcherBase from homeassistant.config_entries import ConfigEntry from homeassistant.const import EVENT_HOMEASSISTANT_STOP, Platform from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.helpers import device_registry as dr +from .const import DOMAIN from .coordinator import SwitcherDataUpdateCoordinator PLATFORMS = [ @@ -77,3 +79,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: SwitcherConfigEntry) -> async def async_unload_entry(hass: HomeAssistant, entry: SwitcherConfigEntry) -> bool: """Unload a config entry.""" return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + + +async def async_remove_config_entry_device( + hass: HomeAssistant, config_entry: SwitcherConfigEntry, device_entry: dr.DeviceEntry +) -> bool: + """Remove a config entry from a device.""" + return not device_entry.identifiers.intersection( + (DOMAIN, device_id) for device_id in config_entry.runtime_data + ) diff --git a/tests/components/switcher_kis/test_init.py b/tests/components/switcher_kis/test_init.py index 14217a7e044..a652348463e 100644 --- a/tests/components/switcher_kis/test_init.py +++ b/tests/components/switcher_kis/test_init.py @@ -4,16 +4,19 @@ from datetime import timedelta import pytest -from homeassistant.components.switcher_kis.const import MAX_UPDATE_INTERVAL_SEC +from homeassistant.components.switcher_kis.const import DOMAIN, MAX_UPDATE_INTERVAL_SEC from homeassistant.config_entries import ConfigEntryState from homeassistant.const import STATE_UNAVAILABLE from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr +from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util, slugify from . import init_integration -from .consts import DUMMY_SWITCHER_DEVICES +from .consts import DUMMY_DEVICE_ID1, DUMMY_DEVICE_ID4, DUMMY_SWITCHER_DEVICES from tests.common import async_fire_time_changed +from tests.typing import WebSocketGenerator async def test_update_fail( @@ -78,3 +81,56 @@ async def test_entry_unload(hass: HomeAssistant, mock_bridge) -> None: assert entry.state is ConfigEntryState.NOT_LOADED assert mock_bridge.is_running is False + + +async def test_remove_device( + hass: HomeAssistant, mock_bridge, hass_ws_client: WebSocketGenerator +) -> None: + """Test being able to remove a disconnected device.""" + assert await async_setup_component(hass, "config", {}) + entry = await init_integration(hass) + entry_id = entry.entry_id + assert mock_bridge + + mock_bridge.mock_callbacks(DUMMY_SWITCHER_DEVICES) + await hass.async_block_till_done() + + assert mock_bridge.is_running is True + assert len(entry.runtime_data) == 2 + + device_registry = dr.async_get(hass) + live_device_id = DUMMY_DEVICE_ID1 + dead_device_id = DUMMY_DEVICE_ID4 + + assert len(dr.async_entries_for_config_entry(device_registry, entry_id)) == 2 + + # Create a dead device + device_registry.async_get_or_create( + config_entry_id=entry.entry_id, + identifiers={(DOMAIN, dead_device_id)}, + manufacturer="Switcher", + model="Switcher Model", + name="Switcher Device", + ) + await hass.async_block_till_done() + assert len(dr.async_entries_for_config_entry(device_registry, entry_id)) == 3 + + # Try to remove a live device - fails + device = device_registry.async_get_device(identifiers={(DOMAIN, live_device_id)}) + client = await hass_ws_client(hass) + response = await client.remove_device(device.id, entry_id) + assert not response["success"] + assert len(dr.async_entries_for_config_entry(device_registry, entry_id)) == 3 + assert ( + device_registry.async_get_device(identifiers={(DOMAIN, live_device_id)}) + is not None + ) + + # Try to remove a dead device - succeeds + device = device_registry.async_get_device(identifiers={(DOMAIN, dead_device_id)}) + response = await client.remove_device(device.id, entry_id) + assert response["success"] + assert len(dr.async_entries_for_config_entry(device_registry, entry_id)) == 2 + assert ( + device_registry.async_get_device(identifiers={(DOMAIN, dead_device_id)}) is None + ) From fe8805de6d71473eee9fcacee82cb3eb7575fdcc Mon Sep 17 00:00:00 2001 From: G Johansson Date: Tue, 18 Jun 2024 22:26:44 +0200 Subject: [PATCH 0803/1445] Remove deprecated blink refresh service (#119919) * Remove deprecated blink refresh service * Remove string * Fix tests --- homeassistant/components/blink/const.py | 1 - homeassistant/components/blink/icons.json | 1 - homeassistant/components/blink/services.py | 44 ++------ homeassistant/components/blink/services.yaml | 8 -- homeassistant/components/blink/strings.json | 10 -- tests/components/blink/test_init.py | 2 - tests/components/blink/test_services.py | 113 +------------------ 7 files changed, 9 insertions(+), 170 deletions(-) diff --git a/homeassistant/components/blink/const.py b/homeassistant/components/blink/const.py index 7de0e860bd8..0f24eec2178 100644 --- a/homeassistant/components/blink/const.py +++ b/homeassistant/components/blink/const.py @@ -21,7 +21,6 @@ TYPE_BATTERY = "battery" TYPE_WIFI_STRENGTH = "wifi_strength" SERVICE_RECORD = "record" -SERVICE_REFRESH = "blink_update" SERVICE_TRIGGER = "trigger_camera" SERVICE_SAVE_VIDEO = "save_video" SERVICE_SAVE_RECENT_CLIPS = "save_recent_clips" diff --git a/homeassistant/components/blink/icons.json b/homeassistant/components/blink/icons.json index 99bc91e37d4..615a3c4c6dc 100644 --- a/homeassistant/components/blink/icons.json +++ b/homeassistant/components/blink/icons.json @@ -12,7 +12,6 @@ } }, "services": { - "blink_update": "mdi:update", "record": "mdi:video-box", "trigger_camera": "mdi:image-refresh", "save_video": "mdi:file-video", diff --git a/homeassistant/components/blink/services.py b/homeassistant/components/blink/services.py index e01371c5c09..298ead00a45 100644 --- a/homeassistant/components/blink/services.py +++ b/homeassistant/components/blink/services.py @@ -8,13 +8,9 @@ from homeassistant.config_entries import ConfigEntry, ConfigEntryState from homeassistant.const import ATTR_DEVICE_ID, CONF_PIN from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.exceptions import HomeAssistantError, ServiceValidationError -from homeassistant.helpers import ( - config_validation as cv, - device_registry as dr, - issue_registry as ir, -) +from homeassistant.helpers import config_validation as cv, device_registry as dr -from .const import ATTR_CONFIG_ENTRY_ID, DOMAIN, SERVICE_REFRESH, SERVICE_SEND_PIN +from .const import ATTR_CONFIG_ENTRY_ID, DOMAIN, SERVICE_SEND_PIN from .coordinator import BlinkUpdateCoordinator SERVICE_UPDATE_SCHEMA = vol.Schema( @@ -93,33 +89,9 @@ def setup_services(hass: HomeAssistant) -> None: call.data[CONF_PIN], ) - async def blink_refresh(call: ServiceCall): - """Call blink to refresh info.""" - ir.async_create_issue( - hass, - DOMAIN, - "service_deprecation", - breaks_in_ha_version="2024.7.0", - is_fixable=True, - is_persistent=True, - severity=ir.IssueSeverity.WARNING, - translation_key="service_deprecation", - ) - - for coordinator in collect_coordinators(call.data[ATTR_DEVICE_ID]): - await coordinator.api.refresh(force_cache=True) - - # Register all the above services - # Refresh service is deprecated and will be removed in 7/2024 - service_mapping = [ - (blink_refresh, SERVICE_REFRESH, SERVICE_UPDATE_SCHEMA), - (send_pin, SERVICE_SEND_PIN, SERVICE_SEND_PIN_SCHEMA), - ] - - for service_handler, service_name, schema in service_mapping: - hass.services.async_register( - DOMAIN, - service_name, - service_handler, - schema=schema, - ) + hass.services.async_register( + DOMAIN, + SERVICE_SEND_PIN, + send_pin, + schema=SERVICE_SEND_PIN_SCHEMA, + ) diff --git a/homeassistant/components/blink/services.yaml b/homeassistant/components/blink/services.yaml index 480810af2ba..244763d5535 100644 --- a/homeassistant/components/blink/services.yaml +++ b/homeassistant/components/blink/services.yaml @@ -1,13 +1,5 @@ # Describes the format for available Blink services -blink_update: - fields: - device_id: - required: true - selector: - device: - integration: blink - record: target: entity: diff --git a/homeassistant/components/blink/strings.json b/homeassistant/components/blink/strings.json index 8f94f8c9543..bd0e7789816 100644 --- a/homeassistant/components/blink/strings.json +++ b/homeassistant/components/blink/strings.json @@ -55,16 +55,6 @@ } }, "services": { - "blink_update": { - "name": "Update", - "description": "Forces a refresh.", - "fields": { - "device_id": { - "name": "Device ID", - "description": "The Blink device id." - } - } - }, "record": { "name": "Record", "description": "Requests camera to record a clip." diff --git a/tests/components/blink/test_init.py b/tests/components/blink/test_init.py index 46806ef3349..3cd2cd51ebd 100644 --- a/tests/components/blink/test_init.py +++ b/tests/components/blink/test_init.py @@ -8,7 +8,6 @@ import pytest from homeassistant.components.blink.const import ( DOMAIN, - SERVICE_REFRESH, SERVICE_SAVE_VIDEO, SERVICE_SEND_PIN, ) @@ -82,7 +81,6 @@ async def test_unload_entry_multiple( assert mock_config_entry.state is ConfigEntryState.LOADED assert await hass.config_entries.async_unload(mock_config_entry.entry_id) assert mock_config_entry.state is ConfigEntryState.NOT_LOADED - assert hass.services.has_service(DOMAIN, SERVICE_REFRESH) assert hass.services.has_service(DOMAIN, SERVICE_SAVE_VIDEO) assert hass.services.has_service(DOMAIN, SERVICE_SEND_PIN) diff --git a/tests/components/blink/test_services.py b/tests/components/blink/test_services.py index d2685bd04eb..856d9e6e8a0 100644 --- a/tests/components/blink/test_services.py +++ b/tests/components/blink/test_services.py @@ -7,14 +7,12 @@ import pytest from homeassistant.components.blink.const import ( ATTR_CONFIG_ENTRY_ID, DOMAIN, - SERVICE_REFRESH, SERVICE_SEND_PIN, ) from homeassistant.config_entries import ConfigEntryState -from homeassistant.const import ATTR_DEVICE_ID, CONF_PIN +from homeassistant.const import CONF_PIN from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import device_registry as dr from tests.common import MockConfigEntry @@ -23,43 +21,6 @@ FILENAME = "blah" PIN = "1234" -async def test_refresh_service_calls( - hass: HomeAssistant, - device_registry: dr.DeviceRegistry, - mock_blink_api: MagicMock, - mock_blink_auth_api: MagicMock, - mock_config_entry: MockConfigEntry, -) -> None: - """Test refrest service calls.""" - - mock_config_entry.add_to_hass(hass) - assert await hass.config_entries.async_setup(mock_config_entry.entry_id) - await hass.async_block_till_done() - - device_entry = device_registry.async_get_device(identifiers={(DOMAIN, "12345")}) - assert device_entry - - assert mock_config_entry.state is ConfigEntryState.LOADED - assert mock_blink_api.refresh.call_count == 1 - - await hass.services.async_call( - DOMAIN, - SERVICE_REFRESH, - {ATTR_DEVICE_ID: [device_entry.id]}, - blocking=True, - ) - - assert mock_blink_api.refresh.call_count == 2 - - with pytest.raises(HomeAssistantError): - await hass.services.async_call( - DOMAIN, - SERVICE_REFRESH, - {ATTR_DEVICE_ID: ["bad-device_id"]}, - blocking=True, - ) - - async def test_pin_service_calls( hass: HomeAssistant, mock_blink_api: MagicMock, @@ -128,47 +89,6 @@ async def test_service_pin_called_with_non_blink_device( ) -async def test_service_update_called_with_non_blink_device( - hass: HomeAssistant, - mock_blink_api: MagicMock, - device_registry: dr.DeviceRegistry, - mock_blink_auth_api: MagicMock, - mock_config_entry: MockConfigEntry, -) -> None: - """Test update service calls with non blink device.""" - - mock_config_entry.add_to_hass(hass) - assert await hass.config_entries.async_setup(mock_config_entry.entry_id) - await hass.async_block_till_done() - - other_domain = "NotBlink" - other_config_id = "555" - other_mock_config_entry = MockConfigEntry( - title="Not Blink", domain=other_domain, entry_id=other_config_id - ) - other_mock_config_entry.add_to_hass(hass) - - device_entry = device_registry.async_get_or_create( - config_entry_id=other_config_id, - identifiers={ - (other_domain, 1), - }, - ) - - hass.config.is_allowed_path = Mock(return_value=True) - mock_blink_api.cameras = {CAMERA_NAME: AsyncMock()} - - parameters = {ATTR_DEVICE_ID: [device_entry.id]} - - with pytest.raises(HomeAssistantError): - await hass.services.async_call( - DOMAIN, - SERVICE_REFRESH, - parameters, - blocking=True, - ) - - async def test_service_pin_called_with_unloaded_entry( hass: HomeAssistant, mock_blink_api: MagicMock, @@ -193,34 +113,3 @@ async def test_service_pin_called_with_unloaded_entry( parameters, blocking=True, ) - - -async def test_service_update_called_with_unloaded_entry( - hass: HomeAssistant, - device_registry: dr.DeviceRegistry, - mock_blink_api: MagicMock, - mock_blink_auth_api: MagicMock, - mock_config_entry: MockConfigEntry, -) -> None: - """Test update service calls with not ready config entry.""" - - mock_config_entry.add_to_hass(hass) - assert await hass.config_entries.async_setup(mock_config_entry.entry_id) - await hass.async_block_till_done() - - mock_config_entry.mock_state(hass, ConfigEntryState.SETUP_ERROR) - hass.config.is_allowed_path = Mock(return_value=True) - mock_blink_api.cameras = {CAMERA_NAME: AsyncMock()} - - device_entry = device_registry.async_get_device(identifiers={(DOMAIN, "12345")}) - assert device_entry - - parameters = {ATTR_DEVICE_ID: [device_entry.id]} - - with pytest.raises(HomeAssistantError): - await hass.services.async_call( - DOMAIN, - SERVICE_REFRESH, - parameters, - blocking=True, - ) From b419ca224115355f43d906cba4db2fbc32df284c Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 18 Jun 2024 22:27:52 +0200 Subject: [PATCH 0804/1445] Register Z-Wave services on integration setup (#119924) --- homeassistant/components/zwave_js/__init__.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index 2b10f415bb7..dedae10400f 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -142,6 +142,12 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: hass.config_entries.async_update_entry( entry, unique_id=str(entry.unique_id) ) + + dev_reg = dr.async_get(hass) + ent_reg = er.async_get(hass) + services = ZWaveServices(hass, ent_reg, dev_reg) + services.async_register() + return True @@ -180,11 +186,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async_delete_issue(hass, DOMAIN, "invalid_server_version") LOGGER.info("Connected to Zwave JS Server") - dev_reg = dr.async_get(hass) - ent_reg = er.async_get(hass) - services = ZWaveServices(hass, ent_reg, dev_reg) - services.async_register() - # Set up websocket API async_register_api(hass) entry.runtime_data = {} From f0026d171e98671876a964a61db9d27ed402666b Mon Sep 17 00:00:00 2001 From: Jeef Date: Tue, 18 Jun 2024 14:31:59 -0600 Subject: [PATCH 0805/1445] Bump weatherflow4py to 0.2.21 (#119889) --- homeassistant/components/weatherflow_cloud/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/weatherflow_cloud/manifest.json b/homeassistant/components/weatherflow_cloud/manifest.json index 361349dcbe8..93df04d833c 100644 --- a/homeassistant/components/weatherflow_cloud/manifest.json +++ b/homeassistant/components/weatherflow_cloud/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/weatherflow_cloud", "iot_class": "cloud_polling", - "requirements": ["weatherflow4py==0.2.20"] + "requirements": ["weatherflow4py==0.2.21"] } diff --git a/requirements_all.txt b/requirements_all.txt index c8569e26b56..8ac84a8608d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2879,7 +2879,7 @@ watchdog==2.3.1 waterfurnace==1.1.0 # homeassistant.components.weatherflow_cloud -weatherflow4py==0.2.20 +weatherflow4py==0.2.21 # homeassistant.components.webmin webmin-xmlrpc==0.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c6eb1178748..3b855490169 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2241,7 +2241,7 @@ wallbox==0.6.0 watchdog==2.3.1 # homeassistant.components.weatherflow_cloud -weatherflow4py==0.2.20 +weatherflow4py==0.2.21 # homeassistant.components.webmin webmin-xmlrpc==0.0.2 From 8a38424c2454520310a2c0aae71f0ce77e82d1fb Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 18 Jun 2024 22:34:11 +0200 Subject: [PATCH 0806/1445] Add more airgradient configuration entities (#119191) --- .../components/airgradient/select.py | 43 ++++++- .../components/airgradient/strings.json | 15 +++ .../airgradient/snapshots/test_select.ambr | 112 ++++++++++++++++++ 3 files changed, 166 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/airgradient/select.py b/homeassistant/components/airgradient/select.py index 7880e55de19..8fac06917fd 100644 --- a/homeassistant/components/airgradient/select.py +++ b/homeassistant/components/airgradient/select.py @@ -4,7 +4,12 @@ from collections.abc import Awaitable, Callable from dataclasses import dataclass from airgradient import AirGradientClient, Config -from airgradient.models import ConfigurationControl, TemperatureUnit +from airgradient.models import ( + ConfigurationControl, + LedBarMode, + PmStandard, + TemperatureUnit, +) from homeassistant.components.select import SelectEntity, SelectEntityDescription from homeassistant.const import EntityCategory @@ -17,6 +22,12 @@ from .const import DOMAIN from .coordinator import AirGradientConfigCoordinator from .entity import AirGradientEntity +PM_STANDARD = { + PmStandard.UGM3: "ugm3", + PmStandard.USAQI: "us_aqi", +} +PM_STANDARD_REVERSE = {v: k for k, v in PM_STANDARD.items()} + @dataclass(frozen=True, kw_only=True) class AirGradientSelectEntityDescription(SelectEntityDescription): @@ -25,6 +36,7 @@ class AirGradientSelectEntityDescription(SelectEntityDescription): value_fn: Callable[[Config], str | None] set_value_fn: Callable[[AirGradientClient, str], Awaitable[None]] requires_display: bool = False + requires_led_bar: bool = False CONFIG_CONTROL_ENTITY = AirGradientSelectEntityDescription( @@ -32,9 +44,11 @@ CONFIG_CONTROL_ENTITY = AirGradientSelectEntityDescription( translation_key="configuration_control", options=[ConfigurationControl.CLOUD.value, ConfigurationControl.LOCAL.value], entity_category=EntityCategory.CONFIG, - value_fn=lambda config: config.configuration_control - if config.configuration_control is not ConfigurationControl.NOT_INITIALIZED - else None, + value_fn=lambda config: ( + config.configuration_control + if config.configuration_control is not ConfigurationControl.NOT_INITIALIZED + else None + ), set_value_fn=lambda client, value: client.set_configuration_control( ConfigurationControl(value) ), @@ -52,6 +66,26 @@ PROTECTED_SELECT_TYPES: tuple[AirGradientSelectEntityDescription, ...] = ( ), requires_display=True, ), + AirGradientSelectEntityDescription( + key="display_pm_standard", + translation_key="display_pm_standard", + options=list(PM_STANDARD_REVERSE), + entity_category=EntityCategory.CONFIG, + value_fn=lambda config: PM_STANDARD.get(config.pm_standard), + set_value_fn=lambda client, value: client.set_pm_standard( + PM_STANDARD_REVERSE[value] + ), + requires_display=True, + ), + AirGradientSelectEntityDescription( + key="led_bar_mode", + translation_key="led_bar_mode", + options=[x.value for x in LedBarMode], + entity_category=EntityCategory.CONFIG, + value_fn=lambda config: config.led_bar_mode, + set_value_fn=lambda client, value: client.set_led_bar_mode(LedBarMode(value)), + requires_led_bar=True, + ), ) @@ -74,6 +108,7 @@ async def async_setup_entry( description.requires_display and measurement_coordinator.data.model.startswith("I") ) + or (description.requires_led_bar and "L" in measurement_coordinator.data.model) ) async_add_entities(entities) diff --git a/homeassistant/components/airgradient/strings.json b/homeassistant/components/airgradient/strings.json index a0f6af08132..f4b558cf31a 100644 --- a/homeassistant/components/airgradient/strings.json +++ b/homeassistant/components/airgradient/strings.json @@ -38,6 +38,21 @@ "c": "Celsius", "f": "Fahrenheit" } + }, + "display_pm_standard": { + "name": "Display PM standard", + "state": { + "ugm3": "µg/m³", + "us_aqi": "US AQI" + } + }, + "led_bar_mode": { + "name": "LED bar mode", + "state": { + "off": "Off", + "co2": "Carbon dioxide", + "pm": "Particulate matter" + } } }, "sensor": { diff --git a/tests/components/airgradient/snapshots/test_select.ambr b/tests/components/airgradient/snapshots/test_select.ambr index fb201b88204..d29c7d23923 100644 --- a/tests/components/airgradient/snapshots/test_select.ambr +++ b/tests/components/airgradient/snapshots/test_select.ambr @@ -54,6 +54,61 @@ 'state': 'local', }) # --- +# name: test_all_entities[select.airgradient_display_pm_standard-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'ugm3', + 'us_aqi', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.airgradient_display_pm_standard', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Display PM standard', + 'platform': 'airgradient', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'display_pm_standard', + 'unique_id': '84fce612f5b8-display_pm_standard', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[select.airgradient_display_pm_standard-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Airgradient Display PM standard', + 'options': list([ + 'ugm3', + 'us_aqi', + ]), + }), + 'context': , + 'entity_id': 'select.airgradient_display_pm_standard', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'ugm3', + }) +# --- # name: test_all_entities[select.airgradient_display_temperature_unit-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -109,6 +164,63 @@ 'state': 'c', }) # --- +# name: test_all_entities[select.airgradient_led_bar_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'off', + 'co2', + 'pm', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.airgradient_led_bar_mode', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'LED bar mode', + 'platform': 'airgradient', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'led_bar_mode', + 'unique_id': '84fce612f5b8-led_bar_mode', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[select.airgradient_led_bar_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Airgradient LED bar mode', + 'options': list([ + 'off', + 'co2', + 'pm', + ]), + }), + 'context': , + 'entity_id': 'select.airgradient_led_bar_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'co2', + }) +# --- # name: test_all_entities_outdoor[select.airgradient_configuration_source-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ From b8cafe7e5efe73d2c47c1b3084b8ac9f2c4b0f45 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 18 Jun 2024 15:43:16 -0500 Subject: [PATCH 0807/1445] Small cleanups to august (#119912) --- .../components/august/binary_sensor.py | 21 +++++------- homeassistant/components/august/camera.py | 4 +-- homeassistant/components/august/lock.py | 13 +++---- homeassistant/components/august/sensor.py | 34 ++----------------- 4 files changed, 21 insertions(+), 51 deletions(-) diff --git a/homeassistant/components/august/binary_sensor.py b/homeassistant/components/august/binary_sensor.py index 8671032f32d..81d84965d58 100644 --- a/homeassistant/components/august/binary_sensor.py +++ b/homeassistant/components/august/binary_sensor.py @@ -210,8 +210,6 @@ class AugustDoorBinarySensor(AugustEntityMixin, BinarySensorEntity): """Initialize the sensor.""" super().__init__(data, device) self.entity_description = description - self._data = data - self._device = device self._attr_unique_id = f"{self._device_id}_{description.key}" @callback @@ -273,22 +271,21 @@ class AugustDoorbellBinarySensor(AugustEntityMixin, BinarySensorEntity): else: self._attr_available = True + @callback + def _async_scheduled_update(self, now: datetime) -> None: + """Timer callback for sensor update.""" + self._check_for_off_update_listener = None + self._update_from_data() + if not self.is_on: + self.async_write_ha_state() + def _schedule_update_to_recheck_turn_off_sensor(self) -> None: """Schedule an update to recheck the sensor to see if it is ready to turn off.""" # If the sensor is already off there is nothing to do if not self.is_on: return - - @callback - def _scheduled_update(now: datetime) -> None: - """Timer callback for sensor update.""" - self._check_for_off_update_listener = None - self._update_from_data() - if not self.is_on: - self.async_write_ha_state() - self._check_for_off_update_listener = async_call_later( - self.hass, TIME_TO_RECHECK_DETECTION.total_seconds(), _scheduled_update + self.hass, TIME_TO_RECHECK_DETECTION, self._async_scheduled_update ) def _cancel_any_pending_updates(self) -> None: diff --git a/homeassistant/components/august/camera.py b/homeassistant/components/august/camera.py index 4c56502e6c7..76ccf9fa4dd 100644 --- a/homeassistant/components/august/camera.py +++ b/homeassistant/components/august/camera.py @@ -43,6 +43,8 @@ class AugustCamera(AugustEntityMixin, Camera): """An implementation of an August security camera.""" _attr_translation_key = "camera" + _attr_motion_detection_enabled = True + _attr_brand = DEFAULT_NAME def __init__( self, data: AugustData, device: Doorbell, session: ClientSession, timeout: int @@ -55,8 +57,6 @@ class AugustCamera(AugustEntityMixin, Camera): self._content_token = None self._image_content = None self._attr_unique_id = f"{self._device_id:s}_camera" - self._attr_motion_detection_enabled = True - self._attr_brand = DEFAULT_NAME @property def is_recording(self) -> bool: diff --git a/homeassistant/components/august/lock.py b/homeassistant/components/august/lock.py index 1817319d823..47b1be52184 100644 --- a/homeassistant/components/august/lock.py +++ b/homeassistant/components/august/lock.py @@ -40,11 +40,11 @@ class AugustLock(AugustEntityMixin, RestoreEntity, LockEntity): """Representation of an August lock.""" _attr_name = None + _lock_status: LockStatus | None = None def __init__(self, data: AugustData, device: Lock) -> None: """Initialize the lock.""" super().__init__(data, device) - self._lock_status = None self._attr_unique_id = f"{self._device_id:s}_lock" if self._detail.unlatch_supported: self._attr_supported_features = LockEntityFeature.OPEN @@ -136,14 +136,15 @@ class AugustLock(AugustEntityMixin, RestoreEntity, LockEntity): update_lock_detail_from_activity(self._detail, bridge_activity) self._update_lock_status_from_detail() - if self._lock_status is None or self._lock_status is LockStatus.UNKNOWN: + lock_status = self._lock_status + if lock_status is None or lock_status is LockStatus.UNKNOWN: self._attr_is_locked = None else: - self._attr_is_locked = self._lock_status is LockStatus.LOCKED + self._attr_is_locked = lock_status is LockStatus.LOCKED - self._attr_is_jammed = self._lock_status is LockStatus.JAMMED - self._attr_is_locking = self._lock_status is LockStatus.LOCKING - self._attr_is_unlocking = self._lock_status in ( + self._attr_is_jammed = lock_status is LockStatus.JAMMED + self._attr_is_locking = lock_status is LockStatus.LOCKING + self._attr_is_unlocking = lock_status in ( LockStatus.UNLOCKING, LockStatus.UNLATCHING, ) diff --git a/homeassistant/components/august/sensor.py b/homeassistant/components/august/sensor.py index c1dc6620f81..8ad32df3c08 100644 --- a/homeassistant/components/august/sensor.py +++ b/homeassistant/components/august/sensor.py @@ -26,7 +26,6 @@ from homeassistant.const import ( EntityCategory, ) from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import AugustConfigEntry, AugustData @@ -37,7 +36,6 @@ from .const import ( ATTR_OPERATION_METHOD, ATTR_OPERATION_REMOTE, ATTR_OPERATION_TAG, - DOMAIN, OPERATION_METHOD_AUTORELOCK, OPERATION_METHOD_KEYPAD, OPERATION_METHOD_MANUAL, @@ -100,7 +98,6 @@ async def async_setup_entry( """Set up the August sensors.""" data = config_entry.runtime_data entities: list[SensorEntity] = [] - migrate_unique_id_devices = [] operation_sensors = [] batteries: dict[str, list[Doorbell | Lock]] = { "device_battery": [], @@ -126,9 +123,7 @@ async def async_setup_entry( device.device_name, ) entities.append( - AugustBatterySensor[LockDetail]( - data, device, device, SENSOR_TYPE_DEVICE_BATTERY - ) + AugustBatterySensor[LockDetail](data, device, SENSOR_TYPE_DEVICE_BATTERY) ) for device in batteries["linked_keypad_battery"]: @@ -145,34 +140,15 @@ async def async_setup_entry( device.device_name, ) keypad_battery_sensor = AugustBatterySensor[KeypadDetail]( - data, detail.keypad, device, SENSOR_TYPE_KEYPAD_BATTERY + data, detail.keypad, SENSOR_TYPE_KEYPAD_BATTERY ) entities.append(keypad_battery_sensor) - migrate_unique_id_devices.append(keypad_battery_sensor) entities.extend(AugustOperatorSensor(data, device) for device in operation_sensors) - await _async_migrate_old_unique_ids(hass, migrate_unique_id_devices) - async_add_entities(entities) -async def _async_migrate_old_unique_ids(hass: HomeAssistant, devices) -> None: - """Keypads now have their own serial number.""" - registry = er.async_get(hass) - for device in devices: - old_entity_id = registry.async_get_entity_id( - "sensor", DOMAIN, device.old_unique_id - ) - if old_entity_id is not None: - _LOGGER.debug( - "Migrating unique_id from [%s] to [%s]", - device.old_unique_id, - device.unique_id, - ) - registry.async_update_entity(old_entity_id, new_unique_id=device.unique_id) - - class AugustOperatorSensor(AugustEntityMixin, RestoreSensor): """Representation of an August lock operation sensor.""" @@ -181,8 +157,6 @@ class AugustOperatorSensor(AugustEntityMixin, RestoreSensor): def __init__(self, data: AugustData, device) -> None: """Initialize the sensor.""" super().__init__(data, device) - self._data = data - self._device = device self._operated_remote: bool | None = None self._operated_keypad: bool | None = None self._operated_manual: bool | None = None @@ -279,15 +253,13 @@ class AugustBatterySensor(AugustEntityMixin, SensorEntity, Generic[_T]): def __init__( self, data: AugustData, - device, - old_device, + device: Doorbell | Lock | KeypadDetail, description: AugustSensorEntityDescription[_T], ) -> None: """Initialize the sensor.""" super().__init__(data, device) self.entity_description = description self._attr_unique_id = f"{self._device_id}_{description.key}" - self.old_unique_id = f"{old_device.device_id}_{description.key}" self._update_from_data() @callback From 3d45ced02e95fd514880ec42eba7626fffca002f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 18 Jun 2024 16:03:46 -0500 Subject: [PATCH 0808/1445] Cleanup code to add august sensors (#119929) --- .../components/august/binary_sensor.py | 40 ++++-------- homeassistant/components/august/sensor.py | 62 +++++-------------- 2 files changed, 29 insertions(+), 73 deletions(-) diff --git a/homeassistant/components/august/binary_sensor.py b/homeassistant/components/august/binary_sensor.py index 81d84965d58..27371f5e3f6 100644 --- a/homeassistant/components/august/binary_sensor.py +++ b/homeassistant/components/august/binary_sensor.py @@ -160,38 +160,22 @@ async def async_setup_entry( data = config_entry.runtime_data entities: list[BinarySensorEntity] = [] - for door in data.locks: - detail = data.get_device_detail(door.device_id) - if not detail.doorsense: - _LOGGER.debug( - ( - "Not adding sensor class door for lock %s because it does not have" - " doorsense" - ), - door.device_name, - ) - continue - - _LOGGER.debug("Adding sensor class door for %s", door.device_name) - entities.append(AugustDoorBinarySensor(data, door, SENSOR_TYPE_DOOR)) + for lock in data.locks: + detail = data.get_device_detail(lock.device_id) + if detail.doorsense: + entities.append(AugustDoorBinarySensor(data, lock, SENSOR_TYPE_DOOR)) if detail.doorbell: - for description in SENSOR_TYPES_DOORBELL: - _LOGGER.debug( - "Adding doorbell sensor class %s for %s", - description.device_class, - door.device_name, - ) - entities.append(AugustDoorbellBinarySensor(data, door, description)) + entities.extend( + AugustDoorbellBinarySensor(data, lock, description) + for description in SENSOR_TYPES_DOORBELL + ) for doorbell in data.doorbells: - for description in SENSOR_TYPES_DOORBELL + SENSOR_TYPES_VIDEO_DOORBELL: - _LOGGER.debug( - "Adding doorbell sensor class %s for %s", - description.device_class, - doorbell.device_name, - ) - entities.append(AugustDoorbellBinarySensor(data, doorbell, description)) + entities.extend( + AugustDoorbellBinarySensor(data, doorbell, description) + for description in SENSOR_TYPES_DOORBELL + SENSOR_TYPES_VIDEO_DOORBELL + ) async_add_entities(entities) diff --git a/homeassistant/components/august/sensor.py b/homeassistant/components/august/sensor.py index 8ad32df3c08..e5d29bb23d8 100644 --- a/homeassistant/components/august/sensor.py +++ b/homeassistant/components/august/sensor.py @@ -4,7 +4,6 @@ from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass -import logging from typing import Any, Generic, TypeVar, cast from yalexs.activity import ActivityType, LockOperationActivity @@ -45,8 +44,6 @@ from .const import ( ) from .entity import AugustEntityMixin -_LOGGER = logging.getLogger(__name__) - def _retrieve_device_battery_state(detail: LockDetail) -> int: """Get the latest state of the sensor.""" @@ -98,53 +95,28 @@ async def async_setup_entry( """Set up the August sensors.""" data = config_entry.runtime_data entities: list[SensorEntity] = [] - operation_sensors = [] - batteries: dict[str, list[Doorbell | Lock]] = { - "device_battery": [], - "linked_keypad_battery": [], - } - for device in data.doorbells: - batteries["device_battery"].append(device) + for device in data.locks: - batteries["device_battery"].append(device) - batteries["linked_keypad_battery"].append(device) - operation_sensors.append(device) - - for device in batteries["device_battery"]: detail = data.get_device_detail(device.device_id) - if detail is None or SENSOR_TYPE_DEVICE_BATTERY.value_fn(detail) is None: - _LOGGER.debug( - "Not adding battery sensor for %s because it is not present", - device.device_name, + entities.append(AugustOperatorSensor(data, device)) + if SENSOR_TYPE_DEVICE_BATTERY.value_fn(detail): + entities.append( + AugustBatterySensor[LockDetail]( + data, device, SENSOR_TYPE_DEVICE_BATTERY + ) ) - continue - _LOGGER.debug( - "Adding battery sensor for %s", - device.device_name, - ) - entities.append( - AugustBatterySensor[LockDetail](data, device, SENSOR_TYPE_DEVICE_BATTERY) - ) - - for device in batteries["linked_keypad_battery"]: - detail = data.get_device_detail(device.device_id) - - if detail.keypad is None: - _LOGGER.debug( - "Not adding keypad battery sensor for %s because it is not present", - device.device_name, + if keypad := detail.keypad: + entities.append( + AugustBatterySensor[KeypadDetail]( + data, keypad, SENSOR_TYPE_KEYPAD_BATTERY + ) ) - continue - _LOGGER.debug( - "Adding keypad battery sensor for %s", - device.device_name, - ) - keypad_battery_sensor = AugustBatterySensor[KeypadDetail]( - data, detail.keypad, SENSOR_TYPE_KEYPAD_BATTERY - ) - entities.append(keypad_battery_sensor) - entities.extend(AugustOperatorSensor(data, device) for device in operation_sensors) + entities.extend( + AugustBatterySensor[Doorbell](data, device, SENSOR_TYPE_DEVICE_BATTERY) + for device in data.doorbells + if SENSOR_TYPE_DEVICE_BATTERY.value_fn(data.get_device_detail(device.device_id)) + ) async_add_entities(entities) From 54f8b5afdfeafaab4133419214b67ee1b0d4de6a Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Tue, 18 Jun 2024 23:08:30 +0200 Subject: [PATCH 0809/1445] Add pulse counter frequency sensors to Shelly (#119898) * Add pulse counter frequency sensors * Cleaning --------- Co-authored-by: Maciej Bieniek <478555+bieniu@users.noreply.github.com> --- homeassistant/components/shelly/sensor.py | 18 +++++++++++++++ tests/components/shelly/conftest.py | 5 ++++- tests/components/shelly/test_sensor.py | 27 +++++++++++++++++++++++ 3 files changed, 49 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py index 7dea45c0c1f..743c7c7ff01 100644 --- a/homeassistant/components/shelly/sensor.py +++ b/homeassistant/components/shelly/sensor.py @@ -989,6 +989,24 @@ RPC_SENSORS: Final = { or status[key]["counts"].get("xtotal") is None ), ), + "counter_frequency": RpcSensorDescription( + key="input", + sub_key="counts", + name="Pulse counter frequency", + native_unit_of_measurement=UnitOfFrequency.HERTZ, + state_class=SensorStateClass.MEASUREMENT, + value=lambda status, _: status["freq"], + removal_condition=lambda config, status, key: (config[key]["enable"] is False), + ), + "counter_frequency_value": RpcSensorDescription( + key="input", + sub_key="counts", + name="Pulse counter frequency value", + value=lambda status, _: status["xfreq"], + removal_condition=lambda config, status, key: ( + config[key]["enable"] is False or status[key]["counts"].get("xfreq") is None + ), + ), } diff --git a/tests/components/shelly/conftest.py b/tests/components/shelly/conftest.py index 8e41cbe060f..a16cc62fbae 100644 --- a/tests/components/shelly/conftest.py +++ b/tests/components/shelly/conftest.py @@ -226,7 +226,10 @@ MOCK_STATUS_RPC = { "switch:0": {"output": True}, "input:0": {"id": 0, "state": None}, "input:1": {"id": 1, "percent": 89, "xpercent": 8.9}, - "input:2": {"id": 2, "counts": {"total": 56174, "xtotal": 561.74}}, + "input:2": { + "id": 2, + "counts": {"total": 56174, "xtotal": 561.74, "freq": 208.00, "xfreq": 6.11}, + }, "light:0": {"output": True, "brightness": 53.0}, "light:1": {"output": True, "brightness": 53.0}, "light:2": {"output": True, "brightness": 53.0}, diff --git a/tests/components/shelly/test_sensor.py b/tests/components/shelly/test_sensor.py index 036a5e0d70e..513bcd875e2 100644 --- a/tests/components/shelly/test_sensor.py +++ b/tests/components/shelly/test_sensor.py @@ -25,6 +25,7 @@ from homeassistant.const import ( STATE_UNAVAILABLE, STATE_UNKNOWN, UnitOfEnergy, + UnitOfFrequency, ) from homeassistant.core import HomeAssistant, State from homeassistant.helpers.device_registry import DeviceRegistry @@ -801,3 +802,29 @@ async def test_rpc_disabled_xtotal_counter( entity_id = f"{SENSOR_DOMAIN}.gas_counter_value" assert hass.states.get(entity_id) is None + + +async def test_rpc_pulse_counter_frequency_sensors( + hass: HomeAssistant, + mock_rpc_device: Mock, + entity_registry: EntityRegistry, +) -> None: + """Test RPC counter sensor.""" + await init_integration(hass, 2) + + entity_id = f"{SENSOR_DOMAIN}.gas_pulse_counter_frequency" + state = hass.states.get(entity_id) + assert state.state == "208.0" + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfFrequency.HERTZ + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT + + entry = entity_registry.async_get(entity_id) + assert entry + assert entry.unique_id == "123456789ABC-input:2-counter_frequency" + + entity_id = f"{SENSOR_DOMAIN}.gas_pulse_counter_frequency_value" + assert hass.states.get(entity_id).state == "6.11" + + entry = entity_registry.async_get(entity_id) + assert entry + assert entry.unique_id == "123456789ABC-input:2-counter_frequency_value" From 98140e0171eaba9386a5aeb013e1783e9b1850b2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 18 Jun 2024 17:35:38 -0500 Subject: [PATCH 0810/1445] Reduce duplicate code in august to create entities (#119934) --- .../components/august/binary_sensor.py | 46 +++---------------- homeassistant/components/august/entity.py | 23 +++++++++- homeassistant/components/august/sensor.py | 18 ++------ 3 files changed, 32 insertions(+), 55 deletions(-) diff --git a/homeassistant/components/august/binary_sensor.py b/homeassistant/components/august/binary_sensor.py index 27371f5e3f6..fe5c1f06505 100644 --- a/homeassistant/components/august/binary_sensor.py +++ b/homeassistant/components/august/binary_sensor.py @@ -13,8 +13,8 @@ from yalexs.activity import ( Activity, ActivityType, ) -from yalexs.doorbell import Doorbell, DoorbellDetail -from yalexs.lock import Lock, LockDetail, LockDoorStatus +from yalexs.doorbell import DoorbellDetail +from yalexs.lock import LockDetail, LockDoorStatus from yalexs.manager.const import ACTIVITY_UPDATE_INTERVAL from yalexs.util import update_lock_detail_from_activity @@ -29,7 +29,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_call_later from . import AugustConfigEntry, AugustData -from .entity import AugustEntityMixin +from .entity import AugustDescriptionEntity _LOGGER = logging.getLogger(__name__) @@ -180,21 +180,11 @@ async def async_setup_entry( async_add_entities(entities) -class AugustDoorBinarySensor(AugustEntityMixin, BinarySensorEntity): +class AugustDoorBinarySensor(AugustDescriptionEntity, BinarySensorEntity): """Representation of an August Door binary sensor.""" _attr_device_class = BinarySensorDeviceClass.DOOR - - def __init__( - self, - data: AugustData, - device: Lock, - description: BinarySensorEntityDescription, - ) -> None: - """Initialize the sensor.""" - super().__init__(data, device) - self.entity_description = description - self._attr_unique_id = f"{self._device_id}_{description.key}" + description: BinarySensorEntityDescription @callback def _update_from_data(self) -> None: @@ -219,29 +209,12 @@ class AugustDoorBinarySensor(AugustEntityMixin, BinarySensorEntity): self._attr_available = self._detail.bridge_is_online self._attr_is_on = self._detail.door_state == LockDoorStatus.OPEN - async def async_added_to_hass(self) -> None: - """Set the initial state when adding to hass.""" - self._update_from_data() - await super().async_added_to_hass() - -class AugustDoorbellBinarySensor(AugustEntityMixin, BinarySensorEntity): +class AugustDoorbellBinarySensor(AugustDescriptionEntity, BinarySensorEntity): """Representation of an August binary sensor.""" entity_description: AugustDoorbellBinarySensorEntityDescription - - def __init__( - self, - data: AugustData, - device: Doorbell | Lock, - description: AugustDoorbellBinarySensorEntityDescription, - ) -> None: - """Initialize the sensor.""" - super().__init__(data, device) - self.entity_description = description - self._check_for_off_update_listener: Callable[[], None] | None = None - self._data = data - self._attr_unique_id = f"{self._device_id}_{description.key}" + _check_for_off_update_listener: Callable[[], None] | None = None @callback def _update_from_data(self) -> None: @@ -280,11 +253,6 @@ class AugustDoorbellBinarySensor(AugustEntityMixin, BinarySensorEntity): self._check_for_off_update_listener() self._check_for_off_update_listener = None - async def async_added_to_hass(self) -> None: - """Call the mixin to subscribe and setup an async_track_point_in_utc_time to turn off the sensor if needed.""" - self._update_from_data() - await super().async_added_to_hass() - async def async_will_remove_from_hass(self) -> None: """When removing cancel any scheduled updates.""" self._cancel_any_pending_updates() diff --git a/homeassistant/components/august/entity.py b/homeassistant/components/august/entity.py index 47cb966bdc1..a13a319b568 100644 --- a/homeassistant/components/august/entity.py +++ b/homeassistant/components/august/entity.py @@ -3,6 +3,7 @@ from abc import abstractmethod from yalexs.doorbell import Doorbell, DoorbellDetail +from yalexs.keypad import KeypadDetail from yalexs.lock import Lock, LockDetail from yalexs.util import get_configuration_url @@ -10,7 +11,7 @@ from homeassistant.const import ATTR_CONNECTIONS from homeassistant.core import callback from homeassistant.helpers import device_registry as dr from homeassistant.helpers.device_registry import DeviceInfo -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import Entity, EntityDescription from . import DOMAIN, AugustData from .const import MANUFACTURER @@ -78,6 +79,26 @@ class AugustEntityMixin(Entity): ) +class AugustDescriptionEntity(AugustEntityMixin): + """An August entity with a description.""" + + def __init__( + self, + data: AugustData, + device: Doorbell | Lock | KeypadDetail, + description: EntityDescription, + ) -> None: + """Initialize an August entity with a description.""" + super().__init__(data, device) + self.entity_description = description + self._attr_unique_id = f"{self._device_id}_{description.key}" + + async def async_added_to_hass(self) -> None: + """Update data before adding to hass.""" + self._update_from_data() + await super().async_added_to_hass() + + def _remove_device_types(name: str, device_types: list[str]) -> str: """Strip device types from a string. diff --git a/homeassistant/components/august/sensor.py b/homeassistant/components/august/sensor.py index e5d29bb23d8..657368e019f 100644 --- a/homeassistant/components/august/sensor.py +++ b/homeassistant/components/august/sensor.py @@ -9,7 +9,7 @@ from typing import Any, Generic, TypeVar, cast from yalexs.activity import ActivityType, LockOperationActivity from yalexs.doorbell import Doorbell from yalexs.keypad import KeypadDetail -from yalexs.lock import Lock, LockDetail +from yalexs.lock import LockDetail from homeassistant.components.sensor import ( RestoreSensor, @@ -42,7 +42,7 @@ from .const import ( OPERATION_METHOD_REMOTE, OPERATION_METHOD_TAG, ) -from .entity import AugustEntityMixin +from .entity import AugustDescriptionEntity, AugustEntityMixin def _retrieve_device_battery_state(detail: LockDetail) -> int: @@ -215,25 +215,13 @@ class AugustOperatorSensor(AugustEntityMixin, RestoreSensor): self._operated_autorelock = last_state.attributes[ATTR_OPERATION_AUTORELOCK] -class AugustBatterySensor(AugustEntityMixin, SensorEntity, Generic[_T]): +class AugustBatterySensor(AugustDescriptionEntity, SensorEntity, Generic[_T]): """Representation of an August sensor.""" entity_description: AugustSensorEntityDescription[_T] _attr_device_class = SensorDeviceClass.BATTERY _attr_native_unit_of_measurement = PERCENTAGE - def __init__( - self, - data: AugustData, - device: Doorbell | Lock | KeypadDetail, - description: AugustSensorEntityDescription[_T], - ) -> None: - """Initialize the sensor.""" - super().__init__(data, device) - self.entity_description = description - self._attr_unique_id = f"{self._device_id}_{description.key}" - self._update_from_data() - @callback def _update_from_data(self) -> None: """Get the latest state of the sensor.""" From 39e5e517b0b6a6c0c41d8ffa6a790ac63ae227e3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 18 Jun 2024 17:35:55 -0500 Subject: [PATCH 0811/1445] Small cleanups to august (#119931) --- homeassistant/components/august/camera.py | 6 ++-- homeassistant/components/august/sensor.py | 36 +++++++++++------------ 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/august/camera.py b/homeassistant/components/august/camera.py index 76ccf9fa4dd..01baf7aa51a 100644 --- a/homeassistant/components/august/camera.py +++ b/homeassistant/components/august/camera.py @@ -45,6 +45,9 @@ class AugustCamera(AugustEntityMixin, Camera): _attr_translation_key = "camera" _attr_motion_detection_enabled = True _attr_brand = DEFAULT_NAME + _image_url: str | None = None + _content_token: str | None = None + _image_content: bytes | None = None def __init__( self, data: AugustData, device: Doorbell, session: ClientSession, timeout: int @@ -53,9 +56,6 @@ class AugustCamera(AugustEntityMixin, Camera): super().__init__(data, device) self._timeout = timeout self._session = session - self._image_url = None - self._content_token = None - self._image_content = None self._attr_unique_id = f"{self._device_id:s}_camera" @property diff --git a/homeassistant/components/august/sensor.py b/homeassistant/components/august/sensor.py index 657368e019f..862117319fa 100644 --- a/homeassistant/components/august/sensor.py +++ b/homeassistant/components/august/sensor.py @@ -125,16 +125,15 @@ class AugustOperatorSensor(AugustEntityMixin, RestoreSensor): """Representation of an August lock operation sensor.""" _attr_translation_key = "operator" + _operated_remote: bool | None = None + _operated_keypad: bool | None = None + _operated_manual: bool | None = None + _operated_tag: bool | None = None + _operated_autorelock: bool | None = None def __init__(self, data: AugustData, device) -> None: """Initialize the sensor.""" super().__init__(data, device) - self._operated_remote: bool | None = None - self._operated_keypad: bool | None = None - self._operated_manual: bool | None = None - self._operated_tag: bool | None = None - self._operated_autorelock: bool | None = None - self._operated_time = None self._attr_unique_id = f"{self._device_id}_lock_operator" self._update_from_data() @@ -201,18 +200,19 @@ class AugustOperatorSensor(AugustEntityMixin, RestoreSensor): return self._attr_native_value = last_sensor_state.native_value - if ATTR_ENTITY_PICTURE in last_state.attributes: - self._attr_entity_picture = last_state.attributes[ATTR_ENTITY_PICTURE] - if ATTR_OPERATION_REMOTE in last_state.attributes: - self._operated_remote = last_state.attributes[ATTR_OPERATION_REMOTE] - if ATTR_OPERATION_KEYPAD in last_state.attributes: - self._operated_keypad = last_state.attributes[ATTR_OPERATION_KEYPAD] - if ATTR_OPERATION_MANUAL in last_state.attributes: - self._operated_manual = last_state.attributes[ATTR_OPERATION_MANUAL] - if ATTR_OPERATION_TAG in last_state.attributes: - self._operated_tag = last_state.attributes[ATTR_OPERATION_TAG] - if ATTR_OPERATION_AUTORELOCK in last_state.attributes: - self._operated_autorelock = last_state.attributes[ATTR_OPERATION_AUTORELOCK] + last_attrs = last_state.attributes + if ATTR_ENTITY_PICTURE in last_attrs: + self._attr_entity_picture = last_attrs[ATTR_ENTITY_PICTURE] + if ATTR_OPERATION_REMOTE in last_attrs: + self._operated_remote = last_attrs[ATTR_OPERATION_REMOTE] + if ATTR_OPERATION_KEYPAD in last_attrs: + self._operated_keypad = last_attrs[ATTR_OPERATION_KEYPAD] + if ATTR_OPERATION_MANUAL in last_attrs: + self._operated_manual = last_attrs[ATTR_OPERATION_MANUAL] + if ATTR_OPERATION_TAG in last_attrs: + self._operated_tag = last_attrs[ATTR_OPERATION_TAG] + if ATTR_OPERATION_AUTORELOCK in last_attrs: + self._operated_autorelock = last_attrs[ATTR_OPERATION_AUTORELOCK] class AugustBatterySensor(AugustDescriptionEntity, SensorEntity, Generic[_T]): From 025d861e67e433e13d6455a42980643d1a6d6261 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 18 Jun 2024 17:36:07 -0500 Subject: [PATCH 0812/1445] Update yalexs to 6.1.0 (#119910) --- homeassistant/components/august/binary_sensor.py | 5 +---- homeassistant/components/august/lock.py | 6 +++--- homeassistant/components/august/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 7 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/august/binary_sensor.py b/homeassistant/components/august/binary_sensor.py index fe5c1f06505..50378b837a4 100644 --- a/homeassistant/components/august/binary_sensor.py +++ b/homeassistant/components/august/binary_sensor.py @@ -84,10 +84,7 @@ def _retrieve_ding_state(data: AugustData, detail: DoorbellDetail | LockDetail) if latest is None: return False - if ( - data.activity_stream.pubnub.connected - and latest.action == ACTION_DOORBELL_CALL_MISSED - ): + if data.push_updates_connected and latest.action == ACTION_DOORBELL_CALL_MISSED: return False return _activity_time_based_state(latest) diff --git a/homeassistant/components/august/lock.py b/homeassistant/components/august/lock.py index 47b1be52184..1e1664018b4 100644 --- a/homeassistant/components/august/lock.py +++ b/homeassistant/components/august/lock.py @@ -53,7 +53,7 @@ class AugustLock(AugustEntityMixin, RestoreEntity, LockEntity): async def async_lock(self, **kwargs: Any) -> None: """Lock the device.""" assert self._data.activity_stream is not None - if self._data.activity_stream.pubnub.connected: + if self._data.push_updates_connected: await self._data.async_lock_async(self._device_id, self._hyper_bridge) return await self._call_lock_operation(self._data.async_lock) @@ -61,7 +61,7 @@ class AugustLock(AugustEntityMixin, RestoreEntity, LockEntity): async def async_open(self, **kwargs: Any) -> None: """Open/unlatch the device.""" assert self._data.activity_stream is not None - if self._data.activity_stream.pubnub.connected: + if self._data.push_updates_connected: await self._data.async_unlatch_async(self._device_id, self._hyper_bridge) return await self._call_lock_operation(self._data.async_unlatch) @@ -69,7 +69,7 @@ class AugustLock(AugustEntityMixin, RestoreEntity, LockEntity): async def async_unlock(self, **kwargs: Any) -> None: """Unlock the device.""" assert self._data.activity_stream is not None - if self._data.activity_stream.pubnub.connected: + if self._data.push_updates_connected: await self._data.async_unlock_async(self._device_id, self._hyper_bridge) return await self._call_lock_operation(self._data.async_unlock) diff --git a/homeassistant/components/august/manifest.json b/homeassistant/components/august/manifest.json index d4bad52c339..923cb11c248 100644 --- a/homeassistant/components/august/manifest.json +++ b/homeassistant/components/august/manifest.json @@ -28,5 +28,5 @@ "documentation": "https://www.home-assistant.io/integrations/august", "iot_class": "cloud_push", "loggers": ["pubnub", "yalexs"], - "requirements": ["yalexs==6.0.0", "yalexs-ble==2.4.2"] + "requirements": ["yalexs==6.1.0", "yalexs-ble==2.4.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 8ac84a8608d..7415ffa9e33 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2939,7 +2939,7 @@ yalesmartalarmclient==0.3.9 yalexs-ble==2.4.2 # homeassistant.components.august -yalexs==6.0.0 +yalexs==6.1.0 # homeassistant.components.yeelight yeelight==0.7.14 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3b855490169..20e5977656d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2295,7 +2295,7 @@ yalesmartalarmclient==0.3.9 yalexs-ble==2.4.2 # homeassistant.components.august -yalexs==6.0.0 +yalexs==6.1.0 # homeassistant.components.yeelight yeelight==0.7.14 From 8f3dcd655762c9d9e6358de40ce06d32bb3447f7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 18 Jun 2024 18:21:19 -0500 Subject: [PATCH 0813/1445] Cleanup august dataclasses (#119938) --- homeassistant/components/august/config_flow.py | 2 +- homeassistant/components/august/sensor.py | 11 ++--------- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/august/config_flow.py b/homeassistant/components/august/config_flow.py index 75543311fdd..18c15ad61a1 100644 --- a/homeassistant/components/august/config_flow.py +++ b/homeassistant/components/august/config_flow.py @@ -66,7 +66,7 @@ async def async_validate_input( } -@dataclass +@dataclass(slots=True) class ValidateResult: """Result from validation.""" diff --git a/homeassistant/components/august/sensor.py b/homeassistant/components/august/sensor.py index 862117319fa..59f4d0cf33f 100644 --- a/homeassistant/components/august/sensor.py +++ b/homeassistant/components/august/sensor.py @@ -58,20 +58,13 @@ def _retrieve_linked_keypad_battery_state(detail: KeypadDetail) -> int | None: _T = TypeVar("_T", LockDetail, KeypadDetail) -@dataclass(frozen=True) -class AugustRequiredKeysMixin(Generic[_T]): +@dataclass(frozen=True, kw_only=True) +class AugustSensorEntityDescription(SensorEntityDescription, Generic[_T]): """Mixin for required keys.""" value_fn: Callable[[_T], int | None] -@dataclass(frozen=True) -class AugustSensorEntityDescription( - SensorEntityDescription, AugustRequiredKeysMixin[_T] -): - """Describes August sensor entity.""" - - SENSOR_TYPE_DEVICE_BATTERY = AugustSensorEntityDescription[LockDetail]( key="device_battery", entity_category=EntityCategory.DIAGNOSTIC, From 60e64d14afbc4de37eca2e34e618561535ded982 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 18 Jun 2024 18:52:41 -0500 Subject: [PATCH 0814/1445] Bump yalexs to 6.3.0 to move camera logic to the lib (#119941) --- homeassistant/components/august/camera.py | 25 +++---------------- homeassistant/components/august/data.py | 8 ------ homeassistant/components/august/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/august/mocks.py | 2 +- 6 files changed, 8 insertions(+), 33 deletions(-) diff --git a/homeassistant/components/august/camera.py b/homeassistant/components/august/camera.py index 01baf7aa51a..f8541388ec2 100644 --- a/homeassistant/components/august/camera.py +++ b/homeassistant/components/august/camera.py @@ -6,8 +6,7 @@ import logging from aiohttp import ClientSession from yalexs.activity import ActivityType -from yalexs.const import Brand -from yalexs.doorbell import ContentTokenExpired, Doorbell +from yalexs.doorbell import Doorbell from yalexs.util import update_doorbell_image_from_activity from homeassistant.components.camera import Camera @@ -46,7 +45,6 @@ class AugustCamera(AugustEntityMixin, Camera): _attr_motion_detection_enabled = True _attr_brand = DEFAULT_NAME _image_url: str | None = None - _content_token: str | None = None _image_content: bytes | None = None def __init__( @@ -91,24 +89,9 @@ class AugustCamera(AugustEntityMixin, Camera): self._update_from_data() if self._image_url is not self._detail.image_url: - self._image_url = self._detail.image_url - self._content_token = self._detail.content_token or self._content_token - _LOGGER.debug( - "calling doorbell async_get_doorbell_image, %s", - self._detail.device_name, + self._image_content = await self._data.async_get_doorbell_image( + self._device_id, self._session, timeout=self._timeout ) - try: - self._image_content = await self._detail.async_get_doorbell_image( - self._session, timeout=self._timeout - ) - except ContentTokenExpired: - if self._data.brand == Brand.YALE_HOME: - _LOGGER.debug( - "Error fetching camera image, updating content-token from api to retry" - ) - await self._async_update() - self._image_content = await self._detail.async_get_doorbell_image( - self._session, timeout=self._timeout - ) + self._image_url = self._detail.image_url return self._image_content diff --git a/homeassistant/components/august/data.py b/homeassistant/components/august/data.py index 59c37dfd2b1..55c7c2bfa03 100644 --- a/homeassistant/components/august/data.py +++ b/homeassistant/components/august/data.py @@ -2,9 +2,7 @@ from __future__ import annotations -from yalexs.const import DEFAULT_BRAND from yalexs.lock import LockDetail -from yalexs.manager.const import CONF_BRAND from yalexs.manager.data import YaleXSData from yalexs_ble import YaleXSBLEDiscovery @@ -51,14 +49,8 @@ class AugustData(YaleXSData): ) -> None: """Init August data object.""" self._hass = hass - self._config_entry = config_entry super().__init__(august_gateway, HomeAssistantError) - @property - def brand(self) -> str: - """Brand of the device.""" - return self._config_entry.data.get(CONF_BRAND, DEFAULT_BRAND) - @callback def async_offline_key_discovered(self, detail: LockDetail) -> None: """Handle offline key discovery.""" diff --git a/homeassistant/components/august/manifest.json b/homeassistant/components/august/manifest.json index 923cb11c248..d4f82fa0aa1 100644 --- a/homeassistant/components/august/manifest.json +++ b/homeassistant/components/august/manifest.json @@ -28,5 +28,5 @@ "documentation": "https://www.home-assistant.io/integrations/august", "iot_class": "cloud_push", "loggers": ["pubnub", "yalexs"], - "requirements": ["yalexs==6.1.0", "yalexs-ble==2.4.2"] + "requirements": ["yalexs==6.3.0", "yalexs-ble==2.4.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 7415ffa9e33..06cff18617c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2939,7 +2939,7 @@ yalesmartalarmclient==0.3.9 yalexs-ble==2.4.2 # homeassistant.components.august -yalexs==6.1.0 +yalexs==6.3.0 # homeassistant.components.yeelight yeelight==0.7.14 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 20e5977656d..a83481f2316 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2295,7 +2295,7 @@ yalesmartalarmclient==0.3.9 yalexs-ble==2.4.2 # homeassistant.components.august -yalexs==6.1.0 +yalexs==6.3.0 # homeassistant.components.yeelight yeelight==0.7.14 diff --git a/tests/components/august/mocks.py b/tests/components/august/mocks.py index 2b9b401e107..62c01d38d0c 100644 --- a/tests/components/august/mocks.py +++ b/tests/components/august/mocks.py @@ -213,7 +213,7 @@ async def _create_august_api_with_devices( async def _mock_setup_august_with_api_side_effects( hass, api_call_side_effects, pubnub, brand=Brand.AUGUST ): - api_instance = MagicMock(name="Api") + api_instance = MagicMock(name="Api", brand=brand) if api_call_side_effects["get_lock_detail"]: type(api_instance).async_get_lock_detail = AsyncMock( From ef51fc0d97fe7ab9d68f5204907804f67c353df0 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Wed, 19 Jun 2024 02:25:11 +0200 Subject: [PATCH 0815/1445] Remove code slated for deletion in integral (#119935) * Remove code slated for deletion in integral --- .../components/integration/sensor.py | 21 +---- tests/components/integration/test_sensor.py | 78 +------------------ 2 files changed, 3 insertions(+), 96 deletions(-) diff --git a/homeassistant/components/integration/sensor.py b/homeassistant/components/integration/sensor.py index e935dd5dc14..02451773558 100644 --- a/homeassistant/components/integration/sensor.py +++ b/homeassistant/components/integration/sensor.py @@ -5,7 +5,7 @@ from __future__ import annotations from abc import ABC, abstractmethod from dataclasses import dataclass from datetime import UTC, datetime, timedelta -from decimal import Decimal, DecimalException, InvalidOperation +from decimal import Decimal, InvalidOperation from enum import Enum import logging from typing import Any, Final, Self @@ -27,7 +27,6 @@ from homeassistant.const import ( CONF_NAME, CONF_UNIQUE_ID, STATE_UNAVAILABLE, - STATE_UNKNOWN, UnitOfTime, ) from homeassistant.core import ( @@ -428,24 +427,6 @@ class IntegrationSensor(RestoreSensor): self._state, self._last_valid_state, ) - elif (state := await self.async_get_last_state()) is not None: - # legacy to be removed on 2023.10 (we are keeping this to avoid losing data during the transition) - if state.state in [STATE_UNAVAILABLE, STATE_UNKNOWN]: - if state.state == STATE_UNAVAILABLE: - self._attr_available = False - else: - try: - self._state = Decimal(state.state) - except (DecimalException, ValueError) as err: - _LOGGER.warning( - "%s could not restore last state %s: %s", - self.entity_id, - state.state, - err, - ) - - self._attr_device_class = state.attributes.get(ATTR_DEVICE_CLASS) - self._unit_of_measurement = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) if self._max_sub_interval is not None: source_state = self.hass.states.get(self._sensor_source_id) diff --git a/tests/components/integration/test_sensor.py b/tests/components/integration/test_sensor.py index 500d567dca4..1a729f6254e 100644 --- a/tests/components/integration/test_sensor.py +++ b/tests/components/integration/test_sensor.py @@ -30,7 +30,6 @@ import homeassistant.util.dt as dt_util from tests.common import ( MockConfigEntry, async_fire_time_changed, - mock_restore_cache, mock_restore_cache_with_extra_data, ) @@ -146,42 +145,6 @@ async def test_state(hass: HomeAssistant, method) -> None: async def test_restore_state(hass: HomeAssistant) -> None: - """Test integration sensor state is restored correctly.""" - mock_restore_cache( - hass, - ( - State( - "sensor.integration", - "100.0", - { - "device_class": SensorDeviceClass.ENERGY, - "unit_of_measurement": UnitOfEnergy.KILO_WATT_HOUR, - }, - ), - ), - ) - - config = { - "sensor": { - "platform": "integration", - "name": "integration", - "source": "sensor.power", - "round": 2, - } - } - - assert await async_setup_component(hass, "sensor", config) - await hass.async_block_till_done() - - state = hass.states.get("sensor.integration") - assert state - assert state.state == "100.00" - assert state.attributes.get("unit_of_measurement") == UnitOfEnergy.KILO_WATT_HOUR - assert state.attributes.get("device_class") == SensorDeviceClass.ENERGY - assert state.attributes.get("last_good_state") is None - - -async def test_restore_unavailable_state(hass: HomeAssistant) -> None: """Test integration sensor state is restored correctly.""" mock_restore_cache_with_extra_data( hass, @@ -237,9 +200,7 @@ async def test_restore_unavailable_state(hass: HomeAssistant) -> None: }, ], ) -async def test_restore_unavailable_state_failed( - hass: HomeAssistant, extra_attributes -) -> None: +async def test_restore_state_failed(hass: HomeAssistant, extra_attributes) -> None: """Test integration sensor state is restored correctly.""" mock_restore_cache_with_extra_data( hass, @@ -271,42 +232,7 @@ async def test_restore_unavailable_state_failed( state = hass.states.get("sensor.integration") assert state - assert state.state == STATE_UNAVAILABLE - - -async def test_restore_state_failed(hass: HomeAssistant) -> None: - """Test integration sensor state is restored correctly.""" - mock_restore_cache( - hass, - ( - State( - "sensor.integration", - "INVALID", - { - "last_reset": "2019-10-06T21:00:00.000000", - }, - ), - ), - ) - - config = { - "sensor": { - "platform": "integration", - "name": "integration", - "source": "sensor.power", - } - } - - assert await async_setup_component(hass, "sensor", config) - await hass.async_block_till_done() - - state = hass.states.get("sensor.integration") - assert state - assert state.state == "unknown" - assert state.attributes.get("unit_of_measurement") is None - assert state.attributes.get("state_class") is SensorStateClass.TOTAL - - assert "device_class" not in state.attributes + assert state.state == STATE_UNKNOWN async def test_trapezoidal(hass: HomeAssistant) -> None: From c686eda3051bc60e7fe85db26b61c2d3abb28b0d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 18 Jun 2024 20:39:32 -0500 Subject: [PATCH 0816/1445] Reduce duplicate code in baf for entities with descriptions (#119945) * Reduce duplicate code in baf for entities with descriptions * Reduce duplicate code in baf for entities with descriptions * no cover * no cover --- .coveragerc | 1 + homeassistant/components/baf/binary_sensor.py | 21 ++++++------------- homeassistant/components/baf/entity.py | 12 ++++++++++- homeassistant/components/baf/number.py | 10 ++------- homeassistant/components/baf/sensor.py | 13 +++--------- homeassistant/components/baf/switch.py | 10 ++------- 6 files changed, 25 insertions(+), 42 deletions(-) diff --git a/.coveragerc b/.coveragerc index d8d8bbdf80d..eeffb341fd8 100644 --- a/.coveragerc +++ b/.coveragerc @@ -121,6 +121,7 @@ omit = homeassistant/components/awair/coordinator.py homeassistant/components/azure_service_bus/* homeassistant/components/baf/__init__.py + homeassistant/components/baf/binary_sensor.py homeassistant/components/baf/climate.py homeassistant/components/baf/entity.py homeassistant/components/baf/fan.py diff --git a/homeassistant/components/baf/binary_sensor.py b/homeassistant/components/baf/binary_sensor.py index b1076a99f8a..7c855711712 100644 --- a/homeassistant/components/baf/binary_sensor.py +++ b/homeassistant/components/baf/binary_sensor.py @@ -17,7 +17,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import BAFConfigEntry -from .entity import BAFEntity +from .entity import BAFDescriptionEntity @dataclass(frozen=True, kw_only=True) @@ -45,27 +45,18 @@ async def async_setup_entry( ) -> None: """Set up BAF binary sensors.""" device = entry.runtime_data - sensors_descriptions: list[BAFBinarySensorDescription] = [] if device.has_occupancy: - sensors_descriptions.extend(OCCUPANCY_SENSORS) - async_add_entities( - BAFBinarySensor(device, description) for description in sensors_descriptions - ) + async_add_entities( + BAFBinarySensor(device, description) for description in OCCUPANCY_SENSORS + ) -class BAFBinarySensor(BAFEntity, BinarySensorEntity): +class BAFBinarySensor(BAFDescriptionEntity, BinarySensorEntity): """BAF binary sensor.""" entity_description: BAFBinarySensorDescription - def __init__(self, device: Device, description: BAFBinarySensorDescription) -> None: - """Initialize the entity.""" - self.entity_description = description - super().__init__(device) - self._attr_unique_id = f"{self._device.mac_address}-{description.key}" - @callback def _async_update_attrs(self) -> None: """Update attrs from device.""" - description = self.entity_description - self._attr_is_on = description.value_fn(self._device) + self._attr_is_on = self.entity_description.value_fn(self._device) diff --git a/homeassistant/components/baf/entity.py b/homeassistant/components/baf/entity.py index 487e601b542..6bb9dbfeca7 100644 --- a/homeassistant/components/baf/entity.py +++ b/homeassistant/components/baf/entity.py @@ -7,7 +7,7 @@ from aiobafi6 import Device from homeassistant.core import callback from homeassistant.helpers import device_registry as dr from homeassistant.helpers.device_registry import DeviceInfo, format_mac -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import Entity, EntityDescription class BAFEntity(Entity): @@ -47,3 +47,13 @@ class BAFEntity(Entity): async def async_will_remove_from_hass(self) -> None: """Remove data updated listener after this object has been initialized.""" self._device.remove_callback(self._async_update_from_device) + + +class BAFDescriptionEntity(BAFEntity): + """Base class for baf entities that use an entity description.""" + + def __init__(self, device: Device, description: EntityDescription) -> None: + """Initialize the entity.""" + self.entity_description = description + super().__init__(device) + self._attr_unique_id = f"{device.mac_address}-{description.key}" diff --git a/homeassistant/components/baf/number.py b/homeassistant/components/baf/number.py index bf9e837eea1..a2e5e704e4d 100644 --- a/homeassistant/components/baf/number.py +++ b/homeassistant/components/baf/number.py @@ -19,7 +19,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import BAFConfigEntry from .const import HALF_DAY_SECS, ONE_DAY_SECS, ONE_MIN_SECS, SPEED_RANGE -from .entity import BAFEntity +from .entity import BAFDescriptionEntity @dataclass(frozen=True, kw_only=True) @@ -130,17 +130,11 @@ async def async_setup_entry( async_add_entities(BAFNumber(device, description) for description in descriptions) -class BAFNumber(BAFEntity, NumberEntity): +class BAFNumber(BAFDescriptionEntity, NumberEntity): """BAF number.""" entity_description: BAFNumberDescription - def __init__(self, device: Device, description: BAFNumberDescription) -> None: - """Initialize the entity.""" - self.entity_description = description - super().__init__(device) - self._attr_unique_id = f"{self._device.mac_address}-{description.key}" - @callback def _async_update_attrs(self) -> None: """Update attrs from device.""" diff --git a/homeassistant/components/baf/sensor.py b/homeassistant/components/baf/sensor.py index a97e2945564..7e664254a38 100644 --- a/homeassistant/components/baf/sensor.py +++ b/homeassistant/components/baf/sensor.py @@ -24,7 +24,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import BAFConfigEntry -from .entity import BAFEntity +from .entity import BAFDescriptionEntity @dataclass(frozen=True, kw_only=True) @@ -111,19 +111,12 @@ async def async_setup_entry( ) -class BAFSensor(BAFEntity, SensorEntity): +class BAFSensor(BAFDescriptionEntity, SensorEntity): """BAF sensor.""" entity_description: BAFSensorDescription - def __init__(self, device: Device, description: BAFSensorDescription) -> None: - """Initialize the entity.""" - self.entity_description = description - super().__init__(device) - self._attr_unique_id = f"{self._device.mac_address}-{description.key}" - @callback def _async_update_attrs(self) -> None: """Update attrs from device.""" - description = self.entity_description - self._attr_native_value = description.value_fn(self._device) + self._attr_native_value = self.entity_description.value_fn(self._device) diff --git a/homeassistant/components/baf/switch.py b/homeassistant/components/baf/switch.py index 789ea365d6d..e18e26ddcaa 100644 --- a/homeassistant/components/baf/switch.py +++ b/homeassistant/components/baf/switch.py @@ -14,7 +14,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import BAFConfigEntry -from .entity import BAFEntity +from .entity import BAFDescriptionEntity @dataclass(frozen=True, kw_only=True) @@ -118,17 +118,11 @@ async def async_setup_entry( async_add_entities(BAFSwitch(device, description) for description in descriptions) -class BAFSwitch(BAFEntity, SwitchEntity): +class BAFSwitch(BAFDescriptionEntity, SwitchEntity): """BAF switch component.""" entity_description: BAFSwitchDescription - def __init__(self, device: Device, description: BAFSwitchDescription) -> None: - """Initialize the entity.""" - self.entity_description = description - super().__init__(device) - self._attr_unique_id = f"{self._device.mac_address}-{description.key}" - @callback def _async_update_attrs(self) -> None: """Update attrs from device.""" From f88b24f8a4a395e8b13cc18962fe49ab15af1ce8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 18 Jun 2024 20:40:21 -0500 Subject: [PATCH 0817/1445] Combine statements that return the same result in august binary_sensor (#119944) --- .../components/august/binary_sensor.py | 31 +++++++++---------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/august/binary_sensor.py b/homeassistant/components/august/binary_sensor.py index 50378b837a4..beb899a174b 100644 --- a/homeassistant/components/august/binary_sensor.py +++ b/homeassistant/components/august/binary_sensor.py @@ -63,30 +63,29 @@ def _retrieve_motion_state(data: AugustData, detail: DoorbellDetail) -> bool: return _activity_time_based_state(latest) -def _retrieve_image_capture_state(data: AugustData, detail: DoorbellDetail) -> bool: - assert data.activity_stream is not None - latest = data.activity_stream.get_latest_device_activity( - detail.device_id, {ActivityType.DOORBELL_IMAGE_CAPTURE} - ) +_IMAGE_ACTIVITIES = {ActivityType.DOORBELL_IMAGE_CAPTURE} + +def _retrieve_image_capture_state(data: AugustData, detail: DoorbellDetail) -> bool: + stream = data.activity_stream + assert stream is not None + latest = stream.get_latest_device_activity(detail.device_id, _IMAGE_ACTIVITIES) if latest is None: return False - return _activity_time_based_state(latest) +_RING_ACTIVITIES = {ActivityType.DOORBELL_DING} + + def _retrieve_ding_state(data: AugustData, detail: DoorbellDetail | LockDetail) -> bool: - assert data.activity_stream is not None - latest = data.activity_stream.get_latest_device_activity( - detail.device_id, {ActivityType.DOORBELL_DING} - ) - - if latest is None: + stream = data.activity_stream + assert stream is not None + latest = stream.get_latest_device_activity(detail.device_id, _RING_ACTIVITIES) + if latest is None or ( + data.push_updates_connected and latest.action == ACTION_DOORBELL_CALL_MISSED + ): return False - - if data.push_updates_connected and latest.action == ACTION_DOORBELL_CALL_MISSED: - return False - return _activity_time_based_state(latest) From b11801df50fd164cb6a0825dc71a0ec4e5eb0afe Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 18 Jun 2024 20:43:06 -0500 Subject: [PATCH 0818/1445] Reduce code needed to set august unique ids (#119942) * Reduce code needed to set august unique ids Set the unique ids in the base class since they always use the same format * Reduce code needed to set august unique ids Set the unique ids in the base class since they always use the same format --- homeassistant/components/august/button.py | 11 ++--------- homeassistant/components/august/camera.py | 3 +-- homeassistant/components/august/entity.py | 14 ++++++-------- homeassistant/components/august/lock.py | 4 +--- homeassistant/components/august/sensor.py | 10 ++-------- 5 files changed, 12 insertions(+), 30 deletions(-) diff --git a/homeassistant/components/august/button.py b/homeassistant/components/august/button.py index d7aefca5d3c..406475db601 100644 --- a/homeassistant/components/august/button.py +++ b/homeassistant/components/august/button.py @@ -1,12 +1,10 @@ """Support for August buttons.""" -from yalexs.lock import Lock - from homeassistant.components.button import ButtonEntity from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import AugustConfigEntry, AugustData +from . import AugustConfigEntry from .entity import AugustEntityMixin @@ -17,7 +15,7 @@ async def async_setup_entry( ) -> None: """Set up August lock wake buttons.""" data = config_entry.runtime_data - async_add_entities(AugustWakeLockButton(data, lock) for lock in data.locks) + async_add_entities(AugustWakeLockButton(data, lock, "wake") for lock in data.locks) class AugustWakeLockButton(AugustEntityMixin, ButtonEntity): @@ -25,11 +23,6 @@ class AugustWakeLockButton(AugustEntityMixin, ButtonEntity): _attr_translation_key = "wake" - def __init__(self, data: AugustData, device: Lock) -> None: - """Initialize the lock wake button.""" - super().__init__(data, device) - self._attr_unique_id = f"{self._device_id}_wake" - async def async_press(self) -> None: """Wake the device.""" await self._data.async_status_async(self._device_id, self._hyper_bridge) diff --git a/homeassistant/components/august/camera.py b/homeassistant/components/august/camera.py index f8541388ec2..ba29b2905d3 100644 --- a/homeassistant/components/august/camera.py +++ b/homeassistant/components/august/camera.py @@ -51,10 +51,9 @@ class AugustCamera(AugustEntityMixin, Camera): self, data: AugustData, device: Doorbell, session: ClientSession, timeout: int ) -> None: """Initialize an August security camera.""" - super().__init__(data, device) + super().__init__(data, device, "camera") self._timeout = timeout self._session = session - self._attr_unique_id = f"{self._device_id:s}_camera" @property def is_recording(self) -> bool: diff --git a/homeassistant/components/august/entity.py b/homeassistant/components/august/entity.py index a13a319b568..960dddbc005 100644 --- a/homeassistant/components/august/entity.py +++ b/homeassistant/components/august/entity.py @@ -25,12 +25,15 @@ class AugustEntityMixin(Entity): _attr_should_poll = False _attr_has_entity_name = True - def __init__(self, data: AugustData, device: Doorbell | Lock) -> None: + def __init__( + self, data: AugustData, device: Doorbell | Lock | KeypadDetail, unique_id: str + ) -> None: """Initialize an August device.""" super().__init__() self._data = data self._device = device detail = self._detail + self._attr_unique_id = f"{device.device_id}_{unique_id}" self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, self._device_id)}, manufacturer=MANUFACTURER, @@ -77,6 +80,7 @@ class AugustEntityMixin(Entity): self._device_id, self._update_from_data_and_write_state ) ) + self._update_from_data() class AugustDescriptionEntity(AugustEntityMixin): @@ -89,14 +93,8 @@ class AugustDescriptionEntity(AugustEntityMixin): description: EntityDescription, ) -> None: """Initialize an August entity with a description.""" - super().__init__(data, device) + super().__init__(data, device, description.key) self.entity_description = description - self._attr_unique_id = f"{self._device_id}_{description.key}" - - async def async_added_to_hass(self) -> None: - """Update data before adding to hass.""" - self._update_from_data() - await super().async_added_to_hass() def _remove_device_types(name: str, device_types: list[str]) -> str: diff --git a/homeassistant/components/august/lock.py b/homeassistant/components/august/lock.py index 1e1664018b4..10d32ebd323 100644 --- a/homeassistant/components/august/lock.py +++ b/homeassistant/components/august/lock.py @@ -44,11 +44,9 @@ class AugustLock(AugustEntityMixin, RestoreEntity, LockEntity): def __init__(self, data: AugustData, device: Lock) -> None: """Initialize the lock.""" - super().__init__(data, device) - self._attr_unique_id = f"{self._device_id:s}_lock" + super().__init__(data, device, "lock") if self._detail.unlatch_supported: self._attr_supported_features = LockEntityFeature.OPEN - self._update_from_data() async def async_lock(self, **kwargs: Any) -> None: """Lock the device.""" diff --git a/homeassistant/components/august/sensor.py b/homeassistant/components/august/sensor.py index 59f4d0cf33f..847d7f32a5a 100644 --- a/homeassistant/components/august/sensor.py +++ b/homeassistant/components/august/sensor.py @@ -27,7 +27,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import AugustConfigEntry, AugustData +from . import AugustConfigEntry from .const import ( ATTR_OPERATION_AUTORELOCK, ATTR_OPERATION_KEYPAD, @@ -91,7 +91,7 @@ async def async_setup_entry( for device in data.locks: detail = data.get_device_detail(device.device_id) - entities.append(AugustOperatorSensor(data, device)) + entities.append(AugustOperatorSensor(data, device, "lock_operator")) if SENSOR_TYPE_DEVICE_BATTERY.value_fn(detail): entities.append( AugustBatterySensor[LockDetail]( @@ -124,12 +124,6 @@ class AugustOperatorSensor(AugustEntityMixin, RestoreSensor): _operated_tag: bool | None = None _operated_autorelock: bool | None = None - def __init__(self, data: AugustData, device) -> None: - """Initialize the sensor.""" - super().__init__(data, device) - self._attr_unique_id = f"{self._device_id}_lock_operator" - self._update_from_data() - @callback def _update_from_data(self) -> None: """Get the latest state of the sensor and update activity.""" From 6a3778c48eb0db8cbc59406f27367646e4dbc7f3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 18 Jun 2024 20:51:24 -0500 Subject: [PATCH 0819/1445] Deprecate register_static_path in favor of async_register_static_paths (#119895) * Deprecate register_static_path in favor of async_register_static_path `hass.http.register_static_path` is deprecated because it does blocking I/O in the event loop, instead call `await hass.http.async_register_static_path([StaticPathConfig(url_path, path, cache_headers)])` The arguments to `async_register_static_path` are the same as `register_static_path` except they are wrapped in the `StaticPathConfig` dataclass and an iterable of them is accepted to allow registering multiple paths at once to avoid multiple executor jobs. * add date * spacing --- homeassistant/components/http/__init__.py | 12 +++++++++++- tests/components/http/test_init.py | 21 +++++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index fae50f97a33..38f0b628b2c 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -34,7 +34,7 @@ from homeassistant.components.network import async_get_source_ip from homeassistant.const import EVENT_HOMEASSISTANT_STOP, SERVER_PORT from homeassistant.core import Event, HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import storage +from homeassistant.helpers import frame, storage import homeassistant.helpers.config_validation as cv from homeassistant.helpers.http import ( KEY_ALLOW_CONFIGURED_CORS, @@ -480,6 +480,16 @@ class HomeAssistantHTTP: self, url_path: str, path: str, cache_headers: bool = True ) -> None: """Register a folder or file to serve as a static path.""" + frame.report( + "calls hass.http.register_static_path which is deprecated because " + "it does blocking I/O in the event loop, instead " + "call `await hass.http.async_register_static_path(" + f'[StaticPathConfig("{url_path}", "{path}", {cache_headers})])`; ' + "This function will be removed in 2025.7", + exclude_integrations={"http"}, + error_if_core=False, + error_if_integration=False, + ) configs = [StaticPathConfig(url_path, path, cache_headers)] resources = self._make_static_resources(configs) self._async_register_static_paths(configs, resources) diff --git a/tests/components/http/test_init.py b/tests/components/http/test_init.py index 489be0878b3..995be3954d9 100644 --- a/tests/components/http/test_init.py +++ b/tests/components/http/test_init.py @@ -526,3 +526,24 @@ async def test_logging( response = await client.get("/api/states/logging.entity") assert response.status == HTTPStatus.OK assert "GET /api/states/logging.entity" not in caplog.text + + +async def test_register_static_paths( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test registering a static path with old api.""" + assert await async_setup_component(hass, "frontend", {}) + path = str(Path(__file__).parent) + hass.http.register_static_path("/something", path) + client = await hass_client() + resp = await client.get("/something/__init__.py") + assert resp.status == HTTPStatus.OK + + assert ( + "Detected code that calls hass.http.register_static_path " + "which is deprecated because it does blocking I/O in the " + "event loop, instead call " + "`await hass.http.async_register_static_path" + ) in caplog.text From 5ae13c2ae2a4afe4a04e2fa2320f6ec2e8b97e9a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 18 Jun 2024 20:52:36 -0500 Subject: [PATCH 0820/1445] Make use_device_name a cached_property in the base entity class (#119758) There is a small risk that _attr_name or entity_description would not be set before state was first written, but that would likely be a bug. --- homeassistant/helpers/entity.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index aab6fa9f59b..cf910a5cba8 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -581,7 +581,7 @@ class Entity( """Return a unique ID.""" return self._attr_unique_id - @property + @cached_property def use_device_name(self) -> bool: """Return if this entity does not have its own name. @@ -589,14 +589,12 @@ class Entity( """ if hasattr(self, "_attr_name"): return not self._attr_name - - if name_translation_key := self._name_translation_key: - if name_translation_key in self.platform.platform_translations: - return False - + if ( + name_translation_key := self._name_translation_key + ) and name_translation_key in self.platform.platform_translations: + return False if hasattr(self, "entity_description"): return not self.entity_description.name - return not self.name @cached_property From a642454e6f06bd22ea261de70339d767698917fb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 19 Jun 2024 01:09:04 -0500 Subject: [PATCH 0821/1445] Bump sqlalchemy to 2.0.31 (#119951) --- homeassistant/components/recorder/manifest.json | 2 +- homeassistant/components/sql/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- pyproject.toml | 2 +- requirements.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/recorder/manifest.json b/homeassistant/components/recorder/manifest.json index 5b06c1720dc..febd1bb8c7c 100644 --- a/homeassistant/components/recorder/manifest.json +++ b/homeassistant/components/recorder/manifest.json @@ -8,7 +8,7 @@ "iot_class": "local_push", "quality_scale": "internal", "requirements": [ - "SQLAlchemy==2.0.30", + "SQLAlchemy==2.0.31", "fnv-hash-fast==0.5.0", "psutil-home-assistant==0.0.1" ] diff --git a/homeassistant/components/sql/manifest.json b/homeassistant/components/sql/manifest.json index f0f1be417ff..dcb5f47829c 100644 --- a/homeassistant/components/sql/manifest.json +++ b/homeassistant/components/sql/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sql", "iot_class": "local_polling", - "requirements": ["SQLAlchemy==2.0.30", "sqlparse==0.5.0"] + "requirements": ["SQLAlchemy==2.0.31", "sqlparse==0.5.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index ae1a95fc5b1..fb0517d9298 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -54,7 +54,7 @@ PyTurboJPEG==1.7.1 pyudev==0.24.1 PyYAML==6.0.1 requests==2.32.3 -SQLAlchemy==2.0.30 +SQLAlchemy==2.0.31 typing-extensions>=4.12.2,<5.0 ulid-transform==0.9.0 urllib3>=1.26.5,<2 diff --git a/pyproject.toml b/pyproject.toml index bbb5b742dab..971f321d3bb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -60,7 +60,7 @@ dependencies = [ "python-slugify==8.0.4", "PyYAML==6.0.1", "requests==2.32.3", - "SQLAlchemy==2.0.30", + "SQLAlchemy==2.0.31", "typing-extensions>=4.12.2,<5.0", "ulid-transform==0.9.0", # Constrain urllib3 to ensure we deal with CVE-2020-26137 and CVE-2021-33503 diff --git a/requirements.txt b/requirements.txt index e08c02510ce..4c5e349d8b6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -35,7 +35,7 @@ psutil-home-assistant==0.0.1 python-slugify==8.0.4 PyYAML==6.0.1 requests==2.32.3 -SQLAlchemy==2.0.30 +SQLAlchemy==2.0.31 typing-extensions>=4.12.2,<5.0 ulid-transform==0.9.0 urllib3>=1.26.5,<2 diff --git a/requirements_all.txt b/requirements_all.txt index 06cff18617c..25aa3683bad 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -122,7 +122,7 @@ RtmAPI==0.7.2 # homeassistant.components.recorder # homeassistant.components.sql -SQLAlchemy==2.0.30 +SQLAlchemy==2.0.31 # homeassistant.components.tami4 Tami4EdgeAPI==3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a83481f2316..8cdf7834eea 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -107,7 +107,7 @@ RtmAPI==0.7.2 # homeassistant.components.recorder # homeassistant.components.sql -SQLAlchemy==2.0.30 +SQLAlchemy==2.0.31 # homeassistant.components.tami4 Tami4EdgeAPI==3.0 From 854b6c99feea86249efbc00651ca5dc4e9486266 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Wed, 19 Jun 2024 10:51:56 +0200 Subject: [PATCH 0822/1445] Address review on comment group registry maintenance (#119952) Address late review on comment group registry maintenance --- homeassistant/components/group/registry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/group/registry.py b/homeassistant/components/group/registry.py index c17a19e24fd..aba1b299ced 100644 --- a/homeassistant/components/group/registry.py +++ b/homeassistant/components/group/registry.py @@ -52,7 +52,7 @@ from .const import DOMAIN, REG_KEY # EXCLUDED_DOMAINS and ON_OFF_STATES are considered immutable # in respect that new platforms should not be added. -# The the only maintenance allowed here is +# The only maintenance allowed here is # if existing platforms add new ON or OFF states. EXCLUDED_DOMAINS: set[Platform | str] = { Platform.AIR_QUALITY, From 52bc006a72b30362aaa1bb5d175e262797703e58 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 19 Jun 2024 11:27:01 +0200 Subject: [PATCH 0823/1445] Update default pylint.importStrategy in dev container (#119900) --- .devcontainer/devcontainer.json | 1 + .vscode/settings.default.json | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 77249f53642..2b15a65ff1d 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -32,6 +32,7 @@ "python.pythonPath": "/home/vscode/.local/ha-venv/bin/python", "python.terminal.activateEnvInCurrentTerminal": true, "python.testing.pytestArgs": ["--no-cov"], + "pylint.importStrategy": "fromEnvironment", "editor.formatOnPaste": false, "editor.formatOnSave": true, "editor.formatOnType": true, diff --git a/.vscode/settings.default.json b/.vscode/settings.default.json index e0792a360f1..681698d08b3 100644 --- a/.vscode/settings.default.json +++ b/.vscode/settings.default.json @@ -4,5 +4,7 @@ // https://github.com/microsoft/vscode-python/issues/14067 "python.testing.pytestArgs": ["--no-cov"], // https://code.visualstudio.com/docs/python/testing#_pytest-configuration-settings - "python.testing.pytestEnabled": false + "python.testing.pytestEnabled": false, + // https://code.visualstudio.com/docs/python/linting#_general-settings + "pylint.importStrategy": "fromEnvironment" } From 87e52bb6603a541eed813d9621826754642d9f01 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 19 Jun 2024 09:21:04 -0500 Subject: [PATCH 0824/1445] Small cleanups to august (#119950) --- homeassistant/components/august/__init__.py | 23 +++------ .../components/august/binary_sensor.py | 49 ++++++------------- homeassistant/components/august/camera.py | 14 ++---- homeassistant/components/august/data.py | 9 +--- homeassistant/components/august/entity.py | 14 ++++-- homeassistant/components/august/lock.py | 38 ++++---------- homeassistant/components/august/sensor.py | 6 +-- 7 files changed, 46 insertions(+), 107 deletions(-) diff --git a/homeassistant/components/august/__init__.py b/homeassistant/components/august/__init__.py index cc4070c0d53..eec794896f6 100644 --- a/homeassistant/components/august/__init__.py +++ b/homeassistant/components/august/__init__.py @@ -11,7 +11,7 @@ from yalexs.manager.exceptions import CannotConnect, InvalidAuth, RequireValidat from yalexs.manager.gateway import Config as YaleXSConfig from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_PASSWORD, EVENT_HOMEASSISTANT_STOP +from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import device_registry as dr @@ -29,13 +29,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: session = async_create_august_clientsession(hass) august_gateway = AugustGateway(Path(hass.config.config_dir), session) try: - return await async_setup_august(hass, entry, august_gateway) + await async_setup_august(hass, entry, august_gateway) except (RequireValidation, InvalidAuth) as err: raise ConfigEntryAuthFailed from err except TimeoutError as err: raise ConfigEntryNotReady("Timed out connecting to august api") from err except (AugustApiAIOHTTPError, ClientResponseError, CannotConnect) as err: raise ConfigEntryNotReady from err + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + return True async def async_unload_entry(hass: HomeAssistant, entry: AugustConfigEntry) -> bool: @@ -45,32 +47,19 @@ async def async_unload_entry(hass: HomeAssistant, entry: AugustConfigEntry) -> b async def async_setup_august( hass: HomeAssistant, entry: AugustConfigEntry, august_gateway: AugustGateway -) -> bool: +) -> None: """Set up the August component.""" config = cast(YaleXSConfig, entry.data) await august_gateway.async_setup(config) - - if CONF_PASSWORD in entry.data: - # We no longer need to store passwords since we do not - # support YAML anymore - config_data = entry.data.copy() - del config_data[CONF_PASSWORD] - hass.config_entries.async_update_entry(entry, data=config_data) - await august_gateway.async_authenticate() await august_gateway.async_refresh_access_token_if_needed() - - data = entry.runtime_data = AugustData(hass, entry, august_gateway) + data = entry.runtime_data = AugustData(hass, august_gateway) entry.async_on_unload( hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, data.async_stop) ) entry.async_on_unload(data.async_stop) await data.async_setup() - await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) - - return True - async def async_remove_config_entry_device( hass: HomeAssistant, config_entry: AugustConfigEntry, device_entry: dr.DeviceEntry diff --git a/homeassistant/components/august/binary_sensor.py b/homeassistant/components/august/binary_sensor.py index beb899a174b..aeeaf9f690c 100644 --- a/homeassistant/components/august/binary_sensor.py +++ b/homeassistant/components/august/binary_sensor.py @@ -5,6 +5,7 @@ from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass from datetime import datetime, timedelta +from functools import partial import logging from yalexs.activity import ( @@ -51,28 +52,14 @@ def _retrieve_online_state( return detail.bridge_is_online -def _retrieve_motion_state(data: AugustData, detail: DoorbellDetail) -> bool: - assert data.activity_stream is not None - latest = data.activity_stream.get_latest_device_activity( - detail.device_id, {ActivityType.DOORBELL_MOTION} - ) - - if latest is None: - return False - - return _activity_time_based_state(latest) - - -_IMAGE_ACTIVITIES = {ActivityType.DOORBELL_IMAGE_CAPTURE} - - -def _retrieve_image_capture_state(data: AugustData, detail: DoorbellDetail) -> bool: +def _retrieve_time_based_state( + activities: set[ActivityType], data: AugustData, detail: DoorbellDetail +) -> bool: + """Get the latest state of the sensor.""" stream = data.activity_stream - assert stream is not None - latest = stream.get_latest_device_activity(detail.device_id, _IMAGE_ACTIVITIES) - if latest is None: - return False - return _activity_time_based_state(latest) + if latest := stream.get_latest_device_activity(detail.device_id, activities): + return _activity_time_based_state(latest) + return False _RING_ACTIVITIES = {ActivityType.DOORBELL_DING} @@ -80,7 +67,6 @@ _RING_ACTIVITIES = {ActivityType.DOORBELL_DING} def _retrieve_ding_state(data: AugustData, detail: DoorbellDetail | LockDetail) -> bool: stream = data.activity_stream - assert stream is not None latest = stream.get_latest_device_activity(detail.device_id, _RING_ACTIVITIES) if latest is None or ( data.push_updates_connected and latest.action == ACTION_DOORBELL_CALL_MISSED @@ -118,13 +104,15 @@ SENSOR_TYPES_VIDEO_DOORBELL = ( AugustDoorbellBinarySensorEntityDescription( key="motion", device_class=BinarySensorDeviceClass.MOTION, - value_fn=_retrieve_motion_state, + value_fn=partial(_retrieve_time_based_state, {ActivityType.DOORBELL_MOTION}), is_time_based=True, ), AugustDoorbellBinarySensorEntityDescription( key="image capture", translation_key="image_capture", - value_fn=_retrieve_image_capture_state, + value_fn=partial( + _retrieve_time_based_state, {ActivityType.DOORBELL_IMAGE_CAPTURE} + ), is_time_based=True, ), AugustDoorbellBinarySensorEntityDescription( @@ -185,22 +173,13 @@ class AugustDoorBinarySensor(AugustDescriptionEntity, BinarySensorEntity): @callback def _update_from_data(self) -> None: """Get the latest state of the sensor and update activity.""" - assert self._data.activity_stream is not None - door_activity = self._data.activity_stream.get_latest_device_activity( - self._device_id, {ActivityType.DOOR_OPERATION} - ) - - if door_activity is not None: + if door_activity := self._get_latest({ActivityType.DOOR_OPERATION}): update_lock_detail_from_activity(self._detail, door_activity) # If the source is pubnub the lock must be online since its a live update if door_activity.source == SOURCE_PUBNUB: self._detail.set_online(True) - bridge_activity = self._data.activity_stream.get_latest_device_activity( - self._device_id, {ActivityType.BRIDGE_OPERATION} - ) - - if bridge_activity is not None: + if bridge_activity := self._get_latest({ActivityType.BRIDGE_OPERATION}): update_lock_detail_from_activity(self._detail, bridge_activity) self._attr_available = self._detail.bridge_is_online self._attr_is_on = self._detail.door_state == LockDoorStatus.OPEN diff --git a/homeassistant/components/august/camera.py b/homeassistant/components/august/camera.py index ba29b2905d3..4e569e2a91e 100644 --- a/homeassistant/components/august/camera.py +++ b/homeassistant/components/august/camera.py @@ -54,17 +54,13 @@ class AugustCamera(AugustEntityMixin, Camera): super().__init__(data, device, "camera") self._timeout = timeout self._session = session + self._attr_model = self._detail.model @property def is_recording(self) -> bool: """Return true if the device is recording.""" return self._device.has_subscription - @property - def model(self) -> str | None: - """Return the camera model.""" - return self._detail.model - async def _async_update(self): """Update device.""" _LOGGER.debug("async_update called %s", self._detail.device_name) @@ -74,11 +70,9 @@ class AugustCamera(AugustEntityMixin, Camera): @callback def _update_from_data(self) -> None: """Get the latest state of the sensor.""" - doorbell_activity = self._data.activity_stream.get_latest_device_activity( - self._device_id, - {ActivityType.DOORBELL_MOTION, ActivityType.DOORBELL_IMAGE_CAPTURE}, - ) - if doorbell_activity is not None: + if doorbell_activity := self._get_latest( + {ActivityType.DOORBELL_MOTION, ActivityType.DOORBELL_IMAGE_CAPTURE} + ): update_doorbell_image_from_activity(self._detail, doorbell_activity) async def async_camera_image( diff --git a/homeassistant/components/august/data.py b/homeassistant/components/august/data.py index 55c7c2bfa03..66ddfeedfde 100644 --- a/homeassistant/components/august/data.py +++ b/homeassistant/components/august/data.py @@ -6,7 +6,7 @@ from yalexs.lock import LockDetail from yalexs.manager.data import YaleXSData from yalexs_ble import YaleXSBLEDiscovery -from homeassistant.config_entries import SOURCE_INTEGRATION_DISCOVERY, ConfigEntry +from homeassistant.config_entries import SOURCE_INTEGRATION_DISCOVERY from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import discovery_flow @@ -41,12 +41,7 @@ def _async_trigger_ble_lock_discovery( class AugustData(YaleXSData): """August data object.""" - def __init__( - self, - hass: HomeAssistant, - config_entry: ConfigEntry, - august_gateway: AugustGateway, - ) -> None: + def __init__(self, hass: HomeAssistant, august_gateway: AugustGateway) -> None: """Init August data object.""" self._hass = hass super().__init__(august_gateway, HomeAssistantError) diff --git a/homeassistant/components/august/entity.py b/homeassistant/components/august/entity.py index 960dddbc005..babf5c587fb 100644 --- a/homeassistant/components/august/entity.py +++ b/homeassistant/components/august/entity.py @@ -2,6 +2,7 @@ from abc import abstractmethod +from yalexs.activity import Activity, ActivityType from yalexs.doorbell import Doorbell, DoorbellDetail from yalexs.keypad import KeypadDetail from yalexs.lock import Lock, LockDetail @@ -31,8 +32,10 @@ class AugustEntityMixin(Entity): """Initialize an August device.""" super().__init__() self._data = data + self._stream = data.activity_stream self._device = device detail = self._detail + self._device_id = device.device_id self._attr_unique_id = f"{device.device_id}_{unique_id}" self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, self._device_id)}, @@ -46,10 +49,6 @@ class AugustEntityMixin(Entity): if isinstance(detail, LockDetail) and (mac := detail.mac_address): self._attr_device_info[ATTR_CONNECTIONS] = {(dr.CONNECTION_BLUETOOTH, mac)} - @property - def _device_id(self) -> str: - return self._device.device_id - @property def _detail(self) -> DoorbellDetail | LockDetail: return self._data.get_device_detail(self._device.device_id) @@ -59,6 +58,11 @@ class AugustEntityMixin(Entity): """Check if the lock has a paired hyper bridge.""" return bool(self._detail.bridge and self._detail.bridge.hyper_bridge) + @callback + def _get_latest(self, activity_types: set[ActivityType]) -> Activity | None: + """Get the latest activity for the device.""" + return self._stream.get_latest_device_activity(self._device_id, activity_types) + @callback def _update_from_data_and_write_state(self) -> None: self._update_from_data() @@ -76,7 +80,7 @@ class AugustEntityMixin(Entity): ) ) self.async_on_remove( - self._data.activity_stream.async_subscribe_device_id( + self._stream.async_subscribe_device_id( self._device_id, self._update_from_data_and_write_state ) ) diff --git a/homeassistant/components/august/lock.py b/homeassistant/components/august/lock.py index 10d32ebd323..7aee612aa41 100644 --- a/homeassistant/components/august/lock.py +++ b/homeassistant/components/august/lock.py @@ -50,7 +50,6 @@ class AugustLock(AugustEntityMixin, RestoreEntity, LockEntity): async def async_lock(self, **kwargs: Any) -> None: """Lock the device.""" - assert self._data.activity_stream is not None if self._data.push_updates_connected: await self._data.async_lock_async(self._device_id, self._hyper_bridge) return @@ -58,7 +57,6 @@ class AugustLock(AugustEntityMixin, RestoreEntity, LockEntity): async def async_open(self, **kwargs: Any) -> None: """Open/unlatch the device.""" - assert self._data.activity_stream is not None if self._data.push_updates_connected: await self._data.async_unlatch_async(self._device_id, self._hyper_bridge) return @@ -66,7 +64,6 @@ class AugustLock(AugustEntityMixin, RestoreEntity, LockEntity): async def async_unlock(self, **kwargs: Any) -> None: """Unlock the device.""" - assert self._data.activity_stream is not None if self._data.push_updates_connected: await self._data.async_unlock_async(self._device_id, self._hyper_bridge) return @@ -105,33 +102,22 @@ class AugustLock(AugustEntityMixin, RestoreEntity, LockEntity): @callback def _update_from_data(self) -> None: """Get the latest state of the sensor and update activity.""" - activity_stream = self._data.activity_stream - device_id = self._device_id - if lock_activity := activity_stream.get_latest_device_activity( - device_id, - {ActivityType.LOCK_OPERATION}, - ): + detail = self._detail + if lock_activity := self._get_latest({ActivityType.LOCK_OPERATION}): self._attr_changed_by = lock_activity.operated_by - - lock_activity_without_operator = activity_stream.get_latest_device_activity( - device_id, - {ActivityType.LOCK_OPERATION_WITHOUT_OPERATOR}, + lock_activity_without_operator = self._get_latest( + {ActivityType.LOCK_OPERATION_WITHOUT_OPERATOR} ) - if latest_activity := get_latest_activity( lock_activity_without_operator, lock_activity ): if latest_activity.source == SOURCE_PUBNUB: # If the source is pubnub the lock must be online since its a live update self._detail.set_online(True) - update_lock_detail_from_activity(self._detail, latest_activity) + update_lock_detail_from_activity(detail, latest_activity) - bridge_activity = self._data.activity_stream.get_latest_device_activity( - self._device_id, {ActivityType.BRIDGE_OPERATION} - ) - - if bridge_activity is not None: - update_lock_detail_from_activity(self._detail, bridge_activity) + if bridge_activity := self._get_latest({ActivityType.BRIDGE_OPERATION}): + update_lock_detail_from_activity(detail, bridge_activity) self._update_lock_status_from_detail() lock_status = self._lock_status @@ -139,20 +125,16 @@ class AugustLock(AugustEntityMixin, RestoreEntity, LockEntity): self._attr_is_locked = None else: self._attr_is_locked = lock_status is LockStatus.LOCKED - self._attr_is_jammed = lock_status is LockStatus.JAMMED self._attr_is_locking = lock_status is LockStatus.LOCKING self._attr_is_unlocking = lock_status in ( LockStatus.UNLOCKING, LockStatus.UNLATCHING, ) - - self._attr_extra_state_attributes = { - ATTR_BATTERY_LEVEL: self._detail.battery_level - } - if self._detail.keypad is not None: + self._attr_extra_state_attributes = {ATTR_BATTERY_LEVEL: detail.battery_level} + if keypad := detail.keypad: self._attr_extra_state_attributes["keypad_battery_level"] = ( - self._detail.keypad.battery_level + keypad.battery_level ) async def async_added_to_hass(self) -> None: diff --git a/homeassistant/components/august/sensor.py b/homeassistant/components/august/sensor.py index 847d7f32a5a..7a4c1a92358 100644 --- a/homeassistant/components/august/sensor.py +++ b/homeassistant/components/august/sensor.py @@ -127,12 +127,8 @@ class AugustOperatorSensor(AugustEntityMixin, RestoreSensor): @callback def _update_from_data(self) -> None: """Get the latest state of the sensor and update activity.""" - lock_activity = self._data.activity_stream.get_latest_device_activity( - self._device_id, {ActivityType.LOCK_OPERATION} - ) - self._attr_available = True - if lock_activity is not None: + if lock_activity := self._get_latest({ActivityType.LOCK_OPERATION}): lock_activity = cast(LockOperationActivity, lock_activity) self._attr_native_value = lock_activity.operated_by self._operated_remote = lock_activity.operated_remote From 9371277b851a330caa509ca2fe3e77ae76634304 Mon Sep 17 00:00:00 2001 From: Tsvi Mostovicz Date: Wed, 19 Jun 2024 17:21:43 +0300 Subject: [PATCH 0825/1445] Bump hdate to 0.10.9 (#119887) --- homeassistant/components/jewish_calendar/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/jewish_calendar/manifest.json b/homeassistant/components/jewish_calendar/manifest.json index 20eb28929bd..6d2fe8ecfa1 100644 --- a/homeassistant/components/jewish_calendar/manifest.json +++ b/homeassistant/components/jewish_calendar/manifest.json @@ -6,6 +6,6 @@ "documentation": "https://www.home-assistant.io/integrations/jewish_calendar", "iot_class": "calculated", "loggers": ["hdate"], - "requirements": ["hdate==0.10.8"], + "requirements": ["hdate==0.10.9"], "single_config_entry": true } diff --git a/requirements_all.txt b/requirements_all.txt index 25aa3683bad..3b8273c6f2e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1062,7 +1062,7 @@ hass-splunk==0.1.1 hassil==1.7.1 # homeassistant.components.jewish_calendar -hdate==0.10.8 +hdate==0.10.9 # homeassistant.components.heatmiser heatmiserV3==1.1.18 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8cdf7834eea..2a505c3de6e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -873,7 +873,7 @@ hass-nabucasa==0.81.1 hassil==1.7.1 # homeassistant.components.jewish_calendar -hdate==0.10.8 +hdate==0.10.9 # homeassistant.components.here_travel_time here-routing==0.2.0 From e1f244e1c2f04d2a228794c34df6400c1b670331 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk <11290930+bouwew@users.noreply.github.com> Date: Wed, 19 Jun 2024 19:23:14 +0200 Subject: [PATCH 0826/1445] Bump plugwise to v0.37.4.1 (#119963) * Bump plugwise to v0.37.4 * bump plugwise to v0.37.4.1 --- homeassistant/components/plugwise/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/plugwise/manifest.json b/homeassistant/components/plugwise/manifest.json index ada7d2d2533..b1937ee219d 100644 --- a/homeassistant/components/plugwise/manifest.json +++ b/homeassistant/components/plugwise/manifest.json @@ -7,6 +7,6 @@ "integration_type": "hub", "iot_class": "local_polling", "loggers": ["plugwise"], - "requirements": ["plugwise==0.37.3"], + "requirements": ["plugwise==0.37.4.1"], "zeroconf": ["_plugwise._tcp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index 3b8273c6f2e..b0b4e878997 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1572,7 +1572,7 @@ plexauth==0.0.6 plexwebsocket==0.0.14 # homeassistant.components.plugwise -plugwise==0.37.3 +plugwise==0.37.4.1 # homeassistant.components.plum_lightpad plumlightpad==0.0.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2a505c3de6e..e7e61e2e194 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1252,7 +1252,7 @@ plexauth==0.0.6 plexwebsocket==0.0.14 # homeassistant.components.plugwise -plugwise==0.37.3 +plugwise==0.37.4.1 # homeassistant.components.plum_lightpad plumlightpad==0.0.11 From 6fa54229fee5c48b7749cb405465f05b6fc54dd2 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 19 Jun 2024 19:38:57 +0200 Subject: [PATCH 0827/1445] Bump airgradient to 0.6.0 (#119962) * Bump airgradient to 0.6.0 * Bump airgradient to 0.6.0 * Bump airgradient to 0.6.0 * Bump airgradient to 0.6.0 --- homeassistant/components/airgradient/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/airgradient/fixtures/get_config.json | 7 +++++-- .../components/airgradient/fixtures/get_config_cloud.json | 7 +++++-- .../components/airgradient/fixtures/get_config_local.json | 7 +++++-- 6 files changed, 18 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/airgradient/manifest.json b/homeassistant/components/airgradient/manifest.json index d3e5fed74ab..7b892c4658a 100644 --- a/homeassistant/components/airgradient/manifest.json +++ b/homeassistant/components/airgradient/manifest.json @@ -6,6 +6,6 @@ "documentation": "https://www.home-assistant.io/integrations/airgradient", "integration_type": "device", "iot_class": "local_polling", - "requirements": ["airgradient==0.5.0"], + "requirements": ["airgradient==0.6.0"], "zeroconf": ["_airgradient._tcp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index b0b4e878997..5d54d7d8b04 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -410,7 +410,7 @@ aiowithings==3.0.1 aioymaps==1.2.2 # homeassistant.components.airgradient -airgradient==0.5.0 +airgradient==0.6.0 # homeassistant.components.airly airly==1.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e7e61e2e194..7c4fa5d1de6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -383,7 +383,7 @@ aiowithings==3.0.1 aioymaps==1.2.2 # homeassistant.components.airgradient -airgradient==0.5.0 +airgradient==0.6.0 # homeassistant.components.airly airly==1.1.0 diff --git a/tests/components/airgradient/fixtures/get_config.json b/tests/components/airgradient/fixtures/get_config.json index db20f762037..e922c4e221f 100644 --- a/tests/components/airgradient/fixtures/get_config.json +++ b/tests/components/airgradient/fixtures/get_config.json @@ -2,12 +2,15 @@ "country": "DE", "pmStandard": "ugm3", "ledBarMode": "co2", - "displayMode": "on", "abcDays": 8, "tvocLearningOffset": 12, "noxLearningOffset": 12, "mqttBrokerUrl": "", "temperatureUnit": "c", "configurationControl": "both", - "postDataToAirGradient": true + "postDataToAirGradient": true, + "ledBarBrightness": 100, + "displayBrightness": 0, + "offlineMode": false, + "model": "I-9PSL-DE" } diff --git a/tests/components/airgradient/fixtures/get_config_cloud.json b/tests/components/airgradient/fixtures/get_config_cloud.json index a5f27957e04..8543fa27228 100644 --- a/tests/components/airgradient/fixtures/get_config_cloud.json +++ b/tests/components/airgradient/fixtures/get_config_cloud.json @@ -2,12 +2,15 @@ "country": "DE", "pmStandard": "ugm3", "ledBarMode": "co2", - "displayMode": "on", "abcDays": 8, "tvocLearningOffset": 12, "noxLearningOffset": 12, "mqttBrokerUrl": "", "temperatureUnit": "c", "configurationControl": "cloud", - "postDataToAirGradient": true + "postDataToAirGradient": true, + "ledBarBrightness": 100, + "displayBrightness": 0, + "offlineMode": false, + "model": "I-9PSL-DE" } diff --git a/tests/components/airgradient/fixtures/get_config_local.json b/tests/components/airgradient/fixtures/get_config_local.json index 09e0e982053..a9ac299c178 100644 --- a/tests/components/airgradient/fixtures/get_config_local.json +++ b/tests/components/airgradient/fixtures/get_config_local.json @@ -2,12 +2,15 @@ "country": "DE", "pmStandard": "ugm3", "ledBarMode": "co2", - "displayMode": "on", "abcDays": 8, "tvocLearningOffset": 12, "noxLearningOffset": 12, "mqttBrokerUrl": "", "temperatureUnit": "c", "configurationControl": "local", - "postDataToAirGradient": true + "postDataToAirGradient": true, + "ledBarBrightness": 100, + "displayBrightness": 0, + "offlineMode": false, + "model": "I-9PSL-DE" } From 970836da0c6872caaffde821fd40411cc00f967b Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 19 Jun 2024 19:42:23 +0200 Subject: [PATCH 0828/1445] Clean up config option tests in UniFi device tracker tests (#119978) --- tests/components/unifi/test_device_tracker.py | 471 ++++-------------- 1 file changed, 110 insertions(+), 361 deletions(-) diff --git a/tests/components/unifi/test_device_tracker.py b/tests/components/unifi/test_device_tracker.py index 3f3913ad0b3..e22c49fd7db 100644 --- a/tests/components/unifi/test_device_tracker.py +++ b/tests/components/unifi/test_device_tracker.py @@ -2,6 +2,7 @@ from collections.abc import Callable from datetime import timedelta +from types import MappingProxyType from typing import Any from aiounifi.models.message import MessageKey @@ -23,12 +24,47 @@ from homeassistant.components.unifi.const import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_HOME, STATE_NOT_HOME, STATE_UNAVAILABLE -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, State from homeassistant.helpers import entity_registry as er import homeassistant.util.dt as dt_util from tests.common import async_fire_time_changed +WIRED_CLIENT_1 = { + "hostname": "wd_client_1", + "is_wired": True, + "last_seen": 1562600145, + "mac": "00:00:00:00:00:02", +} + +WIRELESS_CLIENT_1 = { + "ap_mac": "00:00:00:00:02:01", + "essid": "ssid", + "hostname": "ws_client_1", + "ip": "10.0.0.1", + "is_wired": False, + "last_seen": 1562600145, + "mac": "00:00:00:00:00:01", +} + +SWITCH_1 = { + "board_rev": 3, + "device_id": "mock-id-1", + "has_fan": True, + "fan_level": 0, + "ip": "10.0.1.1", + "last_seen": 1562600145, + "mac": "00:00:00:00:01:01", + "model": "US16P150", + "name": "Switch 1", + "next_interval": 20, + "overheating": True, + "state": 1, + "type": "usw", + "upgradable": True, + "version": "4.0.42.10433", +} + @pytest.mark.parametrize( "client_payload", @@ -496,213 +532,6 @@ async def test_hub_state_change(hass: HomeAssistant, mock_websocket_state) -> No assert hass.states.get("device_tracker.device").state == STATE_HOME -@pytest.mark.parametrize( - "client_payload", - [ - [ - { - "essid": "ssid", - "hostname": "wireless_client", - "is_wired": False, - "last_seen": 1562600145, - "mac": "00:00:00:00:00:01", - }, - { - "is_wired": True, - "last_seen": 1562600145, - "mac": "00:00:00:00:00:02", - "name": "Wired Client", - }, - ] - ], -) -@pytest.mark.parametrize( - "device_payload", - [ - [ - { - "board_rev": 3, - "device_id": "mock-id", - "has_fan": True, - "fan_level": 0, - "ip": "10.0.1.1", - "last_seen": 1562600145, - "mac": "00:00:00:00:01:01", - "model": "US16P150", - "name": "Device", - "next_interval": 20, - "overheating": True, - "state": 1, - "type": "usw", - "upgradable": True, - "version": "4.0.42.10433", - } - ] - ], -) -@pytest.mark.usefixtures("mock_device_registry") -async def test_option_track_clients( - hass: HomeAssistant, config_entry_setup: ConfigEntry -) -> None: - """Test the tracking of clients can be turned off.""" - assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 3 - assert hass.states.get("device_tracker.wireless_client") - assert hass.states.get("device_tracker.wired_client") - assert hass.states.get("device_tracker.device") - - hass.config_entries.async_update_entry( - config_entry_setup, options={CONF_TRACK_CLIENTS: False} - ) - await hass.async_block_till_done() - - assert not hass.states.get("device_tracker.wireless_client") - assert not hass.states.get("device_tracker.wired_client") - assert hass.states.get("device_tracker.device") - - hass.config_entries.async_update_entry( - config_entry_setup, options={CONF_TRACK_CLIENTS: True} - ) - await hass.async_block_till_done() - - assert hass.states.get("device_tracker.wireless_client") - assert hass.states.get("device_tracker.wired_client") - assert hass.states.get("device_tracker.device") - - -@pytest.mark.parametrize( - "client_payload", - [ - [ - { - "essid": "ssid", - "hostname": "wireless_client", - "is_wired": False, - "last_seen": 1562600145, - "mac": "00:00:00:00:00:01", - }, - { - "is_wired": True, - "last_seen": 1562600145, - "mac": "00:00:00:00:00:02", - "name": "Wired Client", - }, - ] - ], -) -@pytest.mark.parametrize( - "device_payload", - [ - [ - { - "board_rev": 3, - "device_id": "mock-id", - "has_fan": True, - "fan_level": 0, - "ip": "10.0.1.1", - "last_seen": 1562600145, - "mac": "00:00:00:00:01:01", - "model": "US16P150", - "name": "Device", - "next_interval": 20, - "overheating": True, - "state": 1, - "type": "usw", - "upgradable": True, - "version": "4.0.42.10433", - } - ] - ], -) -@pytest.mark.usefixtures("mock_device_registry") -async def test_option_track_wired_clients( - hass: HomeAssistant, config_entry_setup: ConfigEntry -) -> None: - """Test the tracking of wired clients can be turned off.""" - assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 3 - assert hass.states.get("device_tracker.wireless_client") - assert hass.states.get("device_tracker.wired_client") - assert hass.states.get("device_tracker.device") - - hass.config_entries.async_update_entry( - config_entry_setup, options={CONF_TRACK_WIRED_CLIENTS: False} - ) - await hass.async_block_till_done() - - assert hass.states.get("device_tracker.wireless_client") - assert not hass.states.get("device_tracker.wired_client") - assert hass.states.get("device_tracker.device") - - hass.config_entries.async_update_entry( - config_entry_setup, options={CONF_TRACK_WIRED_CLIENTS: True} - ) - await hass.async_block_till_done() - - assert hass.states.get("device_tracker.wireless_client") - assert hass.states.get("device_tracker.wired_client") - assert hass.states.get("device_tracker.device") - - -@pytest.mark.parametrize( - "client_payload", - [ - [ - { - "hostname": "client", - "is_wired": True, - "last_seen": 1562600145, - "mac": "00:00:00:00:00:01", - } - ] - ], -) -@pytest.mark.parametrize( - "device_payload", - [ - [ - { - "board_rev": 3, - "device_id": "mock-id", - "last_seen": 1562600145, - "ip": "10.0.1.1", - "mac": "00:00:00:00:01:01", - "model": "US16P150", - "name": "Device", - "next_interval": 20, - "overheating": True, - "state": 1, - "type": "usw", - "upgradable": True, - "version": "4.0.42.10433", - } - ] - ], -) -@pytest.mark.usefixtures("mock_device_registry") -async def test_option_track_devices( - hass: HomeAssistant, config_entry_setup: ConfigEntry -) -> None: - """Test the tracking of devices can be turned off.""" - assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 2 - assert hass.states.get("device_tracker.client") - assert hass.states.get("device_tracker.device") - - hass.config_entries.async_update_entry( - config_entry_setup, options={CONF_TRACK_DEVICES: False} - ) - await hass.async_block_till_done() - - assert hass.states.get("device_tracker.client") - assert not hass.states.get("device_tracker.device") - - hass.config_entries.async_update_entry( - config_entry_setup, options={CONF_TRACK_DEVICES: True} - ) - await hass.async_block_till_done() - - assert hass.states.get("device_tracker.client") - assert hass.states.get("device_tracker.device") - - @pytest.mark.usefixtures("mock_device_registry") async def test_option_ssid_filter( hass: HomeAssistant, @@ -1031,166 +860,86 @@ async def test_restoring_client( assert not hass.states.get("device_tracker.not_restored") -@pytest.mark.parametrize("config_entry_options", [{CONF_TRACK_CLIENTS: False}]) @pytest.mark.parametrize( - "client_payload", + ("config_entry_options", "counts", "expected"), [ - [ - { - "essid": "ssid", - "hostname": "Wireless client", - "ip": "10.0.0.1", - "is_wired": False, - "last_seen": 1562600145, - "mac": "00:00:00:00:00:01", - }, - { - "hostname": "Wired client", - "ip": "10.0.0.2", - "is_wired": True, - "last_seen": 1562600145, - "mac": "00:00:00:00:00:02", - }, - ] - ], -) -@pytest.mark.parametrize( - "device_payload", - [ - [ - { - "board_rev": 3, - "device_id": "mock-id", - "has_fan": True, - "fan_level": 0, - "ip": "10.0.1.1", - "last_seen": 1562600145, - "mac": "00:00:00:00:01:01", - "model": "US16P150", - "name": "Device", - "next_interval": 20, - "overheating": True, - "state": 1, - "type": "usw", - "upgradable": True, - "version": "4.0.42.10433", - }, - ] + ( + {CONF_TRACK_CLIENTS: True}, + (3, 1), + ((True, True, True), (None, None, True)), + ), + ( + {CONF_TRACK_CLIENTS: True, CONF_SSID_FILTER: ["ssid"]}, + (3, 1), + ((True, True, True), (None, None, True)), + ), + ( + {CONF_TRACK_CLIENTS: True, CONF_SSID_FILTER: ["ssid-2"]}, + (2, 1), + ((None, True, True), (None, None, True)), + ), + ( + {CONF_TRACK_CLIENTS: False, CONF_CLIENT_SOURCE: ["00:00:00:00:00:01"]}, + (2, 1), + ((True, None, True), (None, None, True)), + ), + ( + {CONF_TRACK_CLIENTS: False, CONF_CLIENT_SOURCE: ["00:00:00:00:00:02"]}, + (2, 1), + ((None, True, True), (None, None, True)), + ), + ( + {CONF_TRACK_WIRED_CLIENTS: True}, + (3, 2), + ((True, True, True), (True, None, True)), + ), + ( + {CONF_TRACK_DEVICES: True}, + (3, 2), + ((True, True, True), (True, True, None)), + ), ], ) +@pytest.mark.parametrize("client_payload", [[WIRELESS_CLIENT_1, WIRED_CLIENT_1]]) +@pytest.mark.parametrize("device_payload", [[SWITCH_1]]) @pytest.mark.usefixtures("mock_device_registry") -async def test_dont_track_clients( - hass: HomeAssistant, config_entry_setup: ConfigEntry +async def test_config_entry_options_track( + hass: HomeAssistant, + config_entry_setup: ConfigEntry, + config_entry_options: MappingProxyType[str, Any], + counts: tuple[int], + expected: dict[tuple[bool | None]], ) -> None: - """Test don't track clients config works.""" - assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 1 - assert not hass.states.get("device_tracker.wireless_client") - assert not hass.states.get("device_tracker.wired_client") - assert hass.states.get("device_tracker.device") + """Test the different config entry options. - hass.config_entries.async_update_entry( - config_entry_setup, options={CONF_TRACK_CLIENTS: True} - ) + Validates how many entities are created + and that the specific ones exist as expected. + """ + option = next(iter(config_entry_options)) + + def assert_state(state: State | None, expected: bool | None): + """Assert if state expected.""" + assert state is None if expected is None else state + + assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == counts[0] + assert_state(hass.states.get("device_tracker.ws_client_1"), expected[0][0]) + assert_state(hass.states.get("device_tracker.wd_client_1"), expected[0][1]) + assert_state(hass.states.get("device_tracker.switch_1"), expected[0][2]) + + # Keep only the primary option and turn it off, everything else uses default + hass.config_entries.async_update_entry(config_entry_setup, options={option: False}) + await hass.async_block_till_done() + + assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == counts[1] + assert_state(hass.states.get("device_tracker.ws_client_1"), expected[1][0]) + assert_state(hass.states.get("device_tracker.wd_client_1"), expected[1][1]) + assert_state(hass.states.get("device_tracker.switch_1"), expected[1][2]) + + # Turn on the primary option, everything else uses default + hass.config_entries.async_update_entry(config_entry_setup, options={option: True}) await hass.async_block_till_done() assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 3 - assert hass.states.get("device_tracker.wireless_client") - assert hass.states.get("device_tracker.wired_client") - assert hass.states.get("device_tracker.device") - - -@pytest.mark.parametrize("config_entry_options", [{CONF_TRACK_DEVICES: False}]) -@pytest.mark.parametrize( - "client_payload", - [ - [ - { - "hostname": "client", - "is_wired": True, - "last_seen": 1562600145, - "mac": "00:00:00:00:00:01", - }, - ] - ], -) -@pytest.mark.parametrize( - "device_payload", - [ - [ - { - "board_rev": 3, - "device_id": "mock-id", - "has_fan": True, - "fan_level": 0, - "ip": "10.0.1.1", - "last_seen": 1562600145, - "mac": "00:00:00:00:01:01", - "model": "US16P150", - "name": "Device", - "next_interval": 20, - "overheating": True, - "state": 1, - "type": "usw", - "upgradable": True, - "version": "4.0.42.10433", - }, - ] - ], -) -@pytest.mark.usefixtures("mock_device_registry") -async def test_dont_track_devices( - hass: HomeAssistant, config_entry_setup: ConfigEntry -) -> None: - """Test don't track devices config works.""" - assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 1 - assert hass.states.get("device_tracker.client") - assert not hass.states.get("device_tracker.device") - - hass.config_entries.async_update_entry( - config_entry_setup, options={CONF_TRACK_DEVICES: True} - ) - await hass.async_block_till_done() - - assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 2 - assert hass.states.get("device_tracker.client") - assert hass.states.get("device_tracker.device") - - -@pytest.mark.parametrize("config_entry_options", [{CONF_TRACK_WIRED_CLIENTS: False}]) -@pytest.mark.parametrize( - "client_payload", - [ - [ - { - "essid": "ssid", - "hostname": "Wireless Client", - "is_wired": False, - "last_seen": 1562600145, - "mac": "00:00:00:00:00:01", - }, - { - "is_wired": True, - "last_seen": 1562600145, - "mac": "00:00:00:00:00:02", - "name": "Wired Client", - }, - ] - ], -) -@pytest.mark.usefixtures("mock_device_registry") -async def test_dont_track_wired_clients( - hass: HomeAssistant, config_entry_setup: ConfigEntry -) -> None: - """Test don't track wired clients config works.""" - assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 1 - assert hass.states.get("device_tracker.wireless_client") - assert not hass.states.get("device_tracker.wired_client") - - hass.config_entries.async_update_entry( - config_entry_setup, options={CONF_TRACK_WIRED_CLIENTS: True} - ) - await hass.async_block_till_done() - - assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 2 - assert hass.states.get("device_tracker.wireless_client") - assert hass.states.get("device_tracker.wired_client") + assert_state(hass.states.get("device_tracker.ws_client_1"), True) + assert_state(hass.states.get("device_tracker.wd_client_1"), True) + assert_state(hass.states.get("device_tracker.switch_1"), True) From 8ad63a0020a9b99e181878d0881d3237e7ae3fd7 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Wed, 19 Jun 2024 19:43:05 +0200 Subject: [PATCH 0829/1445] Fix flaky todoist test (#119954) Fix flakey todoist test --- tests/components/todoist/test_calendar.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/components/todoist/test_calendar.py b/tests/components/todoist/test_calendar.py index dae5f0a8ee5..8ba4da9b2e8 100644 --- a/tests/components/todoist/test_calendar.py +++ b/tests/components/todoist/test_calendar.py @@ -7,6 +7,7 @@ from unittest.mock import AsyncMock, patch import urllib import zoneinfo +from freezegun.api import FrozenDateTimeFactory import pytest from todoist_api_python.models import Due @@ -146,6 +147,7 @@ async def test_update_entity_for_custom_project_no_due_date_on( ) async def test_update_entity_for_calendar_with_due_date_in_the_future( hass: HomeAssistant, + freezer: FrozenDateTimeFactory, api: AsyncMock, ) -> None: """Test that a task with a due date in the future has on state and correct end_time.""" From 8e3b58dc28af49baea8adaf11937f0a76e3ffd92 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Wed, 19 Jun 2024 19:55:20 +0200 Subject: [PATCH 0830/1445] Clean weather tests (#119916) --- .../weather/snapshots/test_init.ambr | 46 ++----------------- tests/components/weather/test_init.py | 39 ++++------------ 2 files changed, 11 insertions(+), 74 deletions(-) diff --git a/tests/components/weather/snapshots/test_init.ambr b/tests/components/weather/snapshots/test_init.ambr index 1aa78f6bf35..dbb18d5485a 100644 --- a/tests/components/weather/snapshots/test_init.ambr +++ b/tests/components/weather/snapshots/test_init.ambr @@ -1,18 +1,5 @@ # serializer version: 1 -# name: test_get_forecast[daily-1-get_forecast] - dict({ - 'forecast': list([ - dict({ - 'cloud_coverage': None, - 'temperature': 38.0, - 'templow': 38.0, - 'uv_index': None, - 'wind_bearing': None, - }), - ]), - }) -# --- -# name: test_get_forecast[daily-1-get_forecasts] +# name: test_get_forecast[daily-1] dict({ 'weather.testing': dict({ 'forecast': list([ @@ -27,20 +14,7 @@ }), }) # --- -# name: test_get_forecast[hourly-2-get_forecast] - dict({ - 'forecast': list([ - dict({ - 'cloud_coverage': None, - 'temperature': 38.0, - 'templow': 38.0, - 'uv_index': None, - 'wind_bearing': None, - }), - ]), - }) -# --- -# name: test_get_forecast[hourly-2-get_forecasts] +# name: test_get_forecast[hourly-2] dict({ 'weather.testing': dict({ 'forecast': list([ @@ -55,21 +29,7 @@ }), }) # --- -# name: test_get_forecast[twice_daily-4-get_forecast] - dict({ - 'forecast': list([ - dict({ - 'cloud_coverage': None, - 'is_daytime': True, - 'temperature': 38.0, - 'templow': 38.0, - 'uv_index': None, - 'wind_bearing': None, - }), - ]), - }) -# --- -# name: test_get_forecast[twice_daily-4-get_forecasts] +# name: test_get_forecast[twice_daily-4] dict({ 'weather.testing': dict({ 'forecast': list([ diff --git a/tests/components/weather/test_init.py b/tests/components/weather/test_init.py index 78f454b4f95..8ea8895a2a3 100644 --- a/tests/components/weather/test_init.py +++ b/tests/components/weather/test_init.py @@ -602,12 +602,6 @@ async def test_forecast_twice_daily_missing_is_daytime( assert msg["type"] == "result" -@pytest.mark.parametrize( - ("service"), - [ - SERVICE_GET_FORECASTS, - ], -) @pytest.mark.parametrize( ("forecast_type", "supported_features"), [ @@ -625,7 +619,6 @@ async def test_get_forecast( forecast_type: str, supported_features: int, snapshot: SnapshotAssertion, - service: str, ) -> None: """Test get forecast service.""" @@ -656,7 +649,7 @@ async def test_get_forecast( response = await hass.services.async_call( DOMAIN, - service, + SERVICE_GET_FORECASTS, { "entity_id": entity0.entity_id, "type": forecast_type, @@ -667,24 +660,9 @@ async def test_get_forecast( assert response == snapshot -@pytest.mark.parametrize( - ("service", "expected"), - [ - ( - SERVICE_GET_FORECASTS, - { - "weather.testing": { - "forecast": [], - } - }, - ), - ], -) async def test_get_forecast_no_forecast( hass: HomeAssistant, config_flow_fixture: None, - service: str, - expected: dict[str, list | dict[str, list]], ) -> None: """Test get forecast service.""" @@ -705,7 +683,7 @@ async def test_get_forecast_no_forecast( response = await hass.services.async_call( DOMAIN, - service, + SERVICE_GET_FORECASTS, { "entity_id": entity0.entity_id, "type": "daily", @@ -713,13 +691,13 @@ async def test_get_forecast_no_forecast( blocking=True, return_response=True, ) - assert response == expected + assert response == { + "weather.testing": { + "forecast": [], + } + } -@pytest.mark.parametrize( - ("service"), - [SERVICE_GET_FORECASTS], -) @pytest.mark.parametrize( ("supported_features", "forecast_types"), [ @@ -733,7 +711,6 @@ async def test_get_forecast_unsupported( config_flow_fixture: None, forecast_types: list[str], supported_features: int, - service: str, ) -> None: """Test get forecast service.""" @@ -763,7 +740,7 @@ async def test_get_forecast_unsupported( with pytest.raises(HomeAssistantError): await hass.services.async_call( DOMAIN, - service, + SERVICE_GET_FORECASTS, { "entity_id": weather_entity.entity_id, "type": forecast_type, From f0dc39a9035dc62dadf85065f27c6a8bda66a429 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 19 Jun 2024 19:58:07 +0200 Subject: [PATCH 0831/1445] Improve typing in core tests (#119958) Add missing return values in core tests --- tests/test_bootstrap.py | 2 +- tests/test_config.py | 27 ++++++++++++++++++--------- tests/test_core.py | 18 +++++++++--------- 3 files changed, 28 insertions(+), 19 deletions(-) diff --git a/tests/test_bootstrap.py b/tests/test_bootstrap.py index 110a41e4216..ca864006852 100644 --- a/tests/test_bootstrap.py +++ b/tests/test_bootstrap.py @@ -1489,7 +1489,7 @@ async def test_setup_does_base_platforms_first(hass: HomeAssistant) -> None: assert order[3:] == ["root", "first_dep", "second_dep"] -def test_should_rollover_is_always_false(): +def test_should_rollover_is_always_false() -> None: """Test that shouldRollover always returns False.""" assert ( bootstrap._RotatingFileHandlerWithoutShouldRollOver( diff --git a/tests/test_config.py b/tests/test_config.py index 8a8cf8f909b..a30498b422a 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -8,10 +8,11 @@ import logging import os from typing import Any from unittest import mock -from unittest.mock import AsyncMock, Mock, patch +from unittest.mock import AsyncMock, MagicMock, Mock, patch import pytest from syrupy.assertion import SnapshotAssertion +from typing_extensions import Generator import voluptuous as vol from voluptuous import Invalid, MultipleInvalid import yaml @@ -1082,7 +1083,9 @@ async def test_check_ha_config_file_wrong(mock_check, hass: HomeAssistant) -> No ], ) @pytest.mark.usefixtures("mock_hass_config") -async def test_async_hass_config_yaml_merge(merge_log_err, hass: HomeAssistant) -> None: +async def test_async_hass_config_yaml_merge( + merge_log_err: MagicMock, hass: HomeAssistant +) -> None: """Test merge during async config reload.""" conf = await config_util.async_hass_config_yaml(hass) @@ -1094,13 +1097,13 @@ async def test_async_hass_config_yaml_merge(merge_log_err, hass: HomeAssistant) @pytest.fixture -def merge_log_err(hass): +def merge_log_err() -> Generator[MagicMock]: """Patch _merge_log_error from packages.""" with patch("homeassistant.config._LOGGER.error") as logerr: yield logerr -async def test_merge(merge_log_err, hass: HomeAssistant) -> None: +async def test_merge(merge_log_err: MagicMock, hass: HomeAssistant) -> None: """Test if we can merge packages.""" packages = { "pack_dict": {"input_boolean": {"ib1": None}}, @@ -1135,7 +1138,7 @@ async def test_merge(merge_log_err, hass: HomeAssistant) -> None: assert isinstance(config["wake_on_lan"], OrderedDict) -async def test_merge_try_falsy(merge_log_err, hass: HomeAssistant) -> None: +async def test_merge_try_falsy(merge_log_err: MagicMock, hass: HomeAssistant) -> None: """Ensure we don't add falsy items like empty OrderedDict() to list.""" packages = { "pack_falsy_to_lst": {"automation": OrderedDict()}, @@ -1154,7 +1157,7 @@ async def test_merge_try_falsy(merge_log_err, hass: HomeAssistant) -> None: assert len(config["light"]) == 1 -async def test_merge_new(merge_log_err, hass: HomeAssistant) -> None: +async def test_merge_new(merge_log_err: MagicMock, hass: HomeAssistant) -> None: """Test adding new components to outer scope.""" packages = { "pack_1": {"light": [{"platform": "one"}]}, @@ -1175,7 +1178,9 @@ async def test_merge_new(merge_log_err, hass: HomeAssistant) -> None: assert len(config["panel_custom"]) == 1 -async def test_merge_type_mismatch(merge_log_err, hass: HomeAssistant) -> None: +async def test_merge_type_mismatch( + merge_log_err: MagicMock, hass: HomeAssistant +) -> None: """Test if we have a type mismatch for packages.""" packages = { "pack_1": {"input_boolean": [{"ib1": None}]}, @@ -1196,7 +1201,9 @@ async def test_merge_type_mismatch(merge_log_err, hass: HomeAssistant) -> None: assert len(config["light"]) == 2 -async def test_merge_once_only_keys(merge_log_err, hass: HomeAssistant) -> None: +async def test_merge_once_only_keys( + merge_log_err: MagicMock, hass: HomeAssistant +) -> None: """Test if we have a merge for a comp that may occur only once. Keys.""" packages = {"pack_2": {"api": None}} config = {HA_DOMAIN: {config_util.CONF_PACKAGES: packages}, "api": None} @@ -1282,7 +1289,9 @@ async def test_merge_id_schema(hass: HomeAssistant) -> None: assert typ == expected_type, f"{domain} expected {expected_type}, got {typ}" -async def test_merge_duplicate_keys(merge_log_err, hass: HomeAssistant) -> None: +async def test_merge_duplicate_keys( + merge_log_err: MagicMock, hass: HomeAssistant +) -> None: """Test if keys in dicts are duplicates.""" packages = {"pack_1": {"input_select": {"ib1": None}}} config = { diff --git a/tests/test_core.py b/tests/test_core.py index 4c53e1bbd58..a1748638342 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -3094,14 +3094,14 @@ async def test_get_release_channel( assert get_release_channel() == release_channel -def test_is_callback_check_partial(): +def test_is_callback_check_partial() -> None: """Test is_callback_check_partial matches HassJob.""" @ha.callback - def callback_func(): + def callback_func() -> None: pass - def not_callback_func(): + def not_callback_func() -> None: pass assert ha.is_callback(callback_func) @@ -3130,14 +3130,14 @@ def test_is_callback_check_partial(): ) -def test_hassjob_passing_job_type(): +def test_hassjob_passing_job_type() -> None: """Test passing the job type to HassJob when we already know it.""" @ha.callback - def callback_func(): + def callback_func() -> None: pass - def not_callback_func(): + def not_callback_func() -> None: pass assert ( @@ -3237,7 +3237,7 @@ async def test_async_run_job_deprecated( ) -> None: """Test async_run_job warns about its deprecation.""" - async def _test(): + async def _test() -> None: pass hass.async_run_job(_test) @@ -3254,7 +3254,7 @@ async def test_async_add_job_deprecated( ) -> None: """Test async_add_job warns about its deprecation.""" - async def _test(): + async def _test() -> None: pass hass.async_add_job(_test) @@ -3271,7 +3271,7 @@ async def test_async_add_hass_job_deprecated( ) -> None: """Test async_add_hass_job warns about its deprecation.""" - async def _test(): + async def _test() -> None: pass hass.async_add_hass_job(HassJob(_test)) From 5ee418724b8a5cf06776efce52487b8c0cc97b99 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 19 Jun 2024 20:01:02 +0200 Subject: [PATCH 0832/1445] Tweak type annotations of energy websocket handlers (#119957) --- homeassistant/components/energy/websocket_api.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/energy/websocket_api.py b/homeassistant/components/energy/websocket_api.py index 4135c49bf8b..5f48a99133d 100644 --- a/homeassistant/components/energy/websocket_api.py +++ b/homeassistant/components/energy/websocket_api.py @@ -4,7 +4,7 @@ from __future__ import annotations import asyncio from collections import defaultdict -from collections.abc import Awaitable, Callable +from collections.abc import Callable, Coroutine from datetime import timedelta import functools from itertools import chain @@ -39,7 +39,7 @@ type EnergyWebSocketCommandHandler = Callable[ ] type AsyncEnergyWebSocketCommandHandler = Callable[ [HomeAssistant, websocket_api.ActiveConnection, dict[str, Any], EnergyManager], - Awaitable[None], + Coroutine[Any, Any, None], ] @@ -81,11 +81,10 @@ async def async_get_energy_platforms( def _ws_with_manager( - func: Any, -) -> websocket_api.WebSocketCommandHandler: + func: AsyncEnergyWebSocketCommandHandler | EnergyWebSocketCommandHandler, +) -> websocket_api.AsyncWebSocketCommandHandler: """Decorate a function to pass in a manager.""" - @websocket_api.async_response @functools.wraps(func) async def with_manager( hass: HomeAssistant, @@ -107,12 +106,13 @@ def _ws_with_manager( vol.Required("type"): "energy/get_prefs", } ) +@websocket_api.async_response @_ws_with_manager @callback def ws_get_prefs( hass: HomeAssistant, connection: websocket_api.ActiveConnection, - msg: dict, + msg: dict[str, Any], manager: EnergyManager, ) -> None: """Handle get prefs command.""" @@ -131,11 +131,12 @@ def ws_get_prefs( vol.Optional("device_consumption"): [DEVICE_CONSUMPTION_SCHEMA], } ) +@websocket_api.async_response @_ws_with_manager async def ws_save_prefs( hass: HomeAssistant, connection: websocket_api.ActiveConnection, - msg: dict, + msg: dict[str, Any], manager: EnergyManager, ) -> None: """Handle get prefs command.""" @@ -187,6 +188,7 @@ async def ws_validate( vol.Required("type"): "energy/solar_forecast", } ) +@websocket_api.async_response @_ws_with_manager async def ws_solar_forecast( hass: HomeAssistant, From 6c80f865f5229323652b52e6732eb9f7950a640e Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 19 Jun 2024 20:29:40 +0200 Subject: [PATCH 0833/1445] Remove deprecated WLED binary sensor platform (#119984) --- homeassistant/components/wled/__init__.py | 1 - .../components/wled/binary_sensor.py | 60 -------------- .../wled/snapshots/test_binary_sensor.ambr | 82 ------------------- tests/components/wled/test_binary_sensor.py | 48 ----------- 4 files changed, 191 deletions(-) delete mode 100644 homeassistant/components/wled/binary_sensor.py delete mode 100644 tests/components/wled/snapshots/test_binary_sensor.ambr delete mode 100644 tests/components/wled/test_binary_sensor.py diff --git a/homeassistant/components/wled/__init__.py b/homeassistant/components/wled/__init__.py index 3d0add8d198..ba87fb58122 100644 --- a/homeassistant/components/wled/__init__.py +++ b/homeassistant/components/wled/__init__.py @@ -10,7 +10,6 @@ from .const import LOGGER from .coordinator import WLEDDataUpdateCoordinator PLATFORMS = ( - Platform.BINARY_SENSOR, Platform.BUTTON, Platform.LIGHT, Platform.NUMBER, diff --git a/homeassistant/components/wled/binary_sensor.py b/homeassistant/components/wled/binary_sensor.py deleted file mode 100644 index 41f7a4f8ba0..00000000000 --- a/homeassistant/components/wled/binary_sensor.py +++ /dev/null @@ -1,60 +0,0 @@ -"""Support for WLED binary sensor.""" - -from __future__ import annotations - -from homeassistant.components.binary_sensor import ( - BinarySensorDeviceClass, - BinarySensorEntity, -) -from homeassistant.const import EntityCategory -from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity_platform import AddEntitiesCallback - -from . import WLEDConfigEntry -from .coordinator import WLEDDataUpdateCoordinator -from .entity import WLEDEntity - - -async def async_setup_entry( - hass: HomeAssistant, - entry: WLEDConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up a WLED binary sensor based on a config entry.""" - async_add_entities( - [ - WLEDUpdateBinarySensor(entry.runtime_data), - ] - ) - - -class WLEDUpdateBinarySensor(WLEDEntity, BinarySensorEntity): - """Defines a WLED firmware binary sensor.""" - - _attr_entity_category = EntityCategory.DIAGNOSTIC - _attr_device_class = BinarySensorDeviceClass.UPDATE - _attr_translation_key = "firmware" - - # Disabled by default, as this entity is deprecated. - _attr_entity_registry_enabled_default = False - - def __init__(self, coordinator: WLEDDataUpdateCoordinator) -> None: - """Initialize the button entity.""" - super().__init__(coordinator=coordinator) - self._attr_unique_id = f"{coordinator.data.info.mac_address}_update" - - @property - def is_on(self) -> bool: - """Return the state of the sensor.""" - current = self.coordinator.data.info.version - beta = self.coordinator.data.info.version_latest_beta - stable = self.coordinator.data.info.version_latest_stable - - return current is not None and ( - (stable is not None and stable > current) - or ( - beta is not None - and (current.alpha or current.beta or current.release_candidate) - and beta > current - ) - ) diff --git a/tests/components/wled/snapshots/test_binary_sensor.ambr b/tests/components/wled/snapshots/test_binary_sensor.ambr deleted file mode 100644 index b9a083336d2..00000000000 --- a/tests/components/wled/snapshots/test_binary_sensor.ambr +++ /dev/null @@ -1,82 +0,0 @@ -# serializer version: 1 -# name: test_update_available - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'update', - 'friendly_name': 'WLED RGB Light Firmware', - }), - 'context': , - 'entity_id': 'binary_sensor.wled_rgb_light_firmware', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_update_available.1 - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'binary_sensor', - 'entity_category': , - 'entity_id': 'binary_sensor.wled_rgb_light_firmware', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Firmware', - 'platform': 'wled', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'firmware', - 'unique_id': 'aabbccddeeff_update', - 'unit_of_measurement': None, - }) -# --- -# name: test_update_available.2 - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': 'http://127.0.0.1', - 'connections': set({ - tuple( - 'mac', - 'aa:bb:cc:dd:ee:ff', - ), - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': 'esp8266', - 'id': , - 'identifiers': set({ - tuple( - 'wled', - 'aabbccddeeff', - ), - }), - 'is_new': False, - 'labels': set({ - }), - 'manufacturer': 'WLED', - 'model': 'DIY light', - 'name': 'WLED RGB Light', - 'name_by_user': None, - 'serial_number': None, - 'suggested_area': None, - 'sw_version': '0.8.5', - 'via_device_id': None, - }) -# --- diff --git a/tests/components/wled/test_binary_sensor.py b/tests/components/wled/test_binary_sensor.py deleted file mode 100644 index aa75b0c6696..00000000000 --- a/tests/components/wled/test_binary_sensor.py +++ /dev/null @@ -1,48 +0,0 @@ -"""Tests for the WLED binary sensor platform.""" - -import pytest -from syrupy.assertion import SnapshotAssertion - -from homeassistant.const import STATE_OFF -from homeassistant.core import HomeAssistant -from homeassistant.helpers import device_registry as dr, entity_registry as er - -pytestmark = pytest.mark.usefixtures("init_integration") - - -@pytest.mark.usefixtures("entity_registry_enabled_by_default") -async def test_update_available( - hass: HomeAssistant, - device_registry: dr.DeviceRegistry, - entity_registry: er.EntityRegistry, - snapshot: SnapshotAssertion, -) -> None: - """Test the firmware update binary sensor.""" - assert (state := hass.states.get("binary_sensor.wled_rgb_light_firmware")) - assert state == snapshot - - assert (entity_entry := entity_registry.async_get(state.entity_id)) - assert entity_entry == snapshot - - assert entity_entry.device_id - assert (device_entry := device_registry.async_get(entity_entry.device_id)) - assert device_entry == snapshot - - -@pytest.mark.usefixtures("entity_registry_enabled_by_default") -@pytest.mark.parametrize("device_fixture", ["rgb_websocket"]) -async def test_no_update_available(hass: HomeAssistant) -> None: - """Test the update binary sensor. There is no update available.""" - assert (state := hass.states.get("binary_sensor.wled_websocket_firmware")) - assert state.state == STATE_OFF - - -async def test_disabled_by_default( - hass: HomeAssistant, entity_registry: er.EntityRegistry -) -> None: - """Test that the binary update sensor is disabled by default.""" - assert not hass.states.get("binary_sensor.wled_rgb_light_firmware") - - assert (entry := entity_registry.async_get("binary_sensor.wled_rgb_light_firmware")) - assert entry.disabled - assert entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION From 528422d2389bcd44f1f8efbe3150ced88b89ac9c Mon Sep 17 00:00:00 2001 From: Pete Sage <76050312+PeteRager@users.noreply.github.com> Date: Wed, 19 Jun 2024 14:37:26 -0400 Subject: [PATCH 0834/1445] Address Hydrawise review (#119965) adjust formatting --- homeassistant/components/hydrawise/binary_sensor.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/hydrawise/binary_sensor.py b/homeassistant/components/hydrawise/binary_sensor.py index e8426e5423a..52b4c28d718 100644 --- a/homeassistant/components/hydrawise/binary_sensor.py +++ b/homeassistant/components/hydrawise/binary_sensor.py @@ -31,8 +31,10 @@ CONTROLLER_BINARY_SENSORS: tuple[HydrawiseBinarySensorEntityDescription, ...] = HydrawiseBinarySensorEntityDescription( key="status", device_class=BinarySensorDeviceClass.CONNECTIVITY, - value_fn=lambda status_sensor: status_sensor.coordinator.last_update_success - and status_sensor.controller.online, + value_fn=( + lambda status_sensor: status_sensor.coordinator.last_update_success + and status_sensor.controller.online + ), # Connectivtiy sensor is always available always_available=True, ), From d9c7887bbf3efd20ca916512f7555ffda352ae64 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 19 Jun 2024 14:11:36 -0500 Subject: [PATCH 0835/1445] Update yalexs to 6.4.0 (#119987) --- homeassistant/components/august/binary_sensor.py | 10 ++-------- homeassistant/components/august/lock.py | 5 ++--- homeassistant/components/august/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 7 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/august/binary_sensor.py b/homeassistant/components/august/binary_sensor.py index aeeaf9f690c..415b77d3fe9 100644 --- a/homeassistant/components/august/binary_sensor.py +++ b/homeassistant/components/august/binary_sensor.py @@ -8,12 +8,7 @@ from datetime import datetime, timedelta from functools import partial import logging -from yalexs.activity import ( - ACTION_DOORBELL_CALL_MISSED, - SOURCE_PUBNUB, - Activity, - ActivityType, -) +from yalexs.activity import ACTION_DOORBELL_CALL_MISSED, Activity, ActivityType from yalexs.doorbell import DoorbellDetail from yalexs.lock import LockDetail, LockDoorStatus from yalexs.manager.const import ACTIVITY_UPDATE_INTERVAL @@ -175,8 +170,7 @@ class AugustDoorBinarySensor(AugustDescriptionEntity, BinarySensorEntity): """Get the latest state of the sensor and update activity.""" if door_activity := self._get_latest({ActivityType.DOOR_OPERATION}): update_lock_detail_from_activity(self._detail, door_activity) - # If the source is pubnub the lock must be online since its a live update - if door_activity.source == SOURCE_PUBNUB: + if door_activity.was_pushed: self._detail.set_online(True) if bridge_activity := self._get_latest({ActivityType.BRIDGE_OPERATION}): diff --git a/homeassistant/components/august/lock.py b/homeassistant/components/august/lock.py index 7aee612aa41..5382c710229 100644 --- a/homeassistant/components/august/lock.py +++ b/homeassistant/components/august/lock.py @@ -7,7 +7,7 @@ import logging from typing import Any from aiohttp import ClientResponseError -from yalexs.activity import SOURCE_PUBNUB, ActivityType, ActivityTypes +from yalexs.activity import ActivityType, ActivityTypes from yalexs.lock import Lock, LockStatus from yalexs.util import get_latest_activity, update_lock_detail_from_activity @@ -111,8 +111,7 @@ class AugustLock(AugustEntityMixin, RestoreEntity, LockEntity): if latest_activity := get_latest_activity( lock_activity_without_operator, lock_activity ): - if latest_activity.source == SOURCE_PUBNUB: - # If the source is pubnub the lock must be online since its a live update + if latest_activity.was_pushed: self._detail.set_online(True) update_lock_detail_from_activity(detail, latest_activity) diff --git a/homeassistant/components/august/manifest.json b/homeassistant/components/august/manifest.json index d4f82fa0aa1..13658e7401d 100644 --- a/homeassistant/components/august/manifest.json +++ b/homeassistant/components/august/manifest.json @@ -28,5 +28,5 @@ "documentation": "https://www.home-assistant.io/integrations/august", "iot_class": "cloud_push", "loggers": ["pubnub", "yalexs"], - "requirements": ["yalexs==6.3.0", "yalexs-ble==2.4.2"] + "requirements": ["yalexs==6.4.0", "yalexs-ble==2.4.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 5d54d7d8b04..0eb9a4e8f80 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2939,7 +2939,7 @@ yalesmartalarmclient==0.3.9 yalexs-ble==2.4.2 # homeassistant.components.august -yalexs==6.3.0 +yalexs==6.4.0 # homeassistant.components.yeelight yeelight==0.7.14 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7c4fa5d1de6..b48af464f60 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2295,7 +2295,7 @@ yalesmartalarmclient==0.3.9 yalexs-ble==2.4.2 # homeassistant.components.august -yalexs==6.3.0 +yalexs==6.4.0 # homeassistant.components.yeelight yeelight==0.7.14 From 52bf3a028fda700964fc1a8c768235f354f90d04 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 19 Jun 2024 22:16:16 +0200 Subject: [PATCH 0836/1445] Move Nanoleaf event canceling (#119909) * Move Nanoleaf event canceling * Fix * Fix --- homeassistant/components/nanoleaf/__init__.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/nanoleaf/__init__.py b/homeassistant/components/nanoleaf/__init__.py index c8211969f87..5abddfa6778 100644 --- a/homeassistant/components/nanoleaf/__init__.py +++ b/homeassistant/components/nanoleaf/__init__.py @@ -3,6 +3,7 @@ from __future__ import annotations import asyncio +from contextlib import suppress from dataclasses import dataclass import logging @@ -34,7 +35,6 @@ class NanoleafEntryData: device: Nanoleaf coordinator: NanoleafCoordinator - event_listener: asyncio.Task async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: @@ -80,8 +80,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) ) + async def _cancel_listener() -> None: + event_listener.cancel() + with suppress(asyncio.CancelledError): + await event_listener + + entry.async_on_unload(_cancel_listener) + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = NanoleafEntryData( - nanoleaf, coordinator, event_listener + nanoleaf, coordinator ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) @@ -91,7 +98,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - entry_data: NanoleafEntryData = hass.data[DOMAIN].pop(entry.entry_id) - entry_data.event_listener.cancel() - return True + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + hass.data[DOMAIN].pop(entry.entry_id) + return unload_ok From 49349de74e6beaf50a9064e2c4e569ecd83e56c4 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 19 Jun 2024 22:40:13 +0200 Subject: [PATCH 0837/1445] Unifi break out switch availability test to separate test (#119990) --- tests/components/unifi/test_switch.py | 127 ++++++++++---------------- 1 file changed, 50 insertions(+), 77 deletions(-) diff --git a/tests/components/unifi/test_switch.py b/tests/components/unifi/test_switch.py index 3f2e82be7d2..b0ae8bde445 100644 --- a/tests/components/unifi/test_switch.py +++ b/tests/components/unifi/test_switch.py @@ -761,6 +761,19 @@ WLAN = { "x_passphrase": "password", } +PORT_FORWARD_PLEX = { + "_id": "5a32aa4ee4b0412345678911", + "dst_port": "12345", + "enabled": True, + "fwd_port": "23456", + "fwd": "10.0.0.2", + "name": "plex", + "pfwd_interface": "wan", + "proto": "tcp_udp", + "site_id": "5a32aa4ee4b0412345678910", + "src": "any", +} + @pytest.mark.parametrize("client_payload", [[CONTROLLER_HOST]]) @pytest.mark.parametrize("device_payload", [[DEVICE_1]]) @@ -983,9 +996,7 @@ async def test_block_switches( @pytest.mark.parametrize("dpi_app_payload", [DPI_APPS]) @pytest.mark.parametrize("dpi_group_payload", [DPI_GROUPS]) @pytest.mark.usefixtures("config_entry_setup") -async def test_dpi_switches( - hass: HomeAssistant, mock_websocket_message, mock_websocket_state -) -> None: +async def test_dpi_switches(hass: HomeAssistant, mock_websocket_message) -> None: """Test the update_items function with some clients.""" assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 1 @@ -999,16 +1010,6 @@ async def test_dpi_switches( assert hass.states.get("switch.block_media_streaming").state == STATE_OFF - # Availability signalling - - # Controller disconnects - await mock_websocket_state.disconnect() - assert hass.states.get("switch.block_media_streaming").state == STATE_UNAVAILABLE - - # Controller reconnects - await mock_websocket_state.reconnect() - assert hass.states.get("switch.block_media_streaming").state == STATE_OFF - # Remove app mock_websocket_message(data=DPI_GROUP_REMOVE_APP) await hass.async_block_till_done() @@ -1085,7 +1086,6 @@ async def test_outlet_switches( mock_websocket_message, config_entry_setup: ConfigEntry, device_payload: list[dict[str, Any]], - mock_websocket_state, entity_id: str, outlet_index: int, expected_switches: int, @@ -1144,16 +1144,6 @@ async def test_outlet_switches( "outlet_overrides": expected_on_overrides } - # Availability signalling - - # Controller disconnects - await mock_websocket_state.disconnect() - assert hass.states.get(f"switch.{entity_id}").state == STATE_UNAVAILABLE - - # Controller reconnects - await mock_websocket_state.reconnect() - assert hass.states.get(f"switch.{entity_id}").state == STATE_OFF - # Device gets disabled device_1["disabled"] = True mock_websocket_message(message=MessageKey.DEVICE, data=device_1) @@ -1274,7 +1264,6 @@ async def test_poe_port_switches( entity_registry: er.EntityRegistry, aioclient_mock: AiohttpClientMocker, mock_websocket_message, - mock_websocket_state, config_entry_setup: ConfigEntry, device_payload: list[dict[str, Any]], ) -> None: @@ -1292,7 +1281,6 @@ async def test_poe_port_switches( entity_registry.async_update_entity( entity_id="switch.mock_name_port_2_poe", disabled_by=None ) - # await hass.async_block_till_done() async_fire_time_changed( hass, @@ -1356,16 +1344,6 @@ async def test_poe_port_switches( ] } - # Availability signalling - - # Controller disconnects - await mock_websocket_state.disconnect() - assert hass.states.get("switch.mock_name_port_1_poe").state == STATE_UNAVAILABLE - - # Controller reconnects - await mock_websocket_state.reconnect() - assert hass.states.get("switch.mock_name_port_1_poe").state == STATE_OFF - # Device gets disabled device_1["disabled"] = True mock_websocket_message(message=MessageKey.DEVICE, data=device_1) @@ -1385,7 +1363,6 @@ async def test_wlan_switches( entity_registry: er.EntityRegistry, aioclient_mock: AiohttpClientMocker, mock_websocket_message, - mock_websocket_state, config_entry_setup: ConfigEntry, wlan_payload: list[dict[str, Any]], ) -> None: @@ -1435,42 +1412,13 @@ async def test_wlan_switches( assert aioclient_mock.call_count == 2 assert aioclient_mock.mock_calls[1][2] == {"enabled": True} - # Availability signalling - # Controller disconnects - await mock_websocket_state.disconnect() - assert hass.states.get("switch.ssid_1").state == STATE_UNAVAILABLE - - # Controller reconnects - await mock_websocket_state.reconnect() - assert hass.states.get("switch.ssid_1").state == STATE_OFF - - -@pytest.mark.parametrize( - "port_forward_payload", - [ - [ - { - "_id": "5a32aa4ee4b0412345678911", - "dst_port": "12345", - "enabled": True, - "fwd_port": "23456", - "fwd": "10.0.0.2", - "name": "plex", - "pfwd_interface": "wan", - "proto": "tcp_udp", - "site_id": "5a32aa4ee4b0412345678910", - "src": "any", - } - ] - ], -) +@pytest.mark.parametrize("port_forward_payload", [[PORT_FORWARD_PLEX]]) async def test_port_forwarding_switches( hass: HomeAssistant, entity_registry: er.EntityRegistry, aioclient_mock: AiohttpClientMocker, mock_websocket_message, - mock_websocket_state, config_entry_setup: ConfigEntry, port_forward_payload: list[dict[str, Any]], ) -> None: @@ -1522,16 +1470,6 @@ async def test_port_forwarding_switches( assert aioclient_mock.call_count == 2 assert aioclient_mock.mock_calls[1][2] == port_forward_payload[0] - # Availability signalling - - # Controller disconnects - await mock_websocket_state.disconnect() - assert hass.states.get("switch.unifi_network_plex").state == STATE_UNAVAILABLE - - # Controller reconnects - await mock_websocket_state.reconnect() - assert hass.states.get("switch.unifi_network_plex").state == STATE_OFF - # Remove entity on deleted message mock_websocket_message( message=MessageKey.PORT_FORWARD_DELETED, data=port_forward_payload[0] @@ -1604,3 +1542,38 @@ async def test_updating_unique_id( assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 2 assert hass.states.get("switch.plug_outlet_1") assert hass.states.get("switch.switch_port_1_poe") + + +@pytest.mark.parametrize( + "config_entry_options", [{CONF_BLOCK_CLIENT: [UNBLOCKED["mac"]]}] +) +@pytest.mark.parametrize("client_payload", [[UNBLOCKED]]) +@pytest.mark.parametrize("device_payload", [[DEVICE_1, OUTLET_UP1]]) +@pytest.mark.parametrize("dpi_app_payload", [DPI_APPS]) +@pytest.mark.parametrize("dpi_group_payload", [DPI_GROUPS]) +@pytest.mark.parametrize("port_forward_payload", [[PORT_FORWARD_PLEX]]) +@pytest.mark.parametrize("wlan_payload", [[WLAN]]) +@pytest.mark.usefixtures("config_entry_setup") +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_hub_state_change(hass: HomeAssistant, mock_websocket_state) -> None: + """Verify entities state reflect on hub connection becoming unavailable.""" + entity_ids = ( + "switch.block_client_2", + "switch.mock_name_port_1_poe", + "switch.plug_outlet_1", + "switch.block_media_streaming", + "switch.unifi_network_plex", + "switch.ssid_1", + ) + for entity_id in entity_ids: + assert hass.states.get(entity_id).state == STATE_ON + + # Controller disconnects + await mock_websocket_state.disconnect() + for entity_id in entity_ids: + assert hass.states.get(entity_id).state == STATE_UNAVAILABLE + + # Controller reconnects + await mock_websocket_state.reconnect() + for entity_id in entity_ids: + assert hass.states.get(entity_id).state == STATE_ON From 6dc680d25184e91191cf8127a0d2ac2af17b85c0 Mon Sep 17 00:00:00 2001 From: Alexandre CUER Date: Wed, 19 Jun 2024 22:41:32 +0200 Subject: [PATCH 0838/1445] Use aiohttp.ClientSession in EmoncmsClient (#119989) --- homeassistant/components/emoncms/manifest.json | 2 +- homeassistant/components/emoncms/sensor.py | 3 ++- requirements_all.txt | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/emoncms/manifest.json b/homeassistant/components/emoncms/manifest.json index 4b617b0e2f2..09229d0419a 100644 --- a/homeassistant/components/emoncms/manifest.json +++ b/homeassistant/components/emoncms/manifest.json @@ -4,5 +4,5 @@ "codeowners": ["@borpin", "@alexandrecuer"], "documentation": "https://www.home-assistant.io/integrations/emoncms", "iot_class": "local_polling", - "requirements": ["pyemoncms==0.0.6"] + "requirements": ["pyemoncms==0.0.7"] } diff --git a/homeassistant/components/emoncms/sensor.py b/homeassistant/components/emoncms/sensor.py index 443cd1bd5d0..9208aa2a682 100644 --- a/homeassistant/components/emoncms/sensor.py +++ b/homeassistant/components/emoncms/sensor.py @@ -25,6 +25,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.helpers import template +from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -87,7 +88,7 @@ async def async_setup_platform( if value_template is not None: value_template.hass = hass - emoncms_client = EmoncmsClient(url, apikey) + emoncms_client = EmoncmsClient(url, apikey, session=async_get_clientsession(hass)) elems = await emoncms_client.async_list_feeds() if elems is None: diff --git a/requirements_all.txt b/requirements_all.txt index 0eb9a4e8f80..25da9893ddc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1827,7 +1827,7 @@ pyefergy==22.5.0 pyegps==0.2.5 # homeassistant.components.emoncms -pyemoncms==0.0.6 +pyemoncms==0.0.7 # homeassistant.components.enphase_envoy pyenphase==1.20.3 From 7d14b9c5c8e448d33ec98f046624dcfaf51b3494 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 19 Jun 2024 22:45:59 +0200 Subject: [PATCH 0839/1445] Always create a new HomeAssistant object when falling back to recovery mode (#119969) --- homeassistant/bootstrap.py | 59 ++++++++++++++++++++++++-------------- 1 file changed, 38 insertions(+), 21 deletions(-) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 74196cdc625..8435fe73d40 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -256,22 +256,39 @@ async def async_setup_hass( runtime_config: RuntimeConfig, ) -> core.HomeAssistant | None: """Set up Home Assistant.""" - hass = core.HomeAssistant(runtime_config.config_dir) - async_enable_logging( - hass, - runtime_config.verbose, - runtime_config.log_rotate_days, - runtime_config.log_file, - runtime_config.log_no_color, - ) + def create_hass() -> core.HomeAssistant: + """Create the hass object and do basic setup.""" + hass = core.HomeAssistant(runtime_config.config_dir) + loader.async_setup(hass) - if runtime_config.debug or hass.loop.get_debug(): - hass.config.debug = True + async_enable_logging( + hass, + runtime_config.verbose, + runtime_config.log_rotate_days, + runtime_config.log_file, + runtime_config.log_no_color, + ) + + if runtime_config.debug or hass.loop.get_debug(): + hass.config.debug = True + + hass.config.safe_mode = runtime_config.safe_mode + hass.config.skip_pip = runtime_config.skip_pip + hass.config.skip_pip_packages = runtime_config.skip_pip_packages + + return hass + + async def stop_hass(hass: core.HomeAssistant) -> None: + """Stop hass.""" + # Ask integrations to shut down. It's messy but we can't + # do a clean stop without knowing what is broken + with contextlib.suppress(TimeoutError): + async with hass.timeout.async_timeout(10): + await hass.async_stop() + + hass = create_hass() - hass.config.safe_mode = runtime_config.safe_mode - hass.config.skip_pip = runtime_config.skip_pip - hass.config.skip_pip_packages = runtime_config.skip_pip_packages if runtime_config.skip_pip or runtime_config.skip_pip_packages: _LOGGER.warning( "Skipping pip installation of required modules. This may cause issues" @@ -283,7 +300,6 @@ async def async_setup_hass( _LOGGER.info("Config directory: %s", runtime_config.config_dir) - loader.async_setup(hass) block_async_io.enable() config_dict = None @@ -309,27 +325,28 @@ async def async_setup_hass( if config_dict is None: recovery_mode = True + await stop_hass(hass) + hass = create_hass() elif not basic_setup_success: _LOGGER.warning("Unable to set up core integrations. Activating recovery mode") recovery_mode = True + await stop_hass(hass) + hass = create_hass() elif any(domain not in hass.config.components for domain in CRITICAL_INTEGRATIONS): _LOGGER.warning( "Detected that %s did not load. Activating recovery mode", ",".join(CRITICAL_INTEGRATIONS), ) - # Ask integrations to shut down. It's messy but we can't - # do a clean stop without knowing what is broken - with contextlib.suppress(TimeoutError): - async with hass.timeout.async_timeout(10): - await hass.async_stop() - recovery_mode = True old_config = hass.config old_logging = hass.data.get(DATA_LOGGING) - hass = core.HomeAssistant(old_config.config_dir) + recovery_mode = True + await stop_hass(hass) + hass = create_hass() + if old_logging: hass.data[DATA_LOGGING] = old_logging hass.config.debug = old_config.debug From bae008b0e2d70de15b9417a8a66e163100249957 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Wed, 19 Jun 2024 22:46:30 +0200 Subject: [PATCH 0840/1445] Remove legacy_api_password auth provider (#119976) --- .../auth/providers/legacy_api_password.py | 123 ------------------ homeassistant/components/auth/strings.json | 6 - pylint/plugins/hass_enforce_type_hints.py | 1 - .../providers/test_legacy_api_password.py | 90 ------------- tests/components/api/test_init.py | 19 --- tests/components/http/test_auth.py | 20 +-- tests/components/http/test_init.py | 6 +- tests/components/websocket_api/test_auth.py | 8 +- tests/components/websocket_api/test_sensor.py | 6 +- tests/conftest.py | 16 +-- tests/test_config.py | 4 +- 11 files changed, 14 insertions(+), 285 deletions(-) delete mode 100644 homeassistant/auth/providers/legacy_api_password.py delete mode 100644 tests/auth/providers/test_legacy_api_password.py diff --git a/homeassistant/auth/providers/legacy_api_password.py b/homeassistant/auth/providers/legacy_api_password.py deleted file mode 100644 index f04490a354e..00000000000 --- a/homeassistant/auth/providers/legacy_api_password.py +++ /dev/null @@ -1,123 +0,0 @@ -"""Support Legacy API password auth provider. - -It will be removed when auth system production ready -""" - -from __future__ import annotations - -from collections.abc import Mapping -import hmac -from typing import Any, cast - -import voluptuous as vol - -from homeassistant.core import async_get_hass, callback -from homeassistant.exceptions import HomeAssistantError -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue - -from ..models import AuthFlowResult, Credentials, UserMeta -from . import AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, AuthProvider, LoginFlow - -AUTH_PROVIDER_TYPE = "legacy_api_password" -CONF_API_PASSWORD = "api_password" - -_CONFIG_SCHEMA = AUTH_PROVIDER_SCHEMA.extend( - {vol.Required(CONF_API_PASSWORD): cv.string}, extra=vol.PREVENT_EXTRA -) - - -def _create_repair_and_validate(config: dict[str, Any]) -> dict[str, Any]: - async_create_issue( - async_get_hass(), - "auth", - "deprecated_legacy_api_password", - breaks_in_ha_version="2024.6.0", - is_fixable=False, - severity=IssueSeverity.WARNING, - translation_key="deprecated_legacy_api_password", - ) - - return _CONFIG_SCHEMA(config) # type: ignore[no-any-return] - - -CONFIG_SCHEMA = _create_repair_and_validate - - -LEGACY_USER_NAME = "Legacy API password user" - - -class InvalidAuthError(HomeAssistantError): - """Raised when submitting invalid authentication.""" - - -@AUTH_PROVIDERS.register(AUTH_PROVIDER_TYPE) -class LegacyApiPasswordAuthProvider(AuthProvider): - """An auth provider support legacy api_password.""" - - DEFAULT_TITLE = "Legacy API Password" - - @property - def api_password(self) -> str: - """Return api_password.""" - return str(self.config[CONF_API_PASSWORD]) - - async def async_login_flow(self, context: dict[str, Any] | None) -> LoginFlow: - """Return a flow to login.""" - return LegacyLoginFlow(self) - - @callback - def async_validate_login(self, password: str) -> None: - """Validate password.""" - api_password = str(self.config[CONF_API_PASSWORD]) - - if not hmac.compare_digest( - api_password.encode("utf-8"), password.encode("utf-8") - ): - raise InvalidAuthError - - async def async_get_or_create_credentials( - self, flow_result: Mapping[str, str] - ) -> Credentials: - """Return credentials for this login.""" - credentials = await self.async_credentials() - if credentials: - return credentials[0] - - return self.async_create_credentials({}) - - async def async_user_meta_for_credentials( - self, credentials: Credentials - ) -> UserMeta: - """Return info for the user. - - Will be used to populate info when creating a new user. - """ - return UserMeta(name=LEGACY_USER_NAME, is_active=True) - - -class LegacyLoginFlow(LoginFlow): - """Handler for the login flow.""" - - async def async_step_init( - self, user_input: dict[str, str] | None = None - ) -> AuthFlowResult: - """Handle the step of the form.""" - errors = {} - - if user_input is not None: - try: - cast( - LegacyApiPasswordAuthProvider, self._auth_provider - ).async_validate_login(user_input["password"]) - except InvalidAuthError: - errors["base"] = "invalid_auth" - - if not errors: - return await self.async_finish({}) - - return self.async_show_form( - step_id="init", - data_schema=vol.Schema({vol.Required("password"): str}), - errors=errors, - ) diff --git a/homeassistant/components/auth/strings.json b/homeassistant/components/auth/strings.json index 0dd3ee64cdf..d386bb7a488 100644 --- a/homeassistant/components/auth/strings.json +++ b/homeassistant/components/auth/strings.json @@ -31,11 +31,5 @@ "invalid_code": "Invalid code, please try again." } } - }, - "issues": { - "deprecated_legacy_api_password": { - "title": "The legacy API password is deprecated", - "description": "The legacy API password authentication provider is deprecated and will be removed. Please remove it from your YAML configuration and use the default Home Assistant authentication provider instead." - } } } diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index feda93fc7fa..6dd19d96d01 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -132,7 +132,6 @@ _TEST_FIXTURES: dict[str, list[str] | str] = { "hass_ws_client": "WebSocketGenerator", "init_tts_cache_dir_side_effect": "Any", "issue_registry": "IssueRegistry", - "legacy_auth": "LegacyApiPasswordAuthProvider", "local_auth": "HassAuthProvider", "mock_async_zeroconf": "MagicMock", "mock_bleak_scanner_start": "MagicMock", diff --git a/tests/auth/providers/test_legacy_api_password.py b/tests/auth/providers/test_legacy_api_password.py deleted file mode 100644 index a9ef03fd27b..00000000000 --- a/tests/auth/providers/test_legacy_api_password.py +++ /dev/null @@ -1,90 +0,0 @@ -"""Tests for the legacy_api_password auth provider.""" - -import pytest - -from homeassistant import auth, data_entry_flow -from homeassistant.auth import auth_store -from homeassistant.auth.providers import legacy_api_password -from homeassistant.core import HomeAssistant -import homeassistant.helpers.issue_registry as ir -from homeassistant.setup import async_setup_component - -from tests.common import ensure_auth_manager_loaded - -CONFIG = {"type": "legacy_api_password", "api_password": "test-password"} - - -@pytest.fixture -async def store(hass): - """Mock store.""" - store = auth_store.AuthStore(hass) - await store.async_load() - return store - - -@pytest.fixture -def provider(hass, store): - """Mock provider.""" - return legacy_api_password.LegacyApiPasswordAuthProvider(hass, store, CONFIG) - - -@pytest.fixture -def manager(hass, store, provider): - """Mock manager.""" - return auth.AuthManager(hass, store, {(provider.type, provider.id): provider}, {}) - - -async def test_create_new_credential(manager, provider) -> None: - """Test that we create a new credential.""" - credentials = await provider.async_get_or_create_credentials({}) - assert credentials.is_new is True - - user = await manager.async_get_or_create_user(credentials) - assert user.name == legacy_api_password.LEGACY_USER_NAME - assert user.is_active - - -async def test_only_one_credentials(manager, provider) -> None: - """Call create twice will return same credential.""" - credentials = await provider.async_get_or_create_credentials({}) - await manager.async_get_or_create_user(credentials) - credentials2 = await provider.async_get_or_create_credentials({}) - assert credentials2.id == credentials.id - assert credentials2.is_new is False - - -async def test_verify_login(hass: HomeAssistant, provider) -> None: - """Test login using legacy api password auth provider.""" - provider.async_validate_login("test-password") - with pytest.raises(legacy_api_password.InvalidAuthError): - provider.async_validate_login("invalid-password") - - -async def test_login_flow_works(hass: HomeAssistant, manager) -> None: - """Test wrong config.""" - result = await manager.login_flow.async_init(handler=("legacy_api_password", None)) - assert result["type"] == data_entry_flow.FlowResultType.FORM - - result = await manager.login_flow.async_configure( - flow_id=result["flow_id"], user_input={"password": "not-hello"} - ) - assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["errors"]["base"] == "invalid_auth" - - result = await manager.login_flow.async_configure( - flow_id=result["flow_id"], user_input={"password": "test-password"} - ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY - - -async def test_create_repair_issue( - hass: HomeAssistant, issue_registry: ir.IssueRegistry -) -> None: - """Test legacy api password auth provider creates a reapir issue.""" - hass.auth = await auth.auth_manager_from_config(hass, [CONFIG], []) - ensure_auth_manager_loaded(hass.auth) - await async_setup_component(hass, "auth", {}) - - assert issue_registry.async_get_issue( - domain="auth", issue_id="deprecated_legacy_api_password" - ) diff --git a/tests/components/api/test_init.py b/tests/components/api/test_init.py index 5443d48452f..a1453315dbf 100644 --- a/tests/components/api/test_init.py +++ b/tests/components/api/test_init.py @@ -12,9 +12,6 @@ import voluptuous as vol from homeassistant import const from homeassistant.auth.models import Credentials -from homeassistant.auth.providers.legacy_api_password import ( - LegacyApiPasswordAuthProvider, -) from homeassistant.bootstrap import DATA_LOGGING import homeassistant.core as ha from homeassistant.core import HomeAssistant @@ -731,22 +728,6 @@ async def test_rendering_template_admin( assert resp.status == HTTPStatus.UNAUTHORIZED -async def test_rendering_template_legacy_user( - hass: HomeAssistant, - mock_api_client: TestClient, - aiohttp_client: ClientSessionGenerator, - legacy_auth: LegacyApiPasswordAuthProvider, -) -> None: - """Test rendering a template with legacy API password.""" - hass.states.async_set("sensor.temperature", 10) - client = await aiohttp_client(hass.http.app) - resp = await client.post( - const.URL_API_TEMPLATE, - json={"template": "{{ states.sensor.temperature.state }}"}, - ) - assert resp.status == HTTPStatus.UNAUTHORIZED - - async def test_api_call_service_not_found( hass: HomeAssistant, mock_api_client: TestClient ) -> None: diff --git a/tests/components/http/test_auth.py b/tests/components/http/test_auth.py index aa6ed64ff57..20dfe0a3710 100644 --- a/tests/components/http/test_auth.py +++ b/tests/components/http/test_auth.py @@ -15,9 +15,7 @@ import yarl from homeassistant.auth.const import GROUP_ID_READ_ONLY from homeassistant.auth.models import User from homeassistant.auth.providers import trusted_networks -from homeassistant.auth.providers.legacy_api_password import ( - LegacyApiPasswordAuthProvider, -) +from homeassistant.auth.providers.homeassistant import HassAuthProvider from homeassistant.components import websocket_api from homeassistant.components.http import KEY_HASS from homeassistant.components.http.auth import ( @@ -76,14 +74,6 @@ async def mock_handler(request): return web.json_response(data={"user_id": user_id}) -async def get_legacy_user(auth): - """Get the user in legacy_api_password auth provider.""" - provider = auth.get_auth_provider("legacy_api_password", None) - return await auth.async_get_or_create_user( - await provider.async_get_or_create_credentials({}) - ) - - @pytest.fixture def app(hass): """Fixture to set up a web.Application.""" @@ -126,7 +116,7 @@ async def test_auth_middleware_loaded_by_default(hass: HomeAssistant) -> None: async def test_cant_access_with_password_in_header( app, aiohttp_client: ClientSessionGenerator, - legacy_auth: LegacyApiPasswordAuthProvider, + local_auth: HassAuthProvider, hass: HomeAssistant, ) -> None: """Test access with password in header.""" @@ -143,7 +133,7 @@ async def test_cant_access_with_password_in_header( async def test_cant_access_with_password_in_query( app, aiohttp_client: ClientSessionGenerator, - legacy_auth: LegacyApiPasswordAuthProvider, + local_auth: HassAuthProvider, hass: HomeAssistant, ) -> None: """Test access with password in URL.""" @@ -164,7 +154,7 @@ async def test_basic_auth_does_not_work( app, aiohttp_client: ClientSessionGenerator, hass: HomeAssistant, - legacy_auth: LegacyApiPasswordAuthProvider, + local_auth: HassAuthProvider, ) -> None: """Test access with basic authentication.""" await async_setup_auth(hass, app) @@ -278,7 +268,7 @@ async def test_auth_active_access_with_trusted_ip( async def test_auth_legacy_support_api_password_cannot_access( app, aiohttp_client: ClientSessionGenerator, - legacy_auth: LegacyApiPasswordAuthProvider, + local_auth: HassAuthProvider, hass: HomeAssistant, ) -> None: """Test access using api_password if auth.support_legacy.""" diff --git a/tests/components/http/test_init.py b/tests/components/http/test_init.py index 995be3954d9..7a9fb329fcd 100644 --- a/tests/components/http/test_init.py +++ b/tests/components/http/test_init.py @@ -11,9 +11,7 @@ from unittest.mock import Mock, patch import pytest -from homeassistant.auth.providers.legacy_api_password import ( - LegacyApiPasswordAuthProvider, -) +from homeassistant.auth.providers.homeassistant import HassAuthProvider from homeassistant.components import http from homeassistant.core import HomeAssistant from homeassistant.helpers.http import KEY_HASS @@ -115,7 +113,7 @@ async def test_not_log_password( hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator, caplog: pytest.LogCaptureFixture, - legacy_auth: LegacyApiPasswordAuthProvider, + local_auth: HassAuthProvider, ) -> None: """Test access with password doesn't get logged.""" assert await async_setup_component(hass, "api", {"http": {}}) diff --git a/tests/components/websocket_api/test_auth.py b/tests/components/websocket_api/test_auth.py index 595dc7dcc32..62298098adc 100644 --- a/tests/components/websocket_api/test_auth.py +++ b/tests/components/websocket_api/test_auth.py @@ -6,9 +6,7 @@ import aiohttp from aiohttp import WSMsgType import pytest -from homeassistant.auth.providers.legacy_api_password import ( - LegacyApiPasswordAuthProvider, -) +from homeassistant.auth.providers.homeassistant import HassAuthProvider from homeassistant.components.websocket_api.auth import ( TYPE_AUTH, TYPE_AUTH_INVALID, @@ -51,7 +49,7 @@ def track_connected(hass): async def test_auth_events( hass: HomeAssistant, no_auth_websocket_client, - legacy_auth: LegacyApiPasswordAuthProvider, + local_auth: HassAuthProvider, hass_access_token: str, track_connected, ) -> None: @@ -174,7 +172,7 @@ async def test_auth_active_with_password_not_allow( async def test_auth_legacy_support_with_password( hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator, - legacy_auth: LegacyApiPasswordAuthProvider, + local_auth: HassAuthProvider, ) -> None: """Test authenticating with a token.""" assert await async_setup_component(hass, "websocket_api", {}) diff --git a/tests/components/websocket_api/test_sensor.py b/tests/components/websocket_api/test_sensor.py index 72b39b39354..3af02dc8f2b 100644 --- a/tests/components/websocket_api/test_sensor.py +++ b/tests/components/websocket_api/test_sensor.py @@ -1,8 +1,6 @@ """Test cases for the API stream sensor.""" -from homeassistant.auth.providers.legacy_api_password import ( - LegacyApiPasswordAuthProvider, -) +from homeassistant.auth.providers.homeassistant import HassAuthProvider from homeassistant.bootstrap import async_setup_component from homeassistant.components.websocket_api.auth import TYPE_AUTH_REQUIRED from homeassistant.components.websocket_api.http import URL @@ -17,7 +15,7 @@ async def test_websocket_api( hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator, hass_access_token: str, - legacy_auth: LegacyApiPasswordAuthProvider, + local_auth: HassAuthProvider, ) -> None: """Test API streams.""" await async_setup_component( diff --git a/tests/conftest.py b/tests/conftest.py index b2b0eb3487c..14e6f97d7c4 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -43,7 +43,7 @@ from . import patch_time # noqa: F401, isort:skip from homeassistant import core as ha, loader, runner from homeassistant.auth.const import GROUP_ID_ADMIN, GROUP_ID_READ_ONLY from homeassistant.auth.models import Credentials -from homeassistant.auth.providers import homeassistant, legacy_api_password +from homeassistant.auth.providers import homeassistant from homeassistant.components.device_tracker.legacy import Device from homeassistant.components.websocket_api.auth import ( TYPE_AUTH, @@ -751,20 +751,6 @@ async def hass_supervisor_access_token( return hass.auth.async_create_access_token(refresh_token) -@pytest.fixture -def legacy_auth( - hass: HomeAssistant, -) -> legacy_api_password.LegacyApiPasswordAuthProvider: - """Load legacy API password provider.""" - prv = legacy_api_password.LegacyApiPasswordAuthProvider( - hass, - hass.auth._store, - {"type": "legacy_api_password", "api_password": "test-password"}, - ) - hass.auth._providers[(prv.type, prv.id)] = prv - return prv - - @pytest.fixture async def local_auth(hass: HomeAssistant) -> homeassistant.HassAuthProvider: """Load local auth provider.""" diff --git a/tests/test_config.py b/tests/test_config.py index a30498b422a..51c72472a4f 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -1335,7 +1335,6 @@ async def test_auth_provider_config(hass: HomeAssistant) -> None: "time_zone": "GMT", CONF_AUTH_PROVIDERS: [ {"type": "homeassistant"}, - {"type": "legacy_api_password", "api_password": "some-pass"}, ], CONF_AUTH_MFA_MODULES: [{"type": "totp"}, {"type": "totp", "id": "second"}], } @@ -1343,9 +1342,8 @@ async def test_auth_provider_config(hass: HomeAssistant) -> None: del hass.auth await config_util.async_process_ha_core_config(hass, core_config) - assert len(hass.auth.auth_providers) == 2 + assert len(hass.auth.auth_providers) == 1 assert hass.auth.auth_providers[0].type == "homeassistant" - assert hass.auth.auth_providers[1].type == "legacy_api_password" assert len(hass.auth.auth_mfa_modules) == 2 assert hass.auth.auth_mfa_modules[0].id == "totp" assert hass.auth.auth_mfa_modules[1].id == "second" From 42b62ec42751ee4d3203e40b9645df65af8a7c6e Mon Sep 17 00:00:00 2001 From: Artur Pragacz <49985303+arturpragacz@users.noreply.github.com> Date: Wed, 19 Jun 2024 22:48:34 +0200 Subject: [PATCH 0841/1445] Fix Onkyo zone volume (#119949) --- homeassistant/components/onkyo/media_player.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/onkyo/media_player.py b/homeassistant/components/onkyo/media_player.py index 7575443c793..97e0b3e3631 100644 --- a/homeassistant/components/onkyo/media_player.py +++ b/homeassistant/components/onkyo/media_player.py @@ -341,7 +341,7 @@ class OnkyoDevice(MediaPlayerEntity): del self._attr_extra_state_attributes[ATTR_PRESET] self._attr_is_volume_muted = bool(mute_raw[1] == "on") - # AMP_VOL/MAX_RECEIVER_VOL*(MAX_VOL/100) + # AMP_VOL / (MAX_RECEIVER_VOL * (MAX_VOL / 100)) self._attr_volume_level = volume_raw[1] / ( self._receiver_max_volume * self._max_volume / 100 ) @@ -511,9 +511,9 @@ class OnkyoDeviceZone(OnkyoDevice): elif ATTR_PRESET in self._attr_extra_state_attributes: del self._attr_extra_state_attributes[ATTR_PRESET] if self._supports_volume: - # AMP_VOL/MAX_RECEIVER_VOL*(MAX_VOL/100) - self._attr_volume_level = ( - volume_raw[1] / self._receiver_max_volume * (self._max_volume / 100) + # AMP_VOL / (MAX_RECEIVER_VOL * (MAX_VOL / 100)) + self._attr_volume_level = volume_raw[1] / ( + self._receiver_max_volume * self._max_volume / 100 ) @property From f32cb8545c4fff353037031ff3ba81b4a49746cd Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 19 Jun 2024 23:07:56 +0200 Subject: [PATCH 0842/1445] Use MockHAClientWebSocket.send_json_auto_id in blueprint tests (#119956) --- .../blueprint/test_websocket_api.py | 48 +++++-------------- 1 file changed, 12 insertions(+), 36 deletions(-) diff --git a/tests/components/blueprint/test_websocket_api.py b/tests/components/blueprint/test_websocket_api.py index 4052e7c3316..1f684b451ed 100644 --- a/tests/components/blueprint/test_websocket_api.py +++ b/tests/components/blueprint/test_websocket_api.py @@ -46,11 +46,10 @@ async def test_list_blueprints( ) -> None: """Test listing blueprints.""" client = await hass_ws_client(hass) - await client.send_json({"id": 5, "type": "blueprint/list", "domain": "automation"}) + await client.send_json_auto_id({"type": "blueprint/list", "domain": "automation"}) msg = await client.receive_json() - assert msg["id"] == 5 assert msg["success"] blueprints = msg["result"] assert blueprints == { @@ -80,13 +79,10 @@ async def test_list_blueprints_non_existing_domain( ) -> None: """Test listing blueprints.""" client = await hass_ws_client(hass) - await client.send_json( - {"id": 5, "type": "blueprint/list", "domain": "not_existing"} - ) + await client.send_json_auto_id({"type": "blueprint/list", "domain": "not_existing"}) msg = await client.receive_json() - assert msg["id"] == 5 assert msg["success"] blueprints = msg["result"] assert blueprints == {} @@ -108,9 +104,8 @@ async def test_import_blueprint( ) client = await hass_ws_client(hass) - await client.send_json( + await client.send_json_auto_id( { - "id": 5, "type": "blueprint/import", "url": "https://github.com/balloob/home-assistant-config/blob/main/blueprints/automation/motion_light.yaml", } @@ -118,7 +113,6 @@ async def test_import_blueprint( msg = await client.receive_json() - assert msg["id"] == 5 assert msg["success"] assert msg["result"] == { "suggested_filename": "balloob/motion_light", @@ -157,9 +151,8 @@ async def test_import_blueprint_update( ) client = await hass_ws_client(hass) - await client.send_json( + await client.send_json_auto_id( { - "id": 5, "type": "blueprint/import", "url": "https://github.com/in_folder/home-assistant-config/blob/main/blueprints/automation/in_folder_blueprint.yaml", } @@ -167,7 +160,6 @@ async def test_import_blueprint_update( msg = await client.receive_json() - assert msg["id"] == 5 assert msg["success"] assert msg["result"] == { "suggested_filename": "in_folder/in_folder_blueprint", @@ -196,9 +188,8 @@ async def test_save_blueprint( with patch("pathlib.Path.write_text") as write_mock: client = await hass_ws_client(hass) - await client.send_json( + await client.send_json_auto_id( { - "id": 6, "type": "blueprint/save", "path": "test_save", "yaml": raw_data, @@ -209,7 +200,6 @@ async def test_save_blueprint( msg = await client.receive_json() - assert msg["id"] == 6 assert msg["success"] assert write_mock.mock_calls # There are subtle differences in the dumper quoting @@ -245,9 +235,8 @@ async def test_save_existing_file( """Test saving blueprints.""" client = await hass_ws_client(hass) - await client.send_json( + await client.send_json_auto_id( { - "id": 7, "type": "blueprint/save", "path": "test_event_service", "yaml": 'blueprint: {name: "name", domain: "automation"}', @@ -258,7 +247,6 @@ async def test_save_existing_file( msg = await client.receive_json() - assert msg["id"] == 7 assert not msg["success"] assert msg["error"] == {"code": "already_exists", "message": "File already exists"} @@ -271,9 +259,8 @@ async def test_save_existing_file_override( client = await hass_ws_client(hass) with patch("pathlib.Path.write_text") as write_mock: - await client.send_json( + await client.send_json_auto_id( { - "id": 7, "type": "blueprint/save", "path": "test_event_service", "yaml": 'blueprint: {name: "name", domain: "automation"}', @@ -285,7 +272,6 @@ async def test_save_existing_file_override( msg = await client.receive_json() - assert msg["id"] == 7 assert msg["success"] assert msg["result"] == {"overrides_existing": True} assert yaml.safe_load(write_mock.mock_calls[0][1][0]) == { @@ -305,9 +291,8 @@ async def test_save_file_error( """Test saving blueprints with OS error.""" with patch("pathlib.Path.write_text", side_effect=OSError): client = await hass_ws_client(hass) - await client.send_json( + await client.send_json_auto_id( { - "id": 8, "type": "blueprint/save", "path": "test_save", "yaml": "raw_data", @@ -318,7 +303,6 @@ async def test_save_file_error( msg = await client.receive_json() - assert msg["id"] == 8 assert not msg["success"] @@ -329,9 +313,8 @@ async def test_save_invalid_blueprint( """Test saving invalid blueprints.""" client = await hass_ws_client(hass) - await client.send_json( + await client.send_json_auto_id( { - "id": 8, "type": "blueprint/save", "path": "test_wrong", "yaml": "wrong_blueprint", @@ -342,7 +325,6 @@ async def test_save_invalid_blueprint( msg = await client.receive_json() - assert msg["id"] == 8 assert not msg["success"] assert msg["error"] == { "code": "invalid_format", @@ -358,9 +340,8 @@ async def test_delete_blueprint( with patch("pathlib.Path.unlink", return_value=Mock()) as unlink_mock: client = await hass_ws_client(hass) - await client.send_json( + await client.send_json_auto_id( { - "id": 9, "type": "blueprint/delete", "path": "test_delete", "domain": "automation", @@ -370,7 +351,6 @@ async def test_delete_blueprint( msg = await client.receive_json() assert unlink_mock.mock_calls - assert msg["id"] == 9 assert msg["success"] @@ -381,9 +361,8 @@ async def test_delete_non_exist_file_blueprint( """Test deleting non existing blueprints.""" client = await hass_ws_client(hass) - await client.send_json( + await client.send_json_auto_id( { - "id": 9, "type": "blueprint/delete", "path": "none_existing", "domain": "automation", @@ -392,7 +371,6 @@ async def test_delete_non_exist_file_blueprint( msg = await client.receive_json() - assert msg["id"] == 9 assert not msg["success"] @@ -421,9 +399,8 @@ async def test_delete_blueprint_in_use_by_automation( with patch("pathlib.Path.unlink", return_value=Mock()) as unlink_mock: client = await hass_ws_client(hass) - await client.send_json( + await client.send_json_auto_id( { - "id": 9, "type": "blueprint/delete", "path": "test_event_service.yaml", "domain": "automation", @@ -433,7 +410,6 @@ async def test_delete_blueprint_in_use_by_automation( msg = await client.receive_json() assert not unlink_mock.mock_calls - assert msg["id"] == 9 assert not msg["success"] assert msg["error"] == { "code": "home_assistant_error", From e6967298ecc8f52ac8d66f16ea430858ce141d90 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Wed, 19 Jun 2024 23:14:43 +0200 Subject: [PATCH 0843/1445] Remove circuit integration (#119921) --- .coveragerc | 1 - CODEOWNERS | 1 - homeassistant/components/circuit/__init__.py | 50 ------------------- .../components/circuit/manifest.json | 9 ---- homeassistant/components/circuit/notify.py | 46 ----------------- homeassistant/components/circuit/strings.json | 8 --- homeassistant/generated/integrations.json | 6 --- requirements_all.txt | 3 -- 8 files changed, 124 deletions(-) delete mode 100644 homeassistant/components/circuit/__init__.py delete mode 100644 homeassistant/components/circuit/manifest.json delete mode 100644 homeassistant/components/circuit/notify.py delete mode 100644 homeassistant/components/circuit/strings.json diff --git a/.coveragerc b/.coveragerc index eeffb341fd8..74fde968370 100644 --- a/.coveragerc +++ b/.coveragerc @@ -182,7 +182,6 @@ omit = homeassistant/components/canary/camera.py homeassistant/components/cert_expiry/helper.py homeassistant/components/channels/* - homeassistant/components/circuit/* homeassistant/components/cisco_ios/device_tracker.py homeassistant/components/cisco_mobility_express/device_tracker.py homeassistant/components/cisco_webex_teams/notify.py diff --git a/CODEOWNERS b/CODEOWNERS index 71ac96c05e7..aaed793dd41 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -239,7 +239,6 @@ build.json @home-assistant/supervisor /tests/components/ccm15/ @ocalvo /homeassistant/components/cert_expiry/ @jjlawren /tests/components/cert_expiry/ @jjlawren -/homeassistant/components/circuit/ @braam /homeassistant/components/cisco_ios/ @fbradyirl /homeassistant/components/cisco_mobility_express/ @fbradyirl /homeassistant/components/cisco_webex_teams/ @fbradyirl diff --git a/homeassistant/components/circuit/__init__.py b/homeassistant/components/circuit/__init__.py deleted file mode 100644 index 7e7d0eda76e..00000000000 --- a/homeassistant/components/circuit/__init__.py +++ /dev/null @@ -1,50 +0,0 @@ -"""The Unify Circuit component.""" - -import voluptuous as vol - -from homeassistant.const import CONF_NAME, CONF_URL, Platform -from homeassistant.core import HomeAssistant -from homeassistant.helpers import config_validation as cv, discovery -import homeassistant.helpers.issue_registry as ir -from homeassistant.helpers.typing import ConfigType - -DOMAIN = "circuit" -CONF_WEBHOOK = "webhook" - -WEBHOOK_SCHEMA = vol.Schema( - {vol.Optional(CONF_NAME): cv.string, vol.Required(CONF_URL): cv.string} -) - -CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - {vol.Required(CONF_WEBHOOK): vol.All(cv.ensure_list, [WEBHOOK_SCHEMA])} - ) - }, - extra=vol.ALLOW_EXTRA, -) - - -async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Set up the Unify Circuit component.""" - ir.async_create_issue( - hass, - DOMAIN, - "service_removal", - breaks_in_ha_version="2024.7.0", - is_fixable=False, - is_persistent=True, - severity=ir.IssueSeverity.WARNING, - translation_key="service_removal", - translation_placeholders={"integration": "Unify Circuit", "domain": DOMAIN}, - ) - webhooks = config[DOMAIN][CONF_WEBHOOK] - - for webhook_conf in webhooks: - hass.async_create_task( - discovery.async_load_platform( - hass, Platform.NOTIFY, DOMAIN, webhook_conf, config - ) - ) - - return True diff --git a/homeassistant/components/circuit/manifest.json b/homeassistant/components/circuit/manifest.json deleted file mode 100644 index d982aef31ec..00000000000 --- a/homeassistant/components/circuit/manifest.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "domain": "circuit", - "name": "Unify Circuit", - "codeowners": ["@braam"], - "documentation": "https://www.home-assistant.io/integrations/circuit", - "iot_class": "cloud_push", - "loggers": ["circuit_webhook"], - "requirements": ["circuit-webhook==1.0.1"] -} diff --git a/homeassistant/components/circuit/notify.py b/homeassistant/components/circuit/notify.py deleted file mode 100644 index 23884ebd9be..00000000000 --- a/homeassistant/components/circuit/notify.py +++ /dev/null @@ -1,46 +0,0 @@ -"""Unify Circuit platform for notify component.""" - -from __future__ import annotations - -import logging - -from circuit_webhook import Circuit - -from homeassistant.components.notify import BaseNotificationService -from homeassistant.const import CONF_URL -from homeassistant.core import HomeAssistant -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType - -_LOGGER = logging.getLogger(__name__) - - -def get_service( - hass: HomeAssistant, - config: ConfigType, - discovery_info: DiscoveryInfoType | None = None, -) -> CircuitNotificationService | None: - """Get the Unify Circuit notification service.""" - if discovery_info is None: - return None - - return CircuitNotificationService(discovery_info) - - -class CircuitNotificationService(BaseNotificationService): - """Implement the notification service for Unify Circuit.""" - - def __init__(self, config): - """Initialize the service.""" - self.webhook_url = config[CONF_URL] - - def send_message(self, message=None, **kwargs): - """Send a message to the webhook.""" - - webhook_url = self.webhook_url - - if webhook_url and message: - try: - circuit_message = Circuit(url=webhook_url) - circuit_message.post(text=message) - except RuntimeError as err: - _LOGGER.error("Could not send notification. Error: %s", err) diff --git a/homeassistant/components/circuit/strings.json b/homeassistant/components/circuit/strings.json deleted file mode 100644 index b9cb852d5b9..00000000000 --- a/homeassistant/components/circuit/strings.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "issues": { - "service_removal": { - "title": "The {integration} integration is being removed", - "description": "The {integration} integration will be removed, as the service is no longer maintained.\n\n\n\nRemove the `{domain}` configuration from your configuration.yaml file and restart Home Assistant to fix this issue." - } - } -} diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 425702562d0..43b1c1b45f7 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -885,12 +885,6 @@ "config_flow": false, "iot_class": "local_polling" }, - "circuit": { - "name": "Unify Circuit", - "integration_type": "hub", - "config_flow": false, - "iot_class": "cloud_push" - }, "cisco": { "name": "Cisco", "integrations": { diff --git a/requirements_all.txt b/requirements_all.txt index 25da9893ddc..f82ac823eb8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -648,9 +648,6 @@ cached_ipaddress==0.3.0 # homeassistant.components.caldav caldav==1.3.9 -# homeassistant.components.circuit -circuit-webhook==1.0.1 - # homeassistant.components.cisco_mobility_express ciscomobilityexpress==0.3.9 From ebbb63cd080dd9510a43925f611cdde3f672c809 Mon Sep 17 00:00:00 2001 From: Pete Sage <76050312+PeteRager@users.noreply.github.com> Date: Wed, 19 Jun 2024 17:38:49 -0400 Subject: [PATCH 0844/1445] Fix Sonos album images with special characters not displaying in media browser UI (#118249) * initial commit * initial commit * simplify tests * rename symbol * original_uri -> original_url * change symbol name --------- Co-authored-by: Dave T <17680170+davet2001@users.noreply.github.com> --- .../components/sonos/media_browser.py | 28 ++++++++++++++++++- .../sonos/fixtures/music_library_albums.json | 7 +++++ .../sonos/snapshots/test_media_browser.ambr | 12 +++++++- 3 files changed, 45 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sonos/media_browser.py b/homeassistant/components/sonos/media_browser.py index 3416896e879..995d6cea08c 100644 --- a/homeassistant/components/sonos/media_browser.py +++ b/homeassistant/components/sonos/media_browser.py @@ -46,6 +46,32 @@ _LOGGER = logging.getLogger(__name__) type GetBrowseImageUrlType = Callable[[str, str, str | None], str] +def fix_image_url(url: str) -> str: + """Update the image url to fully encode characters to allow image display in media_browser UI. + + Images whose file path contains characters such as ',()+ are not loaded without escaping them. + """ + + # Before parsing encode the plus sign; otherwise it'll be interpreted as a space. + original_url: str = urllib.parse.unquote(url).replace("+", "%2B") + parsed_url = urllib.parse.urlparse(original_url) + query_params = urllib.parse.parse_qsl(parsed_url.query) + new_url = urllib.parse.urlunsplit( + ( + parsed_url.scheme, + parsed_url.netloc, + parsed_url.path, + urllib.parse.urlencode( + query_params, quote_via=urllib.parse.quote, safe="/:" + ), + "", + ) + ) + if original_url != new_url: + _LOGGER.debug("fix_sonos_image_url original: %s new: %s", original_url, new_url) + return new_url + + def get_thumbnail_url_full( media: SonosMedia, is_internal: bool, @@ -63,7 +89,7 @@ def get_thumbnail_url_full( media_content_id, media_content_type, ) - return urllib.parse.unquote(getattr(item, "album_art_uri", "")) + return fix_image_url(getattr(item, "album_art_uri", "")) return urllib.parse.unquote( get_browse_image_url( diff --git a/tests/components/sonos/fixtures/music_library_albums.json b/tests/components/sonos/fixtures/music_library_albums.json index 4941abe8ba7..24ee386e338 100644 --- a/tests/components/sonos/fixtures/music_library_albums.json +++ b/tests/components/sonos/fixtures/music_library_albums.json @@ -19,5 +19,12 @@ "parent_id": "A:ALBUM", "item_class": "object.container.album.musicAlbum", "album_art_uri": "http://192.168.42.2:1400/getaa?u=x-file-cifs%3a%2f%2f192.168.42.100%2fmusic%2fSantana%2fA%2520Between%2520Good%2520And%2520Evil%2f02%2520A%2520Persuasion.m4a&v=53" + }, + { + "title": "Special Characters,'()+", + "item_id": "A:ALBUM/Special%20Characters,'()+", + "parent_id": "A:ALBUM", + "item_class": "object.container.album.musicAlbum", + "album_art_uri": "http://192.168.42.2:1400/getaa?u=x-file-cifs%3a%2f%2f192.168.42.100%2fmusic%2fSpecial%2fA%2520Special%2520Characters,()+%2f01%2520A%2520TheFirstTrack.m4a&v=53" } ] diff --git a/tests/components/sonos/snapshots/test_media_browser.ambr b/tests/components/sonos/snapshots/test_media_browser.ambr index b4388b148e5..ae8e813ae5d 100644 --- a/tests/components/sonos/snapshots/test_media_browser.ambr +++ b/tests/components/sonos/snapshots/test_media_browser.ambr @@ -82,7 +82,7 @@ 'media_class': 'album', 'media_content_id': "A:ALBUM/A%20Hard%20Day's%20Night", 'media_content_type': 'album', - 'thumbnail': "http://192.168.42.2:1400/getaa?u=x-file-cifs://192.168.42.100/music/The%20Beatles/A%20Hard%20Day's%20Night/01%20A%20Hard%20Day's%20Night%201.m4a&v=53", + 'thumbnail': 'http://192.168.42.2:1400/getaa?u=x-file-cifs://192.168.42.100/music/The%20Beatles/A%20Hard%20Day%27s%20Night/01%20A%20Hard%20Day%27s%20Night%201.m4a&v=53', 'title': "A Hard Day's Night", }), dict({ @@ -105,6 +105,16 @@ 'thumbnail': 'http://192.168.42.2:1400/getaa?u=x-file-cifs://192.168.42.100/music/Santana/A%20Between%20Good%20And%20Evil/02%20A%20Persuasion.m4a&v=53', 'title': 'Between Good And Evil', }), + dict({ + 'can_expand': True, + 'can_play': True, + 'children_media_class': None, + 'media_class': 'album', + 'media_content_id': "A:ALBUM/Special%20Characters,'()+", + 'media_content_type': 'album', + 'thumbnail': 'http://192.168.42.2:1400/getaa?u=x-file-cifs://192.168.42.100/music/Special/A%20Special%20Characters%2C%28%29%2B/01%20A%20TheFirstTrack.m4a&v=53', + 'title': "Special Characters,'()+", + }), ]) # --- # name: test_browse_media_root From 0053c92d2b30ea22b6d0cf702c52b5e3388237ef Mon Sep 17 00:00:00 2001 From: Leo Shen Date: Wed, 19 Jun 2024 14:56:20 -0700 Subject: [PATCH 0845/1445] Update PySwitchbot to 0.48.0 (#119998) --- homeassistant/components/switchbot/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/switchbot/manifest.json b/homeassistant/components/switchbot/manifest.json index c408a369761..dc858a688cb 100644 --- a/homeassistant/components/switchbot/manifest.json +++ b/homeassistant/components/switchbot/manifest.json @@ -39,5 +39,5 @@ "documentation": "https://www.home-assistant.io/integrations/switchbot", "iot_class": "local_push", "loggers": ["switchbot"], - "requirements": ["PySwitchbot==0.47.2"] + "requirements": ["PySwitchbot==0.48.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index f82ac823eb8..5ed3a261833 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -90,7 +90,7 @@ PyQRCode==1.2.1 PyRMVtransport==0.3.3 # homeassistant.components.switchbot -PySwitchbot==0.47.2 +PySwitchbot==0.48.0 # homeassistant.components.switchmate PySwitchmate==0.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b48af464f60..d584b69bbf1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -78,7 +78,7 @@ PyQRCode==1.2.1 PyRMVtransport==0.3.3 # homeassistant.components.switchbot -PySwitchbot==0.47.2 +PySwitchbot==0.48.0 # homeassistant.components.syncthru PySyncThru==0.7.10 From 60ba80a28de2b10d88412821429c0694be7a4fb1 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Wed, 19 Jun 2024 23:57:18 +0200 Subject: [PATCH 0846/1445] Only (re)subscribe MQTT topics using the debouncer (#119995) * Only (re)subscribe using the debouncer * Update test * Fix test * Reset mock --- homeassistant/components/mqtt/client.py | 13 ++-- tests/components/mqtt/test_init.py | 85 ++++++++++++++++--------- 2 files changed, 58 insertions(+), 40 deletions(-) diff --git a/homeassistant/components/mqtt/client.py b/homeassistant/components/mqtt/client.py index 562fa230bca..63a90019c20 100644 --- a/homeassistant/components/mqtt/client.py +++ b/homeassistant/components/mqtt/client.py @@ -1035,7 +1035,8 @@ class MQTT: self, birth_message: PublishMessage ) -> None: """Resubscribe to all topics and publish birth message.""" - await self._async_perform_subscriptions() + self._async_queue_resubscribe() + self._subscribe_debouncer.async_schedule() await self._ha_started.wait() # Wait for Home Assistant to start await self._discovery_cooldown() # Wait for MQTT discovery to cool down # Update subscribe cooldown period to a shorter time @@ -1091,7 +1092,6 @@ class MQTT: result_code, ) - self._async_queue_resubscribe() birth: dict[str, Any] if birth := self.conf.get(CONF_BIRTH_MESSAGE, DEFAULT_BIRTH): birth_message = PublishMessage(**birth) @@ -1102,13 +1102,8 @@ class MQTT: ) else: # Update subscribe cooldown period to a shorter time - self.config_entry.async_create_background_task( - self.hass, - self._async_perform_subscriptions(), - name="mqtt re-subscribe", - ) - self._subscribe_debouncer.set_timeout(SUBSCRIBE_COOLDOWN) - _LOGGER.info("MQTT client initialized") + self._async_queue_resubscribe() + self._subscribe_debouncer.async_schedule() self._async_connection_result(True) diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index 18310750558..cd710ba610e 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -1583,6 +1583,8 @@ async def test_replaying_payload_same_topic( mqtt_client_mock.on_disconnect(None, None, 0) mqtt_client_mock.on_connect(None, None, None, 0) await hass.async_block_till_done() + async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) + await hass.async_block_till_done() mqtt_client_mock.subscribe.assert_called() # Simulate a (retained) message played back after reconnecting async_fire_mqtt_message(hass, "test/state", "online", qos=0, retain=True) @@ -1797,6 +1799,7 @@ async def test_not_calling_subscribe_when_unsubscribed_within_cooldown( assert not mqtt_client_mock.subscribe.called +@patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.0) @patch("homeassistant.components.mqtt.client.INITIAL_SUBSCRIBE_COOLDOWN", 0.0) @patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.0) async def test_unsubscribe_race( @@ -1808,6 +1811,9 @@ async def test_unsubscribe_race( mqtt_mock = await mqtt_mock_entry() # Fake that the client is connected mqtt_mock().connected = True + await hass.async_block_till_done() + async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) # cooldown + await hass.async_block_till_done() calls_a: list[ReceiveMessage] = [] calls_b: list[ReceiveMessage] = [] @@ -1868,6 +1874,10 @@ async def test_restore_subscriptions_on_reconnect( mqtt_mock = await mqtt_mock_entry() # Fake that the client is connected mqtt_mock().connected = True + await hass.async_block_till_done() + async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) # cooldown + await hass.async_block_till_done() + mqtt_client_mock.subscribe.reset_mock() await mqtt.async_subscribe(hass, "test/state", record_calls) async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) # cooldown @@ -1876,8 +1886,10 @@ async def test_restore_subscriptions_on_reconnect( mqtt_client_mock.on_disconnect(None, None, 0) mqtt_client_mock.on_connect(None, None, None, 0) + await hass.async_block_till_done() async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) # cooldown await hass.async_block_till_done() + async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) # cooldown await hass.async_block_till_done() assert mqtt_client_mock.subscribe.call_count == 2 @@ -2586,6 +2598,9 @@ async def test_default_birth_message( "mqtt_config_entry_data", [{mqtt.CONF_BROKER: "mock-broker", mqtt.CONF_BIRTH_MESSAGE: {}}], ) +@patch("homeassistant.components.mqtt.client.INITIAL_SUBSCRIBE_COOLDOWN", 0.0) +@patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.0) +@patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.0) async def test_no_birth_message( hass: HomeAssistant, mqtt_client_mock: MqttMockPahoClient, @@ -2593,23 +2608,26 @@ async def test_no_birth_message( ) -> None: """Test disabling birth message.""" await mqtt_mock_entry() - with patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.1): - mqtt_client_mock.on_connect(None, None, 0, 0) - await hass.async_block_till_done() - await asyncio.sleep(0.2) - mqtt_client_mock.publish.assert_not_called() + await hass.async_block_till_done() + async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) + await hass.async_block_till_done() + mqtt_client_mock.reset_mock() + + # Assert no birth message was sent + mqtt_client_mock.on_connect(None, None, 0, 0) + await hass.async_block_till_done() + async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) + await hass.async_block_till_done() + mqtt_client_mock.publish.assert_not_called() async def callback(msg: ReceiveMessage) -> None: """Handle birth message.""" - # Assert the subscribe debouncer subscribes after - # about SUBSCRIBE_COOLDOWN (0.1) sec - # but sooner than INITIAL_SUBSCRIBE_COOLDOWN (1.0) - mqtt_client_mock.reset_mock() await mqtt.async_subscribe(hass, "homeassistant/some-topic", callback) await hass.async_block_till_done() - await asyncio.sleep(0.2) + async_fire_time_changed(hass, utcnow() + timedelta(seconds=5)) + await hass.async_block_till_done() mqtt_client_mock.subscribe.assert_called() @@ -2690,15 +2708,16 @@ async def test_delayed_birth_message( } ], ) +@patch("homeassistant.components.mqtt.client.INITIAL_SUBSCRIBE_COOLDOWN", 0.0) +@patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.0) +@patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.0) async def test_subscription_done_when_birth_message_is_sent( hass: HomeAssistant, mqtt_client_mock: MqttMockPahoClient, + record_calls: MessageCallbackType, mqtt_config_entry_data, - mqtt_mock_entry: MqttMockHAClientGenerator, ) -> None: """Test sending birth message until initial subscription has been completed.""" - mqtt_mock = await mqtt_mock_entry() - hass.set_state(CoreState.starting) birth = asyncio.Event() @@ -2707,32 +2726,27 @@ async def test_subscription_done_when_birth_message_is_sent( entry = MockConfigEntry(domain=mqtt.DOMAIN, data=mqtt_config_entry_data) entry.add_to_hass(hass) assert await hass.config_entries.async_setup(entry.entry_id) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) + await hass.async_block_till_done() + mqtt_client_mock.on_disconnect(None, None, 0, 0) + await hass.async_block_till_done() + async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) await hass.async_block_till_done() - - mqtt_component_mock = MagicMock( - return_value=hass.data["mqtt"].client, - wraps=hass.data["mqtt"].client, - ) - mqtt_component_mock._mqttc = mqtt_client_mock - - hass.data["mqtt"].client = mqtt_component_mock - mqtt_mock = hass.data["mqtt"].client - mqtt_mock.reset_mock() @callback def wait_birth(msg: ReceiveMessage) -> None: """Handle birth message.""" birth.set() + await mqtt.async_subscribe(hass, "topic/test", record_calls) + await mqtt.async_subscribe(hass, "homeassistant/status", wait_birth) + await hass.async_block_till_done() mqtt_client_mock.reset_mock() - with patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.0): - await mqtt.async_subscribe(hass, "homeassistant/status", wait_birth) - mqtt_client_mock.on_connect(None, None, 0, 0) - await hass.async_block_till_done() - hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) - await mqtt.async_subscribe(hass, "topic/test", record_calls) - # We wait until we receive a birth message - await asyncio.wait_for(birth.wait(), 1) + mqtt_client_mock.on_connect(None, None, 0, 0) + # We wait until we receive a birth message + await asyncio.wait_for(birth.wait(), 1) # Assert we already have subscribed at the client # for new config payloads at the time we the birth message is received @@ -2810,6 +2824,9 @@ async def test_no_will_message( } ], ) +@patch("homeassistant.components.mqtt.client.INITIAL_SUBSCRIBE_COOLDOWN", 0.0) +@patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.0) +@patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.0) async def test_mqtt_subscribes_topics_on_connect( hass: HomeAssistant, mqtt_client_mock: MqttMockPahoClient, @@ -2818,6 +2835,10 @@ async def test_mqtt_subscribes_topics_on_connect( ) -> None: """Test subscription to topic on connect.""" await mqtt_mock_entry() + await hass.async_block_till_done() + async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) + await hass.async_block_till_done() + mqtt_client_mock.reset_mock() await mqtt.async_subscribe(hass, "topic/test", record_calls) await mqtt.async_subscribe(hass, "home/sensor", record_calls, 2) @@ -2826,6 +2847,8 @@ async def test_mqtt_subscribes_topics_on_connect( mqtt_client_mock.on_connect(None, None, 0, 0) + await hass.async_block_till_done() + async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) await hass.async_block_till_done() assert mqtt_client_mock.disconnect.call_count == 0 From 1eb8b5a27c9f1c23c4ce8496c58c2b80394f2512 Mon Sep 17 00:00:00 2001 From: "Mr. Bubbles" Date: Thu, 20 Jun 2024 10:03:29 +0200 Subject: [PATCH 0847/1445] Add config flow to One-Time Password (OTP) integration (#118493) --- homeassistant/components/otp/__init__.py | 23 +++- homeassistant/components/otp/config_flow.py | 74 +++++++++++++ homeassistant/components/otp/const.py | 4 + homeassistant/components/otp/manifest.json | 1 + homeassistant/components/otp/sensor.py | 34 +++++- homeassistant/components/otp/strings.json | 19 ++++ homeassistant/generated/config_flows.py | 1 + homeassistant/generated/integrations.json | 2 +- tests/components/otp/__init__.py | 1 + tests/components/otp/conftest.py | 62 +++++++++++ .../components/otp/snapshots/test_sensor.ambr | 15 +++ tests/components/otp/test_config_flow.py | 100 ++++++++++++++++++ tests/components/otp/test_init.py | 23 ++++ tests/components/otp/test_sensor.py | 41 +++++++ 14 files changed, 393 insertions(+), 7 deletions(-) create mode 100644 homeassistant/components/otp/config_flow.py create mode 100644 homeassistant/components/otp/const.py create mode 100644 homeassistant/components/otp/strings.json create mode 100644 tests/components/otp/__init__.py create mode 100644 tests/components/otp/conftest.py create mode 100644 tests/components/otp/snapshots/test_sensor.ambr create mode 100644 tests/components/otp/test_config_flow.py create mode 100644 tests/components/otp/test_init.py create mode 100644 tests/components/otp/test_sensor.py diff --git a/homeassistant/components/otp/__init__.py b/homeassistant/components/otp/__init__.py index bf80d41a92d..5b18301874a 100644 --- a/homeassistant/components/otp/__init__.py +++ b/homeassistant/components/otp/__init__.py @@ -1 +1,22 @@ -"""The otp component.""" +"""The One-Time Password (OTP) integration.""" + +from __future__ import annotations + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant + +PLATFORMS: list[Platform] = [Platform.SENSOR] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up One-Time Password (OTP) from a config entry.""" + + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/otp/config_flow.py b/homeassistant/components/otp/config_flow.py new file mode 100644 index 00000000000..7777b9b733a --- /dev/null +++ b/homeassistant/components/otp/config_flow.py @@ -0,0 +1,74 @@ +"""Config flow for One-Time Password (OTP) integration.""" + +from __future__ import annotations + +import binascii +import logging +from typing import Any + +import pyotp +import voluptuous as vol + +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult +from homeassistant.const import CONF_NAME, CONF_TOKEN + +from .const import DEFAULT_NAME, DOMAIN + +_LOGGER = logging.getLogger(__name__) + +STEP_USER_DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_TOKEN): str, + vol.Required(CONF_NAME, default=DEFAULT_NAME): str, + } +) + + +class TOTPConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a config flow for One-Time Password (OTP).""" + + VERSION = 1 + user_input: dict[str, Any] + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle the initial step.""" + errors: dict[str, str] = {} + if user_input is not None: + try: + await self.hass.async_add_executor_job( + pyotp.TOTP(user_input[CONF_TOKEN]).now + ) + except binascii.Error: + errors["base"] = "invalid_code" + except Exception: + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + await self.async_set_unique_id(user_input[CONF_TOKEN]) + self._abort_if_unique_id_configured() + + return self.async_create_entry( + title=user_input[CONF_NAME], + data=user_input, + ) + + return self.async_show_form( + step_id="user", + data_schema=self.add_suggested_values_to_schema( + data_schema=STEP_USER_DATA_SCHEMA, suggested_values=user_input + ), + errors=errors, + ) + + async def async_step_import(self, import_info: dict[str, Any]) -> ConfigFlowResult: + """Import config from yaml.""" + + await self.async_set_unique_id(import_info[CONF_TOKEN]) + self._abort_if_unique_id_configured() + + return self.async_create_entry( + title=import_info.get(CONF_NAME, DEFAULT_NAME), + data=import_info, + ) diff --git a/homeassistant/components/otp/const.py b/homeassistant/components/otp/const.py new file mode 100644 index 00000000000..180e0a4c5a2 --- /dev/null +++ b/homeassistant/components/otp/const.py @@ -0,0 +1,4 @@ +"""Constants for the One-Time Password (OTP) integration.""" + +DOMAIN = "otp" +DEFAULT_NAME = "OTP Sensor" diff --git a/homeassistant/components/otp/manifest.json b/homeassistant/components/otp/manifest.json index 758824f8772..f62f89cff40 100644 --- a/homeassistant/components/otp/manifest.json +++ b/homeassistant/components/otp/manifest.json @@ -2,6 +2,7 @@ "domain": "otp", "name": "One-Time Password (OTP)", "codeowners": [], + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/otp", "iot_class": "local_polling", "loggers": ["pyotp"], diff --git a/homeassistant/components/otp/sensor.py b/homeassistant/components/otp/sensor.py index 3a62677dfc2..e612b03f66c 100644 --- a/homeassistant/components/otp/sensor.py +++ b/homeassistant/components/otp/sensor.py @@ -8,13 +8,15 @@ import pyotp import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_NAME, CONF_TOKEN -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType -DEFAULT_NAME = "OTP Sensor" +from .const import DEFAULT_NAME, DOMAIN TIME_STEP = 30 # Default time step assumed by Google Authenticator @@ -34,10 +36,32 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the OTP sensor.""" - name = config[CONF_NAME] - token = config[CONF_TOKEN] + async_create_issue( + hass, + HOMEASSISTANT_DOMAIN, + f"deprecated_yaml_{DOMAIN}", + is_fixable=False, + breaks_in_ha_version="2025.1.0", + severity=IssueSeverity.WARNING, + translation_key="deprecated_yaml", + translation_placeholders={ + "domain": DOMAIN, + "integration_title": "One-Time Password (OTP)", + }, + ) + await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=config + ) - async_add_entities([TOTPSensor(name, token)], True) + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up the OTP sensor.""" + + async_add_entities( + [TOTPSensor(entry.data[CONF_NAME], entry.data[CONF_TOKEN])], True + ) # Only TOTP supported at the moment, HOTP might be added later diff --git a/homeassistant/components/otp/strings.json b/homeassistant/components/otp/strings.json new file mode 100644 index 00000000000..fc6031d0433 --- /dev/null +++ b/homeassistant/components/otp/strings.json @@ -0,0 +1,19 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "[%key:common::config_flow::data::name%]", + "token": "Authenticator token (OTP)" + } + } + }, + "error": { + "unknown": "[%key:common::config_flow::error::unknown%]", + "invalid_token": "Invalid token" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 745bad093d2..5d0718092e5 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -397,6 +397,7 @@ FLOWS = { "oralb", "osoenergy", "otbr", + "otp", "ourgroceries", "overkiz", "ovo_energy", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 43b1c1b45f7..4133de4d4a3 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -4409,7 +4409,7 @@ "otp": { "name": "One-Time Password (OTP)", "integration_type": "hub", - "config_flow": false, + "config_flow": true, "iot_class": "local_polling" }, "ourgroceries": { diff --git a/tests/components/otp/__init__.py b/tests/components/otp/__init__.py new file mode 100644 index 00000000000..91a7412323b --- /dev/null +++ b/tests/components/otp/__init__.py @@ -0,0 +1 @@ +"""Test the One-Time Password (OTP).""" diff --git a/tests/components/otp/conftest.py b/tests/components/otp/conftest.py new file mode 100644 index 00000000000..a4e139637c4 --- /dev/null +++ b/tests/components/otp/conftest.py @@ -0,0 +1,62 @@ +"""Common fixtures for the One-Time Password (OTP) tests.""" + +from collections.abc import Generator +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest + +from homeassistant.components.otp.const import DOMAIN +from homeassistant.components.sensor.const import DOMAIN as SENSOR_DOMAIN +from homeassistant.const import CONF_NAME, CONF_PLATFORM, CONF_TOKEN +from homeassistant.helpers.typing import ConfigType + +from tests.common import MockConfigEntry + + +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock, None, None]: + """Override async_setup_entry.""" + with patch( + "homeassistant.components.otp.async_setup_entry", return_value=True + ) as mock_setup_entry: + yield mock_setup_entry + + +@pytest.fixture +def mock_pyotp() -> Generator[MagicMock, None, None]: + """Mock a pyotp.""" + with ( + patch( + "homeassistant.components.otp.config_flow.pyotp", + ) as mock_client, + patch("homeassistant.components.otp.sensor.pyotp", new=mock_client), + ): + mock_totp = MagicMock() + mock_totp.now.return_value = 123456 + mock_client.TOTP.return_value = mock_totp + yield mock_client + + +@pytest.fixture(name="otp_config_entry") +def mock_otp_config_entry() -> MockConfigEntry: + """Mock otp configuration entry.""" + return MockConfigEntry( + domain=DOMAIN, + data={ + CONF_NAME: "OTP Sensor", + CONF_TOKEN: "2FX5FBSYRE6VEC2FSHBQCRKO2GNDVZ52", + }, + unique_id="2FX5FBSYRE6VEC2FSHBQCRKO2GNDVZ52", + ) + + +@pytest.fixture(name="otp_yaml_config") +def mock_otp_yaml_config() -> ConfigType: + """Mock otp configuration entry.""" + return { + SENSOR_DOMAIN: { + CONF_PLATFORM: "otp", + CONF_TOKEN: "2FX5FBSYRE6VEC2FSHBQCRKO2GNDVZ52", + CONF_NAME: "OTP Sensor", + } + } diff --git a/tests/components/otp/snapshots/test_sensor.ambr b/tests/components/otp/snapshots/test_sensor.ambr new file mode 100644 index 00000000000..fbd8741b8b5 --- /dev/null +++ b/tests/components/otp/snapshots/test_sensor.ambr @@ -0,0 +1,15 @@ +# serializer version: 1 +# name: test_setup + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'OTP Sensor', + 'icon': 'mdi:update', + }), + 'context': , + 'entity_id': 'sensor.otp_sensor', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '123456', + }) +# --- diff --git a/tests/components/otp/test_config_flow.py b/tests/components/otp/test_config_flow.py new file mode 100644 index 00000000000..b0bd3e915bd --- /dev/null +++ b/tests/components/otp/test_config_flow.py @@ -0,0 +1,100 @@ +"""Test the One-Time Password (OTP) config flow.""" + +import binascii +from unittest.mock import AsyncMock, MagicMock + +import pytest + +from homeassistant.components.otp.const import DOMAIN +from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER +from homeassistant.const import CONF_NAME, CONF_TOKEN +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +TEST_DATA = { + CONF_NAME: "OTP Sensor", + CONF_TOKEN: "TOKEN_A", +} + + +@pytest.mark.usefixtures("mock_pyotp") +async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: + """Test we get the form.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + TEST_DATA, + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "OTP Sensor" + assert result["data"] == TEST_DATA + assert len(mock_setup_entry.mock_calls) == 1 + + +@pytest.mark.parametrize( + ("exception", "error"), + [ + (binascii.Error, "invalid_code"), + (IndexError, "unknown"), + ], +) +async def test_errors_and_recover( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_pyotp: MagicMock, + exception: Exception, + error: str, +) -> None: + """Test errors and recover.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {} + + mock_pyotp.TOTP().now.side_effect = exception + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=TEST_DATA, + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"base": error} + + mock_pyotp.TOTP().now.side_effect = None + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=TEST_DATA, + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "OTP Sensor" + assert result["data"] == TEST_DATA + assert len(mock_setup_entry.mock_calls) == 1 + + +@pytest.mark.usefixtures("mock_pyotp", "mock_setup_entry") +async def test_flow_import(hass: HomeAssistant) -> None: + """Test that we can import a YAML config.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=TEST_DATA, + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "OTP Sensor" + assert result["data"] == TEST_DATA diff --git a/tests/components/otp/test_init.py b/tests/components/otp/test_init.py new file mode 100644 index 00000000000..0ce8f44523e --- /dev/null +++ b/tests/components/otp/test_init.py @@ -0,0 +1,23 @@ +"""Test the One-Time Password (OTP) init.""" + +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +async def test_entry_setup_unload( + hass: HomeAssistant, otp_config_entry: MockConfigEntry +) -> None: + """Test integration setup and unload.""" + + otp_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(otp_config_entry.entry_id) + await hass.async_block_till_done() + + assert otp_config_entry.state is ConfigEntryState.LOADED + + await hass.config_entries.async_unload(otp_config_entry.entry_id) + await hass.async_block_till_done() + + assert otp_config_entry.state is ConfigEntryState.NOT_LOADED diff --git a/tests/components/otp/test_sensor.py b/tests/components/otp/test_sensor.py new file mode 100644 index 00000000000..b9901c4a914 --- /dev/null +++ b/tests/components/otp/test_sensor.py @@ -0,0 +1,41 @@ +"""Tests for the One-Time Password (OTP) Sensors.""" + +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.otp.const import DOMAIN +from homeassistant.components.sensor.const import DOMAIN as SENSOR_DOMAIN +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant +from homeassistant.helpers import issue_registry as ir +from homeassistant.helpers.typing import ConfigType +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry + + +@pytest.mark.usefixtures("mock_pyotp") +async def test_setup( + hass: HomeAssistant, + otp_config_entry: MockConfigEntry, + snapshot: SnapshotAssertion, +) -> None: + """Test setup of ista EcoTrend sensor platform.""" + + otp_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(otp_config_entry.entry_id) + await hass.async_block_till_done() + + assert hass.states.get("sensor.otp_sensor") == snapshot + + +async def test_deprecated_yaml_issue( + hass: HomeAssistant, issue_registry: ir.IssueRegistry, otp_yaml_config: ConfigType +) -> None: + """Test an issue is created when attempting setup from yaml config.""" + + assert await async_setup_component(hass, SENSOR_DOMAIN, otp_yaml_config) + await hass.async_block_till_done() + + assert issue_registry.async_get_issue( + domain=HOMEASSISTANT_DOMAIN, issue_id=f"deprecated_yaml_{DOMAIN}" + ) From 3224224bf8b70f1fd2b2d0cd92031a1e9dd1e441 Mon Sep 17 00:00:00 2001 From: ashionky <35916938+ashionky@users.noreply.github.com> Date: Thu, 20 Jun 2024 16:29:37 +0800 Subject: [PATCH 0848/1445] Add Sensor for Refoss Integration (#116965) Co-authored-by: Robert Resch --- .coveragerc | 1 + homeassistant/components/refoss/__init__.py | 1 + homeassistant/components/refoss/const.py | 11 ++ homeassistant/components/refoss/entity.py | 5 - homeassistant/components/refoss/sensor.py | 174 +++++++++++++++++++ homeassistant/components/refoss/strings.json | 22 +++ homeassistant/components/refoss/switch.py | 10 ++ 7 files changed, 219 insertions(+), 5 deletions(-) create mode 100644 homeassistant/components/refoss/sensor.py diff --git a/.coveragerc b/.coveragerc index 74fde968370..4d0f78a81f5 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1110,6 +1110,7 @@ omit = homeassistant/components/refoss/bridge.py homeassistant/components/refoss/coordinator.py homeassistant/components/refoss/entity.py + homeassistant/components/refoss/sensor.py homeassistant/components/refoss/switch.py homeassistant/components/refoss/util.py homeassistant/components/rejseplanen/sensor.py diff --git a/homeassistant/components/refoss/__init__.py b/homeassistant/components/refoss/__init__.py index 666a17847c9..0f0c852b043 100644 --- a/homeassistant/components/refoss/__init__.py +++ b/homeassistant/components/refoss/__init__.py @@ -15,6 +15,7 @@ from .const import COORDINATORS, DATA_DISCOVERY_SERVICE, DISCOVERY_SCAN_INTERVAL from .util import refoss_discovery_server PLATFORMS: Final = [ + Platform.SENSOR, Platform.SWITCH, ] diff --git a/homeassistant/components/refoss/const.py b/homeassistant/components/refoss/const.py index 86e40fce43c..0542afe8afb 100644 --- a/homeassistant/components/refoss/const.py +++ b/homeassistant/components/refoss/const.py @@ -19,3 +19,14 @@ DOMAIN = "refoss" COORDINATOR = "coordinator" MAX_ERRORS = 2 + +CHANNEL_DISPLAY_NAME: dict[str, dict[int, str]] = { + "em06": { + 1: "A1", + 2: "B1", + 3: "C1", + 4: "A2", + 5: "B2", + 6: "C2", + } +} diff --git a/homeassistant/components/refoss/entity.py b/homeassistant/components/refoss/entity.py index 3032c32ed51..502101608ec 100644 --- a/homeassistant/components/refoss/entity.py +++ b/homeassistant/components/refoss/entity.py @@ -18,11 +18,6 @@ class RefossEntity(CoordinatorEntity[RefossDataUpdateCoordinator]): mac = coordinator.device.mac self.channel_id = channel - if channel == 0: - self._attr_name = None - else: - self._attr_name = str(channel) - self._attr_unique_id = f"{mac}_{channel}" self._attr_device_info = DeviceInfo( connections={(CONNECTION_NETWORK_MAC, mac)}, diff --git a/homeassistant/components/refoss/sensor.py b/homeassistant/components/refoss/sensor.py new file mode 100644 index 00000000000..018c438ba3c --- /dev/null +++ b/homeassistant/components/refoss/sensor.py @@ -0,0 +1,174 @@ +"""Support for refoss sensors.""" + +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass + +from refoss_ha.controller.electricity import ElectricityXMix + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + UnitOfElectricCurrent, + UnitOfElectricPotential, + UnitOfEnergy, + UnitOfPower, +) +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import StateType + +from .bridge import RefossDataUpdateCoordinator +from .const import ( + CHANNEL_DISPLAY_NAME, + COORDINATORS, + DISPATCH_DEVICE_DISCOVERED, + DOMAIN, +) +from .entity import RefossEntity + + +@dataclass(frozen=True) +class RefossSensorEntityDescription(SensorEntityDescription): + """Describes Refoss sensor entity.""" + + subkey: str | None = None + fn: Callable[[float], float] | None = None + + +SENSORS: dict[str, tuple[RefossSensorEntityDescription, ...]] = { + "em06": ( + RefossSensorEntityDescription( + key="power", + translation_key="power", + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfPower.WATT, + suggested_display_precision=2, + subkey="power", + fn=lambda x: x / 1000.0, + ), + RefossSensorEntityDescription( + key="voltage", + translation_key="voltage", + device_class=SensorDeviceClass.VOLTAGE, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfElectricPotential.MILLIVOLT, + suggested_display_precision=2, + suggested_unit_of_measurement=UnitOfElectricPotential.VOLT, + subkey="voltage", + ), + RefossSensorEntityDescription( + key="current", + translation_key="current", + device_class=SensorDeviceClass.CURRENT, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfElectricCurrent.MILLIAMPERE, + suggested_display_precision=2, + suggested_unit_of_measurement=UnitOfElectricCurrent.AMPERE, + subkey="current", + ), + RefossSensorEntityDescription( + key="factor", + translation_key="power_factor", + device_class=SensorDeviceClass.POWER_FACTOR, + state_class=SensorStateClass.MEASUREMENT, + suggested_display_precision=2, + subkey="factor", + ), + RefossSensorEntityDescription( + key="energy", + translation_key="this_month_energy", + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL, + native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, + suggested_display_precision=2, + subkey="mConsume", + fn=lambda x: x if x > 0 else 0, + ), + RefossSensorEntityDescription( + key="energy_returned", + translation_key="this_month_energy_returned", + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL, + native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, + suggested_display_precision=2, + subkey="mConsume", + fn=lambda x: abs(x) if x < 0 else 0, + ), + ), +} + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Refoss device from a config entry.""" + + @callback + def init_device(coordinator): + """Register the device.""" + device = coordinator.device + + if not isinstance(device, ElectricityXMix): + return + descriptions = SENSORS.get(device.device_type) + new_entities = [] + for channel in device.channels: + for description in descriptions: + entity = RefossSensor( + coordinator=coordinator, + channel=channel, + description=description, + ) + new_entities.append(entity) + + async_add_entities(new_entities) + + for coordinator in hass.data[DOMAIN][COORDINATORS]: + init_device(coordinator) + + config_entry.async_on_unload( + async_dispatcher_connect(hass, DISPATCH_DEVICE_DISCOVERED, init_device) + ) + + +class RefossSensor(RefossEntity, SensorEntity): + """Refoss Sensor Device.""" + + entity_description: RefossSensorEntityDescription + + def __init__( + self, + coordinator: RefossDataUpdateCoordinator, + channel: int, + description: RefossSensorEntityDescription, + ) -> None: + """Init Refoss sensor.""" + super().__init__(coordinator, channel) + self.entity_description = description + self._attr_unique_id = f"{super().unique_id}{description.key}" + device_type = coordinator.device.device_type + channel_name = CHANNEL_DISPLAY_NAME[device_type][channel] + self._attr_translation_placeholders = {"channel_name": channel_name} + + @property + def native_value(self) -> StateType: + """Return the native value.""" + value = self.coordinator.device.get_value( + self.channel_id, self.entity_description.subkey + ) + if value is None: + return None + if self.entity_description.fn is not None: + return self.entity_description.fn(value) + return value diff --git a/homeassistant/components/refoss/strings.json b/homeassistant/components/refoss/strings.json index ad8f0f41ae7..67b4e4a8335 100644 --- a/homeassistant/components/refoss/strings.json +++ b/homeassistant/components/refoss/strings.json @@ -9,5 +9,27 @@ "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]", "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]" } + }, + "entity": { + "sensor": { + "power": { + "name": "{channel_name} power" + }, + "voltage": { + "name": "{channel_name} voltage" + }, + "current": { + "name": "{channel_name} current" + }, + "power_factor": { + "name": "{channel_name} power factor" + }, + "this_month_energy": { + "name": "{channel_name} this month energy" + }, + "this_month_energy_returned": { + "name": "{channel_name} this month energy returned" + } + } } } diff --git a/homeassistant/components/refoss/switch.py b/homeassistant/components/refoss/switch.py index c51f166059e..0f5aba0cfc4 100644 --- a/homeassistant/components/refoss/switch.py +++ b/homeassistant/components/refoss/switch.py @@ -12,6 +12,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback +from .bridge import RefossDataUpdateCoordinator from .const import COORDINATORS, DISPATCH_DEVICE_DISCOVERED, DOMAIN from .entity import RefossEntity @@ -48,6 +49,15 @@ async def async_setup_entry( class RefossSwitch(RefossEntity, SwitchEntity): """Refoss Switch Device.""" + def __init__( + self, + coordinator: RefossDataUpdateCoordinator, + channel: int, + ) -> None: + """Init Refoss switch.""" + super().__init__(coordinator, channel) + self._attr_name = str(channel) + @property def is_on(self) -> bool | None: """Return true if switch is on.""" From 4d7a857555a2596dc427ea2b5b25977997ee8cfa Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 20 Jun 2024 11:08:01 +0200 Subject: [PATCH 0849/1445] Use runtimedata in nanoleaf (#120009) * Use runtimedata in nanoleaf * Update homeassistant/components/nanoleaf/light.py Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> --------- Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> --- homeassistant/components/nanoleaf/__init__.py | 20 +++++-------------- homeassistant/components/nanoleaf/button.py | 12 +++++------ .../components/nanoleaf/diagnostics.py | 9 +++------ homeassistant/components/nanoleaf/light.py | 12 +++++------ 4 files changed, 20 insertions(+), 33 deletions(-) diff --git a/homeassistant/components/nanoleaf/__init__.py b/homeassistant/components/nanoleaf/__init__.py index 5abddfa6778..f607c7277ec 100644 --- a/homeassistant/components/nanoleaf/__init__.py +++ b/homeassistant/components/nanoleaf/__init__.py @@ -4,7 +4,6 @@ from __future__ import annotations import asyncio from contextlib import suppress -from dataclasses import dataclass import logging from aionanoleaf import EffectsEvent, Nanoleaf, StateEvent, TouchEvent @@ -29,15 +28,10 @@ _LOGGER = logging.getLogger(__name__) PLATFORMS = [Platform.BUTTON, Platform.LIGHT] -@dataclass -class NanoleafEntryData: - """Class for sharing data within the Nanoleaf integration.""" - - device: Nanoleaf - coordinator: NanoleafCoordinator +type NanoleafConfigEntry = ConfigEntry[NanoleafCoordinator] -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: NanoleafConfigEntry) -> bool: """Set up Nanoleaf from a config entry.""" nanoleaf = Nanoleaf( async_get_clientsession(hass), entry.data[CONF_HOST], entry.data[CONF_TOKEN] @@ -87,17 +81,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry.async_on_unload(_cancel_listener) - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = NanoleafEntryData( - nanoleaf, coordinator - ) + entry.runtime_data = coordinator await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: NanoleafConfigEntry) -> bool: """Unload a config entry.""" - if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): - hass.data[DOMAIN].pop(entry.entry_id) - return unload_ok + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/nanoleaf/button.py b/homeassistant/components/nanoleaf/button.py index dd0cc221fc2..34d0f4f5076 100644 --- a/homeassistant/components/nanoleaf/button.py +++ b/homeassistant/components/nanoleaf/button.py @@ -1,22 +1,22 @@ """Support for Nanoleaf buttons.""" from homeassistant.components.button import ButtonDeviceClass, ButtonEntity -from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import NanoleafCoordinator, NanoleafEntryData -from .const import DOMAIN +from . import NanoleafConfigEntry +from .coordinator import NanoleafCoordinator from .entity import NanoleafEntity async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, + entry: NanoleafConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Nanoleaf button.""" - entry_data: NanoleafEntryData = hass.data[DOMAIN][entry.entry_id] - async_add_entities([NanoleafIdentifyButton(entry_data.coordinator)]) + async_add_entities([NanoleafIdentifyButton(entry.runtime_data)]) class NanoleafIdentifyButton(NanoleafEntity, ButtonEntity): diff --git a/homeassistant/components/nanoleaf/diagnostics.py b/homeassistant/components/nanoleaf/diagnostics.py index 57f385e5039..6f8691905ef 100644 --- a/homeassistant/components/nanoleaf/diagnostics.py +++ b/homeassistant/components/nanoleaf/diagnostics.py @@ -4,22 +4,19 @@ from __future__ import annotations from typing import Any -from aionanoleaf import Nanoleaf - from homeassistant.components.diagnostics import async_redact_data -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_TOKEN from homeassistant.core import HomeAssistant -from .const import DOMAIN +from . import NanoleafConfigEntry async def async_get_config_entry_diagnostics( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: NanoleafConfigEntry, ) -> dict[str, Any]: """Return diagnostics for a config entry.""" - device: Nanoleaf = hass.data[DOMAIN][config_entry.entry_id].device + device = config_entry.runtime_data.nanoleaf return { "info": async_redact_data(config_entry.as_dict(), (CONF_TOKEN, "title")), diff --git a/homeassistant/components/nanoleaf/light.py b/homeassistant/components/nanoleaf/light.py index a02cb30754b..19d817b9999 100644 --- a/homeassistant/components/nanoleaf/light.py +++ b/homeassistant/components/nanoleaf/light.py @@ -15,7 +15,6 @@ from homeassistant.components.light import ( LightEntity, LightEntityFeature, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util.color import ( @@ -23,8 +22,8 @@ from homeassistant.util.color import ( color_temperature_mired_to_kelvin as mired_to_kelvin, ) -from . import NanoleafCoordinator, NanoleafEntryData -from .const import DOMAIN +from . import NanoleafConfigEntry +from .coordinator import NanoleafCoordinator from .entity import NanoleafEntity RESERVED_EFFECTS = ("*Solid*", "*Static*", "*Dynamic*") @@ -32,11 +31,12 @@ DEFAULT_NAME = "Nanoleaf" async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, + entry: NanoleafConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Nanoleaf light.""" - entry_data: NanoleafEntryData = hass.data[DOMAIN][entry.entry_id] - async_add_entities([NanoleafLight(entry_data.coordinator)]) + async_add_entities([NanoleafLight(entry.runtime_data)]) class NanoleafLight(NanoleafEntity, LightEntity): From e89b9b009311e9b007f9a56cb0b9ac399f245473 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 20 Jun 2024 11:49:03 +0200 Subject: [PATCH 0850/1445] Small clean up for Refoss sensor platform (#120015) --- homeassistant/components/refoss/sensor.py | 39 +++++++++++------------ 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/refoss/sensor.py b/homeassistant/components/refoss/sensor.py index 018c438ba3c..3857b401d0d 100644 --- a/homeassistant/components/refoss/sensor.py +++ b/homeassistant/components/refoss/sensor.py @@ -35,12 +35,12 @@ from .const import ( from .entity import RefossEntity -@dataclass(frozen=True) +@dataclass(frozen=True, kw_only=True) class RefossSensorEntityDescription(SensorEntityDescription): """Describes Refoss sensor entity.""" - subkey: str | None = None - fn: Callable[[float], float] | None = None + subkey: str + fn: Callable[[float], float] = lambda x: x SENSORS: dict[str, tuple[RefossSensorEntityDescription, ...]] = { @@ -50,10 +50,10 @@ SENSORS: dict[str, tuple[RefossSensorEntityDescription, ...]] = { translation_key="power", device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=UnitOfPower.WATT, + native_unit_of_measurement=UnitOfPower.KILO_WATT, + suggested_unit_of_measurement=UnitOfPower.WATT, suggested_display_precision=2, subkey="power", - fn=lambda x: x / 1000.0, ), RefossSensorEntityDescription( key="voltage", @@ -115,24 +115,25 @@ async def async_setup_entry( """Set up the Refoss device from a config entry.""" @callback - def init_device(coordinator): + def init_device(coordinator: RefossDataUpdateCoordinator) -> None: """Register the device.""" device = coordinator.device if not isinstance(device, ElectricityXMix): return - descriptions = SENSORS.get(device.device_type) - new_entities = [] - for channel in device.channels: - for description in descriptions: - entity = RefossSensor( - coordinator=coordinator, - channel=channel, - description=description, - ) - new_entities.append(entity) + descriptions: tuple[RefossSensorEntityDescription, ...] = SENSORS.get( + device.device_type, () + ) - async_add_entities(new_entities) + async_add_entities( + RefossSensor( + coordinator=coordinator, + channel=channel, + description=description, + ) + for channel in device.channels + for description in descriptions + ) for coordinator in hass.data[DOMAIN][COORDINATORS]: init_device(coordinator) @@ -169,6 +170,4 @@ class RefossSensor(RefossEntity, SensorEntity): ) if value is None: return None - if self.entity_description.fn is not None: - return self.entity_description.fn(value) - return value + return self.entity_description.fn(value) From 1235338f1b237d120c000efcb83f79e3a124e513 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 20 Jun 2024 12:39:26 +0200 Subject: [PATCH 0851/1445] Fix hass-component-root-import warnings in otp tests (#120019) --- tests/components/otp/conftest.py | 2 +- tests/components/otp/test_sensor.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/components/otp/conftest.py b/tests/components/otp/conftest.py index a4e139637c4..7c9b2eb545e 100644 --- a/tests/components/otp/conftest.py +++ b/tests/components/otp/conftest.py @@ -6,7 +6,7 @@ from unittest.mock import AsyncMock, MagicMock, patch import pytest from homeassistant.components.otp.const import DOMAIN -from homeassistant.components.sensor.const import DOMAIN as SENSOR_DOMAIN +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.const import CONF_NAME, CONF_PLATFORM, CONF_TOKEN from homeassistant.helpers.typing import ConfigType diff --git a/tests/components/otp/test_sensor.py b/tests/components/otp/test_sensor.py index b9901c4a914..e75ce6707d4 100644 --- a/tests/components/otp/test_sensor.py +++ b/tests/components/otp/test_sensor.py @@ -4,7 +4,7 @@ import pytest from syrupy.assertion import SnapshotAssertion from homeassistant.components.otp.const import DOMAIN -from homeassistant.components.sensor.const import DOMAIN as SENSOR_DOMAIN +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant from homeassistant.helpers import issue_registry as ir from homeassistant.helpers.typing import ConfigType From 99cae16b753f453637701c712ceca4b80fea7b90 Mon Sep 17 00:00:00 2001 From: mikosoft83 <63317931+mikosoft83@users.noreply.github.com> Date: Thu, 20 Jun 2024 12:59:30 +0200 Subject: [PATCH 0852/1445] Change meteoalarm scan interval (#119194) --- homeassistant/components/meteoalarm/binary_sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/meteoalarm/binary_sensor.py b/homeassistant/components/meteoalarm/binary_sensor.py index 8b38ac6dbb3..8fb0ae5cdc8 100644 --- a/homeassistant/components/meteoalarm/binary_sensor.py +++ b/homeassistant/components/meteoalarm/binary_sensor.py @@ -30,7 +30,7 @@ CONF_PROVINCE = "province" DEFAULT_NAME = "meteoalarm" -SCAN_INTERVAL = timedelta(minutes=30) +SCAN_INTERVAL = timedelta(minutes=5) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { From 7c5fcec062e1d2cfaa794a169fafa629a70bbc9e Mon Sep 17 00:00:00 2001 From: BestPig Date: Thu, 20 Jun 2024 13:06:30 +0200 Subject: [PATCH 0853/1445] Fix songpal crash for soundbars without sound modes (#119999) Getting soundField on soundbar that doesn't support it crash raise an exception, so it make the whole components unavailable. As there is no simple way to know if soundField is supported, I just get all sound settings, and then pick soundField one if present. If not present, then return None to make it continue, it will just have to effect to display no sound mode and not able to select one (Exactly what we want). --- .../components/songpal/media_player.py | 7 +++- tests/components/songpal/__init__.py | 13 ++++++- tests/components/songpal/test_media_player.py | 37 +++++++++++++++++++ 3 files changed, 54 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/songpal/media_player.py b/homeassistant/components/songpal/media_player.py index c6d6524cefb..9f828591a08 100644 --- a/homeassistant/components/songpal/media_player.py +++ b/homeassistant/components/songpal/media_player.py @@ -140,7 +140,12 @@ class SongpalEntity(MediaPlayerEntity): async def _get_sound_modes_info(self): """Get available sound modes and the active one.""" - settings = await self._dev.get_sound_settings("soundField") + for settings in await self._dev.get_sound_settings(): + if settings.target == "soundField": + break + else: + return None, {} + if isinstance(settings, Setting): settings = [settings] diff --git a/tests/components/songpal/__init__.py b/tests/components/songpal/__init__.py index ab585c5a6d5..15bf0c530d3 100644 --- a/tests/components/songpal/__init__.py +++ b/tests/components/songpal/__init__.py @@ -23,7 +23,9 @@ CONF_DATA = { } -def _create_mocked_device(throw_exception=False, wired_mac=MAC, wireless_mac=None): +def _create_mocked_device( + throw_exception=False, wired_mac=MAC, wireless_mac=None, no_soundfield=False +): mocked_device = MagicMock() type(mocked_device).get_supported_methods = AsyncMock( @@ -101,7 +103,14 @@ def _create_mocked_device(throw_exception=False, wired_mac=MAC, wireless_mac=Non soundField = MagicMock() soundField.currentValue = "sound_mode2" soundField.candidate = [sound_mode1, sound_mode2, sound_mode3] - type(mocked_device).get_sound_settings = AsyncMock(return_value=[soundField]) + + settings = MagicMock() + settings.target = "soundField" + settings.__iter__.return_value = [soundField] + + type(mocked_device).get_sound_settings = AsyncMock( + return_value=[] if no_soundfield else [settings] + ) type(mocked_device).set_power = AsyncMock() type(mocked_device).set_sound_settings = AsyncMock() diff --git a/tests/components/songpal/test_media_player.py b/tests/components/songpal/test_media_player.py index 2393a5a9086..8f56170b839 100644 --- a/tests/components/songpal/test_media_player.py +++ b/tests/components/songpal/test_media_player.py @@ -159,6 +159,43 @@ async def test_state( assert entity.unique_id == MAC +async def test_state_nosoundmode( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, +) -> None: + """Test state of the entity with no soundField in sound settings.""" + mocked_device = _create_mocked_device(no_soundfield=True) + entry = MockConfigEntry(domain=songpal.DOMAIN, data=CONF_DATA) + entry.add_to_hass(hass) + + with _patch_media_player_device(mocked_device): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get(ENTITY_ID) + assert state.name == FRIENDLY_NAME + assert state.state == STATE_ON + attributes = state.as_dict()["attributes"] + assert attributes["volume_level"] == 0.5 + assert attributes["is_volume_muted"] is False + assert attributes["source_list"] == ["title1", "title2"] + assert attributes["source"] == "title2" + assert "sound_mode_list" not in attributes + assert "sound_mode" not in attributes + assert attributes["supported_features"] == SUPPORT_SONGPAL + + device = device_registry.async_get_device(identifiers={(songpal.DOMAIN, MAC)}) + assert device.connections == {(dr.CONNECTION_NETWORK_MAC, MAC)} + assert device.manufacturer == "Sony Corporation" + assert device.name == FRIENDLY_NAME + assert device.sw_version == SW_VERSION + assert device.model == MODEL + + entity = entity_registry.async_get(ENTITY_ID) + assert entity.unique_id == MAC + + async def test_state_wireless( hass: HomeAssistant, device_registry: dr.DeviceRegistry, From 325a49e8ff0702c4c2c28ef6e3a41c6f3d8db9fb Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 20 Jun 2024 16:02:49 +0200 Subject: [PATCH 0854/1445] Enable pylint on tests (#119279) * Enable pylint on tests * Remove jobs==1 --- .github/workflows/ci.yaml | 45 +++++++++++++++++++++++++++++++++++++++ .pre-commit-config.yaml | 2 +- pyproject.toml | 3 --- 3 files changed, 46 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 1dc1c5af289..6da5a570d22 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -620,6 +620,51 @@ jobs: python --version pylint --ignore-missing-annotations=y homeassistant/components/${{ needs.info.outputs.integrations_glob }} + pylint-tests: + name: Check pylint on tests + runs-on: ubuntu-22.04 + timeout-minutes: 20 + if: | + github.event.inputs.mypy-only != 'true' + || github.event.inputs.pylint-only == 'true' + needs: + - info + - base + steps: + - name: Check out code from GitHub + uses: actions/checkout@v4.1.6 + - name: Set up Python ${{ env.DEFAULT_PYTHON }} + id: python + uses: actions/setup-python@v5.1.0 + with: + python-version: ${{ env.DEFAULT_PYTHON }} + check-latest: true + - name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment + id: cache-venv + uses: actions/cache/restore@v4.0.2 + with: + path: venv + fail-on-cache-miss: true + key: >- + ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ + needs.info.outputs.python_cache_key }} + - name: Register pylint problem matcher + run: | + echo "::add-matcher::.github/workflows/matchers/pylint.json" + - name: Run pylint (fully) + if: needs.info.outputs.test_full_suite == 'true' + run: | + . venv/bin/activate + python --version + pylint --ignore-missing-annotations=y tests + - name: Run pylint (partially) + if: needs.info.outputs.test_full_suite == 'false' + shell: bash + run: | + . venv/bin/activate + python --version + pylint --ignore-missing-annotations=y tests/components/${{ needs.info.outputs.integrations_glob }} + mypy: name: Check mypy runs-on: ubuntu-22.04 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b5f6377ce7b..023f917d89c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -69,7 +69,7 @@ repos: entry: script/run-in-env.sh pylint -j 0 --ignore-missing-annotations=y language: script types_or: [python, pyi] - files: ^homeassistant/.+\.(py|pyi)$ + files: ^(homeassistant|tests)/.+\.(py|pyi)$ - id: gen_requirements_all name: gen_requirements_all entry: script/run-in-env.sh python3 -m script.gen_requirements_all diff --git a/pyproject.toml b/pyproject.toml index 971f321d3bb..56a10cfcd71 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -93,9 +93,6 @@ include = ["homeassistant*"] [tool.pylint.MAIN] py-version = "3.12" -ignore = [ - "tests", -] # Use a conservative default here; 2 should speed up most setups and not hurt # any too bad. Override on command line as appropriate. jobs = 2 From 87e405396bf4837a8562fc1848d79eeb7c84e048 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Thu, 20 Jun 2024 20:12:40 +0200 Subject: [PATCH 0855/1445] Bump aiounifi to v79 (#120033) --- homeassistant/components/unifi/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/unifi/manifest.json b/homeassistant/components/unifi/manifest.json index 504c2f505a7..f4bfaec2d42 100644 --- a/homeassistant/components/unifi/manifest.json +++ b/homeassistant/components/unifi/manifest.json @@ -8,7 +8,7 @@ "iot_class": "local_push", "loggers": ["aiounifi"], "quality_scale": "platinum", - "requirements": ["aiounifi==77"], + "requirements": ["aiounifi==79"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/requirements_all.txt b/requirements_all.txt index 5ed3a261833..b8ddc95d590 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -386,7 +386,7 @@ aiotankerkoenig==0.4.1 aiotractive==0.5.6 # homeassistant.components.unifi -aiounifi==77 +aiounifi==79 # homeassistant.components.vlc_telnet aiovlc==0.3.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d584b69bbf1..d807b767acd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -359,7 +359,7 @@ aiotankerkoenig==0.4.1 aiotractive==0.5.6 # homeassistant.components.unifi -aiounifi==77 +aiounifi==79 # homeassistant.components.vlc_telnet aiovlc==0.3.2 From ee85c0e44c81c7361687179d909acc3f22584f58 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 20 Jun 2024 18:34:57 -0500 Subject: [PATCH 0856/1445] Bump uiprotect to 1.19.2 (#120048) --- homeassistant/components/unifiprotect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index 9cb62e666dc..ee12111b146 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -40,7 +40,7 @@ "integration_type": "hub", "iot_class": "local_push", "loggers": ["uiprotect", "unifi_discovery"], - "requirements": ["uiprotect==1.19.0", "unifi-discovery==1.1.8"], + "requirements": ["uiprotect==1.19.2", "unifi-discovery==1.1.8"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/requirements_all.txt b/requirements_all.txt index b8ddc95d590..c095234ee84 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2788,7 +2788,7 @@ twitchAPI==4.0.0 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==1.19.0 +uiprotect==1.19.2 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d807b767acd..4b97590127c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2168,7 +2168,7 @@ twitchAPI==4.0.0 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==1.19.0 +uiprotect==1.19.2 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 From fb25902de9b4cef707699f48ed3c6d06d204c893 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 20 Jun 2024 18:35:35 -0500 Subject: [PATCH 0857/1445] Cleanup unifiprotect subscriptions logic (#120049) --- homeassistant/components/unifiprotect/data.py | 18 +++++++++++------- .../components/unifiprotect/entity.py | 4 +--- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/unifiprotect/data.py b/homeassistant/components/unifiprotect/data.py index 75c850702f3..e3e4cbc7f50 100644 --- a/homeassistant/components/unifiprotect/data.py +++ b/homeassistant/components/unifiprotect/data.py @@ -2,6 +2,7 @@ from __future__ import annotations +from collections import defaultdict from collections.abc import Callable, Iterable from datetime import datetime, timedelta from functools import partial @@ -78,7 +79,9 @@ class ProtectData: self._entry = entry self._hass = hass self._update_interval = update_interval - self._subscriptions: dict[str, list[Callable[[ProtectDeviceType], None]]] = {} + self._subscriptions: defaultdict[ + str, set[Callable[[ProtectDeviceType], None]] + ] = defaultdict(set) self._pending_camera_ids: set[str] = set() self._unsub_interval: CALLBACK_TYPE | None = None self._unsub_websocket: CALLBACK_TYPE | None = None @@ -302,7 +305,7 @@ class ProtectData: ) @callback - def async_subscribe_device_id( + def async_subscribe( self, mac: str, update_callback: Callable[[ProtectDeviceType], None] ) -> CALLBACK_TYPE: """Add an callback subscriber.""" @@ -310,11 +313,11 @@ class ProtectData: self._unsub_interval = async_track_time_interval( self._hass, self._async_poll, self._update_interval ) - self._subscriptions.setdefault(mac, []).append(update_callback) - return partial(self.async_unsubscribe_device_id, mac, update_callback) + self._subscriptions[mac].add(update_callback) + return partial(self._async_unsubscribe, mac, update_callback) @callback - def async_unsubscribe_device_id( + def _async_unsubscribe( self, mac: str, update_callback: Callable[[ProtectDeviceType], None] ) -> None: """Remove a callback subscriber.""" @@ -328,9 +331,10 @@ class ProtectData: @callback def _async_signal_device_update(self, device: ProtectDeviceType) -> None: """Call the callbacks for a device_id.""" - if not (subscriptions := self._subscriptions.get(device.mac)): + mac = device.mac + if not (subscriptions := self._subscriptions.get(mac)): return - _LOGGER.debug("Updating device: %s (%s)", device.name, device.mac) + _LOGGER.debug("Updating device: %s (%s)", device.name, mac) for update_callback in subscriptions: update_callback(device) diff --git a/homeassistant/components/unifiprotect/entity.py b/homeassistant/components/unifiprotect/entity.py index a4179e023b3..adf0d334e0a 100644 --- a/homeassistant/components/unifiprotect/entity.py +++ b/homeassistant/components/unifiprotect/entity.py @@ -261,9 +261,7 @@ class BaseProtectEntity(Entity): """When entity is added to hass.""" await super().async_added_to_hass() self.async_on_remove( - self.data.async_subscribe_device_id( - self.device.mac, self._async_updated_event - ) + self.data.async_subscribe(self.device.mac, self._async_updated_event) ) From ecadaf314dcf9db64b08c244a2a581336b2c7c18 Mon Sep 17 00:00:00 2001 From: Leo Shen Date: Thu, 20 Jun 2024 17:26:43 -0700 Subject: [PATCH 0858/1445] Add support for Switchbot Lock Pro (#119326) Co-authored-by: J. Nick Koston --- homeassistant/components/switchbot/__init__.py | 7 +++++++ homeassistant/components/switchbot/config_flow.py | 9 +++++---- homeassistant/components/switchbot/const.py | 3 +++ 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/switchbot/__init__.py b/homeassistant/components/switchbot/__init__.py index 82860db6745..7bf02ed37b6 100644 --- a/homeassistant/components/switchbot/__init__.py +++ b/homeassistant/components/switchbot/__init__.py @@ -50,6 +50,11 @@ PLATFORMS_BY_TYPE = { Platform.LOCK, Platform.SENSOR, ], + SupportedModels.LOCK_PRO.value: [ + Platform.BINARY_SENSOR, + Platform.LOCK, + Platform.SENSOR, + ], SupportedModels.BLIND_TILT.value: [ Platform.COVER, Platform.BINARY_SENSOR, @@ -66,6 +71,7 @@ CLASS_BY_DEVICE = { SupportedModels.LIGHT_STRIP.value: switchbot.SwitchbotLightStrip, SupportedModels.HUMIDIFIER.value: switchbot.SwitchbotHumidifier, SupportedModels.LOCK.value: switchbot.SwitchbotLock, + SupportedModels.LOCK_PRO.value: switchbot.SwitchbotLock, SupportedModels.BLIND_TILT.value: switchbot.SwitchbotBlindTilt, } @@ -118,6 +124,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: key_id=entry.data.get(CONF_KEY_ID), encryption_key=entry.data.get(CONF_ENCRYPTION_KEY), retry_count=entry.options[CONF_RETRY_COUNT], + model=switchbot_model, ) except ValueError as error: raise ConfigEntryNotReady( diff --git a/homeassistant/components/switchbot/config_flow.py b/homeassistant/components/switchbot/config_flow.py index bb69da52239..a1c947fd611 100644 --- a/homeassistant/components/switchbot/config_flow.py +++ b/homeassistant/components/switchbot/config_flow.py @@ -11,7 +11,6 @@ from switchbot import ( SwitchbotApiError, SwitchbotAuthenticationError, SwitchbotLock, - SwitchbotModel, parse_advertisement_data, ) import voluptuous as vol @@ -44,6 +43,7 @@ from .const import ( DEFAULT_RETRY_COUNT, DOMAIN, NON_CONNECTABLE_SUPPORTED_MODEL_TYPES, + SUPPORTED_LOCK_MODELS, SUPPORTED_MODEL_TYPES, ) @@ -109,7 +109,7 @@ class SwitchbotConfigFlow(ConfigFlow, domain=DOMAIN): "name": data["modelFriendlyName"], "address": short_address(discovery_info.address), } - if model_name == SwitchbotModel.LOCK: + if model_name in SUPPORTED_LOCK_MODELS: return await self.async_step_lock_choose_method() if self._discovered_adv.data["isEncrypted"]: return await self.async_step_password() @@ -240,6 +240,7 @@ class SwitchbotConfigFlow(ConfigFlow, domain=DOMAIN): self._discovered_adv.device, user_input[CONF_KEY_ID], user_input[CONF_ENCRYPTION_KEY], + model=self._discovered_adv.data["modelName"], ): errors = { "base": "encryption_key_invalid", @@ -305,7 +306,7 @@ class SwitchbotConfigFlow(ConfigFlow, domain=DOMAIN): if user_input is not None: device_adv = self._discovered_advs[user_input[CONF_ADDRESS]] await self._async_set_device(device_adv) - if device_adv.data.get("modelName") == SwitchbotModel.LOCK: + if device_adv.data.get("modelName") in SUPPORTED_LOCK_MODELS: return await self.async_step_lock_choose_method() if device_adv.data["isEncrypted"]: return await self.async_step_password() @@ -317,7 +318,7 @@ class SwitchbotConfigFlow(ConfigFlow, domain=DOMAIN): # or simply confirm it device_adv = list(self._discovered_advs.values())[0] await self._async_set_device(device_adv) - if device_adv.data.get("modelName") == SwitchbotModel.LOCK: + if device_adv.data.get("modelName") in SUPPORTED_LOCK_MODELS: return await self.async_step_lock_choose_method() if device_adv.data["isEncrypted"]: return await self.async_step_password() diff --git a/homeassistant/components/switchbot/const.py b/homeassistant/components/switchbot/const.py index 7e7a1d185f2..0a1ac01e530 100644 --- a/homeassistant/components/switchbot/const.py +++ b/homeassistant/components/switchbot/const.py @@ -26,6 +26,7 @@ class SupportedModels(StrEnum): MOTION = "motion" HUMIDIFIER = "humidifier" LOCK = "lock" + LOCK_PRO = "lock_pro" BLIND_TILT = "blind_tilt" HUB2 = "hub2" @@ -39,6 +40,7 @@ CONNECTABLE_SUPPORTED_MODEL_TYPES = { SwitchbotModel.CEILING_LIGHT: SupportedModels.CEILING_LIGHT, SwitchbotModel.HUMIDIFIER: SupportedModels.HUMIDIFIER, SwitchbotModel.LOCK: SupportedModels.LOCK, + SwitchbotModel.LOCK_PRO: SupportedModels.LOCK_PRO, SwitchbotModel.BLIND_TILT: SupportedModels.BLIND_TILT, SwitchbotModel.HUB2: SupportedModels.HUB2, } @@ -54,6 +56,7 @@ SUPPORTED_MODEL_TYPES = ( CONNECTABLE_SUPPORTED_MODEL_TYPES | NON_CONNECTABLE_SUPPORTED_MODEL_TYPES ) +SUPPORTED_LOCK_MODELS = {SwitchbotModel.LOCK, SwitchbotModel.LOCK_PRO} HASS_SENSOR_TYPE_TO_SWITCHBOT_MODEL = { str(v): k for k, v in SUPPORTED_MODEL_TYPES.items() From 68462b014cda3e5b307e31aa13c09e76748836d4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 20 Jun 2024 22:03:07 -0500 Subject: [PATCH 0859/1445] Fix unifiprotect smart detection when end is set (#120027) --- .../components/unifiprotect/binary_sensor.py | 98 +++++++--- .../components/unifiprotect/entity.py | 34 ++-- .../components/unifiprotect/models.py | 18 +- .../components/unifiprotect/sensor.py | 38 ++-- tests/components/unifiprotect/conftest.py | 1 + .../unifiprotect/test_binary_sensor.py | 167 +++++++++++++++++- tests/components/unifiprotect/test_sensor.py | 66 ++++++- tests/components/unifiprotect/test_switch.py | 16 +- 8 files changed, 372 insertions(+), 66 deletions(-) diff --git a/homeassistant/components/unifiprotect/binary_sensor.py b/homeassistant/components/unifiprotect/binary_sensor.py index e57826fd2f3..9bda0e8f310 100644 --- a/homeassistant/components/unifiprotect/binary_sensor.py +++ b/homeassistant/components/unifiprotect/binary_sensor.py @@ -14,6 +14,7 @@ from uiprotect.data import ( ProtectAdoptableDeviceModel, ProtectModelWithId, Sensor, + SmartDetectObjectType, ) from uiprotect.data.nvr import UOSDisk @@ -436,11 +437,13 @@ EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = ( ufp_enabled="is_motion_detection_on", ufp_event_obj="last_motion_event", ), +) + +SMART_EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = ( ProtectBinaryEventEntityDescription( key="smart_obj_any", name="Object detected", icon="mdi:eye", - ufp_value="is_smart_currently_detected", ufp_required_field="feature_flags.has_smart_detect", ufp_event_obj="last_smart_detect_event", ), @@ -448,7 +451,7 @@ EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = ( key="smart_obj_person", name="Person detected", icon="mdi:walk", - ufp_value="is_person_currently_detected", + ufp_obj_type=SmartDetectObjectType.PERSON, ufp_required_field="can_detect_person", ufp_enabled="is_person_detection_on", ufp_event_obj="last_person_detect_event", @@ -457,7 +460,7 @@ EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = ( key="smart_obj_vehicle", name="Vehicle detected", icon="mdi:car", - ufp_value="is_vehicle_currently_detected", + ufp_obj_type=SmartDetectObjectType.VEHICLE, ufp_required_field="can_detect_vehicle", ufp_enabled="is_vehicle_detection_on", ufp_event_obj="last_vehicle_detect_event", @@ -466,7 +469,7 @@ EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = ( key="smart_obj_animal", name="Animal detected", icon="mdi:paw", - ufp_value="is_animal_currently_detected", + ufp_obj_type=SmartDetectObjectType.ANIMAL, ufp_required_field="can_detect_animal", ufp_enabled="is_animal_detection_on", ufp_event_obj="last_animal_detect_event", @@ -475,8 +478,8 @@ EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = ( key="smart_obj_package", name="Package detected", icon="mdi:package-variant-closed", - ufp_value="is_package_currently_detected", entity_registry_enabled_default=False, + ufp_obj_type=SmartDetectObjectType.PACKAGE, ufp_required_field="can_detect_package", ufp_enabled="is_package_detection_on", ufp_event_obj="last_package_detect_event", @@ -485,7 +488,6 @@ EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = ( key="smart_audio_any", name="Audio object detected", icon="mdi:eye", - ufp_value="is_audio_currently_detected", ufp_required_field="feature_flags.has_smart_detect", ufp_event_obj="last_smart_audio_detect_event", ), @@ -493,7 +495,7 @@ EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = ( key="smart_audio_smoke", name="Smoke alarm detected", icon="mdi:fire", - ufp_value="is_smoke_currently_detected", + ufp_obj_type=SmartDetectObjectType.SMOKE, ufp_required_field="can_detect_smoke", ufp_enabled="is_smoke_detection_on", ufp_event_obj="last_smoke_detect_event", @@ -502,16 +504,16 @@ EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = ( key="smart_audio_cmonx", name="CO alarm detected", icon="mdi:molecule-co", - ufp_value="is_cmonx_currently_detected", ufp_required_field="can_detect_co", ufp_enabled="is_co_detection_on", ufp_event_obj="last_cmonx_detect_event", + ufp_obj_type=SmartDetectObjectType.CMONX, ), ProtectBinaryEventEntityDescription( key="smart_audio_siren", name="Siren detected", icon="mdi:alarm-bell", - ufp_value="is_siren_currently_detected", + ufp_obj_type=SmartDetectObjectType.SIREN, ufp_required_field="can_detect_siren", ufp_enabled="is_siren_detection_on", ufp_event_obj="last_siren_detect_event", @@ -520,7 +522,7 @@ EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = ( key="smart_audio_baby_cry", name="Baby cry detected", icon="mdi:cradle", - ufp_value="is_baby_cry_currently_detected", + ufp_obj_type=SmartDetectObjectType.BABY_CRY, ufp_required_field="can_detect_baby_cry", ufp_enabled="is_baby_cry_detection_on", ufp_event_obj="last_baby_cry_detect_event", @@ -529,7 +531,7 @@ EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = ( key="smart_audio_speak", name="Speaking detected", icon="mdi:account-voice", - ufp_value="is_speaking_currently_detected", + ufp_obj_type=SmartDetectObjectType.SPEAK, ufp_required_field="can_detect_speaking", ufp_enabled="is_speaking_detection_on", ufp_event_obj="last_speaking_detect_event", @@ -538,7 +540,7 @@ EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = ( key="smart_audio_bark", name="Barking detected", icon="mdi:dog", - ufp_value="is_bark_currently_detected", + ufp_obj_type=SmartDetectObjectType.BARK, ufp_required_field="can_detect_bark", ufp_enabled="is_bark_detection_on", ufp_event_obj="last_bark_detect_event", @@ -547,7 +549,7 @@ EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = ( key="smart_audio_car_alarm", name="Car alarm detected", icon="mdi:car", - ufp_value="is_car_alarm_currently_detected", + ufp_obj_type=SmartDetectObjectType.BURGLAR, ufp_required_field="can_detect_car_alarm", ufp_enabled="is_car_alarm_detection_on", ufp_event_obj="last_car_alarm_detect_event", @@ -556,7 +558,7 @@ EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = ( key="smart_audio_car_horn", name="Car horn detected", icon="mdi:bugle", - ufp_value="is_car_horn_currently_detected", + ufp_obj_type=SmartDetectObjectType.CAR_HORN, ufp_required_field="can_detect_car_horn", ufp_enabled="is_car_horn_detection_on", ufp_event_obj="last_car_horn_detect_event", @@ -565,7 +567,7 @@ EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = ( key="smart_audio_glass_break", name="Glass break detected", icon="mdi:glass-fragile", - ufp_value="last_glass_break_detect", + ufp_obj_type=SmartDetectObjectType.GLASS_BREAK, ufp_required_field="can_detect_glass_break", ufp_enabled="is_glass_break_detection_on", ufp_event_obj="last_glass_break_detect_event", @@ -709,11 +711,50 @@ class ProtectEventBinarySensor(EventEntityMixin, BinarySensorEntity): @callback def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None: super()._async_update_device_from_protect(device) - is_on = self.entity_description.get_is_on(self.device, self._event) - self._attr_is_on = is_on - if not is_on: - self._event = None + description = self.entity_description + event = self._event = self.entity_description.get_event_obj(device) + if is_on := bool(description.get_ufp_value(device)): + if event: + self._set_event_attrs(event) + else: self._attr_extra_state_attributes = {} + self._attr_is_on = is_on + + +class ProtectSmartEventBinarySensor(EventEntityMixin, BinarySensorEntity): + """A UniFi Protect Device Binary Sensor for smart events.""" + + device: Camera + entity_description: ProtectBinaryEventEntityDescription + _state_attrs = ("_attr_available", "_attr_is_on", "_attr_extra_state_attributes") + + @callback + def _set_event_done(self) -> None: + self._attr_is_on = False + self._attr_extra_state_attributes = {} + + @callback + def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None: + prev_event = self._event + super()._async_update_device_from_protect(device) + description = self.entity_description + self._event = description.get_event_obj(device) + + if not ( + (event := self._event) + and not self._event_already_ended(prev_event) + and description.has_matching_smart(event) + and ((is_end := event.end) or self.device.is_smart_detected) + ): + self._set_event_done() + return + + was_on = self._attr_is_on + self._attr_is_on = True + self._set_event_attrs(event) + + if is_end and not was_on: + self._async_event_with_immediate_end() MODEL_DESCRIPTIONS_WITH_CLASS = ( @@ -727,12 +768,19 @@ def _async_event_entities( data: ProtectData, ufp_device: ProtectAdoptableDeviceModel | None = None, ) -> list[ProtectDeviceEntity]: - return [ - ProtectEventBinarySensor(data, device, description) - for device in (data.get_cameras() if ufp_device is None else [ufp_device]) - for description in EVENT_SENSORS - if description.has_required(device) - ] + entities: list[ProtectDeviceEntity] = [] + for device in data.get_cameras() if ufp_device is None else [ufp_device]: + entities.extend( + ProtectSmartEventBinarySensor(data, device, description) + for description in SMART_EVENT_SENSORS + if description.has_required(device) + ) + entities.extend( + ProtectEventBinarySensor(data, device, description) + for description in EVENT_SENSORS + if description.has_required(device) + ) + return entities @callback diff --git a/homeassistant/components/unifiprotect/entity.py b/homeassistant/components/unifiprotect/entity.py index adf0d334e0a..3777338209b 100644 --- a/homeassistant/components/unifiprotect/entity.py +++ b/homeassistant/components/unifiprotect/entity.py @@ -305,13 +305,27 @@ class EventEntityMixin(ProtectDeviceEntity): _event: Event | None = None @callback - def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None: - if (event := self.entity_description.get_event_obj(device)) is None: - self._attr_extra_state_attributes = {} - else: - self._attr_extra_state_attributes = { - ATTR_EVENT_ID: event.id, - ATTR_EVENT_SCORE: event.score, - } - self._event = event - super()._async_update_device_from_protect(device) + def _set_event_done(self) -> None: + """Clear the event and state.""" + + @callback + def _set_event_attrs(self, event: Event) -> None: + """Set event attrs.""" + self._attr_extra_state_attributes = { + ATTR_EVENT_ID: event.id, + ATTR_EVENT_SCORE: event.score, + } + + @callback + def _async_event_with_immediate_end(self) -> None: + # If the event is so short that the detection is received + # in the same message as the end of the event we need to write + # state and than clear the event and write state again. + self.async_write_ha_state() + self._set_event_done() + self.async_write_ha_state() + + @callback + def _event_already_ended(self, prev_event: Event | None) -> bool: + event = self._event + return bool(event and event.end and prev_event and prev_event.id == event.id) diff --git a/homeassistant/components/unifiprotect/models.py b/homeassistant/components/unifiprotect/models.py index fc24ddaa6e3..3bd2416b550 100644 --- a/homeassistant/components/unifiprotect/models.py +++ b/homeassistant/components/unifiprotect/models.py @@ -10,7 +10,12 @@ import logging from operator import attrgetter from typing import Any, Generic, TypeVar -from uiprotect.data import NVR, Event, ProtectAdoptableDeviceModel +from uiprotect.data import ( + NVR, + Event, + ProtectAdoptableDeviceModel, + SmartDetectObjectType, +) from homeassistant.helpers.entity import EntityDescription @@ -79,21 +84,24 @@ class ProtectEventMixin(ProtectEntityDescription[T]): """Mixin for events.""" ufp_event_obj: str | None = None + ufp_obj_type: SmartDetectObjectType | None = None def get_event_obj(self, obj: T) -> Event | None: """Return value from UniFi Protect device.""" return None + def has_matching_smart(self, event: Event) -> bool: + """Determine if the detection type is a match.""" + return ( + not (obj_type := self.ufp_obj_type) or obj_type in event.smart_detect_types + ) + def __post_init__(self) -> None: """Override get_event_obj if ufp_event_obj is set.""" if (_ufp_event_obj := self.ufp_event_obj) is not None: object.__setattr__(self, "get_event_obj", attrgetter(_ufp_event_obj)) super().__post_init__() - def get_is_on(self, obj: T, event: Event | None) -> bool: - """Return value if event is active.""" - return event is not None and self.get_ufp_value(obj) - @dataclass(frozen=True, kw_only=True) class ProtectSetableKeysMixin(ProtectEntityDescription[T]): diff --git a/homeassistant/components/unifiprotect/sensor.py b/homeassistant/components/unifiprotect/sensor.py index e166d532dfb..ccd341088ef 100644 --- a/homeassistant/components/unifiprotect/sensor.py +++ b/homeassistant/components/unifiprotect/sensor.py @@ -18,6 +18,7 @@ from uiprotect.data import ( ProtectDeviceModel, ProtectModelWithId, Sensor, + SmartDetectObjectType, ) from homeassistant.components.sensor import ( @@ -542,7 +543,7 @@ LICENSE_PLATE_EVENT_SENSORS: tuple[ProtectSensorEventEntityDescription, ...] = ( name="License plate detected", icon="mdi:car", translation_key="license_plate", - ufp_value="is_license_plate_currently_detected", + ufp_obj_type=SmartDetectObjectType.LICENSE_PLATE, ufp_required_field="can_detect_license_plate", ufp_event_obj="last_license_plate_detect_event", ), @@ -747,19 +748,34 @@ class ProtectEventSensor(EventEntityMixin, SensorEntity): class ProtectLicensePlateEventSensor(ProtectEventSensor): """A UniFi Protect license plate sensor.""" + device: Camera + + @callback + def _set_event_done(self) -> None: + self._attr_native_value = OBJECT_TYPE_NONE + self._attr_extra_state_attributes = {} + @callback def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None: + prev_event = self._event super()._async_update_device_from_protect(device) - event = self._event - entity_description = self.entity_description - if ( - event is None - or (event.metadata is None or event.metadata.license_plate is None) - or not entity_description.get_is_on(self.device, event) + description = self.entity_description + self._event = description.get_event_obj(device) + + if not ( + (event := self._event) + and not self._event_already_ended(prev_event) + and description.has_matching_smart(event) + and ((is_end := event.end) or self.device.is_smart_detected) + and (metadata := event.metadata) + and (license_plate := metadata.license_plate) ): - self._attr_native_value = OBJECT_TYPE_NONE - self._event = None - self._attr_extra_state_attributes = {} + self._set_event_done() return - self._attr_native_value = event.metadata.license_plate.name + previous_plate = self._attr_native_value + self._attr_native_value = license_plate.name + self._set_event_attrs(event) + + if is_end and previous_plate != license_plate.name: + self._async_event_with_immediate_end() diff --git a/tests/components/unifiprotect/conftest.py b/tests/components/unifiprotect/conftest.py index 02a1ce3f421..6366a4f9244 100644 --- a/tests/components/unifiprotect/conftest.py +++ b/tests/components/unifiprotect/conftest.py @@ -217,6 +217,7 @@ def doorbell_fixture(camera: Camera, fixed_now: datetime): SmartDetectObjectType.PERSON, SmartDetectObjectType.VEHICLE, SmartDetectObjectType.ANIMAL, + SmartDetectObjectType.PACKAGE, ] doorbell.has_speaker = True doorbell.feature_flags.has_hdr = True diff --git a/tests/components/unifiprotect/test_binary_sensor.py b/tests/components/unifiprotect/test_binary_sensor.py index 4674ec289ca..51fb882144f 100644 --- a/tests/components/unifiprotect/test_binary_sensor.py +++ b/tests/components/unifiprotect/test_binary_sensor.py @@ -5,7 +5,17 @@ from __future__ import annotations from datetime import datetime, timedelta from unittest.mock import Mock -from uiprotect.data import Camera, Event, EventType, Light, ModelType, MountType, Sensor +import pytest +from uiprotect.data import ( + Camera, + Event, + EventType, + Light, + ModelType, + MountType, + Sensor, + SmartDetectObjectType, +) from uiprotect.data.nvr import EventMetadata from homeassistant.components.binary_sensor import BinarySensorDeviceClass @@ -15,6 +25,7 @@ from homeassistant.components.unifiprotect.binary_sensor import ( LIGHT_SENSORS, MOUNTABLE_SENSE_SENSORS, SENSE_SENSORS, + SMART_EVENT_SENSORS, ) from homeassistant.components.unifiprotect.const import ( ATTR_EVENT_SCORE, @@ -23,12 +34,13 @@ from homeassistant.components.unifiprotect.const import ( from homeassistant.const import ( ATTR_ATTRIBUTION, ATTR_DEVICE_CLASS, + EVENT_STATE_CHANGED, STATE_OFF, STATE_ON, STATE_UNAVAILABLE, Platform, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import Event as HAEvent, EventStateChangedData, HomeAssistant from homeassistant.helpers import entity_registry as er from .utils import ( @@ -40,6 +52,8 @@ from .utils import ( remove_entities, ) +from tests.common import async_capture_events + LIGHT_SENSOR_WRITE = LIGHT_SENSORS[:2] SENSE_SENSORS_WRITE = SENSE_SENSORS[:3] @@ -51,11 +65,11 @@ async def test_binary_sensor_camera_remove( ufp.api.bootstrap.nvr.system_info.ustorage = None await init_entry(hass, ufp, [doorbell, unadopted_camera]) - assert_entity_counts(hass, Platform.BINARY_SENSOR, 8, 8) + assert_entity_counts(hass, Platform.BINARY_SENSOR, 9, 8) await remove_entities(hass, ufp, [doorbell, unadopted_camera]) assert_entity_counts(hass, Platform.BINARY_SENSOR, 0, 0) await adopt_devices(hass, ufp, [doorbell, unadopted_camera]) - assert_entity_counts(hass, Platform.BINARY_SENSOR, 8, 8) + assert_entity_counts(hass, Platform.BINARY_SENSOR, 9, 8) async def test_binary_sensor_light_remove( @@ -123,7 +137,7 @@ async def test_binary_sensor_setup_camera_all( ufp.api.bootstrap.nvr.system_info.ustorage = None await init_entry(hass, ufp, [doorbell, unadopted_camera]) - assert_entity_counts(hass, Platform.BINARY_SENSOR, 8, 8) + assert_entity_counts(hass, Platform.BINARY_SENSOR, 9, 8) description = EVENT_SENSORS[0] unique_id, entity_id = ids_from_device_description( @@ -273,7 +287,7 @@ async def test_binary_sensor_update_motion( """Test binary_sensor motion entity.""" await init_entry(hass, ufp, [doorbell, unadopted_camera]) - assert_entity_counts(hass, Platform.BINARY_SENSOR, 14, 14) + assert_entity_counts(hass, Platform.BINARY_SENSOR, 15, 14) _, entity_id = ids_from_device_description( Platform.BINARY_SENSOR, doorbell, EVENT_SENSORS[1] @@ -421,3 +435,144 @@ async def test_binary_sensor_update_mount_type_garage( assert ( state.attributes[ATTR_DEVICE_CLASS] == BinarySensorDeviceClass.GARAGE_DOOR.value ) + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_binary_sensor_package_detected( + hass: HomeAssistant, + ufp: MockUFPFixture, + doorbell: Camera, + unadopted_camera: Camera, + fixed_now: datetime, +) -> None: + """Test binary_sensor package detection entity.""" + + await init_entry(hass, ufp, [doorbell, unadopted_camera]) + assert_entity_counts(hass, Platform.BINARY_SENSOR, 15, 15) + + doorbell.smart_detect_settings.object_types.append(SmartDetectObjectType.PACKAGE) + + _, entity_id = ids_from_device_description( + Platform.BINARY_SENSOR, doorbell, SMART_EVENT_SENSORS[4] + ) + + event = Event( + model=ModelType.EVENT, + id="test_event_id", + type=EventType.SMART_DETECT, + start=fixed_now - timedelta(seconds=1), + end=None, + score=100, + smart_detect_types=[SmartDetectObjectType.PACKAGE], + smart_detect_event_ids=[], + camera_id=doorbell.id, + api=ufp.api, + ) + + new_camera = doorbell.copy() + new_camera.is_smart_detected = True + new_camera.last_smart_detect_event_ids[SmartDetectObjectType.PACKAGE] = event.id + + ufp.api.bootstrap.cameras = {new_camera.id: new_camera} + ufp.api.bootstrap.events = {event.id: event} + + mock_msg = Mock() + mock_msg.changed_data = {} + mock_msg.new_obj = event + ufp.ws_msg(mock_msg) + + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state + assert state.state == STATE_ON + assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION + assert state.attributes[ATTR_EVENT_SCORE] == 100 + + event = Event( + model=ModelType.EVENT, + id="test_event_id", + type=EventType.SMART_DETECT, + start=fixed_now - timedelta(seconds=1), + end=fixed_now + timedelta(seconds=1), + score=50, + smart_detect_types=[SmartDetectObjectType.PACKAGE], + smart_detect_event_ids=[], + camera_id=doorbell.id, + api=ufp.api, + ) + + new_camera = doorbell.copy() + new_camera.is_smart_detected = True + new_camera.last_smart_detect_event_ids[SmartDetectObjectType.PACKAGE] = event.id + + ufp.api.bootstrap.cameras = {new_camera.id: new_camera} + ufp.api.bootstrap.events = {event.id: event} + + mock_msg = Mock() + mock_msg.changed_data = {} + mock_msg.new_obj = event + ufp.ws_msg(mock_msg) + + await hass.async_block_till_done() + + # Event is already seen and has end, should now be off + state = hass.states.get(entity_id) + assert state + assert state.state == STATE_OFF + + # Now send an event that has an end right away + event = Event( + model=ModelType.EVENT, + id="new_event_id", + type=EventType.SMART_DETECT, + start=fixed_now - timedelta(seconds=1), + end=fixed_now + timedelta(seconds=1), + score=80, + smart_detect_types=[SmartDetectObjectType.PACKAGE], + smart_detect_event_ids=[], + camera_id=doorbell.id, + api=ufp.api, + ) + + new_camera = doorbell.copy() + new_camera.is_smart_detected = True + new_camera.last_smart_detect_event_ids[SmartDetectObjectType.PACKAGE] = event.id + + ufp.api.bootstrap.cameras = {new_camera.id: new_camera} + ufp.api.bootstrap.events = {event.id: event} + + mock_msg = Mock() + mock_msg.changed_data = {} + mock_msg.new_obj = event + + state_changes: list[HAEvent[EventStateChangedData]] = async_capture_events( + hass, EVENT_STATE_CHANGED + ) + ufp.ws_msg(mock_msg) + + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state + assert state.state == STATE_OFF + + assert len(state_changes) == 2 + + on_event = state_changes[0] + state = on_event.data["new_state"] + assert state + assert state.state == STATE_ON + assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION + assert state.attributes[ATTR_EVENT_SCORE] == 80 + + off_event = state_changes[1] + state = off_event.data["new_state"] + assert state + assert state.state == STATE_OFF + assert ATTR_EVENT_SCORE not in state.attributes + + # replay and ensure ignored + ufp.ws_msg(mock_msg) + await hass.async_block_till_done() + assert len(state_changes) == 2 diff --git a/tests/components/unifiprotect/test_sensor.py b/tests/components/unifiprotect/test_sensor.py index ac631ee41a6..b3842be4e0a 100644 --- a/tests/components/unifiprotect/test_sensor.py +++ b/tests/components/unifiprotect/test_sensor.py @@ -30,11 +30,12 @@ from homeassistant.components.unifiprotect.sensor import ( ) from homeassistant.const import ( ATTR_ATTRIBUTION, + EVENT_STATE_CHANGED, STATE_UNAVAILABLE, STATE_UNKNOWN, Platform, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import Event as HAEvent, EventStateChangedData, HomeAssistant from homeassistant.helpers import entity_registry as er from .utils import ( @@ -49,6 +50,8 @@ from .utils import ( time_changed, ) +from tests.common import async_capture_events + CAMERA_SENSORS_WRITE = CAMERA_SENSORS[:5] SENSE_SENSORS_WRITE = SENSE_SENSORS[:8] @@ -554,6 +557,10 @@ async def test_camera_update_license_plate( ufp.api.bootstrap.cameras = {new_camera.id: new_camera} ufp.api.bootstrap.events = {event.id: event} + + state_changes: list[HAEvent[EventStateChangedData]] = async_capture_events( + hass, EVENT_STATE_CHANGED + ) ufp.ws_msg(mock_msg) await hass.async_block_till_done() @@ -561,6 +568,63 @@ async def test_camera_update_license_plate( assert state assert state.state == "ABCD1234" + assert len(state_changes) == 1 + + # ensure reply is ignored + ufp.ws_msg(mock_msg) + await hass.async_block_till_done() + assert len(state_changes) == 1 + + event = Event( + model=ModelType.EVENT, + id="test_event_id", + type=EventType.SMART_DETECT, + start=fixed_now - timedelta(seconds=1), + end=fixed_now + timedelta(seconds=1), + score=100, + smart_detect_types=[SmartDetectObjectType.LICENSE_PLATE], + smart_detect_event_ids=[], + metadata=event_metadata, + api=ufp.api, + ) + + ufp.api.bootstrap.events = {event.id: event} + new_camera.last_smart_detect_event_ids[SmartDetectObjectType.LICENSE_PLATE] = ( + event.id + ) + ufp.ws_msg(mock_msg) + await hass.async_block_till_done() + assert len(state_changes) == 2 + state = hass.states.get(entity_id) + assert state + assert state.state == "none" + + # Now send a new event with end already set + event = Event( + model=ModelType.EVENT, + id="new_event", + type=EventType.SMART_DETECT, + start=fixed_now - timedelta(seconds=1), + end=fixed_now + timedelta(seconds=1), + score=100, + smart_detect_types=[SmartDetectObjectType.LICENSE_PLATE], + smart_detect_event_ids=[], + metadata=event_metadata, + api=ufp.api, + ) + + ufp.api.bootstrap.events = {event.id: event} + new_camera.last_smart_detect_event_ids[SmartDetectObjectType.LICENSE_PLATE] = ( + event.id + ) + ufp.ws_msg(mock_msg) + await hass.async_block_till_done() + assert len(state_changes) == 4 + assert state_changes[2].data["new_state"].state == "ABCD1234" + state = hass.states.get(entity_id) + assert state + assert state.state == "none" + async def test_sensor_precision( hass: HomeAssistant, ufp: MockUFPFixture, sensor_all: Sensor, fixed_now: datetime diff --git a/tests/components/unifiprotect/test_switch.py b/tests/components/unifiprotect/test_switch.py index da16475dc1c..6e5c83ef237 100644 --- a/tests/components/unifiprotect/test_switch.py +++ b/tests/components/unifiprotect/test_switch.py @@ -59,11 +59,11 @@ async def test_switch_camera_remove( ufp.api.bootstrap.nvr.system_info.ustorage = None await init_entry(hass, ufp, [doorbell, unadopted_camera]) - assert_entity_counts(hass, Platform.SWITCH, 16, 14) + assert_entity_counts(hass, Platform.SWITCH, 17, 15) await remove_entities(hass, ufp, [doorbell, unadopted_camera]) assert_entity_counts(hass, Platform.SWITCH, 2, 2) await adopt_devices(hass, ufp, [doorbell, unadopted_camera]) - assert_entity_counts(hass, Platform.SWITCH, 16, 14) + assert_entity_counts(hass, Platform.SWITCH, 17, 15) async def test_switch_light_remove( @@ -175,7 +175,7 @@ async def test_switch_setup_camera_all( """Test switch entity setup for camera devices (all enabled feature flags).""" await init_entry(hass, ufp, [doorbell]) - assert_entity_counts(hass, Platform.SWITCH, 16, 14) + assert_entity_counts(hass, Platform.SWITCH, 17, 15) for description in CAMERA_SWITCHES_BASIC: unique_id, entity_id = ids_from_device_description( @@ -295,7 +295,7 @@ async def test_switch_camera_ssh( """Tests SSH switch for cameras.""" await init_entry(hass, ufp, [doorbell]) - assert_entity_counts(hass, Platform.SWITCH, 16, 14) + assert_entity_counts(hass, Platform.SWITCH, 17, 15) description = CAMERA_SWITCHES[0] @@ -328,7 +328,7 @@ async def test_switch_camera_simple( """Tests all simple switches for cameras.""" await init_entry(hass, ufp, [doorbell]) - assert_entity_counts(hass, Platform.SWITCH, 16, 14) + assert_entity_counts(hass, Platform.SWITCH, 17, 15) assert description.ufp_set_method is not None @@ -357,7 +357,7 @@ async def test_switch_camera_highfps( """Tests High FPS switch for cameras.""" await init_entry(hass, ufp, [doorbell]) - assert_entity_counts(hass, Platform.SWITCH, 16, 14) + assert_entity_counts(hass, Platform.SWITCH, 17, 15) description = CAMERA_SWITCHES[3] @@ -388,7 +388,7 @@ async def test_switch_camera_privacy( previous_record = doorbell.recording_settings.mode = RecordingMode.DETECTIONS await init_entry(hass, ufp, [doorbell]) - assert_entity_counts(hass, Platform.SWITCH, 16, 14) + assert_entity_counts(hass, Platform.SWITCH, 17, 15) description = PRIVACY_MODE_SWITCH @@ -440,7 +440,7 @@ async def test_switch_camera_privacy_already_on( doorbell.add_privacy_zone() await init_entry(hass, ufp, [doorbell]) - assert_entity_counts(hass, Platform.SWITCH, 16, 14) + assert_entity_counts(hass, Platform.SWITCH, 17, 15) description = PRIVACY_MODE_SWITCH From 4de8cca9117c31fd7fed3684e9a691a26b39cca7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 20 Jun 2024 22:12:31 -0500 Subject: [PATCH 0860/1445] Disable generic unifiprotect object sensors by default (#120059) --- homeassistant/components/unifiprotect/binary_sensor.py | 2 ++ tests/components/unifiprotect/test_binary_sensor.py | 8 ++++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/unifiprotect/binary_sensor.py b/homeassistant/components/unifiprotect/binary_sensor.py index 9bda0e8f310..966354749bc 100644 --- a/homeassistant/components/unifiprotect/binary_sensor.py +++ b/homeassistant/components/unifiprotect/binary_sensor.py @@ -446,6 +446,7 @@ SMART_EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = ( icon="mdi:eye", ufp_required_field="feature_flags.has_smart_detect", ufp_event_obj="last_smart_detect_event", + entity_registry_enabled_default=False, ), ProtectBinaryEventEntityDescription( key="smart_obj_person", @@ -490,6 +491,7 @@ SMART_EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = ( icon="mdi:eye", ufp_required_field="feature_flags.has_smart_detect", ufp_event_obj="last_smart_audio_detect_event", + entity_registry_enabled_default=False, ), ProtectBinaryEventEntityDescription( key="smart_audio_smoke", diff --git a/tests/components/unifiprotect/test_binary_sensor.py b/tests/components/unifiprotect/test_binary_sensor.py index 51fb882144f..42782d10429 100644 --- a/tests/components/unifiprotect/test_binary_sensor.py +++ b/tests/components/unifiprotect/test_binary_sensor.py @@ -65,11 +65,11 @@ async def test_binary_sensor_camera_remove( ufp.api.bootstrap.nvr.system_info.ustorage = None await init_entry(hass, ufp, [doorbell, unadopted_camera]) - assert_entity_counts(hass, Platform.BINARY_SENSOR, 9, 8) + assert_entity_counts(hass, Platform.BINARY_SENSOR, 9, 6) await remove_entities(hass, ufp, [doorbell, unadopted_camera]) assert_entity_counts(hass, Platform.BINARY_SENSOR, 0, 0) await adopt_devices(hass, ufp, [doorbell, unadopted_camera]) - assert_entity_counts(hass, Platform.BINARY_SENSOR, 9, 8) + assert_entity_counts(hass, Platform.BINARY_SENSOR, 9, 6) async def test_binary_sensor_light_remove( @@ -137,7 +137,7 @@ async def test_binary_sensor_setup_camera_all( ufp.api.bootstrap.nvr.system_info.ustorage = None await init_entry(hass, ufp, [doorbell, unadopted_camera]) - assert_entity_counts(hass, Platform.BINARY_SENSOR, 9, 8) + assert_entity_counts(hass, Platform.BINARY_SENSOR, 9, 6) description = EVENT_SENSORS[0] unique_id, entity_id = ids_from_device_description( @@ -287,7 +287,7 @@ async def test_binary_sensor_update_motion( """Test binary_sensor motion entity.""" await init_entry(hass, ufp, [doorbell, unadopted_camera]) - assert_entity_counts(hass, Platform.BINARY_SENSOR, 15, 14) + assert_entity_counts(hass, Platform.BINARY_SENSOR, 15, 12) _, entity_id = ids_from_device_description( Platform.BINARY_SENSOR, doorbell, EVENT_SENSORS[1] From 353e4865e1afd425dfbbc2678ca2dde5f2e990b9 Mon Sep 17 00:00:00 2001 From: RJPoelstra <36924801+RJPoelstra@users.noreply.github.com> Date: Fri, 21 Jun 2024 07:22:12 +0200 Subject: [PATCH 0861/1445] Make preset list indicate whether the current mount position matches a preset in Vogel's Motionmount (#118731) --- .../components/motionmount/select.py | 29 +++++++++++++++---- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/motionmount/select.py b/homeassistant/components/motionmount/select.py index b9001b55b7f..d15bbb7326b 100644 --- a/homeassistant/components/motionmount/select.py +++ b/homeassistant/components/motionmount/select.py @@ -24,7 +24,6 @@ class MotionMountPresets(MotionMountEntity, SelectEntity): """The presets of a MotionMount.""" _attr_translation_key = "motionmount_preset" - _attr_current_option: str | None = None def __init__( self, @@ -34,6 +33,7 @@ class MotionMountPresets(MotionMountEntity, SelectEntity): """Initialize Preset selector.""" super().__init__(mm, config_entry) self._attr_unique_id = f"{self._base_unique_id}-preset" + self._presets: list[motionmount.Preset] = [] def _update_options(self, presets: list[motionmount.Preset]) -> None: """Convert presets to select options.""" @@ -44,11 +44,30 @@ class MotionMountPresets(MotionMountEntity, SelectEntity): async def async_update(self) -> None: """Get latest state from MotionMount.""" - presets = await self.mm.get_presets() - self._update_options(presets) + self._presets = await self.mm.get_presets() + self._update_options(self._presets) - if self._attr_current_option is None: - self._attr_current_option = self._attr_options[0] + @property + def current_option(self) -> str | None: + """Get the current option.""" + # When the mount is moving we return the currently selected option + if self.mm.is_moving: + return self._attr_current_option + + # When the mount isn't moving we select the option that matches the current position + self._attr_current_option = None + if self.mm.extension == 0 and self.mm.turn == 0: + self._attr_current_option = self._attr_options[0] # Select Wall preset + else: + for preset in self._presets: + if ( + preset.extension == self.mm.extension + and preset.turn == self.mm.turn + ): + self._attr_current_option = f"{preset.index}: {preset.name}" + break + + return self._attr_current_option async def async_select_option(self, option: str) -> None: """Set the new option.""" From 1962759953978567ae960cc6ca9653c3a202ec34 Mon Sep 17 00:00:00 2001 From: Markus Jacobsen Date: Fri, 21 Jun 2024 08:35:38 +0200 Subject: [PATCH 0862/1445] Add Bang olufsen init testing (#119834) --- .coveragerc | 1 - tests/components/bang_olufsen/conftest.py | 13 +++- tests/components/bang_olufsen/test_init.py | 90 ++++++++++++++++++++++ 3 files changed, 101 insertions(+), 3 deletions(-) create mode 100644 tests/components/bang_olufsen/test_init.py diff --git a/.coveragerc b/.coveragerc index 4d0f78a81f5..303f9696fe3 100644 --- a/.coveragerc +++ b/.coveragerc @@ -130,7 +130,6 @@ omit = homeassistant/components/baf/sensor.py homeassistant/components/baf/switch.py homeassistant/components/baidu/tts.py - homeassistant/components/bang_olufsen/__init__.py homeassistant/components/bang_olufsen/entity.py homeassistant/components/bang_olufsen/media_player.py homeassistant/components/bang_olufsen/util.py diff --git a/tests/components/bang_olufsen/conftest.py b/tests/components/bang_olufsen/conftest.py index e77dc4d16a9..1fbcbe0fe69 100644 --- a/tests/components/bang_olufsen/conftest.py +++ b/tests/components/bang_olufsen/conftest.py @@ -1,10 +1,10 @@ """Test fixtures for bang_olufsen.""" -from unittest.mock import AsyncMock, patch +from collections.abc import Generator +from unittest.mock import AsyncMock, Mock, patch from mozart_api.models import BeolinkPeer import pytest -from typing_extensions import Generator from homeassistant.components.bang_olufsen.const import DOMAIN @@ -44,10 +44,19 @@ def mock_mozart_client() -> Generator[AsyncMock]: ), ): client = mock_client.return_value + + # REST API client methods client.get_beolink_self = AsyncMock() client.get_beolink_self.return_value = BeolinkPeer( friendly_name=TEST_FRIENDLY_NAME, jid=TEST_JID_1 ) + + # Non-REST API client methods + client.check_device_connection = AsyncMock() + client.close_api_client = AsyncMock() + client.connect_notifications = AsyncMock() + client.disconnect_notifications = Mock() + yield client diff --git a/tests/components/bang_olufsen/test_init.py b/tests/components/bang_olufsen/test_init.py new file mode 100644 index 00000000000..11742b846ae --- /dev/null +++ b/tests/components/bang_olufsen/test_init.py @@ -0,0 +1,90 @@ +"""Test the bang_olufsen __init__.""" + +from aiohttp.client_exceptions import ServerTimeoutError + +from homeassistant.components.bang_olufsen import DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import DeviceRegistry + +from .const import TEST_MODEL_BALANCE, TEST_NAME, TEST_SERIAL_NUMBER + + +async def test_setup_entry( + hass: HomeAssistant, + mock_config_entry, + mock_mozart_client, + device_registry: DeviceRegistry, +) -> None: + """Test async_setup_entry.""" + + assert mock_config_entry.state == ConfigEntryState.NOT_LOADED + + # Load entry + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + + assert mock_config_entry.state == ConfigEntryState.LOADED + + # Check that the device has been registered properly + device = device_registry.async_get_device( + identifiers={(DOMAIN, TEST_SERIAL_NUMBER)} + ) + assert device is not None + assert device.name == TEST_NAME + assert device.model == TEST_MODEL_BALANCE + + # Ensure that the connection has been checked WebSocket connection has been initialized + assert mock_mozart_client.check_device_connection.call_count == 1 + assert mock_mozart_client.close_api_client.call_count == 0 + assert mock_mozart_client.connect_notifications.call_count == 1 + + +async def test_setup_entry_failed( + hass: HomeAssistant, mock_config_entry, mock_mozart_client +) -> None: + """Test failed async_setup_entry.""" + + # Set the device connection check to fail + mock_mozart_client.check_device_connection.side_effect = ExceptionGroup( + "", (ServerTimeoutError(), TimeoutError()) + ) + + assert mock_config_entry.state == ConfigEntryState.NOT_LOADED + + # Load entry + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + + assert mock_config_entry.state == ConfigEntryState.SETUP_RETRY + + # Ensure that the connection has been checked, API client correctly closed + # and WebSocket connection has not been initialized + assert mock_mozart_client.check_device_connection.call_count == 1 + assert mock_mozart_client.close_api_client.call_count == 1 + assert mock_mozart_client.connect_notifications.call_count == 0 + + +async def test_unload_entry( + hass: HomeAssistant, mock_config_entry, mock_mozart_client +) -> None: + """Test unload_entry.""" + + # Load entry + assert mock_config_entry.state == ConfigEntryState.NOT_LOADED + + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + + assert mock_config_entry.state == ConfigEntryState.LOADED + + # Unload entry + await hass.config_entries.async_unload(mock_config_entry.entry_id) + + # Ensure WebSocket notification listener and REST API client have been closed + assert mock_mozart_client.disconnect_notifications.call_count == 1 + assert mock_mozart_client.close_api_client.call_count == 1 + + # Ensure that the entry is not loaded and has been removed from hass + assert mock_config_entry.entry_id not in hass.data[DOMAIN] + assert mock_config_entry.state == ConfigEntryState.NOT_LOADED From 2add53e334974f9b82643c8fd4161de9335c44e3 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Fri, 21 Jun 2024 08:47:50 +0200 Subject: [PATCH 0863/1445] Bump aioimaplib to 1.1.0 (#120045) --- homeassistant/components/imap/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/imap/manifest.json b/homeassistant/components/imap/manifest.json index 3c35d00f714..b058a3d50f4 100644 --- a/homeassistant/components/imap/manifest.json +++ b/homeassistant/components/imap/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/imap", "iot_class": "cloud_push", "loggers": ["aioimaplib"], - "requirements": ["aioimaplib==1.0.1"] + "requirements": ["aioimaplib==1.1.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index c095234ee84..65568ed5906 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -267,7 +267,7 @@ aiohomekit==3.1.5 aiohue==4.7.1 # homeassistant.components.imap -aioimaplib==1.0.1 +aioimaplib==1.1.0 # homeassistant.components.apache_kafka aiokafka==0.10.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4b97590127c..8b63f1eb66d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -243,7 +243,7 @@ aiohomekit==3.1.5 aiohue==4.7.1 # homeassistant.components.imap -aioimaplib==1.0.1 +aioimaplib==1.1.0 # homeassistant.components.apache_kafka aiokafka==0.10.0 From b3722d60cbcdd8fc41f1197ea9462123b152ab30 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 21 Jun 2024 08:55:06 +0200 Subject: [PATCH 0864/1445] Bump actions/checkout from 4.1.6 to 4.1.7 (#120063) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 6da5a570d22..b5ae02c2627 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -632,7 +632,7 @@ jobs: - base steps: - name: Check out code from GitHub - uses: actions/checkout@v4.1.6 + uses: actions/checkout@v4.1.7 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python uses: actions/setup-python@v5.1.0 From 4aa7a9faee0256f76ffa8d9ae0e354860d8c713d Mon Sep 17 00:00:00 2001 From: Thomas Kistler Date: Fri, 21 Jun 2024 00:07:14 -0700 Subject: [PATCH 0865/1445] Fix Hydrawise volume unit bug (#119988) --- homeassistant/components/hydrawise/sensor.py | 15 +++++--- tests/components/hydrawise/conftest.py | 7 +++- tests/components/hydrawise/test_sensor.py | 36 ++++++++++++++++++-- 3 files changed, 51 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/hydrawise/sensor.py b/homeassistant/components/hydrawise/sensor.py index 2497fe8f49d..fe4b33d5851 100644 --- a/homeassistant/components/hydrawise/sensor.py +++ b/homeassistant/components/hydrawise/sensor.py @@ -71,7 +71,6 @@ FLOW_CONTROLLER_SENSORS: tuple[HydrawiseSensorEntityDescription, ...] = ( key="daily_total_water_use", translation_key="daily_total_water_use", device_class=SensorDeviceClass.VOLUME, - native_unit_of_measurement=UnitOfVolume.GALLONS, suggested_display_precision=1, value_fn=_get_controller_daily_total_water_use, ), @@ -79,7 +78,6 @@ FLOW_CONTROLLER_SENSORS: tuple[HydrawiseSensorEntityDescription, ...] = ( key="daily_active_water_use", translation_key="daily_active_water_use", device_class=SensorDeviceClass.VOLUME, - native_unit_of_measurement=UnitOfVolume.GALLONS, suggested_display_precision=1, value_fn=_get_controller_daily_active_water_use, ), @@ -87,7 +85,6 @@ FLOW_CONTROLLER_SENSORS: tuple[HydrawiseSensorEntityDescription, ...] = ( key="daily_inactive_water_use", translation_key="daily_inactive_water_use", device_class=SensorDeviceClass.VOLUME, - native_unit_of_measurement=UnitOfVolume.GALLONS, suggested_display_precision=1, value_fn=_get_controller_daily_inactive_water_use, ), @@ -98,7 +95,6 @@ FLOW_ZONE_SENSORS: tuple[SensorEntityDescription, ...] = ( key="daily_active_water_use", translation_key="daily_active_water_use", device_class=SensorDeviceClass.VOLUME, - native_unit_of_measurement=UnitOfVolume.GALLONS, suggested_display_precision=1, value_fn=_get_zone_daily_active_water_use, ), @@ -165,6 +161,17 @@ class HydrawiseSensor(HydrawiseEntity, SensorEntity): entity_description: HydrawiseSensorEntityDescription + @property + def native_unit_of_measurement(self) -> str | None: + """Return the unit_of_measurement of the sensor.""" + if self.entity_description.device_class != SensorDeviceClass.VOLUME: + return self.entity_description.native_unit_of_measurement + return ( + UnitOfVolume.GALLONS + if self.coordinator.data.user.units.units_name == "imperial" + else UnitOfVolume.LITERS + ) + @property def icon(self) -> str | None: """Icon of the entity based on the value.""" diff --git a/tests/components/hydrawise/conftest.py b/tests/components/hydrawise/conftest.py index 8bca1de5fed..eb1518eb7f2 100644 --- a/tests/components/hydrawise/conftest.py +++ b/tests/components/hydrawise/conftest.py @@ -15,6 +15,7 @@ from pydrawise.schema import ( Sensor, SensorModel, SensorStatus, + UnitsSummary, User, Zone, ) @@ -85,7 +86,11 @@ def mock_auth() -> Generator[AsyncMock]: @pytest.fixture def user() -> User: """Hydrawise User fixture.""" - return User(customer_id=12345, email="asdf@asdf.com") + return User( + customer_id=12345, + email="asdf@asdf.com", + units=UnitsSummary(units_name="imperial"), + ) @pytest.fixture diff --git a/tests/components/hydrawise/test_sensor.py b/tests/components/hydrawise/test_sensor.py index fcbc47c41f4..af75ad69ade 100644 --- a/tests/components/hydrawise/test_sensor.py +++ b/tests/components/hydrawise/test_sensor.py @@ -3,13 +3,18 @@ from collections.abc import Awaitable, Callable from unittest.mock import patch -from pydrawise.schema import Controller, Zone +from pydrawise.schema import Controller, User, Zone import pytest from syrupy.assertion import SnapshotAssertion from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er +from homeassistant.util.unit_system import ( + METRIC_SYSTEM, + US_CUSTOMARY_SYSTEM, + UnitSystem, +) from tests.common import MockConfigEntry, snapshot_platform @@ -45,7 +50,7 @@ async def test_suspended_state( assert next_cycle.state == "unknown" -async def test_no_sensor_and_water_state2( +async def test_no_sensor_and_water_state( hass: HomeAssistant, controller: Controller, mock_add_config_entry: Callable[[], Awaitable[MockConfigEntry]], @@ -63,3 +68,30 @@ async def test_no_sensor_and_water_state2( sensor = hass.states.get("binary_sensor.home_controller_connectivity") assert sensor is not None assert sensor.state == "on" + + +@pytest.mark.parametrize( + ("hydrawise_unit_system", "unit_system", "expected_state"), + [ + ("imperial", METRIC_SYSTEM, "454.6279552584"), + ("imperial", US_CUSTOMARY_SYSTEM, "120.1"), + ("metric", METRIC_SYSTEM, "120.1"), + ("metric", US_CUSTOMARY_SYSTEM, "31.7270634882136"), + ], +) +async def test_volume_unit_conversion( + hass: HomeAssistant, + unit_system: UnitSystem, + hydrawise_unit_system: str, + expected_state: str, + user: User, + mock_add_config_entry: Callable[[], Awaitable[MockConfigEntry]], +) -> None: + """Test volume unit conversion.""" + hass.config.units = unit_system + user.units.units_name = hydrawise_unit_system + await mock_add_config_entry() + + daily_active_water_use = hass.states.get("sensor.zone_one_daily_active_water_use") + assert daily_active_water_use is not None + assert daily_active_water_use.state == expected_state From f770fa0de0b7b7d8ee828750fdcb3230161dbd54 Mon Sep 17 00:00:00 2001 From: "Mr. Bubbles" Date: Fri, 21 Jun 2024 09:22:55 +0200 Subject: [PATCH 0866/1445] Fix translation key in config flow of One-Time Password (OTP) integration (#120053) --- homeassistant/components/otp/config_flow.py | 2 +- tests/components/otp/test_config_flow.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/otp/config_flow.py b/homeassistant/components/otp/config_flow.py index 7777b9b733a..5b1551b1d04 100644 --- a/homeassistant/components/otp/config_flow.py +++ b/homeassistant/components/otp/config_flow.py @@ -41,7 +41,7 @@ class TOTPConfigFlow(ConfigFlow, domain=DOMAIN): pyotp.TOTP(user_input[CONF_TOKEN]).now ) except binascii.Error: - errors["base"] = "invalid_code" + errors["base"] = "invalid_token" except Exception: _LOGGER.exception("Unexpected exception") errors["base"] = "unknown" diff --git a/tests/components/otp/test_config_flow.py b/tests/components/otp/test_config_flow.py index b0bd3e915bd..c9fdcdb0fef 100644 --- a/tests/components/otp/test_config_flow.py +++ b/tests/components/otp/test_config_flow.py @@ -42,7 +42,7 @@ async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: @pytest.mark.parametrize( ("exception", "error"), [ - (binascii.Error, "invalid_code"), + (binascii.Error, "invalid_token"), (IndexError, "unknown"), ], ) From 3a8b0c3573f1c72bf2c813ec44c8ed035203420b Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Fri, 21 Jun 2024 03:29:10 -0400 Subject: [PATCH 0867/1445] Bump zwave-js-server-python to 0.57.0 (#120047) --- homeassistant/components/zwave_js/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zwave_js/manifest.json b/homeassistant/components/zwave_js/manifest.json index ee19f8c746d..f394537803a 100644 --- a/homeassistant/components/zwave_js/manifest.json +++ b/homeassistant/components/zwave_js/manifest.json @@ -9,7 +9,7 @@ "iot_class": "local_push", "loggers": ["zwave_js_server"], "quality_scale": "platinum", - "requirements": ["pyserial==3.5", "zwave-js-server-python==0.56.0"], + "requirements": ["pyserial==3.5", "zwave-js-server-python==0.57.0"], "usb": [ { "vid": "0658", diff --git a/requirements_all.txt b/requirements_all.txt index 65568ed5906..e7a81366893 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2996,7 +2996,7 @@ zigpy==0.64.1 zm-py==0.5.4 # homeassistant.components.zwave_js -zwave-js-server-python==0.56.0 +zwave-js-server-python==0.57.0 # homeassistant.components.zwave_me zwave-me-ws==0.4.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8b63f1eb66d..bf84a2eb3b0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2340,7 +2340,7 @@ zigpy-znp==0.12.1 zigpy==0.64.1 # homeassistant.components.zwave_js -zwave-js-server-python==0.56.0 +zwave-js-server-python==0.57.0 # homeassistant.components.zwave_me zwave-me-ws==0.4.3 From e5846fdffd907c31aacd9500914c03491ed227f6 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Fri, 21 Jun 2024 10:16:36 +0200 Subject: [PATCH 0868/1445] Update pydantic to 1.10.17 (#119430) --- homeassistant/components/sfr_box/diagnostics.py | 16 ++++------------ homeassistant/components/xbox/media_source.py | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_test.txt | 2 +- script/gen_requirements_all.py | 2 +- tests/components/lametric/conftest.py | 2 +- 6 files changed, 9 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/sfr_box/diagnostics.py b/homeassistant/components/sfr_box/diagnostics.py index c0c964cd153..b5aca834af5 100644 --- a/homeassistant/components/sfr_box/diagnostics.py +++ b/homeassistant/components/sfr_box/diagnostics.py @@ -28,27 +28,19 @@ async def async_get_config_entry_diagnostics( }, "data": { "dsl": async_redact_data( - dataclasses.asdict( - await data.system.box.dsl_get_info() # type:ignore [call-overload] - ), + dataclasses.asdict(await data.system.box.dsl_get_info()), TO_REDACT, ), "ftth": async_redact_data( - dataclasses.asdict( - await data.system.box.ftth_get_info() # type:ignore [call-overload] - ), + dataclasses.asdict(await data.system.box.ftth_get_info()), TO_REDACT, ), "system": async_redact_data( - dataclasses.asdict( - await data.system.box.system_get_info() # type:ignore [call-overload] - ), + dataclasses.asdict(await data.system.box.system_get_info()), TO_REDACT, ), "wan": async_redact_data( - dataclasses.asdict( - await data.system.box.wan_get_info() # type:ignore [call-overload] - ), + dataclasses.asdict(await data.system.box.wan_get_info()), TO_REDACT, ), }, diff --git a/homeassistant/components/xbox/media_source.py b/homeassistant/components/xbox/media_source.py index af1f1e00e1f..a63f3b2027b 100644 --- a/homeassistant/components/xbox/media_source.py +++ b/homeassistant/components/xbox/media_source.py @@ -5,7 +5,7 @@ from __future__ import annotations from contextlib import suppress from dataclasses import dataclass -from pydantic.error_wrappers import ValidationError +from pydantic import ValidationError from xbox.webapi.api.client import XboxLiveClient from xbox.webapi.api.provider.catalog.models import FieldsTemplate, Image from xbox.webapi.api.provider.gameclips.models import GameclipsResponse diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index fb0517d9298..261e784c9dc 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -133,7 +133,7 @@ backoff>=2.0 # Required to avoid breaking (#101042). # v2 has breaking changes (#99218). -pydantic==1.10.15 +pydantic==1.10.17 # Breaks asyncio # https://github.com/pubnub/python/issues/130 diff --git a/requirements_test.txt b/requirements_test.txt index 8ba327285a0..47c3a834e01 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -13,7 +13,7 @@ freezegun==1.5.0 mock-open==1.4.0 mypy-dev==1.11.0a6 pre-commit==3.7.1 -pydantic==1.10.15 +pydantic==1.10.17 pylint==3.2.2 pylint-per-file-ignores==1.3.2 pipdeptree==2.19.0 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index a12decd5b2c..50bcd9968cd 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -155,7 +155,7 @@ backoff>=2.0 # Required to avoid breaking (#101042). # v2 has breaking changes (#99218). -pydantic==1.10.15 +pydantic==1.10.17 # Breaks asyncio # https://github.com/pubnub/python/issues/130 diff --git a/tests/components/lametric/conftest.py b/tests/components/lametric/conftest.py index 8202caa3b94..dd3885b78d9 100644 --- a/tests/components/lametric/conftest.py +++ b/tests/components/lametric/conftest.py @@ -5,7 +5,7 @@ from __future__ import annotations from unittest.mock import AsyncMock, MagicMock, patch from demetriek import CloudDevice, Device -from pydantic import parse_raw_as +from pydantic import parse_raw_as # pylint: disable=no-name-in-module import pytest from typing_extensions import Generator From 53d3475b1dcb7452aeb1097abf0802586a97b1b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Fri, 21 Jun 2024 10:28:11 +0200 Subject: [PATCH 0869/1445] Update aioairzone to v0.7.7 (#120067) --- homeassistant/components/airzone/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/airzone/manifest.json b/homeassistant/components/airzone/manifest.json index a14215fea6b..889170e31d7 100644 --- a/homeassistant/components/airzone/manifest.json +++ b/homeassistant/components/airzone/manifest.json @@ -11,5 +11,5 @@ "documentation": "https://www.home-assistant.io/integrations/airzone", "iot_class": "local_polling", "loggers": ["aioairzone"], - "requirements": ["aioairzone==0.7.6"] + "requirements": ["aioairzone==0.7.7"] } diff --git a/requirements_all.txt b/requirements_all.txt index e7a81366893..9079820c315 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -185,7 +185,7 @@ aioairq==0.3.2 aioairzone-cloud==0.5.2 # homeassistant.components.airzone -aioairzone==0.7.6 +aioairzone==0.7.7 # homeassistant.components.ambient_network # homeassistant.components.ambient_station diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bf84a2eb3b0..ae641375169 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -164,7 +164,7 @@ aioairq==0.3.2 aioairzone-cloud==0.5.2 # homeassistant.components.airzone -aioairzone==0.7.6 +aioairzone==0.7.7 # homeassistant.components.ambient_network # homeassistant.components.ambient_station From 4515eedea90a35ccdcb90caae62661a4ae365e82 Mon Sep 17 00:00:00 2001 From: "Mr. Bubbles" Date: Fri, 21 Jun 2024 10:28:52 +0200 Subject: [PATCH 0870/1445] Add unique_id to One-Time Password (OTP) (#120050) --- homeassistant/components/otp/sensor.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/otp/sensor.py b/homeassistant/components/otp/sensor.py index e612b03f66c..0c87afb86b7 100644 --- a/homeassistant/components/otp/sensor.py +++ b/homeassistant/components/otp/sensor.py @@ -60,7 +60,8 @@ async def async_setup_entry( """Set up the OTP sensor.""" async_add_entities( - [TOTPSensor(entry.data[CONF_NAME], entry.data[CONF_TOKEN])], True + [TOTPSensor(entry.data[CONF_NAME], entry.data[CONF_TOKEN], entry.entry_id)], + True, ) @@ -73,9 +74,10 @@ class TOTPSensor(SensorEntity): _attr_native_value: StateType = None _next_expiration: float | None = None - def __init__(self, name: str, token: str) -> None: + def __init__(self, name: str, token: str, entry_id: str) -> None: """Initialize the sensor.""" self._attr_name = name + self._attr_unique_id = entry_id self._otp = pyotp.TOTP(token) async def async_added_to_hass(self) -> None: From 7375764301955100006d202859776d87df304f88 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 21 Jun 2024 03:31:37 -0500 Subject: [PATCH 0871/1445] Bump anyio to 4.4.0 (#120061) --- homeassistant/package_constraints.txt | 2 +- script/gen_requirements_all.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 261e784c9dc..38f9d33575a 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -107,7 +107,7 @@ regex==2021.8.28 # these requirements are quite loose. As the entire stack has some outstanding issues, and # even newer versions seem to introduce new issues, it's useful for us to pin all these # requirements so we can directly link HA versions to these library versions. -anyio==4.3.0 +anyio==4.4.0 h11==0.14.0 httpcore==1.0.5 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 50bcd9968cd..57b4a2e1855 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -129,7 +129,7 @@ regex==2021.8.28 # these requirements are quite loose. As the entire stack has some outstanding issues, and # even newer versions seem to introduce new issues, it's useful for us to pin all these # requirements so we can directly link HA versions to these library versions. -anyio==4.3.0 +anyio==4.4.0 h11==0.14.0 httpcore==1.0.5 From f30b20b4df1875b81a9800b0bb8005cdcd7a12ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Fri, 21 Jun 2024 10:34:39 +0200 Subject: [PATCH 0872/1445] Update AEMET-OpenData to v0.5.2 (#120065) --- homeassistant/components/aemet/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/aemet/manifest.json b/homeassistant/components/aemet/manifest.json index b8a19bcd27a..8a22385f82b 100644 --- a/homeassistant/components/aemet/manifest.json +++ b/homeassistant/components/aemet/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/aemet", "iot_class": "cloud_polling", "loggers": ["aemet_opendata"], - "requirements": ["AEMET-OpenData==0.5.1"] + "requirements": ["AEMET-OpenData==0.5.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 9079820c315..848972ae01a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -4,7 +4,7 @@ -r requirements.txt # homeassistant.components.aemet -AEMET-OpenData==0.5.1 +AEMET-OpenData==0.5.2 # homeassistant.components.honeywell AIOSomecomfort==0.0.25 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ae641375169..f44b973749c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -4,7 +4,7 @@ -r requirements_test.txt # homeassistant.components.aemet -AEMET-OpenData==0.5.1 +AEMET-OpenData==0.5.2 # homeassistant.components.honeywell AIOSomecomfort==0.0.25 From 6fb5a12ef16ff801ec7eb5f722f1e73e29e72b9b Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 21 Jun 2024 10:36:52 +0200 Subject: [PATCH 0873/1445] Make UniFi services handle unloaded config entry (#120028) --- homeassistant/components/unifi/services.py | 15 +++++--- tests/components/unifi/test_services.py | 41 ++++++++++++++++++++++ 2 files changed, 51 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/unifi/services.py b/homeassistant/components/unifi/services.py index 5dcc0e9719c..ce726a0f5d0 100644 --- a/homeassistant/components/unifi/services.py +++ b/homeassistant/components/unifi/services.py @@ -6,6 +6,7 @@ from typing import Any from aiounifi.models.client import ClientReconnectRequest, ClientRemoveRequest import voluptuous as vol +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ATTR_DEVICE_ID from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.helpers import device_registry as dr @@ -66,9 +67,9 @@ async def async_reconnect_client(hass: HomeAssistant, data: Mapping[str, Any]) - if mac == "": return - for entry in hass.config_entries.async_entries(UNIFI_DOMAIN): - if ( - (hub := entry.runtime_data) + for config_entry in hass.config_entries.async_entries(UNIFI_DOMAIN): + if config_entry.state is not ConfigEntryState.LOADED or ( + (hub := config_entry.runtime_data) and not hub.available or (client := hub.api.clients.get(mac)) is None or client.is_wired @@ -85,8 +86,12 @@ async def async_remove_clients(hass: HomeAssistant, data: Mapping[str, Any]) -> - Total time between first seen and last seen is less than 15 minutes. - Neither IP, hostname nor name is configured. """ - for entry in hass.config_entries.async_entries(UNIFI_DOMAIN): - if (hub := entry.runtime_data) and not hub.available: + for config_entry in hass.config_entries.async_entries(UNIFI_DOMAIN): + if ( + config_entry.state is not ConfigEntryState.LOADED + or (hub := config_entry.runtime_data) + and not hub.available + ): continue clients_to_remove = [] diff --git a/tests/components/unifi/test_services.py b/tests/components/unifi/test_services.py index a85d4494d4a..e3b03bc868d 100644 --- a/tests/components/unifi/test_services.py +++ b/tests/components/unifi/test_services.py @@ -270,3 +270,44 @@ async def test_remove_clients_no_call_on_empty_list( aioclient_mock.clear_requests() await hass.services.async_call(UNIFI_DOMAIN, SERVICE_REMOVE_CLIENTS, blocking=True) assert aioclient_mock.call_count == 0 + + +@pytest.mark.parametrize( + "clients_all_payload", + [ + [ + { + "first_seen": 100, + "last_seen": 500, + "mac": "00:00:00:00:00:01", + } + ] + ], +) +async def test_services_handle_unloaded_config_entry( + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, + device_registry: dr.DeviceRegistry, + config_entry_setup: ConfigEntry, + clients_all_payload, +) -> None: + """Verify no call is made if config entry is unloaded.""" + await hass.config_entries.async_unload(config_entry_setup.entry_id) + await hass.async_block_till_done() + + aioclient_mock.clear_requests() + + await hass.services.async_call(UNIFI_DOMAIN, SERVICE_REMOVE_CLIENTS, blocking=True) + assert aioclient_mock.call_count == 0 + + device_entry = device_registry.async_get_or_create( + config_entry_id=config_entry_setup.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, clients_all_payload[0]["mac"])}, + ) + await hass.services.async_call( + UNIFI_DOMAIN, + SERVICE_RECONNECT_CLIENT, + service_data={ATTR_DEVICE_ID: device_entry.id}, + blocking=True, + ) + assert aioclient_mock.call_count == 0 From 3b6189a43212dfa9d02bed2efc420ae729ba9161 Mon Sep 17 00:00:00 2001 From: Glenn Waters Date: Fri, 21 Jun 2024 04:37:51 -0400 Subject: [PATCH 0874/1445] Bump env-canada to 0.6.3 (#120035) Co-authored-by: J. Nick Koston --- homeassistant/components/environment_canada/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/environment_canada/manifest.json b/homeassistant/components/environment_canada/manifest.json index f29c8177dfd..a0bdd5d4919 100644 --- a/homeassistant/components/environment_canada/manifest.json +++ b/homeassistant/components/environment_canada/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/environment_canada", "iot_class": "cloud_polling", "loggers": ["env_canada"], - "requirements": ["env-canada==0.6.2"] + "requirements": ["env-canada==0.6.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index 848972ae01a..04dec29d798 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -813,7 +813,7 @@ enocean==0.50 enturclient==0.2.4 # homeassistant.components.environment_canada -env-canada==0.6.2 +env-canada==0.6.3 # homeassistant.components.season ephem==4.1.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f44b973749c..91d0fa2ff2d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -670,7 +670,7 @@ energyzero==2.1.0 enocean==0.50 # homeassistant.components.environment_canada -env-canada==0.6.2 +env-canada==0.6.3 # homeassistant.components.season ephem==4.1.5 From a6724db01bd1b1ce8f996fafdcb59b15ea7af41d Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 21 Jun 2024 10:42:25 +0200 Subject: [PATCH 0875/1445] Fix calculation in Refoss (#120069) --- homeassistant/components/refoss/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/refoss/sensor.py b/homeassistant/components/refoss/sensor.py index 3857b401d0d..9f5ee5d898a 100644 --- a/homeassistant/components/refoss/sensor.py +++ b/homeassistant/components/refoss/sensor.py @@ -50,10 +50,10 @@ SENSORS: dict[str, tuple[RefossSensorEntityDescription, ...]] = { translation_key="power", device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=UnitOfPower.KILO_WATT, - suggested_unit_of_measurement=UnitOfPower.WATT, + native_unit_of_measurement=UnitOfPower.WATT, suggested_display_precision=2, subkey="power", + fn=lambda x: x / 1000.0, ), RefossSensorEntityDescription( key="voltage", From aba5bb08ddb23197d84014637e1c5ccbd45f0a2c Mon Sep 17 00:00:00 2001 From: Thomas Kistler Date: Fri, 21 Jun 2024 01:51:46 -0700 Subject: [PATCH 0876/1445] Add Ambient Weather brand (#115898) --- homeassistant/brands/ambient_weather.json | 5 +++++ homeassistant/generated/integrations.json | 27 ++++++++++++++--------- 2 files changed, 21 insertions(+), 11 deletions(-) create mode 100644 homeassistant/brands/ambient_weather.json diff --git a/homeassistant/brands/ambient_weather.json b/homeassistant/brands/ambient_weather.json new file mode 100644 index 00000000000..157f2a5b7bc --- /dev/null +++ b/homeassistant/brands/ambient_weather.json @@ -0,0 +1,5 @@ +{ + "domain": "ambient_weather", + "name": "Ambient Weather", + "integrations": ["ambient_network", "ambient_station"] +} diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 4133de4d4a3..fb3e33d3289 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -244,17 +244,22 @@ "config_flow": true, "iot_class": "cloud_polling" }, - "ambient_network": { - "name": "Ambient Weather Network", - "integration_type": "service", - "config_flow": true, - "iot_class": "cloud_polling" - }, - "ambient_station": { - "name": "Ambient Weather Station", - "integration_type": "hub", - "config_flow": true, - "iot_class": "cloud_push" + "ambient_weather": { + "name": "Ambient Weather", + "integrations": { + "ambient_network": { + "integration_type": "service", + "config_flow": true, + "iot_class": "cloud_polling", + "name": "Ambient Weather Network" + }, + "ambient_station": { + "integration_type": "hub", + "config_flow": true, + "iot_class": "cloud_push", + "name": "Ambient Weather Station" + } + } }, "amcrest": { "name": "Amcrest", From 54d8ce5ca94f69541f73e74fcc8d00b584903f04 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 21 Jun 2024 11:00:07 +0200 Subject: [PATCH 0877/1445] Revert "Temporary pin CI to Python 3.12.3" (#119454) --- .github/workflows/builder.yml | 2 +- .github/workflows/ci.yaml | 4 ++-- .github/workflows/translations.yml | 2 +- .github/workflows/wheels.yml | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml index 304a077b808..92a13078ce1 100644 --- a/.github/workflows/builder.yml +++ b/.github/workflows/builder.yml @@ -10,7 +10,7 @@ on: env: BUILD_TYPE: core - DEFAULT_PYTHON: "3.12.3" + DEFAULT_PYTHON: "3.12" PIP_TIMEOUT: 60 UV_HTTP_TIMEOUT: 60 UV_SYSTEM_PYTHON: "true" diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index b5ae02c2627..53a0454c7c5 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -37,8 +37,8 @@ env: UV_CACHE_VERSION: 1 MYPY_CACHE_VERSION: 8 HA_SHORT_VERSION: "2024.7" - DEFAULT_PYTHON: "3.12.3" - ALL_PYTHON_VERSIONS: "['3.12.3']" + DEFAULT_PYTHON: "3.12" + ALL_PYTHON_VERSIONS: "['3.12']" # 10.3 is the oldest supported version # - 10.3.32 is the version currently shipped with Synology (as of 17 Feb 2022) # 10.6 is the current long-term-support diff --git a/.github/workflows/translations.yml b/.github/workflows/translations.yml index 69e1792f926..318a1898987 100644 --- a/.github/workflows/translations.yml +++ b/.github/workflows/translations.yml @@ -10,7 +10,7 @@ on: - "**strings.json" env: - DEFAULT_PYTHON: "3.12.3" + DEFAULT_PYTHON: "3.12" jobs: upload: diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index e1c2700cba9..f197a80b294 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -17,7 +17,7 @@ on: - "script/gen_requirements_all.py" env: - DEFAULT_PYTHON: "3.12.3" + DEFAULT_PYTHON: "3.12" concurrency: group: ${{ github.workflow }}-${{ github.ref_name}} From 53022df8a4bf0d91812d59a2b47c0bb8db272dab Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 21 Jun 2024 11:01:42 +0200 Subject: [PATCH 0878/1445] Add sensor tests for APSystems (#117512) --- homeassistant/components/apsystems/sensor.py | 1 + tests/components/apsystems/__init__.py | 11 + tests/components/apsystems/conftest.py | 58 ++- .../apsystems/snapshots/test_sensor.ambr | 460 ++++++++++++++++++ .../components/apsystems/test_config_flow.py | 18 +- tests/components/apsystems/test_sensor.py | 31 ++ 6 files changed, 560 insertions(+), 19 deletions(-) create mode 100644 tests/components/apsystems/snapshots/test_sensor.ambr create mode 100644 tests/components/apsystems/test_sensor.py diff --git a/homeassistant/components/apsystems/sensor.py b/homeassistant/components/apsystems/sensor.py index fdfe7d0f0b7..637def4e418 100644 --- a/homeassistant/components/apsystems/sensor.py +++ b/homeassistant/components/apsystems/sensor.py @@ -132,6 +132,7 @@ class ApSystemsSensorWithDescription( """Base sensor to be used with description.""" entity_description: ApsystemsLocalApiSensorDescription + _attr_has_entity_name = True def __init__( self, diff --git a/tests/components/apsystems/__init__.py b/tests/components/apsystems/__init__.py index 9c3c5990be0..ad86df667ba 100644 --- a/tests/components/apsystems/__init__.py +++ b/tests/components/apsystems/__init__.py @@ -1 +1,12 @@ """Tests for the APsystems Local API integration.""" + +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +async def setup_integration(hass: HomeAssistant, config_entry: MockConfigEntry) -> None: + """Fixture for setting up the component.""" + config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(config_entry.entry_id) diff --git a/tests/components/apsystems/conftest.py b/tests/components/apsystems/conftest.py index ab8b7db5a75..cd04346c070 100644 --- a/tests/components/apsystems/conftest.py +++ b/tests/components/apsystems/conftest.py @@ -1,10 +1,16 @@ """Common fixtures for the APsystems Local API tests.""" -from unittest.mock import AsyncMock, MagicMock, patch +from unittest.mock import AsyncMock, patch +from APsystemsEZ1 import ReturnDeviceInfo, ReturnOutputData import pytest from typing_extensions import Generator +from homeassistant.components.apsystems.const import DOMAIN +from homeassistant.const import CONF_IP_ADDRESS + +from tests.common import MockConfigEntry + @pytest.fixture def mock_setup_entry() -> Generator[AsyncMock]: @@ -17,13 +23,45 @@ def mock_setup_entry() -> Generator[AsyncMock]: @pytest.fixture -def mock_apsystems(): - """Override APsystemsEZ1M.get_device_info() to return MY_SERIAL_NUMBER as the serial number.""" - ret_data = MagicMock() - ret_data.deviceId = "MY_SERIAL_NUMBER" - with patch( - "homeassistant.components.apsystems.config_flow.APsystemsEZ1M", - return_value=AsyncMock(), - ) as mock_api: - mock_api.return_value.get_device_info.return_value = ret_data +def mock_apsystems() -> Generator[AsyncMock, None, None]: + """Mock APSystems lib.""" + with ( + patch( + "homeassistant.components.apsystems.APsystemsEZ1M", + autospec=True, + ) as mock_client, + patch( + "homeassistant.components.apsystems.config_flow.APsystemsEZ1M", + new=mock_client, + ), + ): + mock_api = mock_client.return_value + mock_api.get_device_info.return_value = ReturnDeviceInfo( + deviceId="MY_SERIAL_NUMBER", + devVer="1.0.0", + ssid="MY_SSID", + ipAddr="127.0.01", + minPower=0, + maxPower=1000, + ) + mock_api.get_output_data.return_value = ReturnOutputData( + p1=2.0, + e1=3.0, + te1=4.0, + p2=5.0, + e2=6.0, + te2=7.0, + ) yield mock_api + + +@pytest.fixture +def mock_config_entry() -> MockConfigEntry: + """Mock config entry.""" + return MockConfigEntry( + domain=DOMAIN, + data={ + CONF_IP_ADDRESS: "127.0.0.1", + }, + unique_id="MY_SERIAL_NUMBER", + ) diff --git a/tests/components/apsystems/snapshots/test_sensor.ambr b/tests/components/apsystems/snapshots/test_sensor.ambr new file mode 100644 index 00000000000..669e89fda17 --- /dev/null +++ b/tests/components/apsystems/snapshots/test_sensor.ambr @@ -0,0 +1,460 @@ +# serializer version: 1 +# name: test_all_entities[sensor.mock_title_lifetime_production_of_p1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.mock_title_lifetime_production_of_p1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Lifetime production of P1', + 'platform': 'apsystems', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_production_p1', + 'unique_id': 'MY_SERIAL_NUMBER_lifetime_production_p1', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.mock_title_lifetime_production_of_p1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Mock Title Lifetime production of P1', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.mock_title_lifetime_production_of_p1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '4.0', + }) +# --- +# name: test_all_entities[sensor.mock_title_lifetime_production_of_p2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.mock_title_lifetime_production_of_p2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Lifetime production of P2', + 'platform': 'apsystems', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_production_p2', + 'unique_id': 'MY_SERIAL_NUMBER_lifetime_production_p2', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.mock_title_lifetime_production_of_p2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Mock Title Lifetime production of P2', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.mock_title_lifetime_production_of_p2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '7.0', + }) +# --- +# name: test_all_entities[sensor.mock_title_power_of_p1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.mock_title_power_of_p1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Power of P1', + 'platform': 'apsystems', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'total_power_p1', + 'unique_id': 'MY_SERIAL_NUMBER_total_power_p1', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.mock_title_power_of_p1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Mock Title Power of P1', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.mock_title_power_of_p1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2.0', + }) +# --- +# name: test_all_entities[sensor.mock_title_power_of_p2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.mock_title_power_of_p2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Power of P2', + 'platform': 'apsystems', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'total_power_p2', + 'unique_id': 'MY_SERIAL_NUMBER_total_power_p2', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.mock_title_power_of_p2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Mock Title Power of P2', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.mock_title_power_of_p2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '5.0', + }) +# --- +# name: test_all_entities[sensor.mock_title_production_of_today-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.mock_title_production_of_today', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Production of today', + 'platform': 'apsystems', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'today_production', + 'unique_id': 'MY_SERIAL_NUMBER_today_production', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.mock_title_production_of_today-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Mock Title Production of today', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.mock_title_production_of_today', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '9.0', + }) +# --- +# name: test_all_entities[sensor.mock_title_production_of_today_from_p1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.mock_title_production_of_today_from_p1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Production of today from P1', + 'platform': 'apsystems', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'today_production_p1', + 'unique_id': 'MY_SERIAL_NUMBER_today_production_p1', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.mock_title_production_of_today_from_p1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Mock Title Production of today from P1', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.mock_title_production_of_today_from_p1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '3.0', + }) +# --- +# name: test_all_entities[sensor.mock_title_production_of_today_from_p2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.mock_title_production_of_today_from_p2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Production of today from P2', + 'platform': 'apsystems', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'today_production_p2', + 'unique_id': 'MY_SERIAL_NUMBER_today_production_p2', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.mock_title_production_of_today_from_p2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Mock Title Production of today from P2', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.mock_title_production_of_today_from_p2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '6.0', + }) +# --- +# name: test_all_entities[sensor.mock_title_total_lifetime_production-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.mock_title_total_lifetime_production', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Total lifetime production', + 'platform': 'apsystems', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_production', + 'unique_id': 'MY_SERIAL_NUMBER_lifetime_production', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.mock_title_total_lifetime_production-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Mock Title Total lifetime production', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.mock_title_total_lifetime_production', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '11.0', + }) +# --- +# name: test_all_entities[sensor.mock_title_total_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.mock_title_total_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Total power', + 'platform': 'apsystems', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'total_power', + 'unique_id': 'MY_SERIAL_NUMBER_total_power', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.mock_title_total_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Mock Title Total power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.mock_title_total_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '7.0', + }) +# --- diff --git a/tests/components/apsystems/test_config_flow.py b/tests/components/apsystems/test_config_flow.py index f916240e734..e3fcdf67dcc 100644 --- a/tests/components/apsystems/test_config_flow.py +++ b/tests/components/apsystems/test_config_flow.py @@ -12,7 +12,7 @@ from tests.common import MockConfigEntry async def test_form_create_success( - hass: HomeAssistant, mock_setup_entry, mock_apsystems + hass: HomeAssistant, mock_setup_entry: AsyncMock, mock_apsystems: AsyncMock ) -> None: """Test we handle creatinw with success.""" result = await hass.config_entries.flow.async_init( @@ -28,11 +28,11 @@ async def test_form_create_success( async def test_form_cannot_connect_and_recover( - hass: HomeAssistant, mock_apsystems: AsyncMock, mock_setup_entry + hass: HomeAssistant, mock_apsystems: AsyncMock, mock_setup_entry: AsyncMock ) -> None: """Test we handle cannot connect error.""" - mock_apsystems.return_value.get_device_info.side_effect = TimeoutError + mock_apsystems.get_device_info.side_effect = TimeoutError result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, @@ -44,7 +44,7 @@ async def test_form_cannot_connect_and_recover( assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} - mock_apsystems.return_value.get_device_info.side_effect = None + mock_apsystems.get_device_info.side_effect = None result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -58,13 +58,13 @@ async def test_form_cannot_connect_and_recover( async def test_form_unique_id_already_configured( - hass: HomeAssistant, mock_setup_entry, mock_apsystems + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_apsystems: AsyncMock, + mock_config_entry: MockConfigEntry, ) -> None: """Test we handle cannot connect error.""" - entry = MockConfigEntry( - domain=DOMAIN, data={CONF_IP_ADDRESS: "127.0.0.2"}, unique_id="MY_SERIAL_NUMBER" - ) - entry.add_to_hass(hass) + mock_config_entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( DOMAIN, diff --git a/tests/components/apsystems/test_sensor.py b/tests/components/apsystems/test_sensor.py new file mode 100644 index 00000000000..810ad3e7bdf --- /dev/null +++ b/tests/components/apsystems/test_sensor.py @@ -0,0 +1,31 @@ +"""Test the APSystem sensor module.""" + +from unittest.mock import AsyncMock, patch + +from syrupy import SnapshotAssertion + +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import setup_integration + +from tests.common import MockConfigEntry, snapshot_platform + + +async def test_all_entities( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + mock_apsystems: AsyncMock, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, +) -> None: + """Test all entities.""" + with patch( + "homeassistant.components.apsystems.PLATFORMS", + [Platform.SENSOR], + ): + await setup_integration(hass, mock_config_entry) + await snapshot_platform( + hass, entity_registry, snapshot, mock_config_entry.entry_id + ) From dc6c1f4e87bd8bd8098f61326f3eb67b0e0cb9e8 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 21 Jun 2024 11:04:15 +0200 Subject: [PATCH 0879/1445] Add MockPlatform type hints in tests (#120012) * Add MockPlatform type hints in tests * Remove useless code * Improve * Revert "Improve" This reverts commit 9ad04f925514af46a7cd64f94a518fc093da825c. --- tests/common.py | 28 +++++++++++++------ .../alarm_control_panel/conftest.py | 1 - tests/components/lock/conftest.py | 1 - tests/helpers/test_discovery.py | 4 ++- tests/helpers/test_entity_component.py | 24 +++++++++++----- tests/helpers/test_entity_platform.py | 2 +- tests/test_setup.py | 4 +-- 7 files changed, 43 insertions(+), 21 deletions(-) diff --git a/tests/common.py b/tests/common.py index 5050d67b0cb..851f91cfc3e 100644 --- a/tests/common.py +++ b/tests/common.py @@ -3,7 +3,7 @@ from __future__ import annotations import asyncio -from collections.abc import Mapping, Sequence +from collections.abc import Callable, Coroutine, Mapping, Sequence from contextlib import asynccontextmanager, contextmanager from datetime import UTC, datetime, timedelta from enum import Enum @@ -858,13 +858,25 @@ class MockPlatform: def __init__( self, - setup_platform=None, - dependencies=None, - platform_schema=None, - async_setup_platform=None, - async_setup_entry=None, - scan_interval=None, - ): + *, + setup_platform: Callable[ + [HomeAssistant, ConfigType, AddEntitiesCallback, DiscoveryInfoType | None], + None, + ] + | None = None, + dependencies: list[str] | None = None, + platform_schema: vol.Schema | None = None, + async_setup_platform: Callable[ + [HomeAssistant, ConfigType, AddEntitiesCallback, DiscoveryInfoType | None], + Coroutine[Any, Any, None], + ] + | None = None, + async_setup_entry: Callable[ + [HomeAssistant, ConfigEntry, AddEntitiesCallback], Coroutine[Any, Any, None] + ] + | None = None, + scan_interval: timedelta | None = None, + ) -> None: """Initialize the platform.""" self.DEPENDENCIES = dependencies or [] diff --git a/tests/components/alarm_control_panel/conftest.py b/tests/components/alarm_control_panel/conftest.py index 34a4b483e3b..620b74dd80e 100644 --- a/tests/components/alarm_control_panel/conftest.py +++ b/tests/components/alarm_control_panel/conftest.py @@ -160,7 +160,6 @@ async def setup_lock_platform_test_entity( ) return True - MockPlatform(hass, f"{TEST_DOMAIN}.config_flow") mock_integration( hass, MockModule( diff --git a/tests/components/lock/conftest.py b/tests/components/lock/conftest.py index e8291badd0b..f1715687339 100644 --- a/tests/components/lock/conftest.py +++ b/tests/components/lock/conftest.py @@ -103,7 +103,6 @@ async def setup_lock_platform_test_entity( ) return True - MockPlatform(hass, f"{TEST_DOMAIN}.config_flow") mock_integration( hass, MockModule( diff --git a/tests/helpers/test_discovery.py b/tests/helpers/test_discovery.py index dc4b2951b2f..100b50e2749 100644 --- a/tests/helpers/test_discovery.py +++ b/tests/helpers/test_discovery.py @@ -132,7 +132,9 @@ async def test_circular_import(hass: HomeAssistant) -> None: # dependencies are only set in component level # since we are using manifest to hold them mock_integration(hass, MockModule("test_circular", dependencies=["test_component"])) - mock_platform(hass, "test_circular.switch", MockPlatform(setup_platform)) + mock_platform( + hass, "test_circular.switch", MockPlatform(setup_platform=setup_platform) + ) await setup.async_setup_component( hass, diff --git a/tests/helpers/test_entity_component.py b/tests/helpers/test_entity_component.py index 39cb48eed0e..32ce740edb2 100644 --- a/tests/helpers/test_entity_component.py +++ b/tests/helpers/test_entity_component.py @@ -52,7 +52,7 @@ async def test_setup_loads_platforms(hass: HomeAssistant) -> None: mock_integration(hass, MockModule("test_component", setup=component_setup)) # mock the dependencies mock_integration(hass, MockModule("mod2", dependencies=["test_component"])) - mock_platform(hass, "mod2.test_domain", MockPlatform(platform_setup)) + mock_platform(hass, "mod2.test_domain", MockPlatform(setup_platform=platform_setup)) component = EntityComponent(_LOGGER, DOMAIN, hass) @@ -71,8 +71,12 @@ async def test_setup_recovers_when_setup_raises(hass: HomeAssistant) -> None: platform1_setup = Mock(side_effect=Exception("Broken")) platform2_setup = Mock(return_value=None) - mock_platform(hass, "mod1.test_domain", MockPlatform(platform1_setup)) - mock_platform(hass, "mod2.test_domain", MockPlatform(platform2_setup)) + mock_platform( + hass, "mod1.test_domain", MockPlatform(setup_platform=platform1_setup) + ) + mock_platform( + hass, "mod2.test_domain", MockPlatform(setup_platform=platform2_setup) + ) component = EntityComponent(_LOGGER, DOMAIN, hass) @@ -128,7 +132,9 @@ async def test_set_scan_interval_via_config(hass: HomeAssistant) -> None: """Test the platform setup.""" add_entities([MockEntity(should_poll=True)]) - mock_platform(hass, "platform.test_domain", MockPlatform(platform_setup)) + mock_platform( + hass, "platform.test_domain", MockPlatform(setup_platform=platform_setup) + ) component = EntityComponent(_LOGGER, DOMAIN, hass) @@ -154,7 +160,7 @@ async def test_set_entity_namespace_via_config(hass: HomeAssistant) -> None: """Test the platform setup.""" add_entities([MockEntity(name="beer"), MockEntity(name=None)]) - platform = MockPlatform(platform_setup) + platform = MockPlatform(setup_platform=platform_setup) mock_platform(hass, "platform.test_domain", platform) @@ -204,7 +210,9 @@ async def test_platform_not_ready(hass: HomeAssistant) -> None: """Test that we retry when platform not ready.""" platform1_setup = Mock(side_effect=[PlatformNotReady, PlatformNotReady, None]) mock_integration(hass, MockModule("mod1")) - mock_platform(hass, "mod1.test_domain", MockPlatform(platform1_setup)) + mock_platform( + hass, "mod1.test_domain", MockPlatform(setup_platform=platform1_setup) + ) component = EntityComponent(_LOGGER, DOMAIN, hass) @@ -678,7 +686,9 @@ async def test_platforms_shutdown_on_stop(hass: HomeAssistant) -> None: """Test that we shutdown platforms on stop.""" platform1_setup = Mock(side_effect=[PlatformNotReady, PlatformNotReady, None]) mock_integration(hass, MockModule("mod1")) - mock_platform(hass, "mod1.test_domain", MockPlatform(platform1_setup)) + mock_platform( + hass, "mod1.test_domain", MockPlatform(setup_platform=platform1_setup) + ) component = EntityComponent(_LOGGER, DOMAIN, hass) diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py index 56ddcd9a6c9..68024bc936f 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -228,7 +228,7 @@ async def test_set_scan_interval_via_platform(hass: HomeAssistant) -> None: """Test the platform setup.""" add_entities([MockEntity(should_poll=True)]) - platform = MockPlatform(platform_setup) + platform = MockPlatform(setup_platform=platform_setup) platform.SCAN_INTERVAL = timedelta(seconds=30) mock_platform(hass, "platform.test_domain", platform) diff --git a/tests/test_setup.py b/tests/test_setup.py index 92367b84ab7..4ff0f465e21 100644 --- a/tests/test_setup.py +++ b/tests/test_setup.py @@ -163,7 +163,7 @@ async def test_validate_platform_config_2( mock_platform( hass, "whatever.platform_conf", - MockPlatform("whatever", platform_schema=platform_schema), + MockPlatform(platform_schema=platform_schema), ) with assert_setup_component(1): @@ -192,7 +192,7 @@ async def test_validate_platform_config_3( mock_platform( hass, "whatever.platform_conf", - MockPlatform("whatever", platform_schema=platform_schema), + MockPlatform(platform_schema=platform_schema), ) with assert_setup_component(1): From 5138c3de0afba66416933c2bd6dbba3c573ac348 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 21 Jun 2024 11:04:55 +0200 Subject: [PATCH 0880/1445] Add Mealie integration (#119678) --- CODEOWNERS | 2 + homeassistant/components/mealie/__init__.py | 33 + homeassistant/components/mealie/calendar.py | 81 +++ .../components/mealie/config_flow.py | 55 ++ homeassistant/components/mealie/const.py | 7 + .../components/mealie/coordinator.py | 65 ++ homeassistant/components/mealie/entity.py | 21 + homeassistant/components/mealie/manifest.json | 10 + homeassistant/components/mealie/strings.json | 36 ++ homeassistant/generated/config_flows.py | 1 + homeassistant/generated/integrations.json | 6 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/mealie/__init__.py | 13 + tests/components/mealie/conftest.py | 58 ++ .../mealie/fixtures/get_mealplan_today.json | 253 ++++++++ .../mealie/fixtures/get_mealplans.json | 612 ++++++++++++++++++ .../mealie/snapshots/test_calendar.ambr | 359 ++++++++++ .../mealie/snapshots/test_init.ambr | 31 + tests/components/mealie/test_calendar.py | 69 ++ tests/components/mealie/test_config_flow.py | 107 +++ tests/components/mealie/test_init.py | 70 ++ 22 files changed, 1895 insertions(+) create mode 100644 homeassistant/components/mealie/__init__.py create mode 100644 homeassistant/components/mealie/calendar.py create mode 100644 homeassistant/components/mealie/config_flow.py create mode 100644 homeassistant/components/mealie/const.py create mode 100644 homeassistant/components/mealie/coordinator.py create mode 100644 homeassistant/components/mealie/entity.py create mode 100644 homeassistant/components/mealie/manifest.json create mode 100644 homeassistant/components/mealie/strings.json create mode 100644 tests/components/mealie/__init__.py create mode 100644 tests/components/mealie/conftest.py create mode 100644 tests/components/mealie/fixtures/get_mealplan_today.json create mode 100644 tests/components/mealie/fixtures/get_mealplans.json create mode 100644 tests/components/mealie/snapshots/test_calendar.ambr create mode 100644 tests/components/mealie/snapshots/test_init.ambr create mode 100644 tests/components/mealie/test_calendar.py create mode 100644 tests/components/mealie/test_config_flow.py create mode 100644 tests/components/mealie/test_init.py diff --git a/CODEOWNERS b/CODEOWNERS index aaed793dd41..aa33cdfe38f 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -826,6 +826,8 @@ build.json @home-assistant/supervisor /tests/components/matrix/ @PaarthShah /homeassistant/components/matter/ @home-assistant/matter /tests/components/matter/ @home-assistant/matter +/homeassistant/components/mealie/ @joostlek +/tests/components/mealie/ @joostlek /homeassistant/components/meater/ @Sotolotl @emontnemery /tests/components/meater/ @Sotolotl @emontnemery /homeassistant/components/medcom_ble/ @elafargue diff --git a/homeassistant/components/mealie/__init__.py b/homeassistant/components/mealie/__init__.py new file mode 100644 index 00000000000..c316cf04545 --- /dev/null +++ b/homeassistant/components/mealie/__init__.py @@ -0,0 +1,33 @@ +"""The Mealie integration.""" + +from __future__ import annotations + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant + +from .coordinator import MealieCoordinator + +PLATFORMS: list[Platform] = [Platform.CALENDAR] + + +type MealieConfigEntry = ConfigEntry[MealieCoordinator] + + +async def async_setup_entry(hass: HomeAssistant, entry: MealieConfigEntry) -> bool: + """Set up Mealie from a config entry.""" + + coordinator = MealieCoordinator(hass) + + await coordinator.async_config_entry_first_refresh() + + entry.runtime_data = coordinator + + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: MealieConfigEntry) -> bool: + """Unload a config entry.""" + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/mealie/calendar.py b/homeassistant/components/mealie/calendar.py new file mode 100644 index 00000000000..08e90ebf5ea --- /dev/null +++ b/homeassistant/components/mealie/calendar.py @@ -0,0 +1,81 @@ +"""Calendar platform for Mealie.""" + +from __future__ import annotations + +from datetime import datetime + +from aiomealie import Mealplan, MealplanEntryType + +from homeassistant.components.calendar import CalendarEntity, CalendarEvent +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import MealieConfigEntry, MealieCoordinator +from .entity import MealieEntity + + +async def async_setup_entry( + hass: HomeAssistant, + entry: MealieConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the calendar platform for entity.""" + coordinator = entry.runtime_data + + async_add_entities( + MealieMealplanCalendarEntity(coordinator, entry_type) + for entry_type in MealplanEntryType + ) + + +def _get_event_from_mealplan(mealplan: Mealplan) -> CalendarEvent: + """Create a CalendarEvent from a Mealplan.""" + description: str | None = None + name = "No recipe" + if mealplan.recipe: + name = mealplan.recipe.name + description = mealplan.recipe.description + return CalendarEvent( + start=mealplan.mealplan_date, + end=mealplan.mealplan_date, + summary=name, + description=description, + ) + + +class MealieMealplanCalendarEntity(MealieEntity, CalendarEntity): + """A calendar entity.""" + + def __init__( + self, coordinator: MealieCoordinator, entry_type: MealplanEntryType + ) -> None: + """Create the Calendar entity.""" + super().__init__(coordinator) + self._entry_type = entry_type + self._attr_translation_key = entry_type.name.lower() + self._attr_unique_id = ( + f"{self.coordinator.config_entry.entry_id}_{entry_type.name.lower()}" + ) + + @property + def event(self) -> CalendarEvent | None: + """Return the next upcoming event.""" + mealplans = self.coordinator.data[self._entry_type] + if not mealplans: + return None + return _get_event_from_mealplan(mealplans[0]) + + async def async_get_events( + self, hass: HomeAssistant, start_date: datetime, end_date: datetime + ) -> list[CalendarEvent]: + """Get all events in a specific time frame.""" + mealplans = ( + await self.coordinator.client.get_mealplans( + start_date.date(), end_date.date() + ) + ).items + return [ + _get_event_from_mealplan(mealplan) + for mealplan in mealplans + if mealplan.entry_type is self._entry_type + ] diff --git a/homeassistant/components/mealie/config_flow.py b/homeassistant/components/mealie/config_flow.py new file mode 100644 index 00000000000..b25cade148a --- /dev/null +++ b/homeassistant/components/mealie/config_flow.py @@ -0,0 +1,55 @@ +"""Config flow for Mealie.""" + +from typing import Any + +from aiomealie import MealieAuthenticationError, MealieClient, MealieConnectionError +import voluptuous as vol + +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult +from homeassistant.const import CONF_API_TOKEN, CONF_HOST +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .const import DOMAIN, LOGGER + +SCHEMA = vol.Schema( + { + vol.Required(CONF_HOST): str, + vol.Required(CONF_API_TOKEN): str, + } +) + + +class MealieConfigFlow(ConfigFlow, domain=DOMAIN): + """Mealie config flow.""" + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle a flow initialized by the user.""" + errors: dict[str, str] = {} + if user_input: + self._async_abort_entries_match({CONF_HOST: user_input[CONF_HOST]}) + client = MealieClient( + user_input[CONF_HOST], + token=user_input[CONF_API_TOKEN], + session=async_get_clientsession(self.hass), + ) + try: + await client.get_mealplan_today() + except MealieConnectionError: + errors["base"] = "cannot_connect" + except MealieAuthenticationError: + errors["base"] = "invalid_auth" + except Exception: # noqa: BLE001 + LOGGER.exception("Unexpected error") + errors["base"] = "unknown" + else: + return self.async_create_entry( + title="Mealie", + data=user_input, + ) + return self.async_show_form( + step_id="user", + data_schema=SCHEMA, + errors=errors, + ) diff --git a/homeassistant/components/mealie/const.py b/homeassistant/components/mealie/const.py new file mode 100644 index 00000000000..28c64d3c0f0 --- /dev/null +++ b/homeassistant/components/mealie/const.py @@ -0,0 +1,7 @@ +"""Constants for the Mealie integration.""" + +import logging + +DOMAIN = "mealie" + +LOGGER = logging.getLogger(__package__) diff --git a/homeassistant/components/mealie/coordinator.py b/homeassistant/components/mealie/coordinator.py new file mode 100644 index 00000000000..0c32544d4d7 --- /dev/null +++ b/homeassistant/components/mealie/coordinator.py @@ -0,0 +1,65 @@ +"""Define an object to manage fetching Mealie data.""" + +from __future__ import annotations + +from datetime import timedelta +from typing import TYPE_CHECKING + +from aiomealie import ( + MealieAuthenticationError, + MealieClient, + MealieConnectionError, + Mealplan, + MealplanEntryType, +) + +from homeassistant.const import CONF_API_TOKEN, CONF_HOST +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryError +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed +import homeassistant.util.dt as dt_util + +from .const import LOGGER + +if TYPE_CHECKING: + from . import MealieConfigEntry + +WEEK = timedelta(days=7) + + +class MealieCoordinator(DataUpdateCoordinator[dict[MealplanEntryType, list[Mealplan]]]): + """Class to manage fetching Mealie data.""" + + config_entry: MealieConfigEntry + + def __init__(self, hass: HomeAssistant) -> None: + """Initialize coordinator.""" + super().__init__( + hass, logger=LOGGER, name="Mealie", update_interval=timedelta(hours=1) + ) + self.client = MealieClient( + self.config_entry.data[CONF_HOST], + token=self.config_entry.data[CONF_API_TOKEN], + session=async_get_clientsession(hass), + ) + + async def _async_update_data(self) -> dict[MealplanEntryType, list[Mealplan]]: + next_week = dt_util.now() + WEEK + try: + data = ( + await self.client.get_mealplans(dt_util.now().date(), next_week.date()) + ).items + except MealieAuthenticationError as error: + raise ConfigEntryError("Authentication failed") from error + except MealieConnectionError as error: + raise UpdateFailed(error) from error + res: dict[MealplanEntryType, list[Mealplan]] = { + MealplanEntryType.BREAKFAST: [], + MealplanEntryType.LUNCH: [], + MealplanEntryType.DINNER: [], + MealplanEntryType.SIDE: [], + } + for meal in data: + res[meal.entry_type].append(meal) + return res diff --git a/homeassistant/components/mealie/entity.py b/homeassistant/components/mealie/entity.py new file mode 100644 index 00000000000..5e339c1d4b8 --- /dev/null +++ b/homeassistant/components/mealie/entity.py @@ -0,0 +1,21 @@ +"""Base class for Mealie entities.""" + +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN +from .coordinator import MealieCoordinator + + +class MealieEntity(CoordinatorEntity[MealieCoordinator]): + """Defines a base Mealie entity.""" + + _attr_has_entity_name = True + + def __init__(self, coordinator: MealieCoordinator) -> None: + """Initialize Mealie entity.""" + super().__init__(coordinator) + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, coordinator.config_entry.entry_id)}, + entry_type=DeviceEntryType.SERVICE, + ) diff --git a/homeassistant/components/mealie/manifest.json b/homeassistant/components/mealie/manifest.json new file mode 100644 index 00000000000..3a2a9b58204 --- /dev/null +++ b/homeassistant/components/mealie/manifest.json @@ -0,0 +1,10 @@ +{ + "domain": "mealie", + "name": "Mealie", + "codeowners": ["@joostlek"], + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/mealie", + "integration_type": "service", + "iot_class": "local_polling", + "requirements": ["aiomealie==0.3.1"] +} diff --git a/homeassistant/components/mealie/strings.json b/homeassistant/components/mealie/strings.json new file mode 100644 index 00000000000..0d67bb89759 --- /dev/null +++ b/homeassistant/components/mealie/strings.json @@ -0,0 +1,36 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "[%key:common::config_flow::data::host%]", + "api_token": "[%key:common::config_flow::data::api_token%]" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" + } + }, + "entity": { + "calendar": { + "breakfast": { + "name": "Breakfast" + }, + "dinner": { + "name": "Dinner" + }, + "lunch": { + "name": "Lunch" + }, + "side": { + "name": "Side" + } + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 5d0718092e5..7cd0e270703 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -319,6 +319,7 @@ FLOWS = { "lyric", "mailgun", "matter", + "mealie", "meater", "medcom_ble", "media_extractor", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index fb3e33d3289..0fe63cc02ff 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -3505,6 +3505,12 @@ "config_flow": true, "iot_class": "local_push" }, + "mealie": { + "name": "Mealie", + "integration_type": "service", + "config_flow": true, + "iot_class": "local_polling" + }, "meater": { "name": "Meater", "integration_type": "hub", diff --git a/requirements_all.txt b/requirements_all.txt index 04dec29d798..f949d864f54 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -293,6 +293,9 @@ aiolookin==1.0.0 # homeassistant.components.lyric aiolyric==1.1.0 +# homeassistant.components.mealie +aiomealie==0.3.1 + # homeassistant.components.modern_forms aiomodernforms==0.1.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 91d0fa2ff2d..face667ccc5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -266,6 +266,9 @@ aiolookin==1.0.0 # homeassistant.components.lyric aiolyric==1.1.0 +# homeassistant.components.mealie +aiomealie==0.3.1 + # homeassistant.components.modern_forms aiomodernforms==0.1.8 diff --git a/tests/components/mealie/__init__.py b/tests/components/mealie/__init__.py new file mode 100644 index 00000000000..3e85e241c6f --- /dev/null +++ b/tests/components/mealie/__init__.py @@ -0,0 +1,13 @@ +"""Tests for the Mealie integration.""" + +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +async def setup_integration(hass: HomeAssistant, config_entry: MockConfigEntry) -> None: + """Fixture for setting up the component.""" + config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() diff --git a/tests/components/mealie/conftest.py b/tests/components/mealie/conftest.py new file mode 100644 index 00000000000..dd6309cb524 --- /dev/null +++ b/tests/components/mealie/conftest.py @@ -0,0 +1,58 @@ +"""Mealie tests configuration.""" + +from unittest.mock import patch + +from aiomealie import Mealplan, MealplanResponse +from mashumaro.codecs.orjson import ORJSONDecoder +import pytest +from typing_extensions import Generator + +from homeassistant.components.mealie.const import DOMAIN +from homeassistant.const import CONF_API_TOKEN, CONF_HOST + +from tests.common import MockConfigEntry, load_fixture +from tests.components.smhi.common import AsyncMock + + +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock]: + """Override async_setup_entry.""" + with patch( + "homeassistant.components.mealie.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + yield mock_setup_entry + + +@pytest.fixture +def mock_mealie_client() -> Generator[AsyncMock]: + """Mock a Mealie client.""" + with ( + patch( + "homeassistant.components.mealie.coordinator.MealieClient", + autospec=True, + ) as mock_client, + patch( + "homeassistant.components.mealie.config_flow.MealieClient", + new=mock_client, + ), + ): + client = mock_client.return_value + client.get_mealplans.return_value = MealplanResponse.from_json( + load_fixture("get_mealplans.json", DOMAIN) + ) + client.get_mealplan_today.return_value = ORJSONDecoder(list[Mealplan]).decode( + load_fixture("get_mealplan_today.json", DOMAIN) + ) + yield client + + +@pytest.fixture +def mock_config_entry() -> MockConfigEntry: + """Mock a config entry.""" + return MockConfigEntry( + domain=DOMAIN, + title="Mealie", + data={CONF_HOST: "demo.mealie.io", CONF_API_TOKEN: "token"}, + entry_id="01J0BC4QM2YBRP6H5G933CETT7", + ) diff --git a/tests/components/mealie/fixtures/get_mealplan_today.json b/tests/components/mealie/fixtures/get_mealplan_today.json new file mode 100644 index 00000000000..1413f4a0113 --- /dev/null +++ b/tests/components/mealie/fixtures/get_mealplan_today.json @@ -0,0 +1,253 @@ +[ + { + "date": "2024-01-21", + "entryType": "dinner", + "title": "", + "text": "", + "recipeId": "40393996-417e-4487-a081-28608a668826", + "id": 192, + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "recipe": { + "id": "40393996-417e-4487-a081-28608a668826", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "name": "Cauliflower Salad", + "slug": "cauliflower-salad", + "image": "qLdv", + "recipeYield": "6 servings", + "totalTime": "2 Hours 35 Minutes", + "prepTime": "25 Minutes", + "cookTime": null, + "performTime": "10 Minutes", + "description": "This is a wonderful option for picnics and grill outs when you are looking for a new take on potato salad. This simple side salad made with cauliflower, peas, and hard boiled eggs can be made the day ahead and chilled until party time!", + "recipeCategory": [], + "tags": [], + "tools": [ + { + "id": "6e199f62-8356-46cf-8f6f-ea923780a1e3", + "name": "Stove", + "slug": "stove", + "onHand": false + } + ], + "rating": null, + "orgURL": "https://www.allrecipes.com/recipe/142152/cauliflower-salad/", + "dateAdded": "2023-12-29", + "dateUpdated": "2024-01-06T13:38:55.116185", + "createdAt": "2023-12-29T00:46:50.138612", + "updateAt": "2024-01-06T13:38:55.119029", + "lastMade": "2024-01-06T22:59:59" + } + }, + { + "date": "2024-01-21", + "entryType": "dinner", + "title": "", + "text": "", + "recipeId": "872bb477-8d90-4025-98b0-07a9d0d9ce3a", + "id": 206, + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "recipe": { + "id": "872bb477-8d90-4025-98b0-07a9d0d9ce3a", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "name": "15 Minute Cheesy Sausage & Veg Pasta", + "slug": "15-minute-cheesy-sausage-veg-pasta", + "image": "BeNc", + "recipeYield": "", + "totalTime": null, + "prepTime": null, + "cookTime": null, + "performTime": null, + "description": "Easy, cheesy, sausage pasta! In the whirlwind of mid-week mayhem, dinner doesn’t have to be a chore – this 15-minute pasta, featuring HECK’s Chicken Italia Chipolatas is your ticket to a delicious and hassle-free mid-week meal.", + "recipeCategory": [], + "tags": [], + "tools": [], + "rating": null, + "orgURL": "https://www.annabelkarmel.com/recipes/15-minute-cheesy-sausage-veg-pasta/", + "dateAdded": "2024-01-01", + "dateUpdated": "2024-01-01T20:40:40.441381", + "createdAt": "2024-01-01T20:40:40.443048", + "updateAt": "2024-01-01T20:40:40.443050", + "lastMade": null + } + }, + { + "date": "2024-01-21", + "entryType": "lunch", + "title": "", + "text": "", + "recipeId": "744a9831-fa56-4f61-9e12-fc5ebce58ed9", + "id": 207, + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "recipe": { + "id": "744a9831-fa56-4f61-9e12-fc5ebce58ed9", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "name": "cake", + "slug": "cake", + "image": null, + "recipeYield": null, + "totalTime": null, + "prepTime": null, + "cookTime": null, + "performTime": null, + "description": "", + "recipeCategory": [], + "tags": [], + "tools": [], + "rating": null, + "orgURL": null, + "dateAdded": "2024-01-01", + "dateUpdated": "2024-01-01T14:39:11.214806", + "createdAt": "2024-01-01T14:39:11.216709", + "updateAt": "2024-01-01T14:39:11.216711", + "lastMade": null + } + }, + { + "date": "2024-01-21", + "entryType": "lunch", + "title": "", + "text": "", + "recipeId": "27455eb2-31d3-4682-84ff-02a114bf293a", + "id": 208, + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "recipe": { + "id": "27455eb2-31d3-4682-84ff-02a114bf293a", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "name": "Pomegranate chicken with almond couscous", + "slug": "pomegranate-chicken-with-almond-couscous", + "image": "lF4p", + "recipeYield": "4 servings", + "totalTime": "20 Minutes", + "prepTime": "5 Minutes", + "cookTime": null, + "performTime": "15 Minutes", + "description": "Jazz up chicken breasts in this fruity, sweetly spiced sauce with pomegranate seeds, toasted almonds and tagine paste", + "recipeCategory": [], + "tags": [], + "tools": [], + "rating": null, + "orgURL": "https://www.bbcgoodfood.com/recipes/pomegranate-chicken-almond-couscous", + "dateAdded": "2023-12-29", + "dateUpdated": "2023-12-29T08:29:03.178355", + "createdAt": "2023-12-29T08:29:03.180819", + "updateAt": "2023-12-29T08:29:03.180820", + "lastMade": null + } + }, + { + "date": "2024-01-21", + "entryType": "dinner", + "title": "", + "text": "", + "recipeId": "4233330e-6947-4042-90b7-44c405b70714", + "id": 209, + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "recipe": { + "id": "4233330e-6947-4042-90b7-44c405b70714", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "name": "Csirkés és tofus empanadas", + "slug": "csirkes-es-tofus-empanadas", + "image": "ALqz", + "recipeYield": "16 servings", + "totalTime": "95", + "prepTime": "40", + "cookTime": null, + "performTime": "15", + "description": "", + "recipeCategory": [], + "tags": [], + "tools": [], + "rating": null, + "orgURL": "https://streetkitchen.hu/street-kitchen/csirkes-es-tofus-empanadas/", + "dateAdded": "2023-12-29", + "dateUpdated": "2023-12-29T07:56:20.087496", + "createdAt": "2023-12-29T07:53:47.765573", + "updateAt": "2023-12-29T07:56:20.090890", + "lastMade": null + } + }, + { + "date": "2024-01-21", + "entryType": "dinner", + "title": "", + "text": "", + "recipeId": "48f39d27-4b8e-4c14-bf36-4e1e6497e75e", + "id": 210, + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "recipe": { + "id": "48f39d27-4b8e-4c14-bf36-4e1e6497e75e", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "name": "All-American Beef Stew Recipe", + "slug": "all-american-beef-stew-recipe", + "image": "356X", + "recipeYield": "6 servings", + "totalTime": "3 Hours 15 Minutes", + "prepTime": "5 Minutes", + "cookTime": null, + "performTime": "3 Hours 10 Minutes", + "description": "This All-American beef stew recipe includes tender beef coated in a rich, intense sauce and vegetables that bring complementary texture and flavor.", + "recipeCategory": [], + "tags": [ + { + "id": "78318c97-75c7-4d06-95b6-51ef8f4a0257", + "name": "< 4 Hours", + "slug": "4-hours" + } + ], + "tools": [], + "rating": null, + "orgURL": "https://www.seriouseats.com/all-american-beef-stew-recipe", + "dateAdded": "2024-01-20", + "dateUpdated": "2024-01-21T03:04:45.606075", + "createdAt": "2024-01-20T20:41:29.266390", + "updateAt": "2024-01-21T03:04:45.609563", + "lastMade": null + } + }, + { + "date": "2024-01-21", + "entryType": "dinner", + "title": "", + "text": "", + "recipeId": "27455eb2-31d3-4682-84ff-02a114bf293a", + "id": 223, + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "recipe": { + "id": "27455eb2-31d3-4682-84ff-02a114bf293a", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "name": "Pomegranate chicken with almond couscous", + "slug": "pomegranate-chicken-with-almond-couscous", + "image": "lF4p", + "recipeYield": "4 servings", + "totalTime": "20 Minutes", + "prepTime": "5 Minutes", + "cookTime": null, + "performTime": "15 Minutes", + "description": "Jazz up chicken breasts in this fruity, sweetly spiced sauce with pomegranate seeds, toasted almonds and tagine paste", + "recipeCategory": [], + "tags": [], + "tools": [], + "rating": null, + "orgURL": "https://www.bbcgoodfood.com/recipes/pomegranate-chicken-almond-couscous", + "dateAdded": "2023-12-29", + "dateUpdated": "2023-12-29T08:29:03.178355", + "createdAt": "2023-12-29T08:29:03.180819", + "updateAt": "2023-12-29T08:29:03.180820", + "lastMade": null + } + } +] diff --git a/tests/components/mealie/fixtures/get_mealplans.json b/tests/components/mealie/fixtures/get_mealplans.json new file mode 100644 index 00000000000..2d63b753d99 --- /dev/null +++ b/tests/components/mealie/fixtures/get_mealplans.json @@ -0,0 +1,612 @@ +{ + "page": 1, + "per_page": 50, + "total": 14, + "total_pages": 1, + "items": [ + { + "date": "2024-01-22", + "entryType": "dinner", + "title": "", + "text": "", + "recipeId": "c5f00a93-71a2-4e48-900f-d9ad0bb9de93", + "id": 230, + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "recipe": { + "id": "c5f00a93-71a2-4e48-900f-d9ad0bb9de93", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "name": "Zoete aardappel curry traybake", + "slug": "zoete-aardappel-curry-traybake", + "image": "AiIo", + "recipeYield": "2 servings", + "totalTime": "40 Minutes", + "prepTime": null, + "cookTime": null, + "performTime": null, + "description": "Een traybake is eigenlijk altijd een goed idee. Deze zoete aardappel curry traybake dus ook. Waarom? Omdat je alleen maar wat groenten - en in dit geval kip - op een bakplaat (traybake dus) legt, hier wat kruiden aan toevoegt en deze in de oven schuift. Ideaal dus als je geen zin hebt om lang in de keuken te staan. Maar gewoon lekker op de bank wil ploffen om te wachten tot de oven klaar is. Joe! That\\'s what we like. Deze zoete aardappel curry traybake bevat behalve zoete aardappel en curry ook kikkererwten, kippendijfilet en bloemkoolroosjes. Je gebruikt yoghurt en limoen als een soort dressing. En je serveert deze heerlijke traybake met naanbrood. Je kunt natuurljk ook voor deze traybake met chipolataworstjes gaan. Wil je graag meer ovengerechten? Dan moet je eigenlijk even kijken naar onze Ovenbijbel. Onmisbaar in je keuken! We willen je deze zoete aardappelstamppot met prei ook niet onthouden. Megalekker bordje comfortfood als je \\'t ons vraagt.", + "recipeCategory": [], + "tags": [], + "tools": [], + "rating": null, + "orgURL": "https://chickslovefood.com/recept/zoete-aardappel-curry-traybake/", + "dateAdded": "2024-01-22", + "dateUpdated": "2024-01-22T00:27:46.324512", + "createdAt": "2024-01-22T00:27:46.327546", + "updateAt": "2024-01-22T00:27:46.327548", + "lastMade": null + } + }, + { + "date": "2024-01-23", + "entryType": "breakfast", + "title": "", + "text": "", + "recipeId": "5b055066-d57d-4fd0-8dfd-a2c2f07b36f1", + "id": 229, + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "recipe": { + "id": "5b055066-d57d-4fd0-8dfd-a2c2f07b36f1", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "name": "Roast Chicken", + "slug": "roast-chicken", + "image": "JeQ2", + "recipeYield": "6 servings", + "totalTime": "1 Hour 35 Minutes", + "prepTime": "15 Minutes", + "cookTime": null, + "performTime": "1 Hour 20 Minutes", + "description": "The BEST Roast Chicken recipe is simple, budget friendly, and gives you a tender, mouth-watering chicken full of flavor! Served with roasted vegetables, this recipe is simple enough for any cook!", + "recipeCategory": [], + "tags": [], + "tools": [], + "rating": null, + "orgURL": "https://tastesbetterfromscratch.com/roast-chicken/", + "dateAdded": "2024-01-21", + "dateUpdated": "2024-01-21T15:29:25.664540", + "createdAt": "2024-01-21T15:29:25.667450", + "updateAt": "2024-01-21T15:29:25.667452", + "lastMade": null + } + }, + { + "date": "2024-01-23", + "entryType": "lunch", + "title": "", + "text": "", + "recipeId": "e360a0cc-18b0-4a84-a91b-8aa59e2451c9", + "id": 226, + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "recipe": { + "id": "e360a0cc-18b0-4a84-a91b-8aa59e2451c9", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "name": "Receta de pollo al curry en 10 minutos (con vídeo incluido)", + "slug": "receta-de-pollo-al-curry-en-10-minutos-con-video-incluido", + "image": "INQz", + "recipeYield": "2 servings", + "totalTime": "10 Minutes", + "prepTime": "3 Minutes", + "cookTime": null, + "performTime": "7 Minutes", + "description": "Te explicamos paso a paso, de manera sencilla, la elaboración de la receta de pollo al curry con leche de coco en 10 minutos. Ingredientes, tiempo de...", + "recipeCategory": [], + "tags": [], + "tools": [ + { + "id": "1170e609-20d3-45b8-b0c7-3a4cfa614e88", + "name": "Backofen", + "slug": "backofen", + "onHand": false + }, + { + "id": "9ab522ad-c3be-4dad-8b4f-d9d53817f4d0", + "name": "Magimix blender", + "slug": "magimix-blender", + "onHand": false + }, + { + "id": "b4ca27dc-9bf6-48be-ad10-3e7056cb24bc", + "name": "Alluminio", + "slug": "alluminio", + "onHand": false + } + ], + "rating": null, + "orgURL": "https://www.directoalpaladar.com/recetas-de-carnes-y-aves/receta-de-pollo-al-curry-en-10-minutos", + "dateAdded": "2024-01-21", + "dateUpdated": "2024-01-21T12:56:31.483701", + "createdAt": "2024-01-21T12:45:28.589669", + "updateAt": "2024-01-21T12:56:31.487406", + "lastMade": null + } + }, + { + "date": "2024-01-23", + "entryType": "lunch", + "title": "", + "text": "", + "recipeId": "9c7b8aee-c93c-4b1b-ab48-2625d444743a", + "id": 224, + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "recipe": { + "id": "9c7b8aee-c93c-4b1b-ab48-2625d444743a", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "name": "Boeuf bourguignon : la vraie recette (2)", + "slug": "boeuf-bourguignon-la-vraie-recette-2", + "image": "nj5M", + "recipeYield": "4 servings", + "totalTime": "5 Hours", + "prepTime": "1 Hour", + "cookTime": null, + "performTime": "4 Hours", + "description": "bourguignon, oignon, carotte, bouquet garni, vin rouge, beurre, sel, poivre", + "recipeCategory": [], + "tags": [ + { + "id": "01c2f4ac-54ce-49bc-9bd7-8a49f353a3a4", + "name": "Poivre", + "slug": "poivre" + }, + { + "id": "90a26cea-a8a1-41a1-9e8c-e94e3c40f7a7", + "name": "Sel", + "slug": "sel" + }, + { + "id": "d7b01a4b-5206-4bd2-b9c4-d13b95ca0edb", + "name": "Beurre", + "slug": "beurre" + }, + { + "id": "304faaf8-13ec-4537-91f3-9f39a3585545", + "name": "Facile", + "slug": "facile" + }, + { + "id": "6508fb05-fb60-4bed-90c4-584bd6d74cb5", + "name": "Daube", + "slug": "daube" + }, + { + "id": "18ff59b6-b599-456a-896b-4b76448b08ca", + "name": "Bourguignon", + "slug": "bourguignon" + }, + { + "id": "685a0d90-8de4-494e-8eb8-68e7f5d5ffbe", + "name": "Vin Rouge", + "slug": "vin-rouge" + }, + { + "id": "5dedc8b5-30f5-4d6e-875f-34deefd01883", + "name": "Oignon", + "slug": "oignon" + }, + { + "id": "065b79e0-6276-4ebb-9428-7018b40c55bb", + "name": "Bouquet Garni", + "slug": "bouquet-garni" + }, + { + "id": "d858b1d9-2ca1-46d4-acc2-3d03f991f03f", + "name": "Moyen", + "slug": "moyen" + }, + { + "id": "bded0bd8-8d41-4ec5-ad73-e0107fb60908", + "name": "Boeuf Bourguignon : La Vraie Recette", + "slug": "boeuf-bourguignon-la-vraie-recette" + }, + { + "id": "7f99b04f-914a-408b-a057-511ca1125734", + "name": "Carotte", + "slug": "carotte" + } + ], + "tools": [], + "rating": null, + "orgURL": "https://www.marmiton.org/recettes/recette_boeuf-bourguignon_18889.aspx", + "dateAdded": "2024-01-21", + "dateUpdated": "2024-01-21T08:45:28.780361", + "createdAt": "2024-01-21T08:45:28.782322", + "updateAt": "2024-01-21T08:45:28.782324", + "lastMade": null + } + }, + { + "date": "2024-01-23", + "entryType": "dinner", + "title": "", + "text": "", + "recipeId": "f79f7e9d-4b58-4930-a586-2b127f16ee34", + "id": 222, + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "recipe": { + "id": "f79f7e9d-4b58-4930-a586-2b127f16ee34", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "name": "Εύκολη μακαρονάδα με κεφτεδάκια στον φούρνο (1)", + "slug": "eukole-makaronada-me-kephtedakia-ston-phourno-1", + "image": "En9o", + "recipeYield": "6 servings", + "totalTime": null, + "prepTime": "15 Minutes", + "cookTime": null, + "performTime": "50 Minutes", + "description": "Εύκολη μακαρονάδα με κεφτεδάκια στον φούρνο από τον Άκη Πετρετζίκη. Φτιάξτε την πιο εύκολη μακαρονάδα με κεφτεδάκια σε μόνο ένα σκεύος.", + "recipeCategory": [], + "tags": [], + "tools": [], + "rating": null, + "orgURL": "https://akispetretzikis.com/recipe/7959/efkolh-makaronada-me-keftedakia-ston-fourno", + "dateAdded": "2024-01-21", + "dateUpdated": "2024-01-21T09:08:58.056854", + "createdAt": "2024-01-21T09:08:58.059401", + "updateAt": "2024-01-21T09:08:58.059403", + "lastMade": null + } + }, + { + "date": "2024-01-23", + "entryType": "dinner", + "title": "", + "text": "", + "recipeId": "47595e4c-52bc-441d-b273-3edf4258806d", + "id": 221, + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "recipe": { + "id": "47595e4c-52bc-441d-b273-3edf4258806d", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "name": "Greek Turkey Meatballs with Lemon Orzo & Creamy Feta Yogurt Sauce", + "slug": "greek-turkey-meatballs-with-lemon-orzo-creamy-feta-yogurt-sauce", + "image": "Kn62", + "recipeYield": "4 servings", + "totalTime": "1 Hour", + "prepTime": "40 Minutes", + "cookTime": null, + "performTime": "20 Minutes", + "description": "Delicious Greek turkey meatballs with lemon orzo, tender veggies, and a creamy feta yogurt sauce. These healthy baked Greek turkey meatballs are filled with tons of wonderful herbs and make the perfect protein-packed weeknight meal!", + "recipeCategory": [], + "tags": [], + "tools": [], + "rating": null, + "orgURL": "https://www.ambitiouskitchen.com/greek-turkey-meatballs/", + "dateAdded": "2024-01-04", + "dateUpdated": "2024-01-04T11:51:00.843570", + "createdAt": "2024-01-04T11:51:00.847033", + "updateAt": "2024-01-04T11:51:00.847035", + "lastMade": null + } + }, + { + "date": "2024-01-23", + "entryType": "side", + "title": "", + "text": "", + "recipeId": "9d553779-607e-471b-acf3-84e6be27b159", + "id": 220, + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "recipe": { + "id": "9d553779-607e-471b-acf3-84e6be27b159", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "name": "Einfacher Nudelauflauf mit Brokkoli", + "slug": "einfacher-nudelauflauf-mit-brokkoli", + "image": "nOPT", + "recipeYield": "4 servings", + "totalTime": "35 Minutes", + "prepTime": "15 Minutes", + "cookTime": null, + "performTime": "20 Minutes", + "description": "Einfacher Nudelauflauf mit Brokkoli, Sahnesauce und extra Käse. Dieses vegetarische 5 Zutaten Rezept ist super schnell gemacht und SO gut!", + "recipeCategory": [], + "tags": [ + { + "id": "78318c97-75c7-4d06-95b6-51ef8f4a0257", + "name": "< 4 Hours", + "slug": "4-hours" + } + ], + "tools": [], + "rating": null, + "orgURL": "https://kochkarussell.com/einfacher-nudelauflauf-brokkoli/", + "dateAdded": "2024-01-21", + "dateUpdated": "2024-01-21T03:04:25.718367", + "createdAt": "2024-01-21T02:13:11.323363", + "updateAt": "2024-01-21T03:04:25.721489", + "lastMade": null + } + }, + { + "date": "2024-01-23", + "entryType": "dinner", + "title": "", + "text": "", + "recipeId": "92635fd0-f2dc-4e78-a6e4-ecd556ad361f", + "id": 219, + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "recipe": { + "id": "92635fd0-f2dc-4e78-a6e4-ecd556ad361f", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "name": "Pampered Chef Double Chocolate Mocha Trifle", + "slug": "pampered-chef-double-chocolate-mocha-trifle", + "image": "ibL6", + "recipeYield": "12 servings", + "totalTime": "1 Hour 15 Minutes", + "prepTime": "15 Minutes", + "cookTime": null, + "performTime": "1 Hour", + "description": "This is a modified Pampered Chef recipe. You can use a trifle bowl or large glass punch/salad bowl to show it off. It is really easy to make and I never have any leftovers. Cook time includes chill time.", + "recipeCategory": [], + "tags": [ + { + "id": "0248c21d-c85a-47b2-aaf6-fb6caf1b7726", + "name": "Weeknight", + "slug": "weeknight" + }, + { + "id": "78318c97-75c7-4d06-95b6-51ef8f4a0257", + "name": "< 4 Hours", + "slug": "4-hours" + } + ], + "tools": [], + "rating": 3, + "orgURL": "https://www.food.com/recipe/pampered-chef-double-chocolate-mocha-trifle-74963", + "dateAdded": "2024-01-06", + "dateUpdated": "2024-01-06T08:11:21.427447", + "createdAt": "2024-01-06T06:29:24.966994", + "updateAt": "2024-01-06T08:11:21.430079", + "lastMade": null + } + }, + { + "date": "2024-01-22", + "entryType": "dinner", + "title": "", + "text": "", + "recipeId": "8bdd3656-5e7e-45d3-a3c4-557390846a22", + "id": 217, + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "recipe": { + "id": "8bdd3656-5e7e-45d3-a3c4-557390846a22", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "name": "Cheeseburger Sliders (Easy, 30-min Recipe)", + "slug": "cheeseburger-sliders-easy-30-min-recipe", + "image": "beGq", + "recipeYield": "24 servings", + "totalTime": "30 Minutes", + "prepTime": "8 Minutes", + "cookTime": null, + "performTime": "22 Minutes", + "description": "Cheeseburger Sliders are juicy, cheesy and beefy - everything we love about classic burgers! These sliders are quick and easy plus they are make-ahead and reheat really well.", + "recipeCategory": [], + "tags": [ + { + "id": "7a4ca427-642f-4428-8dc7-557ea9c8d1b4", + "name": "Cheeseburger Sliders", + "slug": "cheeseburger-sliders" + }, + { + "id": "941558d2-50d5-4c9d-8890-a0258f18d493", + "name": "Sliders", + "slug": "sliders" + } + ], + "tools": [], + "rating": 5, + "orgURL": "https://natashaskitchen.com/cheeseburger-sliders/", + "dateAdded": "2024-01-21", + "dateUpdated": "2024-01-21T07:43:24.261010", + "createdAt": "2024-01-21T06:49:35.466777", + "updateAt": "2024-01-21T06:49:35.466778", + "lastMade": "2024-01-22T04:59:59" + } + }, + { + "date": "2024-01-22", + "entryType": "lunch", + "title": "", + "text": "", + "recipeId": "48f39d27-4b8e-4c14-bf36-4e1e6497e75e", + "id": 216, + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "recipe": { + "id": "48f39d27-4b8e-4c14-bf36-4e1e6497e75e", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "name": "All-American Beef Stew Recipe", + "slug": "all-american-beef-stew-recipe", + "image": "356X", + "recipeYield": "6 servings", + "totalTime": "3 Hours 15 Minutes", + "prepTime": "5 Minutes", + "cookTime": null, + "performTime": "3 Hours 10 Minutes", + "description": "This All-American beef stew recipe includes tender beef coated in a rich, intense sauce and vegetables that bring complementary texture and flavor.", + "recipeCategory": [], + "tags": [ + { + "id": "78318c97-75c7-4d06-95b6-51ef8f4a0257", + "name": "< 4 Hours", + "slug": "4-hours" + } + ], + "tools": [], + "rating": null, + "orgURL": "https://www.seriouseats.com/all-american-beef-stew-recipe", + "dateAdded": "2024-01-20", + "dateUpdated": "2024-01-21T03:04:45.606075", + "createdAt": "2024-01-20T20:41:29.266390", + "updateAt": "2024-01-21T03:04:45.609563", + "lastMade": null + } + }, + { + "date": "2024-01-23", + "entryType": "dinner", + "title": "", + "text": "", + "recipeId": "48f39d27-4b8e-4c14-bf36-4e1e6497e75e", + "id": 212, + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "recipe": { + "id": "48f39d27-4b8e-4c14-bf36-4e1e6497e75e", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "name": "All-American Beef Stew Recipe", + "slug": "all-american-beef-stew-recipe", + "image": "356X", + "recipeYield": "6 servings", + "totalTime": "3 Hours 15 Minutes", + "prepTime": "5 Minutes", + "cookTime": null, + "performTime": "3 Hours 10 Minutes", + "description": "This All-American beef stew recipe includes tender beef coated in a rich, intense sauce and vegetables that bring complementary texture and flavor.", + "recipeCategory": [], + "tags": [ + { + "id": "78318c97-75c7-4d06-95b6-51ef8f4a0257", + "name": "< 4 Hours", + "slug": "4-hours" + } + ], + "tools": [], + "rating": null, + "orgURL": "https://www.seriouseats.com/all-american-beef-stew-recipe", + "dateAdded": "2024-01-20", + "dateUpdated": "2024-01-21T03:04:45.606075", + "createdAt": "2024-01-20T20:41:29.266390", + "updateAt": "2024-01-21T03:04:45.609563", + "lastMade": null + } + }, + { + "date": "2024-01-22", + "entryType": "dinner", + "title": "", + "text": "", + "recipeId": "9d553779-607e-471b-acf3-84e6be27b159", + "id": 211, + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "recipe": { + "id": "9d553779-607e-471b-acf3-84e6be27b159", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "name": "Einfacher Nudelauflauf mit Brokkoli", + "slug": "einfacher-nudelauflauf-mit-brokkoli", + "image": "nOPT", + "recipeYield": "4 servings", + "totalTime": "35 Minutes", + "prepTime": "15 Minutes", + "cookTime": null, + "performTime": "20 Minutes", + "description": "Einfacher Nudelauflauf mit Brokkoli, Sahnesauce und extra Käse. Dieses vegetarische 5 Zutaten Rezept ist super schnell gemacht und SO gut!", + "recipeCategory": [], + "tags": [ + { + "id": "78318c97-75c7-4d06-95b6-51ef8f4a0257", + "name": "< 4 Hours", + "slug": "4-hours" + } + ], + "tools": [], + "rating": null, + "orgURL": "https://kochkarussell.com/einfacher-nudelauflauf-brokkoli/", + "dateAdded": "2024-01-21", + "dateUpdated": "2024-01-21T03:04:25.718367", + "createdAt": "2024-01-21T02:13:11.323363", + "updateAt": "2024-01-21T03:04:25.721489", + "lastMade": null + } + }, + { + "date": "2024-01-23", + "entryType": "dinner", + "title": "", + "text": "", + "recipeId": "25b814f2-d9bf-4df0-b40d-d2f2457b4317", + "id": 196, + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "recipe": { + "id": "25b814f2-d9bf-4df0-b40d-d2f2457b4317", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "name": "Miso Udon Noodles with Spinach and Tofu", + "slug": "miso-udon-noodles-with-spinach-and-tofu", + "image": "5G1v", + "recipeYield": "2 servings", + "totalTime": "25 Minutes", + "prepTime": "10 Minutes", + "cookTime": null, + "performTime": "15 Minutes", + "description": "Simple to prepare and ready in 25 minutes, this vegetarian miso noodle recipe can be eaten on its own or served as a side.", + "recipeCategory": [], + "tags": [], + "tools": [], + "rating": null, + "orgURL": "https://www.allrecipes.com/recipe/284039/miso-udon-noodles-with-spinach-and-tofu/", + "dateAdded": "2024-01-05", + "dateUpdated": "2024-01-05T16:35:00.264511", + "createdAt": "2024-01-05T16:00:45.090493", + "updateAt": "2024-01-05T16:35:00.267508", + "lastMade": null + } + }, + { + "date": "2024-01-22", + "entryType": "dinner", + "title": "", + "text": "", + "recipeId": "55c88810-4cf1-4d86-ae50-63b15fd173fb", + "id": 195, + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "recipe": { + "id": "55c88810-4cf1-4d86-ae50-63b15fd173fb", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "name": "Mousse de saumon", + "slug": "mousse-de-saumon", + "image": "rrNL", + "recipeYield": "12 servings", + "totalTime": "17 Minutes", + "prepTime": "15 Minutes", + "cookTime": null, + "performTime": "2 Minutes", + "description": "Avis aux nostalgiques des années 1980, la mousse de saumon est de retour dans une présentation adaptée au goût du jour. On utilise une technique sans faille : un saumon frais cuit au micro-ondes et mélangé au robot avec du fromage à la crème et de la crème sure. On obtient ainsi une texture onctueuse à tartiner, qui n’a rien à envier aux préparations gélatineuses d’antan !", + "recipeCategory": [], + "tags": [], + "tools": [], + "rating": null, + "orgURL": "https://www.ricardocuisine.com/recettes/8919-mousse-de-saumon", + "dateAdded": "2024-01-02", + "dateUpdated": "2024-01-02T06:35:05.206948", + "createdAt": "2024-01-02T06:33:15.329794", + "updateAt": "2024-01-02T06:35:05.209189", + "lastMade": "2024-01-02T22:59:59" + } + } + ], + "next": null, + "previous": null +} diff --git a/tests/components/mealie/snapshots/test_calendar.ambr b/tests/components/mealie/snapshots/test_calendar.ambr new file mode 100644 index 00000000000..6af53c112de --- /dev/null +++ b/tests/components/mealie/snapshots/test_calendar.ambr @@ -0,0 +1,359 @@ +# serializer version: 1 +# name: test_api_calendar + list([ + dict({ + 'entity_id': 'calendar.mealie_breakfast', + 'name': 'Mealie Breakfast', + }), + dict({ + 'entity_id': 'calendar.mealie_dinner', + 'name': 'Mealie Dinner', + }), + dict({ + 'entity_id': 'calendar.mealie_lunch', + 'name': 'Mealie Lunch', + }), + dict({ + 'entity_id': 'calendar.mealie_side', + 'name': 'Mealie Side', + }), + ]) +# --- +# name: test_api_events + list([ + dict({ + 'description': "Een traybake is eigenlijk altijd een goed idee. Deze zoete aardappel curry traybake dus ook. Waarom? Omdat je alleen maar wat groenten - en in dit geval kip - op een bakplaat (traybake dus) legt, hier wat kruiden aan toevoegt en deze in de oven schuift. Ideaal dus als je geen zin hebt om lang in de keuken te staan. Maar gewoon lekker op de bank wil ploffen om te wachten tot de oven klaar is. Joe! That\\'s what we like. Deze zoete aardappel curry traybake bevat behalve zoete aardappel en curry ook kikkererwten, kippendijfilet en bloemkoolroosjes. Je gebruikt yoghurt en limoen als een soort dressing. En je serveert deze heerlijke traybake met naanbrood. Je kunt natuurljk ook voor deze traybake met chipolataworstjes gaan. Wil je graag meer ovengerechten? Dan moet je eigenlijk even kijken naar onze Ovenbijbel. Onmisbaar in je keuken! We willen je deze zoete aardappelstamppot met prei ook niet onthouden. Megalekker bordje comfortfood als je \\'t ons vraagt.", + 'end': dict({ + 'date': '2024-01-23', + }), + 'location': None, + 'recurrence_id': None, + 'rrule': None, + 'start': dict({ + 'date': '2024-01-22', + }), + 'summary': 'Zoete aardappel curry traybake', + 'uid': None, + }), + dict({ + 'description': 'Εύκολη μακαρονάδα με κεφτεδάκια στον φούρνο από τον Άκη Πετρετζίκη. Φτιάξτε την πιο εύκολη μακαρονάδα με κεφτεδάκια σε μόνο ένα σκεύος.', + 'end': dict({ + 'date': '2024-01-24', + }), + 'location': None, + 'recurrence_id': None, + 'rrule': None, + 'start': dict({ + 'date': '2024-01-23', + }), + 'summary': 'Εύκολη μακαρονάδα με κεφτεδάκια στον φούρνο (1)', + 'uid': None, + }), + dict({ + 'description': 'Delicious Greek turkey meatballs with lemon orzo, tender veggies, and a creamy feta yogurt sauce. These healthy baked Greek turkey meatballs are filled with tons of wonderful herbs and make the perfect protein-packed weeknight meal!', + 'end': dict({ + 'date': '2024-01-24', + }), + 'location': None, + 'recurrence_id': None, + 'rrule': None, + 'start': dict({ + 'date': '2024-01-23', + }), + 'summary': 'Greek Turkey Meatballs with Lemon Orzo & Creamy Feta Yogurt Sauce', + 'uid': None, + }), + dict({ + 'description': 'This is a modified Pampered Chef recipe. You can use a trifle bowl or large glass punch/salad bowl to show it off. It is really easy to make and I never have any leftovers. Cook time includes chill time.', + 'end': dict({ + 'date': '2024-01-24', + }), + 'location': None, + 'recurrence_id': None, + 'rrule': None, + 'start': dict({ + 'date': '2024-01-23', + }), + 'summary': 'Pampered Chef Double Chocolate Mocha Trifle', + 'uid': None, + }), + dict({ + 'description': 'Cheeseburger Sliders are juicy, cheesy and beefy - everything we love about classic burgers! These sliders are quick and easy plus they are make-ahead and reheat really well.', + 'end': dict({ + 'date': '2024-01-23', + }), + 'location': None, + 'recurrence_id': None, + 'rrule': None, + 'start': dict({ + 'date': '2024-01-22', + }), + 'summary': 'Cheeseburger Sliders (Easy, 30-min Recipe)', + 'uid': None, + }), + dict({ + 'description': 'This All-American beef stew recipe includes tender beef coated in a rich, intense sauce and vegetables that bring complementary texture and flavor.', + 'end': dict({ + 'date': '2024-01-24', + }), + 'location': None, + 'recurrence_id': None, + 'rrule': None, + 'start': dict({ + 'date': '2024-01-23', + }), + 'summary': 'All-American Beef Stew Recipe', + 'uid': None, + }), + dict({ + 'description': 'Einfacher Nudelauflauf mit Brokkoli, Sahnesauce und extra Käse. Dieses vegetarische 5 Zutaten Rezept ist super schnell gemacht und SO gut!', + 'end': dict({ + 'date': '2024-01-23', + }), + 'location': None, + 'recurrence_id': None, + 'rrule': None, + 'start': dict({ + 'date': '2024-01-22', + }), + 'summary': 'Einfacher Nudelauflauf mit Brokkoli', + 'uid': None, + }), + dict({ + 'description': 'Simple to prepare and ready in 25 minutes, this vegetarian miso noodle recipe can be eaten on its own or served as a side.', + 'end': dict({ + 'date': '2024-01-24', + }), + 'location': None, + 'recurrence_id': None, + 'rrule': None, + 'start': dict({ + 'date': '2024-01-23', + }), + 'summary': 'Miso Udon Noodles with Spinach and Tofu', + 'uid': None, + }), + dict({ + 'description': 'Avis aux nostalgiques des années 1980, la mousse de saumon est de retour dans une présentation adaptée au goût du jour. On utilise une technique sans faille : un saumon frais cuit au micro-ondes et mélangé au robot avec du fromage à la crème et de la crème sure. On obtient ainsi une texture onctueuse à tartiner, qui n’a rien à envier aux préparations gélatineuses d’antan !', + 'end': dict({ + 'date': '2024-01-23', + }), + 'location': None, + 'recurrence_id': None, + 'rrule': None, + 'start': dict({ + 'date': '2024-01-22', + }), + 'summary': 'Mousse de saumon', + 'uid': None, + }), + ]) +# --- +# name: test_entities[calendar.mealie_breakfast-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'calendar', + 'entity_category': None, + 'entity_id': 'calendar.mealie_breakfast', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Breakfast', + 'platform': 'mealie', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'breakfast', + 'unique_id': '01J0BC4QM2YBRP6H5G933CETT7_breakfast', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[calendar.mealie_breakfast-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'all_day': True, + 'description': 'The BEST Roast Chicken recipe is simple, budget friendly, and gives you a tender, mouth-watering chicken full of flavor! Served with roasted vegetables, this recipe is simple enough for any cook!', + 'end_time': '2024-01-24 00:00:00', + 'friendly_name': 'Mealie Breakfast', + 'location': '', + 'message': 'Roast Chicken', + 'start_time': '2024-01-23 00:00:00', + }), + 'context': , + 'entity_id': 'calendar.mealie_breakfast', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_entities[calendar.mealie_dinner-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'calendar', + 'entity_category': None, + 'entity_id': 'calendar.mealie_dinner', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Dinner', + 'platform': 'mealie', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'dinner', + 'unique_id': '01J0BC4QM2YBRP6H5G933CETT7_dinner', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[calendar.mealie_dinner-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'all_day': True, + 'description': "Een traybake is eigenlijk altijd een goed idee. Deze zoete aardappel curry traybake dus ook. Waarom? Omdat je alleen maar wat groenten - en in dit geval kip - op een bakplaat (traybake dus) legt, hier wat kruiden aan toevoegt en deze in de oven schuift. Ideaal dus als je geen zin hebt om lang in de keuken te staan. Maar gewoon lekker op de bank wil ploffen om te wachten tot de oven klaar is. Joe! That\\'s what we like. Deze zoete aardappel curry traybake bevat behalve zoete aardappel en curry ook kikkererwten, kippendijfilet en bloemkoolroosjes. Je gebruikt yoghurt en limoen als een soort dressing. En je serveert deze heerlijke traybake met naanbrood. Je kunt natuurljk ook voor deze traybake met chipolataworstjes gaan. Wil je graag meer ovengerechten? Dan moet je eigenlijk even kijken naar onze Ovenbijbel. Onmisbaar in je keuken! We willen je deze zoete aardappelstamppot met prei ook niet onthouden. Megalekker bordje comfortfood als je \\'t ons vraagt.", + 'end_time': '2024-01-23 00:00:00', + 'friendly_name': 'Mealie Dinner', + 'location': '', + 'message': 'Zoete aardappel curry traybake', + 'start_time': '2024-01-22 00:00:00', + }), + 'context': , + 'entity_id': 'calendar.mealie_dinner', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_entities[calendar.mealie_lunch-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'calendar', + 'entity_category': None, + 'entity_id': 'calendar.mealie_lunch', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Lunch', + 'platform': 'mealie', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lunch', + 'unique_id': '01J0BC4QM2YBRP6H5G933CETT7_lunch', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[calendar.mealie_lunch-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'all_day': True, + 'description': 'Te explicamos paso a paso, de manera sencilla, la elaboración de la receta de pollo al curry con leche de coco en 10 minutos. Ingredientes, tiempo de...', + 'end_time': '2024-01-24 00:00:00', + 'friendly_name': 'Mealie Lunch', + 'location': '', + 'message': 'Receta de pollo al curry en 10 minutos (con vídeo incluido)', + 'start_time': '2024-01-23 00:00:00', + }), + 'context': , + 'entity_id': 'calendar.mealie_lunch', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_entities[calendar.mealie_side-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'calendar', + 'entity_category': None, + 'entity_id': 'calendar.mealie_side', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Side', + 'platform': 'mealie', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'side', + 'unique_id': '01J0BC4QM2YBRP6H5G933CETT7_side', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[calendar.mealie_side-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'all_day': True, + 'description': 'Einfacher Nudelauflauf mit Brokkoli, Sahnesauce und extra Käse. Dieses vegetarische 5 Zutaten Rezept ist super schnell gemacht und SO gut!', + 'end_time': '2024-01-24 00:00:00', + 'friendly_name': 'Mealie Side', + 'location': '', + 'message': 'Einfacher Nudelauflauf mit Brokkoli', + 'start_time': '2024-01-23 00:00:00', + }), + 'context': , + 'entity_id': 'calendar.mealie_side', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- diff --git a/tests/components/mealie/snapshots/test_init.ambr b/tests/components/mealie/snapshots/test_init.ambr new file mode 100644 index 00000000000..c2752d938e4 --- /dev/null +++ b/tests/components/mealie/snapshots/test_init.ambr @@ -0,0 +1,31 @@ +# serializer version: 1 +# name: test_device_info + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': , + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'mealie', + '01J0BC4QM2YBRP6H5G933CETT7', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': None, + 'model': None, + 'name': 'Mealie', + 'name_by_user': None, + 'serial_number': None, + 'suggested_area': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- diff --git a/tests/components/mealie/test_calendar.py b/tests/components/mealie/test_calendar.py new file mode 100644 index 00000000000..9df2c1810fd --- /dev/null +++ b/tests/components/mealie/test_calendar.py @@ -0,0 +1,69 @@ +"""Tests for the Mealie calendar.""" + +from datetime import date +from http import HTTPStatus +from unittest.mock import AsyncMock + +from syrupy.assertion import SnapshotAssertion + +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import setup_integration + +from tests.common import MockConfigEntry, snapshot_platform +from tests.typing import ClientSessionGenerator + + +async def test_api_calendar( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + mock_mealie_client: AsyncMock, + mock_config_entry: MockConfigEntry, + hass_client: ClientSessionGenerator, +) -> None: + """Test the API returns the calendar.""" + await setup_integration(hass, mock_config_entry) + + client = await hass_client() + response = await client.get("/api/calendars") + assert response.status == HTTPStatus.OK + data = await response.json() + assert data == snapshot + + +async def test_entities( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, + mock_mealie_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test the API returns the calendar.""" + await setup_integration(hass, mock_config_entry) + + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + +async def test_api_events( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + mock_mealie_client: AsyncMock, + mock_config_entry: MockConfigEntry, + hass_client: ClientSessionGenerator, +) -> None: + """Test the Mealie calendar view.""" + await setup_integration(hass, mock_config_entry) + + client = await hass_client() + response = await client.get( + "/api/calendars/calendar.mealie_dinner?start=2023-08-01&end=2023-11-01" + ) + assert mock_mealie_client.get_mealplans.called == 1 + assert mock_mealie_client.get_mealplans.call_args_list[1].args == ( + date(2023, 8, 1), + date(2023, 11, 1), + ) + assert response.status == HTTPStatus.OK + events = await response.json() + assert events == snapshot diff --git a/tests/components/mealie/test_config_flow.py b/tests/components/mealie/test_config_flow.py new file mode 100644 index 00000000000..ac68ed2fac5 --- /dev/null +++ b/tests/components/mealie/test_config_flow.py @@ -0,0 +1,107 @@ +"""Tests for the Mealie config flow.""" + +from unittest.mock import AsyncMock + +from aiomealie import MealieAuthenticationError, MealieConnectionError +import pytest + +from homeassistant.components.mealie.const import DOMAIN +from homeassistant.config_entries import SOURCE_USER +from homeassistant.const import CONF_API_TOKEN, CONF_HOST +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +from tests.common import MockConfigEntry + + +async def test_full_flow( + hass: HomeAssistant, + mock_mealie_client: AsyncMock, + mock_setup_entry: AsyncMock, +) -> None: + """Test full flow.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_HOST: "demo.mealie.io", CONF_API_TOKEN: "token"}, + ) + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "Mealie" + assert result["data"] == { + CONF_HOST: "demo.mealie.io", + CONF_API_TOKEN: "token", + } + + +@pytest.mark.parametrize( + ("exception", "error"), + [ + (MealieConnectionError, "cannot_connect"), + (MealieAuthenticationError, "invalid_auth"), + (Exception, "unknown"), + ], +) +async def test_flow_errors( + hass: HomeAssistant, + mock_mealie_client: AsyncMock, + mock_setup_entry: AsyncMock, + exception: Exception, + error: str, +) -> None: + """Test flow errors.""" + mock_mealie_client.get_mealplan_today.side_effect = exception + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_HOST: "demo.mealie.io", CONF_API_TOKEN: "token"}, + ) + + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"base": error} + + mock_mealie_client.get_mealplan_today.side_effect = None + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_HOST: "demo.mealie.io", CONF_API_TOKEN: "token"}, + ) + await hass.async_block_till_done() + assert result["type"] is FlowResultType.CREATE_ENTRY + + +async def test_duplicate( + hass: HomeAssistant, + mock_mealie_client: AsyncMock, + mock_setup_entry: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test duplicate flow.""" + mock_config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_HOST: "demo.mealie.io", CONF_API_TOKEN: "token"}, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" diff --git a/tests/components/mealie/test_init.py b/tests/components/mealie/test_init.py new file mode 100644 index 00000000000..7d63ad135f9 --- /dev/null +++ b/tests/components/mealie/test_init.py @@ -0,0 +1,70 @@ +"""Tests for the Mealie integration.""" + +from unittest.mock import AsyncMock + +from aiomealie import MealieAuthenticationError, MealieConnectionError +import pytest +from syrupy import SnapshotAssertion + +from homeassistant.components.mealie.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr + +from . import setup_integration + +from tests.common import MockConfigEntry + + +async def test_device_info( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + mock_mealie_client: AsyncMock, + mock_config_entry: MockConfigEntry, + device_registry: dr.DeviceRegistry, +) -> None: + """Test device registry integration.""" + await setup_integration(hass, mock_config_entry) + device_entry = device_registry.async_get_device( + identifiers={(DOMAIN, mock_config_entry.entry_id)} + ) + assert device_entry is not None + assert device_entry == snapshot + + +async def test_load_unload_entry( + hass: HomeAssistant, + mock_mealie_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test load and unload entry.""" + await setup_integration(hass, mock_config_entry) + + assert mock_config_entry.state is ConfigEntryState.LOADED + + await hass.config_entries.async_remove(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert mock_config_entry.state is ConfigEntryState.NOT_LOADED + + +@pytest.mark.parametrize( + ("exc", "state"), + [ + (MealieConnectionError, ConfigEntryState.SETUP_RETRY), + (MealieAuthenticationError, ConfigEntryState.SETUP_ERROR), + ], +) +async def test_initialization_failure( + hass: HomeAssistant, + mock_mealie_client: AsyncMock, + mock_config_entry: MockConfigEntry, + exc: Exception, + state: ConfigEntryState, +) -> None: + """Test initialization failure.""" + mock_mealie_client.get_mealplans.side_effect = exc + + await setup_integration(hass, mock_config_entry) + + assert mock_config_entry.state is state From d2a5683fa0f39cae63a40086491b76cdf214ff8e Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 21 Jun 2024 11:07:30 +0200 Subject: [PATCH 0881/1445] Raise repair issues when automations can't be set up (#120010) --- .../components/automation/__init__.py | 49 ++++++++++-- homeassistant/components/automation/config.py | 72 +++++++++++++---- .../components/automation/strings.json | 23 ++++++ tests/components/automation/test_init.py | 78 ++++++++++++++++++- 4 files changed, 199 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index deb3613d668..5a53179cf2c 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -65,7 +65,11 @@ from homeassistant.helpers.deprecation import ( ) from homeassistant.helpers.entity import ToggleEntity from homeassistant.helpers.entity_component import EntityComponent -from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue +from homeassistant.helpers.issue_registry import ( + IssueSeverity, + async_create_issue, + async_delete_issue, +) from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.script import ( ATTR_CUR, @@ -98,7 +102,7 @@ from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass from homeassistant.util.dt import parse_datetime -from .config import AutomationConfig +from .config import AutomationConfig, ValidationStatus from .const import ( CONF_ACTION, CONF_INITIAL_STATE, @@ -426,11 +430,15 @@ class UnavailableAutomationEntity(BaseAutomationEntity): automation_id: str | None, name: str, raw_config: ConfigType | None, + validation_error: str, + validation_status: ValidationStatus, ) -> None: """Initialize an automation entity.""" self._attr_name = name self._attr_unique_id = automation_id self.raw_config = raw_config + self._validation_error = validation_error + self._validation_status = validation_status @cached_property def referenced_labels(self) -> set[str]: @@ -462,6 +470,30 @@ class UnavailableAutomationEntity(BaseAutomationEntity): """Return a set of referenced entities.""" return set() + async def async_added_to_hass(self) -> None: + """Create a repair issue to notify the user the automation has errors.""" + await super().async_added_to_hass() + async_create_issue( + self.hass, + DOMAIN, + f"{self.entity_id}_validation_{self._validation_status}", + is_fixable=False, + severity=IssueSeverity.ERROR, + translation_key=f"validation_{self._validation_status}", + translation_placeholders={ + "edit": f"/config/automation/edit/{self.unique_id}", + "entity_id": self.entity_id, + "error": self._validation_error, + "name": self._attr_name or self.entity_id, + }, + ) + + async def async_will_remove_from_hass(self) -> None: + await super().async_will_remove_from_hass() + async_delete_issue( + self.hass, DOMAIN, f"{self.entity_id}_validation_{self._validation_status}" + ) + async def async_trigger( self, run_variables: dict[str, Any], @@ -864,7 +896,8 @@ class AutomationEntityConfig: list_no: int raw_blueprint_inputs: ConfigType | None raw_config: ConfigType | None - validation_failed: bool + validation_error: str | None + validation_status: ValidationStatus async def _prepare_automation_config( @@ -884,14 +917,16 @@ async def _prepare_automation_config( raw_config = cast(AutomationConfig, config_block).raw_config raw_blueprint_inputs = cast(AutomationConfig, config_block).raw_blueprint_inputs - validation_failed = cast(AutomationConfig, config_block).validation_failed + validation_error = cast(AutomationConfig, config_block).validation_error + validation_status = cast(AutomationConfig, config_block).validation_status automation_configs.append( AutomationEntityConfig( config_block, list_no, raw_blueprint_inputs, raw_config, - validation_failed, + validation_error, + validation_status, ) ) @@ -917,12 +952,14 @@ async def _create_automation_entities( automation_id: str | None = config_block.get(CONF_ID) name = _automation_name(automation_config) - if automation_config.validation_failed: + if automation_config.validation_status != ValidationStatus.OK: entities.append( UnavailableAutomationEntity( automation_id, name, automation_config.raw_config, + cast(str, automation_config.validation_error), + automation_config.validation_status, ) ) continue diff --git a/homeassistant/components/automation/config.py b/homeassistant/components/automation/config.py index 71b4b3c0c6a..676aba946f4 100644 --- a/homeassistant/components/automation/config.py +++ b/homeassistant/components/automation/config.py @@ -4,7 +4,8 @@ from __future__ import annotations from collections.abc import Mapping from contextlib import suppress -from typing import Any +from enum import StrEnum +from typing import Any, cast import voluptuous as vol from voluptuous.humanize import humanize_error @@ -73,7 +74,7 @@ PLATFORM_SCHEMA = vol.All( ) -async def _async_validate_config_item( +async def _async_validate_config_item( # noqa: C901 hass: HomeAssistant, config: ConfigType, raise_on_errors: bool, @@ -86,6 +87,12 @@ async def _async_validate_config_item( with suppress(ValueError): raw_config = dict(config) + def _humanize(err: Exception, config: ConfigType) -> str: + """Humanize vol.Invalid, stringify other exceptions.""" + if isinstance(err, vol.Invalid): + return cast(str, humanize_error(config, err)) + return str(err) + def _log_invalid_automation( err: Exception, automation_name: str, @@ -101,7 +108,7 @@ async def _async_validate_config_item( "Blueprint '%s' generated invalid automation with inputs %s: %s", blueprint_inputs.blueprint.name, blueprint_inputs.inputs, - humanize_error(config, err) if isinstance(err, vol.Invalid) else err, + _humanize(err, config), ) return @@ -109,17 +116,35 @@ async def _async_validate_config_item( "%s %s and has been disabled: %s", automation_name, problem, - humanize_error(config, err) if isinstance(err, vol.Invalid) else err, + _humanize(err, config), ) return - def _minimal_config() -> AutomationConfig: + def _set_validation_status( + automation_config: AutomationConfig, + validation_status: ValidationStatus, + validation_error: Exception, + config: ConfigType, + ) -> None: + """Set validation status.""" + if uses_blueprint: + validation_status = ValidationStatus.FAILED_BLUEPRINT + automation_config.validation_status = validation_status + automation_config.validation_error = _humanize(validation_error, config) + + def _minimal_config( + validation_status: ValidationStatus, + validation_error: Exception, + config: ConfigType, + ) -> AutomationConfig: """Try validating id, alias and description.""" minimal_config = _MINIMAL_PLATFORM_SCHEMA(config) automation_config = AutomationConfig(minimal_config) automation_config.raw_blueprint_inputs = raw_blueprint_inputs automation_config.raw_config = raw_config - automation_config.validation_failed = True + _set_validation_status( + automation_config, validation_status, validation_error, config + ) return automation_config if blueprint.is_blueprint_instance_config(config): @@ -135,7 +160,7 @@ async def _async_validate_config_item( ) if raise_on_errors: raise - return _minimal_config() + return _minimal_config(ValidationStatus.FAILED_BLUEPRINT, err, config) raw_blueprint_inputs = blueprint_inputs.config_with_inputs @@ -152,7 +177,7 @@ async def _async_validate_config_item( ) if raise_on_errors: raise HomeAssistantError(err) from err - return _minimal_config() + return _minimal_config(ValidationStatus.FAILED_BLUEPRINT, err, config) automation_name = "Unnamed automation" if isinstance(config, Mapping): @@ -167,7 +192,7 @@ async def _async_validate_config_item( _log_invalid_automation(err, automation_name, "could not be validated", config) if raise_on_errors: raise - return _minimal_config() + return _minimal_config(ValidationStatus.FAILED_SCHEMA, err, config) automation_config = AutomationConfig(validated_config) automation_config.raw_blueprint_inputs = raw_blueprint_inputs @@ -186,7 +211,9 @@ async def _async_validate_config_item( ) if raise_on_errors: raise - automation_config.validation_failed = True + _set_validation_status( + automation_config, ValidationStatus.FAILED_TRIGGERS, err, validated_config + ) return automation_config if CONF_CONDITION in validated_config: @@ -203,7 +230,12 @@ async def _async_validate_config_item( ) if raise_on_errors: raise - automation_config.validation_failed = True + _set_validation_status( + automation_config, + ValidationStatus.FAILED_CONDITIONS, + err, + validated_config, + ) return automation_config try: @@ -219,18 +251,32 @@ async def _async_validate_config_item( ) if raise_on_errors: raise - automation_config.validation_failed = True + _set_validation_status( + automation_config, ValidationStatus.FAILED_ACTIONS, err, validated_config + ) return automation_config return automation_config +class ValidationStatus(StrEnum): + """What was changed in a config entry.""" + + FAILED_ACTIONS = "failed_actions" + FAILED_BLUEPRINT = "failed_blueprint" + FAILED_CONDITIONS = "failed_conditions" + FAILED_SCHEMA = "failed_schema" + FAILED_TRIGGERS = "failed_triggers" + OK = "ok" + + class AutomationConfig(dict): """Dummy class to allow adding attributes.""" raw_config: dict[str, Any] | None = None raw_blueprint_inputs: dict[str, Any] | None = None - validation_failed: bool = False + validation_status: ValidationStatus = ValidationStatus.OK + validation_error: str | None = None async def _try_async_validate_config_item( diff --git a/homeassistant/components/automation/strings.json b/homeassistant/components/automation/strings.json index 31bd812a947..c0750a38ca8 100644 --- a/homeassistant/components/automation/strings.json +++ b/homeassistant/components/automation/strings.json @@ -1,4 +1,7 @@ { + "common": { + "validation_failed_title": "Automation {name} failed to set up" + }, "title": "Automation", "entity_component": { "_": { @@ -43,6 +46,26 @@ } } } + }, + "validation_failed_actions": { + "title": "[%key:component::automation::common::validation_failed_title%]", + "description": "The automation \"{name}\" (`{entity_id}`) is not active because its actions could not be set up.\n\nError:`{error}`.\n\nTo fix this error, [edit the automation]({edit}) to correct it, then save and reload the automation configuration." + }, + "validation_failed_blueprint": { + "title": "[%key:component::automation::common::validation_failed_title%]", + "description": "The blueprinted automation \"{name}\" (`{entity_id}`) failed to set up.\n\nError:`{error}`.\n\nTo fix this error, [edit the automation]({edit}) to correct it, then save and reload the automation configuration." + }, + "validation_failed_conditions": { + "title": "[%key:component::automation::common::validation_failed_title%]", + "description": "The automation \"{name}\" (`{entity_id}`) is not active because its conditions could not be set up.\n\nError:`{error}`.\n\nTo fix this error, [edit the automation]({edit}) to correct it, then save and reload the automation configuration." + }, + "validation_failed_schema": { + "title": "[%key:component::automation::common::validation_failed_title%]", + "description": "The automation \"{name}\" (`{entity_id}`) is not active because the configuration has errors.\n\nError:`{error}`.\n\nTo fix this error, [edit the automation]({edit}) to correct it, then save and reload the automation configuration." + }, + "validation_failed_triggers": { + "title": "[%key:component::automation::common::validation_failed_title%]", + "description": "The automation \"{name}\" (`{entity_id}`) is not active because its triggers could not be set up.\n\nError:`{error}`.\n\nTo fix this error, [edit the automation]({edit}) to correct it, then save and reload the automation configuration." } }, "services": { diff --git a/tests/components/automation/test_init.py b/tests/components/automation/test_init.py index b4d9e45b7d3..7619589d52a 100644 --- a/tests/components/automation/test_init.py +++ b/tests/components/automation/test_init.py @@ -4,7 +4,7 @@ import asyncio from datetime import timedelta import logging from typing import Any -from unittest.mock import Mock, patch +from unittest.mock import ANY, Mock, patch import pytest @@ -1645,12 +1645,13 @@ async def test_automation_not_trigger_on_bootstrap(hass: HomeAssistant) -> None: @pytest.mark.parametrize( - ("broken_config", "problem", "details"), + ("broken_config", "problem", "details", "issue"), [ ( {}, "could not be validated", "required key not provided @ data['action']", + "validation_failed_schema", ), ( { @@ -1659,6 +1660,7 @@ async def test_automation_not_trigger_on_bootstrap(hass: HomeAssistant) -> None: }, "failed to setup triggers", "Integration 'automation' does not provide trigger support.", + "validation_failed_triggers", ), ( { @@ -1673,6 +1675,7 @@ async def test_automation_not_trigger_on_bootstrap(hass: HomeAssistant) -> None: }, "failed to setup conditions", "Unknown entity registry entry abcdabcdabcdabcdabcdabcdabcdabcd.", + "validation_failed_conditions", ), ( { @@ -1686,15 +1689,19 @@ async def test_automation_not_trigger_on_bootstrap(hass: HomeAssistant) -> None: }, "failed to setup actions", "Unknown entity registry entry abcdabcdabcdabcdabcdabcdabcdabcd.", + "validation_failed_actions", ), ], ) async def test_automation_bad_config_validation( hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, caplog: pytest.LogCaptureFixture, + hass_admin_user, broken_config, problem, details, + issue, ) -> None: """Test bad automation configuration which can be detected during validation.""" assert await async_setup_component( @@ -1715,11 +1722,22 @@ async def test_automation_bad_config_validation( }, ) - # Check we get the expected error message + # Check we get the expected error message and issue assert ( f"Automation with alias 'bad_automation' {problem} and has been disabled:" f" {details}" ) in caplog.text + issues = await get_repairs(hass, hass_ws_client) + assert len(issues) == 1 + assert issues[0]["issue_id"] == f"automation.bad_automation_{issue}" + assert issues[0]["translation_key"] == issue + assert issues[0]["translation_placeholders"] == { + "edit": "/config/automation/edit/None", + "entity_id": "automation.bad_automation", + "error": ANY, + "name": "bad_automation", + } + assert issues[0]["translation_placeholders"]["error"].startswith(details) # Make sure both automations are setup assert set(hass.states.async_entity_ids("automation")) == { @@ -1729,6 +1747,30 @@ async def test_automation_bad_config_validation( # The automation failing validation should be unavailable assert hass.states.get("automation.bad_automation").state == STATE_UNAVAILABLE + # Reloading the automation with fixed config should clear the issue + with patch( + "homeassistant.config.load_yaml_config_file", + autospec=True, + return_value={ + automation.DOMAIN: { + "alias": "bad_automation", + "trigger": {"platform": "event", "event_type": "test_event2"}, + "action": { + "service": "test.automation", + "data_template": {"event": "{{ trigger.event.event_type }}"}, + }, + } + }, + ): + await hass.services.async_call( + automation.DOMAIN, + SERVICE_RELOAD, + context=Context(user_id=hass_admin_user.id), + blocking=True, + ) + issues = await get_repairs(hass, hass_ws_client) + assert len(issues) == 0 + async def test_automation_with_error_in_script( hass: HomeAssistant, @@ -2507,6 +2549,7 @@ async def test_blueprint_automation( ) async def test_blueprint_automation_bad_config( hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, caplog: pytest.LogCaptureFixture, blueprint_inputs, problem, @@ -2528,9 +2571,24 @@ async def test_blueprint_automation_bad_config( assert problem in caplog.text assert details in caplog.text + issues = await get_repairs(hass, hass_ws_client) + assert len(issues) == 1 + issue = "validation_failed_blueprint" + assert issues[0]["issue_id"] == f"automation.automation_0_{issue}" + assert issues[0]["translation_key"] == issue + assert issues[0]["translation_placeholders"] == { + "edit": "/config/automation/edit/None", + "entity_id": "automation.automation_0", + "error": ANY, + "name": "automation 0", + } + assert issues[0]["translation_placeholders"]["error"].startswith(details) + async def test_blueprint_automation_fails_substitution( - hass: HomeAssistant, caplog: pytest.LogCaptureFixture + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + caplog: pytest.LogCaptureFixture, ) -> None: """Test blueprint automation with bad inputs.""" with patch( @@ -2559,6 +2617,18 @@ async def test_blueprint_automation_fails_substitution( " 'a_number': 5}: No substitution found for input blah" ) in caplog.text + issues = await get_repairs(hass, hass_ws_client) + assert len(issues) == 1 + issue = "validation_failed_blueprint" + assert issues[0]["issue_id"] == f"automation.automation_0_{issue}" + assert issues[0]["translation_key"] == issue + assert issues[0]["translation_placeholders"] == { + "edit": "/config/automation/edit/None", + "entity_id": "automation.automation_0", + "error": "No substitution found for input blah", + "name": "automation 0", + } + async def test_trigger_service(hass: HomeAssistant, calls: list[ServiceCall]) -> None: """Test the automation trigger service.""" From 818750dfd181752cf2c423dbed1eff25d5a98129 Mon Sep 17 00:00:00 2001 From: "Mr. Bubbles" Date: Fri, 21 Jun 2024 11:07:45 +0200 Subject: [PATCH 0882/1445] Add icons to One-Time Password (OTP) (#120066) --- homeassistant/components/otp/icons.json | 9 +++++++++ homeassistant/components/otp/sensor.py | 2 +- tests/components/otp/snapshots/test_sensor.ambr | 1 - 3 files changed, 10 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/otp/icons.json diff --git a/homeassistant/components/otp/icons.json b/homeassistant/components/otp/icons.json new file mode 100644 index 00000000000..1cab872e8f8 --- /dev/null +++ b/homeassistant/components/otp/icons.json @@ -0,0 +1,9 @@ +{ + "entity": { + "sensor": { + "token": { + "default": "mdi:lock-clock" + } + } + } +} diff --git a/homeassistant/components/otp/sensor.py b/homeassistant/components/otp/sensor.py index 0c87afb86b7..466fc994cdb 100644 --- a/homeassistant/components/otp/sensor.py +++ b/homeassistant/components/otp/sensor.py @@ -69,7 +69,7 @@ async def async_setup_entry( class TOTPSensor(SensorEntity): """Representation of a TOTP sensor.""" - _attr_icon = "mdi:update" + _attr_translation_key = "token" _attr_should_poll = False _attr_native_value: StateType = None _next_expiration: float | None = None diff --git a/tests/components/otp/snapshots/test_sensor.ambr b/tests/components/otp/snapshots/test_sensor.ambr index fbd8741b8b5..5329b03ad9e 100644 --- a/tests/components/otp/snapshots/test_sensor.ambr +++ b/tests/components/otp/snapshots/test_sensor.ambr @@ -3,7 +3,6 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'OTP Sensor', - 'icon': 'mdi:update', }), 'context': , 'entity_id': 'sensor.otp_sensor', From 0dd5391cd79b0a519725332d1762d8fdcc47a69d Mon Sep 17 00:00:00 2001 From: dontinelli <73341522+dontinelli@users.noreply.github.com> Date: Fri, 21 Jun 2024 11:10:15 +0200 Subject: [PATCH 0883/1445] Add Siemes and Millisiemens as additional units of conductivity and enable conversion between conductivity units (#118728) --- homeassistant/components/fyta/sensor.py | 10 +++++++-- homeassistant/components/mysensors/sensor.py | 4 ++-- homeassistant/components/number/const.py | 8 +++++++ homeassistant/components/plant/__init__.py | 4 ++-- .../components/recorder/statistics.py | 4 +++- .../components/recorder/websocket_api.py | 1 + homeassistant/components/sensor/const.py | 11 ++++++++++ .../components/sensor/device_condition.py | 3 +++ .../components/sensor/device_trigger.py | 3 +++ homeassistant/components/sensor/strings.json | 5 +++++ homeassistant/components/xiaomi_ble/sensor.py | 4 ++-- homeassistant/const.py | 15 ++++++++++++- homeassistant/util/unit_conversion.py | 14 ++++++++++++ tests/components/plant/test_init.py | 22 ++++++++++++++----- .../sensor/test_device_condition.py | 2 ++ .../components/sensor/test_device_trigger.py | 2 ++ tests/util/test_unit_conversion.py | 16 ++++++++++++++ 17 files changed, 112 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/fyta/sensor.py b/homeassistant/components/fyta/sensor.py index 3c7ed35746a..574b4e7b18e 100644 --- a/homeassistant/components/fyta/sensor.py +++ b/homeassistant/components/fyta/sensor.py @@ -16,7 +16,12 @@ from homeassistant.components.sensor import ( SensorStateClass, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import PERCENTAGE, EntityCategory, UnitOfTemperature +from homeassistant.const import ( + PERCENTAGE, + EntityCategory, + UnitOfConductivity, + UnitOfTemperature, +) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -105,7 +110,8 @@ SENSORS: Final[list[FytaSensorEntityDescription]] = [ FytaSensorEntityDescription( key="salinity", translation_key="salinity", - native_unit_of_measurement="mS/cm", + native_unit_of_measurement=UnitOfConductivity.MILLISIEMENS, + device_class=SensorDeviceClass.CONDUCTIVITY, state_class=SensorStateClass.MEASUREMENT, ), FytaSensorEntityDescription( diff --git a/homeassistant/components/mysensors/sensor.py b/homeassistant/components/mysensors/sensor.py index 537bf575af0..a6a91c12a81 100644 --- a/homeassistant/components/mysensors/sensor.py +++ b/homeassistant/components/mysensors/sensor.py @@ -15,12 +15,12 @@ from homeassistant.components.sensor import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( - CONDUCTIVITY, DEGREE, LIGHT_LUX, PERCENTAGE, Platform, UnitOfApparentPower, + UnitOfConductivity, UnitOfElectricCurrent, UnitOfElectricPotential, UnitOfEnergy, @@ -191,7 +191,7 @@ SENSORS: dict[str, SensorEntityDescription] = { ), "V_EC": SensorEntityDescription( key="V_EC", - native_unit_of_measurement=CONDUCTIVITY, + native_unit_of_measurement=UnitOfConductivity.MICROSIEMENS, ), "V_VAR": SensorEntityDescription( key="V_VAR", diff --git a/homeassistant/components/number/const.py b/homeassistant/components/number/const.py index f279ffb72a8..6343c3a599f 100644 --- a/homeassistant/components/number/const.py +++ b/homeassistant/components/number/const.py @@ -18,6 +18,7 @@ from homeassistant.const import ( SIGNAL_STRENGTH_DECIBELS, SIGNAL_STRENGTH_DECIBELS_MILLIWATT, UnitOfApparentPower, + UnitOfConductivity, UnitOfDataRate, UnitOfElectricCurrent, UnitOfElectricPotential, @@ -120,6 +121,12 @@ class NumberDeviceClass(StrEnum): Unit of measurement: `ppm` (parts per million) """ + CONDUCTIVITY = "conductivity" + """Conductivity. + + Unit of measurement: `S/cm`, `mS/cm`, `µS/cm` + """ + CURRENT = "current" """Current. @@ -424,6 +431,7 @@ DEVICE_CLASS_UNITS: dict[NumberDeviceClass, set[type[StrEnum] | str | None]] = { NumberDeviceClass.BATTERY: {PERCENTAGE}, NumberDeviceClass.CO: {CONCENTRATION_PARTS_PER_MILLION}, NumberDeviceClass.CO2: {CONCENTRATION_PARTS_PER_MILLION}, + NumberDeviceClass.CONDUCTIVITY: set(UnitOfConductivity), NumberDeviceClass.CURRENT: set(UnitOfElectricCurrent), NumberDeviceClass.DATA_RATE: set(UnitOfDataRate), NumberDeviceClass.DATA_SIZE: set(UnitOfInformation), diff --git a/homeassistant/components/plant/__init__.py b/homeassistant/components/plant/__init__.py index afce1207add..b549dee2887 100644 --- a/homeassistant/components/plant/__init__.py +++ b/homeassistant/components/plant/__init__.py @@ -10,7 +10,6 @@ import voluptuous as vol from homeassistant.components.recorder import get_instance, history from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, - CONDUCTIVITY, CONF_SENSORS, LIGHT_LUX, PERCENTAGE, @@ -18,6 +17,7 @@ from homeassistant.const import ( STATE_PROBLEM, STATE_UNAVAILABLE, STATE_UNKNOWN, + UnitOfConductivity, UnitOfTemperature, ) from homeassistant.core import ( @@ -148,7 +148,7 @@ class Plant(Entity): "max": CONF_MAX_MOISTURE, }, READING_CONDUCTIVITY: { - ATTR_UNIT_OF_MEASUREMENT: CONDUCTIVITY, + ATTR_UNIT_OF_MEASUREMENT: UnitOfConductivity.MICROSIEMENS, "min": CONF_MIN_CONDUCTIVITY, "max": CONF_MAX_CONDUCTIVITY, }, diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index e1178dea2a9..aeeb30816d7 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -28,6 +28,7 @@ from homeassistant.helpers.typing import UNDEFINED, UndefinedType from homeassistant.util import dt as dt_util from homeassistant.util.unit_conversion import ( BaseUnitConverter, + ConductivityConverter, DataRateConverter, DistanceConverter, DurationConverter, @@ -126,6 +127,7 @@ QUERY_STATISTICS_SUMMARY_SUM = ( STATISTIC_UNIT_TO_UNIT_CONVERTER: dict[str | None, type[BaseUnitConverter]] = { + **{unit: ConductivityConverter for unit in ConductivityConverter.VALID_UNITS}, **{unit: DataRateConverter for unit in DataRateConverter.VALID_UNITS}, **{unit: DistanceConverter for unit in DistanceConverter.VALID_UNITS}, **{unit: DurationConverter for unit in DurationConverter.VALID_UNITS}, @@ -154,7 +156,7 @@ def mean(values: list[float]) -> float | None: This is a very simple version that only works with a non-empty list of floats. The built-in - statistics.mean is more robust but is is almost + statistics.mean is more robust but is almost an order of magnitude slower. """ return sum(values) / len(values) diff --git a/homeassistant/components/recorder/websocket_api.py b/homeassistant/components/recorder/websocket_api.py index b091343e5a4..195d3d3efb0 100644 --- a/homeassistant/components/recorder/websocket_api.py +++ b/homeassistant/components/recorder/websocket_api.py @@ -48,6 +48,7 @@ from .util import PERIOD_SCHEMA, get_instance, resolve_period UNIT_SCHEMA = vol.Schema( { + vol.Optional("conductivity"): vol.In(DataRateConverter.VALID_UNITS), vol.Optional("data_rate"): vol.In(DataRateConverter.VALID_UNITS), vol.Optional("distance"): vol.In(DistanceConverter.VALID_UNITS), vol.Optional("duration"): vol.In(DurationConverter.VALID_UNITS), diff --git a/homeassistant/components/sensor/const.py b/homeassistant/components/sensor/const.py index cc89908f00d..5acf2ecef23 100644 --- a/homeassistant/components/sensor/const.py +++ b/homeassistant/components/sensor/const.py @@ -18,6 +18,7 @@ from homeassistant.const import ( SIGNAL_STRENGTH_DECIBELS, SIGNAL_STRENGTH_DECIBELS_MILLIWATT, UnitOfApparentPower, + UnitOfConductivity, UnitOfDataRate, UnitOfElectricCurrent, UnitOfElectricPotential, @@ -46,6 +47,7 @@ from homeassistant.helpers.deprecation import ( ) from homeassistant.util.unit_conversion import ( BaseUnitConverter, + ConductivityConverter, DataRateConverter, DistanceConverter, DurationConverter, @@ -137,6 +139,12 @@ class SensorDeviceClass(StrEnum): Unit of measurement: `ppm` (parts per million) """ + CONDUCTIVITY = "conductivity" + """Conductivity. + + Unit of measurement: `S/cm`, `mS/cm`, `µS/cm` + """ + CURRENT = "current" """Current. @@ -485,6 +493,7 @@ STATE_CLASSES: Final[list[str]] = [cls.value for cls in SensorStateClass] UNIT_CONVERTERS: dict[SensorDeviceClass | str | None, type[BaseUnitConverter]] = { SensorDeviceClass.ATMOSPHERIC_PRESSURE: PressureConverter, + SensorDeviceClass.CONDUCTIVITY: ConductivityConverter, SensorDeviceClass.CURRENT: ElectricCurrentConverter, SensorDeviceClass.DATA_RATE: DataRateConverter, SensorDeviceClass.DATA_SIZE: InformationConverter, @@ -517,6 +526,7 @@ DEVICE_CLASS_UNITS: dict[SensorDeviceClass, set[type[StrEnum] | str | None]] = { SensorDeviceClass.BATTERY: {PERCENTAGE}, SensorDeviceClass.CO: {CONCENTRATION_PARTS_PER_MILLION}, SensorDeviceClass.CO2: {CONCENTRATION_PARTS_PER_MILLION}, + SensorDeviceClass.CONDUCTIVITY: set(UnitOfConductivity), SensorDeviceClass.CURRENT: set(UnitOfElectricCurrent), SensorDeviceClass.DATA_RATE: set(UnitOfDataRate), SensorDeviceClass.DATA_SIZE: set(UnitOfInformation), @@ -591,6 +601,7 @@ DEVICE_CLASS_STATE_CLASSES: dict[SensorDeviceClass, set[SensorStateClass]] = { SensorDeviceClass.BATTERY: {SensorStateClass.MEASUREMENT}, SensorDeviceClass.CO: {SensorStateClass.MEASUREMENT}, SensorDeviceClass.CO2: {SensorStateClass.MEASUREMENT}, + SensorDeviceClass.CONDUCTIVITY: {SensorStateClass.MEASUREMENT}, SensorDeviceClass.CURRENT: {SensorStateClass.MEASUREMENT}, SensorDeviceClass.DATA_RATE: {SensorStateClass.MEASUREMENT}, SensorDeviceClass.DATA_SIZE: set(SensorStateClass), diff --git a/homeassistant/components/sensor/device_condition.py b/homeassistant/components/sensor/device_condition.py index fb605d9419c..21258db2ac5 100644 --- a/homeassistant/components/sensor/device_condition.py +++ b/homeassistant/components/sensor/device_condition.py @@ -41,6 +41,7 @@ CONF_IS_ATMOSPHERIC_PRESSURE = "is_atmospheric_pressure" CONF_IS_BATTERY_LEVEL = "is_battery_level" CONF_IS_CO = "is_carbon_monoxide" CONF_IS_CO2 = "is_carbon_dioxide" +CONF_IS_CONDUCTIVITY = "is_conductivity" CONF_IS_CURRENT = "is_current" CONF_IS_DATA_RATE = "is_data_rate" CONF_IS_DATA_SIZE = "is_data_size" @@ -90,6 +91,7 @@ ENTITY_CONDITIONS = { SensorDeviceClass.BATTERY: [{CONF_TYPE: CONF_IS_BATTERY_LEVEL}], SensorDeviceClass.CO: [{CONF_TYPE: CONF_IS_CO}], SensorDeviceClass.CO2: [{CONF_TYPE: CONF_IS_CO2}], + SensorDeviceClass.CONDUCTIVITY: [{CONF_TYPE: CONF_IS_CONDUCTIVITY}], SensorDeviceClass.CURRENT: [{CONF_TYPE: CONF_IS_CURRENT}], SensorDeviceClass.DATA_RATE: [{CONF_TYPE: CONF_IS_DATA_RATE}], SensorDeviceClass.DATA_SIZE: [{CONF_TYPE: CONF_IS_DATA_SIZE}], @@ -153,6 +155,7 @@ CONDITION_SCHEMA = vol.All( CONF_IS_BATTERY_LEVEL, CONF_IS_CO, CONF_IS_CO2, + CONF_IS_CONDUCTIVITY, CONF_IS_CURRENT, CONF_IS_DATA_RATE, CONF_IS_DATA_SIZE, diff --git a/homeassistant/components/sensor/device_trigger.py b/homeassistant/components/sensor/device_trigger.py index b46f6260285..0ffc42127bc 100644 --- a/homeassistant/components/sensor/device_trigger.py +++ b/homeassistant/components/sensor/device_trigger.py @@ -40,6 +40,7 @@ CONF_ATMOSPHERIC_PRESSURE = "atmospheric_pressure" CONF_BATTERY_LEVEL = "battery_level" CONF_CO = "carbon_monoxide" CONF_CO2 = "carbon_dioxide" +CONF_CONDUCTIVITY = "conductivity" CONF_CURRENT = "current" CONF_DATA_RATE = "data_rate" CONF_DATA_SIZE = "data_size" @@ -89,6 +90,7 @@ ENTITY_TRIGGERS = { SensorDeviceClass.BATTERY: [{CONF_TYPE: CONF_BATTERY_LEVEL}], SensorDeviceClass.CO: [{CONF_TYPE: CONF_CO}], SensorDeviceClass.CO2: [{CONF_TYPE: CONF_CO2}], + SensorDeviceClass.CONDUCTIVITY: [{CONF_TYPE: CONF_CONDUCTIVITY}], SensorDeviceClass.CURRENT: [{CONF_TYPE: CONF_CURRENT}], SensorDeviceClass.DATA_RATE: [{CONF_TYPE: CONF_DATA_RATE}], SensorDeviceClass.DATA_SIZE: [{CONF_TYPE: CONF_DATA_SIZE}], @@ -153,6 +155,7 @@ TRIGGER_SCHEMA = vol.All( CONF_BATTERY_LEVEL, CONF_CO, CONF_CO2, + CONF_CONDUCTIVITY, CONF_CURRENT, CONF_DATA_RATE, CONF_DATA_SIZE, diff --git a/homeassistant/components/sensor/strings.json b/homeassistant/components/sensor/strings.json index 101b32f373f..fc85f4b05a9 100644 --- a/homeassistant/components/sensor/strings.json +++ b/homeassistant/components/sensor/strings.json @@ -8,6 +8,7 @@ "is_battery_level": "Current {entity_name} battery level", "is_carbon_monoxide": "Current {entity_name} carbon monoxide concentration level", "is_carbon_dioxide": "Current {entity_name} carbon dioxide concentration level", + "is_conductivity": "Current {entity_name} conductivity", "is_current": "Current {entity_name} current", "is_data_rate": "Current {entity_name} data rate", "is_data_size": "Current {entity_name} data size", @@ -57,6 +58,7 @@ "battery_level": "{entity_name} battery level changes", "carbon_monoxide": "{entity_name} carbon monoxide concentration changes", "carbon_dioxide": "{entity_name} carbon dioxide concentration changes", + "conductivity": "{entity_name} conductivity changes", "current": "{entity_name} current changes", "data_rate": "{entity_name} data rate changes", "data_size": "{entity_name} data size changes", @@ -153,6 +155,9 @@ "carbon_dioxide": { "name": "Carbon dioxide" }, + "conductivity": { + "name": "Conductivity" + }, "current": { "name": "Current" }, diff --git a/homeassistant/components/xiaomi_ble/sensor.py b/homeassistant/components/xiaomi_ble/sensor.py index d107af8ef1b..65b33c3c559 100644 --- a/homeassistant/components/xiaomi_ble/sensor.py +++ b/homeassistant/components/xiaomi_ble/sensor.py @@ -20,11 +20,11 @@ from homeassistant.components.sensor import ( ) from homeassistant.const import ( CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER, - CONDUCTIVITY, LIGHT_LUX, PERCENTAGE, SIGNAL_STRENGTH_DECIBELS_MILLIWATT, EntityCategory, + UnitOfConductivity, UnitOfElectricPotential, UnitOfMass, UnitOfPressure, @@ -53,7 +53,7 @@ SENSOR_DESCRIPTIONS = { (DeviceClass.CONDUCTIVITY, Units.CONDUCTIVITY): SensorEntityDescription( key=str(Units.CONDUCTIVITY), device_class=None, - native_unit_of_measurement=CONDUCTIVITY, + native_unit_of_measurement=UnitOfConductivity.MICROSIEMENS, state_class=SensorStateClass.MEASUREMENT, ), ( diff --git a/homeassistant/const.py b/homeassistant/const.py index da059d4230d..577e8df6f39 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1129,8 +1129,21 @@ _DEPRECATED_MASS_POUNDS: Final = DeprecatedConstantEnum( ) """Deprecated: please use UnitOfMass.POUNDS""" + # Conductivity units -CONDUCTIVITY: Final = "µS/cm" +class UnitOfConductivity(StrEnum): + """Conductivity units.""" + + SIEMENS = "S/cm" + MICROSIEMENS = "µS/cm" + MILLISIEMENS = "mS/cm" + + +_DEPRECATED_CONDUCTIVITY: Final = DeprecatedConstantEnum( + UnitOfConductivity.MICROSIEMENS, + "2025.6", +) +"""Deprecated: please use UnitOfConductivity.MICROSIEMENS""" # Light units LIGHT_LUX: Final = "lx" diff --git a/homeassistant/util/unit_conversion.py b/homeassistant/util/unit_conversion.py index 04ce0715192..2b9f73afab7 100644 --- a/homeassistant/util/unit_conversion.py +++ b/homeassistant/util/unit_conversion.py @@ -10,6 +10,7 @@ from homeassistant.const import ( CONCENTRATION_PARTS_PER_MILLION, PERCENTAGE, UNIT_NOT_RECOGNIZED_TEMPLATE, + UnitOfConductivity, UnitOfDataRate, UnitOfElectricCurrent, UnitOfElectricPotential, @@ -169,6 +170,19 @@ class DistanceConverter(BaseUnitConverter): } +class ConductivityConverter(BaseUnitConverter): + """Utility to convert electric current values.""" + + UNIT_CLASS = "conductivity" + NORMALIZED_UNIT = UnitOfConductivity.MICROSIEMENS + _UNIT_CONVERSION: dict[str | None, float] = { + UnitOfConductivity.MICROSIEMENS: 1, + UnitOfConductivity.MILLISIEMENS: 1e-3, + UnitOfConductivity.SIEMENS: 1e-6, + } + VALID_UNITS = set(UnitOfConductivity) + + class ElectricCurrentConverter(BaseUnitConverter): """Utility to convert electric current values.""" diff --git a/tests/components/plant/test_init.py b/tests/components/plant/test_init.py index 97286a28cde..122ac3b75d1 100644 --- a/tests/components/plant/test_init.py +++ b/tests/components/plant/test_init.py @@ -6,11 +6,11 @@ from homeassistant.components import plant from homeassistant.components.recorder import Recorder from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, - CONDUCTIVITY, LIGHT_LUX, STATE_OK, STATE_PROBLEM, STATE_UNAVAILABLE, + UnitOfConductivity, ) from homeassistant.core import HomeAssistant, State from homeassistant.setup import async_setup_component @@ -79,7 +79,9 @@ async def test_low_battery(hass: HomeAssistant) -> None: async def test_initial_states(hass: HomeAssistant) -> None: """Test plant initialises attributes if sensor already exists.""" - hass.states.async_set(MOISTURE_ENTITY, 5, {ATTR_UNIT_OF_MEASUREMENT: CONDUCTIVITY}) + hass.states.async_set( + MOISTURE_ENTITY, 5, {ATTR_UNIT_OF_MEASUREMENT: UnitOfConductivity.MICROSIEMENS} + ) plant_name = "some_plant" assert await async_setup_component( hass, plant.DOMAIN, {plant.DOMAIN: {plant_name: GOOD_CONFIG}} @@ -98,7 +100,9 @@ async def test_update_states(hass: HomeAssistant) -> None: assert await async_setup_component( hass, plant.DOMAIN, {plant.DOMAIN: {plant_name: GOOD_CONFIG}} ) - hass.states.async_set(MOISTURE_ENTITY, 5, {ATTR_UNIT_OF_MEASUREMENT: CONDUCTIVITY}) + hass.states.async_set( + MOISTURE_ENTITY, 5, {ATTR_UNIT_OF_MEASUREMENT: UnitOfConductivity.MICROSIEMENS} + ) await hass.async_block_till_done() state = hass.states.get(f"plant.{plant_name}") assert state.state == STATE_PROBLEM @@ -115,7 +119,9 @@ async def test_unavailable_state(hass: HomeAssistant) -> None: hass, plant.DOMAIN, {plant.DOMAIN: {plant_name: GOOD_CONFIG}} ) hass.states.async_set( - MOISTURE_ENTITY, STATE_UNAVAILABLE, {ATTR_UNIT_OF_MEASUREMENT: CONDUCTIVITY} + MOISTURE_ENTITY, + STATE_UNAVAILABLE, + {ATTR_UNIT_OF_MEASUREMENT: UnitOfConductivity.MICROSIEMENS}, ) await hass.async_block_till_done() state = hass.states.get(f"plant.{plant_name}") @@ -132,13 +138,17 @@ async def test_state_problem_if_unavailable(hass: HomeAssistant) -> None: assert await async_setup_component( hass, plant.DOMAIN, {plant.DOMAIN: {plant_name: GOOD_CONFIG}} ) - hass.states.async_set(MOISTURE_ENTITY, 42, {ATTR_UNIT_OF_MEASUREMENT: CONDUCTIVITY}) + hass.states.async_set( + MOISTURE_ENTITY, 42, {ATTR_UNIT_OF_MEASUREMENT: UnitOfConductivity.MICROSIEMENS} + ) await hass.async_block_till_done() state = hass.states.get(f"plant.{plant_name}") assert state.state == STATE_OK assert state.attributes[plant.READING_MOISTURE] == 42 hass.states.async_set( - MOISTURE_ENTITY, STATE_UNAVAILABLE, {ATTR_UNIT_OF_MEASUREMENT: CONDUCTIVITY} + MOISTURE_ENTITY, + STATE_UNAVAILABLE, + {ATTR_UNIT_OF_MEASUREMENT: UnitOfConductivity.MICROSIEMENS}, ) await hass.async_block_till_done() state = hass.states.get(f"plant.{plant_name}") diff --git a/tests/components/sensor/test_device_condition.py b/tests/components/sensor/test_device_condition.py index 5f0646db8db..3bc9a660e93 100644 --- a/tests/components/sensor/test_device_condition.py +++ b/tests/components/sensor/test_device_condition.py @@ -58,6 +58,7 @@ def test_matches_device_classes(device_class: SensorDeviceClass) -> None: SensorDeviceClass.BATTERY: "CONF_IS_BATTERY_LEVEL", SensorDeviceClass.CO: "CONF_IS_CO", SensorDeviceClass.CO2: "CONF_IS_CO2", + SensorDeviceClass.CONDUCTIVITY: "CONF_IS_CONDUCTIVITY", SensorDeviceClass.ENERGY_STORAGE: "CONF_IS_ENERGY", SensorDeviceClass.VOLUME_STORAGE: "CONF_IS_VOLUME", }.get(device_class, f"CONF_IS_{device_class.value.upper()}") @@ -66,6 +67,7 @@ def test_matches_device_classes(device_class: SensorDeviceClass) -> None: # Ensure it has correct value constant_value = { SensorDeviceClass.BATTERY: "is_battery_level", + SensorDeviceClass.CONDUCTIVITY: "is_conductivity", SensorDeviceClass.ENERGY_STORAGE: "is_energy", SensorDeviceClass.VOLUME_STORAGE: "is_volume", }.get(device_class, f"is_{device_class.value}") diff --git a/tests/components/sensor/test_device_trigger.py b/tests/components/sensor/test_device_trigger.py index 71c844e428a..87a6d9929c3 100644 --- a/tests/components/sensor/test_device_trigger.py +++ b/tests/components/sensor/test_device_trigger.py @@ -62,6 +62,7 @@ def test_matches_device_classes(device_class: SensorDeviceClass) -> None: SensorDeviceClass.BATTERY: "CONF_BATTERY_LEVEL", SensorDeviceClass.CO: "CONF_CO", SensorDeviceClass.CO2: "CONF_CO2", + SensorDeviceClass.CONDUCTIVITY: "CONF_CONDUCTIVITY", SensorDeviceClass.ENERGY_STORAGE: "CONF_ENERGY", SensorDeviceClass.VOLUME_STORAGE: "CONF_VOLUME", }.get(device_class, f"CONF_{device_class.value.upper()}") @@ -70,6 +71,7 @@ def test_matches_device_classes(device_class: SensorDeviceClass) -> None: # Ensure it has correct value constant_value = { SensorDeviceClass.BATTERY: "battery_level", + SensorDeviceClass.CONDUCTIVITY: "conductivity", SensorDeviceClass.ENERGY_STORAGE: "energy", SensorDeviceClass.VOLUME_STORAGE: "volume", }.get(device_class, device_class.value) diff --git a/tests/util/test_unit_conversion.py b/tests/util/test_unit_conversion.py index efac252aa5f..98a6a1da5a6 100644 --- a/tests/util/test_unit_conversion.py +++ b/tests/util/test_unit_conversion.py @@ -11,6 +11,7 @@ from homeassistant.const import ( CONCENTRATION_PARTS_PER_BILLION, CONCENTRATION_PARTS_PER_MILLION, PERCENTAGE, + UnitOfConductivity, UnitOfDataRate, UnitOfElectricCurrent, UnitOfElectricPotential, @@ -31,6 +32,7 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.util import unit_conversion from homeassistant.util.unit_conversion import ( BaseUnitConverter, + ConductivityConverter, DataRateConverter, DistanceConverter, DurationConverter, @@ -57,6 +59,7 @@ INVALID_SYMBOL = "bob" _ALL_CONVERTERS: dict[type[BaseUnitConverter], list[str | None]] = { converter: sorted(converter.VALID_UNITS, key=lambda x: (x is None, x)) for converter in ( + ConductivityConverter, DataRateConverter, DistanceConverter, DurationConverter, @@ -77,6 +80,11 @@ _ALL_CONVERTERS: dict[type[BaseUnitConverter], list[str | None]] = { # Dict containing all converters with a corresponding unit ratio. _GET_UNIT_RATIO: dict[type[BaseUnitConverter], tuple[str | None, str | None, float]] = { + ConductivityConverter: ( + UnitOfConductivity.MICROSIEMENS, + UnitOfConductivity.MILLISIEMENS, + 1000, + ), DataRateConverter: ( UnitOfDataRate.BITS_PER_SECOND, UnitOfDataRate.BYTES_PER_SECOND, @@ -122,6 +130,14 @@ _GET_UNIT_RATIO: dict[type[BaseUnitConverter], tuple[str | None, str | None, flo _CONVERTED_VALUE: dict[ type[BaseUnitConverter], list[tuple[float, str | None, float, str | None]] ] = { + ConductivityConverter: [ + (5, UnitOfConductivity.SIEMENS, 5e3, UnitOfConductivity.MILLISIEMENS), + (5, UnitOfConductivity.SIEMENS, 5e6, UnitOfConductivity.MICROSIEMENS), + (5, UnitOfConductivity.MILLISIEMENS, 5e3, UnitOfConductivity.MICROSIEMENS), + (5, UnitOfConductivity.MILLISIEMENS, 5e-3, UnitOfConductivity.SIEMENS), + (5e6, UnitOfConductivity.MICROSIEMENS, 5e3, UnitOfConductivity.MILLISIEMENS), + (5e6, UnitOfConductivity.MICROSIEMENS, 5, UnitOfConductivity.SIEMENS), + ], DataRateConverter: [ (8e3, UnitOfDataRate.BITS_PER_SECOND, 8, UnitOfDataRate.KILOBITS_PER_SECOND), (8e6, UnitOfDataRate.BITS_PER_SECOND, 8, UnitOfDataRate.MEGABITS_PER_SECOND), From 7af79ba013b4423232066d6db957834f82545cd8 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 21 Jun 2024 11:11:48 +0200 Subject: [PATCH 0884/1445] Add MockModule type hints in tests (#120007) --- tests/common.py | 47 ++++++++++++++++++++++++++++++-------------- tests/test_loader.py | 12 +++++------ 2 files changed, 38 insertions(+), 21 deletions(-) diff --git a/tests/common.py b/tests/common.py index 851f91cfc3e..87a894bcb26 100644 --- a/tests/common.py +++ b/tests/common.py @@ -783,21 +783,38 @@ class MockModule: def __init__( self, - domain=None, - dependencies=None, - setup=None, - requirements=None, - config_schema=None, - platform_schema=None, - platform_schema_base=None, - async_setup=None, - async_setup_entry=None, - async_unload_entry=None, - async_migrate_entry=None, - async_remove_entry=None, - partial_manifest=None, - async_remove_config_entry_device=None, - ): + domain: str | None = None, + *, + dependencies: list[str] | None = None, + setup: Callable[[HomeAssistant, ConfigType], bool] | None = None, + requirements: list[str] | None = None, + config_schema: vol.Schema | None = None, + platform_schema: vol.Schema | None = None, + platform_schema_base: vol.Schema | None = None, + async_setup: Callable[[HomeAssistant, ConfigType], Coroutine[Any, Any, bool]] + | None = None, + async_setup_entry: Callable[ + [HomeAssistant, ConfigEntry], Coroutine[Any, Any, bool] + ] + | None = None, + async_unload_entry: Callable[ + [HomeAssistant, ConfigEntry], Coroutine[Any, Any, bool] + ] + | None = None, + async_migrate_entry: Callable[ + [HomeAssistant, ConfigEntry], Coroutine[Any, Any, bool] + ] + | None = None, + async_remove_entry: Callable[ + [HomeAssistant, ConfigEntry], Coroutine[Any, Any, None] + ] + | None = None, + partial_manifest: dict[str, Any] | None = None, + async_remove_config_entry_device: Callable[ + [HomeAssistant, ConfigEntry, dr.DeviceEntry], Coroutine[Any, Any, bool] + ] + | None = None, + ) -> None: """Initialize the mock module.""" self.__name__ = f"homeassistant.components.{domain}" self.__file__ = f"homeassistant/components/{domain}" diff --git a/tests/test_loader.py b/tests/test_loader.py index a45bec516f6..ae5280b2dcd 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -25,20 +25,20 @@ from .common import MockModule, async_get_persistent_notifications, mock_integra async def test_circular_component_dependencies(hass: HomeAssistant) -> None: """Test if we can detect circular dependencies of components.""" mock_integration(hass, MockModule("mod1")) - mock_integration(hass, MockModule("mod2", ["mod1"])) - mock_integration(hass, MockModule("mod3", ["mod1"])) - mod_4 = mock_integration(hass, MockModule("mod4", ["mod2", "mod3"])) + mock_integration(hass, MockModule("mod2", dependencies=["mod1"])) + mock_integration(hass, MockModule("mod3", dependencies=["mod1"])) + mod_4 = mock_integration(hass, MockModule("mod4", dependencies=["mod2", "mod3"])) deps = await loader._async_component_dependencies(hass, mod_4) assert deps == {"mod1", "mod2", "mod3", "mod4"} # Create a circular dependency - mock_integration(hass, MockModule("mod1", ["mod4"])) + mock_integration(hass, MockModule("mod1", dependencies=["mod4"])) with pytest.raises(loader.CircularDependency): await loader._async_component_dependencies(hass, mod_4) # Create a different circular dependency - mock_integration(hass, MockModule("mod1", ["mod3"])) + mock_integration(hass, MockModule("mod1", dependencies=["mod3"])) with pytest.raises(loader.CircularDependency): await loader._async_component_dependencies(hass, mod_4) @@ -59,7 +59,7 @@ async def test_circular_component_dependencies(hass: HomeAssistant) -> None: async def test_nonexistent_component_dependencies(hass: HomeAssistant) -> None: """Test if we can detect nonexistent dependencies of components.""" - mod_1 = mock_integration(hass, MockModule("mod1", ["nonexistent"])) + mod_1 = mock_integration(hass, MockModule("mod1", dependencies=["nonexistent"])) with pytest.raises(loader.IntegrationNotFound): await loader._async_component_dependencies(hass, mod_1) From d21908a0e48cb6f1dd8b1c60d5be98167863708a Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 21 Jun 2024 11:16:00 +0200 Subject: [PATCH 0885/1445] Add event entity to Nanoleaf (#120013) Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> --- .coveragerc | 1 + homeassistant/components/nanoleaf/__init__.py | 6 +- homeassistant/components/nanoleaf/event.py | 55 +++++++++++++++++++ homeassistant/components/nanoleaf/icons.json | 5 ++ .../components/nanoleaf/strings.json | 25 +++++++-- 5 files changed, 87 insertions(+), 5 deletions(-) create mode 100644 homeassistant/components/nanoleaf/event.py diff --git a/.coveragerc b/.coveragerc index 303f9696fe3..3b44334249d 100644 --- a/.coveragerc +++ b/.coveragerc @@ -855,6 +855,7 @@ omit = homeassistant/components/nanoleaf/button.py homeassistant/components/nanoleaf/coordinator.py homeassistant/components/nanoleaf/entity.py + homeassistant/components/nanoleaf/event.py homeassistant/components/nanoleaf/light.py homeassistant/components/neato/__init__.py homeassistant/components/neato/api.py diff --git a/homeassistant/components/nanoleaf/__init__.py b/homeassistant/components/nanoleaf/__init__.py index f607c7277ec..4a34c2843aa 100644 --- a/homeassistant/components/nanoleaf/__init__.py +++ b/homeassistant/components/nanoleaf/__init__.py @@ -19,13 +19,14 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.dispatcher import async_dispatcher_send from .const import DOMAIN, NANOLEAF_EVENT, TOUCH_GESTURE_TRIGGER_MAP, TOUCH_MODELS from .coordinator import NanoleafCoordinator _LOGGER = logging.getLogger(__name__) -PLATFORMS = [Platform.BUTTON, Platform.LIGHT] +PLATFORMS = [Platform.BUTTON, Platform.EVENT, Platform.LIGHT] type NanoleafConfigEntry = ConfigEntry[NanoleafCoordinator] @@ -65,6 +66,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: NanoleafConfigEntry) -> NANOLEAF_EVENT, {CONF_DEVICE_ID: device_entry.id, CONF_TYPE: gesture_type}, ) + async_dispatcher_send( + hass, f"nanoleaf_gesture_{nanoleaf.serial_no}", gesture_type + ) event_listener = asyncio.create_task( nanoleaf.listen_events( diff --git a/homeassistant/components/nanoleaf/event.py b/homeassistant/components/nanoleaf/event.py new file mode 100644 index 00000000000..5763c2aa595 --- /dev/null +++ b/homeassistant/components/nanoleaf/event.py @@ -0,0 +1,55 @@ +"""Support for Nanoleaf event entity.""" + +from homeassistant.components.event import EventEntity +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import NanoleafConfigEntry, NanoleafCoordinator +from .const import TOUCH_MODELS +from .entity import NanoleafEntity + + +async def async_setup_entry( + hass: HomeAssistant, + entry: NanoleafConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Nanoleaf event.""" + coordinator = entry.runtime_data + if coordinator.nanoleaf.model in TOUCH_MODELS: + async_add_entities([NanoleafGestureEvent(coordinator)]) + + +class NanoleafGestureEvent(NanoleafEntity, EventEntity): + """Representation of a Nanoleaf event entity.""" + + _attr_event_types = [ + "swipe_up", + "swipe_down", + "swipe_left", + "swipe_right", + ] + _attr_translation_key = "touch" + + def __init__(self, coordinator: NanoleafCoordinator) -> None: + """Initialize the Nanoleaf event entity.""" + super().__init__(coordinator) + self._attr_unique_id = f"{self._nanoleaf.serial_no}_gesture" + + async def async_added_to_hass(self) -> None: + """Subscribe to Nanoleaf events.""" + await super().async_added_to_hass() + self.async_on_remove( + async_dispatcher_connect( + self.hass, + f"nanoleaf_gesture_{self._nanoleaf.serial_no}", + self._async_handle_event, + ) + ) + + @callback + def _async_handle_event(self, gesture: str) -> None: + """Handle the event.""" + self._trigger_event(gesture) + self.async_write_ha_state() diff --git a/homeassistant/components/nanoleaf/icons.json b/homeassistant/components/nanoleaf/icons.json index 3f4ebf9ed9f..bedfc2f0718 100644 --- a/homeassistant/components/nanoleaf/icons.json +++ b/homeassistant/components/nanoleaf/icons.json @@ -1,5 +1,10 @@ { "entity": { + "event": { + "touch": { + "default": "mdi:gesture" + } + }, "light": { "light": { "default": "mdi:triangle-outline" diff --git a/homeassistant/components/nanoleaf/strings.json b/homeassistant/components/nanoleaf/strings.json index 13e7c9a11a3..40cd7294ec3 100644 --- a/homeassistant/components/nanoleaf/strings.json +++ b/homeassistant/components/nanoleaf/strings.json @@ -30,10 +30,27 @@ }, "device_automation": { "trigger_type": { - "swipe_up": "Swipe Up", - "swipe_down": "Swipe Down", - "swipe_left": "Swipe Left", - "swipe_right": "Swipe Right" + "swipe_up": "[%key:component::nanoleaf::entity::event::touch::state_attributes::event_type::state::swipe_up%]", + "swipe_down": "[%key:component::nanoleaf::entity::event::touch::state_attributes::event_type::state::swipe_down%]", + "swipe_left": "[%key:component::nanoleaf::entity::event::touch::state_attributes::event_type::state::swipe_left%]", + "swipe_right": "[%key:component::nanoleaf::entity::event::touch::state_attributes::event_type::state::swipe_right%]" + } + }, + "entity": { + "event": { + "touch": { + "name": "Touch gesture", + "state_attributes": { + "event_type": { + "state": { + "swipe_up": "Swipe up", + "swipe_down": "Swipe down", + "swipe_left": "Swipe left", + "swipe_right": "Swipe right" + } + } + } + } } } } From 9c5879656ce29db5d51a3dc71f1865046ec36f09 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 21 Jun 2024 11:18:51 +0200 Subject: [PATCH 0886/1445] Remove legacy list event calendar service (#118663) --- homeassistant/components/calendar/__init__.py | 35 ----------- tests/components/calendar/test_init.py | 58 +------------------ .../components/websocket_api/test_commands.py | 22 +++---- 3 files changed, 14 insertions(+), 101 deletions(-) diff --git a/homeassistant/components/calendar/__init__.py b/homeassistant/components/calendar/__init__.py index 47ea10b71b6..621356f20e2 100644 --- a/homeassistant/components/calendar/__init__.py +++ b/homeassistant/components/calendar/__init__.py @@ -38,7 +38,6 @@ from homeassistant.helpers.config_validation import ( # noqa: F401 from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.event import async_track_point_in_time -from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.template import DATE_STR_FORMAT from homeassistant.helpers.typing import ConfigType from homeassistant.util import dt as dt_util @@ -268,8 +267,6 @@ CALENDAR_EVENT_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) -LEGACY_SERVICE_LIST_EVENTS: Final = "list_events" -"""Deprecated: please use SERVICE_LIST_EVENTS.""" SERVICE_GET_EVENTS: Final = "get_events" SERVICE_GET_EVENTS_SCHEMA: Final = vol.All( cv.has_at_least_one_key(EVENT_END_DATETIME, EVENT_DURATION), @@ -309,12 +306,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async_create_event, required_features=[CalendarEntityFeature.CREATE_EVENT], ) - component.async_register_legacy_entity_service( - LEGACY_SERVICE_LIST_EVENTS, - SERVICE_GET_EVENTS_SCHEMA, - async_list_events_service, - supports_response=SupportsResponse.ONLY, - ) component.async_register_entity_service( SERVICE_GET_EVENTS, SERVICE_GET_EVENTS_SCHEMA, @@ -868,32 +859,6 @@ async def async_create_event(entity: CalendarEntity, call: ServiceCall) -> None: await entity.async_create_event(**params) -async def async_list_events_service( - calendar: CalendarEntity, service_call: ServiceCall -) -> ServiceResponse: - """List events on a calendar during a time range. - - Deprecated: please use async_get_events_service. - """ - _LOGGER.warning( - "Detected use of service 'calendar.list_events'. " - "This is deprecated and will stop working in Home Assistant 2024.6. " - "Use 'calendar.get_events' instead which supports multiple entities", - ) - async_create_issue( - calendar.hass, - DOMAIN, - "deprecated_service_calendar_list_events", - breaks_in_ha_version="2024.6.0", - is_fixable=True, - is_persistent=False, - issue_domain=calendar.platform.platform_name, - severity=IssueSeverity.WARNING, - translation_key="deprecated_service_calendar_list_events", - ) - return await async_get_events_service(calendar, service_call) - - async def async_get_events_service( calendar: CalendarEntity, service_call: ServiceCall ) -> ServiceResponse: diff --git a/tests/components/calendar/test_init.py b/tests/components/calendar/test_init.py index 19209574fa9..116ca70f15e 100644 --- a/tests/components/calendar/test_init.py +++ b/tests/components/calendar/test_init.py @@ -12,17 +12,12 @@ from syrupy.assertion import SnapshotAssertion from typing_extensions import Generator import voluptuous as vol -from homeassistant.components.calendar import ( - DOMAIN, - LEGACY_SERVICE_LIST_EVENTS, - SERVICE_GET_EVENTS, -) +from homeassistant.components.calendar import DOMAIN, SERVICE_GET_EVENTS from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import issue_registry as ir import homeassistant.util.dt as dt_util -from .conftest import TEST_DOMAIN, MockCalendarEntity, MockConfigEntry +from .conftest import MockCalendarEntity, MockConfigEntry from tests.typing import ClientSessionGenerator, WebSocketGenerator @@ -415,20 +410,6 @@ async def test_create_event_service_invalid_params( @pytest.mark.parametrize( ("service", "expected"), [ - ( - LEGACY_SERVICE_LIST_EVENTS, - { - "events": [ - { - "start": "2023-06-22T05:00:00-06:00", - "end": "2023-06-22T06:00:00-06:00", - "summary": "Future Event", - "description": "Future Description", - "location": "Future Location", - } - ] - }, - ), ( SERVICE_GET_EVENTS, { @@ -486,7 +467,6 @@ async def test_list_events_service( @pytest.mark.parametrize( ("service"), [ - (LEGACY_SERVICE_LIST_EVENTS), SERVICE_GET_EVENTS, ], ) @@ -568,37 +548,3 @@ async def test_list_events_missing_fields(hass: HomeAssistant) -> None: blocking=True, return_response=True, ) - - -async def test_issue_deprecated_service_calendar_list_events( - hass: HomeAssistant, - issue_registry: ir.IssueRegistry, - caplog: pytest.LogCaptureFixture, -) -> None: - """Test the issue is raised on deprecated service weather.get_forecast.""" - - _ = await hass.services.async_call( - DOMAIN, - LEGACY_SERVICE_LIST_EVENTS, - target={"entity_id": ["calendar.calendar_1"]}, - service_data={ - "entity_id": "calendar.calendar_1", - "duration": "01:00:00", - }, - blocking=True, - return_response=True, - ) - - issue = issue_registry.async_get_issue( - "calendar", "deprecated_service_calendar_list_events" - ) - assert issue - assert issue.issue_domain == TEST_DOMAIN - assert issue.issue_id == "deprecated_service_calendar_list_events" - assert issue.translation_key == "deprecated_service_calendar_list_events" - - assert ( - "Detected use of service 'calendar.list_events'. " - "This is deprecated and will stop working in Home Assistant 2024.6. " - "Use 'calendar.get_events' instead which supports multiple entities" - ) in caplog.text diff --git a/tests/components/websocket_api/test_commands.py b/tests/components/websocket_api/test_commands.py index a51e51b81b0..276a383d9e9 100644 --- a/tests/components/websocket_api/test_commands.py +++ b/tests/components/websocket_api/test_commands.py @@ -2391,7 +2391,7 @@ async def test_execute_script_complex_response( "type": "execute_script", "sequence": [ { - "service": "calendar.list_events", + "service": "calendar.get_events", "data": {"duration": {"hours": 24, "minutes": 0, "seconds": 0}}, "target": {"entity_id": "calendar.calendar_1"}, "response_variable": "service_result", @@ -2405,15 +2405,17 @@ async def test_execute_script_complex_response( assert msg_no_var["type"] == const.TYPE_RESULT assert msg_no_var["success"] assert msg_no_var["result"]["response"] == { - "events": [ - { - "start": ANY, - "end": ANY, - "summary": "Future Event", - "description": "Future Description", - "location": "Future Location", - } - ] + "calendar.calendar_1": { + "events": [ + { + "start": ANY, + "end": ANY, + "summary": "Future Event", + "description": "Future Description", + "location": "Future Location", + } + ] + } } From 64cef6e082dfac3e1fef050ebb237c53f1d97698 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Fri, 21 Jun 2024 05:28:44 -0400 Subject: [PATCH 0887/1445] Store runtime data inside the config entry in Litter Robot (#119547) --- .../components/litterrobot/__init__.py | 29 +++++++++---------- .../components/litterrobot/binary_sensor.py | 8 ++--- .../components/litterrobot/button.py | 8 ++--- .../components/litterrobot/select.py | 7 ++--- .../components/litterrobot/sensor.py | 8 ++--- .../components/litterrobot/switch.py | 8 ++--- homeassistant/components/litterrobot/time.py | 8 ++--- .../components/litterrobot/update.py | 9 +++--- .../components/litterrobot/vacuum.py | 8 ++--- tests/components/litterrobot/test_init.py | 2 -- 10 files changed, 38 insertions(+), 57 deletions(-) diff --git a/homeassistant/components/litterrobot/__init__.py b/homeassistant/components/litterrobot/__init__.py index ec9849bbb89..3c55c4c4035 100644 --- a/homeassistant/components/litterrobot/__init__.py +++ b/homeassistant/components/litterrobot/__init__.py @@ -12,6 +12,8 @@ from homeassistant.helpers.device_registry import DeviceEntry from .const import DOMAIN from .hub import LitterRobotHub +type LitterRobotConfigEntry = ConfigEntry[LitterRobotHub] + PLATFORMS_BY_TYPE = { Robot: ( Platform.BINARY_SENSOR, @@ -37,40 +39,35 @@ def get_platforms_for_robots(robots: list[Robot]) -> set[Platform]: } -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: LitterRobotConfigEntry) -> bool: """Set up Litter-Robot from a config entry.""" - hass.data.setdefault(DOMAIN, {}) - hub = hass.data[DOMAIN][entry.entry_id] = LitterRobotHub(hass, entry.data) + hub = LitterRobotHub(hass, entry.data) await hub.login(load_robots=True, subscribe_for_updates=True) + entry.runtime_data = hub if platforms := get_platforms_for_robots(hub.account.robots): await hass.config_entries.async_forward_entry_setups(entry, platforms) return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_unload_entry( + hass: HomeAssistant, entry: LitterRobotConfigEntry +) -> bool: """Unload a config entry.""" - hub: LitterRobotHub = hass.data[DOMAIN][entry.entry_id] - await hub.account.disconnect() + await entry.runtime_data.account.disconnect() - platforms = get_platforms_for_robots(hub.account.robots) - unload_ok = await hass.config_entries.async_unload_platforms(entry, platforms) - - if unload_ok: - hass.data[DOMAIN].pop(entry.entry_id) - - return unload_ok + platforms = get_platforms_for_robots(entry.runtime_data.account.robots) + return await hass.config_entries.async_unload_platforms(entry, platforms) async def async_remove_config_entry_device( - hass: HomeAssistant, config_entry: ConfigEntry, device_entry: DeviceEntry + hass: HomeAssistant, entry: LitterRobotConfigEntry, device_entry: DeviceEntry ) -> bool: """Remove a config entry from a device.""" - hub: LitterRobotHub = hass.data[DOMAIN][config_entry.entry_id] return not any( identifier for identifier in device_entry.identifiers if identifier[0] == DOMAIN - for robot in hub.account.robots + for robot in entry.runtime_data.account.robots if robot.serial == identifier[1] ) diff --git a/homeassistant/components/litterrobot/binary_sensor.py b/homeassistant/components/litterrobot/binary_sensor.py index 2f44f44ed53..91113d6c094 100644 --- a/homeassistant/components/litterrobot/binary_sensor.py +++ b/homeassistant/components/litterrobot/binary_sensor.py @@ -13,14 +13,12 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntity, BinarySensorEntityDescription, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN +from . import LitterRobotConfigEntry from .entity import LitterRobotEntity, _RobotT -from .hub import LitterRobotHub @dataclass(frozen=True) @@ -80,11 +78,11 @@ BINARY_SENSOR_MAP: dict[type[Robot], tuple[RobotBinarySensorEntityDescription, . async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: LitterRobotConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up Litter-Robot binary sensors using config entry.""" - hub: LitterRobotHub = hass.data[DOMAIN][entry.entry_id] + hub = entry.runtime_data async_add_entities( LitterRobotBinarySensorEntity(robot=robot, hub=hub, description=description) for robot in hub.account.robots diff --git a/homeassistant/components/litterrobot/button.py b/homeassistant/components/litterrobot/button.py index 02477e7fa03..6e6cc563c8e 100644 --- a/homeassistant/components/litterrobot/button.py +++ b/homeassistant/components/litterrobot/button.py @@ -10,23 +10,21 @@ from typing import Any, Generic from pylitterbot import FeederRobot, LitterRobot3 from homeassistant.components.button import ButtonEntity, ButtonEntityDescription -from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN +from . import LitterRobotConfigEntry from .entity import LitterRobotEntity, _RobotT -from .hub import LitterRobotHub async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: LitterRobotConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up Litter-Robot cleaner using config entry.""" - hub: LitterRobotHub = hass.data[DOMAIN][entry.entry_id] + hub = entry.runtime_data entities: list[LitterRobotButtonEntity] = list( itertools.chain( ( diff --git a/homeassistant/components/litterrobot/select.py b/homeassistant/components/litterrobot/select.py index e7ecbada10d..948fad45a76 100644 --- a/homeassistant/components/litterrobot/select.py +++ b/homeassistant/components/litterrobot/select.py @@ -10,12 +10,11 @@ from pylitterbot import FeederRobot, LitterRobot, LitterRobot4, Robot from pylitterbot.robot.litterrobot4 import BrightnessLevel from homeassistant.components.select import SelectEntity, SelectEntityDescription -from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory, UnitOfTime from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN +from . import LitterRobotConfigEntry from .entity import LitterRobotEntity, _RobotT from .hub import LitterRobotHub @@ -82,11 +81,11 @@ ROBOT_SELECT_MAP: dict[type[Robot], RobotSelectEntityDescription] = { async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + entry: LitterRobotConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up Litter-Robot selects using config entry.""" - hub: LitterRobotHub = hass.data[DOMAIN][config_entry.entry_id] + hub = entry.runtime_data entities = [ LitterRobotSelectEntity(robot=robot, hub=hub, description=description) for robot in hub.account.robots diff --git a/homeassistant/components/litterrobot/sensor.py b/homeassistant/components/litterrobot/sensor.py index 1b4b7f78fdc..c110b89c7da 100644 --- a/homeassistant/components/litterrobot/sensor.py +++ b/homeassistant/components/litterrobot/sensor.py @@ -15,14 +15,12 @@ from homeassistant.components.sensor import ( SensorEntityDescription, SensorStateClass, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import PERCENTAGE, EntityCategory, UnitOfMass from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN +from . import LitterRobotConfigEntry from .entity import LitterRobotEntity, _RobotT -from .hub import LitterRobotHub def icon_for_gauge_level(gauge_level: int | None = None, offset: int = 0) -> str: @@ -157,11 +155,11 @@ ROBOT_SENSOR_MAP: dict[type[Robot], list[RobotSensorEntityDescription]] = { async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: LitterRobotConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up Litter-Robot sensors using config entry.""" - hub: LitterRobotHub = hass.data[DOMAIN][entry.entry_id] + hub = entry.runtime_data entities = [ LitterRobotSensorEntity(robot=robot, hub=hub, description=description) for robot in hub.account.robots diff --git a/homeassistant/components/litterrobot/switch.py b/homeassistant/components/litterrobot/switch.py index 60ca9b4d6c7..133fd897cc6 100644 --- a/homeassistant/components/litterrobot/switch.py +++ b/homeassistant/components/litterrobot/switch.py @@ -9,14 +9,12 @@ from typing import Any, Generic from pylitterbot import FeederRobot, LitterRobot from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription -from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN +from . import LitterRobotConfigEntry from .entity import LitterRobotEntity, _RobotT -from .hub import LitterRobotHub @dataclass(frozen=True) @@ -68,11 +66,11 @@ class RobotSwitchEntity(LitterRobotEntity[_RobotT], SwitchEntity): async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: LitterRobotConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up Litter-Robot switches using config entry.""" - hub: LitterRobotHub = hass.data[DOMAIN][entry.entry_id] + hub = entry.runtime_data entities = [ RobotSwitchEntity(robot=robot, hub=hub, description=description) for description in ROBOT_SWITCHES diff --git a/homeassistant/components/litterrobot/time.py b/homeassistant/components/litterrobot/time.py index e2ada80b234..ace30d9f3a9 100644 --- a/homeassistant/components/litterrobot/time.py +++ b/homeassistant/components/litterrobot/time.py @@ -10,15 +10,13 @@ from typing import Any, Generic from pylitterbot import LitterRobot3 from homeassistant.components.time import TimeEntity, TimeEntityDescription -from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback import homeassistant.util.dt as dt_util -from .const import DOMAIN +from . import LitterRobotConfigEntry from .entity import LitterRobotEntity, _RobotT -from .hub import LitterRobotHub @dataclass(frozen=True) @@ -52,11 +50,11 @@ LITTER_ROBOT_3_SLEEP_START = RobotTimeEntityDescription[LitterRobot3]( async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: LitterRobotConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up Litter-Robot cleaner using config entry.""" - hub: LitterRobotHub = hass.data[DOMAIN][entry.entry_id] + hub = entry.runtime_data async_add_entities( [ LitterRobotTimeEntity( diff --git a/homeassistant/components/litterrobot/update.py b/homeassistant/components/litterrobot/update.py index c4d1ada6080..1d3e1dff57c 100644 --- a/homeassistant/components/litterrobot/update.py +++ b/homeassistant/components/litterrobot/update.py @@ -13,13 +13,12 @@ from homeassistant.components.update import ( UpdateEntityDescription, UpdateEntityFeature, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN -from .entity import LitterRobotEntity, LitterRobotHub +from . import LitterRobotConfigEntry +from .entity import LitterRobotEntity SCAN_INTERVAL = timedelta(days=1) @@ -31,11 +30,11 @@ FIRMWARE_UPDATE_ENTITY = UpdateEntityDescription( async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: LitterRobotConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up Litter-Robot update platform.""" - hub: LitterRobotHub = hass.data[DOMAIN][entry.entry_id] + hub = entry.runtime_data entities = [ RobotUpdateEntity(robot=robot, hub=hub, description=FIRMWARE_UPDATE_ENTITY) for robot in hub.litter_robots() diff --git a/homeassistant/components/litterrobot/vacuum.py b/homeassistant/components/litterrobot/vacuum.py index d752609d7de..a1ed2ea600d 100644 --- a/homeassistant/components/litterrobot/vacuum.py +++ b/homeassistant/components/litterrobot/vacuum.py @@ -18,16 +18,14 @@ from homeassistant.components.vacuum import ( StateVacuumEntityDescription, VacuumEntityFeature, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_OFF from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.entity_platform import AddEntitiesCallback import homeassistant.util.dt as dt_util -from .const import DOMAIN +from . import LitterRobotConfigEntry from .entity import LitterRobotEntity -from .hub import LitterRobotHub SERVICE_SET_SLEEP_MODE = "set_sleep_mode" @@ -51,11 +49,11 @@ LITTER_BOX_ENTITY = StateVacuumEntityDescription( async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: LitterRobotConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up Litter-Robot cleaner using config entry.""" - hub: LitterRobotHub = hass.data[DOMAIN][entry.entry_id] + hub = entry.runtime_data entities = [ LitterRobotCleaner(robot=robot, hub=hub, description=LITTER_BOX_ENTITY) for robot in hub.litter_robots() diff --git a/tests/components/litterrobot/test_init.py b/tests/components/litterrobot/test_init.py index f4ad12aeb20..21b16097603 100644 --- a/tests/components/litterrobot/test_init.py +++ b/tests/components/litterrobot/test_init.py @@ -41,8 +41,6 @@ async def test_unload_entry(hass: HomeAssistant, mock_account: MagicMock) -> Non getattr(mock_account.robots[0], "start_cleaning").assert_called_once() assert await hass.config_entries.async_unload(entry.entry_id) - await hass.async_block_till_done() - assert hass.data[litterrobot.DOMAIN] == {} @pytest.mark.parametrize( From fde7ddfa7148fef7e6373d636edbff4bce5355b8 Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Fri, 21 Jun 2024 19:30:57 +1000 Subject: [PATCH 0888/1445] Fix charge behavior in Tessie (#119546) --- homeassistant/components/tessie/entity.py | 2 +- homeassistant/components/tessie/switch.py | 43 ++++++++++++++------ tests/components/tessie/fixtures/online.json | 2 +- 3 files changed, 33 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/tessie/entity.py b/homeassistant/components/tessie/entity.py index e11a99348ed..35d41af32f2 100644 --- a/homeassistant/components/tessie/entity.py +++ b/homeassistant/components/tessie/entity.py @@ -46,7 +46,7 @@ class TessieEntity(CoordinatorEntity[TessieStateUpdateCoordinator]): @property def _value(self) -> Any: """Return value from coordinator data.""" - return self.coordinator.data[self.key] + return self.coordinator.data.get(self.key) def get(self, key: str | None = None, default: Any | None = None) -> Any: """Return a specific value from coordinator data.""" diff --git a/homeassistant/components/tessie/switch.py b/homeassistant/components/tessie/switch.py index 191d4f3ff5c..2f3902b3bd3 100644 --- a/homeassistant/components/tessie/switch.py +++ b/homeassistant/components/tessie/switch.py @@ -4,6 +4,7 @@ from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass +from itertools import chain from typing import Any from tessie_api import ( @@ -41,11 +42,6 @@ class TessieSwitchEntityDescription(SwitchEntityDescription): DESCRIPTIONS: tuple[TessieSwitchEntityDescription, ...] = ( - TessieSwitchEntityDescription( - key="charge_state_charge_enable_request", - on_func=lambda: start_charging, - off_func=lambda: stop_charging, - ), TessieSwitchEntityDescription( key="climate_state_defrost_mode", on_func=lambda: start_defrost, @@ -68,6 +64,12 @@ DESCRIPTIONS: tuple[TessieSwitchEntityDescription, ...] = ( ), ) +CHARGE_DESCRIPTION: TessieSwitchEntityDescription = TessieSwitchEntityDescription( + key="charge_state_charge_enable_request", + on_func=lambda: start_charging, + off_func=lambda: stop_charging, +) + async def async_setup_entry( hass: HomeAssistant, @@ -75,15 +77,20 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Tessie Switch platform from a config entry.""" - data = entry.runtime_data async_add_entities( - [ - TessieSwitchEntity(vehicle, description) - for vehicle in data.vehicles - for description in DESCRIPTIONS - if description.key in vehicle.data - ] + chain( + ( + TessieSwitchEntity(vehicle, description) + for vehicle in entry.runtime_data.vehicles + for description in DESCRIPTIONS + if description.key in vehicle.data + ), + ( + TessieChargeSwitchEntity(vehicle, CHARGE_DESCRIPTION) + for vehicle in entry.runtime_data.vehicles + ), + ) ) @@ -116,3 +123,15 @@ class TessieSwitchEntity(TessieEntity, SwitchEntity): """Turn off the Switch.""" await self.run(self.entity_description.off_func()) self.set((self.entity_description.key, False)) + + +class TessieChargeSwitchEntity(TessieSwitchEntity): + """Entity class for Tessie charge switch.""" + + @property + def is_on(self) -> bool: + """Return the state of the Switch.""" + + if (charge := self.get("charge_state_user_charge_enable_request")) is not None: + return charge + return self._value diff --git a/tests/components/tessie/fixtures/online.json b/tests/components/tessie/fixtures/online.json index 863e9bca783..ed49b4bfd75 100644 --- a/tests/components/tessie/fixtures/online.json +++ b/tests/components/tessie/fixtures/online.json @@ -68,7 +68,7 @@ "timestamp": 1701139037461, "trip_charging": false, "usable_battery_level": 75, - "user_charge_enable_request": null + "user_charge_enable_request": true }, "climate_state": { "allow_cabin_overheat_protection": true, From 5cdafba667723b5501adb8fcc33122e2fd5d9ba3 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 21 Jun 2024 11:43:53 +0200 Subject: [PATCH 0889/1445] Make attribute names in dnsip lowercase (for translation) (#119727) --- homeassistant/components/dnsip/sensor.py | 4 ++-- homeassistant/components/dnsip/strings.json | 17 +++++++++++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/dnsip/sensor.py b/homeassistant/components/dnsip/sensor.py index 726198e14cc..2f5e0582e21 100644 --- a/homeassistant/components/dnsip/sensor.py +++ b/homeassistant/components/dnsip/sensor.py @@ -89,8 +89,8 @@ class WanIpSensor(SensorEntity): self.querytype = "AAAA" if ipv6 else "A" self._retries = DEFAULT_RETRIES self._attr_extra_state_attributes = { - "Resolver": resolver, - "Querytype": self.querytype, + "resolver": resolver, + "querytype": self.querytype, } self._attr_device_info = DeviceInfo( entry_type=DeviceEntryType.SERVICE, diff --git a/homeassistant/components/dnsip/strings.json b/homeassistant/components/dnsip/strings.json index d8258a65d6a..bc502776cc6 100644 --- a/homeassistant/components/dnsip/strings.json +++ b/homeassistant/components/dnsip/strings.json @@ -32,5 +32,22 @@ "error": { "invalid_resolver": "Invalid IP address or port for resolver" } + }, + "entity": { + "sensor": { + "dnsip": { + "state_attributes": { + "resolver": { + "name": "Resolver" + }, + "querytype": { + "name": "Query type" + }, + "ip_addresses": { + "name": "IP addresses" + } + } + } + } } } From 55134e23ea780cc908998a0602af04b065074bd8 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 21 Jun 2024 11:56:52 +0200 Subject: [PATCH 0890/1445] Add type hints in automation tests (#120077) --- tests/components/automation/test_init.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/components/automation/test_init.py b/tests/components/automation/test_init.py index 7619589d52a..0c300540644 100644 --- a/tests/components/automation/test_init.py +++ b/tests/components/automation/test_init.py @@ -1697,11 +1697,11 @@ async def test_automation_bad_config_validation( hass: HomeAssistant, hass_ws_client: WebSocketGenerator, caplog: pytest.LogCaptureFixture, - hass_admin_user, - broken_config, - problem, - details, - issue, + hass_admin_user: MockUser, + broken_config: dict[str, Any], + problem: str, + details: str, + issue: str, ) -> None: """Test bad automation configuration which can be detected during validation.""" assert await async_setup_component( From ecd61c6b6d5839c636ed422dae272982c825e337 Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Fri, 21 Jun 2024 19:57:19 +1000 Subject: [PATCH 0891/1445] Add entities with no data in Tessie (#119550) --- homeassistant/components/tessie/binary_sensor.py | 1 - homeassistant/components/tessie/number.py | 1 - homeassistant/components/tessie/select.py | 2 +- 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/homeassistant/components/tessie/binary_sensor.py b/homeassistant/components/tessie/binary_sensor.py index b3f97cec380..2d3f1134444 100644 --- a/homeassistant/components/tessie/binary_sensor.py +++ b/homeassistant/components/tessie/binary_sensor.py @@ -170,7 +170,6 @@ async def async_setup_entry( TessieBinarySensorEntity(vehicle, description) for vehicle in data.vehicles for description in DESCRIPTIONS - if description.key in vehicle.data ) diff --git a/homeassistant/components/tessie/number.py b/homeassistant/components/tessie/number.py index 8cd93e10081..222922eba3e 100644 --- a/homeassistant/components/tessie/number.py +++ b/homeassistant/components/tessie/number.py @@ -91,7 +91,6 @@ async def async_setup_entry( TessieNumberEntity(vehicle, description) for vehicle in data.vehicles for description in DESCRIPTIONS - if description.key in vehicle.data ) diff --git a/homeassistant/components/tessie/select.py b/homeassistant/components/tessie/select.py index 5c939b1918e..801d465ea2a 100644 --- a/homeassistant/components/tessie/select.py +++ b/homeassistant/components/tessie/select.py @@ -35,7 +35,7 @@ async def async_setup_entry( TessieSeatHeaterSelectEntity(vehicle, key) for vehicle in data.vehicles for key in SEAT_HEATERS - if key in vehicle.data + if key in vehicle.data # not all vehicles have rear center or third row ) From c8ce935ec7e73b45bfc5b2f5dd960885dec54e4a Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Fri, 21 Jun 2024 11:57:48 +0200 Subject: [PATCH 0892/1445] Check Reolink IPC channels for firmware repair issue (#119241) * Add IPC channels to firmware repair issue * fix tests * fix typo --- homeassistant/components/reolink/__init__.py | 2 +- homeassistant/components/reolink/entity.py | 1 + homeassistant/components/reolink/host.py | 48 ++++++++++++-------- tests/components/reolink/test_init.py | 2 +- 4 files changed, 32 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/reolink/__init__.py b/homeassistant/components/reolink/__init__.py index e9b1d7e8c37..1d933a84ebd 100644 --- a/homeassistant/components/reolink/__init__.py +++ b/homeassistant/components/reolink/__init__.py @@ -228,7 +228,7 @@ def migrate_entity_ids( entity_reg = er.async_get(hass) entities = er.async_entries_for_config_entry(entity_reg, config_entry_id) for entity in entities: - # Can be remove in HA 2025.1.0 + # Can be removed in HA 2025.1.0 if entity.domain == "update" and entity.unique_id == host.unique_id: entity_reg.async_update_entity( entity.entity_id, new_unique_id=f"{host.unique_id}_firmware" diff --git a/homeassistant/components/reolink/entity.py b/homeassistant/components/reolink/entity.py index f722944a2fc..89c98ad0885 100644 --- a/homeassistant/components/reolink/entity.py +++ b/homeassistant/components/reolink/entity.py @@ -127,6 +127,7 @@ class ReolinkChannelCoordinatorEntity(ReolinkHostCoordinatorEntity): name=self._host.api.camera_name(dev_ch), model=self._host.api.camera_model(dev_ch), manufacturer=self._host.api.manufacturer, + hw_version=self._host.api.camera_hardware_version(dev_ch), sw_version=self._host.api.camera_sw_version(dev_ch), serial_number=self._host.api.camera_uid(dev_ch), configuration_url=self._conf_url, diff --git a/homeassistant/components/reolink/host.py b/homeassistant/components/reolink/host.py index 8256ef7f017..9836c5d7a01 100644 --- a/homeassistant/components/reolink/host.py +++ b/homeassistant/components/reolink/host.py @@ -237,25 +237,35 @@ class ReolinkHost: self._async_check_onvif_long_poll, ) - if self._api.sw_version_update_required: - ir.async_create_issue( - self._hass, - DOMAIN, - "firmware_update", - is_fixable=False, - severity=ir.IssueSeverity.WARNING, - translation_key="firmware_update", - translation_placeholders={ - "required_firmware": self._api.sw_version_required.version_string, - "current_firmware": self._api.sw_version, - "model": self._api.model, - "hw_version": self._api.hardware_version, - "name": self._api.nvr_name, - "download_link": "https://reolink.com/download-center/", - }, - ) - else: - ir.async_delete_issue(self._hass, DOMAIN, "firmware_update") + ch_list: list[int | None] = [None] + if self._api.is_nvr: + ch_list.extend(self._api.channels) + for ch in ch_list: + if not self._api.supported(ch, "firmware"): + continue + + key = ch if ch is not None else "host" + if self._api.camera_sw_version_update_required(ch): + ir.async_create_issue( + self._hass, + DOMAIN, + f"firmware_update_{key}", + is_fixable=False, + severity=ir.IssueSeverity.WARNING, + translation_key="firmware_update", + translation_placeholders={ + "required_firmware": self._api.camera_sw_version_required( + ch + ).version_string, + "current_firmware": self._api.camera_sw_version(ch), + "model": self._api.camera_model(ch), + "hw_version": self._api.camera_hardware_version(ch), + "name": self._api.camera_name(ch), + "download_link": "https://reolink.com/download-center/", + }, + ) + else: + ir.async_delete_issue(self._hass, DOMAIN, f"firmware_update_{key}") async def _async_check_onvif(self, *_) -> None: """Check the ONVIF subscription.""" diff --git a/tests/components/reolink/test_init.py b/tests/components/reolink/test_init.py index 3cca1831a28..db6069b097c 100644 --- a/tests/components/reolink/test_init.py +++ b/tests/components/reolink/test_init.py @@ -330,4 +330,4 @@ async def test_firmware_repair_issue( assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert (const.DOMAIN, "firmware_update") in issue_registry.issues + assert (const.DOMAIN, "firmware_update_host") in issue_registry.issues From 15e52de7e97217de46b08cf5a43de30d4a61edbd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 21 Jun 2024 04:58:45 -0500 Subject: [PATCH 0893/1445] Avoid constructing unifiprotect enabled callable when unused (#120074) --- homeassistant/components/unifiprotect/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/unifiprotect/models.py b/homeassistant/components/unifiprotect/models.py index 3bd2416b550..bbd125b4085 100644 --- a/homeassistant/components/unifiprotect/models.py +++ b/homeassistant/components/unifiprotect/models.py @@ -46,7 +46,7 @@ class ProtectEntityDescription(EntityDescription, Generic[T]): # The below are set in __post_init__ has_required: Callable[[T], bool] = bool - get_ufp_enabled: Callable[[T], bool] = bool + get_ufp_enabled: Callable[[T], bool] | None = None def get_ufp_value(self, obj: T) -> Any: """Return value from UniFi Protect device; overridden in __post_init__.""" From 5bbc4c80c565f69bbeb713ddb52472df9a3cc364 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 21 Jun 2024 12:12:15 +0200 Subject: [PATCH 0894/1445] Adjust CI job for Check pylint on tests (#120080) * Adjust Check pylint on tests CI job * Apply suggestion Co-authored-by: Robert Resch --------- Co-authored-by: Robert Resch --- .github/workflows/ci.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 53a0454c7c5..232ffb424aa 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -625,8 +625,8 @@ jobs: runs-on: ubuntu-22.04 timeout-minutes: 20 if: | - github.event.inputs.mypy-only != 'true' - || github.event.inputs.pylint-only == 'true' + (github.event.inputs.mypy-only != 'true' || github.event.inputs.pylint-only == 'true') + && (needs.info.outputs.tests_glob || needs.info.outputs.test_full_suite == 'true') needs: - info - base @@ -663,7 +663,7 @@ jobs: run: | . venv/bin/activate python --version - pylint --ignore-missing-annotations=y tests/components/${{ needs.info.outputs.integrations_glob }} + pylint --ignore-missing-annotations=y tests/components/${{ needs.info.outputs.tests_glob }} mypy: name: Check mypy From 79bc6fc1a8fc0abd92c4bf9d0c6667fd40366c10 Mon Sep 17 00:00:00 2001 From: "Mr. Bubbles" Date: Fri, 21 Jun 2024 12:14:44 +0200 Subject: [PATCH 0895/1445] Bump pyecotrend-ista to 3.3.1 (#120037) --- .../components/ista_ecotrend/__init__.py | 11 ++----- .../components/ista_ecotrend/config_flow.py | 19 +++++------- .../components/ista_ecotrend/coordinator.py | 29 ++++--------------- .../components/ista_ecotrend/manifest.json | 3 +- .../components/ista_ecotrend/sensor.py | 3 ++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/ista_ecotrend/conftest.py | 16 +++++----- .../ista_ecotrend/test_config_flow.py | 2 +- tests/components/ista_ecotrend/test_init.py | 17 ++--------- tests/components/ista_ecotrend/test_util.py | 6 ++-- 11 files changed, 39 insertions(+), 71 deletions(-) diff --git a/homeassistant/components/ista_ecotrend/__init__.py b/homeassistant/components/ista_ecotrend/__init__.py index e1be000ccc4..5c1099f9d67 100644 --- a/homeassistant/components/ista_ecotrend/__init__.py +++ b/homeassistant/components/ista_ecotrend/__init__.py @@ -4,14 +4,7 @@ from __future__ import annotations import logging -from pyecotrend_ista.exception_classes import ( - InternalServerError, - KeycloakError, - LoginError, - ServerError, -) -from pyecotrend_ista.pyecotrend_ista import PyEcotrendIsta -from requests.exceptions import RequestException +from pyecotrend_ista import KeycloakError, LoginError, PyEcotrendIsta, ServerError from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, Platform @@ -37,7 +30,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: IstaConfigEntry) -> bool ) try: await hass.async_add_executor_job(ista.login) - except (ServerError, InternalServerError, RequestException, TimeoutError) as e: + except ServerError as e: raise ConfigEntryNotReady( translation_domain=DOMAIN, translation_key="connection_exception", diff --git a/homeassistant/components/ista_ecotrend/config_flow.py b/homeassistant/components/ista_ecotrend/config_flow.py index 0bf1685eff4..86696950484 100644 --- a/homeassistant/components/ista_ecotrend/config_flow.py +++ b/homeassistant/components/ista_ecotrend/config_flow.py @@ -3,15 +3,9 @@ from __future__ import annotations import logging -from typing import Any +from typing import TYPE_CHECKING, Any -from pyecotrend_ista.exception_classes import ( - InternalServerError, - KeycloakError, - LoginError, - ServerError, -) -from pyecotrend_ista.pyecotrend_ista import PyEcotrendIsta +from pyecotrend_ista import KeycloakError, LoginError, PyEcotrendIsta, ServerError import voluptuous as vol from homeassistant.config_entries import ConfigFlow, ConfigFlowResult @@ -60,7 +54,8 @@ class IstaConfigFlow(ConfigFlow, domain=DOMAIN): ) try: await self.hass.async_add_executor_job(ista.login) - except (ServerError, InternalServerError): + info = ista.get_account() + except ServerError: errors["base"] = "cannot_connect" except (LoginError, KeycloakError): errors["base"] = "invalid_auth" @@ -68,8 +63,10 @@ class IstaConfigFlow(ConfigFlow, domain=DOMAIN): _LOGGER.exception("Unexpected exception") errors["base"] = "unknown" else: - title = f"{ista._a_firstName} {ista._a_lastName}".strip() # noqa: SLF001 - await self.async_set_unique_id(ista._uuid) # noqa: SLF001 + if TYPE_CHECKING: + assert info + title = f"{info["firstName"]} {info["lastName"]}".strip() + await self.async_set_unique_id(info["activeConsumptionUnit"]) self._abort_if_unique_id_configured() return self.async_create_entry( title=title or "ista EcoTrend", data=user_input diff --git a/homeassistant/components/ista_ecotrend/coordinator.py b/homeassistant/components/ista_ecotrend/coordinator.py index 78a31d560dd..b3be5883136 100644 --- a/homeassistant/components/ista_ecotrend/coordinator.py +++ b/homeassistant/components/ista_ecotrend/coordinator.py @@ -6,14 +6,7 @@ from datetime import timedelta import logging from typing import Any -from pyecotrend_ista.exception_classes import ( - InternalServerError, - KeycloakError, - LoginError, - ServerError, -) -from pyecotrend_ista.pyecotrend_ista import PyEcotrendIsta -from requests.exceptions import RequestException +from pyecotrend_ista import KeycloakError, LoginError, PyEcotrendIsta, ServerError from homeassistant.const import CONF_EMAIL from homeassistant.core import HomeAssistant @@ -47,12 +40,7 @@ class IstaCoordinator(DataUpdateCoordinator[dict[str, Any]]): try: return await self.hass.async_add_executor_job(self.get_consumption_data) - except ( - ServerError, - InternalServerError, - RequestException, - TimeoutError, - ) as e: + except ServerError as e: raise UpdateFailed( "Unable to connect and retrieve data from ista EcoTrend, try again later" ) from e @@ -67,8 +55,8 @@ class IstaCoordinator(DataUpdateCoordinator[dict[str, Any]]): """Get raw json data for all consumption units.""" return { - consumption_unit: self.ista.get_raw(consumption_unit) - for consumption_unit in self.ista.getUUIDs() + consumption_unit: self.ista.get_consumption_data(consumption_unit) + for consumption_unit in self.ista.get_uuids() } async def async_get_details(self) -> dict[str, Any]: @@ -77,12 +65,7 @@ class IstaCoordinator(DataUpdateCoordinator[dict[str, Any]]): result = await self.hass.async_add_executor_job( self.ista.get_consumption_unit_details ) - except ( - ServerError, - InternalServerError, - RequestException, - TimeoutError, - ) as e: + except ServerError as e: raise UpdateFailed( "Unable to connect and retrieve data from ista EcoTrend, try again later" ) from e @@ -99,5 +82,5 @@ class IstaCoordinator(DataUpdateCoordinator[dict[str, Any]]): for details in result["consumptionUnits"] if details["id"] == consumption_unit ) - for consumption_unit in self.ista.getUUIDs() + for consumption_unit in self.ista.get_uuids() } diff --git a/homeassistant/components/ista_ecotrend/manifest.json b/homeassistant/components/ista_ecotrend/manifest.json index 497d3d4a984..23d60a0a5bb 100644 --- a/homeassistant/components/ista_ecotrend/manifest.json +++ b/homeassistant/components/ista_ecotrend/manifest.json @@ -5,5 +5,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ista_ecotrend", "iot_class": "cloud_polling", - "requirements": ["pyecotrend-ista==3.2.0"] + "loggers": ["pyecotrend_ista"], + "requirements": ["pyecotrend-ista==3.3.1"] } diff --git a/homeassistant/components/ista_ecotrend/sensor.py b/homeassistant/components/ista_ecotrend/sensor.py index 844b86e1689..c50f322c356 100644 --- a/homeassistant/components/ista_ecotrend/sensor.py +++ b/homeassistant/components/ista_ecotrend/sensor.py @@ -4,6 +4,7 @@ from __future__ import annotations from dataclasses import dataclass from enum import StrEnum +import logging from homeassistant.components.sensor import ( SensorDeviceClass, @@ -23,6 +24,8 @@ from .const import DOMAIN from .coordinator import IstaCoordinator from .util import IstaConsumptionType, IstaValueType, get_native_value +_LOGGER = logging.getLogger(__name__) + @dataclass(kw_only=True, frozen=True) class IstaSensorEntityDescription(SensorEntityDescription): diff --git a/requirements_all.txt b/requirements_all.txt index f949d864f54..c1568ac6056 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1815,7 +1815,7 @@ pyecoforest==0.4.0 pyeconet==0.1.22 # homeassistant.components.ista_ecotrend -pyecotrend-ista==3.2.0 +pyecotrend-ista==3.3.1 # homeassistant.components.edimax pyedimax==0.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index face667ccc5..7995497972c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1429,7 +1429,7 @@ pyecoforest==0.4.0 pyeconet==0.1.22 # homeassistant.components.ista_ecotrend -pyecotrend-ista==3.2.0 +pyecotrend-ista==3.3.1 # homeassistant.components.efergy pyefergy==22.5.0 diff --git a/tests/components/ista_ecotrend/conftest.py b/tests/components/ista_ecotrend/conftest.py index a9eee5cd026..2218ef05ba7 100644 --- a/tests/components/ista_ecotrend/conftest.py +++ b/tests/components/ista_ecotrend/conftest.py @@ -53,9 +53,11 @@ def mock_ista() -> Generator[MagicMock]: ), ): client = mock_client.return_value - client._uuid = "26e93f1a-c828-11ea-87d0-0242ac130003" - client._a_firstName = "Max" - client._a_lastName = "Istamann" + client.get_account.return_value = { + "firstName": "Max", + "lastName": "Istamann", + "activeConsumptionUnit": "26e93f1a-c828-11ea-87d0-0242ac130003", + } client.get_consumption_unit_details.return_value = { "consumptionUnits": [ { @@ -74,17 +76,17 @@ def mock_ista() -> Generator[MagicMock]: }, ] } - client.getUUIDs.return_value = [ + client.get_uuids.return_value = [ "26e93f1a-c828-11ea-87d0-0242ac130003", "eaf5c5c8-889f-4a3c-b68c-e9a676505762", ] - client.get_raw = get_raw + client.get_consumption_data = get_consumption_data yield client -def get_raw(obj_uuid: str | None = None) -> dict[str, Any]: - """Mock function get_raw.""" +def get_consumption_data(obj_uuid: str | None = None) -> dict[str, Any]: + """Mock function get_consumption_data.""" return { "consumptionUnitId": obj_uuid, "consumptions": [ diff --git a/tests/components/ista_ecotrend/test_config_flow.py b/tests/components/ista_ecotrend/test_config_flow.py index e465d85e517..3375394f3f6 100644 --- a/tests/components/ista_ecotrend/test_config_flow.py +++ b/tests/components/ista_ecotrend/test_config_flow.py @@ -2,7 +2,7 @@ from unittest.mock import AsyncMock, MagicMock -from pyecotrend_ista.exception_classes import LoginError, ServerError +from pyecotrend_ista import LoginError, ServerError import pytest from homeassistant.components.ista_ecotrend.const import DOMAIN diff --git a/tests/components/ista_ecotrend/test_init.py b/tests/components/ista_ecotrend/test_init.py index 13b17333bbe..642afc820dd 100644 --- a/tests/components/ista_ecotrend/test_init.py +++ b/tests/components/ista_ecotrend/test_init.py @@ -2,14 +2,8 @@ from unittest.mock import MagicMock -from pyecotrend_ista.exception_classes import ( - InternalServerError, - KeycloakError, - LoginError, - ServerError, -) +from pyecotrend_ista import KeycloakError, LoginError, ParserError, ServerError import pytest -from requests.exceptions import RequestException from syrupy.assertion import SnapshotAssertion from homeassistant.config_entries import ConfigEntryState @@ -39,12 +33,7 @@ async def test_entry_setup_unload( @pytest.mark.parametrize( ("side_effect"), - [ - ServerError, - InternalServerError(None), - RequestException, - TimeoutError, - ], + [ServerError, ParserError], ) async def test_config_entry_not_ready( hass: HomeAssistant, @@ -63,7 +52,7 @@ async def test_config_entry_not_ready( @pytest.mark.parametrize( ("side_effect"), - [LoginError(None), KeycloakError], + [LoginError, KeycloakError], ) async def test_config_entry_error( hass: HomeAssistant, diff --git a/tests/components/ista_ecotrend/test_util.py b/tests/components/ista_ecotrend/test_util.py index e2e799aa78b..616abdea8d6 100644 --- a/tests/components/ista_ecotrend/test_util.py +++ b/tests/components/ista_ecotrend/test_util.py @@ -12,7 +12,7 @@ from homeassistant.components.ista_ecotrend.util import ( last_day_of_month, ) -from .conftest import get_raw +from .conftest import get_consumption_data def test_as_number() -> None: @@ -86,7 +86,7 @@ def test_get_values_by_type(snapshot: SnapshotAssertion) -> None: def test_get_native_value() -> None: """Test getting native value for sensor states.""" - test_data = get_raw("26e93f1a-c828-11ea-87d0-0242ac130003") + test_data = get_consumption_data("26e93f1a-c828-11ea-87d0-0242ac130003") assert get_native_value(test_data, IstaConsumptionType.HEATING) == 35 assert get_native_value(test_data, IstaConsumptionType.HOT_WATER) == 1.0 @@ -123,7 +123,7 @@ def test_get_native_value() -> None: def test_get_statistics(snapshot: SnapshotAssertion) -> None: """Test get_statistics function.""" - test_data = get_raw("26e93f1a-c828-11ea-87d0-0242ac130003") + test_data = get_consumption_data("26e93f1a-c828-11ea-87d0-0242ac130003") for consumption_type in IstaConsumptionType: assert get_statistics(test_data, consumption_type) == snapshot assert get_statistics({"consumptions": None}, consumption_type) is None From 2157d0c05e5cef7a303e3ef749020a6d7cefedf3 Mon Sep 17 00:00:00 2001 From: Max Gross Date: Fri, 21 Jun 2024 05:16:13 -0500 Subject: [PATCH 0896/1445] Fix unit of measurement for Comed Hourly Pricing (#115594) Co-authored-by: Robert Resch --- homeassistant/components/comed_hourly_pricing/sensor.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/comed_hourly_pricing/sensor.py b/homeassistant/components/comed_hourly_pricing/sensor.py index 5d30387a9cb..770866aa319 100644 --- a/homeassistant/components/comed_hourly_pricing/sensor.py +++ b/homeassistant/components/comed_hourly_pricing/sensor.py @@ -15,7 +15,7 @@ from homeassistant.components.sensor import ( SensorEntity, SensorEntityDescription, ) -from homeassistant.const import CONF_NAME, CONF_OFFSET +from homeassistant.const import CONF_NAME, CONF_OFFSET, CURRENCY_CENT, UnitOfEnergy from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv @@ -36,12 +36,12 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key=CONF_FIVE_MINUTE, name="ComEd 5 Minute Price", - native_unit_of_measurement="c", + native_unit_of_measurement=f"{CURRENCY_CENT}/{UnitOfEnergy.KILO_WATT_HOUR}", ), SensorEntityDescription( key=CONF_CURRENT_HOUR_AVERAGE, name="ComEd Current Hour Average Price", - native_unit_of_measurement="c", + native_unit_of_measurement=f"{CURRENCY_CENT}/{UnitOfEnergy.KILO_WATT_HOUR}", ), ) From 6ddc872655e1c9b8411ffc5b9e52b1e6c7017aad Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 21 Jun 2024 12:20:13 +0200 Subject: [PATCH 0897/1445] Improve UniFi device tracker client tests (#119982) --- tests/components/unifi/test_device_tracker.py | 370 +++++------------- 1 file changed, 98 insertions(+), 272 deletions(-) diff --git a/tests/components/unifi/test_device_tracker.py b/tests/components/unifi/test_device_tracker.py index e22c49fd7db..984fe50753f 100644 --- a/tests/components/unifi/test_device_tracker.py +++ b/tests/components/unifi/test_device_tracker.py @@ -5,6 +5,7 @@ from datetime import timedelta from types import MappingProxyType from typing import Any +from aiounifi.models.event import EventKey from aiounifi.models.message import MessageKey from freezegun.api import FrozenDateTimeFactory, freeze_time import pytest @@ -47,6 +48,24 @@ WIRELESS_CLIENT_1 = { "mac": "00:00:00:00:00:01", } +WIRED_BUG_CLIENT = { + "essid": "ssid", + "hostname": "wd_bug_client", + "ip": "10.0.0.3", + "is_wired": True, + "last_seen": 1562600145, + "mac": "00:00:00:00:00:03", +} + +UNSEEN_CLIENT = { + "essid": "ssid", + "hostname": "unseen_client", + "ip": "10.0.0.4", + "is_wired": True, + "last_seen": None, + "mac": "00:00:00:00:00:04", +} + SWITCH_1 = { "board_rev": 3, "device_id": "mock-id-1", @@ -67,292 +86,131 @@ SWITCH_1 = { @pytest.mark.parametrize( - "client_payload", - [ - [ - { - "ap_mac": "00:00:00:00:02:01", - "essid": "ssid", - "hostname": "client", - "ip": "10.0.0.1", - "is_wired": False, - "last_seen": 1562600145, - "mac": "00:00:00:00:00:01", - } - ] - ], + "client_payload", [[WIRELESS_CLIENT_1, WIRED_BUG_CLIENT, UNSEEN_CLIENT]] ) +@pytest.mark.parametrize("known_wireless_clients", [[WIRED_BUG_CLIENT["mac"]]]) @pytest.mark.usefixtures("mock_device_registry") -async def test_tracked_wireless_clients( +async def test_client_state_update( hass: HomeAssistant, mock_websocket_message, - config_entry_setup: ConfigEntry, + config_entry_factory: Callable[[], ConfigEntry], client_payload: list[dict[str, Any]], ) -> None: """Verify tracking of wireless clients.""" - assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 1 - assert hass.states.get("device_tracker.client").state == STATE_NOT_HOME + # A normal client with current timestamp should have STATE_HOME, this is wired bug + client_payload[1] |= {"last_seen": dt_util.as_timestamp(dt_util.utcnow())} + await config_entry_factory() + + assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 3 + + assert hass.states.get("device_tracker.ws_client_1").state == STATE_NOT_HOME + assert ( + hass.states.get("device_tracker.ws_client_1").attributes["host_name"] + == "ws_client_1" + ) + + # Wireless client with wired bug, if bug active on restart mark device away + assert hass.states.get("device_tracker.wd_bug_client").state == STATE_NOT_HOME + + # A client that has never been seen should be marked away. + assert hass.states.get("device_tracker.unseen_client").state == STATE_NOT_HOME # Updated timestamp marks client as home - client = client_payload[0] - client["last_seen"] = dt_util.as_timestamp(dt_util.utcnow()) - mock_websocket_message(message=MessageKey.CLIENT, data=client) + ws_client_1 = client_payload[0] | { + "last_seen": dt_util.as_timestamp(dt_util.utcnow()) + } + mock_websocket_message(message=MessageKey.CLIENT, data=ws_client_1) await hass.async_block_till_done() - assert hass.states.get("device_tracker.client").state == STATE_HOME + assert hass.states.get("device_tracker.ws_client_1").state == STATE_HOME # Change time to mark client as away - new_time = dt_util.utcnow() + timedelta( - seconds=config_entry_setup.options.get( - CONF_DETECTION_TIME, DEFAULT_DETECTION_TIME - ) - ) + new_time = dt_util.utcnow() + timedelta(seconds=DEFAULT_DETECTION_TIME) with freeze_time(new_time): async_fire_time_changed(hass, new_time) await hass.async_block_till_done() - assert hass.states.get("device_tracker.client").state == STATE_NOT_HOME + assert hass.states.get("device_tracker.ws_client_1").state == STATE_NOT_HOME # Same timestamp doesn't explicitly mark client as away - mock_websocket_message(message=MessageKey.CLIENT, data=client) + mock_websocket_message(message=MessageKey.CLIENT, data=ws_client_1) await hass.async_block_till_done() - assert hass.states.get("device_tracker.client").state == STATE_HOME + assert hass.states.get("device_tracker.ws_client_1").state == STATE_HOME -@pytest.mark.parametrize( - "config_entry_options", - [{CONF_SSID_FILTER: ["ssid"], CONF_CLIENT_SOURCE: ["00:00:00:00:00:06"]}], -) -@pytest.mark.parametrize( - "client_payload", - [ - [ - { - "ap_mac": "00:00:00:00:02:01", - "essid": "ssid", - "hostname": "client_1", - "ip": "10.0.0.1", - "is_wired": False, - "last_seen": 1562600145, - "mac": "00:00:00:00:00:01", - }, - { - "ip": "10.0.0.2", - "is_wired": True, - "last_seen": 1562600145, - "mac": "00:00:00:00:00:02", - "name": "Client 2", - }, - { - "essid": "ssid2", - "hostname": "client_3", - "ip": "10.0.0.3", - "is_wired": False, - "last_seen": 1562600145, - "mac": "00:00:00:00:00:03", - }, - { - "essid": "ssid", - "hostname": "client_4", - "ip": "10.0.0.4", - "is_wired": True, - "last_seen": dt_util.as_timestamp(dt_util.utcnow()), - "mac": "00:00:00:00:00:04", - }, - { - "essid": "ssid", - "hostname": "client_5", - "ip": "10.0.0.5", - "is_wired": True, - "last_seen": None, - "mac": "00:00:00:00:00:05", - }, - { - "hostname": "client_6", - "ip": "10.0.0.6", - "is_wired": True, - "last_seen": 1562600145, - "mac": "00:00:00:00:00:06", - }, - ] - ], -) -@pytest.mark.parametrize("known_wireless_clients", [["00:00:00:00:00:04"]]) +@pytest.mark.parametrize("client_payload", [[WIRELESS_CLIENT_1]]) @pytest.mark.usefixtures("config_entry_setup") @pytest.mark.usefixtures("mock_device_registry") -async def test_tracked_clients( - hass: HomeAssistant, mock_websocket_message, client_payload: list[dict[str, Any]] -) -> None: - """Test the update_items function with some clients.""" - assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 5 - 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" - ) - assert hass.states.get("device_tracker.client_6").state == STATE_NOT_HOME - - # Client on SSID not in SSID filter - assert not hass.states.get("device_tracker.client_3") - - # Wireless client with wired bug, if bug active on restart mark device away - assert hass.states.get("device_tracker.client_4").state == STATE_NOT_HOME - - # A client that has never been seen should be marked away. - assert hass.states.get("device_tracker.client_5").state == STATE_NOT_HOME - - # State change signalling works - - client_1 = client_payload[0] - client_1["last_seen"] = dt_util.as_timestamp(dt_util.utcnow()) - mock_websocket_message(message=MessageKey.CLIENT, data=client_1) - await hass.async_block_till_done() - - assert hass.states.get("device_tracker.client_1").state == STATE_HOME - - -@pytest.mark.parametrize( - "client_payload", - [ - [ - { - "ap_mac": "00:00:00:00:02:01", - "essid": "ssid", - "hostname": "client", - "ip": "10.0.0.1", - "is_wired": False, - "last_seen": 1562600145, - "mac": "00:00:00:00:00:01", - } - ] - ], -) -@pytest.mark.usefixtures("mock_device_registry") -async def test_tracked_wireless_clients_event_source( +async def test_client_state_from_event_source( hass: HomeAssistant, freezer: FrozenDateTimeFactory, mock_websocket_message, - config_entry_setup: ConfigEntry, client_payload: list[dict[str, Any]], ) -> None: - """Verify tracking of wireless clients based on event source.""" + """Verify update state of client based on event source.""" + + async def mock_event(client: dict[str, Any], event_key: EventKey) -> dict[str, Any]: + """Create and send event based on client payload.""" + event = { + "user": client["mac"], + "ssid": client["essid"], + "hostname": client["hostname"], + "ap": client["ap_mac"], + "duration": 467, + "bytes": 459039, + "key": event_key, + "subsystem": "wlan", + "site_id": "name", + "time": 1587752927000, + "datetime": "2020-04-24T18:28:47Z", + "_id": "5ea32ff730c49e00f90dca1a", + } + mock_websocket_message(message=MessageKey.EVENT, data=event) + await hass.async_block_till_done() + assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 1 - assert hass.states.get("device_tracker.client").state == STATE_NOT_HOME + assert hass.states.get("device_tracker.ws_client_1").state == STATE_NOT_HOME # State change signalling works with events # Connected event - client = client_payload[0] - event = { - "user": client["mac"], - "ssid": client["essid"], - "ap": client["ap_mac"], - "radio": "na", - "channel": "44", - "hostname": client["hostname"], - "key": "EVT_WU_Connected", - "subsystem": "wlan", - "site_id": "name", - "time": 1587753456179, - "datetime": "2020-04-24T18:37:36Z", - "msg": ( - f'User{[client["mac"]]} has connected to AP[{client["ap_mac"]}] ' - f'with SSID "{client["essid"]}" on "channel 44(na)"' - ), - "_id": "5ea331fa30c49e00f90ddc1a", - } - mock_websocket_message(message=MessageKey.EVENT, data=event) - await hass.async_block_till_done() - assert hass.states.get("device_tracker.client").state == STATE_HOME + await mock_event(client_payload[0], EventKey.WIRELESS_CLIENT_CONNECTED) + assert hass.states.get("device_tracker.ws_client_1").state == STATE_HOME # Disconnected event - event = { - "user": client["mac"], - "ssid": client["essid"], - "hostname": client["hostname"], - "ap": client["ap_mac"], - "duration": 467, - "bytes": 459039, - "key": "EVT_WU_Disconnected", - "subsystem": "wlan", - "site_id": "name", - "time": 1587752927000, - "datetime": "2020-04-24T18:28:47Z", - "msg": ( - f'User{[client["mac"]]} disconnected from "{client["essid"]}" ' - f'(7m 47s connected, 448.28K bytes, last AP[{client["ap_mac"]}])' - ), - "_id": "5ea32ff730c49e00f90dca1a", - } - mock_websocket_message(message=MessageKey.EVENT, data=event) - await hass.async_block_till_done() - assert hass.states.get("device_tracker.client").state == STATE_HOME + await mock_event(client_payload[0], EventKey.WIRELESS_CLIENT_DISCONNECTED) + assert hass.states.get("device_tracker.ws_client_1").state == STATE_HOME # Change time to mark client as away - freezer.tick( - timedelta( - seconds=( - config_entry_setup.options.get( - CONF_DETECTION_TIME, DEFAULT_DETECTION_TIME - ) - + 1 - ) - ) - ) + freezer.tick(timedelta(seconds=(DEFAULT_DETECTION_TIME + 1))) async_fire_time_changed(hass) await hass.async_block_till_done() - assert hass.states.get("device_tracker.client").state == STATE_NOT_HOME + assert hass.states.get("device_tracker.ws_client_1").state == STATE_NOT_HOME # To limit false positives in client tracker # data sources are prioritized when available # once real data is received events will be ignored. # New data - client["last_seen"] = dt_util.as_timestamp(dt_util.utcnow()) - mock_websocket_message(message=MessageKey.CLIENT, data=client) + ws_client_1 = client_payload[0] | { + "last_seen": dt_util.as_timestamp(dt_util.utcnow()) + } + mock_websocket_message(message=MessageKey.CLIENT, data=ws_client_1) await hass.async_block_till_done() - assert hass.states.get("device_tracker.client").state == STATE_HOME + assert hass.states.get("device_tracker.ws_client_1").state == STATE_HOME # Disconnection event will be ignored - event = { - "user": client["mac"], - "ssid": client["essid"], - "hostname": client["hostname"], - "ap": client["ap_mac"], - "duration": 467, - "bytes": 459039, - "key": "EVT_WU_Disconnected", - "subsystem": "wlan", - "site_id": "name", - "time": 1587752927000, - "datetime": "2020-04-24T18:28:47Z", - "msg": ( - f'User{[client["mac"]]} disconnected from "{client["essid"]}" ' - f'(7m 47s connected, 448.28K bytes, last AP[{client["ap_mac"]}])' - ), - "_id": "5ea32ff730c49e00f90dca1a", - } - mock_websocket_message(message=MessageKey.EVENT, data=event) - await hass.async_block_till_done() - assert hass.states.get("device_tracker.client").state == STATE_HOME + await mock_event(client_payload[0], EventKey.WIRELESS_CLIENT_DISCONNECTED) + assert hass.states.get("device_tracker.ws_client_1").state == STATE_HOME # Change time to mark client as away - freezer.tick( - timedelta( - seconds=( - config_entry_setup.options.get( - CONF_DETECTION_TIME, DEFAULT_DETECTION_TIME - ) - + 1 - ) - ) - ) + freezer.tick(timedelta(seconds=(DEFAULT_DETECTION_TIME + 1))) async_fire_time_changed(hass) await hass.async_block_till_done() - assert hass.states.get("device_tracker.client").state == STATE_NOT_HOME + assert hass.states.get("device_tracker.ws_client_1").state == STATE_NOT_HOME @pytest.mark.parametrize( @@ -435,26 +293,7 @@ async def test_tracked_devices( assert hass.states.get("device_tracker.device_2").state == STATE_HOME -@pytest.mark.parametrize( - "client_payload", - [ - [ - { - "essid": "ssid", - "hostname": "client_1", - "is_wired": False, - "last_seen": 1562600145, - "mac": "00:00:00:00:00:01", - }, - { - "hostname": "client_2", - "is_wired": True, - "last_seen": 1562600145, - "mac": "00:00:00:00:00:02", - }, - ] - ], -) +@pytest.mark.parametrize("client_payload", [[WIRELESS_CLIENT_1, WIRED_CLIENT_1]]) @pytest.mark.usefixtures("config_entry_setup") @pytest.mark.usefixtures("mock_device_registry") async def test_remove_clients( @@ -462,17 +301,16 @@ async def test_remove_clients( ) -> None: """Test the remove_items function with some clients.""" assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 2 - assert hass.states.get("device_tracker.client_1") - assert hass.states.get("device_tracker.client_2") + assert hass.states.get("device_tracker.ws_client_1") + assert hass.states.get("device_tracker.wd_client_1") # Remove client mock_websocket_message(message=MessageKey.CLIENT_REMOVED, data=client_payload[0]) await hass.async_block_till_done() - await hass.async_block_till_done() assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 1 - assert not hass.states.get("device_tracker.client_1") - assert hass.states.get("device_tracker.client_2") + assert not hass.states.get("device_tracker.ws_client_1") + assert hass.states.get("device_tracker.wd_client_1") @pytest.mark.parametrize( @@ -793,21 +631,9 @@ async def test_option_ignore_wired_bug( @pytest.mark.parametrize( - "config_entry_options", [{CONF_BLOCK_CLIENT: ["00:00:00:00:00:02"]}] -) -@pytest.mark.parametrize( - "client_payload", - [ - [ - { - "hostname": "client", - "is_wired": True, - "last_seen": 1562600145, - "mac": "00:00:00:00:00:01", - } - ] - ], + "config_entry_options", [{CONF_BLOCK_CLIENT: ["00:00:00:00:00:03"]}] ) +@pytest.mark.parametrize("client_payload", [[WIRED_CLIENT_1]]) @pytest.mark.parametrize( "clients_all_payload", [ @@ -816,13 +642,13 @@ async def test_option_ignore_wired_bug( "hostname": "restored", "is_wired": True, "last_seen": 1562600145, - "mac": "00:00:00:00:00:02", + "mac": "00:00:00:00:00:03", }, { # Not previously seen by integration, will not be restored "hostname": "not_restored", "is_wired": True, "last_seen": 1562600145, - "mac": "00:00:00:00:00:03", + "mac": "00:00:00:00:00:04", }, ] ], @@ -855,7 +681,7 @@ async def test_restoring_client( await config_entry_factory() assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 2 - assert hass.states.get("device_tracker.client") + assert hass.states.get("device_tracker.wd_client_1") assert hass.states.get("device_tracker.restored") assert not hass.states.get("device_tracker.not_restored") From 0aacc67c38a542a2f877ebc4a9548121c74571cf Mon Sep 17 00:00:00 2001 From: Evgeny <940893+freekode@users.noreply.github.com> Date: Fri, 21 Jun 2024 12:21:57 +0200 Subject: [PATCH 0898/1445] OpenWeatherMap remove obsolete forecast sensors (#119922) --- .../components/openweathermap/const.py | 13 --- .../components/openweathermap/sensor.py | 109 ------------------ 2 files changed, 122 deletions(-) diff --git a/homeassistant/components/openweathermap/const.py b/homeassistant/components/openweathermap/const.py index 456ec05b038..6c9997fc061 100644 --- a/homeassistant/components/openweathermap/const.py +++ b/homeassistant/components/openweathermap/const.py @@ -53,19 +53,6 @@ ATTR_API_DAILY_FORECAST = "daily_forecast" UPDATE_LISTENER = "update_listener" PLATFORMS = [Platform.SENSOR, Platform.WEATHER] -ATTR_API_FORECAST_CLOUDS = "clouds" -ATTR_API_FORECAST_CONDITION = "condition" -ATTR_API_FORECAST_FEELS_LIKE_TEMPERATURE = "feels_like_temperature" -ATTR_API_FORECAST_HUMIDITY = "humidity" -ATTR_API_FORECAST_PRECIPITATION = "precipitation" -ATTR_API_FORECAST_PRECIPITATION_PROBABILITY = "precipitation_probability" -ATTR_API_FORECAST_PRESSURE = "pressure" -ATTR_API_FORECAST_TEMP = "temperature" -ATTR_API_FORECAST_TEMP_LOW = "templow" -ATTR_API_FORECAST_TIME = "datetime" -ATTR_API_FORECAST_WIND_BEARING = "wind_bearing" -ATTR_API_FORECAST_WIND_SPEED = "wind_speed" - FORECAST_MODE_HOURLY = "hourly" FORECAST_MODE_DAILY = "daily" FORECAST_MODE_FREE_DAILY = "freedaily" diff --git a/homeassistant/components/openweathermap/sensor.py b/homeassistant/components/openweathermap/sensor.py index 5fe0df60387..89905e99ed9 100644 --- a/homeassistant/components/openweathermap/sensor.py +++ b/homeassistant/components/openweathermap/sensor.py @@ -2,8 +2,6 @@ from __future__ import annotations -from datetime import datetime - from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, @@ -15,7 +13,6 @@ from homeassistant.const import ( PERCENTAGE, UV_INDEX, UnitOfLength, - UnitOfPrecipitationDepth, UnitOfPressure, UnitOfSpeed, UnitOfTemperature, @@ -26,23 +23,14 @@ from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from homeassistant.util import dt as dt_util from . import OpenweathermapConfigEntry from .const import ( - ATTR_API_CLOUD_COVERAGE, ATTR_API_CLOUDS, ATTR_API_CONDITION, ATTR_API_CURRENT, - ATTR_API_DAILY_FORECAST, ATTR_API_DEW_POINT, ATTR_API_FEELS_LIKE_TEMPERATURE, - ATTR_API_FORECAST_PRECIPITATION, - ATTR_API_FORECAST_PRECIPITATION_PROBABILITY, - ATTR_API_FORECAST_PRESSURE, - ATTR_API_FORECAST_TEMP, - ATTR_API_FORECAST_TEMP_LOW, - ATTR_API_FORECAST_TIME, ATTR_API_HUMIDITY, ATTR_API_PRECIPITATION_KIND, ATTR_API_PRESSURE, @@ -161,62 +149,6 @@ WEATHER_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( name="Weather Code", ), ) -FORECAST_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( - SensorEntityDescription( - key=ATTR_API_CONDITION, - name="Condition", - ), - SensorEntityDescription( - key=ATTR_API_FORECAST_PRECIPITATION, - name="Precipitation", - device_class=SensorDeviceClass.PRECIPITATION, - native_unit_of_measurement=UnitOfPrecipitationDepth.MILLIMETERS, - ), - SensorEntityDescription( - key=ATTR_API_FORECAST_PRECIPITATION_PROBABILITY, - name="Precipitation probability", - native_unit_of_measurement=PERCENTAGE, - ), - SensorEntityDescription( - key=ATTR_API_FORECAST_PRESSURE, - name="Pressure", - native_unit_of_measurement=UnitOfPressure.HPA, - device_class=SensorDeviceClass.PRESSURE, - ), - SensorEntityDescription( - key=ATTR_API_FORECAST_TEMP, - name="Temperature", - native_unit_of_measurement=UnitOfTemperature.CELSIUS, - device_class=SensorDeviceClass.TEMPERATURE, - ), - SensorEntityDescription( - key=ATTR_API_FORECAST_TEMP_LOW, - name="Temperature Low", - native_unit_of_measurement=UnitOfTemperature.CELSIUS, - device_class=SensorDeviceClass.TEMPERATURE, - ), - SensorEntityDescription( - key=ATTR_API_FORECAST_TIME, - name="Time", - device_class=SensorDeviceClass.TIMESTAMP, - ), - SensorEntityDescription( - key=ATTR_API_WIND_BEARING, - name="Wind bearing", - native_unit_of_measurement=DEGREE, - ), - SensorEntityDescription( - key=ATTR_API_WIND_SPEED, - name="Wind speed", - native_unit_of_measurement=UnitOfSpeed.METERS_PER_SECOND, - device_class=SensorDeviceClass.WIND_SPEED, - ), - SensorEntityDescription( - key=ATTR_API_CLOUD_COVERAGE, - name="Cloud coverage", - native_unit_of_measurement=PERCENTAGE, - ), -) async def async_setup_entry( @@ -238,19 +170,6 @@ async def async_setup_entry( ) for description in WEATHER_SENSOR_TYPES ] - - entities.extend( - [ - OpenWeatherMapForecastSensor( - f"{name} Forecast", - f"{config_entry.unique_id}-forecast-{description.key}", - description, - weather_coordinator, - ) - for description in FORECAST_SENSOR_TYPES - ] - ) - async_add_entities(entities) @@ -317,31 +236,3 @@ class OpenWeatherMapSensor(AbstractOpenWeatherMapSensor): return self._weather_coordinator.data[ATTR_API_CURRENT].get( self.entity_description.key ) - - -class OpenWeatherMapForecastSensor(AbstractOpenWeatherMapSensor): - """Implementation of an OpenWeatherMap this day forecast sensor.""" - - def __init__( - self, - name: str, - unique_id: str, - description: SensorEntityDescription, - weather_coordinator: WeatherUpdateCoordinator, - ) -> None: - """Initialize the sensor.""" - super().__init__(name, unique_id, description, weather_coordinator) - self._weather_coordinator = weather_coordinator - - @property - def native_value(self) -> StateType | datetime: - """Return the state of the device.""" - forecasts = self._weather_coordinator.data[ATTR_API_DAILY_FORECAST] - value = forecasts[0].get(self.entity_description.key) - if ( - value - and self.entity_description.device_class is SensorDeviceClass.TIMESTAMP - ): - return dt_util.parse_datetime(value) - - return value From a52a2383c51e996f345a9a7f2cd0ccb20ab9eb11 Mon Sep 17 00:00:00 2001 From: Igor Santos <532299+igorsantos07@users.noreply.github.com> Date: Fri, 21 Jun 2024 07:22:33 -0300 Subject: [PATCH 0899/1445] Tuya's light POS actually means "opposite state" (#119948) --- homeassistant/components/tuya/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/tuya/strings.json b/homeassistant/components/tuya/strings.json index cfce12273a0..281d56f7ae4 100644 --- a/homeassistant/components/tuya/strings.json +++ b/homeassistant/components/tuya/strings.json @@ -275,7 +275,7 @@ "name": "Indicator light mode", "state": { "none": "[%key:common::state::off%]", - "pos": "Indicate switch location", + "pos": "Indicate inverted switch state", "relay": "Indicate switch on/off state" } }, From 1c1d5a8d9b140afbff1210a2361010b95c4104c9 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 21 Jun 2024 12:25:03 +0200 Subject: [PATCH 0900/1445] Add unrecorded attributes in dnsip (#119726) * Add unrecorded attributes in dnsip * Fix names --- homeassistant/components/dnsip/sensor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/dnsip/sensor.py b/homeassistant/components/dnsip/sensor.py index 2f5e0582e21..34730e934a0 100644 --- a/homeassistant/components/dnsip/sensor.py +++ b/homeassistant/components/dnsip/sensor.py @@ -71,6 +71,7 @@ class WanIpSensor(SensorEntity): _attr_has_entity_name = True _attr_translation_key = "dnsip" + _unrecorded_attributes = frozenset({"resolver", "querytype", "ip_addresses"}) def __init__( self, From 324f07378add5ac1b0d6ecef82b204763fab2263 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 21 Jun 2024 05:25:28 -0500 Subject: [PATCH 0901/1445] Bump uiprotect to 1.19.3 (#120079) --- homeassistant/components/unifiprotect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index ee12111b146..8dcc102d6fb 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -40,7 +40,7 @@ "integration_type": "hub", "iot_class": "local_push", "loggers": ["uiprotect", "unifi_discovery"], - "requirements": ["uiprotect==1.19.2", "unifi-discovery==1.1.8"], + "requirements": ["uiprotect==1.19.3", "unifi-discovery==1.1.8"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/requirements_all.txt b/requirements_all.txt index c1568ac6056..007701ce2a3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2791,7 +2791,7 @@ twitchAPI==4.0.0 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==1.19.2 +uiprotect==1.19.3 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7995497972c..76565547e00 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2171,7 +2171,7 @@ twitchAPI==4.0.0 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==1.19.2 +uiprotect==1.19.3 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 From fa1e4a225d4a5f0d6872ac871dfd59c198ed7033 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 21 Jun 2024 12:27:33 +0200 Subject: [PATCH 0902/1445] Bump aiomealie to 0.4.0 (#120076) --- homeassistant/components/mealie/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/mealie/manifest.json b/homeassistant/components/mealie/manifest.json index 3a2a9b58204..fb81ff850b8 100644 --- a/homeassistant/components/mealie/manifest.json +++ b/homeassistant/components/mealie/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/mealie", "integration_type": "service", "iot_class": "local_polling", - "requirements": ["aiomealie==0.3.1"] + "requirements": ["aiomealie==0.4.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 007701ce2a3..3c22b5541de 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -294,7 +294,7 @@ aiolookin==1.0.0 aiolyric==1.1.0 # homeassistant.components.mealie -aiomealie==0.3.1 +aiomealie==0.4.0 # homeassistant.components.modern_forms aiomodernforms==0.1.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 76565547e00..f91f683f98a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -267,7 +267,7 @@ aiolookin==1.0.0 aiolyric==1.1.0 # homeassistant.components.mealie -aiomealie==0.3.1 +aiomealie==0.4.0 # homeassistant.components.modern_forms aiomodernforms==0.1.8 From 904cf26d3193d5e7f49d88fb815cafaefded43b3 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 21 Jun 2024 12:32:03 +0200 Subject: [PATCH 0903/1445] Add MockToggleEntity type hints in tests (#120075) --- tests/common.py | 18 +++++++++--------- tests/components/conversation/test_init.py | 8 ++++---- tests/components/light/common.py | 11 ++++++----- tests/components/light/test_init.py | 3 ++- .../custom_components/test/light.py | 11 ++++++----- 5 files changed, 27 insertions(+), 24 deletions(-) diff --git a/tests/common.py b/tests/common.py index 87a894bcb26..30c7cc2d971 100644 --- a/tests/common.py +++ b/tests/common.py @@ -17,7 +17,7 @@ import pathlib import threading import time from types import FrameType, ModuleType -from typing import Any, NoReturn +from typing import Any, Literal, NoReturn from unittest.mock import AsyncMock, Mock, patch from aiohttp.test_utils import unused_port as get_test_instance_port # noqa: F401 @@ -959,41 +959,41 @@ class MockEntityPlatform(entity_platform.EntityPlatform): class MockToggleEntity(entity.ToggleEntity): """Provide a mock toggle device.""" - def __init__(self, name, state, unique_id=None): + def __init__(self, name: str | None, state: Literal["on", "off"] | None) -> None: """Initialize the mock entity.""" self._name = name or DEVICE_DEFAULT_NAME self._state = state - self.calls = [] + self.calls: list[tuple[str, dict[str, Any]]] = [] @property - def name(self): + def name(self) -> str: """Return the name of the entity if any.""" self.calls.append(("name", {})) return self._name @property - def state(self): + def state(self) -> Literal["on", "off"] | None: """Return the state of the entity if any.""" self.calls.append(("state", {})) return self._state @property - def is_on(self): + def is_on(self) -> bool: """Return true if entity is on.""" self.calls.append(("is_on", {})) return self._state == STATE_ON - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn the entity on.""" self.calls.append(("turn_on", kwargs)) self._state = STATE_ON - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn the entity off.""" self.calls.append(("turn_off", kwargs)) self._state = STATE_OFF - def last_call(self, method=None): + def last_call(self, method: str | None = None) -> tuple[str, dict[str, Any]]: """Return the last call.""" if not self.calls: return None diff --git a/tests/components/conversation/test_init.py b/tests/components/conversation/test_init.py index 415c80fffbc..48f227e9497 100644 --- a/tests/components/conversation/test_init.py +++ b/tests/components/conversation/test_init.py @@ -13,7 +13,7 @@ from homeassistant.components.conversation import default_agent from homeassistant.components.conversation.models import ConversationInput from homeassistant.components.cover import SERVICE_OPEN_COVER from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN -from homeassistant.const import ATTR_FRIENDLY_NAME +from homeassistant.const import ATTR_FRIENDLY_NAME, STATE_ON from homeassistant.core import Context, HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import ( @@ -269,7 +269,7 @@ async def test_http_processing_intent_entity_renamed( We want to ensure that renaming an entity later busts the cache so that the new name is used. """ - entity = MockLight("kitchen light", "on") + entity = MockLight("kitchen light", STATE_ON) entity._attr_unique_id = "1234" entity.entity_id = "light.kitchen" setup_test_component_platform(hass, LIGHT_DOMAIN, [entity]) @@ -357,7 +357,7 @@ async def test_http_processing_intent_entity_exposed( We want to ensure that manually exposing an entity later busts the cache so that the new setting is used. """ - entity = MockLight("kitchen light", "on") + entity = MockLight("kitchen light", STATE_ON) entity._attr_unique_id = "1234" entity.entity_id = "light.kitchen" setup_test_component_platform(hass, LIGHT_DOMAIN, [entity]) @@ -458,7 +458,7 @@ async def test_http_processing_intent_conversion_not_expose_new( # Disable exposing new entities to the default agent expose_new(hass, False) - entity = MockLight("kitchen light", "on") + entity = MockLight("kitchen light", STATE_ON) entity._attr_unique_id = "1234" entity.entity_id = "light.kitchen" setup_test_component_platform(hass, LIGHT_DOMAIN, [entity]) diff --git a/tests/components/light/common.py b/tests/components/light/common.py index 7c33c40ab63..4c3e95b5ef9 100644 --- a/tests/components/light/common.py +++ b/tests/components/light/common.py @@ -4,6 +4,8 @@ All containing methods are legacy helpers that should not be used by new components. Instead call the service directly. """ +from typing import Any, Literal + from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_BRIGHTNESS_PCT, @@ -250,13 +252,12 @@ class MockLight(MockToggleEntity, LightEntity): def __init__( self, - name, - state, - unique_id=None, + name: str | None, + state: Literal["on", "off"] | None, supported_color_modes: set[ColorMode] | None = None, ) -> None: """Initialize the mock light.""" - super().__init__(name, state, unique_id) + super().__init__(name, state) if supported_color_modes is None: supported_color_modes = {ColorMode.ONOFF} self._attr_supported_color_modes = supported_color_modes @@ -265,7 +266,7 @@ class MockLight(MockToggleEntity, LightEntity): color_mode = next(iter(supported_color_modes)) self._attr_color_mode = color_mode - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn the entity on.""" super().turn_on(**kwargs) for key, value in kwargs.items(): diff --git a/tests/components/light/test_init.py b/tests/components/light/test_init.py index a01d70d328c..eeb32f1b17a 100644 --- a/tests/components/light/test_init.py +++ b/tests/components/light/test_init.py @@ -1,5 +1,6 @@ """The tests for the Light component.""" +from typing import Literal from unittest.mock import MagicMock, mock_open, patch import pytest @@ -1144,7 +1145,7 @@ invalid_no_brightness_no_color_no_transition,,, @pytest.mark.parametrize("light_state", [STATE_ON, STATE_OFF]) async def test_light_backwards_compatibility_supported_color_modes( - hass: HomeAssistant, light_state + hass: HomeAssistant, light_state: Literal["on", "off"] ) -> None: """Test supported_color_modes if not implemented by the entity.""" entities = [ diff --git a/tests/testing_config/custom_components/test/light.py b/tests/testing_config/custom_components/test/light.py index 6422bb4fccb..d9fad11655e 100644 --- a/tests/testing_config/custom_components/test/light.py +++ b/tests/testing_config/custom_components/test/light.py @@ -3,6 +3,8 @@ Call init before using it in your tests to ensure clean test data. """ +from typing import Any, Literal + from homeassistant.components.light import ColorMode, LightEntity from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant @@ -67,13 +69,12 @@ class MockLight(MockToggleEntity, LightEntity): def __init__( self, - name, - state, - unique_id=None, + name: str | None, + state: Literal["on", "off"] | None, supported_color_modes: set[ColorMode] | None = None, ) -> None: """Initialize the mock light.""" - super().__init__(name, state, unique_id) + super().__init__(name, state) if supported_color_modes is None: supported_color_modes = {ColorMode.ONOFF} self._attr_supported_color_modes = supported_color_modes @@ -82,7 +83,7 @@ class MockLight(MockToggleEntity, LightEntity): color_mode = next(iter(supported_color_modes)) self._attr_color_mode = color_mode - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn the entity on.""" super().turn_on(**kwargs) for key, value in kwargs.items(): From af9f4f310be39a476869ab5f09f3842d38d75177 Mon Sep 17 00:00:00 2001 From: dontinelli <73341522+dontinelli@users.noreply.github.com> Date: Fri, 21 Jun 2024 12:44:25 +0200 Subject: [PATCH 0904/1445] Add additional tests for solarlog (#119928) --- .coveragerc | 3 - tests/components/solarlog/__init__.py | 18 + tests/components/solarlog/conftest.py | 50 +- tests/components/solarlog/const.py | 4 + .../solarlog/fixtures/solarlog_data.json | 24 + .../solarlog/snapshots/test_sensor.ambr | 2183 +++++++++++++++++ tests/components/solarlog/test_config_flow.py | 13 +- tests/components/solarlog/test_init.py | 40 +- tests/components/solarlog/test_sensor.py | 59 + 9 files changed, 2376 insertions(+), 18 deletions(-) create mode 100644 tests/components/solarlog/const.py create mode 100644 tests/components/solarlog/fixtures/solarlog_data.json create mode 100644 tests/components/solarlog/snapshots/test_sensor.ambr create mode 100644 tests/components/solarlog/test_sensor.py diff --git a/.coveragerc b/.coveragerc index 3b44334249d..56a93b586a4 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1259,9 +1259,6 @@ omit = homeassistant/components/solaredge/__init__.py homeassistant/components/solaredge/coordinator.py homeassistant/components/solaredge_local/sensor.py - homeassistant/components/solarlog/__init__.py - homeassistant/components/solarlog/coordinator.py - homeassistant/components/solarlog/sensor.py homeassistant/components/solax/__init__.py homeassistant/components/solax/sensor.py homeassistant/components/soma/__init__.py diff --git a/tests/components/solarlog/__init__.py b/tests/components/solarlog/__init__.py index 9074cab8416..74b19bd297e 100644 --- a/tests/components/solarlog/__init__.py +++ b/tests/components/solarlog/__init__.py @@ -1 +1,19 @@ """Tests for the solarlog integration.""" + +from unittest.mock import patch + +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +async def setup_platform( + hass: HomeAssistant, config_entry: MockConfigEntry, platforms: list[Platform] +) -> MockConfigEntry: + """Set up the SolarLog platform.""" + config_entry.add_to_hass(hass) + + with patch("homeassistant.components.solarlog.PLATFORMS", platforms): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() diff --git a/tests/components/solarlog/conftest.py b/tests/components/solarlog/conftest.py index 71034828025..08340487d99 100644 --- a/tests/components/solarlog/conftest.py +++ b/tests/components/solarlog/conftest.py @@ -5,21 +5,57 @@ from unittest.mock import AsyncMock, patch import pytest +from homeassistant.components.solarlog.const import DOMAIN as SOLARLOG_DOMAIN +from homeassistant.const import CONF_HOST, CONF_NAME from homeassistant.core import HomeAssistant -from tests.common import mock_device_registry, mock_registry +from .const import HOST, NAME + +from tests.common import ( + MockConfigEntry, + load_json_object_fixture, + mock_device_registry, + mock_registry, +) @pytest.fixture -def mock_solarlog(): +def mock_config_entry() -> MockConfigEntry: + """Mock a config entry.""" + return MockConfigEntry( + domain=SOLARLOG_DOMAIN, + title="solarlog", + data={ + CONF_HOST: HOST, + CONF_NAME: NAME, + "extended_data": True, + }, + minor_version=2, + entry_id="ce5f5431554d101905d31797e1232da8", + ) + + +@pytest.fixture +def mock_solarlog_connector(): """Build a fixture for the SolarLog API that connects successfully and returns one device.""" mock_solarlog_api = AsyncMock() - with patch( - "homeassistant.components.solarlog.config_flow.SolarLogConnector", - return_value=mock_solarlog_api, - ) as mock_solarlog_api: - mock_solarlog_api.return_value.test_connection.return_value = True + mock_solarlog_api.test_connection = AsyncMock(return_value=True) + mock_solarlog_api.update_data.return_value = load_json_object_fixture( + "solarlog_data.json", SOLARLOG_DOMAIN + ) + with ( + patch( + "homeassistant.components.solarlog.coordinator.SolarLogConnector", + autospec=True, + return_value=mock_solarlog_api, + ), + patch( + "homeassistant.components.solarlog.config_flow.SolarLogConnector", + autospec=True, + return_value=mock_solarlog_api, + ), + ): yield mock_solarlog_api diff --git a/tests/components/solarlog/const.py b/tests/components/solarlog/const.py new file mode 100644 index 00000000000..e23633c80ae --- /dev/null +++ b/tests/components/solarlog/const.py @@ -0,0 +1,4 @@ +"""Common const used across tests for SolarLog.""" + +NAME = "Solarlog test 1 2 3" +HOST = "http://1.1.1.1" diff --git a/tests/components/solarlog/fixtures/solarlog_data.json b/tests/components/solarlog/fixtures/solarlog_data.json new file mode 100644 index 00000000000..4976f4fa8b7 --- /dev/null +++ b/tests/components/solarlog/fixtures/solarlog_data.json @@ -0,0 +1,24 @@ +{ + "power_ac": 100, + "power_dc": 102, + "voltage_ac": 100, + "voltage_dc": 100, + "yield_day": 4.21, + "yield_yesterday": 5.21, + "yield_month": 515, + "yield_year": 1023, + "yield_total": 56513, + "consumption_ac": 54.87, + "consumption_day": 5.31, + "consumption_yesterday": 7.34, + "consumption_month": 758, + "consumption_year": 4587, + "consumption_total": 354687, + "total_power": 120, + "self_consumption_year": 545, + "alternator_loss": 2, + "efficiency": 0.9804, + "usage": 0.5487, + "power_available": 45.13, + "capacity": 0.85 +} diff --git a/tests/components/solarlog/snapshots/test_sensor.ambr b/tests/components/solarlog/snapshots/test_sensor.ambr new file mode 100644 index 00000000000..5080a001b84 --- /dev/null +++ b/tests/components/solarlog/snapshots/test_sensor.ambr @@ -0,0 +1,2183 @@ +# serializer version: 1 +# name: test_all_entities[sensor.solarlog_alternator_loss-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.solarlog_alternator_loss', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Alternator loss', + 'platform': 'solarlog', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'alternator_loss', + 'unique_id': 'ce5f5431554d101905d31797e1232da8_alternator_loss', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.solarlog_alternator_loss-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'solarlog Alternator loss', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.solarlog_alternator_loss', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2', + }) +# --- +# name: test_all_entities[sensor.solarlog_capacity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.solarlog_capacity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Capacity', + 'platform': 'solarlog', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'capacity', + 'unique_id': 'ce5f5431554d101905d31797e1232da8_capacity', + 'unit_of_measurement': '%', + }) +# --- +# name: test_all_entities[sensor.solarlog_capacity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power_factor', + 'friendly_name': 'solarlog Capacity', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.solarlog_capacity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '85.0', + }) +# --- +# name: test_all_entities[sensor.solarlog_consumption_ac-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.solarlog_consumption_ac', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Consumption AC', + 'platform': 'solarlog', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'consumption_ac', + 'unique_id': 'ce5f5431554d101905d31797e1232da8_consumption_ac', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.solarlog_consumption_ac-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'solarlog Consumption AC', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.solarlog_consumption_ac', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '54.87', + }) +# --- +# name: test_all_entities[sensor.solarlog_consumption_day-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.solarlog_consumption_day', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Consumption day', + 'platform': 'solarlog', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'consumption_day', + 'unique_id': 'ce5f5431554d101905d31797e1232da8_consumption_day', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.solarlog_consumption_day-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'solarlog Consumption day', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.solarlog_consumption_day', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.005', + }) +# --- +# name: test_all_entities[sensor.solarlog_consumption_month-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.solarlog_consumption_month', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Consumption month', + 'platform': 'solarlog', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'consumption_month', + 'unique_id': 'ce5f5431554d101905d31797e1232da8_consumption_month', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.solarlog_consumption_month-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'solarlog Consumption month', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.solarlog_consumption_month', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.758', + }) +# --- +# name: test_all_entities[sensor.solarlog_consumption_total-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.solarlog_consumption_total', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Consumption total', + 'platform': 'solarlog', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'consumption_total', + 'unique_id': 'ce5f5431554d101905d31797e1232da8_consumption_total', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.solarlog_consumption_total-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'solarlog Consumption total', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.solarlog_consumption_total', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '354.687', + }) +# --- +# name: test_all_entities[sensor.solarlog_consumption_year-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.solarlog_consumption_year', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Consumption year', + 'platform': 'solarlog', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'consumption_year', + 'unique_id': 'ce5f5431554d101905d31797e1232da8_consumption_year', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.solarlog_consumption_year-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'solarlog Consumption year', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.solarlog_consumption_year', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '4.587', + }) +# --- +# name: test_all_entities[sensor.solarlog_consumption_yesterday-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.solarlog_consumption_yesterday', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Consumption yesterday', + 'platform': 'solarlog', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'consumption_yesterday', + 'unique_id': 'ce5f5431554d101905d31797e1232da8_consumption_yesterday', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.solarlog_consumption_yesterday-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'solarlog Consumption yesterday', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.solarlog_consumption_yesterday', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.007', + }) +# --- +# name: test_all_entities[sensor.solarlog_efficiency-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.solarlog_efficiency', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Efficiency', + 'platform': 'solarlog', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'efficiency', + 'unique_id': 'ce5f5431554d101905d31797e1232da8_efficiency', + 'unit_of_measurement': '%', + }) +# --- +# name: test_all_entities[sensor.solarlog_efficiency-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power_factor', + 'friendly_name': 'solarlog Efficiency', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.solarlog_efficiency', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '98.0', + }) +# --- +# name: test_all_entities[sensor.solarlog_installed_peak_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.solarlog_installed_peak_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Installed peak power', + 'platform': 'solarlog', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'total_power', + 'unique_id': 'ce5f5431554d101905d31797e1232da8_total_power', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.solarlog_installed_peak_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'solarlog Installed peak power', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.solarlog_installed_peak_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '120', + }) +# --- +# name: test_all_entities[sensor.solarlog_last_update-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.solarlog_last_update', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Last update', + 'platform': 'solarlog', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'last_update', + 'unique_id': 'ce5f5431554d101905d31797e1232da8_last_updated', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[sensor.solarlog_last_update-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'timestamp', + 'friendly_name': 'solarlog Last update', + }), + 'context': , + 'entity_id': 'sensor.solarlog_last_update', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_entities[sensor.solarlog_power_ac-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.solarlog_power_ac', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Power AC', + 'platform': 'solarlog', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'power_ac', + 'unique_id': 'ce5f5431554d101905d31797e1232da8_power_ac', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.solarlog_power_ac-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'solarlog Power AC', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.solarlog_power_ac', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '100', + }) +# --- +# name: test_all_entities[sensor.solarlog_power_available-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.solarlog_power_available', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Power available', + 'platform': 'solarlog', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'power_available', + 'unique_id': 'ce5f5431554d101905d31797e1232da8_power_available', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.solarlog_power_available-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'solarlog Power available', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.solarlog_power_available', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '45.13', + }) +# --- +# name: test_all_entities[sensor.solarlog_power_dc-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.solarlog_power_dc', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Power DC', + 'platform': 'solarlog', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'power_dc', + 'unique_id': 'ce5f5431554d101905d31797e1232da8_power_dc', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.solarlog_power_dc-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'solarlog Power DC', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.solarlog_power_dc', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '102', + }) +# --- +# name: test_all_entities[sensor.solarlog_test_1_2_3_alternator_loss-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.solarlog_test_1_2_3_alternator_loss', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Alternator loss', + 'platform': 'solarlog', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'alternator_loss', + 'unique_id': 'ce5f5431554d101905d31797e1232da8_alternator_loss', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.solarlog_test_1_2_3_alternator_loss-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'solarlog_test_1_2_3 Alternator loss', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.solarlog_test_1_2_3_alternator_loss', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2', + }) +# --- +# name: test_all_entities[sensor.solarlog_test_1_2_3_capacity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.solarlog_test_1_2_3_capacity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Capacity', + 'platform': 'solarlog', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'capacity', + 'unique_id': 'ce5f5431554d101905d31797e1232da8_capacity', + 'unit_of_measurement': '%', + }) +# --- +# name: test_all_entities[sensor.solarlog_test_1_2_3_capacity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power_factor', + 'friendly_name': 'solarlog_test_1_2_3 Capacity', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.solarlog_test_1_2_3_capacity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '85.0', + }) +# --- +# name: test_all_entities[sensor.solarlog_test_1_2_3_consumption_ac-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.solarlog_test_1_2_3_consumption_ac', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Consumption AC', + 'platform': 'solarlog', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'consumption_ac', + 'unique_id': 'ce5f5431554d101905d31797e1232da8_consumption_ac', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.solarlog_test_1_2_3_consumption_ac-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'solarlog_test_1_2_3 Consumption AC', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.solarlog_test_1_2_3_consumption_ac', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '54.87', + }) +# --- +# name: test_all_entities[sensor.solarlog_test_1_2_3_consumption_day-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.solarlog_test_1_2_3_consumption_day', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Consumption day', + 'platform': 'solarlog', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'consumption_day', + 'unique_id': 'ce5f5431554d101905d31797e1232da8_consumption_day', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.solarlog_test_1_2_3_consumption_day-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'solarlog_test_1_2_3 Consumption day', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.solarlog_test_1_2_3_consumption_day', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.005', + }) +# --- +# name: test_all_entities[sensor.solarlog_test_1_2_3_consumption_month-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.solarlog_test_1_2_3_consumption_month', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Consumption month', + 'platform': 'solarlog', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'consumption_month', + 'unique_id': 'ce5f5431554d101905d31797e1232da8_consumption_month', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.solarlog_test_1_2_3_consumption_month-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'solarlog_test_1_2_3 Consumption month', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.solarlog_test_1_2_3_consumption_month', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.758', + }) +# --- +# name: test_all_entities[sensor.solarlog_test_1_2_3_consumption_total-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.solarlog_test_1_2_3_consumption_total', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Consumption total', + 'platform': 'solarlog', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'consumption_total', + 'unique_id': 'ce5f5431554d101905d31797e1232da8_consumption_total', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.solarlog_test_1_2_3_consumption_total-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'solarlog_test_1_2_3 Consumption total', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.solarlog_test_1_2_3_consumption_total', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '354.687', + }) +# --- +# name: test_all_entities[sensor.solarlog_test_1_2_3_consumption_year-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.solarlog_test_1_2_3_consumption_year', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Consumption year', + 'platform': 'solarlog', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'consumption_year', + 'unique_id': 'ce5f5431554d101905d31797e1232da8_consumption_year', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.solarlog_test_1_2_3_consumption_year-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'solarlog_test_1_2_3 Consumption year', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.solarlog_test_1_2_3_consumption_year', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '4.587', + }) +# --- +# name: test_all_entities[sensor.solarlog_test_1_2_3_consumption_yesterday-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.solarlog_test_1_2_3_consumption_yesterday', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Consumption yesterday', + 'platform': 'solarlog', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'consumption_yesterday', + 'unique_id': 'ce5f5431554d101905d31797e1232da8_consumption_yesterday', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.solarlog_test_1_2_3_consumption_yesterday-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'solarlog_test_1_2_3 Consumption yesterday', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.solarlog_test_1_2_3_consumption_yesterday', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.007', + }) +# --- +# name: test_all_entities[sensor.solarlog_test_1_2_3_efficiency-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.solarlog_test_1_2_3_efficiency', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Efficiency', + 'platform': 'solarlog', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'efficiency', + 'unique_id': 'ce5f5431554d101905d31797e1232da8_efficiency', + 'unit_of_measurement': '%', + }) +# --- +# name: test_all_entities[sensor.solarlog_test_1_2_3_efficiency-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power_factor', + 'friendly_name': 'solarlog_test_1_2_3 Efficiency', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.solarlog_test_1_2_3_efficiency', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '98.0', + }) +# --- +# name: test_all_entities[sensor.solarlog_test_1_2_3_installed_peak_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.solarlog_test_1_2_3_installed_peak_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Installed peak power', + 'platform': 'solarlog', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'total_power', + 'unique_id': 'ce5f5431554d101905d31797e1232da8_total_power', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.solarlog_test_1_2_3_installed_peak_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'solarlog_test_1_2_3 Installed peak power', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.solarlog_test_1_2_3_installed_peak_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '120', + }) +# --- +# name: test_all_entities[sensor.solarlog_test_1_2_3_last_update-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.solarlog_test_1_2_3_last_update', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Last update', + 'platform': 'solarlog', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'last_update', + 'unique_id': 'ce5f5431554d101905d31797e1232da8_last_updated', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[sensor.solarlog_test_1_2_3_last_update-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'timestamp', + 'friendly_name': 'solarlog_test_1_2_3 Last update', + }), + 'context': , + 'entity_id': 'sensor.solarlog_test_1_2_3_last_update', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_entities[sensor.solarlog_test_1_2_3_power_ac-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.solarlog_test_1_2_3_power_ac', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Power AC', + 'platform': 'solarlog', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'power_ac', + 'unique_id': 'ce5f5431554d101905d31797e1232da8_power_ac', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.solarlog_test_1_2_3_power_ac-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'solarlog_test_1_2_3 Power AC', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.solarlog_test_1_2_3_power_ac', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '100', + }) +# --- +# name: test_all_entities[sensor.solarlog_test_1_2_3_power_available-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.solarlog_test_1_2_3_power_available', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Power available', + 'platform': 'solarlog', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'power_available', + 'unique_id': 'ce5f5431554d101905d31797e1232da8_power_available', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.solarlog_test_1_2_3_power_available-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'solarlog_test_1_2_3 Power available', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.solarlog_test_1_2_3_power_available', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '45.13', + }) +# --- +# name: test_all_entities[sensor.solarlog_test_1_2_3_power_dc-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.solarlog_test_1_2_3_power_dc', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Power DC', + 'platform': 'solarlog', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'power_dc', + 'unique_id': 'ce5f5431554d101905d31797e1232da8_power_dc', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.solarlog_test_1_2_3_power_dc-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'solarlog_test_1_2_3 Power DC', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.solarlog_test_1_2_3_power_dc', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '102', + }) +# --- +# name: test_all_entities[sensor.solarlog_test_1_2_3_usage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.solarlog_test_1_2_3_usage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Usage', + 'platform': 'solarlog', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'usage', + 'unique_id': 'ce5f5431554d101905d31797e1232da8_usage', + 'unit_of_measurement': '%', + }) +# --- +# name: test_all_entities[sensor.solarlog_test_1_2_3_usage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power_factor', + 'friendly_name': 'solarlog_test_1_2_3 Usage', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.solarlog_test_1_2_3_usage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '54.9', + }) +# --- +# name: test_all_entities[sensor.solarlog_test_1_2_3_voltage_ac-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.solarlog_test_1_2_3_voltage_ac', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Voltage AC', + 'platform': 'solarlog', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'voltage_ac', + 'unique_id': 'ce5f5431554d101905d31797e1232da8_voltage_ac', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.solarlog_test_1_2_3_voltage_ac-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'solarlog_test_1_2_3 Voltage AC', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.solarlog_test_1_2_3_voltage_ac', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '100', + }) +# --- +# name: test_all_entities[sensor.solarlog_test_1_2_3_voltage_dc-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.solarlog_test_1_2_3_voltage_dc', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Voltage DC', + 'platform': 'solarlog', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'voltage_dc', + 'unique_id': 'ce5f5431554d101905d31797e1232da8_voltage_dc', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.solarlog_test_1_2_3_voltage_dc-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'solarlog_test_1_2_3 Voltage DC', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.solarlog_test_1_2_3_voltage_dc', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '100', + }) +# --- +# name: test_all_entities[sensor.solarlog_test_1_2_3_yield_day-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.solarlog_test_1_2_3_yield_day', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Yield day', + 'platform': 'solarlog', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'yield_day', + 'unique_id': 'ce5f5431554d101905d31797e1232da8_yield_day', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.solarlog_test_1_2_3_yield_day-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'solarlog_test_1_2_3 Yield day', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.solarlog_test_1_2_3_yield_day', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.004', + }) +# --- +# name: test_all_entities[sensor.solarlog_test_1_2_3_yield_month-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.solarlog_test_1_2_3_yield_month', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Yield month', + 'platform': 'solarlog', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'yield_month', + 'unique_id': 'ce5f5431554d101905d31797e1232da8_yield_month', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.solarlog_test_1_2_3_yield_month-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'solarlog_test_1_2_3 Yield month', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.solarlog_test_1_2_3_yield_month', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.515', + }) +# --- +# name: test_all_entities[sensor.solarlog_test_1_2_3_yield_total-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.solarlog_test_1_2_3_yield_total', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Yield total', + 'platform': 'solarlog', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'yield_total', + 'unique_id': 'ce5f5431554d101905d31797e1232da8_yield_total', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.solarlog_test_1_2_3_yield_total-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'solarlog_test_1_2_3 Yield total', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.solarlog_test_1_2_3_yield_total', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '56.513', + }) +# --- +# name: test_all_entities[sensor.solarlog_test_1_2_3_yield_year-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.solarlog_test_1_2_3_yield_year', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Yield year', + 'platform': 'solarlog', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'yield_year', + 'unique_id': 'ce5f5431554d101905d31797e1232da8_yield_year', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.solarlog_test_1_2_3_yield_year-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'solarlog_test_1_2_3 Yield year', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.solarlog_test_1_2_3_yield_year', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1.023', + }) +# --- +# name: test_all_entities[sensor.solarlog_test_1_2_3_yield_yesterday-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.solarlog_test_1_2_3_yield_yesterday', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Yield yesterday', + 'platform': 'solarlog', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'yield_yesterday', + 'unique_id': 'ce5f5431554d101905d31797e1232da8_yield_yesterday', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.solarlog_test_1_2_3_yield_yesterday-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'solarlog_test_1_2_3 Yield yesterday', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.solarlog_test_1_2_3_yield_yesterday', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.005', + }) +# --- +# name: test_all_entities[sensor.solarlog_usage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.solarlog_usage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Usage', + 'platform': 'solarlog', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'usage', + 'unique_id': 'ce5f5431554d101905d31797e1232da8_usage', + 'unit_of_measurement': '%', + }) +# --- +# name: test_all_entities[sensor.solarlog_usage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power_factor', + 'friendly_name': 'solarlog Usage', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.solarlog_usage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '54.9', + }) +# --- +# name: test_all_entities[sensor.solarlog_voltage_ac-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.solarlog_voltage_ac', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Voltage AC', + 'platform': 'solarlog', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'voltage_ac', + 'unique_id': 'ce5f5431554d101905d31797e1232da8_voltage_ac', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.solarlog_voltage_ac-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'solarlog Voltage AC', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.solarlog_voltage_ac', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '100', + }) +# --- +# name: test_all_entities[sensor.solarlog_voltage_dc-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.solarlog_voltage_dc', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Voltage DC', + 'platform': 'solarlog', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'voltage_dc', + 'unique_id': 'ce5f5431554d101905d31797e1232da8_voltage_dc', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.solarlog_voltage_dc-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'solarlog Voltage DC', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.solarlog_voltage_dc', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '100', + }) +# --- +# name: test_all_entities[sensor.solarlog_yield_day-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.solarlog_yield_day', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Yield day', + 'platform': 'solarlog', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'yield_day', + 'unique_id': 'ce5f5431554d101905d31797e1232da8_yield_day', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.solarlog_yield_day-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'solarlog Yield day', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.solarlog_yield_day', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.004', + }) +# --- +# name: test_all_entities[sensor.solarlog_yield_month-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.solarlog_yield_month', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Yield month', + 'platform': 'solarlog', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'yield_month', + 'unique_id': 'ce5f5431554d101905d31797e1232da8_yield_month', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.solarlog_yield_month-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'solarlog Yield month', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.solarlog_yield_month', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.515', + }) +# --- +# name: test_all_entities[sensor.solarlog_yield_total-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.solarlog_yield_total', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Yield total', + 'platform': 'solarlog', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'yield_total', + 'unique_id': 'ce5f5431554d101905d31797e1232da8_yield_total', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.solarlog_yield_total-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'solarlog Yield total', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.solarlog_yield_total', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '56.513', + }) +# --- +# name: test_all_entities[sensor.solarlog_yield_year-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.solarlog_yield_year', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Yield year', + 'platform': 'solarlog', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'yield_year', + 'unique_id': 'ce5f5431554d101905d31797e1232da8_yield_year', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.solarlog_yield_year-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'solarlog Yield year', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.solarlog_yield_year', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1.023', + }) +# --- +# name: test_all_entities[sensor.solarlog_yield_yesterday-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.solarlog_yield_yesterday', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Yield yesterday', + 'platform': 'solarlog', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'yield_yesterday', + 'unique_id': 'ce5f5431554d101905d31797e1232da8_yield_yesterday', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.solarlog_yield_yesterday-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'solarlog Yield yesterday', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.solarlog_yield_yesterday', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.005', + }) +# --- diff --git a/tests/components/solarlog/test_config_flow.py b/tests/components/solarlog/test_config_flow.py index 63df582b0e1..cb1092a73e3 100644 --- a/tests/components/solarlog/test_config_flow.py +++ b/tests/components/solarlog/test_config_flow.py @@ -12,10 +12,9 @@ from homeassistant.const import CONF_HOST, CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType -from tests.common import MockConfigEntry +from .const import HOST, NAME -NAME = "Solarlog test 1 2 3" -HOST = "http://1.1.1.1" +from tests.common import MockConfigEntry async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: @@ -56,7 +55,7 @@ def init_config_flow(hass): @pytest.mark.usefixtures("test_connect") async def test_user( hass: HomeAssistant, - mock_solarlog: AsyncMock, + mock_solarlog_connector: AsyncMock, mock_setup_entry: AsyncMock, ) -> None: """Test user config.""" @@ -89,7 +88,7 @@ async def test_form_exceptions( hass: HomeAssistant, exception: Exception, error: dict[str, str], - mock_solarlog: AsyncMock, + mock_solarlog_connector: AsyncMock, ) -> None: """Test we can handle Form exceptions.""" flow = init_config_flow(hass) @@ -98,7 +97,7 @@ async def test_form_exceptions( assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" - mock_solarlog.return_value.test_connection.side_effect = exception + mock_solarlog_connector.test_connection.side_effect = exception # tests with connection error result = await flow.async_step_user( @@ -110,7 +109,7 @@ async def test_form_exceptions( assert result["step_id"] == "user" assert result["errors"] == error - mock_solarlog.return_value.test_connection.side_effect = None + mock_solarlog_connector.test_connection.side_effect = None # tests with all provided result = await flow.async_step_user( diff --git a/tests/components/solarlog/test_init.py b/tests/components/solarlog/test_init.py index 9a8d6cb5bec..f9f00ef601b 100644 --- a/tests/components/solarlog/test_init.py +++ b/tests/components/solarlog/test_init.py @@ -1,16 +1,54 @@ """Test the initialization.""" +from unittest.mock import AsyncMock + +from solarlog_cli.solarlog_exceptions import SolarLogConnectionError + from homeassistant.components.solarlog.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_HOST, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceRegistry from homeassistant.helpers.entity_registry import EntityRegistry -from .test_config_flow import HOST, NAME +from . import setup_platform +from .const import HOST, NAME from tests.common import MockConfigEntry +async def test_load_unload( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_solarlog_connector: AsyncMock, +) -> None: + """Test load and unload.""" + + await setup_platform(hass, mock_config_entry, [Platform.SENSOR]) + assert mock_config_entry.state is ConfigEntryState.LOADED + + assert await hass.config_entries.async_unload(mock_config_entry.entry_id) + await hass.async_block_till_done() + assert mock_config_entry.state is ConfigEntryState.NOT_LOADED + + +async def test_raise_config_entry_not_ready_when_offline( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_solarlog_connector: AsyncMock, +) -> None: + """Config entry state is SETUP_RETRY when Solarlog is offline.""" + + mock_solarlog_connector.update_data.side_effect = SolarLogConnectionError + + await setup_platform(hass, mock_config_entry, [Platform.SENSOR]) + await hass.async_block_till_done() + + assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY + + assert len(hass.config_entries.flow.async_progress()) == 0 + + async def test_migrate_config_entry( hass: HomeAssistant, device_reg: DeviceRegistry, entity_reg: EntityRegistry ) -> None: diff --git a/tests/components/solarlog/test_sensor.py b/tests/components/solarlog/test_sensor.py new file mode 100644 index 00000000000..bc90e8b25c0 --- /dev/null +++ b/tests/components/solarlog/test_sensor.py @@ -0,0 +1,59 @@ +"""Test the Home Assistant solarlog sensor module.""" + +from datetime import timedelta +from unittest.mock import AsyncMock + +from freezegun.api import FrozenDateTimeFactory +import pytest +from solarlog_cli.solarlog_exceptions import ( + SolarLogConnectionError, + SolarLogUpdateError, +) +from syrupy import SnapshotAssertion + +from homeassistant.const import STATE_UNAVAILABLE, Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import setup_platform + +from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform + + +async def test_all_entities( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + mock_solarlog_connector: AsyncMock, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, +) -> None: + """Test all entities.""" + + await setup_platform(hass, mock_config_entry, [Platform.SENSOR]) + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + +@pytest.mark.parametrize( + "exception", + [ + SolarLogConnectionError, + SolarLogUpdateError, + ], +) +async def test_connection_error( + hass: HomeAssistant, + exception: Exception, + mock_solarlog_connector: AsyncMock, + mock_config_entry: MockConfigEntry, + freezer: FrozenDateTimeFactory, +) -> None: + """Test connection error.""" + await setup_platform(hass, mock_config_entry, [Platform.SENSOR]) + + mock_solarlog_connector.update_data.side_effect = exception + + freezer.tick(delta=timedelta(hours=12)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert hass.states.get("sensor.solarlog_power_ac").state == STATE_UNAVAILABLE From 6420837d58472e6d8ed3aeff8f26696bf643e955 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Fri, 21 Jun 2024 12:47:57 +0200 Subject: [PATCH 0905/1445] Calculate device class as soon as it is known in integral (#119940) --- .../components/integration/sensor.py | 46 ++++++++++--- .../integration/snapshots/test_sensor.ambr | 69 +++++++++++++++++++ tests/components/integration/test_sensor.py | 65 ++++++++++++++++- 3 files changed, 169 insertions(+), 11 deletions(-) create mode 100644 tests/components/integration/snapshots/test_sensor.ambr diff --git a/homeassistant/components/integration/sensor.py b/homeassistant/components/integration/sensor.py index 02451773558..d201fab0c6f 100644 --- a/homeassistant/components/integration/sensor.py +++ b/homeassistant/components/integration/sensor.py @@ -13,6 +13,7 @@ from typing import Any, Final, Self import voluptuous as vol from homeassistant.components.sensor import ( + DEVICE_CLASS_UNITS, PLATFORM_SCHEMA, RestoreSensor, SensorDeviceClass, @@ -75,6 +76,10 @@ UNIT_TIME = { UnitOfTime.DAYS: 24 * 60 * 60, } +DEVICE_CLASS_MAP = { + SensorDeviceClass.POWER: SensorDeviceClass.ENERGY, +} + DEFAULT_ROUND = 3 PLATFORM_SCHEMA = vol.All( @@ -381,6 +386,22 @@ class IntegrationSensor(RestoreSensor): return f"{self._unit_prefix_string}{integral_unit}" + def _calculate_device_class( + self, + source_device_class: SensorDeviceClass | None, + unit_of_measurement: str | None, + ) -> SensorDeviceClass | None: + """Deduce device class if possible from source device class and target unit.""" + if source_device_class is None: + return None + + if (device_class := DEVICE_CLASS_MAP.get(source_device_class)) is None: + return None + + if unit_of_measurement not in DEVICE_CLASS_UNITS.get(device_class, set()): + return None + return device_class + def _derive_and_set_attributes_from_state(self, source_state: State) -> None: source_unit = source_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) if source_unit is not None: @@ -389,13 +410,13 @@ class IntegrationSensor(RestoreSensor): # If the source has no defined unit we cannot derive a unit for the integral self._unit_of_measurement = None - if ( - self.device_class is None - and source_state.attributes.get(ATTR_DEVICE_CLASS) - == SensorDeviceClass.POWER - ): - self._attr_device_class = SensorDeviceClass.ENERGY - self._attr_icon = None # Remove this sensors icon default and allow to fallback to the ENERGY default + self._attr_device_class = self._calculate_device_class( + source_state.attributes.get(ATTR_DEVICE_CLASS), self.unit_of_measurement + ) + if self._attr_device_class: + self._attr_icon = None # Remove this sensors icon default and allow to fallback to the device class default + else: + self._attr_icon = "mdi:chart-histogram" def _update_integral(self, area: Decimal) -> None: area_scaled = area / (self._unit_prefix * self._unit_time) @@ -436,6 +457,11 @@ class IntegrationSensor(RestoreSensor): else: handle_state_change = self._integrate_on_state_change_callback + if ( + state := self.hass.states.get(self._source_entity) + ) and state.state != STATE_UNAVAILABLE: + self._derive_and_set_attributes_from_state(state) + self.async_on_remove( async_track_state_change_event( self.hass, @@ -477,7 +503,7 @@ class IntegrationSensor(RestoreSensor): def _integrate_on_state_change( self, old_state: State | None, new_state: State | None ) -> None: - if old_state is None or new_state is None: + if new_state is None: return if new_state.state == STATE_UNAVAILABLE: @@ -488,6 +514,10 @@ class IntegrationSensor(RestoreSensor): self._attr_available = True self._derive_and_set_attributes_from_state(new_state) + if old_state is None: + self.async_write_ha_state() + return + if not (states := self._method.validate_states(old_state, new_state)): self.async_write_ha_state() return diff --git a/tests/components/integration/snapshots/test_sensor.ambr b/tests/components/integration/snapshots/test_sensor.ambr new file mode 100644 index 00000000000..5747e6489b9 --- /dev/null +++ b/tests/components/integration/snapshots/test_sensor.ambr @@ -0,0 +1,69 @@ +# serializer version: 1 +# name: test_initial_state[BTU/h-power-h] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'integration', + 'icon': 'mdi:chart-histogram', + 'source': 'sensor.source', + 'state_class': , + 'unit_of_measurement': 'BTU', + }), + 'context': , + 'entity_id': 'sensor.integration', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_initial_state[ft\xb3/min-volume_flow_rate-min] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'integration', + 'icon': 'mdi:chart-histogram', + 'source': 'sensor.source', + 'state_class': , + 'unit_of_measurement': 'ft³', + }), + 'context': , + 'entity_id': 'sensor.integration', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_initial_state[kW-None-h] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'integration', + 'icon': 'mdi:chart-histogram', + 'source': 'sensor.source', + 'state_class': , + 'unit_of_measurement': 'kWh', + }), + 'context': , + 'entity_id': 'sensor.integration', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_initial_state[kW-power-h] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'integration', + 'source': 'sensor.source', + 'state_class': , + 'unit_of_measurement': 'kWh', + }), + 'context': , + 'entity_id': 'sensor.integration', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- diff --git a/tests/components/integration/test_sensor.py b/tests/components/integration/test_sensor.py index 1a729f6254e..243504cb3e0 100644 --- a/tests/components/integration/test_sensor.py +++ b/tests/components/integration/test_sensor.py @@ -5,10 +5,12 @@ from typing import Any from freezegun import freeze_time import pytest +from syrupy.assertion import SnapshotAssertion from homeassistant.components.integration.const import DOMAIN from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass from homeassistant.const import ( + ATTR_DEVICE_CLASS, ATTR_UNIT_OF_MEASUREMENT, STATE_UNAVAILABLE, STATE_UNKNOWN, @@ -17,6 +19,7 @@ from homeassistant.const import ( UnitOfInformation, UnitOfPower, UnitOfTime, + UnitOfVolumeFlowRate, ) from homeassistant.core import HomeAssistant, State from homeassistant.helpers import ( @@ -36,6 +39,52 @@ from tests.common import ( DEFAULT_MAX_SUB_INTERVAL = {"minutes": 1} +@pytest.mark.parametrize( + ("unit_of_measurement", "device_class", "unit_time"), + [ + (UnitOfPower.KILO_WATT, SensorDeviceClass.POWER, "h"), + (UnitOfPower.KILO_WATT, None, "h"), + (UnitOfPower.BTU_PER_HOUR, SensorDeviceClass.POWER, "h"), + ( + UnitOfVolumeFlowRate.CUBIC_FEET_PER_MINUTE, + SensorDeviceClass.VOLUME_FLOW_RATE, + "min", + ), + ], +) +async def test_initial_state( + hass: HomeAssistant, + unit_of_measurement: str, + device_class: SensorDeviceClass, + unit_time: str, + snapshot: SnapshotAssertion, +) -> None: + """Test integration sensor state.""" + config = { + "sensor": { + "platform": "integration", + "name": "integration", + "source": "sensor.source", + "round": 2, + "method": "left", + "unit_time": unit_time, + } + } + + assert await async_setup_component(hass, "sensor", config) + hass.states.async_set( + "sensor.source", + "1", + { + ATTR_DEVICE_CLASS: device_class, + ATTR_UNIT_OF_MEASUREMENT: unit_of_measurement, + }, + ) + await hass.async_block_till_done() + + assert hass.states.get("sensor.integration") == snapshot + + @pytest.mark.parametrize("method", ["trapezoidal", "left", "right"]) async def test_state(hass: HomeAssistant, method) -> None: """Test integration sensor state.""" @@ -49,13 +98,23 @@ async def test_state(hass: HomeAssistant, method) -> None: } } + assert await async_setup_component(hass, "sensor", config) + await hass.async_block_till_done() + + state = hass.states.get("sensor.integration") + assert state is not None + assert state.attributes.get("state_class") is SensorStateClass.TOTAL + assert "device_class" not in state.attributes + now = dt_util.utcnow() with freeze_time(now): - assert await async_setup_component(hass, "sensor", config) - entity_id = config["sensor"]["source"] hass.states.async_set( - entity_id, 1, {ATTR_UNIT_OF_MEASUREMENT: UnitOfPower.KILO_WATT} + entity_id, + 1, + { + ATTR_UNIT_OF_MEASUREMENT: UnitOfPower.KILO_WATT, + }, ) await hass.async_block_till_done() From 127af149ca45f7242138608f9aa1fff43f6e2482 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 21 Jun 2024 12:53:55 +0200 Subject: [PATCH 0906/1445] Remove legacy template hass config option (#119925) --- homeassistant/config.py | 39 +----------------------- tests/helpers/test_template.py | 17 ----------- tests/test_config.py | 55 +++++++++++++++++----------------- 3 files changed, 28 insertions(+), 83 deletions(-) diff --git a/homeassistant/config.py b/homeassistant/config.py index 751eaca7376..8e22f2051f0 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -292,41 +292,6 @@ def _raise_issue_if_no_country(hass: HomeAssistant, country: str | None) -> None ) -def _raise_issue_if_legacy_templates( - hass: HomeAssistant, legacy_templates: bool | None -) -> None: - # legacy_templates can have the following values: - # - None: Using default value (False) -> Delete repair issues - # - True: Create repair to adopt templates to new syntax - # - False: Create repair to tell user to remove config key - if legacy_templates: - ir.async_create_issue( - hass, - HA_DOMAIN, - "legacy_templates_true", - is_fixable=False, - breaks_in_ha_version="2024.7.0", - severity=ir.IssueSeverity.WARNING, - translation_key="legacy_templates_true", - ) - return - - ir.async_delete_issue(hass, HA_DOMAIN, "legacy_templates_true") - - if legacy_templates is False: - ir.async_create_issue( - hass, - HA_DOMAIN, - "legacy_templates_false", - is_fixable=False, - breaks_in_ha_version="2024.7.0", - severity=ir.IssueSeverity.WARNING, - translation_key="legacy_templates_false", - ) - else: - ir.async_delete_issue(hass, HA_DOMAIN, "legacy_templates_false") - - def _validate_currency(data: Any) -> Any: try: return cv.currency(data) @@ -391,7 +356,7 @@ CORE_CONFIG_SCHEMA = vol.All( _no_duplicate_auth_mfa_module, ), vol.Optional(CONF_MEDIA_DIRS): cv.schema_with_slug_keys(vol.IsDir()), - vol.Optional(CONF_LEGACY_TEMPLATES): cv.boolean, + vol.Remove(CONF_LEGACY_TEMPLATES): cv.boolean, vol.Optional(CONF_CURRENCY): _validate_currency, vol.Optional(CONF_COUNTRY): cv.country, vol.Optional(CONF_LANGUAGE): cv.language, @@ -897,7 +862,6 @@ async def async_process_ha_core_config(hass: HomeAssistant, config: dict) -> Non (CONF_INTERNAL_URL, "internal_url"), (CONF_EXTERNAL_URL, "external_url"), (CONF_MEDIA_DIRS, "media_dirs"), - (CONF_LEGACY_TEMPLATES, "legacy_templates"), (CONF_CURRENCY, "currency"), (CONF_COUNTRY, "country"), (CONF_LANGUAGE, "language"), @@ -909,7 +873,6 @@ async def async_process_ha_core_config(hass: HomeAssistant, config: dict) -> Non if config.get(CONF_DEBUG): hac.debug = True - _raise_issue_if_legacy_templates(hass, config.get(CONF_LEGACY_TEMPLATES)) _raise_issue_if_historic_currency(hass, hass.config.currency) _raise_issue_if_no_country(hass, hass.config.country) diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index 0547ddf8823..26e4f986592 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -18,7 +18,6 @@ import pytest import voluptuous as vol from homeassistant.components import group -from homeassistant.config import async_process_ha_core_config from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, STATE_ON, @@ -5402,22 +5401,6 @@ async def test_unavailable_states(hass: HomeAssistant) -> None: assert tpl.async_render() == "light.none, light.unavailable, light.unknown" -async def test_legacy_templates(hass: HomeAssistant) -> None: - """Test if old template behavior works when legacy templates are enabled.""" - hass.states.async_set("sensor.temperature", "12") - - assert ( - template.Template("{{ states.sensor.temperature.state }}", hass).async_render() - == 12 - ) - - await async_process_ha_core_config(hass, {"legacy_templates": True}) - assert ( - template.Template("{{ states.sensor.temperature.state }}", hass).async_render() - == "12" - ) - - async def test_no_result_parsing(hass: HomeAssistant) -> None: """Test if templates results are not parsed.""" hass.states.async_set("sensor.temperature", "12") diff --git a/tests/test_config.py b/tests/test_config.py index 51c72472a4f..7f94317afea 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -864,7 +864,6 @@ async def test_loading_configuration(hass: HomeAssistant) -> None: "external_url": "https://www.example.com", "internal_url": "http://example.local", "media_dirs": {"mymedia": "/usr"}, - "legacy_templates": True, "debug": True, "currency": "EUR", "country": "SE", @@ -886,7 +885,6 @@ async def test_loading_configuration(hass: HomeAssistant) -> None: assert "/usr" in hass.config.allowlist_external_dirs assert hass.config.media_dirs == {"mymedia": "/usr"} assert hass.config.config_source is ConfigSource.YAML - assert hass.config.legacy_templates is True assert hass.config.debug is True assert hass.config.currency == "EUR" assert hass.config.country == "SE" @@ -2044,32 +2042,6 @@ async def test_core_config_schema_no_country( assert issue -@pytest.mark.parametrize( - ("config", "expected_issue"), - [ - ({}, None), - ({"legacy_templates": True}, "legacy_templates_true"), - ({"legacy_templates": False}, "legacy_templates_false"), - ], -) -async def test_core_config_schema_legacy_template( - hass: HomeAssistant, - config: dict[str, Any], - expected_issue: str | None, - issue_registry: ir.IssueRegistry, -) -> None: - """Test legacy_template core config schema.""" - await config_util.async_process_ha_core_config(hass, config) - - for issue_id in ("legacy_templates_true", "legacy_templates_false"): - issue = issue_registry.async_get_issue("homeassistant", issue_id) - assert issue if issue_id == expected_issue else not issue - - await config_util.async_process_ha_core_config(hass, {}) - for issue_id in ("legacy_templates_true", "legacy_templates_false"): - assert not issue_registry.async_get_issue("homeassistant", issue_id) - - async def test_core_store_no_country( hass: HomeAssistant, hass_storage: dict[str, Any], issue_registry: ir.IssueRegistry ) -> None: @@ -2511,3 +2483,30 @@ async def test_loading_platforms_gathers(hass: HomeAssistant) -> None: ("platform_int", "sensor"), ("platform_int2", "sensor"), ] + + +async def test_configuration_legacy_template_is_removed(hass: HomeAssistant) -> None: + """Test loading core config onto hass object.""" + await config_util.async_process_ha_core_config( + hass, + { + "latitude": 60, + "longitude": 50, + "elevation": 25, + "name": "Huis", + "unit_system": "imperial", + "time_zone": "America/New_York", + "allowlist_external_dirs": "/etc", + "external_url": "https://www.example.com", + "internal_url": "http://example.local", + "media_dirs": {"mymedia": "/usr"}, + "legacy_templates": True, + "debug": True, + "currency": "EUR", + "country": "SE", + "language": "sv", + "radius": 150, + }, + ) + + assert not getattr(hass.config, "legacy_templates") From e1a6ac59e13fc3c62e5e4832b957c46b9e2e8888 Mon Sep 17 00:00:00 2001 From: Rami Mosleh Date: Fri, 21 Jun 2024 13:58:33 +0300 Subject: [PATCH 0907/1445] Move transmission services registration to async_setup (#119593) Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> --- .../components/transmission/__init__.py | 127 +++++++++++------- 1 file changed, 75 insertions(+), 52 deletions(-) diff --git a/homeassistant/components/transmission/__init__.py b/homeassistant/components/transmission/__init__.py index 06f27a1e605..37771430199 100644 --- a/homeassistant/components/transmission/__init__.py +++ b/homeassistant/components/transmission/__init__.py @@ -28,12 +28,17 @@ from homeassistant.const import ( Platform, ) from homeassistant.core import HomeAssistant, ServiceCall, callback -from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady +from homeassistant.exceptions import ( + ConfigEntryAuthFailed, + ConfigEntryNotReady, + HomeAssistantError, +) from homeassistant.helpers import ( config_validation as cv, entity_registry as er, selector, ) +from homeassistant.helpers.typing import ConfigType from .const import ( ATTR_DELETE_DATA, @@ -102,9 +107,17 @@ SERVICE_STOP_TORRENT_SCHEMA = vol.All( ) ) +CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN) + type TransmissionConfigEntry = ConfigEntry[TransmissionDataUpdateCoordinator] +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up the Transmission component.""" + setup_hass_services(hass) + return True + + async def async_setup_entry( hass: HomeAssistant, config_entry: TransmissionConfigEntry ) -> bool: @@ -143,9 +156,63 @@ async def async_setup_entry( await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) + return True + + +async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: + """Unload Transmission Entry from config_entry.""" + return await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS) + + +async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: + """Migrate an old config entry.""" + _LOGGER.debug( + "Migrating from version %s.%s", + config_entry.version, + config_entry.minor_version, + ) + + if config_entry.version == 1: + # Version 1.2 adds ssl and path + if config_entry.minor_version < 2: + new = {**config_entry.data} + + new[CONF_PATH] = DEFAULT_PATH + new[CONF_SSL] = DEFAULT_SSL + + hass.config_entries.async_update_entry( + config_entry, data=new, version=1, minor_version=2 + ) + + _LOGGER.debug( + "Migration to version %s.%s successful", + config_entry.version, + config_entry.minor_version, + ) + + return True + + +def _get_coordinator_from_service_data( + hass: HomeAssistant, entry_id: str +) -> TransmissionDataUpdateCoordinator: + """Return coordinator for entry id.""" + entry: TransmissionConfigEntry | None = hass.config_entries.async_get_entry( + entry_id + ) + if entry is None or entry.state is not ConfigEntryState.LOADED: + raise HomeAssistantError(f"Config entry {entry_id} is not found or not loaded") + return entry.runtime_data + + +def setup_hass_services(hass: HomeAssistant) -> None: + """Home Assistant services.""" + async def add_torrent(service: ServiceCall) -> None: """Add new torrent to download.""" - torrent = service.data[ATTR_TORRENT] + entry_id: str = service.data[CONF_ENTRY_ID] + coordinator = _get_coordinator_from_service_data(hass, entry_id) + torrent: str = service.data[ATTR_TORRENT] if torrent.startswith( ("http", "ftp:", "magnet:") ) or hass.config.is_allowed_path(torrent): @@ -156,18 +223,24 @@ async def async_setup_entry( async def start_torrent(service: ServiceCall) -> None: """Start torrent.""" + entry_id: str = service.data[CONF_ENTRY_ID] + coordinator = _get_coordinator_from_service_data(hass, entry_id) torrent_id = service.data[CONF_ID] await hass.async_add_executor_job(coordinator.api.start_torrent, torrent_id) await coordinator.async_request_refresh() async def stop_torrent(service: ServiceCall) -> None: """Stop torrent.""" + entry_id: str = service.data[CONF_ENTRY_ID] + coordinator = _get_coordinator_from_service_data(hass, entry_id) torrent_id = service.data[CONF_ID] await hass.async_add_executor_job(coordinator.api.stop_torrent, torrent_id) await coordinator.async_request_refresh() async def remove_torrent(service: ServiceCall) -> None: """Remove torrent.""" + entry_id: str = service.data[CONF_ENTRY_ID] + coordinator = _get_coordinator_from_service_data(hass, entry_id) torrent_id = service.data[CONF_ID] delete_data = service.data[ATTR_DELETE_DATA] await hass.async_add_executor_job( @@ -200,56 +273,6 @@ async def async_setup_entry( schema=SERVICE_STOP_TORRENT_SCHEMA, ) - return True - - -async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: - """Unload Transmission Entry from config_entry.""" - if unload_ok := await hass.config_entries.async_unload_platforms( - config_entry, PLATFORMS - ): - loaded_entries = [ - entry - for entry in hass.config_entries.async_entries(DOMAIN) - if entry.state == ConfigEntryState.LOADED - ] - if len(loaded_entries) == 1: - hass.services.async_remove(DOMAIN, SERVICE_ADD_TORRENT) - hass.services.async_remove(DOMAIN, SERVICE_REMOVE_TORRENT) - hass.services.async_remove(DOMAIN, SERVICE_START_TORRENT) - hass.services.async_remove(DOMAIN, SERVICE_STOP_TORRENT) - - return unload_ok - - -async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: - """Migrate an old config entry.""" - _LOGGER.debug( - "Migrating from version %s.%s", - config_entry.version, - config_entry.minor_version, - ) - - if config_entry.version == 1: - # Version 1.2 adds ssl and path - if config_entry.minor_version < 2: - new = {**config_entry.data} - - new[CONF_PATH] = DEFAULT_PATH - new[CONF_SSL] = DEFAULT_SSL - - hass.config_entries.async_update_entry( - config_entry, data=new, version=1, minor_version=2 - ) - - _LOGGER.debug( - "Migration to version %s.%s successful", - config_entry.version, - config_entry.minor_version, - ) - - return True - async def get_api( hass: HomeAssistant, entry: dict[str, Any] From ed7a888c072995d3a01c54a00797b41c2637e359 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 21 Jun 2024 12:59:31 +0200 Subject: [PATCH 0908/1445] Add one UniFi sensor test to validate entity attributes (#119914) --- .../unifi/snapshots/test_sensor.ambr | 262 ++++++++++++++++++ tests/components/unifi/test_sensor.py | 75 ++++- 2 files changed, 336 insertions(+), 1 deletion(-) create mode 100644 tests/components/unifi/snapshots/test_sensor.ambr diff --git a/tests/components/unifi/snapshots/test_sensor.ambr b/tests/components/unifi/snapshots/test_sensor.ambr new file mode 100644 index 00000000000..531da06f7c7 --- /dev/null +++ b/tests/components/unifi/snapshots/test_sensor.ambr @@ -0,0 +1,262 @@ +# serializer version: 1 +# name: test_sensor_sources[client_payload0-sensor.wired_client_rx-config_entry_options0] + 'data_rate' +# --- +# name: test_sensor_sources[client_payload0-sensor.wired_client_rx-config_entry_options0].1 + 'data_rate' +# --- +# name: test_sensor_sources[client_payload0-sensor.wired_client_rx-config_entry_options0].2 + +# --- +# name: test_sensor_sources[client_payload0-sensor.wired_client_rx-config_entry_options0].3 + +# --- +# name: test_sensor_sources[client_payload0-sensor.wired_client_rx-config_entry_options0].4 + '1234.0' +# --- +# name: test_sensor_sources[client_payload0-sensor.wired_client_rx-rx--config_entry_options0] + 'rx-00:00:00:00:00:01' +# --- +# name: test_sensor_sources[client_payload0-sensor.wired_client_rx-rx--config_entry_options0].1 + +# --- +# name: test_sensor_sources[client_payload0-sensor.wired_client_rx-rx--config_entry_options0].2 + 'data_rate' +# --- +# name: test_sensor_sources[client_payload0-sensor.wired_client_rx-rx--config_entry_options0].3 + 'Wired client RX' +# --- +# name: test_sensor_sources[client_payload0-sensor.wired_client_rx-rx--config_entry_options0].4 + +# --- +# name: test_sensor_sources[client_payload0-sensor.wired_client_rx-rx--config_entry_options0].5 + +# --- +# name: test_sensor_sources[client_payload0-sensor.wired_client_rx-rx--config_entry_options0].6 + '1234.0' +# --- +# name: test_sensor_sources[client_payload0-sensor.wired_client_uptime-uptime--config_entry_options0] + 'uptime-00:00:00:00:00:01' +# --- +# name: test_sensor_sources[client_payload0-sensor.wired_client_uptime-uptime--config_entry_options0].1 + +# --- +# name: test_sensor_sources[client_payload0-sensor.wired_client_uptime-uptime--config_entry_options0].2 + 'timestamp' +# --- +# name: test_sensor_sources[client_payload0-sensor.wired_client_uptime-uptime--config_entry_options0].3 + 'Wired client Uptime' +# --- +# name: test_sensor_sources[client_payload0-sensor.wired_client_uptime-uptime--config_entry_options0].4 + None +# --- +# name: test_sensor_sources[client_payload0-sensor.wired_client_uptime-uptime--config_entry_options0].5 + None +# --- +# name: test_sensor_sources[client_payload0-sensor.wired_client_uptime-uptime--config_entry_options0].6 + '2020-09-14T14:41:45+00:00' +# --- +# name: test_sensor_sources[client_payload1-sensor.wired_client_rx-rx--config_entry_options0] + 'rx-00:00:00:00:00:01' +# --- +# name: test_sensor_sources[client_payload1-sensor.wired_client_rx-rx--config_entry_options0].1 + +# --- +# name: test_sensor_sources[client_payload1-sensor.wired_client_rx-rx--config_entry_options0].2 + 'data_rate' +# --- +# name: test_sensor_sources[client_payload1-sensor.wired_client_rx-rx--config_entry_options0].3 + 'Wired client RX' +# --- +# name: test_sensor_sources[client_payload1-sensor.wired_client_rx-rx--config_entry_options0].4 + +# --- +# name: test_sensor_sources[client_payload1-sensor.wired_client_rx-rx--config_entry_options0].5 + +# --- +# name: test_sensor_sources[client_payload1-sensor.wired_client_rx-rx--config_entry_options0].6 + '1234.0' +# --- +# name: test_sensor_sources[client_payload1-sensor.wired_client_tx-config_entry_options0] + 'data_rate' +# --- +# name: test_sensor_sources[client_payload1-sensor.wired_client_tx-config_entry_options0].1 + 'data_rate' +# --- +# name: test_sensor_sources[client_payload1-sensor.wired_client_tx-config_entry_options0].2 + +# --- +# name: test_sensor_sources[client_payload1-sensor.wired_client_tx-config_entry_options0].3 + +# --- +# name: test_sensor_sources[client_payload1-sensor.wired_client_tx-config_entry_options0].4 + '5678.0' +# --- +# name: test_sensor_sources[client_payload1-sensor.wired_client_tx-tx--config_entry_options0] + 'tx-00:00:00:00:00:01' +# --- +# name: test_sensor_sources[client_payload1-sensor.wired_client_tx-tx--config_entry_options0].1 + +# --- +# name: test_sensor_sources[client_payload1-sensor.wired_client_tx-tx--config_entry_options0].2 + 'data_rate' +# --- +# name: test_sensor_sources[client_payload1-sensor.wired_client_tx-tx--config_entry_options0].3 + 'Wired client TX' +# --- +# name: test_sensor_sources[client_payload1-sensor.wired_client_tx-tx--config_entry_options0].4 + +# --- +# name: test_sensor_sources[client_payload1-sensor.wired_client_tx-tx--config_entry_options0].5 + +# --- +# name: test_sensor_sources[client_payload1-sensor.wired_client_tx-tx--config_entry_options0].6 + '5678.0' +# --- +# name: test_sensor_sources[client_payload2-sensor.wired_client_tx-tx--config_entry_options0] + 'tx-00:00:00:00:00:01' +# --- +# name: test_sensor_sources[client_payload2-sensor.wired_client_tx-tx--config_entry_options0].1 + +# --- +# name: test_sensor_sources[client_payload2-sensor.wired_client_tx-tx--config_entry_options0].2 + 'data_rate' +# --- +# name: test_sensor_sources[client_payload2-sensor.wired_client_tx-tx--config_entry_options0].3 + 'Wired client TX' +# --- +# name: test_sensor_sources[client_payload2-sensor.wired_client_tx-tx--config_entry_options0].4 + +# --- +# name: test_sensor_sources[client_payload2-sensor.wired_client_tx-tx--config_entry_options0].5 + +# --- +# name: test_sensor_sources[client_payload2-sensor.wired_client_tx-tx--config_entry_options0].6 + '5678.0' +# --- +# name: test_sensor_sources[client_payload2-sensor.wired_client_uptime-uptime--config_entry_options0] + 'uptime-00:00:00:00:00:01' +# --- +# name: test_sensor_sources[client_payload2-sensor.wired_client_uptime-uptime--config_entry_options0].1 + +# --- +# name: test_sensor_sources[client_payload2-sensor.wired_client_uptime-uptime--config_entry_options0].2 + 'timestamp' +# --- +# name: test_sensor_sources[client_payload2-sensor.wired_client_uptime-uptime--config_entry_options0].3 + 'Wired client Uptime' +# --- +# name: test_sensor_sources[client_payload2-sensor.wired_client_uptime-uptime--config_entry_options0].4 + None +# --- +# name: test_sensor_sources[client_payload2-sensor.wired_client_uptime-uptime--config_entry_options0].5 + None +# --- +# name: test_sensor_sources[client_payload2-sensor.wired_client_uptime-uptime--config_entry_options0].6 + '2020-09-14T14:41:45+00:00' +# --- +# name: test_sensor_sources[client_payload2-sensor.wireless_client_rx-rx--config_entry_options0] + 'rx-00:00:00:00:00:02' +# --- +# name: test_sensor_sources[client_payload2-sensor.wireless_client_rx-rx--config_entry_options0].1 + +# --- +# name: test_sensor_sources[client_payload2-sensor.wireless_client_rx-rx--config_entry_options0].2 + 'data_rate' +# --- +# name: test_sensor_sources[client_payload2-sensor.wireless_client_rx-rx--config_entry_options0].3 + 'Wireless client RX' +# --- +# name: test_sensor_sources[client_payload2-sensor.wireless_client_rx-rx--config_entry_options0].4 + +# --- +# name: test_sensor_sources[client_payload2-sensor.wireless_client_rx-rx--config_entry_options0].5 + +# --- +# name: test_sensor_sources[client_payload2-sensor.wireless_client_rx-rx--config_entry_options0].6 + '2345.0' +# --- +# name: test_sensor_sources[client_payload3-sensor.wireless_client_rx-rx--config_entry_options0] + 'rx-00:00:00:00:00:01' +# --- +# name: test_sensor_sources[client_payload3-sensor.wireless_client_rx-rx--config_entry_options0].1 + +# --- +# name: test_sensor_sources[client_payload3-sensor.wireless_client_rx-rx--config_entry_options0].2 + 'data_rate' +# --- +# name: test_sensor_sources[client_payload3-sensor.wireless_client_rx-rx--config_entry_options0].3 + 'Wireless client RX' +# --- +# name: test_sensor_sources[client_payload3-sensor.wireless_client_rx-rx--config_entry_options0].4 + +# --- +# name: test_sensor_sources[client_payload3-sensor.wireless_client_rx-rx--config_entry_options0].5 + +# --- +# name: test_sensor_sources[client_payload3-sensor.wireless_client_rx-rx--config_entry_options0].6 + '2345.0' +# --- +# name: test_sensor_sources[client_payload3-sensor.wireless_client_tx-tx--config_entry_options0] + 'tx-00:00:00:00:00:02' +# --- +# name: test_sensor_sources[client_payload3-sensor.wireless_client_tx-tx--config_entry_options0].1 + +# --- +# name: test_sensor_sources[client_payload3-sensor.wireless_client_tx-tx--config_entry_options0].2 + 'data_rate' +# --- +# name: test_sensor_sources[client_payload3-sensor.wireless_client_tx-tx--config_entry_options0].3 + 'Wireless client TX' +# --- +# name: test_sensor_sources[client_payload3-sensor.wireless_client_tx-tx--config_entry_options0].4 + +# --- +# name: test_sensor_sources[client_payload3-sensor.wireless_client_tx-tx--config_entry_options0].5 + +# --- +# name: test_sensor_sources[client_payload3-sensor.wireless_client_tx-tx--config_entry_options0].6 + '6789.0' +# --- +# name: test_sensor_sources[client_payload4-sensor.wireless_client_tx-tx--config_entry_options0] + 'tx-00:00:00:00:00:01' +# --- +# name: test_sensor_sources[client_payload4-sensor.wireless_client_tx-tx--config_entry_options0].1 + +# --- +# name: test_sensor_sources[client_payload4-sensor.wireless_client_tx-tx--config_entry_options0].2 + 'data_rate' +# --- +# name: test_sensor_sources[client_payload4-sensor.wireless_client_tx-tx--config_entry_options0].3 + 'Wireless client TX' +# --- +# name: test_sensor_sources[client_payload4-sensor.wireless_client_tx-tx--config_entry_options0].4 + +# --- +# name: test_sensor_sources[client_payload4-sensor.wireless_client_tx-tx--config_entry_options0].5 + +# --- +# name: test_sensor_sources[client_payload4-sensor.wireless_client_tx-tx--config_entry_options0].6 + '6789.0' +# --- +# name: test_sensor_sources[client_payload5-sensor.wireless_client_uptime-uptime--config_entry_options0] + 'uptime-00:00:00:00:00:01' +# --- +# name: test_sensor_sources[client_payload5-sensor.wireless_client_uptime-uptime--config_entry_options0].1 + +# --- +# name: test_sensor_sources[client_payload5-sensor.wireless_client_uptime-uptime--config_entry_options0].2 + 'timestamp' +# --- +# name: test_sensor_sources[client_payload5-sensor.wireless_client_uptime-uptime--config_entry_options0].3 + 'Wireless client Uptime' +# --- +# name: test_sensor_sources[client_payload5-sensor.wireless_client_uptime-uptime--config_entry_options0].4 + None +# --- +# name: test_sensor_sources[client_payload5-sensor.wireless_client_uptime-uptime--config_entry_options0].5 + None +# --- +# name: test_sensor_sources[client_payload5-sensor.wireless_client_uptime-uptime--config_entry_options0].6 + '2021-01-01T01:00:00+00:00' +# --- diff --git a/tests/components/unifi/test_sensor.py b/tests/components/unifi/test_sensor.py index 802166068b2..960a5d3e529 100644 --- a/tests/components/unifi/test_sensor.py +++ b/tests/components/unifi/test_sensor.py @@ -11,6 +11,7 @@ from aiounifi.models.device import DeviceState from aiounifi.models.message import MessageKey from freezegun.api import FrozenDateTimeFactory, freeze_time import pytest +from syrupy.assertion import SnapshotAssertion from homeassistant.components.sensor import ( ATTR_STATE_CLASS, @@ -29,7 +30,13 @@ from homeassistant.components.unifi.const import ( DEVICE_STATES, ) from homeassistant.config_entries import RELOAD_AFTER_UPDATE_DELAY, ConfigEntry -from homeassistant.const import ATTR_DEVICE_CLASS, STATE_UNAVAILABLE, EntityCategory +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_FRIENDLY_NAME, + ATTR_UNIT_OF_MEASUREMENT, + STATE_UNAVAILABLE, + EntityCategory, +) from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity_registry import RegistryEntryDisabler @@ -1332,3 +1339,69 @@ async def test_device_client_sensors( assert hass.states.get("sensor.wired_device_clients").state == "2" assert hass.states.get("sensor.wireless_device_clients").state == "0" + + +WIRED_CLIENT = { + "hostname": "Wired client", + "is_wired": True, + "mac": "00:00:00:00:00:01", + "oui": "Producer", + "wired-rx_bytes-r": 1234000000, + "wired-tx_bytes-r": 5678000000, + "uptime": 1600094505, +} +WIRELESS_CLIENT = { + "is_wired": False, + "mac": "00:00:00:00:00:01", + "name": "Wireless client", + "oui": "Producer", + "rx_bytes-r": 2345000000.0, + "tx_bytes-r": 6789000000.0, + "uptime": 60, +} + + +@pytest.mark.parametrize( + "config_entry_options", + [ + { + CONF_ALLOW_BANDWIDTH_SENSORS: True, + CONF_ALLOW_UPTIME_SENSORS: True, + CONF_TRACK_CLIENTS: False, + CONF_TRACK_DEVICES: False, + } + ], +) +@pytest.mark.parametrize( + ("client_payload", "entity_id", "unique_id_prefix"), + [ + ([WIRED_CLIENT], "sensor.wired_client_rx", "rx-"), + ([WIRED_CLIENT], "sensor.wired_client_tx", "tx-"), + ([WIRED_CLIENT], "sensor.wired_client_uptime", "uptime-"), + ([WIRELESS_CLIENT], "sensor.wireless_client_rx", "rx-"), + ([WIRELESS_CLIENT], "sensor.wireless_client_tx", "tx-"), + ([WIRELESS_CLIENT], "sensor.wireless_client_uptime", "uptime-"), + ], +) +@pytest.mark.usefixtures("config_entry_setup") +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +@pytest.mark.freeze_time("2021-01-01 01:01:00") +async def test_sensor_sources( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, + entity_id: str, + unique_id_prefix: str, +) -> None: + """Test sensor sources and the entity description.""" + ent_reg_entry = entity_registry.async_get(entity_id) + assert ent_reg_entry.unique_id.startswith(unique_id_prefix) + assert ent_reg_entry.unique_id == snapshot + assert ent_reg_entry.entity_category == snapshot + + state = hass.states.get(entity_id) + assert state.attributes.get(ATTR_DEVICE_CLASS) == snapshot + assert state.attributes.get(ATTR_FRIENDLY_NAME) == snapshot + assert state.attributes.get(ATTR_STATE_CLASS) == snapshot + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == snapshot + assert state.state == snapshot From 7bfa1e4729e589c41dd4b9bdb58545b64d36981f Mon Sep 17 00:00:00 2001 From: c0ffeeca7 <38767475+c0ffeeca7@users.noreply.github.com> Date: Fri, 21 Jun 2024 12:59:57 +0200 Subject: [PATCH 0909/1445] System information: apply sentence-style capitalization (#119893) --- homeassistant/components/cloud/strings.json | 22 +++++++++---------- homeassistant/components/hassio/strings.json | 16 +++++++------- .../components/homeassistant/strings.json | 14 ++++++------ 3 files changed, 26 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/cloud/strings.json b/homeassistant/components/cloud/strings.json index 16a82a27c1a..b71ccc0dfa0 100644 --- a/homeassistant/components/cloud/strings.json +++ b/homeassistant/components/cloud/strings.json @@ -7,20 +7,20 @@ }, "system_health": { "info": { - "can_reach_cert_server": "Reach Certificate Server", + "can_reach_cert_server": "Reach certificate server", "can_reach_cloud": "Reach Home Assistant Cloud", - "can_reach_cloud_auth": "Reach Authentication Server", - "certificate_status": "Certificate Status", - "relayer_connected": "Relayer Connected", - "relayer_region": "Relayer Region", - "remote_connected": "Remote Connected", - "remote_enabled": "Remote Enabled", - "remote_server": "Remote Server", - "alexa_enabled": "Alexa Enabled", - "google_enabled": "Google Enabled", + "can_reach_cloud_auth": "Reach authentication server", + "certificate_status": "Certificate status", + "relayer_connected": "Relayer connected", + "relayer_region": "Relayer region", + "remote_connected": "Remote connected", + "remote_enabled": "Remote enabled", + "remote_server": "Remote server", + "alexa_enabled": "Alexa enabled", + "google_enabled": "Google enabled", "logged_in": "Logged In", "instance_id": "Instance ID", - "subscription_expiration": "Subscription Expiration" + "subscription_expiration": "Subscription expiration" } }, "issues": { diff --git a/homeassistant/components/hassio/strings.json b/homeassistant/components/hassio/strings.json index 04e67d625b3..6b81b87e195 100644 --- a/homeassistant/components/hassio/strings.json +++ b/homeassistant/components/hassio/strings.json @@ -1,18 +1,18 @@ { "system_health": { "info": { - "agent_version": "Agent Version", + "agent_version": "Agent version", "board": "Board", - "disk_total": "Disk Total", - "disk_used": "Disk Used", - "docker_version": "Docker Version", + "disk_total": "Disk total", + "disk_used": "Disk used", + "docker_version": "Docker version", "healthy": "Healthy", - "host_os": "Host Operating System", - "installed_addons": "Installed Add-ons", + "host_os": "Host operating system", + "installed_addons": "Installed add-ons", "supervisor_api": "Supervisor API", - "supervisor_version": "Supervisor Version", + "supervisor_version": "Supervisor version", "supported": "Supported", - "update_channel": "Update Channel", + "update_channel": "Update channel", "version_api": "Version API" } }, diff --git a/homeassistant/components/homeassistant/strings.json b/homeassistant/components/homeassistant/strings.json index 09b2f17c947..2acd772b94e 100644 --- a/homeassistant/components/homeassistant/strings.json +++ b/homeassistant/components/homeassistant/strings.json @@ -60,19 +60,19 @@ }, "system_health": { "info": { - "arch": "CPU Architecture", - "config_dir": "Configuration Directory", + "arch": "CPU architecture", + "config_dir": "Configuration directory", "dev": "Development", "docker": "Docker", "hassio": "Supervisor", - "installation_type": "Installation Type", - "os_name": "Operating System Family", - "os_version": "Operating System Version", - "python_version": "Python Version", + "installation_type": "Installation type", + "os_name": "Operating system family", + "os_version": "Operating system version", + "python_version": "Python version", "timezone": "Timezone", "user": "User", "version": "Version", - "virtualenv": "Virtual Environment" + "virtualenv": "Virtual environment" } }, "services": { From 01d4629a2bfea91864ac26d4436bab7490781460 Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Fri, 21 Jun 2024 12:14:32 +0100 Subject: [PATCH 0910/1445] Move coordinator store to entry runtime data for Azure DevOps (#119408) Co-authored-by: Josef Zweck <24647999+zweckj@users.noreply.github.com> --- .../components/azure_devops/__init__.py | 17 +++++++---------- homeassistant/components/azure_devops/sensor.py | 7 +++---- 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/azure_devops/__init__.py b/homeassistant/components/azure_devops/__init__.py index a6e531879b7..9890d47fbb5 100644 --- a/homeassistant/components/azure_devops/__init__.py +++ b/homeassistant/components/azure_devops/__init__.py @@ -8,15 +8,17 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant -from .const import CONF_PAT, CONF_PROJECT, DOMAIN +from .const import CONF_PAT, CONF_PROJECT from .coordinator import AzureDevOpsDataUpdateCoordinator +type AzureDevOpsConfigEntry = ConfigEntry[AzureDevOpsDataUpdateCoordinator] + _LOGGER = logging.getLogger(__name__) PLATFORMS = [Platform.SENSOR] -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: AzureDevOpsConfigEntry) -> bool: """Set up Azure DevOps from a config entry.""" # Create the data update coordinator @@ -26,9 +28,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry=entry, ) - # Store the coordinator in hass data - hass.data.setdefault(DOMAIN, {}) - hass.data[DOMAIN][entry.entry_id] = coordinator + # Store the coordinator in runtime data + entry.runtime_data = coordinator # If a personal access token is set, authorize the client if entry.data.get(CONF_PAT) is not None: @@ -48,8 +49,4 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload Azure DevOps config entry.""" - unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - if unload_ok: - del hass.data[DOMAIN][entry.entry_id] - - return unload_ok + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/azure_devops/sensor.py b/homeassistant/components/azure_devops/sensor.py index 7e1e19cc142..4f0d468cd2d 100644 --- a/homeassistant/components/azure_devops/sensor.py +++ b/homeassistant/components/azure_devops/sensor.py @@ -15,13 +15,12 @@ from homeassistant.components.sensor import ( SensorEntity, SensorEntityDescription, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from homeassistant.util import dt as dt_util -from .const import DOMAIN +from . import AzureDevOpsConfigEntry from .coordinator import AzureDevOpsDataUpdateCoordinator from .entity import AzureDevOpsEntity @@ -128,11 +127,11 @@ def parse_datetime(value: str | None) -> datetime | None: async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: AzureDevOpsConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up Azure DevOps sensor based on a config entry.""" - coordinator = hass.data[DOMAIN][entry.entry_id] + coordinator = entry.runtime_data initial_builds: list[Build] = coordinator.data.builds async_add_entities( From f0452e9ba056a333132c3260124ec01711c648c6 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Fri, 21 Jun 2024 13:15:18 +0200 Subject: [PATCH 0911/1445] Update mypy dev 1.11.0a8 (#120032) --- homeassistant/components/aquacell/__init__.py | 2 +- homeassistant/components/diagnostics/util.py | 3 +-- homeassistant/components/mqtt/client.py | 2 +- homeassistant/components/recorder/const.py | 4 +++- homeassistant/helpers/redact.py | 2 +- homeassistant/helpers/template.py | 2 +- requirements_test.txt | 2 +- 7 files changed, 9 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/aquacell/__init__.py b/homeassistant/components/aquacell/__init__.py index fc67a3f2c53..98cf5d7f0f0 100644 --- a/homeassistant/components/aquacell/__init__.py +++ b/homeassistant/components/aquacell/__init__.py @@ -13,7 +13,7 @@ from .coordinator import AquacellCoordinator PLATFORMS = [Platform.SENSOR] -AquacellConfigEntry = ConfigEntry[AquacellCoordinator] +type AquacellConfigEntry = ConfigEntry[AquacellCoordinator] async def async_setup_entry(hass: HomeAssistant, entry: AquacellConfigEntry) -> bool: diff --git a/homeassistant/components/diagnostics/util.py b/homeassistant/components/diagnostics/util.py index 989433e15b2..0ca85c9a584 100644 --- a/homeassistant/components/diagnostics/util.py +++ b/homeassistant/components/diagnostics/util.py @@ -11,8 +11,7 @@ from .const import REDACTED @overload -def async_redact_data(data: Mapping, to_redact: Iterable[Any]) -> dict: # type: ignore[overload-overlap] - ... +def async_redact_data(data: Mapping, to_redact: Iterable[Any]) -> dict: ... @overload diff --git a/homeassistant/components/mqtt/client.py b/homeassistant/components/mqtt/client.py index 63a90019c20..18ce89beb9b 100644 --- a/homeassistant/components/mqtt/client.py +++ b/homeassistant/components/mqtt/client.py @@ -899,7 +899,7 @@ class MQTT: """Return a string with the exception message.""" # if msg_callback is a partial we return the name of the first argument if isinstance(msg_callback, partial): - call_back_name = getattr(msg_callback.args[0], "__name__") # type: ignore[unreachable] + call_back_name = getattr(msg_callback.args[0], "__name__") else: call_back_name = getattr(msg_callback, "__name__") return ( diff --git a/homeassistant/components/recorder/const.py b/homeassistant/components/recorder/const.py index 97418ee364a..f2af5306ded 100644 --- a/homeassistant/components/recorder/const.py +++ b/homeassistant/components/recorder/const.py @@ -1,5 +1,7 @@ """Recorder constants.""" +from __future__ import annotations + from enum import StrEnum from typing import TYPE_CHECKING @@ -17,7 +19,7 @@ if TYPE_CHECKING: from .core import Recorder # noqa: F401 -DATA_INSTANCE: HassKey["Recorder"] = HassKey("recorder_instance") +DATA_INSTANCE: HassKey[Recorder] = HassKey("recorder_instance") SQLITE_URL_PREFIX = "sqlite://" diff --git a/homeassistant/helpers/redact.py b/homeassistant/helpers/redact.py index 6db0ab4bdd9..cc4f53ae70e 100644 --- a/homeassistant/helpers/redact.py +++ b/homeassistant/helpers/redact.py @@ -29,7 +29,7 @@ def partial_redact( @overload -def async_redact_data[_ValueT]( # type: ignore[overload-overlap] +def async_redact_data[_ValueT]( data: Mapping, to_redact: Iterable[Any] | Mapping[Any, Callable[[_ValueT], _ValueT]] ) -> dict: ... diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index f10913c2478..714a57336bd 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -3045,7 +3045,7 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment): return super().is_safe_attribute(obj, attr, value) @overload - def compile( # type: ignore[overload-overlap] + def compile( self, source: str | jinja2.nodes.Template, name: str | None = None, diff --git a/requirements_test.txt b/requirements_test.txt index 47c3a834e01..9001213f630 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -11,7 +11,7 @@ astroid==3.2.2 coverage==7.5.3 freezegun==1.5.0 mock-open==1.4.0 -mypy-dev==1.11.0a6 +mypy-dev==1.11.0a8 pre-commit==3.7.1 pydantic==1.10.17 pylint==3.2.2 From 4707108146e805103bd710226aee96f48b43da0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20=C4=8Cerm=C3=A1k?= Date: Fri, 21 Jun 2024 13:19:42 +0200 Subject: [PATCH 0912/1445] Samsung AC Wind Mode (#119750) --- .../components/smartthings/climate.py | 20 ++++-- tests/components/smartthings/test_climate.py | 71 +++++++++++++++++++ 2 files changed, 85 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/smartthings/climate.py b/homeassistant/components/smartthings/climate.py index 4c767cbfa30..c3929ababc1 100644 --- a/homeassistant/components/smartthings/climate.py +++ b/homeassistant/components/smartthings/climate.py @@ -56,6 +56,7 @@ OPERATING_STATE_TO_ACTION = { "pending cool": HVACAction.COOLING, "pending heat": HVACAction.HEATING, "vent economizer": HVACAction.FAN, + "wind": HVACAction.FAN, } AC_MODE_TO_STATE = { @@ -67,6 +68,7 @@ AC_MODE_TO_STATE = { "heat": HVACMode.HEAT, "heatClean": HVACMode.HEAT, "fanOnly": HVACMode.FAN_ONLY, + "wind": HVACMode.FAN_ONLY, } STATE_TO_AC_MODE = { HVACMode.HEAT_COOL: "auto", @@ -87,7 +89,7 @@ FAN_OSCILLATION_TO_SWING = { value: key for key, value in SWING_TO_FAN_OSCILLATION.items() } - +WIND = "wind" WINDFREE = "windFree" UNIT_MAP = {"C": UnitOfTemperature.CELSIUS, "F": UnitOfTemperature.FAHRENHEIT} @@ -390,11 +392,17 @@ class SmartThingsAirConditioner(SmartThingsEntity, ClimateEntity): # Turn on the device if it's off before setting mode. if not self._device.status.switch: tasks.append(self._device.switch_on(set_status=True)) - tasks.append( - self._device.set_air_conditioner_mode( - STATE_TO_AC_MODE[hvac_mode], set_status=True - ) - ) + + mode = STATE_TO_AC_MODE[hvac_mode] + # If new hvac_mode is HVAC_MODE_FAN_ONLY and AirConditioner support "wind" mode the AirConditioner new mode has to be "wind" + # The conversion make the mode change working + # The conversion is made only for device that wrongly has capability "wind" instead "fan_only" + if hvac_mode == HVACMode.FAN_ONLY: + supported_modes = self._device.status.supported_ac_modes + if WIND in supported_modes: + mode = WIND + + tasks.append(self._device.set_air_conditioner_mode(mode, set_status=True)) await asyncio.gather(*tasks) # State is set optimistically in the command above, therefore update # the entity state ahead of receiving the confirming push updates diff --git a/tests/components/smartthings/test_climate.py b/tests/components/smartthings/test_climate.py index c97f18e97d9..e4b8cb6d373 100644 --- a/tests/components/smartthings/test_climate.py +++ b/tests/components/smartthings/test_climate.py @@ -202,6 +202,60 @@ def air_conditioner_fixture(device_factory): return device +@pytest.fixture(name="air_conditioner_windfree") +def air_conditioner_windfree_fixture(device_factory): + """Fixture returns a air conditioner.""" + device = device_factory( + "Air Conditioner", + capabilities=[ + Capability.air_conditioner_mode, + Capability.demand_response_load_control, + Capability.air_conditioner_fan_mode, + Capability.switch, + Capability.temperature_measurement, + Capability.thermostat_cooling_setpoint, + Capability.fan_oscillation_mode, + ], + status={ + Attribute.air_conditioner_mode: "auto", + Attribute.supported_ac_modes: [ + "cool", + "dry", + "wind", + "auto", + "heat", + "wind", + ], + Attribute.drlc_status: { + "duration": 0, + "drlcLevel": -1, + "start": "1970-01-01T00:00:00Z", + "override": False, + }, + Attribute.fan_mode: "medium", + Attribute.supported_ac_fan_modes: [ + "auto", + "low", + "medium", + "high", + "turbo", + ], + Attribute.switch: "on", + Attribute.cooling_setpoint: 23, + "supportedAcOptionalMode": ["windFree"], + Attribute.supported_fan_oscillation_modes: [ + "all", + "horizontal", + "vertical", + "fixed", + ], + Attribute.fan_oscillation_mode: "vertical", + }, + ) + device.status.attributes[Attribute.temperature] = Status(24, "C", None) + return device + + async def test_legacy_thermostat_entity_state( hass: HomeAssistant, legacy_thermostat ) -> None: @@ -424,6 +478,23 @@ async def test_ac_set_hvac_mode_off(hass: HomeAssistant, air_conditioner) -> Non assert state.state == HVACMode.OFF +async def test_ac_set_hvac_mode_wind( + hass: HomeAssistant, air_conditioner_windfree +) -> None: + """Test the AC HVAC mode to fan only as wind mode for supported models.""" + await setup_platform(hass, CLIMATE_DOMAIN, devices=[air_conditioner_windfree]) + state = hass.states.get("climate.air_conditioner") + assert state.state != HVACMode.OFF + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, + {ATTR_ENTITY_ID: "climate.air_conditioner", ATTR_HVAC_MODE: HVACMode.FAN_ONLY}, + blocking=True, + ) + state = hass.states.get("climate.air_conditioner") + assert state.state == HVACMode.FAN_ONLY + + async def test_set_temperature_heat_mode(hass: HomeAssistant, thermostat) -> None: """Test the temperature is set successfully when in heat mode.""" thermostat.status.thermostat_mode = "heat" From 955685e1168de709ffff2b0d09bcbbf911fb69e3 Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Fri, 21 Jun 2024 13:22:32 +0200 Subject: [PATCH 0913/1445] Pin codecov-cli to v0.6.0 (#120084) --- .github/workflows/ci.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 232ffb424aa..af29c00af9e 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -1159,6 +1159,7 @@ jobs: fail_ci_if_error: true flags: full-suite token: ${{ secrets.CODECOV_TOKEN }} + version: v0.6.0 pytest-partial: runs-on: ubuntu-22.04 @@ -1293,3 +1294,4 @@ jobs: with: fail_ci_if_error: true token: ${{ secrets.CODECOV_TOKEN }} + version: v0.6.0 From 18767154df5acc9d86fb0f6c078fffe92d1dcf07 Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Fri, 21 Jun 2024 06:24:53 -0500 Subject: [PATCH 0914/1445] Generate and keep conversation id for Wyoming satellite (#118835) --- homeassistant/components/wyoming/satellite.py | 20 ++++ tests/components/wyoming/test_satellite.py | 101 ++++++++++++++++++ 2 files changed, 121 insertions(+) diff --git a/homeassistant/components/wyoming/satellite.py b/homeassistant/components/wyoming/satellite.py index 41ca2887d88..5af0c54abad 100644 --- a/homeassistant/components/wyoming/satellite.py +++ b/homeassistant/components/wyoming/satellite.py @@ -3,7 +3,9 @@ import asyncio import io import logging +import time from typing import Final +from uuid import uuid4 import wave from typing_extensions import AsyncGenerator @@ -38,6 +40,7 @@ _RESTART_SECONDS: Final = 3 _PING_TIMEOUT: Final = 5 _PING_SEND_DELAY: Final = 2 _PIPELINE_FINISH_TIMEOUT: Final = 1 +_CONVERSATION_TIMEOUT_SEC: Final = 5 * 60 # 5 minutes # Wyoming stage -> Assist stage _STAGES: dict[PipelineStage, assist_pipeline.PipelineStage] = { @@ -73,6 +76,9 @@ class WyomingSatellite: self._pipeline_id: str | None = None self._muted_changed_event = asyncio.Event() + self._conversation_id: str | None = None + self._conversation_id_time: float | None = None + self.device.set_is_muted_listener(self._muted_changed) self.device.set_pipeline_listener(self._pipeline_changed) self.device.set_audio_settings_listener(self._audio_settings_changed) @@ -365,6 +371,19 @@ class WyomingSatellite: start_stage, end_stage, ) + + # Reset conversation id, if necessary + if (self._conversation_id_time is None) or ( + (time.monotonic() - self._conversation_id_time) > _CONVERSATION_TIMEOUT_SEC + ): + self._conversation_id = None + + if self._conversation_id is None: + self._conversation_id = str(uuid4()) + + # Update timeout + self._conversation_id_time = time.monotonic() + self._is_pipeline_running = True self._pipeline_ended_event.clear() self.config_entry.async_create_background_task( @@ -393,6 +412,7 @@ class WyomingSatellite: ), device_id=self.device.device_id, wake_word_phrase=wake_word_phrase, + conversation_id=self._conversation_id, ), name="wyoming satellite pipeline", ) diff --git a/tests/components/wyoming/test_satellite.py b/tests/components/wyoming/test_satellite.py index 4d39607158e..1a291153ad0 100644 --- a/tests/components/wyoming/test_satellite.py +++ b/tests/components/wyoming/test_satellite.py @@ -1285,3 +1285,104 @@ async def test_timers(hass: HomeAssistant) -> None: timer_finished = mock_client.timer_finished assert timer_finished is not None assert timer_finished.id == timer_started.id + + +async def test_satellite_conversation_id(hass: HomeAssistant) -> None: + """Test that the same conversation id is used until timeout.""" + assert await async_setup_component(hass, assist_pipeline.DOMAIN, {}) + + events = [ + RunPipeline( + start_stage=PipelineStage.WAKE, + end_stage=PipelineStage.TTS, + restart_on_end=True, + ).event(), + ] + + pipeline_kwargs: dict[str, Any] = {} + pipeline_event_callback: Callable[[assist_pipeline.PipelineEvent], None] | None = ( + None + ) + run_pipeline_called = asyncio.Event() + + async def async_pipeline_from_audio_stream( + hass: HomeAssistant, + context, + event_callback, + stt_metadata, + stt_stream, + **kwargs, + ) -> None: + nonlocal pipeline_kwargs, pipeline_event_callback + pipeline_kwargs = kwargs + pipeline_event_callback = event_callback + + run_pipeline_called.set() + + with ( + patch( + "homeassistant.components.wyoming.data.load_wyoming_info", + return_value=SATELLITE_INFO, + ), + patch( + "homeassistant.components.wyoming.satellite.AsyncTcpClient", + SatelliteAsyncTcpClient(events), + ) as mock_client, + patch( + "homeassistant.components.wyoming.satellite.assist_pipeline.async_pipeline_from_audio_stream", + async_pipeline_from_audio_stream, + ), + patch( + "homeassistant.components.wyoming.satellite.tts.async_get_media_source_audio", + return_value=("wav", get_test_wav()), + ), + patch("homeassistant.components.wyoming.satellite._PING_SEND_DELAY", 0), + ): + entry = await setup_config_entry(hass) + satellite: wyoming.WyomingSatellite = hass.data[wyoming.DOMAIN][ + entry.entry_id + ].satellite + + async with asyncio.timeout(1): + await mock_client.connect_event.wait() + await mock_client.run_satellite_event.wait() + + async with asyncio.timeout(1): + await run_pipeline_called.wait() + + assert pipeline_event_callback is not None + + # A conversation id should have been generated + conversation_id = pipeline_kwargs.get("conversation_id") + assert conversation_id + + # Reset and run again + run_pipeline_called.clear() + pipeline_kwargs.clear() + + pipeline_event_callback( + assist_pipeline.PipelineEvent(assist_pipeline.PipelineEventType.RUN_END) + ) + + async with asyncio.timeout(1): + await run_pipeline_called.wait() + + # Should be the same conversation id + assert pipeline_kwargs.get("conversation_id") == conversation_id + + # Reset and run again, but this time "time out" + satellite._conversation_id_time = None + run_pipeline_called.clear() + pipeline_kwargs.clear() + + pipeline_event_callback( + assist_pipeline.PipelineEvent(assist_pipeline.PipelineEventType.RUN_END) + ) + + async with asyncio.timeout(1): + await run_pipeline_called.wait() + + # Should be a different conversation id + new_conversation_id = pipeline_kwargs.get("conversation_id") + assert new_conversation_id + assert new_conversation_id != conversation_id From f5f2e041261310a380b09331deab600737170f90 Mon Sep 17 00:00:00 2001 From: "Mr. Bubbles" Date: Fri, 21 Jun 2024 13:25:24 +0200 Subject: [PATCH 0915/1445] Add reauth flow to ista EcoTrend integration (#118955) --- .../components/ista_ecotrend/__init__.py | 4 +- .../components/ista_ecotrend/config_flow.py | 60 +++++++++- .../components/ista_ecotrend/coordinator.py | 6 +- .../components/ista_ecotrend/strings.json | 11 +- .../ista_ecotrend/test_config_flow.py | 105 +++++++++++++++++- tests/components/ista_ecotrend/test_init.py | 5 +- 6 files changed, 181 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/ista_ecotrend/__init__.py b/homeassistant/components/ista_ecotrend/__init__.py index 5c1099f9d67..76ef8d13fd4 100644 --- a/homeassistant/components/ista_ecotrend/__init__.py +++ b/homeassistant/components/ista_ecotrend/__init__.py @@ -9,7 +9,7 @@ from pyecotrend_ista import KeycloakError, LoginError, PyEcotrendIsta, ServerErr from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, Platform from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryError, ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from .const import DOMAIN from .coordinator import IstaCoordinator @@ -36,7 +36,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: IstaConfigEntry) -> bool translation_key="connection_exception", ) from e except (LoginError, KeycloakError) as e: - raise ConfigEntryError( + raise ConfigEntryAuthFailed( translation_domain=DOMAIN, translation_key="authentication_exception", translation_placeholders={CONF_EMAIL: entry.data[CONF_EMAIL]}, diff --git a/homeassistant/components/ista_ecotrend/config_flow.py b/homeassistant/components/ista_ecotrend/config_flow.py index 86696950484..b91f10eabdc 100644 --- a/homeassistant/components/ista_ecotrend/config_flow.py +++ b/homeassistant/components/ista_ecotrend/config_flow.py @@ -2,6 +2,7 @@ from __future__ import annotations +from collections.abc import Mapping import logging from typing import TYPE_CHECKING, Any @@ -9,13 +10,14 @@ from pyecotrend_ista import KeycloakError, LoginError, PyEcotrendIsta, ServerErr import voluptuous as vol from homeassistant.config_entries import ConfigFlow, ConfigFlowResult -from homeassistant.const import CONF_EMAIL, CONF_PASSWORD +from homeassistant.const import CONF_EMAIL, CONF_NAME, CONF_PASSWORD from homeassistant.helpers.selector import ( TextSelector, TextSelectorConfig, TextSelectorType, ) +from . import IstaConfigEntry from .const import DOMAIN _LOGGER = logging.getLogger(__name__) @@ -41,6 +43,8 @@ STEP_USER_DATA_SCHEMA = vol.Schema( class IstaConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for ista EcoTrend.""" + reauth_entry: IstaConfigEntry | None = None + async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: @@ -79,3 +83,57 @@ class IstaConfigFlow(ConfigFlow, domain=DOMAIN): ), errors=errors, ) + + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: + """Perform reauth upon an API authentication error.""" + self.reauth_entry = self.hass.config_entries.async_get_entry( + self.context["entry_id"] + ) + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Dialog that informs the user that reauth is required.""" + errors: dict[str, str] = {} + if TYPE_CHECKING: + assert self.reauth_entry + + if user_input is not None: + ista = PyEcotrendIsta( + user_input[CONF_EMAIL], + user_input[CONF_PASSWORD], + _LOGGER, + ) + try: + await self.hass.async_add_executor_job(ista.login) + except (ServerError, InternalServerError): + errors["base"] = "cannot_connect" + except (LoginError, KeycloakError): + errors["base"] = "invalid_auth" + except Exception: + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + return self.async_update_reload_and_abort( + self.reauth_entry, data=user_input + ) + + return self.async_show_form( + step_id="reauth_confirm", + data_schema=self.add_suggested_values_to_schema( + data_schema=STEP_USER_DATA_SCHEMA, + suggested_values={ + CONF_EMAIL: user_input[CONF_EMAIL] + if user_input is not None + else self.reauth_entry.data[CONF_EMAIL] + }, + ), + description_placeholders={ + CONF_NAME: self.reauth_entry.title, + CONF_EMAIL: self.reauth_entry.data[CONF_EMAIL], + }, + errors=errors, + ) diff --git a/homeassistant/components/ista_ecotrend/coordinator.py b/homeassistant/components/ista_ecotrend/coordinator.py index b3be5883136..8d55574f0a1 100644 --- a/homeassistant/components/ista_ecotrend/coordinator.py +++ b/homeassistant/components/ista_ecotrend/coordinator.py @@ -10,7 +10,7 @@ from pyecotrend_ista import KeycloakError, LoginError, PyEcotrendIsta, ServerErr from homeassistant.const import CONF_EMAIL from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryError +from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import DOMAIN @@ -45,7 +45,7 @@ class IstaCoordinator(DataUpdateCoordinator[dict[str, Any]]): "Unable to connect and retrieve data from ista EcoTrend, try again later" ) from e except (LoginError, KeycloakError) as e: - raise ConfigEntryError( + raise ConfigEntryAuthFailed( translation_domain=DOMAIN, translation_key="authentication_exception", translation_placeholders={CONF_EMAIL: self.ista._email}, # noqa: SLF001 @@ -70,7 +70,7 @@ class IstaCoordinator(DataUpdateCoordinator[dict[str, Any]]): "Unable to connect and retrieve data from ista EcoTrend, try again later" ) from e except (LoginError, KeycloakError) as e: - raise ConfigEntryError( + raise ConfigEntryAuthFailed( translation_domain=DOMAIN, translation_key="authentication_exception", translation_placeholders={CONF_EMAIL: self.ista._email}, # noqa: SLF001 diff --git a/homeassistant/components/ista_ecotrend/strings.json b/homeassistant/components/ista_ecotrend/strings.json index af976e89e09..f76cf5286cb 100644 --- a/homeassistant/components/ista_ecotrend/strings.json +++ b/homeassistant/components/ista_ecotrend/strings.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" }, "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", @@ -14,6 +15,14 @@ "email": "[%key:common::config_flow::data::email%]", "password": "[%key:common::config_flow::data::password%]" } + }, + "reauth_confirm": { + "title": "[%key:common::config_flow::title::reauth%]", + "description": "Please reenter the password for: {email}", + "data": { + "email": "[%key:common::config_flow::data::email%]", + "password": "[%key:common::config_flow::data::password%]" + } } } }, diff --git a/tests/components/ista_ecotrend/test_config_flow.py b/tests/components/ista_ecotrend/test_config_flow.py index 3375394f3f6..b702b0331e8 100644 --- a/tests/components/ista_ecotrend/test_config_flow.py +++ b/tests/components/ista_ecotrend/test_config_flow.py @@ -6,7 +6,7 @@ from pyecotrend_ista import LoginError, ServerError import pytest from homeassistant.components.ista_ecotrend.const import DOMAIN -from homeassistant.config_entries import SOURCE_USER +from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER from homeassistant.const import CONF_EMAIL, CONF_PASSWORD from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -87,3 +87,106 @@ async def test_form_invalid_auth( CONF_PASSWORD: "test-password", } assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_reauth( + hass: HomeAssistant, + ista_config_entry: AsyncMock, + mock_ista: MagicMock, +) -> None: + """Test reauth flow.""" + + ista_config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": SOURCE_REAUTH, + "entry_id": ista_config_entry.entry_id, + "unique_id": ista_config_entry.unique_id, + }, + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_EMAIL: "new@example.com", + CONF_PASSWORD: "new-password", + }, + ) + + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reauth_successful" + assert ista_config_entry.data == { + CONF_EMAIL: "new@example.com", + CONF_PASSWORD: "new-password", + } + assert len(hass.config_entries.async_entries()) == 1 + + +@pytest.mark.parametrize( + ("side_effect", "error_text"), + [ + (LoginError(None), "invalid_auth"), + (ServerError, "cannot_connect"), + (IndexError, "unknown"), + ], +) +async def test_reauth_error_and_recover( + hass: HomeAssistant, + ista_config_entry: AsyncMock, + mock_ista: MagicMock, + side_effect: Exception, + error_text: str, +) -> None: + """Test reauth flow.""" + + ista_config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": SOURCE_REAUTH, + "entry_id": ista_config_entry.entry_id, + "unique_id": ista_config_entry.unique_id, + }, + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + + mock_ista.login.side_effect = side_effect + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_EMAIL: "new@example.com", + CONF_PASSWORD: "new-password", + }, + ) + + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"base": error_text} + + mock_ista.login.side_effect = None + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_EMAIL: "new@example.com", + CONF_PASSWORD: "new-password", + }, + ) + + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reauth_successful" + assert ista_config_entry.data == { + CONF_EMAIL: "new@example.com", + CONF_PASSWORD: "new-password", + } + assert len(hass.config_entries.async_entries()) == 1 diff --git a/tests/components/ista_ecotrend/test_init.py b/tests/components/ista_ecotrend/test_init.py index 642afc820dd..a15e4577252 100644 --- a/tests/components/ista_ecotrend/test_init.py +++ b/tests/components/ista_ecotrend/test_init.py @@ -6,7 +6,7 @@ from pyecotrend_ista import KeycloakError, LoginError, ParserError, ServerError import pytest from syrupy.assertion import SnapshotAssertion -from homeassistant.config_entries import ConfigEntryState +from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr @@ -54,7 +54,7 @@ async def test_config_entry_not_ready( ("side_effect"), [LoginError, KeycloakError], ) -async def test_config_entry_error( +async def test_config_entry_auth_failed( hass: HomeAssistant, ista_config_entry: MockConfigEntry, mock_ista: MagicMock, @@ -67,6 +67,7 @@ async def test_config_entry_error( await hass.async_block_till_done() assert ista_config_entry.state is ConfigEntryState.SETUP_ERROR + assert any(ista_config_entry.async_get_active_flows(hass, {SOURCE_REAUTH})) @pytest.mark.usefixtures("mock_ista") From b186b3536fa8ecf25bc6b8870c70f0b300832e52 Mon Sep 17 00:00:00 2001 From: Erwin Douna Date: Fri, 21 Jun 2024 13:26:37 +0200 Subject: [PATCH 0916/1445] Add Home Connect child lock (#118544) --- .../components/home_connect/const.py | 2 + .../components/home_connect/switch.py | 43 +++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/homeassistant/components/home_connect/const.py b/homeassistant/components/home_connect/const.py index 5b0a9e3e9d8..b54637bb524 100644 --- a/homeassistant/components/home_connect/const.py +++ b/homeassistant/components/home_connect/const.py @@ -12,6 +12,8 @@ BSH_POWER_STANDBY = "BSH.Common.EnumType.PowerState.Standby" BSH_ACTIVE_PROGRAM = "BSH.Common.Root.ActiveProgram" BSH_REMOTE_CONTROL_ACTIVATION_STATE = "BSH.Common.Status.RemoteControlActive" BSH_REMOTE_START_ALLOWANCE_STATE = "BSH.Common.Status.RemoteControlStartAllowed" +BSH_CHILD_LOCK_STATE = "BSH.Common.Setting.ChildLock" + BSH_OPERATION_STATE = "BSH.Common.Status.OperationState" BSH_OPERATION_STATE_RUN = "BSH.Common.EnumType.OperationState.Run" diff --git a/homeassistant/components/home_connect/switch.py b/homeassistant/components/home_connect/switch.py index 1239395af2b..8c7ef2eb11a 100644 --- a/homeassistant/components/home_connect/switch.py +++ b/homeassistant/components/home_connect/switch.py @@ -14,6 +14,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ( ATTR_VALUE, BSH_ACTIVE_PROGRAM, + BSH_CHILD_LOCK_STATE, BSH_OPERATION_STATE, BSH_POWER_ON, BSH_POWER_STATE, @@ -39,6 +40,7 @@ async def async_setup_entry( entity_dicts = device_dict.get(CONF_ENTITIES, {}).get("switch", []) entity_list = [HomeConnectProgramSwitch(**d) for d in entity_dicts] entity_list += [HomeConnectPowerSwitch(device_dict[CONF_DEVICE])] + entity_list += [HomeConnectChildLockSwitch(device_dict[CONF_DEVICE])] entities += entity_list return entities @@ -153,3 +155,44 @@ class HomeConnectPowerSwitch(HomeConnectEntity, SwitchEntity): else: self._attr_is_on = None _LOGGER.debug("Updated, new state: %s", self._attr_is_on) + + +class HomeConnectChildLockSwitch(HomeConnectEntity, SwitchEntity): + """Child lock switch class for Home Connect.""" + + def __init__(self, device) -> None: + """Initialize the entity.""" + super().__init__(device, "ChildLock") + + async def async_turn_on(self, **kwargs: Any) -> None: + """Switch child lock on.""" + _LOGGER.debug("Tried to switch child lock on device: %s", self.name) + try: + await self.hass.async_add_executor_job( + self.device.appliance.set_setting, BSH_CHILD_LOCK_STATE, True + ) + except HomeConnectError as err: + _LOGGER.error("Error while trying to turn on child lock on device: %s", err) + self._attr_is_on = False + self.async_entity_update() + + async def async_turn_off(self, **kwargs: Any) -> None: + """Switch child lock off.""" + _LOGGER.debug("Tried to switch off child lock on device: %s", self.name) + try: + await self.hass.async_add_executor_job( + self.device.appliance.set_setting, BSH_CHILD_LOCK_STATE, False + ) + except HomeConnectError as err: + _LOGGER.error( + "Error while trying to turn off child lock on device: %s", err + ) + self._attr_is_on = True + self.async_entity_update() + + async def async_update(self) -> None: + """Update the switch's status.""" + self._attr_is_on = False + if self.device.appliance.status.get(BSH_CHILD_LOCK_STATE, {}).get(ATTR_VALUE): + self._attr_is_on = True + _LOGGER.debug("Updated child lock, new state: %s", self._attr_is_on) From c9ddabaead55b8cd3f3c1186dbb96a131fcc5469 Mon Sep 17 00:00:00 2001 From: neturmel Date: Fri, 21 Jun 2024 13:28:20 +0200 Subject: [PATCH 0917/1445] Support tuya diivoo dual zone irrigationkit (ggq) (#115090) Co-authored-by: Franck Nijhof --- homeassistant/components/tuya/sensor.py | 3 +++ homeassistant/components/tuya/switch.py | 12 ++++++++++++ 2 files changed, 15 insertions(+) diff --git a/homeassistant/components/tuya/sensor.py b/homeassistant/components/tuya/sensor.py index b974ccd5eb0..2b2baea5251 100644 --- a/homeassistant/components/tuya/sensor.py +++ b/homeassistant/components/tuya/sensor.py @@ -547,6 +547,9 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { ), *BATTERY_SENSORS, ), + # Irrigator + # https://developer.tuya.com/en/docs/iot/categoryggq?id=Kaiuz1qib7z0k + "ggq": BATTERY_SENSORS, # Water Detector # https://developer.tuya.com/en/docs/iot/categorysj?id=Kaiuz3iub2sli "sj": BATTERY_SENSORS, diff --git a/homeassistant/components/tuya/switch.py b/homeassistant/components/tuya/switch.py index 0f893aecb42..2d5092d42b2 100644 --- a/homeassistant/components/tuya/switch.py +++ b/homeassistant/components/tuya/switch.py @@ -407,6 +407,18 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { translation_key="switch", ), ), + # Irrigator + # https://developer.tuya.com/en/docs/iot/categoryggq?id=Kaiuz1qib7z0k + "ggq": ( + SwitchEntityDescription( + key=DPCode.SWITCH_1, + translation_key="switch_1", + ), + SwitchEntityDescription( + key=DPCode.SWITCH_2, + translation_key="switch_2", + ), + ), # Siren Alarm # https://developer.tuya.com/en/docs/iot/categorysgbj?id=Kaiuz37tlpbnu "sgbj": ( From 7d86921d09d12c61625bc70d77417ab1685aca60 Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Fri, 21 Jun 2024 12:30:21 +0100 Subject: [PATCH 0918/1445] Reduce line length for unique id (#120086) --- homeassistant/components/azure_devops/sensor.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/azure_devops/sensor.py b/homeassistant/components/azure_devops/sensor.py index 4f0d468cd2d..029d3d875dc 100644 --- a/homeassistant/components/azure_devops/sensor.py +++ b/homeassistant/components/azure_devops/sensor.py @@ -161,7 +161,12 @@ class AzureDevOpsBuildSensor(AzureDevOpsEntity, SensorEntity): super().__init__(coordinator) self.entity_description = description self.item_key = item_key - self._attr_unique_id = f"{self.coordinator.data.organization}_{self.build.project.id}_{self.build.definition.build_id}_{description.key}" + self._attr_unique_id = ( + f"{self.coordinator.data.organization}_" + f"{self.build.project.id}_" + f"{self.build.definition.build_id}_" + f"{description.key}" + ) self._attr_translation_placeholders = { "definition_name": self.build.definition.name } From 988148d38594b5190f922154ab8040bc7bda661b Mon Sep 17 00:00:00 2001 From: Tobias Schmitt Date: Fri, 21 Jun 2024 13:33:53 +0200 Subject: [PATCH 0919/1445] Add ZHA cod.m coordinator discovery (#115471) --- homeassistant/components/zha/manifest.json | 4 ++++ homeassistant/generated/zeroconf.py | 6 ++++++ 2 files changed, 10 insertions(+) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index aed0abd3404..f517742f16f 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -136,6 +136,10 @@ { "type": "_xzg._tcp.local.", "name": "xzg*" + }, + { + "type": "_czc._tcp.local.", + "name": "czc*" } ] } diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index 26078394331..8efe49b7892 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -404,6 +404,12 @@ ZEROCONF = { "domain": "apple_tv", }, ], + "_czc._tcp.local.": [ + { + "domain": "zha", + "name": "czc*", + }, + ], "_daap._tcp.local.": [ { "domain": "forked_daapd", From a0f81cb401c91c1014179efb2dbe8ba3df66e2f0 Mon Sep 17 00:00:00 2001 From: dontinelli <73341522+dontinelli@users.noreply.github.com> Date: Fri, 21 Jun 2024 13:35:22 +0200 Subject: [PATCH 0920/1445] Add solarlog reconfigure flow (#119913) --- .../components/solarlog/config_flow.py | 30 +++++++++++++++- .../components/solarlog/strings.json | 9 ++++- tests/components/solarlog/test_config_flow.py | 34 +++++++++++++++++++ 3 files changed, 71 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/solarlog/config_flow.py b/homeassistant/components/solarlog/config_flow.py index deda2d81779..9f6397bb62e 100644 --- a/homeassistant/components/solarlog/config_flow.py +++ b/homeassistant/components/solarlog/config_flow.py @@ -1,7 +1,7 @@ """Config flow for solarlog integration.""" import logging -from typing import Any +from typing import TYPE_CHECKING, Any from urllib.parse import ParseResult, urlparse from solarlog_cli.solarlog_connector import SolarLogConnector @@ -117,3 +117,31 @@ class SolarLogConfigFlow(ConfigFlow, domain=DOMAIN): return self.async_abort(reason="already_configured") return await self.async_step_user(user_input) + + async def async_step_reconfigure( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle a reconfiguration flow initialized by the user.""" + + entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) + + if TYPE_CHECKING: + assert entry is not None + + if user_input is not None: + return self.async_update_reload_and_abort( + entry, + reason="reconfigure_successful", + data={**entry.data, **user_input}, + ) + + return self.async_show_form( + step_id="reconfigure", + data_schema=vol.Schema( + { + vol.Required( + "extended_data", default=entry.data["extended_data"] + ): bool, + } + ), + ) diff --git a/homeassistant/components/solarlog/strings.json b/homeassistant/components/solarlog/strings.json index 255f35114c1..caa14ac01a6 100644 --- a/homeassistant/components/solarlog/strings.json +++ b/homeassistant/components/solarlog/strings.json @@ -11,6 +11,12 @@ "data_description": { "host": "The hostname or IP address of your Solar-Log device." } + }, + "reconfigure": { + "title": "Configure SolarLog", + "data": { + "extended_data": "[%key:component::solarlog::config::step::user::data::extended_data%]" + } } }, "error": { @@ -19,7 +25,8 @@ "unknown": "[%key:common::config_flow::error::unknown%]" }, "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]" } }, "entity": { diff --git a/tests/components/solarlog/test_config_flow.py b/tests/components/solarlog/test_config_flow.py index cb1092a73e3..34da13cdf8f 100644 --- a/tests/components/solarlog/test_config_flow.py +++ b/tests/components/solarlog/test_config_flow.py @@ -188,3 +188,37 @@ async def test_abort_if_already_setup(hass: HomeAssistant, test_connect) -> None assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "solarlog_test_1_2_3" assert result["data"][CONF_HOST] == "http://2.2.2.2" + + +async def test_reconfigure_flow( + hass: HomeAssistant, mock_setup_entry: AsyncMock +) -> None: + """Test config flow options.""" + entry = MockConfigEntry( + domain=DOMAIN, + title="solarlog_test_1_2_3", + data={ + CONF_HOST: HOST, + "extended_data": False, + }, + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_RECONFIGURE, + "entry_id": entry.entry_id, + }, + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reconfigure" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"extended_data": True} + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reconfigure_successful" + assert len(mock_setup_entry.mock_calls) == 1 From 225e90c99e11ee277952d754b472cc4646901a0b Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Fri, 21 Jun 2024 13:38:51 +0200 Subject: [PATCH 0921/1445] Add playback of autotrack lens to Reolink (#119829) Co-authored-by: Robert Resch Co-authored-by: Franck Nijhof --- .../components/reolink/media_source.py | 64 +++++++++++++++---- tests/components/reolink/test_media_source.py | 21 ++++++ 2 files changed, 72 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/reolink/media_source.py b/homeassistant/components/reolink/media_source.py index c941f5ed055..5d3c16b00fd 100644 --- a/homeassistant/components/reolink/media_source.py +++ b/homeassistant/components/reolink/media_source.py @@ -34,7 +34,15 @@ async def async_get_media_source(hass: HomeAssistant) -> ReolinkVODMediaSource: def res_name(stream: str) -> str: """Return the user friendly name for a stream.""" - return "High res." if stream == "main" else "Low res." + match stream: + case "main": + return "High res." + case "autotrack_sub": + return "Autotrack low res." + case "autotrack_main": + return "Autotrack high res." + case _: + return "Low res." class ReolinkVODMediaSource(MediaSource): @@ -210,9 +218,6 @@ class ReolinkVODMediaSource(MediaSource): "playback only possible using sub stream", host.api.camera_name(channel), ) - return await self._async_generate_camera_days( - config_entry_id, channel, "sub" - ) children = [ BrowseMediaSource( @@ -224,16 +229,49 @@ class ReolinkVODMediaSource(MediaSource): can_play=False, can_expand=True, ), - BrowseMediaSource( - domain=DOMAIN, - identifier=f"RES|{config_entry_id}|{channel}|main", - media_class=MediaClass.CHANNEL, - media_content_type=MediaType.PLAYLIST, - title="High resolution", - can_play=False, - can_expand=True, - ), ] + if main_enc != "h265": + children.append( + BrowseMediaSource( + domain=DOMAIN, + identifier=f"RES|{config_entry_id}|{channel}|main", + media_class=MediaClass.CHANNEL, + media_content_type=MediaType.PLAYLIST, + title="High resolution", + can_play=False, + can_expand=True, + ), + ) + + if host.api.supported(channel, "autotrack_stream"): + children.append( + BrowseMediaSource( + domain=DOMAIN, + identifier=f"RES|{config_entry_id}|{channel}|autotrack_sub", + media_class=MediaClass.CHANNEL, + media_content_type=MediaType.PLAYLIST, + title="Autotrack low resolution", + can_play=False, + can_expand=True, + ), + ) + if main_enc != "h265": + children.append( + BrowseMediaSource( + domain=DOMAIN, + identifier=f"RES|{config_entry_id}|{channel}|autotrack_main", + media_class=MediaClass.CHANNEL, + media_content_type=MediaType.PLAYLIST, + title="Autotrack high resolution", + can_play=False, + can_expand=True, + ), + ) + + if len(children) == 1: + return await self._async_generate_camera_days( + config_entry_id, channel, "sub" + ) return BrowseMediaSource( domain=DOMAIN, diff --git a/tests/components/reolink/test_media_source.py b/tests/components/reolink/test_media_source.py index 3e3cdd02b46..0d86106e8e5 100644 --- a/tests/components/reolink/test_media_source.py +++ b/tests/components/reolink/test_media_source.py @@ -156,11 +156,15 @@ async def test_browsing( browse_resolution_id = f"RESs|{entry_id}|{TEST_CHANNEL}" browse_res_sub_id = f"RES|{entry_id}|{TEST_CHANNEL}|sub" browse_res_main_id = f"RES|{entry_id}|{TEST_CHANNEL}|main" + browse_res_AT_sub_id = f"RES|{entry_id}|{TEST_CHANNEL}|autotrack_sub" + browse_res_AT_main_id = f"RES|{entry_id}|{TEST_CHANNEL}|autotrack_main" assert browse.domain == DOMAIN assert browse.title == TEST_NVR_NAME assert browse.identifier == browse_resolution_id assert browse.children[0].identifier == browse_res_sub_id assert browse.children[1].identifier == browse_res_main_id + assert browse.children[2].identifier == browse_res_AT_sub_id + assert browse.children[3].identifier == browse_res_AT_main_id # browse camera recording days mock_status = MagicMock() @@ -169,6 +173,22 @@ async def test_browsing( mock_status.days = (TEST_DAY, TEST_DAY2) reolink_connect.request_vod_files.return_value = ([mock_status], []) + browse = await async_browse_media(hass, f"{URI_SCHEME}{DOMAIN}/{browse_res_sub_id}") + assert browse.domain == DOMAIN + assert browse.title == f"{TEST_NVR_NAME} Low res." + + browse = await async_browse_media( + hass, f"{URI_SCHEME}{DOMAIN}/{browse_res_AT_sub_id}" + ) + assert browse.domain == DOMAIN + assert browse.title == f"{TEST_NVR_NAME} Autotrack low res." + + browse = await async_browse_media( + hass, f"{URI_SCHEME}{DOMAIN}/{browse_res_AT_main_id}" + ) + assert browse.domain == DOMAIN + assert browse.title == f"{TEST_NVR_NAME} Autotrack high res." + browse = await async_browse_media( hass, f"{URI_SCHEME}{DOMAIN}/{browse_res_main_id}" ) @@ -225,6 +245,7 @@ async def test_browsing_unsupported_encoding( reolink_connect.request_vod_files.return_value = ([mock_status], []) reolink_connect.time.return_value = None reolink_connect.get_encoding.return_value = "h265" + reolink_connect.supported.return_value = False browse = await async_browse_media(hass, f"{URI_SCHEME}{DOMAIN}/{browse_root_id}") From 901317ec3994a5a8095c221ac8a61e23ce8b988a Mon Sep 17 00:00:00 2001 From: Brent Petit Date: Fri, 21 Jun 2024 06:40:26 -0500 Subject: [PATCH 0922/1445] Remove rstrip from ecobee binary_sensor __init__ (#118062) --- homeassistant/components/ecobee/binary_sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/ecobee/binary_sensor.py b/homeassistant/components/ecobee/binary_sensor.py index 18e09178581..4286f2cf757 100644 --- a/homeassistant/components/ecobee/binary_sensor.py +++ b/homeassistant/components/ecobee/binary_sensor.py @@ -42,7 +42,7 @@ class EcobeeBinarySensor(BinarySensorEntity): def __init__(self, data, sensor_name, sensor_index): """Initialize the Ecobee sensor.""" self.data = data - self.sensor_name = sensor_name.rstrip() + self.sensor_name = sensor_name self.index = sensor_index @property From 905c1c5700e569732bb1ddf7c597a586a5ef912d Mon Sep 17 00:00:00 2001 From: "Mr. Bubbles" Date: Fri, 21 Jun 2024 13:51:46 +0200 Subject: [PATCH 0923/1445] Fix removed exception InternalServerError in ista EcoTrend integration (#120089) --- homeassistant/components/ista_ecotrend/config_flow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/ista_ecotrend/config_flow.py b/homeassistant/components/ista_ecotrend/config_flow.py index b91f10eabdc..15222995a37 100644 --- a/homeassistant/components/ista_ecotrend/config_flow.py +++ b/homeassistant/components/ista_ecotrend/config_flow.py @@ -109,7 +109,7 @@ class IstaConfigFlow(ConfigFlow, domain=DOMAIN): ) try: await self.hass.async_add_executor_job(ista.login) - except (ServerError, InternalServerError): + except ServerError: errors["base"] = "cannot_connect" except (LoginError, KeycloakError): errors["base"] = "invalid_auth" From 5c2f78a4b966685a5385e3db2bb59d7441612978 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Fri, 21 Jun 2024 14:35:45 +0200 Subject: [PATCH 0924/1445] Fix solarlog client close (#120092) --- homeassistant/components/solarlog/config_flow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/solarlog/config_flow.py b/homeassistant/components/solarlog/config_flow.py index 9f6397bb62e..eb0971e0d92 100644 --- a/homeassistant/components/solarlog/config_flow.py +++ b/homeassistant/components/solarlog/config_flow.py @@ -62,7 +62,7 @@ class SolarLogConfigFlow(ConfigFlow, domain=DOMAIN): self._errors = {CONF_HOST: "unknown"} return False finally: - solarlog.client.close() + await solarlog.client.close() return True From e2a34d209fe489ba84523852058ff7d9566f1199 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 21 Jun 2024 14:44:28 +0200 Subject: [PATCH 0925/1445] Improve type hints in Config entry oauth2 tests (#120090) --- .../helpers/test_config_entry_oauth2_flow.py | 90 +++++++++++++------ 1 file changed, 63 insertions(+), 27 deletions(-) diff --git a/tests/helpers/test_config_entry_oauth2_flow.py b/tests/helpers/test_config_entry_oauth2_flow.py index 18e1712f764..132a0b41707 100644 --- a/tests/helpers/test_config_entry_oauth2_flow.py +++ b/tests/helpers/test_config_entry_oauth2_flow.py @@ -8,6 +8,7 @@ from unittest.mock import patch import aiohttp import pytest +from typing_extensions import Generator from homeassistant import config_entries, data_entry_flow, setup from homeassistant.core import HomeAssistant @@ -29,7 +30,9 @@ TOKEN_URL = "https://example.como/auth/token" @pytest.fixture -async def local_impl(hass): +async def local_impl( + hass: HomeAssistant, +) -> config_entry_oauth2_flow.LocalOAuth2Implementation: """Local implementation.""" assert await setup.async_setup_component(hass, "auth", {}) return config_entry_oauth2_flow.LocalOAuth2Implementation( @@ -38,7 +41,9 @@ async def local_impl(hass): @pytest.fixture -def flow_handler(hass): +def flow_handler( + hass: HomeAssistant, +) -> Generator[type[config_entry_oauth2_flow.AbstractOAuth2FlowHandler]]: """Return a registered config flow.""" mock_platform(hass, f"{TEST_DOMAIN}.config_flow") @@ -111,7 +116,10 @@ def test_inherit_enforces_domain_set() -> None: TestFlowHandler() -async def test_abort_if_no_implementation(hass: HomeAssistant, flow_handler) -> None: +async def test_abort_if_no_implementation( + hass: HomeAssistant, + flow_handler: type[config_entry_oauth2_flow.AbstractOAuth2FlowHandler], +) -> None: """Check flow abort when no implementations.""" flow = flow_handler() flow.hass = hass @@ -121,7 +129,8 @@ async def test_abort_if_no_implementation(hass: HomeAssistant, flow_handler) -> async def test_missing_credentials_for_domain( - hass: HomeAssistant, flow_handler + hass: HomeAssistant, + flow_handler: type[config_entry_oauth2_flow.AbstractOAuth2FlowHandler], ) -> None: """Check flow abort for integration supporting application credentials.""" flow = flow_handler() @@ -135,7 +144,9 @@ async def test_missing_credentials_for_domain( @pytest.mark.usefixtures("current_request_with_host") async def test_abort_if_authorization_timeout( - hass: HomeAssistant, flow_handler, local_impl + hass: HomeAssistant, + flow_handler: type[config_entry_oauth2_flow.AbstractOAuth2FlowHandler], + local_impl: config_entry_oauth2_flow.LocalOAuth2Implementation, ) -> None: """Check timeout generating authorization url.""" flow_handler.async_register_implementation(hass, local_impl) @@ -155,7 +166,9 @@ async def test_abort_if_authorization_timeout( @pytest.mark.usefixtures("current_request_with_host") async def test_abort_if_no_url_available( - hass: HomeAssistant, flow_handler, local_impl + hass: HomeAssistant, + flow_handler: type[config_entry_oauth2_flow.AbstractOAuth2FlowHandler], + local_impl: config_entry_oauth2_flow.LocalOAuth2Implementation, ) -> None: """Check no_url_available generating authorization url.""" flow_handler.async_register_implementation(hass, local_impl) @@ -176,8 +189,8 @@ async def test_abort_if_no_url_available( @pytest.mark.usefixtures("current_request_with_host") async def test_abort_if_oauth_error( hass: HomeAssistant, - flow_handler, - local_impl, + flow_handler: type[config_entry_oauth2_flow.AbstractOAuth2FlowHandler], + local_impl: config_entry_oauth2_flow.LocalOAuth2Implementation, hass_client_no_auth: ClientSessionGenerator, aioclient_mock: AiohttpClientMocker, expires_in_dict: dict[str, str], @@ -239,8 +252,8 @@ async def test_abort_if_oauth_error( @pytest.mark.usefixtures("current_request_with_host") async def test_abort_if_oauth_rejected( hass: HomeAssistant, - flow_handler, - local_impl, + flow_handler: type[config_entry_oauth2_flow.AbstractOAuth2FlowHandler], + local_impl: config_entry_oauth2_flow.LocalOAuth2Implementation, hass_client_no_auth: ClientSessionGenerator, ) -> None: """Check bad oauth token.""" @@ -293,8 +306,8 @@ async def test_abort_if_oauth_rejected( @pytest.mark.usefixtures("current_request_with_host") async def test_abort_on_oauth_timeout_error( hass: HomeAssistant, - flow_handler, - local_impl, + flow_handler: type[config_entry_oauth2_flow.AbstractOAuth2FlowHandler], + local_impl: config_entry_oauth2_flow.LocalOAuth2Implementation, hass_client_no_auth: ClientSessionGenerator, aioclient_mock: AiohttpClientMocker, ) -> None: @@ -346,7 +359,11 @@ async def test_abort_on_oauth_timeout_error( assert result["reason"] == "oauth_timeout" -async def test_step_discovery(hass: HomeAssistant, flow_handler, local_impl) -> None: +async def test_step_discovery( + hass: HomeAssistant, + flow_handler: type[config_entry_oauth2_flow.AbstractOAuth2FlowHandler], + local_impl: config_entry_oauth2_flow.LocalOAuth2Implementation, +) -> None: """Check flow triggers from discovery.""" flow_handler.async_register_implementation(hass, local_impl) config_entry_oauth2_flow.async_register_implementation( @@ -364,7 +381,9 @@ async def test_step_discovery(hass: HomeAssistant, flow_handler, local_impl) -> async def test_abort_discovered_multiple( - hass: HomeAssistant, flow_handler, local_impl + hass: HomeAssistant, + flow_handler: type[config_entry_oauth2_flow.AbstractOAuth2FlowHandler], + local_impl: config_entry_oauth2_flow.LocalOAuth2Implementation, ) -> None: """Test if aborts when discovered multiple times.""" flow_handler.async_register_implementation(hass, local_impl) @@ -427,8 +446,8 @@ async def test_abort_discovered_multiple( @pytest.mark.usefixtures("current_request_with_host") async def test_abort_if_oauth_token_error( hass: HomeAssistant, - flow_handler, - local_impl, + flow_handler: type[config_entry_oauth2_flow.AbstractOAuth2FlowHandler], + local_impl: config_entry_oauth2_flow.LocalOAuth2Implementation, hass_client_no_auth: ClientSessionGenerator, aioclient_mock: AiohttpClientMocker, status_code: HTTPStatus, @@ -491,8 +510,8 @@ async def test_abort_if_oauth_token_error( @pytest.mark.usefixtures("current_request_with_host") async def test_abort_if_oauth_token_closing_error( hass: HomeAssistant, - flow_handler, - local_impl, + flow_handler: type[config_entry_oauth2_flow.AbstractOAuth2FlowHandler], + local_impl: config_entry_oauth2_flow.LocalOAuth2Implementation, hass_client_no_auth: ClientSessionGenerator, aioclient_mock: AiohttpClientMocker, caplog: pytest.LogCaptureFixture, @@ -550,7 +569,9 @@ async def test_abort_if_oauth_token_closing_error( async def test_abort_discovered_existing_entries( - hass: HomeAssistant, flow_handler, local_impl + hass: HomeAssistant, + flow_handler: type[config_entry_oauth2_flow.AbstractOAuth2FlowHandler], + local_impl: config_entry_oauth2_flow.LocalOAuth2Implementation, ) -> None: """Test if abort discovery when entries exists.""" flow_handler.async_register_implementation(hass, local_impl) @@ -577,8 +598,8 @@ async def test_abort_discovered_existing_entries( @pytest.mark.usefixtures("current_request_with_host") async def test_full_flow( hass: HomeAssistant, - flow_handler, - local_impl, + flow_handler: type[config_entry_oauth2_flow.AbstractOAuth2FlowHandler], + local_impl: config_entry_oauth2_flow.LocalOAuth2Implementation, hass_client_no_auth: ClientSessionGenerator, aioclient_mock: AiohttpClientMocker, ) -> None: @@ -653,7 +674,9 @@ async def test_full_flow( async def test_local_refresh_token( - hass: HomeAssistant, local_impl, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, + local_impl: config_entry_oauth2_flow.LocalOAuth2Implementation, + aioclient_mock: AiohttpClientMocker, ) -> None: """Test we can refresh token.""" aioclient_mock.post( @@ -687,7 +710,10 @@ async def test_local_refresh_token( async def test_oauth_session( - hass: HomeAssistant, flow_handler, local_impl, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, + flow_handler: type[config_entry_oauth2_flow.AbstractOAuth2FlowHandler], + local_impl: config_entry_oauth2_flow.LocalOAuth2Implementation, + aioclient_mock: AiohttpClientMocker, ) -> None: """Test the OAuth2 session helper.""" flow_handler.async_register_implementation(hass, local_impl) @@ -734,7 +760,10 @@ async def test_oauth_session( async def test_oauth_session_with_clock_slightly_out_of_sync( - hass: HomeAssistant, flow_handler, local_impl, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, + flow_handler: type[config_entry_oauth2_flow.AbstractOAuth2FlowHandler], + local_impl: config_entry_oauth2_flow.LocalOAuth2Implementation, + aioclient_mock: AiohttpClientMocker, ) -> None: """Test the OAuth2 session helper when the remote clock is slightly out of sync.""" flow_handler.async_register_implementation(hass, local_impl) @@ -781,7 +810,10 @@ async def test_oauth_session_with_clock_slightly_out_of_sync( async def test_oauth_session_no_token_refresh_needed( - hass: HomeAssistant, flow_handler, local_impl, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, + flow_handler: type[config_entry_oauth2_flow.AbstractOAuth2FlowHandler], + local_impl: config_entry_oauth2_flow.LocalOAuth2Implementation, + aioclient_mock: AiohttpClientMocker, ) -> None: """Test the OAuth2 session helper when no refresh is needed.""" flow_handler.async_register_implementation(hass, local_impl) @@ -879,7 +911,10 @@ async def test_implementation_provider(hass: HomeAssistant, local_impl) -> None: async def test_oauth_session_refresh_failure( - hass: HomeAssistant, flow_handler, local_impl, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, + flow_handler: type[config_entry_oauth2_flow.AbstractOAuth2FlowHandler], + local_impl: config_entry_oauth2_flow.LocalOAuth2Implementation, + aioclient_mock: AiohttpClientMocker, ) -> None: """Test the OAuth2 session helper when no refresh is needed.""" flow_handler.async_register_implementation(hass, local_impl) @@ -908,7 +943,8 @@ async def test_oauth_session_refresh_failure( async def test_oauth2_without_secret_init( - local_impl, hass_client_no_auth: ClientSessionGenerator + local_impl: config_entry_oauth2_flow.LocalOAuth2Implementation, + hass_client_no_auth: ClientSessionGenerator, ) -> None: """Check authorize callback without secret initalizated.""" client = await hass_client_no_auth() From a8ba22f6bb5d14bcfa0c6edf386eb7968736f6d4 Mon Sep 17 00:00:00 2001 From: dougiteixeira <31328123+dougiteixeira@users.noreply.github.com> Date: Fri, 21 Jun 2024 09:46:39 -0300 Subject: [PATCH 0926/1445] Add device linking and stale device link clean up helpers (#119761) --- .../components/utility_meter/__init__.py | 35 +-- .../components/utility_meter/select.py | 28 +-- .../components/utility_meter/sensor.py | 33 +-- homeassistant/helpers/device.py | 75 +++++++ tests/components/utility_meter/test_select.py | 56 +++++ tests/helpers/test_device.py | 211 ++++++++++++++++++ 6 files changed, 358 insertions(+), 80 deletions(-) create mode 100644 homeassistant/helpers/device.py create mode 100644 tests/components/utility_meter/test_select.py create mode 100644 tests/helpers/test_device.py diff --git a/homeassistant/components/utility_meter/__init__.py b/homeassistant/components/utility_meter/__init__.py index c579a684406..c6a8635f831 100644 --- a/homeassistant/components/utility_meter/__init__.py +++ b/homeassistant/components/utility_meter/__init__.py @@ -11,12 +11,11 @@ from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_ENTITY_ID, CONF_NAME, CONF_UNIQUE_ID, Platform from homeassistant.core import HomeAssistant, split_entity_id -from homeassistant.helpers import ( - device_registry as dr, - discovery, - entity_registry as er, -) +from homeassistant.helpers import discovery, entity_registry as er import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.device import ( + async_remove_stale_devices_links_keep_entity_device, +) from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.typing import ConfigType @@ -192,7 +191,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Utility Meter from a config entry.""" - await async_remove_stale_device_links( + async_remove_stale_devices_links_keep_entity_device( hass, entry.entry_id, entry.options[CONF_SOURCE_SENSOR] ) @@ -266,27 +265,3 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> _LOGGER.info("Migration to version %s successful", config_entry.version) return True - - -async def async_remove_stale_device_links( - hass: HomeAssistant, entry_id: str, entity_id: str -) -> None: - """Remove device link for entry, the source device may have changed.""" - - device_registry = dr.async_get(hass) - entity_registry = er.async_get(hass) - - # Resolve source entity device - current_device_id = None - if ((source_entity := entity_registry.async_get(entity_id)) is not None) and ( - source_entity.device_id is not None - ): - current_device_id = source_entity.device_id - - devices_in_entry = device_registry.devices.get_devices_for_config_entry_id(entry_id) - - # Removes all devices from the config entry that are not the same as the current device - for device in devices_in_entry: - if device.id == current_device_id: - continue - device_registry.async_update_device(device.id, remove_config_entry_id=entry_id) diff --git a/homeassistant/components/utility_meter/select.py b/homeassistant/components/utility_meter/select.py index 461fee3ba9f..d5b1206d046 100644 --- a/homeassistant/components/utility_meter/select.py +++ b/homeassistant/components/utility_meter/select.py @@ -8,7 +8,7 @@ from homeassistant.components.select import SelectEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_UNIQUE_ID from homeassistant.core import HomeAssistant -from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers.device import async_device_info_to_link_from_entity from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity @@ -30,28 +30,10 @@ async def async_setup_entry( unique_id = config_entry.entry_id - registry = er.async_get(hass) - source_entity = registry.async_get(config_entry.options[CONF_SOURCE_SENSOR]) - dev_reg = dr.async_get(hass) - # Resolve source entity device - if ( - (source_entity is not None) - and (source_entity.device_id is not None) - and ( - ( - device := dev_reg.async_get( - device_id=source_entity.device_id, - ) - ) - is not None - ) - ): - device_info = DeviceInfo( - identifiers=device.identifiers, - connections=device.connections, - ) - else: - device_info = None + device_info = async_device_info_to_link_from_entity( + hass, + config_entry.options[CONF_SOURCE_SENSOR], + ) tariff_select = TariffSelect( name, diff --git a/homeassistant/components/utility_meter/sensor.py b/homeassistant/components/utility_meter/sensor.py index 4a68248f067..6b8c07c7ef7 100644 --- a/homeassistant/components/utility_meter/sensor.py +++ b/homeassistant/components/utility_meter/sensor.py @@ -37,12 +37,8 @@ from homeassistant.core import ( State, callback, ) -from homeassistant.helpers import ( - device_registry as dr, - entity_platform, - entity_registry as er, -) -from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers import entity_platform, entity_registry as er +from homeassistant.helpers.device import async_device_info_to_link_from_entity from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import ( @@ -130,27 +126,10 @@ async def async_setup_entry( registry, config_entry.options[CONF_SOURCE_SENSOR] ) - source_entity = registry.async_get(source_entity_id) - dev_reg = dr.async_get(hass) - # Resolve source entity device - if ( - (source_entity is not None) - and (source_entity.device_id is not None) - and ( - ( - device := dev_reg.async_get( - device_id=source_entity.device_id, - ) - ) - is not None - ) - ): - device_info = DeviceInfo( - identifiers=device.identifiers, - connections=device.connections, - ) - else: - device_info = None + device_info = async_device_info_to_link_from_entity( + hass, + source_entity_id, + ) cron_pattern = None delta_values = config_entry.options[CONF_METER_DELTA_VALUES] diff --git a/homeassistant/helpers/device.py b/homeassistant/helpers/device.py new file mode 100644 index 00000000000..b9df721ec6c --- /dev/null +++ b/homeassistant/helpers/device.py @@ -0,0 +1,75 @@ +"""Provides useful helpers for handling devices.""" + +from homeassistant.core import HomeAssistant, callback + +from . import device_registry as dr, entity_registry as er + + +@callback +def async_entity_id_to_device_id( + hass: HomeAssistant, + entity_id_or_uuid: str, +) -> str | None: + """Resolve the device id to the entity id or entity uuid.""" + + ent_reg = er.async_get(hass) + + entity_id = er.async_validate_entity_id(ent_reg, entity_id_or_uuid) + if (entity := ent_reg.async_get(entity_id)) is None: + return None + + return entity.device_id + + +@callback +def async_device_info_to_link_from_entity( + hass: HomeAssistant, + entity_id_or_uuid: str, +) -> dr.DeviceInfo | None: + """DeviceInfo with information to link a device to a configuration entry in the link category from a entity id or entity uuid.""" + + dev_reg = dr.async_get(hass) + + if (device_id := async_entity_id_to_device_id(hass, entity_id_or_uuid)) is None or ( + device := dev_reg.async_get(device_id=device_id) + ) is None: + return None + + return dr.DeviceInfo( + identifiers=device.identifiers, + connections=device.connections, + ) + + +@callback +def async_remove_stale_devices_links_keep_entity_device( + hass: HomeAssistant, + entry_id: str, + source_entity_id_or_uuid: str, +) -> None: + """Remove the link between stales devices and a configuration entry, keeping only the device that the informed entity is linked to.""" + + async_remove_stale_devices_links_keep_current_device( + hass=hass, + entry_id=entry_id, + current_device_id=async_entity_id_to_device_id(hass, source_entity_id_or_uuid), + ) + + +@callback +def async_remove_stale_devices_links_keep_current_device( + hass: HomeAssistant, + entry_id: str, + current_device_id: str | None, +) -> None: + """Remove the link between stales devices and a configuration entry, keeping only the device informed. + + Device passed in the current_device_id parameter will be kept linked to the configuration entry. + """ + + dev_reg = dr.async_get(hass) + # Removes all devices from the config entry that are not the same as the current device + for device in dev_reg.devices.get_devices_for_config_entry_id(entry_id): + if device.id == current_device_id: + continue + dev_reg.async_update_device(device.id, remove_config_entry_id=entry_id) diff --git a/tests/components/utility_meter/test_select.py b/tests/components/utility_meter/test_select.py new file mode 100644 index 00000000000..61f6cbe75b9 --- /dev/null +++ b/tests/components/utility_meter/test_select.py @@ -0,0 +1,56 @@ +"""The tests for the utility_meter select platform.""" + +from homeassistant.components.utility_meter.const import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr, entity_registry as er + +from tests.common import MockConfigEntry + + +async def test_device_id( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, +) -> None: + """Test for source entity device for Utility Meter.""" + source_config_entry = MockConfigEntry() + source_config_entry.add_to_hass(hass) + source_device_entry = device_registry.async_get_or_create( + config_entry_id=source_config_entry.entry_id, + identifiers={("sensor", "identifier_test")}, + connections={("mac", "30:31:32:33:34:35")}, + ) + source_entity = entity_registry.async_get_or_create( + "sensor", + "test", + "source", + config_entry=source_config_entry, + device_id=source_device_entry.id, + ) + await hass.async_block_till_done() + assert entity_registry.async_get("sensor.test_source") is not None + + utility_meter_config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={ + "cycle": "monthly", + "delta_values": False, + "name": "Energy", + "net_consumption": False, + "offset": 0, + "periodically_resetting": True, + "source": "sensor.test_source", + "tariffs": ["peak", "offpeak"], + }, + title="Energy", + ) + + utility_meter_config_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(utility_meter_config_entry.entry_id) + await hass.async_block_till_done() + + utility_meter_entity_select = entity_registry.async_get("select.energy") + assert utility_meter_entity_select is not None + assert utility_meter_entity_select.device_id == source_entity.device_id diff --git a/tests/helpers/test_device.py b/tests/helpers/test_device.py new file mode 100644 index 00000000000..9e29288027c --- /dev/null +++ b/tests/helpers/test_device.py @@ -0,0 +1,211 @@ +"""Tests for the Device Utils.""" + +import pytest +import voluptuous as vol + +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers.device import ( + async_device_info_to_link_from_entity, + async_entity_id_to_device_id, + async_remove_stale_devices_links_keep_current_device, + async_remove_stale_devices_links_keep_entity_device, +) + +from tests.common import MockConfigEntry + + +async def test_entity_id_to_device_id( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, +) -> None: + """Test returning an entity's device ID.""" + config_entry = MockConfigEntry(domain="my") + config_entry.add_to_hass(hass) + + device = device_registry.async_get_or_create( + identifiers={("test", "current_device")}, + connections={("mac", "30:31:32:33:34:00")}, + config_entry_id=config_entry.entry_id, + ) + assert device is not None + + # Entity registry + entity = entity_registry.async_get_or_create( + "sensor", + "test", + "source", + config_entry=config_entry, + device_id=device.id, + ) + await hass.async_block_till_done() + assert entity_registry.async_get("sensor.test_source") is not None + + device_id = async_entity_id_to_device_id( + hass, + entity_id_or_uuid=entity.entity_id, + ) + assert device_id == device.id + + with pytest.raises(vol.Invalid): + async_entity_id_to_device_id( + hass, + entity_id_or_uuid="unknown_uuid", + ) + + +async def test_device_info_to_link( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, +) -> None: + """Test for returning device info with device link information.""" + config_entry = MockConfigEntry(domain="my") + config_entry.add_to_hass(hass) + + device = device_registry.async_get_or_create( + identifiers={("test", "my_device")}, + connections={("mac", "30:31:32:33:34:00")}, + config_entry_id=config_entry.entry_id, + ) + assert device is not None + + # Source entity registry + source_entity = entity_registry.async_get_or_create( + "sensor", + "test", + "source", + config_entry=config_entry, + device_id=device.id, + ) + await hass.async_block_till_done() + assert entity_registry.async_get("sensor.test_source") is not None + + result = async_device_info_to_link_from_entity( + hass, entity_id_or_uuid=source_entity.entity_id + ) + assert result == { + "identifiers": {("test", "my_device")}, + "connections": {("mac", "30:31:32:33:34:00")}, + } + + # With a non-existent entity id + result = async_device_info_to_link_from_entity( + hass, entity_id_or_uuid="sensor.invalid" + ) + assert result is None + + +async def test_remove_stale_device_links_keep_entity_device( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, +) -> None: + """Test cleaning works for entity.""" + config_entry = MockConfigEntry(domain="hue") + config_entry.add_to_hass(hass) + + current_device = device_registry.async_get_or_create( + identifiers={("test", "current_device")}, + connections={("mac", "30:31:32:33:34:00")}, + config_entry_id=config_entry.entry_id, + ) + assert current_device is not None + + device_registry.async_get_or_create( + identifiers={("test", "stale_device_1")}, + connections={("mac", "30:31:32:33:34:01")}, + config_entry_id=config_entry.entry_id, + ) + + device_registry.async_get_or_create( + identifiers={("test", "stale_device_2")}, + connections={("mac", "30:31:32:33:34:02")}, + config_entry_id=config_entry.entry_id, + ) + + # Source entity registry + source_entity = entity_registry.async_get_or_create( + "sensor", + "test", + "source", + config_entry=config_entry, + device_id=current_device.id, + ) + await hass.async_block_till_done() + assert entity_registry.async_get("sensor.test_source") is not None + + devices_config_entry = device_registry.devices.get_devices_for_config_entry_id( + config_entry.entry_id + ) + + # 3 devices linked to the config entry are expected (1 current device + 2 stales) + assert len(devices_config_entry) == 3 + + # Manual cleanup should unlink stales devices from the config entry + async_remove_stale_devices_links_keep_entity_device( + hass, + entry_id=config_entry.entry_id, + source_entity_id_or_uuid=source_entity.entity_id, + ) + + devices_config_entry = device_registry.devices.get_devices_for_config_entry_id( + config_entry.entry_id + ) + + # After cleanup, only one device is expected to be linked to the configuration entry if at least source_entity_id_or_uuid or device_id was given, else zero + assert len(devices_config_entry) == 1 + + assert current_device in devices_config_entry + + +async def test_remove_stale_devices_links_keep_current_device( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, +) -> None: + """Test cleanup works for device id.""" + config_entry = MockConfigEntry(domain="hue") + config_entry.add_to_hass(hass) + + current_device = device_registry.async_get_or_create( + identifiers={("test", "current_device")}, + connections={("mac", "30:31:32:33:34:00")}, + config_entry_id=config_entry.entry_id, + ) + assert current_device is not None + + device_registry.async_get_or_create( + identifiers={("test", "stale_device_1")}, + connections={("mac", "30:31:32:33:34:01")}, + config_entry_id=config_entry.entry_id, + ) + + device_registry.async_get_or_create( + identifiers={("test", "stale_device_2")}, + connections={("mac", "30:31:32:33:34:02")}, + config_entry_id=config_entry.entry_id, + ) + + devices_config_entry = device_registry.devices.get_devices_for_config_entry_id( + config_entry.entry_id + ) + + # 3 devices linked to the config entry are expected (1 current device + 2 stales) + assert len(devices_config_entry) == 3 + + # Manual cleanup should unlink stales devices from the config entry + async_remove_stale_devices_links_keep_current_device( + hass, + entry_id=config_entry.entry_id, + current_device_id=current_device.id, + ) + + devices_config_entry = device_registry.devices.get_devices_for_config_entry_id( + config_entry.entry_id + ) + + # After cleanup, only one device is expected to be linked to the configuration entry + assert len(devices_config_entry) == 1 + + assert current_device in devices_config_entry From af59072203716f399244ef286276abf73b7bcba6 Mon Sep 17 00:00:00 2001 From: Lenn <78048721+LennP@users.noreply.github.com> Date: Fri, 21 Jun 2024 14:50:37 +0200 Subject: [PATCH 0927/1445] Bump motionblindsble to 0.1.0 (#120093) --- homeassistant/components/motionblinds_ble/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/motionblinds_ble/manifest.json b/homeassistant/components/motionblinds_ble/manifest.json index aa727be13f8..454c873dfa2 100644 --- a/homeassistant/components/motionblinds_ble/manifest.json +++ b/homeassistant/components/motionblinds_ble/manifest.json @@ -14,5 +14,5 @@ "integration_type": "device", "iot_class": "assumed_state", "loggers": ["motionblindsble"], - "requirements": ["motionblindsble==0.0.9"] + "requirements": ["motionblindsble==0.1.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 3c22b5541de..6e45a44c23b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1353,7 +1353,7 @@ mopeka-iot-ble==0.7.0 motionblinds==0.6.23 # homeassistant.components.motionblinds_ble -motionblindsble==0.0.9 +motionblindsble==0.1.0 # homeassistant.components.motioneye motioneye-client==0.3.14 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f91f683f98a..adaa08ed59b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1101,7 +1101,7 @@ mopeka-iot-ble==0.7.0 motionblinds==0.6.23 # homeassistant.components.motionblinds_ble -motionblindsble==0.0.9 +motionblindsble==0.1.0 # homeassistant.components.motioneye motioneye-client==0.3.14 From 7ba1e4446c1fc384c8d9a22044420c39acf51991 Mon Sep 17 00:00:00 2001 From: karwosts <32912880+karwosts@users.noreply.github.com> Date: Fri, 21 Jun 2024 05:53:28 -0700 Subject: [PATCH 0928/1445] Fix `for` in climate hvac_mode_changed trigger (#116455) --- homeassistant/components/climate/device_trigger.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/climate/device_trigger.py b/homeassistant/components/climate/device_trigger.py index 9702c97d0da..84651dd6d86 100644 --- a/homeassistant/components/climate/device_trigger.py +++ b/homeassistant/components/climate/device_trigger.py @@ -38,6 +38,7 @@ HVAC_MODE_TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend( vol.Required(CONF_ENTITY_ID): cv.entity_id_or_uuid, vol.Required(CONF_TYPE): "hvac_mode_changed", vol.Required(state_trigger.CONF_TO): vol.In(const.HVAC_MODES), + vol.Optional(CONF_FOR): cv.positive_time_period_dict, } ) From bad5eaf329338b3f2524ca89d34236c9f0bfddae Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Fri, 21 Jun 2024 15:04:42 +0200 Subject: [PATCH 0929/1445] Add entity ids to grouped hue light (#113053) --- homeassistant/components/hue/v2/group.py | 30 ++++++++++++++++++++---- tests/components/hue/test_light_v2.py | 9 +++++++ 2 files changed, 34 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/hue/v2/group.py b/homeassistant/components/hue/v2/group.py index db30800a333..34797b0e42c 100644 --- a/homeassistant/components/hue/v2/group.py +++ b/homeassistant/components/hue/v2/group.py @@ -26,6 +26,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback +import homeassistant.helpers.entity_registry as er from ..bridge import HueBridge from ..const import DOMAIN @@ -136,15 +137,18 @@ class GroupedHueLight(HueBaseEntity, LightEntity): scenes = { x.metadata.name for x in self.api.scenes if x.group.rid == self.group.id } - lights = { - self.controller.get_device(x.id).metadata.name - for x in self.controller.get_lights(self.resource.id) - } + light_resource_ids = tuple( + x.id for x in self.controller.get_lights(self.resource.id) + ) + light_names, light_entities = self._get_names_and_entity_ids_for_resource_ids( + light_resource_ids + ) return { "is_hue_group": True, "hue_scenes": scenes, "hue_type": self.group.type.value, - "lights": lights, + "lights": light_names, + "entity_id": light_entities, "dynamics": self._dynamic_mode_active, } @@ -278,3 +282,19 @@ class GroupedHueLight(HueBaseEntity, LightEntity): self._attr_color_mode = ColorMode.BRIGHTNESS else: self._attr_color_mode = ColorMode.ONOFF + + @callback + def _get_names_and_entity_ids_for_resource_ids( + self, resource_ids: tuple[str] + ) -> tuple[set[str], set[str]]: + """Return the names and entity ids for the given Hue (light) resource IDs.""" + ent_reg = er.async_get(self.hass) + light_names: set[str] = set() + light_entities: set[str] = set() + for resource_id in resource_ids: + light_names.add(self.controller.get_device(resource_id).metadata.name) + if entity_id := ent_reg.async_get_entity_id( + self.platform.domain, DOMAIN, resource_id + ): + light_entities.add(entity_id) + return light_names, light_entities diff --git a/tests/components/hue/test_light_v2.py b/tests/components/hue/test_light_v2.py index 1f25649fdaa..fca907eabb0 100644 --- a/tests/components/hue/test_light_v2.py +++ b/tests/components/hue/test_light_v2.py @@ -412,6 +412,11 @@ async def test_grouped_lights( "Hue light with color and color temperature gradient", "Hue light with color and color temperature 2", } + assert test_entity.attributes["entity_id"] == { + "light.hue_light_with_color_and_color_temperature_gradient", + "light.hue_light_with_color_and_color_temperature_2", + "light.hue_light_with_color_and_color_temperature_1", + } # test light created for hue room test_entity = hass.states.get("light.test_room") @@ -431,6 +436,10 @@ async def test_grouped_lights( "Hue on/off light", "Hue light with color temperature only", } + assert test_entity.attributes["entity_id"] == { + "light.hue_light_with_color_temperature_only", + "light.hue_on_off_light", + } # Test calling the turn on service on a grouped light test_light_id = "light.test_zone" From 7f20173f6d1d7edc5826109d800e0b47d10bf782 Mon Sep 17 00:00:00 2001 From: Erwin Douna Date: Fri, 21 Jun 2024 15:07:43 +0200 Subject: [PATCH 0930/1445] MelCloud add diagnostics platform (#115962) --- .../components/melcloud/diagnostics.py | 38 ++++++++++++++++++ .../melcloud/snapshots/test_diagnostics.ambr | 23 +++++++++++ tests/components/melcloud/test_diagnostics.py | 39 +++++++++++++++++++ 3 files changed, 100 insertions(+) create mode 100644 homeassistant/components/melcloud/diagnostics.py create mode 100644 tests/components/melcloud/snapshots/test_diagnostics.ambr create mode 100644 tests/components/melcloud/test_diagnostics.py diff --git a/homeassistant/components/melcloud/diagnostics.py b/homeassistant/components/melcloud/diagnostics.py new file mode 100644 index 00000000000..8c2ad0818ff --- /dev/null +++ b/homeassistant/components/melcloud/diagnostics.py @@ -0,0 +1,38 @@ +"""Diagnostics support for MelCloud.""" + +from __future__ import annotations + +from typing import Any + +from homeassistant.components.diagnostics.util import async_redact_data +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_TOKEN, CONF_USERNAME +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +TO_REDACT = { + CONF_USERNAME, + CONF_TOKEN, +} + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, entry: ConfigEntry +) -> dict[str, Any]: + """Return diagnostics for the config entry.""" + ent_reg = er.async_get(hass) + entities = [ + entity.entity_id + for entity in er.async_entries_for_config_entry(ent_reg, entry.entry_id) + ] + + entity_states = {entity: hass.states.get(entity) for entity in entities} + + entry_dict = entry.as_dict() + if "data" in entry_dict: + entry_dict["data"] = async_redact_data(entry_dict["data"], TO_REDACT) + + return { + "entry": entry_dict, + "entities": entity_states, + } diff --git a/tests/components/melcloud/snapshots/test_diagnostics.ambr b/tests/components/melcloud/snapshots/test_diagnostics.ambr new file mode 100644 index 00000000000..7b0173c240e --- /dev/null +++ b/tests/components/melcloud/snapshots/test_diagnostics.ambr @@ -0,0 +1,23 @@ +# serializer version: 1 +# name: test_get_config_entry_diagnostics + dict({ + 'entities': dict({ + }), + 'entry': dict({ + 'data': dict({ + }), + 'disabled_by': None, + 'domain': 'melcloud', + 'entry_id': 'TEST_ENTRY_ID', + 'minor_version': 1, + 'options': dict({ + }), + 'pref_disable_new_entities': False, + 'pref_disable_polling': False, + 'source': 'user', + 'title': 'melcloud', + 'unique_id': 'UNIQUE_TEST_ID', + 'version': 1, + }), + }) +# --- diff --git a/tests/components/melcloud/test_diagnostics.py b/tests/components/melcloud/test_diagnostics.py new file mode 100644 index 00000000000..cbb35eadfd4 --- /dev/null +++ b/tests/components/melcloud/test_diagnostics.py @@ -0,0 +1,39 @@ +"""Test the DSMR Reader component diagnostics.""" + +from unittest.mock import patch + +from syrupy import SnapshotAssertion + +from homeassistant.components.melcloud.const import DOMAIN +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry +from tests.components.diagnostics import get_diagnostics_for_config_entry +from tests.typing import ClientSessionGenerator + + +async def test_get_config_entry_diagnostics( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + hass_client: ClientSessionGenerator, +) -> None: + """Test if get_config_entry_diagnostics returns the correct data.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + title=DOMAIN, + options={}, + entry_id="TEST_ENTRY_ID", + unique_id="UNIQUE_TEST_ID", + ) + config_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.melcloud.async_setup_entry", return_value=True + ): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + diagnostics = await get_diagnostics_for_config_entry( + hass, hass_client, config_entry + ) + assert diagnostics == snapshot From 6caf614efd63de81cff5af7e95b4d6f42f894442 Mon Sep 17 00:00:00 2001 From: tronikos Date: Fri, 21 Jun 2024 06:08:32 -0700 Subject: [PATCH 0931/1445] Add camera entity in Fully Kiosk Browser (#119483) --- .../components/fully_kiosk/__init__.py | 1 + .../components/fully_kiosk/camera.py | 56 +++++++++++++++++++ tests/components/fully_kiosk/test_camera.py | 55 ++++++++++++++++++ .../fully_kiosk/test_media_player.py | 2 + 4 files changed, 114 insertions(+) create mode 100644 homeassistant/components/fully_kiosk/camera.py create mode 100644 tests/components/fully_kiosk/test_camera.py diff --git a/homeassistant/components/fully_kiosk/__init__.py b/homeassistant/components/fully_kiosk/__init__.py index a0ed0cb4fa0..95d7d59ecbf 100644 --- a/homeassistant/components/fully_kiosk/__init__.py +++ b/homeassistant/components/fully_kiosk/__init__.py @@ -13,6 +13,7 @@ from .services import async_setup_services PLATFORMS = [ Platform.BINARY_SENSOR, Platform.BUTTON, + Platform.CAMERA, Platform.MEDIA_PLAYER, Platform.NUMBER, Platform.SENSOR, diff --git a/homeassistant/components/fully_kiosk/camera.py b/homeassistant/components/fully_kiosk/camera.py new file mode 100644 index 00000000000..99419271c26 --- /dev/null +++ b/homeassistant/components/fully_kiosk/camera.py @@ -0,0 +1,56 @@ +"""Support for Fully Kiosk Browser camera.""" + +from __future__ import annotations + +from homeassistant.components.camera import Camera, CameraEntityFeature +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .coordinator import FullyKioskDataUpdateCoordinator +from .entity import FullyKioskEntity + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up the cameras.""" + coordinator: FullyKioskDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + async_add_entities([FullyCameraEntity(coordinator)]) + + +class FullyCameraEntity(FullyKioskEntity, Camera): + """Fully Kiosk Browser camera entity.""" + + _attr_name = None + _attr_supported_features = CameraEntityFeature.ON_OFF + + def __init__(self, coordinator: FullyKioskDataUpdateCoordinator) -> None: + """Initialize the camera.""" + FullyKioskEntity.__init__(self, coordinator) + Camera.__init__(self) + self._attr_unique_id = f"{coordinator.data['deviceID']}-camera" + + async def async_camera_image( + self, width: int | None = None, height: int | None = None + ) -> bytes | None: + """Return bytes of camera image.""" + image_bytes: bytes = await self.coordinator.fully.getCamshot() + return image_bytes + + async def async_turn_on(self) -> None: + """Turn on camera.""" + await self.coordinator.fully.enableMotionDetection() + await self.coordinator.async_refresh() + + async def async_turn_off(self) -> None: + """Turn off camera.""" + await self.coordinator.fully.disableMotionDetection() + await self.coordinator.async_refresh() + + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + self._attr_is_on = self.coordinator.data["settings"].get("motionDetection") + self.async_write_ha_state() diff --git a/tests/components/fully_kiosk/test_camera.py b/tests/components/fully_kiosk/test_camera.py new file mode 100644 index 00000000000..4e48749eebb --- /dev/null +++ b/tests/components/fully_kiosk/test_camera.py @@ -0,0 +1,55 @@ +"""Test the Fully Kiosk Browser camera platform.""" + +from unittest.mock import MagicMock + +import pytest + +from homeassistant.components.camera import async_get_image +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import entity_registry as er + +from tests.common import MockConfigEntry + + +async def test_camera( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + mock_fully_kiosk: MagicMock, + init_integration: MockConfigEntry, +) -> None: + """Test the camera entity.""" + entity_camera = "camera.amazon_fire" + entity = hass.states.get(entity_camera) + assert entity + assert entity.state == "idle" + entry = entity_registry.async_get(entity_camera) + assert entry + assert entry.unique_id == "abcdef-123456-camera" + + mock_fully_kiosk.getSettings.return_value = {"motionDetection": True} + await hass.services.async_call( + "camera", + "turn_on", + {"entity_id": entity_camera}, + blocking=True, + ) + assert len(mock_fully_kiosk.enableMotionDetection.mock_calls) == 1 + + mock_fully_kiosk.getCamshot.return_value = b"image_bytes" + image = await async_get_image(hass, entity_camera) + assert mock_fully_kiosk.getCamshot.call_count == 1 + assert image.content == b"image_bytes" + + mock_fully_kiosk.getSettings.return_value = {"motionDetection": False} + await hass.services.async_call( + "camera", + "turn_off", + {"entity_id": entity_camera}, + blocking=True, + ) + assert len(mock_fully_kiosk.disableMotionDetection.mock_calls) == 1 + + with pytest.raises(HomeAssistantError) as error: + await async_get_image(hass, entity_camera) + assert error.value.args[0] == "Camera is off" diff --git a/tests/components/fully_kiosk/test_media_player.py b/tests/components/fully_kiosk/test_media_player.py index b6eff4cfa2c..4ee9b595a82 100644 --- a/tests/components/fully_kiosk/test_media_player.py +++ b/tests/components/fully_kiosk/test_media_player.py @@ -113,6 +113,8 @@ async def test_browse_media( { "id": 1, "type": "media_player/browse_media", + "media_content_id": "media-source://media_source", + "media_content_type": "library", "entity_id": "media_player.amazon_fire", } ) From e149aa6b2e14917a120977e72b6943db08c4bc81 Mon Sep 17 00:00:00 2001 From: Josef Zweck <24647999+zweckj@users.noreply.github.com> Date: Fri, 21 Jun 2024 15:27:22 +0200 Subject: [PATCH 0932/1445] Add backflush sensor to lamarzocco (#119888) Co-authored-by: Joost Lekkerkerker --- .../components/lamarzocco/binary_sensor.py | 7 +++ homeassistant/components/lamarzocco/button.py | 1 + .../components/lamarzocco/icons.json | 6 +++ .../components/lamarzocco/strings.json | 3 ++ .../snapshots/test_binary_sensor.ambr | 47 +++++++++++++++++++ .../lamarzocco/test_binary_sensor.py | 1 + 6 files changed, 65 insertions(+) diff --git a/homeassistant/components/lamarzocco/binary_sensor.py b/homeassistant/components/lamarzocco/binary_sensor.py index 2ad72ea4087..81ac3672a0f 100644 --- a/homeassistant/components/lamarzocco/binary_sensor.py +++ b/homeassistant/components/lamarzocco/binary_sensor.py @@ -45,6 +45,13 @@ ENTITIES: tuple[LaMarzoccoBinarySensorEntityDescription, ...] = ( available_fn=lambda device: device.websocket_connected, entity_category=EntityCategory.DIAGNOSTIC, ), + LaMarzoccoBinarySensorEntityDescription( + key="backflush_enabled", + translation_key="backflush_enabled", + device_class=BinarySensorDeviceClass.RUNNING, + is_on_fn=lambda config: config.backflush_enabled, + entity_category=EntityCategory.DIAGNOSTIC, + ), ) diff --git a/homeassistant/components/lamarzocco/button.py b/homeassistant/components/lamarzocco/button.py index c261630836e..7b38c9fbf72 100644 --- a/homeassistant/components/lamarzocco/button.py +++ b/homeassistant/components/lamarzocco/button.py @@ -56,3 +56,4 @@ class LaMarzoccoButtonEntity(LaMarzoccoEntity, ButtonEntity): async def async_press(self) -> None: """Press button.""" await self.entity_description.press_fn(self.coordinator.device) + await self.coordinator.async_request_refresh() diff --git a/homeassistant/components/lamarzocco/icons.json b/homeassistant/components/lamarzocco/icons.json index 965ee7e3c3f..bc7d621d91d 100644 --- a/homeassistant/components/lamarzocco/icons.json +++ b/homeassistant/components/lamarzocco/icons.json @@ -14,6 +14,12 @@ "on": "mdi:cup-water", "off": "mdi:cup-off" } + }, + "backflush_enabled": { + "default": "mdi:water-off", + "state": { + "on": "mdi:water" + } } }, "button": { diff --git a/homeassistant/components/lamarzocco/strings.json b/homeassistant/components/lamarzocco/strings.json index f6b979a30ae..08e3e764379 100644 --- a/homeassistant/components/lamarzocco/strings.json +++ b/homeassistant/components/lamarzocco/strings.json @@ -54,6 +54,9 @@ }, "entity": { "binary_sensor": { + "backflush_enabled": { + "name": "Backflush active" + }, "brew_active": { "name": "Brewing active" }, diff --git a/tests/components/lamarzocco/snapshots/test_binary_sensor.ambr b/tests/components/lamarzocco/snapshots/test_binary_sensor.ambr index f08c2c28851..df47ac002e6 100644 --- a/tests/components/lamarzocco/snapshots/test_binary_sensor.ambr +++ b/tests/components/lamarzocco/snapshots/test_binary_sensor.ambr @@ -1,4 +1,51 @@ # serializer version: 1 +# name: test_binary_sensors[GS01234_backflush_active-binary_sensor] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'running', + 'friendly_name': 'GS01234 Backflush active', + }), + 'context': , + 'entity_id': 'binary_sensor.gs01234_backflush_active', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensors[GS01234_backflush_active-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.gs01234_backflush_active', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Backflush active', + 'platform': 'lamarzocco', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'backflush_enabled', + 'unique_id': 'GS01234_backflush_enabled', + 'unit_of_measurement': None, + }) +# --- # name: test_binary_sensors[GS01234_brewing_active-binary_sensor] StateSnapshot({ 'attributes': ReadOnlyDict({ diff --git a/tests/components/lamarzocco/test_binary_sensor.py b/tests/components/lamarzocco/test_binary_sensor.py index 36acde91a68..d363b96ca21 100644 --- a/tests/components/lamarzocco/test_binary_sensor.py +++ b/tests/components/lamarzocco/test_binary_sensor.py @@ -18,6 +18,7 @@ from tests.common import MockConfigEntry, async_fire_time_changed BINARY_SENSORS = ( "brewing_active", + "backflush_active", "water_tank_empty", ) From 4aecd23f1db48ee575af259ce17c81aa1dd28da8 Mon Sep 17 00:00:00 2001 From: Thomas55555 <59625598+Thomas55555@users.noreply.github.com> Date: Fri, 21 Jun 2024 15:27:39 +0200 Subject: [PATCH 0933/1445] Fix Husqvarna Automower schedule switch turning back on (#117692) --- .../components/husqvarna_automower/switch.py | 8 ++------ tests/components/husqvarna_automower/test_switch.py | 11 +++++------ 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/husqvarna_automower/switch.py b/homeassistant/components/husqvarna_automower/switch.py index fed2d3cfedc..a856e9c9050 100644 --- a/homeassistant/components/husqvarna_automower/switch.py +++ b/homeassistant/components/husqvarna_automower/switch.py @@ -7,8 +7,8 @@ from typing import TYPE_CHECKING, Any from aioautomower.exceptions import ApiException from aioautomower.model import ( MowerActivities, + MowerModes, MowerStates, - RestrictedReasons, StayOutZones, Zone, ) @@ -86,11 +86,7 @@ class AutomowerScheduleSwitchEntity(AutomowerControlEntity, SwitchEntity): @property def is_on(self) -> bool: """Return the state of the switch.""" - attributes = self.mower_attributes - return not ( - attributes.mower.state == MowerStates.RESTRICTED - and attributes.planner.restricted_reason == RestrictedReasons.NOT_APPLICABLE - ) + return self.mower_attributes.mower.mode != MowerModes.HOME @property def available(self) -> bool: diff --git a/tests/components/husqvarna_automower/test_switch.py b/tests/components/husqvarna_automower/test_switch.py index de18f9081ea..08450158876 100644 --- a/tests/components/husqvarna_automower/test_switch.py +++ b/tests/components/husqvarna_automower/test_switch.py @@ -3,7 +3,7 @@ from unittest.mock import AsyncMock, patch from aioautomower.exceptions import ApiException -from aioautomower.model import MowerStates, RestrictedReasons +from aioautomower.model import MowerModes from aioautomower.utils import mower_list_to_dictionary_dataclass from freezegun.api import FrozenDateTimeFactory import pytest @@ -41,12 +41,11 @@ async def test_switch_states( ) await setup_integration(hass, mock_config_entry) - for state, restricted_reson, expected_state in ( - (MowerStates.RESTRICTED, RestrictedReasons.NOT_APPLICABLE, "off"), - (MowerStates.IN_OPERATION, RestrictedReasons.NONE, "on"), + for mode, expected_state in ( + (MowerModes.HOME, "off"), + (MowerModes.MAIN_AREA, "on"), ): - values[TEST_MOWER_ID].mower.state = state - values[TEST_MOWER_ID].planner.restricted_reason = restricted_reson + values[TEST_MOWER_ID].mower.mode = mode mock_automower_client.get_status.return_value = values freezer.tick(SCAN_INTERVAL) async_fire_time_changed(hass) From 648ef948888fd9f5afbd15af89fda3f528cb016a Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 21 Jun 2024 15:43:27 +0200 Subject: [PATCH 0934/1445] Improve type hints in core helper tests (#120096) --- tests/helpers/test_collection.py | 10 ++--- tests/helpers/test_config_validation.py | 6 +-- tests/helpers/test_discovery_flow.py | 15 +++++--- tests/helpers/test_entity.py | 28 +++++++------- tests/helpers/test_json.py | 6 +-- tests/helpers/test_restore_state.py | 4 +- tests/helpers/test_significant_change.py | 14 +++++-- tests/helpers/test_storage.py | 47 +++++++++++++----------- 8 files changed, 71 insertions(+), 59 deletions(-) diff --git a/tests/helpers/test_collection.py b/tests/helpers/test_collection.py index f4d5b06dae0..f0287218d7f 100644 --- a/tests/helpers/test_collection.py +++ b/tests/helpers/test_collection.py @@ -37,7 +37,7 @@ def track_changes(coll: collection.ObservableCollection): class MockEntity(collection.CollectionEntity): """Entity that is config based.""" - def __init__(self, config): + def __init__(self, config: ConfigType) -> None: """Initialize entity.""" self._config = config @@ -52,21 +52,21 @@ class MockEntity(collection.CollectionEntity): raise NotImplementedError @property - def unique_id(self): + def unique_id(self) -> str: """Return unique ID of entity.""" return self._config["id"] @property - def name(self): + def name(self) -> str: """Return name of entity.""" return self._config["name"] @property - def state(self): + def state(self) -> str: """Return state of entity.""" return self._config["state"] - async def async_update_config(self, config): + async def async_update_config(self, config: ConfigType) -> None: """Update entity config.""" self._config = config self.async_write_ha_state() diff --git a/tests/helpers/test_config_validation.py b/tests/helpers/test_config_validation.py index 163a33db988..6df29eefaff 100644 --- a/tests/helpers/test_config_validation.py +++ b/tests/helpers/test_config_validation.py @@ -1240,7 +1240,7 @@ def test_enum() -> None: schema("value3") -def test_socket_timeout(): +def test_socket_timeout() -> None: """Test socket timeout validator.""" schema = vol.Schema(cv.socket_timeout) @@ -1679,7 +1679,7 @@ def test_color_hex() -> None: cv.color_hex(123456) -def test_determine_script_action_ambiguous(): +def test_determine_script_action_ambiguous() -> None: """Test determine script action with ambiguous actions.""" assert ( cv.determine_script_action( @@ -1696,6 +1696,6 @@ def test_determine_script_action_ambiguous(): ) -def test_determine_script_action_non_ambiguous(): +def test_determine_script_action_non_ambiguous() -> None: """Test determine script action with a non ambiguous action.""" assert cv.determine_script_action({"delay": "00:00:05"}) == "delay" diff --git a/tests/helpers/test_discovery_flow.py b/tests/helpers/test_discovery_flow.py index 7710eb2c7c7..9c2249ac17f 100644 --- a/tests/helpers/test_discovery_flow.py +++ b/tests/helpers/test_discovery_flow.py @@ -3,6 +3,7 @@ from unittest.mock import AsyncMock, call, patch import pytest +from typing_extensions import Generator from homeassistant import config_entries from homeassistant.core import EVENT_HOMEASSISTANT_STARTED, CoreState, HomeAssistant @@ -10,7 +11,7 @@ from homeassistant.helpers import discovery_flow @pytest.fixture -def mock_flow_init(hass): +def mock_flow_init(hass: HomeAssistant) -> Generator[AsyncMock]: """Mock hass.config_entries.flow.async_init.""" with patch.object( hass.config_entries.flow, "async_init", return_value=AsyncMock() @@ -18,7 +19,9 @@ def mock_flow_init(hass): yield mock_init -async def test_async_create_flow(hass: HomeAssistant, mock_flow_init) -> None: +async def test_async_create_flow( + hass: HomeAssistant, mock_flow_init: AsyncMock +) -> None: """Test we can create a flow.""" discovery_flow.async_create_flow( hass, @@ -36,7 +39,7 @@ async def test_async_create_flow(hass: HomeAssistant, mock_flow_init) -> None: async def test_async_create_flow_deferred_until_started( - hass: HomeAssistant, mock_flow_init + hass: HomeAssistant, mock_flow_init: AsyncMock ) -> None: """Test flows are deferred until started.""" hass.set_state(CoreState.stopped) @@ -59,7 +62,7 @@ async def test_async_create_flow_deferred_until_started( async def test_async_create_flow_checks_existing_flows_after_startup( - hass: HomeAssistant, mock_flow_init + hass: HomeAssistant, mock_flow_init: AsyncMock ) -> None: """Test existing flows prevent an identical ones from being after startup.""" hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) @@ -77,7 +80,7 @@ async def test_async_create_flow_checks_existing_flows_after_startup( async def test_async_create_flow_checks_existing_flows_before_startup( - hass: HomeAssistant, mock_flow_init + hass: HomeAssistant, mock_flow_init: AsyncMock ) -> None: """Test existing flows prevent an identical ones from being created before startup.""" hass.set_state(CoreState.stopped) @@ -100,7 +103,7 @@ async def test_async_create_flow_checks_existing_flows_before_startup( async def test_async_create_flow_does_nothing_after_stop( - hass: HomeAssistant, mock_flow_init + hass: HomeAssistant, mock_flow_init: AsyncMock ) -> None: """Test we no longer create flows when hass is stopping.""" hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) diff --git a/tests/helpers/test_entity.py b/tests/helpers/test_entity.py index 9d2c9a66a5b..f76b8555580 100644 --- a/tests/helpers/test_entity.py +++ b/tests/helpers/test_entity.py @@ -237,12 +237,12 @@ async def test_async_async_request_call_without_lock(hass: HomeAssistant) -> Non class AsyncEntity(entity.Entity): """Test entity.""" - def __init__(self, entity_id): + def __init__(self, entity_id: str) -> None: """Initialize Async test entity.""" self.entity_id = entity_id self.hass = hass - async def testhelper(self, count): + async def testhelper(self, count: int) -> None: """Helper function.""" updates.append(count) @@ -274,7 +274,7 @@ async def test_async_async_request_call_with_lock(hass: HomeAssistant) -> None: class AsyncEntity(entity.Entity): """Test entity.""" - def __init__(self, entity_id, lock): + def __init__(self, entity_id: str, lock: asyncio.Semaphore) -> None: """Initialize Async test entity.""" self.entity_id = entity_id self.hass = hass @@ -324,13 +324,13 @@ async def test_async_parallel_updates_with_zero(hass: HomeAssistant) -> None: class AsyncEntity(entity.Entity): """Test entity.""" - def __init__(self, entity_id, count): + def __init__(self, entity_id: str, count: int) -> None: """Initialize Async test entity.""" self.entity_id = entity_id self.hass = hass self._count = count - async def async_update(self): + async def async_update(self) -> None: """Test update.""" updates.append(self._count) await test_lock.wait() @@ -363,7 +363,7 @@ async def test_async_parallel_updates_with_zero_on_sync_update( class AsyncEntity(entity.Entity): """Test entity.""" - def __init__(self, entity_id, count): + def __init__(self, entity_id: str, count: int) -> None: """Initialize Async test entity.""" self.entity_id = entity_id self.hass = hass @@ -404,14 +404,14 @@ async def test_async_parallel_updates_with_one(hass: HomeAssistant) -> None: class AsyncEntity(entity.Entity): """Test entity.""" - def __init__(self, entity_id, count): + def __init__(self, entity_id: str, count: int) -> None: """Initialize Async test entity.""" self.entity_id = entity_id self.hass = hass self._count = count self.parallel_updates = test_semaphore - async def async_update(self): + async def async_update(self) -> None: """Test update.""" updates.append(self._count) await test_lock.acquire() @@ -480,14 +480,14 @@ async def test_async_parallel_updates_with_two(hass: HomeAssistant) -> None: class AsyncEntity(entity.Entity): """Test entity.""" - def __init__(self, entity_id, count): + def __init__(self, entity_id: str, count: int) -> None: """Initialize Async test entity.""" self.entity_id = entity_id self.hass = hass self._count = count self.parallel_updates = test_semaphore - async def async_update(self): + async def async_update(self) -> None: """Test update.""" updates.append(self._count) await test_lock.acquire() @@ -550,13 +550,13 @@ async def test_async_parallel_updates_with_one_using_executor( class SyncEntity(entity.Entity): """Test entity.""" - def __init__(self, entity_id): + def __init__(self, entity_id: str) -> None: """Initialize sync test entity.""" self.entity_id = entity_id self.hass = hass self.parallel_updates = test_semaphore - def update(self): + def update(self) -> None: """Test update.""" locked.append(self.parallel_updates.locked()) @@ -629,7 +629,7 @@ async def test_async_remove_twice(hass: HomeAssistant) -> None: def __init__(self) -> None: self.remove_calls = [] - async def async_will_remove_from_hass(self): + async def async_will_remove_from_hass(self) -> None: self.remove_calls.append(None) platform = MockEntityPlatform(hass, domain="test") @@ -2376,7 +2376,7 @@ async def test_cached_entity_property_class_attribute(hass: HomeAssistant) -> No This class overrides the attribute property. """ - def __init__(self): + def __init__(self) -> None: self._attr_attribution = values[0] @cached_property diff --git a/tests/helpers/test_json.py b/tests/helpers/test_json.py index 57269963164..061faed6f93 100644 --- a/tests/helpers/test_json.py +++ b/tests/helpers/test_json.py @@ -7,7 +7,7 @@ import math import os from pathlib import Path import time -from typing import NamedTuple +from typing import Any, NamedTuple from unittest.mock import Mock, patch import pytest @@ -325,10 +325,10 @@ def test_find_unserializable_data() -> None: ) == {"$[0](Event: bad_event).data.bad_attribute": bad_data} class BadData: - def __init__(self): + def __init__(self) -> None: self.bla = bad_data - def as_dict(self): + def as_dict(self) -> dict[str, Any]: return {"bla": self.bla} assert find_paths_unserializable_data( diff --git a/tests/helpers/test_restore_state.py b/tests/helpers/test_restore_state.py index 729212f4c1d..865ee5efaf7 100644 --- a/tests/helpers/test_restore_state.py +++ b/tests/helpers/test_restore_state.py @@ -484,12 +484,12 @@ async def test_restore_entity_end_to_end( class MockRestoreEntity(RestoreEntity): """Mock restore entity.""" - def __init__(self): + def __init__(self) -> None: """Initialize the mock entity.""" self._state: str | None = None @property - def state(self): + def state(self) -> str | None: """Return the state.""" return self._state diff --git a/tests/helpers/test_significant_change.py b/tests/helpers/test_significant_change.py index e930ff30feb..f9dca5b6034 100644 --- a/tests/helpers/test_significant_change.py +++ b/tests/helpers/test_significant_change.py @@ -9,7 +9,9 @@ from homeassistant.helpers import significant_change @pytest.fixture(name="checker") -async def checker_fixture(hass): +async def checker_fixture( + hass: HomeAssistant, +) -> significant_change.SignificantlyChangedChecker: """Checker fixture.""" checker = await significant_change.create_checker(hass, "test") @@ -24,7 +26,9 @@ async def checker_fixture(hass): return checker -async def test_signicant_change(hass: HomeAssistant, checker) -> None: +async def test_signicant_change( + checker: significant_change.SignificantlyChangedChecker, +) -> None: """Test initialize helper works.""" ent_id = "test_domain.test_entity" attrs = {ATTR_DEVICE_CLASS: SensorDeviceClass.BATTERY} @@ -48,7 +52,9 @@ async def test_signicant_change(hass: HomeAssistant, checker) -> None: assert checker.async_is_significant_change(State(ent_id, STATE_UNAVAILABLE, attrs)) -async def test_significant_change_extra(hass: HomeAssistant, checker) -> None: +async def test_significant_change_extra( + checker: significant_change.SignificantlyChangedChecker, +) -> None: """Test extra significant checker works.""" ent_id = "test_domain.test_entity" attrs = {ATTR_DEVICE_CLASS: SensorDeviceClass.BATTERY} @@ -75,7 +81,7 @@ async def test_significant_change_extra(hass: HomeAssistant, checker) -> None: assert checker.async_is_significant_change(State(ent_id, "200", attrs), extra_arg=2) -async def test_check_valid_float(hass: HomeAssistant) -> None: +async def test_check_valid_float() -> None: """Test extra significant checker works.""" assert significant_change.check_valid_float("1") assert significant_change.check_valid_float("1.0") diff --git a/tests/helpers/test_storage.py b/tests/helpers/test_storage.py index 651c7ce5cbc..822b56604c0 100644 --- a/tests/helpers/test_storage.py +++ b/tests/helpers/test_storage.py @@ -40,13 +40,13 @@ MOCK_DATA2 = {"goodbye": "cruel world"} @pytest.fixture -def store(hass): +def store(hass: HomeAssistant) -> storage.Store: """Fixture of a store that prevents writing on Home Assistant stop.""" return storage.Store(hass, MOCK_VERSION, MOCK_KEY) @pytest.fixture -def store_v_1_1(hass): +def store_v_1_1(hass: HomeAssistant) -> storage.Store: """Fixture of a store that prevents writing on Home Assistant stop.""" return storage.Store( hass, MOCK_VERSION, MOCK_KEY, minor_version=MOCK_MINOR_VERSION_1 @@ -54,7 +54,7 @@ def store_v_1_1(hass): @pytest.fixture -def store_v_1_2(hass): +def store_v_1_2(hass: HomeAssistant) -> storage.Store: """Fixture of a store that prevents writing on Home Assistant stop.""" return storage.Store( hass, MOCK_VERSION, MOCK_KEY, minor_version=MOCK_MINOR_VERSION_2 @@ -62,7 +62,7 @@ def store_v_1_2(hass): @pytest.fixture -def store_v_2_1(hass): +def store_v_2_1(hass: HomeAssistant) -> storage.Store: """Fixture of a store that prevents writing on Home Assistant stop.""" return storage.Store( hass, MOCK_VERSION_2, MOCK_KEY, minor_version=MOCK_MINOR_VERSION_1 @@ -70,12 +70,12 @@ def store_v_2_1(hass): @pytest.fixture -def read_only_store(hass): +def read_only_store(hass: HomeAssistant) -> storage.Store: """Fixture of a read only store.""" return storage.Store(hass, MOCK_VERSION, MOCK_KEY, read_only=True) -async def test_loading(hass: HomeAssistant, store) -> None: +async def test_loading(hass: HomeAssistant, store: storage.Store) -> None: """Test we can save and load data.""" await store.async_save(MOCK_DATA) data = await store.async_load() @@ -100,7 +100,7 @@ async def test_custom_encoder(hass: HomeAssistant) -> None: assert data == "9" -async def test_loading_non_existing(hass: HomeAssistant, store) -> None: +async def test_loading_non_existing(hass: HomeAssistant, store: storage.Store) -> None: """Test we can save and load data.""" with patch("homeassistant.util.json.open", side_effect=FileNotFoundError): data = await store.async_load() @@ -109,7 +109,7 @@ async def test_loading_non_existing(hass: HomeAssistant, store) -> None: async def test_loading_parallel( hass: HomeAssistant, - store, + store: storage.Store, hass_storage: dict[str, Any], caplog: pytest.LogCaptureFixture, ) -> None: @@ -292,7 +292,7 @@ async def test_not_saving_while_stopping( async def test_loading_while_delay( - hass: HomeAssistant, store, hass_storage: dict[str, Any] + hass: HomeAssistant, store: storage.Store, hass_storage: dict[str, Any] ) -> None: """Test we load new data even if not written yet.""" await store.async_save({"delay": "no"}) @@ -316,7 +316,7 @@ async def test_loading_while_delay( async def test_writing_while_writing_delay( - hass: HomeAssistant, store, hass_storage: dict[str, Any] + hass: HomeAssistant, store: storage.Store, hass_storage: dict[str, Any] ) -> None: """Test a write while a write with delay is active.""" store.async_delay_save(lambda: {"delay": "yes"}, 1) @@ -343,7 +343,7 @@ async def test_writing_while_writing_delay( async def test_multiple_delay_save_calls( - hass: HomeAssistant, store, hass_storage: dict[str, Any] + hass: HomeAssistant, store: storage.Store, hass_storage: dict[str, Any] ) -> None: """Test a write while a write with changing delays.""" store.async_delay_save(lambda: {"delay": "yes"}, 1) @@ -390,7 +390,7 @@ async def test_delay_save_zero( async def test_multiple_save_calls( - hass: HomeAssistant, store, hass_storage: dict[str, Any] + hass: HomeAssistant, store: storage.Store, hass_storage: dict[str, Any] ) -> None: """Test multiple write tasks.""" @@ -410,7 +410,7 @@ async def test_multiple_save_calls( async def test_migrator_no_existing_config( - hass: HomeAssistant, store, hass_storage: dict[str, Any] + hass: HomeAssistant, store: storage.Store, hass_storage: dict[str, Any] ) -> None: """Test migrator with no existing config.""" with ( @@ -424,7 +424,7 @@ async def test_migrator_no_existing_config( async def test_migrator_existing_config( - hass: HomeAssistant, store, hass_storage: dict[str, Any] + hass: HomeAssistant, store: storage.Store, hass_storage: dict[str, Any] ) -> None: """Test migrating existing config.""" with patch("os.path.isfile", return_value=True), patch("os.remove") as mock_remove: @@ -443,7 +443,7 @@ async def test_migrator_existing_config( async def test_migrator_transforming_config( - hass: HomeAssistant, store, hass_storage: dict[str, Any] + hass: HomeAssistant, store: storage.Store, hass_storage: dict[str, Any] ) -> None: """Test migrating config to new format.""" @@ -471,7 +471,7 @@ async def test_migrator_transforming_config( async def test_minor_version_default( - hass: HomeAssistant, store, hass_storage: dict[str, Any] + hass: HomeAssistant, store: storage.Store, hass_storage: dict[str, Any] ) -> None: """Test minor version default.""" @@ -480,7 +480,7 @@ async def test_minor_version_default( async def test_minor_version( - hass: HomeAssistant, store_v_1_2, hass_storage: dict[str, Any] + hass: HomeAssistant, store_v_1_2: storage.Store, hass_storage: dict[str, Any] ) -> None: """Test minor version.""" @@ -489,7 +489,7 @@ async def test_minor_version( async def test_migrate_major_not_implemented_raises( - hass: HomeAssistant, store, store_v_2_1 + hass: HomeAssistant, store: storage.Store, store_v_2_1: storage.Store ) -> None: """Test migrating between major versions fails if not implemented.""" @@ -499,7 +499,10 @@ async def test_migrate_major_not_implemented_raises( async def test_migrate_minor_not_implemented( - hass: HomeAssistant, hass_storage: dict[str, Any], store_v_1_1, store_v_1_2 + hass: HomeAssistant, + hass_storage: dict[str, Any], + store_v_1_1: storage.Store, + store_v_1_2: storage.Store, ) -> None: """Test migrating between minor versions does not fail if not implemented.""" @@ -525,7 +528,7 @@ async def test_migrate_minor_not_implemented( async def test_migration( - hass: HomeAssistant, hass_storage: dict[str, Any], store_v_1_2 + hass: HomeAssistant, hass_storage: dict[str, Any], store_v_1_2: storage.Store ) -> None: """Test migration.""" calls = 0 @@ -564,7 +567,7 @@ async def test_migration( async def test_legacy_migration( - hass: HomeAssistant, hass_storage: dict[str, Any], store_v_1_2 + hass: HomeAssistant, hass_storage: dict[str, Any], store_v_1_2: storage.Store ) -> None: """Test legacy migration method signature.""" calls = 0 @@ -600,7 +603,7 @@ async def test_legacy_migration( async def test_changing_delayed_written_data( - hass: HomeAssistant, store, hass_storage: dict[str, Any] + hass: HomeAssistant, store: storage.Store, hass_storage: dict[str, Any] ) -> None: """Test changing data that is written with delay.""" data_to_store = {"hello": "world"} From 12f812d6da75124a249a7f03ff881518610a3176 Mon Sep 17 00:00:00 2001 From: jvmahon Date: Fri, 21 Jun 2024 09:53:50 -0400 Subject: [PATCH 0935/1445] Add number platform to Matter integration (#119770) Co-authored-by: Franck Nijhof Co-authored-by: Marcel van der Veldt --- homeassistant/components/matter/discovery.py | 2 + homeassistant/components/matter/entity.py | 1 + homeassistant/components/matter/number.py | 140 +++++++++++++++++++ homeassistant/components/matter/strings.json | 14 ++ tests/components/matter/test_number.py | 56 ++++++++ 5 files changed, 213 insertions(+) create mode 100644 homeassistant/components/matter/number.py create mode 100644 tests/components/matter/test_number.py diff --git a/homeassistant/components/matter/discovery.py b/homeassistant/components/matter/discovery.py index d69c2393083..b457be8583c 100644 --- a/homeassistant/components/matter/discovery.py +++ b/homeassistant/components/matter/discovery.py @@ -17,6 +17,7 @@ from .fan import DISCOVERY_SCHEMAS as FAN_SCHEMAS from .light import DISCOVERY_SCHEMAS as LIGHT_SCHEMAS from .lock import DISCOVERY_SCHEMAS as LOCK_SCHEMAS from .models import MatterDiscoverySchema, MatterEntityInfo +from .number import DISCOVERY_SCHEMAS as NUMBER_SCHEMAS from .sensor import DISCOVERY_SCHEMAS as SENSOR_SCHEMAS from .switch import DISCOVERY_SCHEMAS as SWITCH_SCHEMAS @@ -28,6 +29,7 @@ DISCOVERY_SCHEMAS: dict[Platform, list[MatterDiscoverySchema]] = { Platform.FAN: FAN_SCHEMAS, Platform.LIGHT: LIGHT_SCHEMAS, Platform.LOCK: LOCK_SCHEMAS, + Platform.NUMBER: NUMBER_SCHEMAS, Platform.SENSOR: SENSOR_SCHEMAS, Platform.SWITCH: SWITCH_SCHEMAS, } diff --git a/homeassistant/components/matter/entity.py b/homeassistant/components/matter/entity.py index ded1e1a2d39..876693f354f 100644 --- a/homeassistant/components/matter/entity.py +++ b/homeassistant/components/matter/entity.py @@ -34,6 +34,7 @@ class MatterEntityDescription(EntityDescription): # convert the value from the primary attribute to the value used by HA measurement_to_ha: Callable[[Any], Any] | None = None + ha_to_native_value: Callable[[Any], Any] | None = None class MatterEntity(Entity): diff --git a/homeassistant/components/matter/number.py b/homeassistant/components/matter/number.py new file mode 100644 index 00000000000..c9b40ef71a0 --- /dev/null +++ b/homeassistant/components/matter/number.py @@ -0,0 +1,140 @@ +"""Matter Number Inputs.""" + +from __future__ import annotations + +from dataclasses import dataclass + +from chip.clusters import Objects as clusters +from matter_server.common.helpers.util import create_attribute_path_from_attribute + +from homeassistant.components.number import ( + NumberEntity, + NumberEntityDescription, + NumberMode, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import EntityCategory, Platform, UnitOfTime +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .entity import MatterEntity, MatterEntityDescription +from .helpers import get_matter +from .models import MatterDiscoverySchema + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up Matter Number Input from Config Entry.""" + matter = get_matter(hass) + matter.register_platform_handler(Platform.NUMBER, async_add_entities) + + +@dataclass(frozen=True) +class MatterNumberEntityDescription(NumberEntityDescription, MatterEntityDescription): + """Describe Matter Number Input entities.""" + + +class MatterNumber(MatterEntity, NumberEntity): + """Representation of a Matter Attribute as a Number entity.""" + + entity_description: MatterNumberEntityDescription + + async def async_set_native_value(self, value: float) -> None: + """Update the current value.""" + matter_attribute = self._entity_info.primary_attribute + sendvalue = int(value) + if value_convert := self.entity_description.ha_to_native_value: + sendvalue = value_convert(value) + await self.matter_client.write_attribute( + node_id=self._endpoint.node.node_id, + attribute_path=create_attribute_path_from_attribute( + self._endpoint.endpoint_id, + matter_attribute, + ), + value=sendvalue, + ) + + @callback + def _update_from_device(self) -> None: + """Update from device.""" + value = self.get_matter_attribute_value(self._entity_info.primary_attribute) + if value_convert := self.entity_description.measurement_to_ha: + value = value_convert(value) + self._attr_native_value = value + + +# Discovery schema(s) to map Matter Attributes to HA entities +DISCOVERY_SCHEMAS = [ + MatterDiscoverySchema( + platform=Platform.NUMBER, + entity_description=MatterNumberEntityDescription( + key="on_level", + entity_category=EntityCategory.CONFIG, + translation_key="on_level", + native_max_value=255, + native_min_value=0, + mode=NumberMode.BOX, + # use 255 to indicate that the value should revert to the default + measurement_to_ha=lambda x: 255 if x is None else x, + ha_to_native_value=lambda x: None if x == 255 else int(x), + native_step=1, + native_unit_of_measurement=None, + ), + entity_class=MatterNumber, + required_attributes=(clusters.LevelControl.Attributes.OnLevel,), + ), + MatterDiscoverySchema( + platform=Platform.NUMBER, + entity_description=MatterNumberEntityDescription( + key="on_transition_time", + entity_category=EntityCategory.CONFIG, + translation_key="on_transition_time", + native_max_value=65534, + native_min_value=0, + measurement_to_ha=lambda x: None if x is None else x / 10, + ha_to_native_value=lambda x: round(x * 10), + native_step=0.1, + native_unit_of_measurement=UnitOfTime.SECONDS, + mode=NumberMode.BOX, + ), + entity_class=MatterNumber, + required_attributes=(clusters.LevelControl.Attributes.OnTransitionTime,), + ), + MatterDiscoverySchema( + platform=Platform.NUMBER, + entity_description=MatterNumberEntityDescription( + key="off_transition_time", + entity_category=EntityCategory.CONFIG, + translation_key="off_transition_time", + native_max_value=65534, + native_min_value=0, + measurement_to_ha=lambda x: None if x is None else x / 10, + ha_to_native_value=lambda x: round(x * 10), + native_step=0.1, + native_unit_of_measurement=UnitOfTime.SECONDS, + mode=NumberMode.BOX, + ), + entity_class=MatterNumber, + required_attributes=(clusters.LevelControl.Attributes.OffTransitionTime,), + ), + MatterDiscoverySchema( + platform=Platform.NUMBER, + entity_description=MatterNumberEntityDescription( + key="on_off_transition_time", + entity_category=EntityCategory.CONFIG, + translation_key="on_off_transition_time", + native_max_value=65534, + native_min_value=0, + measurement_to_ha=lambda x: None if x is None else x / 10, + ha_to_native_value=lambda x: round(x * 10), + native_step=0.1, + native_unit_of_measurement=UnitOfTime.SECONDS, + mode=NumberMode.BOX, + ), + entity_class=MatterNumber, + required_attributes=(clusters.LevelControl.Attributes.OnOffTransitionTime,), + ), +] diff --git a/homeassistant/components/matter/strings.json b/homeassistant/components/matter/strings.json index a3f26a5865a..190aae5de43 100644 --- a/homeassistant/components/matter/strings.json +++ b/homeassistant/components/matter/strings.json @@ -78,6 +78,20 @@ } } }, + "number": { + "on_level": { + "name": "On level" + }, + "on_transition_time": { + "name": "On transition time" + }, + "off_transition_time": { + "name": "Off transition time" + }, + "on_off_transition_time": { + "name": "On/Off transition time" + } + }, "sensor": { "activated_carbon_filter_condition": { "name": "Activated carbon filter condition" diff --git a/tests/components/matter/test_number.py b/tests/components/matter/test_number.py new file mode 100644 index 00000000000..917f8138c7a --- /dev/null +++ b/tests/components/matter/test_number.py @@ -0,0 +1,56 @@ +"""Test Matter number entities.""" + +from unittest.mock import MagicMock + +from matter_server.client.models.node import MatterNode +import pytest + +from homeassistant.core import HomeAssistant + +from .common import ( + set_node_attribute, + setup_integration_with_node_fixture, + trigger_subscription_callback, +) + + +@pytest.fixture(name="light_node") +async def dimmable_light_node_fixture( + hass: HomeAssistant, matter_client: MagicMock +) -> MatterNode: + """Fixture for a flow sensor node.""" + return await setup_integration_with_node_fixture( + hass, "dimmable-light", matter_client + ) + + +# This tests needs to be adjusted to remove lingering tasks +@pytest.mark.parametrize("expected_lingering_tasks", [True]) +async def test_level_control_config_entities( + hass: HomeAssistant, + matter_client: MagicMock, + light_node: MatterNode, +) -> None: + """Test number entities are created for the LevelControl cluster (config) attributes.""" + state = hass.states.get("number.mock_dimmable_light_on_level") + assert state + assert state.state == "255" + + state = hass.states.get("number.mock_dimmable_light_on_transition_time") + assert state + assert state.state == "0.0" + + state = hass.states.get("number.mock_dimmable_light_off_transition_time") + assert state + assert state.state == "0.0" + + state = hass.states.get("number.mock_dimmable_light_on_off_transition_time") + assert state + assert state.state == "0.0" + + set_node_attribute(light_node, 1, 0x00000008, 0x0011, 20) + await trigger_subscription_callback(hass, matter_client) + + state = hass.states.get("number.mock_dimmable_light_on_level") + assert state + assert state.state == "20" From a10f9a5f6d0a6a3c21aedc462d95f015ada89d16 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 21 Jun 2024 15:56:22 +0200 Subject: [PATCH 0936/1445] Allow opting out of warnings when removing unknown frontend panel (#119824) --- homeassistant/components/frontend/__init__.py | 8 ++++++-- tests/components/frontend/test_init.py | 14 +++++++++++++- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 5f68ebeac18..dac0f51f608 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -323,12 +323,16 @@ def async_register_built_in_panel( @bind_hass @callback -def async_remove_panel(hass: HomeAssistant, frontend_url_path: str) -> None: +def async_remove_panel( + hass: HomeAssistant, frontend_url_path: str, *, warn_if_unknown: bool = True +) -> None: """Remove a built-in panel.""" panel = hass.data.get(DATA_PANELS, {}).pop(frontend_url_path, None) if panel is None: - _LOGGER.warning("Removing unknown panel %s", frontend_url_path) + if warn_if_unknown: + _LOGGER.warning("Removing unknown panel %s", frontend_url_path) + return hass.bus.async_fire(EVENT_PANELS_UPDATED) diff --git a/tests/components/frontend/test_init.py b/tests/components/frontend/test_init.py index a9c24d256e5..83c82abea35 100644 --- a/tests/components/frontend/test_init.py +++ b/tests/components/frontend/test_init.py @@ -495,7 +495,10 @@ async def test_extra_js( async def test_get_panels( - hass: HomeAssistant, hass_ws_client: WebSocketGenerator, mock_http_client + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + mock_http_client, + caplog: pytest.LogCaptureFixture, ) -> None: """Test get_panels command.""" events = async_capture_events(hass, EVENT_PANELS_UPDATED) @@ -533,6 +536,15 @@ async def test_get_panels( assert len(events) == 2 + # Remove again, will warn but not trigger event + async_remove_panel(hass, "map") + assert "Removing unknown panel map" in caplog.text + caplog.clear() + + # Remove again, without warning + async_remove_panel(hass, "map", warn_if_unknown=False) + assert "Removing unknown panel map" not in caplog.text + async def test_get_panels_non_admin( hass: HomeAssistant, ws_client, hass_admin_user: MockUser From 7fa74fcb07017ed977d65883c1f2c6b0f2e5534a Mon Sep 17 00:00:00 2001 From: "Mr. Bubbles" Date: Fri, 21 Jun 2024 15:57:36 +0200 Subject: [PATCH 0937/1445] Refactor sensor platform of Pyload integration (#119716) --- homeassistant/components/pyload/sensor.py | 82 ++++++++++++------- tests/components/pyload/conftest.py | 1 + .../pyload/snapshots/test_sensor.ambr | 2 +- tests/components/pyload/test_sensor.py | 13 ++- 4 files changed, 66 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/pyload/sensor.py b/homeassistant/components/pyload/sensor.py index 730f0202d5b..a005f848c37 100644 --- a/homeassistant/components/pyload/sensor.py +++ b/homeassistant/components/pyload/sensor.py @@ -3,12 +3,14 @@ from __future__ import annotations from datetime import timedelta +from enum import StrEnum import logging +from time import monotonic +from typing import Any from aiohttp import CookieJar from pyloadapi.api import PyLoadAPI from pyloadapi.exceptions import CannotConnect, InvalidAuth, ParserError -from pyloadapi.types import StatusServerResponse import voluptuous as vol from homeassistant.components.sensor import ( @@ -32,29 +34,37 @@ from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.aiohttp_client import async_create_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType from .const import DEFAULT_HOST, DEFAULT_NAME, DEFAULT_PORT _LOGGER = logging.getLogger(__name__) - SCAN_INTERVAL = timedelta(seconds=15) -SENSOR_TYPES = { - "speed": SensorEntityDescription( - key="speed", + +class PyLoadSensorEntity(StrEnum): + """pyLoad Sensor Entities.""" + + SPEED = "speed" + + +SENSOR_DESCRIPTIONS: tuple[SensorEntityDescription, ...] = ( + SensorEntityDescription( + key=PyLoadSensorEntity.SPEED, name="Speed", - native_unit_of_measurement=UnitOfDataRate.MEGABYTES_PER_SECOND, device_class=SensorDeviceClass.DATA_RATE, - ) -} + native_unit_of_measurement=UnitOfDataRate.BYTES_PER_SECOND, + suggested_unit_of_measurement=UnitOfDataRate.MEGABYTES_PER_SECOND, + suggested_display_precision=1, + ), +) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, vol.Optional(CONF_MONITORED_VARIABLES, default=["speed"]): vol.All( - cv.ensure_list, [vol.In(SENSOR_TYPES)] + cv.ensure_list, [vol.In(PyLoadSensorEntity)] ), vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_PASSWORD): cv.string, @@ -78,7 +88,6 @@ async def async_setup_platform( name = config[CONF_NAME] username = config.get(CONF_USERNAME) password = config.get(CONF_PASSWORD) - monitored_types = config[CONF_MONITORED_VARIABLES] url = f"{protocol}://{host}:{port}/" session = async_create_clientsession( @@ -100,33 +109,36 @@ async def async_setup_platform( f"Authentication failed for {config[CONF_USERNAME]}, check your login credentials" ) from e - devices = [] - for ng_type in monitored_types: - new_sensor = PyLoadSensor( - api=pyloadapi, sensor_type=SENSOR_TYPES[ng_type], client_name=name - ) - devices.append(new_sensor) - - async_add_entities(devices, True) + async_add_entities( + ( + PyLoadSensor( + api=pyloadapi, entity_description=description, client_name=name + ) + for description in SENSOR_DESCRIPTIONS + ), + True, + ) class PyLoadSensor(SensorEntity): """Representation of a pyLoad sensor.""" def __init__( - self, api: PyLoadAPI, sensor_type: SensorEntityDescription, client_name + self, api: PyLoadAPI, entity_description: SensorEntityDescription, client_name ) -> None: """Initialize a new pyLoad sensor.""" - self._attr_name = f"{client_name} {sensor_type.name}" - self.type = sensor_type.key + self._attr_name = f"{client_name} {entity_description.name}" + self.type = entity_description.key self.api = api - self.entity_description = sensor_type - self.data: StatusServerResponse + self.entity_description = entity_description + self._attr_available = False + self.data: dict[str, Any] = {} async def async_update(self) -> None: """Update state of sensor.""" + start = monotonic() try: - self.data = await self.api.get_status() + status = await self.api.get_status() except InvalidAuth: _LOGGER.info("Authentication failed, trying to reauthenticate") try: @@ -143,15 +155,27 @@ class PyLoadSensor(SensorEntity): "but re-authentication was successful" ) return + finally: + self._attr_available = False + except CannotConnect: _LOGGER.debug("Unable to connect and retrieve data from pyLoad API") + self._attr_available = False return except ParserError: _LOGGER.error("Unable to parse data from pyLoad API") + self._attr_available = False return + else: + self.data = status.to_dict() + _LOGGER.debug( + "Finished fetching pyload data in %.3f seconds", + monotonic() - start, + ) - value = getattr(self.data, self.type) + self._attr_available = True - if "speed" in self.type and value > 0: - # Convert download rate from Bytes/s to MBytes/s - self._attr_native_value = round(value / 2**20, 2) + @property + def native_value(self) -> StateType: + """Return the state of the sensor.""" + return self.data.get(self.entity_description.key) diff --git a/tests/components/pyload/conftest.py b/tests/components/pyload/conftest.py index 31f251c6e85..67694bcb4b9 100644 --- a/tests/components/pyload/conftest.py +++ b/tests/components/pyload/conftest.py @@ -71,4 +71,5 @@ def mock_pyloadapi() -> Generator[AsyncMock, None, None]: "captcha": False, } ) + client.free_space.return_value = 99999999999 yield client diff --git a/tests/components/pyload/snapshots/test_sensor.ambr b/tests/components/pyload/snapshots/test_sensor.ambr index 384a59b78b2..226221240d2 100644 --- a/tests/components/pyload/snapshots/test_sensor.ambr +++ b/tests/components/pyload/snapshots/test_sensor.ambr @@ -11,6 +11,6 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '5.16', + 'state': '5.405963', }) # --- diff --git a/tests/components/pyload/test_sensor.py b/tests/components/pyload/test_sensor.py index 6fd85ba0796..e2b392b06f9 100644 --- a/tests/components/pyload/test_sensor.py +++ b/tests/components/pyload/test_sensor.py @@ -9,12 +9,15 @@ from syrupy.assertion import SnapshotAssertion from homeassistant.components.pyload.sensor import SCAN_INTERVAL from homeassistant.components.sensor import DOMAIN +from homeassistant.const import STATE_UNAVAILABLE from homeassistant.core import HomeAssistant from homeassistant.helpers.typing import ConfigType from homeassistant.setup import async_setup_component from tests.common import async_fire_time_changed +SENSORS = ["sensor.pyload_speed"] + @pytest.mark.usefixtures("mock_pyloadapi") async def test_setup( @@ -27,8 +30,9 @@ async def test_setup( assert await async_setup_component(hass, DOMAIN, pyload_config) await hass.async_block_till_done() - result = hass.states.get("sensor.pyload_speed") - assert result == snapshot + for sensor in SENSORS: + result = hass.states.get(sensor) + assert result == snapshot @pytest.mark.parametrize( @@ -76,6 +80,8 @@ async def test_sensor_update_exceptions( exception: Exception, expected_exception: str, caplog: pytest.LogCaptureFixture, + snapshot: SnapshotAssertion, + freezer: FrozenDateTimeFactory, ) -> None: """Test exceptions during update of pyLoad sensor.""" @@ -87,6 +93,9 @@ async def test_sensor_update_exceptions( assert len(hass.states.async_all(DOMAIN)) == 1 assert expected_exception in caplog.text + for sensor in SENSORS: + assert hass.states.get(sensor).state == STATE_UNAVAILABLE + async def test_sensor_invalid_auth( hass: HomeAssistant, From 289a54d632005f7740ef9db446c17a94041f130a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Fri, 21 Jun 2024 15:59:57 +0200 Subject: [PATCH 0938/1445] Update aioairzone-cloud to v0.5.3 (#120100) --- homeassistant/components/airzone_cloud/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../airzone_cloud/snapshots/test_diagnostics.ambr | 4 ++++ tests/components/airzone_cloud/util.py | 6 ++++++ 5 files changed, 13 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/airzone_cloud/manifest.json b/homeassistant/components/airzone_cloud/manifest.json index ca024d0e1a3..555514ecf2a 100644 --- a/homeassistant/components/airzone_cloud/manifest.json +++ b/homeassistant/components/airzone_cloud/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/airzone_cloud", "iot_class": "cloud_push", "loggers": ["aioairzone_cloud"], - "requirements": ["aioairzone-cloud==0.5.2"] + "requirements": ["aioairzone-cloud==0.5.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index 6e45a44c23b..d9dd5bbe61b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -182,7 +182,7 @@ aio-georss-gdacs==0.9 aioairq==0.3.2 # homeassistant.components.airzone_cloud -aioairzone-cloud==0.5.2 +aioairzone-cloud==0.5.3 # homeassistant.components.airzone aioairzone==0.7.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index adaa08ed59b..33ff276c8ad 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -161,7 +161,7 @@ aio-georss-gdacs==0.9 aioairq==0.3.2 # homeassistant.components.airzone_cloud -aioairzone-cloud==0.5.2 +aioairzone-cloud==0.5.3 # homeassistant.components.airzone aioairzone==0.7.7 diff --git a/tests/components/airzone_cloud/snapshots/test_diagnostics.ambr b/tests/components/airzone_cloud/snapshots/test_diagnostics.ambr index 3309c175543..31065d68a47 100644 --- a/tests/components/airzone_cloud/snapshots/test_diagnostics.ambr +++ b/tests/components/airzone_cloud/snapshots/test_diagnostics.ambr @@ -438,6 +438,7 @@ 'zone1': dict({ 'action': 1, 'active': True, + 'air-demand': True, 'aq-active': False, 'aq-index': 1, 'aq-mode-conf': 'auto', @@ -453,6 +454,7 @@ 'aq-status': 'good', 'available': True, 'double-set-point': False, + 'floor-demand': False, 'humidity': 30, 'id': 'zone1', 'installation': 'installation1', @@ -499,6 +501,7 @@ 'zone2': dict({ 'action': 6, 'active': False, + 'air-demand': False, 'aq-active': False, 'aq-index': 1, 'aq-mode-conf': 'auto', @@ -514,6 +517,7 @@ 'aq-status': 'good', 'available': True, 'double-set-point': False, + 'floor-demand': False, 'humidity': 24, 'id': 'zone2', 'installation': 'installation1', diff --git a/tests/components/airzone_cloud/util.py b/tests/components/airzone_cloud/util.py index dfd59199a8a..6e7dad707f1 100644 --- a/tests/components/airzone_cloud/util.py +++ b/tests/components/airzone_cloud/util.py @@ -6,6 +6,7 @@ from unittest.mock import patch from aioairzone_cloud.common import OperationMode from aioairzone_cloud.const import ( API_ACTIVE, + API_AIR_ACTIVE, API_AQ_ACTIVE, API_AQ_MODE_CONF, API_AQ_MODE_VALUES, @@ -42,6 +43,7 @@ from aioairzone_cloud.const import ( API_OLD_ID, API_POWER, API_POWERFUL_MODE, + API_RAD_ACTIVE, API_RANGE_MAX_AIR, API_RANGE_MIN_AIR, API_RANGE_SP_MAX_ACS, @@ -353,6 +355,7 @@ def mock_get_device_status(device: Device) -> dict[str, Any]: if device.get_id() == "zone1": return { API_ACTIVE: True, + API_AIR_ACTIVE: True, API_AQ_ACTIVE: False, API_AQ_MODE_CONF: "auto", API_AQ_MODE_VALUES: ["off", "on", "auto"], @@ -370,6 +373,7 @@ def mock_get_device_status(device: Device) -> dict[str, Any]: OperationMode.VENTILATION.value, OperationMode.DRY.value, ], + API_RAD_ACTIVE: False, API_RANGE_MAX_AIR: {API_CELSIUS: 30, API_FAH: 86}, API_RANGE_SP_MAX_COOL_AIR: {API_FAH: 86, API_CELSIUS: 30}, API_RANGE_SP_MAX_DRY_AIR: {API_FAH: 86, API_CELSIUS: 30}, @@ -398,6 +402,7 @@ def mock_get_device_status(device: Device) -> dict[str, Any]: if device.get_id() == "zone2": return { API_ACTIVE: False, + API_AIR_ACTIVE: False, API_AQ_ACTIVE: False, API_AQ_MODE_CONF: "auto", API_AQ_MODE_VALUES: ["off", "on", "auto"], @@ -410,6 +415,7 @@ def mock_get_device_status(device: Device) -> dict[str, Any]: API_HUMIDITY: 24, API_MODE: OperationMode.COOLING.value, API_MODE_AVAIL: [], + API_RAD_ACTIVE: False, API_RANGE_MAX_AIR: {API_CELSIUS: 30, API_FAH: 86}, API_RANGE_SP_MAX_COOL_AIR: {API_FAH: 86, API_CELSIUS: 30}, API_RANGE_SP_MAX_DRY_AIR: {API_FAH: 86, API_CELSIUS: 30}, From 7f5a71d281d9e8d9aa9d785307207e8656ca10e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ethem=20Cem=20=C3=96zkan?= Date: Fri, 21 Jun 2024 16:01:57 +0200 Subject: [PATCH 0939/1445] Tado water heater code quality changes (#119811) Co-authored-by: Martin Hjelmare --- homeassistant/components/tado/repairs.py | 13 ++++++------- homeassistant/components/tado/water_heater.py | 4 ++-- tests/components/tado/test_repairs.py | 8 ++++---- 3 files changed, 12 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/tado/repairs.py b/homeassistant/components/tado/repairs.py index 5ffc3c76bf7..90e20c615f2 100644 --- a/homeassistant/components/tado/repairs.py +++ b/homeassistant/components/tado/repairs.py @@ -13,20 +13,19 @@ from .const import ( def manage_water_heater_fallback_issue( hass: HomeAssistant, - water_heater_entities: list, + water_heater_names: list[str], integration_overlay_fallback: str | None, ) -> None: """Notify users about water heater respecting fallback setting.""" - if ( - integration_overlay_fallback - in [CONST_OVERLAY_TADO_DEFAULT, CONST_OVERLAY_MANUAL] - and len(water_heater_entities) > 0 + if integration_overlay_fallback in ( + CONST_OVERLAY_TADO_DEFAULT, + CONST_OVERLAY_MANUAL, ): - for water_heater_entity in water_heater_entities: + for water_heater_name in water_heater_names: ir.async_create_issue( hass=hass, domain=DOMAIN, - issue_id=f"{WATER_HEATER_FALLBACK_REPAIR}_{water_heater_entity.zone_name}", + issue_id=f"{WATER_HEATER_FALLBACK_REPAIR}_{water_heater_name}", is_fixable=False, is_persistent=False, severity=ir.IssueSeverity.WARNING, diff --git a/homeassistant/components/tado/water_heater.py b/homeassistant/components/tado/water_heater.py index a31b70a8f9a..1b3b811d231 100644 --- a/homeassistant/components/tado/water_heater.py +++ b/homeassistant/components/tado/water_heater.py @@ -83,12 +83,12 @@ async def async_setup_entry( manage_water_heater_fallback_issue( hass=hass, - water_heater_entities=entities, + water_heater_names=[e.zone_name for e in entities], integration_overlay_fallback=tado.fallback, ) -def _generate_entities(tado: TadoConnector) -> list[WaterHeaterEntity]: +def _generate_entities(tado: TadoConnector) -> list: """Create all water heater entities.""" entities = [] diff --git a/tests/components/tado/test_repairs.py b/tests/components/tado/test_repairs.py index 2e055884272..9b7a010e359 100644 --- a/tests/components/tado/test_repairs.py +++ b/tests/components/tado/test_repairs.py @@ -29,9 +29,9 @@ async def test_manage_water_heater_fallback_issue_not_created( """Test water heater fallback issue is not needed.""" zone_name = "Hot Water" expected_issue_id = f"{WATER_HEATER_FALLBACK_REPAIR}_{zone_name}" - water_heater_entities = [MockWaterHeater(zone_name)] + water_heater_names = [zone_name] manage_water_heater_fallback_issue( - water_heater_entities=water_heater_entities, + water_heater_names=water_heater_names, integration_overlay_fallback=CONST_OVERLAY_TADO_MODE, hass=hass, ) @@ -52,9 +52,9 @@ async def test_manage_water_heater_fallback_issue_created( """Test water heater fallback issue created cases.""" zone_name = "Hot Water" expected_issue_id = f"{WATER_HEATER_FALLBACK_REPAIR}_{zone_name}" - water_heater_entities = [MockWaterHeater(zone_name)] + water_heater_names = [zone_name] manage_water_heater_fallback_issue( - water_heater_entities=water_heater_entities, + water_heater_names=water_heater_names, integration_overlay_fallback=integration_overlay_fallback, hass=hass, ) From db826c97274d27891eb9d37376b664bfce957b45 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Fri, 21 Jun 2024 16:10:57 +0200 Subject: [PATCH 0940/1445] Bum uv to 0.2.13 (#120101) --- Dockerfile | 2 +- requirements_test.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index be4bb899a28..925f6370624 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,7 +12,7 @@ ENV \ ARG QEMU_CPU # Install uv -RUN pip3 install uv==0.1.43 +RUN pip3 install uv==0.2.13 WORKDIR /usr/src diff --git a/requirements_test.txt b/requirements_test.txt index 9001213f630..fce669c4929 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -50,4 +50,4 @@ types-pytz==2024.1.0.20240417 types-PyYAML==6.0.12.20240311 types-requests==2.31.0.3 types-xmltodict==0.13.0.3 -uv==0.1.43 +uv==0.2.13 From b931c3ffcfaa5fade5bf5d8fbff2953b454d75e2 Mon Sep 17 00:00:00 2001 From: tronikos Date: Fri, 21 Jun 2024 07:14:55 -0700 Subject: [PATCH 0941/1445] Include required name in reauth_confirm of Opower (#119627) --- homeassistant/components/opower/config_flow.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/opower/config_flow.py b/homeassistant/components/opower/config_flow.py index 858d14dd832..bbd9315eaa3 100644 --- a/homeassistant/components/opower/config_flow.py +++ b/homeassistant/components/opower/config_flow.py @@ -17,7 +17,7 @@ from opower import ( import voluptuous as vol from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import CONF_NAME, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.aiohttp_client import async_create_clientsession @@ -161,4 +161,5 @@ class OpowerConfigFlow(ConfigFlow, domain=DOMAIN): step_id="reauth_confirm", data_schema=vol.Schema(schema), errors=errors, + description_placeholders={CONF_NAME: self.reauth_entry.title}, ) From 97a025ccc128dba0b33b09348c434780e70d7a3b Mon Sep 17 00:00:00 2001 From: dontinelli <73341522+dontinelli@users.noreply.github.com> Date: Fri, 21 Jun 2024 16:16:09 +0200 Subject: [PATCH 0942/1445] Add sensor for self-consumption in solarlog (#119885) --- homeassistant/components/solarlog/sensor.py | 7 +++++++ homeassistant/components/solarlog/strings.json | 3 +++ 2 files changed, 10 insertions(+) diff --git a/homeassistant/components/solarlog/sensor.py b/homeassistant/components/solarlog/sensor.py index 0b5d56f1a9e..a0d6d4bc540 100644 --- a/homeassistant/components/solarlog/sensor.py +++ b/homeassistant/components/solarlog/sensor.py @@ -146,6 +146,13 @@ SENSOR_TYPES: tuple[SolarLogSensorEntityDescription, ...] = ( state_class=SensorStateClass.TOTAL, value=lambda value: round(value / 1000, 3), ), + SolarLogSensorEntityDescription( + key="self_consumption_year", + translation_key="self_consumption_year", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + ), SolarLogSensorEntityDescription( key="total_power", translation_key="total_power", diff --git a/homeassistant/components/solarlog/strings.json b/homeassistant/components/solarlog/strings.json index caa14ac01a6..f5f5e064294 100644 --- a/homeassistant/components/solarlog/strings.json +++ b/homeassistant/components/solarlog/strings.json @@ -79,6 +79,9 @@ "consumption_total": { "name": "Consumption total" }, + "self_consumption_year": { + "name": "Self-consumption year" + }, "total_power": { "name": "Installed peak power" }, From f353b3fa5410842e56b69b096c204d102e6c218f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Fri, 21 Jun 2024 16:22:05 +0200 Subject: [PATCH 0943/1445] Add Airzone Cloud air/floor demand binary sensors (#120103) --- .../components/airzone_cloud/binary_sensor.py | 12 ++++++++++++ homeassistant/components/airzone_cloud/strings.json | 6 ++++++ tests/components/airzone_cloud/test_binary_sensor.py | 12 ++++++++++++ 3 files changed, 30 insertions(+) diff --git a/homeassistant/components/airzone_cloud/binary_sensor.py b/homeassistant/components/airzone_cloud/binary_sensor.py index f235d9b06d0..3013a2eeadc 100644 --- a/homeassistant/components/airzone_cloud/binary_sensor.py +++ b/homeassistant/components/airzone_cloud/binary_sensor.py @@ -8,8 +8,10 @@ from typing import Any, Final from aioairzone_cloud.const import ( AZD_ACTIVE, AZD_AIDOOS, + AZD_AIR_DEMAND, AZD_AQ_ACTIVE, AZD_ERRORS, + AZD_FLOOR_DEMAND, AZD_PROBLEMS, AZD_SYSTEMS, AZD_WARNINGS, @@ -77,10 +79,20 @@ ZONE_BINARY_SENSOR_TYPES: Final[tuple[AirzoneBinarySensorEntityDescription, ...] device_class=BinarySensorDeviceClass.RUNNING, key=AZD_ACTIVE, ), + AirzoneBinarySensorEntityDescription( + device_class=BinarySensorDeviceClass.RUNNING, + key=AZD_AIR_DEMAND, + translation_key="air_demand", + ), AirzoneBinarySensorEntityDescription( key=AZD_AQ_ACTIVE, translation_key="air_quality_active", ), + AirzoneBinarySensorEntityDescription( + device_class=BinarySensorDeviceClass.RUNNING, + key=AZD_FLOOR_DEMAND, + translation_key="floor_demand", + ), AirzoneBinarySensorEntityDescription( attributes={ "warnings": AZD_WARNINGS, diff --git a/homeassistant/components/airzone_cloud/strings.json b/homeassistant/components/airzone_cloud/strings.json index fe9455aa69e..daeb360719b 100644 --- a/homeassistant/components/airzone_cloud/strings.json +++ b/homeassistant/components/airzone_cloud/strings.json @@ -18,8 +18,14 @@ }, "entity": { "binary_sensor": { + "air_demand": { + "name": "Air demand" + }, "air_quality_active": { "name": "Air Quality active" + }, + "floor_demand": { + "name": "Floor demand" } }, "select": { diff --git a/tests/components/airzone_cloud/test_binary_sensor.py b/tests/components/airzone_cloud/test_binary_sensor.py index b81631728b4..8e065821057 100644 --- a/tests/components/airzone_cloud/test_binary_sensor.py +++ b/tests/components/airzone_cloud/test_binary_sensor.py @@ -41,9 +41,15 @@ async def test_airzone_create_binary_sensors(hass: HomeAssistant) -> None: assert state.attributes.get("warnings") is None # Zones + state = hass.states.get("binary_sensor.dormitorio_air_demand") + assert state.state == STATE_OFF + state = hass.states.get("binary_sensor.dormitorio_air_quality_active") assert state.state == STATE_OFF + state = hass.states.get("binary_sensor.dormitorio_floor_demand") + assert state.state == STATE_OFF + state = hass.states.get("binary_sensor.dormitorio_problem") assert state.state == STATE_OFF assert state.attributes.get("warnings") is None @@ -51,9 +57,15 @@ async def test_airzone_create_binary_sensors(hass: HomeAssistant) -> None: state = hass.states.get("binary_sensor.dormitorio_running") assert state.state == STATE_OFF + state = hass.states.get("binary_sensor.salon_air_demand") + assert state.state == STATE_ON + state = hass.states.get("binary_sensor.salon_air_quality_active") assert state.state == STATE_OFF + state = hass.states.get("binary_sensor.salon_floor_demand") + assert state.state == STATE_OFF + state = hass.states.get("binary_sensor.salon_problem") assert state.state == STATE_OFF assert state.attributes.get("warnings") is None From 180c244a7859d5c8eab55692a8d0723cd637705b Mon Sep 17 00:00:00 2001 From: Thomas Kistler Date: Fri, 21 Jun 2024 07:36:53 -0700 Subject: [PATCH 0944/1445] Change Ambient Network timestamp updates (#116941) --- .../components/ambient_network/coordinator.py | 22 +- .../components/ambient_network/sensor.py | 7 +- .../fixtures/device_details_response_b.json | 3 + .../fixtures/device_details_response_c.json | 2 +- .../fixtures/device_details_response_d.json | 30 + .../snapshots/test_sensor.ambr | 1886 +++++++++++++++++ .../components/ambient_network/test_sensor.py | 57 +- 7 files changed, 1954 insertions(+), 53 deletions(-) create mode 100644 tests/components/ambient_network/fixtures/device_details_response_d.json diff --git a/homeassistant/components/ambient_network/coordinator.py b/homeassistant/components/ambient_network/coordinator.py index f26ddd47b24..2f51c3bc0cb 100644 --- a/homeassistant/components/ambient_network/coordinator.py +++ b/homeassistant/components/ambient_network/coordinator.py @@ -12,6 +12,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_MAC from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed +from homeassistant.util import dt as dt_util from .const import API_LAST_DATA, DOMAIN, LOGGER from .helper import get_station_name @@ -24,6 +25,7 @@ class AmbientNetworkDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]) config_entry: ConfigEntry station_name: str + last_measured: datetime | None = None def __init__(self, hass: HomeAssistant, api: OpenAPI) -> None: """Initialize the coordinator.""" @@ -47,19 +49,13 @@ class AmbientNetworkDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]) f"Station '{self.config_entry.title}' did not report any data" ) - # Eliminate data if the station hasn't been updated for a while. - if (created_at := last_data.get("created_at")) is None: - raise UpdateFailed( - f"Station '{self.config_entry.title}' did not report a time stamp" - ) - - # Eliminate data that has been generated more than an hour ago. The station is - # probably offline. - if int(created_at / 1000) < int( - (datetime.now() - timedelta(hours=1)).timestamp() - ): - raise UpdateFailed( - f"Station '{self.config_entry.title}' reported stale data" + # Some stations do not report a "created_at" or "dateutc". + # See https://github.com/home-assistant/core/issues/116917 + if (ts := last_data.get("created_at")) is not None or ( + ts := last_data.get("dateutc") + ) is not None: + self.last_measured = datetime.fromtimestamp( + ts / 1000, tz=dt_util.DEFAULT_TIME_ZONE ) return cast(dict[str, Any], last_data) diff --git a/homeassistant/components/ambient_network/sensor.py b/homeassistant/components/ambient_network/sensor.py index 028a8f69264..132fc7dbd0d 100644 --- a/homeassistant/components/ambient_network/sensor.py +++ b/homeassistant/components/ambient_network/sensor.py @@ -299,12 +299,10 @@ class AmbientNetworkSensor(AmbientNetworkEntity, SensorEntity): mac_address: str, ) -> None: """Initialize a sensor object.""" - super().__init__(coordinator, description, mac_address) def _update_attrs(self) -> None: """Update sensor attributes.""" - value = self.coordinator.data.get(self.entity_description.key) # Treatments for special units. @@ -315,3 +313,8 @@ class AmbientNetworkSensor(AmbientNetworkEntity, SensorEntity): self._attr_available = value is not None self._attr_native_value = value + + if self.coordinator.last_measured is not None: + self._attr_extra_state_attributes = { + "last_measured": self.coordinator.last_measured + } diff --git a/tests/components/ambient_network/fixtures/device_details_response_b.json b/tests/components/ambient_network/fixtures/device_details_response_b.json index 8249f6f0c30..75fbfe0b31c 100644 --- a/tests/components/ambient_network/fixtures/device_details_response_b.json +++ b/tests/components/ambient_network/fixtures/device_details_response_b.json @@ -3,5 +3,8 @@ "macAddress": "BB:BB:BB:BB:BB:BB", "info": { "name": "Station B" + }, + "lastData": { + "tempf": 82.9 } } diff --git a/tests/components/ambient_network/fixtures/device_details_response_c.json b/tests/components/ambient_network/fixtures/device_details_response_c.json index 8e171f35374..cbd97e0a811 100644 --- a/tests/components/ambient_network/fixtures/device_details_response_c.json +++ b/tests/components/ambient_network/fixtures/device_details_response_c.json @@ -3,7 +3,7 @@ "macAddress": "CC:CC:CC:CC:CC:CC", "lastData": { "stationtype": "AMBWeatherPro_V5.0.6", - "dateutc": 1699474320000, + "dateutc": 1717687683000, "tempf": 82.9, "dewPoint": 82.0, "feelsLike": 85.0, diff --git a/tests/components/ambient_network/fixtures/device_details_response_d.json b/tests/components/ambient_network/fixtures/device_details_response_d.json new file mode 100644 index 00000000000..60b4918b8c2 --- /dev/null +++ b/tests/components/ambient_network/fixtures/device_details_response_d.json @@ -0,0 +1,30 @@ +{ + "_id": "dddddddddddddddddddddddddddddddd", + "macAddress": "DD:DD:DD:DD:DD:DD", + "lastData": { + "stationtype": "AMBWeatherPro_V5.0.6", + "tempf": 82.9, + "dewPoint": 82.0, + "feelsLike": 85.0, + "humidity": 60, + "windspeedmph": 8.72, + "windgustmph": 9.17, + "maxdailygust": 22.82, + "winddir": 11, + "uv": 0, + "solarradiation": 37.64, + "hourlyrainin": 0, + "dailyrainin": 0, + "weeklyrainin": 0, + "monthlyrainin": 0, + "totalrainin": 26.402, + "baromrelin": 29.586, + "baromabsin": 28.869, + "batt_co2": 1, + "type": "weather-data", + "tz": "America/Chicago" + }, + "info": { + "name": "Station D" + } +} diff --git a/tests/components/ambient_network/snapshots/test_sensor.ambr b/tests/components/ambient_network/snapshots/test_sensor.ambr index fadb15ad015..fd48184ca0b 100644 --- a/tests/components/ambient_network/snapshots/test_sensor.ambr +++ b/tests/components/ambient_network/snapshots/test_sensor.ambr @@ -46,6 +46,7 @@ 'attribution': 'Data provided by ambientnetwork.net', 'device_class': 'pressure', 'friendly_name': 'Station A Absolute pressure', + 'last_measured': HAFakeDatetime(2023, 11, 8, 12, 12, 0, 914000, tzinfo=zoneinfo.ZoneInfo(key='US/Pacific')), 'state_class': , 'unit_of_measurement': , }), @@ -104,6 +105,7 @@ 'attribution': 'Data provided by ambientnetwork.net', 'device_class': 'precipitation', 'friendly_name': 'Station A Daily rain', + 'last_measured': HAFakeDatetime(2023, 11, 8, 12, 12, 0, 914000, tzinfo=zoneinfo.ZoneInfo(key='US/Pacific')), 'state_class': , 'unit_of_measurement': , }), @@ -159,6 +161,7 @@ 'attribution': 'Data provided by ambientnetwork.net', 'device_class': 'temperature', 'friendly_name': 'Station A Dew point', + 'last_measured': HAFakeDatetime(2023, 11, 8, 12, 12, 0, 914000, tzinfo=zoneinfo.ZoneInfo(key='US/Pacific')), 'state_class': , 'unit_of_measurement': , }), @@ -214,6 +217,7 @@ 'attribution': 'Data provided by ambientnetwork.net', 'device_class': 'temperature', 'friendly_name': 'Station A Feels like', + 'last_measured': HAFakeDatetime(2023, 11, 8, 12, 12, 0, 914000, tzinfo=zoneinfo.ZoneInfo(key='US/Pacific')), 'state_class': , 'unit_of_measurement': , }), @@ -272,6 +276,7 @@ 'attribution': 'Data provided by ambientnetwork.net', 'device_class': 'precipitation_intensity', 'friendly_name': 'Station A Hourly rain', + 'last_measured': HAFakeDatetime(2023, 11, 8, 12, 12, 0, 914000, tzinfo=zoneinfo.ZoneInfo(key='US/Pacific')), 'state_class': , 'unit_of_measurement': , }), @@ -327,6 +332,7 @@ 'attribution': 'Data provided by ambientnetwork.net', 'device_class': 'humidity', 'friendly_name': 'Station A Humidity', + 'last_measured': HAFakeDatetime(2023, 11, 8, 12, 12, 0, 914000, tzinfo=zoneinfo.ZoneInfo(key='US/Pacific')), 'state_class': , 'unit_of_measurement': '%', }), @@ -382,6 +388,7 @@ 'attribution': 'Data provided by ambientnetwork.net', 'device_class': 'irradiance', 'friendly_name': 'Station A Irradiance', + 'last_measured': HAFakeDatetime(2023, 11, 8, 12, 12, 0, 914000, tzinfo=zoneinfo.ZoneInfo(key='US/Pacific')), 'state_class': , 'unit_of_measurement': , }), @@ -432,6 +439,7 @@ 'attribution': 'Data provided by ambientnetwork.net', 'device_class': 'timestamp', 'friendly_name': 'Station A Last rain', + 'last_measured': HAFakeDatetime(2023, 11, 8, 12, 12, 0, 914000, tzinfo=zoneinfo.ZoneInfo(key='US/Pacific')), }), 'context': , 'entity_id': 'sensor.station_a_last_rain', @@ -488,6 +496,7 @@ 'attribution': 'Data provided by ambientnetwork.net', 'device_class': 'wind_speed', 'friendly_name': 'Station A Max daily gust', + 'last_measured': HAFakeDatetime(2023, 11, 8, 12, 12, 0, 914000, tzinfo=zoneinfo.ZoneInfo(key='US/Pacific')), 'state_class': , 'unit_of_measurement': , }), @@ -546,6 +555,7 @@ 'attribution': 'Data provided by ambientnetwork.net', 'device_class': 'precipitation', 'friendly_name': 'Station A Monthly rain', + 'last_measured': HAFakeDatetime(2023, 11, 8, 12, 12, 0, 914000, tzinfo=zoneinfo.ZoneInfo(key='US/Pacific')), 'state_class': , 'unit_of_measurement': , }), @@ -604,6 +614,7 @@ 'attribution': 'Data provided by ambientnetwork.net', 'device_class': 'pressure', 'friendly_name': 'Station A Relative pressure', + 'last_measured': HAFakeDatetime(2023, 11, 8, 12, 12, 0, 914000, tzinfo=zoneinfo.ZoneInfo(key='US/Pacific')), 'state_class': , 'unit_of_measurement': , }), @@ -659,6 +670,7 @@ 'attribution': 'Data provided by ambientnetwork.net', 'device_class': 'temperature', 'friendly_name': 'Station A Temperature', + 'last_measured': HAFakeDatetime(2023, 11, 8, 12, 12, 0, 914000, tzinfo=zoneinfo.ZoneInfo(key='US/Pacific')), 'state_class': , 'unit_of_measurement': , }), @@ -713,6 +725,7 @@ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by ambientnetwork.net', 'friendly_name': 'Station A UV index', + 'last_measured': HAFakeDatetime(2023, 11, 8, 12, 12, 0, 914000, tzinfo=zoneinfo.ZoneInfo(key='US/Pacific')), 'state_class': , 'unit_of_measurement': 'index', }), @@ -771,6 +784,7 @@ 'attribution': 'Data provided by ambientnetwork.net', 'device_class': 'precipitation', 'friendly_name': 'Station A Weekly rain', + 'last_measured': HAFakeDatetime(2023, 11, 8, 12, 12, 0, 914000, tzinfo=zoneinfo.ZoneInfo(key='US/Pacific')), 'state_class': , 'unit_of_measurement': , }), @@ -823,6 +837,7 @@ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by ambientnetwork.net', 'friendly_name': 'Station A Wind direction', + 'last_measured': HAFakeDatetime(2023, 11, 8, 12, 12, 0, 914000, tzinfo=zoneinfo.ZoneInfo(key='US/Pacific')), 'unit_of_measurement': '°', }), 'context': , @@ -880,6 +895,7 @@ 'attribution': 'Data provided by ambientnetwork.net', 'device_class': 'wind_speed', 'friendly_name': 'Station A Wind gust', + 'last_measured': HAFakeDatetime(2023, 11, 8, 12, 12, 0, 914000, tzinfo=zoneinfo.ZoneInfo(key='US/Pacific')), 'state_class': , 'unit_of_measurement': , }), @@ -938,6 +954,7 @@ 'attribution': 'Data provided by ambientnetwork.net', 'device_class': 'wind_speed', 'friendly_name': 'Station A Wind speed', + 'last_measured': HAFakeDatetime(2023, 11, 8, 12, 12, 0, 914000, tzinfo=zoneinfo.ZoneInfo(key='US/Pacific')), 'state_class': , 'unit_of_measurement': , }), @@ -949,3 +966,1872 @@ 'state': '14.03347968', }) # --- +# name: test_sensors[CC:CC:CC:CC:CC:CC][sensor.station_c_absolute_pressure-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.station_c_absolute_pressure', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Absolute pressure', + 'platform': 'ambient_network', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'absolute_pressure', + 'unique_id': 'CC:CC:CC:CC:CC:CC_baromabsin', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[CC:CC:CC:CC:CC:CC][sensor.station_c_absolute_pressure-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by ambientnetwork.net', + 'device_class': 'pressure', + 'friendly_name': 'Station C Absolute pressure', + 'last_measured': HAFakeDatetime(2024, 6, 6, 8, 28, 3, tzinfo=zoneinfo.ZoneInfo(key='US/Pacific')), + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.station_c_absolute_pressure', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '977.616536580043', + }) +# --- +# name: test_sensors[CC:CC:CC:CC:CC:CC][sensor.station_c_daily_rain-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.station_c_daily_rain', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Daily rain', + 'platform': 'ambient_network', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'daily_rain', + 'unique_id': 'CC:CC:CC:CC:CC:CC_dailyrainin', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[CC:CC:CC:CC:CC:CC][sensor.station_c_daily_rain-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by ambientnetwork.net', + 'device_class': 'precipitation', + 'friendly_name': 'Station C Daily rain', + 'last_measured': HAFakeDatetime(2024, 6, 6, 8, 28, 3, tzinfo=zoneinfo.ZoneInfo(key='US/Pacific')), + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.station_c_daily_rain', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_sensors[CC:CC:CC:CC:CC:CC][sensor.station_c_dew_point-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.station_c_dew_point', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Dew point', + 'platform': 'ambient_network', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'dew_point', + 'unique_id': 'CC:CC:CC:CC:CC:CC_dewPoint', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[CC:CC:CC:CC:CC:CC][sensor.station_c_dew_point-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by ambientnetwork.net', + 'device_class': 'temperature', + 'friendly_name': 'Station C Dew point', + 'last_measured': HAFakeDatetime(2024, 6, 6, 8, 28, 3, tzinfo=zoneinfo.ZoneInfo(key='US/Pacific')), + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.station_c_dew_point', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '27.7777777777778', + }) +# --- +# name: test_sensors[CC:CC:CC:CC:CC:CC][sensor.station_c_feels_like-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.station_c_feels_like', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Feels like', + 'platform': 'ambient_network', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'feels_like', + 'unique_id': 'CC:CC:CC:CC:CC:CC_feelsLike', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[CC:CC:CC:CC:CC:CC][sensor.station_c_feels_like-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by ambientnetwork.net', + 'device_class': 'temperature', + 'friendly_name': 'Station C Feels like', + 'last_measured': HAFakeDatetime(2024, 6, 6, 8, 28, 3, tzinfo=zoneinfo.ZoneInfo(key='US/Pacific')), + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.station_c_feels_like', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '29.4444444444444', + }) +# --- +# name: test_sensors[CC:CC:CC:CC:CC:CC][sensor.station_c_hourly_rain-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.station_c_hourly_rain', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Hourly rain', + 'platform': 'ambient_network', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'hourly_rain', + 'unique_id': 'CC:CC:CC:CC:CC:CC_hourlyrainin', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[CC:CC:CC:CC:CC:CC][sensor.station_c_hourly_rain-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by ambientnetwork.net', + 'device_class': 'precipitation_intensity', + 'friendly_name': 'Station C Hourly rain', + 'last_measured': HAFakeDatetime(2024, 6, 6, 8, 28, 3, tzinfo=zoneinfo.ZoneInfo(key='US/Pacific')), + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.station_c_hourly_rain', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_sensors[CC:CC:CC:CC:CC:CC][sensor.station_c_humidity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.station_c_humidity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Humidity', + 'platform': 'ambient_network', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'CC:CC:CC:CC:CC:CC_humidity', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensors[CC:CC:CC:CC:CC:CC][sensor.station_c_humidity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by ambientnetwork.net', + 'device_class': 'humidity', + 'friendly_name': 'Station C Humidity', + 'last_measured': HAFakeDatetime(2024, 6, 6, 8, 28, 3, tzinfo=zoneinfo.ZoneInfo(key='US/Pacific')), + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.station_c_humidity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '60', + }) +# --- +# name: test_sensors[CC:CC:CC:CC:CC:CC][sensor.station_c_irradiance-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.station_c_irradiance', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Irradiance', + 'platform': 'ambient_network', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'CC:CC:CC:CC:CC:CC_solarradiation', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[CC:CC:CC:CC:CC:CC][sensor.station_c_irradiance-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by ambientnetwork.net', + 'device_class': 'irradiance', + 'friendly_name': 'Station C Irradiance', + 'last_measured': HAFakeDatetime(2024, 6, 6, 8, 28, 3, tzinfo=zoneinfo.ZoneInfo(key='US/Pacific')), + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.station_c_irradiance', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '37.64', + }) +# --- +# name: test_sensors[CC:CC:CC:CC:CC:CC][sensor.station_c_last_rain-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.station_c_last_rain', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Last rain', + 'platform': 'ambient_network', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'last_rain', + 'unique_id': 'CC:CC:CC:CC:CC:CC_lastRain', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[CC:CC:CC:CC:CC:CC][sensor.station_c_last_rain-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by ambientnetwork.net', + 'device_class': 'timestamp', + 'friendly_name': 'Station C Last rain', + 'last_measured': HAFakeDatetime(2024, 6, 6, 8, 28, 3, tzinfo=zoneinfo.ZoneInfo(key='US/Pacific')), + }), + 'context': , + 'entity_id': 'sensor.station_c_last_rain', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2023-10-30T09:45:00+00:00', + }) +# --- +# name: test_sensors[CC:CC:CC:CC:CC:CC][sensor.station_c_max_daily_gust-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.station_c_max_daily_gust', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Max daily gust', + 'platform': 'ambient_network', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'max_daily_gust', + 'unique_id': 'CC:CC:CC:CC:CC:CC_maxdailygust', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[CC:CC:CC:CC:CC:CC][sensor.station_c_max_daily_gust-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by ambientnetwork.net', + 'device_class': 'wind_speed', + 'friendly_name': 'Station C Max daily gust', + 'last_measured': HAFakeDatetime(2024, 6, 6, 8, 28, 3, tzinfo=zoneinfo.ZoneInfo(key='US/Pacific')), + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.station_c_max_daily_gust', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '36.72523008', + }) +# --- +# name: test_sensors[CC:CC:CC:CC:CC:CC][sensor.station_c_monthly_rain-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.station_c_monthly_rain', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Monthly rain', + 'platform': 'ambient_network', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'monthly_rain', + 'unique_id': 'CC:CC:CC:CC:CC:CC_monthlyrainin', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[CC:CC:CC:CC:CC:CC][sensor.station_c_monthly_rain-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by ambientnetwork.net', + 'device_class': 'precipitation', + 'friendly_name': 'Station C Monthly rain', + 'last_measured': HAFakeDatetime(2024, 6, 6, 8, 28, 3, tzinfo=zoneinfo.ZoneInfo(key='US/Pacific')), + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.station_c_monthly_rain', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_sensors[CC:CC:CC:CC:CC:CC][sensor.station_c_relative_pressure-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.station_c_relative_pressure', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Relative pressure', + 'platform': 'ambient_network', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'relative_pressure', + 'unique_id': 'CC:CC:CC:CC:CC:CC_baromrelin', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[CC:CC:CC:CC:CC:CC][sensor.station_c_relative_pressure-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by ambientnetwork.net', + 'device_class': 'pressure', + 'friendly_name': 'Station C Relative pressure', + 'last_measured': HAFakeDatetime(2024, 6, 6, 8, 28, 3, tzinfo=zoneinfo.ZoneInfo(key='US/Pacific')), + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.station_c_relative_pressure', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1001.89694313129', + }) +# --- +# name: test_sensors[CC:CC:CC:CC:CC:CC][sensor.station_c_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.station_c_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Temperature', + 'platform': 'ambient_network', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'CC:CC:CC:CC:CC:CC_tempf', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[CC:CC:CC:CC:CC:CC][sensor.station_c_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by ambientnetwork.net', + 'device_class': 'temperature', + 'friendly_name': 'Station C Temperature', + 'last_measured': HAFakeDatetime(2024, 6, 6, 8, 28, 3, tzinfo=zoneinfo.ZoneInfo(key='US/Pacific')), + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.station_c_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '28.2777777777778', + }) +# --- +# name: test_sensors[CC:CC:CC:CC:CC:CC][sensor.station_c_uv_index-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.station_c_uv_index', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'UV index', + 'platform': 'ambient_network', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'uv_index', + 'unique_id': 'CC:CC:CC:CC:CC:CC_uv', + 'unit_of_measurement': 'index', + }) +# --- +# name: test_sensors[CC:CC:CC:CC:CC:CC][sensor.station_c_uv_index-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by ambientnetwork.net', + 'friendly_name': 'Station C UV index', + 'last_measured': HAFakeDatetime(2024, 6, 6, 8, 28, 3, tzinfo=zoneinfo.ZoneInfo(key='US/Pacific')), + 'state_class': , + 'unit_of_measurement': 'index', + }), + 'context': , + 'entity_id': 'sensor.station_c_uv_index', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensors[CC:CC:CC:CC:CC:CC][sensor.station_c_weekly_rain-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.station_c_weekly_rain', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Weekly rain', + 'platform': 'ambient_network', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'weekly_rain', + 'unique_id': 'CC:CC:CC:CC:CC:CC_weeklyrainin', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[CC:CC:CC:CC:CC:CC][sensor.station_c_weekly_rain-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by ambientnetwork.net', + 'device_class': 'precipitation', + 'friendly_name': 'Station C Weekly rain', + 'last_measured': HAFakeDatetime(2024, 6, 6, 8, 28, 3, tzinfo=zoneinfo.ZoneInfo(key='US/Pacific')), + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.station_c_weekly_rain', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_sensors[CC:CC:CC:CC:CC:CC][sensor.station_c_wind_direction-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.station_c_wind_direction', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Wind direction', + 'platform': 'ambient_network', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'wind_direction', + 'unique_id': 'CC:CC:CC:CC:CC:CC_winddir', + 'unit_of_measurement': '°', + }) +# --- +# name: test_sensors[CC:CC:CC:CC:CC:CC][sensor.station_c_wind_direction-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by ambientnetwork.net', + 'friendly_name': 'Station C Wind direction', + 'last_measured': HAFakeDatetime(2024, 6, 6, 8, 28, 3, tzinfo=zoneinfo.ZoneInfo(key='US/Pacific')), + 'unit_of_measurement': '°', + }), + 'context': , + 'entity_id': 'sensor.station_c_wind_direction', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '11', + }) +# --- +# name: test_sensors[CC:CC:CC:CC:CC:CC][sensor.station_c_wind_gust-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.station_c_wind_gust', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Wind gust', + 'platform': 'ambient_network', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'wind_gust', + 'unique_id': 'CC:CC:CC:CC:CC:CC_windgustmph', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[CC:CC:CC:CC:CC:CC][sensor.station_c_wind_gust-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by ambientnetwork.net', + 'device_class': 'wind_speed', + 'friendly_name': 'Station C Wind gust', + 'last_measured': HAFakeDatetime(2024, 6, 6, 8, 28, 3, tzinfo=zoneinfo.ZoneInfo(key='US/Pacific')), + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.station_c_wind_gust', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '14.75768448', + }) +# --- +# name: test_sensors[CC:CC:CC:CC:CC:CC][sensor.station_c_wind_speed-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.station_c_wind_speed', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Wind speed', + 'platform': 'ambient_network', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'CC:CC:CC:CC:CC:CC_windspeedmph', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[CC:CC:CC:CC:CC:CC][sensor.station_c_wind_speed-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by ambientnetwork.net', + 'device_class': 'wind_speed', + 'friendly_name': 'Station C Wind speed', + 'last_measured': HAFakeDatetime(2024, 6, 6, 8, 28, 3, tzinfo=zoneinfo.ZoneInfo(key='US/Pacific')), + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.station_c_wind_speed', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '14.03347968', + }) +# --- +# name: test_sensors[DD:DD:DD:DD:DD:DD][sensor.station_d_absolute_pressure-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.station_d_absolute_pressure', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Absolute pressure', + 'platform': 'ambient_network', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'absolute_pressure', + 'unique_id': 'DD:DD:DD:DD:DD:DD_baromabsin', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[DD:DD:DD:DD:DD:DD][sensor.station_d_absolute_pressure-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by ambientnetwork.net', + 'device_class': 'pressure', + 'friendly_name': 'Station D Absolute pressure', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.station_d_absolute_pressure', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '977.616536580043', + }) +# --- +# name: test_sensors[DD:DD:DD:DD:DD:DD][sensor.station_d_daily_rain-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.station_d_daily_rain', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Daily rain', + 'platform': 'ambient_network', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'daily_rain', + 'unique_id': 'DD:DD:DD:DD:DD:DD_dailyrainin', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[DD:DD:DD:DD:DD:DD][sensor.station_d_daily_rain-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by ambientnetwork.net', + 'device_class': 'precipitation', + 'friendly_name': 'Station D Daily rain', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.station_d_daily_rain', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_sensors[DD:DD:DD:DD:DD:DD][sensor.station_d_dew_point-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.station_d_dew_point', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Dew point', + 'platform': 'ambient_network', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'dew_point', + 'unique_id': 'DD:DD:DD:DD:DD:DD_dewPoint', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[DD:DD:DD:DD:DD:DD][sensor.station_d_dew_point-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by ambientnetwork.net', + 'device_class': 'temperature', + 'friendly_name': 'Station D Dew point', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.station_d_dew_point', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '27.7777777777778', + }) +# --- +# name: test_sensors[DD:DD:DD:DD:DD:DD][sensor.station_d_feels_like-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.station_d_feels_like', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Feels like', + 'platform': 'ambient_network', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'feels_like', + 'unique_id': 'DD:DD:DD:DD:DD:DD_feelsLike', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[DD:DD:DD:DD:DD:DD][sensor.station_d_feels_like-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by ambientnetwork.net', + 'device_class': 'temperature', + 'friendly_name': 'Station D Feels like', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.station_d_feels_like', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '29.4444444444444', + }) +# --- +# name: test_sensors[DD:DD:DD:DD:DD:DD][sensor.station_d_hourly_rain-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.station_d_hourly_rain', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Hourly rain', + 'platform': 'ambient_network', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'hourly_rain', + 'unique_id': 'DD:DD:DD:DD:DD:DD_hourlyrainin', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[DD:DD:DD:DD:DD:DD][sensor.station_d_hourly_rain-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by ambientnetwork.net', + 'device_class': 'precipitation_intensity', + 'friendly_name': 'Station D Hourly rain', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.station_d_hourly_rain', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_sensors[DD:DD:DD:DD:DD:DD][sensor.station_d_humidity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.station_d_humidity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Humidity', + 'platform': 'ambient_network', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'DD:DD:DD:DD:DD:DD_humidity', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensors[DD:DD:DD:DD:DD:DD][sensor.station_d_humidity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by ambientnetwork.net', + 'device_class': 'humidity', + 'friendly_name': 'Station D Humidity', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.station_d_humidity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '60', + }) +# --- +# name: test_sensors[DD:DD:DD:DD:DD:DD][sensor.station_d_irradiance-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.station_d_irradiance', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Irradiance', + 'platform': 'ambient_network', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'DD:DD:DD:DD:DD:DD_solarradiation', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[DD:DD:DD:DD:DD:DD][sensor.station_d_irradiance-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by ambientnetwork.net', + 'device_class': 'irradiance', + 'friendly_name': 'Station D Irradiance', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.station_d_irradiance', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '37.64', + }) +# --- +# name: test_sensors[DD:DD:DD:DD:DD:DD][sensor.station_d_max_daily_gust-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.station_d_max_daily_gust', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Max daily gust', + 'platform': 'ambient_network', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'max_daily_gust', + 'unique_id': 'DD:DD:DD:DD:DD:DD_maxdailygust', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[DD:DD:DD:DD:DD:DD][sensor.station_d_max_daily_gust-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by ambientnetwork.net', + 'device_class': 'wind_speed', + 'friendly_name': 'Station D Max daily gust', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.station_d_max_daily_gust', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '36.72523008', + }) +# --- +# name: test_sensors[DD:DD:DD:DD:DD:DD][sensor.station_d_monthly_rain-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.station_d_monthly_rain', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Monthly rain', + 'platform': 'ambient_network', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'monthly_rain', + 'unique_id': 'DD:DD:DD:DD:DD:DD_monthlyrainin', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[DD:DD:DD:DD:DD:DD][sensor.station_d_monthly_rain-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by ambientnetwork.net', + 'device_class': 'precipitation', + 'friendly_name': 'Station D Monthly rain', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.station_d_monthly_rain', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_sensors[DD:DD:DD:DD:DD:DD][sensor.station_d_relative_pressure-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.station_d_relative_pressure', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Relative pressure', + 'platform': 'ambient_network', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'relative_pressure', + 'unique_id': 'DD:DD:DD:DD:DD:DD_baromrelin', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[DD:DD:DD:DD:DD:DD][sensor.station_d_relative_pressure-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by ambientnetwork.net', + 'device_class': 'pressure', + 'friendly_name': 'Station D Relative pressure', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.station_d_relative_pressure', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1001.89694313129', + }) +# --- +# name: test_sensors[DD:DD:DD:DD:DD:DD][sensor.station_d_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.station_d_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Temperature', + 'platform': 'ambient_network', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'DD:DD:DD:DD:DD:DD_tempf', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[DD:DD:DD:DD:DD:DD][sensor.station_d_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by ambientnetwork.net', + 'device_class': 'temperature', + 'friendly_name': 'Station D Temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.station_d_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '28.2777777777778', + }) +# --- +# name: test_sensors[DD:DD:DD:DD:DD:DD][sensor.station_d_uv_index-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.station_d_uv_index', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'UV index', + 'platform': 'ambient_network', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'uv_index', + 'unique_id': 'DD:DD:DD:DD:DD:DD_uv', + 'unit_of_measurement': 'index', + }) +# --- +# name: test_sensors[DD:DD:DD:DD:DD:DD][sensor.station_d_uv_index-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by ambientnetwork.net', + 'friendly_name': 'Station D UV index', + 'state_class': , + 'unit_of_measurement': 'index', + }), + 'context': , + 'entity_id': 'sensor.station_d_uv_index', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensors[DD:DD:DD:DD:DD:DD][sensor.station_d_weekly_rain-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.station_d_weekly_rain', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Weekly rain', + 'platform': 'ambient_network', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'weekly_rain', + 'unique_id': 'DD:DD:DD:DD:DD:DD_weeklyrainin', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[DD:DD:DD:DD:DD:DD][sensor.station_d_weekly_rain-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by ambientnetwork.net', + 'device_class': 'precipitation', + 'friendly_name': 'Station D Weekly rain', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.station_d_weekly_rain', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_sensors[DD:DD:DD:DD:DD:DD][sensor.station_d_wind_direction-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.station_d_wind_direction', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Wind direction', + 'platform': 'ambient_network', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'wind_direction', + 'unique_id': 'DD:DD:DD:DD:DD:DD_winddir', + 'unit_of_measurement': '°', + }) +# --- +# name: test_sensors[DD:DD:DD:DD:DD:DD][sensor.station_d_wind_direction-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by ambientnetwork.net', + 'friendly_name': 'Station D Wind direction', + 'unit_of_measurement': '°', + }), + 'context': , + 'entity_id': 'sensor.station_d_wind_direction', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '11', + }) +# --- +# name: test_sensors[DD:DD:DD:DD:DD:DD][sensor.station_d_wind_gust-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.station_d_wind_gust', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Wind gust', + 'platform': 'ambient_network', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'wind_gust', + 'unique_id': 'DD:DD:DD:DD:DD:DD_windgustmph', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[DD:DD:DD:DD:DD:DD][sensor.station_d_wind_gust-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by ambientnetwork.net', + 'device_class': 'wind_speed', + 'friendly_name': 'Station D Wind gust', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.station_d_wind_gust', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '14.75768448', + }) +# --- +# name: test_sensors[DD:DD:DD:DD:DD:DD][sensor.station_d_wind_speed-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.station_d_wind_speed', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Wind speed', + 'platform': 'ambient_network', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'DD:DD:DD:DD:DD:DD_windspeedmph', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[DD:DD:DD:DD:DD:DD][sensor.station_d_wind_speed-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by ambientnetwork.net', + 'device_class': 'wind_speed', + 'friendly_name': 'Station D Wind speed', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.station_d_wind_speed', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '14.03347968', + }) +# --- diff --git a/tests/components/ambient_network/test_sensor.py b/tests/components/ambient_network/test_sensor.py index 0acd9d2d33b..ab4facd0cb9 100644 --- a/tests/components/ambient_network/test_sensor.py +++ b/tests/components/ambient_network/test_sensor.py @@ -1,7 +1,7 @@ """Test Ambient Weather Network sensors.""" from datetime import datetime, timedelta -from unittest.mock import patch +from unittest.mock import AsyncMock, patch from aioambient import OpenAPI from aioambient.errors import RequestError @@ -9,6 +9,7 @@ from freezegun import freeze_time import pytest from syrupy.assertion import SnapshotAssertion +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -17,65 +18,47 @@ from .conftest import setup_platform from tests.common import async_fire_time_changed, snapshot_platform -@freeze_time("2023-11-08") -@pytest.mark.parametrize("config_entry", ["AA:AA:AA:AA:AA:AA"], indirect=True) +@freeze_time("2023-11-9") +@pytest.mark.parametrize( + "config_entry", + ["AA:AA:AA:AA:AA:AA", "CC:CC:CC:CC:CC:CC", "DD:DD:DD:DD:DD:DD"], + indirect=True, +) @pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_sensors( hass: HomeAssistant, open_api: OpenAPI, - aioambient, - config_entry, + aioambient: AsyncMock, + config_entry: ConfigEntry, entity_registry: er.EntityRegistry, snapshot: SnapshotAssertion, ) -> None: """Test all sensors under normal operation.""" await setup_platform(True, hass, config_entry) - await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id) -@freeze_time("2023-11-09") -@pytest.mark.parametrize("config_entry", ["AA:AA:AA:AA:AA:AA"], indirect=True) -async def test_sensors_with_stale_data( - hass: HomeAssistant, open_api: OpenAPI, aioambient, config_entry -) -> None: - """Test that the sensors are not populated if the data is stale.""" - await setup_platform(False, hass, config_entry) - - sensor = hass.states.get("sensor.station_a_absolute_pressure") - assert sensor is None - - -@freeze_time("2023-11-08") @pytest.mark.parametrize("config_entry", ["BB:BB:BB:BB:BB:BB"], indirect=True) async def test_sensors_with_no_data( - hass: HomeAssistant, open_api: OpenAPI, aioambient, config_entry + hass: HomeAssistant, + open_api: OpenAPI, + aioambient: AsyncMock, + config_entry: ConfigEntry, ) -> None: """Test that the sensors are not populated if the last data is absent.""" - await setup_platform(False, hass, config_entry) + await setup_platform(True, hass, config_entry) - sensor = hass.states.get("sensor.station_b_absolute_pressure") - assert sensor is None - - -@freeze_time("2023-11-08") -@pytest.mark.parametrize("config_entry", ["CC:CC:CC:CC:CC:CC"], indirect=True) -async def test_sensors_with_no_update_time( - hass: HomeAssistant, open_api: OpenAPI, aioambient, config_entry -) -> None: - """Test that the sensors are not populated if the update time is missing.""" - await setup_platform(False, hass, config_entry) - - sensor = hass.states.get("sensor.station_c_absolute_pressure") - assert sensor is None + sensor = hass.states.get("sensor.station_b_temperature") + assert sensor is not None + assert "last_measured" not in sensor.attributes @pytest.mark.parametrize("config_entry", ["AA:AA:AA:AA:AA:AA"], indirect=True) async def test_sensors_disappearing( hass: HomeAssistant, open_api: OpenAPI, - aioambient, - config_entry, + aioambient: AsyncMock, + config_entry: ConfigEntry, caplog: pytest.LogCaptureFixture, ) -> None: """Test that we log errors properly.""" From 4110f4f393f2d5899caceb860beb07b2ca8fb5d9 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Fri, 21 Jun 2024 16:42:22 +0200 Subject: [PATCH 0945/1445] Fix Matter entity names (#120038) --- homeassistant/components/matter/climate.py | 2 +- homeassistant/components/matter/cover.py | 10 +- homeassistant/components/matter/entity.py | 44 + homeassistant/components/matter/event.py | 21 +- homeassistant/components/matter/light.py | 10 +- homeassistant/components/matter/lock.py | 4 +- homeassistant/components/matter/models.py | 3 - homeassistant/components/matter/strings.json | 32 +- homeassistant/components/matter/switch.py | 54 +- .../fixtures/nodes/multi-endpoint-light.json | 1637 +++++++++++++++++ .../fixtures/nodes/on-off-plugin-unit.json | 2 +- tests/components/matter/test_adapter.py | 24 +- tests/components/matter/test_climate.py | 50 +- tests/components/matter/test_cover.py | 32 +- tests/components/matter/test_door_lock.py | 30 +- tests/components/matter/test_event.py | 13 +- tests/components/matter/test_fan.py | 12 +- tests/components/matter/test_init.py | 6 +- tests/components/matter/test_light.py | 24 +- tests/components/matter/test_switch.py | 30 +- 20 files changed, 1911 insertions(+), 129 deletions(-) create mode 100644 tests/components/matter/fixtures/nodes/multi-endpoint-light.json diff --git a/homeassistant/components/matter/climate.py b/homeassistant/components/matter/climate.py index 2050a9eb185..d2656d59138 100644 --- a/homeassistant/components/matter/climate.py +++ b/homeassistant/components/matter/climate.py @@ -321,7 +321,7 @@ DISCOVERY_SCHEMAS = [ platform=Platform.CLIMATE, entity_description=ClimateEntityDescription( key="MatterThermostat", - name=None, + translation_key="thermostat", ), entity_class=MatterClimate, required_attributes=(clusters.Thermostat.Attributes.LocalTemperature,), diff --git a/homeassistant/components/matter/cover.py b/homeassistant/components/matter/cover.py index ea5250c9bd3..c32b7bc9e1a 100644 --- a/homeassistant/components/matter/cover.py +++ b/homeassistant/components/matter/cover.py @@ -200,7 +200,9 @@ class MatterCover(MatterEntity, CoverEntity): DISCOVERY_SCHEMAS = [ MatterDiscoverySchema( platform=Platform.COVER, - entity_description=CoverEntityDescription(key="MatterCover", name=None), + entity_description=CoverEntityDescription( + key="MatterCover", translation_key="cover" + ), entity_class=MatterCover, required_attributes=( clusters.WindowCovering.Attributes.OperationalStatus, @@ -214,7 +216,7 @@ DISCOVERY_SCHEMAS = [ MatterDiscoverySchema( platform=Platform.COVER, entity_description=CoverEntityDescription( - key="MatterCoverPositionAwareLift", name=None + key="MatterCoverPositionAwareLift", translation_key="cover" ), entity_class=MatterCover, required_attributes=( @@ -229,7 +231,7 @@ DISCOVERY_SCHEMAS = [ MatterDiscoverySchema( platform=Platform.COVER, entity_description=CoverEntityDescription( - key="MatterCoverPositionAwareTilt", name=None + key="MatterCoverPositionAwareTilt", translation_key="cover" ), entity_class=MatterCover, required_attributes=( @@ -244,7 +246,7 @@ DISCOVERY_SCHEMAS = [ MatterDiscoverySchema( platform=Platform.COVER, entity_description=CoverEntityDescription( - key="MatterCoverPositionAwareLiftAndTilt", name=None + key="MatterCoverPositionAwareLiftAndTilt", translation_key="cover" ), entity_class=MatterCover, required_attributes=( diff --git a/homeassistant/components/matter/entity.py b/homeassistant/components/matter/entity.py index 876693f354f..aaaaf074ddd 100644 --- a/homeassistant/components/matter/entity.py +++ b/homeassistant/components/matter/entity.py @@ -5,9 +5,11 @@ from __future__ import annotations from abc import abstractmethod from collections.abc import Callable from dataclasses import dataclass +from functools import cached_property import logging from typing import TYPE_CHECKING, Any, cast +from chip.clusters import Objects as clusters from chip.clusters.Objects import ClusterAttributeDescriptor, NullValue from matter_server.common.helpers.util import create_attribute_path from matter_server.common.models import EventType, ServerInfoMessage @@ -15,6 +17,7 @@ from matter_server.common.models import EventType, ServerInfoMessage from homeassistant.core import callback from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity import Entity, EntityDescription +from homeassistant.helpers.typing import UndefinedType from .const import DOMAIN, ID_TYPE_DEVICE_ID from .helpers import get_device_id @@ -41,6 +44,7 @@ class MatterEntity(Entity): """Entity class for Matter devices.""" _attr_has_entity_name = True + _name_postfix: str | None = None def __init__( self, @@ -71,6 +75,35 @@ class MatterEntity(Entity): identifiers={(DOMAIN, f"{ID_TYPE_DEVICE_ID}_{node_device_id}")} ) self._attr_available = self._endpoint.node.available + # mark endpoint postfix if the device has the primary attribute on multiple endpoints + if not self._endpoint.node.is_bridge_device and any( + ep + for ep in self._endpoint.node.endpoints.values() + if ep != self._endpoint + and ep.has_attribute(None, entity_info.primary_attribute) + ): + self._name_postfix = str(self._endpoint.endpoint_id) + + # prefer the label attribute for the entity name + # Matter has a way for users and/or vendors to specify a name for an endpoint + # which is always preferred over a standard HA (generated) name + for attr in ( + clusters.FixedLabel.Attributes.LabelList, + clusters.UserLabel.Attributes.LabelList, + ): + if not (labels := self.get_matter_attribute_value(attr)): + continue + for label in labels: + if label.label not in ["Label", "Button"]: + continue + # fixed or user label found: use it + label_value: str = label.value + # in the case the label is only the label id, use it as postfix only + if label_value.isnumeric(): + self._name_postfix = label_value + else: + self._attr_name = label_value + break # make sure to update the attributes once self._update_from_device() @@ -105,6 +138,17 @@ class MatterEntity(Entity): ) ) + @cached_property + def name(self) -> str | UndefinedType | None: + """Return the name of the entity.""" + if hasattr(self, "_attr_name"): + # an explicit entity name was defined, we use that + return self._attr_name + name = super().name + if name and self._name_postfix: + name = f"{name} ({self._name_postfix})" + return name + @callback def _on_matter_event(self, event: EventType, data: Any = None) -> None: """Call on update from the device.""" diff --git a/homeassistant/components/matter/event.py b/homeassistant/components/matter/event.py index ade3452a6cf..dcb67d50523 100644 --- a/homeassistant/components/matter/event.py +++ b/homeassistant/components/matter/event.py @@ -49,8 +49,6 @@ async def async_setup_entry( class MatterEventEntity(MatterEntity, EventEntity): """Representation of a Matter Event entity.""" - _attr_translation_key = "push" - def __init__(self, *args: Any, **kwargs: Any) -> None: """Initialize the entity.""" super().__init__(*args, **kwargs) @@ -72,21 +70,6 @@ class MatterEventEntity(MatterEntity, EventEntity): event_types.append("multi_press_ongoing") event_types.append("multi_press_complete") self._attr_event_types = event_types - # the optional label attribute could be used to identify multiple buttons - # e.g. in case of a dimmer switch with 4 buttons, each button - # will have its own name, prefixed by the device name. - if labels := self.get_matter_attribute_value( - clusters.FixedLabel.Attributes.LabelList - ): - for label in labels: - if label.label in ["Label", "Button"]: - label_value: str = label.value - # in the case the label is only the label id, prettify it a bit - if label_value.isnumeric(): - self._attr_name = f"Button {label_value}" - else: - self._attr_name = label_value - break async def async_added_to_hass(self) -> None: """Handle being added to Home Assistant.""" @@ -122,7 +105,9 @@ DISCOVERY_SCHEMAS = [ MatterDiscoverySchema( platform=Platform.EVENT, entity_description=EventEntityDescription( - key="GenericSwitch", device_class=EventDeviceClass.BUTTON, name=None + key="GenericSwitch", + device_class=EventDeviceClass.BUTTON, + translation_key="button", ), entity_class=MatterEventEntity, required_attributes=( diff --git a/homeassistant/components/matter/light.py b/homeassistant/components/matter/light.py index 007bcd1a33a..777e4a69010 100644 --- a/homeassistant/components/matter/light.py +++ b/homeassistant/components/matter/light.py @@ -421,7 +421,9 @@ class MatterLight(MatterEntity, LightEntity): DISCOVERY_SCHEMAS = [ MatterDiscoverySchema( platform=Platform.LIGHT, - entity_description=LightEntityDescription(key="MatterLight", name=None), + entity_description=LightEntityDescription( + key="MatterLight", translation_key="light" + ), entity_class=MatterLight, required_attributes=(clusters.OnOff.Attributes.OnOff,), optional_attributes=( @@ -445,7 +447,7 @@ DISCOVERY_SCHEMAS = [ MatterDiscoverySchema( platform=Platform.LIGHT, entity_description=LightEntityDescription( - key="MatterHSColorLightFallback", name=None + key="MatterHSColorLightFallback", translation_key="light" ), entity_class=MatterLight, required_attributes=( @@ -465,7 +467,7 @@ DISCOVERY_SCHEMAS = [ MatterDiscoverySchema( platform=Platform.LIGHT, entity_description=LightEntityDescription( - key="MatterXYColorLightFallback", name=None + key="MatterXYColorLightFallback", translation_key="light" ), entity_class=MatterLight, required_attributes=( @@ -485,7 +487,7 @@ DISCOVERY_SCHEMAS = [ MatterDiscoverySchema( platform=Platform.LIGHT, entity_description=LightEntityDescription( - key="MatterColorTemperatureLightFallback", name=None + key="MatterColorTemperatureLightFallback", translation_key="light" ), entity_class=MatterLight, required_attributes=( diff --git a/homeassistant/components/matter/lock.py b/homeassistant/components/matter/lock.py index f58ded01013..5456554a535 100644 --- a/homeassistant/components/matter/lock.py +++ b/homeassistant/components/matter/lock.py @@ -176,7 +176,9 @@ class MatterLock(MatterEntity, LockEntity): DISCOVERY_SCHEMAS = [ MatterDiscoverySchema( platform=Platform.LOCK, - entity_description=LockEntityDescription(key="MatterLock", name=None), + entity_description=LockEntityDescription( + key="MatterLock", translation_key="lock" + ), entity_class=MatterLock, required_attributes=(clusters.DoorLock.Attributes.LockState,), optional_attributes=(clusters.DoorLock.Attributes.DoorState,), diff --git a/homeassistant/components/matter/models.py b/homeassistant/components/matter/models.py index c77d6b42dcd..bb79d3571cf 100644 --- a/homeassistant/components/matter/models.py +++ b/homeassistant/components/matter/models.py @@ -107,6 +107,3 @@ class MatterDiscoverySchema: # [optional] bool to specify if this primary value may be discovered # by multiple platforms allow_multi: bool = False - - # [optional] bool to specify if this primary value should be polled - should_poll: bool = False diff --git a/homeassistant/components/matter/strings.json b/homeassistant/components/matter/strings.json index 190aae5de43..db71feab9c4 100644 --- a/homeassistant/components/matter/strings.json +++ b/homeassistant/components/matter/strings.json @@ -45,8 +45,19 @@ } }, "entity": { + "climate": { + "thermostat": { + "name": "Thermostat" + } + }, + "cover": { + "cover": { + "name": "[%key:component::cover::title%]" + } + }, "event": { - "push": { + "button": { + "name": "Button", "state_attributes": { "event_type": { "state": { @@ -64,6 +75,7 @@ }, "fan": { "fan": { + "name": "[%key:component::fan::title%]", "state_attributes": { "preset_mode": { "state": { @@ -92,6 +104,16 @@ "name": "On/Off transition time" } }, + "light": { + "light": { + "name": "[%key:component::light::title%]" + } + }, + "lock": { + "lock": { + "name": "[%key:component::lock::title%]" + } + }, "sensor": { "activated_carbon_filter_condition": { "name": "Activated carbon filter condition" @@ -114,6 +136,14 @@ "hepa_filter_condition": { "name": "Hepa filter condition" } + }, + "switch": { + "switch": { + "name": "[%key:component::switch::title%]" + }, + "power": { + "name": "Power" + } } }, "services": { diff --git a/homeassistant/components/matter/switch.py b/homeassistant/components/matter/switch.py index f148102cfcd..efa78446fc5 100644 --- a/homeassistant/components/matter/switch.py +++ b/homeassistant/components/matter/switch.py @@ -64,7 +64,9 @@ DISCOVERY_SCHEMAS = [ MatterDiscoverySchema( platform=Platform.SWITCH, entity_description=SwitchEntityDescription( - key="MatterPlug", device_class=SwitchDeviceClass.OUTLET, name=None + key="MatterPlug", + device_class=SwitchDeviceClass.OUTLET, + translation_key="switch", ), entity_class=MatterSwitch, required_attributes=(clusters.OnOff.Attributes.OnOff,), @@ -73,7 +75,38 @@ DISCOVERY_SCHEMAS = [ MatterDiscoverySchema( platform=Platform.SWITCH, entity_description=SwitchEntityDescription( - key="MatterSwitch", device_class=SwitchDeviceClass.SWITCH, name=None + key="MatterPowerToggle", + device_class=SwitchDeviceClass.SWITCH, + translation_key="power", + ), + entity_class=MatterSwitch, + required_attributes=(clusters.OnOff.Attributes.OnOff,), + device_type=( + device_types.AirPurifier, + device_types.BasicVideoPlayer, + device_types.CastingVideoPlayer, + device_types.CookSurface, + device_types.Cooktop, + device_types.Dishwasher, + device_types.ExtractorHood, + device_types.HeatingCoolingUnit, + device_types.LaundryDryer, + device_types.LaundryWasher, + device_types.Oven, + device_types.Pump, + device_types.PumpController, + device_types.Refrigerator, + device_types.RoboticVacuumCleaner, + device_types.RoomAirConditioner, + device_types.Speaker, + ), + ), + MatterDiscoverySchema( + platform=Platform.SWITCH, + entity_description=SwitchEntityDescription( + key="MatterSwitch", + device_class=SwitchDeviceClass.OUTLET, + translation_key="switch", ), entity_class=MatterSwitch, required_attributes=(clusters.OnOff.Attributes.OnOff,), @@ -83,6 +116,23 @@ DISCOVERY_SCHEMAS = [ device_types.ExtendedColorLight, device_types.ColorDimmerSwitch, device_types.OnOffLight, + device_types.AirPurifier, + device_types.BasicVideoPlayer, + device_types.CastingVideoPlayer, + device_types.CookSurface, + device_types.Cooktop, + device_types.Dishwasher, + device_types.ExtractorHood, + device_types.HeatingCoolingUnit, + device_types.LaundryDryer, + device_types.LaundryWasher, + device_types.Oven, + device_types.Pump, + device_types.PumpController, + device_types.Refrigerator, + device_types.RoboticVacuumCleaner, + device_types.RoomAirConditioner, + device_types.Speaker, ), ), ] diff --git a/tests/components/matter/fixtures/nodes/multi-endpoint-light.json b/tests/components/matter/fixtures/nodes/multi-endpoint-light.json new file mode 100644 index 00000000000..e3a01da9e7c --- /dev/null +++ b/tests/components/matter/fixtures/nodes/multi-endpoint-light.json @@ -0,0 +1,1637 @@ +{ + "node_id": 197, + "date_commissioned": "2024-06-21T00:23:41.026916", + "last_interview": "2024-06-21T00:23:41.026923", + "interview_version": 6, + "available": true, + "is_bridge": false, + "attributes": { + "0/29/0": [ + { + "0": 18, + "1": 1 + }, + { + "0": 22, + "1": 1 + } + ], + "0/29/1": [29, 31, 40, 42, 48, 49, 51, 53, 60, 62, 63, 64], + "0/29/2": [41], + "0/29/3": [1, 2, 3, 4, 5, 6], + "0/29/65532": 0, + "0/29/65533": 1, + "0/29/65528": [], + "0/29/65529": [], + "0/29/65530": [], + "0/29/65531": [0, 1, 2, 3, 65528, 65529, 65530, 65531, 65532, 65533], + "0/31/0": [ + { + "1": 5, + "2": 2, + "3": [112233], + "4": null, + "254": 18 + } + ], + "0/31/1": [], + "0/31/2": 4, + "0/31/3": 3, + "0/31/4": 4, + "0/31/65532": 0, + "0/31/65533": 1, + "0/31/65528": [], + "0/31/65529": [], + "0/31/65530": [0, 1], + "0/31/65531": [0, 1, 2, 3, 4, 65528, 65529, 65530, 65531, 65532, 65533], + "0/40/0": 1, + "0/40/1": "Inovelli", + "0/40/2": 4961, + "0/40/3": "VTM31-SN", + "0/40/4": 1, + "0/40/5": "Inovelli", + "0/40/6": "**REDACTED**", + "0/40/7": 1, + "0/40/8": "0.0.0.1", + "0/40/9": 100, + "0/40/10": "1.0.0", + "0/40/11": "20231207", + "0/40/12": "850007431228", + "0/40/13": "https://inovelli.com/products/thread-matter-white-series-smart-2-1-on-off-dimmer-switch", + "0/40/14": "White Series Smart 2-1 Switch", + "0/40/15": "", + "0/40/16": false, + "0/40/17": true, + "0/40/18": "", + "0/40/19": { + "0": 3, + "1": 3 + }, + "0/40/65532": 0, + "0/40/65533": 1, + "0/40/65528": [], + "0/40/65529": [], + "0/40/65530": [0], + "0/40/65531": [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, + 65528, 65529, 65530, 65531, 65532, 65533 + ], + "0/42/0": [], + "0/42/1": true, + "0/42/2": 1, + "0/42/3": null, + "0/42/65532": 0, + "0/42/65533": 1, + "0/42/65528": [], + "0/42/65529": [0], + "0/42/65530": [0, 1, 2], + "0/42/65531": [0, 1, 2, 3, 65528, 65529, 65530, 65531, 65532, 65533], + "0/48/0": 0, + "0/48/1": { + "0": 60, + "1": 900 + }, + "0/48/2": 0, + "0/48/3": 0, + "0/48/4": true, + "0/48/65532": 0, + "0/48/65533": 1, + "0/48/65528": [1, 3, 5], + "0/48/65529": [0, 2, 4], + "0/48/65530": [], + "0/48/65531": [0, 1, 2, 3, 4, 65528, 65529, 65530, 65531, 65532, 65533], + "0/49/0": 1, + "0/49/1": [ + { + "0": "guA0lmuCSNw=", + "1": true + } + ], + "0/49/2": 10, + "0/49/3": 20, + "0/49/4": true, + "0/49/5": 0, + "0/49/6": "guA0lmuCSNw=", + "0/49/7": null, + "0/49/65532": 2, + "0/49/65533": 1, + "0/49/65528": [1, 5, 7], + "0/49/65529": [0, 3, 4, 6, 8], + "0/49/65530": [], + "0/49/65531": [ + 0, 1, 2, 3, 4, 5, 6, 7, 65528, 65529, 65530, 65531, 65532, 65533 + ], + "0/51/0": [ + { + "0": "MyHome442262884", + "1": true, + "2": null, + "3": null, + "4": "pNwAIEFBCBY=", + "5": [], + "6": [], + "7": 4 + } + ], + "0/51/1": 102, + "0/51/2": 1069632, + "0/51/3": 297, + "0/51/4": 0, + "0/51/5": [], + "0/51/6": [], + "0/51/7": [], + "0/51/8": false, + "0/51/65532": 0, + "0/51/65533": 1, + "0/51/65528": [], + "0/51/65529": [0], + "0/51/65530": [3], + "0/51/65531": [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 65528, 65529, 65530, 65531, 65532, 65533 + ], + "0/53/0": 25, + "0/53/1": 5, + "0/53/2": "MyHome442262884", + "0/53/3": 27622, + "0/53/4": 9430595440367257820, + "0/53/5": "QP2Ea5ozpY2d", + "0/53/6": 0, + "0/53/7": [ + { + "0": 8852464968076080128, + "1": 12, + "2": 9216, + "3": 1183717, + "4": 39695, + "5": 3, + "6": -74, + "7": -74, + "8": 64, + "9": 15, + "10": true, + "11": true, + "12": true, + "13": false + }, + { + "0": 13720920983629429643, + "1": 17, + "2": 13312, + "3": 256914, + "4": 61057, + "5": 2, + "6": -84, + "7": -84, + "8": 16, + "9": 1, + "10": true, + "11": true, + "12": true, + "13": false + }, + { + "0": 9388760890908673655, + "1": 26, + "2": 17408, + "3": 2054526, + "4": 79216, + "5": 2, + "6": -85, + "7": -86, + "8": 1, + "9": 0, + "10": true, + "11": true, + "12": true, + "13": false + }, + { + "0": 6844302060861963395, + "1": 48, + "2": 21504, + "3": 23719, + "4": 9471, + "5": 2, + "6": -84, + "7": -84, + "8": 0, + "9": 0, + "10": true, + "11": true, + "12": true, + "13": false + }, + { + "0": 14305772551860424697, + "1": 1, + "2": 23552, + "3": 189996, + "4": 65613, + "5": 2, + "6": -85, + "7": -85, + "8": 21, + "9": 1, + "10": true, + "11": true, + "12": true, + "13": false + }, + { + "0": 17491005778920105492, + "1": 44, + "2": 28672, + "3": 310232, + "4": 144381, + "5": 3, + "6": -61, + "7": -61, + "8": 5, + "9": 0, + "10": true, + "11": true, + "12": true, + "13": false + }, + { + "0": 7968688256206678783, + "1": 9, + "2": 30720, + "3": 31923, + "4": 15482, + "5": 2, + "6": -88, + "7": -89, + "8": 0, + "9": 0, + "10": true, + "11": true, + "12": true, + "13": false + }, + { + "0": 2195983971765588925, + "1": 3, + "2": 31744, + "3": 658867, + "4": 53332, + "5": 3, + "6": -77, + "7": -78, + "8": 51, + "9": 2, + "10": true, + "11": true, + "12": true, + "13": false + }, + { + "0": 8533237363532831991, + "1": 32, + "2": 38912, + "3": 196496, + "4": 66926, + "5": 3, + "6": -75, + "7": -75, + "8": 0, + "9": 0, + "10": true, + "11": true, + "12": true, + "13": false + }, + { + "0": 15462742133285018414, + "1": 30, + "2": 51200, + "3": 156349, + "4": 91387, + "5": 1, + "6": -93, + "7": -94, + "8": 0, + "9": 0, + "10": true, + "11": true, + "12": true, + "13": false + }, + { + "0": 9106713788407201067, + "1": 14, + "2": 54272, + "3": 228318, + "4": 145504, + "5": 3, + "6": -65, + "7": -65, + "8": 59, + "9": 6, + "10": true, + "11": true, + "12": true, + "13": false + } + ], + "0/53/8": [ + { + "0": 9392545512105173771, + "1": 0, + "2": 0, + "3": 63, + "4": 0, + "5": 0, + "6": 0, + "7": 0, + "8": true, + "9": false + }, + { + "0": 0, + "1": 1024, + "2": 1, + "3": 28, + "4": 1, + "5": 0, + "6": 0, + "7": 13, + "8": true, + "9": false + }, + { + "0": 0, + "1": 2048, + "2": 2, + "3": 53, + "4": 1, + "5": 0, + "6": 0, + "7": 3, + "8": true, + "9": false + }, + { + "0": 8852464968076080128, + "1": 9216, + "2": 9, + "3": 28, + "4": 1, + "5": 3, + "6": 2, + "7": 12, + "8": true, + "9": true + }, + { + "0": 0, + "1": 11264, + "2": 11, + "3": 28, + "4": 1, + "5": 0, + "6": 0, + "7": 119, + "8": true, + "9": false + }, + { + "0": 13720920983629429643, + "1": 13312, + "2": 13, + "3": 28, + "4": 1, + "5": 2, + "6": 2, + "7": 17, + "8": true, + "9": true + }, + { + "0": 9388760890908673655, + "1": 17408, + "2": 17, + "3": 28, + "4": 1, + "5": 2, + "6": 0, + "7": 27, + "8": true, + "9": true + }, + { + "0": 6844302060861963395, + "1": 21504, + "2": 21, + "3": 28, + "4": 1, + "5": 2, + "6": 2, + "7": 48, + "8": true, + "9": true + }, + { + "0": 14305772551860424697, + "1": 23552, + "2": 23, + "3": 28, + "4": 1, + "5": 2, + "6": 2, + "7": 1, + "8": true, + "9": true + }, + { + "0": 0, + "1": 27648, + "2": 27, + "3": 53, + "4": 1, + "5": 0, + "6": 0, + "7": 36, + "8": true, + "9": false + }, + { + "0": 17491005778920105492, + "1": 28672, + "2": 28, + "3": 38, + "4": 1, + "5": 3, + "6": 3, + "7": 44, + "8": true, + "9": true + }, + { + "0": 14584221614789315818, + "1": 29696, + "2": 29, + "3": 28, + "4": 1, + "5": 0, + "6": 0, + "7": 15, + "8": true, + "9": false + }, + { + "0": 7968688256206678783, + "1": 30720, + "2": 30, + "3": 28, + "4": 1, + "5": 2, + "6": 1, + "7": 9, + "8": true, + "9": true + }, + { + "0": 2195983971765588925, + "1": 31744, + "2": 31, + "3": 28, + "4": 1, + "5": 3, + "6": 1, + "7": 4, + "8": true, + "9": true + }, + { + "0": 8533237363532831991, + "1": 38912, + "2": 38, + "3": 28, + "4": 1, + "5": 3, + "6": 3, + "7": 32, + "8": true, + "9": true + }, + { + "0": 0, + "1": 45056, + "2": 44, + "3": 28, + "4": 1, + "5": 0, + "6": 0, + "7": 5, + "8": true, + "9": false + }, + { + "0": 5655139244129535392, + "1": 50176, + "2": 49, + "3": 28, + "4": 1, + "5": 0, + "6": 0, + "7": 10, + "8": true, + "9": false + }, + { + "0": 15462742133285018414, + "1": 51200, + "2": 50, + "3": 38, + "4": 1, + "5": 1, + "6": 0, + "7": 30, + "8": true, + "9": true + }, + { + "0": 9106713788407201067, + "1": 54272, + "2": 53, + "3": 28, + "4": 1, + "5": 3, + "6": 3, + "7": 14, + "8": true, + "9": true + }, + { + "0": 0, + "1": 55296, + "2": 54, + "3": 28, + "4": 2, + "5": 0, + "6": 0, + "7": 99, + "8": true, + "9": false + }, + { + "0": 0, + "1": 62464, + "2": 61, + "3": 28, + "4": 1, + "5": 0, + "6": 0, + "7": 51, + "8": true, + "9": false + } + ], + "0/53/9": 544200770, + "0/53/10": 68, + "0/53/11": 57, + "0/53/12": 158, + "0/53/13": 9, + "0/53/14": 66, + "0/53/15": 49, + "0/53/16": 3, + "0/53/17": 17, + "0/53/18": 36, + "0/53/19": 38, + "0/53/20": 33, + "0/53/21": 39, + "0/53/22": 240406, + "0/53/23": 214223, + "0/53/24": 26183, + "0/53/25": 214223, + "0/53/26": 203603, + "0/53/27": 26183, + "0/53/28": 240407, + "0/53/29": 0, + "0/53/30": 0, + "0/53/31": 0, + "0/53/32": 0, + "0/53/33": 408189, + "0/53/34": 10621, + "0/53/35": 0, + "0/53/36": 70745, + "0/53/37": 0, + "0/53/38": 1949, + "0/53/39": 1239481, + "0/53/40": 99469, + "0/53/41": 976396, + "0/53/42": 1046263, + "0/53/43": 0, + "0/53/44": 41, + "0/53/45": 0, + "0/53/46": 0, + "0/53/47": 0, + "0/53/48": 1, + "0/53/49": 21522, + "0/53/50": 0, + "0/53/51": 163615, + "0/53/52": 0, + "0/53/53": 8039, + "0/53/54": 0, + "0/53/55": 0, + "0/53/56": 111822352547840, + "0/53/57": 0, + "0/53/58": 0, + "0/53/59": { + "0": 672, + "1": 8335 + }, + "0/53/60": "AB//wA==", + "0/53/61": { + "0": true, + "1": false, + "2": true, + "3": true, + "4": true, + "5": true, + "6": false, + "7": true, + "8": true, + "9": true, + "10": true, + "11": true + }, + "0/53/62": [0, 0, 0, 0], + "0/53/65532": 15, + "0/53/65533": 1, + "0/53/65528": [], + "0/53/65529": [0], + "0/53/65530": [], + "0/53/65531": [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, + 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, + 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, + 57, 58, 59, 60, 61, 62, 65528, 65529, 65530, 65531, 65532, 65533 + ], + "0/60/0": 0, + "0/60/1": null, + "0/60/2": null, + "0/60/65532": 0, + "0/60/65533": 1, + "0/60/65528": [], + "0/60/65529": [0, 1, 2], + "0/60/65530": [], + "0/60/65531": [0, 1, 2, 65528, 65529, 65530, 65531, 65532, 65533], + "0/62/0": [ + { + "1": "FTABAQEkAgE3AyQTAhgmBIAigScmBYAlTTo3BiQVAiQRxRgkBwEkCAEwCUEE+6maBmkSYz6Mc9CPP3rVE6+GVAI1RSaoMuQPvtSHBroJwa2mFK7Aah+sESC00TJ2vzX7jiix1pooU7vKr7hAHDcKNQEoARgkAgE2AwQCBAEYMAQUfXAGsiTrIa0biWN7/3bBx6IQNycwBRR0PzXGsFYhV/yy0eOyHr2WB98K3hgwC0A6AV48fcu123c1UzRL9vZoUGrLYUe3fMtdk27EMXARmFoecygVw3UxOyRE1e7ovYyq1l/B+OS46cFn+Z1Op1TBGA==", + "2": "FTABAQEkAgE3AyQUARgmBIAigScmBYAlTTo3BiQTAhgkBwEkCAEwCUEE2Zdr5BSI2FoASW7GpgNmBbxI18Gw0g/s2d/3ZLNWQJo3+HNMCxP0f+lmfQPqTta6hH2eALCXvrOemwZwB4OVkDcKNQEpARgkAmAwBBR0PzXGsFYhV/yy0eOyHr2WB98K3jAFFCTX1BG9PZC96rn83WyNVu55l7B8GDALQC2XiH6ek61BOXlOMlWF4CQZkjKupEy4prJWFWaNGg+vcJ7sR/xBtfhfThZhg1Re1atY3aapbB6V2j4xJiCq9HgY", + "254": 18 + } + ], + "0/62/1": [ + { + "1": "BDT3GwcQ+jgb6JHKilDo0cIOCVRVzt/Qp1MGXzpJumBOSFenMDvr940AGy6NI4WfqROrVh9KmrroTnXEqOIhA6Y=", + "2": 4939, + "3": 2, + "4": 197, + "5": "", + "254": 18 + } + ], + "0/62/2": 5, + "0/62/3": 3, + "0/62/4": [ + "FTABAQAkAgE3AyYU2nLdAyYVar5MzhgmBLnNRi0kBQA3BiYU2nLdAyYVar5MzhgkBwEkCAEwCUEENrEEk8M5ztCYkE5UAh3jIAN89pc0KFJ/gbwBIWeN3Ws5aFKjFWCndluUHWDEWPtSMxWTrno8vATU3x8j+yycijcKNQEpARgkAmAwBBQErHkbm0I53zyvS+R5vrTzJR1doTAFFASseRubQjnfPK9L5Hm+tPMlHV2hGDALQLv4FZpuAoq/m0iIdjOY2OTPnm3JjQIWd4QLBf4ncy6uPlPhdDlvanQvCxSl7xaF/XW8j+EsWacZDK15mD4jzuQY", + "FTABAQAkAgE3AycUxxt9sfxycj8mFZkiBagYJgQNz0YtJAUANwYnFMcbfbH8cnI/JhWZIgWoGCQHASQIATAJQQTGlfTQVqZk2GnxHCh364hEd0J4+rUEblxiWQDmYIienGmHY50RviHxI+875LHFTo9rcntChj+TPxP00yUIw3yoNwo1ASkBGCQCYDAEFK5Ln3+cjAgPxBcWXXzMO1MEyW6oMAUUrkuff5yMCA/EFxZdfMw7UwTJbqgYMAtAleRSrdtPawWmPJ2A0t6EFlYTVKtqseAiuHxSwE+U4sEeL+QCO9OCT6f1bsTzD5KDjqTBlWPSjeUDfd5u61o30Bg=", + "FTABAQEkAgE3AyQUARgmBIAigScmBYAlTTo3BiQUARgkBwEkCAEwCUEENPcbBxD6OBvokcqKUOjRwg4JVFXO39CnUwZfOkm6YE5IV6cwO+v3jQAbLo0jhZ+pE6tWH0qauuhOdcSo4iEDpjcKNQEpARgkAmAwBBQk19QRvT2Qveq5/N1sjVbueZewfDAFFCTX1BG9PZC96rn83WyNVu55l7B8GDALQEUvBGKd7aRh6/0l82kua682xBcREAV7Xn4PFsZ7tEs7H4PYHnCZTzgSC7mqY2u0y2AhTztdJ7tCeffml9HQQGwY" + ], + "0/62/5": 18, + "0/62/65532": 0, + "0/62/65533": 1, + "0/62/65528": [1, 3, 5, 8], + "0/62/65529": [0, 2, 4, 6, 7, 9, 10, 11], + "0/62/65530": [], + "0/62/65531": [0, 1, 2, 3, 4, 5, 65528, 65529, 65530, 65531, 65532, 65533], + "0/63/0": [], + "0/63/1": [], + "0/63/2": 4, + "0/63/3": 3, + "0/63/65532": 0, + "0/63/65533": 1, + "0/63/65528": [2, 5], + "0/63/65529": [0, 1, 3, 4], + "0/63/65530": [], + "0/63/65531": [0, 1, 2, 3, 65528, 65529, 65530, 65531, 65532, 65533], + "0/64/0": [ + { + "0": "Vendor", + "1": "Inovelli" + }, + { + "0": "Product", + "1": "VTM31-SN" + } + ], + "0/64/65532": 0, + "0/64/65533": 1, + "0/64/65528": [], + "0/64/65529": [], + "0/64/65530": [], + "0/64/65531": [0, 65528, 65529, 65530, 65531, 65532, 65533], + "1/3/0": 0, + "1/3/1": 2, + "1/3/65532": 0, + "1/3/65533": 4, + "1/3/65528": [], + "1/3/65529": [0, 64], + "1/3/65530": [], + "1/3/65531": [0, 1, 65528, 65529, 65530, 65531, 65532, 65533], + "1/4/0": 128, + "1/4/65532": 1, + "1/4/65533": 4, + "1/4/65528": [0, 1, 2, 3], + "1/4/65529": [0, 1, 2, 3, 4, 5], + "1/4/65530": [], + "1/4/65531": [0, 65528, 65529, 65530, 65531, 65532, 65533], + "1/5/0": null, + "1/5/1": 0, + "1/5/2": 0, + "1/5/3": false, + "1/5/4": 128, + "1/5/65532": 1, + "1/5/65533": 4, + "1/5/65528": [0, 1, 2, 3, 4, 6], + "1/5/65529": [0, 1, 2, 3, 4, 5, 6], + "1/5/65530": [], + "1/5/65531": [0, 1, 2, 3, 4, 65528, 65529, 65530, 65531, 65532, 65533], + "1/6/0": false, + "1/6/16384": true, + "1/6/16385": 0, + "1/6/16386": 0, + "1/6/16387": null, + "1/6/65532": 1, + "1/6/65533": 4, + "1/6/65528": [], + "1/6/65529": [0, 1, 2, 64, 65, 66], + "1/6/65530": [], + "1/6/65531": [ + 0, 16384, 16385, 16386, 16387, 65528, 65529, 65530, 65531, 65532, 65533 + ], + "1/8/0": 1, + "1/8/1": 0, + "1/8/2": 1, + "1/8/3": 254, + "1/8/15": 0, + "1/8/16": 5, + "1/8/17": 137, + "1/8/18": 15, + "1/8/19": 5, + "1/8/20": 50, + "1/8/16384": null, + "1/8/65532": 3, + "1/8/65533": 5, + "1/8/65528": [], + "1/8/65529": [0, 1, 2, 3, 4, 5, 6, 7], + "1/8/65530": [], + "1/8/65531": [ + 0, 1, 2, 3, 15, 16, 17, 18, 19, 20, 16384, 65528, 65529, 65530, 65531, + 65532, 65533 + ], + "1/29/0": [ + { + "0": 257, + "1": 1 + } + ], + "1/29/1": [3, 4, 5, 6, 8, 29, 64, 80, 305134641], + "1/29/2": [], + "1/29/3": [], + "1/29/65532": 0, + "1/29/65533": 1, + "1/29/65528": [], + "1/29/65529": [], + "1/29/65530": [], + "1/29/65531": [0, 1, 2, 3, 65528, 65529, 65530, 65531, 65532, 65533], + "1/64/0": [ + { + "0": "DeviceType", + "1": "DimmableLight" + } + ], + "1/64/65532": 0, + "1/64/65533": 1, + "1/64/65528": [], + "1/64/65529": [], + "1/64/65530": [], + "1/64/65531": [0, 65528, 65529, 65530, 65531, 65532, 65533], + "1/80/0": "Switch Mode", + "1/80/1": 0, + "1/80/2": [ + { + "0": "OnOff+Single", + "1": 0, + "2": [ + { + "0": 0, + "1": 0 + } + ] + }, + { + "0": "OnOff+Dumb", + "1": 1, + "2": [ + { + "0": 0, + "1": 0 + } + ] + }, + { + "0": "OnOff+AUX", + "1": 2, + "2": [ + { + "0": 0, + "1": 0 + } + ] + }, + { + "0": "OnOff+Full Wave", + "1": 3, + "2": [ + { + "0": 0, + "1": 0 + } + ] + }, + { + "0": "Dimmer+Single", + "1": 4, + "2": [ + { + "0": 0, + "1": 0 + } + ] + }, + { + "0": "Dimmer+Dumb", + "1": 5, + "2": [ + { + "0": 0, + "1": 0 + } + ] + }, + { + "0": "Dimmer+Aux", + "1": 6, + "2": [ + { + "0": 0, + "1": 0 + } + ] + } + ], + "1/80/3": 4, + "1/80/65532": 0, + "1/80/65533": 1, + "1/80/65528": [], + "1/80/65529": [0], + "1/80/65530": [], + "1/80/65531": [0, 1, 2, 3, 65528, 65529, 65530, 65531, 65532, 65533], + "1/305134641/305070080": 1, + "1/305134641/305070081": 20, + "1/305134641/305070082": 127, + "1/305134641/305070083": 127, + "1/305134641/305070084": 127, + "1/305134641/305070085": 127, + "1/305134641/305070086": 127, + "1/305134641/305070087": 127, + "1/305134641/305070088": 127, + "1/305134641/305070089": 1, + "1/305134641/305070090": 255, + "1/305134641/305070091": false, + "1/305134641/305070092": 0, + "1/305134641/305070093": 255, + "1/305134641/305070094": 255, + "1/305134641/305070095": 255, + "1/305134641/305070097": 11, + "1/305134641/305070101": true, + "1/305134641/305070102": 0, + "1/305134641/305070106": 0, + "1/305134641/305070112": 30, + "1/305134641/305070113": false, + "1/305134641/305070130": 5, + "1/305134641/305070132": false, + "1/305134641/305070133": false, + "1/305134641/305070134": false, + "1/305134641/305070135": 254, + "1/305134641/305070136": 2, + "1/305134641/305070175": 35, + "1/305134641/305070176": 35, + "1/305134641/305070177": 33, + "1/305134641/305070178": 1, + "1/305134641/305070336": false, + "1/305134641/305070338": false, + "1/305134641/305070339": false, + "1/305134641/305070340": true, + "1/305134641/305070341": true, + "1/305134641/305070342": false, + "1/305134641/65532": 0, + "1/305134641/65533": 1, + "1/305134641/65528": [], + "1/305134641/65529": [305070081, 305070083, 305070276], + "1/305134641/65530": [], + "1/305134641/65531": [ + 65528, 65529, 65530, 65531, 305070080, 305070081, 305070082, 305070083, + 305070084, 305070085, 305070086, 305070087, 305070088, 305070089, + 305070090, 305070091, 305070092, 305070093, 305070094, 305070095, + 305070097, 305070101, 305070102, 305070106, 305070112, 305070113, + 305070130, 305070132, 305070133, 305070134, 305070135, 305070136, + 305070175, 305070176, 305070177, 305070178, 305070336, 305070338, + 305070339, 305070340, 305070341, 305070342, 65532, 65533 + ], + "2/3/0": 0, + "2/3/1": 2, + "2/3/65532": 0, + "2/3/65533": 4, + "2/3/65528": [], + "2/3/65529": [0, 64], + "2/3/65530": [], + "2/3/65531": [0, 1, 65528, 65529, 65530, 65531, 65532, 65533], + "2/29/0": [ + { + "0": 260, + "1": 1 + } + ], + "2/29/1": [3, 29, 30, 64, 80], + "2/29/2": [3, 6, 8], + "2/29/3": [], + "2/29/65532": 0, + "2/29/65533": 1, + "2/29/65528": [], + "2/29/65529": [], + "2/29/65530": [], + "2/29/65531": [0, 1, 2, 3, 65528, 65529, 65530, 65531, 65532, 65533], + "2/30/0": [], + "2/30/65532": 0, + "2/30/65533": 1, + "2/30/65528": [], + "2/30/65529": [], + "2/30/65530": [], + "2/30/65531": [0, 65528, 65529, 65530, 65531, 65532, 65533], + "2/64/0": [ + { + "0": "DeviceType", + "1": "DimmableSwitch" + } + ], + "2/64/65532": 0, + "2/64/65533": 1, + "2/64/65528": [], + "2/64/65529": [], + "2/64/65530": [], + "2/64/65531": [0, 65528, 65529, 65530, 65531, 65532, 65533], + "2/80/0": "Smart Bulb Mode", + "2/80/1": 0, + "2/80/2": [ + { + "0": "Smart Bulb Disable", + "1": 0, + "2": [ + { + "0": 0, + "1": 0 + } + ] + }, + { + "0": "Smart Bulb Enable", + "1": 1, + "2": [ + { + "0": 0, + "1": 0 + } + ] + } + ], + "2/80/3": 0, + "2/80/65532": 0, + "2/80/65533": 1, + "2/80/65528": [], + "2/80/65529": [0], + "2/80/65530": [], + "2/80/65531": [0, 1, 2, 3, 65528, 65529, 65530, 65531, 65532, 65533], + "3/3/0": 0, + "3/3/1": 2, + "3/3/65532": 0, + "3/3/65533": 4, + "3/3/65528": [], + "3/3/65529": [0, 64], + "3/3/65530": [], + "3/3/65531": [0, 1, 65528, 65529, 65530, 65531, 65532, 65533], + "3/29/0": [ + { + "0": 15, + "1": 1 + } + ], + "3/29/1": [3, 29, 59, 64, 80], + "3/29/2": [], + "3/29/3": [], + "3/29/65532": 0, + "3/29/65533": 1, + "3/29/65528": [], + "3/29/65529": [], + "3/29/65530": [], + "3/29/65531": [0, 1, 2, 3, 65528, 65529, 65530, 65531, 65532, 65533], + "3/59/0": 2, + "3/59/1": 0, + "3/59/2": 5, + "3/59/65532": 30, + "3/59/65533": 1, + "3/59/65528": [], + "3/59/65529": [], + "3/59/65530": [1, 2, 3, 4, 5, 6], + "3/59/65531": [0, 1, 2, 65528, 65529, 65530, 65531, 65532, 65533], + "3/64/0": [ + { + "0": "Button", + "1": "Up" + } + ], + "3/64/65532": 0, + "3/64/65533": 1, + "3/64/65528": [], + "3/64/65529": [], + "3/64/65530": [], + "3/64/65531": [0, 65528, 65529, 65530, 65531, 65532, 65533], + "3/80/0": "Dimming Edge", + "3/80/1": 0, + "3/80/2": [ + { + "0": "Leading", + "1": 0, + "2": [ + { + "0": 0, + "1": 0 + } + ] + }, + { + "0": "Trailing", + "1": 1, + "2": [ + { + "0": 0, + "1": 0 + } + ] + } + ], + "3/80/3": 0, + "3/80/65532": 0, + "3/80/65533": 1, + "3/80/65528": [], + "3/80/65529": [0], + "3/80/65530": [], + "3/80/65531": [0, 1, 2, 3, 65528, 65529, 65530, 65531, 65532, 65533], + "4/3/0": 0, + "4/3/1": 2, + "4/3/65532": 0, + "4/3/65533": 4, + "4/3/65528": [], + "4/3/65529": [0, 64], + "4/3/65530": [], + "4/3/65531": [0, 1, 65528, 65529, 65530, 65531, 65532, 65533], + "4/29/0": [ + { + "0": 15, + "1": 1 + } + ], + "4/29/1": [3, 29, 59, 64, 80], + "4/29/2": [], + "4/29/3": [], + "4/29/65532": 0, + "4/29/65533": 1, + "4/29/65528": [], + "4/29/65529": [], + "4/29/65530": [], + "4/29/65531": [0, 1, 2, 3, 65528, 65529, 65530, 65531, 65532, 65533], + "4/59/0": 2, + "4/59/1": 0, + "4/59/2": 5, + "4/59/65532": 30, + "4/59/65533": 1, + "4/59/65528": [], + "4/59/65529": [], + "4/59/65530": [1, 2, 3, 4, 5, 6], + "4/59/65531": [0, 1, 2, 65528, 65529, 65530, 65531, 65532, 65533], + "4/64/0": [ + { + "0": "Button", + "1": "Down" + } + ], + "4/64/65532": 0, + "4/64/65533": 1, + "4/64/65528": [], + "4/64/65529": [], + "4/64/65530": [], + "4/64/65531": [0, 65528, 65529, 65530, 65531, 65532, 65533], + "4/80/0": "Dimming Speed", + "4/80/1": 0, + "4/80/2": [ + { + "0": "Instant", + "1": 0, + "2": [ + { + "0": 0, + "1": 0 + } + ] + }, + { + "0": "500ms", + "1": 5, + "2": [ + { + "0": 0, + "1": 0 + } + ] + }, + { + "0": "800ms", + "1": 8, + "2": [ + { + "0": 0, + "1": 0 + } + ] + }, + { + "0": "1s", + "1": 10, + "2": [ + { + "0": 0, + "1": 0 + } + ] + }, + { + "0": "1.5s", + "1": 15, + "2": [ + { + "0": 0, + "1": 0 + } + ] + }, + { + "0": "2s", + "1": 20, + "2": [ + { + "0": 0, + "1": 0 + } + ] + }, + { + "0": "2.5s", + "1": 25, + "2": [ + { + "0": 0, + "1": 0 + } + ] + }, + { + "0": "3s", + "1": 30, + "2": [ + { + "0": 0, + "1": 0 + } + ] + }, + { + "0": "3.5s", + "1": 35, + "2": [ + { + "0": 0, + "1": 0 + } + ] + }, + { + "0": "4s", + "1": 40, + "2": [ + { + "0": 0, + "1": 0 + } + ] + }, + { + "0": "5s", + "1": 50, + "2": [ + { + "0": 0, + "1": 0 + } + ] + }, + { + "0": "6s", + "1": 60, + "2": [ + { + "0": 0, + "1": 0 + } + ] + }, + { + "0": "7s", + "1": 70, + "2": [ + { + "0": 0, + "1": 0 + } + ] + }, + { + "0": "8s", + "1": 80, + "2": [ + { + "0": 0, + "1": 0 + } + ] + }, + { + "0": "10s", + "1": 100, + "2": [ + { + "0": 0, + "1": 0 + } + ] + } + ], + "4/80/3": 20, + "4/80/65532": 0, + "4/80/65533": 1, + "4/80/65528": [], + "4/80/65529": [0], + "4/80/65530": [], + "4/80/65531": [0, 1, 2, 3, 65528, 65529, 65530, 65531, 65532, 65533], + "5/3/0": 0, + "5/3/1": 2, + "5/3/65532": 0, + "5/3/65533": 4, + "5/3/65528": [], + "5/3/65529": [0, 64], + "5/3/65530": [], + "5/3/65531": [0, 1, 65528, 65529, 65530, 65531, 65532, 65533], + "5/29/0": [ + { + "0": 15, + "1": 1 + } + ], + "5/29/1": [3, 29, 59, 64, 80], + "5/29/2": [], + "5/29/3": [], + "5/29/65532": 0, + "5/29/65533": 1, + "5/29/65528": [], + "5/29/65529": [], + "5/29/65530": [], + "5/29/65531": [0, 1, 2, 3, 65528, 65529, 65530, 65531, 65532, 65533], + "5/59/0": 2, + "5/59/1": 0, + "5/59/2": 5, + "5/59/65532": 30, + "5/59/65533": 1, + "5/59/65528": [], + "5/59/65529": [], + "5/59/65530": [1, 2, 3, 4, 5, 6], + "5/59/65531": [0, 1, 2, 65528, 65529, 65530, 65531, 65532, 65533], + "5/64/0": [ + { + "0": "Button", + "1": "Config" + } + ], + "5/64/65532": 0, + "5/64/65533": 1, + "5/64/65528": [], + "5/64/65529": [], + "5/64/65530": [], + "5/64/65531": [0, 65528, 65529, 65530, 65531, 65532, 65533], + "5/80/0": "Relay", + "5/80/1": 0, + "5/80/2": [ + { + "0": "Relay Click Enable", + "1": 0, + "2": [ + { + "0": 0, + "1": 0 + } + ] + }, + { + "0": "Relay Click Disable", + "1": 1, + "2": [ + { + "0": 0, + "1": 0 + } + ] + } + ], + "5/80/3": 1, + "5/80/65532": 0, + "5/80/65533": 1, + "5/80/65528": [], + "5/80/65529": [0], + "5/80/65530": [], + "5/80/65531": [0, 1, 2, 3, 65528, 65529, 65530, 65531, 65532, 65533], + "6/3/0": 0, + "6/3/1": 2, + "6/3/65532": 0, + "6/3/65533": 4, + "6/3/65528": [], + "6/3/65529": [0, 64], + "6/3/65530": [], + "6/3/65531": [0, 1, 65528, 65529, 65530, 65531, 65532, 65533], + "6/4/0": 128, + "6/4/65532": 1, + "6/4/65533": 4, + "6/4/65528": [0, 1, 2, 3], + "6/4/65529": [0, 1, 2, 3, 4, 5], + "6/4/65530": [], + "6/4/65531": [0, 65528, 65529, 65530, 65531, 65532, 65533], + "6/5/0": null, + "6/5/1": 0, + "6/5/2": 0, + "6/5/3": false, + "6/5/4": 128, + "6/5/65532": 0, + "6/5/65533": 4, + "6/5/65528": [0, 1, 2, 3, 4, 6], + "6/5/65529": [0, 1, 2, 3, 4, 5, 6], + "6/5/65530": [], + "6/5/65531": [0, 1, 2, 3, 4, 65528, 65529, 65530, 65531, 65532, 65533], + "6/6/0": false, + "6/6/16384": true, + "6/6/16385": 0, + "6/6/16386": 0, + "6/6/16387": 0, + "6/6/65532": 1, + "6/6/65533": 4, + "6/6/65528": [], + "6/6/65529": [0, 1, 2, 64, 65, 66], + "6/6/65530": [], + "6/6/65531": [ + 0, 16384, 16385, 16386, 16387, 65528, 65529, 65530, 65531, 65532, 65533 + ], + "6/8/0": 224, + "6/8/1": 0, + "6/8/2": 1, + "6/8/3": 254, + "6/8/15": 0, + "6/8/17": 254, + "6/8/16384": 128, + "6/8/65532": 0, + "6/8/65533": 5, + "6/8/65528": [], + "6/8/65529": [0, 1, 2, 3, 4, 5, 6, 7], + "6/8/65530": [], + "6/8/65531": [ + 0, 1, 2, 3, 15, 17, 16384, 65528, 65529, 65530, 65531, 65532, 65533 + ], + "6/29/0": [ + { + "0": 269, + "1": 1 + } + ], + "6/29/1": [3, 4, 5, 6, 8, 29, 64, 80, 768], + "6/29/2": [], + "6/29/3": [], + "6/29/65532": 0, + "6/29/65533": 1, + "6/29/65528": [], + "6/29/65529": [], + "6/29/65530": [], + "6/29/65531": [0, 1, 2, 3, 65528, 65529, 65530, 65531, 65532, 65533], + "6/64/0": [ + { + "0": "DeviceType", + "1": "DimmableLight" + }, + { + "0": "Light", + "1": "LED Bar" + } + ], + "6/64/65532": 0, + "6/64/65533": 1, + "6/64/65528": [], + "6/64/65529": [], + "6/64/65530": [], + "6/64/65531": [0, 65528, 65529, 65530, 65531, 65532, 65533], + "6/80/0": "LED Color", + "6/80/1": 0, + "6/80/2": [ + { + "0": "Red", + "1": 0, + "2": [ + { + "0": 0, + "1": 0 + } + ] + }, + { + "0": "Orange", + "1": 1, + "2": [ + { + "0": 0, + "1": 0 + } + ] + }, + { + "0": "Lemon", + "1": 2, + "2": [ + { + "0": 0, + "1": 0 + } + ] + }, + { + "0": "Lime", + "1": 3, + "2": [ + { + "0": 0, + "1": 0 + } + ] + }, + { + "0": "Green", + "1": 4, + "2": [ + { + "0": 0, + "1": 0 + } + ] + }, + { + "0": "Teal", + "1": 5, + "2": [ + { + "0": 0, + "1": 0 + } + ] + }, + { + "0": "Cyan", + "1": 6, + "2": [ + { + "0": 0, + "1": 0 + } + ] + }, + { + "0": "Aqua", + "1": 7, + "2": [ + { + "0": 0, + "1": 0 + } + ] + }, + { + "0": "Blue", + "1": 8, + "2": [ + { + "0": 0, + "1": 0 + } + ] + }, + { + "0": "Violet", + "1": 9, + "2": [ + { + "0": 0, + "1": 0 + } + ] + }, + { + "0": "Magenta", + "1": 10, + "2": [ + { + "0": 0, + "1": 0 + } + ] + }, + { + "0": "Pink", + "1": 11, + "2": [ + { + "0": 0, + "1": 0 + } + ] + }, + { + "0": "White", + "1": 12, + "2": [ + { + "0": 0, + "1": 0 + } + ] + } + ], + "6/80/3": 2, + "6/80/65532": 0, + "6/80/65533": 1, + "6/80/65528": [], + "6/80/65529": [0], + "6/80/65530": [], + "6/80/65531": [0, 1, 2, 3, 65528, 65529, 65530, 65531, 65532, 65533], + "6/768/0": 6, + "6/768/1": 170, + "6/768/2": 0, + "6/768/3": 24939, + "6/768/4": 24701, + "6/768/7": 500, + "6/768/8": 0, + "6/768/15": 0, + "6/768/16": 0, + "6/768/16385": 0, + "6/768/16394": 25, + "6/768/16395": 0, + "6/768/16396": 65279, + "6/768/16397": 0, + "6/768/16400": 0, + "6/768/65532": 25, + "6/768/65533": 5, + "6/768/65528": [], + "6/768/65529": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 71, 75, 76], + "6/768/65530": [], + "6/768/65531": [ + 0, 1, 2, 3, 4, 7, 8, 15, 16, 16385, 16394, 16395, 16396, 16397, 16400, + 65528, 65529, 65530, 65531, 65532, 65533 + ] + }, + "attribute_subscriptions": [] +} diff --git a/tests/components/matter/fixtures/nodes/on-off-plugin-unit.json b/tests/components/matter/fixtures/nodes/on-off-plugin-unit.json index 8d523f5443a..3b4831a7485 100644 --- a/tests/components/matter/fixtures/nodes/on-off-plugin-unit.json +++ b/tests/components/matter/fixtures/nodes/on-off-plugin-unit.json @@ -24,7 +24,7 @@ "0/40/0": 1, "0/40/1": "Nabu Casa", "0/40/2": 65521, - "0/40/3": "Mock OnOffPluginUnit (powerplug/switch)", + "0/40/3": "Mock OnOffPluginUnit", "0/40/4": 32768, "0/40/5": "", "0/40/6": "XX", diff --git a/tests/components/matter/test_adapter.py b/tests/components/matter/test_adapter.py index 16a7ec3a780..da2ef179c44 100644 --- a/tests/components/matter/test_adapter.py +++ b/tests/components/matter/test_adapter.py @@ -81,7 +81,7 @@ async def test_device_registry_single_node_device_alt( assert entry is not None # test name is derived from productName (because nodeLabel is absent) - assert entry.name == "Mock OnOffPluginUnit (powerplug/switch)" + assert entry.name == "Mock OnOffPluginUnit" # test serial id NOT present as additional identifier assert (DOMAIN, "serial_TEST_SN") not in entry.identifiers @@ -163,13 +163,13 @@ async def test_node_added_subscription( ) ) - entity_state = hass.states.get("light.mock_onoff_light") + entity_state = hass.states.get("light.mock_onoff_light_light") assert not entity_state node_added_callback(EventType.NODE_ADDED, node) await hass.async_block_till_done() - entity_state = hass.states.get("light.mock_onoff_light") + entity_state = hass.states.get("light.mock_onoff_light_light") assert entity_state @@ -187,6 +187,24 @@ async def test_device_registry_single_node_composed_device( assert len(dev_reg.devices) == 1 +async def test_multi_endpoint_name( + hass: HomeAssistant, + matter_client: MagicMock, +) -> None: + """Test that the entity name gets postfixed if the device has multiple primary endpoints.""" + await setup_integration_with_node_fixture( + hass, + "multi-endpoint-light", + matter_client, + ) + entity_state = hass.states.get("light.inovelli_light_1") + assert entity_state + assert entity_state.name == "Inovelli Light (1)" + entity_state = hass.states.get("light.inovelli_light_6") + assert entity_state + assert entity_state.name == "Inovelli Light (6)" + + async def test_get_clean_name_() -> None: """Test get_clean_name helper. diff --git a/tests/components/matter/test_climate.py b/tests/components/matter/test_climate.py index 2150c733700..6a4cf34a640 100644 --- a/tests/components/matter/test_climate.py +++ b/tests/components/matter/test_climate.py @@ -44,7 +44,7 @@ async def test_thermostat_base( ) -> None: """Test thermostat base attributes and state updates.""" # test entity attributes - state = hass.states.get("climate.longan_link_hvac") + state = hass.states.get("climate.longan_link_hvac_thermostat") assert state assert state.attributes["min_temp"] == 7 assert state.attributes["max_temp"] == 35 @@ -66,7 +66,7 @@ async def test_thermostat_base( set_node_attribute(thermostat, 1, 513, 5, 1600) set_node_attribute(thermostat, 1, 513, 6, 3000) await trigger_subscription_callback(hass, matter_client) - state = hass.states.get("climate.longan_link_hvac") + state = hass.states.get("climate.longan_link_hvac_thermostat") assert state assert state.attributes["min_temp"] == 16 assert state.attributes["max_temp"] == 30 @@ -80,56 +80,56 @@ async def test_thermostat_base( # test system mode update from device set_node_attribute(thermostat, 1, 513, 28, 0) await trigger_subscription_callback(hass, matter_client) - state = hass.states.get("climate.longan_link_hvac") + state = hass.states.get("climate.longan_link_hvac_thermostat") assert state assert state.state == HVACMode.OFF # test running state update from device set_node_attribute(thermostat, 1, 513, 41, 1) await trigger_subscription_callback(hass, matter_client) - state = hass.states.get("climate.longan_link_hvac") + state = hass.states.get("climate.longan_link_hvac_thermostat") assert state assert state.attributes["hvac_action"] == HVACAction.HEATING set_node_attribute(thermostat, 1, 513, 41, 8) await trigger_subscription_callback(hass, matter_client) - state = hass.states.get("climate.longan_link_hvac") + state = hass.states.get("climate.longan_link_hvac_thermostat") assert state assert state.attributes["hvac_action"] == HVACAction.HEATING set_node_attribute(thermostat, 1, 513, 41, 2) await trigger_subscription_callback(hass, matter_client) - state = hass.states.get("climate.longan_link_hvac") + state = hass.states.get("climate.longan_link_hvac_thermostat") assert state assert state.attributes["hvac_action"] == HVACAction.COOLING set_node_attribute(thermostat, 1, 513, 41, 16) await trigger_subscription_callback(hass, matter_client) - state = hass.states.get("climate.longan_link_hvac") + state = hass.states.get("climate.longan_link_hvac_thermostat") assert state assert state.attributes["hvac_action"] == HVACAction.COOLING set_node_attribute(thermostat, 1, 513, 41, 4) await trigger_subscription_callback(hass, matter_client) - state = hass.states.get("climate.longan_link_hvac") + state = hass.states.get("climate.longan_link_hvac_thermostat") assert state assert state.attributes["hvac_action"] == HVACAction.FAN set_node_attribute(thermostat, 1, 513, 41, 32) await trigger_subscription_callback(hass, matter_client) - state = hass.states.get("climate.longan_link_hvac") + state = hass.states.get("climate.longan_link_hvac_thermostat") assert state assert state.attributes["hvac_action"] == HVACAction.FAN set_node_attribute(thermostat, 1, 513, 41, 64) await trigger_subscription_callback(hass, matter_client) - state = hass.states.get("climate.longan_link_hvac") + state = hass.states.get("climate.longan_link_hvac_thermostat") assert state assert state.attributes["hvac_action"] == HVACAction.FAN set_node_attribute(thermostat, 1, 513, 41, 66) await trigger_subscription_callback(hass, matter_client) - state = hass.states.get("climate.longan_link_hvac") + state = hass.states.get("climate.longan_link_hvac_thermostat") assert state assert state.attributes["hvac_action"] == HVACAction.OFF @@ -137,7 +137,7 @@ async def test_thermostat_base( set_node_attribute(thermostat, 1, 513, 28, 4) await trigger_subscription_callback(hass, matter_client) - state = hass.states.get("climate.longan_link_hvac") + state = hass.states.get("climate.longan_link_hvac_thermostat") assert state assert state.state == HVACMode.HEAT @@ -145,7 +145,7 @@ async def test_thermostat_base( set_node_attribute(thermostat, 1, 513, 18, 2000) await trigger_subscription_callback(hass, matter_client) - state = hass.states.get("climate.longan_link_hvac") + state = hass.states.get("climate.longan_link_hvac_thermostat") assert state assert state.attributes["temperature"] == 20 @@ -159,14 +159,14 @@ async def test_thermostat_service_calls( ) -> None: """Test climate platform service calls.""" # test single-setpoint temperature adjustment when cool mode is active - state = hass.states.get("climate.longan_link_hvac") + state = hass.states.get("climate.longan_link_hvac_thermostat") assert state assert state.state == HVACMode.COOL await hass.services.async_call( "climate", "set_temperature", { - "entity_id": "climate.longan_link_hvac", + "entity_id": "climate.longan_link_hvac_thermostat", "temperature": 25, }, blocking=True, @@ -187,7 +187,7 @@ async def test_thermostat_service_calls( "climate", "set_temperature", { - "entity_id": "climate.longan_link_hvac", + "entity_id": "climate.longan_link_hvac_thermostat", "temperature": 25, }, blocking=True, @@ -199,7 +199,7 @@ async def test_thermostat_service_calls( # test single-setpoint temperature adjustment when heat mode is active set_node_attribute(thermostat, 1, 513, 28, 4) await trigger_subscription_callback(hass, matter_client) - state = hass.states.get("climate.longan_link_hvac") + state = hass.states.get("climate.longan_link_hvac_thermostat") assert state assert state.state == HVACMode.HEAT @@ -207,7 +207,7 @@ async def test_thermostat_service_calls( "climate", "set_temperature", { - "entity_id": "climate.longan_link_hvac", + "entity_id": "climate.longan_link_hvac_thermostat", "temperature": 20, }, blocking=True, @@ -224,7 +224,7 @@ async def test_thermostat_service_calls( # test dual setpoint temperature adjustments when heat_cool mode is active set_node_attribute(thermostat, 1, 513, 28, 1) await trigger_subscription_callback(hass, matter_client) - state = hass.states.get("climate.longan_link_hvac") + state = hass.states.get("climate.longan_link_hvac_thermostat") assert state assert state.state == HVACMode.HEAT_COOL @@ -232,7 +232,7 @@ async def test_thermostat_service_calls( "climate", "set_temperature", { - "entity_id": "climate.longan_link_hvac", + "entity_id": "climate.longan_link_hvac_thermostat", "target_temp_low": 10, "target_temp_high": 30, }, @@ -257,7 +257,7 @@ async def test_thermostat_service_calls( "climate", "set_hvac_mode", { - "entity_id": "climate.longan_link_hvac", + "entity_id": "climate.longan_link_hvac_thermostat", "hvac_mode": HVACMode.HEAT, }, blocking=True, @@ -281,7 +281,7 @@ async def test_thermostat_service_calls( "climate", "set_temperature", { - "entity_id": "climate.longan_link_hvac", + "entity_id": "climate.longan_link_hvac_thermostat", "temperature": 22, "hvac_mode": HVACMode.COOL, }, @@ -312,7 +312,7 @@ async def test_room_airconditioner( room_airconditioner: MatterNode, ) -> None: """Test if a climate entity is created for a Room Airconditioner device.""" - state = hass.states.get("climate.room_airconditioner") + state = hass.states.get("climate.room_airconditioner_thermostat") assert state assert state.attributes["current_temperature"] == 20 assert state.attributes["min_temp"] == 16 @@ -335,13 +335,13 @@ async def test_room_airconditioner( # test fan-only hvac mode set_node_attribute(room_airconditioner, 1, 513, 28, 7) await trigger_subscription_callback(hass, matter_client) - state = hass.states.get("climate.room_airconditioner") + state = hass.states.get("climate.room_airconditioner_thermostat") assert state assert state.state == HVACMode.FAN_ONLY # test dry hvac mode set_node_attribute(room_airconditioner, 1, 513, 28, 8) await trigger_subscription_callback(hass, matter_client) - state = hass.states.get("climate.room_airconditioner") + state = hass.states.get("climate.room_airconditioner_thermostat") assert state assert state.state == HVACMode.DRY diff --git a/tests/components/matter/test_cover.py b/tests/components/matter/test_cover.py index ff6e933a1ab..f526205234d 100644 --- a/tests/components/matter/test_cover.py +++ b/tests/components/matter/test_cover.py @@ -27,11 +27,11 @@ from .common import ( @pytest.mark.parametrize( ("fixture", "entity_id"), [ - ("window-covering_lift", "cover.mock_lift_window_covering"), - ("window-covering_pa-lift", "cover.longan_link_wncv_da01"), - ("window-covering_tilt", "cover.mock_tilt_window_covering"), - ("window-covering_pa-tilt", "cover.mock_pa_tilt_window_covering"), - ("window-covering_full", "cover.mock_full_window_covering"), + ("window-covering_lift", "cover.mock_lift_window_covering_cover"), + ("window-covering_pa-lift", "cover.longan_link_wncv_da01_cover"), + ("window-covering_tilt", "cover.mock_tilt_window_covering_cover"), + ("window-covering_pa-tilt", "cover.mock_pa_tilt_window_covering_cover"), + ("window-covering_full", "cover.mock_full_window_covering_cover"), ], ) async def test_cover( @@ -105,9 +105,9 @@ async def test_cover( @pytest.mark.parametrize( ("fixture", "entity_id"), [ - ("window-covering_lift", "cover.mock_lift_window_covering"), - ("window-covering_pa-lift", "cover.longan_link_wncv_da01"), - ("window-covering_full", "cover.mock_full_window_covering"), + ("window-covering_lift", "cover.mock_lift_window_covering_cover"), + ("window-covering_pa-lift", "cover.longan_link_wncv_da01_cover"), + ("window-covering_full", "cover.mock_full_window_covering_cover"), ], ) async def test_cover_lift( @@ -162,7 +162,7 @@ async def test_cover_lift( @pytest.mark.parametrize( ("fixture", "entity_id"), [ - ("window-covering_lift", "cover.mock_lift_window_covering"), + ("window-covering_lift", "cover.mock_lift_window_covering_cover"), ], ) async def test_cover_lift_only( @@ -207,7 +207,7 @@ async def test_cover_lift_only( @pytest.mark.parametrize( ("fixture", "entity_id"), [ - ("window-covering_pa-lift", "cover.longan_link_wncv_da01"), + ("window-covering_pa-lift", "cover.longan_link_wncv_da01_cover"), ], ) async def test_cover_position_aware_lift( @@ -259,9 +259,9 @@ async def test_cover_position_aware_lift( @pytest.mark.parametrize( ("fixture", "entity_id"), [ - ("window-covering_tilt", "cover.mock_tilt_window_covering"), - ("window-covering_pa-tilt", "cover.mock_pa_tilt_window_covering"), - ("window-covering_full", "cover.mock_full_window_covering"), + ("window-covering_tilt", "cover.mock_tilt_window_covering_cover"), + ("window-covering_pa-tilt", "cover.mock_pa_tilt_window_covering_cover"), + ("window-covering_full", "cover.mock_full_window_covering_cover"), ], ) async def test_cover_tilt( @@ -317,7 +317,7 @@ async def test_cover_tilt( @pytest.mark.parametrize( ("fixture", "entity_id"), [ - ("window-covering_tilt", "cover.mock_tilt_window_covering"), + ("window-covering_tilt", "cover.mock_tilt_window_covering_cover"), ], ) async def test_cover_tilt_only( @@ -360,7 +360,7 @@ async def test_cover_tilt_only( @pytest.mark.parametrize( ("fixture", "entity_id"), [ - ("window-covering_pa-tilt", "cover.mock_pa_tilt_window_covering"), + ("window-covering_pa-tilt", "cover.mock_pa_tilt_window_covering_cover"), ], ) async def test_cover_position_aware_tilt( @@ -410,7 +410,7 @@ async def test_cover_full_features( "window-covering_full", matter_client, ) - entity_id = "cover.mock_full_window_covering" + entity_id = "cover.mock_full_window_covering_cover" state = hass.states.get(entity_id) assert state diff --git a/tests/components/matter/test_door_lock.py b/tests/components/matter/test_door_lock.py index 6e0e0846ad5..a0664612aba 100644 --- a/tests/components/matter/test_door_lock.py +++ b/tests/components/matter/test_door_lock.py @@ -34,7 +34,7 @@ async def test_lock( "lock", "unlock", { - "entity_id": "lock.mock_door_lock", + "entity_id": "lock.mock_door_lock_lock", }, blocking=True, ) @@ -52,7 +52,7 @@ async def test_lock( "lock", "lock", { - "entity_id": "lock.mock_door_lock", + "entity_id": "lock.mock_door_lock_lock", }, blocking=True, ) @@ -66,35 +66,35 @@ async def test_lock( ) matter_client.send_device_command.reset_mock() - state = hass.states.get("lock.mock_door_lock") + state = hass.states.get("lock.mock_door_lock_lock") assert state assert state.state == STATE_LOCKED set_node_attribute(door_lock, 1, 257, 0, 0) await trigger_subscription_callback(hass, matter_client) - state = hass.states.get("lock.mock_door_lock") + state = hass.states.get("lock.mock_door_lock_lock") assert state assert state.state == STATE_UNLOCKING set_node_attribute(door_lock, 1, 257, 0, 2) await trigger_subscription_callback(hass, matter_client) - state = hass.states.get("lock.mock_door_lock") + state = hass.states.get("lock.mock_door_lock_lock") assert state assert state.state == STATE_UNLOCKED set_node_attribute(door_lock, 1, 257, 0, 0) await trigger_subscription_callback(hass, matter_client) - state = hass.states.get("lock.mock_door_lock") + state = hass.states.get("lock.mock_door_lock_lock") assert state assert state.state == STATE_LOCKING set_node_attribute(door_lock, 1, 257, 0, None) await trigger_subscription_callback(hass, matter_client) - state = hass.states.get("lock.mock_door_lock") + state = hass.states.get("lock.mock_door_lock_lock") assert state assert state.state == STATE_UNKNOWN @@ -122,7 +122,7 @@ async def test_lock_requires_pin( await hass.services.async_call( "lock", "lock", - {"entity_id": "lock.mock_door_lock", ATTR_CODE: "1234"}, + {"entity_id": "lock.mock_door_lock_lock", ATTR_CODE: "1234"}, blocking=True, ) @@ -131,7 +131,7 @@ async def test_lock_requires_pin( await hass.services.async_call( "lock", "lock", - {"entity_id": "lock.mock_door_lock", ATTR_CODE: code}, + {"entity_id": "lock.mock_door_lock_lock", ATTR_CODE: code}, blocking=True, ) assert matter_client.send_device_command.call_count == 1 @@ -145,13 +145,13 @@ async def test_lock_requires_pin( # Lock door using default code default_code = "7654321" entity_registry.async_update_entity_options( - "lock.mock_door_lock", "lock", {"default_code": default_code} + "lock.mock_door_lock_lock", "lock", {"default_code": default_code} ) await trigger_subscription_callback(hass, matter_client) await hass.services.async_call( "lock", "lock", - {"entity_id": "lock.mock_door_lock"}, + {"entity_id": "lock.mock_door_lock_lock"}, blocking=True, ) assert matter_client.send_device_command.call_count == 2 @@ -171,7 +171,7 @@ async def test_lock_with_unbolt( door_lock_with_unbolt: MatterNode, ) -> None: """Test door lock.""" - state = hass.states.get("lock.mock_door_lock") + state = hass.states.get("lock.mock_door_lock_lock") assert state assert state.state == STATE_LOCKED assert state.attributes["supported_features"] & LockEntityFeature.OPEN @@ -180,7 +180,7 @@ async def test_lock_with_unbolt( "lock", "unlock", { - "entity_id": "lock.mock_door_lock", + "entity_id": "lock.mock_door_lock_lock", }, blocking=True, ) @@ -198,7 +198,7 @@ async def test_lock_with_unbolt( "lock", "open", { - "entity_id": "lock.mock_door_lock", + "entity_id": "lock.mock_door_lock_lock", }, blocking=True, ) @@ -213,6 +213,6 @@ async def test_lock_with_unbolt( set_node_attribute(door_lock_with_unbolt, 1, 257, 3, 0) await trigger_subscription_callback(hass, matter_client) - state = hass.states.get("lock.mock_door_lock") + state = hass.states.get("lock.mock_door_lock_lock") assert state assert state.state == STATE_OPEN diff --git a/tests/components/matter/test_event.py b/tests/components/matter/test_event.py index 2bdcfb6adb7..a7bd7c91f7b 100644 --- a/tests/components/matter/test_event.py +++ b/tests/components/matter/test_event.py @@ -40,11 +40,10 @@ async def test_generic_switch_node( generic_switch_node: MatterNode, ) -> None: """Test event entity for a GenericSwitch node.""" - state = hass.states.get("event.mock_generic_switch") + state = hass.states.get("event.mock_generic_switch_button") assert state assert state.state == "unknown" - # the switch endpoint has no label so the entity name should be the device itself - assert state.name == "Mock Generic Switch" + assert state.name == "Mock Generic Switch Button" # check event_types from featuremap 30 assert state.attributes[ATTR_EVENT_TYPES] == [ "initial_press", @@ -71,7 +70,7 @@ async def test_generic_switch_node( data=None, ), ) - state = hass.states.get("event.mock_generic_switch") + state = hass.states.get("event.mock_generic_switch_button") assert state.attributes[ATTR_EVENT_TYPE] == "initial_press" # trigger firing a multi press event await trigger_subscription_callback( @@ -90,7 +89,7 @@ async def test_generic_switch_node( data={"NewPosition": 3}, ), ) - state = hass.states.get("event.mock_generic_switch") + state = hass.states.get("event.mock_generic_switch_button") assert state.attributes[ATTR_EVENT_TYPE] == "multi_press_ongoing" assert state.attributes["NewPosition"] == 3 @@ -106,8 +105,8 @@ async def test_generic_switch_multi_node( state_button_1 = hass.states.get("event.mock_generic_switch_button_1") assert state_button_1 assert state_button_1.state == "unknown" - # name should be 'DeviceName Button 1' due to the label set to just '1' - assert state_button_1.name == "Mock Generic Switch Button 1" + # name should be 'DeviceName Button (1)' due to the label set to just '1' + assert state_button_1.name == "Mock Generic Switch Button (1)" # check event_types from featuremap 14 assert state_button_1.attributes[ATTR_EVENT_TYPES] == [ "initial_press", diff --git a/tests/components/matter/test_fan.py b/tests/components/matter/test_fan.py index 3c4a990018b..30bd7f4a009 100644 --- a/tests/components/matter/test_fan.py +++ b/tests/components/matter/test_fan.py @@ -45,7 +45,7 @@ async def test_fan_base( air_purifier: MatterNode, ) -> None: """Test Fan platform.""" - entity_id = "fan.air_purifier" + entity_id = "fan.air_purifier_fan" state = hass.states.get(entity_id) assert state assert state.attributes["preset_modes"] == [ @@ -100,7 +100,7 @@ async def test_fan_turn_on_with_percentage( air_purifier: MatterNode, ) -> None: """Test turning on the fan with a specific percentage.""" - entity_id = "fan.air_purifier" + entity_id = "fan.air_purifier_fan" await hass.services.async_call( FAN_DOMAIN, SERVICE_TURN_ON, @@ -121,7 +121,7 @@ async def test_fan_turn_on_with_preset_mode( air_purifier: MatterNode, ) -> None: """Test turning on the fan with a specific preset mode.""" - entity_id = "fan.air_purifier" + entity_id = "fan.air_purifier_fan" await hass.services.async_call( FAN_DOMAIN, SERVICE_TURN_ON, @@ -193,7 +193,7 @@ async def test_fan_turn_off( air_purifier: MatterNode, ) -> None: """Test turning off the fan.""" - entity_id = "fan.air_purifier" + entity_id = "fan.air_purifier_fan" await hass.services.async_call( FAN_DOMAIN, SERVICE_TURN_OFF, @@ -235,7 +235,7 @@ async def test_fan_oscillate( air_purifier: MatterNode, ) -> None: """Test oscillating the fan.""" - entity_id = "fan.air_purifier" + entity_id = "fan.air_purifier_fan" for oscillating, value in ((True, 1), (False, 0)): await hass.services.async_call( FAN_DOMAIN, @@ -258,7 +258,7 @@ async def test_fan_set_direction( air_purifier: MatterNode, ) -> None: """Test oscillating the fan.""" - entity_id = "fan.air_purifier" + entity_id = "fan.air_purifier_fan" for direction, value in ((DIRECTION_FORWARD, 0), (DIRECTION_REVERSE, 1)): await hass.services.async_call( FAN_DOMAIN, diff --git a/tests/components/matter/test_init.py b/tests/components/matter/test_init.py index e3d8e799658..d3712f24d12 100644 --- a/tests/components/matter/test_init.py +++ b/tests/components/matter/test_init.py @@ -69,7 +69,7 @@ async def test_entry_setup_unload( assert matter_client.connect.call_count == 1 assert entry.state is ConfigEntryState.LOADED - entity_state = hass.states.get("light.mock_onoff_light") + entity_state = hass.states.get("light.mock_onoff_light_light") assert entity_state assert entity_state.state != STATE_UNAVAILABLE @@ -77,7 +77,7 @@ async def test_entry_setup_unload( assert matter_client.disconnect.call_count == 1 assert entry.state is ConfigEntryState.NOT_LOADED - entity_state = hass.states.get("light.mock_onoff_light") + entity_state = hass.states.get("light.mock_onoff_light_light") assert entity_state assert entity_state.state == STATE_UNAVAILABLE @@ -625,7 +625,7 @@ async def test_remove_config_entry_device( device_entry = dr.async_entries_for_config_entry( device_registry, config_entry.entry_id )[0] - entity_id = "light.m5stamp_lighting_app" + entity_id = "light.m5stamp_lighting_app_light" assert device_entry assert entity_registry.async_get(entity_id) diff --git a/tests/components/matter/test_light.py b/tests/components/matter/test_light.py index 2589e041b3b..4fd73b6457b 100644 --- a/tests/components/matter/test_light.py +++ b/tests/components/matter/test_light.py @@ -22,17 +22,17 @@ from .common import ( [ ( "extended-color-light", - "light.mock_extended_color_light", + "light.mock_extended_color_light_light", ["color_temp", "hs", "xy"], ), ( "color-temperature-light", - "light.mock_color_temperature_light", + "light.mock_color_temperature_light_light", ["color_temp"], ), - ("dimmable-light", "light.mock_dimmable_light", ["brightness"]), - ("onoff-light", "light.mock_onoff_light", ["onoff"]), - ("onoff-light-with-levelcontrol-present", "light.d215s", ["onoff"]), + ("dimmable-light", "light.mock_dimmable_light_light", ["brightness"]), + ("onoff-light", "light.mock_onoff_light_light", ["onoff"]), + ("onoff-light-with-levelcontrol-present", "light.d215s_light", ["onoff"]), ], ) async def test_light_turn_on_off( @@ -113,10 +113,10 @@ async def test_light_turn_on_off( @pytest.mark.parametrize( ("fixture", "entity_id"), [ - ("extended-color-light", "light.mock_extended_color_light"), - ("color-temperature-light", "light.mock_color_temperature_light"), - ("dimmable-light", "light.mock_dimmable_light"), - ("dimmable-plugin-unit", "light.dimmable_plugin_unit"), + ("extended-color-light", "light.mock_extended_color_light_light"), + ("color-temperature-light", "light.mock_color_temperature_light_light"), + ("dimmable-light", "light.mock_dimmable_light_light"), + ("dimmable-plugin-unit", "light.dimmable_plugin_unit_light"), ], ) async def test_dimmable_light( @@ -189,8 +189,8 @@ async def test_dimmable_light( @pytest.mark.parametrize( ("fixture", "entity_id"), [ - ("extended-color-light", "light.mock_extended_color_light"), - ("color-temperature-light", "light.mock_color_temperature_light"), + ("extended-color-light", "light.mock_extended_color_light_light"), + ("color-temperature-light", "light.mock_color_temperature_light_light"), ], ) async def test_color_temperature_light( @@ -287,7 +287,7 @@ async def test_color_temperature_light( @pytest.mark.parametrize( ("fixture", "entity_id"), [ - ("extended-color-light", "light.mock_extended_color_light"), + ("extended-color-light", "light.mock_extended_color_light_light"), ], ) async def test_extended_color_light( diff --git a/tests/components/matter/test_switch.py b/tests/components/matter/test_switch.py index 5fc23fa7b34..0327e9ea5fe 100644 --- a/tests/components/matter/test_switch.py +++ b/tests/components/matter/test_switch.py @@ -41,7 +41,7 @@ async def test_turn_on( powerplug_node: MatterNode, ) -> None: """Test turning on a switch.""" - state = hass.states.get("switch.mock_onoffpluginunit_powerplug_switch") + state = hass.states.get("switch.mock_onoffpluginunit_switch") assert state assert state.state == "off" @@ -49,7 +49,7 @@ async def test_turn_on( "switch", "turn_on", { - "entity_id": "switch.mock_onoffpluginunit_powerplug_switch", + "entity_id": "switch.mock_onoffpluginunit_switch", }, blocking=True, ) @@ -64,7 +64,7 @@ async def test_turn_on( set_node_attribute(powerplug_node, 1, 6, 0, True) await trigger_subscription_callback(hass, matter_client) - state = hass.states.get("switch.mock_onoffpluginunit_powerplug_switch") + state = hass.states.get("switch.mock_onoffpluginunit_switch") assert state assert state.state == "on" @@ -77,7 +77,7 @@ async def test_turn_off( powerplug_node: MatterNode, ) -> None: """Test turning off a switch.""" - state = hass.states.get("switch.mock_onoffpluginunit_powerplug_switch") + state = hass.states.get("switch.mock_onoffpluginunit_switch") assert state assert state.state == "off" @@ -85,7 +85,7 @@ async def test_turn_off( "switch", "turn_off", { - "entity_id": "switch.mock_onoffpluginunit_powerplug_switch", + "entity_id": "switch.mock_onoffpluginunit_switch", }, blocking=True, ) @@ -109,7 +109,23 @@ async def test_switch_unit( # A switch entity should be discovered as fallback for ANY Matter device (endpoint) # that has the OnOff cluster and does not fall into an explicit discovery schema # by another platform (e.g. light, lock etc.). - state = hass.states.get("switch.mock_switchunit") + state = hass.states.get("switch.mock_switchunit_switch") assert state assert state.state == "off" - assert state.attributes["friendly_name"] == "Mock SwitchUnit" + assert state.attributes["friendly_name"] == "Mock SwitchUnit Switch" + + +# This tests needs to be adjusted to remove lingering tasks +@pytest.mark.parametrize("expected_lingering_tasks", [True]) +async def test_power_switch( + hass: HomeAssistant, + matter_client: MagicMock, +) -> None: + """Test if a Power switch entity is created for a device that supports that.""" + await setup_integration_with_node_fixture( + hass, "room-airconditioner", matter_client + ) + state = hass.states.get("switch.room_airconditioner_power") + assert state + assert state.state == "off" + assert state.attributes["friendly_name"] == "Room AirConditioner Power" From f03759295f60724becf9254b91de962a0abbd550 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 21 Jun 2024 16:52:09 +0200 Subject: [PATCH 0946/1445] Refactor Tibber realtime entity creation (#118031) --- homeassistant/components/tibber/sensor.py | 85 ++++++++++++++--------- 1 file changed, 53 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/tibber/sensor.py b/homeassistant/components/tibber/sensor.py index 8d036157494..a9090add49b 100644 --- a/homeassistant/components/tibber/sensor.py +++ b/homeassistant/components/tibber/sensor.py @@ -2,6 +2,7 @@ from __future__ import annotations +from collections.abc import Callable import datetime from datetime import timedelta import logging @@ -291,9 +292,12 @@ async def async_setup_entry( ) if home.has_real_time_consumption: + entity_creator = TibberRtEntityCreator( + async_add_entities, home, entity_registry + ) await home.rt_subscribe( TibberRtDataCoordinator( - async_add_entities, home, hass + entity_creator.add_sensors, home, hass ).async_set_updated_data ) @@ -520,38 +524,20 @@ class TibberSensorRT(TibberSensor, CoordinatorEntity["TibberRtDataCoordinator"]) self.async_write_ha_state() -class TibberRtDataCoordinator(DataUpdateCoordinator): # pylint: disable=hass-enforce-coordinator-module - """Handle Tibber realtime data.""" +class TibberRtEntityCreator: + """Create realtime Tibber entities.""" def __init__( self, async_add_entities: AddEntitiesCallback, tibber_home: tibber.TibberHome, - hass: HomeAssistant, + entity_registry: er.EntityRegistry, ) -> None: """Initialize the data handler.""" self._async_add_entities = async_add_entities self._tibber_home = tibber_home - self.hass = hass self._added_sensors: set[str] = set() - super().__init__( - hass, - _LOGGER, - name=tibber_home.info["viewer"]["home"]["address"].get( - "address1", "Tibber" - ), - ) - - self._async_remove_device_updates_handler = self.async_add_listener( - self._add_sensors - ) - self.entity_registry = er.async_get(hass) - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self._handle_ha_stop) - - @callback - def _handle_ha_stop(self, _event: Event) -> None: - """Handle Home Assistant stopping.""" - self._async_remove_device_updates_handler() + self._entity_registry = entity_registry @callback def _migrate_unique_id(self, sensor_description: SensorEntityDescription) -> None: @@ -561,19 +547,19 @@ class TibberRtDataCoordinator(DataUpdateCoordinator): # pylint: disable=hass-en description_key = sensor_description.key entity_id: str | None = None if translation_key in RT_SENSORS_UNIQUE_ID_MIGRATION_SIMPLE: - entity_id = self.entity_registry.async_get_entity_id( + entity_id = self._entity_registry.async_get_entity_id( "sensor", TIBBER_DOMAIN, f"{home_id}_rt_{translation_key.replace('_', ' ')}", ) elif translation_key in RT_SENSORS_UNIQUE_ID_MIGRATION: - entity_id = self.entity_registry.async_get_entity_id( + entity_id = self._entity_registry.async_get_entity_id( "sensor", TIBBER_DOMAIN, f"{home_id}_rt_{RT_SENSORS_UNIQUE_ID_MIGRATION[translation_key]}", ) elif translation_key != description_key: - entity_id = self.entity_registry.async_get_entity_id( + entity_id = self._entity_registry.async_get_entity_id( "sensor", TIBBER_DOMAIN, f"{home_id}_rt_{translation_key}", @@ -590,18 +576,17 @@ class TibberRtDataCoordinator(DataUpdateCoordinator): # pylint: disable=hass-en new_unique_id, ) try: - self.entity_registry.async_update_entity( + self._entity_registry.async_update_entity( entity_id, new_unique_id=new_unique_id ) except ValueError as err: _LOGGER.error(err) @callback - def _add_sensors(self) -> None: + def add_sensors( + self, coordinator: TibberRtDataCoordinator, live_measurement: Any + ) -> None: """Add sensor.""" - if not (live_measurement := self.get_live_measurement()): - return - new_entities = [] for sensor_description in RT_SENSORS: if sensor_description.key in self._added_sensors: @@ -615,13 +600,49 @@ class TibberRtDataCoordinator(DataUpdateCoordinator): # pylint: disable=hass-en self._tibber_home, sensor_description, state, - self, + coordinator, ) new_entities.append(entity) self._added_sensors.add(sensor_description.key) if new_entities: self._async_add_entities(new_entities) + +class TibberRtDataCoordinator(DataUpdateCoordinator): # pylint: disable=hass-enforce-coordinator-module + """Handle Tibber realtime data.""" + + def __init__( + self, + add_sensor_callback: Callable[[TibberRtDataCoordinator, Any], None], + tibber_home: tibber.TibberHome, + hass: HomeAssistant, + ) -> None: + """Initialize the data handler.""" + self._add_sensor_callback = add_sensor_callback + super().__init__( + hass, + _LOGGER, + name=tibber_home.info["viewer"]["home"]["address"].get( + "address1", "Tibber" + ), + ) + + self._async_remove_device_updates_handler = self.async_add_listener( + self._data_updated + ) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self._handle_ha_stop) + + @callback + def _handle_ha_stop(self, _event: Event) -> None: + """Handle Home Assistant stopping.""" + self._async_remove_device_updates_handler() + + @callback + def _data_updated(self) -> None: + """Triggered when data is updated.""" + if live_measurement := self.get_live_measurement(): + self._add_sensor_callback(self, live_measurement) + def get_live_measurement(self) -> Any: """Get live measurement data.""" if errors := self.data.get("errors"): From c342c1e4d63801e7ba501633320bd5a435d0644f Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Fri, 21 Jun 2024 17:00:55 +0200 Subject: [PATCH 0947/1445] Device automation extra fields translation for ZHA (#119520) Co-authored-by: Matthias Alphart Co-authored-by: puddly <32534428+puddly@users.noreply.github.com> --- homeassistant/components/zha/strings.json | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/homeassistant/components/zha/strings.json b/homeassistant/components/zha/strings.json index 04cef23b2df..f25fdf1ebe4 100644 --- a/homeassistant/components/zha/strings.json +++ b/homeassistant/components/zha/strings.json @@ -249,6 +249,13 @@ "face_4": "With face 4 activated", "face_5": "With face 5 activated", "face_6": "With face 6 activated" + }, + "extra_fields": { + "color": "Color hue", + "duration": "Duration in seconds", + "effect_type": "Effect type", + "led_number": "LED number", + "level": "Brightness (%)" } }, "services": { From 710e245819b36b6bf9945f2ae4d40d1e261457e4 Mon Sep 17 00:00:00 2001 From: Thomas55555 <59625598+Thomas55555@users.noreply.github.com> Date: Fri, 21 Jun 2024 17:02:20 +0200 Subject: [PATCH 0948/1445] Also test if command can be send successfully in Husqvarna Automower (#120107) --- tests/components/husqvarna_automower/test_lawn_mower.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/components/husqvarna_automower/test_lawn_mower.py b/tests/components/husqvarna_automower/test_lawn_mower.py index 849339e4d96..ff5a67971be 100644 --- a/tests/components/husqvarna_automower/test_lawn_mower.py +++ b/tests/components/husqvarna_automower/test_lawn_mower.py @@ -70,6 +70,15 @@ async def test_lawn_mower_commands( ) -> None: """Test lawn_mower commands.""" await setup_integration(hass, mock_config_entry) + await hass.services.async_call( + domain="lawn_mower", + service=service, + service_data={"entity_id": "lawn_mower.test_mower_1"}, + blocking=True, + ) + mocked_method = getattr(mock_automower_client.commands, aioautomower_command) + mocked_method.assert_called_once_with(TEST_MOWER_ID) + getattr( mock_automower_client.commands, aioautomower_command ).side_effect = ApiException("Test error") From 2770811dda4594aadf650ca06d00c560c667a74e Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 21 Jun 2024 17:22:03 +0200 Subject: [PATCH 0949/1445] Add Knocki integration (#119140) Co-authored-by: Josef Zweck <24647999+zweckj@users.noreply.github.com> --- .strict-typing | 1 + CODEOWNERS | 2 + homeassistant/components/knocki/__init__.py | 52 +++++++++ .../components/knocki/config_flow.py | 62 ++++++++++ homeassistant/components/knocki/const.py | 7 ++ homeassistant/components/knocki/event.py | 64 ++++++++++ homeassistant/components/knocki/manifest.json | 11 ++ homeassistant/components/knocki/strings.json | 29 +++++ homeassistant/generated/config_flows.py | 1 + homeassistant/generated/integrations.json | 6 + mypy.ini | 10 ++ requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/knocki/__init__.py | 12 ++ tests/components/knocki/conftest.py | 57 +++++++++ .../components/knocki/fixtures/triggers.json | 16 +++ .../knocki/snapshots/test_event.ambr | 55 +++++++++ tests/components/knocki/test_config_flow.py | 109 ++++++++++++++++++ tests/components/knocki/test_event.py | 75 ++++++++++++ tests/components/knocki/test_init.py | 43 +++++++ 20 files changed, 618 insertions(+) create mode 100644 homeassistant/components/knocki/__init__.py create mode 100644 homeassistant/components/knocki/config_flow.py create mode 100644 homeassistant/components/knocki/const.py create mode 100644 homeassistant/components/knocki/event.py create mode 100644 homeassistant/components/knocki/manifest.json create mode 100644 homeassistant/components/knocki/strings.json create mode 100644 tests/components/knocki/__init__.py create mode 100644 tests/components/knocki/conftest.py create mode 100644 tests/components/knocki/fixtures/triggers.json create mode 100644 tests/components/knocki/snapshots/test_event.ambr create mode 100644 tests/components/knocki/test_config_flow.py create mode 100644 tests/components/knocki/test_event.py create mode 100644 tests/components/knocki/test_init.py diff --git a/.strict-typing b/.strict-typing index 313dda48649..2a6edfedd32 100644 --- a/.strict-typing +++ b/.strict-typing @@ -261,6 +261,7 @@ homeassistant.components.jellyfin.* homeassistant.components.jewish_calendar.* homeassistant.components.jvc_projector.* homeassistant.components.kaleidescape.* +homeassistant.components.knocki.* homeassistant.components.knx.* homeassistant.components.kraken.* homeassistant.components.lacrosse.* diff --git a/CODEOWNERS b/CODEOWNERS index aa33cdfe38f..6999f9e08a0 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -737,6 +737,8 @@ build.json @home-assistant/supervisor /tests/components/kitchen_sink/ @home-assistant/core /homeassistant/components/kmtronic/ @dgomes /tests/components/kmtronic/ @dgomes +/homeassistant/components/knocki/ @joostlek @jgatto1 +/tests/components/knocki/ @joostlek @jgatto1 /homeassistant/components/knx/ @Julius2342 @farmio @marvin-w /tests/components/knx/ @Julius2342 @farmio @marvin-w /homeassistant/components/kodi/ @OnFreund diff --git a/homeassistant/components/knocki/__init__.py b/homeassistant/components/knocki/__init__.py new file mode 100644 index 00000000000..ef024d6f4d6 --- /dev/null +++ b/homeassistant/components/knocki/__init__.py @@ -0,0 +1,52 @@ +"""The Knocki integration.""" + +from __future__ import annotations + +from dataclasses import dataclass + +from knocki import KnockiClient, KnockiConnectionError, Trigger + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_TOKEN, Platform +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +PLATFORMS: list[Platform] = [Platform.EVENT] + +type KnockiConfigEntry = ConfigEntry[KnockiData] + + +@dataclass +class KnockiData: + """Knocki data.""" + + client: KnockiClient + triggers: list[Trigger] + + +async def async_setup_entry(hass: HomeAssistant, entry: KnockiConfigEntry) -> bool: + """Set up Knocki from a config entry.""" + client = KnockiClient( + session=async_get_clientsession(hass), token=entry.data[CONF_TOKEN] + ) + + try: + triggers = await client.get_triggers() + except KnockiConnectionError as exc: + raise ConfigEntryNotReady from exc + + entry.runtime_data = KnockiData(client=client, triggers=triggers) + + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + + entry.async_create_background_task( + hass, client.start_websocket(), "knocki-websocket" + ) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: KnockiConfigEntry) -> bool: + """Unload a config entry.""" + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/knocki/config_flow.py b/homeassistant/components/knocki/config_flow.py new file mode 100644 index 00000000000..724c65f83df --- /dev/null +++ b/homeassistant/components/knocki/config_flow.py @@ -0,0 +1,62 @@ +"""Config flow for Knocki integration.""" + +from __future__ import annotations + +from typing import Any + +from knocki import KnockiClient, KnockiConnectionError +import voluptuous as vol + +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult +from homeassistant.const import CONF_PASSWORD, CONF_TOKEN, CONF_USERNAME +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .const import DOMAIN, LOGGER + +DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_USERNAME): str, + vol.Required(CONF_PASSWORD): str, + } +) + + +class KnockiConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a config flow for Knocki.""" + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle the initial step.""" + errors: dict[str, str] = {} + if user_input is not None: + client = KnockiClient(session=async_get_clientsession(self.hass)) + try: + token_response = await client.login( + user_input[CONF_USERNAME], user_input[CONF_PASSWORD] + ) + await self.async_set_unique_id(token_response.user_id) + self._abort_if_unique_id_configured() + client.token = token_response.token + await client.link() + except HomeAssistantError: + # Catch the unique_id abort and reraise it to keep the code clean + raise + except KnockiConnectionError: + errors["base"] = "cannot_connect" + except Exception: # noqa: BLE001 + LOGGER.exception("Error logging into the Knocki API") + errors["base"] = "unknown" + else: + return self.async_create_entry( + title=user_input[CONF_USERNAME], + data={ + CONF_TOKEN: token_response.token, + }, + ) + return self.async_show_form( + step_id="user", + errors=errors, + data_schema=DATA_SCHEMA, + ) diff --git a/homeassistant/components/knocki/const.py b/homeassistant/components/knocki/const.py new file mode 100644 index 00000000000..a54852e9292 --- /dev/null +++ b/homeassistant/components/knocki/const.py @@ -0,0 +1,7 @@ +"""Constants for the Knocki integration.""" + +import logging + +DOMAIN = "knocki" + +LOGGER = logging.getLogger(__package__) diff --git a/homeassistant/components/knocki/event.py b/homeassistant/components/knocki/event.py new file mode 100644 index 00000000000..8cd5de21958 --- /dev/null +++ b/homeassistant/components/knocki/event.py @@ -0,0 +1,64 @@ +"""Event entity for Knocki integration.""" + +from knocki import Event, EventType, KnockiClient, Trigger + +from homeassistant.components.event import EventEntity +from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import KnockiConfigEntry +from .const import DOMAIN + + +async def async_setup_entry( + hass: HomeAssistant, + entry: KnockiConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up Knocki from a config entry.""" + entry_data = entry.runtime_data + + async_add_entities( + KnockiTrigger(trigger, entry_data.client) for trigger in entry_data.triggers + ) + + +EVENT_TRIGGERED = "triggered" + + +class KnockiTrigger(EventEntity): + """Representation of a Knocki trigger.""" + + _attr_event_types = [EVENT_TRIGGERED] + _attr_has_entity_name = True + _attr_translation_key = "knocki" + + def __init__(self, trigger: Trigger, client: KnockiClient) -> None: + """Initialize the entity.""" + self._trigger = trigger + self._client = client + self._attr_name = trigger.details.name + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, trigger.device_id)}, + manufacturer="Knocki", + serial_number=trigger.device_id, + name=trigger.device_id, + ) + self._attr_unique_id = f"{trigger.device_id}_{trigger.details.trigger_id}" + + async def async_added_to_hass(self) -> None: + """Register listener.""" + await super().async_added_to_hass() + self.async_on_remove( + self._client.register_listener(EventType.TRIGGERED, self._handle_event) + ) + + def _handle_event(self, event: Event) -> None: + """Handle incoming event.""" + if ( + event.payload.details.trigger_id == self._trigger.details.trigger_id + and event.payload.device_id == self._trigger.device_id + ): + self._trigger_event(EVENT_TRIGGERED) + self.schedule_update_ha_state() diff --git a/homeassistant/components/knocki/manifest.json b/homeassistant/components/knocki/manifest.json new file mode 100644 index 00000000000..bf4dcea4b67 --- /dev/null +++ b/homeassistant/components/knocki/manifest.json @@ -0,0 +1,11 @@ +{ + "domain": "knocki", + "name": "Knocki", + "codeowners": ["@joostlek", "@jgatto1"], + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/knocki", + "integration_type": "device", + "iot_class": "cloud_push", + "loggers": ["knocki"], + "requirements": ["knocki==0.1.5"] +} diff --git a/homeassistant/components/knocki/strings.json b/homeassistant/components/knocki/strings.json new file mode 100644 index 00000000000..b7a7daad1fc --- /dev/null +++ b/homeassistant/components/knocki/strings.json @@ -0,0 +1,29 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + } + }, + "entity": { + "event": { + "knocki": { + "state_attributes": { + "event_type": { + "state": { + "triggered": "Triggered" + } + } + } + } + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 7cd0e270703..f33e37c1a7b 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -281,6 +281,7 @@ FLOWS = { "kegtron", "keymitt_ble", "kmtronic", + "knocki", "knx", "kodi", "konnected", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 0fe63cc02ff..fbb2e8ed8aa 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -3078,6 +3078,12 @@ "config_flow": true, "iot_class": "local_push" }, + "knocki": { + "name": "Knocki", + "integration_type": "device", + "config_flow": true, + "iot_class": "cloud_push" + }, "knx": { "name": "KNX", "integration_type": "hub", diff --git a/mypy.ini b/mypy.ini index 4e4d9cc624b..740eb4f2b5b 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2373,6 +2373,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.knocki.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.knx.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/requirements_all.txt b/requirements_all.txt index d9dd5bbe61b..a87d781d649 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1208,6 +1208,9 @@ kegtron-ble==0.4.0 # homeassistant.components.kiwi kiwiki-client==0.1.1 +# homeassistant.components.knocki +knocki==0.1.5 + # homeassistant.components.knx knx-frontend==2024.1.20.105944 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 33ff276c8ad..787062155c5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -986,6 +986,9 @@ justnimbus==0.7.3 # homeassistant.components.kegtron kegtron-ble==0.4.0 +# homeassistant.components.knocki +knocki==0.1.5 + # homeassistant.components.knx knx-frontend==2024.1.20.105944 diff --git a/tests/components/knocki/__init__.py b/tests/components/knocki/__init__.py new file mode 100644 index 00000000000..4ebf6b0dd01 --- /dev/null +++ b/tests/components/knocki/__init__.py @@ -0,0 +1,12 @@ +"""Tests for the Knocki integration.""" + +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +async def setup_integration(hass: HomeAssistant, config_entry: MockConfigEntry) -> None: + """Fixture for setting up the component.""" + config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(config_entry.entry_id) diff --git a/tests/components/knocki/conftest.py b/tests/components/knocki/conftest.py new file mode 100644 index 00000000000..e1bc2e29cde --- /dev/null +++ b/tests/components/knocki/conftest.py @@ -0,0 +1,57 @@ +"""Common fixtures for the Knocki tests.""" + +from unittest.mock import AsyncMock, patch + +from knocki import TokenResponse, Trigger +import pytest +from typing_extensions import Generator + +from homeassistant.components.knocki.const import DOMAIN +from homeassistant.const import CONF_TOKEN + +from tests.common import MockConfigEntry, load_json_array_fixture + + +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock]: + """Override async_setup_entry.""" + with patch( + "homeassistant.components.knocki.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + yield mock_setup_entry + + +@pytest.fixture +def mock_knocki_client() -> Generator[AsyncMock]: + """Mock a Knocki client.""" + with ( + patch( + "homeassistant.components.knocki.KnockiClient", + autospec=True, + ) as mock_client, + patch( + "homeassistant.components.knocki.config_flow.KnockiClient", + new=mock_client, + ), + ): + client = mock_client.return_value + client.login.return_value = TokenResponse(token="test-token", user_id="test-id") + client.get_triggers.return_value = [ + Trigger.from_dict(trigger) + for trigger in load_json_array_fixture("triggers.json", DOMAIN) + ] + yield client + + +@pytest.fixture +def mock_config_entry() -> MockConfigEntry: + """Mock a config entry.""" + return MockConfigEntry( + domain=DOMAIN, + title="Knocki", + unique_id="test-id", + data={ + CONF_TOKEN: "test-token", + }, + ) diff --git a/tests/components/knocki/fixtures/triggers.json b/tests/components/knocki/fixtures/triggers.json new file mode 100644 index 00000000000..13dc3906b35 --- /dev/null +++ b/tests/components/knocki/fixtures/triggers.json @@ -0,0 +1,16 @@ +[ + { + "device": "KNC1-W-00000214", + "gesture": "d060b870-15ba-42c9-a932-2d2951087152", + "details": { + "description": "Eeee", + "name": "Aaaa", + "id": 31 + }, + "type": "homeassistant", + "user": "7a4d5bf9-01b1-413b-bb4d-77728e931dcc", + "updatedAt": 1716378013721, + "createdAt": 1716378013721, + "id": "1a050b25-7fed-4e0e-b5af-792b8b4650de" + } +] diff --git a/tests/components/knocki/snapshots/test_event.ambr b/tests/components/knocki/snapshots/test_event.ambr new file mode 100644 index 00000000000..fba1c90b45d --- /dev/null +++ b/tests/components/knocki/snapshots/test_event.ambr @@ -0,0 +1,55 @@ +# serializer version: 1 +# name: test_entities[event.knc1_w_00000214_aaaa-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'event_types': list([ + 'triggered', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'event', + 'entity_category': None, + 'entity_id': 'event.knc1_w_00000214_aaaa', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Aaaa', + 'platform': 'knocki', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'knocki', + 'unique_id': 'KNC1-W-00000214_31', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[event.knc1_w_00000214_aaaa-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'event_type': None, + 'event_types': list([ + 'triggered', + ]), + 'friendly_name': 'KNC1-W-00000214 Aaaa', + }), + 'context': , + 'entity_id': 'event.knc1_w_00000214_aaaa', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- diff --git a/tests/components/knocki/test_config_flow.py b/tests/components/knocki/test_config_flow.py new file mode 100644 index 00000000000..baf43c3ad30 --- /dev/null +++ b/tests/components/knocki/test_config_flow.py @@ -0,0 +1,109 @@ +"""Tests for the Knocki event platform.""" + +from unittest.mock import AsyncMock + +from knocki import KnockiConnectionError +import pytest + +from homeassistant.components.knocki.const import DOMAIN +from homeassistant.config_entries import SOURCE_USER +from homeassistant.const import CONF_PASSWORD, CONF_TOKEN, CONF_USERNAME +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +from tests.common import MockConfigEntry + + +async def test_full_flow( + hass: HomeAssistant, + mock_knocki_client: AsyncMock, + mock_setup_entry: AsyncMock, +) -> None: + """Test full flow.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] is FlowResultType.FORM + assert not result["errors"] + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", + }, + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "test-username" + assert result["data"] == { + CONF_TOKEN: "test-token", + } + assert result["result"].unique_id == "test-id" + assert len(mock_knocki_client.link.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_duplcate_entry( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_knocki_client: AsyncMock, +) -> None: + """Test abort when setting up duplicate entry.""" + mock_config_entry.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] is FlowResultType.FORM + assert not result["errors"] + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", + }, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +@pytest.mark.parametrize(("field"), ["login", "link"]) +@pytest.mark.parametrize( + ("exception", "error"), + [(KnockiConnectionError, "cannot_connect"), (Exception, "unknown")], +) +async def test_exceptions( + hass: HomeAssistant, + mock_knocki_client: AsyncMock, + mock_setup_entry: AsyncMock, + field: str, + exception: Exception, + error: str, +) -> None: + """Test exceptions.""" + getattr(mock_knocki_client, field).side_effect = exception + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", + }, + ) + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"base": error} + + getattr(mock_knocki_client, field).side_effect = None + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", + }, + ) + assert result["type"] is FlowResultType.CREATE_ENTRY diff --git a/tests/components/knocki/test_event.py b/tests/components/knocki/test_event.py new file mode 100644 index 00000000000..a53e2811854 --- /dev/null +++ b/tests/components/knocki/test_event.py @@ -0,0 +1,75 @@ +"""Tests for the Knocki event platform.""" + +from collections.abc import Callable +from unittest.mock import AsyncMock + +from knocki import Event, EventType, Trigger, TriggerDetails +import pytest +from syrupy import SnapshotAssertion + +from homeassistant.const import STATE_UNKNOWN +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import setup_integration + +from tests.common import MockConfigEntry, snapshot_platform + + +async def test_entities( + hass: HomeAssistant, + mock_knocki_client: AsyncMock, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, +) -> None: + """Test entities.""" + await setup_integration(hass, mock_config_entry) + + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + +@pytest.mark.freeze_time("2022-01-01T12:00:00Z") +async def test_subscription( + hass: HomeAssistant, + mock_knocki_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test subscription.""" + await setup_integration(hass, mock_config_entry) + + assert hass.states.get("event.knc1_w_00000214_aaaa").state == STATE_UNKNOWN + + event_function: Callable[[Event], None] = ( + mock_knocki_client.register_listener.call_args[0][1] + ) + + async def _call_event_function( + device_id: str = "KNC1-W-00000214", trigger_id: int = 31 + ) -> None: + event_function( + Event( + EventType.TRIGGERED, + Trigger( + device_id=device_id, details=TriggerDetails(trigger_id, "aaaa") + ), + ) + ) + await hass.async_block_till_done() + + await _call_event_function(device_id="KNC1-W-00000215") + assert hass.states.get("event.knc1_w_00000214_aaaa").state == STATE_UNKNOWN + + await _call_event_function(trigger_id=32) + assert hass.states.get("event.knc1_w_00000214_aaaa").state == STATE_UNKNOWN + + await _call_event_function() + assert ( + hass.states.get("event.knc1_w_00000214_aaaa").state + == "2022-01-01T12:00:00.000+00:00" + ) + + await hass.config_entries.async_unload(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert mock_knocki_client.register_listener.return_value.called diff --git a/tests/components/knocki/test_init.py b/tests/components/knocki/test_init.py new file mode 100644 index 00000000000..7db0e1047b5 --- /dev/null +++ b/tests/components/knocki/test_init.py @@ -0,0 +1,43 @@ +"""Test the Home Knocki init module.""" + +from __future__ import annotations + +from unittest.mock import AsyncMock + +from knocki import KnockiConnectionError + +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant + +from . import setup_integration + +from tests.common import MockConfigEntry + + +async def test_load_unload_entry( + hass: HomeAssistant, + mock_knocki_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test load and unload entry.""" + await setup_integration(hass, mock_config_entry) + + assert mock_config_entry.state is ConfigEntryState.LOADED + + await hass.config_entries.async_remove(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert mock_config_entry.state is ConfigEntryState.NOT_LOADED + + +async def test_initialization_failure( + hass: HomeAssistant, + mock_knocki_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test initialization failure.""" + mock_knocki_client.get_triggers.side_effect = KnockiConnectionError + + await setup_integration(hass, mock_config_entry) + + assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY From 842763bd277b9f57f666ff9bb370779db72fda3b Mon Sep 17 00:00:00 2001 From: Robert Contreras Date: Fri, 21 Jun 2024 08:37:22 -0700 Subject: [PATCH 0950/1445] Add Home Connect binary_sensor unit tests (#115323) --- .coveragerc | 1 - .../home_connect/test_binary_sensor.py | 74 +++++++++++++++++++ tests/components/home_connect/test_init.py | 1 + 3 files changed, 75 insertions(+), 1 deletion(-) create mode 100644 tests/components/home_connect/test_binary_sensor.py diff --git a/.coveragerc b/.coveragerc index 56a93b586a4..350c39ca3d2 100644 --- a/.coveragerc +++ b/.coveragerc @@ -536,7 +536,6 @@ omit = homeassistant/components/hko/weather.py homeassistant/components/hlk_sw16/__init__.py homeassistant/components/hlk_sw16/switch.py - homeassistant/components/home_connect/binary_sensor.py homeassistant/components/home_connect/entity.py homeassistant/components/home_connect/light.py homeassistant/components/home_connect/switch.py diff --git a/tests/components/home_connect/test_binary_sensor.py b/tests/components/home_connect/test_binary_sensor.py new file mode 100644 index 00000000000..d21aec35045 --- /dev/null +++ b/tests/components/home_connect/test_binary_sensor.py @@ -0,0 +1,74 @@ +"""Tests for home_connect binary_sensor entities.""" + +from collections.abc import Awaitable, Callable, Generator +from typing import Any +from unittest.mock import MagicMock, Mock + +import pytest + +from homeassistant.components.home_connect.const import ( + BSH_DOOR_STATE, + BSH_DOOR_STATE_CLOSED, + BSH_DOOR_STATE_LOCKED, + BSH_DOOR_STATE_OPEN, +) +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_component import async_update_entity + +from tests.common import MockConfigEntry + + +@pytest.fixture +def platforms() -> list[str]: + """Fixture to specify platforms to test.""" + return [Platform.BINARY_SENSOR] + + +async def test_binary_sensors( + bypass_throttle: Generator[None, Any, None], + hass: HomeAssistant, + config_entry: MockConfigEntry, + integration_setup: Callable[[], Awaitable[bool]], + setup_credentials: None, + get_appliances: MagicMock, + appliance: Mock, +) -> None: + """Test binary sensor entities.""" + get_appliances.return_value = [appliance] + assert config_entry.state == ConfigEntryState.NOT_LOADED + assert await integration_setup() + assert config_entry.state == ConfigEntryState.LOADED + + +@pytest.mark.parametrize( + ("state", "expected"), + [ + (BSH_DOOR_STATE_CLOSED, "off"), + (BSH_DOOR_STATE_LOCKED, "off"), + (BSH_DOOR_STATE_OPEN, "on"), + ("", "unavailable"), + ], +) +async def test_binary_sensors_door_states( + expected: str, + state: str, + bypass_throttle: Generator[None, Any, None], + hass: HomeAssistant, + config_entry: MockConfigEntry, + integration_setup: Callable[[], Awaitable[bool]], + setup_credentials: None, + get_appliances: MagicMock, + appliance: Mock, +) -> None: + """Tests for Appliance door states.""" + entity_id = "binary_sensor.washer_door" + get_appliances.return_value = [appliance] + assert config_entry.state == ConfigEntryState.NOT_LOADED + assert await integration_setup() + assert config_entry.state == ConfigEntryState.LOADED + appliance.status.update({BSH_DOOR_STATE: {"value": state}}) + await async_update_entity(hass, entity_id) + await hass.async_block_till_done() + assert hass.states.is_state(entity_id, expected) diff --git a/tests/components/home_connect/test_init.py b/tests/components/home_connect/test_init.py index 10e7d8ca911..616a82edebc 100644 --- a/tests/components/home_connect/test_init.py +++ b/tests/components/home_connect/test_init.py @@ -118,6 +118,7 @@ SERVICE_APPLIANCE_METHOD_MAPPING = { async def test_api_setup( + bypass_throttle: Generator[None, Any, None], hass: HomeAssistant, config_entry: MockConfigEntry, integration_setup: Callable[[], Awaitable[bool]], From 1bebf79e5c59590a8cae5cfb254f4a850116a066 Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Fri, 21 Jun 2024 17:53:05 +0200 Subject: [PATCH 0951/1445] Fix Solarlog snapshot missing self-consumption sensor (#120111) --- .../solarlog/snapshots/test_sensor.ambr | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/tests/components/solarlog/snapshots/test_sensor.ambr b/tests/components/solarlog/snapshots/test_sensor.ambr index 5080a001b84..5fb369bc3b6 100644 --- a/tests/components/solarlog/snapshots/test_sensor.ambr +++ b/tests/components/solarlog/snapshots/test_sensor.ambr @@ -694,6 +694,57 @@ 'state': '102', }) # --- +# name: test_all_entities[sensor.solarlog_self_consumption_year-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.solarlog_self_consumption_year', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Self-consumption year', + 'platform': 'solarlog', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'self_consumption_year', + 'unique_id': 'ce5f5431554d101905d31797e1232da8_self_consumption_year', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.solarlog_self_consumption_year-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'solarlog Self-consumption year', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.solarlog_self_consumption_year', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '545', + }) +# --- # name: test_all_entities[sensor.solarlog_test_1_2_3_alternator_loss-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ From 8aed04cd3ca188ec61729421ba13f1fdd34f2b60 Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Fri, 21 Jun 2024 11:19:52 -0500 Subject: [PATCH 0952/1445] Bump intents to 2024.6.21 (#120106) --- homeassistant/components/conversation/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../components/conversation/snapshots/test_init.ambr | 12 ++++++------ 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/conversation/manifest.json b/homeassistant/components/conversation/manifest.json index a3af6607aba..ee0b29f22fc 100644 --- a/homeassistant/components/conversation/manifest.json +++ b/homeassistant/components/conversation/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/conversation", "integration_type": "system", "quality_scale": "internal", - "requirements": ["hassil==1.7.1", "home-assistant-intents==2024.6.5"] + "requirements": ["hassil==1.7.1", "home-assistant-intents==2024.6.21"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 38f9d33575a..7dfec9e63b3 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -33,7 +33,7 @@ hass-nabucasa==0.81.1 hassil==1.7.1 home-assistant-bluetooth==1.12.1 home-assistant-frontend==20240610.1 -home-assistant-intents==2024.6.5 +home-assistant-intents==2024.6.21 httpx==0.27.0 ifaddr==0.2.0 Jinja2==3.1.4 diff --git a/requirements_all.txt b/requirements_all.txt index a87d781d649..03f74795190 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1096,7 +1096,7 @@ holidays==0.51 home-assistant-frontend==20240610.1 # homeassistant.components.conversation -home-assistant-intents==2024.6.5 +home-assistant-intents==2024.6.21 # homeassistant.components.home_connect homeconnect==0.7.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 787062155c5..ce4fe348b4f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -901,7 +901,7 @@ holidays==0.51 home-assistant-frontend==20240610.1 # homeassistant.components.conversation -home-assistant-intents==2024.6.5 +home-assistant-intents==2024.6.21 # homeassistant.components.home_connect homeconnect==0.7.2 diff --git a/tests/components/conversation/snapshots/test_init.ambr b/tests/components/conversation/snapshots/test_init.ambr index 6264e61863f..403c72aaa10 100644 --- a/tests/components/conversation/snapshots/test_init.ambr +++ b/tests/components/conversation/snapshots/test_init.ambr @@ -563,7 +563,7 @@ 'speech': dict({ 'plain': dict({ 'extra_data': None, - 'speech': 'Sorry, I am not aware of any device called kitchen light', + 'speech': 'Sorry, I am not aware of any device called kitchen', }), }), }), @@ -703,7 +703,7 @@ 'speech': dict({ 'plain': dict({ 'extra_data': None, - 'speech': 'Sorry, I am not aware of any device called late added light', + 'speech': 'Sorry, I am not aware of any device called late added', }), }), }), @@ -783,7 +783,7 @@ 'speech': dict({ 'plain': dict({ 'extra_data': None, - 'speech': 'Sorry, I am not aware of any device called kitchen light', + 'speech': 'Sorry, I am not aware of any device called kitchen', }), }), }), @@ -803,7 +803,7 @@ 'speech': dict({ 'plain': dict({ 'extra_data': None, - 'speech': 'Sorry, I am not aware of any device called my cool light', + 'speech': 'Sorry, I am not aware of any device called my cool', }), }), }), @@ -943,7 +943,7 @@ 'speech': dict({ 'plain': dict({ 'extra_data': None, - 'speech': 'Sorry, I am not aware of any device called kitchen light', + 'speech': 'Sorry, I am not aware of any device called kitchen', }), }), }), @@ -993,7 +993,7 @@ 'speech': dict({ 'plain': dict({ 'extra_data': None, - 'speech': 'Sorry, I am not aware of any device called renamed light', + 'speech': 'Sorry, I am not aware of any device called renamed', }), }), }), From 5e375dbf381a16ec7ba14821d569efcc40e49964 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 21 Jun 2024 11:26:14 -0500 Subject: [PATCH 0953/1445] Update uiprotect to 1.20.0 (#120108) --- .../components/unifiprotect/manifest.json | 2 +- .../components/unifiprotect/models.py | 21 +++++++------------ .../components/unifiprotect/utils.py | 18 +--------------- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 11 insertions(+), 34 deletions(-) diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index 8dcc102d6fb..987329abbba 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -40,7 +40,7 @@ "integration_type": "hub", "iot_class": "local_push", "loggers": ["uiprotect", "unifi_discovery"], - "requirements": ["uiprotect==1.19.3", "unifi-discovery==1.1.8"], + "requirements": ["uiprotect==1.20.0", "unifi-discovery==1.1.8"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/homeassistant/components/unifiprotect/models.py b/homeassistant/components/unifiprotect/models.py index bbd125b4085..23106a4e5d7 100644 --- a/homeassistant/components/unifiprotect/models.py +++ b/homeassistant/components/unifiprotect/models.py @@ -10,6 +10,7 @@ import logging from operator import attrgetter from typing import Any, Generic, TypeVar +from uiprotect import make_enabled_getter, make_required_getter, make_value_getter from uiprotect.data import ( NVR, Event, @@ -19,8 +20,6 @@ from uiprotect.data import ( from homeassistant.helpers.entity import EntityDescription -from .utils import get_nested_attr - _LOGGER = logging.getLogger(__name__) T = TypeVar("T", bound=ProtectAdoptableDeviceModel | NVR) @@ -61,22 +60,16 @@ class ProtectEntityDescription(EntityDescription, Generic[T]): """Override get_ufp_value, has_required, and get_ufp_enabled if required.""" _setter = partial(object.__setattr__, self) - if (_ufp_value := self.ufp_value) is not None: - ufp_value = tuple(_ufp_value.split(".")) - _setter("get_ufp_value", partial(get_nested_attr, attrs=ufp_value)) + if (ufp_value := self.ufp_value) is not None: + _setter("get_ufp_value", make_value_getter(ufp_value)) elif (ufp_value_fn := self.ufp_value_fn) is not None: _setter("get_ufp_value", ufp_value_fn) - if (_ufp_enabled := self.ufp_enabled) is not None: - ufp_enabled = tuple(_ufp_enabled.split(".")) - _setter("get_ufp_enabled", partial(get_nested_attr, attrs=ufp_enabled)) + if (ufp_enabled := self.ufp_enabled) is not None: + _setter("get_ufp_enabled", make_enabled_getter(ufp_enabled)) - if (_ufp_required_field := self.ufp_required_field) is not None: - ufp_required_field = tuple(_ufp_required_field.split(".")) - _setter( - "has_required", - lambda obj: bool(get_nested_attr(obj, ufp_required_field)), - ) + if (ufp_required_field := self.ufp_required_field) is not None: + _setter("has_required", make_required_getter(ufp_required_field)) @dataclass(frozen=True, kw_only=True) diff --git a/homeassistant/components/unifiprotect/utils.py b/homeassistant/components/unifiprotect/utils.py index c9dcfa6b37f..d98ad72e1d1 100644 --- a/homeassistant/components/unifiprotect/utils.py +++ b/homeassistant/components/unifiprotect/utils.py @@ -4,10 +4,9 @@ from __future__ import annotations from collections.abc import Iterable import contextlib -from enum import Enum from pathlib import Path import socket -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING from aiohttp import CookieJar from typing_extensions import Generator @@ -42,21 +41,6 @@ from .const import ( if TYPE_CHECKING: from .data import UFPConfigEntry -_SENTINEL = object() - - -def get_nested_attr(obj: Any, attrs: tuple[str, ...]) -> Any: - """Fetch a nested attribute.""" - if len(attrs) == 1: - value = getattr(obj, attrs[0], None) - else: - value = obj - for key in attrs: - if (value := getattr(value, key, _SENTINEL)) is _SENTINEL: - return None - - return value.value if isinstance(value, Enum) else value - @callback def _async_unifi_mac_from_hass(mac: str) -> str: diff --git a/requirements_all.txt b/requirements_all.txt index 03f74795190..422a87e202c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2794,7 +2794,7 @@ twitchAPI==4.0.0 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==1.19.3 +uiprotect==1.20.0 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ce4fe348b4f..76fadaa6511 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2174,7 +2174,7 @@ twitchAPI==4.0.0 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==1.19.3 +uiprotect==1.20.0 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 From a1884ed821df5e894bb0b9ba5f32becb67527c71 Mon Sep 17 00:00:00 2001 From: AlCalzone Date: Fri, 21 Jun 2024 18:39:22 +0200 Subject: [PATCH 0954/1445] Add discovery for Z-Wave Meter Reset (#119968) --- homeassistant/components/zwave_js/discovery.py | 17 ++++++++++++++++- tests/components/zwave_js/test_discovery.py | 8 ++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/zwave_js/discovery.py b/homeassistant/components/zwave_js/discovery.py index 0dda3d639bc..39b97e5d3f4 100644 --- a/homeassistant/components/zwave_js/discovery.py +++ b/homeassistant/components/zwave_js/discovery.py @@ -27,7 +27,10 @@ from zwave_js_server.const.command_class.lock import ( DOOR_STATUS_PROPERTY, LOCKED_PROPERTY, ) -from zwave_js_server.const.command_class.meter import VALUE_PROPERTY +from zwave_js_server.const.command_class.meter import ( + RESET_PROPERTY as RESET_METER_PROPERTY, + VALUE_PROPERTY, +) from zwave_js_server.const.command_class.protection import LOCAL_PROPERTY, RF_PROPERTY from zwave_js_server.const.command_class.sound_switch import ( DEFAULT_TONE_ID_PROPERTY, @@ -1180,6 +1183,18 @@ DISCOVERY_SCHEMAS = [ stateful=False, ), ), + # button + # Meter CC idle + ZWaveDiscoverySchema( + platform=Platform.BUTTON, + hint="meter reset", + primary_value=ZWaveValueDiscoverySchema( + command_class={CommandClass.METER}, + property={RESET_METER_PROPERTY}, + type={ValueType.BOOLEAN}, + ), + entity_category=EntityCategory.DIAGNOSTIC, + ), ] diff --git a/tests/components/zwave_js/test_discovery.py b/tests/components/zwave_js/test_discovery.py index a177e01afad..1179d8e843c 100644 --- a/tests/components/zwave_js/test_discovery.py +++ b/tests/components/zwave_js/test_discovery.py @@ -29,6 +29,14 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er +async def test_aeon_smart_switch_6_state( + hass: HomeAssistant, client, aeon_smart_switch_6, integration +) -> None: + """Test that Smart Switch 6 has a meter reset button.""" + state = hass.states.get("button.smart_switch_6_reset_accumulated_values") + assert state + + async def test_iblinds_v2(hass: HomeAssistant, client, iblinds_v2, integration) -> None: """Test that an iBlinds v2.0 multilevel switch value is discovered as a cover.""" node = iblinds_v2 From cb563f25fae4a93ae78e199a53a60a418a7fc68d Mon Sep 17 00:00:00 2001 From: Erwin Douna Date: Fri, 21 Jun 2024 18:52:39 +0200 Subject: [PATCH 0955/1445] Add DSMR MQTT subscribe error handling (#118316) Add eror handling --- .../components/dsmr_reader/sensor.py | 23 ++++++++++++++++--- .../components/dsmr_reader/strings.json | 6 +++++ 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/dsmr_reader/sensor.py b/homeassistant/components/dsmr_reader/sensor.py index 3c07ad65de6..784a4cdec51 100644 --- a/homeassistant/components/dsmr_reader/sensor.py +++ b/homeassistant/components/dsmr_reader/sensor.py @@ -6,9 +6,12 @@ from homeassistant.components import mqtt from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.util import slugify +from .const import DOMAIN from .definitions import SENSORS, DSMRReaderSensorEntityDescription @@ -53,6 +56,20 @@ class DSMRSensor(SensorEntity): self.async_write_ha_state() - await mqtt.async_subscribe( - self.hass, self.entity_description.key, message_received, 1 - ) + try: + await mqtt.async_subscribe( + self.hass, self.entity_description.key, message_received, 1 + ) + except HomeAssistantError: + async_create_issue( + self.hass, + DOMAIN, + f"cannot_subscribe_mqtt_topic_{self.entity_description.key}", + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key="cannot_subscribe_mqtt_topic", + translation_placeholders={ + "topic": self.entity_description.key, + "topic_title": self.entity_description.key.split("/")[-1], + }, + ) diff --git a/homeassistant/components/dsmr_reader/strings.json b/homeassistant/components/dsmr_reader/strings.json index fce274e8917..90cf0533a72 100644 --- a/homeassistant/components/dsmr_reader/strings.json +++ b/homeassistant/components/dsmr_reader/strings.json @@ -259,5 +259,11 @@ "name": "Quarter-hour peak end time" } } + }, + "issues": { + "cannot_subscribe_mqtt_topic": { + "title": "Cannot subscribe to MQTT topic {topic_title}", + "description": "The DSMR Reader integration cannot subscribe to the MQTT topic: `{topic}`. Please check the configuration of the MQTT broker and the topic.\nDSMR Reader needs to be running, before starting this integration." + } } } From 2ad5b1c3a6140a49d1113e86e46b68165cf26884 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ludovic=20BOU=C3=89?= Date: Fri, 21 Jun 2024 18:57:18 +0200 Subject: [PATCH 0956/1445] Add Matter discovery schemas for BooleanState sensors (#117870) Co-authored-by: Stefan Agner Co-authored-by: Franck Nijhof Co-authored-by: Marcel van der Veldt --- .../components/matter/binary_sensor.py | 58 ++++-- homeassistant/components/matter/strings.json | 11 ++ .../matter/fixtures/nodes/leak-sensor.json | 185 ++++++++++++++++++ tests/components/matter/test_binary_sensor.py | 60 +++--- 4 files changed, 280 insertions(+), 34 deletions(-) create mode 100644 tests/components/matter/fixtures/nodes/leak-sensor.json diff --git a/homeassistant/components/matter/binary_sensor.py b/homeassistant/components/matter/binary_sensor.py index 23ac2195355..b71c35c9cce 100644 --- a/homeassistant/components/matter/binary_sensor.py +++ b/homeassistant/components/matter/binary_sensor.py @@ -7,6 +7,7 @@ from dataclasses import dataclass from chip.clusters import Objects as clusters from chip.clusters.Objects import uint from chip.clusters.Types import Nullable, NullValue +from matter_server.client.models import device_types from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, @@ -73,17 +74,6 @@ DISCOVERY_SCHEMAS = [ vendor_id=(4107,), product_name=("Hue motion sensor",), ), - MatterDiscoverySchema( - platform=Platform.BINARY_SENSOR, - entity_description=MatterBinarySensorEntityDescription( - key="ContactSensor", - device_class=BinarySensorDeviceClass.DOOR, - # value is inverted on matter to what we expect - measurement_to_ha=lambda x: not x, - ), - entity_class=MatterBinarySensor, - required_attributes=(clusters.BooleanState.Attributes.StateValue,), - ), MatterDiscoverySchema( platform=Platform.BINARY_SENSOR, entity_description=MatterBinarySensorEntityDescription( @@ -109,4 +99,50 @@ DISCOVERY_SCHEMAS = [ # only add binary battery sensor if a regular percentage based is not available absent_attributes=(clusters.PowerSource.Attributes.BatPercentRemaining,), ), + # BooleanState sensors (tied to device type) + MatterDiscoverySchema( + platform=Platform.BINARY_SENSOR, + entity_description=MatterBinarySensorEntityDescription( + key="ContactSensor", + device_class=BinarySensorDeviceClass.DOOR, + # value is inverted on matter to what we expect + measurement_to_ha=lambda x: not x, + ), + entity_class=MatterBinarySensor, + required_attributes=(clusters.BooleanState.Attributes.StateValue,), + device_type=(device_types.ContactSensor,), + ), + MatterDiscoverySchema( + platform=Platform.BINARY_SENSOR, + entity_description=MatterBinarySensorEntityDescription( + key="WaterLeakDetector", + translation_key="water_leak", + device_class=BinarySensorDeviceClass.MOISTURE, + ), + entity_class=MatterBinarySensor, + required_attributes=(clusters.BooleanState.Attributes.StateValue,), + device_type=(device_types.WaterLeakDetector,), + ), + MatterDiscoverySchema( + platform=Platform.BINARY_SENSOR, + entity_description=MatterBinarySensorEntityDescription( + key="WaterFreezeDetector", + translation_key="water_freeze", + device_class=BinarySensorDeviceClass.COLD, + ), + entity_class=MatterBinarySensor, + required_attributes=(clusters.BooleanState.Attributes.StateValue,), + device_type=(device_types.WaterFreezeDetector,), + ), + MatterDiscoverySchema( + platform=Platform.BINARY_SENSOR, + entity_description=MatterBinarySensorEntityDescription( + key="RainSensor", + translation_key="rain", + device_class=BinarySensorDeviceClass.MOISTURE, + ), + entity_class=MatterBinarySensor, + required_attributes=(clusters.BooleanState.Attributes.StateValue,), + device_type=(device_types.RainSensor,), + ), ] diff --git a/homeassistant/components/matter/strings.json b/homeassistant/components/matter/strings.json index db71feab9c4..e94ab2e1780 100644 --- a/homeassistant/components/matter/strings.json +++ b/homeassistant/components/matter/strings.json @@ -45,6 +45,17 @@ } }, "entity": { + "binary_sensor": { + "water_leak": { + "name": "Water leak" + }, + "water_freeze": { + "name": "Water freeze" + }, + "rain": { + "name": "Rain" + } + }, "climate": { "thermostat": { "name": "Thermostat" diff --git a/tests/components/matter/fixtures/nodes/leak-sensor.json b/tests/components/matter/fixtures/nodes/leak-sensor.json new file mode 100644 index 00000000000..35cfb281e11 --- /dev/null +++ b/tests/components/matter/fixtures/nodes/leak-sensor.json @@ -0,0 +1,185 @@ +{ + "node_id": 32, + "date_commissioned": "2024-06-21T14:13:02.370603", + "last_interview": "2024-06-21T14:14:49.941142", + "interview_version": 6, + "available": true, + "is_bridge": true, + "attributes": { + "0/29/0": [ + { + "0": 22, + "1": 1 + } + ], + "0/29/1": [29, 31, 40, 43, 44, 48, 49, 50, 51, 52, 56, 60, 62, 63], + "0/29/2": [31], + "0/29/3": [1, 2], + "0/29/65528": [], + "0/29/65529": [], + "0/29/65530": [], + "0/29/65531": [0, 1, 2, 3, 65528, 65529, 65530, 65531, 65532, 65533], + "0/29/65532": 0, + "0/29/65533": 1, + "0/31/65528": [], + "0/31/65529": [], + "0/31/65530": [], + "0/31/65531": [0, 2, 3, 4, 65528, 65529, 65530, 65531, 65532, 65533], + "0/31/65532": 0, + "0/31/65533": 1, + "0/40/0": 1, + "0/40/1": "Mock", + "0/40/2": 65521, + "0/40/3": "Water Leak Detector", + "0/40/4": 32768, + "0/40/5": "Water Leak Detector", + "0/40/6": "", + "0/40/7": 0, + "0/40/8": "", + "0/40/9": 234946562, + "0/40/10": "14.1.0.2", + "0/40/15": "", + "0/40/17": true, + "0/40/18": "", + "0/40/19": { + "0": 3, + "1": 3 + }, + "0/40/65528": [], + "0/40/65529": [], + "0/40/65530": [], + "0/40/65531": [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 15, 17, 18, 19, 65528, 65529, 65530, + 65531, 65532, 65533 + ], + "0/40/65532": 0, + "0/40/65533": 2, + "0/43/0": "en", + "0/43/1": ["en"], + "0/43/65528": [], + "0/43/65529": [], + "0/43/65530": [], + "0/43/65531": [0, 1, 65528, 65529, 65530, 65531, 65532, 65533], + "0/43/65532": 0, + "0/43/65533": 1, + "0/44/0": 1, + "0/44/1": 4, + "0/44/2": [], + "0/44/65528": [], + "0/44/65529": [], + "0/44/65530": [], + "0/44/65531": [0, 1, 2, 65528, 65529, 65530, 65531, 65532, 65533], + "0/44/65532": 0, + "0/44/65533": 1, + "0/48/0": 0, + "0/48/1": { + "0": 60, + "1": 900 + }, + "0/48/2": 2, + "0/48/3": 2, + "0/48/4": false, + "0/48/65528": [], + "0/48/65529": [], + "0/48/65530": [], + "0/48/65531": [0, 1, 2, 3, 4, 65528, 65529, 65530, 65531, 65532, 65533], + "0/48/65532": 0, + "0/48/65533": 1, + "0/49/3": 30, + "0/49/65528": [], + "0/49/65529": [], + "0/49/65530": [], + "0/49/65531": [3, 4, 65528, 65529, 65530, 65531, 65532, 65533], + "0/49/65532": 4, + "0/49/65533": 1, + "0/50/65528": [], + "0/50/65529": [], + "0/50/65530": [], + "0/50/65531": [65528, 65529, 65530, 65531, 65532, 65533], + "0/50/65532": 0, + "0/50/65533": 1, + "0/51/1": 7, + "0/51/2": 17, + "0/51/8": false, + "0/51/65528": [], + "0/51/65529": [], + "0/51/65530": [], + "0/51/65531": [0, 1, 2, 8, 65528, 65529, 65530, 65531, 65532, 65533], + "0/51/65532": 0, + "0/51/65533": 1, + "0/52/65528": [], + "0/52/65529": [], + "0/52/65530": [], + "0/52/65531": [65528, 65529, 65530, 65531, 65532, 65533], + "0/52/65532": 0, + "0/52/65533": 1, + "0/56/0": 1718979287000000, + "0/56/1": 3, + "0/56/7": 1718982887000000, + "0/56/65528": [], + "0/56/65529": [], + "0/56/65530": [], + "0/56/65531": [0, 1, 7, 65528, 65529, 65530, 65531, 65532, 65533], + "0/56/65532": 0, + "0/56/65533": 2, + "0/60/0": 0, + "0/60/65528": [], + "0/60/65529": [], + "0/60/65530": [], + "0/60/65531": [0, 1, 2, 65528, 65529, 65530, 65531, 65532, 65533], + "0/60/65532": 0, + "0/60/65533": 1, + "0/62/2": 5, + "0/62/3": 3, + "0/62/5": 3, + "0/62/65528": [], + "0/62/65529": [], + "0/62/65530": [], + "0/62/65531": [0, 1, 2, 3, 4, 5, 65528, 65529, 65530, 65531, 65532, 65533], + "0/62/65532": 0, + "0/62/65533": 1, + "0/63/65528": [], + "0/63/65529": [], + "0/63/65530": [], + "0/63/65531": [65528, 65529, 65530, 65531, 65532, 65533], + "0/63/65532": 0, + "0/63/65533": 2, + "1/3/0": 0, + "1/3/1": 0, + "1/3/65528": [], + "1/3/65529": [], + "1/3/65530": [], + "1/3/65531": [0, 1, 65528, 65529, 65530, 65531, 65532, 65533], + "1/3/65532": 0, + "1/3/65533": 4, + "1/4/65528": [], + "1/4/65529": [], + "1/4/65530": [], + "1/4/65531": [0, 65528, 65529, 65530, 65531, 65532, 65533], + "1/4/65532": 0, + "1/4/65533": 4, + "1/29/0": [ + { + "0": 67, + "1": 1 + } + ], + "1/29/1": [3, 4, 5, 29, 57, 69], + "1/29/2": [], + "1/29/3": [], + "1/29/65528": [], + "1/29/65529": [], + "1/29/65530": [], + "1/29/65531": [0, 1, 2, 3, 65528, 65529, 65530, 65531, 65532, 65533], + "1/29/65532": 0, + "1/29/65533": 1, + "1/69/0": true, + "1/69/65528": [], + "1/69/65529": [], + "1/69/65530": [], + "1/69/65531": [0, 65528, 65529, 65530, 65531, 65532, 65533], + "1/69/65532": 0, + "1/69/65533": 1 + }, + "attribute_subscriptions": [] +} diff --git a/tests/components/matter/test_binary_sensor.py b/tests/components/matter/test_binary_sensor.py index 24928520ee5..becedc0af62 100644 --- a/tests/components/matter/test_binary_sensor.py +++ b/tests/components/matter/test_binary_sensor.py @@ -32,29 +32,6 @@ def binary_sensor_platform() -> Generator[None]: yield -# This tests needs to be adjusted to remove lingering tasks -@pytest.mark.parametrize("expected_lingering_tasks", [True]) -async def test_contact_sensor( - hass: HomeAssistant, - matter_client: MagicMock, - eve_contact_sensor_node: MatterNode, -) -> None: - """Test contact sensor.""" - entity_id = "binary_sensor.eve_door_door" - state = hass.states.get(entity_id) - assert state - assert state.state == "on" - - set_node_attribute(eve_contact_sensor_node, 1, 69, 0, True) - await trigger_subscription_callback( - hass, matter_client, data=(eve_contact_sensor_node.node_id, "1/69/0", True) - ) - - state = hass.states.get(entity_id) - assert state - assert state.state == "off" - - @pytest.fixture(name="occupancy_sensor_node") async def occupancy_sensor_node_fixture( hass: HomeAssistant, matter_client: MagicMock @@ -87,6 +64,43 @@ async def test_occupancy_sensor( assert state.state == "off" +# This tests needs to be adjusted to remove lingering tasks +@pytest.mark.parametrize("expected_lingering_tasks", [True]) +@pytest.mark.parametrize( + ("fixture", "entity_id"), + [ + ("eve-contact-sensor", "binary_sensor.eve_door_door"), + ("leak-sensor", "binary_sensor.water_leak_detector_water_leak"), + ], +) +async def test_boolean_state_sensors( + hass: HomeAssistant, + matter_client: MagicMock, + fixture: str, + entity_id: str, +) -> None: + """Test if binary sensors get created from devices with Boolean State cluster.""" + node = await setup_integration_with_node_fixture( + hass, + fixture, + matter_client, + ) + state = hass.states.get(entity_id) + assert state + assert state.state == "on" + + # invert the value + cur_attr_value = node.get_attribute_value(1, 69, 0) + set_node_attribute(node, 1, 69, 0, not cur_attr_value) + await trigger_subscription_callback( + hass, matter_client, data=(node.node_id, "1/69/0", not cur_attr_value) + ) + + state = hass.states.get(entity_id) + assert state + assert state.state == "off" + + # This tests needs to be adjusted to remove lingering tasks @pytest.mark.parametrize("expected_lingering_tasks", [True]) async def test_battery_sensor( From f7e194b32c692713924c542a19b8576cccff507f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 21 Jun 2024 12:55:22 -0500 Subject: [PATCH 0957/1445] Adjust blocking I/O messages to provide developer help (#120113) --- homeassistant/util/loop.py | 54 ++++++++++++++++++++++++------------ tests/test_block_async_io.py | 11 +++----- tests/util/test_loop.py | 33 ++++++++++++++++++++-- 3 files changed, 70 insertions(+), 28 deletions(-) diff --git a/homeassistant/util/loop.py b/homeassistant/util/loop.py index 64be00cfe35..8a469569601 100644 --- a/homeassistant/util/loop.py +++ b/homeassistant/util/loop.py @@ -31,14 +31,9 @@ def raise_for_blocking_call( check_allowed: Callable[[dict[str, Any]], bool] | None = None, strict: bool = True, strict_core: bool = True, - advise_msg: str | None = None, **mapped_args: Any, ) -> None: - """Warn if called inside the event loop. Raise if `strict` is True. - - The default advisory message is 'Use `await hass.async_add_executor_job()' - Set `advise_msg` to an alternate message if the solution differs. - """ + """Warn if called inside the event loop. Raise if `strict` is True.""" if check_allowed is not None and check_allowed(mapped_args): return @@ -55,24 +50,31 @@ def raise_for_blocking_call( if not strict_core: _LOGGER.warning( "Detected blocking call to %s with args %s in %s, " - "line %s: %s inside the event loop\n" + "line %s: %s inside the event loop; " + "This is causing stability issues. " + "Please create a bug report at " + "https://github.com/home-assistant/core/issues?q=is%%3Aopen+is%%3Aissue\n" + "%s\n" "Traceback (most recent call last):\n%s", func.__name__, mapped_args.get("args"), offender_filename, offender_lineno, offender_line, + _dev_help_message(func.__name__), "".join(traceback.format_stack(f=offender_frame)), ) return if found_frame is None: raise RuntimeError( # noqa: TRY200 - f"Detected blocking call to {func.__name__} inside the event loop " - f"in {offender_filename}, line {offender_lineno}: {offender_line}. " - f"{advise_msg or 'Use `await hass.async_add_executor_job()`'}; " - "This is causing stability issues. Please create a bug report at " - f"https://github.com/home-assistant/core/issues?q=is%3Aopen+is%3Aissue" + f"Caught blocking call to {func.__name__} with args {mapped_args.get("args")} " + f"in {offender_filename}, line {offender_lineno}: {offender_line} " + "inside the event loop; " + "This is causing stability issues. " + "Please create a bug report at " + "https://github.com/home-assistant/core/issues?q=is%3Aopen+is%3Aissue\n" + f"{_dev_help_message(func.__name__)}" ) report_issue = async_suggest_report_issue( @@ -82,10 +84,13 @@ def raise_for_blocking_call( ) _LOGGER.warning( - "Detected blocking call to %s inside the event loop by %sintegration '%s' " + "Detected blocking call to %s with args %s " + "inside the event loop by %sintegration '%s' " "at %s, line %s: %s (offender: %s, line %s: %s), please %s\n" + "%s\n" "Traceback (most recent call last):\n%s", func.__name__, + mapped_args.get("args"), "custom " if integration_frame.custom_integration else "", integration_frame.integration, integration_frame.relative_filename, @@ -95,19 +100,32 @@ def raise_for_blocking_call( offender_lineno, offender_line, report_issue, + _dev_help_message(func.__name__), "".join(traceback.format_stack(f=integration_frame.frame)), ) if strict: raise RuntimeError( - "Blocking calls must be done in the executor or a separate thread;" - f" {advise_msg or 'Use `await hass.async_add_executor_job()`'}; at" - f" {integration_frame.relative_filename}, line {integration_frame.line_number}:" - f" {integration_frame.line} " - f"(offender: {offender_filename}, line {offender_lineno}: {offender_line})" + "Caught blocking call to {func.__name__} with args " + f"{mapped_args.get('args')} inside the event loop by" + f"{'custom ' if integration_frame.custom_integration else ''}" + "integration '{integration_frame.integration}' at " + f"{integration_frame.relative_filename}, line {integration_frame.line_number}:" + f" {integration_frame.line}. (offender: {offender_filename}, line " + f"{offender_lineno}: {offender_line}), please {report_issue}\n" + f"{_dev_help_message(func.__name__)}" ) +def _dev_help_message(what: str) -> str: + """Generate help message to guide developers.""" + return ( + "For developers, please see " + "https://developers.home-assistant.io/docs/asyncio_blocking_operations/" + f"#{what.replace('.', '')}" + ) + + def protect_loop[**_P, _R]( func: Callable[_P, _R], loop_thread_id: int, diff --git a/tests/test_block_async_io.py b/tests/test_block_async_io.py index 20089cf15b9..ae77fbee217 100644 --- a/tests/test_block_async_io.py +++ b/tests/test_block_async_io.py @@ -61,9 +61,7 @@ async def test_protect_loop_sleep() -> None: ] ) with ( - pytest.raises( - RuntimeError, match="Detected blocking call to sleep inside the event loop" - ), + pytest.raises(RuntimeError, match="Caught blocking call to sleep with args"), patch( "homeassistant.block_async_io.get_current_frame", return_value=frames, @@ -89,9 +87,7 @@ async def test_protect_loop_sleep_get_current_frame_raises() -> None: ] ) with ( - pytest.raises( - RuntimeError, match="Detected blocking call to sleep inside the event loop" - ), + pytest.raises(RuntimeError, match="Caught blocking call to sleep with args"), patch( "homeassistant.block_async_io.get_current_frame", side_effect=ValueError, @@ -204,7 +200,8 @@ async def test_protect_loop_importlib_import_module_in_integration( importlib.import_module("not_loaded_module") assert ( - "Detected blocking call to import_module inside the event loop by " + "Detected blocking call to import_module with args ('not_loaded_module',) " + "inside the event loop by " "integration 'hue' at homeassistant/components/hue/light.py, line 23" ) in caplog.text diff --git a/tests/util/test_loop.py b/tests/util/test_loop.py index 506614d7631..585f32a965f 100644 --- a/tests/util/test_loop.py +++ b/tests/util/test_loop.py @@ -28,6 +28,14 @@ async def test_raise_for_blocking_call_async_non_strict_core( haloop.raise_for_blocking_call(banned_function, strict_core=False) assert "Detected blocking call to banned_function" in caplog.text assert "Traceback (most recent call last)" in caplog.text + assert ( + "Please create a bug report at https://github.com/home-assistant/core/issues" + in caplog.text + ) + assert ( + "For developers, please see " + "https://developers.home-assistant.io/docs/asyncio_blocking_operations/#banned_function" + ) in caplog.text async def test_raise_for_blocking_call_async_integration( @@ -74,12 +82,17 @@ async def test_raise_for_blocking_call_async_integration( ): haloop.raise_for_blocking_call(banned_function) assert ( - "Detected blocking call to banned_function inside the event loop by integration" + "Detected blocking call to banned_function with args None" + " inside the event loop by integration" " 'hue' at homeassistant/components/hue/light.py, line 23: self.light.is_on " "(offender: /home/paulus/aiohue/lights.py, line 2: mock_line), please create " "a bug report at https://github.com/home-assistant/core/issues?" "q=is%3Aopen+is%3Aissue+label%3A%22integration%3A+hue%22" in caplog.text ) + assert ( + "For developers, please see " + "https://developers.home-assistant.io/docs/asyncio_blocking_operations/#banned_function" + ) in caplog.text async def test_raise_for_blocking_call_async_integration_non_strict( @@ -125,7 +138,8 @@ async def test_raise_for_blocking_call_async_integration_non_strict( ): haloop.raise_for_blocking_call(banned_function, strict=False) assert ( - "Detected blocking call to banned_function inside the event loop by integration" + "Detected blocking call to banned_function with args None" + " inside the event loop by integration" " 'hue' at homeassistant/components/hue/light.py, line 23: self.light.is_on " "(offender: /home/paulus/aiohue/lights.py, line 2: mock_line), " "please create a bug report at https://github.com/home-assistant/core/issues?" @@ -136,6 +150,14 @@ async def test_raise_for_blocking_call_async_integration_non_strict( 'File "/home/paulus/homeassistant/components/hue/light.py", line 23' in caplog.text ) + assert ( + "please create a bug report at https://github.com/home-assistant/core/issues" + in caplog.text + ) + assert ( + "For developers, please see " + "https://developers.home-assistant.io/docs/asyncio_blocking_operations/#banned_function" + ) in caplog.text async def test_raise_for_blocking_call_async_custom( @@ -182,7 +204,8 @@ async def test_raise_for_blocking_call_async_custom( ): haloop.raise_for_blocking_call(banned_function) assert ( - "Detected blocking call to banned_function inside the event loop by custom " + "Detected blocking call to banned_function with args None" + " inside the event loop by custom " "integration 'hue' at custom_components/hue/light.py, line 23: self.light.is_on" " (offender: /home/paulus/aiohue/lights.py, line 2: mock_line), " "please create a bug report at https://github.com/home-assistant/core/issues?" @@ -193,6 +216,10 @@ async def test_raise_for_blocking_call_async_custom( 'File "/home/paulus/config/custom_components/hue/light.py", line 23' in caplog.text ) + assert ( + "For developers, please see " + "https://developers.home-assistant.io/docs/asyncio_blocking_operations/#banned_function" + ) in caplog.text async def test_raise_for_blocking_call_sync( From c13efa36647410d2531937f026a59ae6ba734c72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc=20H=C3=B6rsken?= Date: Fri, 21 Jun 2024 20:08:08 +0200 Subject: [PATCH 0958/1445] Bump blinkpy to 0.23.0 (#119418) --- homeassistant/components/blink/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/blink/test_config_flow.py | 1 + 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/blink/manifest.json b/homeassistant/components/blink/manifest.json index 445a469b141..82f48a3c1ea 100644 --- a/homeassistant/components/blink/manifest.json +++ b/homeassistant/components/blink/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/blink", "iot_class": "cloud_polling", "loggers": ["blinkpy"], - "requirements": ["blinkpy==0.22.6"] + "requirements": ["blinkpy==0.23.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 422a87e202c..8f75ae00d32 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -578,7 +578,7 @@ bleak==0.22.1 blebox-uniapi==2.4.2 # homeassistant.components.blink -blinkpy==0.22.6 +blinkpy==0.23.0 # homeassistant.components.bitcoin blockchain==1.4.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 76fadaa6511..e635084616a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -500,7 +500,7 @@ bleak==0.22.1 blebox-uniapi==2.4.2 # homeassistant.components.blink -blinkpy==0.22.6 +blinkpy==0.23.0 # homeassistant.components.blue_current bluecurrent-api==1.2.3 diff --git a/tests/components/blink/test_config_flow.py b/tests/components/blink/test_config_flow.py index 82ea847dcf2..9c3193ec7d6 100644 --- a/tests/components/blink/test_config_flow.py +++ b/tests/components/blink/test_config_flow.py @@ -49,6 +49,7 @@ async def test_form(hass: HomeAssistant) -> None: "account_id": None, "client_id": None, "region_id": None, + "user_id": None, } assert len(mock_setup_entry.mock_calls) == 1 From ba7388546efb380dccf3a7bc07841633e9002e43 Mon Sep 17 00:00:00 2001 From: tronikos Date: Fri, 21 Jun 2024 11:17:04 -0700 Subject: [PATCH 0959/1445] Implement Android TV Remote browse media with apps and activity list (#117126) --- .../androidtv_remote/config_flow.py | 101 +++++++++++++++- .../components/androidtv_remote/const.py | 3 + .../components/androidtv_remote/entity.py | 5 +- .../androidtv_remote/media_player.py | 41 ++++++- .../components/androidtv_remote/remote.py | 24 +++- .../components/androidtv_remote/strings.json | 11 ++ .../androidtv_remote/test_config_flow.py | 108 ++++++++++++++++-- .../androidtv_remote/test_media_player.py | 88 ++++++++++++++ .../androidtv_remote/test_remote.py | 22 ++++ 9 files changed, 388 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/androidtv_remote/config_flow.py b/homeassistant/components/androidtv_remote/config_flow.py index a9b32c22700..813c0eda14b 100644 --- a/homeassistant/components/androidtv_remote/config_flow.py +++ b/homeassistant/components/androidtv_remote/config_flow.py @@ -24,12 +24,22 @@ from homeassistant.config_entries import ( from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME from homeassistant.core import callback from homeassistant.helpers.device_registry import format_mac +from homeassistant.helpers.selector import ( + SelectOptionDict, + SelectSelector, + SelectSelectorConfig, + SelectSelectorMode, +) -from .const import CONF_ENABLE_IME, DOMAIN +from .const import CONF_APP_ICON, CONF_APP_NAME, CONF_APPS, CONF_ENABLE_IME, DOMAIN from .helpers import create_api, get_enable_ime _LOGGER = logging.getLogger(__name__) +APPS_NEW_ID = "NewApp" +CONF_APP_DELETE = "app_delete" +CONF_APP_ID = "app_id" + STEP_USER_DATA_SCHEMA = vol.Schema( { vol.Required("host"): str, @@ -213,17 +223,46 @@ class AndroidTVRemoteConfigFlow(ConfigFlow, domain=DOMAIN): class AndroidTVRemoteOptionsFlowHandler(OptionsFlowWithConfigEntry): """Android TV Remote options flow.""" + def __init__(self, config_entry: ConfigEntry) -> None: + """Initialize options flow.""" + super().__init__(config_entry) + self._apps: dict[str, Any] = self.options.setdefault(CONF_APPS, {}) + self._conf_app_id: str | None = None + + @callback + def _save_config(self, data: dict[str, Any]) -> ConfigFlowResult: + """Save the updated options.""" + new_data = {k: v for k, v in data.items() if k not in [CONF_APPS]} + if self._apps: + new_data[CONF_APPS] = self._apps + + return self.async_create_entry(title="", data=new_data) + async def async_step_init( self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: """Manage the options.""" if user_input is not None: - return self.async_create_entry(title="", data=user_input) + if sel_app := user_input.get(CONF_APPS): + return await self.async_step_apps(None, sel_app) + return self._save_config(user_input) + apps_list = { + k: f"{v[CONF_APP_NAME]} ({k})" if CONF_APP_NAME in v else k + for k, v in self._apps.items() + } + apps = [SelectOptionDict(value=APPS_NEW_ID, label="Add new")] + [ + SelectOptionDict(value=k, label=v) for k, v in apps_list.items() + ] return self.async_show_form( step_id="init", data_schema=vol.Schema( { + vol.Optional(CONF_APPS): SelectSelector( + SelectSelectorConfig( + options=apps, mode=SelectSelectorMode.DROPDOWN + ) + ), vol.Required( CONF_ENABLE_IME, default=get_enable_ime(self.config_entry), @@ -231,3 +270,61 @@ class AndroidTVRemoteOptionsFlowHandler(OptionsFlowWithConfigEntry): } ), ) + + async def async_step_apps( + self, user_input: dict[str, Any] | None = None, app_id: str | None = None + ) -> ConfigFlowResult: + """Handle options flow for apps list.""" + if app_id is not None: + self._conf_app_id = app_id if app_id != APPS_NEW_ID else None + return self._async_apps_form(app_id) + + if user_input is not None: + app_id = user_input.get(CONF_APP_ID, self._conf_app_id) + if app_id: + if user_input.get(CONF_APP_DELETE, False): + self._apps.pop(app_id) + else: + self._apps[app_id] = { + CONF_APP_NAME: user_input.get(CONF_APP_NAME, ""), + CONF_APP_ICON: user_input.get(CONF_APP_ICON, ""), + } + + return await self.async_step_init() + + @callback + def _async_apps_form(self, app_id: str) -> ConfigFlowResult: + """Return configuration form for apps.""" + + app_schema = { + vol.Optional( + CONF_APP_NAME, + description={ + "suggested_value": self._apps[app_id].get(CONF_APP_NAME, "") + if app_id in self._apps + else "" + }, + ): str, + vol.Optional( + CONF_APP_ICON, + description={ + "suggested_value": self._apps[app_id].get(CONF_APP_ICON, "") + if app_id in self._apps + else "" + }, + ): str, + } + if app_id == APPS_NEW_ID: + data_schema = vol.Schema({**app_schema, vol.Optional(CONF_APP_ID): str}) + else: + data_schema = vol.Schema( + {**app_schema, vol.Optional(CONF_APP_DELETE, default=False): bool} + ) + + return self.async_show_form( + step_id="apps", + data_schema=data_schema, + description_placeholders={ + "app_id": f"`{app_id}`" if app_id != APPS_NEW_ID else "", + }, + ) diff --git a/homeassistant/components/androidtv_remote/const.py b/homeassistant/components/androidtv_remote/const.py index 9d2a7fcb240..540c8186e20 100644 --- a/homeassistant/components/androidtv_remote/const.py +++ b/homeassistant/components/androidtv_remote/const.py @@ -6,5 +6,8 @@ from typing import Final DOMAIN: Final = "androidtv_remote" +CONF_APPS = "apps" CONF_ENABLE_IME: Final = "enable_ime" CONF_ENABLE_IME_DEFAULT_VALUE: Final = True +CONF_APP_NAME = "app_name" +CONF_APP_ICON = "app_icon" diff --git a/homeassistant/components/androidtv_remote/entity.py b/homeassistant/components/androidtv_remote/entity.py index fa070e1ec18..44b2d2a5f20 100644 --- a/homeassistant/components/androidtv_remote/entity.py +++ b/homeassistant/components/androidtv_remote/entity.py @@ -2,6 +2,8 @@ from __future__ import annotations +from typing import Any + from androidtvremote2 import AndroidTVRemote, ConnectionClosed from homeassistant.config_entries import ConfigEntry @@ -11,7 +13,7 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo from homeassistant.helpers.entity import Entity -from .const import DOMAIN +from .const import CONF_APPS, DOMAIN class AndroidTVRemoteBaseEntity(Entity): @@ -26,6 +28,7 @@ class AndroidTVRemoteBaseEntity(Entity): self._api = api self._host = config_entry.data[CONF_HOST] self._name = config_entry.data[CONF_NAME] + self._apps: dict[str, Any] = config_entry.options.get(CONF_APPS, {}) self._attr_unique_id = config_entry.unique_id self._attr_is_on = api.is_on device_info = api.device_info diff --git a/homeassistant/components/androidtv_remote/media_player.py b/homeassistant/components/androidtv_remote/media_player.py index 571eab4a15b..554aa2f2946 100644 --- a/homeassistant/components/androidtv_remote/media_player.py +++ b/homeassistant/components/androidtv_remote/media_player.py @@ -8,17 +8,20 @@ from typing import Any from androidtvremote2 import AndroidTVRemote, ConnectionClosed from homeassistant.components.media_player import ( + MediaClass, MediaPlayerDeviceClass, MediaPlayerEntity, MediaPlayerEntityFeature, MediaPlayerState, MediaType, ) +from homeassistant.components.media_player.browse_media import BrowseMedia from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import AndroidTVRemoteConfigEntry +from .const import CONF_APP_ICON, CONF_APP_NAME from .entity import AndroidTVRemoteBaseEntity PARALLEL_UPDATES = 0 @@ -50,6 +53,7 @@ class AndroidTVRemoteMediaPlayerEntity(AndroidTVRemoteBaseEntity, MediaPlayerEnt | MediaPlayerEntityFeature.PLAY | MediaPlayerEntityFeature.STOP | MediaPlayerEntityFeature.PLAY_MEDIA + | MediaPlayerEntityFeature.BROWSE_MEDIA ) def __init__( @@ -65,7 +69,11 @@ class AndroidTVRemoteMediaPlayerEntity(AndroidTVRemoteBaseEntity, MediaPlayerEnt def _update_current_app(self, current_app: str) -> None: """Update current app info.""" self._attr_app_id = current_app - self._attr_app_name = current_app + self._attr_app_name = ( + self._apps[current_app].get(CONF_APP_NAME, current_app) + if current_app in self._apps + else current_app + ) def _update_volume_info(self, volume_info: dict[str, str | bool]) -> None: """Update volume info.""" @@ -176,12 +184,41 @@ class AndroidTVRemoteMediaPlayerEntity(AndroidTVRemoteBaseEntity, MediaPlayerEnt await self._channel_set_task return - if media_type == MediaType.URL: + if media_type in [MediaType.URL, MediaType.APP]: self._send_launch_app_command(media_id) return raise ValueError(f"Invalid media type: {media_type}") + async def async_browse_media( + self, + media_content_type: MediaType | str | None = None, + media_content_id: str | None = None, + ) -> BrowseMedia: + """Browse apps.""" + children = [ + BrowseMedia( + media_class=MediaClass.APP, + media_content_type=MediaType.APP, + media_content_id=app_id, + title=app.get(CONF_APP_NAME, ""), + thumbnail=app.get(CONF_APP_ICON, ""), + can_play=False, + can_expand=False, + ) + for app_id, app in self._apps.items() + ] + return BrowseMedia( + title="Applications", + media_class=MediaClass.DIRECTORY, + media_content_id="apps", + media_content_type=MediaType.APPS, + children_media_class=MediaClass.APP, + can_play=False, + can_expand=True, + children=children, + ) + async def _send_key_commands( self, key_codes: list[str], delay_secs: float = 0.1 ) -> None: diff --git a/homeassistant/components/androidtv_remote/remote.py b/homeassistant/components/androidtv_remote/remote.py index 72387a54bf0..c9a261c8735 100644 --- a/homeassistant/components/androidtv_remote/remote.py +++ b/homeassistant/components/androidtv_remote/remote.py @@ -21,6 +21,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import AndroidTVRemoteConfigEntry +from .const import CONF_APP_NAME from .entity import AndroidTVRemoteBaseEntity PARALLEL_UPDATES = 0 @@ -41,17 +42,28 @@ class AndroidTVRemoteEntity(AndroidTVRemoteBaseEntity, RemoteEntity): _attr_supported_features = RemoteEntityFeature.ACTIVITY + def _update_current_app(self, current_app: str) -> None: + """Update current app info.""" + self._attr_current_activity = ( + self._apps[current_app].get(CONF_APP_NAME, current_app) + if current_app in self._apps + else current_app + ) + @callback def _current_app_updated(self, current_app: str) -> None: """Update the state when the current app changes.""" - self._attr_current_activity = current_app + self._update_current_app(current_app) self.async_write_ha_state() async def async_added_to_hass(self) -> None: """Register callbacks.""" await super().async_added_to_hass() - self._attr_current_activity = self._api.current_app + self._attr_activity_list = [ + app.get(CONF_APP_NAME, "") for app in self._apps.values() + ] + self._update_current_app(self._api.current_app) self._api.add_current_app_updated_callback(self._current_app_updated) async def async_will_remove_from_hass(self) -> None: @@ -66,6 +78,14 @@ class AndroidTVRemoteEntity(AndroidTVRemoteBaseEntity, RemoteEntity): self._send_key_command("POWER") activity = kwargs.get(ATTR_ACTIVITY, "") if activity: + activity = next( + ( + app_id + for app_id, app in self._apps.items() + if app.get(CONF_APP_NAME, "") == activity + ), + activity, + ) self._send_launch_app_command(activity) async def async_turn_off(self, **kwargs: Any) -> None: diff --git a/homeassistant/components/androidtv_remote/strings.json b/homeassistant/components/androidtv_remote/strings.json index da9bdd8bd3b..33970171d40 100644 --- a/homeassistant/components/androidtv_remote/strings.json +++ b/homeassistant/components/androidtv_remote/strings.json @@ -39,8 +39,19 @@ "step": { "init": { "data": { + "apps": "Configure applications list", "enable_ime": "Enable IME. Needed for getting the current app. Disable for devices that show 'Use keyboard on mobile device screen' instead of the on screen keyboard." } + }, + "apps": { + "title": "Configure Android Apps", + "description": "Configure application id {app_id}", + "data": { + "app_name": "Application Name", + "app_id": "Application ID", + "app_icon": "Application Icon", + "app_delete": "Check to delete this application" + } } } } diff --git a/tests/components/androidtv_remote/test_config_flow.py b/tests/components/androidtv_remote/test_config_flow.py index 062b9a4a55c..93c9067d1c8 100644 --- a/tests/components/androidtv_remote/test_config_flow.py +++ b/tests/components/androidtv_remote/test_config_flow.py @@ -7,7 +7,18 @@ from androidtvremote2 import CannotConnect, ConnectionClosed, InvalidAuth from homeassistant import config_entries from homeassistant.components import zeroconf -from homeassistant.components.androidtv_remote.const import DOMAIN +from homeassistant.components.androidtv_remote.config_flow import ( + APPS_NEW_ID, + CONF_APP_DELETE, + CONF_APP_ID, +) +from homeassistant.components.androidtv_remote.const import ( + CONF_APP_ICON, + CONF_APP_NAME, + CONF_APPS, + CONF_ENABLE_IME, + DOMAIN, +) from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -886,14 +897,14 @@ async def test_options_flow( assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" data_schema = result["data_schema"].schema - assert set(data_schema) == {"enable_ime"} + assert set(data_schema) == {CONF_APPS, CONF_ENABLE_IME} result = await hass.config_entries.options.async_configure( result["flow_id"], - user_input={"enable_ime": False}, + user_input={CONF_ENABLE_IME: False}, ) assert result["type"] is FlowResultType.CREATE_ENTRY - assert mock_config_entry.options == {"enable_ime": False} + assert mock_config_entry.options == {CONF_ENABLE_IME: False} await hass.async_block_till_done() assert mock_api.disconnect.call_count == 1 @@ -903,10 +914,10 @@ async def test_options_flow( result = await hass.config_entries.options.async_init(mock_config_entry.entry_id) result = await hass.config_entries.options.async_configure( result["flow_id"], - user_input={"enable_ime": False}, + user_input={CONF_ENABLE_IME: False}, ) assert result["type"] is FlowResultType.CREATE_ENTRY - assert mock_config_entry.options == {"enable_ime": False} + assert mock_config_entry.options == {CONF_ENABLE_IME: False} await hass.async_block_till_done() assert mock_api.disconnect.call_count == 1 @@ -916,11 +927,92 @@ async def test_options_flow( result = await hass.config_entries.options.async_init(mock_config_entry.entry_id) result = await hass.config_entries.options.async_configure( result["flow_id"], - user_input={"enable_ime": True}, + user_input={CONF_ENABLE_IME: True}, ) assert result["type"] is FlowResultType.CREATE_ENTRY - assert mock_config_entry.options == {"enable_ime": True} + assert mock_config_entry.options == {CONF_ENABLE_IME: True} await hass.async_block_till_done() assert mock_api.disconnect.call_count == 2 assert mock_api.async_connect.call_count == 3 + + # test app form with new app + result = await hass.config_entries.options.async_init(mock_config_entry.entry_id) + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_APPS: APPS_NEW_ID, + }, + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "apps" + + # test save value for new app + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_APP_ID: "app1", + CONF_APP_NAME: "App1", + CONF_APP_ICON: "Icon1", + }, + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "init" + + # test app form with existing app + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_APPS: "app1", + }, + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "apps" + + # test change value in apps form + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_APP_NAME: "Application1", + CONF_APP_ICON: "Icon1", + }, + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "init" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], user_input={} + ) + assert result["type"] is FlowResultType.CREATE_ENTRY + assert mock_config_entry.options == { + CONF_APPS: {"app1": {CONF_APP_NAME: "Application1", CONF_APP_ICON: "Icon1"}}, + CONF_ENABLE_IME: True, + } + await hass.async_block_till_done() + + # test app form for delete + result = await hass.config_entries.options.async_init(mock_config_entry.entry_id) + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_APPS: "app1", + }, + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "apps" + + # test delete app1 + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_APP_DELETE: True, + }, + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "init" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], user_input={} + ) + assert result["type"] is FlowResultType.CREATE_ENTRY + assert mock_config_entry.options == {CONF_ENABLE_IME: True} diff --git a/tests/components/androidtv_remote/test_media_player.py b/tests/components/androidtv_remote/test_media_player.py index c7937e9e02d..ad7c049e32f 100644 --- a/tests/components/androidtv_remote/test_media_player.py +++ b/tests/components/androidtv_remote/test_media_player.py @@ -11,6 +11,7 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from tests.common import MockConfigEntry +from tests.typing import WebSocketGenerator MEDIA_PLAYER_ENTITY = "media_player.my_android_tv" @@ -19,6 +20,9 @@ async def test_media_player_receives_push_updates( hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_api: MagicMock ) -> None: """Test the Android TV Remote media player receives push updates and state is updated.""" + mock_config_entry.options = { + "apps": {"com.google.android.youtube.tv": {"app_name": "YouTube"}} + } mock_config_entry.add_to_hass(hass) await hass.config_entries.async_setup(mock_config_entry.entry_id) assert mock_config_entry.state is ConfigEntryState.LOADED @@ -39,6 +43,13 @@ async def test_media_player_receives_push_updates( == "com.google.android.tvlauncher" ) + mock_api._on_current_app_updated("com.google.android.youtube.tv") + assert ( + hass.states.get(MEDIA_PLAYER_ENTITY).attributes.get("app_id") + == "com.google.android.youtube.tv" + ) + assert hass.states.get(MEDIA_PLAYER_ENTITY).attributes.get("app_name") == "YouTube" + mock_api._on_volume_info_updated({"level": 35, "muted": False, "max": 100}) assert hass.states.get(MEDIA_PLAYER_ENTITY).attributes.get("volume_level") == 0.35 @@ -267,6 +278,18 @@ async def test_media_player_play_media( ) mock_api.send_launch_app_command.assert_called_with("https://www.youtube.com") + await hass.services.async_call( + "media_player", + "play_media", + { + "entity_id": MEDIA_PLAYER_ENTITY, + "media_content_type": "app", + "media_content_id": "tv.twitch.android.app", + }, + blocking=True, + ) + mock_api.send_launch_app_command.assert_called_with("tv.twitch.android.app") + with pytest.raises(ValueError): await hass.services.async_call( "media_player", @@ -292,6 +315,71 @@ async def test_media_player_play_media( ) +async def test_browse_media( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + mock_config_entry: MockConfigEntry, + mock_api: MagicMock, +) -> None: + """Test the Android TV Remote media player browse media.""" + mock_config_entry.options = { + "apps": { + "com.google.android.youtube.tv": { + "app_name": "YouTube", + "app_icon": "https://www.youtube.com/icon.png", + }, + "org.xbmc.kodi": {"app_name": "Kodi"}, + } + } + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + assert mock_config_entry.state is ConfigEntryState.LOADED + + client = await hass_ws_client() + await client.send_json( + { + "id": 1, + "type": "media_player/browse_media", + "entity_id": MEDIA_PLAYER_ENTITY, + } + ) + response = await client.receive_json() + assert response["success"] + assert { + "title": "Applications", + "media_class": "directory", + "media_content_type": "apps", + "media_content_id": "apps", + "children_media_class": "app", + "can_play": False, + "can_expand": True, + "thumbnail": None, + "not_shown": 0, + "children": [ + { + "title": "YouTube", + "media_class": "app", + "media_content_type": "app", + "media_content_id": "com.google.android.youtube.tv", + "children_media_class": None, + "can_play": False, + "can_expand": False, + "thumbnail": "https://www.youtube.com/icon.png", + }, + { + "title": "Kodi", + "media_class": "app", + "media_content_type": "app", + "media_content_id": "org.xbmc.kodi", + "children_media_class": None, + "can_play": False, + "can_expand": False, + "thumbnail": "", + }, + ], + } == response["result"] + + async def test_media_player_connection_closed( hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_api: MagicMock ) -> None: diff --git a/tests/components/androidtv_remote/test_remote.py b/tests/components/androidtv_remote/test_remote.py index eba955a6aba..7ca63685747 100644 --- a/tests/components/androidtv_remote/test_remote.py +++ b/tests/components/androidtv_remote/test_remote.py @@ -19,6 +19,9 @@ async def test_remote_receives_push_updates( hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_api: MagicMock ) -> None: """Test the Android TV Remote receives push updates and state is updated.""" + mock_config_entry.options = { + "apps": {"com.google.android.youtube.tv": {"app_name": "YouTube"}} + } mock_config_entry.add_to_hass(hass) await hass.config_entries.async_setup(mock_config_entry.entry_id) assert mock_config_entry.state is ConfigEntryState.LOADED @@ -34,6 +37,11 @@ async def test_remote_receives_push_updates( hass.states.get(REMOTE_ENTITY).attributes.get("current_activity") == "activity1" ) + mock_api._on_current_app_updated("com.google.android.youtube.tv") + assert ( + hass.states.get(REMOTE_ENTITY).attributes.get("current_activity") == "YouTube" + ) + mock_api._on_is_available_updated(False) assert hass.states.is_state(REMOTE_ENTITY, STATE_UNAVAILABLE) @@ -45,6 +53,9 @@ async def test_remote_toggles( hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_api: MagicMock ) -> None: """Test the Android TV Remote toggles.""" + mock_config_entry.options = { + "apps": {"com.google.android.youtube.tv": {"app_name": "YouTube"}} + } mock_config_entry.add_to_hass(hass) await hass.config_entries.async_setup(mock_config_entry.entry_id) assert mock_config_entry.state is ConfigEntryState.LOADED @@ -81,6 +92,17 @@ async def test_remote_toggles( assert mock_api.send_key_command.call_count == 2 assert mock_api.send_launch_app_command.call_count == 1 + await hass.services.async_call( + "remote", + "turn_on", + {"entity_id": REMOTE_ENTITY, "activity": "YouTube"}, + blocking=True, + ) + + mock_api.send_key_command.send_launch_app_command("com.google.android.youtube.tv") + assert mock_api.send_key_command.call_count == 2 + assert mock_api.send_launch_app_command.call_count == 2 + async def test_remote_send_command( hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_api: MagicMock From d6be73328795922f7ff2c1d3517150a78a699990 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Fri, 21 Jun 2024 20:23:47 +0200 Subject: [PATCH 0960/1445] Add config flow to Feedreader (#118047) --- .../components/feedreader/__init__.py | 126 ++++++-- .../components/feedreader/config_flow.py | 195 ++++++++++++ homeassistant/components/feedreader/const.py | 6 + .../components/feedreader/coordinator.py | 35 +- .../components/feedreader/manifest.json | 1 + .../components/feedreader/strings.json | 47 +++ homeassistant/generated/config_flows.py | 1 + homeassistant/generated/integrations.json | 2 +- tests/components/feedreader/__init__.py | 47 +++ tests/components/feedreader/conftest.py | 58 ++++ tests/components/feedreader/const.py | 14 + .../components/feedreader/test_config_flow.py | 298 ++++++++++++++++++ tests/components/feedreader/test_init.py | 267 +++++++--------- 13 files changed, 897 insertions(+), 200 deletions(-) create mode 100644 homeassistant/components/feedreader/config_flow.py create mode 100644 homeassistant/components/feedreader/strings.json create mode 100644 tests/components/feedreader/conftest.py create mode 100644 tests/components/feedreader/const.py create mode 100644 tests/components/feedreader/test_config_flow.py diff --git a/homeassistant/components/feedreader/__init__.py b/homeassistant/components/feedreader/__init__.py index 1a87a61bfd2..36ffe545996 100644 --- a/homeassistant/components/feedreader/__init__.py +++ b/homeassistant/components/feedreader/__init__.py @@ -2,61 +2,119 @@ from __future__ import annotations -import asyncio -from datetime import timedelta - import voluptuous as vol -from homeassistant.const import CONF_SCAN_INTERVAL -from homeassistant.core import HomeAssistant +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.const import CONF_SCAN_INTERVAL, CONF_URL +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType +from homeassistant.util.hass_dict import HassKey -from .const import DOMAIN +from .const import CONF_MAX_ENTRIES, DEFAULT_MAX_ENTRIES, DEFAULT_SCAN_INTERVAL, DOMAIN from .coordinator import FeedReaderCoordinator, StoredData +type FeedReaderConfigEntry = ConfigEntry[FeedReaderCoordinator] + CONF_URLS = "urls" -CONF_MAX_ENTRIES = "max_entries" - -DEFAULT_MAX_ENTRIES = 20 -DEFAULT_SCAN_INTERVAL = timedelta(hours=1) +MY_KEY: HassKey[StoredData] = HassKey(DOMAIN) CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: { - vol.Required(CONF_URLS): vol.All(cv.ensure_list, [cv.url]), - vol.Optional( - CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL - ): cv.time_period, - vol.Optional( - CONF_MAX_ENTRIES, default=DEFAULT_MAX_ENTRIES - ): cv.positive_int, - } - }, + vol.All( + cv.deprecated(DOMAIN), + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_URLS): vol.All(cv.ensure_list, [cv.url]), + vol.Optional( + CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL + ): cv.time_period, + vol.Optional( + CONF_MAX_ENTRIES, default=DEFAULT_MAX_ENTRIES + ): cv.positive_int, + } + ) + }, + ), extra=vol.ALLOW_EXTRA, ) async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Feedreader component.""" - urls: list[str] = config[DOMAIN][CONF_URLS] - if not urls: - return False + if DOMAIN in config: + for url in config[DOMAIN][CONF_URLS]: + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data={ + CONF_URL: url, + CONF_MAX_ENTRIES: config[DOMAIN][CONF_MAX_ENTRIES], + }, + ) + ) - scan_interval: timedelta = config[DOMAIN][CONF_SCAN_INTERVAL] - max_entries: int = config[DOMAIN][CONF_MAX_ENTRIES] - storage = StoredData(hass) - await storage.async_setup() - feeds = [ - FeedReaderCoordinator(hass, url, scan_interval, max_entries, storage) - for url in urls - ] + async_create_issue( + hass, + HOMEASSISTANT_DOMAIN, + f"deprecated_yaml_{DOMAIN}", + breaks_in_ha_version="2025.1.0", + is_fixable=False, + is_persistent=False, + issue_domain=DOMAIN, + severity=IssueSeverity.WARNING, + translation_key="deprecated_yaml", + translation_placeholders={ + "domain": DOMAIN, + "integration_title": "Feedreader", + }, + ) - await asyncio.gather(*[feed.async_refresh() for feed in feeds]) + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: FeedReaderConfigEntry) -> bool: + """Set up Feedreader from a config entry.""" + storage = hass.data.setdefault(MY_KEY, StoredData(hass)) + if not storage.is_initialized: + await storage.async_setup() + + coordinator = FeedReaderCoordinator( + hass, + entry.data[CONF_URL], + entry.options[CONF_MAX_ENTRIES], + storage, + ) + + await coordinator.async_config_entry_first_refresh() # workaround because coordinators without listeners won't update # can be removed when we have entities to update - [feed.async_add_listener(lambda: None) for feed in feeds] + coordinator.async_add_listener(lambda: None) + + entry.runtime_data = coordinator + + entry.async_on_unload(entry.add_update_listener(_async_update_listener)) return True + + +async def async_unload_entry(hass: HomeAssistant, entry: FeedReaderConfigEntry) -> bool: + """Unload a config entry.""" + entries = hass.config_entries.async_entries( + DOMAIN, include_disabled=False, include_ignore=False + ) + # if this is the last entry, remove the storage + if len(entries) == 1: + hass.data.pop(MY_KEY) + return True + + +async def _async_update_listener( + hass: HomeAssistant, entry: FeedReaderConfigEntry +) -> None: + """Handle reconfiguration.""" + await hass.config_entries.async_reload(entry.entry_id) diff --git a/homeassistant/components/feedreader/config_flow.py b/homeassistant/components/feedreader/config_flow.py new file mode 100644 index 00000000000..6fa153b8177 --- /dev/null +++ b/homeassistant/components/feedreader/config_flow.py @@ -0,0 +1,195 @@ +"""Config flow for RSS/Atom feeds.""" + +from __future__ import annotations + +import logging +from typing import TYPE_CHECKING, Any +import urllib.error + +import feedparser +import voluptuous as vol + +from homeassistant.config_entries import ( + SOURCE_IMPORT, + ConfigEntry, + ConfigFlow, + ConfigFlowResult, + OptionsFlow, + OptionsFlowWithConfigEntry, +) +from homeassistant.const import CONF_URL +from homeassistant.core import HomeAssistant, callback +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue +from homeassistant.helpers.selector import ( + TextSelector, + TextSelectorConfig, + TextSelectorType, +) +from homeassistant.util import slugify + +from .const import CONF_MAX_ENTRIES, DEFAULT_MAX_ENTRIES, DOMAIN + +LOGGER = logging.getLogger(__name__) + + +async def async_fetch_feed(hass: HomeAssistant, url: str) -> feedparser.FeedParserDict: + """Fetch the feed.""" + return await hass.async_add_executor_job(feedparser.parse, url) + + +class FeedReaderConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a config flow.""" + + VERSION = 1 + _config_entry: ConfigEntry + _max_entries: int | None = None + + @staticmethod + @callback + def async_get_options_flow(config_entry: ConfigEntry) -> OptionsFlow: + """Get the options flow for this handler.""" + return FeedReaderOptionsFlowHandler(config_entry) + + def show_user_form( + self, + user_input: dict[str, Any] | None = None, + errors: dict[str, str] | None = None, + description_placeholders: dict[str, str] | None = None, + step_id: str = "user", + ) -> ConfigFlowResult: + """Show the user form.""" + if user_input is None: + user_input = {} + return self.async_show_form( + step_id=step_id, + data_schema=vol.Schema( + { + vol.Required( + CONF_URL, default=user_input.get(CONF_URL, "") + ): TextSelector(TextSelectorConfig(type=TextSelectorType.URL)) + } + ), + description_placeholders=description_placeholders, + errors=errors, + ) + + def abort_on_import_error(self, url: str, error: str) -> ConfigFlowResult: + """Abort import flow on error.""" + async_create_issue( + self.hass, + DOMAIN, + f"import_yaml_error_{DOMAIN}_{error}_{slugify(url)}", + breaks_in_ha_version="2025.1.0", + is_fixable=False, + issue_domain=DOMAIN, + severity=IssueSeverity.WARNING, + translation_key=f"import_yaml_error_{error}", + translation_placeholders={"url": url}, + ) + return self.async_abort(reason=error) + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle a flow initialized by the user.""" + if not user_input: + return self.show_user_form() + + self._async_abort_entries_match({CONF_URL: user_input[CONF_URL]}) + + feed = await async_fetch_feed(self.hass, user_input[CONF_URL]) + + if feed.bozo: + LOGGER.debug("feed bozo_exception: %s", feed.bozo_exception) + if isinstance(feed.bozo_exception, urllib.error.URLError): + if self.context["source"] == SOURCE_IMPORT: + return self.abort_on_import_error(user_input[CONF_URL], "url_error") + return self.show_user_form(user_input, {"base": "url_error"}) + + if not feed.entries: + if self.context["source"] == SOURCE_IMPORT: + return self.abort_on_import_error( + user_input[CONF_URL], "no_feed_entries" + ) + return self.show_user_form(user_input, {"base": "no_feed_entries"}) + + feed_title = feed["feed"]["title"] + + return self.async_create_entry( + title=feed_title, + data=user_input, + options={CONF_MAX_ENTRIES: self._max_entries or DEFAULT_MAX_ENTRIES}, + ) + + async def async_step_import(self, user_input: dict[str, Any]) -> ConfigFlowResult: + """Handle an import flow.""" + self._max_entries = user_input[CONF_MAX_ENTRIES] + return await self.async_step_user({CONF_URL: user_input[CONF_URL]}) + + async def async_step_reconfigure( + self, _: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle a reconfiguration flow initialized by the user.""" + config_entry = self.hass.config_entries.async_get_entry( + self.context["entry_id"] + ) + if TYPE_CHECKING: + assert config_entry is not None + self._config_entry = config_entry + return await self.async_step_reconfigure_confirm() + + async def async_step_reconfigure_confirm( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle a reconfiguration flow initialized by the user.""" + if not user_input: + return self.show_user_form( + user_input={**self._config_entry.data}, + description_placeholders={"name": self._config_entry.title}, + step_id="reconfigure_confirm", + ) + + feed = await async_fetch_feed(self.hass, user_input[CONF_URL]) + + if feed.bozo: + LOGGER.debug("feed bozo_exception: %s", feed.bozo_exception) + if isinstance(feed.bozo_exception, urllib.error.URLError): + return self.show_user_form( + user_input=user_input, + description_placeholders={"name": self._config_entry.title}, + step_id="reconfigure_confirm", + errors={"base": "url_error"}, + ) + if not feed.entries: + return self.show_user_form( + user_input=user_input, + description_placeholders={"name": self._config_entry.title}, + step_id="reconfigure_confirm", + errors={"base": "no_feed_entries"}, + ) + + self.hass.config_entries.async_update_entry(self._config_entry, data=user_input) + return self.async_abort(reason="reconfigure_successful") + + +class FeedReaderOptionsFlowHandler(OptionsFlowWithConfigEntry): + """Handle an options flow.""" + + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle options flow.""" + + if user_input is not None: + return self.async_create_entry(title="", data=user_input) + + data_schema = vol.Schema( + { + vol.Optional( + CONF_MAX_ENTRIES, + default=self.options.get(CONF_MAX_ENTRIES, DEFAULT_MAX_ENTRIES), + ): cv.positive_int, + } + ) + return self.async_show_form(step_id="init", data_schema=data_schema) diff --git a/homeassistant/components/feedreader/const.py b/homeassistant/components/feedreader/const.py index 05edf85ec13..c0aa6633669 100644 --- a/homeassistant/components/feedreader/const.py +++ b/homeassistant/components/feedreader/const.py @@ -1,3 +1,9 @@ """Constants for RSS/Atom feeds.""" +from datetime import timedelta + DOMAIN = "feedreader" + +CONF_MAX_ENTRIES = "max_entries" +DEFAULT_MAX_ENTRIES = 20 +DEFAULT_SCAN_INTERVAL = timedelta(hours=1) diff --git a/homeassistant/components/feedreader/coordinator.py b/homeassistant/components/feedreader/coordinator.py index 5bfbc984ccc..e116d804b3d 100644 --- a/homeassistant/components/feedreader/coordinator.py +++ b/homeassistant/components/feedreader/coordinator.py @@ -3,18 +3,19 @@ from __future__ import annotations from calendar import timegm -from datetime import datetime, timedelta +from datetime import datetime from logging import getLogger from time import gmtime, struct_time +from urllib.error import URLError import feedparser from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.storage import Store -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.util import dt as dt_util -from .const import DOMAIN +from .const import DEFAULT_SCAN_INTERVAL, DOMAIN DELAY_SAVE = 30 EVENT_FEEDREADER = "feedreader" @@ -31,7 +32,6 @@ class FeedReaderCoordinator(DataUpdateCoordinator[None]): self, hass: HomeAssistant, url: str, - scan_interval: timedelta, max_entries: int, storage: StoredData, ) -> None: @@ -40,7 +40,7 @@ class FeedReaderCoordinator(DataUpdateCoordinator[None]): hass=hass, logger=_LOGGER, name=f"{DOMAIN} {url}", - update_interval=scan_interval, + update_interval=DEFAULT_SCAN_INTERVAL, ) self._url = url self._max_entries = max_entries @@ -69,8 +69,8 @@ class FeedReaderCoordinator(DataUpdateCoordinator[None]): self._feed = await self.hass.async_add_executor_job(self._fetch_feed) if not self._feed: - _LOGGER.error("Error fetching feed data from %s", self._url) - return None + raise UpdateFailed(f"Error fetching feed data from {self._url}") + # The 'bozo' flag really only indicates that there was an issue # during the initial parsing of the XML, but it doesn't indicate # whether this is an unrecoverable error. In this case the @@ -78,6 +78,12 @@ class FeedReaderCoordinator(DataUpdateCoordinator[None]): # If an error is detected here, log warning message but continue # processing the feed entries if present. if self._feed.bozo != 0: + if isinstance(self._feed.bozo_exception, URLError): + raise UpdateFailed( + f"Error fetching feed data from {self._url}: {self._feed.bozo_exception}" + ) + + # no connection issue, but parsing issue _LOGGER.warning( "Possible issue parsing feed %s: %s", self._url, @@ -169,16 +175,17 @@ class StoredData: self._data: dict[str, struct_time] = {} self.hass = hass self._store: Store[dict[str, str]] = Store(hass, STORAGE_VERSION, DOMAIN) + self.is_initialized = False async def async_setup(self) -> None: """Set up storage.""" - if (store_data := await self._store.async_load()) is None: - return - # Make sure that dst is set to 0, by using gmtime() on the timestamp. - self._data = { - feed_id: gmtime(datetime.fromisoformat(timestamp_string).timestamp()) - for feed_id, timestamp_string in store_data.items() - } + if (store_data := await self._store.async_load()) is not None: + # Make sure that dst is set to 0, by using gmtime() on the timestamp. + self._data = { + feed_id: gmtime(datetime.fromisoformat(timestamp_string).timestamp()) + for feed_id, timestamp_string in store_data.items() + } + self.is_initialized = True def get_timestamp(self, feed_id: str) -> struct_time | None: """Return stored timestamp for given feed id.""" diff --git a/homeassistant/components/feedreader/manifest.json b/homeassistant/components/feedreader/manifest.json index fe52dc4d4c2..5103e1e807c 100644 --- a/homeassistant/components/feedreader/manifest.json +++ b/homeassistant/components/feedreader/manifest.json @@ -2,6 +2,7 @@ "domain": "feedreader", "name": "Feedreader", "codeowners": [], + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/feedreader", "iot_class": "cloud_polling", "loggers": ["feedparser", "sgmllib3k"], diff --git a/homeassistant/components/feedreader/strings.json b/homeassistant/components/feedreader/strings.json new file mode 100644 index 00000000000..31881b4112a --- /dev/null +++ b/homeassistant/components/feedreader/strings.json @@ -0,0 +1,47 @@ +{ + "config": { + "step": { + "user": { + "data": { + "url": "[%key:common::config_flow::data::url%]" + } + }, + "reconfigure_confirm": { + "description": "Update your configuration information for {name}.", + "data": { + "url": "[%key:common::config_flow::data::url%]" + } + } + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_service%]", + "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]" + }, + "error": { + "url_error": "The URL could not be opened.", + "no_feed_entries": "The URL seems not to serve any feed entries." + } + }, + "options": { + "step": { + "init": { + "data": { + "max_entries": "Maximum feed entries" + }, + "data_description": { + "max_entries": "The maximum number of entries to extract from each feed." + } + } + } + }, + "issues": { + "import_yaml_error_url_error": { + "title": "The Feedreader YAML configuration import failed", + "description": "Configuring the Feedreader using YAML is being removed but there was a connection error when trying to import the YAML configuration for `{url}`.\n\nPlease verify that url is reachable and accessable for Home Assistant and restart Home Assistant to try again or remove the Feedreader YAML configuration from your configuration.yaml file and continue to set up the integration manually." + }, + "import_yaml_error_no_feed_entries": { + "title": "[%key:component::feedreader::issues::import_yaml_error_url_error::title%]", + "description": "Configuring the Feedreader using YAML is being removed but when trying to import the YAML configuration for `{url}` no feed entries were found.\n\nPlease verify that url serves any feed entries and restart Home Assistant to try again or remove the Feedreader YAML configuration from your configuration.yaml file and continue to set up the integration manually." + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index f33e37c1a7b..a9f9993c90e 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -166,6 +166,7 @@ FLOWS = { "ezviz", "faa_delays", "fastdotcom", + "feedreader", "fibaro", "file", "filesize", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index fbb2e8ed8aa..542d0563189 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -1792,7 +1792,7 @@ "feedreader": { "name": "Feedreader", "integration_type": "hub", - "config_flow": false, + "config_flow": true, "iot_class": "cloud_polling" }, "ffmpeg": { diff --git a/tests/components/feedreader/__init__.py b/tests/components/feedreader/__init__.py index 3667f7c75ea..cb017ed944d 100644 --- a/tests/components/feedreader/__init__.py +++ b/tests/components/feedreader/__init__.py @@ -1 +1,48 @@ """Tests for the feedreader component.""" + +from typing import Any +from unittest.mock import patch + +from homeassistant.components.feedreader.const import CONF_MAX_ENTRIES, DOMAIN +from homeassistant.const import CONF_URL +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry, load_fixture + + +def load_fixture_bytes(src: str) -> bytes: + """Return byte stream of fixture.""" + feed_data = load_fixture(src, DOMAIN) + return bytes(feed_data, "utf-8") + + +def create_mock_entry( + data: dict[str, Any], +) -> MockConfigEntry: + """Create config entry mock from data.""" + return MockConfigEntry( + domain=DOMAIN, + data={CONF_URL: data[CONF_URL]}, + options={CONF_MAX_ENTRIES: data[CONF_MAX_ENTRIES]}, + ) + + +async def async_setup_config_entry( + hass: HomeAssistant, + data: dict[str, Any], + return_value: bytes | None = None, + side_effect: bytes | None = None, +) -> bool: + """Do setup of a MockConfigEntry.""" + entry = create_mock_entry(data) + entry.add_to_hass(hass) + with patch( + "homeassistant.components.feedreader.coordinator.feedparser.http.get", + ) as feedparser: + if return_value: + feedparser.return_value = return_value + if side_effect: + feedparser.side_effect = side_effect + result = await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + return result diff --git a/tests/components/feedreader/conftest.py b/tests/components/feedreader/conftest.py new file mode 100644 index 00000000000..0a5342615a9 --- /dev/null +++ b/tests/components/feedreader/conftest.py @@ -0,0 +1,58 @@ +"""Fixtures for the tests for the feedreader component.""" + +import pytest + +from homeassistant.components.feedreader.coordinator import EVENT_FEEDREADER +from homeassistant.core import Event, HomeAssistant + +from . import load_fixture_bytes + +from tests.common import async_capture_events + + +@pytest.fixture(name="feed_one_event") +def fixture_feed_one_event(hass: HomeAssistant) -> bytes: + """Load test feed data for one event.""" + return load_fixture_bytes("feedreader.xml") + + +@pytest.fixture(name="feed_two_event") +def fixture_feed_two_events(hass: HomeAssistant) -> bytes: + """Load test feed data for two event.""" + return load_fixture_bytes("feedreader1.xml") + + +@pytest.fixture(name="feed_21_events") +def fixture_feed_21_events(hass: HomeAssistant) -> bytes: + """Load test feed data for twenty one events.""" + return load_fixture_bytes("feedreader2.xml") + + +@pytest.fixture(name="feed_three_events") +def fixture_feed_three_events(hass: HomeAssistant) -> bytes: + """Load test feed data for three events.""" + return load_fixture_bytes("feedreader3.xml") + + +@pytest.fixture(name="feed_four_events") +def fixture_feed_four_events(hass: HomeAssistant) -> bytes: + """Load test feed data for three events.""" + return load_fixture_bytes("feedreader4.xml") + + +@pytest.fixture(name="feed_atom_event") +def fixture_feed_atom_event(hass: HomeAssistant) -> bytes: + """Load test feed data for atom event.""" + return load_fixture_bytes("feedreader5.xml") + + +@pytest.fixture(name="feed_identically_timed_events") +def fixture_feed_identically_timed_events(hass: HomeAssistant) -> bytes: + """Load test feed data for two events published at the exact same time.""" + return load_fixture_bytes("feedreader6.xml") + + +@pytest.fixture(name="events") +async def fixture_events(hass: HomeAssistant) -> list[Event]: + """Fixture that catches alexa events.""" + return async_capture_events(hass, EVENT_FEEDREADER) diff --git a/tests/components/feedreader/const.py b/tests/components/feedreader/const.py new file mode 100644 index 00000000000..bbd0f82bcfa --- /dev/null +++ b/tests/components/feedreader/const.py @@ -0,0 +1,14 @@ +"""Constants for the tests for the feedreader component.""" + +from homeassistant.components.feedreader.const import ( + CONF_MAX_ENTRIES, + DEFAULT_MAX_ENTRIES, +) +from homeassistant.const import CONF_URL + +URL = "http://some.rss.local/rss_feed.xml" +FEED_TITLE = "RSS Sample" +VALID_CONFIG_DEFAULT = {CONF_URL: URL, CONF_MAX_ENTRIES: DEFAULT_MAX_ENTRIES} +VALID_CONFIG_100 = {CONF_URL: URL, CONF_MAX_ENTRIES: 100} +VALID_CONFIG_5 = {CONF_URL: URL, CONF_MAX_ENTRIES: 5} +VALID_CONFIG_1 = {CONF_URL: URL, CONF_MAX_ENTRIES: 1} diff --git a/tests/components/feedreader/test_config_flow.py b/tests/components/feedreader/test_config_flow.py new file mode 100644 index 00000000000..48c341492e0 --- /dev/null +++ b/tests/components/feedreader/test_config_flow.py @@ -0,0 +1,298 @@ +"""The tests for the feedreader config flow.""" + +from unittest.mock import Mock, patch +import urllib + +import pytest + +from homeassistant.components.feedreader import CONF_URLS +from homeassistant.components.feedreader.const import ( + CONF_MAX_ENTRIES, + DEFAULT_MAX_ENTRIES, + DOMAIN, +) +from homeassistant.config_entries import SOURCE_RECONFIGURE, SOURCE_USER +from homeassistant.const import CONF_URL +from homeassistant.core import DOMAIN as HA_DOMAIN, HomeAssistant +from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers import issue_registry as ir +from homeassistant.setup import async_setup_component + +from . import create_mock_entry +from .const import FEED_TITLE, URL, VALID_CONFIG_DEFAULT + + +@pytest.fixture(name="feedparser") +def feedparser_fixture(feed_one_event: bytes) -> Mock: + """Patch libraries.""" + with ( + patch( + "homeassistant.components.feedreader.config_flow.feedparser.http.get", + return_value=feed_one_event, + ) as feedparser, + ): + yield feedparser + + +@pytest.fixture(name="setup_entry") +def setup_entry_fixture(feed_one_event: bytes) -> Mock: + """Patch libraries.""" + with ( + patch("homeassistant.components.feedreader.async_setup_entry") as setup_entry, + ): + yield setup_entry + + +async def test_user(hass: HomeAssistant, feedparser, setup_entry) -> None: + """Test starting a flow by user.""" + # init user flow + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + + # success + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_URL: URL} + ) + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == FEED_TITLE + assert result["data"][CONF_URL] == URL + assert result["options"][CONF_MAX_ENTRIES] == DEFAULT_MAX_ENTRIES + + +async def test_user_errors( + hass: HomeAssistant, feedparser, setup_entry, feed_one_event +) -> None: + """Test starting a flow by user which results in an URL error.""" + # init user flow + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + + # raise URLError + feedparser.side_effect = urllib.error.URLError("Test") + feedparser.return_value = None + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_URL: URL} + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {"base": "url_error"} + + # no feed entries returned + feedparser.side_effect = None + feedparser.return_value = None + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_URL: URL} + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {"base": "no_feed_entries"} + + # success + feedparser.side_effect = None + feedparser.return_value = feed_one_event + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_URL: URL} + ) + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == FEED_TITLE + assert result["data"][CONF_URL] == URL + assert result["options"][CONF_MAX_ENTRIES] == DEFAULT_MAX_ENTRIES + + +@pytest.mark.parametrize( + ("data", "expected_data", "expected_options"), + [ + ({CONF_URLS: [URL]}, {CONF_URL: URL}, {CONF_MAX_ENTRIES: DEFAULT_MAX_ENTRIES}), + ( + {CONF_URLS: [URL], CONF_MAX_ENTRIES: 5}, + {CONF_URL: URL}, + {CONF_MAX_ENTRIES: 5}, + ), + ], +) +async def test_import( + hass: HomeAssistant, + issue_registry: ir.IssueRegistry, + data, + expected_data, + expected_options, + feedparser, + setup_entry, +) -> None: + """Test starting an import flow.""" + config_entries = hass.config_entries.async_entries(DOMAIN) + assert not config_entries + + assert await async_setup_component(hass, DOMAIN, {DOMAIN: data}) + + config_entries = hass.config_entries.async_entries(DOMAIN) + assert config_entries + assert len(config_entries) == 1 + assert config_entries[0].title == FEED_TITLE + assert config_entries[0].data == expected_data + assert config_entries[0].options == expected_options + + assert issue_registry.async_get_issue(HA_DOMAIN, "deprecated_yaml_feedreader") + + +@pytest.mark.parametrize( + ("side_effect", "return_value", "expected_issue_id"), + [ + ( + urllib.error.URLError("Test"), + None, + "import_yaml_error_feedreader_url_error_http_some_rss_local_rss_feed_xml", + ), + ( + None, + None, + "import_yaml_error_feedreader_no_feed_entries_http_some_rss_local_rss_feed_xml", + ), + ], +) +async def test_import_errors( + hass: HomeAssistant, + issue_registry: ir.IssueRegistry, + feedparser, + setup_entry, + feed_one_event, + side_effect, + return_value, + expected_issue_id, +) -> None: + """Test starting an import flow which results in an URL error.""" + config_entries = hass.config_entries.async_entries(DOMAIN) + assert not config_entries + + # raise URLError + feedparser.side_effect = side_effect + feedparser.return_value = return_value + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_URLS: [URL]}}) + assert issue_registry.async_get_issue(DOMAIN, expected_issue_id) + + +async def test_reconfigure(hass: HomeAssistant, feedparser) -> None: + """Test starting a reconfigure flow.""" + entry = create_mock_entry(VALID_CONFIG_DEFAULT) + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + # init user flow + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": SOURCE_RECONFIGURE, + "entry_id": entry.entry_id, + }, + data=entry.data, + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reconfigure_confirm" + + # success + with patch( + "homeassistant.config_entries.ConfigEntries.async_reload" + ) as mock_async_reload: + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_URL: "http://other.rss.local/rss_feed.xml", + }, + ) + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reconfigure_successful" + assert entry.data == { + CONF_URL: "http://other.rss.local/rss_feed.xml", + } + + await hass.async_block_till_done() + assert mock_async_reload.call_count == 1 + + +async def test_reconfigure_errors( + hass: HomeAssistant, feedparser, setup_entry, feed_one_event +) -> None: + """Test starting a reconfigure flow by user which results in an URL error.""" + entry = create_mock_entry(VALID_CONFIG_DEFAULT) + entry.add_to_hass(hass) + + # init user flow + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": SOURCE_RECONFIGURE, + "entry_id": entry.entry_id, + }, + data=entry.data, + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reconfigure_confirm" + + # raise URLError + feedparser.side_effect = urllib.error.URLError("Test") + feedparser.return_value = None + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_URL: "http://other.rss.local/rss_feed.xml", + }, + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reconfigure_confirm" + assert result["errors"] == {"base": "url_error"} + + # no feed entries returned + feedparser.side_effect = None + feedparser.return_value = None + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_URL: "http://other.rss.local/rss_feed.xml", + }, + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reconfigure_confirm" + assert result["errors"] == {"base": "no_feed_entries"} + + # success + feedparser.side_effect = None + feedparser.return_value = feed_one_event + + # success + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_URL: "http://other.rss.local/rss_feed.xml", + }, + ) + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reconfigure_successful" + assert entry.data == { + CONF_URL: "http://other.rss.local/rss_feed.xml", + } + + +async def test_options_flow(hass: HomeAssistant) -> None: + """Test options flow.""" + entry = create_mock_entry(VALID_CONFIG_DEFAULT) + entry.add_to_hass(hass) + + result = await hass.config_entries.options.async_init(entry.entry_id) + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_MAX_ENTRIES: 10, + }, + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["data"] == { + CONF_MAX_ENTRIES: 10, + } diff --git a/tests/components/feedreader/test_init.py b/tests/components/feedreader/test_init.py index d10a17231f9..1dcbf5ba45d 100644 --- a/tests/components/feedreader/test_init.py +++ b/tests/components/feedreader/test_init.py @@ -4,90 +4,38 @@ from datetime import datetime, timedelta from time import gmtime from typing import Any from unittest.mock import patch +import urllib +import urllib.error +from freezegun.api import FrozenDateTimeFactory import pytest -from homeassistant.components.feedreader import CONF_MAX_ENTRIES, CONF_URLS from homeassistant.components.feedreader.const import DOMAIN -from homeassistant.components.feedreader.coordinator import EVENT_FEEDREADER -from homeassistant.const import CONF_SCAN_INTERVAL, EVENT_HOMEASSISTANT_START from homeassistant.core import Event, HomeAssistant -from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.common import async_capture_events, async_fire_time_changed, load_fixture +from . import async_setup_config_entry, create_mock_entry +from .const import ( + URL, + VALID_CONFIG_1, + VALID_CONFIG_5, + VALID_CONFIG_100, + VALID_CONFIG_DEFAULT, +) -URL = "http://some.rss.local/rss_feed.xml" -VALID_CONFIG_1 = {DOMAIN: {CONF_URLS: [URL]}} -VALID_CONFIG_2 = {DOMAIN: {CONF_URLS: [URL], CONF_SCAN_INTERVAL: 60}} -VALID_CONFIG_3 = {DOMAIN: {CONF_URLS: [URL], CONF_MAX_ENTRIES: 100}} -VALID_CONFIG_4 = {DOMAIN: {CONF_URLS: [URL], CONF_MAX_ENTRIES: 5}} -VALID_CONFIG_5 = {DOMAIN: {CONF_URLS: [URL], CONF_MAX_ENTRIES: 1}} +from tests.common import async_fire_time_changed -def load_fixture_bytes(src: str) -> bytes: - """Return byte stream of fixture.""" - feed_data = load_fixture(src, DOMAIN) - return bytes(feed_data, "utf-8") - - -@pytest.fixture(name="feed_one_event") -def fixture_feed_one_event(hass: HomeAssistant) -> bytes: - """Load test feed data for one event.""" - return load_fixture_bytes("feedreader.xml") - - -@pytest.fixture(name="feed_two_event") -def fixture_feed_two_events(hass: HomeAssistant) -> bytes: - """Load test feed data for two event.""" - return load_fixture_bytes("feedreader1.xml") - - -@pytest.fixture(name="feed_21_events") -def fixture_feed_21_events(hass: HomeAssistant) -> bytes: - """Load test feed data for twenty one events.""" - return load_fixture_bytes("feedreader2.xml") - - -@pytest.fixture(name="feed_three_events") -def fixture_feed_three_events(hass: HomeAssistant) -> bytes: - """Load test feed data for three events.""" - return load_fixture_bytes("feedreader3.xml") - - -@pytest.fixture(name="feed_atom_event") -def fixture_feed_atom_event(hass: HomeAssistant) -> bytes: - """Load test feed data for atom event.""" - return load_fixture_bytes("feedreader5.xml") - - -@pytest.fixture(name="feed_identically_timed_events") -def fixture_feed_identically_timed_events(hass: HomeAssistant) -> bytes: - """Load test feed data for two events published at the exact same time.""" - return load_fixture_bytes("feedreader6.xml") - - -@pytest.fixture(name="events") -async def fixture_events(hass: HomeAssistant) -> list[Event]: - """Fixture that catches alexa events.""" - return async_capture_events(hass, EVENT_FEEDREADER) - - -async def test_setup_one_feed(hass: HomeAssistant) -> None: - """Test the general setup of this component.""" - assert await async_setup_component(hass, DOMAIN, VALID_CONFIG_1) - - -async def test_setup_no_feeds(hass: HomeAssistant) -> None: - """Test config with no urls.""" - assert not await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_URLS: []}}) - - -async def test_storage_data_loading( +@pytest.mark.parametrize( + "config", + [VALID_CONFIG_DEFAULT, VALID_CONFIG_1, VALID_CONFIG_100, VALID_CONFIG_5], +) +async def test_setup( hass: HomeAssistant, events: list[Event], feed_one_event: bytes, hass_storage: dict[str, Any], + config: dict[str, Any], ) -> None: """Test loading existing storage data.""" storage_data: dict[str, str] = {URL: "2018-04-30T05:10:00+00:00"} @@ -97,15 +45,7 @@ async def test_storage_data_loading( "key": DOMAIN, "data": storage_data, } - - with patch( - "feedparser.http.get", - return_value=feed_one_event, - ): - assert await async_setup_component(hass, DOMAIN, VALID_CONFIG_2) - - hass.bus.async_fire(EVENT_HOMEASSISTANT_START) - await hass.async_block_till_done() + assert await async_setup_config_entry(hass, config, return_value=feed_one_event) # no new events assert not events @@ -121,16 +61,11 @@ async def test_storage_data_writing( storage_data: dict[str, str] = {URL: "2018-04-30T05:10:00+00:00"} with ( - patch( - "feedparser.http.get", - return_value=feed_one_event, - ), patch("homeassistant.components.feedreader.coordinator.DELAY_SAVE", new=0), ): - assert await async_setup_component(hass, DOMAIN, VALID_CONFIG_2) - - hass.bus.async_fire(EVENT_HOMEASSISTANT_START) - await hass.async_block_till_done() + assert await async_setup_config_entry( + hass, VALID_CONFIG_DEFAULT, return_value=feed_one_event + ) # one new event assert len(events) == 1 @@ -139,22 +74,11 @@ async def test_storage_data_writing( assert hass_storage[DOMAIN]["data"] == storage_data -async def test_setup_max_entries(hass: HomeAssistant) -> None: - """Test the setup of this component with max entries.""" - assert await async_setup_component(hass, DOMAIN, VALID_CONFIG_3) - await hass.async_block_till_done() - - async def test_feed(hass: HomeAssistant, events, feed_one_event) -> None: """Test simple rss feed with valid data.""" - with patch( - "feedparser.http.get", - return_value=feed_one_event, - ): - assert await async_setup_component(hass, DOMAIN, VALID_CONFIG_2) - - hass.bus.async_fire(EVENT_HOMEASSISTANT_START) - await hass.async_block_till_done() + assert await async_setup_config_entry( + hass, VALID_CONFIG_DEFAULT, return_value=feed_one_event + ) assert len(events) == 1 assert events[0].data.title == "Title 1" @@ -170,14 +94,9 @@ async def test_feed(hass: HomeAssistant, events, feed_one_event) -> None: async def test_atom_feed(hass: HomeAssistant, events, feed_atom_event) -> None: """Test simple atom feed with valid data.""" - with patch( - "feedparser.http.get", - return_value=feed_atom_event, - ): - assert await async_setup_component(hass, DOMAIN, VALID_CONFIG_5) - - hass.bus.async_fire(EVENT_HOMEASSISTANT_START) - await hass.async_block_till_done() + assert await async_setup_config_entry( + hass, VALID_CONFIG_DEFAULT, return_value=feed_atom_event + ) assert len(events) == 1 assert events[0].data.title == "Atom-Powered Robots Run Amok" @@ -196,10 +115,6 @@ async def test_feed_identical_timestamps( ) -> None: """Test feed with 2 entries with identical timestamps.""" with ( - patch( - "feedparser.http.get", - return_value=feed_identically_timed_events, - ), patch( "homeassistant.components.feedreader.coordinator.StoredData.get_timestamp", return_value=gmtime( @@ -207,10 +122,9 @@ async def test_feed_identical_timestamps( ), ), ): - assert await async_setup_component(hass, DOMAIN, VALID_CONFIG_2) - - hass.bus.async_fire(EVENT_HOMEASSISTANT_START) - await hass.async_block_till_done() + assert await async_setup_config_entry( + hass, VALID_CONFIG_DEFAULT, return_value=feed_identically_timed_events + ) assert len(events) == 2 assert events[0].data.title == "Title 1" @@ -261,11 +175,13 @@ async def test_feed_updates( feed_two_event, ] + entry = create_mock_entry(VALID_CONFIG_DEFAULT) + entry.add_to_hass(hass) with patch( "homeassistant.components.feedreader.coordinator.feedparser.http.get", side_effect=side_effect, ): - assert await async_setup_component(hass, DOMAIN, VALID_CONFIG_2) + assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() assert len(events) == 1 @@ -289,22 +205,20 @@ async def test_feed_default_max_length( hass: HomeAssistant, events, feed_21_events ) -> None: """Test long feed beyond the default 20 entry limit.""" - with patch("feedparser.http.get", return_value=feed_21_events): - assert await async_setup_component(hass, DOMAIN, VALID_CONFIG_2) - - hass.bus.async_fire(EVENT_HOMEASSISTANT_START) - await hass.async_block_till_done() + assert await async_setup_config_entry( + hass, VALID_CONFIG_DEFAULT, return_value=feed_21_events + ) + await hass.async_block_till_done() assert len(events) == 20 async def test_feed_max_length(hass: HomeAssistant, events, feed_21_events) -> None: """Test long feed beyond a configured 5 entry limit.""" - with patch("feedparser.http.get", return_value=feed_21_events): - assert await async_setup_component(hass, DOMAIN, VALID_CONFIG_4) - - hass.bus.async_fire(EVENT_HOMEASSISTANT_START) - await hass.async_block_till_done() + assert await async_setup_config_entry( + hass, VALID_CONFIG_5, return_value=feed_21_events + ) + await hass.async_block_till_done() assert len(events) == 5 @@ -313,53 +227,104 @@ async def test_feed_without_publication_date_and_title( hass: HomeAssistant, events, feed_three_events ) -> None: """Test simple feed with entry without publication date and title.""" - with patch("feedparser.http.get", return_value=feed_three_events): - assert await async_setup_component(hass, DOMAIN, VALID_CONFIG_2) - - hass.bus.async_fire(EVENT_HOMEASSISTANT_START) - await hass.async_block_till_done() + assert await async_setup_config_entry( + hass, VALID_CONFIG_DEFAULT, return_value=feed_three_events + ) + await hass.async_block_till_done() assert len(events) == 3 async def test_feed_with_unrecognized_publication_date( - hass: HomeAssistant, events + hass: HomeAssistant, events, feed_four_events ) -> None: """Test simple feed with entry with unrecognized publication date.""" - with patch( - "feedparser.http.get", return_value=load_fixture_bytes("feedreader4.xml") - ): - assert await async_setup_component(hass, DOMAIN, VALID_CONFIG_2) - - hass.bus.async_fire(EVENT_HOMEASSISTANT_START) - await hass.async_block_till_done() + assert await async_setup_config_entry( + hass, VALID_CONFIG_DEFAULT, return_value=feed_four_events + ) + await hass.async_block_till_done() assert len(events) == 1 async def test_feed_invalid_data(hass: HomeAssistant, events) -> None: """Test feed with invalid data.""" - invalid_data = bytes("INVALID DATA", "utf-8") - with patch("feedparser.http.get", return_value=invalid_data): - assert await async_setup_component(hass, DOMAIN, VALID_CONFIG_2) - - hass.bus.async_fire(EVENT_HOMEASSISTANT_START) - await hass.async_block_till_done() + assert await async_setup_config_entry( + hass, VALID_CONFIG_DEFAULT, return_value=bytes("INVALID DATA", "utf-8") + ) + await hass.async_block_till_done() assert len(events) == 0 async def test_feed_parsing_failed( - hass: HomeAssistant, events, caplog: pytest.LogCaptureFixture + hass: HomeAssistant, events, feed_one_event, caplog: pytest.LogCaptureFixture ) -> None: """Test feed where parsing fails.""" assert "Error fetching feed data" not in caplog.text with patch("feedparser.parse", return_value=None): - assert await async_setup_component(hass, DOMAIN, VALID_CONFIG_2) - - hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + assert not await async_setup_config_entry( + hass, VALID_CONFIG_DEFAULT, return_value=feed_one_event + ) await hass.async_block_till_done() assert "Error fetching feed data" in caplog.text assert not events + + +async def test_feed_errors( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + caplog: pytest.LogCaptureFixture, + feed_one_event, +) -> None: + """Test feed errors.""" + entry = create_mock_entry(VALID_CONFIG_DEFAULT) + entry.add_to_hass(hass) + with patch( + "homeassistant.components.feedreader.coordinator.feedparser.http.get" + ) as feedreader: + # success setup + feedreader.return_value = feed_one_event + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + # raise URL error + feedreader.side_effect = urllib.error.URLError("Test") + freezer.tick(timedelta(hours=1, seconds=1)) + async_fire_time_changed(hass) + await hass.async_block_till_done(wait_background_tasks=True) + assert ( + "Error fetching feed data from http://some.rss.local/rss_feed.xml: " + in caplog.text + ) + + # success + feedreader.side_effect = None + feedreader.return_value = feed_one_event + freezer.tick(timedelta(hours=1, seconds=1)) + async_fire_time_changed(hass) + await hass.async_block_till_done(wait_background_tasks=True) + caplog.clear() + + # no feed returned + freezer.tick(timedelta(hours=1, seconds=1)) + with patch( + "homeassistant.components.feedreader.coordinator.feedparser.parse", + return_value=None, + ): + async_fire_time_changed(hass) + await hass.async_block_till_done(wait_background_tasks=True) + assert ( + "Error fetching feed data from http://some.rss.local/rss_feed.xml" + in caplog.text + ) + caplog.clear() + + # success + feedreader.side_effect = None + feedreader.return_value = feed_one_event + freezer.tick(timedelta(hours=1, seconds=1)) + async_fire_time_changed(hass) + await hass.async_block_till_done(wait_background_tasks=True) From 8b4a5042bba2117e2b758013ab70a87fe6d7f802 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Fri, 21 Jun 2024 20:27:30 +0200 Subject: [PATCH 0961/1445] Use UID instead of MAC or channel for unique_ID in Reolink (#119744) --- homeassistant/components/reolink/__init__.py | 80 ++++++++++-- .../components/reolink/config_flow.py | 3 +- homeassistant/components/reolink/entity.py | 16 ++- homeassistant/components/reolink/host.py | 5 +- .../components/reolink/media_source.py | 10 +- homeassistant/components/reolink/switch.py | 2 - tests/components/reolink/conftest.py | 4 +- tests/components/reolink/test_init.py | 121 +++++++++++++++++- 8 files changed, 213 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/reolink/__init__.py b/homeassistant/components/reolink/__init__.py index 1d933a84ebd..27bd504e9bb 100644 --- a/homeassistant/components/reolink/__init__.py +++ b/homeassistant/components/reolink/__init__.py @@ -15,6 +15,7 @@ from homeassistant.const import EVENT_HOMEASSISTANT_STOP, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers.device_registry import format_mac from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import DOMAIN @@ -141,8 +142,9 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b firmware_coordinator=firmware_coordinator, ) - cleanup_disconnected_cams(hass, config_entry.entry_id, host) + # first migrate and then cleanup, otherwise entities lost migrate_entity_ids(hass, config_entry.entry_id, host) + cleanup_disconnected_cams(hass, config_entry.entry_id, host) await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) @@ -173,6 +175,24 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> return unload_ok +def get_device_uid_and_ch( + device: dr.DeviceEntry, host: ReolinkHost +) -> tuple[list[str], int | None]: + """Get the channel and the split device_uid from a reolink DeviceEntry.""" + device_uid = [ + dev_id[1].split("_") for dev_id in device.identifiers if dev_id[0] == DOMAIN + ][0] + + if len(device_uid) < 2: + # NVR itself + ch = None + elif device_uid[1].startswith("ch") and len(device_uid[1]) <= 5: + ch = int(device_uid[1][2:]) + else: + ch = host.api.channel_for_uid(device_uid[1]) + return (device_uid, ch) + + def cleanup_disconnected_cams( hass: HomeAssistant, config_entry_id: str, host: ReolinkHost ) -> None: @@ -183,17 +203,10 @@ def cleanup_disconnected_cams( device_reg = dr.async_get(hass) devices = dr.async_entries_for_config_entry(device_reg, config_entry_id) for device in devices: - device_id = [ - dev_id[1].split("_ch") - for dev_id in device.identifiers - if dev_id[0] == DOMAIN - ][0] + (device_uid, ch) = get_device_uid_and_ch(device, host) + if ch is None: + continue # Do not consider the NVR itself - if len(device_id) < 2: - # Do not consider the NVR itself - continue - - ch = int(device_id[1]) ch_model = host.api.camera_model(ch) remove = False if ch not in host.api.channels: @@ -225,11 +238,54 @@ def migrate_entity_ids( hass: HomeAssistant, config_entry_id: str, host: ReolinkHost ) -> None: """Migrate entity IDs if needed.""" + device_reg = dr.async_get(hass) + devices = dr.async_entries_for_config_entry(device_reg, config_entry_id) + ch_device_ids = {} + for device in devices: + (device_uid, ch) = get_device_uid_and_ch(device, host) + + if host.api.supported(None, "UID") and device_uid[0] != host.unique_id: + if ch is None: + new_device_id = f"{host.unique_id}" + else: + new_device_id = f"{host.unique_id}_{device_uid[1]}" + new_identifiers = {(DOMAIN, new_device_id)} + device_reg.async_update_device(device.id, new_identifiers=new_identifiers) + + if ch is None: + continue # Do not consider the NVR itself + + ch_device_ids[device.id] = ch + if host.api.supported(ch, "UID") and device_uid[1] != host.api.camera_uid(ch): + if host.api.supported(None, "UID"): + new_device_id = f"{host.unique_id}_{host.api.camera_uid(ch)}" + else: + new_device_id = f"{device_uid[0]}_{host.api.camera_uid(ch)}" + new_identifiers = {(DOMAIN, new_device_id)} + device_reg.async_update_device(device.id, new_identifiers=new_identifiers) + entity_reg = er.async_get(hass) entities = er.async_entries_for_config_entry(entity_reg, config_entry_id) for entity in entities: # Can be removed in HA 2025.1.0 - if entity.domain == "update" and entity.unique_id == host.unique_id: + if entity.domain == "update" and entity.unique_id in [ + host.unique_id, + format_mac(host.api.mac_address), + ]: entity_reg.async_update_entity( entity.entity_id, new_unique_id=f"{host.unique_id}_firmware" ) + continue + + if host.api.supported(None, "UID") and not entity.unique_id.startswith( + host.unique_id + ): + new_id = f"{host.unique_id}_{entity.unique_id.split("_", 1)[1]}" + entity_reg.async_update_entity(entity.entity_id, new_unique_id=new_id) + + if entity.device_id in ch_device_ids: + ch = ch_device_ids[entity.device_id] + id_parts = entity.unique_id.split("_", 2) + if host.api.supported(ch, "UID") and id_parts[1] != host.api.camera_uid(ch): + new_id = f"{host.unique_id}_{host.api.camera_uid(ch)}_{id_parts[2]}" + entity_reg.async_update_entity(entity.entity_id, new_unique_id=new_id) diff --git a/homeassistant/components/reolink/config_flow.py b/homeassistant/components/reolink/config_flow.py index 29da4a55ea1..d8caff9f120 100644 --- a/homeassistant/components/reolink/config_flow.py +++ b/homeassistant/components/reolink/config_flow.py @@ -228,8 +228,9 @@ class ReolinkFlowHandler(ConfigFlow, domain=DOMAIN): user_input[CONF_PORT] = host.api.port user_input[CONF_USE_HTTPS] = host.api.use_https + mac_address = format_mac(host.api.mac_address) existing_entry = await self.async_set_unique_id( - host.unique_id, raise_on_progress=False + mac_address, raise_on_progress=False ) if existing_entry and self._reauth: if self.hass.config_entries.async_update_entry( diff --git a/homeassistant/components/reolink/entity.py b/homeassistant/components/reolink/entity.py index 89c98ad0885..cf582c69e2d 100644 --- a/homeassistant/components/reolink/entity.py +++ b/homeassistant/components/reolink/entity.py @@ -112,17 +112,25 @@ class ReolinkChannelCoordinatorEntity(ReolinkHostCoordinatorEntity): super().__init__(reolink_data, coordinator) self._channel = channel - self._attr_unique_id = ( - f"{self._host.unique_id}_{channel}_{self.entity_description.key}" - ) + if self._host.api.supported(channel, "UID"): + self._attr_unique_id = f"{self._host.unique_id}_{self._host.api.camera_uid(channel)}_{self.entity_description.key}" + else: + self._attr_unique_id = ( + f"{self._host.unique_id}_{channel}_{self.entity_description.key}" + ) dev_ch = channel if self._host.api.model in DUAL_LENS_MODELS: dev_ch = 0 if self._host.api.is_nvr: + if self._host.api.supported(dev_ch, "UID"): + dev_id = f"{self._host.unique_id}_{self._host.api.camera_uid(dev_ch)}" + else: + dev_id = f"{self._host.unique_id}_ch{dev_ch}" + self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, f"{self._host.unique_id}_ch{dev_ch}")}, + identifiers={(DOMAIN, dev_id)}, via_device=(DOMAIN, self._host.unique_id), name=self._host.api.camera_name(dev_ch), model=self._host.api.camera_model(dev_ch), diff --git a/homeassistant/components/reolink/host.py b/homeassistant/components/reolink/host.py index 9836c5d7a01..c69a80ce972 100644 --- a/homeassistant/components/reolink/host.py +++ b/homeassistant/components/reolink/host.py @@ -191,7 +191,10 @@ class ReolinkHost: else: ir.async_delete_issue(self._hass, DOMAIN, "enable_port") - self._unique_id = format_mac(self._api.mac_address) + if self._api.supported(None, "UID"): + self._unique_id = self._api.uid + else: + self._unique_id = format_mac(self._api.mac_address) if self._onvif_push_supported: try: diff --git a/homeassistant/components/reolink/media_source.py b/homeassistant/components/reolink/media_source.py index 5d3c16b00fd..7a77e482f56 100644 --- a/homeassistant/components/reolink/media_source.py +++ b/homeassistant/components/reolink/media_source.py @@ -164,10 +164,14 @@ class ReolinkVODMediaSource(MediaSource): continue device = device_reg.async_get(entity.device_id) - ch = entity.unique_id.split("_")[1] - if ch in channels or device is None: + ch_id = entity.unique_id.split("_")[1] + if ch_id in channels or device is None: continue - channels.append(ch) + channels.append(ch_id) + + ch: int | str = ch_id + if len(ch_id) > 3: + ch = host.api.channel_for_uid(ch_id) if ( host.api.api_version("recReplay", int(ch)) < 1 diff --git a/homeassistant/components/reolink/switch.py b/homeassistant/components/reolink/switch.py index f1a8de09509..9dfce88f93a 100644 --- a/homeassistant/components/reolink/switch.py +++ b/homeassistant/components/reolink/switch.py @@ -330,8 +330,6 @@ class ReolinkNVRSwitchEntity(ReolinkHostCoordinatorEntity, SwitchEntity): self.entity_description = entity_description super().__init__(reolink_data) - self._attr_unique_id = f"{self._host.unique_id}_{entity_description.key}" - @property def is_on(self) -> bool: """Return true if switch is on.""" diff --git a/tests/components/reolink/conftest.py b/tests/components/reolink/conftest.py index 4fed102b320..3541aa1f856 100644 --- a/tests/components/reolink/conftest.py +++ b/tests/components/reolink/conftest.py @@ -29,6 +29,7 @@ TEST_MAC = "aa:bb:cc:dd:ee:ff" TEST_MAC2 = "ff:ee:dd:cc:bb:aa" DHCP_FORMATTED_MAC = "aabbccddeeff" TEST_UID = "ABC1234567D89EFG" +TEST_UID_CAM = "DEF7654321D89GHT" TEST_PORT = 1234 TEST_NVR_NAME = "test_reolink_name" TEST_NVR_NAME2 = "test2_reolink_name" @@ -86,7 +87,8 @@ def reolink_connect_class() -> Generator[MagicMock]: host_mock.camera_name.return_value = TEST_NVR_NAME host_mock.camera_hardware_version.return_value = "IPC_00001" host_mock.camera_sw_version.return_value = "v1.1.0.0.0.0000" - host_mock.camera_uid.return_value = TEST_UID + host_mock.camera_uid.return_value = TEST_UID_CAM + host_mock.channel_for_uid.return_value = 0 host_mock.get_encoding.return_value = "h264" host_mock.firmware_update_available.return_value = False host_mock.session_active = True diff --git a/tests/components/reolink/test_init.py b/tests/components/reolink/test_init.py index db6069b097c..466836e52ef 100644 --- a/tests/components/reolink/test_init.py +++ b/tests/components/reolink/test_init.py @@ -20,7 +20,14 @@ from homeassistant.helpers import ( from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow -from .conftest import TEST_CAM_MODEL, TEST_HOST_MODEL, TEST_MAC, TEST_NVR_NAME +from .conftest import ( + TEST_CAM_MODEL, + TEST_HOST_MODEL, + TEST_MAC, + TEST_NVR_NAME, + TEST_UID, + TEST_UID_CAM, +) from tests.common import MockConfigEntry, async_fire_time_changed @@ -178,17 +185,104 @@ async def test_cleanup_disconnected_cams( assert sorted(device_models) == sorted(expected_models) +@pytest.mark.parametrize( + ( + "original_id", + "new_id", + "original_dev_id", + "new_dev_id", + "domain", + "support_uid", + "support_ch_uid", + ), + [ + ( + TEST_MAC, + f"{TEST_MAC}_firmware", + f"{TEST_MAC}", + f"{TEST_MAC}", + Platform.UPDATE, + False, + False, + ), + ( + TEST_MAC, + f"{TEST_UID}_firmware", + f"{TEST_MAC}", + f"{TEST_UID}", + Platform.UPDATE, + True, + False, + ), + ( + f"{TEST_MAC}_0_record_audio", + f"{TEST_UID}_0_record_audio", + f"{TEST_MAC}_ch0", + f"{TEST_UID}_ch0", + Platform.SWITCH, + True, + False, + ), + ( + f"{TEST_MAC}_0_record_audio", + f"{TEST_MAC}_{TEST_UID_CAM}_record_audio", + f"{TEST_MAC}_ch0", + f"{TEST_MAC}_{TEST_UID_CAM}", + Platform.SWITCH, + False, + True, + ), + ( + f"{TEST_MAC}_0_record_audio", + f"{TEST_UID}_{TEST_UID_CAM}_record_audio", + f"{TEST_MAC}_ch0", + f"{TEST_UID}_{TEST_UID_CAM}", + Platform.SWITCH, + True, + True, + ), + ( + f"{TEST_UID}_0_record_audio", + f"{TEST_UID}_{TEST_UID_CAM}_record_audio", + f"{TEST_UID}_ch0", + f"{TEST_UID}_{TEST_UID_CAM}", + Platform.SWITCH, + True, + True, + ), + ], +) async def test_migrate_entity_ids( hass: HomeAssistant, config_entry: MockConfigEntry, reolink_connect: MagicMock, entity_registry: er.EntityRegistry, + device_registry: dr.DeviceRegistry, + original_id: str, + new_id: str, + original_dev_id: str, + new_dev_id: str, + domain: Platform, + support_uid: bool, + support_ch_uid: bool, ) -> None: """Test entity ids that need to be migrated.""" + + def mock_supported(ch, capability): + if capability == "UID" and ch is None: + return support_uid + if capability == "UID": + return support_ch_uid + return True + reolink_connect.channels = [0] - original_id = f"{TEST_MAC}" - new_id = f"{TEST_MAC}_firmware" - domain = Platform.UPDATE + reolink_connect.supported = mock_supported + + dev_entry = device_registry.async_get_or_create( + identifiers={(const.DOMAIN, original_dev_id)}, + config_entry_id=config_entry.entry_id, + disabled_by=None, + ) entity_registry.async_get_or_create( domain=domain, @@ -197,11 +291,21 @@ async def test_migrate_entity_ids( config_entry=config_entry, suggested_object_id=original_id, disabled_by=None, + device_id=dev_entry.id, ) assert entity_registry.async_get_entity_id(domain, const.DOMAIN, original_id) assert entity_registry.async_get_entity_id(domain, const.DOMAIN, new_id) is None + assert device_registry.async_get_device( + identifiers={(const.DOMAIN, original_dev_id)} + ) + if new_dev_id != original_dev_id: + assert ( + device_registry.async_get_device(identifiers={(const.DOMAIN, new_dev_id)}) + is None + ) + # setup CH 0 and host entities/device with patch("homeassistant.components.reolink.PLATFORMS", [domain]): assert await hass.config_entries.async_setup(config_entry.entry_id) @@ -212,6 +316,15 @@ async def test_migrate_entity_ids( ) assert entity_registry.async_get_entity_id(domain, const.DOMAIN, new_id) + if new_dev_id != original_dev_id: + assert ( + device_registry.async_get_device( + identifiers={(const.DOMAIN, original_dev_id)} + ) + is None + ) + assert device_registry.async_get_device(identifiers={(const.DOMAIN, new_dev_id)}) + async def test_no_repair_issue( hass: HomeAssistant, config_entry: MockConfigEntry, issue_registry: ir.IssueRegistry From c3ab72a1f9753070f8258695ee4caa28b21b03da Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 21 Jun 2024 14:48:09 -0500 Subject: [PATCH 0962/1445] Fix comparing end of event in unifiprotect (#120124) --- .../components/unifiprotect/binary_sensor.py | 19 ++-- .../components/unifiprotect/entity.py | 21 ++++- .../components/unifiprotect/sensor.py | 17 ++-- tests/components/unifiprotect/test_sensor.py | 87 +++++++++++++++++++ 4 files changed, 125 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/unifiprotect/binary_sensor.py b/homeassistant/components/unifiprotect/binary_sensor.py index 966354749bc..decb0bf2a18 100644 --- a/homeassistant/components/unifiprotect/binary_sensor.py +++ b/homeassistant/components/unifiprotect/binary_sensor.py @@ -714,7 +714,7 @@ class ProtectEventBinarySensor(EventEntityMixin, BinarySensorEntity): def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None: super()._async_update_device_from_protect(device) description = self.entity_description - event = self._event = self.entity_description.get_event_obj(device) + event = self.entity_description.get_event_obj(device) if is_on := bool(description.get_ufp_value(device)): if event: self._set_event_attrs(event) @@ -737,25 +737,26 @@ class ProtectSmartEventBinarySensor(EventEntityMixin, BinarySensorEntity): @callback def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None: - prev_event = self._event - super()._async_update_device_from_protect(device) description = self.entity_description - self._event = description.get_event_obj(device) + + prev_event = self._event + prev_event_end = self._event_end + super()._async_update_device_from_protect(device) + event = self._event = description.get_event_obj(device) + self._event_end = event.end if event else None if not ( - (event := self._event) - and not self._event_already_ended(prev_event) + event + and not self._event_already_ended(prev_event, prev_event_end) and description.has_matching_smart(event) and ((is_end := event.end) or self.device.is_smart_detected) ): self._set_event_done() return - was_on = self._attr_is_on self._attr_is_on = True self._set_event_attrs(event) - - if is_end and not was_on: + if is_end: self._async_event_with_immediate_end() diff --git a/homeassistant/components/unifiprotect/entity.py b/homeassistant/components/unifiprotect/entity.py index 3777338209b..7eceb861955 100644 --- a/homeassistant/components/unifiprotect/entity.py +++ b/homeassistant/components/unifiprotect/entity.py @@ -3,6 +3,7 @@ from __future__ import annotations from collections.abc import Callable, Sequence +from datetime import datetime from functools import partial import logging from operator import attrgetter @@ -303,6 +304,7 @@ class EventEntityMixin(ProtectDeviceEntity): entity_description: ProtectEventMixin _unrecorded_attributes = frozenset({ATTR_EVENT_ID, ATTR_EVENT_SCORE}) _event: Event | None = None + _event_end: datetime | None = None @callback def _set_event_done(self) -> None: @@ -326,6 +328,21 @@ class EventEntityMixin(ProtectDeviceEntity): self.async_write_ha_state() @callback - def _event_already_ended(self, prev_event: Event | None) -> bool: + def _event_already_ended( + self, prev_event: Event | None, prev_event_end: datetime | None + ) -> bool: + """Determine if the event has already ended. + + The event_end time is passed because the prev_event and event object + may be the same object, and the uiprotect code will mutate the + event object so we need to check the datetime object that was + saved from the last time the entity was updated. + """ event = self._event - return bool(event and event.end and prev_event and prev_event.id == event.id) + return bool( + event + and event.end + and prev_event + and prev_event_end + and prev_event.id == event.id + ) diff --git a/homeassistant/components/unifiprotect/sensor.py b/homeassistant/components/unifiprotect/sensor.py index ccd341088ef..da0742afcd5 100644 --- a/homeassistant/components/unifiprotect/sensor.py +++ b/homeassistant/components/unifiprotect/sensor.py @@ -757,14 +757,17 @@ class ProtectLicensePlateEventSensor(ProtectEventSensor): @callback def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None: - prev_event = self._event - super()._async_update_device_from_protect(device) description = self.entity_description - self._event = description.get_event_obj(device) + + prev_event = self._event + prev_event_end = self._event_end + super()._async_update_device_from_protect(device) + event = self._event = description.get_event_obj(device) + self._event_end = event.end if event else None if not ( - (event := self._event) - and not self._event_already_ended(prev_event) + event + and not self._event_already_ended(prev_event, prev_event_end) and description.has_matching_smart(event) and ((is_end := event.end) or self.device.is_smart_detected) and (metadata := event.metadata) @@ -773,9 +776,7 @@ class ProtectLicensePlateEventSensor(ProtectEventSensor): self._set_event_done() return - previous_plate = self._attr_native_value self._attr_native_value = license_plate.name self._set_event_attrs(event) - - if is_end and previous_plate != license_plate.name: + if is_end: self._async_event_with_immediate_end() diff --git a/tests/components/unifiprotect/test_sensor.py b/tests/components/unifiprotect/test_sensor.py index b3842be4e0a..f1f4b608aea 100644 --- a/tests/components/unifiprotect/test_sensor.py +++ b/tests/components/unifiprotect/test_sensor.py @@ -626,6 +626,93 @@ async def test_camera_update_license_plate( assert state.state == "none" +async def test_camera_update_license_plate_changes_number_during_detect( + hass: HomeAssistant, ufp: MockUFPFixture, camera: Camera, fixed_now: datetime +) -> None: + """Test license plate sensor that changes number during detect.""" + + camera.feature_flags.smart_detect_types.append(SmartDetectObjectType.LICENSE_PLATE) + camera.feature_flags.has_smart_detect = True + camera.smart_detect_settings.object_types.append( + SmartDetectObjectType.LICENSE_PLATE + ) + + await init_entry(hass, ufp, [camera]) + assert_entity_counts(hass, Platform.SENSOR, 23, 13) + + _, entity_id = ids_from_device_description( + Platform.SENSOR, camera, LICENSE_PLATE_EVENT_SENSORS[0] + ) + + event_metadata = EventMetadata( + license_plate=LicensePlateMetadata(name="ABCD1234", confidence_level=95) + ) + event = Event( + model=ModelType.EVENT, + id="test_event_id", + type=EventType.SMART_DETECT, + start=fixed_now - timedelta(seconds=1), + end=None, + score=100, + smart_detect_types=[SmartDetectObjectType.LICENSE_PLATE], + smart_detect_event_ids=[], + metadata=event_metadata, + api=ufp.api, + ) + + new_camera = camera.copy() + new_camera.is_smart_detected = True + new_camera.last_smart_detect_event_ids[SmartDetectObjectType.LICENSE_PLATE] = ( + event.id + ) + + mock_msg = Mock() + mock_msg.changed_data = {} + mock_msg.new_obj = new_camera + + ufp.api.bootstrap.cameras = {new_camera.id: new_camera} + ufp.api.bootstrap.events = {event.id: event} + + state_changes: list[HAEvent[EventStateChangedData]] = async_capture_events( + hass, EVENT_STATE_CHANGED + ) + ufp.ws_msg(mock_msg) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state + assert state.state == "ABCD1234" + + assert len(state_changes) == 1 + + ufp.ws_msg(mock_msg) + await hass.async_block_till_done() + assert len(state_changes) == 1 + + # Now mutate the original event so it ends + # Also change the metadata to a different license plate + # since the model may not get the plate correct on + # the first update. + event.score = 99 + event.end = fixed_now + timedelta(seconds=1) + event_metadata.license_plate.name = "DCBA4321" + ufp.api.bootstrap.events = {event.id: event} + + ufp.ws_msg(mock_msg) + await hass.async_block_till_done() + assert len(state_changes) == 3 + state = hass.states.get(entity_id) + assert state + assert state.state == "none" + + assert state_changes[0].data["new_state"].state == "ABCD1234" + assert state_changes[1].data["new_state"].state == "DCBA4321" + assert state_changes[2].data["new_state"].state == "none" + state = hass.states.get(entity_id) + assert state + assert state.state == "none" + + async def test_sensor_precision( hass: HomeAssistant, ufp: MockUFPFixture, sensor_all: Sensor, fixed_now: datetime ) -> None: From 4d11dd67392a804aa28df9b60e05de8b516a28a4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 21 Jun 2024 15:16:33 -0500 Subject: [PATCH 0963/1445] Add additional license plate test coverage to unifiprotect (#120125) --- tests/components/unifiprotect/test_sensor.py | 128 ++++++++++++++++++- 1 file changed, 127 insertions(+), 1 deletion(-) diff --git a/tests/components/unifiprotect/test_sensor.py b/tests/components/unifiprotect/test_sensor.py index f1f4b608aea..06d87440e94 100644 --- a/tests/components/unifiprotect/test_sensor.py +++ b/tests/components/unifiprotect/test_sensor.py @@ -17,7 +17,10 @@ from uiprotect.data import ( ) from uiprotect.data.nvr import EventMetadata, LicensePlateMetadata -from homeassistant.components.unifiprotect.const import DEFAULT_ATTRIBUTION +from homeassistant.components.unifiprotect.const import ( + ATTR_EVENT_SCORE, + DEFAULT_ATTRIBUTION, +) from homeassistant.components.unifiprotect.sensor import ( ALL_DEVICES_SENSORS, CAMERA_DISABLED_SENSORS, @@ -713,6 +716,129 @@ async def test_camera_update_license_plate_changes_number_during_detect( assert state.state == "none" +async def test_camera_update_license_plate_multiple_updates( + hass: HomeAssistant, ufp: MockUFPFixture, camera: Camera, fixed_now: datetime +) -> None: + """Test license plate sensor that updates multiple times.""" + + camera.feature_flags.smart_detect_types.append(SmartDetectObjectType.LICENSE_PLATE) + camera.feature_flags.has_smart_detect = True + camera.smart_detect_settings.object_types.append( + SmartDetectObjectType.LICENSE_PLATE + ) + + await init_entry(hass, ufp, [camera]) + assert_entity_counts(hass, Platform.SENSOR, 23, 13) + + _, entity_id = ids_from_device_description( + Platform.SENSOR, camera, LICENSE_PLATE_EVENT_SENSORS[0] + ) + + event_metadata = EventMetadata( + license_plate=LicensePlateMetadata(name="ABCD1234", confidence_level=95) + ) + event = Event( + model=ModelType.EVENT, + id="test_event_id", + type=EventType.SMART_DETECT, + start=fixed_now - timedelta(seconds=1), + end=None, + score=100, + smart_detect_types=[SmartDetectObjectType.LICENSE_PLATE], + smart_detect_event_ids=[], + metadata=event_metadata, + api=ufp.api, + ) + + new_camera = camera.copy() + new_camera.is_smart_detected = True + new_camera.last_smart_detect_event_ids[SmartDetectObjectType.LICENSE_PLATE] = ( + event.id + ) + + mock_msg = Mock() + mock_msg.changed_data = {} + mock_msg.new_obj = new_camera + + ufp.api.bootstrap.cameras = {new_camera.id: new_camera} + ufp.api.bootstrap.events = {event.id: event} + + state_changes: list[HAEvent[EventStateChangedData]] = async_capture_events( + hass, EVENT_STATE_CHANGED + ) + ufp.ws_msg(mock_msg) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state + assert state.state == "ABCD1234" + assert state.attributes[ATTR_EVENT_SCORE] == 100 + + assert len(state_changes) == 1 + + ufp.ws_msg(mock_msg) + await hass.async_block_till_done() + assert len(state_changes) == 1 + + # Now mutate the original event so the score changes + event.score = 99 + event_metadata.license_plate.name = "DCBA4321" + ufp.api.bootstrap.events = {event.id: event} + + ufp.ws_msg(mock_msg) + await hass.async_block_till_done() + assert len(state_changes) == 2 + state = hass.states.get(entity_id) + assert state + assert state.state == "DCBA4321" + assert state.attributes[ATTR_EVENT_SCORE] == 99 + + # Now mutate the original event so the score changes again + event.score = 40 + ufp.api.bootstrap.events = {event.id: event} + + ufp.ws_msg(mock_msg) + await hass.async_block_till_done() + assert len(state_changes) == 3 + state = hass.states.get(entity_id) + assert state + assert state.state == "DCBA4321" + assert state.attributes[ATTR_EVENT_SCORE] == 40 + + # Now send the event again + ufp.api.bootstrap.events = {event.id: event} + + ufp.ws_msg(mock_msg) + await hass.async_block_till_done() + assert len(state_changes) == 3 + state = hass.states.get(entity_id) + assert state + assert state.state == "DCBA4321" + assert state.attributes[ATTR_EVENT_SCORE] == 40 + + # Now mutate the original event to add an end time + event.end = fixed_now + timedelta(seconds=1) + ufp.api.bootstrap.events = {event.id: event} + + ufp.ws_msg(mock_msg) + await hass.async_block_till_done() + assert len(state_changes) == 4 + state = hass.states.get(entity_id) + assert state + assert state.state == "none" + + # Now send the event again + event.end = fixed_now + timedelta(seconds=1) + ufp.api.bootstrap.events = {event.id: event} + + ufp.ws_msg(mock_msg) + await hass.async_block_till_done() + assert len(state_changes) == 4 + state = hass.states.get(entity_id) + assert state + assert state.state == "none" + + async def test_sensor_precision( hass: HomeAssistant, ufp: MockUFPFixture, sensor_all: Sensor, fixed_now: datetime ) -> None: From 1aa9094d3d415b3f9a4b8ccfa47ce78924b1deba Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Fri, 21 Jun 2024 23:19:47 +0200 Subject: [PATCH 0964/1445] Adjust hddtemp test Telnet patch location (#120121) --- pyproject.toml | 1 + tests/components/hddtemp/test_sensor.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 56a10cfcd71..6578bcb5f36 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -585,6 +585,7 @@ filterwarnings = [ # -- Python 3.13 # HomeAssistant "ignore:'audioop' is deprecated and slated for removal in Python 3.13:DeprecationWarning:homeassistant.components.assist_pipeline.websocket_api", + "ignore:'telnetlib' is deprecated and slated for removal in Python 3.13:DeprecationWarning:homeassistant.components.hddtemp.sensor", # https://pypi.org/project/nextcord/ - v2.6.0 - 2023-09-23 # https://github.com/nextcord/nextcord/issues/1174 # https://github.com/nextcord/nextcord/blob/v2.6.1/nextcord/player.py#L5 diff --git a/tests/components/hddtemp/test_sensor.py b/tests/components/hddtemp/test_sensor.py index f1851f959f0..2bd0519c12c 100644 --- a/tests/components/hddtemp/test_sensor.py +++ b/tests/components/hddtemp/test_sensor.py @@ -85,7 +85,7 @@ class TelnetMock: @pytest.fixture def telnetmock(): """Mock telnet.""" - with patch("telnetlib.Telnet", new=TelnetMock): + with patch("homeassistant.components.hddtemp.sensor.Telnet", new=TelnetMock): yield From 47587ee3fb8083b31d72a29c50e5ea7833104d45 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 21 Jun 2024 17:11:28 -0500 Subject: [PATCH 0965/1445] Fix race against is_smart_detected in unifiprotect (#120133) --- .../components/unifiprotect/binary_sensor.py | 10 +- .../components/unifiprotect/sensor.py | 12 +-- tests/components/unifiprotect/test_sensor.py | 98 +++++++++++++++++++ 3 files changed, 109 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/unifiprotect/binary_sensor.py b/homeassistant/components/unifiprotect/binary_sensor.py index decb0bf2a18..5596d3b7a62 100644 --- a/homeassistant/components/unifiprotect/binary_sensor.py +++ b/homeassistant/components/unifiprotect/binary_sensor.py @@ -742,21 +742,21 @@ class ProtectSmartEventBinarySensor(EventEntityMixin, BinarySensorEntity): prev_event = self._event prev_event_end = self._event_end super()._async_update_device_from_protect(device) - event = self._event = description.get_event_obj(device) - self._event_end = event.end if event else None + if event := description.get_event_obj(device): + self._event = event + self._event_end = event.end if event else None if not ( event - and not self._event_already_ended(prev_event, prev_event_end) and description.has_matching_smart(event) - and ((is_end := event.end) or self.device.is_smart_detected) + and not self._event_already_ended(prev_event, prev_event_end) ): self._set_event_done() return self._attr_is_on = True self._set_event_attrs(event) - if is_end: + if event.end: self._async_event_with_immediate_end() diff --git a/homeassistant/components/unifiprotect/sensor.py b/homeassistant/components/unifiprotect/sensor.py index da0742afcd5..84cac342d00 100644 --- a/homeassistant/components/unifiprotect/sensor.py +++ b/homeassistant/components/unifiprotect/sensor.py @@ -762,21 +762,21 @@ class ProtectLicensePlateEventSensor(ProtectEventSensor): prev_event = self._event prev_event_end = self._event_end super()._async_update_device_from_protect(device) - event = self._event = description.get_event_obj(device) - self._event_end = event.end if event else None + if event := description.get_event_obj(device): + self._event = event + self._event_end = event.end if not ( event - and not self._event_already_ended(prev_event, prev_event_end) - and description.has_matching_smart(event) - and ((is_end := event.end) or self.device.is_smart_detected) and (metadata := event.metadata) and (license_plate := metadata.license_plate) + and description.has_matching_smart(event) + and not self._event_already_ended(prev_event, prev_event_end) ): self._set_event_done() return self._attr_native_value = license_plate.name self._set_event_attrs(event) - if is_end: + if event.end: self._async_event_with_immediate_end() diff --git a/tests/components/unifiprotect/test_sensor.py b/tests/components/unifiprotect/test_sensor.py index 06d87440e94..bc5f372c598 100644 --- a/tests/components/unifiprotect/test_sensor.py +++ b/tests/components/unifiprotect/test_sensor.py @@ -839,6 +839,104 @@ async def test_camera_update_license_plate_multiple_updates( assert state.state == "none" +async def test_camera_update_license_no_dupes( + hass: HomeAssistant, ufp: MockUFPFixture, camera: Camera, fixed_now: datetime +) -> None: + """Test license plate sensor does not generate duplicate reads.""" + + camera.feature_flags.smart_detect_types.append(SmartDetectObjectType.LICENSE_PLATE) + camera.feature_flags.has_smart_detect = True + camera.smart_detect_settings.object_types.append( + SmartDetectObjectType.LICENSE_PLATE + ) + + await init_entry(hass, ufp, [camera]) + assert_entity_counts(hass, Platform.SENSOR, 23, 13) + + _, entity_id = ids_from_device_description( + Platform.SENSOR, camera, LICENSE_PLATE_EVENT_SENSORS[0] + ) + + event_metadata = EventMetadata( + license_plate=LicensePlateMetadata(name="FPR2238", confidence_level=91) + ) + event = Event( + model=ModelType.EVENT, + id="6675e36400de8c03e40bd5e3", + type=EventType.SMART_DETECT, + start=fixed_now - timedelta(seconds=1), + end=None, + score=83, + smart_detect_types=[SmartDetectObjectType.LICENSE_PLATE], + smart_detect_event_ids=[], + metadata=event_metadata, + api=ufp.api, + ) + + new_camera = camera.copy() + new_camera.is_smart_detected = True + new_camera.last_smart_detect_event_ids[SmartDetectObjectType.LICENSE_PLATE] = ( + event.id + ) + + mock_msg = Mock() + mock_msg.changed_data = {} + mock_msg.new_obj = new_camera + + ufp.api.bootstrap.cameras = {new_camera.id: new_camera} + ufp.api.bootstrap.events = {event.id: event} + + state_changes: list[HAEvent[EventStateChangedData]] = async_capture_events( + hass, EVENT_STATE_CHANGED + ) + ufp.ws_msg(mock_msg) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state + assert state.state == "FPR2238" + assert state.attributes[ATTR_EVENT_SCORE] == 83 + + assert len(state_changes) == 1 + + # Now send it again + ufp.api.bootstrap.events = {event.id: event} + ufp.ws_msg(mock_msg) + await hass.async_block_till_done() + assert len(state_changes) == 1 + + # Again send it again + ufp.api.bootstrap.events = {event.id: event} + ufp.ws_msg(mock_msg) + await hass.async_block_till_done() + assert len(state_changes) == 1 + + # Now add the end time and change the confidence level + event.end = fixed_now + timedelta(seconds=1) + event.metadata.license_plate.confidence_level = 96 + ufp.api.bootstrap.events = {event.id: event} + ufp.ws_msg(mock_msg) + await hass.async_block_till_done() + assert len(state_changes) == 2 + + state = hass.states.get(entity_id) + assert state + assert state.state == "none" + + # Now send it 3 more times + for _ in range(3): + ufp.api.bootstrap.events = {event.id: event} + ufp.ws_msg(mock_msg) + await hass.async_block_till_done() + assert len(state_changes) == 2 + + # Now clear the event + ufp.api.bootstrap.events = {} + ufp.ws_msg(mock_msg) + await hass.async_block_till_done() + assert len(state_changes) == 2 + + async def test_sensor_precision( hass: HomeAssistant, ufp: MockUFPFixture, sensor_all: Sensor, fixed_now: datetime ) -> None: From cb78caf4551309c101008ab815a9d27f47c4e399 Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Sat, 22 Jun 2024 16:56:50 +1000 Subject: [PATCH 0966/1445] Platinum quality on Teslemetry (#115191) --- homeassistant/components/teslemetry/binary_sensor.py | 2 ++ homeassistant/components/teslemetry/button.py | 2 ++ homeassistant/components/teslemetry/climate.py | 2 ++ homeassistant/components/teslemetry/cover.py | 2 ++ homeassistant/components/teslemetry/device_tracker.py | 2 ++ homeassistant/components/teslemetry/lock.py | 2 ++ homeassistant/components/teslemetry/manifest.json | 1 + homeassistant/components/teslemetry/media_player.py | 2 ++ homeassistant/components/teslemetry/number.py | 2 ++ homeassistant/components/teslemetry/select.py | 2 ++ homeassistant/components/teslemetry/sensor.py | 2 ++ homeassistant/components/teslemetry/switch.py | 2 ++ homeassistant/components/teslemetry/update.py | 2 ++ 13 files changed, 25 insertions(+) diff --git a/homeassistant/components/teslemetry/binary_sensor.py b/homeassistant/components/teslemetry/binary_sensor.py index 5613f622aeb..e3f9a5716f6 100644 --- a/homeassistant/components/teslemetry/binary_sensor.py +++ b/homeassistant/components/teslemetry/binary_sensor.py @@ -26,6 +26,8 @@ from .entity import ( ) from .models import TeslemetryEnergyData, TeslemetryVehicleData +PARALLEL_UPDATES = 0 + @dataclass(frozen=True, kw_only=True) class TeslemetryBinarySensorEntityDescription(BinarySensorEntityDescription): diff --git a/homeassistant/components/teslemetry/button.py b/homeassistant/components/teslemetry/button.py index 011879525b8..a9bf3eddd6a 100644 --- a/homeassistant/components/teslemetry/button.py +++ b/homeassistant/components/teslemetry/button.py @@ -17,6 +17,8 @@ from .entity import TeslemetryVehicleEntity from .helpers import handle_vehicle_command from .models import TeslemetryVehicleData +PARALLEL_UPDATES = 0 + @dataclass(frozen=True, kw_only=True) class TeslemetryButtonEntityDescription(ButtonEntityDescription): diff --git a/homeassistant/components/teslemetry/climate.py b/homeassistant/components/teslemetry/climate.py index 1158822f960..5b093b0c6f1 100644 --- a/homeassistant/components/teslemetry/climate.py +++ b/homeassistant/components/teslemetry/climate.py @@ -32,6 +32,8 @@ from .models import TeslemetryVehicleData DEFAULT_MIN_TEMP = 15 DEFAULT_MAX_TEMP = 28 +PARALLEL_UPDATES = 0 + async def async_setup_entry( hass: HomeAssistant, diff --git a/homeassistant/components/teslemetry/cover.py b/homeassistant/components/teslemetry/cover.py index 4fbbb5fdb2b..44e84626eb2 100644 --- a/homeassistant/components/teslemetry/cover.py +++ b/homeassistant/components/teslemetry/cover.py @@ -23,6 +23,8 @@ from .models import TeslemetryVehicleData OPEN = 1 CLOSED = 0 +PARALLEL_UPDATES = 0 + async def async_setup_entry( hass: HomeAssistant, diff --git a/homeassistant/components/teslemetry/device_tracker.py b/homeassistant/components/teslemetry/device_tracker.py index 8e270f9cf29..399d28533f1 100644 --- a/homeassistant/components/teslemetry/device_tracker.py +++ b/homeassistant/components/teslemetry/device_tracker.py @@ -11,6 +11,8 @@ from . import TeslemetryConfigEntry from .entity import TeslemetryVehicleEntity from .models import TeslemetryVehicleData +PARALLEL_UPDATES = 0 + async def async_setup_entry( hass: HomeAssistant, diff --git a/homeassistant/components/teslemetry/lock.py b/homeassistant/components/teslemetry/lock.py index 2201b898d66..e23747924f6 100644 --- a/homeassistant/components/teslemetry/lock.py +++ b/homeassistant/components/teslemetry/lock.py @@ -19,6 +19,8 @@ from .models import TeslemetryVehicleData ENGAGED = "Engaged" +PARALLEL_UPDATES = 0 + async def async_setup_entry( hass: HomeAssistant, diff --git a/homeassistant/components/teslemetry/manifest.json b/homeassistant/components/teslemetry/manifest.json index 36a655b3b11..2eb3e221855 100644 --- a/homeassistant/components/teslemetry/manifest.json +++ b/homeassistant/components/teslemetry/manifest.json @@ -6,5 +6,6 @@ "documentation": "https://www.home-assistant.io/integrations/teslemetry", "iot_class": "cloud_polling", "loggers": ["tesla-fleet-api"], + "quality_scale": "platinum", "requirements": ["tesla-fleet-api==0.6.1"] } diff --git a/homeassistant/components/teslemetry/media_player.py b/homeassistant/components/teslemetry/media_player.py index 31c58e9505b..b21ba0f733d 100644 --- a/homeassistant/components/teslemetry/media_player.py +++ b/homeassistant/components/teslemetry/media_player.py @@ -27,6 +27,8 @@ STATES = { VOLUME_MAX = 11.0 VOLUME_STEP = 1.0 / 3 +PARALLEL_UPDATES = 0 + async def async_setup_entry( hass: HomeAssistant, diff --git a/homeassistant/components/teslemetry/number.py b/homeassistant/components/teslemetry/number.py index 258fc5c5559..8c14c8e4186 100644 --- a/homeassistant/components/teslemetry/number.py +++ b/homeassistant/components/teslemetry/number.py @@ -26,6 +26,8 @@ from .entity import TeslemetryEnergyInfoEntity, TeslemetryVehicleEntity from .helpers import handle_command, handle_vehicle_command from .models import TeslemetryEnergyData, TeslemetryVehicleData +PARALLEL_UPDATES = 0 + @dataclass(frozen=True, kw_only=True) class TeslemetryNumberVehicleEntityDescription(NumberEntityDescription): diff --git a/homeassistant/components/teslemetry/select.py b/homeassistant/components/teslemetry/select.py index 10d925ad94d..7cbdd4e31d2 100644 --- a/homeassistant/components/teslemetry/select.py +++ b/homeassistant/components/teslemetry/select.py @@ -22,6 +22,8 @@ LOW = "low" MEDIUM = "medium" HIGH = "high" +PARALLEL_UPDATES = 0 + @dataclass(frozen=True, kw_only=True) class SeatHeaterDescription(SelectEntityDescription): diff --git a/homeassistant/components/teslemetry/sensor.py b/homeassistant/components/teslemetry/sensor.py index c179d0edf5d..90b37cc1dac 100644 --- a/homeassistant/components/teslemetry/sensor.py +++ b/homeassistant/components/teslemetry/sensor.py @@ -42,6 +42,8 @@ from .entity import ( ) from .models import TeslemetryEnergyData, TeslemetryVehicleData +PARALLEL_UPDATES = 0 + CHARGE_STATES = { "Starting": "starting", "Charging": "charging", diff --git a/homeassistant/components/teslemetry/switch.py b/homeassistant/components/teslemetry/switch.py index e23d34f242a..3204d73410f 100644 --- a/homeassistant/components/teslemetry/switch.py +++ b/homeassistant/components/teslemetry/switch.py @@ -22,6 +22,8 @@ from .entity import TeslemetryEnergyInfoEntity, TeslemetryVehicleEntity from .helpers import handle_command, handle_vehicle_command from .models import TeslemetryEnergyData, TeslemetryVehicleData +PARALLEL_UPDATES = 0 + @dataclass(frozen=True, kw_only=True) class TeslemetrySwitchEntityDescription(SwitchEntityDescription): diff --git a/homeassistant/components/teslemetry/update.py b/homeassistant/components/teslemetry/update.py index 74ecec8020d..de508fa58d4 100644 --- a/homeassistant/components/teslemetry/update.py +++ b/homeassistant/components/teslemetry/update.py @@ -21,6 +21,8 @@ INSTALLING = "installing" WIFI_WAIT = "downloading_wifi_wait" SCHEDULED = "scheduled" +PARALLEL_UPDATES = 0 + async def async_setup_entry( hass: HomeAssistant, From 2e3aeae5204592067f67f2113b3ecd6add876c6c Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 22 Jun 2024 09:06:05 +0200 Subject: [PATCH 0967/1445] Extend component root imports in tests (2) (#120123) --- tests/components/demo/test_media_player.py | 271 ++++++++++++--------- 1 file changed, 153 insertions(+), 118 deletions(-) diff --git a/tests/components/demo/test_media_player.py b/tests/components/demo/test_media_player.py index 8e7b32cc4b7..a6669fa705c 100644 --- a/tests/components/demo/test_media_player.py +++ b/tests/components/demo/test_media_player.py @@ -6,11 +6,46 @@ from unittest.mock import patch import pytest import voluptuous as vol -import homeassistant.components.media_player as mp +from homeassistant.components.media_player import ( + ATTR_GROUP_MEMBERS, + ATTR_INPUT_SOURCE, + ATTR_MEDIA_CONTENT_ID, + ATTR_MEDIA_CONTENT_TYPE, + ATTR_MEDIA_EPISODE, + ATTR_MEDIA_REPEAT, + ATTR_MEDIA_SEEK_POSITION, + ATTR_MEDIA_TRACK, + ATTR_MEDIA_VOLUME_LEVEL, + ATTR_MEDIA_VOLUME_MUTED, + DOMAIN as MP_DOMAIN, + SERVICE_CLEAR_PLAYLIST, + SERVICE_JOIN, + SERVICE_PLAY_MEDIA, + SERVICE_SELECT_SOURCE, + SERVICE_UNJOIN, + MediaPlayerEntityFeature, + RepeatMode, + is_on, +) from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_ENTITY_PICTURE, ATTR_SUPPORTED_FEATURES, + SERVICE_MEDIA_NEXT_TRACK, + SERVICE_MEDIA_PAUSE, + SERVICE_MEDIA_PLAY, + SERVICE_MEDIA_PLAY_PAUSE, + SERVICE_MEDIA_PREVIOUS_TRACK, + SERVICE_MEDIA_SEEK, + SERVICE_MEDIA_STOP, + SERVICE_REPEAT_SET, + SERVICE_TOGGLE, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + SERVICE_VOLUME_DOWN, + SERVICE_VOLUME_MUTE, + SERVICE_VOLUME_SET, + SERVICE_VOLUME_UP, STATE_OFF, STATE_PAUSED, STATE_PLAYING, @@ -50,30 +85,30 @@ async def test_source_select(hass: HomeAssistant) -> None: entity_id = "media_player.lounge_room" assert await async_setup_component( - hass, mp.DOMAIN, {"media_player": {"platform": "demo"}} + hass, MP_DOMAIN, {"media_player": {"platform": "demo"}} ) await hass.async_block_till_done() state = hass.states.get(entity_id) - assert state.attributes.get(mp.ATTR_INPUT_SOURCE) == "dvd" + assert state.attributes.get(ATTR_INPUT_SOURCE) == "dvd" with pytest.raises(vol.Invalid): await hass.services.async_call( - mp.DOMAIN, - mp.SERVICE_SELECT_SOURCE, - {ATTR_ENTITY_ID: entity_id, mp.ATTR_INPUT_SOURCE: None}, + MP_DOMAIN, + SERVICE_SELECT_SOURCE, + {ATTR_ENTITY_ID: entity_id, ATTR_INPUT_SOURCE: None}, blocking=True, ) state = hass.states.get(entity_id) - assert state.attributes.get(mp.ATTR_INPUT_SOURCE) == "dvd" + assert state.attributes.get(ATTR_INPUT_SOURCE) == "dvd" await hass.services.async_call( - mp.DOMAIN, - mp.SERVICE_SELECT_SOURCE, - {ATTR_ENTITY_ID: entity_id, mp.ATTR_INPUT_SOURCE: "xbox"}, + MP_DOMAIN, + SERVICE_SELECT_SOURCE, + {ATTR_ENTITY_ID: entity_id, ATTR_INPUT_SOURCE: "xbox"}, blocking=True, ) state = hass.states.get(entity_id) - assert state.attributes.get(mp.ATTR_INPUT_SOURCE) == "xbox" + assert state.attributes.get(ATTR_INPUT_SOURCE) == "xbox" async def test_repeat_set(hass: HomeAssistant) -> None: @@ -81,26 +116,26 @@ async def test_repeat_set(hass: HomeAssistant) -> None: entity_id = "media_player.walkman" assert await async_setup_component( - hass, mp.DOMAIN, {"media_player": {"platform": "demo"}} + hass, MP_DOMAIN, {"media_player": {"platform": "demo"}} ) await hass.async_block_till_done() state = hass.states.get(entity_id) - assert state.attributes.get(mp.ATTR_MEDIA_REPEAT) == mp.const.REPEAT_MODE_OFF + assert state.attributes.get(ATTR_MEDIA_REPEAT) == RepeatMode.OFF await hass.services.async_call( - mp.DOMAIN, - mp.SERVICE_REPEAT_SET, - {ATTR_ENTITY_ID: entity_id, mp.ATTR_MEDIA_REPEAT: mp.const.REPEAT_MODE_ALL}, + MP_DOMAIN, + SERVICE_REPEAT_SET, + {ATTR_ENTITY_ID: entity_id, ATTR_MEDIA_REPEAT: RepeatMode.ALL}, blocking=True, ) state = hass.states.get(entity_id) - assert state.attributes.get(mp.ATTR_MEDIA_REPEAT) == mp.const.REPEAT_MODE_ALL + assert state.attributes.get(ATTR_MEDIA_REPEAT) == RepeatMode.ALL async def test_clear_playlist(hass: HomeAssistant) -> None: """Test clear playlist.""" assert await async_setup_component( - hass, mp.DOMAIN, {"media_player": {"platform": "demo"}} + hass, MP_DOMAIN, {"media_player": {"platform": "demo"}} ) await hass.async_block_till_done() @@ -108,8 +143,8 @@ async def test_clear_playlist(hass: HomeAssistant) -> None: assert state.state == STATE_PLAYING await hass.services.async_call( - mp.DOMAIN, - mp.SERVICE_CLEAR_PLAYLIST, + MP_DOMAIN, + SERVICE_CLEAR_PLAYLIST, {ATTR_ENTITY_ID: TEST_ENTITY_ID}, blocking=True, ) @@ -120,79 +155,79 @@ async def test_clear_playlist(hass: HomeAssistant) -> None: async def test_volume_services(hass: HomeAssistant) -> None: """Test the volume service.""" assert await async_setup_component( - hass, mp.DOMAIN, {"media_player": {"platform": "demo"}} + hass, MP_DOMAIN, {"media_player": {"platform": "demo"}} ) await hass.async_block_till_done() state = hass.states.get(TEST_ENTITY_ID) - assert state.attributes.get(mp.ATTR_MEDIA_VOLUME_LEVEL) == 1.0 + assert state.attributes.get(ATTR_MEDIA_VOLUME_LEVEL) == 1.0 with pytest.raises(vol.Invalid): await hass.services.async_call( - mp.DOMAIN, - mp.SERVICE_VOLUME_SET, - {ATTR_ENTITY_ID: TEST_ENTITY_ID, mp.ATTR_MEDIA_VOLUME_LEVEL: None}, + MP_DOMAIN, + SERVICE_VOLUME_SET, + {ATTR_ENTITY_ID: TEST_ENTITY_ID, ATTR_MEDIA_VOLUME_LEVEL: None}, blocking=True, ) state = hass.states.get(TEST_ENTITY_ID) - assert state.attributes.get(mp.ATTR_MEDIA_VOLUME_LEVEL) == 1.0 + assert state.attributes.get(ATTR_MEDIA_VOLUME_LEVEL) == 1.0 await hass.services.async_call( - mp.DOMAIN, - mp.SERVICE_VOLUME_SET, - {ATTR_ENTITY_ID: TEST_ENTITY_ID, mp.ATTR_MEDIA_VOLUME_LEVEL: 0.5}, + MP_DOMAIN, + SERVICE_VOLUME_SET, + {ATTR_ENTITY_ID: TEST_ENTITY_ID, ATTR_MEDIA_VOLUME_LEVEL: 0.5}, blocking=True, ) state = hass.states.get(TEST_ENTITY_ID) - assert state.attributes.get(mp.ATTR_MEDIA_VOLUME_LEVEL) == 0.5 + assert state.attributes.get(ATTR_MEDIA_VOLUME_LEVEL) == 0.5 await hass.services.async_call( - mp.DOMAIN, - mp.SERVICE_VOLUME_DOWN, + MP_DOMAIN, + SERVICE_VOLUME_DOWN, {ATTR_ENTITY_ID: TEST_ENTITY_ID}, blocking=True, ) state = hass.states.get(TEST_ENTITY_ID) - assert state.attributes.get(mp.ATTR_MEDIA_VOLUME_LEVEL) == 0.4 + assert state.attributes.get(ATTR_MEDIA_VOLUME_LEVEL) == 0.4 await hass.services.async_call( - mp.DOMAIN, - mp.SERVICE_VOLUME_UP, + MP_DOMAIN, + SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: TEST_ENTITY_ID}, blocking=True, ) state = hass.states.get(TEST_ENTITY_ID) - assert state.attributes.get(mp.ATTR_MEDIA_VOLUME_LEVEL) == 0.5 + assert state.attributes.get(ATTR_MEDIA_VOLUME_LEVEL) == 0.5 - assert state.attributes.get(mp.ATTR_MEDIA_VOLUME_MUTED) is False + assert state.attributes.get(ATTR_MEDIA_VOLUME_MUTED) is False with pytest.raises(vol.Invalid): await hass.services.async_call( - mp.DOMAIN, - mp.SERVICE_VOLUME_MUTE, - {ATTR_ENTITY_ID: TEST_ENTITY_ID, mp.ATTR_MEDIA_VOLUME_MUTED: None}, + MP_DOMAIN, + SERVICE_VOLUME_MUTE, + {ATTR_ENTITY_ID: TEST_ENTITY_ID, ATTR_MEDIA_VOLUME_MUTED: None}, blocking=True, ) state = hass.states.get(TEST_ENTITY_ID) - assert state.attributes.get(mp.ATTR_MEDIA_VOLUME_MUTED) is False + assert state.attributes.get(ATTR_MEDIA_VOLUME_MUTED) is False await hass.services.async_call( - mp.DOMAIN, - mp.SERVICE_VOLUME_MUTE, - {ATTR_ENTITY_ID: TEST_ENTITY_ID, mp.ATTR_MEDIA_VOLUME_MUTED: True}, + MP_DOMAIN, + SERVICE_VOLUME_MUTE, + {ATTR_ENTITY_ID: TEST_ENTITY_ID, ATTR_MEDIA_VOLUME_MUTED: True}, blocking=True, ) state = hass.states.get(TEST_ENTITY_ID) - assert state.attributes.get(mp.ATTR_MEDIA_VOLUME_MUTED) is True + assert state.attributes.get(ATTR_MEDIA_VOLUME_MUTED) is True async def test_turning_off_and_on(hass: HomeAssistant) -> None: """Test turn_on and turn_off.""" assert await async_setup_component( - hass, mp.DOMAIN, {"media_player": {"platform": "demo"}} + hass, MP_DOMAIN, {"media_player": {"platform": "demo"}} ) await hass.async_block_till_done() @@ -200,40 +235,40 @@ async def test_turning_off_and_on(hass: HomeAssistant) -> None: assert state.state == STATE_PLAYING await hass.services.async_call( - mp.DOMAIN, - mp.SERVICE_TURN_OFF, + MP_DOMAIN, + SERVICE_TURN_OFF, {ATTR_ENTITY_ID: TEST_ENTITY_ID}, blocking=True, ) state = hass.states.get(TEST_ENTITY_ID) assert state.state == STATE_OFF - assert not mp.is_on(hass, TEST_ENTITY_ID) + assert not is_on(hass, TEST_ENTITY_ID) await hass.services.async_call( - mp.DOMAIN, - mp.SERVICE_TURN_ON, + MP_DOMAIN, + SERVICE_TURN_ON, {ATTR_ENTITY_ID: TEST_ENTITY_ID}, blocking=True, ) state = hass.states.get(TEST_ENTITY_ID) assert state.state == STATE_PLAYING - assert mp.is_on(hass, TEST_ENTITY_ID) + assert is_on(hass, TEST_ENTITY_ID) await hass.services.async_call( - mp.DOMAIN, - mp.SERVICE_TOGGLE, + MP_DOMAIN, + SERVICE_TOGGLE, {ATTR_ENTITY_ID: TEST_ENTITY_ID}, blocking=True, ) state = hass.states.get(TEST_ENTITY_ID) assert state.state == STATE_OFF - assert not mp.is_on(hass, TEST_ENTITY_ID) + assert not is_on(hass, TEST_ENTITY_ID) async def test_playing_pausing(hass: HomeAssistant) -> None: """Test media_pause.""" assert await async_setup_component( - hass, mp.DOMAIN, {"media_player": {"platform": "demo"}} + hass, MP_DOMAIN, {"media_player": {"platform": "demo"}} ) await hass.async_block_till_done() @@ -241,8 +276,8 @@ async def test_playing_pausing(hass: HomeAssistant) -> None: assert state.state == STATE_PLAYING await hass.services.async_call( - mp.DOMAIN, - mp.SERVICE_MEDIA_PAUSE, + MP_DOMAIN, + SERVICE_MEDIA_PAUSE, {ATTR_ENTITY_ID: TEST_ENTITY_ID}, blocking=True, ) @@ -250,8 +285,8 @@ async def test_playing_pausing(hass: HomeAssistant) -> None: assert state.state == STATE_PAUSED await hass.services.async_call( - mp.DOMAIN, - mp.SERVICE_MEDIA_PLAY_PAUSE, + MP_DOMAIN, + SERVICE_MEDIA_PLAY_PAUSE, {ATTR_ENTITY_ID: TEST_ENTITY_ID}, blocking=True, ) @@ -259,8 +294,8 @@ async def test_playing_pausing(hass: HomeAssistant) -> None: assert state.state == STATE_PLAYING await hass.services.async_call( - mp.DOMAIN, - mp.SERVICE_MEDIA_PLAY_PAUSE, + MP_DOMAIN, + SERVICE_MEDIA_PLAY_PAUSE, {ATTR_ENTITY_ID: TEST_ENTITY_ID}, blocking=True, ) @@ -268,8 +303,8 @@ async def test_playing_pausing(hass: HomeAssistant) -> None: assert state.state == STATE_PAUSED await hass.services.async_call( - mp.DOMAIN, - mp.SERVICE_MEDIA_PLAY, + MP_DOMAIN, + SERVICE_MEDIA_PLAY, {ATTR_ENTITY_ID: TEST_ENTITY_ID}, blocking=True, ) @@ -280,148 +315,148 @@ async def test_playing_pausing(hass: HomeAssistant) -> None: async def test_prev_next_track(hass: HomeAssistant) -> None: """Test media_next_track and media_previous_track .""" assert await async_setup_component( - hass, mp.DOMAIN, {"media_player": {"platform": "demo"}} + hass, MP_DOMAIN, {"media_player": {"platform": "demo"}} ) await hass.async_block_till_done() state = hass.states.get(TEST_ENTITY_ID) - assert state.attributes.get(mp.ATTR_MEDIA_TRACK) == 1 + assert state.attributes.get(ATTR_MEDIA_TRACK) == 1 await hass.services.async_call( - mp.DOMAIN, - mp.SERVICE_MEDIA_NEXT_TRACK, + MP_DOMAIN, + SERVICE_MEDIA_NEXT_TRACK, {ATTR_ENTITY_ID: TEST_ENTITY_ID}, blocking=True, ) state = hass.states.get(TEST_ENTITY_ID) - assert state.attributes.get(mp.ATTR_MEDIA_TRACK) == 2 + assert state.attributes.get(ATTR_MEDIA_TRACK) == 2 await hass.services.async_call( - mp.DOMAIN, - mp.SERVICE_MEDIA_NEXT_TRACK, + MP_DOMAIN, + SERVICE_MEDIA_NEXT_TRACK, {ATTR_ENTITY_ID: TEST_ENTITY_ID}, blocking=True, ) state = hass.states.get(TEST_ENTITY_ID) - assert state.attributes.get(mp.ATTR_MEDIA_TRACK) == 3 + assert state.attributes.get(ATTR_MEDIA_TRACK) == 3 await hass.services.async_call( - mp.DOMAIN, - mp.SERVICE_MEDIA_PREVIOUS_TRACK, + MP_DOMAIN, + SERVICE_MEDIA_PREVIOUS_TRACK, {ATTR_ENTITY_ID: TEST_ENTITY_ID}, blocking=True, ) state = hass.states.get(TEST_ENTITY_ID) - assert state.attributes.get(mp.ATTR_MEDIA_TRACK) == 2 + assert state.attributes.get(ATTR_MEDIA_TRACK) == 2 assert await async_setup_component( - hass, mp.DOMAIN, {"media_player": {"platform": "demo"}} + hass, MP_DOMAIN, {"media_player": {"platform": "demo"}} ) await hass.async_block_till_done() ent_id = "media_player.lounge_room" state = hass.states.get(ent_id) - assert state.attributes.get(mp.ATTR_MEDIA_EPISODE) == "1" + assert state.attributes.get(ATTR_MEDIA_EPISODE) == "1" await hass.services.async_call( - mp.DOMAIN, - mp.SERVICE_MEDIA_NEXT_TRACK, + MP_DOMAIN, + SERVICE_MEDIA_NEXT_TRACK, {ATTR_ENTITY_ID: ent_id}, blocking=True, ) state = hass.states.get(ent_id) - assert state.attributes.get(mp.ATTR_MEDIA_EPISODE) == "2" + assert state.attributes.get(ATTR_MEDIA_EPISODE) == "2" await hass.services.async_call( - mp.DOMAIN, - mp.SERVICE_MEDIA_PREVIOUS_TRACK, + MP_DOMAIN, + SERVICE_MEDIA_PREVIOUS_TRACK, {ATTR_ENTITY_ID: ent_id}, blocking=True, ) state = hass.states.get(ent_id) - assert state.attributes.get(mp.ATTR_MEDIA_EPISODE) == "1" + assert state.attributes.get(ATTR_MEDIA_EPISODE) == "1" async def test_play_media(hass: HomeAssistant) -> None: """Test play_media .""" assert await async_setup_component( - hass, mp.DOMAIN, {"media_player": {"platform": "demo"}} + hass, MP_DOMAIN, {"media_player": {"platform": "demo"}} ) await hass.async_block_till_done() ent_id = "media_player.living_room" state = hass.states.get(ent_id) assert ( - mp.MediaPlayerEntityFeature.PLAY_MEDIA + MediaPlayerEntityFeature.PLAY_MEDIA & state.attributes.get(ATTR_SUPPORTED_FEATURES) > 0 ) - assert state.attributes.get(mp.ATTR_MEDIA_CONTENT_ID) is not None + assert state.attributes.get(ATTR_MEDIA_CONTENT_ID) is not None with pytest.raises(vol.Invalid): await hass.services.async_call( - mp.DOMAIN, - mp.SERVICE_PLAY_MEDIA, - {ATTR_ENTITY_ID: ent_id, mp.ATTR_MEDIA_CONTENT_ID: "some_id"}, + MP_DOMAIN, + SERVICE_PLAY_MEDIA, + {ATTR_ENTITY_ID: ent_id, ATTR_MEDIA_CONTENT_ID: "some_id"}, blocking=True, ) state = hass.states.get(ent_id) assert ( - mp.MediaPlayerEntityFeature.PLAY_MEDIA + MediaPlayerEntityFeature.PLAY_MEDIA & state.attributes.get(ATTR_SUPPORTED_FEATURES) > 0 ) - assert state.attributes.get(mp.ATTR_MEDIA_CONTENT_ID) != "some_id" + assert state.attributes.get(ATTR_MEDIA_CONTENT_ID) != "some_id" await hass.services.async_call( - mp.DOMAIN, - mp.SERVICE_PLAY_MEDIA, + MP_DOMAIN, + SERVICE_PLAY_MEDIA, { ATTR_ENTITY_ID: ent_id, - mp.ATTR_MEDIA_CONTENT_TYPE: "youtube", - mp.ATTR_MEDIA_CONTENT_ID: "some_id", + ATTR_MEDIA_CONTENT_TYPE: "youtube", + ATTR_MEDIA_CONTENT_ID: "some_id", }, blocking=True, ) state = hass.states.get(ent_id) assert ( - mp.MediaPlayerEntityFeature.PLAY_MEDIA + MediaPlayerEntityFeature.PLAY_MEDIA & state.attributes.get(ATTR_SUPPORTED_FEATURES) > 0 ) - assert state.attributes.get(mp.ATTR_MEDIA_CONTENT_ID) == "some_id" + assert state.attributes.get(ATTR_MEDIA_CONTENT_ID) == "some_id" async def test_seek(hass: HomeAssistant, mock_media_seek) -> None: """Test seek.""" assert await async_setup_component( - hass, mp.DOMAIN, {"media_player": {"platform": "demo"}} + hass, MP_DOMAIN, {"media_player": {"platform": "demo"}} ) await hass.async_block_till_done() ent_id = "media_player.living_room" state = hass.states.get(ent_id) - assert state.attributes[ATTR_SUPPORTED_FEATURES] & mp.MediaPlayerEntityFeature.SEEK + assert state.attributes[ATTR_SUPPORTED_FEATURES] & MediaPlayerEntityFeature.SEEK assert not mock_media_seek.called with pytest.raises(vol.Invalid): await hass.services.async_call( - mp.DOMAIN, - mp.SERVICE_MEDIA_SEEK, + MP_DOMAIN, + SERVICE_MEDIA_SEEK, { ATTR_ENTITY_ID: ent_id, - mp.ATTR_MEDIA_SEEK_POSITION: None, + ATTR_MEDIA_SEEK_POSITION: None, }, blocking=True, ) assert not mock_media_seek.called await hass.services.async_call( - mp.DOMAIN, - mp.SERVICE_MEDIA_SEEK, + MP_DOMAIN, + SERVICE_MEDIA_SEEK, { ATTR_ENTITY_ID: ent_id, - mp.ATTR_MEDIA_SEEK_POSITION: 100, + ATTR_MEDIA_SEEK_POSITION: 100, }, blocking=True, ) @@ -431,7 +466,7 @@ async def test_seek(hass: HomeAssistant, mock_media_seek) -> None: async def test_stop(hass: HomeAssistant) -> None: """Test stop.""" assert await async_setup_component( - hass, mp.DOMAIN, {"media_player": {"platform": "demo"}} + hass, MP_DOMAIN, {"media_player": {"platform": "demo"}} ) await hass.async_block_till_done() @@ -439,8 +474,8 @@ async def test_stop(hass: HomeAssistant) -> None: assert state.state == STATE_PLAYING await hass.services.async_call( - mp.DOMAIN, - mp.SERVICE_MEDIA_STOP, + MP_DOMAIN, + SERVICE_MEDIA_STOP, {ATTR_ENTITY_ID: TEST_ENTITY_ID}, blocking=True, ) @@ -453,7 +488,7 @@ async def test_media_image_proxy( ) -> None: """Test the media server image proxy server .""" assert await async_setup_component( - hass, mp.DOMAIN, {"media_player": {"platform": "demo"}} + hass, MP_DOMAIN, {"media_player": {"platform": "demo"}} ) await hass.async_block_till_done() @@ -500,31 +535,31 @@ async def test_grouping(hass: HomeAssistant) -> None: kitchen = "media_player.kitchen" assert await async_setup_component( - hass, mp.DOMAIN, {"media_player": {"platform": "demo"}} + hass, MP_DOMAIN, {"media_player": {"platform": "demo"}} ) await hass.async_block_till_done() state = hass.states.get(walkman) - assert state.attributes.get(mp.ATTR_GROUP_MEMBERS) == [] + assert state.attributes.get(ATTR_GROUP_MEMBERS) == [] await hass.services.async_call( - mp.DOMAIN, - mp.SERVICE_JOIN, + MP_DOMAIN, + SERVICE_JOIN, { ATTR_ENTITY_ID: walkman, - mp.ATTR_GROUP_MEMBERS: [ + ATTR_GROUP_MEMBERS: [ kitchen, ], }, blocking=True, ) state = hass.states.get(walkman) - assert state.attributes.get(mp.ATTR_GROUP_MEMBERS) == [walkman, kitchen] + assert state.attributes.get(ATTR_GROUP_MEMBERS) == [walkman, kitchen] await hass.services.async_call( - mp.DOMAIN, - mp.SERVICE_UNJOIN, + MP_DOMAIN, + SERVICE_UNJOIN, {ATTR_ENTITY_ID: walkman}, blocking=True, ) state = hass.states.get(walkman) - assert state.attributes.get(mp.ATTR_GROUP_MEMBERS) == [] + assert state.attributes.get(ATTR_GROUP_MEMBERS) == [] From bd72637fec9a83a7aade79a5a5079d54dc0fc04f Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 22 Jun 2024 09:06:31 +0200 Subject: [PATCH 0968/1445] Extend component root imports in tests (1) (#120122) --- tests/components/homematic/test_notify.py | 12 +++--- .../test_image_processing.py | 28 ++++++------- .../test_image_processing.py | 28 ++++++------- .../notify/test_persistent_notification.py | 6 +-- .../sighthound/test_image_processing.py | 41 +++++++++++-------- tests/components/yamaha/test_media_player.py | 16 ++++---- 6 files changed, 68 insertions(+), 63 deletions(-) diff --git a/tests/components/homematic/test_notify.py b/tests/components/homematic/test_notify.py index 014c0b0ae53..a07bece9850 100644 --- a/tests/components/homematic/test_notify.py +++ b/tests/components/homematic/test_notify.py @@ -1,6 +1,6 @@ """The tests for the Homematic notification platform.""" -import homeassistant.components.notify as notify_comp +from homeassistant.components.notify import DOMAIN as NOTIFY_DOMAIN from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component @@ -30,7 +30,7 @@ async def test_setup_full(hass: HomeAssistant) -> None: } }, ) - assert handle_config[notify_comp.DOMAIN] + assert handle_config[NOTIFY_DOMAIN] async def test_setup_without_optional(hass: HomeAssistant) -> None: @@ -55,12 +55,12 @@ async def test_setup_without_optional(hass: HomeAssistant) -> None: } }, ) - assert handle_config[notify_comp.DOMAIN] + assert handle_config[NOTIFY_DOMAIN] async def test_bad_config(hass: HomeAssistant) -> None: """Test invalid configuration.""" - config = {notify_comp.DOMAIN: {"name": "test", "platform": "homematic"}} + config = {NOTIFY_DOMAIN: {"name": "test", "platform": "homematic"}} with assert_setup_component(0, domain="notify") as handle_config: - assert await async_setup_component(hass, notify_comp.DOMAIN, config) - assert not handle_config[notify_comp.DOMAIN] + assert await async_setup_component(hass, NOTIFY_DOMAIN, config) + assert not handle_config[NOTIFY_DOMAIN] diff --git a/tests/components/microsoft_face_detect/test_image_processing.py b/tests/components/microsoft_face_detect/test_image_processing.py index 0c0bcb59c0b..7525663143f 100644 --- a/tests/components/microsoft_face_detect/test_image_processing.py +++ b/tests/components/microsoft_face_detect/test_image_processing.py @@ -4,8 +4,8 @@ from unittest.mock import PropertyMock, patch import pytest -import homeassistant.components.image_processing as ip -import homeassistant.components.microsoft_face as mf +from homeassistant.components.image_processing import DOMAIN as IP_DOMAIN +from homeassistant.components.microsoft_face import DOMAIN as MF_DOMAIN, FACE_API_URL from homeassistant.const import ATTR_ENTITY_PICTURE from homeassistant.core import HomeAssistant, callback from homeassistant.setup import async_setup_component @@ -15,16 +15,16 @@ from tests.components.image_processing import common from tests.test_util.aiohttp import AiohttpClientMocker CONFIG = { - ip.DOMAIN: { + IP_DOMAIN: { "platform": "microsoft_face_detect", "source": {"entity_id": "camera.demo_camera", "name": "test local"}, "attributes": ["age", "gender"], }, "camera": {"platform": "demo"}, - mf.DOMAIN: {"api_key": "12345678abcdef6"}, + MF_DOMAIN: {"api_key": "12345678abcdef6"}, } -ENDPOINT_URL = f"https://westus.{mf.FACE_API_URL}" +ENDPOINT_URL = f"https://westus.{FACE_API_URL}" @pytest.fixture(autouse=True) @@ -57,17 +57,17 @@ def poll_mock(): async def test_setup_platform(hass: HomeAssistant, store_mock) -> None: """Set up platform with one entity.""" config = { - ip.DOMAIN: { + IP_DOMAIN: { "platform": "microsoft_face_detect", "source": {"entity_id": "camera.demo_camera"}, "attributes": ["age", "gender"], }, "camera": {"platform": "demo"}, - mf.DOMAIN: {"api_key": "12345678abcdef6"}, + MF_DOMAIN: {"api_key": "12345678abcdef6"}, } - with assert_setup_component(1, ip.DOMAIN): - await async_setup_component(hass, ip.DOMAIN, config) + with assert_setup_component(1, IP_DOMAIN): + await async_setup_component(hass, IP_DOMAIN, config) await hass.async_block_till_done() assert hass.states.get("image_processing.microsoftface_demo_camera") @@ -76,16 +76,16 @@ async def test_setup_platform(hass: HomeAssistant, store_mock) -> None: async def test_setup_platform_name(hass: HomeAssistant, store_mock) -> None: """Set up platform with one entity and set name.""" config = { - ip.DOMAIN: { + IP_DOMAIN: { "platform": "microsoft_face_detect", "source": {"entity_id": "camera.demo_camera", "name": "test local"}, }, "camera": {"platform": "demo"}, - mf.DOMAIN: {"api_key": "12345678abcdef6"}, + MF_DOMAIN: {"api_key": "12345678abcdef6"}, } - with assert_setup_component(1, ip.DOMAIN): - await async_setup_component(hass, ip.DOMAIN, config) + with assert_setup_component(1, IP_DOMAIN): + await async_setup_component(hass, IP_DOMAIN, config) await hass.async_block_till_done() assert hass.states.get("image_processing.test_local") @@ -108,7 +108,7 @@ async def test_ms_detect_process_image( text=load_fixture("persons.json", "microsoft_face_detect"), ) - await async_setup_component(hass, ip.DOMAIN, CONFIG) + await async_setup_component(hass, IP_DOMAIN, CONFIG) await hass.async_block_till_done() state = hass.states.get("camera.demo_camera") diff --git a/tests/components/microsoft_face_identify/test_image_processing.py b/tests/components/microsoft_face_identify/test_image_processing.py index 6258448dd05..1f162e0eb9b 100644 --- a/tests/components/microsoft_face_identify/test_image_processing.py +++ b/tests/components/microsoft_face_identify/test_image_processing.py @@ -4,8 +4,8 @@ from unittest.mock import PropertyMock, patch import pytest -import homeassistant.components.image_processing as ip -import homeassistant.components.microsoft_face as mf +from homeassistant.components.image_processing import DOMAIN as IP_DOMAIN +from homeassistant.components.microsoft_face import DOMAIN as MF_DOMAIN, FACE_API_URL from homeassistant.const import ATTR_ENTITY_PICTURE, STATE_UNKNOWN from homeassistant.core import HomeAssistant, callback from homeassistant.setup import async_setup_component @@ -43,32 +43,32 @@ def poll_mock(): CONFIG = { - ip.DOMAIN: { + IP_DOMAIN: { "platform": "microsoft_face_identify", "source": {"entity_id": "camera.demo_camera", "name": "test local"}, "group": "Test Group1", }, "camera": {"platform": "demo"}, - mf.DOMAIN: {"api_key": "12345678abcdef6"}, + MF_DOMAIN: {"api_key": "12345678abcdef6"}, } -ENDPOINT_URL = f"https://westus.{mf.FACE_API_URL}" +ENDPOINT_URL = f"https://westus.{FACE_API_URL}" async def test_setup_platform(hass: HomeAssistant, store_mock) -> None: """Set up platform with one entity.""" config = { - ip.DOMAIN: { + IP_DOMAIN: { "platform": "microsoft_face_identify", "source": {"entity_id": "camera.demo_camera"}, "group": "Test Group1", }, "camera": {"platform": "demo"}, - mf.DOMAIN: {"api_key": "12345678abcdef6"}, + MF_DOMAIN: {"api_key": "12345678abcdef6"}, } - with assert_setup_component(1, ip.DOMAIN): - await async_setup_component(hass, ip.DOMAIN, config) + with assert_setup_component(1, IP_DOMAIN): + await async_setup_component(hass, IP_DOMAIN, config) await hass.async_block_till_done() assert hass.states.get("image_processing.microsoftface_demo_camera") @@ -77,17 +77,17 @@ async def test_setup_platform(hass: HomeAssistant, store_mock) -> None: async def test_setup_platform_name(hass: HomeAssistant, store_mock) -> None: """Set up platform with one entity and set name.""" config = { - ip.DOMAIN: { + IP_DOMAIN: { "platform": "microsoft_face_identify", "source": {"entity_id": "camera.demo_camera", "name": "test local"}, "group": "Test Group1", }, "camera": {"platform": "demo"}, - mf.DOMAIN: {"api_key": "12345678abcdef6"}, + MF_DOMAIN: {"api_key": "12345678abcdef6"}, } - with assert_setup_component(1, ip.DOMAIN): - await async_setup_component(hass, ip.DOMAIN, config) + with assert_setup_component(1, IP_DOMAIN): + await async_setup_component(hass, IP_DOMAIN, config) await hass.async_block_till_done() assert hass.states.get("image_processing.test_local") @@ -110,7 +110,7 @@ async def test_ms_identify_process_image( text=load_fixture("persons.json", "microsoft_face_identify"), ) - await async_setup_component(hass, ip.DOMAIN, CONFIG) + await async_setup_component(hass, IP_DOMAIN, CONFIG) await hass.async_block_till_done() state = hass.states.get("camera.demo_camera") diff --git a/tests/components/notify/test_persistent_notification.py b/tests/components/notify/test_persistent_notification.py index bbf571b69ae..d46b97e5bc2 100644 --- a/tests/components/notify/test_persistent_notification.py +++ b/tests/components/notify/test_persistent_notification.py @@ -1,7 +1,7 @@ """The tests for the notify.persistent_notification service.""" from homeassistant.components import notify -import homeassistant.components.persistent_notification as pn +from homeassistant.components.persistent_notification import DOMAIN as PN_DOMAIN from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component @@ -10,7 +10,7 @@ from tests.common import async_get_persistent_notifications async def test_async_send_message(hass: HomeAssistant) -> None: """Test sending a message to notify.persistent_notification service.""" - await async_setup_component(hass, pn.DOMAIN, {"core": {}}) + await async_setup_component(hass, PN_DOMAIN, {"core": {}}) await async_setup_component(hass, notify.DOMAIN, {}) await hass.async_block_till_done() @@ -30,7 +30,7 @@ async def test_async_send_message(hass: HomeAssistant) -> None: async def test_async_supports_notification_id(hass: HomeAssistant) -> None: """Test that notify.persistent_notification supports notification_id.""" - await async_setup_component(hass, pn.DOMAIN, {"core": {}}) + await async_setup_component(hass, PN_DOMAIN, {"core": {}}) await async_setup_component(hass, notify.DOMAIN, {}) await hass.async_block_till_done() diff --git a/tests/components/sighthound/test_image_processing.py b/tests/components/sighthound/test_image_processing.py index 09d6c2a1ca8..5db6347a832 100644 --- a/tests/components/sighthound/test_image_processing.py +++ b/tests/components/sighthound/test_image_processing.py @@ -10,19 +10,24 @@ from PIL import UnidentifiedImageError import pytest import simplehound.core as hound -import homeassistant.components.image_processing as ip +from homeassistant.components.image_processing import DOMAIN as IP_DOMAIN, SERVICE_SCAN import homeassistant.components.sighthound.image_processing as sh -from homeassistant.const import ATTR_ENTITY_ID, CONF_API_KEY +from homeassistant.const import ( + ATTR_ENTITY_ID, + CONF_API_KEY, + CONF_ENTITY_ID, + CONF_SOURCE, +) from homeassistant.core import HomeAssistant, callback from homeassistant.setup import async_setup_component TEST_DIR = os.path.dirname(__file__) VALID_CONFIG = { - ip.DOMAIN: { + IP_DOMAIN: { "platform": "sighthound", CONF_API_KEY: "abc123", - ip.CONF_SOURCE: {ip.CONF_ENTITY_ID: "camera.demo_camera"}, + CONF_SOURCE: {CONF_ENTITY_ID: "camera.demo_camera"}, }, "camera": {"platform": "demo"}, } @@ -96,7 +101,7 @@ async def test_bad_api_key( with mock.patch( "simplehound.core.cloud.detect", side_effect=hound.SimplehoundException ): - await async_setup_component(hass, ip.DOMAIN, VALID_CONFIG) + await async_setup_component(hass, IP_DOMAIN, VALID_CONFIG) await hass.async_block_till_done() assert "Sighthound error" in caplog.text assert not hass.states.get(VALID_ENTITY_ID) @@ -104,14 +109,14 @@ async def test_bad_api_key( async def test_setup_platform(hass: HomeAssistant, mock_detections) -> None: """Set up platform with one entity.""" - await async_setup_component(hass, ip.DOMAIN, VALID_CONFIG) + await async_setup_component(hass, IP_DOMAIN, VALID_CONFIG) await hass.async_block_till_done() assert hass.states.get(VALID_ENTITY_ID) async def test_process_image(hass: HomeAssistant, mock_image, mock_detections) -> None: """Process an image.""" - await async_setup_component(hass, ip.DOMAIN, VALID_CONFIG) + await async_setup_component(hass, IP_DOMAIN, VALID_CONFIG) await hass.async_block_till_done() assert hass.states.get(VALID_ENTITY_ID) @@ -125,7 +130,7 @@ async def test_process_image(hass: HomeAssistant, mock_image, mock_detections) - hass.bus.async_listen(sh.EVENT_PERSON_DETECTED, capture_person_event) data = {ATTR_ENTITY_ID: VALID_ENTITY_ID} - await hass.services.async_call(ip.DOMAIN, ip.SERVICE_SCAN, service_data=data) + await hass.services.async_call(IP_DOMAIN, SERVICE_SCAN, service_data=data) await hass.async_block_till_done() state = hass.states.get(VALID_ENTITY_ID) @@ -142,13 +147,13 @@ async def test_catch_bad_image( ) -> None: """Process an image.""" valid_config_save_file = deepcopy(VALID_CONFIG) - valid_config_save_file[ip.DOMAIN].update({sh.CONF_SAVE_FILE_FOLDER: TEST_DIR}) - await async_setup_component(hass, ip.DOMAIN, valid_config_save_file) + valid_config_save_file[IP_DOMAIN].update({sh.CONF_SAVE_FILE_FOLDER: TEST_DIR}) + await async_setup_component(hass, IP_DOMAIN, valid_config_save_file) await hass.async_block_till_done() assert hass.states.get(VALID_ENTITY_ID) data = {ATTR_ENTITY_ID: VALID_ENTITY_ID} - await hass.services.async_call(ip.DOMAIN, ip.SERVICE_SCAN, service_data=data) + await hass.services.async_call(IP_DOMAIN, SERVICE_SCAN, service_data=data) await hass.async_block_till_done() assert "Sighthound unable to process image" in caplog.text @@ -156,8 +161,8 @@ async def test_catch_bad_image( async def test_save_image(hass: HomeAssistant, mock_image, mock_detections) -> None: """Save a processed image.""" valid_config_save_file = deepcopy(VALID_CONFIG) - valid_config_save_file[ip.DOMAIN].update({sh.CONF_SAVE_FILE_FOLDER: TEST_DIR}) - await async_setup_component(hass, ip.DOMAIN, valid_config_save_file) + valid_config_save_file[IP_DOMAIN].update({sh.CONF_SAVE_FILE_FOLDER: TEST_DIR}) + await async_setup_component(hass, IP_DOMAIN, valid_config_save_file) await hass.async_block_till_done() assert hass.states.get(VALID_ENTITY_ID) @@ -167,7 +172,7 @@ async def test_save_image(hass: HomeAssistant, mock_image, mock_detections) -> N pil_img = pil_img_open.return_value pil_img = pil_img.convert.return_value data = {ATTR_ENTITY_ID: VALID_ENTITY_ID} - await hass.services.async_call(ip.DOMAIN, ip.SERVICE_SCAN, service_data=data) + await hass.services.async_call(IP_DOMAIN, SERVICE_SCAN, service_data=data) await hass.async_block_till_done() state = hass.states.get(VALID_ENTITY_ID) assert state.state == "2" @@ -183,9 +188,9 @@ async def test_save_timestamped_image( ) -> None: """Save a processed image.""" valid_config_save_ts_file = deepcopy(VALID_CONFIG) - valid_config_save_ts_file[ip.DOMAIN].update({sh.CONF_SAVE_FILE_FOLDER: TEST_DIR}) - valid_config_save_ts_file[ip.DOMAIN].update({sh.CONF_SAVE_TIMESTAMPTED_FILE: True}) - await async_setup_component(hass, ip.DOMAIN, valid_config_save_ts_file) + valid_config_save_ts_file[IP_DOMAIN].update({sh.CONF_SAVE_FILE_FOLDER: TEST_DIR}) + valid_config_save_ts_file[IP_DOMAIN].update({sh.CONF_SAVE_TIMESTAMPTED_FILE: True}) + await async_setup_component(hass, IP_DOMAIN, valid_config_save_ts_file) await hass.async_block_till_done() assert hass.states.get(VALID_ENTITY_ID) @@ -195,7 +200,7 @@ async def test_save_timestamped_image( pil_img = pil_img_open.return_value pil_img = pil_img.convert.return_value data = {ATTR_ENTITY_ID: VALID_ENTITY_ID} - await hass.services.async_call(ip.DOMAIN, ip.SERVICE_SCAN, service_data=data) + await hass.services.async_call(IP_DOMAIN, SERVICE_SCAN, service_data=data) await hass.async_block_till_done() state = hass.states.get(VALID_ENTITY_ID) assert state.state == "2" diff --git a/tests/components/yamaha/test_media_player.py b/tests/components/yamaha/test_media_player.py index 73885bc8ac7..02246e69269 100644 --- a/tests/components/yamaha/test_media_player.py +++ b/tests/components/yamaha/test_media_player.py @@ -4,7 +4,7 @@ from unittest.mock import MagicMock, PropertyMock, call, patch import pytest -import homeassistant.components.media_player as mp +from homeassistant.components.media_player import DOMAIN as MP_DOMAIN from homeassistant.components.yamaha import media_player as yamaha from homeassistant.components.yamaha.const import DOMAIN from homeassistant.core import HomeAssistant @@ -52,7 +52,7 @@ def device_fixture(main_zone): async def test_setup_host(hass: HomeAssistant, device, main_zone) -> None: """Test set up integration with host.""" - assert await async_setup_component(hass, mp.DOMAIN, CONFIG) + assert await async_setup_component(hass, MP_DOMAIN, CONFIG) await hass.async_block_till_done() state = hass.states.get("media_player.yamaha_receiver_main_zone") @@ -65,7 +65,7 @@ async def test_setup_no_host(hass: HomeAssistant, device, main_zone) -> None: """Test set up integration without host.""" with patch("rxv.find", return_value=[device]): assert await async_setup_component( - hass, mp.DOMAIN, {"media_player": {"platform": "yamaha"}} + hass, MP_DOMAIN, {"media_player": {"platform": "yamaha"}} ) await hass.async_block_till_done() @@ -84,7 +84,7 @@ async def test_setup_discovery(hass: HomeAssistant, device, main_zone) -> None: "description_url": "http://receiver/description", } await async_load_platform( - hass, mp.DOMAIN, "yamaha", discovery_info, {mp.DOMAIN: {}} + hass, MP_DOMAIN, "yamaha", discovery_info, {MP_DOMAIN: {}} ) await hass.async_block_till_done() @@ -98,7 +98,7 @@ async def test_setup_zone_ignore(hass: HomeAssistant, device, main_zone) -> None """Test set up integration without host.""" assert await async_setup_component( hass, - mp.DOMAIN, + MP_DOMAIN, { "media_player": { "platform": "yamaha", @@ -116,7 +116,7 @@ async def test_setup_zone_ignore(hass: HomeAssistant, device, main_zone) -> None async def test_enable_output(hass: HomeAssistant, device, main_zone) -> None: """Test enable output service.""" - assert await async_setup_component(hass, mp.DOMAIN, CONFIG) + assert await async_setup_component(hass, MP_DOMAIN, CONFIG) await hass.async_block_till_done() port = "hdmi1" @@ -147,7 +147,7 @@ async def test_enable_output(hass: HomeAssistant, device, main_zone) -> None: @pytest.mark.usefixtures("device") async def test_menu_cursor(hass: HomeAssistant, main_zone, cursor, method) -> None: """Verify that the correct menu method is called for the menu_cursor service.""" - assert await async_setup_component(hass, mp.DOMAIN, CONFIG) + assert await async_setup_component(hass, MP_DOMAIN, CONFIG) await hass.async_block_till_done() data = { @@ -166,7 +166,7 @@ async def test_select_scene( scene_prop = PropertyMock(return_value=None) type(main_zone).scene = scene_prop - assert await async_setup_component(hass, mp.DOMAIN, CONFIG) + assert await async_setup_component(hass, MP_DOMAIN, CONFIG) await hass.async_block_till_done() scene = "TV Viewing" From cbfb587f2d55cb10b2d6b3e1dad7a512bd1e484d Mon Sep 17 00:00:00 2001 From: Pete Sage <76050312+PeteRager@users.noreply.github.com> Date: Sat, 22 Jun 2024 03:08:12 -0400 Subject: [PATCH 0969/1445] Sonos add tests for media_player.play_media favorite_item_id (#120120) --- tests/components/sonos/test_media_player.py | 42 +++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/tests/components/sonos/test_media_player.py b/tests/components/sonos/test_media_player.py index 2be9aa5f823..b84dd419578 100644 --- a/tests/components/sonos/test_media_player.py +++ b/tests/components/sonos/test_media_player.py @@ -427,3 +427,45 @@ async def test_select_source_error( ) assert "invalid_source" in str(sve.value) assert "Could not find a Sonos favorite" in str(sve.value) + + +async def test_play_media_favorite_item_id( + hass: HomeAssistant, + soco_factory: SoCoMockFactory, + async_autosetup_sonos, +) -> None: + """Test playing media with a favorite item id.""" + soco_mock = soco_factory.mock_list.get("192.168.42.2") + await hass.services.async_call( + MP_DOMAIN, + SERVICE_PLAY_MEDIA, + { + "entity_id": "media_player.zone_a", + "media_content_type": "favorite_item_id", + "media_content_id": "FV:2/4", + }, + blocking=True, + ) + assert soco_mock.play_uri.call_count == 1 + assert ( + soco_mock.play_uri.call_args_list[0].args[0] + == "x-sonosapi-hls:Api%3atune%3aliveAudio%3ajazzcafe%3aetc" + ) + assert ( + soco_mock.play_uri.call_args_list[0].kwargs["timeout"] == LONG_SERVICE_TIMEOUT + ) + assert soco_mock.play_uri.call_args_list[0].kwargs["title"] == "66 - Watercolors" + + # Test exception handling with an invalid id. + with pytest.raises(ValueError) as sve: + await hass.services.async_call( + MP_DOMAIN, + SERVICE_PLAY_MEDIA, + { + "entity_id": "media_player.zone_a", + "media_content_type": "favorite_item_id", + "media_content_id": "UNKNOWN_ID", + }, + blocking=True, + ) + assert "UNKNOWN_ID" in str(sve.value) From 88039597e56260f5dde24a9aeab4608d3c8433d9 Mon Sep 17 00:00:00 2001 From: Pete Sage <76050312+PeteRager@users.noreply.github.com> Date: Sat, 22 Jun 2024 03:09:38 -0400 Subject: [PATCH 0970/1445] Sonos add tests for media_player.play_media library track (#120119) --- tests/components/sonos/conftest.py | 1 + tests/components/sonos/test_media_player.py | 110 ++++++++++++++++++++ 2 files changed, 111 insertions(+) diff --git a/tests/components/sonos/conftest.py b/tests/components/sonos/conftest.py index c7f5cfb7223..378989c58fa 100644 --- a/tests/components/sonos/conftest.py +++ b/tests/components/sonos/conftest.py @@ -205,6 +205,7 @@ class SoCoMockFactory: my_speaker_info["uid"] = mock_soco.uid mock_soco.get_speaker_info = Mock(return_value=my_speaker_info) mock_soco.add_to_queue = Mock(return_value=10) + mock_soco.add_uri_to_queue = Mock(return_value=10) mock_soco.avTransport = SonosMockService("AVTransport", ip_address) mock_soco.renderingControl = SonosMockService("RenderingControl", ip_address) diff --git a/tests/components/sonos/test_media_player.py b/tests/components/sonos/test_media_player.py index b84dd419578..a975538cdec 100644 --- a/tests/components/sonos/test_media_player.py +++ b/tests/components/sonos/test_media_player.py @@ -192,6 +192,116 @@ async def test_play_media_library( ) +_track_url = "S://192.168.42.100/music/iTunes/The%20Beatles/A%20Hard%20Day%2fs%I%20Should%20Have%20Known%20Better.mp3" + + +async def test_play_media_lib_track_play( + hass: HomeAssistant, + soco_factory: SoCoMockFactory, + async_autosetup_sonos, +) -> None: + """Tests playing media track with enqueue mode play.""" + soco_mock = soco_factory.mock_list.get("192.168.42.2") + await hass.services.async_call( + MP_DOMAIN, + SERVICE_PLAY_MEDIA, + { + "entity_id": "media_player.zone_a", + "media_content_type": "track", + "media_content_id": _track_url, + ATTR_MEDIA_ENQUEUE: MediaPlayerEnqueue.PLAY, + }, + blocking=True, + ) + assert soco_mock.add_uri_to_queue.call_count == 1 + assert soco_mock.add_uri_to_queue.call_args_list[0].args[0] == _track_url + assert soco_mock.add_uri_to_queue.call_args_list[0].kwargs["position"] == 1 + assert ( + soco_mock.add_uri_to_queue.call_args_list[0].kwargs["timeout"] + == LONG_SERVICE_TIMEOUT + ) + assert soco_mock.play_from_queue.call_count == 1 + assert soco_mock.play_from_queue.call_args_list[0].args[0] == 9 + + +async def test_play_media_lib_track_next( + hass: HomeAssistant, + soco_factory: SoCoMockFactory, + async_autosetup_sonos, +) -> None: + """Tests playing media track with enqueue mode next.""" + soco_mock = soco_factory.mock_list.get("192.168.42.2") + await hass.services.async_call( + MP_DOMAIN, + SERVICE_PLAY_MEDIA, + { + "entity_id": "media_player.zone_a", + "media_content_type": "track", + "media_content_id": _track_url, + ATTR_MEDIA_ENQUEUE: MediaPlayerEnqueue.NEXT, + }, + blocking=True, + ) + assert soco_mock.add_uri_to_queue.call_count == 1 + assert soco_mock.add_uri_to_queue.call_args_list[0].args[0] == _track_url + assert soco_mock.add_uri_to_queue.call_args_list[0].kwargs["position"] == 1 + assert ( + soco_mock.add_uri_to_queue.call_args_list[0].kwargs["timeout"] + == LONG_SERVICE_TIMEOUT + ) + assert soco_mock.play_from_queue.call_count == 0 + + +async def test_play_media_lib_track_replace( + hass: HomeAssistant, + soco_factory: SoCoMockFactory, + async_autosetup_sonos, +) -> None: + """Tests playing media track with enqueue mode replace.""" + soco_mock = soco_factory.mock_list.get("192.168.42.2") + await hass.services.async_call( + MP_DOMAIN, + SERVICE_PLAY_MEDIA, + { + "entity_id": "media_player.zone_a", + "media_content_type": "track", + "media_content_id": _track_url, + ATTR_MEDIA_ENQUEUE: MediaPlayerEnqueue.REPLACE, + }, + blocking=True, + ) + assert soco_mock.play_uri.call_count == 1 + assert soco_mock.play_uri.call_args_list[0].args[0] == _track_url + assert soco_mock.play_uri.call_args_list[0].kwargs["force_radio"] is False + + +async def test_play_media_lib_track_add( + hass: HomeAssistant, + soco_factory: SoCoMockFactory, + async_autosetup_sonos, +) -> None: + """Tests playing media track with enqueue mode add.""" + soco_mock = soco_factory.mock_list.get("192.168.42.2") + await hass.services.async_call( + MP_DOMAIN, + SERVICE_PLAY_MEDIA, + { + "entity_id": "media_player.zone_a", + "media_content_type": "track", + "media_content_id": _track_url, + ATTR_MEDIA_ENQUEUE: MediaPlayerEnqueue.ADD, + }, + blocking=True, + ) + assert soco_mock.add_uri_to_queue.call_count == 1 + assert soco_mock.add_uri_to_queue.call_args_list[0].args[0] == _track_url + assert ( + soco_mock.add_uri_to_queue.call_args_list[0].kwargs["timeout"] + == LONG_SERVICE_TIMEOUT + ) + assert soco_mock.play_from_queue.call_count == 0 + + _mock_playlists = [ MockMusicServiceItem( "playlist1", From 32a94fc1140be37cfff659cb8dfb18b2fcd16950 Mon Sep 17 00:00:00 2001 From: Lenn <78048721+LennP@users.noreply.github.com> Date: Sat, 22 Jun 2024 09:13:14 +0200 Subject: [PATCH 0971/1445] Motionblinds Bluetooth options (#120110) --- .../components/motionblinds_ble/__init__.py | 35 ++++++++++- .../motionblinds_ble/config_flow.py | 59 ++++++++++++++++++- .../components/motionblinds_ble/const.py | 3 + .../components/motionblinds_ble/strings.json | 12 ++++ .../motionblinds_ble/test_config_flow.py | 32 ++++++++++ 5 files changed, 138 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/motionblinds_ble/__init__.py b/homeassistant/components/motionblinds_ble/__init__.py index 3c6df12e878..1b664eeede3 100644 --- a/homeassistant/components/motionblinds_ble/__init__.py +++ b/homeassistant/components/motionblinds_ble/__init__.py @@ -24,7 +24,13 @@ from homeassistant.helpers import config_validation as cv from homeassistant.helpers.event import async_call_later from homeassistant.helpers.typing import ConfigType -from .const import CONF_BLIND_TYPE, CONF_MAC_CODE, DOMAIN +from .const import ( + CONF_BLIND_TYPE, + CONF_MAC_CODE, + DOMAIN, + OPTION_DISCONNECT_TIME, + OPTION_PERMANENT_CONNECTION, +) _LOGGER = logging.getLogger(__name__) @@ -86,13 +92,40 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {})[entry.entry_id] = device + # Register OptionsFlow update listener + entry.async_on_unload(entry.add_update_listener(options_update_listener)) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + # Apply options + entry.async_create_background_task( + hass, apply_options(hass, entry), device.ble_device.address + ) + _LOGGER.debug("(%s) Finished setting up device", entry.data[CONF_MAC_CODE]) return True +async def options_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Handle options update.""" + _LOGGER.debug( + "(%s) Updated device options: %s", entry.data[CONF_MAC_CODE], entry.options + ) + await apply_options(hass, entry) + + +async def apply_options(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Apply the options from the OptionsFlow.""" + + device: MotionDevice = hass.data[DOMAIN][entry.entry_id] + disconnect_time: float | None = entry.options.get(OPTION_DISCONNECT_TIME, None) + permanent_connection: bool = entry.options.get(OPTION_PERMANENT_CONNECTION, False) + + device.set_custom_disconnect_time(disconnect_time) + await device.set_permanent_connection(permanent_connection) + + async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload Motionblinds Bluetooth device from a config entry.""" diff --git a/homeassistant/components/motionblinds_ble/config_flow.py b/homeassistant/components/motionblinds_ble/config_flow.py index 23302ae9624..b8e03386844 100644 --- a/homeassistant/components/motionblinds_ble/config_flow.py +++ b/homeassistant/components/motionblinds_ble/config_flow.py @@ -7,13 +7,19 @@ import re from typing import TYPE_CHECKING, Any from bleak.backends.device import BLEDevice -from motionblindsble.const import DISPLAY_NAME, MotionBlindType +from motionblindsble.const import DISPLAY_NAME, SETTING_DISCONNECT_TIME, MotionBlindType import voluptuous as vol from homeassistant.components import bluetooth from homeassistant.components.bluetooth import BluetoothServiceInfoBleak -from homeassistant.config_entries import ConfigFlow, ConfigFlowResult +from homeassistant.config_entries import ( + ConfigEntry, + ConfigFlow, + ConfigFlowResult, + OptionsFlow, +) from homeassistant.const import CONF_ADDRESS +from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.selector import ( SelectSelector, @@ -30,6 +36,8 @@ from .const import ( ERROR_INVALID_MAC_CODE, ERROR_NO_BLUETOOTH_ADAPTER, ERROR_NO_DEVICES_FOUND, + OPTION_DISCONNECT_TIME, + OPTION_PERMANENT_CONNECTION, ) _LOGGER = logging.getLogger(__name__) @@ -174,6 +182,53 @@ class FlowHandler(ConfigFlow, domain=DOMAIN): self._mac_code = mac_code.upper() self._display_name = DISPLAY_NAME.format(mac_code=self._mac_code) + @staticmethod + @callback + def async_get_options_flow( + config_entry: ConfigEntry, + ) -> OptionsFlow: + """Create the options flow.""" + return OptionsFlowHandler(config_entry) + + +class OptionsFlowHandler(OptionsFlow): + """Handle an options flow for Motionblinds BLE.""" + + def __init__(self, config_entry: ConfigEntry) -> None: + """Initialize options flow.""" + self.config_entry = config_entry + + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Manage the options.""" + if user_input is not None: + return self.async_create_entry(title="", data=user_input) + + return self.async_show_form( + step_id="init", + data_schema=vol.Schema( + { + vol.Required( + OPTION_PERMANENT_CONNECTION, + default=( + self.config_entry.options.get( + OPTION_PERMANENT_CONNECTION, False + ) + ), + ): bool, + vol.Optional( + OPTION_DISCONNECT_TIME, + default=( + self.config_entry.options.get( + OPTION_DISCONNECT_TIME, SETTING_DISCONNECT_TIME + ) + ), + ): vol.All(vol.Coerce(int), vol.Range(min=0)), + } + ), + ) + def is_valid_mac(data: str) -> bool: """Validate the provided MAC address.""" diff --git a/homeassistant/components/motionblinds_ble/const.py b/homeassistant/components/motionblinds_ble/const.py index bd88927559e..0b4a2a7f947 100644 --- a/homeassistant/components/motionblinds_ble/const.py +++ b/homeassistant/components/motionblinds_ble/const.py @@ -19,3 +19,6 @@ ERROR_NO_DEVICES_FOUND = "no_devices_found" ICON_VERTICAL_BLIND = "mdi:blinds-vertical-closed" MANUFACTURER = "Motionblinds" + +OPTION_DISCONNECT_TIME = "disconnect_time" +OPTION_PERMANENT_CONNECTION = "permanent_connection" diff --git a/homeassistant/components/motionblinds_ble/strings.json b/homeassistant/components/motionblinds_ble/strings.json index 0bc9ad4c012..ab26f26ce44 100644 --- a/homeassistant/components/motionblinds_ble/strings.json +++ b/homeassistant/components/motionblinds_ble/strings.json @@ -20,6 +20,18 @@ } } }, + "options": { + "step": { + "init": { + "title": "Connection options", + "description": "The default disconnect time is 15 seconds, adjustable using the slider below. You may want to adjust this if you have larger blinds or other specific needs. You can also enable a permanent connection to the motor, which disables the disconnect time and automatically reconnects when the motor is disconnected for any reason.\n**WARNING**: Changing any of the below options may significantly reduce battery life of your motor!", + "data": { + "permanent_connection": "Permanent connection", + "disconnect_time": "Disconnect time (seconds)" + } + } + } + }, "selector": { "blind_type": { "options": { diff --git a/tests/components/motionblinds_ble/test_config_flow.py b/tests/components/motionblinds_ble/test_config_flow.py index 90d2cbdcbc6..4cab12269dd 100644 --- a/tests/components/motionblinds_ble/test_config_flow.py +++ b/tests/components/motionblinds_ble/test_config_flow.py @@ -14,6 +14,7 @@ from homeassistant.data_entry_flow import FlowResultType from .conftest import TEST_ADDRESS, TEST_MAC, TEST_NAME +from tests.common import MockConfigEntry from tests.components.bluetooth import generate_advertisement_data, generate_ble_device TEST_BLIND_TYPE = MotionBlindType.ROLLER.name.lower() @@ -255,3 +256,34 @@ async def test_config_flow_bluetooth_success(hass: HomeAssistant) -> None: const.CONF_BLIND_TYPE: TEST_BLIND_TYPE, } assert result["options"] == {} + + +async def test_options_flow(hass: HomeAssistant) -> None: + """Test the options flow.""" + entry = MockConfigEntry( + domain=const.DOMAIN, + unique_id="0123456789", + data={ + const.CONF_BLIND_TYPE: MotionBlindType.ROLLER, + }, + ) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + + await hass.async_block_till_done() + + result = await hass.config_entries.options.async_init(entry.entry_id) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "init" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + const.OPTION_PERMANENT_CONNECTION: True, + const.OPTION_DISCONNECT_TIME: 10, + }, + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY From f3d2ba7d8d0f23d79deccb2bcf49ba3438ad9d6b Mon Sep 17 00:00:00 2001 From: Charles Garwood Date: Sat, 22 Jun 2024 03:27:17 -0400 Subject: [PATCH 0972/1445] Add additional checks for Enpower supported feature (#117107) --- homeassistant/components/enphase_envoy/number.py | 1 + homeassistant/components/enphase_envoy/select.py | 1 + 2 files changed, 2 insertions(+) diff --git a/homeassistant/components/enphase_envoy/number.py b/homeassistant/components/enphase_envoy/number.py index 38bb18ad768..63c5879cfe8 100644 --- a/homeassistant/components/enphase_envoy/number.py +++ b/homeassistant/components/enphase_envoy/number.py @@ -89,6 +89,7 @@ async def async_setup_entry( envoy_data.tariff and envoy_data.tariff.storage_settings and coordinator.envoy.supported_features & SupportedFeatures.ENCHARGE + and coordinator.envoy.supported_features & SupportedFeatures.ENPOWER ): entities.append( EnvoyStorageSettingsNumberEntity(coordinator, STORAGE_RESERVE_SOC_ENTITY) diff --git a/homeassistant/components/enphase_envoy/select.py b/homeassistant/components/enphase_envoy/select.py index 98374d16394..0971c7b5715 100644 --- a/homeassistant/components/enphase_envoy/select.py +++ b/homeassistant/components/enphase_envoy/select.py @@ -144,6 +144,7 @@ async def async_setup_entry( envoy_data.tariff and envoy_data.tariff.storage_settings and coordinator.envoy.supported_features & SupportedFeatures.ENCHARGE + and coordinator.envoy.supported_features & SupportedFeatures.ENPOWER ): entities.append( EnvoyStorageSettingsSelectEntity(coordinator, STORAGE_MODE_ENTITY) From 0a30032b9652e85a68222c1481d7e1075b194001 Mon Sep 17 00:00:00 2001 From: wittypluck Date: Sat, 22 Jun 2024 10:31:20 +0200 Subject: [PATCH 0973/1445] Enable statistics for UniFi remaining power sensors (#120073) Unifi: Add StateClass Measurement to all power sensors --- homeassistant/components/unifi/sensor.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/unifi/sensor.py b/homeassistant/components/unifi/sensor.py index ba1da7ea6c8..028d70d8880 100644 --- a/homeassistant/components/unifi/sensor.py +++ b/homeassistant/components/unifi/sensor.py @@ -341,6 +341,7 @@ ENTITY_DESCRIPTIONS: tuple[UnifiSensorEntityDescription, ...] = ( key="Outlet power metering", device_class=SensorDeviceClass.POWER, entity_category=EntityCategory.DIAGNOSTIC, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfPower.WATT, api_handler_fn=lambda api: api.outlets, available_fn=async_device_available_fn, @@ -356,6 +357,7 @@ ENTITY_DESCRIPTIONS: tuple[UnifiSensorEntityDescription, ...] = ( key="SmartPower AC power budget", device_class=SensorDeviceClass.POWER, entity_category=EntityCategory.DIAGNOSTIC, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfPower.WATT, suggested_display_precision=1, api_handler_fn=lambda api: api.devices, @@ -371,6 +373,7 @@ ENTITY_DESCRIPTIONS: tuple[UnifiSensorEntityDescription, ...] = ( key="SmartPower AC power consumption", device_class=SensorDeviceClass.POWER, entity_category=EntityCategory.DIAGNOSTIC, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfPower.WATT, suggested_display_precision=1, api_handler_fn=lambda api: api.devices, From 7efd5479623de8ca38d5b1f11b437897b92502af Mon Sep 17 00:00:00 2001 From: IceBotYT <34712694+IceBotYT@users.noreply.github.com> Date: Sat, 22 Jun 2024 04:37:37 -0400 Subject: [PATCH 0974/1445] Fix peco integration (#117165) --- homeassistant/components/peco/__init__.py | 16 +++++++++------- homeassistant/components/peco/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 12 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/peco/__init__.py b/homeassistant/components/peco/__init__.py index 168b045ff4d..12979f27793 100644 --- a/homeassistant/components/peco/__init__.py +++ b/homeassistant/components/peco/__init__.py @@ -49,7 +49,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Outage Counter Setup county: str = entry.data[CONF_COUNTY] - async def async_update_outage_data() -> OutageResults: + async def async_update_outage_data() -> PECOCoordinatorData: """Fetch data from API.""" try: outages: OutageResults = ( @@ -65,7 +65,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: raise UpdateFailed(f"Error parsing data: {err}") from err return data - coordinator = DataUpdateCoordinator( + outage_coordinator = DataUpdateCoordinator( hass, LOGGER, name="PECO Outage Count", @@ -73,9 +73,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: update_interval=timedelta(minutes=OUTAGE_SCAN_INTERVAL), ) - await coordinator.async_config_entry_first_refresh() + await outage_coordinator.async_config_entry_first_refresh() - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = {"outage_count": coordinator} + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = { + "outage_count": outage_coordinator + } if phone_number := entry.data.get(CONF_PHONE_NUMBER): # Smart Meter Setup] @@ -92,7 +94,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: raise UpdateFailed(f"Error parsing data: {err}") from err return data - coordinator = DataUpdateCoordinator( + meter_coordinator = DataUpdateCoordinator( hass, LOGGER, name="PECO Smart Meter", @@ -100,9 +102,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: update_interval=timedelta(minutes=SMART_METER_SCAN_INTERVAL), ) - await coordinator.async_config_entry_first_refresh() + await meter_coordinator.async_config_entry_first_refresh() - hass.data[DOMAIN][entry.entry_id]["smart_meter"] = coordinator + hass.data[DOMAIN][entry.entry_id]["smart_meter"] = meter_coordinator await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/peco/manifest.json b/homeassistant/components/peco/manifest.json index dd0403d8041..698981e9361 100644 --- a/homeassistant/components/peco/manifest.json +++ b/homeassistant/components/peco/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/peco", "iot_class": "cloud_polling", - "requirements": ["peco==0.0.29"] + "requirements": ["peco==0.0.30"] } diff --git a/requirements_all.txt b/requirements_all.txt index 8f75ae00d32..5356fa75d9b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1543,7 +1543,7 @@ panasonic-viera==0.3.6 pdunehd==1.3.2 # homeassistant.components.peco -peco==0.0.29 +peco==0.0.30 # homeassistant.components.pencom pencompy==0.0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e635084616a..c33072d9b79 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1240,7 +1240,7 @@ panasonic-viera==0.3.6 pdunehd==1.3.2 # homeassistant.components.peco -peco==0.0.29 +peco==0.0.30 # homeassistant.components.escea pescea==1.0.12 From a76fa9f3bf2934ef084223a3401a1d0778d479ec Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 22 Jun 2024 10:45:18 +0200 Subject: [PATCH 0975/1445] Update pytest warnings filter (#120143) --- pyproject.toml | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 6578bcb5f36..9f83edd7f3e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -456,6 +456,8 @@ filterwarnings = [ # Ignore custom pytest marks "ignore:Unknown pytest.mark.disable_autouse_fixture:pytest.PytestUnknownMarkWarning:tests.components.met", "ignore:Unknown pytest.mark.dataset:pytest.PytestUnknownMarkWarning:tests.components.screenlogic", + # https://github.com/rokam/sunweg/blob/3.0.1/sunweg/plant.py#L96 - v3.0.1 - 2024-05-29 + "ignore:The '(kwh_per_kwp|performance_rate)' property is deprecated and will return 0:DeprecationWarning:tests.components.sunweg.test_init", # -- design choice 3rd party # https://github.com/gwww/elkm1/blob/2.2.7/elkm1_lib/util.py#L8-L19 @@ -471,7 +473,8 @@ filterwarnings = [ # -- Setuptools DeprecationWarnings # https://github.com/googleapis/google-cloud-python/issues/11184 # https://github.com/zopefoundation/meta/issues/194 - "ignore:Deprecated call to `pkg_resources.declare_namespace\\(('google.*'|'pywinusb'|'repoze'|'xbox'|'zope')\\)`:DeprecationWarning:pkg_resources", + # https://github.com/Azure/azure-sdk-for-python + "ignore:Deprecated call to `pkg_resources.declare_namespace\\(('azure'|'google.*'|'pywinusb'|'repoze'|'xbox'|'zope')\\)`:DeprecationWarning:pkg_resources", # -- tracked upstream / open PRs # https://github.com/certbot/certbot/issues/9828 - v2.10.0 @@ -486,8 +489,6 @@ filterwarnings = [ "ignore:'telnetlib' is deprecated and slated for removal in Python 3.13:DeprecationWarning:ndms2_client.connection", # -- fixed, waiting for release / update - # https://github.com/mkmer/AIOAladdinConnect/commit/8851fff4473d80d70ac518db2533f0fbef63b69c - >=0.2.0 - "ignore:module 'sre_constants' is deprecated:DeprecationWarning:AIOAladdinConnect", # https://github.com/bachya/aiopurpleair/pull/200 - >=2023.10.0 "ignore:datetime.*utcfromtimestamp\\(\\) is deprecated and scheduled for removal:DeprecationWarning:aiopurpleair.helpers.validators", # https://github.com/DataDog/datadogpy/pull/290 - >=0.23.0 @@ -507,25 +508,23 @@ filterwarnings = [ # https://github.com/eclipse/paho.mqtt.python/issues/653 - >=2.0.0 # https://github.com/eclipse/paho.mqtt.python/pull/665 "ignore:ssl.PROTOCOL_TLS is deprecated:DeprecationWarning:paho.mqtt.client", + # https://github.com/vacanza/python-holidays/discussions/1800 - >1.0.0 + "ignore::DeprecationWarning:holidays", # https://github.com/rytilahti/python-miio/pull/1809 - >=0.6.0.dev0 "ignore:datetime.*utcnow\\(\\) is deprecated and scheduled for removal:DeprecationWarning:miio.protocol", "ignore:datetime.*utcnow\\(\\) is deprecated and scheduled for removal:DeprecationWarning:miio.miioprotocol", # https://github.com/hunterjm/python-onvif-zeep-async/pull/51 - >3.1.12 "ignore:datetime.*utcnow\\(\\) is deprecated and scheduled for removal:DeprecationWarning:onvif.client", - # https://github.com/pkkid/python-plexapi/pull/1404 - >4.15.13 - "ignore:invalid escape sequence:SyntaxWarning:.*plexapi.base", # https://github.com/googleapis/python-pubsub/commit/060f00bcea5cd129be3a2d37078535cc97b4f5e8 - >=2.13.12 "ignore:pkg_resources is deprecated as an API:DeprecationWarning:google.pubsub_v1.services.publisher.client", # https://github.com/okunishinishi/python-stringcase/commit/6a5c5bbd3fe5337862abc7fd0853a0f36e18b2e1 - >1.2.0 "ignore:invalid escape sequence:SyntaxWarning:.*stringcase", + # https://github.com/mvantellingen/python-zeep/pull/1364 - >4.2.1 + "ignore:'cgi' is deprecated and slated for removal in Python 3.13:DeprecationWarning:zeep.utils", # https://github.com/timmo001/system-bridge-connector/pull/27 - >=4.1.0 "ignore:pkg_resources is deprecated as an API:DeprecationWarning:systembridgeconnector.version", # https://github.com/jschlyter/ttls/commit/d64f1251397b8238cf6a35bea64784de25e3386c - >=1.8.1 "ignore:pkg_resources is deprecated as an API:DeprecationWarning:ttls", - # https://github.com/mvantellingen/python-zeep/pull/1364 - >4.2.1 - "ignore:'cgi' is deprecated and slated for removal in Python 3.13:DeprecationWarning:zeep.utils", - # https://github.com/vacanza/python-holidays/discussions/1800 - "ignore::DeprecationWarning:holidays", # -- fixed for Python 3.13 # https://github.com/rhasspy/wyoming/commit/e34af30d455b6f2bb9e5cfb25fad8d276914bc54 - >=1.4.2 @@ -546,6 +545,10 @@ filterwarnings = [ "ignore:loop argument is deprecated:DeprecationWarning:emulated_roku", # https://github.com/thecynic/pylutron - v0.2.13 "ignore:setDaemon\\(\\) is deprecated, set the daemon attribute instead:DeprecationWarning:pylutron", + # https://github.com/pschmitt/pynuki/blob/1.6.3/pynuki/utils.py#L21 - v1.6.3 + "ignore:datetime.*utcnow\\(\\) is deprecated and scheduled for removal:DeprecationWarning:pynuki.utils", + # https://github.com/briis/pyweatherflowudp/blob/v1.4.5/pyweatherflowudp/const.py#L20 - v1.4.5 - 2023-10-10 + "ignore:This function will be removed in future versions of pint:DeprecationWarning:pyweatherflowudp.const", # Wrong stacklevel # https://bugs.launchpad.net/beautifulsoup/+bug/2034451 "ignore:It looks like you're parsing an XML document using an HTML parser:UserWarning:html.parser", @@ -554,6 +557,7 @@ filterwarnings = [ # - SyntaxWarnings # https://pypi.org/project/aprslib/ - v0.7.2 - 2022-07-10 "ignore:invalid escape sequence:SyntaxWarning:.*aprslib.parsing.common", + "ignore:datetime.*utcnow\\(\\) is deprecated and scheduled for removal:DeprecationWarning:aprslib.parsing.common", # https://pypi.org/project/pyblackbird/ - v0.6 - 2023-03-15 # https://github.com/koolsb/pyblackbird/pull/9 -> closed "ignore:invalid escape sequence:SyntaxWarning:.*pyblackbird", @@ -578,7 +582,7 @@ filterwarnings = [ "ignore:pkg_resources is deprecated as an API:DeprecationWarning:pybotvac.version", # https://github.com/home-assistant-ecosystem/python-mystrom/blob/2.2.0/pymystrom/__init__.py#L10 - v2.2.0 - 2023-05-21 "ignore:pkg_resources is deprecated as an API:DeprecationWarning:pymystrom", - # https://pypi.org/project/velbus-aio/ - v2024.4.1 + # https://pypi.org/project/velbus-aio/ - v2024.4.1 - 2024-04-07 # https://github.com/Cereal2nd/velbus-aio/blob/2024.4.1/velbusaio/handler.py#L12 "ignore:pkg_resources is deprecated as an API:DeprecationWarning:velbusaio.handler", From 03aba7e7abd5b992f8fcf1d9c551a66290a367ad Mon Sep 17 00:00:00 2001 From: Shai Ungar Date: Sat, 22 Jun 2024 11:46:31 +0300 Subject: [PATCH 0976/1445] Address late seventeentrack review (#116792) --- .../components/seventeentrack/__init__.py | 52 ++++++++++++++- .../components/seventeentrack/strings.json | 8 +++ .../seventeentrack/test_services.py | 65 ++++++++++++++++++- 3 files changed, 119 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/seventeentrack/__init__.py b/homeassistant/components/seventeentrack/__init__.py index 40c9c8d58d1..6d89c4c0a76 100644 --- a/homeassistant/components/seventeentrack/__init__.py +++ b/homeassistant/components/seventeentrack/__init__.py @@ -1,9 +1,13 @@ """The seventeentrack component.""" +from typing import Final + from py17track import Client as SeventeenTrackClient from py17track.errors import SeventeenTrackError +from py17track.package import PACKAGE_STATUS_MAP +import voluptuous as vol -from homeassistant.config_entries import ConfigEntry +from homeassistant.config_entries import ConfigEntry, ConfigEntryState from homeassistant.const import ( ATTR_FRIENDLY_NAME, ATTR_LOCATION, @@ -17,8 +21,8 @@ from homeassistant.core import ( ServiceResponse, SupportsResponse, ) -from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.helpers import config_validation as cv +from homeassistant.exceptions import ConfigEntryNotReady, ServiceValidationError +from homeassistant.helpers import config_validation as cv, selector from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.typing import ConfigType from homeassistant.util import slugify @@ -39,6 +43,27 @@ PLATFORMS: list[Platform] = [Platform.SENSOR] CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN) +SERVICE_SCHEMA: Final = vol.Schema( + { + vol.Required(ATTR_CONFIG_ENTRY_ID): selector.ConfigEntrySelector( + { + "integration": DOMAIN, + } + ), + vol.Optional(ATTR_PACKAGE_STATE): selector.SelectSelector( + selector.SelectSelectorConfig( + multiple=True, + options=[ + value.lower().replace(" ", "_") + for value in PACKAGE_STATUS_MAP.values() + ], + mode=selector.SelectSelectorMode.DROPDOWN, + translation_key=ATTR_PACKAGE_STATE, + ) + ), + } +) + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the 17Track component.""" @@ -47,6 +72,26 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Get packages from 17Track.""" config_entry_id = call.data[ATTR_CONFIG_ENTRY_ID] package_states = call.data.get(ATTR_PACKAGE_STATE, []) + + entry: ConfigEntry | None = hass.config_entries.async_get_entry(config_entry_id) + + if not entry: + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="invalid_config_entry", + translation_placeholders={ + "config_entry_id": config_entry_id, + }, + ) + if entry.state != ConfigEntryState.LOADED: + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="unloaded_config_entry", + translation_placeholders={ + "config_entry_id": entry.title, + }, + ) + seventeen_coordinator: SeventeenTrackCoordinator = hass.data[DOMAIN][ config_entry_id ] @@ -75,6 +120,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: DOMAIN, SERVICE_GET_PACKAGES, get_packages, + schema=SERVICE_SCHEMA, supports_response=SupportsResponse.ONLY, ) return True diff --git a/homeassistant/components/seventeentrack/strings.json b/homeassistant/components/seventeentrack/strings.json index 626af29e856..cad04fca8b9 100644 --- a/homeassistant/components/seventeentrack/strings.json +++ b/homeassistant/components/seventeentrack/strings.json @@ -18,6 +18,14 @@ "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" } }, + "exceptions": { + "invalid_config_entry": { + "message": "Invalid config entry provided. Got {config_entry_id}" + }, + "unloaded_config_entry": { + "message": "Invalid config entry provided. {config_entry_id} is not loaded." + } + }, "options": { "step": { "init": { diff --git a/tests/components/seventeentrack/test_services.py b/tests/components/seventeentrack/test_services.py index 148286d66d4..4347189a5c0 100644 --- a/tests/components/seventeentrack/test_services.py +++ b/tests/components/seventeentrack/test_services.py @@ -2,10 +2,14 @@ from unittest.mock import AsyncMock +import pytest from syrupy import SnapshotAssertion from homeassistant.components.seventeentrack import DOMAIN, SERVICE_GET_PACKAGES -from homeassistant.core import HomeAssistant, SupportsResponse +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import device_registry as dr from . import init_integration from .conftest import get_package @@ -30,7 +34,7 @@ async def test_get_packages_from_list( "package_state": ["in_transit", "delivered"], }, blocking=True, - return_response=SupportsResponse.ONLY, + return_response=True, ) assert service_response == snapshot @@ -52,12 +56,67 @@ async def test_get_all_packages( "config_entry_id": mock_config_entry.entry_id, }, blocking=True, - return_response=SupportsResponse.ONLY, + return_response=True, ) assert service_response == snapshot +async def test_service_called_with_unloaded_entry( + hass: HomeAssistant, + mock_seventeentrack: AsyncMock, + mock_config_entry: MockConfigEntry, + snapshot: SnapshotAssertion, +) -> None: + """Test service call with not ready config entry.""" + await init_integration(hass, mock_config_entry) + mock_config_entry.mock_state(hass, ConfigEntryState.SETUP_ERROR) + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + DOMAIN, + SERVICE_GET_PACKAGES, + { + "config_entry_id": mock_config_entry.entry_id, + }, + blocking=True, + return_response=True, + ) + + +async def test_service_called_with_non_17track_device( + hass: HomeAssistant, + mock_seventeentrack: AsyncMock, + mock_config_entry: MockConfigEntry, + snapshot: SnapshotAssertion, + device_registry: dr.DeviceRegistry, +) -> None: + """Test service calls with non 17Track device.""" + await init_integration(hass, mock_config_entry) + + other_domain = "Not17Track" + other_config_id = "555" + other_mock_config_entry = MockConfigEntry( + title="Not 17Track", domain=other_domain, entry_id=other_config_id + ) + other_mock_config_entry.add_to_hass(hass) + + device_entry = device_registry.async_get_or_create( + config_entry_id=other_config_id, + identifiers={(other_domain, "1")}, + ) + + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + DOMAIN, + SERVICE_GET_PACKAGES, + { + "config_entry_id": device_entry.id, + }, + blocking=True, + return_response=True, + ) + + async def _mock_packages(mock_seventeentrack): package1 = get_package(status=10) package2 = get_package( From e0d8c4d7262845ca0093f8616a41906d0e107fd7 Mon Sep 17 00:00:00 2001 From: Kevin Stillhammer Date: Sat, 22 Jun 2024 10:47:21 +0200 Subject: [PATCH 0977/1445] Ensure kraken tracked pairs can be deselected (#117461) --- .../components/kraken/config_flow.py | 13 +++- tests/components/kraken/test_config_flow.py | 67 ++++++++++++++++++- 2 files changed, 78 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/kraken/config_flow.py b/homeassistant/components/kraken/config_flow.py index 3375746f25d..93c3c6606a3 100644 --- a/homeassistant/components/kraken/config_flow.py +++ b/homeassistant/components/kraken/config_flow.py @@ -69,6 +69,17 @@ class KrakenOptionsFlowHandler(OptionsFlow): get_tradable_asset_pairs, api ) tradable_asset_pairs_for_multi_select = {v: v for v in tradable_asset_pairs} + + # Ensure that a previously selected tracked asset pair is still available in multiselect + # even if it is not tradable anymore + tracked_asset_pairs = self.config_entry.options.get( + CONF_TRACKED_ASSET_PAIRS, [] + ) + for tracked_asset_pair in tracked_asset_pairs: + tradable_asset_pairs_for_multi_select[tracked_asset_pair] = ( + tracked_asset_pair + ) + options = { vol.Optional( CONF_SCAN_INTERVAL, @@ -78,7 +89,7 @@ class KrakenOptionsFlowHandler(OptionsFlow): ): int, vol.Optional( CONF_TRACKED_ASSET_PAIRS, - default=self.config_entry.options.get(CONF_TRACKED_ASSET_PAIRS, []), + default=tracked_asset_pairs, ): cv.multi_select(tradable_asset_pairs_for_multi_select), } diff --git a/tests/components/kraken/test_config_flow.py b/tests/components/kraken/test_config_flow.py index e1971ec3ab8..d2221d161c2 100644 --- a/tests/components/kraken/test_config_flow.py +++ b/tests/components/kraken/test_config_flow.py @@ -7,7 +7,11 @@ from homeassistant.const import CONF_SCAN_INTERVAL from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType -from .const import TICKER_INFORMATION_RESPONSE, TRADEABLE_ASSET_PAIR_RESPONSE +from .const import ( + MISSING_PAIR_TRADEABLE_ASSET_PAIR_RESPONSE, + TICKER_INFORMATION_RESPONSE, + TRADEABLE_ASSET_PAIR_RESPONSE, +) from tests.common import MockConfigEntry @@ -94,3 +98,64 @@ async def test_options(hass: HomeAssistant) -> None: assert ada_eth_sensor.state == "0.0003494" assert hass.states.get("sensor.xbt_usd_ask") is None + + +async def test_deselect_removed_pair(hass: HomeAssistant) -> None: + """Test options for Kraken.""" + entry = MockConfigEntry( + domain=DOMAIN, + options={ + CONF_SCAN_INTERVAL: 60, + CONF_TRACKED_ASSET_PAIRS: [ + "XBT/USD", + ], + }, + ) + entry.add_to_hass(hass) + + with ( + patch( + "homeassistant.components.kraken.config_flow.KrakenAPI.get_tradable_asset_pairs", + return_value=TRADEABLE_ASSET_PAIR_RESPONSE, + ), + patch( + "pykrakenapi.KrakenAPI.get_tradable_asset_pairs", + return_value=TRADEABLE_ASSET_PAIR_RESPONSE, + ), + patch( + "pykrakenapi.KrakenAPI.get_ticker_information", + return_value=TICKER_INFORMATION_RESPONSE, + ), + ): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + with ( + patch( + "homeassistant.components.kraken.config_flow.KrakenAPI.get_tradable_asset_pairs", + return_value=MISSING_PAIR_TRADEABLE_ASSET_PAIR_RESPONSE, + ), + patch( + "pykrakenapi.KrakenAPI.get_tradable_asset_pairs", + return_value=MISSING_PAIR_TRADEABLE_ASSET_PAIR_RESPONSE, + ), + patch( + "pykrakenapi.KrakenAPI.get_ticker_information", + return_value=TICKER_INFORMATION_RESPONSE, + ), + ): + result = await hass.config_entries.options.async_init(entry.entry_id) + schema = result["data_schema"].schema + assert "XBT/USD" in schema.get(CONF_TRACKED_ASSET_PAIRS).options + result = await hass.config_entries.options.async_configure( + result["flow_id"], + { + CONF_SCAN_INTERVAL: 10, + CONF_TRACKED_ASSET_PAIRS: ["ADA/ETH"], + }, + ) + assert result["type"] is FlowResultType.CREATE_ENTRY + await hass.async_block_till_done() + + ada_eth_sensor = hass.states.get("sensor.ada_eth_ask") + assert ada_eth_sensor.state == "0.0003494" From f1759982ad5d0ef37055c1569af46b83e76652e1 Mon Sep 17 00:00:00 2001 From: Ryan Mattson Date: Sat, 22 Jun 2024 03:48:02 -0500 Subject: [PATCH 0978/1445] Lyric: Only pull priority rooms when its an LCC device (#116876) --- homeassistant/components/lyric/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/lyric/__init__.py b/homeassistant/components/lyric/__init__.py index 349e4f871a3..7c002229741 100644 --- a/homeassistant/components/lyric/__init__.py +++ b/homeassistant/components/lyric/__init__.py @@ -84,6 +84,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: for location in lyric.locations for device in location.devices if device.deviceClass == "Thermostat" + and device.deviceID.startswith("LCC") ) ) From 3d9f05325627cf925f264df9fcf3a4c57504c4a4 Mon Sep 17 00:00:00 2001 From: peteS-UK <64092177+peteS-UK@users.noreply.github.com> Date: Sat, 22 Jun 2024 09:58:23 +0100 Subject: [PATCH 0979/1445] Update naming to reflect name change from Logitech Media Server to Lyrion Music Server (#119480) Co-authored-by: Franck Nijhof --- homeassistant/components/squeezebox/__init__.py | 4 ++-- homeassistant/components/squeezebox/config_flow.py | 4 ++-- homeassistant/components/squeezebox/manifest.json | 2 +- homeassistant/components/squeezebox/media_player.py | 2 +- homeassistant/components/squeezebox/strings.json | 6 +++--- homeassistant/generated/integrations.json | 2 +- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/squeezebox/__init__.py b/homeassistant/components/squeezebox/__init__.py index b3e2717d075..baaddbef0b6 100644 --- a/homeassistant/components/squeezebox/__init__.py +++ b/homeassistant/components/squeezebox/__init__.py @@ -1,4 +1,4 @@ -"""The Logitech Squeezebox integration.""" +"""The Squeezebox integration.""" import logging @@ -14,7 +14,7 @@ PLATFORMS = [Platform.MEDIA_PLAYER] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: - """Set up Logitech Squeezebox from a config entry.""" + """Set up Squeezebox from a config entry.""" await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/squeezebox/config_flow.py b/homeassistant/components/squeezebox/config_flow.py index 0da8fcce3f7..9ccac13223b 100644 --- a/homeassistant/components/squeezebox/config_flow.py +++ b/homeassistant/components/squeezebox/config_flow.py @@ -1,4 +1,4 @@ -"""Config flow for Logitech Squeezebox integration.""" +"""Config flow for Squeezebox integration.""" import asyncio from http import HTTPStatus @@ -64,7 +64,7 @@ def _base_schema(discovery_info=None): class SqueezeboxConfigFlow(ConfigFlow, domain=DOMAIN): - """Handle a config flow for Logitech Squeezebox.""" + """Handle a config flow for Squeezebox.""" VERSION = 1 diff --git a/homeassistant/components/squeezebox/manifest.json b/homeassistant/components/squeezebox/manifest.json index 83ca3ff1b00..40bc8f36d22 100644 --- a/homeassistant/components/squeezebox/manifest.json +++ b/homeassistant/components/squeezebox/manifest.json @@ -1,6 +1,6 @@ { "domain": "squeezebox", - "name": "Squeezebox (Logitech Media Server)", + "name": "Squeezebox (Lyrion Music Server)", "codeowners": ["@rajlaud"], "config_flow": true, "dhcp": [ diff --git a/homeassistant/components/squeezebox/media_player.py b/homeassistant/components/squeezebox/media_player.py index e822fe817b9..bf1ad1d77c4 100644 --- a/homeassistant/components/squeezebox/media_player.py +++ b/homeassistant/components/squeezebox/media_player.py @@ -1,4 +1,4 @@ -"""Support for interfacing to the Logitech SqueezeBox API.""" +"""Support for interfacing to the SqueezeBox API.""" from __future__ import annotations diff --git a/homeassistant/components/squeezebox/strings.json b/homeassistant/components/squeezebox/strings.json index fd232851e8a..899d35813aa 100644 --- a/homeassistant/components/squeezebox/strings.json +++ b/homeassistant/components/squeezebox/strings.json @@ -7,7 +7,7 @@ "host": "[%key:common::config_flow::data::host%]" }, "data_description": { - "host": "The hostname or IP address of your Logitech Media Server." + "host": "The hostname or IP address of your Lyrion Music Server." } }, "edit": { @@ -39,11 +39,11 @@ "fields": { "command": { "name": "Command", - "description": "Command to pass to Logitech Media Server (p0 in the CLI documentation)." + "description": "Command to pass to Lyrion Music Server (p0 in the CLI documentation)." }, "parameters": { "name": "Parameters", - "description": "Array of additional parameters to pass to Logitech Media Server (p1, ..., pN in the CLI documentation).\n." + "description": "Array of additional parameters to pass to Lyrion Music Server (p1, ..., pN in the CLI documentation).\n." } } }, diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 542d0563189..bfe57db8883 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -3386,7 +3386,7 @@ "integration_type": "hub", "config_flow": true, "iot_class": "local_polling", - "name": "Squeezebox (Logitech Media Server)" + "name": "Squeezebox (Lyrion Music Server)" } } }, From 77edc149ec20f91cdd027a00fc344f801cb48e3d Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Sat, 22 Jun 2024 11:12:21 +0200 Subject: [PATCH 0980/1445] Add distinct import / export entities to Fronius (#116535) --- homeassistant/components/fronius/sensor.py | 60 ++++++++++++++++++- homeassistant/components/fronius/strings.json | 18 ++++++ tests/components/fronius/test_sensor.py | 43 +++++++------ 3 files changed, 103 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/fronius/sensor.py b/homeassistant/components/fronius/sensor.py index 3b283c33326..31f080c1f51 100644 --- a/homeassistant/components/fronius/sensor.py +++ b/homeassistant/components/fronius/sensor.py @@ -549,6 +549,25 @@ POWER_FLOW_ENTITY_DESCRIPTIONS: list[FroniusSensorEntityDescription] = [ native_unit_of_measurement=UnitOfPower.WATT, device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + ), + FroniusSensorEntityDescription( + key="power_battery_discharge", + response_key="power_battery", + default_value=0, + value_fn=lambda value: max(value, 0), # type: ignore[type-var] + native_unit_of_measurement=UnitOfPower.WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + ), + FroniusSensorEntityDescription( + key="power_battery_charge", + response_key="power_battery", + default_value=0, + value_fn=lambda value: max(0 - value, 0), # type: ignore[operator] + native_unit_of_measurement=UnitOfPower.WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, ), FroniusSensorEntityDescription( key="power_grid", @@ -556,6 +575,25 @@ POWER_FLOW_ENTITY_DESCRIPTIONS: list[FroniusSensorEntityDescription] = [ native_unit_of_measurement=UnitOfPower.WATT, device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + ), + FroniusSensorEntityDescription( + key="power_grid_import", + response_key="power_grid", + default_value=0, + value_fn=lambda value: max(value, 0), # type: ignore[type-var] + native_unit_of_measurement=UnitOfPower.WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + ), + FroniusSensorEntityDescription( + key="power_grid_export", + response_key="power_grid", + default_value=0, + value_fn=lambda value: max(0 - value, 0), # type: ignore[operator] + native_unit_of_measurement=UnitOfPower.WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, ), FroniusSensorEntityDescription( key="power_load", @@ -563,6 +601,26 @@ POWER_FLOW_ENTITY_DESCRIPTIONS: list[FroniusSensorEntityDescription] = [ native_unit_of_measurement=UnitOfPower.WATT, device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + ), + FroniusSensorEntityDescription( + key="power_load_generated", + response_key="power_load", + default_value=0, + value_fn=lambda value: max(value, 0), # type: ignore[type-var] + native_unit_of_measurement=UnitOfPower.WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + ), + FroniusSensorEntityDescription( + key="power_load_consumed", + response_key="power_load", + default_value=0, + value_fn=lambda value: max(0 - value, 0), # type: ignore[operator] + native_unit_of_measurement=UnitOfPower.WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, ), FroniusSensorEntityDescription( key="power_photovoltaics", @@ -670,7 +728,7 @@ class _FroniusSensorEntity(CoordinatorEntity["FroniusCoordinatorBase"], SensorEn if self.entity_description.invalid_when_falsy and not new_value: return None if self.entity_description.value_fn is not None: - return self.entity_description.value_fn(new_value) + new_value = self.entity_description.value_fn(new_value) if isinstance(new_value, float): return round(new_value, 4) return new_value diff --git a/homeassistant/components/fronius/strings.json b/homeassistant/components/fronius/strings.json index de066704644..af93694284a 100644 --- a/homeassistant/components/fronius/strings.json +++ b/homeassistant/components/fronius/strings.json @@ -234,12 +234,30 @@ "power_battery": { "name": "Power battery" }, + "power_battery_discharge": { + "name": "Power battery discharge" + }, + "power_battery_charge": { + "name": "Power battery charge" + }, "power_grid": { "name": "Power grid" }, + "power_grid_import": { + "name": "Power grid import" + }, + "power_grid_export": { + "name": "Power grid export" + }, "power_load": { "name": "Power load" }, + "power_load_generated": { + "name": "Power load generated" + }, + "power_load_consumed": { + "name": "Power load consumed" + }, "power_photovoltaics": { "name": "Power photovoltaics" }, diff --git a/tests/components/fronius/test_sensor.py b/tests/components/fronius/test_sensor.py index f5e77660271..04c25ce26f2 100644 --- a/tests/components/fronius/test_sensor.py +++ b/tests/components/fronius/test_sensor.py @@ -34,14 +34,14 @@ async def test_symo_inverter( mock_responses(aioclient_mock, night=True) config_entry = await setup_fronius_integration(hass) - assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 21 + assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 22 await enable_all_entities( hass, freezer, config_entry.entry_id, FroniusInverterUpdateCoordinator.default_interval, ) - assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 54 + assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 58 assert_state("sensor.symo_20_dc_current", 0) assert_state("sensor.symo_20_energy_day", 10828) assert_state("sensor.symo_20_total_energy", 44186900) @@ -54,14 +54,14 @@ async def test_symo_inverter( freezer.tick(FroniusInverterUpdateCoordinator.default_interval) async_fire_time_changed(hass) await hass.async_block_till_done() - assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 58 + assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 62 await enable_all_entities( hass, freezer, config_entry.entry_id, FroniusInverterUpdateCoordinator.default_interval, ) - assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 60 + assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 64 # 4 additional AC entities assert_state("sensor.symo_20_dc_current", 2.19) assert_state("sensor.symo_20_energy_day", 1113) @@ -97,7 +97,7 @@ async def test_symo_logger( mock_responses(aioclient_mock) await setup_fronius_integration(hass) - assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 25 + assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 26 # states are rounded to 4 decimals assert_state("sensor.solarnet_grid_export_tariff", 0.078) assert_state("sensor.solarnet_co2_factor", 0.53) @@ -119,14 +119,14 @@ async def test_symo_meter( mock_responses(aioclient_mock) config_entry = await setup_fronius_integration(hass) - assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 25 + assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 26 await enable_all_entities( hass, freezer, config_entry.entry_id, FroniusMeterUpdateCoordinator.default_interval, ) - assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 60 + assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 64 # states are rounded to 4 decimals assert_state("sensor.smart_meter_63a_current_phase_1", 7.755) assert_state("sensor.smart_meter_63a_current_phase_2", 6.68) @@ -222,20 +222,23 @@ async def test_symo_power_flow( mock_responses(aioclient_mock, night=True) config_entry = await setup_fronius_integration(hass) - assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 21 + assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 22 await enable_all_entities( hass, freezer, config_entry.entry_id, FroniusInverterUpdateCoordinator.default_interval, ) - assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 54 + assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 58 # states are rounded to 4 decimals assert_state("sensor.solarnet_energy_day", 10828) assert_state("sensor.solarnet_total_energy", 44186900) assert_state("sensor.solarnet_energy_year", 25507686) assert_state("sensor.solarnet_power_grid", 975.31) + assert_state("sensor.solarnet_power_grid_import", 975.31) + assert_state("sensor.solarnet_power_grid_export", 0) assert_state("sensor.solarnet_power_load", -975.31) + assert_state("sensor.solarnet_power_load_consumed", 975.31) assert_state("sensor.solarnet_relative_autonomy", 0) # Second test at daytime when inverter is producing @@ -244,12 +247,16 @@ async def test_symo_power_flow( async_fire_time_changed(hass) await hass.async_block_till_done() # 54 because power_flow `rel_SelfConsumption` and `P_PV` is not `null` anymore - assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 56 + assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 60 assert_state("sensor.solarnet_energy_day", 1101.7001) assert_state("sensor.solarnet_total_energy", 44188000) assert_state("sensor.solarnet_energy_year", 25508788) assert_state("sensor.solarnet_power_grid", 1703.74) + assert_state("sensor.solarnet_power_grid_import", 1703.74) + assert_state("sensor.solarnet_power_grid_export", 0) assert_state("sensor.solarnet_power_load", -2814.74) + assert_state("sensor.solarnet_power_load_generated", 0) + assert_state("sensor.solarnet_power_load_consumed", 2814.74) assert_state("sensor.solarnet_power_photovoltaics", 1111) assert_state("sensor.solarnet_relative_autonomy", 39.4708) assert_state("sensor.solarnet_relative_self_consumption", 100) @@ -259,7 +266,7 @@ async def test_symo_power_flow( freezer.tick(FroniusPowerFlowUpdateCoordinator.default_interval) async_fire_time_changed(hass) await hass.async_block_till_done() - assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 56 + assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 60 assert_state("sensor.solarnet_energy_day", 10828) assert_state("sensor.solarnet_total_energy", 44186900) assert_state("sensor.solarnet_energy_year", 25507686) @@ -285,14 +292,14 @@ async def test_gen24( mock_responses(aioclient_mock, fixture_set="gen24") config_entry = await setup_fronius_integration(hass, is_logger=False) - assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 23 + assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 24 await enable_all_entities( hass, freezer, config_entry.entry_id, FroniusMeterUpdateCoordinator.default_interval, ) - assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 54 + assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 58 # inverter 1 assert_state("sensor.inverter_name_ac_current", 0.1589) assert_state("sensor.inverter_name_dc_current_2", 0.0754) @@ -386,14 +393,14 @@ async def test_gen24_storage( hass, is_logger=False, unique_id="12345678" ) - assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 35 + assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 37 await enable_all_entities( hass, freezer, config_entry.entry_id, FroniusMeterUpdateCoordinator.default_interval, ) - assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 66 + assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 72 # inverter 1 assert_state("sensor.gen24_storage_dc_current", 0.3952) assert_state("sensor.gen24_storage_dc_voltage_2", 318.8103) @@ -452,6 +459,8 @@ async def test_gen24_storage( # power_flow assert_state("sensor.solarnet_power_grid", 2274.9) assert_state("sensor.solarnet_power_battery", 0.1591) + assert_state("sensor.solarnet_power_battery_charge", 0) + assert_state("sensor.solarnet_power_battery_discharge", 0.1591) assert_state("sensor.solarnet_power_load", -2459.3092) assert_state("sensor.solarnet_relative_self_consumption", 100.0) assert_state("sensor.solarnet_power_photovoltaics", 216.4328) @@ -514,14 +523,14 @@ async def test_primo_s0( mock_responses(aioclient_mock, fixture_set="primo_s0", inverter_ids=[1, 2]) config_entry = await setup_fronius_integration(hass, is_logger=True) - assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 30 + assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 31 await enable_all_entities( hass, freezer, config_entry.entry_id, FroniusMeterUpdateCoordinator.default_interval, ) - assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 43 + assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 47 # logger assert_state("sensor.solarnet_grid_export_tariff", 1) assert_state("sensor.solarnet_co2_factor", 0.53) From d9e26077c6bc3e5c74109784fcc2c3cc0fb23226 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Sat, 22 Jun 2024 05:22:32 -0400 Subject: [PATCH 0981/1445] Add discovery rule for a Z-Wave Basic CC sensor (#105134) --- .../components/zwave_js/discovery.py | 23 ++++- homeassistant/components/zwave_js/sensor.py | 17 ++++ tests/components/zwave_js/conftest.py | 14 +++ .../fixtures/basic_cc_sensor_state.json | 87 +++++++++++++++++++ tests/components/zwave_js/test_sensor.py | 9 ++ 5 files changed, 146 insertions(+), 4 deletions(-) create mode 100644 tests/components/zwave_js/fixtures/basic_cc_sensor_state.json diff --git a/homeassistant/components/zwave_js/discovery.py b/homeassistant/components/zwave_js/discovery.py index 39b97e5d3f4..0b66567c036 100644 --- a/homeassistant/components/zwave_js/discovery.py +++ b/homeassistant/components/zwave_js/discovery.py @@ -1106,7 +1106,7 @@ DISCOVERY_SCHEMAS = [ platform=Platform.LIGHT, primary_value=SWITCH_MULTILEVEL_CURRENT_VALUE_SCHEMA, ), - # light for Basic CC + # light for Basic CC with target ZWaveDiscoverySchema( platform=Platform.LIGHT, primary_value=ZWaveValueDiscoverySchema( @@ -1116,9 +1116,24 @@ DISCOVERY_SCHEMAS = [ ), required_values=[ ZWaveValueDiscoverySchema( - command_class={ - CommandClass.BASIC, - }, + command_class={CommandClass.BASIC}, + type={ValueType.NUMBER}, + property={TARGET_VALUE_PROPERTY}, + ) + ], + ), + # sensor for Basic CC without target + ZWaveDiscoverySchema( + platform=Platform.SENSOR, + hint="numeric_sensor", + primary_value=ZWaveValueDiscoverySchema( + command_class={CommandClass.BASIC}, + type={ValueType.NUMBER}, + property={CURRENT_VALUE_PROPERTY}, + ), + absent_values=[ + ZWaveValueDiscoverySchema( + command_class={CommandClass.BASIC}, type={ValueType.NUMBER}, property={TARGET_VALUE_PROPERTY}, ) diff --git a/homeassistant/components/zwave_js/sensor.py b/homeassistant/components/zwave_js/sensor.py index c07420615a1..e43c620ff54 100644 --- a/homeassistant/components/zwave_js/sensor.py +++ b/homeassistant/components/zwave_js/sensor.py @@ -689,6 +689,23 @@ class ZwaveSensor(ZWaveBaseEntity, SensorEntity): class ZWaveNumericSensor(ZwaveSensor): """Representation of a Z-Wave Numeric sensor.""" + def __init__( + self, + config_entry: ConfigEntry, + driver: Driver, + info: ZwaveDiscoveryInfo, + entity_description: SensorEntityDescription, + unit_of_measurement: str | None = None, + ) -> None: + """Initialize a ZWaveBasicSensor entity.""" + super().__init__( + config_entry, driver, info, entity_description, unit_of_measurement + ) + if self.info.primary_value.command_class == CommandClass.BASIC: + self._attr_name = self.generate_name( + include_value_name=True, alternate_value_name="Basic" + ) + @callback def on_value_update(self) -> None: """Handle scale changes for this value on value updated event.""" diff --git a/tests/components/zwave_js/conftest.py b/tests/components/zwave_js/conftest.py index 63a22d86b50..a2a4c217b8b 100644 --- a/tests/components/zwave_js/conftest.py +++ b/tests/components/zwave_js/conftest.py @@ -687,6 +687,12 @@ def light_device_class_is_null_state_fixture(): return json.loads(load_fixture("zwave_js/light_device_class_is_null_state.json")) +@pytest.fixture(name="basic_cc_sensor_state", scope="package") +def basic_cc_sensor_state_fixture(): + """Load node with Basic CC sensor fixture data.""" + return json.loads(load_fixture("zwave_js/basic_cc_sensor_state.json")) + + # model fixtures @@ -1355,3 +1361,11 @@ def light_device_class_is_null_fixture(client, light_device_class_is_null_state) node = Node(client, copy.deepcopy(light_device_class_is_null_state)) client.driver.controller.nodes[node.node_id] = node return node + + +@pytest.fixture(name="basic_cc_sensor") +def basic_cc_sensor_fixture(client, basic_cc_sensor_state): + """Mock a node with a Basic CC.""" + node = Node(client, copy.deepcopy(basic_cc_sensor_state)) + client.driver.controller.nodes[node.node_id] = node + return node diff --git a/tests/components/zwave_js/fixtures/basic_cc_sensor_state.json b/tests/components/zwave_js/fixtures/basic_cc_sensor_state.json new file mode 100644 index 00000000000..1d749af2021 --- /dev/null +++ b/tests/components/zwave_js/fixtures/basic_cc_sensor_state.json @@ -0,0 +1,87 @@ +{ + "nodeId": 52, + "index": 0, + "installerIcon": 3079, + "userIcon": 3079, + "status": 1, + "ready": true, + "deviceClass": { + "basic": { "key": 2, "label": "Static Controller" }, + "generic": { "key": 21, "label": "Multilevel Sensor" }, + "specific": { "key": 1, "label": "Routing Multilevel Sensor" }, + "mandatorySupportedCCs": [], + "mandatoryControlledCCs": [] + }, + "isListening": true, + "isFrequentListening": false, + "isRouting": true, + "maxBaudRate": 40000, + "isSecure": false, + "version": 4, + "isBeaming": true, + "manufacturerId": 134, + "productId": 100, + "productType": 258, + "firmwareVersion": "1.12", + "zwavePlusVersion": 1, + "nodeType": 0, + "roleType": 5, + "deviceConfig": { + "manufacturerId": 134, + "manufacturer": "Test", + "label": "test", + "description": "foo", + "devices": [ + { + "productType": "0xffff", + "productId": "0xffff" + } + ], + "firmwareVersion": { + "min": "1.10", + "max": "255.255" + }, + "paramInformation": { + "_map": {} + } + }, + "label": "test", + "neighbors": [1, 32], + "interviewAttempts": 1, + "endpoints": [ + { + "nodeId": 52, + "index": 0, + "installerIcon": 3079, + "userIcon": 3079, + "commandClasses": [ + { + "id": 32, + "name": "Basic", + "version": 8, + "isSecure": false + } + ] + } + ], + "values": [ + { + "commandClassName": "Basic", + "commandClass": 32, + "endpoint": 0, + "property": "currentValue", + "propertyName": "currentValue", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 99, + "label": "Current value" + }, + "value": 255 + } + ], + "highestSecurityClass": 7, + "isControllerNode": false +} diff --git a/tests/components/zwave_js/test_sensor.py b/tests/components/zwave_js/test_sensor.py index 358c1036369..02b3df17e22 100644 --- a/tests/components/zwave_js/test_sensor.py +++ b/tests/components/zwave_js/test_sensor.py @@ -212,6 +212,15 @@ async def test_energy_sensors( assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.CURRENT +async def test_basic_cc_sensor( + hass: HomeAssistant, client, basic_cc_sensor, integration +) -> None: + """Test a Basic CC sensor gets discovered correctly.""" + state = hass.states.get("sensor.foo_basic") + assert state is not None + assert state.state == "255.0" + + async def test_disabled_notification_sensor( hass: HomeAssistant, entity_registry: er.EntityRegistry, multisensor_6, integration ) -> None: From 6e15c06aa96ab4965a67667b316517d0d41816af Mon Sep 17 00:00:00 2001 From: Erwin Douna Date: Sat, 22 Jun 2024 11:25:42 +0200 Subject: [PATCH 0982/1445] Melcloud add reconfigure flow (#115999) --- CODEOWNERS | 2 + .../components/melcloud/config_flow.py | 64 ++++++++- .../components/melcloud/manifest.json | 2 +- .../components/melcloud/strings.json | 13 +- tests/components/melcloud/test_config_flow.py | 136 +++++++++++++++++- 5 files changed, 213 insertions(+), 4 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 6999f9e08a0..9b23b5cc83a 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -841,6 +841,8 @@ build.json @home-assistant/supervisor /homeassistant/components/media_source/ @hunterjm /tests/components/media_source/ @hunterjm /homeassistant/components/mediaroom/ @dgomes +/homeassistant/components/melcloud/ @erwindouna +/tests/components/melcloud/ @erwindouna /homeassistant/components/melissa/ @kennedyshead /tests/components/melissa/ @kennedyshead /homeassistant/components/melnor/ @vanstinator diff --git a/homeassistant/components/melcloud/config_flow.py b/homeassistant/components/melcloud/config_flow.py index f071b64988d..c4392535364 100644 --- a/homeassistant/components/melcloud/config_flow.py +++ b/homeassistant/components/melcloud/config_flow.py @@ -25,7 +25,6 @@ class FlowHandler(ConfigFlow, domain=DOMAIN): """Handle a config flow.""" VERSION = 1 - entry: ConfigEntry | None = None async def _create_entry(self, username: str, token: str) -> ConfigFlowResult: @@ -148,3 +147,66 @@ class FlowHandler(ConfigFlow, domain=DOMAIN): errors["base"] = "cannot_connect" return acquired_token, errors + + async def async_step_reconfigure( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle a reconfiguration flow initialized by the user.""" + self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) + return await self.async_step_reconfigure_confirm() + + async def async_step_reconfigure_confirm( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle a reconfiguration flow initialized by the user.""" + errors: dict[str, str] = {} + acquired_token = None + assert self.entry + + if user_input is not None: + user_input[CONF_USERNAME] = self.entry.data[CONF_USERNAME] + try: + async with asyncio.timeout(10): + acquired_token = await pymelcloud.login( + user_input[CONF_USERNAME], + user_input[CONF_PASSWORD], + async_get_clientsession(self.hass), + ) + except (ClientResponseError, AttributeError) as err: + if ( + isinstance(err, ClientResponseError) + and err.status + in ( + HTTPStatus.UNAUTHORIZED, + HTTPStatus.FORBIDDEN, + ) + or isinstance(err, AttributeError) + and err.name == "get" + ): + errors["base"] = "invalid_auth" + else: + errors["base"] = "cannot_connect" + except ( + TimeoutError, + ClientError, + ): + errors["base"] = "cannot_connect" + + if not errors: + user_input[CONF_TOKEN] = acquired_token + return self.async_update_reload_and_abort( + self.entry, + data={**self.entry.data, **user_input}, + reason="reconfigure_successful", + ) + + return self.async_show_form( + step_id="reconfigure_confirm", + data_schema=vol.Schema( + { + vol.Required(CONF_PASSWORD): str, + } + ), + errors=errors, + description_placeholders={CONF_USERNAME: self.entry.data[CONF_USERNAME]}, + ) diff --git a/homeassistant/components/melcloud/manifest.json b/homeassistant/components/melcloud/manifest.json index 0122c840373..f61ed412be1 100644 --- a/homeassistant/components/melcloud/manifest.json +++ b/homeassistant/components/melcloud/manifest.json @@ -1,7 +1,7 @@ { "domain": "melcloud", "name": "MELCloud", - "codeowners": [], + "codeowners": ["@erwindouna"], "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/melcloud", "iot_class": "cloud_polling", diff --git a/homeassistant/components/melcloud/strings.json b/homeassistant/components/melcloud/strings.json index 6a98b88e2d3..968f9cf4e50 100644 --- a/homeassistant/components/melcloud/strings.json +++ b/homeassistant/components/melcloud/strings.json @@ -16,6 +16,16 @@ "username": "[%key:common::config_flow::data::email%]", "password": "[%key:common::config_flow::data::password%]" } + }, + "reconfigure_confirm": { + "title": "Reconfigure your MelCloud", + "description": "Reconfigure the entry to obtain a new token, for your account: `{username}`.", + "data": { + "password": "[%key:common::config_flow::data::password%]" + }, + "data_description": { + "password": "Enter the (new) password for MelCloud." + } } }, "error": { @@ -25,7 +35,8 @@ }, "abort": { "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", - "already_configured": "MELCloud integration already configured for this email. Access token has been refreshed." + "already_configured": "MELCloud integration already configured for this email. Access token has been refreshed.", + "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]" } }, "services": { diff --git a/tests/components/melcloud/test_config_flow.py b/tests/components/melcloud/test_config_flow.py index 621838e8c67..c1c6c10ac4c 100644 --- a/tests/components/melcloud/test_config_flow.py +++ b/tests/components/melcloud/test_config_flow.py @@ -9,7 +9,8 @@ import pytest from homeassistant import config_entries from homeassistant.components.melcloud.const import DOMAIN -from homeassistant.config_entries import SOURCE_REAUTH +from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_RECONFIGURE +from homeassistant.const import CONF_PASSWORD from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -305,3 +306,136 @@ async def test_client_errors_reauthentication( assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" + + +@pytest.mark.parametrize( + ("error", "reason"), + [ + (HTTPStatus.UNAUTHORIZED, "invalid_auth"), + (HTTPStatus.FORBIDDEN, "invalid_auth"), + (HTTPStatus.INTERNAL_SERVER_ERROR, "cannot_connect"), + ], +) +async def test_reconfigure_flow( + hass: HomeAssistant, mock_login, mock_request_info, error, reason +) -> None: + """Test re-configuration flow.""" + mock_login.side_effect = ClientResponseError(mock_request_info(), (), status=error) + mock_entry = MockConfigEntry( + domain=DOMAIN, + data={"username": "test-email@test-domain.com", "token": "test-original-token"}, + unique_id="test-email@test-domain.com", + ) + mock_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": SOURCE_RECONFIGURE, + "unique_id": mock_entry.unique_id, + "entry_id": mock_entry.entry_id, + }, + data=mock_entry.data, + ) + + assert result["type"] is FlowResultType.FORM + + with patch( + "homeassistant.components.melcloud.async_setup_entry", + return_value=True, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_PASSWORD: "test-password"}, + ) + await hass.async_block_till_done() + + assert result["errors"]["base"] == reason + assert result["type"] is FlowResultType.FORM + + mock_login.side_effect = None + with patch( + "homeassistant.components.melcloud.async_setup_entry", + return_value=True, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_PASSWORD: "test-password"}, + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reconfigure_successful" + entry = hass.config_entries.async_get_entry(mock_entry.entry_id) + assert entry + assert entry.title == "Mock Title" + assert entry.data == { + "username": "test-email@test-domain.com", + "token": "test-token", + "password": "test-password", + } + + +@pytest.mark.parametrize( + ("error", "reason"), + [ + (TimeoutError(), "cannot_connect"), + (AttributeError(name="get"), "invalid_auth"), + ], +) +async def test_form_errors_reconfigure( + hass: HomeAssistant, mock_login, error, reason +) -> None: + """Test we handle cannot connect error.""" + mock_login.side_effect = error + mock_entry = MockConfigEntry( + domain=DOMAIN, + data={"username": "test-email@test-domain.com", "token": "test-original-token"}, + unique_id="test-email@test-domain.com", + ) + mock_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": SOURCE_RECONFIGURE, + "unique_id": mock_entry.unique_id, + "entry_id": mock_entry.entry_id, + }, + data=mock_entry.data, + ) + + with patch( + "homeassistant.components.melcloud.async_setup_entry", + return_value=True, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_PASSWORD: "test-password"}, + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.FORM + assert result["errors"]["base"] == reason + + mock_login.side_effect = None + with patch( + "homeassistant.components.melcloud.async_setup_entry", + return_value=True, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_PASSWORD: "test-password"}, + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reconfigure_successful" + entry = hass.config_entries.async_get_entry(mock_entry.entry_id) + assert entry + assert entry.title == "Mock Title" + assert entry.data == { + "username": "test-email@test-domain.com", + "token": "test-token", + "password": "test-password", + } From 5e71eb4e0df2f7651513e5d68a216a980c0220cd Mon Sep 17 00:00:00 2001 From: Jan Gaedicke Date: Sat, 22 Jun 2024 11:28:59 +0200 Subject: [PATCH 0983/1445] Add support for VESKA-micro-inverter (VK-800) to tuya integration (#115996) --- homeassistant/components/tuya/const.py | 2 ++ homeassistant/components/tuya/sensor.py | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/homeassistant/components/tuya/const.py b/homeassistant/components/tuya/const.py index d731a93f858..f54b2af36e0 100644 --- a/homeassistant/components/tuya/const.py +++ b/homeassistant/components/tuya/const.py @@ -253,6 +253,7 @@ class DPCode(StrEnum): POWDER_SET = "powder_set" # Powder POWER = "power" POWER_GO = "power_go" + POWER_TOTAL = "power_total" PRESENCE_STATE = "presence_state" PRESSURE_STATE = "pressure_state" PRESSURE_VALUE = "pressure_value" @@ -267,6 +268,7 @@ class DPCode(StrEnum): RESET_FILTER = "reset_filter" RESET_MAP = "reset_map" RESET_ROLL_BRUSH = "reset_roll_brush" + REVERSE_ENERGY_TOTAL = "reverse_energy_total" ROLL_BRUSH = "roll_brush" SEEK = "seek" SENSITIVITY = "sensitivity" # Sensitivity diff --git a/homeassistant/components/tuya/sensor.py b/homeassistant/components/tuya/sensor.py index 2b2baea5251..0937f64d911 100644 --- a/homeassistant/components/tuya/sensor.py +++ b/homeassistant/components/tuya/sensor.py @@ -1094,6 +1094,24 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { ), *BATTERY_SENSORS, ), + # VESKA-micro inverter + "znnbq": ( + TuyaSensorEntityDescription( + key=DPCode.REVERSE_ENERGY_TOTAL, + translation_key="total_energy", + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + TuyaSensorEntityDescription( + key=DPCode.POWER_TOTAL, + translation_key="power", + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfPower.WATT, + suggested_display_precision=0, + suggested_unit_of_measurement=UnitOfPower.WATT, + ), + ), } # Socket (duplicate of `kg`) From ed2ad5ceaa953002436cd5d502cef48b2df892f3 Mon Sep 17 00:00:00 2001 From: Bouke Haarsma Date: Sat, 22 Jun 2024 11:46:11 +0200 Subject: [PATCH 0984/1445] Increase precision of Huisbaasje gas readings (#120138) --- homeassistant/components/huisbaasje/sensor.py | 30 ++++++++--------- tests/components/huisbaasje/test_sensor.py | 32 +++++++++---------- 2 files changed, 30 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/huisbaasje/sensor.py b/homeassistant/components/huisbaasje/sensor.py index d09b559516b..142d013ed1e 100644 --- a/homeassistant/components/huisbaasje/sensor.py +++ b/homeassistant/components/huisbaasje/sensor.py @@ -54,7 +54,6 @@ class HuisbaasjeSensorEntityDescription(SensorEntityDescription): """Class describing Airly sensor entities.""" sensor_type: str = SENSOR_TYPE_RATE - precision: int = 0 SENSORS_INFO = [ @@ -105,7 +104,7 @@ SENSORS_INFO = [ key=SOURCE_TYPE_ELECTRICITY_IN, sensor_type=SENSOR_TYPE_THIS_DAY, state_class=SensorStateClass.TOTAL_INCREASING, - precision=3, + suggested_display_precision=3, ), HuisbaasjeSensorEntityDescription( translation_key="energy_consumption_off_peak_today", @@ -114,7 +113,7 @@ SENSORS_INFO = [ key=SOURCE_TYPE_ELECTRICITY_IN_LOW, sensor_type=SENSOR_TYPE_THIS_DAY, state_class=SensorStateClass.TOTAL_INCREASING, - precision=3, + suggested_display_precision=3, ), HuisbaasjeSensorEntityDescription( translation_key="energy_production_peak_today", @@ -123,7 +122,7 @@ SENSORS_INFO = [ key=SOURCE_TYPE_ELECTRICITY_OUT, sensor_type=SENSOR_TYPE_THIS_DAY, state_class=SensorStateClass.TOTAL_INCREASING, - precision=3, + suggested_display_precision=3, ), HuisbaasjeSensorEntityDescription( translation_key="energy_production_off_peak_today", @@ -132,7 +131,7 @@ SENSORS_INFO = [ key=SOURCE_TYPE_ELECTRICITY_OUT_LOW, sensor_type=SENSOR_TYPE_THIS_DAY, state_class=SensorStateClass.TOTAL_INCREASING, - precision=3, + suggested_display_precision=3, ), HuisbaasjeSensorEntityDescription( translation_key="energy_today", @@ -141,7 +140,7 @@ SENSORS_INFO = [ state_class=SensorStateClass.TOTAL, key=SOURCE_TYPE_ELECTRICITY, sensor_type=SENSOR_TYPE_THIS_DAY, - precision=1, + suggested_display_precision=1, ), HuisbaasjeSensorEntityDescription( translation_key="energy_week", @@ -150,7 +149,7 @@ SENSORS_INFO = [ state_class=SensorStateClass.TOTAL, key=SOURCE_TYPE_ELECTRICITY, sensor_type=SENSOR_TYPE_THIS_WEEK, - precision=1, + suggested_display_precision=1, ), HuisbaasjeSensorEntityDescription( translation_key="energy_month", @@ -159,7 +158,7 @@ SENSORS_INFO = [ state_class=SensorStateClass.TOTAL, key=SOURCE_TYPE_ELECTRICITY, sensor_type=SENSOR_TYPE_THIS_MONTH, - precision=1, + suggested_display_precision=1, ), HuisbaasjeSensorEntityDescription( translation_key="energy_year", @@ -168,7 +167,7 @@ SENSORS_INFO = [ state_class=SensorStateClass.TOTAL, key=SOURCE_TYPE_ELECTRICITY, sensor_type=SENSOR_TYPE_THIS_YEAR, - precision=1, + suggested_display_precision=1, ), HuisbaasjeSensorEntityDescription( translation_key="current_gas", @@ -176,7 +175,7 @@ SENSORS_INFO = [ sensor_type=SENSOR_TYPE_RATE, state_class=SensorStateClass.MEASUREMENT, key=SOURCE_TYPE_GAS, - precision=1, + suggested_display_precision=2, ), HuisbaasjeSensorEntityDescription( translation_key="gas_today", @@ -185,7 +184,7 @@ SENSORS_INFO = [ key=SOURCE_TYPE_GAS, sensor_type=SENSOR_TYPE_THIS_DAY, state_class=SensorStateClass.TOTAL_INCREASING, - precision=1, + suggested_display_precision=2, ), HuisbaasjeSensorEntityDescription( translation_key="gas_week", @@ -194,7 +193,7 @@ SENSORS_INFO = [ key=SOURCE_TYPE_GAS, sensor_type=SENSOR_TYPE_THIS_WEEK, state_class=SensorStateClass.TOTAL_INCREASING, - precision=1, + suggested_display_precision=2, ), HuisbaasjeSensorEntityDescription( translation_key="gas_month", @@ -203,7 +202,7 @@ SENSORS_INFO = [ key=SOURCE_TYPE_GAS, sensor_type=SENSOR_TYPE_THIS_MONTH, state_class=SensorStateClass.TOTAL_INCREASING, - precision=1, + suggested_display_precision=2, ), HuisbaasjeSensorEntityDescription( translation_key="gas_year", @@ -212,7 +211,7 @@ SENSORS_INFO = [ key=SOURCE_TYPE_GAS, sensor_type=SENSOR_TYPE_THIS_YEAR, state_class=SensorStateClass.TOTAL_INCREASING, - precision=1, + suggested_display_precision=2, ), ] @@ -253,7 +252,6 @@ class HuisbaasjeSensor( self.entity_description = description self._source_type = description.key self._sensor_type = description.sensor_type - self._precision = description.precision self._attr_unique_id = ( f"{DOMAIN}_{user_id}_{description.key}_{description.sensor_type}" ) @@ -266,7 +264,7 @@ class HuisbaasjeSensor( self.entity_description.sensor_type ] ) is not None: - return round(data, self._precision) + return data return None @property diff --git a/tests/components/huisbaasje/test_sensor.py b/tests/components/huisbaasje/test_sensor.py index 02a05c78763..5f5707bdd5d 100644 --- a/tests/components/huisbaasje/test_sensor.py +++ b/tests/components/huisbaasje/test_sensor.py @@ -59,7 +59,7 @@ async def test_setup_entry(hass: HomeAssistant) -> None: # Assert data is loaded current_power = hass.states.get("sensor.current_power") - assert current_power.state == "1012.0" + assert current_power.state == "1011.66666666667" assert ( current_power.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.POWER ) @@ -72,7 +72,7 @@ async def test_setup_entry(hass: HomeAssistant) -> None: ) current_power_in = hass.states.get("sensor.current_power_in_peak") - assert current_power_in.state == "1012.0" + assert current_power_in.state == "1011.66666666667" assert ( current_power_in.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.POWER @@ -134,7 +134,7 @@ async def test_setup_entry(hass: HomeAssistant) -> None: energy_consumption_peak_today = hass.states.get( "sensor.energy_consumption_peak_today" ) - assert energy_consumption_peak_today.state == "2.67" + assert energy_consumption_peak_today.state == "2.669999453" assert ( energy_consumption_peak_today.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENERGY @@ -151,7 +151,7 @@ async def test_setup_entry(hass: HomeAssistant) -> None: energy_consumption_off_peak_today = hass.states.get( "sensor.energy_consumption_off_peak_today" ) - assert energy_consumption_off_peak_today.state == "0.627" + assert energy_consumption_off_peak_today.state == "0.626666416" assert ( energy_consumption_off_peak_today.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENERGY @@ -168,7 +168,7 @@ async def test_setup_entry(hass: HomeAssistant) -> None: energy_production_peak_today = hass.states.get( "sensor.energy_production_peak_today" ) - assert energy_production_peak_today.state == "1.512" + assert energy_production_peak_today.state == "1.51234" assert ( energy_production_peak_today.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENERGY @@ -185,7 +185,7 @@ async def test_setup_entry(hass: HomeAssistant) -> None: energy_production_off_peak_today = hass.states.get( "sensor.energy_production_off_peak_today" ) - assert energy_production_off_peak_today.state == "1.093" + assert energy_production_off_peak_today.state == "1.09281" assert ( energy_production_off_peak_today.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENERGY @@ -200,7 +200,7 @@ async def test_setup_entry(hass: HomeAssistant) -> None: ) energy_today = hass.states.get("sensor.energy_today") - assert energy_today.state == "3.3" + assert energy_today.state == "3.296665869" assert ( energy_today.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENERGY ) @@ -211,7 +211,7 @@ async def test_setup_entry(hass: HomeAssistant) -> None: ) energy_this_week = hass.states.get("sensor.energy_this_week") - assert energy_this_week.state == "17.5" + assert energy_this_week.state == "17.509996085" assert ( energy_this_week.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENERGY @@ -225,7 +225,7 @@ async def test_setup_entry(hass: HomeAssistant) -> None: ) energy_this_month = hass.states.get("sensor.energy_this_month") - assert energy_this_month.state == "103.3" + assert energy_this_month.state == "103.28830788" assert ( energy_this_month.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENERGY @@ -239,7 +239,7 @@ async def test_setup_entry(hass: HomeAssistant) -> None: ) energy_this_year = hass.states.get("sensor.energy_this_year") - assert energy_this_year.state == "673.0" + assert energy_this_year.state == "672.97811773" assert ( energy_this_year.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENERGY @@ -264,7 +264,7 @@ async def test_setup_entry(hass: HomeAssistant) -> None: ) gas_today = hass.states.get("sensor.gas_today") - assert gas_today.state == "1.1" + assert gas_today.state == "1.07" assert gas_today.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.GAS assert ( gas_today.attributes.get(ATTR_STATE_CLASS) @@ -276,7 +276,7 @@ async def test_setup_entry(hass: HomeAssistant) -> None: ) gas_this_week = hass.states.get("sensor.gas_this_week") - assert gas_this_week.state == "5.6" + assert gas_this_week.state == "5.634224386" assert gas_this_week.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.GAS assert ( gas_this_week.attributes.get(ATTR_STATE_CLASS) @@ -288,7 +288,7 @@ async def test_setup_entry(hass: HomeAssistant) -> None: ) gas_this_month = hass.states.get("sensor.gas_this_month") - assert gas_this_month.state == "39.1" + assert gas_this_month.state == "39.14" assert gas_this_month.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.GAS assert ( gas_this_month.attributes.get(ATTR_STATE_CLASS) @@ -300,7 +300,7 @@ async def test_setup_entry(hass: HomeAssistant) -> None: ) gas_this_year = hass.states.get("sensor.gas_this_year") - assert gas_this_year.state == "116.7" + assert gas_this_year.state == "116.73" assert gas_this_year.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.GAS assert ( gas_this_year.attributes.get(ATTR_STATE_CLASS) @@ -349,13 +349,13 @@ async def test_setup_entry_absent_measurement(hass: HomeAssistant) -> None: await hass.async_block_till_done() # Assert data is loaded - assert hass.states.get("sensor.current_power").state == "1012.0" + assert hass.states.get("sensor.current_power").state == "1011.66666666667" assert hass.states.get("sensor.current_power_in_peak").state == "unknown" assert hass.states.get("sensor.current_power_in_off_peak").state == "unknown" assert hass.states.get("sensor.current_power_out_peak").state == "unknown" assert hass.states.get("sensor.current_power_out_off_peak").state == "unknown" assert hass.states.get("sensor.current_gas").state == "unknown" - assert hass.states.get("sensor.energy_today").state == "3.3" + assert hass.states.get("sensor.energy_today").state == "3.296665869" assert ( hass.states.get("sensor.energy_consumption_peak_today").state == "unknown" ) From 03d8f4162e14eedd75d02775189b0a9f58f67453 Mon Sep 17 00:00:00 2001 From: Marcos A L M Macedo Date: Sat, 22 Jun 2024 06:58:36 -0300 Subject: [PATCH 0985/1445] Add sensor total production energy for Tuya (#113565) --- homeassistant/components/tuya/const.py | 3 ++- homeassistant/components/tuya/sensor.py | 6 ++++++ homeassistant/components/tuya/strings.json | 3 +++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/tuya/const.py b/homeassistant/components/tuya/const.py index f54b2af36e0..524cd0a4983 100644 --- a/homeassistant/components/tuya/const.py +++ b/homeassistant/components/tuya/const.py @@ -166,6 +166,7 @@ class DPCode(StrEnum): CRY_DETECTION_SWITCH = "cry_detection_switch" CUP_NUMBER = "cup_number" # NUmber of cups CUR_CURRENT = "cur_current" # Actual current + CUR_NEUTRAL = "cur_neutral" # Total reverse energy CUR_POWER = "cur_power" # Actual power CUR_VOLTAGE = "cur_voltage" # Actual voltage DECIBEL_SENSITIVITY = "decibel_sensitivity" @@ -444,7 +445,7 @@ UNITS = ( ), UnitOfMeasurement( unit=UnitOfEnergy.KILO_WATT_HOUR, - aliases={"kwh", "kilowatt-hour", "kW·h"}, + aliases={"kwh", "kilowatt-hour", "kW·h", "kW.h"}, device_classes={SensorDeviceClass.ENERGY}, ), UnitOfMeasurement( diff --git a/homeassistant/components/tuya/sensor.py b/homeassistant/components/tuya/sensor.py index 0937f64d911..1468f90a452 100644 --- a/homeassistant/components/tuya/sensor.py +++ b/homeassistant/components/tuya/sensor.py @@ -778,6 +778,12 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, ), + TuyaSensorEntityDescription( + key=DPCode.CUR_NEUTRAL, + translation_key="total_production", + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + ), TuyaSensorEntityDescription( key=DPCode.PHASE_A, translation_key="phase_a_current", diff --git a/homeassistant/components/tuya/strings.json b/homeassistant/components/tuya/strings.json index 281d56f7ae4..46530a1d938 100644 --- a/homeassistant/components/tuya/strings.json +++ b/homeassistant/components/tuya/strings.json @@ -517,6 +517,9 @@ "total_energy": { "name": "Total energy" }, + "total_production": { + "name": "Total production" + }, "phase_a_current": { "name": "Phase A current" }, From 1bbfe7854fee627600bc122cfc665d90241948e1 Mon Sep 17 00:00:00 2001 From: Michael Oborne Date: Sat, 22 Jun 2024 19:58:54 +1000 Subject: [PATCH 0986/1445] Add Tuya reverse_energy_total and total_power sensors (#114801) Co-authored-by: Franck Nijhof --- homeassistant/components/tuya/const.py | 1 + homeassistant/components/tuya/sensor.py | 14 ++++++++++++++ 2 files changed, 15 insertions(+) diff --git a/homeassistant/components/tuya/const.py b/homeassistant/components/tuya/const.py index 524cd0a4983..3b0d22e8cf7 100644 --- a/homeassistant/components/tuya/const.py +++ b/homeassistant/components/tuya/const.py @@ -345,6 +345,7 @@ class DPCode(StrEnum): TOTAL_FORWARD_ENERGY = "total_forward_energy" TOTAL_TIME = "total_time" TOTAL_PM = "total_pm" + TOTAL_POWER = "total_power" TVOC = "tvoc" UPPER_TEMP = "upper_temp" UPPER_TEMP_F = "upper_temp_f" diff --git a/homeassistant/components/tuya/sensor.py b/homeassistant/components/tuya/sensor.py index 1468f90a452..78e3976a416 100644 --- a/homeassistant/components/tuya/sensor.py +++ b/homeassistant/components/tuya/sensor.py @@ -696,6 +696,20 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, ), + TuyaSensorEntityDescription( + key=DPCode.REVERSE_ENERGY_TOTAL, + translation_key="total_energy", + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + TuyaSensorEntityDescription( + key=DPCode.TOTAL_POWER, + translation_key="total_power", + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfPower.KILO_WATT, + subkey="power", + ), TuyaSensorEntityDescription( key=DPCode.PHASE_A, translation_key="phase_a_current", From 9002d85f9b7d33700435b758bf7c481cd0b64e04 Mon Sep 17 00:00:00 2001 From: tronikos Date: Sat, 22 Jun 2024 03:05:39 -0700 Subject: [PATCH 0987/1445] Support playback of videos in Fully Kiosk Browser (#119496) --- .../components/fully_kiosk/media_player.py | 28 ++++++++- .../fully_kiosk/test_media_player.py | 57 +++++++++++++++++++ 2 files changed, 82 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/fully_kiosk/media_player.py b/homeassistant/components/fully_kiosk/media_player.py index 1e258c928e7..ae61a39bb81 100644 --- a/homeassistant/components/fully_kiosk/media_player.py +++ b/homeassistant/components/fully_kiosk/media_player.py @@ -14,6 +14,7 @@ from homeassistant.components.media_player import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import AUDIOMANAGER_STREAM_MUSIC, DOMAIN, MEDIA_SUPPORT_FULLYKIOSK @@ -54,13 +55,33 @@ class FullyMediaPlayer(FullyKioskEntity, MediaPlayerEntity): ) media_id = async_process_play_media_url(self.hass, play_item.url) - await self.coordinator.fully.playSound(media_id, AUDIOMANAGER_STREAM_MUSIC) + if media_type.startswith("audio/"): + media_type = MediaType.MUSIC + elif media_type.startswith("video/"): + media_type = MediaType.VIDEO + if media_type == MediaType.MUSIC: + self._attr_media_content_type = MediaType.MUSIC + await self.coordinator.fully.playSound(media_id, AUDIOMANAGER_STREAM_MUSIC) + elif media_type == MediaType.VIDEO: + self._attr_media_content_type = MediaType.VIDEO + await self.coordinator.fully.sendCommand( + "playVideo", + url=media_id, + stream=AUDIOMANAGER_STREAM_MUSIC, + showControls=1, + exitOnCompletion=1, + ) + else: + raise HomeAssistantError(f"Unsupported media type {media_type}") self._attr_state = MediaPlayerState.PLAYING self.async_write_ha_state() async def async_media_stop(self) -> None: """Stop playing media.""" - await self.coordinator.fully.stopSound() + if self._attr_media_content_type == MediaType.VIDEO: + await self.coordinator.fully.sendCommand("stopVideo") + else: + await self.coordinator.fully.stopSound() self._attr_state = MediaPlayerState.IDLE self.async_write_ha_state() @@ -81,7 +102,8 @@ class FullyMediaPlayer(FullyKioskEntity, MediaPlayerEntity): return await media_source.async_browse_media( self.hass, media_content_id, - content_filter=lambda item: item.media_content_type.startswith("audio/"), + content_filter=lambda item: item.media_content_type.startswith("audio/") + or item.media_content_type.startswith("video/"), ) @callback diff --git a/tests/components/fully_kiosk/test_media_player.py b/tests/components/fully_kiosk/test_media_player.py index 4ee9b595a82..aa53421616f 100644 --- a/tests/components/fully_kiosk/test_media_player.py +++ b/tests/components/fully_kiosk/test_media_player.py @@ -2,11 +2,14 @@ from unittest.mock import MagicMock, Mock, patch +import pytest + from homeassistant.components import media_player from homeassistant.components.fully_kiosk.const import DOMAIN, MEDIA_SUPPORT_FULLYKIOSK from homeassistant.components.media_source import DOMAIN as MS_DOMAIN from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.setup import async_setup_component @@ -97,6 +100,60 @@ async def test_media_player( assert device_entry.sw_version == "1.42.5" +@pytest.mark.parametrize("media_content_type", ["video", "video/mp4"]) +async def test_media_player_video( + hass: HomeAssistant, + mock_fully_kiosk: MagicMock, + init_integration: MockConfigEntry, + media_content_type: str, +) -> None: + """Test Fully Kiosk media player for videos.""" + await hass.services.async_call( + media_player.DOMAIN, + "play_media", + { + ATTR_ENTITY_ID: "media_player.amazon_fire", + "media_content_type": media_content_type, + "media_content_id": "test.mp4", + }, + blocking=True, + ) + assert len(mock_fully_kiosk.sendCommand.mock_calls) == 1 + mock_fully_kiosk.sendCommand.assert_called_with( + "playVideo", url="test.mp4", stream=3, showControls=1, exitOnCompletion=1 + ) + + await hass.services.async_call( + media_player.DOMAIN, + "media_stop", + { + ATTR_ENTITY_ID: "media_player.amazon_fire", + }, + blocking=True, + ) + mock_fully_kiosk.sendCommand.assert_called_with("stopVideo") + + +async def test_media_player_unsupported( + hass: HomeAssistant, + mock_fully_kiosk: MagicMock, + init_integration: MockConfigEntry, +) -> None: + """Test Fully Kiosk media player for unsupported media.""" + with pytest.raises(HomeAssistantError) as error: + await hass.services.async_call( + media_player.DOMAIN, + "play_media", + { + ATTR_ENTITY_ID: "media_player.amazon_fire", + "media_content_type": "playlist", + "media_content_id": "test.m4u", + }, + blocking=True, + ) + assert error.value.args[0] == "Unsupported media type playlist" + + async def test_browse_media( hass: HomeAssistant, hass_ws_client: WebSocketGenerator, From 0feead385ac9c04224db82d37be08e3cfab5586e Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 22 Jun 2024 12:23:17 +0200 Subject: [PATCH 0988/1445] Add unique ID support to Flux (#120142) --- homeassistant/components/flux/switch.py | 7 +++++++ tests/components/flux/test_switch.py | 26 +++++++++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/homeassistant/components/flux/switch.py b/homeassistant/components/flux/switch.py index 63f58ff64c4..fac31d445cc 100644 --- a/homeassistant/components/flux/switch.py +++ b/homeassistant/components/flux/switch.py @@ -50,6 +50,8 @@ from homeassistant.util.dt import as_local, utcnow as dt_utcnow _LOGGER = logging.getLogger(__name__) +ATTR_UNIQUE_ID = "unique_id" + CONF_START_TIME = "start_time" CONF_STOP_TIME = "stop_time" CONF_START_CT = "start_colortemp" @@ -88,6 +90,7 @@ PLATFORM_SCHEMA = vol.Schema( ), vol.Optional(CONF_INTERVAL, default=30): cv.positive_int, vol.Optional(ATTR_TRANSITION, default=30): VALID_TRANSITION, + vol.Optional(ATTR_UNIQUE_ID): cv.string, } ) @@ -151,6 +154,7 @@ async def async_setup_platform( mode = config.get(CONF_MODE) interval = config.get(CONF_INTERVAL) transition = config.get(ATTR_TRANSITION) + unique_id = config.get(ATTR_UNIQUE_ID) flux = FluxSwitch( name, hass, @@ -165,6 +169,7 @@ async def async_setup_platform( mode, interval, transition, + unique_id, ) async_add_entities([flux]) @@ -194,6 +199,7 @@ class FluxSwitch(SwitchEntity, RestoreEntity): mode, interval, transition, + unique_id, ): """Initialize the Flux switch.""" self._name = name @@ -209,6 +215,7 @@ class FluxSwitch(SwitchEntity, RestoreEntity): self._mode = mode self._interval = interval self._transition = transition + self._attr_unique_id = unique_id self.unsub_tracker = None @property diff --git a/tests/components/flux/test_switch.py b/tests/components/flux/test_switch.py index baf568b79b4..ab85303584f 100644 --- a/tests/components/flux/test_switch.py +++ b/tests/components/flux/test_switch.py @@ -14,6 +14,7 @@ from homeassistant.const import ( SUN_EVENT_SUNRISE, ) from homeassistant.core import HomeAssistant, State +from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util @@ -52,6 +53,31 @@ async def test_valid_config(hass: HomeAssistant) -> None: assert state.state == "off" +async def test_unique_id( + hass: HomeAssistant, entity_registry: er.EntityRegistry +) -> None: + """Test configuration with unique ID.""" + assert await async_setup_component( + hass, + "switch", + { + "switch": { + "platform": "flux", + "name": "flux", + "lights": ["light.desk", "light.lamp"], + "unique_id": "zaphotbeeblebrox", + } + }, + ) + await hass.async_block_till_done() + state = hass.states.get("switch.flux") + assert state + assert state.state == "off" + + assert len(entity_registry.entities) == 1 + assert entity_registry.async_get_entity_id("switch", "flux", "zaphotbeeblebrox") + + async def test_restore_state_last_on(hass: HomeAssistant) -> None: """Test restoring state when the last state is on.""" mock_restore_cache(hass, [State("switch.flux", "on")]) From f676760ab1ff09103db60201d82669958fd88259 Mon Sep 17 00:00:00 2001 From: mletenay Date: Sat, 22 Jun 2024 12:27:32 +0200 Subject: [PATCH 0989/1445] Add GoodWe async_update support to number/select entities (#116739) --- homeassistant/components/goodwe/number.py | 5 +++++ homeassistant/components/goodwe/select.py | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/homeassistant/components/goodwe/number.py b/homeassistant/components/goodwe/number.py index d54fb8d8d0c..ce36bb35bf9 100644 --- a/homeassistant/components/goodwe/number.py +++ b/homeassistant/components/goodwe/number.py @@ -131,6 +131,11 @@ class InverterNumberEntity(NumberEntity): self._attr_native_value = float(current_value) self._inverter: Inverter = inverter + async def async_update(self) -> None: + """Get the current value from inverter.""" + value = await self.entity_description.getter(self._inverter) + self._attr_native_value = float(value) + async def async_set_native_value(self, value: float) -> None: """Set new value.""" await self.entity_description.setter(self._inverter, int(value)) diff --git a/homeassistant/components/goodwe/select.py b/homeassistant/components/goodwe/select.py index f42f50c93fc..4fa84c8401f 100644 --- a/homeassistant/components/goodwe/select.py +++ b/homeassistant/components/goodwe/select.py @@ -89,6 +89,11 @@ class InverterOperationModeEntity(SelectEntity): self._attr_current_option = current_mode self._inverter: Inverter = inverter + async def async_update(self) -> None: + """Get the current value from inverter.""" + value = await self._inverter.get_operation_mode() + self._attr_current_option = _MODE_TO_OPTION[value] + async def async_select_option(self, option: str) -> None: """Change the selected option.""" await self._inverter.set_operation_mode(_OPTION_TO_MODE[option]) From 57eb8dab6a344771d099ee570038c307e2e61d96 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Sat, 22 Jun 2024 12:28:41 +0200 Subject: [PATCH 0990/1445] Fix file yaml import fails on scan_interval (#120154) --- homeassistant/components/file/__init__.py | 10 +++++++++- tests/components/file/test_sensor.py | 1 + 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/file/__init__.py b/homeassistant/components/file/__init__.py index 9e91aa07103..aa3e241cc81 100644 --- a/homeassistant/components/file/__init__.py +++ b/homeassistant/components/file/__init__.py @@ -2,7 +2,13 @@ from homeassistant.components.notify import migrate_notify_issue from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import CONF_FILE_PATH, CONF_NAME, CONF_PLATFORM, Platform +from homeassistant.const import ( + CONF_FILE_PATH, + CONF_NAME, + CONF_PLATFORM, + CONF_SCAN_INTERVAL, + Platform, +) from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import ( @@ -63,6 +69,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: if item[CONF_PLATFORM] == DOMAIN: file_config_item = IMPORT_SCHEMA[domain](item) file_config_item[CONF_PLATFORM] = domain + if CONF_SCAN_INTERVAL in file_config_item: + del file_config_item[CONF_SCAN_INTERVAL] hass.async_create_task( hass.config_entries.flow.async_init( DOMAIN, diff --git a/tests/components/file/test_sensor.py b/tests/components/file/test_sensor.py index d2059f4d564..60a81df2b1e 100644 --- a/tests/components/file/test_sensor.py +++ b/tests/components/file/test_sensor.py @@ -21,6 +21,7 @@ async def test_file_value_yaml_setup( config = { "sensor": { "platform": "file", + "scan_interval": 30, "name": "file1", "file_path": get_fixture_path("file_value.txt", "file"), } From ad1f0db5a46f985e995d7287bf05471d7bfdf8a7 Mon Sep 17 00:00:00 2001 From: tronikos Date: Sat, 22 Jun 2024 03:35:48 -0700 Subject: [PATCH 0991/1445] Pass prompt as system_instruction for Gemini 1.5 models (#120147) --- .../conversation.py | 172 ++++++++-------- homeassistant/helpers/llm.py | 1 + .../snapshots/test_conversation.ambr | 192 +++++++++++++----- .../test_conversation.py | 29 ++- 4 files changed, 253 insertions(+), 141 deletions(-) diff --git a/homeassistant/components/google_generative_ai_conversation/conversation.py b/homeassistant/components/google_generative_ai_conversation/conversation.py index 65c0dc7fd93..b9f0006dbff 100644 --- a/homeassistant/components/google_generative_ai_conversation/conversation.py +++ b/homeassistant/components/google_generative_ai_conversation/conversation.py @@ -161,10 +161,14 @@ class GoogleGenerativeAIConversationEntity( self, user_input: conversation.ConversationInput ) -> conversation.ConversationResult: """Process a sentence.""" - intent_response = intent.IntentResponse(language=user_input.language) - llm_api: llm.APIInstance | None = None - tools: list[dict[str, Any]] | None = None - user_name: str | None = None + result = conversation.ConversationResult( + response=intent.IntentResponse(language=user_input.language), + conversation_id=user_input.conversation_id + if user_input.conversation_id in self.history + else ulid.ulid_now(), + ) + assert result.conversation_id + llm_context = llm.LLMContext( platform=DOMAIN, context=user_input.context, @@ -173,7 +177,8 @@ class GoogleGenerativeAIConversationEntity( assistant=conversation.DOMAIN, device_id=user_input.device_id, ) - + llm_api: llm.APIInstance | None = None + tools: list[dict[str, Any]] | None = None if self.entry.options.get(CONF_LLM_HASS_API): try: llm_api = await llm.async_get_api( @@ -183,17 +188,33 @@ class GoogleGenerativeAIConversationEntity( ) except HomeAssistantError as err: LOGGER.error("Error getting LLM API: %s", err) - intent_response.async_set_error( + result.response.async_set_error( intent.IntentResponseErrorCode.UNKNOWN, f"Error preparing LLM API: {err}", ) - return conversation.ConversationResult( - response=intent_response, conversation_id=user_input.conversation_id - ) + return result tools = [_format_tool(tool) for tool in llm_api.tools] + try: + prompt = await self._async_render_prompt(user_input, llm_api, llm_context) + except TemplateError as err: + LOGGER.error("Error rendering prompt: %s", err) + result.response.async_set_error( + intent.IntentResponseErrorCode.UNKNOWN, + f"Sorry, I had a problem with my template: {err}", + ) + return result + + model_name = self.entry.options.get(CONF_CHAT_MODEL, RECOMMENDED_CHAT_MODEL) + # Gemini 1.0 doesn't support system_instruction while 1.5 does. + # Assume future versions will support it (if not, the request fails with a + # clear message at which point we can fix). + supports_system_instruction = ( + "gemini-1.0" not in model_name and "gemini-pro" not in model_name + ) + model = genai.GenerativeModel( - model_name=self.entry.options.get(CONF_CHAT_MODEL, RECOMMENDED_CHAT_MODEL), + model_name=model_name, generation_config={ "temperature": self.entry.options.get( CONF_TEMPERATURE, RECOMMENDED_TEMPERATURE @@ -219,69 +240,25 @@ class GoogleGenerativeAIConversationEntity( ), }, tools=tools or None, + system_instruction=prompt if supports_system_instruction else None, ) - if user_input.conversation_id in self.history: - conversation_id = user_input.conversation_id - messages = self.history[conversation_id] - else: - conversation_id = ulid.ulid_now() - messages = [{}, {"role": "model", "parts": "Ok"}] - - if ( - user_input.context - and user_input.context.user_id - and ( - user := await self.hass.auth.async_get_user(user_input.context.user_id) - ) - ): - user_name = user.name - - try: - if llm_api: - api_prompt = llm_api.api_prompt - else: - api_prompt = llm.async_render_no_api_prompt(self.hass) - - prompt = "\n".join( - ( - template.Template( - llm.BASE_PROMPT - + self.entry.options.get( - CONF_PROMPT, llm.DEFAULT_INSTRUCTIONS_PROMPT - ), - self.hass, - ).async_render( - { - "ha_name": self.hass.config.location_name, - "user_name": user_name, - "llm_context": llm_context, - }, - parse_result=False, - ), - api_prompt, - ) - ) - - except TemplateError as err: - LOGGER.error("Error rendering prompt: %s", err) - intent_response.async_set_error( - intent.IntentResponseErrorCode.UNKNOWN, - f"Sorry, I had a problem with my template: {err}", - ) - return conversation.ConversationResult( - response=intent_response, conversation_id=conversation_id - ) - - # Make a copy, because we attach it to the trace event. - messages = [ - {"role": "user", "parts": prompt}, - *messages[1:], - ] + messages = self.history.get(result.conversation_id, []) + if not supports_system_instruction: + if not messages: + messages = [{}, {"role": "model", "parts": "Ok"}] + messages[0] = {"role": "user", "parts": prompt} LOGGER.debug("Input: '%s' with history: %s", user_input.text, messages) trace.async_conversation_trace_append( - trace.ConversationTraceEventType.AGENT_DETAIL, {"messages": messages} + trace.ConversationTraceEventType.AGENT_DETAIL, + { + # Make a copy to attach it to the trace event. + "messages": messages[:] + if supports_system_instruction + else messages[2:], + "prompt": prompt, + }, ) chat = model.start_chat(history=messages) @@ -307,24 +284,20 @@ class GoogleGenerativeAIConversationEntity( f"Sorry, I had a problem talking to Google Generative AI: {err}" ) - intent_response.async_set_error( + result.response.async_set_error( intent.IntentResponseErrorCode.UNKNOWN, error, ) - return conversation.ConversationResult( - response=intent_response, conversation_id=conversation_id - ) + return result LOGGER.debug("Response: %s", chat_response.parts) if not chat_response.parts: - intent_response.async_set_error( + result.response.async_set_error( intent.IntentResponseErrorCode.UNKNOWN, "Sorry, I had a problem getting a response from Google Generative AI.", ) - return conversation.ConversationResult( - response=intent_response, conversation_id=conversation_id - ) - self.history[conversation_id] = chat.history + return result + self.history[result.conversation_id] = chat.history function_calls = [ part.function_call for part in chat_response.parts if part.function_call ] @@ -355,9 +328,48 @@ class GoogleGenerativeAIConversationEntity( ) chat_request = protos.Content(parts=tool_responses) - intent_response.async_set_speech( + result.response.async_set_speech( " ".join([part.text.strip() for part in chat_response.parts if part.text]) ) - return conversation.ConversationResult( - response=intent_response, conversation_id=conversation_id + return result + + async def _async_render_prompt( + self, + user_input: conversation.ConversationInput, + llm_api: llm.APIInstance | None, + llm_context: llm.LLMContext, + ) -> str: + user_name: str | None = None + if ( + user_input.context + and user_input.context.user_id + and ( + user := await self.hass.auth.async_get_user(user_input.context.user_id) + ) + ): + user_name = user.name + + if llm_api: + api_prompt = llm_api.api_prompt + else: + api_prompt = llm.async_render_no_api_prompt(self.hass) + + return "\n".join( + ( + template.Template( + llm.BASE_PROMPT + + self.entry.options.get( + CONF_PROMPT, llm.DEFAULT_INSTRUCTIONS_PROMPT + ), + self.hass, + ).async_render( + { + "ha_name": self.hass.config.location_name, + "user_name": user_name, + "llm_context": llm_context, + }, + parse_result=False, + ), + api_prompt, + ) ) diff --git a/homeassistant/helpers/llm.py b/homeassistant/helpers/llm.py index 903e52af1a2..53ec092fda2 100644 --- a/homeassistant/helpers/llm.py +++ b/homeassistant/helpers/llm.py @@ -43,6 +43,7 @@ BASE_PROMPT = ( ) DEFAULT_INSTRUCTIONS_PROMPT = """You are a voice assistant for Home Assistant. +Answer questions about the world truthfully. Answer in plain text. Keep it simple and to the point. """ diff --git a/tests/components/google_generative_ai_conversation/snapshots/test_conversation.ambr b/tests/components/google_generative_ai_conversation/snapshots/test_conversation.ambr index 70db5d11868..aec8d088b20 100644 --- a/tests/components/google_generative_ai_conversation/snapshots/test_conversation.ambr +++ b/tests/components/google_generative_ai_conversation/snapshots/test_conversation.ambr @@ -1,5 +1,5 @@ # serializer version: 1 -# name: test_chat_history +# name: test_chat_history[models/gemini-1.0-pro-False] list([ tuple( '', @@ -12,13 +12,14 @@ 'top_k': 64, 'top_p': 0.95, }), - 'model_name': 'models/gemini-1.5-flash-latest', + 'model_name': 'models/gemini-1.0-pro', 'safety_settings': dict({ 'DANGEROUS': 'BLOCK_MEDIUM_AND_ABOVE', 'HARASSMENT': 'BLOCK_MEDIUM_AND_ABOVE', 'HATE': 'BLOCK_MEDIUM_AND_ABOVE', 'SEXUAL': 'BLOCK_MEDIUM_AND_ABOVE', }), + 'system_instruction': None, 'tools': None, }), ), @@ -32,6 +33,7 @@ 'parts': ''' Current time is 05:00:00. Today's date is 2024-05-24. You are a voice assistant for Home Assistant. + Answer questions about the world truthfully. Answer in plain text. Keep it simple and to the point. Only if the user wants to control a device, tell them to edit the AI configuration and allow access to Home Assistant. ''', @@ -63,13 +65,14 @@ 'top_k': 64, 'top_p': 0.95, }), - 'model_name': 'models/gemini-1.5-flash-latest', + 'model_name': 'models/gemini-1.0-pro', 'safety_settings': dict({ 'DANGEROUS': 'BLOCK_MEDIUM_AND_ABOVE', 'HARASSMENT': 'BLOCK_MEDIUM_AND_ABOVE', 'HATE': 'BLOCK_MEDIUM_AND_ABOVE', 'SEXUAL': 'BLOCK_MEDIUM_AND_ABOVE', }), + 'system_instruction': None, 'tools': None, }), ), @@ -83,6 +86,7 @@ 'parts': ''' Current time is 05:00:00. Today's date is 2024-05-24. You are a voice assistant for Home Assistant. + Answer questions about the world truthfully. Answer in plain text. Keep it simple and to the point. Only if the user wants to control a device, tell them to edit the AI configuration and allow access to Home Assistant. ''', @@ -113,6 +117,108 @@ ), ]) # --- +# name: test_chat_history[models/gemini-1.5-pro-True] + list([ + tuple( + '', + tuple( + ), + dict({ + 'generation_config': dict({ + 'max_output_tokens': 150, + 'temperature': 1.0, + 'top_k': 64, + 'top_p': 0.95, + }), + 'model_name': 'models/gemini-1.5-pro', + 'safety_settings': dict({ + 'DANGEROUS': 'BLOCK_MEDIUM_AND_ABOVE', + 'HARASSMENT': 'BLOCK_MEDIUM_AND_ABOVE', + 'HATE': 'BLOCK_MEDIUM_AND_ABOVE', + 'SEXUAL': 'BLOCK_MEDIUM_AND_ABOVE', + }), + 'system_instruction': ''' + Current time is 05:00:00. Today's date is 2024-05-24. + You are a voice assistant for Home Assistant. + Answer questions about the world truthfully. + Answer in plain text. Keep it simple and to the point. + Only if the user wants to control a device, tell them to edit the AI configuration and allow access to Home Assistant. + ''', + 'tools': None, + }), + ), + tuple( + '().start_chat', + tuple( + ), + dict({ + 'history': list([ + ]), + }), + ), + tuple( + '().start_chat().send_message_async', + tuple( + '1st user request', + ), + dict({ + }), + ), + tuple( + '', + tuple( + ), + dict({ + 'generation_config': dict({ + 'max_output_tokens': 150, + 'temperature': 1.0, + 'top_k': 64, + 'top_p': 0.95, + }), + 'model_name': 'models/gemini-1.5-pro', + 'safety_settings': dict({ + 'DANGEROUS': 'BLOCK_MEDIUM_AND_ABOVE', + 'HARASSMENT': 'BLOCK_MEDIUM_AND_ABOVE', + 'HATE': 'BLOCK_MEDIUM_AND_ABOVE', + 'SEXUAL': 'BLOCK_MEDIUM_AND_ABOVE', + }), + 'system_instruction': ''' + Current time is 05:00:00. Today's date is 2024-05-24. + You are a voice assistant for Home Assistant. + Answer questions about the world truthfully. + Answer in plain text. Keep it simple and to the point. + Only if the user wants to control a device, tell them to edit the AI configuration and allow access to Home Assistant. + ''', + 'tools': None, + }), + ), + tuple( + '().start_chat', + tuple( + ), + dict({ + 'history': list([ + dict({ + 'parts': '1st user request', + 'role': 'user', + }), + dict({ + 'parts': '1st model response', + 'role': 'model', + }), + ]), + }), + ), + tuple( + '().start_chat().send_message_async', + tuple( + '2nd user request', + ), + dict({ + }), + ), + ]) +# --- # name: test_default_prompt[config_entry_options0-None] list([ tuple( @@ -133,6 +239,13 @@ 'HATE': 'BLOCK_MEDIUM_AND_ABOVE', 'SEXUAL': 'BLOCK_MEDIUM_AND_ABOVE', }), + 'system_instruction': ''' + Current time is 05:00:00. Today's date is 2024-05-24. + You are a voice assistant for Home Assistant. + Answer questions about the world truthfully. + Answer in plain text. Keep it simple and to the point. + + ''', 'tools': None, }), ), @@ -142,19 +255,6 @@ ), dict({ 'history': list([ - dict({ - 'parts': ''' - Current time is 05:00:00. Today's date is 2024-05-24. - You are a voice assistant for Home Assistant. - Answer in plain text. Keep it simple and to the point. - - ''', - 'role': 'user', - }), - dict({ - 'parts': 'Ok', - 'role': 'model', - }), ]), }), ), @@ -188,6 +288,13 @@ 'HATE': 'BLOCK_MEDIUM_AND_ABOVE', 'SEXUAL': 'BLOCK_MEDIUM_AND_ABOVE', }), + 'system_instruction': ''' + Current time is 05:00:00. Today's date is 2024-05-24. + You are a voice assistant for Home Assistant. + Answer questions about the world truthfully. + Answer in plain text. Keep it simple and to the point. + + ''', 'tools': None, }), ), @@ -197,19 +304,6 @@ ), dict({ 'history': list([ - dict({ - 'parts': ''' - Current time is 05:00:00. Today's date is 2024-05-24. - You are a voice assistant for Home Assistant. - Answer in plain text. Keep it simple and to the point. - - ''', - 'role': 'user', - }), - dict({ - 'parts': 'Ok', - 'role': 'model', - }), ]), }), ), @@ -243,6 +337,13 @@ 'HATE': 'BLOCK_MEDIUM_AND_ABOVE', 'SEXUAL': 'BLOCK_MEDIUM_AND_ABOVE', }), + 'system_instruction': ''' + Current time is 05:00:00. Today's date is 2024-05-24. + You are a voice assistant for Home Assistant. + Answer questions about the world truthfully. + Answer in plain text. Keep it simple and to the point. + + ''', 'tools': None, }), ), @@ -252,19 +353,6 @@ ), dict({ 'history': list([ - dict({ - 'parts': ''' - Current time is 05:00:00. Today's date is 2024-05-24. - You are a voice assistant for Home Assistant. - Answer in plain text. Keep it simple and to the point. - - ''', - 'role': 'user', - }), - dict({ - 'parts': 'Ok', - 'role': 'model', - }), ]), }), ), @@ -298,6 +386,13 @@ 'HATE': 'BLOCK_MEDIUM_AND_ABOVE', 'SEXUAL': 'BLOCK_MEDIUM_AND_ABOVE', }), + 'system_instruction': ''' + Current time is 05:00:00. Today's date is 2024-05-24. + You are a voice assistant for Home Assistant. + Answer questions about the world truthfully. + Answer in plain text. Keep it simple and to the point. + + ''', 'tools': None, }), ), @@ -307,19 +402,6 @@ ), dict({ 'history': list([ - dict({ - 'parts': ''' - Current time is 05:00:00. Today's date is 2024-05-24. - You are a voice assistant for Home Assistant. - Answer in plain text. Keep it simple and to the point. - - ''', - 'role': 'user', - }), - dict({ - 'parts': 'Ok', - 'role': 'model', - }), ]), }), ), diff --git a/tests/components/google_generative_ai_conversation/test_conversation.py b/tests/components/google_generative_ai_conversation/test_conversation.py index e84efffe7df..7f4fe886e90 100644 --- a/tests/components/google_generative_ai_conversation/test_conversation.py +++ b/tests/components/google_generative_ai_conversation/test_conversation.py @@ -12,6 +12,9 @@ import voluptuous as vol from homeassistant.components import conversation from homeassistant.components.conversation import trace +from homeassistant.components.google_generative_ai_conversation.const import ( + CONF_CHAT_MODEL, +) from homeassistant.components.google_generative_ai_conversation.conversation import ( _escape_decode, ) @@ -99,13 +102,22 @@ async def test_default_prompt( assert mock_get_tools.called == (CONF_LLM_HASS_API in config_entry_options) +@pytest.mark.parametrize( + ("model_name", "supports_system_instruction"), + [("models/gemini-1.5-pro", True), ("models/gemini-1.0-pro", False)], +) async def test_chat_history( hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_init_component, + model_name: str, + supports_system_instruction: bool, snapshot: SnapshotAssertion, ) -> None: """Test that the agent keeps track of the chat history.""" + hass.config_entries.async_update_entry( + mock_config_entry, options={CONF_CHAT_MODEL: model_name} + ) with patch("google.generativeai.GenerativeModel") as mock_model: mock_chat = AsyncMock() mock_model.return_value.start_chat.return_value = mock_chat @@ -115,9 +127,14 @@ async def test_chat_history( mock_part.function_call = None mock_part.text = "1st model response" chat_response.parts = [mock_part] - mock_chat.history = [ - {"role": "user", "parts": "prompt"}, - {"role": "model", "parts": "Ok"}, + if supports_system_instruction: + mock_chat.history = [] + else: + mock_chat.history = [ + {"role": "user", "parts": "prompt"}, + {"role": "model", "parts": "Ok"}, + ] + mock_chat.history += [ {"role": "user", "parts": "1st user request"}, {"role": "model", "parts": "1st model response"}, ] @@ -256,7 +273,7 @@ async def test_function_call( ] # AGENT_DETAIL event contains the raw prompt passed to the model detail_event = trace_events[1] - assert "Answer in plain text" in detail_event["data"]["messages"][0]["parts"] + assert "Answer in plain text" in detail_event["data"]["prompt"] @patch( @@ -492,9 +509,9 @@ async def test_template_variables( ), result assert ( "The user name is Test User." - in mock_model.mock_calls[1][2]["history"][0]["parts"] + in mock_model.mock_calls[0][2]["system_instruction"] ) - assert "The user id is 12345." in mock_model.mock_calls[1][2]["history"][0]["parts"] + assert "The user id is 12345." in mock_model.mock_calls[0][2]["system_instruction"] async def test_conversation_agent( From 1a962b415e90c31a4f2b8bbbfda8349e16da68e7 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Sat, 22 Jun 2024 12:40:03 +0200 Subject: [PATCH 0992/1445] Add support to consider device holiday and summer mode in AVM Fritz!Smarthome (#119862) --- homeassistant/components/fritzbox/climate.py | 80 +++++++++--- homeassistant/components/fritzbox/icons.json | 16 +++ .../components/fritzbox/strings.json | 23 ++++ tests/components/fritzbox/__init__.py | 4 +- tests/components/fritzbox/test_climate.py | 114 +++++++++++++++++- 5 files changed, 213 insertions(+), 24 deletions(-) create mode 100644 homeassistant/components/fritzbox/icons.json diff --git a/homeassistant/components/fritzbox/climate.py b/homeassistant/components/fritzbox/climate.py index cfaa7a298ad..5288682c388 100644 --- a/homeassistant/components/fritzbox/climate.py +++ b/homeassistant/components/fritzbox/climate.py @@ -19,6 +19,7 @@ from homeassistant.const import ( UnitOfTemperature, ) from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import FritzBoxDeviceEntity @@ -27,18 +28,26 @@ from .const import ( ATTR_STATE_HOLIDAY_MODE, ATTR_STATE_SUMMER_MODE, ATTR_STATE_WINDOW_OPEN, + DOMAIN, LOGGER, ) -from .coordinator import FritzboxConfigEntry +from .coordinator import FritzboxConfigEntry, FritzboxDataUpdateCoordinator from .model import ClimateExtraAttributes -OPERATION_LIST = [HVACMode.HEAT, HVACMode.OFF] +HVAC_MODES = [HVACMode.HEAT, HVACMode.OFF] +PRESET_HOLIDAY = "holiday" +PRESET_SUMMER = "summer" +PRESET_MODES = [PRESET_ECO, PRESET_COMFORT] +SUPPORTED_FEATURES = ( + ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.PRESET_MODE + | ClimateEntityFeature.TURN_OFF + | ClimateEntityFeature.TURN_ON +) MIN_TEMPERATURE = 8 MAX_TEMPERATURE = 28 -PRESET_MANUAL = "manual" - # special temperatures for on/off in Fritz!Box API (modified by pyfritzhome) ON_API_TEMPERATURE = 127.0 OFF_API_TEMPERATURE = 126.5 @@ -76,15 +85,38 @@ class FritzboxThermostat(FritzBoxDeviceEntity, ClimateEntity): """The thermostat class for FRITZ!SmartHome thermostats.""" _attr_precision = PRECISION_HALVES - _attr_supported_features = ( - ClimateEntityFeature.TARGET_TEMPERATURE - | ClimateEntityFeature.PRESET_MODE - | ClimateEntityFeature.TURN_OFF - | ClimateEntityFeature.TURN_ON - ) _attr_temperature_unit = UnitOfTemperature.CELSIUS + _attr_translation_key = "thermostat" _enable_turn_on_off_backwards_compatibility = False + def __init__( + self, + coordinator: FritzboxDataUpdateCoordinator, + ain: str, + ) -> None: + """Initialize the thermostat.""" + self._attr_supported_features = SUPPORTED_FEATURES + self._attr_hvac_modes = HVAC_MODES + self._attr_preset_modes = PRESET_MODES + super().__init__(coordinator, ain) + + @callback + def async_write_ha_state(self) -> None: + """Write the state to the HASS state machine.""" + if self.data.holiday_active: + self._attr_supported_features = ClimateEntityFeature.PRESET_MODE + self._attr_hvac_modes = [HVACMode.HEAT] + self._attr_preset_modes = [PRESET_HOLIDAY] + elif self.data.summer_active: + self._attr_supported_features = ClimateEntityFeature.PRESET_MODE + self._attr_hvac_modes = [HVACMode.OFF] + self._attr_preset_modes = [PRESET_SUMMER] + else: + self._attr_supported_features = SUPPORTED_FEATURES + self._attr_hvac_modes = HVAC_MODES + self._attr_preset_modes = PRESET_MODES + return super().async_write_ha_state() + @property def current_temperature(self) -> float: """Return the current temperature.""" @@ -116,6 +148,10 @@ class FritzboxThermostat(FritzBoxDeviceEntity, ClimateEntity): @property def hvac_mode(self) -> HVACMode: """Return the current operation mode.""" + if self.data.holiday_active: + return HVACMode.HEAT + if self.data.summer_active: + return HVACMode.OFF if self.data.target_temperature in ( OFF_REPORT_SET_TEMPERATURE, OFF_API_TEMPERATURE, @@ -124,13 +160,13 @@ class FritzboxThermostat(FritzBoxDeviceEntity, ClimateEntity): return HVACMode.HEAT - @property - def hvac_modes(self) -> list[HVACMode]: - """Return the list of available operation modes.""" - return OPERATION_LIST - async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set new operation mode.""" + if self.data.holiday_active or self.data.summer_active: + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="change_hvac_while_active_mode", + ) if self.hvac_mode == hvac_mode: LOGGER.debug( "%s is already in requested hvac mode %s", self.name, hvac_mode @@ -144,19 +180,23 @@ class FritzboxThermostat(FritzBoxDeviceEntity, ClimateEntity): @property def preset_mode(self) -> str | None: """Return current preset mode.""" + if self.data.holiday_active: + return PRESET_HOLIDAY + if self.data.summer_active: + return PRESET_SUMMER if self.data.target_temperature == self.data.comfort_temperature: return PRESET_COMFORT if self.data.target_temperature == self.data.eco_temperature: return PRESET_ECO return None - @property - def preset_modes(self) -> list[str]: - """Return supported preset modes.""" - return [PRESET_ECO, PRESET_COMFORT] - async def async_set_preset_mode(self, preset_mode: str) -> None: """Set preset mode.""" + if self.data.holiday_active or self.data.summer_active: + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="change_preset_while_active_mode", + ) if preset_mode == PRESET_COMFORT: await self.async_set_temperature(temperature=self.data.comfort_temperature) elif preset_mode == PRESET_ECO: diff --git a/homeassistant/components/fritzbox/icons.json b/homeassistant/components/fritzbox/icons.json new file mode 100644 index 00000000000..5eb819cdde8 --- /dev/null +++ b/homeassistant/components/fritzbox/icons.json @@ -0,0 +1,16 @@ +{ + "entity": { + "climate": { + "thermostat": { + "state_attributes": { + "preset_mode": { + "state": { + "holiday": "mdi:bag-suitcase-outline", + "summer": "mdi:radiator-off" + } + } + } + } + } + } +} diff --git a/homeassistant/components/fritzbox/strings.json b/homeassistant/components/fritzbox/strings.json index 755cc97d7d8..cee0afa26c1 100644 --- a/homeassistant/components/fritzbox/strings.json +++ b/homeassistant/components/fritzbox/strings.json @@ -56,6 +56,21 @@ "device_lock": { "name": "Button lock via UI" }, "lock": { "name": "Button lock on device" } }, + "climate": { + "thermostat": { + "state_attributes": { + "preset_mode": { + "name": "[%key:component::climate::entity_component::_::state_attributes::preset_mode::name%]", + "state": { + "eco": "[%key:component::climate::entity_component::_::state_attributes::preset_mode::state::eco%]", + "comfort": "[%key:component::climate::entity_component::_::state_attributes::preset_mode::state::comfort%]", + "holiday": "Holiday", + "summer": "Summer" + } + } + } + } + }, "sensor": { "comfort_temperature": { "name": "Comfort temperature" }, "eco_temperature": { "name": "Eco temperature" }, @@ -64,5 +79,13 @@ "nextchange_time": { "name": "Next scheduled change time" }, "scheduled_preset": { "name": "Current scheduled preset" } } + }, + "exceptions": { + "change_preset_while_active_mode": { + "message": "Can't change preset while holiday or summer mode is active on the device." + }, + "change_hvac_while_active_mode": { + "message": "Can't change hvac mode while holiday or summer mode is active on the device." + } } } diff --git a/tests/components/fritzbox/__init__.py b/tests/components/fritzbox/__init__.py index 5fb9c853bf5..2bd8f26d73b 100644 --- a/tests/components/fritzbox/__init__.py +++ b/tests/components/fritzbox/__init__.py @@ -103,10 +103,10 @@ class FritzDeviceClimateMock(FritzEntityBaseMock): has_temperature_sensor = True has_thermostat = True has_blind = False - holiday_active = "fake_holiday" + holiday_active = False lock = "fake_locked" present = True - summer_active = "fake_summer" + summer_active = False target_temperature = 19.5 window_open = "fake_window" nextchange_temperature = 22.0 diff --git a/tests/components/fritzbox/test_climate.py b/tests/components/fritzbox/test_climate.py index 54d222c6899..8d1da9d09d5 100644 --- a/tests/components/fritzbox/test_climate.py +++ b/tests/components/fritzbox/test_climate.py @@ -3,6 +3,8 @@ from datetime import timedelta from unittest.mock import Mock, call +from freezegun.api import FrozenDateTimeFactory +import pytest from requests.exceptions import HTTPError from homeassistant.components.climate import ( @@ -21,6 +23,7 @@ from homeassistant.components.climate import ( SERVICE_SET_TEMPERATURE, HVACMode, ) +from homeassistant.components.fritzbox.climate import PRESET_HOLIDAY, PRESET_SUMMER from homeassistant.components.fritzbox.const import ( ATTR_STATE_BATTERY_LOW, ATTR_STATE_HOLIDAY_MODE, @@ -40,6 +43,7 @@ from homeassistant.const import ( UnitOfTemperature, ) from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError import homeassistant.util.dt as dt_util from . import FritzDeviceClimateMock, set_devices, setup_config_entry @@ -68,8 +72,8 @@ async def test_setup(hass: HomeAssistant, fritz: Mock) -> None: assert state.attributes[ATTR_PRESET_MODE] is None assert state.attributes[ATTR_PRESET_MODES] == [PRESET_ECO, PRESET_COMFORT] assert state.attributes[ATTR_STATE_BATTERY_LOW] is True - assert state.attributes[ATTR_STATE_HOLIDAY_MODE] == "fake_holiday" - assert state.attributes[ATTR_STATE_SUMMER_MODE] == "fake_summer" + assert state.attributes[ATTR_STATE_HOLIDAY_MODE] is False + assert state.attributes[ATTR_STATE_SUMMER_MODE] is False assert state.attributes[ATTR_STATE_WINDOW_OPEN] == "fake_window" assert state.attributes[ATTR_TEMPERATURE] == 19.5 assert ATTR_STATE_CLASS not in state.attributes @@ -444,3 +448,109 @@ async def test_discover_new_device(hass: HomeAssistant, fritz: Mock) -> None: state = hass.states.get(f"{DOMAIN}.new_climate") assert state + + +async def test_holidy_summer_mode( + hass: HomeAssistant, freezer: FrozenDateTimeFactory, fritz: Mock +) -> None: + """Test holiday and summer mode.""" + device = FritzDeviceClimateMock() + assert await setup_config_entry( + hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz + ) + + # initial state + state = hass.states.get(ENTITY_ID) + assert state + assert state.attributes[ATTR_STATE_HOLIDAY_MODE] is False + assert state.attributes[ATTR_STATE_SUMMER_MODE] is False + assert state.attributes[ATTR_HVAC_MODES] == [HVACMode.HEAT, HVACMode.OFF] + assert state.attributes[ATTR_PRESET_MODE] is None + assert state.attributes[ATTR_PRESET_MODES] == [PRESET_ECO, PRESET_COMFORT] + + # test holiday mode + device.holiday_active = True + device.summer_active = False + freezer.tick(timedelta(seconds=200)) + async_fire_time_changed(hass) + await hass.async_block_till_done(wait_background_tasks=True) + + state = hass.states.get(ENTITY_ID) + assert state + assert state.attributes[ATTR_STATE_HOLIDAY_MODE] + assert state.attributes[ATTR_STATE_SUMMER_MODE] is False + assert state.attributes[ATTR_HVAC_MODES] == [HVACMode.HEAT] + assert state.attributes[ATTR_PRESET_MODE] == PRESET_HOLIDAY + assert state.attributes[ATTR_PRESET_MODES] == [PRESET_HOLIDAY] + + with pytest.raises( + HomeAssistantError, + match="Can't change hvac mode while holiday or summer mode is active on the device", + ): + await hass.services.async_call( + "climate", + SERVICE_SET_HVAC_MODE, + {"entity_id": ENTITY_ID, ATTR_HVAC_MODE: HVACMode.HEAT}, + blocking=True, + ) + with pytest.raises( + HomeAssistantError, + match="Can't change preset while holiday or summer mode is active on the device", + ): + await hass.services.async_call( + "climate", + SERVICE_SET_PRESET_MODE, + {"entity_id": ENTITY_ID, ATTR_PRESET_MODE: PRESET_HOLIDAY}, + blocking=True, + ) + + # test summer mode + device.holiday_active = False + device.summer_active = True + freezer.tick(timedelta(seconds=200)) + async_fire_time_changed(hass) + await hass.async_block_till_done(wait_background_tasks=True) + + state = hass.states.get(ENTITY_ID) + assert state + assert state.attributes[ATTR_STATE_HOLIDAY_MODE] is False + assert state.attributes[ATTR_STATE_SUMMER_MODE] + assert state.attributes[ATTR_HVAC_MODES] == [HVACMode.OFF] + assert state.attributes[ATTR_PRESET_MODE] == PRESET_SUMMER + assert state.attributes[ATTR_PRESET_MODES] == [PRESET_SUMMER] + + with pytest.raises( + HomeAssistantError, + match="Can't change hvac mode while holiday or summer mode is active on the device", + ): + await hass.services.async_call( + "climate", + SERVICE_SET_HVAC_MODE, + {"entity_id": ENTITY_ID, ATTR_HVAC_MODE: HVACMode.HEAT}, + blocking=True, + ) + with pytest.raises( + HomeAssistantError, + match="Can't change preset while holiday or summer mode is active on the device", + ): + await hass.services.async_call( + "climate", + SERVICE_SET_PRESET_MODE, + {"entity_id": ENTITY_ID, ATTR_PRESET_MODE: PRESET_SUMMER}, + blocking=True, + ) + + # back to normal state + device.holiday_active = False + device.summer_active = False + freezer.tick(timedelta(seconds=200)) + async_fire_time_changed(hass) + await hass.async_block_till_done(wait_background_tasks=True) + + state = hass.states.get(ENTITY_ID) + assert state + assert state.attributes[ATTR_STATE_HOLIDAY_MODE] is False + assert state.attributes[ATTR_STATE_SUMMER_MODE] is False + assert state.attributes[ATTR_HVAC_MODES] == [HVACMode.HEAT, HVACMode.OFF] + assert state.attributes[ATTR_PRESET_MODE] is None + assert state.attributes[ATTR_PRESET_MODES] == [PRESET_ECO, PRESET_COMFORT] From cb045a794d414d9546832ba5c2327c5554f34f41 Mon Sep 17 00:00:00 2001 From: Alexandre CUER Date: Sat, 22 Jun 2024 12:41:54 +0200 Subject: [PATCH 0993/1445] Add coordinator to emoncms (#120008) --- .../components/emoncms/coordinator.py | 31 +++++++++++ homeassistant/components/emoncms/sensor.py | 52 +++++++++++-------- 2 files changed, 61 insertions(+), 22 deletions(-) create mode 100644 homeassistant/components/emoncms/coordinator.py diff --git a/homeassistant/components/emoncms/coordinator.py b/homeassistant/components/emoncms/coordinator.py new file mode 100644 index 00000000000..16258a11f4d --- /dev/null +++ b/homeassistant/components/emoncms/coordinator.py @@ -0,0 +1,31 @@ +"""DataUpdateCoordinator for the emoncms integration.""" + +from datetime import timedelta +import logging +from typing import Any + +from pyemoncms import EmoncmsClient + +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +_LOGGER = logging.getLogger(__name__) + + +class EmoncmsCoordinator(DataUpdateCoordinator[list[dict[str, Any]] | None]): + """Emoncms Data Update Coordinator.""" + + def __init__( + self, + hass: HomeAssistant, + emoncms_client: EmoncmsClient, + scan_interval: timedelta, + ) -> None: + """Initialize the emoncms data coordinator.""" + super().__init__( + hass, + _LOGGER, + name="emoncms_coordinator", + update_method=emoncms_client.async_list_feeds, + update_interval=scan_interval, + ) diff --git a/homeassistant/components/emoncms/sensor.py b/homeassistant/components/emoncms/sensor.py index 9208aa2a682..97c69619fa9 100644 --- a/homeassistant/components/emoncms/sensor.py +++ b/homeassistant/components/emoncms/sensor.py @@ -2,6 +2,7 @@ from __future__ import annotations +from datetime import timedelta import logging from typing import Any @@ -17,18 +18,22 @@ from homeassistant.components.sensor import ( from homeassistant.const import ( CONF_API_KEY, CONF_ID, + CONF_SCAN_INTERVAL, CONF_UNIT_OF_MEASUREMENT, CONF_URL, CONF_VALUE_TEMPLATE, STATE_UNKNOWN, UnitOfPower, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import template from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .coordinator import EmoncmsCoordinator _LOGGER = logging.getLogger(__name__) @@ -84,19 +89,21 @@ async def async_setup_platform( exclude_feeds = config.get(CONF_EXCLUDE_FEEDID) include_only_feeds = config.get(CONF_ONLY_INCLUDE_FEEDID) sensor_names = config.get(CONF_SENSOR_NAMES) + scan_interval = config.get(CONF_SCAN_INTERVAL, timedelta(seconds=30)) if value_template is not None: value_template.hass = hass emoncms_client = EmoncmsClient(url, apikey, session=async_get_clientsession(hass)) - elems = await emoncms_client.async_list_feeds() - + coordinator = EmoncmsCoordinator(hass, emoncms_client, scan_interval) + await coordinator.async_refresh() + elems = coordinator.data if elems is None: return - sensors = [] + sensors: list[EmonCmsSensor] = [] - for elem in elems: + for idx, elem in enumerate(elems): if exclude_feeds is not None and int(elem["id"]) in exclude_feeds: continue @@ -114,48 +121,48 @@ async def async_setup_platform( sensors.append( EmonCmsSensor( - hass, - emoncms_client, + coordinator, name, value_template, unit_of_measurement, str(sensorid), - elem, + idx, ) ) async_add_entities(sensors) -class EmonCmsSensor(SensorEntity): +class EmonCmsSensor(CoordinatorEntity[EmoncmsCoordinator], SensorEntity): """Implementation of an Emoncms sensor.""" def __init__( self, - hass: HomeAssistant, - emoncms_client: EmoncmsClient, + coordinator: EmoncmsCoordinator, name: str | None, value_template: template.Template | None, unit_of_measurement: str | None, sensorid: str, - elem: dict[str, Any], + idx: int, ) -> None: """Initialize the sensor.""" + super().__init__(coordinator) + self.idx = idx + elem = {} + if self.coordinator.data: + elem = self.coordinator.data[self.idx] if name is None: # Suppress ID in sensor name if it's 1, since most people won't # have more than one EmonCMS source and it's redundant to show the # ID if there's only one. id_for_name = "" if str(sensorid) == "1" else sensorid # Use the feed name assigned in EmonCMS or fall back to the feed ID - feed_name = elem.get("name") or f"Feed {elem['id']}" + feed_name = elem.get("name", f"Feed {elem.get('id')}") self._attr_name = f"EmonCMS{id_for_name} {feed_name}" else: self._attr_name = name - self._hass = hass - self._emoncms_client = emoncms_client self._value_template = value_template self._attr_native_unit_of_measurement = unit_of_measurement self._sensorid = sensorid - self._feed_id = elem["id"] if unit_of_measurement in ("kWh", "Wh"): self._attr_device_class = SensorDeviceClass.ENERGY @@ -208,9 +215,10 @@ class EmonCmsSensor(SensorEntity): elif elem["value"] is not None: self._attr_native_value = round(float(elem["value"]), DECIMALS) - async def async_update(self) -> None: - """Get the latest data and updates the state.""" - elem = await self._emoncms_client.async_get_feed_fields(self._feed_id) - if elem is None: - return - self._update_attributes(elem) + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + data = self.coordinator.data + if data: + self._update_attributes(data[self.idx]) + super()._handle_coordinator_update() From f8c171075302d4ff3e761f34d3ef277d3a66b6fa Mon Sep 17 00:00:00 2001 From: cRemE-fReSh <42785404+cRemE-fReSh@users.noreply.github.com> Date: Sat, 22 Jun 2024 12:42:32 +0200 Subject: [PATCH 0994/1445] Add Tuya pool heating pumps (#118415) Co-authored-by: Franck Nijhof Co-authored-by: Franck Nijhof --- homeassistant/components/tuya/number.py | 8 ++++++++ homeassistant/components/tuya/sensor.py | 9 +++++++++ homeassistant/components/tuya/switch.py | 7 +++++++ 3 files changed, 24 insertions(+) diff --git a/homeassistant/components/tuya/number.py b/homeassistant/components/tuya/number.py index 424450c7fec..d7614fb837a 100644 --- a/homeassistant/components/tuya/number.py +++ b/homeassistant/components/tuya/number.py @@ -277,6 +277,14 @@ NUMBERS: dict[str, tuple[NumberEntityDescription, ...]] = { device_class=NumberDeviceClass.TEMPERATURE, ), ), + # Pool HeatPump + "znrb": ( + NumberEntityDescription( + key=DPCode.TEMP_SET, + translation_key="temperature", + device_class=NumberDeviceClass.TEMPERATURE, + ), + ), } diff --git a/homeassistant/components/tuya/sensor.py b/homeassistant/components/tuya/sensor.py index 78e3976a416..5b6a4ed053e 100644 --- a/homeassistant/components/tuya/sensor.py +++ b/homeassistant/components/tuya/sensor.py @@ -1132,6 +1132,15 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { suggested_unit_of_measurement=UnitOfPower.WATT, ), ), + # Pool HeatPump + "znrb": ( + TuyaSensorEntityDescription( + key=DPCode.TEMP_CURRENT, + translation_key="temperature", + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + ), + ), } # Socket (duplicate of `kg`) diff --git a/homeassistant/components/tuya/switch.py b/homeassistant/components/tuya/switch.py index 2d5092d42b2..3039462be61 100644 --- a/homeassistant/components/tuya/switch.py +++ b/homeassistant/components/tuya/switch.py @@ -672,6 +672,13 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { entity_category=EntityCategory.CONFIG, ), ), + # Pool HeatPump + "znrb": ( + SwitchEntityDescription( + key=DPCode.SWITCH, + translation_key="switch", + ), + ), } # Socket (duplicate of `pc`) From f258034f9c4298e1ac1e3fc0b0f87231e89a5c77 Mon Sep 17 00:00:00 2001 From: David Symonds Date: Sat, 22 Jun 2024 20:42:46 +1000 Subject: [PATCH 0995/1445] Support todoist task description in new_task service (#116203) --- homeassistant/components/todoist/calendar.py | 3 +++ homeassistant/components/todoist/services.yaml | 3 +++ homeassistant/components/todoist/strings.json | 4 ++++ 3 files changed, 10 insertions(+) diff --git a/homeassistant/components/todoist/calendar.py b/homeassistant/components/todoist/calendar.py index 9b8d0a7c08f..e3f87043e02 100644 --- a/homeassistant/components/todoist/calendar.py +++ b/homeassistant/components/todoist/calendar.py @@ -66,6 +66,7 @@ _LOGGER = logging.getLogger(__name__) NEW_TASK_SERVICE_SCHEMA = vol.Schema( { vol.Required(CONTENT): cv.string, + vol.Optional(DESCRIPTION): cv.string, vol.Optional(PROJECT_NAME, default="inbox"): vol.All(cv.string, vol.Lower), vol.Optional(LABELS): cv.ensure_list_csv, vol.Optional(ASSIGNEE): cv.string, @@ -225,6 +226,8 @@ def async_register_services( content = call.data[CONTENT] data: dict[str, Any] = {"project_id": project_id} + if description := call.data.get(DESCRIPTION): + data["description"] = description if task_labels := call.data.get(LABELS): data["labels"] = task_labels diff --git a/homeassistant/components/todoist/services.yaml b/homeassistant/components/todoist/services.yaml index 9593b6bb6a4..1bd6320ebe3 100644 --- a/homeassistant/components/todoist/services.yaml +++ b/homeassistant/components/todoist/services.yaml @@ -5,6 +5,9 @@ new_task: example: Pick up the mail. selector: text: + description: + selector: + text: project: example: Errands default: Inbox diff --git a/homeassistant/components/todoist/strings.json b/homeassistant/components/todoist/strings.json index 0f81702a4d0..0cc74c9c8c6 100644 --- a/homeassistant/components/todoist/strings.json +++ b/homeassistant/components/todoist/strings.json @@ -29,6 +29,10 @@ "name": "Content", "description": "The name of the task." }, + "description": { + "name": "Description", + "description": "A description for the task." + }, "project": { "name": "Project", "description": "The name of the project this task should belong to." From 6e32a96ff3c5fc67b739e29ca065cc2238c33dc0 Mon Sep 17 00:00:00 2001 From: dougiteixeira <31328123+dougiteixeira@users.noreply.github.com> Date: Sat, 22 Jun 2024 07:45:06 -0300 Subject: [PATCH 0996/1445] Add the ability to bind the template helper entity to a device (#117753) --- homeassistant/components/template/__init__.py | 12 +- .../components/template/binary_sensor.py | 9 +- .../components/template/config_flow.py | 3 + homeassistant/components/template/sensor.py | 9 +- .../components/template/strings.json | 16 ++ homeassistant/helpers/device.py | 17 +- .../components/template/test_binary_sensor.py | 39 ++- tests/components/template/test_config_flow.py | 232 ++++++++++++++++++ tests/components/template/test_init.py | 90 ++++++- tests/components/template/test_sensor.py | 39 ++- tests/helpers/test_device.py | 15 ++ 11 files changed, 472 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/template/__init__.py b/homeassistant/components/template/__init__.py index f881e61fb76..efa99342699 100644 --- a/homeassistant/components/template/__init__.py +++ b/homeassistant/components/template/__init__.py @@ -7,10 +7,13 @@ import logging from homeassistant import config as conf_util from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_UNIQUE_ID, SERVICE_RELOAD +from homeassistant.const import CONF_DEVICE_ID, CONF_UNIQUE_ID, SERVICE_RELOAD from homeassistant.core import Event, HomeAssistant, ServiceCall from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import discovery +from homeassistant.helpers.device import ( + async_remove_stale_devices_links_keep_current_device, +) from homeassistant.helpers.reload import async_reload_integration_platforms from homeassistant.helpers.service import async_register_admin_service from homeassistant.helpers.typing import ConfigType @@ -57,6 +60,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a config entry.""" + + async_remove_stale_devices_links_keep_current_device( + hass, + entry.entry_id, + entry.options.get(CONF_DEVICE_ID), + ) + await hass.config_entries.async_forward_entry_setups( entry, (entry.options["template_type"],) ) diff --git a/homeassistant/components/template/binary_sensor.py b/homeassistant/components/template/binary_sensor.py index 920b2090c47..0fa588a78f1 100644 --- a/homeassistant/components/template/binary_sensor.py +++ b/homeassistant/components/template/binary_sensor.py @@ -22,6 +22,7 @@ from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, CONF_DEVICE_CLASS, + CONF_DEVICE_ID, CONF_ENTITY_PICTURE_TEMPLATE, CONF_FRIENDLY_NAME, CONF_FRIENDLY_NAME_TEMPLATE, @@ -39,8 +40,9 @@ from homeassistant.const import ( ) from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.exceptions import TemplateError -from homeassistant.helpers import template +from homeassistant.helpers import selector, template import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.device import async_device_info_to_link_from_device_id from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_call_later, async_track_point_in_utc_time @@ -86,6 +88,7 @@ BINARY_SENSOR_SCHEMA = vol.Schema( vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, vol.Required(CONF_STATE): cv.template, vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, + vol.Optional(CONF_DEVICE_ID): selector.DeviceSelector(), } ).extend(TEMPLATE_ENTITY_COMMON_SCHEMA.schema) @@ -244,6 +247,10 @@ class BinarySensorTemplate(TemplateEntity, BinarySensorEntity, RestoreEntity): self._delay_on_raw = config.get(CONF_DELAY_ON) self._delay_off = None self._delay_off_raw = config.get(CONF_DELAY_OFF) + self._attr_device_info = async_device_info_to_link_from_device_id( + hass, + config.get(CONF_DEVICE_ID), + ) async def async_added_to_hass(self) -> None: """Restore state.""" diff --git a/homeassistant/components/template/config_flow.py b/homeassistant/components/template/config_flow.py index 8a5ecca5b4b..5c28a68a8ae 100644 --- a/homeassistant/components/template/config_flow.py +++ b/homeassistant/components/template/config_flow.py @@ -18,6 +18,7 @@ from homeassistant.components.sensor import ( ) from homeassistant.const import ( CONF_DEVICE_CLASS, + CONF_DEVICE_ID, CONF_NAME, CONF_STATE, CONF_UNIT_OF_MEASUREMENT, @@ -95,6 +96,8 @@ def generate_schema(domain: str, flow_type: str) -> dict[vol.Marker, Any]: ), } + schema[vol.Optional(CONF_DEVICE_ID)] = selector.DeviceSelector() + return schema diff --git a/homeassistant/components/template/sensor.py b/homeassistant/components/template/sensor.py index 171a8667d8f..6cb73a15632 100644 --- a/homeassistant/components/template/sensor.py +++ b/homeassistant/components/template/sensor.py @@ -25,6 +25,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_ENTITY_ID, CONF_DEVICE_CLASS, + CONF_DEVICE_ID, CONF_ENTITY_PICTURE_TEMPLATE, CONF_FRIENDLY_NAME, CONF_FRIENDLY_NAME_TEMPLATE, @@ -40,7 +41,8 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import TemplateError -from homeassistant.helpers import config_validation as cv, template +from homeassistant.helpers import config_validation as cv, selector, template +from homeassistant.helpers.device import async_device_info_to_link_from_device_id from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.trigger_template_entity import TEMPLATE_SENSOR_BASE_SCHEMA @@ -86,6 +88,7 @@ SENSOR_SCHEMA = vol.All( { vol.Required(CONF_STATE): cv.template, vol.Optional(ATTR_LAST_RESET): cv.template, + vol.Optional(CONF_DEVICE_ID): selector.DeviceSelector(), } ) .extend(TEMPLATE_SENSOR_BASE_SCHEMA.schema) @@ -260,6 +263,10 @@ class SensorTemplate(TemplateEntity, SensorEntity): self._attr_last_reset_template: template.Template | None = config.get( ATTR_LAST_RESET ) + self._attr_device_info = async_device_info_to_link_from_device_id( + hass, + config.get(CONF_DEVICE_ID), + ) if (object_id := config.get(CONF_OBJECT_ID)) is not None: self.entity_id = async_generate_entity_id( ENTITY_ID_FORMAT, object_id, hass=hass diff --git a/homeassistant/components/template/strings.json b/homeassistant/components/template/strings.json index f5958ec550e..4a1377cbf0b 100644 --- a/homeassistant/components/template/strings.json +++ b/homeassistant/components/template/strings.json @@ -3,20 +3,28 @@ "step": { "binary_sensor": { "data": { + "device_id": "[%key:common::config_flow::data::device%]", "device_class": "[%key:component::template::config::step::sensor::data::device_class%]", "name": "[%key:common::config_flow::data::name%]", "state": "[%key:component::template::config::step::sensor::data::state%]" }, + "data_description": { + "device_id": "[%key:component::template::config::step::sensor::data_description::device_id%]" + }, "title": "Template binary sensor" }, "sensor": { "data": { + "device_id": "[%key:common::config_flow::data::device%]", "device_class": "Device class", "name": "[%key:common::config_flow::data::name%]", "state_class": "[%key:component::sensor::entity_component::_::state_attributes::state_class::name%]", "state": "State template", "unit_of_measurement": "Unit of measurement" }, + "data_description": { + "device_id": "Select a device to link to this entity." + }, "title": "Template sensor" }, "user": { @@ -33,17 +41,25 @@ "step": { "binary_sensor": { "data": { + "device_id": "[%key:common::config_flow::data::device%]", "state": "[%key:component::template::config::step::sensor::data::state%]" }, + "data_description": { + "device_id": "[%key:component::template::config::step::sensor::data_description::device_id%]" + }, "title": "[%key:component::template::config::step::binary_sensor::title%]" }, "sensor": { "data": { + "device_id": "[%key:common::config_flow::data::device%]", "device_class": "[%key:component::template::config::step::sensor::data::device_class%]", "state_class": "[%key:component::sensor::entity_component::_::state_attributes::state_class::name%]", "state": "[%key:component::template::config::step::sensor::data::state%]", "unit_of_measurement": "[%key:component::template::config::step::sensor::data::unit_of_measurement%]" }, + "data_description": { + "device_id": "[%key:component::template::config::step::sensor::data_description::device_id%]" + }, "title": "[%key:component::template::config::step::sensor::title%]" } } diff --git a/homeassistant/helpers/device.py b/homeassistant/helpers/device.py index b9df721ec6c..e1b9ded5723 100644 --- a/homeassistant/helpers/device.py +++ b/homeassistant/helpers/device.py @@ -28,11 +28,22 @@ def async_device_info_to_link_from_entity( ) -> dr.DeviceInfo | None: """DeviceInfo with information to link a device to a configuration entry in the link category from a entity id or entity uuid.""" + return async_device_info_to_link_from_device_id( + hass, + async_entity_id_to_device_id(hass, entity_id_or_uuid), + ) + + +@callback +def async_device_info_to_link_from_device_id( + hass: HomeAssistant, + device_id: str | None, +) -> dr.DeviceInfo | None: + """DeviceInfo with information to link a device to a configuration entry in the link category from a device id.""" + dev_reg = dr.async_get(hass) - if (device_id := async_entity_id_to_device_id(hass, entity_id_or_uuid)) is None or ( - device := dev_reg.async_get(device_id=device_id) - ) is None: + if device_id is None or (device := dev_reg.async_get(device_id=device_id)) is None: return None return dr.DeviceInfo( diff --git a/tests/components/template/test_binary_sensor.py b/tests/components/template/test_binary_sensor.py index 63d9b338eaa..ab74e4dec0d 100644 --- a/tests/components/template/test_binary_sensor.py +++ b/tests/components/template/test_binary_sensor.py @@ -19,7 +19,7 @@ from homeassistant.const import ( STATE_UNKNOWN, ) from homeassistant.core import Context, CoreState, HomeAssistant, State -from homeassistant.helpers import entity_registry as er +from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.entity_component import async_update_entity from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util @@ -1403,3 +1403,40 @@ async def test_trigger_entity_restore_state_auto_off_expired( state = hass.states.get("binary_sensor.test") assert state.state == OFF + + +async def test_device_id(hass: HomeAssistant) -> None: + """Test for device for Template.""" + device_registry = dr.async_get(hass) + entity_registry = er.async_get(hass) + + device_config_entry = MockConfigEntry() + device_config_entry.add_to_hass(hass) + device_entry = device_registry.async_get_or_create( + config_entry_id=device_config_entry.entry_id, + identifiers={("sensor", "identifier_test")}, + connections={("mac", "30:31:32:33:34:35")}, + ) + await hass.async_block_till_done() + assert device_entry is not None + assert device_entry.id is not None + + template_config_entry = MockConfigEntry( + data={}, + domain=template.DOMAIN, + options={ + "name": "My template", + "state": "{{10 > 8}}", + "template_type": "binary_sensor", + "device_id": device_entry.id, + }, + title="My template", + ) + template_config_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(template_config_entry.entry_id) + await hass.async_block_till_done() + + template_entity = entity_registry.async_get("binary_sensor.my_template") + assert template_entity is not None + assert template_entity.device_id == device_entry.id diff --git a/tests/components/template/test_config_flow.py b/tests/components/template/test_config_flow.py index 591fe877cc2..40f0c2da0e8 100644 --- a/tests/components/template/test_config_flow.py +++ b/tests/components/template/test_config_flow.py @@ -11,6 +11,7 @@ from homeassistant.components.template import DOMAIN, async_setup_entry from homeassistant.const import STATE_UNAVAILABLE from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers import device_registry as dr from tests.common import MockConfigEntry from tests.typing import WebSocketGenerator @@ -122,6 +123,91 @@ async def test_config_flow( assert state.attributes[key] == extra_attrs[key] +@pytest.mark.parametrize( + ( + "template_type", + "state_template", + ), + [ + ( + "sensor", + "{{ 15 }}", + ), + ( + "binary_sensor", + "{{ false }}", + ), + ], +) +async def test_config_flow_device( + hass: HomeAssistant, + template_type: str, + state_template: str, +) -> None: + """Test remove the device registry configuration entry when the device changes.""" + + device_registry = dr.async_get(hass) + + # Configure a device registry + entry_device = MockConfigEntry() + entry_device.add_to_hass(hass) + device = device_registry.async_get_or_create( + config_entry_id=entry_device.entry_id, + identifiers={("test", "identifier_test1")}, + connections={("mac", "20:31:32:33:34:01")}, + ) + await hass.async_block_till_done() + + device_id = device.id + assert device_id is not None + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] is FlowResultType.MENU + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"next_step_id": template_type}, + ) + await hass.async_block_till_done() + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == template_type + + with patch( + "homeassistant.components.template.async_setup_entry", wraps=async_setup_entry + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "name": "My template", + "state": state_template, + "device_id": device_id, + }, + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "My template" + assert result["data"] == {} + assert result["options"] == { + "name": "My template", + "state": state_template, + "template_type": template_type, + "device_id": device_id, + } + assert len(mock_setup_entry.mock_calls) == 1 + + config_entry = hass.config_entries.async_entries(DOMAIN)[0] + assert config_entry.data == {} + assert config_entry.options == { + "name": "My template", + "state": state_template, + "template_type": template_type, + "device_id": device_id, + } + + def get_suggested(schema, key): """Get suggested value for key in voluptuous schema.""" for k in schema: @@ -852,3 +938,149 @@ async def test_option_flow_sensor_preview_config_entry_removed( msg = await client.receive_json() assert not msg["success"] assert msg["error"] == {"code": "home_assistant_error", "message": "Unknown error"} + + +@pytest.mark.parametrize( + ( + "template_type", + "state_template", + ), + [ + ( + "sensor", + "{{ 15 }}", + ), + ( + "binary_sensor", + "{{ false }}", + ), + ], +) +async def test_options_flow_change_device( + hass: HomeAssistant, + template_type: str, + state_template: str, +) -> None: + """Test remove the device registry configuration entry when the device changes.""" + + device_registry = dr.async_get(hass) + + # Configure a device registry + entry_device1 = MockConfigEntry() + entry_device1.add_to_hass(hass) + device1 = device_registry.async_get_or_create( + config_entry_id=entry_device1.entry_id, + identifiers={("test", "identifier_test1")}, + connections={("mac", "20:31:32:33:34:01")}, + ) + entry_device2 = MockConfigEntry() + entry_device2.add_to_hass(hass) + device2 = device_registry.async_get_or_create( + config_entry_id=entry_device1.entry_id, + identifiers={("test", "identifier_test2")}, + connections={("mac", "20:31:32:33:34:02")}, + ) + await hass.async_block_till_done() + + device_id1 = device1.id + assert device_id1 is not None + + device_id2 = device2.id + assert device_id2 is not None + + # Setup the config entry with device 1 + template_config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={ + "template_type": template_type, + "name": "Test", + "state": state_template, + "device_id": device_id1, + }, + title="Sensor template", + ) + template_config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(template_config_entry.entry_id) + await hass.async_block_till_done() + + # Change to link to device 2 + result = await hass.config_entries.options.async_init( + template_config_entry.entry_id + ) + assert result["type"] is FlowResultType.FORM + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + "state": state_template, + "device_id": device_id2, + }, + ) + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["data"] == { + "template_type": template_type, + "name": "Test", + "state": state_template, + "device_id": device_id2, + } + assert template_config_entry.data == {} + assert template_config_entry.options == { + "template_type": template_type, + "name": "Test", + "state": state_template, + "device_id": device_id2, + } + + # Remove link with device + result = await hass.config_entries.options.async_init( + template_config_entry.entry_id + ) + assert result["type"] is FlowResultType.FORM + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + "state": state_template, + }, + ) + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["data"] == { + "template_type": template_type, + "name": "Test", + "state": state_template, + } + assert template_config_entry.data == {} + assert template_config_entry.options == { + "template_type": template_type, + "name": "Test", + "state": state_template, + } + + # Change to link to device 1 + result = await hass.config_entries.options.async_init( + template_config_entry.entry_id + ) + assert result["type"] is FlowResultType.FORM + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + "state": state_template, + "device_id": device_id1, + }, + ) + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["data"] == { + "template_type": template_type, + "name": "Test", + "state": state_template, + "device_id": device_id1, + } + assert template_config_entry.data == {} + assert template_config_entry.options == { + "template_type": template_type, + "name": "Test", + "state": state_template, + "device_id": device_id1, + } diff --git a/tests/components/template/test_init.py b/tests/components/template/test_init.py index 991228623b1..0b2ed873a9c 100644 --- a/tests/components/template/test_init.py +++ b/tests/components/template/test_init.py @@ -8,11 +8,12 @@ import pytest from homeassistant import config from homeassistant.components.template import DOMAIN from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr from homeassistant.helpers.reload import SERVICE_RELOAD from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util -from tests.common import async_fire_time_changed, get_fixture_path +from tests.common import MockConfigEntry, async_fire_time_changed, get_fixture_path @pytest.mark.parametrize(("count", "domain"), [(1, "sensor")]) @@ -268,3 +269,90 @@ async def async_yaml_patch_helper(hass, filename): blocking=True, ) await hass.async_block_till_done() + + +async def test_change_device(hass: HomeAssistant) -> None: + """Test remove the device registry configuration entry when the device changes.""" + + device_registry = dr.async_get(hass) + + # Configure a device registry + entry_device1 = MockConfigEntry() + entry_device1.add_to_hass(hass) + device1 = device_registry.async_get_or_create( + config_entry_id=entry_device1.entry_id, + identifiers={("test", "identifier_test1")}, + connections={("mac", "20:31:32:33:34:01")}, + ) + entry_device2 = MockConfigEntry() + entry_device2.add_to_hass(hass) + device2 = device_registry.async_get_or_create( + config_entry_id=entry_device1.entry_id, + identifiers={("test", "identifier_test2")}, + connections={("mac", "20:31:32:33:34:02")}, + ) + await hass.async_block_till_done() + + device_id1 = device1.id + assert device_id1 is not None + + device_id2 = device2.id + assert device_id2 is not None + + # Setup the config entry (binary_sensor) + sensor_config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={ + "template_type": "binary_sensor", + "name": "Teste", + "state": "{{15}}", + "device_id": device_id1, + }, + title="Binary sensor template", + ) + sensor_config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(sensor_config_entry.entry_id) + await hass.async_block_till_done() + + # Confirm that the configuration entry has been added to the device 1 registry (current) + current_device = device_registry.async_get(device_id=device_id1) + assert sensor_config_entry.entry_id in current_device.config_entries + + # Change configuration options to use device 2 and reload the integration + result = await hass.config_entries.options.async_init(sensor_config_entry.entry_id) + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + "state": "{{15}}", + "device_id": device_id2, + }, + ) + await hass.async_block_till_done() + + # Confirm that the configuration entry has been removed from the device 1 registry (previous) + previous_device = device_registry.async_get(device_id=device_id1) + assert sensor_config_entry.entry_id not in previous_device.config_entries + + # Confirm that the configuration entry has been added to the device 2 registry (current) + current_device = device_registry.async_get(device_id=device_id2) + assert sensor_config_entry.entry_id in current_device.config_entries + + result = await hass.config_entries.options.async_init(sensor_config_entry.entry_id) + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + "state": "{{15}}", + }, + ) + await hass.async_block_till_done() + + # Confirm that the configuration entry has been removed from the device 2 registry (previous) + previous_device = device_registry.async_get(device_id=device_id2) + assert sensor_config_entry.entry_id not in previous_device.config_entries + + # Confirm that there is no device with the helper configuration entry + assert ( + dr.async_entries_for_config_entry(device_registry, sensor_config_entry.entry_id) + == [] + ) diff --git a/tests/components/template/test_sensor.py b/tests/components/template/test_sensor.py index 54e53f5257e..53c31c680dd 100644 --- a/tests/components/template/test_sensor.py +++ b/tests/components/template/test_sensor.py @@ -21,7 +21,7 @@ from homeassistant.const import ( STATE_UNKNOWN, ) from homeassistant.core import Context, CoreState, HomeAssistant, State, callback -from homeassistant.helpers import entity_registry as er +from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.entity_component import async_update_entity from homeassistant.helpers.template import Template from homeassistant.setup import ATTR_COMPONENT, async_setup_component @@ -1896,3 +1896,40 @@ async def test_trigger_action( assert len(events) == 1 assert events[0].context.parent_id == context.id + + +async def test_device_id(hass: HomeAssistant) -> None: + """Test for device for Template.""" + device_registry = dr.async_get(hass) + + device_config_entry = MockConfigEntry() + device_config_entry.add_to_hass(hass) + device_entry = device_registry.async_get_or_create( + config_entry_id=device_config_entry.entry_id, + identifiers={("sensor", "identifier_test")}, + connections={("mac", "30:31:32:33:34:35")}, + ) + await hass.async_block_till_done() + assert device_entry is not None + assert device_entry.id is not None + + template_config_entry = MockConfigEntry( + data={}, + domain=template.DOMAIN, + options={ + "name": "My template", + "state": "{{10}}", + "template_type": "sensor", + "device_id": device_entry.id, + }, + title="My template", + ) + template_config_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(template_config_entry.entry_id) + await hass.async_block_till_done() + + entity_registry = er.async_get(hass) + template_entity = entity_registry.async_get("sensor.my_template") + assert template_entity is not None + assert template_entity.device_id == device_entry.id diff --git a/tests/helpers/test_device.py b/tests/helpers/test_device.py index 9e29288027c..72c602bec48 100644 --- a/tests/helpers/test_device.py +++ b/tests/helpers/test_device.py @@ -6,6 +6,7 @@ import voluptuous as vol from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.device import ( + async_device_info_to_link_from_device_id, async_device_info_to_link_from_entity, async_entity_id_to_device_id, async_remove_stale_devices_links_keep_current_device, @@ -90,12 +91,26 @@ async def test_device_info_to_link( "connections": {("mac", "30:31:32:33:34:00")}, } + result = async_device_info_to_link_from_device_id(hass, device_id=device.id) + assert result == { + "identifiers": {("test", "my_device")}, + "connections": {("mac", "30:31:32:33:34:00")}, + } + # With a non-existent entity id result = async_device_info_to_link_from_entity( hass, entity_id_or_uuid="sensor.invalid" ) assert result is None + # With a non-existent device id + result = async_device_info_to_link_from_device_id(hass, device_id="abcdefghi") + assert result is None + + # With a None device id + result = async_device_info_to_link_from_device_id(hass, device_id=None) + assert result is None + async def test_remove_stale_device_links_keep_entity_device( hass: HomeAssistant, From 6a198087180de966d18cd13d819872e55e4b970e Mon Sep 17 00:00:00 2001 From: GraceGRD <123941606+GraceGRD@users.noreply.github.com> Date: Sat, 22 Jun 2024 12:53:13 +0200 Subject: [PATCH 0997/1445] Add transparent command to opentherm_gw (#116494) --- .../components/opentherm_gw/__init__.py | 30 +++++++++++++++++++ .../components/opentherm_gw/const.py | 3 ++ .../components/opentherm_gw/icons.json | 3 +- .../components/opentherm_gw/services.yaml | 16 ++++++++++ .../components/opentherm_gw/strings.json | 18 +++++++++++ 5 files changed, 69 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/opentherm_gw/__init__.py b/homeassistant/components/opentherm_gw/__init__.py index ca37b7baaef..46cc6f3daa0 100644 --- a/homeassistant/components/opentherm_gw/__init__.py +++ b/homeassistant/components/opentherm_gw/__init__.py @@ -36,6 +36,8 @@ from .const import ( ATTR_DHW_OVRD, ATTR_GW_ID, ATTR_LEVEL, + ATTR_TRANSP_ARG, + ATTR_TRANSP_CMD, CONF_CLIMATE, CONF_FLOOR_TEMP, CONF_PRECISION, @@ -46,6 +48,7 @@ from .const import ( DATA_OPENTHERM_GW, DOMAIN, SERVICE_RESET_GATEWAY, + SERVICE_SEND_TRANSP_CMD, SERVICE_SET_CH_OVRD, SERVICE_SET_CLOCK, SERVICE_SET_CONTROL_SETPOINT, @@ -254,6 +257,19 @@ def register_services(hass: HomeAssistant) -> None: ), } ) + service_send_transp_cmd_schema = vol.Schema( + { + vol.Required(ATTR_GW_ID): vol.All( + cv.string, vol.In(hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS]) + ), + vol.Required(ATTR_TRANSP_CMD): vol.All( + cv.string, vol.Length(min=2, max=2), vol.Coerce(str.upper) + ), + vol.Required(ATTR_TRANSP_ARG): vol.All( + cv.string, vol.Length(min=1, max=12) + ), + } + ) async def reset_gateway(call: ServiceCall) -> None: """Reset the OpenTherm Gateway.""" @@ -377,6 +393,20 @@ def register_services(hass: HomeAssistant) -> None: DOMAIN, SERVICE_SET_SB_TEMP, set_setback_temp, service_set_sb_temp_schema ) + async def send_transparent_cmd(call: ServiceCall) -> None: + """Send a transparent OpenTherm Gateway command.""" + gw_dev = hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][call.data[ATTR_GW_ID]] + transp_cmd = call.data[ATTR_TRANSP_CMD] + transp_arg = call.data[ATTR_TRANSP_ARG] + await gw_dev.gateway.send_transparent_command(transp_cmd, transp_arg) + + hass.services.async_register( + DOMAIN, + SERVICE_SEND_TRANSP_CMD, + send_transparent_cmd, + service_send_transp_cmd_schema, + ) + async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Cleanup and disconnect from gateway.""" diff --git a/homeassistant/components/opentherm_gw/const.py b/homeassistant/components/opentherm_gw/const.py index 74b856b4eaf..6b0a27aec92 100644 --- a/homeassistant/components/opentherm_gw/const.py +++ b/homeassistant/components/opentherm_gw/const.py @@ -19,6 +19,8 @@ ATTR_GW_ID = "gateway_id" ATTR_LEVEL = "level" ATTR_DHW_OVRD = "dhw_override" ATTR_CH_OVRD = "ch_override" +ATTR_TRANSP_CMD = "transp_cmd" +ATTR_TRANSP_ARG = "transp_arg" CONF_CLIMATE = "climate" CONF_FLOOR_TEMP = "floor_temperature" @@ -45,6 +47,7 @@ SERVICE_SET_LED_MODE = "set_led_mode" SERVICE_SET_MAX_MOD = "set_max_modulation" SERVICE_SET_OAT = "set_outside_temperature" SERVICE_SET_SB_TEMP = "set_setback_temperature" +SERVICE_SEND_TRANSP_CMD = "send_transparent_command" TRANSLATE_SOURCE = { gw_vars.BOILER: "Boiler", diff --git a/homeassistant/components/opentherm_gw/icons.json b/homeassistant/components/opentherm_gw/icons.json index 9d5d903aabc..13dbe0a70a1 100644 --- a/homeassistant/components/opentherm_gw/icons.json +++ b/homeassistant/components/opentherm_gw/icons.json @@ -10,6 +10,7 @@ "set_led_mode": "mdi:led-on", "set_max_modulation": "mdi:thermometer-lines", "set_outside_temperature": "mdi:thermometer-lines", - "set_setback_temperature": "mdi:thermometer-lines" + "set_setback_temperature": "mdi:thermometer-lines", + "send_transparent_command": "mdi:console" } } diff --git a/homeassistant/components/opentherm_gw/services.yaml b/homeassistant/components/opentherm_gw/services.yaml index d68624e0763..d521425d06b 100644 --- a/homeassistant/components/opentherm_gw/services.yaml +++ b/homeassistant/components/opentherm_gw/services.yaml @@ -181,3 +181,19 @@ set_setback_temperature: max: 30 step: 0.1 unit_of_measurement: "°" + +send_transparent_command: + fields: + gateway_id: + required: true + example: "opentherm_gateway" + selector: + text: + transp_cmd: + required: true + selector: + text: + transp_arg: + required: true + selector: + text: diff --git a/homeassistant/components/opentherm_gw/strings.json b/homeassistant/components/opentherm_gw/strings.json index a5b8395b56b..2ad34f8d659 100644 --- a/homeassistant/components/opentherm_gw/strings.json +++ b/homeassistant/components/opentherm_gw/strings.json @@ -190,6 +190,24 @@ "description": "The setback temperature to configure on the gateway." } } + }, + "send_transparent_command": { + "name": "Send transparent command", + "description": "Sends custom otgw commands (https://otgw.tclcode.com/firmware.html) through a transparent interface.", + "fields": { + "gateway_id": { + "name": "[%key:component::opentherm_gw::services::reset_gateway::fields::gateway_id::name%]", + "description": "[%key:component::opentherm_gw::services::reset_gateway::fields::gateway_id::description%]" + }, + "transp_cmd": { + "name": "Command", + "description": "The command to be sent to the OpenTherm Gateway." + }, + "transp_arg": { + "name": "Argument", + "description": "The argument of the command to be sent to the OpenTherm Gateway." + } + } } } } From 6d0ae772884d29d581e7c3be24587b2ad9f70e1e Mon Sep 17 00:00:00 2001 From: On Freund Date: Sat, 22 Jun 2024 13:55:42 +0300 Subject: [PATCH 0998/1445] Reload Risco on connection reset (#120150) --- homeassistant/components/risco/__init__.py | 3 ++ homeassistant/components/risco/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/risco/test_init.py | 30 ++++++++++++++++++++ 5 files changed, 36 insertions(+), 3 deletions(-) create mode 100644 tests/components/risco/test_init.py diff --git a/homeassistant/components/risco/__init__.py b/homeassistant/components/risco/__init__.py index b1847b002ea..7255c724e3f 100644 --- a/homeassistant/components/risco/__init__.py +++ b/homeassistant/components/risco/__init__.py @@ -90,6 +90,9 @@ async def _async_setup_local_entry(hass: HomeAssistant, entry: ConfigEntry) -> b async def _error(error: Exception) -> None: _LOGGER.error("Error in Risco library", exc_info=error) + if isinstance(error, ConnectionResetError) and not hass.is_stopping: + _LOGGER.debug("Disconnected from panel. Reloading integration") + hass.async_create_task(hass.config_entries.async_reload(entry.entry_id)) entry.async_on_unload(risco.add_error_handler(_error)) diff --git a/homeassistant/components/risco/manifest.json b/homeassistant/components/risco/manifest.json index 25520d1f96e..372d8e0c629 100644 --- a/homeassistant/components/risco/manifest.json +++ b/homeassistant/components/risco/manifest.json @@ -7,5 +7,5 @@ "iot_class": "local_push", "loggers": ["pyrisco"], "quality_scale": "platinum", - "requirements": ["pyrisco==0.6.2"] + "requirements": ["pyrisco==0.6.4"] } diff --git a/requirements_all.txt b/requirements_all.txt index 5356fa75d9b..3de5ee532da 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2126,7 +2126,7 @@ pyrecswitch==1.0.2 pyrepetierng==0.1.0 # homeassistant.components.risco -pyrisco==0.6.2 +pyrisco==0.6.4 # homeassistant.components.rituals_perfume_genie pyrituals==0.0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c33072d9b79..496eff4d327 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1671,7 +1671,7 @@ pyqwikswitch==0.93 pyrainbird==6.0.1 # homeassistant.components.risco -pyrisco==0.6.2 +pyrisco==0.6.4 # homeassistant.components.rituals_perfume_genie pyrituals==0.0.6 diff --git a/tests/components/risco/test_init.py b/tests/components/risco/test_init.py new file mode 100644 index 00000000000..4f604c75fe9 --- /dev/null +++ b/tests/components/risco/test_init.py @@ -0,0 +1,30 @@ +"""Tests for the Risco integration.""" + +from unittest.mock import patch + +import pytest + +from homeassistant.core import HomeAssistant + + +@pytest.fixture +def mock_error_handler(): + """Create a mock for add_error_handler.""" + with patch("homeassistant.components.risco.RiscoLocal.add_error_handler") as mock: + yield mock + + +async def test_connection_reset( + hass: HomeAssistant, two_zone_local, mock_error_handler, setup_risco_local +) -> None: + """Test config entry reload on connection reset.""" + + callback = mock_error_handler.call_args.args[0] + assert callback is not None + + with patch.object(hass.config_entries, "async_reload") as reload_mock: + await callback(Exception()) + reload_mock.assert_not_awaited() + + await callback(ConnectionResetError()) + reload_mock.assert_awaited_once() From 2b2c4e826203aeefa6ba8cb35354faf12902b72c Mon Sep 17 00:00:00 2001 From: Jack Boswell Date: Sat, 22 Jun 2024 22:56:28 +1200 Subject: [PATCH 0999/1445] Expose altitude for Starlink device tracker (#115508) --- homeassistant/components/starlink/const.py | 2 ++ homeassistant/components/starlink/device_tracker.py | 12 +++++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/starlink/const.py b/homeassistant/components/starlink/const.py index e2f88c5e442..c1a7b1cfd2c 100644 --- a/homeassistant/components/starlink/const.py +++ b/homeassistant/components/starlink/const.py @@ -1,3 +1,5 @@ """Constants for the Starlink integration.""" DOMAIN = "starlink" + +ATTR_ALTITUDE = "altitude" diff --git a/homeassistant/components/starlink/device_tracker.py b/homeassistant/components/starlink/device_tracker.py index 84c0a4cac24..34769d687ff 100644 --- a/homeassistant/components/starlink/device_tracker.py +++ b/homeassistant/components/starlink/device_tracker.py @@ -2,6 +2,7 @@ from collections.abc import Callable from dataclasses import dataclass +from typing import Any from homeassistant.components.device_tracker import SourceType, TrackerEntity from homeassistant.config_entries import ConfigEntry @@ -9,7 +10,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import EntityDescription from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN +from .const import ATTR_ALTITUDE, DOMAIN from .coordinator import StarlinkData from .entity import StarlinkEntity @@ -32,6 +33,7 @@ class StarlinkDeviceTrackerEntityDescription(EntityDescription): latitude_fn: Callable[[StarlinkData], float] longitude_fn: Callable[[StarlinkData], float] + altitude_fn: Callable[[StarlinkData], float] DEVICE_TRACKERS = [ @@ -41,6 +43,7 @@ DEVICE_TRACKERS = [ entity_registry_enabled_default=False, latitude_fn=lambda data: data.location["latitude"], longitude_fn=lambda data: data.location["longitude"], + altitude_fn=lambda data: data.location["altitude"], ), ] @@ -64,3 +67,10 @@ class StarlinkDeviceTrackerEntity(StarlinkEntity, TrackerEntity): def longitude(self) -> float | None: """Return longitude value of the device.""" return self.entity_description.longitude_fn(self.coordinator.data) + + @property + def extra_state_attributes(self) -> dict[str, Any]: + """Return device specific attributes.""" + return { + ATTR_ALTITUDE: self.entity_description.altitude_fn(self.coordinator.data) + } From 1c2aa9a49bf4f0ed6f100126d7a1199b7228921f Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Sat, 22 Jun 2024 13:19:57 +0200 Subject: [PATCH 1000/1445] Add preview to Threshold config & option flow (#117181) Co-authored-by: Erik Montnemery Co-authored-by: Franck Nijhof --- .../components/threshold/binary_sensor.py | 60 +++++- .../components/threshold/config_flow.py | 70 ++++++- .../threshold/snapshots/test_config_flow.ambr | 47 +++++ .../components/threshold/test_config_flow.py | 183 ++++++++++++++++++ 4 files changed, 351 insertions(+), 9 deletions(-) create mode 100644 tests/components/threshold/snapshots/test_config_flow.ambr diff --git a/homeassistant/components/threshold/binary_sensor.py b/homeassistant/components/threshold/binary_sensor.py index 9674357eb60..ac970a53f55 100644 --- a/homeassistant/components/threshold/binary_sensor.py +++ b/homeassistant/components/threshold/binary_sensor.py @@ -2,6 +2,7 @@ from __future__ import annotations +from collections.abc import Callable, Mapping import logging from typing import Any @@ -22,7 +23,13 @@ from homeassistant.const import ( STATE_UNAVAILABLE, STATE_UNKNOWN, ) -from homeassistant.core import Event, EventStateChangedData, HomeAssistant, callback +from homeassistant.core import ( + CALLBACK_TYPE, + Event, + EventStateChangedData, + HomeAssistant, + callback, +) from homeassistant.helpers import ( config_validation as cv, device_registry as dr, @@ -111,7 +118,6 @@ async def async_setup_entry( async_add_entities( [ ThresholdSensor( - hass, entity_id, name, lower, @@ -145,7 +151,7 @@ async def async_setup_platform( async_add_entities( [ ThresholdSensor( - hass, entity_id, name, lower, upper, hysteresis, device_class, None + entity_id, name, lower, upper, hysteresis, device_class, None ) ], ) @@ -167,7 +173,6 @@ class ThresholdSensor(BinarySensorEntity): def __init__( self, - hass: HomeAssistant, entity_id: str, name: str, lower: float | None, @@ -178,6 +183,7 @@ class ThresholdSensor(BinarySensorEntity): device_info: DeviceInfo | None = None, ) -> None: """Initialize the Threshold sensor.""" + self._preview_callback: Callable[[str, Mapping[str, Any]], None] | None = None self._attr_unique_id = unique_id self._attr_device_info = device_info self._entity_id = entity_id @@ -193,9 +199,17 @@ class ThresholdSensor(BinarySensorEntity): self._state: bool | None = None self.sensor_value: float | None = None + async def async_added_to_hass(self) -> None: + """Run when entity about to be added to hass.""" + self._async_setup_sensor() + + @callback + def _async_setup_sensor(self) -> None: + """Set up the sensor and start tracking state changes.""" + def _update_sensor_state() -> None: """Handle sensor state changes.""" - if (new_state := hass.states.get(self._entity_id)) is None: + if (new_state := self.hass.states.get(self._entity_id)) is None: return try: @@ -210,17 +224,26 @@ class ThresholdSensor(BinarySensorEntity): self._update_state() + if self._preview_callback: + calculated_state = self._async_calculate_state() + self._preview_callback( + calculated_state.state, calculated_state.attributes + ) + @callback def async_threshold_sensor_state_listener( event: Event[EventStateChangedData], ) -> None: """Handle sensor state changes.""" _update_sensor_state() - self.async_write_ha_state() + + # only write state to the state machine if we are not in preview mode + if not self._preview_callback: + self.async_write_ha_state() self.async_on_remove( async_track_state_change_event( - hass, [entity_id], async_threshold_sensor_state_listener + self.hass, [self._entity_id], async_threshold_sensor_state_listener ) ) _update_sensor_state() @@ -305,3 +328,26 @@ class ThresholdSensor(BinarySensorEntity): self._state_position = POSITION_IN_RANGE self._state = True return + + @callback + def async_start_preview( + self, + preview_callback: Callable[[str, Mapping[str, Any]], None], + ) -> CALLBACK_TYPE: + """Render a preview.""" + # abort early if there is no entity_id + # as without we can't track changes + # or if neither lower nor upper thresholds are set + if not self._entity_id or ( + not hasattr(self, "_threshold_lower") + and not hasattr(self, "_threshold_upper") + ): + self._attr_available = False + calculated_state = self._async_calculate_state() + preview_callback(calculated_state.state, calculated_state.attributes) + return self._call_on_remove_callbacks + + self._preview_callback = preview_callback + + self._async_setup_sensor() + return self._call_on_remove_callbacks diff --git a/homeassistant/components/threshold/config_flow.py b/homeassistant/components/threshold/config_flow.py index 08a4a18fca7..24f58333782 100644 --- a/homeassistant/components/threshold/config_flow.py +++ b/homeassistant/components/threshold/config_flow.py @@ -7,8 +7,11 @@ from typing import Any import voluptuous as vol +from homeassistant.components import websocket_api from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.const import CONF_ENTITY_ID, CONF_NAME +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import selector from homeassistant.helpers.schema_config_entry_flow import ( SchemaCommonFlowHandler, @@ -17,6 +20,7 @@ from homeassistant.helpers.schema_config_entry_flow import ( SchemaFlowFormStep, ) +from .binary_sensor import ThresholdSensor from .const import CONF_HYSTERESIS, CONF_LOWER, CONF_UPPER, DEFAULT_HYSTERESIS, DOMAIN @@ -61,11 +65,15 @@ CONFIG_SCHEMA = vol.Schema( ).extend(OPTIONS_SCHEMA.schema) CONFIG_FLOW = { - "user": SchemaFlowFormStep(CONFIG_SCHEMA, validate_user_input=_validate_mode) + "user": SchemaFlowFormStep( + CONFIG_SCHEMA, preview="threshold", validate_user_input=_validate_mode + ) } OPTIONS_FLOW = { - "init": SchemaFlowFormStep(OPTIONS_SCHEMA, validate_user_input=_validate_mode) + "init": SchemaFlowFormStep( + OPTIONS_SCHEMA, preview="threshold", validate_user_input=_validate_mode + ) } @@ -79,3 +87,61 @@ class ConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN): """Return config entry title.""" name: str = options[CONF_NAME] return name + + @staticmethod + async def async_setup_preview(hass: HomeAssistant) -> None: + """Set up preview WS API.""" + websocket_api.async_register_command(hass, ws_start_preview) + + +@websocket_api.websocket_command( + { + vol.Required("type"): "threshold/start_preview", + vol.Required("flow_id"): str, + vol.Required("flow_type"): vol.Any("config_flow", "options_flow"), + vol.Required("user_input"): dict, + } +) +@callback +def ws_start_preview( + hass: HomeAssistant, + connection: websocket_api.ActiveConnection, + msg: dict[str, Any], +) -> None: + """Generate a preview.""" + + if msg["flow_type"] == "config_flow": + entity_id = msg["user_input"][CONF_ENTITY_ID] + name = msg["user_input"][CONF_NAME] + else: + flow_status = hass.config_entries.options.async_get(msg["flow_id"]) + config_entry = hass.config_entries.async_get_entry(flow_status["handler"]) + if not config_entry: + raise HomeAssistantError("Config entry not found") + entity_id = config_entry.options[CONF_ENTITY_ID] + name = config_entry.options[CONF_NAME] + + @callback + def async_preview_updated(state: str, attributes: Mapping[str, Any]) -> None: + """Forward config entry state events to websocket.""" + connection.send_message( + websocket_api.event_message( + msg["id"], {"attributes": attributes, "state": state} + ) + ) + + preview_entity = ThresholdSensor( + entity_id, + name, + msg["user_input"].get(CONF_LOWER), + msg["user_input"].get(CONF_UPPER), + msg["user_input"].get(CONF_HYSTERESIS), + None, + None, + ) + preview_entity.hass = hass + + connection.send_result(msg["id"]) + connection.subscriptions[msg["id"]] = preview_entity.async_start_preview( + async_preview_updated + ) diff --git a/tests/components/threshold/snapshots/test_config_flow.ambr b/tests/components/threshold/snapshots/test_config_flow.ambr new file mode 100644 index 00000000000..d6b4489c930 --- /dev/null +++ b/tests/components/threshold/snapshots/test_config_flow.ambr @@ -0,0 +1,47 @@ +# serializer version: 1 +# name: test_config_flow_preview_success[missing_entity_id] + dict({ + 'attributes': dict({ + 'friendly_name': '', + }), + 'state': 'unavailable', + }) +# --- +# name: test_config_flow_preview_success[missing_upper_lower] + dict({ + 'attributes': dict({ + 'friendly_name': 'Test Sensor', + }), + 'state': 'unavailable', + }) +# --- +# name: test_config_flow_preview_success[success] + dict({ + 'attributes': dict({ + 'entity_id': 'sensor.test_monitored', + 'friendly_name': 'Test Sensor', + 'hysteresis': 0.0, + 'lower': 20.0, + 'position': 'below', + 'sensor_value': 16.0, + 'type': 'lower', + 'upper': None, + }), + 'state': 'on', + }) +# --- +# name: test_options_flow_preview + dict({ + 'attributes': dict({ + 'entity_id': 'sensor.test_monitored', + 'friendly_name': 'Test Sensor', + 'hysteresis': 0.0, + 'lower': 20.0, + 'position': 'below', + 'sensor_value': 16.0, + 'type': 'lower', + 'upper': None, + }), + 'state': 'on', + }) +# --- diff --git a/tests/components/threshold/test_config_flow.py b/tests/components/threshold/test_config_flow.py index e337c5c41c5..c13717800bf 100644 --- a/tests/components/threshold/test_config_flow.py +++ b/tests/components/threshold/test_config_flow.py @@ -3,13 +3,16 @@ from unittest.mock import patch import pytest +from syrupy import SnapshotAssertion from homeassistant import config_entries from homeassistant.components.threshold.const import DOMAIN +from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, UnitOfTemperature from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry +from tests.typing import WebSocketGenerator async def test_config_flow(hass: HomeAssistant) -> None: @@ -162,3 +165,183 @@ async def test_options(hass: HomeAssistant) -> None: state = hass.states.get("binary_sensor.my_threshold") assert state.state == "off" assert state.attributes["type"] == "upper" + + +@pytest.mark.parametrize( + "user_input", + [ + ( + { + "name": "Test Sensor", + "entity_id": "sensor.test_monitored", + "hysteresis": 0.0, + "lower": 20.0, + } + ), + ( + { + "name": "Test Sensor", + "entity_id": "sensor.test_monitored", + "hysteresis": 0.0, + } + ), + ( + { + "name": "", + "entity_id": "", + "hysteresis": 0.0, + "lower": 20.0, + } + ), + ], + ids=("success", "missing_upper_lower", "missing_entity_id"), +) +async def test_config_flow_preview_success( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + user_input: str, + snapshot: SnapshotAssertion, +) -> None: + """Test the config flow preview.""" + client = await hass_ws_client(hass) + + # add state for the tests + hass.states.async_set( + "sensor.test_monitored", + 16, + {ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS}, + ) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] is None + assert result["preview"] == "threshold" + + await client.send_json_auto_id( + { + "type": "threshold/start_preview", + "flow_id": result["flow_id"], + "flow_type": "config_flow", + "user_input": user_input, + } + ) + msg = await client.receive_json() + assert msg["success"] + assert msg["result"] is None + + msg = await client.receive_json() + assert msg["event"] == snapshot + assert len(hass.states.async_all()) == 1 + + +async def test_options_flow_preview( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + snapshot: SnapshotAssertion, +) -> None: + """Test the options flow preview.""" + client = await hass_ws_client(hass) + + # add state for the tests + hass.states.async_set( + "sensor.test_monitored", + 16, + {ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS}, + ) + + # Setup the config entry + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={ + "entity_id": "sensor.test_monitored", + "hysteresis": 0.0, + "lower": 20.0, + "name": "Test Sensor", + "upper": None, + }, + title="Test Sensor", + ) + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["type"] == FlowResultType.FORM + assert result["errors"] is None + assert result["preview"] == "threshold" + + await client.send_json_auto_id( + { + "type": "threshold/start_preview", + "flow_id": result["flow_id"], + "flow_type": "options_flow", + "user_input": { + "name": "Test Sensor", + "entity_id": "sensor.test_monitored", + "hysteresis": 0.0, + "lower": 20.0, + }, + } + ) + msg = await client.receive_json() + assert msg["success"] + assert msg["result"] is None + + msg = await client.receive_json() + assert msg["event"] == snapshot + assert len(hass.states.async_all()) == 2 + + +async def test_options_flow_sensor_preview_config_entry_removed( + hass: HomeAssistant, hass_ws_client: WebSocketGenerator +) -> None: + """Test the option flow preview where the config entry is removed.""" + client = await hass_ws_client(hass) + + # Setup the config entry + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={ + "entity_id": "sensor.test_monitored", + "hysteresis": 0.0, + "lower": 20.0, + "name": "Test Sensor", + "upper": None, + }, + title="Test Sensor", + ) + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["type"] == FlowResultType.FORM + assert result["errors"] is None + assert result["preview"] == "threshold" + + await hass.config_entries.async_remove(config_entry.entry_id) + + await client.send_json_auto_id( + { + "type": "threshold/start_preview", + "flow_id": result["flow_id"], + "flow_type": "options_flow", + "user_input": { + "name": "Test Sensor", + "entity_id": "sensor.test_monitored", + "hysteresis": 0.0, + "lower": 20.0, + }, + } + ) + msg = await client.receive_json() + assert not msg["success"] + assert msg["error"] == { + "code": "home_assistant_error", + "message": "Config entry not found", + } From ea8d0ba2ce0d51e98f5d377c698889eb2c70c408 Mon Sep 17 00:00:00 2001 From: Matthew FitzGerald-Chamberlain Date: Sat, 22 Jun 2024 06:20:33 -0500 Subject: [PATCH 1001/1445] Add sensors for Aprilaire integration (#113194) * Add sensors for Aprilaire integration * Exclude from coverage * Exclude from coverage * Add comment * Update homeassistant/components/aprilaire/sensor.py Co-authored-by: Joost Lekkerkerker * Code review updates * Code review updates * Code review updates * Code review updates * Remove temperature conversion * Add suggested display precision * Code review updates * Merge fix * Code review fixes * Fix type errors * Revert change * Fix type errors * Type errors * Use common keys --------- Co-authored-by: Joost Lekkerkerker --- .coveragerc | 1 + .../components/aprilaire/__init__.py | 2 +- homeassistant/components/aprilaire/sensor.py | 308 ++++++++++++++++++ .../components/aprilaire/strings.json | 53 +++ 4 files changed, 363 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/aprilaire/sensor.py diff --git a/.coveragerc b/.coveragerc index 350c39ca3d2..43dc6edafc5 100644 --- a/.coveragerc +++ b/.coveragerc @@ -87,6 +87,7 @@ omit = homeassistant/components/aprilaire/climate.py homeassistant/components/aprilaire/coordinator.py homeassistant/components/aprilaire/entity.py + homeassistant/components/aprilaire/sensor.py homeassistant/components/apsystems/__init__.py homeassistant/components/apsystems/coordinator.py homeassistant/components/apsystems/entity.py diff --git a/homeassistant/components/aprilaire/__init__.py b/homeassistant/components/aprilaire/__init__.py index 4fa5cdac68d..ba310615567 100644 --- a/homeassistant/components/aprilaire/__init__.py +++ b/homeassistant/components/aprilaire/__init__.py @@ -15,7 +15,7 @@ from homeassistant.helpers.device_registry import format_mac from .const import DOMAIN from .coordinator import AprilaireCoordinator -PLATFORMS: list[Platform] = [Platform.CLIMATE] +PLATFORMS: list[Platform] = [Platform.CLIMATE, Platform.SENSOR] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/aprilaire/sensor.py b/homeassistant/components/aprilaire/sensor.py new file mode 100644 index 00000000000..249c1b3850f --- /dev/null +++ b/homeassistant/components/aprilaire/sensor.py @@ -0,0 +1,308 @@ +"""The Aprilaire sensor component.""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import cast + +from pyaprilaire.const import Attribute + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import PERCENTAGE, UnitOfTemperature +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import StateType + +from .const import DOMAIN +from .coordinator import AprilaireCoordinator +from .entity import BaseAprilaireEntity + +DEHUMIDIFICATION_STATUS_MAP: dict[StateType, str] = { + 0: "idle", + 1: "idle", + 2: "on", + 3: "on", + 4: "off", +} + +HUMIDIFICATION_STATUS_MAP: dict[StateType, str] = { + 0: "idle", + 1: "idle", + 2: "on", + 3: "off", +} + +VENTILATION_STATUS_MAP: dict[StateType, str] = { + 0: "idle", + 1: "idle", + 2: "on", + 3: "idle", + 4: "idle", + 5: "idle", + 6: "off", +} + +AIR_CLEANING_STATUS_MAP: dict[StateType, str] = { + 0: "idle", + 1: "idle", + 2: "on", + 3: "off", +} + +FAN_STATUS_MAP: dict[StateType, str] = {0: "off", 1: "on"} + + +def get_entities( + entity_class: type[BaseAprilaireSensor], + coordinator: AprilaireCoordinator, + unique_id: str, + descriptions: tuple[AprilaireSensorDescription, ...], +) -> list[BaseAprilaireSensor]: + """Get the entities for a list of sensor descriptions.""" + + entities = ( + entity_class(coordinator, description, unique_id) + for description in descriptions + ) + + return [entity for entity in entities if entity.exists] + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up Aprilaire sensor devices.""" + + coordinator: AprilaireCoordinator = hass.data[DOMAIN][config_entry.unique_id] + + assert config_entry.unique_id is not None + + entities = ( + get_entities( + AprilaireHumiditySensor, + coordinator, + config_entry.unique_id, + HUMIDITY_SENSORS, + ) + + get_entities( + AprilaireTemperatureSensor, + coordinator, + config_entry.unique_id, + TEMPERATURE_SENSORS, + ) + + get_entities( + AprilaireStatusSensor, coordinator, config_entry.unique_id, STATUS_SENSORS + ) + ) + + async_add_entities(entities) + + +@dataclass(frozen=True, kw_only=True) +class AprilaireSensorDescription(SensorEntityDescription): + """Class describing Aprilaire sensor entities.""" + + status_key: str | None + value_key: str + + +@dataclass(frozen=True, kw_only=True) +class AprilaireStatusSensorDescription(AprilaireSensorDescription): + """Class describing Aprilaire status sensor entities.""" + + status_map: dict[StateType, str] + + +HUMIDITY_SENSORS: tuple[AprilaireSensorDescription, ...] = ( + AprilaireSensorDescription( + key="indoor_humidity_controlling_sensor", + translation_key="indoor_humidity_controlling_sensor", + device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=PERCENTAGE, + status_key=Attribute.INDOOR_HUMIDITY_CONTROLLING_SENSOR_STATUS, + value_key=Attribute.INDOOR_HUMIDITY_CONTROLLING_SENSOR_VALUE, + ), + AprilaireSensorDescription( + key="outdoor_humidity_controlling_sensor", + translation_key="outdoor_humidity_controlling_sensor", + device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=PERCENTAGE, + status_key=Attribute.OUTDOOR_HUMIDITY_CONTROLLING_SENSOR_STATUS, + value_key=Attribute.OUTDOOR_HUMIDITY_CONTROLLING_SENSOR_VALUE, + ), +) + +TEMPERATURE_SENSORS: tuple[AprilaireSensorDescription, ...] = ( + AprilaireSensorDescription( + key="indoor_temperature_controlling_sensor", + translation_key="indoor_temperature_controlling_sensor", + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + status_key=Attribute.INDOOR_TEMPERATURE_CONTROLLING_SENSOR_STATUS, + value_key=Attribute.INDOOR_TEMPERATURE_CONTROLLING_SENSOR_VALUE, + ), + AprilaireSensorDescription( + key="outdoor_temperature_controlling_sensor", + translation_key="outdoor_temperature_controlling_sensor", + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + status_key=Attribute.OUTDOOR_TEMPERATURE_CONTROLLING_SENSOR_STATUS, + value_key=Attribute.OUTDOOR_TEMPERATURE_CONTROLLING_SENSOR_VALUE, + ), +) + +STATUS_SENSORS: tuple[AprilaireSensorDescription, ...] = ( + AprilaireStatusSensorDescription( + key="dehumidification_status", + translation_key="dehumidification_status", + device_class=SensorDeviceClass.ENUM, + status_key=Attribute.DEHUMIDIFICATION_AVAILABLE, + value_key=Attribute.DEHUMIDIFICATION_STATUS, + status_map=DEHUMIDIFICATION_STATUS_MAP, + options=list(set(DEHUMIDIFICATION_STATUS_MAP.values())), + ), + AprilaireStatusSensorDescription( + key="humidification_status", + translation_key="humidification_status", + device_class=SensorDeviceClass.ENUM, + status_key=Attribute.HUMIDIFICATION_AVAILABLE, + value_key=Attribute.HUMIDIFICATION_STATUS, + status_map=HUMIDIFICATION_STATUS_MAP, + options=list(set(HUMIDIFICATION_STATUS_MAP.values())), + ), + AprilaireStatusSensorDescription( + key="ventilation_status", + translation_key="ventilation_status", + device_class=SensorDeviceClass.ENUM, + status_key=Attribute.VENTILATION_AVAILABLE, + value_key=Attribute.VENTILATION_STATUS, + status_map=VENTILATION_STATUS_MAP, + options=list(set(VENTILATION_STATUS_MAP.values())), + ), + AprilaireStatusSensorDescription( + key="air_cleaning_status", + translation_key="air_cleaning_status", + device_class=SensorDeviceClass.ENUM, + status_key=Attribute.AIR_CLEANING_AVAILABLE, + value_key=Attribute.AIR_CLEANING_STATUS, + status_map=AIR_CLEANING_STATUS_MAP, + options=list(set(AIR_CLEANING_STATUS_MAP.values())), + ), + AprilaireStatusSensorDescription( + key="fan_status", + translation_key="fan_status", + device_class=SensorDeviceClass.ENUM, + status_key=None, + value_key=Attribute.FAN_STATUS, + status_map=FAN_STATUS_MAP, + options=list(set(FAN_STATUS_MAP.values())), + ), +) + + +class BaseAprilaireSensor(BaseAprilaireEntity, SensorEntity): + """Base sensor entity for Aprilaire.""" + + entity_description: AprilaireSensorDescription + status_sensor_available_value: int | None = None + status_sensor_exists_values: list[int] + + def __init__( + self, + coordinator: AprilaireCoordinator, + description: AprilaireSensorDescription, + unique_id: str, + ) -> None: + """Initialize a sensor for an Aprilaire device.""" + + self.entity_description = description + + super().__init__(coordinator, unique_id) + + @property + def exists(self) -> bool: + """Return True if the sensor exists.""" + + if self.entity_description.status_key is None: + return True + + return ( + self.coordinator.data.get(self.entity_description.status_key) + in self.status_sensor_exists_values + ) + + @property + def available(self) -> bool: + """Return True if the sensor is available.""" + + if ( + self.entity_description.status_key is None + or self.status_sensor_available_value is None + ): + return True + + if not super().available: + return False + + return ( + self.coordinator.data.get(self.entity_description.status_key) + == self.status_sensor_available_value + ) + + @property + def native_value(self) -> StateType: + """Return the value reported by the sensor.""" + + # Valid cast as pyaprilaire only provides str | int | float + return cast( + StateType, self.coordinator.data.get(self.entity_description.value_key) + ) + + +class AprilaireHumiditySensor(BaseAprilaireSensor): + """Humidity sensor entity for Aprilaire.""" + + status_sensor_available_value = 0 + status_sensor_exists_values = [0, 1, 2] + + +class AprilaireTemperatureSensor(BaseAprilaireSensor): + """Temperature sensor entity for Aprilaire.""" + + status_sensor_available_value = 0 + status_sensor_exists_values = [0, 1, 2] + + @property + def suggested_display_precision(self) -> int | None: + """Return the suggested number of decimal digits for display.""" + if self.unit_of_measurement == UnitOfTemperature.CELSIUS: + return 1 + + return 0 + + +class AprilaireStatusSensor(BaseAprilaireSensor): + """Status sensor entity for Aprilaire.""" + + status_sensor_exists_values = [1, 2] + entity_description: AprilaireStatusSensorDescription + + @property + def native_value(self) -> StateType: + """Return the value reported by the sensor mapped to the status option.""" + + raw_value = super().native_value + + return self.entity_description.status_map.get(raw_value) diff --git a/homeassistant/components/aprilaire/strings.json b/homeassistant/components/aprilaire/strings.json index e996691f21f..72005e0215c 100644 --- a/homeassistant/components/aprilaire/strings.json +++ b/homeassistant/components/aprilaire/strings.json @@ -23,6 +23,59 @@ "thermostat": { "name": "Thermostat" } + }, + "sensor": { + "indoor_humidity_controlling_sensor": { + "name": "Indoor humidity controlling sensor" + }, + "outdoor_humidity_controlling_sensor": { + "name": "Outdoor humidity controlling sensor" + }, + "indoor_temperature_controlling_sensor": { + "name": "Indoor temperature controlling sensor" + }, + "outdoor_temperature_controlling_sensor": { + "name": "Outdoor temperature controlling sensor" + }, + "dehumidification_status": { + "name": "Dehumidification status", + "state": { + "idle": "[%key:common::state::idle%]", + "on": "[%key:common::state::on%]", + "off": "[%key:common::state::off%]" + } + }, + "humidification_status": { + "name": "Humidification status", + "state": { + "idle": "[%key:common::state::idle%]", + "on": "[%key:common::state::on%]", + "off": "[%key:common::state::off%]" + } + }, + "ventilation_status": { + "name": "Ventilation status", + "state": { + "idle": "[%key:common::state::idle%]", + "on": "[%key:common::state::on%]", + "off": "[%key:common::state::off%]" + } + }, + "air_cleaning_status": { + "name": "Air cleaning status", + "state": { + "idle": "[%key:common::state::idle%]", + "on": "[%key:common::state::on%]", + "off": "[%key:common::state::off%]" + } + }, + "fan_status": { + "name": "Fan status", + "state": { + "on": "[%key:common::state::on%]", + "off": "[%key:common::state::off%]" + } + } } } } From 93e87997bef7ba7e92af72647db958026048281e Mon Sep 17 00:00:00 2001 From: Lenn <78048721+LennP@users.noreply.github.com> Date: Sat, 22 Jun 2024 13:25:03 +0200 Subject: [PATCH 1002/1445] Add sensors to Motionblinds BLE integration (#114226) * Add sensors * Add sensor.py to .coveragerc * Move icons to icons.json * Remove signal strength translation key * Change native_value attribute name in entity description to initial_value * Use str instead of enum for MotionConnectionType for options * Add calibration options to entity description * Fix icons * Change translations of connection and calibration * Move entity descriptions to __init__ * Use generic sensor class * Use generic sensor class * Update homeassistant/components/motionblinds_ble/strings.json Co-authored-by: Joost Lekkerkerker * Update homeassistant/components/motionblinds_ble/sensor.py Co-authored-by: Joost Lekkerkerker * Update homeassistant/components/motionblinds_ble/strings.json Co-authored-by: Joost Lekkerkerker * Update homeassistant/components/motionblinds_ble/strings.json Co-authored-by: Joost Lekkerkerker --------- Co-authored-by: Joost Lekkerkerker --- .coveragerc | 1 + .../components/motionblinds_ble/__init__.py | 7 +- .../components/motionblinds_ble/const.py | 4 + .../components/motionblinds_ble/icons.json | 8 + .../components/motionblinds_ble/sensor.py | 195 ++++++++++++++++++ .../components/motionblinds_ble/strings.json | 19 ++ 6 files changed, 233 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/motionblinds_ble/sensor.py diff --git a/.coveragerc b/.coveragerc index 43dc6edafc5..003b4908b17 100644 --- a/.coveragerc +++ b/.coveragerc @@ -816,6 +816,7 @@ omit = homeassistant/components/motionblinds_ble/cover.py homeassistant/components/motionblinds_ble/entity.py homeassistant/components/motionblinds_ble/select.py + homeassistant/components/motionblinds_ble/sensor.py homeassistant/components/motionmount/__init__.py homeassistant/components/motionmount/binary_sensor.py homeassistant/components/motionmount/entity.py diff --git a/homeassistant/components/motionblinds_ble/__init__.py b/homeassistant/components/motionblinds_ble/__init__.py index 1b664eeede3..76ceac1097c 100644 --- a/homeassistant/components/motionblinds_ble/__init__.py +++ b/homeassistant/components/motionblinds_ble/__init__.py @@ -34,7 +34,12 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -PLATFORMS: list[Platform] = [Platform.BUTTON, Platform.COVER, Platform.SELECT] +PLATFORMS: list[Platform] = [ + Platform.BUTTON, + Platform.COVER, + Platform.SELECT, + Platform.SENSOR, +] CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN) diff --git a/homeassistant/components/motionblinds_ble/const.py b/homeassistant/components/motionblinds_ble/const.py index 0b4a2a7f947..6b958170a4a 100644 --- a/homeassistant/components/motionblinds_ble/const.py +++ b/homeassistant/components/motionblinds_ble/const.py @@ -1,8 +1,12 @@ """Constants for the Motionblinds Bluetooth integration.""" +ATTR_BATTERY = "battery" +ATTR_CALIBRATION = "calibration" ATTR_CONNECT = "connect" +ATTR_CONNECTION = "connection" ATTR_DISCONNECT = "disconnect" ATTR_FAVORITE = "favorite" +ATTR_SIGNAL_STRENGTH = "signal_strength" ATTR_SPEED = "speed" CONF_LOCAL_NAME = "local_name" diff --git a/homeassistant/components/motionblinds_ble/icons.json b/homeassistant/components/motionblinds_ble/icons.json index c8d2b085d75..7a7561360a2 100644 --- a/homeassistant/components/motionblinds_ble/icons.json +++ b/homeassistant/components/motionblinds_ble/icons.json @@ -15,6 +15,14 @@ "speed": { "default": "mdi:run-fast" } + }, + "sensor": { + "calibration": { + "default": "mdi:tune" + }, + "connection": { + "default": "mdi:bluetooth-connect" + } } } } diff --git a/homeassistant/components/motionblinds_ble/sensor.py b/homeassistant/components/motionblinds_ble/sensor.py new file mode 100644 index 00000000000..fbab5d06251 --- /dev/null +++ b/homeassistant/components/motionblinds_ble/sensor.py @@ -0,0 +1,195 @@ +"""Sensor entities for the Motionblinds BLE integration.""" + +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass +import logging +from math import ceil +from typing import Generic, TypeVar + +from motionblindsble.const import ( + MotionBlindType, + MotionCalibrationType, + MotionConnectionType, +) +from motionblindsble.device import MotionDevice + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + PERCENTAGE, + SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + EntityCategory, +) +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import StateType + +from .const import ( + ATTR_BATTERY, + ATTR_CALIBRATION, + ATTR_CONNECTION, + ATTR_SIGNAL_STRENGTH, + CONF_MAC_CODE, + DOMAIN, +) +from .entity import MotionblindsBLEEntity + +_LOGGER = logging.getLogger(__name__) + +PARALLEL_UPDATES = 0 + +_T = TypeVar("_T") + + +@dataclass(frozen=True, kw_only=True) +class MotionblindsBLESensorEntityDescription(SensorEntityDescription, Generic[_T]): + """Entity description of a sensor entity with initial_value attribute.""" + + initial_value: str | None = None + register_callback_func: Callable[ + [MotionDevice], Callable[[Callable[[_T | None], None]], None] + ] + value_func: Callable[[_T | None], StateType] + is_supported: Callable[[MotionDevice], bool] = lambda device: True + + +SENSORS: tuple[MotionblindsBLESensorEntityDescription, ...] = ( + MotionblindsBLESensorEntityDescription[MotionConnectionType]( + key=ATTR_CONNECTION, + translation_key=ATTR_CONNECTION, + device_class=SensorDeviceClass.ENUM, + entity_category=EntityCategory.DIAGNOSTIC, + options=["connected", "connecting", "disconnected", "disconnecting"], + initial_value=MotionConnectionType.DISCONNECTED.value, + register_callback_func=lambda device: device.register_connection_callback, + value_func=lambda value: value.value if value else None, + ), + MotionblindsBLESensorEntityDescription[MotionCalibrationType]( + key=ATTR_CALIBRATION, + translation_key=ATTR_CALIBRATION, + device_class=SensorDeviceClass.ENUM, + entity_category=EntityCategory.DIAGNOSTIC, + options=["calibrated", "uncalibrated", "calibrating"], + register_callback_func=lambda device: device.register_calibration_callback, + value_func=lambda value: value.value if value else None, + is_supported=lambda device: device.blind_type + in {MotionBlindType.CURTAIN, MotionBlindType.VERTICAL}, + ), + MotionblindsBLESensorEntityDescription[int]( + key=ATTR_SIGNAL_STRENGTH, + device_class=SensorDeviceClass.SIGNAL_STRENGTH, + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + register_callback_func=lambda device: device.register_signal_strength_callback, + value_func=lambda value: value, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up sensor entities based on a config entry.""" + + device: MotionDevice = hass.data[DOMAIN][entry.entry_id] + + entities: list[SensorEntity] = [ + MotionblindsBLESensorEntity(device, entry, description) + for description in SENSORS + if description.is_supported(device) + ] + entities.append(BatterySensor(device, entry)) + async_add_entities(entities) + + +class MotionblindsBLESensorEntity(MotionblindsBLEEntity, SensorEntity, Generic[_T]): + """Representation of a sensor entity.""" + + entity_description: MotionblindsBLESensorEntityDescription[_T] + + def __init__( + self, + device: MotionDevice, + entry: ConfigEntry, + entity_description: MotionblindsBLESensorEntityDescription[_T], + ) -> None: + """Initialize the sensor entity.""" + super().__init__( + device, entry, entity_description, unique_id_suffix=entity_description.key + ) + self._attr_native_value = entity_description.initial_value + + async def async_added_to_hass(self) -> None: + """Log sensor entity information.""" + _LOGGER.debug( + "(%s) Setting up %s sensor entity", + self.entry.data[CONF_MAC_CODE], + self.entity_description.key.replace("_", " "), + ) + + def async_callback(value: _T | None) -> None: + """Update the sensor value.""" + self._attr_native_value = self.entity_description.value_func(value) + self.async_write_ha_state() + + self.entity_description.register_callback_func(self.device)(async_callback) + + +class BatterySensor(MotionblindsBLEEntity, SensorEntity): + """Representation of a battery sensor entity.""" + + def __init__( + self, + device: MotionDevice, + entry: ConfigEntry, + ) -> None: + """Initialize the sensor entity.""" + entity_description = SensorEntityDescription( + key=ATTR_BATTERY, + native_unit_of_measurement=PERCENTAGE, + device_class=SensorDeviceClass.BATTERY, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + ) + super().__init__(device, entry, entity_description) + + async def async_added_to_hass(self) -> None: + """Register device callbacks.""" + await super().async_added_to_hass() + self.device.register_battery_callback(self.async_update_battery) + + @callback + def async_update_battery( + self, + battery_percentage: int | None, + is_charging: bool | None, + is_wired: bool | None, + ) -> None: + """Update the battery sensor value and icon.""" + self._attr_native_value = battery_percentage + if battery_percentage is None: + # Battery percentage is unknown + self._attr_icon = "mdi:battery-unknown" + elif is_wired: + # Motor is wired and does not have a battery + self._attr_icon = "mdi:power-plug-outline" + elif battery_percentage > 90 and not is_charging: + # Full battery icon if battery > 90% and not charging + self._attr_icon = "mdi:battery" + elif battery_percentage <= 5 and not is_charging: + # Empty battery icon with alert if battery <= 5% and not charging + self._attr_icon = "mdi:battery-alert-variant-outline" + else: + battery_icon_prefix = ( + "mdi:battery-charging" if is_charging else "mdi:battery" + ) + battery_percentage_multiple_ten = ceil(battery_percentage / 10) * 10 + self._attr_icon = f"{battery_icon_prefix}-{battery_percentage_multiple_ten}" + self.async_write_ha_state() diff --git a/homeassistant/components/motionblinds_ble/strings.json b/homeassistant/components/motionblinds_ble/strings.json index ab26f26ce44..d6532f12386 100644 --- a/homeassistant/components/motionblinds_ble/strings.json +++ b/homeassistant/components/motionblinds_ble/strings.json @@ -67,6 +67,25 @@ "3": "High" } } + }, + "sensor": { + "connection": { + "name": "Connection status", + "state": { + "connected": "Connected", + "disconnected": "Disconnected", + "connecting": "Connecting", + "disconnecting": "Disconnecting" + } + }, + "calibration": { + "name": "Calibration status", + "state": { + "calibrated": "Calibrated", + "uncalibrated": "Uncalibrated", + "calibrating": "Calibration in progress" + } + } } } } From 5cdd6500232c15bb4211811c7a670b3d42b0d995 Mon Sep 17 00:00:00 2001 From: Dawid Pietryga Date: Sat, 22 Jun 2024 13:35:26 +0200 Subject: [PATCH 1003/1445] Add satel integra binary switches unique_id (#118660) --- .../components/satel_integra/binary_sensor.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/satel_integra/binary_sensor.py b/homeassistant/components/satel_integra/binary_sensor.py index b668ced326c..209b6c38cda 100644 --- a/homeassistant/components/satel_integra/binary_sensor.py +++ b/homeassistant/components/satel_integra/binary_sensor.py @@ -41,7 +41,7 @@ async def async_setup_platform( zone_type = device_config_data[CONF_ZONE_TYPE] zone_name = device_config_data[CONF_ZONE_NAME] device = SatelIntegraBinarySensor( - controller, zone_num, zone_name, zone_type, SIGNAL_ZONES_UPDATED + controller, zone_num, zone_name, zone_type, CONF_ZONES, SIGNAL_ZONES_UPDATED ) devices.append(device) @@ -51,7 +51,12 @@ async def async_setup_platform( zone_type = device_config_data[CONF_ZONE_TYPE] zone_name = device_config_data[CONF_ZONE_NAME] device = SatelIntegraBinarySensor( - controller, zone_num, zone_name, zone_type, SIGNAL_OUTPUTS_UPDATED + controller, + zone_num, + zone_name, + zone_type, + CONF_OUTPUTS, + SIGNAL_OUTPUTS_UPDATED, ) devices.append(device) @@ -64,10 +69,17 @@ class SatelIntegraBinarySensor(BinarySensorEntity): _attr_should_poll = False def __init__( - self, controller, device_number, device_name, zone_type, react_to_signal + self, + controller, + device_number, + device_name, + zone_type, + sensor_type, + react_to_signal, ): """Initialize the binary_sensor.""" self._device_number = device_number + self._attr_unique_id = f"satel_{sensor_type}_{device_number}" self._name = device_name self._zone_type = zone_type self._state = 0 From 56d5e41b28bf04749f3e704d764f38ee1027202e Mon Sep 17 00:00:00 2001 From: vmonkey Date: Sat, 22 Jun 2024 13:41:45 +0200 Subject: [PATCH 1004/1445] Add switches to Tuya dehumidifier: anion, filter_reset, and child_lock (#105200) Co-authored-by: Franck Nijhof --- homeassistant/components/tuya/switch.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/homeassistant/components/tuya/switch.py b/homeassistant/components/tuya/switch.py index 3039462be61..f84e63aba37 100644 --- a/homeassistant/components/tuya/switch.py +++ b/homeassistant/components/tuya/switch.py @@ -49,6 +49,28 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { translation_key="water", ), ), + # Dehumidifier + # https://developer.tuya.com/en/docs/iot/s?id=K9gf48r6jke8e + "cs": ( + SwitchEntityDescription( + key=DPCode.ANION, + translation_key="ionizer", + icon="mdi:atom", + entity_category=EntityCategory.CONFIG, + ), + SwitchEntityDescription( + key=DPCode.CHILD_LOCK, + translation_key="child_lock", + icon="mdi:account-lock", + entity_category=EntityCategory.CONFIG, + ), + SwitchEntityDescription( + key=DPCode.FILTER_RESET, + translation_key="filter_reset", + icon="mdi:filter", + entity_category=EntityCategory.CONFIG, + ), + ), # Smart Pet Feeder # https://developer.tuya.com/en/docs/iot/categorycwwsq?id=Kaiuz2b6vydld "cwwsq": ( From 2ce510357d031ffe87dd6d98449c499bc101c284 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Sat, 22 Jun 2024 13:42:20 +0200 Subject: [PATCH 1005/1445] Mark ambilight as not available when off (#120155) --- homeassistant/components/philips_js/light.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/homeassistant/components/philips_js/light.py b/homeassistant/components/philips_js/light.py index d08ecdba8a6..8e500592704 100644 --- a/homeassistant/components/philips_js/light.py +++ b/homeassistant/components/philips_js/light.py @@ -379,3 +379,12 @@ class PhilipsTVLightEntity(PhilipsJsEntity, LightEntity): self._update_from_coordinator() self.async_write_ha_state() + + @property + def available(self) -> bool: + """Return true if entity is available.""" + if not super().available: + return False + if not self.coordinator.api.on: + return False + return self.coordinator.api.powerstate == "On" From 2dfa0a3c9027f90034ba0661f96e669c6cbe192d Mon Sep 17 00:00:00 2001 From: SLaks Date: Sat, 22 Jun 2024 08:30:19 -0400 Subject: [PATCH 1006/1445] Add Jewish Calendar attributes for non-date sensors (#116252) --- .../components/jewish_calendar/sensor.py | 30 +++++++--- .../components/jewish_calendar/strings.json | 11 ++++ .../components/jewish_calendar/test_sensor.py | 60 +++++++++++++++++-- 3 files changed, 88 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/jewish_calendar/sensor.py b/homeassistant/components/jewish_calendar/sensor.py index 88d8ecf1751..aff9d7ee602 100644 --- a/homeassistant/components/jewish_calendar/sensor.py +++ b/homeassistant/components/jewish_calendar/sensor.py @@ -4,9 +4,9 @@ from __future__ import annotations from datetime import date as Date import logging -from typing import Any +from typing import Any, cast -from hdate import HDate +from hdate import HDate, HebrewDate, htables from hdate.zmanim import Zmanim from homeassistant.components.sensor import ( @@ -36,16 +36,19 @@ INFO_SENSORS: tuple[SensorEntityDescription, ...] = ( key="date", name="Date", icon="mdi:star-david", + translation_key="hebrew_date", ), SensorEntityDescription( key="weekly_portion", name="Parshat Hashavua", icon="mdi:book-open-variant", + device_class=SensorDeviceClass.ENUM, ), SensorEntityDescription( key="holiday", name="Holiday", icon="mdi:calendar-star", + device_class=SensorDeviceClass.ENUM, ), SensorEntityDescription( key="omer_count", @@ -190,7 +193,7 @@ class JewishCalendarSensor(SensorEntity): self._candle_lighting_offset = data[CONF_CANDLE_LIGHT_MINUTES] self._havdalah_offset = data[CONF_HAVDALAH_OFFSET_MINUTES] self._diaspora = data[CONF_DIASPORA] - self._holiday_attrs: dict[str, str] = {} + self._attrs: dict[str, str] = {} async def async_update(self) -> None: """Update the state of the sensor.""" @@ -247,9 +250,7 @@ class JewishCalendarSensor(SensorEntity): @property def extra_state_attributes(self) -> dict[str, str]: """Return the state attributes.""" - if self.entity_description.key != "holiday": - return {} - return self._holiday_attrs + return self._attrs def get_state( self, daytime_date: HDate, after_shkia_date: HDate, after_tzais_date: HDate @@ -258,16 +259,31 @@ class JewishCalendarSensor(SensorEntity): # Terminology note: by convention in py-libhdate library, "upcoming" # refers to "current" or "upcoming" dates. if self.entity_description.key == "date": + hdate = cast(HebrewDate, after_shkia_date.hdate) + month = htables.MONTHS[hdate.month.value - 1] + self._attrs = { + "hebrew_year": hdate.year, + "hebrew_month_name": month.hebrew if self._hebrew else month.english, + "hebrew_day": hdate.day, + } return after_shkia_date.hebrew_date if self.entity_description.key == "weekly_portion": + self._attr_options = [ + (p.hebrew if self._hebrew else p.english) for p in htables.PARASHAOT + ] # Compute the weekly portion based on the upcoming shabbat. return after_tzais_date.upcoming_shabbat.parasha if self.entity_description.key == "holiday": - self._holiday_attrs = { + self._attrs = { "id": after_shkia_date.holiday_name, "type": after_shkia_date.holiday_type.name, "type_id": after_shkia_date.holiday_type.value, } + self._attr_options = [ + h.description.hebrew.long if self._hebrew else h.description.english + for h in htables.HOLIDAYS + ] + return after_shkia_date.holiday_description if self.entity_description.key == "omer_count": return after_shkia_date.omer_day diff --git a/homeassistant/components/jewish_calendar/strings.json b/homeassistant/components/jewish_calendar/strings.json index ce659cc0d06..e5367b5819e 100644 --- a/homeassistant/components/jewish_calendar/strings.json +++ b/homeassistant/components/jewish_calendar/strings.json @@ -1,4 +1,15 @@ { + "entity": { + "sensor": { + "hebrew_date": { + "state_attributes": { + "hebrew_year": { "name": "Hebrew Year" }, + "hebrew_month_name": { "name": "Hebrew Month Name" }, + "hebrew_day": { "name": "Hebrew Day" } + } + } + } + }, "config": { "step": { "user": { diff --git a/tests/components/jewish_calendar/test_sensor.py b/tests/components/jewish_calendar/test_sensor.py index 965e461083b..509e17017d5 100644 --- a/tests/components/jewish_calendar/test_sensor.py +++ b/tests/components/jewish_calendar/test_sensor.py @@ -2,6 +2,7 @@ from datetime import datetime as dt, timedelta +from hdate import htables import pytest from homeassistant.components.binary_sensor import DOMAIN as SENSOR_DOMAIN @@ -40,7 +41,17 @@ async def test_jewish_calendar_hebrew(hass: HomeAssistant) -> None: TEST_PARAMS = [ - (dt(2018, 9, 3), "UTC", 31.778, 35.235, "english", "date", False, "23 Elul 5778"), + ( + dt(2018, 9, 3), + "UTC", + 31.778, + 35.235, + "english", + "date", + False, + "23 Elul 5778", + None, + ), ( dt(2018, 9, 3), "UTC", @@ -50,8 +61,19 @@ TEST_PARAMS = [ "date", False, 'כ"ג אלול ה\' תשע"ח', + None, + ), + ( + dt(2018, 9, 10), + "UTC", + 31.778, + 35.235, + "hebrew", + "holiday", + False, + "א' ראש השנה", + None, ), - (dt(2018, 9, 10), "UTC", 31.778, 35.235, "hebrew", "holiday", False, "א' ראש השנה"), ( dt(2018, 9, 10), "UTC", @@ -61,6 +83,15 @@ TEST_PARAMS = [ "holiday", False, "Rosh Hashana I", + { + "device_class": "enum", + "friendly_name": "Jewish Calendar Holiday", + "icon": "mdi:calendar-star", + "id": "rosh_hashana_i", + "type": "YOM_TOV", + "type_id": 1, + "options": [h.description.english for h in htables.HOLIDAYS], + }, ), ( dt(2018, 9, 8), @@ -71,6 +102,12 @@ TEST_PARAMS = [ "parshat_hashavua", False, "נצבים", + { + "device_class": "enum", + "friendly_name": "Jewish Calendar Parshat Hashavua", + "icon": "mdi:book-open-variant", + "options": [p.hebrew for p in htables.PARASHAOT], + }, ), ( dt(2018, 9, 8), @@ -81,6 +118,7 @@ TEST_PARAMS = [ "t_set_hakochavim", True, dt(2018, 9, 8, 19, 45), + None, ), ( dt(2018, 9, 8), @@ -91,6 +129,7 @@ TEST_PARAMS = [ "t_set_hakochavim", False, dt(2018, 9, 8, 19, 19), + None, ), ( dt(2018, 10, 14), @@ -101,6 +140,7 @@ TEST_PARAMS = [ "parshat_hashavua", False, "לך לך", + None, ), ( dt(2018, 10, 14, 17, 0, 0), @@ -111,6 +151,7 @@ TEST_PARAMS = [ "date", False, "ה' מרחשוון ה' תשע\"ט", + None, ), ( dt(2018, 10, 14, 19, 0, 0), @@ -121,6 +162,13 @@ TEST_PARAMS = [ "date", False, "ו' מרחשוון ה' תשע\"ט", + { + "hebrew_year": 5779, + "hebrew_month_name": "מרחשוון", + "hebrew_day": 6, + "icon": "mdi:star-david", + "friendly_name": "Jewish Calendar Date", + }, ), ] @@ -148,6 +196,7 @@ TEST_IDS = [ "sensor", "diaspora", "result", + "attrs", ), TEST_PARAMS, ids=TEST_IDS, @@ -162,6 +211,7 @@ async def test_jewish_calendar_sensor( sensor, diaspora, result, + attrs, ) -> None: """Test Jewish calendar sensor output.""" time_zone = dt_util.get_time_zone(tzname) @@ -196,10 +246,8 @@ async def test_jewish_calendar_sensor( sensor_object = hass.states.get(f"sensor.jewish_calendar_{sensor}") assert sensor_object.state == result - if sensor == "holiday": - assert sensor_object.attributes.get("id") == "rosh_hashana_i" - assert sensor_object.attributes.get("type") == "YOM_TOV" - assert sensor_object.attributes.get("type_id") == 1 + if attrs: + assert sensor_object.attributes == attrs SHABBAT_PARAMS = [ From 89b7bf21088934676d4828179e2fc4aa0ebcccac Mon Sep 17 00:00:00 2001 From: dougiteixeira <31328123+dougiteixeira@users.noreply.github.com> Date: Sat, 22 Jun 2024 10:03:43 -0300 Subject: [PATCH 1007/1445] Add the ability to change the source entity of the Derivative helper (#119754) --- .../components/derivative/__init__.py | 10 ++- .../components/derivative/config_flow.py | 78 +++++++++++++---- homeassistant/components/derivative/sensor.py | 32 ++----- .../components/derivative/test_config_flow.py | 23 +++-- tests/components/derivative/test_init.py | 87 ++++++++++++++++++- 5 files changed, 181 insertions(+), 49 deletions(-) diff --git a/homeassistant/components/derivative/__init__.py b/homeassistant/components/derivative/__init__.py index 2b365e96244..5117663f3c5 100644 --- a/homeassistant/components/derivative/__init__.py +++ b/homeassistant/components/derivative/__init__.py @@ -3,12 +3,20 @@ from __future__ import annotations from homeassistant.config_entries import ConfigEntry -from homeassistant.const import Platform +from homeassistant.const import CONF_SOURCE, Platform from homeassistant.core import HomeAssistant +from homeassistant.helpers.device import ( + async_remove_stale_devices_links_keep_entity_device, +) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Derivative from a config entry.""" + + async_remove_stale_devices_links_keep_entity_device( + hass, entry.entry_id, entry.options[CONF_SOURCE] + ) + await hass.config_entries.async_forward_entry_setups(entry, (Platform.SENSOR,)) entry.async_on_unload(entry.add_update_listener(config_entry_update_listener)) return True diff --git a/homeassistant/components/derivative/config_flow.py b/homeassistant/components/derivative/config_flow.py index e15741ce9cf..2ef2018eda8 100644 --- a/homeassistant/components/derivative/config_flow.py +++ b/homeassistant/components/derivative/config_flow.py @@ -10,11 +10,19 @@ import voluptuous as vol from homeassistant.components.counter import DOMAIN as COUNTER_DOMAIN from homeassistant.components.input_number import DOMAIN as INPUT_NUMBER_DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN -from homeassistant.const import CONF_NAME, CONF_SOURCE, UnitOfTime +from homeassistant.const import ( + ATTR_UNIT_OF_MEASUREMENT, + CONF_NAME, + CONF_SOURCE, + UnitOfTime, +) +from homeassistant.core import callback from homeassistant.helpers import selector from homeassistant.helpers.schema_config_entry_flow import ( + SchemaCommonFlowHandler, SchemaConfigFlowHandler, SchemaFlowFormStep, + SchemaOptionsFlowHandler, ) from .const import ( @@ -42,8 +50,43 @@ TIME_UNITS = [ UnitOfTime.DAYS, ] -OPTIONS_SCHEMA = vol.Schema( - { +ALLOWED_DOMAINS = [COUNTER_DOMAIN, INPUT_NUMBER_DOMAIN, SENSOR_DOMAIN] + + +@callback +def entity_selector_compatible( + handler: SchemaOptionsFlowHandler, +) -> selector.EntitySelector: + """Return an entity selector which compatible entities.""" + current = handler.hass.states.get(handler.options[CONF_SOURCE]) + unit_of_measurement = ( + current.attributes.get(ATTR_UNIT_OF_MEASUREMENT) if current else None + ) + + entities = [ + ent.entity_id + for ent in handler.hass.states.async_all(ALLOWED_DOMAINS) + if ent.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == unit_of_measurement + and ent.domain in ALLOWED_DOMAINS + ] + + return selector.EntitySelector( + selector.EntitySelectorConfig(include_entities=entities) + ) + + +async def _get_options_dict(handler: SchemaCommonFlowHandler | None) -> dict: + if handler is None or not isinstance( + handler.parent_handler, SchemaOptionsFlowHandler + ): + entity_selector = selector.EntitySelector( + selector.EntitySelectorConfig(domain=ALLOWED_DOMAINS) + ) + else: + entity_selector = entity_selector_compatible(handler.parent_handler) + + return { + vol.Required(CONF_SOURCE): entity_selector, vol.Required(CONF_ROUND_DIGITS, default=2): selector.NumberSelector( selector.NumberSelectorConfig( min=0, @@ -62,25 +105,28 @@ OPTIONS_SCHEMA = vol.Schema( ), ), } -) -CONFIG_SCHEMA = vol.Schema( - { - vol.Required(CONF_NAME): selector.TextSelector(), - vol.Required(CONF_SOURCE): selector.EntitySelector( - selector.EntitySelectorConfig( - domain=[COUNTER_DOMAIN, INPUT_NUMBER_DOMAIN, SENSOR_DOMAIN] - ), - ), - } -).extend(OPTIONS_SCHEMA.schema) + +async def _get_options_schema(handler: SchemaCommonFlowHandler) -> vol.Schema: + return vol.Schema(await _get_options_dict(handler)) + + +async def _get_config_schema(handler: SchemaCommonFlowHandler) -> vol.Schema: + options = await _get_options_dict(handler) + return vol.Schema( + { + vol.Required(CONF_NAME): selector.TextSelector(), + **options, + } + ) + CONFIG_FLOW = { - "user": SchemaFlowFormStep(CONFIG_SCHEMA), + "user": SchemaFlowFormStep(_get_config_schema), } OPTIONS_FLOW = { - "init": SchemaFlowFormStep(OPTIONS_SCHEMA), + "init": SchemaFlowFormStep(_get_options_schema), } diff --git a/homeassistant/components/derivative/sensor.py b/homeassistant/components/derivative/sensor.py index d5a83035ed5..fd430c6ef4d 100644 --- a/homeassistant/components/derivative/sensor.py +++ b/homeassistant/components/derivative/sensor.py @@ -20,11 +20,8 @@ from homeassistant.const import ( UnitOfTime, ) from homeassistant.core import Event, EventStateChangedData, HomeAssistant, callback -from homeassistant.helpers import ( - config_validation as cv, - device_registry as dr, - entity_registry as er, -) +from homeassistant.helpers import config_validation as cv, entity_registry as er +from homeassistant.helpers.device import async_device_info_to_link_from_entity from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_state_change_event @@ -90,27 +87,10 @@ async def async_setup_entry( registry, config_entry.options[CONF_SOURCE] ) - source_entity = registry.async_get(source_entity_id) - dev_reg = dr.async_get(hass) - # Resolve source entity device - if ( - (source_entity is not None) - and (source_entity.device_id is not None) - and ( - ( - device := dev_reg.async_get( - device_id=source_entity.device_id, - ) - ) - is not None - ) - ): - device_info = DeviceInfo( - identifiers=device.identifiers, - connections=device.connections, - ) - else: - device_info = None + device_info = async_device_info_to_link_from_entity( + hass, + source_entity_id, + ) if (unit_prefix := config_entry.options.get(CONF_UNIT_PREFIX)) == "none": # Before we had support for optional selectors, "none" was used for selecting nothing diff --git a/tests/components/derivative/test_config_flow.py b/tests/components/derivative/test_config_flow.py index d111df76ece..efdde93173c 100644 --- a/tests/components/derivative/test_config_flow.py +++ b/tests/components/derivative/test_config_flow.py @@ -8,6 +8,7 @@ from homeassistant import config_entries from homeassistant.components.derivative.const import DOMAIN from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers import selector from tests.common import MockConfigEntry @@ -95,6 +96,10 @@ async def test_options(hass: HomeAssistant, platform) -> None: assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() + hass.states.async_set("sensor.input", 10, {"unit_of_measurement": "dog"}) + hass.states.async_set("sensor.valid", 10, {"unit_of_measurement": "dog"}) + hass.states.async_set("sensor.invalid", 10, {"unit_of_measurement": "cat"}) + result = await hass.config_entries.options.async_init(config_entry.entry_id) assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" @@ -104,9 +109,17 @@ async def test_options(hass: HomeAssistant, platform) -> None: assert get_suggested(schema, "unit_prefix") == "k" assert get_suggested(schema, "unit_time") == "min" + source = schema["source"] + assert isinstance(source, selector.EntitySelector) + assert source.config["include_entities"] == [ + "sensor.input", + "sensor.valid", + ] + result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={ + "source": "sensor.valid", "round": 2.0, "time_window": {"seconds": 10.0}, "unit_time": "h", @@ -116,7 +129,7 @@ async def test_options(hass: HomeAssistant, platform) -> None: assert result["data"] == { "name": "My derivative", "round": 2.0, - "source": "sensor.input", + "source": "sensor.valid", "time_window": {"seconds": 10.0}, "unit_time": "h", } @@ -124,7 +137,7 @@ async def test_options(hass: HomeAssistant, platform) -> None: assert config_entry.options == { "name": "My derivative", "round": 2.0, - "source": "sensor.input", + "source": "sensor.valid", "time_window": {"seconds": 10.0}, "unit_time": "h", } @@ -134,11 +147,11 @@ async def test_options(hass: HomeAssistant, platform) -> None: await hass.async_block_till_done() # Check the entity was updated, no new entity was created - assert len(hass.states.async_all()) == 1 + assert len(hass.states.async_all()) == 4 # Check the state of the entity has changed as expected - hass.states.async_set("sensor.input", 10, {"unit_of_measurement": "cat"}) - hass.states.async_set("sensor.input", 11, {"unit_of_measurement": "cat"}) + hass.states.async_set("sensor.valid", 10, {"unit_of_measurement": "cat"}) + hass.states.async_set("sensor.valid", 11, {"unit_of_measurement": "cat"}) await hass.async_block_till_done() state = hass.states.get(f"{platform}.my_derivative") assert state.attributes["unit_of_measurement"] == "cat/h" diff --git a/tests/components/derivative/test_init.py b/tests/components/derivative/test_init.py index 34fe385032b..32b763ee84d 100644 --- a/tests/components/derivative/test_init.py +++ b/tests/components/derivative/test_init.py @@ -4,7 +4,7 @@ import pytest from homeassistant.components.derivative.const import DOMAIN from homeassistant.core import HomeAssistant -from homeassistant.helpers import entity_registry as er +from homeassistant.helpers import device_registry as dr, entity_registry as er from tests.common import MockConfigEntry @@ -60,3 +60,88 @@ async def test_setup_and_remove_config_entry( # Check the state and entity registry entry are removed assert hass.states.get(derivative_entity_id) is None assert entity_registry.async_get(derivative_entity_id) is None + + +async def test_device_cleaning(hass: HomeAssistant) -> None: + """Test for source entity device for Derivative.""" + device_registry = dr.async_get(hass) + entity_registry = er.async_get(hass) + + # Source entity device config entry + source_config_entry = MockConfigEntry() + source_config_entry.add_to_hass(hass) + + # Device entry of the source entity + source_device1_entry = device_registry.async_get_or_create( + config_entry_id=source_config_entry.entry_id, + identifiers={("sensor", "identifier_test1")}, + connections={("mac", "30:31:32:33:34:01")}, + ) + + # Source entity registry + source_entity = entity_registry.async_get_or_create( + "sensor", + "test", + "source", + config_entry=source_config_entry, + device_id=source_device1_entry.id, + ) + await hass.async_block_till_done() + assert entity_registry.async_get("sensor.test_source") is not None + + # Configure the configuration entry for Derivative + derivative_config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={ + "name": "Derivative", + "round": 1.0, + "source": "sensor.test_source", + "time_window": {"seconds": 0.0}, + "unit_prefix": "k", + "unit_time": "min", + }, + title="Derivative", + ) + derivative_config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(derivative_config_entry.entry_id) + await hass.async_block_till_done() + + # Confirm the link between the source entity device and the derivative sensor + derivative_entity = entity_registry.async_get("sensor.derivative") + assert derivative_entity is not None + assert derivative_entity.device_id == source_entity.device_id + + # Device entry incorrectly linked to Derivative config entry + device_registry.async_get_or_create( + config_entry_id=derivative_config_entry.entry_id, + identifiers={("sensor", "identifier_test2")}, + connections={("mac", "30:31:32:33:34:02")}, + ) + device_registry.async_get_or_create( + config_entry_id=derivative_config_entry.entry_id, + identifiers={("sensor", "identifier_test3")}, + connections={("mac", "30:31:32:33:34:03")}, + ) + await hass.async_block_till_done() + + # Before reloading the config entry, two devices are expected to be linked + devices_before_reload = device_registry.devices.get_devices_for_config_entry_id( + derivative_config_entry.entry_id + ) + assert len(devices_before_reload) == 3 + + # Config entry reload + await hass.config_entries.async_reload(derivative_config_entry.entry_id) + await hass.async_block_till_done() + + # Confirm the link between the source entity device and the derivative sensor after reload + derivative_entity = entity_registry.async_get("sensor.derivative") + assert derivative_entity is not None + assert derivative_entity.device_id == source_entity.device_id + + # After reloading the config entry, only one linked device is expected + devices_after_reload = device_registry.devices.get_devices_for_config_entry_id( + derivative_config_entry.entry_id + ) + assert len(devices_after_reload) == 1 From cea7231aab298f74bc4c56bce5528e73143f4f02 Mon Sep 17 00:00:00 2001 From: tronikos Date: Sat, 22 Jun 2024 06:04:52 -0700 Subject: [PATCH 1008/1445] Add notify entities in Fully Kiosk Browser (#119371) --- .../components/fully_kiosk/__init__.py | 1 + .../components/fully_kiosk/notify.py | 74 +++++++++++++++++++ .../components/fully_kiosk/strings.json | 8 ++ tests/components/fully_kiosk/test_notify.py | 70 ++++++++++++++++++ 4 files changed, 153 insertions(+) create mode 100644 homeassistant/components/fully_kiosk/notify.py create mode 100644 tests/components/fully_kiosk/test_notify.py diff --git a/homeassistant/components/fully_kiosk/__init__.py b/homeassistant/components/fully_kiosk/__init__.py index 95d7d59ecbf..582ae23aea4 100644 --- a/homeassistant/components/fully_kiosk/__init__.py +++ b/homeassistant/components/fully_kiosk/__init__.py @@ -15,6 +15,7 @@ PLATFORMS = [ Platform.BUTTON, Platform.CAMERA, Platform.MEDIA_PLAYER, + Platform.NOTIFY, Platform.NUMBER, Platform.SENSOR, Platform.SWITCH, diff --git a/homeassistant/components/fully_kiosk/notify.py b/homeassistant/components/fully_kiosk/notify.py new file mode 100644 index 00000000000..aa47c178f03 --- /dev/null +++ b/homeassistant/components/fully_kiosk/notify.py @@ -0,0 +1,74 @@ +"""Support for Fully Kiosk Browser notifications.""" + +from __future__ import annotations + +from dataclasses import dataclass + +from fullykiosk import FullyKioskError + +from homeassistant.components.notify import NotifyEntity, NotifyEntityDescription +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .coordinator import FullyKioskDataUpdateCoordinator +from .entity import FullyKioskEntity + + +@dataclass(frozen=True, kw_only=True) +class FullyNotifyEntityDescription(NotifyEntityDescription): + """Fully Kiosk Browser notify entity description.""" + + cmd: str + + +NOTIFIERS: tuple[FullyNotifyEntityDescription, ...] = ( + FullyNotifyEntityDescription( + key="overlay_message", + translation_key="overlay_message", + cmd="setOverlayMessage", + ), + FullyNotifyEntityDescription( + key="tts", + translation_key="tts", + cmd="textToSpeech", + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up the Fully Kiosk Browser notify entities.""" + coordinator: FullyKioskDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + async_add_entities( + FullyNotifyEntity(coordinator, description) for description in NOTIFIERS + ) + + +class FullyNotifyEntity(FullyKioskEntity, NotifyEntity): + """Implement the notify entity for Fully Kiosk Browser.""" + + entity_description: FullyNotifyEntityDescription + + def __init__( + self, + coordinator: FullyKioskDataUpdateCoordinator, + description: FullyNotifyEntityDescription, + ) -> None: + """Initialize the entity.""" + FullyKioskEntity.__init__(self, coordinator) + NotifyEntity.__init__(self) + self.entity_description = description + self._attr_unique_id = f"{coordinator.data['deviceID']}-{description.key}" + + async def async_send_message(self, message: str, title: str | None = None) -> None: + """Send a message.""" + try: + await self.coordinator.fully.sendCommand( + self.entity_description.cmd, text=message + ) + except FullyKioskError as err: + raise HomeAssistantError(err) from err diff --git a/homeassistant/components/fully_kiosk/strings.json b/homeassistant/components/fully_kiosk/strings.json index c1a1ef1fcf0..c6fe65b8383 100644 --- a/homeassistant/components/fully_kiosk/strings.json +++ b/homeassistant/components/fully_kiosk/strings.json @@ -56,6 +56,14 @@ "name": "Load start URL" } }, + "notify": { + "overlay_message": { + "name": "Overlay message" + }, + "tts": { + "name": "Text to speech" + } + }, "number": { "screensaver_time": { "name": "Screensaver timer" diff --git a/tests/components/fully_kiosk/test_notify.py b/tests/components/fully_kiosk/test_notify.py new file mode 100644 index 00000000000..727457f1b84 --- /dev/null +++ b/tests/components/fully_kiosk/test_notify.py @@ -0,0 +1,70 @@ +"""Test the Fully Kiosk Browser notify platform.""" + +from unittest.mock import MagicMock + +from fullykiosk import FullyKioskError +import pytest + +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError + +from tests.common import MockConfigEntry + + +async def test_notify_text_to_speech( + hass: HomeAssistant, + mock_fully_kiosk: MagicMock, + init_integration: MockConfigEntry, +) -> None: + """Test notify text to speech entity.""" + message = "one, two, testing, testing" + await hass.services.async_call( + "notify", + "send_message", + { + "entity_id": "notify.amazon_fire_text_to_speech", + "message": message, + }, + blocking=True, + ) + mock_fully_kiosk.sendCommand.assert_called_with("textToSpeech", text=message) + + +async def test_notify_text_to_speech_raises( + hass: HomeAssistant, + mock_fully_kiosk: MagicMock, + init_integration: MockConfigEntry, +) -> None: + """Test notify text to speech entity raises.""" + mock_fully_kiosk.sendCommand.side_effect = FullyKioskError("error", "status") + message = "one, two, testing, testing" + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + "notify", + "send_message", + { + "entity_id": "notify.amazon_fire_text_to_speech", + "message": message, + }, + blocking=True, + ) + mock_fully_kiosk.sendCommand.assert_called_with("textToSpeech", text=message) + + +async def test_notify_overlay_message( + hass: HomeAssistant, + mock_fully_kiosk: MagicMock, + init_integration: MockConfigEntry, +) -> None: + """Test notify overlay message entity.""" + message = "one, two, testing, testing" + await hass.services.async_call( + "notify", + "send_message", + { + "entity_id": "notify.amazon_fire_overlay_message", + "message": message, + }, + blocking=True, + ) + mock_fully_kiosk.sendCommand.assert_called_with("setOverlayMessage", text=message) From f2a4566eefb153429fc3c45a78f35a5b4816f1df Mon Sep 17 00:00:00 2001 From: "Mr. Bubbles" Date: Sat, 22 Jun 2024 15:14:53 +0200 Subject: [PATCH 1009/1445] Add re-auth flow to Bring integration (#115327) --- homeassistant/components/bring/__init__.py | 7 +- homeassistant/components/bring/config_flow.py | 87 ++++++++++++---- homeassistant/components/bring/coordinator.py | 16 ++- homeassistant/components/bring/strings.json | 11 ++- tests/components/bring/conftest.py | 4 +- tests/components/bring/test_config_flow.py | 98 ++++++++++++++++++- tests/components/bring/test_init.py | 4 +- 7 files changed, 194 insertions(+), 33 deletions(-) diff --git a/homeassistant/components/bring/__init__.py b/homeassistant/components/bring/__init__.py index 72d3894af3a..30cbbbbbfa0 100644 --- a/homeassistant/components/bring/__init__.py +++ b/homeassistant/components/bring/__init__.py @@ -14,7 +14,7 @@ from bring_api.exceptions import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, Platform from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryError, ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import DOMAIN @@ -38,7 +38,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: BringConfigEntry) -> boo try: await bring.login() - await bring.load_lists() except BringRequestException as e: raise ConfigEntryNotReady( translation_domain=DOMAIN, @@ -47,10 +46,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: BringConfigEntry) -> boo except BringParseException as e: raise ConfigEntryNotReady( translation_domain=DOMAIN, - translation_key="setup_request_exception", + translation_key="setup_parse_exception", ) from e except BringAuthException as e: - raise ConfigEntryError( + raise ConfigEntryAuthFailed( translation_domain=DOMAIN, translation_key="setup_authentication_exception", translation_placeholders={CONF_EMAIL: email}, diff --git a/homeassistant/components/bring/config_flow.py b/homeassistant/components/bring/config_flow.py index 756b2312e88..333837a20f2 100644 --- a/homeassistant/components/bring/config_flow.py +++ b/homeassistant/components/bring/config_flow.py @@ -2,15 +2,17 @@ from __future__ import annotations +from collections.abc import Mapping import logging from typing import Any from bring_api.bring import Bring from bring_api.exceptions import BringAuthException, BringRequestException +from bring_api.types import BringAuthResponse import voluptuous as vol from homeassistant.config_entries import ConfigFlow, ConfigFlowResult -from homeassistant.const import CONF_EMAIL, CONF_PASSWORD +from homeassistant.const import CONF_EMAIL, CONF_NAME, CONF_PASSWORD from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.selector import ( TextSelector, @@ -18,6 +20,7 @@ from homeassistant.helpers.selector import ( TextSelectorType, ) +from . import BringConfigEntry from .const import DOMAIN _LOGGER = logging.getLogger(__name__) @@ -42,33 +45,75 @@ class BringConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Bring!.""" VERSION = 1 + reauth_entry: BringConfigEntry | None = None + info: BringAuthResponse async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: """Handle the initial step.""" errors: dict[str, str] = {} - if user_input is not None: - session = async_get_clientsession(self.hass) - bring = Bring(session, user_input[CONF_EMAIL], user_input[CONF_PASSWORD]) - - try: - info = await bring.login() - await bring.load_lists() - except BringRequestException: - errors["base"] = "cannot_connect" - except BringAuthException: - errors["base"] = "invalid_auth" - except Exception: - _LOGGER.exception("Unexpected exception") - errors["base"] = "unknown" - else: - await self.async_set_unique_id(bring.uuid) - self._abort_if_unique_id_configured() - return self.async_create_entry( - title=info["name"] or user_input[CONF_EMAIL], data=user_input - ) + if user_input is not None and not ( + errors := await self.validate_input(user_input) + ): + self._abort_if_unique_id_configured() + return self.async_create_entry( + title=self.info["name"] or user_input[CONF_EMAIL], data=user_input + ) return self.async_show_form( step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors ) + + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: + """Perform reauth upon an API authentication error.""" + self.reauth_entry = self.hass.config_entries.async_get_entry( + self.context["entry_id"] + ) + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Dialog that informs the user that reauth is required.""" + errors: dict[str, str] = {} + + assert self.reauth_entry + + if user_input is not None: + if not (errors := await self.validate_input(user_input)): + return self.async_update_reload_and_abort( + self.reauth_entry, data=user_input + ) + + return self.async_show_form( + step_id="reauth_confirm", + data_schema=self.add_suggested_values_to_schema( + data_schema=STEP_USER_DATA_SCHEMA, + suggested_values={CONF_EMAIL: self.reauth_entry.data[CONF_EMAIL]}, + ), + description_placeholders={CONF_NAME: self.reauth_entry.title}, + errors=errors, + ) + + async def validate_input(self, user_input: Mapping[str, Any]) -> dict[str, str]: + """Auth Helper.""" + + errors: dict[str, str] = {} + session = async_get_clientsession(self.hass) + bring = Bring(session, user_input[CONF_EMAIL], user_input[CONF_PASSWORD]) + + try: + self.info = await bring.login() + except BringRequestException: + errors["base"] = "cannot_connect" + except BringAuthException: + errors["base"] = "invalid_auth" + except Exception: + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + await self.async_set_unique_id(bring.uuid) + return errors diff --git a/homeassistant/components/bring/coordinator.py b/homeassistant/components/bring/coordinator.py index 1447338d408..222c650e614 100644 --- a/homeassistant/components/bring/coordinator.py +++ b/homeassistant/components/bring/coordinator.py @@ -14,7 +14,9 @@ from bring_api.exceptions import ( from bring_api.types import BringItemsResponse, BringList from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_EMAIL from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import DOMAIN @@ -49,8 +51,20 @@ class BringDataUpdateCoordinator(DataUpdateCoordinator[dict[str, BringData]]): except BringParseException as e: raise UpdateFailed("Unable to parse response from bring") from e except BringAuthException as e: + # try to recover by refreshing access token, otherwise + # initiate reauth flow + try: + await self.bring.retrieve_new_access_token() + except (BringRequestException, BringParseException) as exc: + raise UpdateFailed("Refreshing authentication token failed") from exc + except BringAuthException as exc: + raise ConfigEntryAuthFailed( + translation_domain=DOMAIN, + translation_key="setup_authentication_exception", + translation_placeholders={CONF_EMAIL: self.bring.mail}, + ) from exc raise UpdateFailed( - "Unable to retrieve data from bring, authentication failed" + "Authentication failed but re-authentication was successful, trying again later" ) from e list_dict: dict[str, BringData] = {} diff --git a/homeassistant/components/bring/strings.json b/homeassistant/components/bring/strings.json index 5deb0759c17..652958a1b1f 100644 --- a/homeassistant/components/bring/strings.json +++ b/homeassistant/components/bring/strings.json @@ -6,6 +6,14 @@ "email": "[%key:common::config_flow::data::email%]", "password": "[%key:common::config_flow::data::password%]" } + }, + "reauth_confirm": { + "title": "[%key:common::config_flow::title::reauth%]", + "description": "The Bring! integration needs to re-authenticate your account", + "data": { + "email": "[%key:common::config_flow::data::email%]", + "password": "[%key:common::config_flow::data::password%]" + } } }, "error": { @@ -14,7 +22,8 @@ "unknown": "[%key:common::config_flow::error::unknown%]" }, "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" + "already_configured": "[%key:common::config_flow::abort::already_configured_service%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" } }, "exceptions": { diff --git a/tests/components/bring/conftest.py b/tests/components/bring/conftest.py index 0760bdd296a..25330c10ba4 100644 --- a/tests/components/bring/conftest.py +++ b/tests/components/bring/conftest.py @@ -1,7 +1,9 @@ """Common fixtures for the Bring! tests.""" +from typing import cast from unittest.mock import AsyncMock, patch +from bring_api.types import BringAuthResponse import pytest from typing_extensions import Generator @@ -40,7 +42,7 @@ def mock_bring_client() -> Generator[AsyncMock]: ): client = mock_client.return_value client.uuid = UUID - client.login.return_value = {"name": "Bring"} + client.login.return_value = cast(BringAuthResponse, {"name": "Bring"}) client.load_lists.return_value = {"lists": []} yield client diff --git a/tests/components/bring/test_config_flow.py b/tests/components/bring/test_config_flow.py index 86fdbc1853b..d307e0ccbbe 100644 --- a/tests/components/bring/test_config_flow.py +++ b/tests/components/bring/test_config_flow.py @@ -9,8 +9,8 @@ from bring_api.exceptions import ( ) import pytest -from homeassistant import config_entries from homeassistant.components.bring.const import DOMAIN +from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER from homeassistant.const import CONF_EMAIL, CONF_PASSWORD from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -30,7 +30,7 @@ async def test_form( ) -> None: """Test we get the form.""" result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": SOURCE_USER} ) assert result["type"] is FlowResultType.FORM assert result["errors"] == {} @@ -66,7 +66,7 @@ async def test_flow_user_init_data_unknown_error_and_recover( mock_bring_client.login.side_effect = raise_error result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"} + DOMAIN, context={"source": SOURCE_USER} ) result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -112,3 +112,95 @@ async def test_flow_user_init_data_already_configured( assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" + + +async def test_flow_reauth( + hass: HomeAssistant, + mock_bring_client: AsyncMock, + bring_config_entry: MockConfigEntry, +) -> None: + """Test reauth flow.""" + + bring_config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": SOURCE_REAUTH, + "entry_id": bring_config_entry.entry_id, + "unique_id": bring_config_entry.unique_id, + }, + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_EMAIL: "new-email", CONF_PASSWORD: "new-password"}, + ) + + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reauth_successful" + assert bring_config_entry.data == { + CONF_EMAIL: "new-email", + CONF_PASSWORD: "new-password", + } + assert len(hass.config_entries.async_entries()) == 1 + + +@pytest.mark.parametrize( + ("raise_error", "text_error"), + [ + (BringRequestException(), "cannot_connect"), + (BringAuthException(), "invalid_auth"), + (BringParseException(), "unknown"), + (IndexError(), "unknown"), + ], +) +async def test_flow_reauth_error_and_recover( + hass: HomeAssistant, + mock_bring_client: AsyncMock, + bring_config_entry: MockConfigEntry, + raise_error, + text_error, +) -> None: + """Test reauth flow.""" + + bring_config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": SOURCE_REAUTH, + "entry_id": bring_config_entry.entry_id, + "unique_id": bring_config_entry.unique_id, + }, + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + + mock_bring_client.login.side_effect = raise_error + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_EMAIL: "new-email", CONF_PASSWORD: "new-password"}, + ) + + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"base": text_error} + + mock_bring_client.login.side_effect = None + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_EMAIL: "new-email", CONF_PASSWORD: "new-password"}, + ) + + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reauth_successful" + + assert len(hass.config_entries.async_entries()) == 1 diff --git a/tests/components/bring/test_init.py b/tests/components/bring/test_init.py index db402bdd6d1..f1b1f78e775 100644 --- a/tests/components/bring/test_init.py +++ b/tests/components/bring/test_init.py @@ -13,7 +13,7 @@ from homeassistant.components.bring import ( from homeassistant.components.bring.const import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryError, ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from tests.common import MockConfigEntry @@ -70,7 +70,7 @@ async def test_init_failure( ("exception", "expected"), [ (BringRequestException, ConfigEntryNotReady), - (BringAuthException, ConfigEntryError), + (BringAuthException, ConfigEntryAuthFailed), (BringParseException, ConfigEntryNotReady), ], ) From 30f3f1082f3dbac2df37149d6dbca33b614d8ec7 Mon Sep 17 00:00:00 2001 From: dougiteixeira <31328123+dougiteixeira@users.noreply.github.com> Date: Sat, 22 Jun 2024 10:30:38 -0300 Subject: [PATCH 1010/1445] Use the new device helpers in Integral (#120157) --- .../components/integration/__init__.py | 21 +++-- .../components/integration/sensor.py | 32 ++----- tests/components/integration/test_init.py | 88 +++++++++++++++++++ 3 files changed, 106 insertions(+), 35 deletions(-) diff --git a/homeassistant/components/integration/__init__.py b/homeassistant/components/integration/__init__.py index effa0c4df55..4ccf0dec258 100644 --- a/homeassistant/components/integration/__init__.py +++ b/homeassistant/components/integration/__init__.py @@ -5,11 +5,22 @@ from __future__ import annotations from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant -from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.device import ( + async_remove_stale_devices_links_keep_entity_device, +) + +from .const import CONF_SOURCE_SENSOR async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Integration from a config entry.""" + + async_remove_stale_devices_links_keep_entity_device( + hass, + entry.entry_id, + entry.options[CONF_SOURCE_SENSOR], + ) + await hass.config_entries.async_forward_entry_setups(entry, (Platform.SENSOR,)) entry.async_on_unload(entry.add_update_listener(config_entry_update_listener)) return True @@ -19,14 +30,6 @@ async def config_entry_update_listener(hass: HomeAssistant, entry: ConfigEntry) """Update listener, called when the config entry options are changed.""" # Remove device link for entry, the source device may have changed. # The link will be recreated after load. - device_registry = dr.async_get(hass) - devices = device_registry.devices.get_devices_for_config_entry_id(entry.entry_id) - - for device in devices: - device_registry.async_update_device( - device.id, remove_config_entry_id=entry.entry_id - ) - await hass.config_entries.async_reload(entry.entry_id) diff --git a/homeassistant/components/integration/sensor.py b/homeassistant/components/integration/sensor.py index d201fab0c6f..ffb7a3d8e6a 100644 --- a/homeassistant/components/integration/sensor.py +++ b/homeassistant/components/integration/sensor.py @@ -38,11 +38,8 @@ from homeassistant.core import ( State, callback, ) -from homeassistant.helpers import ( - config_validation as cv, - device_registry as dr, - entity_registry as er, -) +from homeassistant.helpers import config_validation as cv, entity_registry as er +from homeassistant.helpers.device import async_device_info_to_link_from_entity from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_call_later, async_track_state_change_event @@ -249,27 +246,10 @@ async def async_setup_entry( registry, config_entry.options[CONF_SOURCE_SENSOR] ) - source_entity = er.EntityRegistry.async_get(registry, source_entity_id) - dev_reg = dr.async_get(hass) - # Resolve source entity device - if ( - (source_entity is not None) - and (source_entity.device_id is not None) - and ( - ( - device := dev_reg.async_get( - device_id=source_entity.device_id, - ) - ) - is not None - ) - ): - device_info = DeviceInfo( - identifiers=device.identifiers, - connections=device.connections, - ) - else: - device_info = None + device_info = async_device_info_to_link_from_entity( + hass, + source_entity_id, + ) if (unit_prefix := config_entry.options.get(CONF_UNIT_PREFIX)) == "none": # Before we had support for optional selectors, "none" was used for selecting nothing diff --git a/tests/components/integration/test_init.py b/tests/components/integration/test_init.py index 2ed32c7645c..9fee54f4500 100644 --- a/tests/components/integration/test_init.py +++ b/tests/components/integration/test_init.py @@ -121,3 +121,91 @@ async def test_entry_changed(hass: HomeAssistant, platform) -> None: # Check that the config entry association has updated assert config_entry.entry_id not in _get_device_config_entries(input_entry) assert config_entry.entry_id in _get_device_config_entries(valid_entry) + + +async def test_device_cleaning( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, +) -> None: + """Test for source entity device for Integration.""" + + # Source entity device config entry + source_config_entry = MockConfigEntry() + source_config_entry.add_to_hass(hass) + + # Device entry of the source entity + source_device1_entry = device_registry.async_get_or_create( + config_entry_id=source_config_entry.entry_id, + identifiers={("sensor", "identifier_test1")}, + connections={("mac", "30:31:32:33:34:01")}, + ) + + # Source entity registry + source_entity = entity_registry.async_get_or_create( + "sensor", + "test", + "source", + config_entry=source_config_entry, + device_id=source_device1_entry.id, + ) + await hass.async_block_till_done() + assert entity_registry.async_get("sensor.test_source") is not None + + # Configure the configuration entry for Integration + integration_config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={ + "method": "trapezoidal", + "name": "Integration", + "round": 1.0, + "source": "sensor.test_source", + "unit_prefix": "k", + "unit_time": "min", + "max_sub_interval": {"minutes": 1}, + }, + title="Integration", + ) + integration_config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(integration_config_entry.entry_id) + await hass.async_block_till_done() + + # Confirm the link between the source entity device and the integration sensor + integration_entity = entity_registry.async_get("sensor.integration") + assert integration_entity is not None + assert integration_entity.device_id == source_entity.device_id + + # Device entry incorrectly linked to Integration config entry + device_registry.async_get_or_create( + config_entry_id=integration_config_entry.entry_id, + identifiers={("sensor", "identifier_test2")}, + connections={("mac", "30:31:32:33:34:02")}, + ) + device_registry.async_get_or_create( + config_entry_id=integration_config_entry.entry_id, + identifiers={("sensor", "identifier_test3")}, + connections={("mac", "30:31:32:33:34:03")}, + ) + await hass.async_block_till_done() + + # Before reloading the config entry, two devices are expected to be linked + devices_before_reload = device_registry.devices.get_devices_for_config_entry_id( + integration_config_entry.entry_id + ) + assert len(devices_before_reload) == 3 + + # Config entry reload + await hass.config_entries.async_reload(integration_config_entry.entry_id) + await hass.async_block_till_done() + + # Confirm the link between the source entity device and the integration sensor after reload + integration_entity = entity_registry.async_get("sensor.integration") + assert integration_entity is not None + assert integration_entity.device_id == source_entity.device_id + + # After reloading the config entry, only one linked device is expected + devices_after_reload = device_registry.devices.get_devices_for_config_entry_id( + integration_config_entry.entry_id + ) + assert len(devices_after_reload) == 1 From 0b5c533669d8138344dbe9bac6f9be330307c03f Mon Sep 17 00:00:00 2001 From: dougiteixeira <31328123+dougiteixeira@users.noreply.github.com> Date: Sat, 22 Jun 2024 10:31:47 -0300 Subject: [PATCH 1011/1445] Link the Trend helper entity to the source entity device (#119755) --- homeassistant/components/trend/__init__.py | 11 ++- .../components/trend/binary_sensor.py | 10 +++ tests/components/trend/test_binary_sensor.py | 45 ++++++++++ tests/components/trend/test_init.py | 87 ++++++++++++++++++- 4 files changed, 151 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/trend/__init__.py b/homeassistant/components/trend/__init__.py index 7ec2d140c5e..c38730e7591 100644 --- a/homeassistant/components/trend/__init__.py +++ b/homeassistant/components/trend/__init__.py @@ -3,8 +3,11 @@ from __future__ import annotations from homeassistant.config_entries import ConfigEntry -from homeassistant.const import Platform +from homeassistant.const import CONF_ENTITY_ID, Platform from homeassistant.core import HomeAssistant +from homeassistant.helpers.device import ( + async_remove_stale_devices_links_keep_entity_device, +) PLATFORMS = [Platform.BINARY_SENSOR] @@ -12,6 +15,12 @@ PLATFORMS = [Platform.BINARY_SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Trend from a config entry.""" + async_remove_stale_devices_links_keep_entity_device( + hass, + entry.entry_id, + entry.options[CONF_ENTITY_ID], + ) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload(entry.add_update_listener(config_entry_update_listener)) diff --git a/homeassistant/components/trend/binary_sensor.py b/homeassistant/components/trend/binary_sensor.py index 2b70e2394f0..6788d22219b 100644 --- a/homeassistant/components/trend/binary_sensor.py +++ b/homeassistant/components/trend/binary_sensor.py @@ -32,7 +32,9 @@ from homeassistant.const import ( STATE_UNKNOWN, ) from homeassistant.core import Event, EventStateChangedData, HomeAssistant, callback +from homeassistant.helpers import device_registry as dr import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.device import async_device_info_to_link_from_entity from homeassistant.helpers.entity import generate_entity_id from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_state_change_event @@ -133,6 +135,11 @@ async def async_setup_entry( ) -> None: """Set up trend sensor from config entry.""" + device_info = async_device_info_to_link_from_entity( + hass, + entry.options[CONF_ENTITY_ID], + ) + async_add_entities( [ SensorTrend( @@ -147,6 +154,7 @@ async def async_setup_entry( min_samples=entry.options.get(CONF_MIN_SAMPLES, DEFAULT_MIN_SAMPLES), max_samples=entry.options.get(CONF_MAX_SAMPLES, DEFAULT_MAX_SAMPLES), unique_id=entry.entry_id, + device_info=device_info, ) ] ) @@ -172,6 +180,7 @@ class SensorTrend(BinarySensorEntity, RestoreEntity): unique_id: str | None = None, device_class: BinarySensorDeviceClass | None = None, sensor_entity_id: str | None = None, + device_info: dr.DeviceInfo | None = None, ) -> None: """Initialize the sensor.""" self._entity_id = entity_id @@ -185,6 +194,7 @@ class SensorTrend(BinarySensorEntity, RestoreEntity): self._attr_name = name self._attr_device_class = device_class self._attr_unique_id = unique_id + self._attr_device_info = device_info if sensor_entity_id: self.entity_id = sensor_entity_id diff --git a/tests/components/trend/test_binary_sensor.py b/tests/components/trend/test_binary_sensor.py index 23d5a5357a7..ad85f65a9fc 100644 --- a/tests/components/trend/test_binary_sensor.py +++ b/tests/components/trend/test_binary_sensor.py @@ -8,8 +8,10 @@ from freezegun.api import FrozenDateTimeFactory import pytest from homeassistant import setup +from homeassistant.components.trend.const import DOMAIN from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNKNOWN from homeassistant.core import HomeAssistant, State +from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.setup import async_setup_component from .conftest import ComponentSetup @@ -350,3 +352,46 @@ async def test_invalid_min_sample( "Invalid config for 'binary_sensor' from integration 'trend': min_samples must " "be smaller than or equal to max_samples" in record.message ) + + +async def test_device_id( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + device_registry: dr.DeviceRegistry, +) -> None: + """Test for source entity device for Trend.""" + source_config_entry = MockConfigEntry() + source_config_entry.add_to_hass(hass) + source_device_entry = device_registry.async_get_or_create( + config_entry_id=source_config_entry.entry_id, + identifiers={("sensor", "identifier_test")}, + connections={("mac", "30:31:32:33:34:35")}, + ) + source_entity = entity_registry.async_get_or_create( + "sensor", + "test", + "source", + config_entry=source_config_entry, + device_id=source_device_entry.id, + ) + await hass.async_block_till_done() + assert entity_registry.async_get("sensor.test_source") is not None + + trend_config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={ + "name": "Trend", + "entity_id": "sensor.test_source", + "invert": False, + }, + title="Trend", + ) + trend_config_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(trend_config_entry.entry_id) + await hass.async_block_till_done() + + trend_entity = entity_registry.async_get("binary_sensor.trend") + assert trend_entity is not None + assert trend_entity.device_id == source_entity.device_id diff --git a/tests/components/trend/test_init.py b/tests/components/trend/test_init.py index eea76025d65..7ffb18de297 100644 --- a/tests/components/trend/test_init.py +++ b/tests/components/trend/test_init.py @@ -1,8 +1,9 @@ """Test the Trend integration.""" +from homeassistant.components.trend.const import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant -from homeassistant.helpers import entity_registry as er +from homeassistant.helpers import device_registry as dr, entity_registry as er from .conftest import ComponentSetup @@ -50,3 +51,87 @@ async def test_reload_config_entry( assert config_entry.state is ConfigEntryState.LOADED assert config_entry.data == {**config_entry.data, "max_samples": 4.0} + + +async def test_device_cleaning( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, +) -> None: + """Test for source entity device for Trend.""" + + # Source entity device config entry + source_config_entry = MockConfigEntry() + source_config_entry.add_to_hass(hass) + + # Device entry of the source entity + source_device1_entry = device_registry.async_get_or_create( + config_entry_id=source_config_entry.entry_id, + identifiers={("sensor", "identifier_test1")}, + connections={("mac", "30:31:32:33:34:01")}, + ) + + # Source entity registry + source_entity = entity_registry.async_get_or_create( + "sensor", + "test", + "source", + config_entry=source_config_entry, + device_id=source_device1_entry.id, + ) + await hass.async_block_till_done() + assert entity_registry.async_get("sensor.test_source") is not None + + # Configure the configuration entry for Trend + trend_config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={ + "name": "Trend", + "entity_id": "sensor.test_source", + "invert": False, + }, + title="Trend", + ) + trend_config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(trend_config_entry.entry_id) + await hass.async_block_till_done() + + # Confirm the link between the source entity device and the trend sensor + trend_entity = entity_registry.async_get("binary_sensor.trend") + assert trend_entity is not None + assert trend_entity.device_id == source_entity.device_id + + # Device entry incorrectly linked to Trend config entry + device_registry.async_get_or_create( + config_entry_id=trend_config_entry.entry_id, + identifiers={("sensor", "identifier_test2")}, + connections={("mac", "30:31:32:33:34:02")}, + ) + device_registry.async_get_or_create( + config_entry_id=trend_config_entry.entry_id, + identifiers={("sensor", "identifier_test3")}, + connections={("mac", "30:31:32:33:34:03")}, + ) + await hass.async_block_till_done() + + # Before reloading the config entry, two devices are expected to be linked + devices_before_reload = device_registry.devices.get_devices_for_config_entry_id( + trend_config_entry.entry_id + ) + assert len(devices_before_reload) == 3 + + # Config entry reload + await hass.config_entries.async_reload(trend_config_entry.entry_id) + await hass.async_block_till_done() + + # Confirm the link between the source entity device and the trend sensor after reload + trend_entity = entity_registry.async_get("binary_sensor.trend") + assert trend_entity is not None + assert trend_entity.device_id == source_entity.device_id + + # After reloading the config entry, only one linked device is expected + devices_after_reload = device_registry.devices.get_devices_for_config_entry_id( + trend_config_entry.entry_id + ) + assert len(devices_after_reload) == 1 From 61931d15cd43cf15cfb6306a3d7a82bfc41405f2 Mon Sep 17 00:00:00 2001 From: dougiteixeira <31328123+dougiteixeira@users.noreply.github.com> Date: Sat, 22 Jun 2024 10:38:58 -0300 Subject: [PATCH 1012/1445] Use the new device helpers in Threshold (#120158) --- .../components/threshold/__init__.py | 23 +++-- .../components/threshold/binary_sensor.py | 32 ++----- tests/components/threshold/test_init.py | 86 +++++++++++++++++++ 3 files changed, 103 insertions(+), 38 deletions(-) diff --git a/homeassistant/components/threshold/__init__.py b/homeassistant/components/threshold/__init__.py index fb9e7145951..ea8b469fd32 100644 --- a/homeassistant/components/threshold/__init__.py +++ b/homeassistant/components/threshold/__init__.py @@ -1,13 +1,22 @@ """The threshold component.""" from homeassistant.config_entries import ConfigEntry -from homeassistant.const import Platform +from homeassistant.const import CONF_ENTITY_ID, Platform from homeassistant.core import HomeAssistant -from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.device import ( + async_remove_stale_devices_links_keep_entity_device, +) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Min/Max from a config entry.""" + + async_remove_stale_devices_links_keep_entity_device( + hass, + entry.entry_id, + entry.options[CONF_ENTITY_ID], + ) + await hass.config_entries.async_forward_entry_setups( entry, (Platform.BINARY_SENSOR,) ) @@ -20,16 +29,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def config_entry_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: """Update listener, called when the config entry options are changed.""" - # Remove device link for entry, the source device may have changed. - # The link will be recreated after load. - device_registry = dr.async_get(hass) - devices = device_registry.devices.get_devices_for_config_entry_id(entry.entry_id) - - for device in devices: - device_registry.async_update_device( - device.id, remove_config_entry_id=entry.entry_id - ) - await hass.config_entries.async_reload(entry.entry_id) diff --git a/homeassistant/components/threshold/binary_sensor.py b/homeassistant/components/threshold/binary_sensor.py index ac970a53f55..8c3882ff360 100644 --- a/homeassistant/components/threshold/binary_sensor.py +++ b/homeassistant/components/threshold/binary_sensor.py @@ -30,11 +30,8 @@ from homeassistant.core import ( HomeAssistant, callback, ) -from homeassistant.helpers import ( - config_validation as cv, - device_registry as dr, - entity_registry as er, -) +from homeassistant.helpers import config_validation as cv, entity_registry as er +from homeassistant.helpers.device import async_device_info_to_link_from_entity from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_state_change_event @@ -87,27 +84,10 @@ async def async_setup_entry( registry, config_entry.options[CONF_ENTITY_ID] ) - source_entity = registry.async_get(entity_id) - dev_reg = dr.async_get(hass) - # Resolve source entity device - if ( - (source_entity is not None) - and (source_entity.device_id is not None) - and ( - ( - device := dev_reg.async_get( - device_id=source_entity.device_id, - ) - ) - is not None - ) - ): - device_info = DeviceInfo( - identifiers=device.identifiers, - connections=device.connections, - ) - else: - device_info = None + device_info = async_device_info_to_link_from_entity( + hass, + entity_id, + ) hysteresis = config_entry.options[CONF_HYSTERESIS] lower = config_entry.options[CONF_LOWER] diff --git a/tests/components/threshold/test_init.py b/tests/components/threshold/test_init.py index d1fda706911..6e85d659922 100644 --- a/tests/components/threshold/test_init.py +++ b/tests/components/threshold/test_init.py @@ -122,3 +122,89 @@ async def test_entry_changed(hass: HomeAssistant, platform) -> None: # Check that the config entry association has updated assert config_entry.entry_id not in _get_device_config_entries(run1_entry) assert config_entry.entry_id in _get_device_config_entries(run2_entry) + + +async def test_device_cleaning( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, +) -> None: + """Test for source entity device for Threshold.""" + + # Source entity device config entry + source_config_entry = MockConfigEntry() + source_config_entry.add_to_hass(hass) + + # Device entry of the source entity + source_device1_entry = device_registry.async_get_or_create( + config_entry_id=source_config_entry.entry_id, + identifiers={("sensor", "identifier_test1")}, + connections={("mac", "30:31:32:33:34:01")}, + ) + + # Source entity registry + source_entity = entity_registry.async_get_or_create( + "sensor", + "test", + "source", + config_entry=source_config_entry, + device_id=source_device1_entry.id, + ) + await hass.async_block_till_done() + assert entity_registry.async_get("sensor.test_source") is not None + + # Configure the configuration entry for Threshold + threshold_config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={ + "entity_id": "sensor.test_source", + "hysteresis": 0.0, + "lower": -2.0, + "name": "Threshold", + "upper": None, + }, + title="Threshold", + ) + threshold_config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(threshold_config_entry.entry_id) + await hass.async_block_till_done() + + # Confirm the link between the source entity device and the threshold sensor + threshold_entity = entity_registry.async_get("binary_sensor.threshold") + assert threshold_entity is not None + assert threshold_entity.device_id == source_entity.device_id + + # Device entry incorrectly linked to Threshold config entry + device_registry.async_get_or_create( + config_entry_id=threshold_config_entry.entry_id, + identifiers={("sensor", "identifier_test2")}, + connections={("mac", "30:31:32:33:34:02")}, + ) + device_registry.async_get_or_create( + config_entry_id=threshold_config_entry.entry_id, + identifiers={("sensor", "identifier_test3")}, + connections={("mac", "30:31:32:33:34:03")}, + ) + await hass.async_block_till_done() + + # Before reloading the config entry, two devices are expected to be linked + devices_before_reload = device_registry.devices.get_devices_for_config_entry_id( + threshold_config_entry.entry_id + ) + assert len(devices_before_reload) == 3 + + # Config entry reload + await hass.config_entries.async_reload(threshold_config_entry.entry_id) + await hass.async_block_till_done() + + # Confirm the link between the source entity device and the threshold sensor after reload + threshold_entity = entity_registry.async_get("binary_sensor.threshold") + assert threshold_entity is not None + assert threshold_entity.device_id == source_entity.device_id + + # After reloading the config entry, only one linked device is expected + devices_after_reload = device_registry.devices.get_devices_for_config_entry_id( + threshold_config_entry.entry_id + ) + assert len(devices_after_reload) == 1 From e9515b7584c44493701e4a19e9081c2de6c0b37e Mon Sep 17 00:00:00 2001 From: dougiteixeira <31328123+dougiteixeira@users.noreply.github.com> Date: Sat, 22 Jun 2024 10:46:55 -0300 Subject: [PATCH 1013/1445] Update `test_device_cleaning` in Utiltity Meter. (#120161) --- tests/components/utility_meter/test_init.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/components/utility_meter/test_init.py b/tests/components/utility_meter/test_init.py index 77d223454ec..cd549c77913 100644 --- a/tests/components/utility_meter/test_init.py +++ b/tests/components/utility_meter/test_init.py @@ -444,10 +444,12 @@ async def test_setup_and_remove_config_entry( assert len(entity_registry.entities) == 0 -async def test_device_cleaning(hass: HomeAssistant) -> None: +async def test_device_cleaning( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, +) -> None: """Test for source entity device for Utility Meter.""" - device_registry = dr.async_get(hass) - entity_registry = er.async_get(hass) # Source entity device config entry source_config_entry = MockConfigEntry() From 02f00508195f96f611bfaca9527afa89ea1a5856 Mon Sep 17 00:00:00 2001 From: dougiteixeira <31328123+dougiteixeira@users.noreply.github.com> Date: Sat, 22 Jun 2024 10:54:35 -0300 Subject: [PATCH 1014/1445] Update `test_device_cleaning` in Derivative (#120162) --- tests/components/derivative/test_init.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/components/derivative/test_init.py b/tests/components/derivative/test_init.py index 32b763ee84d..0081ab97580 100644 --- a/tests/components/derivative/test_init.py +++ b/tests/components/derivative/test_init.py @@ -62,10 +62,12 @@ async def test_setup_and_remove_config_entry( assert entity_registry.async_get(derivative_entity_id) is None -async def test_device_cleaning(hass: HomeAssistant) -> None: +async def test_device_cleaning( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, +) -> None: """Test for source entity device for Derivative.""" - device_registry = dr.async_get(hass) - entity_registry = er.async_get(hass) # Source entity device config entry source_config_entry = MockConfigEntry() From 10edf853119d78e575f46fd8bb0b7d5e56085775 Mon Sep 17 00:00:00 2001 From: dougiteixeira <31328123+dougiteixeira@users.noreply.github.com> Date: Sat, 22 Jun 2024 10:59:46 -0300 Subject: [PATCH 1015/1445] Update `test_device_cleaning` in Template (#120163) --- tests/components/template/test_binary_sensor.py | 8 +++++--- tests/components/template/test_config_flow.py | 6 ++---- tests/components/template/test_init.py | 7 ++++--- tests/components/template/test_sensor.py | 8 +++++--- 4 files changed, 16 insertions(+), 13 deletions(-) diff --git a/tests/components/template/test_binary_sensor.py b/tests/components/template/test_binary_sensor.py index ab74e4dec0d..50cad5be9e1 100644 --- a/tests/components/template/test_binary_sensor.py +++ b/tests/components/template/test_binary_sensor.py @@ -1405,10 +1405,12 @@ async def test_trigger_entity_restore_state_auto_off_expired( assert state.state == OFF -async def test_device_id(hass: HomeAssistant) -> None: +async def test_device_id( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, +) -> None: """Test for device for Template.""" - device_registry = dr.async_get(hass) - entity_registry = er.async_get(hass) device_config_entry = MockConfigEntry() device_config_entry.add_to_hass(hass) diff --git a/tests/components/template/test_config_flow.py b/tests/components/template/test_config_flow.py index 40f0c2da0e8..f277b918661 100644 --- a/tests/components/template/test_config_flow.py +++ b/tests/components/template/test_config_flow.py @@ -143,11 +143,10 @@ async def test_config_flow_device( hass: HomeAssistant, template_type: str, state_template: str, + device_registry: dr.DeviceRegistry, ) -> None: """Test remove the device registry configuration entry when the device changes.""" - device_registry = dr.async_get(hass) - # Configure a device registry entry_device = MockConfigEntry() entry_device.add_to_hass(hass) @@ -960,11 +959,10 @@ async def test_options_flow_change_device( hass: HomeAssistant, template_type: str, state_template: str, + device_registry: dr.DeviceRegistry, ) -> None: """Test remove the device registry configuration entry when the device changes.""" - device_registry = dr.async_get(hass) - # Configure a device registry entry_device1 = MockConfigEntry() entry_device1.add_to_hass(hass) diff --git a/tests/components/template/test_init.py b/tests/components/template/test_init.py index 0b2ed873a9c..d13fd9035b0 100644 --- a/tests/components/template/test_init.py +++ b/tests/components/template/test_init.py @@ -271,11 +271,12 @@ async def async_yaml_patch_helper(hass, filename): await hass.async_block_till_done() -async def test_change_device(hass: HomeAssistant) -> None: +async def test_change_device( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, +) -> None: """Test remove the device registry configuration entry when the device changes.""" - device_registry = dr.async_get(hass) - # Configure a device registry entry_device1 = MockConfigEntry() entry_device1.add_to_hass(hass) diff --git a/tests/components/template/test_sensor.py b/tests/components/template/test_sensor.py index 53c31c680dd..37d6d120491 100644 --- a/tests/components/template/test_sensor.py +++ b/tests/components/template/test_sensor.py @@ -1898,9 +1898,12 @@ async def test_trigger_action( assert events[0].context.parent_id == context.id -async def test_device_id(hass: HomeAssistant) -> None: +async def test_device_id( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, +) -> None: """Test for device for Template.""" - device_registry = dr.async_get(hass) device_config_entry = MockConfigEntry() device_config_entry.add_to_hass(hass) @@ -1929,7 +1932,6 @@ async def test_device_id(hass: HomeAssistant) -> None: assert await hass.config_entries.async_setup(template_config_entry.entry_id) await hass.async_block_till_done() - entity_registry = er.async_get(hass) template_entity = entity_registry.async_get("sensor.my_template") assert template_entity is not None assert template_entity.device_id == device_entry.id From 856aa38539f6e68fd6f762254bce766426ac1828 Mon Sep 17 00:00:00 2001 From: "Mr. Bubbles" Date: Sat, 22 Jun 2024 16:43:04 +0200 Subject: [PATCH 1016/1445] Add feature to generate OTP token in One-Time Password (OTP) integration (#120055) --- homeassistant/components/otp/config_flow.py | 102 ++++++++++++++++---- homeassistant/components/otp/const.py | 1 + homeassistant/components/otp/strings.json | 13 ++- tests/components/otp/conftest.py | 3 + tests/components/otp/test_config_flow.py | 101 +++++++++++++++++-- 5 files changed, 192 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/otp/config_flow.py b/homeassistant/components/otp/config_flow.py index 5b1551b1d04..15d04c910ad 100644 --- a/homeassistant/components/otp/config_flow.py +++ b/homeassistant/components/otp/config_flow.py @@ -10,19 +10,29 @@ import pyotp import voluptuous as vol from homeassistant.config_entries import ConfigFlow, ConfigFlowResult -from homeassistant.const import CONF_NAME, CONF_TOKEN +from homeassistant.const import CONF_CODE, CONF_NAME, CONF_TOKEN +from homeassistant.helpers.selector import ( + BooleanSelector, + BooleanSelectorConfig, + QrCodeSelector, + QrCodeSelectorConfig, + QrErrorCorrectionLevel, +) -from .const import DEFAULT_NAME, DOMAIN +from .const import CONF_NEW_TOKEN, DEFAULT_NAME, DOMAIN _LOGGER = logging.getLogger(__name__) STEP_USER_DATA_SCHEMA = vol.Schema( { - vol.Required(CONF_TOKEN): str, + vol.Optional(CONF_TOKEN): str, + vol.Optional(CONF_NEW_TOKEN): BooleanSelector(BooleanSelectorConfig()), vol.Required(CONF_NAME, default=DEFAULT_NAME): str, } ) +STEP_CONFIRM_DATA_SCHEMA = vol.Schema({vol.Required(CONF_CODE): str}) + class TOTPConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for One-Time Password (OTP).""" @@ -36,23 +46,31 @@ class TOTPConfigFlow(ConfigFlow, domain=DOMAIN): """Handle the initial step.""" errors: dict[str, str] = {} if user_input is not None: - try: - await self.hass.async_add_executor_job( - pyotp.TOTP(user_input[CONF_TOKEN]).now + if user_input.get(CONF_TOKEN) and not user_input.get(CONF_NEW_TOKEN): + try: + await self.hass.async_add_executor_job( + pyotp.TOTP(user_input[CONF_TOKEN]).now + ) + except binascii.Error: + errors["base"] = "invalid_token" + except Exception: + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + await self.async_set_unique_id(user_input[CONF_TOKEN]) + self._abort_if_unique_id_configured() + return self.async_create_entry( + title=user_input[CONF_NAME], + data=user_input, + ) + elif user_input.get(CONF_NEW_TOKEN): + user_input[CONF_TOKEN] = await self.hass.async_add_executor_job( + pyotp.random_base32 ) - except binascii.Error: - errors["base"] = "invalid_token" - except Exception: - _LOGGER.exception("Unexpected exception") - errors["base"] = "unknown" + self.user_input = user_input + return await self.async_step_confirm() else: - await self.async_set_unique_id(user_input[CONF_TOKEN]) - self._abort_if_unique_id_configured() - - return self.async_create_entry( - title=user_input[CONF_NAME], - data=user_input, - ) + errors["base"] = "invalid_token" return self.async_show_form( step_id="user", @@ -72,3 +90,51 @@ class TOTPConfigFlow(ConfigFlow, domain=DOMAIN): title=import_info.get(CONF_NAME, DEFAULT_NAME), data=import_info, ) + + async def async_step_confirm( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle the confirmation step.""" + + errors: dict[str, str] = {} + + if user_input is not None: + if await self.hass.async_add_executor_job( + pyotp.TOTP(self.user_input[CONF_TOKEN]).verify, user_input["code"] + ): + return self.async_create_entry( + title=self.user_input[CONF_NAME], + data={ + CONF_NAME: self.user_input[CONF_NAME], + CONF_TOKEN: self.user_input[CONF_TOKEN], + }, + ) + + errors["base"] = "invalid_code" + + provisioning_uri = await self.hass.async_add_executor_job( + pyotp.TOTP(self.user_input[CONF_TOKEN]).provisioning_uri, + self.user_input[CONF_NAME], + "Home Assistant", + ) + data_schema = STEP_CONFIRM_DATA_SCHEMA.extend( + { + vol.Optional("qr_code"): QrCodeSelector( + config=QrCodeSelectorConfig( + data=provisioning_uri, + scale=6, + error_correction_level=QrErrorCorrectionLevel.QUARTILE, + ) + ) + } + ) + return self.async_show_form( + step_id="confirm", + data_schema=data_schema, + description_placeholders={ + "auth_app1": "[Google Authenticator](https://support.google.com/accounts/answer/1066447)", + "auth_app2": "[Authy](https://authy.com/)", + "code": self.user_input[CONF_TOKEN], + }, + errors=errors, + ) diff --git a/homeassistant/components/otp/const.py b/homeassistant/components/otp/const.py index 180e0a4c5a2..6ccec165ec5 100644 --- a/homeassistant/components/otp/const.py +++ b/homeassistant/components/otp/const.py @@ -2,3 +2,4 @@ DOMAIN = "otp" DEFAULT_NAME = "OTP Sensor" +CONF_NEW_TOKEN = "new_token" diff --git a/homeassistant/components/otp/strings.json b/homeassistant/components/otp/strings.json index fc6031d0433..9152aeaa89e 100644 --- a/homeassistant/components/otp/strings.json +++ b/homeassistant/components/otp/strings.json @@ -4,13 +4,22 @@ "user": { "data": { "name": "[%key:common::config_flow::data::name%]", - "token": "Authenticator token (OTP)" + "token": "Authenticator token (OTP)", + "new_token": "Generate a new token?" + } + }, + "confirm": { + "title": "Verify One-Time Password (OTP)", + "description": "Before completing the setup of One-Time Password (OTP), confirm with a verification code. Scan the QR code with your authentication app. If you don't have one, we recommend either {auth_app1} or {auth_app2}.\n\nAfter scanning the code, enter the six-digit code from your app to verify the setup. If you have problems scanning the QR code, do a manual setup with code **`{code}`**.", + "data": { + "code": "Verification code (OTP)" } } }, "error": { "unknown": "[%key:common::config_flow::error::unknown%]", - "invalid_token": "Invalid token" + "invalid_token": "Invalid token", + "invalid_code": "Invalid code, please try again. If you get this error consistently, please make sure the clock of your Home Assistant system is accurate." }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" diff --git a/tests/components/otp/conftest.py b/tests/components/otp/conftest.py index 7c9b2eb545e..7443d772c69 100644 --- a/tests/components/otp/conftest.py +++ b/tests/components/otp/conftest.py @@ -33,7 +33,10 @@ def mock_pyotp() -> Generator[MagicMock, None, None]: ): mock_totp = MagicMock() mock_totp.now.return_value = 123456 + mock_totp.verify.return_value = True + mock_totp.provisioning_uri.return_value = "otpauth://totp/Home%20Assistant:OTP%20Sensor?secret=2FX5FBSYRE6VEC2FSHBQCRKO2GNDVZ52&issuer=Home%20Assistant" mock_client.TOTP.return_value = mock_totp + mock_client.random_base32.return_value = "2FX5FBSYRE6VEC2FSHBQCRKO2GNDVZ52" yield mock_client diff --git a/tests/components/otp/test_config_flow.py b/tests/components/otp/test_config_flow.py index c9fdcdb0fef..eefb1a6f4e0 100644 --- a/tests/components/otp/test_config_flow.py +++ b/tests/components/otp/test_config_flow.py @@ -5,15 +5,25 @@ from unittest.mock import AsyncMock, MagicMock import pytest -from homeassistant.components.otp.const import DOMAIN +from homeassistant.components.otp.const import CONF_NEW_TOKEN, DOMAIN from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER -from homeassistant.const import CONF_NAME, CONF_TOKEN +from homeassistant.const import CONF_CODE, CONF_NAME, CONF_TOKEN from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType TEST_DATA = { CONF_NAME: "OTP Sensor", - CONF_TOKEN: "TOKEN_A", + CONF_TOKEN: "2FX5FBSYRE6VEC2FSHBQCRKO2GNDVZ52", +} + +TEST_DATA_2 = { + CONF_NAME: "OTP Sensor", + CONF_NEW_TOKEN: True, +} + +TEST_DATA_3 = { + CONF_NAME: "OTP Sensor", + CONF_TOKEN: "", } @@ -33,11 +43,6 @@ async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: ) await hass.async_block_till_done() - assert result["type"] is FlowResultType.CREATE_ENTRY - assert result["title"] == "OTP Sensor" - assert result["data"] == TEST_DATA - assert len(mock_setup_entry.mock_calls) == 1 - @pytest.mark.parametrize( ("exception", "error"), @@ -98,3 +103,83 @@ async def test_flow_import(hass: HomeAssistant) -> None: assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "OTP Sensor" assert result["data"] == TEST_DATA + + +@pytest.mark.usefixtures("mock_pyotp") +async def test_generate_new_token( + hass: HomeAssistant, mock_setup_entry: AsyncMock +) -> None: + """Test form generate new token.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + TEST_DATA_2, + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {} + assert result["step_id"] == "confirm" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_CODE: "123456"}, + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "OTP Sensor" + assert result["data"] == TEST_DATA + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_generate_new_token_errors( + hass: HomeAssistant, mock_setup_entry: AsyncMock, mock_pyotp +) -> None: + """Test input validation errors.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + TEST_DATA_3, + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"base": "invalid_token"} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + TEST_DATA_2, + ) + mock_pyotp.TOTP().verify.return_value = False + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_CODE: "123456"}, + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"base": "invalid_code"} + + mock_pyotp.TOTP().verify.return_value = True + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_CODE: "123456"}, + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "OTP Sensor" + assert result["data"] == TEST_DATA + assert len(mock_setup_entry.mock_calls) == 1 From ed0e0eee71c9f06a7b3e3c6c6bd7cb956b249a43 Mon Sep 17 00:00:00 2001 From: Brent Petit Date: Sat, 22 Jun 2024 09:44:43 -0500 Subject: [PATCH 1017/1445] Create auxHeatOnly switch in Ecobee integration (#116323) Co-authored-by: Franck Nijhof --- homeassistant/components/ecobee/climate.py | 52 ++++++++++++++----- homeassistant/components/ecobee/const.py | 2 + homeassistant/components/ecobee/strings.json | 18 +++++++ homeassistant/components/ecobee/switch.py | 46 +++++++++++++++- .../ecobee/fixtures/ecobee-data.json | 23 ++++++-- tests/components/ecobee/test_repairs.py | 37 ++++++++++++- tests/components/ecobee/test_switch.py | 31 +++++++++++ 7 files changed, 191 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/ecobee/climate.py b/homeassistant/components/ecobee/climate.py index 11675c0bf61..8dcc7285590 100644 --- a/homeassistant/components/ecobee/climate.py +++ b/homeassistant/components/ecobee/climate.py @@ -36,10 +36,17 @@ from homeassistant.helpers import entity_platform import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.util.unit_conversion import TemperatureConverter from . import EcobeeData -from .const import _LOGGER, DOMAIN, ECOBEE_MODEL_TO_NAME, MANUFACTURER +from .const import ( + _LOGGER, + DOMAIN, + ECOBEE_AUX_HEAT_ONLY, + ECOBEE_MODEL_TO_NAME, + MANUFACTURER, +) from .util import ecobee_date, ecobee_time, is_indefinite_hold ATTR_COOL_TEMP = "cool_temp" @@ -69,9 +76,6 @@ DEFAULT_MIN_HUMIDITY = 15 DEFAULT_MAX_HUMIDITY = 50 HUMIDIFIER_MANUAL_MODE = "manual" -ECOBEE_AUX_HEAT_ONLY = "auxHeatOnly" - - # Order matters, because for reverse mapping we don't want to map HEAT to AUX ECOBEE_HVAC_TO_HASS = collections.OrderedDict( [ @@ -79,9 +83,13 @@ ECOBEE_HVAC_TO_HASS = collections.OrderedDict( ("cool", HVACMode.COOL), ("auto", HVACMode.HEAT_COOL), ("off", HVACMode.OFF), - ("auxHeatOnly", HVACMode.HEAT), + (ECOBEE_AUX_HEAT_ONLY, HVACMode.HEAT), ] ) +# Reverse key/value pair, drop auxHeatOnly as it doesn't map to specific HASS mode +HASS_TO_ECOBEE_HVAC = { + v: k for k, v in ECOBEE_HVAC_TO_HASS.items() if k != ECOBEE_AUX_HEAT_ONLY +} ECOBEE_HVAC_ACTION_TO_HASS = { # Map to None if we do not know how to represent. @@ -570,17 +578,39 @@ class Thermostat(ClimateEntity): """Return true if aux heater.""" return self.settings["hvacMode"] == ECOBEE_AUX_HEAT_ONLY - def turn_aux_heat_on(self) -> None: + async def async_turn_aux_heat_on(self) -> None: """Turn auxiliary heater on.""" + async_create_issue( + self.hass, + DOMAIN, + "migrate_aux_heat", + breaks_in_ha_version="2024.10.0", + is_fixable=True, + is_persistent=True, + translation_key="migrate_aux_heat", + severity=IssueSeverity.WARNING, + ) _LOGGER.debug("Setting HVAC mode to auxHeatOnly to turn on aux heat") self._last_hvac_mode_before_aux_heat = self.hvac_mode - self.data.ecobee.set_hvac_mode(self.thermostat_index, ECOBEE_AUX_HEAT_ONLY) + await self.hass.async_add_executor_job( + self.data.ecobee.set_hvac_mode, self.thermostat_index, ECOBEE_AUX_HEAT_ONLY + ) self.update_without_throttle = True - def turn_aux_heat_off(self) -> None: + async def async_turn_aux_heat_off(self) -> None: """Turn auxiliary heater off.""" + async_create_issue( + self.hass, + DOMAIN, + "migrate_aux_heat", + breaks_in_ha_version="2024.10.0", + is_fixable=True, + is_persistent=True, + translation_key="migrate_aux_heat", + severity=IssueSeverity.WARNING, + ) _LOGGER.debug("Setting HVAC mode to last mode to disable aux heat") - self.set_hvac_mode(self._last_hvac_mode_before_aux_heat) + await self.async_set_hvac_mode(self._last_hvac_mode_before_aux_heat) self.update_without_throttle = True def set_preset_mode(self, preset_mode: str) -> None: @@ -740,9 +770,7 @@ class Thermostat(ClimateEntity): def set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set HVAC mode (auto, auxHeatOnly, cool, heat, off).""" - ecobee_value = next( - (k for k, v in ECOBEE_HVAC_TO_HASS.items() if v == hvac_mode), None - ) + ecobee_value = HASS_TO_ECOBEE_HVAC.get(hvac_mode) if ecobee_value is None: _LOGGER.error("Invalid mode for set_hvac_mode: %s", hvac_mode) return diff --git a/homeassistant/components/ecobee/const.py b/homeassistant/components/ecobee/const.py index 8adc7f9638b..85a332f3c87 100644 --- a/homeassistant/components/ecobee/const.py +++ b/homeassistant/components/ecobee/const.py @@ -55,6 +55,8 @@ PLATFORMS = [ MANUFACTURER = "ecobee" +ECOBEE_AUX_HEAT_ONLY = "auxHeatOnly" + # Translates ecobee API weatherSymbol to Home Assistant usable names # https://www.ecobee.com/home/developer/api/documentation/v1/objects/WeatherForecast.shtml ECOBEE_WEATHER_SYMBOL_TO_HASS = { diff --git a/homeassistant/components/ecobee/strings.json b/homeassistant/components/ecobee/strings.json index b1d1df65417..56cf6e9ebf0 100644 --- a/homeassistant/components/ecobee/strings.json +++ b/homeassistant/components/ecobee/strings.json @@ -38,6 +38,11 @@ "ventilator_min_type_away": { "name": "Ventilator min time away" } + }, + "switch": { + "aux_heat_only": { + "name": "Aux heat only" + } } }, "services": { @@ -163,5 +168,18 @@ } } } + }, + "issues": { + "migrate_aux_heat": { + "title": "Migration of Ecobee set_aux_heat service", + "fix_flow": { + "step": { + "confirm": { + "description": "The Ecobee `set_aux_heat` service has been migrated. A new `aux_heat_only` switch entity is available for each thermostat that supports a Heat Pump.\n\nUpdate any automations to use the new `aux_heat_only` switch entity. When this is done, fix this issue and restart Home Assistant.", + "title": "Disable legacy Ecobee set_aux_heat service" + } + } + } + } } } diff --git a/homeassistant/components/ecobee/switch.py b/homeassistant/components/ecobee/switch.py index 607585887f0..67be78fb21d 100644 --- a/homeassistant/components/ecobee/switch.py +++ b/homeassistant/components/ecobee/switch.py @@ -6,6 +6,7 @@ from datetime import tzinfo import logging from typing import Any +from homeassistant.components.climate import HVACMode from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -13,7 +14,8 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import dt as dt_util from . import EcobeeData -from .const import DOMAIN +from .climate import HASS_TO_ECOBEE_HVAC +from .const import DOMAIN, ECOBEE_AUX_HEAT_ONLY from .entity import EcobeeBaseEntity _LOGGER = logging.getLogger(__name__) @@ -43,6 +45,12 @@ async def async_setup_entry( update_before_add=True, ) + async_add_entities( + EcobeeSwitchAuxHeatOnly(data, index) + for index, thermostat in enumerate(data.ecobee.thermostats) + if thermostat["settings"]["hasHeatPump"] + ) + class EcobeeVentilator20MinSwitch(EcobeeBaseEntity, SwitchEntity): """A Switch class, representing 20 min timer for an ecobee thermostat with ventilator attached.""" @@ -93,3 +101,39 @@ class EcobeeVentilator20MinSwitch(EcobeeBaseEntity, SwitchEntity): self.data.ecobee.set_ventilator_timer, self.thermostat_index, False ) self.update_without_throttle = True + + +class EcobeeSwitchAuxHeatOnly(EcobeeBaseEntity, SwitchEntity): + """Representation of a aux_heat_only ecobee switch.""" + + _attr_has_entity_name = True + _attr_translation_key = "aux_heat_only" + + def __init__( + self, + data: EcobeeData, + thermostat_index: int, + ) -> None: + """Initialize ecobee ventilator platform.""" + super().__init__(data, thermostat_index) + self._attr_unique_id = f"{self.base_unique_id}_aux_heat_only" + + self._last_hvac_mode_before_aux_heat = HASS_TO_ECOBEE_HVAC.get( + HVACMode.HEAT_COOL + ) + + def turn_on(self, **kwargs: Any) -> None: + """Set the hvacMode to auxHeatOnly.""" + self._last_hvac_mode_before_aux_heat = self.thermostat["settings"]["hvacMode"] + self.data.ecobee.set_hvac_mode(self.thermostat_index, ECOBEE_AUX_HEAT_ONLY) + + def turn_off(self, **kwargs: Any) -> None: + """Set the hvacMode back to the prior setting.""" + self.data.ecobee.set_hvac_mode( + self.thermostat_index, self._last_hvac_mode_before_aux_heat + ) + + @property + def is_on(self) -> bool: + """Return true if auxHeatOnly mode is active.""" + return self.thermostat["settings"]["hvacMode"] == ECOBEE_AUX_HEAT_ONLY diff --git a/tests/components/ecobee/fixtures/ecobee-data.json b/tests/components/ecobee/fixtures/ecobee-data.json index c86782d9c0b..b2f336e064d 100644 --- a/tests/components/ecobee/fixtures/ecobee-data.json +++ b/tests/components/ecobee/fixtures/ecobee-data.json @@ -11,8 +11,14 @@ }, "program": { "climates": [ - { "name": "Climate1", "climateRef": "c1" }, - { "name": "Climate2", "climateRef": "c2" } + { + "name": "Climate1", + "climateRef": "c1" + }, + { + "name": "Climate2", + "climateRef": "c2" + } ], "currentClimateRef": "c1" }, @@ -39,6 +45,7 @@ "isVentilatorTimerOn": false, "hasHumidifier": true, "humidifierMode": "manual", + "hasHeatPump": false, "humidity": "30" }, "equipmentStatus": "fan", @@ -82,8 +89,14 @@ "modelNumber": "athenaSmart", "program": { "climates": [ - { "name": "Climate1", "climateRef": "c1" }, - { "name": "Climate2", "climateRef": "c2" } + { + "name": "Climate1", + "climateRef": "c1" + }, + { + "name": "Climate2", + "climateRef": "c2" + } ], "currentClimateRef": "c1" }, @@ -109,6 +122,7 @@ "isVentilatorTimerOn": false, "hasHumidifier": true, "humidifierMode": "manual", + "hasHeatPump": true, "humidity": "30" }, "equipmentStatus": "fan", @@ -184,6 +198,7 @@ "isVentilatorTimerOn": false, "hasHumidifier": true, "humidifierMode": "manual", + "hasHeatPump": false, "humidity": "30" }, "equipmentStatus": "fan", diff --git a/tests/components/ecobee/test_repairs.py b/tests/components/ecobee/test_repairs.py index 9821d31ac64..1473f8eb3a1 100644 --- a/tests/components/ecobee/test_repairs.py +++ b/tests/components/ecobee/test_repairs.py @@ -3,6 +3,11 @@ from http import HTTPStatus from unittest.mock import MagicMock +from homeassistant.components.climate import ( + ATTR_AUX_HEAT, + DOMAIN as CLIMATE_DOMAIN, + SERVICE_SET_AUX_HEAT, +) from homeassistant.components.ecobee import DOMAIN from homeassistant.components.notify import DOMAIN as NOTIFY_DOMAIN from homeassistant.components.repairs.issue_handler import ( @@ -12,6 +17,7 @@ from homeassistant.components.repairs.websocket_api import ( RepairsFlowIndexView, RepairsFlowResourceView, ) +from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import HomeAssistant from homeassistant.helpers import issue_registry as ir @@ -22,7 +28,7 @@ from tests.typing import ClientSessionGenerator THERMOSTAT_ID = 0 -async def test_ecobee_repair_flow( +async def test_ecobee_notify_repair_flow( hass: HomeAssistant, mock_ecobee: MagicMock, hass_client: ClientSessionGenerator, @@ -77,3 +83,32 @@ async def test_ecobee_repair_flow( issue_id=f"migrate_notify_{DOMAIN}_{DOMAIN}", ) assert len(issue_registry.issues) == 0 + + +async def test_ecobee_aux_heat_repair_flow( + hass: HomeAssistant, + mock_ecobee: MagicMock, + hass_client: ClientSessionGenerator, + issue_registry: ir.IssueRegistry, +) -> None: + """Test the ecobee aux_heat service repair flow is triggered.""" + await setup_platform(hass, CLIMATE_DOMAIN) + await async_process_repairs_platforms(hass) + + ENTITY_ID = "climate.ecobee2" + + # Simulate legacy service being used + assert hass.services.has_service(CLIMATE_DOMAIN, SERVICE_SET_AUX_HEAT) + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_AUX_HEAT, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_AUX_HEAT: True}, + blocking=True, + ) + + # Assert the issue is present + assert issue_registry.async_get_issue( + domain="ecobee", + issue_id="migrate_aux_heat", + ) + assert len(issue_registry.issues) == 1 diff --git a/tests/components/ecobee/test_switch.py b/tests/components/ecobee/test_switch.py index 94b7296dcf5..05cea5a5e9d 100644 --- a/tests/components/ecobee/test_switch.py +++ b/tests/components/ecobee/test_switch.py @@ -112,3 +112,34 @@ async def test_turn_off_20min_ventilator(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() mock_set_20min_ventilator.assert_called_once_with(THERMOSTAT_ID, False) + + +DEVICE_ID = "switch.ecobee2_aux_heat_only" + + +async def test_aux_heat_only_turn_on(hass: HomeAssistant) -> None: + """Test the switch can be turned on.""" + with patch("pyecobee.Ecobee.set_hvac_mode") as mock_turn_on: + await setup_platform(hass, DOMAIN) + + await hass.services.async_call( + DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: DEVICE_ID}, + blocking=True, + ) + mock_turn_on.assert_called_once_with(1, "auxHeatOnly") + + +async def test_aux_heat_only_turn_off(hass: HomeAssistant) -> None: + """Test the switch can be turned off.""" + with patch("pyecobee.Ecobee.set_hvac_mode") as mock_turn_off: + await setup_platform(hass, DOMAIN) + + await hass.services.async_call( + DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: DEVICE_ID}, + blocking=True, + ) + mock_turn_off.assert_called_once_with(1, "auto") From b5a7fb1c33cf7c463fc5b75bd57b7c8d504afa12 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Sat, 22 Jun 2024 17:02:53 +0200 Subject: [PATCH 1018/1445] Add valve entity to gardena (#120160) Co-authored-by: Franck Nijhof --- .../components/gardena_bluetooth/__init__.py | 1 + .../components/gardena_bluetooth/switch.py | 1 + .../components/gardena_bluetooth/valve.py | 74 ++++++++++++++++ .../snapshots/test_valve.ambr | 29 +++++++ .../gardena_bluetooth/test_valve.py | 85 +++++++++++++++++++ 5 files changed, 190 insertions(+) create mode 100644 homeassistant/components/gardena_bluetooth/valve.py create mode 100644 tests/components/gardena_bluetooth/snapshots/test_valve.ambr create mode 100644 tests/components/gardena_bluetooth/test_valve.py diff --git a/homeassistant/components/gardena_bluetooth/__init__.py b/homeassistant/components/gardena_bluetooth/__init__.py index c2b3ae6732b..ed5b1c14ba3 100644 --- a/homeassistant/components/gardena_bluetooth/__init__.py +++ b/homeassistant/components/gardena_bluetooth/__init__.py @@ -26,6 +26,7 @@ PLATFORMS: list[Platform] = [ Platform.NUMBER, Platform.SENSOR, Platform.SWITCH, + Platform.VALVE, ] LOGGER = logging.getLogger(__name__) TIMEOUT = 20.0 diff --git a/homeassistant/components/gardena_bluetooth/switch.py b/homeassistant/components/gardena_bluetooth/switch.py index a57130c3acf..d010665e427 100644 --- a/homeassistant/components/gardena_bluetooth/switch.py +++ b/homeassistant/components/gardena_bluetooth/switch.py @@ -50,6 +50,7 @@ class GardenaBluetoothValveSwitch(GardenaBluetoothEntity, SwitchEntity): self._attr_unique_id = f"{coordinator.address}-{Valve.state.uuid}" self._attr_translation_key = "state" self._attr_is_on = None + self._attr_entity_registry_enabled_default = False def _handle_coordinator_update(self) -> None: self._attr_is_on = self.coordinator.get_cached(Valve.state) diff --git a/homeassistant/components/gardena_bluetooth/valve.py b/homeassistant/components/gardena_bluetooth/valve.py new file mode 100644 index 00000000000..3faf758f7e9 --- /dev/null +++ b/homeassistant/components/gardena_bluetooth/valve.py @@ -0,0 +1,74 @@ +"""Support for switch entities.""" + +from __future__ import annotations + +from typing import Any + +from gardena_bluetooth.const import Valve + +from homeassistant.components.valve import ValveEntity, ValveEntityFeature +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .coordinator import Coordinator, GardenaBluetoothEntity + +FALLBACK_WATERING_TIME_IN_SECONDS = 60 * 60 + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up switch based on a config entry.""" + coordinator: Coordinator = hass.data[DOMAIN][entry.entry_id] + entities = [] + if GardenaBluetoothValve.characteristics.issubset(coordinator.characteristics): + entities.append(GardenaBluetoothValve(coordinator)) + + async_add_entities(entities) + + +class GardenaBluetoothValve(GardenaBluetoothEntity, ValveEntity): + """Representation of a valve switch.""" + + _attr_name = None + _attr_is_closed: bool | None = None + _attr_reports_position = False + _attr_supported_features = ValveEntityFeature.OPEN | ValveEntityFeature.CLOSE + + characteristics = { + Valve.state.uuid, + Valve.manual_watering_time.uuid, + Valve.remaining_open_time.uuid, + } + + def __init__( + self, + coordinator: Coordinator, + ) -> None: + """Initialize the switch.""" + super().__init__( + coordinator, {Valve.state.uuid, Valve.manual_watering_time.uuid} + ) + self._attr_unique_id = f"{coordinator.address}-{Valve.state.uuid}" + + def _handle_coordinator_update(self) -> None: + self._attr_is_closed = not self.coordinator.get_cached(Valve.state) + super()._handle_coordinator_update() + + async def async_open_valve(self, **kwargs: Any) -> None: + """Turn the entity on.""" + value = ( + self.coordinator.get_cached(Valve.manual_watering_time) + or FALLBACK_WATERING_TIME_IN_SECONDS + ) + await self.coordinator.write(Valve.remaining_open_time, value) + self._attr_is_closed = False + self.async_write_ha_state() + + async def async_close_valve(self, **kwargs: Any) -> None: + """Turn the entity off.""" + await self.coordinator.write(Valve.remaining_open_time, 0) + self._attr_is_closed = True + self.async_write_ha_state() diff --git a/tests/components/gardena_bluetooth/snapshots/test_valve.ambr b/tests/components/gardena_bluetooth/snapshots/test_valve.ambr new file mode 100644 index 00000000000..c030332e75b --- /dev/null +++ b/tests/components/gardena_bluetooth/snapshots/test_valve.ambr @@ -0,0 +1,29 @@ +# serializer version: 1 +# name: test_setup + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mock Title', + 'supported_features': , + }), + 'context': , + 'entity_id': 'valve.mock_title', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'closed', + }) +# --- +# name: test_setup.1 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mock Title', + 'supported_features': , + }), + 'context': , + 'entity_id': 'valve.mock_title', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'open', + }) +# --- diff --git a/tests/components/gardena_bluetooth/test_valve.py b/tests/components/gardena_bluetooth/test_valve.py new file mode 100644 index 00000000000..411778658f4 --- /dev/null +++ b/tests/components/gardena_bluetooth/test_valve.py @@ -0,0 +1,85 @@ +"""Test Gardena Bluetooth valve.""" + +from collections.abc import Awaitable, Callable +from unittest.mock import Mock, call + +from gardena_bluetooth.const import Valve +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.valve import DOMAIN as VALVE_DOMAIN +from homeassistant.const import ( + ATTR_ENTITY_ID, + SERVICE_CLOSE_VALVE, + SERVICE_OPEN_VALVE, + Platform, +) +from homeassistant.core import HomeAssistant + +from . import setup_entry + +from tests.common import MockConfigEntry + + +@pytest.fixture +def mock_switch_chars(mock_read_char_raw): + """Mock data on device.""" + mock_read_char_raw[Valve.state.uuid] = b"\x00" + mock_read_char_raw[Valve.remaining_open_time.uuid] = ( + Valve.remaining_open_time.encode(0) + ) + mock_read_char_raw[Valve.manual_watering_time.uuid] = ( + Valve.manual_watering_time.encode(1000) + ) + return mock_read_char_raw + + +async def test_setup( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + mock_entry: MockConfigEntry, + mock_client: Mock, + mock_switch_chars: dict[str, bytes], + scan_step: Callable[[], Awaitable[None]], +) -> None: + """Test setup creates expected entities.""" + + entity_id = "valve.mock_title" + await setup_entry(hass, mock_entry, [Platform.VALVE]) + assert hass.states.get(entity_id) == snapshot + + mock_switch_chars[Valve.state.uuid] = b"\x01" + await scan_step() + assert hass.states.get(entity_id) == snapshot + + +async def test_switching( + hass: HomeAssistant, + mock_entry: MockConfigEntry, + mock_client: Mock, + mock_switch_chars: dict[str, bytes], +) -> None: + """Test switching makes correct calls.""" + + entity_id = "valve.mock_title" + await setup_entry(hass, mock_entry, [Platform.VALVE]) + assert hass.states.get(entity_id) + + await hass.services.async_call( + VALVE_DOMAIN, + SERVICE_OPEN_VALVE, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + + await hass.services.async_call( + VALVE_DOMAIN, + SERVICE_CLOSE_VALVE, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + + assert mock_client.write_char.mock_calls == [ + call(Valve.remaining_open_time, 1000), + call(Valve.remaining_open_time, 0), + ] From 3d7a47fb6b33e7f9e7d359dfdab43769e1ee2b25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20Mind=C3=AAllo=20de=20Andrade?= Date: Sat, 22 Jun 2024 12:22:46 -0300 Subject: [PATCH 1019/1445] Tuya curtain robot stuck in open state (#118444) --- homeassistant/components/tuya/cover.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/tuya/cover.py b/homeassistant/components/tuya/cover.py index 2e81529f974..e92c6f5c5f2 100644 --- a/homeassistant/components/tuya/cover.py +++ b/homeassistant/components/tuya/cover.py @@ -46,7 +46,7 @@ COVERS: dict[str, tuple[TuyaCoverEntityDescription, ...]] = { key=DPCode.CONTROL, translation_key="curtain", current_state=DPCode.SITUATION_SET, - current_position=(DPCode.PERCENT_CONTROL, DPCode.PERCENT_STATE), + current_position=(DPCode.PERCENT_STATE, DPCode.PERCENT_CONTROL), set_position=DPCode.PERCENT_CONTROL, device_class=CoverDeviceClass.CURTAIN, ), From abb88bcb8a3534f6ca887b8708caa805c4e7fba6 Mon Sep 17 00:00:00 2001 From: Indu Prakash <6459774+iprak@users.noreply.github.com> Date: Sat, 22 Jun 2024 10:24:31 -0500 Subject: [PATCH 1020/1445] Updated pynws to 1.8.2 (#120164) --- homeassistant/components/nws/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nws/manifest.json b/homeassistant/components/nws/manifest.json index cae36ea0fbe..d11a0e62bcf 100644 --- a/homeassistant/components/nws/manifest.json +++ b/homeassistant/components/nws/manifest.json @@ -7,5 +7,5 @@ "iot_class": "cloud_polling", "loggers": ["metar", "pynws"], "quality_scale": "platinum", - "requirements": ["pynws[retry]==1.8.1"] + "requirements": ["pynws[retry]==1.8.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 3de5ee532da..b05bd04154a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2034,7 +2034,7 @@ pynobo==1.8.1 pynuki==1.6.3 # homeassistant.components.nws -pynws[retry]==1.8.1 +pynws[retry]==1.8.2 # homeassistant.components.nx584 pynx584==0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 496eff4d327..8b810078a98 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1600,7 +1600,7 @@ pynobo==1.8.1 pynuki==1.6.3 # homeassistant.components.nws -pynws[retry]==1.8.1 +pynws[retry]==1.8.2 # homeassistant.components.nx584 pynx584==0.5 From cdc157de7452d98b7e9350022229af5d9c53f5b8 Mon Sep 17 00:00:00 2001 From: r-xyz <100710244+r-xyz@users.noreply.github.com> Date: Sat, 22 Jun 2024 17:29:42 +0200 Subject: [PATCH 1021/1445] Add styled formatting option to Signal Messenger integration - Bump pysignalclirestapi to 0.3.24 (#117148) --- .../components/signal_messenger/manifest.json | 2 +- .../components/signal_messenger/notify.py | 25 ++++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../signal_messenger/test_notify.py | 87 +++++++++++++++++++ 5 files changed, 110 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/signal_messenger/manifest.json b/homeassistant/components/signal_messenger/manifest.json index 058b01535ea..217109bfa2c 100644 --- a/homeassistant/components/signal_messenger/manifest.json +++ b/homeassistant/components/signal_messenger/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/signal_messenger", "iot_class": "cloud_push", "loggers": ["pysignalclirestapi"], - "requirements": ["pysignalclirestapi==0.3.23"] + "requirements": ["pysignalclirestapi==0.3.24"] } diff --git a/homeassistant/components/signal_messenger/notify.py b/homeassistant/components/signal_messenger/notify.py index 9c8846b2767..b93e5bb43e2 100644 --- a/homeassistant/components/signal_messenger/notify.py +++ b/homeassistant/components/signal_messenger/notify.py @@ -27,18 +27,32 @@ CONF_MAX_ALLOWED_DOWNLOAD_SIZE_BYTES = 52428800 ATTR_FILENAMES = "attachments" ATTR_URLS = "urls" ATTR_VERIFY_SSL = "verify_ssl" +ATTR_TEXTMODE = "text_mode" -DATA_FILENAMES_SCHEMA = vol.Schema({vol.Required(ATTR_FILENAMES): [cv.string]}) +TEXTMODE_OPTIONS = ["normal", "styled"] + +DATA_FILENAMES_SCHEMA = vol.Schema( + { + vol.Required(ATTR_FILENAMES): [cv.string], + vol.Optional(ATTR_TEXTMODE, default="normal"): vol.In(TEXTMODE_OPTIONS), + } +) DATA_URLS_SCHEMA = vol.Schema( { vol.Required(ATTR_URLS): [cv.url], vol.Optional(ATTR_VERIFY_SSL, default=True): cv.boolean, + vol.Optional(ATTR_TEXTMODE, default="normal"): vol.In(TEXTMODE_OPTIONS), } ) DATA_SCHEMA = vol.Any( None, + vol.Schema( + { + vol.Optional(ATTR_TEXTMODE, default="normal"): vol.In(TEXTMODE_OPTIONS), + } + ), DATA_FILENAMES_SCHEMA, DATA_URLS_SCHEMA, ) @@ -100,10 +114,13 @@ class SignalNotificationService(BaseNotificationService): attachments_as_bytes = self.get_attachments_as_bytes( data, CONF_MAX_ALLOWED_DOWNLOAD_SIZE_BYTES, self._hass ) - try: self._signal_cli_rest_api.send_message( - message, self._recp_nrs, filenames, attachments_as_bytes + message, + self._recp_nrs, + filenames, + attachments_as_bytes, + text_mode="normal" if data is None else data.get(ATTR_TEXTMODE), ) except SignalCliRestApiError as ex: _LOGGER.error("%s", ex) @@ -116,7 +133,6 @@ class SignalNotificationService(BaseNotificationService): data = DATA_FILENAMES_SCHEMA(data) except vol.Invalid: return None - return data[ATTR_FILENAMES] @staticmethod @@ -130,7 +146,6 @@ class SignalNotificationService(BaseNotificationService): data = DATA_URLS_SCHEMA(data) except vol.Invalid: return None - urls = data[ATTR_URLS] attachments_as_bytes: list[bytearray] = [] diff --git a/requirements_all.txt b/requirements_all.txt index b05bd04154a..37d3b6e68a0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2167,7 +2167,7 @@ pysesame2==1.0.1 pysiaalarm==3.1.1 # homeassistant.components.signal_messenger -pysignalclirestapi==0.3.23 +pysignalclirestapi==0.3.24 # homeassistant.components.sky_hub pyskyqhub==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8b810078a98..0c565ad79f2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1706,7 +1706,7 @@ pyserial==3.5 pysiaalarm==3.1.1 # homeassistant.components.signal_messenger -pysignalclirestapi==0.3.23 +pysignalclirestapi==0.3.24 # homeassistant.components.sma pysma==0.7.3 diff --git a/tests/components/signal_messenger/test_notify.py b/tests/components/signal_messenger/test_notify.py index 012de07df0e..d0085fd6e21 100644 --- a/tests/components/signal_messenger/test_notify.py +++ b/tests/components/signal_messenger/test_notify.py @@ -64,6 +64,26 @@ def test_send_message( assert_sending_requests(signal_requests_mock) +def test_send_message_styled( + signal_notification_service: SignalNotificationService, + signal_requests_mock_factory: Mocker, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test send styled message.""" + signal_requests_mock = signal_requests_mock_factory() + with caplog.at_level( + logging.DEBUG, logger="homeassistant.components.signal_messenger.notify" + ): + data = {"text_mode": "styled"} + signal_notification_service.send_message(MESSAGE, data=data) + post_data = json.loads(signal_requests_mock.request_history[-1].text) + assert "Sending signal message" in caplog.text + assert signal_requests_mock.called + assert signal_requests_mock.call_count == 2 + assert post_data["text_mode"] == "styled" + assert_sending_requests(signal_requests_mock) + + def test_send_message_to_api_with_bad_data_throws_error( signal_notification_service: SignalNotificationService, signal_requests_mock_factory: Mocker, @@ -103,6 +123,27 @@ def test_send_message_with_bad_data_throws_vol_error( assert "extra keys not allowed" in str(exc.value) +def test_send_message_styled_with_bad_data_throws_vol_error( + signal_notification_service: SignalNotificationService, + signal_requests_mock_factory: Mocker, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test sending a styled message with bad data throws an error.""" + with ( + caplog.at_level( + logging.DEBUG, logger="homeassistant.components.signal_messenger.notify" + ), + pytest.raises(vol.Invalid) as exc, + ): + signal_notification_service.send_message(MESSAGE, data={"text_mode": "test"}) + + assert "Sending signal message" in caplog.text + assert ( + "value must be one of ['normal', 'styled'] for dictionary value @ data['text_mode']" + in str(exc.value) + ) + + def test_send_message_with_attachment( signal_notification_service: SignalNotificationService, signal_requests_mock_factory: Mocker, @@ -128,6 +169,32 @@ def test_send_message_with_attachment( assert_sending_requests(signal_requests_mock, 1) +def test_send_message_styled_with_attachment( + signal_notification_service: SignalNotificationService, + signal_requests_mock_factory: Mocker, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test send message with attachment.""" + signal_requests_mock = signal_requests_mock_factory() + with ( + caplog.at_level( + logging.DEBUG, logger="homeassistant.components.signal_messenger.notify" + ), + tempfile.NamedTemporaryFile( + mode="w", suffix=".png", prefix=os.path.basename(__file__) + ) as temp_file, + ): + temp_file.write("attachment_data") + data = {"attachments": [temp_file.name], "text_mode": "styled"} + signal_notification_service.send_message(MESSAGE, data=data) + post_data = json.loads(signal_requests_mock.request_history[-1].text) + assert "Sending signal message" in caplog.text + assert signal_requests_mock.called + assert signal_requests_mock.call_count == 2 + assert_sending_requests(signal_requests_mock, 1) + assert post_data["text_mode"] == "styled" + + def test_send_message_with_attachment_as_url( signal_notification_service: SignalNotificationService, signal_requests_mock_factory: Mocker, @@ -147,6 +214,26 @@ def test_send_message_with_attachment_as_url( assert_sending_requests(signal_requests_mock, 1) +def test_send_message_styled_with_attachment_as_url( + signal_notification_service: SignalNotificationService, + signal_requests_mock_factory: Mocker, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test send message with attachment as URL.""" + signal_requests_mock = signal_requests_mock_factory(True, str(len(CONTENT))) + with caplog.at_level( + logging.DEBUG, logger="homeassistant.components.signal_messenger.notify" + ): + data = {"urls": [URL_ATTACHMENT], "text_mode": "styled"} + signal_notification_service.send_message(MESSAGE, data=data) + post_data = json.loads(signal_requests_mock.request_history[-1].text) + assert "Sending signal message" in caplog.text + assert signal_requests_mock.called + assert signal_requests_mock.call_count == 3 + assert_sending_requests(signal_requests_mock, 1) + assert post_data["text_mode"] == "styled" + + def test_get_attachments( signal_notification_service: SignalNotificationService, signal_requests_mock_factory: Mocker, From 65a740f35e9ad50c4aec357a29b9268ee4526e65 Mon Sep 17 00:00:00 2001 From: Indu Prakash <6459774+iprak@users.noreply.github.com> Date: Sat, 22 Jun 2024 10:31:39 -0500 Subject: [PATCH 1022/1445] Fix airnow timezone look up (#120136) --- homeassistant/components/airnow/const.py | 1 + homeassistant/components/airnow/coordinator.py | 6 +++++- homeassistant/components/airnow/sensor.py | 5 ++--- .../airnow/snapshots/test_diagnostics.ambr | 2 +- tests/components/airnow/test_diagnostics.py | 18 +++++++++++++----- 5 files changed, 22 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/airnow/const.py b/homeassistant/components/airnow/const.py index 1f468bf0cf7..054a5cbfea7 100644 --- a/homeassistant/components/airnow/const.py +++ b/homeassistant/components/airnow/const.py @@ -14,6 +14,7 @@ ATTR_API_POLLUTANT = "Pollutant" ATTR_API_REPORT_DATE = "DateObserved" ATTR_API_REPORT_HOUR = "HourObserved" ATTR_API_REPORT_TZ = "LocalTimeZone" +ATTR_API_REPORT_TZINFO = "LocalTimeZoneInfo" ATTR_API_STATE = "StateCode" ATTR_API_STATION = "ReportingArea" ATTR_API_STATION_LATITUDE = "Latitude" diff --git a/homeassistant/components/airnow/coordinator.py b/homeassistant/components/airnow/coordinator.py index 32185080d25..35f8a0e0abf 100644 --- a/homeassistant/components/airnow/coordinator.py +++ b/homeassistant/components/airnow/coordinator.py @@ -12,6 +12,7 @@ from pyairnow.errors import AirNowError from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed +from homeassistant.util import dt as dt_util from .const import ( ATTR_API_AQI, @@ -26,6 +27,7 @@ from .const import ( ATTR_API_REPORT_DATE, ATTR_API_REPORT_HOUR, ATTR_API_REPORT_TZ, + ATTR_API_REPORT_TZINFO, ATTR_API_STATE, ATTR_API_STATION, ATTR_API_STATION_LATITUDE, @@ -96,7 +98,9 @@ class AirNowDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): # Copy Report Details data[ATTR_API_REPORT_DATE] = obv[ATTR_API_REPORT_DATE] data[ATTR_API_REPORT_HOUR] = obv[ATTR_API_REPORT_HOUR] - data[ATTR_API_REPORT_TZ] = obv[ATTR_API_REPORT_TZ] + data[ATTR_API_REPORT_TZINFO] = await dt_util.async_get_time_zone( + obv[ATTR_API_REPORT_TZ] + ) # Copy Station Details data[ATTR_API_STATE] = obv[ATTR_API_STATE] diff --git a/homeassistant/components/airnow/sensor.py b/homeassistant/components/airnow/sensor.py index f98a984658d..722c0d6f4a9 100644 --- a/homeassistant/components/airnow/sensor.py +++ b/homeassistant/components/airnow/sensor.py @@ -23,7 +23,6 @@ from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity -from homeassistant.util.dt import get_time_zone from . import AirNowConfigEntry, AirNowDataUpdateCoordinator from .const import ( @@ -35,7 +34,7 @@ from .const import ( ATTR_API_PM25, ATTR_API_REPORT_DATE, ATTR_API_REPORT_HOUR, - ATTR_API_REPORT_TZ, + ATTR_API_REPORT_TZINFO, ATTR_API_STATION, ATTR_API_STATION_LATITUDE, ATTR_API_STATION_LONGITUDE, @@ -84,7 +83,7 @@ SENSOR_TYPES: tuple[AirNowEntityDescription, ...] = ( f"{data[ATTR_API_REPORT_DATE]} {data[ATTR_API_REPORT_HOUR]}", "%Y-%m-%d %H", ) - .replace(tzinfo=get_time_zone(data[ATTR_API_REPORT_TZ])) + .replace(tzinfo=data[ATTR_API_REPORT_TZINFO]) .isoformat(), }, ), diff --git a/tests/components/airnow/snapshots/test_diagnostics.ambr b/tests/components/airnow/snapshots/test_diagnostics.ambr index 71fda040c1d..c2004d759a9 100644 --- a/tests/components/airnow/snapshots/test_diagnostics.ambr +++ b/tests/components/airnow/snapshots/test_diagnostics.ambr @@ -8,7 +8,7 @@ 'DateObserved': '2020-12-20', 'HourObserved': 15, 'Latitude': '**REDACTED**', - 'LocalTimeZone': 'PST', + 'LocalTimeZoneInfo': 'PST', 'Longitude': '**REDACTED**', 'O3': 0.048, 'PM10': 12, diff --git a/tests/components/airnow/test_diagnostics.py b/tests/components/airnow/test_diagnostics.py index a1348b49531..7329398e789 100644 --- a/tests/components/airnow/test_diagnostics.py +++ b/tests/components/airnow/test_diagnostics.py @@ -1,5 +1,7 @@ """Test AirNow diagnostics.""" +from unittest.mock import patch + import pytest from syrupy import SnapshotAssertion @@ -18,8 +20,14 @@ async def test_entry_diagnostics( snapshot: SnapshotAssertion, ) -> None: """Test config entry diagnostics.""" - assert await hass.config_entries.async_setup(config_entry.entry_id) - assert ( - await get_diagnostics_for_config_entry(hass, hass_client, config_entry) - == snapshot - ) + + # Fake LocalTimeZoneInfo + with patch( + "homeassistant.util.dt.async_get_time_zone", + return_value="PST", + ): + assert await hass.config_entries.async_setup(config_entry.entry_id) + assert ( + await get_diagnostics_for_config_entry(hass, hass_client, config_entry) + == snapshot + ) From 6045c2bb087d057c90cbc999dddff0ce3e1392a2 Mon Sep 17 00:00:00 2001 From: Christian Neumeier <47736781+NECH2004@users.noreply.github.com> Date: Sat, 22 Jun 2024 17:34:48 +0200 Subject: [PATCH 1023/1445] Add diagnostics support to Zeversolar integration (#118245) --- .../components/zeversolar/diagnostics.py | 58 +++++++++++++++++++ tests/components/zeversolar/__init__.py | 13 +++-- .../snapshots/test_diagnostics.ambr | 25 ++++++++ .../zeversolar/snapshots/test_sensor.ambr | 2 +- .../components/zeversolar/test_diagnostics.py | 46 +++++++++++++++ 5 files changed, 137 insertions(+), 7 deletions(-) create mode 100644 homeassistant/components/zeversolar/diagnostics.py create mode 100644 tests/components/zeversolar/snapshots/test_diagnostics.ambr create mode 100644 tests/components/zeversolar/test_diagnostics.py diff --git a/homeassistant/components/zeversolar/diagnostics.py b/homeassistant/components/zeversolar/diagnostics.py new file mode 100644 index 00000000000..b8901a7e793 --- /dev/null +++ b/homeassistant/components/zeversolar/diagnostics.py @@ -0,0 +1,58 @@ +"""Provides diagnostics for Zeversolar.""" + +from typing import Any + +from zeversolar import ZeverSolarData + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import DeviceEntry + +from .const import DOMAIN +from .coordinator import ZeversolarCoordinator + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, config_entry: ConfigEntry +) -> dict[str, Any]: + """Return diagnostics for a config entry.""" + + coordinator: ZeversolarCoordinator = hass.data[DOMAIN][config_entry.entry_id] + data: ZeverSolarData = coordinator.data + + payload: dict[str, Any] = { + "wifi_enabled": data.wifi_enabled, + "serial_or_registry_id": data.serial_or_registry_id, + "registry_key": data.registry_key, + "hardware_version": data.hardware_version, + "software_version": data.software_version, + "reported_datetime": data.reported_datetime, + "communication_status": data.communication_status.value, + "num_inverters": data.num_inverters, + "serial_number": data.serial_number, + "pac": data.pac, + "status": data.status.value, + "meter_status": data.meter_status.value, + } + + return payload + + +async def async_get_device_diagnostics( + hass: HomeAssistant, entry: ConfigEntry, device: DeviceEntry +) -> dict[str, Any]: + """Return diagnostics for a device entry.""" + coordinator: ZeversolarCoordinator = hass.data[DOMAIN][entry.entry_id] + + updateInterval = ( + None + if coordinator.update_interval is None + else coordinator.update_interval.total_seconds() + ) + + return { + "name": coordinator.name, + "always_update": coordinator.always_update, + "last_update_success": coordinator.last_update_success, + "update_interval": updateInterval, + } diff --git a/tests/components/zeversolar/__init__.py b/tests/components/zeversolar/__init__.py index f4d0f0e56d6..9beaad38e3c 100644 --- a/tests/components/zeversolar/__init__.py +++ b/tests/components/zeversolar/__init__.py @@ -12,6 +12,7 @@ from tests.common import MockConfigEntry MOCK_HOST_ZEVERSOLAR = "zeversolar-fake-host" MOCK_PORT_ZEVERSOLAR = 10200 +MOCK_SERIAL_NUMBER = "123456778" async def init_integration(hass: HomeAssistant) -> MockConfigEntry: @@ -19,16 +20,16 @@ async def init_integration(hass: HomeAssistant) -> MockConfigEntry: zeverData = ZeverSolarData( wifi_enabled=False, - serial_or_registry_id="1223", - registry_key="A-2", + serial_or_registry_id="EAB9615C0001", + registry_key="WSMQKHTQ3JVYQWA9", hardware_version="M10", - software_version="123-23", - reported_datetime="19900101 23:00", + software_version="19703-826R+17511-707R", + reported_datetime="19900101 23:01:45", communication_status=StatusEnum.OK, num_inverters=1, - serial_number="123456778", + serial_number=MOCK_SERIAL_NUMBER, pac=1234, - energy_today=123, + energy_today=123.4, status=StatusEnum.OK, meter_status=StatusEnum.OK, ) diff --git a/tests/components/zeversolar/snapshots/test_diagnostics.ambr b/tests/components/zeversolar/snapshots/test_diagnostics.ambr new file mode 100644 index 00000000000..eebc8468076 --- /dev/null +++ b/tests/components/zeversolar/snapshots/test_diagnostics.ambr @@ -0,0 +1,25 @@ +# serializer version: 1 +# name: test_device_diagnostics + dict({ + 'always_update': True, + 'last_update_success': True, + 'name': 'zeversolar', + 'update_interval': 60.0, + }) +# --- +# name: test_entry_diagnostics + dict({ + 'communication_status': 'OK', + 'hardware_version': 'M10', + 'meter_status': 'OK', + 'num_inverters': 1, + 'pac': 1234, + 'registry_key': 'WSMQKHTQ3JVYQWA9', + 'reported_datetime': '19900101 23:01:45', + 'serial_number': '123456778', + 'serial_or_registry_id': 'EAB9615C0001', + 'software_version': '19703-826R+17511-707R', + 'status': 'OK', + 'wifi_enabled': False, + }) +# --- diff --git a/tests/components/zeversolar/snapshots/test_sensor.ambr b/tests/components/zeversolar/snapshots/test_sensor.ambr index 358be386253..bee522133a5 100644 --- a/tests/components/zeversolar/snapshots/test_sensor.ambr +++ b/tests/components/zeversolar/snapshots/test_sensor.ambr @@ -67,7 +67,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '123', + 'state': '123.4', }) # --- # name: test_sensors[sensor.zeversolar_sensor_power-entry] diff --git a/tests/components/zeversolar/test_diagnostics.py b/tests/components/zeversolar/test_diagnostics.py new file mode 100644 index 00000000000..0d7a919b023 --- /dev/null +++ b/tests/components/zeversolar/test_diagnostics.py @@ -0,0 +1,46 @@ +"""Tests for the diagnostics data provided by the Zeversolar integration.""" + +from syrupy import SnapshotAssertion + +from homeassistant.components.zeversolar import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr + +from . import MOCK_SERIAL_NUMBER, init_integration + +from tests.components.diagnostics import ( + get_diagnostics_for_config_entry, + get_diagnostics_for_device, +) +from tests.typing import ClientSessionGenerator + + +async def test_entry_diagnostics( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + snapshot: SnapshotAssertion, +) -> None: + """Test config entry diagnostics.""" + + entry = await init_integration(hass) + + assert await get_diagnostics_for_config_entry(hass, hass_client, entry) == snapshot + + +async def test_device_diagnostics( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + device_registry: dr.DeviceRegistry, + snapshot: SnapshotAssertion, +) -> None: + """Test device diagnostics.""" + + entry = await init_integration(hass) + + device = device_registry.async_get_device( + identifiers={(DOMAIN, MOCK_SERIAL_NUMBER)} + ) + + assert ( + await get_diagnostics_for_device(hass, hass_client, entry, device) == snapshot + ) From cac55d0f47ef41b653dede98476c2e9859813176 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 22 Jun 2024 18:23:35 +0200 Subject: [PATCH 1024/1445] Remove YAML import for lutron (#120159) * Remove YAML import for lutron * Restore constants --- homeassistant/components/lutron/__init__.py | 71 +---------------- .../components/lutron/config_flow.py | 34 --------- homeassistant/components/lutron/strings.json | 8 -- tests/components/lutron/test_config_flow.py | 76 +------------------ 4 files changed, 2 insertions(+), 187 deletions(-) diff --git a/homeassistant/components/lutron/__init__.py b/homeassistant/components/lutron/__init__.py index 828182547c2..1521a05df8e 100644 --- a/homeassistant/components/lutron/__init__.py +++ b/homeassistant/components/lutron/__init__.py @@ -4,17 +4,11 @@ from dataclasses import dataclass import logging from pylutron import Button, Keypad, Led, Lutron, OccupancyGroup, Output -import voluptuous as vol -from homeassistant import config_entries from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, Platform -from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant -from homeassistant.data_entry_flow import FlowResultType +from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue -from homeassistant.helpers.typing import ConfigType from .const import DOMAIN @@ -35,69 +29,6 @@ ATTR_ACTION = "action" ATTR_FULL_ID = "full_id" ATTR_UUID = "uuid" -CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - { - vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Required(CONF_USERNAME): cv.string, - } - ) - }, - extra=vol.ALLOW_EXTRA, -) - - -async def _async_import(hass: HomeAssistant, base_config: ConfigType) -> None: - """Import a config entry from configuration.yaml.""" - - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data=base_config[DOMAIN], - ) - if ( - result["type"] == FlowResultType.CREATE_ENTRY - or result["reason"] == "single_instance_allowed" - ): - async_create_issue( - hass, - HOMEASSISTANT_DOMAIN, - f"deprecated_yaml_{DOMAIN}", - breaks_in_ha_version="2024.7.0", - is_fixable=False, - issue_domain=DOMAIN, - severity=IssueSeverity.WARNING, - translation_key="deprecated_yaml", - translation_placeholders={ - "domain": DOMAIN, - "integration_title": "Lutron", - }, - ) - return - async_create_issue( - hass, - DOMAIN, - f"deprecated_yaml_import_issue_{result['reason']}", - breaks_in_ha_version="2024.7.0", - is_fixable=False, - issue_domain=DOMAIN, - severity=IssueSeverity.WARNING, - translation_key=f"deprecated_yaml_import_issue_{result['reason']}", - translation_placeholders={ - "domain": DOMAIN, - "integration_title": "Lutron", - }, - ) - - -async def async_setup(hass: HomeAssistant, base_config: ConfigType) -> bool: - """Set up the Lutron component.""" - if DOMAIN in base_config: - hass.async_create_task(_async_import(hass, base_config)) - return True - @dataclass(slots=True, kw_only=True) class LutronData: diff --git a/homeassistant/components/lutron/config_flow.py b/homeassistant/components/lutron/config_flow.py index d267a646b03..e14d56fde57 100644 --- a/homeassistant/components/lutron/config_flow.py +++ b/homeassistant/components/lutron/config_flow.py @@ -73,37 +73,3 @@ class LutronConfigFlow(ConfigFlow, domain=DOMAIN): ), errors=errors, ) - - async def async_step_import( - self, import_config: dict[str, Any] - ) -> ConfigFlowResult: - """Attempt to import the existing configuration.""" - if self._async_current_entries(): - return self.async_abort(reason="single_instance_allowed") - main_repeater = Lutron( - import_config[CONF_HOST], - import_config[CONF_USERNAME], - import_config[CONF_PASSWORD], - ) - - def _load_db() -> None: - main_repeater.load_xml_db() - - try: - await self.hass.async_add_executor_job(_load_db) - except HTTPError: - _LOGGER.exception("Http error") - return self.async_abort(reason="cannot_connect") - except Exception: - _LOGGER.exception("Unknown error") - return self.async_abort(reason="unknown") - - guid = main_repeater.guid - - if len(guid) <= 10: - return self.async_abort(reason="cannot_connect") - _LOGGER.debug("Main Repeater GUID: %s", main_repeater.guid) - - await self.async_set_unique_id(guid) - self._abort_if_unique_id_configured() - return self.async_create_entry(title="Lutron", data=import_config) diff --git a/homeassistant/components/lutron/strings.json b/homeassistant/components/lutron/strings.json index 0212c8845d5..d5197375dc1 100644 --- a/homeassistant/components/lutron/strings.json +++ b/homeassistant/components/lutron/strings.json @@ -38,14 +38,6 @@ } }, "issues": { - "deprecated_yaml_import_issue_cannot_connect": { - "title": "The Lutron YAML configuration import cannot connect to server", - "description": "Configuring Lutron using YAML is being removed but there was an connection error importing your YAML configuration.\n\nThings you can try:\nMake sure your home assistant can reach the main repeater.\nRestart the main repeater by unplugging it for 60 seconds.\nTry logging into the main repeater at the IP address you specified in a web browser and the same login information.\n\nThen restart Home Assistant to try importing this integration again.\n\nAlternatively, you may remove the Lutron configuration from your YAML configuration entirely, restart Home Assistant, and add the Lutron integration manually." - }, - "deprecated_yaml_import_issue_unknown": { - "title": "The Lutron YAML configuration import request failed due to an unknown error", - "description": "Configuring Lutron using YAML is being removed but there was an unknown error while importing your existing configuration.\nSetup will not proceed.\n\nThe specific error can be found in the logs. The most likely cause is a networking error or the Main Repeater is down or has an invalid configuration.\n\nVerify that your Lutron system is operating correctly and restart Home Assistant to attempt the import again.\n\nAlternatively, you may remove the Lutron configuration from your YAML configuration entirely, restart Home Assistant, and add the Lutron integration manually." - }, "deprecated_light_fan_entity": { "title": "Detected Lutron fan entity created as a light", "description": "Fan entities have been added to the Lutron integration.\nWe detected that entity `{entity}` is being used in `{info}`\n\nWe have created a new fan entity and you should migrate `{info}` to use this new entity.\n\nWhen you are done migrating `{info}` and are ready to have the deprecated light entity removed, disable the entity and restart Home Assistant." diff --git a/tests/components/lutron/test_config_flow.py b/tests/components/lutron/test_config_flow.py index e4904838e1a..47b2a4891cf 100644 --- a/tests/components/lutron/test_config_flow.py +++ b/tests/components/lutron/test_config_flow.py @@ -7,7 +7,7 @@ from urllib.error import HTTPError import pytest from homeassistant.components.lutron.const import DOMAIN -from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER +from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -146,77 +146,3 @@ MOCK_DATA_IMPORT = { CONF_USERNAME: "lutron", CONF_PASSWORD: "integration", } - - -async def test_import( - hass: HomeAssistant, - mock_setup_entry: AsyncMock, -) -> None: - """Test import flow.""" - with ( - patch("homeassistant.components.lutron.config_flow.Lutron.load_xml_db"), - patch("homeassistant.components.lutron.config_flow.Lutron.guid", "12345678901"), - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=MOCK_DATA_IMPORT - ) - await hass.async_block_till_done() - - assert result["type"] is FlowResultType.CREATE_ENTRY - assert result["data"] == MOCK_DATA_IMPORT - assert len(mock_setup_entry.mock_calls) == 1 - - -@pytest.mark.parametrize( - ("raise_error", "reason"), - [ - (HTTPError("", 404, "", Message(), None), "cannot_connect"), - (Exception, "unknown"), - ], -) -async def test_import_flow_failure( - hass: HomeAssistant, raise_error: Exception, reason: str -) -> None: - """Test handling errors while importing.""" - - with patch( - "homeassistant.components.lutron.config_flow.Lutron.load_xml_db", - side_effect=raise_error, - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=MOCK_DATA_IMPORT - ) - - assert result["type"] is FlowResultType.ABORT - assert result["reason"] == reason - - -async def test_import_flow_guid_failure(hass: HomeAssistant) -> None: - """Test handling errors while importing.""" - - with ( - patch("homeassistant.components.lutron.config_flow.Lutron.load_xml_db"), - patch("homeassistant.components.lutron.config_flow.Lutron.guid", "123"), - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=MOCK_DATA_IMPORT - ) - - assert result["type"] is FlowResultType.ABORT - assert result["reason"] == "cannot_connect" - - -async def test_import_already_configured(hass: HomeAssistant) -> None: - """Test we abort import when entry is already configured.""" - - entry = MockConfigEntry( - domain=DOMAIN, data=MOCK_DATA_IMPORT, unique_id="12345678901" - ) - entry.add_to_hass(hass) - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=MOCK_DATA_IMPORT - ) - - assert result["type"] is FlowResultType.ABORT - assert result["reason"] == "single_instance_allowed" From 6a34e1b7ca614040978db8b071dd055c4dc324af Mon Sep 17 00:00:00 2001 From: Etienne Soufflet Date: Sat, 22 Jun 2024 18:25:17 +0200 Subject: [PATCH 1025/1445] Add tado climate swings and fan level (#117378) --- homeassistant/components/tado/__init__.py | 11 +- homeassistant/components/tado/climate.py | 162 +++++++++++++++++--- homeassistant/components/tado/const.py | 32 +++- homeassistant/components/tado/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 182 insertions(+), 29 deletions(-) diff --git a/homeassistant/components/tado/__init__.py b/homeassistant/components/tado/__init__.py index 8f69ccdaffb..be58c68be91 100644 --- a/homeassistant/components/tado/__init__.py +++ b/homeassistant/components/tado/__init__.py @@ -384,12 +384,15 @@ class TadoConnector: mode=None, fan_speed=None, swing=None, + fan_level=None, + vertical_swing=None, + horizontal_swing=None, ): """Set a zone overlay.""" _LOGGER.debug( ( "Set overlay for zone %s: overlay_mode=%s, temp=%s, duration=%s," - " type=%s, mode=%s fan_speed=%s swing=%s" + " type=%s, mode=%s fan_speed=%s swing=%s fan_level=%s vertical_swing=%s horizontal_swing=%s" ), zone_id, overlay_mode, @@ -399,6 +402,9 @@ class TadoConnector: mode, fan_speed, swing, + fan_level, + vertical_swing, + horizontal_swing, ) try: @@ -412,6 +418,9 @@ class TadoConnector: mode, fan_speed=fan_speed, swing=swing, + fan_level=fan_level, + vertical_swing=vertical_swing, + horizontal_swing=horizontal_swing, ) except RequestException as exc: diff --git a/homeassistant/components/tado/climate.py b/homeassistant/components/tado/climate.py index 3cb5d7fbce9..2698b6e1446 100644 --- a/homeassistant/components/tado/climate.py +++ b/homeassistant/components/tado/climate.py @@ -13,6 +13,10 @@ from homeassistant.components.climate import ( FAN_AUTO, PRESET_AWAY, PRESET_HOME, + SWING_BOTH, + SWING_HORIZONTAL, + SWING_OFF, + SWING_VERTICAL, ClimateEntity, ClimateEntityFeature, HVACAction, @@ -42,6 +46,7 @@ from .const import ( HA_TERMINATION_DURATION, HA_TERMINATION_TYPE, HA_TO_TADO_FAN_MODE_MAP, + HA_TO_TADO_FAN_MODE_MAP_LEGACY, HA_TO_TADO_HVAC_MODE_MAP, HA_TO_TADO_SWING_MODE_MAP, ORDERED_KNOWN_TADO_MODES, @@ -51,11 +56,14 @@ from .const import ( SUPPORT_PRESET_MANUAL, TADO_DEFAULT_MAX_TEMP, TADO_DEFAULT_MIN_TEMP, + TADO_FAN_LEVELS, + TADO_FAN_SPEEDS, TADO_HVAC_ACTION_TO_HA_HVAC_ACTION, TADO_MODES_WITH_NO_TEMP_SETTING, TADO_SWING_OFF, TADO_SWING_ON, TADO_TO_HA_FAN_MODE_MAP, + TADO_TO_HA_FAN_MODE_MAP_LEGACY, TADO_TO_HA_HVAC_MODE_MAP, TADO_TO_HA_OFFSET_MAP, TADO_TO_HA_SWING_MODE_MAP, @@ -147,6 +155,7 @@ def create_climate_entity( TADO_TO_HA_HVAC_MODE_MAP[CONST_MODE_SMART_SCHEDULE], ] supported_fan_modes = None + supported_swing_modes = None heat_temperatures = None cool_temperatures = None @@ -157,10 +166,31 @@ def create_climate_entity( continue supported_hvac_modes.append(TADO_TO_HA_HVAC_MODE_MAP[mode]) - if capabilities[mode].get("swings"): + if ( + capabilities[mode].get("swings") + or capabilities[mode].get("verticalSwing") + or capabilities[mode].get("horizontalSwing") + ): support_flags |= ClimateEntityFeature.SWING_MODE + supported_swing_modes = [] + if capabilities[mode].get("swings"): + supported_swing_modes.append( + TADO_TO_HA_SWING_MODE_MAP[TADO_SWING_ON] + ) + if capabilities[mode].get("verticalSwing"): + supported_swing_modes.append(SWING_VERTICAL) + if capabilities[mode].get("horizontalSwing"): + supported_swing_modes.append(SWING_HORIZONTAL) + if ( + SWING_HORIZONTAL in supported_swing_modes + and SWING_HORIZONTAL in supported_swing_modes + ): + supported_swing_modes.append(SWING_BOTH) + supported_swing_modes.append(TADO_TO_HA_SWING_MODE_MAP[TADO_SWING_OFF]) - if not capabilities[mode].get("fanSpeeds"): + if not capabilities[mode].get("fanSpeeds") and not capabilities[mode].get( + "fanLevel" + ): continue support_flags |= ClimateEntityFeature.FAN_MODE @@ -168,10 +198,16 @@ def create_climate_entity( if supported_fan_modes: continue - supported_fan_modes = [ - TADO_TO_HA_FAN_MODE_MAP[speed] - for speed in capabilities[mode]["fanSpeeds"] - ] + if capabilities[mode].get("fanSpeeds"): + supported_fan_modes = [ + TADO_TO_HA_FAN_MODE_MAP_LEGACY[speed] + for speed in capabilities[mode]["fanSpeeds"] + ] + else: + supported_fan_modes = [ + TADO_TO_HA_FAN_MODE_MAP[level] + for level in capabilities[mode]["fanLevel"] + ] cool_temperatures = capabilities[CONST_MODE_COOL]["temperatures"] else: @@ -219,6 +255,7 @@ def create_climate_entity( cool_max_temp, cool_step, supported_fan_modes, + supported_swing_modes, ) @@ -247,6 +284,7 @@ class TadoClimate(TadoZoneEntity, ClimateEntity): cool_max_temp: float | None = None, cool_step: float | None = None, supported_fan_modes: list[str] | None = None, + supported_swing_modes: list[str] | None = None, ) -> None: """Initialize of Tado climate entity.""" self._tado = tado @@ -267,11 +305,7 @@ class TadoClimate(TadoZoneEntity, ClimateEntity): self._cur_temp = None self._cur_humidity = None - if self.supported_features & ClimateEntityFeature.SWING_MODE: - self._attr_swing_modes = [ - TADO_TO_HA_SWING_MODE_MAP[TADO_SWING_ON], - TADO_TO_HA_SWING_MODE_MAP[TADO_SWING_OFF], - ] + self._attr_swing_modes = supported_swing_modes self._heat_min_temp = heat_min_temp self._heat_max_temp = heat_max_temp @@ -287,6 +321,8 @@ class TadoClimate(TadoZoneEntity, ClimateEntity): self._current_tado_hvac_mode = CONST_MODE_OFF self._current_tado_hvac_action = HVACAction.OFF self._current_tado_swing_mode = TADO_SWING_OFF + self._current_tado_vertical_swing = TADO_SWING_OFF + self._current_tado_horizontal_swing = TADO_SWING_OFF self._tado_zone_data: PyTado.TadoZone = {} self._tado_geofence_data: dict[str, str] | None = None @@ -348,12 +384,20 @@ class TadoClimate(TadoZoneEntity, ClimateEntity): def fan_mode(self) -> str | None: """Return the fan setting.""" if self._ac_device: - return TADO_TO_HA_FAN_MODE_MAP.get(self._current_tado_fan_speed, FAN_AUTO) + return TADO_TO_HA_FAN_MODE_MAP.get( + self._current_tado_fan_speed, + TADO_TO_HA_FAN_MODE_MAP_LEGACY.get( + self._current_tado_fan_speed, FAN_AUTO + ), + ) return None def set_fan_mode(self, fan_mode: str) -> None: """Turn fan on/off.""" - self._control_hvac(fan_mode=HA_TO_TADO_FAN_MODE_MAP[fan_mode]) + if self._current_tado_fan_speed in TADO_FAN_LEVELS: + self._control_hvac(fan_mode=HA_TO_TADO_FAN_MODE_MAP[fan_mode]) + else: + self._control_hvac(fan_mode=HA_TO_TADO_FAN_MODE_MAP_LEGACY[fan_mode]) @property def preset_mode(self) -> str: @@ -476,7 +520,23 @@ class TadoClimate(TadoZoneEntity, ClimateEntity): @property def swing_mode(self) -> str | None: """Active swing mode for the device.""" - return TADO_TO_HA_SWING_MODE_MAP[self._current_tado_swing_mode] + swing_modes_tuple = ( + self._current_tado_swing_mode, + self._current_tado_vertical_swing, + self._current_tado_horizontal_swing, + ) + if swing_modes_tuple == (TADO_SWING_OFF, TADO_SWING_OFF, TADO_SWING_OFF): + return TADO_TO_HA_SWING_MODE_MAP[TADO_SWING_OFF] + if swing_modes_tuple == (TADO_SWING_ON, TADO_SWING_OFF, TADO_SWING_OFF): + return TADO_TO_HA_SWING_MODE_MAP[TADO_SWING_ON] + if swing_modes_tuple == (TADO_SWING_OFF, TADO_SWING_ON, TADO_SWING_OFF): + return SWING_VERTICAL + if swing_modes_tuple == (TADO_SWING_OFF, TADO_SWING_OFF, TADO_SWING_ON): + return SWING_HORIZONTAL + if swing_modes_tuple == (TADO_SWING_OFF, TADO_SWING_ON, TADO_SWING_ON): + return SWING_BOTH + + return TADO_TO_HA_SWING_MODE_MAP[TADO_SWING_OFF] @property def extra_state_attributes(self) -> Mapping[str, Any] | None: @@ -492,7 +552,35 @@ class TadoClimate(TadoZoneEntity, ClimateEntity): def set_swing_mode(self, swing_mode: str) -> None: """Set swing modes for the device.""" - self._control_hvac(swing_mode=HA_TO_TADO_SWING_MODE_MAP[swing_mode]) + vertical_swing = None + horizontal_swing = None + swing = None + if self._attr_swing_modes is None: + return + if ( + SWING_VERTICAL in self._attr_swing_modes + or SWING_HORIZONTAL in self._attr_swing_modes + ): + if swing_mode == SWING_VERTICAL: + vertical_swing = TADO_SWING_ON + elif swing_mode == SWING_HORIZONTAL: + horizontal_swing = TADO_SWING_ON + elif swing_mode == SWING_BOTH: + vertical_swing = TADO_SWING_ON + horizontal_swing = TADO_SWING_ON + elif swing_mode == SWING_OFF: + if SWING_VERTICAL in self._attr_swing_modes: + vertical_swing = TADO_SWING_OFF + if SWING_HORIZONTAL in self._attr_swing_modes: + horizontal_swing = TADO_SWING_OFF + else: + swing = HA_TO_TADO_SWING_MODE_MAP[swing_mode] + + self._control_hvac( + swing_mode=swing, + vertical_swing=vertical_swing, + horizontal_swing=horizontal_swing, + ) @callback def _async_update_zone_data(self) -> None: @@ -509,10 +597,22 @@ class TadoClimate(TadoZoneEntity, ClimateEntity): self._tado_zone_temp_offset[attr] = self._tado.data["device"][ self._device_id ][TEMP_OFFSET][offset_key] - self._current_tado_fan_speed = self._tado_zone_data.current_fan_speed + + self._current_tado_fan_speed = ( + self._tado_zone_data.current_fan_level + if self._tado_zone_data.current_fan_level is not None + else self._tado_zone_data.current_fan_speed + ) + self._current_tado_hvac_mode = self._tado_zone_data.current_hvac_mode self._current_tado_hvac_action = self._tado_zone_data.current_hvac_action self._current_tado_swing_mode = self._tado_zone_data.current_swing_mode + self._current_tado_vertical_swing = ( + self._tado_zone_data.current_vertical_swing_mode + ) + self._current_tado_horizontal_swing = ( + self._tado_zone_data.current_horizontal_swing_mode + ) @callback def _async_update_zone_callback(self) -> None: @@ -556,6 +656,8 @@ class TadoClimate(TadoZoneEntity, ClimateEntity): swing_mode: str | None = None, duration: int | None = None, overlay_mode: str | None = None, + vertical_swing: str | None = None, + horizontal_swing: str | None = None, ): """Send new target temperature to Tado.""" if hvac_mode: @@ -570,6 +672,12 @@ class TadoClimate(TadoZoneEntity, ClimateEntity): if swing_mode: self._current_tado_swing_mode = swing_mode + if vertical_swing: + self._current_tado_vertical_swing = vertical_swing + + if horizontal_swing: + self._current_tado_horizontal_swing = horizontal_swing + self._normalize_target_temp_for_hvac_mode() # tado does not permit setting the fan speed to @@ -627,11 +735,24 @@ class TadoClimate(TadoZoneEntity, ClimateEntity): temperature_to_send = None fan_speed = None + fan_level = None if self.supported_features & ClimateEntityFeature.FAN_MODE: - fan_speed = self._current_tado_fan_speed + if self._current_tado_fan_speed in TADO_FAN_LEVELS: + fan_level = self._current_tado_fan_speed + elif self._current_tado_fan_speed in TADO_FAN_SPEEDS: + fan_speed = self._current_tado_fan_speed swing = None - if self.supported_features & ClimateEntityFeature.SWING_MODE: - swing = self._current_tado_swing_mode + vertical_swing = None + horizontal_swing = None + if ( + self.supported_features & ClimateEntityFeature.SWING_MODE + ) and self._attr_swing_modes is not None: + if SWING_VERTICAL in self._attr_swing_modes: + vertical_swing = self._current_tado_vertical_swing + if SWING_HORIZONTAL in self._attr_swing_modes: + horizontal_swing = self._current_tado_horizontal_swing + if vertical_swing is None and horizontal_swing is None: + swing = self._current_tado_swing_mode self._tado.set_zone_overlay( zone_id=self.zone_id, @@ -642,4 +763,7 @@ class TadoClimate(TadoZoneEntity, ClimateEntity): mode=self._current_tado_hvac_mode, fan_speed=fan_speed, # api defaults to not sending fanSpeed if None specified swing=swing, # api defaults to not sending swing if None specified + fan_level=fan_level, # api defaults to not sending fanLevel if fanSpeend not None + vertical_swing=vertical_swing, # api defaults to not sending verticalSwing if swing not None + horizontal_swing=horizontal_swing, # api defaults to not sending horizontalSwing if swing not None ) diff --git a/homeassistant/components/tado/const.py b/homeassistant/components/tado/const.py index be35bbb8e25..a41003da95f 100644 --- a/homeassistant/components/tado/const.py +++ b/homeassistant/components/tado/const.py @@ -77,9 +77,13 @@ CONST_LINK_OFFLINE = "OFFLINE" CONST_FAN_OFF = "OFF" CONST_FAN_AUTO = "AUTO" -CONST_FAN_LOW = "LOW" -CONST_FAN_MIDDLE = "MIDDLE" -CONST_FAN_HIGH = "HIGH" +CONST_FAN_LOW_LEGACY = "LOW" +CONST_FAN_MIDDLE_LEGACY = "MIDDLE" +CONST_FAN_HIGH_LEGACY = "HIGH" + +CONST_FAN_LEVEL_1 = "LEVEL1" +CONST_FAN_LEVEL_2 = "LEVEL2" +CONST_FAN_LEVEL_3 = "LEVEL3" # When we change the temperature setting, we need an overlay mode @@ -139,20 +143,36 @@ HA_TO_TADO_HVAC_MODE_MAP = { HVACMode.FAN_ONLY: CONST_MODE_FAN, } +HA_TO_TADO_FAN_MODE_MAP_LEGACY = { + FAN_AUTO: CONST_FAN_AUTO, + FAN_OFF: CONST_FAN_OFF, + FAN_LOW: CONST_FAN_LOW_LEGACY, + FAN_MEDIUM: CONST_FAN_MIDDLE_LEGACY, + FAN_HIGH: CONST_FAN_HIGH_LEGACY, +} + HA_TO_TADO_FAN_MODE_MAP = { FAN_AUTO: CONST_FAN_AUTO, FAN_OFF: CONST_FAN_OFF, - FAN_LOW: CONST_FAN_LOW, - FAN_MEDIUM: CONST_FAN_MIDDLE, - FAN_HIGH: CONST_FAN_HIGH, + FAN_LOW: CONST_FAN_LEVEL_1, + FAN_MEDIUM: CONST_FAN_LEVEL_2, + FAN_HIGH: CONST_FAN_LEVEL_3, } TADO_TO_HA_HVAC_MODE_MAP = { value: key for key, value in HA_TO_TADO_HVAC_MODE_MAP.items() } +TADO_TO_HA_FAN_MODE_MAP_LEGACY = { + value: key for key, value in HA_TO_TADO_FAN_MODE_MAP_LEGACY.items() +} + TADO_TO_HA_FAN_MODE_MAP = {value: key for key, value in HA_TO_TADO_FAN_MODE_MAP.items()} +TADO_FAN_SPEEDS = list(HA_TO_TADO_FAN_MODE_MAP_LEGACY.values()) + +TADO_FAN_LEVELS = list(HA_TO_TADO_FAN_MODE_MAP.values()) + DEFAULT_TADO_PRECISION = 0.1 # Constant for Auto Geolocation mode diff --git a/homeassistant/components/tado/manifest.json b/homeassistant/components/tado/manifest.json index 0f3288ba904..b0c00c888b7 100644 --- a/homeassistant/components/tado/manifest.json +++ b/homeassistant/components/tado/manifest.json @@ -14,5 +14,5 @@ }, "iot_class": "cloud_polling", "loggers": ["PyTado"], - "requirements": ["python-tado==0.17.4"] + "requirements": ["python-tado==0.17.6"] } diff --git a/requirements_all.txt b/requirements_all.txt index 37d3b6e68a0..9c940ff410a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2327,7 +2327,7 @@ python-smarttub==0.0.36 python-songpal==0.16.2 # homeassistant.components.tado -python-tado==0.17.4 +python-tado==0.17.6 # homeassistant.components.technove python-technove==1.2.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0c565ad79f2..0d3112c7aba 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1821,7 +1821,7 @@ python-smarttub==0.0.36 python-songpal==0.16.2 # homeassistant.components.tado -python-tado==0.17.4 +python-tado==0.17.6 # homeassistant.components.technove python-technove==1.2.2 From 4d982a9227bf2ad1f6652c267bc124218a78293d Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Sat, 22 Jun 2024 18:26:39 +0200 Subject: [PATCH 1026/1445] Add config flow to generic thermostat (#119930) Co-authored-by: Franck Nijhof --- .../components/generic_thermostat/__init__.py | 19 +++ .../components/generic_thermostat/climate.py | 49 +++++-- .../generic_thermostat/config_flow.py | 96 +++++++++++++ .../generic_thermostat/manifest.json | 2 + .../generic_thermostat/strings.json | 70 +++++++++ homeassistant/generated/config_flows.py | 1 + homeassistant/generated/integrations.json | 12 +- .../snapshots/test_config_flow.ambr | 89 ++++++++++++ .../generic_thermostat/test_config_flow.py | 134 ++++++++++++++++++ 9 files changed, 457 insertions(+), 15 deletions(-) create mode 100644 homeassistant/components/generic_thermostat/config_flow.py create mode 100644 tests/components/generic_thermostat/snapshots/test_config_flow.ambr create mode 100644 tests/components/generic_thermostat/test_config_flow.py diff --git a/homeassistant/components/generic_thermostat/__init__.py b/homeassistant/components/generic_thermostat/__init__.py index 75f69bbe88c..6a59e24ebd2 100644 --- a/homeassistant/components/generic_thermostat/__init__.py +++ b/homeassistant/components/generic_thermostat/__init__.py @@ -1,6 +1,25 @@ """The generic_thermostat component.""" +from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform +from homeassistant.core import HomeAssistant DOMAIN = "generic_thermostat" PLATFORMS = [Platform.CLIMATE] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up from a config entry.""" + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + entry.async_on_unload(entry.add_update_listener(config_entry_update_listener)) + return True + + +async def config_entry_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Update listener, called when the config entry options are changed.""" + await hass.config_entries.async_reload(entry.entry_id) + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/generic_thermostat/climate.py b/homeassistant/components/generic_thermostat/climate.py index 4c660bd03e9..91ff1af122d 100644 --- a/homeassistant/components/generic_thermostat/climate.py +++ b/homeassistant/components/generic_thermostat/climate.py @@ -3,6 +3,7 @@ from __future__ import annotations import asyncio +from collections.abc import Mapping from datetime import datetime, timedelta import logging import math @@ -25,6 +26,7 @@ from homeassistant.components.climate import ( HVACAction, HVACMode, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_TEMPERATURE, @@ -51,8 +53,7 @@ from homeassistant.core import ( callback, ) from homeassistant.exceptions import ConditionError -from homeassistant.helpers import condition -import homeassistant.helpers.config_validation as cv +from homeassistant.helpers import condition, config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import ( async_track_state_change_event, @@ -95,7 +96,7 @@ CONF_PRESETS = { ) } -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA_COMMON = vol.Schema( { vol.Required(CONF_HEATER): cv.entity_id, vol.Required(CONF_SENSOR): cv.entity_id, @@ -111,15 +112,34 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( vol.Optional(CONF_INITIAL_HVAC_MODE): vol.In( [HVACMode.COOL, HVACMode.HEAT, HVACMode.OFF] ), - vol.Optional(CONF_PRECISION): vol.In( - [PRECISION_TENTHS, PRECISION_HALVES, PRECISION_WHOLE] + vol.Optional(CONF_PRECISION): vol.All( + vol.Coerce(float), + vol.In([PRECISION_TENTHS, PRECISION_HALVES, PRECISION_WHOLE]), ), - vol.Optional(CONF_TEMP_STEP): vol.In( - [PRECISION_TENTHS, PRECISION_HALVES, PRECISION_WHOLE] + vol.Optional(CONF_TEMP_STEP): vol.All( + vol.In([PRECISION_TENTHS, PRECISION_HALVES, PRECISION_WHOLE]) ), vol.Optional(CONF_UNIQUE_ID): cv.string, + **{vol.Optional(v): vol.Coerce(float) for v in CONF_PRESETS.values()}, } -).extend({vol.Optional(v): vol.Coerce(float) for (k, v) in CONF_PRESETS.items()}) +) + + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(PLATFORM_SCHEMA_COMMON.schema) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Initialize config entry.""" + await _async_setup_config( + hass, + PLATFORM_SCHEMA_COMMON(dict(config_entry.options)), + config_entry.entry_id, + async_add_entities, + ) async def async_setup_platform( @@ -131,6 +151,18 @@ async def async_setup_platform( """Set up the generic thermostat platform.""" await async_setup_reload_service(hass, DOMAIN, PLATFORMS) + await _async_setup_config( + hass, config, config.get(CONF_UNIQUE_ID), async_add_entities + ) + + +async def _async_setup_config( + hass: HomeAssistant, + config: Mapping[str, Any], + unique_id: str | None, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the generic thermostat platform.""" name: str = config[CONF_NAME] heater_entity_id: str = config[CONF_HEATER] @@ -150,7 +182,6 @@ async def async_setup_platform( precision: float | None = config.get(CONF_PRECISION) target_temperature_step: float | None = config.get(CONF_TEMP_STEP) unit = hass.config.units.temperature_unit - unique_id: str | None = config.get(CONF_UNIQUE_ID) async_add_entities( [ diff --git a/homeassistant/components/generic_thermostat/config_flow.py b/homeassistant/components/generic_thermostat/config_flow.py new file mode 100644 index 00000000000..f1fe1ecfe25 --- /dev/null +++ b/homeassistant/components/generic_thermostat/config_flow.py @@ -0,0 +1,96 @@ +"""Config flow for Generic hygrostat.""" + +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, cast + +import voluptuous as vol + +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN, SensorDeviceClass +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +from homeassistant.const import CONF_NAME, DEGREE +from homeassistant.helpers import selector +from homeassistant.helpers.schema_config_entry_flow import ( + SchemaConfigFlowHandler, + SchemaFlowFormStep, +) + +from .climate import ( + CONF_AC_MODE, + CONF_COLD_TOLERANCE, + CONF_HEATER, + CONF_HOT_TOLERANCE, + CONF_MIN_DUR, + CONF_PRESETS, + CONF_SENSOR, + DEFAULT_TOLERANCE, + DOMAIN, +) + +OPTIONS_SCHEMA = { + vol.Required(CONF_AC_MODE): selector.BooleanSelector( + selector.BooleanSelectorConfig(), + ), + vol.Required(CONF_SENSOR): selector.EntitySelector( + selector.EntitySelectorConfig( + domain=SENSOR_DOMAIN, device_class=SensorDeviceClass.TEMPERATURE + ) + ), + vol.Required(CONF_HEATER): selector.EntitySelector( + selector.EntitySelectorConfig(domain=SWITCH_DOMAIN) + ), + vol.Required( + CONF_COLD_TOLERANCE, default=DEFAULT_TOLERANCE + ): selector.NumberSelector( + selector.NumberSelectorConfig( + mode=selector.NumberSelectorMode.BOX, unit_of_measurement=DEGREE, step=0.1 + ) + ), + vol.Required( + CONF_HOT_TOLERANCE, default=DEFAULT_TOLERANCE + ): selector.NumberSelector( + selector.NumberSelectorConfig( + mode=selector.NumberSelectorMode.BOX, unit_of_measurement=DEGREE, step=0.1 + ) + ), + vol.Optional(CONF_MIN_DUR): selector.DurationSelector( + selector.DurationSelectorConfig(allow_negative=False) + ), +} + +PRESETS_SCHEMA = { + vol.Optional(v): selector.NumberSelector( + selector.NumberSelectorConfig( + mode=selector.NumberSelectorMode.BOX, unit_of_measurement=DEGREE + ) + ) + for v in CONF_PRESETS.values() +} + +CONFIG_SCHEMA = { + vol.Required(CONF_NAME): selector.TextSelector(), + **OPTIONS_SCHEMA, +} + + +CONFIG_FLOW = { + "user": SchemaFlowFormStep(vol.Schema(CONFIG_SCHEMA), next_step="presets"), + "presets": SchemaFlowFormStep(vol.Schema(PRESETS_SCHEMA)), +} + +OPTIONS_FLOW = { + "init": SchemaFlowFormStep(vol.Schema(OPTIONS_SCHEMA), next_step="presets"), + "presets": SchemaFlowFormStep(vol.Schema(PRESETS_SCHEMA)), +} + + +class ConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN): + """Handle a config or options flow.""" + + config_flow = CONFIG_FLOW + options_flow = OPTIONS_FLOW + + def async_config_entry_title(self, options: Mapping[str, Any]) -> str: + """Return config entry title.""" + return cast(str, options["name"]) diff --git a/homeassistant/components/generic_thermostat/manifest.json b/homeassistant/components/generic_thermostat/manifest.json index 7bfa1000845..320de2aeb3e 100644 --- a/homeassistant/components/generic_thermostat/manifest.json +++ b/homeassistant/components/generic_thermostat/manifest.json @@ -2,7 +2,9 @@ "domain": "generic_thermostat", "name": "Generic Thermostat", "codeowners": [], + "config_flow": true, "dependencies": ["sensor", "switch"], "documentation": "https://www.home-assistant.io/integrations/generic_thermostat", + "integration_type": "helper", "iot_class": "local_polling" } diff --git a/homeassistant/components/generic_thermostat/strings.json b/homeassistant/components/generic_thermostat/strings.json index 8834892b7ab..27a563a9d8d 100644 --- a/homeassistant/components/generic_thermostat/strings.json +++ b/homeassistant/components/generic_thermostat/strings.json @@ -1,4 +1,74 @@ { + "title": "Generic thermostat", + "config": { + "step": { + "user": { + "title": "Add generic thermostat helper", + "description": "Create a climate entity that controls the temperature via a switch and sensor.", + "data": { + "ac_mode": "Cooling mode", + "heater": "Actuator switch", + "target_sensor": "Temperature sensor", + "min_cycle_duration": "Minimum cycle duration", + "name": "[%key:common::config_flow::data::name%]", + "cold_tolerance": "Cold tolerance", + "hot_tolerance": "Hot tolerance" + }, + "data_description": { + "ac_mode": "Set the actuator specified to be treated as a cooling device instead of a heating device.", + "heater": "Switch entity used to cool or heat depending on A/C mode.", + "target_sensor": "Temperature sensor that reflect the current temperature.", + "min_cycle_duration": "Set a minimum amount of time that the switch specified must be in its current state prior to being switched either off or on. This option will be ignored if the keep alive option is set.", + "cold_tolerance": "Minimum amount of difference between the temperature read by the temperature sensor the target temperature that must change prior to being switched on. For example, if the target temperature is 25 and the tolerance is 0.5 the heater will start when the sensor equals or goes below 24.5.", + "hot_tolerance": "Minimum amount of difference between the temperature read by the temperature sensor the target temperature that must change prior to being switched off. For example, if the target temperature is 25 and the tolerance is 0.5 the heater will stop when the sensor equals or goes above 25.5." + } + }, + "presets": { + "title": "Temperature presets", + "data": { + "away_temp": "[%key:component::climate::entity_component::_::state_attributes::preset_mode::state::away%]", + "comfort_temp": "[%key:component::climate::entity_component::_::state_attributes::preset_mode::state::comfort%]", + "eco_temp": "[%key:component::climate::entity_component::_::state_attributes::preset_mode::state::eco%]", + "home_temp": "[%common::state::home%]", + "sleep_temp": "[%key:component::climate::entity_component::_::state_attributes::preset_mode::state::sleep%]", + "activity_temp": "[%key:component::climate::entity_component::_::state_attributes::preset_mode::state::activity%]" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "ac_mode": "[%key:component::generic_thermostat::config::step::user::data::ac_mode%]", + "heater": "[%key:component::generic_thermostat::config::step::user::data::heater%]", + "target_sensor": "[%key:component::generic_thermostat::config::step::user::data::target_sensor%]", + "min_cycle_duration": "[%key:component::generic_thermostat::config::step::user::data::min_cycle_duration%]", + "cold_tolerance": "[%key:component::generic_thermostat::config::step::user::data::cold_tolerance%]", + "hot_tolerance": "[%key:component::generic_thermostat::config::step::user::data::hot_tolerance%]" + }, + "data_description": { + "heater": "[%key:component::generic_thermostat::config::step::user::data_description::heater%]", + "target_sensor": "[%key:component::generic_thermostat::config::step::user::data_description::target_sensor%]", + "ac_mode": "[%key:component::generic_thermostat::config::step::user::data_description::ac_mode%]", + "min_cycle_duration": "[%key:component::generic_thermostat::config::step::user::data_description::min_cycle_duration%]", + "cold_tolerance": "[%key:component::generic_thermostat::config::step::user::data_description::cold_tolerance%]", + "hot_tolerance": "[%key:component::generic_thermostat::config::step::user::data_description::hot_tolerance%]" + } + }, + "presets": { + "title": "[%key:component::generic_thermostat::config::step::presets::title%]", + "data": { + "away_temp": "[%key:component::climate::entity_component::_::state_attributes::preset_mode::state::away%]", + "comfort_temp": "[%key:component::climate::entity_component::_::state_attributes::preset_mode::state::comfort%]", + "eco_temp": "[%key:component::climate::entity_component::_::state_attributes::preset_mode::state::eco%]", + "home_temp": "[%key:common::state::home%]", + "sleep_temp": "[%key:component::climate::entity_component::_::state_attributes::preset_mode::state::sleep%]", + "activity_temp": "[%key:component::climate::entity_component::_::state_attributes::preset_mode::state::activity%]" + } + } + } + }, "services": { "reload": { "name": "[%key:common::action::reload%]", diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index a9f9993c90e..cf6e2bb4fa7 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -6,6 +6,7 @@ To update, run python3 -m script.hassfest FLOWS = { "helper": [ "derivative", + "generic_thermostat", "group", "integration", "min_max", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index bfe57db8883..cdcd9c906d8 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -2133,12 +2133,6 @@ "config_flow": false, "iot_class": "local_polling" }, - "generic_thermostat": { - "name": "Generic Thermostat", - "integration_type": "hub", - "config_flow": false, - "iot_class": "local_polling" - }, "geniushub": { "name": "Genius Hub", "integration_type": "hub", @@ -7166,6 +7160,11 @@ "config_flow": true, "iot_class": "calculated" }, + "generic_thermostat": { + "integration_type": "helper", + "config_flow": true, + "iot_class": "local_polling" + }, "group": { "integration_type": "helper", "config_flow": true, @@ -7266,6 +7265,7 @@ "filesize", "garages_amsterdam", "generic", + "generic_thermostat", "google_travel_time", "group", "growatt_server", diff --git a/tests/components/generic_thermostat/snapshots/test_config_flow.ambr b/tests/components/generic_thermostat/snapshots/test_config_flow.ambr new file mode 100644 index 00000000000..d515d52a81b --- /dev/null +++ b/tests/components/generic_thermostat/snapshots/test_config_flow.ambr @@ -0,0 +1,89 @@ +# serializer version: 1 +# name: test_config_flow[create_entry] + FlowResultSnapshot({ + 'result': ConfigEntrySnapshot({ + 'title': 'My thermostat', + }), + 'title': 'My thermostat', + 'type': , + }) +# --- +# name: test_config_flow[init] + FlowResultSnapshot({ + 'type': , + }) +# --- +# name: test_config_flow[presets] + FlowResultSnapshot({ + 'type': , + }) +# --- +# name: test_options[create_entry] + FlowResultSnapshot({ + 'result': True, + 'type': , + }) +# --- +# name: test_options[init] + FlowResultSnapshot({ + 'type': , + }) +# --- +# name: test_options[presets] + FlowResultSnapshot({ + 'type': , + }) +# --- +# name: test_options[with_away] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'current_temperature': 15.0, + 'friendly_name': 'My thermostat', + 'hvac_action': , + 'hvac_modes': list([ + , + , + ]), + 'max_temp': 35, + 'min_temp': 7, + 'preset_mode': 'none', + 'preset_modes': list([ + 'none', + 'away', + ]), + 'supported_features': , + 'target_temp_step': 0.1, + 'temperature': 7, + }), + 'context': , + 'entity_id': 'climate.my_thermostat', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_options[without_away] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'current_temperature': 15.0, + 'friendly_name': 'My thermostat', + 'hvac_action': , + 'hvac_modes': list([ + , + , + ]), + 'max_temp': 35, + 'min_temp': 7, + 'supported_features': , + 'target_temp_step': 0.1, + 'temperature': 7.0, + }), + 'context': , + 'entity_id': 'climate.my_thermostat', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- diff --git a/tests/components/generic_thermostat/test_config_flow.py b/tests/components/generic_thermostat/test_config_flow.py new file mode 100644 index 00000000000..81e06146a14 --- /dev/null +++ b/tests/components/generic_thermostat/test_config_flow.py @@ -0,0 +1,134 @@ +"""Test the generic hygrostat config flow.""" + +from unittest.mock import patch + +from syrupy.assertion import SnapshotAssertion +from syrupy.filters import props + +from homeassistant.components.climate import PRESET_AWAY +from homeassistant.components.generic_thermostat.climate import ( + CONF_AC_MODE, + CONF_COLD_TOLERANCE, + CONF_HEATER, + CONF_HOT_TOLERANCE, + CONF_NAME, + CONF_PRESETS, + CONF_SENSOR, + DOMAIN, +) +from homeassistant.components.sensor import SensorDeviceClass +from homeassistant.config_entries import SOURCE_USER +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_UNIT_OF_MEASUREMENT, + STATE_OFF, + UnitOfTemperature, +) +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + +SNAPSHOT_FLOW_PROPS = props("type", "title", "result", "error") + + +async def test_config_flow(hass: HomeAssistant, snapshot: SnapshotAssertion) -> None: + """Test the config flow.""" + with patch( + "homeassistant.components.generic_thermostat.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result == snapshot(name="init", include=SNAPSHOT_FLOW_PROPS) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_NAME: "My thermostat", + CONF_HEATER: "switch.run", + CONF_SENSOR: "sensor.temperature", + CONF_AC_MODE: False, + CONF_COLD_TOLERANCE: 0.3, + CONF_HOT_TOLERANCE: 0.3, + }, + ) + assert result == snapshot(name="presets", include=SNAPSHOT_FLOW_PROPS) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_PRESETS[PRESET_AWAY]: 20, + }, + ) + assert result == snapshot(name="create_entry", include=SNAPSHOT_FLOW_PROPS) + + await hass.async_block_till_done() + + assert len(mock_setup_entry.mock_calls) == 1 + + config_entry = hass.config_entries.async_entries(DOMAIN)[0] + assert config_entry.data == {} + assert config_entry.title == "My thermostat" + + +async def test_options(hass: HomeAssistant, snapshot: SnapshotAssertion) -> None: + """Test reconfiguring.""" + + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={ + CONF_NAME: "My thermostat", + CONF_HEATER: "switch.run", + CONF_SENSOR: "sensor.temperature", + CONF_AC_MODE: False, + CONF_COLD_TOLERANCE: 0.3, + CONF_HOT_TOLERANCE: 0.3, + CONF_PRESETS[PRESET_AWAY]: 20, + }, + title="My dehumidifier", + ) + config_entry.add_to_hass(hass) + + hass.states.async_set( + "sensor.temperature", + "15", + { + ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS, + ATTR_DEVICE_CLASS: SensorDeviceClass.TEMPERATURE, + }, + ) + hass.states.async_set("switch.run", STATE_OFF) + + assert await hass.config_entries.async_setup(config_entry.entry_id) + + # check that it is setup + await hass.async_block_till_done() + assert hass.states.get("climate.my_thermostat") == snapshot(name="with_away") + + # remove away preset + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result == snapshot(name="init", include=SNAPSHOT_FLOW_PROPS) + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_HEATER: "switch.run", + CONF_SENSOR: "sensor.temperature", + CONF_AC_MODE: False, + CONF_COLD_TOLERANCE: 0.3, + CONF_HOT_TOLERANCE: 0.3, + }, + ) + assert result == snapshot(name="presets", include=SNAPSHOT_FLOW_PROPS) + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={}, + ) + assert result == snapshot(name="create_entry", include=SNAPSHOT_FLOW_PROPS) + + # Check config entry is reloaded with new options + await hass.async_block_till_done() + assert hass.states.get("climate.my_thermostat") == snapshot(name="without_away") From bd65afa207c4324645e2639052e51d65efac5d10 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Sat, 22 Jun 2024 12:37:55 -0400 Subject: [PATCH 1027/1445] Prioritize the correct CP2102N serial port on macOS (#116461) --- homeassistant/components/usb/__init__.py | 31 ++++++- tests/components/usb/test_init.py | 106 +++++++++++++++++++++++ 2 files changed, 133 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/usb/__init__.py b/homeassistant/components/usb/__init__.py index 46950ba5b91..d4201d7f284 100644 --- a/homeassistant/components/usb/__init__.py +++ b/homeassistant/components/usb/__init__.py @@ -362,10 +362,33 @@ class USBDiscovery: async def _async_process_ports(self, ports: list[ListPortInfo]) -> None: """Process each discovered port.""" - for port in ports: - if port.vid is None and port.pid is None: - continue - await self._async_process_discovered_usb_device(usb_device_from_port(port)) + usb_devices = [ + usb_device_from_port(port) + for port in ports + if port.vid is not None or port.pid is not None + ] + + # CP2102N chips create *two* serial ports on macOS: `/dev/cu.usbserial-` and + # `/dev/cu.SLAB_USBtoUART*`. The former does not work and we should ignore them. + if sys.platform == "darwin": + silabs_serials = { + dev.serial_number + for dev in usb_devices + if dev.device.startswith("/dev/cu.SLAB_USBtoUART") + } + + usb_devices = [ + dev + for dev in usb_devices + if dev.serial_number not in silabs_serials + or ( + dev.serial_number in silabs_serials + and dev.device.startswith("/dev/cu.SLAB_USBtoUART") + ) + ] + + for usb_device in usb_devices: + await self._async_process_discovered_usb_device(usb_device) async def _async_scan_serial(self) -> None: """Scan serial ports.""" diff --git a/tests/components/usb/test_init.py b/tests/components/usb/test_init.py index effc63bf8aa..bbd802afc95 100644 --- a/tests/components/usb/test_init.py +++ b/tests/components/usb/test_init.py @@ -1054,3 +1054,109 @@ async def test_resolve_serial_by_id( assert len(mock_config_flow.mock_calls) == 1 assert mock_config_flow.mock_calls[0][1][0] == "test1" assert mock_config_flow.mock_calls[0][2]["data"].device == "/dev/serial/by-id/bla" + + +@pytest.mark.parametrize( + "ports", + [ + [ + MagicMock( + device="/dev/cu.usbserial-2120", + vid=0x3039, + pid=0x3039, + serial_number=conbee_device.serial_number, + manufacturer=conbee_device.manufacturer, + description=conbee_device.description, + ), + MagicMock( + device="/dev/cu.usbserial-1120", + vid=0x3039, + pid=0x3039, + serial_number=slae_sh_device.serial_number, + manufacturer=slae_sh_device.manufacturer, + description=slae_sh_device.description, + ), + MagicMock( + device="/dev/cu.SLAB_USBtoUART", + vid=0x3039, + pid=0x3039, + serial_number=conbee_device.serial_number, + manufacturer=conbee_device.manufacturer, + description=conbee_device.description, + ), + MagicMock( + device="/dev/cu.SLAB_USBtoUART2", + vid=0x3039, + pid=0x3039, + serial_number=slae_sh_device.serial_number, + manufacturer=slae_sh_device.manufacturer, + description=slae_sh_device.description, + ), + ], + [ + MagicMock( + device="/dev/cu.SLAB_USBtoUART2", + vid=0x3039, + pid=0x3039, + serial_number=slae_sh_device.serial_number, + manufacturer=slae_sh_device.manufacturer, + description=slae_sh_device.description, + ), + MagicMock( + device="/dev/cu.SLAB_USBtoUART", + vid=0x3039, + pid=0x3039, + serial_number=conbee_device.serial_number, + manufacturer=conbee_device.manufacturer, + description=conbee_device.description, + ), + MagicMock( + device="/dev/cu.usbserial-1120", + vid=0x3039, + pid=0x3039, + serial_number=slae_sh_device.serial_number, + manufacturer=slae_sh_device.manufacturer, + description=slae_sh_device.description, + ), + MagicMock( + device="/dev/cu.usbserial-2120", + vid=0x3039, + pid=0x3039, + serial_number=conbee_device.serial_number, + manufacturer=conbee_device.manufacturer, + description=conbee_device.description, + ), + ], + ], +) +async def test_cp2102n_ordering_on_macos( + ports: list[MagicMock], hass: HomeAssistant, hass_ws_client: WebSocketGenerator +) -> None: + """Test CP2102N ordering on macOS.""" + + new_usb = [ + {"domain": "test1", "vid": "3039", "pid": "3039", "description": "*2652*"} + ] + + with ( + patch("sys.platform", "darwin"), + patch("pyudev.Context", side_effect=ImportError), + patch("homeassistant.components.usb.async_get_usb", return_value=new_usb), + patch("homeassistant.components.usb.comports", return_value=ports), + patch.object(hass.config_entries.flow, "async_init") as mock_config_flow, + ): + assert await async_setup_component(hass, "usb", {"usb": {}}) + await hass.async_block_till_done() + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + ws_client = await hass_ws_client(hass) + await ws_client.send_json({"id": 1, "type": "usb/scan"}) + response = await ws_client.receive_json() + assert response["success"] + await hass.async_block_till_done() + + assert len(mock_config_flow.mock_calls) == 1 + assert mock_config_flow.mock_calls[0][1][0] == "test1" + + # We always use `cu.SLAB_USBtoUART` + assert mock_config_flow.mock_calls[0][2]["data"].device == "/dev/cu.SLAB_USBtoUART2" From 1bd95d359640f37745433d9acd7148ef64abaa6c Mon Sep 17 00:00:00 2001 From: Thomas55555 <59625598+Thomas55555@users.noreply.github.com> Date: Sat, 22 Jun 2024 18:40:13 +0200 Subject: [PATCH 1028/1445] Add service for Husqvarna Automower (#117269) --- .../components/husqvarna_automower/icons.json | 3 + .../husqvarna_automower/lawn_mower.py | 88 ++++++++++++----- .../husqvarna_automower/services.yaml | 21 ++++ .../husqvarna_automower/strings.json | 24 +++++ .../husqvarna_automower/test_lawn_mower.py | 98 ++++++++++++++++++- 5 files changed, 208 insertions(+), 26 deletions(-) create mode 100644 homeassistant/components/husqvarna_automower/services.yaml diff --git a/homeassistant/components/husqvarna_automower/icons.json b/homeassistant/components/husqvarna_automower/icons.json index 2ecbf9c198a..a9002c5b44a 100644 --- a/homeassistant/components/husqvarna_automower/icons.json +++ b/homeassistant/components/husqvarna_automower/icons.json @@ -32,5 +32,8 @@ "default": "mdi:tooltip-question" } } + }, + "services": { + "override_schedule": "mdi:debug-step-over" } } diff --git a/homeassistant/components/husqvarna_automower/lawn_mower.py b/homeassistant/components/husqvarna_automower/lawn_mower.py index 50333076308..c0b566a7f66 100644 --- a/homeassistant/components/husqvarna_automower/lawn_mower.py +++ b/homeassistant/components/husqvarna_automower/lawn_mower.py @@ -1,9 +1,14 @@ """Husqvarna Automower lawn mower entity.""" +from collections.abc import Awaitable, Callable, Coroutine +from datetime import timedelta +import functools import logging +from typing import Any from aioautomower.exceptions import ApiException from aioautomower.model import MowerActivities, MowerStates +import voluptuous as vol from homeassistant.components.lawn_mower import ( LawnMowerActivity, @@ -12,18 +17,14 @@ from homeassistant.components.lawn_mower import ( ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import AutomowerConfigEntry +from .const import DOMAIN from .coordinator import AutomowerDataUpdateCoordinator from .entity import AutomowerControlEntity -SUPPORT_STATE_SERVICES = ( - LawnMowerEntityFeature.DOCK - | LawnMowerEntityFeature.PAUSE - | LawnMowerEntityFeature.START_MOWING -) - DOCKED_ACTIVITIES = (MowerActivities.PARKED_IN_CS, MowerActivities.CHARGING) MOWING_ACTIVITIES = ( MowerActivities.MOWING, @@ -35,11 +36,38 @@ PAUSED_STATES = [ MowerStates.WAIT_UPDATING, MowerStates.WAIT_POWER_UP, ] +SUPPORT_STATE_SERVICES = ( + LawnMowerEntityFeature.DOCK + | LawnMowerEntityFeature.PAUSE + | LawnMowerEntityFeature.START_MOWING +) +MOW = "mow" +PARK = "park" +OVERRIDE_MODES = [MOW, PARK] _LOGGER = logging.getLogger(__name__) +def handle_sending_exception( + func: Callable[..., Awaitable[Any]], +) -> Callable[..., Coroutine[Any, Any, None]]: + """Handle exceptions while sending a command.""" + + @functools.wraps(func) + async def wrapper(self: Any, *args: Any, **kwargs: Any) -> Any: + try: + return await func(self, *args, **kwargs) + except ApiException as exception: + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="command_send_failed", + translation_placeholders={"exception": str(exception)}, + ) from exception + + return wrapper + + async def async_setup_entry( hass: HomeAssistant, entry: AutomowerConfigEntry, @@ -51,6 +79,20 @@ async def async_setup_entry( AutomowerLawnMowerEntity(mower_id, coordinator) for mower_id in coordinator.data ) + platform = entity_platform.async_get_current_platform() + platform.async_register_entity_service( + "override_schedule", + { + vol.Required("override_mode"): vol.In(OVERRIDE_MODES), + vol.Required("duration"): vol.All( + cv.time_period, + cv.positive_timedelta, + vol.Range(min=timedelta(minutes=1), max=timedelta(days=42)), + ), + }, + "async_override_schedule", + ) + class AutomowerLawnMowerEntity(AutomowerControlEntity, LawnMowerEntity): """Defining each mower Entity.""" @@ -81,29 +123,27 @@ class AutomowerLawnMowerEntity(AutomowerControlEntity, LawnMowerEntity): return LawnMowerActivity.DOCKED return LawnMowerActivity.ERROR + @handle_sending_exception async def async_start_mowing(self) -> None: """Resume schedule.""" - try: - await self.coordinator.api.commands.resume_schedule(self.mower_id) - except ApiException as exception: - raise HomeAssistantError( - f"Command couldn't be sent to the command queue: {exception}" - ) from exception + await self.coordinator.api.commands.resume_schedule(self.mower_id) + @handle_sending_exception async def async_pause(self) -> None: """Pauses the mower.""" - try: - await self.coordinator.api.commands.pause_mowing(self.mower_id) - except ApiException as exception: - raise HomeAssistantError( - f"Command couldn't be sent to the command queue: {exception}" - ) from exception + await self.coordinator.api.commands.pause_mowing(self.mower_id) + @handle_sending_exception async def async_dock(self) -> None: """Parks the mower until next schedule.""" - try: - await self.coordinator.api.commands.park_until_next_schedule(self.mower_id) - except ApiException as exception: - raise HomeAssistantError( - f"Command couldn't be sent to the command queue: {exception}" - ) from exception + await self.coordinator.api.commands.park_until_next_schedule(self.mower_id) + + @handle_sending_exception + async def async_override_schedule( + self, override_mode: str, duration: timedelta + ) -> None: + """Override the schedule with mowing or parking.""" + if override_mode == MOW: + await self.coordinator.api.commands.start_for(self.mower_id, duration) + if override_mode == PARK: + await self.coordinator.api.commands.park_for(self.mower_id, duration) diff --git a/homeassistant/components/husqvarna_automower/services.yaml b/homeassistant/components/husqvarna_automower/services.yaml new file mode 100644 index 00000000000..94687a2ebfa --- /dev/null +++ b/homeassistant/components/husqvarna_automower/services.yaml @@ -0,0 +1,21 @@ +override_schedule: + target: + entity: + integration: "husqvarna_automower" + domain: "lawn_mower" + fields: + duration: + required: true + example: "{'days': 1, 'hours': 12, 'minutes': 30}" + selector: + duration: + enable_day: true + override_mode: + required: true + example: "mow" + selector: + select: + translation_key: override_modes + options: + - "mow" + - "park" diff --git a/homeassistant/components/husqvarna_automower/strings.json b/homeassistant/components/husqvarna_automower/strings.json index a403a56cc5e..6cb1c17421a 100644 --- a/homeassistant/components/husqvarna_automower/strings.json +++ b/homeassistant/components/husqvarna_automower/strings.json @@ -269,5 +269,29 @@ "command_send_failed": { "message": "Failed to send command: {exception}" } + }, + "selector": { + "override_modes": { + "options": { + "mow": "Mow", + "park": "Park" + } + } + }, + "services": { + "override_schedule": { + "name": "Override schedule", + "description": "Override the schedule to either mow or park for a duration of time.", + "fields": { + "duration": { + "name": "Duration", + "description": "Minimum: 1 minute, maximum: 42 days, seconds will be ignored." + }, + "override_mode": { + "name": "Override mode", + "description": "With which action the schedule should be overridden." + } + } + } } } diff --git a/tests/components/husqvarna_automower/test_lawn_mower.py b/tests/components/husqvarna_automower/test_lawn_mower.py index ff5a67971be..5d5cacfc6bf 100644 --- a/tests/components/husqvarna_automower/test_lawn_mower.py +++ b/tests/components/husqvarna_automower/test_lawn_mower.py @@ -1,11 +1,13 @@ """Tests for lawn_mower module.""" +from datetime import timedelta from unittest.mock import AsyncMock from aioautomower.exceptions import ApiException from aioautomower.utils import mower_list_to_dictionary_dataclass from freezegun.api import FrozenDateTimeFactory import pytest +from voluptuous.error import MultipleInvalid from homeassistant.components.husqvarna_automower.const import DOMAIN from homeassistant.components.husqvarna_automower.coordinator import SCAN_INTERVAL @@ -84,11 +86,103 @@ async def test_lawn_mower_commands( ).side_effect = ApiException("Test error") with pytest.raises( HomeAssistantError, - match="Command couldn't be sent to the command queue: Test error", + match="Failed to send command: Test error", ): await hass.services.async_call( domain="lawn_mower", service=service, - service_data={"entity_id": "lawn_mower.test_mower_1"}, + target={"entity_id": "lawn_mower.test_mower_1"}, + blocking=True, + ) + + +@pytest.mark.parametrize( + ("aioautomower_command", "extra_data", "service", "service_data"), + [ + ( + "start_for", + timedelta(hours=3), + "override_schedule", + { + "duration": {"days": 0, "hours": 3, "minutes": 0}, + "override_mode": "mow", + }, + ), + ( + "park_for", + timedelta(days=1, hours=12, minutes=30), + "override_schedule", + { + "duration": {"days": 1, "hours": 12, "minutes": 30}, + "override_mode": "park", + }, + ), + ], +) +async def test_lawn_mower_service_commands( + hass: HomeAssistant, + aioautomower_command: str, + extra_data: int | None, + service: str, + service_data: dict[str, int] | None, + mock_automower_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test lawn_mower commands.""" + await setup_integration(hass, mock_config_entry) + mocked_method = AsyncMock() + setattr(mock_automower_client.commands, aioautomower_command, mocked_method) + await hass.services.async_call( + domain=DOMAIN, + service=service, + target={"entity_id": "lawn_mower.test_mower_1"}, + service_data=service_data, + blocking=True, + ) + mocked_method.assert_called_once_with(TEST_MOWER_ID, extra_data) + + getattr( + mock_automower_client.commands, aioautomower_command + ).side_effect = ApiException("Test error") + with pytest.raises( + HomeAssistantError, + match="Failed to send command: Test error", + ): + await hass.services.async_call( + domain=DOMAIN, + service=service, + target={"entity_id": "lawn_mower.test_mower_1"}, + service_data=service_data, + blocking=True, + ) + + +@pytest.mark.parametrize( + ("service", "service_data"), + [ + ( + "override_schedule", + { + "duration": {"days": 1, "hours": 12, "minutes": 30}, + "override_mode": "fly_to_moon", + }, + ), + ], +) +async def test_lawn_mower_wrong_service_commands( + hass: HomeAssistant, + service: str, + service_data: dict[str, int] | None, + mock_automower_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test lawn_mower commands.""" + await setup_integration(hass, mock_config_entry) + with pytest.raises(MultipleInvalid): + await hass.services.async_call( + domain=DOMAIN, + service=service, + target={"entity_id": "lawn_mower.test_mower_1"}, + service_data=service_data, blocking=True, ) From b2ade94d15e927f2008e1eb60880aa358d3530ba Mon Sep 17 00:00:00 2001 From: Yazan Majadba Date: Sat, 22 Jun 2024 19:52:18 +0300 Subject: [PATCH 1029/1445] Add new Islamic prayer times calculation methods (#113763) Co-authored-by: J. Nick Koston --- homeassistant/components/islamic_prayer_times/const.py | 8 ++++++++ .../components/islamic_prayer_times/strings.json | 8 ++++++++ 2 files changed, 16 insertions(+) diff --git a/homeassistant/components/islamic_prayer_times/const.py b/homeassistant/components/islamic_prayer_times/const.py index dc4237e5efa..c749c66f8b3 100644 --- a/homeassistant/components/islamic_prayer_times/const.py +++ b/homeassistant/components/islamic_prayer_times/const.py @@ -23,6 +23,14 @@ CALC_METHODS: Final = [ "turkey", "russia", "moonsighting", + "dubai", + "jakim", + "tunisia", + "algeria", + "kemenag", + "morocco", + "portugal", + "jordan", "custom", ] DEFAULT_CALC_METHOD: Final = "isna" diff --git a/homeassistant/components/islamic_prayer_times/strings.json b/homeassistant/components/islamic_prayer_times/strings.json index 87703e5fdae..359d4626bd4 100644 --- a/homeassistant/components/islamic_prayer_times/strings.json +++ b/homeassistant/components/islamic_prayer_times/strings.json @@ -41,6 +41,14 @@ "turkey": "Diyanet İşleri Başkanlığı, Turkey", "russia": "Spiritual Administration of Muslims of Russia", "moonsighting": "Moonsighting Committee Worldwide", + "dubai": "Dubai", + "jakim": "Jabatan Kemajuan Islam Malaysia (JAKIM)", + "tunisia": "Tunisia", + "algeria": "Algeria", + "kemenag": "ementerian Agama Republik Indonesia", + "morocco": "Morocco", + "portugal": "Comunidade Islamica de Lisboa", + "jordan": "Ministry of Awqaf, Islamic Affairs and Holy Places, Jordan", "custom": "Custom" } }, From 8e93116ed3fdc5c0639b21de8a78479e4390d9e2 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 22 Jun 2024 18:52:44 +0200 Subject: [PATCH 1030/1445] Update Home Assistant base image to 2024.06.1 (#120168) --- build.yaml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/build.yaml b/build.yaml index 7607998bacd..13618740ab8 100644 --- a/build.yaml +++ b/build.yaml @@ -1,10 +1,10 @@ image: ghcr.io/home-assistant/{arch}-homeassistant build_from: - aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2024.06.0 - armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2024.06.0 - armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2024.06.0 - amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2024.06.0 - i386: ghcr.io/home-assistant/i386-homeassistant-base:2024.06.0 + aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2024.06.1 + armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2024.06.1 + armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2024.06.1 + amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2024.06.1 + i386: ghcr.io/home-assistant/i386-homeassistant-base:2024.06.1 codenotary: signer: notary@home-assistant.io base_image: notary@home-assistant.io From 28eef00cce9aa6ff04b9a1a7082ac7060bbb2543 Mon Sep 17 00:00:00 2001 From: Bouke Haarsma Date: Sat, 22 Jun 2024 19:09:40 +0200 Subject: [PATCH 1031/1445] Huisbaasje rebranded to EnergyFlip (#120151) Co-authored-by: Franck Nijhof --- .../components/huisbaasje/__init__.py | 18 +++---- .../components/huisbaasje/config_flow.py | 6 +-- homeassistant/components/huisbaasje/const.py | 4 +- .../components/huisbaasje/manifest.json | 4 +- homeassistant/components/huisbaasje/sensor.py | 48 +++++++++---------- homeassistant/generated/integrations.json | 2 +- 6 files changed, 41 insertions(+), 41 deletions(-) diff --git a/homeassistant/components/huisbaasje/__init__.py b/homeassistant/components/huisbaasje/__init__.py index b02d0bf577c..3e0c9845c92 100644 --- a/homeassistant/components/huisbaasje/__init__.py +++ b/homeassistant/components/huisbaasje/__init__.py @@ -1,4 +1,4 @@ -"""The Huisbaasje integration.""" +"""The EnergyFlip integration.""" import asyncio from datetime import timedelta @@ -31,8 +31,8 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: - """Set up Huisbaasje from a config entry.""" - # Create the Huisbaasje client + """Set up EnergyFlip from a config entry.""" + # Create the EnergyFlip client energyflip = EnergyFlip( username=entry.data[CONF_USERNAME], password=entry.data[CONF_PASSWORD], @@ -48,7 +48,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return False async def async_update_data() -> dict[str, dict[str, Any]]: - return await async_update_huisbaasje(energyflip) + return await async_update_energyflip(energyflip) # Create a coordinator for polling updates coordinator = DataUpdateCoordinator( @@ -75,21 +75,21 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Forward the unloading of the entry to the platform unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - # If successful, unload the Huisbaasje client + # If successful, unload the EnergyFlip client if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) return unload_ok -async def async_update_huisbaasje(energyflip: EnergyFlip) -> dict[str, dict[str, Any]]: - """Update the data by performing a request to Huisbaasje.""" +async def async_update_energyflip(energyflip: EnergyFlip) -> dict[str, dict[str, Any]]: + """Update the data by performing a request to EnergyFlip.""" try: # Note: TimeoutError and aiohttp.ClientError are already # handled by the data update coordinator. async with asyncio.timeout(FETCH_TIMEOUT): if not energyflip.is_authenticated(): - _LOGGER.warning("Huisbaasje is unauthenticated. Reauthenticating") + _LOGGER.warning("EnergyFlip is unauthenticated. Reauthenticating") await energyflip.authenticate() current_measurements = await energyflip.current_measurements() @@ -125,7 +125,7 @@ def _get_cumulative_value( ): """Get the cumulative energy consumption for a certain period. - :param current_measurements: The result from the Huisbaasje client + :param current_measurements: The result from the EnergyFlip client :param source_type: The source of energy (electricity or gas) :param period_type: The period for which cumulative value should be given. """ diff --git a/homeassistant/components/huisbaasje/config_flow.py b/homeassistant/components/huisbaasje/config_flow.py index d0d2632c386..ecf8cdbe431 100644 --- a/homeassistant/components/huisbaasje/config_flow.py +++ b/homeassistant/components/huisbaasje/config_flow.py @@ -1,4 +1,4 @@ -"""Config flow for Huisbaasje integration.""" +"""Config flow for EnergyFlip integration.""" import logging @@ -18,8 +18,8 @@ DATA_SCHEMA = vol.Schema( ) -class HuisbaasjeConfigFlow(ConfigFlow, domain=DOMAIN): - """Handle a config flow for Huisbaasje.""" +class EnergyFlipConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a config flow for EnergyFlip.""" VERSION = 1 diff --git a/homeassistant/components/huisbaasje/const.py b/homeassistant/components/huisbaasje/const.py index 108e3fffa1e..2738289343f 100644 --- a/homeassistant/components/huisbaasje/const.py +++ b/homeassistant/components/huisbaasje/const.py @@ -1,4 +1,4 @@ -"""Constants for the Huisbaasje integration.""" +"""Constants for the EnergyFlip integration.""" from energyflip.const import ( SOURCE_TYPE_ELECTRICITY, @@ -13,7 +13,7 @@ DATA_COORDINATOR = "coordinator" DOMAIN = "huisbaasje" -"""Interval in seconds between polls to huisbaasje.""" +"""Interval in seconds between polls to EnergyFlip.""" POLLING_INTERVAL = 20 """Timeout for fetching sensor data""" diff --git a/homeassistant/components/huisbaasje/manifest.json b/homeassistant/components/huisbaasje/manifest.json index 610abc833ce..7ea7be258b6 100644 --- a/homeassistant/components/huisbaasje/manifest.json +++ b/homeassistant/components/huisbaasje/manifest.json @@ -1,10 +1,10 @@ { "domain": "huisbaasje", - "name": "Huisbaasje", + "name": "EnergyFlip", "codeowners": ["@dennisschroer"], "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/huisbaasje", "iot_class": "cloud_polling", - "loggers": ["huisbaasje"], + "loggers": ["energyflip"], "requirements": ["energyflip-client==0.2.2"] } diff --git a/homeassistant/components/huisbaasje/sensor.py b/homeassistant/components/huisbaasje/sensor.py index 142d013ed1e..c024e3030fa 100644 --- a/homeassistant/components/huisbaasje/sensor.py +++ b/homeassistant/components/huisbaasje/sensor.py @@ -50,14 +50,14 @@ _LOGGER = logging.getLogger(__name__) @dataclass(frozen=True) -class HuisbaasjeSensorEntityDescription(SensorEntityDescription): +class EnergyFlipSensorEntityDescription(SensorEntityDescription): """Class describing Airly sensor entities.""" sensor_type: str = SENSOR_TYPE_RATE SENSORS_INFO = [ - HuisbaasjeSensorEntityDescription( + EnergyFlipSensorEntityDescription( translation_key="current_power", sensor_type=SENSOR_TYPE_RATE, device_class=SensorDeviceClass.POWER, @@ -65,7 +65,7 @@ SENSORS_INFO = [ key=SOURCE_TYPE_ELECTRICITY, state_class=SensorStateClass.MEASUREMENT, ), - HuisbaasjeSensorEntityDescription( + EnergyFlipSensorEntityDescription( translation_key="current_power_peak", sensor_type=SENSOR_TYPE_RATE, device_class=SensorDeviceClass.POWER, @@ -73,7 +73,7 @@ SENSORS_INFO = [ key=SOURCE_TYPE_ELECTRICITY_IN, state_class=SensorStateClass.MEASUREMENT, ), - HuisbaasjeSensorEntityDescription( + EnergyFlipSensorEntityDescription( translation_key="current_power_off_peak", sensor_type=SENSOR_TYPE_RATE, device_class=SensorDeviceClass.POWER, @@ -81,7 +81,7 @@ SENSORS_INFO = [ key=SOURCE_TYPE_ELECTRICITY_IN_LOW, state_class=SensorStateClass.MEASUREMENT, ), - HuisbaasjeSensorEntityDescription( + EnergyFlipSensorEntityDescription( translation_key="current_power_out_peak", sensor_type=SENSOR_TYPE_RATE, device_class=SensorDeviceClass.POWER, @@ -89,7 +89,7 @@ SENSORS_INFO = [ key=SOURCE_TYPE_ELECTRICITY_OUT, state_class=SensorStateClass.MEASUREMENT, ), - HuisbaasjeSensorEntityDescription( + EnergyFlipSensorEntityDescription( translation_key="current_power_out_off_peak", sensor_type=SENSOR_TYPE_RATE, device_class=SensorDeviceClass.POWER, @@ -97,7 +97,7 @@ SENSORS_INFO = [ key=SOURCE_TYPE_ELECTRICITY_OUT_LOW, state_class=SensorStateClass.MEASUREMENT, ), - HuisbaasjeSensorEntityDescription( + EnergyFlipSensorEntityDescription( translation_key="energy_consumption_peak_today", device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, @@ -106,7 +106,7 @@ SENSORS_INFO = [ state_class=SensorStateClass.TOTAL_INCREASING, suggested_display_precision=3, ), - HuisbaasjeSensorEntityDescription( + EnergyFlipSensorEntityDescription( translation_key="energy_consumption_off_peak_today", device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, @@ -115,7 +115,7 @@ SENSORS_INFO = [ state_class=SensorStateClass.TOTAL_INCREASING, suggested_display_precision=3, ), - HuisbaasjeSensorEntityDescription( + EnergyFlipSensorEntityDescription( translation_key="energy_production_peak_today", device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, @@ -124,7 +124,7 @@ SENSORS_INFO = [ state_class=SensorStateClass.TOTAL_INCREASING, suggested_display_precision=3, ), - HuisbaasjeSensorEntityDescription( + EnergyFlipSensorEntityDescription( translation_key="energy_production_off_peak_today", device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, @@ -133,7 +133,7 @@ SENSORS_INFO = [ state_class=SensorStateClass.TOTAL_INCREASING, suggested_display_precision=3, ), - HuisbaasjeSensorEntityDescription( + EnergyFlipSensorEntityDescription( translation_key="energy_today", device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, @@ -142,7 +142,7 @@ SENSORS_INFO = [ sensor_type=SENSOR_TYPE_THIS_DAY, suggested_display_precision=1, ), - HuisbaasjeSensorEntityDescription( + EnergyFlipSensorEntityDescription( translation_key="energy_week", device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, @@ -151,7 +151,7 @@ SENSORS_INFO = [ sensor_type=SENSOR_TYPE_THIS_WEEK, suggested_display_precision=1, ), - HuisbaasjeSensorEntityDescription( + EnergyFlipSensorEntityDescription( translation_key="energy_month", device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, @@ -160,7 +160,7 @@ SENSORS_INFO = [ sensor_type=SENSOR_TYPE_THIS_MONTH, suggested_display_precision=1, ), - HuisbaasjeSensorEntityDescription( + EnergyFlipSensorEntityDescription( translation_key="energy_year", device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, @@ -169,7 +169,7 @@ SENSORS_INFO = [ sensor_type=SENSOR_TYPE_THIS_YEAR, suggested_display_precision=1, ), - HuisbaasjeSensorEntityDescription( + EnergyFlipSensorEntityDescription( translation_key="current_gas", native_unit_of_measurement=UnitOfVolumeFlowRate.CUBIC_METERS_PER_HOUR, sensor_type=SENSOR_TYPE_RATE, @@ -177,7 +177,7 @@ SENSORS_INFO = [ key=SOURCE_TYPE_GAS, suggested_display_precision=2, ), - HuisbaasjeSensorEntityDescription( + EnergyFlipSensorEntityDescription( translation_key="gas_today", device_class=SensorDeviceClass.GAS, native_unit_of_measurement=UnitOfVolume.CUBIC_METERS, @@ -186,7 +186,7 @@ SENSORS_INFO = [ state_class=SensorStateClass.TOTAL_INCREASING, suggested_display_precision=2, ), - HuisbaasjeSensorEntityDescription( + EnergyFlipSensorEntityDescription( translation_key="gas_week", device_class=SensorDeviceClass.GAS, native_unit_of_measurement=UnitOfVolume.CUBIC_METERS, @@ -195,7 +195,7 @@ SENSORS_INFO = [ state_class=SensorStateClass.TOTAL_INCREASING, suggested_display_precision=2, ), - HuisbaasjeSensorEntityDescription( + EnergyFlipSensorEntityDescription( translation_key="gas_month", device_class=SensorDeviceClass.GAS, native_unit_of_measurement=UnitOfVolume.CUBIC_METERS, @@ -204,7 +204,7 @@ SENSORS_INFO = [ state_class=SensorStateClass.TOTAL_INCREASING, suggested_display_precision=2, ), - HuisbaasjeSensorEntityDescription( + EnergyFlipSensorEntityDescription( translation_key="gas_year", device_class=SensorDeviceClass.GAS, native_unit_of_measurement=UnitOfVolume.CUBIC_METERS, @@ -228,24 +228,24 @@ async def async_setup_entry( user_id = config_entry.data[CONF_ID] async_add_entities( - HuisbaasjeSensor(coordinator, user_id, description) + EnergyFlipSensor(coordinator, user_id, description) for description in SENSORS_INFO ) -class HuisbaasjeSensor( +class EnergyFlipSensor( CoordinatorEntity[DataUpdateCoordinator[dict[str, dict[str, Any]]]], SensorEntity ): - """Defines a Huisbaasje sensor.""" + """Defines a EnergyFlip sensor.""" - entity_description: HuisbaasjeSensorEntityDescription + entity_description: EnergyFlipSensorEntityDescription _attr_has_entity_name = True def __init__( self, coordinator: DataUpdateCoordinator[dict[str, dict[str, Any]]], user_id: str, - description: HuisbaasjeSensorEntityDescription, + description: EnergyFlipSensorEntityDescription, ) -> None: """Initialize the sensor.""" super().__init__(coordinator) diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index cdcd9c906d8..bbf96e4461b 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -2653,7 +2653,7 @@ "iot_class": "local_polling" }, "huisbaasje": { - "name": "Huisbaasje", + "name": "EnergyFlip", "integration_type": "hub", "config_flow": true, "iot_class": "cloud_polling" From 3cf52a4767f3fb6983886f69167b1ad658094a60 Mon Sep 17 00:00:00 2001 From: Pete Sage <76050312+PeteRager@users.noreply.github.com> Date: Sat, 22 Jun 2024 13:13:37 -0400 Subject: [PATCH 1032/1445] Sonos add tests for media_player.play_media share link (#120169) --- tests/components/sonos/conftest.py | 11 ++ tests/components/sonos/test_media_player.py | 128 ++++++++++++++++++++ 2 files changed, 139 insertions(+) diff --git a/tests/components/sonos/conftest.py b/tests/components/sonos/conftest.py index 378989c58fa..51dd2b9047c 100644 --- a/tests/components/sonos/conftest.py +++ b/tests/components/sonos/conftest.py @@ -237,6 +237,17 @@ def patch_gethostbyname(host: str) -> str: return host +@pytest.fixture(name="soco_sharelink") +def soco_sharelink(): + """Fixture to mock soco.plugins.sharelink.ShareLinkPlugin.""" + with patch("homeassistant.components.sonos.speaker.ShareLinkPlugin") as mock_share: + mock_instance = MagicMock() + mock_instance.is_share_link.return_value = True + mock_instance.add_share_link_to_queue.return_value = 10 + mock_share.return_value = mock_instance + yield mock_instance + + @pytest.fixture(name="soco_factory") def soco_factory( music_library, speaker_info, current_track_info_empty, battery_info, alarm_clock diff --git a/tests/components/sonos/test_media_player.py b/tests/components/sonos/test_media_player.py index a975538cdec..ab9b598bb04 100644 --- a/tests/components/sonos/test_media_player.py +++ b/tests/components/sonos/test_media_player.py @@ -302,6 +302,134 @@ async def test_play_media_lib_track_add( assert soco_mock.play_from_queue.call_count == 0 +_share_link: str = "spotify:playlist:abcdefghij0123456789XY" + + +async def test_play_media_share_link_add( + hass: HomeAssistant, + soco_factory: SoCoMockFactory, + async_autosetup_sonos, + soco_sharelink, +) -> None: + """Tests playing a share link with enqueue option add.""" + await hass.services.async_call( + MP_DOMAIN, + SERVICE_PLAY_MEDIA, + { + "entity_id": "media_player.zone_a", + "media_content_type": "playlist", + "media_content_id": _share_link, + ATTR_MEDIA_ENQUEUE: MediaPlayerEnqueue.ADD, + }, + blocking=True, + ) + assert soco_sharelink.add_share_link_to_queue.call_count == 1 + assert ( + soco_sharelink.add_share_link_to_queue.call_args_list[0].args[0] == _share_link + ) + assert ( + soco_sharelink.add_share_link_to_queue.call_args_list[0].kwargs["timeout"] + == LONG_SERVICE_TIMEOUT + ) + + +async def test_play_media_share_link_next( + hass: HomeAssistant, + soco_factory: SoCoMockFactory, + async_autosetup_sonos, + soco_sharelink, +) -> None: + """Tests playing a share link with enqueue option next.""" + await hass.services.async_call( + MP_DOMAIN, + SERVICE_PLAY_MEDIA, + { + "entity_id": "media_player.zone_a", + "media_content_type": "playlist", + "media_content_id": _share_link, + ATTR_MEDIA_ENQUEUE: MediaPlayerEnqueue.NEXT, + }, + blocking=True, + ) + assert soco_sharelink.add_share_link_to_queue.call_count == 1 + assert ( + soco_sharelink.add_share_link_to_queue.call_args_list[0].args[0] == _share_link + ) + assert ( + soco_sharelink.add_share_link_to_queue.call_args_list[0].kwargs["timeout"] + == LONG_SERVICE_TIMEOUT + ) + assert ( + soco_sharelink.add_share_link_to_queue.call_args_list[0].kwargs["position"] == 1 + ) + + +async def test_play_media_share_link_play( + hass: HomeAssistant, + soco_factory: SoCoMockFactory, + async_autosetup_sonos, + soco_sharelink, +) -> None: + """Tests playing a share link with enqueue option play.""" + soco_mock = soco_factory.mock_list.get("192.168.42.2") + await hass.services.async_call( + MP_DOMAIN, + SERVICE_PLAY_MEDIA, + { + "entity_id": "media_player.zone_a", + "media_content_type": "playlist", + "media_content_id": _share_link, + ATTR_MEDIA_ENQUEUE: MediaPlayerEnqueue.PLAY, + }, + blocking=True, + ) + assert soco_sharelink.add_share_link_to_queue.call_count == 1 + assert ( + soco_sharelink.add_share_link_to_queue.call_args_list[0].args[0] == _share_link + ) + assert ( + soco_sharelink.add_share_link_to_queue.call_args_list[0].kwargs["timeout"] + == LONG_SERVICE_TIMEOUT + ) + assert ( + soco_sharelink.add_share_link_to_queue.call_args_list[0].kwargs["position"] == 1 + ) + assert soco_mock.play_from_queue.call_count == 1 + soco_mock.play_from_queue.assert_called_with(9) + + +async def test_play_media_share_link_replace( + hass: HomeAssistant, + soco_factory: SoCoMockFactory, + async_autosetup_sonos, + soco_sharelink, +) -> None: + """Tests playing a share link with enqueue option replace.""" + soco_mock = soco_factory.mock_list.get("192.168.42.2") + await hass.services.async_call( + MP_DOMAIN, + SERVICE_PLAY_MEDIA, + { + "entity_id": "media_player.zone_a", + "media_content_type": "playlist", + "media_content_id": _share_link, + ATTR_MEDIA_ENQUEUE: MediaPlayerEnqueue.REPLACE, + }, + blocking=True, + ) + assert soco_mock.clear_queue.call_count == 1 + assert soco_sharelink.add_share_link_to_queue.call_count == 1 + assert ( + soco_sharelink.add_share_link_to_queue.call_args_list[0].args[0] == _share_link + ) + assert ( + soco_sharelink.add_share_link_to_queue.call_args_list[0].kwargs["timeout"] + == LONG_SERVICE_TIMEOUT + ) + assert soco_mock.play_from_queue.call_count == 1 + soco_mock.play_from_queue.assert_called_with(0) + + _mock_playlists = [ MockMusicServiceItem( "playlist1", From 753ab08b5ec06c7e3639be2eda366e25159fd553 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 22 Jun 2024 19:30:28 +0200 Subject: [PATCH 1033/1445] Add capability to exclude all attributes from recording (#119725) --- .../components/recorder/db_schema.py | 24 ++++++- tests/components/recorder/db_schema_42.py | 24 ++++++- tests/components/recorder/test_init.py | 65 +++++++++++++++++++ 3 files changed, 111 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/recorder/db_schema.py b/homeassistant/components/recorder/db_schema.py index 186b873047b..ce463067824 100644 --- a/homeassistant/components/recorder/db_schema.py +++ b/homeassistant/components/recorder/db_schema.py @@ -35,7 +35,12 @@ from sqlalchemy.ext.compiler import compiles from sqlalchemy.orm import DeclarativeBase, Mapped, aliased, mapped_column, relationship from sqlalchemy.types import TypeDecorator +from homeassistant.components.sensor import ATTR_STATE_CLASS from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_FRIENDLY_NAME, + ATTR_UNIT_OF_MEASUREMENT, + MATCH_ALL, MAX_LENGTH_EVENT_EVENT_TYPE, MAX_LENGTH_STATE_ENTITY_ID, MAX_LENGTH_STATE_STATE, @@ -584,10 +589,27 @@ class StateAttributes(Base): if (state := event.data["new_state"]) is None: return b"{}" if state_info := state.state_info: + unrecorded_attributes = state_info["unrecorded_attributes"] exclude_attrs = { *ALL_DOMAIN_EXCLUDE_ATTRS, - *state_info["unrecorded_attributes"], + *unrecorded_attributes, } + if MATCH_ALL in unrecorded_attributes: + # Don't exclude device class, state class, unit of measurement + # or friendly name when using the MATCH_ALL exclude constant + _exclude_attributes = { + k: v + for k, v in state.attributes.items() + if k + not in ( + ATTR_DEVICE_CLASS, + ATTR_STATE_CLASS, + ATTR_UNIT_OF_MEASUREMENT, + ATTR_FRIENDLY_NAME, + ) + } + exclude_attrs.update(_exclude_attributes) + else: exclude_attrs = ALL_DOMAIN_EXCLUDE_ATTRS encoder = json_bytes_strip_null if dialect == PSQL_DIALECT else json_bytes diff --git a/tests/components/recorder/db_schema_42.py b/tests/components/recorder/db_schema_42.py index b8e49aef592..c0dfc70571d 100644 --- a/tests/components/recorder/db_schema_42.py +++ b/tests/components/recorder/db_schema_42.py @@ -54,7 +54,12 @@ from homeassistant.components.recorder.models import ( ulid_to_bytes_or_none, uuid_hex_to_bytes_or_none, ) +from homeassistant.components.sensor import ATTR_STATE_CLASS from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_FRIENDLY_NAME, + ATTR_UNIT_OF_MEASUREMENT, + MATCH_ALL, MAX_LENGTH_EVENT_EVENT_TYPE, MAX_LENGTH_STATE_ENTITY_ID, MAX_LENGTH_STATE_STATE, @@ -577,10 +582,27 @@ class StateAttributes(Base): if state is None: return b"{}" if state_info := state.state_info: + unrecorded_attributes = state_info["unrecorded_attributes"] exclude_attrs = { *ALL_DOMAIN_EXCLUDE_ATTRS, - *state_info["unrecorded_attributes"], + *unrecorded_attributes, } + if MATCH_ALL in unrecorded_attributes: + # Don't exclude device class, state class, unit of measurement + # or friendly name when using the MATCH_ALL exclude constant + _exclude_attributes = { + k: v + for k, v in state.attributes.items() + if k + not in ( + ATTR_DEVICE_CLASS, + ATTR_STATE_CLASS, + ATTR_UNIT_OF_MEASUREMENT, + ATTR_FRIENDLY_NAME, + ) + } + exclude_attrs.update(_exclude_attributes) + else: exclude_attrs = ALL_DOMAIN_EXCLUDE_ATTRS encoder = json_bytes_strip_null if dialect == PSQL_DIALECT else json_bytes diff --git a/tests/components/recorder/test_init.py b/tests/components/recorder/test_init.py index 300d338fcb3..52947ce0c19 100644 --- a/tests/components/recorder/test_init.py +++ b/tests/components/recorder/test_init.py @@ -2420,6 +2420,71 @@ async def test_excluding_attributes_by_integration( assert state.as_dict() == expected.as_dict() +async def test_excluding_all_attributes_by_integration( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + setup_recorder: None, +) -> None: + """Test that an entity can exclude all attributes from being recorded using MATCH_ALL.""" + state = "restoring_from_db" + attributes = { + "test_attr": 5, + "excluded_component": 10, + "excluded_integration": 20, + "device_class": "test", + "state_class": "test", + "friendly_name": "Test entity", + "unit_of_measurement": "mm", + } + mock_platform( + hass, + "fake_integration.recorder", + Mock(exclude_attributes=lambda hass: {"excluded"}), + ) + hass.config.components.add("fake_integration") + hass.bus.async_fire(EVENT_COMPONENT_LOADED, {"component": "fake_integration"}) + await hass.async_block_till_done() + + class EntityWithExcludedAttributes(MockEntity): + _unrecorded_attributes = frozenset({MATCH_ALL}) + + entity_id = "test.fake_integration_recorder" + entity_platform = MockEntityPlatform(hass, platform_name="fake_integration") + entity = EntityWithExcludedAttributes( + entity_id=entity_id, + extra_state_attributes=attributes, + ) + await entity_platform.async_add_entities([entity]) + await hass.async_block_till_done() + + await async_wait_recording_done(hass) + + with session_scope(hass=hass, read_only=True) as session: + db_states = [] + for db_state, db_state_attributes, states_meta in ( + session.query(States, StateAttributes, StatesMeta) + .outerjoin( + StateAttributes, States.attributes_id == StateAttributes.attributes_id + ) + .outerjoin(StatesMeta, States.metadata_id == StatesMeta.metadata_id) + ): + db_state.entity_id = states_meta.entity_id + db_states.append(db_state) + state = db_state.to_native() + state.attributes = db_state_attributes.to_native() + assert len(db_states) == 1 + assert db_states[0].event_id is None + + expected = _state_with_context(hass, entity_id) + expected.attributes = { + "device_class": "test", + "state_class": "test", + "friendly_name": "Test entity", + "unit_of_measurement": "mm", + } + assert state.as_dict() == expected.as_dict() + + async def test_lru_increases_with_many_entities( small_cache_size: None, hass: HomeAssistant, setup_recorder: None ) -> None: From 725c309c0de3a3d54eab67b7fd05680514255d93 Mon Sep 17 00:00:00 2001 From: tronikos Date: Sat, 22 Jun 2024 10:37:18 -0700 Subject: [PATCH 1034/1445] Add image entity (screenshot) in Fully Kiosk Browser (#119622) --- .../components/fully_kiosk/__init__.py | 1 + homeassistant/components/fully_kiosk/image.py | 74 +++++++++++++++++++ .../components/fully_kiosk/strings.json | 5 ++ tests/components/fully_kiosk/test_image.py | 42 +++++++++++ 4 files changed, 122 insertions(+) create mode 100644 homeassistant/components/fully_kiosk/image.py create mode 100644 tests/components/fully_kiosk/test_image.py diff --git a/homeassistant/components/fully_kiosk/__init__.py b/homeassistant/components/fully_kiosk/__init__.py index 582ae23aea4..99b477c2989 100644 --- a/homeassistant/components/fully_kiosk/__init__.py +++ b/homeassistant/components/fully_kiosk/__init__.py @@ -14,6 +14,7 @@ PLATFORMS = [ Platform.BINARY_SENSOR, Platform.BUTTON, Platform.CAMERA, + Platform.IMAGE, Platform.MEDIA_PLAYER, Platform.NOTIFY, Platform.NUMBER, diff --git a/homeassistant/components/fully_kiosk/image.py b/homeassistant/components/fully_kiosk/image.py new file mode 100644 index 00000000000..fbf3481e38b --- /dev/null +++ b/homeassistant/components/fully_kiosk/image.py @@ -0,0 +1,74 @@ +"""Support for Fully Kiosk Browser image.""" + +from __future__ import annotations + +from collections.abc import Callable, Coroutine +from dataclasses import dataclass +from typing import Any + +from fullykiosk import FullyKiosk, FullyKioskError + +from homeassistant.components.image import ImageEntity, ImageEntityDescription +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.entity_platform import AddEntitiesCallback +import homeassistant.util.dt as dt_util + +from .const import DOMAIN +from .coordinator import FullyKioskDataUpdateCoordinator +from .entity import FullyKioskEntity + + +@dataclass(frozen=True, kw_only=True) +class FullyImageEntityDescription(ImageEntityDescription): + """Fully Kiosk Browser image entity description.""" + + image_fn: Callable[[FullyKiosk], Coroutine[Any, Any, bytes]] + + +IMAGES: tuple[FullyImageEntityDescription, ...] = ( + FullyImageEntityDescription( + key="screenshot", + translation_key="screenshot", + image_fn=lambda fully: fully.getScreenshot(), + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up the Fully Kiosk Browser image entities.""" + coordinator: FullyKioskDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + async_add_entities( + FullyImageEntity(coordinator, description) for description in IMAGES + ) + + +class FullyImageEntity(FullyKioskEntity, ImageEntity): + """Implement the image entity for Fully Kiosk Browser.""" + + entity_description: FullyImageEntityDescription + _attr_content_type = "image/png" + + def __init__( + self, + coordinator: FullyKioskDataUpdateCoordinator, + description: FullyImageEntityDescription, + ) -> None: + """Initialize the entity.""" + FullyKioskEntity.__init__(self, coordinator) + ImageEntity.__init__(self, coordinator.hass) + self.entity_description = description + self._attr_unique_id = f"{coordinator.data['deviceID']}-{description.key}" + + async def async_image(self) -> bytes | None: + """Return bytes of image.""" + try: + image_bytes = await self.entity_description.image_fn(self.coordinator.fully) + except FullyKioskError as err: + raise HomeAssistantError(err) from err + else: + self._attr_image_last_updated = dt_util.utcnow() + return image_bytes diff --git a/homeassistant/components/fully_kiosk/strings.json b/homeassistant/components/fully_kiosk/strings.json index c6fe65b8383..9c0049d3e5f 100644 --- a/homeassistant/components/fully_kiosk/strings.json +++ b/homeassistant/components/fully_kiosk/strings.json @@ -56,6 +56,11 @@ "name": "Load start URL" } }, + "image": { + "screenshot": { + "name": "Screenshot" + } + }, "notify": { "overlay_message": { "name": "Overlay message" diff --git a/tests/components/fully_kiosk/test_image.py b/tests/components/fully_kiosk/test_image.py new file mode 100644 index 00000000000..0dda707037f --- /dev/null +++ b/tests/components/fully_kiosk/test_image.py @@ -0,0 +1,42 @@ +"""Test the Fully Kiosk Browser image platform.""" + +from http import HTTPStatus +from unittest.mock import MagicMock + +from fullykiosk import FullyKioskError + +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from tests.common import MockConfigEntry +from tests.typing import ClientSessionGenerator + + +async def test_image( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + hass_client: ClientSessionGenerator, + mock_fully_kiosk: MagicMock, + init_integration: MockConfigEntry, +) -> None: + """Test the image entity.""" + entity_image = "image.amazon_fire_screenshot" + entity = hass.states.get(entity_image) + assert entity + assert entity.state == "unknown" + entry = entity_registry.async_get(entity_image) + assert entry + assert entry.unique_id == "abcdef-123456-screenshot" + + mock_fully_kiosk.getScreenshot.return_value = b"image_bytes" + client = await hass_client() + resp = await client.get(f"/api/image_proxy/{entity_image}") + assert resp.status == HTTPStatus.OK + assert resp.headers["Content-Type"] == "image/png" + assert await resp.read() == b"image_bytes" + assert mock_fully_kiosk.getScreenshot.call_count == 1 + + mock_fully_kiosk.getScreenshot.side_effect = FullyKioskError("error", "status") + client = await hass_client() + resp = await client.get(f"/api/image_proxy/{entity_image}") + assert resp.status == HTTPStatus.INTERNAL_SERVER_ERROR From 9b341f5b67737c4cccd1bfedbc7d56aa31530969 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 22 Jun 2024 19:42:55 +0200 Subject: [PATCH 1035/1445] Don't record attributes in sql (#120170) --- homeassistant/components/sql/sensor.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/sql/sensor.py b/homeassistant/components/sql/sensor.py index fd9762dcafc..f09f7ae95cf 100644 --- a/homeassistant/components/sql/sensor.py +++ b/homeassistant/components/sql/sensor.py @@ -30,6 +30,7 @@ from homeassistant.const import ( CONF_UNIT_OF_MEASUREMENT, CONF_VALUE_TEMPLATE, EVENT_HOMEASSISTANT_STOP, + MATCH_ALL, ) from homeassistant.core import Event, HomeAssistant, callback from homeassistant.exceptions import TemplateError @@ -307,6 +308,8 @@ def _generate_lambda_stmt(query: str) -> StatementLambdaElement: class SQLSensor(ManualTriggerSensorEntity): """Representation of an SQL sensor.""" + _unrecorded_attributes = frozenset({MATCH_ALL}) + def __init__( self, trigger_entity_config: ConfigType, From f06bd1b66f90c9a26d766cbc5faee0efabc9217c Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 22 Jun 2024 20:05:34 +0200 Subject: [PATCH 1036/1445] Remove YAML import from homeworks (#120171) --- .../components/homeworks/__init__.py | 49 +------ .../components/homeworks/config_flow.py | 120 +----------------- .../components/homeworks/test_config_flow.py | 114 +---------------- tests/components/homeworks/test_init.py | 27 +--- 4 files changed, 6 insertions(+), 304 deletions(-) diff --git a/homeassistant/components/homeworks/__init__.py b/homeassistant/components/homeworks/__init__.py index 2370cb1f577..e30778f7f15 100644 --- a/homeassistant/components/homeworks/__init__.py +++ b/homeassistant/components/homeworks/__init__.py @@ -11,7 +11,7 @@ from typing import Any from pyhomeworks.pyhomeworks import HW_BUTTON_PRESSED, HW_BUTTON_RELEASED, Homeworks import voluptuous as vol -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_HOST, CONF_ID, @@ -29,14 +29,7 @@ from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import ConfigType from homeassistant.util import slugify -from .const import ( - CONF_ADDR, - CONF_CONTROLLER_ID, - CONF_DIMMERS, - CONF_KEYPADS, - CONF_RATE, - DOMAIN, -) +from .const import CONF_ADDR, CONF_CONTROLLER_ID, CONF_KEYPADS, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -51,35 +44,7 @@ DEFAULT_FADE_RATE = 1.0 KEYPAD_LEDSTATE_POLL_COOLDOWN = 1.0 -CV_FADE_RATE = vol.All(vol.Coerce(float), vol.Range(min=0, max=20)) - -DIMMER_SCHEMA = vol.Schema( - { - vol.Required(CONF_ADDR): cv.string, - vol.Required(CONF_NAME): cv.string, - vol.Optional(CONF_RATE, default=DEFAULT_FADE_RATE): CV_FADE_RATE, - } -) - -KEYPAD_SCHEMA = vol.Schema( - {vol.Required(CONF_ADDR): cv.string, vol.Required(CONF_NAME): cv.string} -) - -CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - { - vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_PORT): cv.port, - vol.Required(CONF_DIMMERS): vol.All(cv.ensure_list, [DIMMER_SCHEMA]), - vol.Optional(CONF_KEYPADS, default=[]): vol.All( - cv.ensure_list, [KEYPAD_SCHEMA] - ), - } - ) - }, - extra=vol.ALLOW_EXTRA, -) +CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN) SERVICE_SEND_COMMAND_SCHEMA = vol.Schema( { @@ -157,14 +122,6 @@ async def async_send_command(hass: HomeAssistant, data: Mapping[str, Any]) -> No async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Start Homeworks controller.""" - - if DOMAIN in config: - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=config[DOMAIN] - ) - ) - async_setup_services(hass) return True diff --git a/homeassistant/components/homeworks/config_flow.py b/homeassistant/components/homeworks/config_flow.py index 02054fcf8e7..4b91018036a 100644 --- a/homeassistant/components/homeworks/config_flow.py +++ b/homeassistant/components/homeworks/config_flow.py @@ -14,17 +14,11 @@ from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT -from homeassistant.core import ( - DOMAIN as HOMEASSISTANT_DOMAIN, - HomeAssistant, - async_get_hass, - callback, -) +from homeassistant.core import async_get_hass, callback from homeassistant.data_entry_flow import AbortFlow from homeassistant.helpers import ( config_validation as cv, entity_registry as er, - issue_registry as ir, selector, ) from homeassistant.helpers.schema_config_entry_flow import ( @@ -148,24 +142,6 @@ async def _try_connection(user_input: dict[str, Any]) -> None: raise SchemaFlowError("unknown_error") from err -def _create_import_issue(hass: HomeAssistant) -> None: - """Create a repair issue asking the user to remove YAML.""" - ir.async_create_issue( - hass, - HOMEASSISTANT_DOMAIN, - f"deprecated_yaml_{DOMAIN}", - breaks_in_ha_version="2024.6.0", - is_fixable=False, - issue_domain=DOMAIN, - severity=ir.IssueSeverity.WARNING, - translation_key="deprecated_yaml", - translation_placeholders={ - "domain": DOMAIN, - "integration_title": "Lutron Homeworks", - }, - ) - - def _validate_address(handler: SchemaCommonFlowHandler, addr: str) -> None: """Validate address.""" try: @@ -547,100 +523,6 @@ OPTIONS_FLOW = { class HomeworksConfigFlowHandler(ConfigFlow, domain=DOMAIN): """Config flow for Lutron Homeworks.""" - import_config: dict[str, Any] - - async def async_step_import(self, config: dict[str, Any]) -> ConfigFlowResult: - """Start importing configuration from yaml.""" - self.import_config = { - CONF_HOST: config[CONF_HOST], - CONF_PORT: config[CONF_PORT], - CONF_DIMMERS: [ - { - CONF_ADDR: light[CONF_ADDR], - CONF_NAME: light[CONF_NAME], - CONF_RATE: light[CONF_RATE], - } - for light in config[CONF_DIMMERS] - ], - CONF_KEYPADS: [ - { - CONF_ADDR: keypad[CONF_ADDR], - CONF_BUTTONS: [], - CONF_NAME: keypad[CONF_NAME], - } - for keypad in config[CONF_KEYPADS] - ], - } - return await self.async_step_import_controller_name() - - async def async_step_import_controller_name( - self, user_input: dict[str, Any] | None = None - ) -> ConfigFlowResult: - """Ask user to set a name of the controller.""" - errors = {} - try: - self._async_abort_entries_match( - { - CONF_HOST: self.import_config[CONF_HOST], - CONF_PORT: self.import_config[CONF_PORT], - } - ) - except AbortFlow: - _create_import_issue(self.hass) - raise - - if user_input: - try: - user_input[CONF_CONTROLLER_ID] = slugify(user_input[CONF_NAME]) - self._async_abort_entries_match( - {CONF_CONTROLLER_ID: user_input[CONF_CONTROLLER_ID]} - ) - except AbortFlow: - errors["base"] = "duplicated_controller_id" - else: - self.import_config |= user_input - return await self.async_step_import_finish() - - return self.async_show_form( - step_id="import_controller_name", - data_schema=vol.Schema( - { - vol.Required( - CONF_NAME, description={"suggested_value": "Lutron Homeworks"} - ): selector.TextSelector(), - } - ), - errors=errors, - ) - - async def async_step_import_finish( - self, user_input: dict[str, Any] | None = None - ) -> ConfigFlowResult: - """Ask user to remove YAML configuration.""" - - if user_input is not None: - entity_registry = er.async_get(self.hass) - config = self.import_config - for light in config[CONF_DIMMERS]: - addr = light[CONF_ADDR] - if entity_id := entity_registry.async_get_entity_id( - LIGHT_DOMAIN, DOMAIN, f"homeworks.{addr}" - ): - entity_registry.async_update_entity( - entity_id, - new_unique_id=calculate_unique_id( - config[CONF_CONTROLLER_ID], addr, 0 - ), - ) - name = config.pop(CONF_NAME) - return self.async_create_entry( - title=name, - data={}, - options=config, - ) - - return self.async_show_form(step_id="import_finish", data_schema=vol.Schema({})) - async def _validate_edit_controller( self, user_input: dict[str, Any] ) -> dict[str, Any]: diff --git a/tests/components/homeworks/test_config_flow.py b/tests/components/homeworks/test_config_flow.py index d00b5a13150..8f5334b21f9 100644 --- a/tests/components/homeworks/test_config_flow.py +++ b/tests/components/homeworks/test_config_flow.py @@ -9,21 +9,17 @@ from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAI from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN from homeassistant.components.homeworks.const import ( CONF_ADDR, - CONF_DIMMERS, CONF_INDEX, - CONF_KEYPADS, CONF_LED, CONF_NUMBER, CONF_RATE, CONF_RELEASE_DELAY, DOMAIN, ) -from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN -from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_RECONFIGURE, SOURCE_USER +from homeassistant.config_entries import SOURCE_RECONFIGURE, SOURCE_USER from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType -from homeassistant.helpers import entity_registry as er, issue_registry as ir from tests.common import MockConfigEntry @@ -129,114 +125,6 @@ async def test_user_flow_cannot_connect( assert result["step_id"] == "user" -async def test_import_flow( - hass: HomeAssistant, - entity_registry: er.EntityRegistry, - issue_registry: ir.IssueRegistry, - mock_homeworks: MagicMock, - mock_setup_entry, -) -> None: - """Test importing yaml config.""" - entry = entity_registry.async_get_or_create( - LIGHT_DOMAIN, DOMAIN, "homeworks.[02:08:01:01]" - ) - - mock_controller = MagicMock() - mock_homeworks.return_value = mock_controller - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data={ - CONF_HOST: "192.168.0.1", - CONF_PORT: 1234, - CONF_DIMMERS: [ - { - CONF_ADDR: "[02:08:01:01]", - CONF_NAME: "Foyer Sconces", - CONF_RATE: 1.0, - } - ], - CONF_KEYPADS: [ - { - CONF_ADDR: "[02:08:02:01]", - CONF_NAME: "Foyer Keypad", - } - ], - }, - ) - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "import_controller_name" - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={CONF_NAME: "Main controller"} - ) - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "import_finish" - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={} - ) - assert result["type"] is FlowResultType.CREATE_ENTRY - assert result["title"] == "Main controller" - assert result["data"] == {} - assert result["options"] == { - "controller_id": "main_controller", - "dimmers": [{"addr": "[02:08:01:01]", "name": "Foyer Sconces", "rate": 1.0}], - "host": "192.168.0.1", - "keypads": [ - { - "addr": "[02:08:02:01]", - "buttons": [], - "name": "Foyer Keypad", - } - ], - "port": 1234, - } - assert len(issue_registry.issues) == 0 - - # Check unique ID is updated in entity registry - entry = entity_registry.async_get(entry.id) - assert entry.unique_id == "homeworks.main_controller.[02:08:01:01].0" - - -async def test_import_flow_already_exists( - hass: HomeAssistant, - issue_registry: ir.IssueRegistry, - mock_empty_config_entry: MockConfigEntry, -) -> None: - """Test importing yaml config where entry already exists.""" - mock_empty_config_entry.add_to_hass(hass) - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data={"host": "192.168.0.1", "port": 1234, CONF_DIMMERS: [], CONF_KEYPADS: []}, - ) - assert result["type"] is FlowResultType.ABORT - assert result["reason"] == "already_configured" - assert len(issue_registry.issues) == 1 - - -async def test_import_flow_controller_id_exists( - hass: HomeAssistant, mock_empty_config_entry: MockConfigEntry -) -> None: - """Test importing yaml config where entry already exists.""" - mock_empty_config_entry.add_to_hass(hass) - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data={"host": "192.168.0.2", "port": 1234, CONF_DIMMERS: [], CONF_KEYPADS: []}, - ) - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "import_controller_name" - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={CONF_NAME: "Main controller"} - ) - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "import_controller_name" - assert result["errors"] == {"base": "duplicated_controller_id"} - - async def test_reconfigure_flow( hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_homeworks: MagicMock ) -> None: diff --git a/tests/components/homeworks/test_init.py b/tests/components/homeworks/test_init.py index 1969bb448ec..87aabb6258f 100644 --- a/tests/components/homeworks/test_init.py +++ b/tests/components/homeworks/test_init.py @@ -6,39 +6,14 @@ from pyhomeworks.pyhomeworks import HW_BUTTON_PRESSED, HW_BUTTON_RELEASED import pytest from homeassistant.components.homeworks import EVENT_BUTTON_PRESS, EVENT_BUTTON_RELEASE -from homeassistant.components.homeworks.const import CONF_DIMMERS, CONF_KEYPADS, DOMAIN +from homeassistant.components.homeworks.const import DOMAIN from homeassistant.config_entries import ConfigEntryState -from homeassistant.const import CONF_HOST, CONF_PORT from homeassistant.core import HomeAssistant from homeassistant.exceptions import ServiceValidationError -from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry, async_capture_events -async def test_import( - hass: HomeAssistant, - mock_homeworks: MagicMock, -) -> None: - """Test the Homeworks YAML import.""" - await async_setup_component( - hass, - DOMAIN, - { - DOMAIN: { - CONF_HOST: "192.168.0.1", - CONF_PORT: 1234, - CONF_DIMMERS: [], - CONF_KEYPADS: [], - } - }, - ) - await hass.async_block_till_done() - - assert len(hass.config_entries.flow.async_progress()) == 1 - assert hass.config_entries.flow.async_progress()[0]["context"]["source"] == "import" - - async def test_load_unload_config_entry( hass: HomeAssistant, mock_config_entry: MockConfigEntry, From f14e8b728cf1d1a8919720496f0409ffcd9daf3d Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 22 Jun 2024 20:39:32 +0200 Subject: [PATCH 1037/1445] Remove YAML import from ping (#120176) --- homeassistant/components/ping/__init__.py | 2 +- .../components/ping/binary_sensor.py | 58 +--------- homeassistant/components/ping/config_flow.py | 26 +---- .../components/ping/device_tracker.py | 109 +----------------- tests/components/ping/test_binary_sensor.py | 30 +---- tests/components/ping/test_config_flow.py | 41 ------- tests/components/ping/test_device_tracker.py | 65 +---------- 7 files changed, 14 insertions(+), 317 deletions(-) diff --git a/homeassistant/components/ping/__init__.py b/homeassistant/components/ping/__init__.py index 12bad449f99..f4a04caae5b 100644 --- a/homeassistant/components/ping/__init__.py +++ b/homeassistant/components/ping/__init__.py @@ -19,7 +19,7 @@ from .helpers import PingDataICMPLib, PingDataSubProcess _LOGGER = logging.getLogger(__name__) -CONFIG_SCHEMA = cv.platform_only_config_schema(DOMAIN) +CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN) PLATFORMS = [Platform.BINARY_SENSOR, Platform.DEVICE_TRACKER, Platform.SENSOR] diff --git a/homeassistant/components/ping/binary_sensor.py b/homeassistant/components/ping/binary_sensor.py index 2c26b460047..93f4e0f3896 100644 --- a/homeassistant/components/ping/binary_sensor.py +++ b/homeassistant/components/ping/binary_sensor.py @@ -2,78 +2,26 @@ from __future__ import annotations -import logging from typing import Any -import voluptuous as vol - from homeassistant.components.binary_sensor import ( - PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, BinarySensorDeviceClass, BinarySensorEntity, ) -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import CONF_HOST, CONF_NAME -from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant -import homeassistant.helpers.config_validation as cv +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import PingConfigEntry -from .const import CONF_IMPORTED_BY, CONF_PING_COUNT, DEFAULT_PING_COUNT, DOMAIN +from .const import CONF_IMPORTED_BY from .coordinator import PingUpdateCoordinator from .entity import PingEntity -_LOGGER = logging.getLogger(__name__) - ATTR_ROUND_TRIP_TIME_AVG = "round_trip_time_avg" ATTR_ROUND_TRIP_TIME_MAX = "round_trip_time_max" ATTR_ROUND_TRIP_TIME_MDEV = "round_trip_time_mdev" ATTR_ROUND_TRIP_TIME_MIN = "round_trip_time_min" -PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_PING_COUNT, default=DEFAULT_PING_COUNT): vol.Range( - min=1, max=100 - ), - } -) - - -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigType, - async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """YAML init: import via config flow.""" - - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data={CONF_IMPORTED_BY: "binary_sensor", **config}, - ) - ) - - async_create_issue( - hass, - HOMEASSISTANT_DOMAIN, - f"deprecated_yaml_{DOMAIN}", - breaks_in_ha_version="2024.6.0", - is_fixable=False, - issue_domain=DOMAIN, - severity=IssueSeverity.WARNING, - translation_key="deprecated_yaml", - translation_placeholders={ - "domain": DOMAIN, - "integration_title": "Ping", - }, - ) - async def async_setup_entry( hass: HomeAssistant, entry: PingConfigEntry, async_add_entities: AddEntitiesCallback diff --git a/homeassistant/components/ping/config_flow.py b/homeassistant/components/ping/config_flow.py index 52600c379c4..9470b2134d4 100644 --- a/homeassistant/components/ping/config_flow.py +++ b/homeassistant/components/ping/config_flow.py @@ -2,7 +2,6 @@ from __future__ import annotations -from collections.abc import Mapping import logging from typing import Any @@ -18,12 +17,12 @@ from homeassistant.config_entries import ( ConfigFlowResult, OptionsFlow, ) -from homeassistant.const import CONF_HOST, CONF_NAME +from homeassistant.const import CONF_HOST from homeassistant.core import callback from homeassistant.helpers import selector from homeassistant.util.network import is_ip_address -from .const import CONF_IMPORTED_BY, CONF_PING_COUNT, DEFAULT_PING_COUNT, DOMAIN +from .const import CONF_PING_COUNT, DEFAULT_PING_COUNT, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -61,27 +60,6 @@ class PingConfigFlow(ConfigFlow, domain=DOMAIN): }, ) - async def async_step_import( - self, import_info: Mapping[str, Any] - ) -> ConfigFlowResult: - """Import an entry.""" - - to_import = { - CONF_HOST: import_info[CONF_HOST], - CONF_PING_COUNT: import_info[CONF_PING_COUNT], - CONF_CONSIDER_HOME: import_info.get( - CONF_CONSIDER_HOME, DEFAULT_CONSIDER_HOME - ).seconds, - } - title = import_info.get(CONF_NAME, import_info[CONF_HOST]) - - self._async_abort_entries_match({CONF_HOST: to_import[CONF_HOST]}) - return self.async_create_entry( - title=title, - data={CONF_IMPORTED_BY: import_info[CONF_IMPORTED_BY]}, - options=to_import, - ) - @staticmethod @callback def async_get_options_flow( diff --git a/homeassistant/components/ping/device_tracker.py b/homeassistant/components/ping/device_tracker.py index bbbc336a423..ce7cc4522a0 100644 --- a/homeassistant/components/ping/device_tracker.py +++ b/homeassistant/components/ping/device_tracker.py @@ -3,126 +3,23 @@ from __future__ import annotations from datetime import datetime, timedelta -import logging -from typing import Any - -import voluptuous as vol from homeassistant.components.device_tracker import ( CONF_CONSIDER_HOME, DEFAULT_CONSIDER_HOME, - PLATFORM_SCHEMA as BASE_PLATFORM_SCHEMA, - AsyncSeeCallback, ScannerEntity, SourceType, ) -from homeassistant.components.device_tracker.legacy import ( - YAML_DEVICES, - remove_device_from_config, -) -from homeassistant.config import load_yaml_config_file -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import ( - CONF_HOST, - CONF_HOSTS, - CONF_NAME, - EVENT_HOMEASSISTANT_STARTED, -) -from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, Event, HomeAssistant -from homeassistant.exceptions import HomeAssistantError -import homeassistant.helpers.config_validation as cv +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util import dt as dt_util from . import PingConfigEntry -from .const import CONF_IMPORTED_BY, CONF_PING_COUNT, DOMAIN +from .const import CONF_IMPORTED_BY from .coordinator import PingUpdateCoordinator -_LOGGER = logging.getLogger(__name__) - -PLATFORM_SCHEMA = BASE_PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_HOSTS): {cv.slug: cv.string}, - vol.Optional(CONF_PING_COUNT, default=1): cv.positive_int, - } -) - - -async def async_setup_scanner( - hass: HomeAssistant, - config: ConfigType, - async_see: AsyncSeeCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> bool: - """Legacy init: import via config flow.""" - - async def _run_import(_: Event) -> None: - """Delete devices from known_device.yaml and import them via config flow.""" - _LOGGER.debug( - "Home Assistant successfully started, importing ping device tracker config entries now" - ) - - devices: dict[str, dict[str, Any]] = {} - try: - devices = await hass.async_add_executor_job( - load_yaml_config_file, hass.config.path(YAML_DEVICES) - ) - except (FileNotFoundError, HomeAssistantError): - _LOGGER.debug( - "No valid known_devices.yaml found, " - "skip removal of devices from known_devices.yaml" - ) - - for dev_name, dev_host in config[CONF_HOSTS].items(): - if dev_name in devices: - await hass.async_add_executor_job( - remove_device_from_config, hass, dev_name - ) - _LOGGER.debug("Removed device %s from known_devices.yaml", dev_name) - - if not hass.states.async_available(f"device_tracker.{dev_name}"): - hass.states.async_remove(f"device_tracker.{dev_name}") - - # run import after everything has been cleaned up - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data={ - CONF_IMPORTED_BY: "device_tracker", - CONF_NAME: dev_name, - CONF_HOST: dev_host, - CONF_PING_COUNT: config[CONF_PING_COUNT], - CONF_CONSIDER_HOME: config[CONF_CONSIDER_HOME], - }, - ) - ) - - async_create_issue( - hass, - HOMEASSISTANT_DOMAIN, - f"deprecated_yaml_{DOMAIN}", - breaks_in_ha_version="2024.6.0", - is_fixable=False, - issue_domain=DOMAIN, - severity=IssueSeverity.WARNING, - translation_key="deprecated_yaml", - translation_placeholders={ - "domain": DOMAIN, - "integration_title": "Ping", - }, - ) - - # delay the import until after Home Assistant has started and everything has been initialized, - # as the legacy device tracker entities will be restored after the legacy device tracker platforms - # have been set up, so we can only remove the entities from the state machine then - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, _run_import) - - return True - async def async_setup_entry( hass: HomeAssistant, entry: PingConfigEntry, async_add_entities: AddEntitiesCallback diff --git a/tests/components/ping/test_binary_sensor.py b/tests/components/ping/test_binary_sensor.py index ea3145af253..660b5ca31f1 100644 --- a/tests/components/ping/test_binary_sensor.py +++ b/tests/components/ping/test_binary_sensor.py @@ -10,8 +10,8 @@ from syrupy import SnapshotAssertion from syrupy.filters import props from homeassistant.components.ping.const import CONF_IMPORTED_BY, DOMAIN -from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant -from homeassistant.helpers import entity_registry as er, issue_registry as ir +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry @@ -64,29 +64,3 @@ async def test_disabled_after_import( assert entry assert entry.disabled assert entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION - - -async def test_import_issue_creation( - hass: HomeAssistant, - issue_registry: ir.IssueRegistry, -) -> None: - """Test if import issue is raised.""" - - await async_setup_component( - hass, - "binary_sensor", - { - "binary_sensor": { - "platform": "ping", - "name": "test", - "host": "127.0.0.1", - "count": 1, - } - }, - ) - await hass.async_block_till_done() - - issue = issue_registry.async_get_issue( - HOMEASSISTANT_DOMAIN, f"deprecated_yaml_{DOMAIN}" - ) - assert issue diff --git a/tests/components/ping/test_config_flow.py b/tests/components/ping/test_config_flow.py index 1f55957410d..8204a000f29 100644 --- a/tests/components/ping/test_config_flow.py +++ b/tests/components/ping/test_config_flow.py @@ -6,12 +6,9 @@ import pytest from homeassistant import config_entries from homeassistant.components.ping import DOMAIN -from homeassistant.components.ping.const import CONF_IMPORTED_BY from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType -from .const import BINARY_SENSOR_IMPORT_DATA - from tests.common import MockConfigEntry @@ -87,41 +84,3 @@ async def test_options(hass: HomeAssistant, host, count, expected_title) -> None "host": "10.10.10.1", "consider_home": 180, } - - -@pytest.mark.usefixtures("patch_setup") -async def test_step_import(hass: HomeAssistant) -> None: - """Test for import step.""" - - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={CONF_IMPORTED_BY: "binary_sensor", **BINARY_SENSOR_IMPORT_DATA}, - ) - await hass.async_block_till_done() - - assert result["type"] is FlowResultType.CREATE_ENTRY - assert result["title"] == "test2" - assert result["data"] == {CONF_IMPORTED_BY: "binary_sensor"} - assert result["options"] == { - "host": "127.0.0.1", - "count": 1, - "consider_home": 240, - } - - # test import without name - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={CONF_IMPORTED_BY: "binary_sensor", "host": "10.10.10.10", "count": 5}, - ) - await hass.async_block_till_done() - - assert result["type"] is FlowResultType.CREATE_ENTRY - assert result["title"] == "10.10.10.10" - assert result["data"] == {CONF_IMPORTED_BY: "binary_sensor"} - assert result["options"] == { - "host": "10.10.10.10", - "count": 5, - "consider_home": 180, - } diff --git a/tests/components/ping/test_device_tracker.py b/tests/components/ping/test_device_tracker.py index f65f619b3c6..5aa425226b3 100644 --- a/tests/components/ping/test_device_tracker.py +++ b/tests/components/ping/test_device_tracker.py @@ -8,15 +8,10 @@ from icmplib import Host import pytest from typing_extensions import Generator -from homeassistant.components.device_tracker import legacy -from homeassistant.components.ping.const import DOMAIN -from homeassistant.const import EVENT_HOMEASSISTANT_STARTED -from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant -from homeassistant.helpers import entity_registry as er, issue_registry as ir -from homeassistant.setup import async_setup_component -from homeassistant.util.yaml import dump +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er -from tests.common import MockConfigEntry, async_fire_time_changed, patch_yaml_files +from tests.common import MockConfigEntry, async_fire_time_changed @pytest.fixture @@ -85,60 +80,6 @@ async def test_setup_and_update( assert state.state == "home" -async def test_import_issue_creation( - hass: HomeAssistant, - issue_registry: ir.IssueRegistry, -) -> None: - """Test if import issue is raised.""" - - await async_setup_component( - hass, - "device_tracker", - {"device_tracker": {"platform": "ping", "hosts": {"test": "10.10.10.10"}}}, - ) - await hass.async_block_till_done() - - hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) - await hass.async_block_till_done() - - issue = issue_registry.async_get_issue( - HOMEASSISTANT_DOMAIN, f"deprecated_yaml_{DOMAIN}" - ) - assert issue - - -async def test_import_delete_known_devices(hass: HomeAssistant) -> None: - """Test if import deletes known devices.""" - yaml_devices = { - "test": { - "hide_if_away": True, - "mac": "00:11:22:33:44:55", - "name": "Test name", - "picture": "/local/test.png", - "track": True, - }, - } - files = {legacy.YAML_DEVICES: dump(yaml_devices)} - - with ( - patch_yaml_files(files, True), - patch( - "homeassistant.components.ping.device_tracker.remove_device_from_config" - ) as remove_device_from_config, - ): - await async_setup_component( - hass, - "device_tracker", - {"device_tracker": {"platform": "ping", "hosts": {"test": "10.10.10.10"}}}, - ) - await hass.async_block_till_done() - - hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) - await hass.async_block_till_done() - - assert len(remove_device_from_config.mock_calls) == 1 - - @pytest.mark.usefixtures("entity_registry_enabled_by_default", "setup_integration") async def test_reload_not_triggering_home( hass: HomeAssistant, From 5ddda14e5963c92e5d5ce7970aee7742c644eac7 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 22 Jun 2024 20:55:03 +0200 Subject: [PATCH 1038/1445] Remove deprecated (moved) helpers from helpers.__init__ (#120172) --- homeassistant/helpers/__init__.py | 59 ------------------------------- tests/helpers/test_init.py | 50 -------------------------- 2 files changed, 109 deletions(-) delete mode 100644 tests/helpers/test_init.py diff --git a/homeassistant/helpers/__init__.py b/homeassistant/helpers/__init__.py index 9f72445822e..abb9bc79dc8 100644 --- a/homeassistant/helpers/__init__.py +++ b/homeassistant/helpers/__init__.py @@ -1,60 +1 @@ """Helper methods for components within Home Assistant.""" - -from __future__ import annotations - -from collections.abc import Iterable, Sequence -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from .typing import ConfigType - - -def config_per_platform( - config: ConfigType, domain: str -) -> Iterable[tuple[str | None, ConfigType]]: - """Break a component config into different platforms. - - For example, will find 'switch', 'switch 2', 'switch 3', .. etc - Async friendly. - """ - # pylint: disable-next=import-outside-toplevel - from homeassistant import config as ha_config - - # pylint: disable-next=import-outside-toplevel - from .deprecation import _print_deprecation_warning - - _print_deprecation_warning( - config_per_platform, - "config.config_per_platform", - "function", - "called", - "2024.6", - ) - return ha_config.config_per_platform(config, domain) - - -config_per_platform.__name__ = "helpers.config_per_platform" - - -def extract_domain_configs(config: ConfigType, domain: str) -> Sequence[str]: - """Extract keys from config for given domain name. - - Async friendly. - """ - # pylint: disable-next=import-outside-toplevel - from homeassistant import config as ha_config - - # pylint: disable-next=import-outside-toplevel - from .deprecation import _print_deprecation_warning - - _print_deprecation_warning( - extract_domain_configs, - "config.extract_domain_configs", - "function", - "called", - "2024.6", - ) - return ha_config.extract_domain_configs(config, domain) - - -extract_domain_configs.__name__ = "helpers.extract_domain_configs" diff --git a/tests/helpers/test_init.py b/tests/helpers/test_init.py deleted file mode 100644 index 39b387000ca..00000000000 --- a/tests/helpers/test_init.py +++ /dev/null @@ -1,50 +0,0 @@ -"""Test component helpers.""" - -from collections import OrderedDict - -import pytest - -from homeassistant import helpers - - -def test_extract_domain_configs(caplog: pytest.LogCaptureFixture) -> None: - """Test the extraction of domain configuration.""" - config = { - "zone": None, - "zoner": None, - "zone ": None, - "zone Hallo": None, - "zone 100": None, - } - - assert {"zone", "zone Hallo", "zone 100"} == set( - helpers.extract_domain_configs(config, "zone") - ) - - assert ( - "helpers.extract_domain_configs is a deprecated function which will be removed " - "in HA Core 2024.6. Use config.extract_domain_configs instead" in caplog.text - ) - - -def test_config_per_platform(caplog: pytest.LogCaptureFixture) -> None: - """Test config per platform method.""" - config = OrderedDict( - [ - ("zone", {"platform": "hello"}), - ("zoner", None), - ("zone Hallo", [1, {"platform": "hello 2"}]), - ("zone 100", None), - ] - ) - - assert [ - ("hello", config["zone"]), - (None, 1), - ("hello 2", config["zone Hallo"][1]), - ] == list(helpers.config_per_platform(config, "zone")) - - assert ( - "helpers.config_per_platform is a deprecated function which will be removed " - "in HA Core 2024.6. Use config.config_per_platform instead" in caplog.text - ) From 08fae5d4197de891b998a5853fca98097d886a62 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Sat, 22 Jun 2024 21:12:32 +0200 Subject: [PATCH 1039/1445] Add reconfigure flow to Fronius (#116132) --- .../components/fronius/config_flow.py | 91 ++++--- homeassistant/components/fronius/strings.json | 9 +- tests/components/fronius/test_config_flow.py | 233 +++++++++++++++++- tests/components/fronius/test_diagnostics.py | 2 +- 4 files changed, 303 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/fronius/config_flow.py b/homeassistant/components/fronius/config_flow.py index cd0078230a3..b16f43d58e8 100644 --- a/homeassistant/components/fronius/config_flow.py +++ b/homeassistant/components/fronius/config_flow.py @@ -10,7 +10,7 @@ from pyfronius import Fronius, FroniusError import voluptuous as vol from homeassistant.components.dhcp import DhcpServiceInfo -from homeassistant.config_entries import ConfigFlow, ConfigFlowResult +from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError @@ -22,12 +22,6 @@ _LOGGER: Final = logging.getLogger(__name__) DHCP_REQUEST_DELAY: Final = 60 -STEP_USER_DATA_SCHEMA = vol.Schema( - { - vol.Required(CONF_HOST): str, - } -) - def create_title(info: FroniusConfigEntryData) -> str: """Return the title of the config flow.""" @@ -40,10 +34,7 @@ def create_title(info: FroniusConfigEntryData) -> str: async def validate_host( hass: HomeAssistant, host: str ) -> tuple[str, FroniusConfigEntryData]: - """Validate the user input allows us to connect. - - Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user. - """ + """Validate the user input allows us to connect.""" fronius = Fronius(async_get_clientsession(hass), host) try: @@ -81,33 +72,32 @@ class FroniusConfigFlow(ConfigFlow, domain=DOMAIN): def __init__(self) -> None: """Initialize flow.""" self.info: FroniusConfigEntryData + self._entry: ConfigEntry | None = None async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: """Handle the initial step.""" - if user_input is None: - return self.async_show_form( - step_id="user", data_schema=STEP_USER_DATA_SCHEMA - ) - errors = {} - try: - unique_id, info = await validate_host(self.hass, user_input[CONF_HOST]) - except CannotConnect: - errors["base"] = "cannot_connect" - except Exception: - _LOGGER.exception("Unexpected exception") - errors["base"] = "unknown" - else: - await self.async_set_unique_id(unique_id, raise_on_progress=False) - self._abort_if_unique_id_configured(updates=dict(info)) + if user_input is not None: + try: + unique_id, info = await validate_host(self.hass, user_input[CONF_HOST]) + except CannotConnect: + errors["base"] = "cannot_connect" + except Exception: + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + await self.async_set_unique_id(unique_id, raise_on_progress=False) + self._abort_if_unique_id_configured(updates=dict(info)) - return self.async_create_entry(title=create_title(info), data=info) + return self.async_create_entry(title=create_title(info), data=info) return self.async_show_form( - step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors + step_id="user", + data_schema=vol.Schema({vol.Required(CONF_HOST): str}), + errors=errors, ) async def async_step_dhcp( @@ -150,6 +140,51 @@ class FroniusConfigFlow(ConfigFlow, domain=DOMAIN): }, ) + async def async_step_reconfigure( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Add reconfigure step to allow to reconfigure a config entry.""" + errors = {} + + if user_input is not None: + try: + unique_id, info = await validate_host(self.hass, user_input[CONF_HOST]) + except CannotConnect: + errors["base"] = "cannot_connect" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + # Config didn't change or is already configured in another entry + self._async_abort_entries_match(dict(info)) + + existing_entry = await self.async_set_unique_id( + unique_id, raise_on_progress=False + ) + assert self._entry is not None + if existing_entry and existing_entry.entry_id != self._entry.entry_id: + # Uid of device is already configured in another entry (but with different host) + self._abort_if_unique_id_configured() + + return self.async_update_reload_and_abort( + self._entry, + data=info, + reason="reconfigure_successful", + ) + + if self._entry is None: + self._entry = self.hass.config_entries.async_get_entry( + self.context["entry_id"] + ) + assert self._entry is not None + host = self._entry.data[CONF_HOST] + return self.async_show_form( + step_id="reconfigure", + data_schema=vol.Schema({vol.Required(CONF_HOST, default=host): str}), + description_placeholders={"device": self._entry.title}, + errors=errors, + ) + class CannotConnect(HomeAssistantError): """Error to indicate we cannot connect.""" diff --git a/homeassistant/components/fronius/strings.json b/homeassistant/components/fronius/strings.json index af93694284a..ccfb88852a8 100644 --- a/homeassistant/components/fronius/strings.json +++ b/homeassistant/components/fronius/strings.json @@ -11,6 +11,12 @@ }, "confirm_discovery": { "description": "Do you want to add {device} to Home Assistant?" + }, + "reconfigure": { + "description": "Update your configuration information for {device}.", + "data": { + "host": "[%key:common::config_flow::data::host%]" + } } }, "error": { @@ -19,7 +25,8 @@ }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "invalid_host": "[%key:common::config_flow::error::invalid_host%]" + "invalid_host": "[%key:common::config_flow::error::invalid_host%]", + "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]" } }, "entity": { diff --git a/tests/components/fronius/test_config_flow.py b/tests/components/fronius/test_config_flow.py index bf5ef360752..41593a0ad2e 100644 --- a/tests/components/fronius/test_config_flow.py +++ b/tests/components/fronius/test_config_flow.py @@ -50,7 +50,7 @@ async def test_form_with_logger(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] is FlowResultType.FORM - assert result["errors"] is None + assert not result["errors"] with ( patch( @@ -85,7 +85,7 @@ async def test_form_with_inverter(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] is FlowResultType.FORM - assert result["errors"] is None + assert not result["errors"] with ( patch( @@ -338,3 +338,232 @@ async def test_dhcp_invalid( ) assert result["type"] is FlowResultType.ABORT assert result["reason"] == "invalid_host" + + +async def test_reconfigure(hass: HomeAssistant) -> None: + """Test reconfiguring an entry.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="123.4567890", + data={ + CONF_HOST: "10.1.2.3", + "is_logger": True, + }, + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_RECONFIGURE, + "entry_id": entry.entry_id, + }, + data=entry.data, + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reconfigure" + + with ( + patch( + "pyfronius.Fronius.current_logger_info", + side_effect=FroniusError, + ), + patch( + "pyfronius.Fronius.inverter_info", + return_value=INVERTER_INFO_RETURN_VALUE, + ), + patch( + "homeassistant.components.fronius.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + "host": "10.9.1.1", + }, + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reconfigure_successful" + assert entry.data == { + "host": "10.9.1.1", + "is_logger": False, + } + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_reconfigure_cannot_connect(hass: HomeAssistant) -> None: + """Test we handle cannot connect error.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="123.4567890", + data={ + CONF_HOST: "10.1.2.3", + "is_logger": True, + }, + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_RECONFIGURE, + "entry_id": entry.entry_id, + }, + data=entry.data, + ) + + with ( + patch( + "pyfronius.Fronius.current_logger_info", + side_effect=FroniusError, + ), + patch( + "pyfronius.Fronius.inverter_info", + side_effect=FroniusError, + ), + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.1.1.1", + }, + ) + + assert result2["type"] is FlowResultType.FORM + assert result2["errors"] == {"base": "cannot_connect"} + + +async def test_reconfigure_unexpected(hass: HomeAssistant) -> None: + """Test we handle unexpected error.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="123.4567890", + data={ + CONF_HOST: "10.1.2.3", + "is_logger": True, + }, + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_RECONFIGURE, + "entry_id": entry.entry_id, + }, + data=entry.data, + ) + + with patch( + "pyfronius.Fronius.current_logger_info", + side_effect=KeyError, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.1.1.1", + }, + ) + + assert result2["type"] is FlowResultType.FORM + assert result2["errors"] == {"base": "unknown"} + + +async def test_reconfigure_already_configured(hass: HomeAssistant) -> None: + """Test reconfiguring an entry.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="123.4567890", + data={ + CONF_HOST: "10.1.2.3", + "is_logger": True, + }, + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_RECONFIGURE, + "entry_id": entry.entry_id, + }, + data=entry.data, + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reconfigure" + + with ( + patch( + "pyfronius.Fronius.current_logger_info", + return_value=LOGGER_INFO_RETURN_VALUE, + ), + patch( + "pyfronius.Fronius.inverter_info", + return_value=INVERTER_INFO_RETURN_VALUE, + ), + patch( + "homeassistant.components.fronius.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + "host": "10.1.2.3", + }, + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + assert len(mock_setup_entry.mock_calls) == 0 + + +async def test_reconfigure_already_existing(hass: HomeAssistant) -> None: + """Test reconfiguring entry to already existing device.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="123.4567890", + data={ + CONF_HOST: "10.1.2.3", + "is_logger": True, + }, + ) + entry.add_to_hass(hass) + + entry_2_uid = "222.2222222" + entry_2 = MockConfigEntry( + domain=DOMAIN, + unique_id=entry_2_uid, + data={ + CONF_HOST: "10.2.2.2", + "is_logger": True, + }, + ) + entry_2.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_RECONFIGURE, + "entry_id": entry.entry_id, + }, + data=entry.data, + ) + with patch( + "pyfronius.Fronius.current_logger_info", + return_value={"unique_identifier": {"value": entry_2_uid}}, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "10.1.1.1", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] is FlowResultType.ABORT + assert result2["reason"] == "already_configured" diff --git a/tests/components/fronius/test_diagnostics.py b/tests/components/fronius/test_diagnostics.py index 7d8a49dcb7d..7b1f384e405 100644 --- a/tests/components/fronius/test_diagnostics.py +++ b/tests/components/fronius/test_diagnostics.py @@ -1,4 +1,4 @@ -"""Tests for the diagnostics data provided by the KNX integration.""" +"""Tests for the diagnostics data provided by the Fronius integration.""" from syrupy import SnapshotAssertion From b59e7ede9a5b2cf4a54b7673999eb7385a2573bd Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 22 Jun 2024 22:05:45 +0200 Subject: [PATCH 1040/1445] Raise on incorrect suggested unit for sensor (#120180) --- homeassistant/components/sensor/__init__.py | 12 +++-------- tests/components/sensor/test_init.py | 23 +++++---------------- 2 files changed, 8 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index 689be1100f6..8d81df6431f 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -383,15 +383,9 @@ class SensorEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): ): if not self._invalid_suggested_unit_of_measurement_reported: self._invalid_suggested_unit_of_measurement_reported = True - report_issue = self._suggest_report_issue() - # This should raise in Home Assistant Core 2024.5 - _LOGGER.warning( - ( - "%s sets an invalid suggested_unit_of_measurement. Please %s. " - "This warning will become an error in Home Assistant Core 2024.5" - ), - type(self), - report_issue, + raise ValueError( + f"Entity {type(self)} suggest an incorrect " + f"unit of measurement: {suggested_unit_of_measurement}." ) return False diff --git a/tests/components/sensor/test_init.py b/tests/components/sensor/test_init.py index 0aa0ff3de85..126e327f364 100644 --- a/tests/components/sensor/test_init.py +++ b/tests/components/sensor/test_init.py @@ -4,7 +4,6 @@ from __future__ import annotations from datetime import UTC, date, datetime from decimal import Decimal -import logging from types import ModuleType from typing import Any @@ -2634,25 +2633,13 @@ async def test_suggested_unit_guard_invalid_unit( assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) await hass.async_block_till_done() - # Unit of measurement should be native one - state = hass.states.get(entity.entity_id) - assert int(state.state) == state_value - assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == native_unit + assert not hass.states.get("sensor.invalid") + assert not entity_registry.async_get("sensor.invalid") - # Assert the suggested unit is ignored and not stored in the entity registry - entry = entity_registry.async_get(entity.entity_id) - assert entry.unit_of_measurement == native_unit - assert entry.options == {} assert ( - "homeassistant.components.sensor", - logging.WARNING, - ( - " sets an" - " invalid suggested_unit_of_measurement. Please create a bug report at " - "https://github.com/home-assistant/core/issues?q=is%3Aopen+is%3Aissue+label%3A%22integration%3A+test%22." - " This warning will become an error in Home Assistant Core 2024.5" - ), - ) in caplog.record_tuples + "Entity suggest an incorrect unit of measurement: invalid_unit" + in caplog.text + ) @pytest.mark.parametrize( From 1ca187611d63e073e744301c233865839704ac3b Mon Sep 17 00:00:00 2001 From: Scott K Logan Date: Sat, 22 Jun 2024 16:35:21 -0500 Subject: [PATCH 1041/1445] Bump aioraven to 0.6.0 (#120184) --- homeassistant/components/rainforest_raven/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/rainforest_raven/manifest.json b/homeassistant/components/rainforest_raven/manifest.json index a2717f0e886..bc44c3fc30c 100644 --- a/homeassistant/components/rainforest_raven/manifest.json +++ b/homeassistant/components/rainforest_raven/manifest.json @@ -6,7 +6,7 @@ "dependencies": ["usb"], "documentation": "https://www.home-assistant.io/integrations/rainforest_raven", "iot_class": "local_polling", - "requirements": ["aioraven==0.5.3"], + "requirements": ["aioraven==0.6.0"], "usb": [ { "vid": "0403", diff --git a/requirements_all.txt b/requirements_all.txt index 9c940ff410a..0d98c4f773c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -344,7 +344,7 @@ aiopyarr==23.4.0 aioqsw==0.3.5 # homeassistant.components.rainforest_raven -aioraven==0.5.3 +aioraven==0.6.0 # homeassistant.components.recollect_waste aiorecollect==2023.09.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0d3112c7aba..ad51c91f9b6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -317,7 +317,7 @@ aiopyarr==23.4.0 aioqsw==0.3.5 # homeassistant.components.rainforest_raven -aioraven==0.5.3 +aioraven==0.6.0 # homeassistant.components.recollect_waste aiorecollect==2023.09.0 From 57e615aa3697c5bed443ab87a4ca3f2b1d537a31 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Sun, 23 Jun 2024 00:36:58 +0300 Subject: [PATCH 1042/1445] Don't log Shelly push update failures if there are no errors (#120189) --- homeassistant/components/shelly/coordinator.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/shelly/coordinator.py b/homeassistant/components/shelly/coordinator.py index f15eca51413..82d358b33d8 100644 --- a/homeassistant/components/shelly/coordinator.py +++ b/homeassistant/components/shelly/coordinator.py @@ -404,9 +404,10 @@ class ShellyBlockCoordinator(ShellyCoordinatorBase[BlockDevice]): "ip_address": self.device.ip_address, }, ) - LOGGER.debug( - "Push update failures for %s: %s", self.name, self._push_update_failures - ) + if self._push_update_failures: + LOGGER.debug( + "Push update failures for %s: %s", self.name, self._push_update_failures + ) self.async_set_updated_data(None) def async_setup(self, pending_platforms: list[Platform] | None = None) -> None: From ea0c93e3dbf03fea8a3a32cebce56c8e73bf4a46 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 22 Jun 2024 18:11:48 -0500 Subject: [PATCH 1043/1445] Update uiprotect to 3.1.1 (#120173) --- .../components/unifiprotect/__init__.py | 17 ++- .../components/unifiprotect/binary_sensor.py | 3 +- .../components/unifiprotect/const.py | 7 +- homeassistant/components/unifiprotect/data.py | 113 +++++++++--------- .../components/unifiprotect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/unifiprotect/conftest.py | 8 ++ tests/components/unifiprotect/test_camera.py | 38 +++++- tests/components/unifiprotect/test_init.py | 16 ++- tests/components/unifiprotect/utils.py | 2 + 11 files changed, 123 insertions(+), 87 deletions(-) diff --git a/homeassistant/components/unifiprotect/__init__.py b/homeassistant/components/unifiprotect/__init__.py index 068c5665e6b..394a7f43329 100644 --- a/homeassistant/components/unifiprotect/__init__.py +++ b/homeassistant/components/unifiprotect/__init__.py @@ -6,6 +6,7 @@ from datetime import timedelta import logging from aiohttp.client_exceptions import ServerDisconnectedError +from uiprotect.api import DEVICE_UPDATE_INTERVAL from uiprotect.data import Bootstrap from uiprotect.data.types import FirmwareReleaseChannel from uiprotect.exceptions import ClientError, NotAuthorized @@ -29,7 +30,6 @@ from homeassistant.helpers.typing import ConfigType from .const import ( AUTH_RETRIES, CONF_ALLOW_EA, - DEFAULT_SCAN_INTERVAL, DEVICES_THAT_ADOPT, DOMAIN, MIN_REQUIRED_PROTECT_V, @@ -49,7 +49,7 @@ from .views import ThumbnailProxyView, VideoProxyView _LOGGER = logging.getLogger(__name__) -SCAN_INTERVAL = timedelta(seconds=DEFAULT_SCAN_INTERVAL) +SCAN_INTERVAL = timedelta(seconds=DEVICE_UPDATE_INTERVAL) CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN) @@ -70,11 +70,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: UFPConfigEntry) -> bool: """Set up the UniFi Protect config entries.""" protect = async_create_api_client(hass, entry) _LOGGER.debug("Connect to UniFi Protect") - data_service = ProtectData(hass, protect, SCAN_INTERVAL, entry) try: - bootstrap = await protect.get_bootstrap() - nvr_info = bootstrap.nvr + await protect.update() except NotAuthorized as err: retry_key = f"{entry.entry_id}_auth" retries = hass.data.setdefault(DOMAIN, {}).get(retry_key, 0) @@ -86,6 +84,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: UFPConfigEntry) -> bool: except (TimeoutError, ClientError, ServerDisconnectedError) as err: raise ConfigEntryNotReady from err + data_service = ProtectData(hass, protect, SCAN_INTERVAL, entry) + bootstrap = protect.bootstrap + nvr_info = bootstrap.nvr auth_user = bootstrap.users.get(bootstrap.auth_user_id) if auth_user and auth_user.cloud_account: ir.async_create_issue( @@ -169,11 +170,7 @@ async def _async_setup_entry( bootstrap: Bootstrap, ) -> None: await async_migrate_data(hass, entry, data_service.api, bootstrap) - - await data_service.async_setup() - if not data_service.last_update_success: - raise ConfigEntryNotReady - + data_service.async_setup() await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) hass.http.register_view(ThumbnailProxyView(hass)) hass.http.register_view(VideoProxyView(hass)) diff --git a/homeassistant/components/unifiprotect/binary_sensor.py b/homeassistant/components/unifiprotect/binary_sensor.py index 5596d3b7a62..fb60158580e 100644 --- a/homeassistant/components/unifiprotect/binary_sensor.py +++ b/homeassistant/components/unifiprotect/binary_sensor.py @@ -691,6 +691,7 @@ class ProtectDiskBinarySensor(ProtectNVREntity, BinarySensorEntity): super()._async_update_device_from_protect(device) slot = self._disk.slot self._attr_available = False + available = self.data.last_update_success # should not be possible since it would require user to # _downgrade_ to make ustorage disppear @@ -698,7 +699,7 @@ class ProtectDiskBinarySensor(ProtectNVREntity, BinarySensorEntity): for disk in self.device.system_info.ustorage.disks: if disk.slot == slot: self._disk = disk - self._attr_available = True + self._attr_available = available break self._attr_is_on = not self._disk.is_healthy diff --git a/homeassistant/components/unifiprotect/const.py b/homeassistant/components/unifiprotect/const.py index 9839d823585..b56761263f4 100644 --- a/homeassistant/components/unifiprotect/const.py +++ b/homeassistant/components/unifiprotect/const.py @@ -5,9 +5,9 @@ from uiprotect.data import ModelType, Version from homeassistant.const import Platform DOMAIN = "unifiprotect" -# some UniFi OS consoles have an unknown rate limit on auth -# if rate limit is triggered a 401 is returned -AUTH_RETRIES = 11 # ~12 hours of retries with the last waiting ~6 hours +# If rate limit for 4.x or later a 429 is returned +# so we can use a lower value +AUTH_RETRIES = 2 ATTR_EVENT_SCORE = "event_score" ATTR_EVENT_ID = "event_id" @@ -35,7 +35,6 @@ CONFIG_OPTIONS = [ DEFAULT_PORT = 443 DEFAULT_ATTRIBUTION = "Powered by UniFi Protect Server" DEFAULT_BRAND = "Ubiquiti" -DEFAULT_SCAN_INTERVAL = 60 DEFAULT_VERIFY_SSL = False DEFAULT_MAX_MEDIA = 1000 diff --git a/homeassistant/components/unifiprotect/data.py b/homeassistant/components/unifiprotect/data.py index e3e4cbc7f50..6b502eaa5f3 100644 --- a/homeassistant/components/unifiprotect/data.py +++ b/homeassistant/components/unifiprotect/data.py @@ -13,7 +13,6 @@ from typing_extensions import Generator from uiprotect import ProtectApiClient from uiprotect.data import ( NVR, - Bootstrap, Camera, Event, EventType, @@ -23,6 +22,7 @@ from uiprotect.data import ( ) from uiprotect.exceptions import ClientError, NotAuthorized from uiprotect.utils import log_event +from uiprotect.websocket import WebsocketState from homeassistant.config_entries import ConfigEntry from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback @@ -83,8 +83,7 @@ class ProtectData: str, set[Callable[[ProtectDeviceType], None]] ] = defaultdict(set) self._pending_camera_ids: set[str] = set() - self._unsub_interval: CALLBACK_TYPE | None = None - self._unsub_websocket: CALLBACK_TYPE | None = None + self._unsubs: list[CALLBACK_TYPE] = [] self._auth_failures = 0 self.last_update_success = False self.api = protect @@ -115,11 +114,9 @@ class ProtectData: self, device_types: Iterable[ModelType], ignore_unadopted: bool = True ) -> Generator[ProtectAdoptableDeviceModel]: """Get all devices matching types.""" + bootstrap = self.api.bootstrap for device_type in device_types: - devices = async_get_devices_by_type( - self.api.bootstrap, device_type - ).values() - for device in devices: + for device in async_get_devices_by_type(bootstrap, device_type).values(): if ignore_unadopted and not device.is_adopted_by_us: continue yield device @@ -130,33 +127,61 @@ class ProtectData: Generator[Camera], self.get_by_types({ModelType.CAMERA}, ignore_unadopted) ) - async def async_setup(self) -> None: + @callback + def async_setup(self) -> None: """Subscribe and do the refresh.""" - self._unsub_websocket = self.api.subscribe_websocket( - self._async_process_ws_message - ) - await self.async_refresh() + self.last_update_success = True + self._async_update_change(True, force_update=True) + api = self.api + self._unsubs = [ + api.subscribe_websocket_state(self._async_websocket_state_changed), + api.subscribe_websocket(self._async_process_ws_message), + async_track_time_interval( + self._hass, self._async_poll, self._update_interval + ), + ] + + @callback + def _async_websocket_state_changed(self, state: WebsocketState) -> None: + """Handle a change in the websocket state.""" + self._async_update_change(state is WebsocketState.CONNECTED) + + def _async_update_change( + self, + success: bool, + force_update: bool = False, + exception: Exception | None = None, + ) -> None: + """Process a change in update success.""" + was_success = self.last_update_success + self.last_update_success = success + + if not success: + level = logging.ERROR if was_success else logging.DEBUG + title = self._entry.title + _LOGGER.log(level, "%s: Connection lost", title, exc_info=exception) + self._async_process_updates() + return + + self._auth_failures = 0 + if not was_success: + _LOGGER.info("%s: Connection restored", self._entry.title) + self._async_process_updates() + elif force_update: + self._async_process_updates() async def async_stop(self, *args: Any) -> None: """Stop processing data.""" - if self._unsub_websocket: - self._unsub_websocket() - self._unsub_websocket = None - if self._unsub_interval: - self._unsub_interval() - self._unsub_interval = None + for unsub in self._unsubs: + unsub() + self._unsubs.clear() await self.api.async_disconnect_ws() - async def async_refresh(self, *_: Any, force: bool = False) -> None: + async def async_refresh(self) -> None: """Update the data.""" - - # if last update was failure, force until success - if not self.last_update_success: - force = True - try: - updates = await self.api.update(force=force) - except NotAuthorized: + await self.api.update() + except NotAuthorized as ex: if self._auth_failures < AUTH_RETRIES: _LOGGER.exception("Auth error while updating") self._auth_failures += 1 @@ -164,17 +189,11 @@ class ProtectData: await self.async_stop() _LOGGER.exception("Reauthentication required") self._entry.async_start_reauth(self._hass) - self.last_update_success = False - except ClientError: - if self.last_update_success: - _LOGGER.exception("Error while updating") - self.last_update_success = False - # manually trigger update to mark entities unavailable - self._async_process_updates(self.api.bootstrap) + self._async_update_change(False, exception=ex) + except ClientError as ex: + self._async_update_change(False, exception=ex) else: - self.last_update_success = True - self._auth_failures = 0 - self._async_process_updates(updates) + self._async_update_change(True, force_update=True) @callback def async_add_pending_camera_id(self, camera_id: str) -> None: @@ -184,7 +203,6 @@ class ProtectData: initialized yet. Will cause Websocket code to check for channels to be initialized for the camera and issue a dispatch once they do. """ - self._pending_camera_ids.add(camera_id) @callback @@ -278,25 +296,15 @@ class ProtectData: self._async_update_device(new_obj, message.changed_data) @callback - def _async_process_updates(self, updates: Bootstrap | None) -> None: + def _async_process_updates(self) -> None: """Process update from the protect data.""" - - # Websocket connected, use data from it - if updates is None: - return - self._async_signal_device_update(self.api.bootstrap.nvr) for device in self.get_by_types(DEVICES_THAT_ADOPT): self._async_signal_device_update(device) @callback def _async_poll(self, now: datetime) -> None: - """Poll the Protect API. - - If the websocket is connected, most of the time - this will be a no-op. If the websocket is disconnected, - this will trigger a reconnect and refresh. - """ + """Poll the Protect API.""" self._entry.async_create_background_task( self._hass, self.async_refresh(), @@ -309,10 +317,6 @@ class ProtectData: self, mac: str, update_callback: Callable[[ProtectDeviceType], None] ) -> CALLBACK_TYPE: """Add an callback subscriber.""" - if not self._subscriptions: - self._unsub_interval = async_track_time_interval( - self._hass, self._async_poll, self._update_interval - ) self._subscriptions[mac].add(update_callback) return partial(self._async_unsubscribe, mac, update_callback) @@ -324,9 +328,6 @@ class ProtectData: self._subscriptions[mac].remove(update_callback) if not self._subscriptions[mac]: del self._subscriptions[mac] - if not self._subscriptions and self._unsub_interval: - self._unsub_interval() - self._unsub_interval = None @callback def _async_signal_device_update(self, device: ProtectDeviceType) -> None: diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index 987329abbba..15b8b5b4a1b 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -40,7 +40,7 @@ "integration_type": "hub", "iot_class": "local_push", "loggers": ["uiprotect", "unifi_discovery"], - "requirements": ["uiprotect==1.20.0", "unifi-discovery==1.1.8"], + "requirements": ["uiprotect==3.1.1", "unifi-discovery==1.1.8"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/requirements_all.txt b/requirements_all.txt index 0d98c4f773c..2343fa9bd4e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2794,7 +2794,7 @@ twitchAPI==4.0.0 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==1.20.0 +uiprotect==3.1.1 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ad51c91f9b6..fdd67cf9e29 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2174,7 +2174,7 @@ twitchAPI==4.0.0 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==1.20.0 +uiprotect==3.1.1 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 diff --git a/tests/components/unifiprotect/conftest.py b/tests/components/unifiprotect/conftest.py index 6366a4f9244..0bef1ff0eb9 100644 --- a/tests/components/unifiprotect/conftest.py +++ b/tests/components/unifiprotect/conftest.py @@ -29,6 +29,7 @@ from uiprotect.data import ( Viewer, WSSubscriptionMessage, ) +from uiprotect.websocket import WebsocketState from homeassistant.components.unifiprotect.const import DOMAIN from homeassistant.core import HomeAssistant @@ -148,7 +149,14 @@ def mock_entry( ufp.ws_subscription = ws_callback return Mock() + def subscribe_websocket_state( + ws_state_subscription: Callable[[WebsocketState], None], + ) -> Any: + ufp.ws_state_subscription = ws_state_subscription + return Mock() + ufp_client.subscribe_websocket = subscribe + ufp_client.subscribe_websocket_state = subscribe_websocket_state yield ufp diff --git a/tests/components/unifiprotect/test_camera.py b/tests/components/unifiprotect/test_camera.py index 444898fbd85..9fedb67fea4 100644 --- a/tests/components/unifiprotect/test_camera.py +++ b/tests/components/unifiprotect/test_camera.py @@ -4,10 +4,13 @@ from __future__ import annotations from unittest.mock import AsyncMock, Mock +from uiprotect.api import DEVICE_UPDATE_INTERVAL from uiprotect.data import Camera as ProtectCamera, CameraChannel, StateType from uiprotect.exceptions import NvrError +from uiprotect.websocket import WebsocketState from homeassistant.components.camera import ( + STATE_IDLE, CameraEntityFeature, async_get_image, async_get_stream_source, @@ -19,13 +22,13 @@ from homeassistant.components.unifiprotect.const import ( ATTR_HEIGHT, ATTR_WIDTH, DEFAULT_ATTRIBUTION, - DEFAULT_SCAN_INTERVAL, ) from homeassistant.components.unifiprotect.utils import get_camera_base_name from homeassistant.const import ( ATTR_ATTRIBUTION, ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, + STATE_UNAVAILABLE, Platform, ) from homeassistant.core import HomeAssistant @@ -377,7 +380,7 @@ async def test_camera_interval_update( ufp.api.bootstrap.cameras = {new_camera.id: new_camera} ufp.api.update = AsyncMock(return_value=ufp.api.bootstrap) - await time_changed(hass, DEFAULT_SCAN_INTERVAL) + await time_changed(hass, DEVICE_UPDATE_INTERVAL) state = hass.states.get(entity_id) assert state and state.state == "recording" @@ -397,19 +400,46 @@ async def test_camera_bad_interval_update( # update fails ufp.api.update = AsyncMock(side_effect=NvrError) - await time_changed(hass, DEFAULT_SCAN_INTERVAL) + await time_changed(hass, DEVICE_UPDATE_INTERVAL) state = hass.states.get(entity_id) assert state and state.state == "unavailable" # next update succeeds ufp.api.update = AsyncMock(return_value=ufp.api.bootstrap) - await time_changed(hass, DEFAULT_SCAN_INTERVAL) + await time_changed(hass, DEVICE_UPDATE_INTERVAL) state = hass.states.get(entity_id) assert state and state.state == "idle" +async def test_camera_websocket_disconnected( + hass: HomeAssistant, ufp: MockUFPFixture, camera: ProtectCamera +) -> None: + """Test the websocket gets disconnected and reconnected.""" + + await init_entry(hass, ufp, [camera]) + assert_entity_counts(hass, Platform.CAMERA, 2, 1) + entity_id = "camera.test_camera_high_resolution_channel" + + state = hass.states.get(entity_id) + assert state and state.state == STATE_IDLE + + # websocket disconnects + ufp.ws_state_subscription(WebsocketState.DISCONNECTED) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state and state.state == STATE_UNAVAILABLE + + # websocket reconnects + ufp.ws_state_subscription(WebsocketState.CONNECTED) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state and state.state == STATE_IDLE + + async def test_camera_ws_update( hass: HomeAssistant, ufp: MockUFPFixture, camera: ProtectCamera ) -> None: diff --git a/tests/components/unifiprotect/test_init.py b/tests/components/unifiprotect/test_init.py index 3b75afaace8..46e57c62101 100644 --- a/tests/components/unifiprotect/test_init.py +++ b/tests/components/unifiprotect/test_init.py @@ -5,12 +5,12 @@ from __future__ import annotations from unittest.mock import AsyncMock, patch from uiprotect import NotAuthorized, NvrError, ProtectApiClient +from uiprotect.api import DEVICE_UPDATE_INTERVAL from uiprotect.data import NVR, Bootstrap, CloudAccount, Light from homeassistant.components.unifiprotect.const import ( AUTH_RETRIES, CONF_DISABLE_RTSP, - DEFAULT_SCAN_INTERVAL, DOMAIN, ) from homeassistant.config_entries import ConfigEntry, ConfigEntryState @@ -116,12 +116,12 @@ async def test_setup_too_old( old_bootstrap = ufp.api.bootstrap.copy() old_bootstrap.nvr = old_nvr - ufp.api.get_bootstrap.return_value = old_bootstrap + ufp.api.update.return_value = old_bootstrap + ufp.api.bootstrap = old_bootstrap await hass.config_entries.async_setup(ufp.entry.entry_id) await hass.async_block_till_done() assert ufp.entry.state is ConfigEntryState.SETUP_ERROR - assert not ufp.api.update.called async def test_setup_cloud_account( @@ -179,13 +179,13 @@ async def test_setup_failed_update_reauth( # to verify it is not transient ufp.api.update = AsyncMock(side_effect=NotAuthorized) for _ in range(AUTH_RETRIES): - await time_changed(hass, DEFAULT_SCAN_INTERVAL) + await time_changed(hass, DEVICE_UPDATE_INTERVAL) assert len(hass.config_entries.flow._progress) == 0 assert ufp.api.update.call_count == AUTH_RETRIES assert ufp.entry.state is ConfigEntryState.LOADED - await time_changed(hass, DEFAULT_SCAN_INTERVAL) + await time_changed(hass, DEVICE_UPDATE_INTERVAL) assert ufp.api.update.call_count == AUTH_RETRIES + 1 assert len(hass.config_entries.flow._progress) == 1 @@ -193,18 +193,17 @@ async def test_setup_failed_update_reauth( async def test_setup_failed_error(hass: HomeAssistant, ufp: MockUFPFixture) -> None: """Test setup of unifiprotect entry with generic error.""" - ufp.api.get_bootstrap = AsyncMock(side_effect=NvrError) + ufp.api.update = AsyncMock(side_effect=NvrError) await hass.config_entries.async_setup(ufp.entry.entry_id) await hass.async_block_till_done() assert ufp.entry.state is ConfigEntryState.SETUP_RETRY - assert not ufp.api.update.called async def test_setup_failed_auth(hass: HomeAssistant, ufp: MockUFPFixture) -> None: """Test setup of unifiprotect entry with unauthorized error after multiple retries.""" - ufp.api.get_bootstrap = AsyncMock(side_effect=NotAuthorized) + ufp.api.update = AsyncMock(side_effect=NotAuthorized) await hass.config_entries.async_setup(ufp.entry.entry_id) assert ufp.entry.state is ConfigEntryState.SETUP_RETRY @@ -215,7 +214,6 @@ async def test_setup_failed_auth(hass: HomeAssistant, ufp: MockUFPFixture) -> No await hass.config_entries.async_reload(ufp.entry.entry_id) assert ufp.entry.state is ConfigEntryState.SETUP_ERROR - assert not ufp.api.update.called async def test_setup_starts_discovery( diff --git a/tests/components/unifiprotect/utils.py b/tests/components/unifiprotect/utils.py index ab3aefaa09d..21c01f77c5f 100644 --- a/tests/components/unifiprotect/utils.py +++ b/tests/components/unifiprotect/utils.py @@ -20,6 +20,7 @@ from uiprotect.data import ( ) from uiprotect.data.bootstrap import ProtectDeviceRef from uiprotect.test_util.anonymize import random_hex +from uiprotect.websocket import WebsocketState from homeassistant.const import Platform from homeassistant.core import HomeAssistant, split_entity_id @@ -38,6 +39,7 @@ class MockUFPFixture: entry: MockConfigEntry api: ProtectApiClient ws_subscription: Callable[[WSSubscriptionMessage], None] | None = None + ws_state_subscription: Callable[[WebsocketState], None] | None = None def ws_msg(self, msg: WSSubscriptionMessage) -> Any: """Emit WS message for testing.""" From 22467cc575c7d837993e39081817fe296401f649 Mon Sep 17 00:00:00 2001 From: tronikos Date: Sat, 22 Jun 2024 18:33:44 -0700 Subject: [PATCH 1044/1445] Avoid Opower time gaps (#117763) * Avoid time gaps * fix mypy * async_get_time_zone --- .../components/opower/coordinator.py | 94 +++++++++++-------- 1 file changed, 54 insertions(+), 40 deletions(-) diff --git a/homeassistant/components/opower/coordinator.py b/homeassistant/components/opower/coordinator.py index 94a56bb1922..d0795ae4e15 100644 --- a/homeassistant/components/opower/coordinator.py +++ b/homeassistant/components/opower/coordinator.py @@ -29,6 +29,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers import aiohttp_client from homeassistant.helpers.update_coordinator import DataUpdateCoordinator +from homeassistant.util import dt as dt_util from .const import CONF_TOTP_SECRET, CONF_UTILITY, DOMAIN @@ -113,13 +114,17 @@ class OpowerCoordinator(DataUpdateCoordinator[dict[str, Forecast]]): ) if not last_stat: _LOGGER.debug("Updating statistic for the first time") - cost_reads = await self._async_get_all_cost_reads(account) + cost_reads = await self._async_get_cost_reads( + account, self.api.utility.timezone() + ) cost_sum = 0.0 consumption_sum = 0.0 last_stats_time = None else: - cost_reads = await self._async_get_recent_cost_reads( - account, last_stat[cost_statistic_id][0]["start"] + cost_reads = await self._async_get_cost_reads( + account, + self.api.utility.timezone(), + last_stat[cost_statistic_id][0]["start"], ) if not cost_reads: _LOGGER.debug("No recent usage/cost data. Skipping update") @@ -187,59 +192,68 @@ class OpowerCoordinator(DataUpdateCoordinator[dict[str, Forecast]]): self.hass, consumption_metadata, consumption_statistics ) - async def _async_get_all_cost_reads(self, account: Account) -> list[CostRead]: - """Get all cost reads since account activation but at different resolutions depending on age. + async def _async_get_cost_reads( + self, account: Account, time_zone_str: str, start_time: float | None = None + ) -> list[CostRead]: + """Get cost reads. + If start_time is None, get cost reads since account activation, + otherwise since start_time - 30 days to allow corrections in data from utilities + + We read at different resolutions depending on age: - month resolution for all years (since account activation) - day resolution for past 3 years (if account's read resolution supports it) - hour resolution for past 2 months (if account's read resolution supports it) """ - cost_reads = [] - start = None - end = datetime.now() - if account.read_resolution != ReadResolution.BILLING: - end -= timedelta(days=3 * 365) - cost_reads += await self.api.async_get_cost_reads( + def _update_with_finer_cost_reads( + cost_reads: list[CostRead], finer_cost_reads: list[CostRead] + ) -> None: + for i, cost_read in enumerate(cost_reads): + for j, finer_cost_read in enumerate(finer_cost_reads): + if cost_read.start_time == finer_cost_read.start_time: + cost_reads[i:] = finer_cost_reads[j:] + return + if cost_read.end_time == finer_cost_read.start_time: + cost_reads[i + 1 :] = finer_cost_reads[j:] + return + if cost_read.end_time < finer_cost_read.start_time: + break + cost_reads += finer_cost_reads + + tz = await dt_util.async_get_time_zone(time_zone_str) + if start_time is None: + start = None + else: + start = datetime.fromtimestamp(start_time, tz=tz) - timedelta(days=30) + end = dt_util.now(tz) + cost_reads = await self.api.async_get_cost_reads( account, AggregateType.BILL, start, end ) if account.read_resolution == ReadResolution.BILLING: return cost_reads - start = end if not cost_reads else cost_reads[-1].end_time - end = datetime.now() - if account.read_resolution != ReadResolution.DAY: - end -= timedelta(days=2 * 30) - cost_reads += await self.api.async_get_cost_reads( + if start_time is None: + start = end - timedelta(days=3 * 365) + else: + if cost_reads: + start = cost_reads[0].start_time + assert start + start = max(start, end - timedelta(days=3 * 365)) + daily_cost_reads = await self.api.async_get_cost_reads( account, AggregateType.DAY, start, end ) + _update_with_finer_cost_reads(cost_reads, daily_cost_reads) if account.read_resolution == ReadResolution.DAY: return cost_reads - start = end if not cost_reads else cost_reads[-1].end_time - end = datetime.now() - cost_reads += await self.api.async_get_cost_reads( + if start_time is None: + start = end - timedelta(days=2 * 30) + else: + assert start + start = max(start, end - timedelta(days=2 * 30)) + hourly_cost_reads = await self.api.async_get_cost_reads( account, AggregateType.HOUR, start, end ) + _update_with_finer_cost_reads(cost_reads, hourly_cost_reads) return cost_reads - - async def _async_get_recent_cost_reads( - self, account: Account, last_stat_time: float - ) -> list[CostRead]: - """Get cost reads within the past 30 days to allow corrections in data from utilities.""" - if account.read_resolution in [ - ReadResolution.HOUR, - ReadResolution.HALF_HOUR, - ReadResolution.QUARTER_HOUR, - ]: - aggregate_type = AggregateType.HOUR - elif account.read_resolution == ReadResolution.DAY: - aggregate_type = AggregateType.DAY - else: - aggregate_type = AggregateType.BILL - return await self.api.async_get_cost_reads( - account, - aggregate_type, - datetime.fromtimestamp(last_stat_time) - timedelta(days=30), - datetime.now(), - ) From bc45dcbad3bad93cd2cc417b3f51fd679bd0f9d6 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 22 Jun 2024 21:51:09 -0400 Subject: [PATCH 1045/1445] Add template config_entry_attr function (#119899) * Template config_entry_attr function * Complete test coverage * Improve readability --- homeassistant/helpers/template.py | 21 +++++++++++++++ tests/helpers/test_template.py | 43 +++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+) diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 714a57336bd..cc619e25aed 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -1381,6 +1381,24 @@ def device_attr(hass: HomeAssistant, device_or_entity_id: str, attr_name: str) - return getattr(device, attr_name) +def config_entry_attr( + hass: HomeAssistant, config_entry_id_: str, attr_name: str +) -> Any: + """Get config entry specific attribute.""" + if not isinstance(config_entry_id_, str): + raise TemplateError("Must provide a config entry ID") + + if attr_name not in ("domain", "title", "state", "source", "disabled_by"): + raise TemplateError("Invalid config entry attribute") + + config_entry = hass.config_entries.async_get_entry(config_entry_id_) + + if config_entry is None: + return None + + return getattr(config_entry, attr_name) + + def is_device_attr( hass: HomeAssistant, device_or_entity_id: str, attr_name: str, attr_value: Any ) -> bool: @@ -2868,6 +2886,9 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment): self.globals["device_attr"] = hassfunction(device_attr) self.filters["device_attr"] = self.globals["device_attr"] + self.globals["config_entry_attr"] = hassfunction(config_entry_attr) + self.filters["config_entry_attr"] = self.globals["config_entry_attr"] + self.globals["is_device_attr"] = hassfunction(is_device_attr) self.tests["is_device_attr"] = hassfunction(is_device_attr, pass_eval_context) diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index 26e4f986592..3123c01f500 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -17,6 +17,7 @@ import orjson import pytest import voluptuous as vol +from homeassistant import config_entries from homeassistant.components import group from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, @@ -3990,6 +3991,48 @@ async def test_device_attr( assert info.rate_limit is None +async def test_config_entry_attr(hass: HomeAssistant) -> None: + """Test config entry attr.""" + info = { + "domain": "mock_light", + "title": "mock title", + "source": config_entries.SOURCE_BLUETOOTH, + "disabled_by": config_entries.ConfigEntryDisabler.USER, + } + config_entry = MockConfigEntry(**info) + config_entry.add_to_hass(hass) + + info["state"] = config_entries.ConfigEntryState.NOT_LOADED + + for key, value in info.items(): + tpl = template.Template( + "{{ config_entry_attr('" + config_entry.entry_id + "', '" + key + "') }}", + hass, + ) + assert tpl.async_render(parse_result=False) == str(value) + + for config_entry_id, key in ( + (config_entry.entry_id, "invalid_key"), + (56, "domain"), + ): + with pytest.raises(TemplateError): + template.Template( + "{{ config_entry_attr(" + + json.dumps(config_entry_id) + + ", '" + + key + + "') }}", + hass, + ).async_render() + + assert ( + template.Template( + "{{ config_entry_attr('invalid_id', 'domain') }}", hass + ).async_render(parse_result=False) + == "None" + ) + + async def test_issues(hass: HomeAssistant, issue_registry: ir.IssueRegistry) -> None: """Test issues function.""" # Test no issues From f0d5640f5ddde7a181938fe4e8944fa540b2b9b7 Mon Sep 17 00:00:00 2001 From: "Mr. Bubbles" Date: Sun, 23 Jun 2024 04:22:13 +0200 Subject: [PATCH 1046/1445] Bump pyloadapi to v1.2.0 (#120218) --- homeassistant/components/pyload/manifest.json | 2 +- homeassistant/components/pyload/sensor.py | 14 +++++++++----- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/pyload/conftest.py | 6 ++++-- 5 files changed, 16 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/pyload/manifest.json b/homeassistant/components/pyload/manifest.json index 90d750ff9b8..2a6e54fdf54 100644 --- a/homeassistant/components/pyload/manifest.json +++ b/homeassistant/components/pyload/manifest.json @@ -6,5 +6,5 @@ "integration_type": "service", "iot_class": "local_polling", "loggers": ["pyloadapi"], - "requirements": ["PyLoadAPI==1.1.0"] + "requirements": ["PyLoadAPI==1.2.0"] } diff --git a/homeassistant/components/pyload/sensor.py b/homeassistant/components/pyload/sensor.py index a005f848c37..a0420db819c 100644 --- a/homeassistant/components/pyload/sensor.py +++ b/homeassistant/components/pyload/sensor.py @@ -6,11 +6,15 @@ from datetime import timedelta from enum import StrEnum import logging from time import monotonic -from typing import Any from aiohttp import CookieJar -from pyloadapi.api import PyLoadAPI -from pyloadapi.exceptions import CannotConnect, InvalidAuth, ParserError +from pyloadapi import ( + CannotConnect, + InvalidAuth, + ParserError, + PyLoadAPI, + StatusServerResponse, +) import voluptuous as vol from homeassistant.components.sensor import ( @@ -132,7 +136,7 @@ class PyLoadSensor(SensorEntity): self.api = api self.entity_description = entity_description self._attr_available = False - self.data: dict[str, Any] = {} + self.data: StatusServerResponse async def async_update(self) -> None: """Update state of sensor.""" @@ -167,7 +171,7 @@ class PyLoadSensor(SensorEntity): self._attr_available = False return else: - self.data = status.to_dict() + self.data = status _LOGGER.debug( "Finished fetching pyload data in %.3f seconds", monotonic() - start, diff --git a/requirements_all.txt b/requirements_all.txt index 2343fa9bd4e..a1ce60412ba 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -60,7 +60,7 @@ PyFlume==0.6.5 PyFronius==0.7.3 # homeassistant.components.pyload -PyLoadAPI==1.1.0 +PyLoadAPI==1.2.0 # homeassistant.components.mvglive PyMVGLive==1.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fdd67cf9e29..7e1316c8b74 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -51,7 +51,7 @@ PyFlume==0.6.5 PyFronius==0.7.3 # homeassistant.components.pyload -PyLoadAPI==1.1.0 +PyLoadAPI==1.2.0 # homeassistant.components.met_eireann PyMetEireann==2021.8.0 diff --git a/tests/components/pyload/conftest.py b/tests/components/pyload/conftest.py index 67694bcb4b9..53e86639c4a 100644 --- a/tests/components/pyload/conftest.py +++ b/tests/components/pyload/conftest.py @@ -47,7 +47,7 @@ def mock_pyloadapi() -> Generator[AsyncMock, None, None]: ): client = mock_client.return_value client.username = "username" - client.login.return_value = LoginResponse.from_dict( + client.login.return_value = LoginResponse( { "_permanent": True, "authenticated": True, @@ -59,7 +59,8 @@ def mock_pyloadapi() -> Generator[AsyncMock, None, None]: "_flashes": [["message", "Logged in successfully"]], } ) - client.get_status.return_value = StatusServerResponse.from_dict( + + client.get_status.return_value = StatusServerResponse( { "pause": False, "active": 1, @@ -71,5 +72,6 @@ def mock_pyloadapi() -> Generator[AsyncMock, None, None]: "captcha": False, } ) + client.free_space.return_value = 99999999999 yield client From f257fcb0d1f210b50a1a13ca6c565546790a76cd Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk <11290930+bouwew@users.noreply.github.com> Date: Sun, 23 Jun 2024 10:58:08 +0200 Subject: [PATCH 1047/1445] Bump plugwise to v0.38.3 (#120152) --- homeassistant/components/plugwise/climate.py | 2 +- homeassistant/components/plugwise/manifest.json | 2 +- homeassistant/components/plugwise/number.py | 14 +++++++------- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../adam_multiple_devices_per_zone/all_data.json | 1 + .../fixtures/anna_heatpump_heating/all_data.json | 1 + .../plugwise/fixtures/m_adam_cooling/all_data.json | 3 ++- .../plugwise/fixtures/m_adam_heating/all_data.json | 3 ++- .../plugwise/fixtures/m_adam_jip/all_data.json | 11 ++--------- .../fixtures/m_anna_heatpump_cooling/all_data.json | 1 + .../fixtures/m_anna_heatpump_idle/all_data.json | 1 + .../fixtures/p1v4_442_single/all_data.json | 1 + .../fixtures/p1v4_442_triple/all_data.json | 1 + .../plugwise/snapshots/test_diagnostics.ambr | 1 + tests/components/plugwise/test_climate.py | 2 +- tests/components/plugwise/test_number.py | 14 +++++++------- 17 files changed, 32 insertions(+), 30 deletions(-) diff --git a/homeassistant/components/plugwise/climate.py b/homeassistant/components/plugwise/climate.py index 006cfbe87da..29d44fe8159 100644 --- a/homeassistant/components/plugwise/climate.py +++ b/homeassistant/components/plugwise/climate.py @@ -155,7 +155,7 @@ class PlugwiseClimateEntity(PlugwiseEntity, ClimateEntity): if "regulation_modes" in self.gateway_data: hvac_modes.append(HVACMode.OFF) - if self.device["available_schedules"] != ["None"]: + if "available_schedules" in self.device: hvac_modes.append(HVACMode.AUTO) if self.cdr_gateway["cooling_present"]: diff --git a/homeassistant/components/plugwise/manifest.json b/homeassistant/components/plugwise/manifest.json index b1937ee219d..10faf75d0f1 100644 --- a/homeassistant/components/plugwise/manifest.json +++ b/homeassistant/components/plugwise/manifest.json @@ -7,6 +7,6 @@ "integration_type": "hub", "iot_class": "local_polling", "loggers": ["plugwise"], - "requirements": ["plugwise==0.37.4.1"], + "requirements": ["plugwise==0.38.3"], "zeroconf": ["_plugwise._tcp.local."] } diff --git a/homeassistant/components/plugwise/number.py b/homeassistant/components/plugwise/number.py index f00b9e38876..c84ca2cf5c7 100644 --- a/homeassistant/components/plugwise/number.py +++ b/homeassistant/components/plugwise/number.py @@ -35,8 +35,8 @@ NUMBER_TYPES = ( PlugwiseNumberEntityDescription( key="maximum_boiler_temperature", translation_key="maximum_boiler_temperature", - command=lambda api, number, dev_id, value: api.set_number_setpoint( - number, dev_id, value + command=lambda api, dev_id, number, value: api.set_number( + dev_id, number, value ), device_class=NumberDeviceClass.TEMPERATURE, entity_category=EntityCategory.CONFIG, @@ -45,8 +45,8 @@ NUMBER_TYPES = ( PlugwiseNumberEntityDescription( key="max_dhw_temperature", translation_key="max_dhw_temperature", - command=lambda api, number, dev_id, value: api.set_number_setpoint( - number, dev_id, value + command=lambda api, dev_id, number, value: api.set_number( + dev_id, number, value ), device_class=NumberDeviceClass.TEMPERATURE, entity_category=EntityCategory.CONFIG, @@ -55,8 +55,8 @@ NUMBER_TYPES = ( PlugwiseNumberEntityDescription( key="temperature_offset", translation_key="temperature_offset", - command=lambda api, number, dev_id, value: api.set_temperature_offset( - number, dev_id, value + command=lambda api, dev_id, number, value: api.set_temperature_offset( + dev_id, value ), device_class=NumberDeviceClass.TEMPERATURE, entity_category=EntityCategory.CONFIG, @@ -124,6 +124,6 @@ class PlugwiseNumberEntity(PlugwiseEntity, NumberEntity): async def async_set_native_value(self, value: float) -> None: """Change to the new setpoint value.""" await self.entity_description.command( - self.coordinator.api, self.entity_description.key, self.device_id, value + self.coordinator.api, self.device_id, self.entity_description.key, value ) await self.coordinator.async_request_refresh() diff --git a/requirements_all.txt b/requirements_all.txt index a1ce60412ba..d65267ea5f6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1575,7 +1575,7 @@ plexauth==0.0.6 plexwebsocket==0.0.14 # homeassistant.components.plugwise -plugwise==0.37.4.1 +plugwise==0.38.3 # homeassistant.components.plum_lightpad plumlightpad==0.0.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7e1316c8b74..02ec3650970 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1258,7 +1258,7 @@ plexauth==0.0.6 plexwebsocket==0.0.14 # homeassistant.components.plugwise -plugwise==0.37.4.1 +plugwise==0.38.3 # homeassistant.components.plum_lightpad plumlightpad==0.0.11 diff --git a/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/all_data.json b/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/all_data.json index 47c8e4dceb0..9c17df5072d 100644 --- a/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/all_data.json +++ b/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/all_data.json @@ -479,6 +479,7 @@ "warning": "Node Plug (with MAC address 000D6F000D13CB01, in room 'n.a.') has been unreachable since 23:03 2020-01-18. Please check the connection and restart the device." } }, + "reboot": true, "smile_name": "Adam" } } diff --git a/tests/components/plugwise/fixtures/anna_heatpump_heating/all_data.json b/tests/components/plugwise/fixtures/anna_heatpump_heating/all_data.json index d496edb4149..5088281404a 100644 --- a/tests/components/plugwise/fixtures/anna_heatpump_heating/all_data.json +++ b/tests/components/plugwise/fixtures/anna_heatpump_heating/all_data.json @@ -99,6 +99,7 @@ "heater_id": "1cbf783bb11e4a7c8a6843dee3a86927", "item_count": 66, "notifications": {}, + "reboot": true, "smile_name": "Smile Anna" } } diff --git a/tests/components/plugwise/fixtures/m_adam_cooling/all_data.json b/tests/components/plugwise/fixtures/m_adam_cooling/all_data.json index 6cd3241a637..759d0094dbb 100644 --- a/tests/components/plugwise/fixtures/m_adam_cooling/all_data.json +++ b/tests/components/plugwise/fixtures/m_adam_cooling/all_data.json @@ -66,7 +66,7 @@ "model": "ThermoTouch", "name": "Anna", "preset_modes": ["no_frost", "asleep", "vacation", "home", "away"], - "select_schedule": "None", + "select_schedule": "off", "sensors": { "setpoint": 23.5, "temperature": 25.8 @@ -165,6 +165,7 @@ "heater_id": "056ee145a816487eaa69243c3280f8bf", "item_count": 147, "notifications": {}, + "reboot": true, "smile_name": "Adam" } } diff --git a/tests/components/plugwise/fixtures/m_adam_heating/all_data.json b/tests/components/plugwise/fixtures/m_adam_heating/all_data.json index 0e9df1a5079..e2c23df42d6 100644 --- a/tests/components/plugwise/fixtures/m_adam_heating/all_data.json +++ b/tests/components/plugwise/fixtures/m_adam_heating/all_data.json @@ -71,7 +71,7 @@ "model": "ThermoTouch", "name": "Anna", "preset_modes": ["no_frost", "asleep", "vacation", "home", "away"], - "select_schedule": "None", + "select_schedule": "off", "sensors": { "setpoint": 20.0, "temperature": 19.1 @@ -164,6 +164,7 @@ "heater_id": "056ee145a816487eaa69243c3280f8bf", "item_count": 147, "notifications": {}, + "reboot": true, "smile_name": "Adam" } } diff --git a/tests/components/plugwise/fixtures/m_adam_jip/all_data.json b/tests/components/plugwise/fixtures/m_adam_jip/all_data.json index 378a5e0a760..7888d777804 100644 --- a/tests/components/plugwise/fixtures/m_adam_jip/all_data.json +++ b/tests/components/plugwise/fixtures/m_adam_jip/all_data.json @@ -3,7 +3,6 @@ "1346fbd8498d4dbcab7e18d51b771f3d": { "active_preset": "no_frost", "available": true, - "available_schedules": ["None"], "control_state": "off", "dev_class": "zone_thermostat", "firmware": "2016-10-27T02:00:00+02:00", @@ -13,7 +12,6 @@ "model": "Lisa", "name": "Slaapkamer", "preset_modes": ["home", "asleep", "away", "vacation", "no_frost"], - "select_schedule": "None", "sensors": { "battery": 92, "setpoint": 13.0, @@ -99,7 +97,6 @@ "6f3e9d7084214c21b9dfa46f6eeb8700": { "active_preset": "home", "available": true, - "available_schedules": ["None"], "control_state": "off", "dev_class": "zone_thermostat", "firmware": "2016-10-27T02:00:00+02:00", @@ -109,7 +106,6 @@ "model": "Lisa", "name": "Kinderkamer", "preset_modes": ["home", "asleep", "away", "vacation", "no_frost"], - "select_schedule": "None", "sensors": { "battery": 79, "setpoint": 13.0, @@ -156,7 +152,6 @@ "a6abc6a129ee499c88a4d420cc413b47": { "active_preset": "home", "available": true, - "available_schedules": ["None"], "control_state": "off", "dev_class": "zone_thermostat", "firmware": "2016-10-27T02:00:00+02:00", @@ -166,7 +161,6 @@ "model": "Lisa", "name": "Logeerkamer", "preset_modes": ["home", "asleep", "away", "vacation", "no_frost"], - "select_schedule": "None", "sensors": { "battery": 80, "setpoint": 13.0, @@ -269,7 +263,6 @@ "f61f1a2535f54f52ad006a3d18e459ca": { "active_preset": "home", "available": true, - "available_schedules": ["None"], "control_state": "off", "dev_class": "zone_thermometer", "firmware": "2020-09-01T02:00:00+02:00", @@ -279,7 +272,6 @@ "model": "Jip", "name": "Woonkamer", "preset_modes": ["home", "asleep", "away", "vacation", "no_frost"], - "select_schedule": "None", "sensors": { "battery": 100, "humidity": 56.2, @@ -306,8 +298,9 @@ "cooling_present": false, "gateway_id": "b5c2386c6f6342669e50fe49dd05b188", "heater_id": "e4684553153b44afbef2200885f379dc", - "item_count": 221, + "item_count": 213, "notifications": {}, + "reboot": true, "smile_name": "Adam" } } diff --git a/tests/components/plugwise/fixtures/m_anna_heatpump_cooling/all_data.json b/tests/components/plugwise/fixtures/m_anna_heatpump_cooling/all_data.json index ef7af8a362b..cb30b919797 100644 --- a/tests/components/plugwise/fixtures/m_anna_heatpump_cooling/all_data.json +++ b/tests/components/plugwise/fixtures/m_anna_heatpump_cooling/all_data.json @@ -99,6 +99,7 @@ "heater_id": "1cbf783bb11e4a7c8a6843dee3a86927", "item_count": 66, "notifications": {}, + "reboot": true, "smile_name": "Smile Anna" } } diff --git a/tests/components/plugwise/fixtures/m_anna_heatpump_idle/all_data.json b/tests/components/plugwise/fixtures/m_anna_heatpump_idle/all_data.json index 8f2e6a75f3f..660f6b5a76b 100644 --- a/tests/components/plugwise/fixtures/m_anna_heatpump_idle/all_data.json +++ b/tests/components/plugwise/fixtures/m_anna_heatpump_idle/all_data.json @@ -99,6 +99,7 @@ "heater_id": "1cbf783bb11e4a7c8a6843dee3a86927", "item_count": 66, "notifications": {}, + "reboot": true, "smile_name": "Smile Anna" } } diff --git a/tests/components/plugwise/fixtures/p1v4_442_single/all_data.json b/tests/components/plugwise/fixtures/p1v4_442_single/all_data.json index 318035a5d2c..7f152779252 100644 --- a/tests/components/plugwise/fixtures/p1v4_442_single/all_data.json +++ b/tests/components/plugwise/fixtures/p1v4_442_single/all_data.json @@ -44,6 +44,7 @@ "gateway_id": "a455b61e52394b2db5081ce025a430f3", "item_count": 31, "notifications": {}, + "reboot": true, "smile_name": "Smile P1" } } diff --git a/tests/components/plugwise/fixtures/p1v4_442_triple/all_data.json b/tests/components/plugwise/fixtures/p1v4_442_triple/all_data.json index ecda8049163..582c883a3a7 100644 --- a/tests/components/plugwise/fixtures/p1v4_442_triple/all_data.json +++ b/tests/components/plugwise/fixtures/p1v4_442_triple/all_data.json @@ -57,6 +57,7 @@ "warning": "The Smile P1 is not connected to a smart meter." } }, + "reboot": true, "smile_name": "Smile P1" } } diff --git a/tests/components/plugwise/snapshots/test_diagnostics.ambr b/tests/components/plugwise/snapshots/test_diagnostics.ambr index 0fa3df4e660..44f4023d014 100644 --- a/tests/components/plugwise/snapshots/test_diagnostics.ambr +++ b/tests/components/plugwise/snapshots/test_diagnostics.ambr @@ -511,6 +511,7 @@ 'warning': "Node Plug (with MAC address 000D6F000D13CB01, in room 'n.a.') has been unreachable since 23:03 2020-01-18. Please check the connection and restart the device.", }), }), + 'reboot': True, 'smile_name': 'Adam', }), }) diff --git a/tests/components/plugwise/test_climate.py b/tests/components/plugwise/test_climate.py index 5cdc468a957..b3f42031ed8 100644 --- a/tests/components/plugwise/test_climate.py +++ b/tests/components/plugwise/test_climate.py @@ -395,7 +395,7 @@ async def test_anna_climate_entity_climate_changes( "c784ee9fdab44e1395b8dee7d7a497d5", "off" ) data = mock_smile_anna.async_update.return_value - data.devices["3cb70739631c4d17a86b8b12e8a5161b"]["available_schedules"] = ["None"] + data.devices["3cb70739631c4d17a86b8b12e8a5161b"].pop("available_schedules") with patch(HA_PLUGWISE_SMILE_ASYNC_UPDATE, return_value=data): async_fire_time_changed(hass, utcnow() + timedelta(minutes=1)) await hass.async_block_till_done() diff --git a/tests/components/plugwise/test_number.py b/tests/components/plugwise/test_number.py index 6fa65b3e65a..8d49d07b9fb 100644 --- a/tests/components/plugwise/test_number.py +++ b/tests/components/plugwise/test_number.py @@ -36,9 +36,9 @@ async def test_anna_max_boiler_temp_change( blocking=True, ) - assert mock_smile_anna.set_number_setpoint.call_count == 1 - mock_smile_anna.set_number_setpoint.assert_called_with( - "maximum_boiler_temperature", "1cbf783bb11e4a7c8a6843dee3a86927", 65.0 + assert mock_smile_anna.set_number.call_count == 1 + mock_smile_anna.set_number.assert_called_with( + "1cbf783bb11e4a7c8a6843dee3a86927", "maximum_boiler_temperature", 65.0 ) @@ -65,9 +65,9 @@ async def test_adam_dhw_setpoint_change( blocking=True, ) - assert mock_smile_adam_2.set_number_setpoint.call_count == 1 - mock_smile_adam_2.set_number_setpoint.assert_called_with( - "max_dhw_temperature", "056ee145a816487eaa69243c3280f8bf", 55.0 + assert mock_smile_adam_2.set_number.call_count == 1 + mock_smile_adam_2.set_number.assert_called_with( + "056ee145a816487eaa69243c3280f8bf", "max_dhw_temperature", 55.0 ) @@ -99,5 +99,5 @@ async def test_adam_temperature_offset_change( assert mock_smile_adam.set_temperature_offset.call_count == 1 mock_smile_adam.set_temperature_offset.assert_called_with( - "temperature_offset", "6a3bf693d05e48e0b460c815a4fdd09d", 1.0 + "6a3bf693d05e48e0b460c815a4fdd09d", 1.0 ) From 28fb361c64d8466ddd61ea60b27d3ae14b4104e4 Mon Sep 17 00:00:00 2001 From: "Mr. Bubbles" Date: Sun, 23 Jun 2024 12:34:32 +0200 Subject: [PATCH 1048/1445] Add config flow to pyLoad integration (#120135) * Add config flow to pyLoad integration * address issues * remove suggested values * Fix exception * abort import flow on error * readd repair issues on error * fix ruff * changes * changes * exception hints --- homeassistant/components/pyload/__init__.py | 71 +++- .../components/pyload/config_flow.py | 120 ++++++ homeassistant/components/pyload/const.py | 2 + homeassistant/components/pyload/manifest.json | 1 + homeassistant/components/pyload/sensor.py | 94 +++-- homeassistant/components/pyload/strings.json | 44 ++ homeassistant/generated/config_flows.py | 1 + homeassistant/generated/integrations.json | 2 +- tests/components/pyload/conftest.py | 61 ++- .../pyload/snapshots/test_sensor.ambr | 378 ++++++++++++++++++ tests/components/pyload/test_config_flow.py | 166 ++++++++ tests/components/pyload/test_init.py | 63 +++ tests/components/pyload/test_sensor.py | 150 ++++--- 13 files changed, 1041 insertions(+), 112 deletions(-) create mode 100644 homeassistant/components/pyload/config_flow.py create mode 100644 homeassistant/components/pyload/strings.json create mode 100644 tests/components/pyload/test_config_flow.py create mode 100644 tests/components/pyload/test_init.py diff --git a/homeassistant/components/pyload/__init__.py b/homeassistant/components/pyload/__init__.py index 19103572e0b..a2e105e6454 100644 --- a/homeassistant/components/pyload/__init__.py +++ b/homeassistant/components/pyload/__init__.py @@ -1 +1,70 @@ -"""The pyload component.""" +"""The pyLoad integration.""" + +from __future__ import annotations + +from aiohttp import CookieJar +from pyloadapi.api import PyLoadAPI +from pyloadapi.exceptions import CannotConnect, InvalidAuth, ParserError + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + CONF_HOST, + CONF_PASSWORD, + CONF_PORT, + CONF_SSL, + CONF_USERNAME, + CONF_VERIFY_SSL, + Platform, +) +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryError, ConfigEntryNotReady +from homeassistant.helpers.aiohttp_client import async_create_clientsession + +PLATFORMS: list[Platform] = [Platform.SENSOR] + +type PyLoadConfigEntry = ConfigEntry[PyLoadAPI] + + +async def async_setup_entry(hass: HomeAssistant, entry: PyLoadConfigEntry) -> bool: + """Set up pyLoad from a config entry.""" + + url = ( + f"{"https" if entry.data[CONF_SSL] else "http"}://" + f"{entry.data[CONF_HOST]}:{entry.data[CONF_PORT]}/" + ) + + session = async_create_clientsession( + hass, + verify_ssl=entry.data[CONF_VERIFY_SSL], + cookie_jar=CookieJar(unsafe=True), + ) + pyloadapi = PyLoadAPI( + session, + api_url=url, + username=entry.data[CONF_USERNAME], + password=entry.data[CONF_PASSWORD], + ) + + try: + await pyloadapi.login() + except CannotConnect as e: + raise ConfigEntryNotReady( + "Unable to connect and retrieve data from pyLoad API" + ) from e + except ParserError as e: + raise ConfigEntryNotReady("Unable to parse data from pyLoad API") from e + except InvalidAuth as e: + raise ConfigEntryError( + f"Authentication failed for {entry.data[CONF_USERNAME]}, check your login credentials" + ) from e + + entry.runtime_data = pyloadapi + + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: PyLoadConfigEntry) -> bool: + """Unload a config entry.""" + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/pyload/config_flow.py b/homeassistant/components/pyload/config_flow.py new file mode 100644 index 00000000000..7ebc4a501d4 --- /dev/null +++ b/homeassistant/components/pyload/config_flow.py @@ -0,0 +1,120 @@ +"""Config flow for pyLoad integration.""" + +from __future__ import annotations + +import logging +from typing import Any + +from aiohttp import CookieJar +from pyloadapi.api import PyLoadAPI +from pyloadapi.exceptions import CannotConnect, InvalidAuth, ParserError +import voluptuous as vol + +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult +from homeassistant.const import ( + CONF_HOST, + CONF_NAME, + CONF_PASSWORD, + CONF_PORT, + CONF_SSL, + CONF_USERNAME, + CONF_VERIFY_SSL, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.aiohttp_client import async_create_clientsession +import homeassistant.helpers.config_validation as cv + +from .const import DEFAULT_HOST, DEFAULT_NAME, DEFAULT_PORT, DOMAIN + +_LOGGER = logging.getLogger(__name__) + +STEP_USER_DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_HOST): str, + vol.Required(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Required(CONF_SSL, default=False): cv.boolean, + vol.Required(CONF_VERIFY_SSL, default=True): bool, + vol.Required(CONF_USERNAME): str, + vol.Required(CONF_PASSWORD): str, + } +) + + +async def validate_input(hass: HomeAssistant, user_input: dict[str, Any]) -> None: + """Validate the user input and try to connect to PyLoad.""" + + session = async_create_clientsession( + hass, + user_input[CONF_VERIFY_SSL], + cookie_jar=CookieJar(unsafe=True), + ) + + url = ( + f"{"https" if user_input[CONF_SSL] else "http"}://" + f"{user_input[CONF_HOST]}:{user_input[CONF_PORT]}/" + ) + pyload = PyLoadAPI( + session, + api_url=url, + username=user_input[CONF_USERNAME], + password=user_input[CONF_PASSWORD], + ) + + await pyload.login() + + +class PyLoadConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a config flow for pyLoad.""" + + VERSION = 1 + # store values from yaml import so we can use them as + # suggested values when the configuration step is resumed + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle the initial step.""" + errors: dict[str, str] = {} + if user_input is not None: + self._async_abort_entries_match( + {CONF_HOST: user_input[CONF_HOST], CONF_PORT: user_input[CONF_PORT]} + ) + try: + await validate_input(self.hass, user_input) + except (CannotConnect, ParserError): + errors["base"] = "cannot_connect" + except InvalidAuth: + errors["base"] = "invalid_auth" + except Exception: + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + title = user_input.pop(CONF_NAME, DEFAULT_NAME) + return self.async_create_entry(title=title, data=user_input) + + return self.async_show_form( + step_id="user", + data_schema=self.add_suggested_values_to_schema( + STEP_USER_DATA_SCHEMA, user_input + ), + errors=errors, + ) + + async def async_step_import(self, import_info: dict[str, Any]) -> ConfigFlowResult: + """Import config from yaml.""" + + config = { + CONF_NAME: import_info.get(CONF_NAME), + CONF_HOST: import_info.get(CONF_HOST, DEFAULT_HOST), + CONF_PASSWORD: import_info.get(CONF_PASSWORD, ""), + CONF_PORT: import_info.get(CONF_PORT, DEFAULT_PORT), + CONF_SSL: import_info.get(CONF_SSL, False), + CONF_USERNAME: import_info.get(CONF_USERNAME, ""), + CONF_VERIFY_SSL: False, + } + + result = await self.async_step_user(config) + + if errors := result.get("errors"): + return self.async_abort(reason=errors["base"]) + return result diff --git a/homeassistant/components/pyload/const.py b/homeassistant/components/pyload/const.py index a7d155d8b33..8ee1c05696f 100644 --- a/homeassistant/components/pyload/const.py +++ b/homeassistant/components/pyload/const.py @@ -5,3 +5,5 @@ DOMAIN = "pyload" DEFAULT_HOST = "localhost" DEFAULT_NAME = "pyLoad" DEFAULT_PORT = 8000 + +ISSUE_PLACEHOLDER = {"url": "/config/integrations/dashboard/add?domain=pyload"} diff --git a/homeassistant/components/pyload/manifest.json b/homeassistant/components/pyload/manifest.json index 2a6e54fdf54..95e73118c42 100644 --- a/homeassistant/components/pyload/manifest.json +++ b/homeassistant/components/pyload/manifest.json @@ -2,6 +2,7 @@ "domain": "pyload", "name": "pyLoad", "codeowners": ["@tr4nt0r"], + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/pyload", "integration_type": "service", "iot_class": "local_polling", diff --git a/homeassistant/components/pyload/sensor.py b/homeassistant/components/pyload/sensor.py index a0420db819c..131fec68609 100644 --- a/homeassistant/components/pyload/sensor.py +++ b/homeassistant/components/pyload/sensor.py @@ -7,7 +7,6 @@ from enum import StrEnum import logging from time import monotonic -from aiohttp import CookieJar from pyloadapi import ( CannotConnect, InvalidAuth, @@ -23,6 +22,7 @@ from homeassistant.components.sensor import ( SensorEntity, SensorEntityDescription, ) +from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import ( CONF_HOST, CONF_MONITORED_VARIABLES, @@ -33,14 +33,15 @@ from homeassistant.const import ( CONF_USERNAME, UnitOfDataRate, ) -from homeassistant.core import HomeAssistant -from homeassistant.exceptions import PlatformNotReady -from homeassistant.helpers.aiohttp_client import async_create_clientsession +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant +from homeassistant.data_entry_flow import FlowResultType import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType -from .const import DEFAULT_HOST, DEFAULT_NAME, DEFAULT_PORT +from . import PyLoadConfigEntry +from .const import DEFAULT_HOST, DEFAULT_NAME, DEFAULT_PORT, DOMAIN, ISSUE_PLACEHOLDER _LOGGER = logging.getLogger(__name__) @@ -82,41 +83,63 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( async def async_setup_platform( hass: HomeAssistant, config: ConfigType, - async_add_entities: AddEntitiesCallback, + add_entities: AddEntitiesCallback, discovery_info: DiscoveryInfoType | None = None, ) -> None: - """Set up the pyLoad sensors.""" - host = config[CONF_HOST] - port = config[CONF_PORT] - protocol = "https" if config[CONF_SSL] else "http" - name = config[CONF_NAME] - username = config.get(CONF_USERNAME) - password = config.get(CONF_PASSWORD) - url = f"{protocol}://{host}:{port}/" + """Import config from yaml.""" - session = async_create_clientsession( - hass, - verify_ssl=False, - cookie_jar=CookieJar(unsafe=True), + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=config ) - pyloadapi = PyLoadAPI(session, api_url=url, username=username, password=password) - try: - await pyloadapi.login() - except CannotConnect as conn_err: - raise PlatformNotReady( - "Unable to connect and retrieve data from pyLoad API" - ) from conn_err - except ParserError as e: - raise PlatformNotReady("Unable to parse data from pyLoad API") from e - except InvalidAuth as e: - raise PlatformNotReady( - f"Authentication failed for {config[CONF_USERNAME]}, check your login credentials" - ) from e + _LOGGER.debug(result) + if ( + result.get("type") == FlowResultType.CREATE_ENTRY + or result.get("reason") == "already_configured" + ): + async_create_issue( + hass, + HOMEASSISTANT_DOMAIN, + f"deprecated_yaml_{DOMAIN}", + is_fixable=False, + issue_domain=DOMAIN, + breaks_in_ha_version="2025.2.0", + severity=IssueSeverity.WARNING, + translation_key="deprecated_yaml", + translation_placeholders={ + "domain": DOMAIN, + "integration_title": "pyLoad", + }, + ) + elif error := result.get("reason"): + async_create_issue( + hass, + DOMAIN, + f"deprecated_yaml_import_issue_{error}", + breaks_in_ha_version="2025.2.0", + is_fixable=False, + issue_domain=DOMAIN, + severity=IssueSeverity.WARNING, + translation_key=f"deprecated_yaml_import_issue_{error}", + translation_placeholders=ISSUE_PLACEHOLDER, + ) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: PyLoadConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the pyLoad sensors.""" + + pyloadapi = entry.runtime_data async_add_entities( ( PyLoadSensor( - api=pyloadapi, entity_description=description, client_name=name + api=pyloadapi, + entity_description=description, + client_name=entry.title, + entry_id=entry.entry_id, ) for description in SENSOR_DESCRIPTIONS ), @@ -128,12 +151,17 @@ class PyLoadSensor(SensorEntity): """Representation of a pyLoad sensor.""" def __init__( - self, api: PyLoadAPI, entity_description: SensorEntityDescription, client_name + self, + api: PyLoadAPI, + entity_description: SensorEntityDescription, + client_name: str, + entry_id: str, ) -> None: """Initialize a new pyLoad sensor.""" self._attr_name = f"{client_name} {entity_description.name}" self.type = entity_description.key self.api = api + self._attr_unique_id = f"{entry_id}_{entity_description.key}" self.entity_description = entity_description self._attr_available = False self.data: StatusServerResponse diff --git a/homeassistant/components/pyload/strings.json b/homeassistant/components/pyload/strings.json new file mode 100644 index 00000000000..30e2366eb86 --- /dev/null +++ b/homeassistant/components/pyload/strings.json @@ -0,0 +1,44 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "[%key:common::config_flow::data::name%]", + "host": "[%key:common::config_flow::data::host%]", + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]", + "ssl": "[%key:common::config_flow::data::ssl%]", + "verify_ssl": "[%key:common::config_flow::data::verify_ssl%]", + "port": "[%key:common::config_flow::data::port%]" + }, + "data_description": { + "name": "The name to use for your pyLoad instance in Home Assistant", + "host": "The hostname or IP address of the device running your pyLoad instance.", + "port": "pyLoad uses port 8000 by default." + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + }, + "issues": { + "deprecated_yaml_import_issue_cannot_connect": { + "title": "The pyLoad YAML configuration import failed", + "description": "Configuring pyLoad using YAML is being removed but there was a connection error when trying to import the YAML configuration.\n\nPlease verify that you have a stable internet connection and restart Home Assistant to try again or remove the pyLoad YAML configuration from your configuration.yaml file and continue to [set up the integration]({url}) manually." + }, + "deprecated_yaml_import_issue_invalid_auth": { + "title": "The pyLoad YAML configuration import failed", + "description": "Configuring pyLoad using YAML is being removed but there was an authentication error when trying to import the YAML configuration.\n\nCorrect the YAML configuration and restart Home Assistant to try again or remove the pyLoad YAML configuration from your configuration.yaml file and continue to [set up the integration]({url}) manually." + }, + "deprecated_yaml_import_issue_unknown": { + "title": "The pyLoad YAML configuration import failed", + "description": "Configuring pyLoad using YAML is being removed but there was an unknown error when trying to import the YAML configuration.\n\nEnsure the YAML configuration is correct and restart Home Assistant to try again or remove the pyLoad YAML configuration from your configuration.yaml file and continue to [set up the integration]({url}) manually." + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index cf6e2bb4fa7..631a5b6abb4 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -435,6 +435,7 @@ FLOWS = { "pushover", "pvoutput", "pvpc_hourly_pricing", + "pyload", "qbittorrent", "qingping", "qnap", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index bbf96e4461b..eef78c212c8 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -4781,7 +4781,7 @@ "pyload": { "name": "pyLoad", "integration_type": "service", - "config_flow": false, + "config_flow": true, "iot_class": "local_polling" }, "python_script": { diff --git a/tests/components/pyload/conftest.py b/tests/components/pyload/conftest.py index 53e86639c4a..0dafb9af4df 100644 --- a/tests/components/pyload/conftest.py +++ b/tests/components/pyload/conftest.py @@ -6,6 +6,7 @@ from unittest.mock import AsyncMock, patch from pyloadapi.types import LoginResponse, StatusServerResponse import pytest +from homeassistant.components.pyload.const import DEFAULT_NAME, DOMAIN from homeassistant.const import ( CONF_HOST, CONF_MONITORED_VARIABLES, @@ -15,25 +16,46 @@ from homeassistant.const import ( CONF_PORT, CONF_SSL, CONF_USERNAME, + CONF_VERIFY_SSL, ) from homeassistant.helpers.typing import ConfigType +from tests.common import MockConfigEntry + +USER_INPUT = { + CONF_HOST: "pyload.local", + CONF_PASSWORD: "test-password", + CONF_PORT: 8000, + CONF_SSL: True, + CONF_USERNAME: "test-username", + CONF_VERIFY_SSL: False, +} + +YAML_INPUT = { + CONF_HOST: "pyload.local", + CONF_MONITORED_VARIABLES: ["speed"], + CONF_NAME: "test-name", + CONF_PASSWORD: "test-password", + CONF_PLATFORM: "pyload", + CONF_PORT: 8000, + CONF_SSL: True, + CONF_USERNAME: "test-username", +} + + +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock]: + """Override async_setup_entry.""" + with patch( + "homeassistant.components.pyload.async_setup_entry", return_value=True + ) as mock_setup_entry: + yield mock_setup_entry + @pytest.fixture def pyload_config() -> ConfigType: """Mock pyload configuration entry.""" - return { - "sensor": { - CONF_PLATFORM: "pyload", - CONF_HOST: "localhost", - CONF_PORT: 8000, - CONF_USERNAME: "username", - CONF_PASSWORD: "password", - CONF_SSL: True, - CONF_MONITORED_VARIABLES: ["speed"], - CONF_NAME: "pyload", - } - } + return {"sensor": YAML_INPUT} @pytest.fixture @@ -41,12 +63,15 @@ def mock_pyloadapi() -> Generator[AsyncMock, None, None]: """Mock PyLoadAPI.""" with ( patch( - "homeassistant.components.pyload.sensor.PyLoadAPI", - autospec=True, + "homeassistant.components.pyload.PyLoadAPI", autospec=True ) as mock_client, + patch("homeassistant.components.pyload.config_flow.PyLoadAPI", new=mock_client), + patch("homeassistant.components.pyload.sensor.PyLoadAPI", new=mock_client), ): client = mock_client.return_value client.username = "username" + client.api_url = "https://pyload.local:8000/" + client.login.return_value = LoginResponse( { "_permanent": True, @@ -75,3 +100,11 @@ def mock_pyloadapi() -> Generator[AsyncMock, None, None]: client.free_space.return_value = 99999999999 yield client + + +@pytest.fixture(name="config_entry") +def mock_config_entry() -> MockConfigEntry: + """Mock pyLoad configuration entry.""" + return MockConfigEntry( + domain=DOMAIN, title=DEFAULT_NAME, data=USER_INPUT, entry_id="XXXXXXXXXXXXXX" + ) diff --git a/tests/components/pyload/snapshots/test_sensor.ambr b/tests/components/pyload/snapshots/test_sensor.ambr index 226221240d2..3aaa7f4679f 100644 --- a/tests/components/pyload/snapshots/test_sensor.ambr +++ b/tests/components/pyload/snapshots/test_sensor.ambr @@ -1,4 +1,328 @@ # serializer version: 1 +# name: test_sensor_update_exceptions[CannotConnect][sensor.pyload_speed-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.pyload_speed', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'pyLoad Speed', + 'platform': 'pyload', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'XXXXXXXXXXXXXX_speed', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor_update_exceptions[CannotConnect][sensor.pyload_speed-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'data_rate', + 'friendly_name': 'pyLoad Speed', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.pyload_speed', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_sensor_update_exceptions[InvalidAuth][sensor.pyload_speed-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.pyload_speed', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'pyLoad Speed', + 'platform': 'pyload', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'XXXXXXXXXXXXXX_speed', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor_update_exceptions[InvalidAuth][sensor.pyload_speed-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'data_rate', + 'friendly_name': 'pyLoad Speed', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.pyload_speed', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_sensor_update_exceptions[ParserError][sensor.pyload_speed-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.pyload_speed', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'pyLoad Speed', + 'platform': 'pyload', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'XXXXXXXXXXXXXX_speed', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor_update_exceptions[ParserError][sensor.pyload_speed-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'data_rate', + 'friendly_name': 'pyLoad Speed', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.pyload_speed', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_sensors_unavailable[CannotConnect][sensor.pyload_speed-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.pyload_speed', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Speed', + 'platform': 'pyload', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'XXXXXXXXXXXXXX_speed', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors_unavailable[CannotConnect][sensor.pyload_speed-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'data_rate', + 'friendly_name': 'pyLoad Speed', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.pyload_speed', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_sensors_unavailable[InvalidAuth][sensor.pyload_speed-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.pyload_speed', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Speed', + 'platform': 'pyload', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'XXXXXXXXXXXXXX_speed', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors_unavailable[InvalidAuth][sensor.pyload_speed-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'data_rate', + 'friendly_name': 'pyLoad Speed', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.pyload_speed', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_sensors_unavailable[ParserError][sensor.pyload_speed-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.pyload_speed', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Speed', + 'platform': 'pyload', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'XXXXXXXXXXXXXX_speed', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors_unavailable[ParserError][sensor.pyload_speed-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'data_rate', + 'friendly_name': 'pyLoad Speed', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.pyload_speed', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- # name: test_setup StateSnapshot({ 'attributes': ReadOnlyDict({ @@ -14,3 +338,57 @@ 'state': '5.405963', }) # --- +# name: test_setup[sensor.pyload_speed-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.pyload_speed', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'pyLoad Speed', + 'platform': 'pyload', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'XXXXXXXXXXXXXX_speed', + 'unit_of_measurement': , + }) +# --- +# name: test_setup[sensor.pyload_speed-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'data_rate', + 'friendly_name': 'pyLoad Speed', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.pyload_speed', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '5.405963', + }) +# --- diff --git a/tests/components/pyload/test_config_flow.py b/tests/components/pyload/test_config_flow.py new file mode 100644 index 00000000000..70d324fd980 --- /dev/null +++ b/tests/components/pyload/test_config_flow.py @@ -0,0 +1,166 @@ +"""Test the pyLoad config flow.""" + +from unittest.mock import AsyncMock + +from pyloadapi.exceptions import CannotConnect, InvalidAuth, ParserError +import pytest + +from homeassistant.components.pyload.const import DEFAULT_NAME, DOMAIN +from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +from .conftest import USER_INPUT, YAML_INPUT + +from tests.common import MockConfigEntry + + +async def test_form( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_pyloadapi: AsyncMock, +) -> None: + """Test we get the form.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + USER_INPUT, + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == DEFAULT_NAME + assert result["data"] == USER_INPUT + assert len(mock_setup_entry.mock_calls) == 1 + + +@pytest.mark.parametrize( + ("exception", "expected_error"), + [ + (InvalidAuth, "invalid_auth"), + (CannotConnect, "cannot_connect"), + (ParserError, "cannot_connect"), + (ValueError, "unknown"), + ], +) +async def test_form_errors( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_pyloadapi: AsyncMock, + exception: Exception, + expected_error: str, +) -> None: + """Test we handle invalid auth.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + mock_pyloadapi.login.side_effect = exception + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + USER_INPUT, + ) + + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"base": expected_error} + + mock_pyloadapi.login.side_effect = None + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + USER_INPUT, + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == DEFAULT_NAME + assert result["data"] == USER_INPUT + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_flow_user_already_configured( + hass: HomeAssistant, config_entry: MockConfigEntry, mock_pyloadapi: AsyncMock +) -> None: + """Test we abort user data set when entry is already configured.""" + + config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=USER_INPUT, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +async def test_flow_import( + hass: HomeAssistant, + mock_pyloadapi: AsyncMock, +) -> None: + """Test that we can import a YAML config.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=YAML_INPUT, + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "test-name" + assert result["data"] == USER_INPUT + + +async def test_flow_import_already_configured( + hass: HomeAssistant, config_entry: MockConfigEntry, mock_pyloadapi: AsyncMock +) -> None: + """Test we abort import data set when entry is already configured.""" + + config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=YAML_INPUT, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +@pytest.mark.parametrize( + ("exception", "reason"), + [ + (InvalidAuth, "invalid_auth"), + (CannotConnect, "cannot_connect"), + (ParserError, "cannot_connect"), + (ValueError, "unknown"), + ], +) +async def test_flow_import_errors( + hass: HomeAssistant, + config_entry: MockConfigEntry, + mock_pyloadapi: AsyncMock, + exception: Exception, + reason: str, +) -> None: + """Test we abort import data set when entry is already configured.""" + + mock_pyloadapi.login.side_effect = exception + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=YAML_INPUT, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == reason diff --git a/tests/components/pyload/test_init.py b/tests/components/pyload/test_init.py new file mode 100644 index 00000000000..a1ecf294523 --- /dev/null +++ b/tests/components/pyload/test_init.py @@ -0,0 +1,63 @@ +"""Test pyLoad init.""" + +from unittest.mock import MagicMock + +from pyloadapi.exceptions import CannotConnect, InvalidAuth, ParserError +import pytest + +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +async def test_entry_setup_unload( + hass: HomeAssistant, + config_entry: MockConfigEntry, + mock_pyloadapi: MagicMock, +) -> None: + """Test integration setup and unload.""" + + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.LOADED + + await hass.config_entries.async_unload(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.NOT_LOADED + + +@pytest.mark.parametrize( + ("side_effect"), + [CannotConnect, ParserError], +) +async def test_config_entry_setup_errors( + hass: HomeAssistant, + config_entry: MockConfigEntry, + mock_pyloadapi: MagicMock, + side_effect: Exception, +) -> None: + """Test config entry not ready.""" + mock_pyloadapi.login.side_effect = side_effect + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.SETUP_RETRY + + +async def test_config_entry_setup_invalid_auth( + hass: HomeAssistant, + config_entry: MockConfigEntry, + mock_pyloadapi: MagicMock, +) -> None: + """Test config entry authentication.""" + mock_pyloadapi.login.side_effect = InvalidAuth + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.SETUP_ERROR diff --git a/tests/components/pyload/test_sensor.py b/tests/components/pyload/test_sensor.py index e2b392b06f9..d0e912f82f2 100644 --- a/tests/components/pyload/test_sensor.py +++ b/tests/components/pyload/test_sensor.py @@ -7,108 +7,74 @@ from pyloadapi.exceptions import CannotConnect, InvalidAuth, ParserError import pytest from syrupy.assertion import SnapshotAssertion +from homeassistant.components.pyload.const import DOMAIN from homeassistant.components.pyload.sensor import SCAN_INTERVAL -from homeassistant.components.sensor import DOMAIN -from homeassistant.const import STATE_UNAVAILABLE -from homeassistant.core import HomeAssistant +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant +from homeassistant.helpers import entity_registry as er, issue_registry as ir from homeassistant.helpers.typing import ConfigType from homeassistant.setup import async_setup_component -from tests.common import async_fire_time_changed - -SENSORS = ["sensor.pyload_speed"] +from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform -@pytest.mark.usefixtures("mock_pyloadapi") async def test_setup( hass: HomeAssistant, - pyload_config: ConfigType, + config_entry: MockConfigEntry, snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, + mock_pyloadapi: AsyncMock, ) -> None: """Test setup of the pyload sensor platform.""" - - assert await async_setup_component(hass, DOMAIN, pyload_config) + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - for sensor in SENSORS: - result = hass.states.get(sensor) - assert result == snapshot + assert config_entry.state is ConfigEntryState.LOADED + + await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id) @pytest.mark.parametrize( - ("exception", "expected_exception"), - [ - (CannotConnect, "Unable to connect and retrieve data from pyLoad API"), - (ParserError, "Unable to parse data from pyLoad API"), - ( - InvalidAuth, - "Authentication failed for username, check your login credentials", - ), - ], -) -async def test_setup_exceptions( - hass: HomeAssistant, - pyload_config: ConfigType, - mock_pyloadapi: AsyncMock, - exception: Exception, - expected_exception: str, - caplog: pytest.LogCaptureFixture, -) -> None: - """Test exceptions during setup up pyLoad platform.""" - - mock_pyloadapi.login.side_effect = exception - - assert await async_setup_component(hass, DOMAIN, pyload_config) - await hass.async_block_till_done() - - assert len(hass.states.async_all(DOMAIN)) == 0 - assert expected_exception in caplog.text - - -@pytest.mark.parametrize( - ("exception", "expected_exception"), - [ - (CannotConnect, "Unable to connect and retrieve data from pyLoad API"), - (ParserError, "Unable to parse data from pyLoad API"), - (InvalidAuth, "Authentication failed, trying to reauthenticate"), - ], + "exception", + [CannotConnect, InvalidAuth, ParserError], ) async def test_sensor_update_exceptions( hass: HomeAssistant, - pyload_config: ConfigType, + config_entry: MockConfigEntry, mock_pyloadapi: AsyncMock, exception: Exception, - expected_exception: str, - caplog: pytest.LogCaptureFixture, snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, freezer: FrozenDateTimeFactory, ) -> None: - """Test exceptions during update of pyLoad sensor.""" + """Test if pyLoad sensors go unavailable when exceptions occur (except ParserErrors).""" - mock_pyloadapi.get_status.side_effect = exception - - assert await async_setup_component(hass, DOMAIN, pyload_config) + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert len(hass.states.async_all(DOMAIN)) == 1 - assert expected_exception in caplog.text + mock_pyloadapi.get_status.side_effect = exception + freezer.tick(SCAN_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done() - for sensor in SENSORS: - assert hass.states.get(sensor).state == STATE_UNAVAILABLE + await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id) async def test_sensor_invalid_auth( hass: HomeAssistant, - pyload_config: ConfigType, + config_entry: MockConfigEntry, mock_pyloadapi: AsyncMock, caplog: pytest.LogCaptureFixture, freezer: FrozenDateTimeFactory, ) -> None: """Test invalid auth during sensor update.""" - assert await async_setup_component(hass, DOMAIN, pyload_config) + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert len(hass.states.async_all(DOMAIN)) == 1 mock_pyloadapi.get_status.side_effect = InvalidAuth mock_pyloadapi.login.side_effect = InvalidAuth @@ -121,3 +87,61 @@ async def test_sensor_invalid_auth( "Authentication failed for username, check your login credentials" in caplog.text ) + + +async def test_platform_setup_triggers_import_flow( + hass: HomeAssistant, + pyload_config: ConfigType, + mock_setup_entry: AsyncMock, + mock_pyloadapi: AsyncMock, +) -> None: + """Test if an issue is created when attempting setup from yaml config.""" + + assert await async_setup_component(hass, SENSOR_DOMAIN, pyload_config) + await hass.async_block_till_done() + + assert len(mock_setup_entry.mock_calls) == 1 + + +@pytest.mark.parametrize( + ("exception", "reason"), + [ + (InvalidAuth, "invalid_auth"), + (CannotConnect, "cannot_connect"), + (ParserError, "cannot_connect"), + (ValueError, "unknown"), + ], +) +async def test_deprecated_yaml_import_issue( + hass: HomeAssistant, + issue_registry: ir.IssueRegistry, + pyload_config: ConfigType, + mock_pyloadapi: AsyncMock, + exception: Exception, + reason: str, +) -> None: + """Test an issue is created when attempting setup from yaml config and an error happens.""" + + mock_pyloadapi.login.side_effect = exception + await async_setup_component(hass, SENSOR_DOMAIN, pyload_config) + await hass.async_block_till_done() + + assert issue_registry.async_get_issue( + domain=DOMAIN, issue_id=f"deprecated_yaml_import_issue_{reason}" + ) + + +async def test_deprecated_yaml( + hass: HomeAssistant, + issue_registry: ir.IssueRegistry, + pyload_config: ConfigType, + mock_pyloadapi: AsyncMock, +) -> None: + """Test an issue is created when we import from yaml config.""" + + await async_setup_component(hass, SENSOR_DOMAIN, pyload_config) + await hass.async_block_till_done() + + assert issue_registry.async_get_issue( + domain=HOMEASSISTANT_DOMAIN, issue_id=f"deprecated_yaml_{DOMAIN}" + ) From 4474e8c7ef0b1af2944a6ae02dff9f44b80ac8e8 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 23 Jun 2024 12:51:12 +0200 Subject: [PATCH 1049/1445] Remove YAML import for tado (#120231) --- homeassistant/components/tado/config_flow.py | 44 ------ .../components/tado/device_tracker.py | 57 +------ homeassistant/components/tado/strings.json | 16 -- tests/components/tado/test_config_flow.py | 141 ------------------ 4 files changed, 3 insertions(+), 255 deletions(-) diff --git a/homeassistant/components/tado/config_flow.py b/homeassistant/components/tado/config_flow.py index e52b87796f7..d27a8c4b10b 100644 --- a/homeassistant/components/tado/config_flow.py +++ b/homeassistant/components/tado/config_flow.py @@ -23,7 +23,6 @@ from homeassistant.exceptions import HomeAssistantError from .const import ( CONF_FALLBACK, - CONF_HOME_ID, CONST_OVERLAY_TADO_DEFAULT, CONST_OVERLAY_TADO_OPTIONS, DOMAIN, @@ -117,49 +116,6 @@ class TadoConfigFlow(ConfigFlow, domain=DOMAIN): self._abort_if_unique_id_configured() return await self.async_step_user() - async def async_step_import( - self, import_config: dict[str, Any] - ) -> ConfigFlowResult: - """Import a config entry from configuration.yaml.""" - _LOGGER.debug("Importing Tado from configuration.yaml") - username = import_config[CONF_USERNAME] - password = import_config[CONF_PASSWORD] - imported_home_id = import_config[CONF_HOME_ID] - - self._async_abort_entries_match( - { - CONF_USERNAME: username, - CONF_PASSWORD: password, - CONF_HOME_ID: imported_home_id, - } - ) - - try: - validate_result = await validate_input( - self.hass, - { - CONF_USERNAME: username, - CONF_PASSWORD: password, - }, - ) - except HomeAssistantError: - return self.async_abort(reason="import_failed") - except PyTado.exceptions.TadoWrongCredentialsException: - return self.async_abort(reason="import_failed_invalid_auth") - - home_id = validate_result[UNIQUE_ID] - await self.async_set_unique_id(home_id) - self._abort_if_unique_id_configured() - - return self.async_create_entry( - title=import_config[CONF_USERNAME], - data={ - CONF_USERNAME: username, - CONF_PASSWORD: password, - CONF_HOME_ID: home_id, - }, - ) - async def async_step_reconfigure( self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: diff --git a/homeassistant/components/tado/device_tracker.py b/homeassistant/components/tado/device_tracker.py index d3996db7faf..1caea1b3103 100644 --- a/homeassistant/components/tado/device_tracker.py +++ b/homeassistant/components/tado/device_tracker.py @@ -4,74 +4,23 @@ from __future__ import annotations import logging -import voluptuous as vol - from homeassistant.components.device_tracker import ( DOMAIN as DEVICE_TRACKER_DOMAIN, - PLATFORM_SCHEMA as BASE_PLATFORM_SCHEMA, - DeviceScanner, SourceType, TrackerEntity, ) -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, STATE_HOME, STATE_NOT_HOME +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import STATE_HOME, STATE_NOT_HOME from homeassistant.core import HomeAssistant, callback -from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import entity_registry as er -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue -from homeassistant.helpers.typing import ConfigType from . import TadoConnector -from .const import CONF_HOME_ID, DATA, DOMAIN, SIGNAL_TADO_MOBILE_DEVICE_UPDATE_RECEIVED +from .const import DATA, DOMAIN, SIGNAL_TADO_MOBILE_DEVICE_UPDATE_RECEIVED _LOGGER = logging.getLogger(__name__) -PLATFORM_SCHEMA = BASE_PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_HOME_ID): cv.string, - } -) - - -async def async_get_scanner( - hass: HomeAssistant, config: ConfigType -) -> DeviceScanner | None: - """Configure the Tado device scanner.""" - device_config = config["device_tracker"] - import_result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data={ - CONF_USERNAME: device_config[CONF_USERNAME], - CONF_PASSWORD: device_config[CONF_PASSWORD], - CONF_HOME_ID: device_config.get(CONF_HOME_ID), - }, - ) - - translation_key = "deprecated_yaml_import_device_tracker" - if import_result.get("type") == FlowResultType.ABORT: - translation_key = "import_aborted" - if import_result.get("reason") == "import_failed": - translation_key = "import_failed" - if import_result.get("reason") == "import_failed_invalid_auth": - translation_key = "import_failed_invalid_auth" - - async_create_issue( - hass, - DOMAIN, - "deprecated_yaml_import_device_tracker", - breaks_in_ha_version="2024.7.0", - is_fixable=False, - severity=IssueSeverity.WARNING, - translation_key=translation_key, - ) - return None - async def async_setup_entry( hass: HomeAssistant, diff --git a/homeassistant/components/tado/strings.json b/homeassistant/components/tado/strings.json index d992befe112..ab903dafb5b 100644 --- a/homeassistant/components/tado/strings.json +++ b/homeassistant/components/tado/strings.json @@ -150,22 +150,6 @@ } }, "issues": { - "deprecated_yaml_import_device_tracker": { - "title": "Tado YAML device tracker configuration imported", - "description": "Configuring the Tado Device Tracker using YAML is being removed.\nRemove the YAML device tracker configuration and restart Home Assistant." - }, - "import_aborted": { - "title": "Import aborted", - "description": "Configuring the Tado Device Tracker using YAML is being removed.\n The import was aborted, due to an existing config entry being the same as the data being imported in the YAML. Remove the YAML device tracker configuration and restart Home Assistant. Please use the UI to configure Tado." - }, - "import_failed": { - "title": "Failed to import", - "description": "Failed to import the configuration for the Tado Device Tracker. Please use the UI to configure Tado. Don't forget to delete the YAML configuration." - }, - "import_failed_invalid_auth": { - "title": "Failed to import, invalid credentials", - "description": "Failed to import the configuration for the Tado Device Tracker, due to invalid credentials. Please fix the YAML configuration and restart Home Assistant. Alternatively you can use the UI to configure Tado. Don't forget to delete the YAML configuration, once the import is successful." - }, "water_heater_fallback": { "title": "Tado Water Heater entities now support fallback options", "description": "Due to added support for water heaters entities, these entities may use different overlay. Please configure integration entity and tado app water heater zone overlay options." diff --git a/tests/components/tado/test_config_flow.py b/tests/components/tado/test_config_flow.py index a8883f47fe2..4f5f4180fb5 100644 --- a/tests/components/tado/test_config_flow.py +++ b/tests/components/tado/test_config_flow.py @@ -271,147 +271,6 @@ async def test_form_homekit(hass: HomeAssistant) -> None: assert result["type"] is FlowResultType.ABORT -async def test_import_step(hass: HomeAssistant) -> None: - """Test import step.""" - mock_tado_api = _get_mock_tado_api(getMe={"homes": [{"id": 1, "name": "myhome"}]}) - - with ( - patch( - "homeassistant.components.tado.config_flow.Tado", - return_value=mock_tado_api, - ), - patch( - "homeassistant.components.tado.async_setup_entry", - return_value=True, - ) as mock_setup_entry, - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={ - "username": "test-username", - "password": "test-password", - "home_id": 1, - }, - ) - await hass.async_block_till_done() - - assert result["type"] is FlowResultType.CREATE_ENTRY - assert result["data"] == { - "username": "test-username", - "password": "test-password", - "home_id": "1", - } - assert mock_setup_entry.call_count == 1 - - -async def test_import_step_existing_entry(hass: HomeAssistant) -> None: - """Test import step with existing entry.""" - entry = MockConfigEntry( - domain=DOMAIN, - data={ - "username": "test-username", - "password": "test-password", - "home_id": 1, - }, - ) - entry.add_to_hass(hass) - - with patch( - "homeassistant.components.tado.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={ - "username": "test-username", - "password": "test-password", - "home_id": 1, - }, - ) - await hass.async_block_till_done() - - assert result["type"] is FlowResultType.ABORT - assert result["reason"] == "already_configured" - assert mock_setup_entry.call_count == 0 - - -async def test_import_step_validation_failed(hass: HomeAssistant) -> None: - """Test import step with validation failed.""" - with patch( - "homeassistant.components.tado.config_flow.Tado", - side_effect=RuntimeError, - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={ - "username": "test-username", - "password": "test-password", - "home_id": 1, - }, - ) - await hass.async_block_till_done() - - assert result["type"] is FlowResultType.ABORT - assert result["reason"] == "import_failed" - - -async def test_import_step_device_authentication_failed(hass: HomeAssistant) -> None: - """Test import step with device tracker authentication failed.""" - with patch( - "homeassistant.components.tado.config_flow.Tado", - side_effect=PyTado.exceptions.TadoWrongCredentialsException, - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={ - "username": "test-username", - "password": "test-password", - "home_id": 1, - }, - ) - await hass.async_block_till_done() - - assert result["type"] is FlowResultType.ABORT - assert result["reason"] == "import_failed_invalid_auth" - - -async def test_import_step_unique_id_configured(hass: HomeAssistant) -> None: - """Test import step with unique ID already configured.""" - entry = MockConfigEntry( - domain=DOMAIN, - data={ - "username": "test-username", - "password": "test-password", - "home_id": 1, - }, - unique_id="unique_id", - ) - entry.add_to_hass(hass) - - with patch( - "homeassistant.components.tado.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={ - "username": "test-username", - "password": "test-password", - "home_id": 1, - }, - ) - await hass.async_block_till_done() - - assert result["type"] is FlowResultType.ABORT - assert result["reason"] == "already_configured" - assert mock_setup_entry.call_count == 0 - - @pytest.mark.parametrize( ("exception", "error"), [ From 84d1d111385dd705fa38bbcde34a95d2d171afcb Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Sun, 23 Jun 2024 12:56:41 +0200 Subject: [PATCH 1050/1445] Add config flow to generic hygrostat (#119017) * Add config flow to hygrostat Co-authored-by: Franck Nijhof --- .../components/generic_hygrostat/__init__.py | 20 ++++ .../generic_hygrostat/config_flow.py | 100 +++++++++++++++++ .../generic_hygrostat/humidifier.py | 50 +++++++-- .../generic_hygrostat/manifest.json | 2 + .../components/generic_hygrostat/strings.json | 56 +++++++++ homeassistant/generated/config_flows.py | 1 + homeassistant/generated/integrations.json | 12 +- .../snapshots/test_config_flow.ambr | 66 +++++++++++ .../generic_hygrostat/test_config_flow.py | 106 ++++++++++++++++++ 9 files changed, 400 insertions(+), 13 deletions(-) create mode 100644 homeassistant/components/generic_hygrostat/config_flow.py create mode 100644 homeassistant/components/generic_hygrostat/strings.json create mode 100644 tests/components/generic_hygrostat/snapshots/test_config_flow.ambr create mode 100644 tests/components/generic_hygrostat/test_config_flow.py diff --git a/homeassistant/components/generic_hygrostat/__init__.py b/homeassistant/components/generic_hygrostat/__init__.py index 467a9f0e0c5..ef032da1ee2 100644 --- a/homeassistant/components/generic_hygrostat/__init__.py +++ b/homeassistant/components/generic_hygrostat/__init__.py @@ -3,6 +3,7 @@ import voluptuous as vol from homeassistant.components.humidifier import HumidifierDeviceClass +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_NAME, CONF_UNIQUE_ID, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv, discovery @@ -73,3 +74,22 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: ) return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up from a config entry.""" + await hass.config_entries.async_forward_entry_setups(entry, (Platform.HUMIDIFIER,)) + entry.async_on_unload(entry.add_update_listener(config_entry_update_listener)) + return True + + +async def config_entry_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Update listener, called when the config entry options are changed.""" + await hass.config_entries.async_reload(entry.entry_id) + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + return await hass.config_entries.async_unload_platforms( + entry, (Platform.HUMIDIFIER,) + ) diff --git a/homeassistant/components/generic_hygrostat/config_flow.py b/homeassistant/components/generic_hygrostat/config_flow.py new file mode 100644 index 00000000000..cade566968d --- /dev/null +++ b/homeassistant/components/generic_hygrostat/config_flow.py @@ -0,0 +1,100 @@ +"""Config flow for Generic hygrostat.""" + +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, cast + +import voluptuous as vol + +from homeassistant.components.humidifier import HumidifierDeviceClass +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN, SensorDeviceClass +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +from homeassistant.const import CONF_NAME, PERCENTAGE +from homeassistant.helpers import selector +from homeassistant.helpers.schema_config_entry_flow import ( + SchemaConfigFlowHandler, + SchemaFlowFormStep, +) + +from . import ( + CONF_DEVICE_CLASS, + CONF_DRY_TOLERANCE, + CONF_HUMIDIFIER, + CONF_MIN_DUR, + CONF_SENSOR, + CONF_WET_TOLERANCE, + DEFAULT_TOLERANCE, + DOMAIN, +) + +OPTIONS_SCHEMA = { + vol.Required(CONF_DEVICE_CLASS): selector.SelectSelector( + selector.SelectSelectorConfig( + options=[ + HumidifierDeviceClass.HUMIDIFIER, + HumidifierDeviceClass.DEHUMIDIFIER, + ], + translation_key=CONF_DEVICE_CLASS, + mode=selector.SelectSelectorMode.DROPDOWN, + ), + ), + vol.Required(CONF_SENSOR): selector.EntitySelector( + selector.EntitySelectorConfig( + domain=SENSOR_DOMAIN, device_class=SensorDeviceClass.HUMIDITY + ) + ), + vol.Required(CONF_HUMIDIFIER): selector.EntitySelector( + selector.EntitySelectorConfig(domain=SWITCH_DOMAIN) + ), + vol.Required( + CONF_DRY_TOLERANCE, default=DEFAULT_TOLERANCE + ): selector.NumberSelector( + selector.NumberSelectorConfig( + min=0, + max=100, + step=0.5, + unit_of_measurement=PERCENTAGE, + mode=selector.NumberSelectorMode.BOX, + ) + ), + vol.Required( + CONF_WET_TOLERANCE, default=DEFAULT_TOLERANCE + ): selector.NumberSelector( + selector.NumberSelectorConfig( + min=0, + max=100, + step=0.5, + unit_of_measurement=PERCENTAGE, + mode=selector.NumberSelectorMode.BOX, + ) + ), + vol.Optional(CONF_MIN_DUR): selector.DurationSelector( + selector.DurationSelectorConfig(allow_negative=False) + ), +} + +CONFIG_SCHEMA = { + vol.Required(CONF_NAME): selector.TextSelector(), + **OPTIONS_SCHEMA, +} + + +CONFIG_FLOW = { + "user": SchemaFlowFormStep(vol.Schema(CONFIG_SCHEMA)), +} + +OPTIONS_FLOW = { + "init": SchemaFlowFormStep(vol.Schema(OPTIONS_SCHEMA)), +} + + +class ConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN): + """Handle a config or options flow.""" + + config_flow = CONFIG_FLOW + options_flow = OPTIONS_FLOW + + def async_config_entry_title(self, options: Mapping[str, Any]) -> str: + """Return config entry title.""" + return cast(str, options["name"]) diff --git a/homeassistant/components/generic_hygrostat/humidifier.py b/homeassistant/components/generic_hygrostat/humidifier.py index dea614d92f2..c22904a4caa 100644 --- a/homeassistant/components/generic_hygrostat/humidifier.py +++ b/homeassistant/components/generic_hygrostat/humidifier.py @@ -3,10 +3,10 @@ from __future__ import annotations import asyncio -from collections.abc import Callable +from collections.abc import Callable, Mapping from datetime import datetime, timedelta import logging -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, cast from homeassistant.components.humidifier import ( ATTR_HUMIDITY, @@ -18,6 +18,7 @@ from homeassistant.components.humidifier import ( HumidifierEntity, HumidifierEntityFeature, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_MODE, @@ -39,7 +40,7 @@ from homeassistant.core import ( State, callback, ) -from homeassistant.helpers import condition +from homeassistant.helpers import condition, config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import ( async_track_state_change_event, @@ -83,6 +84,38 @@ async def async_setup_platform( """Set up the generic hygrostat platform.""" if discovery_info: config = discovery_info + await _async_setup_config( + hass, config, config.get(CONF_UNIQUE_ID), async_add_entities + ) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Initialize config entry.""" + + await _async_setup_config( + hass, + config_entry.options, + config_entry.entry_id, + async_add_entities, + ) + + +def _time_period_or_none(value: Any) -> timedelta | None: + if value is None: + return None + return cast(timedelta, cv.time_period(value)) + + +async def _async_setup_config( + hass: HomeAssistant, + config: Mapping[str, Any], + unique_id: str | None, + async_add_entities: AddEntitiesCallback, +) -> None: name: str = config[CONF_NAME] switch_entity_id: str = config[CONF_HUMIDIFIER] sensor_entity_id: str = config[CONF_SENSOR] @@ -90,15 +123,18 @@ async def async_setup_platform( max_humidity: float | None = config.get(CONF_MAX_HUMIDITY) target_humidity: float | None = config.get(CONF_TARGET_HUMIDITY) device_class: HumidifierDeviceClass | None = config.get(CONF_DEVICE_CLASS) - min_cycle_duration: timedelta | None = config.get(CONF_MIN_DUR) - sensor_stale_duration: timedelta | None = config.get(CONF_STALE_DURATION) + min_cycle_duration: timedelta | None = _time_period_or_none( + config.get(CONF_MIN_DUR) + ) + sensor_stale_duration: timedelta | None = _time_period_or_none( + config.get(CONF_STALE_DURATION) + ) dry_tolerance: float = config[CONF_DRY_TOLERANCE] wet_tolerance: float = config[CONF_WET_TOLERANCE] - keep_alive: timedelta | None = config.get(CONF_KEEP_ALIVE) + keep_alive: timedelta | None = _time_period_or_none(config.get(CONF_KEEP_ALIVE)) initial_state: bool | None = config.get(CONF_INITIAL_STATE) away_humidity: int | None = config.get(CONF_AWAY_HUMIDITY) away_fixed: bool | None = config.get(CONF_AWAY_FIXED) - unique_id: str | None = config.get(CONF_UNIQUE_ID) async_add_entities( [ diff --git a/homeassistant/components/generic_hygrostat/manifest.json b/homeassistant/components/generic_hygrostat/manifest.json index cf0ace5e011..20222fd3617 100644 --- a/homeassistant/components/generic_hygrostat/manifest.json +++ b/homeassistant/components/generic_hygrostat/manifest.json @@ -2,7 +2,9 @@ "domain": "generic_hygrostat", "name": "Generic hygrostat", "codeowners": ["@Shulyaka"], + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/generic_hygrostat", + "integration_type": "helper", "iot_class": "local_polling", "quality_scale": "internal" } diff --git a/homeassistant/components/generic_hygrostat/strings.json b/homeassistant/components/generic_hygrostat/strings.json new file mode 100644 index 00000000000..a21ab68c628 --- /dev/null +++ b/homeassistant/components/generic_hygrostat/strings.json @@ -0,0 +1,56 @@ +{ + "title": "Generic hygrostat", + "config": { + "step": { + "user": { + "title": "Add generic hygrostat", + "description": "Create a entity that control the humidity via a switch and sensor.", + "data": { + "device_class": "Device class", + "dry_tolerance": "Dry tolerance", + "humidifier": "Switch", + "min_cycle_duration": "Minimum cycle duration", + "name": "[%key:common::config_flow::data::name%]", + "target_sensor": "Humidity sensor", + "wet_tolerance": "Wet tolerance" + }, + "data_description": { + "dry_tolerance": "The minimum amount of difference between the humidity read by the sensor specified in the target sensor option and the target humidity that must change prior to being switched on.", + "humidifier": "Humidifier or dehumidifier switch; must be a toggle device.", + "min_cycle_duration": "Set a minimum amount of time that the switch specified in the humidifier option must be in its current state prior to being switched either off or on.", + "target_sensor": "Sensor with current humidity.", + "wet_tolerance": "The minimum amount of difference between the humidity read by the sensor specified in the target sensor option and the target humidity that must change prior to being switched off." + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "device_class": "[%key:component::generic_hygrostat::config::step::user::data::device_class%]", + "dry_tolerance": "[%key:component::generic_hygrostat::config::step::user::data::dry_tolerance%]", + "humidifier": "[%key:component::generic_hygrostat::config::step::user::data::humidifier%]", + "min_cycle_duration": "[%key:component::generic_hygrostat::config::step::user::data::min_cycle_duration%]", + "target_sensor": "[%key:component::generic_hygrostat::config::step::user::data::target_sensor%]", + "wet_tolerance": "[%key:component::generic_hygrostat::config::step::user::data::wet_tolerance%]" + }, + "data_description": { + "dry_tolerance": "[%key:component::generic_hygrostat::config::step::user::data_description::dry_tolerance%]", + "humidifier": "[%key:component::generic_hygrostat::config::step::user::data_description::humidifier%]", + "min_cycle_duration": "[%key:component::generic_hygrostat::config::step::user::data_description::min_cycle_duration%]", + "target_sensor": "[%key:component::generic_hygrostat::config::step::user::data_description::target_sensor%]", + "wet_tolerance": "[%key:component::generic_hygrostat::config::step::user::data_description::wet_tolerance%]" + } + } + } + }, + "selector": { + "device_class": { + "options": { + "humidifier": "Humidifier", + "dehumidifier": "Dehumidifier" + } + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 631a5b6abb4..e5eeeb29403 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -6,6 +6,7 @@ To update, run python3 -m script.hassfest FLOWS = { "helper": [ "derivative", + "generic_hygrostat", "generic_thermostat", "group", "integration", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index eef78c212c8..d3380fdd17f 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -2127,12 +2127,6 @@ "config_flow": true, "iot_class": "local_push" }, - "generic_hygrostat": { - "name": "Generic hygrostat", - "integration_type": "hub", - "config_flow": false, - "iot_class": "local_polling" - }, "geniushub": { "name": "Genius Hub", "integration_type": "hub", @@ -7160,6 +7154,11 @@ "config_flow": true, "iot_class": "calculated" }, + "generic_hygrostat": { + "integration_type": "helper", + "config_flow": true, + "iot_class": "local_polling" + }, "generic_thermostat": { "integration_type": "helper", "config_flow": true, @@ -7265,6 +7264,7 @@ "filesize", "garages_amsterdam", "generic", + "generic_hygrostat", "generic_thermostat", "google_travel_time", "group", diff --git a/tests/components/generic_hygrostat/snapshots/test_config_flow.ambr b/tests/components/generic_hygrostat/snapshots/test_config_flow.ambr new file mode 100644 index 00000000000..3527596c9b9 --- /dev/null +++ b/tests/components/generic_hygrostat/snapshots/test_config_flow.ambr @@ -0,0 +1,66 @@ +# serializer version: 1 +# name: test_config_flow[create] + FlowResultSnapshot({ + 'result': ConfigEntrySnapshot({ + 'title': 'My hygrostat', + }), + 'title': 'My hygrostat', + 'type': , + }) +# --- +# name: test_config_flow[init] + FlowResultSnapshot({ + 'type': , + }) +# --- +# name: test_options[create_entry] + FlowResultSnapshot({ + 'result': True, + 'type': , + }) +# --- +# name: test_options[dehumidifier] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'action': , + 'current_humidity': 10.0, + 'device_class': 'dehumidifier', + 'friendly_name': 'My hygrostat', + 'humidity': 100, + 'max_humidity': 100, + 'min_humidity': 0, + 'supported_features': , + }), + 'context': , + 'entity_id': 'humidifier.my_hygrostat', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_options[humidifier] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'action': , + 'current_humidity': 10.0, + 'device_class': 'humidifier', + 'friendly_name': 'My hygrostat', + 'humidity': 100, + 'max_humidity': 100, + 'min_humidity': 0, + 'supported_features': , + }), + 'context': , + 'entity_id': 'humidifier.my_hygrostat', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_options[init] + FlowResultSnapshot({ + 'type': , + }) +# --- diff --git a/tests/components/generic_hygrostat/test_config_flow.py b/tests/components/generic_hygrostat/test_config_flow.py new file mode 100644 index 00000000000..49572e296e4 --- /dev/null +++ b/tests/components/generic_hygrostat/test_config_flow.py @@ -0,0 +1,106 @@ +"""Test the generic hygrostat config flow.""" + +from unittest.mock import patch + +from syrupy.assertion import SnapshotAssertion +from syrupy.filters import props + +from homeassistant.components.generic_hygrostat import ( + CONF_DEVICE_CLASS, + CONF_DRY_TOLERANCE, + CONF_HUMIDIFIER, + CONF_NAME, + CONF_SENSOR, + CONF_WET_TOLERANCE, + DOMAIN, +) +from homeassistant.config_entries import SOURCE_USER +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + +SNAPSHOT_FLOW_PROPS = props("type", "title", "result", "error") + + +async def test_config_flow(hass: HomeAssistant, snapshot: SnapshotAssertion) -> None: + """Test the config flow.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result == snapshot(name="init", include=SNAPSHOT_FLOW_PROPS) + + with patch( + "homeassistant.components.generic_hygrostat.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_NAME: "My hygrostat", + CONF_DRY_TOLERANCE: 2, + CONF_WET_TOLERANCE: 4, + CONF_HUMIDIFIER: "switch.run", + CONF_SENSOR: "sensor.humidity", + CONF_DEVICE_CLASS: "dehumidifier", + }, + ) + await hass.async_block_till_done() + + assert result == snapshot(name="create", include=SNAPSHOT_FLOW_PROPS) + assert len(mock_setup_entry.mock_calls) == 1 + + config_entry = hass.config_entries.async_entries(DOMAIN)[0] + assert config_entry.data == {} + assert config_entry.title == "My hygrostat" + + +async def test_options(hass: HomeAssistant, snapshot: SnapshotAssertion) -> None: + """Test reconfiguring.""" + + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={ + CONF_DEVICE_CLASS: "dehumidifier", + CONF_DRY_TOLERANCE: 2.0, + CONF_HUMIDIFIER: "switch.run", + CONF_NAME: "My hygrostat", + CONF_SENSOR: "sensor.humidity", + CONF_WET_TOLERANCE: 4.0, + }, + title="My hygrostat", + ) + config_entry.add_to_hass(hass) + + # set some initial values + hass.states.async_set( + "sensor.humidity", + "10", + {"unit_of_measurement": "%", "device_class": "humidity"}, + ) + hass.states.async_set("switch.run", "on") + + # check that it is setup + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert hass.states.get("humidifier.my_hygrostat") == snapshot(name="dehumidifier") + + # switch to humidifier + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result == snapshot(name="init", include=SNAPSHOT_FLOW_PROPS) + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_DRY_TOLERANCE: 2, + CONF_WET_TOLERANCE: 4, + CONF_HUMIDIFIER: "switch.run", + CONF_SENSOR: "sensor.humidity", + CONF_DEVICE_CLASS: "humidifier", + }, + ) + assert result == snapshot(name="create_entry", include=SNAPSHOT_FLOW_PROPS) + + # Check config entry is reloaded with new options + await hass.async_block_till_done() + assert hass.states.get("humidifier.my_hygrostat") == snapshot(name="humidifier") From 826587abb265e4a16142893587130ef0203671d5 Mon Sep 17 00:00:00 2001 From: "Mr. Bubbles" Date: Sun, 23 Jun 2024 13:16:00 +0200 Subject: [PATCH 1051/1445] Add `DeviceInfo` to pyLoad integration (#120232) * Add device info to pyLoad integration * Update homeassistant/components/pyload/sensor.py Co-authored-by: Joost Lekkerkerker * Update homeassistant/components/pyload/sensor.py Co-authored-by: Joost Lekkerkerker * remove name, add entry_type --------- Co-authored-by: Joost Lekkerkerker --- homeassistant/components/pyload/sensor.py | 11 ++++++++++- .../components/pyload/snapshots/test_sensor.ambr | 16 ++++++++-------- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/pyload/sensor.py b/homeassistant/components/pyload/sensor.py index 131fec68609..75f3227d542 100644 --- a/homeassistant/components/pyload/sensor.py +++ b/homeassistant/components/pyload/sensor.py @@ -36,6 +36,7 @@ from homeassistant.const import ( from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant from homeassistant.data_entry_flow import FlowResultType import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType @@ -150,6 +151,8 @@ async def async_setup_entry( class PyLoadSensor(SensorEntity): """Representation of a pyLoad sensor.""" + _attr_has_entity_name = True + def __init__( self, api: PyLoadAPI, @@ -158,13 +161,19 @@ class PyLoadSensor(SensorEntity): entry_id: str, ) -> None: """Initialize a new pyLoad sensor.""" - self._attr_name = f"{client_name} {entity_description.name}" self.type = entity_description.key self.api = api self._attr_unique_id = f"{entry_id}_{entity_description.key}" self.entity_description = entity_description self._attr_available = False self.data: StatusServerResponse + self.device_info = DeviceInfo( + entry_type=DeviceEntryType.SERVICE, + manufacturer="PyLoad Team", + model="pyLoad", + configuration_url=api.api_url, + identifiers={(DOMAIN, entry_id)}, + ) async def async_update(self) -> None: """Update state of sensor.""" diff --git a/tests/components/pyload/snapshots/test_sensor.ambr b/tests/components/pyload/snapshots/test_sensor.ambr index 3aaa7f4679f..b772a2c39b1 100644 --- a/tests/components/pyload/snapshots/test_sensor.ambr +++ b/tests/components/pyload/snapshots/test_sensor.ambr @@ -12,7 +12,7 @@ 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.pyload_speed', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -29,7 +29,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'pyLoad Speed', + 'original_name': 'Speed', 'platform': 'pyload', 'previous_unique_id': None, 'supported_features': 0, @@ -66,7 +66,7 @@ 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.pyload_speed', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -83,7 +83,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'pyLoad Speed', + 'original_name': 'Speed', 'platform': 'pyload', 'previous_unique_id': None, 'supported_features': 0, @@ -120,7 +120,7 @@ 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.pyload_speed', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -137,7 +137,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'pyLoad Speed', + 'original_name': 'Speed', 'platform': 'pyload', 'previous_unique_id': None, 'supported_features': 0, @@ -351,7 +351,7 @@ 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.pyload_speed', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -368,7 +368,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'pyLoad Speed', + 'original_name': 'Speed', 'platform': 'pyload', 'previous_unique_id': None, 'supported_features': 0, From 001abdaccf5b21df646c0e942ed2717d8c412658 Mon Sep 17 00:00:00 2001 From: Virenbar Date: Sun, 23 Jun 2024 16:49:43 +0500 Subject: [PATCH 1052/1445] Fix generic thermostat string (#120235) --- homeassistant/components/generic_thermostat/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/generic_thermostat/strings.json b/homeassistant/components/generic_thermostat/strings.json index 27a563a9d8d..1ddd41de734 100644 --- a/homeassistant/components/generic_thermostat/strings.json +++ b/homeassistant/components/generic_thermostat/strings.json @@ -29,7 +29,7 @@ "away_temp": "[%key:component::climate::entity_component::_::state_attributes::preset_mode::state::away%]", "comfort_temp": "[%key:component::climate::entity_component::_::state_attributes::preset_mode::state::comfort%]", "eco_temp": "[%key:component::climate::entity_component::_::state_attributes::preset_mode::state::eco%]", - "home_temp": "[%common::state::home%]", + "home_temp": "[%key:common::state::home%]", "sleep_temp": "[%key:component::climate::entity_component::_::state_attributes::preset_mode::state::sleep%]", "activity_temp": "[%key:component::climate::entity_component::_::state_attributes::preset_mode::state::activity%]" } From 473b3b61ebea3096ec75ebcf54c25615a32f3247 Mon Sep 17 00:00:00 2001 From: "Mr. Bubbles" Date: Sun, 23 Jun 2024 14:25:36 +0200 Subject: [PATCH 1053/1445] Add string and icon translations to pyLoad integration (#120234) add string and icon translations to pyLoad --- homeassistant/components/pyload/icons.json | 9 +++++++++ homeassistant/components/pyload/sensor.py | 2 +- homeassistant/components/pyload/strings.json | 7 +++++++ tests/components/pyload/snapshots/test_sensor.ambr | 8 ++++---- 4 files changed, 21 insertions(+), 5 deletions(-) create mode 100644 homeassistant/components/pyload/icons.json diff --git a/homeassistant/components/pyload/icons.json b/homeassistant/components/pyload/icons.json new file mode 100644 index 00000000000..b3b7d148b1a --- /dev/null +++ b/homeassistant/components/pyload/icons.json @@ -0,0 +1,9 @@ +{ + "entity": { + "sensor": { + "speed": { + "default": "mdi:speedometer" + } + } + } +} diff --git a/homeassistant/components/pyload/sensor.py b/homeassistant/components/pyload/sensor.py index 75f3227d542..8c35f8e7431 100644 --- a/homeassistant/components/pyload/sensor.py +++ b/homeassistant/components/pyload/sensor.py @@ -58,7 +58,7 @@ class PyLoadSensorEntity(StrEnum): SENSOR_DESCRIPTIONS: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key=PyLoadSensorEntity.SPEED, - name="Speed", + translation_key=PyLoadSensorEntity.SPEED, device_class=SensorDeviceClass.DATA_RATE, native_unit_of_measurement=UnitOfDataRate.BYTES_PER_SECOND, suggested_unit_of_measurement=UnitOfDataRate.MEGABYTES_PER_SECOND, diff --git a/homeassistant/components/pyload/strings.json b/homeassistant/components/pyload/strings.json index 30e2366eb86..a8544bf48eb 100644 --- a/homeassistant/components/pyload/strings.json +++ b/homeassistant/components/pyload/strings.json @@ -27,6 +27,13 @@ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } }, + "entity": { + "sensor": { + "speed": { + "name": "Speed" + } + } + }, "issues": { "deprecated_yaml_import_issue_cannot_connect": { "title": "The pyLoad YAML configuration import failed", diff --git a/tests/components/pyload/snapshots/test_sensor.ambr b/tests/components/pyload/snapshots/test_sensor.ambr index b772a2c39b1..77a79e3eddd 100644 --- a/tests/components/pyload/snapshots/test_sensor.ambr +++ b/tests/components/pyload/snapshots/test_sensor.ambr @@ -33,7 +33,7 @@ 'platform': 'pyload', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': , 'unique_id': 'XXXXXXXXXXXXXX_speed', 'unit_of_measurement': , }) @@ -87,7 +87,7 @@ 'platform': 'pyload', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': , 'unique_id': 'XXXXXXXXXXXXXX_speed', 'unit_of_measurement': , }) @@ -141,7 +141,7 @@ 'platform': 'pyload', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': , 'unique_id': 'XXXXXXXXXXXXXX_speed', 'unit_of_measurement': , }) @@ -372,7 +372,7 @@ 'platform': 'pyload', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': , 'unique_id': 'XXXXXXXXXXXXXX_speed', 'unit_of_measurement': , }) From 2cc34fd7e7d00d09df28d3358af1b867a1c99ac1 Mon Sep 17 00:00:00 2001 From: Tsvi Mostovicz Date: Sun, 23 Jun 2024 18:26:45 +0300 Subject: [PATCH 1054/1445] Improve Jewish calendar entities (#120236) * By default don't enable all sensors * Fix tests * Add entity category * Set has_entity_name to true * Revert "Set has_entity_name to true" This reverts commit 5ebfcde78ab0ff54bdca037b3bf3e6ec187cafea. --- .../jewish_calendar/binary_sensor.py | 5 +++- .../components/jewish_calendar/sensor.py | 25 ++++++++++++++++++- .../components/jewish_calendar/test_sensor.py | 4 +++ 3 files changed, 32 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/jewish_calendar/binary_sensor.py b/homeassistant/components/jewish_calendar/binary_sensor.py index c28dee88cf5..54080fcefd8 100644 --- a/homeassistant/components/jewish_calendar/binary_sensor.py +++ b/homeassistant/components/jewish_calendar/binary_sensor.py @@ -16,7 +16,7 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_LANGUAGE, CONF_LOCATION +from homeassistant.const import CONF_LANGUAGE, CONF_LOCATION, EntityCategory from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.helpers import event from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -55,11 +55,13 @@ BINARY_SENSORS: tuple[JewishCalendarBinarySensorEntityDescription, ...] = ( key="erev_shabbat_hag", name="Erev Shabbat/Hag", is_on=lambda state: bool(state.erev_shabbat_chag), + entity_registry_enabled_default=False, ), JewishCalendarBinarySensorEntityDescription( key="motzei_shabbat_hag", name="Motzei Shabbat/Hag", is_on=lambda state: bool(state.motzei_shabbat_chag), + entity_registry_enabled_default=False, ), ) @@ -82,6 +84,7 @@ class JewishCalendarBinarySensor(BinarySensorEntity): """Representation of an Jewish Calendar binary sensor.""" _attr_should_poll = False + _attr_entity_category = EntityCategory.DIAGNOSTIC entity_description: JewishCalendarBinarySensorEntityDescription def __init__( diff --git a/homeassistant/components/jewish_calendar/sensor.py b/homeassistant/components/jewish_calendar/sensor.py index aff9d7ee602..02a5da27119 100644 --- a/homeassistant/components/jewish_calendar/sensor.py +++ b/homeassistant/components/jewish_calendar/sensor.py @@ -15,7 +15,12 @@ from homeassistant.components.sensor import ( SensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_LANGUAGE, CONF_LOCATION, SUN_EVENT_SUNSET +from homeassistant.const import ( + CONF_LANGUAGE, + CONF_LOCATION, + SUN_EVENT_SUNSET, + EntityCategory, +) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.sun import get_astral_event_date @@ -54,11 +59,13 @@ INFO_SENSORS: tuple[SensorEntityDescription, ...] = ( key="omer_count", name="Day of the Omer", icon="mdi:counter", + entity_registry_enabled_default=False, ), SensorEntityDescription( key="daf_yomi", name="Daf Yomi", icon="mdi:book-open-variant", + entity_registry_enabled_default=False, ), ) @@ -67,11 +74,13 @@ TIME_SENSORS: tuple[SensorEntityDescription, ...] = ( key="first_light", name="Alot Hashachar", # codespell:ignore alot icon="mdi:weather-sunset-up", + entity_registry_enabled_default=False, ), SensorEntityDescription( key="talit", name="Talit and Tefillin", icon="mdi:calendar-clock", + entity_registry_enabled_default=False, ), SensorEntityDescription( key="sunrise", @@ -82,41 +91,49 @@ TIME_SENSORS: tuple[SensorEntityDescription, ...] = ( key="gra_end_shma", name='Latest time for Shma Gr"a', icon="mdi:calendar-clock", + entity_registry_enabled_default=False, ), SensorEntityDescription( key="mga_end_shma", name='Latest time for Shma MG"A', icon="mdi:calendar-clock", + entity_registry_enabled_default=False, ), SensorEntityDescription( key="gra_end_tfila", name='Latest time for Tefilla Gr"a', icon="mdi:calendar-clock", + entity_registry_enabled_default=False, ), SensorEntityDescription( key="mga_end_tfila", name='Latest time for Tefilla MG"A', icon="mdi:calendar-clock", + entity_registry_enabled_default=False, ), SensorEntityDescription( key="midday", name="Chatzot Hayom", icon="mdi:calendar-clock", + entity_registry_enabled_default=False, ), SensorEntityDescription( key="big_mincha", name="Mincha Gedola", icon="mdi:calendar-clock", + entity_registry_enabled_default=False, ), SensorEntityDescription( key="small_mincha", name="Mincha Ketana", icon="mdi:calendar-clock", + entity_registry_enabled_default=False, ), SensorEntityDescription( key="plag_mincha", name="Plag Hamincha", icon="mdi:weather-sunset-down", + entity_registry_enabled_default=False, ), SensorEntityDescription( key="sunset", @@ -127,21 +144,25 @@ TIME_SENSORS: tuple[SensorEntityDescription, ...] = ( key="first_stars", name="T'set Hakochavim", icon="mdi:weather-night", + entity_registry_enabled_default=False, ), SensorEntityDescription( key="three_stars", name="T'set Hakochavim, 3 stars", icon="mdi:weather-night", + entity_registry_enabled_default=False, ), SensorEntityDescription( key="upcoming_shabbat_candle_lighting", name="Upcoming Shabbat Candle Lighting", icon="mdi:candle", + entity_registry_enabled_default=False, ), SensorEntityDescription( key="upcoming_shabbat_havdalah", name="Upcoming Shabbat Havdalah", icon="mdi:weather-night", + entity_registry_enabled_default=False, ), SensorEntityDescription( key="upcoming_candle_lighting", @@ -178,6 +199,8 @@ async def async_setup_entry( class JewishCalendarSensor(SensorEntity): """Representation of an Jewish calendar sensor.""" + _attr_entity_category = EntityCategory.DIAGNOSTIC + def __init__( self, entry_id: str, diff --git a/tests/components/jewish_calendar/test_sensor.py b/tests/components/jewish_calendar/test_sensor.py index 509e17017d5..e2f7cf25244 100644 --- a/tests/components/jewish_calendar/test_sensor.py +++ b/tests/components/jewish_calendar/test_sensor.py @@ -201,6 +201,7 @@ TEST_IDS = [ TEST_PARAMS, ids=TEST_IDS, ) +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_jewish_calendar_sensor( hass: HomeAssistant, now, @@ -541,6 +542,7 @@ SHABBAT_TEST_IDS = [ SHABBAT_PARAMS, ids=SHABBAT_TEST_IDS, ) +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_shabbat_times_sensor( hass: HomeAssistant, language, @@ -617,6 +619,7 @@ OMER_TEST_IDS = [ @pytest.mark.parametrize(("test_time", "result"), OMER_PARAMS, ids=OMER_TEST_IDS) +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_omer_sensor(hass: HomeAssistant, test_time, result) -> None: """Test Omer Count sensor output.""" test_time = test_time.replace(tzinfo=dt_util.get_time_zone(hass.config.time_zone)) @@ -651,6 +654,7 @@ DAFYOMI_TEST_IDS = [ @pytest.mark.parametrize(("test_time", "result"), DAFYOMI_PARAMS, ids=DAFYOMI_TEST_IDS) +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_dafyomi_sensor(hass: HomeAssistant, test_time, result) -> None: """Test Daf Yomi sensor output.""" test_time = test_time.replace(tzinfo=dt_util.get_time_zone(hass.config.time_zone)) From f1fd52bc306d07576dc1726cdc2cb581b5497e4c Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Sun, 23 Jun 2024 17:37:08 +0200 Subject: [PATCH 1055/1445] Fix issue in mqtt fixture calling disconnect handler (#120246) --- tests/components/mqtt/test_init.py | 67 +++++++----------------------- tests/conftest.py | 1 + 2 files changed, 15 insertions(+), 53 deletions(-) diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index cd710ba610e..264f80f48f8 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -1111,9 +1111,7 @@ async def test_subscribe_and_resubscribe( record_calls: MessageCallbackType, ) -> None: """Test resubscribing within the debounce time.""" - mqtt_mock = await mqtt_mock_entry() - # Fake that the client is connected - mqtt_mock().connected = True + await mqtt_mock_entry() unsub = await mqtt.async_subscribe(hass, "test-topic", record_calls) # This unsub will be un-done with the following subscribe @@ -1452,10 +1450,7 @@ async def test_subscribe_same_topic( When subscribing to the same topic again, SUBSCRIBE must be sent to the broker again for it to resend any retained messages. """ - mqtt_mock = await mqtt_mock_entry() - - # Fake that the client is connected - mqtt_mock().connected = True + await mqtt_mock_entry() calls_a: list[ReceiveMessage] = [] calls_b: list[ReceiveMessage] = [] @@ -1506,10 +1501,7 @@ async def test_replaying_payload_same_topic( Retained messages must only be replayed for new subscriptions, except when the MQTT client is reconnecting. """ - mqtt_mock = await mqtt_mock_entry() - - # Fake that the client is connected - mqtt_mock().connected = True + await mqtt_mock_entry() calls_a: list[ReceiveMessage] = [] calls_b: list[ReceiveMessage] = [] @@ -1613,10 +1605,7 @@ async def test_replaying_payload_after_resubscribing( Retained messages must only be replayed for new subscriptions, except when the MQTT client is reconnection. """ - mqtt_mock = await mqtt_mock_entry() - - # Fake that the client is connected - mqtt_mock().connected = True + await mqtt_mock_entry() calls_a: list[ReceiveMessage] = [] @@ -1677,10 +1666,7 @@ async def test_replaying_payload_wildcard_topic( Retained messages should only be replayed for new subscriptions, except when the MQTT client is reconnection. """ - mqtt_mock = await mqtt_mock_entry() - - # Fake that the client is connected - mqtt_mock().connected = True + await mqtt_mock_entry() calls_a: list[ReceiveMessage] = [] calls_b: list[ReceiveMessage] = [] @@ -1759,10 +1745,7 @@ async def test_not_calling_unsubscribe_with_active_subscribers( record_calls: MessageCallbackType, ) -> None: """Test not calling unsubscribe() when other subscribers are active.""" - mqtt_mock = await mqtt_mock_entry() - # Fake that the client is connected - mqtt_mock().connected = True - + await mqtt_mock_entry() unsub = await mqtt.async_subscribe(hass, "test/state", record_calls, 2) await mqtt.async_subscribe(hass, "test/state", record_calls, 1) await hass.async_block_till_done() @@ -1787,10 +1770,8 @@ async def test_not_calling_subscribe_when_unsubscribed_within_cooldown( Make sure subscriptions are cleared if unsubscribed before the subscribe cool down period has ended. """ - mqtt_mock = await mqtt_mock_entry() + await mqtt_mock_entry() mqtt_client_mock.subscribe.reset_mock() - # Fake that the client is connected - mqtt_mock().connected = True unsub = await mqtt.async_subscribe(hass, "test/state", record_calls) unsub() @@ -1808,9 +1789,7 @@ async def test_unsubscribe_race( mqtt_mock_entry: MqttMockHAClientGenerator, ) -> None: """Test not calling unsubscribe() when other subscribers are active.""" - mqtt_mock = await mqtt_mock_entry() - # Fake that the client is connected - mqtt_mock().connected = True + await mqtt_mock_entry() await hass.async_block_till_done() async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) # cooldown await hass.async_block_till_done() @@ -1871,9 +1850,7 @@ async def test_restore_subscriptions_on_reconnect( record_calls: MessageCallbackType, ) -> None: """Test subscriptions are restored on reconnect.""" - mqtt_mock = await mqtt_mock_entry() - # Fake that the client is connected - mqtt_mock().connected = True + await mqtt_mock_entry() await hass.async_block_till_done() async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) # cooldown await hass.async_block_till_done() @@ -1909,9 +1886,7 @@ async def test_restore_all_active_subscriptions_on_reconnect( freezer: FrozenDateTimeFactory, ) -> None: """Test active subscriptions are restored correctly on reconnect.""" - mqtt_mock = await mqtt_mock_entry() - # Fake that the client is connected - mqtt_mock().connected = True + await mqtt_mock_entry() unsub = await mqtt.async_subscribe(hass, "test/state", record_calls, qos=2) await mqtt.async_subscribe(hass, "test/state", record_calls, qos=1) @@ -1964,9 +1939,7 @@ async def test_subscribed_at_highest_qos( freezer: FrozenDateTimeFactory, ) -> None: """Test the highest qos as assigned when subscribing to the same topic.""" - mqtt_mock = await mqtt_mock_entry() - # Fake that the client is connected - mqtt_mock().connected = True + await mqtt_mock_entry() await mqtt.async_subscribe(hass, "test/state", record_calls, qos=0) await hass.async_block_till_done() @@ -2064,12 +2037,9 @@ async def test_canceling_debouncer_on_shutdown( ) -> None: """Test canceling the debouncer when HA shuts down.""" - mqtt_mock = await mqtt_mock_entry() + await mqtt_mock_entry() mqtt_client_mock.subscribe.reset_mock() - # Fake that the client is connected - mqtt_mock().connected = True - await mqtt.async_subscribe(hass, "test/state1", record_calls) async_fire_time_changed(hass, utcnow() + timedelta(seconds=0.2)) await hass.async_block_till_done() @@ -2879,9 +2849,7 @@ async def test_mqtt_subscribes_in_single_call( record_calls: MessageCallbackType, ) -> None: """Test bundled client subscription to topic.""" - mqtt_mock = await mqtt_mock_entry() - # Fake that the client is connected - mqtt_mock().connected = True + await mqtt_mock_entry() mqtt_client_mock.subscribe.reset_mock() await mqtt.async_subscribe(hass, "topic/test", record_calls) @@ -2919,9 +2887,7 @@ async def test_mqtt_subscribes_and_unsubscribes_in_chunks( record_calls: MessageCallbackType, ) -> None: """Test chunked client subscriptions.""" - mqtt_mock = await mqtt_mock_entry() - # Fake that the client is connected - mqtt_mock().connected = True + await mqtt_mock_entry() mqtt_client_mock.subscribe.reset_mock() unsub_tasks: list[CALLBACK_TYPE] = [] @@ -4177,9 +4143,6 @@ async def test_reload_config_entry( assert await hass.config_entries.async_reload(entry.entry_id) assert entry.state is ConfigEntryState.LOADED await hass.async_block_till_done() - # Assert the MQTT client was connected gracefully - with caplog.at_level(logging.INFO): - assert "Disconnected from MQTT server mock-broker:1883" in caplog.text assert (state := hass.states.get("sensor.test_manual1")) is not None assert state.attributes["friendly_name"] == "test_manual1_updated" @@ -4609,8 +4572,6 @@ async def test_client_sock_failure_after_connect( ) -> None: """Test handling the socket connected and disconnected.""" mqtt_mock = await mqtt_mock_entry() - # Fake that the client is connected - mqtt_mock().connected = True await hass.async_block_till_done() assert mqtt_mock.connected is True diff --git a/tests/conftest.py b/tests/conftest.py index 14e6f97d7c4..6aa370ae539 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -960,6 +960,7 @@ def mqtt_client_mock(hass: HomeAssistant) -> Generator[MqttMockPahoClient]: mock_client.subscribe.side_effect = _subscribe mock_client.unsubscribe.side_effect = _unsubscribe mock_client.publish.side_effect = _async_fire_mqtt_message + mock_client.loop_read.return_value = 0 yield mock_client From 9769dec44b304844c73a2024b00b77d435caef7d Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sun, 23 Jun 2024 17:45:43 +0200 Subject: [PATCH 1056/1445] Add number platform to AirGradient (#120247) * Add number entity * Add airgradient number entities * Fix --- .../components/airgradient/__init__.py | 2 +- .../components/airgradient/number.py | 130 ++++++++++++++++++ .../components/airgradient/strings.json | 8 ++ .../airgradient/snapshots/test_number.ambr | 113 +++++++++++++++ tests/components/airgradient/test_number.py | 101 ++++++++++++++ 5 files changed, 353 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/airgradient/number.py create mode 100644 tests/components/airgradient/snapshots/test_number.ambr create mode 100644 tests/components/airgradient/test_number.py diff --git a/homeassistant/components/airgradient/__init__.py b/homeassistant/components/airgradient/__init__.py index 91ee0a440a6..76e11c05527 100644 --- a/homeassistant/components/airgradient/__init__.py +++ b/homeassistant/components/airgradient/__init__.py @@ -15,7 +15,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import DOMAIN from .coordinator import AirGradientConfigCoordinator, AirGradientMeasurementCoordinator -PLATFORMS: list[Platform] = [Platform.SELECT, Platform.SENSOR] +PLATFORMS: list[Platform] = [Platform.NUMBER, Platform.SELECT, Platform.SENSOR] @dataclass diff --git a/homeassistant/components/airgradient/number.py b/homeassistant/components/airgradient/number.py new file mode 100644 index 00000000000..e065b76ed51 --- /dev/null +++ b/homeassistant/components/airgradient/number.py @@ -0,0 +1,130 @@ +"""Support for AirGradient number entities.""" + +from collections.abc import Awaitable, Callable +from dataclasses import dataclass + +from airgradient import AirGradientClient, Config +from airgradient.models import ConfigurationControl + +from homeassistant.components.number import ( + DOMAIN as NUMBER_DOMAIN, + NumberEntity, + NumberEntityDescription, +) +from homeassistant.const import PERCENTAGE, EntityCategory +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import AirGradientConfigEntry +from .const import DOMAIN +from .coordinator import AirGradientConfigCoordinator +from .entity import AirGradientEntity + + +@dataclass(frozen=True, kw_only=True) +class AirGradientNumberEntityDescription(NumberEntityDescription): + """Describes AirGradient number entity.""" + + value_fn: Callable[[Config], int] + set_value_fn: Callable[[AirGradientClient, int], Awaitable[None]] + + +DISPLAY_BRIGHTNESS = AirGradientNumberEntityDescription( + key="display_brightness", + translation_key="display_brightness", + entity_category=EntityCategory.CONFIG, + native_min_value=0, + native_max_value=100, + native_step=1, + native_unit_of_measurement=PERCENTAGE, + value_fn=lambda config: config.display_brightness, + set_value_fn=lambda client, value: client.set_display_brightness(value), +) + +LED_BAR_BRIGHTNESS = AirGradientNumberEntityDescription( + key="led_bar_brightness", + translation_key="led_bar_brightness", + entity_category=EntityCategory.CONFIG, + native_min_value=0, + native_max_value=100, + native_step=1, + native_unit_of_measurement=PERCENTAGE, + value_fn=lambda config: config.led_bar_brightness, + set_value_fn=lambda client, value: client.set_led_bar_brightness(value), +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: AirGradientConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up AirGradient number entities based on a config entry.""" + + model = entry.runtime_data.measurement.data.model + coordinator = entry.runtime_data.config + + added_entities = False + + @callback + def _async_check_entities() -> None: + nonlocal added_entities + + if ( + coordinator.data.configuration_control is ConfigurationControl.LOCAL + and not added_entities + ): + entities = [] + if "I" in model: + entities.append(AirGradientNumber(coordinator, DISPLAY_BRIGHTNESS)) + if "L" in model: + entities.append(AirGradientNumber(coordinator, LED_BAR_BRIGHTNESS)) + + async_add_entities(entities) + added_entities = True + elif ( + coordinator.data.configuration_control is not ConfigurationControl.LOCAL + and added_entities + ): + entity_registry = er.async_get(hass) + unique_ids = [ + f"{coordinator.serial_number}-{entity_description.key}" + for entity_description in (DISPLAY_BRIGHTNESS, LED_BAR_BRIGHTNESS) + ] + for unique_id in unique_ids: + if entity_id := entity_registry.async_get_entity_id( + NUMBER_DOMAIN, DOMAIN, unique_id + ): + entity_registry.async_remove(entity_id) + added_entities = False + + coordinator.async_add_listener(_async_check_entities) + _async_check_entities() + + +class AirGradientNumber(AirGradientEntity, NumberEntity): + """Defines an AirGradient number entity.""" + + entity_description: AirGradientNumberEntityDescription + coordinator: AirGradientConfigCoordinator + + def __init__( + self, + coordinator: AirGradientConfigCoordinator, + description: AirGradientNumberEntityDescription, + ) -> None: + """Initialize AirGradient number.""" + super().__init__(coordinator) + self.entity_description = description + self._attr_unique_id = f"{coordinator.serial_number}-{description.key}" + + @property + def native_value(self) -> int | None: + """Return the state of the number.""" + return self.entity_description.value_fn(self.coordinator.data) + + async def async_set_native_value(self, value: float) -> None: + """Set the selected value.""" + await self.entity_description.set_value_fn(self.coordinator.client, int(value)) + await self.coordinator.async_request_refresh() diff --git a/homeassistant/components/airgradient/strings.json b/homeassistant/components/airgradient/strings.json index f4b558cf31a..0ab80286570 100644 --- a/homeassistant/components/airgradient/strings.json +++ b/homeassistant/components/airgradient/strings.json @@ -24,6 +24,14 @@ } }, "entity": { + "number": { + "led_bar_brightness": { + "name": "LED bar brightness" + }, + "display_brightness": { + "name": "Display brightness" + } + }, "select": { "configuration_control": { "name": "Configuration source", diff --git a/tests/components/airgradient/snapshots/test_number.ambr b/tests/components/airgradient/snapshots/test_number.ambr new file mode 100644 index 00000000000..87df8757eeb --- /dev/null +++ b/tests/components/airgradient/snapshots/test_number.ambr @@ -0,0 +1,113 @@ +# serializer version: 1 +# name: test_all_entities[number.airgradient_display_brightness-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 100, + 'min': 0, + 'mode': , + 'step': 1, + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.airgradient_display_brightness', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Display brightness', + 'platform': 'airgradient', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'display_brightness', + 'unique_id': '84fce612f5b8-display_brightness', + 'unit_of_measurement': '%', + }) +# --- +# name: test_all_entities[number.airgradient_display_brightness-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Airgradient Display brightness', + 'max': 100, + 'min': 0, + 'mode': , + 'step': 1, + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'number.airgradient_display_brightness', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_all_entities[number.airgradient_led_bar_brightness-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 100, + 'min': 0, + 'mode': , + 'step': 1, + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.airgradient_led_bar_brightness', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'LED bar brightness', + 'platform': 'airgradient', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'led_bar_brightness', + 'unique_id': '84fce612f5b8-led_bar_brightness', + 'unit_of_measurement': '%', + }) +# --- +# name: test_all_entities[number.airgradient_led_bar_brightness-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Airgradient LED bar brightness', + 'max': 100, + 'min': 0, + 'mode': , + 'step': 1, + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'number.airgradient_led_bar_brightness', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '100', + }) +# --- diff --git a/tests/components/airgradient/test_number.py b/tests/components/airgradient/test_number.py new file mode 100644 index 00000000000..ba659829c50 --- /dev/null +++ b/tests/components/airgradient/test_number.py @@ -0,0 +1,101 @@ +"""Tests for the AirGradient button platform.""" + +from datetime import timedelta +from unittest.mock import AsyncMock, patch + +from airgradient import Config +from freezegun.api import FrozenDateTimeFactory +from syrupy import SnapshotAssertion + +from homeassistant.components.airgradient import DOMAIN +from homeassistant.components.number import ( + ATTR_VALUE, + DOMAIN as NUMBER_DOMAIN, + SERVICE_SET_VALUE, +) +from homeassistant.const import ATTR_ENTITY_ID, Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import setup_integration + +from tests.common import ( + MockConfigEntry, + async_fire_time_changed, + load_fixture, + snapshot_platform, +) + + +async def test_all_entities( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + mock_airgradient_client: AsyncMock, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, +) -> None: + """Test all entities.""" + with patch("homeassistant.components.airgradient.PLATFORMS", [Platform.NUMBER]): + await setup_integration(hass, mock_config_entry) + + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + +async def test_setting_value( + hass: HomeAssistant, + mock_airgradient_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test setting value.""" + await setup_integration(hass, mock_config_entry) + + await hass.services.async_call( + NUMBER_DOMAIN, + SERVICE_SET_VALUE, + service_data={ATTR_VALUE: 50}, + target={ATTR_ENTITY_ID: "number.airgradient_display_brightness"}, + blocking=True, + ) + mock_airgradient_client.set_display_brightness.assert_called_once() + + await hass.services.async_call( + NUMBER_DOMAIN, + SERVICE_SET_VALUE, + service_data={ATTR_VALUE: 50}, + target={ATTR_ENTITY_ID: "number.airgradient_led_bar_brightness"}, + blocking=True, + ) + mock_airgradient_client.set_led_bar_brightness.assert_called_once() + + +async def test_cloud_creates_no_number( + hass: HomeAssistant, + mock_cloud_airgradient_client: AsyncMock, + mock_config_entry: MockConfigEntry, + freezer: FrozenDateTimeFactory, +) -> None: + """Test cloud configuration control.""" + with patch("homeassistant.components.airgradient.PLATFORMS", [Platform.NUMBER]): + await setup_integration(hass, mock_config_entry) + + assert len(hass.states.async_all()) == 0 + + mock_cloud_airgradient_client.get_config.return_value = Config.from_json( + load_fixture("get_config_local.json", DOMAIN) + ) + + freezer.tick(timedelta(minutes=5)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 2 + + mock_cloud_airgradient_client.get_config.return_value = Config.from_json( + load_fixture("get_config_cloud.json", DOMAIN) + ) + + freezer.tick(timedelta(minutes=5)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 0 From 080d90b63a6fa805d913cc0fbc8f30e1f4ba30fc Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sun, 23 Jun 2024 17:48:54 +0200 Subject: [PATCH 1057/1445] Add airgradient param fixture (#120241) --- tests/components/airgradient/conftest.py | 17 +- ...ures.json => current_measures_indoor.json} | 0 .../airgradient/snapshots/test_init.ambr | 32 +- .../airgradient/snapshots/test_select.ambr | 20 +- .../airgradient/snapshots/test_sensor.ambr | 347 ++++++++++++++---- tests/components/airgradient/test_init.py | 2 +- tests/components/airgradient/test_select.py | 25 +- tests/components/airgradient/test_sensor.py | 4 +- 8 files changed, 334 insertions(+), 113 deletions(-) rename tests/components/airgradient/fixtures/{current_measures.json => current_measures_indoor.json} (100%) diff --git a/tests/components/airgradient/conftest.py b/tests/components/airgradient/conftest.py index c5cc46cc8eb..7ca1198ce5f 100644 --- a/tests/components/airgradient/conftest.py +++ b/tests/components/airgradient/conftest.py @@ -39,7 +39,7 @@ def mock_airgradient_client() -> Generator[AsyncMock]: client = mock_client.return_value client.host = "10.0.0.131" client.get_current_measures.return_value = Measures.from_json( - load_fixture("current_measures.json", DOMAIN) + load_fixture("current_measures_indoor.json", DOMAIN) ) client.get_config.return_value = Config.from_json( load_fixture("get_config_local.json", DOMAIN) @@ -47,10 +47,21 @@ def mock_airgradient_client() -> Generator[AsyncMock]: yield client +@pytest.fixture(params=["indoor", "outdoor"]) +def airgradient_devices( + mock_airgradient_client: AsyncMock, request: pytest.FixtureRequest +) -> Generator[AsyncMock]: + """Return a list of AirGradient devices.""" + mock_airgradient_client.get_current_measures.return_value = Measures.from_json( + load_fixture(f"current_measures_{request.param}.json", DOMAIN) + ) + return mock_airgradient_client + + @pytest.fixture def mock_new_airgradient_client( mock_airgradient_client: AsyncMock, -) -> Generator[AsyncMock]: +) -> AsyncMock: """Mock a new AirGradient client.""" mock_airgradient_client.get_config.return_value = Config.from_json( load_fixture("get_config.json", DOMAIN) @@ -61,7 +72,7 @@ def mock_new_airgradient_client( @pytest.fixture def mock_cloud_airgradient_client( mock_airgradient_client: AsyncMock, -) -> Generator[AsyncMock]: +) -> AsyncMock: """Mock a cloud AirGradient client.""" mock_airgradient_client.get_config.return_value = Config.from_json( load_fixture("get_config_cloud.json", DOMAIN) diff --git a/tests/components/airgradient/fixtures/current_measures.json b/tests/components/airgradient/fixtures/current_measures_indoor.json similarity index 100% rename from tests/components/airgradient/fixtures/current_measures.json rename to tests/components/airgradient/fixtures/current_measures_indoor.json diff --git a/tests/components/airgradient/snapshots/test_init.ambr b/tests/components/airgradient/snapshots/test_init.ambr index 7109f603c9d..7c2e6ce4f78 100644 --- a/tests/components/airgradient/snapshots/test_init.ambr +++ b/tests/components/airgradient/snapshots/test_init.ambr @@ -1,5 +1,5 @@ # serializer version: 1 -# name: test_device_info +# name: test_device_info[indoor] DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , @@ -29,3 +29,33 @@ 'via_device_id': None, }) # --- +# name: test_device_info[outdoor] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'airgradient', + '84fce612f5b8', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'AirGradient', + 'model': 'O-1PPT', + 'name': 'Airgradient', + 'name_by_user': None, + 'serial_number': '84fce60bec38', + 'suggested_area': None, + 'sw_version': '3.1.1', + 'via_device_id': None, + }) +# --- diff --git a/tests/components/airgradient/snapshots/test_select.ambr b/tests/components/airgradient/snapshots/test_select.ambr index d29c7d23923..409eae52225 100644 --- a/tests/components/airgradient/snapshots/test_select.ambr +++ b/tests/components/airgradient/snapshots/test_select.ambr @@ -1,5 +1,5 @@ # serializer version: 1 -# name: test_all_entities[select.airgradient_configuration_source-entry] +# name: test_all_entities[indoor][select.airgradient_configuration_source-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -37,7 +37,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_all_entities[select.airgradient_configuration_source-state] +# name: test_all_entities[indoor][select.airgradient_configuration_source-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Airgradient Configuration source', @@ -54,7 +54,7 @@ 'state': 'local', }) # --- -# name: test_all_entities[select.airgradient_display_pm_standard-entry] +# name: test_all_entities[indoor][select.airgradient_display_pm_standard-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -92,7 +92,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_all_entities[select.airgradient_display_pm_standard-state] +# name: test_all_entities[indoor][select.airgradient_display_pm_standard-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Airgradient Display PM standard', @@ -109,7 +109,7 @@ 'state': 'ugm3', }) # --- -# name: test_all_entities[select.airgradient_display_temperature_unit-entry] +# name: test_all_entities[indoor][select.airgradient_display_temperature_unit-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -147,7 +147,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_all_entities[select.airgradient_display_temperature_unit-state] +# name: test_all_entities[indoor][select.airgradient_display_temperature_unit-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Airgradient Display temperature unit', @@ -164,7 +164,7 @@ 'state': 'c', }) # --- -# name: test_all_entities[select.airgradient_led_bar_mode-entry] +# name: test_all_entities[indoor][select.airgradient_led_bar_mode-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -203,7 +203,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_all_entities[select.airgradient_led_bar_mode-state] +# name: test_all_entities[indoor][select.airgradient_led_bar_mode-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Airgradient LED bar mode', @@ -221,7 +221,7 @@ 'state': 'co2', }) # --- -# name: test_all_entities_outdoor[select.airgradient_configuration_source-entry] +# name: test_all_entities[outdoor][select.airgradient_configuration_source-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -259,7 +259,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_all_entities_outdoor[select.airgradient_configuration_source-state] +# name: test_all_entities[outdoor][select.airgradient_configuration_source-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Airgradient Configuration source', diff --git a/tests/components/airgradient/snapshots/test_sensor.ambr b/tests/components/airgradient/snapshots/test_sensor.ambr index b0e22e7a9af..e96d2be1004 100644 --- a/tests/components/airgradient/snapshots/test_sensor.ambr +++ b/tests/components/airgradient/snapshots/test_sensor.ambr @@ -1,5 +1,5 @@ # serializer version: 1 -# name: test_all_entities[sensor.airgradient_carbon_dioxide-entry] +# name: test_all_entities[indoor][sensor.airgradient_carbon_dioxide-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -34,7 +34,7 @@ 'unit_of_measurement': 'ppm', }) # --- -# name: test_all_entities[sensor.airgradient_carbon_dioxide-state] +# name: test_all_entities[indoor][sensor.airgradient_carbon_dioxide-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'carbon_dioxide', @@ -50,7 +50,7 @@ 'state': '778', }) # --- -# name: test_all_entities[sensor.airgradient_humidity-entry] +# name: test_all_entities[indoor][sensor.airgradient_humidity-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -85,7 +85,7 @@ 'unit_of_measurement': '%', }) # --- -# name: test_all_entities[sensor.airgradient_humidity-state] +# name: test_all_entities[indoor][sensor.airgradient_humidity-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'humidity', @@ -101,7 +101,7 @@ 'state': '48.0', }) # --- -# name: test_all_entities[sensor.airgradient_nox_index-entry] +# name: test_all_entities[indoor][sensor.airgradient_nox_index-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -136,7 +136,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_all_entities[sensor.airgradient_nox_index-state] +# name: test_all_entities[indoor][sensor.airgradient_nox_index-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Airgradient NOx index', @@ -150,7 +150,7 @@ 'state': '1', }) # --- -# name: test_all_entities[sensor.airgradient_pm0_3-entry] +# name: test_all_entities[indoor][sensor.airgradient_pm0_3-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -185,7 +185,7 @@ 'unit_of_measurement': 'particles/dL', }) # --- -# name: test_all_entities[sensor.airgradient_pm0_3-state] +# name: test_all_entities[indoor][sensor.airgradient_pm0_3-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Airgradient PM0.3', @@ -200,57 +200,7 @@ 'state': '270', }) # --- -# name: test_all_entities[sensor.airgradient_pm0_3_count-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.airgradient_pm0_3_count', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'PM0.3 count', - 'platform': 'airgradient', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'pm003_count', - 'unique_id': '84fce612f5b8-pm003', - 'unit_of_measurement': 'particles/dL', - }) -# --- -# name: test_all_entities[sensor.airgradient_pm0_3_count-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Airgradient PM0.3 count', - 'state_class': , - 'unit_of_measurement': 'particles/dL', - }), - 'context': , - 'entity_id': 'sensor.airgradient_pm0_3_count', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '270', - }) -# --- -# name: test_all_entities[sensor.airgradient_pm1-entry] +# name: test_all_entities[indoor][sensor.airgradient_pm1-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -285,7 +235,7 @@ 'unit_of_measurement': 'µg/m³', }) # --- -# name: test_all_entities[sensor.airgradient_pm1-state] +# name: test_all_entities[indoor][sensor.airgradient_pm1-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'pm1', @@ -301,7 +251,7 @@ 'state': '22', }) # --- -# name: test_all_entities[sensor.airgradient_pm10-entry] +# name: test_all_entities[indoor][sensor.airgradient_pm10-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -336,7 +286,7 @@ 'unit_of_measurement': 'µg/m³', }) # --- -# name: test_all_entities[sensor.airgradient_pm10-state] +# name: test_all_entities[indoor][sensor.airgradient_pm10-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'pm10', @@ -352,7 +302,7 @@ 'state': '41', }) # --- -# name: test_all_entities[sensor.airgradient_pm2_5-entry] +# name: test_all_entities[indoor][sensor.airgradient_pm2_5-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -387,7 +337,7 @@ 'unit_of_measurement': 'µg/m³', }) # --- -# name: test_all_entities[sensor.airgradient_pm2_5-state] +# name: test_all_entities[indoor][sensor.airgradient_pm2_5-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'pm25', @@ -403,7 +353,7 @@ 'state': '34', }) # --- -# name: test_all_entities[sensor.airgradient_raw_nox-entry] +# name: test_all_entities[indoor][sensor.airgradient_raw_nox-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -438,7 +388,7 @@ 'unit_of_measurement': 'ticks', }) # --- -# name: test_all_entities[sensor.airgradient_raw_nox-state] +# name: test_all_entities[indoor][sensor.airgradient_raw_nox-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Airgradient Raw NOx', @@ -453,7 +403,7 @@ 'state': '16931', }) # --- -# name: test_all_entities[sensor.airgradient_raw_voc-entry] +# name: test_all_entities[indoor][sensor.airgradient_raw_voc-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -488,7 +438,7 @@ 'unit_of_measurement': 'ticks', }) # --- -# name: test_all_entities[sensor.airgradient_raw_voc-state] +# name: test_all_entities[indoor][sensor.airgradient_raw_voc-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Airgradient Raw VOC', @@ -503,7 +453,7 @@ 'state': '31792', }) # --- -# name: test_all_entities[sensor.airgradient_signal_strength-entry] +# name: test_all_entities[indoor][sensor.airgradient_signal_strength-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -538,7 +488,7 @@ 'unit_of_measurement': 'dBm', }) # --- -# name: test_all_entities[sensor.airgradient_signal_strength-state] +# name: test_all_entities[indoor][sensor.airgradient_signal_strength-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'signal_strength', @@ -554,7 +504,7 @@ 'state': '-52', }) # --- -# name: test_all_entities[sensor.airgradient_temperature-entry] +# name: test_all_entities[indoor][sensor.airgradient_temperature-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -589,7 +539,7 @@ 'unit_of_measurement': , }) # --- -# name: test_all_entities[sensor.airgradient_temperature-state] +# name: test_all_entities[indoor][sensor.airgradient_temperature-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'temperature', @@ -605,7 +555,7 @@ 'state': '27.96', }) # --- -# name: test_all_entities[sensor.airgradient_voc_index-entry] +# name: test_all_entities[indoor][sensor.airgradient_voc_index-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -640,7 +590,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_all_entities[sensor.airgradient_voc_index-state] +# name: test_all_entities[indoor][sensor.airgradient_voc_index-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Airgradient VOC index', @@ -654,3 +604,252 @@ 'state': '99', }) # --- +# name: test_all_entities[outdoor][sensor.airgradient_nox_index-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.airgradient_nox_index', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'NOx index', + 'platform': 'airgradient', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'nitrogen_index', + 'unique_id': '84fce612f5b8-nitrogen_index', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[outdoor][sensor.airgradient_nox_index-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Airgradient NOx index', + 'state_class': , + }), + 'context': , + 'entity_id': 'sensor.airgradient_nox_index', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1', + }) +# --- +# name: test_all_entities[outdoor][sensor.airgradient_raw_nox-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.airgradient_raw_nox', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Raw NOx', + 'platform': 'airgradient', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'raw_nitrogen', + 'unique_id': '84fce612f5b8-nox_raw', + 'unit_of_measurement': 'ticks', + }) +# --- +# name: test_all_entities[outdoor][sensor.airgradient_raw_nox-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Airgradient Raw NOx', + 'state_class': , + 'unit_of_measurement': 'ticks', + }), + 'context': , + 'entity_id': 'sensor.airgradient_raw_nox', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '16359', + }) +# --- +# name: test_all_entities[outdoor][sensor.airgradient_raw_voc-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.airgradient_raw_voc', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Raw VOC', + 'platform': 'airgradient', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'raw_total_volatile_organic_component', + 'unique_id': '84fce612f5b8-tvoc_raw', + 'unit_of_measurement': 'ticks', + }) +# --- +# name: test_all_entities[outdoor][sensor.airgradient_raw_voc-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Airgradient Raw VOC', + 'state_class': , + 'unit_of_measurement': 'ticks', + }), + 'context': , + 'entity_id': 'sensor.airgradient_raw_voc', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '30802', + }) +# --- +# name: test_all_entities[outdoor][sensor.airgradient_signal_strength-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.airgradient_signal_strength', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Signal strength', + 'platform': 'airgradient', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '84fce612f5b8-signal_strength', + 'unit_of_measurement': 'dBm', + }) +# --- +# name: test_all_entities[outdoor][sensor.airgradient_signal_strength-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'signal_strength', + 'friendly_name': 'Airgradient Signal strength', + 'state_class': , + 'unit_of_measurement': 'dBm', + }), + 'context': , + 'entity_id': 'sensor.airgradient_signal_strength', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '-64', + }) +# --- +# name: test_all_entities[outdoor][sensor.airgradient_voc_index-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.airgradient_voc_index', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'VOC index', + 'platform': 'airgradient', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'total_volatile_organic_component_index', + 'unique_id': '84fce612f5b8-tvoc', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[outdoor][sensor.airgradient_voc_index-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Airgradient VOC index', + 'state_class': , + }), + 'context': , + 'entity_id': 'sensor.airgradient_voc_index', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '49', + }) +# --- diff --git a/tests/components/airgradient/test_init.py b/tests/components/airgradient/test_init.py index 273f425f4fc..408e6f5f3ba 100644 --- a/tests/components/airgradient/test_init.py +++ b/tests/components/airgradient/test_init.py @@ -16,7 +16,7 @@ from tests.common import MockConfigEntry async def test_device_info( hass: HomeAssistant, snapshot: SnapshotAssertion, - mock_airgradient_client: AsyncMock, + airgradient_devices: AsyncMock, mock_config_entry: MockConfigEntry, device_registry: dr.DeviceRegistry, ) -> None: diff --git a/tests/components/airgradient/test_select.py b/tests/components/airgradient/test_select.py index 986295bd245..84bf081af63 100644 --- a/tests/components/airgradient/test_select.py +++ b/tests/components/airgradient/test_select.py @@ -2,11 +2,10 @@ from unittest.mock import AsyncMock, patch -from airgradient import ConfigurationControl, Measures +from airgradient import ConfigurationControl import pytest from syrupy import SnapshotAssertion -from homeassistant.components.airgradient import DOMAIN from homeassistant.components.select import ( DOMAIN as SELECT_DOMAIN, SERVICE_SELECT_OPTION, @@ -18,14 +17,14 @@ from homeassistant.helpers import entity_registry as er from . import setup_integration -from tests.common import MockConfigEntry, load_fixture, snapshot_platform +from tests.common import MockConfigEntry, snapshot_platform @pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_all_entities( hass: HomeAssistant, snapshot: SnapshotAssertion, - mock_airgradient_client: AsyncMock, + airgradient_devices: AsyncMock, mock_config_entry: MockConfigEntry, entity_registry: er.EntityRegistry, ) -> None: @@ -36,24 +35,6 @@ async def test_all_entities( await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) -@pytest.mark.usefixtures("entity_registry_enabled_by_default") -async def test_all_entities_outdoor( - hass: HomeAssistant, - snapshot: SnapshotAssertion, - mock_airgradient_client: AsyncMock, - mock_config_entry: MockConfigEntry, - entity_registry: er.EntityRegistry, -) -> None: - """Test all entities.""" - mock_airgradient_client.get_current_measures.return_value = Measures.from_json( - load_fixture("current_measures_outdoor.json", DOMAIN) - ) - with patch("homeassistant.components.airgradient.PLATFORMS", [Platform.SELECT]): - await setup_integration(hass, mock_config_entry) - - await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) - - async def test_setting_value( hass: HomeAssistant, mock_airgradient_client: AsyncMock, diff --git a/tests/components/airgradient/test_sensor.py b/tests/components/airgradient/test_sensor.py index 65c96a0669f..c2e53ef4de2 100644 --- a/tests/components/airgradient/test_sensor.py +++ b/tests/components/airgradient/test_sensor.py @@ -27,7 +27,7 @@ from tests.common import ( async def test_all_entities( hass: HomeAssistant, snapshot: SnapshotAssertion, - mock_airgradient_client: AsyncMock, + airgradient_devices: AsyncMock, mock_config_entry: MockConfigEntry, entity_registry: er.EntityRegistry, ) -> None: @@ -53,7 +53,7 @@ async def test_create_entities( assert len(hass.states.async_all()) == 0 mock_airgradient_client.get_current_measures.return_value = Measures.from_json( - load_fixture("current_measures.json", DOMAIN) + load_fixture("current_measures_indoor.json", DOMAIN) ) freezer.tick(timedelta(minutes=1)) async_fire_time_changed(hass) From fa4b7f307878c20c6530676b672dab4cffea62e9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 23 Jun 2024 11:16:11 -0500 Subject: [PATCH 1058/1445] Bump yalexs to 6.4.1 (#120248) --- homeassistant/components/august/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/august/manifest.json b/homeassistant/components/august/manifest.json index 13658e7401d..a8f087e3acc 100644 --- a/homeassistant/components/august/manifest.json +++ b/homeassistant/components/august/manifest.json @@ -28,5 +28,5 @@ "documentation": "https://www.home-assistant.io/integrations/august", "iot_class": "cloud_push", "loggers": ["pubnub", "yalexs"], - "requirements": ["yalexs==6.4.0", "yalexs-ble==2.4.2"] + "requirements": ["yalexs==6.4.1", "yalexs-ble==2.4.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index d65267ea5f6..c7e395b89ef 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2942,7 +2942,7 @@ yalesmartalarmclient==0.3.9 yalexs-ble==2.4.2 # homeassistant.components.august -yalexs==6.4.0 +yalexs==6.4.1 # homeassistant.components.yeelight yeelight==0.7.14 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 02ec3650970..c14856fcb5b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2301,7 +2301,7 @@ yalesmartalarmclient==0.3.9 yalexs-ble==2.4.2 # homeassistant.components.august -yalexs==6.4.0 +yalexs==6.4.1 # homeassistant.components.yeelight yeelight==0.7.14 From 5fbb965624e048497b2ac22309982a8204152129 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 23 Jun 2024 11:35:58 -0500 Subject: [PATCH 1059/1445] Bump uiprotect to 3.1.8 (#120244) --- homeassistant/components/unifiprotect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index 15b8b5b4a1b..817d7c9c074 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -40,7 +40,7 @@ "integration_type": "hub", "iot_class": "local_push", "loggers": ["uiprotect", "unifi_discovery"], - "requirements": ["uiprotect==3.1.1", "unifi-discovery==1.1.8"], + "requirements": ["uiprotect==3.1.8", "unifi-discovery==1.1.8"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/requirements_all.txt b/requirements_all.txt index c7e395b89ef..ec94fe54fc9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2794,7 +2794,7 @@ twitchAPI==4.0.0 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==3.1.1 +uiprotect==3.1.8 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c14856fcb5b..d0ca8262a19 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2174,7 +2174,7 @@ twitchAPI==4.0.0 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==3.1.1 +uiprotect==3.1.8 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 From 7efd1079bd7bb78eadde2b03259e3bc93d5638cd Mon Sep 17 00:00:00 2001 From: Cyrill Raccaud Date: Sun, 23 Jun 2024 19:26:55 +0200 Subject: [PATCH 1060/1445] Add Swiss public transport via stations (#115891) * add via stations * bump minor version due to backwards incompatibility * better coverage of many via station options in unit tests * fix migration unit test for new minor version 1.3 * switch version bump to major and improve migration test * fixes * improve error messages * use placeholders for strings --- .../swiss_public_transport/__init__.py | 34 ++++--- .../swiss_public_transport/config_flow.py | 61 ++++++++----- .../swiss_public_transport/const.py | 8 +- .../swiss_public_transport/helper.py | 15 +++ .../swiss_public_transport/strings.json | 14 ++- .../test_config_flow.py | 58 +++++++++--- .../swiss_public_transport/test_init.py | 91 ++++++++++++++++--- 7 files changed, 216 insertions(+), 65 deletions(-) create mode 100644 homeassistant/components/swiss_public_transport/helper.py diff --git a/homeassistant/components/swiss_public_transport/__init__.py b/homeassistant/components/swiss_public_transport/__init__.py index 74a7d90cfb2..1242c95269e 100644 --- a/homeassistant/components/swiss_public_transport/__init__.py +++ b/homeassistant/components/swiss_public_transport/__init__.py @@ -14,8 +14,9 @@ from homeassistant.exceptions import ConfigEntryError, ConfigEntryNotReady from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.aiohttp_client import async_get_clientsession -from .const import CONF_DESTINATION, CONF_START, DOMAIN +from .const import CONF_DESTINATION, CONF_START, CONF_VIA, DOMAIN, PLACEHOLDERS from .coordinator import SwissPublicTransportDataUpdateCoordinator +from .helper import unique_id_from_config _LOGGER = logging.getLogger(__name__) @@ -33,19 +34,28 @@ async def async_setup_entry( destination = config[CONF_DESTINATION] session = async_get_clientsession(hass) - opendata = OpendataTransport(start, destination, session) + opendata = OpendataTransport(start, destination, session, via=config.get(CONF_VIA)) try: await opendata.async_get_data() except OpendataTransportConnectionError as e: raise ConfigEntryNotReady( - f"Timeout while connecting for entry '{start} {destination}'" + translation_domain=DOMAIN, + translation_key="request_timeout", + translation_placeholders={ + "config_title": entry.title, + "error": e, + }, ) from e except OpendataTransportError as e: raise ConfigEntryError( - f"Setup failed for entry '{start} {destination}' with invalid data, check " - "at http://transport.opendata.ch/examples/stationboard.html if your " - "station names are valid" + translation_domain=DOMAIN, + translation_key="invalid_data", + translation_placeholders={ + **PLACEHOLDERS, + "config_title": entry.title, + "error": e, + }, ) from e coordinator = SwissPublicTransportDataUpdateCoordinator(hass, opendata) @@ -72,15 +82,13 @@ async def async_migrate_entry( """Migrate config entry.""" _LOGGER.debug("Migrating from version %s", config_entry.version) - if config_entry.minor_version > 3: + if config_entry.version > 2: # This means the user has downgraded from a future version return False - if config_entry.minor_version == 1: + if config_entry.version == 1 and config_entry.minor_version == 1: # Remove wrongly registered devices and entries - new_unique_id = ( - f"{config_entry.data[CONF_START]} {config_entry.data[CONF_DESTINATION]}" - ) + new_unique_id = unique_id_from_config(config_entry.data) entity_registry = er.async_get(hass) device_registry = dr.async_get(hass) device_entries = dr.async_entries_for_config_entry( @@ -109,6 +117,10 @@ async def async_migrate_entry( config_entry, unique_id=new_unique_id, minor_version=2 ) + if config_entry.version < 2: + # Via stations now available, which are not backwards compatible if used, changes unique id + hass.config_entries.async_update_entry(config_entry, version=2, minor_version=1) + _LOGGER.debug( "Migration to version %s.%s successful", config_entry.version, diff --git a/homeassistant/components/swiss_public_transport/config_flow.py b/homeassistant/components/swiss_public_transport/config_flow.py index bb852efd211..74c6223f1d9 100644 --- a/homeassistant/components/swiss_public_transport/config_flow.py +++ b/homeassistant/components/swiss_public_transport/config_flow.py @@ -13,12 +13,24 @@ import voluptuous as vol from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.selector import ( + TextSelector, + TextSelectorConfig, + TextSelectorType, +) -from .const import CONF_DESTINATION, CONF_START, DOMAIN, PLACEHOLDERS +from .const import CONF_DESTINATION, CONF_START, CONF_VIA, DOMAIN, MAX_VIA, PLACEHOLDERS +from .helper import unique_id_from_config DATA_SCHEMA = vol.Schema( { vol.Required(CONF_START): cv.string, + vol.Optional(CONF_VIA): TextSelector( + TextSelectorConfig( + type=TextSelectorType.TEXT, + multiple=True, + ), + ), vol.Required(CONF_DESTINATION): cv.string, } ) @@ -29,8 +41,8 @@ _LOGGER = logging.getLogger(__name__) class SwissPublicTransportConfigFlow(ConfigFlow, domain=DOMAIN): """Swiss public transport config flow.""" - VERSION = 1 - MINOR_VERSION = 2 + VERSION = 2 + MINOR_VERSION = 1 async def async_step_user( self, user_input: dict[str, Any] | None = None @@ -38,29 +50,34 @@ class SwissPublicTransportConfigFlow(ConfigFlow, domain=DOMAIN): """Async user step to set up the connection.""" errors: dict[str, str] = {} if user_input is not None: - await self.async_set_unique_id( - f"{user_input[CONF_START]} {user_input[CONF_DESTINATION]}" - ) + unique_id = unique_id_from_config(user_input) + await self.async_set_unique_id(unique_id) self._abort_if_unique_id_configured() - session = async_get_clientsession(self.hass) - opendata = OpendataTransport( - user_input[CONF_START], user_input[CONF_DESTINATION], session - ) - try: - await opendata.async_get_data() - except OpendataTransportConnectionError: - errors["base"] = "cannot_connect" - except OpendataTransportError: - errors["base"] = "bad_config" - except Exception: - _LOGGER.exception("Unknown error") - errors["base"] = "unknown" + if CONF_VIA in user_input and len(user_input[CONF_VIA]) > MAX_VIA: + errors["base"] = "too_many_via_stations" else: - return self.async_create_entry( - title=f"{user_input[CONF_START]} {user_input[CONF_DESTINATION]}", - data=user_input, + session = async_get_clientsession(self.hass) + opendata = OpendataTransport( + user_input[CONF_START], + user_input[CONF_DESTINATION], + session, + via=user_input.get(CONF_VIA), ) + try: + await opendata.async_get_data() + except OpendataTransportConnectionError: + errors["base"] = "cannot_connect" + except OpendataTransportError: + errors["base"] = "bad_config" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unknown error") + errors["base"] = "unknown" + else: + return self.async_create_entry( + title=unique_id, + data=user_input, + ) return self.async_show_form( step_id="user", diff --git a/homeassistant/components/swiss_public_transport/const.py b/homeassistant/components/swiss_public_transport/const.py index 6ae3cc9fd2f..32b6427ced5 100644 --- a/homeassistant/components/swiss_public_transport/const.py +++ b/homeassistant/components/swiss_public_transport/const.py @@ -1,12 +1,16 @@ """Constants for the swiss_public_transport integration.""" +from typing import Final + DOMAIN = "swiss_public_transport" -CONF_DESTINATION = "to" -CONF_START = "from" +CONF_DESTINATION: Final = "to" +CONF_START: Final = "from" +CONF_VIA: Final = "via" DEFAULT_NAME = "Next Destination" +MAX_VIA = 5 SENSOR_CONNECTIONS_COUNT = 3 diff --git a/homeassistant/components/swiss_public_transport/helper.py b/homeassistant/components/swiss_public_transport/helper.py new file mode 100644 index 00000000000..af03f7ad193 --- /dev/null +++ b/homeassistant/components/swiss_public_transport/helper.py @@ -0,0 +1,15 @@ +"""Helper functions for swiss_public_transport.""" + +from types import MappingProxyType +from typing import Any + +from .const import CONF_DESTINATION, CONF_START, CONF_VIA + + +def unique_id_from_config(config: MappingProxyType[str, Any] | dict[str, Any]) -> str: + """Build a unique id from a config entry.""" + return f"{config[CONF_START]} {config[CONF_DESTINATION]}" + ( + " via " + ", ".join(config[CONF_VIA]) + if CONF_VIA in config and len(config[CONF_VIA]) > 0 + else "" + ) diff --git a/homeassistant/components/swiss_public_transport/strings.json b/homeassistant/components/swiss_public_transport/strings.json index 4732bb0f527..4f4bc0522fc 100644 --- a/homeassistant/components/swiss_public_transport/strings.json +++ b/homeassistant/components/swiss_public_transport/strings.json @@ -3,6 +3,7 @@ "error": { "cannot_connect": "Cannot connect to server", "bad_config": "Request failed due to bad config: Check at [stationboard]({stationboard_url}) if your station names are valid", + "too_many_via_stations": "Too many via stations, only up to 5 via stations are allowed per connection.", "unknown": "An unknown error was raised by python-opendata-transport" }, "abort": { @@ -15,9 +16,10 @@ "user": { "data": { "from": "Start station", - "to": "End station" + "to": "End station", + "via": "List of up to 5 via stations" }, - "description": "Provide start and end station for your connection\n\nCheck the [stationboard]({stationboard_url}) for valid stations.", + "description": "Provide start and end station for your connection,\nand optionally up to 5 via stations.\n\nCheck the [stationboard]({stationboard_url}) for valid stations.", "title": "Swiss Public Transport" } } @@ -46,5 +48,13 @@ "name": "Delay" } } + }, + "exceptions": { + "invalid_data": { + "message": "Setup failed for entry {config_title} with invalid data, check at the [stationboard]({stationboard_url}) if your station names are valid.\n{error}" + }, + "request_timeout": { + "message": "Timeout while connecting for entry {config_title}.\n{error}" + } } } diff --git a/tests/components/swiss_public_transport/test_config_flow.py b/tests/components/swiss_public_transport/test_config_flow.py index b728c87d4b0..027336e28a6 100644 --- a/tests/components/swiss_public_transport/test_config_flow.py +++ b/tests/components/swiss_public_transport/test_config_flow.py @@ -12,7 +12,10 @@ from homeassistant.components.swiss_public_transport import config_flow from homeassistant.components.swiss_public_transport.const import ( CONF_DESTINATION, CONF_START, + CONF_VIA, + MAX_VIA, ) +from homeassistant.components.swiss_public_transport.helper import unique_id_from_config from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -25,8 +28,36 @@ MOCK_DATA_STEP = { CONF_DESTINATION: "test_destination", } +MOCK_DATA_STEP_ONE_VIA = { + **MOCK_DATA_STEP, + CONF_VIA: ["via_station"], +} -async def test_flow_user_init_data_success(hass: HomeAssistant) -> None: +MOCK_DATA_STEP_MANY_VIA = { + **MOCK_DATA_STEP, + CONF_VIA: ["via_station_1", "via_station_2", "via_station_3"], +} + +MOCK_DATA_STEP_TOO_MANY_STATIONS = { + **MOCK_DATA_STEP, + CONF_VIA: MOCK_DATA_STEP_ONE_VIA[CONF_VIA] * (MAX_VIA + 1), +} + + +@pytest.mark.parametrize( + ("user_input", "config_title"), + [ + (MOCK_DATA_STEP, "test_start test_destination"), + (MOCK_DATA_STEP_ONE_VIA, "test_start test_destination via via_station"), + ( + MOCK_DATA_STEP_MANY_VIA, + "test_start test_destination via via_station_1, via_station_2, via_station_3", + ), + ], +) +async def test_flow_user_init_data_success( + hass: HomeAssistant, user_input, config_title +) -> None: """Test success response.""" result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, context={"source": "user"} @@ -47,25 +78,26 @@ async def test_flow_user_init_data_success(hass: HomeAssistant) -> None: ) result = await hass.config_entries.flow.async_configure( result["flow_id"], - user_input=MOCK_DATA_STEP, + user_input=user_input, ) - assert result["type"] is FlowResultType.CREATE_ENTRY - assert result["result"].title == "test_start test_destination" + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["result"].title == config_title - assert result["data"] == MOCK_DATA_STEP + assert result["data"] == user_input @pytest.mark.parametrize( - ("raise_error", "text_error"), + ("raise_error", "text_error", "user_input_error"), [ - (OpendataTransportConnectionError(), "cannot_connect"), - (OpendataTransportError(), "bad_config"), - (IndexError(), "unknown"), + (OpendataTransportConnectionError(), "cannot_connect", MOCK_DATA_STEP), + (OpendataTransportError(), "bad_config", MOCK_DATA_STEP), + (None, "too_many_via_stations", MOCK_DATA_STEP_TOO_MANY_STATIONS), + (IndexError(), "unknown", MOCK_DATA_STEP), ], ) async def test_flow_user_init_data_error_and_recover( - hass: HomeAssistant, raise_error, text_error + hass: HomeAssistant, raise_error, text_error, user_input_error ) -> None: """Test unknown errors.""" with patch( @@ -78,7 +110,7 @@ async def test_flow_user_init_data_error_and_recover( ) result = await hass.config_entries.flow.async_configure( result["flow_id"], - user_input=MOCK_DATA_STEP, + user_input=user_input_error, ) assert result["type"] is FlowResultType.FORM @@ -92,7 +124,7 @@ async def test_flow_user_init_data_error_and_recover( user_input=MOCK_DATA_STEP, ) - assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["result"].title == "test_start test_destination" assert result["data"] == MOCK_DATA_STEP @@ -104,7 +136,7 @@ async def test_flow_user_init_data_already_configured(hass: HomeAssistant) -> No entry = MockConfigEntry( domain=config_flow.DOMAIN, data=MOCK_DATA_STEP, - unique_id=f"{MOCK_DATA_STEP[CONF_START]} {MOCK_DATA_STEP[CONF_DESTINATION]}", + unique_id=unique_id_from_config(MOCK_DATA_STEP), ) entry.add_to_hass(hass) diff --git a/tests/components/swiss_public_transport/test_init.py b/tests/components/swiss_public_transport/test_init.py index e1b27cf5fe1..47360f93cf2 100644 --- a/tests/components/swiss_public_transport/test_init.py +++ b/tests/components/swiss_public_transport/test_init.py @@ -2,22 +2,32 @@ from unittest.mock import AsyncMock, patch +import pytest + from homeassistant.components.swiss_public_transport.const import ( CONF_DESTINATION, CONF_START, + CONF_VIA, DOMAIN, ) +from homeassistant.components.swiss_public_transport.helper import unique_id_from_config +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from tests.common import MockConfigEntry -MOCK_DATA_STEP = { +MOCK_DATA_STEP_BASE = { CONF_START: "test_start", CONF_DESTINATION: "test_destination", } +MOCK_DATA_STEP_VIA = { + **MOCK_DATA_STEP_BASE, + CONF_VIA: ["via_station"], +} + CONNECTIONS = [ { "departure": "2024-01-06T18:03:00+0100", @@ -46,19 +56,38 @@ CONNECTIONS = [ ] -async def test_migration_1_1_to_1_2( - hass: HomeAssistant, entity_registry: er.EntityRegistry +@pytest.mark.parametrize( + ( + "from_version", + "from_minor_version", + "config_data", + "overwrite_unique_id", + ), + [ + (1, 1, MOCK_DATA_STEP_BASE, "None_departure"), + (1, 2, MOCK_DATA_STEP_BASE, None), + (2, 1, MOCK_DATA_STEP_VIA, None), + ], +) +async def test_migration_from( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + from_version, + from_minor_version, + config_data, + overwrite_unique_id, ) -> None: """Test successful setup.""" - config_entry_faulty = MockConfigEntry( + config_entry = MockConfigEntry( domain=DOMAIN, - data=MOCK_DATA_STEP, - title="MIGRATION_TEST", - version=1, - minor_version=1, + data=config_data, + title=f"MIGRATION_TEST from {from_version}.{from_minor_version}", + version=from_version, + minor_version=from_minor_version, + unique_id=overwrite_unique_id or unique_id_from_config(config_data), ) - config_entry_faulty.add_to_hass(hass) + config_entry.add_to_hass(hass) with patch( "homeassistant.components.swiss_public_transport.OpendataTransport", @@ -67,21 +96,53 @@ async def test_migration_1_1_to_1_2( mock().connections = CONNECTIONS # Setup the config entry - await hass.config_entries.async_setup(config_entry_faulty.entry_id) + unique_id = unique_id_from_config(config_entry.data) + await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() assert entity_registry.async_is_registered( entity_registry.entities.get_entity_id( - (Platform.SENSOR, DOMAIN, "test_start test_destination_departure") + ( + Platform.SENSOR, + DOMAIN, + f"{unique_id}_departure", + ) ) ) - # Check change in config entry - assert config_entry_faulty.minor_version == 2 - assert config_entry_faulty.unique_id == "test_start test_destination" + # Check change in config entry and verify most recent version + assert config_entry.version == 2 + assert config_entry.minor_version == 1 + assert config_entry.unique_id == unique_id - # Check "None" is gone + # Check "None" is gone from version 1.1 to 1.2 assert not entity_registry.async_is_registered( entity_registry.entities.get_entity_id( (Platform.SENSOR, DOMAIN, "None_departure") ) ) + + +async def test_migrate_error_from_future(hass: HomeAssistant) -> None: + """Test a future version isn't migrated.""" + + mock_entry = MockConfigEntry( + domain=DOMAIN, + version=3, + minor_version=1, + unique_id="some_crazy_future_unique_id", + data=MOCK_DATA_STEP_BASE, + ) + + mock_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.swiss_public_transport.OpendataTransport", + return_value=AsyncMock(), + ) as mock: + mock().connections = CONNECTIONS + + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + + entry = hass.config_entries.async_get_entry(mock_entry.entry_id) + assert entry.state is ConfigEntryState.MIGRATION_ERROR From 034b5e88e08dd9e906d0d6b98162be331cb46b94 Mon Sep 17 00:00:00 2001 From: Matthew FitzGerald-Chamberlain Date: Sun, 23 Jun 2024 12:47:09 -0500 Subject: [PATCH 1061/1445] Add Aprilaire air cleaning and fresh air functionality (#120174) * Add custom services for Aprilaire * Add icons.json * Use select/number entities instead of services * Remove unneeded consts * Remove number platform * Code review updates * Update homeassistant/components/aprilaire/strings.json Co-authored-by: Joost Lekkerkerker * Code review updates --------- Co-authored-by: Joost Lekkerkerker --- .coveragerc | 1 + .../components/aprilaire/__init__.py | 6 +- homeassistant/components/aprilaire/select.py | 153 ++++++++++++++++++ .../components/aprilaire/strings.json | 33 ++++ 4 files changed, 192 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/aprilaire/select.py diff --git a/.coveragerc b/.coveragerc index 003b4908b17..da3b7b91ece 100644 --- a/.coveragerc +++ b/.coveragerc @@ -87,6 +87,7 @@ omit = homeassistant/components/aprilaire/climate.py homeassistant/components/aprilaire/coordinator.py homeassistant/components/aprilaire/entity.py + homeassistant/components/aprilaire/select.py homeassistant/components/aprilaire/sensor.py homeassistant/components/apsystems/__init__.py homeassistant/components/apsystems/coordinator.py diff --git a/homeassistant/components/aprilaire/__init__.py b/homeassistant/components/aprilaire/__init__.py index ba310615567..9747a4d40a4 100644 --- a/homeassistant/components/aprilaire/__init__.py +++ b/homeassistant/components/aprilaire/__init__.py @@ -15,7 +15,11 @@ from homeassistant.helpers.device_registry import format_mac from .const import DOMAIN from .coordinator import AprilaireCoordinator -PLATFORMS: list[Platform] = [Platform.CLIMATE, Platform.SENSOR] +PLATFORMS: list[Platform] = [ + Platform.CLIMATE, + Platform.SELECT, + Platform.SENSOR, +] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/aprilaire/select.py b/homeassistant/components/aprilaire/select.py new file mode 100644 index 00000000000..504453f7463 --- /dev/null +++ b/homeassistant/components/aprilaire/select.py @@ -0,0 +1,153 @@ +"""The Aprilaire select component.""" + +from __future__ import annotations + +from collections.abc import Awaitable, Callable +from dataclasses import dataclass +from typing import cast + +from pyaprilaire.const import Attribute + +from homeassistant.components.select import SelectEntity, SelectEntityDescription +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .coordinator import AprilaireCoordinator +from .entity import BaseAprilaireEntity + +AIR_CLEANING_EVENT_MAP = {0: "off", 3: "event_clean", 4: "allergies"} +AIR_CLEANING_MODE_MAP = {0: "off", 1: "constant_clean", 2: "automatic"} +FRESH_AIR_EVENT_MAP = {0: "off", 2: "3hour", 3: "24hour"} +FRESH_AIR_MODE_MAP = {0: "off", 1: "automatic"} + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up Aprilaire select devices.""" + + coordinator: AprilaireCoordinator = hass.data[DOMAIN][config_entry.unique_id] + + assert config_entry.unique_id is not None + + descriptions: list[AprilaireSelectDescription] = [] + + if coordinator.data.get(Attribute.AIR_CLEANING_AVAILABLE) == 1: + descriptions.extend( + [ + AprilaireSelectDescription( + key="air_cleaning_event", + translation_key="air_cleaning_event", + options_map=AIR_CLEANING_EVENT_MAP, + event_value_key=Attribute.AIR_CLEANING_EVENT, + mode_value_key=Attribute.AIR_CLEANING_MODE, + is_event=True, + select_option_fn=coordinator.client.set_air_cleaning, + ), + AprilaireSelectDescription( + key="air_cleaning_mode", + translation_key="air_cleaning_mode", + options_map=AIR_CLEANING_MODE_MAP, + event_value_key=Attribute.AIR_CLEANING_EVENT, + mode_value_key=Attribute.AIR_CLEANING_MODE, + is_event=False, + select_option_fn=coordinator.client.set_air_cleaning, + ), + ] + ) + + if coordinator.data.get(Attribute.VENTILATION_AVAILABLE) == 1: + descriptions.extend( + [ + AprilaireSelectDescription( + key="fresh_air_event", + translation_key="fresh_air_event", + options_map=FRESH_AIR_EVENT_MAP, + event_value_key=Attribute.FRESH_AIR_EVENT, + mode_value_key=Attribute.FRESH_AIR_MODE, + is_event=True, + select_option_fn=coordinator.client.set_fresh_air, + ), + AprilaireSelectDescription( + key="fresh_air_mode", + translation_key="fresh_air_mode", + options_map=FRESH_AIR_MODE_MAP, + event_value_key=Attribute.FRESH_AIR_EVENT, + mode_value_key=Attribute.FRESH_AIR_MODE, + is_event=False, + select_option_fn=coordinator.client.set_fresh_air, + ), + ] + ) + + async_add_entities( + AprilaireSelectEntity(coordinator, description, config_entry.unique_id) + for description in descriptions + ) + + +@dataclass(frozen=True, kw_only=True) +class AprilaireSelectDescription(SelectEntityDescription): + """Class describing Aprilaire select entities.""" + + options_map: dict[int, str] + event_value_key: str + mode_value_key: str + is_event: bool + select_option_fn: Callable[[int, int], Awaitable] + + +class AprilaireSelectEntity(BaseAprilaireEntity, SelectEntity): + """Base select entity for Aprilaire.""" + + entity_description: AprilaireSelectDescription + + def __init__( + self, + coordinator: AprilaireCoordinator, + description: AprilaireSelectDescription, + unique_id: str, + ) -> None: + """Initialize a select for an Aprilaire device.""" + + self.entity_description = description + self.values_map = {v: k for k, v in description.options_map.items()} + + super().__init__(coordinator, unique_id) + + self._attr_options = list(description.options_map.values()) + + @property + def current_option(self) -> str: + """Get the current option.""" + + if self.entity_description.is_event: + value_key = self.entity_description.event_value_key + else: + value_key = self.entity_description.mode_value_key + + current_value = int(self.coordinator.data.get(value_key, 0)) + + return self.entity_description.options_map.get(current_value, "off") + + async def async_select_option(self, option: str) -> None: + """Set the current option.""" + + if self.entity_description.is_event: + event_value = self.values_map[option] + + mode_value = cast( + int, self.coordinator.data.get(self.entity_description.mode_value_key) + ) + else: + mode_value = self.values_map[option] + + event_value = cast( + int, self.coordinator.data.get(self.entity_description.event_value_key) + ) + + await self.entity_description.select_option_fn(mode_value, event_value) diff --git a/homeassistant/components/aprilaire/strings.json b/homeassistant/components/aprilaire/strings.json index 72005e0215c..0849f2255dd 100644 --- a/homeassistant/components/aprilaire/strings.json +++ b/homeassistant/components/aprilaire/strings.json @@ -24,6 +24,39 @@ "name": "Thermostat" } }, + "select": { + "air_cleaning_event": { + "name": "Air cleaning event", + "state": { + "off": "[%key:common::state::off%]", + "event_clean": "Event clean (3 hour)", + "allergies": "Allergies (24 hour)" + } + }, + "air_cleaning_mode": { + "name": "Air cleaning mode", + "state": { + "off": "[%key:common::state::off%]", + "constant_clean": "Constant clean", + "automatic": "Automatic" + } + }, + "fresh_air_event": { + "name": "Fresh air event", + "state": { + "off": "[%key:common::state::off%]", + "3hour": "3 hour event", + "24hour": "24 hour event" + } + }, + "fresh_air_mode": { + "name": "Fresh air mode", + "state": { + "off": "[%key:common::state::off%]", + "automatic": "[%key:component::aprilaire::entity::select::air_cleaning_mode::state::automatic%]" + } + } + }, "sensor": { "indoor_humidity_controlling_sensor": { "name": "Indoor humidity controlling sensor" From 29da88d8f6ee7d11b548094eb41837a2b16686de Mon Sep 17 00:00:00 2001 From: Tsvi Mostovicz Date: Sun, 23 Jun 2024 20:55:27 +0300 Subject: [PATCH 1062/1445] Create a Jewish Calendar entity (#120253) * Set has_entity_name to true * Move common code to jewish calendar service entity * Remove already existing assignment * Move data to common entity * Remove description name * Use config entry title instead of name for the device * Address comments --- .../jewish_calendar/binary_sensor.py | 33 ++++----------- .../components/jewish_calendar/entity.py | 41 +++++++++++++++++++ .../components/jewish_calendar/sensor.py | 33 ++++----------- .../jewish_calendar/test_binary_sensor.py | 3 ++ .../components/jewish_calendar/test_sensor.py | 13 ++++-- 5 files changed, 68 insertions(+), 55 deletions(-) create mode 100644 homeassistant/components/jewish_calendar/entity.py diff --git a/homeassistant/components/jewish_calendar/binary_sensor.py b/homeassistant/components/jewish_calendar/binary_sensor.py index 54080fcefd8..060650ee25c 100644 --- a/homeassistant/components/jewish_calendar/binary_sensor.py +++ b/homeassistant/components/jewish_calendar/binary_sensor.py @@ -6,7 +6,6 @@ from collections.abc import Callable from dataclasses import dataclass import datetime as dt from datetime import datetime -from typing import Any import hdate from hdate.zmanim import Zmanim @@ -16,18 +15,14 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_LANGUAGE, CONF_LOCATION, EntityCategory +from homeassistant.const import EntityCategory from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.helpers import event from homeassistant.helpers.entity_platform import AddEntitiesCallback import homeassistant.util.dt as dt_util -from .const import ( - CONF_CANDLE_LIGHT_MINUTES, - CONF_HAVDALAH_OFFSET_MINUTES, - DEFAULT_NAME, - DOMAIN, -) +from .const import DOMAIN +from .entity import JewishCalendarEntity @dataclass(frozen=True) @@ -75,33 +70,19 @@ async def async_setup_entry( entry = hass.data[DOMAIN][config_entry.entry_id] async_add_entities( - JewishCalendarBinarySensor(config_entry.entry_id, entry, description) + JewishCalendarBinarySensor(config_entry, entry, description) for description in BINARY_SENSORS ) -class JewishCalendarBinarySensor(BinarySensorEntity): +class JewishCalendarBinarySensor(JewishCalendarEntity, BinarySensorEntity): """Representation of an Jewish Calendar binary sensor.""" _attr_should_poll = False _attr_entity_category = EntityCategory.DIAGNOSTIC - entity_description: JewishCalendarBinarySensorEntityDescription + _update_unsub: CALLBACK_TYPE | None = None - def __init__( - self, - entry_id: str, - data: dict[str, Any], - description: JewishCalendarBinarySensorEntityDescription, - ) -> None: - """Initialize the binary sensor.""" - self.entity_description = description - self._attr_name = f"{DEFAULT_NAME} {description.name}" - self._attr_unique_id = f"{entry_id}-{description.key}" - self._location = data[CONF_LOCATION] - self._hebrew = data[CONF_LANGUAGE] == "hebrew" - self._candle_lighting_offset = data[CONF_CANDLE_LIGHT_MINUTES] - self._havdalah_offset = data[CONF_HAVDALAH_OFFSET_MINUTES] - self._update_unsub: CALLBACK_TYPE | None = None + entity_description: JewishCalendarBinarySensorEntityDescription @property def is_on(self) -> bool: diff --git a/homeassistant/components/jewish_calendar/entity.py b/homeassistant/components/jewish_calendar/entity.py new file mode 100644 index 00000000000..aba76599f63 --- /dev/null +++ b/homeassistant/components/jewish_calendar/entity.py @@ -0,0 +1,41 @@ +"""Entity representing a Jewish Calendar sensor.""" + +from typing import Any + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_LANGUAGE, CONF_LOCATION +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo +from homeassistant.helpers.entity import Entity, EntityDescription + +from .const import ( + CONF_CANDLE_LIGHT_MINUTES, + CONF_DIASPORA, + CONF_HAVDALAH_OFFSET_MINUTES, + DOMAIN, +) + + +class JewishCalendarEntity(Entity): + """An HA implementation for Jewish Calendar entity.""" + + _attr_has_entity_name = True + + def __init__( + self, + config_entry: ConfigEntry, + data: dict[str, Any], + description: EntityDescription, + ) -> None: + """Initialize a Jewish Calendar entity.""" + self.entity_description = description + self._attr_unique_id = f"{config_entry.entry_id}_{description.key}" + self._attr_device_info = DeviceInfo( + entry_type=DeviceEntryType.SERVICE, + identifiers={(DOMAIN, config_entry.entry_id)}, + name=config_entry.title, + ) + self._location = data[CONF_LOCATION] + self._hebrew = data[CONF_LANGUAGE] == "hebrew" + self._candle_lighting_offset = data[CONF_CANDLE_LIGHT_MINUTES] + self._havdalah_offset = data[CONF_HAVDALAH_OFFSET_MINUTES] + self._diaspora = data[CONF_DIASPORA] diff --git a/homeassistant/components/jewish_calendar/sensor.py b/homeassistant/components/jewish_calendar/sensor.py index 02a5da27119..87b4375b8b2 100644 --- a/homeassistant/components/jewish_calendar/sensor.py +++ b/homeassistant/components/jewish_calendar/sensor.py @@ -15,24 +15,14 @@ from homeassistant.components.sensor import ( SensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - CONF_LANGUAGE, - CONF_LOCATION, - SUN_EVENT_SUNSET, - EntityCategory, -) +from homeassistant.const import SUN_EVENT_SUNSET, EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.sun import get_astral_event_date import homeassistant.util.dt as dt_util -from .const import ( - CONF_CANDLE_LIGHT_MINUTES, - CONF_DIASPORA, - CONF_HAVDALAH_OFFSET_MINUTES, - DEFAULT_NAME, - DOMAIN, -) +from .const import DOMAIN +from .entity import JewishCalendarEntity _LOGGER = logging.getLogger(__name__) @@ -185,37 +175,30 @@ async def async_setup_entry( """Set up the Jewish calendar sensors .""" entry = hass.data[DOMAIN][config_entry.entry_id] sensors = [ - JewishCalendarSensor(config_entry.entry_id, entry, description) + JewishCalendarSensor(config_entry, entry, description) for description in INFO_SENSORS ] sensors.extend( - JewishCalendarTimeSensor(config_entry.entry_id, entry, description) + JewishCalendarTimeSensor(config_entry, entry, description) for description in TIME_SENSORS ) async_add_entities(sensors) -class JewishCalendarSensor(SensorEntity): +class JewishCalendarSensor(JewishCalendarEntity, SensorEntity): """Representation of an Jewish calendar sensor.""" _attr_entity_category = EntityCategory.DIAGNOSTIC def __init__( self, - entry_id: str, + config_entry: ConfigEntry, data: dict[str, Any], description: SensorEntityDescription, ) -> None: """Initialize the Jewish calendar sensor.""" - self.entity_description = description - self._attr_name = f"{DEFAULT_NAME} {description.name}" - self._attr_unique_id = f"{entry_id}-{description.key}" - self._location = data[CONF_LOCATION] - self._hebrew = data[CONF_LANGUAGE] == "hebrew" - self._candle_lighting_offset = data[CONF_CANDLE_LIGHT_MINUTES] - self._havdalah_offset = data[CONF_HAVDALAH_OFFSET_MINUTES] - self._diaspora = data[CONF_DIASPORA] + super().__init__(config_entry, data, description) self._attrs: dict[str, str] = {} async def async_update(self) -> None: diff --git a/tests/components/jewish_calendar/test_binary_sensor.py b/tests/components/jewish_calendar/test_binary_sensor.py index b60e7698266..8abaaecb77d 100644 --- a/tests/components/jewish_calendar/test_binary_sensor.py +++ b/tests/components/jewish_calendar/test_binary_sensor.py @@ -10,6 +10,7 @@ from homeassistant.components.jewish_calendar.const import ( CONF_CANDLE_LIGHT_MINUTES, CONF_DIASPORA, CONF_HAVDALAH_OFFSET_MINUTES, + DEFAULT_NAME, DOMAIN, ) from homeassistant.const import CONF_LANGUAGE, CONF_PLATFORM, STATE_OFF, STATE_ON @@ -192,6 +193,7 @@ async def test_issur_melacha_sensor( with alter_time(test_time): entry = MockConfigEntry( + title=DEFAULT_NAME, domain=DOMAIN, data={ CONF_LANGUAGE: "english", @@ -264,6 +266,7 @@ async def test_issur_melacha_sensor_update( with alter_time(test_time): entry = MockConfigEntry( + title=DEFAULT_NAME, domain=DOMAIN, data={ CONF_LANGUAGE: "english", diff --git a/tests/components/jewish_calendar/test_sensor.py b/tests/components/jewish_calendar/test_sensor.py index e2f7cf25244..cb054751f67 100644 --- a/tests/components/jewish_calendar/test_sensor.py +++ b/tests/components/jewish_calendar/test_sensor.py @@ -10,6 +10,7 @@ from homeassistant.components.jewish_calendar.const import ( CONF_CANDLE_LIGHT_MINUTES, CONF_DIASPORA, CONF_HAVDALAH_OFFSET_MINUTES, + DEFAULT_NAME, DOMAIN, ) from homeassistant.const import CONF_LANGUAGE, CONF_PLATFORM @@ -24,7 +25,7 @@ from tests.common import MockConfigEntry, async_fire_time_changed async def test_jewish_calendar_min_config(hass: HomeAssistant) -> None: """Test minimum jewish calendar configuration.""" - entry = MockConfigEntry(domain=DOMAIN, data={}) + entry = MockConfigEntry(title=DEFAULT_NAME, domain=DOMAIN, data={}) entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() @@ -33,7 +34,9 @@ async def test_jewish_calendar_min_config(hass: HomeAssistant) -> None: async def test_jewish_calendar_hebrew(hass: HomeAssistant) -> None: """Test jewish calendar sensor with language set to hebrew.""" - entry = MockConfigEntry(domain=DOMAIN, data={"language": "hebrew"}) + entry = MockConfigEntry( + title=DEFAULT_NAME, domain=DOMAIN, data={"language": "hebrew"} + ) entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() @@ -224,6 +227,7 @@ async def test_jewish_calendar_sensor( with alter_time(test_time): entry = MockConfigEntry( + title=DEFAULT_NAME, domain=DOMAIN, data={ CONF_LANGUAGE: language, @@ -565,6 +569,7 @@ async def test_shabbat_times_sensor( with alter_time(test_time): entry = MockConfigEntry( + title=DEFAULT_NAME, domain=DOMAIN, data={ CONF_LANGUAGE: language, @@ -625,7 +630,7 @@ async def test_omer_sensor(hass: HomeAssistant, test_time, result) -> None: test_time = test_time.replace(tzinfo=dt_util.get_time_zone(hass.config.time_zone)) with alter_time(test_time): - entry = MockConfigEntry(domain=DOMAIN) + entry = MockConfigEntry(title=DEFAULT_NAME, domain=DOMAIN) entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() @@ -660,7 +665,7 @@ async def test_dafyomi_sensor(hass: HomeAssistant, test_time, result) -> None: test_time = test_time.replace(tzinfo=dt_util.get_time_zone(hass.config.time_zone)) with alter_time(test_time): - entry = MockConfigEntry(domain=DOMAIN) + entry = MockConfigEntry(title=DEFAULT_NAME, domain=DOMAIN) entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() From 480ffeda2ceeedaadaf99c3e25a5d69246ca6170 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ab=C3=ADlio=20Costa?= Date: Sun, 23 Jun 2024 18:56:10 +0100 Subject: [PATCH 1063/1445] Remove connection state handling from Idasen Desk (#120242) --- .../components/idasen_desk/__init__.py | 2 +- .../components/idasen_desk/coordinator.py | 39 ++---------- .../components/idasen_desk/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/idasen_desk/test_init.py | 63 +++---------------- 6 files changed, 20 insertions(+), 90 deletions(-) diff --git a/homeassistant/components/idasen_desk/__init__.py b/homeassistant/components/idasen_desk/__init__.py index 1ea9b3b2f00..f0d8013cb50 100644 --- a/homeassistant/components/idasen_desk/__init__.py +++ b/homeassistant/components/idasen_desk/__init__.py @@ -68,7 +68,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) -> None: """Update from a Bluetooth callback to ensure that a new BLEDevice is fetched.""" _LOGGER.debug("Bluetooth callback triggered") - hass.async_create_task(coordinator.async_ensure_connection_state()) + hass.async_create_task(coordinator.async_connect_if_expected()) entry.async_on_unload( bluetooth.async_register_callback( diff --git a/homeassistant/components/idasen_desk/coordinator.py b/homeassistant/components/idasen_desk/coordinator.py index 5bdf1b37331..0661f2dede1 100644 --- a/homeassistant/components/idasen_desk/coordinator.py +++ b/homeassistant/components/idasen_desk/coordinator.py @@ -2,13 +2,12 @@ from __future__ import annotations -import asyncio import logging from idasen_ha import Desk from homeassistant.components import bluetooth -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator _LOGGER = logging.getLogger(__name__) @@ -29,55 +28,29 @@ class IdasenDeskCoordinator(DataUpdateCoordinator[int | None]): super().__init__(hass, logger, name=name) self._address = address self._expected_connected = False - self._connection_lost = False - self._disconnect_lock = asyncio.Lock() self.desk = Desk(self.async_set_updated_data) async def async_connect(self) -> bool: """Connect to desk.""" _LOGGER.debug("Trying to connect %s", self._address) + self._expected_connected = True ble_device = bluetooth.async_ble_device_from_address( self.hass, self._address, connectable=True ) if ble_device is None: _LOGGER.debug("No BLEDevice for %s", self._address) return False - self._expected_connected = True await self.desk.connect(ble_device) return True async def async_disconnect(self) -> None: """Disconnect from desk.""" - _LOGGER.debug("Disconnecting from %s", self._address) self._expected_connected = False - self._connection_lost = False + _LOGGER.debug("Disconnecting from %s", self._address) await self.desk.disconnect() - async def async_ensure_connection_state(self) -> None: - """Check if the expected connection state matches the current state. - - If the expected and current state don't match, calls connect/disconnect - as needed. - """ + async def async_connect_if_expected(self) -> None: + """Ensure that the desk is connected if that is the expected state.""" if self._expected_connected: - if not self.desk.is_connected: - _LOGGER.debug("Desk disconnected. Reconnecting") - self._connection_lost = True - await self.async_connect() - elif self._connection_lost: - _LOGGER.info("Reconnected to desk") - self._connection_lost = False - elif self.desk.is_connected: - if self._disconnect_lock.locked(): - _LOGGER.debug("Already disconnecting") - return - async with self._disconnect_lock: - _LOGGER.debug("Desk is connected but should not be. Disconnecting") - await self.desk.disconnect() - - @callback - def async_set_updated_data(self, data: int | None) -> None: - """Handle data update.""" - self.hass.async_create_task(self.async_ensure_connection_state()) - return super().async_set_updated_data(data) + await self.async_connect() diff --git a/homeassistant/components/idasen_desk/manifest.json b/homeassistant/components/idasen_desk/manifest.json index a912fabfa54..a09d155b5b0 100644 --- a/homeassistant/components/idasen_desk/manifest.json +++ b/homeassistant/components/idasen_desk/manifest.json @@ -12,5 +12,5 @@ "documentation": "https://www.home-assistant.io/integrations/idasen_desk", "iot_class": "local_push", "quality_scale": "silver", - "requirements": ["idasen-ha==2.5.3"] + "requirements": ["idasen-ha==2.6.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index ec94fe54fc9..1f4ada7f499 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1140,7 +1140,7 @@ ical==8.0.1 icmplib==3.0 # homeassistant.components.idasen_desk -idasen-ha==2.5.3 +idasen-ha==2.6.1 # homeassistant.components.network ifaddr==0.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d0ca8262a19..65de7d41374 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -936,7 +936,7 @@ ical==8.0.1 icmplib==3.0 # homeassistant.components.idasen_desk -idasen-ha==2.5.3 +idasen-ha==2.6.1 # homeassistant.components.network ifaddr==0.2.0 diff --git a/tests/components/idasen_desk/test_init.py b/tests/components/idasen_desk/test_init.py index 60f1fb3e5e3..ae7bd5e3fdf 100644 --- a/tests/components/idasen_desk/test_init.py +++ b/tests/components/idasen_desk/test_init.py @@ -1,6 +1,5 @@ """Test the IKEA Idasen Desk init.""" -import asyncio from unittest import mock from unittest.mock import AsyncMock, MagicMock @@ -66,63 +65,21 @@ async def test_reconnect_on_bluetooth_callback( mock_desk_api.connect.assert_called_once() mock_register_callback.assert_called_once() - mock_desk_api.is_connected = False _, register_callback_args, _ = mock_register_callback.mock_calls[0] bt_callback = register_callback_args[1] + + mock_desk_api.connect.reset_mock() bt_callback(None, None) await hass.async_block_till_done() - assert mock_desk_api.connect.call_count == 2 + mock_desk_api.connect.assert_called_once() - -async def test_duplicated_disconnect_is_no_op( - hass: HomeAssistant, mock_desk_api: MagicMock -) -> None: - """Test that calling disconnect while disconnecting is a no-op.""" - await init_integration(hass) - - await hass.services.async_call( - "button", "press", {"entity_id": "button.test_disconnect"}, blocking=True - ) - await hass.async_block_till_done() - - async def mock_disconnect(): - await asyncio.sleep(0) - - mock_desk_api.disconnect.reset_mock() - mock_desk_api.disconnect.side_effect = mock_disconnect - - # Since the disconnect button was pressed but the desk indicates "connected", - # any update event will call disconnect() - mock_desk_api.is_connected = True - mock_desk_api.trigger_update_callback(None) - mock_desk_api.trigger_update_callback(None) - mock_desk_api.trigger_update_callback(None) - await hass.async_block_till_done() - mock_desk_api.disconnect.assert_called_once() - - -async def test_ensure_connection_state( - hass: HomeAssistant, mock_desk_api: MagicMock -) -> None: - """Test that the connection state is ensured.""" - await init_integration(hass) - - mock_desk_api.connect.reset_mock() - mock_desk_api.is_connected = False - mock_desk_api.trigger_update_callback(None) - await hass.async_block_till_done() - mock_desk_api.connect.assert_called_once() - - await hass.services.async_call( - "button", "press", {"entity_id": "button.test_disconnect"}, blocking=True - ) - await hass.async_block_till_done() - - mock_desk_api.disconnect.reset_mock() - mock_desk_api.is_connected = True - mock_desk_api.trigger_update_callback(None) - await hass.async_block_till_done() - mock_desk_api.disconnect.assert_called_once() + mock_desk_api.connect.reset_mock() + await hass.services.async_call( + "button", "press", {"entity_id": "button.test_disconnect"}, blocking=True + ) + bt_callback(None, None) + await hass.async_block_till_done() + assert mock_desk_api.connect.call_count == 0 async def test_unload_entry(hass: HomeAssistant, mock_desk_api: MagicMock) -> None: From 55a2645e78ae1509c480e64feb2a5c822ef312ba Mon Sep 17 00:00:00 2001 From: Steven Looman Date: Sun, 23 Jun 2024 21:21:56 +0200 Subject: [PATCH 1064/1445] Bump async_upnp_client to 0.39.0 (#120250) --- homeassistant/components/dlna_dmr/manifest.json | 2 +- homeassistant/components/dlna_dms/manifest.json | 2 +- homeassistant/components/samsungtv/manifest.json | 2 +- homeassistant/components/ssdp/manifest.json | 2 +- homeassistant/components/upnp/device.py | 15 +++++---------- homeassistant/components/upnp/manifest.json | 2 +- homeassistant/components/yeelight/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/dlna_dmr/conftest.py | 1 + tests/components/dlna_dmr/test_config_flow.py | 2 ++ tests/components/upnp/conftest.py | 11 +++++------ tests/components/upnp/test_binary_sensor.py | 11 +++++------ tests/components/upnp/test_sensor.py | 11 +++++------ 15 files changed, 32 insertions(+), 37 deletions(-) diff --git a/homeassistant/components/dlna_dmr/manifest.json b/homeassistant/components/dlna_dmr/manifest.json index ebbab957700..963a22850df 100644 --- a/homeassistant/components/dlna_dmr/manifest.json +++ b/homeassistant/components/dlna_dmr/manifest.json @@ -8,7 +8,7 @@ "documentation": "https://www.home-assistant.io/integrations/dlna_dmr", "iot_class": "local_push", "loggers": ["async_upnp_client"], - "requirements": ["async-upnp-client==0.38.3", "getmac==0.9.4"], + "requirements": ["async-upnp-client==0.39.0", "getmac==0.9.4"], "ssdp": [ { "deviceType": "urn:schemas-upnp-org:device:MediaRenderer:1", diff --git a/homeassistant/components/dlna_dms/manifest.json b/homeassistant/components/dlna_dms/manifest.json index c87e5e87779..e02326376b3 100644 --- a/homeassistant/components/dlna_dms/manifest.json +++ b/homeassistant/components/dlna_dms/manifest.json @@ -8,7 +8,7 @@ "documentation": "https://www.home-assistant.io/integrations/dlna_dms", "iot_class": "local_polling", "quality_scale": "platinum", - "requirements": ["async-upnp-client==0.38.3"], + "requirements": ["async-upnp-client==0.39.0"], "ssdp": [ { "deviceType": "urn:schemas-upnp-org:device:MediaServer:1", diff --git a/homeassistant/components/samsungtv/manifest.json b/homeassistant/components/samsungtv/manifest.json index 460e191828e..7d9a8a9a0a8 100644 --- a/homeassistant/components/samsungtv/manifest.json +++ b/homeassistant/components/samsungtv/manifest.json @@ -39,7 +39,7 @@ "samsungctl[websocket]==0.7.1", "samsungtvws[async,encrypted]==2.6.0", "wakeonlan==2.1.0", - "async-upnp-client==0.38.3" + "async-upnp-client==0.39.0" ], "ssdp": [ { diff --git a/homeassistant/components/ssdp/manifest.json b/homeassistant/components/ssdp/manifest.json index 5e549c31806..304ee4b6410 100644 --- a/homeassistant/components/ssdp/manifest.json +++ b/homeassistant/components/ssdp/manifest.json @@ -8,5 +8,5 @@ "iot_class": "local_push", "loggers": ["async_upnp_client"], "quality_scale": "internal", - "requirements": ["async-upnp-client==0.38.3"] + "requirements": ["async-upnp-client==0.39.0"] } diff --git a/homeassistant/components/upnp/device.py b/homeassistant/components/upnp/device.py index 0b9eecb1b15..bb0bcfc6a6e 100644 --- a/homeassistant/components/upnp/device.py +++ b/homeassistant/components/upnp/device.py @@ -154,14 +154,9 @@ class Device: async def async_get_data(self) -> dict[str, str | datetime | int | float | None]: """Get all data from device.""" _LOGGER.debug("Getting data for device: %s", self) - igd_state = await self._igd_device.async_get_traffic_and_status_data() - status_info = igd_state.status_info - if status_info is not None and not isinstance(status_info, BaseException): - wan_status = status_info.connection_status - router_uptime = status_info.uptime - else: - wan_status = None - router_uptime = None + igd_state = await self._igd_device.async_get_traffic_and_status_data( + force_poll=True + ) def get_value(value: Any) -> Any: if value is None or isinstance(value, BaseException): @@ -175,8 +170,8 @@ class Device: BYTES_SENT: get_value(igd_state.bytes_sent), PACKETS_RECEIVED: get_value(igd_state.packets_received), PACKETS_SENT: get_value(igd_state.packets_sent), - WAN_STATUS: wan_status, - ROUTER_UPTIME: router_uptime, + WAN_STATUS: get_value(igd_state.connection_status), + ROUTER_UPTIME: get_value(igd_state.uptime), ROUTER_IP: get_value(igd_state.external_ip_address), KIBIBYTES_PER_SEC_RECEIVED: igd_state.kibibytes_per_sec_received, KIBIBYTES_PER_SEC_SENT: igd_state.kibibytes_per_sec_sent, diff --git a/homeassistant/components/upnp/manifest.json b/homeassistant/components/upnp/manifest.json index 7d353a475c7..b2972fc7790 100644 --- a/homeassistant/components/upnp/manifest.json +++ b/homeassistant/components/upnp/manifest.json @@ -8,7 +8,7 @@ "integration_type": "device", "iot_class": "local_polling", "loggers": ["async_upnp_client"], - "requirements": ["async-upnp-client==0.38.3", "getmac==0.9.4"], + "requirements": ["async-upnp-client==0.39.0", "getmac==0.9.4"], "ssdp": [ { "st": "urn:schemas-upnp-org:device:InternetGatewayDevice:1" diff --git a/homeassistant/components/yeelight/manifest.json b/homeassistant/components/yeelight/manifest.json index e9f304d38cb..4c63ab79baf 100644 --- a/homeassistant/components/yeelight/manifest.json +++ b/homeassistant/components/yeelight/manifest.json @@ -17,7 +17,7 @@ "iot_class": "local_push", "loggers": ["async_upnp_client", "yeelight"], "quality_scale": "platinum", - "requirements": ["yeelight==0.7.14", "async-upnp-client==0.38.3"], + "requirements": ["yeelight==0.7.14", "async-upnp-client==0.39.0"], "zeroconf": [ { "type": "_miio._udp.local.", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 7dfec9e63b3..a21d89705e8 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -10,7 +10,7 @@ aiohttp_cors==0.7.0 aiozoneinfo==0.2.0 astral==2.2 async-interrupt==1.1.1 -async-upnp-client==0.38.3 +async-upnp-client==0.39.0 atomicwrites-homeassistant==1.4.1 attrs==23.2.0 awesomeversion==24.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 1f4ada7f499..4f80886a568 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -490,7 +490,7 @@ asterisk_mbox==0.5.0 # homeassistant.components.ssdp # homeassistant.components.upnp # homeassistant.components.yeelight -async-upnp-client==0.38.3 +async-upnp-client==0.39.0 # homeassistant.components.arve asyncarve==0.0.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 65de7d41374..bafff5f2f77 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -445,7 +445,7 @@ asterisk_mbox==0.5.0 # homeassistant.components.ssdp # homeassistant.components.upnp # homeassistant.components.yeelight -async-upnp-client==0.38.3 +async-upnp-client==0.39.0 # homeassistant.components.arve asyncarve==0.0.9 diff --git a/tests/components/dlna_dmr/conftest.py b/tests/components/dlna_dmr/conftest.py index 0d88009f58e..f470fbabc6f 100644 --- a/tests/components/dlna_dmr/conftest.py +++ b/tests/components/dlna_dmr/conftest.py @@ -72,6 +72,7 @@ def domain_data_mock(hass: HomeAssistant) -> Iterable[Mock]: service_id="urn:upnp-org:serviceId:RenderingControl", ), } + upnp_device.all_services = list(upnp_device.services.values()) seal(upnp_device) domain_data.upnp_factory.async_create_device.return_value = upnp_device diff --git a/tests/components/dlna_dmr/test_config_flow.py b/tests/components/dlna_dmr/test_config_flow.py index 765d65ff0b9..a91cd4744d9 100644 --- a/tests/components/dlna_dmr/test_config_flow.py +++ b/tests/components/dlna_dmr/test_config_flow.py @@ -238,7 +238,9 @@ async def test_user_flow_embedded_st( embedded_device.device_type = MOCK_DEVICE_TYPE embedded_device.name = MOCK_DEVICE_NAME embedded_device.services = upnp_device.services + embedded_device.all_services = upnp_device.all_services upnp_device.services = {} + upnp_device.all_services = [] upnp_device.all_devices.append(embedded_device) result = await hass.config_entries.flow.async_init( diff --git a/tests/components/upnp/conftest.py b/tests/components/upnp/conftest.py index 00e8db124f0..0bfcd062ac0 100644 --- a/tests/components/upnp/conftest.py +++ b/tests/components/upnp/conftest.py @@ -8,7 +8,7 @@ from unittest.mock import AsyncMock, MagicMock, PropertyMock, create_autospec, p from urllib.parse import urlparse from async_upnp_client.client import UpnpDevice -from async_upnp_client.profiles.igd import IgdDevice, IgdState, StatusInfo +from async_upnp_client.profiles.igd import IgdDevice, IgdState import pytest from homeassistant.components import ssdp @@ -87,16 +87,15 @@ def mock_igd_device(mock_async_create_device) -> IgdDevice: bytes_sent=0, packets_received=0, packets_sent=0, - status_info=StatusInfo( - "Connected", - "", - 10, - ), + connection_status="Connected", + last_connection_error="", + uptime=10, external_ip_address="8.9.10.11", kibibytes_per_sec_received=None, kibibytes_per_sec_sent=None, packets_per_sec_received=None, packets_per_sec_sent=None, + port_mapping_number_of_entries=0, ) with patch( diff --git a/tests/components/upnp/test_binary_sensor.py b/tests/components/upnp/test_binary_sensor.py index 3a800ca75b9..087cd9e9fb4 100644 --- a/tests/components/upnp/test_binary_sensor.py +++ b/tests/components/upnp/test_binary_sensor.py @@ -2,7 +2,7 @@ from datetime import datetime, timedelta -from async_upnp_client.profiles.igd import IgdDevice, IgdState, StatusInfo +from async_upnp_client.profiles.igd import IgdDevice, IgdState from homeassistant.components.upnp.const import DEFAULT_SCAN_INTERVAL from homeassistant.core import HomeAssistant @@ -27,16 +27,15 @@ async def test_upnp_binary_sensors( bytes_sent=0, packets_received=0, packets_sent=0, - status_info=StatusInfo( - "Disconnected", - "", - 40, - ), + connection_status="Disconnected", + last_connection_error="", + uptime=40, external_ip_address="8.9.10.11", kibibytes_per_sec_received=None, kibibytes_per_sec_sent=None, packets_per_sec_received=None, packets_per_sec_sent=None, + port_mapping_number_of_entries=0, ) async_fire_time_changed( diff --git a/tests/components/upnp/test_sensor.py b/tests/components/upnp/test_sensor.py index 7dfbb144b01..67a64b265d9 100644 --- a/tests/components/upnp/test_sensor.py +++ b/tests/components/upnp/test_sensor.py @@ -2,7 +2,7 @@ from datetime import datetime, timedelta -from async_upnp_client.profiles.igd import IgdDevice, IgdState, StatusInfo +from async_upnp_client.profiles.igd import IgdDevice, IgdState from homeassistant.components.upnp.const import DEFAULT_SCAN_INTERVAL from homeassistant.core import HomeAssistant @@ -35,16 +35,15 @@ async def test_upnp_sensors( bytes_sent=20480, packets_received=30, packets_sent=40, - status_info=StatusInfo( - "Disconnected", - "", - 40, - ), + connection_status="Disconnected", + last_connection_error="", + uptime=40, external_ip_address="", kibibytes_per_sec_received=10.0, kibibytes_per_sec_sent=20.0, packets_per_sec_received=30.0, packets_per_sec_sent=40.0, + port_mapping_number_of_entries=0, ) now = dt_util.utcnow() From 436c36e3ddb766b7d6a8d55b0e1aab02fe4d2bb4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 23 Jun 2024 15:20:46 -0500 Subject: [PATCH 1065/1445] Bump aioesphomeapi to 24.6.1 (#120261) --- homeassistant/components/esphome/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index de855e15d4c..ab175028bea 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -16,7 +16,7 @@ "loggers": ["aioesphomeapi", "noiseprotocol", "bleak_esphome"], "mqtt": ["esphome/discover/#"], "requirements": [ - "aioesphomeapi==24.6.0", + "aioesphomeapi==24.6.1", "esphome-dashboard-api==1.2.3", "bleak-esphome==1.0.0" ], diff --git a/requirements_all.txt b/requirements_all.txt index 4f80886a568..dbbfe7f6790 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -243,7 +243,7 @@ aioelectricitymaps==0.4.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==24.6.0 +aioesphomeapi==24.6.1 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bafff5f2f77..ddf9a87d7ee 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -222,7 +222,7 @@ aioelectricitymaps==0.4.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==24.6.0 +aioesphomeapi==24.6.1 # homeassistant.components.flo aioflo==2021.11.0 From 143e8d09af3aee6fd27c22506e767f5739f76259 Mon Sep 17 00:00:00 2001 From: Tsvi Mostovicz Date: Mon, 24 Jun 2024 00:04:16 +0300 Subject: [PATCH 1066/1445] Fix blocking call in Jewish Calendar while initializing location (#120265) --- .../components/jewish_calendar/__init__.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/jewish_calendar/__init__.py b/homeassistant/components/jewish_calendar/__init__.py index 81fe6cb5377..fd238e8d615 100644 --- a/homeassistant/components/jewish_calendar/__init__.py +++ b/homeassistant/components/jewish_calendar/__init__.py @@ -2,6 +2,8 @@ from __future__ import annotations +from functools import partial + from hdate import Location import voluptuous as vol @@ -129,13 +131,16 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b CONF_HAVDALAH_OFFSET_MINUTES, DEFAULT_HAVDALAH_OFFSET_MINUTES ) - location = Location( - name=hass.config.location_name, - diaspora=diaspora, - latitude=config_entry.data.get(CONF_LATITUDE, hass.config.latitude), - longitude=config_entry.data.get(CONF_LONGITUDE, hass.config.longitude), - altitude=config_entry.data.get(CONF_ELEVATION, hass.config.elevation), - timezone=config_entry.data.get(CONF_TIME_ZONE, hass.config.time_zone), + location = await hass.async_add_executor_job( + partial( + Location, + name=hass.config.location_name, + diaspora=diaspora, + latitude=config_entry.data.get(CONF_LATITUDE, hass.config.latitude), + longitude=config_entry.data.get(CONF_LONGITUDE, hass.config.longitude), + altitude=config_entry.data.get(CONF_ELEVATION, hass.config.elevation), + timezone=config_entry.data.get(CONF_TIME_ZONE, hass.config.time_zone), + ) ) hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = { From 19f97a3e53ed99e85fb3778521aa0e4358229fbc Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 23 Jun 2024 17:09:57 -0400 Subject: [PATCH 1067/1445] LLM to handle decimal attributes (#120257) --- homeassistant/helpers/llm.py | 5 ++++- tests/helpers/test_llm.py | 10 +++++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/homeassistant/helpers/llm.py b/homeassistant/helpers/llm.py index 53ec092fda2..6673786e2e1 100644 --- a/homeassistant/helpers/llm.py +++ b/homeassistant/helpers/llm.py @@ -4,6 +4,7 @@ from __future__ import annotations from abc import ABC, abstractmethod from dataclasses import dataclass +from decimal import Decimal from enum import Enum from functools import cache, partial from typing import Any @@ -461,7 +462,9 @@ def _get_exposed_entities( info["areas"] = ", ".join(area_names) if attributes := { - attr_name: str(attr_value) if isinstance(attr_value, Enum) else attr_value + attr_name: str(attr_value) + if isinstance(attr_value, (Enum, Decimal)) + else attr_value for attr_name, attr_value in state.attributes.items() if attr_name in interesting_attributes }: diff --git a/tests/helpers/test_llm.py b/tests/helpers/test_llm.py index e62d9ffdbee..5389490b401 100644 --- a/tests/helpers/test_llm.py +++ b/tests/helpers/test_llm.py @@ -1,5 +1,6 @@ """Tests for the llm helpers.""" +from decimal import Decimal from unittest.mock import patch import pytest @@ -402,7 +403,11 @@ async def test_assist_api_prompt( suggested_object_id="living_room", device_id=device.id, ) - hass.states.async_set(entry1.entity_id, "on", {"friendly_name": "Kitchen"}) + hass.states.async_set( + entry1.entity_id, + "on", + {"friendly_name": "Kitchen", "temperature": Decimal("0.9")}, + ) hass.states.async_set(entry2.entity_id, "on", {"friendly_name": "Living Room"}) def create_entity(device: dr.DeviceEntry, write_state=True) -> None: @@ -510,6 +515,9 @@ async def test_assist_api_prompt( entry1.entity_id: { "names": "Kitchen", "state": "on", + "attributes": { + "temperature": "0.9", + }, }, entry2.entity_id: { "areas": "Test Area, Alternative name", From 66b91a84f9209e0c58c944fbcc243b1b3cbc5f59 Mon Sep 17 00:00:00 2001 From: Danilo Bargen Date: Mon, 24 Jun 2024 03:39:58 +0200 Subject: [PATCH 1068/1445] mystrom: Add MAC and Config URL to devices (#120271) * Add MAC address to mystrom switch devices * Add configuration URL to mystrom switch devices --- homeassistant/components/mystrom/switch.py | 4 +++- tests/components/mystrom/__init__.py | 7 +++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/mystrom/switch.py b/homeassistant/components/mystrom/switch.py index 9958fcf7f01..af135027aac 100644 --- a/homeassistant/components/mystrom/switch.py +++ b/homeassistant/components/mystrom/switch.py @@ -10,7 +10,7 @@ from pymystrom.exceptions import MyStromConnectionError from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo, format_mac from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN, MANUFACTURER @@ -43,6 +43,8 @@ class MyStromSwitch(SwitchEntity): name=name, manufacturer=MANUFACTURER, sw_version=self.plug.firmware, + connections={("mac", format_mac(self.plug.mac))}, + configuration_url=self.plug.uri, ) async def async_turn_on(self, **kwargs: Any) -> None: diff --git a/tests/components/mystrom/__init__.py b/tests/components/mystrom/__init__.py index ac6ac1d8c54..8ee62996f92 100644 --- a/tests/components/mystrom/__init__.py +++ b/tests/components/mystrom/__init__.py @@ -173,3 +173,10 @@ class MyStromSwitchMock(MyStromDeviceMock): if not self._requested_state: return None return self._state["temperature"] + + @property + def uri(self) -> str | None: + """Return the URI.""" + if not self._requested_state: + return None + return f"http://{self._state["ip"]}" From d095d4e60d1ba7b0c4de814fc0f452d87ede878f Mon Sep 17 00:00:00 2001 From: "Mr. Bubbles" Date: Mon, 24 Jun 2024 07:53:15 +0200 Subject: [PATCH 1069/1445] Change suggested data rate unit to Mbit/s in pyLoad (#120275) Change data rate unit to Mbit/s --- homeassistant/components/pyload/sensor.py | 2 +- .../pyload/snapshots/test_sensor.ambr | 26 +++++++++---------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/pyload/sensor.py b/homeassistant/components/pyload/sensor.py index 8c35f8e7431..aa86dde9260 100644 --- a/homeassistant/components/pyload/sensor.py +++ b/homeassistant/components/pyload/sensor.py @@ -61,7 +61,7 @@ SENSOR_DESCRIPTIONS: tuple[SensorEntityDescription, ...] = ( translation_key=PyLoadSensorEntity.SPEED, device_class=SensorDeviceClass.DATA_RATE, native_unit_of_measurement=UnitOfDataRate.BYTES_PER_SECOND, - suggested_unit_of_measurement=UnitOfDataRate.MEGABYTES_PER_SECOND, + suggested_unit_of_measurement=UnitOfDataRate.MEGABITS_PER_SECOND, suggested_display_precision=1, ), ) diff --git a/tests/components/pyload/snapshots/test_sensor.ambr b/tests/components/pyload/snapshots/test_sensor.ambr index 77a79e3eddd..f1e42ea049c 100644 --- a/tests/components/pyload/snapshots/test_sensor.ambr +++ b/tests/components/pyload/snapshots/test_sensor.ambr @@ -24,7 +24,7 @@ 'suggested_display_precision': 1, }), 'sensor.private': dict({ - 'suggested_unit_of_measurement': , + 'suggested_unit_of_measurement': , }), }), 'original_device_class': , @@ -35,7 +35,7 @@ 'supported_features': 0, 'translation_key': , 'unique_id': 'XXXXXXXXXXXXXX_speed', - 'unit_of_measurement': , + 'unit_of_measurement': , }) # --- # name: test_sensor_update_exceptions[CannotConnect][sensor.pyload_speed-state] @@ -43,7 +43,7 @@ 'attributes': ReadOnlyDict({ 'device_class': 'data_rate', 'friendly_name': 'pyLoad Speed', - 'unit_of_measurement': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.pyload_speed', @@ -78,7 +78,7 @@ 'suggested_display_precision': 1, }), 'sensor.private': dict({ - 'suggested_unit_of_measurement': , + 'suggested_unit_of_measurement': , }), }), 'original_device_class': , @@ -89,7 +89,7 @@ 'supported_features': 0, 'translation_key': , 'unique_id': 'XXXXXXXXXXXXXX_speed', - 'unit_of_measurement': , + 'unit_of_measurement': , }) # --- # name: test_sensor_update_exceptions[InvalidAuth][sensor.pyload_speed-state] @@ -97,7 +97,7 @@ 'attributes': ReadOnlyDict({ 'device_class': 'data_rate', 'friendly_name': 'pyLoad Speed', - 'unit_of_measurement': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.pyload_speed', @@ -132,7 +132,7 @@ 'suggested_display_precision': 1, }), 'sensor.private': dict({ - 'suggested_unit_of_measurement': , + 'suggested_unit_of_measurement': , }), }), 'original_device_class': , @@ -143,7 +143,7 @@ 'supported_features': 0, 'translation_key': , 'unique_id': 'XXXXXXXXXXXXXX_speed', - 'unit_of_measurement': , + 'unit_of_measurement': , }) # --- # name: test_sensor_update_exceptions[ParserError][sensor.pyload_speed-state] @@ -151,7 +151,7 @@ 'attributes': ReadOnlyDict({ 'device_class': 'data_rate', 'friendly_name': 'pyLoad Speed', - 'unit_of_measurement': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.pyload_speed', @@ -363,7 +363,7 @@ 'suggested_display_precision': 1, }), 'sensor.private': dict({ - 'suggested_unit_of_measurement': , + 'suggested_unit_of_measurement': , }), }), 'original_device_class': , @@ -374,7 +374,7 @@ 'supported_features': 0, 'translation_key': , 'unique_id': 'XXXXXXXXXXXXXX_speed', - 'unit_of_measurement': , + 'unit_of_measurement': , }) # --- # name: test_setup[sensor.pyload_speed-state] @@ -382,13 +382,13 @@ 'attributes': ReadOnlyDict({ 'device_class': 'data_rate', 'friendly_name': 'pyLoad Speed', - 'unit_of_measurement': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.pyload_speed', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '5.405963', + 'state': '43.247704', }) # --- From fe3027f7de279d1708d4cefb02503f9bf0facb3d Mon Sep 17 00:00:00 2001 From: Thomas55555 <59625598+Thomas55555@users.noreply.github.com> Date: Mon, 24 Jun 2024 08:16:26 +0200 Subject: [PATCH 1070/1445] Adjust base entities in Husqvarna Automower (#120258) * adjust base entities * Adjust docstrings --- .../components/husqvarna_automower/button.py | 4 +-- .../components/husqvarna_automower/entity.py | 33 +++++++++++++++++-- .../husqvarna_automower/lawn_mower.py | 4 +-- .../components/husqvarna_automower/switch.py | 31 +---------------- 4 files changed, 35 insertions(+), 37 deletions(-) diff --git a/homeassistant/components/husqvarna_automower/button.py b/homeassistant/components/husqvarna_automower/button.py index 60c05b92a31..a9747108393 100644 --- a/homeassistant/components/husqvarna_automower/button.py +++ b/homeassistant/components/husqvarna_automower/button.py @@ -12,7 +12,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import AutomowerConfigEntry from .const import DOMAIN from .coordinator import AutomowerDataUpdateCoordinator -from .entity import AutomowerControlEntity +from .entity import AutomowerAvailableEntity _LOGGER = logging.getLogger(__name__) @@ -29,7 +29,7 @@ async def async_setup_entry( ) -class AutomowerButtonEntity(AutomowerControlEntity, ButtonEntity): +class AutomowerButtonEntity(AutomowerAvailableEntity, ButtonEntity): """Defining the AutomowerButtonEntity.""" _attr_translation_key = "confirm_error" diff --git a/homeassistant/components/husqvarna_automower/entity.py b/homeassistant/components/husqvarna_automower/entity.py index 4d20d2d677b..80a936c2caf 100644 --- a/homeassistant/components/husqvarna_automower/entity.py +++ b/homeassistant/components/husqvarna_automower/entity.py @@ -2,7 +2,7 @@ import logging -from aioautomower.model import MowerAttributes +from aioautomower.model import MowerActivities, MowerAttributes, MowerStates from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -12,6 +12,21 @@ from .const import DOMAIN _LOGGER = logging.getLogger(__name__) +ERROR_ACTIVITIES = ( + MowerActivities.STOPPED_IN_GARDEN, + MowerActivities.UNKNOWN, + MowerActivities.NOT_APPLICABLE, +) +ERROR_STATES = [ + MowerStates.FATAL_ERROR, + MowerStates.ERROR, + MowerStates.ERROR_AT_POWER_UP, + MowerStates.NOT_APPLICABLE, + MowerStates.UNKNOWN, + MowerStates.STOPPED, + MowerStates.OFF, +] + class AutomowerBaseEntity(CoordinatorEntity[AutomowerDataUpdateCoordinator]): """Defining the Automower base Entity.""" @@ -41,10 +56,22 @@ class AutomowerBaseEntity(CoordinatorEntity[AutomowerDataUpdateCoordinator]): return self.coordinator.data[self.mower_id] -class AutomowerControlEntity(AutomowerBaseEntity): - """AutomowerControlEntity, for dynamic availability.""" +class AutomowerAvailableEntity(AutomowerBaseEntity): + """Replies available when the mower is connected.""" @property def available(self) -> bool: """Return True if the device is available.""" return super().available and self.mower_attributes.metadata.connected + + +class AutomowerControlEntity(AutomowerAvailableEntity): + """Replies available when the mower is connected and not in error state.""" + + @property + def available(self) -> bool: + """Return True if the device is available.""" + return super().available and ( + self.mower_attributes.mower.state not in ERROR_STATES + or self.mower_attributes.mower.activity not in ERROR_ACTIVITIES + ) diff --git a/homeassistant/components/husqvarna_automower/lawn_mower.py b/homeassistant/components/husqvarna_automower/lawn_mower.py index c0b566a7f66..e59d9e635e9 100644 --- a/homeassistant/components/husqvarna_automower/lawn_mower.py +++ b/homeassistant/components/husqvarna_automower/lawn_mower.py @@ -23,7 +23,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import AutomowerConfigEntry from .const import DOMAIN from .coordinator import AutomowerDataUpdateCoordinator -from .entity import AutomowerControlEntity +from .entity import AutomowerAvailableEntity DOCKED_ACTIVITIES = (MowerActivities.PARKED_IN_CS, MowerActivities.CHARGING) MOWING_ACTIVITIES = ( @@ -94,7 +94,7 @@ async def async_setup_entry( ) -class AutomowerLawnMowerEntity(AutomowerControlEntity, LawnMowerEntity): +class AutomowerLawnMowerEntity(AutomowerAvailableEntity, LawnMowerEntity): """Defining each mower Entity.""" _attr_name = None diff --git a/homeassistant/components/husqvarna_automower/switch.py b/homeassistant/components/husqvarna_automower/switch.py index a856e9c9050..8a450b8e81a 100644 --- a/homeassistant/components/husqvarna_automower/switch.py +++ b/homeassistant/components/husqvarna_automower/switch.py @@ -5,13 +5,7 @@ import logging from typing import TYPE_CHECKING, Any from aioautomower.exceptions import ApiException -from aioautomower.model import ( - MowerActivities, - MowerModes, - MowerStates, - StayOutZones, - Zone, -) +from aioautomower.model import MowerModes, StayOutZones, Zone from homeassistant.components.switch import SwitchEntity from homeassistant.const import Platform @@ -27,21 +21,6 @@ from .entity import AutomowerControlEntity _LOGGER = logging.getLogger(__name__) -ERROR_ACTIVITIES = ( - MowerActivities.STOPPED_IN_GARDEN, - MowerActivities.UNKNOWN, - MowerActivities.NOT_APPLICABLE, -) -ERROR_STATES = [ - MowerStates.FATAL_ERROR, - MowerStates.ERROR, - MowerStates.ERROR_AT_POWER_UP, - MowerStates.NOT_APPLICABLE, - MowerStates.UNKNOWN, - MowerStates.STOPPED, - MowerStates.OFF, -] - async def async_setup_entry( hass: HomeAssistant, @@ -88,14 +67,6 @@ class AutomowerScheduleSwitchEntity(AutomowerControlEntity, SwitchEntity): """Return the state of the switch.""" return self.mower_attributes.mower.mode != MowerModes.HOME - @property - def available(self) -> bool: - """Return True if the device is available.""" - return super().available and ( - self.mower_attributes.mower.state not in ERROR_STATES - or self.mower_attributes.mower.activity not in ERROR_ACTIVITIES - ) - async def async_turn_off(self, **kwargs: Any) -> None: """Turn the entity off.""" try: From fdade672119143191b9ae927b787272005b1ca54 Mon Sep 17 00:00:00 2001 From: Luca Angemi Date: Mon, 24 Jun 2024 08:20:34 +0200 Subject: [PATCH 1071/1445] Add device info for Aemet (#120243) * Update sensor.py * Update weather.py * Update sensor.py * ruff * add device info to entity * remove info from sensor * remove info from weather * ruff * amend entity * Update sensor.py * Update weather.py * ruff again * add DOMAIN * type unique_id * Update entity.py * Update entity.py * assert * update tests * change snapshot --- homeassistant/components/aemet/entity.py | 18 ++++++++++++++++++ homeassistant/components/aemet/sensor.py | 6 ++++-- homeassistant/components/aemet/weather.py | 2 +- .../aemet/snapshots/test_diagnostics.ambr | 2 +- tests/components/aemet/util.py | 1 + 5 files changed, 25 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/aemet/entity.py b/homeassistant/components/aemet/entity.py index ba3f7e56193..f48eaa1593d 100644 --- a/homeassistant/components/aemet/entity.py +++ b/homeassistant/components/aemet/entity.py @@ -7,14 +7,32 @@ from typing import Any from aemet_opendata.helpers import dict_nested_value from homeassistant.components.weather import Forecast +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity +from .const import DOMAIN from .coordinator import WeatherUpdateCoordinator class AemetEntity(CoordinatorEntity[WeatherUpdateCoordinator]): """Define an AEMET entity.""" + def __init__( + self, + coordinator: WeatherUpdateCoordinator, + name: str, + unique_id: str, + ) -> None: + """Initialize the entity.""" + super().__init__(coordinator) + self._attr_device_info = DeviceInfo( + name=name, + entry_type=DeviceEntryType.SERVICE, + identifiers={(DOMAIN, unique_id)}, + manufacturer="AEMET", + model="Forecast", + ) + def get_aemet_forecast(self, forecast_mode: str) -> list[Forecast]: """Return AEMET entity forecast by mode.""" return self.coordinator.data["forecast"][forecast_mode] diff --git a/homeassistant/components/aemet/sensor.py b/homeassistant/components/aemet/sensor.py index 268112070e8..cafb9be8a70 100644 --- a/homeassistant/components/aemet/sensor.py +++ b/homeassistant/components/aemet/sensor.py @@ -392,10 +392,12 @@ class AemetSensor(AemetEntity, SensorEntity): config_entry: ConfigEntry, ) -> None: """Initialize the sensor.""" - super().__init__(coordinator) + assert config_entry.unique_id is not None + unique_id = config_entry.unique_id + super().__init__(coordinator, name, unique_id) self.entity_description = description self._attr_name = f"{name} {description.name}" - self._attr_unique_id = f"{config_entry.unique_id}-{description.key}" + self._attr_unique_id = f"{unique_id}-{description.key}" @property def native_value(self): diff --git a/homeassistant/components/aemet/weather.py b/homeassistant/components/aemet/weather.py index 4df0b1081f5..9c905941f62 100644 --- a/homeassistant/components/aemet/weather.py +++ b/homeassistant/components/aemet/weather.py @@ -71,7 +71,7 @@ class AemetWeather( coordinator: WeatherUpdateCoordinator, ) -> None: """Initialize the sensor.""" - super().__init__(coordinator) + super().__init__(coordinator, name, unique_id) self._attr_name = name self._attr_unique_id = unique_id diff --git a/tests/components/aemet/snapshots/test_diagnostics.ambr b/tests/components/aemet/snapshots/test_diagnostics.ambr index 4b678dc1da5..8d4132cad84 100644 --- a/tests/components/aemet/snapshots/test_diagnostics.ambr +++ b/tests/components/aemet/snapshots/test_diagnostics.ambr @@ -20,7 +20,7 @@ 'pref_disable_polling': False, 'source': 'user', 'title': 'Mock Title', - 'unique_id': None, + 'unique_id': '**REDACTED**', 'version': 1, }), 'coord_data': dict({ diff --git a/tests/components/aemet/util.py b/tests/components/aemet/util.py index bb8885f7b4c..162ee657513 100644 --- a/tests/components/aemet/util.py +++ b/tests/components/aemet/util.py @@ -68,6 +68,7 @@ async def async_init_integration(hass: HomeAssistant): CONF_NAME: "AEMET", }, entry_id="7442b231f139e813fc1939281123f220", + unique_id="40.30403754--3.72935236", ) config_entry.add_to_hass(hass) From 6a5c1fc613982912b52f70fbaba7dcdd02e6445f Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Mon, 24 Jun 2024 02:43:13 -0400 Subject: [PATCH 1072/1445] Replace custom validator from zwave_js with `from_dict` funcs (#120279) --- homeassistant/components/zwave_js/api.py | 63 +----------------------- 1 file changed, 2 insertions(+), 61 deletions(-) diff --git a/homeassistant/components/zwave_js/api.py b/homeassistant/components/zwave_js/api.py index fee828c9fd8..8f81790708f 100644 --- a/homeassistant/components/zwave_js/api.py +++ b/homeassistant/components/zwave_js/api.py @@ -166,65 +166,6 @@ STRATEGY = "strategy" MINIMUM_QR_STRING_LENGTH = 52 -def convert_planned_provisioning_entry(info: dict) -> ProvisioningEntry: - """Handle provisioning entry dict to ProvisioningEntry.""" - return ProvisioningEntry( - dsk=info[DSK], - security_classes=info[SECURITY_CLASSES], - status=info[STATUS], - requested_security_classes=info.get(REQUESTED_SECURITY_CLASSES), - additional_properties={ - k: v - for k, v in info.items() - if k not in (DSK, SECURITY_CLASSES, STATUS, REQUESTED_SECURITY_CLASSES) - }, - ) - - -def convert_qr_provisioning_information(info: dict) -> QRProvisioningInformation: - """Convert QR provisioning information dict to QRProvisioningInformation.""" - ## Remove this when we have fix for QRProvisioningInformation.from_dict() - return QRProvisioningInformation( - version=info[VERSION], - security_classes=info[SECURITY_CLASSES], - dsk=info[DSK], - generic_device_class=info[GENERIC_DEVICE_CLASS], - specific_device_class=info[SPECIFIC_DEVICE_CLASS], - installer_icon_type=info[INSTALLER_ICON_TYPE], - manufacturer_id=info[MANUFACTURER_ID], - product_type=info[PRODUCT_TYPE], - product_id=info[PRODUCT_ID], - application_version=info[APPLICATION_VERSION], - max_inclusion_request_interval=info.get(MAX_INCLUSION_REQUEST_INTERVAL), - uuid=info.get(UUID), - supported_protocols=info.get(SUPPORTED_PROTOCOLS), - status=info[STATUS], - requested_security_classes=info.get(REQUESTED_SECURITY_CLASSES), - additional_properties={ - k: v - for k, v in info.items() - if k - not in ( - VERSION, - SECURITY_CLASSES, - DSK, - GENERIC_DEVICE_CLASS, - SPECIFIC_DEVICE_CLASS, - INSTALLER_ICON_TYPE, - MANUFACTURER_ID, - PRODUCT_TYPE, - PRODUCT_ID, - APPLICATION_VERSION, - MAX_INCLUSION_REQUEST_INTERVAL, - UUID, - SUPPORTED_PROTOCOLS, - STATUS, - REQUESTED_SECURITY_CLASSES, - ) - }, - ) - - # Helper schemas PLANNED_PROVISIONING_ENTRY_SCHEMA = vol.All( vol.Schema( @@ -244,7 +185,7 @@ PLANNED_PROVISIONING_ENTRY_SCHEMA = vol.All( # Provisioning entries can have extra keys for SmartStart extra=vol.ALLOW_EXTRA, ), - convert_planned_provisioning_entry, + ProvisioningEntry.from_dict, ) QR_PROVISIONING_INFORMATION_SCHEMA = vol.All( @@ -278,7 +219,7 @@ QR_PROVISIONING_INFORMATION_SCHEMA = vol.All( }, extra=vol.ALLOW_EXTRA, ), - convert_qr_provisioning_information, + QRProvisioningInformation.from_dict, ) QR_CODE_STRING_SCHEMA = vol.All(str, vol.Length(min=MINIMUM_QR_STRING_LENGTH)) From fa9bced6b0788bbdd495203f8c88c6f0fab3932f Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 23 Jun 2024 23:43:42 -0700 Subject: [PATCH 1073/1445] Load local calendar ics in background thread to avoid timezone I/O in event loop (#120276) --- homeassistant/components/local_calendar/calendar.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/local_calendar/calendar.py b/homeassistant/components/local_calendar/calendar.py index 213ee37ef37..66b3f80c19c 100644 --- a/homeassistant/components/local_calendar/calendar.py +++ b/homeassistant/components/local_calendar/calendar.py @@ -44,7 +44,9 @@ async def async_setup_entry( """Set up the local calendar platform.""" store = hass.data[DOMAIN][config_entry.entry_id] ics = await store.async_load() - calendar = IcsCalendarStream.calendar_from_ics(ics) + calendar: Calendar = await hass.async_add_executor_job( + IcsCalendarStream.calendar_from_ics, ics + ) calendar.prodid = PRODID name = config_entry.data[CONF_CALENDAR_NAME] From 4785810dc3811ad25b1b947cb4af8bd37682d2b8 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 24 Jun 2024 08:54:46 +0200 Subject: [PATCH 1074/1445] Migrate AEMET to has entity name (#120284) --- homeassistant/components/aemet/entity.py | 5 ++++- homeassistant/components/aemet/sensor.py | 13 +++++-------- homeassistant/components/aemet/weather.py | 17 ++++++++--------- 3 files changed, 17 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/aemet/entity.py b/homeassistant/components/aemet/entity.py index f48eaa1593d..562d82fd9c7 100644 --- a/homeassistant/components/aemet/entity.py +++ b/homeassistant/components/aemet/entity.py @@ -10,13 +10,16 @@ from homeassistant.components.weather import Forecast from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import DOMAIN +from .const import ATTRIBUTION, DOMAIN from .coordinator import WeatherUpdateCoordinator class AemetEntity(CoordinatorEntity[WeatherUpdateCoordinator]): """Define an AEMET entity.""" + _attr_attribution = ATTRIBUTION + _attr_has_entity_name = True + def __init__( self, coordinator: WeatherUpdateCoordinator, diff --git a/homeassistant/components/aemet/sensor.py b/homeassistant/components/aemet/sensor.py index cafb9be8a70..83d490f7fe2 100644 --- a/homeassistant/components/aemet/sensor.py +++ b/homeassistant/components/aemet/sensor.py @@ -43,7 +43,6 @@ from homeassistant.components.sensor import ( SensorEntityDescription, SensorStateClass, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( DEGREE, PERCENTAGE, @@ -86,7 +85,6 @@ from .const import ( ATTR_API_WIND_BEARING, ATTR_API_WIND_MAX_SPEED, ATTR_API_WIND_SPEED, - ATTRIBUTION, CONDITIONS_MAP, ) from .coordinator import WeatherUpdateCoordinator @@ -366,12 +364,15 @@ async def async_setup_entry( name = domain_data.name coordinator = domain_data.coordinator + unique_id = config_entry.unique_id + assert unique_id is not None + async_add_entities( AemetSensor( name, coordinator, description, - config_entry, + unique_id, ) for description in FORECAST_SENSORS + WEATHER_SENSORS if dict_nested_value(coordinator.data["lib"], description.keys) is not None @@ -381,7 +382,6 @@ async def async_setup_entry( class AemetSensor(AemetEntity, SensorEntity): """Implementation of an AEMET OpenData sensor.""" - _attr_attribution = ATTRIBUTION entity_description: AemetSensorEntityDescription def __init__( @@ -389,14 +389,11 @@ class AemetSensor(AemetEntity, SensorEntity): name: str, coordinator: WeatherUpdateCoordinator, description: AemetSensorEntityDescription, - config_entry: ConfigEntry, + unique_id: str, ) -> None: """Initialize the sensor.""" - assert config_entry.unique_id is not None - unique_id = config_entry.unique_id super().__init__(coordinator, name, unique_id) self.entity_description = description - self._attr_name = f"{name} {description.name}" self._attr_unique_id = f"{unique_id}-{description.key}" @property diff --git a/homeassistant/components/aemet/weather.py b/homeassistant/components/aemet/weather.py index 9c905941f62..341b81d71c4 100644 --- a/homeassistant/components/aemet/weather.py +++ b/homeassistant/components/aemet/weather.py @@ -28,7 +28,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import AemetConfigEntry -from .const import ATTRIBUTION, CONDITIONS_MAP +from .const import CONDITIONS_MAP from .coordinator import WeatherUpdateCoordinator from .entity import AemetEntity @@ -43,10 +43,10 @@ async def async_setup_entry( name = domain_data.name weather_coordinator = domain_data.coordinator - async_add_entities( - [AemetWeather(name, config_entry.unique_id, weather_coordinator)], - False, - ) + unique_id = config_entry.unique_id + assert unique_id is not None + + async_add_entities([AemetWeather(name, unique_id, weather_coordinator)]) class AemetWeather( @@ -55,7 +55,6 @@ class AemetWeather( ): """Implementation of an AEMET OpenData weather.""" - _attr_attribution = ATTRIBUTION _attr_native_precipitation_unit = UnitOfPrecipitationDepth.MILLIMETERS _attr_native_pressure_unit = UnitOfPressure.HPA _attr_native_temperature_unit = UnitOfTemperature.CELSIUS @@ -63,16 +62,16 @@ class AemetWeather( _attr_supported_features = ( WeatherEntityFeature.FORECAST_DAILY | WeatherEntityFeature.FORECAST_HOURLY ) + _attr_name = None def __init__( self, - name, - unique_id, + name: str, + unique_id: str, coordinator: WeatherUpdateCoordinator, ) -> None: """Initialize the sensor.""" super().__init__(coordinator, name, unique_id) - self._attr_name = name self._attr_unique_id = unique_id @property From 5c2db162c4042d9a29e75191de532fa45fbff58a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 24 Jun 2024 02:57:59 -0400 Subject: [PATCH 1075/1445] Remove "no API" prompt (#120280) --- .../conversation.py | 41 ++++++++----------- .../openai_conversation/conversation.py | 39 ++++++++---------- homeassistant/helpers/llm.py | 10 ++--- .../snapshots/test_conversation.ambr | 6 --- .../test_conversation.py | 4 -- 5 files changed, 41 insertions(+), 59 deletions(-) diff --git a/homeassistant/components/google_generative_ai_conversation/conversation.py b/homeassistant/components/google_generative_ai_conversation/conversation.py index b9f0006dbff..2cfbc09ed08 100644 --- a/homeassistant/components/google_generative_ai_conversation/conversation.py +++ b/homeassistant/components/google_generative_ai_conversation/conversation.py @@ -349,27 +349,22 @@ class GoogleGenerativeAIConversationEntity( ): user_name = user.name - if llm_api: - api_prompt = llm_api.api_prompt - else: - api_prompt = llm.async_render_no_api_prompt(self.hass) - - return "\n".join( - ( - template.Template( - llm.BASE_PROMPT - + self.entry.options.get( - CONF_PROMPT, llm.DEFAULT_INSTRUCTIONS_PROMPT - ), - self.hass, - ).async_render( - { - "ha_name": self.hass.config.location_name, - "user_name": user_name, - "llm_context": llm_context, - }, - parse_result=False, - ), - api_prompt, + parts = [ + template.Template( + llm.BASE_PROMPT + + self.entry.options.get(CONF_PROMPT, llm.DEFAULT_INSTRUCTIONS_PROMPT), + self.hass, + ).async_render( + { + "ha_name": self.hass.config.location_name, + "user_name": user_name, + "llm_context": llm_context, + }, + parse_result=False, ) - ) + ] + + if llm_api: + parts.append(llm_api.api_prompt) + + return "\n".join(parts) diff --git a/homeassistant/components/openai_conversation/conversation.py b/homeassistant/components/openai_conversation/conversation.py index d0b3ef8f895..40242f5c6cc 100644 --- a/homeassistant/components/openai_conversation/conversation.py +++ b/homeassistant/components/openai_conversation/conversation.py @@ -172,28 +172,20 @@ class OpenAIConversationEntity( user_name = user.name try: - if llm_api: - api_prompt = llm_api.api_prompt - else: - api_prompt = llm.async_render_no_api_prompt(self.hass) - - prompt = "\n".join( - ( - template.Template( - llm.BASE_PROMPT - + options.get(CONF_PROMPT, llm.DEFAULT_INSTRUCTIONS_PROMPT), - self.hass, - ).async_render( - { - "ha_name": self.hass.config.location_name, - "user_name": user_name, - "llm_context": llm_context, - }, - parse_result=False, - ), - api_prompt, + prompt_parts = [ + template.Template( + llm.BASE_PROMPT + + options.get(CONF_PROMPT, llm.DEFAULT_INSTRUCTIONS_PROMPT), + self.hass, + ).async_render( + { + "ha_name": self.hass.config.location_name, + "user_name": user_name, + "llm_context": llm_context, + }, + parse_result=False, ) - ) + ] except TemplateError as err: LOGGER.error("Error rendering prompt: %s", err) @@ -206,6 +198,11 @@ class OpenAIConversationEntity( response=intent_response, conversation_id=conversation_id ) + if llm_api: + prompt_parts.append(llm_api.api_prompt) + + prompt = "\n".join(prompt_parts) + # Create a copy of the variable because we attach it to the trace messages = [ ChatCompletionSystemMessageParam(role="system", content=prompt), diff --git a/homeassistant/helpers/llm.py b/homeassistant/helpers/llm.py index 6673786e2e1..a4e18fdb2c0 100644 --- a/homeassistant/helpers/llm.py +++ b/homeassistant/helpers/llm.py @@ -51,11 +51,11 @@ Answer in plain text. Keep it simple and to the point. @callback def async_render_no_api_prompt(hass: HomeAssistant) -> str: - """Return the prompt to be used when no API is configured.""" - return ( - "Only if the user wants to control a device, tell them to edit the AI configuration " - "and allow access to Home Assistant." - ) + """Return the prompt to be used when no API is configured. + + No longer used since Home Assistant 2024.7. + """ + return "" @singleton("llm") diff --git a/tests/components/google_generative_ai_conversation/snapshots/test_conversation.ambr b/tests/components/google_generative_ai_conversation/snapshots/test_conversation.ambr index aec8d088b20..b0a0ce967de 100644 --- a/tests/components/google_generative_ai_conversation/snapshots/test_conversation.ambr +++ b/tests/components/google_generative_ai_conversation/snapshots/test_conversation.ambr @@ -35,7 +35,6 @@ You are a voice assistant for Home Assistant. Answer questions about the world truthfully. Answer in plain text. Keep it simple and to the point. - Only if the user wants to control a device, tell them to edit the AI configuration and allow access to Home Assistant. ''', 'role': 'user', }), @@ -88,7 +87,6 @@ You are a voice assistant for Home Assistant. Answer questions about the world truthfully. Answer in plain text. Keep it simple and to the point. - Only if the user wants to control a device, tell them to edit the AI configuration and allow access to Home Assistant. ''', 'role': 'user', }), @@ -142,7 +140,6 @@ You are a voice assistant for Home Assistant. Answer questions about the world truthfully. Answer in plain text. Keep it simple and to the point. - Only if the user wants to control a device, tell them to edit the AI configuration and allow access to Home Assistant. ''', 'tools': None, }), @@ -187,7 +184,6 @@ You are a voice assistant for Home Assistant. Answer questions about the world truthfully. Answer in plain text. Keep it simple and to the point. - Only if the user wants to control a device, tell them to edit the AI configuration and allow access to Home Assistant. ''', 'tools': None, }), @@ -244,7 +240,6 @@ You are a voice assistant for Home Assistant. Answer questions about the world truthfully. Answer in plain text. Keep it simple and to the point. - ''', 'tools': None, }), @@ -293,7 +288,6 @@ You are a voice assistant for Home Assistant. Answer questions about the world truthfully. Answer in plain text. Keep it simple and to the point. - ''', 'tools': None, }), diff --git a/tests/components/google_generative_ai_conversation/test_conversation.py b/tests/components/google_generative_ai_conversation/test_conversation.py index 7f4fe886e90..990058aa89d 100644 --- a/tests/components/google_generative_ai_conversation/test_conversation.py +++ b/tests/components/google_generative_ai_conversation/test_conversation.py @@ -75,10 +75,6 @@ async def test_default_prompt( "homeassistant.components.google_generative_ai_conversation.conversation.llm.AssistAPI._async_get_api_prompt", return_value="", ), - patch( - "homeassistant.components.google_generative_ai_conversation.conversation.llm.async_render_no_api_prompt", - return_value="", - ), ): mock_chat = AsyncMock() mock_model.return_value.start_chat.return_value = mock_chat From ab9cbf64da39fab4583fd1c0becc159342ab0b9a Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 24 Jun 2024 09:54:22 +0200 Subject: [PATCH 1076/1445] Add sensors to Airgradient (#120286) --- homeassistant/components/airgradient/const.py | 8 + .../components/airgradient/select.py | 15 +- .../components/airgradient/sensor.py | 173 +++++- .../components/airgradient/strings.json | 37 ++ .../airgradient/snapshots/test_sensor.ambr | 552 ++++++++++++++++++ 5 files changed, 750 insertions(+), 35 deletions(-) diff --git a/homeassistant/components/airgradient/const.py b/homeassistant/components/airgradient/const.py index bbb15a3741d..2817a27e37b 100644 --- a/homeassistant/components/airgradient/const.py +++ b/homeassistant/components/airgradient/const.py @@ -2,6 +2,14 @@ import logging +from airgradient import PmStandard + DOMAIN = "airgradient" LOGGER = logging.getLogger(__package__) + +PM_STANDARD = { + PmStandard.UGM3: "ugm3", + PmStandard.USAQI: "us_aqi", +} +PM_STANDARD_REVERSE = {v: k for k, v in PM_STANDARD.items()} diff --git a/homeassistant/components/airgradient/select.py b/homeassistant/components/airgradient/select.py index 8fac06917fd..e85e1224000 100644 --- a/homeassistant/components/airgradient/select.py +++ b/homeassistant/components/airgradient/select.py @@ -4,12 +4,7 @@ from collections.abc import Awaitable, Callable from dataclasses import dataclass from airgradient import AirGradientClient, Config -from airgradient.models import ( - ConfigurationControl, - LedBarMode, - PmStandard, - TemperatureUnit, -) +from airgradient.models import ConfigurationControl, LedBarMode, TemperatureUnit from homeassistant.components.select import SelectEntity, SelectEntityDescription from homeassistant.const import EntityCategory @@ -18,16 +13,10 @@ from homeassistant.exceptions import ServiceValidationError from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import AirGradientConfigEntry -from .const import DOMAIN +from .const import DOMAIN, PM_STANDARD, PM_STANDARD_REVERSE from .coordinator import AirGradientConfigCoordinator from .entity import AirGradientEntity -PM_STANDARD = { - PmStandard.UGM3: "ugm3", - PmStandard.USAQI: "us_aqi", -} -PM_STANDARD_REVERSE = {v: k for k, v in PM_STANDARD.items()} - @dataclass(frozen=True, kw_only=True) class AirGradientSelectEntityDescription(SelectEntityDescription): diff --git a/homeassistant/components/airgradient/sensor.py b/homeassistant/components/airgradient/sensor.py index 6123d4289f9..f431c49ed2a 100644 --- a/homeassistant/components/airgradient/sensor.py +++ b/homeassistant/components/airgradient/sensor.py @@ -3,7 +3,13 @@ from collections.abc import Callable from dataclasses import dataclass -from airgradient.models import Measures +from airgradient import Config +from airgradient.models import ( + ConfigurationControl, + LedBarMode, + Measures, + TemperatureUnit, +) from homeassistant.components.sensor import ( SensorDeviceClass, @@ -18,60 +24,69 @@ from homeassistant.const import ( SIGNAL_STRENGTH_DECIBELS_MILLIWATT, EntityCategory, UnitOfTemperature, + UnitOfTime, ) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from . import AirGradientConfigEntry -from .coordinator import AirGradientMeasurementCoordinator +from .const import PM_STANDARD, PM_STANDARD_REVERSE +from .coordinator import AirGradientConfigCoordinator, AirGradientMeasurementCoordinator from .entity import AirGradientEntity @dataclass(frozen=True, kw_only=True) -class AirGradientSensorEntityDescription(SensorEntityDescription): - """Describes AirGradient sensor entity.""" +class AirGradientMeasurementSensorEntityDescription(SensorEntityDescription): + """Describes AirGradient measurement sensor entity.""" value_fn: Callable[[Measures], StateType] -SENSOR_TYPES: tuple[AirGradientSensorEntityDescription, ...] = ( - AirGradientSensorEntityDescription( +@dataclass(frozen=True, kw_only=True) +class AirGradientConfigSensorEntityDescription(SensorEntityDescription): + """Describes AirGradient config sensor entity.""" + + value_fn: Callable[[Config], StateType] + + +MEASUREMENT_SENSOR_TYPES: tuple[AirGradientMeasurementSensorEntityDescription, ...] = ( + AirGradientMeasurementSensorEntityDescription( key="pm01", device_class=SensorDeviceClass.PM1, native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, state_class=SensorStateClass.MEASUREMENT, value_fn=lambda status: status.pm01, ), - AirGradientSensorEntityDescription( + AirGradientMeasurementSensorEntityDescription( key="pm02", device_class=SensorDeviceClass.PM25, native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, state_class=SensorStateClass.MEASUREMENT, value_fn=lambda status: status.pm02, ), - AirGradientSensorEntityDescription( + AirGradientMeasurementSensorEntityDescription( key="pm10", device_class=SensorDeviceClass.PM10, native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, state_class=SensorStateClass.MEASUREMENT, value_fn=lambda status: status.pm10, ), - AirGradientSensorEntityDescription( + AirGradientMeasurementSensorEntityDescription( key="temperature", device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=UnitOfTemperature.CELSIUS, state_class=SensorStateClass.MEASUREMENT, value_fn=lambda status: status.ambient_temperature, ), - AirGradientSensorEntityDescription( + AirGradientMeasurementSensorEntityDescription( key="humidity", device_class=SensorDeviceClass.HUMIDITY, native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, value_fn=lambda status: status.relative_humidity, ), - AirGradientSensorEntityDescription( + AirGradientMeasurementSensorEntityDescription( key="signal_strength", device_class=SensorDeviceClass.SIGNAL_STRENGTH, native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, @@ -80,33 +95,33 @@ SENSOR_TYPES: tuple[AirGradientSensorEntityDescription, ...] = ( entity_registry_enabled_default=False, value_fn=lambda status: status.signal_strength, ), - AirGradientSensorEntityDescription( + AirGradientMeasurementSensorEntityDescription( key="tvoc", translation_key="total_volatile_organic_component_index", state_class=SensorStateClass.MEASUREMENT, value_fn=lambda status: status.total_volatile_organic_component_index, ), - AirGradientSensorEntityDescription( + AirGradientMeasurementSensorEntityDescription( key="nitrogen_index", translation_key="nitrogen_index", state_class=SensorStateClass.MEASUREMENT, value_fn=lambda status: status.nitrogen_index, ), - AirGradientSensorEntityDescription( + AirGradientMeasurementSensorEntityDescription( key="co2", device_class=SensorDeviceClass.CO2, native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, state_class=SensorStateClass.MEASUREMENT, value_fn=lambda status: status.rco2, ), - AirGradientSensorEntityDescription( + AirGradientMeasurementSensorEntityDescription( key="pm003", translation_key="pm003_count", native_unit_of_measurement="particles/dL", state_class=SensorStateClass.MEASUREMENT, value_fn=lambda status: status.pm003_count, ), - AirGradientSensorEntityDescription( + AirGradientMeasurementSensorEntityDescription( key="nox_raw", translation_key="raw_nitrogen", native_unit_of_measurement="ticks", @@ -114,7 +129,7 @@ SENSOR_TYPES: tuple[AirGradientSensorEntityDescription, ...] = ( entity_registry_enabled_default=False, value_fn=lambda status: status.raw_nitrogen, ), - AirGradientSensorEntityDescription( + AirGradientMeasurementSensorEntityDescription( key="tvoc_raw", translation_key="raw_total_volatile_organic_component", native_unit_of_measurement="ticks", @@ -124,6 +139,77 @@ SENSOR_TYPES: tuple[AirGradientSensorEntityDescription, ...] = ( ), ) +CONFIG_SENSOR_TYPES: tuple[AirGradientConfigSensorEntityDescription, ...] = ( + AirGradientConfigSensorEntityDescription( + key="co2_automatic_baseline_calibration_days", + translation_key="co2_automatic_baseline_calibration_days", + device_class=SensorDeviceClass.DURATION, + native_unit_of_measurement=UnitOfTime.DAYS, + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda config: config.co2_automatic_baseline_calibration_days, + ), + AirGradientConfigSensorEntityDescription( + key="nox_learning_offset", + translation_key="nox_learning_offset", + device_class=SensorDeviceClass.DURATION, + native_unit_of_measurement=UnitOfTime.DAYS, + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda config: config.nox_learning_offset, + ), + AirGradientConfigSensorEntityDescription( + key="tvoc_learning_offset", + translation_key="tvoc_learning_offset", + device_class=SensorDeviceClass.DURATION, + native_unit_of_measurement=UnitOfTime.DAYS, + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda config: config.tvoc_learning_offset, + ), +) + +CONFIG_LED_BAR_SENSOR_TYPES: tuple[AirGradientConfigSensorEntityDescription, ...] = ( + AirGradientConfigSensorEntityDescription( + key="led_bar_mode", + translation_key="led_bar_mode", + device_class=SensorDeviceClass.ENUM, + options=[x.value for x in LedBarMode], + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda config: config.led_bar_mode, + ), + AirGradientConfigSensorEntityDescription( + key="led_bar_brightness", + translation_key="led_bar_brightness", + native_unit_of_measurement=PERCENTAGE, + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda config: config.led_bar_brightness, + ), +) + +CONFIG_DISPLAY_SENSOR_TYPES: tuple[AirGradientConfigSensorEntityDescription, ...] = ( + AirGradientConfigSensorEntityDescription( + key="display_temperature_unit", + translation_key="display_temperature_unit", + device_class=SensorDeviceClass.ENUM, + options=[x.value for x in TemperatureUnit], + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda config: config.temperature_unit, + ), + AirGradientConfigSensorEntityDescription( + key="display_pm_standard", + translation_key="display_pm_standard", + device_class=SensorDeviceClass.ENUM, + options=list(PM_STANDARD_REVERSE), + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda config: PM_STANDARD.get(config.pm_standard), + ), + AirGradientConfigSensorEntityDescription( + key="display_brightness", + translation_key="display_brightness", + native_unit_of_measurement=PERCENTAGE, + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda config: config.display_brightness, + ), +) + async def async_setup_entry( hass: HomeAssistant, @@ -134,7 +220,9 @@ async def async_setup_entry( coordinator = entry.runtime_data.measurement listener: Callable[[], None] | None = None - not_setup: set[AirGradientSensorEntityDescription] = set(SENSOR_TYPES) + not_setup: set[AirGradientMeasurementSensorEntityDescription] = set( + MEASUREMENT_SENSOR_TYPES + ) @callback def add_entities() -> None: @@ -147,7 +235,7 @@ async def async_setup_entry( if description.value_fn(coordinator.data) is None: not_setup.add(description) else: - sensors.append(AirGradientSensor(coordinator, description)) + sensors.append(AirGradientMeasurementSensor(coordinator, description)) if sensors: async_add_entities(sensors) @@ -159,17 +247,33 @@ async def async_setup_entry( add_entities() + entities = [ + AirGradientConfigSensor(entry.runtime_data.config, description) + for description in CONFIG_SENSOR_TYPES + ] + if "L" in coordinator.data.model: + entities.extend( + AirGradientConfigSensor(entry.runtime_data.config, description) + for description in CONFIG_LED_BAR_SENSOR_TYPES + ) + if "I" in coordinator.data.model: + entities.extend( + AirGradientConfigSensor(entry.runtime_data.config, description) + for description in CONFIG_DISPLAY_SENSOR_TYPES + ) + async_add_entities(entities) -class AirGradientSensor(AirGradientEntity, SensorEntity): + +class AirGradientMeasurementSensor(AirGradientEntity, SensorEntity): """Defines an AirGradient sensor.""" - entity_description: AirGradientSensorEntityDescription + entity_description: AirGradientMeasurementSensorEntityDescription coordinator: AirGradientMeasurementCoordinator def __init__( self, coordinator: AirGradientMeasurementCoordinator, - description: AirGradientSensorEntityDescription, + description: AirGradientMeasurementSensorEntityDescription, ) -> None: """Initialize airgradient sensor.""" super().__init__(coordinator) @@ -180,3 +284,28 @@ class AirGradientSensor(AirGradientEntity, SensorEntity): def native_value(self) -> StateType: """Return the state of the sensor.""" return self.entity_description.value_fn(self.coordinator.data) + + +class AirGradientConfigSensor(AirGradientEntity, SensorEntity): + """Defines an AirGradient sensor.""" + + entity_description: AirGradientConfigSensorEntityDescription + coordinator: AirGradientConfigCoordinator + + def __init__( + self, + coordinator: AirGradientConfigCoordinator, + description: AirGradientConfigSensorEntityDescription, + ) -> None: + """Initialize airgradient sensor.""" + super().__init__(coordinator) + self.entity_description = description + self._attr_unique_id = f"{coordinator.serial_number}-{description.key}" + self._attr_entity_registry_enabled_default = ( + coordinator.data.configuration_control is not ConfigurationControl.LOCAL + ) + + @property + def native_value(self) -> StateType: + """Return the state of the sensor.""" + return self.entity_description.value_fn(self.coordinator.data) diff --git a/homeassistant/components/airgradient/strings.json b/homeassistant/components/airgradient/strings.json index 0ab80286570..6c079419839 100644 --- a/homeassistant/components/airgradient/strings.json +++ b/homeassistant/components/airgradient/strings.json @@ -78,6 +78,43 @@ }, "raw_nitrogen": { "name": "Raw NOx" + }, + "display_pm_standard": { + "name": "[%key:component::airgradient::entity::select::display_pm_standard::name%]", + "state": { + "ugm3": "[%key:component::airgradient::entity::select::display_pm_standard::state::ugm3%]", + "us_aqi": "[%key:component::airgradient::entity::select::display_pm_standard::state::us_aqi%]" + } + }, + "co2_automatic_baseline_calibration_days": { + "name": "Carbon dioxide automatic baseline calibration" + }, + "nox_learning_offset": { + "name": "NOx learning offset" + }, + "tvoc_learning_offset": { + "name": "VOC learning offset" + }, + "led_bar_mode": { + "name": "[%key:component::airgradient::entity::select::led_bar_mode::name%]", + "state": { + "off": "[%key:component::airgradient::entity::select::led_bar_mode::state::off%]", + "co2": "[%key:component::airgradient::entity::select::led_bar_mode::state::co2%]", + "pm": "[%key:component::airgradient::entity::select::led_bar_mode::state::pm%]" + } + }, + "led_bar_brightness": { + "name": "[%key:component::airgradient::entity::number::led_bar_brightness::name%]" + }, + "display_temperature_unit": { + "name": "[%key:component::airgradient::entity::select::display_temperature_unit::name%]", + "state": { + "c": "[%key:component::airgradient::entity::select::display_temperature_unit::state::c%]", + "f": "[%key:component::airgradient::entity::select::display_temperature_unit::state::f%]" + } + }, + "display_brightness": { + "name": "[%key:component::airgradient::entity::number::display_brightness::name%]" } } }, diff --git a/tests/components/airgradient/snapshots/test_sensor.ambr b/tests/components/airgradient/snapshots/test_sensor.ambr index e96d2be1004..c3d14c7d8fc 100644 --- a/tests/components/airgradient/snapshots/test_sensor.ambr +++ b/tests/components/airgradient/snapshots/test_sensor.ambr @@ -50,6 +50,213 @@ 'state': '778', }) # --- +# name: test_all_entities[indoor][sensor.airgradient_carbon_dioxide_automatic_baseline_calibration-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.airgradient_carbon_dioxide_automatic_baseline_calibration', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Carbon dioxide automatic baseline calibration', + 'platform': 'airgradient', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'co2_automatic_baseline_calibration_days', + 'unique_id': '84fce612f5b8-co2_automatic_baseline_calibration_days', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[indoor][sensor.airgradient_carbon_dioxide_automatic_baseline_calibration-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'duration', + 'friendly_name': 'Airgradient Carbon dioxide automatic baseline calibration', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.airgradient_carbon_dioxide_automatic_baseline_calibration', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '8', + }) +# --- +# name: test_all_entities[indoor][sensor.airgradient_display_brightness-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.airgradient_display_brightness', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Display brightness', + 'platform': 'airgradient', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'display_brightness', + 'unique_id': '84fce612f5b8-display_brightness', + 'unit_of_measurement': '%', + }) +# --- +# name: test_all_entities[indoor][sensor.airgradient_display_brightness-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Airgradient Display brightness', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.airgradient_display_brightness', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_all_entities[indoor][sensor.airgradient_display_pm_standard-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'ugm3', + 'us_aqi', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.airgradient_display_pm_standard', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Display PM standard', + 'platform': 'airgradient', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'display_pm_standard', + 'unique_id': '84fce612f5b8-display_pm_standard', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[indoor][sensor.airgradient_display_pm_standard-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Airgradient Display PM standard', + 'options': list([ + 'ugm3', + 'us_aqi', + ]), + }), + 'context': , + 'entity_id': 'sensor.airgradient_display_pm_standard', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'ugm3', + }) +# --- +# name: test_all_entities[indoor][sensor.airgradient_display_temperature_unit-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'c', + 'f', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.airgradient_display_temperature_unit', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Display temperature unit', + 'platform': 'airgradient', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'display_temperature_unit', + 'unique_id': '84fce612f5b8-display_temperature_unit', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[indoor][sensor.airgradient_display_temperature_unit-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Airgradient Display temperature unit', + 'options': list([ + 'c', + 'f', + ]), + }), + 'context': , + 'entity_id': 'sensor.airgradient_display_temperature_unit', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'c', + }) +# --- # name: test_all_entities[indoor][sensor.airgradient_humidity-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -101,6 +308,111 @@ 'state': '48.0', }) # --- +# name: test_all_entities[indoor][sensor.airgradient_led_bar_brightness-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.airgradient_led_bar_brightness', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'LED bar brightness', + 'platform': 'airgradient', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'led_bar_brightness', + 'unique_id': '84fce612f5b8-led_bar_brightness', + 'unit_of_measurement': '%', + }) +# --- +# name: test_all_entities[indoor][sensor.airgradient_led_bar_brightness-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Airgradient LED bar brightness', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.airgradient_led_bar_brightness', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '100', + }) +# --- +# name: test_all_entities[indoor][sensor.airgradient_led_bar_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'off', + 'co2', + 'pm', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.airgradient_led_bar_mode', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'LED bar mode', + 'platform': 'airgradient', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'led_bar_mode', + 'unique_id': '84fce612f5b8-led_bar_mode', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[indoor][sensor.airgradient_led_bar_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Airgradient LED bar mode', + 'options': list([ + 'off', + 'co2', + 'pm', + ]), + }), + 'context': , + 'entity_id': 'sensor.airgradient_led_bar_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'co2', + }) +# --- # name: test_all_entities[indoor][sensor.airgradient_nox_index-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -150,6 +462,54 @@ 'state': '1', }) # --- +# name: test_all_entities[indoor][sensor.airgradient_nox_learning_offset-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.airgradient_nox_learning_offset', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'NOx learning offset', + 'platform': 'airgradient', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'nox_learning_offset', + 'unique_id': '84fce612f5b8-nox_learning_offset', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[indoor][sensor.airgradient_nox_learning_offset-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'duration', + 'friendly_name': 'Airgradient NOx learning offset', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.airgradient_nox_learning_offset', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '12', + }) +# --- # name: test_all_entities[indoor][sensor.airgradient_pm0_3-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -604,6 +964,102 @@ 'state': '99', }) # --- +# name: test_all_entities[indoor][sensor.airgradient_voc_learning_offset-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.airgradient_voc_learning_offset', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'VOC learning offset', + 'platform': 'airgradient', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'tvoc_learning_offset', + 'unique_id': '84fce612f5b8-tvoc_learning_offset', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[indoor][sensor.airgradient_voc_learning_offset-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'duration', + 'friendly_name': 'Airgradient VOC learning offset', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.airgradient_voc_learning_offset', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '12', + }) +# --- +# name: test_all_entities[outdoor][sensor.airgradient_carbon_dioxide_automatic_baseline_calibration-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.airgradient_carbon_dioxide_automatic_baseline_calibration', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Carbon dioxide automatic baseline calibration', + 'platform': 'airgradient', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'co2_automatic_baseline_calibration_days', + 'unique_id': '84fce612f5b8-co2_automatic_baseline_calibration_days', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[outdoor][sensor.airgradient_carbon_dioxide_automatic_baseline_calibration-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'duration', + 'friendly_name': 'Airgradient Carbon dioxide automatic baseline calibration', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.airgradient_carbon_dioxide_automatic_baseline_calibration', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '8', + }) +# --- # name: test_all_entities[outdoor][sensor.airgradient_nox_index-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -653,6 +1109,54 @@ 'state': '1', }) # --- +# name: test_all_entities[outdoor][sensor.airgradient_nox_learning_offset-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.airgradient_nox_learning_offset', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'NOx learning offset', + 'platform': 'airgradient', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'nox_learning_offset', + 'unique_id': '84fce612f5b8-nox_learning_offset', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[outdoor][sensor.airgradient_nox_learning_offset-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'duration', + 'friendly_name': 'Airgradient NOx learning offset', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.airgradient_nox_learning_offset', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '12', + }) +# --- # name: test_all_entities[outdoor][sensor.airgradient_raw_nox-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -853,3 +1357,51 @@ 'state': '49', }) # --- +# name: test_all_entities[outdoor][sensor.airgradient_voc_learning_offset-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.airgradient_voc_learning_offset', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'VOC learning offset', + 'platform': 'airgradient', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'tvoc_learning_offset', + 'unique_id': '84fce612f5b8-tvoc_learning_offset', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[outdoor][sensor.airgradient_voc_learning_offset-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'duration', + 'friendly_name': 'Airgradient VOC learning offset', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.airgradient_voc_learning_offset', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '12', + }) +# --- From 213cb6f0fd04ec75eb702868a3fe17114b2d1e6f Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk <11290930+bouwew@users.noreply.github.com> Date: Mon, 24 Jun 2024 09:57:04 +0200 Subject: [PATCH 1077/1445] Improve Plugwise runtime-updating (#120230) --- .../components/plugwise/binary_sensor.py | 28 ++++++++----------- homeassistant/components/plugwise/climate.py | 7 ++--- .../components/plugwise/coordinator.py | 16 ++++------- homeassistant/components/plugwise/number.py | 7 ++--- homeassistant/components/plugwise/select.py | 7 ++--- homeassistant/components/plugwise/sensor.py | 27 ++++++------------ homeassistant/components/plugwise/switch.py | 22 ++++++--------- 7 files changed, 41 insertions(+), 73 deletions(-) diff --git a/homeassistant/components/plugwise/binary_sensor.py b/homeassistant/components/plugwise/binary_sensor.py index ef1051fa7b2..4b251d20a02 100644 --- a/homeassistant/components/plugwise/binary_sensor.py +++ b/homeassistant/components/plugwise/binary_sensor.py @@ -89,26 +89,20 @@ async def async_setup_entry( if not coordinator.new_devices: return - entities: list[PlugwiseBinarySensorEntity] = [] - for device_id, device in coordinator.data.devices.items(): - if not (binary_sensors := device.get("binary_sensors")): - continue - for description in BINARY_SENSORS: - if description.key not in binary_sensors: - continue - - entities.append( - PlugwiseBinarySensorEntity( - coordinator, - device_id, - description, - ) + async_add_entities( + PlugwiseBinarySensorEntity(coordinator, device_id, description) + for device_id in coordinator.new_devices + if ( + binary_sensors := coordinator.data.devices[device_id].get( + "binary_sensors" ) - async_add_entities(entities) - - entry.async_on_unload(coordinator.async_add_listener(_add_entities)) + ) + for description in BINARY_SENSORS + if description.key in binary_sensors + ) _add_entities() + entry.async_on_unload(coordinator.async_add_listener(_add_entities)) class PlugwiseBinarySensorEntity(PlugwiseEntity, BinarySensorEntity): diff --git a/homeassistant/components/plugwise/climate.py b/homeassistant/components/plugwise/climate.py index 29d44fe8159..7b0fe35835d 100644 --- a/homeassistant/components/plugwise/climate.py +++ b/homeassistant/components/plugwise/climate.py @@ -41,13 +41,12 @@ async def async_setup_entry( async_add_entities( PlugwiseClimateEntity(coordinator, device_id) - for device_id, device in coordinator.data.devices.items() - if device["dev_class"] in MASTER_THERMOSTATS + for device_id in coordinator.new_devices + if coordinator.data.devices[device_id]["dev_class"] in MASTER_THERMOSTATS ) - entry.async_on_unload(coordinator.async_add_listener(_add_entities)) - _add_entities() + entry.async_on_unload(coordinator.async_add_listener(_add_entities)) class PlugwiseClimateEntity(PlugwiseEntity, ClimateEntity): diff --git a/homeassistant/components/plugwise/coordinator.py b/homeassistant/components/plugwise/coordinator.py index 34d983510ed..1dff11d26d8 100644 --- a/homeassistant/components/plugwise/coordinator.py +++ b/homeassistant/components/plugwise/coordinator.py @@ -15,7 +15,6 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryError -from homeassistant.helpers import device_registry as dr from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.debounce import Debouncer from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -55,8 +54,8 @@ class PlugwiseDataUpdateCoordinator(DataUpdateCoordinator[PlugwiseData]): timeout=30, websession=async_get_clientsession(hass, verify_ssl=False), ) - self.device_list: list[dr.DeviceEntry] = [] - self.new_devices: bool = False + self._current_devices: set[str] = set() + self.new_devices: set[str] = set() async def _connect(self) -> None: """Connect to the Plugwise Smile.""" @@ -81,13 +80,8 @@ class PlugwiseDataUpdateCoordinator(DataUpdateCoordinator[PlugwiseData]): raise ConfigEntryError("Device with unsupported firmware") from err except ConnectionFailedError as err: raise UpdateFailed("Failed to connect to the Plugwise Smile") from err - - device_reg = dr.async_get(self.hass) - device_list = dr.async_entries_for_config_entry( - device_reg, self.config_entry.entry_id - ) - - self.new_devices = len(data.devices.keys()) - len(self.device_list) > 0 - self.device_list = device_list + else: + self.new_devices = set(data.devices) - self._current_devices + self._current_devices = set(data.devices) return data diff --git a/homeassistant/components/plugwise/number.py b/homeassistant/components/plugwise/number.py index c84ca2cf5c7..1f12b2374b3 100644 --- a/homeassistant/components/plugwise/number.py +++ b/homeassistant/components/plugwise/number.py @@ -81,14 +81,13 @@ async def async_setup_entry( async_add_entities( PlugwiseNumberEntity(coordinator, device_id, description) - for device_id, device in coordinator.data.devices.items() + for device_id in coordinator.new_devices for description in NUMBER_TYPES - if description.key in device + if description.key in coordinator.data.devices[device_id] ) - entry.async_on_unload(coordinator.async_add_listener(_add_entities)) - _add_entities() + entry.async_on_unload(coordinator.async_add_listener(_add_entities)) class PlugwiseNumberEntity(PlugwiseEntity, NumberEntity): diff --git a/homeassistant/components/plugwise/select.py b/homeassistant/components/plugwise/select.py index 88c97b9b9f3..c8c9791c0da 100644 --- a/homeassistant/components/plugwise/select.py +++ b/homeassistant/components/plugwise/select.py @@ -74,14 +74,13 @@ async def async_setup_entry( async_add_entities( PlugwiseSelectEntity(coordinator, device_id, description) - for device_id, device in coordinator.data.devices.items() + for device_id in coordinator.new_devices for description in SELECT_TYPES - if description.options_key in device + if description.options_key in coordinator.data.devices[device_id] ) - entry.async_on_unload(coordinator.async_add_listener(_add_entities)) - _add_entities() + entry.async_on_unload(coordinator.async_add_listener(_add_entities)) class PlugwiseSelectEntity(PlugwiseEntity, SelectEntity): diff --git a/homeassistant/components/plugwise/sensor.py b/homeassistant/components/plugwise/sensor.py index 147bab828a8..ae5b4e6ed91 100644 --- a/homeassistant/components/plugwise/sensor.py +++ b/homeassistant/components/plugwise/sensor.py @@ -414,27 +414,16 @@ async def async_setup_entry( if not coordinator.new_devices: return - entities: list[PlugwiseSensorEntity] = [] - for device_id, device in coordinator.data.devices.items(): - if not (sensors := device.get("sensors")): - continue - for description in SENSORS: - if description.key not in sensors: - continue - - entities.append( - PlugwiseSensorEntity( - coordinator, - device_id, - description, - ) - ) - - async_add_entities(entities) - - entry.async_on_unload(coordinator.async_add_listener(_add_entities)) + async_add_entities( + PlugwiseSensorEntity(coordinator, device_id, description) + for device_id in coordinator.new_devices + if (sensors := coordinator.data.devices[device_id].get("sensors")) + for description in SENSORS + if description.key in sensors + ) _add_entities() + entry.async_on_unload(coordinator.async_add_listener(_add_entities)) class PlugwiseSensorEntity(PlugwiseEntity, SensorEntity): diff --git a/homeassistant/components/plugwise/switch.py b/homeassistant/components/plugwise/switch.py index 3ed2d14b8dd..a134ab5b044 100644 --- a/homeassistant/components/plugwise/switch.py +++ b/homeassistant/components/plugwise/switch.py @@ -68,22 +68,16 @@ async def async_setup_entry( if not coordinator.new_devices: return - entities: list[PlugwiseSwitchEntity] = [] - for device_id, device in coordinator.data.devices.items(): - if not (switches := device.get("switches")): - continue - for description in SWITCHES: - if description.key not in switches: - continue - entities.append( - PlugwiseSwitchEntity(coordinator, device_id, description) - ) - - async_add_entities(entities) - - entry.async_on_unload(coordinator.async_add_listener(_add_entities)) + async_add_entities( + PlugwiseSwitchEntity(coordinator, device_id, description) + for device_id in coordinator.new_devices + if (switches := coordinator.data.devices[device_id].get("switches")) + for description in SWITCHES + if description.key in switches + ) _add_entities() + entry.async_on_unload(coordinator.async_add_listener(_add_entities)) class PlugwiseSwitchEntity(PlugwiseEntity, SwitchEntity): From b798d7670634d4318d6a2cbef52d6a411aa2de05 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 24 Jun 2024 09:57:15 +0200 Subject: [PATCH 1078/1445] Update mypy-dev to 1.11.0a9 (#120289) --- .../bluetooth/passive_update_processor.py | 15 +++++++-------- homeassistant/components/recorder/core.py | 2 ++ homeassistant/components/sonos/__init__.py | 9 ++++----- homeassistant/components/tessie/lock.py | 2 +- requirements_test.txt | 2 +- 5 files changed, 15 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/bluetooth/passive_update_processor.py b/homeassistant/components/bluetooth/passive_update_processor.py index 29ebda3488b..3e7e4e96659 100644 --- a/homeassistant/components/bluetooth/passive_update_processor.py +++ b/homeassistant/components/bluetooth/passive_update_processor.py @@ -146,20 +146,19 @@ class PassiveBluetoothDataUpdate[_T]: """ device_change = False changed_entity_keys: set[PassiveBluetoothEntityKey] = set() - for key, device_info in new_data.devices.items(): - if device_change or self.devices.get(key, UNDEFINED) != device_info: + for device_key, device_info in new_data.devices.items(): + if device_change or self.devices.get(device_key, UNDEFINED) != device_info: device_change = True - self.devices[key] = device_info + self.devices[device_key] = device_info for incoming, current in ( (new_data.entity_descriptions, self.entity_descriptions), (new_data.entity_names, self.entity_names), (new_data.entity_data, self.entity_data), ): - # mypy can't seem to work this out - for key, data in incoming.items(): # type: ignore[attr-defined] - if current.get(key, UNDEFINED) != data: # type: ignore[attr-defined] - changed_entity_keys.add(key) # type: ignore[arg-type] - current[key] = data # type: ignore[index] + for key, data in incoming.items(): + if current.get(key, UNDEFINED) != data: + changed_entity_keys.add(key) + current[key] = data # type: ignore[assignment] # If the device changed we don't need to return the changed # entity keys as all entities will be updated return None if device_change else changed_entity_keys diff --git a/homeassistant/components/recorder/core.py b/homeassistant/components/recorder/core.py index a5eecf42f22..4e5ac04c3bf 100644 --- a/homeassistant/components/recorder/core.py +++ b/homeassistant/components/recorder/core.py @@ -85,6 +85,7 @@ from .db_schema import ( ) from .executor import DBInterruptibleThreadPoolExecutor from .migration import ( + BaseRunTimeMigration, EntityIDMigration, EventsContextIDMigration, EventTypeIDMigration, @@ -805,6 +806,7 @@ class Recorder(threading.Thread): for row in execute_stmt_lambda_element(session, get_migration_changes()) } + migrator: BaseRunTimeMigration for migrator_cls in (StatesContextIDMigration, EventsContextIDMigration): migrator = migrator_cls(session, schema_version, migration_changes) if migrator.needs_migrate(): diff --git a/homeassistant/components/sonos/__init__.py b/homeassistant/components/sonos/__init__.py index 2049cb4c8c7..912a8d04f4e 100644 --- a/homeassistant/components/sonos/__init__.py +++ b/homeassistant/components/sonos/__init__.py @@ -9,7 +9,7 @@ import datetime from functools import partial import logging import socket -from typing import TYPE_CHECKING, Any, cast +from typing import Any, cast from urllib.parse import urlparse from aiohttp import ClientError @@ -372,12 +372,11 @@ class SonosDiscoveryManager: (SonosAlarms, self.data.alarms), (SonosFavorites, self.data.favorites), ): - if TYPE_CHECKING: - coord_dict = cast(dict[str, Any], coord_dict) - if soco.household_id not in coord_dict: + c_dict: dict[str, Any] = coord_dict + if soco.household_id not in c_dict: new_coordinator = coordinator(self.hass, soco.household_id) new_coordinator.setup(soco) - coord_dict[soco.household_id] = new_coordinator + c_dict[soco.household_id] = new_coordinator speaker.setup(self.entry) except (OSError, SoCoException, Timeout) as ex: _LOGGER.warning("Failed to add SonosSpeaker using %s: %s", soco, ex) diff --git a/homeassistant/components/tessie/lock.py b/homeassistant/components/tessie/lock.py index 9457d476e32..0ea65ce4781 100644 --- a/homeassistant/components/tessie/lock.py +++ b/homeassistant/components/tessie/lock.py @@ -35,7 +35,7 @@ async def async_setup_entry( """Set up the Tessie sensor platform from a config entry.""" data = entry.runtime_data - entities = [ + entities: list[TessieEntity] = [ klass(vehicle) for klass in (TessieLockEntity, TessieCableLockEntity) for vehicle in data.vehicles diff --git a/requirements_test.txt b/requirements_test.txt index fce669c4929..460da410db6 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -11,7 +11,7 @@ astroid==3.2.2 coverage==7.5.3 freezegun==1.5.0 mock-open==1.4.0 -mypy-dev==1.11.0a8 +mypy-dev==1.11.0a9 pre-commit==3.7.1 pydantic==1.10.17 pylint==3.2.2 From 0dff7e8a55fc48375abad2733b1a1265be4a790c Mon Sep 17 00:00:00 2001 From: tronikos Date: Mon, 24 Jun 2024 01:02:30 -0700 Subject: [PATCH 1079/1445] Bump PyFlume to 0.8.7 (#120288) --- homeassistant/components/flume/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/flume/manifest.json b/homeassistant/components/flume/manifest.json index 953d9791f2f..bb6783bafbe 100644 --- a/homeassistant/components/flume/manifest.json +++ b/homeassistant/components/flume/manifest.json @@ -11,5 +11,5 @@ "documentation": "https://www.home-assistant.io/integrations/flume", "iot_class": "cloud_polling", "loggers": ["pyflume"], - "requirements": ["PyFlume==0.6.5"] + "requirements": ["PyFlume==0.8.7"] } diff --git a/requirements_all.txt b/requirements_all.txt index dbbfe7f6790..52b887723fe 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -54,7 +54,7 @@ PyChromecast==14.0.1 PyFlick==0.0.2 # homeassistant.components.flume -PyFlume==0.6.5 +PyFlume==0.8.7 # homeassistant.components.fronius PyFronius==0.7.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ddf9a87d7ee..88caa904094 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -45,7 +45,7 @@ PyChromecast==14.0.1 PyFlick==0.0.2 # homeassistant.components.flume -PyFlume==0.6.5 +PyFlume==0.8.7 # homeassistant.components.fronius PyFronius==0.7.3 From deee10813c48904bc5fabce619716adb8ed3d47b Mon Sep 17 00:00:00 2001 From: tronikos Date: Mon, 24 Jun 2024 01:08:51 -0700 Subject: [PATCH 1080/1445] Ensure flume sees the most recent notifications (#120290) --- homeassistant/components/flume/coordinator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/flume/coordinator.py b/homeassistant/components/flume/coordinator.py index 30e7962304c..c75bffdc615 100644 --- a/homeassistant/components/flume/coordinator.py +++ b/homeassistant/components/flume/coordinator.py @@ -98,7 +98,7 @@ class FlumeNotificationDataUpdateCoordinator(DataUpdateCoordinator[None]): # The related binary sensors (leak detected, high flow, low battery) # will be active until the notification is deleted in the Flume app. self.notifications = pyflume.FlumeNotificationList( - self.auth, read=None + self.auth, read=None, sort_direction="DESC" ).notification_list _LOGGER.debug("Notifications %s", self.notifications) From 158c8b84008b53d3b96559e68b8234e433a2dafb Mon Sep 17 00:00:00 2001 From: Arie Catsman <120491684+catsmanac@users.noreply.github.com> Date: Mon, 24 Jun 2024 10:37:32 +0200 Subject: [PATCH 1081/1445] Add optional test fixture collection to enphase_envoy diagnostic report (#116242) * diagnostics_fixtures * fix codespell errors * fix merge order and typo * remove pointless-string-statement --- .../components/enphase_envoy/config_flow.py | 50 +- .../components/enphase_envoy/const.py | 3 + .../components/enphase_envoy/diagnostics.py | 52 +- .../components/enphase_envoy/strings.json | 10 + tests/components/enphase_envoy/conftest.py | 13 +- .../snapshots/test_diagnostics.ambr | 8033 +++++++++++++++++ .../enphase_envoy/test_config_flow.py | 42 +- .../enphase_envoy/test_diagnostics.py | 85 +- 8 files changed, 8282 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/enphase_envoy/config_flow.py b/homeassistant/components/enphase_envoy/config_flow.py index e115f0c6ea8..695709627b7 100644 --- a/homeassistant/components/enphase_envoy/config_flow.py +++ b/homeassistant/components/enphase_envoy/config_flow.py @@ -12,12 +12,22 @@ from pyenphase import AUTH_TOKEN_MIN_VERSION, Envoy, EnvoyError import voluptuous as vol from homeassistant.components import zeroconf -from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult +from homeassistant.config_entries import ( + ConfigEntry, + ConfigFlow, + ConfigFlowResult, + OptionsFlowWithConfigEntry, +) from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.httpx_client import get_async_client -from .const import DOMAIN, INVALID_AUTH_ERRORS +from .const import ( + DOMAIN, + INVALID_AUTH_ERRORS, + OPTION_DIAGNOSTICS_INCLUDE_FIXTURES, + OPTION_DIAGNOSTICS_INCLUDE_FIXTURES_DEFAULT_VALUE, +) _LOGGER = logging.getLogger(__name__) @@ -50,6 +60,12 @@ class EnphaseConfigFlow(ConfigFlow, domain=DOMAIN): self.protovers: str | None = None self._reauth_entry: ConfigEntry | None = None + @staticmethod + @callback + def async_get_options_flow(config_entry: ConfigEntry) -> EnvoyOptionsFlowHandler: + """Options flow handler for Enphase_Envoy.""" + return EnvoyOptionsFlowHandler(config_entry) + @callback def _async_generate_schema(self) -> vol.Schema: """Generate schema.""" @@ -282,3 +298,33 @@ class EnphaseConfigFlow(ConfigFlow, domain=DOMAIN): description_placeholders=description_placeholders, errors=errors, ) + + +class EnvoyOptionsFlowHandler(OptionsFlowWithConfigEntry): + """Envoy config flow options handler.""" + + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Manage the options.""" + if user_input is not None: + return self.async_create_entry(title="", data=user_input) + + return self.async_show_form( + step_id="init", + data_schema=vol.Schema( + { + vol.Required( + OPTION_DIAGNOSTICS_INCLUDE_FIXTURES, + default=self.config_entry.options.get( + OPTION_DIAGNOSTICS_INCLUDE_FIXTURES, + OPTION_DIAGNOSTICS_INCLUDE_FIXTURES_DEFAULT_VALUE, + ), + ): bool, + } + ), + description_placeholders={ + CONF_SERIAL: self.config_entry.unique_id, + CONF_HOST: self.config_entry.data.get("host"), + }, + ) diff --git a/homeassistant/components/enphase_envoy/const.py b/homeassistant/components/enphase_envoy/const.py index fe8e7e9ec1f..80ce8604f24 100644 --- a/homeassistant/components/enphase_envoy/const.py +++ b/homeassistant/components/enphase_envoy/const.py @@ -15,3 +15,6 @@ PLATFORMS = [ ] INVALID_AUTH_ERRORS = (EnvoyAuthenticationError, EnvoyAuthenticationRequired) + +OPTION_DIAGNOSTICS_INCLUDE_FIXTURES = "diagnostics_include_fixtures" +OPTION_DIAGNOSTICS_INCLUDE_FIXTURES_DEFAULT_VALUE = False diff --git a/homeassistant/components/enphase_envoy/diagnostics.py b/homeassistant/components/enphase_envoy/diagnostics.py index 28d9690ae70..0fe7be8aaef 100644 --- a/homeassistant/components/enphase_envoy/diagnostics.py +++ b/homeassistant/components/enphase_envoy/diagnostics.py @@ -6,6 +6,8 @@ import copy from typing import TYPE_CHECKING, Any from attr import asdict +from pyenphase.envoy import Envoy +from pyenphase.exceptions import EnvoyError from homeassistant.components.diagnostics import async_redact_data from homeassistant.config_entries import ConfigEntry @@ -21,7 +23,7 @@ from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.json import json_dumps from homeassistant.util.json import json_loads -from .const import DOMAIN +from .const import DOMAIN, OPTION_DIAGNOSTICS_INCLUDE_FIXTURES from .coordinator import EnphaseUpdateCoordinator CONF_TITLE = "title" @@ -38,6 +40,46 @@ TO_REDACT = { } +async def _get_fixture_collection(envoy: Envoy, serial: str) -> dict[str, Any]: + """Collect Envoy endpoints to use for test fixture set.""" + fixture_data: dict[str, Any] = {} + end_points = [ + "/info", + "/api/v1/production", + "/api/v1/production/inverters", + "/production.json", + "/production.json?details=1", + "/production", + "/ivp/ensemble/power", + "/ivp/ensemble/inventory", + "/ivp/ensemble/dry_contacts", + "/ivp/ensemble/status", + "/ivp/ensemble/secctrl", + "/ivp/ss/dry_contact_settings", + "/admin/lib/tariff", + "/ivp/ss/gen_config", + "/ivp/ss/gen_schedule", + "/ivp/sc/pvlimit", + "/ivp/ss/pel_settings", + "/ivp/ensemble/generator", + "/ivp/meters", + "/ivp/meters/readings", + ] + + for end_point in end_points: + response = await envoy.request(end_point) + fixture_data[end_point] = response.text.replace("\n", "").replace( + serial, CLEAN_TEXT + ) + fixture_data[f"{end_point}_log"] = json_dumps( + { + "headers": dict(response.headers.items()), + "code": response.status_code, + } + ) + return fixture_data + + async def async_get_config_entry_diagnostics( hass: HomeAssistant, entry: ConfigEntry ) -> dict[str, Any]: @@ -113,12 +155,20 @@ async def async_get_config_entry_diagnostics( "ct_storage_meter": envoy.storage_meter_type, } + fixture_data: dict[str, Any] = {} + if entry.options.get(OPTION_DIAGNOSTICS_INCLUDE_FIXTURES, False): + try: + fixture_data = await _get_fixture_collection(envoy=envoy, serial=old_serial) + except EnvoyError as err: + fixture_data["Error"] = repr(err) + diagnostic_data: dict[str, Any] = { "config_entry": async_redact_data(entry.as_dict(), TO_REDACT), "envoy_properties": envoy_properties, "raw_data": json_loads(coordinator_data_cleaned), "envoy_model_data": envoy_model, "envoy_entities_by_device": json_loads(device_entities_cleaned), + "fixtures": fixture_data, } return diagnostic_data diff --git a/homeassistant/components/enphase_envoy/strings.json b/homeassistant/components/enphase_envoy/strings.json index 295aa1948f8..f7964bf2f45 100644 --- a/homeassistant/components/enphase_envoy/strings.json +++ b/homeassistant/components/enphase_envoy/strings.json @@ -36,6 +36,16 @@ "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" } }, + "options": { + "step": { + "init": { + "title": "Envoy {serial} {host} options", + "data": { + "diagnostics_include_fixtures": "Include test fixture data in diagnostic report. Use when requested to provide test data for troubleshooting or development activies. With this option enabled the diagnostic report may take more time to download. When report is created best disable this option again." + } + } + } + }, "entity": { "binary_sensor": { "communicating": { diff --git a/tests/components/enphase_envoy/conftest.py b/tests/components/enphase_envoy/conftest.py index 965af3b40fc..5dd62419b2b 100644 --- a/tests/components/enphase_envoy/conftest.py +++ b/tests/components/enphase_envoy/conftest.py @@ -339,11 +339,22 @@ def mock_envoy_fixture( raw={"varies_by": "firmware_version"}, ) mock_envoy.update = AsyncMock(return_value=mock_envoy.data) + + response = Mock() + response.status_code = 200 + response.text = "Testing request \nreplies." + response.headers = {"Hello": "World"} + mock_envoy.request = AsyncMock(return_value=response) + return mock_envoy @pytest.fixture(name="setup_enphase_envoy") -async def setup_enphase_envoy_fixture(hass: HomeAssistant, config, mock_envoy): +async def setup_enphase_envoy_fixture( + hass: HomeAssistant, + config, + mock_envoy, +): """Define a fixture to set up Enphase Envoy.""" with ( patch( diff --git a/tests/components/enphase_envoy/snapshots/test_diagnostics.ambr b/tests/components/enphase_envoy/snapshots/test_diagnostics.ambr index c2ab51a7dbd..008922e8d2b 100644 --- a/tests/components/enphase_envoy/snapshots/test_diagnostics.ambr +++ b/tests/components/enphase_envoy/snapshots/test_diagnostics.ambr @@ -3986,6 +3986,8039 @@ 'CTMETERS', ]), }), + 'fixtures': dict({ + }), + 'raw_data': dict({ + 'varies_by': 'firmware_version', + }), + }) +# --- +# name: test_entry_diagnostics_with_fixtures + dict({ + 'config_entry': dict({ + 'data': dict({ + 'host': '1.1.1.1', + 'name': '**REDACTED**', + 'password': '**REDACTED**', + 'token': '**REDACTED**', + 'username': '**REDACTED**', + }), + 'disabled_by': None, + 'domain': 'enphase_envoy', + 'entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'minor_version': 1, + 'options': dict({ + 'diagnostics_include_fixtures': True, + }), + 'pref_disable_new_entities': False, + 'pref_disable_polling': False, + 'source': 'user', + 'title': '**REDACTED**', + 'unique_id': '**REDACTED**', + 'version': 1, + }), + 'envoy_entities_by_device': list([ + dict({ + 'device': dict({ + 'area_id': None, + 'config_entries': list([ + '45a36e55aaddb2007c5f6602e0c38e72', + ]), + 'configuration_url': None, + 'connections': list([ + ]), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': '<>56789', + 'identifiers': list([ + list([ + 'enphase_envoy', + '<>', + ]), + ]), + 'is_new': False, + 'labels': list([ + ]), + 'manufacturer': 'Enphase', + 'model': 'Envoy, phases: 3, phase mode: three, net-consumption CT, production CT, storage CT', + 'name': 'Envoy <>', + 'name_by_user': None, + 'serial_number': '<>', + 'suggested_area': None, + 'sw_version': '7.1.2', + }), + 'entities': list([ + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_current_power_production', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 3, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kW', + }), + }), + 'original_device_class': 'power', + 'original_icon': 'mdi:flash', + 'original_name': 'Current power production', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'current_power_production', + 'unique_id': '<>_production', + 'unit_of_measurement': 'kW', + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'power', + 'friendly_name': 'Envoy <> Current power production', + 'icon': 'mdi:flash', + 'state_class': 'measurement', + 'unit_of_measurement': 'kW', + }), + 'entity_id': 'sensor.envoy_<>_current_power_production', + 'state': '1.234', + }), + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_energy_production_today', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Energy production today', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'daily_production', + 'unique_id': '<>_daily_production', + 'unit_of_measurement': 'kWh', + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'energy', + 'friendly_name': 'Envoy <> Energy production today', + 'icon': 'mdi:flash', + 'state_class': 'total_increasing', + 'unit_of_measurement': 'kWh', + }), + 'entity_id': 'sensor.envoy_<>_energy_production_today', + 'state': '1.234', + }), + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_energy_production_last_seven_days', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Energy production last seven days', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'seven_days_production', + 'unique_id': '<>_seven_days_production', + 'unit_of_measurement': 'kWh', + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'energy', + 'friendly_name': 'Envoy <> Energy production last seven days', + 'icon': 'mdi:flash', + 'unit_of_measurement': 'kWh', + }), + 'entity_id': 'sensor.envoy_<>_energy_production_last_seven_days', + 'state': '1.234', + }), + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_lifetime_energy_production', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 3, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'MWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime energy production', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_production', + 'unique_id': '<>_lifetime_production', + 'unit_of_measurement': 'MWh', + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'energy', + 'friendly_name': 'Envoy <> Lifetime energy production', + 'icon': 'mdi:flash', + 'state_class': 'total_increasing', + 'unit_of_measurement': 'MWh', + }), + 'entity_id': 'sensor.envoy_<>_lifetime_energy_production', + 'state': '0.00<>', + }), + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_current_power_consumption', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 3, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kW', + }), + }), + 'original_device_class': 'power', + 'original_icon': 'mdi:flash', + 'original_name': 'Current power consumption', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'current_power_consumption', + 'unique_id': '<>_consumption', + 'unit_of_measurement': 'kW', + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'power', + 'friendly_name': 'Envoy <> Current power consumption', + 'icon': 'mdi:flash', + 'state_class': 'measurement', + 'unit_of_measurement': 'kW', + }), + 'entity_id': 'sensor.envoy_<>_current_power_consumption', + 'state': '1.234', + }), + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_energy_consumption_today', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Energy consumption today', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'daily_consumption', + 'unique_id': '<>_daily_consumption', + 'unit_of_measurement': 'kWh', + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'energy', + 'friendly_name': 'Envoy <> Energy consumption today', + 'icon': 'mdi:flash', + 'state_class': 'total_increasing', + 'unit_of_measurement': 'kWh', + }), + 'entity_id': 'sensor.envoy_<>_energy_consumption_today', + 'state': '1.234', + }), + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_energy_consumption_last_seven_days', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Energy consumption last seven days', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'seven_days_consumption', + 'unique_id': '<>_seven_days_consumption', + 'unit_of_measurement': 'kWh', + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'energy', + 'friendly_name': 'Envoy <> Energy consumption last seven days', + 'icon': 'mdi:flash', + 'unit_of_measurement': 'kWh', + }), + 'entity_id': 'sensor.envoy_<>_energy_consumption_last_seven_days', + 'state': '1.234', + }), + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_lifetime_energy_consumption', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 3, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'MWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime energy consumption', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_consumption', + 'unique_id': '<>_lifetime_consumption', + 'unit_of_measurement': 'MWh', + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'energy', + 'friendly_name': 'Envoy <> Lifetime energy consumption', + 'icon': 'mdi:flash', + 'state_class': 'total_increasing', + 'unit_of_measurement': 'MWh', + }), + 'entity_id': 'sensor.envoy_<>_lifetime_energy_consumption', + 'state': '0.00<>', + }), + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_current_power_production_l1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kW', + }), + }), + 'original_device_class': 'power', + 'original_icon': 'mdi:flash', + 'original_name': 'Current power production l1', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'current_power_production_phase', + 'unique_id': '<>_production_l1', + 'unit_of_measurement': 'kW', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_energy_production_today_l1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Energy production today l1', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'daily_production_phase', + 'unique_id': '<>_daily_production_l1', + 'unit_of_measurement': 'kWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_energy_production_last_seven_days_l1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Energy production last seven days l1', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'seven_days_production_phase', + 'unique_id': '<>_seven_days_production_l1', + 'unit_of_measurement': 'kWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_lifetime_energy_production_l1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'MWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime energy production l1', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_production_phase', + 'unique_id': '<>_lifetime_production_l1', + 'unit_of_measurement': 'MWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_current_power_production_l2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kW', + }), + }), + 'original_device_class': 'power', + 'original_icon': 'mdi:flash', + 'original_name': 'Current power production l2', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'current_power_production_phase', + 'unique_id': '<>_production_l2', + 'unit_of_measurement': 'kW', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_energy_production_today_l2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Energy production today l2', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'daily_production_phase', + 'unique_id': '<>_daily_production_l2', + 'unit_of_measurement': 'kWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_energy_production_last_seven_days_l2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Energy production last seven days l2', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'seven_days_production_phase', + 'unique_id': '<>_seven_days_production_l2', + 'unit_of_measurement': 'kWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_lifetime_energy_production_l2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'MWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime energy production l2', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_production_phase', + 'unique_id': '<>_lifetime_production_l2', + 'unit_of_measurement': 'MWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_current_power_production_l3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kW', + }), + }), + 'original_device_class': 'power', + 'original_icon': 'mdi:flash', + 'original_name': 'Current power production l3', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'current_power_production_phase', + 'unique_id': '<>_production_l3', + 'unit_of_measurement': 'kW', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_energy_production_today_l3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Energy production today l3', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'daily_production_phase', + 'unique_id': '<>_daily_production_l3', + 'unit_of_measurement': 'kWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_energy_production_last_seven_days_l3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Energy production last seven days l3', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'seven_days_production_phase', + 'unique_id': '<>_seven_days_production_l3', + 'unit_of_measurement': 'kWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_lifetime_energy_production_l3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'MWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime energy production l3', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_production_phase', + 'unique_id': '<>_lifetime_production_l3', + 'unit_of_measurement': 'MWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_current_power_consumption_l1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kW', + }), + }), + 'original_device_class': 'power', + 'original_icon': 'mdi:flash', + 'original_name': 'Current power consumption l1', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'current_power_consumption_phase', + 'unique_id': '<>_consumption_l1', + 'unit_of_measurement': 'kW', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_energy_consumption_today_l1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Energy consumption today l1', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'daily_consumption_phase', + 'unique_id': '<>_daily_consumption_l1', + 'unit_of_measurement': 'kWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_energy_consumption_last_seven_days_l1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Energy consumption last seven days l1', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'seven_days_consumption_phase', + 'unique_id': '<>_seven_days_consumption_l1', + 'unit_of_measurement': 'kWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_lifetime_energy_consumption_l1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'MWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime energy consumption l1', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_consumption_phase', + 'unique_id': '<>_lifetime_consumption_l1', + 'unit_of_measurement': 'MWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_current_power_consumption_l2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kW', + }), + }), + 'original_device_class': 'power', + 'original_icon': 'mdi:flash', + 'original_name': 'Current power consumption l2', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'current_power_consumption_phase', + 'unique_id': '<>_consumption_l2', + 'unit_of_measurement': 'kW', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_energy_consumption_today_l2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Energy consumption today l2', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'daily_consumption_phase', + 'unique_id': '<>_daily_consumption_l2', + 'unit_of_measurement': 'kWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_energy_consumption_last_seven_days_l2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Energy consumption last seven days l2', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'seven_days_consumption_phase', + 'unique_id': '<>_seven_days_consumption_l2', + 'unit_of_measurement': 'kWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_lifetime_energy_consumption_l2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'MWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime energy consumption l2', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_consumption_phase', + 'unique_id': '<>_lifetime_consumption_l2', + 'unit_of_measurement': 'MWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_current_power_consumption_l3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kW', + }), + }), + 'original_device_class': 'power', + 'original_icon': 'mdi:flash', + 'original_name': 'Current power consumption l3', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'current_power_consumption_phase', + 'unique_id': '<>_consumption_l3', + 'unit_of_measurement': 'kW', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_energy_consumption_today_l3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Energy consumption today l3', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'daily_consumption_phase', + 'unique_id': '<>_daily_consumption_l3', + 'unit_of_measurement': 'kWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_energy_consumption_last_seven_days_l3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Energy consumption last seven days l3', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'seven_days_consumption_phase', + 'unique_id': '<>_seven_days_consumption_l3', + 'unit_of_measurement': 'kWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_lifetime_energy_consumption_l3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'MWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime energy consumption l3', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_consumption_phase', + 'unique_id': '<>_lifetime_consumption_l3', + 'unit_of_measurement': 'MWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_lifetime_net_energy_consumption', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 3, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'MWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime net energy consumption', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_net_consumption', + 'unique_id': '<>_lifetime_net_consumption', + 'unit_of_measurement': 'MWh', + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'energy', + 'friendly_name': 'Envoy <> Lifetime net energy consumption', + 'icon': 'mdi:flash', + 'state_class': 'total_increasing', + 'unit_of_measurement': 'MWh', + }), + 'entity_id': 'sensor.envoy_<>_lifetime_net_energy_consumption', + 'state': '0.02<>', + }), + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_lifetime_net_energy_production', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 3, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'MWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime net energy production', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_net_production', + 'unique_id': '<>_lifetime_net_production', + 'unit_of_measurement': 'MWh', + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'energy', + 'friendly_name': 'Envoy <> Lifetime net energy production', + 'icon': 'mdi:flash', + 'state_class': 'total_increasing', + 'unit_of_measurement': 'MWh', + }), + 'entity_id': 'sensor.envoy_<>_lifetime_net_energy_production', + 'state': '0.022345', + }), + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_current_net_power_consumption', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 3, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kW', + }), + }), + 'original_device_class': 'power', + 'original_icon': 'mdi:flash', + 'original_name': 'Current net power consumption', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'net_consumption', + 'unique_id': '<>_net_consumption', + 'unit_of_measurement': 'kW', + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'power', + 'friendly_name': 'Envoy <> Current net power consumption', + 'icon': 'mdi:flash', + 'state_class': 'measurement', + 'unit_of_measurement': 'kW', + }), + 'entity_id': 'sensor.envoy_<>_current_net_power_consumption', + 'state': '0.101', + }), + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_frequency_net_consumption_ct', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': 'frequency', + 'original_icon': 'mdi:flash', + 'original_name': 'Frequency net consumption CT', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'net_ct_frequency', + 'unique_id': '<>_frequency', + 'unit_of_measurement': 'Hz', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_voltage_net_consumption_ct', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'V', + }), + }), + 'original_device_class': 'voltage', + 'original_icon': 'mdi:flash', + 'original_name': 'Voltage net consumption CT', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'net_ct_voltage', + 'unique_id': '<>_voltage', + 'unit_of_measurement': 'V', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'normal', + 'not-metering', + 'check-wiring', + ]), + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_metering_status_net_consumption_ct', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': 'enum', + 'original_icon': 'mdi:flash', + 'original_name': 'Metering status net consumption CT', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'net_ct_metering_status', + 'unique_id': '<>_net_consumption_ct_metering_status', + 'unit_of_measurement': None, + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_meter_status_flags_active_net_consumption_ct', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:flash', + 'original_name': 'Meter status flags active net consumption CT', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'net_ct_status_flags', + 'unique_id': '<>_net_consumption_ct_status_flags', + 'unit_of_measurement': None, + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_lifetime_net_energy_consumption_l1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'MWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime net energy consumption l1', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_net_consumption_phase', + 'unique_id': '<>_lifetime_net_consumption_l1', + 'unit_of_measurement': 'MWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_lifetime_net_energy_production_l1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'MWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime net energy production l1', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_net_production_phase', + 'unique_id': '<>_lifetime_net_production_l1', + 'unit_of_measurement': 'MWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_current_net_power_consumption_l1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kW', + }), + }), + 'original_device_class': 'power', + 'original_icon': 'mdi:flash', + 'original_name': 'Current net power consumption l1', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'net_consumption_phase', + 'unique_id': '<>_net_consumption_l1', + 'unit_of_measurement': 'kW', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_frequency_net_consumption_ct_l1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': 'frequency', + 'original_icon': 'mdi:flash', + 'original_name': 'Frequency net consumption CT l1', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'net_ct_frequency_phase', + 'unique_id': '<>_frequency_l1', + 'unit_of_measurement': 'Hz', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_voltage_net_consumption_ct_l1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'V', + }), + }), + 'original_device_class': 'voltage', + 'original_icon': 'mdi:flash', + 'original_name': 'Voltage net consumption CT l1', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'net_ct_voltage_phase', + 'unique_id': '<>_voltage_l1', + 'unit_of_measurement': 'V', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'normal', + 'not-metering', + 'check-wiring', + ]), + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_metering_status_net_consumption_ct_l1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': 'enum', + 'original_icon': 'mdi:flash', + 'original_name': 'Metering status net consumption CT l1', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'net_ct_metering_status_phase', + 'unique_id': '<>_net_consumption_ct_metering_status_l1', + 'unit_of_measurement': None, + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_meter_status_flags_active_net_consumption_ct_l1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:flash', + 'original_name': 'Meter status flags active net consumption CT l1', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'net_ct_status_flags_phase', + 'unique_id': '<>_net_consumption_ct_status_flags_l1', + 'unit_of_measurement': None, + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_lifetime_net_energy_consumption_l2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'MWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime net energy consumption l2', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_net_consumption_phase', + 'unique_id': '<>_lifetime_net_consumption_l2', + 'unit_of_measurement': 'MWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_lifetime_net_energy_production_l2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'MWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime net energy production l2', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_net_production_phase', + 'unique_id': '<>_lifetime_net_production_l2', + 'unit_of_measurement': 'MWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_current_net_power_consumption_l2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kW', + }), + }), + 'original_device_class': 'power', + 'original_icon': 'mdi:flash', + 'original_name': 'Current net power consumption l2', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'net_consumption_phase', + 'unique_id': '<>_net_consumption_l2', + 'unit_of_measurement': 'kW', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_frequency_net_consumption_ct_l2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': 'frequency', + 'original_icon': 'mdi:flash', + 'original_name': 'Frequency net consumption CT l2', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'net_ct_frequency_phase', + 'unique_id': '<>_frequency_l2', + 'unit_of_measurement': 'Hz', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_voltage_net_consumption_ct_l2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'V', + }), + }), + 'original_device_class': 'voltage', + 'original_icon': 'mdi:flash', + 'original_name': 'Voltage net consumption CT l2', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'net_ct_voltage_phase', + 'unique_id': '<>_voltage_l2', + 'unit_of_measurement': 'V', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'normal', + 'not-metering', + 'check-wiring', + ]), + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_metering_status_net_consumption_ct_l2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': 'enum', + 'original_icon': 'mdi:flash', + 'original_name': 'Metering status net consumption CT l2', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'net_ct_metering_status_phase', + 'unique_id': '<>_net_consumption_ct_metering_status_l2', + 'unit_of_measurement': None, + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_meter_status_flags_active_net_consumption_ct_l2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:flash', + 'original_name': 'Meter status flags active net consumption CT l2', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'net_ct_status_flags_phase', + 'unique_id': '<>_net_consumption_ct_status_flags_l2', + 'unit_of_measurement': None, + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_lifetime_net_energy_consumption_l3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'MWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime net energy consumption l3', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_net_consumption_phase', + 'unique_id': '<>_lifetime_net_consumption_l3', + 'unit_of_measurement': 'MWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_lifetime_net_energy_production_l3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'MWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime net energy production l3', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_net_production_phase', + 'unique_id': '<>_lifetime_net_production_l3', + 'unit_of_measurement': 'MWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_current_net_power_consumption_l3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kW', + }), + }), + 'original_device_class': 'power', + 'original_icon': 'mdi:flash', + 'original_name': 'Current net power consumption l3', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'net_consumption_phase', + 'unique_id': '<>_net_consumption_l3', + 'unit_of_measurement': 'kW', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_frequency_net_consumption_ct_l3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': 'frequency', + 'original_icon': 'mdi:flash', + 'original_name': 'Frequency net consumption CT l3', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'net_ct_frequency_phase', + 'unique_id': '<>_frequency_l3', + 'unit_of_measurement': 'Hz', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_voltage_net_consumption_ct_l3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'V', + }), + }), + 'original_device_class': 'voltage', + 'original_icon': 'mdi:flash', + 'original_name': 'Voltage net consumption CT l3', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'net_ct_voltage_phase', + 'unique_id': '<>_voltage_l3', + 'unit_of_measurement': 'V', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'normal', + 'not-metering', + 'check-wiring', + ]), + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_metering_status_net_consumption_ct_l3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': 'enum', + 'original_icon': 'mdi:flash', + 'original_name': 'Metering status net consumption CT l3', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'net_ct_metering_status_phase', + 'unique_id': '<>_net_consumption_ct_metering_status_l3', + 'unit_of_measurement': None, + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_meter_status_flags_active_net_consumption_ct_l3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:flash', + 'original_name': 'Meter status flags active net consumption CT l3', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'net_ct_status_flags_phase', + 'unique_id': '<>_net_consumption_ct_status_flags_l3', + 'unit_of_measurement': None, + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'normal', + 'not-metering', + 'check-wiring', + ]), + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_metering_status_production_ct', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': 'enum', + 'original_icon': 'mdi:flash', + 'original_name': 'Metering status production CT', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'production_ct_metering_status', + 'unique_id': '<>_production_ct_metering_status', + 'unit_of_measurement': None, + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_meter_status_flags_active_production_ct', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:flash', + 'original_name': 'Meter status flags active production CT', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'production_ct_status_flags', + 'unique_id': '<>_production_ct_status_flags', + 'unit_of_measurement': None, + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'normal', + 'not-metering', + 'check-wiring', + ]), + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_metering_status_production_ct_l1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': 'enum', + 'original_icon': 'mdi:flash', + 'original_name': 'Metering status production CT l1', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'production_ct_metering_status_phase', + 'unique_id': '<>_production_ct_metering_status_l1', + 'unit_of_measurement': None, + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_meter_status_flags_active_production_ct_l1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:flash', + 'original_name': 'Meter status flags active production CT l1', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'production_ct_status_flags_phase', + 'unique_id': '<>_production_ct_status_flags_l1', + 'unit_of_measurement': None, + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'normal', + 'not-metering', + 'check-wiring', + ]), + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_metering_status_production_ct_l2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': 'enum', + 'original_icon': 'mdi:flash', + 'original_name': 'Metering status production CT l2', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'production_ct_metering_status_phase', + 'unique_id': '<>_production_ct_metering_status_l2', + 'unit_of_measurement': None, + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_meter_status_flags_active_production_ct_l2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:flash', + 'original_name': 'Meter status flags active production CT l2', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'production_ct_status_flags_phase', + 'unique_id': '<>_production_ct_status_flags_l2', + 'unit_of_measurement': None, + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'normal', + 'not-metering', + 'check-wiring', + ]), + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_metering_status_production_ct_l3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': 'enum', + 'original_icon': 'mdi:flash', + 'original_name': 'Metering status production CT l3', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'production_ct_metering_status_phase', + 'unique_id': '<>_production_ct_metering_status_l3', + 'unit_of_measurement': None, + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_meter_status_flags_active_production_ct_l3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:flash', + 'original_name': 'Meter status flags active production CT l3', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'production_ct_status_flags_phase', + 'unique_id': '<>_production_ct_status_flags_l3', + 'unit_of_measurement': None, + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_lifetime_battery_energy_discharged', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 3, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'MWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime battery energy discharged', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_battery_discharged', + 'unique_id': '<>_lifetime_battery_discharged', + 'unit_of_measurement': 'MWh', + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'energy', + 'friendly_name': 'Envoy <> Lifetime battery energy discharged', + 'icon': 'mdi:flash', + 'state_class': 'total_increasing', + 'unit_of_measurement': 'MWh', + }), + 'entity_id': 'sensor.envoy_<>_lifetime_battery_energy_discharged', + 'state': '0.03<>', + }), + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_lifetime_battery_energy_charged', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 3, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'MWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime battery energy charged', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_battery_charged', + 'unique_id': '<>_lifetime_battery_charged', + 'unit_of_measurement': 'MWh', + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'energy', + 'friendly_name': 'Envoy <> Lifetime battery energy charged', + 'icon': 'mdi:flash', + 'state_class': 'total_increasing', + 'unit_of_measurement': 'MWh', + }), + 'entity_id': 'sensor.envoy_<>_lifetime_battery_energy_charged', + 'state': '0.032345', + }), + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_current_battery_discharge', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 3, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kW', + }), + }), + 'original_device_class': 'power', + 'original_icon': 'mdi:flash', + 'original_name': 'Current battery discharge', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'battery_discharge', + 'unique_id': '<>_battery_discharge', + 'unit_of_measurement': 'kW', + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'power', + 'friendly_name': 'Envoy <> Current battery discharge', + 'icon': 'mdi:flash', + 'state_class': 'measurement', + 'unit_of_measurement': 'kW', + }), + 'entity_id': 'sensor.envoy_<>_current_battery_discharge', + 'state': '0.103', + }), + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_voltage_storage_ct', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'V', + }), + }), + 'original_device_class': 'voltage', + 'original_icon': 'mdi:flash', + 'original_name': 'Voltage storage CT', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'storage_ct_voltage', + 'unique_id': '<>_storage_voltage', + 'unit_of_measurement': 'V', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'normal', + 'not-metering', + 'check-wiring', + ]), + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_metering_status_storage_ct', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': 'enum', + 'original_icon': 'mdi:flash', + 'original_name': 'Metering status storage CT', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'storage_ct_metering_status', + 'unique_id': '<>_storage_ct_metering_status', + 'unit_of_measurement': None, + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_meter_status_flags_active_storage_ct', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:flash', + 'original_name': 'Meter status flags active storage CT', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'storage_ct_status_flags', + 'unique_id': '<>_storage_ct_status_flags', + 'unit_of_measurement': None, + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_lifetime_battery_energy_discharged_l1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'MWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime battery energy discharged l1', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_battery_discharged_phase', + 'unique_id': '<>_lifetime_battery_discharged_l1', + 'unit_of_measurement': 'MWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_lifetime_battery_energy_charged_l1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'MWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime battery energy charged l1', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_battery_charged_phase', + 'unique_id': '<>_lifetime_battery_charged_l1', + 'unit_of_measurement': 'MWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_current_battery_discharge_l1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kW', + }), + }), + 'original_device_class': 'power', + 'original_icon': 'mdi:flash', + 'original_name': 'Current battery discharge l1', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'battery_discharge_phase', + 'unique_id': '<>_battery_discharge_l1', + 'unit_of_measurement': 'kW', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_voltage_storage_ct_l1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'V', + }), + }), + 'original_device_class': 'voltage', + 'original_icon': 'mdi:flash', + 'original_name': 'Voltage storage CT l1', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'storage_ct_voltage_phase', + 'unique_id': '<>_storage_voltage_l1', + 'unit_of_measurement': 'V', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'normal', + 'not-metering', + 'check-wiring', + ]), + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_metering_status_storage_ct_l1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': 'enum', + 'original_icon': 'mdi:flash', + 'original_name': 'Metering status storage CT l1', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'storage_ct_metering_status_phase', + 'unique_id': '<>_storage_ct_metering_status_l1', + 'unit_of_measurement': None, + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_meter_status_flags_active_storage_ct_l1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:flash', + 'original_name': 'Meter status flags active storage CT l1', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'storage_ct_status_flags_phase', + 'unique_id': '<>_storage_ct_status_flags_l1', + 'unit_of_measurement': None, + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_lifetime_battery_energy_discharged_l2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'MWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime battery energy discharged l2', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_battery_discharged_phase', + 'unique_id': '<>_lifetime_battery_discharged_l2', + 'unit_of_measurement': 'MWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_lifetime_battery_energy_charged_l2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'MWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime battery energy charged l2', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_battery_charged_phase', + 'unique_id': '<>_lifetime_battery_charged_l2', + 'unit_of_measurement': 'MWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_current_battery_discharge_l2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kW', + }), + }), + 'original_device_class': 'power', + 'original_icon': 'mdi:flash', + 'original_name': 'Current battery discharge l2', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'battery_discharge_phase', + 'unique_id': '<>_battery_discharge_l2', + 'unit_of_measurement': 'kW', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_voltage_storage_ct_l2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'V', + }), + }), + 'original_device_class': 'voltage', + 'original_icon': 'mdi:flash', + 'original_name': 'Voltage storage CT l2', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'storage_ct_voltage_phase', + 'unique_id': '<>_storage_voltage_l2', + 'unit_of_measurement': 'V', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'normal', + 'not-metering', + 'check-wiring', + ]), + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_metering_status_storage_ct_l2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': 'enum', + 'original_icon': 'mdi:flash', + 'original_name': 'Metering status storage CT l2', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'storage_ct_metering_status_phase', + 'unique_id': '<>_storage_ct_metering_status_l2', + 'unit_of_measurement': None, + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_meter_status_flags_active_storage_ct_l2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:flash', + 'original_name': 'Meter status flags active storage CT l2', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'storage_ct_status_flags_phase', + 'unique_id': '<>_storage_ct_status_flags_l2', + 'unit_of_measurement': None, + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_lifetime_battery_energy_discharged_l3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'MWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime battery energy discharged l3', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_battery_discharged_phase', + 'unique_id': '<>_lifetime_battery_discharged_l3', + 'unit_of_measurement': 'MWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_lifetime_battery_energy_charged_l3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'MWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime battery energy charged l3', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_battery_charged_phase', + 'unique_id': '<>_lifetime_battery_charged_l3', + 'unit_of_measurement': 'MWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_current_battery_discharge_l3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kW', + }), + }), + 'original_device_class': 'power', + 'original_icon': 'mdi:flash', + 'original_name': 'Current battery discharge l3', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'battery_discharge_phase', + 'unique_id': '<>_battery_discharge_l3', + 'unit_of_measurement': 'kW', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_voltage_storage_ct_l3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'V', + }), + }), + 'original_device_class': 'voltage', + 'original_icon': 'mdi:flash', + 'original_name': 'Voltage storage CT l3', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'storage_ct_voltage_phase', + 'unique_id': '<>_storage_voltage_l3', + 'unit_of_measurement': 'V', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'normal', + 'not-metering', + 'check-wiring', + ]), + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_metering_status_storage_ct_l3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': 'enum', + 'original_icon': 'mdi:flash', + 'original_name': 'Metering status storage CT l3', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'storage_ct_metering_status_phase', + 'unique_id': '<>_storage_ct_metering_status_l3', + 'unit_of_measurement': None, + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_meter_status_flags_active_storage_ct_l3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:flash', + 'original_name': 'Meter status flags active storage CT l3', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'storage_ct_status_flags_phase', + 'unique_id': '<>_storage_ct_status_flags_l3', + 'unit_of_measurement': None, + }), + 'state': None, + }), + ]), + }), + dict({ + 'device': dict({ + 'area_id': None, + 'config_entries': list([ + '45a36e55aaddb2007c5f6602e0c38e72', + ]), + 'configuration_url': None, + 'connections': list([ + ]), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'identifiers': list([ + list([ + 'enphase_envoy', + '1', + ]), + ]), + 'is_new': False, + 'labels': list([ + ]), + 'manufacturer': 'Enphase', + 'model': 'Inverter', + 'name': 'Inverter 1', + 'name_by_user': None, + 'serial_number': None, + 'suggested_area': None, + 'sw_version': None, + }), + 'entities': list([ + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.inverter_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': 'power', + 'original_icon': 'mdi:flash', + 'original_name': None, + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '1', + 'unit_of_measurement': 'W', + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'power', + 'friendly_name': 'Inverter 1', + 'icon': 'mdi:flash', + 'state_class': 'measurement', + 'unit_of_measurement': 'W', + }), + 'entity_id': 'sensor.inverter_1', + 'state': '1', + }), + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.inverter_1_last_reported', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': 'timestamp', + 'original_icon': 'mdi:flash', + 'original_name': 'Last reported', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'last_reported', + 'unique_id': '1_last_reported', + 'unit_of_measurement': None, + }), + 'state': None, + }), + ]), + }), + ]), + 'envoy_model_data': dict({ + 'ctmeter_consumption': dict({ + '__type': "", + 'repr': "EnvoyMeterData(eid='100000020', timestamp=1708006120, energy_delivered=21234, energy_received=22345, active_power=101, power_factor=0.21, voltage=112, current=0.3, frequency=50.2, state=, measurement_type=, metering_status=, status_flags=[])", + }), + 'ctmeter_consumption_phases': dict({ + 'L1': dict({ + '__type': "", + 'repr': "EnvoyMeterData(eid='100000021', timestamp=1708006121, energy_delivered=212341, energy_received=223451, active_power=21, power_factor=0.22, voltage=112, current=0.3, frequency=50.2, state=, measurement_type=, metering_status=, status_flags=[])", + }), + 'L2': dict({ + '__type': "", + 'repr': "EnvoyMeterData(eid='100000022', timestamp=1708006122, energy_delivered=212342, energy_received=223452, active_power=31, power_factor=0.23, voltage=112, current=0.3, frequency=50.2, state=, measurement_type=, metering_status=, status_flags=[])", + }), + 'L3': dict({ + '__type': "", + 'repr': "EnvoyMeterData(eid='100000023', timestamp=1708006123, energy_delivered=212343, energy_received=223453, active_power=51, power_factor=0.24, voltage=112, current=0.3, frequency=50.2, state=, measurement_type=, metering_status=, status_flags=[])", + }), + }), + 'ctmeter_production': dict({ + '__type': "", + 'repr': "EnvoyMeterData(eid='100000010', timestamp=1708006110, energy_delivered=11234, energy_received=12345, active_power=100, power_factor=0.11, voltage=111, current=0.2, frequency=50.1, state=, measurement_type=, metering_status=, status_flags=[, ])", + }), + 'ctmeter_production_phases': dict({ + 'L1': dict({ + '__type': "", + 'repr': "EnvoyMeterData(eid='100000011', timestamp=1708006111, energy_delivered=112341, energy_received=123451, active_power=20, power_factor=0.12, voltage=111, current=0.2, frequency=50.1, state=, measurement_type=, metering_status=, status_flags=[])", + }), + 'L2': dict({ + '__type': "", + 'repr': "EnvoyMeterData(eid='100000012', timestamp=1708006112, energy_delivered=112342, energy_received=123452, active_power=30, power_factor=0.13, voltage=111, current=0.2, frequency=50.1, state=, measurement_type=, metering_status=, status_flags=[])", + }), + 'L3': dict({ + '__type': "", + 'repr': "EnvoyMeterData(eid='100000013', timestamp=1708006113, energy_delivered=112343, energy_received=123453, active_power=50, power_factor=0.14, voltage=111, current=0.2, frequency=50.1, state=, measurement_type=, metering_status=, status_flags=[])", + }), + }), + 'ctmeter_storage': dict({ + '__type': "", + 'repr': "EnvoyMeterData(eid='100000030', timestamp=1708006120, energy_delivered=31234, energy_received=32345, active_power=103, power_factor=0.23, voltage=113, current=0.4, frequency=50.3, state=, measurement_type=, metering_status=, status_flags=[])", + }), + 'ctmeter_storage_phases': dict({ + 'L1': dict({ + '__type': "", + 'repr': "EnvoyMeterData(eid='100000031', timestamp=1708006121, energy_delivered=312341, energy_received=323451, active_power=22, power_factor=0.32, voltage=113, current=0.4, frequency=50.3, state=, measurement_type=, metering_status=, status_flags=[])", + }), + 'L2': dict({ + '__type': "", + 'repr': "EnvoyMeterData(eid='100000032', timestamp=1708006122, energy_delivered=312342, energy_received=323452, active_power=33, power_factor=0.23, voltage=112, current=0.3, frequency=50.2, state=, measurement_type=, metering_status=, status_flags=[])", + }), + 'L3': dict({ + '__type': "", + 'repr': "EnvoyMeterData(eid='100000033', timestamp=1708006123, energy_delivered=312343, energy_received=323453, active_power=53, power_factor=0.24, voltage=112, current=0.3, frequency=50.2, state=, measurement_type=, metering_status=, status_flags=[])", + }), + }), + 'dry_contact_settings': dict({ + }), + 'dry_contact_status': dict({ + }), + 'encharge_aggregate': None, + 'encharge_inventory': None, + 'encharge_power': None, + 'enpower': None, + 'inverters': dict({ + '1': dict({ + '__type': "", + 'repr': "EnvoyInverter(serial_number='1', last_report_date=1, last_report_watts=1, max_report_watts=1)", + }), + }), + 'system_consumption': dict({ + '__type': "", + 'repr': 'EnvoySystemConsumption(watt_hours_lifetime=1234, watt_hours_last_7_days=1234, watt_hours_today=1234, watts_now=1234)', + }), + 'system_consumption_phases': dict({ + 'L1': dict({ + '__type': "", + 'repr': 'EnvoySystemConsumption(watt_hours_lifetime=1322, watt_hours_last_7_days=1321, watt_hours_today=1323, watts_now=1324)', + }), + 'L2': dict({ + '__type': "", + 'repr': 'EnvoySystemConsumption(watt_hours_lifetime=2322, watt_hours_last_7_days=2321, watt_hours_today=2323, watts_now=2324)', + }), + 'L3': dict({ + '__type': "", + 'repr': 'EnvoySystemConsumption(watt_hours_lifetime=3322, watt_hours_last_7_days=3321, watt_hours_today=3323, watts_now=3324)', + }), + }), + 'system_production': dict({ + '__type': "", + 'repr': 'EnvoySystemProduction(watt_hours_lifetime=1234, watt_hours_last_7_days=1234, watt_hours_today=1234, watts_now=1234)', + }), + 'system_production_phases': dict({ + 'L1': dict({ + '__type': "", + 'repr': 'EnvoySystemProduction(watt_hours_lifetime=1232, watt_hours_last_7_days=1231, watt_hours_today=1233, watts_now=1234)', + }), + 'L2': dict({ + '__type': "", + 'repr': 'EnvoySystemProduction(watt_hours_lifetime=2232, watt_hours_last_7_days=2231, watt_hours_today=2233, watts_now=2234)', + }), + 'L3': dict({ + '__type': "", + 'repr': 'EnvoySystemProduction(watt_hours_lifetime=3232, watt_hours_last_7_days=3231, watt_hours_today=3233, watts_now=3234)', + }), + }), + 'tariff': None, + }), + 'envoy_properties': dict({ + 'active_phasecount': 3, + 'ct_consumption_meter': 'net-consumption', + 'ct_count': 3, + 'ct_production_meter': 'production', + 'ct_storage_meter': 'storage', + 'envoy_firmware': '7.1.2', + 'envoy_model': 'Envoy, phases: 3, phase mode: three, net-consumption CT, production CT, storage CT', + 'part_number': '123456789', + 'phase_count': 3, + 'phase_mode': 'three', + 'supported_features': list([ + 'INVERTERS', + 'METERING', + 'PRODUCTION', + 'THREEPHASE', + 'CTMETERS', + ]), + }), + 'fixtures': dict({ + '/admin/lib/tariff': 'Testing request replies.', + '/admin/lib/tariff_log': '{"headers":{"Hello":"World"},"code":200}', + '/api/v1/production': 'Testing request replies.', + '/api/v1/production/inverters': 'Testing request replies.', + '/api/v1/production/inverters_log': '{"headers":{"Hello":"World"},"code":200}', + '/api/v1/production_log': '{"headers":{"Hello":"World"},"code":200}', + '/info': 'Testing request replies.', + '/info_log': '{"headers":{"Hello":"World"},"code":200}', + '/ivp/ensemble/dry_contacts': 'Testing request replies.', + '/ivp/ensemble/dry_contacts_log': '{"headers":{"Hello":"World"},"code":200}', + '/ivp/ensemble/generator': 'Testing request replies.', + '/ivp/ensemble/generator_log': '{"headers":{"Hello":"World"},"code":200}', + '/ivp/ensemble/inventory': 'Testing request replies.', + '/ivp/ensemble/inventory_log': '{"headers":{"Hello":"World"},"code":200}', + '/ivp/ensemble/power': 'Testing request replies.', + '/ivp/ensemble/power_log': '{"headers":{"Hello":"World"},"code":200}', + '/ivp/ensemble/secctrl': 'Testing request replies.', + '/ivp/ensemble/secctrl_log': '{"headers":{"Hello":"World"},"code":200}', + '/ivp/ensemble/status': 'Testing request replies.', + '/ivp/ensemble/status_log': '{"headers":{"Hello":"World"},"code":200}', + '/ivp/meters': 'Testing request replies.', + '/ivp/meters/readings': 'Testing request replies.', + '/ivp/meters/readings_log': '{"headers":{"Hello":"World"},"code":200}', + '/ivp/meters_log': '{"headers":{"Hello":"World"},"code":200}', + '/ivp/sc/pvlimit': 'Testing request replies.', + '/ivp/sc/pvlimit_log': '{"headers":{"Hello":"World"},"code":200}', + '/ivp/ss/dry_contact_settings': 'Testing request replies.', + '/ivp/ss/dry_contact_settings_log': '{"headers":{"Hello":"World"},"code":200}', + '/ivp/ss/gen_config': 'Testing request replies.', + '/ivp/ss/gen_config_log': '{"headers":{"Hello":"World"},"code":200}', + '/ivp/ss/gen_schedule': 'Testing request replies.', + '/ivp/ss/gen_schedule_log': '{"headers":{"Hello":"World"},"code":200}', + '/ivp/ss/pel_settings': 'Testing request replies.', + '/ivp/ss/pel_settings_log': '{"headers":{"Hello":"World"},"code":200}', + '/production': 'Testing request replies.', + '/production.json': 'Testing request replies.', + '/production.json?details=1': 'Testing request replies.', + '/production.json?details=1_log': '{"headers":{"Hello":"World"},"code":200}', + '/production.json_log': '{"headers":{"Hello":"World"},"code":200}', + '/production_log': '{"headers":{"Hello":"World"},"code":200}', + }), + 'raw_data': dict({ + 'varies_by': 'firmware_version', + }), + }) +# --- +# name: test_entry_diagnostics_with_fixtures_with_error + dict({ + 'config_entry': dict({ + 'data': dict({ + 'host': '1.1.1.1', + 'name': '**REDACTED**', + 'password': '**REDACTED**', + 'token': '**REDACTED**', + 'username': '**REDACTED**', + }), + 'disabled_by': None, + 'domain': 'enphase_envoy', + 'entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'minor_version': 1, + 'options': dict({ + 'diagnostics_include_fixtures': True, + }), + 'pref_disable_new_entities': False, + 'pref_disable_polling': False, + 'source': 'user', + 'title': '**REDACTED**', + 'unique_id': '**REDACTED**', + 'version': 1, + }), + 'envoy_entities_by_device': list([ + dict({ + 'device': dict({ + 'area_id': None, + 'config_entries': list([ + '45a36e55aaddb2007c5f6602e0c38e72', + ]), + 'configuration_url': None, + 'connections': list([ + ]), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': '<>56789', + 'identifiers': list([ + list([ + 'enphase_envoy', + '<>', + ]), + ]), + 'is_new': False, + 'labels': list([ + ]), + 'manufacturer': 'Enphase', + 'model': 'Envoy, phases: 3, phase mode: three, net-consumption CT, production CT, storage CT', + 'name': 'Envoy <>', + 'name_by_user': None, + 'serial_number': '<>', + 'suggested_area': None, + 'sw_version': '7.1.2', + }), + 'entities': list([ + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_current_power_production', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 3, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kW', + }), + }), + 'original_device_class': 'power', + 'original_icon': 'mdi:flash', + 'original_name': 'Current power production', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'current_power_production', + 'unique_id': '<>_production', + 'unit_of_measurement': 'kW', + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'power', + 'friendly_name': 'Envoy <> Current power production', + 'icon': 'mdi:flash', + 'state_class': 'measurement', + 'unit_of_measurement': 'kW', + }), + 'entity_id': 'sensor.envoy_<>_current_power_production', + 'state': '1.234', + }), + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_energy_production_today', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Energy production today', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'daily_production', + 'unique_id': '<>_daily_production', + 'unit_of_measurement': 'kWh', + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'energy', + 'friendly_name': 'Envoy <> Energy production today', + 'icon': 'mdi:flash', + 'state_class': 'total_increasing', + 'unit_of_measurement': 'kWh', + }), + 'entity_id': 'sensor.envoy_<>_energy_production_today', + 'state': '1.234', + }), + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_energy_production_last_seven_days', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Energy production last seven days', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'seven_days_production', + 'unique_id': '<>_seven_days_production', + 'unit_of_measurement': 'kWh', + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'energy', + 'friendly_name': 'Envoy <> Energy production last seven days', + 'icon': 'mdi:flash', + 'unit_of_measurement': 'kWh', + }), + 'entity_id': 'sensor.envoy_<>_energy_production_last_seven_days', + 'state': '1.234', + }), + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_lifetime_energy_production', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 3, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'MWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime energy production', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_production', + 'unique_id': '<>_lifetime_production', + 'unit_of_measurement': 'MWh', + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'energy', + 'friendly_name': 'Envoy <> Lifetime energy production', + 'icon': 'mdi:flash', + 'state_class': 'total_increasing', + 'unit_of_measurement': 'MWh', + }), + 'entity_id': 'sensor.envoy_<>_lifetime_energy_production', + 'state': '0.00<>', + }), + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_current_power_consumption', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 3, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kW', + }), + }), + 'original_device_class': 'power', + 'original_icon': 'mdi:flash', + 'original_name': 'Current power consumption', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'current_power_consumption', + 'unique_id': '<>_consumption', + 'unit_of_measurement': 'kW', + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'power', + 'friendly_name': 'Envoy <> Current power consumption', + 'icon': 'mdi:flash', + 'state_class': 'measurement', + 'unit_of_measurement': 'kW', + }), + 'entity_id': 'sensor.envoy_<>_current_power_consumption', + 'state': '1.234', + }), + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_energy_consumption_today', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Energy consumption today', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'daily_consumption', + 'unique_id': '<>_daily_consumption', + 'unit_of_measurement': 'kWh', + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'energy', + 'friendly_name': 'Envoy <> Energy consumption today', + 'icon': 'mdi:flash', + 'state_class': 'total_increasing', + 'unit_of_measurement': 'kWh', + }), + 'entity_id': 'sensor.envoy_<>_energy_consumption_today', + 'state': '1.234', + }), + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_energy_consumption_last_seven_days', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Energy consumption last seven days', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'seven_days_consumption', + 'unique_id': '<>_seven_days_consumption', + 'unit_of_measurement': 'kWh', + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'energy', + 'friendly_name': 'Envoy <> Energy consumption last seven days', + 'icon': 'mdi:flash', + 'unit_of_measurement': 'kWh', + }), + 'entity_id': 'sensor.envoy_<>_energy_consumption_last_seven_days', + 'state': '1.234', + }), + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_lifetime_energy_consumption', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 3, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'MWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime energy consumption', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_consumption', + 'unique_id': '<>_lifetime_consumption', + 'unit_of_measurement': 'MWh', + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'energy', + 'friendly_name': 'Envoy <> Lifetime energy consumption', + 'icon': 'mdi:flash', + 'state_class': 'total_increasing', + 'unit_of_measurement': 'MWh', + }), + 'entity_id': 'sensor.envoy_<>_lifetime_energy_consumption', + 'state': '0.00<>', + }), + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_current_power_production_l1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kW', + }), + }), + 'original_device_class': 'power', + 'original_icon': 'mdi:flash', + 'original_name': 'Current power production l1', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'current_power_production_phase', + 'unique_id': '<>_production_l1', + 'unit_of_measurement': 'kW', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_energy_production_today_l1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Energy production today l1', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'daily_production_phase', + 'unique_id': '<>_daily_production_l1', + 'unit_of_measurement': 'kWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_energy_production_last_seven_days_l1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Energy production last seven days l1', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'seven_days_production_phase', + 'unique_id': '<>_seven_days_production_l1', + 'unit_of_measurement': 'kWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_lifetime_energy_production_l1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'MWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime energy production l1', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_production_phase', + 'unique_id': '<>_lifetime_production_l1', + 'unit_of_measurement': 'MWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_current_power_production_l2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kW', + }), + }), + 'original_device_class': 'power', + 'original_icon': 'mdi:flash', + 'original_name': 'Current power production l2', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'current_power_production_phase', + 'unique_id': '<>_production_l2', + 'unit_of_measurement': 'kW', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_energy_production_today_l2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Energy production today l2', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'daily_production_phase', + 'unique_id': '<>_daily_production_l2', + 'unit_of_measurement': 'kWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_energy_production_last_seven_days_l2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Energy production last seven days l2', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'seven_days_production_phase', + 'unique_id': '<>_seven_days_production_l2', + 'unit_of_measurement': 'kWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_lifetime_energy_production_l2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'MWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime energy production l2', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_production_phase', + 'unique_id': '<>_lifetime_production_l2', + 'unit_of_measurement': 'MWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_current_power_production_l3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kW', + }), + }), + 'original_device_class': 'power', + 'original_icon': 'mdi:flash', + 'original_name': 'Current power production l3', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'current_power_production_phase', + 'unique_id': '<>_production_l3', + 'unit_of_measurement': 'kW', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_energy_production_today_l3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Energy production today l3', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'daily_production_phase', + 'unique_id': '<>_daily_production_l3', + 'unit_of_measurement': 'kWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_energy_production_last_seven_days_l3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Energy production last seven days l3', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'seven_days_production_phase', + 'unique_id': '<>_seven_days_production_l3', + 'unit_of_measurement': 'kWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_lifetime_energy_production_l3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'MWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime energy production l3', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_production_phase', + 'unique_id': '<>_lifetime_production_l3', + 'unit_of_measurement': 'MWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_current_power_consumption_l1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kW', + }), + }), + 'original_device_class': 'power', + 'original_icon': 'mdi:flash', + 'original_name': 'Current power consumption l1', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'current_power_consumption_phase', + 'unique_id': '<>_consumption_l1', + 'unit_of_measurement': 'kW', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_energy_consumption_today_l1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Energy consumption today l1', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'daily_consumption_phase', + 'unique_id': '<>_daily_consumption_l1', + 'unit_of_measurement': 'kWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_energy_consumption_last_seven_days_l1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Energy consumption last seven days l1', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'seven_days_consumption_phase', + 'unique_id': '<>_seven_days_consumption_l1', + 'unit_of_measurement': 'kWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_lifetime_energy_consumption_l1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'MWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime energy consumption l1', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_consumption_phase', + 'unique_id': '<>_lifetime_consumption_l1', + 'unit_of_measurement': 'MWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_current_power_consumption_l2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kW', + }), + }), + 'original_device_class': 'power', + 'original_icon': 'mdi:flash', + 'original_name': 'Current power consumption l2', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'current_power_consumption_phase', + 'unique_id': '<>_consumption_l2', + 'unit_of_measurement': 'kW', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_energy_consumption_today_l2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Energy consumption today l2', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'daily_consumption_phase', + 'unique_id': '<>_daily_consumption_l2', + 'unit_of_measurement': 'kWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_energy_consumption_last_seven_days_l2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Energy consumption last seven days l2', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'seven_days_consumption_phase', + 'unique_id': '<>_seven_days_consumption_l2', + 'unit_of_measurement': 'kWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_lifetime_energy_consumption_l2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'MWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime energy consumption l2', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_consumption_phase', + 'unique_id': '<>_lifetime_consumption_l2', + 'unit_of_measurement': 'MWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_current_power_consumption_l3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kW', + }), + }), + 'original_device_class': 'power', + 'original_icon': 'mdi:flash', + 'original_name': 'Current power consumption l3', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'current_power_consumption_phase', + 'unique_id': '<>_consumption_l3', + 'unit_of_measurement': 'kW', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_energy_consumption_today_l3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Energy consumption today l3', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'daily_consumption_phase', + 'unique_id': '<>_daily_consumption_l3', + 'unit_of_measurement': 'kWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_energy_consumption_last_seven_days_l3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Energy consumption last seven days l3', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'seven_days_consumption_phase', + 'unique_id': '<>_seven_days_consumption_l3', + 'unit_of_measurement': 'kWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_lifetime_energy_consumption_l3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'MWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime energy consumption l3', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_consumption_phase', + 'unique_id': '<>_lifetime_consumption_l3', + 'unit_of_measurement': 'MWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_lifetime_net_energy_consumption', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 3, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'MWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime net energy consumption', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_net_consumption', + 'unique_id': '<>_lifetime_net_consumption', + 'unit_of_measurement': 'MWh', + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'energy', + 'friendly_name': 'Envoy <> Lifetime net energy consumption', + 'icon': 'mdi:flash', + 'state_class': 'total_increasing', + 'unit_of_measurement': 'MWh', + }), + 'entity_id': 'sensor.envoy_<>_lifetime_net_energy_consumption', + 'state': '0.02<>', + }), + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_lifetime_net_energy_production', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 3, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'MWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime net energy production', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_net_production', + 'unique_id': '<>_lifetime_net_production', + 'unit_of_measurement': 'MWh', + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'energy', + 'friendly_name': 'Envoy <> Lifetime net energy production', + 'icon': 'mdi:flash', + 'state_class': 'total_increasing', + 'unit_of_measurement': 'MWh', + }), + 'entity_id': 'sensor.envoy_<>_lifetime_net_energy_production', + 'state': '0.022345', + }), + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_current_net_power_consumption', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 3, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kW', + }), + }), + 'original_device_class': 'power', + 'original_icon': 'mdi:flash', + 'original_name': 'Current net power consumption', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'net_consumption', + 'unique_id': '<>_net_consumption', + 'unit_of_measurement': 'kW', + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'power', + 'friendly_name': 'Envoy <> Current net power consumption', + 'icon': 'mdi:flash', + 'state_class': 'measurement', + 'unit_of_measurement': 'kW', + }), + 'entity_id': 'sensor.envoy_<>_current_net_power_consumption', + 'state': '0.101', + }), + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_frequency_net_consumption_ct', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': 'frequency', + 'original_icon': 'mdi:flash', + 'original_name': 'Frequency net consumption CT', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'net_ct_frequency', + 'unique_id': '<>_frequency', + 'unit_of_measurement': 'Hz', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_voltage_net_consumption_ct', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'V', + }), + }), + 'original_device_class': 'voltage', + 'original_icon': 'mdi:flash', + 'original_name': 'Voltage net consumption CT', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'net_ct_voltage', + 'unique_id': '<>_voltage', + 'unit_of_measurement': 'V', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'normal', + 'not-metering', + 'check-wiring', + ]), + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_metering_status_net_consumption_ct', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': 'enum', + 'original_icon': 'mdi:flash', + 'original_name': 'Metering status net consumption CT', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'net_ct_metering_status', + 'unique_id': '<>_net_consumption_ct_metering_status', + 'unit_of_measurement': None, + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_meter_status_flags_active_net_consumption_ct', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:flash', + 'original_name': 'Meter status flags active net consumption CT', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'net_ct_status_flags', + 'unique_id': '<>_net_consumption_ct_status_flags', + 'unit_of_measurement': None, + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_lifetime_net_energy_consumption_l1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'MWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime net energy consumption l1', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_net_consumption_phase', + 'unique_id': '<>_lifetime_net_consumption_l1', + 'unit_of_measurement': 'MWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_lifetime_net_energy_production_l1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'MWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime net energy production l1', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_net_production_phase', + 'unique_id': '<>_lifetime_net_production_l1', + 'unit_of_measurement': 'MWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_current_net_power_consumption_l1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kW', + }), + }), + 'original_device_class': 'power', + 'original_icon': 'mdi:flash', + 'original_name': 'Current net power consumption l1', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'net_consumption_phase', + 'unique_id': '<>_net_consumption_l1', + 'unit_of_measurement': 'kW', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_frequency_net_consumption_ct_l1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': 'frequency', + 'original_icon': 'mdi:flash', + 'original_name': 'Frequency net consumption CT l1', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'net_ct_frequency_phase', + 'unique_id': '<>_frequency_l1', + 'unit_of_measurement': 'Hz', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_voltage_net_consumption_ct_l1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'V', + }), + }), + 'original_device_class': 'voltage', + 'original_icon': 'mdi:flash', + 'original_name': 'Voltage net consumption CT l1', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'net_ct_voltage_phase', + 'unique_id': '<>_voltage_l1', + 'unit_of_measurement': 'V', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'normal', + 'not-metering', + 'check-wiring', + ]), + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_metering_status_net_consumption_ct_l1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': 'enum', + 'original_icon': 'mdi:flash', + 'original_name': 'Metering status net consumption CT l1', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'net_ct_metering_status_phase', + 'unique_id': '<>_net_consumption_ct_metering_status_l1', + 'unit_of_measurement': None, + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_meter_status_flags_active_net_consumption_ct_l1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:flash', + 'original_name': 'Meter status flags active net consumption CT l1', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'net_ct_status_flags_phase', + 'unique_id': '<>_net_consumption_ct_status_flags_l1', + 'unit_of_measurement': None, + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_lifetime_net_energy_consumption_l2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'MWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime net energy consumption l2', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_net_consumption_phase', + 'unique_id': '<>_lifetime_net_consumption_l2', + 'unit_of_measurement': 'MWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_lifetime_net_energy_production_l2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'MWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime net energy production l2', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_net_production_phase', + 'unique_id': '<>_lifetime_net_production_l2', + 'unit_of_measurement': 'MWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_current_net_power_consumption_l2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kW', + }), + }), + 'original_device_class': 'power', + 'original_icon': 'mdi:flash', + 'original_name': 'Current net power consumption l2', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'net_consumption_phase', + 'unique_id': '<>_net_consumption_l2', + 'unit_of_measurement': 'kW', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_frequency_net_consumption_ct_l2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': 'frequency', + 'original_icon': 'mdi:flash', + 'original_name': 'Frequency net consumption CT l2', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'net_ct_frequency_phase', + 'unique_id': '<>_frequency_l2', + 'unit_of_measurement': 'Hz', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_voltage_net_consumption_ct_l2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'V', + }), + }), + 'original_device_class': 'voltage', + 'original_icon': 'mdi:flash', + 'original_name': 'Voltage net consumption CT l2', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'net_ct_voltage_phase', + 'unique_id': '<>_voltage_l2', + 'unit_of_measurement': 'V', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'normal', + 'not-metering', + 'check-wiring', + ]), + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_metering_status_net_consumption_ct_l2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': 'enum', + 'original_icon': 'mdi:flash', + 'original_name': 'Metering status net consumption CT l2', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'net_ct_metering_status_phase', + 'unique_id': '<>_net_consumption_ct_metering_status_l2', + 'unit_of_measurement': None, + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_meter_status_flags_active_net_consumption_ct_l2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:flash', + 'original_name': 'Meter status flags active net consumption CT l2', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'net_ct_status_flags_phase', + 'unique_id': '<>_net_consumption_ct_status_flags_l2', + 'unit_of_measurement': None, + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_lifetime_net_energy_consumption_l3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'MWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime net energy consumption l3', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_net_consumption_phase', + 'unique_id': '<>_lifetime_net_consumption_l3', + 'unit_of_measurement': 'MWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_lifetime_net_energy_production_l3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'MWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime net energy production l3', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_net_production_phase', + 'unique_id': '<>_lifetime_net_production_l3', + 'unit_of_measurement': 'MWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_current_net_power_consumption_l3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kW', + }), + }), + 'original_device_class': 'power', + 'original_icon': 'mdi:flash', + 'original_name': 'Current net power consumption l3', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'net_consumption_phase', + 'unique_id': '<>_net_consumption_l3', + 'unit_of_measurement': 'kW', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_frequency_net_consumption_ct_l3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': 'frequency', + 'original_icon': 'mdi:flash', + 'original_name': 'Frequency net consumption CT l3', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'net_ct_frequency_phase', + 'unique_id': '<>_frequency_l3', + 'unit_of_measurement': 'Hz', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_voltage_net_consumption_ct_l3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'V', + }), + }), + 'original_device_class': 'voltage', + 'original_icon': 'mdi:flash', + 'original_name': 'Voltage net consumption CT l3', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'net_ct_voltage_phase', + 'unique_id': '<>_voltage_l3', + 'unit_of_measurement': 'V', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'normal', + 'not-metering', + 'check-wiring', + ]), + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_metering_status_net_consumption_ct_l3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': 'enum', + 'original_icon': 'mdi:flash', + 'original_name': 'Metering status net consumption CT l3', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'net_ct_metering_status_phase', + 'unique_id': '<>_net_consumption_ct_metering_status_l3', + 'unit_of_measurement': None, + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_meter_status_flags_active_net_consumption_ct_l3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:flash', + 'original_name': 'Meter status flags active net consumption CT l3', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'net_ct_status_flags_phase', + 'unique_id': '<>_net_consumption_ct_status_flags_l3', + 'unit_of_measurement': None, + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'normal', + 'not-metering', + 'check-wiring', + ]), + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_metering_status_production_ct', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': 'enum', + 'original_icon': 'mdi:flash', + 'original_name': 'Metering status production CT', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'production_ct_metering_status', + 'unique_id': '<>_production_ct_metering_status', + 'unit_of_measurement': None, + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_meter_status_flags_active_production_ct', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:flash', + 'original_name': 'Meter status flags active production CT', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'production_ct_status_flags', + 'unique_id': '<>_production_ct_status_flags', + 'unit_of_measurement': None, + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'normal', + 'not-metering', + 'check-wiring', + ]), + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_metering_status_production_ct_l1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': 'enum', + 'original_icon': 'mdi:flash', + 'original_name': 'Metering status production CT l1', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'production_ct_metering_status_phase', + 'unique_id': '<>_production_ct_metering_status_l1', + 'unit_of_measurement': None, + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_meter_status_flags_active_production_ct_l1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:flash', + 'original_name': 'Meter status flags active production CT l1', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'production_ct_status_flags_phase', + 'unique_id': '<>_production_ct_status_flags_l1', + 'unit_of_measurement': None, + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'normal', + 'not-metering', + 'check-wiring', + ]), + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_metering_status_production_ct_l2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': 'enum', + 'original_icon': 'mdi:flash', + 'original_name': 'Metering status production CT l2', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'production_ct_metering_status_phase', + 'unique_id': '<>_production_ct_metering_status_l2', + 'unit_of_measurement': None, + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_meter_status_flags_active_production_ct_l2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:flash', + 'original_name': 'Meter status flags active production CT l2', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'production_ct_status_flags_phase', + 'unique_id': '<>_production_ct_status_flags_l2', + 'unit_of_measurement': None, + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'normal', + 'not-metering', + 'check-wiring', + ]), + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_metering_status_production_ct_l3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': 'enum', + 'original_icon': 'mdi:flash', + 'original_name': 'Metering status production CT l3', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'production_ct_metering_status_phase', + 'unique_id': '<>_production_ct_metering_status_l3', + 'unit_of_measurement': None, + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_meter_status_flags_active_production_ct_l3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:flash', + 'original_name': 'Meter status flags active production CT l3', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'production_ct_status_flags_phase', + 'unique_id': '<>_production_ct_status_flags_l3', + 'unit_of_measurement': None, + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_lifetime_battery_energy_discharged', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 3, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'MWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime battery energy discharged', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_battery_discharged', + 'unique_id': '<>_lifetime_battery_discharged', + 'unit_of_measurement': 'MWh', + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'energy', + 'friendly_name': 'Envoy <> Lifetime battery energy discharged', + 'icon': 'mdi:flash', + 'state_class': 'total_increasing', + 'unit_of_measurement': 'MWh', + }), + 'entity_id': 'sensor.envoy_<>_lifetime_battery_energy_discharged', + 'state': '0.03<>', + }), + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_lifetime_battery_energy_charged', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 3, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'MWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime battery energy charged', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_battery_charged', + 'unique_id': '<>_lifetime_battery_charged', + 'unit_of_measurement': 'MWh', + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'energy', + 'friendly_name': 'Envoy <> Lifetime battery energy charged', + 'icon': 'mdi:flash', + 'state_class': 'total_increasing', + 'unit_of_measurement': 'MWh', + }), + 'entity_id': 'sensor.envoy_<>_lifetime_battery_energy_charged', + 'state': '0.032345', + }), + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_current_battery_discharge', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 3, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kW', + }), + }), + 'original_device_class': 'power', + 'original_icon': 'mdi:flash', + 'original_name': 'Current battery discharge', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'battery_discharge', + 'unique_id': '<>_battery_discharge', + 'unit_of_measurement': 'kW', + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'power', + 'friendly_name': 'Envoy <> Current battery discharge', + 'icon': 'mdi:flash', + 'state_class': 'measurement', + 'unit_of_measurement': 'kW', + }), + 'entity_id': 'sensor.envoy_<>_current_battery_discharge', + 'state': '0.103', + }), + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_voltage_storage_ct', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'V', + }), + }), + 'original_device_class': 'voltage', + 'original_icon': 'mdi:flash', + 'original_name': 'Voltage storage CT', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'storage_ct_voltage', + 'unique_id': '<>_storage_voltage', + 'unit_of_measurement': 'V', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'normal', + 'not-metering', + 'check-wiring', + ]), + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_metering_status_storage_ct', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': 'enum', + 'original_icon': 'mdi:flash', + 'original_name': 'Metering status storage CT', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'storage_ct_metering_status', + 'unique_id': '<>_storage_ct_metering_status', + 'unit_of_measurement': None, + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_meter_status_flags_active_storage_ct', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:flash', + 'original_name': 'Meter status flags active storage CT', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'storage_ct_status_flags', + 'unique_id': '<>_storage_ct_status_flags', + 'unit_of_measurement': None, + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_lifetime_battery_energy_discharged_l1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'MWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime battery energy discharged l1', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_battery_discharged_phase', + 'unique_id': '<>_lifetime_battery_discharged_l1', + 'unit_of_measurement': 'MWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_lifetime_battery_energy_charged_l1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'MWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime battery energy charged l1', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_battery_charged_phase', + 'unique_id': '<>_lifetime_battery_charged_l1', + 'unit_of_measurement': 'MWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_current_battery_discharge_l1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kW', + }), + }), + 'original_device_class': 'power', + 'original_icon': 'mdi:flash', + 'original_name': 'Current battery discharge l1', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'battery_discharge_phase', + 'unique_id': '<>_battery_discharge_l1', + 'unit_of_measurement': 'kW', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_voltage_storage_ct_l1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'V', + }), + }), + 'original_device_class': 'voltage', + 'original_icon': 'mdi:flash', + 'original_name': 'Voltage storage CT l1', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'storage_ct_voltage_phase', + 'unique_id': '<>_storage_voltage_l1', + 'unit_of_measurement': 'V', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'normal', + 'not-metering', + 'check-wiring', + ]), + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_metering_status_storage_ct_l1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': 'enum', + 'original_icon': 'mdi:flash', + 'original_name': 'Metering status storage CT l1', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'storage_ct_metering_status_phase', + 'unique_id': '<>_storage_ct_metering_status_l1', + 'unit_of_measurement': None, + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_meter_status_flags_active_storage_ct_l1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:flash', + 'original_name': 'Meter status flags active storage CT l1', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'storage_ct_status_flags_phase', + 'unique_id': '<>_storage_ct_status_flags_l1', + 'unit_of_measurement': None, + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_lifetime_battery_energy_discharged_l2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'MWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime battery energy discharged l2', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_battery_discharged_phase', + 'unique_id': '<>_lifetime_battery_discharged_l2', + 'unit_of_measurement': 'MWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_lifetime_battery_energy_charged_l2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'MWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime battery energy charged l2', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_battery_charged_phase', + 'unique_id': '<>_lifetime_battery_charged_l2', + 'unit_of_measurement': 'MWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_current_battery_discharge_l2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kW', + }), + }), + 'original_device_class': 'power', + 'original_icon': 'mdi:flash', + 'original_name': 'Current battery discharge l2', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'battery_discharge_phase', + 'unique_id': '<>_battery_discharge_l2', + 'unit_of_measurement': 'kW', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_voltage_storage_ct_l2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'V', + }), + }), + 'original_device_class': 'voltage', + 'original_icon': 'mdi:flash', + 'original_name': 'Voltage storage CT l2', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'storage_ct_voltage_phase', + 'unique_id': '<>_storage_voltage_l2', + 'unit_of_measurement': 'V', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'normal', + 'not-metering', + 'check-wiring', + ]), + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_metering_status_storage_ct_l2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': 'enum', + 'original_icon': 'mdi:flash', + 'original_name': 'Metering status storage CT l2', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'storage_ct_metering_status_phase', + 'unique_id': '<>_storage_ct_metering_status_l2', + 'unit_of_measurement': None, + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_meter_status_flags_active_storage_ct_l2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:flash', + 'original_name': 'Meter status flags active storage CT l2', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'storage_ct_status_flags_phase', + 'unique_id': '<>_storage_ct_status_flags_l2', + 'unit_of_measurement': None, + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_lifetime_battery_energy_discharged_l3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'MWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime battery energy discharged l3', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_battery_discharged_phase', + 'unique_id': '<>_lifetime_battery_discharged_l3', + 'unit_of_measurement': 'MWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_lifetime_battery_energy_charged_l3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'MWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime battery energy charged l3', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_battery_charged_phase', + 'unique_id': '<>_lifetime_battery_charged_l3', + 'unit_of_measurement': 'MWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_current_battery_discharge_l3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kW', + }), + }), + 'original_device_class': 'power', + 'original_icon': 'mdi:flash', + 'original_name': 'Current battery discharge l3', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'battery_discharge_phase', + 'unique_id': '<>_battery_discharge_l3', + 'unit_of_measurement': 'kW', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_voltage_storage_ct_l3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'V', + }), + }), + 'original_device_class': 'voltage', + 'original_icon': 'mdi:flash', + 'original_name': 'Voltage storage CT l3', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'storage_ct_voltage_phase', + 'unique_id': '<>_storage_voltage_l3', + 'unit_of_measurement': 'V', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'normal', + 'not-metering', + 'check-wiring', + ]), + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_metering_status_storage_ct_l3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': 'enum', + 'original_icon': 'mdi:flash', + 'original_name': 'Metering status storage CT l3', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'storage_ct_metering_status_phase', + 'unique_id': '<>_storage_ct_metering_status_l3', + 'unit_of_measurement': None, + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_meter_status_flags_active_storage_ct_l3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:flash', + 'original_name': 'Meter status flags active storage CT l3', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'storage_ct_status_flags_phase', + 'unique_id': '<>_storage_ct_status_flags_l3', + 'unit_of_measurement': None, + }), + 'state': None, + }), + ]), + }), + dict({ + 'device': dict({ + 'area_id': None, + 'config_entries': list([ + '45a36e55aaddb2007c5f6602e0c38e72', + ]), + 'configuration_url': None, + 'connections': list([ + ]), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'identifiers': list([ + list([ + 'enphase_envoy', + '1', + ]), + ]), + 'is_new': False, + 'labels': list([ + ]), + 'manufacturer': 'Enphase', + 'model': 'Inverter', + 'name': 'Inverter 1', + 'name_by_user': None, + 'serial_number': None, + 'suggested_area': None, + 'sw_version': None, + }), + 'entities': list([ + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.inverter_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': 'power', + 'original_icon': 'mdi:flash', + 'original_name': None, + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '1', + 'unit_of_measurement': 'W', + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'power', + 'friendly_name': 'Inverter 1', + 'icon': 'mdi:flash', + 'state_class': 'measurement', + 'unit_of_measurement': 'W', + }), + 'entity_id': 'sensor.inverter_1', + 'state': '1', + }), + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.inverter_1_last_reported', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': 'timestamp', + 'original_icon': 'mdi:flash', + 'original_name': 'Last reported', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'last_reported', + 'unique_id': '1_last_reported', + 'unit_of_measurement': None, + }), + 'state': None, + }), + ]), + }), + ]), + 'envoy_model_data': dict({ + 'ctmeter_consumption': dict({ + '__type': "", + 'repr': "EnvoyMeterData(eid='100000020', timestamp=1708006120, energy_delivered=21234, energy_received=22345, active_power=101, power_factor=0.21, voltage=112, current=0.3, frequency=50.2, state=, measurement_type=, metering_status=, status_flags=[])", + }), + 'ctmeter_consumption_phases': dict({ + 'L1': dict({ + '__type': "", + 'repr': "EnvoyMeterData(eid='100000021', timestamp=1708006121, energy_delivered=212341, energy_received=223451, active_power=21, power_factor=0.22, voltage=112, current=0.3, frequency=50.2, state=, measurement_type=, metering_status=, status_flags=[])", + }), + 'L2': dict({ + '__type': "", + 'repr': "EnvoyMeterData(eid='100000022', timestamp=1708006122, energy_delivered=212342, energy_received=223452, active_power=31, power_factor=0.23, voltage=112, current=0.3, frequency=50.2, state=, measurement_type=, metering_status=, status_flags=[])", + }), + 'L3': dict({ + '__type': "", + 'repr': "EnvoyMeterData(eid='100000023', timestamp=1708006123, energy_delivered=212343, energy_received=223453, active_power=51, power_factor=0.24, voltage=112, current=0.3, frequency=50.2, state=, measurement_type=, metering_status=, status_flags=[])", + }), + }), + 'ctmeter_production': dict({ + '__type': "", + 'repr': "EnvoyMeterData(eid='100000010', timestamp=1708006110, energy_delivered=11234, energy_received=12345, active_power=100, power_factor=0.11, voltage=111, current=0.2, frequency=50.1, state=, measurement_type=, metering_status=, status_flags=[, ])", + }), + 'ctmeter_production_phases': dict({ + 'L1': dict({ + '__type': "", + 'repr': "EnvoyMeterData(eid='100000011', timestamp=1708006111, energy_delivered=112341, energy_received=123451, active_power=20, power_factor=0.12, voltage=111, current=0.2, frequency=50.1, state=, measurement_type=, metering_status=, status_flags=[])", + }), + 'L2': dict({ + '__type': "", + 'repr': "EnvoyMeterData(eid='100000012', timestamp=1708006112, energy_delivered=112342, energy_received=123452, active_power=30, power_factor=0.13, voltage=111, current=0.2, frequency=50.1, state=, measurement_type=, metering_status=, status_flags=[])", + }), + 'L3': dict({ + '__type': "", + 'repr': "EnvoyMeterData(eid='100000013', timestamp=1708006113, energy_delivered=112343, energy_received=123453, active_power=50, power_factor=0.14, voltage=111, current=0.2, frequency=50.1, state=, measurement_type=, metering_status=, status_flags=[])", + }), + }), + 'ctmeter_storage': dict({ + '__type': "", + 'repr': "EnvoyMeterData(eid='100000030', timestamp=1708006120, energy_delivered=31234, energy_received=32345, active_power=103, power_factor=0.23, voltage=113, current=0.4, frequency=50.3, state=, measurement_type=, metering_status=, status_flags=[])", + }), + 'ctmeter_storage_phases': dict({ + 'L1': dict({ + '__type': "", + 'repr': "EnvoyMeterData(eid='100000031', timestamp=1708006121, energy_delivered=312341, energy_received=323451, active_power=22, power_factor=0.32, voltage=113, current=0.4, frequency=50.3, state=, measurement_type=, metering_status=, status_flags=[])", + }), + 'L2': dict({ + '__type': "", + 'repr': "EnvoyMeterData(eid='100000032', timestamp=1708006122, energy_delivered=312342, energy_received=323452, active_power=33, power_factor=0.23, voltage=112, current=0.3, frequency=50.2, state=, measurement_type=, metering_status=, status_flags=[])", + }), + 'L3': dict({ + '__type': "", + 'repr': "EnvoyMeterData(eid='100000033', timestamp=1708006123, energy_delivered=312343, energy_received=323453, active_power=53, power_factor=0.24, voltage=112, current=0.3, frequency=50.2, state=, measurement_type=, metering_status=, status_flags=[])", + }), + }), + 'dry_contact_settings': dict({ + }), + 'dry_contact_status': dict({ + }), + 'encharge_aggregate': None, + 'encharge_inventory': None, + 'encharge_power': None, + 'enpower': None, + 'inverters': dict({ + '1': dict({ + '__type': "", + 'repr': "EnvoyInverter(serial_number='1', last_report_date=1, last_report_watts=1, max_report_watts=1)", + }), + }), + 'system_consumption': dict({ + '__type': "", + 'repr': 'EnvoySystemConsumption(watt_hours_lifetime=1234, watt_hours_last_7_days=1234, watt_hours_today=1234, watts_now=1234)', + }), + 'system_consumption_phases': dict({ + 'L1': dict({ + '__type': "", + 'repr': 'EnvoySystemConsumption(watt_hours_lifetime=1322, watt_hours_last_7_days=1321, watt_hours_today=1323, watts_now=1324)', + }), + 'L2': dict({ + '__type': "", + 'repr': 'EnvoySystemConsumption(watt_hours_lifetime=2322, watt_hours_last_7_days=2321, watt_hours_today=2323, watts_now=2324)', + }), + 'L3': dict({ + '__type': "", + 'repr': 'EnvoySystemConsumption(watt_hours_lifetime=3322, watt_hours_last_7_days=3321, watt_hours_today=3323, watts_now=3324)', + }), + }), + 'system_production': dict({ + '__type': "", + 'repr': 'EnvoySystemProduction(watt_hours_lifetime=1234, watt_hours_last_7_days=1234, watt_hours_today=1234, watts_now=1234)', + }), + 'system_production_phases': dict({ + 'L1': dict({ + '__type': "", + 'repr': 'EnvoySystemProduction(watt_hours_lifetime=1232, watt_hours_last_7_days=1231, watt_hours_today=1233, watts_now=1234)', + }), + 'L2': dict({ + '__type': "", + 'repr': 'EnvoySystemProduction(watt_hours_lifetime=2232, watt_hours_last_7_days=2231, watt_hours_today=2233, watts_now=2234)', + }), + 'L3': dict({ + '__type': "", + 'repr': 'EnvoySystemProduction(watt_hours_lifetime=3232, watt_hours_last_7_days=3231, watt_hours_today=3233, watts_now=3234)', + }), + }), + 'tariff': None, + }), + 'envoy_properties': dict({ + 'active_phasecount': 3, + 'ct_consumption_meter': 'net-consumption', + 'ct_count': 3, + 'ct_production_meter': 'production', + 'ct_storage_meter': 'storage', + 'envoy_firmware': '7.1.2', + 'envoy_model': 'Envoy, phases: 3, phase mode: three, net-consumption CT, production CT, storage CT', + 'part_number': '123456789', + 'phase_count': 3, + 'phase_mode': 'three', + 'supported_features': list([ + 'INVERTERS', + 'METERING', + 'PRODUCTION', + 'THREEPHASE', + 'CTMETERS', + ]), + }), + 'fixtures': dict({ + 'Error': "EnvoyError('Test')", + }), 'raw_data': dict({ 'varies_by': 'firmware_version', }), diff --git a/tests/components/enphase_envoy/test_config_flow.py b/tests/components/enphase_envoy/test_config_flow.py index 7e1808ffa52..b60b03e5df9 100644 --- a/tests/components/enphase_envoy/test_config_flow.py +++ b/tests/components/enphase_envoy/test_config_flow.py @@ -10,7 +10,12 @@ from syrupy.assertion import SnapshotAssertion from homeassistant import config_entries from homeassistant.components import zeroconf -from homeassistant.components.enphase_envoy.const import DOMAIN, PLATFORMS +from homeassistant.components.enphase_envoy.const import ( + DOMAIN, + OPTION_DIAGNOSTICS_INCLUDE_FIXTURES, + OPTION_DIAGNOSTICS_INCLUDE_FIXTURES_DEFAULT_VALUE, + PLATFORMS, +) from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -656,6 +661,41 @@ async def test_reauth(hass: HomeAssistant, config_entry, setup_enphase_envoy) -> assert result2["reason"] == "reauth_successful" +async def test_options_default( + hass: HomeAssistant, config_entry, setup_enphase_envoy +) -> None: + """Test we can configure options.""" + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "init" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + OPTION_DIAGNOSTICS_INCLUDE_FIXTURES: OPTION_DIAGNOSTICS_INCLUDE_FIXTURES_DEFAULT_VALUE + }, + ) + assert result["type"] is FlowResultType.CREATE_ENTRY + assert config_entry.options == { + OPTION_DIAGNOSTICS_INCLUDE_FIXTURES: OPTION_DIAGNOSTICS_INCLUDE_FIXTURES_DEFAULT_VALUE + } + + +async def test_options_set( + hass: HomeAssistant, config_entry, setup_enphase_envoy +) -> None: + """Test we can configure options.""" + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "init" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], user_input={OPTION_DIAGNOSTICS_INCLUDE_FIXTURES: True} + ) + assert result["type"] is FlowResultType.CREATE_ENTRY + assert config_entry.options == {OPTION_DIAGNOSTICS_INCLUDE_FIXTURES: True} + + async def test_reconfigure( hass: HomeAssistant, config_entry, setup_enphase_envoy ) -> None: diff --git a/tests/components/enphase_envoy/test_diagnostics.py b/tests/components/enphase_envoy/test_diagnostics.py index a3b4f8e0f3c..9ee6b7905e7 100644 --- a/tests/components/enphase_envoy/test_diagnostics.py +++ b/tests/components/enphase_envoy/test_diagnostics.py @@ -1,10 +1,20 @@ """Test Enphase Envoy diagnostics.""" -from syrupy import SnapshotAssertion +from unittest.mock import AsyncMock, patch +from pyenphase.exceptions import EnvoyError +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.enphase_envoy.const import ( + DOMAIN, + OPTION_DIAGNOSTICS_INCLUDE_FIXTURES, +) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component +from tests.common import MockConfigEntry from tests.components.diagnostics import get_diagnostics_for_config_entry from tests.typing import ClientSessionGenerator @@ -35,3 +45,76 @@ async def test_entry_diagnostics( assert await get_diagnostics_for_config_entry( hass, hass_client, config_entry ) == snapshot(exclude=limit_diagnostic_attrs) + + +@pytest.fixture(name="config_entry_options") +def config_entry_options_fixture(hass: HomeAssistant, config, serial_number): + """Define a config entry fixture.""" + entry = MockConfigEntry( + domain=DOMAIN, + entry_id="45a36e55aaddb2007c5f6602e0c38e72", + title=f"Envoy {serial_number}" if serial_number else "Envoy", + unique_id=serial_number, + data=config, + options={OPTION_DIAGNOSTICS_INCLUDE_FIXTURES: True}, + ) + entry.add_to_hass(hass) + return entry + + +async def test_entry_diagnostics_with_fixtures( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + config_entry_options: ConfigEntry, + setup_enphase_envoy, + snapshot: SnapshotAssertion, +) -> None: + """Test config entry diagnostics.""" + assert await get_diagnostics_for_config_entry( + hass, hass_client, config_entry_options + ) == snapshot(exclude=limit_diagnostic_attrs) + + +@pytest.fixture(name="setup_enphase_envoy_options_error") +async def setup_enphase_envoy_options_error_fixture( + hass: HomeAssistant, + config, + mock_envoy_options_error, +): + """Define a fixture to set up Enphase Envoy.""" + with ( + patch( + "homeassistant.components.enphase_envoy.config_flow.Envoy", + return_value=mock_envoy_options_error, + ), + patch( + "homeassistant.components.enphase_envoy.Envoy", + return_value=mock_envoy_options_error, + ), + ): + assert await async_setup_component(hass, DOMAIN, config) + await hass.async_block_till_done() + yield + + +@pytest.fixture(name="mock_envoy_options_error") +def mock_envoy_options_fixture( + mock_envoy, +): + """Mock envoy with error in request.""" + mock_envoy_options = mock_envoy + mock_envoy_options.request.side_effect = AsyncMock(side_effect=EnvoyError("Test")) + return mock_envoy_options + + +async def test_entry_diagnostics_with_fixtures_with_error( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + config_entry_options: ConfigEntry, + setup_enphase_envoy_options_error, + snapshot: SnapshotAssertion, +) -> None: + """Test config entry diagnostics.""" + assert await get_diagnostics_for_config_entry( + hass, hass_client, config_entry_options + ) == snapshot(exclude=limit_diagnostic_attrs) From be6dfc7a709fb04143b4a4413b0e39464ca8f913 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 24 Jun 2024 11:07:22 +0200 Subject: [PATCH 1082/1445] Typing improvements (#120297) --- homeassistant/components/fritz/switch.py | 4 ++-- homeassistant/components/geniushub/sensor.py | 2 +- homeassistant/components/heos/media_player.py | 2 +- homeassistant/components/keenetic_ndms2/device_tracker.py | 2 +- homeassistant/components/knx/config_flow.py | 1 + homeassistant/components/lovelace/dashboard.py | 2 +- homeassistant/components/modbus/sensor.py | 6 +++--- homeassistant/components/zwave_me/light.py | 5 ++--- 8 files changed, 12 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/fritz/switch.py b/homeassistant/components/fritz/switch.py index 8af5b8ba529..ce89cfc736d 100644 --- a/homeassistant/components/fritz/switch.py +++ b/homeassistant/components/fritz/switch.py @@ -331,7 +331,7 @@ class FritzBoxBaseSwitch(FritzBoxBaseEntity, SwitchEntity): self._name = f"{self._friendly_name} {self._description}" self._unique_id = f"{self._avm_wrapper.unique_id}-{slugify(self._description)}" - self._attributes: dict[str, str] = {} + self._attributes: dict[str, str | None] = {} self._is_available = True @property @@ -355,7 +355,7 @@ class FritzBoxBaseSwitch(FritzBoxBaseEntity, SwitchEntity): return self._is_available @property - def extra_state_attributes(self) -> dict[str, str]: + def extra_state_attributes(self) -> dict[str, str | None]: """Return device attributes.""" return self._attributes diff --git a/homeassistant/components/geniushub/sensor.py b/homeassistant/components/geniushub/sensor.py index 998bd6f1edb..f5cd8625e8b 100644 --- a/homeassistant/components/geniushub/sensor.py +++ b/homeassistant/components/geniushub/sensor.py @@ -85,7 +85,7 @@ class GeniusBattery(GeniusDevice, SensorEntity): return icon @property - def native_value(self) -> str: + def native_value(self) -> int: """Return the state of the sensor.""" level = self._device.data["state"][self._state_attr] return level if level != 255 else 0 diff --git a/homeassistant/components/heos/media_player.py b/homeassistant/components/heos/media_player.py index 820bcb2fb2b..858ebd225b7 100644 --- a/homeassistant/components/heos/media_player.py +++ b/homeassistant/components/heos/media_player.py @@ -377,7 +377,7 @@ class HeosMediaPlayer(MediaPlayerEntity): return self._media_position_updated_at @property - def media_image_url(self) -> str: + def media_image_url(self) -> str | None: """Image url of current playing media.""" # May be an empty string, if so, return None image_url = self._player.now_playing_media.image_url diff --git a/homeassistant/components/keenetic_ndms2/device_tracker.py b/homeassistant/components/keenetic_ndms2/device_tracker.py index e15c96d8353..34c5cb502c6 100644 --- a/homeassistant/components/keenetic_ndms2/device_tracker.py +++ b/homeassistant/components/keenetic_ndms2/device_tracker.py @@ -119,7 +119,7 @@ class KeeneticTracker(ScannerEntity): return f"{self._device.mac}_{self._router.config_entry.entry_id}" @property - def ip_address(self) -> str: + def ip_address(self) -> str | None: """Return the primary ip address of the device.""" return self._device.ip if self.is_connected else None diff --git a/homeassistant/components/knx/config_flow.py b/homeassistant/components/knx/config_flow.py index 22c4a647e80..c526a1e25f6 100644 --- a/homeassistant/components/knx/config_flow.py +++ b/homeassistant/components/knx/config_flow.py @@ -332,6 +332,7 @@ class KNXCommonFlow(ABC, ConfigEntryBaseFlow): self.initial_data.get(CONF_KNX_CONNECTION_TYPE) in CONF_KNX_TUNNELING_TYPE_LABELS ) + ip_address: str | None if ( # initial attempt on ConfigFlow or coming from automatic / routing (isinstance(self, ConfigFlow) or not _reconfiguring_existing_tunnel) and not user_input diff --git a/homeassistant/components/lovelace/dashboard.py b/homeassistant/components/lovelace/dashboard.py index db6db2fa7ef..411bbae9153 100644 --- a/homeassistant/components/lovelace/dashboard.py +++ b/homeassistant/components/lovelace/dashboard.py @@ -56,7 +56,7 @@ class LovelaceConfig(ABC): self.config = None @property - def url_path(self) -> str: + def url_path(self) -> str | None: """Return url path.""" return self.config[CONF_URL_PATH] if self.config else None diff --git a/homeassistant/components/modbus/sensor.py b/homeassistant/components/modbus/sensor.py index 6c6e1ef1830..dbc464e98a9 100644 --- a/homeassistant/components/modbus/sensor.py +++ b/homeassistant/components/modbus/sensor.py @@ -74,7 +74,7 @@ class ModbusRegisterSensor(BaseStructPlatform, RestoreSensor, SensorEntity): super().__init__(hass, hub, entry) if slave_count: self._count = self._count * (slave_count + 1) - self._coordinator: DataUpdateCoordinator[list[int] | None] | None = None + self._coordinator: DataUpdateCoordinator[list[float] | None] | None = None self._attr_native_unit_of_measurement = entry.get(CONF_UNIT_OF_MEASUREMENT) self._attr_state_class = entry.get(CONF_STATE_CLASS) self._attr_device_class = entry.get(CONF_DEVICE_CLASS) @@ -142,7 +142,7 @@ class ModbusRegisterSensor(BaseStructPlatform, RestoreSensor, SensorEntity): class SlaveSensor( - CoordinatorEntity[DataUpdateCoordinator[list[int] | None]], + CoordinatorEntity[DataUpdateCoordinator[list[float] | None]], RestoreSensor, SensorEntity, ): @@ -150,7 +150,7 @@ class SlaveSensor( def __init__( self, - coordinator: DataUpdateCoordinator[list[int] | None], + coordinator: DataUpdateCoordinator[list[float] | None], idx: int, entry: dict[str, Any], ) -> None: diff --git a/homeassistant/components/zwave_me/light.py b/homeassistant/components/zwave_me/light.py index b1065d45160..2289fe7b115 100644 --- a/homeassistant/components/zwave_me/light.py +++ b/homeassistant/components/zwave_me/light.py @@ -84,9 +84,8 @@ class ZWaveMeRGB(ZWaveMeEntity, LightEntity): self.device.id, f"exact?level={round(brightness / 2.55)}" ) return - cmd = "exact?red={}&green={}&blue={}".format( - *color if any(color) else 255, 255, 255 - ) + cmd = "exact?red={}&green={}&blue={}" + cmd = cmd.format(*color) if any(color) else cmd.format(*(255, 255, 255)) self.controller.zwave_api.send_command(self.device.id, cmd) @property From e32a27a8ff2864f43a9686dbd68e6813ee95cf31 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 24 Jun 2024 11:14:08 +0200 Subject: [PATCH 1083/1445] Remove hass_recorder test fixture (#120295) --- pylint/plugins/hass_enforce_type_hints.py | 1 - tests/common.py | 26 ----- tests/conftest.py | 116 ---------------------- 3 files changed, 143 deletions(-) diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index 6dd19d96d01..67eea59bc9a 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -125,7 +125,6 @@ _TEST_FIXTURES: dict[str, list[str] | str] = { "hass_owner_user": "MockUser", "hass_read_only_access_token": "str", "hass_read_only_user": "MockUser", - "hass_recorder": "Callable[..., HomeAssistant]", "hass_storage": "dict[str, Any]", "hass_supervisor_access_token": "str", "hass_supervisor_user": "MockUser", diff --git a/tests/common.py b/tests/common.py index 30c7cc2d971..f5531dbf40d 100644 --- a/tests/common.py +++ b/tests/common.py @@ -70,7 +70,6 @@ from homeassistant.helpers import ( intent, issue_registry as ir, label_registry as lr, - recorder as recorder_helper, restore_state as rs, storage, translation, @@ -83,7 +82,6 @@ from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.json import JSONEncoder, _orjson_default_encoder, json_dumps from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from homeassistant.setup import setup_component from homeassistant.util.async_ import run_callback_threadsafe import homeassistant.util.dt as dt_util from homeassistant.util.json import ( @@ -1162,30 +1160,6 @@ def assert_setup_component(count, domain=None): ), f"setup_component failed, expected {count} got {res_len}: {res}" -def init_recorder_component(hass, add_config=None, db_url="sqlite://"): - """Initialize the recorder.""" - # Local import to avoid processing recorder and SQLite modules when running a - # testcase which does not use the recorder. - # pylint: disable-next=import-outside-toplevel - from homeassistant.components import recorder - - config = dict(add_config) if add_config else {} - if recorder.CONF_DB_URL not in config: - config[recorder.CONF_DB_URL] = db_url - if recorder.CONF_COMMIT_INTERVAL not in config: - config[recorder.CONF_COMMIT_INTERVAL] = 0 - - with patch("homeassistant.components.recorder.ALLOW_IN_MEMORY_DB", True): - if recorder.DOMAIN not in hass.data: - recorder_helper.async_initialize_recorder(hass) - assert setup_component(hass, recorder.DOMAIN, {recorder.DOMAIN: config}) - assert recorder.DOMAIN in hass.config.components - _LOGGER.info( - "Test recorder successfully started, database location: %s", - config[recorder.CONF_DB_URL], - ) - - def mock_restore_cache(hass: HomeAssistant, states: Sequence[State]) -> None: """Mock the DATA_RESTORE_CACHE.""" key = rs.DATA_RESTORE_STATE diff --git a/tests/conftest.py b/tests/conftest.py index 6aa370ae539..1d4699647c9 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -106,8 +106,6 @@ from .common import ( # noqa: E402, isort:skip MockUser, async_fire_mqtt_message, async_test_home_assistant, - get_test_home_assistant, - init_recorder_component, mock_storage, patch_yaml_files, extract_stack_to_frame, @@ -1350,120 +1348,6 @@ def recorder_db_url( sqlalchemy_utils.drop_database(db_url) -@pytest.fixture -def hass_recorder( - recorder_db_url: str, - enable_nightly_purge: bool, - enable_statistics: bool, - enable_schema_validation: bool, - enable_migrate_context_ids: bool, - enable_migrate_event_type_ids: bool, - enable_migrate_entity_ids: bool, - hass_storage: dict[str, Any], -) -> Generator[Callable[..., HomeAssistant]]: - """Home Assistant fixture with in-memory recorder.""" - # pylint: disable-next=import-outside-toplevel - from homeassistant.components import recorder - - # pylint: disable-next=import-outside-toplevel - from homeassistant.components.recorder import migration - - with get_test_home_assistant() as hass: - nightly = ( - recorder.Recorder.async_nightly_tasks if enable_nightly_purge else None - ) - stats = ( - recorder.Recorder.async_periodic_statistics if enable_statistics else None - ) - compile_missing = ( - recorder.Recorder._schedule_compile_missing_statistics - if enable_statistics - else None - ) - schema_validate = ( - migration._find_schema_errors - if enable_schema_validation - else itertools.repeat(set()) - ) - migrate_states_context_ids = ( - recorder.Recorder._migrate_states_context_ids - if enable_migrate_context_ids - else None - ) - migrate_events_context_ids = ( - recorder.Recorder._migrate_events_context_ids - if enable_migrate_context_ids - else None - ) - migrate_event_type_ids = ( - recorder.Recorder._migrate_event_type_ids - if enable_migrate_event_type_ids - else None - ) - migrate_entity_ids = ( - recorder.Recorder._migrate_entity_ids if enable_migrate_entity_ids else None - ) - with ( - patch( - "homeassistant.components.recorder.Recorder.async_nightly_tasks", - side_effect=nightly, - autospec=True, - ), - patch( - "homeassistant.components.recorder.Recorder.async_periodic_statistics", - side_effect=stats, - autospec=True, - ), - patch( - "homeassistant.components.recorder.migration._find_schema_errors", - side_effect=schema_validate, - autospec=True, - ), - patch( - "homeassistant.components.recorder.Recorder._migrate_events_context_ids", - side_effect=migrate_events_context_ids, - autospec=True, - ), - patch( - "homeassistant.components.recorder.Recorder._migrate_states_context_ids", - side_effect=migrate_states_context_ids, - autospec=True, - ), - patch( - "homeassistant.components.recorder.Recorder._migrate_event_type_ids", - side_effect=migrate_event_type_ids, - autospec=True, - ), - patch( - "homeassistant.components.recorder.Recorder._migrate_entity_ids", - side_effect=migrate_entity_ids, - autospec=True, - ), - patch( - "homeassistant.components.recorder.Recorder._schedule_compile_missing_statistics", - side_effect=compile_missing, - autospec=True, - ), - ): - - def setup_recorder( - *, config: dict[str, Any] | None = None, timezone: str | None = None - ) -> HomeAssistant: - """Set up with params.""" - if timezone is not None: - asyncio.run_coroutine_threadsafe( - hass.config.async_set_time_zone(timezone), hass.loop - ).result() - init_recorder_component(hass, config, recorder_db_url) - hass.start() - hass.block_till_done() - hass.data[recorder.DATA_INSTANCE].block_till_done() - return hass - - yield setup_recorder - hass.stop() - - async def _async_init_recorder_component( hass: HomeAssistant, add_config: dict[str, Any] | None = None, From 59dd63ea86a3bdb2173ffe8130dc8e7d03f4539a Mon Sep 17 00:00:00 2001 From: Cyrill Raccaud Date: Mon, 24 Jun 2024 11:18:10 +0200 Subject: [PATCH 1084/1445] Remove deprecated attributes from Swiss public transport integration (#120256) --- .../swiss_public_transport/coordinator.py | 4 --- .../swiss_public_transport/sensor.py | 28 ------------------- 2 files changed, 32 deletions(-) diff --git a/homeassistant/components/swiss_public_transport/coordinator.py b/homeassistant/components/swiss_public_transport/coordinator.py index eb6ab9c6017..ae7e1b2366d 100644 --- a/homeassistant/components/swiss_public_transport/coordinator.py +++ b/homeassistant/components/swiss_public_transport/coordinator.py @@ -23,8 +23,6 @@ class DataConnection(TypedDict): """A connection data class.""" departure: datetime | None - next_departure: datetime | None - next_on_departure: datetime | None duration: int | None platform: str remaining_time: str @@ -88,8 +86,6 @@ class SwissPublicTransportDataUpdateCoordinator( return [ DataConnection( departure=self.nth_departure_time(i), - next_departure=self.nth_departure_time(i + 1), - next_on_departure=self.nth_departure_time(i + 2), train_number=connections[i]["number"], platform=connections[i]["platform"], transfers=connections[i]["transfers"], diff --git a/homeassistant/components/swiss_public_transport/sensor.py b/homeassistant/components/swiss_public_transport/sensor.py index 844797e5dd5..88a6dbecae4 100644 --- a/homeassistant/components/swiss_public_transport/sensor.py +++ b/homeassistant/components/swiss_public_transport/sensor.py @@ -15,7 +15,6 @@ from homeassistant.components.sensor import ( SensorEntityDescription, ) from homeassistant.const import UnitOfTime -from homeassistant.core import callback from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType @@ -36,7 +35,6 @@ class SwissPublicTransportSensorEntityDescription(SensorEntityDescription): value_fn: Callable[[DataConnection], StateType | datetime] index: int = 0 - has_legacy_attributes: bool = False SENSORS: tuple[SwissPublicTransportSensorEntityDescription, ...] = ( @@ -45,7 +43,6 @@ SENSORS: tuple[SwissPublicTransportSensorEntityDescription, ...] = ( key=f"departure{i or ''}", translation_key=f"departure{i}", device_class=SensorDeviceClass.TIMESTAMP, - has_legacy_attributes=i == 0, value_fn=lambda data_connection: data_connection["departure"], index=i, ) @@ -127,28 +124,3 @@ class SwissPublicTransportSensor( return self.entity_description.value_fn( self.coordinator.data[self.entity_description.index] ) - - async def async_added_to_hass(self) -> None: - """Prepare the extra attributes at start.""" - if self.entity_description.has_legacy_attributes: - self._async_update_attrs() - await super().async_added_to_hass() - - @callback - def _handle_coordinator_update(self) -> None: - """Handle the state update and prepare the extra state attributes.""" - if self.entity_description.has_legacy_attributes: - self._async_update_attrs() - return super()._handle_coordinator_update() - - @callback - def _async_update_attrs(self) -> None: - """Update the extra state attributes based on the coordinator data.""" - if self.entity_description.has_legacy_attributes: - self._attr_extra_state_attributes = { - key: value - for key, value in self.coordinator.data[ - self.entity_description.index - ].items() - if key not in {"departure"} - } From c04a6cc639dba4b9fd4164c67101a165c53729eb Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 24 Jun 2024 05:37:12 -0400 Subject: [PATCH 1085/1445] Bump jaraco.abode to 5.1.2 (#117363) Co-authored-by: Joost Lekkerkerker --- .../components/abode/binary_sensor.py | 17 ++++---------- homeassistant/components/abode/camera.py | 3 +-- homeassistant/components/abode/cover.py | 3 +-- homeassistant/components/abode/light.py | 3 +-- homeassistant/components/abode/lock.py | 3 +-- homeassistant/components/abode/manifest.json | 2 +- homeassistant/components/abode/sensor.py | 23 ++++++------------- homeassistant/components/abode/switch.py | 5 ++-- requirements_all.txt | 5 +--- requirements_test_all.txt | 5 +--- .../abode/test_alarm_control_panel.py | 8 +++---- 11 files changed, 24 insertions(+), 53 deletions(-) diff --git a/homeassistant/components/abode/binary_sensor.py b/homeassistant/components/abode/binary_sensor.py index 1bccbf61701..0f1372dc8be 100644 --- a/homeassistant/components/abode/binary_sensor.py +++ b/homeassistant/components/abode/binary_sensor.py @@ -5,13 +5,6 @@ from __future__ import annotations from typing import cast from jaraco.abode.devices.sensor import BinarySensor -from jaraco.abode.helpers.constants import ( - TYPE_CONNECTIVITY, - TYPE_MOISTURE, - TYPE_MOTION, - TYPE_OCCUPANCY, - TYPE_OPENING, -) from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, @@ -34,11 +27,11 @@ async def async_setup_entry( data: AbodeSystem = hass.data[DOMAIN] device_types = [ - TYPE_CONNECTIVITY, - TYPE_MOISTURE, - TYPE_MOTION, - TYPE_OCCUPANCY, - TYPE_OPENING, + "connectivity", + "moisture", + "motion", + "occupancy", + "door", ] async_add_entities( diff --git a/homeassistant/components/abode/camera.py b/homeassistant/components/abode/camera.py index 57fcbf1fca4..58107f16462 100644 --- a/homeassistant/components/abode/camera.py +++ b/homeassistant/components/abode/camera.py @@ -8,7 +8,6 @@ from typing import Any, cast from jaraco.abode.devices.base import Device from jaraco.abode.devices.camera import Camera as AbodeCam from jaraco.abode.helpers import timeline -from jaraco.abode.helpers.constants import TYPE_CAMERA import requests from requests.models import Response @@ -34,7 +33,7 @@ async def async_setup_entry( async_add_entities( AbodeCamera(data, device, timeline.CAPTURE_IMAGE) - for device in data.abode.get_devices(generic_type=TYPE_CAMERA) + for device in data.abode.get_devices(generic_type="camera") ) diff --git a/homeassistant/components/abode/cover.py b/homeassistant/components/abode/cover.py index 96270cfd966..b5b1e878b96 100644 --- a/homeassistant/components/abode/cover.py +++ b/homeassistant/components/abode/cover.py @@ -3,7 +3,6 @@ from typing import Any from jaraco.abode.devices.cover import Cover -from jaraco.abode.helpers.constants import TYPE_COVER from homeassistant.components.cover import CoverEntity from homeassistant.config_entries import ConfigEntry @@ -23,7 +22,7 @@ async def async_setup_entry( async_add_entities( AbodeCover(data, device) - for device in data.abode.get_devices(generic_type=TYPE_COVER) + for device in data.abode.get_devices(generic_type="cover") ) diff --git a/homeassistant/components/abode/light.py b/homeassistant/components/abode/light.py index 83f00e417ad..d69aad80875 100644 --- a/homeassistant/components/abode/light.py +++ b/homeassistant/components/abode/light.py @@ -6,7 +6,6 @@ from math import ceil from typing import Any from jaraco.abode.devices.light import Light -from jaraco.abode.helpers.constants import TYPE_LIGHT from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -36,7 +35,7 @@ async def async_setup_entry( async_add_entities( AbodeLight(data, device) - for device in data.abode.get_devices(generic_type=TYPE_LIGHT) + for device in data.abode.get_devices(generic_type="light") ) diff --git a/homeassistant/components/abode/lock.py b/homeassistant/components/abode/lock.py index 3a65fa4d6dc..ceff263e6b5 100644 --- a/homeassistant/components/abode/lock.py +++ b/homeassistant/components/abode/lock.py @@ -3,7 +3,6 @@ from typing import Any from jaraco.abode.devices.lock import Lock -from jaraco.abode.helpers.constants import TYPE_LOCK from homeassistant.components.lock import LockEntity from homeassistant.config_entries import ConfigEntry @@ -23,7 +22,7 @@ async def async_setup_entry( async_add_entities( AbodeLock(data, device) - for device in data.abode.get_devices(generic_type=TYPE_LOCK) + for device in data.abode.get_devices(generic_type="lock") ) diff --git a/homeassistant/components/abode/manifest.json b/homeassistant/components/abode/manifest.json index c7d51c7ea1f..de1000319f1 100644 --- a/homeassistant/components/abode/manifest.json +++ b/homeassistant/components/abode/manifest.json @@ -9,5 +9,5 @@ }, "iot_class": "cloud_push", "loggers": ["jaraco.abode", "lomond"], - "requirements": ["jaraco.abode==3.3.0", "jaraco.functools==3.9.0"] + "requirements": ["jaraco.abode==5.1.2"] } diff --git a/homeassistant/components/abode/sensor.py b/homeassistant/components/abode/sensor.py index b57b3e77abc..d6a5389029b 100644 --- a/homeassistant/components/abode/sensor.py +++ b/homeassistant/components/abode/sensor.py @@ -7,15 +7,6 @@ from dataclasses import dataclass from typing import cast from jaraco.abode.devices.sensor import Sensor -from jaraco.abode.helpers.constants import ( - HUMI_STATUS_KEY, - LUX_STATUS_KEY, - STATUSES_KEY, - TEMP_STATUS_KEY, - TYPE_SENSOR, - UNIT_CELSIUS, - UNIT_FAHRENHEIT, -) from homeassistant.components.sensor import ( SensorDeviceClass, @@ -32,8 +23,8 @@ from .const import DOMAIN from .entity import AbodeDevice ABODE_TEMPERATURE_UNIT_HA_UNIT = { - UNIT_FAHRENHEIT: UnitOfTemperature.FAHRENHEIT, - UNIT_CELSIUS: UnitOfTemperature.CELSIUS, + "°F": UnitOfTemperature.FAHRENHEIT, + "°C": UnitOfTemperature.CELSIUS, } @@ -47,7 +38,7 @@ class AbodeSensorDescription(SensorEntityDescription): SENSOR_TYPES: tuple[AbodeSensorDescription, ...] = ( AbodeSensorDescription( - key=TEMP_STATUS_KEY, + key="temperature", device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement_fn=lambda device: ABODE_TEMPERATURE_UNIT_HA_UNIT[ device.temp_unit @@ -55,13 +46,13 @@ SENSOR_TYPES: tuple[AbodeSensorDescription, ...] = ( value_fn=lambda device: cast(float, device.temp), ), AbodeSensorDescription( - key=HUMI_STATUS_KEY, + key="humidity", device_class=SensorDeviceClass.HUMIDITY, native_unit_of_measurement_fn=lambda _: PERCENTAGE, value_fn=lambda device: cast(float, device.humidity), ), AbodeSensorDescription( - key=LUX_STATUS_KEY, + key="lux", device_class=SensorDeviceClass.ILLUMINANCE, native_unit_of_measurement_fn=lambda _: LIGHT_LUX, value_fn=lambda device: cast(float, device.lux), @@ -78,8 +69,8 @@ async def async_setup_entry( async_add_entities( AbodeSensor(data, device, description) for description in SENSOR_TYPES - for device in data.abode.get_devices(generic_type=TYPE_SENSOR) - if description.key in device.get_value(STATUSES_KEY) + for device in data.abode.get_devices(generic_type="sensor") + if description.key in device.get_value("statuses") ) diff --git a/homeassistant/components/abode/switch.py b/homeassistant/components/abode/switch.py index 64eb3529aab..7dad750c8d5 100644 --- a/homeassistant/components/abode/switch.py +++ b/homeassistant/components/abode/switch.py @@ -5,7 +5,6 @@ from __future__ import annotations from typing import Any, cast from jaraco.abode.devices.switch import Switch -from jaraco.abode.helpers.constants import TYPE_SWITCH, TYPE_VALVE from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry @@ -17,7 +16,7 @@ from . import AbodeSystem from .const import DOMAIN from .entity import AbodeAutomation, AbodeDevice -DEVICE_TYPES = [TYPE_SWITCH, TYPE_VALVE] +DEVICE_TYPES = ["switch", "valve"] async def async_setup_entry( @@ -89,4 +88,4 @@ class AbodeAutomationSwitch(AbodeAutomation, SwitchEntity): @property def is_on(self) -> bool: """Return True if the automation is enabled.""" - return bool(self._automation.is_enabled) + return bool(self._automation.enabled) diff --git a/requirements_all.txt b/requirements_all.txt index 52b887723fe..45b56edecbf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1182,10 +1182,7 @@ isal==1.6.1 ismartgate==5.0.1 # homeassistant.components.abode -jaraco.abode==3.3.0 - -# homeassistant.components.abode -jaraco.functools==3.9.0 +jaraco.abode==5.1.2 # homeassistant.components.jellyfin jellyfin-apiclient-python==1.9.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 88caa904094..8feebec2ef1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -969,10 +969,7 @@ isal==1.6.1 ismartgate==5.0.1 # homeassistant.components.abode -jaraco.abode==3.3.0 - -# homeassistant.components.abode -jaraco.functools==3.9.0 +jaraco.abode==5.1.2 # homeassistant.components.jellyfin jellyfin-apiclient-python==1.9.2 diff --git a/tests/components/abode/test_alarm_control_panel.py b/tests/components/abode/test_alarm_control_panel.py index 428e2791ee2..51e0ee46838 100644 --- a/tests/components/abode/test_alarm_control_panel.py +++ b/tests/components/abode/test_alarm_control_panel.py @@ -2,8 +2,6 @@ from unittest.mock import PropertyMock, patch -from jaraco.abode.helpers import constants as CONST - from homeassistant.components.abode import ATTR_DEVICE_ID from homeassistant.components.alarm_control_panel import DOMAIN as ALARM_DOMAIN from homeassistant.const import ( @@ -70,7 +68,7 @@ async def test_set_alarm_away(hass: HomeAssistant) -> None: "jaraco.abode.devices.alarm.Alarm.mode", new_callable=PropertyMock, ) as mock_mode: - mock_mode.return_value = CONST.MODE_AWAY + mock_mode.return_value = "away" update_callback = mock_callback.call_args[0][1] await hass.async_add_executor_job(update_callback, "area_1") @@ -100,7 +98,7 @@ async def test_set_alarm_home(hass: HomeAssistant) -> None: with patch( "jaraco.abode.devices.alarm.Alarm.mode", new_callable=PropertyMock ) as mock_mode: - mock_mode.return_value = CONST.MODE_HOME + mock_mode.return_value = "home" update_callback = mock_callback.call_args[0][1] await hass.async_add_executor_job(update_callback, "area_1") @@ -129,7 +127,7 @@ async def test_set_alarm_standby(hass: HomeAssistant) -> None: with patch( "jaraco.abode.devices.alarm.Alarm.mode", new_callable=PropertyMock ) as mock_mode: - mock_mode.return_value = CONST.MODE_STANDBY + mock_mode.return_value = "standby" update_callback = mock_callback.call_args[0][1] await hass.async_add_executor_job(update_callback, "area_1") From f3a1ca6d5403641fa35844ad9d0d36a976ec7249 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 24 Jun 2024 11:41:33 +0200 Subject: [PATCH 1086/1445] Add coordinator to Knocki (#120251) --- homeassistant/components/knocki/__init__.py | 30 +++++++--------- .../components/knocki/coordinator.py | 34 +++++++++++++++++++ homeassistant/components/knocki/event.py | 22 ++++++++++-- tests/components/knocki/test_event.py | 25 +++++++++++++- 4 files changed, 89 insertions(+), 22 deletions(-) create mode 100644 homeassistant/components/knocki/coordinator.py diff --git a/homeassistant/components/knocki/__init__.py b/homeassistant/components/knocki/__init__.py index ef024d6f4d6..ddf389649f2 100644 --- a/homeassistant/components/knocki/__init__.py +++ b/homeassistant/components/knocki/__init__.py @@ -2,27 +2,18 @@ from __future__ import annotations -from dataclasses import dataclass - -from knocki import KnockiClient, KnockiConnectionError, Trigger +from knocki import EventType, KnockiClient from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_TOKEN, Platform from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession +from .coordinator import KnockiCoordinator + PLATFORMS: list[Platform] = [Platform.EVENT] -type KnockiConfigEntry = ConfigEntry[KnockiData] - - -@dataclass -class KnockiData: - """Knocki data.""" - - client: KnockiClient - triggers: list[Trigger] +type KnockiConfigEntry = ConfigEntry[KnockiCoordinator] async def async_setup_entry(hass: HomeAssistant, entry: KnockiConfigEntry) -> bool: @@ -31,12 +22,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: KnockiConfigEntry) -> bo session=async_get_clientsession(hass), token=entry.data[CONF_TOKEN] ) - try: - triggers = await client.get_triggers() - except KnockiConnectionError as exc: - raise ConfigEntryNotReady from exc + coordinator = KnockiCoordinator(hass, client) - entry.runtime_data = KnockiData(client=client, triggers=triggers) + await coordinator.async_config_entry_first_refresh() + + entry.async_on_unload( + client.register_listener(EventType.CREATED, coordinator.add_trigger) + ) + + entry.runtime_data = coordinator await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) diff --git a/homeassistant/components/knocki/coordinator.py b/homeassistant/components/knocki/coordinator.py new file mode 100644 index 00000000000..020b3921a1e --- /dev/null +++ b/homeassistant/components/knocki/coordinator.py @@ -0,0 +1,34 @@ +"""Update coordinator for Knocki integration.""" + +from knocki import Event, KnockiClient, KnockiConnectionError, Trigger + +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import DOMAIN, LOGGER + + +class KnockiCoordinator(DataUpdateCoordinator[dict[int, Trigger]]): + """The Knocki coordinator.""" + + def __init__(self, hass: HomeAssistant, client: KnockiClient) -> None: + """Initialize the coordinator.""" + super().__init__( + hass, + logger=LOGGER, + name=DOMAIN, + ) + self.client = client + + async def _async_update_data(self) -> dict[int, Trigger]: + try: + triggers = await self.client.get_triggers() + except KnockiConnectionError as exc: + raise UpdateFailed from exc + return {trigger.details.trigger_id: trigger for trigger in triggers} + + def add_trigger(self, event: Event) -> None: + """Add a trigger to the coordinator.""" + self.async_set_updated_data( + {**self.data, event.payload.details.trigger_id: event.payload} + ) diff --git a/homeassistant/components/knocki/event.py b/homeassistant/components/knocki/event.py index 8cd5de21958..adaf344e468 100644 --- a/homeassistant/components/knocki/event.py +++ b/homeassistant/components/knocki/event.py @@ -3,7 +3,7 @@ from knocki import Event, EventType, KnockiClient, Trigger from homeassistant.components.event import EventEntity -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -17,10 +17,26 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up Knocki from a config entry.""" - entry_data = entry.runtime_data + coordinator = entry.runtime_data + + added_triggers = set(coordinator.data) + + @callback + def _async_add_entities() -> None: + current_triggers = set(coordinator.data) + new_triggers = current_triggers - added_triggers + added_triggers.update(new_triggers) + if new_triggers: + async_add_entities( + KnockiTrigger(coordinator.data[trigger], coordinator.client) + for trigger in new_triggers + ) + + coordinator.async_add_listener(_async_add_entities) async_add_entities( - KnockiTrigger(trigger, entry_data.client) for trigger in entry_data.triggers + KnockiTrigger(trigger, coordinator.client) + for trigger in coordinator.data.values() ) diff --git a/tests/components/knocki/test_event.py b/tests/components/knocki/test_event.py index a53e2811854..4740ddc9167 100644 --- a/tests/components/knocki/test_event.py +++ b/tests/components/knocki/test_event.py @@ -7,13 +7,14 @@ from knocki import Event, EventType, Trigger, TriggerDetails import pytest from syrupy import SnapshotAssertion +from homeassistant.components.knocki.const import DOMAIN from homeassistant.const import STATE_UNKNOWN from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from . import setup_integration -from tests.common import MockConfigEntry, snapshot_platform +from tests.common import MockConfigEntry, load_json_array_fixture, snapshot_platform async def test_entities( @@ -73,3 +74,25 @@ async def test_subscription( await hass.async_block_till_done() assert mock_knocki_client.register_listener.return_value.called + + +async def test_adding_runtime_entities( + hass: HomeAssistant, + mock_knocki_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test we can create devices on runtime.""" + mock_knocki_client.get_triggers.return_value = [] + + await setup_integration(hass, mock_config_entry) + + assert not hass.states.get("event.knc1_w_00000214_aaaa") + + add_trigger_function: Callable[[Event], None] = ( + mock_knocki_client.register_listener.call_args[0][1] + ) + trigger = Trigger.from_dict(load_json_array_fixture("triggers.json", DOMAIN)[0]) + + add_trigger_function(Event(EventType.CREATED, trigger)) + + assert hass.states.get("event.knc1_w_00000214_aaaa") is not None From 674dfa6e9cf425c8f323057c82b4ab3ef246457b Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 24 Jun 2024 11:55:48 +0200 Subject: [PATCH 1087/1445] Add button platform to AirGradient (#119917) --- .../components/airgradient/__init__.py | 7 +- .../components/airgradient/button.py | 104 +++++++++++++ .../components/airgradient/icons.json | 8 + .../components/airgradient/number.py | 7 +- .../components/airgradient/strings.json | 8 + .../airgradient/snapshots/test_button.ambr | 139 ++++++++++++++++++ tests/components/airgradient/test_button.py | 99 +++++++++++++ 7 files changed, 366 insertions(+), 6 deletions(-) create mode 100644 homeassistant/components/airgradient/button.py create mode 100644 tests/components/airgradient/snapshots/test_button.ambr create mode 100644 tests/components/airgradient/test_button.py diff --git a/homeassistant/components/airgradient/__init__.py b/homeassistant/components/airgradient/__init__.py index 76e11c05527..b1b5a28ef67 100644 --- a/homeassistant/components/airgradient/__init__.py +++ b/homeassistant/components/airgradient/__init__.py @@ -15,7 +15,12 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import DOMAIN from .coordinator import AirGradientConfigCoordinator, AirGradientMeasurementCoordinator -PLATFORMS: list[Platform] = [Platform.NUMBER, Platform.SELECT, Platform.SENSOR] +PLATFORMS: list[Platform] = [ + Platform.BUTTON, + Platform.NUMBER, + Platform.SELECT, + Platform.SENSOR, +] @dataclass diff --git a/homeassistant/components/airgradient/button.py b/homeassistant/components/airgradient/button.py new file mode 100644 index 00000000000..b59188ebdd4 --- /dev/null +++ b/homeassistant/components/airgradient/button.py @@ -0,0 +1,104 @@ +"""Support for AirGradient buttons.""" + +from collections.abc import Awaitable, Callable +from dataclasses import dataclass + +from airgradient import AirGradientClient, ConfigurationControl + +from homeassistant.components.button import ( + DOMAIN as BUTTON_DOMAIN, + ButtonEntity, + ButtonEntityDescription, +) +from homeassistant.const import EntityCategory +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import DOMAIN, AirGradientConfigEntry +from .coordinator import AirGradientConfigCoordinator +from .entity import AirGradientEntity + + +@dataclass(frozen=True, kw_only=True) +class AirGradientButtonEntityDescription(ButtonEntityDescription): + """Describes AirGradient button entity.""" + + press_fn: Callable[[AirGradientClient], Awaitable[None]] + + +CO2_CALIBRATION = AirGradientButtonEntityDescription( + key="co2_calibration", + translation_key="co2_calibration", + entity_category=EntityCategory.CONFIG, + press_fn=lambda client: client.request_co2_calibration(), +) +LED_BAR_TEST = AirGradientButtonEntityDescription( + key="led_bar_test", + translation_key="led_bar_test", + entity_category=EntityCategory.CONFIG, + press_fn=lambda client: client.request_led_bar_test(), +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: AirGradientConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up AirGradient button entities based on a config entry.""" + model = entry.runtime_data.measurement.data.model + coordinator = entry.runtime_data.config + + added_entities = False + + @callback + def _check_entities() -> None: + nonlocal added_entities + + if ( + coordinator.data.configuration_control is ConfigurationControl.LOCAL + and not added_entities + ): + entities = [AirGradientButton(coordinator, CO2_CALIBRATION)] + if "L" in model: + entities.append(AirGradientButton(coordinator, LED_BAR_TEST)) + + async_add_entities(entities) + added_entities = True + elif ( + coordinator.data.configuration_control is not ConfigurationControl.LOCAL + and added_entities + ): + entity_registry = er.async_get(hass) + for entity_description in (CO2_CALIBRATION, LED_BAR_TEST): + unique_id = f"{coordinator.serial_number}-{entity_description.key}" + if entity_id := entity_registry.async_get_entity_id( + BUTTON_DOMAIN, DOMAIN, unique_id + ): + entity_registry.async_remove(entity_id) + added_entities = False + + coordinator.async_add_listener(_check_entities) + _check_entities() + + +class AirGradientButton(AirGradientEntity, ButtonEntity): + """Defines an AirGradient button.""" + + entity_description: AirGradientButtonEntityDescription + coordinator: AirGradientConfigCoordinator + + def __init__( + self, + coordinator: AirGradientConfigCoordinator, + description: AirGradientButtonEntityDescription, + ) -> None: + """Initialize airgradient button.""" + super().__init__(coordinator) + self.entity_description = description + self._attr_unique_id = f"{coordinator.serial_number}-{description.key}" + + async def async_press(self) -> None: + """Press the button.""" + await self.entity_description.press_fn(self.coordinator.client) diff --git a/homeassistant/components/airgradient/icons.json b/homeassistant/components/airgradient/icons.json index cf0c80c873e..45d1e12d46e 100644 --- a/homeassistant/components/airgradient/icons.json +++ b/homeassistant/components/airgradient/icons.json @@ -1,5 +1,13 @@ { "entity": { + "button": { + "co2_calibration": { + "default": "mdi:molecule-co2" + }, + "led_bar_test": { + "default": "mdi:lightbulb-on-outline" + } + }, "sensor": { "total_volatile_organic_component_index": { "default": "mdi:molecule" diff --git a/homeassistant/components/airgradient/number.py b/homeassistant/components/airgradient/number.py index e065b76ed51..139357f3753 100644 --- a/homeassistant/components/airgradient/number.py +++ b/homeassistant/components/airgradient/number.py @@ -88,11 +88,8 @@ async def async_setup_entry( and added_entities ): entity_registry = er.async_get(hass) - unique_ids = [ - f"{coordinator.serial_number}-{entity_description.key}" - for entity_description in (DISPLAY_BRIGHTNESS, LED_BAR_BRIGHTNESS) - ] - for unique_id in unique_ids: + for entity_description in (DISPLAY_BRIGHTNESS, LED_BAR_BRIGHTNESS): + unique_id = f"{coordinator.serial_number}-{entity_description.key}" if entity_id := entity_registry.async_get_entity_id( NUMBER_DOMAIN, DOMAIN, unique_id ): diff --git a/homeassistant/components/airgradient/strings.json b/homeassistant/components/airgradient/strings.json index 6c079419839..0b5c245f04c 100644 --- a/homeassistant/components/airgradient/strings.json +++ b/homeassistant/components/airgradient/strings.json @@ -24,6 +24,14 @@ } }, "entity": { + "button": { + "co2_calibration": { + "name": "Calibrate CO2 sensor" + }, + "led_bar_test": { + "name": "Test LED bar" + } + }, "number": { "led_bar_brightness": { "name": "LED bar brightness" diff --git a/tests/components/airgradient/snapshots/test_button.ambr b/tests/components/airgradient/snapshots/test_button.ambr new file mode 100644 index 00000000000..fa3f8994c3c --- /dev/null +++ b/tests/components/airgradient/snapshots/test_button.ambr @@ -0,0 +1,139 @@ +# serializer version: 1 +# name: test_all_entities[indoor][button.airgradient_calibrate_co2_sensor-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.airgradient_calibrate_co2_sensor', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Calibrate CO2 sensor', + 'platform': 'airgradient', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'co2_calibration', + 'unique_id': '84fce612f5b8-co2_calibration', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[indoor][button.airgradient_calibrate_co2_sensor-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Airgradient Calibrate CO2 sensor', + }), + 'context': , + 'entity_id': 'button.airgradient_calibrate_co2_sensor', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_entities[indoor][button.airgradient_test_led_bar-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.airgradient_test_led_bar', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Test LED bar', + 'platform': 'airgradient', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'led_bar_test', + 'unique_id': '84fce612f5b8-led_bar_test', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[indoor][button.airgradient_test_led_bar-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Airgradient Test LED bar', + }), + 'context': , + 'entity_id': 'button.airgradient_test_led_bar', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_entities[outdoor][button.airgradient_calibrate_co2_sensor-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.airgradient_calibrate_co2_sensor', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Calibrate CO2 sensor', + 'platform': 'airgradient', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'co2_calibration', + 'unique_id': '84fce612f5b8-co2_calibration', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[outdoor][button.airgradient_calibrate_co2_sensor-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Airgradient Calibrate CO2 sensor', + }), + 'context': , + 'entity_id': 'button.airgradient_calibrate_co2_sensor', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- diff --git a/tests/components/airgradient/test_button.py b/tests/components/airgradient/test_button.py new file mode 100644 index 00000000000..7901c3a067b --- /dev/null +++ b/tests/components/airgradient/test_button.py @@ -0,0 +1,99 @@ +"""Tests for the AirGradient button platform.""" + +from datetime import timedelta +from unittest.mock import AsyncMock, patch + +from airgradient import Config +from freezegun.api import FrozenDateTimeFactory +from syrupy import SnapshotAssertion + +from homeassistant.components.airgradient import DOMAIN +from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS +from homeassistant.const import ATTR_ENTITY_ID, Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import setup_integration + +from tests.common import ( + MockConfigEntry, + async_fire_time_changed, + load_fixture, + snapshot_platform, +) + + +async def test_all_entities( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + airgradient_devices: AsyncMock, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, +) -> None: + """Test all entities.""" + with patch("homeassistant.components.airgradient.PLATFORMS", [Platform.BUTTON]): + await setup_integration(hass, mock_config_entry) + + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + +async def test_pressing_button( + hass: HomeAssistant, + mock_airgradient_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test pressing button.""" + await setup_integration(hass, mock_config_entry) + + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + { + ATTR_ENTITY_ID: "button.airgradient_calibrate_co2_sensor", + }, + blocking=True, + ) + mock_airgradient_client.request_co2_calibration.assert_called_once() + + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + { + ATTR_ENTITY_ID: "button.airgradient_test_led_bar", + }, + blocking=True, + ) + mock_airgradient_client.request_led_bar_test.assert_called_once() + + +async def test_cloud_creates_no_button( + hass: HomeAssistant, + mock_cloud_airgradient_client: AsyncMock, + mock_config_entry: MockConfigEntry, + freezer: FrozenDateTimeFactory, +) -> None: + """Test cloud configuration control.""" + with patch("homeassistant.components.airgradient.PLATFORMS", [Platform.BUTTON]): + await setup_integration(hass, mock_config_entry) + + assert len(hass.states.async_all()) == 0 + + mock_cloud_airgradient_client.get_config.return_value = Config.from_json( + load_fixture("get_config_local.json", DOMAIN) + ) + + freezer.tick(timedelta(minutes=5)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 2 + + mock_cloud_airgradient_client.get_config.return_value = Config.from_json( + load_fixture("get_config_cloud.json", DOMAIN) + ) + + freezer.tick(timedelta(minutes=5)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 0 From 237f20de6ce462f6013228b70808b0de0c68937b Mon Sep 17 00:00:00 2001 From: "Mr. Bubbles" Date: Mon, 24 Jun 2024 12:58:37 +0200 Subject: [PATCH 1088/1445] Add DataUpdateCoordinator to pyLoad integration (#120237) * Add DataUpdateCoordinator * Update tests * changes * changes * test coverage * some changes * Update homeassistant/components/pyload/sensor.py * use dataclass * fix ConfigEntry * fix configtype * fix some issues * remove logger * remove unnecessary else * revert fixture changes --------- Co-authored-by: Joost Lekkerkerker --- homeassistant/components/pyload/__init__.py | 8 +- .../components/pyload/coordinator.py | 78 ++++++++ homeassistant/components/pyload/sensor.py | 87 ++------- tests/components/pyload/conftest.py | 4 +- .../pyload/snapshots/test_sensor.ambr | 177 ------------------ tests/components/pyload/test_sensor.py | 2 +- 6 files changed, 100 insertions(+), 256 deletions(-) create mode 100644 homeassistant/components/pyload/coordinator.py diff --git a/homeassistant/components/pyload/__init__.py b/homeassistant/components/pyload/__init__.py index a2e105e6454..d7c7e9454ea 100644 --- a/homeassistant/components/pyload/__init__.py +++ b/homeassistant/components/pyload/__init__.py @@ -20,9 +20,11 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryError, ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_create_clientsession +from .coordinator import PyLoadCoordinator + PLATFORMS: list[Platform] = [Platform.SENSOR] -type PyLoadConfigEntry = ConfigEntry[PyLoadAPI] +type PyLoadConfigEntry = ConfigEntry[PyLoadCoordinator] async def async_setup_entry(hass: HomeAssistant, entry: PyLoadConfigEntry) -> bool: @@ -57,9 +59,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: PyLoadConfigEntry) -> bo raise ConfigEntryError( f"Authentication failed for {entry.data[CONF_USERNAME]}, check your login credentials" ) from e + coordinator = PyLoadCoordinator(hass, pyloadapi) - entry.runtime_data = pyloadapi + await coordinator.async_config_entry_first_refresh() + entry.runtime_data = coordinator await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/pyload/coordinator.py b/homeassistant/components/pyload/coordinator.py new file mode 100644 index 00000000000..008375c3a34 --- /dev/null +++ b/homeassistant/components/pyload/coordinator.py @@ -0,0 +1,78 @@ +"""Update coordinator for pyLoad Integration.""" + +from dataclasses import dataclass +from datetime import timedelta +import logging + +from pyloadapi import CannotConnect, InvalidAuth, ParserError, PyLoadAPI + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryError +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +SCAN_INTERVAL = timedelta(seconds=20) + + +@dataclass(kw_only=True) +class pyLoadData: + """Data from pyLoad.""" + + pause: bool + active: int + queue: int + total: int + speed: float + download: bool + reconnect: bool + captcha: bool + free_space: int + + +class PyLoadCoordinator(DataUpdateCoordinator[pyLoadData]): + """pyLoad coordinator.""" + + config_entry: ConfigEntry + + def __init__(self, hass: HomeAssistant, pyload: PyLoadAPI) -> None: + """Initialize pyLoad coordinator.""" + super().__init__( + hass, + _LOGGER, + name=DOMAIN, + update_interval=SCAN_INTERVAL, + ) + self.pyload = pyload + self.version: str | None = None + + async def _async_update_data(self) -> pyLoadData: + """Fetch data from API endpoint.""" + try: + if not self.version: + self.version = await self.pyload.version() + return pyLoadData( + **await self.pyload.get_status(), + free_space=await self.pyload.free_space(), + ) + + except InvalidAuth as e: + try: + await self.pyload.login() + except InvalidAuth as exc: + raise ConfigEntryError( + f"Authentication failed for {self.pyload.username}, check your login credentials", + ) from exc + + raise UpdateFailed( + "Unable to retrieve data due to cookie expiration but re-authentication was successful." + ) from e + except CannotConnect as e: + raise UpdateFailed( + "Unable to connect and retrieve data from pyLoad API" + ) from e + except ParserError as e: + raise UpdateFailed("Unable to parse data from pyLoad API") from e diff --git a/homeassistant/components/pyload/sensor.py b/homeassistant/components/pyload/sensor.py index aa86dde9260..7caef84d2dc 100644 --- a/homeassistant/components/pyload/sensor.py +++ b/homeassistant/components/pyload/sensor.py @@ -2,18 +2,8 @@ from __future__ import annotations -from datetime import timedelta from enum import StrEnum -import logging -from time import monotonic -from pyloadapi import ( - CannotConnect, - InvalidAuth, - ParserError, - PyLoadAPI, - StatusServerResponse, -) import voluptuous as vol from homeassistant.components.sensor import ( @@ -40,13 +30,11 @@ from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType +from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import PyLoadConfigEntry from .const import DEFAULT_HOST, DEFAULT_NAME, DEFAULT_PORT, DOMAIN, ISSUE_PLACEHOLDER - -_LOGGER = logging.getLogger(__name__) - -SCAN_INTERVAL = timedelta(seconds=15) +from .coordinator import PyLoadCoordinator class PyLoadSensorEntity(StrEnum): @@ -92,7 +80,6 @@ async def async_setup_platform( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_IMPORT}, data=config ) - _LOGGER.debug(result) if ( result.get("type") == FlowResultType.CREATE_ENTRY or result.get("reason") == "already_configured" @@ -132,91 +119,45 @@ async def async_setup_entry( ) -> None: """Set up the pyLoad sensors.""" - pyloadapi = entry.runtime_data + coordinator = entry.runtime_data async_add_entities( ( PyLoadSensor( - api=pyloadapi, + coordinator=coordinator, entity_description=description, - client_name=entry.title, - entry_id=entry.entry_id, ) for description in SENSOR_DESCRIPTIONS ), - True, ) -class PyLoadSensor(SensorEntity): +class PyLoadSensor(CoordinatorEntity[PyLoadCoordinator], SensorEntity): """Representation of a pyLoad sensor.""" _attr_has_entity_name = True def __init__( self, - api: PyLoadAPI, + coordinator: PyLoadCoordinator, entity_description: SensorEntityDescription, - client_name: str, - entry_id: str, ) -> None: """Initialize a new pyLoad sensor.""" - self.type = entity_description.key - self.api = api - self._attr_unique_id = f"{entry_id}_{entity_description.key}" + super().__init__(coordinator) + self._attr_unique_id = ( + f"{coordinator.config_entry.entry_id}_{entity_description.key}" + ) self.entity_description = entity_description - self._attr_available = False - self.data: StatusServerResponse self.device_info = DeviceInfo( entry_type=DeviceEntryType.SERVICE, manufacturer="PyLoad Team", model="pyLoad", - configuration_url=api.api_url, - identifiers={(DOMAIN, entry_id)}, + configuration_url=coordinator.pyload.api_url, + identifiers={(DOMAIN, coordinator.config_entry.entry_id)}, + sw_version=coordinator.version, ) - async def async_update(self) -> None: - """Update state of sensor.""" - start = monotonic() - try: - status = await self.api.get_status() - except InvalidAuth: - _LOGGER.info("Authentication failed, trying to reauthenticate") - try: - await self.api.login() - except InvalidAuth: - _LOGGER.error( - "Authentication failed for %s, check your login credentials", - self.api.username, - ) - return - else: - _LOGGER.info( - "Unable to retrieve data due to cookie expiration " - "but re-authentication was successful" - ) - return - finally: - self._attr_available = False - - except CannotConnect: - _LOGGER.debug("Unable to connect and retrieve data from pyLoad API") - self._attr_available = False - return - except ParserError: - _LOGGER.error("Unable to parse data from pyLoad API") - self._attr_available = False - return - else: - self.data = status - _LOGGER.debug( - "Finished fetching pyload data in %.3f seconds", - monotonic() - start, - ) - - self._attr_available = True - @property def native_value(self) -> StateType: """Return the state of the sensor.""" - return self.data.get(self.entity_description.key) + return getattr(self.coordinator.data, self.entity_description.key) diff --git a/tests/components/pyload/conftest.py b/tests/components/pyload/conftest.py index 0dafb9af4df..3c6f9fdb49a 100644 --- a/tests/components/pyload/conftest.py +++ b/tests/components/pyload/conftest.py @@ -66,12 +66,10 @@ def mock_pyloadapi() -> Generator[AsyncMock, None, None]: "homeassistant.components.pyload.PyLoadAPI", autospec=True ) as mock_client, patch("homeassistant.components.pyload.config_flow.PyLoadAPI", new=mock_client), - patch("homeassistant.components.pyload.sensor.PyLoadAPI", new=mock_client), ): client = mock_client.return_value client.username = "username" client.api_url = "https://pyload.local:8000/" - client.login.return_value = LoginResponse( { "_permanent": True, @@ -97,7 +95,7 @@ def mock_pyloadapi() -> Generator[AsyncMock, None, None]: "captcha": False, } ) - + client.version.return_value = "0.5.0" client.free_space.return_value = 99999999999 yield client diff --git a/tests/components/pyload/snapshots/test_sensor.ambr b/tests/components/pyload/snapshots/test_sensor.ambr index f1e42ea049c..a6049577f47 100644 --- a/tests/components/pyload/snapshots/test_sensor.ambr +++ b/tests/components/pyload/snapshots/test_sensor.ambr @@ -161,183 +161,6 @@ 'state': 'unavailable', }) # --- -# name: test_sensors_unavailable[CannotConnect][sensor.pyload_speed-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.pyload_speed', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 1, - }), - 'sensor.private': dict({ - 'suggested_unit_of_measurement': , - }), - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Speed', - 'platform': 'pyload', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': 'XXXXXXXXXXXXXX_speed', - 'unit_of_measurement': , - }) -# --- -# name: test_sensors_unavailable[CannotConnect][sensor.pyload_speed-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'data_rate', - 'friendly_name': 'pyLoad Speed', - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.pyload_speed', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unavailable', - }) -# --- -# name: test_sensors_unavailable[InvalidAuth][sensor.pyload_speed-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.pyload_speed', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 1, - }), - 'sensor.private': dict({ - 'suggested_unit_of_measurement': , - }), - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Speed', - 'platform': 'pyload', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': 'XXXXXXXXXXXXXX_speed', - 'unit_of_measurement': , - }) -# --- -# name: test_sensors_unavailable[InvalidAuth][sensor.pyload_speed-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'data_rate', - 'friendly_name': 'pyLoad Speed', - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.pyload_speed', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unavailable', - }) -# --- -# name: test_sensors_unavailable[ParserError][sensor.pyload_speed-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.pyload_speed', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 1, - }), - 'sensor.private': dict({ - 'suggested_unit_of_measurement': , - }), - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Speed', - 'platform': 'pyload', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': 'XXXXXXXXXXXXXX_speed', - 'unit_of_measurement': , - }) -# --- -# name: test_sensors_unavailable[ParserError][sensor.pyload_speed-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'data_rate', - 'friendly_name': 'pyLoad Speed', - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.pyload_speed', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unavailable', - }) -# --- -# name: test_setup - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'data_rate', - 'friendly_name': 'pyload Speed', - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.pyload_speed', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '5.405963', - }) -# --- # name: test_setup[sensor.pyload_speed-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/pyload/test_sensor.py b/tests/components/pyload/test_sensor.py index d0e912f82f2..49795284fc6 100644 --- a/tests/components/pyload/test_sensor.py +++ b/tests/components/pyload/test_sensor.py @@ -8,7 +8,7 @@ import pytest from syrupy.assertion import SnapshotAssertion from homeassistant.components.pyload.const import DOMAIN -from homeassistant.components.pyload.sensor import SCAN_INTERVAL +from homeassistant.components.pyload.coordinator import SCAN_INTERVAL from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant From e3806d12f442bb2373f879de3a52bebacbca81d9 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 24 Jun 2024 13:34:23 +0200 Subject: [PATCH 1089/1445] Improve type hints in simplisafe tests (#120303) --- tests/components/simplisafe/conftest.py | 53 +++++++++++++++---------- 1 file changed, 33 insertions(+), 20 deletions(-) diff --git a/tests/components/simplisafe/conftest.py b/tests/components/simplisafe/conftest.py index cc387ee765b..aaf853863e5 100644 --- a/tests/components/simplisafe/conftest.py +++ b/tests/components/simplisafe/conftest.py @@ -1,18 +1,20 @@ """Define test fixtures for SimpliSafe.""" -import json from unittest.mock import AsyncMock, Mock, patch import pytest from simplipy.system.v3 import SystemV3 +from typing_extensions import AsyncGenerator from homeassistant.components.simplisafe.const import DOMAIN from homeassistant.const import CONF_CODE, CONF_PASSWORD, CONF_TOKEN, CONF_USERNAME +from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component +from homeassistant.util.json import JsonObjectType from .common import REFRESH_TOKEN, USER_ID, USERNAME -from tests.common import MockConfigEntry, load_fixture +from tests.common import MockConfigEntry, load_json_object_fixture CODE = "12345" PASSWORD = "password" @@ -20,7 +22,9 @@ SYSTEM_ID = 12345 @pytest.fixture(name="api") -def api_fixture(data_subscription, system_v3, websocket): +def api_fixture( + data_subscription: JsonObjectType, system_v3: SystemV3, websocket: Mock +) -> Mock: """Define a simplisafe-python API object.""" return Mock( async_get_systems=AsyncMock(return_value={SYSTEM_ID: system_v3}), @@ -32,7 +36,9 @@ def api_fixture(data_subscription, system_v3, websocket): @pytest.fixture(name="config_entry") -def config_entry_fixture(hass, config, unique_id): +def config_entry_fixture( + hass: HomeAssistant, config: dict[str, str], unique_id: str +) -> MockConfigEntry: """Define a config entry.""" entry = MockConfigEntry( domain=DOMAIN, unique_id=unique_id, data=config, options={CONF_CODE: "1234"} @@ -42,7 +48,7 @@ def config_entry_fixture(hass, config, unique_id): @pytest.fixture(name="config") -def config_fixture(): +def config_fixture() -> dict[str, str]: """Define config entry data config.""" return { CONF_TOKEN: REFRESH_TOKEN, @@ -51,7 +57,7 @@ def config_fixture(): @pytest.fixture(name="credentials_config") -def credentials_config_fixture(): +def credentials_config_fixture() -> dict[str, str]: """Define a username/password config.""" return { CONF_USERNAME: USERNAME, @@ -60,32 +66,32 @@ def credentials_config_fixture(): @pytest.fixture(name="data_latest_event", scope="package") -def data_latest_event_fixture(): +def data_latest_event_fixture() -> JsonObjectType: """Define latest event data.""" - return json.loads(load_fixture("latest_event_data.json", "simplisafe")) + return load_json_object_fixture("latest_event_data.json", "simplisafe") @pytest.fixture(name="data_sensor", scope="package") -def data_sensor_fixture(): +def data_sensor_fixture() -> JsonObjectType: """Define sensor data.""" - return json.loads(load_fixture("sensor_data.json", "simplisafe")) + return load_json_object_fixture("sensor_data.json", "simplisafe") @pytest.fixture(name="data_settings", scope="package") -def data_settings_fixture(): +def data_settings_fixture() -> JsonObjectType: """Define settings data.""" - return json.loads(load_fixture("settings_data.json", "simplisafe")) + return load_json_object_fixture("settings_data.json", "simplisafe") @pytest.fixture(name="data_subscription", scope="package") -def data_subscription_fixture(): +def data_subscription_fixture() -> JsonObjectType: """Define subscription data.""" - data = json.loads(load_fixture("subscription_data.json", "simplisafe")) + data = load_json_object_fixture("subscription_data.json", "simplisafe") return {SYSTEM_ID: data} @pytest.fixture(name="reauth_config") -def reauth_config_fixture(): +def reauth_config_fixture() -> dict[str, str]: """Define a reauth config.""" return { CONF_PASSWORD: PASSWORD, @@ -93,7 +99,9 @@ def reauth_config_fixture(): @pytest.fixture(name="setup_simplisafe") -async def setup_simplisafe_fixture(hass, api, config): +async def setup_simplisafe_fixture( + hass: HomeAssistant, api: Mock, config: dict[str, str] +) -> AsyncGenerator[None]: """Define a fixture to set up SimpliSafe.""" with ( patch( @@ -122,7 +130,7 @@ async def setup_simplisafe_fixture(hass, api, config): @pytest.fixture(name="sms_config") -def sms_config_fixture(): +def sms_config_fixture() -> dict[str, str]: """Define a SMS-based two-factor authentication config.""" return { CONF_CODE: CODE, @@ -130,7 +138,12 @@ def sms_config_fixture(): @pytest.fixture(name="system_v3") -def system_v3_fixture(data_latest_event, data_sensor, data_settings, data_subscription): +def system_v3_fixture( + data_latest_event: JsonObjectType, + data_sensor: JsonObjectType, + data_settings: JsonObjectType, + data_subscription: JsonObjectType, +) -> SystemV3: """Define a simplisafe-python V3 System object.""" system = SystemV3(Mock(subscription_data=data_subscription), SYSTEM_ID) system.async_get_latest_event = AsyncMock(return_value=data_latest_event) @@ -141,13 +154,13 @@ def system_v3_fixture(data_latest_event, data_sensor, data_settings, data_subscr @pytest.fixture(name="unique_id") -def unique_id_fixture(): +def unique_id_fixture() -> str: """Define a unique ID.""" return USER_ID @pytest.fixture(name="websocket") -def websocket_fixture(): +def websocket_fixture() -> Mock: """Define a simplisafe-python websocket object.""" return Mock( async_connect=AsyncMock(), From aef2f7d7078cf317a17bff7f17f949097137b4ae Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 24 Jun 2024 13:34:44 +0200 Subject: [PATCH 1090/1445] Improve type hints in canary tests (#120305) --- tests/components/canary/conftest.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/tests/components/canary/conftest.py b/tests/components/canary/conftest.py index 336e6577ecc..583986fd483 100644 --- a/tests/components/canary/conftest.py +++ b/tests/components/canary/conftest.py @@ -4,16 +4,19 @@ from unittest.mock import MagicMock, patch from canary.api import Api import pytest +from typing_extensions import Generator + +from homeassistant.core import HomeAssistant @pytest.fixture(autouse=True) -def mock_ffmpeg(hass): +def mock_ffmpeg(hass: HomeAssistant) -> None: """Mock ffmpeg is loaded.""" hass.config.components.add("ffmpeg") @pytest.fixture -def canary(hass): +def canary() -> Generator[MagicMock]: """Mock the CanaryApi for easier testing.""" with ( patch.object(Api, "login", return_value=True), @@ -38,7 +41,7 @@ def canary(hass): @pytest.fixture -def canary_config_flow(hass): +def canary_config_flow() -> Generator[MagicMock]: """Mock the CanaryApi for easier config flow testing.""" with ( patch.object(Api, "login", return_value=True), From b4d0de9c0ff37a979a487b246935df210c4c3477 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 24 Jun 2024 13:41:55 +0200 Subject: [PATCH 1091/1445] Improve type hints in conversation tests (#120306) --- .../conversation/test_default_agent.py | 104 ++++++++++-------- 1 file changed, 59 insertions(+), 45 deletions(-) diff --git a/tests/components/conversation/test_default_agent.py b/tests/components/conversation/test_default_agent.py index 511967e3a9c..dee7b4ca0ff 100644 --- a/tests/components/conversation/test_default_agent.py +++ b/tests/components/conversation/test_default_agent.py @@ -1,6 +1,7 @@ """Test for the default agent.""" from collections import defaultdict +from typing import Any from unittest.mock import AsyncMock, patch from hassil.recognize import Intent, IntentData, MatchEntity, RecognizeResult @@ -34,7 +35,7 @@ from tests.common import MockConfigEntry, async_mock_service @pytest.fixture -async def init_components(hass): +async def init_components(hass: HomeAssistant) -> None: """Initialize relevant components with empty configs.""" assert await async_setup_component(hass, "homeassistant", {}) assert await async_setup_component(hass, "conversation", {}) @@ -50,8 +51,9 @@ async def init_components(hass): {"entity_category": entity.EntityCategory.DIAGNOSTIC}, ], ) +@pytest.mark.usefixtures("init_components") async def test_hidden_entities_skipped( - hass: HomeAssistant, init_components, er_kwargs, entity_registry: er.EntityRegistry + hass: HomeAssistant, er_kwargs: dict[str, Any], entity_registry: er.EntityRegistry ) -> None: """Test we skip hidden entities.""" @@ -69,7 +71,8 @@ async def test_hidden_entities_skipped( assert result.response.error_code == intent.IntentResponseErrorCode.NO_VALID_TARGETS -async def test_exposed_domains(hass: HomeAssistant, init_components) -> None: +@pytest.mark.usefixtures("init_components") +async def test_exposed_domains(hass: HomeAssistant) -> None: """Test that we can't interact with entities that aren't exposed.""" hass.states.async_set( "lock.front_door", "off", attributes={ATTR_FRIENDLY_NAME: "Front Door"} @@ -93,9 +96,9 @@ async def test_exposed_domains(hass: HomeAssistant, init_components) -> None: assert result.response.error_code == intent.IntentResponseErrorCode.NO_VALID_TARGETS +@pytest.mark.usefixtures("init_components") async def test_exposed_areas( hass: HomeAssistant, - init_components, area_registry: ar.AreaRegistry, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, @@ -160,10 +163,8 @@ async def test_exposed_areas( assert result.response.response_type == intent.IntentResponseType.QUERY_ANSWER -async def test_conversation_agent( - hass: HomeAssistant, - init_components, -) -> None: +@pytest.mark.usefixtures("init_components") +async def test_conversation_agent(hass: HomeAssistant) -> None: """Test DefaultAgent.""" agent = default_agent.async_get_default_agent(hass) with patch( @@ -209,9 +210,9 @@ async def test_expose_flag_automatically_set( } +@pytest.mark.usefixtures("init_components") async def test_unexposed_entities_skipped( hass: HomeAssistant, - init_components, area_registry: ar.AreaRegistry, entity_registry: er.EntityRegistry, ) -> None: @@ -262,7 +263,8 @@ async def test_unexposed_entities_skipped( assert result.response.matched_states[0].entity_id == exposed_light.entity_id -async def test_trigger_sentences(hass: HomeAssistant, init_components) -> None: +@pytest.mark.usefixtures("init_components") +async def test_trigger_sentences(hass: HomeAssistant) -> None: """Test registering/unregistering/matching a few trigger sentences.""" trigger_sentences = ["It's party time", "It is time to party"] trigger_response = "Cowabunga!" @@ -303,9 +305,8 @@ async def test_trigger_sentences(hass: HomeAssistant, init_components) -> None: assert len(callback.mock_calls) == 0 -async def test_shopping_list_add_item( - hass: HomeAssistant, init_components, sl_setup -) -> None: +@pytest.mark.usefixtures("init_components", "sl_setup") +async def test_shopping_list_add_item(hass: HomeAssistant) -> None: """Test adding an item to the shopping list through the default agent.""" result = await conversation.async_converse( hass, "add apples to my shopping list", None, Context() @@ -316,7 +317,8 @@ async def test_shopping_list_add_item( } -async def test_nevermind_item(hass: HomeAssistant, init_components) -> None: +@pytest.mark.usefixtures("init_components") +async def test_nevermind_item(hass: HomeAssistant) -> None: """Test HassNevermind intent through the default agent.""" result = await conversation.async_converse(hass, "nevermind", None, Context()) assert result.response.intent is not None @@ -326,9 +328,9 @@ async def test_nevermind_item(hass: HomeAssistant, init_components) -> None: assert not result.response.speech +@pytest.mark.usefixtures("init_components") async def test_device_area_context( hass: HomeAssistant, - init_components, area_registry: ar.AreaRegistry, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, @@ -465,7 +467,8 @@ async def test_device_area_context( } -async def test_error_no_device(hass: HomeAssistant, init_components) -> None: +@pytest.mark.usefixtures("init_components") +async def test_error_no_device(hass: HomeAssistant) -> None: """Test error message when device/entity is missing.""" result = await conversation.async_converse( hass, "turn on missing entity", None, Context(), None @@ -479,7 +482,8 @@ async def test_error_no_device(hass: HomeAssistant, init_components) -> None: ) -async def test_error_no_area(hass: HomeAssistant, init_components) -> None: +@pytest.mark.usefixtures("init_components") +async def test_error_no_area(hass: HomeAssistant) -> None: """Test error message when area is missing.""" result = await conversation.async_converse( hass, "turn on the lights in missing area", None, Context(), None @@ -493,7 +497,8 @@ async def test_error_no_area(hass: HomeAssistant, init_components) -> None: ) -async def test_error_no_floor(hass: HomeAssistant, init_components) -> None: +@pytest.mark.usefixtures("init_components") +async def test_error_no_floor(hass: HomeAssistant) -> None: """Test error message when floor is missing.""" result = await conversation.async_converse( hass, "turn on all the lights on missing floor", None, Context(), None @@ -507,8 +512,9 @@ async def test_error_no_floor(hass: HomeAssistant, init_components) -> None: ) +@pytest.mark.usefixtures("init_components") async def test_error_no_device_in_area( - hass: HomeAssistant, init_components, area_registry: ar.AreaRegistry + hass: HomeAssistant, area_registry: ar.AreaRegistry ) -> None: """Test error message when area is missing a device/entity.""" area_kitchen = area_registry.async_get_or_create("kitchen_id") @@ -525,9 +531,8 @@ async def test_error_no_device_in_area( ) -async def test_error_no_domain( - hass: HomeAssistant, init_components, area_registry: ar.AreaRegistry -) -> None: +@pytest.mark.usefixtures("init_components") +async def test_error_no_domain(hass: HomeAssistant) -> None: """Test error message when no devices/entities exist for a domain.""" # We don't have a sentence for turning on all fans @@ -558,8 +563,9 @@ async def test_error_no_domain( ) +@pytest.mark.usefixtures("init_components") async def test_error_no_domain_in_area( - hass: HomeAssistant, init_components, area_registry: ar.AreaRegistry + hass: HomeAssistant, area_registry: ar.AreaRegistry ) -> None: """Test error message when no devices/entities for a domain exist in an area.""" area_kitchen = area_registry.async_get_or_create("kitchen_id") @@ -576,9 +582,9 @@ async def test_error_no_domain_in_area( ) +@pytest.mark.usefixtures("init_components") async def test_error_no_domain_in_floor( hass: HomeAssistant, - init_components, area_registry: ar.AreaRegistry, floor_registry: fr.FloorRegistry, ) -> None: @@ -618,7 +624,8 @@ async def test_error_no_domain_in_floor( ) -async def test_error_no_device_class(hass: HomeAssistant, init_components) -> None: +@pytest.mark.usefixtures("init_components") +async def test_error_no_device_class(hass: HomeAssistant) -> None: """Test error message when no entities of a device class exist.""" # Create a cover entity that is not a window. # This ensures that the filtering below won't exit early because there are @@ -658,8 +665,9 @@ async def test_error_no_device_class(hass: HomeAssistant, init_components) -> No ) +@pytest.mark.usefixtures("init_components") async def test_error_no_device_class_in_area( - hass: HomeAssistant, init_components, area_registry: ar.AreaRegistry + hass: HomeAssistant, area_registry: ar.AreaRegistry ) -> None: """Test error message when no entities of a device class exist in an area.""" area_bedroom = area_registry.async_get_or_create("bedroom_id") @@ -676,7 +684,8 @@ async def test_error_no_device_class_in_area( ) -async def test_error_no_intent(hass: HomeAssistant, init_components) -> None: +@pytest.mark.usefixtures("init_components") +async def test_error_no_intent(hass: HomeAssistant) -> None: """Test response with an intent match failure.""" with patch( "homeassistant.components.conversation.default_agent.recognize_all", @@ -696,8 +705,9 @@ async def test_error_no_intent(hass: HomeAssistant, init_components) -> None: ) +@pytest.mark.usefixtures("init_components") async def test_error_duplicate_names( - hass: HomeAssistant, init_components, entity_registry: er.EntityRegistry + hass: HomeAssistant, entity_registry: er.EntityRegistry ) -> None: """Test error message when multiple devices have the same name (or alias).""" kitchen_light_1 = entity_registry.async_get_or_create("light", "demo", "1234") @@ -747,9 +757,9 @@ async def test_error_duplicate_names( ) +@pytest.mark.usefixtures("init_components") async def test_error_duplicate_names_in_area( hass: HomeAssistant, - init_components, area_registry: ar.AreaRegistry, entity_registry: er.EntityRegistry, ) -> None: @@ -805,7 +815,8 @@ async def test_error_duplicate_names_in_area( ) -async def test_error_wrong_state(hass: HomeAssistant, init_components) -> None: +@pytest.mark.usefixtures("init_components") +async def test_error_wrong_state(hass: HomeAssistant) -> None: """Test error message when no entities are in the correct state.""" assert await async_setup_component(hass, media_player.DOMAIN, {}) @@ -824,9 +835,8 @@ async def test_error_wrong_state(hass: HomeAssistant, init_components) -> None: assert result.response.speech["plain"]["speech"] == "Sorry, no device is playing" -async def test_error_feature_not_supported( - hass: HomeAssistant, init_components -) -> None: +@pytest.mark.usefixtures("init_components") +async def test_error_feature_not_supported(hass: HomeAssistant) -> None: """Test error message when no devices support a required feature.""" assert await async_setup_component(hass, media_player.DOMAIN, {}) @@ -849,7 +859,8 @@ async def test_error_feature_not_supported( ) -async def test_error_no_timer_support(hass: HomeAssistant, init_components) -> None: +@pytest.mark.usefixtures("init_components") +async def test_error_no_timer_support(hass: HomeAssistant) -> None: """Test error message when a device does not support timers (no handler is registered).""" device_id = "test_device" @@ -866,7 +877,8 @@ async def test_error_no_timer_support(hass: HomeAssistant, init_components) -> N ) -async def test_error_timer_not_found(hass: HomeAssistant, init_components) -> None: +@pytest.mark.usefixtures("init_components") +async def test_error_timer_not_found(hass: HomeAssistant) -> None: """Test error message when a timer cannot be matched.""" device_id = "test_device" @@ -888,9 +900,9 @@ async def test_error_timer_not_found(hass: HomeAssistant, init_components) -> No ) +@pytest.mark.usefixtures("init_components") async def test_error_multiple_timers_matched( hass: HomeAssistant, - init_components, area_registry: ar.AreaRegistry, device_registry: dr.DeviceRegistry, ) -> None: @@ -938,8 +950,9 @@ async def test_error_multiple_timers_matched( ) +@pytest.mark.usefixtures("init_components") async def test_no_states_matched_default_error( - hass: HomeAssistant, init_components, area_registry: ar.AreaRegistry + hass: HomeAssistant, area_registry: ar.AreaRegistry ) -> None: """Test default response when no states match and slots are missing.""" area_kitchen = area_registry.async_get_or_create("kitchen_id") @@ -966,9 +979,9 @@ async def test_no_states_matched_default_error( ) +@pytest.mark.usefixtures("init_components") async def test_empty_aliases( hass: HomeAssistant, - init_components, area_registry: ar.AreaRegistry, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, @@ -1031,7 +1044,8 @@ async def test_empty_aliases( assert floors.values[0].text_in.text == floor_1.name -async def test_all_domains_loaded(hass: HomeAssistant, init_components) -> None: +@pytest.mark.usefixtures("init_components") +async def test_all_domains_loaded(hass: HomeAssistant) -> None: """Test that sentences for all domains are always loaded.""" # light domain is not loaded @@ -1050,9 +1064,9 @@ async def test_all_domains_loaded(hass: HomeAssistant, init_components) -> None: ) +@pytest.mark.usefixtures("init_components") async def test_same_named_entities_in_different_areas( hass: HomeAssistant, - init_components, area_registry: ar.AreaRegistry, entity_registry: er.EntityRegistry, ) -> None: @@ -1147,9 +1161,9 @@ async def test_same_named_entities_in_different_areas( assert result.response.response_type == intent.IntentResponseType.QUERY_ANSWER +@pytest.mark.usefixtures("init_components") async def test_same_aliased_entities_in_different_areas( hass: HomeAssistant, - init_components, area_registry: ar.AreaRegistry, entity_registry: er.EntityRegistry, ) -> None: @@ -1238,7 +1252,8 @@ async def test_same_aliased_entities_in_different_areas( assert result.response.response_type == intent.IntentResponseType.QUERY_ANSWER -async def test_device_id_in_handler(hass: HomeAssistant, init_components) -> None: +@pytest.mark.usefixtures("init_components") +async def test_device_id_in_handler(hass: HomeAssistant) -> None: """Test that the default agent passes device_id to intent handler.""" device_id = "test_device" @@ -1270,9 +1285,8 @@ async def test_device_id_in_handler(hass: HomeAssistant, init_components) -> Non assert handler.device_id == device_id -async def test_name_wildcard_lower_priority( - hass: HomeAssistant, init_components -) -> None: +@pytest.mark.usefixtures("init_components") +async def test_name_wildcard_lower_priority(hass: HomeAssistant) -> None: """Test that the default agent does not prioritize a {name} slot when it's a wildcard.""" class OrderBeerIntentHandler(intent.IntentHandler): From fbdc06647b2594f709b08f2d46924ec8b086bd9b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 24 Jun 2024 14:05:13 +0200 Subject: [PATCH 1092/1445] Bump aiodhcpwatcher to 1.0.2 (#120311) --- homeassistant/components/dhcp/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/dhcp/manifest.json b/homeassistant/components/dhcp/manifest.json index b8abd0a9919..ff81540b0ea 100644 --- a/homeassistant/components/dhcp/manifest.json +++ b/homeassistant/components/dhcp/manifest.json @@ -14,7 +14,7 @@ ], "quality_scale": "internal", "requirements": [ - "aiodhcpwatcher==1.0.0", + "aiodhcpwatcher==1.0.2", "aiodiscover==2.1.0", "cached_ipaddress==0.3.0" ] diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index a21d89705e8..c1924ef5afb 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -1,6 +1,6 @@ # Automatically generated by gen_requirements_all.py, do not edit -aiodhcpwatcher==1.0.0 +aiodhcpwatcher==1.0.2 aiodiscover==2.1.0 aiodns==3.2.0 aiohttp-fast-url-dispatcher==0.3.0 diff --git a/requirements_all.txt b/requirements_all.txt index 45b56edecbf..5c98a020f1d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -219,7 +219,7 @@ aiobotocore==2.13.0 aiocomelit==0.9.0 # homeassistant.components.dhcp -aiodhcpwatcher==1.0.0 +aiodhcpwatcher==1.0.2 # homeassistant.components.dhcp aiodiscover==2.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8feebec2ef1..3b16aa85752 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -198,7 +198,7 @@ aiobotocore==2.13.0 aiocomelit==0.9.0 # homeassistant.components.dhcp -aiodhcpwatcher==1.0.0 +aiodhcpwatcher==1.0.2 # homeassistant.components.dhcp aiodiscover==2.1.0 From a5e6728227669af6afb0e0328b88fb6b0dba81bf Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 24 Jun 2024 14:17:52 +0200 Subject: [PATCH 1093/1445] Improve integration sensor tests (#120316) --- tests/components/integration/test_sensor.py | 37 +++++++++++++++++---- 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/tests/components/integration/test_sensor.py b/tests/components/integration/test_sensor.py index 243504cb3e0..3c8798600e9 100644 --- a/tests/components/integration/test_sensor.py +++ b/tests/components/integration/test_sensor.py @@ -314,7 +314,12 @@ async def test_trapezoidal(hass: HomeAssistant) -> None: start_time = dt_util.utcnow() with freeze_time(start_time) as freezer: # Testing a power sensor with non-monotonic intervals and values - for time, value in ((20, 10), (30, 30), (40, 5), (50, 0)): + for time, value, expected in ( + (20, 10, 1.67), + (30, 30, 5.0), + (40, 5, 7.92), + (50, 0, 8.33), + ): freezer.move_to(start_time + timedelta(minutes=time)) hass.states.async_set( entity_id, @@ -323,6 +328,8 @@ async def test_trapezoidal(hass: HomeAssistant) -> None: force_update=True, ) await hass.async_block_till_done() + state = hass.states.get("sensor.integration") + assert round(float(state.state), config["sensor"]["round"]) == expected state = hass.states.get("sensor.integration") assert state is not None @@ -353,9 +360,15 @@ async def test_left(hass: HomeAssistant) -> None: await hass.async_block_till_done() # Testing a power sensor with non-monotonic intervals and values - for time, value in ((20, 10), (30, 30), (40, 5), (50, 0)): - now = dt_util.utcnow() + timedelta(minutes=time) - with freeze_time(now): + start_time = dt_util.utcnow() + with freeze_time(start_time) as freezer: + for time, value, expected in ( + (20, 10, 0.0), + (30, 30, 1.67), + (40, 5, 6.67), + (50, 0, 7.5), + ): + freezer.move_to(start_time + timedelta(minutes=time)) hass.states.async_set( entity_id, value, @@ -363,6 +376,8 @@ async def test_left(hass: HomeAssistant) -> None: force_update=True, ) await hass.async_block_till_done() + state = hass.states.get("sensor.integration") + assert round(float(state.state), config["sensor"]["round"]) == expected state = hass.states.get("sensor.integration") assert state is not None @@ -393,9 +408,15 @@ async def test_right(hass: HomeAssistant) -> None: await hass.async_block_till_done() # Testing a power sensor with non-monotonic intervals and values - for time, value in ((20, 10), (30, 30), (40, 5), (50, 0)): - now = dt_util.utcnow() + timedelta(minutes=time) - with freeze_time(now): + start_time = dt_util.utcnow() + with freeze_time(start_time) as freezer: + for time, value, expected in ( + (20, 10, 3.33), + (30, 30, 8.33), + (40, 5, 9.17), + (50, 0, 9.17), + ): + freezer.move_to(start_time + timedelta(minutes=time)) hass.states.async_set( entity_id, value, @@ -403,6 +424,8 @@ async def test_right(hass: HomeAssistant) -> None: force_update=True, ) await hass.async_block_till_done() + state = hass.states.get("sensor.integration") + assert round(float(state.state), config["sensor"]["round"]) == expected state = hass.states.get("sensor.integration") assert state is not None From 8acb73df2e2aac8f7840ab45955f0eaee799336d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 24 Jun 2024 14:32:28 +0200 Subject: [PATCH 1094/1445] Bump aiooui to 0.1.6 (#120312) --- homeassistant/components/nmap_tracker/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nmap_tracker/manifest.json b/homeassistant/components/nmap_tracker/manifest.json index 5200f778d4c..08d9b94cf2d 100644 --- a/homeassistant/components/nmap_tracker/manifest.json +++ b/homeassistant/components/nmap_tracker/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/nmap_tracker", "iot_class": "local_polling", "loggers": ["nmap"], - "requirements": ["netmap==0.7.0.2", "getmac==0.9.4", "aiooui==0.1.5"] + "requirements": ["netmap==0.7.0.2", "getmac==0.9.4", "aiooui==0.1.6"] } diff --git a/requirements_all.txt b/requirements_all.txt index 5c98a020f1d..5da81532ac5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -318,7 +318,7 @@ aiooncue==0.3.7 aioopenexchangerates==0.4.0 # homeassistant.components.nmap_tracker -aiooui==0.1.5 +aiooui==0.1.6 # homeassistant.components.pegel_online aiopegelonline==0.0.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3b16aa85752..3635684f156 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -291,7 +291,7 @@ aiooncue==0.3.7 aioopenexchangerates==0.4.0 # homeassistant.components.nmap_tracker -aiooui==0.1.5 +aiooui==0.1.6 # homeassistant.components.pegel_online aiopegelonline==0.0.10 From 37c60d800e470bd3438daa2be66e81d22808d4ac Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 24 Jun 2024 14:32:52 +0200 Subject: [PATCH 1095/1445] Bump aionut to 4.3.3 (#120313) --- homeassistant/components/nut/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nut/manifest.json b/homeassistant/components/nut/manifest.json index 1f649a32d7f..9e968b5a349 100644 --- a/homeassistant/components/nut/manifest.json +++ b/homeassistant/components/nut/manifest.json @@ -7,6 +7,6 @@ "integration_type": "device", "iot_class": "local_polling", "loggers": ["aionut"], - "requirements": ["aionut==4.3.2"], + "requirements": ["aionut==4.3.3"], "zeroconf": ["_nut._tcp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index 5da81532ac5..1fb02e82aec 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -309,7 +309,7 @@ aionanoleaf==0.2.1 aionotion==2024.03.0 # homeassistant.components.nut -aionut==4.3.2 +aionut==4.3.3 # homeassistant.components.oncue aiooncue==0.3.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3635684f156..ede794f1cfe 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -282,7 +282,7 @@ aionanoleaf==0.2.1 aionotion==2024.03.0 # homeassistant.components.nut -aionut==4.3.2 +aionut==4.3.3 # homeassistant.components.oncue aiooncue==0.3.7 From 604561aac0f420515293d1eb59f5bd60876af2b4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 24 Jun 2024 14:33:04 +0200 Subject: [PATCH 1096/1445] Bump uiprotect to 3.3.1 (#120314) --- homeassistant/components/unifiprotect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index 817d7c9c074..ba8e6f89dd5 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -40,7 +40,7 @@ "integration_type": "hub", "iot_class": "local_push", "loggers": ["uiprotect", "unifi_discovery"], - "requirements": ["uiprotect==3.1.8", "unifi-discovery==1.1.8"], + "requirements": ["uiprotect==3.3.1", "unifi-discovery==1.1.8"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/requirements_all.txt b/requirements_all.txt index 1fb02e82aec..dfcdff788c2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2791,7 +2791,7 @@ twitchAPI==4.0.0 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==3.1.8 +uiprotect==3.3.1 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ede794f1cfe..5543c68f5e4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2171,7 +2171,7 @@ twitchAPI==4.0.0 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==3.1.8 +uiprotect==3.3.1 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 From e6cb68d199f97bd58754795246010d0c075e657f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 24 Jun 2024 14:33:16 +0200 Subject: [PATCH 1097/1445] Bump aiohttp-fast-zlib to 0.1.1 (#120315) --- homeassistant/package_constraints.txt | 2 +- pyproject.toml | 2 +- requirements.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index c1924ef5afb..3dd4fb13f7b 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -4,7 +4,7 @@ aiodhcpwatcher==1.0.2 aiodiscover==2.1.0 aiodns==3.2.0 aiohttp-fast-url-dispatcher==0.3.0 -aiohttp-fast-zlib==0.1.0 +aiohttp-fast-zlib==0.1.1 aiohttp==3.9.5 aiohttp_cors==0.7.0 aiozoneinfo==0.2.0 diff --git a/pyproject.toml b/pyproject.toml index 9f83edd7f3e..f3269ee9765 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,7 +27,7 @@ dependencies = [ "aiohttp==3.9.5", "aiohttp_cors==0.7.0", "aiohttp-fast-url-dispatcher==0.3.0", - "aiohttp-fast-zlib==0.1.0", + "aiohttp-fast-zlib==0.1.1", "aiozoneinfo==0.2.0", "astral==2.2", "async-interrupt==1.1.1", diff --git a/requirements.txt b/requirements.txt index 4c5e349d8b6..a470f12e57b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,7 +7,7 @@ aiodns==3.2.0 aiohttp==3.9.5 aiohttp_cors==0.7.0 aiohttp-fast-url-dispatcher==0.3.0 -aiohttp-fast-zlib==0.1.0 +aiohttp-fast-zlib==0.1.1 aiozoneinfo==0.2.0 astral==2.2 async-interrupt==1.1.1 From 57cdd3353736f7aada47746aa36c1297b1cbec1a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 24 Jun 2024 14:56:48 +0200 Subject: [PATCH 1098/1445] Bump aiosteamist to 1.0.0 (#120318) --- homeassistant/components/steamist/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/steamist/manifest.json b/homeassistant/components/steamist/manifest.json index 91ebc7f6a21..dcb0a50a9a9 100644 --- a/homeassistant/components/steamist/manifest.json +++ b/homeassistant/components/steamist/manifest.json @@ -16,5 +16,5 @@ "documentation": "https://www.home-assistant.io/integrations/steamist", "iot_class": "local_polling", "loggers": ["aiosteamist", "discovery30303"], - "requirements": ["aiosteamist==0.3.2", "discovery30303==0.2.1"] + "requirements": ["aiosteamist==1.0.0", "discovery30303==0.2.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index dfcdff788c2..c443edba8ba 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -374,7 +374,7 @@ aioslimproto==3.0.0 aiosolaredge==0.2.0 # homeassistant.components.steamist -aiosteamist==0.3.2 +aiosteamist==1.0.0 # homeassistant.components.switcher_kis aioswitcher==3.4.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5543c68f5e4..1c07b5c2ae2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -347,7 +347,7 @@ aioslimproto==3.0.0 aiosolaredge==0.2.0 # homeassistant.components.steamist -aiosteamist==0.3.2 +aiosteamist==1.0.0 # homeassistant.components.switcher_kis aioswitcher==3.4.3 From 389b9d1ad64f5d7a082266bb3f12c2ccae30b60a Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Mon, 24 Jun 2024 15:16:09 +0200 Subject: [PATCH 1099/1445] Make sure ACK's are processed before mqtt tests are teared down (#120329) --- tests/components/mqtt/test_binary_sensor.py | 2 ++ tests/components/mqtt/test_device_tracker.py | 2 ++ tests/components/mqtt/test_event.py | 2 ++ tests/components/mqtt/test_mixins.py | 2 +- 4 files changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/components/mqtt/test_binary_sensor.py b/tests/components/mqtt/test_binary_sensor.py index 995aadd7dba..afa9ca9970e 100644 --- a/tests/components/mqtt/test_binary_sensor.py +++ b/tests/components/mqtt/test_binary_sensor.py @@ -1173,6 +1173,8 @@ async def test_cleanup_triggers_and_restoring_state( state = hass.states.get("binary_sensor.test2") assert state.state == state2 + await hass.async_block_till_done(wait_background_tasks=True) + @pytest.mark.parametrize( "hass_config", diff --git a/tests/components/mqtt/test_device_tracker.py b/tests/components/mqtt/test_device_tracker.py index 254885919b0..76129d4c549 100644 --- a/tests/components/mqtt/test_device_tracker.py +++ b/tests/components/mqtt/test_device_tracker.py @@ -240,6 +240,8 @@ async def test_device_tracker_discovery_update( # Entity was not updated as the state was not changed assert state.last_updated == datetime(2023, 8, 22, 19, 16, tzinfo=UTC) + await hass.async_block_till_done(wait_background_tasks=True) + async def test_cleanup_device_tracker( hass: HomeAssistant, diff --git a/tests/components/mqtt/test_event.py b/tests/components/mqtt/test_event.py index 64a2003606c..fd4f8eb3e5d 100644 --- a/tests/components/mqtt/test_event.py +++ b/tests/components/mqtt/test_event.py @@ -222,6 +222,8 @@ async def test_setting_event_value_via_mqtt_json_message_and_default_current_sta assert state.attributes.get("val") == "valcontent" assert state.attributes.get("par") == "parcontent" + await hass.async_block_till_done(wait_background_tasks=True) + @pytest.mark.parametrize("hass_config", [DEFAULT_CONFIG]) async def test_availability_when_connection_lost( diff --git a/tests/components/mqtt/test_mixins.py b/tests/components/mqtt/test_mixins.py index e46f0b56c15..ae4d232ba54 100644 --- a/tests/components/mqtt/test_mixins.py +++ b/tests/components/mqtt/test_mixins.py @@ -335,7 +335,7 @@ async def test_default_entity_and_device_name( # Assert that no issues ware registered assert len(events) == 0 - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) # Assert that no issues ware registered assert len(events) == 0 From 1a27cea6f2a69ce5576fb46a7fd6e3942e07b923 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 24 Jun 2024 15:25:08 +0200 Subject: [PATCH 1100/1445] Bump bluetooth-adapters to 0.19.2 (#120324) --- homeassistant/components/bluetooth/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/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index 095eeff7f30..7239b5b3d05 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -16,7 +16,7 @@ "requirements": [ "bleak==0.22.1", "bleak-retry-connector==3.5.0", - "bluetooth-adapters==0.19.2", + "bluetooth-adapters==0.19.3", "bluetooth-auto-recovery==1.4.2", "bluetooth-data-tools==1.19.0", "dbus-fast==2.21.3", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 3dd4fb13f7b..4dc15aaf94b 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -17,7 +17,7 @@ awesomeversion==24.2.0 bcrypt==4.1.2 bleak-retry-connector==3.5.0 bleak==0.22.1 -bluetooth-adapters==0.19.2 +bluetooth-adapters==0.19.3 bluetooth-auto-recovery==1.4.2 bluetooth-data-tools==1.19.0 cached_ipaddress==0.3.0 diff --git a/requirements_all.txt b/requirements_all.txt index c443edba8ba..b518a91a788 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -594,7 +594,7 @@ bluemaestro-ble==0.2.3 # bluepy==1.3.0 # homeassistant.components.bluetooth -bluetooth-adapters==0.19.2 +bluetooth-adapters==0.19.3 # homeassistant.components.bluetooth bluetooth-auto-recovery==1.4.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1c07b5c2ae2..01718dcb96e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -509,7 +509,7 @@ bluecurrent-api==1.2.3 bluemaestro-ble==0.2.3 # homeassistant.components.bluetooth -bluetooth-adapters==0.19.2 +bluetooth-adapters==0.19.3 # homeassistant.components.bluetooth bluetooth-auto-recovery==1.4.2 From 0d1b05052035fdc0d303eafe74318ca6b4719f1f Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 24 Jun 2024 15:41:08 +0200 Subject: [PATCH 1101/1445] Remove create_create from StorageCollectionWebsocket.async_setup (#119489) --- .../components/assist_pipeline/pipeline.py | 9 ++--- .../components/image_upload/__init__.py | 18 ++++++++-- .../components/lovelace/resources.py | 9 ++--- homeassistant/helpers/collection.py | 34 ++++++++----------- 4 files changed, 34 insertions(+), 36 deletions(-) diff --git a/homeassistant/components/assist_pipeline/pipeline.py b/homeassistant/components/assist_pipeline/pipeline.py index ff360676cf7..6c1b3ced470 100644 --- a/homeassistant/components/assist_pipeline/pipeline.py +++ b/homeassistant/components/assist_pipeline/pipeline.py @@ -1605,14 +1605,9 @@ class PipelineStorageCollectionWebsocket( """Class to expose storage collection management over websocket.""" @callback - def async_setup( - self, - hass: HomeAssistant, - *, - create_create: bool = True, - ) -> None: + def async_setup(self, hass: HomeAssistant) -> None: """Set up the websocket commands.""" - super().async_setup(hass, create_create=create_create) + super().async_setup(hass) websocket_api.async_register_command( hass, diff --git a/homeassistant/components/image_upload/__init__.py b/homeassistant/components/image_upload/__init__.py index 69e2b0f12db..59b594561f0 100644 --- a/homeassistant/components/image_upload/__init__.py +++ b/homeassistant/components/image_upload/__init__.py @@ -14,6 +14,7 @@ from aiohttp.web_request import FileField from PIL import Image, ImageOps, UnidentifiedImageError import voluptuous as vol +from homeassistant.components import websocket_api from homeassistant.components.http import KEY_HASS, HomeAssistantView from homeassistant.components.http.static import CACHE_HEADERS from homeassistant.const import CONF_ID @@ -47,13 +48,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: image_dir = pathlib.Path(hass.config.path("image")) hass.data[DOMAIN] = storage_collection = ImageStorageCollection(hass, image_dir) await storage_collection.async_load() - collection.DictStorageCollectionWebsocket( + ImageUploadStorageCollectionWebsocket( storage_collection, "image", "image", CREATE_FIELDS, UPDATE_FIELDS, - ).async_setup(hass, create_create=False) + ).async_setup(hass) hass.http.register_view(ImageUploadView) hass.http.register_view(ImageServeView(image_dir, storage_collection)) @@ -151,6 +152,19 @@ class ImageStorageCollection(collection.DictStorageCollection): await self.hass.async_add_executor_job(shutil.rmtree, self.image_dir / item_id) +class ImageUploadStorageCollectionWebsocket(collection.DictStorageCollectionWebsocket): + """Class to expose storage collection management over websocket.""" + + async def ws_create_item( + self, hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict + ) -> None: + """Create an item. + + Not supported, images are uploaded via the ImageUploadView. + """ + raise NotImplementedError + + class ImageUploadView(HomeAssistantView): """View to upload images.""" diff --git a/homeassistant/components/lovelace/resources.py b/homeassistant/components/lovelace/resources.py index c25c81e2c6f..316a31e8e9d 100644 --- a/homeassistant/components/lovelace/resources.py +++ b/homeassistant/components/lovelace/resources.py @@ -133,14 +133,9 @@ class ResourceStorageCollectionWebsocket(collection.DictStorageCollectionWebsock """Class to expose storage collection management over websocket.""" @callback - def async_setup( - self, - hass: HomeAssistant, - *, - create_create: bool = True, - ) -> None: + def async_setup(self, hass: HomeAssistant) -> None: """Set up the websocket commands.""" - super().async_setup(hass, create_create=create_create) + super().async_setup(hass) # Register lovelace/resources for backwards compatibility, remove in # Home Assistant Core 2025.1 diff --git a/homeassistant/helpers/collection.py b/homeassistant/helpers/collection.py index 1dd94d85f9a..b9993098003 100644 --- a/homeassistant/helpers/collection.py +++ b/homeassistant/helpers/collection.py @@ -536,12 +536,7 @@ class StorageCollectionWebsocket[_StorageCollectionT: StorageCollection]: return f"{self.model_name}_id" @callback - def async_setup( - self, - hass: HomeAssistant, - *, - create_create: bool = True, - ) -> None: + def async_setup(self, hass: HomeAssistant) -> None: """Set up the websocket commands.""" websocket_api.async_register_command( hass, @@ -552,20 +547,19 @@ class StorageCollectionWebsocket[_StorageCollectionT: StorageCollection]: ), ) - if create_create: - websocket_api.async_register_command( - hass, - f"{self.api_prefix}/create", - websocket_api.require_admin( - websocket_api.async_response(self.ws_create_item) - ), - websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( - { - **self.create_schema, - vol.Required("type"): f"{self.api_prefix}/create", - } - ), - ) + websocket_api.async_register_command( + hass, + f"{self.api_prefix}/create", + websocket_api.require_admin( + websocket_api.async_response(self.ws_create_item) + ), + websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( + { + **self.create_schema, + vol.Required("type"): f"{self.api_prefix}/create", + } + ), + ) websocket_api.async_register_command( hass, From 2776b28bb796356468140a135a2ba52be0a55bae Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 24 Jun 2024 16:20:07 +0200 Subject: [PATCH 1102/1445] Bump govee-ble to 0.31.3 (#120335) --- homeassistant/components/govee_ble/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/govee_ble/manifest.json b/homeassistant/components/govee_ble/manifest.json index 98b802f8233..858e916d2d8 100644 --- a/homeassistant/components/govee_ble/manifest.json +++ b/homeassistant/components/govee_ble/manifest.json @@ -90,5 +90,5 @@ "dependencies": ["bluetooth_adapters"], "documentation": "https://www.home-assistant.io/integrations/govee_ble", "iot_class": "local_push", - "requirements": ["govee-ble==0.31.2"] + "requirements": ["govee-ble==0.31.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index b518a91a788..590fa2fcc29 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -995,7 +995,7 @@ goslide-api==0.5.1 gotailwind==0.2.3 # homeassistant.components.govee_ble -govee-ble==0.31.2 +govee-ble==0.31.3 # homeassistant.components.govee_light_local govee-local-api==1.5.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 01718dcb96e..5c40fb32cc7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -821,7 +821,7 @@ googlemaps==2.5.1 gotailwind==0.2.3 # homeassistant.components.govee_ble -govee-ble==0.31.2 +govee-ble==0.31.3 # homeassistant.components.govee_light_local govee-local-api==1.5.0 From 85720f9e02e8cd7bfc6779245a352d9daa2c5bba Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Mon, 24 Jun 2024 16:20:44 +0200 Subject: [PATCH 1103/1445] Fix setup and tear down issues for mqtt discovery and config flow tests (#120333) * Fix setup and tear down issues for mqtt discovery and config flow tests * Use async callback --- tests/components/mqtt/conftest.py | 12 ++- tests/components/mqtt/test_config_flow.py | 10 +- tests/components/mqtt/test_discovery.py | 112 ++++++++++++++-------- 3 files changed, 85 insertions(+), 49 deletions(-) diff --git a/tests/components/mqtt/conftest.py b/tests/components/mqtt/conftest.py index bc4fa2e6634..9649e0b9ddf 100644 --- a/tests/components/mqtt/conftest.py +++ b/tests/components/mqtt/conftest.py @@ -6,7 +6,17 @@ from unittest.mock import patch import pytest from typing_extensions import Generator -from tests.components.light.conftest import mock_light_profiles # noqa: F401 +from homeassistant.components import mqtt + +ENTRY_DEFAULT_BIRTH_MESSAGE = { + mqtt.CONF_BROKER: "mock-broker", + mqtt.CONF_BIRTH_MESSAGE: { + mqtt.ATTR_TOPIC: "homeassistant/status", + mqtt.ATTR_PAYLOAD: "online", + mqtt.ATTR_QOS: 0, + mqtt.ATTR_RETAIN: False, + }, +} @pytest.fixture(autouse=True) diff --git a/tests/components/mqtt/test_config_flow.py b/tests/components/mqtt/test_config_flow.py index 8df5de8e2fb..21ddf5ecc11 100644 --- a/tests/components/mqtt/test_config_flow.py +++ b/tests/components/mqtt/test_config_flow.py @@ -1104,7 +1104,6 @@ async def test_skipping_advanced_options( ) async def test_step_reauth( hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, mqtt_client_mock: MqttMockPahoClient, mock_try_connection: MagicMock, mock_reload_after_entry_update: MagicMock, @@ -1115,12 +1114,9 @@ async def test_step_reauth( """Test that the reauth step works.""" # Prepare the config entry - config_entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0] - hass.config_entries.async_update_entry( - config_entry, - data=test_input, - ) - await mqtt_mock_entry() + config_entry = MockConfigEntry(domain=mqtt.DOMAIN, data=test_input) + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) # Start reauth flow config_entry.async_start_reauth(hass) diff --git a/tests/components/mqtt/test_discovery.py b/tests/components/mqtt/test_discovery.py index 911d205269c..e36971e386f 100644 --- a/tests/components/mqtt/test_discovery.py +++ b/tests/components/mqtt/test_discovery.py @@ -22,7 +22,9 @@ from homeassistant.components.mqtt.discovery import ( MQTTDiscoveryPayload, async_start, ) +from homeassistant.components.mqtt.models import ReceiveMessage from homeassistant.const import ( + EVENT_HOMEASSISTANT_STARTED, EVENT_STATE_CHANGED, STATE_ON, STATE_UNAVAILABLE, @@ -40,6 +42,7 @@ from homeassistant.helpers.service_info.mqtt import MqttServiceInfo from homeassistant.setup import async_setup_component from homeassistant.util.signal_type import SignalTypeFormat +from .conftest import ENTRY_DEFAULT_BIRTH_MESSAGE from .test_common import help_all_subscribe_calls, help_test_unload_config_entry from tests.common import ( @@ -1454,32 +1457,15 @@ async def test_complex_discovery_topic_prefix( ].discovery_already_discovered +@patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.0) @patch("homeassistant.components.mqtt.client.INITIAL_SUBSCRIBE_COOLDOWN", 0.0) @patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.0) @patch("homeassistant.components.mqtt.client.UNSUBSCRIBE_COOLDOWN", 0.0) async def test_mqtt_integration_discovery_subscribe_unsubscribe( hass: HomeAssistant, mqtt_client_mock: MqttMockPahoClient, - mqtt_mock_entry: MqttMockHAClientGenerator, ) -> None: """Check MQTT integration discovery subscribe and unsubscribe.""" - mqtt_mock = await mqtt_mock_entry() - mock_platform(hass, "comp.config_flow", None) - - entry = hass.config_entries.async_entries("mqtt")[0] - mqtt_mock().connected = True - - with patch( - "homeassistant.components.mqtt.discovery.async_get_mqtt", - return_value={"comp": ["comp/discovery/#"]}, - ): - await async_start(hass, "homeassistant", entry) - await hass.async_block_till_done() - await hass.async_block_till_done() - await hass.async_block_till_done() - - assert ("comp/discovery/#", 0) in help_all_subscribe_calls(mqtt_client_mock) - assert not mqtt_client_mock.unsubscribe.called class TestFlow(config_entries.ConfigFlow): """Test flow.""" @@ -1488,49 +1474,57 @@ async def test_mqtt_integration_discovery_subscribe_unsubscribe( """Test mqtt step.""" return self.async_abort(reason="already_configured") - assert not mqtt_client_mock.unsubscribe.called + mock_platform(hass, "comp.config_flow", None) + + birth = asyncio.Event() + + @callback + def wait_birth(msg: ReceiveMessage) -> None: + """Handle birth message.""" + birth.set() wait_unsub = asyncio.Event() + @callback def _mock_unsubscribe(topics: list[str]) -> tuple[int, int]: wait_unsub.set() return (0, 0) + entry = MockConfigEntry(domain=mqtt.DOMAIN, data=ENTRY_DEFAULT_BIRTH_MESSAGE) + entry.add_to_hass(hass) with ( + patch( + "homeassistant.components.mqtt.discovery.async_get_mqtt", + return_value={"comp": ["comp/discovery/#"]}, + ), mock_config_flow("comp", TestFlow), patch.object(mqtt_client_mock, "unsubscribe", side_effect=_mock_unsubscribe), ): + assert await hass.config_entries.async_setup(entry.entry_id) + await mqtt.async_subscribe(hass, "homeassistant/status", wait_birth) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await birth.wait() + + assert ("comp/discovery/#", 0) in help_all_subscribe_calls(mqtt_client_mock) + assert not mqtt_client_mock.unsubscribe.called + mqtt_client_mock.reset_mock() + + await hass.async_block_till_done(wait_background_tasks=True) async_fire_mqtt_message(hass, "comp/discovery/bla/config", "") await wait_unsub.wait() mqtt_client_mock.unsubscribe.assert_called_once_with(["comp/discovery/#"]) + await hass.async_block_till_done(wait_background_tasks=True) +@patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.0) @patch("homeassistant.components.mqtt.client.INITIAL_SUBSCRIBE_COOLDOWN", 0.0) @patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.0) @patch("homeassistant.components.mqtt.client.UNSUBSCRIBE_COOLDOWN", 0.0) async def test_mqtt_discovery_unsubscribe_once( hass: HomeAssistant, mqtt_client_mock: MqttMockPahoClient, - mqtt_mock_entry: MqttMockHAClientGenerator, ) -> None: """Check MQTT integration discovery unsubscribe once.""" - mqtt_mock = await mqtt_mock_entry() - mock_platform(hass, "comp.config_flow", None) - - entry = hass.config_entries.async_entries("mqtt")[0] - mqtt_mock().connected = True - - with patch( - "homeassistant.components.mqtt.discovery.async_get_mqtt", - return_value={"comp": ["comp/discovery/#"]}, - ): - await async_start(hass, "homeassistant", entry) - await hass.async_block_till_done() - await hass.async_block_till_done() - await hass.async_block_till_done() - - assert ("comp/discovery/#", 0) in help_all_subscribe_calls(mqtt_client_mock) - assert not mqtt_client_mock.unsubscribe.called class TestFlow(config_entries.ConfigFlow): """Test flow.""" @@ -1540,13 +1534,49 @@ async def test_mqtt_discovery_unsubscribe_once( await asyncio.sleep(0.1) return self.async_abort(reason="already_configured") - with mock_config_flow("comp", TestFlow): + mock_platform(hass, "comp.config_flow", None) + + birth = asyncio.Event() + + @callback + def wait_birth(msg: ReceiveMessage) -> None: + """Handle birth message.""" + birth.set() + + wait_unsub = asyncio.Event() + + @callback + def _mock_unsubscribe(topics: list[str]) -> tuple[int, int]: + wait_unsub.set() + return (0, 0) + + entry = MockConfigEntry(domain=mqtt.DOMAIN, data=ENTRY_DEFAULT_BIRTH_MESSAGE) + entry.add_to_hass(hass) + + with ( + patch( + "homeassistant.components.mqtt.discovery.async_get_mqtt", + return_value={"comp": ["comp/discovery/#"]}, + ), + mock_config_flow("comp", TestFlow), + patch.object(mqtt_client_mock, "unsubscribe", side_effect=_mock_unsubscribe), + ): + assert await hass.config_entries.async_setup(entry.entry_id) + await mqtt.async_subscribe(hass, "homeassistant/status", wait_birth) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await birth.wait() + + assert ("comp/discovery/#", 0) in help_all_subscribe_calls(mqtt_client_mock) + assert not mqtt_client_mock.unsubscribe.called + + await hass.async_block_till_done(wait_background_tasks=True) async_fire_mqtt_message(hass, "comp/discovery/bla/config", "") async_fire_mqtt_message(hass, "comp/discovery/bla/config", "") - await asyncio.sleep(0.1) - await hass.async_block_till_done() - await hass.async_block_till_done() + await wait_unsub.wait() + await asyncio.sleep(0.2) + await hass.async_block_till_done(wait_background_tasks=True) mqtt_client_mock.unsubscribe.assert_called_once_with(["comp/discovery/#"]) + await hass.async_block_till_done(wait_background_tasks=True) async def test_clear_config_topic_disabled_entity( From 015bc0e172407a5779b7e9edee3e32101d8b15c1 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 24 Jun 2024 16:37:07 +0200 Subject: [PATCH 1104/1445] Use HassKey in homeassistant integration (#120332) --- homeassistant/components/homeassistant/const.py | 10 ++++++++-- .../components/homeassistant/exposed_entities.py | 16 ++++++++-------- tests/components/cloud/test_alexa_config.py | 7 +++---- tests/components/cloud/test_client.py | 3 +-- tests/components/cloud/test_google_config.py | 7 +++---- tests/components/conversation/__init__.py | 7 +++---- .../homeassistant/test_exposed_entities.py | 10 +++++----- 7 files changed, 31 insertions(+), 29 deletions(-) diff --git a/homeassistant/components/homeassistant/const.py b/homeassistant/components/homeassistant/const.py index d56ab4397d9..7a51e218a16 100644 --- a/homeassistant/components/homeassistant/const.py +++ b/homeassistant/components/homeassistant/const.py @@ -1,12 +1,18 @@ """Constants for the Homeassistant integration.""" -from typing import Final +from __future__ import annotations + +from typing import TYPE_CHECKING, Final import homeassistant.core as ha +from homeassistant.util.hass_dict import HassKey + +if TYPE_CHECKING: + from .exposed_entities import ExposedEntities DOMAIN = ha.DOMAIN -DATA_EXPOSED_ENTITIES = f"{DOMAIN}.exposed_entites" +DATA_EXPOSED_ENTITIES: HassKey[ExposedEntities] = HassKey(f"{DOMAIN}.exposed_entites") DATA_STOP_HANDLER = f"{DOMAIN}.stop_handler" SERVICE_HOMEASSISTANT_STOP: Final = "stop" diff --git a/homeassistant/components/homeassistant/exposed_entities.py b/homeassistant/components/homeassistant/exposed_entities.py index 68632223045..7bd9f9ab7bc 100644 --- a/homeassistant/components/homeassistant/exposed_entities.py +++ b/homeassistant/components/homeassistant/exposed_entities.py @@ -440,7 +440,7 @@ def ws_list_exposed_entities( """Expose an entity to an assistant.""" result: dict[str, Any] = {} - exposed_entities: ExposedEntities = hass.data[DATA_EXPOSED_ENTITIES] + exposed_entities = hass.data[DATA_EXPOSED_ENTITIES] entity_registry = er.async_get(hass) for entity_id in chain(exposed_entities.entities, entity_registry.entities): result[entity_id] = {} @@ -464,7 +464,7 @@ def ws_expose_new_entities_get( hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any] ) -> None: """Check if new entities are exposed to an assistant.""" - exposed_entities: ExposedEntities = hass.data[DATA_EXPOSED_ENTITIES] + exposed_entities = hass.data[DATA_EXPOSED_ENTITIES] expose_new = exposed_entities.async_get_expose_new_entities(msg["assistant"]) connection.send_result(msg["id"], {"expose_new": expose_new}) @@ -482,7 +482,7 @@ def ws_expose_new_entities_set( hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any] ) -> None: """Expose new entities to an assistant.""" - exposed_entities: ExposedEntities = hass.data[DATA_EXPOSED_ENTITIES] + exposed_entities = hass.data[DATA_EXPOSED_ENTITIES] exposed_entities.async_set_expose_new_entities(msg["assistant"], msg["expose_new"]) connection.send_result(msg["id"]) @@ -492,7 +492,7 @@ def async_listen_entity_updates( hass: HomeAssistant, assistant: str, listener: Callable[[], None] ) -> CALLBACK_TYPE: """Listen for updates to entity expose settings.""" - exposed_entities: ExposedEntities = hass.data[DATA_EXPOSED_ENTITIES] + exposed_entities = hass.data[DATA_EXPOSED_ENTITIES] return exposed_entities.async_listen_entity_updates(assistant, listener) @@ -501,7 +501,7 @@ def async_get_assistant_settings( hass: HomeAssistant, assistant: str ) -> dict[str, Mapping[str, Any]]: """Get all entity expose settings for an assistant.""" - exposed_entities: ExposedEntities = hass.data[DATA_EXPOSED_ENTITIES] + exposed_entities = hass.data[DATA_EXPOSED_ENTITIES] return exposed_entities.async_get_assistant_settings(assistant) @@ -510,7 +510,7 @@ def async_get_entity_settings( hass: HomeAssistant, entity_id: str ) -> dict[str, Mapping[str, Any]]: """Get assistant expose settings for an entity.""" - exposed_entities: ExposedEntities = hass.data[DATA_EXPOSED_ENTITIES] + exposed_entities = hass.data[DATA_EXPOSED_ENTITIES] return exposed_entities.async_get_entity_settings(entity_id) @@ -530,7 +530,7 @@ def async_expose_entity( @callback def async_should_expose(hass: HomeAssistant, assistant: str, entity_id: str) -> bool: """Return True if an entity should be exposed to an assistant.""" - exposed_entities: ExposedEntities = hass.data[DATA_EXPOSED_ENTITIES] + exposed_entities = hass.data[DATA_EXPOSED_ENTITIES] return exposed_entities.async_should_expose(assistant, entity_id) @@ -542,5 +542,5 @@ def async_set_assistant_option( Notify listeners if expose flag was changed. """ - exposed_entities: ExposedEntities = hass.data[DATA_EXPOSED_ENTITIES] + exposed_entities = hass.data[DATA_EXPOSED_ENTITIES] exposed_entities.async_set_assistant_option(assistant, entity_id, option, value) diff --git a/tests/components/cloud/test_alexa_config.py b/tests/components/cloud/test_alexa_config.py index a6b05198ca4..f37ee114220 100644 --- a/tests/components/cloud/test_alexa_config.py +++ b/tests/components/cloud/test_alexa_config.py @@ -15,7 +15,6 @@ from homeassistant.components.cloud.const import ( from homeassistant.components.cloud.prefs import CloudPreferences from homeassistant.components.homeassistant.exposed_entities import ( DATA_EXPOSED_ENTITIES, - ExposedEntities, async_expose_entity, async_get_entity_settings, ) @@ -39,13 +38,13 @@ def cloud_stub(): return Mock(is_logged_in=True, subscription_expired=False) -def expose_new(hass, expose_new): +def expose_new(hass: HomeAssistant, expose_new: bool) -> None: """Enable exposing new entities to Alexa.""" - exposed_entities: ExposedEntities = hass.data[DATA_EXPOSED_ENTITIES] + exposed_entities = hass.data[DATA_EXPOSED_ENTITIES] exposed_entities.async_set_expose_new_entities("cloud.alexa", expose_new) -def expose_entity(hass, entity_id, should_expose): +def expose_entity(hass: HomeAssistant, entity_id: str, should_expose: bool) -> None: """Expose an entity to Alexa.""" async_expose_entity(hass, "cloud.alexa", entity_id, should_expose) diff --git a/tests/components/cloud/test_client.py b/tests/components/cloud/test_client.py index 7c04373c261..3126d56e3fb 100644 --- a/tests/components/cloud/test_client.py +++ b/tests/components/cloud/test_client.py @@ -21,7 +21,6 @@ from homeassistant.components.cloud.const import ( ) from homeassistant.components.homeassistant.exposed_entities import ( DATA_EXPOSED_ENTITIES, - ExposedEntities, async_expose_entity, ) from homeassistant.const import CONTENT_TYPE_JSON, __version__ as HA_VERSION @@ -262,7 +261,7 @@ async def test_google_config_expose_entity( """Test Google config exposing entity method uses latest config.""" # Enable exposing new entities to Google - exposed_entities: ExposedEntities = hass.data[DATA_EXPOSED_ENTITIES] + exposed_entities = hass.data[DATA_EXPOSED_ENTITIES] exposed_entities.async_set_expose_new_entities("cloud.google_assistant", True) # Register a light entity diff --git a/tests/components/cloud/test_google_config.py b/tests/components/cloud/test_google_config.py index 66530bfa3f8..89882d92037 100644 --- a/tests/components/cloud/test_google_config.py +++ b/tests/components/cloud/test_google_config.py @@ -18,7 +18,6 @@ from homeassistant.components.cloud.prefs import CloudPreferences from homeassistant.components.google_assistant import helpers as ga_helpers from homeassistant.components.homeassistant.exposed_entities import ( DATA_EXPOSED_ENTITIES, - ExposedEntities, async_expose_entity, async_get_entity_settings, ) @@ -47,13 +46,13 @@ def mock_conf(hass, cloud_prefs): ) -def expose_new(hass, expose_new): +def expose_new(hass: HomeAssistant, expose_new: bool) -> None: """Enable exposing new entities to Google.""" - exposed_entities: ExposedEntities = hass.data[DATA_EXPOSED_ENTITIES] + exposed_entities = hass.data[DATA_EXPOSED_ENTITIES] exposed_entities.async_set_expose_new_entities("cloud.google_assistant", expose_new) -def expose_entity(hass, entity_id, should_expose): +def expose_entity(hass: HomeAssistant, entity_id: str, should_expose: bool) -> None: """Expose an entity to Google.""" async_expose_entity(hass, "cloud.google_assistant", entity_id, should_expose) diff --git a/tests/components/conversation/__init__.py b/tests/components/conversation/__init__.py index fb9bcab7498..1ae3372968e 100644 --- a/tests/components/conversation/__init__.py +++ b/tests/components/conversation/__init__.py @@ -11,7 +11,6 @@ from homeassistant.components.conversation.models import ( ) from homeassistant.components.homeassistant.exposed_entities import ( DATA_EXPOSED_ENTITIES, - ExposedEntities, async_expose_entity, ) from homeassistant.core import HomeAssistant @@ -45,12 +44,12 @@ class MockAgent(conversation.AbstractConversationAgent): ) -def expose_new(hass: HomeAssistant, expose_new: bool): +def expose_new(hass: HomeAssistant, expose_new: bool) -> None: """Enable exposing new entities to the default agent.""" - exposed_entities: ExposedEntities = hass.data[DATA_EXPOSED_ENTITIES] + exposed_entities = hass.data[DATA_EXPOSED_ENTITIES] exposed_entities.async_set_expose_new_entities(conversation.DOMAIN, expose_new) -def expose_entity(hass: HomeAssistant, entity_id: str, should_expose: bool): +def expose_entity(hass: HomeAssistant, entity_id: str, should_expose: bool) -> None: """Expose an entity to the default agent.""" async_expose_entity(hass, conversation.DOMAIN, entity_id, should_expose) diff --git a/tests/components/homeassistant/test_exposed_entities.py b/tests/components/homeassistant/test_exposed_entities.py index b3ff6594509..1f1955c2f82 100644 --- a/tests/components/homeassistant/test_exposed_entities.py +++ b/tests/components/homeassistant/test_exposed_entities.py @@ -103,7 +103,7 @@ async def test_load_preferences(hass: HomeAssistant) -> None: """Make sure that we can load/save data correctly.""" assert await async_setup_component(hass, "homeassistant", {}) - exposed_entities: ExposedEntities = hass.data[DATA_EXPOSED_ENTITIES] + exposed_entities = hass.data[DATA_EXPOSED_ENTITIES] assert exposed_entities._assistants == {} exposed_entities.async_set_expose_new_entities("test1", True) @@ -139,7 +139,7 @@ async def test_expose_entity( entry1 = entity_registry.async_get_or_create("test", "test", "unique1") entry2 = entity_registry.async_get_or_create("test", "test", "unique2") - exposed_entities: ExposedEntities = hass.data[DATA_EXPOSED_ENTITIES] + exposed_entities = hass.data[DATA_EXPOSED_ENTITIES] assert len(exposed_entities.entities) == 0 # Set options @@ -196,7 +196,7 @@ async def test_expose_entity_unknown( assert await async_setup_component(hass, "homeassistant", {}) await hass.async_block_till_done() - exposed_entities: ExposedEntities = hass.data[DATA_EXPOSED_ENTITIES] + exposed_entities = hass.data[DATA_EXPOSED_ENTITIES] assert len(exposed_entities.entities) == 0 # Set options @@ -442,7 +442,7 @@ async def test_should_expose( ) # Check with a different assistant - exposed_entities: ExposedEntities = hass.data[DATA_EXPOSED_ENTITIES] + exposed_entities = hass.data[DATA_EXPOSED_ENTITIES] exposed_entities.async_set_expose_new_entities("cloud.no_default_expose", False) assert ( async_should_expose( @@ -545,7 +545,7 @@ async def test_listeners( """Make sure we call entity listeners.""" assert await async_setup_component(hass, "homeassistant", {}) - exposed_entities: ExposedEntities = hass.data[DATA_EXPOSED_ENTITIES] + exposed_entities = hass.data[DATA_EXPOSED_ENTITIES] callbacks = [] exposed_entities.async_listen_entity_updates("test1", lambda: callbacks.append(1)) From d1de42a299148f921cbed1f99fc6d4d4281ca693 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 24 Jun 2024 16:44:59 +0200 Subject: [PATCH 1105/1445] Replace deprecated attribute in abode (#120343) --- homeassistant/components/abode/entity.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/abode/entity.py b/homeassistant/components/abode/entity.py index adbb68d86c6..70fe3a7caa4 100644 --- a/homeassistant/components/abode/entity.py +++ b/homeassistant/components/abode/entity.py @@ -105,7 +105,7 @@ class AbodeAutomation(AbodeEntity): super().__init__(data) self._automation = automation self._attr_name = automation.name - self._attr_unique_id = automation.automation_id + self._attr_unique_id = automation.id self._attr_extra_state_attributes = { "type": "CUE automation", } From e2f9ba5455712c3f9ecc8c4b0a5356b06bf2ce01 Mon Sep 17 00:00:00 2001 From: Lennard Beers Date: Mon, 24 Jun 2024 17:00:37 +0200 Subject: [PATCH 1106/1445] Bump eq3btsmart to 1.1.9 (#120339) --- homeassistant/components/eq3btsmart/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/eq3btsmart/manifest.json b/homeassistant/components/eq3btsmart/manifest.json index bf5489531bc..d308d02027d 100644 --- a/homeassistant/components/eq3btsmart/manifest.json +++ b/homeassistant/components/eq3btsmart/manifest.json @@ -23,5 +23,5 @@ "iot_class": "local_polling", "loggers": ["eq3btsmart"], "quality_scale": "silver", - "requirements": ["eq3btsmart==1.1.8", "bleak-esphome==1.0.0"] + "requirements": ["eq3btsmart==1.1.9", "bleak-esphome==1.0.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 590fa2fcc29..442a73e658b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -831,7 +831,7 @@ epion==0.0.3 epson-projector==0.5.1 # homeassistant.components.eq3btsmart -eq3btsmart==1.1.8 +eq3btsmart==1.1.9 # homeassistant.components.esphome esphome-dashboard-api==1.2.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5c40fb32cc7..e9f8fb5ca76 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -688,7 +688,7 @@ epion==0.0.3 epson-projector==0.5.1 # homeassistant.components.eq3btsmart -eq3btsmart==1.1.8 +eq3btsmart==1.1.9 # homeassistant.components.esphome esphome-dashboard-api==1.2.3 From bd6164ad4be690ba76fe14428d81a1bb94de7da7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 24 Jun 2024 17:10:57 +0200 Subject: [PATCH 1107/1445] Bump bluetooth-data-tools to 1.19.3 (#120323) --- homeassistant/components/bluetooth/manifest.json | 2 +- homeassistant/components/ld2410_ble/manifest.json | 2 +- homeassistant/components/led_ble/manifest.json | 2 +- homeassistant/components/private_ble_device/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index 7239b5b3d05..0d6116f436a 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -18,7 +18,7 @@ "bleak-retry-connector==3.5.0", "bluetooth-adapters==0.19.3", "bluetooth-auto-recovery==1.4.2", - "bluetooth-data-tools==1.19.0", + "bluetooth-data-tools==1.19.3", "dbus-fast==2.21.3", "habluetooth==3.1.1" ] diff --git a/homeassistant/components/ld2410_ble/manifest.json b/homeassistant/components/ld2410_ble/manifest.json index 8b220f78e53..2389e3199e2 100644 --- a/homeassistant/components/ld2410_ble/manifest.json +++ b/homeassistant/components/ld2410_ble/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/ld2410_ble", "integration_type": "device", "iot_class": "local_push", - "requirements": ["bluetooth-data-tools==1.19.0", "ld2410-ble==0.1.1"] + "requirements": ["bluetooth-data-tools==1.19.3", "ld2410-ble==0.1.1"] } diff --git a/homeassistant/components/led_ble/manifest.json b/homeassistant/components/led_ble/manifest.json index ee5d0431fc8..b793c64f67d 100644 --- a/homeassistant/components/led_ble/manifest.json +++ b/homeassistant/components/led_ble/manifest.json @@ -35,5 +35,5 @@ "dependencies": ["bluetooth_adapters"], "documentation": "https://www.home-assistant.io/integrations/led_ble", "iot_class": "local_polling", - "requirements": ["bluetooth-data-tools==1.19.0", "led-ble==1.0.1"] + "requirements": ["bluetooth-data-tools==1.19.3", "led-ble==1.0.1"] } diff --git a/homeassistant/components/private_ble_device/manifest.json b/homeassistant/components/private_ble_device/manifest.json index bc4ad0f2912..bb29e2cf105 100644 --- a/homeassistant/components/private_ble_device/manifest.json +++ b/homeassistant/components/private_ble_device/manifest.json @@ -6,5 +6,5 @@ "dependencies": ["bluetooth_adapters"], "documentation": "https://www.home-assistant.io/integrations/private_ble_device", "iot_class": "local_push", - "requirements": ["bluetooth-data-tools==1.19.0"] + "requirements": ["bluetooth-data-tools==1.19.3"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 4dc15aaf94b..7aa76295d4b 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -19,7 +19,7 @@ bleak-retry-connector==3.5.0 bleak==0.22.1 bluetooth-adapters==0.19.3 bluetooth-auto-recovery==1.4.2 -bluetooth-data-tools==1.19.0 +bluetooth-data-tools==1.19.3 cached_ipaddress==0.3.0 certifi>=2021.5.30 ciso8601==2.3.1 diff --git a/requirements_all.txt b/requirements_all.txt index 442a73e658b..9bc0d50edec 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -603,7 +603,7 @@ bluetooth-auto-recovery==1.4.2 # homeassistant.components.ld2410_ble # homeassistant.components.led_ble # homeassistant.components.private_ble_device -bluetooth-data-tools==1.19.0 +bluetooth-data-tools==1.19.3 # homeassistant.components.bond bond-async==0.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e9f8fb5ca76..10e011a0aa5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -518,7 +518,7 @@ bluetooth-auto-recovery==1.4.2 # homeassistant.components.ld2410_ble # homeassistant.components.led_ble # homeassistant.components.private_ble_device -bluetooth-data-tools==1.19.0 +bluetooth-data-tools==1.19.3 # homeassistant.components.bond bond-async==0.2.1 From b7bf61a8c9f63029413ab5c3104aa9e068ec9fe5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 24 Jun 2024 17:35:33 +0200 Subject: [PATCH 1108/1445] Bump habluetooth to 3.1.3 (#120337) --- homeassistant/components/bluetooth/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/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index 0d6116f436a..8883e63f286 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -20,6 +20,6 @@ "bluetooth-auto-recovery==1.4.2", "bluetooth-data-tools==1.19.3", "dbus-fast==2.21.3", - "habluetooth==3.1.1" + "habluetooth==3.1.3" ] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 7aa76295d4b..577889288a1 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -28,7 +28,7 @@ dbus-fast==2.21.3 fnv-hash-fast==0.5.0 ha-av==10.1.1 ha-ffmpeg==3.2.0 -habluetooth==3.1.1 +habluetooth==3.1.3 hass-nabucasa==0.81.1 hassil==1.7.1 home-assistant-bluetooth==1.12.1 diff --git a/requirements_all.txt b/requirements_all.txt index 9bc0d50edec..6a0aaa29ae0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1050,7 +1050,7 @@ ha-philipsjs==3.2.2 habitipy==0.3.1 # homeassistant.components.bluetooth -habluetooth==3.1.1 +habluetooth==3.1.3 # homeassistant.components.cloud hass-nabucasa==0.81.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 10e011a0aa5..4645df451cb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -867,7 +867,7 @@ ha-philipsjs==3.2.2 habitipy==0.3.1 # homeassistant.components.bluetooth -habluetooth==3.1.1 +habluetooth==3.1.3 # homeassistant.components.cloud hass-nabucasa==0.81.1 From 063a3f3bca6eae8308c6b21968710aee30ee3e1b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 24 Jun 2024 17:36:48 +0200 Subject: [PATCH 1109/1445] Bump discovery30303 to 0.3.2 (#120340) --- homeassistant/components/steamist/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/steamist/manifest.json b/homeassistant/components/steamist/manifest.json index dcb0a50a9a9..b15d7f87312 100644 --- a/homeassistant/components/steamist/manifest.json +++ b/homeassistant/components/steamist/manifest.json @@ -16,5 +16,5 @@ "documentation": "https://www.home-assistant.io/integrations/steamist", "iot_class": "local_polling", "loggers": ["aiosteamist", "discovery30303"], - "requirements": ["aiosteamist==1.0.0", "discovery30303==0.2.1"] + "requirements": ["aiosteamist==1.0.0", "discovery30303==0.3.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 6a0aaa29ae0..19e8655df0a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -741,7 +741,7 @@ directv==0.4.0 discogs-client==2.3.0 # homeassistant.components.steamist -discovery30303==0.2.1 +discovery30303==0.3.2 # homeassistant.components.dovado dovado==0.4.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4645df451cb..3ee23e7602d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -619,7 +619,7 @@ devolo-plc-api==1.4.1 directv==0.4.0 # homeassistant.components.steamist -discovery30303==0.2.1 +discovery30303==0.3.2 # homeassistant.components.dremel_3d_printer dremel3dpy==2.1.1 From eab1dc5255a2c9477a41781c9fe754a8c4993232 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 24 Jun 2024 17:38:17 +0200 Subject: [PATCH 1110/1445] Bump home-assistant-bluetooth to 1.12.2 (#120338) --- homeassistant/package_constraints.txt | 2 +- pyproject.toml | 2 +- requirements.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 577889288a1..92e8f0a08e3 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -31,7 +31,7 @@ ha-ffmpeg==3.2.0 habluetooth==3.1.3 hass-nabucasa==0.81.1 hassil==1.7.1 -home-assistant-bluetooth==1.12.1 +home-assistant-bluetooth==1.12.2 home-assistant-frontend==20240610.1 home-assistant-intents==2024.6.21 httpx==0.27.0 diff --git a/pyproject.toml b/pyproject.toml index f3269ee9765..527451eaf61 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,7 +44,7 @@ dependencies = [ # When bumping httpx, please check the version pins of # httpcore, anyio, and h11 in gen_requirements_all "httpx==0.27.0", - "home-assistant-bluetooth==1.12.1", + "home-assistant-bluetooth==1.12.2", "ifaddr==0.2.0", "Jinja2==3.1.4", "lru-dict==1.3.0", diff --git a/requirements.txt b/requirements.txt index a470f12e57b..265d6231250 100644 --- a/requirements.txt +++ b/requirements.txt @@ -20,7 +20,7 @@ ciso8601==2.3.1 fnv-hash-fast==0.5.0 hass-nabucasa==0.81.1 httpx==0.27.0 -home-assistant-bluetooth==1.12.1 +home-assistant-bluetooth==1.12.2 ifaddr==0.2.0 Jinja2==3.1.4 lru-dict==1.3.0 From 8e2665591527793db18134f34b152f1ed6064aab Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 24 Jun 2024 17:51:30 +0200 Subject: [PATCH 1111/1445] Bump led-ble to 1.0.2 (#120347) --- homeassistant/components/led_ble/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/led_ble/manifest.json b/homeassistant/components/led_ble/manifest.json index b793c64f67d..bf15ab1cc66 100644 --- a/homeassistant/components/led_ble/manifest.json +++ b/homeassistant/components/led_ble/manifest.json @@ -35,5 +35,5 @@ "dependencies": ["bluetooth_adapters"], "documentation": "https://www.home-assistant.io/integrations/led_ble", "iot_class": "local_polling", - "requirements": ["bluetooth-data-tools==1.19.3", "led-ble==1.0.1"] + "requirements": ["bluetooth-data-tools==1.19.3", "led-ble==1.0.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 19e8655df0a..061f2f2e849 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1233,7 +1233,7 @@ ld2410-ble==0.1.1 leaone-ble==0.1.0 # homeassistant.components.led_ble -led-ble==1.0.1 +led-ble==1.0.2 # homeassistant.components.foscam libpyfoscam==1.2.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3ee23e7602d..8374190865d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1008,7 +1008,7 @@ ld2410-ble==0.1.1 leaone-ble==0.1.0 # homeassistant.components.led_ble -led-ble==1.0.1 +led-ble==1.0.2 # homeassistant.components.foscam libpyfoscam==1.2.2 From 0247f9185517163bb50f04ef57eed141f227ddf3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 24 Jun 2024 18:32:32 +0200 Subject: [PATCH 1112/1445] Bump bleak to 0.22.2 (#120325) --- homeassistant/components/bluetooth/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/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index 8883e63f286..df2278399ab 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -14,7 +14,7 @@ ], "quality_scale": "internal", "requirements": [ - "bleak==0.22.1", + "bleak==0.22.2", "bleak-retry-connector==3.5.0", "bluetooth-adapters==0.19.3", "bluetooth-auto-recovery==1.4.2", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 92e8f0a08e3..438a73586c0 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -16,7 +16,7 @@ attrs==23.2.0 awesomeversion==24.2.0 bcrypt==4.1.2 bleak-retry-connector==3.5.0 -bleak==0.22.1 +bleak==0.22.2 bluetooth-adapters==0.19.3 bluetooth-auto-recovery==1.4.2 bluetooth-data-tools==1.19.3 diff --git a/requirements_all.txt b/requirements_all.txt index 061f2f2e849..5b321763d2e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -572,7 +572,7 @@ bleak-esphome==1.0.0 bleak-retry-connector==3.5.0 # homeassistant.components.bluetooth -bleak==0.22.1 +bleak==0.22.2 # homeassistant.components.blebox blebox-uniapi==2.4.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8374190865d..3d12f32d295 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -494,7 +494,7 @@ bleak-esphome==1.0.0 bleak-retry-connector==3.5.0 # homeassistant.components.bluetooth -bleak==0.22.1 +bleak==0.22.2 # homeassistant.components.blebox blebox-uniapi==2.4.2 From d073fd9b37f6cc4a6e7b1971088b8e6a422425da Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 24 Jun 2024 18:33:08 +0200 Subject: [PATCH 1113/1445] Improve integration sensor tests (#120326) --- tests/components/integration/test_sensor.py | 135 ++++++++++++++------ 1 file changed, 96 insertions(+), 39 deletions(-) diff --git a/tests/components/integration/test_sensor.py b/tests/components/integration/test_sensor.py index 3c8798600e9..03df38893a2 100644 --- a/tests/components/integration/test_sensor.py +++ b/tests/components/integration/test_sensor.py @@ -294,7 +294,36 @@ async def test_restore_state_failed(hass: HomeAssistant, extra_attributes) -> No assert state.state == STATE_UNKNOWN -async def test_trapezoidal(hass: HomeAssistant) -> None: +@pytest.mark.parametrize( + ("force_update", "sequence"), + [ + ( + False, + ( + (20, 10, 1.67), + (30, 30, 5.0), + (40, 5, 7.92), + (50, 5, 7.92), + (60, 0, 8.75), + ), + ), + ( + True, + ( + (20, 10, 1.67), + (30, 30, 5.0), + (40, 5, 7.92), + (50, 5, 8.75), + (60, 0, 9.17), + ), + ), + ], +) +async def test_trapezoidal( + hass: HomeAssistant, + sequence: tuple[tuple[float, float, float, ...]], + force_update: bool, +) -> None: """Test integration sensor state.""" config = { "sensor": { @@ -314,32 +343,51 @@ async def test_trapezoidal(hass: HomeAssistant) -> None: start_time = dt_util.utcnow() with freeze_time(start_time) as freezer: # Testing a power sensor with non-monotonic intervals and values - for time, value, expected in ( - (20, 10, 1.67), - (30, 30, 5.0), - (40, 5, 7.92), - (50, 0, 8.33), - ): + for time, value, expected in sequence: freezer.move_to(start_time + timedelta(minutes=time)) hass.states.async_set( entity_id, value, {ATTR_UNIT_OF_MEASUREMENT: UnitOfPower.KILO_WATT}, - force_update=True, + force_update=force_update, ) await hass.async_block_till_done() state = hass.states.get("sensor.integration") assert round(float(state.state), config["sensor"]["round"]) == expected - state = hass.states.get("sensor.integration") - assert state is not None - - assert round(float(state.state), config["sensor"]["round"]) == 8.33 - assert state.attributes.get("unit_of_measurement") == UnitOfEnergy.KILO_WATT_HOUR -async def test_left(hass: HomeAssistant) -> None: +@pytest.mark.parametrize( + ("force_update", "sequence"), + [ + ( + False, + ( + (20, 10, 0.0), + (30, 30, 1.67), + (40, 5, 6.67), + (50, 5, 6.67), + (60, 0, 8.33), + ), + ), + ( + True, + ( + (20, 10, 0.0), + (30, 30, 1.67), + (40, 5, 6.67), + (50, 5, 7.5), + (60, 0, 8.33), + ), + ), + ], +) +async def test_left( + hass: HomeAssistant, + sequence: tuple[tuple[float, float, float, ...]], + force_update: bool, +) -> None: """Test integration sensor state with left reimann method.""" config = { "sensor": { @@ -362,32 +410,51 @@ async def test_left(hass: HomeAssistant) -> None: # Testing a power sensor with non-monotonic intervals and values start_time = dt_util.utcnow() with freeze_time(start_time) as freezer: - for time, value, expected in ( - (20, 10, 0.0), - (30, 30, 1.67), - (40, 5, 6.67), - (50, 0, 7.5), - ): + for time, value, expected in sequence: freezer.move_to(start_time + timedelta(minutes=time)) hass.states.async_set( entity_id, value, {ATTR_UNIT_OF_MEASUREMENT: UnitOfPower.KILO_WATT}, - force_update=True, + force_update=force_update, ) await hass.async_block_till_done() state = hass.states.get("sensor.integration") assert round(float(state.state), config["sensor"]["round"]) == expected - state = hass.states.get("sensor.integration") - assert state is not None - - assert round(float(state.state), config["sensor"]["round"]) == 7.5 - assert state.attributes.get("unit_of_measurement") == UnitOfEnergy.KILO_WATT_HOUR -async def test_right(hass: HomeAssistant) -> None: +@pytest.mark.parametrize( + ("force_update", "sequence"), + [ + ( + False, + ( + (20, 10, 3.33), + (30, 30, 8.33), + (40, 5, 9.17), + (50, 5, 9.17), + (60, 0, 9.17), + ), + ), + ( + True, + ( + (20, 10, 3.33), + (30, 30, 8.33), + (40, 5, 9.17), + (50, 5, 10.0), + (60, 0, 10.0), + ), + ), + ], +) +async def test_right( + hass: HomeAssistant, + sequence: tuple[tuple[float, float, float, ...]], + force_update: bool, +) -> None: """Test integration sensor state with left reimann method.""" config = { "sensor": { @@ -410,28 +477,18 @@ async def test_right(hass: HomeAssistant) -> None: # Testing a power sensor with non-monotonic intervals and values start_time = dt_util.utcnow() with freeze_time(start_time) as freezer: - for time, value, expected in ( - (20, 10, 3.33), - (30, 30, 8.33), - (40, 5, 9.17), - (50, 0, 9.17), - ): + for time, value, expected in sequence: freezer.move_to(start_time + timedelta(minutes=time)) hass.states.async_set( entity_id, value, {ATTR_UNIT_OF_MEASUREMENT: UnitOfPower.KILO_WATT}, - force_update=True, + force_update=force_update, ) await hass.async_block_till_done() state = hass.states.get("sensor.integration") assert round(float(state.state), config["sensor"]["round"]) == expected - state = hass.states.get("sensor.integration") - assert state is not None - - assert round(float(state.state), config["sensor"]["round"]) == 9.17 - assert state.attributes.get("unit_of_measurement") == UnitOfEnergy.KILO_WATT_HOUR From 4089b808c3512e5460b3cde8044e967f0e9a964a Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 24 Jun 2024 18:33:51 +0200 Subject: [PATCH 1114/1445] Improve type hints in comfoconnect tests (#120345) --- tests/components/comfoconnect/test_sensor.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/tests/components/comfoconnect/test_sensor.py b/tests/components/comfoconnect/test_sensor.py index cea5ed0122f..91e7e1f0e25 100644 --- a/tests/components/comfoconnect/test_sensor.py +++ b/tests/components/comfoconnect/test_sensor.py @@ -1,9 +1,9 @@ """Tests for the comfoconnect sensor platform.""" -# import json -from unittest.mock import patch +from unittest.mock import MagicMock, patch import pytest +from typing_extensions import Generator from homeassistant.components.sensor import DOMAIN from homeassistant.core import HomeAssistant @@ -28,7 +28,7 @@ VALID_CONFIG = { @pytest.fixture -def mock_bridge_discover(): +def mock_bridge_discover() -> Generator[MagicMock]: """Mock the bridge discover method.""" with patch("pycomfoconnect.bridge.Bridge.discover") as mock_bridge_discover: mock_bridge_discover.return_value[0].uuid.hex.return_value = "00" @@ -36,7 +36,7 @@ def mock_bridge_discover(): @pytest.fixture -def mock_comfoconnect_command(): +def mock_comfoconnect_command() -> Generator[MagicMock]: """Mock the ComfoConnect connect method.""" with patch( "pycomfoconnect.comfoconnect.ComfoConnect._command" @@ -45,14 +45,19 @@ def mock_comfoconnect_command(): @pytest.fixture -async def setup_sensor(hass, mock_bridge_discover, mock_comfoconnect_command): +async def setup_sensor( + hass: HomeAssistant, + mock_bridge_discover: MagicMock, + mock_comfoconnect_command: MagicMock, +) -> None: """Set up demo sensor component.""" with assert_setup_component(1, DOMAIN): await async_setup_component(hass, DOMAIN, VALID_CONFIG) await hass.async_block_till_done() -async def test_sensors(hass: HomeAssistant, setup_sensor) -> None: +@pytest.mark.usefixtures("setup_sensor") +async def test_sensors(hass: HomeAssistant) -> None: """Test the sensors.""" state = hass.states.get("sensor.comfoairq_inside_humidity") assert state is not None From 8bad421a04b7568028e2633a20cfa8c7111247d0 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 24 Jun 2024 18:36:57 +0200 Subject: [PATCH 1115/1445] Improve type hints in config tests (#120346) --- tests/components/config/conftest.py | 6 +- .../test_auth_provider_homeassistant.py | 22 ++++-- tests/components/config/test_automation.py | 34 ++++---- .../components/config/test_config_entries.py | 79 +++++++++++-------- tests/components/config/test_scene.py | 23 +++--- tests/components/config/test_script.py | 25 +++--- 6 files changed, 109 insertions(+), 80 deletions(-) diff --git a/tests/components/config/conftest.py b/tests/components/config/conftest.py index ffd2f764922..c401ac19fa9 100644 --- a/tests/components/config/conftest.py +++ b/tests/components/config/conftest.py @@ -5,9 +5,11 @@ from copy import deepcopy import json import logging from os.path import basename +from typing import Any from unittest.mock import patch import pytest +from typing_extensions import Generator from homeassistant.core import HomeAssistant @@ -17,7 +19,7 @@ _LOGGER = logging.getLogger(__name__) @contextmanager -def mock_config_store(data=None): +def mock_config_store(data: dict[str, Any] | None = None) -> Generator[dict[str, Any]]: """Mock config yaml store. Data is a dict {'key': {'version': version, 'data': data}} @@ -72,7 +74,7 @@ def mock_config_store(data=None): @pytest.fixture -def hass_config_store(): +def hass_config_store() -> Generator[dict[str, Any]]: """Fixture to mock config yaml store.""" with mock_config_store() as stored_data: yield stored_data diff --git a/tests/components/config/test_auth_provider_homeassistant.py b/tests/components/config/test_auth_provider_homeassistant.py index 5c5661376e2..044d6cdb571 100644 --- a/tests/components/config/test_auth_provider_homeassistant.py +++ b/tests/components/config/test_auth_provider_homeassistant.py @@ -38,7 +38,9 @@ async def owner_access_token(hass: HomeAssistant, hass_owner_user: MockUser) -> @pytest.fixture -async def hass_admin_credential(hass, auth_provider): +async def hass_admin_credential( + hass: HomeAssistant, auth_provider: prov_ha.HassAuthProvider +): """Overload credentials to admin user.""" await hass.async_add_executor_job( auth_provider.data.add_auth, "test-user", "test-pass" @@ -284,7 +286,9 @@ async def test_delete_unknown_auth( async def test_change_password( - hass: HomeAssistant, hass_ws_client: WebSocketGenerator, auth_provider + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + auth_provider: prov_ha.HassAuthProvider, ) -> None: """Test that change password succeeds with valid password.""" client = await hass_ws_client(hass) @@ -306,7 +310,7 @@ async def test_change_password_wrong_pw( hass: HomeAssistant, hass_ws_client: WebSocketGenerator, hass_admin_user: MockUser, - auth_provider, + auth_provider: prov_ha.HassAuthProvider, ) -> None: """Test that change password fails with invalid password.""" @@ -349,7 +353,9 @@ async def test_change_password_no_creds( async def test_admin_change_password_not_owner( - hass: HomeAssistant, hass_ws_client: WebSocketGenerator, auth_provider + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + auth_provider: prov_ha.HassAuthProvider, ) -> None: """Test that change password fails when not owner.""" client = await hass_ws_client(hass) @@ -372,7 +378,7 @@ async def test_admin_change_password_not_owner( async def test_admin_change_password_no_user( - hass: HomeAssistant, hass_ws_client: WebSocketGenerator, owner_access_token + hass: HomeAssistant, hass_ws_client: WebSocketGenerator, owner_access_token: str ) -> None: """Test that change password fails with unknown user.""" client = await hass_ws_client(hass, owner_access_token) @@ -394,7 +400,7 @@ async def test_admin_change_password_no_user( async def test_admin_change_password_no_cred( hass: HomeAssistant, hass_ws_client: WebSocketGenerator, - owner_access_token, + owner_access_token: str, hass_admin_user: MockUser, ) -> None: """Test that change password fails with unknown credential.""" @@ -419,8 +425,8 @@ async def test_admin_change_password_no_cred( async def test_admin_change_password( hass: HomeAssistant, hass_ws_client: WebSocketGenerator, - owner_access_token, - auth_provider, + owner_access_token: str, + auth_provider: prov_ha.HassAuthProvider, hass_admin_user: MockUser, ) -> None: """Test that owners can change any password.""" diff --git a/tests/components/config/test_automation.py b/tests/components/config/test_automation.py index 9d9ee5d5649..f907732109d 100644 --- a/tests/components/config/test_automation.py +++ b/tests/components/config/test_automation.py @@ -26,7 +26,7 @@ def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None: @pytest.fixture async def setup_automation( hass: HomeAssistant, - automation_config, + automation_config: dict[str, Any], stub_blueprint_populate: None, ) -> None: """Set up automation integration.""" @@ -36,11 +36,11 @@ async def setup_automation( @pytest.mark.parametrize("automation_config", [{}]) +@pytest.mark.usefixtures("setup_automation") async def test_get_automation_config( hass: HomeAssistant, hass_client: ClientSessionGenerator, - hass_config_store, - setup_automation, + hass_config_store: dict[str, Any], ) -> None: """Test getting automation config.""" with patch.object(config, "SECTIONS", [automation]): @@ -59,11 +59,11 @@ async def test_get_automation_config( @pytest.mark.parametrize("automation_config", [{}]) +@pytest.mark.usefixtures("setup_automation") async def test_update_automation_config( hass: HomeAssistant, hass_client: ClientSessionGenerator, - hass_config_store, - setup_automation, + hass_config_store: dict[str, Any], ) -> None: """Test updating automation config.""" with patch.object(config, "SECTIONS", [automation]): @@ -143,11 +143,11 @@ async def test_update_automation_config( ), ], ) +@pytest.mark.usefixtures("setup_automation") async def test_update_automation_config_with_error( hass: HomeAssistant, hass_client: ClientSessionGenerator, - hass_config_store, - setup_automation, + hass_config_store: dict[str, Any], caplog: pytest.LogCaptureFixture, updated_config: Any, validation_error: str, @@ -196,11 +196,11 @@ async def test_update_automation_config_with_error( ), ], ) +@pytest.mark.usefixtures("setup_automation") async def test_update_automation_config_with_blueprint_substitution_error( hass: HomeAssistant, hass_client: ClientSessionGenerator, - hass_config_store, - setup_automation, + hass_config_store: dict[str, Any], caplog: pytest.LogCaptureFixture, updated_config: Any, validation_error: str, @@ -235,11 +235,11 @@ async def test_update_automation_config_with_blueprint_substitution_error( @pytest.mark.parametrize("automation_config", [{}]) +@pytest.mark.usefixtures("setup_automation") async def test_update_remove_key_automation_config( hass: HomeAssistant, hass_client: ClientSessionGenerator, - hass_config_store, - setup_automation, + hass_config_store: dict[str, Any], ) -> None: """Test updating automation config while removing a key.""" with patch.object(config, "SECTIONS", [automation]): @@ -272,11 +272,11 @@ async def test_update_remove_key_automation_config( @pytest.mark.parametrize("automation_config", [{}]) +@pytest.mark.usefixtures("setup_automation") async def test_bad_formatted_automations( hass: HomeAssistant, hass_client: ClientSessionGenerator, - hass_config_store, - setup_automation, + hass_config_store: dict[str, Any], ) -> None: """Test that we handle automations without ID.""" with patch.object(config, "SECTIONS", [automation]): @@ -332,12 +332,12 @@ async def test_bad_formatted_automations( ], ], ) +@pytest.mark.usefixtures("setup_automation") async def test_delete_automation( hass: HomeAssistant, hass_client: ClientSessionGenerator, entity_registry: er.EntityRegistry, - hass_config_store, - setup_automation, + hass_config_store: dict[str, Any], ) -> None: """Test deleting an automation.""" @@ -373,12 +373,12 @@ async def test_delete_automation( @pytest.mark.parametrize("automation_config", [{}]) +@pytest.mark.usefixtures("setup_automation") async def test_api_calls_require_admin( hass: HomeAssistant, hass_client: ClientSessionGenerator, hass_read_only_access_token: str, - hass_config_store, - setup_automation, + hass_config_store: dict[str, Any], ) -> None: """Test cloud APIs endpoints do not work as a normal user.""" with patch.object(config, "SECTIONS", [automation]): diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py index 95ff87c2beb..e023a60f215 100644 --- a/tests/components/config/test_config_entries.py +++ b/tests/components/config/test_config_entries.py @@ -6,6 +6,7 @@ from unittest.mock import ANY, AsyncMock, patch from aiohttp.test_utils import TestClient import pytest +from typing_extensions import Generator import voluptuous as vol from homeassistant import config_entries as core_ce, data_entry_flow, loader @@ -30,14 +31,14 @@ from tests.typing import ClientSessionGenerator, WebSocketGenerator @pytest.fixture -def clear_handlers(): +def clear_handlers() -> Generator[None]: """Clear config entry handlers.""" with patch.dict(HANDLERS, clear=True): yield @pytest.fixture(autouse=True) -def mock_test_component(hass): +def mock_test_component(hass: HomeAssistant) -> None: """Ensure a component called 'test' exists.""" mock_integration(hass, MockModule("test")) @@ -53,7 +54,7 @@ async def client( @pytest.fixture -async def mock_flow(): +def mock_flow() -> Generator[None]: """Mock a config flow.""" class Comp1ConfigFlow(ConfigFlow): @@ -68,9 +69,8 @@ async def mock_flow(): yield -async def test_get_entries( - hass: HomeAssistant, client, clear_handlers, mock_flow -) -> None: +@pytest.mark.usefixtures("clear_handlers", "mock_flow") +async def test_get_entries(hass: HomeAssistant, client: TestClient) -> None: """Test get entries.""" mock_integration(hass, MockModule("comp1")) mock_integration( @@ -238,7 +238,7 @@ async def test_get_entries( assert data[0]["domain"] == "comp5" -async def test_remove_entry(hass: HomeAssistant, client) -> None: +async def test_remove_entry(hass: HomeAssistant, client: TestClient) -> None: """Test removing an entry via the API.""" entry = MockConfigEntry( domain="kitchen_sink", state=core_ce.ConfigEntryState.LOADED @@ -251,7 +251,7 @@ async def test_remove_entry(hass: HomeAssistant, client) -> None: assert len(hass.config_entries.async_entries()) == 0 -async def test_reload_entry(hass: HomeAssistant, client) -> None: +async def test_reload_entry(hass: HomeAssistant, client: TestClient) -> None: """Test reloading an entry via the API.""" entry = MockConfigEntry( domain="kitchen_sink", state=core_ce.ConfigEntryState.LOADED @@ -267,14 +267,14 @@ async def test_reload_entry(hass: HomeAssistant, client) -> None: assert len(hass.config_entries.async_entries()) == 1 -async def test_reload_invalid_entry(hass: HomeAssistant, client) -> None: +async def test_reload_invalid_entry(hass: HomeAssistant, client: TestClient) -> None: """Test reloading an invalid entry via the API.""" resp = await client.post("/api/config/config_entries/entry/invalid/reload") assert resp.status == HTTPStatus.NOT_FOUND async def test_remove_entry_unauth( - hass: HomeAssistant, client, hass_admin_user: MockUser + hass: HomeAssistant, client: TestClient, hass_admin_user: MockUser ) -> None: """Test removing an entry via the API.""" hass_admin_user.groups = [] @@ -286,7 +286,7 @@ async def test_remove_entry_unauth( async def test_reload_entry_unauth( - hass: HomeAssistant, client, hass_admin_user: MockUser + hass: HomeAssistant, client: TestClient, hass_admin_user: MockUser ) -> None: """Test reloading an entry via the API.""" hass_admin_user.groups = [] @@ -300,7 +300,7 @@ async def test_reload_entry_unauth( async def test_reload_entry_in_failed_state( - hass: HomeAssistant, client, hass_admin_user: MockUser + hass: HomeAssistant, client: TestClient, hass_admin_user: MockUser ) -> None: """Test reloading an entry via the API that has already failed to unload.""" entry = MockConfigEntry(domain="demo", state=core_ce.ConfigEntryState.FAILED_UNLOAD) @@ -314,7 +314,7 @@ async def test_reload_entry_in_failed_state( async def test_reload_entry_in_setup_retry( - hass: HomeAssistant, client, hass_admin_user: MockUser + hass: HomeAssistant, client: TestClient, hass_admin_user: MockUser ) -> None: """Test reloading an entry via the API that is in setup retry.""" mock_setup_entry = AsyncMock(return_value=True) @@ -356,7 +356,7 @@ async def test_reload_entry_in_setup_retry( ], ) async def test_available_flows( - hass: HomeAssistant, client, type_filter, result + hass: HomeAssistant, client: TestClient, type_filter: str | None, result: set[str] ) -> None: """Test querying the available flows.""" with patch.object( @@ -378,7 +378,7 @@ async def test_available_flows( ############################ -async def test_initialize_flow(hass: HomeAssistant, client) -> None: +async def test_initialize_flow(hass: HomeAssistant, client: TestClient) -> None: """Test we can initialize a flow.""" mock_platform(hass, "test.config_flow", None) @@ -427,7 +427,9 @@ async def test_initialize_flow(hass: HomeAssistant, client) -> None: } -async def test_initialize_flow_unmet_dependency(hass: HomeAssistant, client) -> None: +async def test_initialize_flow_unmet_dependency( + hass: HomeAssistant, client: TestClient +) -> None: """Test unmet dependencies are listed.""" mock_platform(hass, "test.config_flow", None) @@ -457,7 +459,7 @@ async def test_initialize_flow_unmet_dependency(hass: HomeAssistant, client) -> async def test_initialize_flow_unauth( - hass: HomeAssistant, client, hass_admin_user: MockUser + hass: HomeAssistant, client: TestClient, hass_admin_user: MockUser ) -> None: """Test we can initialize a flow.""" hass_admin_user.groups = [] @@ -483,7 +485,7 @@ async def test_initialize_flow_unauth( assert resp.status == HTTPStatus.UNAUTHORIZED -async def test_abort(hass: HomeAssistant, client) -> None: +async def test_abort(hass: HomeAssistant, client: TestClient) -> None: """Test a flow that aborts.""" mock_platform(hass, "test.config_flow", None) @@ -508,7 +510,7 @@ async def test_abort(hass: HomeAssistant, client) -> None: @pytest.mark.usefixtures("enable_custom_integrations") -async def test_create_account(hass: HomeAssistant, client) -> None: +async def test_create_account(hass: HomeAssistant, client: TestClient) -> None: """Test a flow that creates an account.""" mock_platform(hass, "test.config_flow", None) @@ -566,7 +568,7 @@ async def test_create_account(hass: HomeAssistant, client) -> None: @pytest.mark.usefixtures("enable_custom_integrations") -async def test_two_step_flow(hass: HomeAssistant, client) -> None: +async def test_two_step_flow(hass: HomeAssistant, client: TestClient) -> None: """Test we can finish a two step flow.""" mock_integration( hass, MockModule("test", async_setup_entry=AsyncMock(return_value=True)) @@ -646,7 +648,7 @@ async def test_two_step_flow(hass: HomeAssistant, client) -> None: async def test_continue_flow_unauth( - hass: HomeAssistant, client, hass_admin_user: MockUser + hass: HomeAssistant, client: TestClient, hass_admin_user: MockUser ) -> None: """Test we can't finish a two step flow.""" mock_integration( @@ -745,7 +747,7 @@ async def test_get_progress_index_unauth( assert response["error"]["code"] == "unauthorized" -async def test_get_progress_flow(hass: HomeAssistant, client) -> None: +async def test_get_progress_flow(hass: HomeAssistant, client: TestClient) -> None: """Test we can query the API for same result as we get from init a flow.""" mock_platform(hass, "test.config_flow", None) @@ -780,7 +782,7 @@ async def test_get_progress_flow(hass: HomeAssistant, client) -> None: async def test_get_progress_flow_unauth( - hass: HomeAssistant, client, hass_admin_user: MockUser + hass: HomeAssistant, client: TestClient, hass_admin_user: MockUser ) -> None: """Test we can can't query the API for result of flow.""" mock_platform(hass, "test.config_flow", None) @@ -814,7 +816,7 @@ async def test_get_progress_flow_unauth( assert resp2.status == HTTPStatus.UNAUTHORIZED -async def test_options_flow(hass: HomeAssistant, client) -> None: +async def test_options_flow(hass: HomeAssistant, client: TestClient) -> None: """Test we can change options.""" class TestFlow(core_ce.ConfigFlow): @@ -874,7 +876,11 @@ async def test_options_flow(hass: HomeAssistant, client) -> None: ], ) async def test_options_flow_unauth( - hass: HomeAssistant, client, hass_admin_user: MockUser, endpoint: str, method: str + hass: HomeAssistant, + client: TestClient, + hass_admin_user: MockUser, + endpoint: str, + method: str, ) -> None: """Test unauthorized on options flow.""" @@ -911,7 +917,7 @@ async def test_options_flow_unauth( assert resp.status == HTTPStatus.UNAUTHORIZED -async def test_two_step_options_flow(hass: HomeAssistant, client) -> None: +async def test_two_step_options_flow(hass: HomeAssistant, client: TestClient) -> None: """Test we can finish a two step options flow.""" mock_integration( hass, MockModule("test", async_setup_entry=AsyncMock(return_value=True)) @@ -977,7 +983,9 @@ async def test_two_step_options_flow(hass: HomeAssistant, client) -> None: } -async def test_options_flow_with_invalid_data(hass: HomeAssistant, client) -> None: +async def test_options_flow_with_invalid_data( + hass: HomeAssistant, client: TestClient +) -> None: """Test an options flow with invalid_data.""" mock_integration( hass, MockModule("test", async_setup_entry=AsyncMock(return_value=True)) @@ -1358,8 +1366,9 @@ async def test_ignore_flow_nonexisting( assert response["error"]["code"] == "not_found" +@pytest.mark.usefixtures("clear_handlers") async def test_get_matching_entries_ws( - hass: HomeAssistant, hass_ws_client: WebSocketGenerator, clear_handlers + hass: HomeAssistant, hass_ws_client: WebSocketGenerator ) -> None: """Test get entries with the websocket api.""" assert await async_setup_component(hass, "config", {}) @@ -1748,8 +1757,9 @@ async def test_get_matching_entries_ws( assert response["success"] is False +@pytest.mark.usefixtures("clear_handlers") async def test_subscribe_entries_ws( - hass: HomeAssistant, hass_ws_client: WebSocketGenerator, clear_handlers + hass: HomeAssistant, hass_ws_client: WebSocketGenerator ) -> None: """Test subscribe entries with the websocket api.""" assert await async_setup_component(hass, "config", {}) @@ -1934,8 +1944,9 @@ async def test_subscribe_entries_ws( ] +@pytest.mark.usefixtures("clear_handlers") async def test_subscribe_entries_ws_filtered( - hass: HomeAssistant, hass_ws_client: WebSocketGenerator, clear_handlers + hass: HomeAssistant, hass_ws_client: WebSocketGenerator ) -> None: """Test subscribe entries with the websocket api with a type filter.""" assert await async_setup_component(hass, "config", {}) @@ -2139,7 +2150,9 @@ async def test_subscribe_entries_ws_filtered( ] -async def test_flow_with_multiple_schema_errors(hass: HomeAssistant, client) -> None: +async def test_flow_with_multiple_schema_errors( + hass: HomeAssistant, client: TestClient +) -> None: """Test an config flow with multiple schema errors.""" mock_integration( hass, MockModule("test", async_setup_entry=AsyncMock(return_value=True)) @@ -2182,7 +2195,7 @@ async def test_flow_with_multiple_schema_errors(hass: HomeAssistant, client) -> async def test_flow_with_multiple_schema_errors_base( - hass: HomeAssistant, client + hass: HomeAssistant, client: TestClient ) -> None: """Test an config flow with multiple schema errors where fields are not in the schema.""" mock_integration( @@ -2226,7 +2239,7 @@ async def test_flow_with_multiple_schema_errors_base( @pytest.mark.usefixtures("enable_custom_integrations") -async def test_supports_reconfigure(hass: HomeAssistant, client) -> None: +async def test_supports_reconfigure(hass: HomeAssistant, client: TestClient) -> None: """Test a flow that support reconfigure step.""" mock_platform(hass, "test.config_flow", None) diff --git a/tests/components/config/test_scene.py b/tests/components/config/test_scene.py index 6ca42e7f56d..22bcfa345a2 100644 --- a/tests/components/config/test_scene.py +++ b/tests/components/config/test_scene.py @@ -2,6 +2,7 @@ from http import HTTPStatus import json +from typing import Any from unittest.mock import ANY, patch import pytest @@ -16,18 +17,18 @@ from tests.typing import ClientSessionGenerator @pytest.fixture -async def setup_scene(hass, scene_config): +async def setup_scene(hass: HomeAssistant, scene_config: dict[str, Any]) -> None: """Set up scene integration.""" assert await async_setup_component(hass, "scene", {"scene": scene_config}) await hass.async_block_till_done() @pytest.mark.parametrize("scene_config", [{}]) +@pytest.mark.usefixtures("setup_scene") async def test_create_scene( hass: HomeAssistant, hass_client: ClientSessionGenerator, - hass_config_store, - setup_scene, + hass_config_store: dict[str, Any], ) -> None: """Test creating a scene.""" with patch.object(config, "SECTIONS", [scene]): @@ -70,11 +71,11 @@ async def test_create_scene( @pytest.mark.parametrize("scene_config", [{}]) +@pytest.mark.usefixtures("setup_scene") async def test_update_scene( hass: HomeAssistant, hass_client: ClientSessionGenerator, - hass_config_store, - setup_scene, + hass_config_store: dict[str, Any], ) -> None: """Test updating a scene.""" with patch.object(config, "SECTIONS", [scene]): @@ -118,11 +119,11 @@ async def test_update_scene( @pytest.mark.parametrize("scene_config", [{}]) +@pytest.mark.usefixtures("setup_scene") async def test_bad_formatted_scene( hass: HomeAssistant, hass_client: ClientSessionGenerator, - hass_config_store, - setup_scene, + hass_config_store: dict[str, Any], ) -> None: """Test that we handle scene without ID.""" with patch.object(config, "SECTIONS", [scene]): @@ -184,12 +185,12 @@ async def test_bad_formatted_scene( ], ], ) +@pytest.mark.usefixtures("setup_scene") async def test_delete_scene( hass: HomeAssistant, hass_client: ClientSessionGenerator, entity_registry: er.EntityRegistry, - hass_config_store, - setup_scene, + hass_config_store: dict[str, Any], ) -> None: """Test deleting a scene.""" @@ -227,12 +228,12 @@ async def test_delete_scene( @pytest.mark.parametrize("scene_config", [{}]) +@pytest.mark.usefixtures("setup_scene") async def test_api_calls_require_admin( hass: HomeAssistant, hass_client: ClientSessionGenerator, hass_read_only_access_token: str, - hass_config_store, - setup_scene, + hass_config_store: dict[str, Any], ) -> None: """Test scene APIs endpoints do not work as a normal user.""" with patch.object(config, "SECTIONS", [scene]): diff --git a/tests/components/config/test_script.py b/tests/components/config/test_script.py index 3ee45aec26a..4771576ed6e 100644 --- a/tests/components/config/test_script.py +++ b/tests/components/config/test_script.py @@ -31,7 +31,9 @@ async def setup_script(hass: HomeAssistant, script_config: dict[str, Any]) -> No @pytest.mark.parametrize("script_config", [{}]) async def test_get_script_config( - hass: HomeAssistant, hass_client: ClientSessionGenerator, hass_config_store + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + hass_config_store: dict[str, Any], ) -> None: """Test getting script config.""" with patch.object(config, "SECTIONS", [script]): @@ -54,7 +56,9 @@ async def test_get_script_config( @pytest.mark.parametrize("script_config", [{}]) async def test_update_script_config( - hass: HomeAssistant, hass_client: ClientSessionGenerator, hass_config_store + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + hass_config_store: dict[str, Any], ) -> None: """Test updating script config.""" with patch.object(config, "SECTIONS", [script]): @@ -90,7 +94,9 @@ async def test_update_script_config( @pytest.mark.parametrize("script_config", [{}]) async def test_invalid_object_id( - hass: HomeAssistant, hass_client: ClientSessionGenerator, hass_config_store + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + hass_config_store: dict[str, Any], ) -> None: """Test creating a script with an invalid object_id.""" with patch.object(config, "SECTIONS", [script]): @@ -152,7 +158,7 @@ async def test_invalid_object_id( async def test_update_script_config_with_error( hass: HomeAssistant, hass_client: ClientSessionGenerator, - hass_config_store, + hass_config_store: dict[str, Any], caplog: pytest.LogCaptureFixture, updated_config: Any, validation_error: str, @@ -202,8 +208,7 @@ async def test_update_script_config_with_error( async def test_update_script_config_with_blueprint_substitution_error( hass: HomeAssistant, hass_client: ClientSessionGenerator, - hass_config_store, - # setup_automation, + hass_config_store: dict[str, Any], caplog: pytest.LogCaptureFixture, updated_config: Any, validation_error: str, @@ -239,7 +244,9 @@ async def test_update_script_config_with_blueprint_substitution_error( @pytest.mark.parametrize("script_config", [{}]) async def test_update_remove_key_script_config( - hass: HomeAssistant, hass_client: ClientSessionGenerator, hass_config_store + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + hass_config_store: dict[str, Any], ) -> None: """Test updating script config while removing a key.""" with patch.object(config, "SECTIONS", [script]): @@ -286,7 +293,7 @@ async def test_delete_script( hass: HomeAssistant, hass_client: ClientSessionGenerator, entity_registry: er.EntityRegistry, - hass_config_store, + hass_config_store: dict[str, Any], ) -> None: """Test deleting a script.""" with patch.object(config, "SECTIONS", [script]): @@ -325,7 +332,7 @@ async def test_api_calls_require_admin( hass: HomeAssistant, hass_client: ClientSessionGenerator, hass_read_only_access_token: str, - hass_config_store, + hass_config_store: dict[str, Any], ) -> None: """Test script APIs endpoints do not work as a normal user.""" with patch.object(config, "SECTIONS", [script]): From dd379a9a0a21d9a850628ed349d12fe2a48d63a0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 24 Jun 2024 18:39:38 +0200 Subject: [PATCH 1116/1445] Bump aiozoneinfo to 0.2.1 (#120319) --- homeassistant/package_constraints.txt | 2 +- pyproject.toml | 2 +- requirements.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 438a73586c0..e8e0638beac 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -7,7 +7,7 @@ aiohttp-fast-url-dispatcher==0.3.0 aiohttp-fast-zlib==0.1.1 aiohttp==3.9.5 aiohttp_cors==0.7.0 -aiozoneinfo==0.2.0 +aiozoneinfo==0.2.1 astral==2.2 async-interrupt==1.1.1 async-upnp-client==0.39.0 diff --git a/pyproject.toml b/pyproject.toml index 527451eaf61..e6847385e0a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,7 +28,7 @@ dependencies = [ "aiohttp_cors==0.7.0", "aiohttp-fast-url-dispatcher==0.3.0", "aiohttp-fast-zlib==0.1.1", - "aiozoneinfo==0.2.0", + "aiozoneinfo==0.2.1", "astral==2.2", "async-interrupt==1.1.1", "attrs==23.2.0", diff --git a/requirements.txt b/requirements.txt index 265d6231250..db1137875aa 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,7 +8,7 @@ aiohttp==3.9.5 aiohttp_cors==0.7.0 aiohttp-fast-url-dispatcher==0.3.0 aiohttp-fast-zlib==0.1.1 -aiozoneinfo==0.2.0 +aiozoneinfo==0.2.1 astral==2.2 async-interrupt==1.1.1 attrs==23.2.0 From 1e3ee8419f390c0d372b201362efb5bace98a690 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 24 Jun 2024 18:41:42 +0200 Subject: [PATCH 1117/1445] Bump async-interrupt to 1.1.2 (#120321) --- homeassistant/package_constraints.txt | 2 +- pyproject.toml | 2 +- requirements.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index e8e0638beac..1f1811eca4c 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -9,7 +9,7 @@ aiohttp==3.9.5 aiohttp_cors==0.7.0 aiozoneinfo==0.2.1 astral==2.2 -async-interrupt==1.1.1 +async-interrupt==1.1.2 async-upnp-client==0.39.0 atomicwrites-homeassistant==1.4.1 attrs==23.2.0 diff --git a/pyproject.toml b/pyproject.toml index e6847385e0a..d7fbe67edba 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,7 +30,7 @@ dependencies = [ "aiohttp-fast-zlib==0.1.1", "aiozoneinfo==0.2.1", "astral==2.2", - "async-interrupt==1.1.1", + "async-interrupt==1.1.2", "attrs==23.2.0", "atomicwrites-homeassistant==1.4.1", "awesomeversion==24.2.0", diff --git a/requirements.txt b/requirements.txt index db1137875aa..cff85c2478f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,7 +10,7 @@ aiohttp-fast-url-dispatcher==0.3.0 aiohttp-fast-zlib==0.1.1 aiozoneinfo==0.2.1 astral==2.2 -async-interrupt==1.1.1 +async-interrupt==1.1.2 attrs==23.2.0 atomicwrites-homeassistant==1.4.1 awesomeversion==24.2.0 From 641507a45ac77085402127e486ab8f44c672d896 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Mon, 24 Jun 2024 18:51:19 +0200 Subject: [PATCH 1118/1445] Add change username endpoint (#109057) --- homeassistant/auth/__init__.py | 7 + homeassistant/auth/auth_store.py | 8 + homeassistant/auth/providers/homeassistant.py | 92 +++++++++- homeassistant/components/auth/strings.json | 8 + .../config/auth_provider_homeassistant.py | 42 +++++ script/hassfest/translations.py | 14 +- tests/auth/providers/test_homeassistant.py | 147 +++++++++++---- .../test_auth_provider_homeassistant.py | 167 ++++++++++++++++++ 8 files changed, 440 insertions(+), 45 deletions(-) diff --git a/homeassistant/auth/__init__.py b/homeassistant/auth/__init__.py index c39657b6147..8c991d3f227 100644 --- a/homeassistant/auth/__init__.py +++ b/homeassistant/auth/__init__.py @@ -374,6 +374,13 @@ class AuthManager: self.hass.bus.async_fire(EVENT_USER_UPDATED, {"user_id": user.id}) + @callback + def async_update_user_credentials_data( + self, credentials: models.Credentials, data: dict[str, Any] + ) -> None: + """Update credentials data.""" + self._store.async_update_user_credentials_data(credentials, data=data) + async def async_activate_user(self, user: models.User) -> None: """Activate a user.""" await self._store.async_activate_user(user) diff --git a/homeassistant/auth/auth_store.py b/homeassistant/auth/auth_store.py index 3bf025c058c..7843cb58df2 100644 --- a/homeassistant/auth/auth_store.py +++ b/homeassistant/auth/auth_store.py @@ -296,6 +296,14 @@ class AuthStore: refresh_token.expire_at = None self._async_schedule_save() + @callback + def async_update_user_credentials_data( + self, credentials: models.Credentials, data: dict[str, Any] + ) -> None: + """Update credentials data.""" + credentials.data = data + self._async_schedule_save() + async def async_load(self) -> None: # noqa: C901 """Load the users.""" if self._loaded: diff --git a/homeassistant/auth/providers/homeassistant.py b/homeassistant/auth/providers/homeassistant.py index d277ce96fe2..1ed2f1dd3f7 100644 --- a/homeassistant/auth/providers/homeassistant.py +++ b/homeassistant/auth/providers/homeassistant.py @@ -55,6 +55,27 @@ class InvalidUser(HomeAssistantError): """ +class InvalidUsername(InvalidUser): + """Raised when invalid username is specified. + + Will not be raised when validating authentication. + """ + + def __init__( + self, + *args: object, + translation_key: str | None = None, + translation_placeholders: dict[str, str] | None = None, + ) -> None: + """Initialize exception.""" + super().__init__( + *args, + translation_domain="auth", + translation_key=translation_key, + translation_placeholders=translation_placeholders, + ) + + class Data: """Hold the user data.""" @@ -71,9 +92,11 @@ class Data: self.is_legacy = False @callback - def normalize_username(self, username: str) -> str: + def normalize_username( + self, username: str, *, force_normalize: bool = False + ) -> str: """Normalize a username based on the mode.""" - if self.is_legacy: + if self.is_legacy and not force_normalize: return username return username.strip().casefold() @@ -162,13 +185,11 @@ class Data: return hashed def add_auth(self, username: str, password: str) -> None: - """Add a new authenticated user/pass.""" - username = self.normalize_username(username) + """Add a new authenticated user/pass. - if any( - self.normalize_username(user["username"]) == username for user in self.users - ): - raise InvalidUser + Raises InvalidUsername if the new username is invalid. + """ + self._validate_new_username(username) self.users.append( { @@ -207,6 +228,45 @@ class Data: else: raise InvalidUser + def _validate_new_username(self, new_username: str) -> None: + """Validate that username is normalized and unique. + + Raises InvalidUsername if the new username is invalid. + """ + normalized_username = self.normalize_username( + new_username, force_normalize=True + ) + if normalized_username != new_username: + raise InvalidUsername( + translation_key="username_not_normalized", + translation_placeholders={"new_username": new_username}, + ) + + if any( + self.normalize_username(user["username"]) == normalized_username + for user in self.users + ): + raise InvalidUsername( + translation_key="username_already_exists", + translation_placeholders={"username": new_username}, + ) + + def change_username(self, username: str, new_username: str) -> None: + """Update the username. + + Raises InvalidUser if user cannot be found. + Raises InvalidUsername if the new username is invalid. + """ + username = self.normalize_username(username) + self._validate_new_username(new_username) + + for user in self.users: + if self.normalize_username(user["username"]) == username: + user["username"] = new_username + break + else: + raise InvalidUser + async def async_save(self) -> None: """Save data.""" if self._data is not None: @@ -278,6 +338,22 @@ class HassAuthProvider(AuthProvider): ) await self.data.async_save() + async def async_change_username( + self, credential: Credentials, new_username: str + ) -> None: + """Validate new username and change it including updating credentials object.""" + if self.data is None: + await self.async_initialize() + assert self.data is not None + + await self.hass.async_add_executor_job( + self.data.change_username, credential.data["username"], new_username + ) + self.hass.auth.async_update_user_credentials_data( + credential, {**credential.data, "username": new_username} + ) + await self.data.async_save() + async def async_get_or_create_credentials( self, flow_result: Mapping[str, str] ) -> Credentials: diff --git a/homeassistant/components/auth/strings.json b/homeassistant/components/auth/strings.json index d386bb7a488..2b96b84c1cf 100644 --- a/homeassistant/components/auth/strings.json +++ b/homeassistant/components/auth/strings.json @@ -31,5 +31,13 @@ "invalid_code": "Invalid code, please try again." } } + }, + "exceptions": { + "username_already_exists": { + "message": "Username \"{username}\" already exists" + }, + "username_not_normalized": { + "message": "Username \"{new_username}\" is not normalized" + } } } diff --git a/homeassistant/components/config/auth_provider_homeassistant.py b/homeassistant/components/config/auth_provider_homeassistant.py index 94c179e1a5f..1cfcda6d4b2 100644 --- a/homeassistant/components/config/auth_provider_homeassistant.py +++ b/homeassistant/components/config/auth_provider_homeassistant.py @@ -19,6 +19,7 @@ def async_setup(hass: HomeAssistant) -> bool: websocket_api.async_register_command(hass, websocket_delete) websocket_api.async_register_command(hass, websocket_change_password) websocket_api.async_register_command(hass, websocket_admin_change_password) + websocket_api.async_register_command(hass, websocket_admin_change_username) return True @@ -194,3 +195,44 @@ async def websocket_admin_change_password( msg["id"], "credentials_not_found", "Credentials not found" ) return + + +@websocket_api.websocket_command( + { + vol.Required( + "type" + ): "config/auth_provider/homeassistant/admin_change_username", + vol.Required("user_id"): str, + vol.Required("username"): str, + } +) +@websocket_api.require_admin +@websocket_api.async_response +async def websocket_admin_change_username( + hass: HomeAssistant, + connection: websocket_api.ActiveConnection, + msg: dict[str, Any], +) -> None: + """Change the username for any user.""" + if not connection.user.is_owner: + raise Unauthorized(context=connection.context(msg)) + + if (user := await hass.auth.async_get_user(msg["user_id"])) is None: + connection.send_error(msg["id"], "user_not_found", "User not found") + return + + provider = auth_ha.async_get_provider(hass) + found_credential = None + for credential in user.credentials: + if credential.auth_provider_type == provider.type: + found_credential = credential + break + + if found_credential is None: + connection.send_error( + msg["id"], "credentials_not_found", "Credentials not found" + ) + return + + await provider.async_change_username(found_credential, msg["username"]) + connection.send_result(msg["id"]) diff --git a/script/hassfest/translations.py b/script/hassfest/translations.py index 04ea85ca5d5..7ffb5861bb4 100644 --- a/script/hassfest/translations.py +++ b/script/hassfest/translations.py @@ -250,6 +250,14 @@ def gen_issues_schema(config: Config, integration: Integration) -> dict[str, Any } +_EXCEPTIONS_SCHEMA = { + vol.Optional("exceptions"): cv.schema_with_slug_keys( + {vol.Optional("message"): translation_value_validator}, + slug_validator=cv.slug, + ), +} + + def gen_strings_schema(config: Config, integration: Integration) -> vol.Schema: """Generate a strings schema.""" return vol.Schema( @@ -355,10 +363,7 @@ def gen_strings_schema(config: Config, integration: Integration) -> vol.Schema: ), slug_validator=cv.slug, ), - vol.Optional("exceptions"): cv.schema_with_slug_keys( - {vol.Optional("message"): translation_value_validator}, - slug_validator=cv.slug, - ), + **_EXCEPTIONS_SCHEMA, vol.Optional("services"): cv.schema_with_slug_keys( { vol.Required("name"): translation_value_validator, @@ -397,6 +402,7 @@ def gen_auth_schema(config: Config, integration: Integration) -> vol.Schema: ) }, vol.Optional("issues"): gen_issues_schema(config, integration), + **_EXCEPTIONS_SCHEMA, } ) diff --git a/tests/auth/providers/test_homeassistant.py b/tests/auth/providers/test_homeassistant.py index dc5c255579c..3224bf6b4f7 100644 --- a/tests/auth/providers/test_homeassistant.py +++ b/tests/auth/providers/test_homeassistant.py @@ -13,10 +13,11 @@ from homeassistant.auth.providers import ( homeassistant as hass_auth, ) from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component @pytest.fixture -def data(hass): +def data(hass: HomeAssistant) -> hass_auth.Data: """Create a loaded data class.""" data = hass_auth.Data(hass) hass.loop.run_until_complete(data.async_load()) @@ -24,7 +25,7 @@ def data(hass): @pytest.fixture -def legacy_data(hass): +def legacy_data(hass: HomeAssistant) -> hass_auth.Data: """Create a loaded legacy data class.""" data = hass_auth.Data(hass) hass.loop.run_until_complete(data.async_load()) @@ -32,7 +33,13 @@ def legacy_data(hass): return data -async def test_validating_password_invalid_user(data, hass: HomeAssistant) -> None: +@pytest.fixture +async def load_auth_component(hass: HomeAssistant) -> None: + """Load the auth component for translations.""" + await async_setup_component(hass, "auth", {}) + + +async def test_validating_password_invalid_user(data: hass_auth.Data) -> None: """Test validating an invalid user.""" with pytest.raises(hass_auth.InvalidAuth): data.validate_login("non-existing", "pw") @@ -48,7 +55,9 @@ async def test_not_allow_set_id() -> None: ) -async def test_new_users_populate_values(hass: HomeAssistant, data) -> None: +async def test_new_users_populate_values( + hass: HomeAssistant, data: hass_auth.Data +) -> None: """Test that we populate data for new users.""" data.add_auth("hello", "test-pass") await data.async_save() @@ -61,7 +70,7 @@ async def test_new_users_populate_values(hass: HomeAssistant, data) -> None: assert user.is_active -async def test_changing_password_raises_invalid_user(data, hass: HomeAssistant) -> None: +async def test_changing_password_raises_invalid_user(data: hass_auth.Data) -> None: """Test that changing password raises invalid user.""" with pytest.raises(hass_auth.InvalidUser): data.change_password("non-existing", "pw") @@ -70,20 +79,34 @@ async def test_changing_password_raises_invalid_user(data, hass: HomeAssistant) # Modern mode -async def test_adding_user(data, hass: HomeAssistant) -> None: +async def test_adding_user(data: hass_auth.Data) -> None: """Test adding a user.""" data.add_auth("test-user", "test-pass") data.validate_login(" test-user ", "test-pass") -async def test_adding_user_duplicate_username(data, hass: HomeAssistant) -> None: +@pytest.mark.parametrize("username", ["test-user ", "TEST-USER"]) +@pytest.mark.usefixtures("load_auth_component") +def test_adding_user_not_normalized(data: hass_auth.Data, username: str) -> None: + """Test adding a user.""" + with pytest.raises( + hass_auth.InvalidUsername, match=f'Username "{username}" is not normalized' + ): + data.add_auth(username, "test-pass") + + +@pytest.mark.usefixtures("load_auth_component") +def test_adding_user_duplicate_username(data: hass_auth.Data) -> None: """Test adding a user with duplicate username.""" data.add_auth("test-user", "test-pass") - with pytest.raises(hass_auth.InvalidUser): - data.add_auth("TEST-user ", "other-pass") + + with pytest.raises( + hass_auth.InvalidUsername, match='Username "test-user" already exists' + ): + data.add_auth("test-user", "other-pass") -async def test_validating_password_invalid_password(data, hass: HomeAssistant) -> None: +async def test_validating_password_invalid_password(data: hass_auth.Data) -> None: """Test validating an invalid password.""" data.add_auth("test-user", "test-pass") @@ -97,7 +120,7 @@ async def test_validating_password_invalid_password(data, hass: HomeAssistant) - data.validate_login("test-user", "Test-pass") -async def test_changing_password(data, hass: HomeAssistant) -> None: +async def test_changing_password(data: hass_auth.Data) -> None: """Test adding a user.""" data.add_auth("test-user", "test-pass") data.change_password("TEST-USER ", "new-pass") @@ -108,7 +131,7 @@ async def test_changing_password(data, hass: HomeAssistant) -> None: data.validate_login("test-UsEr", "new-pass") -async def test_login_flow_validates(data, hass: HomeAssistant) -> None: +async def test_login_flow_validates(data: hass_auth.Data, hass: HomeAssistant) -> None: """Test login flow.""" data.add_auth("test-user", "test-pass") await data.async_save() @@ -139,7 +162,7 @@ async def test_login_flow_validates(data, hass: HomeAssistant) -> None: assert result["data"]["username"] == "test-USER" -async def test_saving_loading(data, hass: HomeAssistant) -> None: +async def test_saving_loading(data: hass_auth.Data, hass: HomeAssistant) -> None: """Test saving and loading JSON.""" data.add_auth("test-user", "test-pass") data.add_auth("second-user", "second-pass") @@ -151,7 +174,9 @@ async def test_saving_loading(data, hass: HomeAssistant) -> None: data.validate_login("second-user ", "second-pass") -async def test_get_or_create_credentials(hass: HomeAssistant, data) -> None: +async def test_get_or_create_credentials( + hass: HomeAssistant, data: hass_auth.Data +) -> None: """Test that we can get or create credentials.""" manager = await auth_manager_from_config(hass, [{"type": "homeassistant"}], []) provider = manager.auth_providers[0] @@ -167,26 +192,14 @@ async def test_get_or_create_credentials(hass: HomeAssistant, data) -> None: # Legacy mode -async def test_legacy_adding_user(legacy_data, hass: HomeAssistant) -> None: +async def test_legacy_adding_user(legacy_data: hass_auth.Data) -> None: """Test in legacy mode adding a user.""" legacy_data.add_auth("test-user", "test-pass") legacy_data.validate_login("test-user", "test-pass") -async def test_legacy_adding_user_duplicate_username( - legacy_data, hass: HomeAssistant -) -> None: - """Test in legacy mode adding a user with duplicate username.""" - legacy_data.add_auth("test-user", "test-pass") - with pytest.raises(hass_auth.InvalidUser): - legacy_data.add_auth("test-user", "other-pass") - # Not considered duplicate - legacy_data.add_auth("test-user ", "test-pass") - legacy_data.add_auth("Test-user", "test-pass") - - async def test_legacy_validating_password_invalid_password( - legacy_data, hass: HomeAssistant + legacy_data: hass_auth.Data, ) -> None: """Test in legacy mode validating an invalid password.""" legacy_data.add_auth("test-user", "test-pass") @@ -195,7 +208,7 @@ async def test_legacy_validating_password_invalid_password( legacy_data.validate_login("test-user", "invalid-pass") -async def test_legacy_changing_password(legacy_data, hass: HomeAssistant) -> None: +async def test_legacy_changing_password(legacy_data: hass_auth.Data) -> None: """Test in legacy mode adding a user.""" user = "test-user" legacy_data.add_auth(user, "test-pass") @@ -208,14 +221,16 @@ async def test_legacy_changing_password(legacy_data, hass: HomeAssistant) -> Non async def test_legacy_changing_password_raises_invalid_user( - legacy_data, hass: HomeAssistant + legacy_data: hass_auth.Data, ) -> None: """Test in legacy mode that we initialize an empty config.""" with pytest.raises(hass_auth.InvalidUser): legacy_data.change_password("non-existing", "pw") -async def test_legacy_login_flow_validates(legacy_data, hass: HomeAssistant) -> None: +async def test_legacy_login_flow_validates( + legacy_data: hass_auth.Data, hass: HomeAssistant +) -> None: """Test in legacy mode login flow.""" legacy_data.add_auth("test-user", "test-pass") await legacy_data.async_save() @@ -246,7 +261,9 @@ async def test_legacy_login_flow_validates(legacy_data, hass: HomeAssistant) -> assert result["data"]["username"] == "test-user" -async def test_legacy_saving_loading(legacy_data, hass: HomeAssistant) -> None: +async def test_legacy_saving_loading( + legacy_data: hass_auth.Data, hass: HomeAssistant +) -> None: """Test in legacy mode saving and loading JSON.""" legacy_data.add_auth("test-user", "test-pass") legacy_data.add_auth("second-user", "second-pass") @@ -263,7 +280,7 @@ async def test_legacy_saving_loading(legacy_data, hass: HomeAssistant) -> None: async def test_legacy_get_or_create_credentials( - hass: HomeAssistant, legacy_data + hass: HomeAssistant, legacy_data: hass_auth.Data ) -> None: """Test in legacy mode that we can get or create credentials.""" manager = await auth_manager_from_config(hass, [{"type": "homeassistant"}], []) @@ -308,3 +325,67 @@ async def test_race_condition_in_data_loading(hass: HomeAssistant) -> None: assert isinstance(results[0], hass_auth.InvalidAuth) # results[1] will be a TypeError if race condition occurred assert isinstance(results[1], hass_auth.InvalidAuth) + + +def test_change_username(data: hass_auth.Data) -> None: + """Test changing username.""" + data.add_auth("test-user", "test-pass") + users = data.users + assert len(users) == 1 + assert users[0]["username"] == "test-user" + + data.change_username("test-user", "new-user") + + users = data.users + assert len(users) == 1 + assert users[0]["username"] == "new-user" + + +@pytest.mark.parametrize("username", ["test-user ", "TEST-USER"]) +def test_change_username_legacy(legacy_data: hass_auth.Data, username: str) -> None: + """Test changing username.""" + # Cannot use add_auth as it normalizes username + legacy_data.users.append( + { + "username": username, + "password": legacy_data.hash_password("test-pass", True).decode(), + } + ) + + users = legacy_data.users + assert len(users) == 1 + assert users[0]["username"] == username + + legacy_data.change_username(username, "test-user") + + users = legacy_data.users + assert len(users) == 1 + assert users[0]["username"] == "test-user" + + +def test_change_username_invalid_user(data: hass_auth.Data) -> None: + """Test changing username raises on invalid user.""" + data.add_auth("test-user", "test-pass") + users = data.users + assert len(users) == 1 + assert users[0]["username"] == "test-user" + + with pytest.raises(hass_auth.InvalidUser): + data.change_username("non-existing", "new-user") + + users = data.users + assert len(users) == 1 + assert users[0]["username"] == "test-user" + + +@pytest.mark.usefixtures("load_auth_component") +async def test_change_username_not_normalized( + data: hass_auth.Data, hass: HomeAssistant +) -> None: + """Test changing username raises on not normalized username.""" + data.add_auth("test-user", "test-pass") + + with pytest.raises( + hass_auth.InvalidUsername, match='Username "TEST-user " is not normalized' + ): + data.change_username("test-user", "TEST-user ") diff --git a/tests/components/config/test_auth_provider_homeassistant.py b/tests/components/config/test_auth_provider_homeassistant.py index 044d6cdb571..ffee88f91ec 100644 --- a/tests/components/config/test_auth_provider_homeassistant.py +++ b/tests/components/config/test_auth_provider_homeassistant.py @@ -445,3 +445,170 @@ async def test_admin_change_password( assert result["success"], result await auth_provider.async_validate_login("test-user", "new-pass") + + +def _assert_username( + local_auth: prov_ha.HassAuthProvider, username: str, *, should_exist: bool +) -> None: + if any(user["username"] == username for user in local_auth.data.users): + if should_exist: + return # found + + pytest.fail(f"Found user with username {username} when not expected") + + if should_exist: + pytest.fail(f"Did not find user with username {username}") + + +async def _test_admin_change_username( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + local_auth: prov_ha.HassAuthProvider, + hass_admin_user: MockUser, + owner_access_token: str, + new_username: str, +) -> dict[str, Any]: + """Test admin change username ws endpoint.""" + client = await hass_ws_client(hass, owner_access_token) + current_username_user = hass_admin_user.credentials[0].data["username"] + _assert_username(local_auth, current_username_user, should_exist=True) + + await client.send_json_auto_id( + { + "type": "config/auth_provider/homeassistant/admin_change_username", + "user_id": hass_admin_user.id, + "username": new_username, + } + ) + return await client.receive_json() + + +async def test_admin_change_username_success( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + local_auth: prov_ha.HassAuthProvider, + hass_admin_user: MockUser, + owner_access_token: str, +) -> None: + """Test that change username succeeds.""" + current_username = hass_admin_user.credentials[0].data["username"] + new_username = "blabla" + + result = await _test_admin_change_username( + hass, + hass_ws_client, + local_auth, + hass_admin_user, + owner_access_token, + new_username, + ) + + assert result["success"], result + _assert_username(local_auth, current_username, should_exist=False) + _assert_username(local_auth, new_username, should_exist=True) + assert hass_admin_user.credentials[0].data["username"] == new_username + # Validate new login works + await local_auth.async_validate_login(new_username, "test-pass") + with pytest.raises(prov_ha.InvalidAuth): + # Verify old login does not work + await local_auth.async_validate_login(current_username, "test-pass") + + +@pytest.mark.parametrize("new_username", [" bla", "bla ", "BlA"]) +async def test_admin_change_username_error_not_normalized( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + local_auth: prov_ha.HassAuthProvider, + hass_admin_user: MockUser, + owner_access_token: str, + new_username: str, +) -> None: + """Test that change username raises error.""" + current_username = hass_admin_user.credentials[0].data["username"] + + result = await _test_admin_change_username( + hass, + hass_ws_client, + local_auth, + hass_admin_user, + owner_access_token, + new_username, + ) + assert not result["success"], result + assert result["error"] == { + "code": "home_assistant_error", + "message": "username_not_normalized", + "translation_key": "username_not_normalized", + "translation_placeholders": {"new_username": new_username}, + "translation_domain": "auth", + } + _assert_username(local_auth, current_username, should_exist=True) + _assert_username(local_auth, new_username, should_exist=False) + assert hass_admin_user.credentials[0].data["username"] == current_username + # Validate old login still works + await local_auth.async_validate_login(current_username, "test-pass") + + +async def test_admin_change_username_not_owner( + hass: HomeAssistant, hass_ws_client: WebSocketGenerator, auth_provider +) -> None: + """Test that change username fails when not owner.""" + client = await hass_ws_client(hass) + + await client.send_json_auto_id( + { + "type": "config/auth_provider/homeassistant/admin_change_username", + "user_id": "test-user", + "username": "new-user", + } + ) + + result = await client.receive_json() + assert not result["success"], result + assert result["error"]["code"] == "unauthorized" + + # Validate old login still works + await auth_provider.async_validate_login("test-user", "test-pass") + + +async def test_admin_change_username_no_user( + hass: HomeAssistant, hass_ws_client: WebSocketGenerator, owner_access_token +) -> None: + """Test that change username fails with unknown user.""" + client = await hass_ws_client(hass, owner_access_token) + + await client.send_json_auto_id( + { + "type": "config/auth_provider/homeassistant/admin_change_username", + "user_id": "non-existing", + "username": "new-username", + } + ) + + result = await client.receive_json() + assert not result["success"], result + assert result["error"]["code"] == "user_not_found" + + +async def test_admin_change_username_no_cred( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + owner_access_token, + hass_admin_user: MockUser, +) -> None: + """Test that change username fails with unknown credential.""" + + hass_admin_user.credentials.clear() + client = await hass_ws_client(hass, owner_access_token) + + await client.send_json_auto_id( + { + "type": "config/auth_provider/homeassistant/admin_change_username", + "user_id": hass_admin_user.id, + "username": "new-username", + } + ) + + result = await client.receive_json() + assert not result["success"], result + assert result["error"]["code"] == "credentials_not_found" From a4e22bcba690ae638d224e48806baad9fb34ecc4 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 24 Jun 2024 18:52:49 +0200 Subject: [PATCH 1119/1445] Update tenacity constraint (#120348) --- homeassistant/package_constraints.txt | 2 +- script/gen_requirements_all.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 1f1811eca4c..f3be7c5515e 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -199,4 +199,4 @@ scapy>=2.5.0 tuf>=4.0.0 # https://github.com/jd/tenacity/issues/471 -tenacity<8.4.0 +tenacity!=8.4.0 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 57b4a2e1855..434b4d0071f 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -221,7 +221,7 @@ scapy>=2.5.0 tuf>=4.0.0 # https://github.com/jd/tenacity/issues/471 -tenacity<8.4.0 +tenacity!=8.4.0 """ GENERATED_MESSAGE = ( From 31157828e1e58ca47439cd64147abf2af289d924 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 24 Jun 2024 18:55:36 +0200 Subject: [PATCH 1120/1445] Improve type hints in cloudflare tests (#120344) --- tests/components/cloudflare/__init__.py | 23 ++++++++----------- tests/components/cloudflare/conftest.py | 15 ++++++------ .../components/cloudflare/test_config_flow.py | 22 +++++++++++------- tests/components/cloudflare/test_init.py | 18 ++++++++------- 4 files changed, 41 insertions(+), 37 deletions(-) diff --git a/tests/components/cloudflare/__init__.py b/tests/components/cloudflare/__init__.py index 5e1529a9da8..9827355c9cc 100644 --- a/tests/components/cloudflare/__init__.py +++ b/tests/components/cloudflare/__init__.py @@ -3,7 +3,7 @@ from __future__ import annotations from typing import Any -from unittest.mock import AsyncMock, patch +from unittest.mock import AsyncMock, Mock, patch import pycfdns @@ -80,25 +80,20 @@ async def init_integration( return entry -def _get_mock_client( - zone: pycfdns.ZoneModel | UndefinedType = UNDEFINED, - records: list[pycfdns.RecordModel] | UndefinedType = UNDEFINED, -): - client: pycfdns.Client = AsyncMock() +def get_mock_client() -> Mock: + """Return of Mock of pycfdns.Client.""" + client = Mock() - client.list_zones = AsyncMock( - return_value=[MOCK_ZONE if zone is UNDEFINED else zone] - ) - client.list_dns_records = AsyncMock( - return_value=MOCK_ZONE_RECORDS if records is UNDEFINED else records - ) + client.list_zones = AsyncMock(return_value=[MOCK_ZONE]) + client.list_dns_records = AsyncMock(return_value=MOCK_ZONE_RECORDS) client.update_dns_record = AsyncMock(return_value=None) return client -def _patch_async_setup_entry(return_value=True): +def patch_async_setup_entry() -> AsyncMock: + """Patch the async_setup_entry method and return a mock.""" return patch( "homeassistant.components.cloudflare.async_setup_entry", - return_value=return_value, + return_value=True, ) diff --git a/tests/components/cloudflare/conftest.py b/tests/components/cloudflare/conftest.py index 81b52dd291d..6c41e9fd179 100644 --- a/tests/components/cloudflare/conftest.py +++ b/tests/components/cloudflare/conftest.py @@ -1,16 +1,17 @@ """Define fixtures available for all tests.""" -from unittest.mock import patch +from unittest.mock import MagicMock, patch import pytest +from typing_extensions import Generator -from . import _get_mock_client +from . import get_mock_client @pytest.fixture -def cfupdate(hass): +def cfupdate() -> Generator[MagicMock]: """Mock the CloudflareUpdater for easier testing.""" - mock_cfupdate = _get_mock_client() + mock_cfupdate = get_mock_client() with patch( "homeassistant.components.cloudflare.pycfdns.Client", return_value=mock_cfupdate, @@ -19,11 +20,11 @@ def cfupdate(hass): @pytest.fixture -def cfupdate_flow(hass): +def cfupdate_flow() -> Generator[MagicMock]: """Mock the CloudflareUpdater for easier config flow testing.""" - mock_cfupdate = _get_mock_client() + mock_cfupdate = get_mock_client() with patch( - "homeassistant.components.cloudflare.pycfdns.Client", + "homeassistant.components.cloudflare.config_flow.pycfdns.Client", return_value=mock_cfupdate, ) as mock_api: yield mock_api diff --git a/tests/components/cloudflare/test_config_flow.py b/tests/components/cloudflare/test_config_flow.py index 4b0df91bc60..1278113c0c7 100644 --- a/tests/components/cloudflare/test_config_flow.py +++ b/tests/components/cloudflare/test_config_flow.py @@ -1,5 +1,7 @@ """Test the Cloudflare config flow.""" +from unittest.mock import MagicMock + import pycfdns from homeassistant.components.cloudflare.const import CONF_RECORDS, DOMAIN @@ -13,13 +15,13 @@ from . import ( USER_INPUT, USER_INPUT_RECORDS, USER_INPUT_ZONE, - _patch_async_setup_entry, + patch_async_setup_entry, ) from tests.common import MockConfigEntry -async def test_user_form(hass: HomeAssistant, cfupdate_flow) -> None: +async def test_user_form(hass: HomeAssistant, cfupdate_flow: MagicMock) -> None: """Test we get the user initiated form.""" result = await hass.config_entries.flow.async_init( @@ -49,7 +51,7 @@ async def test_user_form(hass: HomeAssistant, cfupdate_flow) -> None: assert result["step_id"] == "records" assert result["errors"] is None - with _patch_async_setup_entry() as mock_setup_entry: + with patch_async_setup_entry() as mock_setup_entry: result = await hass.config_entries.flow.async_configure( result["flow_id"], USER_INPUT_RECORDS, @@ -70,7 +72,9 @@ async def test_user_form(hass: HomeAssistant, cfupdate_flow) -> None: assert len(mock_setup_entry.mock_calls) == 1 -async def test_user_form_cannot_connect(hass: HomeAssistant, cfupdate_flow) -> None: +async def test_user_form_cannot_connect( + hass: HomeAssistant, cfupdate_flow: MagicMock +) -> None: """Test we handle cannot connect error.""" instance = cfupdate_flow.return_value @@ -88,7 +92,9 @@ async def test_user_form_cannot_connect(hass: HomeAssistant, cfupdate_flow) -> N assert result["errors"] == {"base": "cannot_connect"} -async def test_user_form_invalid_auth(hass: HomeAssistant, cfupdate_flow) -> None: +async def test_user_form_invalid_auth( + hass: HomeAssistant, cfupdate_flow: MagicMock +) -> None: """Test we handle invalid auth error.""" instance = cfupdate_flow.return_value @@ -107,7 +113,7 @@ async def test_user_form_invalid_auth(hass: HomeAssistant, cfupdate_flow) -> Non async def test_user_form_unexpected_exception( - hass: HomeAssistant, cfupdate_flow + hass: HomeAssistant, cfupdate_flow: MagicMock ) -> None: """Test we handle unexpected exception.""" instance = cfupdate_flow.return_value @@ -140,7 +146,7 @@ async def test_user_form_single_instance_allowed(hass: HomeAssistant) -> None: assert result["reason"] == "single_instance_allowed" -async def test_reauth_flow(hass: HomeAssistant, cfupdate_flow) -> None: +async def test_reauth_flow(hass: HomeAssistant, cfupdate_flow: MagicMock) -> None: """Test the reauthentication configuration flow.""" entry = MockConfigEntry(domain=DOMAIN, data=ENTRY_CONFIG) entry.add_to_hass(hass) @@ -157,7 +163,7 @@ async def test_reauth_flow(hass: HomeAssistant, cfupdate_flow) -> None: assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" - with _patch_async_setup_entry() as mock_setup_entry: + with patch_async_setup_entry() as mock_setup_entry: result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_API_TOKEN: "other_token"}, diff --git a/tests/components/cloudflare/test_init.py b/tests/components/cloudflare/test_init.py index 3b2a6803566..d629607e503 100644 --- a/tests/components/cloudflare/test_init.py +++ b/tests/components/cloudflare/test_init.py @@ -1,7 +1,7 @@ """Test the Cloudflare integration.""" from datetime import timedelta -from unittest.mock import patch +from unittest.mock import MagicMock, patch import pycfdns import pytest @@ -23,7 +23,7 @@ from . import ENTRY_CONFIG, init_integration from tests.common import MockConfigEntry, async_fire_time_changed -async def test_unload_entry(hass: HomeAssistant, cfupdate) -> None: +async def test_unload_entry(hass: HomeAssistant, cfupdate: MagicMock) -> None: """Test successful unload of entry.""" entry = await init_integration(hass) @@ -42,7 +42,7 @@ async def test_unload_entry(hass: HomeAssistant, cfupdate) -> None: [pycfdns.ComunicationException()], ) async def test_async_setup_raises_entry_not_ready( - hass: HomeAssistant, cfupdate, side_effect + hass: HomeAssistant, cfupdate: MagicMock, side_effect: Exception ) -> None: """Test that it throws ConfigEntryNotReady when exception occurs during setup.""" instance = cfupdate.return_value @@ -57,7 +57,7 @@ async def test_async_setup_raises_entry_not_ready( async def test_async_setup_raises_entry_auth_failed( - hass: HomeAssistant, cfupdate + hass: HomeAssistant, cfupdate: MagicMock ) -> None: """Test that it throws ConfigEntryAuthFailed when exception occurs during setup.""" instance = cfupdate.return_value @@ -84,7 +84,7 @@ async def test_async_setup_raises_entry_auth_failed( async def test_integration_services( - hass: HomeAssistant, cfupdate, caplog: pytest.LogCaptureFixture + hass: HomeAssistant, cfupdate: MagicMock, caplog: pytest.LogCaptureFixture ) -> None: """Test integration services.""" instance = cfupdate.return_value @@ -120,7 +120,9 @@ async def test_integration_services( assert "All target records are up to date" not in caplog.text -async def test_integration_services_with_issue(hass: HomeAssistant, cfupdate) -> None: +async def test_integration_services_with_issue( + hass: HomeAssistant, cfupdate: MagicMock +) -> None: """Test integration services with issue.""" instance = cfupdate.return_value @@ -145,7 +147,7 @@ async def test_integration_services_with_issue(hass: HomeAssistant, cfupdate) -> async def test_integration_services_with_nonexisting_record( - hass: HomeAssistant, cfupdate, caplog: pytest.LogCaptureFixture + hass: HomeAssistant, cfupdate: MagicMock, caplog: pytest.LogCaptureFixture ) -> None: """Test integration services.""" instance = cfupdate.return_value @@ -185,7 +187,7 @@ async def test_integration_services_with_nonexisting_record( async def test_integration_update_interval( hass: HomeAssistant, - cfupdate, + cfupdate: MagicMock, caplog: pytest.LogCaptureFixture, ) -> None: """Test integration update interval.""" From 1e5f4c2d754bee1caaae073bca803bba46bfe73c Mon Sep 17 00:00:00 2001 From: "Mr. Bubbles" Date: Mon, 24 Jun 2024 18:56:33 +0200 Subject: [PATCH 1121/1445] Add additional sensors to pyLoad integration (#120309) --- homeassistant/components/pyload/icons.json | 9 + homeassistant/components/pyload/sensor.py | 25 + homeassistant/components/pyload/strings.json | 12 + .../pyload/snapshots/test_sensor.ambr | 952 ++++++++++++++++++ 4 files changed, 998 insertions(+) diff --git a/homeassistant/components/pyload/icons.json b/homeassistant/components/pyload/icons.json index b3b7d148b1a..bc068165851 100644 --- a/homeassistant/components/pyload/icons.json +++ b/homeassistant/components/pyload/icons.json @@ -3,6 +3,15 @@ "sensor": { "speed": { "default": "mdi:speedometer" + }, + "active": { + "default": "mdi:cloud-download" + }, + "queue": { + "default": "mdi:cloud-clock" + }, + "total": { + "default": "mdi:cloud-alert" } } } diff --git a/homeassistant/components/pyload/sensor.py b/homeassistant/components/pyload/sensor.py index 7caef84d2dc..c4fea3e43bb 100644 --- a/homeassistant/components/pyload/sensor.py +++ b/homeassistant/components/pyload/sensor.py @@ -22,6 +22,7 @@ from homeassistant.const import ( CONF_SSL, CONF_USERNAME, UnitOfDataRate, + UnitOfInformation, ) from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -40,7 +41,11 @@ from .coordinator import PyLoadCoordinator class PyLoadSensorEntity(StrEnum): """pyLoad Sensor Entities.""" + ACTIVE = "active" + FREE_SPACE = "free_space" + QUEUE = "queue" SPEED = "speed" + TOTAL = "total" SENSOR_DESCRIPTIONS: tuple[SensorEntityDescription, ...] = ( @@ -52,6 +57,26 @@ SENSOR_DESCRIPTIONS: tuple[SensorEntityDescription, ...] = ( suggested_unit_of_measurement=UnitOfDataRate.MEGABITS_PER_SECOND, suggested_display_precision=1, ), + SensorEntityDescription( + key=PyLoadSensorEntity.ACTIVE, + translation_key=PyLoadSensorEntity.ACTIVE, + ), + SensorEntityDescription( + key=PyLoadSensorEntity.QUEUE, + translation_key=PyLoadSensorEntity.QUEUE, + ), + SensorEntityDescription( + key=PyLoadSensorEntity.TOTAL, + translation_key=PyLoadSensorEntity.TOTAL, + ), + SensorEntityDescription( + key=PyLoadSensorEntity.FREE_SPACE, + translation_key=PyLoadSensorEntity.FREE_SPACE, + device_class=SensorDeviceClass.DATA_SIZE, + native_unit_of_measurement=UnitOfInformation.BYTES, + suggested_unit_of_measurement=UnitOfInformation.GIBIBYTES, + suggested_display_precision=1, + ), ) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( diff --git a/homeassistant/components/pyload/strings.json b/homeassistant/components/pyload/strings.json index a8544bf48eb..cc53ef7465b 100644 --- a/homeassistant/components/pyload/strings.json +++ b/homeassistant/components/pyload/strings.json @@ -31,6 +31,18 @@ "sensor": { "speed": { "name": "Speed" + }, + "active": { + "name": "Active downloads" + }, + "queue": { + "name": "Downloads in queue" + }, + "total": { + "name": "Total downlods" + }, + "free_space": { + "name": "Free space" } } }, diff --git a/tests/components/pyload/snapshots/test_sensor.ambr b/tests/components/pyload/snapshots/test_sensor.ambr index a6049577f47..8675fb696a5 100644 --- a/tests/components/pyload/snapshots/test_sensor.ambr +++ b/tests/components/pyload/snapshots/test_sensor.ambr @@ -1,4 +1,196 @@ # serializer version: 1 +# name: test_sensor_update_exceptions[CannotConnect][sensor.pyload_active_downloads-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.pyload_active_downloads', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Active downloads', + 'platform': 'pyload', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'XXXXXXXXXXXXXX_active', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor_update_exceptions[CannotConnect][sensor.pyload_active_downloads-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'pyLoad Active downloads', + }), + 'context': , + 'entity_id': 'sensor.pyload_active_downloads', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_sensor_update_exceptions[CannotConnect][sensor.pyload_downloads_in_queue-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.pyload_downloads_in_queue', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Downloads in queue', + 'platform': 'pyload', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'XXXXXXXXXXXXXX_queue', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor_update_exceptions[CannotConnect][sensor.pyload_downloads_in_queue-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'pyLoad Downloads in queue', + }), + 'context': , + 'entity_id': 'sensor.pyload_downloads_in_queue', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_sensor_update_exceptions[CannotConnect][sensor.pyload_downloads_total-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.pyload_downloads_total', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Downloads total', + 'platform': 'pyload', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'XXXXXXXXXXXXXX_total', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor_update_exceptions[CannotConnect][sensor.pyload_downloads_total-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'pyLoad Downloads total', + }), + 'context': , + 'entity_id': 'sensor.pyload_downloads_total', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_sensor_update_exceptions[CannotConnect][sensor.pyload_free_space-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.pyload_free_space', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Free space', + 'platform': 'pyload', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'XXXXXXXXXXXXXX_free_space', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor_update_exceptions[CannotConnect][sensor.pyload_free_space-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'data_size', + 'friendly_name': 'pyLoad Free space', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.pyload_free_space', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- # name: test_sensor_update_exceptions[CannotConnect][sensor.pyload_speed-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -53,6 +245,244 @@ 'state': 'unavailable', }) # --- +# name: test_sensor_update_exceptions[CannotConnect][sensor.pyload_total_downlods-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.pyload_total_downlods', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Total downlods', + 'platform': 'pyload', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'XXXXXXXXXXXXXX_total', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor_update_exceptions[CannotConnect][sensor.pyload_total_downlods-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'pyLoad Total downlods', + }), + 'context': , + 'entity_id': 'sensor.pyload_total_downlods', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_sensor_update_exceptions[InvalidAuth][sensor.pyload_active_downloads-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.pyload_active_downloads', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Active downloads', + 'platform': 'pyload', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'XXXXXXXXXXXXXX_active', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor_update_exceptions[InvalidAuth][sensor.pyload_active_downloads-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'pyLoad Active downloads', + }), + 'context': , + 'entity_id': 'sensor.pyload_active_downloads', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_sensor_update_exceptions[InvalidAuth][sensor.pyload_downloads_in_queue-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.pyload_downloads_in_queue', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Downloads in queue', + 'platform': 'pyload', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'XXXXXXXXXXXXXX_queue', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor_update_exceptions[InvalidAuth][sensor.pyload_downloads_in_queue-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'pyLoad Downloads in queue', + }), + 'context': , + 'entity_id': 'sensor.pyload_downloads_in_queue', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_sensor_update_exceptions[InvalidAuth][sensor.pyload_downloads_total-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.pyload_downloads_total', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Downloads total', + 'platform': 'pyload', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'XXXXXXXXXXXXXX_total', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor_update_exceptions[InvalidAuth][sensor.pyload_downloads_total-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'pyLoad Downloads total', + }), + 'context': , + 'entity_id': 'sensor.pyload_downloads_total', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_sensor_update_exceptions[InvalidAuth][sensor.pyload_free_space-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.pyload_free_space', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Free space', + 'platform': 'pyload', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'XXXXXXXXXXXXXX_free_space', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor_update_exceptions[InvalidAuth][sensor.pyload_free_space-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'data_size', + 'friendly_name': 'pyLoad Free space', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.pyload_free_space', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- # name: test_sensor_update_exceptions[InvalidAuth][sensor.pyload_speed-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -107,6 +537,244 @@ 'state': 'unavailable', }) # --- +# name: test_sensor_update_exceptions[InvalidAuth][sensor.pyload_total_downlods-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.pyload_total_downlods', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Total downlods', + 'platform': 'pyload', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'XXXXXXXXXXXXXX_total', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor_update_exceptions[InvalidAuth][sensor.pyload_total_downlods-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'pyLoad Total downlods', + }), + 'context': , + 'entity_id': 'sensor.pyload_total_downlods', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_sensor_update_exceptions[ParserError][sensor.pyload_active_downloads-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.pyload_active_downloads', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Active downloads', + 'platform': 'pyload', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'XXXXXXXXXXXXXX_active', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor_update_exceptions[ParserError][sensor.pyload_active_downloads-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'pyLoad Active downloads', + }), + 'context': , + 'entity_id': 'sensor.pyload_active_downloads', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_sensor_update_exceptions[ParserError][sensor.pyload_downloads_in_queue-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.pyload_downloads_in_queue', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Downloads in queue', + 'platform': 'pyload', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'XXXXXXXXXXXXXX_queue', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor_update_exceptions[ParserError][sensor.pyload_downloads_in_queue-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'pyLoad Downloads in queue', + }), + 'context': , + 'entity_id': 'sensor.pyload_downloads_in_queue', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_sensor_update_exceptions[ParserError][sensor.pyload_downloads_total-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.pyload_downloads_total', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Downloads total', + 'platform': 'pyload', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'XXXXXXXXXXXXXX_total', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor_update_exceptions[ParserError][sensor.pyload_downloads_total-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'pyLoad Downloads total', + }), + 'context': , + 'entity_id': 'sensor.pyload_downloads_total', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_sensor_update_exceptions[ParserError][sensor.pyload_free_space-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.pyload_free_space', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Free space', + 'platform': 'pyload', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'XXXXXXXXXXXXXX_free_space', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor_update_exceptions[ParserError][sensor.pyload_free_space-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'data_size', + 'friendly_name': 'pyLoad Free space', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.pyload_free_space', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- # name: test_sensor_update_exceptions[ParserError][sensor.pyload_speed-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -161,6 +829,244 @@ 'state': 'unavailable', }) # --- +# name: test_sensor_update_exceptions[ParserError][sensor.pyload_total_downlods-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.pyload_total_downlods', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Total downlods', + 'platform': 'pyload', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'XXXXXXXXXXXXXX_total', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor_update_exceptions[ParserError][sensor.pyload_total_downlods-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'pyLoad Total downlods', + }), + 'context': , + 'entity_id': 'sensor.pyload_total_downlods', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_setup[sensor.pyload_active_downloads-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.pyload_active_downloads', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Active downloads', + 'platform': 'pyload', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'XXXXXXXXXXXXXX_active', + 'unit_of_measurement': None, + }) +# --- +# name: test_setup[sensor.pyload_active_downloads-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'pyLoad Active downloads', + }), + 'context': , + 'entity_id': 'sensor.pyload_active_downloads', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1', + }) +# --- +# name: test_setup[sensor.pyload_downloads_in_queue-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.pyload_downloads_in_queue', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Downloads in queue', + 'platform': 'pyload', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'XXXXXXXXXXXXXX_queue', + 'unit_of_measurement': None, + }) +# --- +# name: test_setup[sensor.pyload_downloads_in_queue-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'pyLoad Downloads in queue', + }), + 'context': , + 'entity_id': 'sensor.pyload_downloads_in_queue', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '6', + }) +# --- +# name: test_setup[sensor.pyload_downloads_total-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.pyload_downloads_total', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Downloads total', + 'platform': 'pyload', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'XXXXXXXXXXXXXX_total', + 'unit_of_measurement': None, + }) +# --- +# name: test_setup[sensor.pyload_downloads_total-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'pyLoad Downloads total', + }), + 'context': , + 'entity_id': 'sensor.pyload_downloads_total', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '37', + }) +# --- +# name: test_setup[sensor.pyload_free_space-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.pyload_free_space', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Free space', + 'platform': 'pyload', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'XXXXXXXXXXXXXX_free_space', + 'unit_of_measurement': , + }) +# --- +# name: test_setup[sensor.pyload_free_space-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'data_size', + 'friendly_name': 'pyLoad Free space', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.pyload_free_space', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '93.1322574606165', + }) +# --- # name: test_setup[sensor.pyload_speed-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -215,3 +1121,49 @@ 'state': '43.247704', }) # --- +# name: test_setup[sensor.pyload_total_downlods-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.pyload_total_downlods', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Total downlods', + 'platform': 'pyload', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'XXXXXXXXXXXXXX_total', + 'unit_of_measurement': None, + }) +# --- +# name: test_setup[sensor.pyload_total_downlods-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'pyLoad Total downlods', + }), + 'context': , + 'entity_id': 'sensor.pyload_total_downlods', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '37', + }) +# --- From a7200a70b2968b5938d97c1ab8539683405e36fe Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Mon, 24 Jun 2024 19:42:32 +0200 Subject: [PATCH 1122/1445] Set up mqtt tests from client fixture of mqtt entry setup fixture, not both (#120274) * Fix entry setup and cleanup issues in mqtt tests * Reduce changes by using mqtt_client_mock alias * Reduce sleep time where possibe --- tests/components/mqtt/conftest.py | 42 +- tests/components/mqtt/test_discovery.py | 4 +- tests/components/mqtt/test_init.py | 741 ++++++++++-------------- 3 files changed, 339 insertions(+), 448 deletions(-) diff --git a/tests/components/mqtt/conftest.py b/tests/components/mqtt/conftest.py index 9649e0b9ddf..39b9f122f75 100644 --- a/tests/components/mqtt/conftest.py +++ b/tests/components/mqtt/conftest.py @@ -1,12 +1,20 @@ """Test fixtures for mqtt component.""" +import asyncio from random import getrandbits +from typing import Any from unittest.mock import patch import pytest -from typing_extensions import Generator +from typing_extensions import AsyncGenerator, Generator from homeassistant.components import mqtt +from homeassistant.components.mqtt.models import ReceiveMessage +from homeassistant.const import EVENT_HOMEASSISTANT_STARTED +from homeassistant.core import HomeAssistant, callback + +from tests.common import MockConfigEntry +from tests.typing import MqttMockPahoClient ENTRY_DEFAULT_BIRTH_MESSAGE = { mqtt.CONF_BROKER: "mock-broker", @@ -39,3 +47,35 @@ def mock_temp_dir(temp_dir_prefix: str) -> Generator[str]: f"home-assistant-mqtt-{temp_dir_prefix}-{getrandbits(10):03x}", ) as mocked_temp_dir: yield mocked_temp_dir + + +@pytest.fixture +async def setup_with_birth_msg_client_mock( + hass: HomeAssistant, + mqtt_config_entry_data: dict[str, Any] | None, + mqtt_client_mock: MqttMockPahoClient, +) -> AsyncGenerator[MqttMockPahoClient]: + """Test sending birth message.""" + birth = asyncio.Event() + with ( + patch("homeassistant.components.mqtt.client.INITIAL_SUBSCRIBE_COOLDOWN", 0.0), + patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.0), + patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.0), + ): + entry = MockConfigEntry( + domain=mqtt.DOMAIN, data={mqtt.CONF_BROKER: "test-broker"} + ) + entry.add_to_hass(hass) + hass.config.components.add(mqtt.DOMAIN) + assert await hass.config_entries.async_setup(entry.entry_id) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + + @callback + def wait_birth(msg: ReceiveMessage) -> None: + """Handle birth message.""" + birth.set() + + await mqtt.async_subscribe(hass, "homeassistant/status", wait_birth) + await hass.async_block_till_done() + await birth.wait() + yield mqtt_client_mock diff --git a/tests/components/mqtt/test_discovery.py b/tests/components/mqtt/test_discovery.py index e36971e386f..b9ef1a3c210 100644 --- a/tests/components/mqtt/test_discovery.py +++ b/tests/components/mqtt/test_discovery.py @@ -1531,7 +1531,7 @@ async def test_mqtt_discovery_unsubscribe_once( async def async_step_mqtt(self, discovery_info: MqttServiceInfo) -> FlowResult: """Test mqtt step.""" - await asyncio.sleep(0.1) + await asyncio.sleep(0) return self.async_abort(reason="already_configured") mock_platform(hass, "comp.config_flow", None) @@ -1573,7 +1573,7 @@ async def test_mqtt_discovery_unsubscribe_once( async_fire_mqtt_message(hass, "comp/discovery/bla/config", "") async_fire_mqtt_message(hass, "comp/discovery/bla/config", "") await wait_unsub.wait() - await asyncio.sleep(0.2) + await asyncio.sleep(0) await hass.async_block_till_done(wait_background_tasks=True) mqtt_client_mock.unsubscribe.assert_called_once_with(["comp/discovery/#"]) await hass.async_block_till_done(wait_background_tasks=True) diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index 264f80f48f8..8a76c71f1f3 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -56,6 +56,7 @@ from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util from homeassistant.util.dt import utcnow +from .conftest import ENTRY_DEFAULT_BIRTH_MESSAGE from .test_common import help_all_subscribe_calls from tests.common import ( @@ -149,21 +150,19 @@ def help_assert_message( async def test_mqtt_connects_on_home_assistant_mqtt_setup( hass: HomeAssistant, - mqtt_client_mock: MqttMockPahoClient, - mqtt_mock_entry: MqttMockHAClientGenerator, + setup_with_birth_msg_client_mock: MqttMockPahoClient, ) -> None: """Test if client is connected after mqtt init on bootstrap.""" - await mqtt_mock_entry() + mqtt_client_mock = setup_with_birth_msg_client_mock assert mqtt_client_mock.connect.call_count == 1 async def test_mqtt_does_not_disconnect_on_home_assistant_stop( hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - mqtt_client_mock: MqttMockPahoClient, + setup_with_birth_msg_client_mock: MqttMockPahoClient, ) -> None: """Test if client is not disconnected on HA stop.""" - await mqtt_mock_entry() + mqtt_client_mock = setup_with_birth_msg_client_mock hass.bus.fire(EVENT_HOMEASSISTANT_STOP) await hass.async_block_till_done() await hass.async_block_till_done() @@ -226,12 +225,13 @@ async def test_mqtt_await_ack_at_disconnect( await hass.async_block_till_done(wait_background_tasks=True) +@pytest.mark.parametrize("mqtt_config_entry_data", [ENTRY_DEFAULT_BIRTH_MESSAGE]) async def test_publish( - hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator + hass: HomeAssistant, + setup_with_birth_msg_client_mock: MqttMockPahoClient, ) -> None: """Test the publish function.""" - mqtt_mock = await mqtt_mock_entry() - publish_mock: MagicMock = mqtt_mock._mqttc.publish + publish_mock: MagicMock = setup_with_birth_msg_client_mock.publish await mqtt.async_publish(hass, "test-topic", "test-payload") await hass.async_block_till_done() assert publish_mock.called @@ -292,7 +292,7 @@ async def test_publish( 0, False, ) - mqtt_mock.reset_mock() + publish_mock.reset_mock() # test null payload mqtt.publish( @@ -1100,42 +1100,40 @@ async def test_subscribe_mqtt_config_entry_disabled( await mqtt.async_subscribe(hass, "test-topic", record_calls) -@patch("homeassistant.components.mqtt.client.INITIAL_SUBSCRIBE_COOLDOWN", 0.0) -@patch("homeassistant.components.mqtt.client.UNSUBSCRIBE_COOLDOWN", 0.2) async def test_subscribe_and_resubscribe( hass: HomeAssistant, client_debug_log: None, - mqtt_mock_entry: MqttMockHAClientGenerator, - mqtt_client_mock: MqttMockPahoClient, + setup_with_birth_msg_client_mock: MqttMockPahoClient, recorded_calls: list[ReceiveMessage], record_calls: MessageCallbackType, ) -> None: """Test resubscribing within the debounce time.""" - await mqtt_mock_entry() + mqtt_client_mock = setup_with_birth_msg_client_mock + with ( + patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.4), + patch("homeassistant.components.mqtt.client.UNSUBSCRIBE_COOLDOWN", 0.4), + ): + unsub = await mqtt.async_subscribe(hass, "test-topic", record_calls) + # This unsub will be un-done with the following subscribe + # unsubscribe should not be called at the broker + unsub() + unsub = await mqtt.async_subscribe(hass, "test-topic", record_calls) - unsub = await mqtt.async_subscribe(hass, "test-topic", record_calls) - # This unsub will be un-done with the following subscribe - # unsubscribe should not be called at the broker - unsub() - await asyncio.sleep(0.1) - unsub = await mqtt.async_subscribe(hass, "test-topic", record_calls) - await asyncio.sleep(0.1) - await hass.async_block_till_done() + async_fire_mqtt_message(hass, "test-topic", "test-payload") + await asyncio.sleep(0.1) + await hass.async_block_till_done(wait_background_tasks=True) - async_fire_mqtt_message(hass, "test-topic", "test-payload") - await hass.async_block_till_done() + assert len(recorded_calls) == 1 + assert recorded_calls[0].topic == "test-topic" + assert recorded_calls[0].payload == "test-payload" + # assert unsubscribe was not called + mqtt_client_mock.unsubscribe.assert_not_called() - assert len(recorded_calls) == 1 - assert recorded_calls[0].topic == "test-topic" - assert recorded_calls[0].payload == "test-payload" - # assert unsubscribe was not called - mqtt_client_mock.unsubscribe.assert_not_called() + unsub() - unsub() - - await asyncio.sleep(0.2) - await hass.async_block_till_done() - mqtt_client_mock.unsubscribe.assert_called_once_with(["test-topic"]) + await asyncio.sleep(0.1) + await hass.async_block_till_done(wait_background_tasks=True) + mqtt_client_mock.unsubscribe.assert_called_once_with(["test-topic"]) async def test_subscribe_topic_non_async( @@ -1442,25 +1440,26 @@ async def test_subscribe_special_characters( @patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.0) async def test_subscribe_same_topic( hass: HomeAssistant, - mqtt_client_mock: MqttMockPahoClient, - mqtt_mock_entry: MqttMockHAClientGenerator, + setup_with_birth_msg_client_mock: MqttMockPahoClient, ) -> None: """Test subscribing to same topic twice and simulate retained messages. When subscribing to the same topic again, SUBSCRIBE must be sent to the broker again for it to resend any retained messages. """ - await mqtt_mock_entry() - + mqtt_client_mock = setup_with_birth_msg_client_mock calls_a: list[ReceiveMessage] = [] calls_b: list[ReceiveMessage] = [] + @callback def _callback_a(msg: ReceiveMessage) -> None: calls_a.append(msg) + @callback def _callback_b(msg: ReceiveMessage) -> None: calls_b.append(msg) + mqtt_client_mock.reset_mock() await mqtt.async_subscribe(hass, "test/state", _callback_a, qos=0) # Simulate a non retained message after the first subscription async_fire_mqtt_message(hass, "test/state", "online", qos=0, retain=False) @@ -1486,13 +1485,9 @@ async def test_subscribe_same_topic( mqtt_client_mock.subscribe.assert_called() -@patch("homeassistant.components.mqtt.client.INITIAL_SUBSCRIBE_COOLDOWN", 0.0) -@patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.0) -@patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.0) async def test_replaying_payload_same_topic( hass: HomeAssistant, - mqtt_client_mock: MqttMockPahoClient, - mqtt_mock_entry: MqttMockHAClientGenerator, + setup_with_birth_msg_client_mock: MqttMockPahoClient, ) -> None: """Test replaying retained messages. @@ -1501,22 +1496,26 @@ async def test_replaying_payload_same_topic( Retained messages must only be replayed for new subscriptions, except when the MQTT client is reconnecting. """ - await mqtt_mock_entry() - + mqtt_client_mock = setup_with_birth_msg_client_mock calls_a: list[ReceiveMessage] = [] calls_b: list[ReceiveMessage] = [] + @callback def _callback_a(msg: ReceiveMessage) -> None: calls_a.append(msg) + @callback def _callback_b(msg: ReceiveMessage) -> None: calls_b.append(msg) + mqtt_client_mock.reset_mock() await mqtt.async_subscribe(hass, "test/state", _callback_a) + await hass.async_block_till_done() async_fire_mqtt_message( hass, "test/state", "online", qos=0, retain=True ) # Simulate a (retained) message played back - await hass.async_block_till_done() + async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) + await asyncio.sleep(0) await hass.async_block_till_done() assert len(calls_a) == 1 @@ -1538,6 +1537,7 @@ async def test_replaying_payload_same_topic( # Make sure the debouncer delay was passed await hass.async_block_till_done() async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) + await asyncio.sleep(0) await hass.async_block_till_done() # The current subscription only received the message without retain flag @@ -1562,6 +1562,7 @@ async def test_replaying_payload_same_topic( async_fire_mqtt_message(hass, "test/state", "online", qos=0, retain=False) await hass.async_block_till_done() async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) + await asyncio.sleep(0) await hass.async_block_till_done() assert len(calls_a) == 1 assert help_assert_message(calls_a[0], "test/state", "online", qos=0, retain=False) @@ -1576,13 +1577,15 @@ async def test_replaying_payload_same_topic( mqtt_client_mock.on_connect(None, None, None, 0) await hass.async_block_till_done() async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) + await asyncio.sleep(0) await hass.async_block_till_done() mqtt_client_mock.subscribe.assert_called() # Simulate a (retained) message played back after reconnecting async_fire_mqtt_message(hass, "test/state", "online", qos=0, retain=True) await hass.async_block_till_done() async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) - await hass.async_block_till_done() + await asyncio.sleep(0) + await hass.async_block_till_done(wait_background_tasks=True) # Both subscriptions now should replay the retained message assert len(calls_a) == 1 assert help_assert_message(calls_a[0], "test/state", "online", qos=0, retain=True) @@ -1595,8 +1598,7 @@ async def test_replaying_payload_same_topic( @patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.0) async def test_replaying_payload_after_resubscribing( hass: HomeAssistant, - mqtt_client_mock: MqttMockPahoClient, - mqtt_mock_entry: MqttMockHAClientGenerator, + setup_with_birth_msg_client_mock: MqttMockPahoClient, ) -> None: """Test replaying and filtering retained messages after resubscribing. @@ -1605,13 +1607,14 @@ async def test_replaying_payload_after_resubscribing( Retained messages must only be replayed for new subscriptions, except when the MQTT client is reconnection. """ - await mqtt_mock_entry() - + mqtt_client_mock = setup_with_birth_msg_client_mock calls_a: list[ReceiveMessage] = [] + @callback def _callback_a(msg: ReceiveMessage) -> None: calls_a.append(msg) + mqtt_client_mock.reset_mock() unsub = await mqtt.async_subscribe(hass, "test/state", _callback_a) await hass.async_block_till_done() async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) @@ -1655,8 +1658,7 @@ async def test_replaying_payload_after_resubscribing( @patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.0) async def test_replaying_payload_wildcard_topic( hass: HomeAssistant, - mqtt_client_mock: MqttMockPahoClient, - mqtt_mock_entry: MqttMockHAClientGenerator, + setup_with_birth_msg_client_mock: MqttMockPahoClient, ) -> None: """Test replaying retained messages. @@ -1666,23 +1668,26 @@ async def test_replaying_payload_wildcard_topic( Retained messages should only be replayed for new subscriptions, except when the MQTT client is reconnection. """ - await mqtt_mock_entry() - + mqtt_client_mock = setup_with_birth_msg_client_mock calls_a: list[ReceiveMessage] = [] calls_b: list[ReceiveMessage] = [] + @callback def _callback_a(msg: ReceiveMessage) -> None: calls_a.append(msg) + @callback def _callback_b(msg: ReceiveMessage) -> None: calls_b.append(msg) + mqtt_client_mock.reset_mock() await mqtt.async_subscribe(hass, "test/#", _callback_a) # Simulate (retained) messages being played back on new subscriptions async_fire_mqtt_message(hass, "test/state1", "new_value_1", qos=0, retain=True) async_fire_mqtt_message(hass, "test/state2", "new_value_2", qos=0, retain=True) await hass.async_block_till_done() async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) # cooldown + await asyncio.sleep(0) await hass.async_block_till_done() assert len(calls_a) == 2 mqtt_client_mock.subscribe.assert_called() @@ -1696,6 +1701,7 @@ async def test_replaying_payload_wildcard_topic( async_fire_mqtt_message(hass, "test/state2", "initial_value_2", qos=0, retain=True) await hass.async_block_till_done() async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) # cooldown + await asyncio.sleep(0) await hass.async_block_till_done() # The retained messages playback should only be processed for the new subscriptions assert len(calls_a) == 0 @@ -1710,6 +1716,7 @@ async def test_replaying_payload_wildcard_topic( async_fire_mqtt_message(hass, "test/state1", "update_value_1", qos=0, retain=False) async_fire_mqtt_message(hass, "test/state2", "update_value_2", qos=0, retain=False) await hass.async_block_till_done() + await asyncio.sleep(0) assert len(calls_a) == 2 assert len(calls_b) == 2 @@ -1721,6 +1728,7 @@ async def test_replaying_payload_wildcard_topic( mqtt_client_mock.on_connect(None, None, None, 0) await hass.async_block_till_done() async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) # cooldown + await asyncio.sleep(0) await hass.async_block_till_done() mqtt_client_mock.subscribe.assert_called() # Simulate the (retained) messages are played back after reconnecting @@ -1729,40 +1737,38 @@ async def test_replaying_payload_wildcard_topic( async_fire_mqtt_message(hass, "test/state2", "update_value_2", qos=0, retain=True) await hass.async_block_till_done() async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) # cooldown - await hass.async_block_till_done() + await asyncio.sleep(0) + await hass.async_block_till_done(wait_background_tasks=True) # Both subscriptions should replay assert len(calls_a) == 2 assert len(calls_b) == 2 -@patch("homeassistant.components.mqtt.client.INITIAL_SUBSCRIBE_COOLDOWN", 0.0) -@patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.0) -@patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.0) async def test_not_calling_unsubscribe_with_active_subscribers( hass: HomeAssistant, - mqtt_client_mock: MqttMockPahoClient, - mqtt_mock_entry: MqttMockHAClientGenerator, + setup_with_birth_msg_client_mock: MqttMockPahoClient, record_calls: MessageCallbackType, ) -> None: """Test not calling unsubscribe() when other subscribers are active.""" - await mqtt_mock_entry() + mqtt_client_mock = setup_with_birth_msg_client_mock + mqtt_client_mock.reset_mock() unsub = await mqtt.async_subscribe(hass, "test/state", record_calls, 2) await mqtt.async_subscribe(hass, "test/state", record_calls, 1) await hass.async_block_till_done() - async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) # cooldown - await hass.async_block_till_done() + await asyncio.sleep(0) await hass.async_block_till_done() assert mqtt_client_mock.subscribe.called unsub() await hass.async_block_till_done() + async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) # cooldown + await hass.async_block_till_done(wait_background_tasks=True) assert not mqtt_client_mock.unsubscribe.called async def test_not_calling_subscribe_when_unsubscribed_within_cooldown( hass: HomeAssistant, - mqtt_client_mock: MqttMockPahoClient, - mqtt_mock_entry: MqttMockHAClientGenerator, + setup_with_birth_msg_client_mock: MqttMockPahoClient, record_calls: MessageCallbackType, ) -> None: """Test not calling subscribe() when it is unsubscribed. @@ -1770,7 +1776,7 @@ async def test_not_calling_subscribe_when_unsubscribed_within_cooldown( Make sure subscriptions are cleared if unsubscribed before the subscribe cool down period has ended. """ - await mqtt_mock_entry() + mqtt_client_mock = setup_with_birth_msg_client_mock mqtt_client_mock.subscribe.reset_mock() unsub = await mqtt.async_subscribe(hass, "test/state", record_calls) @@ -1780,26 +1786,20 @@ async def test_not_calling_subscribe_when_unsubscribed_within_cooldown( assert not mqtt_client_mock.subscribe.called -@patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.0) -@patch("homeassistant.components.mqtt.client.INITIAL_SUBSCRIBE_COOLDOWN", 0.0) -@patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.0) async def test_unsubscribe_race( hass: HomeAssistant, - mqtt_client_mock: MqttMockPahoClient, - mqtt_mock_entry: MqttMockHAClientGenerator, + setup_with_birth_msg_client_mock: MqttMockPahoClient, ) -> None: """Test not calling unsubscribe() when other subscribers are active.""" - await mqtt_mock_entry() - await hass.async_block_till_done() - async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) # cooldown - await hass.async_block_till_done() - + mqtt_client_mock = setup_with_birth_msg_client_mock calls_a: list[ReceiveMessage] = [] calls_b: list[ReceiveMessage] = [] + @callback def _callback_a(msg: ReceiveMessage) -> None: calls_a.append(msg) + @callback def _callback_b(msg: ReceiveMessage) -> None: calls_b.append(msg) @@ -1807,10 +1807,11 @@ async def test_unsubscribe_race( unsub = await mqtt.async_subscribe(hass, "test/state", _callback_a) unsub() await mqtt.async_subscribe(hass, "test/state", _callback_b) - await hass.async_block_till_done() + await asyncio.sleep(0) await hass.async_block_till_done() async_fire_mqtt_message(hass, "test/state", "online") + await asyncio.sleep(0) await hass.async_block_till_done() assert not calls_a assert calls_b @@ -1840,54 +1841,44 @@ async def test_unsubscribe_race( "mqtt_config_entry_data", [{mqtt.CONF_BROKER: "mock-broker", mqtt.CONF_DISCOVERY: False}], ) -@patch("homeassistant.components.mqtt.client.INITIAL_SUBSCRIBE_COOLDOWN", 0.0) -@patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.0) -@patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.0) async def test_restore_subscriptions_on_reconnect( hass: HomeAssistant, - mqtt_client_mock: MqttMockPahoClient, - mqtt_mock_entry: MqttMockHAClientGenerator, + setup_with_birth_msg_client_mock: MqttMockPahoClient, record_calls: MessageCallbackType, ) -> None: """Test subscriptions are restored on reconnect.""" - await mqtt_mock_entry() - await hass.async_block_till_done() - async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) # cooldown - await hass.async_block_till_done() - mqtt_client_mock.subscribe.reset_mock() + mqtt_client_mock = setup_with_birth_msg_client_mock + + mqtt_client_mock.reset_mock() await mqtt.async_subscribe(hass, "test/state", record_calls) async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) # cooldown + await asyncio.sleep(0) await hass.async_block_till_done() - assert mqtt_client_mock.subscribe.call_count == 1 + assert ("test/state", 0) in help_all_subscribe_calls(mqtt_client_mock) + mqtt_client_mock.reset_mock() mqtt_client_mock.on_disconnect(None, None, 0) mqtt_client_mock.on_connect(None, None, None, 0) await hass.async_block_till_done() - async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) # cooldown - await hass.async_block_till_done() - async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) # cooldown - await hass.async_block_till_done() - assert mqtt_client_mock.subscribe.call_count == 2 + await asyncio.sleep(0.1) + await hass.async_block_till_done(wait_background_tasks=True) + assert ("test/state", 0) in help_all_subscribe_calls(mqtt_client_mock) @pytest.mark.parametrize( "mqtt_config_entry_data", [{mqtt.CONF_BROKER: "mock-broker", mqtt.CONF_DISCOVERY: False}], ) -@patch("homeassistant.components.mqtt.client.INITIAL_SUBSCRIBE_COOLDOWN", 1.0) -@patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.0) -@patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 1.0) async def test_restore_all_active_subscriptions_on_reconnect( hass: HomeAssistant, - mqtt_client_mock: MqttMockPahoClient, - mqtt_mock_entry: MqttMockHAClientGenerator, + setup_with_birth_msg_client_mock: MqttMockPahoClient, record_calls: MessageCallbackType, freezer: FrozenDateTimeFactory, ) -> None: """Test active subscriptions are restored correctly on reconnect.""" - await mqtt_mock_entry() - + mqtt_client_mock = setup_with_birth_msg_client_mock + mqtt_client_mock.reset_mock() unsub = await mqtt.async_subscribe(hass, "test/state", record_calls, qos=2) await mqtt.async_subscribe(hass, "test/state", record_calls, qos=1) await mqtt.async_subscribe(hass, "test/state", record_calls, qos=0) @@ -1914,14 +1905,15 @@ async def test_restore_all_active_subscriptions_on_reconnect( await hass.async_block_till_done() expected.append(call([("test/state", 1)])) - assert mqtt_client_mock.subscribe.mock_calls == expected + for expected_call in expected: + assert mqtt_client_mock.subscribe.hass_call(expected_call) freezer.tick(3) async_fire_time_changed(hass) # cooldown await hass.async_block_till_done() freezer.tick(3) async_fire_time_changed(hass) # cooldown - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) @pytest.mark.parametrize( @@ -1933,14 +1925,13 @@ async def test_restore_all_active_subscriptions_on_reconnect( @patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 1.0) async def test_subscribed_at_highest_qos( hass: HomeAssistant, - mqtt_client_mock: MqttMockPahoClient, - mqtt_mock_entry: MqttMockHAClientGenerator, + setup_with_birth_msg_client_mock: MqttMockPahoClient, record_calls: MessageCallbackType, freezer: FrozenDateTimeFactory, ) -> None: """Test the highest qos as assigned when subscribing to the same topic.""" - await mqtt_mock_entry() - + mqtt_client_mock = setup_with_birth_msg_client_mock + mqtt_client_mock.reset_mock() await mqtt.async_subscribe(hass, "test/state", record_calls, qos=0) await hass.async_block_till_done() freezer.tick(5) @@ -1974,7 +1965,6 @@ async def test_reload_entry_with_restored_subscriptions( entry = MockConfigEntry(domain=mqtt.DOMAIN, data={mqtt.CONF_BROKER: "test-broker"}) entry.add_to_hass(hass) hass.config.components.add(mqtt.DOMAIN) - mqtt_client_mock.connect.return_value = 0 with patch("homeassistant.config.load_yaml_config_file", return_value={}): await hass.config_entries.async_setup(entry.entry_id) @@ -2026,49 +2016,42 @@ async def test_reload_entry_with_restored_subscriptions( assert recorded_calls[1].payload == "wild-card-payload3" -@patch("homeassistant.components.mqtt.client.INITIAL_SUBSCRIBE_COOLDOWN", 2) -@patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 2) -@patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 2) async def test_canceling_debouncer_on_shutdown( hass: HomeAssistant, record_calls: MessageCallbackType, - mqtt_client_mock: MqttMockPahoClient, - mqtt_mock_entry: MqttMockHAClientGenerator, + setup_with_birth_msg_client_mock: MqttMockPahoClient, ) -> None: """Test canceling the debouncer when HA shuts down.""" + mqtt_client_mock = setup_with_birth_msg_client_mock - await mqtt_mock_entry() - mqtt_client_mock.subscribe.reset_mock() + with patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 2): + await hass.async_block_till_done() + async_fire_time_changed(hass, utcnow() + timedelta(seconds=5)) + await hass.async_block_till_done() + await mqtt.async_subscribe(hass, "test/state1", record_calls) + async_fire_time_changed(hass, utcnow() + timedelta(seconds=0.1)) + # Stop HA so the scheduled debouncer task will be canceled + mqtt_client_mock.subscribe.reset_mock() + hass.bus.fire(EVENT_HOMEASSISTANT_STOP) + await mqtt.async_subscribe(hass, "test/state2", record_calls) + async_fire_time_changed(hass, utcnow() + timedelta(seconds=0.1)) + await mqtt.async_subscribe(hass, "test/state3", record_calls) + async_fire_time_changed(hass, utcnow() + timedelta(seconds=0.1)) + await mqtt.async_subscribe(hass, "test/state4", record_calls) + async_fire_time_changed(hass, utcnow() + timedelta(seconds=0.1)) + await mqtt.async_subscribe(hass, "test/state5", record_calls) + async_fire_time_changed(hass, utcnow() + timedelta(seconds=0.1)) + await hass.async_block_till_done() - await mqtt.async_subscribe(hass, "test/state1", record_calls) - async_fire_time_changed(hass, utcnow() + timedelta(seconds=0.2)) - await hass.async_block_till_done() + mqtt_client_mock.subscribe.assert_not_called() - await mqtt.async_subscribe(hass, "test/state2", record_calls) - async_fire_time_changed(hass, utcnow() + timedelta(seconds=0.2)) - await hass.async_block_till_done() - - await mqtt.async_subscribe(hass, "test/state3", record_calls) - async_fire_time_changed(hass, utcnow() + timedelta(seconds=0.2)) - await hass.async_block_till_done() - - await mqtt.async_subscribe(hass, "test/state4", record_calls) - async_fire_time_changed(hass, utcnow() + timedelta(seconds=0.2)) - await hass.async_block_till_done() - - await mqtt.async_subscribe(hass, "test/state5", record_calls) - - mqtt_client_mock.subscribe.assert_not_called() - - # Stop HA so the scheduled task will be canceled - hass.bus.fire(EVENT_HOMEASSISTANT_STOP) - # mock disconnect status - mqtt_client_mock.on_disconnect(None, None, 0) - await hass.async_block_till_done() - await hass.async_block_till_done() - async_fire_time_changed(hass, utcnow() + timedelta(seconds=5)) - await hass.async_block_till_done() - mqtt_client_mock.subscribe.assert_not_called() + # Note thet the broker connection will not be disconnected gracefully + await hass.async_block_till_done() + async_fire_time_changed(hass, utcnow() + timedelta(seconds=5)) + await asyncio.sleep(0) + await hass.async_block_till_done(wait_background_tasks=True) + mqtt_client_mock.subscribe.assert_not_called() + mqtt_client_mock.disconnect.assert_not_called() async def test_canceling_debouncer_normal( @@ -2130,13 +2113,13 @@ async def test_initial_setup_logs_error( async def test_logs_error_if_no_connect_broker( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, - mqtt_mock_entry: MqttMockHAClientGenerator, - mqtt_client_mock: MqttMockPahoClient, + setup_with_birth_msg_client_mock: MqttMockPahoClient, ) -> None: """Test for setup failure if connection to broker is missing.""" - await mqtt_mock_entry() + mqtt_client_mock = setup_with_birth_msg_client_mock # test with rc = 3 -> broker unavailable - mqtt_client_mock.on_connect(mqtt_client_mock, None, None, 3) + mqtt_client_mock.on_disconnect(Mock(), None, 0) + mqtt_client_mock.on_connect(Mock(), None, None, 3) await hass.async_block_till_done() assert ( "Unable to connect to the MQTT broker: Connection Refused: broker unavailable." @@ -2148,14 +2131,14 @@ async def test_logs_error_if_no_connect_broker( async def test_triggers_reauth_flow_if_auth_fails( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, - mqtt_mock_entry: MqttMockHAClientGenerator, - mqtt_client_mock: MqttMockPahoClient, + setup_with_birth_msg_client_mock: MqttMockPahoClient, return_code: int, ) -> None: """Test re-auth is triggered if authentication is failing.""" - await mqtt_mock_entry() + mqtt_client_mock = setup_with_birth_msg_client_mock # test with rc = 4 -> CONNACK_REFUSED_NOT_AUTHORIZED and 5 -> CONNACK_REFUSED_BAD_USERNAME_PASSWORD - mqtt_client_mock.on_connect(mqtt_client_mock, None, None, return_code) + mqtt_client_mock.on_disconnect(Mock(), None, 0) + mqtt_client_mock.on_connect(Mock(), None, None, return_code) await hass.async_block_till_done() flows = hass.config_entries.flow.async_progress() assert len(flows) == 1 @@ -2166,11 +2149,10 @@ async def test_triggers_reauth_flow_if_auth_fails( async def test_handle_mqtt_on_callback( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, - mqtt_mock_entry: MqttMockHAClientGenerator, - mqtt_client_mock: MqttMockPahoClient, + setup_with_birth_msg_client_mock: MqttMockPahoClient, ) -> None: """Test receiving an ACK callback before waiting for it.""" - await mqtt_mock_entry() + mqtt_client_mock = setup_with_birth_msg_client_mock with patch.object(mqtt_client_mock, "get_mid", return_value=100): # Simulate an ACK for mid == 100, this will call mqtt_mock._async_get_mid_future(mid) mqtt_client_mock.on_publish(mqtt_client_mock, None, 100) @@ -2222,51 +2204,47 @@ async def test_publish_error( assert "Failed to connect to MQTT server: Out of memory." in caplog.text -@patch("homeassistant.components.mqtt.client.INITIAL_SUBSCRIBE_COOLDOWN", 0.0) async def test_subscribe_error( hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - mqtt_client_mock: MqttMockPahoClient, + setup_with_birth_msg_client_mock: MqttMockPahoClient, record_calls: MessageCallbackType, caplog: pytest.LogCaptureFixture, ) -> None: """Test publish error.""" - await mqtt_mock_entry() - mqtt_client_mock.on_connect(mqtt_client_mock, None, None, 0) - await hass.async_block_till_done() - await hass.async_block_till_done() + mqtt_client_mock = setup_with_birth_msg_client_mock mqtt_client_mock.reset_mock() # simulate client is not connected error before subscribing mqtt_client_mock.subscribe.side_effect = lambda *args: (4, None) - await mqtt.async_subscribe(hass, "some-topic", record_calls) - while mqtt_client_mock.subscribe.call_count == 0: + with patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.0): + await mqtt.async_subscribe(hass, "some-topic", record_calls) + while mqtt_client_mock.subscribe.call_count == 0: + await hass.async_block_till_done() await hass.async_block_till_done() - await hass.async_block_till_done() - await hass.async_block_till_done() - assert ( - "Error talking to MQTT: The client is not currently connected." in caplog.text - ) + await hass.async_block_till_done() + assert ( + "Error talking to MQTT: The client is not currently connected." + in caplog.text + ) async def test_handle_message_callback( hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - mqtt_client_mock: MqttMockPahoClient, + setup_with_birth_msg_client_mock: MqttMockPahoClient, ) -> None: """Test for handling an incoming message callback.""" + mqtt_client_mock = setup_with_birth_msg_client_mock callbacks = [] @callback def _callback(args) -> None: callbacks.append(args) - mock_mqtt = await mqtt_mock_entry() msg = ReceiveMessage( "some-topic", b"test-payload", 1, False, "some-topic", datetime.now() ) - mqtt_client_mock.on_connect(mqtt_client_mock, None, None, 0) await mqtt.async_subscribe(hass, "some-topic", _callback) - mqtt_client_mock.on_message(mock_mqtt, None, msg) + mqtt_client_mock.reset_mock() + mqtt_client_mock.on_message(None, None, msg) await hass.async_block_till_done() await hass.async_block_till_done() @@ -2395,8 +2373,6 @@ async def test_handle_mqtt_timeout_on_callback( ) entry.add_to_hass(hass) - # Make sure we are connected correctly - mock_client.on_connect(mock_client, None, None, 0) # Set up the integration assert await hass.config_entries.async_setup(entry.entry_id) @@ -2506,62 +2482,48 @@ async def test_tls_version( } ], ) +@patch("homeassistant.components.mqtt.client.INITIAL_SUBSCRIBE_COOLDOWN", 0.0) +@patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.0) +@patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.0) async def test_custom_birth_message( hass: HomeAssistant, + mqtt_config_entry_data: dict[str, Any], mqtt_client_mock: MqttMockPahoClient, - mqtt_mock_entry: MqttMockHAClientGenerator, ) -> None: """Test sending birth message.""" - await mqtt_mock_entry() - birth = asyncio.Event() - async def wait_birth(msg: ReceiveMessage) -> None: + birth = asyncio.Event() + entry = MockConfigEntry(domain=mqtt.DOMAIN, data=mqtt_config_entry_data) + entry.add_to_hass(hass) + hass.config.components.add(mqtt.DOMAIN) + assert await hass.config_entries.async_setup(entry.entry_id) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + + @callback + def wait_birth(msg: ReceiveMessage) -> None: """Handle birth message.""" birth.set() - with patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.1): - await mqtt.async_subscribe(hass, "birth", wait_birth) - mqtt_client_mock.on_connect(None, None, 0, 0) - await hass.async_block_till_done() - await birth.wait() - mqtt_client_mock.publish.assert_called_with("birth", "birth", 0, False) + await mqtt.async_subscribe(hass, "birth", wait_birth) + await hass.async_block_till_done() + await birth.wait() + mqtt_client_mock.publish.assert_called_with("birth", "birth", 0, False) @pytest.mark.parametrize( "mqtt_config_entry_data", - [ - { - mqtt.CONF_BROKER: "mock-broker", - mqtt.CONF_BIRTH_MESSAGE: { - mqtt.ATTR_TOPIC: "homeassistant/status", - mqtt.ATTR_PAYLOAD: "online", - mqtt.ATTR_QOS: 0, - mqtt.ATTR_RETAIN: False, - }, - } - ], + [ENTRY_DEFAULT_BIRTH_MESSAGE], ) async def test_default_birth_message( hass: HomeAssistant, - mqtt_client_mock: MqttMockPahoClient, - mqtt_mock_entry: MqttMockHAClientGenerator, + setup_with_birth_msg_client_mock: MqttMockPahoClient, ) -> None: """Test sending birth message.""" - await mqtt_mock_entry() - birth = asyncio.Event() - - async def wait_birth(msg: ReceiveMessage) -> None: - """Handle birth message.""" - birth.set() - - with patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.1): - await mqtt.async_subscribe(hass, "homeassistant/status", wait_birth) - mqtt_client_mock.on_connect(None, None, 0, 0) - await hass.async_block_till_done() - await birth.wait() - mqtt_client_mock.publish.assert_called_with( - "homeassistant/status", "online", 0, False - ) + mqtt_client_mock = setup_with_birth_msg_client_mock + await hass.async_block_till_done(wait_background_tasks=True) + mqtt_client_mock.publish.assert_called_with( + "homeassistant/status", "online", 0, False + ) @pytest.mark.parametrize( @@ -2573,28 +2535,30 @@ async def test_default_birth_message( @patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.0) async def test_no_birth_message( hass: HomeAssistant, + mqtt_config_entry_data: dict[str, Any], mqtt_client_mock: MqttMockPahoClient, - mqtt_mock_entry: MqttMockHAClientGenerator, ) -> None: """Test disabling birth message.""" - await mqtt_mock_entry() + entry = MockConfigEntry(domain=mqtt.DOMAIN, data=mqtt_config_entry_data) + entry.add_to_hass(hass) + hass.config.components.add(mqtt.DOMAIN) + assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) await hass.async_block_till_done() mqtt_client_mock.reset_mock() - # Assert no birth message was sent - mqtt_client_mock.on_connect(None, None, 0, 0) await hass.async_block_till_done() async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) await hass.async_block_till_done() mqtt_client_mock.publish.assert_not_called() - async def callback(msg: ReceiveMessage) -> None: - """Handle birth message.""" + @callback + def msg_callback(msg: ReceiveMessage) -> None: + """Handle callback.""" mqtt_client_mock.reset_mock() - await mqtt.async_subscribe(hass, "homeassistant/some-topic", callback) + await mqtt.async_subscribe(hass, "homeassistant/some-topic", msg_callback) await hass.async_block_till_done() async_fire_time_changed(hass, utcnow() + timedelta(seconds=5)) await hass.async_block_till_done() @@ -2603,130 +2567,61 @@ async def test_no_birth_message( @pytest.mark.parametrize( "mqtt_config_entry_data", - [ - { - mqtt.CONF_BROKER: "mock-broker", - mqtt.CONF_BIRTH_MESSAGE: { - mqtt.ATTR_TOPIC: "homeassistant/status", - mqtt.ATTR_PAYLOAD: "online", - mqtt.ATTR_QOS: 0, - mqtt.ATTR_RETAIN: False, - }, - } - ], + [ENTRY_DEFAULT_BIRTH_MESSAGE], ) +@patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.2) async def test_delayed_birth_message( hass: HomeAssistant, + mqtt_config_entry_data: dict[str, Any], mqtt_client_mock: MqttMockPahoClient, - mqtt_config_entry_data, - mqtt_mock_entry: MqttMockHAClientGenerator, ) -> None: """Test sending birth message does not happen until Home Assistant starts.""" - mqtt_mock = await mqtt_mock_entry() - hass.set_state(CoreState.starting) - birth = asyncio.Event() - await hass.async_block_till_done() - + birth = asyncio.Event() entry = MockConfigEntry(domain=mqtt.DOMAIN, data=mqtt_config_entry_data) entry.add_to_hass(hass) + hass.config.components.add(mqtt.DOMAIN) assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - mqtt_component_mock = MagicMock( - return_value=hass.data["mqtt"].client, - wraps=hass.data["mqtt"].client, - ) - mqtt_component_mock._mqttc = mqtt_client_mock - - hass.data["mqtt"].client = mqtt_component_mock - mqtt_mock = hass.data["mqtt"].client - mqtt_mock.reset_mock() - - async def wait_birth(msg: ReceiveMessage) -> None: - """Handle birth message.""" - birth.set() - - with patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.1): - await mqtt.async_subscribe(hass, "homeassistant/status", wait_birth) - mqtt_client_mock.on_connect(None, None, 0, 0) - await hass.async_block_till_done() - with pytest.raises(TimeoutError): - await asyncio.wait_for(birth.wait(), 0.2) - assert not mqtt_client_mock.publish.called - assert not birth.is_set() - - hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) - await birth.wait() - mqtt_client_mock.publish.assert_called_with( - "homeassistant/status", "online", 0, False - ) - - -@pytest.mark.parametrize( - "mqtt_config_entry_data", - [ - { - mqtt.CONF_BROKER: "mock-broker", - mqtt.CONF_BIRTH_MESSAGE: { - mqtt.ATTR_TOPIC: "homeassistant/status", - mqtt.ATTR_PAYLOAD: "online", - mqtt.ATTR_QOS: 0, - mqtt.ATTR_RETAIN: False, - }, - } - ], -) -@patch("homeassistant.components.mqtt.client.INITIAL_SUBSCRIBE_COOLDOWN", 0.0) -@patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.0) -@patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.0) -async def test_subscription_done_when_birth_message_is_sent( - hass: HomeAssistant, - mqtt_client_mock: MqttMockPahoClient, - record_calls: MessageCallbackType, - mqtt_config_entry_data, -) -> None: - """Test sending birth message until initial subscription has been completed.""" - hass.set_state(CoreState.starting) - birth = asyncio.Event() - - await hass.async_block_till_done() - - entry = MockConfigEntry(domain=mqtt.DOMAIN, data=mqtt_config_entry_data) - entry.add_to_hass(hass) - assert await hass.config_entries.async_setup(entry.entry_id) - hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) - await hass.async_block_till_done() - async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) - await hass.async_block_till_done() - mqtt_client_mock.on_disconnect(None, None, 0, 0) - await hass.async_block_till_done() - async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) - await hass.async_block_till_done() - @callback def wait_birth(msg: ReceiveMessage) -> None: """Handle birth message.""" birth.set() - await mqtt.async_subscribe(hass, "topic/test", record_calls) await mqtt.async_subscribe(hass, "homeassistant/status", wait_birth) await hass.async_block_till_done() - mqtt_client_mock.reset_mock() - mqtt_client_mock.on_connect(None, None, 0, 0) - # We wait until we receive a birth message - await asyncio.wait_for(birth.wait(), 1) + with pytest.raises(TimeoutError): + await asyncio.wait_for(birth.wait(), 0.05) + assert not mqtt_client_mock.publish.called + assert not birth.is_set() - # Assert we already have subscribed at the client - # for new config payloads at the time we the birth message is received + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await birth.wait() + mqtt_client_mock.publish.assert_called_with( + "homeassistant/status", "online", 0, False + ) + + +@pytest.mark.parametrize( + "mqtt_config_entry_data", + [ENTRY_DEFAULT_BIRTH_MESSAGE], +) +async def test_subscription_done_when_birth_message_is_sent( + hass: HomeAssistant, + mqtt_config_entry_data: dict[str, Any], + setup_with_birth_msg_client_mock: MqttMockPahoClient, + record_calls: MessageCallbackType, +) -> None: + """Test sending birth message until initial subscription has been completed.""" + mqtt_client_mock = setup_with_birth_msg_client_mock subscribe_calls = help_all_subscribe_calls(mqtt_client_mock) assert ("homeassistant/+/+/config", 0) in subscribe_calls assert ("homeassistant/+/+/+/config", 0) in subscribe_calls mqtt_client_mock.publish.assert_called_with( "homeassistant/status", "online", 0, False ) - assert ("topic/test", 0) in subscribe_calls @pytest.mark.parametrize( @@ -2745,11 +2640,15 @@ async def test_subscription_done_when_birth_message_is_sent( ) async def test_custom_will_message( hass: HomeAssistant, + mqtt_config_entry_data: dict[str, Any], mqtt_client_mock: MqttMockPahoClient, - mqtt_mock_entry: MqttMockHAClientGenerator, ) -> None: """Test will message.""" - await mqtt_mock_entry() + entry = MockConfigEntry(domain=mqtt.DOMAIN, data=mqtt_config_entry_data) + entry.add_to_hass(hass) + hass.config.components.add(mqtt.DOMAIN) + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() mqtt_client_mock.will_set.assert_called_with( topic="death", payload="death", qos=0, retain=False @@ -2758,12 +2657,10 @@ async def test_custom_will_message( async def test_default_will_message( hass: HomeAssistant, - mqtt_client_mock: MqttMockPahoClient, - mqtt_mock_entry: MqttMockHAClientGenerator, + setup_with_birth_msg_client_mock: MqttMockPahoClient, ) -> None: """Test will message.""" - await mqtt_mock_entry() - + mqtt_client_mock = setup_with_birth_msg_client_mock mqtt_client_mock.will_set.assert_called_with( topic="homeassistant/status", payload="offline", qos=0, retain=False ) @@ -2775,56 +2672,47 @@ async def test_default_will_message( ) async def test_no_will_message( hass: HomeAssistant, + mqtt_config_entry_data: dict[str, Any], mqtt_client_mock: MqttMockPahoClient, - mqtt_mock_entry: MqttMockHAClientGenerator, ) -> None: """Test will message.""" - await mqtt_mock_entry() + entry = MockConfigEntry(domain=mqtt.DOMAIN, data=mqtt_config_entry_data) + entry.add_to_hass(hass) + hass.config.components.add(mqtt.DOMAIN) + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() mqtt_client_mock.will_set.assert_not_called() @pytest.mark.parametrize( "mqtt_config_entry_data", - [ - { - mqtt.CONF_BROKER: "mock-broker", - mqtt.CONF_BIRTH_MESSAGE: {}, - mqtt.CONF_DISCOVERY: False, - } - ], + [ENTRY_DEFAULT_BIRTH_MESSAGE | {mqtt.CONF_DISCOVERY: False}], ) -@patch("homeassistant.components.mqtt.client.INITIAL_SUBSCRIBE_COOLDOWN", 0.0) -@patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.0) -@patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.0) async def test_mqtt_subscribes_topics_on_connect( hass: HomeAssistant, - mqtt_client_mock: MqttMockPahoClient, - mqtt_mock_entry: MqttMockHAClientGenerator, + setup_with_birth_msg_client_mock: MqttMockPahoClient, record_calls: MessageCallbackType, ) -> None: """Test subscription to topic on connect.""" - await mqtt_mock_entry() - await hass.async_block_till_done() - async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) - await hass.async_block_till_done() - mqtt_client_mock.reset_mock() + mqtt_client_mock = setup_with_birth_msg_client_mock await mqtt.async_subscribe(hass, "topic/test", record_calls) await mqtt.async_subscribe(hass, "home/sensor", record_calls, 2) await mqtt.async_subscribe(hass, "still/pending", record_calls) await mqtt.async_subscribe(hass, "still/pending", record_calls, 1) - mqtt_client_mock.on_connect(None, None, 0, 0) + mqtt_client_mock.on_disconnect(Mock(), None, 0) + + mqtt_client_mock.reset_mock() + + mqtt_client_mock.on_connect(Mock(), None, 0, 0) await hass.async_block_till_done() async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) - await hass.async_block_till_done() - - assert mqtt_client_mock.disconnect.call_count == 0 + await hass.async_block_till_done(wait_background_tasks=True) subscribe_calls = help_all_subscribe_calls(mqtt_client_mock) - assert len(subscribe_calls) == 3 assert ("topic/test", 0) in subscribe_calls assert ("home/sensor", 2) in subscribe_calls assert ("still/pending", 1) in subscribe_calls @@ -2832,31 +2720,21 @@ async def test_mqtt_subscribes_topics_on_connect( @pytest.mark.parametrize( "mqtt_config_entry_data", - [ - { - mqtt.CONF_BROKER: "mock-broker", - mqtt.CONF_BIRTH_MESSAGE: {}, - mqtt.CONF_DISCOVERY: False, - } - ], + [ENTRY_DEFAULT_BIRTH_MESSAGE], ) -@patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.0) -@patch("homeassistant.components.mqtt.client.INITIAL_SUBSCRIBE_COOLDOWN", 0.0) async def test_mqtt_subscribes_in_single_call( hass: HomeAssistant, - mqtt_client_mock: MqttMockPahoClient, - mqtt_mock_entry: MqttMockHAClientGenerator, + setup_with_birth_msg_client_mock: MqttMockPahoClient, record_calls: MessageCallbackType, ) -> None: """Test bundled client subscription to topic.""" - await mqtt_mock_entry() - + mqtt_client_mock = setup_with_birth_msg_client_mock mqtt_client_mock.subscribe.reset_mock() await mqtt.async_subscribe(hass, "topic/test", record_calls) await mqtt.async_subscribe(hass, "home/sensor", record_calls) - await hass.async_block_till_done() # Make sure the debouncer finishes - await asyncio.sleep(0.2) + await asyncio.sleep(0) + await hass.async_block_till_done(wait_background_tasks=True) assert mqtt_client_mock.subscribe.call_count == 1 # Assert we have a single subscription call with both subscriptions @@ -2866,28 +2744,16 @@ async def test_mqtt_subscribes_in_single_call( ] -@pytest.mark.parametrize( - "mqtt_config_entry_data", - [ - { - mqtt.CONF_BROKER: "mock-broker", - mqtt.CONF_BIRTH_MESSAGE: {}, - mqtt.CONF_DISCOVERY: False, - } - ], -) +@pytest.mark.parametrize("mqtt_config_entry_data", [ENTRY_DEFAULT_BIRTH_MESSAGE]) @patch("homeassistant.components.mqtt.client.MAX_SUBSCRIBES_PER_CALL", 2) @patch("homeassistant.components.mqtt.client.MAX_UNSUBSCRIBES_PER_CALL", 2) -@patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.0) -@patch("homeassistant.components.mqtt.client.INITIAL_SUBSCRIBE_COOLDOWN", 0.0) async def test_mqtt_subscribes_and_unsubscribes_in_chunks( hass: HomeAssistant, - mqtt_client_mock: MqttMockPahoClient, - mqtt_mock_entry: MqttMockHAClientGenerator, + setup_with_birth_msg_client_mock: MqttMockPahoClient, record_calls: MessageCallbackType, ) -> None: """Test chunked client subscriptions.""" - await mqtt_mock_entry() + mqtt_client_mock = setup_with_birth_msg_client_mock mqtt_client_mock.subscribe.reset_mock() unsub_tasks: list[CALLBACK_TYPE] = [] @@ -2895,9 +2761,9 @@ async def test_mqtt_subscribes_and_unsubscribes_in_chunks( unsub_tasks.append(await mqtt.async_subscribe(hass, "home/sensor1", record_calls)) unsub_tasks.append(await mqtt.async_subscribe(hass, "topic/test2", record_calls)) unsub_tasks.append(await mqtt.async_subscribe(hass, "home/sensor2", record_calls)) - await hass.async_block_till_done() # Make sure the debouncer finishes - await asyncio.sleep(0.2) + await asyncio.sleep(0.1) + await hass.async_block_till_done(wait_background_tasks=True) assert mqtt_client_mock.subscribe.call_count == 2 # Assert we have a 2 subscription calls with both 2 subscriptions @@ -2909,7 +2775,8 @@ async def test_mqtt_subscribes_and_unsubscribes_in_chunks( task() await hass.async_block_till_done() # Make sure the debouncer finishes - await asyncio.sleep(0.2) + await asyncio.sleep(0.1) + await hass.async_block_till_done(wait_background_tasks=True) assert mqtt_client_mock.unsubscribe.call_count == 2 # Assert we have a 2 unsubscribe calls with both 2 topic @@ -2920,7 +2787,6 @@ async def test_mqtt_subscribes_and_unsubscribes_in_chunks( async def test_default_entry_setting_are_applied( hass: HomeAssistant, device_registry: dr.DeviceRegistry, - mqtt_mock_entry: MqttMockHAClientGenerator, mqtt_client_mock: MqttMockPahoClient, caplog: pytest.LogCaptureFixture, ) -> None: @@ -2932,11 +2798,12 @@ async def test_default_entry_setting_are_applied( ) # Config entry data is incomplete but valid according the schema - entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0] - hass.config_entries.async_update_entry( - entry, data={"broker": "test-broker", "port": 1234} + entry = MockConfigEntry( + domain=mqtt.DOMAIN, data={"broker": "test-broker", "port": 1234} ) - await mqtt_mock_entry() + entry.add_to_hass(hass) + hass.config.components.add(mqtt.DOMAIN) + assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() # Discover a device to verify the entry was setup correctly @@ -2977,10 +2844,9 @@ async def test_message_callback_exception_gets_logged( async def test_message_partial_callback_exception_gets_logged( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, - mqtt_mock_entry: MqttMockHAClientGenerator, + setup_with_birth_msg_client_mock: MqttMockPahoClient, ) -> None: """Test exception raised by message handler.""" - await mqtt_mock_entry() @callback def bad_handler(msg: ReceiveMessage) -> None: @@ -2998,8 +2864,12 @@ async def test_message_partial_callback_exception_gets_logged( await mqtt.async_subscribe( hass, "test-topic", partial(parial_handler, bad_handler, {"some_attr"}) ) + await hass.async_block_till_done(wait_background_tasks=True) async_fire_mqtt_message(hass, "test-topic", "test") await hass.async_block_till_done() + await hass.async_block_till_done() + await asyncio.sleep(0) + await hass.async_block_till_done(wait_background_tasks=True) assert ( "Exception in bad_handler when handling msg on 'test-topic':" @@ -3726,11 +3596,10 @@ async def test_publish_json_from_template( async def test_subscribe_connection_status( hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - mqtt_client_mock: MqttMockPahoClient, + setup_with_birth_msg_client_mock: MqttMockPahoClient, ) -> None: """Test connextion status subscription.""" - mqtt_mock = await mqtt_mock_entry() + mqtt_client_mock = setup_with_birth_msg_client_mock mqtt_connected_calls_callback: list[bool] = [] mqtt_connected_calls_async: list[bool] = [] @@ -3743,7 +3612,13 @@ async def test_subscribe_connection_status( """Update state on connection/disconnection to MQTT broker.""" mqtt_connected_calls_async.append(status) - mqtt_mock.connected = True + # Check connection status + assert mqtt.is_connected(hass) is True + + # Mock disconnect status + mqtt_client_mock.on_disconnect(None, None, 0) + await hass.async_block_till_done() + assert mqtt.is_connected(hass) is False unsub_callback = mqtt.async_subscribe_connection_status( hass, async_mqtt_connected_callback @@ -3753,7 +3628,7 @@ async def test_subscribe_connection_status( ) await hass.async_block_till_done() - # Mock connection status + # Mock connect status mqtt_client_mock.on_connect(None, None, 0, 0) await hass.async_block_till_done() assert mqtt.is_connected(hass) is True @@ -3761,13 +3636,17 @@ async def test_subscribe_connection_status( # Mock disconnect status mqtt_client_mock.on_disconnect(None, None, 0) await hass.async_block_till_done() + assert mqtt.is_connected(hass) is False # Unsubscribe unsub_callback() unsub_async() + # Mock connect status mqtt_client_mock.on_connect(None, None, 0, 0) + await asyncio.sleep(0) await hass.async_block_till_done() + assert mqtt.is_connected(hass) is True # Check calls assert len(mqtt_connected_calls_callback) == 2 @@ -3781,11 +3660,11 @@ async def test_subscribe_connection_status( async def test_unload_config_entry( hass: HomeAssistant, - mqtt_mock: MqttMockHAClient, - mqtt_client_mock: MqttMockPahoClient, + setup_with_birth_msg_client_mock: MqttMockPahoClient, caplog: pytest.LogCaptureFixture, ) -> None: """Test unloading the MQTT entry.""" + mqtt_client_mock = setup_with_birth_msg_client_mock assert hass.services.has_service(mqtt.DOMAIN, "dump") assert hass.services.has_service(mqtt.DOMAIN, "publish") @@ -4015,6 +3894,7 @@ async def test_link_config_entry( mqtt_config_entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0] mqtt_platforms = async_get_platforms(hass, mqtt.DOMAIN) + @callback def _check_entities() -> int: entities: list[Entity] = [] for mqtt_platform in mqtt_platforms: @@ -4096,6 +3976,7 @@ async def test_reload_config_entry( entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0] + @callback def _check_entities() -> int: entities: list[Entity] = [] mqtt_platforms = async_get_platforms(hass, mqtt.DOMAIN) @@ -4406,19 +4287,14 @@ async def test_multi_platform_discovery( ) -@patch("homeassistant.components.mqtt.client.INITIAL_SUBSCRIBE_COOLDOWN", 0.0) -@patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.0) -@patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.0) async def test_auto_reconnect( hass: HomeAssistant, - mqtt_client_mock: MqttMockPahoClient, - mqtt_mock_entry: MqttMockHAClientGenerator, + setup_with_birth_msg_client_mock: MqttMockPahoClient, caplog: pytest.LogCaptureFixture, ) -> None: """Test reconnection is automatically done.""" - mqtt_mock = await mqtt_mock_entry() - await hass.async_block_till_done() - assert mqtt_mock.connected is True + mqtt_client_mock = setup_with_birth_msg_client_mock + assert mqtt_client_mock.connect.call_count == 1 mqtt_client_mock.reconnect.reset_mock() mqtt_client_mock.disconnect() @@ -4454,20 +4330,15 @@ async def test_auto_reconnect( assert len(mqtt_client_mock.reconnect.mock_calls) == 2 -@patch("homeassistant.components.mqtt.client.INITIAL_SUBSCRIBE_COOLDOWN", 0.0) -@patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.0) -@patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.0) async def test_server_sock_connect_and_disconnect( hass: HomeAssistant, - mqtt_client_mock: MqttMockPahoClient, - mqtt_mock_entry: MqttMockHAClientGenerator, + setup_with_birth_msg_client_mock: MqttMockPahoClient, recorded_calls: list[ReceiveMessage], record_calls: MessageCallbackType, ) -> None: """Test handling the socket connected and disconnected.""" - mqtt_mock = await mqtt_mock_entry() - await hass.async_block_till_done() - assert mqtt_mock.connected is True + mqtt_client_mock = setup_with_birth_msg_client_mock + assert mqtt_client_mock.connect.call_count == 1 mqtt_client_mock.loop_misc.return_value = paho_mqtt.MQTT_ERR_SUCCESS @@ -4495,19 +4366,14 @@ async def test_server_sock_connect_and_disconnect( assert len(recorded_calls) == 0 -@patch("homeassistant.components.mqtt.client.INITIAL_SUBSCRIBE_COOLDOWN", 0.0) -@patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.0) -@patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.0) async def test_server_sock_buffer_size( hass: HomeAssistant, - mqtt_client_mock: MqttMockPahoClient, - mqtt_mock_entry: MqttMockHAClientGenerator, + setup_with_birth_msg_client_mock: MqttMockPahoClient, caplog: pytest.LogCaptureFixture, ) -> None: """Test handling the socket buffer size fails.""" - mqtt_mock = await mqtt_mock_entry() - await hass.async_block_till_done() - assert mqtt_mock.connected is True + mqtt_client_mock = setup_with_birth_msg_client_mock + assert mqtt_client_mock.connect.call_count == 1 mqtt_client_mock.loop_misc.return_value = paho_mqtt.MQTT_ERR_SUCCESS @@ -4523,19 +4389,14 @@ async def test_server_sock_buffer_size( assert "Unable to increase the socket buffer size" in caplog.text -@patch("homeassistant.components.mqtt.client.INITIAL_SUBSCRIBE_COOLDOWN", 0.0) -@patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.0) -@patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.0) async def test_server_sock_buffer_size_with_websocket( hass: HomeAssistant, - mqtt_client_mock: MqttMockPahoClient, - mqtt_mock_entry: MqttMockHAClientGenerator, + setup_with_birth_msg_client_mock: MqttMockPahoClient, caplog: pytest.LogCaptureFixture, ) -> None: """Test handling the socket buffer size fails.""" - mqtt_mock = await mqtt_mock_entry() - await hass.async_block_till_done() - assert mqtt_mock.connected is True + mqtt_client_mock = setup_with_birth_msg_client_mock + assert mqtt_client_mock.connect.call_count == 1 mqtt_client_mock.loop_misc.return_value = paho_mqtt.MQTT_ERR_SUCCESS @@ -4560,20 +4421,15 @@ async def test_server_sock_buffer_size_with_websocket( assert "Unable to increase the socket buffer size" in caplog.text -@patch("homeassistant.components.mqtt.client.INITIAL_SUBSCRIBE_COOLDOWN", 0.0) -@patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.0) -@patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.0) async def test_client_sock_failure_after_connect( hass: HomeAssistant, - mqtt_client_mock: MqttMockPahoClient, - mqtt_mock_entry: MqttMockHAClientGenerator, + setup_with_birth_msg_client_mock: MqttMockPahoClient, recorded_calls: list[ReceiveMessage], record_calls: MessageCallbackType, ) -> None: """Test handling the socket connected and disconnected.""" - mqtt_mock = await mqtt_mock_entry() - await hass.async_block_till_done() - assert mqtt_mock.connected is True + mqtt_client_mock = setup_with_birth_msg_client_mock + assert mqtt_client_mock.connect.call_count == 1 mqtt_client_mock.loop_misc.return_value = paho_mqtt.MQTT_ERR_SUCCESS @@ -4589,7 +4445,7 @@ async def test_client_sock_failure_after_connect( mqtt_client_mock.loop_write.side_effect = OSError("foo") client.close() # close the client socket out from under the client - assert mqtt_mock.connected is True + assert mqtt_client_mock.connect.call_count == 1 unsub = await mqtt.async_subscribe(hass, "test-topic", record_calls) async_fire_time_changed(hass, utcnow() + timedelta(seconds=5)) await hass.async_block_till_done() @@ -4599,19 +4455,14 @@ async def test_client_sock_failure_after_connect( assert len(recorded_calls) == 0 -@patch("homeassistant.components.mqtt.client.INITIAL_SUBSCRIBE_COOLDOWN", 0.0) -@patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.0) -@patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.0) async def test_loop_write_failure( hass: HomeAssistant, - mqtt_client_mock: MqttMockPahoClient, - mqtt_mock_entry: MqttMockHAClientGenerator, + setup_with_birth_msg_client_mock: MqttMockPahoClient, caplog: pytest.LogCaptureFixture, ) -> None: """Test handling the socket connected and disconnected.""" - mqtt_mock = await mqtt_mock_entry() - await hass.async_block_till_done() - assert mqtt_mock.connected is True + mqtt_client_mock = setup_with_birth_msg_client_mock + assert mqtt_client_mock.connect.call_count == 1 mqtt_client_mock.loop_misc.return_value = paho_mqtt.MQTT_ERR_SUCCESS @@ -4642,7 +4493,7 @@ async def test_loop_write_failure( # Final for the disconnect callback await hass.async_block_till_done() - assert "Disconnected from MQTT server mock-broker:1883" in caplog.text + assert "Disconnected from MQTT server test-broker:1883" in caplog.text @pytest.mark.parametrize( From e2b0c558839aa0105a56daa9c2ab3be12ab92332 Mon Sep 17 00:00:00 2001 From: Charles Garwood Date: Mon, 24 Jun 2024 14:42:31 -0400 Subject: [PATCH 1123/1445] Bump python-fullykiosk to 0.0.14 (#120361) --- homeassistant/components/fully_kiosk/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/fully_kiosk/manifest.json b/homeassistant/components/fully_kiosk/manifest.json index 8d9ba85a058..4d7d1a2d7da 100644 --- a/homeassistant/components/fully_kiosk/manifest.json +++ b/homeassistant/components/fully_kiosk/manifest.json @@ -12,5 +12,5 @@ "documentation": "https://www.home-assistant.io/integrations/fully_kiosk", "iot_class": "local_polling", "mqtt": ["fully/deviceInfo/+"], - "requirements": ["python-fullykiosk==0.0.13"] + "requirements": ["python-fullykiosk==0.0.14"] } diff --git a/requirements_all.txt b/requirements_all.txt index 5b321763d2e..0330e991994 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2248,7 +2248,7 @@ python-etherscan-api==0.0.3 python-family-hub-local==0.0.2 # homeassistant.components.fully_kiosk -python-fullykiosk==0.0.13 +python-fullykiosk==0.0.14 # homeassistant.components.sms # python-gammu==3.2.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3d12f32d295..8f90092740a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1760,7 +1760,7 @@ python-bsblan==0.5.18 python-ecobee-api==0.2.18 # homeassistant.components.fully_kiosk -python-fullykiosk==0.0.13 +python-fullykiosk==0.0.14 # homeassistant.components.sms # python-gammu==3.2.4 From 6b78e913f2ab32d629a3e5843f21d3807d39e2fa Mon Sep 17 00:00:00 2001 From: Nathan Spencer Date: Mon, 24 Jun 2024 12:45:30 -0600 Subject: [PATCH 1124/1445] Bump pybalboa to 1.0.2 (#120360) --- homeassistant/components/balboa/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/balboa/manifest.json b/homeassistant/components/balboa/manifest.json index 152a89bde31..d7c15bab88f 100644 --- a/homeassistant/components/balboa/manifest.json +++ b/homeassistant/components/balboa/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/balboa", "iot_class": "local_push", "loggers": ["pybalboa"], - "requirements": ["pybalboa==1.0.1"] + "requirements": ["pybalboa==1.0.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 0330e991994..ea455bf47a5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1734,7 +1734,7 @@ pyatv==0.14.3 pyaussiebb==0.0.15 # homeassistant.components.balboa -pybalboa==1.0.1 +pybalboa==1.0.2 # homeassistant.components.bbox pybbox==0.0.5-alpha diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8f90092740a..3d78a857095 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1381,7 +1381,7 @@ pyatv==0.14.3 pyaussiebb==0.0.15 # homeassistant.components.balboa -pybalboa==1.0.1 +pybalboa==1.0.2 # homeassistant.components.blackbird pyblackbird==0.6 From fb3059e6e68684732a6b9fd85b4ce9bfed036ce9 Mon Sep 17 00:00:00 2001 From: Koen van Zuijlen <8818390+kvanzuijlen@users.noreply.github.com> Date: Mon, 24 Jun 2024 20:47:42 +0200 Subject: [PATCH 1125/1445] Bump justnimbus to 0.7.4 (#120355) --- homeassistant/components/justnimbus/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/justnimbus/manifest.json b/homeassistant/components/justnimbus/manifest.json index 26cbc80e166..48fdad69ac8 100644 --- a/homeassistant/components/justnimbus/manifest.json +++ b/homeassistant/components/justnimbus/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/justnimbus", "iot_class": "cloud_polling", - "requirements": ["justnimbus==0.7.3"] + "requirements": ["justnimbus==0.7.4"] } diff --git a/requirements_all.txt b/requirements_all.txt index ea455bf47a5..e21548fce85 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1191,7 +1191,7 @@ jellyfin-apiclient-python==1.9.2 jsonpath==0.82.2 # homeassistant.components.justnimbus -justnimbus==0.7.3 +justnimbus==0.7.4 # homeassistant.components.kaiterra kaiterra-async-client==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3d78a857095..2dd55cd9f7c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -978,7 +978,7 @@ jellyfin-apiclient-python==1.9.2 jsonpath==0.82.2 # homeassistant.components.justnimbus -justnimbus==0.7.3 +justnimbus==0.7.4 # homeassistant.components.kegtron kegtron-ble==0.4.0 From 00621ad512e2f377f48cad5c27d8fc2e9529f60b Mon Sep 17 00:00:00 2001 From: G Johansson Date: Mon, 24 Jun 2024 20:53:49 +0200 Subject: [PATCH 1126/1445] Use runtime data in version (#120363) --- homeassistant/components/version/__init__.py | 13 ++++++------- homeassistant/components/version/binary_sensor.py | 9 ++++----- homeassistant/components/version/diagnostics.py | 8 +++----- homeassistant/components/version/sensor.py | 9 ++++----- 4 files changed, 17 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/version/__init__.py b/homeassistant/components/version/__init__.py index 4112cc51e46..cf13821dc8a 100644 --- a/homeassistant/components/version/__init__.py +++ b/homeassistant/components/version/__init__.py @@ -16,15 +16,16 @@ from .const import ( CONF_CHANNEL, CONF_IMAGE, CONF_SOURCE, - DOMAIN, PLATFORMS, ) from .coordinator import VersionDataUpdateCoordinator _LOGGER = logging.getLogger(__name__) +type VersionConfigEntry = ConfigEntry[VersionDataUpdateCoordinator] -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + +async def async_setup_entry(hass: HomeAssistant, entry: VersionConfigEntry) -> bool: """Set up the version integration from a config entry.""" board = entry.data[CONF_BOARD] @@ -50,14 +51,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) await coordinator.async_config_entry_first_refresh() - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator + entry.runtime_data = coordinator await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: VersionConfigEntry) -> bool: """Unload the config entry.""" - if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): - hass.data[DOMAIN].pop(entry.entry_id) - return unload_ok + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/version/binary_sensor.py b/homeassistant/components/version/binary_sensor.py index ff4f51e409f..827029e1d8c 100644 --- a/homeassistant/components/version/binary_sensor.py +++ b/homeassistant/components/version/binary_sensor.py @@ -9,13 +9,12 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntity, BinarySensorEntityDescription, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_NAME, EntityCategory, __version__ as HA_VERSION from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import CONF_SOURCE, DEFAULT_NAME, DOMAIN -from .coordinator import VersionDataUpdateCoordinator +from . import VersionConfigEntry +from .const import CONF_SOURCE, DEFAULT_NAME from .entity import VersionEntity HA_VERSION_OBJECT = AwesomeVersion(HA_VERSION) @@ -23,11 +22,11 @@ HA_VERSION_OBJECT = AwesomeVersion(HA_VERSION) async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: VersionConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up version binary_sensors.""" - coordinator: VersionDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + coordinator = config_entry.runtime_data if (source := config_entry.data[CONF_SOURCE]) == "local": return diff --git a/homeassistant/components/version/diagnostics.py b/homeassistant/components/version/diagnostics.py index 194027d6ef4..ca7318f468b 100644 --- a/homeassistant/components/version/diagnostics.py +++ b/homeassistant/components/version/diagnostics.py @@ -6,20 +6,18 @@ from typing import Any from attr import asdict -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er -from .const import DOMAIN -from .coordinator import VersionDataUpdateCoordinator +from . import VersionConfigEntry async def async_get_config_entry_diagnostics( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: VersionConfigEntry, ) -> dict[str, Any]: """Return diagnostics for a config entry.""" - coordinator: VersionDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + coordinator = config_entry.runtime_data device_registry = dr.async_get(hass) entity_registry = er.async_get(hass) diff --git a/homeassistant/components/version/sensor.py b/homeassistant/components/version/sensor.py index 6b0565b8cb3..e1d552bcd36 100644 --- a/homeassistant/components/version/sensor.py +++ b/homeassistant/components/version/sensor.py @@ -5,24 +5,23 @@ from __future__ import annotations from typing import Any from homeassistant.components.sensor import SensorEntity, SensorEntityDescription -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType -from .const import CONF_SOURCE, DEFAULT_NAME, DOMAIN -from .coordinator import VersionDataUpdateCoordinator +from . import VersionConfigEntry +from .const import CONF_SOURCE, DEFAULT_NAME from .entity import VersionEntity async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: VersionConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up version sensors.""" - coordinator: VersionDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + coordinator = entry.runtime_data if (entity_name := entry.data[CONF_NAME]) == DEFAULT_NAME: entity_name = entry.title From 6689dbbcc68ff0d527cfc1160928feb60c348f05 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Mon, 24 Jun 2024 20:56:35 +0200 Subject: [PATCH 1127/1445] Deprecate DTE Energy Bridge (#120350) Co-authored-by: Franck Nijhof --- .../components/dte_energy_bridge/sensor.py | 14 ++++++++++++++ .../components/dte_energy_bridge/strings.json | 8 ++++++++ 2 files changed, 22 insertions(+) create mode 100644 homeassistant/components/dte_energy_bridge/strings.json diff --git a/homeassistant/components/dte_energy_bridge/sensor.py b/homeassistant/components/dte_energy_bridge/sensor.py index c33bb37e468..112ebd55f94 100644 --- a/homeassistant/components/dte_energy_bridge/sensor.py +++ b/homeassistant/components/dte_energy_bridge/sensor.py @@ -18,6 +18,7 @@ from homeassistant.const import CONF_NAME, UnitOfPower from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.issue_registry import IssueSeverity, create_issue from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType _LOGGER = logging.getLogger(__name__) @@ -27,6 +28,7 @@ CONF_VERSION = "version" DEFAULT_NAME = "Current Energy Usage" DEFAULT_VERSION = 1 +DOMAIN = "dte_energy_bridge" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { @@ -46,6 +48,18 @@ def setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the DTE energy bridge sensor.""" + create_issue( + hass, + DOMAIN, + "deprecated_integration", + breaks_in_ha_version="2025.1.0", + is_fixable=False, + issue_domain=DOMAIN, + severity=IssueSeverity.WARNING, + translation_key="deprecated_integration", + translation_placeholders={"domain": DOMAIN}, + ) + name = config[CONF_NAME] ip_address = config[CONF_IP_ADDRESS] version = config[CONF_VERSION] diff --git a/homeassistant/components/dte_energy_bridge/strings.json b/homeassistant/components/dte_energy_bridge/strings.json new file mode 100644 index 00000000000..f75867b8faa --- /dev/null +++ b/homeassistant/components/dte_energy_bridge/strings.json @@ -0,0 +1,8 @@ +{ + "issues": { + "deprecated_integration": { + "title": "The DTE Energy Bridge integration will be removed", + "description": "The DTE Energy Bridge integration will be removed as new users can't get any supported devices, and the integration will fail as soon as a current device gets internet access.\n\n Please remove all `{domain}`platform sensors from your configuration and restart Home Assistant." + } + } +} From 46dcf1dc44e1c44336969592b9a4d6a4013864a9 Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Mon, 24 Jun 2024 13:57:56 -0500 Subject: [PATCH 1128/1445] Prioritize custom intents over builtin (#120358) --- .../components/conversation/default_agent.py | 32 +++++- tests/components/conversation/test_init.py | 103 ++++++++++++++++++ 2 files changed, 134 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/conversation/default_agent.py b/homeassistant/components/conversation/default_agent.py index 7bb2c2182b3..71b14f8d299 100644 --- a/homeassistant/components/conversation/default_agent.py +++ b/homeassistant/components/conversation/default_agent.py @@ -419,6 +419,7 @@ class DefaultAgent(ConversationEntity): language: str, ) -> RecognizeResult | None: """Search intents for a match to user input.""" + custom_result: RecognizeResult | None = None name_result: RecognizeResult | None = None best_results: list[RecognizeResult] = [] best_text_chunks_matched: int | None = None @@ -429,6 +430,20 @@ class DefaultAgent(ConversationEntity): intent_context=intent_context, language=language, ): + # User intents have highest priority + if (result.intent_metadata is not None) and result.intent_metadata.get( + METADATA_CUSTOM_SENTENCE + ): + if (custom_result is None) or ( + result.text_chunks_matched > custom_result.text_chunks_matched + ): + custom_result = result + + # Clear builtin results + best_results = [] + name_result = None + continue + # Prioritize results with a "name" slot, but still prefer ones with # more literal text matched. if ( @@ -453,6 +468,10 @@ class DefaultAgent(ConversationEntity): # We will resolve the ambiguity below. best_results.append(result) + if custom_result is not None: + # Prioritize user intents + return custom_result + if name_result is not None: # Prioritize matches with entity names above area names return name_result @@ -718,11 +737,22 @@ class DefaultAgent(ConversationEntity): if self._config_intents and ( self.hass.config.language in (language, language_variant) ): + hass_config_path = self.hass.config.path() merge_dict( intents_dict, { "intents": { - intent_name: {"data": [{"sentences": sentences}]} + intent_name: { + "data": [ + { + "sentences": sentences, + "metadata": { + METADATA_CUSTOM_SENTENCE: True, + METADATA_CUSTOM_FILE: hass_config_path, + }, + } + ] + } for intent_name, sentences in self._config_intents.items() } }, diff --git a/tests/components/conversation/test_init.py b/tests/components/conversation/test_init.py index 48f227e9497..dc940dba81b 100644 --- a/tests/components/conversation/test_init.py +++ b/tests/components/conversation/test_init.py @@ -1,12 +1,15 @@ """The tests for the Conversation component.""" from http import HTTPStatus +import os +import tempfile from typing import Any from unittest.mock import patch import pytest from syrupy.assertion import SnapshotAssertion import voluptuous as vol +import yaml from homeassistant.components import conversation from homeassistant.components.conversation import default_agent @@ -1389,3 +1392,103 @@ async def test_ws_hass_agent_debug_sentence_trigger( # Trigger should not have been executed assert len(calls) == 0 + + +async def test_custom_sentences_priority( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + hass_admin_user: MockUser, + snapshot: SnapshotAssertion, +) -> None: + """Test that user intents from custom_sentences have priority over builtin intents/sentences.""" + with tempfile.NamedTemporaryFile( + mode="w+", + encoding="utf-8", + suffix=".yaml", + dir=os.path.join(hass.config.config_dir, "custom_sentences", "en"), + ) as custom_sentences_file: + # Add a custom sentence that would match a builtin sentence. + # Custom sentences have priority. + yaml.dump( + { + "language": "en", + "intents": { + "CustomIntent": {"data": [{"sentences": ["turn on the lamp"]}]} + }, + }, + custom_sentences_file, + ) + custom_sentences_file.flush() + custom_sentences_file.seek(0) + + assert await async_setup_component(hass, "homeassistant", {}) + assert await async_setup_component(hass, "conversation", {}) + assert await async_setup_component(hass, "light", {}) + assert await async_setup_component(hass, "intent", {}) + assert await async_setup_component( + hass, + "intent_script", + { + "intent_script": { + "CustomIntent": {"speech": {"text": "custom response"}} + } + }, + ) + + # Ensure that a "lamp" exists so that we can verify the custom intent + # overrides the builtin sentence. + hass.states.async_set("light.lamp", "off") + + client = await hass_client() + resp = await client.post( + "/api/conversation/process", + json={ + "text": "turn on the lamp", + "language": hass.config.language, + }, + ) + assert resp.status == HTTPStatus.OK + data = await resp.json() + assert data["response"]["response_type"] == "action_done" + assert data["response"]["speech"]["plain"]["speech"] == "custom response" + + +async def test_config_sentences_priority( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + hass_admin_user: MockUser, + snapshot: SnapshotAssertion, +) -> None: + """Test that user intents from configuration.yaml have priority over builtin intents/sentences.""" + # Add a custom sentence that would match a builtin sentence. + # Custom sentences have priority. + assert await async_setup_component(hass, "homeassistant", {}) + assert await async_setup_component(hass, "intent", {}) + assert await async_setup_component( + hass, + "conversation", + {"conversation": {"intents": {"CustomIntent": ["turn on the lamp"]}}}, + ) + assert await async_setup_component(hass, "light", {}) + assert await async_setup_component( + hass, + "intent_script", + {"intent_script": {"CustomIntent": {"speech": {"text": "custom response"}}}}, + ) + + # Ensure that a "lamp" exists so that we can verify the custom intent + # overrides the builtin sentence. + hass.states.async_set("light.lamp", "off") + + client = await hass_client() + resp = await client.post( + "/api/conversation/process", + json={ + "text": "turn on the lamp", + "language": hass.config.language, + }, + ) + assert resp.status == HTTPStatus.OK + data = await resp.json() + assert data["response"]["response_type"] == "action_done" + assert data["response"]["speech"]["plain"]["speech"] == "custom response" From 3b79ab6e1832aac01e557aa5e72a3098a3a2ea0c Mon Sep 17 00:00:00 2001 From: David Knowles Date: Mon, 24 Jun 2024 14:58:54 -0400 Subject: [PATCH 1129/1445] Reduce the amount of data fetched in individual Hydrawise API calls (#120328) --- homeassistant/components/hydrawise/config_flow.py | 4 ++-- homeassistant/components/hydrawise/coordinator.py | 6 +++++- tests/components/hydrawise/conftest.py | 9 +++------ tests/components/hydrawise/test_config_flow.py | 2 +- 4 files changed, 11 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/hydrawise/config_flow.py b/homeassistant/components/hydrawise/config_flow.py index 1c2c1c5cf29..ab9ebbb065d 100644 --- a/homeassistant/components/hydrawise/config_flow.py +++ b/homeassistant/components/hydrawise/config_flow.py @@ -37,8 +37,8 @@ class HydrawiseConfigFlow(ConfigFlow, domain=DOMAIN): # Verify that the provided credentials work.""" api = client.Hydrawise(auth.Auth(username, password)) try: - # Skip fetching zones to save on metered API calls. - user = await api.get_user() + # Don't fetch zones because we don't need them yet. + user = await api.get_user(fetch_zones=False) except NotAuthorizedError: return on_failure("invalid_auth") except TimeoutError: diff --git a/homeassistant/components/hydrawise/coordinator.py b/homeassistant/components/hydrawise/coordinator.py index d046dfcc92a..50caaa0c0de 100644 --- a/homeassistant/components/hydrawise/coordinator.py +++ b/homeassistant/components/hydrawise/coordinator.py @@ -40,13 +40,17 @@ class HydrawiseDataUpdateCoordinator(DataUpdateCoordinator[HydrawiseData]): async def _async_update_data(self) -> HydrawiseData: """Fetch the latest data from Hydrawise.""" - user = await self.api.get_user() + # Don't fetch zones. We'll fetch them for each controller later. + # This is to prevent 502 errors in some cases. + # See: https://github.com/home-assistant/core/issues/120128 + user = await self.api.get_user(fetch_zones=False) controllers = {} zones = {} sensors = {} daily_water_use: dict[int, ControllerWaterUseSummary] = {} for controller in user.controllers: controllers[controller.id] = controller + controller.zones = await self.api.get_zones(controller) for zone in controller.zones: zones[zone.id] = zone for sensor in controller.sensors: diff --git a/tests/components/hydrawise/conftest.py b/tests/components/hydrawise/conftest.py index eb1518eb7f2..0b5327cd7b2 100644 --- a/tests/components/hydrawise/conftest.py +++ b/tests/components/hydrawise/conftest.py @@ -1,6 +1,6 @@ """Common fixtures for the Hydrawise tests.""" -from collections.abc import Awaitable, Callable +from collections.abc import Awaitable, Callable, Generator from datetime import datetime, timedelta from unittest.mock import AsyncMock, patch @@ -20,7 +20,6 @@ from pydrawise.schema import ( Zone, ) import pytest -from typing_extensions import Generator from homeassistant.components.hydrawise.const import DOMAIN from homeassistant.const import CONF_API_KEY, CONF_PASSWORD, CONF_USERNAME @@ -67,9 +66,9 @@ def mock_pydrawise( """Mock Hydrawise.""" with patch("pydrawise.client.Hydrawise", autospec=True) as mock_pydrawise: user.controllers = [controller] - controller.zones = zones controller.sensors = sensors mock_pydrawise.return_value.get_user.return_value = user + mock_pydrawise.return_value.get_zones.return_value = zones mock_pydrawise.return_value.get_water_use_summary.return_value = ( controller_water_use_summary ) @@ -142,7 +141,7 @@ def sensors() -> list[Sensor]: ), status=SensorStatus( water_flow=LocalizedValueType(value=577.0044752010709, unit="gal"), - active=None, + active=False, ), ), ] @@ -154,7 +153,6 @@ def zones() -> list[Zone]: return [ Zone( name="Zone One", - number=1, id=5965394, scheduled_runs=ScheduledZoneRuns( summary="", @@ -171,7 +169,6 @@ def zones() -> list[Zone]: ), Zone( name="Zone Two", - number=2, id=5965395, scheduled_runs=ScheduledZoneRuns( current_run=ScheduledZoneRun( diff --git a/tests/components/hydrawise/test_config_flow.py b/tests/components/hydrawise/test_config_flow.py index a7fbc008aab..e85b1b9b249 100644 --- a/tests/components/hydrawise/test_config_flow.py +++ b/tests/components/hydrawise/test_config_flow.py @@ -46,7 +46,7 @@ async def test_form( CONF_PASSWORD: "__password__", } assert len(mock_setup_entry.mock_calls) == 1 - mock_pydrawise.get_user.assert_called_once_with() + mock_pydrawise.get_user.assert_called_once_with(fetch_zones=False) async def test_form_api_error( From b223cb7bb9c052516dd8267c1a3e2d9e22b9d079 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 24 Jun 2024 21:00:12 +0200 Subject: [PATCH 1130/1445] Ensure config_entry is added to hass in tests (#120327) --- tests/components/honeywell/test_init.py | 3 +-- tests/components/switch_as_x/test_init.py | 4 +++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/components/honeywell/test_init.py b/tests/components/honeywell/test_init.py index cdd767f019d..ac24876413d 100644 --- a/tests/components/honeywell/test_init.py +++ b/tests/components/honeywell/test_init.py @@ -173,14 +173,13 @@ async def test_remove_stale_device( identifiers={("OtherDomain", 7654321)}, ) + config_entry.add_to_hass(hass) device_registry.async_update_device( device_entry_other.id, add_config_entry_id=config_entry.entry_id, merge_identifiers={(DOMAIN, 7654321)}, ) - config_entry.add_to_hass(hass) - await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() assert config_entry.state is ConfigEntryState.LOADED diff --git a/tests/components/switch_as_x/test_init.py b/tests/components/switch_as_x/test_init.py index 3889a43f741..e250cacb7ac 100644 --- a/tests/components/switch_as_x/test_init.py +++ b/tests/components/switch_as_x/test_init.py @@ -171,8 +171,10 @@ async def test_device_registry_config_entry_1( original_name="ABC", ) # Add another config entry to the same device + other_config_entry = MockConfigEntry() + other_config_entry.add_to_hass(hass) device_registry.async_update_device( - device_entry.id, add_config_entry_id=MockConfigEntry().entry_id + device_entry.id, add_config_entry_id=other_config_entry.entry_id ) switch_as_x_config_entry = MockConfigEntry( From ea09d0cbed706bcf720d3340c04da8e4289169fb Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 24 Jun 2024 21:02:08 +0200 Subject: [PATCH 1131/1445] Use HassKey in cloud integration (#120322) --- homeassistant/components/cloud/__init__.py | 25 ++++++----- .../components/cloud/account_link.py | 10 +++-- .../components/cloud/assist_pipeline.py | 4 +- .../components/cloud/binary_sensor.py | 4 +- homeassistant/components/cloud/const.py | 14 ++++++- homeassistant/components/cloud/http_api.py | 42 +++++++++---------- homeassistant/components/cloud/repairs.py | 6 +-- homeassistant/components/cloud/stt.py | 7 ++-- .../components/cloud/system_health.py | 7 +--- homeassistant/components/cloud/tts.py | 9 ++-- tests/components/cloud/__init__.py | 9 ++-- tests/components/cloud/conftest.py | 9 ++-- tests/components/cloud/test_account_link.py | 7 ++-- tests/components/cloud/test_alexa_config.py | 9 ++-- tests/components/cloud/test_client.py | 19 +++++---- tests/components/cloud/test_google_config.py | 19 +++++---- tests/components/cloud/test_init.py | 11 +++-- 17 files changed, 109 insertions(+), 102 deletions(-) diff --git a/homeassistant/components/cloud/__init__.py b/homeassistant/components/cloud/__init__.py index cd8e5101e73..80c02571d24 100644 --- a/homeassistant/components/cloud/__init__.py +++ b/homeassistant/components/cloud/__init__.py @@ -55,6 +55,7 @@ from .const import ( CONF_SERVICEHANDLERS_SERVER, CONF_THINGTALK_SERVER, CONF_USER_POOL_ID, + DATA_CLOUD, DATA_PLATFORMS_SETUP, DOMAIN, MODE_DEV, @@ -155,14 +156,14 @@ def async_is_logged_in(hass: HomeAssistant) -> bool: Note: This returns True even if not currently connected to the cloud. """ - return DOMAIN in hass.data and hass.data[DOMAIN].is_logged_in + return DATA_CLOUD in hass.data and hass.data[DATA_CLOUD].is_logged_in @bind_hass @callback def async_is_connected(hass: HomeAssistant) -> bool: """Test if connected to the cloud.""" - return DOMAIN in hass.data and hass.data[DOMAIN].iot.connected + return DATA_CLOUD in hass.data and hass.data[DATA_CLOUD].iot.connected @callback @@ -178,7 +179,7 @@ def async_listen_connection_change( @callback def async_active_subscription(hass: HomeAssistant) -> bool: """Test if user has an active subscription.""" - return async_is_logged_in(hass) and not hass.data[DOMAIN].subscription_expired + return async_is_logged_in(hass) and not hass.data[DATA_CLOUD].subscription_expired async def async_get_or_create_cloudhook(hass: HomeAssistant, webhook_id: str) -> str: @@ -189,7 +190,7 @@ async def async_get_or_create_cloudhook(hass: HomeAssistant, webhook_id: str) -> if not async_is_logged_in(hass): raise CloudNotAvailable - cloud: Cloud[CloudClient] = hass.data[DOMAIN] + cloud = hass.data[DATA_CLOUD] cloudhooks = cloud.client.cloudhooks if hook := cloudhooks.get(webhook_id): return cast(str, hook["cloudhook_url"]) @@ -206,7 +207,7 @@ async def async_create_cloudhook(hass: HomeAssistant, webhook_id: str) -> str: if not async_is_logged_in(hass): raise CloudNotAvailable - cloud: Cloud[CloudClient] = hass.data[DOMAIN] + cloud = hass.data[DATA_CLOUD] hook = await cloud.cloudhooks.async_create(webhook_id, True) cloudhook_url: str = hook["cloudhook_url"] return cloudhook_url @@ -215,10 +216,10 @@ async def async_create_cloudhook(hass: HomeAssistant, webhook_id: str) -> str: @bind_hass async def async_delete_cloudhook(hass: HomeAssistant, webhook_id: str) -> None: """Delete a cloudhook.""" - if DOMAIN not in hass.data: + if DATA_CLOUD not in hass.data: raise CloudNotAvailable - await hass.data[DOMAIN].cloudhooks.async_delete(webhook_id) + await hass.data[DATA_CLOUD].cloudhooks.async_delete(webhook_id) @bind_hass @@ -228,10 +229,10 @@ def async_remote_ui_url(hass: HomeAssistant) -> str: if not async_is_logged_in(hass): raise CloudNotAvailable - if not hass.data[DOMAIN].client.prefs.remote_enabled: + if not hass.data[DATA_CLOUD].client.prefs.remote_enabled: raise CloudNotAvailable - if not (remote_domain := hass.data[DOMAIN].client.prefs.remote_domain): + if not (remote_domain := hass.data[DATA_CLOUD].client.prefs.remote_domain): raise CloudNotAvailable return f"https://{remote_domain}" @@ -256,7 +257,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # Initialize Cloud websession = async_get_clientsession(hass) client = CloudClient(hass, prefs, websession, alexa_conf, google_conf) - cloud = hass.data[DOMAIN] = Cloud(client, **kwargs) + cloud = hass.data[DATA_CLOUD] = Cloud(client, **kwargs) async def _shutdown(event: Event) -> None: """Shutdown event.""" @@ -373,9 +374,7 @@ def _remote_handle_prefs_updated(cloud: Cloud[CloudClient]) -> None: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a config entry.""" await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) - stt_tts_entities_added: asyncio.Event = hass.data[DATA_PLATFORMS_SETUP][ - "stt_tts_entities_added" - ] + stt_tts_entities_added = hass.data[DATA_PLATFORMS_SETUP]["stt_tts_entities_added"] stt_tts_entities_added.set() return True diff --git a/homeassistant/components/cloud/account_link.py b/homeassistant/components/cloud/account_link.py index 784de14e6ad..b67c1afad71 100644 --- a/homeassistant/components/cloud/account_link.py +++ b/homeassistant/components/cloud/account_link.py @@ -14,7 +14,7 @@ from homeassistant.const import __version__ as HA_VERSION from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_entry_oauth2_flow, event -from .const import DOMAIN +from .const import DATA_CLOUD, DOMAIN DATA_SERVICES = "cloud_account_link_services" CACHE_TIMEOUT = 3600 @@ -68,7 +68,9 @@ async def _get_services(hass: HomeAssistant) -> list[dict[str, Any]]: return services # noqa: RET504 try: - services = await account_link.async_fetch_available_services(hass.data[DOMAIN]) + services = await account_link.async_fetch_available_services( + hass.data[DATA_CLOUD] + ) except (aiohttp.ClientError, TimeoutError): return [] @@ -105,7 +107,7 @@ class CloudOAuth2Implementation(config_entry_oauth2_flow.AbstractOAuth2Implement async def async_generate_authorize_url(self, flow_id: str) -> str: """Generate a url for the user to authorize.""" helper = account_link.AuthorizeAccountHelper( - self.hass.data[DOMAIN], self.service + self.hass.data[DATA_CLOUD], self.service ) authorize_url = await helper.async_get_authorize_url() @@ -138,6 +140,6 @@ class CloudOAuth2Implementation(config_entry_oauth2_flow.AbstractOAuth2Implement async def _async_refresh_token(self, token: dict) -> dict: """Refresh a token.""" new_token = await account_link.async_fetch_access_token( - self.hass.data[DOMAIN], self.service, token["refresh_token"] + self.hass.data[DATA_CLOUD], self.service, token["refresh_token"] ) return {**token, **new_token} diff --git a/homeassistant/components/cloud/assist_pipeline.py b/homeassistant/components/cloud/assist_pipeline.py index e9d66bdcc1f..f3a591d6eda 100644 --- a/homeassistant/components/cloud/assist_pipeline.py +++ b/homeassistant/components/cloud/assist_pipeline.py @@ -27,7 +27,7 @@ async def async_create_cloud_pipeline(hass: HomeAssistant) -> str | None: """Create a cloud assist pipeline.""" # Wait for stt and tts platforms to set up and entities to be added # before creating the pipeline. - platforms_setup: dict[str, asyncio.Event] = hass.data[DATA_PLATFORMS_SETUP] + platforms_setup = hass.data[DATA_PLATFORMS_SETUP] await asyncio.gather(*(event.wait() for event in platforms_setup.values())) # Make sure the pipeline store is loaded, needed because assist_pipeline # is an after dependency of cloud @@ -91,7 +91,7 @@ async def async_migrate_cloud_pipeline_engine( else: raise ValueError(f"Invalid platform {platform}") - platforms_setup: dict[str, asyncio.Event] = hass.data[DATA_PLATFORMS_SETUP] + platforms_setup = hass.data[DATA_PLATFORMS_SETUP] await platforms_setup[wait_for_platform].wait() # Make sure the pipeline store is loaded, needed because assist_pipeline diff --git a/homeassistant/components/cloud/binary_sensor.py b/homeassistant/components/cloud/binary_sensor.py index 0693a8285ce..75cbd3c9f3d 100644 --- a/homeassistant/components/cloud/binary_sensor.py +++ b/homeassistant/components/cloud/binary_sensor.py @@ -18,7 +18,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback from .client import CloudClient -from .const import DISPATCHER_REMOTE_UPDATE, DOMAIN +from .const import DATA_CLOUD, DISPATCHER_REMOTE_UPDATE WAIT_UNTIL_CHANGE = 3 @@ -29,7 +29,7 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Home Assistant Cloud binary sensors.""" - cloud: Cloud[CloudClient] = hass.data[DOMAIN] + cloud = hass.data[DATA_CLOUD] async_add_entities([CloudRemoteBinary(cloud)]) diff --git a/homeassistant/components/cloud/const.py b/homeassistant/components/cloud/const.py index 2c58dd57340..5e9fb2e9dc7 100644 --- a/homeassistant/components/cloud/const.py +++ b/homeassistant/components/cloud/const.py @@ -2,12 +2,22 @@ from __future__ import annotations -from typing import Any +import asyncio +from typing import TYPE_CHECKING, Any +from homeassistant.util.hass_dict import HassKey from homeassistant.util.signal_type import SignalType +if TYPE_CHECKING: + from hass_nabucasa import Cloud + + from .client import CloudClient + DOMAIN = "cloud" -DATA_PLATFORMS_SETUP = "cloud_platforms_setup" +DATA_CLOUD: HassKey[Cloud[CloudClient]] = HassKey(DOMAIN) +DATA_PLATFORMS_SETUP: HassKey[dict[str, asyncio.Event]] = HassKey( + "cloud_platforms_setup" +) REQUEST_TIMEOUT = 10 PREF_ENABLE_ALEXA = "alexa_enabled" diff --git a/homeassistant/components/cloud/http_api.py b/homeassistant/components/cloud/http_api.py index bd2860b19df..b1931515745 100644 --- a/homeassistant/components/cloud/http_api.py +++ b/homeassistant/components/cloud/http_api.py @@ -38,7 +38,7 @@ from .alexa_config import entity_supported as entity_supported_by_alexa from .assist_pipeline import async_create_cloud_pipeline from .client import CloudClient from .const import ( - DOMAIN, + DATA_CLOUD, PREF_ALEXA_REPORT_STATE, PREF_DISABLE_2FA, PREF_ENABLE_ALEXA, @@ -196,7 +196,7 @@ class GoogleActionsSyncView(HomeAssistantView): async def post(self, request: web.Request) -> web.Response: """Trigger a Google Actions sync.""" hass = request.app[KEY_HASS] - cloud: Cloud[CloudClient] = hass.data[DOMAIN] + cloud = hass.data[DATA_CLOUD] gconf = await cloud.client.get_google_config() status = await gconf.async_sync_entities(gconf.agent_user_id) return self.json({}, status_code=status) @@ -216,7 +216,7 @@ class CloudLoginView(HomeAssistantView): async def post(self, request: web.Request, data: dict[str, Any]) -> web.Response: """Handle login request.""" hass = request.app[KEY_HASS] - cloud: Cloud[CloudClient] = hass.data[DOMAIN] + cloud = hass.data[DATA_CLOUD] await cloud.login(data["email"], data["password"]) if "assist_pipeline" in hass.config.components: @@ -237,7 +237,7 @@ class CloudLogoutView(HomeAssistantView): async def post(self, request: web.Request) -> web.Response: """Handle logout request.""" hass = request.app[KEY_HASS] - cloud: Cloud[CloudClient] = hass.data[DOMAIN] + cloud = hass.data[DATA_CLOUD] async with asyncio.timeout(REQUEST_TIMEOUT): await cloud.logout() @@ -264,7 +264,7 @@ class CloudRegisterView(HomeAssistantView): async def post(self, request: web.Request, data: dict[str, Any]) -> web.Response: """Handle registration request.""" hass = request.app[KEY_HASS] - cloud: Cloud[CloudClient] = hass.data[DOMAIN] + cloud = hass.data[DATA_CLOUD] client_metadata = None @@ -301,7 +301,7 @@ class CloudResendConfirmView(HomeAssistantView): async def post(self, request: web.Request, data: dict[str, Any]) -> web.Response: """Handle resending confirm email code request.""" hass = request.app[KEY_HASS] - cloud: Cloud[CloudClient] = hass.data[DOMAIN] + cloud = hass.data[DATA_CLOUD] async with asyncio.timeout(REQUEST_TIMEOUT): await cloud.auth.async_resend_email_confirm(data["email"]) @@ -321,7 +321,7 @@ class CloudForgotPasswordView(HomeAssistantView): async def post(self, request: web.Request, data: dict[str, Any]) -> web.Response: """Handle forgot password request.""" hass = request.app[KEY_HASS] - cloud: Cloud[CloudClient] = hass.data[DOMAIN] + cloud = hass.data[DATA_CLOUD] async with asyncio.timeout(REQUEST_TIMEOUT): await cloud.auth.async_forgot_password(data["email"]) @@ -341,7 +341,7 @@ async def websocket_cloud_remove_data( Async friendly. """ - cloud: Cloud[CloudClient] = hass.data[DOMAIN] + cloud = hass.data[DATA_CLOUD] if cloud.is_logged_in: connection.send_message( websocket_api.error_message( @@ -367,7 +367,7 @@ async def websocket_cloud_status( Async friendly. """ - cloud: Cloud[CloudClient] = hass.data[DOMAIN] + cloud = hass.data[DATA_CLOUD] connection.send_message( websocket_api.result_message(msg["id"], await _account_data(hass, cloud)) ) @@ -391,7 +391,7 @@ def _require_cloud_login( msg: dict[str, Any], ) -> None: """Require to be logged into the cloud.""" - cloud: Cloud[CloudClient] = hass.data[DOMAIN] + cloud = hass.data[DATA_CLOUD] if not cloud.is_logged_in: connection.send_message( websocket_api.error_message( @@ -414,7 +414,7 @@ async def websocket_subscription( msg: dict[str, Any], ) -> None: """Handle request for account info.""" - cloud: Cloud[CloudClient] = hass.data[DOMAIN] + cloud = hass.data[DATA_CLOUD] if (data := await async_subscription_info(cloud)) is None: connection.send_error( msg["id"], "request_failed", "Failed to request subscription" @@ -457,7 +457,7 @@ async def websocket_update_prefs( msg: dict[str, Any], ) -> None: """Handle request for account info.""" - cloud: Cloud[CloudClient] = hass.data[DOMAIN] + cloud = hass.data[DATA_CLOUD] changes = dict(msg) changes.pop("id") @@ -508,7 +508,7 @@ async def websocket_hook_create( msg: dict[str, Any], ) -> None: """Handle request for account info.""" - cloud: Cloud[CloudClient] = hass.data[DOMAIN] + cloud = hass.data[DATA_CLOUD] hook = await cloud.cloudhooks.async_create(msg["webhook_id"], False) connection.send_message(websocket_api.result_message(msg["id"], hook)) @@ -528,7 +528,7 @@ async def websocket_hook_delete( msg: dict[str, Any], ) -> None: """Handle request for account info.""" - cloud: Cloud[CloudClient] = hass.data[DOMAIN] + cloud = hass.data[DATA_CLOUD] await cloud.cloudhooks.async_delete(msg["webhook_id"]) connection.send_message(websocket_api.result_message(msg["id"])) @@ -597,7 +597,7 @@ async def websocket_remote_connect( msg: dict[str, Any], ) -> None: """Handle request for connect remote.""" - cloud: Cloud[CloudClient] = hass.data[DOMAIN] + cloud = hass.data[DATA_CLOUD] await cloud.client.prefs.async_update(remote_enabled=True) connection.send_result(msg["id"], await _account_data(hass, cloud)) @@ -613,7 +613,7 @@ async def websocket_remote_disconnect( msg: dict[str, Any], ) -> None: """Handle request for disconnect remote.""" - cloud: Cloud[CloudClient] = hass.data[DOMAIN] + cloud = hass.data[DATA_CLOUD] await cloud.client.prefs.async_update(remote_enabled=False) connection.send_result(msg["id"], await _account_data(hass, cloud)) @@ -634,7 +634,7 @@ async def google_assistant_get( msg: dict[str, Any], ) -> None: """Get data for a single google assistant entity.""" - cloud: Cloud[CloudClient] = hass.data[DOMAIN] + cloud = hass.data[DATA_CLOUD] gconf = await cloud.client.get_google_config() entity_id: str = msg["entity_id"] state = hass.states.get(entity_id) @@ -682,7 +682,7 @@ async def google_assistant_list( msg: dict[str, Any], ) -> None: """List all google assistant entities.""" - cloud: Cloud[CloudClient] = hass.data[DOMAIN] + cloud = hass.data[DATA_CLOUD] gconf = await cloud.client.get_google_config() entities = google_helpers.async_get_entities(hass, gconf) @@ -774,7 +774,7 @@ async def alexa_list( msg: dict[str, Any], ) -> None: """List all alexa entities.""" - cloud: Cloud[CloudClient] = hass.data[DOMAIN] + cloud = hass.data[DATA_CLOUD] alexa_config = await cloud.client.get_alexa_config() entities = alexa_entities.async_get_entities(hass, alexa_config) @@ -800,7 +800,7 @@ async def alexa_sync( msg: dict[str, Any], ) -> None: """Sync with Alexa.""" - cloud: Cloud[CloudClient] = hass.data[DOMAIN] + cloud = hass.data[DATA_CLOUD] alexa_config = await cloud.client.get_alexa_config() async with asyncio.timeout(10): @@ -830,7 +830,7 @@ async def thingtalk_convert( msg: dict[str, Any], ) -> None: """Convert a query.""" - cloud: Cloud[CloudClient] = hass.data[DOMAIN] + cloud = hass.data[DATA_CLOUD] async with asyncio.timeout(10): try: diff --git a/homeassistant/components/cloud/repairs.py b/homeassistant/components/cloud/repairs.py index 9042a010589..fe418fb5340 100644 --- a/homeassistant/components/cloud/repairs.py +++ b/homeassistant/components/cloud/repairs.py @@ -5,7 +5,6 @@ from __future__ import annotations import asyncio from typing import Any -from hass_nabucasa import Cloud import voluptuous as vol from homeassistant.components.repairs import ( @@ -17,8 +16,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import issue_registry as ir -from .client import CloudClient -from .const import DOMAIN +from .const import DATA_CLOUD, DOMAIN from .subscription import async_migrate_paypal_agreement, async_subscription_info BACKOFF_TIME = 5 @@ -73,7 +71,7 @@ class LegacySubscriptionRepairFlow(RepairsFlow): async def async_step_change_plan(self, _: None = None) -> FlowResult: """Wait for the user to authorize the app installation.""" - cloud: Cloud[CloudClient] = self.hass.data[DOMAIN] + cloud = self.hass.data[DATA_CLOUD] async def _async_wait_for_plan_change() -> None: flow_manager = repairs_flow_manager(self.hass) diff --git a/homeassistant/components/cloud/stt.py b/homeassistant/components/cloud/stt.py index c68e9f245ee..b2154448d3a 100644 --- a/homeassistant/components/cloud/stt.py +++ b/homeassistant/components/cloud/stt.py @@ -2,7 +2,6 @@ from __future__ import annotations -import asyncio from collections.abc import AsyncIterable import logging @@ -28,7 +27,7 @@ from homeassistant.setup import async_when_setup from .assist_pipeline import async_migrate_cloud_pipeline_engine from .client import CloudClient -from .const import DATA_PLATFORMS_SETUP, DOMAIN, STT_ENTITY_UNIQUE_ID +from .const import DATA_CLOUD, DATA_PLATFORMS_SETUP, STT_ENTITY_UNIQUE_ID _LOGGER = logging.getLogger(__name__) @@ -39,9 +38,9 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up Home Assistant Cloud speech platform via config entry.""" - stt_platform_loaded: asyncio.Event = hass.data[DATA_PLATFORMS_SETUP][Platform.STT] + stt_platform_loaded = hass.data[DATA_PLATFORMS_SETUP][Platform.STT] stt_platform_loaded.set() - cloud: Cloud[CloudClient] = hass.data[DOMAIN] + cloud = hass.data[DATA_CLOUD] async_add_entities([CloudProviderEntity(cloud)]) diff --git a/homeassistant/components/cloud/system_health.py b/homeassistant/components/cloud/system_health.py index 866626f4c79..0e65aa93eaf 100644 --- a/homeassistant/components/cloud/system_health.py +++ b/homeassistant/components/cloud/system_health.py @@ -2,13 +2,10 @@ from typing import Any -from hass_nabucasa import Cloud - from homeassistant.components import system_health from homeassistant.core import HomeAssistant, callback -from .client import CloudClient -from .const import DOMAIN +from .const import DATA_CLOUD @callback @@ -21,7 +18,7 @@ def async_register( async def system_health_info(hass: HomeAssistant) -> dict[str, Any]: """Get info for the info page.""" - cloud: Cloud[CloudClient] = hass.data[DOMAIN] + cloud = hass.data[DATA_CLOUD] client = cloud.client data: dict[str, Any] = { diff --git a/homeassistant/components/cloud/tts.py b/homeassistant/components/cloud/tts.py index 53cec74d133..8cf18c08314 100644 --- a/homeassistant/components/cloud/tts.py +++ b/homeassistant/components/cloud/tts.py @@ -2,7 +2,6 @@ from __future__ import annotations -import asyncio import logging from typing import Any @@ -31,7 +30,7 @@ from homeassistant.setup import async_when_setup from .assist_pipeline import async_migrate_cloud_pipeline_engine from .client import CloudClient -from .const import DATA_PLATFORMS_SETUP, DOMAIN, TTS_ENTITY_UNIQUE_ID +from .const import DATA_CLOUD, DATA_PLATFORMS_SETUP, DOMAIN, TTS_ENTITY_UNIQUE_ID from .prefs import CloudPreferences ATTR_GENDER = "gender" @@ -97,7 +96,7 @@ async def async_get_engine( discovery_info: DiscoveryInfoType | None = None, ) -> CloudProvider: """Set up Cloud speech component.""" - cloud: Cloud[CloudClient] = hass.data[DOMAIN] + cloud = hass.data[DATA_CLOUD] cloud_provider = CloudProvider(cloud) if discovery_info is not None: discovery_info["platform_loaded"].set() @@ -110,9 +109,9 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up Home Assistant Cloud text-to-speech platform.""" - tts_platform_loaded: asyncio.Event = hass.data[DATA_PLATFORMS_SETUP][Platform.TTS] + tts_platform_loaded = hass.data[DATA_PLATFORMS_SETUP][Platform.TTS] tts_platform_loaded.set() - cloud: Cloud[CloudClient] = hass.data[DOMAIN] + cloud = hass.data[DATA_CLOUD] async_add_entities([CloudTTSEntity(cloud)]) diff --git a/tests/components/cloud/__init__.py b/tests/components/cloud/__init__.py index d527cbbeec2..82280336a8c 100644 --- a/tests/components/cloud/__init__.py +++ b/tests/components/cloud/__init__.py @@ -2,10 +2,9 @@ from unittest.mock import AsyncMock, patch -from hass_nabucasa import Cloud - from homeassistant.components import cloud from homeassistant.components.cloud import const, prefs as cloud_prefs +from homeassistant.components.cloud.const import DATA_CLOUD from homeassistant.setup import async_setup_component PIPELINE_DATA = { @@ -64,7 +63,7 @@ async def mock_cloud(hass, config=None): assert await async_setup_component(hass, "homeassistant", {}) assert await async_setup_component(hass, cloud.DOMAIN, {"cloud": config or {}}) - cloud_inst: Cloud = hass.data["cloud"] + cloud_inst = hass.data[DATA_CLOUD] with patch("hass_nabucasa.Cloud.run_executor", AsyncMock(return_value=None)): await cloud_inst.initialize() @@ -79,5 +78,5 @@ def mock_cloud_prefs(hass, prefs): const.PREF_GOOGLE_SETTINGS_VERSION: cloud_prefs.GOOGLE_SETTINGS_VERSION, } prefs_to_set.update(prefs) - hass.data[cloud.DOMAIN].client._prefs._prefs = prefs_to_set - return hass.data[cloud.DOMAIN].client._prefs + hass.data[DATA_CLOUD].client._prefs._prefs = prefs_to_set + return hass.data[DATA_CLOUD].client._prefs diff --git a/tests/components/cloud/conftest.py b/tests/components/cloud/conftest.py index ebd9ea6663e..3058718551e 100644 --- a/tests/components/cloud/conftest.py +++ b/tests/components/cloud/conftest.py @@ -17,7 +17,8 @@ import jwt import pytest from typing_extensions import AsyncGenerator -from homeassistant.components.cloud import CloudClient, const, prefs +from homeassistant.components.cloud import CloudClient, prefs +from homeassistant.components.cloud.const import DATA_CLOUD from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow @@ -223,7 +224,7 @@ async def mock_cloud_setup(hass): @pytest.fixture def mock_cloud_login(hass, mock_cloud_setup): """Mock cloud is logged in.""" - hass.data[const.DOMAIN].id_token = jwt.encode( + hass.data[DATA_CLOUD].id_token = jwt.encode( { "email": "hello@home-assistant.io", "custom:sub-exp": "2300-01-03", @@ -231,7 +232,7 @@ def mock_cloud_login(hass, mock_cloud_setup): }, "test", ) - with patch.object(hass.data[const.DOMAIN].auth, "async_check_token"): + with patch.object(hass.data[DATA_CLOUD].auth, "async_check_token"): yield @@ -248,7 +249,7 @@ def mock_auth_fixture(): @pytest.fixture def mock_expired_cloud_login(hass, mock_cloud_setup): """Mock cloud is logged in.""" - hass.data[const.DOMAIN].id_token = jwt.encode( + hass.data[DATA_CLOUD].id_token = jwt.encode( { "email": "hello@home-assistant.io", "custom:sub-exp": "2018-01-01", diff --git a/tests/components/cloud/test_account_link.py b/tests/components/cloud/test_account_link.py index 3f108961bc5..7a85531904a 100644 --- a/tests/components/cloud/test_account_link.py +++ b/tests/components/cloud/test_account_link.py @@ -9,6 +9,7 @@ import pytest from homeassistant import config_entries from homeassistant.components.cloud import account_link +from homeassistant.components.cloud.const import DATA_CLOUD from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import config_entry_oauth2_flow @@ -133,7 +134,7 @@ async def test_setup_provide_implementation(hass: HomeAssistant) -> None: async def test_get_services_cached(hass: HomeAssistant) -> None: """Test that we cache services.""" - hass.data["cloud"] = None + hass.data[DATA_CLOUD] = None services = 1 @@ -165,7 +166,7 @@ async def test_get_services_cached(hass: HomeAssistant) -> None: async def test_get_services_error(hass: HomeAssistant) -> None: """Test that we cache services.""" - hass.data["cloud"] = None + hass.data[DATA_CLOUD] = None with ( patch.object(account_link, "CACHE_TIMEOUT", 0), @@ -181,7 +182,7 @@ async def test_get_services_error(hass: HomeAssistant) -> None: @pytest.mark.usefixtures("current_request_with_host") async def test_implementation(hass: HomeAssistant, flow_handler) -> None: """Test Cloud OAuth2 implementation.""" - hass.data["cloud"] = None + hass.data[DATA_CLOUD] = None impl = account_link.CloudOAuth2Implementation(hass, "test") assert impl.name == "Home Assistant Cloud" diff --git a/tests/components/cloud/test_alexa_config.py b/tests/components/cloud/test_alexa_config.py index f37ee114220..e4ad425d4d4 100644 --- a/tests/components/cloud/test_alexa_config.py +++ b/tests/components/cloud/test_alexa_config.py @@ -8,6 +8,7 @@ import pytest from homeassistant.components.alexa import errors from homeassistant.components.cloud import ALEXA_SCHEMA, alexa_config from homeassistant.components.cloud.const import ( + DATA_CLOUD, PREF_ALEXA_DEFAULT_EXPOSE, PREF_ALEXA_ENTITY_CONFIGS, PREF_SHOULD_EXPOSE, @@ -425,7 +426,7 @@ async def test_alexa_entity_registry_sync( expose_new(hass, True) await alexa_config.CloudAlexaConfig( - hass, ALEXA_SCHEMA({}), "mock-user-id", cloud_prefs, hass.data["cloud"] + hass, ALEXA_SCHEMA({}), "mock-user-id", cloud_prefs, hass.data[DATA_CLOUD] ).async_initialize() with patch_sync_helper() as (to_update, to_remove): @@ -506,11 +507,11 @@ def test_enabled_requires_valid_sub( ) -> None: """Test that alexa config enabled requires a valid Cloud sub.""" assert cloud_prefs.alexa_enabled - assert hass.data["cloud"].is_logged_in - assert hass.data["cloud"].subscription_expired + assert hass.data[DATA_CLOUD].is_logged_in + assert hass.data[DATA_CLOUD].subscription_expired config = alexa_config.CloudAlexaConfig( - hass, ALEXA_SCHEMA({}), "mock-user-id", cloud_prefs, hass.data["cloud"] + hass, ALEXA_SCHEMA({}), "mock-user-id", cloud_prefs, hass.data[DATA_CLOUD] ) assert not config.enabled diff --git a/tests/components/cloud/test_client.py b/tests/components/cloud/test_client.py index 3126d56e3fb..62af4e88857 100644 --- a/tests/components/cloud/test_client.py +++ b/tests/components/cloud/test_client.py @@ -15,6 +15,7 @@ from homeassistant.components.cloud.client import ( CloudClient, ) from homeassistant.components.cloud.const import ( + DATA_CLOUD, PREF_ALEXA_REPORT_STATE, PREF_ENABLE_ALEXA, PREF_ENABLE_GOOGLE, @@ -63,7 +64,7 @@ async def test_handler_alexa(hass: HomeAssistant) -> None: ) mock_cloud_prefs(hass, {PREF_ALEXA_REPORT_STATE: False}) - cloud = hass.data["cloud"] + cloud = hass.data[DATA_CLOUD] resp = await cloud.client.async_alexa_message( test_alexa.get_new_request("Alexa.Discovery", "Discover") @@ -83,7 +84,7 @@ async def test_handler_alexa(hass: HomeAssistant) -> None: async def test_handler_alexa_disabled(hass: HomeAssistant, mock_cloud_fixture) -> None: """Test handler Alexa when user has disabled it.""" mock_cloud_fixture._prefs[PREF_ENABLE_ALEXA] = False - cloud = hass.data["cloud"] + cloud = hass.data[DATA_CLOUD] resp = await cloud.client.async_alexa_message( test_alexa.get_new_request("Alexa.Discovery", "Discover") @@ -117,7 +118,7 @@ async def test_handler_google_actions(hass: HomeAssistant) -> None: ) mock_cloud_prefs(hass, {}) - cloud = hass.data["cloud"] + cloud = hass.data[DATA_CLOUD] reqid = "5711642932632160983" data = {"requestId": reqid, "inputs": [{"intent": "action.devices.SYNC"}]} @@ -164,7 +165,7 @@ async def test_handler_google_actions_disabled( reqid = "5711642932632160983" data = {"requestId": reqid, "inputs": [{"intent": intent}]} - cloud = hass.data["cloud"] + cloud = hass.data[DATA_CLOUD] with patch( "hass_nabucasa.Cloud._decode_claims", return_value={"cognito:username": "myUserName"}, @@ -182,7 +183,7 @@ async def test_webhook_msg( with patch("hass_nabucasa.Cloud.initialize"): setup = await async_setup_component(hass, "cloud", {"cloud": {}}) assert setup - cloud = hass.data["cloud"] + cloud = hass.data[DATA_CLOUD] await cloud.client.prefs.async_initialize() await cloud.client.prefs.async_update( @@ -269,7 +270,7 @@ async def test_google_config_expose_entity( "light", "test", "unique", suggested_object_id="kitchen" ) - cloud_client = hass.data[DOMAIN].client + cloud_client = hass.data[DATA_CLOUD].client state = State(entity_entry.entity_id, "on") gconf = await cloud_client.get_google_config() @@ -293,7 +294,7 @@ async def test_google_config_should_2fa( "light", "test", "unique", suggested_object_id="kitchen" ) - cloud_client = hass.data[DOMAIN].client + cloud_client = hass.data[DATA_CLOUD].client gconf = await cloud_client.get_google_config() state = State(entity_entry.entity_id, "on") @@ -350,7 +351,7 @@ async def test_system_msg(hass: HomeAssistant) -> None: with patch("hass_nabucasa.Cloud.initialize"): setup = await async_setup_component(hass, "cloud", {"cloud": {}}) assert setup - cloud = hass.data["cloud"] + cloud = hass.data[DATA_CLOUD] assert cloud.client.relayer_region is None @@ -373,7 +374,7 @@ async def test_cloud_connection_info(hass: HomeAssistant) -> None: hexmock.return_value = "12345678901234567890" setup = await async_setup_component(hass, "cloud", {"cloud": {}}) assert setup - cloud = hass.data["cloud"] + cloud = hass.data[DATA_CLOUD] response = await cloud.client.async_cloud_connection_info({}) diff --git a/tests/components/cloud/test_google_config.py b/tests/components/cloud/test_google_config.py index 89882d92037..40d3f6ef2c5 100644 --- a/tests/components/cloud/test_google_config.py +++ b/tests/components/cloud/test_google_config.py @@ -8,6 +8,7 @@ import pytest from homeassistant.components.cloud import GACTIONS_SCHEMA from homeassistant.components.cloud.const import ( + DATA_CLOUD, PREF_DISABLE_2FA, PREF_GOOGLE_DEFAULT_EXPOSE, PREF_GOOGLE_ENTITY_CONFIGS, @@ -196,7 +197,7 @@ async def test_google_entity_registry_sync( expose_new(hass, True) config = CloudGoogleConfig( - hass, GACTIONS_SCHEMA({}), "mock-user-id", cloud_prefs, hass.data["cloud"] + hass, GACTIONS_SCHEMA({}), "mock-user-id", cloud_prefs, hass.data[DATA_CLOUD] ) await config.async_initialize() await config.async_connect_agent_user("mock-user-id") @@ -264,7 +265,7 @@ async def test_google_device_registry_sync( ) -> None: """Test Google config responds to device registry.""" config = CloudGoogleConfig( - hass, GACTIONS_SCHEMA({}), "mock-user-id", cloud_prefs, hass.data["cloud"] + hass, GACTIONS_SCHEMA({}), "mock-user-id", cloud_prefs, hass.data[DATA_CLOUD] ) # Enable exposing new entities to Google @@ -333,7 +334,7 @@ async def test_sync_google_when_started( ) -> None: """Test Google config syncs on init.""" config = CloudGoogleConfig( - hass, GACTIONS_SCHEMA({}), "mock-user-id", cloud_prefs, hass.data["cloud"] + hass, GACTIONS_SCHEMA({}), "mock-user-id", cloud_prefs, hass.data[DATA_CLOUD] ) with patch.object(config, "async_sync_entities_all") as mock_sync: await config.async_initialize() @@ -346,7 +347,7 @@ async def test_sync_google_on_home_assistant_start( ) -> None: """Test Google config syncs when home assistant started.""" config = CloudGoogleConfig( - hass, GACTIONS_SCHEMA({}), "mock-user-id", cloud_prefs, hass.data["cloud"] + hass, GACTIONS_SCHEMA({}), "mock-user-id", cloud_prefs, hass.data[DATA_CLOUD] ) hass.set_state(CoreState.not_running) with patch.object(config, "async_sync_entities_all") as mock_sync: @@ -441,11 +442,11 @@ def test_enabled_requires_valid_sub( ) -> None: """Test that google config enabled requires a valid Cloud sub.""" assert cloud_prefs.google_enabled - assert hass.data["cloud"].is_logged_in - assert hass.data["cloud"].subscription_expired + assert hass.data[DATA_CLOUD].is_logged_in + assert hass.data[DATA_CLOUD].subscription_expired config = CloudGoogleConfig( - hass, GACTIONS_SCHEMA({}), "mock-user-id", cloud_prefs, hass.data["cloud"] + hass, GACTIONS_SCHEMA({}), "mock-user-id", cloud_prefs, hass.data[DATA_CLOUD] ) assert not config.enabled @@ -494,7 +495,7 @@ async def test_google_handle_logout( await cloud_prefs.get_cloud_user() with patch.object( - hass.data["cloud"].auth, + hass.data[DATA_CLOUD].auth, "async_check_token", side_effect=AssertionError("Should not be called"), ): @@ -857,7 +858,7 @@ async def test_google_config_get_agent_user_id( ) -> None: """Test overridden get_agent_user_id_from_webhook method.""" config = CloudGoogleConfig( - hass, GACTIONS_SCHEMA({}), "mock-user-id", cloud_prefs, hass.data["cloud"] + hass, GACTIONS_SCHEMA({}), "mock-user-id", cloud_prefs, hass.data[DATA_CLOUD] ) assert ( config.get_agent_user_id_from_webhook(cloud_prefs.google_local_webhook_id) diff --git a/tests/components/cloud/test_init.py b/tests/components/cloud/test_init.py index 9cc1324ebc1..db8253b0329 100644 --- a/tests/components/cloud/test_init.py +++ b/tests/components/cloud/test_init.py @@ -4,7 +4,6 @@ from collections.abc import Callable, Coroutine from typing import Any from unittest.mock import MagicMock, patch -from hass_nabucasa import Cloud import pytest from homeassistant.components import cloud @@ -13,7 +12,7 @@ from homeassistant.components.cloud import ( CloudNotConnected, async_get_or_create_cloudhook, ) -from homeassistant.components.cloud.const import DOMAIN, PREF_CLOUDHOOKS +from homeassistant.components.cloud.const import DATA_CLOUD, DOMAIN, PREF_CLOUDHOOKS from homeassistant.components.cloud.prefs import STORAGE_KEY from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import Context, HomeAssistant @@ -47,7 +46,7 @@ async def test_constructor_loads_info_from_config(hass: HomeAssistant) -> None: ) assert result - cl = hass.data["cloud"] + cl = hass.data[DATA_CLOUD] assert cl.mode == cloud.MODE_DEV assert cl.cognito_client_id == "test-cognito_client_id" assert cl.user_pool_id == "test-user_pool_id" @@ -65,7 +64,7 @@ async def test_remote_services( hass: HomeAssistant, mock_cloud_fixture, hass_read_only_user: MockUser ) -> None: """Setup cloud component and test services.""" - cloud = hass.data[DOMAIN] + cloud = hass.data[DATA_CLOUD] assert hass.services.has_service(DOMAIN, "remote_connect") assert hass.services.has_service(DOMAIN, "remote_disconnect") @@ -145,7 +144,7 @@ async def test_setup_existing_cloud_user( async def test_on_connect(hass: HomeAssistant, mock_cloud_fixture) -> None: """Test cloud on connect triggers.""" - cl: Cloud[cloud.client.CloudClient] = hass.data["cloud"] + cl = hass.data[DATA_CLOUD] assert len(cl.iot._on_connect) == 3 @@ -202,7 +201,7 @@ async def test_on_connect(hass: HomeAssistant, mock_cloud_fixture) -> None: async def test_remote_ui_url(hass: HomeAssistant, mock_cloud_fixture) -> None: """Test getting remote ui url.""" - cl = hass.data["cloud"] + cl = hass.data[DATA_CLOUD] # Not logged in with pytest.raises(cloud.CloudNotAvailable): From bbb8bb31f9cb4bdc2220635391cb4fdd1e91e9cd Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Mon, 24 Jun 2024 21:03:41 +0200 Subject: [PATCH 1132/1445] Only raise Reolink re-auth flow when login fails 3 consecutive times (#120291) --- homeassistant/components/reolink/__init__.py | 13 +++++-- homeassistant/components/reolink/host.py | 2 ++ tests/components/reolink/test_init.py | 38 ++++++++++++++++++-- 3 files changed, 47 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/reolink/__init__.py b/homeassistant/components/reolink/__init__.py index 27bd504e9bb..a3e49f1f526 100644 --- a/homeassistant/components/reolink/__init__.py +++ b/homeassistant/components/reolink/__init__.py @@ -38,6 +38,7 @@ PLATFORMS = [ ] DEVICE_UPDATE_INTERVAL = timedelta(seconds=60) FIRMWARE_UPDATE_INTERVAL = timedelta(hours=12) +NUM_CRED_ERRORS = 3 @dataclass @@ -82,10 +83,16 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b try: await host.update_states() except CredentialsInvalidError as err: - await host.stop() - raise ConfigEntryAuthFailed(err) from err - except ReolinkError as err: + host.credential_errors += 1 + if host.credential_errors >= NUM_CRED_ERRORS: + await host.stop() + raise ConfigEntryAuthFailed(err) from err raise UpdateFailed(str(err)) from err + except ReolinkError as err: + host.credential_errors = 0 + raise UpdateFailed(str(err)) from err + + host.credential_errors = 0 async with asyncio.timeout(host.api.timeout * (RETRY_ATTEMPTS + 2)): await host.renew() diff --git a/homeassistant/components/reolink/host.py b/homeassistant/components/reolink/host.py index c69a80ce972..bccb5c5b684 100644 --- a/homeassistant/components/reolink/host.py +++ b/homeassistant/components/reolink/host.py @@ -79,6 +79,8 @@ class ReolinkHost: ) self.firmware_ch_list: list[int | None] = [] + self.credential_errors: int = 0 + self.webhook_id: str | None = None self._onvif_push_supported: bool = True self._onvif_long_poll_supported: bool = True diff --git a/tests/components/reolink/test_init.py b/tests/components/reolink/test_init.py index 466836e52ef..922fe0829f6 100644 --- a/tests/components/reolink/test_init.py +++ b/tests/components/reolink/test_init.py @@ -7,11 +7,16 @@ from unittest.mock import AsyncMock, MagicMock, Mock, patch import pytest from reolink_aio.exceptions import CredentialsInvalidError, ReolinkError -from homeassistant.components.reolink import FIRMWARE_UPDATE_INTERVAL, const +from homeassistant.components.reolink import ( + DEVICE_UPDATE_INTERVAL, + FIRMWARE_UPDATE_INTERVAL, + NUM_CRED_ERRORS, + const, +) from homeassistant.config import async_process_ha_core_config from homeassistant.config_entries import ConfigEntryState from homeassistant.const import STATE_OFF, STATE_UNAVAILABLE, Platform -from homeassistant.core import HomeAssistant +from homeassistant.core import DOMAIN as HA_DOMAIN, HomeAssistant from homeassistant.helpers import ( device_registry as dr, entity_registry as er, @@ -58,7 +63,7 @@ pytestmark = pytest.mark.usefixtures("reolink_connect", "reolink_platforms") ConfigEntryState.SETUP_RETRY, ), ( - "get_states", + "get_host_data", AsyncMock(side_effect=CredentialsInvalidError("Test error")), ConfigEntryState.SETUP_ERROR, ), @@ -113,6 +118,33 @@ async def test_firmware_error_twice( assert hass.states.is_state(entity_id, STATE_UNAVAILABLE) +async def test_credential_error_three( + hass: HomeAssistant, + reolink_connect: MagicMock, + config_entry: MockConfigEntry, + issue_registry: ir.IssueRegistry, +) -> None: + """Test when the update gives credential error 3 times.""" + with patch("homeassistant.components.reolink.PLATFORMS", [Platform.SWITCH]): + assert await hass.config_entries.async_setup(config_entry.entry_id) is True + await hass.async_block_till_done() + assert config_entry.state is ConfigEntryState.LOADED + + reolink_connect.get_states = AsyncMock( + side_effect=CredentialsInvalidError("Test error") + ) + + issue_id = f"config_entry_reauth_{const.DOMAIN}_{config_entry.entry_id}" + for _ in range(NUM_CRED_ERRORS): + assert (HA_DOMAIN, issue_id) not in issue_registry.issues + async_fire_time_changed( + hass, utcnow() + DEVICE_UPDATE_INTERVAL + timedelta(seconds=30) + ) + await hass.async_block_till_done() + + assert (HA_DOMAIN, issue_id) in issue_registry.issues + + async def test_entry_reloading( hass: HomeAssistant, config_entry: MockConfigEntry, From 72d1b3e36093e2e93a97f41392d5891a5255475c Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 24 Jun 2024 21:05:23 +0200 Subject: [PATCH 1133/1445] Deprecate Nanoleaf gesture device trigger (#120078) --- homeassistant/components/nanoleaf/device_trigger.py | 10 ++++++++++ homeassistant/components/nanoleaf/strings.json | 6 ++++++ 2 files changed, 16 insertions(+) diff --git a/homeassistant/components/nanoleaf/device_trigger.py b/homeassistant/components/nanoleaf/device_trigger.py index 15b14e9719e..b4049f2199d 100644 --- a/homeassistant/components/nanoleaf/device_trigger.py +++ b/homeassistant/components/nanoleaf/device_trigger.py @@ -16,6 +16,7 @@ from homeassistant.const import ( ) from homeassistant.core import CALLBACK_TYPE, HomeAssistant from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType @@ -60,6 +61,15 @@ async def async_attach_trigger( trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Attach a trigger.""" + async_create_issue( + hass, + DOMAIN, + "deprecated_device_trigger_nanoleaf", + is_fixable=False, + breaks_in_ha_version="2025.1.0", + severity=IssueSeverity.WARNING, + translation_key="deprecated_device_trigger", + ) event_config = event_trigger.TRIGGER_SCHEMA( { event_trigger.CONF_PLATFORM: CONF_EVENT, diff --git a/homeassistant/components/nanoleaf/strings.json b/homeassistant/components/nanoleaf/strings.json index 40cd7294ec3..ef7df8c0ab5 100644 --- a/homeassistant/components/nanoleaf/strings.json +++ b/homeassistant/components/nanoleaf/strings.json @@ -52,5 +52,11 @@ } } } + }, + "issues": { + "deprecated_device_trigger": { + "title": "Nanoleaf device trigger is deprecated", + "description": "The Nanoleaf device trigger is deprecated and will be removed in a future release. You can now use the gesture event entity to build automations." + } } } From d0961ca473a099b88e353ba6dcd6b71c7bb1fc0f Mon Sep 17 00:00:00 2001 From: Markus Jacobsen Date: Mon, 24 Jun 2024 21:06:57 +0200 Subject: [PATCH 1134/1445] Make Bang & Olufsen products ignore .m3u media source files (#120317) --- homeassistant/components/bang_olufsen/media_player.py | 7 +++++-- homeassistant/components/bang_olufsen/strings.json | 5 +++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/bang_olufsen/media_player.py b/homeassistant/components/bang_olufsen/media_player.py index 5c214a3fb17..d23c75046ff 100644 --- a/homeassistant/components/bang_olufsen/media_player.py +++ b/homeassistant/components/bang_olufsen/media_player.py @@ -45,6 +45,7 @@ from homeassistant.components.media_player import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_MODEL from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -572,9 +573,11 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity): media_id = async_process_play_media_url(self.hass, sourced_media.url) - # Remove playlist extension as it is unsupported. + # Exit if the source uses unsupported file. if media_id.endswith(".m3u"): - media_id = media_id.replace(".m3u", "") + raise HomeAssistantError( + translation_domain=DOMAIN, translation_key="m3u_invalid_format" + ) if announce: extra = kwargs.get(ATTR_MEDIA_EXTRA, {}) diff --git a/homeassistant/components/bang_olufsen/strings.json b/homeassistant/components/bang_olufsen/strings.json index 3cebfb891bc..93b55cf0db2 100644 --- a/homeassistant/components/bang_olufsen/strings.json +++ b/homeassistant/components/bang_olufsen/strings.json @@ -24,5 +24,10 @@ "description": "Confirm the configuration of the {model}-{serial_number} @ {host}." } } + }, + "exceptions": { + "m3u_invalid_format": { + "message": "Media sources with the .m3u extension are not supported." + } } } From 6d917f0242f35e487358fda4d2090501056bdc9e Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Mon, 24 Jun 2024 15:21:51 -0500 Subject: [PATCH 1135/1445] Don't run timer callbacks for delayed commands (#120367) * Don't send timer events for delayed commands * Don't run timer callbacks for delayed commands --- homeassistant/components/intent/timers.py | 28 +++++++++++------------ tests/components/intent/test_timers.py | 18 +++++---------- 2 files changed, 20 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/intent/timers.py b/homeassistant/components/intent/timers.py index cddfce55b9f..40b55134e92 100644 --- a/homeassistant/components/intent/timers.py +++ b/homeassistant/components/intent/timers.py @@ -278,7 +278,7 @@ class TimerManager: name=f"Timer {timer_id}", ) - if timer.device_id in self.handlers: + if (not timer.conversation_command) and (timer.device_id in self.handlers): self.handlers[timer.device_id](TimerEventType.STARTED, timer) _LOGGER.debug( "Timer started: id=%s, name=%s, hours=%s, minutes=%s, seconds=%s, device_id=%s", @@ -317,7 +317,7 @@ class TimerManager: timer.cancel() - if timer.device_id in self.handlers: + if (not timer.conversation_command) and (timer.device_id in self.handlers): self.handlers[timer.device_id](TimerEventType.CANCELLED, timer) _LOGGER.debug( "Timer cancelled: id=%s, name=%s, seconds_left=%s, device_id=%s", @@ -346,7 +346,7 @@ class TimerManager: name=f"Timer {timer_id}", ) - if timer.device_id in self.handlers: + if (not timer.conversation_command) and (timer.device_id in self.handlers): self.handlers[timer.device_id](TimerEventType.UPDATED, timer) if seconds > 0: @@ -384,7 +384,7 @@ class TimerManager: task = self.timer_tasks.pop(timer_id) task.cancel() - if timer.device_id in self.handlers: + if (not timer.conversation_command) and (timer.device_id in self.handlers): self.handlers[timer.device_id](TimerEventType.UPDATED, timer) _LOGGER.debug( "Timer paused: id=%s, name=%s, seconds_left=%s, device_id=%s", @@ -410,7 +410,7 @@ class TimerManager: name=f"Timer {timer.id}", ) - if timer.device_id in self.handlers: + if (not timer.conversation_command) and (timer.device_id in self.handlers): self.handlers[timer.device_id](TimerEventType.UPDATED, timer) _LOGGER.debug( "Timer unpaused: id=%s, name=%s, seconds_left=%s, device_id=%s", @@ -426,15 +426,6 @@ class TimerManager: timer.finish() - if timer.device_id in self.handlers: - self.handlers[timer.device_id](TimerEventType.FINISHED, timer) - _LOGGER.debug( - "Timer finished: id=%s, name=%s, device_id=%s", - timer_id, - timer.name, - timer.device_id, - ) - if timer.conversation_command: # pylint: disable-next=import-outside-toplevel from homeassistant.components.conversation import async_converse @@ -451,6 +442,15 @@ class TimerManager: ), "timer assist command", ) + elif timer.device_id in self.handlers: + self.handlers[timer.device_id](TimerEventType.FINISHED, timer) + + _LOGGER.debug( + "Timer finished: id=%s, name=%s, device_id=%s", + timer_id, + timer.name, + timer.device_id, + ) def is_timer_device(self, device_id: str) -> bool: """Return True if device has been registered to handle timer events.""" diff --git a/tests/components/intent/test_timers.py b/tests/components/intent/test_timers.py index a884fd13de5..329db6e8b2b 100644 --- a/tests/components/intent/test_timers.py +++ b/tests/components/intent/test_timers.py @@ -1430,18 +1430,10 @@ async def test_start_timer_with_conversation_command( timer_name = "test timer" test_command = "turn on the lights" agent_id = "test_agent" - finished_event = asyncio.Event() - @callback - def handle_timer(event_type: TimerEventType, timer: TimerInfo) -> None: - if event_type == TimerEventType.FINISHED: - assert timer.conversation_command == test_command - assert timer.conversation_agent_id == agent_id - finished_event.set() + mock_handle_timer = MagicMock() + async_register_timer_handler(hass, device_id, mock_handle_timer) - async_register_timer_handler(hass, device_id, handle_timer) - - # Device id is required if no conversation command timer_manager = TimerManager(hass) with pytest.raises(ValueError): timer_manager.start_timer( @@ -1468,9 +1460,11 @@ async def test_start_timer_with_conversation_command( assert result.response_type == intent.IntentResponseType.ACTION_DONE - async with asyncio.timeout(1): - await finished_event.wait() + # No timer events for delayed commands + mock_handle_timer.assert_not_called() + # Wait for process service call to finish + await hass.async_block_till_done() mock_converse.assert_called_once() assert mock_converse.call_args.args[1] == test_command From 1e16afb43b3a38cd82375054b95e62835a5bf818 Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Mon, 24 Jun 2024 16:03:34 -0500 Subject: [PATCH 1136/1445] Fix pylint error in Google generative AI tests (#120371) * Fix pylint error * Add second fix --- .../google_generative_ai_conversation/test_config_flow.py | 2 +- tests/components/google_generative_ai_conversation/test_init.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/components/google_generative_ai_conversation/test_config_flow.py b/tests/components/google_generative_ai_conversation/test_config_flow.py index 24ed06a408f..c835a4d3b13 100644 --- a/tests/components/google_generative_ai_conversation/test_config_flow.py +++ b/tests/components/google_generative_ai_conversation/test_config_flow.py @@ -3,7 +3,7 @@ from unittest.mock import AsyncMock, Mock, patch from google.api_core.exceptions import ClientError, DeadlineExceeded -from google.rpc.error_details_pb2 import ErrorInfo +from google.rpc.error_details_pb2 import ErrorInfo # pylint: disable=no-name-in-module import pytest from homeassistant import config_entries diff --git a/tests/components/google_generative_ai_conversation/test_init.py b/tests/components/google_generative_ai_conversation/test_init.py index 7afa9b4a31e..eeaa777f614 100644 --- a/tests/components/google_generative_ai_conversation/test_init.py +++ b/tests/components/google_generative_ai_conversation/test_init.py @@ -3,7 +3,7 @@ from unittest.mock import AsyncMock, MagicMock, patch from google.api_core.exceptions import ClientError, DeadlineExceeded -from google.rpc.error_details_pb2 import ErrorInfo +from google.rpc.error_details_pb2 import ErrorInfo # pylint: disable=no-name-in-module import pytest from syrupy.assertion import SnapshotAssertion From f1ddf80dff8644b4aab33cf966896faf0673bb47 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 25 Jun 2024 03:34:23 +0200 Subject: [PATCH 1137/1445] Fix dlna_dms test RuntimeWarning (#120341) --- tests/components/dlna_dms/conftest.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/components/dlna_dms/conftest.py b/tests/components/dlna_dms/conftest.py index 1fa56f4bc24..ed05dfa4c76 100644 --- a/tests/components/dlna_dms/conftest.py +++ b/tests/components/dlna_dms/conftest.py @@ -4,7 +4,7 @@ from __future__ import annotations from collections.abc import AsyncIterable, Iterable from typing import Final, cast -from unittest.mock import Mock, create_autospec, patch, seal +from unittest.mock import AsyncMock, MagicMock, Mock, create_autospec, patch, seal from async_upnp_client.client import UpnpDevice, UpnpService from async_upnp_client.utils import absolute_url @@ -87,6 +87,8 @@ def aiohttp_session_requester_mock() -> Iterable[Mock]: with patch( "homeassistant.components.dlna_dms.dms.AiohttpSessionRequester", autospec=True ) as requester_mock: + requester_mock.return_value = mock = AsyncMock() + mock.async_http_request.return_value.body = MagicMock() yield requester_mock From 59080a3a6fa3befd25dc6c8f7502f8da90631afb Mon Sep 17 00:00:00 2001 From: "Mr. Bubbles" Date: Tue, 25 Jun 2024 08:00:19 +0200 Subject: [PATCH 1138/1445] Strip whitespace characters from token in One-Time-Passwort (OTP) integration (#120380) --- homeassistant/components/otp/config_flow.py | 2 ++ tests/components/otp/test_config_flow.py | 20 ++++++++++++++------ 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/otp/config_flow.py b/homeassistant/components/otp/config_flow.py index 15d04c910ad..6aa4532683a 100644 --- a/homeassistant/components/otp/config_flow.py +++ b/homeassistant/components/otp/config_flow.py @@ -4,6 +4,7 @@ from __future__ import annotations import binascii import logging +from re import sub from typing import Any import pyotp @@ -47,6 +48,7 @@ class TOTPConfigFlow(ConfigFlow, domain=DOMAIN): errors: dict[str, str] = {} if user_input is not None: if user_input.get(CONF_TOKEN) and not user_input.get(CONF_NEW_TOKEN): + user_input[CONF_TOKEN] = sub(r"\s+", "", user_input[CONF_TOKEN]) try: await self.hass.async_add_executor_job( pyotp.TOTP(user_input[CONF_TOKEN]).now diff --git a/tests/components/otp/test_config_flow.py b/tests/components/otp/test_config_flow.py index eefb1a6f4e0..f9fac433ff9 100644 --- a/tests/components/otp/test_config_flow.py +++ b/tests/components/otp/test_config_flow.py @@ -12,6 +12,10 @@ from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType TEST_DATA = { + CONF_NAME: "OTP Sensor", + CONF_TOKEN: "2FX5 FBSY RE6V EC2F SHBQ CRKO 2GND VZ52", +} +TEST_DATA_RESULT = { CONF_NAME: "OTP Sensor", CONF_TOKEN: "2FX5FBSYRE6VEC2FSHBQCRKO2GNDVZ52", } @@ -41,7 +45,11 @@ async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: result["flow_id"], TEST_DATA, ) - await hass.async_block_till_done() + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "OTP Sensor" + assert result["data"] == TEST_DATA_RESULT + assert len(mock_setup_entry.mock_calls) == 1 @pytest.mark.parametrize( @@ -85,7 +93,7 @@ async def test_errors_and_recover( assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "OTP Sensor" - assert result["data"] == TEST_DATA + assert result["data"] == TEST_DATA_RESULT assert len(mock_setup_entry.mock_calls) == 1 @@ -96,13 +104,13 @@ async def test_flow_import(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_IMPORT}, - data=TEST_DATA, + data=TEST_DATA_RESULT, ) await hass.async_block_till_done() assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "OTP Sensor" - assert result["data"] == TEST_DATA + assert result["data"] == TEST_DATA_RESULT @pytest.mark.usefixtures("mock_pyotp") @@ -134,7 +142,7 @@ async def test_generate_new_token( assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "OTP Sensor" - assert result["data"] == TEST_DATA + assert result["data"] == TEST_DATA_RESULT assert len(mock_setup_entry.mock_calls) == 1 @@ -181,5 +189,5 @@ async def test_generate_new_token_errors( assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "OTP Sensor" - assert result["data"] == TEST_DATA + assert result["data"] == TEST_DATA_RESULT assert len(mock_setup_entry.mock_calls) == 1 From aa8427abe507687e1395d1e9f2c2404027ba4dd4 Mon Sep 17 00:00:00 2001 From: Markus Jacobsen Date: Tue, 25 Jun 2024 08:02:02 +0200 Subject: [PATCH 1139/1445] Bump Bang & Olufsen mozart-open-api to 3.4.1.8.6 fixing blocking IO call (#120369) Co-authored-by: J. Nick Koston --- homeassistant/components/bang_olufsen/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/bang_olufsen/manifest.json b/homeassistant/components/bang_olufsen/manifest.json index f2b31293227..3cc9fdb5cd1 100644 --- a/homeassistant/components/bang_olufsen/manifest.json +++ b/homeassistant/components/bang_olufsen/manifest.json @@ -6,6 +6,6 @@ "documentation": "https://www.home-assistant.io/integrations/bang_olufsen", "integration_type": "device", "iot_class": "local_push", - "requirements": ["mozart-api==3.4.1.8.5"], + "requirements": ["mozart-api==3.4.1.8.6"], "zeroconf": ["_bangolufsen._tcp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index e21548fce85..87d0bfd84c6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1359,7 +1359,7 @@ motionblindsble==0.1.0 motioneye-client==0.3.14 # homeassistant.components.bang_olufsen -mozart-api==3.4.1.8.5 +mozart-api==3.4.1.8.6 # homeassistant.components.mullvad mullvad-api==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2dd55cd9f7c..4890b8bdf3e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1107,7 +1107,7 @@ motionblindsble==0.1.0 motioneye-client==0.3.14 # homeassistant.components.bang_olufsen -mozart-api==3.4.1.8.5 +mozart-api==3.4.1.8.6 # homeassistant.components.mullvad mullvad-api==1.0.0 From 59998bc48a9d805adc2bc041ab1b3e51715d279e Mon Sep 17 00:00:00 2001 From: G Johansson Date: Tue, 25 Jun 2024 08:02:57 +0200 Subject: [PATCH 1140/1445] Use runtime_data in github (#120362) --- homeassistant/components/github/__init__.py | 18 +++++++++--------- homeassistant/components/github/diagnostics.py | 5 +---- homeassistant/components/github/sensor.py | 6 +++--- 3 files changed, 13 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/github/__init__.py b/homeassistant/components/github/__init__.py index 20df559b819..74575e38e09 100644 --- a/homeassistant/components/github/__init__.py +++ b/homeassistant/components/github/__init__.py @@ -19,10 +19,11 @@ from .coordinator import GitHubDataUpdateCoordinator PLATFORMS: list[Platform] = [Platform.SENSOR] -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: - """Set up GitHub from a config entry.""" - hass.data.setdefault(DOMAIN, {}) +type GithubConfigEntry = ConfigEntry[dict[str, GitHubDataUpdateCoordinator]] + +async def async_setup_entry(hass: HomeAssistant, entry: GithubConfigEntry) -> bool: + """Set up GitHub from a config entry.""" client = GitHubAPI( token=entry.data[CONF_ACCESS_TOKEN], session=async_get_clientsession(hass), @@ -31,6 +32,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: repositories: list[str] = entry.options[CONF_REPOSITORIES] + entry.runtime_data = {} for repository in repositories: coordinator = GitHubDataUpdateCoordinator( hass=hass, @@ -43,7 +45,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if not entry.pref_disable_polling: await coordinator.subscribe() - hass.data[DOMAIN][repository] = coordinator + entry.runtime_data[repository] = coordinator async_cleanup_device_registry(hass=hass, entry=entry) @@ -81,15 +83,13 @@ def async_cleanup_device_registry( break -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: GithubConfigEntry) -> bool: """Unload a config entry.""" - repositories: dict[str, GitHubDataUpdateCoordinator] = hass.data[DOMAIN] + repositories = entry.runtime_data for coordinator in repositories.values(): coordinator.unsubscribe() - if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): - hass.data.pop(DOMAIN) - return unload_ok + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: diff --git a/homeassistant/components/github/diagnostics.py b/homeassistant/components/github/diagnostics.py index df1e4b4a4cf..8d2d496a813 100644 --- a/homeassistant/components/github/diagnostics.py +++ b/homeassistant/components/github/diagnostics.py @@ -14,9 +14,6 @@ from homeassistant.helpers.aiohttp_client import ( async_get_clientsession, ) -from .const import DOMAIN -from .coordinator import GitHubDataUpdateCoordinator - async def async_get_config_entry_diagnostics( hass: HomeAssistant, @@ -37,7 +34,7 @@ async def async_get_config_entry_diagnostics( else: data["rate_limit"] = rate_limit_response.data.as_dict - repositories: dict[str, GitHubDataUpdateCoordinator] = hass.data[DOMAIN] + repositories = config_entry.runtime_data data["repositories"] = {} for repository, coordinator in repositories.items(): diff --git a/homeassistant/components/github/sensor.py b/homeassistant/components/github/sensor.py index a082f888767..9a2b5ef5ac4 100644 --- a/homeassistant/components/github/sensor.py +++ b/homeassistant/components/github/sensor.py @@ -11,7 +11,6 @@ from homeassistant.components.sensor import ( SensorEntityDescription, SensorStateClass, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo @@ -19,6 +18,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity +from . import GithubConfigEntry from .const import DOMAIN from .coordinator import GitHubDataUpdateCoordinator @@ -145,11 +145,11 @@ SENSOR_DESCRIPTIONS: tuple[GitHubSensorEntityDescription, ...] = ( async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: GithubConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up GitHub sensor based on a config entry.""" - repositories: dict[str, GitHubDataUpdateCoordinator] = hass.data[DOMAIN] + repositories = entry.runtime_data async_add_entities( ( GitHubSensorEntity(coordinator, description) From adc074f60adc55926faab970b7913e2e76bb7106 Mon Sep 17 00:00:00 2001 From: Luke Lashley Date: Tue, 25 Jun 2024 02:03:20 -0400 Subject: [PATCH 1141/1445] Remove humbertogontijo as Codeowner for Roborock (#120336) --- CODEOWNERS | 4 ++-- homeassistant/components/roborock/manifest.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 9b23b5cc83a..2e954ed1315 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1185,8 +1185,8 @@ build.json @home-assistant/supervisor /tests/components/rituals_perfume_genie/ @milanmeu @frenck /homeassistant/components/rmvtransport/ @cgtobi /tests/components/rmvtransport/ @cgtobi -/homeassistant/components/roborock/ @humbertogontijo @Lash-L -/tests/components/roborock/ @humbertogontijo @Lash-L +/homeassistant/components/roborock/ @Lash-L +/tests/components/roborock/ @Lash-L /homeassistant/components/roku/ @ctalkington /tests/components/roku/ @ctalkington /homeassistant/components/romy/ @xeniter diff --git a/homeassistant/components/roborock/manifest.json b/homeassistant/components/roborock/manifest.json index 42c0f9ba347..51b1835247f 100644 --- a/homeassistant/components/roborock/manifest.json +++ b/homeassistant/components/roborock/manifest.json @@ -1,7 +1,7 @@ { "domain": "roborock", "name": "Roborock", - "codeowners": ["@humbertogontijo", "@Lash-L"], + "codeowners": ["@Lash-L"], "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/roborock", "iot_class": "local_polling", From fd0fee1900bb50b9ad2198694eef98aa55d5f46c Mon Sep 17 00:00:00 2001 From: "Mr. Bubbles" Date: Tue, 25 Jun 2024 08:09:54 +0200 Subject: [PATCH 1142/1445] Add button platform to pyLoad integration (#120359) --- homeassistant/components/pyload/__init__.py | 2 +- homeassistant/components/pyload/button.py | 107 ++++++++++ homeassistant/components/pyload/const.py | 3 + homeassistant/components/pyload/icons.json | 14 ++ homeassistant/components/pyload/strings.json | 14 ++ .../pyload/snapshots/test_button.ambr | 185 ++++++++++++++++++ tests/components/pyload/test_button.py | 83 ++++++++ tests/components/pyload/test_sensor.py | 14 +- 8 files changed, 420 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/pyload/button.py create mode 100644 tests/components/pyload/snapshots/test_button.ambr create mode 100644 tests/components/pyload/test_button.py diff --git a/homeassistant/components/pyload/__init__.py b/homeassistant/components/pyload/__init__.py index d7c7e9454ea..b30b044e238 100644 --- a/homeassistant/components/pyload/__init__.py +++ b/homeassistant/components/pyload/__init__.py @@ -22,7 +22,7 @@ from homeassistant.helpers.aiohttp_client import async_create_clientsession from .coordinator import PyLoadCoordinator -PLATFORMS: list[Platform] = [Platform.SENSOR] +PLATFORMS: list[Platform] = [Platform.BUTTON, Platform.SENSOR] type PyLoadConfigEntry = ConfigEntry[PyLoadCoordinator] diff --git a/homeassistant/components/pyload/button.py b/homeassistant/components/pyload/button.py new file mode 100644 index 00000000000..1f6bf3c3d10 --- /dev/null +++ b/homeassistant/components/pyload/button.py @@ -0,0 +1,107 @@ +"""Support for monitoring pyLoad.""" + +from __future__ import annotations + +from collections.abc import Awaitable, Callable +from dataclasses import dataclass +from enum import StrEnum +from typing import Any + +from pyloadapi.api import PyLoadAPI + +from homeassistant.components.button import ButtonEntity, ButtonEntityDescription +from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from . import PyLoadConfigEntry +from .const import DOMAIN, MANUFACTURER, SERVICE_NAME +from .coordinator import PyLoadCoordinator + + +@dataclass(kw_only=True, frozen=True) +class PyLoadButtonEntityDescription(ButtonEntityDescription): + """Describes pyLoad button entity.""" + + press_fn: Callable[[PyLoadAPI], Awaitable[Any]] + + +class PyLoadButtonEntity(StrEnum): + """PyLoad button Entities.""" + + ABORT_DOWNLOADS = "abort_downloads" + RESTART_FAILED = "restart_failed" + DELETE_FINISHED = "delete_finished" + RESTART = "restart" + + +SENSOR_DESCRIPTIONS: tuple[PyLoadButtonEntityDescription, ...] = ( + PyLoadButtonEntityDescription( + key=PyLoadButtonEntity.ABORT_DOWNLOADS, + translation_key=PyLoadButtonEntity.ABORT_DOWNLOADS, + press_fn=lambda api: api.stop_all_downloads(), + ), + PyLoadButtonEntityDescription( + key=PyLoadButtonEntity.RESTART_FAILED, + translation_key=PyLoadButtonEntity.RESTART_FAILED, + press_fn=lambda api: api.restart_failed(), + ), + PyLoadButtonEntityDescription( + key=PyLoadButtonEntity.DELETE_FINISHED, + translation_key=PyLoadButtonEntity.DELETE_FINISHED, + press_fn=lambda api: api.delete_finished(), + ), + PyLoadButtonEntityDescription( + key=PyLoadButtonEntity.RESTART, + translation_key=PyLoadButtonEntity.RESTART, + press_fn=lambda api: api.restart(), + entity_registry_enabled_default=False, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: PyLoadConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up buttons from a config entry.""" + + coordinator = entry.runtime_data + + async_add_entities( + PyLoadBinarySensor(coordinator, description) + for description in SENSOR_DESCRIPTIONS + ) + + +class PyLoadBinarySensor(CoordinatorEntity[PyLoadCoordinator], ButtonEntity): + """Representation of a pyLoad button.""" + + _attr_has_entity_name = True + entity_description: PyLoadButtonEntityDescription + + def __init__( + self, + coordinator: PyLoadCoordinator, + entity_description: PyLoadButtonEntityDescription, + ) -> None: + """Initialize the button.""" + super().__init__(coordinator) + self._attr_unique_id = ( + f"{coordinator.config_entry.entry_id}_{entity_description.key}" + ) + self.entity_description = entity_description + self._attr_device_info = DeviceInfo( + entry_type=DeviceEntryType.SERVICE, + manufacturer=MANUFACTURER, + model=SERVICE_NAME, + configuration_url=coordinator.pyload.api_url, + identifiers={(DOMAIN, coordinator.config_entry.entry_id)}, + translation_key=DOMAIN, + ) + + async def async_press(self) -> None: + """Handle the button press.""" + await self.entity_description.press_fn(self.coordinator.pyload) diff --git a/homeassistant/components/pyload/const.py b/homeassistant/components/pyload/const.py index 8ee1c05696f..9419786fd88 100644 --- a/homeassistant/components/pyload/const.py +++ b/homeassistant/components/pyload/const.py @@ -7,3 +7,6 @@ DEFAULT_NAME = "pyLoad" DEFAULT_PORT = 8000 ISSUE_PLACEHOLDER = {"url": "/config/integrations/dashboard/add?domain=pyload"} + +MANUFACTURER = "pyLoad Team" +SERVICE_NAME = "pyLoad" diff --git a/homeassistant/components/pyload/icons.json b/homeassistant/components/pyload/icons.json index bc068165851..8f6f016641f 100644 --- a/homeassistant/components/pyload/icons.json +++ b/homeassistant/components/pyload/icons.json @@ -1,5 +1,19 @@ { "entity": { + "button": { + "abort_downloads": { + "default": "mdi:stop" + }, + "restart_failed": { + "default": "mdi:cached" + }, + "delete_finished": { + "default": "mdi:trash-can" + }, + "restart": { + "default": "mdi:restart" + } + }, "sensor": { "speed": { "default": "mdi:speedometer" diff --git a/homeassistant/components/pyload/strings.json b/homeassistant/components/pyload/strings.json index cc53ef7465b..94c0c29d286 100644 --- a/homeassistant/components/pyload/strings.json +++ b/homeassistant/components/pyload/strings.json @@ -28,6 +28,20 @@ } }, "entity": { + "button": { + "abort_downloads": { + "name": "Abort all running downloads" + }, + "restart_failed": { + "name": "Restart all failed files" + }, + "delete_finished": { + "name": "Delete finished files/packages" + }, + "restart": { + "name": "Restart pyload core" + } + }, "sensor": { "speed": { "name": "Speed" diff --git a/tests/components/pyload/snapshots/test_button.ambr b/tests/components/pyload/snapshots/test_button.ambr new file mode 100644 index 00000000000..c9a901aba15 --- /dev/null +++ b/tests/components/pyload/snapshots/test_button.ambr @@ -0,0 +1,185 @@ +# serializer version: 1 +# name: test_state[button.pyload_abort_all_running_downloads-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': None, + 'entity_id': 'button.pyload_abort_all_running_downloads', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Abort all running downloads', + 'platform': 'pyload', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'XXXXXXXXXXXXXX_abort_downloads', + 'unit_of_measurement': None, + }) +# --- +# name: test_state[button.pyload_abort_all_running_downloads-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'pyload Abort all running downloads', + }), + 'context': , + 'entity_id': 'button.pyload_abort_all_running_downloads', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_state[button.pyload_delete_finished_files_packages-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': None, + 'entity_id': 'button.pyload_delete_finished_files_packages', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Delete finished files/packages', + 'platform': 'pyload', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'XXXXXXXXXXXXXX_delete_finished', + 'unit_of_measurement': None, + }) +# --- +# name: test_state[button.pyload_delete_finished_files_packages-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'pyload Delete finished files/packages', + }), + 'context': , + 'entity_id': 'button.pyload_delete_finished_files_packages', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_state[button.pyload_restart_all_failed_files-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': None, + 'entity_id': 'button.pyload_restart_all_failed_files', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Restart all failed files', + 'platform': 'pyload', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'XXXXXXXXXXXXXX_restart_failed', + 'unit_of_measurement': None, + }) +# --- +# name: test_state[button.pyload_restart_all_failed_files-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'pyload Restart all failed files', + }), + 'context': , + 'entity_id': 'button.pyload_restart_all_failed_files', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_state[button.pyload_restart_pyload_core-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': None, + 'entity_id': 'button.pyload_restart_pyload_core', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Restart pyload core', + 'platform': 'pyload', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'XXXXXXXXXXXXXX_restart', + 'unit_of_measurement': None, + }) +# --- +# name: test_state[button.pyload_restart_pyload_core-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'pyload Restart pyload core', + }), + 'context': , + 'entity_id': 'button.pyload_restart_pyload_core', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- diff --git a/tests/components/pyload/test_button.py b/tests/components/pyload/test_button.py new file mode 100644 index 00000000000..b30a4cefd42 --- /dev/null +++ b/tests/components/pyload/test_button.py @@ -0,0 +1,83 @@ +"""The tests for the button component.""" + +from collections.abc import AsyncGenerator +from unittest.mock import AsyncMock, call, patch + +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS +from homeassistant.components.pyload.button import PyLoadButtonEntity +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import ATTR_ENTITY_ID, Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from tests.common import MockConfigEntry, snapshot_platform + +API_CALL = { + PyLoadButtonEntity.ABORT_DOWNLOADS: call.stop_all_downloads, + PyLoadButtonEntity.RESTART_FAILED: call.restart_failed, + PyLoadButtonEntity.DELETE_FINISHED: call.delete_finished, + PyLoadButtonEntity.RESTART: call.restart, +} + + +@pytest.fixture(autouse=True) +async def button_only() -> AsyncGenerator[None, None]: + """Enable only the button platform.""" + with patch( + "homeassistant.components.pyload.PLATFORMS", + [Platform.BUTTON], + ): + yield + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_state( + hass: HomeAssistant, + config_entry: MockConfigEntry, + snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, + mock_pyloadapi: AsyncMock, +) -> None: + """Test button state.""" + + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.LOADED + + await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id) + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_button_press( + hass: HomeAssistant, + config_entry: MockConfigEntry, + mock_pyloadapi: AsyncMock, + entity_registry: er.EntityRegistry, +) -> None: + """Test switch turn on method.""" + + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.LOADED + + entity_entries = er.async_entries_for_config_entry( + entity_registry, config_entry.entry_id + ) + + for entity_entry in entity_entries: + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + {ATTR_ENTITY_ID: entity_entry.entity_id}, + blocking=True, + ) + await hass.async_block_till_done() + assert API_CALL[entity_entry.translation_key] in mock_pyloadapi.method_calls + mock_pyloadapi.reset_mock() diff --git a/tests/components/pyload/test_sensor.py b/tests/components/pyload/test_sensor.py index 49795284fc6..61a9a872f33 100644 --- a/tests/components/pyload/test_sensor.py +++ b/tests/components/pyload/test_sensor.py @@ -1,6 +1,7 @@ """Tests for the pyLoad Sensors.""" -from unittest.mock import AsyncMock +from collections.abc import AsyncGenerator +from unittest.mock import AsyncMock, patch from freezegun.api import FrozenDateTimeFactory from pyloadapi.exceptions import CannotConnect, InvalidAuth, ParserError @@ -11,6 +12,7 @@ from homeassistant.components.pyload.const import DOMAIN from homeassistant.components.pyload.coordinator import SCAN_INTERVAL from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import Platform from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant from homeassistant.helpers import entity_registry as er, issue_registry as ir from homeassistant.helpers.typing import ConfigType @@ -19,6 +21,16 @@ from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform +@pytest.fixture(autouse=True) +async def sensor_only() -> AsyncGenerator[None, None]: + """Enable only the sensor platform.""" + with patch( + "homeassistant.components.pyload.PLATFORMS", + [Platform.SENSOR], + ): + yield + + async def test_setup( hass: HomeAssistant, config_entry: MockConfigEntry, From ced6c0dd8c3e26beae4cfc140b6d50802e2e7b3c Mon Sep 17 00:00:00 2001 From: Jan Schneider Date: Tue, 25 Jun 2024 08:11:26 +0200 Subject: [PATCH 1143/1445] Update moehlenhoff-alpha2 to 1.3.1 (#120351) Co-authored-by: J. Nick Koston --- homeassistant/components/moehlenhoff_alpha2/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/moehlenhoff_alpha2/manifest.json b/homeassistant/components/moehlenhoff_alpha2/manifest.json index f4cc11453e0..14f40991a84 100644 --- a/homeassistant/components/moehlenhoff_alpha2/manifest.json +++ b/homeassistant/components/moehlenhoff_alpha2/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/moehlenhoff_alpha2", "iot_class": "local_push", - "requirements": ["moehlenhoff-alpha2==1.3.0"] + "requirements": ["moehlenhoff-alpha2==1.3.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 87d0bfd84c6..8c5d9662e20 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1341,7 +1341,7 @@ minio==7.1.12 moat-ble==0.1.1 # homeassistant.components.moehlenhoff_alpha2 -moehlenhoff-alpha2==1.3.0 +moehlenhoff-alpha2==1.3.1 # homeassistant.components.monzo monzopy==1.3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4890b8bdf3e..9755e583d9d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1089,7 +1089,7 @@ minio==7.1.12 moat-ble==0.1.1 # homeassistant.components.moehlenhoff_alpha2 -moehlenhoff-alpha2==1.3.0 +moehlenhoff-alpha2==1.3.1 # homeassistant.components.monzo monzopy==1.3.0 From 744161928e450978f73b48a1d26849c372cf2bf6 Mon Sep 17 00:00:00 2001 From: David Bonnes Date: Tue, 25 Jun 2024 07:25:04 +0100 Subject: [PATCH 1144/1445] Bump evohome-async to 0.4.20 (#120353) --- homeassistant/components/evohome/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/evohome/manifest.json b/homeassistant/components/evohome/manifest.json index 6b893dc8f48..e81e71c5b07 100644 --- a/homeassistant/components/evohome/manifest.json +++ b/homeassistant/components/evohome/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/evohome", "iot_class": "cloud_polling", "loggers": ["evohomeasync", "evohomeasync2"], - "requirements": ["evohome-async==0.4.19"] + "requirements": ["evohome-async==0.4.20"] } diff --git a/requirements_all.txt b/requirements_all.txt index 8c5d9662e20..da4afc918f3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -846,7 +846,7 @@ eufylife-ble-client==0.1.8 # evdev==1.6.1 # homeassistant.components.evohome -evohome-async==0.4.19 +evohome-async==0.4.20 # homeassistant.components.faa_delays faadelays==2023.9.1 From 6fb400f76bcb722bc07c27349065d3d9ed9583c8 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 25 Jun 2024 09:47:43 +0200 Subject: [PATCH 1145/1445] Add test of get_all_descriptions resolving features (#120384) --- tests/helpers/test_service.py | 77 +++++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/tests/helpers/test_service.py b/tests/helpers/test_service.py index 60fe87db9d2..3e7d8e6ef03 100644 --- a/tests/helpers/test_service.py +++ b/tests/helpers/test_service.py @@ -971,6 +971,83 @@ async def test_async_get_all_descriptions_dot_keys(hass: HomeAssistant) -> None: } +async def test_async_get_all_descriptions_filter(hass: HomeAssistant) -> None: + """Test async_get_all_descriptions with filters.""" + service_descriptions = """ + test_service: + target: + entity: + domain: alarm_control_panel + supported_features: + - alarm_control_panel.AlarmControlPanelEntityFeature.ARM_HOME + fields: + temperature: + filter: + supported_features: + - alarm_control_panel.AlarmControlPanelEntityFeature.ARM_HOME + attribute: + supported_color_modes: + - light.ColorMode.COLOR_TEMP + selector: + number: + """ + + domain = "test_domain" + + hass.services.async_register(domain, "test_service", lambda call: None) + mock_integration(hass, MockModule(domain), top_level_files={"services.yaml"}) + assert await async_setup_component(hass, domain, {}) + + def load_yaml(fname, secrets=None): + with io.StringIO(service_descriptions) as file: + return parse_yaml(file) + + with ( + patch( + "homeassistant.helpers.service._load_services_files", + side_effect=service._load_services_files, + ) as proxy_load_services_files, + patch( + "homeassistant.util.yaml.loader.load_yaml", + side_effect=load_yaml, + ) as mock_load_yaml, + ): + descriptions = await service.async_get_all_descriptions(hass) + + mock_load_yaml.assert_called_once_with("services.yaml", None) + assert proxy_load_services_files.mock_calls[0][1][1] == unordered( + [ + await async_get_integration(hass, domain), + ] + ) + + test_service_schema = { + "description": "", + "fields": { + "temperature": { + "filter": { + "attribute": {"supported_color_modes": ["color_temp"]}, + "supported_features": [1], + }, + "selector": {"number": None}, + }, + }, + "name": "", + "target": { + "entity": [ + { + "domain": ["alarm_control_panel"], + "supported_features": [1], + }, + ], + }, + } + + assert descriptions == { + "test_domain": {"test_service": test_service_schema}, + } + + async def test_async_get_all_descriptions_failing_integration( hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: From 7f20c1a4891844610c002f35b44230facf16b810 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 25 Jun 2024 09:55:59 +0200 Subject: [PATCH 1146/1445] Improve type hints in demo tests (#120387) --- tests/components/demo/test_camera.py | 5 +++-- tests/components/demo/test_climate.py | 5 +++-- tests/components/demo/test_cover.py | 29 ++++++++++++++------------- tests/components/demo/test_init.py | 7 ++++--- tests/components/demo/test_light.py | 5 +++-- tests/components/demo/test_number.py | 5 +++-- tests/components/demo/test_switch.py | 11 +++++----- tests/components/demo/test_text.py | 5 +++-- 8 files changed, 40 insertions(+), 32 deletions(-) diff --git a/tests/components/demo/test_camera.py b/tests/components/demo/test_camera.py index ecbd3fecee3..756609ed094 100644 --- a/tests/components/demo/test_camera.py +++ b/tests/components/demo/test_camera.py @@ -3,6 +3,7 @@ from unittest.mock import patch import pytest +from typing_extensions import Generator from homeassistant.components.camera import ( DOMAIN as CAMERA_DOMAIN, @@ -24,7 +25,7 @@ ENTITY_CAMERA = "camera.demo_camera" @pytest.fixture -async def camera_only() -> None: +def camera_only() -> Generator[None]: """Enable only the button platform.""" with patch( "homeassistant.components.demo.COMPONENTS_WITH_CONFIG_ENTRY_DEMO_PLATFORM", @@ -34,7 +35,7 @@ async def camera_only() -> None: @pytest.fixture(autouse=True) -async def demo_camera(hass, camera_only): +async def demo_camera(hass: HomeAssistant, camera_only: None) -> None: """Initialize a demo camera platform.""" assert await async_setup_component( hass, CAMERA_DOMAIN, {CAMERA_DOMAIN: {"platform": DOMAIN}} diff --git a/tests/components/demo/test_climate.py b/tests/components/demo/test_climate.py index ff18f9e6a4e..682b85f0845 100644 --- a/tests/components/demo/test_climate.py +++ b/tests/components/demo/test_climate.py @@ -3,6 +3,7 @@ from unittest.mock import patch import pytest +from typing_extensions import Generator import voluptuous as vol from homeassistant.components.climate import ( @@ -50,7 +51,7 @@ ENTITY_HEATPUMP = "climate.heatpump" @pytest.fixture -async def climate_only() -> None: +def climate_only() -> Generator[None]: """Enable only the climate platform.""" with patch( "homeassistant.components.demo.COMPONENTS_WITH_CONFIG_ENTRY_DEMO_PLATFORM", @@ -60,7 +61,7 @@ async def climate_only() -> None: @pytest.fixture(autouse=True) -async def setup_demo_climate(hass, climate_only): +async def setup_demo_climate(hass: HomeAssistant, climate_only: None) -> None: """Initialize setup demo climate.""" hass.config.units = METRIC_SYSTEM assert await async_setup_component(hass, DOMAIN, {"climate": {"platform": "demo"}}) diff --git a/tests/components/demo/test_cover.py b/tests/components/demo/test_cover.py index 9ea743a0a01..7ee408d3bfc 100644 --- a/tests/components/demo/test_cover.py +++ b/tests/components/demo/test_cover.py @@ -4,6 +4,7 @@ from datetime import timedelta from unittest.mock import patch import pytest +from typing_extensions import Generator from homeassistant.components.cover import ( ATTR_CURRENT_POSITION, @@ -42,7 +43,7 @@ ENTITY_COVER = "cover.living_room_window" @pytest.fixture -async def cover_only() -> None: +def cover_only() -> Generator[None]: """Enable only the climate platform.""" with patch( "homeassistant.components.demo.COMPONENTS_WITH_CONFIG_ENTRY_DEMO_PLATFORM", @@ -51,15 +52,15 @@ async def cover_only() -> None: yield -@pytest.fixture -async def setup_comp(hass, cover_only): +@pytest.fixture(autouse=True) +async def setup_comp(hass: HomeAssistant, cover_only: None) -> None: """Set up demo cover component.""" with assert_setup_component(1, DOMAIN): await async_setup_component(hass, DOMAIN, CONFIG) await hass.async_block_till_done() -async def test_supported_features(hass: HomeAssistant, setup_comp) -> None: +async def test_supported_features(hass: HomeAssistant) -> None: """Test cover supported features.""" state = hass.states.get("cover.garage_door") assert state.attributes[ATTR_SUPPORTED_FEATURES] == 3 @@ -71,7 +72,7 @@ async def test_supported_features(hass: HomeAssistant, setup_comp) -> None: assert state.attributes[ATTR_SUPPORTED_FEATURES] == 255 -async def test_close_cover(hass: HomeAssistant, setup_comp) -> None: +async def test_close_cover(hass: HomeAssistant) -> None: """Test closing the cover.""" state = hass.states.get(ENTITY_COVER) assert state.state == STATE_OPEN @@ -92,7 +93,7 @@ async def test_close_cover(hass: HomeAssistant, setup_comp) -> None: assert state.attributes[ATTR_CURRENT_POSITION] == 0 -async def test_open_cover(hass: HomeAssistant, setup_comp) -> None: +async def test_open_cover(hass: HomeAssistant) -> None: """Test opening the cover.""" state = hass.states.get(ENTITY_COVER) assert state.state == STATE_OPEN @@ -112,7 +113,7 @@ async def test_open_cover(hass: HomeAssistant, setup_comp) -> None: assert state.attributes[ATTR_CURRENT_POSITION] == 100 -async def test_toggle_cover(hass: HomeAssistant, setup_comp) -> None: +async def test_toggle_cover(hass: HomeAssistant) -> None: """Test toggling the cover.""" # Start open await hass.services.async_call( @@ -152,7 +153,7 @@ async def test_toggle_cover(hass: HomeAssistant, setup_comp) -> None: assert state.attributes[ATTR_CURRENT_POSITION] == 100 -async def test_set_cover_position(hass: HomeAssistant, setup_comp) -> None: +async def test_set_cover_position(hass: HomeAssistant) -> None: """Test moving the cover to a specific position.""" state = hass.states.get(ENTITY_COVER) assert state.attributes[ATTR_CURRENT_POSITION] == 70 @@ -171,7 +172,7 @@ async def test_set_cover_position(hass: HomeAssistant, setup_comp) -> None: assert state.attributes[ATTR_CURRENT_POSITION] == 10 -async def test_stop_cover(hass: HomeAssistant, setup_comp) -> None: +async def test_stop_cover(hass: HomeAssistant) -> None: """Test stopping the cover.""" state = hass.states.get(ENTITY_COVER) assert state.attributes[ATTR_CURRENT_POSITION] == 70 @@ -190,7 +191,7 @@ async def test_stop_cover(hass: HomeAssistant, setup_comp) -> None: assert state.attributes[ATTR_CURRENT_POSITION] == 80 -async def test_close_cover_tilt(hass: HomeAssistant, setup_comp) -> None: +async def test_close_cover_tilt(hass: HomeAssistant) -> None: """Test closing the cover tilt.""" state = hass.states.get(ENTITY_COVER) assert state.attributes[ATTR_CURRENT_TILT_POSITION] == 50 @@ -206,7 +207,7 @@ async def test_close_cover_tilt(hass: HomeAssistant, setup_comp) -> None: assert state.attributes[ATTR_CURRENT_TILT_POSITION] == 0 -async def test_open_cover_tilt(hass: HomeAssistant, setup_comp) -> None: +async def test_open_cover_tilt(hass: HomeAssistant) -> None: """Test opening the cover tilt.""" state = hass.states.get(ENTITY_COVER) assert state.attributes[ATTR_CURRENT_TILT_POSITION] == 50 @@ -222,7 +223,7 @@ async def test_open_cover_tilt(hass: HomeAssistant, setup_comp) -> None: assert state.attributes[ATTR_CURRENT_TILT_POSITION] == 100 -async def test_toggle_cover_tilt(hass: HomeAssistant, setup_comp) -> None: +async def test_toggle_cover_tilt(hass: HomeAssistant) -> None: """Test toggling the cover tilt.""" # Start open await hass.services.async_call( @@ -259,7 +260,7 @@ async def test_toggle_cover_tilt(hass: HomeAssistant, setup_comp) -> None: assert state.attributes[ATTR_CURRENT_TILT_POSITION] == 100 -async def test_set_cover_tilt_position(hass: HomeAssistant, setup_comp) -> None: +async def test_set_cover_tilt_position(hass: HomeAssistant) -> None: """Test moving the cover til to a specific position.""" state = hass.states.get(ENTITY_COVER) assert state.attributes[ATTR_CURRENT_TILT_POSITION] == 50 @@ -278,7 +279,7 @@ async def test_set_cover_tilt_position(hass: HomeAssistant, setup_comp) -> None: assert state.attributes[ATTR_CURRENT_TILT_POSITION] == 90 -async def test_stop_cover_tilt(hass: HomeAssistant, setup_comp) -> None: +async def test_stop_cover_tilt(hass: HomeAssistant) -> None: """Test stopping the cover tilt.""" state = hass.states.get(ENTITY_COVER) assert state.attributes[ATTR_CURRENT_TILT_POSITION] == 50 diff --git a/tests/components/demo/test_init.py b/tests/components/demo/test_init.py index 2d60f7caf94..498a03600cb 100644 --- a/tests/components/demo/test_init.py +++ b/tests/components/demo/test_init.py @@ -4,6 +4,7 @@ import json from unittest.mock import patch import pytest +from typing_extensions import Generator from homeassistant.components.demo import DOMAIN from homeassistant.core import HomeAssistant @@ -12,19 +13,19 @@ from homeassistant.setup import async_setup_component @pytest.fixture -def mock_history(hass): +def mock_history(hass: HomeAssistant) -> None: """Mock history component loaded.""" hass.config.components.add("history") @pytest.fixture(autouse=True) -def mock_device_tracker_update_config(): +def mock_device_tracker_update_config() -> Generator[None]: """Prevent device tracker from creating known devices file.""" with patch("homeassistant.components.device_tracker.legacy.update_config"): yield -async def test_setting_up_demo(mock_history, hass: HomeAssistant) -> None: +async def test_setting_up_demo(mock_history: None, hass: HomeAssistant) -> None: """Test if we can set up the demo and dump it to JSON.""" assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) await hass.async_block_till_done() diff --git a/tests/components/demo/test_light.py b/tests/components/demo/test_light.py index b67acf3f60f..5c2c478b0bf 100644 --- a/tests/components/demo/test_light.py +++ b/tests/components/demo/test_light.py @@ -3,6 +3,7 @@ from unittest.mock import patch import pytest +from typing_extensions import Generator from homeassistant.components.demo import DOMAIN from homeassistant.components.light import ( @@ -27,7 +28,7 @@ ENTITY_LIGHT = "light.bed_light" @pytest.fixture -async def light_only() -> None: +def light_only() -> Generator[None]: """Enable only the light platform.""" with patch( "homeassistant.components.demo.COMPONENTS_WITH_CONFIG_ENTRY_DEMO_PLATFORM", @@ -37,7 +38,7 @@ async def light_only() -> None: @pytest.fixture(autouse=True) -async def setup_comp(hass, light_only): +async def setup_comp(hass: HomeAssistant, light_only: None) -> None: """Set up demo component.""" assert await async_setup_component( hass, LIGHT_DOMAIN, {LIGHT_DOMAIN: {"platform": DOMAIN}} diff --git a/tests/components/demo/test_number.py b/tests/components/demo/test_number.py index 20e3ce8fc11..37763b6e289 100644 --- a/tests/components/demo/test_number.py +++ b/tests/components/demo/test_number.py @@ -3,6 +3,7 @@ from unittest.mock import patch import pytest +from typing_extensions import Generator import voluptuous as vol from homeassistant.components.number import ( @@ -26,7 +27,7 @@ ENTITY_SMALL_RANGE = "number.small_range" @pytest.fixture -async def number_only() -> None: +def number_only() -> Generator[None]: """Enable only the number platform.""" with patch( "homeassistant.components.demo.COMPONENTS_WITH_CONFIG_ENTRY_DEMO_PLATFORM", @@ -36,7 +37,7 @@ async def number_only() -> None: @pytest.fixture(autouse=True) -async def setup_demo_number(hass, number_only): +async def setup_demo_number(hass: HomeAssistant, number_only: None) -> None: """Initialize setup demo Number entity.""" assert await async_setup_component(hass, DOMAIN, {"number": {"platform": "demo"}}) await hass.async_block_till_done() diff --git a/tests/components/demo/test_switch.py b/tests/components/demo/test_switch.py index d8c3284875e..8b78171fd17 100644 --- a/tests/components/demo/test_switch.py +++ b/tests/components/demo/test_switch.py @@ -3,6 +3,7 @@ from unittest.mock import patch import pytest +from typing_extensions import Generator from homeassistant.components.demo import DOMAIN from homeassistant.components.switch import ( @@ -18,7 +19,7 @@ SWITCH_ENTITY_IDS = ["switch.decorative_lights", "switch.ac"] @pytest.fixture -async def switch_only() -> None: +def switch_only() -> Generator[None]: """Enable only the switch platform.""" with patch( "homeassistant.components.demo.COMPONENTS_WITH_CONFIG_ENTRY_DEMO_PLATFORM", @@ -28,7 +29,7 @@ async def switch_only() -> None: @pytest.fixture(autouse=True) -async def setup_comp(hass, switch_only): +async def setup_comp(hass: HomeAssistant, switch_only: None) -> None: """Set up demo component.""" assert await async_setup_component( hass, SWITCH_DOMAIN, {SWITCH_DOMAIN: {"platform": DOMAIN}} @@ -37,7 +38,7 @@ async def setup_comp(hass, switch_only): @pytest.mark.parametrize("switch_entity_id", SWITCH_ENTITY_IDS) -async def test_turn_on(hass: HomeAssistant, switch_entity_id) -> None: +async def test_turn_on(hass: HomeAssistant, switch_entity_id: str) -> None: """Test switch turn on method.""" await hass.services.async_call( SWITCH_DOMAIN, @@ -61,7 +62,7 @@ async def test_turn_on(hass: HomeAssistant, switch_entity_id) -> None: @pytest.mark.parametrize("switch_entity_id", SWITCH_ENTITY_IDS) -async def test_turn_off(hass: HomeAssistant, switch_entity_id) -> None: +async def test_turn_off(hass: HomeAssistant, switch_entity_id: str) -> None: """Test switch turn off method.""" await hass.services.async_call( SWITCH_DOMAIN, @@ -86,7 +87,7 @@ async def test_turn_off(hass: HomeAssistant, switch_entity_id) -> None: @pytest.mark.parametrize("switch_entity_id", SWITCH_ENTITY_IDS) async def test_turn_off_without_entity_id( - hass: HomeAssistant, switch_entity_id + hass: HomeAssistant, switch_entity_id: str ) -> None: """Test switch turn off all switches.""" await hass.services.async_call( diff --git a/tests/components/demo/test_text.py b/tests/components/demo/test_text.py index faf611d9875..3588330c75c 100644 --- a/tests/components/demo/test_text.py +++ b/tests/components/demo/test_text.py @@ -3,6 +3,7 @@ from unittest.mock import patch import pytest +from typing_extensions import Generator from homeassistant.components.text import ( ATTR_MAX, @@ -25,7 +26,7 @@ ENTITY_TEXT = "text.text" @pytest.fixture -async def text_only() -> None: +def text_only() -> Generator[None]: """Enable only the text platform.""" with patch( "homeassistant.components.demo.COMPONENTS_WITH_CONFIG_ENTRY_DEMO_PLATFORM", @@ -35,7 +36,7 @@ async def text_only() -> None: @pytest.fixture(autouse=True) -async def setup_demo_text(hass, text_only): +async def setup_demo_text(hass: HomeAssistant, text_only: None) -> None: """Initialize setup demo text.""" assert await async_setup_component(hass, DOMAIN, {"text": {"platform": "demo"}}) await hass.async_block_till_done() From f8d5c9144a45b3a3fa5ab3489616fb07120705ec Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 25 Jun 2024 10:06:38 +0200 Subject: [PATCH 1147/1445] Improve type hints in device_tracker tests (#120390) --- tests/components/device_tracker/test_device_trigger.py | 2 +- tests/components/device_tracker/test_init.py | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/tests/components/device_tracker/test_device_trigger.py b/tests/components/device_tracker/test_device_trigger.py index 8932eb15997..4236e316424 100644 --- a/tests/components/device_tracker/test_device_trigger.py +++ b/tests/components/device_tracker/test_device_trigger.py @@ -43,7 +43,7 @@ def calls(hass: HomeAssistant) -> list[ServiceCall]: @pytest.fixture(autouse=True) -def setup_zone(hass): +def setup_zone(hass: HomeAssistant) -> None: """Create test zone.""" hass.loop.run_until_complete( async_setup_component( diff --git a/tests/components/device_tracker/test_init.py b/tests/components/device_tracker/test_init.py index 6999a99f7ba..cedf2a2f0bc 100644 --- a/tests/components/device_tracker/test_init.py +++ b/tests/components/device_tracker/test_init.py @@ -8,6 +8,7 @@ from types import ModuleType from unittest.mock import call, patch import pytest +from typing_extensions import Generator from homeassistant.components import device_tracker, zone from homeassistant.components.device_tracker import SourceType, const, legacy @@ -49,7 +50,7 @@ _LOGGER = logging.getLogger(__name__) @pytest.fixture(name="yaml_devices") -def mock_yaml_devices(hass): +def mock_yaml_devices(hass: HomeAssistant) -> Generator[str]: """Get a path for storing yaml devices.""" yaml_devices = hass.config.path(legacy.YAML_DEVICES) if os.path.isfile(yaml_devices): @@ -108,7 +109,7 @@ async def test_reading_broken_yaml_config(hass: HomeAssistant) -> None: assert res[0].dev_id == "my_device" -async def test_reading_yaml_config(hass: HomeAssistant, yaml_devices) -> None: +async def test_reading_yaml_config(hass: HomeAssistant, yaml_devices: str) -> None: """Test the rendering of the YAML configuration.""" dev_id = "test" device = legacy.Device( @@ -186,7 +187,7 @@ async def test_duplicate_mac_dev_id(mock_warning, hass: HomeAssistant) -> None: assert "Duplicate device IDs" in args[0], "Duplicate device IDs warning expected" -async def test_setup_without_yaml_file(hass: HomeAssistant, yaml_devices) -> None: +async def test_setup_without_yaml_file(hass: HomeAssistant, yaml_devices: str) -> None: """Test with no YAML file.""" with assert_setup_component(1, device_tracker.DOMAIN): assert await async_setup_component(hass, device_tracker.DOMAIN, TEST_PLATFORM) @@ -487,7 +488,7 @@ async def test_invalid_dev_id( assert not devices -async def test_see_state(hass: HomeAssistant, yaml_devices) -> None: +async def test_see_state(hass: HomeAssistant, yaml_devices: str) -> None: """Test device tracker see records state correctly.""" assert await async_setup_component(hass, device_tracker.DOMAIN, TEST_PLATFORM) await hass.async_block_till_done() From 46ed76df314454874b2a25bad2698dc62aa57a0e Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 25 Jun 2024 10:07:05 +0200 Subject: [PATCH 1148/1445] Improve type hints in diagnostics tests (#120391) --- tests/components/diagnostics/test_init.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/components/diagnostics/test_init.py b/tests/components/diagnostics/test_init.py index eeb4f420225..7f583395387 100644 --- a/tests/components/diagnostics/test_init.py +++ b/tests/components/diagnostics/test_init.py @@ -19,7 +19,7 @@ from tests.typing import ClientSessionGenerator, WebSocketGenerator @pytest.fixture(autouse=True) -async def mock_diagnostics_integration(hass): +async def mock_diagnostics_integration(hass: HomeAssistant) -> None: """Mock a diagnostics integration.""" hass.config.components.add("fake_integration") mock_platform( From 1d16cbec96801dd6fda9fda187ce1be43cc00003 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Tue, 25 Jun 2024 10:33:58 +0200 Subject: [PATCH 1149/1445] Move mqtt debouncer to mqtt utils (#120392) --- homeassistant/components/mqtt/client.py | 100 +--------------------- homeassistant/components/mqtt/util.py | 106 +++++++++++++++++++++++- tests/components/mqtt/conftest.py | 20 ++++- tests/components/mqtt/test_init.py | 100 +--------------------- tests/components/mqtt/test_util.py | 88 +++++++++++++++++++- 5 files changed, 213 insertions(+), 201 deletions(-) diff --git a/homeassistant/components/mqtt/client.py b/homeassistant/components/mqtt/client.py index 18ce89beb9b..7788c1db641 100644 --- a/homeassistant/components/mqtt/client.py +++ b/homeassistant/components/mqtt/client.py @@ -45,7 +45,6 @@ from homeassistant.helpers.start import async_at_started from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass from homeassistant.setup import SetupPhases, async_pause_setup -from homeassistant.util.async_ import create_eager_task from homeassistant.util.collection import chunked_or_all from homeassistant.util.logging import catch_log_exception, log_exception @@ -85,7 +84,7 @@ from .models import ( PublishPayloadType, ReceiveMessage, ) -from .util import get_file_path, mqtt_config_entry_enabled +from .util import EnsureJobAfterCooldown, get_file_path, mqtt_config_entry_enabled if TYPE_CHECKING: # Only import for paho-mqtt type checking here, imports are done locally @@ -358,103 +357,6 @@ class MqttClientSetup: return self._client -class EnsureJobAfterCooldown: - """Ensure a cool down period before executing a job. - - When a new execute request arrives we cancel the current request - and start a new one. - """ - - def __init__( - self, timeout: float, callback_job: Callable[[], Coroutine[Any, None, None]] - ) -> None: - """Initialize the timer.""" - self._loop = asyncio.get_running_loop() - self._timeout = timeout - self._callback = callback_job - self._task: asyncio.Task | None = None - self._timer: asyncio.TimerHandle | None = None - self._next_execute_time = 0.0 - - def set_timeout(self, timeout: float) -> None: - """Set a new timeout period.""" - self._timeout = timeout - - async def _async_job(self) -> None: - """Execute after a cooldown period.""" - try: - await self._callback() - except HomeAssistantError as ha_error: - _LOGGER.error("%s", ha_error) - - @callback - def _async_task_done(self, task: asyncio.Task) -> None: - """Handle task done.""" - self._task = None - - @callback - def async_execute(self) -> asyncio.Task: - """Execute the job.""" - if self._task: - # Task already running, - # so we schedule another run - self.async_schedule() - return self._task - - self._async_cancel_timer() - self._task = create_eager_task(self._async_job()) - self._task.add_done_callback(self._async_task_done) - return self._task - - @callback - def _async_cancel_timer(self) -> None: - """Cancel any pending task.""" - if self._timer: - self._timer.cancel() - self._timer = None - - @callback - def async_schedule(self) -> None: - """Ensure we execute after a cooldown period.""" - # We want to reschedule the timer in the future - # every time this is called. - next_when = self._loop.time() + self._timeout - if not self._timer: - self._timer = self._loop.call_at(next_when, self._async_timer_reached) - return - - if self._timer.when() < next_when: - # Timer already running, set the next execute time - # if it fires too early, it will get rescheduled - self._next_execute_time = next_when - - @callback - def _async_timer_reached(self) -> None: - """Handle timer fire.""" - self._timer = None - if self._loop.time() >= self._next_execute_time: - self.async_execute() - return - # Timer fired too early because there were multiple - # calls async_schedule. Reschedule the timer. - self._timer = self._loop.call_at( - self._next_execute_time, self._async_timer_reached - ) - - async def async_cleanup(self) -> None: - """Cleanup any pending task.""" - self._async_cancel_timer() - if not self._task: - return - self._task.cancel() - try: - await self._task - except asyncio.CancelledError: - pass - except Exception: - _LOGGER.exception("Error cleaning up task") - - class MQTT: """Home Assistant MQTT client.""" diff --git a/homeassistant/components/mqtt/util.py b/homeassistant/components/mqtt/util.py index 256bad71ba6..97fa616fdd1 100644 --- a/homeassistant/components/mqtt/util.py +++ b/homeassistant/components/mqtt/util.py @@ -3,6 +3,7 @@ from __future__ import annotations import asyncio +from collections.abc import Callable, Coroutine from functools import lru_cache import logging import os @@ -14,7 +15,8 @@ import voluptuous as vol from homeassistant.config_entries import ConfigEntry, ConfigEntryState from homeassistant.const import MAX_LENGTH_STATE_STATE, STATE_UNKNOWN, Platform -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv, template from homeassistant.helpers.typing import ConfigType from homeassistant.util.async_ import create_eager_task @@ -40,6 +42,108 @@ TEMP_DIR_NAME = f"home-assistant-{DOMAIN}" _VALID_QOS_SCHEMA = vol.All(vol.Coerce(int), vol.In([0, 1, 2])) +_LOGGER = logging.getLogger(__name__) + + +class EnsureJobAfterCooldown: + """Ensure a cool down period before executing a job. + + When a new execute request arrives we cancel the current request + and start a new one. + + We allow patching this util, as we generally have exceptions + for sleeps/waits/debouncers/timers causing long run times in tests. + """ + + def __init__( + self, timeout: float, callback_job: Callable[[], Coroutine[Any, None, None]] + ) -> None: + """Initialize the timer.""" + self._loop = asyncio.get_running_loop() + self._timeout = timeout + self._callback = callback_job + self._task: asyncio.Task | None = None + self._timer: asyncio.TimerHandle | None = None + self._next_execute_time = 0.0 + + def set_timeout(self, timeout: float) -> None: + """Set a new timeout period.""" + self._timeout = timeout + + async def _async_job(self) -> None: + """Execute after a cooldown period.""" + try: + await self._callback() + except HomeAssistantError as ha_error: + _LOGGER.error("%s", ha_error) + + @callback + def _async_task_done(self, task: asyncio.Task) -> None: + """Handle task done.""" + self._task = None + + @callback + def async_execute(self) -> asyncio.Task: + """Execute the job.""" + if self._task: + # Task already running, + # so we schedule another run + self.async_schedule() + return self._task + + self._async_cancel_timer() + self._task = create_eager_task(self._async_job()) + self._task.add_done_callback(self._async_task_done) + return self._task + + @callback + def _async_cancel_timer(self) -> None: + """Cancel any pending task.""" + if self._timer: + self._timer.cancel() + self._timer = None + + @callback + def async_schedule(self) -> None: + """Ensure we execute after a cooldown period.""" + # We want to reschedule the timer in the future + # every time this is called. + next_when = self._loop.time() + self._timeout + if not self._timer: + self._timer = self._loop.call_at(next_when, self._async_timer_reached) + return + + if self._timer.when() < next_when: + # Timer already running, set the next execute time + # if it fires too early, it will get rescheduled + self._next_execute_time = next_when + + @callback + def _async_timer_reached(self) -> None: + """Handle timer fire.""" + self._timer = None + if self._loop.time() >= self._next_execute_time: + self.async_execute() + return + # Timer fired too early because there were multiple + # calls async_schedule. Reschedule the timer. + self._timer = self._loop.call_at( + self._next_execute_time, self._async_timer_reached + ) + + async def async_cleanup(self) -> None: + """Cleanup any pending task.""" + self._async_cancel_timer() + if not self._task: + return + self._task.cancel() + try: + await self._task + except asyncio.CancelledError: + pass + except Exception: + _LOGGER.exception("Error cleaning up task") + def platforms_from_config(config: list[ConfigType]) -> set[Platform | str]: """Return the platforms to be set up.""" diff --git a/tests/components/mqtt/conftest.py b/tests/components/mqtt/conftest.py index 39b9f122f75..5a1f65667cf 100644 --- a/tests/components/mqtt/conftest.py +++ b/tests/components/mqtt/conftest.py @@ -9,7 +9,7 @@ import pytest from typing_extensions import AsyncGenerator, Generator from homeassistant.components import mqtt -from homeassistant.components.mqtt.models import ReceiveMessage +from homeassistant.components.mqtt.models import MessageCallbackType, ReceiveMessage from homeassistant.const import EVENT_HOMEASSISTANT_STARTED from homeassistant.core import HomeAssistant, callback @@ -79,3 +79,21 @@ async def setup_with_birth_msg_client_mock( await hass.async_block_till_done() await birth.wait() yield mqtt_client_mock + + +@pytest.fixture +def recorded_calls() -> list[ReceiveMessage]: + """Fixture to hold recorded calls.""" + return [] + + +@pytest.fixture +def record_calls(recorded_calls: list[ReceiveMessage]) -> MessageCallbackType: + """Fixture to record calls.""" + + @callback + def record_calls(msg: ReceiveMessage) -> None: + """Record calls.""" + recorded_calls.append(msg) + + return record_calls diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index 8a76c71f1f3..2c3ca31bff9 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -24,7 +24,6 @@ from homeassistant.components.mqtt import debug_info from homeassistant.components.mqtt.client import ( _LOGGER as CLIENT_LOGGER, RECONNECT_INTERVAL_SECONDS, - EnsureJobAfterCooldown, ) from homeassistant.components.mqtt.models import ( MessageCallbackType, @@ -101,24 +100,6 @@ def mock_storage(hass_storage: dict[str, Any]) -> None: """Autouse hass_storage for the TestCase tests.""" -@pytest.fixture -def recorded_calls() -> list[ReceiveMessage]: - """Fixture to hold recorded calls.""" - return [] - - -@pytest.fixture -def record_calls(recorded_calls: list[ReceiveMessage]) -> MessageCallbackType: - """Fixture to record calls.""" - - @callback - def record_calls(msg: ReceiveMessage) -> None: - """Record calls.""" - recorded_calls.append(msg) - - return record_calls - - @pytest.fixture def client_debug_log() -> Generator[None]: """Set the mqtt client log level to DEBUG.""" @@ -1070,6 +1051,7 @@ async def test_subscribe_topic( async def test_subscribe_topic_not_initialize( hass: HomeAssistant, + record_calls: MessageCallbackType, mqtt_mock_entry: MqttMockHAClientGenerator, ) -> None: """Test the subscription of a topic when MQTT was not initialized.""" @@ -1080,7 +1062,7 @@ async def test_subscribe_topic_not_initialize( async def test_subscribe_mqtt_config_entry_disabled( - hass: HomeAssistant, mqtt_mock: MqttMockHAClient + hass: HomeAssistant, mqtt_mock: MqttMockHAClient, record_calls: MessageCallbackType ) -> None: """Test the subscription of a topic when MQTT config entry is disabled.""" mqtt_mock.connected = True @@ -2016,84 +1998,6 @@ async def test_reload_entry_with_restored_subscriptions( assert recorded_calls[1].payload == "wild-card-payload3" -async def test_canceling_debouncer_on_shutdown( - hass: HomeAssistant, - record_calls: MessageCallbackType, - setup_with_birth_msg_client_mock: MqttMockPahoClient, -) -> None: - """Test canceling the debouncer when HA shuts down.""" - mqtt_client_mock = setup_with_birth_msg_client_mock - - with patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 2): - await hass.async_block_till_done() - async_fire_time_changed(hass, utcnow() + timedelta(seconds=5)) - await hass.async_block_till_done() - await mqtt.async_subscribe(hass, "test/state1", record_calls) - async_fire_time_changed(hass, utcnow() + timedelta(seconds=0.1)) - # Stop HA so the scheduled debouncer task will be canceled - mqtt_client_mock.subscribe.reset_mock() - hass.bus.fire(EVENT_HOMEASSISTANT_STOP) - await mqtt.async_subscribe(hass, "test/state2", record_calls) - async_fire_time_changed(hass, utcnow() + timedelta(seconds=0.1)) - await mqtt.async_subscribe(hass, "test/state3", record_calls) - async_fire_time_changed(hass, utcnow() + timedelta(seconds=0.1)) - await mqtt.async_subscribe(hass, "test/state4", record_calls) - async_fire_time_changed(hass, utcnow() + timedelta(seconds=0.1)) - await mqtt.async_subscribe(hass, "test/state5", record_calls) - async_fire_time_changed(hass, utcnow() + timedelta(seconds=0.1)) - await hass.async_block_till_done() - - mqtt_client_mock.subscribe.assert_not_called() - - # Note thet the broker connection will not be disconnected gracefully - await hass.async_block_till_done() - async_fire_time_changed(hass, utcnow() + timedelta(seconds=5)) - await asyncio.sleep(0) - await hass.async_block_till_done(wait_background_tasks=True) - mqtt_client_mock.subscribe.assert_not_called() - mqtt_client_mock.disconnect.assert_not_called() - - -async def test_canceling_debouncer_normal( - hass: HomeAssistant, - caplog: pytest.LogCaptureFixture, -) -> None: - """Test canceling the debouncer before completion.""" - - async def _async_myjob() -> None: - await asyncio.sleep(1.0) - - debouncer = EnsureJobAfterCooldown(0.0, _async_myjob) - debouncer.async_schedule() - await asyncio.sleep(0.01) - assert debouncer._task is not None - await debouncer.async_cleanup() - assert debouncer._task is None - - -async def test_canceling_debouncer_throws( - hass: HomeAssistant, - caplog: pytest.LogCaptureFixture, -) -> None: - """Test canceling the debouncer when HA shuts down.""" - - async def _async_myjob() -> None: - await asyncio.sleep(1.0) - - debouncer = EnsureJobAfterCooldown(0.0, _async_myjob) - debouncer.async_schedule() - await asyncio.sleep(0.01) - assert debouncer._task is not None - # let debouncer._task fail by mocking it - with patch.object(debouncer, "_task") as task: - task.cancel = MagicMock(return_value=True) - await debouncer.async_cleanup() - assert "Error cleaning up task" in caplog.text - await hass.async_block_till_done() - async_fire_time_changed(hass, utcnow() + timedelta(seconds=5)) - await hass.async_block_till_done() - - async def test_initial_setup_logs_error( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, diff --git a/tests/components/mqtt/test_util.py b/tests/components/mqtt/test_util.py index 290f561e1ad..955fc88448c 100644 --- a/tests/components/mqtt/test_util.py +++ b/tests/components/mqtt/test_util.py @@ -1,22 +1,106 @@ """Test MQTT utils.""" +import asyncio from collections.abc import Callable +from datetime import timedelta from pathlib import Path from random import getrandbits import shutil import tempfile -from unittest.mock import patch +from unittest.mock import MagicMock, patch import pytest from homeassistant.components import mqtt +from homeassistant.components.mqtt.models import MessageCallbackType +from homeassistant.components.mqtt.util import EnsureJobAfterCooldown from homeassistant.config_entries import ConfigEntryDisabler, ConfigEntryState +from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import CoreState, HomeAssistant +from homeassistant.util.dt import utcnow -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, async_fire_time_changed from tests.typing import MqttMockHAClient, MqttMockPahoClient +async def test_canceling_debouncer_on_shutdown( + hass: HomeAssistant, + record_calls: MessageCallbackType, + setup_with_birth_msg_client_mock: MqttMockPahoClient, +) -> None: + """Test canceling the debouncer when HA shuts down.""" + mqtt_client_mock = setup_with_birth_msg_client_mock + + with patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 2): + await hass.async_block_till_done() + async_fire_time_changed(hass, utcnow() + timedelta(seconds=5)) + await hass.async_block_till_done() + await mqtt.async_subscribe(hass, "test/state1", record_calls) + async_fire_time_changed(hass, utcnow() + timedelta(seconds=0.1)) + # Stop HA so the scheduled debouncer task will be canceled + mqtt_client_mock.subscribe.reset_mock() + hass.bus.fire(EVENT_HOMEASSISTANT_STOP) + await mqtt.async_subscribe(hass, "test/state2", record_calls) + async_fire_time_changed(hass, utcnow() + timedelta(seconds=0.1)) + await mqtt.async_subscribe(hass, "test/state3", record_calls) + async_fire_time_changed(hass, utcnow() + timedelta(seconds=0.1)) + await mqtt.async_subscribe(hass, "test/state4", record_calls) + async_fire_time_changed(hass, utcnow() + timedelta(seconds=0.1)) + await mqtt.async_subscribe(hass, "test/state5", record_calls) + async_fire_time_changed(hass, utcnow() + timedelta(seconds=0.1)) + await hass.async_block_till_done() + + mqtt_client_mock.subscribe.assert_not_called() + + # Note thet the broker connection will not be disconnected gracefully + await hass.async_block_till_done() + async_fire_time_changed(hass, utcnow() + timedelta(seconds=5)) + await asyncio.sleep(0) + await hass.async_block_till_done(wait_background_tasks=True) + mqtt_client_mock.subscribe.assert_not_called() + mqtt_client_mock.disconnect.assert_not_called() + + +async def test_canceling_debouncer_normal( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test canceling the debouncer before completion.""" + + async def _async_myjob() -> None: + await asyncio.sleep(1.0) + + debouncer = EnsureJobAfterCooldown(0.0, _async_myjob) + debouncer.async_schedule() + await asyncio.sleep(0.01) + assert debouncer._task is not None + await debouncer.async_cleanup() + assert debouncer._task is None + + +async def test_canceling_debouncer_throws( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test canceling the debouncer when HA shuts down.""" + + async def _async_myjob() -> None: + await asyncio.sleep(1.0) + + debouncer = EnsureJobAfterCooldown(0.0, _async_myjob) + debouncer.async_schedule() + await asyncio.sleep(0.01) + assert debouncer._task is not None + # let debouncer._task fail by mocking it + with patch.object(debouncer, "_task") as task: + task.cancel = MagicMock(return_value=True) + await debouncer.async_cleanup() + assert "Error cleaning up task" in caplog.text + await hass.async_block_till_done() + async_fire_time_changed(hass, utcnow() + timedelta(seconds=5)) + await hass.async_block_till_done() + + async def help_create_test_certificate_file( hass: HomeAssistant, mock_temp_dir: str, From ddd8083302e2ccf6154e62b8538137215078c732 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Tue, 25 Jun 2024 10:37:42 +0200 Subject: [PATCH 1150/1445] Fix translation error in Reolink reauth flow (#120385) --- homeassistant/components/reolink/config_flow.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/reolink/config_flow.py b/homeassistant/components/reolink/config_flow.py index d8caff9f120..be897a69d7d 100644 --- a/homeassistant/components/reolink/config_flow.py +++ b/homeassistant/components/reolink/config_flow.py @@ -123,7 +123,10 @@ class ReolinkFlowHandler(ConfigFlow, domain=DOMAIN): """Dialog that informs the user that reauth is required.""" if user_input is not None: return await self.async_step_user() - return self.async_show_form(step_id="reauth_confirm") + placeholders = {"name": self.context["title_placeholders"]["name"]} + return self.async_show_form( + step_id="reauth_confirm", description_placeholders=placeholders + ) async def async_step_dhcp( self, discovery_info: dhcp.DhcpServiceInfo From 3d1ff72a8834befbe546cf8ce4937df51b85199c Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 25 Jun 2024 10:58:10 +0200 Subject: [PATCH 1151/1445] Improve type hints in device_automation tests (#120389) --- tests/components/device_automation/test_init.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/components/device_automation/test_init.py b/tests/components/device_automation/test_init.py index 7d68a944de1..b270d2ddd7a 100644 --- a/tests/components/device_automation/test_init.py +++ b/tests/components/device_automation/test_init.py @@ -46,7 +46,7 @@ def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None: @pytest.fixture -def fake_integration(hass): +def fake_integration(hass: HomeAssistant) -> None: """Set up a mock integration with device automation support.""" DOMAIN = "fake_integration" From 0545ed8082c91ca5cd5bea88e6dd07169b365c7b Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 25 Jun 2024 11:02:00 +0200 Subject: [PATCH 1152/1445] Section support for data entry flows (#118369) * Add expandable support for data entry form flows * Update config_validation.py * optional options * Adjust * Correct translations of data within sections * Update homeassistant/components/kitchen_sink/config_flow.py Co-authored-by: Robert Resch * Add missing import * Update tests/components/kitchen_sink/test_config_flow.py Co-authored-by: Robert Resch * Format code * Match frontend when serializing * Move section class to data_entry_flow * Correct serializing * Fix import in kitchen_sink * Move and update test --------- Co-authored-by: Bram Kragten Co-authored-by: Robert Resch --- .../components/kitchen_sink/config_flow.py | 69 ++++++++++++++++++- .../components/kitchen_sink/icons.json | 11 +++ .../components/kitchen_sink/strings.json | 20 ++++++ homeassistant/data_entry_flow.py | 27 ++++++++ homeassistant/helpers/config_validation.py | 10 +++ script/hassfest/icons.py | 18 +++++ script/hassfest/translations.py | 7 ++ .../kitchen_sink/test_config_flow.py | 38 ++++++++++ tests/test_data_entry_flow.py | 23 +++++++ 9 files changed, 222 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/kitchen_sink/icons.json diff --git a/homeassistant/components/kitchen_sink/config_flow.py b/homeassistant/components/kitchen_sink/config_flow.py index 93c8a292ba9..c561ca29b8a 100644 --- a/homeassistant/components/kitchen_sink/config_flow.py +++ b/homeassistant/components/kitchen_sink/config_flow.py @@ -4,16 +4,36 @@ from __future__ import annotations from typing import Any -from homeassistant.config_entries import ConfigFlow, ConfigFlowResult +import voluptuous as vol + +from homeassistant import data_entry_flow +from homeassistant.config_entries import ( + ConfigEntry, + ConfigFlow, + ConfigFlowResult, + OptionsFlowWithConfigEntry, +) +from homeassistant.core import callback from . import DOMAIN +CONF_BOOLEAN = "bool" +CONF_INT = "int" + class KitchenSinkConfigFlow(ConfigFlow, domain=DOMAIN): """Kitchen Sink configuration flow.""" VERSION = 1 + @staticmethod + @callback + def async_get_options_flow( + config_entry: ConfigEntry, + ) -> OptionsFlowHandler: + """Get the options flow for this handler.""" + return OptionsFlowHandler(config_entry) + async def async_step_import(self, import_info: dict[str, Any]) -> ConfigFlowResult: """Set the config entry up from yaml.""" if self._async_current_entries(): @@ -30,3 +50,50 @@ class KitchenSinkConfigFlow(ConfigFlow, domain=DOMAIN): if user_input is None: return self.async_show_form(step_id="reauth_confirm") return self.async_abort(reason="reauth_successful") + + +class OptionsFlowHandler(OptionsFlowWithConfigEntry): + """Handle options.""" + + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Manage the options.""" + return await self.async_step_options_1() + + async def async_step_options_1( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Manage the options.""" + if user_input is not None: + self.options.update(user_input) + return await self._update_options() + + return self.async_show_form( + step_id="options_1", + data_schema=vol.Schema( + { + vol.Required("section_1"): data_entry_flow.section( + vol.Schema( + { + vol.Optional( + CONF_BOOLEAN, + default=self.config_entry.options.get( + CONF_BOOLEAN, False + ), + ): bool, + vol.Optional( + CONF_INT, + default=self.config_entry.options.get(CONF_INT, 10), + ): int, + } + ), + {"collapsed": False}, + ), + } + ), + ) + + async def _update_options(self) -> ConfigFlowResult: + """Update config entry options.""" + return self.async_create_entry(title="", data=self.options) diff --git a/homeassistant/components/kitchen_sink/icons.json b/homeassistant/components/kitchen_sink/icons.json new file mode 100644 index 00000000000..85472996819 --- /dev/null +++ b/homeassistant/components/kitchen_sink/icons.json @@ -0,0 +1,11 @@ +{ + "options": { + "step": { + "options_1": { + "section": { + "section_1": "mdi:robot" + } + } + } + } +} diff --git a/homeassistant/components/kitchen_sink/strings.json b/homeassistant/components/kitchen_sink/strings.json index ecfbe406aab..e67527d8468 100644 --- a/homeassistant/components/kitchen_sink/strings.json +++ b/homeassistant/components/kitchen_sink/strings.json @@ -6,6 +6,26 @@ } } }, + "options": { + "step": { + "init": { + "data": {} + }, + "options_1": { + "section": { + "section_1": { + "data": { + "bool": "Optional boolean", + "int": "Numeric input" + }, + "description": "This section allows input of some extra data", + "name": "Collapsible section" + } + }, + "submit": "Save!" + } + } + }, "device": { "n_ch_power_strip": { "name": "Power strip with {number_of_sockets} sockets" diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py index de45702ad95..155e64d259e 100644 --- a/homeassistant/data_entry_flow.py +++ b/homeassistant/data_entry_flow.py @@ -906,6 +906,33 @@ class FlowHandler(Generic[_FlowResultT, _HandlerT]): self.__progress_task = progress_task +class SectionConfig(TypedDict, total=False): + """Class to represent a section config.""" + + collapsed: bool + + +class section: + """Data entry flow section.""" + + CONFIG_SCHEMA = vol.Schema( + { + vol.Optional("collapsed", default=False): bool, + }, + ) + + def __init__( + self, schema: vol.Schema, options: SectionConfig | None = None + ) -> None: + """Initialize.""" + self.schema = schema + self.options: SectionConfig = self.CONFIG_SCHEMA(options or {}) + + def __call__(self, value: Any) -> Any: + """Validate input.""" + return self.schema(value) + + # These can be removed if no deprecated constant are in this module anymore __getattr__ = partial(check_if_deprecated_constant, module_globals=globals()) __dir__ = partial( diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 295cd13fed4..0463bb07e11 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -1037,6 +1037,7 @@ def key_dependency( def custom_serializer(schema: Any) -> Any: """Serialize additional types for voluptuous_serialize.""" + from .. import data_entry_flow # pylint: disable=import-outside-toplevel from . import selector # pylint: disable=import-outside-toplevel if schema is positive_time_period_dict: @@ -1048,6 +1049,15 @@ def custom_serializer(schema: Any) -> Any: if schema is boolean: return {"type": "boolean"} + if isinstance(schema, data_entry_flow.section): + return { + "type": "expandable", + "schema": voluptuous_serialize.convert( + schema.schema, custom_serializer=custom_serializer + ), + "expanded": not schema.options["collapsed"], + } + if isinstance(schema, multi_select): return {"type": "multi_select", "options": schema.options} diff --git a/script/hassfest/icons.py b/script/hassfest/icons.py index e7451dfd498..087d395afeb 100644 --- a/script/hassfest/icons.py +++ b/script/hassfest/icons.py @@ -47,6 +47,19 @@ def ensure_not_same_as_default(value: dict) -> dict: return value +DATA_ENTRY_ICONS_SCHEMA = vol.Schema( + { + "step": { + str: { + "section": { + str: icon_value_validator, + } + } + } + } +) + + def icon_schema(integration_type: str, no_entity_platform: bool) -> vol.Schema: """Create an icon schema.""" @@ -73,6 +86,11 @@ def icon_schema(integration_type: str, no_entity_platform: bool) -> vol.Schema: schema = vol.Schema( { + vol.Optional("config"): DATA_ENTRY_ICONS_SCHEMA, + vol.Optional("issues"): vol.Schema( + {str: {"fix_flow": DATA_ENTRY_ICONS_SCHEMA}} + ), + vol.Optional("options"): DATA_ENTRY_ICONS_SCHEMA, vol.Optional("services"): state_validator, } ) diff --git a/script/hassfest/translations.py b/script/hassfest/translations.py index 7ffb5861bb4..965d1dc62b8 100644 --- a/script/hassfest/translations.py +++ b/script/hassfest/translations.py @@ -166,6 +166,13 @@ def gen_data_entry_schema( vol.Optional("data_description"): {str: translation_value_validator}, vol.Optional("menu_options"): {str: translation_value_validator}, vol.Optional("submit"): translation_value_validator, + vol.Optional("section"): { + str: { + vol.Optional("data"): {str: translation_value_validator}, + vol.Optional("description"): translation_value_validator, + vol.Optional("name"): translation_value_validator, + }, + }, } }, vol.Optional("error"): {str: translation_value_validator}, diff --git a/tests/components/kitchen_sink/test_config_flow.py b/tests/components/kitchen_sink/test_config_flow.py index e530ed0e6f3..290167196cd 100644 --- a/tests/components/kitchen_sink/test_config_flow.py +++ b/tests/components/kitchen_sink/test_config_flow.py @@ -1,13 +1,28 @@ """Test the Everything but the Kitchen Sink config flow.""" +from collections.abc import AsyncGenerator from unittest.mock import patch +import pytest + from homeassistant import config_entries, setup from homeassistant.components.kitchen_sink import DOMAIN from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType from homeassistant.setup import async_setup_component +from tests.common import MockConfigEntry + + +@pytest.fixture +async def no_platforms() -> AsyncGenerator[None, None]: + """Don't enable any platforms.""" + with patch( + "homeassistant.components.kitchen_sink.COMPONENTS_WITH_DEMO_PLATFORM", + [], + ): + yield + async def test_import(hass: HomeAssistant) -> None: """Test that we can import a config entry.""" @@ -66,3 +81,26 @@ async def test_reauth(hass: HomeAssistant) -> None: assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" + + +@pytest.mark.usefixtures("no_platforms") +async def test_options_flow(hass: HomeAssistant) -> None: + """Test config flow options.""" + config_entry = MockConfigEntry(domain=DOMAIN) + config_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "options_1" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={"section_1": {"bool": True, "int": 15}}, + ) + assert result["type"] is FlowResultType.CREATE_ENTRY + assert config_entry.options == {"section_1": {"bool": True, "int": 15}} + + await hass.async_block_till_done() diff --git a/tests/test_data_entry_flow.py b/tests/test_data_entry_flow.py index 782f349f9f2..967b2565206 100644 --- a/tests/test_data_entry_flow.py +++ b/tests/test_data_entry_flow.py @@ -10,6 +10,7 @@ import voluptuous as vol from homeassistant import config_entries, data_entry_flow from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.helpers import config_validation as cv from homeassistant.util.decorator import Registry from .common import ( @@ -1075,3 +1076,25 @@ def test_deprecated_constants( import_and_test_deprecated_constant_enum( caplog, data_entry_flow, enum, "RESULT_TYPE_", "2025.1" ) + + +def test_section_in_serializer() -> None: + """Test section with custom_serializer.""" + assert cv.custom_serializer( + data_entry_flow.section( + vol.Schema( + { + vol.Optional("option_1", default=False): bool, + vol.Required("option_2"): int, + } + ), + {"collapsed": False}, + ) + ) == { + "expanded": True, + "schema": [ + {"default": False, "name": "option_1", "optional": True, "type": "boolean"}, + {"name": "option_2", "required": True, "type": "integer"}, + ], + "type": "expandable", + } From d3e76b1f39ba5756bdf362853fecc036a34c9e40 Mon Sep 17 00:00:00 2001 From: treetip Date: Tue, 25 Jun 2024 12:24:57 +0300 Subject: [PATCH 1153/1445] Update vallox_websocket_api to 5.3.0 (#120395) --- homeassistant/components/vallox/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/vallox/manifest.json b/homeassistant/components/vallox/manifest.json index 9a57358cd14..bbc806d8f38 100644 --- a/homeassistant/components/vallox/manifest.json +++ b/homeassistant/components/vallox/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/vallox", "iot_class": "local_polling", "loggers": ["vallox_websocket_api"], - "requirements": ["vallox-websocket-api==5.1.1"] + "requirements": ["vallox-websocket-api==5.3.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index da4afc918f3..d1481147699 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2826,7 +2826,7 @@ uvcclient==0.11.0 vacuum-map-parser-roborock==0.1.2 # homeassistant.components.vallox -vallox-websocket-api==5.1.1 +vallox-websocket-api==5.3.0 # homeassistant.components.rdw vehicle==2.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9755e583d9d..3af1fa4184a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2200,7 +2200,7 @@ uvcclient==0.11.0 vacuum-map-parser-roborock==0.1.2 # homeassistant.components.vallox -vallox-websocket-api==5.1.1 +vallox-websocket-api==5.3.0 # homeassistant.components.rdw vehicle==2.2.1 From 53f5dec1b4533d4cd1677298d3fe3ab79c2755ba Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 25 Jun 2024 11:26:45 +0200 Subject: [PATCH 1154/1445] Install libturbojpeg [ci] (#120397) --- .github/workflows/ci.yaml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index af29c00af9e..8a030d7d45c 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -488,6 +488,7 @@ jobs: sudo apt-get -y install \ bluez \ ffmpeg \ + libturbojpeg \ libavcodec-dev \ libavdevice-dev \ libavfilter-dev \ @@ -747,6 +748,7 @@ jobs: sudo apt-get -y install \ bluez \ ffmpeg \ + libturbojpeg \ libgammu-dev - name: Check out code from GitHub uses: actions/checkout@v4.1.7 @@ -809,6 +811,7 @@ jobs: sudo apt-get -y install \ bluez \ ffmpeg \ + libturbojpeg \ libgammu-dev - name: Check out code from GitHub uses: actions/checkout@v4.1.7 @@ -926,6 +929,7 @@ jobs: sudo apt-get -y install \ bluez \ ffmpeg \ + libturbojpeg \ libmariadb-dev-compat - name: Check out code from GitHub uses: actions/checkout@v4.1.7 @@ -1050,6 +1054,7 @@ jobs: sudo apt-get -y install \ bluez \ ffmpeg \ + libturbojpeg \ postgresql-server-dev-14 - name: Check out code from GitHub uses: actions/checkout@v4.1.7 @@ -1194,6 +1199,7 @@ jobs: sudo apt-get -y install \ bluez \ ffmpeg \ + libturbojpeg \ libgammu-dev - name: Check out code from GitHub uses: actions/checkout@v4.1.7 From b4eee166aa820626119ebff590a90a0f034ca766 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 25 Jun 2024 11:58:27 +0200 Subject: [PATCH 1155/1445] Add voluptuous type aliases (#120399) --- homeassistant/core.py | 8 ++++---- homeassistant/helpers/entity_component.py | 7 +++---- homeassistant/helpers/entity_platform.py | 6 ++---- homeassistant/helpers/typing.py | 4 ++++ 4 files changed, 13 insertions(+), 12 deletions(-) diff --git a/homeassistant/core.py b/homeassistant/core.py index ac287fb2d5f..f114049b2b2 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -96,7 +96,7 @@ from .helpers.deprecation import ( dir_with_deprecated_constants, ) from .helpers.json import json_bytes, json_fragment -from .helpers.typing import UNDEFINED, UndefinedType +from .helpers.typing import UNDEFINED, UndefinedType, VolSchemaType from .util import dt as dt_util, location from .util.async_ import ( cancelling, @@ -2355,7 +2355,7 @@ class Service: | EntityServiceResponse | None, ], - schema: vol.Schema | None, + schema: VolSchemaType | None, domain: str, service: str, context: Context | None = None, @@ -2503,7 +2503,7 @@ class ServiceRegistry: | EntityServiceResponse | None, ], - schema: vol.Schema | None = None, + schema: VolSchemaType | None = None, supports_response: SupportsResponse = SupportsResponse.NONE, job_type: HassJobType | None = None, ) -> None: @@ -2530,7 +2530,7 @@ class ServiceRegistry: | EntityServiceResponse | None, ], - schema: vol.Schema | None = None, + schema: VolSchemaType | None = None, supports_response: SupportsResponse = SupportsResponse.NONE, job_type: HassJobType | None = None, ) -> None: diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py index aae0e2058e4..0034eb1c6fc 100644 --- a/homeassistant/helpers/entity_component.py +++ b/homeassistant/helpers/entity_component.py @@ -11,7 +11,6 @@ from types import ModuleType from typing import Any, Generic from typing_extensions import TypeVar -import voluptuous as vol from homeassistant import config as conf_util from homeassistant.config_entries import ConfigEntry @@ -36,7 +35,7 @@ from homeassistant.setup import async_prepare_setup_platform from . import config_validation as cv, discovery, entity, service from .entity_platform import EntityPlatform -from .typing import ConfigType, DiscoveryInfoType +from .typing import ConfigType, DiscoveryInfoType, VolDictType, VolSchemaType DEFAULT_SCAN_INTERVAL = timedelta(seconds=15) DATA_INSTANCES = "entity_components" @@ -222,7 +221,7 @@ class EntityComponent(Generic[_EntityT]): def async_register_legacy_entity_service( self, name: str, - schema: dict[str | vol.Marker, Any] | vol.Schema, + schema: VolDictType | VolSchemaType, func: str | Callable[..., Any], required_features: list[int] | None = None, supports_response: SupportsResponse = SupportsResponse.NONE, @@ -259,7 +258,7 @@ class EntityComponent(Generic[_EntityT]): def async_register_entity_service( self, name: str, - schema: dict[str | vol.Marker, Any] | vol.Schema, + schema: VolDictType | VolSchemaType, func: str | Callable[..., Any], required_features: list[int] | None = None, supports_response: SupportsResponse = SupportsResponse.NONE, diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index 4dbe3ac68d8..6774780f00f 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -10,8 +10,6 @@ from functools import partial from logging import Logger, getLogger from typing import TYPE_CHECKING, Any, Protocol -import voluptuous as vol - from homeassistant import config_entries from homeassistant.const import ( ATTR_RESTORED, @@ -52,7 +50,7 @@ from . import ( from .entity_registry import EntityRegistry, RegistryEntryDisabler, RegistryEntryHider from .event import async_call_later from .issue_registry import IssueSeverity, async_create_issue -from .typing import UNDEFINED, ConfigType, DiscoveryInfoType +from .typing import UNDEFINED, ConfigType, DiscoveryInfoType, VolDictType, VolSchemaType if TYPE_CHECKING: from .entity import Entity @@ -987,7 +985,7 @@ class EntityPlatform: def async_register_entity_service( self, name: str, - schema: dict[str | vol.Marker, Any] | vol.Schema, + schema: VolDictType | VolSchemaType, func: str | Callable[..., Any], required_features: Iterable[int] | None = None, supports_response: SupportsResponse = SupportsResponse.NONE, diff --git a/homeassistant/helpers/typing.py b/homeassistant/helpers/typing.py index 3cdd9ec9250..65774a0b168 100644 --- a/homeassistant/helpers/typing.py +++ b/homeassistant/helpers/typing.py @@ -5,6 +5,8 @@ from enum import Enum from functools import partial from typing import Any, Never +import voluptuous as vol + from .deprecation import ( DeferredDeprecatedAlias, all_with_deprecated_constants, @@ -19,6 +21,8 @@ type ServiceDataType = dict[str, Any] type StateType = str | int | float | None type TemplateVarsType = Mapping[str, Any] | None type NoEventData = Mapping[str, Never] +type VolSchemaType = vol.Schema | vol.All | vol.Any +type VolDictType = dict[str | vol.Marker, Any] # Custom type for recorder Queries type QueryType = Any From 3a5acd6a57db11e7fc8ee31c7f22eea9157e501f Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 25 Jun 2024 12:33:41 +0200 Subject: [PATCH 1156/1445] Use VolDictType for service schemas (#120403) --- homeassistant/components/camera/__init__.py | 8 ++++---- .../components/elkm1/alarm_control_panel.py | 3 ++- homeassistant/components/elkm1/const.py | 3 ++- homeassistant/components/elkm1/sensor.py | 3 ++- .../components/environment_canada/camera.py | 3 ++- homeassistant/components/flux_led/light.py | 7 ++++--- homeassistant/components/geniushub/switch.py | 4 ++-- homeassistant/components/harmony/remote.py | 3 ++- homeassistant/components/isy994/services.py | 7 +++++-- homeassistant/components/izone/climate.py | 4 ++-- homeassistant/components/keymitt_ble/switch.py | 3 ++- homeassistant/components/kodi/media_player.py | 4 ++-- homeassistant/components/lifx/light.py | 5 +++-- homeassistant/components/light/__init__.py | 9 ++++++--- homeassistant/components/lyric/climate.py | 3 ++- homeassistant/components/motion_blinds/cover.py | 3 ++- homeassistant/components/nexia/climate.py | 5 +++-- homeassistant/components/rainbird/switch.py | 3 ++- homeassistant/components/rainmachine/switch.py | 6 ++++-- homeassistant/components/renson/fan.py | 7 ++++--- homeassistant/components/roku/media_player.py | 3 ++- homeassistant/components/siren/__init__.py | 4 ++-- homeassistant/components/smarttub/binary_sensor.py | 5 +++-- homeassistant/components/smarttub/sensor.py | 3 ++- homeassistant/components/switcher_kis/switch.py | 5 +++-- homeassistant/components/tado/climate.py | 5 +++-- homeassistant/components/tado/water_heater.py | 3 ++- homeassistant/components/upb/const.py | 3 ++- homeassistant/components/vizio/const.py | 3 ++- homeassistant/components/wemo/fan.py | 3 ++- homeassistant/components/yardian/switch.py | 3 ++- homeassistant/components/yeelight/__init__.py | 4 ++-- homeassistant/components/yeelight/light.py | 13 +++++++------ 33 files changed, 91 insertions(+), 59 deletions(-) diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index 4d2ba00900f..428e8d856fb 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -64,7 +64,7 @@ from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.network import get_url from homeassistant.helpers.template import Template -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.typing import ConfigType, VolDictType from homeassistant.loader import bind_hass from .const import ( # noqa: F401 @@ -130,14 +130,14 @@ _RND: Final = SystemRandom() MIN_STREAM_INTERVAL: Final = 0.5 # seconds -CAMERA_SERVICE_SNAPSHOT: Final = {vol.Required(ATTR_FILENAME): cv.template} +CAMERA_SERVICE_SNAPSHOT: VolDictType = {vol.Required(ATTR_FILENAME): cv.template} -CAMERA_SERVICE_PLAY_STREAM: Final = { +CAMERA_SERVICE_PLAY_STREAM: VolDictType = { vol.Required(ATTR_MEDIA_PLAYER): cv.entities_domain(DOMAIN_MP), vol.Optional(ATTR_FORMAT, default="hls"): vol.In(OUTPUT_FORMATS), } -CAMERA_SERVICE_RECORD: Final = { +CAMERA_SERVICE_RECORD: VolDictType = { vol.Required(CONF_FILENAME): cv.template, vol.Optional(CONF_DURATION, default=30): vol.Coerce(int), vol.Optional(CONF_LOOKBACK, default=0): vol.Coerce(int), diff --git a/homeassistant/components/elkm1/alarm_control_panel.py b/homeassistant/components/elkm1/alarm_control_panel.py index eb8d7360ce2..b24d0f869c6 100644 --- a/homeassistant/components/elkm1/alarm_control_panel.py +++ b/homeassistant/components/elkm1/alarm_control_panel.py @@ -31,6 +31,7 @@ from homeassistant.helpers import entity_platform import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity +from homeassistant.helpers.typing import VolDictType from . import ElkAttachedEntity, ElkEntity, ElkM1ConfigEntry, create_elk_entities from .const import ( @@ -41,7 +42,7 @@ from .const import ( ) from .models import ELKM1Data -DISPLAY_MESSAGE_SERVICE_SCHEMA = { +DISPLAY_MESSAGE_SERVICE_SCHEMA: VolDictType = { vol.Optional("clear", default=2): vol.All(vol.Coerce(int), vol.In([0, 1, 2])), vol.Optional("beep", default=False): cv.boolean, vol.Optional("timeout", default=0): vol.All( diff --git a/homeassistant/components/elkm1/const.py b/homeassistant/components/elkm1/const.py index 9e952c7ee0b..61d1994b797 100644 --- a/homeassistant/components/elkm1/const.py +++ b/homeassistant/components/elkm1/const.py @@ -6,6 +6,7 @@ from elkm1_lib.const import Max import voluptuous as vol from homeassistant.const import ATTR_CODE, CONF_ZONE +from homeassistant.helpers.typing import VolDictType DOMAIN = "elkm1" @@ -48,6 +49,6 @@ ATTR_CHANGED_BY_ID = "changed_by_id" ATTR_CHANGED_BY_TIME = "changed_by_time" ATTR_VALUE = "value" -ELK_USER_CODE_SERVICE_SCHEMA = { +ELK_USER_CODE_SERVICE_SCHEMA: VolDictType = { vol.Required(ATTR_CODE): vol.All(vol.Coerce(int), vol.Range(0, 999999)) } diff --git a/homeassistant/components/elkm1/sensor.py b/homeassistant/components/elkm1/sensor.py index 801a09b76eb..7d3601f0bd0 100644 --- a/homeassistant/components/elkm1/sensor.py +++ b/homeassistant/components/elkm1/sensor.py @@ -20,6 +20,7 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_platform from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import VolDictType from . import ElkAttachedEntity, ElkEntity, ElkM1ConfigEntry, create_elk_entities from .const import ATTR_VALUE, ELK_USER_CODE_SERVICE_SCHEMA @@ -30,7 +31,7 @@ SERVICE_SENSOR_ZONE_BYPASS = "sensor_zone_bypass" SERVICE_SENSOR_ZONE_TRIGGER = "sensor_zone_trigger" UNDEFINED_TEMPERATURE = -40 -ELK_SET_COUNTER_SERVICE_SCHEMA = { +ELK_SET_COUNTER_SERVICE_SCHEMA: VolDictType = { vol.Required(ATTR_VALUE): vol.All(vol.Coerce(int), vol.Range(0, 65535)) } diff --git a/homeassistant/components/environment_canada/camera.py b/homeassistant/components/environment_canada/camera.py index 73032f59ac2..1625cd253da 100644 --- a/homeassistant/components/environment_canada/camera.py +++ b/homeassistant/components/environment_canada/camera.py @@ -11,13 +11,14 @@ from homeassistant.helpers.entity_platform import ( AddEntitiesCallback, async_get_current_platform, ) +from homeassistant.helpers.typing import VolDictType from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import device_info from .const import ATTR_OBSERVATION_TIME, DOMAIN SERVICE_SET_RADAR_TYPE = "set_radar_type" -SET_RADAR_TYPE_SCHEMA = { +SET_RADAR_TYPE_SCHEMA: VolDictType = { vol.Required("radar_type"): vol.In(["Auto", "Rain", "Snow"]), } diff --git a/homeassistant/components/flux_led/light.py b/homeassistant/components/flux_led/light.py index 6456eb36dbb..f4982a13c3a 100644 --- a/homeassistant/components/flux_led/light.py +++ b/homeassistant/components/flux_led/light.py @@ -28,6 +28,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_platform import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import VolDictType from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util.color import ( color_temperature_kelvin_to_mired, @@ -88,7 +89,7 @@ SERVICE_CUSTOM_EFFECT: Final = "set_custom_effect" SERVICE_SET_ZONES: Final = "set_zones" SERVICE_SET_MUSIC_MODE: Final = "set_music_mode" -CUSTOM_EFFECT_DICT: Final = { +CUSTOM_EFFECT_DICT: VolDictType = { vol.Required(CONF_COLORS): vol.All( cv.ensure_list, vol.Length(min=1, max=16), @@ -102,7 +103,7 @@ CUSTOM_EFFECT_DICT: Final = { ), } -SET_MUSIC_MODE_DICT: Final = { +SET_MUSIC_MODE_DICT: VolDictType = { vol.Optional(ATTR_SENSITIVITY, default=100): vol.All( vol.Coerce(int), vol.Range(min=0, max=100) ), @@ -121,7 +122,7 @@ SET_MUSIC_MODE_DICT: Final = { ), } -SET_ZONES_DICT: Final = { +SET_ZONES_DICT: VolDictType = { vol.Required(CONF_COLORS): vol.All( cv.ensure_list, vol.Length(min=1, max=2048), diff --git a/homeassistant/components/geniushub/switch.py b/homeassistant/components/geniushub/switch.py index b703df57f90..85f7f1bb03a 100644 --- a/homeassistant/components/geniushub/switch.py +++ b/homeassistant/components/geniushub/switch.py @@ -11,7 +11,7 @@ from homeassistant.components.switch import SwitchDeviceClass, SwitchEntity from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, VolDictType from . import ATTR_DURATION, DOMAIN, GeniusZone @@ -19,7 +19,7 @@ GH_ON_OFF_ZONE = "on / off" SVC_SET_SWITCH_OVERRIDE = "set_switch_override" -SET_SWITCH_OVERRIDE_SCHEMA = { +SET_SWITCH_OVERRIDE_SCHEMA: VolDictType = { vol.Optional(ATTR_DURATION): vol.All( cv.time_period, vol.Range(min=timedelta(minutes=5), max=timedelta(days=1)), diff --git a/homeassistant/components/harmony/remote.py b/homeassistant/components/harmony/remote.py index 0c9bdcb9c6e..a52f298dc41 100644 --- a/homeassistant/components/harmony/remote.py +++ b/homeassistant/components/harmony/remote.py @@ -26,6 +26,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity +from homeassistant.helpers.typing import VolDictType from .const import ( ACTIVITY_POWER_OFF, @@ -50,7 +51,7 @@ PARALLEL_UPDATES = 0 ATTR_CHANNEL = "channel" -HARMONY_CHANGE_CHANNEL_SCHEMA = { +HARMONY_CHANGE_CHANNEL_SCHEMA: VolDictType = { vol.Required(ATTR_CHANNEL): cv.positive_int, } diff --git a/homeassistant/components/isy994/services.py b/homeassistant/components/isy994/services.py index fedf7f8e902..ffcea5cc8f8 100644 --- a/homeassistant/components/isy994/services.py +++ b/homeassistant/components/isy994/services.py @@ -18,6 +18,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_platform import async_get_platforms from homeassistant.helpers.service import entity_service_call +from homeassistant.helpers.typing import VolDictType from .const import _LOGGER, DOMAIN @@ -102,12 +103,14 @@ SERVICE_SET_ZWAVE_PARAMETER_SCHEMA = { vol.Required(CONF_SIZE): vol.All(vol.Coerce(int), vol.In(VALID_PARAMETER_SIZES)), } -SERVICE_SET_USER_CODE_SCHEMA = { +SERVICE_SET_USER_CODE_SCHEMA: VolDictType = { vol.Required(CONF_USER_NUM): vol.Coerce(int), vol.Required(CONF_CODE): vol.Coerce(int), } -SERVICE_DELETE_USER_CODE_SCHEMA = {vol.Required(CONF_USER_NUM): vol.Coerce(int)} +SERVICE_DELETE_USER_CODE_SCHEMA: VolDictType = { + vol.Required(CONF_USER_NUM): vol.Coerce(int) +} SERVICE_SEND_PROGRAM_COMMAND_SCHEMA = vol.All( cv.has_at_least_one_key(CONF_ADDRESS, CONF_NAME), diff --git a/homeassistant/components/izone/climate.py b/homeassistant/components/izone/climate.py index 14267a626fc..3a1279a9bd4 100644 --- a/homeassistant/components/izone/climate.py +++ b/homeassistant/components/izone/climate.py @@ -35,7 +35,7 @@ from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.temperature import display_temp as show_temp -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.typing import ConfigType, VolDictType from .const import ( DATA_CONFIG, @@ -65,7 +65,7 @@ ATTR_AIRFLOW = "airflow" IZONE_SERVICE_AIRFLOW_MIN = "airflow_min" IZONE_SERVICE_AIRFLOW_MAX = "airflow_max" -IZONE_SERVICE_AIRFLOW_SCHEMA = { +IZONE_SERVICE_AIRFLOW_SCHEMA: VolDictType = { vol.Required(ATTR_AIRFLOW): vol.All( vol.Coerce(int), vol.Range(min=0, max=100), msg="invalid airflow" ), diff --git a/homeassistant/components/keymitt_ble/switch.py b/homeassistant/components/keymitt_ble/switch.py index 2c255ecdf28..ca458c5020f 100644 --- a/homeassistant/components/keymitt_ble/switch.py +++ b/homeassistant/components/keymitt_ble/switch.py @@ -14,13 +14,14 @@ from homeassistant.helpers.entity_platform import ( AddEntitiesCallback, async_get_current_platform, ) +from homeassistant.helpers.typing import VolDictType from .const import DOMAIN from .coordinator import MicroBotDataUpdateCoordinator from .entity import MicroBotEntity CALIBRATE = "calibrate" -CALIBRATE_SCHEMA = { +CALIBRATE_SCHEMA: VolDictType = { vol.Required("depth"): cv.positive_int, vol.Required("duration"): cv.positive_int, vol.Required("mode"): vol.In(["normal", "invert", "toggle"]), diff --git a/homeassistant/components/kodi/media_player.py b/homeassistant/components/kodi/media_player.py index 2bfe21b6eaa..3ba5804f8b3 100644 --- a/homeassistant/components/kodi/media_player.py +++ b/homeassistant/components/kodi/media_player.py @@ -49,7 +49,7 @@ from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.network import is_internal_request -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, VolDictType import homeassistant.util.dt as dt_util from .browse_media import ( @@ -147,7 +147,7 @@ ATTR_MEDIA_ID = "media_id" ATTR_METHOD = "method" -KODI_ADD_MEDIA_SCHEMA = { +KODI_ADD_MEDIA_SCHEMA: VolDictType = { vol.Required(ATTR_MEDIA_TYPE): cv.string, vol.Optional(ATTR_MEDIA_ID): cv.string, vol.Optional(ATTR_MEDIA_NAME): cv.string, diff --git a/homeassistant/components/lifx/light.py b/homeassistant/components/lifx/light.py index 90632f82d9e..caa1140b099 100644 --- a/homeassistant/components/lifx/light.py +++ b/homeassistant/components/lifx/light.py @@ -25,6 +25,7 @@ from homeassistant.helpers import entity_platform import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_call_later +from homeassistant.helpers.typing import VolDictType from .const import ( _LOGGER, @@ -53,7 +54,7 @@ LIFX_STATE_SETTLE_DELAY = 0.3 SERVICE_LIFX_SET_STATE = "set_state" -LIFX_SET_STATE_SCHEMA = { +LIFX_SET_STATE_SCHEMA: VolDictType = { **LIGHT_TURN_ON_SCHEMA, ATTR_INFRARED: vol.All(vol.Coerce(int), vol.Clamp(min=0, max=255)), ATTR_ZONES: vol.All(cv.ensure_list, [cv.positive_int]), @@ -63,7 +64,7 @@ LIFX_SET_STATE_SCHEMA = { SERVICE_LIFX_SET_HEV_CYCLE_STATE = "set_hev_cycle_state" -LIFX_SET_HEV_CYCLE_STATE_SCHEMA = { +LIFX_SET_HEV_CYCLE_STATE_SCHEMA: VolDictType = { ATTR_POWER: vol.Required(cv.boolean), ATTR_DURATION: vol.All(vol.Coerce(float), vol.Clamp(min=0, max=86400)), } diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index 6d3065c48c9..16367c35ec5 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -31,7 +31,7 @@ from homeassistant.helpers.config_validation import ( # noqa: F401 ) from homeassistant.helpers.entity import ToggleEntity, ToggleEntityDescription from homeassistant.helpers.entity_component import EntityComponent -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.typing import ConfigType, VolDictType from homeassistant.loader import bind_hass import homeassistant.util.color as color_util @@ -247,7 +247,7 @@ VALID_BRIGHTNESS_STEP = vol.All(vol.Coerce(int), vol.Clamp(min=-255, max=255)) VALID_BRIGHTNESS_STEP_PCT = vol.All(vol.Coerce(float), vol.Clamp(min=-100, max=100)) VALID_FLASH = vol.In([FLASH_SHORT, FLASH_LONG]) -LIGHT_TURN_ON_SCHEMA = { +LIGHT_TURN_ON_SCHEMA: VolDictType = { vol.Exclusive(ATTR_PROFILE, COLOR_GROUP): cv.string, ATTR_TRANSITION: VALID_TRANSITION, vol.Exclusive(ATTR_BRIGHTNESS, ATTR_BRIGHTNESS): VALID_BRIGHTNESS, @@ -286,7 +286,10 @@ LIGHT_TURN_ON_SCHEMA = { ATTR_EFFECT: cv.string, } -LIGHT_TURN_OFF_SCHEMA = {ATTR_TRANSITION: VALID_TRANSITION, ATTR_FLASH: VALID_FLASH} +LIGHT_TURN_OFF_SCHEMA: VolDictType = { + ATTR_TRANSITION: VALID_TRANSITION, + ATTR_FLASH: VALID_FLASH, +} _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/lyric/climate.py b/homeassistant/components/lyric/climate.py index f8ae978c2fd..50add155915 100644 --- a/homeassistant/components/lyric/climate.py +++ b/homeassistant/components/lyric/climate.py @@ -37,6 +37,7 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_platform import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import VolDictType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from . import LyricDeviceEntity @@ -111,7 +112,7 @@ HVAC_ACTIONS = { SERVICE_HOLD_TIME = "set_hold_time" ATTR_TIME_PERIOD = "time_period" -SCHEMA_HOLD_TIME = { +SCHEMA_HOLD_TIME: VolDictType = { vol.Required(ATTR_TIME_PERIOD, default="01:00:00"): vol.All( cv.time_period, cv.positive_timedelta, diff --git a/homeassistant/components/motion_blinds/cover.py b/homeassistant/components/motion_blinds/cover.py index eb40a1d66ca..2cbee96adb7 100644 --- a/homeassistant/components/motion_blinds/cover.py +++ b/homeassistant/components/motion_blinds/cover.py @@ -20,6 +20,7 @@ from homeassistant.core import CALLBACK_TYPE, HomeAssistant from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_call_later +from homeassistant.helpers.typing import VolDictType from .const import ( ATTR_ABSOLUTE_POSITION, @@ -75,7 +76,7 @@ TDBU_DEVICE_MAP = { } -SET_ABSOLUTE_POSITION_SCHEMA = { +SET_ABSOLUTE_POSITION_SCHEMA: VolDictType = { vol.Required(ATTR_ABSOLUTE_POSITION): vol.All(cv.positive_int, vol.Range(max=100)), vol.Optional(ATTR_TILT_POSITION): vol.All(cv.positive_int, vol.Range(max=100)), vol.Optional(ATTR_WIDTH): vol.All(cv.positive_int, vol.Range(max=100)), diff --git a/homeassistant/components/nexia/climate.py b/homeassistant/components/nexia/climate.py index 7d09f710828..7c28062f4b8 100644 --- a/homeassistant/components/nexia/climate.py +++ b/homeassistant/components/nexia/climate.py @@ -37,6 +37,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_platform import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import VolDictType from .const import ( ATTR_AIRCLEANER_MODE, @@ -55,11 +56,11 @@ SERVICE_SET_AIRCLEANER_MODE = "set_aircleaner_mode" SERVICE_SET_HUMIDIFY_SETPOINT = "set_humidify_setpoint" SERVICE_SET_HVAC_RUN_MODE = "set_hvac_run_mode" -SET_AIRCLEANER_SCHEMA = { +SET_AIRCLEANER_SCHEMA: VolDictType = { vol.Required(ATTR_AIRCLEANER_MODE): cv.string, } -SET_HUMIDITY_SCHEMA = { +SET_HUMIDITY_SCHEMA: VolDictType = { vol.Required(ATTR_HUMIDITY): vol.All(vol.Coerce(int), vol.Range(min=35, max=65)), } diff --git a/homeassistant/components/rainbird/switch.py b/homeassistant/components/rainbird/switch.py index 7f43553aa41..62a2a7c4a32 100644 --- a/homeassistant/components/rainbird/switch.py +++ b/homeassistant/components/rainbird/switch.py @@ -14,6 +14,7 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import VolDictType from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ATTR_DURATION, CONF_IMPORTED_NAMES, DOMAIN, MANUFACTURER @@ -23,7 +24,7 @@ _LOGGER = logging.getLogger(__name__) SERVICE_START_IRRIGATION = "start_irrigation" -SERVICE_SCHEMA_IRRIGATION = { +SERVICE_SCHEMA_IRRIGATION: VolDictType = { vol.Required(ATTR_DURATION): cv.positive_float, } diff --git a/homeassistant/components/rainmachine/switch.py b/homeassistant/components/rainmachine/switch.py index 328d5193e1e..667e609e11c 100644 --- a/homeassistant/components/rainmachine/switch.py +++ b/homeassistant/components/rainmachine/switch.py @@ -6,7 +6,7 @@ import asyncio from collections.abc import Awaitable, Callable, Coroutine from dataclasses import dataclass from datetime import datetime -from typing import Any, Concatenate +from typing import Any, Concatenate, cast from regenmaschine.errors import RainMachineError import voluptuous as vol @@ -18,6 +18,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import VolDictType from . import RainMachineData, RainMachineEntity, async_update_programs_and_zones from .const import ( @@ -191,7 +192,8 @@ async def async_setup_entry( ("stop_program", {}, "async_stop_program"), ("stop_zone", {}, "async_stop_zone"), ): - platform.async_register_entity_service(service_name, schema, method) + schema_dict = cast(VolDictType, schema) + platform.async_register_entity_service(service_name, schema_dict, method) data: RainMachineData = hass.data[DOMAIN][entry.entry_id] entities: list[RainMachineBaseSwitch] = [] diff --git a/homeassistant/components/renson/fan.py b/homeassistant/components/renson/fan.py index 226d623af2b..bff84017e29 100644 --- a/homeassistant/components/renson/fan.py +++ b/homeassistant/components/renson/fan.py @@ -21,6 +21,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_platform import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import VolDictType from homeassistant.util.percentage import ( percentage_to_ranged_value, ranged_value_to_percentage, @@ -51,20 +52,20 @@ SPEED_MAPPING = { Level.LEVEL4.value: 4, } -SET_TIMER_LEVEL_SCHEMA = { +SET_TIMER_LEVEL_SCHEMA: VolDictType = { vol.Required("timer_level"): vol.In( ["level1", "level2", "level3", "level4", "holiday", "breeze"] ), vol.Required("minutes"): cv.positive_int, } -SET_BREEZE_SCHEMA = { +SET_BREEZE_SCHEMA: VolDictType = { vol.Required("breeze_level"): vol.In(["level1", "level2", "level3", "level4"]), vol.Required("temperature"): cv.positive_int, vol.Required("activate"): bool, } -SET_POLLUTION_SETTINGS_SCHEMA = { +SET_POLLUTION_SETTINGS_SCHEMA: VolDictType = { vol.Required("day_pollution_level"): vol.In( ["level1", "level2", "level3", "level4"] ), diff --git a/homeassistant/components/roku/media_player.py b/homeassistant/components/roku/media_player.py index 92361909219..881dda38f15 100644 --- a/homeassistant/components/roku/media_player.py +++ b/homeassistant/components/roku/media_player.py @@ -28,6 +28,7 @@ from homeassistant.const import ATTR_NAME from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_platform from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import VolDictType from .browse_media import async_browse_media from .const import ( @@ -78,7 +79,7 @@ ATTRS_TO_PLAY_ON_ROKU_AUDIO_PARAMS = { ATTR_THUMBNAIL: "albumArtUrl", } -SEARCH_SCHEMA = {vol.Required(ATTR_KEYWORD): str} +SEARCH_SCHEMA: VolDictType = {vol.Required(ATTR_KEYWORD): str} async def async_setup_entry( diff --git a/homeassistant/components/siren/__init__.py b/homeassistant/components/siren/__init__.py index a0a599dd2df..e5837fdd1bf 100644 --- a/homeassistant/components/siren/__init__.py +++ b/homeassistant/components/siren/__init__.py @@ -24,7 +24,7 @@ from homeassistant.helpers.deprecation import ( ) from homeassistant.helpers.entity import ToggleEntity, ToggleEntityDescription from homeassistant.helpers.entity_component import EntityComponent -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.typing import ConfigType, VolDictType from .const import ( # noqa: F401 _DEPRECATED_SUPPORT_DURATION, @@ -44,7 +44,7 @@ _LOGGER = logging.getLogger(__name__) SCAN_INTERVAL = timedelta(seconds=60) -TURN_ON_SCHEMA = { +TURN_ON_SCHEMA: VolDictType = { vol.Optional(ATTR_TONE): vol.Any(vol.Coerce(int), cv.string), vol.Optional(ATTR_DURATION): cv.positive_int, vol.Optional(ATTR_VOLUME_LEVEL): cv.small_float, diff --git a/homeassistant/components/smarttub/binary_sensor.py b/homeassistant/components/smarttub/binary_sensor.py index cca0c6bc2ce..f665f5e61b3 100644 --- a/homeassistant/components/smarttub/binary_sensor.py +++ b/homeassistant/components/smarttub/binary_sensor.py @@ -13,6 +13,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_platform from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import VolDictType from .const import ATTR_ERRORS, ATTR_REMINDERS, DOMAIN, SMARTTUB_CONTROLLER from .entity import SmartTubEntity, SmartTubSensorBase @@ -29,12 +30,12 @@ ATTR_UPDATED_AT = "updated_at" # how many days to snooze the reminder for ATTR_REMINDER_DAYS = "days" -RESET_REMINDER_SCHEMA = { +RESET_REMINDER_SCHEMA: VolDictType = { vol.Required(ATTR_REMINDER_DAYS): vol.All( vol.Coerce(int), vol.Range(min=30, max=365) ) } -SNOOZE_REMINDER_SCHEMA = { +SNOOZE_REMINDER_SCHEMA: VolDictType = { vol.Required(ATTR_REMINDER_DAYS): vol.All( vol.Coerce(int), vol.Range(min=10, max=120) ) diff --git a/homeassistant/components/smarttub/sensor.py b/homeassistant/components/smarttub/sensor.py index 3694ca81a6b..585e8859432 100644 --- a/homeassistant/components/smarttub/sensor.py +++ b/homeassistant/components/smarttub/sensor.py @@ -10,6 +10,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import VolDictType from .const import DOMAIN, SMARTTUB_CONTROLLER from .entity import SmartTubSensorBase @@ -31,7 +32,7 @@ SET_PRIMARY_FILTRATION_SCHEMA = vol.All( ), ) -SET_SECONDARY_FILTRATION_SCHEMA = { +SET_SECONDARY_FILTRATION_SCHEMA: VolDictType = { vol.Required(ATTR_MODE): vol.In( { mode.name.lower() diff --git a/homeassistant/components/switcher_kis/switch.py b/homeassistant/components/switcher_kis/switch.py index 2280d6bc845..aac5da10ae1 100644 --- a/homeassistant/components/switcher_kis/switch.py +++ b/homeassistant/components/switcher_kis/switch.py @@ -21,6 +21,7 @@ from homeassistant.helpers import ( from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import VolDictType from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ( @@ -37,11 +38,11 @@ _LOGGER = logging.getLogger(__name__) API_CONTROL_DEVICE = "control_device" API_SET_AUTO_SHUTDOWN = "set_auto_shutdown" -SERVICE_SET_AUTO_OFF_SCHEMA = { +SERVICE_SET_AUTO_OFF_SCHEMA: VolDictType = { vol.Required(CONF_AUTO_OFF): cv.time_period_str, } -SERVICE_TURN_ON_WITH_TIMER_SCHEMA = { +SERVICE_TURN_ON_WITH_TIMER_SCHEMA: VolDictType = { vol.Required(CONF_TIMER_MINUTES): vol.All( cv.positive_int, vol.Range(min=1, max=150) ), diff --git a/homeassistant/components/tado/climate.py b/homeassistant/components/tado/climate.py index 2698b6e1446..40bdb19b31b 100644 --- a/homeassistant/components/tado/climate.py +++ b/homeassistant/components/tado/climate.py @@ -28,6 +28,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import VolDictType from . import TadoConnector from .const import ( @@ -80,7 +81,7 @@ SERVICE_CLIMATE_TIMER = "set_climate_timer" ATTR_TIME_PERIOD = "time_period" ATTR_REQUESTED_OVERLAY = "requested_overlay" -CLIMATE_TIMER_SCHEMA = { +CLIMATE_TIMER_SCHEMA: VolDictType = { vol.Required(ATTR_TEMPERATURE): vol.Coerce(float), vol.Exclusive(ATTR_TIME_PERIOD, CONST_EXCLUSIVE_OVERLAY_GROUP): vol.All( cv.time_period, cv.positive_timedelta, lambda td: td.total_seconds() @@ -93,7 +94,7 @@ CLIMATE_TIMER_SCHEMA = { SERVICE_TEMP_OFFSET = "set_climate_temperature_offset" ATTR_OFFSET = "offset" -CLIMATE_TEMP_OFFSET_SCHEMA = { +CLIMATE_TEMP_OFFSET_SCHEMA: VolDictType = { vol.Required(ATTR_OFFSET, default=0): vol.Coerce(float), } diff --git a/homeassistant/components/tado/water_heater.py b/homeassistant/components/tado/water_heater.py index 1b3b811d231..0954db71460 100644 --- a/homeassistant/components/tado/water_heater.py +++ b/homeassistant/components/tado/water_heater.py @@ -15,6 +15,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import VolDictType from . import TadoConnector from .const import ( @@ -55,7 +56,7 @@ WATER_HEATER_MAP_TADO = { SERVICE_WATER_HEATER_TIMER = "set_water_heater_timer" ATTR_TIME_PERIOD = "time_period" -WATER_HEATER_TIMER_SCHEMA = { +WATER_HEATER_TIMER_SCHEMA: VolDictType = { vol.Required(ATTR_TIME_PERIOD, default="01:00:00"): vol.All( cv.time_period, cv.positive_timedelta, lambda td: td.total_seconds() ), diff --git a/homeassistant/components/upb/const.py b/homeassistant/components/upb/const.py index 8a2c435a70f..16f2f1b7923 100644 --- a/homeassistant/components/upb/const.py +++ b/homeassistant/components/upb/const.py @@ -3,6 +3,7 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.typing import VolDictType DOMAIN = "upb" @@ -29,7 +30,7 @@ UPB_BRIGHTNESS_RATE_SCHEMA = vol.All( ), ) -UPB_BLINK_RATE_SCHEMA = { +UPB_BLINK_RATE_SCHEMA: VolDictType = { vol.Required(ATTR_BLINK_RATE, default=0.5): vol.All( vol.Coerce(float), vol.Range(min=0, max=4.25) ) diff --git a/homeassistant/components/vizio/const.py b/homeassistant/components/vizio/const.py index 03caa723771..4eb96256d2e 100644 --- a/homeassistant/components/vizio/const.py +++ b/homeassistant/components/vizio/const.py @@ -19,6 +19,7 @@ from homeassistant.const import ( CONF_NAME, ) import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.typing import VolDictType SERVICE_UPDATE_SETTING = "update_setting" @@ -26,7 +27,7 @@ ATTR_SETTING_TYPE = "setting_type" ATTR_SETTING_NAME = "setting_name" ATTR_NEW_VALUE = "new_value" -UPDATE_SETTING_SCHEMA = { +UPDATE_SETTING_SCHEMA: VolDictType = { vol.Required(ATTR_SETTING_TYPE): vol.All(cv.string, vol.Lower, cv.slugify), vol.Required(ATTR_SETTING_NAME): vol.All(cv.string, vol.Lower, cv.slugify), vol.Required(ATTR_NEW_VALUE): vol.Any(vol.Coerce(int), cv.string), diff --git a/homeassistant/components/wemo/fan.py b/homeassistant/components/wemo/fan.py index 3ef8aa67a3d..e1b9aaf2388 100644 --- a/homeassistant/components/wemo/fan.py +++ b/homeassistant/components/wemo/fan.py @@ -14,6 +14,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_platform from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import VolDictType from homeassistant.util.percentage import ( percentage_to_ranged_value, ranged_value_to_percentage, @@ -37,7 +38,7 @@ ATTR_WATER_LEVEL = "water_level" SPEED_RANGE = (FanMode.Minimum, FanMode.Maximum) # off is not included -SET_HUMIDITY_SCHEMA = { +SET_HUMIDITY_SCHEMA: VolDictType = { vol.Required(ATTR_TARGET_HUMIDITY): vol.All( vol.Coerce(float), vol.Range(min=0, max=100) ), diff --git a/homeassistant/components/yardian/switch.py b/homeassistant/components/yardian/switch.py index 549331b6b5f..910bacc1c2e 100644 --- a/homeassistant/components/yardian/switch.py +++ b/homeassistant/components/yardian/switch.py @@ -11,13 +11,14 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import VolDictType from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DEFAULT_WATERING_DURATION, DOMAIN from .coordinator import YardianUpdateCoordinator SERVICE_START_IRRIGATION = "start_irrigation" -SERVICE_SCHEMA_START_IRRIGATION = { +SERVICE_SCHEMA_START_IRRIGATION: VolDictType = { vol.Required("duration"): cv.positive_int, } diff --git a/homeassistant/components/yeelight/__init__.py b/homeassistant/components/yeelight/__init__.py index 0ed75318ac7..9b71bbc3b16 100644 --- a/homeassistant/components/yeelight/__init__.py +++ b/homeassistant/components/yeelight/__init__.py @@ -20,7 +20,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.typing import ConfigType, VolDictType from .const import ( ACTION_OFF, @@ -59,7 +59,7 @@ from .scanner import YeelightScanner _LOGGER = logging.getLogger(__name__) -YEELIGHT_FLOW_TRANSITION_SCHEMA = { +YEELIGHT_FLOW_TRANSITION_SCHEMA: VolDictType = { vol.Optional(ATTR_COUNT, default=0): cv.positive_int, vol.Optional(ATTR_ACTION, default=ACTION_RECOVER): vol.Any( ACTION_RECOVER, ACTION_OFF, ACTION_STAY diff --git a/homeassistant/components/yeelight/light.py b/homeassistant/components/yeelight/light.py index 1d514c131d2..d0d53510859 100644 --- a/homeassistant/components/yeelight/light.py +++ b/homeassistant/components/yeelight/light.py @@ -38,6 +38,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_call_later +from homeassistant.helpers.typing import VolDictType import homeassistant.util.color as color_util from homeassistant.util.color import ( color_temperature_kelvin_to_mired as kelvin_to_mired, @@ -170,22 +171,22 @@ EFFECTS_MAP = { VALID_BRIGHTNESS = vol.All(vol.Coerce(int), vol.Range(min=1, max=100)) -SERVICE_SCHEMA_SET_MODE = { +SERVICE_SCHEMA_SET_MODE: VolDictType = { vol.Required(ATTR_MODE): vol.In([mode.name.lower() for mode in PowerMode]) } -SERVICE_SCHEMA_SET_MUSIC_MODE = {vol.Required(ATTR_MODE_MUSIC): cv.boolean} +SERVICE_SCHEMA_SET_MUSIC_MODE: VolDictType = {vol.Required(ATTR_MODE_MUSIC): cv.boolean} SERVICE_SCHEMA_START_FLOW = YEELIGHT_FLOW_TRANSITION_SCHEMA -SERVICE_SCHEMA_SET_COLOR_SCENE = { +SERVICE_SCHEMA_SET_COLOR_SCENE: VolDictType = { vol.Required(ATTR_RGB_COLOR): vol.All( vol.Coerce(tuple), vol.ExactSequence((cv.byte, cv.byte, cv.byte)) ), vol.Required(ATTR_BRIGHTNESS): VALID_BRIGHTNESS, } -SERVICE_SCHEMA_SET_HSV_SCENE = { +SERVICE_SCHEMA_SET_HSV_SCENE: VolDictType = { vol.Required(ATTR_HS_COLOR): vol.All( vol.Coerce(tuple), vol.ExactSequence( @@ -198,14 +199,14 @@ SERVICE_SCHEMA_SET_HSV_SCENE = { vol.Required(ATTR_BRIGHTNESS): VALID_BRIGHTNESS, } -SERVICE_SCHEMA_SET_COLOR_TEMP_SCENE = { +SERVICE_SCHEMA_SET_COLOR_TEMP_SCENE: VolDictType = { vol.Required(ATTR_KELVIN): vol.All(vol.Coerce(int), vol.Range(min=1700, max=6500)), vol.Required(ATTR_BRIGHTNESS): VALID_BRIGHTNESS, } SERVICE_SCHEMA_SET_COLOR_FLOW_SCENE = YEELIGHT_FLOW_TRANSITION_SCHEMA -SERVICE_SCHEMA_SET_AUTO_DELAY_OFF_SCENE = { +SERVICE_SCHEMA_SET_AUTO_DELAY_OFF_SCENE: VolDictType = { vol.Required(ATTR_MINUTES): vol.All(vol.Coerce(int), vol.Range(min=1, max=60)), vol.Required(ATTR_BRIGHTNESS): VALID_BRIGHTNESS, } From de8bccb650a1515d7a27f2a7404f34b6d291b0c7 Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Tue, 25 Jun 2024 20:44:06 +1000 Subject: [PATCH 1157/1445] Add services to Teslemetry (#119119) * Add custom services * Fixes * wip * Test coverage * Update homeassistant/components/teslemetry/__init__.py Co-authored-by: G Johansson * Add error translations * Translate command error * Fix test * Expand on comment as requested * Remove impossible cases --------- Co-authored-by: G Johansson --- .../components/teslemetry/__init__.py | 20 ++ .../components/teslemetry/icons.json | 8 + .../components/teslemetry/services.py | 321 ++++++++++++++++++ .../components/teslemetry/services.yaml | 132 +++++++ .../components/teslemetry/strings.json | 150 ++++++++ tests/components/teslemetry/test_services.py | 238 +++++++++++++ 6 files changed, 869 insertions(+) create mode 100644 homeassistant/components/teslemetry/services.py create mode 100644 homeassistant/components/teslemetry/services.yaml create mode 100644 tests/components/teslemetry/test_services.py diff --git a/homeassistant/components/teslemetry/__init__.py b/homeassistant/components/teslemetry/__init__.py index 21ea2915884..b65f5fb64ce 100644 --- a/homeassistant/components/teslemetry/__init__.py +++ b/homeassistant/components/teslemetry/__init__.py @@ -15,8 +15,11 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_ACCESS_TOKEN, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady +from homeassistant.helpers import device_registry as dr from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.typing import ConfigType from .const import DOMAIN, LOGGER, MODELS from .coordinator import ( @@ -25,6 +28,7 @@ from .coordinator import ( TeslemetryVehicleDataCoordinator, ) from .models import TeslemetryData, TeslemetryEnergyData, TeslemetryVehicleData +from .services import async_register_services PLATFORMS: Final = [ Platform.BINARY_SENSOR, @@ -43,6 +47,14 @@ PLATFORMS: Final = [ type TeslemetryConfigEntry = ConfigEntry[TeslemetryData] +CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN) + + +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up the Telemetry integration.""" + async_register_services(hass) + return True + async def async_setup_entry(hass: HomeAssistant, entry: TeslemetryConfigEntry) -> bool: """Set up Teslemetry config.""" @@ -65,6 +77,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: TeslemetryConfigEntry) - except TeslaFleetError as e: raise ConfigEntryNotReady from e + device_registry = dr.async_get(hass) + # Create array of classes vehicles: list[TeslemetryVehicleData] = [] energysites: list[TeslemetryEnergyData] = [] @@ -143,6 +157,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: TeslemetryConfigEntry) - if models: energysite.device["model"] = ", ".join(sorted(models)) + # Create the energy site device regardless of it having entities + # This is so users with a Wall Connector but without a Powerwall can still make service calls + device_registry.async_get_or_create( + config_entry_id=entry.entry_id, **energysite.device + ) + # Setup Platforms entry.runtime_data = TeslemetryData(vehicles, energysites, scopes) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) diff --git a/homeassistant/components/teslemetry/icons.json b/homeassistant/components/teslemetry/icons.json index 089a3bea548..aea98e95e0b 100644 --- a/homeassistant/components/teslemetry/icons.json +++ b/homeassistant/components/teslemetry/icons.json @@ -257,5 +257,13 @@ "default": "mdi:speedometer-slow" } } + }, + "services": { + "navigation_gps_request": "mdi:crosshairs-gps", + "set_scheduled_charging": "mdi:timeline-clock-outline", + "set_scheduled_departure": "mdi:home-clock", + "speed_limit": "mdi:car-speed-limiter", + "valet_mode": "mdi:speedometer-slow", + "time_of_use": "mdi:clock-time-eight-outline" } } diff --git a/homeassistant/components/teslemetry/services.py b/homeassistant/components/teslemetry/services.py new file mode 100644 index 00000000000..97cfffa1699 --- /dev/null +++ b/homeassistant/components/teslemetry/services.py @@ -0,0 +1,321 @@ +"""Service calls for the Teslemetry integration.""" + +import logging + +import voluptuous as vol +from voluptuous import All, Range + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_DEVICE_ID, CONF_LATITUDE, CONF_LONGITUDE +from homeassistant.core import HomeAssistant, ServiceCall +from homeassistant.exceptions import HomeAssistantError, ServiceValidationError +from homeassistant.helpers import config_validation as cv, device_registry as dr + +from .const import DOMAIN +from .helpers import handle_command, handle_vehicle_command, wake_up_vehicle +from .models import TeslemetryEnergyData, TeslemetryVehicleData + +_LOGGER = logging.getLogger(__name__) + +# Attributes +ATTR_ID = "id" +ATTR_GPS = "gps" +ATTR_TYPE = "type" +ATTR_VALUE = "value" +ATTR_LOCALE = "locale" +ATTR_ORDER = "order" +ATTR_TIMESTAMP = "timestamp" +ATTR_FIELDS = "fields" +ATTR_ENABLE = "enable" +ATTR_TIME = "time" +ATTR_PIN = "pin" +ATTR_TOU_SETTINGS = "tou_settings" +ATTR_PRECONDITIONING_ENABLED = "preconditioning_enabled" +ATTR_PRECONDITIONING_WEEKDAYS = "preconditioning_weekdays_only" +ATTR_DEPARTURE_TIME = "departure_time" +ATTR_OFF_PEAK_CHARGING_ENABLED = "off_peak_charging_enabled" +ATTR_OFF_PEAK_CHARGING_WEEKDAYS = "off_peak_charging_weekdays_only" +ATTR_END_OFF_PEAK_TIME = "end_off_peak_time" + +# Services +SERVICE_NAVIGATE_ATTR_GPS_REQUEST = "navigation_gps_request" +SERVICE_SET_SCHEDULED_CHARGING = "set_scheduled_charging" +SERVICE_SET_SCHEDULED_DEPARTURE = "set_scheduled_departure" +SERVICE_VALET_MODE = "valet_mode" +SERVICE_SPEED_LIMIT = "speed_limit" +SERVICE_TIME_OF_USE = "time_of_use" + + +def async_get_device_for_service_call( + hass: HomeAssistant, call: ServiceCall +) -> dr.DeviceEntry: + """Get the device entry related to a service call.""" + device_id = call.data[CONF_DEVICE_ID] + device_registry = dr.async_get(hass) + if (device_entry := device_registry.async_get(device_id)) is None: + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="invalid_device", + translation_placeholders={"device_id": device_id}, + ) + + return device_entry + + +def async_get_config_for_device( + hass: HomeAssistant, device_entry: dr.DeviceEntry +) -> ConfigEntry: + """Get the config entry related to a device entry.""" + config_entry: ConfigEntry + for entry_id in device_entry.config_entries: + if entry := hass.config_entries.async_get_entry(entry_id): + if entry.domain == DOMAIN: + config_entry = entry + return config_entry + + +def async_get_vehicle_for_entry( + hass: HomeAssistant, device: dr.DeviceEntry, config: ConfigEntry +) -> TeslemetryVehicleData: + """Get the vehicle data for a config entry.""" + vehicle_data: TeslemetryVehicleData + assert device.serial_number is not None + for vehicle in config.runtime_data.vehicles: + if vehicle.vin == device.serial_number: + vehicle_data = vehicle + return vehicle_data + + +def async_get_energy_site_for_entry( + hass: HomeAssistant, device: dr.DeviceEntry, config: ConfigEntry +) -> TeslemetryEnergyData: + """Get the energy site data for a config entry.""" + energy_data: TeslemetryEnergyData + assert device.serial_number is not None + for energysite in config.runtime_data.energysites: + if str(energysite.id) == device.serial_number: + energy_data = energysite + return energy_data + + +def async_register_services(hass: HomeAssistant) -> None: # noqa: C901 + """Set up the Teslemetry services.""" + + async def navigate_gps_request(call: ServiceCall) -> None: + """Send lat,lon,order with a vehicle.""" + device = async_get_device_for_service_call(hass, call) + config = async_get_config_for_device(hass, device) + vehicle = async_get_vehicle_for_entry(hass, device, config) + + await wake_up_vehicle(vehicle) + await handle_vehicle_command( + vehicle.api.navigation_gps_request( + lat=call.data[ATTR_GPS][CONF_LATITUDE], + lon=call.data[ATTR_GPS][CONF_LONGITUDE], + order=call.data.get(ATTR_ORDER), + ) + ) + + hass.services.async_register( + DOMAIN, + SERVICE_NAVIGATE_ATTR_GPS_REQUEST, + navigate_gps_request, + schema=vol.Schema( + { + vol.Required(CONF_DEVICE_ID): cv.string, + vol.Required(ATTR_GPS): { + vol.Required(CONF_LATITUDE): cv.latitude, + vol.Required(CONF_LONGITUDE): cv.longitude, + }, + vol.Optional(ATTR_ORDER): cv.positive_int, + } + ), + ) + + async def set_scheduled_charging(call: ServiceCall) -> None: + """Configure fleet telemetry.""" + device = async_get_device_for_service_call(hass, call) + config = async_get_config_for_device(hass, device) + vehicle = async_get_vehicle_for_entry(hass, device, config) + + time: int | None = None + # Convert time to minutes since minute + if "time" in call.data: + (hours, minutes, *seconds) = call.data["time"].split(":") + time = int(hours) * 60 + int(minutes) + elif call.data["enable"]: + raise ServiceValidationError( + translation_domain=DOMAIN, translation_key="set_scheduled_charging_time" + ) + + await wake_up_vehicle(vehicle) + await handle_vehicle_command( + vehicle.api.set_scheduled_charging(enable=call.data["enable"], time=time) + ) + + hass.services.async_register( + DOMAIN, + SERVICE_SET_SCHEDULED_CHARGING, + set_scheduled_charging, + schema=vol.Schema( + { + vol.Required(CONF_DEVICE_ID): cv.string, + vol.Required(ATTR_ENABLE): bool, + vol.Optional(ATTR_TIME): str, + } + ), + ) + + async def set_scheduled_departure(call: ServiceCall) -> None: + """Configure fleet telemetry.""" + device = async_get_device_for_service_call(hass, call) + config = async_get_config_for_device(hass, device) + vehicle = async_get_vehicle_for_entry(hass, device, config) + + enable = call.data.get("enable", True) + + # Preconditioning + preconditioning_enabled = call.data.get(ATTR_PRECONDITIONING_ENABLED, False) + preconditioning_weekdays_only = call.data.get( + ATTR_PRECONDITIONING_WEEKDAYS, False + ) + departure_time: int | None = None + if ATTR_DEPARTURE_TIME in call.data: + (hours, minutes, *seconds) = call.data[ATTR_DEPARTURE_TIME].split(":") + departure_time = int(hours) * 60 + int(minutes) + elif preconditioning_enabled: + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="set_scheduled_departure_preconditioning", + ) + + # Off peak charging + off_peak_charging_enabled = call.data.get(ATTR_OFF_PEAK_CHARGING_ENABLED, False) + off_peak_charging_weekdays_only = call.data.get( + ATTR_OFF_PEAK_CHARGING_WEEKDAYS, False + ) + end_off_peak_time: int | None = None + + if ATTR_END_OFF_PEAK_TIME in call.data: + (hours, minutes, *seconds) = call.data[ATTR_END_OFF_PEAK_TIME].split(":") + end_off_peak_time = int(hours) * 60 + int(minutes) + elif off_peak_charging_enabled: + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="set_scheduled_departure_off_peak", + ) + + await wake_up_vehicle(vehicle) + await handle_vehicle_command( + vehicle.api.set_scheduled_departure( + enable, + preconditioning_enabled, + preconditioning_weekdays_only, + departure_time, + off_peak_charging_enabled, + off_peak_charging_weekdays_only, + end_off_peak_time, + ) + ) + + hass.services.async_register( + DOMAIN, + SERVICE_SET_SCHEDULED_DEPARTURE, + set_scheduled_departure, + schema=vol.Schema( + { + vol.Required(CONF_DEVICE_ID): cv.string, + vol.Optional(ATTR_ENABLE): bool, + vol.Optional(ATTR_PRECONDITIONING_ENABLED): bool, + vol.Optional(ATTR_PRECONDITIONING_WEEKDAYS): bool, + vol.Optional(ATTR_DEPARTURE_TIME): str, + vol.Optional(ATTR_OFF_PEAK_CHARGING_ENABLED): bool, + vol.Optional(ATTR_OFF_PEAK_CHARGING_WEEKDAYS): bool, + vol.Optional(ATTR_END_OFF_PEAK_TIME): str, + } + ), + ) + + async def valet_mode(call: ServiceCall) -> None: + """Configure fleet telemetry.""" + device = async_get_device_for_service_call(hass, call) + config = async_get_config_for_device(hass, device) + vehicle = async_get_vehicle_for_entry(hass, device, config) + + await wake_up_vehicle(vehicle) + await handle_vehicle_command( + vehicle.api.set_valet_mode( + call.data.get("enable"), call.data.get("pin", "") + ) + ) + + hass.services.async_register( + DOMAIN, + SERVICE_VALET_MODE, + valet_mode, + schema=vol.Schema( + { + vol.Required(CONF_DEVICE_ID): cv.string, + vol.Required(ATTR_ENABLE): cv.boolean, + vol.Required(ATTR_PIN): All(cv.positive_int, Range(min=1000, max=9999)), + } + ), + ) + + async def speed_limit(call: ServiceCall) -> None: + """Configure fleet telemetry.""" + device = async_get_device_for_service_call(hass, call) + config = async_get_config_for_device(hass, device) + vehicle = async_get_vehicle_for_entry(hass, device, config) + + await wake_up_vehicle(vehicle) + enable = call.data.get("enable") + if enable is True: + await handle_vehicle_command( + vehicle.api.speed_limit_activate(call.data.get("pin")) + ) + elif enable is False: + await handle_vehicle_command( + vehicle.api.speed_limit_deactivate(call.data.get("pin")) + ) + + hass.services.async_register( + DOMAIN, + SERVICE_SPEED_LIMIT, + speed_limit, + schema=vol.Schema( + { + vol.Required(CONF_DEVICE_ID): cv.string, + vol.Required(ATTR_ENABLE): cv.boolean, + vol.Required(ATTR_PIN): All(cv.positive_int, Range(min=1000, max=9999)), + } + ), + ) + + async def time_of_use(call: ServiceCall) -> None: + """Configure time of use settings.""" + device = async_get_device_for_service_call(hass, call) + config = async_get_config_for_device(hass, device) + site = async_get_energy_site_for_entry(hass, device, config) + + resp = await handle_command( + site.api.time_of_use_settings(call.data.get(ATTR_TOU_SETTINGS)) + ) + if "error" in resp: + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="command_error", + translation_placeholders={"error": resp["error"]}, + ) + + hass.services.async_register( + DOMAIN, + SERVICE_TIME_OF_USE, + time_of_use, + schema=vol.Schema( + { + vol.Required(CONF_DEVICE_ID): cv.string, + vol.Required(ATTR_TOU_SETTINGS): dict, + } + ), + ) diff --git a/homeassistant/components/teslemetry/services.yaml b/homeassistant/components/teslemetry/services.yaml new file mode 100644 index 00000000000..e98f124dd19 --- /dev/null +++ b/homeassistant/components/teslemetry/services.yaml @@ -0,0 +1,132 @@ +navigation_gps_request: + fields: + device_id: + required: true + selector: + device: + filter: + - integration: teslemetry + gps: + required: true + example: '{"latitude": -27.9699373, "longitude": 153.4081865}' + selector: + location: + radius: false + order: + required: false + default: 1 + selector: + number: + +time_of_use: + fields: + device_id: + required: true + selector: + device: + filter: + - integration: teslemetry + tou_settings: + required: true + selector: + object: + +set_scheduled_charging: + fields: + device_id: + required: true + selector: + device: + filter: + integration: teslemetry + enable: + required: true + default: true + selector: + boolean: + time: + required: false + selector: + time: + +set_scheduled_departure: + fields: + device_id: + required: true + selector: + device: + filter: + integration: teslemetry + enable: + required: false + default: true + selector: + boolean: + preconditioning_enabled: + required: false + default: false + selector: + boolean: + preconditioning_weekdays_only: + required: false + default: false + selector: + boolean: + departure_time: + required: false + selector: + time: + off_peak_charging_enabled: + required: false + default: false + selector: + boolean: + off_peak_charging_weekdays_only: + required: false + default: false + selector: + boolean: + end_off_peak_time: + required: false + selector: + time: + +valet_mode: + fields: + device_id: + required: true + selector: + device: + filter: + integration: teslemetry + enable: + required: true + selector: + boolean: + pin: + required: true + selector: + number: + min: 1000 + max: 9999 + mode: box + +speed_limit: + fields: + device_id: + required: true + selector: + device: + filter: + integration: teslemetry + enable: + required: true + selector: + boolean: + pin: + required: true + selector: + number: + min: 1000 + max: 9999 + mode: box diff --git a/homeassistant/components/teslemetry/strings.json b/homeassistant/components/teslemetry/strings.json index fe45b4ee9e3..9ff14f2dc8c 100644 --- a/homeassistant/components/teslemetry/strings.json +++ b/homeassistant/components/teslemetry/strings.json @@ -473,6 +473,156 @@ }, "invalid_cop_temp": { "message": "Cabin overheat protection does not support that temperature" + }, + "set_scheduled_charging_time": { + "message": "Time required to complete the operation" + }, + "set_scheduled_departure_preconditioning": { + "message": "Departure time required to enable preconditioning" + }, + "set_scheduled_departure_off_peak": { + "message": "To enable scheduled departure, end off peak time is required." + }, + "invalid_device": { + "message": "Invalid device ID: {device_id}" + }, + "no_config_entry_for_device": { + "message": "No config entry for device ID: {device_id}" + }, + "no_vehicle_data_for_device": { + "message": "No vehicle data for device ID: {device_id}" + }, + "no_energy_site_data_for_device": { + "message": "No energy site data for device ID: {device_id}" + }, + "command_error": { + "message": "Command returned error: {error}" + } + }, + "services": { + "navigation_gps_request": { + "description": "Set vehicle navigation to the provided latitude/longitude coordinates.", + "fields": { + "device_id": { + "description": "Vehicle to share to.", + "name": "Vehicle" + }, + "gps": { + "description": "Location to navigate to.", + "name": "Location" + }, + "order": { + "description": "Order for this destination if specifying multiple destinations.", + "name": "Order" + } + }, + "name": "Navigate to coordinates" + }, + "set_scheduled_charging": { + "description": "Sets a time at which charging should be completed.", + "fields": { + "device_id": { + "description": "Vehicle to schedule.", + "name": "Vehicle" + }, + "enable": { + "description": "Enable or disable scheduled charging.", + "name": "Enable" + }, + "time": { + "description": "Time to start charging.", + "name": "Time" + } + }, + "name": "Set scheduled charging" + }, + "set_scheduled_departure": { + "description": "Sets a time at which departure should be completed.", + "fields": { + "departure_time": { + "description": "Time to be preconditioned by.", + "name": "Departure time" + }, + "device_id": { + "description": "Vehicle to schedule.", + "name": "Vehicle" + }, + "enable": { + "description": "Enable or disable scheduled departure.", + "name": "Enable" + }, + "end_off_peak_time": { + "description": "Time to complete charging by.", + "name": "End off peak time" + }, + "off_peak_charging_enabled": { + "description": "Enable off peak charging.", + "name": "Off peak charging enabled" + }, + "off_peak_charging_weekdays_only": { + "description": "Enable off peak charging on weekdays only.", + "name": "Off peak charging weekdays only" + }, + "preconditioning_enabled": { + "description": "Enable preconditioning.", + "name": "Preconditioning enabled" + }, + "preconditioning_weekdays_only": { + "description": "Enable preconditioning on weekdays only.", + "name": "Preconditioning weekdays only" + } + }, + "name": "Set scheduled departure" + }, + "speed_limit": { + "description": "Activate the speed limit of the vehicle.", + "fields": { + "device_id": { + "description": "Vehicle to limit.", + "name": "Vehicle" + }, + "enable": { + "description": "Enable or disable speed limit.", + "name": "Enable" + }, + "pin": { + "description": "4 digit PIN.", + "name": "PIN" + } + }, + "name": "Set speed limit" + }, + "time_of_use": { + "description": "Update the time of use settings for the energy site.", + "fields": { + "device_id": { + "description": "Energy Site to configure.", + "name": "Energy Site" + }, + "tou_settings": { + "description": "See https://developer.tesla.com/docs/fleet-api#time_of_use_settings for details.", + "name": "Settings" + } + }, + "name": "Time of use settings" + }, + "valet_mode": { + "description": "Activate the valet mode of the vehicle.", + "fields": { + "device_id": { + "description": "Vehicle to limit.", + "name": "Vehicle" + }, + "enable": { + "description": "Enable or disable valet mode.", + "name": "Enable" + }, + "pin": { + "description": "4 digit PIN.", + "name": "PIN" + } + }, + "name": "Set valet mode" } } } diff --git a/tests/components/teslemetry/test_services.py b/tests/components/teslemetry/test_services.py new file mode 100644 index 00000000000..a5b55f5dcc5 --- /dev/null +++ b/tests/components/teslemetry/test_services.py @@ -0,0 +1,238 @@ +"""Test the Teslemetry services.""" + +from unittest.mock import patch + +import pytest + +from homeassistant.components.teslemetry.const import DOMAIN +from homeassistant.components.teslemetry.services import ( + ATTR_DEPARTURE_TIME, + ATTR_ENABLE, + ATTR_END_OFF_PEAK_TIME, + ATTR_GPS, + ATTR_OFF_PEAK_CHARGING_ENABLED, + ATTR_OFF_PEAK_CHARGING_WEEKDAYS, + ATTR_PIN, + ATTR_PRECONDITIONING_ENABLED, + ATTR_PRECONDITIONING_WEEKDAYS, + ATTR_TIME, + ATTR_TOU_SETTINGS, + SERVICE_NAVIGATE_ATTR_GPS_REQUEST, + SERVICE_SET_SCHEDULED_CHARGING, + SERVICE_SET_SCHEDULED_DEPARTURE, + SERVICE_SPEED_LIMIT, + SERVICE_TIME_OF_USE, + SERVICE_VALET_MODE, +) +from homeassistant.const import CONF_DEVICE_ID, CONF_LATITUDE, CONF_LONGITUDE +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError, ServiceValidationError +from homeassistant.helpers import entity_registry as er + +from . import setup_platform +from .const import COMMAND_ERROR, COMMAND_OK + +lat = -27.9699373 +lon = 153.3726526 + + +async def test_services( + hass: HomeAssistant, +) -> None: + """Tests that the custom services are correct.""" + + await setup_platform(hass) + entity_registry = er.async_get(hass) + + # Get a vehicle device ID + vehicle_device = entity_registry.async_get("sensor.test_charging").device_id + energy_device = entity_registry.async_get( + "sensor.energy_site_battery_power" + ).device_id + + with patch( + "homeassistant.components.teslemetry.VehicleSpecific.navigation_gps_request", + return_value=COMMAND_OK, + ) as navigation_gps_request: + await hass.services.async_call( + DOMAIN, + SERVICE_NAVIGATE_ATTR_GPS_REQUEST, + { + CONF_DEVICE_ID: vehicle_device, + ATTR_GPS: {CONF_LATITUDE: lat, CONF_LONGITUDE: lon}, + }, + blocking=True, + ) + navigation_gps_request.assert_called_once() + + with patch( + "homeassistant.components.teslemetry.VehicleSpecific.set_scheduled_charging", + return_value=COMMAND_OK, + ) as set_scheduled_charging: + await hass.services.async_call( + DOMAIN, + SERVICE_SET_SCHEDULED_CHARGING, + { + CONF_DEVICE_ID: vehicle_device, + ATTR_ENABLE: True, + ATTR_TIME: "6:00", + }, + blocking=True, + ) + set_scheduled_charging.assert_called_once() + + with pytest.raises(ServiceValidationError): + await hass.services.async_call( + DOMAIN, + SERVICE_SET_SCHEDULED_CHARGING, + { + CONF_DEVICE_ID: vehicle_device, + ATTR_ENABLE: True, + }, + blocking=True, + ) + + with patch( + "homeassistant.components.teslemetry.VehicleSpecific.set_scheduled_departure", + return_value=COMMAND_OK, + ) as set_scheduled_departure: + await hass.services.async_call( + DOMAIN, + SERVICE_SET_SCHEDULED_DEPARTURE, + { + CONF_DEVICE_ID: vehicle_device, + ATTR_ENABLE: True, + ATTR_PRECONDITIONING_ENABLED: True, + ATTR_PRECONDITIONING_WEEKDAYS: False, + ATTR_DEPARTURE_TIME: "6:00", + ATTR_OFF_PEAK_CHARGING_ENABLED: True, + ATTR_OFF_PEAK_CHARGING_WEEKDAYS: False, + ATTR_END_OFF_PEAK_TIME: "5:00", + }, + blocking=True, + ) + set_scheduled_departure.assert_called_once() + + with pytest.raises(ServiceValidationError): + await hass.services.async_call( + DOMAIN, + SERVICE_SET_SCHEDULED_DEPARTURE, + { + CONF_DEVICE_ID: vehicle_device, + ATTR_ENABLE: True, + ATTR_PRECONDITIONING_ENABLED: True, + }, + blocking=True, + ) + + with pytest.raises(ServiceValidationError): + await hass.services.async_call( + DOMAIN, + SERVICE_SET_SCHEDULED_DEPARTURE, + { + CONF_DEVICE_ID: vehicle_device, + ATTR_ENABLE: True, + ATTR_OFF_PEAK_CHARGING_ENABLED: True, + }, + blocking=True, + ) + + with patch( + "homeassistant.components.teslemetry.VehicleSpecific.set_valet_mode", + return_value=COMMAND_OK, + ) as set_valet_mode: + await hass.services.async_call( + DOMAIN, + SERVICE_VALET_MODE, + { + CONF_DEVICE_ID: vehicle_device, + ATTR_ENABLE: True, + ATTR_PIN: 1234, + }, + blocking=True, + ) + set_valet_mode.assert_called_once() + + with patch( + "homeassistant.components.teslemetry.VehicleSpecific.speed_limit_activate", + return_value=COMMAND_OK, + ) as speed_limit_activate: + await hass.services.async_call( + DOMAIN, + SERVICE_SPEED_LIMIT, + { + CONF_DEVICE_ID: vehicle_device, + ATTR_ENABLE: True, + ATTR_PIN: 1234, + }, + blocking=True, + ) + speed_limit_activate.assert_called_once() + + with patch( + "homeassistant.components.teslemetry.VehicleSpecific.speed_limit_deactivate", + return_value=COMMAND_OK, + ) as speed_limit_deactivate: + await hass.services.async_call( + DOMAIN, + SERVICE_SPEED_LIMIT, + { + CONF_DEVICE_ID: vehicle_device, + ATTR_ENABLE: False, + ATTR_PIN: 1234, + }, + blocking=True, + ) + speed_limit_deactivate.assert_called_once() + + with patch( + "homeassistant.components.teslemetry.EnergySpecific.time_of_use_settings", + return_value=COMMAND_OK, + ) as set_time_of_use: + await hass.services.async_call( + DOMAIN, + SERVICE_TIME_OF_USE, + { + CONF_DEVICE_ID: energy_device, + ATTR_TOU_SETTINGS: {}, + }, + blocking=True, + ) + set_time_of_use.assert_called_once() + + with ( + patch( + "homeassistant.components.teslemetry.EnergySpecific.time_of_use_settings", + return_value=COMMAND_ERROR, + ) as set_time_of_use, + pytest.raises(HomeAssistantError), + ): + await hass.services.async_call( + DOMAIN, + SERVICE_TIME_OF_USE, + { + CONF_DEVICE_ID: energy_device, + ATTR_TOU_SETTINGS: {}, + }, + blocking=True, + ) + + +async def test_service_validation_errors( + hass: HomeAssistant, +) -> None: + """Tests that the custom services handle bad data.""" + + await setup_platform(hass) + + # Bad device ID + with pytest.raises(ServiceValidationError): + await hass.services.async_call( + DOMAIN, + SERVICE_NAVIGATE_ATTR_GPS_REQUEST, + { + CONF_DEVICE_ID: "nope", + ATTR_GPS: {CONF_LATITUDE: lat, CONF_LONGITUDE: lon}, + }, + blocking=True, + ) From 62fd692d2704715adf0514b1e626576a9cc54720 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 25 Jun 2024 12:48:00 +0200 Subject: [PATCH 1158/1445] Improve async_register_admin_service schema typing (#120405) --- homeassistant/components/zha/websocket_api.py | 5 +++-- homeassistant/helpers/service.py | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/zha/websocket_api.py b/homeassistant/components/zha/websocket_api.py index 1a51a06243e..cb95e930b1a 100644 --- a/homeassistant/components/zha/websocket_api.py +++ b/homeassistant/components/zha/websocket_api.py @@ -21,6 +21,7 @@ from homeassistant.helpers import entity_registry as er import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.service import async_register_admin_service +from homeassistant.helpers.typing import VolDictType, VolSchemaType from .api import ( async_change_channel, @@ -126,7 +127,7 @@ def _ensure_list_if_present[_T](value: _T | None) -> list[_T] | list[Any] | None return cast("list[_T]", value) if isinstance(value, list) else [value] -SERVICE_PERMIT_PARAMS = { +SERVICE_PERMIT_PARAMS: VolDictType = { vol.Optional(ATTR_IEEE): IEEE_SCHEMA, vol.Optional(ATTR_DURATION, default=60): vol.All( vol.Coerce(int), vol.Range(0, 254) @@ -138,7 +139,7 @@ SERVICE_PERMIT_PARAMS = { vol.Exclusive(ATTR_QR_CODE, "install_code"): vol.All(cv.string, qr_to_install_code), } -SERVICE_SCHEMAS = { +SERVICE_SCHEMAS: dict[str, VolSchemaType] = { SERVICE_PERMIT: vol.Schema( vol.All( cv.deprecated(ATTR_IEEE_ADDRESS, replacement_key=ATTR_IEEE), diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index a9959902084..22f5e7f8710 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -63,7 +63,7 @@ from . import ( ) from .group import expand_entity_ids from .selector import TargetSelector -from .typing import ConfigType, TemplateVarsType +from .typing import ConfigType, TemplateVarsType, VolSchemaType if TYPE_CHECKING: from .entity import Entity @@ -1100,7 +1100,7 @@ def async_register_admin_service( domain: str, service: str, service_func: Callable[[ServiceCall], Awaitable[None] | None], - schema: vol.Schema = vol.Schema({}, extra=vol.PREVENT_EXTRA), + schema: VolSchemaType = vol.Schema({}, extra=vol.PREVENT_EXTRA), ) -> None: """Register a service that requires admin access.""" hass.services.async_register( From 7453b7df63d7fe6a330d1612fa0d0ff5a69886f5 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 25 Jun 2024 13:03:18 +0200 Subject: [PATCH 1159/1445] Improve mqtt schema typing (#120407) --- homeassistant/components/mqtt/climate.py | 4 ++-- homeassistant/components/mqtt/cover.py | 4 ++-- homeassistant/components/mqtt/device_tracker.py | 4 ++-- homeassistant/components/mqtt/event.py | 4 ++-- homeassistant/components/mqtt/fan.py | 4 ++-- homeassistant/components/mqtt/humidifier.py | 4 ++-- homeassistant/components/mqtt/image.py | 4 ++-- homeassistant/components/mqtt/lawn_mower.py | 4 ++-- homeassistant/components/mqtt/light/__init__.py | 6 +++--- homeassistant/components/mqtt/light/schema_basic.py | 4 ++-- homeassistant/components/mqtt/light/schema_json.py | 4 ++-- .../components/mqtt/light/schema_template.py | 4 ++-- homeassistant/components/mqtt/mixins.py | 7 ++++--- homeassistant/components/mqtt/models.py | 11 +++++++---- homeassistant/components/mqtt/number.py | 4 ++-- homeassistant/components/mqtt/select.py | 4 ++-- homeassistant/components/mqtt/sensor.py | 4 ++-- homeassistant/components/mqtt/siren.py | 4 ++-- homeassistant/components/mqtt/text.py | 4 ++-- homeassistant/components/mqtt/update.py | 4 ++-- homeassistant/components/mqtt/vacuum.py | 4 ++-- homeassistant/components/mqtt/valve.py | 4 ++-- homeassistant/components/mqtt/water_heater.py | 4 ++-- 23 files changed, 54 insertions(+), 50 deletions(-) diff --git a/homeassistant/components/mqtt/climate.py b/homeassistant/components/mqtt/climate.py index f63c9ecc7ae..7873b056889 100644 --- a/homeassistant/components/mqtt/climate.py +++ b/homeassistant/components/mqtt/climate.py @@ -47,7 +47,7 @@ from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.template import Template -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.typing import ConfigType, VolSchemaType from homeassistant.util.unit_conversion import TemperatureConverter from . import subscription @@ -550,7 +550,7 @@ class MqttClimate(MqttTemperatureControlEntity, ClimateEntity): _enable_turn_on_off_backwards_compatibility = False @staticmethod - def config_schema() -> vol.Schema: + def config_schema() -> VolSchemaType: """Return the config schema.""" return DISCOVERY_SCHEMA diff --git a/homeassistant/components/mqtt/cover.py b/homeassistant/components/mqtt/cover.py index bd79c0f9470..2d1b64d002a 100644 --- a/homeassistant/components/mqtt/cover.py +++ b/homeassistant/components/mqtt/cover.py @@ -31,7 +31,7 @@ from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.service_info.mqtt import ReceivePayloadType -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.typing import ConfigType, VolSchemaType from homeassistant.util.json import JSON_DECODE_EXCEPTIONS, json_loads from homeassistant.util.percentage import ( percentage_to_ranged_value, @@ -246,7 +246,7 @@ class MqttCover(MqttEntity, CoverEntity): _tilt_range: tuple[int, int] @staticmethod - def config_schema() -> vol.Schema: + def config_schema() -> VolSchemaType: """Return the config schema.""" return DISCOVERY_SCHEMA diff --git a/homeassistant/components/mqtt/device_tracker.py b/homeassistant/components/mqtt/device_tracker.py index 082483a64a3..b2aeb4c0fc1 100644 --- a/homeassistant/components/mqtt/device_tracker.py +++ b/homeassistant/components/mqtt/device_tracker.py @@ -27,7 +27,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.typing import ConfigType, VolSchemaType from . import subscription from .config import MQTT_BASE_SCHEMA @@ -103,7 +103,7 @@ class MqttDeviceTracker(MqttEntity, TrackerEntity): _value_template: Callable[[ReceivePayloadType], ReceivePayloadType] @staticmethod - def config_schema() -> vol.Schema: + def config_schema() -> VolSchemaType: """Return the config schema.""" return DISCOVERY_SCHEMA diff --git a/homeassistant/components/mqtt/event.py b/homeassistant/components/mqtt/event.py index 15b70b1b98d..5e801fda54b 100644 --- a/homeassistant/components/mqtt/event.py +++ b/homeassistant/components/mqtt/event.py @@ -19,7 +19,7 @@ from homeassistant.const import CONF_DEVICE_CLASS, CONF_NAME, CONF_VALUE_TEMPLAT from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.typing import ConfigType, VolSchemaType from homeassistant.util.json import JSON_DECODE_EXCEPTIONS, json_loads_object from . import subscription @@ -94,7 +94,7 @@ class MqttEvent(MqttEntity, EventEntity): _template: Callable[[ReceivePayloadType, PayloadSentinel], ReceivePayloadType] @staticmethod - def config_schema() -> vol.Schema: + def config_schema() -> VolSchemaType: """Return the config schema.""" return DISCOVERY_SCHEMA diff --git a/homeassistant/components/mqtt/fan.py b/homeassistant/components/mqtt/fan.py index 1933b5e17b5..dd777bd178e 100644 --- a/homeassistant/components/mqtt/fan.py +++ b/homeassistant/components/mqtt/fan.py @@ -30,7 +30,7 @@ from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.template import Template -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.typing import ConfigType, VolSchemaType from homeassistant.util.percentage import ( percentage_to_ranged_value, ranged_value_to_percentage, @@ -226,7 +226,7 @@ class MqttFan(MqttEntity, FanEntity): _speed_range: tuple[int, int] @staticmethod - def config_schema() -> vol.Schema: + def config_schema() -> VolSchemaType: """Return the config schema.""" return DISCOVERY_SCHEMA diff --git a/homeassistant/components/mqtt/humidifier.py b/homeassistant/components/mqtt/humidifier.py index 8f7eda21240..a4510ee5951 100644 --- a/homeassistant/components/mqtt/humidifier.py +++ b/homeassistant/components/mqtt/humidifier.py @@ -33,7 +33,7 @@ from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.template import Template -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.typing import ConfigType, VolSchemaType from . import subscription from .config import MQTT_RW_SCHEMA @@ -212,7 +212,7 @@ class MqttHumidifier(MqttEntity, HumidifierEntity): _topic: dict[str, Any] @staticmethod - def config_schema() -> vol.Schema: + def config_schema() -> VolSchemaType: """Return the config schema.""" return DISCOVERY_SCHEMA diff --git a/homeassistant/components/mqtt/image.py b/homeassistant/components/mqtt/image.py index d5930a1668a..30fd102764d 100644 --- a/homeassistant/components/mqtt/image.py +++ b/homeassistant/components/mqtt/image.py @@ -20,7 +20,7 @@ from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.httpx_client import get_async_client from homeassistant.helpers.service_info.mqtt import ReceivePayloadType -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, VolSchemaType from homeassistant.util import dt as dt_util from . import subscription @@ -117,7 +117,7 @@ class MqttImage(MqttEntity, ImageEntity): MqttEntity.__init__(self, hass, config, config_entry, discovery_data) @staticmethod - def config_schema() -> vol.Schema: + def config_schema() -> VolSchemaType: """Return the config schema.""" return DISCOVERY_SCHEMA diff --git a/homeassistant/components/mqtt/lawn_mower.py b/homeassistant/components/mqtt/lawn_mower.py index 853ce743f12..a74d278401c 100644 --- a/homeassistant/components/mqtt/lawn_mower.py +++ b/homeassistant/components/mqtt/lawn_mower.py @@ -20,7 +20,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.typing import ConfigType, VolSchemaType from . import subscription from .config import MQTT_BASE_SCHEMA @@ -103,7 +103,7 @@ class MqttLawnMower(MqttEntity, LawnMowerEntity, RestoreEntity): _value_template: Callable[[ReceivePayloadType], ReceivePayloadType] @staticmethod - def config_schema() -> vol.Schema: + def config_schema() -> VolSchemaType: """Return the config schema.""" return DISCOVERY_SCHEMA diff --git a/homeassistant/components/mqtt/light/__init__.py b/homeassistant/components/mqtt/light/__init__.py index ac2d1ff14ee..04619b08e11 100644 --- a/homeassistant/components/mqtt/light/__init__.py +++ b/homeassistant/components/mqtt/light/__init__.py @@ -10,7 +10,7 @@ from homeassistant.components import light from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.typing import ConfigType, VolSchemaType from ..mixins import async_setup_entity_entry_helper from .schema import CONF_SCHEMA, MQTT_LIGHT_SCHEMA_SCHEMA @@ -33,7 +33,7 @@ from .schema_template import ( def validate_mqtt_light_discovery(config_value: dict[str, Any]) -> ConfigType: """Validate MQTT light schema for discovery.""" - schemas = { + schemas: dict[str, VolSchemaType] = { "basic": DISCOVERY_SCHEMA_BASIC, "json": DISCOVERY_SCHEMA_JSON, "template": DISCOVERY_SCHEMA_TEMPLATE, @@ -44,7 +44,7 @@ def validate_mqtt_light_discovery(config_value: dict[str, Any]) -> ConfigType: def validate_mqtt_light_modern(config_value: dict[str, Any]) -> ConfigType: """Validate MQTT light schema for setup from configuration.yaml.""" - schemas = { + schemas: dict[str, VolSchemaType] = { "basic": PLATFORM_SCHEMA_MODERN_BASIC, "json": PLATFORM_SCHEMA_MODERN_JSON, "template": PLATFORM_SCHEMA_MODERN_TEMPLATE, diff --git a/homeassistant/components/mqtt/light/schema_basic.py b/homeassistant/components/mqtt/light/schema_basic.py index 565cf4d7132..b0ffae4e328 100644 --- a/homeassistant/components/mqtt/light/schema_basic.py +++ b/homeassistant/components/mqtt/light/schema_basic.py @@ -39,7 +39,7 @@ from homeassistant.const import ( from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.restore_state import RestoreEntity -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.typing import ConfigType, VolSchemaType import homeassistant.util.color as color_util from .. import subscription @@ -249,7 +249,7 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): _optimistic_xy_color: bool @staticmethod - def config_schema() -> vol.Schema: + def config_schema() -> VolSchemaType: """Return the config schema.""" return DISCOVERY_SCHEMA_BASIC diff --git a/homeassistant/components/mqtt/light/schema_json.py b/homeassistant/components/mqtt/light/schema_json.py index 1d3ad3a6ef0..58fde4a3800 100644 --- a/homeassistant/components/mqtt/light/schema_json.py +++ b/homeassistant/components/mqtt/light/schema_json.py @@ -51,7 +51,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.json import json_dumps from homeassistant.helpers.restore_state import RestoreEntity -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.typing import ConfigType, VolSchemaType import homeassistant.util.color as color_util from homeassistant.util.json import json_loads_object from homeassistant.util.yaml import dump as yaml_dump @@ -267,7 +267,7 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): _deprecated_color_handling: bool = False @staticmethod - def config_schema() -> vol.Schema: + def config_schema() -> VolSchemaType: """Return the config schema.""" return DISCOVERY_SCHEMA_JSON diff --git a/homeassistant/components/mqtt/light/schema_template.py b/homeassistant/components/mqtt/light/schema_template.py index d414f219241..c35b0e6ced9 100644 --- a/homeassistant/components/mqtt/light/schema_template.py +++ b/homeassistant/components/mqtt/light/schema_template.py @@ -31,7 +31,7 @@ from homeassistant.const import ( from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.restore_state import RestoreEntity -from homeassistant.helpers.typing import ConfigType, TemplateVarsType +from homeassistant.helpers.typing import ConfigType, TemplateVarsType, VolSchemaType import homeassistant.util.color as color_util from .. import subscription @@ -120,7 +120,7 @@ class MqttLightTemplate(MqttEntity, LightEntity, RestoreEntity): _topics: dict[str, str | None] @staticmethod - def config_schema() -> vol.Schema: + def config_schema() -> VolSchemaType: """Return the config schema.""" return DISCOVERY_SCHEMA_TEMPLATE diff --git a/homeassistant/components/mqtt/mixins.py b/homeassistant/components/mqtt/mixins.py index 55b76337db0..0800aeb8ee4 100644 --- a/homeassistant/components/mqtt/mixins.py +++ b/homeassistant/components/mqtt/mixins.py @@ -53,6 +53,7 @@ from homeassistant.helpers.typing import ( ConfigType, DiscoveryInfoType, UndefinedType, + VolSchemaType, ) from homeassistant.util.json import json_loads from homeassistant.util.yaml import dump as yaml_dump @@ -247,8 +248,8 @@ def async_setup_entity_entry_helper( entity_class: type[MqttEntity] | None, domain: str, async_add_entities: AddEntitiesCallback, - discovery_schema: vol.Schema, - platform_schema_modern: vol.Schema, + discovery_schema: VolSchemaType, + platform_schema_modern: VolSchemaType, schema_class_mapping: dict[str, type[MqttEntity]] | None = None, ) -> None: """Set up entity creation dynamically through MQTT discovery.""" @@ -1187,7 +1188,7 @@ class MqttEntity( @staticmethod @abstractmethod - def config_schema() -> vol.Schema: + def config_schema() -> VolSchemaType: """Return the config schema.""" def _set_entity_name(self, config: ConfigType) -> None: diff --git a/homeassistant/components/mqtt/models.py b/homeassistant/components/mqtt/models.py index f26ed196663..e5a9a9c44da 100644 --- a/homeassistant/components/mqtt/models.py +++ b/homeassistant/components/mqtt/models.py @@ -11,15 +11,18 @@ from enum import StrEnum import logging from typing import TYPE_CHECKING, Any, TypedDict -import voluptuous as vol - from homeassistant.const import ATTR_ENTITY_ID, ATTR_NAME, Platform from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.exceptions import ServiceValidationError, TemplateError from homeassistant.helpers import template from homeassistant.helpers.entity import Entity from homeassistant.helpers.service_info.mqtt import ReceivePayloadType -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, TemplateVarsType +from homeassistant.helpers.typing import ( + ConfigType, + DiscoveryInfoType, + TemplateVarsType, + VolSchemaType, +) from homeassistant.util.hass_dict import HassKey if TYPE_CHECKING: @@ -418,7 +421,7 @@ class MqttData: platforms_loaded: set[Platform | str] = field(default_factory=set) reload_dispatchers: list[CALLBACK_TYPE] = field(default_factory=list) reload_handlers: dict[str, CALLBACK_TYPE] = field(default_factory=dict) - reload_schema: dict[str, vol.Schema] = field(default_factory=dict) + reload_schema: dict[str, VolSchemaType] = field(default_factory=dict) state_write_requests: EntityTopicState = field(default_factory=EntityTopicState) subscriptions_to_restore: list[Subscription] = field(default_factory=list) tags: dict[str, dict[str, MQTTTagScanner]] = field(default_factory=dict) diff --git a/homeassistant/components/mqtt/number.py b/homeassistant/components/mqtt/number.py index 50a4f398c7d..e8f2cf0cfe4 100644 --- a/homeassistant/components/mqtt/number.py +++ b/homeassistant/components/mqtt/number.py @@ -28,7 +28,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.typing import ConfigType, VolSchemaType from . import subscription from .config import MQTT_RW_SCHEMA @@ -133,7 +133,7 @@ class MqttNumber(MqttEntity, RestoreNumber): _value_template: Callable[[ReceivePayloadType], ReceivePayloadType] @staticmethod - def config_schema() -> vol.Schema: + def config_schema() -> VolSchemaType: """Return the config schema.""" return DISCOVERY_SCHEMA diff --git a/homeassistant/components/mqtt/select.py b/homeassistant/components/mqtt/select.py index ea0a0886082..5cc7a586c71 100644 --- a/homeassistant/components/mqtt/select.py +++ b/homeassistant/components/mqtt/select.py @@ -15,7 +15,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.typing import ConfigType, VolSchemaType from . import subscription from .config import MQTT_RW_SCHEMA @@ -84,7 +84,7 @@ class MqttSelect(MqttEntity, SelectEntity, RestoreEntity): _optimistic: bool = False @staticmethod - def config_schema() -> vol.Schema: + def config_schema() -> VolSchemaType: """Return the config schema.""" return DISCOVERY_SCHEMA diff --git a/homeassistant/components/mqtt/sensor.py b/homeassistant/components/mqtt/sensor.py index 043bc9a5c0e..4a41f486831 100644 --- a/homeassistant/components/mqtt/sensor.py +++ b/homeassistant/components/mqtt/sensor.py @@ -33,7 +33,7 @@ from homeassistant.core import CALLBACK_TYPE, HomeAssistant, State, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_call_later -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.typing import ConfigType, VolSchemaType from homeassistant.util import dt as dt_util from . import subscription @@ -185,7 +185,7 @@ class MqttSensor(MqttEntity, RestoreSensor): await MqttEntity.async_will_remove_from_hass(self) @staticmethod - def config_schema() -> vol.Schema: + def config_schema() -> VolSchemaType: """Return the config schema.""" return DISCOVERY_SCHEMA diff --git a/homeassistant/components/mqtt/siren.py b/homeassistant/components/mqtt/siren.py index 49645f7b1b4..9f1466dd95d 100644 --- a/homeassistant/components/mqtt/siren.py +++ b/homeassistant/components/mqtt/siren.py @@ -32,7 +32,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.json import json_dumps from homeassistant.helpers.template import Template -from homeassistant.helpers.typing import ConfigType, TemplateVarsType +from homeassistant.helpers.typing import ConfigType, TemplateVarsType, VolSchemaType from homeassistant.util.json import JSON_DECODE_EXCEPTIONS, json_loads_object from . import subscription @@ -142,7 +142,7 @@ class MqttSiren(MqttEntity, SirenEntity): _optimistic: bool @staticmethod - def config_schema() -> vol.Schema: + def config_schema() -> VolSchemaType: """Return the config schema.""" return DISCOVERY_SCHEMA diff --git a/homeassistant/components/mqtt/text.py b/homeassistant/components/mqtt/text.py index 73adaa2cb0c..0b122dec7b5 100644 --- a/homeassistant/components/mqtt/text.py +++ b/homeassistant/components/mqtt/text.py @@ -22,7 +22,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.typing import ConfigType, VolSchemaType from . import subscription from .config import MQTT_RW_SCHEMA @@ -121,7 +121,7 @@ class MqttTextEntity(MqttEntity, TextEntity): _value_template: Callable[[ReceivePayloadType], ReceivePayloadType] @staticmethod - def config_schema() -> vol.Schema: + def config_schema() -> VolSchemaType: """Return the config schema.""" return DISCOVERY_SCHEMA diff --git a/homeassistant/components/mqtt/update.py b/homeassistant/components/mqtt/update.py index eecd7b967de..4b87e0ef7da 100644 --- a/homeassistant/components/mqtt/update.py +++ b/homeassistant/components/mqtt/update.py @@ -19,7 +19,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.typing import ConfigType, VolSchemaType from homeassistant.util.json import JSON_DECODE_EXCEPTIONS, json_loads from . import subscription @@ -107,7 +107,7 @@ class MqttUpdate(MqttEntity, UpdateEntity, RestoreEntity): return super().entity_picture @staticmethod - def config_schema() -> vol.Schema: + def config_schema() -> VolSchemaType: """Return the config schema.""" return DISCOVERY_SCHEMA diff --git a/homeassistant/components/mqtt/vacuum.py b/homeassistant/components/mqtt/vacuum.py index eac3556a28b..8aa42270016 100644 --- a/homeassistant/components/mqtt/vacuum.py +++ b/homeassistant/components/mqtt/vacuum.py @@ -35,7 +35,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.json import json_dumps -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, VolSchemaType from homeassistant.util.json import json_loads_object from . import subscription @@ -281,7 +281,7 @@ class MqttStateVacuum(MqttEntity, StateVacuumEntity): MqttEntity.__init__(self, hass, config, config_entry, discovery_data) @staticmethod - def config_schema() -> vol.Schema: + def config_schema() -> VolSchemaType: """Return the config schema.""" return DISCOVERY_SCHEMA diff --git a/homeassistant/components/mqtt/valve.py b/homeassistant/components/mqtt/valve.py index f3c76462269..02127dfc19c 100644 --- a/homeassistant/components/mqtt/valve.py +++ b/homeassistant/components/mqtt/valve.py @@ -28,7 +28,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.typing import ConfigType, VolSchemaType from homeassistant.util.json import JSON_DECODE_EXCEPTIONS, json_loads from homeassistant.util.percentage import ( percentage_to_ranged_value, @@ -163,7 +163,7 @@ class MqttValve(MqttEntity, ValveEntity): _tilt_optimistic: bool @staticmethod - def config_schema() -> vol.Schema: + def config_schema() -> VolSchemaType: """Return the config schema.""" return DISCOVERY_SCHEMA diff --git a/homeassistant/components/mqtt/water_heater.py b/homeassistant/components/mqtt/water_heater.py index ac3c8aacc92..13b0478210f 100644 --- a/homeassistant/components/mqtt/water_heater.py +++ b/homeassistant/components/mqtt/water_heater.py @@ -38,7 +38,7 @@ from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.template import Template -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.typing import ConfigType, VolSchemaType from homeassistant.util.unit_conversion import TemperatureConverter from .climate import MqttTemperatureControlEntity @@ -188,7 +188,7 @@ class MqttWaterHeater(MqttTemperatureControlEntity, WaterHeaterEntity): _attr_target_temperature_high: float | None = None @staticmethod - def config_schema() -> vol.Schema: + def config_schema() -> VolSchemaType: """Return the config schema.""" return DISCOVERY_SCHEMA From fccb7ea1f9596733f9869b0f69c7378d463fc5bd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 25 Jun 2024 13:10:09 +0200 Subject: [PATCH 1160/1445] Migrate ESPHome to use entry.runtime_data (#120402) --- homeassistant/components/esphome/__init__.py | 11 ++++---- .../components/esphome/alarm_control_panel.py | 6 ++-- .../components/esphome/binary_sensor.py | 9 +++--- homeassistant/components/esphome/button.py | 6 ++-- homeassistant/components/esphome/camera.py | 6 ++-- homeassistant/components/esphome/climate.py | 6 ++-- homeassistant/components/esphome/cover.py | 6 ++-- homeassistant/components/esphome/dashboard.py | 2 +- homeassistant/components/esphome/date.py | 4 +-- homeassistant/components/esphome/datetime.py | 4 +-- .../components/esphome/diagnostics.py | 8 +++--- .../components/esphome/domain_data.py | 28 ++++++++----------- homeassistant/components/esphome/entity.py | 9 ++---- .../components/esphome/entry_data.py | 9 ++++-- homeassistant/components/esphome/event.py | 6 ++-- homeassistant/components/esphome/fan.py | 6 ++-- homeassistant/components/esphome/light.py | 6 ++-- homeassistant/components/esphome/lock.py | 6 ++-- homeassistant/components/esphome/manager.py | 14 +++++----- homeassistant/components/esphome/select.py | 8 ++---- homeassistant/components/esphome/switch.py | 6 ++-- homeassistant/components/esphome/valve.py | 6 ++-- 22 files changed, 93 insertions(+), 79 deletions(-) diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index 3de5d48391f..3af95576c18 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -5,7 +5,6 @@ from __future__ import annotations from aioesphomeapi import APIClient from homeassistant.components import zeroconf -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, @@ -21,7 +20,7 @@ from .dashboard import async_setup as async_setup_dashboard from .domain_data import DomainData # Import config flow so that it's added to the registry -from .entry_data import RuntimeEntryData +from .entry_data import ESPHomeConfigEntry, RuntimeEntryData from .manager import ESPHomeManager, cleanup_instance CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN) @@ -33,7 +32,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: return True -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: ESPHomeConfigEntry) -> bool: """Set up the esphome component.""" host = entry.data[CONF_HOST] port = entry.data[CONF_PORT] @@ -59,7 +58,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: store=domain_data.get_or_create_store(hass, entry), original_options=dict(entry.options), ) - domain_data.set_entry_data(entry, entry_data) + entry.runtime_data = entry_data manager = ESPHomeManager( hass, entry, host, password, cli, zeroconf_instance, domain_data, entry_data @@ -69,7 +68,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: ESPHomeConfigEntry) -> bool: """Unload an esphome config entry.""" entry_data = await cleanup_instance(hass, entry) return await hass.config_entries.async_unload_platforms( @@ -77,6 +76,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) -async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: +async def async_remove_entry(hass: HomeAssistant, entry: ESPHomeConfigEntry) -> None: """Remove an esphome config entry.""" await DomainData.get(hass).get_or_create_store(hass, entry).async_remove() diff --git a/homeassistant/components/esphome/alarm_control_panel.py b/homeassistant/components/esphome/alarm_control_panel.py index 54bce4e6015..17079fe8c6a 100644 --- a/homeassistant/components/esphome/alarm_control_panel.py +++ b/homeassistant/components/esphome/alarm_control_panel.py @@ -16,7 +16,6 @@ from homeassistant.components.alarm_control_panel import ( AlarmControlPanelEntityFeature, CodeFormat, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_CUSTOM_BYPASS, @@ -38,6 +37,7 @@ from .entity import ( esphome_state_property, platform_async_setup_entry, ) +from .entry_data import ESPHomeConfigEntry from .enum_mapper import EsphomeEnumMapper _ESPHOME_ACP_STATE_TO_HASS_STATE: EsphomeEnumMapper[AlarmControlPanelState, str] = ( @@ -70,7 +70,9 @@ class EspHomeACPFeatures(APIIntEnum): async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, + entry: ESPHomeConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up ESPHome switches based on a config entry.""" await platform_async_setup_entry( diff --git a/homeassistant/components/esphome/binary_sensor.py b/homeassistant/components/esphome/binary_sensor.py index 05ddfc2c43f..32d96785601 100644 --- a/homeassistant/components/esphome/binary_sensor.py +++ b/homeassistant/components/esphome/binary_sensor.py @@ -9,17 +9,18 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntity, BinarySensorEntityDescription, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util.enum import try_parse_enum -from .domain_data import DomainData from .entity import EsphomeAssistEntity, EsphomeEntity, platform_async_setup_entry +from .entry_data import ESPHomeConfigEntry async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, + entry: ESPHomeConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up ESPHome binary sensors based on a config entry.""" await platform_async_setup_entry( @@ -31,7 +32,7 @@ async def async_setup_entry( state_type=BinarySensorState, ) - entry_data = DomainData.get(hass).get_entry_data(entry) + entry_data = entry.runtime_data assert entry_data.device_info is not None if entry_data.device_info.voice_assistant_feature_flags_compat( entry_data.api_version diff --git a/homeassistant/components/esphome/button.py b/homeassistant/components/esphome/button.py index a825bb9b9b4..8883c4b6bea 100644 --- a/homeassistant/components/esphome/button.py +++ b/homeassistant/components/esphome/button.py @@ -5,7 +5,6 @@ from __future__ import annotations from aioesphomeapi import ButtonInfo, EntityInfo, EntityState from homeassistant.components.button import ButtonDeviceClass, ButtonEntity -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util.enum import try_parse_enum @@ -15,10 +14,13 @@ from .entity import ( convert_api_error_ha_error, platform_async_setup_entry, ) +from .entry_data import ESPHomeConfigEntry async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, + entry: ESPHomeConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up ESPHome buttons based on a config entry.""" await platform_async_setup_entry( diff --git a/homeassistant/components/esphome/camera.py b/homeassistant/components/esphome/camera.py index 83cf8d03e78..abe7f6809e6 100644 --- a/homeassistant/components/esphome/camera.py +++ b/homeassistant/components/esphome/camera.py @@ -12,15 +12,17 @@ from aiohttp import web from homeassistant.components import camera from homeassistant.components.camera import Camera -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from .entity import EsphomeEntity, platform_async_setup_entry +from .entry_data import ESPHomeConfigEntry async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, + entry: ESPHomeConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up esphome cameras based on a config entry.""" await platform_async_setup_entry( diff --git a/homeassistant/components/esphome/climate.py b/homeassistant/components/esphome/climate.py index 4225f60af0c..6c82207ddc9 100644 --- a/homeassistant/components/esphome/climate.py +++ b/homeassistant/components/esphome/climate.py @@ -45,7 +45,6 @@ from homeassistant.components.climate import ( HVACAction, HVACMode, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_TEMPERATURE, PRECISION_HALVES, @@ -62,13 +61,16 @@ from .entity import ( esphome_state_property, platform_async_setup_entry, ) +from .entry_data import ESPHomeConfigEntry from .enum_mapper import EsphomeEnumMapper FAN_QUIET = "quiet" async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, + entry: ESPHomeConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up ESPHome climate devices based on a config entry.""" await platform_async_setup_entry( diff --git a/homeassistant/components/esphome/cover.py b/homeassistant/components/esphome/cover.py index 0b845c255a3..4597b4f3566 100644 --- a/homeassistant/components/esphome/cover.py +++ b/homeassistant/components/esphome/cover.py @@ -13,7 +13,6 @@ from homeassistant.components.cover import ( CoverEntity, CoverEntityFeature, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util.enum import try_parse_enum @@ -24,10 +23,13 @@ from .entity import ( esphome_state_property, platform_async_setup_entry, ) +from .entry_data import ESPHomeConfigEntry async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, + entry: ESPHomeConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up ESPHome covers based on a config entry.""" await platform_async_setup_entry( diff --git a/homeassistant/components/esphome/dashboard.py b/homeassistant/components/esphome/dashboard.py index b2d0487df9c..b0a37aefd0d 100644 --- a/homeassistant/components/esphome/dashboard.py +++ b/homeassistant/components/esphome/dashboard.py @@ -106,7 +106,7 @@ class ESPHomeDashboardManager: reloads = [ hass.config_entries.async_reload(entry.entry_id) for entry in hass.config_entries.async_entries(DOMAIN) - if entry.state == ConfigEntryState.LOADED + if entry.state is ConfigEntryState.LOADED ] # Re-auth flows will check the dashboard for encryption key when the form is requested # but we only trigger reauth if the dashboard is available. diff --git a/homeassistant/components/esphome/date.py b/homeassistant/components/esphome/date.py index 9998eea1a5d..eb26ec918d0 100644 --- a/homeassistant/components/esphome/date.py +++ b/homeassistant/components/esphome/date.py @@ -7,16 +7,16 @@ from datetime import date from aioesphomeapi import DateInfo, DateState from homeassistant.components.date import DateEntity -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from .entity import EsphomeEntity, esphome_state_property, platform_async_setup_entry +from .entry_data import ESPHomeConfigEntry async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: ESPHomeConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up esphome dates based on a config entry.""" diff --git a/homeassistant/components/esphome/datetime.py b/homeassistant/components/esphome/datetime.py index 15509a46158..5d578ae4928 100644 --- a/homeassistant/components/esphome/datetime.py +++ b/homeassistant/components/esphome/datetime.py @@ -7,17 +7,17 @@ from datetime import datetime from aioesphomeapi import DateTimeInfo, DateTimeState from homeassistant.components.datetime import DateTimeEntity -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback import homeassistant.util.dt as dt_util from .entity import EsphomeEntity, esphome_state_property, platform_async_setup_entry +from .entry_data import ESPHomeConfigEntry async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: ESPHomeConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up esphome datetimes based on a config entry.""" diff --git a/homeassistant/components/esphome/diagnostics.py b/homeassistant/components/esphome/diagnostics.py index 44241f5950c..58c9a8fe666 100644 --- a/homeassistant/components/esphome/diagnostics.py +++ b/homeassistant/components/esphome/diagnostics.py @@ -6,12 +6,12 @@ from typing import Any from homeassistant.components.bluetooth import async_scanner_by_source from homeassistant.components.diagnostics import async_redact_data -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD from homeassistant.core import HomeAssistant -from . import CONF_NOISE_PSK, DomainData +from . import CONF_NOISE_PSK from .dashboard import async_get_dashboard +from .entry_data import ESPHomeConfigEntry CONF_MAC_ADDRESS = "mac_address" @@ -19,14 +19,14 @@ REDACT_KEYS = {CONF_NOISE_PSK, CONF_PASSWORD, CONF_MAC_ADDRESS} async def async_get_config_entry_diagnostics( - hass: HomeAssistant, config_entry: ConfigEntry + hass: HomeAssistant, config_entry: ESPHomeConfigEntry ) -> dict[str, Any]: """Return diagnostics for a config entry.""" diag: dict[str, Any] = {} diag["config"] = config_entry.as_dict() - entry_data = DomainData.get(hass).get_entry_data(config_entry) + entry_data = config_entry.runtime_data if (storage_data := await entry_data.store.async_load()) is not None: diag["storage_data"] = storage_data diff --git a/homeassistant/components/esphome/domain_data.py b/homeassistant/components/esphome/domain_data.py index 9ac8fe97614..e9057ddfeaa 100644 --- a/homeassistant/components/esphome/domain_data.py +++ b/homeassistant/components/esphome/domain_data.py @@ -3,16 +3,16 @@ from __future__ import annotations from dataclasses import dataclass, field -from typing import Self, cast +from functools import cache +from typing import Self from bleak_esphome.backend.cache import ESPHomeBluetoothCache -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.json import JSONEncoder from .const import DOMAIN -from .entry_data import ESPHomeStorage, RuntimeEntryData +from .entry_data import ESPHomeConfigEntry, ESPHomeStorage, RuntimeEntryData STORAGE_VERSION = 1 @@ -21,30 +21,26 @@ STORAGE_VERSION = 1 class DomainData: """Define a class that stores global esphome data in hass.data[DOMAIN].""" - _entry_datas: dict[str, RuntimeEntryData] = field(default_factory=dict) _stores: dict[str, ESPHomeStorage] = field(default_factory=dict) bluetooth_cache: ESPHomeBluetoothCache = field( default_factory=ESPHomeBluetoothCache ) - def get_entry_data(self, entry: ConfigEntry) -> RuntimeEntryData: + def get_entry_data(self, entry: ESPHomeConfigEntry) -> RuntimeEntryData: """Return the runtime entry data associated with this config entry. Raises KeyError if the entry isn't loaded yet. """ - return self._entry_datas[entry.entry_id] + return entry.runtime_data - def set_entry_data(self, entry: ConfigEntry, entry_data: RuntimeEntryData) -> None: + def set_entry_data( + self, entry: ESPHomeConfigEntry, entry_data: RuntimeEntryData + ) -> None: """Set the runtime entry data associated with this config entry.""" - assert entry.entry_id not in self._entry_datas, "Entry data already set!" - self._entry_datas[entry.entry_id] = entry_data - - def pop_entry_data(self, entry: ConfigEntry) -> RuntimeEntryData: - """Pop the runtime entry data instance associated with this config entry.""" - return self._entry_datas.pop(entry.entry_id) + entry.runtime_data = entry_data def get_or_create_store( - self, hass: HomeAssistant, entry: ConfigEntry + self, hass: HomeAssistant, entry: ESPHomeConfigEntry ) -> ESPHomeStorage: """Get or create a Store instance for the given config entry.""" return self._stores.setdefault( @@ -55,10 +51,8 @@ class DomainData: ) @classmethod + @cache def get(cls, hass: HomeAssistant) -> Self: """Get the global DomainData instance stored in hass.data.""" - # Don't use setdefault - this is a hot code path - if DOMAIN in hass.data: - return cast(Self, hass.data[DOMAIN]) ret = hass.data[DOMAIN] = cls() return ret diff --git a/homeassistant/components/esphome/entity.py b/homeassistant/components/esphome/entity.py index 374c22eef72..8241d0f4563 100644 --- a/homeassistant/components/esphome/entity.py +++ b/homeassistant/components/esphome/entity.py @@ -16,7 +16,6 @@ from aioesphomeapi import ( ) import voluptuous as vol -from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError @@ -27,10 +26,8 @@ from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .domain_data import DomainData - # Import config flow so that it's added to the registry -from .entry_data import RuntimeEntryData +from .entry_data import ESPHomeConfigEntry, RuntimeEntryData from .enum_mapper import EsphomeEnumMapper _R = TypeVar("_R") @@ -85,7 +82,7 @@ def async_static_info_updated( async def platform_async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: ESPHomeConfigEntry, async_add_entities: AddEntitiesCallback, *, info_type: type[_InfoT], @@ -97,7 +94,7 @@ async def platform_async_setup_entry( This method is in charge of receiving, distributing and storing info and state updates. """ - entry_data: RuntimeEntryData = DomainData.get(hass).get_entry_data(entry) + entry_data = entry.runtime_data entry_data.info[info_type] = {} entry_data.state.setdefault(state_type, {}) platform = entity_platform.async_get_current_platform() diff --git a/homeassistant/components/esphome/entry_data.py b/homeassistant/components/esphome/entry_data.py index 7a491d1863b..ff6f048eba1 100644 --- a/homeassistant/components/esphome/entry_data.py +++ b/homeassistant/components/esphome/entry_data.py @@ -55,6 +55,9 @@ from homeassistant.helpers.storage import Store from .const import DOMAIN from .dashboard import async_get_dashboard +type ESPHomeConfigEntry = ConfigEntry[RuntimeEntryData] + + INFO_TO_COMPONENT_TYPE: Final = {v: k for k, v in COMPONENT_TYPE_TO_INFO.items()} _SENTINEL = object() @@ -248,7 +251,7 @@ class RuntimeEntryData: async def _ensure_platforms_loaded( self, hass: HomeAssistant, - entry: ConfigEntry, + entry: ESPHomeConfigEntry, platforms: set[Platform], ) -> None: async with self.platform_load_lock: @@ -259,7 +262,7 @@ class RuntimeEntryData: async def async_update_static_infos( self, hass: HomeAssistant, - entry: ConfigEntry, + entry: ESPHomeConfigEntry, infos: list[EntityInfo], mac: str, ) -> None: @@ -452,7 +455,7 @@ class RuntimeEntryData: await self.store.async_save(self._pending_storage()) async def async_update_listener( - self, hass: HomeAssistant, entry: ConfigEntry + self, hass: HomeAssistant, entry: ESPHomeConfigEntry ) -> None: """Handle options update.""" if self.original_options == entry.options: diff --git a/homeassistant/components/esphome/event.py b/homeassistant/components/esphome/event.py index 3c7331beba0..9435597e25b 100644 --- a/homeassistant/components/esphome/event.py +++ b/homeassistant/components/esphome/event.py @@ -5,16 +5,18 @@ from __future__ import annotations from aioesphomeapi import EntityInfo, Event, EventInfo from homeassistant.components.event import EventDeviceClass, EventEntity -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util.enum import try_parse_enum from .entity import EsphomeEntity, platform_async_setup_entry +from .entry_data import ESPHomeConfigEntry async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, + entry: ESPHomeConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up ESPHome event based on a config entry.""" await platform_async_setup_entry( diff --git a/homeassistant/components/esphome/fan.py b/homeassistant/components/esphome/fan.py index 082de3f7b7d..35a19348281 100644 --- a/homeassistant/components/esphome/fan.py +++ b/homeassistant/components/esphome/fan.py @@ -13,7 +13,6 @@ from homeassistant.components.fan import ( FanEntity, FanEntityFeature, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util.percentage import ( @@ -29,13 +28,16 @@ from .entity import ( esphome_state_property, platform_async_setup_entry, ) +from .entry_data import ESPHomeConfigEntry from .enum_mapper import EsphomeEnumMapper ORDERED_NAMED_FAN_SPEEDS = [FanSpeed.LOW, FanSpeed.MEDIUM, FanSpeed.HIGH] async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, + entry: ESPHomeConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up ESPHome fans based on a config entry.""" await platform_async_setup_entry( diff --git a/homeassistant/components/esphome/light.py b/homeassistant/components/esphome/light.py index bbb4021d58f..c5f83805cce 100644 --- a/homeassistant/components/esphome/light.py +++ b/homeassistant/components/esphome/light.py @@ -29,7 +29,6 @@ from homeassistant.components.light import ( LightEntity, LightEntityFeature, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -39,12 +38,15 @@ from .entity import ( esphome_state_property, platform_async_setup_entry, ) +from .entry_data import ESPHomeConfigEntry FLASH_LENGTHS = {FLASH_SHORT: 2, FLASH_LONG: 10} async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, + entry: ESPHomeConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up ESPHome lights based on a config entry.""" await platform_async_setup_entry( diff --git a/homeassistant/components/esphome/lock.py b/homeassistant/components/esphome/lock.py index 98efdece92e..c00f81839cb 100644 --- a/homeassistant/components/esphome/lock.py +++ b/homeassistant/components/esphome/lock.py @@ -7,7 +7,6 @@ from typing import Any from aioesphomeapi import EntityInfo, LockCommand, LockEntityState, LockInfo, LockState from homeassistant.components.lock import LockEntity, LockEntityFeature -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_CODE from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -18,10 +17,13 @@ from .entity import ( esphome_state_property, platform_async_setup_entry, ) +from .entry_data import ESPHomeConfigEntry async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, + entry: ESPHomeConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up ESPHome switches based on a config entry.""" await platform_async_setup_entry( diff --git a/homeassistant/components/esphome/manager.py b/homeassistant/components/esphome/manager.py index f191c36c574..870bd704ee4 100644 --- a/homeassistant/components/esphome/manager.py +++ b/homeassistant/components/esphome/manager.py @@ -28,7 +28,6 @@ import voluptuous as vol from homeassistant.components import tag, zeroconf from homeassistant.components.intent import async_register_timer_handler -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_DEVICE_ID, CONF_MODE, @@ -73,7 +72,7 @@ from .dashboard import async_get_dashboard from .domain_data import DomainData # Import config flow so that it's added to the registry -from .entry_data import RuntimeEntryData +from .entry_data import ESPHomeConfigEntry, RuntimeEntryData from .voice_assistant import ( VoiceAssistantAPIPipeline, VoiceAssistantPipeline, @@ -159,7 +158,7 @@ class ESPHomeManager: def __init__( self, hass: HomeAssistant, - entry: ConfigEntry, + entry: ESPHomeConfigEntry, host: str, password: str | None, cli: APIClient, @@ -639,7 +638,7 @@ class ESPHomeManager: @callback def _async_setup_device_registry( - hass: HomeAssistant, entry: ConfigEntry, entry_data: RuntimeEntryData + hass: HomeAssistant, entry: ESPHomeConfigEntry, entry_data: RuntimeEntryData ) -> str: """Set up device registry feature for a particular config entry.""" device_info = entry_data.device_info @@ -839,10 +838,11 @@ def _setup_services( _async_register_service(hass, entry_data, device_info, service) -async def cleanup_instance(hass: HomeAssistant, entry: ConfigEntry) -> RuntimeEntryData: +async def cleanup_instance( + hass: HomeAssistant, entry: ESPHomeConfigEntry +) -> RuntimeEntryData: """Cleanup the esphome client if it exists.""" - domain_data = DomainData.get(hass) - data = domain_data.pop_entry_data(entry) + data = entry.runtime_data data.async_on_disconnect() for cleanup_callback in data.cleanup_callbacks: cleanup_callback() diff --git a/homeassistant/components/esphome/select.py b/homeassistant/components/esphome/select.py index 612ffc4bcc6..ed37a9a6ab8 100644 --- a/homeassistant/components/esphome/select.py +++ b/homeassistant/components/esphome/select.py @@ -9,12 +9,10 @@ from homeassistant.components.assist_pipeline.select import ( VadSensitivitySelect, ) from homeassistant.components.select import SelectEntity -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN -from .domain_data import DomainData from .entity import ( EsphomeAssistEntity, EsphomeEntity, @@ -22,12 +20,12 @@ from .entity import ( esphome_state_property, platform_async_setup_entry, ) -from .entry_data import RuntimeEntryData +from .entry_data import ESPHomeConfigEntry, RuntimeEntryData async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: ESPHomeConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up esphome selects based on a config entry.""" @@ -40,7 +38,7 @@ async def async_setup_entry( state_type=SelectState, ) - entry_data = DomainData.get(hass).get_entry_data(entry) + entry_data = entry.runtime_data assert entry_data.device_info is not None if entry_data.device_info.voice_assistant_feature_flags_compat( entry_data.api_version diff --git a/homeassistant/components/esphome/switch.py b/homeassistant/components/esphome/switch.py index 6fa73058bd2..b2245c78f52 100644 --- a/homeassistant/components/esphome/switch.py +++ b/homeassistant/components/esphome/switch.py @@ -7,7 +7,6 @@ from typing import Any from aioesphomeapi import EntityInfo, SwitchInfo, SwitchState from homeassistant.components.switch import SwitchDeviceClass, SwitchEntity -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util.enum import try_parse_enum @@ -18,10 +17,13 @@ from .entity import ( esphome_state_property, platform_async_setup_entry, ) +from .entry_data import ESPHomeConfigEntry async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, + entry: ESPHomeConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up ESPHome switches based on a config entry.""" await platform_async_setup_entry( diff --git a/homeassistant/components/esphome/valve.py b/homeassistant/components/esphome/valve.py index 5798d38803f..a82d65366c6 100644 --- a/homeassistant/components/esphome/valve.py +++ b/homeassistant/components/esphome/valve.py @@ -11,7 +11,6 @@ from homeassistant.components.valve import ( ValveEntity, ValveEntityFeature, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util.enum import try_parse_enum @@ -22,10 +21,13 @@ from .entity import ( esphome_state_property, platform_async_setup_entry, ) +from .entry_data import ESPHomeConfigEntry async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, + entry: ESPHomeConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up ESPHome valves based on a config entry.""" await platform_async_setup_entry( From cbb3d48bd9417fe855820b33ca61ea3d9350a4dc Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 25 Jun 2024 13:11:27 +0200 Subject: [PATCH 1161/1445] Improve type hints in dsmr tests (#120393) --- tests/components/dsmr/conftest.py | 15 +++-- tests/components/dsmr/test_config_flow.py | 37 ++++++++---- tests/components/dsmr/test_mbus_migration.py | 5 +- tests/components/dsmr/test_sensor.py | 62 +++++++++++++++----- 4 files changed, 85 insertions(+), 34 deletions(-) diff --git a/tests/components/dsmr/conftest.py b/tests/components/dsmr/conftest.py index 05881d9c877..2257b8414a6 100644 --- a/tests/components/dsmr/conftest.py +++ b/tests/components/dsmr/conftest.py @@ -15,10 +15,11 @@ from dsmr_parser.obis_references import ( ) from dsmr_parser.objects import CosemObject import pytest +from typing_extensions import Generator @pytest.fixture -async def dsmr_connection_fixture(hass): +def dsmr_connection_fixture() -> Generator[tuple[MagicMock, MagicMock, MagicMock]]: """Fixture that mocks serial connection.""" transport = MagicMock(spec=asyncio.Transport) @@ -44,7 +45,9 @@ async def dsmr_connection_fixture(hass): @pytest.fixture -async def rfxtrx_dsmr_connection_fixture(hass): +def rfxtrx_dsmr_connection_fixture() -> ( + Generator[tuple[MagicMock, MagicMock, MagicMock]] +): """Fixture that mocks RFXtrx connection.""" transport = MagicMock(spec=asyncio.Transport) @@ -70,7 +73,9 @@ async def rfxtrx_dsmr_connection_fixture(hass): @pytest.fixture -async def dsmr_connection_send_validate_fixture(hass): +def dsmr_connection_send_validate_fixture() -> ( + Generator[tuple[MagicMock, MagicMock, MagicMock]] +): """Fixture that mocks serial connection.""" transport = MagicMock(spec=asyncio.Transport) @@ -151,7 +156,9 @@ async def dsmr_connection_send_validate_fixture(hass): @pytest.fixture -async def rfxtrx_dsmr_connection_send_validate_fixture(hass): +def rfxtrx_dsmr_connection_send_validate_fixture() -> ( + Generator[tuple[MagicMock, MagicMock, MagicMock]] +): """Fixture that mocks serial connection.""" transport = MagicMock(spec=asyncio.Transport) diff --git a/tests/components/dsmr/test_config_flow.py b/tests/components/dsmr/test_config_flow.py index 711b29f4ae0..3b4dc533993 100644 --- a/tests/components/dsmr/test_config_flow.py +++ b/tests/components/dsmr/test_config_flow.py @@ -32,7 +32,8 @@ def com_port(): async def test_setup_network( - hass: HomeAssistant, dsmr_connection_send_validate_fixture + hass: HomeAssistant, + dsmr_connection_send_validate_fixture: tuple[MagicMock, MagicMock, MagicMock], ) -> None: """Test we can setup network.""" result = await hass.config_entries.flow.async_init( @@ -77,8 +78,10 @@ async def test_setup_network( async def test_setup_network_rfxtrx( hass: HomeAssistant, - dsmr_connection_send_validate_fixture, - rfxtrx_dsmr_connection_send_validate_fixture, + dsmr_connection_send_validate_fixture: tuple[MagicMock, MagicMock, MagicMock], + rfxtrx_dsmr_connection_send_validate_fixture: tuple[ + MagicMock, MagicMock, MagicMock + ], ) -> None: """Test we can setup network.""" (connection_factory, transport, protocol) = dsmr_connection_send_validate_fixture @@ -185,7 +188,7 @@ async def test_setup_network_rfxtrx( async def test_setup_serial( com_mock, hass: HomeAssistant, - dsmr_connection_send_validate_fixture, + dsmr_connection_send_validate_fixture: tuple[MagicMock, MagicMock, MagicMock], version: str, entry_data: dict[str, Any], ) -> None: @@ -225,8 +228,10 @@ async def test_setup_serial( async def test_setup_serial_rfxtrx( com_mock, hass: HomeAssistant, - dsmr_connection_send_validate_fixture, - rfxtrx_dsmr_connection_send_validate_fixture, + dsmr_connection_send_validate_fixture: tuple[MagicMock, MagicMock, MagicMock], + rfxtrx_dsmr_connection_send_validate_fixture: tuple[ + MagicMock, MagicMock, MagicMock + ], ) -> None: """Test we can setup serial.""" (connection_factory, transport, protocol) = dsmr_connection_send_validate_fixture @@ -273,7 +278,9 @@ async def test_setup_serial_rfxtrx( @patch("serial.tools.list_ports.comports", return_value=[com_port()]) async def test_setup_serial_manual( - com_mock, hass: HomeAssistant, dsmr_connection_send_validate_fixture + com_mock, + hass: HomeAssistant, + dsmr_connection_send_validate_fixture: tuple[MagicMock, MagicMock, MagicMock], ) -> None: """Test we can setup serial with manual entry.""" result = await hass.config_entries.flow.async_init( @@ -321,7 +328,9 @@ async def test_setup_serial_manual( @patch("serial.tools.list_ports.comports", return_value=[com_port()]) async def test_setup_serial_fail( - com_mock, hass: HomeAssistant, dsmr_connection_send_validate_fixture + com_mock, + hass: HomeAssistant, + dsmr_connection_send_validate_fixture: tuple[MagicMock, MagicMock, MagicMock], ) -> None: """Test failed serial connection.""" (connection_factory, transport, protocol) = dsmr_connection_send_validate_fixture @@ -369,8 +378,10 @@ async def test_setup_serial_fail( async def test_setup_serial_timeout( com_mock, hass: HomeAssistant, - dsmr_connection_send_validate_fixture, - rfxtrx_dsmr_connection_send_validate_fixture, + dsmr_connection_send_validate_fixture: tuple[MagicMock, MagicMock, MagicMock], + rfxtrx_dsmr_connection_send_validate_fixture: tuple[ + MagicMock, MagicMock, MagicMock + ], ) -> None: """Test failed serial connection.""" (connection_factory, transport, protocol) = dsmr_connection_send_validate_fixture @@ -425,8 +436,10 @@ async def test_setup_serial_timeout( async def test_setup_serial_wrong_telegram( com_mock, hass: HomeAssistant, - dsmr_connection_send_validate_fixture, - rfxtrx_dsmr_connection_send_validate_fixture, + dsmr_connection_send_validate_fixture: tuple[MagicMock, MagicMock, MagicMock], + rfxtrx_dsmr_connection_send_validate_fixture: tuple[ + MagicMock, MagicMock, MagicMock + ], ) -> None: """Test failed telegram data.""" (connection_factory, transport, protocol) = dsmr_connection_send_validate_fixture diff --git a/tests/components/dsmr/test_mbus_migration.py b/tests/components/dsmr/test_mbus_migration.py index 284a0001b89..18f5e850ecd 100644 --- a/tests/components/dsmr/test_mbus_migration.py +++ b/tests/components/dsmr/test_mbus_migration.py @@ -2,6 +2,7 @@ import datetime from decimal import Decimal +from unittest.mock import MagicMock from dsmr_parser.obis_references import ( BELGIUM_MBUS1_DEVICE_TYPE, @@ -22,7 +23,7 @@ async def test_migrate_gas_to_mbus( hass: HomeAssistant, entity_registry: er.EntityRegistry, device_registry: dr.DeviceRegistry, - dsmr_connection_fixture, + dsmr_connection_fixture: tuple[MagicMock, MagicMock, MagicMock], ) -> None: """Test migration of unique_id.""" (connection_factory, transport, protocol) = dsmr_connection_fixture @@ -113,7 +114,7 @@ async def test_migrate_gas_to_mbus_exists( hass: HomeAssistant, entity_registry: er.EntityRegistry, device_registry: dr.DeviceRegistry, - dsmr_connection_fixture, + dsmr_connection_fixture: tuple[MagicMock, MagicMock, MagicMock], ) -> None: """Test migration of unique_id.""" (connection_factory, transport, protocol) = dsmr_connection_fixture diff --git a/tests/components/dsmr/test_sensor.py b/tests/components/dsmr/test_sensor.py index e014fdb68f2..435594d4eef 100644 --- a/tests/components/dsmr/test_sensor.py +++ b/tests/components/dsmr/test_sensor.py @@ -63,7 +63,9 @@ from tests.common import MockConfigEntry, patch async def test_default_setup( - hass: HomeAssistant, entity_registry: er.EntityRegistry, dsmr_connection_fixture + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + dsmr_connection_fixture: tuple[MagicMock, MagicMock, MagicMock], ) -> None: """Test the default setup.""" (connection_factory, transport, protocol) = dsmr_connection_fixture @@ -191,7 +193,9 @@ async def test_default_setup( async def test_setup_only_energy( - hass: HomeAssistant, entity_registry: er.EntityRegistry, dsmr_connection_fixture + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + dsmr_connection_fixture: tuple[MagicMock, MagicMock, MagicMock], ) -> None: """Test the default setup.""" (connection_factory, transport, protocol) = dsmr_connection_fixture @@ -240,7 +244,9 @@ async def test_setup_only_energy( assert not entry -async def test_v4_meter(hass: HomeAssistant, dsmr_connection_fixture) -> None: +async def test_v4_meter( + hass: HomeAssistant, dsmr_connection_fixture: tuple[MagicMock, MagicMock, MagicMock] +) -> None: """Test if v4 meter is correctly parsed.""" (connection_factory, transport, protocol) = dsmr_connection_fixture @@ -319,7 +325,10 @@ async def test_v4_meter(hass: HomeAssistant, dsmr_connection_fixture) -> None: ], ) async def test_v5_meter( - hass: HomeAssistant, dsmr_connection_fixture, value: Decimal, state: str + hass: HomeAssistant, + dsmr_connection_fixture: tuple[MagicMock, MagicMock, MagicMock], + value: Decimal, + state: str, ) -> None: """Test if v5 meter is correctly parsed.""" (connection_factory, transport, protocol) = dsmr_connection_fixture @@ -386,7 +395,9 @@ async def test_v5_meter( ) -async def test_luxembourg_meter(hass: HomeAssistant, dsmr_connection_fixture) -> None: +async def test_luxembourg_meter( + hass: HomeAssistant, dsmr_connection_fixture: tuple[MagicMock, MagicMock, MagicMock] +) -> None: """Test if v5 meter is correctly parsed.""" (connection_factory, transport, protocol) = dsmr_connection_fixture @@ -468,7 +479,9 @@ async def test_luxembourg_meter(hass: HomeAssistant, dsmr_connection_fixture) -> ) -async def test_belgian_meter(hass: HomeAssistant, dsmr_connection_fixture) -> None: +async def test_belgian_meter( + hass: HomeAssistant, dsmr_connection_fixture: tuple[MagicMock, MagicMock, MagicMock] +) -> None: """Test if Belgian meter is correctly parsed.""" (connection_factory, transport, protocol) = dsmr_connection_fixture @@ -651,7 +664,9 @@ async def test_belgian_meter(hass: HomeAssistant, dsmr_connection_fixture) -> No ) -async def test_belgian_meter_alt(hass: HomeAssistant, dsmr_connection_fixture) -> None: +async def test_belgian_meter_alt( + hass: HomeAssistant, dsmr_connection_fixture: tuple[MagicMock, MagicMock, MagicMock] +) -> None: """Test if Belgian meter is correctly parsed.""" (connection_factory, transport, protocol) = dsmr_connection_fixture @@ -798,7 +813,9 @@ async def test_belgian_meter_alt(hass: HomeAssistant, dsmr_connection_fixture) - ) -async def test_belgian_meter_mbus(hass: HomeAssistant, dsmr_connection_fixture) -> None: +async def test_belgian_meter_mbus( + hass: HomeAssistant, dsmr_connection_fixture: tuple[MagicMock, MagicMock, MagicMock] +) -> None: """Test if Belgian meter is correctly parsed.""" (connection_factory, transport, protocol) = dsmr_connection_fixture @@ -905,7 +922,9 @@ async def test_belgian_meter_mbus(hass: HomeAssistant, dsmr_connection_fixture) ) -async def test_belgian_meter_low(hass: HomeAssistant, dsmr_connection_fixture) -> None: +async def test_belgian_meter_low( + hass: HomeAssistant, dsmr_connection_fixture: tuple[MagicMock, MagicMock, MagicMock] +) -> None: """Test if Belgian meter is correctly parsed.""" (connection_factory, transport, protocol) = dsmr_connection_fixture @@ -951,7 +970,9 @@ async def test_belgian_meter_low(hass: HomeAssistant, dsmr_connection_fixture) - assert active_tariff.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None -async def test_swedish_meter(hass: HomeAssistant, dsmr_connection_fixture) -> None: +async def test_swedish_meter( + hass: HomeAssistant, dsmr_connection_fixture: tuple[MagicMock, MagicMock, MagicMock] +) -> None: """Test if v5 meter is correctly parsed.""" (connection_factory, transport, protocol) = dsmr_connection_fixture @@ -1017,7 +1038,9 @@ async def test_swedish_meter(hass: HomeAssistant, dsmr_connection_fixture) -> No ) -async def test_easymeter(hass: HomeAssistant, dsmr_connection_fixture) -> None: +async def test_easymeter( + hass: HomeAssistant, dsmr_connection_fixture: tuple[MagicMock, MagicMock, MagicMock] +) -> None: """Test if Q3D meter is correctly parsed.""" (connection_factory, transport, protocol) = dsmr_connection_fixture @@ -1086,7 +1109,9 @@ async def test_easymeter(hass: HomeAssistant, dsmr_connection_fixture) -> None: ) -async def test_tcp(hass: HomeAssistant, dsmr_connection_fixture) -> None: +async def test_tcp( + hass: HomeAssistant, dsmr_connection_fixture: tuple[MagicMock, MagicMock, MagicMock] +) -> None: """If proper config provided TCP connection should be made.""" (connection_factory, transport, protocol) = dsmr_connection_fixture @@ -1112,7 +1137,10 @@ async def test_tcp(hass: HomeAssistant, dsmr_connection_fixture) -> None: assert connection_factory.call_args_list[0][0][1] == "1234" -async def test_rfxtrx_tcp(hass: HomeAssistant, rfxtrx_dsmr_connection_fixture) -> None: +async def test_rfxtrx_tcp( + hass: HomeAssistant, + rfxtrx_dsmr_connection_fixture: tuple[MagicMock, MagicMock, MagicMock], +) -> None: """If proper config provided RFXtrx TCP connection should be made.""" (connection_factory, transport, protocol) = rfxtrx_dsmr_connection_fixture @@ -1140,7 +1168,7 @@ async def test_rfxtrx_tcp(hass: HomeAssistant, rfxtrx_dsmr_connection_fixture) - @patch("homeassistant.components.dsmr.sensor.DEFAULT_RECONNECT_INTERVAL", 0) async def test_connection_errors_retry( - hass: HomeAssistant, dsmr_connection_fixture + hass: HomeAssistant, dsmr_connection_fixture: tuple[MagicMock, MagicMock, MagicMock] ) -> None: """Connection should be retried on error during setup.""" (connection_factory, transport, protocol) = dsmr_connection_fixture @@ -1177,7 +1205,9 @@ async def test_connection_errors_retry( @patch("homeassistant.components.dsmr.sensor.DEFAULT_RECONNECT_INTERVAL", 0) -async def test_reconnect(hass: HomeAssistant, dsmr_connection_fixture) -> None: +async def test_reconnect( + hass: HomeAssistant, dsmr_connection_fixture: tuple[MagicMock, MagicMock, MagicMock] +) -> None: """If transport disconnects, the connection should be retried.""" (connection_factory, transport, protocol) = dsmr_connection_fixture @@ -1255,7 +1285,7 @@ async def test_reconnect(hass: HomeAssistant, dsmr_connection_fixture) -> None: async def test_gas_meter_providing_energy_reading( - hass: HomeAssistant, dsmr_connection_fixture + hass: HomeAssistant, dsmr_connection_fixture: tuple[MagicMock, MagicMock, MagicMock] ) -> None: """Test that gas providing energy readings use the correct device class.""" (connection_factory, transport, protocol) = dsmr_connection_fixture From 76e890865e4c832f3e4f9efb0aee0f3192f5af25 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 25 Jun 2024 13:13:14 +0200 Subject: [PATCH 1162/1445] Adjust imports in cloud tests (#120386) --- tests/components/cloud/__init__.py | 28 ++++++++---- tests/components/cloud/conftest.py | 13 ++++-- tests/components/cloud/test_init.py | 45 +++++++++++--------- tests/components/cloud/test_repairs.py | 14 +++--- tests/components/cloud/test_stt.py | 2 +- tests/components/cloud/test_system_health.py | 2 +- tests/components/cloud/test_tts.py | 24 +++++------ 7 files changed, 75 insertions(+), 53 deletions(-) diff --git a/tests/components/cloud/__init__.py b/tests/components/cloud/__init__.py index 82280336a8c..f1ce24e576f 100644 --- a/tests/components/cloud/__init__.py +++ b/tests/components/cloud/__init__.py @@ -2,9 +2,19 @@ from unittest.mock import AsyncMock, patch -from homeassistant.components import cloud -from homeassistant.components.cloud import const, prefs as cloud_prefs -from homeassistant.components.cloud.const import DATA_CLOUD +from homeassistant.components.cloud.const import ( + DATA_CLOUD, + DOMAIN, + PREF_ALEXA_SETTINGS_VERSION, + PREF_ENABLE_ALEXA, + PREF_ENABLE_GOOGLE, + PREF_GOOGLE_SECURE_DEVICES_PIN, + PREF_GOOGLE_SETTINGS_VERSION, +) +from homeassistant.components.cloud.prefs import ( + ALEXA_SETTINGS_VERSION, + GOOGLE_SETTINGS_VERSION, +) from homeassistant.setup import async_setup_component PIPELINE_DATA = { @@ -62,7 +72,7 @@ async def mock_cloud(hass, config=None): # because it's always setup by bootstrap. Set it up manually in tests. assert await async_setup_component(hass, "homeassistant", {}) - assert await async_setup_component(hass, cloud.DOMAIN, {"cloud": config or {}}) + assert await async_setup_component(hass, DOMAIN, {"cloud": config or {}}) cloud_inst = hass.data[DATA_CLOUD] with patch("hass_nabucasa.Cloud.run_executor", AsyncMock(return_value=None)): await cloud_inst.initialize() @@ -71,11 +81,11 @@ async def mock_cloud(hass, config=None): def mock_cloud_prefs(hass, prefs): """Fixture for cloud component.""" prefs_to_set = { - const.PREF_ALEXA_SETTINGS_VERSION: cloud_prefs.ALEXA_SETTINGS_VERSION, - const.PREF_ENABLE_ALEXA: True, - const.PREF_ENABLE_GOOGLE: True, - const.PREF_GOOGLE_SECURE_DEVICES_PIN: None, - const.PREF_GOOGLE_SETTINGS_VERSION: cloud_prefs.GOOGLE_SETTINGS_VERSION, + PREF_ALEXA_SETTINGS_VERSION: ALEXA_SETTINGS_VERSION, + PREF_ENABLE_ALEXA: True, + PREF_ENABLE_GOOGLE: True, + PREF_GOOGLE_SECURE_DEVICES_PIN: None, + PREF_GOOGLE_SETTINGS_VERSION: GOOGLE_SETTINGS_VERSION, } prefs_to_set.update(prefs) hass.data[DATA_CLOUD].client._prefs._prefs = prefs_to_set diff --git a/tests/components/cloud/conftest.py b/tests/components/cloud/conftest.py index 3058718551e..a7abb932124 100644 --- a/tests/components/cloud/conftest.py +++ b/tests/components/cloud/conftest.py @@ -17,8 +17,13 @@ import jwt import pytest from typing_extensions import AsyncGenerator -from homeassistant.components.cloud import CloudClient, prefs +from homeassistant.components.cloud.client import CloudClient from homeassistant.components.cloud.const import DATA_CLOUD +from homeassistant.components.cloud.prefs import ( + PREF_ALEXA_DEFAULT_EXPOSE, + PREF_GOOGLE_DEFAULT_EXPOSE, + CloudPreferences, +) from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow @@ -174,8 +179,8 @@ def set_cloud_prefs_fixture( async def set_cloud_prefs(prefs_settings: dict[str, Any]) -> None: """Set cloud prefs.""" prefs_to_set = cloud.client.prefs.as_dict() - prefs_to_set.pop(prefs.PREF_ALEXA_DEFAULT_EXPOSE) - prefs_to_set.pop(prefs.PREF_GOOGLE_DEFAULT_EXPOSE) + prefs_to_set.pop(PREF_ALEXA_DEFAULT_EXPOSE) + prefs_to_set.pop(PREF_GOOGLE_DEFAULT_EXPOSE) prefs_to_set.update(prefs_settings) await cloud.client.prefs.async_update(**prefs_to_set) @@ -210,7 +215,7 @@ def mock_cloud_fixture(hass): @pytest.fixture async def cloud_prefs(hass): """Fixture for cloud preferences.""" - cloud_prefs = prefs.CloudPreferences(hass) + cloud_prefs = CloudPreferences(hass) await cloud_prefs.async_initialize() return cloud_prefs diff --git a/tests/components/cloud/test_init.py b/tests/components/cloud/test_init.py index db8253b0329..d201b45b670 100644 --- a/tests/components/cloud/test_init.py +++ b/tests/components/cloud/test_init.py @@ -6,15 +6,22 @@ from unittest.mock import MagicMock, patch import pytest -from homeassistant.components import cloud from homeassistant.components.cloud import ( + CloudConnectionState, CloudNotAvailable, CloudNotConnected, async_get_or_create_cloudhook, + async_listen_connection_change, + async_remote_ui_url, +) +from homeassistant.components.cloud.const import ( + DATA_CLOUD, + DOMAIN, + MODE_DEV, + PREF_CLOUDHOOKS, ) -from homeassistant.components.cloud.const import DATA_CLOUD, DOMAIN, PREF_CLOUDHOOKS from homeassistant.components.cloud.prefs import STORAGE_KEY -from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.const import CONF_MODE, EVENT_HOMEASSISTANT_STOP from homeassistant.core import Context, HomeAssistant from homeassistant.exceptions import Unauthorized from homeassistant.setup import async_setup_component @@ -31,7 +38,7 @@ async def test_constructor_loads_info_from_config(hass: HomeAssistant) -> None: { "http": {}, "cloud": { - cloud.CONF_MODE: cloud.MODE_DEV, + CONF_MODE: MODE_DEV, "cognito_client_id": "test-cognito_client_id", "user_pool_id": "test-user_pool_id", "region": "test-region", @@ -47,7 +54,7 @@ async def test_constructor_loads_info_from_config(hass: HomeAssistant) -> None: assert result cl = hass.data[DATA_CLOUD] - assert cl.mode == cloud.MODE_DEV + assert cl.mode == MODE_DEV assert cl.cognito_client_id == "test-cognito_client_id" assert cl.user_pool_id == "test-user_pool_id" assert cl.region == "test-region" @@ -129,7 +136,7 @@ async def test_setup_existing_cloud_user( { "http": {}, "cloud": { - cloud.CONF_MODE: cloud.MODE_DEV, + CONF_MODE: MODE_DEV, "cognito_client_id": "test-cognito_client_id", "user_pool_id": "test-user_pool_id", "region": "test-region", @@ -156,7 +163,7 @@ async def test_on_connect(hass: HomeAssistant, mock_cloud_fixture) -> None: nonlocal cloud_states cloud_states.append(cloud_state) - cloud.async_listen_connection_change(hass, handle_state) + async_listen_connection_change(hass, handle_state) assert "async_setup" in str(cl.iot._on_connect[-1]) await cl.iot._on_connect[-1]() @@ -178,12 +185,12 @@ async def test_on_connect(hass: HomeAssistant, mock_cloud_fixture) -> None: assert len(mock_load.mock_calls) == 0 assert len(cloud_states) == 1 - assert cloud_states[-1] == cloud.CloudConnectionState.CLOUD_CONNECTED + assert cloud_states[-1] == CloudConnectionState.CLOUD_CONNECTED await cl.iot._on_connect[-1]() await hass.async_block_till_done() assert len(cloud_states) == 2 - assert cloud_states[-1] == cloud.CloudConnectionState.CLOUD_CONNECTED + assert cloud_states[-1] == CloudConnectionState.CLOUD_CONNECTED assert len(cl.iot._on_disconnect) == 2 assert "async_setup" in str(cl.iot._on_disconnect[-1]) @@ -191,12 +198,12 @@ async def test_on_connect(hass: HomeAssistant, mock_cloud_fixture) -> None: await hass.async_block_till_done() assert len(cloud_states) == 3 - assert cloud_states[-1] == cloud.CloudConnectionState.CLOUD_DISCONNECTED + assert cloud_states[-1] == CloudConnectionState.CLOUD_DISCONNECTED await cl.iot._on_disconnect[-1]() await hass.async_block_till_done() assert len(cloud_states) == 4 - assert cloud_states[-1] == cloud.CloudConnectionState.CLOUD_DISCONNECTED + assert cloud_states[-1] == CloudConnectionState.CLOUD_DISCONNECTED async def test_remote_ui_url(hass: HomeAssistant, mock_cloud_fixture) -> None: @@ -204,26 +211,26 @@ async def test_remote_ui_url(hass: HomeAssistant, mock_cloud_fixture) -> None: cl = hass.data[DATA_CLOUD] # Not logged in - with pytest.raises(cloud.CloudNotAvailable): - cloud.async_remote_ui_url(hass) + with pytest.raises(CloudNotAvailable): + async_remote_ui_url(hass) - with patch.object(cloud, "async_is_logged_in", return_value=True): + with patch("homeassistant.components.cloud.async_is_logged_in", return_value=True): # Remote not enabled - with pytest.raises(cloud.CloudNotAvailable): - cloud.async_remote_ui_url(hass) + with pytest.raises(CloudNotAvailable): + async_remote_ui_url(hass) with patch.object(cl.remote, "connect"): await cl.client.prefs.async_update(remote_enabled=True) await hass.async_block_till_done() # No instance domain - with pytest.raises(cloud.CloudNotAvailable): - cloud.async_remote_ui_url(hass) + with pytest.raises(CloudNotAvailable): + async_remote_ui_url(hass) # Remote finished initializing cl.client.prefs._prefs["remote_domain"] = "example.com" - assert cloud.async_remote_ui_url(hass) == "https://example.com" + assert async_remote_ui_url(hass) == "https://example.com" async def test_async_get_or_create_cloudhook( diff --git a/tests/components/cloud/test_repairs.py b/tests/components/cloud/test_repairs.py index 7ca20d84bce..d165a129dbe 100644 --- a/tests/components/cloud/test_repairs.py +++ b/tests/components/cloud/test_repairs.py @@ -6,8 +6,10 @@ from unittest.mock import patch import pytest -from homeassistant.components.cloud import DOMAIN -import homeassistant.components.cloud.repairs as cloud_repairs +from homeassistant.components.cloud.const import DOMAIN +from homeassistant.components.cloud.repairs import ( + async_manage_legacy_subscription_issue, +) from homeassistant.components.repairs import DOMAIN as REPAIRS_DOMAIN from homeassistant.core import HomeAssistant import homeassistant.helpers.issue_registry as ir @@ -65,12 +67,12 @@ async def test_legacy_subscription_delete_issue_if_no_longer_legacy( issue_registry: ir.IssueRegistry, ) -> None: """Test that we delete the legacy subscription issue if no longer legacy.""" - cloud_repairs.async_manage_legacy_subscription_issue(hass, {"provider": "legacy"}) + async_manage_legacy_subscription_issue(hass, {"provider": "legacy"}) assert issue_registry.async_get_issue( domain="cloud", issue_id="legacy_subscription" ) - cloud_repairs.async_manage_legacy_subscription_issue(hass, {}) + async_manage_legacy_subscription_issue(hass, {}) assert not issue_registry.async_get_issue( domain="cloud", issue_id="legacy_subscription" ) @@ -93,7 +95,7 @@ async def test_legacy_subscription_repair_flow( json={"url": "https://paypal.com"}, ) - cloud_repairs.async_manage_legacy_subscription_issue(hass, {"provider": "legacy"}) + async_manage_legacy_subscription_issue(hass, {"provider": "legacy"}) repair_issue = issue_registry.async_get_issue( domain="cloud", issue_id="legacy_subscription" ) @@ -174,7 +176,7 @@ async def test_legacy_subscription_repair_flow_timeout( status=403, ) - cloud_repairs.async_manage_legacy_subscription_issue(hass, {"provider": "legacy"}) + async_manage_legacy_subscription_issue(hass, {"provider": "legacy"}) repair_issue = issue_registry.async_get_issue( domain="cloud", issue_id="legacy_subscription" ) diff --git a/tests/components/cloud/test_stt.py b/tests/components/cloud/test_stt.py index a20325d6dc3..df9e62380f8 100644 --- a/tests/components/cloud/test_stt.py +++ b/tests/components/cloud/test_stt.py @@ -10,7 +10,7 @@ import pytest from typing_extensions import AsyncGenerator from homeassistant.components.assist_pipeline.pipeline import STORAGE_KEY -from homeassistant.components.cloud import DOMAIN +from homeassistant.components.cloud.const import DOMAIN from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component diff --git a/tests/components/cloud/test_system_health.py b/tests/components/cloud/test_system_health.py index c6e738011d6..60b23e47fec 100644 --- a/tests/components/cloud/test_system_health.py +++ b/tests/components/cloud/test_system_health.py @@ -8,7 +8,7 @@ from unittest.mock import MagicMock from aiohttp import ClientError from hass_nabucasa.remote import CertificateStatus -from homeassistant.components.cloud import DOMAIN +from homeassistant.components.cloud.const import DOMAIN from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component diff --git a/tests/components/cloud/test_tts.py b/tests/components/cloud/test_tts.py index 00466d0d177..bf45b6b2895 100644 --- a/tests/components/cloud/test_tts.py +++ b/tests/components/cloud/test_tts.py @@ -12,7 +12,8 @@ from typing_extensions import AsyncGenerator import voluptuous as vol from homeassistant.components.assist_pipeline.pipeline import STORAGE_KEY -from homeassistant.components.cloud import DOMAIN, const, tts +from homeassistant.components.cloud.const import DEFAULT_TTS_DEFAULT_VOICE, DOMAIN +from homeassistant.components.cloud.tts import PLATFORM_SCHEMA, SUPPORT_LANGUAGES, Voice from homeassistant.components.media_player import ( ATTR_MEDIA_CONTENT_ID, DOMAIN as DOMAIN_MP, @@ -57,33 +58,30 @@ async def internal_url_mock(hass: HomeAssistant) -> None: def test_default_exists() -> None: """Test our default language exists.""" - assert const.DEFAULT_TTS_DEFAULT_VOICE[0] in TTS_VOICES - assert ( - const.DEFAULT_TTS_DEFAULT_VOICE[1] - in TTS_VOICES[const.DEFAULT_TTS_DEFAULT_VOICE[0]] - ) + assert DEFAULT_TTS_DEFAULT_VOICE[0] in TTS_VOICES + assert DEFAULT_TTS_DEFAULT_VOICE[1] in TTS_VOICES[DEFAULT_TTS_DEFAULT_VOICE[0]] def test_schema() -> None: """Test schema.""" - assert "nl-NL" in tts.SUPPORT_LANGUAGES + assert "nl-NL" in SUPPORT_LANGUAGES - processed = tts.PLATFORM_SCHEMA({"platform": "cloud", "language": "nl-NL"}) + processed = PLATFORM_SCHEMA({"platform": "cloud", "language": "nl-NL"}) assert processed["gender"] == "female" with pytest.raises(vol.Invalid): - tts.PLATFORM_SCHEMA( + PLATFORM_SCHEMA( {"platform": "cloud", "language": "non-existing", "gender": "female"} ) with pytest.raises(vol.Invalid): - tts.PLATFORM_SCHEMA( + PLATFORM_SCHEMA( {"platform": "cloud", "language": "nl-NL", "gender": "not-supported"} ) # Should not raise - tts.PLATFORM_SCHEMA({"platform": "cloud", "language": "nl-NL", "gender": "female"}) - tts.PLATFORM_SCHEMA({"platform": "cloud"}) + PLATFORM_SCHEMA({"platform": "cloud", "language": "nl-NL", "gender": "female"}) + PLATFORM_SCHEMA({"platform": "cloud"}) @pytest.mark.parametrize( @@ -188,7 +186,7 @@ async def test_provider_properties( assert "nl-NL" in engine.supported_languages supported_voices = engine.async_get_supported_voices("nl-NL") assert supported_voices is not None - assert tts.Voice("ColetteNeural", "ColetteNeural") in supported_voices + assert Voice("ColetteNeural", "ColetteNeural") in supported_voices supported_voices = engine.async_get_supported_voices("missing_language") assert supported_voices is None From c15718519bd8c867b8cdc339cebc363e32dbbdf1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 25 Jun 2024 13:14:11 +0200 Subject: [PATCH 1163/1445] Improve test coverage for ESPHome manager (#120400) --- tests/components/esphome/conftest.py | 35 ++++++++++++++++++++---- tests/components/esphome/test_manager.py | 25 +++++++++++++++++ 2 files changed, 54 insertions(+), 6 deletions(-) diff --git a/tests/components/esphome/conftest.py b/tests/components/esphome/conftest.py index 43edca54158..f55ab9cbe4a 100644 --- a/tests/components/esphome/conftest.py +++ b/tests/components/esphome/conftest.py @@ -263,6 +263,7 @@ async def _mock_generic_device_entry( mock_list_entities_services: tuple[list[EntityInfo], list[UserService]], states: list[EntityState], entry: MockConfigEntry | None = None, + hass_storage: dict[str, Any] | None = None, ) -> MockESPHomeDevice: if not entry: entry = MockConfigEntry( @@ -286,6 +287,17 @@ async def _mock_generic_device_entry( } device_info = DeviceInfo(**(default_device_info | mock_device_info)) + if hass_storage: + storage_key = f"{DOMAIN}.{entry.entry_id}" + hass_storage[storage_key] = { + "version": 1, + "minor_version": 1, + "key": storage_key, + "data": { + "device_info": device_info.to_dict(), + }, + } + mock_device = MockESPHomeDevice(entry, mock_client, device_info) def _subscribe_states(callback: Callable[[EntityState], None]) -> None: @@ -453,6 +465,7 @@ async def mock_bluetooth_entry_with_legacy_adv( @pytest.fixture async def mock_generic_device_entry( hass: HomeAssistant, + hass_storage: dict[str, Any], ) -> Callable[ [APIClient, list[EntityInfo], list[UserService], list[EntityState]], Awaitable[MockConfigEntry], @@ -464,10 +477,17 @@ async def mock_generic_device_entry( entity_info: list[EntityInfo], user_service: list[UserService], states: list[EntityState], + mock_storage: bool = False, ) -> MockConfigEntry: return ( await _mock_generic_device_entry( - hass, mock_client, {}, (entity_info, user_service), states + hass, + mock_client, + {}, + (entity_info, user_service), + states, + None, + hass_storage if mock_storage else None, ) ).entry @@ -477,6 +497,7 @@ async def mock_generic_device_entry( @pytest.fixture async def mock_esphome_device( hass: HomeAssistant, + hass_storage: dict[str, Any], ) -> Callable[ [APIClient, list[EntityInfo], list[UserService], list[EntityState]], Awaitable[MockESPHomeDevice], @@ -485,19 +506,21 @@ async def mock_esphome_device( async def _mock_device( mock_client: APIClient, - entity_info: list[EntityInfo], - user_service: list[UserService], - states: list[EntityState], + entity_info: list[EntityInfo] | None = None, + user_service: list[UserService] | None = None, + states: list[EntityState] | None = None, entry: MockConfigEntry | None = None, device_info: dict[str, Any] | None = None, + mock_storage: bool = False, ) -> MockESPHomeDevice: return await _mock_generic_device_entry( hass, mock_client, device_info or {}, - (entity_info, user_service), - states, + (entity_info or [], user_service or []), + states or [], entry, + hass_storage if mock_storage else None, ) return _mock_device diff --git a/tests/components/esphome/test_manager.py b/tests/components/esphome/test_manager.py index c17ff9a7d8c..d937b63b1db 100644 --- a/tests/components/esphome/test_manager.py +++ b/tests/components/esphome/test_manager.py @@ -1156,3 +1156,28 @@ async def test_start_reauth( assert len(flows) == 1 flow = flows[0] assert flow["context"]["source"] == "reauth" + + +async def test_entry_missing_unique_id( + hass: HomeAssistant, + mock_client: APIClient, + mock_esphome_device: Callable[ + [APIClient, list[EntityInfo], list[UserService], list[EntityState]], + Awaitable[MockESPHomeDevice], + ], +) -> None: + """Test the unique id is added from storage if available.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id=None, + data={ + CONF_HOST: "test.local", + CONF_PORT: 6053, + CONF_PASSWORD: "", + }, + options={CONF_ALLOW_SERVICE_CALLS: True}, + ) + entry.add_to_hass(hass) + await mock_esphome_device(mock_client=mock_client, mock_storage=True) + await hass.async_block_till_done() + assert entry.unique_id == "11:22:33:44:55:aa" From b5afc5a7f02bd34ebdf4e3aae32ab3fc91dafe94 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 25 Jun 2024 13:31:50 +0200 Subject: [PATCH 1164/1445] Fix incorrect mocking in ESPHome tests (#120410) --- tests/components/esphome/test_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/components/esphome/test_manager.py b/tests/components/esphome/test_manager.py index d937b63b1db..92c21842e78 100644 --- a/tests/components/esphome/test_manager.py +++ b/tests/components/esphome/test_manager.py @@ -575,7 +575,7 @@ async def test_connection_aborted_wrong_device( entry.add_to_hass(hass) disconnect_done = hass.loop.create_future() - def async_disconnect(*args, **kwargs) -> None: + async def async_disconnect(*args, **kwargs) -> None: disconnect_done.set_result(None) mock_client.disconnect = async_disconnect From b816fce976de453f04f796bf13f7c7e5b77a3b77 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 25 Jun 2024 14:08:30 +0200 Subject: [PATCH 1165/1445] Improve websocket_api schema typing (#120411) --- homeassistant/components/websocket_api/__init__.py | 6 ++---- homeassistant/components/websocket_api/decorators.py | 3 ++- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/websocket_api/__init__.py b/homeassistant/components/websocket_api/__init__.py index d8427bff10e..f9bc4396e01 100644 --- a/homeassistant/components/websocket_api/__init__.py +++ b/homeassistant/components/websocket_api/__init__.py @@ -4,11 +4,9 @@ from __future__ import annotations from typing import Final, cast -import voluptuous as vol - from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.typing import ConfigType, VolSchemaType from homeassistant.loader import bind_hass from . import commands, connection, const, decorators, http, messages # noqa: F401 @@ -55,7 +53,7 @@ def async_register_command( hass: HomeAssistant, command_or_handler: str | const.WebSocketCommandHandler, handler: const.WebSocketCommandHandler | None = None, - schema: vol.Schema | None = None, + schema: VolSchemaType | None = None, ) -> None: """Register a websocket command.""" if handler is None: diff --git a/homeassistant/components/websocket_api/decorators.py b/homeassistant/components/websocket_api/decorators.py index 5131d02b4d3..b9924bc91d1 100644 --- a/homeassistant/components/websocket_api/decorators.py +++ b/homeassistant/components/websocket_api/decorators.py @@ -11,6 +11,7 @@ import voluptuous as vol from homeassistant.const import HASSIO_USER_NAME from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import Unauthorized +from homeassistant.helpers.typing import VolDictType from . import const, messages from .connection import ActiveConnection @@ -130,7 +131,7 @@ def ws_require_user( def websocket_command( - schema: dict[vol.Marker, Any] | vol.All, + schema: VolDictType | vol.All, ) -> Callable[[const.WebSocketCommandHandler], const.WebSocketCommandHandler]: """Tag a function as a websocket command. From aa05f7321066250e82df498fbea1464f5a4536d5 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Tue, 25 Jun 2024 14:26:20 +0200 Subject: [PATCH 1166/1445] Add fixture to synchronize with debouncer in MQTT tests (#120373) * Add fixture to synchronze with debouncer in MQTT tests * Migrate more tests to use the debouncer * Migrate more tests * Migrate util tests * Improve mqtt on_callback test using new fixture * Improve test_subscribe_error * Migrate other tests * Import EnsureJobAfterCooldown from `util.py` but patch `client.py` --- tests/components/mqtt/conftest.py | 24 ++ tests/components/mqtt/test_init.py | 354 +++++++++++++---------------- tests/components/mqtt/test_util.py | 15 +- 3 files changed, 192 insertions(+), 201 deletions(-) diff --git a/tests/components/mqtt/conftest.py b/tests/components/mqtt/conftest.py index 5a1f65667cf..774785bb42a 100644 --- a/tests/components/mqtt/conftest.py +++ b/tests/components/mqtt/conftest.py @@ -10,6 +10,7 @@ from typing_extensions import AsyncGenerator, Generator from homeassistant.components import mqtt from homeassistant.components.mqtt.models import MessageCallbackType, ReceiveMessage +from homeassistant.components.mqtt.util import EnsureJobAfterCooldown from homeassistant.const import EVENT_HOMEASSISTANT_STARTED from homeassistant.core import HomeAssistant, callback @@ -49,6 +50,29 @@ def mock_temp_dir(temp_dir_prefix: str) -> Generator[str]: yield mocked_temp_dir +@pytest.fixture +def mock_debouncer(hass: HomeAssistant) -> Generator[asyncio.Event]: + """Mock EnsureJobAfterCooldown. + + Returns an asyncio.Event that allows to await the debouncer task to be finished. + """ + task_done = asyncio.Event() + + class MockDeboncer(EnsureJobAfterCooldown): + """Mock the MQTT client (un)subscribe debouncer.""" + + async def _async_job(self) -> None: + """Execute after a cooldown period.""" + await super()._async_job() + task_done.set() + + # We mock the import of EnsureJobAfterCooldown in client.py + with patch( + "homeassistant.components.mqtt.client.EnsureJobAfterCooldown", MockDeboncer + ): + yield task_done + + @pytest.fixture async def setup_with_birth_msg_client_mock( hass: HomeAssistant, diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index 2c3ca31bff9..231379601c6 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -140,13 +140,13 @@ async def test_mqtt_connects_on_home_assistant_mqtt_setup( async def test_mqtt_does_not_disconnect_on_home_assistant_stop( hass: HomeAssistant, + mock_debouncer: asyncio.Event, setup_with_birth_msg_client_mock: MqttMockPahoClient, ) -> None: """Test if client is not disconnected on HA stop.""" mqtt_client_mock = setup_with_birth_msg_client_mock hass.bus.fire(EVENT_HOMEASSISTANT_STOP) - await hass.async_block_till_done() - await hass.async_block_till_done() + await mock_debouncer.wait() assert mqtt_client_mock.disconnect.call_count == 0 @@ -1085,6 +1085,7 @@ async def test_subscribe_mqtt_config_entry_disabled( async def test_subscribe_and_resubscribe( hass: HomeAssistant, client_debug_log: None, + mock_debouncer: asyncio.Event, setup_with_birth_msg_client_mock: MqttMockPahoClient, recorded_calls: list[ReceiveMessage], record_calls: MessageCallbackType, @@ -1095,15 +1096,16 @@ async def test_subscribe_and_resubscribe( patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.4), patch("homeassistant.components.mqtt.client.UNSUBSCRIBE_COOLDOWN", 0.4), ): + mock_debouncer.clear() unsub = await mqtt.async_subscribe(hass, "test-topic", record_calls) # This unsub will be un-done with the following subscribe # unsubscribe should not be called at the broker unsub() unsub = await mqtt.async_subscribe(hass, "test-topic", record_calls) + await mock_debouncer.wait() + mock_debouncer.clear() async_fire_mqtt_message(hass, "test-topic", "test-payload") - await asyncio.sleep(0.1) - await hass.async_block_till_done(wait_background_tasks=True) assert len(recorded_calls) == 1 assert recorded_calls[0].topic == "test-topic" @@ -1111,38 +1113,41 @@ async def test_subscribe_and_resubscribe( # assert unsubscribe was not called mqtt_client_mock.unsubscribe.assert_not_called() + mock_debouncer.clear() unsub() - await asyncio.sleep(0.1) - await hass.async_block_till_done(wait_background_tasks=True) + await mock_debouncer.wait() mqtt_client_mock.unsubscribe.assert_called_once_with(["test-topic"]) async def test_subscribe_topic_non_async( hass: HomeAssistant, + mock_debouncer: asyncio.Event, mqtt_mock_entry: MqttMockHAClientGenerator, recorded_calls: list[ReceiveMessage], record_calls: MessageCallbackType, ) -> None: """Test the subscription of a topic using the non-async function.""" await mqtt_mock_entry() + await mock_debouncer.wait() + mock_debouncer.clear() unsub = await hass.async_add_executor_job( mqtt.subscribe, hass, "test-topic", record_calls ) - await hass.async_block_till_done() + await mock_debouncer.wait() async_fire_mqtt_message(hass, "test-topic", "test-payload") - await hass.async_block_till_done() assert len(recorded_calls) == 1 assert recorded_calls[0].topic == "test-topic" assert recorded_calls[0].payload == "test-payload" + mock_debouncer.clear() await hass.async_add_executor_job(unsub) + await mock_debouncer.wait() async_fire_mqtt_message(hass, "test-topic", "test-payload") - await hass.async_block_till_done() assert len(recorded_calls) == 1 @@ -1417,11 +1422,9 @@ async def test_subscribe_special_characters( assert recorded_calls[0].payload == payload -@patch("homeassistant.components.mqtt.client.INITIAL_SUBSCRIBE_COOLDOWN", 0.0) -@patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.0) -@patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.0) async def test_subscribe_same_topic( hass: HomeAssistant, + mock_debouncer: asyncio.Event, setup_with_birth_msg_client_mock: MqttMockPahoClient, ) -> None: """Test subscribing to same topic twice and simulate retained messages. @@ -1442,25 +1445,22 @@ async def test_subscribe_same_topic( calls_b.append(msg) mqtt_client_mock.reset_mock() + mock_debouncer.clear() await mqtt.async_subscribe(hass, "test/state", _callback_a, qos=0) # Simulate a non retained message after the first subscription async_fire_mqtt_message(hass, "test/state", "online", qos=0, retain=False) - async_fire_time_changed(hass, utcnow() + timedelta(seconds=1)) - await hass.async_block_till_done() + await mock_debouncer.wait() assert len(calls_a) == 1 mqtt_client_mock.subscribe.assert_called() calls_a = [] mqtt_client_mock.reset_mock() - async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) await hass.async_block_till_done() + mock_debouncer.clear() await mqtt.async_subscribe(hass, "test/state", _callback_b, qos=1) # Simulate an other non retained message after the second subscription async_fire_mqtt_message(hass, "test/state", "online", qos=0, retain=False) - async_fire_time_changed(hass, utcnow() + timedelta(seconds=1)) - await hass.async_block_till_done() - async_fire_time_changed(hass, utcnow() + timedelta(seconds=1)) - await hass.async_block_till_done() + await mock_debouncer.wait() # Both subscriptions should receive updates assert len(calls_a) == 1 assert len(calls_b) == 1 @@ -1469,6 +1469,7 @@ async def test_subscribe_same_topic( async def test_replaying_payload_same_topic( hass: HomeAssistant, + mock_debouncer: asyncio.Event, setup_with_birth_msg_client_mock: MqttMockPahoClient, ) -> None: """Test replaying retained messages. @@ -1491,21 +1492,20 @@ async def test_replaying_payload_same_topic( calls_b.append(msg) mqtt_client_mock.reset_mock() + mock_debouncer.clear() await mqtt.async_subscribe(hass, "test/state", _callback_a) - await hass.async_block_till_done() + await mock_debouncer.wait() async_fire_mqtt_message( hass, "test/state", "online", qos=0, retain=True ) # Simulate a (retained) message played back - async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) - await asyncio.sleep(0) - await hass.async_block_till_done() - assert len(calls_a) == 1 mqtt_client_mock.subscribe.assert_called() calls_a = [] mqtt_client_mock.reset_mock() + mock_debouncer.clear() await mqtt.async_subscribe(hass, "test/state", _callback_b) + await mock_debouncer.wait() # Simulate edge case where non retained message was received # after subscription at HA but before the debouncer delay was passed. @@ -1516,12 +1516,6 @@ async def test_replaying_payload_same_topic( # Simulate a (retained) message played back on new subscriptions async_fire_mqtt_message(hass, "test/state", "online", qos=0, retain=True) - # Make sure the debouncer delay was passed - await hass.async_block_till_done() - async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) - await asyncio.sleep(0) - await hass.async_block_till_done() - # The current subscription only received the message without retain flag assert len(calls_a) == 1 assert help_assert_message(calls_a[0], "test/state", "online", qos=0, retain=False) @@ -1542,10 +1536,6 @@ async def test_replaying_payload_same_topic( # After connecting the retain flag will not be set, even if the # payload published was retained, we cannot see that async_fire_mqtt_message(hass, "test/state", "online", qos=0, retain=False) - await hass.async_block_till_done() - async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) - await asyncio.sleep(0) - await hass.async_block_till_done() assert len(calls_a) == 1 assert help_assert_message(calls_a[0], "test/state", "online", qos=0, retain=False) assert len(calls_b) == 1 @@ -1556,18 +1546,13 @@ async def test_replaying_payload_same_topic( calls_b = [] mqtt_client_mock.reset_mock() mqtt_client_mock.on_disconnect(None, None, 0) + + mock_debouncer.clear() mqtt_client_mock.on_connect(None, None, None, 0) - await hass.async_block_till_done() - async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) - await asyncio.sleep(0) - await hass.async_block_till_done() + await mock_debouncer.wait() mqtt_client_mock.subscribe.assert_called() # Simulate a (retained) message played back after reconnecting async_fire_mqtt_message(hass, "test/state", "online", qos=0, retain=True) - await hass.async_block_till_done() - async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) - await asyncio.sleep(0) - await hass.async_block_till_done(wait_background_tasks=True) # Both subscriptions now should replay the retained message assert len(calls_a) == 1 assert help_assert_message(calls_a[0], "test/state", "online", qos=0, retain=True) @@ -1575,11 +1560,9 @@ async def test_replaying_payload_same_topic( assert help_assert_message(calls_b[0], "test/state", "online", qos=0, retain=True) -@patch("homeassistant.components.mqtt.client.INITIAL_SUBSCRIBE_COOLDOWN", 0.0) -@patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.0) -@patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.0) async def test_replaying_payload_after_resubscribing( hass: HomeAssistant, + mock_debouncer: asyncio.Event, setup_with_birth_msg_client_mock: MqttMockPahoClient, ) -> None: """Test replaying and filtering retained messages after resubscribing. @@ -1597,22 +1580,18 @@ async def test_replaying_payload_after_resubscribing( calls_a.append(msg) mqtt_client_mock.reset_mock() + mock_debouncer.clear() unsub = await mqtt.async_subscribe(hass, "test/state", _callback_a) - await hass.async_block_till_done() - async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) - await hass.async_block_till_done() - await hass.async_block_till_done() + await mock_debouncer.wait() mqtt_client_mock.subscribe.assert_called() # Simulate a (retained) message played back async_fire_mqtt_message(hass, "test/state", "online", qos=0, retain=True) - await hass.async_block_till_done() assert help_assert_message(calls_a[0], "test/state", "online", qos=0, retain=True) calls_a.clear() # Test we get updates async_fire_mqtt_message(hass, "test/state", "offline", qos=0, retain=False) - await hass.async_block_till_done() assert help_assert_message(calls_a[0], "test/state", "offline", qos=0, retain=False) calls_a.clear() @@ -1622,24 +1601,20 @@ async def test_replaying_payload_after_resubscribing( assert len(calls_a) == 0 # Unsubscribe an resubscribe again + mock_debouncer.clear() unsub() unsub = await mqtt.async_subscribe(hass, "test/state", _callback_a) - await hass.async_block_till_done() - async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) - await hass.async_block_till_done() + await mock_debouncer.wait() mqtt_client_mock.subscribe.assert_called() # Simulate we can receive a (retained) played back message again async_fire_mqtt_message(hass, "test/state", "online", qos=0, retain=True) - await hass.async_block_till_done() assert help_assert_message(calls_a[0], "test/state", "online", qos=0, retain=True) -@patch("homeassistant.components.mqtt.client.INITIAL_SUBSCRIBE_COOLDOWN", 0.0) -@patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.0) -@patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.0) async def test_replaying_payload_wildcard_topic( hass: HomeAssistant, + mock_debouncer: asyncio.Event, setup_with_birth_msg_client_mock: MqttMockPahoClient, ) -> None: """Test replaying retained messages. @@ -1663,28 +1638,24 @@ async def test_replaying_payload_wildcard_topic( calls_b.append(msg) mqtt_client_mock.reset_mock() + mock_debouncer.clear() await mqtt.async_subscribe(hass, "test/#", _callback_a) + await mock_debouncer.wait() # Simulate (retained) messages being played back on new subscriptions async_fire_mqtt_message(hass, "test/state1", "new_value_1", qos=0, retain=True) async_fire_mqtt_message(hass, "test/state2", "new_value_2", qos=0, retain=True) - await hass.async_block_till_done() - async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) # cooldown - await asyncio.sleep(0) - await hass.async_block_till_done() assert len(calls_a) == 2 mqtt_client_mock.subscribe.assert_called() calls_a = [] mqtt_client_mock.reset_mock() # resubscribe to the wild card topic again + mock_debouncer.clear() await mqtt.async_subscribe(hass, "test/#", _callback_b) + await mock_debouncer.wait() # Simulate (retained) messages being played back on new subscriptions async_fire_mqtt_message(hass, "test/state1", "initial_value_1", qos=0, retain=True) async_fire_mqtt_message(hass, "test/state2", "initial_value_2", qos=0, retain=True) - await hass.async_block_till_done() - async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) # cooldown - await asyncio.sleep(0) - await hass.async_block_till_done() # The retained messages playback should only be processed for the new subscriptions assert len(calls_a) == 0 assert len(calls_b) == 2 @@ -1697,8 +1668,6 @@ async def test_replaying_payload_wildcard_topic( # Simulate new messages being received async_fire_mqtt_message(hass, "test/state1", "update_value_1", qos=0, retain=False) async_fire_mqtt_message(hass, "test/state2", "update_value_2", qos=0, retain=False) - await hass.async_block_till_done() - await asyncio.sleep(0) assert len(calls_a) == 2 assert len(calls_b) == 2 @@ -1707,20 +1676,16 @@ async def test_replaying_payload_wildcard_topic( calls_b = [] mqtt_client_mock.reset_mock() mqtt_client_mock.on_disconnect(None, None, 0) + + mock_debouncer.clear() mqtt_client_mock.on_connect(None, None, None, 0) - await hass.async_block_till_done() - async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) # cooldown - await asyncio.sleep(0) - await hass.async_block_till_done() + await mock_debouncer.wait() + mqtt_client_mock.subscribe.assert_called() # Simulate the (retained) messages are played back after reconnecting # for all subscriptions async_fire_mqtt_message(hass, "test/state1", "update_value_1", qos=0, retain=True) async_fire_mqtt_message(hass, "test/state2", "update_value_2", qos=0, retain=True) - await hass.async_block_till_done() - async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) # cooldown - await asyncio.sleep(0) - await hass.async_block_till_done(wait_background_tasks=True) # Both subscriptions should replay assert len(calls_a) == 2 assert len(calls_b) == 2 @@ -1728,29 +1693,32 @@ async def test_replaying_payload_wildcard_topic( async def test_not_calling_unsubscribe_with_active_subscribers( hass: HomeAssistant, + mock_debouncer: asyncio.Event, setup_with_birth_msg_client_mock: MqttMockPahoClient, record_calls: MessageCallbackType, ) -> None: """Test not calling unsubscribe() when other subscribers are active.""" mqtt_client_mock = setup_with_birth_msg_client_mock mqtt_client_mock.reset_mock() + mock_debouncer.clear() unsub = await mqtt.async_subscribe(hass, "test/state", record_calls, 2) await mqtt.async_subscribe(hass, "test/state", record_calls, 1) - await hass.async_block_till_done() - await asyncio.sleep(0) - await hass.async_block_till_done() + await mock_debouncer.wait() assert mqtt_client_mock.subscribe.called + mock_debouncer.clear() unsub() await hass.async_block_till_done() - async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) # cooldown await hass.async_block_till_done(wait_background_tasks=True) + async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) # cooldown assert not mqtt_client_mock.unsubscribe.called + assert not mock_debouncer.is_set() async def test_not_calling_subscribe_when_unsubscribed_within_cooldown( hass: HomeAssistant, - setup_with_birth_msg_client_mock: MqttMockPahoClient, + mock_debouncer: asyncio.Event, + mqtt_mock_entry: MqttMockHAClientGenerator, record_calls: MessageCallbackType, ) -> None: """Test not calling subscribe() when it is unsubscribed. @@ -1758,18 +1726,22 @@ async def test_not_calling_subscribe_when_unsubscribed_within_cooldown( Make sure subscriptions are cleared if unsubscribed before the subscribe cool down period has ended. """ - mqtt_client_mock = setup_with_birth_msg_client_mock - mqtt_client_mock.subscribe.reset_mock() + mqtt_mock = await mqtt_mock_entry() + mqtt_client_mock = mqtt_mock._mqttc + await mock_debouncer.wait() + mock_debouncer.clear() + mqtt_client_mock.subscribe.reset_mock() unsub = await mqtt.async_subscribe(hass, "test/state", record_calls) unsub() - async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) # cooldown - await hass.async_block_till_done() + await mock_debouncer.wait() + # The debouncer executes without an pending subscribes assert not mqtt_client_mock.subscribe.called async def test_unsubscribe_race( hass: HomeAssistant, + mock_debouncer: asyncio.Event, setup_with_birth_msg_client_mock: MqttMockPahoClient, ) -> None: """Test not calling unsubscribe() when other subscribers are active.""" @@ -1786,15 +1758,14 @@ async def test_unsubscribe_race( calls_b.append(msg) mqtt_client_mock.reset_mock() + + mock_debouncer.clear() unsub = await mqtt.async_subscribe(hass, "test/state", _callback_a) unsub() await mqtt.async_subscribe(hass, "test/state", _callback_b) - await asyncio.sleep(0) - await hass.async_block_till_done() + await mock_debouncer.wait() async_fire_mqtt_message(hass, "test/state", "online") - await asyncio.sleep(0) - await hass.async_block_till_done() assert not calls_a assert calls_b @@ -1825,6 +1796,7 @@ async def test_unsubscribe_race( ) async def test_restore_subscriptions_on_reconnect( hass: HomeAssistant, + mock_debouncer: asyncio.Event, setup_with_birth_msg_client_mock: MqttMockPahoClient, record_calls: MessageCallbackType, ) -> None: @@ -1833,18 +1805,18 @@ async def test_restore_subscriptions_on_reconnect( mqtt_client_mock.reset_mock() + mock_debouncer.clear() await mqtt.async_subscribe(hass, "test/state", record_calls) async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) # cooldown - await asyncio.sleep(0) - await hass.async_block_till_done() + await mock_debouncer.wait() assert ("test/state", 0) in help_all_subscribe_calls(mqtt_client_mock) mqtt_client_mock.reset_mock() mqtt_client_mock.on_disconnect(None, None, 0) + + mock_debouncer.clear() mqtt_client_mock.on_connect(None, None, None, 0) - await hass.async_block_till_done() - await asyncio.sleep(0.1) - await hass.async_block_till_done(wait_background_tasks=True) + await mock_debouncer.wait() assert ("test/state", 0) in help_all_subscribe_calls(mqtt_client_mock) @@ -1854,20 +1826,19 @@ async def test_restore_subscriptions_on_reconnect( ) async def test_restore_all_active_subscriptions_on_reconnect( hass: HomeAssistant, + mock_debouncer: asyncio.Event, setup_with_birth_msg_client_mock: MqttMockPahoClient, record_calls: MessageCallbackType, - freezer: FrozenDateTimeFactory, ) -> None: """Test active subscriptions are restored correctly on reconnect.""" mqtt_client_mock = setup_with_birth_msg_client_mock mqtt_client_mock.reset_mock() + mock_debouncer.clear() unsub = await mqtt.async_subscribe(hass, "test/state", record_calls, qos=2) await mqtt.async_subscribe(hass, "test/state", record_calls, qos=1) await mqtt.async_subscribe(hass, "test/state", record_calls, qos=0) - await hass.async_block_till_done() - freezer.tick(3) - async_fire_time_changed(hass) # cooldown - await hass.async_block_till_done() + # cooldown + await mock_debouncer.wait() # the subscription with the highest QoS should survive expected = [ @@ -1876,68 +1847,54 @@ async def test_restore_all_active_subscriptions_on_reconnect( assert mqtt_client_mock.subscribe.mock_calls == expected unsub() - await hass.async_block_till_done() assert mqtt_client_mock.unsubscribe.call_count == 0 mqtt_client_mock.on_disconnect(None, None, 0) - await hass.async_block_till_done() + + mock_debouncer.clear() mqtt_client_mock.on_connect(None, None, None, 0) - freezer.tick(3) - async_fire_time_changed(hass) # cooldown - await hass.async_block_till_done() + # wait for cooldown + await mock_debouncer.wait() expected.append(call([("test/state", 1)])) for expected_call in expected: assert mqtt_client_mock.subscribe.hass_call(expected_call) - freezer.tick(3) - async_fire_time_changed(hass) # cooldown - await hass.async_block_till_done() - freezer.tick(3) - async_fire_time_changed(hass) # cooldown - await hass.async_block_till_done(wait_background_tasks=True) - @pytest.mark.parametrize( "mqtt_config_entry_data", [{mqtt.CONF_BROKER: "mock-broker", mqtt.CONF_DISCOVERY: False}], ) -@patch("homeassistant.components.mqtt.client.INITIAL_SUBSCRIBE_COOLDOWN", 1.0) -@patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.0) -@patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 1.0) async def test_subscribed_at_highest_qos( hass: HomeAssistant, + mock_debouncer: asyncio.Event, setup_with_birth_msg_client_mock: MqttMockPahoClient, record_calls: MessageCallbackType, - freezer: FrozenDateTimeFactory, ) -> None: """Test the highest qos as assigned when subscribing to the same topic.""" mqtt_client_mock = setup_with_birth_msg_client_mock mqtt_client_mock.reset_mock() + mock_debouncer.clear() await mqtt.async_subscribe(hass, "test/state", record_calls, qos=0) await hass.async_block_till_done() - freezer.tick(5) - async_fire_time_changed(hass) # cooldown - await hass.async_block_till_done() + # cooldown + await mock_debouncer.wait() assert ("test/state", 0) in help_all_subscribe_calls(mqtt_client_mock) mqtt_client_mock.reset_mock() - freezer.tick(5) - async_fire_time_changed(hass) # cooldown - await hass.async_block_till_done() - await hass.async_block_till_done() + mock_debouncer.clear() await mqtt.async_subscribe(hass, "test/state", record_calls, qos=1) await mqtt.async_subscribe(hass, "test/state", record_calls, qos=2) - await hass.async_block_till_done() - freezer.tick(5) - async_fire_time_changed(hass) # cooldown - await hass.async_block_till_done() + # cooldown + await mock_debouncer.wait() + # the subscription with the highest QoS should survive assert help_all_subscribe_calls(mqtt_client_mock) == [("test/state", 2)] async def test_reload_entry_with_restored_subscriptions( hass: HomeAssistant, + mock_debouncer: asyncio.Event, mqtt_client_mock: MqttMockPahoClient, record_calls: MessageCallbackType, recorded_calls: list[ReceiveMessage], @@ -1950,13 +1907,15 @@ async def test_reload_entry_with_restored_subscriptions( with patch("homeassistant.config.load_yaml_config_file", return_value={}): await hass.config_entries.async_setup(entry.entry_id) + mock_debouncer.clear() await mqtt.async_subscribe(hass, "test-topic", record_calls) await mqtt.async_subscribe(hass, "wild/+/card", record_calls) + # cooldown + await mock_debouncer.wait() async_fire_mqtt_message(hass, "test-topic", "test-payload") async_fire_mqtt_message(hass, "wild/any/card", "wild-card-payload") - await hass.async_block_till_done() assert len(recorded_calls) == 2 assert recorded_calls[0].topic == "test-topic" assert recorded_calls[0].payload == "test-payload" @@ -1967,13 +1926,14 @@ async def test_reload_entry_with_restored_subscriptions( # Reload the entry with patch("homeassistant.config.load_yaml_config_file", return_value={}): assert await hass.config_entries.async_reload(entry.entry_id) + mock_debouncer.clear() assert entry.state is ConfigEntryState.LOADED - await hass.async_block_till_done() + # cooldown + await mock_debouncer.wait() async_fire_mqtt_message(hass, "test-topic", "test-payload2") async_fire_mqtt_message(hass, "wild/any/card", "wild-card-payload2") - await hass.async_block_till_done() assert len(recorded_calls) == 2 assert recorded_calls[0].topic == "test-topic" assert recorded_calls[0].payload == "test-payload2" @@ -1984,13 +1944,14 @@ async def test_reload_entry_with_restored_subscriptions( # Reload the entry again with patch("homeassistant.config.load_yaml_config_file", return_value={}): assert await hass.config_entries.async_reload(entry.entry_id) + mock_debouncer.clear() assert entry.state is ConfigEntryState.LOADED - await hass.async_block_till_done() + # cooldown + await mock_debouncer.wait() async_fire_mqtt_message(hass, "test-topic", "test-payload3") async_fire_mqtt_message(hass, "wild/any/card", "wild-card-payload3") - await hass.async_block_till_done() assert len(recorded_calls) == 2 assert recorded_calls[0].topic == "test-topic" assert recorded_calls[0].payload == "test-payload3" @@ -2079,9 +2040,9 @@ async def test_handle_mqtt_on_callback_after_timeout( """Test receiving an ACK after a timeout.""" mqtt_mock = await mqtt_mock_entry() # Simulate the mid future getting a timeout - mqtt_mock()._async_get_mid_future(100).set_exception(asyncio.TimeoutError) - # Simulate an ACK for mid == 100, being received after the timeout - mqtt_client_mock.on_publish(mqtt_client_mock, None, 100) + mqtt_mock()._async_get_mid_future(101).set_exception(asyncio.TimeoutError) + # Simulate an ACK for mid == 101, being received after the timeout + mqtt_client_mock.on_publish(mqtt_client_mock, None, 101) await hass.async_block_till_done() assert "No ACK from MQTT server" not in caplog.text assert "InvalidStateError" not in caplog.text @@ -2119,20 +2080,18 @@ async def test_subscribe_error( mqtt_client_mock.reset_mock() # simulate client is not connected error before subscribing mqtt_client_mock.subscribe.side_effect = lambda *args: (4, None) - with patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.0): - await mqtt.async_subscribe(hass, "some-topic", record_calls) - while mqtt_client_mock.subscribe.call_count == 0: - await hass.async_block_till_done() + await mqtt.async_subscribe(hass, "some-topic", record_calls) + while mqtt_client_mock.subscribe.call_count == 0: await hass.async_block_till_done() - await hass.async_block_till_done() - assert ( - "Error talking to MQTT: The client is not currently connected." - in caplog.text - ) + await hass.async_block_till_done() + assert ( + "Error talking to MQTT: The client is not currently connected." in caplog.text + ) async def test_handle_message_callback( hass: HomeAssistant, + mock_debouncer: asyncio.Event, setup_with_birth_msg_client_mock: MqttMockPahoClient, ) -> None: """Test for handling an incoming message callback.""" @@ -2146,12 +2105,12 @@ async def test_handle_message_callback( msg = ReceiveMessage( "some-topic", b"test-payload", 1, False, "some-topic", datetime.now() ) + mock_debouncer.clear() await mqtt.async_subscribe(hass, "some-topic", _callback) + await mock_debouncer.wait() mqtt_client_mock.reset_mock() mqtt_client_mock.on_message(None, None, msg) - await hass.async_block_till_done() - await hass.async_block_till_done() assert len(callbacks) == 1 assert callbacks[0].topic == "some-topic" assert callbacks[0].qos == 1 @@ -2239,7 +2198,7 @@ async def test_setup_mqtt_client_protocol( @patch("homeassistant.components.mqtt.client.TIMEOUT_ACK", 0.2) async def test_handle_mqtt_timeout_on_callback( - hass: HomeAssistant, caplog: pytest.LogCaptureFixture + hass: HomeAssistant, caplog: pytest.LogCaptureFixture, mock_debouncer: asyncio.Event ) -> None: """Test publish without receiving an ACK callback.""" mid = 0 @@ -2247,7 +2206,7 @@ async def test_handle_mqtt_timeout_on_callback( class FakeInfo: """Returns a simulated client publish response.""" - mid = 100 + mid = 102 rc = 0 with patch( @@ -2264,7 +2223,9 @@ async def test_handle_mqtt_timeout_on_callback( # We want to simulate the publish behaviour MQTT client mock_client = mock_client.return_value mock_client.publish.return_value = FakeInfo() + # Mock we get a mid and rc=0 mock_client.subscribe.side_effect = _mock_ack + mock_client.unsubscribe.side_effect = _mock_ack mock_client.connect = MagicMock( return_value=0, side_effect=lambda *args, **kwargs: hass.loop.call_soon_threadsafe( @@ -2278,6 +2239,7 @@ async def test_handle_mqtt_timeout_on_callback( entry.add_to_hass(hass) # Set up the integration + mock_debouncer.clear() assert await hass.config_entries.async_setup(entry.entry_id) # Now call we publish without simulating and ACK callback @@ -2286,6 +2248,12 @@ async def test_handle_mqtt_timeout_on_callback( # There is no ACK so we should see a timeout in the log after publishing assert len(mock_client.publish.mock_calls) == 1 assert "No ACK from MQTT server" in caplog.text + # Ensure we stop lingering background tasks + await hass.config_entries.async_unload(entry.entry_id) + # Assert we did not have any completed subscribes, + # because the debouncer subscribe job failed to receive an ACK, + # and the time auto caused the debouncer job to fail. + assert not mock_debouncer.is_set() async def test_setup_raises_config_entry_not_ready_if_no_connect_broker( @@ -2391,26 +2359,22 @@ async def test_tls_version( @patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.0) async def test_custom_birth_message( hass: HomeAssistant, + mock_debouncer: asyncio.Event, mqtt_config_entry_data: dict[str, Any], mqtt_client_mock: MqttMockPahoClient, ) -> None: """Test sending birth message.""" - birth = asyncio.Event() entry = MockConfigEntry(domain=mqtt.DOMAIN, data=mqtt_config_entry_data) entry.add_to_hass(hass) hass.config.components.add(mqtt.DOMAIN) assert await hass.config_entries.async_setup(entry.entry_id) + mock_debouncer.clear() hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) - - @callback - def wait_birth(msg: ReceiveMessage) -> None: - """Handle birth message.""" - birth.set() - - await mqtt.async_subscribe(hass, "birth", wait_birth) - await hass.async_block_till_done() - await birth.wait() + # discovery cooldown + await mock_debouncer.wait() + # Wait for publish call to finish + await hass.async_block_till_done(wait_background_tasks=True) mqtt_client_mock.publish.assert_called_with("birth", "birth", 0, False) @@ -2439,6 +2403,8 @@ async def test_default_birth_message( @patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.0) async def test_no_birth_message( hass: HomeAssistant, + record_calls: MessageCallbackType, + mock_debouncer: asyncio.Event, mqtt_config_entry_data: dict[str, Any], mqtt_client_mock: MqttMockPahoClient, ) -> None: @@ -2446,26 +2412,19 @@ async def test_no_birth_message( entry = MockConfigEntry(domain=mqtt.DOMAIN, data=mqtt_config_entry_data) entry.add_to_hass(hass) hass.config.components.add(mqtt.DOMAIN) + mock_debouncer.clear() assert await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) - await hass.async_block_till_done() - mqtt_client_mock.reset_mock() - - await hass.async_block_till_done() - async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) - await hass.async_block_till_done() + # Wait for discovery cooldown + await mock_debouncer.wait() + # Ensure any publishing could have been processed + await hass.async_block_till_done(wait_background_tasks=True) mqtt_client_mock.publish.assert_not_called() - @callback - def msg_callback(msg: ReceiveMessage) -> None: - """Handle callback.""" - mqtt_client_mock.reset_mock() - await mqtt.async_subscribe(hass, "homeassistant/some-topic", msg_callback) - await hass.async_block_till_done() - async_fire_time_changed(hass, utcnow() + timedelta(seconds=5)) - await hass.async_block_till_done() + mock_debouncer.clear() + await mqtt.async_subscribe(hass, "homeassistant/some-topic", record_calls) + # Wait for discovery cooldown + await mock_debouncer.wait() mqtt_client_mock.subscribe.assert_called() @@ -2487,7 +2446,6 @@ async def test_delayed_birth_message( entry.add_to_hass(hass) hass.config.components.add(mqtt.DOMAIN) assert await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() @callback def wait_birth(msg: ReceiveMessage) -> None: @@ -2495,7 +2453,6 @@ async def test_delayed_birth_message( birth.set() await mqtt.async_subscribe(hass, "homeassistant/status", wait_birth) - await hass.async_block_till_done() with pytest.raises(TimeoutError): await asyncio.wait_for(birth.wait(), 0.05) assert not mqtt_client_mock.publish.called @@ -2595,26 +2552,27 @@ async def test_no_will_message( ) async def test_mqtt_subscribes_topics_on_connect( hass: HomeAssistant, + mock_debouncer: asyncio.Event, setup_with_birth_msg_client_mock: MqttMockPahoClient, record_calls: MessageCallbackType, ) -> None: """Test subscription to topic on connect.""" mqtt_client_mock = setup_with_birth_msg_client_mock + mock_debouncer.clear() await mqtt.async_subscribe(hass, "topic/test", record_calls) await mqtt.async_subscribe(hass, "home/sensor", record_calls, 2) await mqtt.async_subscribe(hass, "still/pending", record_calls) await mqtt.async_subscribe(hass, "still/pending", record_calls, 1) + await mock_debouncer.wait() mqtt_client_mock.on_disconnect(Mock(), None, 0) mqtt_client_mock.reset_mock() + mock_debouncer.clear() mqtt_client_mock.on_connect(Mock(), None, 0, 0) - - await hass.async_block_till_done() - async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) - await hass.async_block_till_done(wait_background_tasks=True) + await mock_debouncer.wait() subscribe_calls = help_all_subscribe_calls(mqtt_client_mock) assert ("topic/test", 0) in subscribe_calls @@ -2628,17 +2586,18 @@ async def test_mqtt_subscribes_topics_on_connect( ) async def test_mqtt_subscribes_in_single_call( hass: HomeAssistant, + mock_debouncer: asyncio.Event, setup_with_birth_msg_client_mock: MqttMockPahoClient, record_calls: MessageCallbackType, ) -> None: """Test bundled client subscription to topic.""" mqtt_client_mock = setup_with_birth_msg_client_mock mqtt_client_mock.subscribe.reset_mock() + mock_debouncer.clear() await mqtt.async_subscribe(hass, "topic/test", record_calls) await mqtt.async_subscribe(hass, "home/sensor", record_calls) # Make sure the debouncer finishes - await asyncio.sleep(0) - await hass.async_block_till_done(wait_background_tasks=True) + await mock_debouncer.wait() assert mqtt_client_mock.subscribe.call_count == 1 # Assert we have a single subscription call with both subscriptions @@ -2653,6 +2612,7 @@ async def test_mqtt_subscribes_in_single_call( @patch("homeassistant.components.mqtt.client.MAX_UNSUBSCRIBES_PER_CALL", 2) async def test_mqtt_subscribes_and_unsubscribes_in_chunks( hass: HomeAssistant, + mock_debouncer: asyncio.Event, setup_with_birth_msg_client_mock: MqttMockPahoClient, record_calls: MessageCallbackType, ) -> None: @@ -2661,13 +2621,13 @@ async def test_mqtt_subscribes_and_unsubscribes_in_chunks( mqtt_client_mock.subscribe.reset_mock() unsub_tasks: list[CALLBACK_TYPE] = [] + mock_debouncer.clear() unsub_tasks.append(await mqtt.async_subscribe(hass, "topic/test1", record_calls)) unsub_tasks.append(await mqtt.async_subscribe(hass, "home/sensor1", record_calls)) unsub_tasks.append(await mqtt.async_subscribe(hass, "topic/test2", record_calls)) unsub_tasks.append(await mqtt.async_subscribe(hass, "home/sensor2", record_calls)) # Make sure the debouncer finishes - await asyncio.sleep(0.1) - await hass.async_block_till_done(wait_background_tasks=True) + await mock_debouncer.wait() assert mqtt_client_mock.subscribe.call_count == 2 # Assert we have a 2 subscription calls with both 2 subscriptions @@ -2675,12 +2635,11 @@ async def test_mqtt_subscribes_and_unsubscribes_in_chunks( assert len(mqtt_client_mock.subscribe.mock_calls[1][1][0]) == 2 # Unsubscribe all topics + mock_debouncer.clear() for task in unsub_tasks: task() - await hass.async_block_till_done() # Make sure the debouncer finishes - await asyncio.sleep(0.1) - await hass.async_block_till_done(wait_background_tasks=True) + await mock_debouncer.wait() assert mqtt_client_mock.unsubscribe.call_count == 2 # Assert we have a 2 unsubscribe calls with both 2 topic @@ -2748,6 +2707,7 @@ async def test_message_callback_exception_gets_logged( async def test_message_partial_callback_exception_gets_logged( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, + mock_debouncer: asyncio.Event, setup_with_birth_msg_client_mock: MqttMockPahoClient, ) -> None: """Test exception raised by message handler.""" @@ -2765,15 +2725,13 @@ async def test_message_partial_callback_exception_gets_logged( """Partial callback handler.""" msg_callback(msg) + mock_debouncer.clear() await mqtt.async_subscribe( hass, "test-topic", partial(parial_handler, bad_handler, {"some_attr"}) ) - await hass.async_block_till_done(wait_background_tasks=True) + await mock_debouncer.wait() async_fire_mqtt_message(hass, "test-topic", "test") await hass.async_block_till_done() - await hass.async_block_till_done() - await asyncio.sleep(0) - await hass.async_block_till_done(wait_background_tasks=True) assert ( "Exception in bad_handler when handling msg on 'test-topic':" @@ -3500,6 +3458,7 @@ async def test_publish_json_from_template( async def test_subscribe_connection_status( hass: HomeAssistant, + mock_debouncer: asyncio.Event, setup_with_birth_msg_client_mock: MqttMockPahoClient, ) -> None: """Test connextion status subscription.""" @@ -3533,8 +3492,9 @@ async def test_subscribe_connection_status( await hass.async_block_till_done() # Mock connect status + mock_debouncer.clear() mqtt_client_mock.on_connect(None, None, 0, 0) - await hass.async_block_till_done() + await mock_debouncer.wait() assert mqtt.is_connected(hass) is True # Mock disconnect status @@ -3547,9 +3507,9 @@ async def test_subscribe_connection_status( unsub_async() # Mock connect status + mock_debouncer.clear() mqtt_client_mock.on_connect(None, None, 0, 0) - await asyncio.sleep(0) - await hass.async_block_till_done() + await mock_debouncer.wait() assert mqtt.is_connected(hass) is True # Check calls @@ -3584,7 +3544,7 @@ async def test_unload_config_entry( new_mqtt_config_entry = mqtt_config_entry mqtt_client_mock.publish.assert_any_call("just_in_time", "published", 0, False) assert new_mqtt_config_entry.state is ConfigEntryState.NOT_LOADED - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert not hass.services.has_service(mqtt.DOMAIN, "dump") assert not hass.services.has_service(mqtt.DOMAIN, "publish") assert "No ACK from MQTT server" not in caplog.text @@ -4236,6 +4196,7 @@ async def test_auto_reconnect( async def test_server_sock_connect_and_disconnect( hass: HomeAssistant, + mock_debouncer: asyncio.Event, setup_with_birth_msg_client_mock: MqttMockPahoClient, recorded_calls: list[ReceiveMessage], record_calls: MessageCallbackType, @@ -4257,14 +4218,19 @@ async def test_server_sock_connect_and_disconnect( server.close() # mock the server closing the connection on us + mock_debouncer.clear() unsub = await mqtt.async_subscribe(hass, "test-topic", record_calls) + await mock_debouncer.wait() mqtt_client_mock.loop_misc.return_value = paho_mqtt.MQTT_ERR_CONN_LOST mqtt_client_mock.on_socket_unregister_write(mqtt_client_mock, None, client) mqtt_client_mock.on_socket_close(mqtt_client_mock, None, client) mqtt_client_mock.on_disconnect(mqtt_client_mock, None, client) await hass.async_block_till_done() + mock_debouncer.clear() unsub() + await hass.async_block_till_done() + assert not mock_debouncer.is_set() # Should have failed assert len(recorded_calls) == 0 diff --git a/tests/components/mqtt/test_util.py b/tests/components/mqtt/test_util.py index 955fc88448c..a3802de69da 100644 --- a/tests/components/mqtt/test_util.py +++ b/tests/components/mqtt/test_util.py @@ -26,15 +26,15 @@ from tests.typing import MqttMockHAClient, MqttMockPahoClient async def test_canceling_debouncer_on_shutdown( hass: HomeAssistant, record_calls: MessageCallbackType, + mock_debouncer: asyncio.Event, setup_with_birth_msg_client_mock: MqttMockPahoClient, ) -> None: """Test canceling the debouncer when HA shuts down.""" mqtt_client_mock = setup_with_birth_msg_client_mock - + # Mock we are past initial setup + await mock_debouncer.wait() with patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 2): - await hass.async_block_till_done() - async_fire_time_changed(hass, utcnow() + timedelta(seconds=5)) - await hass.async_block_till_done() + mock_debouncer.clear() await mqtt.async_subscribe(hass, "test/state1", record_calls) async_fire_time_changed(hass, utcnow() + timedelta(seconds=0.1)) # Stop HA so the scheduled debouncer task will be canceled @@ -47,9 +47,10 @@ async def test_canceling_debouncer_on_shutdown( await mqtt.async_subscribe(hass, "test/state4", record_calls) async_fire_time_changed(hass, utcnow() + timedelta(seconds=0.1)) await mqtt.async_subscribe(hass, "test/state5", record_calls) - async_fire_time_changed(hass, utcnow() + timedelta(seconds=0.1)) - await hass.async_block_till_done() - + async_fire_time_changed(hass, utcnow() + timedelta(seconds=5)) + await hass.async_block_till_done(wait_background_tasks=True) + # Assert the debouncer subscribe job was not executed + assert not mock_debouncer.is_set() mqtt_client_mock.subscribe.assert_not_called() # Note thet the broker connection will not be disconnected gracefully From bcd1243686e565d8c49e90d971fbfc442f9262ca Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 25 Jun 2024 15:15:59 +0200 Subject: [PATCH 1167/1445] Use VolDictType to improve schema typing (#120417) --- .../components/asuswrt/config_flow.py | 2 ++ homeassistant/components/axis/config_flow.py | 3 ++- .../components/dlna_dmr/config_flow.py | 3 ++- .../components/ecovacs/config_flow.py | 3 ++- .../components/enphase_envoy/config_flow.py | 3 ++- .../homekit_controller/config_flow.py | 3 ++- .../components/http/data_validator.py | 6 +++++- .../components/humidifier/device_action.py | 4 ++-- .../components/ibeacon/config_flow.py | 3 ++- homeassistant/components/knx/config_flow.py | 6 +++--- .../components/light/device_action.py | 4 ++-- .../components/mysensors/config_flow.py | 18 +++++++++--------- .../components/nmap_tracker/config_flow.py | 3 ++- homeassistant/components/opower/config_flow.py | 3 ++- .../components/proximity/config_flow.py | 3 ++- homeassistant/components/rfxtrx/config_flow.py | 4 +++- .../components/synology_dsm/config_flow.py | 8 +++----- homeassistant/components/tplink/light.py | 9 +++++---- .../components/zwave_js/config_flow.py | 3 ++- 19 files changed, 54 insertions(+), 37 deletions(-) diff --git a/homeassistant/components/asuswrt/config_flow.py b/homeassistant/components/asuswrt/config_flow.py index f5db3dfa3d8..d58a216aaee 100644 --- a/homeassistant/components/asuswrt/config_flow.py +++ b/homeassistant/components/asuswrt/config_flow.py @@ -32,6 +32,7 @@ from homeassistant.helpers.schema_config_entry_flow import ( SchemaOptionsFlowHandler, ) from homeassistant.helpers.selector import SelectSelector, SelectSelectorConfig +from homeassistant.helpers.typing import VolDictType from .bridge import AsusWrtBridge from .const import ( @@ -143,6 +144,7 @@ class AsusWrtFlowHandler(ConfigFlow, domain=DOMAIN): user_input = self._config_data + add_schema: VolDictType if self.show_advanced_options: add_schema = { vol.Exclusive(CONF_PASSWORD, PASS_KEY, PASS_KEY_MSG): str, diff --git a/homeassistant/components/axis/config_flow.py b/homeassistant/components/axis/config_flow.py index 1754e37853f..63cac941423 100644 --- a/homeassistant/components/axis/config_flow.py +++ b/homeassistant/components/axis/config_flow.py @@ -30,6 +30,7 @@ from homeassistant.const import ( ) from homeassistant.core import callback from homeassistant.helpers.device_registry import format_mac +from homeassistant.helpers.typing import VolDictType from homeassistant.util.network import is_link_local from . import AxisConfigEntry @@ -63,7 +64,7 @@ class AxisFlowHandler(ConfigFlow, domain=AXIS_DOMAIN): def __init__(self) -> None: """Initialize the Axis config flow.""" self.config: dict[str, Any] = {} - self.discovery_schema: dict[vol.Required, type[str | int]] | None = None + self.discovery_schema: VolDictType | None = None async def async_step_user( self, user_input: dict[str, Any] | None = None diff --git a/homeassistant/components/dlna_dmr/config_flow.py b/homeassistant/components/dlna_dmr/config_flow.py index 6b551f0e999..265c78fd9a9 100644 --- a/homeassistant/components/dlna_dmr/config_flow.py +++ b/homeassistant/components/dlna_dmr/config_flow.py @@ -27,6 +27,7 @@ from homeassistant.const import CONF_DEVICE_ID, CONF_HOST, CONF_MAC, CONF_TYPE, from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import IntegrationError from homeassistant.helpers import config_validation as cv, device_registry as dr +from homeassistant.helpers.typing import VolDictType from .const import ( CONF_BROWSE_UNFILTERED, @@ -382,7 +383,7 @@ class DlnaDmrOptionsFlowHandler(OptionsFlow): if not errors: return self.async_create_entry(title="", data=options) - fields = {} + fields: VolDictType = {} def _add_with_suggestion(key: str, validator: Callable | type[bool]) -> None: """Add a field to with a suggested value. diff --git a/homeassistant/components/ecovacs/config_flow.py b/homeassistant/components/ecovacs/config_flow.py index 7e4bfbe5597..a254731a946 100644 --- a/homeassistant/components/ecovacs/config_flow.py +++ b/homeassistant/components/ecovacs/config_flow.py @@ -22,6 +22,7 @@ from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant from homeassistant.data_entry_flow import AbortFlow from homeassistant.helpers import aiohttp_client, selector from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue +from homeassistant.helpers.typing import VolDictType from homeassistant.loader import async_get_issue_tracker from homeassistant.util.ssl import get_default_no_verify_context @@ -181,7 +182,7 @@ class EcovacsConfigFlow(ConfigFlow, domain=DOMAIN): title=user_input[CONF_USERNAME], data=user_input ) - schema = { + schema: VolDictType = { vol.Required(CONF_USERNAME): selector.TextSelector( selector.TextSelectorConfig(type=selector.TextSelectorType.TEXT) ), diff --git a/homeassistant/components/enphase_envoy/config_flow.py b/homeassistant/components/enphase_envoy/config_flow.py index 695709627b7..c18401859de 100644 --- a/homeassistant/components/enphase_envoy/config_flow.py +++ b/homeassistant/components/enphase_envoy/config_flow.py @@ -21,6 +21,7 @@ from homeassistant.config_entries import ( from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.httpx_client import get_async_client +from homeassistant.helpers.typing import VolDictType from .const import ( DOMAIN, @@ -69,7 +70,7 @@ class EnphaseConfigFlow(ConfigFlow, domain=DOMAIN): @callback def _async_generate_schema(self) -> vol.Schema: """Generate schema.""" - schema = {} + schema: VolDictType = {} if self.ip_address: schema[vol.Required(CONF_HOST, default=self.ip_address)] = vol.In( diff --git a/homeassistant/components/homekit_controller/config_flow.py b/homeassistant/components/homekit_controller/config_flow.py index 48aa3fc2bc7..2ca32ccb911 100644 --- a/homeassistant/components/homekit_controller/config_flow.py +++ b/homeassistant/components/homekit_controller/config_flow.py @@ -24,6 +24,7 @@ from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.core import callback from homeassistant.data_entry_flow import AbortFlow from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.typing import VolDictType from .const import DOMAIN, KNOWN_DEVICES from .storage import async_get_entity_storage @@ -555,7 +556,7 @@ class HomekitControllerFlowHandler(ConfigFlow, domain=DOMAIN): "category": formatted_category(self.category), } - schema = {vol.Required("pairing_code"): vol.All(str, vol.Strip)} + schema: VolDictType = {vol.Required("pairing_code"): vol.All(str, vol.Strip)} if errors and errors.get("pairing_code") == "insecure_setup_code": schema[vol.Optional("allow_insecure_setup_codes")] = bool diff --git a/homeassistant/components/http/data_validator.py b/homeassistant/components/http/data_validator.py index b2f6496a77b..abfeadc7189 100644 --- a/homeassistant/components/http/data_validator.py +++ b/homeassistant/components/http/data_validator.py @@ -11,6 +11,8 @@ from typing import Any, Concatenate from aiohttp import web import voluptuous as vol +from homeassistant.helpers.typing import VolDictType + from .view import HomeAssistantView _LOGGER = logging.getLogger(__name__) @@ -25,7 +27,9 @@ class RequestDataValidator: Will return a 400 if no JSON provided or doesn't match schema. """ - def __init__(self, schema: vol.Schema, allow_empty: bool = False) -> None: + def __init__( + self, schema: VolDictType | vol.Schema, allow_empty: bool = False + ) -> None: """Initialize the decorator.""" if isinstance(schema, dict): schema = vol.Schema(schema) diff --git a/homeassistant/components/humidifier/device_action.py b/homeassistant/components/humidifier/device_action.py index 74ef73443d6..de1d4c871e3 100644 --- a/homeassistant/components/humidifier/device_action.py +++ b/homeassistant/components/humidifier/device_action.py @@ -22,7 +22,7 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import get_capability, get_supported_features -from homeassistant.helpers.typing import ConfigType, TemplateVarsType +from homeassistant.helpers.typing import ConfigType, TemplateVarsType, VolDictType from . import DOMAIN, const @@ -114,7 +114,7 @@ async def async_get_action_capabilities( """List action capabilities.""" action_type = config[CONF_TYPE] - fields = {} + fields: VolDictType = {} if action_type == "set_humidity": fields[vol.Required(const.ATTR_HUMIDITY)] = vol.Coerce(int) diff --git a/homeassistant/components/ibeacon/config_flow.py b/homeassistant/components/ibeacon/config_flow.py index ccedaa675b6..424befa81ec 100644 --- a/homeassistant/components/ibeacon/config_flow.py +++ b/homeassistant/components/ibeacon/config_flow.py @@ -16,6 +16,7 @@ from homeassistant.config_entries import ( ) from homeassistant.core import callback import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.typing import VolDictType from .const import CONF_ALLOW_NAMELESS_UUIDS, DOMAIN @@ -81,7 +82,7 @@ class IBeaconOptionsFlow(OptionsFlow): data = {CONF_ALLOW_NAMELESS_UUIDS: list(updated_uuids)} return self.async_create_entry(title="", data=data) - schema = { + schema: VolDictType = { vol.Optional( "new_uuid", description={"suggested_value": new_uuid}, diff --git a/homeassistant/components/knx/config_flow.py b/homeassistant/components/knx/config_flow.py index c526a1e25f6..226abc1b868 100644 --- a/homeassistant/components/knx/config_flow.py +++ b/homeassistant/components/knx/config_flow.py @@ -29,7 +29,7 @@ from homeassistant.config_entries import ( from homeassistant.const import CONF_HOST, CONF_PORT from homeassistant.core import callback from homeassistant.helpers import selector -from homeassistant.helpers.typing import UNDEFINED +from homeassistant.helpers.typing import UNDEFINED, VolDictType from .const import ( CONF_KNX_AUTOMATIC, @@ -368,7 +368,7 @@ class KNXCommonFlow(ABC, ConfigEntryBaseFlow): CONF_KNX_ROUTE_BACK, not bool(self._selected_tunnel) ) - fields = { + fields: VolDictType = { vol.Required(CONF_KNX_TUNNELING_TYPE, default=default_type): vol.In( CONF_KNX_TUNNELING_TYPE_LABELS ), @@ -694,7 +694,7 @@ class KNXCommonFlow(ABC, ConfigEntryBaseFlow): router for router in routers if router.routing_requires_secure ) - fields = { + fields: VolDictType = { vol.Required( CONF_KNX_INDIVIDUAL_ADDRESS, default=_individual_address ): _IA_SELECTOR, diff --git a/homeassistant/components/light/device_action.py b/homeassistant/components/light/device_action.py index dbdf7200a7b..45e9731c5b8 100644 --- a/homeassistant/components/light/device_action.py +++ b/homeassistant/components/light/device_action.py @@ -21,7 +21,7 @@ from homeassistant.core import Context, HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv, entity_registry as er from homeassistant.helpers.entity import get_supported_features -from homeassistant.helpers.typing import ConfigType, TemplateVarsType +from homeassistant.helpers.typing import ConfigType, TemplateVarsType, VolDictType from . import ( ATTR_BRIGHTNESS_PCT, @@ -150,7 +150,7 @@ async def async_get_action_capabilities( supported_color_modes = None supported_features = 0 - extra_fields = {} + extra_fields: VolDictType = {} if brightness_supported(supported_color_modes): extra_fields[vol.Optional(ATTR_BRIGHTNESS_PCT)] = VALID_BRIGHTNESS_PCT diff --git a/homeassistant/components/mysensors/config_flow.py b/homeassistant/components/mysensors/config_flow.py index 9a8d79ca3a7..f3fb03ffac8 100644 --- a/homeassistant/components/mysensors/config_flow.py +++ b/homeassistant/components/mysensors/config_flow.py @@ -22,6 +22,7 @@ from homeassistant.const import CONF_DEVICE from homeassistant.core import callback from homeassistant.helpers import selector import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.typing import VolDictType from .const import ( CONF_BAUD_RATE, @@ -153,7 +154,7 @@ class MySensorsConfigFlowHandler(ConfigFlow, domain=DOMAIN): return self._async_create_entry(user_input) user_input = user_input or {} - schema = { + schema: VolDictType = { vol.Required( CONF_DEVICE, default=user_input.get(CONF_DEVICE, "/dev/ttyACM0") ): str, @@ -164,9 +165,8 @@ class MySensorsConfigFlowHandler(ConfigFlow, domain=DOMAIN): } schema.update(_get_schema_common(user_input)) - schema = vol.Schema(schema) return self.async_show_form( - step_id="gw_serial", data_schema=schema, errors=errors + step_id="gw_serial", data_schema=vol.Schema(schema), errors=errors ) async def async_step_gw_tcp( @@ -182,7 +182,7 @@ class MySensorsConfigFlowHandler(ConfigFlow, domain=DOMAIN): return self._async_create_entry(user_input) user_input = user_input or {} - schema = { + schema: VolDictType = { vol.Required( CONF_DEVICE, default=user_input.get(CONF_DEVICE, "127.0.0.1") ): str, @@ -192,8 +192,9 @@ class MySensorsConfigFlowHandler(ConfigFlow, domain=DOMAIN): } schema.update(_get_schema_common(user_input)) - schema = vol.Schema(schema) - return self.async_show_form(step_id="gw_tcp", data_schema=schema, errors=errors) + return self.async_show_form( + step_id="gw_tcp", data_schema=vol.Schema(schema), errors=errors + ) def _check_topic_exists(self, topic: str) -> bool: for other_config in self._async_current_entries(): @@ -243,7 +244,7 @@ class MySensorsConfigFlowHandler(ConfigFlow, domain=DOMAIN): return self._async_create_entry(user_input) user_input = user_input or {} - schema = { + schema: VolDictType = { vol.Required( CONF_TOPIC_IN_PREFIX, default=user_input.get(CONF_TOPIC_IN_PREFIX, "") ): str, @@ -254,9 +255,8 @@ class MySensorsConfigFlowHandler(ConfigFlow, domain=DOMAIN): } schema.update(_get_schema_common(user_input)) - schema = vol.Schema(schema) return self.async_show_form( - step_id="gw_mqtt", data_schema=schema, errors=errors + step_id="gw_mqtt", data_schema=vol.Schema(schema), errors=errors ) @callback diff --git a/homeassistant/components/nmap_tracker/config_flow.py b/homeassistant/components/nmap_tracker/config_flow.py index a89c50a2210..b724dca1a81 100644 --- a/homeassistant/components/nmap_tracker/config_flow.py +++ b/homeassistant/components/nmap_tracker/config_flow.py @@ -23,6 +23,7 @@ from homeassistant.config_entries import ( from homeassistant.const import CONF_EXCLUDE, CONF_HOSTS from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.typing import VolDictType from .const import ( CONF_HOME_INTERVAL, @@ -110,7 +111,7 @@ async def _async_build_schema_with_user_input( exclude = user_input.get( CONF_EXCLUDE, await network.async_get_source_ip(hass, MDNS_TARGET_IP) ) - schema = { + schema: VolDictType = { vol.Required(CONF_HOSTS, default=hosts): str, vol.Required( CONF_HOME_INTERVAL, default=user_input.get(CONF_HOME_INTERVAL, 0) diff --git a/homeassistant/components/opower/config_flow.py b/homeassistant/components/opower/config_flow.py index bbd9315eaa3..574062aca52 100644 --- a/homeassistant/components/opower/config_flow.py +++ b/homeassistant/components/opower/config_flow.py @@ -20,6 +20,7 @@ from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResu from homeassistant.const import CONF_NAME, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.aiohttp_client import async_create_clientsession +from homeassistant.helpers.typing import VolDictType from .const import CONF_TOTP_SECRET, CONF_UTILITY, DOMAIN @@ -151,7 +152,7 @@ class OpowerConfigFlow(ConfigFlow, domain=DOMAIN): ) await self.hass.config_entries.async_reload(self.reauth_entry.entry_id) return self.async_abort(reason="reauth_successful") - schema = { + schema: VolDictType = { vol.Required(CONF_USERNAME): self.reauth_entry.data[CONF_USERNAME], vol.Required(CONF_PASSWORD): str, } diff --git a/homeassistant/components/proximity/config_flow.py b/homeassistant/components/proximity/config_flow.py index 14f26f5d45d..d133b14cb6a 100644 --- a/homeassistant/components/proximity/config_flow.py +++ b/homeassistant/components/proximity/config_flow.py @@ -23,6 +23,7 @@ from homeassistant.helpers.selector import ( NumberSelector, NumberSelectorConfig, ) +from homeassistant.helpers.typing import VolDictType from homeassistant.util import slugify from .const import ( @@ -37,7 +38,7 @@ from .const import ( RESULT_SUCCESS = "success" -def _base_schema(user_input: dict[str, Any]) -> vol.Schema: +def _base_schema(user_input: dict[str, Any]) -> VolDictType: return { vol.Required( CONF_TRACKED_ENTITIES, default=user_input.get(CONF_TRACKED_ENTITIES, []) diff --git a/homeassistant/components/rfxtrx/config_flow.py b/homeassistant/components/rfxtrx/config_flow.py index 1fbb2e8fc29..ceb9bea4661 100644 --- a/homeassistant/components/rfxtrx/config_flow.py +++ b/homeassistant/components/rfxtrx/config_flow.py @@ -38,6 +38,7 @@ from homeassistant.helpers import ( entity_registry as er, ) from homeassistant.helpers.event import async_track_state_change_event +from homeassistant.helpers.typing import VolDictType from . import ( DOMAIN, @@ -245,9 +246,10 @@ class RfxtrxOptionsFlow(OptionsFlow): device_data = self._selected_device - data_schema = {} + data_schema: VolDictType = {} if binary_supported(self._selected_device_object): + off_delay_schema: VolDictType if device_data.get(CONF_OFF_DELAY): off_delay_schema = { vol.Optional( diff --git a/homeassistant/components/synology_dsm/config_flow.py b/homeassistant/components/synology_dsm/config_flow.py index 63ff804951c..6e2b090fc98 100644 --- a/homeassistant/components/synology_dsm/config_flow.py +++ b/homeassistant/components/synology_dsm/config_flow.py @@ -41,7 +41,7 @@ from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.typing import DiscoveryInfoType +from homeassistant.helpers.typing import DiscoveryInfoType, VolDictType from homeassistant.util.network import is_ip_address as is_ip from .const import ( @@ -79,7 +79,7 @@ def _reauth_schema() -> vol.Schema: def _user_schema_with_defaults(user_input: dict[str, Any]) -> vol.Schema: - user_schema = { + user_schema: VolDictType = { vol.Required(CONF_HOST, default=user_input.get(CONF_HOST, "")): str, } user_schema.update(_ordered_shared_schema(user_input)) @@ -87,9 +87,7 @@ def _user_schema_with_defaults(user_input: dict[str, Any]) -> vol.Schema: return vol.Schema(user_schema) -def _ordered_shared_schema( - schema_input: dict[str, Any], -) -> dict[vol.Required | vol.Optional, Any]: +def _ordered_shared_schema(schema_input: dict[str, Any]) -> VolDictType: return { vol.Required(CONF_USERNAME, default=schema_input.get(CONF_USERNAME, "")): str, vol.Required(CONF_PASSWORD, default=schema_input.get(CONF_PASSWORD, "")): str, diff --git a/homeassistant/components/tplink/light.py b/homeassistant/components/tplink/light.py index aa50c3f2ed2..977e75215aa 100644 --- a/homeassistant/components/tplink/light.py +++ b/homeassistant/components/tplink/light.py @@ -4,7 +4,7 @@ from __future__ import annotations from collections.abc import Sequence import logging -from typing import Any, Final, cast +from typing import Any, cast from kasa import SmartBulb, SmartLightStrip import voluptuous as vol @@ -25,6 +25,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_platform import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import VolDictType from . import legacy_device_id from .const import DOMAIN @@ -43,7 +44,7 @@ VAL = vol.Range(min=0, max=100) TRANSITION = vol.Range(min=0, max=6000) HSV_SEQUENCE = vol.ExactSequence((HUE, SAT, VAL)) -BASE_EFFECT_DICT: Final = { +BASE_EFFECT_DICT: VolDictType = { vol.Optional("brightness", default=100): vol.All( vol.Coerce(int), vol.Range(min=0, max=100) ), @@ -58,7 +59,7 @@ BASE_EFFECT_DICT: Final = { ), } -SEQUENCE_EFFECT_DICT: Final = { +SEQUENCE_EFFECT_DICT: VolDictType = { **BASE_EFFECT_DICT, vol.Required("sequence"): vol.All( cv.ensure_list, @@ -76,7 +77,7 @@ SEQUENCE_EFFECT_DICT: Final = { ), } -RANDOM_EFFECT_DICT: Final = { +RANDOM_EFFECT_DICT: VolDictType = { **BASE_EFFECT_DICT, vol.Optional("fadeoff", default=0): vol.All( vol.Coerce(int), vol.Range(min=0, max=3000) diff --git a/homeassistant/components/zwave_js/config_flow.py b/homeassistant/components/zwave_js/config_flow.py index dff582558b1..e73fa9fc3a7 100644 --- a/homeassistant/components/zwave_js/config_flow.py +++ b/homeassistant/components/zwave_js/config_flow.py @@ -38,6 +38,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.data_entry_flow import AbortFlow, FlowManager from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.typing import VolDictType from . import disconnect_client from .addon import get_addon_manager @@ -639,7 +640,7 @@ class ZWaveJSConfigFlow(BaseZwaveJSFlow, ConfigFlow, domain=DOMAIN): CONF_ADDON_LR_S2_AUTHENTICATED_KEY, self.lr_s2_authenticated_key or "" ) - schema = { + schema: VolDictType = { vol.Optional(CONF_S0_LEGACY_KEY, default=s0_legacy_key): str, vol.Optional( CONF_S2_ACCESS_CONTROL_KEY, default=s2_access_control_key From f934fea754a8c27fe456648b27d80d8dad5d305f Mon Sep 17 00:00:00 2001 From: Aaron Godfrey Date: Tue, 25 Jun 2024 06:34:54 -0700 Subject: [PATCH 1168/1445] Apply all todoist custom project filters for calendar events (#117454) Co-authored-by: Robert Resch --- homeassistant/components/todoist/calendar.py | 14 ++-- tests/components/todoist/test_calendar.py | 67 ++++++++++++++++++++ 2 files changed, 76 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/todoist/calendar.py b/homeassistant/components/todoist/calendar.py index e3f87043e02..baa7103f7eb 100644 --- a/homeassistant/components/todoist/calendar.py +++ b/homeassistant/components/todoist/calendar.py @@ -436,7 +436,7 @@ class TodoistProjectData: self._coordinator = coordinator self._name = project_data[CONF_NAME] - # If no ID is defined, fetch all tasks. + # If no ID is defined, this is a custom project. self._id = project_data.get(CONF_ID) # All labels the user has defined, for easy lookup. @@ -497,6 +497,13 @@ class TodoistProjectData: SUMMARY: data.content, } + if ( + self._project_id_whitelist + and data.project_id not in self._project_id_whitelist + ): + # Project isn't in `include_projects` filter. + return None + # All task Labels (optional parameter). task[LABELS] = [ label.name for label in self._labels if label.name in data.labels @@ -625,10 +632,7 @@ class TodoistProjectData: tasks = self._coordinator.data if self._id is None: project_task_data = [ - task - for task in tasks - if not self._project_id_whitelist - or task.project_id in self._project_id_whitelist + task for task in tasks if self.create_todoist_task(task) is not None ] else: project_task_data = [task for task in tasks if task.project_id == self._id] diff --git a/tests/components/todoist/test_calendar.py b/tests/components/todoist/test_calendar.py index 8ba4da9b2e8..d8123af3231 100644 --- a/tests/components/todoist/test_calendar.py +++ b/tests/components/todoist/test_calendar.py @@ -366,6 +366,73 @@ async def test_task_due_datetime( assert await response.json() == [] +@pytest.mark.parametrize( + ("todoist_config", "due", "start", "end", "expected_response"), + [ + ( + {"custom_projects": [{"name": "Test", "labels": ["Label1"]}]}, + Due(date="2023-03-30", is_recurring=False, string="Mar 30"), + "2023-03-28T00:00:00.000Z", + "2023-04-01T00:00:00.000Z", + [get_events_response({"date": "2023-03-30"}, {"date": "2023-03-31"})], + ), + ( + {"custom_projects": [{"name": "Test", "labels": ["custom"]}]}, + Due(date="2023-03-30", is_recurring=False, string="Mar 30"), + "2023-03-28T00:00:00.000Z", + "2023-04-01T00:00:00.000Z", + [], + ), + ( + {"custom_projects": [{"name": "Test", "include_projects": ["Name"]}]}, + Due(date="2023-03-30", is_recurring=False, string="Mar 30"), + "2023-03-28T00:00:00.000Z", + "2023-04-01T00:00:00.000Z", + [get_events_response({"date": "2023-03-30"}, {"date": "2023-03-31"})], + ), + ( + {"custom_projects": [{"name": "Test", "due_date_days": 1}]}, + Due(date="2023-03-30", is_recurring=False, string="Mar 30"), + "2023-03-28T00:00:00.000Z", + "2023-04-01T00:00:00.000Z", + [get_events_response({"date": "2023-03-30"}, {"date": "2023-03-31"})], + ), + ( + {"custom_projects": [{"name": "Test", "due_date_days": 1}]}, + Due( + date=(dt_util.now() + timedelta(days=2)).strftime("%Y-%m-%d"), + is_recurring=False, + string="Mar 30", + ), + dt_util.now().isoformat(), + (dt_util.now() + timedelta(days=5)).isoformat(), + [], + ), + ], + ids=[ + "in_labels_whitelist", + "not_in_labels_whitelist", + "in_include_projects", + "in_due_date_days", + "not_in_due_date_days", + ], +) +async def test_events_filtered_for_custom_projects( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + start: str, + end: str, + expected_response: dict[str, Any], +) -> None: + """Test we filter out tasks from custom projects based on their config.""" + client = await hass_client() + response = await client.get( + get_events_url("calendar.test", start, end), + ) + assert response.status == HTTPStatus.OK + assert await response.json() == expected_response + + @pytest.mark.parametrize( ("due", "setup_platform"), [ From edaa5c60a7108eca8956861539f4c3c6ab4be8aa Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 25 Jun 2024 17:00:03 +0200 Subject: [PATCH 1169/1445] Small cleanups to ESPHome (#120414) --- homeassistant/components/esphome/__init__.py | 14 ++++++++------ homeassistant/components/esphome/domain_data.py | 6 ------ homeassistant/components/esphome/manager.py | 3 +-- tests/components/esphome/test_config_flow.py | 9 +++------ 4 files changed, 12 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index 3af95576c18..b06fcd4bab0 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -25,6 +25,8 @@ from .manager import ESPHomeManager, cleanup_instance CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN) +CLIENT_INFO = f"Home Assistant {ha_version}" + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the esphome component.""" @@ -34,10 +36,10 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ESPHomeConfigEntry) -> bool: """Set up the esphome component.""" - host = entry.data[CONF_HOST] - port = entry.data[CONF_PORT] - password = entry.data[CONF_PASSWORD] - noise_psk = entry.data.get(CONF_NOISE_PSK) + host: str = entry.data[CONF_HOST] + port: int = entry.data[CONF_PORT] + password: str | None = entry.data[CONF_PASSWORD] + noise_psk: str | None = entry.data.get(CONF_NOISE_PSK) zeroconf_instance = await zeroconf.async_get_instance(hass) @@ -45,7 +47,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ESPHomeConfigEntry) -> b host, port, password, - client_info=f"Home Assistant {ha_version}", + client_info=CLIENT_INFO, zeroconf_instance=zeroconf_instance, noise_psk=noise_psk, ) @@ -61,7 +63,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ESPHomeConfigEntry) -> b entry.runtime_data = entry_data manager = ESPHomeManager( - hass, entry, host, password, cli, zeroconf_instance, domain_data, entry_data + hass, entry, host, password, cli, zeroconf_instance, domain_data ) await manager.async_start() diff --git a/homeassistant/components/esphome/domain_data.py b/homeassistant/components/esphome/domain_data.py index e9057ddfeaa..aa46469c40e 100644 --- a/homeassistant/components/esphome/domain_data.py +++ b/homeassistant/components/esphome/domain_data.py @@ -33,12 +33,6 @@ class DomainData: """ return entry.runtime_data - def set_entry_data( - self, entry: ESPHomeConfigEntry, entry_data: RuntimeEntryData - ) -> None: - """Set the runtime entry data associated with this config entry.""" - entry.runtime_data = entry_data - def get_or_create_store( self, hass: HomeAssistant, entry: ESPHomeConfigEntry ) -> ESPHomeStorage: diff --git a/homeassistant/components/esphome/manager.py b/homeassistant/components/esphome/manager.py index 870bd704ee4..5ab0265c1d4 100644 --- a/homeassistant/components/esphome/manager.py +++ b/homeassistant/components/esphome/manager.py @@ -164,7 +164,6 @@ class ESPHomeManager: cli: APIClient, zeroconf_instance: zeroconf.HaZeroconf, domain_data: DomainData, - entry_data: RuntimeEntryData, ) -> None: """Initialize the esphome manager.""" self.hass = hass @@ -177,7 +176,7 @@ class ESPHomeManager: self.voice_assistant_pipeline: VoiceAssistantPipeline | None = None self.reconnect_logic: ReconnectLogic | None = None self.zeroconf_instance = zeroconf_instance - self.entry_data = entry_data + self.entry_data = entry.runtime_data async def on_stop(self, event: Event) -> None: """Cleanup the socket client on HA close.""" diff --git a/tests/components/esphome/test_config_flow.py b/tests/components/esphome/test_config_flow.py index 9c61a5d0615..9a2b1f1a80e 100644 --- a/tests/components/esphome/test_config_flow.py +++ b/tests/components/esphome/test_config_flow.py @@ -2,7 +2,7 @@ from ipaddress import ip_address import json -from unittest.mock import AsyncMock, MagicMock, patch +from unittest.mock import AsyncMock, patch from aioesphomeapi import ( APIClient, @@ -18,7 +18,7 @@ import pytest from homeassistant import config_entries from homeassistant.components import dhcp, zeroconf -from homeassistant.components.esphome import DomainData, dashboard +from homeassistant.components.esphome import dashboard from homeassistant.components.esphome.const import ( CONF_ALLOW_SERVICE_CALLS, CONF_DEVICE_NAME, @@ -1136,10 +1136,7 @@ async def test_discovery_dhcp_no_changes( ) entry.add_to_hass(hass) - mock_entry_data = MagicMock() - mock_entry_data.device_info.name = "test8266" - domain_data = DomainData.get(hass) - domain_data.set_entry_data(entry, mock_entry_data) + mock_client.device_info = AsyncMock(return_value=DeviceInfo(name="test8266")) service_info = dhcp.DhcpServiceInfo( ip="192.168.43.183", From 49e6316c42c7dc6a5586e5e836346c9c3f6b4184 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 25 Jun 2024 17:00:26 +0200 Subject: [PATCH 1170/1445] Bump yalexs-ble to 2.4.3 (#120428) --- homeassistant/components/august/manifest.json | 2 +- homeassistant/components/yalexs_ble/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/august/manifest.json b/homeassistant/components/august/manifest.json index a8f087e3acc..f898ce64ce6 100644 --- a/homeassistant/components/august/manifest.json +++ b/homeassistant/components/august/manifest.json @@ -28,5 +28,5 @@ "documentation": "https://www.home-assistant.io/integrations/august", "iot_class": "cloud_push", "loggers": ["pubnub", "yalexs"], - "requirements": ["yalexs==6.4.1", "yalexs-ble==2.4.2"] + "requirements": ["yalexs==6.4.1", "yalexs-ble==2.4.3"] } diff --git a/homeassistant/components/yalexs_ble/manifest.json b/homeassistant/components/yalexs_ble/manifest.json index 0cf142b63b5..293ba87df86 100644 --- a/homeassistant/components/yalexs_ble/manifest.json +++ b/homeassistant/components/yalexs_ble/manifest.json @@ -12,5 +12,5 @@ "dependencies": ["bluetooth_adapters"], "documentation": "https://www.home-assistant.io/integrations/yalexs_ble", "iot_class": "local_push", - "requirements": ["yalexs-ble==2.4.2"] + "requirements": ["yalexs-ble==2.4.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index d1481147699..2b1b393d1f4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2936,7 +2936,7 @@ yalesmartalarmclient==0.3.9 # homeassistant.components.august # homeassistant.components.yalexs_ble -yalexs-ble==2.4.2 +yalexs-ble==2.4.3 # homeassistant.components.august yalexs==6.4.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3af1fa4184a..135581ff6ea 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2295,7 +2295,7 @@ yalesmartalarmclient==0.3.9 # homeassistant.components.august # homeassistant.components.yalexs_ble -yalexs-ble==2.4.2 +yalexs-ble==2.4.3 # homeassistant.components.august yalexs==6.4.1 From 4feca36ca60cff05211fe430743fa13e192cff7f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 25 Jun 2024 17:03:04 +0200 Subject: [PATCH 1171/1445] Refactor esphome platform setup to reduce boilerplate (#120415) --- .../components/esphome/alarm_control_panel.py | 30 +++++++----------- homeassistant/components/esphome/button.py | 30 +++++++----------- homeassistant/components/esphome/camera.py | 28 ++++++----------- homeassistant/components/esphome/climate.py | 29 ++++++----------- homeassistant/components/esphome/cover.py | 29 ++++++----------- homeassistant/components/esphome/date.py | 28 ++++++----------- homeassistant/components/esphome/datetime.py | 28 ++++++----------- homeassistant/components/esphome/event.py | 30 +++++++----------- homeassistant/components/esphome/fan.py | 29 ++++++----------- homeassistant/components/esphome/light.py | 30 ++++++------------ homeassistant/components/esphome/lock.py | 29 ++++++----------- .../components/esphome/media_player.py | 30 ++++++------------ homeassistant/components/esphome/number.py | 30 ++++++------------ homeassistant/components/esphome/switch.py | 29 ++++++----------- homeassistant/components/esphome/text.py | 31 +++++++------------ homeassistant/components/esphome/time.py | 28 ++++++----------- homeassistant/components/esphome/valve.py | 29 ++++++----------- 17 files changed, 170 insertions(+), 327 deletions(-) diff --git a/homeassistant/components/esphome/alarm_control_panel.py b/homeassistant/components/esphome/alarm_control_panel.py index 17079fe8c6a..64a0210f0f7 100644 --- a/homeassistant/components/esphome/alarm_control_panel.py +++ b/homeassistant/components/esphome/alarm_control_panel.py @@ -2,6 +2,8 @@ from __future__ import annotations +from functools import partial + from aioesphomeapi import ( AlarmControlPanelCommand, AlarmControlPanelEntityState, @@ -28,8 +30,7 @@ from homeassistant.const import ( STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED, ) -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.core import callback from .entity import ( EsphomeEntity, @@ -37,7 +38,6 @@ from .entity import ( esphome_state_property, platform_async_setup_entry, ) -from .entry_data import ESPHomeConfigEntry from .enum_mapper import EsphomeEnumMapper _ESPHOME_ACP_STATE_TO_HASS_STATE: EsphomeEnumMapper[AlarmControlPanelState, str] = ( @@ -69,22 +69,6 @@ class EspHomeACPFeatures(APIIntEnum): ARM_VACATION = 32 -async def async_setup_entry( - hass: HomeAssistant, - entry: ESPHomeConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up ESPHome switches based on a config entry.""" - await platform_async_setup_entry( - hass, - entry, - async_add_entities, - info_type=AlarmControlPanelInfo, - entity_type=EsphomeAlarmControlPanel, - state_type=AlarmControlPanelEntityState, - ) - - class EsphomeAlarmControlPanel( EsphomeEntity[AlarmControlPanelInfo, AlarmControlPanelEntityState], AlarmControlPanelEntity, @@ -169,3 +153,11 @@ class EsphomeAlarmControlPanel( self._client.alarm_control_panel_command( self._key, AlarmControlPanelCommand.TRIGGER, code ) + + +async_setup_entry = partial( + platform_async_setup_entry, + info_type=AlarmControlPanelInfo, + entity_type=EsphomeAlarmControlPanel, + state_type=AlarmControlPanelEntityState, +) diff --git a/homeassistant/components/esphome/button.py b/homeassistant/components/esphome/button.py index 8883c4b6bea..f13fa65ede1 100644 --- a/homeassistant/components/esphome/button.py +++ b/homeassistant/components/esphome/button.py @@ -2,11 +2,12 @@ from __future__ import annotations +from functools import partial + from aioesphomeapi import ButtonInfo, EntityInfo, EntityState from homeassistant.components.button import ButtonDeviceClass, ButtonEntity -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.core import callback from homeassistant.util.enum import try_parse_enum from .entity import ( @@ -14,23 +15,6 @@ from .entity import ( convert_api_error_ha_error, platform_async_setup_entry, ) -from .entry_data import ESPHomeConfigEntry - - -async def async_setup_entry( - hass: HomeAssistant, - entry: ESPHomeConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up ESPHome buttons based on a config entry.""" - await platform_async_setup_entry( - hass, - entry, - async_add_entities, - info_type=ButtonInfo, - entity_type=EsphomeButton, - state_type=EntityState, - ) class EsphomeButton(EsphomeEntity[ButtonInfo, EntityState], ButtonEntity): @@ -63,3 +47,11 @@ class EsphomeButton(EsphomeEntity[ButtonInfo, EntityState], ButtonEntity): async def async_press(self) -> None: """Press the button.""" self._client.button_command(self._key) + + +async_setup_entry = partial( + platform_async_setup_entry, + info_type=ButtonInfo, + entity_type=EsphomeButton, + state_type=EntityState, +) diff --git a/homeassistant/components/esphome/camera.py b/homeassistant/components/esphome/camera.py index abe7f6809e6..6038bf52e06 100644 --- a/homeassistant/components/esphome/camera.py +++ b/homeassistant/components/esphome/camera.py @@ -12,27 +12,9 @@ from aiohttp import web from homeassistant.components import camera from homeassistant.components.camera import Camera -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.core import callback from .entity import EsphomeEntity, platform_async_setup_entry -from .entry_data import ESPHomeConfigEntry - - -async def async_setup_entry( - hass: HomeAssistant, - entry: ESPHomeConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up esphome cameras based on a config entry.""" - await platform_async_setup_entry( - hass, - entry, - async_add_entities, - info_type=CameraInfo, - entity_type=EsphomeCamera, - state_type=CameraState, - ) class EsphomeCamera(Camera, EsphomeEntity[CameraInfo, CameraState]): @@ -95,3 +77,11 @@ class EsphomeCamera(Camera, EsphomeEntity[CameraInfo, CameraState]): return await camera.async_get_still_stream( request, stream_request, camera.DEFAULT_CONTENT_TYPE, 0.0 ) + + +async_setup_entry = partial( + platform_async_setup_entry, + info_type=CameraInfo, + entity_type=EsphomeCamera, + state_type=CameraState, +) diff --git a/homeassistant/components/esphome/climate.py b/homeassistant/components/esphome/climate.py index 6c82207ddc9..da1cdfb0eab 100644 --- a/homeassistant/components/esphome/climate.py +++ b/homeassistant/components/esphome/climate.py @@ -2,6 +2,7 @@ from __future__ import annotations +from functools import partial from typing import Any, cast from aioesphomeapi import ( @@ -52,8 +53,7 @@ from homeassistant.const import ( PRECISION_WHOLE, UnitOfTemperature, ) -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.core import callback from .entity import ( EsphomeEntity, @@ -61,28 +61,11 @@ from .entity import ( esphome_state_property, platform_async_setup_entry, ) -from .entry_data import ESPHomeConfigEntry from .enum_mapper import EsphomeEnumMapper FAN_QUIET = "quiet" -async def async_setup_entry( - hass: HomeAssistant, - entry: ESPHomeConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up ESPHome climate devices based on a config entry.""" - await platform_async_setup_entry( - hass, - entry, - async_add_entities, - info_type=ClimateInfo, - entity_type=EsphomeClimateEntity, - state_type=ClimateState, - ) - - _CLIMATE_MODES: EsphomeEnumMapper[ClimateMode, HVACMode] = EsphomeEnumMapper( { ClimateMode.OFF: HVACMode.OFF, @@ -335,3 +318,11 @@ class EsphomeClimateEntity(EsphomeEntity[ClimateInfo, ClimateState], ClimateEnti self._client.climate_command( key=self._key, swing_mode=_SWING_MODES.from_hass(swing_mode) ) + + +async_setup_entry = partial( + platform_async_setup_entry, + info_type=ClimateInfo, + entity_type=EsphomeClimateEntity, + state_type=ClimateState, +) diff --git a/homeassistant/components/esphome/cover.py b/homeassistant/components/esphome/cover.py index 4597b4f3566..19ce4cbf55a 100644 --- a/homeassistant/components/esphome/cover.py +++ b/homeassistant/components/esphome/cover.py @@ -2,6 +2,7 @@ from __future__ import annotations +from functools import partial from typing import Any from aioesphomeapi import APIVersion, CoverInfo, CoverOperation, CoverState, EntityInfo @@ -13,8 +14,7 @@ from homeassistant.components.cover import ( CoverEntity, CoverEntityFeature, ) -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.core import callback from homeassistant.util.enum import try_parse_enum from .entity import ( @@ -23,23 +23,6 @@ from .entity import ( esphome_state_property, platform_async_setup_entry, ) -from .entry_data import ESPHomeConfigEntry - - -async def async_setup_entry( - hass: HomeAssistant, - entry: ESPHomeConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up ESPHome covers based on a config entry.""" - await platform_async_setup_entry( - hass, - entry, - async_add_entities, - info_type=CoverInfo, - entity_type=EsphomeCover, - state_type=CoverState, - ) class EsphomeCover(EsphomeEntity[CoverInfo, CoverState], CoverEntity): @@ -137,3 +120,11 @@ class EsphomeCover(EsphomeEntity[CoverInfo, CoverState], CoverEntity): """Move the cover tilt to a specific position.""" tilt_position: int = kwargs[ATTR_TILT_POSITION] self._client.cover_command(key=self._key, tilt=tilt_position / 100) + + +async_setup_entry = partial( + platform_async_setup_entry, + info_type=CoverInfo, + entity_type=EsphomeCover, + state_type=CoverState, +) diff --git a/homeassistant/components/esphome/date.py b/homeassistant/components/esphome/date.py index eb26ec918d0..28bc532918a 100644 --- a/homeassistant/components/esphome/date.py +++ b/homeassistant/components/esphome/date.py @@ -3,31 +3,13 @@ from __future__ import annotations from datetime import date +from functools import partial from aioesphomeapi import DateInfo, DateState from homeassistant.components.date import DateEntity -from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity_platform import AddEntitiesCallback from .entity import EsphomeEntity, esphome_state_property, platform_async_setup_entry -from .entry_data import ESPHomeConfigEntry - - -async def async_setup_entry( - hass: HomeAssistant, - entry: ESPHomeConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up esphome dates based on a config entry.""" - await platform_async_setup_entry( - hass, - entry, - async_add_entities, - info_type=DateInfo, - entity_type=EsphomeDate, - state_type=DateState, - ) class EsphomeDate(EsphomeEntity[DateInfo, DateState], DateEntity): @@ -45,3 +27,11 @@ class EsphomeDate(EsphomeEntity[DateInfo, DateState], DateEntity): async def async_set_value(self, value: date) -> None: """Update the current date.""" self._client.date_command(self._key, value.year, value.month, value.day) + + +async_setup_entry = partial( + platform_async_setup_entry, + info_type=DateInfo, + entity_type=EsphomeDate, + state_type=DateState, +) diff --git a/homeassistant/components/esphome/datetime.py b/homeassistant/components/esphome/datetime.py index 5d578ae4928..20d0d651bba 100644 --- a/homeassistant/components/esphome/datetime.py +++ b/homeassistant/components/esphome/datetime.py @@ -3,32 +3,14 @@ from __future__ import annotations from datetime import datetime +from functools import partial from aioesphomeapi import DateTimeInfo, DateTimeState from homeassistant.components.datetime import DateTimeEntity -from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity_platform import AddEntitiesCallback import homeassistant.util.dt as dt_util from .entity import EsphomeEntity, esphome_state_property, platform_async_setup_entry -from .entry_data import ESPHomeConfigEntry - - -async def async_setup_entry( - hass: HomeAssistant, - entry: ESPHomeConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up esphome datetimes based on a config entry.""" - await platform_async_setup_entry( - hass, - entry, - async_add_entities, - info_type=DateTimeInfo, - entity_type=EsphomeDateTime, - state_type=DateTimeState, - ) class EsphomeDateTime(EsphomeEntity[DateTimeInfo, DateTimeState], DateTimeEntity): @@ -46,3 +28,11 @@ class EsphomeDateTime(EsphomeEntity[DateTimeInfo, DateTimeState], DateTimeEntity async def async_set_value(self, value: datetime) -> None: """Update the current datetime.""" self._client.datetime_command(self._key, int(value.timestamp())) + + +async_setup_entry = partial( + platform_async_setup_entry, + info_type=DateTimeInfo, + entity_type=EsphomeDateTime, + state_type=DateTimeState, +) diff --git a/homeassistant/components/esphome/event.py b/homeassistant/components/esphome/event.py index 9435597e25b..11a5d0cfb33 100644 --- a/homeassistant/components/esphome/event.py +++ b/homeassistant/components/esphome/event.py @@ -2,31 +2,15 @@ from __future__ import annotations +from functools import partial + from aioesphomeapi import EntityInfo, Event, EventInfo from homeassistant.components.event import EventDeviceClass, EventEntity -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.core import callback from homeassistant.util.enum import try_parse_enum from .entity import EsphomeEntity, platform_async_setup_entry -from .entry_data import ESPHomeConfigEntry - - -async def async_setup_entry( - hass: HomeAssistant, - entry: ESPHomeConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up ESPHome event based on a config entry.""" - await platform_async_setup_entry( - hass, - entry, - async_add_entities, - info_type=EventInfo, - entity_type=EsphomeEvent, - state_type=Event, - ) class EsphomeEvent(EsphomeEntity[EventInfo, Event], EventEntity): @@ -48,3 +32,11 @@ class EsphomeEvent(EsphomeEntity[EventInfo, Event], EventEntity): self._update_state_from_entry_data() self._trigger_event(self._state.event_type) self.async_write_ha_state() + + +async_setup_entry = partial( + platform_async_setup_entry, + info_type=EventInfo, + entity_type=EsphomeEvent, + state_type=Event, +) diff --git a/homeassistant/components/esphome/fan.py b/homeassistant/components/esphome/fan.py index 35a19348281..43ffd96c475 100644 --- a/homeassistant/components/esphome/fan.py +++ b/homeassistant/components/esphome/fan.py @@ -2,6 +2,7 @@ from __future__ import annotations +from functools import partial import math from typing import Any @@ -13,8 +14,7 @@ from homeassistant.components.fan import ( FanEntity, FanEntityFeature, ) -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.core import callback from homeassistant.util.percentage import ( ordered_list_item_to_percentage, percentage_to_ordered_list_item, @@ -28,28 +28,11 @@ from .entity import ( esphome_state_property, platform_async_setup_entry, ) -from .entry_data import ESPHomeConfigEntry from .enum_mapper import EsphomeEnumMapper ORDERED_NAMED_FAN_SPEEDS = [FanSpeed.LOW, FanSpeed.MEDIUM, FanSpeed.HIGH] -async def async_setup_entry( - hass: HomeAssistant, - entry: ESPHomeConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up ESPHome fans based on a config entry.""" - await platform_async_setup_entry( - hass, - entry, - async_add_entities, - info_type=FanInfo, - entity_type=EsphomeFan, - state_type=FanState, - ) - - _FAN_DIRECTIONS: EsphomeEnumMapper[FanDirection, str] = EsphomeEnumMapper( { FanDirection.FORWARD: DIRECTION_FORWARD, @@ -180,3 +163,11 @@ class EsphomeFan(EsphomeEntity[FanInfo, FanState], FanEntity): self._attr_speed_count = len(ORDERED_NAMED_FAN_SPEEDS) else: self._attr_speed_count = static_info.supported_speed_levels + + +async_setup_entry = partial( + platform_async_setup_entry, + info_type=FanInfo, + entity_type=EsphomeFan, + state_type=FanState, +) diff --git a/homeassistant/components/esphome/light.py b/homeassistant/components/esphome/light.py index c5f83805cce..295f9365cd0 100644 --- a/homeassistant/components/esphome/light.py +++ b/homeassistant/components/esphome/light.py @@ -2,7 +2,7 @@ from __future__ import annotations -from functools import lru_cache +from functools import lru_cache, partial from typing import TYPE_CHECKING, Any, cast from aioesphomeapi import ( @@ -29,8 +29,7 @@ from homeassistant.components.light import ( LightEntity, LightEntityFeature, ) -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.core import callback from .entity import ( EsphomeEntity, @@ -38,27 +37,10 @@ from .entity import ( esphome_state_property, platform_async_setup_entry, ) -from .entry_data import ESPHomeConfigEntry FLASH_LENGTHS = {FLASH_SHORT: 2, FLASH_LONG: 10} -async def async_setup_entry( - hass: HomeAssistant, - entry: ESPHomeConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up ESPHome lights based on a config entry.""" - await platform_async_setup_entry( - hass, - entry, - async_add_entities, - info_type=LightInfo, - entity_type=EsphomeLight, - state_type=LightState, - ) - - _COLOR_MODE_MAPPING = { ColorMode.ONOFF: [ LightColorCapability.ON_OFF, @@ -437,3 +419,11 @@ class EsphomeLight(EsphomeEntity[LightInfo, LightState], LightEntity): if ColorMode.COLOR_TEMP in supported: self._attr_min_color_temp_kelvin = _mired_to_kelvin(static_info.max_mireds) self._attr_max_color_temp_kelvin = _mired_to_kelvin(static_info.min_mireds) + + +async_setup_entry = partial( + platform_async_setup_entry, + info_type=LightInfo, + entity_type=EsphomeLight, + state_type=LightState, +) diff --git a/homeassistant/components/esphome/lock.py b/homeassistant/components/esphome/lock.py index c00f81839cb..4caa1f68612 100644 --- a/homeassistant/components/esphome/lock.py +++ b/homeassistant/components/esphome/lock.py @@ -2,14 +2,14 @@ from __future__ import annotations +from functools import partial from typing import Any from aioesphomeapi import EntityInfo, LockCommand, LockEntityState, LockInfo, LockState from homeassistant.components.lock import LockEntity, LockEntityFeature from homeassistant.const import ATTR_CODE -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.core import callback from .entity import ( EsphomeEntity, @@ -17,23 +17,6 @@ from .entity import ( esphome_state_property, platform_async_setup_entry, ) -from .entry_data import ESPHomeConfigEntry - - -async def async_setup_entry( - hass: HomeAssistant, - entry: ESPHomeConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up ESPHome switches based on a config entry.""" - await platform_async_setup_entry( - hass, - entry, - async_add_entities, - info_type=LockInfo, - entity_type=EsphomeLock, - state_type=LockEntityState, - ) class EsphomeLock(EsphomeEntity[LockInfo, LockEntityState], LockEntity): @@ -92,3 +75,11 @@ class EsphomeLock(EsphomeEntity[LockInfo, LockEntityState], LockEntity): async def async_open(self, **kwargs: Any) -> None: """Open the door latch.""" self._client.lock_command(self._key, LockCommand.OPEN) + + +async_setup_entry = partial( + platform_async_setup_entry, + info_type=LockInfo, + entity_type=EsphomeLock, + state_type=LockEntityState, +) diff --git a/homeassistant/components/esphome/media_player.py b/homeassistant/components/esphome/media_player.py index 8caad0f939d..ec9d61fb9e7 100644 --- a/homeassistant/components/esphome/media_player.py +++ b/homeassistant/components/esphome/media_player.py @@ -2,6 +2,7 @@ from __future__ import annotations +from functools import partial from typing import Any from aioesphomeapi import ( @@ -23,9 +24,7 @@ from homeassistant.components.media_player import ( MediaType, async_process_play_media_url, ) -from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.core import callback from .entity import ( EsphomeEntity, @@ -35,23 +34,6 @@ from .entity import ( ) from .enum_mapper import EsphomeEnumMapper - -async def async_setup_entry( - hass: HomeAssistant, - entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up esphome media players based on a config entry.""" - await platform_async_setup_entry( - hass, - entry, - async_add_entities, - info_type=MediaPlayerInfo, - entity_type=EsphomeMediaPlayer, - state_type=MediaPlayerEntityState, - ) - - _STATES: EsphomeEnumMapper[EspMediaPlayerState, MediaPlayerState] = EsphomeEnumMapper( { EspMediaPlayerState.IDLE: MediaPlayerState.IDLE, @@ -159,3 +141,11 @@ class EsphomeMediaPlayer( self._key, command=MediaPlayerCommand.MUTE if mute else MediaPlayerCommand.UNMUTE, ) + + +async_setup_entry = partial( + platform_async_setup_entry, + info_type=MediaPlayerInfo, + entity_type=EsphomeMediaPlayer, + state_type=MediaPlayerEntityState, +) diff --git a/homeassistant/components/esphome/number.py b/homeassistant/components/esphome/number.py index 01744dd9998..1e588c8d35e 100644 --- a/homeassistant/components/esphome/number.py +++ b/homeassistant/components/esphome/number.py @@ -2,6 +2,7 @@ from __future__ import annotations +from functools import partial import math from aioesphomeapi import ( @@ -12,9 +13,7 @@ from aioesphomeapi import ( ) from homeassistant.components.number import NumberDeviceClass, NumberEntity, NumberMode -from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.core import callback from homeassistant.util.enum import try_parse_enum from .entity import ( @@ -25,23 +24,6 @@ from .entity import ( ) from .enum_mapper import EsphomeEnumMapper - -async def async_setup_entry( - hass: HomeAssistant, - entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up esphome numbers based on a config entry.""" - await platform_async_setup_entry( - hass, - entry, - async_add_entities, - info_type=NumberInfo, - entity_type=EsphomeNumber, - state_type=NumberState, - ) - - NUMBER_MODES: EsphomeEnumMapper[EsphomeNumberMode, NumberMode] = EsphomeEnumMapper( { EsphomeNumberMode.AUTO: NumberMode.AUTO, @@ -87,3 +69,11 @@ class EsphomeNumber(EsphomeEntity[NumberInfo, NumberState], NumberEntity): async def async_set_native_value(self, value: float) -> None: """Update the current value.""" self._client.number_command(self._key, value) + + +async_setup_entry = partial( + platform_async_setup_entry, + info_type=NumberInfo, + entity_type=EsphomeNumber, + state_type=NumberState, +) diff --git a/homeassistant/components/esphome/switch.py b/homeassistant/components/esphome/switch.py index b2245c78f52..c210ae1440b 100644 --- a/homeassistant/components/esphome/switch.py +++ b/homeassistant/components/esphome/switch.py @@ -2,13 +2,13 @@ from __future__ import annotations +from functools import partial from typing import Any from aioesphomeapi import EntityInfo, SwitchInfo, SwitchState from homeassistant.components.switch import SwitchDeviceClass, SwitchEntity -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.core import callback from homeassistant.util.enum import try_parse_enum from .entity import ( @@ -17,23 +17,6 @@ from .entity import ( esphome_state_property, platform_async_setup_entry, ) -from .entry_data import ESPHomeConfigEntry - - -async def async_setup_entry( - hass: HomeAssistant, - entry: ESPHomeConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up ESPHome switches based on a config entry.""" - await platform_async_setup_entry( - hass, - entry, - async_add_entities, - info_type=SwitchInfo, - entity_type=EsphomeSwitch, - state_type=SwitchState, - ) class EsphomeSwitch(EsphomeEntity[SwitchInfo, SwitchState], SwitchEntity): @@ -64,3 +47,11 @@ class EsphomeSwitch(EsphomeEntity[SwitchInfo, SwitchState], SwitchEntity): async def async_turn_off(self, **kwargs: Any) -> None: """Turn the entity off.""" self._client.switch_command(self._key, False) + + +async_setup_entry = partial( + platform_async_setup_entry, + info_type=SwitchInfo, + entity_type=EsphomeSwitch, + state_type=SwitchState, +) diff --git a/homeassistant/components/esphome/text.py b/homeassistant/components/esphome/text.py index 7d455e9ec21..f9dbbbcd853 100644 --- a/homeassistant/components/esphome/text.py +++ b/homeassistant/components/esphome/text.py @@ -2,12 +2,12 @@ from __future__ import annotations +from functools import partial + from aioesphomeapi import EntityInfo, TextInfo, TextMode as EsphomeTextMode, TextState from homeassistant.components.text import TextEntity, TextMode -from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.core import callback from .entity import ( EsphomeEntity, @@ -17,23 +17,6 @@ from .entity import ( ) from .enum_mapper import EsphomeEnumMapper - -async def async_setup_entry( - hass: HomeAssistant, - entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up esphome texts based on a config entry.""" - await platform_async_setup_entry( - hass, - entry, - async_add_entities, - info_type=TextInfo, - entity_type=EsphomeText, - state_type=TextState, - ) - - TEXT_MODES: EsphomeEnumMapper[EsphomeTextMode, TextMode] = EsphomeEnumMapper( { EsphomeTextMode.TEXT: TextMode.TEXT, @@ -68,3 +51,11 @@ class EsphomeText(EsphomeEntity[TextInfo, TextState], TextEntity): async def async_set_value(self, value: str) -> None: """Update the current value.""" self._client.text_command(self._key, value) + + +async_setup_entry = partial( + platform_async_setup_entry, + info_type=TextInfo, + entity_type=EsphomeText, + state_type=TextState, +) diff --git a/homeassistant/components/esphome/time.py b/homeassistant/components/esphome/time.py index de985a1e1d6..477c47cf636 100644 --- a/homeassistant/components/esphome/time.py +++ b/homeassistant/components/esphome/time.py @@ -3,33 +3,15 @@ from __future__ import annotations from datetime import time +from functools import partial from aioesphomeapi import TimeInfo, TimeState from homeassistant.components.time import TimeEntity -from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity_platform import AddEntitiesCallback from .entity import EsphomeEntity, esphome_state_property, platform_async_setup_entry -async def async_setup_entry( - hass: HomeAssistant, - entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up esphome times based on a config entry.""" - await platform_async_setup_entry( - hass, - entry, - async_add_entities, - info_type=TimeInfo, - entity_type=EsphomeTime, - state_type=TimeState, - ) - - class EsphomeTime(EsphomeEntity[TimeInfo, TimeState], TimeEntity): """A time implementation for esphome.""" @@ -45,3 +27,11 @@ class EsphomeTime(EsphomeEntity[TimeInfo, TimeState], TimeEntity): async def async_set_value(self, value: time) -> None: """Update the current time.""" self._client.time_command(self._key, value.hour, value.minute, value.second) + + +async_setup_entry = partial( + platform_async_setup_entry, + info_type=TimeInfo, + entity_type=EsphomeTime, + state_type=TimeState, +) diff --git a/homeassistant/components/esphome/valve.py b/homeassistant/components/esphome/valve.py index a82d65366c6..d779a6abb9f 100644 --- a/homeassistant/components/esphome/valve.py +++ b/homeassistant/components/esphome/valve.py @@ -2,6 +2,7 @@ from __future__ import annotations +from functools import partial from typing import Any from aioesphomeapi import EntityInfo, ValveInfo, ValveOperation, ValveState @@ -11,8 +12,7 @@ from homeassistant.components.valve import ( ValveEntity, ValveEntityFeature, ) -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.core import callback from homeassistant.util.enum import try_parse_enum from .entity import ( @@ -21,23 +21,6 @@ from .entity import ( esphome_state_property, platform_async_setup_entry, ) -from .entry_data import ESPHomeConfigEntry - - -async def async_setup_entry( - hass: HomeAssistant, - entry: ESPHomeConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up ESPHome valves based on a config entry.""" - await platform_async_setup_entry( - hass, - entry, - async_add_entities, - info_type=ValveInfo, - entity_type=EsphomeValve, - state_type=ValveState, - ) class EsphomeValve(EsphomeEntity[ValveInfo, ValveState], ValveEntity): @@ -103,3 +86,11 @@ class EsphomeValve(EsphomeEntity[ValveInfo, ValveState], ValveEntity): async def async_set_valve_position(self, position: float) -> None: """Move the valve to a specific position.""" self._client.valve_command(key=self._key, position=position / 100) + + +async_setup_entry = partial( + platform_async_setup_entry, + info_type=ValveInfo, + entity_type=EsphomeValve, + state_type=ValveState, +) From 6e5bc0da94b56e18a0e16267fd70354ff1e600fa Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 25 Jun 2024 17:07:50 +0200 Subject: [PATCH 1172/1445] Improve type hints in cloud tests (#120420) --- tests/components/cloud/__init__.py | 7 ++- tests/components/cloud/conftest.py | 16 +++---- tests/components/cloud/test_account_link.py | 10 ++++- tests/components/cloud/test_alexa_config.py | 41 ++++++++++------- tests/components/cloud/test_client.py | 19 +++++--- tests/components/cloud/test_google_config.py | 47 +++++++++++++------- tests/components/cloud/test_init.py | 12 +++-- 7 files changed, 96 insertions(+), 56 deletions(-) diff --git a/tests/components/cloud/__init__.py b/tests/components/cloud/__init__.py index f1ce24e576f..18f8cd4d311 100644 --- a/tests/components/cloud/__init__.py +++ b/tests/components/cloud/__init__.py @@ -1,5 +1,6 @@ """Tests for the cloud component.""" +from typing import Any from unittest.mock import AsyncMock, patch from homeassistant.components.cloud.const import ( @@ -14,7 +15,9 @@ from homeassistant.components.cloud.const import ( from homeassistant.components.cloud.prefs import ( ALEXA_SETTINGS_VERSION, GOOGLE_SETTINGS_VERSION, + CloudPreferences, ) +from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component PIPELINE_DATA = { @@ -66,7 +69,7 @@ PIPELINE_DATA = { } -async def mock_cloud(hass, config=None): +async def mock_cloud(hass: HomeAssistant, config: dict[str, Any] | None = None) -> None: """Mock cloud.""" # The homeassistant integration is needed by cloud. It's not in it's requirements # because it's always setup by bootstrap. Set it up manually in tests. @@ -78,7 +81,7 @@ async def mock_cloud(hass, config=None): await cloud_inst.initialize() -def mock_cloud_prefs(hass, prefs): +def mock_cloud_prefs(hass: HomeAssistant, prefs: dict[str, Any]) -> CloudPreferences: """Fixture for cloud component.""" prefs_to_set = { PREF_ALEXA_SETTINGS_VERSION: ALEXA_SETTINGS_VERSION, diff --git a/tests/components/cloud/conftest.py b/tests/components/cloud/conftest.py index a7abb932124..c7d0702ea88 100644 --- a/tests/components/cloud/conftest.py +++ b/tests/components/cloud/conftest.py @@ -15,7 +15,7 @@ from hass_nabucasa.remote import RemoteUI from hass_nabucasa.voice import Voice import jwt import pytest -from typing_extensions import AsyncGenerator +from typing_extensions import AsyncGenerator, Generator from homeassistant.components.cloud.client import CloudClient from homeassistant.components.cloud.const import DATA_CLOUD @@ -199,21 +199,21 @@ def tts_mutagen_mock_fixture_autouse(tts_mutagen_mock: MagicMock) -> None: @pytest.fixture(autouse=True) -def mock_user_data(): +def mock_user_data() -> Generator[MagicMock]: """Mock os module.""" with patch("hass_nabucasa.Cloud._write_user_info") as writer: yield writer @pytest.fixture -def mock_cloud_fixture(hass): +def mock_cloud_fixture(hass: HomeAssistant) -> CloudPreferences: """Fixture for cloud component.""" hass.loop.run_until_complete(mock_cloud(hass)) return mock_cloud_prefs(hass, {}) @pytest.fixture -async def cloud_prefs(hass): +async def cloud_prefs(hass: HomeAssistant) -> CloudPreferences: """Fixture for cloud preferences.""" cloud_prefs = CloudPreferences(hass) await cloud_prefs.async_initialize() @@ -221,13 +221,13 @@ async def cloud_prefs(hass): @pytest.fixture -async def mock_cloud_setup(hass): +async def mock_cloud_setup(hass: HomeAssistant) -> None: """Set up the cloud.""" await mock_cloud(hass) @pytest.fixture -def mock_cloud_login(hass, mock_cloud_setup): +def mock_cloud_login(hass: HomeAssistant, mock_cloud_setup: None) -> Generator[None]: """Mock cloud is logged in.""" hass.data[DATA_CLOUD].id_token = jwt.encode( { @@ -242,7 +242,7 @@ def mock_cloud_login(hass, mock_cloud_setup): @pytest.fixture(name="mock_auth") -def mock_auth_fixture(): +def mock_auth_fixture() -> Generator[None]: """Mock check token.""" with ( patch("hass_nabucasa.auth.CognitoAuth.async_check_token"), @@ -252,7 +252,7 @@ def mock_auth_fixture(): @pytest.fixture -def mock_expired_cloud_login(hass, mock_cloud_setup): +def mock_expired_cloud_login(hass: HomeAssistant, mock_cloud_setup: None) -> None: """Mock cloud is logged in.""" hass.data[DATA_CLOUD].id_token = jwt.encode( { diff --git a/tests/components/cloud/test_account_link.py b/tests/components/cloud/test_account_link.py index 7a85531904a..acaff7db76c 100644 --- a/tests/components/cloud/test_account_link.py +++ b/tests/components/cloud/test_account_link.py @@ -6,6 +6,7 @@ from time import time from unittest.mock import AsyncMock, Mock, patch import pytest +from typing_extensions import Generator from homeassistant import config_entries from homeassistant.components.cloud import account_link @@ -21,7 +22,9 @@ TEST_DOMAIN = "oauth2_test" @pytest.fixture -def flow_handler(hass): +def flow_handler( + hass: HomeAssistant, +) -> Generator[type[config_entry_oauth2_flow.AbstractOAuth2FlowHandler]]: """Return a registered config flow.""" mock_platform(hass, f"{TEST_DOMAIN}.config_flow") @@ -180,7 +183,10 @@ async def test_get_services_error(hass: HomeAssistant) -> None: @pytest.mark.usefixtures("current_request_with_host") -async def test_implementation(hass: HomeAssistant, flow_handler) -> None: +async def test_implementation( + hass: HomeAssistant, + flow_handler: type[config_entry_oauth2_flow.AbstractOAuth2FlowHandler], +) -> None: """Test Cloud OAuth2 implementation.""" hass.data[DATA_CLOUD] = None diff --git a/tests/components/cloud/test_alexa_config.py b/tests/components/cloud/test_alexa_config.py index e4ad425d4d4..3b4868b56ac 100644 --- a/tests/components/cloud/test_alexa_config.py +++ b/tests/components/cloud/test_alexa_config.py @@ -34,7 +34,7 @@ from tests.test_util.aiohttp import AiohttpClientMocker @pytest.fixture -def cloud_stub(): +def cloud_stub() -> Mock: """Stub the cloud.""" return Mock(is_logged_in=True, subscription_expired=False) @@ -51,7 +51,10 @@ def expose_entity(hass: HomeAssistant, entity_id: str, should_expose: bool) -> N async def test_alexa_config_expose_entity_prefs( - hass: HomeAssistant, cloud_prefs, cloud_stub, entity_registry: er.EntityRegistry + hass: HomeAssistant, + cloud_prefs: CloudPreferences, + cloud_stub: Mock, + entity_registry: er.EntityRegistry, ) -> None: """Test Alexa config should expose using prefs.""" assert await async_setup_component(hass, "homeassistant", {}) @@ -133,7 +136,7 @@ async def test_alexa_config_expose_entity_prefs( async def test_alexa_config_report_state( - hass: HomeAssistant, cloud_prefs, cloud_stub + hass: HomeAssistant, cloud_prefs: CloudPreferences, cloud_stub: Mock ) -> None: """Test Alexa config should expose using prefs.""" assert await async_setup_component(hass, "homeassistant", {}) @@ -168,7 +171,9 @@ async def test_alexa_config_report_state( async def test_alexa_config_invalidate_token( - hass: HomeAssistant, cloud_prefs, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, + cloud_prefs: CloudPreferences, + aioclient_mock: AiohttpClientMocker, ) -> None: """Test Alexa config should expose using prefs.""" assert await async_setup_component(hass, "homeassistant", {}) @@ -218,11 +223,11 @@ async def test_alexa_config_invalidate_token( ) async def test_alexa_config_fail_refresh_token( hass: HomeAssistant, - cloud_prefs, + cloud_prefs: CloudPreferences, aioclient_mock: AiohttpClientMocker, entity_registry: er.EntityRegistry, - reject_reason, - expected_exception, + reject_reason: str, + expected_exception: type[Exception], ) -> None: """Test Alexa config failing to refresh token.""" assert await async_setup_component(hass, "homeassistant", {}) @@ -342,7 +347,10 @@ def patch_sync_helper(): async def test_alexa_update_expose_trigger_sync( - hass: HomeAssistant, entity_registry: er.EntityRegistry, cloud_prefs, cloud_stub + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + cloud_prefs: CloudPreferences, + cloud_stub: Mock, ) -> None: """Test Alexa config responds to updating exposed entities.""" assert await async_setup_component(hass, "homeassistant", {}) @@ -415,11 +423,11 @@ async def test_alexa_update_expose_trigger_sync( ] +@pytest.mark.usefixtures("mock_cloud_login") async def test_alexa_entity_registry_sync( hass: HomeAssistant, entity_registry: er.EntityRegistry, - mock_cloud_login, - cloud_prefs, + cloud_prefs: CloudPreferences, ) -> None: """Test Alexa config responds to entity registry.""" # Enable exposing new entities to Alexa @@ -475,7 +483,7 @@ async def test_alexa_entity_registry_sync( async def test_alexa_update_report_state( - hass: HomeAssistant, cloud_prefs, cloud_stub + hass: HomeAssistant, cloud_prefs: CloudPreferences, cloud_stub: Mock ) -> None: """Test Alexa config responds to reporting state.""" assert await async_setup_component(hass, "homeassistant", {}) @@ -502,8 +510,9 @@ async def test_alexa_update_report_state( assert len(mock_sync.mock_calls) == 1 +@pytest.mark.usefixtures("mock_expired_cloud_login") def test_enabled_requires_valid_sub( - hass: HomeAssistant, mock_expired_cloud_login, cloud_prefs + hass: HomeAssistant, cloud_prefs: CloudPreferences ) -> None: """Test that alexa config enabled requires a valid Cloud sub.""" assert cloud_prefs.alexa_enabled @@ -518,7 +527,7 @@ def test_enabled_requires_valid_sub( async def test_alexa_handle_logout( - hass: HomeAssistant, cloud_prefs, cloud_stub + hass: HomeAssistant, cloud_prefs: CloudPreferences, cloud_stub: Mock ) -> None: """Test Alexa config responds to logging out.""" assert await async_setup_component(hass, "homeassistant", {}) @@ -561,7 +570,7 @@ async def test_alexa_handle_logout( async def test_alexa_config_migrate_expose_entity_prefs( hass: HomeAssistant, cloud_prefs: CloudPreferences, - cloud_stub, + cloud_stub: Mock, entity_registry: er.EntityRegistry, alexa_settings_version: int, ) -> None: @@ -755,7 +764,7 @@ async def test_alexa_config_migrate_expose_entity_prefs_v2_exposed( async def test_alexa_config_migrate_expose_entity_prefs_default_none( hass: HomeAssistant, cloud_prefs: CloudPreferences, - cloud_stub, + cloud_stub: Mock, entity_registry: er.EntityRegistry, ) -> None: """Test migrating Alexa entity config.""" @@ -793,7 +802,7 @@ async def test_alexa_config_migrate_expose_entity_prefs_default_none( async def test_alexa_config_migrate_expose_entity_prefs_default( hass: HomeAssistant, cloud_prefs: CloudPreferences, - cloud_stub, + cloud_stub: Mock, entity_registry: er.EntityRegistry, ) -> None: """Test migrating Alexa entity config.""" diff --git a/tests/components/cloud/test_client.py b/tests/components/cloud/test_client.py index 62af4e88857..005efd990fb 100644 --- a/tests/components/cloud/test_client.py +++ b/tests/components/cloud/test_client.py @@ -1,6 +1,7 @@ """Test the cloud.iot module.""" from datetime import timedelta +from typing import Any from unittest.mock import AsyncMock, MagicMock, Mock, PropertyMock, patch import aiohttp @@ -20,6 +21,7 @@ from homeassistant.components.cloud.const import ( PREF_ENABLE_ALEXA, PREF_ENABLE_GOOGLE, ) +from homeassistant.components.cloud.prefs import CloudPreferences from homeassistant.components.homeassistant.exposed_entities import ( DATA_EXPOSED_ENTITIES, async_expose_entity, @@ -37,7 +39,7 @@ from tests.components.alexa import test_smart_home as test_alexa @pytest.fixture -def mock_cloud_inst(): +def mock_cloud_inst() -> MagicMock: """Mock cloud class.""" return MagicMock(subscription_expired=False) @@ -81,7 +83,9 @@ async def test_handler_alexa(hass: HomeAssistant) -> None: assert device["manufacturerName"] == "Home Assistant" -async def test_handler_alexa_disabled(hass: HomeAssistant, mock_cloud_fixture) -> None: +async def test_handler_alexa_disabled( + hass: HomeAssistant, mock_cloud_fixture: CloudPreferences +) -> None: """Test handler Alexa when user has disabled it.""" mock_cloud_fixture._prefs[PREF_ENABLE_ALEXA] = False cloud = hass.data[DATA_CLOUD] @@ -154,7 +158,10 @@ async def test_handler_google_actions(hass: HomeAssistant) -> None: ], ) async def test_handler_google_actions_disabled( - hass: HomeAssistant, mock_cloud_fixture, intent, response_payload + hass: HomeAssistant, + mock_cloud_fixture: CloudPreferences, + intent: str, + response_payload: dict[str, Any], ) -> None: """Test handler Google Actions when user has disabled it.""" mock_cloud_fixture._prefs[PREF_ENABLE_GOOGLE] = False @@ -253,11 +260,10 @@ async def test_webhook_msg( assert '{"nonexisting": "payload"}' in caplog.text +@pytest.mark.usefixtures("mock_cloud_setup", "mock_cloud_login") async def test_google_config_expose_entity( hass: HomeAssistant, entity_registry: er.EntityRegistry, - mock_cloud_setup, - mock_cloud_login, ) -> None: """Test Google config exposing entity method uses latest config.""" @@ -281,11 +287,10 @@ async def test_google_config_expose_entity( assert not gconf.should_expose(state) +@pytest.mark.usefixtures("mock_cloud_setup", "mock_cloud_login") async def test_google_config_should_2fa( hass: HomeAssistant, entity_registry: er.EntityRegistry, - mock_cloud_setup, - mock_cloud_login, ) -> None: """Test Google config disabling 2FA method uses latest config.""" diff --git a/tests/components/cloud/test_google_config.py b/tests/components/cloud/test_google_config.py index 40d3f6ef2c5..b152309b24a 100644 --- a/tests/components/cloud/test_google_config.py +++ b/tests/components/cloud/test_google_config.py @@ -36,7 +36,7 @@ from tests.common import async_fire_time_changed @pytest.fixture -def mock_conf(hass, cloud_prefs): +def mock_conf(hass: HomeAssistant, cloud_prefs: CloudPreferences) -> CloudGoogleConfig: """Mock Google conf.""" return CloudGoogleConfig( hass, @@ -59,7 +59,7 @@ def expose_entity(hass: HomeAssistant, entity_id: str, should_expose: bool) -> N async def test_google_update_report_state( - mock_conf, hass: HomeAssistant, cloud_prefs + mock_conf: CloudGoogleConfig, hass: HomeAssistant, cloud_prefs: CloudPreferences ) -> None: """Test Google config responds to updating preference.""" assert await async_setup_component(hass, "homeassistant", {}) @@ -83,7 +83,7 @@ async def test_google_update_report_state( async def test_google_update_report_state_subscription_expired( - mock_conf, hass: HomeAssistant, cloud_prefs + mock_conf: CloudGoogleConfig, hass: HomeAssistant, cloud_prefs: CloudPreferences ) -> None: """Test Google config not reporting state when subscription has expired.""" assert await async_setup_component(hass, "homeassistant", {}) @@ -106,7 +106,9 @@ async def test_google_update_report_state_subscription_expired( assert len(mock_report_state.mock_calls) == 0 -async def test_sync_entities(mock_conf, hass: HomeAssistant, cloud_prefs) -> None: +async def test_sync_entities( + mock_conf: CloudGoogleConfig, hass: HomeAssistant, cloud_prefs: CloudPreferences +) -> None: """Test sync devices.""" assert await async_setup_component(hass, "homeassistant", {}) @@ -129,7 +131,9 @@ async def test_sync_entities(mock_conf, hass: HomeAssistant, cloud_prefs) -> Non async def test_google_update_expose_trigger_sync( - hass: HomeAssistant, entity_registry: er.EntityRegistry, cloud_prefs + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + cloud_prefs: CloudPreferences, ) -> None: """Test Google config responds to updating exposed entities.""" assert await async_setup_component(hass, "homeassistant", {}) @@ -185,11 +189,11 @@ async def test_google_update_expose_trigger_sync( assert len(mock_sync.mock_calls) == 1 +@pytest.mark.usefixtures("mock_cloud_login") async def test_google_entity_registry_sync( hass: HomeAssistant, entity_registry: er.EntityRegistry, - mock_cloud_login, - cloud_prefs, + cloud_prefs: CloudPreferences, ) -> None: """Test Google config responds to entity registry.""" @@ -257,11 +261,11 @@ async def test_google_entity_registry_sync( assert len(mock_sync.mock_calls) == 3 +@pytest.mark.usefixtures("mock_cloud_login") async def test_google_device_registry_sync( hass: HomeAssistant, entity_registry: er.EntityRegistry, - mock_cloud_login, - cloud_prefs, + cloud_prefs: CloudPreferences, ) -> None: """Test Google config responds to device registry.""" config = CloudGoogleConfig( @@ -329,8 +333,9 @@ async def test_google_device_registry_sync( assert len(mock_sync.mock_calls) == 1 +@pytest.mark.usefixtures("mock_cloud_login") async def test_sync_google_when_started( - hass: HomeAssistant, mock_cloud_login, cloud_prefs + hass: HomeAssistant, cloud_prefs: CloudPreferences ) -> None: """Test Google config syncs on init.""" config = CloudGoogleConfig( @@ -342,8 +347,9 @@ async def test_sync_google_when_started( assert len(mock_sync.mock_calls) == 1 +@pytest.mark.usefixtures("mock_cloud_login") async def test_sync_google_on_home_assistant_start( - hass: HomeAssistant, mock_cloud_login, cloud_prefs + hass: HomeAssistant, cloud_prefs: CloudPreferences ) -> None: """Test Google config syncs when home assistant started.""" config = CloudGoogleConfig( @@ -361,7 +367,10 @@ async def test_sync_google_on_home_assistant_start( async def test_google_config_expose_entity_prefs( - hass: HomeAssistant, mock_conf, cloud_prefs, entity_registry: er.EntityRegistry + hass: HomeAssistant, + mock_conf: CloudGoogleConfig, + cloud_prefs: CloudPreferences, + entity_registry: er.EntityRegistry, ) -> None: """Test Google config should expose using prefs.""" assert await async_setup_component(hass, "homeassistant", {}) @@ -437,8 +446,9 @@ async def test_google_config_expose_entity_prefs( assert not mock_conf.should_expose(state_not_exposed) +@pytest.mark.usefixtures("mock_expired_cloud_login") def test_enabled_requires_valid_sub( - hass: HomeAssistant, mock_expired_cloud_login, cloud_prefs + hass: HomeAssistant, cloud_prefs: CloudPreferences ) -> None: """Test that google config enabled requires a valid Cloud sub.""" assert cloud_prefs.google_enabled @@ -453,7 +463,7 @@ def test_enabled_requires_valid_sub( async def test_setup_google_assistant( - hass: HomeAssistant, mock_conf, cloud_prefs + hass: HomeAssistant, mock_conf: CloudGoogleConfig, cloud_prefs: CloudPreferences ) -> None: """Test that we set up the google_assistant integration if enabled in cloud.""" assert await async_setup_component(hass, "homeassistant", {}) @@ -472,8 +482,9 @@ async def test_setup_google_assistant( assert "google_assistant" in hass.config.components +@pytest.mark.usefixtures("mock_cloud_login") async def test_google_handle_logout( - hass: HomeAssistant, cloud_prefs, mock_cloud_login + hass: HomeAssistant, cloud_prefs: CloudPreferences ) -> None: """Test Google config responds to logging out.""" gconf = CloudGoogleConfig( @@ -853,8 +864,9 @@ async def test_google_config_migrate_expose_entity_prefs_default( } +@pytest.mark.usefixtures("mock_cloud_login") async def test_google_config_get_agent_user_id( - hass: HomeAssistant, mock_cloud_login, cloud_prefs + hass: HomeAssistant, cloud_prefs: CloudPreferences ) -> None: """Test overridden get_agent_user_id_from_webhook method.""" config = CloudGoogleConfig( @@ -867,8 +879,9 @@ async def test_google_config_get_agent_user_id( assert config.get_agent_user_id_from_webhook("other_id") != config.agent_user_id +@pytest.mark.usefixtures("mock_cloud_login") async def test_google_config_get_agent_users( - hass: HomeAssistant, mock_cloud_login, cloud_prefs + hass: HomeAssistant, cloud_prefs: CloudPreferences ) -> None: """Test overridden async_get_agent_users method.""" username_mock = PropertyMock(return_value="blah") diff --git a/tests/components/cloud/test_init.py b/tests/components/cloud/test_init.py index d201b45b670..ad123cded84 100644 --- a/tests/components/cloud/test_init.py +++ b/tests/components/cloud/test_init.py @@ -67,8 +67,9 @@ async def test_constructor_loads_info_from_config(hass: HomeAssistant) -> None: assert cl.remotestate_server == "test-remotestate-server" +@pytest.mark.usefixtures("mock_cloud_fixture") async def test_remote_services( - hass: HomeAssistant, mock_cloud_fixture, hass_read_only_user: MockUser + hass: HomeAssistant, hass_read_only_user: MockUser ) -> None: """Setup cloud component and test services.""" cloud = hass.data[DATA_CLOUD] @@ -114,7 +115,8 @@ async def test_remote_services( assert mock_disconnect.called is False -async def test_shutdown_event(hass: HomeAssistant, mock_cloud_fixture) -> None: +@pytest.mark.usefixtures("mock_cloud_fixture") +async def test_shutdown_event(hass: HomeAssistant) -> None: """Test if the cloud will stop on shutdown event.""" with patch("hass_nabucasa.Cloud.stop") as mock_stop: hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) @@ -149,7 +151,8 @@ async def test_setup_existing_cloud_user( assert hass_storage[STORAGE_KEY]["data"]["cloud_user"] == user.id -async def test_on_connect(hass: HomeAssistant, mock_cloud_fixture) -> None: +@pytest.mark.usefixtures("mock_cloud_fixture") +async def test_on_connect(hass: HomeAssistant) -> None: """Test cloud on connect triggers.""" cl = hass.data[DATA_CLOUD] @@ -206,7 +209,8 @@ async def test_on_connect(hass: HomeAssistant, mock_cloud_fixture) -> None: assert cloud_states[-1] == CloudConnectionState.CLOUD_DISCONNECTED -async def test_remote_ui_url(hass: HomeAssistant, mock_cloud_fixture) -> None: +@pytest.mark.usefixtures("mock_cloud_fixture") +async def test_remote_ui_url(hass: HomeAssistant) -> None: """Test getting remote ui url.""" cl = hass.data[DATA_CLOUD] From 6a370bde34c96d06dbc9129124ad3439814733b7 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 25 Jun 2024 17:08:10 +0200 Subject: [PATCH 1173/1445] Adjust imports in samsungtv tests (#120409) --- tests/components/samsungtv/test_init.py | 21 ++-- .../components/samsungtv/test_media_player.py | 108 +++++++++--------- tests/components/samsungtv/test_remote.py | 4 +- 3 files changed, 68 insertions(+), 65 deletions(-) diff --git a/tests/components/samsungtv/test_init.py b/tests/components/samsungtv/test_init.py index 479664d4ec0..5715bd4b0aa 100644 --- a/tests/components/samsungtv/test_init.py +++ b/tests/components/samsungtv/test_init.py @@ -6,13 +6,16 @@ import pytest from samsungtvws.async_remote import SamsungTVWSAsyncRemote from syrupy.assertion import SnapshotAssertion -from homeassistant.components.media_player import DOMAIN, MediaPlayerEntityFeature +from homeassistant.components.media_player import ( + DOMAIN as MP_DOMAIN, + MediaPlayerEntityFeature, +) from homeassistant.components.samsungtv.const import ( CONF_MANUFACTURER, CONF_SESSION_ID, CONF_SSDP_MAIN_TV_AGENT_LOCATION, CONF_SSDP_RENDERING_CONTROL_LOCATION, - DOMAIN as SAMSUNGTV_DOMAIN, + DOMAIN, LEGACY_PORT, METHOD_LEGACY, METHOD_WEBSOCKET, @@ -47,7 +50,7 @@ from .const import ( from tests.common import MockConfigEntry -ENTITY_ID = f"{DOMAIN}.fake_name" +ENTITY_ID = f"{MP_DOMAIN}.fake_name" MOCK_CONFIG = { CONF_HOST: "fake_host", CONF_NAME: "fake_name", @@ -71,7 +74,7 @@ async def test_setup(hass: HomeAssistant) -> None: # test host and port await hass.services.async_call( - DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID}, True + MP_DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID}, True ) @@ -94,7 +97,7 @@ async def test_setup_without_port_device_offline(hass: HomeAssistant) -> None: ): await setup_samsungtv_entry(hass, MOCK_CONFIG) - config_entries_domain = hass.config_entries.async_entries(SAMSUNGTV_DOMAIN) + config_entries_domain = hass.config_entries.async_entries(DOMAIN) assert len(config_entries_domain) == 1 assert config_entries_domain[0].state is ConfigEntryState.SETUP_RETRY @@ -104,7 +107,7 @@ async def test_setup_without_port_device_online(hass: HomeAssistant) -> None: """Test import from yaml when the device is online.""" await setup_samsungtv_entry(hass, MOCK_CONFIG) - config_entries_domain = hass.config_entries.async_entries(SAMSUNGTV_DOMAIN) + config_entries_domain = hass.config_entries.async_entries(DOMAIN) assert len(config_entries_domain) == 1 assert config_entries_domain[0].data[CONF_MAC] == "aa:bb:aa:aa:aa:aa" @@ -183,7 +186,7 @@ async def test_update_imported_legacy_without_method(hass: HomeAssistant) -> Non hass, {CONF_HOST: "fake_host", CONF_MANUFACTURER: "Samsung"} ) - entries = hass.config_entries.async_entries(SAMSUNGTV_DOMAIN) + entries = hass.config_entries.async_entries(DOMAIN) assert len(entries) == 1 assert entries[0].data[CONF_METHOD] == METHOD_LEGACY assert entries[0].data[CONF_PORT] == LEGACY_PORT @@ -214,7 +217,7 @@ async def test_incorrectly_formatted_mac_fixed(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - config_entries = hass.config_entries.async_entries(SAMSUNGTV_DOMAIN) + config_entries = hass.config_entries.async_entries(DOMAIN) assert len(config_entries) == 1 assert config_entries[0].data[CONF_MAC] == "aa:bb:aa:aa:aa:aa" @@ -229,7 +232,7 @@ async def test_cleanup_mac( Reverted due to device registry collisions in #119249 / #119082 """ entry = MockConfigEntry( - domain=SAMSUNGTV_DOMAIN, + domain=DOMAIN, data=MOCK_ENTRY_WS_WITH_MAC, entry_id="123456", unique_id="any", diff --git a/tests/components/samsungtv/test_media_player.py b/tests/components/samsungtv/test_media_player.py index 4c7ee0e116d..ef7e58251e8 100644 --- a/tests/components/samsungtv/test_media_player.py +++ b/tests/components/samsungtv/test_media_player.py @@ -31,7 +31,7 @@ from homeassistant.components.media_player import ( ATTR_MEDIA_CONTENT_TYPE, ATTR_MEDIA_VOLUME_LEVEL, ATTR_MEDIA_VOLUME_MUTED, - DOMAIN, + DOMAIN as MP_DOMAIN, SERVICE_PLAY_MEDIA, SERVICE_SELECT_SOURCE, MediaPlayerDeviceClass, @@ -39,7 +39,7 @@ from homeassistant.components.media_player import ( ) from homeassistant.components.samsungtv.const import ( CONF_SSDP_RENDERING_CONTROL_LOCATION, - DOMAIN as SAMSUNGTV_DOMAIN, + DOMAIN, ENCRYPTED_WEBSOCKET_PORT, METHOD_ENCRYPTED_WEBSOCKET, METHOD_WEBSOCKET, @@ -91,7 +91,7 @@ from .const import ( from tests.common import MockConfigEntry, async_fire_time_changed -ENTITY_ID = f"{DOMAIN}.fake" +ENTITY_ID = f"{MP_DOMAIN}.fake" MOCK_CONFIGWS = { CONF_HOST: "fake_host", CONF_NAME: "fake", @@ -145,7 +145,7 @@ async def test_setup_websocket(hass: HomeAssistant) -> None: await hass.async_block_till_done() - config_entries = hass.config_entries.async_entries(SAMSUNGTV_DOMAIN) + config_entries = hass.config_entries.async_entries(DOMAIN) assert len(config_entries) == 1 assert config_entries[0].data[CONF_MAC] == "aa:bb:aa:aa:aa:aa" @@ -155,16 +155,16 @@ async def test_setup_websocket_2( hass: HomeAssistant, freezer: FrozenDateTimeFactory, mock_now: datetime ) -> None: """Test setup of platform from config entry.""" - entity_id = f"{DOMAIN}.fake" + entity_id = f"{MP_DOMAIN}.fake" entry = MockConfigEntry( - domain=SAMSUNGTV_DOMAIN, + domain=DOMAIN, data=MOCK_ENTRY_WS, unique_id=entity_id, ) entry.add_to_hass(hass) - config_entries = hass.config_entries.async_entries(SAMSUNGTV_DOMAIN) + config_entries = hass.config_entries.async_entries(DOMAIN) assert len(config_entries) == 1 assert entry is config_entries[0] @@ -549,7 +549,7 @@ async def test_send_key(hass: HomeAssistant, remote: Mock) -> None: """Test for send key.""" await setup_samsungtv_entry(hass, MOCK_CONFIG) await hass.services.async_call( - DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID}, True + MP_DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID}, True ) state = hass.states.get(ENTITY_ID) # key called @@ -563,7 +563,7 @@ async def test_send_key_broken_pipe(hass: HomeAssistant, remote: Mock) -> None: await setup_samsungtv_entry(hass, MOCK_CONFIG) remote.control = Mock(side_effect=BrokenPipeError("Boom")) await hass.services.async_call( - DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID}, True + MP_DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID}, True ) state = hass.states.get(ENTITY_ID) assert state.state == STATE_ON @@ -578,7 +578,7 @@ async def test_send_key_connection_closed_retry_succeed( side_effect=[exceptions.ConnectionClosed("Boom"), DEFAULT_MOCK, DEFAULT_MOCK] ) await hass.services.async_call( - DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID}, True + MP_DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID}, True ) state = hass.states.get(ENTITY_ID) # key because of retry two times @@ -595,7 +595,7 @@ async def test_send_key_unhandled_response(hass: HomeAssistant, remote: Mock) -> await setup_samsungtv_entry(hass, MOCK_CONFIG) remote.control = Mock(side_effect=exceptions.UnhandledResponse("Boom")) await hass.services.async_call( - DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID}, True + MP_DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID}, True ) state = hass.states.get(ENTITY_ID) assert state.state == STATE_ON @@ -607,7 +607,7 @@ async def test_send_key_websocketexception(hass: HomeAssistant, remotews: Mock) await setup_samsungtv_entry(hass, MOCK_CONFIGWS) remotews.send_commands = Mock(side_effect=WebSocketException("Boom")) await hass.services.async_call( - DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID}, True + MP_DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID}, True ) state = hass.states.get(ENTITY_ID) assert state.state == STATE_ON @@ -621,7 +621,7 @@ async def test_send_key_websocketexception_encrypted( await setup_samsungtv_entry(hass, MOCK_ENTRYDATA_ENCRYPTED_WS) remoteencws.send_commands = Mock(side_effect=WebSocketException("Boom")) await hass.services.async_call( - DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID}, True + MP_DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID}, True ) state = hass.states.get(ENTITY_ID) assert state.state == STATE_ON @@ -633,7 +633,7 @@ async def test_send_key_os_error_ws(hass: HomeAssistant, remotews: Mock) -> None await setup_samsungtv_entry(hass, MOCK_CONFIGWS) remotews.send_commands = Mock(side_effect=OSError("Boom")) await hass.services.async_call( - DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID}, True + MP_DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID}, True ) state = hass.states.get(ENTITY_ID) assert state.state == STATE_ON @@ -647,7 +647,7 @@ async def test_send_key_os_error_ws_encrypted( await setup_samsungtv_entry(hass, MOCK_ENTRYDATA_ENCRYPTED_WS) remoteencws.send_commands = Mock(side_effect=OSError("Boom")) await hass.services.async_call( - DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID}, True + MP_DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID}, True ) state = hass.states.get(ENTITY_ID) assert state.state == STATE_ON @@ -658,7 +658,7 @@ async def test_send_key_os_error(hass: HomeAssistant, remote: Mock) -> None: await setup_samsungtv_entry(hass, MOCK_CONFIG) remote.control = Mock(side_effect=OSError("Boom")) await hass.services.async_call( - DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID}, True + MP_DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID}, True ) state = hass.states.get(ENTITY_ID) assert state.state == STATE_ON @@ -677,12 +677,12 @@ async def test_state(hass: HomeAssistant, freezer: FrozenDateTimeFactory) -> Non """Test for state property.""" await setup_samsungtv_entry(hass, MOCK_CONFIG) await hass.services.async_call( - DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID}, True + MP_DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID}, True ) state = hass.states.get(ENTITY_ID) assert state.state == STATE_ON await hass.services.async_call( - DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_ID}, True + MP_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_ID}, True ) state = hass.states.get(ENTITY_ID) # Should be STATE_UNAVAILABLE after the timer expires @@ -733,7 +733,7 @@ async def test_turn_off_websocket( remotews.send_commands.reset_mock() await hass.services.async_call( - DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_ID}, True + MP_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_ID}, True ) # key called assert remotews.send_commands.call_count == 1 @@ -745,11 +745,11 @@ async def test_turn_off_websocket( # commands not sent : power off in progress remotews.send_commands.reset_mock() await hass.services.async_call( - DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID}, True + MP_DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID}, True ) assert "TV is powering off, not sending keys: ['KEY_VOLUP']" in caplog.text await hass.services.async_call( - DOMAIN, + MP_DOMAIN, SERVICE_SELECT_SOURCE, {ATTR_ENTITY_ID: ENTITY_ID, ATTR_INPUT_SOURCE: "Deezer"}, True, @@ -772,7 +772,7 @@ async def test_turn_off_websocket_frame( remotews.send_commands.reset_mock() await hass.services.async_call( - DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_ID}, True + MP_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_ID}, True ) # key called assert remotews.send_commands.call_count == 1 @@ -800,7 +800,7 @@ async def test_turn_off_encrypted_websocket( caplog.clear() await hass.services.async_call( - DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_ID}, True + MP_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_ID}, True ) # key called assert remoteencws.send_commands.call_count == 1 @@ -815,7 +815,7 @@ async def test_turn_off_encrypted_websocket( # commands not sent : power off in progress remoteencws.send_commands.reset_mock() await hass.services.async_call( - DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID}, True + MP_DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID}, True ) assert "TV is powering off, not sending keys: ['KEY_VOLUP']" in caplog.text remoteencws.send_commands.assert_not_called() @@ -841,7 +841,7 @@ async def test_turn_off_encrypted_websocket_key_type( caplog.clear() await hass.services.async_call( - DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_ID}, True + MP_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_ID}, True ) # key called assert remoteencws.send_commands.call_count == 1 @@ -856,7 +856,7 @@ async def test_turn_off_legacy(hass: HomeAssistant, remote: Mock) -> None: """Test for turn_off.""" await setup_samsungtv_entry(hass, MOCK_CONFIG) await hass.services.async_call( - DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_ID}, True + MP_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_ID}, True ) # key called assert remote.control.call_count == 1 @@ -871,7 +871,7 @@ async def test_turn_off_os_error( await setup_samsungtv_entry(hass, MOCK_CONFIG) remote.close = Mock(side_effect=OSError("BOOM")) await hass.services.async_call( - DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_ID}, True + MP_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_ID}, True ) assert "Could not establish connection" in caplog.text @@ -885,7 +885,7 @@ async def test_turn_off_ws_os_error( await setup_samsungtv_entry(hass, MOCK_CONFIGWS) remotews.close = Mock(side_effect=OSError("BOOM")) await hass.services.async_call( - DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_ID}, True + MP_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_ID}, True ) assert "Error closing connection" in caplog.text @@ -899,7 +899,7 @@ async def test_turn_off_encryptedws_os_error( await setup_samsungtv_entry(hass, MOCK_ENTRYDATA_ENCRYPTED_WS) remoteencws.close = Mock(side_effect=OSError("BOOM")) await hass.services.async_call( - DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_ID}, True + MP_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_ID}, True ) assert "Error closing connection" in caplog.text @@ -908,7 +908,7 @@ async def test_volume_up(hass: HomeAssistant, remote: Mock) -> None: """Test for volume_up.""" await setup_samsungtv_entry(hass, MOCK_CONFIG) await hass.services.async_call( - DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID}, True + MP_DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID}, True ) # key called assert remote.control.call_count == 1 @@ -919,7 +919,7 @@ async def test_volume_down(hass: HomeAssistant, remote: Mock) -> None: """Test for volume_down.""" await setup_samsungtv_entry(hass, MOCK_CONFIG) await hass.services.async_call( - DOMAIN, SERVICE_VOLUME_DOWN, {ATTR_ENTITY_ID: ENTITY_ID}, True + MP_DOMAIN, SERVICE_VOLUME_DOWN, {ATTR_ENTITY_ID: ENTITY_ID}, True ) # key called assert remote.control.call_count == 1 @@ -930,7 +930,7 @@ async def test_mute_volume(hass: HomeAssistant, remote: Mock) -> None: """Test for mute_volume.""" await setup_samsungtv_entry(hass, MOCK_CONFIG) await hass.services.async_call( - DOMAIN, + MP_DOMAIN, SERVICE_VOLUME_MUTE, {ATTR_ENTITY_ID: ENTITY_ID, ATTR_MEDIA_VOLUME_MUTED: True}, True, @@ -944,14 +944,14 @@ async def test_media_play(hass: HomeAssistant, remote: Mock) -> None: """Test for media_play.""" await setup_samsungtv_entry(hass, MOCK_CONFIG) await hass.services.async_call( - DOMAIN, SERVICE_MEDIA_PLAY, {ATTR_ENTITY_ID: ENTITY_ID}, True + MP_DOMAIN, SERVICE_MEDIA_PLAY, {ATTR_ENTITY_ID: ENTITY_ID}, True ) # key called assert remote.control.call_count == 1 assert remote.control.call_args_list == [call("KEY_PLAY")] await hass.services.async_call( - DOMAIN, SERVICE_MEDIA_PLAY_PAUSE, {ATTR_ENTITY_ID: ENTITY_ID}, True + MP_DOMAIN, SERVICE_MEDIA_PLAY_PAUSE, {ATTR_ENTITY_ID: ENTITY_ID}, True ) # key called assert remote.control.call_count == 2 @@ -962,14 +962,14 @@ async def test_media_pause(hass: HomeAssistant, remote: Mock) -> None: """Test for media_pause.""" await setup_samsungtv_entry(hass, MOCK_CONFIG) await hass.services.async_call( - DOMAIN, SERVICE_MEDIA_PAUSE, {ATTR_ENTITY_ID: ENTITY_ID}, True + MP_DOMAIN, SERVICE_MEDIA_PAUSE, {ATTR_ENTITY_ID: ENTITY_ID}, True ) # key called assert remote.control.call_count == 1 assert remote.control.call_args_list == [call("KEY_PAUSE")] await hass.services.async_call( - DOMAIN, SERVICE_MEDIA_PLAY_PAUSE, {ATTR_ENTITY_ID: ENTITY_ID}, True + MP_DOMAIN, SERVICE_MEDIA_PLAY_PAUSE, {ATTR_ENTITY_ID: ENTITY_ID}, True ) # key called assert remote.control.call_count == 2 @@ -980,7 +980,7 @@ async def test_media_next_track(hass: HomeAssistant, remote: Mock) -> None: """Test for media_next_track.""" await setup_samsungtv_entry(hass, MOCK_CONFIG) await hass.services.async_call( - DOMAIN, SERVICE_MEDIA_NEXT_TRACK, {ATTR_ENTITY_ID: ENTITY_ID}, True + MP_DOMAIN, SERVICE_MEDIA_NEXT_TRACK, {ATTR_ENTITY_ID: ENTITY_ID}, True ) # key called assert remote.control.call_count == 1 @@ -991,7 +991,7 @@ async def test_media_previous_track(hass: HomeAssistant, remote: Mock) -> None: """Test for media_previous_track.""" await setup_samsungtv_entry(hass, MOCK_CONFIG) await hass.services.async_call( - DOMAIN, SERVICE_MEDIA_PREVIOUS_TRACK, {ATTR_ENTITY_ID: ENTITY_ID}, True + MP_DOMAIN, SERVICE_MEDIA_PREVIOUS_TRACK, {ATTR_ENTITY_ID: ENTITY_ID}, True ) # key called assert remote.control.call_count == 1 @@ -1002,7 +1002,7 @@ async def test_media_previous_track(hass: HomeAssistant, remote: Mock) -> None: async def test_turn_on_wol(hass: HomeAssistant) -> None: """Test turn on.""" entry = MockConfigEntry( - domain=SAMSUNGTV_DOMAIN, + domain=DOMAIN, data=MOCK_ENTRY_WS_WITH_MAC, unique_id="any", ) @@ -1013,7 +1013,7 @@ async def test_turn_on_wol(hass: HomeAssistant) -> None: "homeassistant.components.samsungtv.entity.send_magic_packet" ) as mock_send_magic_packet: await hass.services.async_call( - DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: ENTITY_ID}, True + MP_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: ENTITY_ID}, True ) await hass.async_block_till_done() assert mock_send_magic_packet.called @@ -1024,7 +1024,7 @@ async def test_turn_on_without_turnon(hass: HomeAssistant, remote: Mock) -> None await setup_samsungtv_entry(hass, MOCK_CONFIG) with pytest.raises(HomeAssistantError, match="does not support this service"): await hass.services.async_call( - DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: ENTITY_ID}, True + MP_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: ENTITY_ID}, True ) # nothing called as not supported feature assert remote.control.call_count == 0 @@ -1035,7 +1035,7 @@ async def test_play_media(hass: HomeAssistant, remote: Mock) -> None: await setup_samsungtv_entry(hass, MOCK_CONFIG) with patch("homeassistant.components.samsungtv.bridge.asyncio.sleep") as sleep: await hass.services.async_call( - DOMAIN, + MP_DOMAIN, SERVICE_PLAY_MEDIA, { ATTR_ENTITY_ID: ENTITY_ID, @@ -1062,7 +1062,7 @@ async def test_play_media_invalid_type(hass: HomeAssistant) -> None: await setup_samsungtv_entry(hass, MOCK_CONFIG) remote.reset_mock() await hass.services.async_call( - DOMAIN, + MP_DOMAIN, SERVICE_PLAY_MEDIA, { ATTR_ENTITY_ID: ENTITY_ID, @@ -1082,7 +1082,7 @@ async def test_play_media_channel_as_string(hass: HomeAssistant) -> None: await setup_samsungtv_entry(hass, MOCK_CONFIG) remote.reset_mock() await hass.services.async_call( - DOMAIN, + MP_DOMAIN, SERVICE_PLAY_MEDIA, { ATTR_ENTITY_ID: ENTITY_ID, @@ -1101,7 +1101,7 @@ async def test_play_media_channel_as_non_positive(hass: HomeAssistant) -> None: await setup_samsungtv_entry(hass, MOCK_CONFIG) remote.reset_mock() await hass.services.async_call( - DOMAIN, + MP_DOMAIN, SERVICE_PLAY_MEDIA, { ATTR_ENTITY_ID: ENTITY_ID, @@ -1118,7 +1118,7 @@ async def test_select_source(hass: HomeAssistant, remote: Mock) -> None: """Test for select_source.""" await setup_samsungtv_entry(hass, MOCK_CONFIG) await hass.services.async_call( - DOMAIN, + MP_DOMAIN, SERVICE_SELECT_SOURCE, {ATTR_ENTITY_ID: ENTITY_ID, ATTR_INPUT_SOURCE: "HDMI"}, True, @@ -1134,7 +1134,7 @@ async def test_select_source_invalid_source(hass: HomeAssistant) -> None: await setup_samsungtv_entry(hass, MOCK_CONFIG) remote.reset_mock() await hass.services.async_call( - DOMAIN, + MP_DOMAIN, SERVICE_SELECT_SOURCE, {ATTR_ENTITY_ID: ENTITY_ID, ATTR_INPUT_SOURCE: "INVALID"}, True, @@ -1150,7 +1150,7 @@ async def test_play_media_app(hass: HomeAssistant, remotews: Mock) -> None: remotews.send_commands.reset_mock() await hass.services.async_call( - DOMAIN, + MP_DOMAIN, SERVICE_PLAY_MEDIA, { ATTR_ENTITY_ID: ENTITY_ID, @@ -1174,7 +1174,7 @@ async def test_select_source_app(hass: HomeAssistant, remotews: Mock) -> None: remotews.send_commands.reset_mock() await hass.services.async_call( - DOMAIN, + MP_DOMAIN, SERVICE_SELECT_SOURCE, {ATTR_ENTITY_ID: ENTITY_ID, ATTR_INPUT_SOURCE: "Deezer"}, True, @@ -1199,7 +1199,7 @@ async def test_websocket_unsupported_remote_control( remotews.send_commands.reset_mock() await hass.services.async_call( - DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_ID}, True + MP_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_ID}, True ) remotews.raise_mock_ws_event_callback( "ms.error", @@ -1248,7 +1248,7 @@ async def test_volume_control_upnp( # Upnp action succeeds await hass.services.async_call( - DOMAIN, + MP_DOMAIN, SERVICE_VOLUME_SET, {ATTR_ENTITY_ID: ENTITY_ID, ATTR_MEDIA_VOLUME_LEVEL: 0.5}, True, @@ -1262,7 +1262,7 @@ async def test_volume_control_upnp( status=500, error_code=501, error_desc="Action Failed" ) await hass.services.async_call( - DOMAIN, + MP_DOMAIN, SERVICE_VOLUME_SET, {ATTR_ENTITY_ID: ENTITY_ID, ATTR_MEDIA_VOLUME_LEVEL: 0.6}, True, @@ -1281,7 +1281,7 @@ async def test_upnp_not_available( # Upnp action fails await hass.services.async_call( - DOMAIN, + MP_DOMAIN, SERVICE_VOLUME_SET, {ATTR_ENTITY_ID: ENTITY_ID, ATTR_MEDIA_VOLUME_LEVEL: 0.6}, True, @@ -1299,7 +1299,7 @@ async def test_upnp_missing_service( # Upnp action fails await hass.services.async_call( - DOMAIN, + MP_DOMAIN, SERVICE_VOLUME_SET, {ATTR_ENTITY_ID: ENTITY_ID, ATTR_MEDIA_VOLUME_LEVEL: 0.6}, True, diff --git a/tests/components/samsungtv/test_remote.py b/tests/components/samsungtv/test_remote.py index 98cf712e0d2..854c92207bf 100644 --- a/tests/components/samsungtv/test_remote.py +++ b/tests/components/samsungtv/test_remote.py @@ -10,7 +10,7 @@ from homeassistant.components.remote import ( DOMAIN as REMOTE_DOMAIN, SERVICE_SEND_COMMAND, ) -from homeassistant.components.samsungtv.const import DOMAIN as SAMSUNGTV_DOMAIN +from homeassistant.components.samsungtv.const import DOMAIN from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError @@ -102,7 +102,7 @@ async def test_send_command_service(hass: HomeAssistant, remoteencws: Mock) -> N async def test_turn_on_wol(hass: HomeAssistant) -> None: """Test turn on.""" entry = MockConfigEntry( - domain=SAMSUNGTV_DOMAIN, + domain=DOMAIN, data=MOCK_ENTRY_WS_WITH_MAC, unique_id="any", ) From 253514a1242bf1ecc1236e6a7b1d1628ea405df6 Mon Sep 17 00:00:00 2001 From: Kevin Stillhammer Date: Tue, 25 Jun 2024 17:08:36 +0200 Subject: [PATCH 1174/1445] Bump pywaze to 1.0.2 (#120412) --- homeassistant/components/waze_travel_time/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/waze_travel_time/manifest.json b/homeassistant/components/waze_travel_time/manifest.json index ce7c9105781..9d615431c7d 100644 --- a/homeassistant/components/waze_travel_time/manifest.json +++ b/homeassistant/components/waze_travel_time/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/waze_travel_time", "iot_class": "cloud_polling", "loggers": ["pywaze", "homeassistant.helpers.location"], - "requirements": ["pywaze==1.0.1"] + "requirements": ["pywaze==1.0.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 2b1b393d1f4..48eae313cf4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2391,7 +2391,7 @@ pyvlx==0.2.21 pyvolumio==0.1.5 # homeassistant.components.waze_travel_time -pywaze==1.0.1 +pywaze==1.0.2 # homeassistant.components.weatherflow pyweatherflowudp==1.4.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 135581ff6ea..3b3adcf409a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1870,7 +1870,7 @@ pyvlx==0.2.21 pyvolumio==0.1.5 # homeassistant.components.waze_travel_time -pywaze==1.0.1 +pywaze==1.0.2 # homeassistant.components.weatherflow pyweatherflowudp==1.4.5 From 77fea8a73eb271d35c5e359f9a62567c1d760a32 Mon Sep 17 00:00:00 2001 From: "Mr. Bubbles" Date: Tue, 25 Jun 2024 17:15:12 +0200 Subject: [PATCH 1175/1445] Add reauth flow to pyLoad integration (#120376) Add reauth flow --- homeassistant/components/pyload/__init__.py | 4 +- .../components/pyload/config_flow.py | 77 +++++++++++++++- .../components/pyload/coordinator.py | 6 +- homeassistant/components/pyload/strings.json | 10 ++- tests/components/pyload/conftest.py | 13 +++ tests/components/pyload/test_config_flow.py | 90 ++++++++++++++++++- tests/components/pyload/test_init.py | 4 +- 7 files changed, 192 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/pyload/__init__.py b/homeassistant/components/pyload/__init__.py index b30b044e238..8bf065797e5 100644 --- a/homeassistant/components/pyload/__init__.py +++ b/homeassistant/components/pyload/__init__.py @@ -17,7 +17,7 @@ from homeassistant.const import ( Platform, ) from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryError, ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_create_clientsession from .coordinator import PyLoadCoordinator @@ -56,7 +56,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: PyLoadConfigEntry) -> bo except ParserError as e: raise ConfigEntryNotReady("Unable to parse data from pyLoad API") from e except InvalidAuth as e: - raise ConfigEntryError( + raise ConfigEntryAuthFailed( f"Authentication failed for {entry.data[CONF_USERNAME]}, check your login credentials" ) from e coordinator = PyLoadCoordinator(hass, pyloadapi) diff --git a/homeassistant/components/pyload/config_flow.py b/homeassistant/components/pyload/config_flow.py index 7ebc4a501d4..7a2dfddeb5b 100644 --- a/homeassistant/components/pyload/config_flow.py +++ b/homeassistant/components/pyload/config_flow.py @@ -2,8 +2,9 @@ from __future__ import annotations +from collections.abc import Mapping import logging -from typing import Any +from typing import TYPE_CHECKING, Any from aiohttp import CookieJar from pyloadapi.api import PyLoadAPI @@ -23,7 +24,13 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_create_clientsession import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.selector import ( + TextSelector, + TextSelectorConfig, + TextSelectorType, +) +from . import PyLoadConfigEntry from .const import DEFAULT_HOST, DEFAULT_NAME, DEFAULT_PORT, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -39,6 +46,23 @@ STEP_USER_DATA_SCHEMA = vol.Schema( } ) +REAUTH_SCHEMA = vol.Schema( + { + vol.Required(CONF_USERNAME): TextSelector( + TextSelectorConfig( + type=TextSelectorType.TEXT, + autocomplete="username", + ), + ), + vol.Required(CONF_PASSWORD): TextSelector( + TextSelectorConfig( + type=TextSelectorType.PASSWORD, + autocomplete="current-password", + ), + ), + } +) + async def validate_input(hass: HomeAssistant, user_input: dict[str, Any]) -> None: """Validate the user input and try to connect to PyLoad.""" @@ -67,8 +91,7 @@ class PyLoadConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for pyLoad.""" VERSION = 1 - # store values from yaml import so we can use them as - # suggested values when the configuration step is resumed + config_entry: PyLoadConfigEntry | None async def async_step_user( self, user_input: dict[str, Any] | None = None @@ -118,3 +141,51 @@ class PyLoadConfigFlow(ConfigFlow, domain=DOMAIN): if errors := result.get("errors"): return self.async_abort(reason=errors["base"]) return result + + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: + """Perform reauth upon an API authentication error.""" + self.config_entry = self.hass.config_entries.async_get_entry( + self.context["entry_id"] + ) + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Dialog that informs the user that reauth is required.""" + errors = {} + + if TYPE_CHECKING: + assert self.config_entry + + if user_input is not None: + new_input = self.config_entry.data | user_input + try: + await validate_input(self.hass, new_input) + except (CannotConnect, ParserError): + errors["base"] = "cannot_connect" + except InvalidAuth: + errors["base"] = "invalid_auth" + except Exception: + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + return self.async_update_reload_and_abort( + self.config_entry, data=new_input + ) + + return self.async_show_form( + step_id="reauth_confirm", + data_schema=self.add_suggested_values_to_schema( + REAUTH_SCHEMA, + { + CONF_USERNAME: user_input[CONF_USERNAME] + if user_input is not None + else self.config_entry.data[CONF_USERNAME] + }, + ), + description_placeholders={CONF_NAME: self.config_entry.data[CONF_USERNAME]}, + errors=errors, + ) diff --git a/homeassistant/components/pyload/coordinator.py b/homeassistant/components/pyload/coordinator.py index 008375c3a34..b96a8d2ccbf 100644 --- a/homeassistant/components/pyload/coordinator.py +++ b/homeassistant/components/pyload/coordinator.py @@ -8,7 +8,7 @@ from pyloadapi import CannotConnect, InvalidAuth, ParserError, PyLoadAPI from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryError +from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import DOMAIN @@ -63,12 +63,12 @@ class PyLoadCoordinator(DataUpdateCoordinator[pyLoadData]): try: await self.pyload.login() except InvalidAuth as exc: - raise ConfigEntryError( + raise ConfigEntryAuthFailed( f"Authentication failed for {self.pyload.username}, check your login credentials", ) from exc raise UpdateFailed( - "Unable to retrieve data due to cookie expiration but re-authentication was successful." + "Unable to retrieve data due to cookie expiration" ) from e except CannotConnect as e: raise UpdateFailed( diff --git a/homeassistant/components/pyload/strings.json b/homeassistant/components/pyload/strings.json index 94c0c29d286..6efdb23eaf4 100644 --- a/homeassistant/components/pyload/strings.json +++ b/homeassistant/components/pyload/strings.json @@ -16,6 +16,13 @@ "host": "The hostname or IP address of the device running your pyLoad instance.", "port": "pyLoad uses port 8000 by default." } + }, + "reauth_confirm": { + "title": "[%key:common::config_flow::title::reauth%]", + "data": { + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]" + } } }, "error": { @@ -24,7 +31,8 @@ "unknown": "[%key:common::config_flow::error::unknown%]" }, "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" } }, "entity": { diff --git a/tests/components/pyload/conftest.py b/tests/components/pyload/conftest.py index 3c6f9fdb49a..1d7b11567c7 100644 --- a/tests/components/pyload/conftest.py +++ b/tests/components/pyload/conftest.py @@ -41,6 +41,19 @@ YAML_INPUT = { CONF_SSL: True, CONF_USERNAME: "test-username", } +REAUTH_INPUT = { + CONF_PASSWORD: "new-password", + CONF_USERNAME: "new-username", +} + +NEW_INPUT = { + CONF_HOST: "pyload.local", + CONF_PASSWORD: "new-password", + CONF_PORT: 8000, + CONF_SSL: True, + CONF_USERNAME: "new-username", + CONF_VERIFY_SSL: False, +} @pytest.fixture diff --git a/tests/components/pyload/test_config_flow.py b/tests/components/pyload/test_config_flow.py index 70d324fd980..63297de7127 100644 --- a/tests/components/pyload/test_config_flow.py +++ b/tests/components/pyload/test_config_flow.py @@ -6,11 +6,11 @@ from pyloadapi.exceptions import CannotConnect, InvalidAuth, ParserError import pytest from homeassistant.components.pyload.const import DEFAULT_NAME, DOMAIN -from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER +from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_REAUTH, SOURCE_USER from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType -from .conftest import USER_INPUT, YAML_INPUT +from .conftest import NEW_INPUT, REAUTH_INPUT, USER_INPUT, YAML_INPUT from tests.common import MockConfigEntry @@ -164,3 +164,89 @@ async def test_flow_import_errors( assert result["type"] is FlowResultType.ABORT assert result["reason"] == reason + + +async def test_reauth( + hass: HomeAssistant, + config_entry: MockConfigEntry, + mock_pyloadapi: AsyncMock, +) -> None: + """Test reauth flow.""" + + config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": SOURCE_REAUTH, + "entry_id": config_entry.entry_id, + "unique_id": config_entry.unique_id, + }, + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + REAUTH_INPUT, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reauth_successful" + assert config_entry.data == NEW_INPUT + assert len(hass.config_entries.async_entries()) == 1 + + +@pytest.mark.parametrize( + ("side_effect", "error_text"), + [ + (InvalidAuth, "invalid_auth"), + (CannotConnect, "cannot_connect"), + (IndexError, "unknown"), + ], +) +async def test_reauth_errors( + hass: HomeAssistant, + config_entry: MockConfigEntry, + mock_pyloadapi: AsyncMock, + side_effect: Exception, + error_text: str, +) -> None: + """Test reauth flow.""" + + config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": SOURCE_REAUTH, + "entry_id": config_entry.entry_id, + "unique_id": config_entry.unique_id, + }, + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + + mock_pyloadapi.login.side_effect = side_effect + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + REAUTH_INPUT, + ) + + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"base": error_text} + + mock_pyloadapi.login.side_effect = None + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + REAUTH_INPUT, + ) + + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reauth_successful" + assert config_entry.data == NEW_INPUT + assert len(hass.config_entries.async_entries()) == 1 diff --git a/tests/components/pyload/test_init.py b/tests/components/pyload/test_init.py index a1ecf294523..12713ef2e54 100644 --- a/tests/components/pyload/test_init.py +++ b/tests/components/pyload/test_init.py @@ -5,7 +5,7 @@ from unittest.mock import MagicMock from pyloadapi.exceptions import CannotConnect, InvalidAuth, ParserError import pytest -from homeassistant.config_entries import ConfigEntryState +from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry @@ -61,3 +61,5 @@ async def test_config_entry_setup_invalid_auth( await hass.async_block_till_done() assert config_entry.state is ConfigEntryState.SETUP_ERROR + + assert any(config_entry.async_get_active_flows(hass, {SOURCE_REAUTH})) From 2386ed383002facb46424e6dc88ee20a9f83ab9e Mon Sep 17 00:00:00 2001 From: Denis Shulyaka Date: Tue, 25 Jun 2024 18:43:26 +0300 Subject: [PATCH 1176/1445] Add script llm tool (#118936) * Add script llm tool * Add tests * More tests * more test * more test * Add area and floor resolving * coverage * coverage * fix ColorTempSelector * fix mypy * fix mypy * add script reload test * Cache script tool parameters * Make custom_serializer a part of api --------- Co-authored-by: Michael Hansen --- .../conversation.py | 13 +- .../manifest.json | 2 +- .../openai_conversation/conversation.py | 16 +- .../openai_conversation/manifest.json | 2 +- homeassistant/helpers/intent.py | 10 +- homeassistant/helpers/llm.py | 284 ++++++++++++++- homeassistant/helpers/selector.py | 27 +- homeassistant/package_constraints.txt | 1 + pyproject.toml | 1 + requirements.txt | 1 + requirements_all.txt | 4 - requirements_test_all.txt | 4 - tests/helpers/test_llm.py | 327 +++++++++++++++++- tests/helpers/test_selector.py | 2 + 14 files changed, 639 insertions(+), 55 deletions(-) diff --git a/homeassistant/components/google_generative_ai_conversation/conversation.py b/homeassistant/components/google_generative_ai_conversation/conversation.py index 2cfbc09ed08..fb7f5c3b21c 100644 --- a/homeassistant/components/google_generative_ai_conversation/conversation.py +++ b/homeassistant/components/google_generative_ai_conversation/conversation.py @@ -3,6 +3,7 @@ from __future__ import annotations import codecs +from collections.abc import Callable from typing import Any, Literal from google.api_core.exceptions import GoogleAPICallError @@ -89,10 +90,14 @@ def _format_schema(schema: dict[str, Any]) -> dict[str, Any]: return result -def _format_tool(tool: llm.Tool) -> dict[str, Any]: +def _format_tool( + tool: llm.Tool, custom_serializer: Callable[[Any], Any] | None +) -> dict[str, Any]: """Format tool specification.""" - parameters = _format_schema(convert(tool.parameters)) + parameters = _format_schema( + convert(tool.parameters, custom_serializer=custom_serializer) + ) return protos.Tool( { @@ -193,7 +198,9 @@ class GoogleGenerativeAIConversationEntity( f"Error preparing LLM API: {err}", ) return result - tools = [_format_tool(tool) for tool in llm_api.tools] + tools = [ + _format_tool(tool, llm_api.custom_serializer) for tool in llm_api.tools + ] try: prompt = await self._async_render_prompt(user_input, llm_api, llm_context) diff --git a/homeassistant/components/google_generative_ai_conversation/manifest.json b/homeassistant/components/google_generative_ai_conversation/manifest.json index 168fee105a0..9e0dc1ddeab 100644 --- a/homeassistant/components/google_generative_ai_conversation/manifest.json +++ b/homeassistant/components/google_generative_ai_conversation/manifest.json @@ -9,5 +9,5 @@ "integration_type": "service", "iot_class": "cloud_polling", "quality_scale": "platinum", - "requirements": ["google-generativeai==0.6.0", "voluptuous-openapi==0.0.4"] + "requirements": ["google-generativeai==0.6.0"] } diff --git a/homeassistant/components/openai_conversation/conversation.py b/homeassistant/components/openai_conversation/conversation.py index 40242f5c6cc..46be803bcad 100644 --- a/homeassistant/components/openai_conversation/conversation.py +++ b/homeassistant/components/openai_conversation/conversation.py @@ -1,7 +1,8 @@ """Conversation support for OpenAI.""" +from collections.abc import Callable import json -from typing import Literal +from typing import Any, Literal import openai from openai._types import NOT_GIVEN @@ -58,9 +59,14 @@ async def async_setup_entry( async_add_entities([agent]) -def _format_tool(tool: llm.Tool) -> ChatCompletionToolParam: +def _format_tool( + tool: llm.Tool, custom_serializer: Callable[[Any], Any] | None +) -> ChatCompletionToolParam: """Format tool specification.""" - tool_spec = FunctionDefinition(name=tool.name, parameters=convert(tool.parameters)) + tool_spec = FunctionDefinition( + name=tool.name, + parameters=convert(tool.parameters, custom_serializer=custom_serializer), + ) if tool.description: tool_spec["description"] = tool.description return ChatCompletionToolParam(type="function", function=tool_spec) @@ -139,7 +145,9 @@ class OpenAIConversationEntity( return conversation.ConversationResult( response=intent_response, conversation_id=user_input.conversation_id ) - tools = [_format_tool(tool) for tool in llm_api.tools] + tools = [ + _format_tool(tool, llm_api.custom_serializer) for tool in llm_api.tools + ] if user_input.conversation_id is None: conversation_id = ulid.ulid_now() diff --git a/homeassistant/components/openai_conversation/manifest.json b/homeassistant/components/openai_conversation/manifest.json index 480712574c4..0c06a3d4cd8 100644 --- a/homeassistant/components/openai_conversation/manifest.json +++ b/homeassistant/components/openai_conversation/manifest.json @@ -8,5 +8,5 @@ "documentation": "https://www.home-assistant.io/integrations/openai_conversation", "integration_type": "service", "iot_class": "cloud_polling", - "requirements": ["openai==1.3.8", "voluptuous-openapi==0.0.4"] + "requirements": ["openai==1.3.8"] } diff --git a/homeassistant/helpers/intent.py b/homeassistant/helpers/intent.py index b1ddf5eacc7..502b20eaf8f 100644 --- a/homeassistant/helpers/intent.py +++ b/homeassistant/helpers/intent.py @@ -352,7 +352,7 @@ class MatchTargetsCandidate: matched_name: str | None = None -def _find_areas( +def find_areas( name: str, areas: area_registry.AreaRegistry ) -> Iterable[area_registry.AreaEntry]: """Find all areas matching a name (including aliases).""" @@ -372,7 +372,7 @@ def _find_areas( break -def _find_floors( +def find_floors( name: str, floors: floor_registry.FloorRegistry ) -> Iterable[floor_registry.FloorEntry]: """Find all floors matching a name (including aliases).""" @@ -530,7 +530,7 @@ def async_match_targets( # noqa: C901 if not states: return MatchTargetsResult(False, MatchFailedReason.STATE) - # Exit early so we can to avoid registry lookups + # Exit early so we can avoid registry lookups if not ( constraints.name or constraints.features @@ -580,7 +580,7 @@ def async_match_targets( # noqa: C901 if constraints.floor_name: # Filter by areas associated with floor fr = floor_registry.async_get(hass) - targeted_floors = list(_find_floors(constraints.floor_name, fr)) + targeted_floors = list(find_floors(constraints.floor_name, fr)) if not targeted_floors: return MatchTargetsResult( False, @@ -609,7 +609,7 @@ def async_match_targets( # noqa: C901 possible_area_ids = {area.id for area in ar.async_list_areas()} if constraints.area_name: - targeted_areas = list(_find_areas(constraints.area_name, ar)) + targeted_areas = list(find_areas(constraints.area_name, ar)) if not targeted_areas: return MatchTargetsResult( False, diff --git a/homeassistant/helpers/llm.py b/homeassistant/helpers/llm.py index a4e18fdb2c0..480b9cb5237 100644 --- a/homeassistant/helpers/llm.py +++ b/homeassistant/helpers/llm.py @@ -3,6 +3,7 @@ from __future__ import annotations from abc import ABC, abstractmethod +from collections.abc import Callable from dataclasses import dataclass from decimal import Decimal from enum import Enum @@ -11,6 +12,7 @@ from typing import Any import slugify as unicode_slug import voluptuous as vol +from voluptuous_openapi import UNSUPPORTED, convert from homeassistant.components.climate.intent import INTENT_GET_TEMPERATURE from homeassistant.components.conversation.trace import ( @@ -20,22 +22,39 @@ from homeassistant.components.conversation.trace import ( from homeassistant.components.cover.intent import INTENT_CLOSE_COVER, INTENT_OPEN_COVER from homeassistant.components.homeassistant.exposed_entities import async_should_expose from homeassistant.components.intent import async_device_supports_timers +from homeassistant.components.script import ATTR_VARIABLES, DOMAIN as SCRIPT_DOMAIN from homeassistant.components.weather.intent import INTENT_GET_WEATHER -from homeassistant.core import Context, HomeAssistant, callback +from homeassistant.const import ( + ATTR_DOMAIN, + ATTR_ENTITY_ID, + ATTR_SERVICE, + EVENT_HOMEASSISTANT_CLOSE, + EVENT_SERVICE_REMOVED, + SERVICE_TURN_ON, +) +from homeassistant.core import Context, Event, HomeAssistant, callback, split_entity_id from homeassistant.exceptions import HomeAssistantError from homeassistant.util import yaml +from homeassistant.util.hass_dict import HassKey from homeassistant.util.json import JsonObjectType from . import ( area_registry as ar, + config_validation as cv, device_registry as dr, entity_registry as er, floor_registry as fr, intent, + selector, service, ) from .singleton import singleton +SCRIPT_PARAMETERS_CACHE: HassKey[dict[str, tuple[str | None, vol.Schema]]] = HassKey( + "llm_script_parameters_cache" +) + + LLM_API_ASSIST = "assist" BASE_PROMPT = ( @@ -143,6 +162,7 @@ class APIInstance: api_prompt: str llm_context: LLMContext tools: list[Tool] + custom_serializer: Callable[[Any], Any] | None = None async def async_call_tool(self, tool_input: ToolInput) -> JsonObjectType: """Call a LLM tool, validate args and return the response.""" @@ -284,6 +304,7 @@ class AssistAPI(API): api_prompt=self._async_get_api_prompt(llm_context, exposed_entities), llm_context=llm_context, tools=self._async_get_tools(llm_context, exposed_entities), + custom_serializer=_selector_serializer, ) @callback @@ -372,7 +393,7 @@ class AssistAPI(API): exposed_domains: set[str] | None = None if exposed_entities is not None: exposed_domains = { - entity_id.split(".")[0] for entity_id in exposed_entities + split_entity_id(entity_id)[0] for entity_id in exposed_entities } intent_handlers = [ intent_handler @@ -381,11 +402,22 @@ class AssistAPI(API): or intent_handler.platforms & exposed_domains ] - return [ + tools: list[Tool] = [ IntentTool(self.cached_slugify(intent_handler.intent_type), intent_handler) for intent_handler in intent_handlers ] + if llm_context.assistant is not None: + for state in self.hass.states.async_all(SCRIPT_DOMAIN): + if not async_should_expose( + self.hass, llm_context.assistant, state.entity_id + ): + continue + + tools.append(ScriptTool(self.hass, state.entity_id)) + + return tools + def _get_exposed_entities( hass: HomeAssistant, assistant: str @@ -413,13 +445,15 @@ def _get_exposed_entities( entities = {} for state in hass.states.async_all(): + if state.domain == SCRIPT_DOMAIN: + continue + if not async_should_expose(hass, assistant, state.entity_id): continue entity_entry = entity_registry.async_get(state.entity_id) names = [state.name] area_names = [] - description: str | None = None if entity_entry is not None: names.extend(entity_entry.aliases) @@ -439,25 +473,11 @@ def _get_exposed_entities( area_names.append(area.name) area_names.extend(area.aliases) - if ( - state.domain == "script" - and entity_entry.unique_id - and ( - service_desc := service.async_get_cached_service_description( - hass, "script", entity_entry.unique_id - ) - ) - ): - description = service_desc.get("description") - info: dict[str, Any] = { "names": ", ".join(names), "state": state.state, } - if description: - info["description"] = description - if area_names: info["areas"] = ", ".join(area_names) @@ -473,3 +493,231 @@ def _get_exposed_entities( entities[state.entity_id] = info return entities + + +def _selector_serializer(schema: Any) -> Any: # noqa: C901 + """Convert selectors into OpenAPI schema.""" + if not isinstance(schema, selector.Selector): + return UNSUPPORTED + + if isinstance(schema, selector.BackupLocationSelector): + return {"type": "string", "pattern": "^(?:\\/backup|\\w+)$"} + + if isinstance(schema, selector.BooleanSelector): + return {"type": "boolean"} + + if isinstance(schema, selector.ColorRGBSelector): + return { + "type": "array", + "items": {"type": "number"}, + "minItems": 3, + "maxItems": 3, + "format": "RGB", + } + + if isinstance(schema, selector.ConditionSelector): + return convert(cv.CONDITIONS_SCHEMA) + + if isinstance(schema, selector.ConstantSelector): + return {"enum": [schema.config["value"]]} + + result: dict[str, Any] + if isinstance(schema, selector.ColorTempSelector): + result = {"type": "number"} + if "min" in schema.config: + result["minimum"] = schema.config["min"] + elif "min_mireds" in schema.config: + result["minimum"] = schema.config["min_mireds"] + if "max" in schema.config: + result["maximum"] = schema.config["max"] + elif "max_mireds" in schema.config: + result["maximum"] = schema.config["max_mireds"] + return result + + if isinstance(schema, selector.CountrySelector): + if schema.config.get("countries"): + return {"type": "string", "enum": schema.config["countries"]} + return {"type": "string", "format": "ISO 3166-1 alpha-2"} + + if isinstance(schema, selector.DateSelector): + return {"type": "string", "format": "date"} + + if isinstance(schema, selector.DateTimeSelector): + return {"type": "string", "format": "date-time"} + + if isinstance(schema, selector.DurationSelector): + return convert(cv.time_period_dict) + + if isinstance(schema, selector.EntitySelector): + if schema.config.get("multiple"): + return {"type": "array", "items": {"type": "string", "format": "entity_id"}} + + return {"type": "string", "format": "entity_id"} + + if isinstance(schema, selector.LanguageSelector): + if schema.config.get("languages"): + return {"type": "string", "enum": schema.config["languages"]} + return {"type": "string", "format": "RFC 5646"} + + if isinstance(schema, (selector.LocationSelector, selector.MediaSelector)): + return convert(schema.DATA_SCHEMA) + + if isinstance(schema, selector.NumberSelector): + result = {"type": "number"} + if "min" in schema.config: + result["minimum"] = schema.config["min"] + if "max" in schema.config: + result["maximum"] = schema.config["max"] + return result + + if isinstance(schema, selector.ObjectSelector): + return {"type": "object"} + + if isinstance(schema, selector.SelectSelector): + options = [ + x["value"] if isinstance(x, dict) else x for x in schema.config["options"] + ] + if schema.config.get("multiple"): + return { + "type": "array", + "items": {"type": "string", "enum": options}, + "uniqueItems": True, + } + return {"type": "string", "enum": options} + + if isinstance(schema, selector.TargetSelector): + return convert(cv.TARGET_SERVICE_FIELDS) + + if isinstance(schema, selector.TemplateSelector): + return {"type": "string", "format": "jinja2"} + + if isinstance(schema, selector.TimeSelector): + return {"type": "string", "format": "time"} + + if isinstance(schema, selector.TriggerSelector): + return convert(cv.TRIGGER_SCHEMA) + + if schema.config.get("multiple"): + return {"type": "array", "items": {"type": "string"}} + + return {"type": "string"} + + +class ScriptTool(Tool): + """LLM Tool representing a Script.""" + + def __init__( + self, + hass: HomeAssistant, + script_entity_id: str, + ) -> None: + """Init the class.""" + entity_registry = er.async_get(hass) + + self.name = split_entity_id(script_entity_id)[1] + self.parameters = vol.Schema({}) + entity_entry = entity_registry.async_get(script_entity_id) + if entity_entry and entity_entry.unique_id: + parameters_cache = hass.data.get(SCRIPT_PARAMETERS_CACHE) + + if parameters_cache is None: + parameters_cache = hass.data[SCRIPT_PARAMETERS_CACHE] = {} + + @callback + def clear_cache(event: Event) -> None: + """Clear script parameter cache on script reload or delete.""" + if ( + event.data[ATTR_DOMAIN] == SCRIPT_DOMAIN + and event.data[ATTR_SERVICE] in parameters_cache + ): + parameters_cache.pop(event.data[ATTR_SERVICE]) + + cancel = hass.bus.async_listen(EVENT_SERVICE_REMOVED, clear_cache) + + @callback + def on_homeassistant_close(event: Event) -> None: + """Cleanup.""" + cancel() + + hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_CLOSE, on_homeassistant_close + ) + + if entity_entry.unique_id in parameters_cache: + self.description, self.parameters = parameters_cache[ + entity_entry.unique_id + ] + return + + if service_desc := service.async_get_cached_service_description( + hass, SCRIPT_DOMAIN, entity_entry.unique_id + ): + self.description = service_desc.get("description") + schema: dict[vol.Marker, Any] = {} + fields = service_desc.get("fields", {}) + + for field, config in fields.items(): + description = config.get("description") + if not description: + description = config.get("name") + if config.get("required"): + key = vol.Required(field, description=description) + else: + key = vol.Optional(field, description=description) + if "selector" in config: + schema[key] = selector.selector(config["selector"]) + else: + schema[key] = cv.string + + self.parameters = vol.Schema(schema) + + parameters_cache[entity_entry.unique_id] = ( + self.description, + self.parameters, + ) + + async def async_call( + self, hass: HomeAssistant, tool_input: ToolInput, llm_context: LLMContext + ) -> JsonObjectType: + """Run the script.""" + + for field, validator in self.parameters.schema.items(): + if field not in tool_input.tool_args: + continue + if isinstance(validator, selector.AreaSelector): + area_reg = ar.async_get(hass) + if validator.config.get("multiple"): + areas: list[ar.AreaEntry] = [] + for area in tool_input.tool_args[field]: + areas.extend(intent.find_areas(area, area_reg)) + tool_input.tool_args[field] = list({area.id for area in areas}) + else: + area = tool_input.tool_args[field] + area = list(intent.find_areas(area, area_reg))[0].id + tool_input.tool_args[field] = area + + elif isinstance(validator, selector.FloorSelector): + floor_reg = fr.async_get(hass) + if validator.config.get("multiple"): + floors: list[fr.FloorEntry] = [] + for floor in tool_input.tool_args[field]: + floors.extend(intent.find_floors(floor, floor_reg)) + tool_input.tool_args[field] = list( + {floor.floor_id for floor in floors} + ) + else: + floor = tool_input.tool_args[field] + floor = list(intent.find_floors(floor, floor_reg))[0].floor_id + tool_input.tool_args[field] = floor + + await hass.services.async_call( + SCRIPT_DOMAIN, + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: SCRIPT_DOMAIN + "." + self.name, + ATTR_VARIABLES: tool_input.tool_args, + }, + context=llm_context.context, + ) + + return {"success": True} diff --git a/homeassistant/helpers/selector.py b/homeassistant/helpers/selector.py index 1db4dd9f80b..16aaa40db86 100644 --- a/homeassistant/helpers/selector.py +++ b/homeassistant/helpers/selector.py @@ -75,6 +75,13 @@ class Selector[_T: Mapping[str, Any]]: self.config = self.CONFIG_SCHEMA(config) + def __eq__(self, other: object) -> bool: + """Check equality.""" + if not isinstance(other, Selector): + return NotImplemented + + return self.selector_type == other.selector_type and self.config == other.config + def serialize(self) -> dict[str, dict[str, _T]]: """Serialize Selector for voluptuous_serialize.""" return {"selector": {self.selector_type: self.config}} @@ -278,7 +285,7 @@ class AssistPipelineSelector(Selector[AssistPipelineSelectorConfig]): CONFIG_SCHEMA = vol.Schema({}) - def __init__(self, config: AssistPipelineSelectorConfig) -> None: + def __init__(self, config: AssistPipelineSelectorConfig | None = None) -> None: """Instantiate a selector.""" super().__init__(config) @@ -430,10 +437,10 @@ class ColorTempSelector(Selector[ColorTempSelectorConfig]): range_min = self.config.get("min") range_max = self.config.get("max") - if not range_min: + if range_min is None: range_min = self.config.get("min_mireds") - if not range_max: + if range_max is None: range_max = self.config.get("max_mireds") value: int = vol.All( @@ -517,7 +524,7 @@ class ConstantSelector(Selector[ConstantSelectorConfig]): } ) - def __init__(self, config: ConstantSelectorConfig | None = None) -> None: + def __init__(self, config: ConstantSelectorConfig) -> None: """Instantiate a selector.""" super().__init__(config) @@ -560,7 +567,7 @@ class QrCodeSelector(Selector[QrCodeSelectorConfig]): } ) - def __init__(self, config: QrCodeSelectorConfig | None = None) -> None: + def __init__(self, config: QrCodeSelectorConfig) -> None: """Instantiate a selector.""" super().__init__(config) @@ -588,7 +595,7 @@ class ConversationAgentSelector(Selector[ConversationAgentSelectorConfig]): } ) - def __init__(self, config: ConversationAgentSelectorConfig) -> None: + def __init__(self, config: ConversationAgentSelectorConfig | None = None) -> None: """Instantiate a selector.""" super().__init__(config) @@ -820,7 +827,7 @@ class FloorSelectorConfig(TypedDict, total=False): @SELECTORS.register("floor") -class FloorSelector(Selector[AreaSelectorConfig]): +class FloorSelector(Selector[FloorSelectorConfig]): """Selector of a single or list of floors.""" selector_type = "floor" @@ -934,7 +941,7 @@ class LanguageSelector(Selector[LanguageSelectorConfig]): } ) - def __init__(self, config: LanguageSelectorConfig) -> None: + def __init__(self, config: LanguageSelectorConfig | None = None) -> None: """Instantiate a selector.""" super().__init__(config) @@ -1159,7 +1166,7 @@ class SelectSelector(Selector[SelectSelectorConfig]): } ) - def __init__(self, config: SelectSelectorConfig | None = None) -> None: + def __init__(self, config: SelectSelectorConfig) -> None: """Instantiate a selector.""" super().__init__(config) @@ -1434,7 +1441,7 @@ class FileSelector(Selector[FileSelectorConfig]): } ) - def __init__(self, config: FileSelectorConfig | None = None) -> None: + def __init__(self, config: FileSelectorConfig) -> None: """Instantiate a selector.""" super().__init__(config) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index f3be7c5515e..25d10874239 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -58,6 +58,7 @@ SQLAlchemy==2.0.31 typing-extensions>=4.12.2,<5.0 ulid-transform==0.9.0 urllib3>=1.26.5,<2 +voluptuous-openapi==0.0.4 voluptuous-serialize==2.6.0 voluptuous==0.13.1 webrtc-noise-gain==1.2.3 diff --git a/pyproject.toml b/pyproject.toml index d7fbe67edba..6ecbb8b51d1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -69,6 +69,7 @@ dependencies = [ "urllib3>=1.26.5,<2", "voluptuous==0.13.1", "voluptuous-serialize==2.6.0", + "voluptuous-openapi==0.0.4", "yarl==1.9.4", ] diff --git a/requirements.txt b/requirements.txt index cff85c2478f..5b1c57c7e1c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -41,4 +41,5 @@ ulid-transform==0.9.0 urllib3>=1.26.5,<2 voluptuous==0.13.1 voluptuous-serialize==2.6.0 +voluptuous-openapi==0.0.4 yarl==1.9.4 diff --git a/requirements_all.txt b/requirements_all.txt index 48eae313cf4..14c4ed00a0a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2846,10 +2846,6 @@ voip-utils==0.1.0 # homeassistant.components.volkszaehler volkszaehler==0.4.0 -# homeassistant.components.google_generative_ai_conversation -# homeassistant.components.openai_conversation -voluptuous-openapi==0.0.4 - # homeassistant.components.volvooncall volvooncall==0.10.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3b3adcf409a..0e70472b67b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2217,10 +2217,6 @@ vilfo-api-client==0.5.0 # homeassistant.components.voip voip-utils==0.1.0 -# homeassistant.components.google_generative_ai_conversation -# homeassistant.components.openai_conversation -voluptuous-openapi==0.0.4 - # homeassistant.components.volvooncall volvooncall==0.10.3 diff --git a/tests/helpers/test_llm.py b/tests/helpers/test_llm.py index 5389490b401..872297b09ec 100644 --- a/tests/helpers/test_llm.py +++ b/tests/helpers/test_llm.py @@ -8,6 +8,7 @@ import voluptuous as vol from homeassistant.components.homeassistant.exposed_entities import async_expose_entity from homeassistant.components.intent import async_register_timer_handler +from homeassistant.components.script.config import ScriptConfig from homeassistant.core import Context, HomeAssistant, State from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import ( @@ -18,6 +19,7 @@ from homeassistant.helpers import ( floor_registry as fr, intent, llm, + selector, ) from homeassistant.setup import async_setup_component from homeassistant.util import yaml @@ -564,11 +566,6 @@ async def test_assist_api_prompt( "names": "Unnamed Device", "state": "unavailable", }, - "script.test_script": { - "description": "This is a test script", - "names": "test_script", - "state": "off", - }, } exposed_entities_prompt = ( "An overview of the areas and the devices in this smart home:\n" @@ -634,3 +631,323 @@ async def test_assist_api_prompt( {area_prompt} {exposed_entities_prompt}""" ) + + +async def test_script_tool( + hass: HomeAssistant, + area_registry: ar.AreaRegistry, + floor_registry: fr.FloorRegistry, +) -> None: + """Test ScriptTool for the assist API.""" + assert await async_setup_component(hass, "homeassistant", {}) + assert await async_setup_component(hass, "intent", {}) + context = Context() + llm_context = llm.LLMContext( + platform="test_platform", + context=context, + user_prompt="test_text", + language="*", + assistant="conversation", + device_id=None, + ) + + # Create a script with a unique ID + assert await async_setup_component( + hass, + "script", + { + "script": { + "test_script": { + "description": "This is a test script", + "sequence": [], + "fields": { + "beer": {"description": "Number of beers", "required": True}, + "wine": {"selector": {"number": {"min": 0, "max": 3}}}, + "where": {"selector": {"area": {}}}, + "area_list": {"selector": {"area": {"multiple": True}}}, + "floor": {"selector": {"floor": {}}}, + "floor_list": {"selector": {"floor": {"multiple": True}}}, + "extra_field": {"selector": {"area": {}}}, + }, + }, + "unexposed_script": { + "sequence": [], + }, + } + }, + ) + async_expose_entity(hass, "conversation", "script.test_script", True) + + area = area_registry.async_create("Living room") + floor = floor_registry.async_create("2") + + assert llm.SCRIPT_PARAMETERS_CACHE not in hass.data + + api = await llm.async_get_api(hass, "assist", llm_context) + + tools = [tool for tool in api.tools if isinstance(tool, llm.ScriptTool)] + assert len(tools) == 1 + + tool = tools[0] + assert tool.name == "test_script" + assert tool.description == "This is a test script" + schema = { + vol.Required("beer", description="Number of beers"): cv.string, + vol.Optional("wine"): selector.NumberSelector({"min": 0, "max": 3}), + vol.Optional("where"): selector.AreaSelector(), + vol.Optional("area_list"): selector.AreaSelector({"multiple": True}), + vol.Optional("floor"): selector.FloorSelector(), + vol.Optional("floor_list"): selector.FloorSelector({"multiple": True}), + vol.Optional("extra_field"): selector.AreaSelector(), + } + assert tool.parameters.schema == schema + + assert hass.data[llm.SCRIPT_PARAMETERS_CACHE] == { + "test_script": ("This is a test script", vol.Schema(schema)) + } + + tool_input = llm.ToolInput( + tool_name="test_script", + tool_args={ + "beer": "3", + "wine": 0, + "where": "Living room", + "area_list": ["Living room"], + "floor": "2", + "floor_list": ["2"], + }, + ) + + with patch("homeassistant.core.ServiceRegistry.async_call") as mock_service_call: + response = await api.async_call_tool(tool_input) + + mock_service_call.assert_awaited_once_with( + "script", + "turn_on", + { + "entity_id": "script.test_script", + "variables": { + "beer": "3", + "wine": 0, + "where": area.id, + "area_list": [area.id], + "floor": floor.floor_id, + "floor_list": [floor.floor_id], + }, + }, + context=context, + ) + assert response == {"success": True} + + # Test reload script with new parameters + config = { + "script": { + "test_script": ScriptConfig( + { + "description": "This is a new test script", + "sequence": [], + "mode": "single", + "max": 2, + "max_exceeded": "WARNING", + "trace": {}, + "fields": { + "beer": {"description": "Number of beers", "required": True}, + }, + } + ) + } + } + + with patch( + "homeassistant.helpers.entity_component.EntityComponent.async_prepare_reload", + return_value=config, + ): + await hass.services.async_call("script", "reload", blocking=True) + + assert hass.data[llm.SCRIPT_PARAMETERS_CACHE] == {} + + api = await llm.async_get_api(hass, "assist", llm_context) + + tools = [tool for tool in api.tools if isinstance(tool, llm.ScriptTool)] + assert len(tools) == 1 + + tool = tools[0] + assert tool.name == "test_script" + assert tool.description == "This is a new test script" + schema = {vol.Required("beer", description="Number of beers"): cv.string} + assert tool.parameters.schema == schema + + assert hass.data[llm.SCRIPT_PARAMETERS_CACHE] == { + "test_script": ("This is a new test script", vol.Schema(schema)) + } + + +async def test_selector_serializer( + hass: HomeAssistant, llm_context: llm.LLMContext +) -> None: + """Test serialization of Selectors in Open API format.""" + api = await llm.async_get_api(hass, "assist", llm_context) + selector_serializer = api.custom_serializer + + assert selector_serializer(selector.ActionSelector()) == {"type": "string"} + assert selector_serializer(selector.AddonSelector()) == {"type": "string"} + assert selector_serializer(selector.AreaSelector()) == {"type": "string"} + assert selector_serializer(selector.AreaSelector({"multiple": True})) == { + "type": "array", + "items": {"type": "string"}, + } + assert selector_serializer(selector.AssistPipelineSelector()) == {"type": "string"} + assert selector_serializer( + selector.AttributeSelector({"entity_id": "sensor.test"}) + ) == {"type": "string"} + assert selector_serializer(selector.BackupLocationSelector()) == { + "type": "string", + "pattern": "^(?:\\/backup|\\w+)$", + } + assert selector_serializer(selector.BooleanSelector()) == {"type": "boolean"} + assert selector_serializer(selector.ColorRGBSelector()) == { + "type": "array", + "items": {"type": "number"}, + "maxItems": 3, + "minItems": 3, + "format": "RGB", + } + assert selector_serializer(selector.ColorTempSelector()) == {"type": "number"} + assert selector_serializer(selector.ColorTempSelector({"min": 0, "max": 1000})) == { + "type": "number", + "minimum": 0, + "maximum": 1000, + } + assert selector_serializer( + selector.ColorTempSelector({"min_mireds": 100, "max_mireds": 1000}) + ) == {"type": "number", "minimum": 100, "maximum": 1000} + assert selector_serializer(selector.ConfigEntrySelector()) == {"type": "string"} + assert selector_serializer(selector.ConstantSelector({"value": "test"})) == { + "enum": ["test"] + } + assert selector_serializer(selector.ConstantSelector({"value": 1})) == {"enum": [1]} + assert selector_serializer(selector.ConstantSelector({"value": True})) == { + "enum": [True] + } + assert selector_serializer(selector.QrCodeSelector({"data": "test"})) == { + "type": "string" + } + assert selector_serializer(selector.ConversationAgentSelector()) == { + "type": "string" + } + assert selector_serializer(selector.CountrySelector()) == { + "type": "string", + "format": "ISO 3166-1 alpha-2", + } + assert selector_serializer( + selector.CountrySelector({"countries": ["GB", "FR"]}) + ) == {"type": "string", "enum": ["GB", "FR"]} + assert selector_serializer(selector.DateSelector()) == { + "type": "string", + "format": "date", + } + assert selector_serializer(selector.DateTimeSelector()) == { + "type": "string", + "format": "date-time", + } + assert selector_serializer(selector.DeviceSelector()) == {"type": "string"} + assert selector_serializer(selector.DeviceSelector({"multiple": True})) == { + "type": "array", + "items": {"type": "string"}, + } + assert selector_serializer(selector.EntitySelector()) == { + "type": "string", + "format": "entity_id", + } + assert selector_serializer(selector.EntitySelector({"multiple": True})) == { + "type": "array", + "items": {"type": "string", "format": "entity_id"}, + } + assert selector_serializer(selector.FloorSelector()) == {"type": "string"} + assert selector_serializer(selector.FloorSelector({"multiple": True})) == { + "type": "array", + "items": {"type": "string"}, + } + assert selector_serializer(selector.IconSelector()) == {"type": "string"} + assert selector_serializer(selector.LabelSelector()) == {"type": "string"} + assert selector_serializer(selector.LabelSelector({"multiple": True})) == { + "type": "array", + "items": {"type": "string"}, + } + assert selector_serializer(selector.LanguageSelector()) == { + "type": "string", + "format": "RFC 5646", + } + assert selector_serializer( + selector.LanguageSelector({"languages": ["en", "fr"]}) + ) == {"type": "string", "enum": ["en", "fr"]} + assert selector_serializer(selector.LocationSelector()) == { + "type": "object", + "properties": { + "latitude": {"type": "number"}, + "longitude": {"type": "number"}, + "radius": {"type": "number"}, + }, + "required": ["latitude", "longitude"], + } + assert selector_serializer(selector.MediaSelector()) == { + "type": "object", + "properties": { + "entity_id": {"type": "string"}, + "media_content_id": {"type": "string"}, + "media_content_type": {"type": "string"}, + "metadata": {"type": "object", "additionalProperties": True}, + }, + "required": ["entity_id", "media_content_id", "media_content_type"], + } + assert selector_serializer(selector.NumberSelector({"mode": "box"})) == { + "type": "number" + } + assert selector_serializer(selector.NumberSelector({"min": 30, "max": 100})) == { + "type": "number", + "minimum": 30, + "maximum": 100, + } + assert selector_serializer(selector.ObjectSelector()) == {"type": "object"} + assert selector_serializer( + selector.SelectSelector( + { + "options": [ + {"value": "A", "label": "Letter A"}, + {"value": "B", "label": "Letter B"}, + {"value": "C", "label": "Letter C"}, + ] + } + ) + ) == {"type": "string", "enum": ["A", "B", "C"]} + assert selector_serializer( + selector.SelectSelector({"options": ["A", "B", "C"], "multiple": True}) + ) == { + "type": "array", + "items": {"type": "string", "enum": ["A", "B", "C"]}, + "uniqueItems": True, + } + assert selector_serializer( + selector.StateSelector({"entity_id": "sensor.test"}) + ) == {"type": "string"} + assert selector_serializer(selector.TemplateSelector()) == { + "type": "string", + "format": "jinja2", + } + assert selector_serializer(selector.TextSelector()) == {"type": "string"} + assert selector_serializer(selector.TextSelector({"multiple": True})) == { + "type": "array", + "items": {"type": "string"}, + } + assert selector_serializer(selector.ThemeSelector()) == {"type": "string"} + assert selector_serializer(selector.TimeSelector()) == { + "type": "string", + "format": "time", + } + assert selector_serializer(selector.TriggerSelector()) == { + "type": "array", + "items": {"type": "string"}, + } + assert selector_serializer(selector.FileSelector({"accept": ".txt"})) == { + "type": "string" + } diff --git a/tests/helpers/test_selector.py b/tests/helpers/test_selector.py index 6db313baa24..e93ec3b8c22 100644 --- a/tests/helpers/test_selector.py +++ b/tests/helpers/test_selector.py @@ -55,6 +55,8 @@ def _test_selector( config = {selector_type: schema} selector.validate_selector(config) selector_instance = selector.selector(config) + assert selector_instance == selector.selector(config) + assert selector_instance != 5 # We do not allow enums in the config, as they cannot serialize assert not any(isinstance(val, Enum) for val in selector_instance.config.values()) From 09e8f7e9bba68614a4bc74974867da7fdd63bb6d Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 25 Jun 2024 17:44:03 +0200 Subject: [PATCH 1177/1445] Improve type hints in deconz tests (#120388) --- tests/components/deconz/test_device_trigger.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/components/deconz/test_device_trigger.py b/tests/components/deconz/test_device_trigger.py index 329cf0405db..54b735ba021 100644 --- a/tests/components/deconz/test_device_trigger.py +++ b/tests/components/deconz/test_device_trigger.py @@ -27,7 +27,7 @@ from homeassistant.const import ( CONF_TYPE, STATE_UNAVAILABLE, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.trigger import async_initialize_triggers from homeassistant.setup import async_setup_component @@ -44,7 +44,7 @@ def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None: @pytest.fixture -def automation_calls(hass): +def automation_calls(hass: HomeAssistant) -> list[ServiceCall]: """Track automation calls to a mock service.""" return async_mock_service(hass, "test", "automation") @@ -300,7 +300,7 @@ async def test_functional_device_trigger( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, mock_deconz_websocket, - automation_calls, + automation_calls: list[ServiceCall], device_registry: dr.DeviceRegistry, ) -> None: """Test proper matching and attachment of device trigger automation.""" From e0b98551605ae3e1073068cd5cdebe4f3943b18d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 25 Jun 2024 18:22:56 +0200 Subject: [PATCH 1178/1445] Bump uiprotect to 3.4.0 (#120433) --- homeassistant/components/unifiprotect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index ba8e6f89dd5..7a1556387a8 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -40,7 +40,7 @@ "integration_type": "hub", "iot_class": "local_push", "loggers": ["uiprotect", "unifi_discovery"], - "requirements": ["uiprotect==3.3.1", "unifi-discovery==1.1.8"], + "requirements": ["uiprotect==3.4.0", "unifi-discovery==1.1.8"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/requirements_all.txt b/requirements_all.txt index 14c4ed00a0a..7c6f39ed88b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2791,7 +2791,7 @@ twitchAPI==4.0.0 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==3.3.1 +uiprotect==3.4.0 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0e70472b67b..9bc9a740278 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2171,7 +2171,7 @@ twitchAPI==4.0.0 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==3.3.1 +uiprotect==3.4.0 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 From f017134199fbfa13ceb380cacfb67bbf37ed2952 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 25 Jun 2024 18:53:06 +0200 Subject: [PATCH 1179/1445] Fix missing vol.Optional keyword (#120444) --- homeassistant/components/proxy/camera.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/proxy/camera.py b/homeassistant/components/proxy/camera.py index 3a93c7a2d36..5cd72b05871 100644 --- a/homeassistant/components/proxy/camera.py +++ b/homeassistant/components/proxy/camera.py @@ -49,8 +49,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_ENTITY_ID): cv.entity_id, vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_CACHE_IMAGES, False): cv.boolean, - vol.Optional(CONF_FORCE_RESIZE, False): cv.boolean, + vol.Optional(CONF_CACHE_IMAGES, default=False): cv.boolean, + vol.Optional(CONF_FORCE_RESIZE, default=False): cv.boolean, vol.Optional(CONF_MODE, default=MODE_RESIZE): vol.In([MODE_RESIZE, MODE_CROP]), vol.Optional(CONF_IMAGE_QUALITY): int, vol.Optional(CONF_IMAGE_REFRESH_RATE): float, From 197062139e867030a6a251116f96e99014f5fc63 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 25 Jun 2024 18:54:06 +0200 Subject: [PATCH 1180/1445] Fix schema typing (1) (#120443) --- homeassistant/components/forked_daapd/config_flow.py | 2 +- homeassistant/components/linear_garage_door/config_flow.py | 4 +--- homeassistant/components/motion_blinds/config_flow.py | 2 +- homeassistant/components/synology_dsm/config_flow.py | 2 +- homeassistant/components/vizio/config_flow.py | 2 +- homeassistant/components/zha/config_flow.py | 4 ++-- 6 files changed, 7 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/forked_daapd/config_flow.py b/homeassistant/components/forked_daapd/config_flow.py index 2440fc82943..7edf25a2595 100644 --- a/homeassistant/components/forked_daapd/config_flow.py +++ b/homeassistant/components/forked_daapd/config_flow.py @@ -111,7 +111,7 @@ class ForkedDaapdFlowHandler(ConfigFlow, domain=DOMAIN): def __init__(self) -> None: """Initialize.""" - self.discovery_schema = None + self.discovery_schema: vol.Schema | None = None @staticmethod @callback diff --git a/homeassistant/components/linear_garage_door/config_flow.py b/homeassistant/components/linear_garage_door/config_flow.py index dca2780cfea..d1dda97c513 100644 --- a/homeassistant/components/linear_garage_door/config_flow.py +++ b/homeassistant/components/linear_garage_door/config_flow.py @@ -75,9 +75,7 @@ class LinearGarageDoorConfigFlow(ConfigFlow, domain=DOMAIN): self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: """Handle the initial step.""" - data_schema = STEP_USER_DATA_SCHEMA - - data_schema = vol.Schema(data_schema) + data_schema = vol.Schema(STEP_USER_DATA_SCHEMA) if user_input is None: return self.async_show_form(step_id="user", data_schema=data_schema) diff --git a/homeassistant/components/motion_blinds/config_flow.py b/homeassistant/components/motion_blinds/config_flow.py index c838825a4bd..131299314a2 100644 --- a/homeassistant/components/motion_blinds/config_flow.py +++ b/homeassistant/components/motion_blinds/config_flow.py @@ -75,7 +75,7 @@ class MotionBlindsFlowHandler(ConfigFlow, domain=DOMAIN): """Initialize the Motionblinds flow.""" self._host: str | None = None self._ips: list[str] = [] - self._config_settings = None + self._config_settings: vol.Schema | None = None @staticmethod @callback diff --git a/homeassistant/components/synology_dsm/config_flow.py b/homeassistant/components/synology_dsm/config_flow.py index 6e2b090fc98..d019361edad 100644 --- a/homeassistant/components/synology_dsm/config_flow.py +++ b/homeassistant/components/synology_dsm/config_flow.py @@ -138,7 +138,7 @@ class SynologyDSMFlowHandler(ConfigFlow, domain=DOMAIN): user_input = {} description_placeholders = {} - data_schema = {} + data_schema = None if step_id == "link": user_input.update(self.discovered_conf) diff --git a/homeassistant/components/vizio/config_flow.py b/homeassistant/components/vizio/config_flow.py index fb5f74f4e09..d8b99595f54 100644 --- a/homeassistant/components/vizio/config_flow.py +++ b/homeassistant/components/vizio/config_flow.py @@ -188,7 +188,7 @@ class VizioConfigFlow(ConfigFlow, domain=DOMAIN): def __init__(self) -> None: """Initialize config flow.""" - self._user_schema = None + self._user_schema: vol.Schema | None = None self._must_show_form: bool | None = None self._ch_type: str | None = None self._pairing_token: str | None = None diff --git a/homeassistant/components/zha/config_flow.py b/homeassistant/components/zha/config_flow.py index 037ad4192bd..9be27f7b37c 100644 --- a/homeassistant/components/zha/config_flow.py +++ b/homeassistant/components/zha/config_flow.py @@ -221,7 +221,7 @@ class BaseZhaFlow(ConfigEntryBaseFlow): return await self.async_step_verify_radio() # Pre-select the currently configured port - default_port = vol.UNDEFINED + default_port: vol.Undefined | str = vol.UNDEFINED if self._radio_mgr.device_path is not None: for description, port in zip(list_of_ports, ports, strict=False): @@ -251,7 +251,7 @@ class BaseZhaFlow(ConfigEntryBaseFlow): return await self.async_step_manual_port_config() # Pre-select the current radio type - default = vol.UNDEFINED + default: vol.Undefined | str = vol.UNDEFINED if self._radio_mgr.radio_type is not None: default = self._radio_mgr.radio_type.description From b393024acd4d046b0acf5111433115cf15398890 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 25 Jun 2024 18:57:15 +0200 Subject: [PATCH 1181/1445] Improve collection schema typing (#120441) --- .../components/application_credentials/__init__.py | 6 +++--- homeassistant/components/assist_pipeline/pipeline.py | 4 ++-- homeassistant/components/counter/__init__.py | 4 ++-- homeassistant/components/image_upload/__init__.py | 6 +++--- homeassistant/components/input_boolean/__init__.py | 4 ++-- homeassistant/components/input_button/__init__.py | 4 ++-- homeassistant/components/input_datetime/__init__.py | 4 ++-- homeassistant/components/input_number/__init__.py | 4 ++-- homeassistant/components/input_select/__init__.py | 4 ++-- homeassistant/components/input_text/__init__.py | 4 ++-- homeassistant/components/lovelace/const.py | 11 ++++++----- homeassistant/components/person/__init__.py | 6 +++--- homeassistant/components/schedule/__init__.py | 10 +++++----- homeassistant/components/tag/__init__.py | 10 +++++----- homeassistant/components/timer/__init__.py | 4 ++-- homeassistant/components/zone/__init__.py | 6 +++--- homeassistant/helpers/collection.py | 6 +++--- 17 files changed, 49 insertions(+), 48 deletions(-) diff --git a/homeassistant/components/application_credentials/__init__.py b/homeassistant/components/application_credentials/__init__.py index aacd18fc795..22deb124859 100644 --- a/homeassistant/components/application_credentials/__init__.py +++ b/homeassistant/components/application_credentials/__init__.py @@ -29,7 +29,7 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import collection, config_entry_oauth2_flow import homeassistant.helpers.config_validation as cv from homeassistant.helpers.storage import Store -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.typing import ConfigType, VolDictType from homeassistant.loader import ( IntegrationNotFound, async_get_application_credentials, @@ -49,14 +49,14 @@ DATA_STORAGE = "storage" CONF_AUTH_DOMAIN = "auth_domain" DEFAULT_IMPORT_NAME = "Import from configuration.yaml" -CREATE_FIELDS = { +CREATE_FIELDS: VolDictType = { vol.Required(CONF_DOMAIN): cv.string, vol.Required(CONF_CLIENT_ID): vol.All(cv.string, vol.Strip), vol.Required(CONF_CLIENT_SECRET): vol.All(cv.string, vol.Strip), vol.Optional(CONF_AUTH_DOMAIN): cv.string, vol.Optional(CONF_NAME): cv.string, } -UPDATE_FIELDS: dict = {} # Not supported +UPDATE_FIELDS: VolDictType = {} # Not supported CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN) diff --git a/homeassistant/components/assist_pipeline/pipeline.py b/homeassistant/components/assist_pipeline/pipeline.py index 6c1b3ced470..56f88f60104 100644 --- a/homeassistant/components/assist_pipeline/pipeline.py +++ b/homeassistant/components/assist_pipeline/pipeline.py @@ -45,7 +45,7 @@ from homeassistant.helpers.collection import ( ) from homeassistant.helpers.singleton import singleton from homeassistant.helpers.storage import Store -from homeassistant.helpers.typing import UNDEFINED, UndefinedType +from homeassistant.helpers.typing import UNDEFINED, UndefinedType, VolDictType from homeassistant.util import ( dt as dt_util, language as language_util, @@ -94,7 +94,7 @@ def validate_language(data: dict[str, Any]) -> Any: return data -PIPELINE_FIELDS = { +PIPELINE_FIELDS: VolDictType = { vol.Required("conversation_engine"): str, vol.Required("conversation_language"): str, vol.Required("language"): str, diff --git a/homeassistant/components/counter/__init__.py b/homeassistant/components/counter/__init__.py index 3d68d70e575..324668a63e2 100644 --- a/homeassistant/components/counter/__init__.py +++ b/homeassistant/components/counter/__init__.py @@ -21,7 +21,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.storage import Store -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.typing import ConfigType, VolDictType _LOGGER = logging.getLogger(__name__) @@ -49,7 +49,7 @@ SERVICE_SET_VALUE = "set_value" STORAGE_KEY = DOMAIN STORAGE_VERSION = 1 -STORAGE_FIELDS = { +STORAGE_FIELDS: VolDictType = { vol.Optional(CONF_ICON): cv.icon, vol.Optional(CONF_INITIAL, default=DEFAULT_INITIAL): cv.positive_int, vol.Required(CONF_NAME): vol.All(cv.string, vol.Length(min=1)), diff --git a/homeassistant/components/image_upload/__init__.py b/homeassistant/components/image_upload/__init__.py index 59b594561f0..8bb3aca3708 100644 --- a/homeassistant/components/image_upload/__init__.py +++ b/homeassistant/components/image_upload/__init__.py @@ -21,7 +21,7 @@ from homeassistant.const import CONF_ID from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import collection, config_validation as cv from homeassistant.helpers.storage import Store -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.typing import ConfigType, VolDictType import homeassistant.util.dt as dt_util from .const import DOMAIN @@ -32,11 +32,11 @@ STORAGE_VERSION = 1 VALID_SIZES = {256, 512} MAX_SIZE = 1024 * 1024 * 10 -CREATE_FIELDS = { +CREATE_FIELDS: VolDictType = { vol.Required("file"): FileField, } -UPDATE_FIELDS = { +UPDATE_FIELDS: VolDictType = { vol.Optional("name"): vol.All(str, vol.Length(min=1)), } diff --git a/homeassistant/components/input_boolean/__init__.py b/homeassistant/components/input_boolean/__init__.py index 91c7de96fe0..57165c5508a 100644 --- a/homeassistant/components/input_boolean/__init__.py +++ b/homeassistant/components/input_boolean/__init__.py @@ -26,7 +26,7 @@ from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.restore_state import RestoreEntity import homeassistant.helpers.service from homeassistant.helpers.storage import Store -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.typing import ConfigType, VolDictType from homeassistant.loader import bind_hass DOMAIN = "input_boolean" @@ -35,7 +35,7 @@ _LOGGER = logging.getLogger(__name__) CONF_INITIAL = "initial" -STORAGE_FIELDS = { +STORAGE_FIELDS: VolDictType = { vol.Required(CONF_NAME): vol.All(str, vol.Length(min=1)), vol.Optional(CONF_INITIAL): cv.boolean, vol.Optional(CONF_ICON): cv.icon, diff --git a/homeassistant/components/input_button/__init__.py b/homeassistant/components/input_button/__init__.py index d6c3644487b..e70bbacd933 100644 --- a/homeassistant/components/input_button/__init__.py +++ b/homeassistant/components/input_button/__init__.py @@ -22,13 +22,13 @@ from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.restore_state import RestoreEntity import homeassistant.helpers.service from homeassistant.helpers.storage import Store -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.typing import ConfigType, VolDictType DOMAIN = "input_button" _LOGGER = logging.getLogger(__name__) -STORAGE_FIELDS = { +STORAGE_FIELDS: VolDictType = { vol.Required(CONF_NAME): vol.All(str, vol.Length(min=1)), vol.Optional(CONF_ICON): cv.icon, } diff --git a/homeassistant/components/input_datetime/__init__.py b/homeassistant/components/input_datetime/__init__.py index 11aab52e6a4..5d2c1e7ff8d 100644 --- a/homeassistant/components/input_datetime/__init__.py +++ b/homeassistant/components/input_datetime/__init__.py @@ -24,7 +24,7 @@ from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.restore_state import RestoreEntity import homeassistant.helpers.service from homeassistant.helpers.storage import Store -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.typing import ConfigType, VolDictType from homeassistant.util import dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -59,7 +59,7 @@ def validate_set_datetime_attrs(config): STORAGE_KEY = DOMAIN STORAGE_VERSION = 1 -STORAGE_FIELDS = { +STORAGE_FIELDS: VolDictType = { vol.Required(CONF_NAME): vol.All(str, vol.Length(min=1)), vol.Optional(CONF_HAS_DATE, default=False): cv.boolean, vol.Optional(CONF_HAS_TIME, default=False): cv.boolean, diff --git a/homeassistant/components/input_number/__init__.py b/homeassistant/components/input_number/__init__.py index e37f530b8af..f55ceabc6f0 100644 --- a/homeassistant/components/input_number/__init__.py +++ b/homeassistant/components/input_number/__init__.py @@ -25,7 +25,7 @@ from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.restore_state import RestoreEntity import homeassistant.helpers.service from homeassistant.helpers.storage import Store -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.typing import ConfigType, VolDictType _LOGGER = logging.getLogger(__name__) @@ -64,7 +64,7 @@ def _cv_input_number(cfg): return cfg -STORAGE_FIELDS = { +STORAGE_FIELDS: VolDictType = { vol.Required(CONF_NAME): vol.All(str, vol.Length(min=1)), vol.Required(CONF_MIN): vol.Coerce(float), vol.Required(CONF_MAX): vol.Coerce(float), diff --git a/homeassistant/components/input_select/__init__.py b/homeassistant/components/input_select/__init__.py index 2741c9e21bc..44d2df02a92 100644 --- a/homeassistant/components/input_select/__init__.py +++ b/homeassistant/components/input_select/__init__.py @@ -33,7 +33,7 @@ from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.restore_state import RestoreEntity import homeassistant.helpers.service from homeassistant.helpers.storage import Store -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.typing import ConfigType, VolDictType _LOGGER = logging.getLogger(__name__) @@ -55,7 +55,7 @@ def _unique(options: Any) -> Any: raise HomeAssistantError("Duplicate options are not allowed") from exc -STORAGE_FIELDS = { +STORAGE_FIELDS: VolDictType = { vol.Required(CONF_NAME): vol.All(str, vol.Length(min=1)), vol.Required(CONF_OPTIONS): vol.All( cv.ensure_list, vol.Length(min=1), _unique, [cv.string] diff --git a/homeassistant/components/input_text/__init__.py b/homeassistant/components/input_text/__init__.py index 55b43ee8a1e..3d75ff9f5c2 100644 --- a/homeassistant/components/input_text/__init__.py +++ b/homeassistant/components/input_text/__init__.py @@ -24,7 +24,7 @@ from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.restore_state import RestoreEntity import homeassistant.helpers.service from homeassistant.helpers.storage import Store -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.typing import ConfigType, VolDictType _LOGGER = logging.getLogger(__name__) @@ -50,7 +50,7 @@ SERVICE_SET_VALUE = "set_value" STORAGE_KEY = DOMAIN STORAGE_VERSION = 1 -STORAGE_FIELDS = { +STORAGE_FIELDS: VolDictType = { vol.Required(CONF_NAME): vol.All(str, vol.Length(min=1)), vol.Optional(CONF_MIN, default=CONF_MIN_VALUE): vol.Coerce(int), vol.Optional(CONF_MAX, default=CONF_MAX_VALUE): vol.Coerce(int), diff --git a/homeassistant/components/lovelace/const.py b/homeassistant/components/lovelace/const.py index 538bd49d72c..86f47fe2b5c 100644 --- a/homeassistant/components/lovelace/const.py +++ b/homeassistant/components/lovelace/const.py @@ -13,6 +13,7 @@ from homeassistant.const import ( ) from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.typing import VolDictType from homeassistant.util import slugify DOMAIN = "lovelace" @@ -37,12 +38,12 @@ RESOURCE_FIELDS = { RESOURCE_SCHEMA = vol.Schema(RESOURCE_FIELDS) -RESOURCE_CREATE_FIELDS = { +RESOURCE_CREATE_FIELDS: VolDictType = { vol.Required(CONF_RESOURCE_TYPE_WS): vol.In(RESOURCE_TYPES), vol.Required(CONF_URL): cv.string, } -RESOURCE_UPDATE_FIELDS = { +RESOURCE_UPDATE_FIELDS: VolDictType = { vol.Optional(CONF_RESOURCE_TYPE_WS): vol.In(RESOURCE_TYPES), vol.Optional(CONF_URL): cv.string, } @@ -54,7 +55,7 @@ CONF_TITLE = "title" CONF_REQUIRE_ADMIN = "require_admin" CONF_SHOW_IN_SIDEBAR = "show_in_sidebar" -DASHBOARD_BASE_CREATE_FIELDS = { +DASHBOARD_BASE_CREATE_FIELDS: VolDictType = { vol.Optional(CONF_REQUIRE_ADMIN, default=False): cv.boolean, vol.Optional(CONF_ICON): cv.icon, vol.Required(CONF_TITLE): cv.string, @@ -62,7 +63,7 @@ DASHBOARD_BASE_CREATE_FIELDS = { } -DASHBOARD_BASE_UPDATE_FIELDS = { +DASHBOARD_BASE_UPDATE_FIELDS: VolDictType = { vol.Optional(CONF_REQUIRE_ADMIN): cv.boolean, vol.Optional(CONF_ICON): vol.Any(cv.icon, None), vol.Optional(CONF_TITLE): cv.string, @@ -70,7 +71,7 @@ DASHBOARD_BASE_UPDATE_FIELDS = { } -STORAGE_DASHBOARD_CREATE_FIELDS = { +STORAGE_DASHBOARD_CREATE_FIELDS: VolDictType = { **DASHBOARD_BASE_CREATE_FIELDS, vol.Required(CONF_URL_PATH): cv.string, # For now we write "storage" as all modes. diff --git a/homeassistant/components/person/__init__.py b/homeassistant/components/person/__init__.py index 0779140a091..b793f4b33ae 100644 --- a/homeassistant/components/person/__init__.py +++ b/homeassistant/components/person/__init__.py @@ -50,7 +50,7 @@ from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.event import async_track_state_change_event from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.storage import Store -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.typing import ConfigType, VolDictType from homeassistant.loader import bind_hass from .const import DOMAIN @@ -165,7 +165,7 @@ def entities_in_person(hass: HomeAssistant, entity_id: str) -> list[str]: return person_entity.device_trackers -CREATE_FIELDS = { +CREATE_FIELDS: VolDictType = { vol.Required(CONF_NAME): vol.All(str, vol.Length(min=1)), vol.Optional(CONF_USER_ID): vol.Any(str, None), vol.Optional(CONF_DEVICE_TRACKERS, default=list): vol.All( @@ -175,7 +175,7 @@ CREATE_FIELDS = { } -UPDATE_FIELDS = { +UPDATE_FIELDS: VolDictType = { vol.Optional(CONF_NAME): vol.All(str, vol.Length(min=1)), vol.Optional(CONF_USER_ID): vol.Any(str, None), vol.Optional(CONF_DEVICE_TRACKERS, default=list): vol.All( diff --git a/homeassistant/components/schedule/__init__.py b/homeassistant/components/schedule/__init__.py index e69a6761bc7..08d0b083f7c 100644 --- a/homeassistant/components/schedule/__init__.py +++ b/homeassistant/components/schedule/__init__.py @@ -33,7 +33,7 @@ from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.helpers.service import async_register_admin_service from homeassistant.helpers.storage import Store -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.typing import ConfigType, VolDictType from homeassistant.util import dt as dt_util from .const import ( @@ -104,12 +104,12 @@ def serialize_to_time(value: Any) -> Any: return vol.Coerce(str)(value) -BASE_SCHEMA = { +BASE_SCHEMA: VolDictType = { vol.Required(CONF_NAME): vol.All(str, vol.Length(min=1)), vol.Optional(CONF_ICON): cv.icon, } -TIME_RANGE_SCHEMA = { +TIME_RANGE_SCHEMA: VolDictType = { vol.Required(CONF_FROM): cv.time, vol.Required(CONF_TO): deserialize_to_time, } @@ -122,13 +122,13 @@ STORAGE_TIME_RANGE_SCHEMA = vol.Schema( } ) -SCHEDULE_SCHEMA = { +SCHEDULE_SCHEMA: VolDictType = { vol.Optional(day, default=[]): vol.All( cv.ensure_list, [TIME_RANGE_SCHEMA], valid_schedule ) for day in CONF_ALL_DAYS } -STORAGE_SCHEDULE_SCHEMA = { +STORAGE_SCHEDULE_SCHEMA: VolDictType = { vol.Optional(day, default=[]): vol.All( cv.ensure_list, [TIME_RANGE_SCHEMA], valid_schedule, [STORAGE_TIME_RANGE_SCHEMA] ) diff --git a/homeassistant/components/tag/__init__.py b/homeassistant/components/tag/__init__.py index af3d06cf2d4..97307112f22 100644 --- a/homeassistant/components/tag/__init__.py +++ b/homeassistant/components/tag/__init__.py @@ -18,7 +18,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.storage import Store -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.typing import ConfigType, VolDictType from homeassistant.util import slugify import homeassistant.util.dt as dt_util from homeassistant.util.hass_dict import HassKey @@ -35,7 +35,7 @@ STORAGE_VERSION_MINOR = 3 TAG_DATA: HassKey[TagStorageCollection] = HassKey(DOMAIN) -CREATE_FIELDS = { +CREATE_FIELDS: VolDictType = { vol.Optional(TAG_ID): cv.string, vol.Optional(CONF_NAME): vol.All(str, vol.Length(min=1)), vol.Optional("description"): cv.string, @@ -43,7 +43,7 @@ CREATE_FIELDS = { vol.Optional(DEVICE_ID): cv.string, } -UPDATE_FIELDS = { +UPDATE_FIELDS: VolDictType = { vol.Optional(CONF_NAME): vol.All(str, vol.Length(min=1)), vol.Optional("description"): cv.string, vol.Optional(LAST_SCANNED): cv.datetime, @@ -192,8 +192,8 @@ class TagDictStorageCollectionWebsocket( storage_collection: TagStorageCollection, api_prefix: str, model_name: str, - create_schema: ConfigType, - update_schema: ConfigType, + create_schema: VolDictType, + update_schema: VolDictType, ) -> None: """Initialize a websocket for tag.""" super().__init__( diff --git a/homeassistant/components/timer/__init__.py b/homeassistant/components/timer/__init__.py index 8927439a6cc..3f2b4bd7f43 100644 --- a/homeassistant/components/timer/__init__.py +++ b/homeassistant/components/timer/__init__.py @@ -26,7 +26,7 @@ from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.helpers.restore_state import RestoreEntity import homeassistant.helpers.service from homeassistant.helpers.storage import Store -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.typing import ConfigType, VolDictType import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -66,7 +66,7 @@ SERVICE_FINISH = "finish" STORAGE_KEY = DOMAIN STORAGE_VERSION = 1 -STORAGE_FIELDS = { +STORAGE_FIELDS: VolDictType = { vol.Required(CONF_NAME): cv.string, vol.Optional(CONF_ICON): cv.icon, vol.Optional(CONF_DURATION, default=DEFAULT_DURATION): cv.time_period, diff --git a/homeassistant/components/zone/__init__.py b/homeassistant/components/zone/__init__.py index 0fef9961679..1c43a79e10e 100644 --- a/homeassistant/components/zone/__init__.py +++ b/homeassistant/components/zone/__init__.py @@ -45,7 +45,7 @@ from homeassistant.helpers import ( service, storage, ) -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.typing import ConfigType, VolDictType from homeassistant.loader import bind_hass from homeassistant.util.location import distance @@ -62,7 +62,7 @@ ENTITY_ID_HOME = ENTITY_ID_FORMAT.format(HOME_ZONE) ICON_HOME = "mdi:home" ICON_IMPORT = "mdi:import" -CREATE_FIELDS = { +CREATE_FIELDS: VolDictType = { vol.Required(CONF_NAME): cv.string, vol.Required(CONF_LATITUDE): cv.latitude, vol.Required(CONF_LONGITUDE): cv.longitude, @@ -72,7 +72,7 @@ CREATE_FIELDS = { } -UPDATE_FIELDS = { +UPDATE_FIELDS: VolDictType = { vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_LATITUDE): cv.latitude, vol.Optional(CONF_LONGITUDE): cv.longitude, diff --git a/homeassistant/helpers/collection.py b/homeassistant/helpers/collection.py index b9993098003..036aaacf0e9 100644 --- a/homeassistant/helpers/collection.py +++ b/homeassistant/helpers/collection.py @@ -26,7 +26,7 @@ from . import entity_registry from .entity import Entity from .entity_component import EntityComponent from .storage import Store -from .typing import ConfigType +from .typing import ConfigType, VolDictType STORAGE_VERSION = 1 SAVE_DELAY = 10 @@ -515,8 +515,8 @@ class StorageCollectionWebsocket[_StorageCollectionT: StorageCollection]: storage_collection: _StorageCollectionT, api_prefix: str, model_name: str, - create_schema: dict, - update_schema: dict, + create_schema: VolDictType, + update_schema: VolDictType, ) -> None: """Initialize a websocket CRUD.""" self.storage_collection = storage_collection From 185e79fa1ba9b2023c2dd584820f51731d34c5bc Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 25 Jun 2024 19:02:04 +0200 Subject: [PATCH 1182/1445] Improve intent schema typing (#120442) --- homeassistant/helpers/intent.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/homeassistant/helpers/intent.py b/homeassistant/helpers/intent.py index 502b20eaf8f..e191bddf102 100644 --- a/homeassistant/helpers/intent.py +++ b/homeassistant/helpers/intent.py @@ -33,6 +33,7 @@ from . import ( entity_registry, floor_registry, ) +from .typing import VolSchemaType _LOGGER = logging.getLogger(__name__) type _SlotsType = dict[str, Any] @@ -807,8 +808,8 @@ class DynamicServiceIntentHandler(IntentHandler): self, intent_type: str, speech: str | None = None, - required_slots: dict[str | tuple[str, str], vol.Schema] | None = None, - optional_slots: dict[str | tuple[str, str], vol.Schema] | None = None, + required_slots: dict[str | tuple[str, str], VolSchemaType] | None = None, + optional_slots: dict[str | tuple[str, str], VolSchemaType] | None = None, required_domains: set[str] | None = None, required_features: int | None = None, required_states: set[str] | None = None, @@ -824,7 +825,7 @@ class DynamicServiceIntentHandler(IntentHandler): self.description = description self.platforms = platforms - self.required_slots: dict[tuple[str, str], vol.Schema] = {} + self.required_slots: dict[tuple[str, str], VolSchemaType] = {} if required_slots: for key, value_schema in required_slots.items(): if isinstance(key, str): @@ -833,7 +834,7 @@ class DynamicServiceIntentHandler(IntentHandler): self.required_slots[key] = value_schema - self.optional_slots: dict[tuple[str, str], vol.Schema] = {} + self.optional_slots: dict[tuple[str, str], VolSchemaType] = {} if optional_slots: for key, value_schema in optional_slots.items(): if isinstance(key, str): @@ -1107,8 +1108,8 @@ class ServiceIntentHandler(DynamicServiceIntentHandler): domain: str, service: str, speech: str | None = None, - required_slots: dict[str | tuple[str, str], vol.Schema] | None = None, - optional_slots: dict[str | tuple[str, str], vol.Schema] | None = None, + required_slots: dict[str | tuple[str, str], VolSchemaType] | None = None, + optional_slots: dict[str | tuple[str, str], VolSchemaType] | None = None, required_domains: set[str] | None = None, required_features: int | None = None, required_states: set[str] | None = None, From cbcf29720dd42f49c0b9e8975e44380b89c3ccb3 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Tue, 25 Jun 2024 19:15:11 +0200 Subject: [PATCH 1183/1445] Cleanup common mqtt tests (#120446) --- .../mqtt/test_alarm_control_panel.py | 48 ++++----------- tests/components/mqtt/test_binary_sensor.py | 51 ++++------------ tests/components/mqtt/test_button.py | 48 ++++----------- tests/components/mqtt/test_camera.py | 40 +++---------- tests/components/mqtt/test_climate.py | 41 ++++--------- tests/components/mqtt/test_common.py | 19 +----- tests/components/mqtt/test_cover.py | 41 ++++--------- tests/components/mqtt/test_event.py | 24 ++------ tests/components/mqtt/test_fan.py | 37 ++++-------- tests/components/mqtt/test_humidifier.py | 46 ++++----------- tests/components/mqtt/test_image.py | 41 ++++--------- tests/components/mqtt/test_lawn_mower.py | 38 +++--------- tests/components/mqtt/test_light.py | 45 ++++---------- tests/components/mqtt/test_light_json.py | 57 ++++-------------- tests/components/mqtt/test_light_template.py | 41 ++++--------- tests/components/mqtt/test_lock.py | 37 ++++-------- tests/components/mqtt/test_notify.py | 48 ++++----------- tests/components/mqtt/test_number.py | 43 ++++---------- tests/components/mqtt/test_scene.py | 46 ++++----------- tests/components/mqtt/test_select.py | 43 ++++---------- tests/components/mqtt/test_sensor.py | 42 +++----------- tests/components/mqtt/test_siren.py | 39 +++---------- tests/components/mqtt/test_switch.py | 38 +++--------- tests/components/mqtt/test_text.py | 58 +++++-------------- tests/components/mqtt/test_update.py | 40 +++---------- tests/components/mqtt/test_vacuum.py | 40 +++---------- tests/components/mqtt/test_valve.py | 38 +++--------- tests/components/mqtt/test_water_heater.py | 41 ++++--------- 28 files changed, 266 insertions(+), 904 deletions(-) diff --git a/tests/components/mqtt/test_alarm_control_panel.py b/tests/components/mqtt/test_alarm_control_panel.py index a90e71cebe5..cd7e8ab7339 100644 --- a/tests/components/mqtt/test_alarm_control_panel.py +++ b/tests/components/mqtt/test_alarm_control_panel.py @@ -192,7 +192,7 @@ def does_not_raise(): ], ) async def test_fail_setup_without_state_or_command_topic( - hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, valid + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, valid: bool ) -> None: """Test for failing setup with no state or command topic.""" assert await mqtt_mock_entry() @@ -351,8 +351,8 @@ async def test_supported_features( async def test_publish_mqtt_no_code( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, - service, - payload, + service: str, + payload: str, ) -> None: """Test publishing of MQTT messages when no code is configured.""" mqtt_mock = await mqtt_mock_entry() @@ -952,17 +952,11 @@ async def test_update_with_json_attrs_bad_json( async def test_discovery_update_attr( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, - mqtt_mock_entry, - caplog, - alarm_control_panel.DOMAIN, - DEFAULT_CONFIG, + hass, mqtt_mock_entry, alarm_control_panel.DOMAIN, DEFAULT_CONFIG ) @@ -997,21 +991,17 @@ async def test_unique_id( async def test_discovery_removal_alarm( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test removal of discovered alarm_control_panel.""" data = json.dumps(DEFAULT_CONFIG[mqtt.DOMAIN][alarm_control_panel.DOMAIN]) await help_test_discovery_removal( - hass, mqtt_mock_entry, caplog, alarm_control_panel.DOMAIN, data + hass, mqtt_mock_entry, alarm_control_panel.DOMAIN, data ) async def test_discovery_update_alarm_topic_and_template( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered alarm_control_panel.""" config1 = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][alarm_control_panel.DOMAIN]) @@ -1036,7 +1026,6 @@ async def test_discovery_update_alarm_topic_and_template( await help_test_discovery_update( hass, mqtt_mock_entry, - caplog, alarm_control_panel.DOMAIN, config1, config2, @@ -1046,9 +1035,7 @@ async def test_discovery_update_alarm_topic_and_template( async def test_discovery_update_alarm_template( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered alarm_control_panel.""" config1 = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][alarm_control_panel.DOMAIN]) @@ -1071,7 +1058,6 @@ async def test_discovery_update_alarm_template( await help_test_discovery_update( hass, mqtt_mock_entry, - caplog, alarm_control_panel.DOMAIN, config1, config2, @@ -1081,9 +1067,7 @@ async def test_discovery_update_alarm_template( async def test_discovery_update_unchanged_alarm( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered alarm_control_panel.""" config1 = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][alarm_control_panel.DOMAIN]) @@ -1096,7 +1080,6 @@ async def test_discovery_update_unchanged_alarm( await help_test_discovery_update_unchanged( hass, mqtt_mock_entry, - caplog, alarm_control_panel.DOMAIN, data1, discovery_update, @@ -1105,9 +1088,7 @@ async def test_discovery_update_unchanged_alarm( @pytest.mark.no_fail_on_log_exception async def test_discovery_broken( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test handling of bad discovery message.""" data1 = '{ "name": "Beer" }' @@ -1117,12 +1098,7 @@ async def test_discovery_broken( ' "command_topic": "test_topic" }' ) await help_test_discovery_broken( - hass, - mqtt_mock_entry, - caplog, - alarm_control_panel.DOMAIN, - data1, - data2, + hass, mqtt_mock_entry, alarm_control_panel.DOMAIN, data1, data2 ) diff --git a/tests/components/mqtt/test_binary_sensor.py b/tests/components/mqtt/test_binary_sensor.py index afa9ca9970e..7ad394243df 100644 --- a/tests/components/mqtt/test_binary_sensor.py +++ b/tests/components/mqtt/test_binary_sensor.py @@ -90,7 +90,6 @@ DEFAULT_CONFIG = { async def test_setting_sensor_value_expires_availability_topic( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, ) -> None: """Test the expiration of the value.""" await mqtt_mock_entry() @@ -797,17 +796,11 @@ async def test_update_with_json_attrs_bad_json( async def test_discovery_update_attr( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, - mqtt_mock_entry, - caplog, - binary_sensor.DOMAIN, - DEFAULT_CONFIG, + hass, mqtt_mock_entry, binary_sensor.DOMAIN, DEFAULT_CONFIG ) @@ -840,21 +833,15 @@ async def test_unique_id( async def test_discovery_removal_binary_sensor( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test removal of discovered binary_sensor.""" data = json.dumps(DEFAULT_CONFIG[mqtt.DOMAIN][binary_sensor.DOMAIN]) - await help_test_discovery_removal( - hass, mqtt_mock_entry, caplog, binary_sensor.DOMAIN, data - ) + await help_test_discovery_removal(hass, mqtt_mock_entry, binary_sensor.DOMAIN, data) async def test_discovery_update_binary_sensor_topic_template( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered binary_sensor.""" config1 = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][binary_sensor.DOMAIN]) @@ -881,7 +868,6 @@ async def test_discovery_update_binary_sensor_topic_template( await help_test_discovery_update( hass, mqtt_mock_entry, - caplog, binary_sensor.DOMAIN, config1, config2, @@ -891,9 +877,7 @@ async def test_discovery_update_binary_sensor_topic_template( async def test_discovery_update_binary_sensor_template( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered binary_sensor.""" config1 = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][binary_sensor.DOMAIN]) @@ -918,7 +902,6 @@ async def test_discovery_update_binary_sensor_template( await help_test_discovery_update( hass, mqtt_mock_entry, - caplog, binary_sensor.DOMAIN, config1, config2, @@ -962,9 +945,7 @@ async def test_encoding_subscribable_topics( async def test_discovery_update_unchanged_binary_sensor( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered binary_sensor.""" config1 = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][binary_sensor.DOMAIN]) @@ -975,31 +956,19 @@ async def test_discovery_update_unchanged_binary_sensor( "homeassistant.components.mqtt.binary_sensor.MqttBinarySensor.discovery_update" ) as discovery_update: await help_test_discovery_update_unchanged( - hass, - mqtt_mock_entry, - caplog, - binary_sensor.DOMAIN, - data1, - discovery_update, + hass, mqtt_mock_entry, binary_sensor.DOMAIN, data1, discovery_update ) @pytest.mark.no_fail_on_log_exception async def test_discovery_broken( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test handling of bad discovery message.""" data1 = '{ "name": "Beer", "off_delay": -1 }' data2 = '{ "name": "Milk", "state_topic": "test_topic" }' await help_test_discovery_broken( - hass, - mqtt_mock_entry, - caplog, - binary_sensor.DOMAIN, - data1, - data2, + hass, mqtt_mock_entry, binary_sensor.DOMAIN, data1, data2 ) diff --git a/tests/components/mqtt/test_button.py b/tests/components/mqtt/test_button.py index 3d5d295d4d4..2d21128237e 100644 --- a/tests/components/mqtt/test_button.py +++ b/tests/components/mqtt/test_button.py @@ -252,17 +252,11 @@ async def test_update_with_json_attrs_bad_json( async def test_discovery_update_attr( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, - mqtt_mock_entry, - caplog, - button.DOMAIN, - DEFAULT_CONFIG, + hass, mqtt_mock_entry, button.DOMAIN, DEFAULT_CONFIG ) @@ -295,21 +289,15 @@ async def test_unique_id( async def test_discovery_removal_button( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test removal of discovered button.""" data = '{ "name": "test", "command_topic": "test_topic" }' - await help_test_discovery_removal( - hass, mqtt_mock_entry, caplog, button.DOMAIN, data - ) + await help_test_discovery_removal(hass, mqtt_mock_entry, button.DOMAIN, data) async def test_discovery_update_button( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered button.""" config1 = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][button.DOMAIN]) @@ -318,19 +306,12 @@ async def test_discovery_update_button( config2["name"] = "Milk" await help_test_discovery_update( - hass, - mqtt_mock_entry, - caplog, - button.DOMAIN, - config1, - config2, + hass, mqtt_mock_entry, button.DOMAIN, config1, config2 ) async def test_discovery_update_unchanged_button( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered button.""" data1 = ( @@ -342,27 +323,18 @@ async def test_discovery_update_unchanged_button( "homeassistant.components.mqtt.button.MqttButton.discovery_update" ) as discovery_update: await help_test_discovery_update_unchanged( - hass, - mqtt_mock_entry, - caplog, - button.DOMAIN, - data1, - discovery_update, + hass, mqtt_mock_entry, button.DOMAIN, data1, discovery_update ) @pytest.mark.no_fail_on_log_exception async def test_discovery_broken( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test handling of bad discovery message.""" data1 = '{ "name": "Beer" }' data2 = '{ "name": "Milk", "command_topic": "test_topic" }' - await help_test_discovery_broken( - hass, mqtt_mock_entry, caplog, button.DOMAIN, data1, data2 - ) + await help_test_discovery_broken(hass, mqtt_mock_entry, button.DOMAIN, data1, data2) async def test_entity_device_info_with_connection( diff --git a/tests/components/mqtt/test_camera.py b/tests/components/mqtt/test_camera.py index fb0107d6780..9dbf5035fc9 100644 --- a/tests/components/mqtt/test_camera.py +++ b/tests/components/mqtt/test_camera.py @@ -246,17 +246,11 @@ async def test_update_with_json_attrs_bad_json( async def test_discovery_update_attr( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, - mqtt_mock_entry, - caplog, - camera.DOMAIN, - DEFAULT_CONFIG, + hass, mqtt_mock_entry, camera.DOMAIN, DEFAULT_CONFIG ) @@ -289,35 +283,28 @@ async def test_unique_id( async def test_discovery_removal_camera( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test removal of discovered camera.""" data = json.dumps(DEFAULT_CONFIG[mqtt.DOMAIN][camera.DOMAIN]) - await help_test_discovery_removal( - hass, mqtt_mock_entry, caplog, camera.DOMAIN, data - ) + await help_test_discovery_removal(hass, mqtt_mock_entry, camera.DOMAIN, data) async def test_discovery_update_camera( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, ) -> None: """Test update of discovered camera.""" config1 = {"name": "Beer", "topic": "test_topic"} config2 = {"name": "Milk", "topic": "test_topic"} await help_test_discovery_update( - hass, mqtt_mock_entry, caplog, camera.DOMAIN, config1, config2 + hass, mqtt_mock_entry, camera.DOMAIN, config1, config2 ) async def test_discovery_update_unchanged_camera( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered camera.""" data1 = '{ "name": "Beer", "topic": "test_topic"}' @@ -325,28 +312,19 @@ async def test_discovery_update_unchanged_camera( "homeassistant.components.mqtt.camera.MqttCamera.discovery_update" ) as discovery_update: await help_test_discovery_update_unchanged( - hass, - mqtt_mock_entry, - caplog, - camera.DOMAIN, - data1, - discovery_update, + hass, mqtt_mock_entry, camera.DOMAIN, data1, discovery_update ) @pytest.mark.no_fail_on_log_exception async def test_discovery_broken( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test handling of bad discovery message.""" data1 = '{ "name": "Beer" }' data2 = '{ "name": "Milk", "topic": "test_topic"}' - await help_test_discovery_broken( - hass, mqtt_mock_entry, caplog, camera.DOMAIN, data1, data2 - ) + await help_test_discovery_broken(hass, mqtt_mock_entry, camera.DOMAIN, data1, data2) async def test_entity_device_info_with_connection( diff --git a/tests/components/mqtt/test_climate.py b/tests/components/mqtt/test_climate.py index 2bf78e59e42..5428dc9b3e1 100644 --- a/tests/components/mqtt/test_climate.py +++ b/tests/components/mqtt/test_climate.py @@ -1903,17 +1903,11 @@ async def test_update_with_json_attrs_bad_json( async def test_discovery_update_attr( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, - mqtt_mock_entry, - caplog, - climate.DOMAIN, - DEFAULT_CONFIG, + hass, mqtt_mock_entry, climate.DOMAIN, DEFAULT_CONFIG ) @@ -1987,34 +1981,26 @@ async def test_encoding_subscribable_topics( async def test_discovery_removal_climate( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test removal of discovered climate.""" data = json.dumps(DEFAULT_CONFIG[mqtt.DOMAIN][climate.DOMAIN]) - await help_test_discovery_removal( - hass, mqtt_mock_entry, caplog, climate.DOMAIN, data - ) + await help_test_discovery_removal(hass, mqtt_mock_entry, climate.DOMAIN, data) async def test_discovery_update_climate( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered climate.""" config1 = {"name": "Beer"} config2 = {"name": "Milk"} await help_test_discovery_update( - hass, mqtt_mock_entry, caplog, climate.DOMAIN, config1, config2 + hass, mqtt_mock_entry, climate.DOMAIN, config1, config2 ) async def test_discovery_update_unchanged_climate( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered climate.""" data1 = '{ "name": "Beer" }' @@ -2022,26 +2008,19 @@ async def test_discovery_update_unchanged_climate( "homeassistant.components.mqtt.climate.MqttClimate.discovery_update" ) as discovery_update: await help_test_discovery_update_unchanged( - hass, - mqtt_mock_entry, - caplog, - climate.DOMAIN, - data1, - discovery_update, + hass, mqtt_mock_entry, climate.DOMAIN, data1, discovery_update ) @pytest.mark.no_fail_on_log_exception async def test_discovery_broken( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test handling of bad discovery message.""" data1 = '{ "name": "Beer", "power_command_topic": "test_topic#" }' data2 = '{ "name": "Milk", "power_command_topic": "test_topic" }' await help_test_discovery_broken( - hass, mqtt_mock_entry, caplog, climate.DOMAIN, data1, data2 + hass, mqtt_mock_entry, climate.DOMAIN, data1, data2 ) diff --git a/tests/components/mqtt/test_common.py b/tests/components/mqtt/test_common.py index d196e1998fb..8d457d9da85 100644 --- a/tests/components/mqtt/test_common.py +++ b/tests/components/mqtt/test_common.py @@ -103,9 +103,7 @@ def help_custom_config( async def help_test_availability_when_connection_lost( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - domain: str, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, domain: str ) -> None: """Test availability after MQTT disconnection.""" mqtt_mock = await mqtt_mock_entry() @@ -251,8 +249,6 @@ async def help_test_default_availability_list_payload_all( domain: str, config: ConfigType, no_assumed_state: bool = False, - state_topic: str | None = None, - state_message: str | None = None, ) -> None: """Test availability by default payload with defined topic. @@ -314,8 +310,6 @@ async def help_test_default_availability_list_payload_any( domain: str, config: ConfigType, no_assumed_state: bool = False, - state_topic: str | None = None, - state_message: str | None = None, ) -> None: """Test availability by default payload with defined topic. @@ -657,7 +651,6 @@ async def help_test_update_with_json_attrs_bad_json( async def help_test_discovery_update_attr( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, domain: str, config: ConfigType, ) -> None: @@ -696,9 +689,7 @@ async def help_test_discovery_update_attr( async def help_test_unique_id( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - domain: str, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, domain: str ) -> None: """Test unique id option only creates one entity per unique_id.""" await mqtt_mock_entry() @@ -709,7 +700,6 @@ async def help_test_unique_id( async def help_test_discovery_removal( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, domain: str, data: str, ) -> None: @@ -735,8 +725,7 @@ async def help_test_discovery_removal( async def help_test_discovery_update( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, - caplog, - domain, + domain: str, discovery_config1: DiscoveryInfoType, discovery_config2: DiscoveryInfoType, state_data1: _StateDataType | None = None, @@ -800,7 +789,6 @@ async def help_test_discovery_update( async def help_test_discovery_update_unchanged( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, domain: str, data1: str, discovery_update: MagicMock, @@ -826,7 +814,6 @@ async def help_test_discovery_update_unchanged( async def help_test_discovery_broken( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, domain: str, data1: str, data2: str, diff --git a/tests/components/mqtt/test_cover.py b/tests/components/mqtt/test_cover.py index 4b46f49c629..988119d09c1 100644 --- a/tests/components/mqtt/test_cover.py +++ b/tests/components/mqtt/test_cover.py @@ -2571,17 +2571,11 @@ async def test_update_with_json_attrs_bad_json( async def test_discovery_update_attr( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, - mqtt_mock_entry, - caplog, - cover.DOMAIN, - DEFAULT_CONFIG, + hass, mqtt_mock_entry, cover.DOMAIN, DEFAULT_CONFIG ) @@ -2614,32 +2608,26 @@ async def test_unique_id( async def test_discovery_removal_cover( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test removal of discovered cover.""" data = '{ "name": "test", "command_topic": "test_topic" }' - await help_test_discovery_removal(hass, mqtt_mock_entry, caplog, cover.DOMAIN, data) + await help_test_discovery_removal(hass, mqtt_mock_entry, cover.DOMAIN, data) async def test_discovery_update_cover( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered cover.""" config1 = {"name": "Beer", "command_topic": "test_topic"} config2 = {"name": "Milk", "command_topic": "test_topic"} await help_test_discovery_update( - hass, mqtt_mock_entry, caplog, cover.DOMAIN, config1, config2 + hass, mqtt_mock_entry, cover.DOMAIN, config1, config2 ) async def test_discovery_update_unchanged_cover( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered cover.""" data1 = '{ "name": "Beer", "command_topic": "test_topic" }' @@ -2647,27 +2635,18 @@ async def test_discovery_update_unchanged_cover( "homeassistant.components.mqtt.cover.MqttCover.discovery_update" ) as discovery_update: await help_test_discovery_update_unchanged( - hass, - mqtt_mock_entry, - caplog, - cover.DOMAIN, - data1, - discovery_update, + hass, mqtt_mock_entry, cover.DOMAIN, data1, discovery_update ) @pytest.mark.no_fail_on_log_exception async def test_discovery_broken( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test handling of bad discovery message.""" data1 = '{ "name": "Beer", "command_topic": "test_topic#" }' data2 = '{ "name": "Milk", "command_topic": "test_topic" }' - await help_test_discovery_broken( - hass, mqtt_mock_entry, caplog, cover.DOMAIN, data1, data2 - ) + await help_test_discovery_broken(hass, mqtt_mock_entry, cover.DOMAIN, data1, data2) async def test_entity_device_info_with_connection( diff --git a/tests/components/mqtt/test_event.py b/tests/components/mqtt/test_event.py index fd4f8eb3e5d..48f80bf41d7 100644 --- a/tests/components/mqtt/test_event.py +++ b/tests/components/mqtt/test_event.py @@ -397,17 +397,11 @@ async def test_update_with_json_attrs_bad_json( async def test_discovery_update_attr( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, - mqtt_mock_entry, - caplog, - event.DOMAIN, - DEFAULT_CONFIG, + hass, mqtt_mock_entry, event.DOMAIN, DEFAULT_CONFIG ) @@ -442,13 +436,11 @@ async def test_unique_id( async def test_discovery_removal_event( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test removal of discovered event.""" data = '{ "name": "test", "state_topic": "test_topic", "event_types": ["press"] }' - await help_test_discovery_removal(hass, mqtt_mock_entry, caplog, event.DOMAIN, data) + await help_test_discovery_removal(hass, mqtt_mock_entry, event.DOMAIN, data) async def test_discovery_update_event_template( @@ -491,16 +483,12 @@ async def test_discovery_update_event_template( @pytest.mark.no_fail_on_log_exception async def test_discovery_broken( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test handling of bad discovery message.""" data1 = '{ "name": "Beer", "state_topic": "test_topic#", "event_types": ["press"] }' data2 = '{ "name": "Milk", "state_topic": "test_topic", "event_types": ["press"] }' - await help_test_discovery_broken( - hass, mqtt_mock_entry, caplog, event.DOMAIN, data1, data2 - ) + await help_test_discovery_broken(hass, mqtt_mock_entry, event.DOMAIN, data1, data2) async def test_entity_device_info_with_connection( diff --git a/tests/components/mqtt/test_fan.py b/tests/components/mqtt/test_fan.py index 0dbfa3037b2..80e45c87789 100644 --- a/tests/components/mqtt/test_fan.py +++ b/tests/components/mqtt/test_fan.py @@ -1989,13 +1989,11 @@ async def test_update_with_json_attrs_bad_json( async def test_discovery_update_attr( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, mqtt_mock_entry, caplog, fan.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry, fan.DOMAIN, DEFAULT_CONFIG ) @@ -2030,32 +2028,26 @@ async def test_unique_id( async def test_discovery_removal_fan( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test removal of discovered fan.""" data = '{ "name": "test", "command_topic": "test_topic" }' - await help_test_discovery_removal(hass, mqtt_mock_entry, caplog, fan.DOMAIN, data) + await help_test_discovery_removal(hass, mqtt_mock_entry, fan.DOMAIN, data) async def test_discovery_update_fan( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered fan.""" config1 = {"name": "Beer", "command_topic": "test_topic"} config2 = {"name": "Milk", "command_topic": "test_topic"} await help_test_discovery_update( - hass, mqtt_mock_entry, caplog, fan.DOMAIN, config1, config2 + hass, mqtt_mock_entry, fan.DOMAIN, config1, config2 ) async def test_discovery_update_unchanged_fan( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered fan.""" data1 = '{ "name": "Beer", "command_topic": "test_topic" }' @@ -2063,28 +2055,19 @@ async def test_discovery_update_unchanged_fan( "homeassistant.components.mqtt.fan.MqttFan.discovery_update" ) as discovery_update: await help_test_discovery_update_unchanged( - hass, - mqtt_mock_entry, - caplog, - fan.DOMAIN, - data1, - discovery_update, + hass, mqtt_mock_entry, fan.DOMAIN, data1, discovery_update ) @pytest.mark.no_fail_on_log_exception async def test_discovery_broken( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test handling of bad discovery message.""" data1 = '{ "name": "Beer" }' data2 = '{ "name": "Milk", "command_topic": "test_topic" }' - await help_test_discovery_broken( - hass, mqtt_mock_entry, caplog, fan.DOMAIN, data1, data2 - ) + await help_test_discovery_broken(hass, mqtt_mock_entry, fan.DOMAIN, data1, data2) async def test_entity_device_info_with_connection( diff --git a/tests/components/mqtt/test_humidifier.py b/tests/components/mqtt/test_humidifier.py index 4e8918d330e..b583412b4ff 100644 --- a/tests/components/mqtt/test_humidifier.py +++ b/tests/components/mqtt/test_humidifier.py @@ -1276,17 +1276,11 @@ async def test_update_with_json_attrs_bad_json( async def test_discovery_update_attr( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, - mqtt_mock_entry, - caplog, - humidifier.DOMAIN, - DEFAULT_CONFIG, + hass, mqtt_mock_entry, humidifier.DOMAIN, DEFAULT_CONFIG ) @@ -1323,21 +1317,15 @@ async def test_unique_id( async def test_discovery_removal_humidifier( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test removal of discovered humidifier.""" data = '{ "name": "test", "command_topic": "test_topic", "target_humidity_command_topic": "test-topic2" }' - await help_test_discovery_removal( - hass, mqtt_mock_entry, caplog, humidifier.DOMAIN, data - ) + await help_test_discovery_removal(hass, mqtt_mock_entry, humidifier.DOMAIN, data) async def test_discovery_update_humidifier( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered humidifier.""" config1 = { @@ -1351,19 +1339,12 @@ async def test_discovery_update_humidifier( "target_humidity_command_topic": "test-topic2", } await help_test_discovery_update( - hass, - mqtt_mock_entry, - caplog, - humidifier.DOMAIN, - config1, - config2, + hass, mqtt_mock_entry, humidifier.DOMAIN, config1, config2 ) async def test_discovery_update_unchanged_humidifier( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered humidifier.""" data1 = '{ "name": "Beer", "command_topic": "test_topic", "target_humidity_command_topic": "test-topic2" }' @@ -1371,26 +1352,19 @@ async def test_discovery_update_unchanged_humidifier( "homeassistant.components.mqtt.fan.MqttFan.discovery_update" ) as discovery_update: await help_test_discovery_update_unchanged( - hass, - mqtt_mock_entry, - caplog, - humidifier.DOMAIN, - data1, - discovery_update, + hass, mqtt_mock_entry, humidifier.DOMAIN, data1, discovery_update ) @pytest.mark.no_fail_on_log_exception async def test_discovery_broken( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test handling of bad discovery message.""" data1 = '{ "name": "Beer" }' data2 = '{ "name": "Milk", "command_topic": "test_topic", "target_humidity_command_topic": "test-topic2" }' await help_test_discovery_broken( - hass, mqtt_mock_entry, caplog, humidifier.DOMAIN, data1, data2 + hass, mqtt_mock_entry, humidifier.DOMAIN, data1, data2 ) diff --git a/tests/components/mqtt/test_image.py b/tests/components/mqtt/test_image.py index 79e6cf1d281..a299474c0ac 100644 --- a/tests/components/mqtt/test_image.py +++ b/tests/components/mqtt/test_image.py @@ -600,17 +600,11 @@ async def test_update_with_json_attrs_bad_json( async def test_discovery_update_attr( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, - mqtt_mock_entry, - caplog, - image.DOMAIN, - DEFAULT_CONFIG, + hass, mqtt_mock_entry, image.DOMAIN, DEFAULT_CONFIG ) @@ -643,33 +637,27 @@ async def test_unique_id( async def test_discovery_removal_image( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test removal of discovered image.""" data = json.dumps(DEFAULT_CONFIG[mqtt.DOMAIN][image.DOMAIN]) - await help_test_discovery_removal(hass, mqtt_mock_entry, caplog, image.DOMAIN, data) + await help_test_discovery_removal(hass, mqtt_mock_entry, image.DOMAIN, data) async def test_discovery_update_image( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered image.""" config1 = {"name": "Beer", "image_topic": "test_topic"} config2 = {"name": "Milk", "image_topic": "test_topic"} await help_test_discovery_update( - hass, mqtt_mock_entry, caplog, image.DOMAIN, config1, config2 + hass, mqtt_mock_entry, image.DOMAIN, config1, config2 ) async def test_discovery_update_unchanged_image( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered image.""" data1 = '{ "name": "Beer", "image_topic": "test_topic"}' @@ -677,28 +665,19 @@ async def test_discovery_update_unchanged_image( "homeassistant.components.mqtt.image.MqttImage.discovery_update" ) as discovery_update: await help_test_discovery_update_unchanged( - hass, - mqtt_mock_entry, - caplog, - image.DOMAIN, - data1, - discovery_update, + hass, mqtt_mock_entry, image.DOMAIN, data1, discovery_update ) @pytest.mark.no_fail_on_log_exception async def test_discovery_broken( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test handling of bad discovery message.""" data1 = '{ "name": "Beer" }' data2 = '{ "name": "Milk", "image_topic": "test_topic"}' - await help_test_discovery_broken( - hass, mqtt_mock_entry, caplog, image.DOMAIN, data1, data2 - ) + await help_test_discovery_broken(hass, mqtt_mock_entry, image.DOMAIN, data1, data2) async def test_entity_device_info_with_connection( diff --git a/tests/components/mqtt/test_lawn_mower.py b/tests/components/mqtt/test_lawn_mower.py index a258339e9cc..120a09deb88 100644 --- a/tests/components/mqtt/test_lawn_mower.py +++ b/tests/components/mqtt/test_lawn_mower.py @@ -466,17 +466,11 @@ async def test_update_with_json_attrs_bad_json( async def test_discovery_update_attr( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, - mqtt_mock_entry, - caplog, - lawn_mower.DOMAIN, - DEFAULT_CONFIG, + hass, mqtt_mock_entry, lawn_mower.DOMAIN, DEFAULT_CONFIG ) @@ -509,21 +503,16 @@ async def test_unique_id( async def test_discovery_removal_lawn_mower( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test removal of discovered lawn_mower.""" data = json.dumps(DEFAULT_CONFIG[mqtt.DOMAIN][lawn_mower.DOMAIN]) - await help_test_discovery_removal( - hass, mqtt_mock_entry, caplog, lawn_mower.DOMAIN, data - ) + await help_test_discovery_removal(hass, mqtt_mock_entry, lawn_mower.DOMAIN, data) async def test_discovery_update_lawn_mower( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, ) -> None: """Test update of discovered lawn_mower.""" config1 = { @@ -540,14 +529,12 @@ async def test_discovery_update_lawn_mower( } await help_test_discovery_update( - hass, mqtt_mock_entry, caplog, lawn_mower.DOMAIN, config1, config2 + hass, mqtt_mock_entry, lawn_mower.DOMAIN, config1, config2 ) async def test_discovery_update_unchanged_lawn_mower( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered lawn_mower.""" data1 = '{ "name": "Beer", "activity_state_topic": "test-topic", "command_topic": "test-topic", "actions": ["milk", "beer"]}' @@ -555,27 +542,20 @@ async def test_discovery_update_unchanged_lawn_mower( "homeassistant.components.mqtt.lawn_mower.MqttLawnMower.discovery_update" ) as discovery_update: await help_test_discovery_update_unchanged( - hass, - mqtt_mock_entry, - caplog, - lawn_mower.DOMAIN, - data1, - discovery_update, + hass, mqtt_mock_entry, lawn_mower.DOMAIN, data1, discovery_update ) @pytest.mark.no_fail_on_log_exception async def test_discovery_broken( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test handling of bad discovery message.""" data1 = '{ "invalid" }' data2 = '{ "name": "Milk", "activity_state_topic": "test-topic", "pause_command_topic": "test-topic"}' await help_test_discovery_broken( - hass, mqtt_mock_entry, caplog, lawn_mower.DOMAIN, data1, data2 + hass, mqtt_mock_entry, lawn_mower.DOMAIN, data1, data2 ) diff --git a/tests/components/mqtt/test_light.py b/tests/components/mqtt/test_light.py index 492bc6806da..bfce49b9ecb 100644 --- a/tests/components/mqtt/test_light.py +++ b/tests/components/mqtt/test_light.py @@ -2516,17 +2516,11 @@ async def test_update_with_json_attrs_bad_json( async def test_discovery_update_attr( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, - mqtt_mock_entry, - caplog, - light.DOMAIN, - DEFAULT_CONFIG, + hass, mqtt_mock_entry, light.DOMAIN, DEFAULT_CONFIG ) @@ -2561,9 +2555,7 @@ async def test_unique_id( async def test_discovery_removal_light( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test removal of discovered light.""" data = ( @@ -2571,7 +2563,7 @@ async def test_discovery_removal_light( ' "state_topic": "test_topic",' ' "command_topic": "test_topic" }' ) - await help_test_discovery_removal(hass, mqtt_mock_entry, caplog, light.DOMAIN, data) + await help_test_discovery_removal(hass, mqtt_mock_entry, light.DOMAIN, data) async def test_discovery_ignores_extra_keys( @@ -2591,9 +2583,7 @@ async def test_discovery_ignores_extra_keys( async def test_discovery_update_light_topic_and_template( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered light.""" config1 = { @@ -2838,7 +2828,6 @@ async def test_discovery_update_light_topic_and_template( await help_test_discovery_update( hass, mqtt_mock_entry, - caplog, light.DOMAIN, config1, config2, @@ -2848,9 +2837,7 @@ async def test_discovery_update_light_topic_and_template( async def test_discovery_update_light_template( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered light.""" config1 = { @@ -3053,7 +3040,6 @@ async def test_discovery_update_light_template( await help_test_discovery_update( hass, mqtt_mock_entry, - caplog, light.DOMAIN, config1, config2, @@ -3063,9 +3049,7 @@ async def test_discovery_update_light_template( async def test_discovery_update_unchanged_light( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered light.""" data1 = ( @@ -3077,20 +3061,13 @@ async def test_discovery_update_unchanged_light( "homeassistant.components.mqtt.light.schema_basic.MqttLight.discovery_update" ) as discovery_update: await help_test_discovery_update_unchanged( - hass, - mqtt_mock_entry, - caplog, - light.DOMAIN, - data1, - discovery_update, + hass, mqtt_mock_entry, light.DOMAIN, data1, discovery_update ) @pytest.mark.no_fail_on_log_exception async def test_discovery_broken( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test handling of bad discovery message.""" data1 = '{ "name": "Beer" }' @@ -3099,9 +3076,7 @@ async def test_discovery_broken( ' "state_topic": "test_topic",' ' "command_topic": "test_topic" }' ) - await help_test_discovery_broken( - hass, mqtt_mock_entry, caplog, light.DOMAIN, data1, data2 - ) + await help_test_discovery_broken(hass, mqtt_mock_entry, light.DOMAIN, data1, data2) async def test_entity_device_info_with_connection( diff --git a/tests/components/mqtt/test_light_json.py b/tests/components/mqtt/test_light_json.py index 739240a352c..5ab2a32dc83 100644 --- a/tests/components/mqtt/test_light_json.py +++ b/tests/components/mqtt/test_light_json.py @@ -2398,17 +2398,11 @@ async def test_update_with_json_attrs_bad_json( async def test_discovery_update_attr( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, - mqtt_mock_entry, - caplog, - light.DOMAIN, - DEFAULT_CONFIG, + hass, mqtt_mock_entry, light.DOMAIN, DEFAULT_CONFIG ) @@ -2445,25 +2439,15 @@ async def test_unique_id( async def test_discovery_removal( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test removal of discovered mqtt_json lights.""" data = '{ "name": "test", "schema": "json", "command_topic": "test_topic" }' - await help_test_discovery_removal( - hass, - mqtt_mock_entry, - caplog, - light.DOMAIN, - data, - ) + await help_test_discovery_removal(hass, mqtt_mock_entry, light.DOMAIN, data) async def test_discovery_update_light( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered light.""" config1 = { @@ -2479,19 +2463,12 @@ async def test_discovery_update_light( "command_topic": "test_topic", } await help_test_discovery_update( - hass, - mqtt_mock_entry, - caplog, - light.DOMAIN, - config1, - config2, + hass, mqtt_mock_entry, light.DOMAIN, config1, config2 ) async def test_discovery_update_unchanged_light( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered light.""" data1 = ( @@ -2504,20 +2481,13 @@ async def test_discovery_update_unchanged_light( "homeassistant.components.mqtt.light.schema_json.MqttLightJson.discovery_update" ) as discovery_update: await help_test_discovery_update_unchanged( - hass, - mqtt_mock_entry, - caplog, - light.DOMAIN, - data1, - discovery_update, + hass, mqtt_mock_entry, light.DOMAIN, data1, discovery_update ) @pytest.mark.no_fail_on_log_exception async def test_discovery_broken( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test handling of bad discovery message.""" data1 = '{ "name": "Beer" }' @@ -2527,14 +2497,7 @@ async def test_discovery_broken( ' "state_topic": "test_topic",' ' "command_topic": "test_topic" }' ) - await help_test_discovery_broken( - hass, - mqtt_mock_entry, - caplog, - light.DOMAIN, - data1, - data2, - ) + await help_test_discovery_broken(hass, mqtt_mock_entry, light.DOMAIN, data1, data2) async def test_entity_device_info_with_connection( diff --git a/tests/components/mqtt/test_light_template.py b/tests/components/mqtt/test_light_template.py index da6195fa32e..aace09f402a 100644 --- a/tests/components/mqtt/test_light_template.py +++ b/tests/components/mqtt/test_light_template.py @@ -1002,17 +1002,11 @@ async def test_update_with_json_attrs_bad_json( async def test_discovery_update_attr( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, - mqtt_mock_entry, - caplog, - light.DOMAIN, - DEFAULT_CONFIG, + hass, mqtt_mock_entry, light.DOMAIN, DEFAULT_CONFIG ) @@ -1053,9 +1047,7 @@ async def test_unique_id( async def test_discovery_removal( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test removal of discovered mqtt_json lights.""" data = ( @@ -1065,13 +1057,11 @@ async def test_discovery_removal( ' "command_on_template": "on",' ' "command_off_template": "off"}' ) - await help_test_discovery_removal(hass, mqtt_mock_entry, caplog, light.DOMAIN, data) + await help_test_discovery_removal(hass, mqtt_mock_entry, light.DOMAIN, data) async def test_discovery_update_light( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered light.""" config1 = { @@ -1091,14 +1081,12 @@ async def test_discovery_update_light( "command_off_template": "off", } await help_test_discovery_update( - hass, mqtt_mock_entry, caplog, light.DOMAIN, config1, config2 + hass, mqtt_mock_entry, light.DOMAIN, config1, config2 ) async def test_discovery_update_unchanged_light( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered light.""" data1 = ( @@ -1113,20 +1101,13 @@ async def test_discovery_update_unchanged_light( "homeassistant.components.mqtt.light.schema_template.MqttLightTemplate.discovery_update" ) as discovery_update: await help_test_discovery_update_unchanged( - hass, - mqtt_mock_entry, - caplog, - light.DOMAIN, - data1, - discovery_update, + hass, mqtt_mock_entry, light.DOMAIN, data1, discovery_update ) @pytest.mark.no_fail_on_log_exception async def test_discovery_broken( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test handling of bad discovery message.""" data1 = '{ "name": "Beer" }' @@ -1138,9 +1119,7 @@ async def test_discovery_broken( ' "command_on_template": "on",' ' "command_off_template": "off"}' ) - await help_test_discovery_broken( - hass, mqtt_mock_entry, caplog, light.DOMAIN, data1, data2 - ) + await help_test_discovery_broken(hass, mqtt_mock_entry, light.DOMAIN, data1, data2) async def test_entity_device_info_with_connection( diff --git a/tests/components/mqtt/test_lock.py b/tests/components/mqtt/test_lock.py index c9c2928f991..c9546bdfdb3 100644 --- a/tests/components/mqtt/test_lock.py +++ b/tests/components/mqtt/test_lock.py @@ -805,13 +805,11 @@ async def test_update_with_json_attrs_bad_json( async def test_discovery_update_attr( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, mqtt_mock_entry, caplog, lock.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry, lock.DOMAIN, DEFAULT_CONFIG ) @@ -846,19 +844,15 @@ async def test_unique_id( async def test_discovery_removal_lock( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test removal of discovered lock.""" data = '{ "name": "test", "command_topic": "test_topic" }' - await help_test_discovery_removal(hass, mqtt_mock_entry, caplog, lock.DOMAIN, data) + await help_test_discovery_removal(hass, mqtt_mock_entry, lock.DOMAIN, data) async def test_discovery_update_lock( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered lock.""" config1 = { @@ -874,14 +868,12 @@ async def test_discovery_update_lock( "availability_topic": "availability_topic2", } await help_test_discovery_update( - hass, mqtt_mock_entry, caplog, lock.DOMAIN, config1, config2 + hass, mqtt_mock_entry, lock.DOMAIN, config1, config2 ) async def test_discovery_update_unchanged_lock( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered lock.""" data1 = ( @@ -893,27 +885,18 @@ async def test_discovery_update_unchanged_lock( "homeassistant.components.mqtt.lock.MqttLock.discovery_update" ) as discovery_update: await help_test_discovery_update_unchanged( - hass, - mqtt_mock_entry, - caplog, - lock.DOMAIN, - data1, - discovery_update, + hass, mqtt_mock_entry, lock.DOMAIN, data1, discovery_update ) @pytest.mark.no_fail_on_log_exception async def test_discovery_broken( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test handling of bad discovery message.""" data1 = '{ "name": "Beer" }' data2 = '{ "name": "Milk", "command_topic": "test_topic" }' - await help_test_discovery_broken( - hass, mqtt_mock_entry, caplog, lock.DOMAIN, data1, data2 - ) + await help_test_discovery_broken(hass, mqtt_mock_entry, lock.DOMAIN, data1, data2) async def test_entity_device_info_with_connection( diff --git a/tests/components/mqtt/test_notify.py b/tests/components/mqtt/test_notify.py index bc833b79eb0..540dbbafd99 100644 --- a/tests/components/mqtt/test_notify.py +++ b/tests/components/mqtt/test_notify.py @@ -223,17 +223,11 @@ async def test_update_with_json_attrs_bad_json( async def test_discovery_update_attr( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, - mqtt_mock_entry, - caplog, - notify.DOMAIN, - DEFAULT_CONFIG, + hass, mqtt_mock_entry, notify.DOMAIN, DEFAULT_CONFIG ) @@ -266,21 +260,15 @@ async def test_unique_id( async def test_discovery_removal_notify( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test removal of discovered notify.""" data = '{ "name": "test", "command_topic": "test_topic" }' - await help_test_discovery_removal( - hass, mqtt_mock_entry, caplog, notify.DOMAIN, data - ) + await help_test_discovery_removal(hass, mqtt_mock_entry, notify.DOMAIN, data) async def test_discovery_update_notify( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered notify.""" config1 = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][notify.DOMAIN]) @@ -289,19 +277,12 @@ async def test_discovery_update_notify( config2["name"] = "Milk" await help_test_discovery_update( - hass, - mqtt_mock_entry, - caplog, - notify.DOMAIN, - config1, - config2, + hass, mqtt_mock_entry, notify.DOMAIN, config1, config2 ) async def test_discovery_update_unchanged_notify( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered notify.""" data1 = ( @@ -313,27 +294,18 @@ async def test_discovery_update_unchanged_notify( "homeassistant.components.mqtt.notify.MqttNotify.discovery_update" ) as discovery_update: await help_test_discovery_update_unchanged( - hass, - mqtt_mock_entry, - caplog, - notify.DOMAIN, - data1, - discovery_update, + hass, mqtt_mock_entry, notify.DOMAIN, data1, discovery_update ) @pytest.mark.no_fail_on_log_exception async def test_discovery_broken( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test handling of bad discovery message.""" data1 = '{ "name": "Beer" }' data2 = '{ "name": "Milk", "command_topic": "test_topic" }' - await help_test_discovery_broken( - hass, mqtt_mock_entry, caplog, notify.DOMAIN, data1, data2 - ) + await help_test_discovery_broken(hass, mqtt_mock_entry, notify.DOMAIN, data1, data2) async def test_entity_device_info_with_connection( diff --git a/tests/components/mqtt/test_number.py b/tests/components/mqtt/test_number.py index b0f9e79cb3e..2cd5c5390f5 100644 --- a/tests/components/mqtt/test_number.py +++ b/tests/components/mqtt/test_number.py @@ -581,17 +581,11 @@ async def test_update_with_json_attrs_bad_json( async def test_discovery_update_attr( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, - mqtt_mock_entry, - caplog, - number.DOMAIN, - DEFAULT_CONFIG, + hass, mqtt_mock_entry, number.DOMAIN, DEFAULT_CONFIG ) @@ -626,21 +620,15 @@ async def test_unique_id( async def test_discovery_removal_number( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test removal of discovered number.""" data = json.dumps(DEFAULT_CONFIG[mqtt.DOMAIN][number.DOMAIN]) - await help_test_discovery_removal( - hass, mqtt_mock_entry, caplog, number.DOMAIN, data - ) + await help_test_discovery_removal(hass, mqtt_mock_entry, number.DOMAIN, data) async def test_discovery_update_number( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered number.""" config1 = { @@ -655,14 +643,12 @@ async def test_discovery_update_number( } await help_test_discovery_update( - hass, mqtt_mock_entry, caplog, number.DOMAIN, config1, config2 + hass, mqtt_mock_entry, number.DOMAIN, config1, config2 ) async def test_discovery_update_unchanged_number( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered number.""" data1 = ( @@ -672,20 +658,13 @@ async def test_discovery_update_unchanged_number( "homeassistant.components.mqtt.number.MqttNumber.discovery_update" ) as discovery_update: await help_test_discovery_update_unchanged( - hass, - mqtt_mock_entry, - caplog, - number.DOMAIN, - data1, - discovery_update, + hass, mqtt_mock_entry, number.DOMAIN, data1, discovery_update ) @pytest.mark.no_fail_on_log_exception async def test_discovery_broken( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test handling of bad discovery message.""" data1 = '{ "name": "Beer" }' @@ -693,9 +672,7 @@ async def test_discovery_broken( '{ "name": "Milk", "state_topic": "test-topic", "command_topic": "test-topic"}' ) - await help_test_discovery_broken( - hass, mqtt_mock_entry, caplog, number.DOMAIN, data1, data2 - ) + await help_test_discovery_broken(hass, mqtt_mock_entry, number.DOMAIN, data1, data2) async def test_entity_device_info_with_connection( diff --git a/tests/components/mqtt/test_scene.py b/tests/components/mqtt/test_scene.py index 3e9eacd3be2..9badd6aeee0 100644 --- a/tests/components/mqtt/test_scene.py +++ b/tests/components/mqtt/test_scene.py @@ -183,19 +183,15 @@ async def test_unique_id( async def test_discovery_removal_scene( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test removal of discovered scene.""" data = '{ "name": "test", "command_topic": "test_topic" }' - await help_test_discovery_removal(hass, mqtt_mock_entry, caplog, scene.DOMAIN, data) + await help_test_discovery_removal(hass, mqtt_mock_entry, scene.DOMAIN, data) async def test_discovery_update_payload( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered scene.""" config1 = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][scene.DOMAIN]) @@ -206,19 +202,12 @@ async def test_discovery_update_payload( config2["payload_on"] = "ACTIVATE" await help_test_discovery_update( - hass, - mqtt_mock_entry, - caplog, - scene.DOMAIN, - config1, - config2, + hass, mqtt_mock_entry, scene.DOMAIN, config1, config2 ) async def test_discovery_update_unchanged_scene( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered scene.""" data1 = '{ "name": "Beer", "command_topic": "test_topic" }' @@ -226,27 +215,18 @@ async def test_discovery_update_unchanged_scene( "homeassistant.components.mqtt.scene.MqttScene.discovery_update" ) as discovery_update: await help_test_discovery_update_unchanged( - hass, - mqtt_mock_entry, - caplog, - scene.DOMAIN, - data1, - discovery_update, + hass, mqtt_mock_entry, scene.DOMAIN, data1, discovery_update ) @pytest.mark.no_fail_on_log_exception async def test_discovery_broken( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test handling of bad discovery message.""" data1 = '{ "name": "Beer" }' data2 = '{ "name": "Milk", "command_topic": "test_topic" }' - await help_test_discovery_broken( - hass, mqtt_mock_entry, caplog, scene.DOMAIN, data1, data2 - ) + await help_test_discovery_broken(hass, mqtt_mock_entry, scene.DOMAIN, data1, data2) async def test_setting_attribute_via_mqtt_json_message( @@ -307,17 +287,11 @@ async def test_update_with_json_attrs_bad_json( async def test_discovery_update_attr( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, - mqtt_mock_entry, - caplog, - scene.DOMAIN, - DEFAULT_CONFIG, + hass, mqtt_mock_entry, scene.DOMAIN, DEFAULT_CONFIG ) diff --git a/tests/components/mqtt/test_select.py b/tests/components/mqtt/test_select.py index b8c55dd2ffb..26a64d70fee 100644 --- a/tests/components/mqtt/test_select.py +++ b/tests/components/mqtt/test_select.py @@ -431,17 +431,11 @@ async def test_update_with_json_attrs_bad_json( async def test_discovery_update_attr( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, - mqtt_mock_entry, - caplog, - select.DOMAIN, - DEFAULT_CONFIG, + hass, mqtt_mock_entry, select.DOMAIN, DEFAULT_CONFIG ) @@ -478,21 +472,15 @@ async def test_unique_id( async def test_discovery_removal_select( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test removal of discovered select.""" data = json.dumps(DEFAULT_CONFIG[mqtt.DOMAIN][select.DOMAIN]) - await help_test_discovery_removal( - hass, mqtt_mock_entry, caplog, select.DOMAIN, data - ) + await help_test_discovery_removal(hass, mqtt_mock_entry, select.DOMAIN, data) async def test_discovery_update_select( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered select.""" config1 = { @@ -509,14 +497,12 @@ async def test_discovery_update_select( } await help_test_discovery_update( - hass, mqtt_mock_entry, caplog, select.DOMAIN, config1, config2 + hass, mqtt_mock_entry, select.DOMAIN, config1, config2 ) async def test_discovery_update_unchanged_select( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered select.""" data1 = '{ "name": "Beer", "state_topic": "test-topic", "command_topic": "test-topic", "options": ["milk", "beer"]}' @@ -524,28 +510,19 @@ async def test_discovery_update_unchanged_select( "homeassistant.components.mqtt.select.MqttSelect.discovery_update" ) as discovery_update: await help_test_discovery_update_unchanged( - hass, - mqtt_mock_entry, - caplog, - select.DOMAIN, - data1, - discovery_update, + hass, mqtt_mock_entry, select.DOMAIN, data1, discovery_update ) @pytest.mark.no_fail_on_log_exception async def test_discovery_broken( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test handling of bad discovery message.""" data1 = '{ "name": "Beer" }' data2 = '{ "name": "Milk", "state_topic": "test-topic", "command_topic": "test-topic", "options": ["milk", "beer"]}' - await help_test_discovery_broken( - hass, mqtt_mock_entry, caplog, select.DOMAIN, data1, data2 - ) + await help_test_discovery_broken(hass, mqtt_mock_entry, select.DOMAIN, data1, data2) async def test_entity_device_info_with_connection( diff --git a/tests/components/mqtt/test_sensor.py b/tests/components/mqtt/test_sensor.py index bde85abf3fb..94eb049dda7 100644 --- a/tests/components/mqtt/test_sensor.py +++ b/tests/components/mqtt/test_sensor.py @@ -978,15 +978,12 @@ async def test_update_with_json_attrs_bad_json( async def test_discovery_update_attr( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( hass, mqtt_mock_entry, - caplog, sensor.DOMAIN, DEFAULT_CONFIG, ) @@ -1021,21 +1018,15 @@ async def test_unique_id( async def test_discovery_removal_sensor( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test removal of discovered sensor.""" data = '{ "name": "test", "state_topic": "test_topic" }' - await help_test_discovery_removal( - hass, mqtt_mock_entry, caplog, sensor.DOMAIN, data - ) + await help_test_discovery_removal(hass, mqtt_mock_entry, sensor.DOMAIN, data) async def test_discovery_update_sensor_topic_template( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered sensor.""" config = {"name": "test", "state_topic": "test_topic"} @@ -1060,7 +1051,6 @@ async def test_discovery_update_sensor_topic_template( await help_test_discovery_update( hass, mqtt_mock_entry, - caplog, sensor.DOMAIN, config1, config2, @@ -1070,9 +1060,7 @@ async def test_discovery_update_sensor_topic_template( async def test_discovery_update_sensor_template( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered sensor.""" config = {"name": "test", "state_topic": "test_topic"} @@ -1095,7 +1083,6 @@ async def test_discovery_update_sensor_template( await help_test_discovery_update( hass, mqtt_mock_entry, - caplog, sensor.DOMAIN, config1, config2, @@ -1105,9 +1092,7 @@ async def test_discovery_update_sensor_template( async def test_discovery_update_unchanged_sensor( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered sensor.""" data1 = '{ "name": "Beer", "state_topic": "test_topic" }' @@ -1115,27 +1100,18 @@ async def test_discovery_update_unchanged_sensor( "homeassistant.components.mqtt.sensor.MqttSensor.discovery_update" ) as discovery_update: await help_test_discovery_update_unchanged( - hass, - mqtt_mock_entry, - caplog, - sensor.DOMAIN, - data1, - discovery_update, + hass, mqtt_mock_entry, sensor.DOMAIN, data1, discovery_update ) @pytest.mark.no_fail_on_log_exception async def test_discovery_broken( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test handling of bad discovery message.""" data1 = '{ "name": "Beer", "state_topic": "test_topic#" }' data2 = '{ "name": "Milk", "state_topic": "test_topic" }' - await help_test_discovery_broken( - hass, mqtt_mock_entry, caplog, sensor.DOMAIN, data1, data2 - ) + await help_test_discovery_broken(hass, mqtt_mock_entry, sensor.DOMAIN, data1, data2) async def test_entity_device_info_with_connection( diff --git a/tests/components/mqtt/test_siren.py b/tests/components/mqtt/test_siren.py index 28b88e2793d..c32c57d4f02 100644 --- a/tests/components/mqtt/test_siren.py +++ b/tests/components/mqtt/test_siren.py @@ -642,17 +642,11 @@ async def test_update_with_json_attrs_bad_json( async def test_discovery_update_attr( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, - mqtt_mock_entry, - caplog, - siren.DOMAIN, - DEFAULT_CONFIG, + hass, mqtt_mock_entry, siren.DOMAIN, DEFAULT_CONFIG ) @@ -687,9 +681,7 @@ async def test_unique_id( async def test_discovery_removal_siren( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test removal of discovered siren.""" data = ( @@ -697,13 +689,11 @@ async def test_discovery_removal_siren( ' "state_topic": "test_topic",' ' "command_topic": "test_topic" }' ) - await help_test_discovery_removal(hass, mqtt_mock_entry, caplog, siren.DOMAIN, data) + await help_test_discovery_removal(hass, mqtt_mock_entry, siren.DOMAIN, data) async def test_discovery_update_siren_topic_template( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered siren.""" config1 = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][siren.DOMAIN]) @@ -730,7 +720,6 @@ async def test_discovery_update_siren_topic_template( await help_test_discovery_update( hass, mqtt_mock_entry, - caplog, siren.DOMAIN, config1, config2, @@ -740,9 +729,7 @@ async def test_discovery_update_siren_topic_template( async def test_discovery_update_siren_template( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered siren.""" config1 = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][siren.DOMAIN]) @@ -767,7 +754,6 @@ async def test_discovery_update_siren_template( await help_test_discovery_update( hass, mqtt_mock_entry, - caplog, siren.DOMAIN, config1, config2, @@ -867,9 +853,7 @@ async def test_command_templates( async def test_discovery_update_unchanged_siren( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered siren.""" data1 = ( @@ -884,7 +868,6 @@ async def test_discovery_update_unchanged_siren( await help_test_discovery_update_unchanged( hass, mqtt_mock_entry, - caplog, siren.DOMAIN, data1, discovery_update, @@ -893,9 +876,7 @@ async def test_discovery_update_unchanged_siren( @pytest.mark.no_fail_on_log_exception async def test_discovery_broken( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test handling of bad discovery message.""" data1 = '{ "name": "Beer" }' @@ -904,9 +885,7 @@ async def test_discovery_broken( ' "state_topic": "test_topic",' ' "command_topic": "test_topic" }' ) - await help_test_discovery_broken( - hass, mqtt_mock_entry, caplog, siren.DOMAIN, data1, data2 - ) + await help_test_discovery_broken(hass, mqtt_mock_entry, siren.DOMAIN, data1, data2) async def test_entity_device_info_with_connection( diff --git a/tests/components/mqtt/test_switch.py b/tests/components/mqtt/test_switch.py index b497d4a2f52..42d2e092d83 100644 --- a/tests/components/mqtt/test_switch.py +++ b/tests/components/mqtt/test_switch.py @@ -403,17 +403,11 @@ async def test_update_with_json_attrs_bad_json( async def test_discovery_update_attr( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, - mqtt_mock_entry, - caplog, - switch.DOMAIN, - DEFAULT_CONFIG, + hass, mqtt_mock_entry, switch.DOMAIN, DEFAULT_CONFIG ) @@ -448,9 +442,7 @@ async def test_unique_id( async def test_discovery_removal_switch( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test removal of discovered switch.""" data = ( @@ -458,15 +450,11 @@ async def test_discovery_removal_switch( ' "state_topic": "test_topic",' ' "command_topic": "test_topic" }' ) - await help_test_discovery_removal( - hass, mqtt_mock_entry, caplog, switch.DOMAIN, data - ) + await help_test_discovery_removal(hass, mqtt_mock_entry, switch.DOMAIN, data) async def test_discovery_update_switch_topic_template( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered switch.""" config1 = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][switch.DOMAIN]) @@ -493,7 +481,6 @@ async def test_discovery_update_switch_topic_template( await help_test_discovery_update( hass, mqtt_mock_entry, - caplog, switch.DOMAIN, config1, config2, @@ -503,9 +490,7 @@ async def test_discovery_update_switch_topic_template( async def test_discovery_update_switch_template( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered switch.""" config1 = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][switch.DOMAIN]) @@ -530,7 +515,6 @@ async def test_discovery_update_switch_template( await help_test_discovery_update( hass, mqtt_mock_entry, - caplog, switch.DOMAIN, config1, config2, @@ -542,7 +526,6 @@ async def test_discovery_update_switch_template( async def test_discovery_update_unchanged_switch( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, ) -> None: """Test update of discovered switch.""" data1 = ( @@ -557,7 +540,6 @@ async def test_discovery_update_unchanged_switch( await help_test_discovery_update_unchanged( hass, mqtt_mock_entry, - caplog, switch.DOMAIN, data1, discovery_update, @@ -566,9 +548,7 @@ async def test_discovery_update_unchanged_switch( @pytest.mark.no_fail_on_log_exception async def test_discovery_broken( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test handling of bad discovery message.""" data1 = '{ "name": "Beer" }' @@ -577,9 +557,7 @@ async def test_discovery_broken( ' "state_topic": "test_topic",' ' "command_topic": "test_topic" }' ) - await help_test_discovery_broken( - hass, mqtt_mock_entry, caplog, switch.DOMAIN, data1, data2 - ) + await help_test_discovery_broken(hass, mqtt_mock_entry, switch.DOMAIN, data1, data2) async def test_entity_device_info_with_connection( diff --git a/tests/components/mqtt/test_text.py b/tests/components/mqtt/test_text.py index 2c58cae690d..fc714efa513 100644 --- a/tests/components/mqtt/test_text.py +++ b/tests/components/mqtt/test_text.py @@ -529,17 +529,11 @@ async def test_update_with_json_attrs_bad_json( async def test_discovery_update_attr( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, - mqtt_mock_entry, - caplog, - text.DOMAIN, - DEFAULT_CONFIG, + hass, mqtt_mock_entry, text.DOMAIN, DEFAULT_CONFIG ) @@ -574,9 +568,7 @@ async def test_unique_id( async def test_discovery_removal_text( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test removal of discovered text entity.""" data = ( @@ -584,13 +576,11 @@ async def test_discovery_removal_text( ' "state_topic": "test_topic",' ' "command_topic": "test_topic" }' ) - await help_test_discovery_removal(hass, mqtt_mock_entry, caplog, text.DOMAIN, data) + await help_test_discovery_removal(hass, mqtt_mock_entry, text.DOMAIN, data) async def test_discovery_text_update( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered text entity.""" config1 = { @@ -605,14 +595,12 @@ async def test_discovery_text_update( } await help_test_discovery_update( - hass, mqtt_mock_entry, caplog, text.DOMAIN, config1, config2 + hass, mqtt_mock_entry, text.DOMAIN, config1, config2 ) async def test_discovery_update_unchanged_update( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered update.""" data1 = '{ "name": "Beer", "state_topic": "text-topic", "command_topic": "command-topic"}' @@ -620,32 +608,23 @@ async def test_discovery_update_unchanged_update( "homeassistant.components.mqtt.text.MqttTextEntity.discovery_update" ) as discovery_update: await help_test_discovery_update_unchanged( - hass, - mqtt_mock_entry, - caplog, - text.DOMAIN, - data1, - discovery_update, + hass, mqtt_mock_entry, text.DOMAIN, data1, discovery_update ) async def test_discovery_update_text( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered text entity.""" config1 = {"name": "Beer", "command_topic": "cmd-topic1"} config2 = {"name": "Milk", "command_topic": "cmd-topic2"} await help_test_discovery_update( - hass, mqtt_mock_entry, caplog, text.DOMAIN, config1, config2 + hass, mqtt_mock_entry, text.DOMAIN, config1, config2 ) async def test_discovery_update_unchanged_climate( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered text entity.""" data1 = '{ "name": "Beer", "command_topic": "cmd-topic" }' @@ -653,20 +632,13 @@ async def test_discovery_update_unchanged_climate( "homeassistant.components.mqtt.text.MqttTextEntity.discovery_update" ) as discovery_update: await help_test_discovery_update_unchanged( - hass, - mqtt_mock_entry, - caplog, - text.DOMAIN, - data1, - discovery_update, + hass, mqtt_mock_entry, text.DOMAIN, data1, discovery_update ) @pytest.mark.no_fail_on_log_exception async def test_discovery_broken( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test handling of bad discovery message.""" data1 = '{ "name": "Beer" }' @@ -675,9 +647,7 @@ async def test_discovery_broken( ' "state_topic": "test_topic",' ' "command_topic": "test_topic" }' ) - await help_test_discovery_broken( - hass, mqtt_mock_entry, caplog, text.DOMAIN, data1, data2 - ) + await help_test_discovery_broken(hass, mqtt_mock_entry, text.DOMAIN, data1, data2) async def test_entity_device_info_with_connection( diff --git a/tests/components/mqtt/test_update.py b/tests/components/mqtt/test_update.py index bb80a0c274f..bb9ae12c66b 100644 --- a/tests/components/mqtt/test_update.py +++ b/tests/components/mqtt/test_update.py @@ -530,15 +530,10 @@ async def test_update_with_json_attrs_bad_json( async def test_discovery_update_attr( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, ) -> None: """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, - mqtt_mock_entry, - caplog, - update.DOMAIN, - DEFAULT_CONFIG, + hass, mqtt_mock_entry, update.DOMAIN, DEFAULT_CONFIG ) @@ -573,21 +568,15 @@ async def test_unique_id( async def test_discovery_removal_update( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test removal of discovered update.""" data = json.dumps(DEFAULT_CONFIG[mqtt.DOMAIN][update.DOMAIN]) - await help_test_discovery_removal( - hass, mqtt_mock_entry, caplog, update.DOMAIN, data - ) + await help_test_discovery_removal(hass, mqtt_mock_entry, update.DOMAIN, data) async def test_discovery_update_update( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered update.""" config1 = { @@ -602,14 +591,12 @@ async def test_discovery_update_update( } await help_test_discovery_update( - hass, mqtt_mock_entry, caplog, update.DOMAIN, config1, config2 + hass, mqtt_mock_entry, update.DOMAIN, config1, config2 ) async def test_discovery_update_unchanged_update( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered update.""" data1 = '{ "name": "Beer", "state_topic": "installed-topic", "latest_version_topic": "latest-topic"}' @@ -617,28 +604,19 @@ async def test_discovery_update_unchanged_update( "homeassistant.components.mqtt.update.MqttUpdate.discovery_update" ) as discovery_update: await help_test_discovery_update_unchanged( - hass, - mqtt_mock_entry, - caplog, - update.DOMAIN, - data1, - discovery_update, + hass, mqtt_mock_entry, update.DOMAIN, data1, discovery_update ) @pytest.mark.no_fail_on_log_exception async def test_discovery_broken( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test handling of bad discovery message.""" data1 = '{ "name": "Beer" }' data2 = '{ "name": "Milk", "state_topic": "installed-topic", "latest_version_topic": "latest-topic" }' - await help_test_discovery_broken( - hass, mqtt_mock_entry, caplog, update.DOMAIN, data1, data2 - ) + await help_test_discovery_broken(hass, mqtt_mock_entry, update.DOMAIN, data1, data2) async def test_entity_device_info_with_connection( diff --git a/tests/components/mqtt/test_vacuum.py b/tests/components/mqtt/test_vacuum.py index 0a06759c7e6..8c01138ccb9 100644 --- a/tests/components/mqtt/test_vacuum.py +++ b/tests/components/mqtt/test_vacuum.py @@ -531,17 +531,11 @@ async def test_update_with_json_attrs_bad_json( async def test_discovery_update_attr( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, - mqtt_mock_entry, - caplog, - vacuum.DOMAIN, - DEFAULT_CONFIG_2, + hass, mqtt_mock_entry, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) @@ -574,34 +568,27 @@ async def test_unique_id( async def test_discovery_removal_vacuum( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test removal of discovered vacuum.""" data = '{"name": "test", "command_topic": "test_topic"}' - await help_test_discovery_removal( - hass, mqtt_mock_entry, caplog, vacuum.DOMAIN, data - ) + await help_test_discovery_removal(hass, mqtt_mock_entry, vacuum.DOMAIN, data) async def test_discovery_update_vacuum( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered vacuum.""" config1 = {"name": "Beer", "command_topic": "test_topic"} config2 = {"name": "Milk", "command_topic": "test_topic"} await help_test_discovery_update( - hass, mqtt_mock_entry, caplog, vacuum.DOMAIN, config1, config2 + hass, mqtt_mock_entry, vacuum.DOMAIN, config1, config2 ) async def test_discovery_update_unchanged_vacuum( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, ) -> None: """Test update of discovered vacuum.""" data1 = '{"name": "Beer", "command_topic": "test_topic"}' @@ -609,27 +596,18 @@ async def test_discovery_update_unchanged_vacuum( "homeassistant.components.mqtt.vacuum.MqttStateVacuum.discovery_update" ) as discovery_update: await help_test_discovery_update_unchanged( - hass, - mqtt_mock_entry, - caplog, - vacuum.DOMAIN, - data1, - discovery_update, + hass, mqtt_mock_entry, vacuum.DOMAIN, data1, discovery_update ) @pytest.mark.no_fail_on_log_exception async def test_discovery_broken( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test handling of bad discovery message.""" data1 = '{"name": "Beer", "command_topic": "test_topic#"}' data2 = '{"name": "Milk", "command_topic": "test_topic"}' - await help_test_discovery_broken( - hass, mqtt_mock_entry, caplog, vacuum.DOMAIN, data1, data2 - ) + await help_test_discovery_broken(hass, mqtt_mock_entry, vacuum.DOMAIN, data1, data2) async def test_entity_device_info_with_connection( diff --git a/tests/components/mqtt/test_valve.py b/tests/components/mqtt/test_valve.py index 2efa30d096a..6f88e160b73 100644 --- a/tests/components/mqtt/test_valve.py +++ b/tests/components/mqtt/test_valve.py @@ -1200,15 +1200,10 @@ async def test_update_with_json_attrs_bad_json( async def test_discovery_update_attr( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, ) -> None: """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, - mqtt_mock_entry, - caplog, - valve.DOMAIN, - DEFAULT_CONFIG, + hass, mqtt_mock_entry, valve.DOMAIN, DEFAULT_CONFIG ) @@ -1241,32 +1236,26 @@ async def test_unique_id( async def test_discovery_removal_valve( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test removal of discovered valve.""" data = '{ "name": "test", "command_topic": "test_topic" }' - await help_test_discovery_removal(hass, mqtt_mock_entry, caplog, valve.DOMAIN, data) + await help_test_discovery_removal(hass, mqtt_mock_entry, valve.DOMAIN, data) async def test_discovery_update_valve( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered valve.""" config1 = {"name": "Beer", "command_topic": "test_topic"} config2 = {"name": "Milk", "command_topic": "test_topic"} await help_test_discovery_update( - hass, mqtt_mock_entry, caplog, valve.DOMAIN, config1, config2 + hass, mqtt_mock_entry, valve.DOMAIN, config1, config2 ) async def test_discovery_update_unchanged_valve( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered valve.""" data1 = '{ "name": "Beer", "command_topic": "test_topic" }' @@ -1274,27 +1263,18 @@ async def test_discovery_update_unchanged_valve( "homeassistant.components.mqtt.valve.MqttValve.discovery_update" ) as discovery_update: await help_test_discovery_update_unchanged( - hass, - mqtt_mock_entry, - caplog, - valve.DOMAIN, - data1, - discovery_update, + hass, mqtt_mock_entry, valve.DOMAIN, data1, discovery_update ) @pytest.mark.no_fail_on_log_exception async def test_discovery_broken( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test handling of bad discovery message.""" data1 = '{ "name": "Beer", "command_topic": "test_topic#" }' data2 = '{ "name": "Milk", "command_topic": "test_topic" }' - await help_test_discovery_broken( - hass, mqtt_mock_entry, caplog, valve.DOMAIN, data1, data2 - ) + await help_test_discovery_broken(hass, mqtt_mock_entry, valve.DOMAIN, data1, data2) async def test_entity_device_info_with_connection( diff --git a/tests/components/mqtt/test_water_heater.py b/tests/components/mqtt/test_water_heater.py index a80ab59657f..849a1ac8785 100644 --- a/tests/components/mqtt/test_water_heater.py +++ b/tests/components/mqtt/test_water_heater.py @@ -858,17 +858,11 @@ async def test_update_with_json_attrs_bad_json( async def test_discovery_update_attr( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, - mqtt_mock_entry, - caplog, - water_heater.DOMAIN, - DEFAULT_CONFIG, + hass, mqtt_mock_entry, water_heater.DOMAIN, DEFAULT_CONFIG ) @@ -933,34 +927,26 @@ async def test_encoding_subscribable_topics( async def test_discovery_removal_water_heater( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test removal of discovered water heater.""" data = json.dumps(DEFAULT_CONFIG[mqtt.DOMAIN][water_heater.DOMAIN]) - await help_test_discovery_removal( - hass, mqtt_mock_entry, caplog, water_heater.DOMAIN, data - ) + await help_test_discovery_removal(hass, mqtt_mock_entry, water_heater.DOMAIN, data) async def test_discovery_update_water_heater( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered water heater.""" config1 = {"name": "Beer"} config2 = {"name": "Milk"} await help_test_discovery_update( - hass, mqtt_mock_entry, caplog, water_heater.DOMAIN, config1, config2 + hass, mqtt_mock_entry, water_heater.DOMAIN, config1, config2 ) async def test_discovery_update_unchanged_water_heater( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered water heater.""" data1 = '{ "name": "Beer" }' @@ -968,26 +954,19 @@ async def test_discovery_update_unchanged_water_heater( "homeassistant.components.mqtt.water_heater.MqttWaterHeater.discovery_update" ) as discovery_update: await help_test_discovery_update_unchanged( - hass, - mqtt_mock_entry, - caplog, - water_heater.DOMAIN, - data1, - discovery_update, + hass, mqtt_mock_entry, water_heater.DOMAIN, data1, discovery_update ) @pytest.mark.no_fail_on_log_exception async def test_discovery_broken( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test handling of bad discovery message.""" data1 = '{ "name": "Beer", "mode_command_topic": "test_topic#" }' data2 = '{ "name": "Milk", "mode_command_topic": "test_topic" }' await help_test_discovery_broken( - hass, mqtt_mock_entry, caplog, water_heater.DOMAIN, data1, data2 + hass, mqtt_mock_entry, water_heater.DOMAIN, data1, data2 ) From 3559755aeda368ea17133b388ac366ab058e2a77 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 25 Jun 2024 19:16:19 +0200 Subject: [PATCH 1184/1445] Add import aliases for PLATFORM_SCHEMA (#120445) --- homeassistant/components/azure_service_bus/notify.py | 4 ++-- homeassistant/components/blackbird/media_player.py | 4 ++-- homeassistant/components/broadlink/switch.py | 4 ++-- homeassistant/components/citybikes/sensor.py | 4 ++-- homeassistant/components/history_stats/sensor.py | 4 ++-- homeassistant/components/integration/sensor.py | 4 ++-- homeassistant/components/rest/binary_sensor.py | 7 +++---- homeassistant/components/rest/sensor.py | 7 +++---- homeassistant/components/statistics/sensor.py | 4 ++-- homeassistant/components/template/light.py | 6 ++++-- homeassistant/components/template/sensor.py | 4 ++-- homeassistant/components/template/weather.py | 6 ++++-- homeassistant/components/tts/notify.py | 7 +++++-- 13 files changed, 35 insertions(+), 30 deletions(-) diff --git a/homeassistant/components/azure_service_bus/notify.py b/homeassistant/components/azure_service_bus/notify.py index 38c57b3db19..a0aa36804c3 100644 --- a/homeassistant/components/azure_service_bus/notify.py +++ b/homeassistant/components/azure_service_bus/notify.py @@ -18,7 +18,7 @@ from homeassistant.components.notify import ( ATTR_DATA, ATTR_TARGET, ATTR_TITLE, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as NOTIFY_PLATFORM_SCHEMA, BaseNotificationService, ) from homeassistant.const import CONTENT_TYPE_JSON @@ -36,7 +36,7 @@ ATTR_ASB_TARGET = "target" PLATFORM_SCHEMA = vol.All( cv.has_at_least_one_key(CONF_QUEUE_NAME, CONF_TOPIC_NAME), - PLATFORM_SCHEMA.extend( + NOTIFY_PLATFORM_SCHEMA.extend( { vol.Required(CONF_CONNECTION_STRING): cv.string, vol.Exclusive( diff --git a/homeassistant/components/blackbird/media_player.py b/homeassistant/components/blackbird/media_player.py index 4006b12738f..46cabaf4099 100644 --- a/homeassistant/components/blackbird/media_player.py +++ b/homeassistant/components/blackbird/media_player.py @@ -9,7 +9,7 @@ from serial import SerialException import voluptuous as vol from homeassistant.components.media_player import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as MEDIA_PLAYER_PLATFORM_SCHEMA, MediaPlayerEntity, MediaPlayerEntityFeature, MediaPlayerState, @@ -56,7 +56,7 @@ SOURCE_IDS = vol.All(vol.Coerce(int), vol.Range(min=1, max=8)) PLATFORM_SCHEMA = vol.All( cv.has_at_least_one_key(CONF_PORT, CONF_HOST), - PLATFORM_SCHEMA.extend( + MEDIA_PLAYER_PLATFORM_SCHEMA.extend( { vol.Exclusive(CONF_PORT, CONF_TYPE): cv.string, vol.Exclusive(CONF_HOST, CONF_TYPE): cv.string, diff --git a/homeassistant/components/broadlink/switch.py b/homeassistant/components/broadlink/switch.py index 9cf7e3391fa..cc3b9dad464 100644 --- a/homeassistant/components/broadlink/switch.py +++ b/homeassistant/components/broadlink/switch.py @@ -10,7 +10,7 @@ from broadlink.exceptions import BroadlinkException import voluptuous as vol from homeassistant.components.switch import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SWITCH_PLATFORM_SCHEMA, SwitchDeviceClass, SwitchEntity, ) @@ -56,7 +56,7 @@ PLATFORM_SCHEMA = vol.All( cv.deprecated(CONF_SLOTS), cv.deprecated(CONF_TIMEOUT), cv.deprecated(CONF_TYPE), - PLATFORM_SCHEMA.extend( + SWITCH_PLATFORM_SCHEMA.extend( { vol.Required(CONF_MAC): mac_address, vol.Optional(CONF_HOST): cv.string, diff --git a/homeassistant/components/citybikes/sensor.py b/homeassistant/components/citybikes/sensor.py index 4049a656caf..5e4da231eef 100644 --- a/homeassistant/components/citybikes/sensor.py +++ b/homeassistant/components/citybikes/sensor.py @@ -11,7 +11,7 @@ import voluptuous as vol from homeassistant.components.sensor import ( ENTITY_ID_FORMAT, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, SensorEntity, ) from homeassistant.const import ( @@ -73,7 +73,7 @@ CITYBIKES_NETWORKS = "citybikes_networks" PLATFORM_SCHEMA = vol.All( cv.has_at_least_one_key(CONF_RADIUS, CONF_STATIONS_LIST), - PLATFORM_SCHEMA.extend( + SENSOR_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_NAME, default=""): cv.string, vol.Optional(CONF_NETWORK): cv.string, diff --git a/homeassistant/components/history_stats/sensor.py b/homeassistant/components/history_stats/sensor.py index 0b02ddb2a8e..16279560d30 100644 --- a/homeassistant/components/history_stats/sensor.py +++ b/homeassistant/components/history_stats/sensor.py @@ -9,7 +9,7 @@ from typing import Any import voluptuous as vol from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, SensorStateClass, @@ -66,7 +66,7 @@ def exactly_two_period_keys[_T: dict[str, Any]](conf: _T) -> _T: PLATFORM_SCHEMA = vol.All( - PLATFORM_SCHEMA.extend( + SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_ENTITY_ID): cv.entity_id, vol.Required(CONF_STATE): vol.All(cv.ensure_list, [cv.string]), diff --git a/homeassistant/components/integration/sensor.py b/homeassistant/components/integration/sensor.py index ffb7a3d8e6a..106eb9cc79c 100644 --- a/homeassistant/components/integration/sensor.py +++ b/homeassistant/components/integration/sensor.py @@ -14,7 +14,7 @@ import voluptuous as vol from homeassistant.components.sensor import ( DEVICE_CLASS_UNITS, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, RestoreSensor, SensorDeviceClass, SensorExtraStoredData, @@ -81,7 +81,7 @@ DEFAULT_ROUND = 3 PLATFORM_SCHEMA = vol.All( cv.removed(CONF_UNIT_OF_MEASUREMENT), - PLATFORM_SCHEMA.extend( + SENSOR_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_UNIQUE_ID): cv.string, diff --git a/homeassistant/components/rest/binary_sensor.py b/homeassistant/components/rest/binary_sensor.py index 5aafd727178..e8119a40f8c 100644 --- a/homeassistant/components/rest/binary_sensor.py +++ b/homeassistant/components/rest/binary_sensor.py @@ -10,7 +10,7 @@ import voluptuous as vol from homeassistant.components.binary_sensor import ( DOMAIN as BINARY_SENSOR_DOMAIN, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as BINARY_SENSOR_PLATFORM_SCHEMA, BinarySensorEntity, ) from homeassistant.const import ( @@ -44,10 +44,9 @@ from .schema import BINARY_SENSOR_SCHEMA, RESOURCE_SCHEMA _LOGGER = logging.getLogger(__name__) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({**RESOURCE_SCHEMA, **BINARY_SENSOR_SCHEMA}) - PLATFORM_SCHEMA = vol.All( - cv.has_at_least_one_key(CONF_RESOURCE, CONF_RESOURCE_TEMPLATE), PLATFORM_SCHEMA + BINARY_SENSOR_PLATFORM_SCHEMA.extend({**RESOURCE_SCHEMA, **BINARY_SENSOR_SCHEMA}), + cv.has_at_least_one_key(CONF_RESOURCE, CONF_RESOURCE_TEMPLATE), ) TRIGGER_ENTITY_OPTIONS = ( diff --git a/homeassistant/components/rest/sensor.py b/homeassistant/components/rest/sensor.py index 810d286d147..d7bb0ea33fb 100644 --- a/homeassistant/components/rest/sensor.py +++ b/homeassistant/components/rest/sensor.py @@ -12,7 +12,7 @@ import voluptuous as vol from homeassistant.components.sensor import ( CONF_STATE_CLASS, DOMAIN as SENSOR_DOMAIN, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, SensorDeviceClass, ) from homeassistant.components.sensor.helpers import async_parse_date_datetime @@ -49,10 +49,9 @@ from .util import parse_json_attributes _LOGGER = logging.getLogger(__name__) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({**RESOURCE_SCHEMA, **SENSOR_SCHEMA}) - PLATFORM_SCHEMA = vol.All( - cv.has_at_least_one_key(CONF_RESOURCE, CONF_RESOURCE_TEMPLATE), PLATFORM_SCHEMA + SENSOR_PLATFORM_SCHEMA.extend({**RESOURCE_SCHEMA, **SENSOR_SCHEMA}), + cv.has_at_least_one_key(CONF_RESOURCE, CONF_RESOURCE_TEMPLATE), ) TRIGGER_ENTITY_OPTIONS = ( diff --git a/homeassistant/components/statistics/sensor.py b/homeassistant/components/statistics/sensor.py index fef10f7296f..eb4df4d98b2 100644 --- a/homeassistant/components/statistics/sensor.py +++ b/homeassistant/components/statistics/sensor.py @@ -17,7 +17,7 @@ from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAI from homeassistant.components.recorder import get_instance, history from homeassistant.components.sensor import ( DEVICE_CLASS_STATE_CLASSES, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, SensorStateClass, @@ -229,7 +229,7 @@ def valid_keep_last_sample(config: dict[str, Any]) -> dict[str, Any]: return config -_PLATFORM_SCHEMA_BASE = PLATFORM_SCHEMA.extend( +_PLATFORM_SCHEMA_BASE = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_ENTITY_ID): cv.entity_id, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/template/light.py b/homeassistant/components/template/light.py index 71443789703..de8a2998d34 100644 --- a/homeassistant/components/template/light.py +++ b/homeassistant/components/template/light.py @@ -34,7 +34,9 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import TemplateError import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.config_validation import PLATFORM_SCHEMA +from homeassistant.helpers.config_validation import ( + PLATFORM_SCHEMA as BASE_PLATFORM_SCHEMA, +) from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.script import Script @@ -115,7 +117,7 @@ PLATFORM_SCHEMA = vol.All( # CONF_WHITE_VALUE_* is deprecated, support will be removed in release 2022.9 cv.removed(CONF_WHITE_VALUE_ACTION), cv.removed(CONF_WHITE_VALUE_TEMPLATE), - PLATFORM_SCHEMA.extend( + BASE_PLATFORM_SCHEMA.extend( {vol.Required(CONF_LIGHTS): cv.schema_with_slug_keys(LIGHT_SCHEMA)} ), ) diff --git a/homeassistant/components/template/sensor.py b/homeassistant/components/template/sensor.py index 6cb73a15632..51669f11afe 100644 --- a/homeassistant/components/template/sensor.py +++ b/homeassistant/components/template/sensor.py @@ -14,7 +14,7 @@ from homeassistant.components.sensor import ( DEVICE_CLASSES_SCHEMA, DOMAIN as SENSOR_DOMAIN, ENTITY_ID_FORMAT, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, RestoreSensor, SensorDeviceClass, SensorEntity, @@ -152,7 +152,7 @@ def rewrite_legacy_to_modern_conf(cfg: dict[str, dict]) -> list[dict]: PLATFORM_SCHEMA = vol.All( - PLATFORM_SCHEMA.extend( + SENSOR_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_TRIGGER): cv.match_all, # to raise custom warning vol.Required(CONF_SENSORS): cv.schema_with_slug_keys(LEGACY_SENSOR_SCHEMA), diff --git a/homeassistant/components/template/weather.py b/homeassistant/components/template/weather.py index e8981fb33f9..0f80f65f501 100644 --- a/homeassistant/components/template/weather.py +++ b/homeassistant/components/template/weather.py @@ -41,7 +41,9 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import TemplateError from homeassistant.helpers import template import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.config_validation import PLATFORM_SCHEMA +from homeassistant.helpers.config_validation import ( + PLATFORM_SCHEMA as BASE_PLATFORM_SCHEMA, +) from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import ExtraStoredData, RestoreEntity @@ -136,7 +138,7 @@ WEATHER_SCHEMA = vol.Schema( PLATFORM_SCHEMA = vol.All( cv.deprecated(CONF_FORECAST_TEMPLATE), - PLATFORM_SCHEMA.extend(WEATHER_SCHEMA.schema), + BASE_PLATFORM_SCHEMA.extend(WEATHER_SCHEMA.schema), ) diff --git a/homeassistant/components/tts/notify.py b/homeassistant/components/tts/notify.py index e6963619043..429d46660e7 100644 --- a/homeassistant/components/tts/notify.py +++ b/homeassistant/components/tts/notify.py @@ -7,7 +7,10 @@ from typing import Any import voluptuous as vol -from homeassistant.components.notify import PLATFORM_SCHEMA, BaseNotificationService +from homeassistant.components.notify import ( + PLATFORM_SCHEMA as NOTIFY_PLATFORM_SCHEMA, + BaseNotificationService, +) from homeassistant.const import ATTR_ENTITY_ID, CONF_ENTITY_ID, CONF_NAME from homeassistant.core import HomeAssistant, split_entity_id import homeassistant.helpers.config_validation as cv @@ -23,7 +26,7 @@ _LOGGER = logging.getLogger(__name__) PLATFORM_SCHEMA = vol.All( cv.has_at_least_one_key(CONF_TTS_SERVICE, CONF_ENTITY_ID), - PLATFORM_SCHEMA.extend( + NOTIFY_PLATFORM_SCHEMA.extend( { vol.Required(CONF_NAME): cv.string, vol.Exclusive(CONF_TTS_SERVICE, ENTITY_LEGACY_PROVIDER_GROUP): cv.entity_id, From d4e93dd01dc076fcb3a54cb7228537ade02b532a Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 25 Jun 2024 19:17:54 +0200 Subject: [PATCH 1185/1445] Validate new device identifiers and connections (#120413) --- homeassistant/helpers/device_registry.py | 110 ++++++++++++++- tests/helpers/test_device_registry.py | 164 +++++++++++++++++++++++ 2 files changed, 271 insertions(+), 3 deletions(-) diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index 2a90d885d70..36249733f71 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -185,6 +185,35 @@ class DeviceInfoError(HomeAssistantError): self.domain = domain +class DeviceCollisionError(HomeAssistantError): + """Raised when a device collision is detected.""" + + +class DeviceIdentifierCollisionError(DeviceCollisionError): + """Raised when a device identifier collision is detected.""" + + def __init__( + self, identifiers: set[tuple[str, str]], existing_device: DeviceEntry + ) -> None: + """Initialize error.""" + super().__init__( + f"Identifiers {identifiers} already registered with {existing_device}" + ) + + +class DeviceConnectionCollisionError(DeviceCollisionError): + """Raised when a device connection collision is detected.""" + + def __init__( + self, normalized_connections: set[tuple[str, str]], existing_device: DeviceEntry + ) -> None: + """Initialize error.""" + super().__init__( + f"Connections {normalized_connections} " + f"already registered with {existing_device}" + ) + + def _validate_device_info( config_entry: ConfigEntry, device_info: DeviceInfo, @@ -759,6 +788,7 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]): device = self.async_update_device( device.id, + allow_collisions=True, add_config_entry_id=config_entry_id, configuration_url=configuration_url, device_info_type=device_info_type, @@ -782,11 +812,14 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]): return device @callback - def async_update_device( + def async_update_device( # noqa: C901 self, device_id: str, *, add_config_entry_id: str | UndefinedType = UNDEFINED, + # Temporary flag so we don't blow up when collisions are implicitly introduced + # by calls to async_get_or_create. Must not be set by integrations. + allow_collisions: bool = False, area_id: str | None | UndefinedType = UNDEFINED, configuration_url: str | URL | None | UndefinedType = UNDEFINED, device_info_type: str | UndefinedType = UNDEFINED, @@ -894,12 +927,36 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]): new_values[attr_name] = old_value | setvalue old_values[attr_name] = old_value + if merge_connections is not UNDEFINED: + normalized_connections = self._validate_connections( + device_id, + merge_connections, + allow_collisions, + ) + old_connections = old.connections + if not normalized_connections.issubset(old_connections): + new_values["connections"] = old_connections | normalized_connections + old_values["connections"] = old_connections + + if merge_identifiers is not UNDEFINED: + merge_identifiers = self._validate_identifiers( + device_id, merge_identifiers, allow_collisions + ) + old_identifiers = old.identifiers + if not merge_identifiers.issubset(old_identifiers): + new_values["identifiers"] = old_identifiers | merge_identifiers + old_values["identifiers"] = old_identifiers + if new_connections is not UNDEFINED: - new_values["connections"] = _normalize_connections(new_connections) + new_values["connections"] = self._validate_connections( + device_id, new_connections, False + ) old_values["connections"] = old.connections if new_identifiers is not UNDEFINED: - new_values["identifiers"] = new_identifiers + new_values["identifiers"] = self._validate_identifiers( + device_id, new_identifiers, False + ) old_values["identifiers"] = old.identifiers if configuration_url is not UNDEFINED: @@ -955,6 +1012,53 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]): return new + @callback + def _validate_connections( + self, + device_id: str, + connections: set[tuple[str, str]], + allow_collisions: bool, + ) -> set[tuple[str, str]]: + """Normalize and validate connections, raise on collision with other devices.""" + normalized_connections = _normalize_connections(connections) + if allow_collisions: + return normalized_connections + + for connection in normalized_connections: + # We need to iterate over each connection because if there is a + # conflict, the index will only see the last one and we will not + # be able to tell which one caused the conflict + if ( + existing_device := self.async_get_device(connections={connection}) + ) and existing_device.id != device_id: + raise DeviceConnectionCollisionError( + normalized_connections, existing_device + ) + + return normalized_connections + + @callback + def _validate_identifiers( + self, + device_id: str, + identifiers: set[tuple[str, str]], + allow_collisions: bool, + ) -> set[tuple[str, str]]: + """Validate identifiers, raise on collision with other devices.""" + if allow_collisions: + return identifiers + + for identifier in identifiers: + # We need to iterate over each identifier because if there is a + # conflict, the index will only see the last one and we will not + # be able to tell which one caused the conflict + if ( + existing_device := self.async_get_device(identifiers={identifier}) + ) and existing_device.id != device_id: + raise DeviceIdentifierCollisionError(identifiers, existing_device) + + return identifiers + @callback def async_remove_device(self, device_id: str) -> None: """Remove a device from the device registry.""" diff --git a/tests/helpers/test_device_registry.py b/tests/helpers/test_device_registry.py index b141e29f678..f8f10baad08 100644 --- a/tests/helpers/test_device_registry.py +++ b/tests/helpers/test_device_registry.py @@ -2630,3 +2630,167 @@ async def test_async_remove_device_thread_safety( await hass.async_add_executor_job( device_registry.async_remove_device, device.id ) + + +async def test_device_registry_connections_collision( + hass: HomeAssistant, device_registry: dr.DeviceRegistry +) -> None: + """Test connection collisions in the device registry.""" + config_entry = MockConfigEntry() + config_entry.add_to_hass(hass) + + device1 = device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "none")}, + manufacturer="manufacturer", + model="model", + ) + device2 = device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "none")}, + manufacturer="manufacturer", + model="model", + ) + + assert device1.id == device2.id + + device3 = device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + identifiers={("bridgeid", "0123")}, + manufacturer="manufacturer", + model="model", + ) + + # Attempt to merge connection for device3 with the same + # connection that already exists in device1 + with pytest.raises( + HomeAssistantError, match=f"Connections.*already registered.*{device1.id}" + ): + device_registry.async_update_device( + device3.id, + merge_connections={ + (dr.CONNECTION_NETWORK_MAC, "EE:EE:EE:EE:EE:EE"), + (dr.CONNECTION_NETWORK_MAC, "none"), + }, + ) + + # Attempt to add new connections for device3 with the same + # connection that already exists in device1 + with pytest.raises( + HomeAssistantError, match=f"Connections.*already registered.*{device1.id}" + ): + device_registry.async_update_device( + device3.id, + new_connections={ + (dr.CONNECTION_NETWORK_MAC, "EE:EE:EE:EE:EE:EE"), + (dr.CONNECTION_NETWORK_MAC, "none"), + }, + ) + + device3_refetched = device_registry.async_get(device3.id) + assert device3_refetched.connections == set() + assert device3_refetched.identifiers == {("bridgeid", "0123")} + + device1_refetched = device_registry.async_get(device1.id) + assert device1_refetched.connections == {(dr.CONNECTION_NETWORK_MAC, "none")} + assert device1_refetched.identifiers == set() + + device2_refetched = device_registry.async_get(device2.id) + assert device2_refetched.connections == {(dr.CONNECTION_NETWORK_MAC, "none")} + assert device2_refetched.identifiers == set() + + assert device2_refetched.id == device1_refetched.id + assert len(device_registry.devices) == 2 + + # Attempt to implicitly merge connection for device3 with the same + # connection that already exists in device1 + device4 = device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + identifiers={("bridgeid", "0123")}, + connections={ + (dr.CONNECTION_NETWORK_MAC, "EE:EE:EE:EE:EE:EE"), + (dr.CONNECTION_NETWORK_MAC, "none"), + }, + ) + assert len(device_registry.devices) == 2 + assert device4.id in (device1.id, device3.id) + + device3_refetched = device_registry.async_get(device3.id) + device1_refetched = device_registry.async_get(device1.id) + assert not device1_refetched.connections.isdisjoint(device3_refetched.connections) + + +async def test_device_registry_identifiers_collision( + hass: HomeAssistant, device_registry: dr.DeviceRegistry +) -> None: + """Test identifiers collisions in the device registry.""" + config_entry = MockConfigEntry() + config_entry.add_to_hass(hass) + + device1 = device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + identifiers={("bridgeid", "0123")}, + manufacturer="manufacturer", + model="model", + ) + device2 = device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + identifiers={("bridgeid", "0123")}, + manufacturer="manufacturer", + model="model", + ) + + assert device1.id == device2.id + + device3 = device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + identifiers={("bridgeid", "4567")}, + manufacturer="manufacturer", + model="model", + ) + + # Attempt to merge identifiers for device3 with the same + # connection that already exists in device1 + with pytest.raises( + HomeAssistantError, match=f"Identifiers.*already registered.*{device1.id}" + ): + device_registry.async_update_device( + device3.id, merge_identifiers={("bridgeid", "0123"), ("bridgeid", "8888")} + ) + + # Attempt to add new identifiers for device3 with the same + # connection that already exists in device1 + with pytest.raises( + HomeAssistantError, match=f"Identifiers.*already registered.*{device1.id}" + ): + device_registry.async_update_device( + device3.id, new_identifiers={("bridgeid", "0123"), ("bridgeid", "8888")} + ) + + device3_refetched = device_registry.async_get(device3.id) + assert device3_refetched.connections == set() + assert device3_refetched.identifiers == {("bridgeid", "4567")} + + device1_refetched = device_registry.async_get(device1.id) + assert device1_refetched.connections == set() + assert device1_refetched.identifiers == {("bridgeid", "0123")} + + device2_refetched = device_registry.async_get(device2.id) + assert device2_refetched.connections == set() + assert device2_refetched.identifiers == {("bridgeid", "0123")} + + assert device2_refetched.id == device1_refetched.id + assert len(device_registry.devices) == 2 + + # Attempt to implicitly merge identifiers for device3 with the same + # connection that already exists in device1 + device4 = device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + identifiers={("bridgeid", "4567"), ("bridgeid", "0123")}, + ) + assert len(device_registry.devices) == 2 + assert device4.id in (device1.id, device3.id) + + device3_refetched = device_registry.async_get(device3.id) + device1_refetched = device_registry.async_get(device1.id) + assert not device1_refetched.identifiers.isdisjoint(device3_refetched.identifiers) From c4b277b6ab6a3b9ca0bed68316ff18a1dc00a9a2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 25 Jun 2024 19:40:04 +0200 Subject: [PATCH 1186/1445] Small cleanups to ESPHome manager reconnect shutdown (#120401) --- homeassistant/components/esphome/manager.py | 33 ++++++++++----------- 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/esphome/manager.py b/homeassistant/components/esphome/manager.py index 5ab0265c1d4..e8d002fba9d 100644 --- a/homeassistant/components/esphome/manager.py +++ b/homeassistant/components/esphome/manager.py @@ -586,23 +586,6 @@ class ESPHomeManager: if entry.options.get(CONF_ALLOW_SERVICE_CALLS, DEFAULT_ALLOW_SERVICE_CALLS): async_delete_issue(hass, DOMAIN, self.services_issue) - # Use async_listen instead of async_listen_once so that we don't deregister - # the callback twice when shutting down Home Assistant. - # "Unable to remove unknown listener - # .onetime_listener>" - # We only close the connection at the last possible moment - # when the CLOSE event is fired so anything using a Bluetooth - # proxy has a chance to shut down properly. - entry_data.cleanup_callbacks.append( - hass.bus.async_listen(EVENT_HOMEASSISTANT_CLOSE, self.on_stop) - ) - entry_data.cleanup_callbacks.append( - hass.bus.async_listen( - EVENT_LOGGING_CHANGED, - self._async_handle_logging_changed, - ) - ) - reconnect_logic = ReconnectLogic( client=self.cli, on_connect=self.on_connect, @@ -613,6 +596,21 @@ class ESPHomeManager: ) self.reconnect_logic = reconnect_logic + # Use async_listen instead of async_listen_once so that we don't deregister + # the callback twice when shutting down Home Assistant. + # "Unable to remove unknown listener + # .onetime_listener>" + # We only close the connection at the last possible moment + # when the CLOSE event is fired so anything using a Bluetooth + # proxy has a chance to shut down properly. + bus = hass.bus + cleanups = ( + bus.async_listen(EVENT_HOMEASSISTANT_CLOSE, self.on_stop), + bus.async_listen(EVENT_LOGGING_CHANGED, self._async_handle_logging_changed), + reconnect_logic.stop_callback, + ) + entry_data.cleanup_callbacks.extend(cleanups) + infos, services = await entry_data.async_load_from_store() if entry.unique_id: await entry_data.async_update_static_infos( @@ -628,7 +626,6 @@ class ESPHomeManager: ) await reconnect_logic.start() - entry_data.cleanup_callbacks.append(reconnect_logic.stop_callback) entry.async_on_unload( entry.add_update_listener(entry_data.async_update_listener) From 75c7ae7c699ab1c53d6da4d496005d0580ffedcf Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 25 Jun 2024 20:00:48 +0200 Subject: [PATCH 1187/1445] Support in service descriptions for input sections (#116100) --- homeassistant/components/light/services.yaml | 180 +++++++++---------- homeassistant/components/light/strings.json | 18 +- homeassistant/helpers/service.py | 11 +- script/hassfest/services.py | 55 +++++- script/hassfest/translations.py | 7 + tests/helpers/test_service.py | 22 +++ 6 files changed, 198 insertions(+), 95 deletions(-) diff --git a/homeassistant/components/light/services.yaml b/homeassistant/components/light/services.yaml index 6183d2a49df..2a1fbd11afd 100644 --- a/homeassistant/components/light/services.yaml +++ b/homeassistant/components/light/services.yaml @@ -199,45 +199,6 @@ turn_on: example: "[255, 100, 100]" selector: color_rgb: - rgbw_color: &rgbw_color - filter: *color_support - advanced: true - example: "[255, 100, 100, 50]" - selector: - object: - rgbww_color: &rgbww_color - filter: *color_support - advanced: true - example: "[255, 100, 100, 50, 70]" - selector: - object: - color_name: &color_name - filter: *color_support - advanced: true - selector: - select: - translation_key: color_name - options: *named_colors - hs_color: &hs_color - filter: *color_support - advanced: true - example: "[300, 70]" - selector: - object: - xy_color: &xy_color - filter: *color_support - advanced: true - example: "[0.52, 0.43]" - selector: - object: - color_temp: &color_temp - filter: *color_temp_support - advanced: true - selector: - color_temp: - unit: "mired" - min: 153 - max: 500 kelvin: &kelvin filter: *color_temp_support selector: @@ -245,13 +206,6 @@ turn_on: unit: "kelvin" min: 2000 max: 6500 - brightness: &brightness - filter: *brightness_support - advanced: true - selector: - number: - min: 0 - max: 255 brightness_pct: &brightness_pct filter: *brightness_support selector: @@ -259,13 +213,6 @@ turn_on: min: 0 max: 100 unit_of_measurement: "%" - brightness_step: - filter: *brightness_support - advanced: true - selector: - number: - min: -225 - max: 255 brightness_step_pct: filter: *brightness_support selector: @@ -273,39 +220,84 @@ turn_on: min: -100 max: 100 unit_of_measurement: "%" - white: &white - filter: - attribute: - supported_color_modes: - - light.ColorMode.WHITE - advanced: true - selector: - constant: - value: true - label: Enabled - profile: &profile - advanced: true - example: relax - selector: - text: - flash: &flash - filter: - supported_features: - - light.LightEntityFeature.FLASH - advanced: true - selector: - select: - options: - - label: "Long" - value: "long" - - label: "Short" - value: "short" effect: &effect filter: supported_features: - light.LightEntityFeature.EFFECT selector: text: + advanced_fields: + collapsed: true + fields: + rgbw_color: &rgbw_color + filter: *color_support + example: "[255, 100, 100, 50]" + selector: + object: + rgbww_color: &rgbww_color + filter: *color_support + example: "[255, 100, 100, 50, 70]" + selector: + object: + color_name: &color_name + filter: *color_support + selector: + select: + translation_key: color_name + options: *named_colors + hs_color: &hs_color + filter: *color_support + example: "[300, 70]" + selector: + object: + xy_color: &xy_color + filter: *color_support + example: "[0.52, 0.43]" + selector: + object: + color_temp: &color_temp + filter: *color_temp_support + selector: + color_temp: + unit: "mired" + min: 153 + max: 500 + brightness: &brightness + filter: *brightness_support + selector: + number: + min: 0 + max: 255 + brightness_step: + filter: *brightness_support + selector: + number: + min: -225 + max: 255 + white: &white + filter: + attribute: + supported_color_modes: + - light.ColorMode.WHITE + selector: + constant: + value: true + label: Enabled + profile: &profile + example: relax + selector: + text: + flash: &flash + filter: + supported_features: + - light.LightEntityFeature.FLASH + selector: + select: + options: + - label: "Long" + value: "long" + - label: "Short" + value: "short" turn_off: target: @@ -313,7 +305,10 @@ turn_off: domain: light fields: transition: *transition - flash: *flash + advanced_fields: + collapsed: true + fields: + flash: *flash toggle: target: @@ -322,16 +317,19 @@ toggle: fields: transition: *transition rgb_color: *rgb_color - rgbw_color: *rgbw_color - rgbww_color: *rgbww_color - color_name: *color_name - hs_color: *hs_color - xy_color: *xy_color - color_temp: *color_temp kelvin: *kelvin - brightness: *brightness brightness_pct: *brightness_pct - white: *white - profile: *profile - flash: *flash effect: *effect + advanced_fields: + collapsed: true + fields: + rgbw_color: *rgbw_color + rgbww_color: *rgbww_color + color_name: *color_name + hs_color: *hs_color + xy_color: *xy_color + color_temp: *color_temp + brightness: *brightness + white: *white + profile: *profile + flash: *flash diff --git a/homeassistant/components/light/strings.json b/homeassistant/components/light/strings.json index 76156404991..b874e48406e 100644 --- a/homeassistant/components/light/strings.json +++ b/homeassistant/components/light/strings.json @@ -34,7 +34,8 @@ "field_white_description": "Set the light to white mode.", "field_white_name": "White", "field_xy_color_description": "Color in XY-format. A list of two decimal numbers between 0 and 1.", - "field_xy_color_name": "XY-color" + "field_xy_color_name": "XY-color", + "section_advanced_fields_name": "Advanced options" }, "device_automation": { "action_type": { @@ -354,6 +355,11 @@ "name": "[%key:component::light::common::field_effect_name%]", "description": "[%key:component::light::common::field_effect_description%]" } + }, + "sections": { + "advanced_fields": { + "name": "[%key:component::light::common::section_advanced_fields_name%]" + } } }, "turn_off": { @@ -368,6 +374,11 @@ "name": "[%key:component::light::common::field_flash_name%]", "description": "[%key:component::light::common::field_flash_description%]" } + }, + "sections": { + "advanced_fields": { + "name": "[%key:component::light::common::section_advanced_fields_name%]" + } } }, "toggle": { @@ -434,6 +445,11 @@ "name": "[%key:component::light::common::field_effect_name%]", "description": "[%key:component::light::common::field_effect_description%]" } + }, + "sections": { + "advanced_fields": { + "name": "[%key:component::light::common::section_advanced_fields_name%]" + } } } } diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index 22f5e7f8710..35c682437cb 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -179,10 +179,19 @@ _FIELD_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) +_SECTION_SCHEMA = vol.Schema( + { + vol.Required("fields"): vol.Schema({str: _FIELD_SCHEMA}), + }, + extra=vol.ALLOW_EXTRA, +) + _SERVICE_SCHEMA = vol.Schema( { vol.Optional("target"): vol.Any(TargetSelector.CONFIG_SCHEMA, None), - vol.Optional("fields"): vol.Schema({str: _FIELD_SCHEMA}), + vol.Optional("fields"): vol.Schema( + {str: vol.Any(_SECTION_SCHEMA, _FIELD_SCHEMA)} + ), }, extra=vol.ALLOW_EXTRA, ) diff --git a/script/hassfest/services.py b/script/hassfest/services.py index ea4503d5410..92fca14d373 100644 --- a/script/hassfest/services.py +++ b/script/hassfest/services.py @@ -26,6 +26,23 @@ def exists(value: Any) -> Any: return value +def unique_field_validator(fields: Any) -> Any: + """Validate the inputs don't have duplicate keys under different sections.""" + all_fields = set() + for key, value in fields.items(): + if value and "fields" in value: + for key in value["fields"]: + if key in all_fields: + raise vol.Invalid(f"Duplicate use of field {key} in service.") + all_fields.add(key) + else: + if key in all_fields: + raise vol.Invalid(f"Duplicate use of field {key} in service.") + all_fields.add(key) + + return fields + + CORE_INTEGRATION_FIELD_SCHEMA = vol.Schema( { vol.Optional("example"): exists, @@ -44,6 +61,13 @@ CORE_INTEGRATION_FIELD_SCHEMA = vol.Schema( } ) +CORE_INTEGRATION_SECTION_SCHEMA = vol.Schema( + { + vol.Optional("collapsed"): bool, + vol.Required("fields"): vol.Schema({str: CORE_INTEGRATION_FIELD_SCHEMA}), + } +) + CUSTOM_INTEGRATION_FIELD_SCHEMA = CORE_INTEGRATION_FIELD_SCHEMA.extend( { vol.Optional("description"): str, @@ -57,7 +81,17 @@ CORE_INTEGRATION_SERVICE_SCHEMA = vol.Any( vol.Optional("target"): vol.Any( selector.TargetSelector.CONFIG_SCHEMA, None ), - vol.Optional("fields"): vol.Schema({str: CORE_INTEGRATION_FIELD_SCHEMA}), + vol.Optional("fields"): vol.All( + vol.Schema( + { + str: vol.Any( + CORE_INTEGRATION_FIELD_SCHEMA, + CORE_INTEGRATION_SECTION_SCHEMA, + ) + } + ), + unique_field_validator, + ), } ), None, @@ -107,7 +141,7 @@ def grep_dir(path: pathlib.Path, glob_pattern: str, search_pattern: str) -> bool return False -def validate_services(config: Config, integration: Integration) -> None: +def validate_services(config: Config, integration: Integration) -> None: # noqa: C901 """Validate services.""" try: data = load_yaml_dict(str(integration.path / "services.yaml")) @@ -200,6 +234,9 @@ def validate_services(config: Config, integration: Integration) -> None: # The same check is done for the description in each of the fields of the # service schema. for field_name, field_schema in service_schema.get("fields", {}).items(): + if "fields" in field_schema: + # This is a section + continue if "name" not in field_schema: try: strings["services"][service_name]["fields"][field_name]["name"] @@ -233,6 +270,20 @@ def validate_services(config: Config, integration: Integration) -> None: f"Service {service_name} has a field {field_name} with a selector with a translation key {translation_key} that is not in the translations file", ) + # The same check is done for the description in each of the sections of the + # service schema. + for section_name, section_schema in service_schema.get("fields", {}).items(): + if "fields" not in section_schema: + # This is not a section + continue + try: + strings["services"][service_name]["sections"][section_name]["name"] + except KeyError: + integration.add_error( + "services", + f"Service {service_name} has a section {section_name} with no name {error_msg_suffix}", + ) + def validate(integrations: dict[str, Integration], config: Config) -> None: """Handle dependencies for integrations.""" diff --git a/script/hassfest/translations.py b/script/hassfest/translations.py index 965d1dc62b8..c39c070eba2 100644 --- a/script/hassfest/translations.py +++ b/script/hassfest/translations.py @@ -383,6 +383,13 @@ def gen_strings_schema(config: Config, integration: Integration) -> vol.Schema: }, slug_validator=translation_key_validator, ), + vol.Optional("sections"): cv.schema_with_slug_keys( + { + vol.Required("name"): str, + vol.Optional("description"): translation_value_validator, + }, + slug_validator=translation_key_validator, + ), }, slug_validator=translation_key_validator, ), diff --git a/tests/helpers/test_service.py b/tests/helpers/test_service.py index 3e7d8e6ef03..9c5cda67725 100644 --- a/tests/helpers/test_service.py +++ b/tests/helpers/test_service.py @@ -990,6 +990,17 @@ async def test_async_get_all_descriptions_filter(hass: HomeAssistant) -> None: - light.ColorMode.COLOR_TEMP selector: number: + advanced_stuff: + fields: + temperature: + filter: + supported_features: + - alarm_control_panel.AlarmControlPanelEntityFeature.ARM_HOME + attribute: + supported_color_modes: + - light.ColorMode.COLOR_TEMP + selector: + number: """ domain = "test_domain" @@ -1024,6 +1035,17 @@ async def test_async_get_all_descriptions_filter(hass: HomeAssistant) -> None: test_service_schema = { "description": "", "fields": { + "advanced_stuff": { + "fields": { + "temperature": { + "filter": { + "attribute": {"supported_color_modes": ["color_temp"]}, + "supported_features": [1], + }, + "selector": {"number": None}, + }, + }, + }, "temperature": { "filter": { "attribute": {"supported_color_modes": ["color_temp"]}, From 9dc26652ee3b2d5b26d609bd152568be29e5bb03 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 25 Jun 2024 20:08:04 +0200 Subject: [PATCH 1188/1445] Fix gtfs typing (#120451) --- homeassistant/components/gtfs/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/gtfs/sensor.py b/homeassistant/components/gtfs/sensor.py index a0a0f0ebc0e..fbc65050704 100644 --- a/homeassistant/components/gtfs/sensor.py +++ b/homeassistant/components/gtfs/sensor.py @@ -270,7 +270,7 @@ def get_next_departure( schedule: Any, start_station_id: Any, end_station_id: Any, - offset: cv.time_period, + offset: datetime.timedelta, include_tomorrow: bool = False, ) -> dict: """Get the next departure for the given schedule.""" @@ -405,7 +405,7 @@ def get_next_departure( item = {} for key in sorted(timetable.keys()): - if dt_util.parse_datetime(key) > now: + if (value := dt_util.parse_datetime(key)) is not None and value > now: item = timetable[key] _LOGGER.debug( "Departure found for station %s @ %s -> %s", start_station_id, key, item From 7d2ae5b3a5a0be2076aac39d543fb7667e52d123 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 25 Jun 2024 20:15:11 +0200 Subject: [PATCH 1189/1445] Add WS command blueprint/substitute (#119890) --- .../components/blueprint/websocket_api.py | 107 ++++++++++++---- .../blueprint/test_websocket_api.py | 119 +++++++++++++++++- 2 files changed, 200 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/blueprint/websocket_api.py b/homeassistant/components/blueprint/websocket_api.py index 98cc8131166..9d3329d8195 100644 --- a/homeassistant/components/blueprint/websocket_api.py +++ b/homeassistant/components/blueprint/websocket_api.py @@ -3,6 +3,8 @@ from __future__ import annotations import asyncio +from collections.abc import Callable, Coroutine +import functools from typing import Any, cast import voluptuous as vol @@ -15,16 +17,50 @@ from homeassistant.util import yaml from . import importer, models from .const import DOMAIN -from .errors import FailedToLoad, FileAlreadyExists +from .errors import BlueprintException, FailedToLoad, FileAlreadyExists @callback def async_setup(hass: HomeAssistant) -> None: """Set up the websocket API.""" - websocket_api.async_register_command(hass, ws_list_blueprints) - websocket_api.async_register_command(hass, ws_import_blueprint) - websocket_api.async_register_command(hass, ws_save_blueprint) websocket_api.async_register_command(hass, ws_delete_blueprint) + websocket_api.async_register_command(hass, ws_import_blueprint) + websocket_api.async_register_command(hass, ws_list_blueprints) + websocket_api.async_register_command(hass, ws_save_blueprint) + websocket_api.async_register_command(hass, ws_substitute_blueprint) + + +def _ws_with_blueprint_domain( + func: Callable[ + [ + HomeAssistant, + websocket_api.ActiveConnection, + dict[str, Any], + models.DomainBlueprints, + ], + Coroutine[Any, Any, None], + ], +) -> websocket_api.AsyncWebSocketCommandHandler: + """Decorate a function to pass in the domain blueprints.""" + + @functools.wraps(func) + async def with_domain_blueprints( + hass: HomeAssistant, + connection: websocket_api.ActiveConnection, + msg: dict[str, Any], + ) -> None: + domain_blueprints: models.DomainBlueprints | None = hass.data.get( + DOMAIN, {} + ).get(msg["domain"]) + if domain_blueprints is None: + connection.send_error( + msg["id"], websocket_api.ERR_INVALID_FORMAT, "Unsupported domain" + ) + return + + await func(hass, connection, msg, domain_blueprints) + + return with_domain_blueprints @websocket_api.websocket_command( @@ -124,23 +160,18 @@ async def ws_import_blueprint( } ) @websocket_api.async_response +@_ws_with_blueprint_domain async def ws_save_blueprint( hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any], + domain_blueprints: models.DomainBlueprints, ) -> None: """Save a blueprint.""" path = msg["path"] domain = msg["domain"] - domain_blueprints: dict[str, models.DomainBlueprints] = hass.data.get(DOMAIN, {}) - - if domain not in domain_blueprints: - connection.send_error( - msg["id"], websocket_api.ERR_INVALID_FORMAT, "Unsupported domain" - ) - try: yaml_data = cast(dict[str, Any], yaml.parse_yaml(msg["yaml"])) blueprint = models.Blueprint(yaml_data, expected_domain=domain) @@ -154,7 +185,7 @@ async def ws_save_blueprint( path = f"{path}.yaml" try: - overrides_existing = await domain_blueprints[domain].async_add_blueprint( + overrides_existing = await domain_blueprints.async_add_blueprint( blueprint, path, allow_override=msg.get("allow_override", False) ) except FileAlreadyExists: @@ -180,25 +211,16 @@ async def ws_save_blueprint( } ) @websocket_api.async_response +@_ws_with_blueprint_domain async def ws_delete_blueprint( hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any], + domain_blueprints: models.DomainBlueprints, ) -> None: """Delete a blueprint.""" - - path = msg["path"] - domain = msg["domain"] - - domain_blueprints: dict[str, models.DomainBlueprints] = hass.data.get(DOMAIN, {}) - - if domain not in domain_blueprints: - connection.send_error( - msg["id"], websocket_api.ERR_INVALID_FORMAT, "Unsupported domain" - ) - try: - await domain_blueprints[domain].async_remove_blueprint(path) + await domain_blueprints.async_remove_blueprint(msg["path"]) except OSError as err: connection.send_error(msg["id"], websocket_api.ERR_UNKNOWN_ERROR, str(err)) return @@ -206,3 +228,40 @@ async def ws_delete_blueprint( connection.send_result( msg["id"], ) + + +@websocket_api.websocket_command( + { + vol.Required("type"): "blueprint/substitute", + vol.Required("domain"): cv.string, + vol.Required("path"): cv.path, + vol.Required("input"): dict, + } +) +@websocket_api.async_response +@_ws_with_blueprint_domain +async def ws_substitute_blueprint( + hass: HomeAssistant, + connection: websocket_api.ActiveConnection, + msg: dict[str, Any], + domain_blueprints: models.DomainBlueprints, +) -> None: + """Process a blueprinted config to allow editing.""" + + blueprint_config = {"use_blueprint": {"path": msg["path"], "input": msg["input"]}} + + try: + blueprint_inputs = await domain_blueprints.async_inputs_from_config( + blueprint_config + ) + except BlueprintException as err: + connection.send_error(msg["id"], websocket_api.ERR_UNKNOWN_ERROR, str(err)) + return + + try: + config = blueprint_inputs.async_substitute() + except yaml.UndefinedSubstitution as err: + connection.send_error(msg["id"], websocket_api.ERR_UNKNOWN_ERROR, str(err)) + return + + connection.send_result(msg["id"], {"substituted_config": config}) diff --git a/tests/components/blueprint/test_websocket_api.py b/tests/components/blueprint/test_websocket_api.py index 1f684b451ed..13615803569 100644 --- a/tests/components/blueprint/test_websocket_api.py +++ b/tests/components/blueprint/test_websocket_api.py @@ -9,7 +9,7 @@ import yaml from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component -from homeassistant.util.yaml import parse_yaml +from homeassistant.util.yaml import UndefinedSubstitution, parse_yaml from tests.test_util.aiohttp import AiohttpClientMocker from tests.typing import WebSocketGenerator @@ -454,9 +454,124 @@ async def test_delete_blueprint_in_use_by_script( msg = await client.receive_json() assert not unlink_mock.mock_calls - assert msg["id"] == 9 assert not msg["success"] assert msg["error"] == { "code": "home_assistant_error", "message": "Blueprint in use", } + + +async def test_substituting_blueprint_inputs( + hass: HomeAssistant, hass_ws_client: WebSocketGenerator +) -> None: + """Test substituting blueprint inputs.""" + client = await hass_ws_client(hass) + await client.send_json_auto_id( + { + "type": "blueprint/substitute", + "domain": "automation", + "path": "test_event_service.yaml", + "input": { + "trigger_event": "test_event", + "service_to_call": "test.automation", + "a_number": 5, + }, + } + ) + + msg = await client.receive_json() + + assert msg["success"] + assert msg["result"]["substituted_config"] == { + "action": { + "entity_id": "light.kitchen", + "service": "test.automation", + }, + "trigger": { + "event_type": "test_event", + "platform": "event", + }, + } + + +async def test_substituting_blueprint_inputs_unknown_domain( + hass: HomeAssistant, hass_ws_client: WebSocketGenerator +) -> None: + """Test substituting blueprint inputs.""" + client = await hass_ws_client(hass) + await client.send_json_auto_id( + { + "type": "blueprint/substitute", + "domain": "donald_duck", + "path": "test_event_service.yaml", + "input": { + "trigger_event": "test_event", + "service_to_call": "test.automation", + "a_number": 5, + }, + } + ) + + msg = await client.receive_json() + + assert not msg["success"] + assert msg["error"] == { + "code": "invalid_format", + "message": "Unsupported domain", + } + + +async def test_substituting_blueprint_inputs_incomplete_input( + hass: HomeAssistant, hass_ws_client: WebSocketGenerator +) -> None: + """Test substituting blueprint inputs.""" + client = await hass_ws_client(hass) + await client.send_json_auto_id( + { + "type": "blueprint/substitute", + "domain": "automation", + "path": "test_event_service.yaml", + "input": { + "service_to_call": "test.automation", + "a_number": 5, + }, + } + ) + + msg = await client.receive_json() + + assert not msg["success"] + assert msg["error"] == { + "code": "unknown_error", + "message": "Missing input trigger_event", + } + + +async def test_substituting_blueprint_inputs_incomplete_input_2( + hass: HomeAssistant, hass_ws_client: WebSocketGenerator +) -> None: + """Test substituting blueprint inputs.""" + client = await hass_ws_client(hass) + with patch( + "homeassistant.components.blueprint.models.BlueprintInputs.async_substitute", + side_effect=UndefinedSubstitution("blah"), + ): + await client.send_json_auto_id( + { + "type": "blueprint/substitute", + "domain": "automation", + "path": "test_event_service.yaml", + "input": { + "trigger_event": "test_event", + "service_to_call": "test.automation", + "a_number": 5, + }, + } + ) + msg = await client.receive_json() + + assert not msg["success"] + assert msg["error"] == { + "code": "unknown_error", + "message": "No substitution found for input blah", + } From f4b124f5f15f3df7474ed76027db2ddf0f318193 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 25 Jun 2024 20:17:38 +0200 Subject: [PATCH 1190/1445] Fix invalid schemas (#120450) --- homeassistant/components/elkm1/__init__.py | 2 +- .../components/mobile_app/webhook.py | 24 ++++++++++--------- homeassistant/components/ombi/__init__.py | 22 +++++++++-------- 3 files changed, 26 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/elkm1/__init__.py b/homeassistant/components/elkm1/__init__.py index fff40b6ad73..b66a4ce2ed8 100644 --- a/homeassistant/components/elkm1/__init__.py +++ b/homeassistant/components/elkm1/__init__.py @@ -171,8 +171,8 @@ DEVICE_SCHEMA = vol.All( vol.Optional(CONF_THERMOSTAT, default={}): DEVICE_SCHEMA_SUBDOMAIN, vol.Optional(CONF_ZONE, default={}): DEVICE_SCHEMA_SUBDOMAIN, }, - _host_validator, ), + _host_validator, ) CONFIG_SCHEMA = vol.Schema( diff --git a/homeassistant/components/mobile_app/webhook.py b/homeassistant/components/mobile_app/webhook.py index e7cccd0f151..e93b4c5ea99 100644 --- a/homeassistant/components/mobile_app/webhook.py +++ b/homeassistant/components/mobile_app/webhook.py @@ -406,18 +406,20 @@ async def webhook_render_template( @WEBHOOK_COMMANDS.register("update_location") @validate_schema( - vol.Schema( + vol.All( cv.key_dependency(ATTR_GPS, ATTR_GPS_ACCURACY), - { - vol.Optional(ATTR_LOCATION_NAME): cv.string, - vol.Optional(ATTR_GPS): cv.gps, - vol.Optional(ATTR_GPS_ACCURACY): cv.positive_int, - vol.Optional(ATTR_BATTERY): cv.positive_int, - vol.Optional(ATTR_SPEED): cv.positive_int, - vol.Optional(ATTR_ALTITUDE): vol.Coerce(float), - vol.Optional(ATTR_COURSE): cv.positive_int, - vol.Optional(ATTR_VERTICAL_ACCURACY): cv.positive_int, - }, + vol.Schema( + { + vol.Optional(ATTR_LOCATION_NAME): cv.string, + vol.Optional(ATTR_GPS): cv.gps, + vol.Optional(ATTR_GPS_ACCURACY): cv.positive_int, + vol.Optional(ATTR_BATTERY): cv.positive_int, + vol.Optional(ATTR_SPEED): cv.positive_int, + vol.Optional(ATTR_ALTITUDE): vol.Coerce(float), + vol.Optional(ATTR_COURSE): cv.positive_int, + vol.Optional(ATTR_VERTICAL_ACCURACY): cv.positive_int, + }, + ), ) ) async def webhook_update_location( diff --git a/homeassistant/components/ombi/__init__.py b/homeassistant/components/ombi/__init__.py index 719efdc8ae3..a4cbe39f3e0 100644 --- a/homeassistant/components/ombi/__init__.py +++ b/homeassistant/components/ombi/__init__.py @@ -61,16 +61,18 @@ SUBMIT_TV_REQUEST_SERVICE_SCHEMA = vol.Schema( CONFIG_SCHEMA = vol.Schema( { - DOMAIN: vol.Schema( - { - vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_USERNAME): cv.string, - vol.Exclusive(CONF_API_KEY, "auth"): cv.string, - vol.Exclusive(CONF_PASSWORD, "auth"): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Optional(CONF_URLBASE, default=DEFAULT_URLBASE): urlbase, - vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean, - }, + DOMAIN: vol.All( + vol.Schema( + { + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_USERNAME): cv.string, + vol.Exclusive(CONF_API_KEY, "auth"): cv.string, + vol.Exclusive(CONF_PASSWORD, "auth"): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_URLBASE, default=DEFAULT_URLBASE): urlbase, + vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean, + } + ), cv.has_at_least_one_key("auth"), ) }, From 9bc436185595bb7be3ea57d91179bec7e38fc4d4 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 25 Jun 2024 20:17:51 +0200 Subject: [PATCH 1191/1445] Bump Knocki to 0.2.0 (#120447) --- homeassistant/components/knocki/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/knocki/manifest.json b/homeassistant/components/knocki/manifest.json index bf4dcea4b67..e78e9856d62 100644 --- a/homeassistant/components/knocki/manifest.json +++ b/homeassistant/components/knocki/manifest.json @@ -7,5 +7,5 @@ "integration_type": "device", "iot_class": "cloud_push", "loggers": ["knocki"], - "requirements": ["knocki==0.1.5"] + "requirements": ["knocki==0.2.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 7c6f39ed88b..b75555283a2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1206,7 +1206,7 @@ kegtron-ble==0.4.0 kiwiki-client==0.1.1 # homeassistant.components.knocki -knocki==0.1.5 +knocki==0.2.0 # homeassistant.components.knx knx-frontend==2024.1.20.105944 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9bc9a740278..37b3700372b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -984,7 +984,7 @@ justnimbus==0.7.4 kegtron-ble==0.4.0 # homeassistant.components.knocki -knocki==0.1.5 +knocki==0.2.0 # homeassistant.components.knx knx-frontend==2024.1.20.105944 From 4290a1fcb5675c618a71093222b232a39239f95f Mon Sep 17 00:00:00 2001 From: Teemu R Date: Tue, 25 Jun 2024 22:01:21 +0200 Subject: [PATCH 1192/1445] Upgrade tplink with new platforms, features and device support (#120060) Co-authored-by: Teemu Rytilahti Co-authored-by: sdb9696 Co-authored-by: Steven B <51370195+sdb9696@users.noreply.github.com> Co-authored-by: J. Nick Koston Co-authored-by: Teemu R. --- homeassistant/components/tplink/README.md | 34 + homeassistant/components/tplink/__init__.py | 125 ++- .../components/tplink/binary_sensor.py | 96 +++ homeassistant/components/tplink/button.py | 69 ++ homeassistant/components/tplink/climate.py | 140 ++++ .../components/tplink/config_flow.py | 56 +- homeassistant/components/tplink/const.py | 22 +- .../components/tplink/coordinator.py | 8 +- .../components/tplink/diagnostics.py | 12 +- homeassistant/components/tplink/entity.py | 375 ++++++++- homeassistant/components/tplink/fan.py | 111 +++ homeassistant/components/tplink/icons.json | 99 +++ homeassistant/components/tplink/light.py | 190 +++-- homeassistant/components/tplink/manifest.json | 30 +- homeassistant/components/tplink/number.py | 108 +++ homeassistant/components/tplink/select.py | 95 +++ homeassistant/components/tplink/sensor.py | 225 +++-- homeassistant/components/tplink/strings.json | 140 +++- homeassistant/components/tplink/switch.py | 182 ++-- homeassistant/generated/dhcp.py | 35 + requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/tplink/__init__.py | 492 +++++++---- tests/components/tplink/conftest.py | 12 +- .../components/tplink/fixtures/features.json | 287 +++++++ .../tplink/snapshots/test_binary_sensor.ambr | 369 ++++++++ .../tplink/snapshots/test_button.ambr | 127 +++ .../tplink/snapshots/test_climate.ambr | 94 +++ .../components/tplink/snapshots/test_fan.ambr | 194 +++++ .../tplink/snapshots/test_number.ambr | 255 ++++++ .../tplink/snapshots/test_select.ambr | 238 ++++++ .../tplink/snapshots/test_sensor.ambr | 790 ++++++++++++++++++ .../tplink/snapshots/test_switch.ambr | 311 +++++++ tests/components/tplink/test_binary_sensor.py | 124 +++ tests/components/tplink/test_button.py | 153 ++++ tests/components/tplink/test_climate.py | 226 +++++ tests/components/tplink/test_config_flow.py | 54 +- tests/components/tplink/test_diagnostics.py | 10 +- tests/components/tplink/test_fan.py | 154 ++++ tests/components/tplink/test_init.py | 190 ++++- tests/components/tplink/test_light.py | 427 ++++++---- tests/components/tplink/test_number.py | 163 ++++ tests/components/tplink/test_select.py | 158 ++++ tests/components/tplink/test_sensor.py | 233 +++++- tests/components/tplink/test_switch.py | 160 +++- 45 files changed, 6528 insertions(+), 849 deletions(-) create mode 100644 homeassistant/components/tplink/README.md create mode 100644 homeassistant/components/tplink/binary_sensor.py create mode 100644 homeassistant/components/tplink/button.py create mode 100644 homeassistant/components/tplink/climate.py create mode 100644 homeassistant/components/tplink/fan.py create mode 100644 homeassistant/components/tplink/number.py create mode 100644 homeassistant/components/tplink/select.py create mode 100644 tests/components/tplink/fixtures/features.json create mode 100644 tests/components/tplink/snapshots/test_binary_sensor.ambr create mode 100644 tests/components/tplink/snapshots/test_button.ambr create mode 100644 tests/components/tplink/snapshots/test_climate.ambr create mode 100644 tests/components/tplink/snapshots/test_fan.ambr create mode 100644 tests/components/tplink/snapshots/test_number.ambr create mode 100644 tests/components/tplink/snapshots/test_select.ambr create mode 100644 tests/components/tplink/snapshots/test_sensor.ambr create mode 100644 tests/components/tplink/snapshots/test_switch.ambr create mode 100644 tests/components/tplink/test_binary_sensor.py create mode 100644 tests/components/tplink/test_button.py create mode 100644 tests/components/tplink/test_climate.py create mode 100644 tests/components/tplink/test_fan.py create mode 100644 tests/components/tplink/test_number.py create mode 100644 tests/components/tplink/test_select.py diff --git a/homeassistant/components/tplink/README.md b/homeassistant/components/tplink/README.md new file mode 100644 index 00000000000..129d9e7fcce --- /dev/null +++ b/homeassistant/components/tplink/README.md @@ -0,0 +1,34 @@ +# TPLink Integration + +This document covers details that new contributors may find helpful when getting started. + +## Modules vs Features + +The python-kasa library which this integration depends on exposes functionality via modules and features. +The `Module` APIs encapsulate groups of functionality provided by a device, +e.g. Light which has multiple attributes and methods such as `set_hsv`, `brightness` etc. +The `features` encapsulate unitary functions and allow for introspection. +e.g. `on_since`, `voltage` etc. + +If the integration implements a platform that presents single functions or data points, such as `sensor`, +`button`, `switch` it uses features. +If it's implementing a platform with more complex functionality like `light`, `fan` or `climate` it will +use modules. + +## Adding new entities + +All feature-based entities are created based on the information from the upstream library. +If you want to add new feature, it needs to be implemented at first in there. +After the feature is exposed by the upstream library, +it needs to be added to the `_DESCRIPTIONS` list of the corresponding platform. +The integration logs missing descriptions on features supported by the device to help spotting them. + +In many cases it is enough to define the `key` (corresponding to upstream `feature.id`), +but you can pass more information for nicer user experience: +* `device_class` and `state_class` should be set accordingly for binary_sensor and sensor +* If no matching classes are available, you need to update `strings.json` and `icons.json` +When doing so, do not forget to run `script/setup` to generate the translations. + +Other information like the category and whether to enable per default are read from the feature, +as are information about units and display precision hints. + diff --git a/homeassistant/components/tplink/__init__.py b/homeassistant/components/tplink/__init__.py index fbb176b2d5f..764867f0bee 100644 --- a/homeassistant/components/tplink/__init__.py +++ b/homeassistant/components/tplink/__init__.py @@ -9,14 +9,15 @@ from typing import Any from aiohttp import ClientSession from kasa import ( - AuthenticationException, + AuthenticationError, Credentials, + Device, DeviceConfig, Discover, - SmartDevice, - SmartDeviceException, + KasaException, ) from kasa.httpclient import get_cookie_jar +from kasa.iot import IotStrip from homeassistant import config_entries from homeassistant.components import network @@ -51,6 +52,8 @@ from .const import ( from .coordinator import TPLinkDataUpdateCoordinator from .models import TPLinkData +type TPLinkConfigEntry = ConfigEntry[TPLinkData] + DISCOVERY_INTERVAL = timedelta(minutes=15) CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN) @@ -67,7 +70,7 @@ def create_async_tplink_clientsession(hass: HomeAssistant) -> ClientSession: @callback def async_trigger_discovery( hass: HomeAssistant, - discovered_devices: dict[str, SmartDevice], + discovered_devices: dict[str, Device], ) -> None: """Trigger config flows for discovered devices.""" for formatted_mac, device in discovered_devices.items(): @@ -87,7 +90,7 @@ def async_trigger_discovery( ) -async def async_discover_devices(hass: HomeAssistant) -> dict[str, SmartDevice]: +async def async_discover_devices(hass: HomeAssistant) -> dict[str, Device]: """Discover TPLink devices on configured network interfaces.""" credentials = await get_credentials(hass) @@ -101,7 +104,7 @@ async def async_discover_devices(hass: HomeAssistant) -> dict[str, SmartDevice]: ) for address in broadcast_addresses ] - discovered_devices: dict[str, SmartDevice] = {} + discovered_devices: dict[str, Device] = {} for device_list in await asyncio.gather(*tasks): for device in device_list.values(): discovered_devices[dr.format_mac(device.mac)] = device @@ -126,7 +129,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: return True -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: TPLinkConfigEntry) -> bool: """Set up TPLink from a config entry.""" host: str = entry.data[CONF_HOST] credentials = await get_credentials(hass) @@ -135,7 +138,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if config_dict := entry.data.get(CONF_DEVICE_CONFIG): try: config = DeviceConfig.from_dict(config_dict) - except SmartDeviceException: + except KasaException: _LOGGER.warning( "Invalid connection type dict for %s: %s", host, config_dict ) @@ -151,10 +154,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if credentials: config.credentials = credentials try: - device: SmartDevice = await SmartDevice.connect(config=config) - except AuthenticationException as ex: + device: Device = await Device.connect(config=config) + except AuthenticationError as ex: raise ConfigEntryAuthFailed from ex - except SmartDeviceException as ex: + except KasaException as ex: raise ConfigEntryNotReady from ex device_config_dict = device.config.to_dict( @@ -189,7 +192,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: parent_coordinator = TPLinkDataUpdateCoordinator(hass, device, timedelta(seconds=5)) child_coordinators: list[TPLinkDataUpdateCoordinator] = [] - if device.is_strip: + # The iot HS300 allows a limited number of concurrent requests and fetching the + # emeter information requires separate ones so create child coordinators here. + if isinstance(device, IotStrip): child_coordinators = [ # The child coordinators only update energy data so we can # set a longer update interval to avoid flooding the device @@ -197,27 +202,23 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: for child in device.children ] - hass.data[DOMAIN][entry.entry_id] = TPLinkData( - parent_coordinator, child_coordinators - ) + entry.runtime_data = TPLinkData(parent_coordinator, child_coordinators) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: TPLinkConfigEntry) -> bool: """Unload a config entry.""" - hass_data: dict[str, Any] = hass.data[DOMAIN] - data: TPLinkData = hass_data[entry.entry_id] + data = entry.runtime_data device = data.parent_coordinator.device - if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): - hass_data.pop(entry.entry_id) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) await device.protocol.close() return unload_ok -def legacy_device_id(device: SmartDevice) -> str: +def legacy_device_id(device: Device) -> str: """Convert the device id so it matches what was used in the original version.""" device_id: str = device.device_id # Plugs are prefixed with the mac in python-kasa but not @@ -227,6 +228,24 @@ def legacy_device_id(device: SmartDevice) -> str: return device_id.split("_")[1] +def get_device_name(device: Device, parent: Device | None = None) -> str: + """Get a name for the device. alias can be none on some devices.""" + if device.alias: + return device.alias + # Return the child device type with an index if there's more than one child device + # of the same type. i.e. Devices like the ks240 with one child of each type + # skip the suffix + if parent: + devices = [ + child.device_id + for child in parent.children + if child.device_type is device.device_type + ] + suffix = f" {devices.index(device.device_id) + 1}" if len(devices) > 1 else "" + return f"{device.device_type.value.capitalize()}{suffix}" + return f"Unnamed {device.model}" + + async def get_credentials(hass: HomeAssistant) -> Credentials | None: """Retrieve the credentials from hass data.""" if DOMAIN in hass.data and CONF_AUTHENTICATION in hass.data[DOMAIN]: @@ -247,3 +266,67 @@ async def set_credentials(hass: HomeAssistant, username: str, password: str) -> def mac_alias(mac: str) -> str: """Convert a MAC address to a short address for the UI.""" return mac.replace(":", "")[-4:].upper() + + +async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: + """Migrate old entry.""" + version = config_entry.version + minor_version = config_entry.minor_version + + _LOGGER.debug("Migrating from version %s.%s", version, minor_version) + + if version == 1 and minor_version < 3: + # Previously entities on child devices added themselves to the parent + # device and set their device id as identifiers along with mac + # as a connection which creates a single device entry linked by all + # identifiers. Now we create separate devices connected with via_device + # so the identifier linkage must be removed otherwise the devices will + # always be linked into one device. + dev_reg = dr.async_get(hass) + for device in dr.async_entries_for_config_entry(dev_reg, config_entry.entry_id): + new_identifiers: set[tuple[str, str]] | None = None + if len(device.identifiers) > 1 and ( + mac := next( + iter( + [ + conn[1] + for conn in device.connections + if conn[0] == dr.CONNECTION_NETWORK_MAC + ] + ), + None, + ) + ): + for identifier in device.identifiers: + # Previously only iot devices that use the MAC address as + # device_id had child devices so check for mac as the + # parent device. + if identifier[0] == DOMAIN and identifier[1].upper() == mac.upper(): + new_identifiers = {identifier} + break + if new_identifiers: + dev_reg.async_update_device( + device.id, new_identifiers=new_identifiers + ) + _LOGGER.debug( + "Replaced identifiers for device %s (%s): %s with: %s", + device.name, + device.model, + device.identifiers, + new_identifiers, + ) + else: + # No match on mac so raise an error. + _LOGGER.error( + "Unable to replace identifiers for device %s (%s): %s", + device.name, + device.model, + device.identifiers, + ) + + minor_version = 3 + hass.config_entries.async_update_entry(config_entry, minor_version=3) + + _LOGGER.debug("Migration to version %s.%s successful", version, minor_version) + + return True diff --git a/homeassistant/components/tplink/binary_sensor.py b/homeassistant/components/tplink/binary_sensor.py new file mode 100644 index 00000000000..97bb794a8f9 --- /dev/null +++ b/homeassistant/components/tplink/binary_sensor.py @@ -0,0 +1,96 @@ +"""Support for TPLink binary sensors.""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Final + +from kasa import Feature + +from homeassistant.components.binary_sensor import ( + BinarySensorDeviceClass, + BinarySensorEntity, + BinarySensorEntityDescription, +) +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import TPLinkConfigEntry +from .entity import CoordinatedTPLinkFeatureEntity, TPLinkFeatureEntityDescription + + +@dataclass(frozen=True, kw_only=True) +class TPLinkBinarySensorEntityDescription( + BinarySensorEntityDescription, TPLinkFeatureEntityDescription +): + """Base class for a TPLink feature based sensor entity description.""" + + +BINARY_SENSOR_DESCRIPTIONS: Final = ( + TPLinkBinarySensorEntityDescription( + key="overheated", + device_class=BinarySensorDeviceClass.PROBLEM, + ), + TPLinkBinarySensorEntityDescription( + key="battery_low", + device_class=BinarySensorDeviceClass.BATTERY, + ), + TPLinkBinarySensorEntityDescription( + key="cloud_connection", + device_class=BinarySensorDeviceClass.CONNECTIVITY, + ), + # To be replaced & disabled per default by the upcoming update platform. + TPLinkBinarySensorEntityDescription( + key="update_available", + device_class=BinarySensorDeviceClass.UPDATE, + ), + TPLinkBinarySensorEntityDescription( + key="temperature_warning", + ), + TPLinkBinarySensorEntityDescription( + key="humidity_warning", + ), + TPLinkBinarySensorEntityDescription( + key="is_open", + device_class=BinarySensorDeviceClass.DOOR, + ), + TPLinkBinarySensorEntityDescription( + key="water_alert", + device_class=BinarySensorDeviceClass.MOISTURE, + ), +) + +BINARYSENSOR_DESCRIPTIONS_MAP = {desc.key: desc for desc in BINARY_SENSOR_DESCRIPTIONS} + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: TPLinkConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up sensors.""" + data = config_entry.runtime_data + parent_coordinator = data.parent_coordinator + children_coordinators = data.children_coordinators + device = parent_coordinator.device + + entities = CoordinatedTPLinkFeatureEntity.entities_for_device_and_its_children( + device=device, + coordinator=parent_coordinator, + feature_type=Feature.Type.BinarySensor, + entity_class=TPLinkBinarySensorEntity, + descriptions=BINARYSENSOR_DESCRIPTIONS_MAP, + child_coordinators=children_coordinators, + ) + async_add_entities(entities) + + +class TPLinkBinarySensorEntity(CoordinatedTPLinkFeatureEntity, BinarySensorEntity): + """Representation of a TPLink binary sensor.""" + + entity_description: TPLinkBinarySensorEntityDescription + + @callback + def _async_update_attrs(self) -> None: + """Update the entity's attributes.""" + self._attr_is_on = self._feature.value diff --git a/homeassistant/components/tplink/button.py b/homeassistant/components/tplink/button.py new file mode 100644 index 00000000000..4dcc27858a8 --- /dev/null +++ b/homeassistant/components/tplink/button.py @@ -0,0 +1,69 @@ +"""Support for TPLink button entities.""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Final + +from kasa import Feature + +from homeassistant.components.button import ButtonEntity, ButtonEntityDescription +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import TPLinkConfigEntry +from .entity import CoordinatedTPLinkFeatureEntity, TPLinkFeatureEntityDescription + + +@dataclass(frozen=True, kw_only=True) +class TPLinkButtonEntityDescription( + ButtonEntityDescription, TPLinkFeatureEntityDescription +): + """Base class for a TPLink feature based button entity description.""" + + +BUTTON_DESCRIPTIONS: Final = [ + TPLinkButtonEntityDescription( + key="test_alarm", + ), + TPLinkButtonEntityDescription( + key="stop_alarm", + ), +] + +BUTTON_DESCRIPTIONS_MAP = {desc.key: desc for desc in BUTTON_DESCRIPTIONS} + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: TPLinkConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up buttons.""" + data = config_entry.runtime_data + parent_coordinator = data.parent_coordinator + children_coordinators = data.children_coordinators + device = parent_coordinator.device + + entities = CoordinatedTPLinkFeatureEntity.entities_for_device_and_its_children( + device=device, + coordinator=parent_coordinator, + feature_type=Feature.Type.Action, + entity_class=TPLinkButtonEntity, + descriptions=BUTTON_DESCRIPTIONS_MAP, + child_coordinators=children_coordinators, + ) + async_add_entities(entities) + + +class TPLinkButtonEntity(CoordinatedTPLinkFeatureEntity, ButtonEntity): + """Representation of a TPLink button entity.""" + + entity_description: TPLinkButtonEntityDescription + + async def async_press(self) -> None: + """Execute action.""" + await self._feature.set_value(True) + + def _async_update_attrs(self) -> None: + """No need to update anything.""" diff --git a/homeassistant/components/tplink/climate.py b/homeassistant/components/tplink/climate.py new file mode 100644 index 00000000000..99a8c43fac3 --- /dev/null +++ b/homeassistant/components/tplink/climate.py @@ -0,0 +1,140 @@ +"""Support for TP-Link thermostats.""" + +from __future__ import annotations + +import logging +from typing import Any, cast + +from kasa import Device, DeviceType +from kasa.smart.modules.temperaturecontrol import ThermostatState + +from homeassistant.components.climate import ( + ATTR_TEMPERATURE, + ClimateEntity, + ClimateEntityFeature, + HVACAction, + HVACMode, +) +from homeassistant.const import PRECISION_WHOLE +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import ServiceValidationError +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import TPLinkConfigEntry +from .const import UNIT_MAPPING +from .coordinator import TPLinkDataUpdateCoordinator +from .entity import CoordinatedTPLinkEntity, async_refresh_after + +# Upstream state to HVACAction +STATE_TO_ACTION = { + ThermostatState.Idle: HVACAction.IDLE, + ThermostatState.Heating: HVACAction.HEATING, + ThermostatState.Off: HVACAction.OFF, +} + + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: TPLinkConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up climate entities.""" + data = config_entry.runtime_data + parent_coordinator = data.parent_coordinator + device = parent_coordinator.device + + # As there are no standalone thermostats, we just iterate over the children. + async_add_entities( + TPLinkClimateEntity(child, parent_coordinator, parent=device) + for child in device.children + if child.device_type is DeviceType.Thermostat + ) + + +class TPLinkClimateEntity(CoordinatedTPLinkEntity, ClimateEntity): + """Representation of a TPLink thermostat.""" + + _attr_name = None + _attr_supported_features = ( + ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.TURN_OFF + | ClimateEntityFeature.TURN_ON + ) + _attr_hvac_modes = [HVACMode.HEAT, HVACMode.OFF] + _attr_precision = PRECISION_WHOLE + + # This disables the warning for async_turn_{on,off}, can be removed later. + _enable_turn_on_off_backwards_compatibility = False + + def __init__( + self, + device: Device, + coordinator: TPLinkDataUpdateCoordinator, + *, + parent: Device, + ) -> None: + """Initialize the climate entity.""" + super().__init__(device, coordinator, parent=parent) + self._state_feature = self._device.features["state"] + self._mode_feature = self._device.features["thermostat_mode"] + self._temp_feature = self._device.features["temperature"] + self._target_feature = self._device.features["target_temperature"] + + self._attr_min_temp = self._target_feature.minimum_value + self._attr_max_temp = self._target_feature.maximum_value + self._attr_temperature_unit = UNIT_MAPPING[cast(str, self._temp_feature.unit)] + + @async_refresh_after + async def async_set_temperature(self, **kwargs: Any) -> None: + """Set target temperature.""" + await self._target_feature.set_value(int(kwargs[ATTR_TEMPERATURE])) + + @async_refresh_after + async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: + """Set hvac mode (heat/off).""" + if hvac_mode is HVACMode.HEAT: + await self._state_feature.set_value(True) + elif hvac_mode is HVACMode.OFF: + await self._state_feature.set_value(False) + else: + raise ServiceValidationError(f"Tried to set unsupported mode: {hvac_mode}") + + @async_refresh_after + async def async_turn_on(self) -> None: + """Turn heating on.""" + await self._state_feature.set_value(True) + + @async_refresh_after + async def async_turn_off(self) -> None: + """Turn heating off.""" + await self._state_feature.set_value(False) + + @callback + def _async_update_attrs(self) -> None: + """Update the entity's attributes.""" + self._attr_current_temperature = self._temp_feature.value + self._attr_target_temperature = self._target_feature.value + + self._attr_hvac_mode = ( + HVACMode.HEAT if self._state_feature.value else HVACMode.OFF + ) + + if ( + self._mode_feature.value not in STATE_TO_ACTION + and self._attr_hvac_action is not HVACAction.OFF + ): + _LOGGER.warning( + "Unknown thermostat state, defaulting to OFF: %s", + self._mode_feature.value, + ) + self._attr_hvac_action = HVACAction.OFF + return + + self._attr_hvac_action = STATE_TO_ACTION[self._mode_feature.value] + + def _get_unique_id(self) -> str: + """Return unique id.""" + return f"{self._device.device_id}_climate" diff --git a/homeassistant/components/tplink/config_flow.py b/homeassistant/components/tplink/config_flow.py index df3291561fa..7bead2207a3 100644 --- a/homeassistant/components/tplink/config_flow.py +++ b/homeassistant/components/tplink/config_flow.py @@ -6,13 +6,13 @@ from collections.abc import Mapping from typing import Any from kasa import ( - AuthenticationException, + AuthenticationError, Credentials, + Device, DeviceConfig, Discover, - SmartDevice, - SmartDeviceException, - TimeoutException, + KasaException, + TimeoutError, ) import voluptuous as vol @@ -55,13 +55,13 @@ class TPLinkConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for tplink.""" VERSION = 1 - MINOR_VERSION = 2 + MINOR_VERSION = 3 reauth_entry: ConfigEntry | None = None def __init__(self) -> None: """Initialize the config flow.""" - self._discovered_devices: dict[str, SmartDevice] = {} - self._discovered_device: SmartDevice | None = None + self._discovered_devices: dict[str, Device] = {} + self._discovered_device: Device | None = None async def async_step_dhcp( self, discovery_info: dhcp.DhcpServiceInfo @@ -129,9 +129,9 @@ class TPLinkConfigFlow(ConfigFlow, domain=DOMAIN): await self._async_try_discover_and_update( host, credentials, raise_on_progress=True ) - except AuthenticationException: + except AuthenticationError: return await self.async_step_discovery_auth_confirm() - except SmartDeviceException: + except KasaException: return self.async_abort(reason="cannot_connect") return await self.async_step_discovery_confirm() @@ -149,7 +149,7 @@ class TPLinkConfigFlow(ConfigFlow, domain=DOMAIN): device = await self._async_try_connect( self._discovered_device, credentials ) - except AuthenticationException: + except AuthenticationError: pass # Authentication exceptions should continue to the rest of the step else: self._discovered_device = device @@ -165,10 +165,10 @@ class TPLinkConfigFlow(ConfigFlow, domain=DOMAIN): device = await self._async_try_connect( self._discovered_device, credentials ) - except AuthenticationException as ex: + except AuthenticationError as ex: errors[CONF_PASSWORD] = "invalid_auth" placeholders["error"] = str(ex) - except SmartDeviceException as ex: + except KasaException as ex: errors["base"] = "cannot_connect" placeholders["error"] = str(ex) else: @@ -229,9 +229,9 @@ class TPLinkConfigFlow(ConfigFlow, domain=DOMAIN): device = await self._async_try_discover_and_update( host, credentials, raise_on_progress=False ) - except AuthenticationException: + except AuthenticationError: return await self.async_step_user_auth_confirm() - except SmartDeviceException as ex: + except KasaException as ex: errors["base"] = "cannot_connect" placeholders["error"] = str(ex) else: @@ -261,10 +261,10 @@ class TPLinkConfigFlow(ConfigFlow, domain=DOMAIN): device = await self._async_try_connect( self._discovered_device, credentials ) - except AuthenticationException as ex: + except AuthenticationError as ex: errors[CONF_PASSWORD] = "invalid_auth" placeholders["error"] = str(ex) - except SmartDeviceException as ex: + except KasaException as ex: errors["base"] = "cannot_connect" placeholders["error"] = str(ex) else: @@ -298,9 +298,9 @@ class TPLinkConfigFlow(ConfigFlow, domain=DOMAIN): device = await self._async_try_connect( self._discovered_device, credentials ) - except AuthenticationException: + except AuthenticationError: return await self.async_step_user_auth_confirm() - except SmartDeviceException: + except KasaException: return self.async_abort(reason="cannot_connect") return self._async_create_entry_from_device(device) @@ -343,7 +343,7 @@ class TPLinkConfigFlow(ConfigFlow, domain=DOMAIN): _config_entries.flow.async_abort(flow["flow_id"]) @callback - def _async_create_entry_from_device(self, device: SmartDevice) -> ConfigFlowResult: + def _async_create_entry_from_device(self, device: Device) -> ConfigFlowResult: """Create a config entry from a smart device.""" self._abort_if_unique_id_configured(updates={CONF_HOST: device.host}) return self.async_create_entry( @@ -364,7 +364,7 @@ class TPLinkConfigFlow(ConfigFlow, domain=DOMAIN): host: str, credentials: Credentials | None, raise_on_progress: bool, - ) -> SmartDevice: + ) -> Device: """Try to discover the device and call update. Will try to connect to legacy devices if discovery fails. @@ -373,11 +373,9 @@ class TPLinkConfigFlow(ConfigFlow, domain=DOMAIN): self._discovered_device = await Discover.discover_single( host, credentials=credentials ) - except TimeoutException: + except TimeoutError: # Try connect() to legacy devices if discovery fails - self._discovered_device = await SmartDevice.connect( - config=DeviceConfig(host) - ) + self._discovered_device = await Device.connect(config=DeviceConfig(host)) else: if self._discovered_device.config.uses_http: self._discovered_device.config.http_client = ( @@ -392,9 +390,9 @@ class TPLinkConfigFlow(ConfigFlow, domain=DOMAIN): async def _async_try_connect( self, - discovered_device: SmartDevice, + discovered_device: Device, credentials: Credentials | None, - ) -> SmartDevice: + ) -> Device: """Try to connect.""" self._async_abort_entries_match({CONF_HOST: discovered_device.host}) @@ -405,7 +403,7 @@ class TPLinkConfigFlow(ConfigFlow, domain=DOMAIN): if config.uses_http: config.http_client = create_async_tplink_clientsession(self.hass) - self._discovered_device = await SmartDevice.connect(config=config) + self._discovered_device = await Device.connect(config=config) await self.async_set_unique_id( dr.format_mac(self._discovered_device.mac), raise_on_progress=False, @@ -442,10 +440,10 @@ class TPLinkConfigFlow(ConfigFlow, domain=DOMAIN): credentials=credentials, raise_on_progress=True, ) - except AuthenticationException as ex: + except AuthenticationError as ex: errors[CONF_PASSWORD] = "invalid_auth" placeholders["error"] = str(ex) - except SmartDeviceException as ex: + except KasaException as ex: errors["base"] = "cannot_connect" placeholders["error"] = str(ex) else: diff --git a/homeassistant/components/tplink/const.py b/homeassistant/components/tplink/const.py index 96892bacee7..d77d415aa9c 100644 --- a/homeassistant/components/tplink/const.py +++ b/homeassistant/components/tplink/const.py @@ -4,13 +4,16 @@ from __future__ import annotations from typing import Final -from homeassistant.const import Platform +from homeassistant.const import Platform, UnitOfTemperature DOMAIN = "tplink" DISCOVERY_TIMEOUT = 5 # Home Assistant will complain if startup takes > 10s CONNECT_TIMEOUT = 5 +# Identifier used for primary control state. +PRIMARY_STATE_ID = "state" + ATTR_CURRENT_A: Final = "current_a" ATTR_CURRENT_POWER_W: Final = "current_power_w" ATTR_TODAY_ENERGY_KWH: Final = "today_energy_kwh" @@ -18,4 +21,19 @@ ATTR_TOTAL_ENERGY_KWH: Final = "total_energy_kwh" CONF_DEVICE_CONFIG: Final = "device_config" -PLATFORMS: Final = [Platform.LIGHT, Platform.SENSOR, Platform.SWITCH] +PLATFORMS: Final = [ + Platform.BINARY_SENSOR, + Platform.BUTTON, + Platform.CLIMATE, + Platform.FAN, + Platform.LIGHT, + Platform.NUMBER, + Platform.SELECT, + Platform.SENSOR, + Platform.SWITCH, +] + +UNIT_MAPPING = { + "celsius": UnitOfTemperature.CELSIUS, + "fahrenheit": UnitOfTemperature.FAHRENHEIT, +} diff --git a/homeassistant/components/tplink/coordinator.py b/homeassistant/components/tplink/coordinator.py index 7595cdd8f90..1c362d33746 100644 --- a/homeassistant/components/tplink/coordinator.py +++ b/homeassistant/components/tplink/coordinator.py @@ -5,7 +5,7 @@ from __future__ import annotations from datetime import timedelta import logging -from kasa import AuthenticationException, SmartDevice, SmartDeviceException +from kasa import AuthenticationError, Device, KasaException from homeassistant import config_entries from homeassistant.core import HomeAssistant @@ -26,7 +26,7 @@ class TPLinkDataUpdateCoordinator(DataUpdateCoordinator[None]): def __init__( self, hass: HomeAssistant, - device: SmartDevice, + device: Device, update_interval: timedelta, ) -> None: """Initialize DataUpdateCoordinator to gather data for specific SmartPlug.""" @@ -47,7 +47,7 @@ class TPLinkDataUpdateCoordinator(DataUpdateCoordinator[None]): """Fetch all device and sensor data from api.""" try: await self.device.update(update_children=False) - except AuthenticationException as ex: + except AuthenticationError as ex: raise ConfigEntryAuthFailed from ex - except SmartDeviceException as ex: + except KasaException as ex: raise UpdateFailed(ex) from ex diff --git a/homeassistant/components/tplink/diagnostics.py b/homeassistant/components/tplink/diagnostics.py index e5e84b48162..46a5f0cb1bd 100644 --- a/homeassistant/components/tplink/diagnostics.py +++ b/homeassistant/components/tplink/diagnostics.py @@ -5,12 +5,10 @@ from __future__ import annotations from typing import Any from homeassistant.components.diagnostics import async_redact_data -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import format_mac -from .const import DOMAIN -from .models import TPLinkData +from . import TPLinkConfigEntry TO_REDACT = { # Entry fields @@ -23,6 +21,7 @@ TO_REDACT = { "hwId", "oemId", "deviceId", + "id", # child id for HS300 # Device location "latitude", "latitude_i", @@ -38,14 +37,17 @@ TO_REDACT = { "ssid", "nickname", "ip", + # Child device information + "original_device_id", + "parent_device_id", } async def async_get_config_entry_diagnostics( - hass: HomeAssistant, entry: ConfigEntry + hass: HomeAssistant, entry: TPLinkConfigEntry ) -> dict[str, Any]: """Return diagnostics for a config entry.""" - data: TPLinkData = hass.data[DOMAIN][entry.entry_id] + data = entry.runtime_data coordinator = data.parent_coordinator oui = format_mac(coordinator.device.mac)[:8].upper() return async_redact_data( diff --git a/homeassistant/components/tplink/entity.py b/homeassistant/components/tplink/entity.py index 52b226a1c57..4e8ec0e0779 100644 --- a/homeassistant/components/tplink/entity.py +++ b/homeassistant/components/tplink/entity.py @@ -2,24 +2,81 @@ from __future__ import annotations -from collections.abc import Awaitable, Callable, Coroutine +from abc import ABC, abstractmethod +from collections.abc import Awaitable, Callable, Coroutine, Mapping +from dataclasses import dataclass, replace +import logging from typing import Any, Concatenate from kasa import ( - AuthenticationException, - SmartDevice, - SmartDeviceException, - TimeoutException, + AuthenticationError, + Device, + DeviceType, + Feature, + KasaException, + TimeoutError, ) +from homeassistant.const import EntityCategory +from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity import EntityDescription +from homeassistant.helpers.typing import UNDEFINED, UndefinedType from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import DOMAIN +from . import get_device_name, legacy_device_id +from .const import ( + ATTR_CURRENT_A, + ATTR_CURRENT_POWER_W, + ATTR_TODAY_ENERGY_KWH, + ATTR_TOTAL_ENERGY_KWH, + DOMAIN, + PRIMARY_STATE_ID, +) from .coordinator import TPLinkDataUpdateCoordinator +_LOGGER = logging.getLogger(__name__) + +# Mapping from upstream category to homeassistant category +FEATURE_CATEGORY_TO_ENTITY_CATEGORY = { + Feature.Category.Config: EntityCategory.CONFIG, + Feature.Category.Info: EntityCategory.DIAGNOSTIC, + Feature.Category.Debug: EntityCategory.DIAGNOSTIC, +} + +# Skips creating entities for primary features supported by a specialized platform. +# For example, we do not need a separate "state" switch for light bulbs. +DEVICETYPES_WITH_SPECIALIZED_PLATFORMS = { + DeviceType.Bulb, + DeviceType.LightStrip, + DeviceType.Dimmer, + DeviceType.Fan, + DeviceType.Thermostat, +} + +# Features excluded due to future platform additions +EXCLUDED_FEATURES = { + # update + "current_firmware_version", + "available_firmware_version", + # fan + "fan_speed_level", +} + +LEGACY_KEY_MAPPING = { + "current": ATTR_CURRENT_A, + "current_consumption": ATTR_CURRENT_POWER_W, + "consumption_today": ATTR_TODAY_ENERGY_KWH, + "consumption_total": ATTR_TOTAL_ENERGY_KWH, +} + + +@dataclass(frozen=True, kw_only=True) +class TPLinkFeatureEntityDescription(EntityDescription): + """Base class for a TPLink feature based entity description.""" + def async_refresh_after[_T: CoordinatedTPLinkEntity, **_P]( func: Callable[Concatenate[_T, _P], Awaitable[None]], @@ -29,7 +86,7 @@ def async_refresh_after[_T: CoordinatedTPLinkEntity, **_P]( async def _async_wrap(self: _T, *args: _P.args, **kwargs: _P.kwargs) -> None: try: await func(self, *args, **kwargs) - except AuthenticationException as ex: + except AuthenticationError as ex: self.coordinator.config_entry.async_start_reauth(self.hass) raise HomeAssistantError( translation_domain=DOMAIN, @@ -39,7 +96,7 @@ def async_refresh_after[_T: CoordinatedTPLinkEntity, **_P]( "exc": str(ex), }, ) from ex - except TimeoutException as ex: + except TimeoutError as ex: raise HomeAssistantError( translation_domain=DOMAIN, translation_key="device_timeout", @@ -48,7 +105,7 @@ def async_refresh_after[_T: CoordinatedTPLinkEntity, **_P]( "exc": str(ex), }, ) from ex - except SmartDeviceException as ex: + except KasaException as ex: raise HomeAssistantError( translation_domain=DOMAIN, translation_key="device_error", @@ -62,24 +119,302 @@ def async_refresh_after[_T: CoordinatedTPLinkEntity, **_P]( return _async_wrap -class CoordinatedTPLinkEntity(CoordinatorEntity[TPLinkDataUpdateCoordinator]): +class CoordinatedTPLinkEntity(CoordinatorEntity[TPLinkDataUpdateCoordinator], ABC): """Common base class for all coordinated tplink entities.""" _attr_has_entity_name = True + _device: Device def __init__( - self, device: SmartDevice, coordinator: TPLinkDataUpdateCoordinator + self, + device: Device, + coordinator: TPLinkDataUpdateCoordinator, + *, + feature: Feature | None = None, + parent: Device | None = None, ) -> None: - """Initialize the switch.""" + """Initialize the entity.""" super().__init__(coordinator) - self.device: SmartDevice = device - self._attr_unique_id = device.device_id + self._device: Device = device + self._feature = feature + + registry_device = device + device_name = get_device_name(device, parent=parent) + if parent and parent.device_type is not Device.Type.Hub: + if not feature or feature.id == PRIMARY_STATE_ID: + # Entity will be added to parent if not a hub and no feature + # parameter (i.e. core platform like Light, Fan) or the feature + # is the primary state + registry_device = parent + device_name = get_device_name(registry_device) + else: + # Prefix the device name with the parent name unless it is a + # hub attached device. Sensible default for child devices like + # strip plugs or the ks240 where the child alias makes more + # sense in the context of the parent. i.e. Hall Ceiling Fan & + # Bedroom Ceiling Fan; Child device aliases will be Ceiling Fan + # and Dimmer Switch for both so should be distinguished by the + # parent name. + device_name = f"{get_device_name(parent)} {get_device_name(device, parent=parent)}" + self._attr_device_info = DeviceInfo( - connections={(dr.CONNECTION_NETWORK_MAC, device.mac)}, - identifiers={(DOMAIN, str(device.device_id))}, + identifiers={(DOMAIN, str(registry_device.device_id))}, manufacturer="TP-Link", - model=device.model, - name=device.alias, - sw_version=device.hw_info["sw_ver"], - hw_version=device.hw_info["hw_ver"], + model=registry_device.model, + name=device_name, + sw_version=registry_device.hw_info["sw_ver"], + hw_version=registry_device.hw_info["hw_ver"], ) + + if ( + parent is not None + and parent != registry_device + and parent.device_type is not Device.Type.WallSwitch + ): + self._attr_device_info["via_device"] = (DOMAIN, parent.device_id) + else: + self._attr_device_info["connections"] = { + (dr.CONNECTION_NETWORK_MAC, device.mac) + } + + self._attr_unique_id = self._get_unique_id() + + def _get_unique_id(self) -> str: + """Return unique ID for the entity.""" + return legacy_device_id(self._device) + + async def async_added_to_hass(self) -> None: + """Handle being added to hass.""" + self._async_call_update_attrs() + return await super().async_added_to_hass() + + @abstractmethod + @callback + def _async_update_attrs(self) -> None: + """Platforms implement this to update the entity internals.""" + raise NotImplementedError + + @callback + def _async_call_update_attrs(self) -> None: + """Call update_attrs and make entity unavailable on error. + + update_attrs can sometimes fail if a device firmware update breaks the + downstream library. + """ + try: + self._async_update_attrs() + except Exception as ex: # noqa: BLE001 + if self._attr_available: + _LOGGER.warning( + "Unable to read data for %s %s: %s", + self._device, + self.entity_id, + ex, + ) + self._attr_available = False + else: + self._attr_available = True + + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + self._async_call_update_attrs() + super()._handle_coordinator_update() + + @property + def available(self) -> bool: + """Return if entity is available.""" + return self.coordinator.last_update_success and self._attr_available + + +class CoordinatedTPLinkFeatureEntity(CoordinatedTPLinkEntity, ABC): + """Common base class for all coordinated tplink feature entities.""" + + entity_description: TPLinkFeatureEntityDescription + _feature: Feature + + def __init__( + self, + device: Device, + coordinator: TPLinkDataUpdateCoordinator, + *, + feature: Feature, + description: TPLinkFeatureEntityDescription, + parent: Device | None = None, + ) -> None: + """Initialize the entity.""" + self.entity_description = description + super().__init__(device, coordinator, parent=parent, feature=feature) + + def _get_unique_id(self) -> str: + """Return unique ID for the entity.""" + key = self.entity_description.key + # The unique id for the state feature in the switch platform is the + # device_id + if key == PRIMARY_STATE_ID: + return legacy_device_id(self._device) + + # Historically the legacy device emeter attributes which are now + # replaced with features used slightly different keys. This ensures + # that those entities are not orphaned. Returns the mapped key or the + # provided key if not mapped. + key = LEGACY_KEY_MAPPING.get(key, key) + return f"{legacy_device_id(self._device)}_{key}" + + @classmethod + def _category_for_feature(cls, feature: Feature | None) -> EntityCategory | None: + """Return entity category for a feature.""" + # Main controls have no category + if feature is None or feature.category is Feature.Category.Primary: + return None + + if ( + entity_category := FEATURE_CATEGORY_TO_ENTITY_CATEGORY.get(feature.category) + ) is None: + _LOGGER.error( + "Unhandled category %s, fallback to DIAGNOSTIC", feature.category + ) + entity_category = EntityCategory.DIAGNOSTIC + + return entity_category + + @classmethod + def _description_for_feature[_D: EntityDescription]( + cls, + feature: Feature, + descriptions: Mapping[str, _D], + *, + device: Device, + parent: Device | None = None, + ) -> _D | None: + """Return description object for the given feature. + + This is responsible for setting the common parameters & deciding + based on feature id which additional parameters are passed. + """ + + if descriptions and (desc := descriptions.get(feature.id)): + translation_key: str | None = feature.id + # HA logic is to name entities based on the following logic: + # _attr_name > translation.name > description.name + # > device_class (if base platform supports). + name: str | None | UndefinedType = UNDEFINED + + # The state feature gets the device name or the child device + # name if it's a child device + if feature.id == PRIMARY_STATE_ID: + translation_key = None + # if None will use device name + name = get_device_name(device, parent=parent) if parent else None + + return replace( + desc, + translation_key=translation_key, + name=name, # if undefined will use translation key + entity_category=cls._category_for_feature(feature), + # enabled_default can be overridden to False in the description + entity_registry_enabled_default=feature.category + is not Feature.Category.Debug + and desc.entity_registry_enabled_default, + ) + + _LOGGER.info( + "Device feature: %s (%s) needs an entity description defined in HA", + feature.name, + feature.id, + ) + return None + + @classmethod + def _entities_for_device[ + _E: CoordinatedTPLinkFeatureEntity, + _D: TPLinkFeatureEntityDescription, + ]( + cls, + device: Device, + coordinator: TPLinkDataUpdateCoordinator, + *, + feature_type: Feature.Type, + entity_class: type[_E], + descriptions: Mapping[str, _D], + parent: Device | None = None, + ) -> list[_E]: + """Return a list of entities to add. + + This filters out unwanted features to avoid creating unnecessary entities + for device features that are implemented by specialized platforms like light. + """ + entities: list[_E] = [ + entity_class( + device, + coordinator, + feature=feat, + description=desc, + parent=parent, + ) + for feat in device.features.values() + if feat.type == feature_type + and feat.id not in EXCLUDED_FEATURES + and ( + feat.category is not Feature.Category.Primary + or device.device_type not in DEVICETYPES_WITH_SPECIALIZED_PLATFORMS + ) + and ( + desc := cls._description_for_feature( + feat, descriptions, device=device, parent=parent + ) + ) + ] + return entities + + @classmethod + def entities_for_device_and_its_children[ + _E: CoordinatedTPLinkFeatureEntity, + _D: TPLinkFeatureEntityDescription, + ]( + cls, + device: Device, + coordinator: TPLinkDataUpdateCoordinator, + *, + feature_type: Feature.Type, + entity_class: type[_E], + descriptions: Mapping[str, _D], + child_coordinators: list[TPLinkDataUpdateCoordinator] | None = None, + ) -> list[_E]: + """Create entities for device and its children. + + This is a helper that calls *_entities_for_device* for the device and its children. + """ + entities: list[_E] = [] + # Add parent entities before children so via_device id works. + entities.extend( + cls._entities_for_device( + device, + coordinator=coordinator, + feature_type=feature_type, + entity_class=entity_class, + descriptions=descriptions, + ) + ) + if device.children: + _LOGGER.debug("Initializing device with %s children", len(device.children)) + for idx, child in enumerate(device.children): + # HS300 does not like too many concurrent requests and its + # emeter data requires a request for each socket, so we receive + # separate coordinators. + if child_coordinators: + child_coordinator = child_coordinators[idx] + else: + child_coordinator = coordinator + entities.extend( + cls._entities_for_device( + child, + coordinator=child_coordinator, + feature_type=feature_type, + entity_class=entity_class, + descriptions=descriptions, + parent=device, + ) + ) + + return entities diff --git a/homeassistant/components/tplink/fan.py b/homeassistant/components/tplink/fan.py new file mode 100644 index 00000000000..947a9072329 --- /dev/null +++ b/homeassistant/components/tplink/fan.py @@ -0,0 +1,111 @@ +"""Support for TPLink Fan devices.""" + +import logging +import math +from typing import Any + +from kasa import Device, Module +from kasa.interfaces import Fan as FanInterface + +from homeassistant.components.fan import FanEntity, FanEntityFeature +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.util.percentage import ( + percentage_to_ranged_value, + ranged_value_to_percentage, +) +from homeassistant.util.scaling import int_states_in_range + +from . import TPLinkConfigEntry +from .coordinator import TPLinkDataUpdateCoordinator +from .entity import CoordinatedTPLinkEntity, async_refresh_after + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: TPLinkConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up fans.""" + data = config_entry.runtime_data + parent_coordinator = data.parent_coordinator + device = parent_coordinator.device + entities: list[CoordinatedTPLinkEntity] = [] + if Module.Fan in device.modules: + entities.append( + TPLinkFanEntity( + device, parent_coordinator, fan_module=device.modules[Module.Fan] + ) + ) + entities.extend( + TPLinkFanEntity( + child, + parent_coordinator, + fan_module=child.modules[Module.Fan], + parent=device, + ) + for child in device.children + if Module.Fan in child.modules + ) + async_add_entities(entities) + + +SPEED_RANGE = (1, 4) # off is not included + + +class TPLinkFanEntity(CoordinatedTPLinkEntity, FanEntity): + """Representation of a fan for a TPLink Fan device.""" + + _attr_speed_count = int_states_in_range(SPEED_RANGE) + _attr_supported_features = FanEntityFeature.SET_SPEED + + def __init__( + self, + device: Device, + coordinator: TPLinkDataUpdateCoordinator, + fan_module: FanInterface, + parent: Device | None = None, + ) -> None: + """Initialize the fan.""" + super().__init__(device, coordinator, parent=parent) + self.fan_module = fan_module + # If _attr_name is None the entity name will be the device name + self._attr_name = None if parent is None else device.alias + + @async_refresh_after + async def async_turn_on( + self, + percentage: int | None = None, + preset_mode: str | None = None, + **kwargs: Any, + ) -> None: + """Turn on the fan.""" + if percentage is not None: + value_in_range = math.ceil( + percentage_to_ranged_value(SPEED_RANGE, percentage) + ) + else: + value_in_range = SPEED_RANGE[1] + await self.fan_module.set_fan_speed_level(value_in_range) + + @async_refresh_after + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the fan off.""" + await self.fan_module.set_fan_speed_level(0) + + async def async_set_percentage(self, percentage: int) -> None: + """Set the speed percentage of the fan.""" + value_in_range = math.ceil(percentage_to_ranged_value(SPEED_RANGE, percentage)) + await self.fan_module.set_fan_speed_level(value_in_range) + + @callback + def _async_update_attrs(self) -> None: + """Update the entity's attributes.""" + fan_speed = self.fan_module.fan_speed_level + self._attr_is_on = fan_speed != 0 + if self._attr_is_on: + self._attr_percentage = ranged_value_to_percentage(SPEED_RANGE, fan_speed) + else: + self._attr_percentage = None diff --git a/homeassistant/components/tplink/icons.json b/homeassistant/components/tplink/icons.json index 9b83b3abc85..3da3b4806d3 100644 --- a/homeassistant/components/tplink/icons.json +++ b/homeassistant/components/tplink/icons.json @@ -1,11 +1,110 @@ { "entity": { + "binary_sensor": { + "humidity_warning": { + "default": "mdi:water-percent", + "state": { + "on": "mdi:water-percent-alert" + } + }, + "temperature_warning": { + "default": "mdi:thermometer-check", + "state": { + "on": "mdi:thermometer-alert" + } + } + }, + "button": { + "test_alarm": { + "default": "mdi:bell-alert" + }, + "stop_alarm": { + "default": "mdi:bell-cancel" + } + }, + "select": { + "light_preset": { + "default": "mdi:sign-direction" + }, + "alarm_sound": { + "default": "mdi:music-note" + }, + "alarm_volume": { + "default": "mdi:volume-medium", + "state": { + "low": "mdi:volume-low", + "medium": "mdi:volume-medium", + "high": "mdi:volume-high" + } + } + }, "switch": { "led": { "default": "mdi:led-off", "state": { "on": "mdi:led-on" } + }, + "auto_update_enabled": { + "default": "mdi:autorenew-off", + "state": { + "on": "mdi:autorenew" + } + }, + "auto_off_enabled": { + "default": "mdi:sleep-off", + "state": { + "on": "mdi:sleep" + } + }, + "smooth_transitions": { + "default": "mdi:transition-masked", + "state": { + "on": "mdi:transition" + } + }, + "fan_sleep_mode": { + "default": "mdi:sleep-off", + "state": { + "on": "mdi:sleep" + } + } + }, + "sensor": { + "on_since": { + "default": "mdi:clock" + }, + "ssid": { + "default": "mdi:wifi" + }, + "signal_level": { + "default": "mdi:signal" + }, + "current_firmware_version": { + "default": "mdi:information" + }, + "available_firmware_version": { + "default": "mdi:information-outline" + }, + "alarm_source": { + "default": "mdi:bell" + } + }, + "number": { + "smooth_transition_off": { + "default": "mdi:weather-sunset-down" + }, + "smooth_transition_on": { + "default": "mdi:weather-sunset-up" + }, + "auto_off_minutes": { + "default": "mdi:sleep" + }, + "temperature_offset": { + "default": "mdi:contrast" + }, + "target_temperature": { + "default": "mdi:thermometer" } } }, diff --git a/homeassistant/components/tplink/light.py b/homeassistant/components/tplink/light.py index 977e75215aa..633648bbf23 100644 --- a/homeassistant/components/tplink/light.py +++ b/homeassistant/components/tplink/light.py @@ -4,9 +4,11 @@ from __future__ import annotations from collections.abc import Sequence import logging -from typing import Any, cast +from typing import Any -from kasa import SmartBulb, SmartLightStrip +from kasa import Device, DeviceType, LightState, Module +from kasa.interfaces import Light, LightEffect +from kasa.iot import IotDevice import voluptuous as vol from homeassistant.components.light import ( @@ -15,23 +17,21 @@ from homeassistant.components.light import ( ATTR_EFFECT, ATTR_HS_COLOR, ATTR_TRANSITION, + EFFECT_OFF, ColorMode, LightEntity, LightEntityFeature, filter_supported_color_modes, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_platform import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import VolDictType -from . import legacy_device_id -from .const import DOMAIN +from . import TPLinkConfigEntry, legacy_device_id from .coordinator import TPLinkDataUpdateCoordinator from .entity import CoordinatedTPLinkEntity, async_refresh_after -from .models import TPLinkData _LOGGER = logging.getLogger(__name__) @@ -132,16 +132,24 @@ def _async_build_base_effect( async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: TPLinkConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up switches.""" - data: TPLinkData = hass.data[DOMAIN][config_entry.entry_id] + data = config_entry.runtime_data parent_coordinator = data.parent_coordinator device = parent_coordinator.device - if device.is_light_strip: - async_add_entities( - [TPLinkSmartLightStrip(cast(SmartLightStrip, device), parent_coordinator)] + entities: list[TPLinkLightEntity | TPLinkLightEffectEntity] = [] + if ( + effect_module := device.modules.get(Module.LightEffect) + ) and effect_module.has_custom_effects: + entities.append( + TPLinkLightEffectEntity( + device, + parent_coordinator, + light_module=device.modules[Module.Light], + effect_module=effect_module, + ) ) platform = entity_platform.async_get_current_platform() platform.async_register_entity_service( @@ -154,52 +162,83 @@ async def async_setup_entry( SEQUENCE_EFFECT_DICT, "async_set_sequence_effect", ) - elif device.is_bulb or device.is_dimmer: - async_add_entities( - [TPLinkSmartBulb(cast(SmartBulb, device), parent_coordinator)] + elif Module.Light in device.modules: + entities.append( + TPLinkLightEntity( + device, parent_coordinator, light_module=device.modules[Module.Light] + ) ) + entities.extend( + TPLinkLightEntity( + child, + parent_coordinator, + light_module=child.modules[Module.Light], + parent=device, + ) + for child in device.children + if Module.Light in child.modules + ) + async_add_entities(entities) -class TPLinkSmartBulb(CoordinatedTPLinkEntity, LightEntity): +class TPLinkLightEntity(CoordinatedTPLinkEntity, LightEntity): """Representation of a TPLink Smart Bulb.""" _attr_supported_features = LightEntityFeature.TRANSITION - _attr_name = None _fixed_color_mode: ColorMode | None = None - device: SmartBulb - def __init__( self, - device: SmartBulb, + device: Device, coordinator: TPLinkDataUpdateCoordinator, + *, + light_module: Light, + parent: Device | None = None, ) -> None: - """Initialize the switch.""" - super().__init__(device, coordinator) - # For backwards compat with pyHS100 - if device.is_dimmer: - # Dimmers used to use the switch format since - # pyHS100 treated them as SmartPlug but the old code - # created them as lights - # https://github.com/home-assistant/core/blob/2021.9.7/homeassistant/components/tplink/common.py#L86 - self._attr_unique_id = legacy_device_id(device) - else: - self._attr_unique_id = device.mac.replace(":", "").upper() + """Initialize the light.""" + self._parent = parent + super().__init__(device, coordinator, parent=parent) + self._light_module = light_module + # If _attr_name is None the entity name will be the device name + self._attr_name = None if parent is None else device.alias modes: set[ColorMode] = {ColorMode.ONOFF} - if device.is_variable_color_temp: + if light_module.is_variable_color_temp: modes.add(ColorMode.COLOR_TEMP) - temp_range = device.valid_temperature_range + temp_range = light_module.valid_temperature_range self._attr_min_color_temp_kelvin = temp_range.min self._attr_max_color_temp_kelvin = temp_range.max - if device.is_color: + if light_module.is_color: modes.add(ColorMode.HS) - if device.is_dimmable: + if light_module.is_dimmable: modes.add(ColorMode.BRIGHTNESS) self._attr_supported_color_modes = filter_supported_color_modes(modes) if len(self._attr_supported_color_modes) == 1: # If the light supports only a single color mode, set it now self._fixed_color_mode = next(iter(self._attr_supported_color_modes)) - self._async_update_attrs() + self._async_call_update_attrs() + + def _get_unique_id(self) -> str: + """Return unique ID for the entity.""" + # For historical reasons the light platform uses the mac address as + # the unique id whereas all other platforms use device_id. + device = self._device + + # For backwards compat with pyHS100 + if device.device_type is DeviceType.Dimmer and isinstance(device, IotDevice): + # Dimmers used to use the switch format since + # pyHS100 treated them as SmartPlug but the old code + # created them as lights + # https://github.com/home-assistant/core/blob/2021.9.7/ \ + # homeassistant/components/tplink/common.py#L86 + return legacy_device_id(device) + + # Newer devices can have child lights. While there isn't currently + # an example of a device with more than one light we use the device_id + # for consistency and future proofing + if self._parent or device.children: + return legacy_device_id(device) + + return device.mac.replace(":", "").upper() @callback def _async_extract_brightness_transition( @@ -211,12 +250,12 @@ class TPLinkSmartBulb(CoordinatedTPLinkEntity, LightEntity): if (brightness := kwargs.get(ATTR_BRIGHTNESS)) is not None: brightness = round((brightness * 100.0) / 255.0) - if self.device.is_dimmer and transition is None: - # This is a stopgap solution for inconsistent set_brightness handling - # in the upstream library, see #57265. + if self._device.device_type is DeviceType.Dimmer and transition is None: + # This is a stopgap solution for inconsistent set_brightness + # handling in the upstream library, see #57265. # This should be removed when the upstream has fixed the issue. # The device logic is to change the settings without turning it on - # except when transition is defined, so we leverage that here for now. + # except when transition is defined so we leverage that for now. transition = 1 return brightness, transition @@ -226,13 +265,13 @@ class TPLinkSmartBulb(CoordinatedTPLinkEntity, LightEntity): ) -> None: # TP-Link requires integers. hue, sat = tuple(int(val) for val in hs_color) - await self.device.set_hsv(hue, sat, brightness, transition=transition) + await self._light_module.set_hsv(hue, sat, brightness, transition=transition) async def _async_set_color_temp( self, color_temp: float, brightness: int | None, transition: int | None ) -> None: - device = self.device - valid_temperature_range = device.valid_temperature_range + light_module = self._light_module + valid_temperature_range = light_module.valid_temperature_range requested_color_temp = round(color_temp) # Clamp color temp to valid range # since if the light in a group we will @@ -242,7 +281,7 @@ class TPLinkSmartBulb(CoordinatedTPLinkEntity, LightEntity): valid_temperature_range.max, max(valid_temperature_range.min, requested_color_temp), ) - await device.set_color_temp( + await light_module.set_color_temp( clamped_color_temp, brightness=brightness, transition=transition, @@ -253,9 +292,11 @@ class TPLinkSmartBulb(CoordinatedTPLinkEntity, LightEntity): ) -> None: # Fallback to adjusting brightness or turning the bulb on if brightness is not None: - await self.device.set_brightness(brightness, transition=transition) + await self._light_module.set_brightness(brightness, transition=transition) return - await self.device.turn_on(transition=transition) + await self._light_module.set_state( + LightState(light_on=True, transition=transition) + ) @async_refresh_after async def async_turn_on(self, **kwargs: Any) -> None: @@ -275,7 +316,9 @@ class TPLinkSmartBulb(CoordinatedTPLinkEntity, LightEntity): """Turn the light off.""" if (transition := kwargs.get(ATTR_TRANSITION)) is not None: transition = int(transition * 1_000) - await self.device.turn_off(transition=transition) + await self._light_module.set_state( + LightState(light_on=False, transition=transition) + ) def _determine_color_mode(self) -> ColorMode: """Return the active color mode.""" @@ -284,48 +327,53 @@ class TPLinkSmartBulb(CoordinatedTPLinkEntity, LightEntity): return self._fixed_color_mode # The light supports both color temp and color, determine which on is active - if self.device.is_variable_color_temp and self.device.color_temp: + if self._light_module.is_variable_color_temp and self._light_module.color_temp: return ColorMode.COLOR_TEMP return ColorMode.HS @callback def _async_update_attrs(self) -> None: """Update the entity's attributes.""" - device = self.device - self._attr_is_on = device.is_on - if device.is_dimmable: - self._attr_brightness = round((device.brightness * 255.0) / 100.0) + light_module = self._light_module + self._attr_is_on = light_module.state.light_on is True + if light_module.is_dimmable: + self._attr_brightness = round((light_module.brightness * 255.0) / 100.0) color_mode = self._determine_color_mode() self._attr_color_mode = color_mode if color_mode is ColorMode.COLOR_TEMP: - self._attr_color_temp_kelvin = device.color_temp + self._attr_color_temp_kelvin = light_module.color_temp elif color_mode is ColorMode.HS: - hue, saturation, _ = device.hsv + hue, saturation, _ = light_module.hsv self._attr_hs_color = hue, saturation - @callback - def _handle_coordinator_update(self) -> None: - """Handle updated data from the coordinator.""" - self._async_update_attrs() - super()._handle_coordinator_update() - -class TPLinkSmartLightStrip(TPLinkSmartBulb): +class TPLinkLightEffectEntity(TPLinkLightEntity): """Representation of a TPLink Smart Light Strip.""" - device: SmartLightStrip + def __init__( + self, + device: Device, + coordinator: TPLinkDataUpdateCoordinator, + *, + light_module: Light, + effect_module: LightEffect, + ) -> None: + """Initialize the light strip.""" + self._effect_module = effect_module + super().__init__(device, coordinator, light_module=light_module) + _attr_supported_features = LightEntityFeature.TRANSITION | LightEntityFeature.EFFECT @callback def _async_update_attrs(self) -> None: """Update the entity's attributes.""" super()._async_update_attrs() - device = self.device - if (effect := device.effect) and effect["enable"]: - self._attr_effect = effect["name"] + effect_module = self._effect_module + if effect_module.effect != LightEffect.LIGHT_EFFECTS_OFF: + self._attr_effect = effect_module.effect else: - self._attr_effect = None - if effect_list := device.effect_list: + self._attr_effect = EFFECT_OFF + if effect_list := effect_module.effect_list: self._attr_effect_list = effect_list else: self._attr_effect_list = None @@ -335,15 +383,15 @@ class TPLinkSmartLightStrip(TPLinkSmartBulb): """Turn the light on.""" brightness, transition = self._async_extract_brightness_transition(**kwargs) if ATTR_EFFECT in kwargs: - await self.device.set_effect( + await self._effect_module.set_effect( kwargs[ATTR_EFFECT], brightness=brightness, transition=transition ) elif ATTR_COLOR_TEMP_KELVIN in kwargs: if self.effect: # If there is an effect in progress - # we have to set an HSV value to clear the effect + # we have to clear the effect # before we can set a color temp - await self.device.set_hsv(0, 0, brightness) + await self._light_module.set_hsv(0, 0, brightness) await self._async_set_color_temp( kwargs[ATTR_COLOR_TEMP_KELVIN], brightness, transition ) @@ -390,7 +438,7 @@ class TPLinkSmartLightStrip(TPLinkSmartBulb): if transition_range: effect["transition_range"] = transition_range effect["transition"] = 0 - await self.device.set_custom_effect(effect) + await self._effect_module.set_custom_effect(effect) async def async_set_sequence_effect( self, @@ -412,4 +460,4 @@ class TPLinkSmartLightStrip(TPLinkSmartBulb): "spread": spread, "direction": direction, } - await self.device.set_custom_effect(effect) + await self._effect_module.set_custom_effect(effect) diff --git a/homeassistant/components/tplink/manifest.json b/homeassistant/components/tplink/manifest.json index a91e7e5a46f..5b8e6f8fc1b 100644 --- a/homeassistant/components/tplink/manifest.json +++ b/homeassistant/components/tplink/manifest.json @@ -40,6 +40,10 @@ "hostname": "k[lps]*", "macaddress": "5091E3*" }, + { + "hostname": "p1*", + "macaddress": "5091E3*" + }, { "hostname": "k[lps]*", "macaddress": "9C5322*" @@ -216,14 +220,26 @@ "hostname": "s5*", "macaddress": "3C52A1*" }, + { + "hostname": "h1*", + "macaddress": "3C52A1*" + }, { "hostname": "l9*", "macaddress": "A842A1*" }, + { + "hostname": "p1*", + "macaddress": "A842A1*" + }, { "hostname": "l9*", "macaddress": "3460F9*" }, + { + "hostname": "p1*", + "macaddress": "3460F9*" + }, { "hostname": "hs*", "macaddress": "704F57*" @@ -232,6 +248,10 @@ "hostname": "k[lps]*", "macaddress": "74DA88*" }, + { + "hostname": "p1*", + "macaddress": "74DA88*" + }, { "hostname": "p3*", "macaddress": "788CB5*" @@ -263,11 +283,19 @@ { "hostname": "l9*", "macaddress": "F0A731*" + }, + { + "hostname": "ks2*", + "macaddress": "F0A731*" + }, + { + "hostname": "kh1*", + "macaddress": "F0A731*" } ], "documentation": "https://www.home-assistant.io/integrations/tplink", "iot_class": "local_polling", "loggers": ["kasa"], "quality_scale": "platinum", - "requirements": ["python-kasa[speedups]==0.6.2.1"] + "requirements": ["python-kasa[speedups]==0.7.0.1"] } diff --git a/homeassistant/components/tplink/number.py b/homeassistant/components/tplink/number.py new file mode 100644 index 00000000000..4b273800e6a --- /dev/null +++ b/homeassistant/components/tplink/number.py @@ -0,0 +1,108 @@ +"""Support for TPLink number entities.""" + +from __future__ import annotations + +import logging +from typing import Final + +from kasa import Device, Feature + +from homeassistant.components.number import ( + NumberEntity, + NumberEntityDescription, + NumberMode, +) +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import TPLinkConfigEntry +from .entity import ( + CoordinatedTPLinkFeatureEntity, + TPLinkDataUpdateCoordinator, + TPLinkFeatureEntityDescription, + async_refresh_after, +) + +_LOGGER = logging.getLogger(__name__) + + +class TPLinkNumberEntityDescription( + NumberEntityDescription, TPLinkFeatureEntityDescription +): + """Base class for a TPLink feature based sensor entity description.""" + + +NUMBER_DESCRIPTIONS: Final = ( + TPLinkNumberEntityDescription( + key="smooth_transition_on", + mode=NumberMode.BOX, + ), + TPLinkNumberEntityDescription( + key="smooth_transition_off", + mode=NumberMode.BOX, + ), + TPLinkNumberEntityDescription( + key="auto_off_minutes", + mode=NumberMode.BOX, + ), + TPLinkNumberEntityDescription( + key="temperature_offset", + mode=NumberMode.BOX, + ), +) + +NUMBER_DESCRIPTIONS_MAP = {desc.key: desc for desc in NUMBER_DESCRIPTIONS} + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: TPLinkConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up sensors.""" + data = config_entry.runtime_data + parent_coordinator = data.parent_coordinator + children_coordinators = data.children_coordinators + device = parent_coordinator.device + entities = CoordinatedTPLinkFeatureEntity.entities_for_device_and_its_children( + device=device, + coordinator=parent_coordinator, + feature_type=Feature.Type.Number, + entity_class=TPLinkNumberEntity, + descriptions=NUMBER_DESCRIPTIONS_MAP, + child_coordinators=children_coordinators, + ) + + async_add_entities(entities) + + +class TPLinkNumberEntity(CoordinatedTPLinkFeatureEntity, NumberEntity): + """Representation of a feature-based TPLink sensor.""" + + entity_description: TPLinkNumberEntityDescription + + def __init__( + self, + device: Device, + coordinator: TPLinkDataUpdateCoordinator, + *, + feature: Feature, + description: TPLinkFeatureEntityDescription, + parent: Device | None = None, + ) -> None: + """Initialize the a switch.""" + super().__init__( + device, coordinator, feature=feature, description=description, parent=parent + ) + self._attr_native_min_value = self._feature.minimum_value + self._attr_native_max_value = self._feature.maximum_value + + @async_refresh_after + async def async_set_native_value(self, value: float) -> None: + """Set feature value.""" + await self._feature.set_value(int(value)) + + @callback + def _async_update_attrs(self) -> None: + """Update the entity's attributes.""" + self._attr_native_value = self._feature.value diff --git a/homeassistant/components/tplink/select.py b/homeassistant/components/tplink/select.py new file mode 100644 index 00000000000..41703b27e5a --- /dev/null +++ b/homeassistant/components/tplink/select.py @@ -0,0 +1,95 @@ +"""Support for TPLink select entities.""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Final, cast + +from kasa import Device, Feature + +from homeassistant.components.select import SelectEntity, SelectEntityDescription +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import TPLinkConfigEntry +from .entity import ( + CoordinatedTPLinkFeatureEntity, + TPLinkDataUpdateCoordinator, + TPLinkFeatureEntityDescription, + async_refresh_after, +) + + +@dataclass(frozen=True, kw_only=True) +class TPLinkSelectEntityDescription( + SelectEntityDescription, TPLinkFeatureEntityDescription +): + """Base class for a TPLink feature based sensor entity description.""" + + +SELECT_DESCRIPTIONS: Final = [ + TPLinkSelectEntityDescription( + key="light_preset", + ), + TPLinkSelectEntityDescription( + key="alarm_sound", + ), + TPLinkSelectEntityDescription( + key="alarm_volume", + ), +] + +SELECT_DESCRIPTIONS_MAP = {desc.key: desc for desc in SELECT_DESCRIPTIONS} + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: TPLinkConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up sensors.""" + data = config_entry.runtime_data + parent_coordinator = data.parent_coordinator + children_coordinators = data.children_coordinators + device = parent_coordinator.device + + entities = CoordinatedTPLinkFeatureEntity.entities_for_device_and_its_children( + device=device, + coordinator=parent_coordinator, + feature_type=Feature.Type.Choice, + entity_class=TPLinkSelectEntity, + descriptions=SELECT_DESCRIPTIONS_MAP, + child_coordinators=children_coordinators, + ) + async_add_entities(entities) + + +class TPLinkSelectEntity(CoordinatedTPLinkFeatureEntity, SelectEntity): + """Representation of a tplink select entity.""" + + entity_description: TPLinkSelectEntityDescription + + def __init__( + self, + device: Device, + coordinator: TPLinkDataUpdateCoordinator, + *, + feature: Feature, + description: TPLinkFeatureEntityDescription, + parent: Device | None = None, + ) -> None: + """Initialize a select.""" + super().__init__( + device, coordinator, feature=feature, description=description, parent=parent + ) + self._attr_options = cast(list, self._feature.choices) + + @async_refresh_after + async def async_select_option(self, option: str) -> None: + """Update the current selected option.""" + await self._feature.set_value(option) + + @callback + def _async_update_attrs(self) -> None: + """Update the entity's attributes.""" + self._attr_current_option = self._feature.value diff --git a/homeassistant/components/tplink/sensor.py b/homeassistant/components/tplink/sensor.py index d7563dd0401..474ee6bfacf 100644 --- a/homeassistant/components/tplink/sensor.py +++ b/homeassistant/components/tplink/sensor.py @@ -1,11 +1,11 @@ -"""Support for TPLink HS100/HS110/HS200 smart switch energy sensors.""" +"""Support for TPLink sensor entities.""" from __future__ import annotations from dataclasses import dataclass from typing import cast -from kasa import SmartDevice +from kasa import Device, Feature from homeassistant.components.sensor import ( SensorDeviceClass, @@ -13,175 +13,164 @@ from homeassistant.components.sensor import ( SensorEntityDescription, SensorStateClass, ) -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - ATTR_VOLTAGE, - UnitOfElectricCurrent, - UnitOfElectricPotential, - UnitOfEnergy, - UnitOfPower, -) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import legacy_device_id -from .const import ( - ATTR_CURRENT_A, - ATTR_CURRENT_POWER_W, - ATTR_TODAY_ENERGY_KWH, - ATTR_TOTAL_ENERGY_KWH, - DOMAIN, -) +from . import TPLinkConfigEntry +from .const import UNIT_MAPPING from .coordinator import TPLinkDataUpdateCoordinator -from .entity import CoordinatedTPLinkEntity -from .models import TPLinkData +from .entity import CoordinatedTPLinkFeatureEntity, TPLinkFeatureEntityDescription -@dataclass(frozen=True) -class TPLinkSensorEntityDescription(SensorEntityDescription): - """Describes TPLink sensor entity.""" - - emeter_attr: str | None = None - precision: int | None = None +@dataclass(frozen=True, kw_only=True) +class TPLinkSensorEntityDescription( + SensorEntityDescription, TPLinkFeatureEntityDescription +): + """Base class for a TPLink feature based sensor entity description.""" -ENERGY_SENSORS: tuple[TPLinkSensorEntityDescription, ...] = ( +SENSOR_DESCRIPTIONS: tuple[TPLinkSensorEntityDescription, ...] = ( TPLinkSensorEntityDescription( - key=ATTR_CURRENT_POWER_W, - translation_key="current_consumption", - native_unit_of_measurement=UnitOfPower.WATT, + key="current_consumption", device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, - emeter_attr="power", - precision=1, ), TPLinkSensorEntityDescription( - key=ATTR_TOTAL_ENERGY_KWH, - translation_key="total_consumption", - native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + key="consumption_total", device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, - emeter_attr="total", - precision=3, ), TPLinkSensorEntityDescription( - key=ATTR_TODAY_ENERGY_KWH, - translation_key="today_consumption", - native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + key="consumption_today", device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, - precision=3, ), TPLinkSensorEntityDescription( - key=ATTR_VOLTAGE, - native_unit_of_measurement=UnitOfElectricPotential.VOLT, + key="consumption_this_month", + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + TPLinkSensorEntityDescription( + key="voltage", device_class=SensorDeviceClass.VOLTAGE, state_class=SensorStateClass.MEASUREMENT, - emeter_attr="voltage", - precision=1, ), TPLinkSensorEntityDescription( - key=ATTR_CURRENT_A, - native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, + key="current", device_class=SensorDeviceClass.CURRENT, state_class=SensorStateClass.MEASUREMENT, - emeter_attr="current", - precision=2, + ), + TPLinkSensorEntityDescription( + key="temperature", + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + ), + TPLinkSensorEntityDescription( + # Disable as the value reported by the device changes seconds frequently + entity_registry_enabled_default=False, + key="on_since", + device_class=SensorDeviceClass.TIMESTAMP, + ), + TPLinkSensorEntityDescription( + key="rssi", + device_class=SensorDeviceClass.SIGNAL_STRENGTH, + state_class=SensorStateClass.MEASUREMENT, + ), + TPLinkSensorEntityDescription( + key="signal_level", + state_class=SensorStateClass.MEASUREMENT, + ), + TPLinkSensorEntityDescription( + key="ssid", + ), + TPLinkSensorEntityDescription( + key="battery_level", + device_class=SensorDeviceClass.BATTERY, + state_class=SensorStateClass.MEASUREMENT, + ), + TPLinkSensorEntityDescription( + key="auto_off_at", + device_class=SensorDeviceClass.TIMESTAMP, + ), + TPLinkSensorEntityDescription( + key="device_time", + device_class=SensorDeviceClass.TIMESTAMP, + ), + TPLinkSensorEntityDescription( + key="humidity", + device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, + ), + TPLinkSensorEntityDescription( + key="report_interval", + device_class=SensorDeviceClass.DURATION, + ), + TPLinkSensorEntityDescription( + key="alarm_source", + ), + TPLinkSensorEntityDescription( + key="temperature", + device_class=SensorDeviceClass.TEMPERATURE, ), ) - -def async_emeter_from_device( - device: SmartDevice, description: TPLinkSensorEntityDescription -) -> float | None: - """Map a sensor key to the device attribute.""" - if attr := description.emeter_attr: - if (val := getattr(device.emeter_realtime, attr)) is None: - return None - return round(cast(float, val), description.precision) - - # ATTR_TODAY_ENERGY_KWH - if (emeter_today := device.emeter_today) is not None: - return round(cast(float, emeter_today), description.precision) - # today's consumption not available, when device was off all the day - # bulb's do not report this information, so filter it out - return None if device.is_bulb else 0.0 - - -def _async_sensors_for_device( - device: SmartDevice, - coordinator: TPLinkDataUpdateCoordinator, - has_parent: bool = False, -) -> list[SmartPlugSensor]: - """Generate the sensors for the device.""" - return [ - SmartPlugSensor(device, coordinator, description, has_parent) - for description in ENERGY_SENSORS - if async_emeter_from_device(device, description) is not None - ] +SENSOR_DESCRIPTIONS_MAP = {desc.key: desc for desc in SENSOR_DESCRIPTIONS} async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: TPLinkConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up sensors.""" - data: TPLinkData = hass.data[DOMAIN][config_entry.entry_id] + data = config_entry.runtime_data parent_coordinator = data.parent_coordinator children_coordinators = data.children_coordinators - entities: list[SmartPlugSensor] = [] - parent = parent_coordinator.device - if not parent.has_emeter: - return - - if parent.is_strip: - # Historically we only add the children if the device is a strip - for idx, child in enumerate(parent.children): - entities.extend( - _async_sensors_for_device(child, children_coordinators[idx], True) - ) - else: - entities.extend(_async_sensors_for_device(parent, parent_coordinator)) + device = parent_coordinator.device + entities = CoordinatedTPLinkFeatureEntity.entities_for_device_and_its_children( + device=device, + coordinator=parent_coordinator, + feature_type=Feature.Type.Sensor, + entity_class=TPLinkSensorEntity, + descriptions=SENSOR_DESCRIPTIONS_MAP, + child_coordinators=children_coordinators, + ) async_add_entities(entities) -class SmartPlugSensor(CoordinatedTPLinkEntity, SensorEntity): - """Representation of a TPLink Smart Plug energy sensor.""" +class TPLinkSensorEntity(CoordinatedTPLinkFeatureEntity, SensorEntity): + """Representation of a feature-based TPLink sensor.""" entity_description: TPLinkSensorEntityDescription def __init__( self, - device: SmartDevice, + device: Device, coordinator: TPLinkDataUpdateCoordinator, + *, + feature: Feature, description: TPLinkSensorEntityDescription, - has_parent: bool = False, + parent: Device | None = None, ) -> None: - """Initialize the switch.""" - super().__init__(device, coordinator) - self.entity_description = description - self._attr_unique_id = f"{legacy_device_id(device)}_{description.key}" - if has_parent: - assert device.alias - self._attr_translation_placeholders = {"device_name": device.alias} - if description.translation_key: - self._attr_translation_key = f"{description.translation_key}_child" - else: - assert description.device_class - self._attr_translation_key = f"{description.device_class.value}_child" - self._async_update_attrs() + """Initialize the sensor.""" + super().__init__( + device, coordinator, description=description, feature=feature, parent=parent + ) + self._async_call_update_attrs() @callback def _async_update_attrs(self) -> None: """Update the entity's attributes.""" - self._attr_native_value = async_emeter_from_device( - self.device, self.entity_description - ) + value = self._feature.value + if value is not None and self._feature.precision_hint is not None: + value = round(cast(float, value), self._feature.precision_hint) + # We probably do not need this, when we are rounding already? + self._attr_suggested_display_precision = self._feature.precision_hint - @callback - def _handle_coordinator_update(self) -> None: - """Handle updated data from the coordinator.""" - self._async_update_attrs() - super()._handle_coordinator_update() + self._attr_native_value = value + # Map to homeassistant units and fallback to upstream one if none found + if self._feature.unit is not None: + self._attr_native_unit_of_measurement = UNIT_MAPPING.get( + self._feature.unit, self._feature.unit + ) diff --git a/homeassistant/components/tplink/strings.json b/homeassistant/components/tplink/strings.json index c863df7c81c..34ce96612f5 100644 --- a/homeassistant/components/tplink/strings.json +++ b/homeassistant/components/tplink/strings.json @@ -59,35 +59,151 @@ } }, "entity": { + "binary_sensor": { + "humidity_warning": { + "name": "Humidity warning" + }, + "temperature_warning": { + "name": "Temperature warning" + }, + "overheated": { + "name": "Overheated" + }, + "battery_low": { + "name": "Battery low" + }, + "cloud_connection": { + "name": "Cloud connection" + }, + "update_available": { + "name": "[%key:component::binary_sensor::entity_component::update::name%]", + "state": { + "off": "[%key:component::binary_sensor::entity_component::update::state::off%]", + "on": "[%key:component::binary_sensor::entity_component::update::state::on%]" + } + }, + "is_open": { + "name": "[%key:component::binary_sensor::entity_component::door::name%]", + "state": { + "off": "[%key:common::state::closed%]", + "on": "[%key:common::state::open%]" + } + }, + "water_alert": { + "name": "[%key:component::binary_sensor::entity_component::moisture::name%]", + "state": { + "off": "[%key:component::binary_sensor::entity_component::moisture::state::off%]", + "on": "[%key:component::binary_sensor::entity_component::moisture::state::on%]" + } + } + }, + "button": { + "test_alarm": { + "name": "Test alarm" + }, + "stop_alarm": { + "name": "Stop alarm" + } + }, + "select": { + "light_preset": { + "name": "Light preset" + }, + "alarm_sound": { + "name": "Alarm sound" + }, + "alarm_volume": { + "name": "Alarm volume" + } + }, "sensor": { "current_consumption": { "name": "Current consumption" }, - "total_consumption": { + "consumption_total": { "name": "Total consumption" }, - "today_consumption": { + "consumption_today": { "name": "Today's consumption" }, - "current_consumption_child": { - "name": "{device_name} current consumption" + "consumption_this_month": { + "name": "This month's consumption" }, - "total_consumption_child": { - "name": "{device_name} total consumption" + "on_since": { + "name": "On since" }, - "today_consumption_child": { - "name": "{device_name} today's consumption" + "ssid": { + "name": "SSID" }, - "current_child": { - "name": "{device_name} current" + "signal_level": { + "name": "Signal level" }, - "voltage_child": { - "name": "{device_name} voltage" + "current_firmware_version": { + "name": "Current firmware version" + }, + "available_firmware_version": { + "name": "Available firmware version" + }, + "battery_level": { + "name": "Battery level" + }, + "temperature": { + "name": "[%key:component::sensor::entity_component::temperature::name%]" + }, + "voltage": { + "name": "[%key:component::sensor::entity_component::voltage::name%]" + }, + "current": { + "name": "[%key:component::sensor::entity_component::current::name%]" + }, + "humidity": { + "name": "[%key:component::sensor::entity_component::humidity::name%]" + }, + "device_time": { + "name": "Device time" + }, + "auto_off_at": { + "name": "Auto off at" + }, + "report_interval": { + "name": "Report interval" + }, + "alarm_source": { + "name": "Alarm source" + }, + "rssi": { + "name": "[%key:component::sensor::entity_component::signal_strength::name%]" } }, "switch": { "led": { "name": "LED" + }, + "auto_update_enabled": { + "name": "Auto update enabled" + }, + "auto_off_enabled": { + "name": "Auto off enabled" + }, + "smooth_transitions": { + "name": "Smooth transitions" + }, + "fan_sleep_mode": { + "name": "Fan sleep mode" + } + }, + "number": { + "smooth_transition_on": { + "name": "Smooth on" + }, + "smooth_transition_off": { + "name": "Smooth off" + }, + "auto_off_minutes": { + "name": "Turn off in" + }, + "temperature_offset": { + "name": "Temperature offset" } } }, diff --git a/homeassistant/components/tplink/switch.py b/homeassistant/components/tplink/switch.py index da3dda9c041..2520de9dd3e 100644 --- a/homeassistant/components/tplink/switch.py +++ b/homeassistant/components/tplink/switch.py @@ -1,158 +1,112 @@ -"""Support for TPLink HS100/HS110/HS200 smart switch.""" +"""Support for TPLink switch entities.""" from __future__ import annotations +from dataclasses import dataclass import logging -from typing import Any, cast +from typing import Any -from kasa import SmartDevice, SmartPlug +from kasa import Device, Feature -from homeassistant.components.switch import SwitchEntity -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import EntityCategory +from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import legacy_device_id -from .const import DOMAIN +from . import TPLinkConfigEntry from .coordinator import TPLinkDataUpdateCoordinator -from .entity import CoordinatedTPLinkEntity, async_refresh_after -from .models import TPLinkData +from .entity import ( + CoordinatedTPLinkFeatureEntity, + TPLinkFeatureEntityDescription, + async_refresh_after, +) _LOGGER = logging.getLogger(__name__) +@dataclass(frozen=True, kw_only=True) +class TPLinkSwitchEntityDescription( + SwitchEntityDescription, TPLinkFeatureEntityDescription +): + """Base class for a TPLink feature based sensor entity description.""" + + +SWITCH_DESCRIPTIONS: tuple[TPLinkSwitchEntityDescription, ...] = ( + TPLinkSwitchEntityDescription( + key="state", + ), + TPLinkSwitchEntityDescription( + key="led", + ), + TPLinkSwitchEntityDescription( + key="auto_update_enabled", + ), + TPLinkSwitchEntityDescription( + key="auto_off_enabled", + ), + TPLinkSwitchEntityDescription( + key="smooth_transitions", + ), + TPLinkSwitchEntityDescription( + key="fan_sleep_mode", + ), +) + +SWITCH_DESCRIPTIONS_MAP = {desc.key: desc for desc in SWITCH_DESCRIPTIONS} + + async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: TPLinkConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up switches.""" - data: TPLinkData = hass.data[DOMAIN][config_entry.entry_id] + data = config_entry.runtime_data parent_coordinator = data.parent_coordinator - device = cast(SmartPlug, parent_coordinator.device) - if not device.is_plug and not device.is_strip and not device.is_dimmer: - return - entities: list = [] - if device.is_strip: - # Historically we only add the children if the device is a strip - _LOGGER.debug("Initializing strip with %s sockets", len(device.children)) - entities.extend( - SmartPlugSwitchChild(device, parent_coordinator, child) - for child in device.children - ) - elif device.is_plug: - entities.append(SmartPlugSwitch(device, parent_coordinator)) + device = parent_coordinator.device - # this will be removed on the led is implemented - if hasattr(device, "led"): - entities.append(SmartPlugLedSwitch(device, parent_coordinator)) + entities = CoordinatedTPLinkFeatureEntity.entities_for_device_and_its_children( + device, + coordinator=parent_coordinator, + feature_type=Feature.Switch, + entity_class=TPLinkSwitch, + descriptions=SWITCH_DESCRIPTIONS_MAP, + ) async_add_entities(entities) -class SmartPlugLedSwitch(CoordinatedTPLinkEntity, SwitchEntity): - """Representation of switch for the LED of a TPLink Smart Plug.""" +class TPLinkSwitch(CoordinatedTPLinkFeatureEntity, SwitchEntity): + """Representation of a feature-based TPLink switch.""" - device: SmartPlug - - _attr_translation_key = "led" - _attr_entity_category = EntityCategory.CONFIG - - def __init__( - self, device: SmartPlug, coordinator: TPLinkDataUpdateCoordinator - ) -> None: - """Initialize the LED switch.""" - super().__init__(device, coordinator) - self._attr_unique_id = f"{device.mac}_led" - self._async_update_attrs() - - @async_refresh_after - async def async_turn_on(self, **kwargs: Any) -> None: - """Turn the LED switch on.""" - await self.device.set_led(True) - - @async_refresh_after - async def async_turn_off(self, **kwargs: Any) -> None: - """Turn the LED switch off.""" - await self.device.set_led(False) - - @callback - def _async_update_attrs(self) -> None: - """Update the entity's attributes.""" - self._attr_is_on = self.device.led - - @callback - def _handle_coordinator_update(self) -> None: - """Handle updated data from the coordinator.""" - self._async_update_attrs() - super()._handle_coordinator_update() - - -class SmartPlugSwitch(CoordinatedTPLinkEntity, SwitchEntity): - """Representation of a TPLink Smart Plug switch.""" - - _attr_name: str | None = None + entity_description: TPLinkSwitchEntityDescription def __init__( self, - device: SmartDevice, + device: Device, coordinator: TPLinkDataUpdateCoordinator, + *, + feature: Feature, + description: TPLinkSwitchEntityDescription, + parent: Device | None = None, ) -> None: """Initialize the switch.""" - super().__init__(device, coordinator) - # For backwards compat with pyHS100 - self._attr_unique_id = legacy_device_id(device) - self._async_update_attrs() + super().__init__( + device, coordinator, description=description, feature=feature, parent=parent + ) + + self._async_call_update_attrs() @async_refresh_after async def async_turn_on(self, **kwargs: Any) -> None: """Turn the switch on.""" - await self.device.turn_on() + await self._feature.set_value(True) @async_refresh_after async def async_turn_off(self, **kwargs: Any) -> None: """Turn the switch off.""" - await self.device.turn_off() + await self._feature.set_value(False) @callback def _async_update_attrs(self) -> None: """Update the entity's attributes.""" - self._attr_is_on = self.device.is_on - - @callback - def _handle_coordinator_update(self) -> None: - """Handle updated data from the coordinator.""" - self._async_update_attrs() - super()._handle_coordinator_update() - - -class SmartPlugSwitchChild(SmartPlugSwitch): - """Representation of an individual plug of a TPLink Smart Plug strip.""" - - def __init__( - self, - device: SmartDevice, - coordinator: TPLinkDataUpdateCoordinator, - plug: SmartDevice, - ) -> None: - """Initialize the child switch.""" - self._plug = plug - super().__init__(device, coordinator) - self._attr_unique_id = legacy_device_id(plug) - self._attr_name = plug.alias - - @async_refresh_after - async def async_turn_on(self, **kwargs: Any) -> None: - """Turn the child switch on.""" - await self._plug.turn_on() - - @async_refresh_after - async def async_turn_off(self, **kwargs: Any) -> None: - """Turn the child switch off.""" - await self._plug.turn_off() - - @callback - def _async_update_attrs(self) -> None: - """Update the entity's attributes.""" - self._attr_is_on = self._plug.is_on + self._attr_is_on = self._feature.value diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index 3b5fe9843f2..e898f64d128 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -650,6 +650,11 @@ DHCP: Final[list[dict[str, str | bool]]] = [ "hostname": "k[lps]*", "macaddress": "5091E3*", }, + { + "domain": "tplink", + "hostname": "p1*", + "macaddress": "5091E3*", + }, { "domain": "tplink", "hostname": "k[lps]*", @@ -870,16 +875,31 @@ DHCP: Final[list[dict[str, str | bool]]] = [ "hostname": "s5*", "macaddress": "3C52A1*", }, + { + "domain": "tplink", + "hostname": "h1*", + "macaddress": "3C52A1*", + }, { "domain": "tplink", "hostname": "l9*", "macaddress": "A842A1*", }, + { + "domain": "tplink", + "hostname": "p1*", + "macaddress": "A842A1*", + }, { "domain": "tplink", "hostname": "l9*", "macaddress": "3460F9*", }, + { + "domain": "tplink", + "hostname": "p1*", + "macaddress": "3460F9*", + }, { "domain": "tplink", "hostname": "hs*", @@ -890,6 +910,11 @@ DHCP: Final[list[dict[str, str | bool]]] = [ "hostname": "k[lps]*", "macaddress": "74DA88*", }, + { + "domain": "tplink", + "hostname": "p1*", + "macaddress": "74DA88*", + }, { "domain": "tplink", "hostname": "p3*", @@ -930,6 +955,16 @@ DHCP: Final[list[dict[str, str | bool]]] = [ "hostname": "l9*", "macaddress": "F0A731*", }, + { + "domain": "tplink", + "hostname": "ks2*", + "macaddress": "F0A731*", + }, + { + "domain": "tplink", + "hostname": "kh1*", + "macaddress": "F0A731*", + }, { "domain": "tuya", "macaddress": "105A17*", diff --git a/requirements_all.txt b/requirements_all.txt index b75555283a2..de167c2f7e1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2278,7 +2278,7 @@ python-join-api==0.0.9 python-juicenet==1.1.0 # homeassistant.components.tplink -python-kasa[speedups]==0.6.2.1 +python-kasa[speedups]==0.7.0.1 # homeassistant.components.lirc # python-lirc==1.2.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 37b3700372b..8eb468b0947 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1778,7 +1778,7 @@ python-izone==1.2.9 python-juicenet==1.1.0 # homeassistant.components.tplink -python-kasa[speedups]==0.6.2.1 +python-kasa[speedups]==0.7.0.1 # homeassistant.components.matter python-matter-server==6.1.0 diff --git a/tests/components/tplink/__init__.py b/tests/components/tplink/__init__.py index d1454d12e68..9c8aeb99be1 100644 --- a/tests/components/tplink/__init__.py +++ b/tests/components/tplink/__init__.py @@ -1,21 +1,24 @@ """Tests for the TP-Link component.""" +from collections import namedtuple +from datetime import datetime +from typing import Any from unittest.mock import AsyncMock, MagicMock, patch from kasa import ( - ConnectionType, + Device, DeviceConfig, - DeviceFamilyType, - EncryptType, - SmartBulb, - SmartDevice, - SmartDimmer, - SmartLightStrip, - SmartPlug, - SmartStrip, + DeviceConnectionParameters, + DeviceEncryptionType, + DeviceFamily, + DeviceType, + Feature, + KasaException, + Module, ) -from kasa.exceptions import SmartDeviceException +from kasa.interfaces import Fan, Light, LightEffect, LightState from kasa.protocol import BaseProtocol +from syrupy import SnapshotAssertion from homeassistant.components.tplink import ( CONF_ALIAS, @@ -25,9 +28,17 @@ from homeassistant.components.tplink import ( Credentials, ) from homeassistant.components.tplink.const import DOMAIN +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers.translation import async_get_translations +from homeassistant.helpers.typing import UNDEFINED +from homeassistant.setup import async_setup_component -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, load_json_value_fixture + +ColorTempRange = namedtuple("ColorTempRange", ["min", "max"]) MODULE = "homeassistant.components.tplink" MODULE_CONFIG_FLOW = "homeassistant.components.tplink.config_flow" @@ -36,6 +47,7 @@ IP_ADDRESS2 = "127.0.0.2" ALIAS = "My Bulb" MODEL = "HS100" MAC_ADDRESS = "aa:bb:cc:dd:ee:ff" +DEVICE_ID = "123456789ABCDEFGH" DHCP_FORMATTED_MAC_ADDRESS = MAC_ADDRESS.replace(":", "") MAC_ADDRESS2 = "11:22:33:44:55:66" DEFAULT_ENTRY_TITLE = f"{ALIAS} {MODEL}" @@ -49,16 +61,16 @@ CREDENTIALS_HASH_AUTH = "abcdefghijklmnopqrstuv==" DEVICE_CONFIG_AUTH = DeviceConfig( IP_ADDRESS, credentials=CREDENTIALS, - connection_type=ConnectionType( - DeviceFamilyType.IotSmartPlugSwitch, EncryptType.Klap + connection_type=DeviceConnectionParameters( + DeviceFamily.IotSmartPlugSwitch, DeviceEncryptionType.Klap ), uses_http=True, ) DEVICE_CONFIG_AUTH2 = DeviceConfig( IP_ADDRESS2, credentials=CREDENTIALS, - connection_type=ConnectionType( - DeviceFamilyType.IotSmartPlugSwitch, EncryptType.Klap + connection_type=DeviceConnectionParameters( + DeviceFamily.IotSmartPlugSwitch, DeviceEncryptionType.Klap ), uses_http=True, ) @@ -90,190 +102,316 @@ CREATE_ENTRY_DATA_AUTH2 = { } +def _load_feature_fixtures(): + fixtures = load_json_value_fixture("features.json", DOMAIN) + for fixture in fixtures.values(): + if isinstance(fixture["value"], str): + try: + time = datetime.strptime(fixture["value"], "%Y-%m-%d %H:%M:%S.%f%z") + fixture["value"] = time + except ValueError: + pass + return fixtures + + +FEATURES_FIXTURE = _load_feature_fixtures() + + +async def setup_platform_for_device( + hass: HomeAssistant, config_entry: ConfigEntry, platform: Platform, device: Device +): + """Set up a single tplink platform with a device.""" + config_entry.add_to_hass(hass) + + with ( + patch("homeassistant.components.tplink.PLATFORMS", [platform]), + _patch_discovery(device=device), + _patch_connect(device=device), + ): + await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + # Good practice to wait background tasks in tests see PR #112726 + await hass.async_block_till_done(wait_background_tasks=True) + + +async def snapshot_platform( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + device_registry: dr.DeviceRegistry, + snapshot: SnapshotAssertion, + config_entry_id: str, +) -> None: + """Snapshot a platform.""" + device_entries = dr.async_entries_for_config_entry(device_registry, config_entry_id) + assert device_entries + for device_entry in device_entries: + assert device_entry == snapshot( + name=f"{device_entry.name}-entry" + ), f"device entry snapshot failed for {device_entry.name}" + + entity_entries = er.async_entries_for_config_entry(entity_registry, config_entry_id) + assert entity_entries + assert ( + len({entity_entry.domain for entity_entry in entity_entries}) == 1 + ), "Please limit the loaded platforms to 1 platform." + + translations = await async_get_translations(hass, "en", "entity", [DOMAIN]) + for entity_entry in entity_entries: + if entity_entry.translation_key: + key = f"component.{DOMAIN}.entity.{entity_entry.domain}.{entity_entry.translation_key}.name" + assert ( + key in translations + ), f"No translation for entity {entity_entry.unique_id}, expected {key}" + assert entity_entry == snapshot( + name=f"{entity_entry.entity_id}-entry" + ), f"entity entry snapshot failed for {entity_entry.entity_id}" + if entity_entry.disabled_by is None: + state = hass.states.get(entity_entry.entity_id) + assert state, f"State not found for {entity_entry.entity_id}" + assert state == snapshot( + name=f"{entity_entry.entity_id}-state" + ), f"state snapshot failed for {entity_entry.entity_id}" + + def _mock_protocol() -> BaseProtocol: - protocol = MagicMock(auto_spec=BaseProtocol) + protocol = MagicMock(spec=BaseProtocol) protocol.close = AsyncMock() return protocol -def _mocked_bulb( +def _mocked_device( device_config=DEVICE_CONFIG_LEGACY, credentials_hash=CREDENTIALS_HASH_LEGACY, mac=MAC_ADDRESS, + device_id=DEVICE_ID, alias=ALIAS, -) -> SmartBulb: - bulb = MagicMock(auto_spec=SmartBulb, name="Mocked bulb") - bulb.update = AsyncMock() - bulb.mac = mac - bulb.alias = alias - bulb.model = MODEL - bulb.host = IP_ADDRESS - bulb.brightness = 50 - bulb.color_temp = 4000 - bulb.is_color = True - bulb.is_strip = False - bulb.is_plug = False - bulb.is_dimmer = False - bulb.is_light_strip = False - bulb.has_effects = False - bulb.effect = None - bulb.effect_list = None - bulb.hsv = (10, 30, 5) - bulb.device_id = mac - bulb.valid_temperature_range.min = 4000 - bulb.valid_temperature_range.max = 9000 - bulb.hw_info = {"sw_ver": "1.0.0", "hw_ver": "1.0.0"} - bulb.turn_off = AsyncMock() - bulb.turn_on = AsyncMock() - bulb.set_brightness = AsyncMock() - bulb.set_hsv = AsyncMock() - bulb.set_color_temp = AsyncMock() - bulb.protocol = _mock_protocol() - bulb.config = device_config - bulb.credentials_hash = credentials_hash - return bulb + model=MODEL, + ip_address=IP_ADDRESS, + modules: list[str] | None = None, + children: list[Device] | None = None, + features: list[str | Feature] | None = None, + device_type=None, + spec: type = Device, +) -> Device: + device = MagicMock(spec=spec, name="Mocked device") + device.update = AsyncMock() + device.turn_off = AsyncMock() + device.turn_on = AsyncMock() + + device.mac = mac + device.alias = alias + device.model = model + device.host = ip_address + device.device_id = device_id + device.hw_info = {"sw_ver": "1.0.0", "hw_ver": "1.0.0"} + device.modules = {} + device.features = {} + + if modules: + device.modules = { + module_name: MODULE_TO_MOCK_GEN[module_name]() for module_name in modules + } + + if features: + device.features = { + feature_id: _mocked_feature(feature_id, require_fixture=True) + for feature_id in features + if isinstance(feature_id, str) + } + + device.features.update( + { + feature.id: feature + for feature in features + if isinstance(feature, Feature) + } + ) + device.children = [] + if children: + for child in children: + child.mac = mac + device.children = children + device.device_type = device_type if device_type else DeviceType.Unknown + if ( + not device_type + and device.children + and all( + child.device_type is DeviceType.StripSocket for child in device.children + ) + ): + device.device_type = DeviceType.Strip + + device.protocol = _mock_protocol() + device.config = device_config + device.credentials_hash = credentials_hash + return device -class MockedSmartLightStrip(SmartLightStrip): - """Mock a SmartLightStrip.""" +def _mocked_feature( + id: str, + *, + require_fixture=False, + value: Any = UNDEFINED, + name=None, + type_=None, + category=None, + precision_hint=None, + choices=None, + unit=None, + minimum_value=0, + maximum_value=2**16, # Arbitrary max +) -> Feature: + """Get a mocked feature. - def __new__(cls, *args, **kwargs): - """Mock a SmartLightStrip that will pass an isinstance check.""" - return MagicMock(spec=cls) + If kwargs are provided they will override the attributes for any features defined in fixtures.json + """ + feature = MagicMock(spec=Feature, name=f"Mocked {id} feature") + feature.id = id + feature.name = name or id.upper() + feature.set_value = AsyncMock() + if not (fixture := FEATURES_FIXTURE.get(id)): + assert ( + require_fixture is False + ), f"No fixture defined for feature {id} and require_fixture is True" + assert ( + value is not UNDEFINED + ), f"Value must be provided if feature {id} not defined in features.json" + fixture = {"value": value, "category": "Primary", "type": "Sensor"} + elif value is not UNDEFINED: + fixture["value"] = value + feature.value = fixture["value"] + + feature.type = type_ or Feature.Type[fixture["type"]] + feature.category = category or Feature.Category[fixture["category"]] + + # sensor + feature.precision_hint = precision_hint or fixture.get("precision_hint") + feature.unit = unit or fixture.get("unit") + + # number + feature.minimum_value = minimum_value or fixture.get("minimum_value") + feature.maximum_value = maximum_value or fixture.get("maximum_value") + + # select + feature.choices = choices or fixture.get("choices") + return feature -def _mocked_smart_light_strip() -> SmartLightStrip: - strip = MockedSmartLightStrip() - strip.update = AsyncMock() - strip.mac = MAC_ADDRESS - strip.alias = ALIAS - strip.model = MODEL - strip.host = IP_ADDRESS - strip.brightness = 50 - strip.color_temp = 4000 - strip.is_color = True - strip.is_strip = False - strip.is_plug = False - strip.is_dimmer = False - strip.is_light_strip = True - strip.has_effects = True - strip.effect = {"name": "Effect1", "enable": 1} - strip.effect_list = ["Effect1", "Effect2"] - strip.hsv = (10, 30, 5) - strip.device_id = MAC_ADDRESS - strip.valid_temperature_range.min = 4000 - strip.valid_temperature_range.max = 9000 - strip.hw_info = {"sw_ver": "1.0.0", "hw_ver": "1.0.0"} - strip.turn_off = AsyncMock() - strip.turn_on = AsyncMock() - strip.set_brightness = AsyncMock() - strip.set_hsv = AsyncMock() - strip.set_color_temp = AsyncMock() - strip.set_effect = AsyncMock() - strip.set_custom_effect = AsyncMock() - strip.protocol = _mock_protocol() - strip.config = DEVICE_CONFIG_LEGACY - strip.credentials_hash = CREDENTIALS_HASH_LEGACY - return strip +def _mocked_light_module() -> Light: + light = MagicMock(spec=Light, name="Mocked light module") + light.update = AsyncMock() + light.brightness = 50 + light.color_temp = 4000 + light.state = LightState( + light_on=True, brightness=light.brightness, color_temp=light.color_temp + ) + light.is_color = True + light.is_variable_color_temp = True + light.is_dimmable = True + light.is_brightness = True + light.has_effects = False + light.hsv = (10, 30, 5) + light.valid_temperature_range = ColorTempRange(min=4000, max=9000) + light.hw_info = {"sw_ver": "1.0.0", "hw_ver": "1.0.0"} + light.set_state = AsyncMock() + light.set_brightness = AsyncMock() + light.set_hsv = AsyncMock() + light.set_color_temp = AsyncMock() + light.protocol = _mock_protocol() + return light -def _mocked_dimmer() -> SmartDimmer: - dimmer = MagicMock(auto_spec=SmartDimmer, name="Mocked dimmer") - dimmer.update = AsyncMock() - dimmer.mac = MAC_ADDRESS - dimmer.alias = "My Dimmer" - dimmer.model = MODEL - dimmer.host = IP_ADDRESS - dimmer.brightness = 50 - dimmer.color_temp = 4000 - dimmer.is_color = True - dimmer.is_strip = False - dimmer.is_plug = False - dimmer.is_dimmer = True - dimmer.is_light_strip = False - dimmer.effect = None - dimmer.effect_list = None - dimmer.hsv = (10, 30, 5) - dimmer.device_id = MAC_ADDRESS - dimmer.valid_temperature_range.min = 4000 - dimmer.valid_temperature_range.max = 9000 - dimmer.hw_info = {"sw_ver": "1.0.0", "hw_ver": "1.0.0"} - dimmer.turn_off = AsyncMock() - dimmer.turn_on = AsyncMock() - dimmer.set_brightness = AsyncMock() - dimmer.set_hsv = AsyncMock() - dimmer.set_color_temp = AsyncMock() - dimmer.set_led = AsyncMock() - dimmer.protocol = _mock_protocol() - dimmer.config = DEVICE_CONFIG_LEGACY - dimmer.credentials_hash = CREDENTIALS_HASH_LEGACY - return dimmer +def _mocked_light_effect_module() -> LightEffect: + effect = MagicMock(spec=LightEffect, name="Mocked light effect") + effect.has_effects = True + effect.has_custom_effects = True + effect.effect = "Effect1" + effect.effect_list = ["Off", "Effect1", "Effect2"] + effect.set_effect = AsyncMock() + effect.set_custom_effect = AsyncMock() + return effect -def _mocked_plug() -> SmartPlug: - plug = MagicMock(auto_spec=SmartPlug, name="Mocked plug") - plug.update = AsyncMock() - plug.mac = MAC_ADDRESS - plug.alias = "My Plug" - plug.model = MODEL - plug.host = IP_ADDRESS - plug.is_light_strip = False - plug.is_bulb = False - plug.is_dimmer = False - plug.is_strip = False - plug.is_plug = True - plug.device_id = MAC_ADDRESS - plug.hw_info = {"sw_ver": "1.0.0", "hw_ver": "1.0.0"} - plug.turn_off = AsyncMock() - plug.turn_on = AsyncMock() - plug.set_led = AsyncMock() - plug.protocol = _mock_protocol() - plug.config = DEVICE_CONFIG_LEGACY - plug.credentials_hash = CREDENTIALS_HASH_LEGACY - return plug +def _mocked_fan_module() -> Fan: + fan = MagicMock(auto_spec=Fan, name="Mocked fan") + fan.fan_speed_level = 0 + fan.set_fan_speed_level = AsyncMock() + return fan -def _mocked_strip() -> SmartStrip: - strip = MagicMock(auto_spec=SmartStrip, name="Mocked strip") - strip.update = AsyncMock() - strip.mac = MAC_ADDRESS - strip.alias = "My Strip" - strip.model = MODEL - strip.host = IP_ADDRESS - strip.is_light_strip = False - strip.is_bulb = False - strip.is_dimmer = False - strip.is_strip = True - strip.is_plug = True - strip.device_id = MAC_ADDRESS - strip.hw_info = {"sw_ver": "1.0.0", "hw_ver": "1.0.0"} - strip.turn_off = AsyncMock() - strip.turn_on = AsyncMock() - strip.set_led = AsyncMock() - strip.protocol = _mock_protocol() - strip.config = DEVICE_CONFIG_LEGACY - strip.credentials_hash = CREDENTIALS_HASH_LEGACY - plug0 = _mocked_plug() - plug0.alias = "Plug0" - plug0.device_id = "bb:bb:cc:dd:ee:ff_PLUG0DEVICEID" - plug0.mac = "bb:bb:cc:dd:ee:ff" +def _mocked_strip_children(features=None, alias=None) -> list[Device]: + plug0 = _mocked_device( + alias="Plug0" if alias is None else alias, + device_id="bb:bb:cc:dd:ee:ff_PLUG0DEVICEID", + mac="bb:bb:cc:dd:ee:ff", + device_type=DeviceType.StripSocket, + features=features, + ) + plug1 = _mocked_device( + alias="Plug1" if alias is None else alias, + device_id="cc:bb:cc:dd:ee:ff_PLUG1DEVICEID", + mac="cc:bb:cc:dd:ee:ff", + device_type=DeviceType.StripSocket, + features=features, + ) plug0.is_on = True - plug0.protocol = _mock_protocol() - plug1 = _mocked_plug() - plug1.device_id = "cc:bb:cc:dd:ee:ff_PLUG1DEVICEID" - plug1.mac = "cc:bb:cc:dd:ee:ff" - plug1.alias = "Plug1" - plug1.protocol = _mock_protocol() plug1.is_on = False - strip.children = [plug0, plug1] - return strip + return [plug0, plug1] + + +def _mocked_energy_features( + power=None, total=None, voltage=None, current=None, today=None +) -> list[Feature]: + feats = [] + if power is not None: + feats.append( + _mocked_feature( + "current_consumption", + value=power, + ) + ) + if total is not None: + feats.append( + _mocked_feature( + "consumption_total", + value=total, + ) + ) + if voltage is not None: + feats.append( + _mocked_feature( + "voltage", + value=voltage, + ) + ) + if current is not None: + feats.append( + _mocked_feature( + "current", + value=current, + ) + ) + # Today is always reported as 0 by the library rather than none + feats.append( + _mocked_feature( + "consumption_today", + value=today if today is not None else 0.0, + ) + ) + return feats + + +MODULE_TO_MOCK_GEN = { + Module.Light: _mocked_light_module, + Module.LightEffect: _mocked_light_effect_module, + Module.Fan: _mocked_fan_module, +} def _patch_discovery(device=None, no_device=False): async def _discovery(*args, **kwargs): if no_device: return {} - return {IP_ADDRESS: _mocked_bulb()} + return {IP_ADDRESS: _mocked_device()} return patch("homeassistant.components.tplink.Discover.discover", new=_discovery) @@ -281,8 +419,8 @@ def _patch_discovery(device=None, no_device=False): def _patch_single_discovery(device=None, no_device=False): async def _discover_single(*args, **kwargs): if no_device: - raise SmartDeviceException - return device if device else _mocked_bulb() + raise KasaException + return device if device else _mocked_device() return patch( "homeassistant.components.tplink.Discover.discover_single", new=_discover_single @@ -292,14 +430,14 @@ def _patch_single_discovery(device=None, no_device=False): def _patch_connect(device=None, no_device=False): async def _connect(*args, **kwargs): if no_device: - raise SmartDeviceException - return device if device else _mocked_bulb() + raise KasaException + return device if device else _mocked_device() - return patch("homeassistant.components.tplink.SmartDevice.connect", new=_connect) + return patch("homeassistant.components.tplink.Device.connect", new=_connect) async def initialize_config_entry_for_device( - hass: HomeAssistant, dev: SmartDevice + hass: HomeAssistant, dev: Device ) -> MockConfigEntry: """Create a mocked configuration entry for the given device. diff --git a/tests/components/tplink/conftest.py b/tests/components/tplink/conftest.py index 88da9b699a7..f8d933de71e 100644 --- a/tests/components/tplink/conftest.py +++ b/tests/components/tplink/conftest.py @@ -17,7 +17,7 @@ from . import ( IP_ADDRESS2, MAC_ADDRESS, MAC_ADDRESS2, - _mocked_bulb, + _mocked_device, ) from tests.common import MockConfigEntry, mock_device_registry, mock_registry @@ -31,13 +31,13 @@ def mock_discovery(): discover=DEFAULT, discover_single=DEFAULT, ) as mock_discovery: - device = _mocked_bulb( + device = _mocked_device( device_config=copy.deepcopy(DEVICE_CONFIG_AUTH), credentials_hash=CREDENTIALS_HASH_AUTH, alias=None, ) devices = { - "127.0.0.1": _mocked_bulb( + "127.0.0.1": _mocked_device( device_config=copy.deepcopy(DEVICE_CONFIG_AUTH), credentials_hash=CREDENTIALS_HASH_AUTH, alias=None, @@ -52,12 +52,12 @@ def mock_discovery(): @pytest.fixture def mock_connect(): """Mock python-kasa connect.""" - with patch("homeassistant.components.tplink.SmartDevice.connect") as mock_connect: + with patch("homeassistant.components.tplink.Device.connect") as mock_connect: devices = { - IP_ADDRESS: _mocked_bulb( + IP_ADDRESS: _mocked_device( device_config=DEVICE_CONFIG_AUTH, credentials_hash=CREDENTIALS_HASH_AUTH ), - IP_ADDRESS2: _mocked_bulb( + IP_ADDRESS2: _mocked_device( device_config=DEVICE_CONFIG_AUTH, credentials_hash=CREDENTIALS_HASH_AUTH, mac=MAC_ADDRESS2, diff --git a/tests/components/tplink/fixtures/features.json b/tests/components/tplink/fixtures/features.json new file mode 100644 index 00000000000..daf86a74643 --- /dev/null +++ b/tests/components/tplink/fixtures/features.json @@ -0,0 +1,287 @@ +{ + "state": { + "value": true, + "type": "Switch", + "category": "Primary" + }, + "led": { + "value": true, + "type": "Switch", + "category": "Config" + }, + "auto_update_enabled": { + "value": true, + "type": "Switch", + "category": "Config" + }, + "auto_off_enabled": { + "value": true, + "type": "Switch", + "category": "Config" + }, + "smooth_transitions": { + "value": true, + "type": "Switch", + "category": "Config" + }, + "frost_protection_enabled": { + "value": true, + "type": "Switch", + "category": "Config" + }, + "fan_sleep_mode": { + "value": false, + "type": "Switch", + "category": "Config" + }, + "current_consumption": { + "value": 5.23, + "type": "Sensor", + "category": "Primary", + "unit": "W", + "precision_hint": 1 + }, + "consumption_today": { + "value": 5.23, + "type": "Sensor", + "category": "Info", + "unit": "kWh", + "precision_hint": 3 + }, + "consumption_this_month": { + "value": 15.345, + "type": "Sensor", + "category": "Info", + "unit": "kWh", + "precision_hint": 3 + }, + "consumption_total": { + "value": 30.0049, + "type": "Sensor", + "category": "Info", + "unit": "kWh", + "precision_hint": 3 + }, + "current": { + "value": 5.035, + "type": "Sensor", + "category": "Primary", + "unit": "A", + "precision_hint": 2 + }, + "voltage": { + "value": 121.1, + "type": "Sensor", + "category": "Primary", + "unit": "v", + "precision_hint": 1 + }, + "device_id": { + "value": "94hd2dn298812je12u0931828", + "type": "Sensor", + "category": "Debug" + }, + "signal_level": { + "value": 2, + "type": "Sensor", + "category": "Info" + }, + "rssi": { + "value": -62, + "type": "Sensor", + "category": "Debug" + }, + "ssid": { + "value": "HOMEWIFI", + "type": "Sensor", + "category": "Debug" + }, + "on_since": { + "value": "2024-06-24 10:03:11.046643+01:00", + "type": "Sensor", + "category": "Debug" + }, + "battery_level": { + "value": 85, + "type": "Sensor", + "category": "Info", + "unit": "%" + }, + "auto_off_at": { + "value": "2024-06-24 10:03:11.046643+01:00", + "type": "Sensor", + "category": "Info" + }, + "humidity": { + "value": 12, + "type": "Sensor", + "category": "Primary", + "unit": "%" + }, + "report_interval": { + "value": 16, + "type": "Sensor", + "category": "Debug", + "unit": "%" + }, + "alarm_source": { + "value": "", + "type": "Sensor", + "category": "Debug" + }, + "device_time": { + "value": "2024-06-24 10:03:11.046643+01:00", + "type": "Sensor", + "category": "Debug" + }, + "temperature": { + "value": 19.2, + "type": "Sensor", + "category": "Debug", + "unit": "celsius" + }, + "current_firmware_version": { + "value": "1.1.2", + "type": "Sensor", + "category": "Debug" + }, + "available_firmware_version": { + "value": "1.1.3", + "type": "Sensor", + "category": "Debug" + }, + "thermostat_mode": { + "value": "off", + "type": "Sensor", + "category": "Primary" + }, + "overheated": { + "value": false, + "type": "BinarySensor", + "category": "Info" + }, + "battery_low": { + "value": false, + "type": "BinarySensor", + "category": "Debug" + }, + "update_available": { + "value": false, + "type": "BinarySensor", + "category": "Info" + }, + "cloud_connection": { + "value": false, + "type": "BinarySensor", + "category": "Info" + }, + "temperature_warning": { + "value": false, + "type": "BinarySensor", + "category": "Debug" + }, + "humidity_warning": { + "value": false, + "type": "BinarySensor", + "category": "Debug" + }, + "water_alert": { + "value": false, + "type": "BinarySensor", + "category": "Primary" + }, + "is_open": { + "value": false, + "type": "BinarySensor", + "category": "Primary" + }, + "test_alarm": { + "value": "", + "type": "Action", + "category": "Config" + }, + "stop_alarm": { + "value": "", + "type": "Action", + "category": "Config" + }, + "smooth_transition_on": { + "value": false, + "type": "Number", + "category": "Config", + "minimum_value": 0, + "maximum_value": 60 + }, + "smooth_transition_off": { + "value": false, + "type": "Number", + "category": "Config", + "minimum_value": 0, + "maximum_value": 60 + }, + "auto_off_minutes": { + "value": false, + "type": "Number", + "category": "Config", + "unit": "min", + "minimum_value": 0, + "maximum_value": 60 + }, + "temperature_offset": { + "value": false, + "type": "Number", + "category": "Config", + "minimum_value": -10, + "maximum_value": 10 + }, + "target_temperature": { + "value": false, + "type": "Number", + "category": "Primary" + }, + "fan_speed_level": { + "value": 2, + "type": "Number", + "category": "Primary", + "minimum_value": 0, + "maximum_value": 4 + }, + "light_preset": { + "value": "Off", + "type": "Choice", + "category": "Config", + "choices": ["Off", "Preset 1", "Preset 2"] + }, + "alarm_sound": { + "value": "Phone Ring", + "type": "Choice", + "category": "Config", + "choices": [ + "Doorbell Ring 1", + "Doorbell Ring 2", + "Doorbell Ring 3", + "Doorbell Ring 4", + "Doorbell Ring 5", + "Doorbell Ring 6", + "Doorbell Ring 7", + "Doorbell Ring 8", + "Doorbell Ring 9", + "Doorbell Ring 10", + "Phone Ring", + "Alarm 1", + "Alarm 2", + "Alarm 3", + "Alarm 4", + "Dripping Tap", + "Alarm 5", + "Connection 1", + "Connection 2" + ] + }, + "alarm_volume": { + "value": "normal", + "type": "Choice", + "category": "Config", + "choices": ["low", "normal", "high"] + } +} diff --git a/tests/components/tplink/snapshots/test_binary_sensor.ambr b/tests/components/tplink/snapshots/test_binary_sensor.ambr new file mode 100644 index 00000000000..27b1372df27 --- /dev/null +++ b/tests/components/tplink/snapshots/test_binary_sensor.ambr @@ -0,0 +1,369 @@ +# serializer version: 1 +# name: test_states[binary_sensor.my_device_battery_low-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': , + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.my_device_battery_low', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery low', + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'battery_low', + 'unique_id': '123456789ABCDEFGH_battery_low', + 'unit_of_measurement': None, + }) +# --- +# name: test_states[binary_sensor.my_device_cloud_connection-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.my_device_cloud_connection', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Cloud connection', + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'cloud_connection', + 'unique_id': '123456789ABCDEFGH_cloud_connection', + 'unit_of_measurement': None, + }) +# --- +# name: test_states[binary_sensor.my_device_cloud_connection-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'connectivity', + 'friendly_name': 'my_device Cloud connection', + }), + 'context': , + 'entity_id': 'binary_sensor.my_device_cloud_connection', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_states[binary_sensor.my_device_door-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.my_device_door', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Door', + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'is_open', + 'unique_id': '123456789ABCDEFGH_is_open', + 'unit_of_measurement': None, + }) +# --- +# name: test_states[binary_sensor.my_device_door-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'door', + 'friendly_name': 'my_device Door', + }), + 'context': , + 'entity_id': 'binary_sensor.my_device_door', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_states[binary_sensor.my_device_humidity_warning-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': , + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.my_device_humidity_warning', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Humidity warning', + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'humidity_warning', + 'unique_id': '123456789ABCDEFGH_humidity_warning', + 'unit_of_measurement': None, + }) +# --- +# name: test_states[binary_sensor.my_device_moisture-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.my_device_moisture', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Moisture', + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'water_alert', + 'unique_id': '123456789ABCDEFGH_water_alert', + 'unit_of_measurement': None, + }) +# --- +# name: test_states[binary_sensor.my_device_moisture-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'moisture', + 'friendly_name': 'my_device Moisture', + }), + 'context': , + 'entity_id': 'binary_sensor.my_device_moisture', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_states[binary_sensor.my_device_overheated-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.my_device_overheated', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Overheated', + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'overheated', + 'unique_id': '123456789ABCDEFGH_overheated', + 'unit_of_measurement': None, + }) +# --- +# name: test_states[binary_sensor.my_device_overheated-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'problem', + 'friendly_name': 'my_device Overheated', + }), + 'context': , + 'entity_id': 'binary_sensor.my_device_overheated', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_states[binary_sensor.my_device_temperature_warning-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': , + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.my_device_temperature_warning', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Temperature warning', + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'temperature_warning', + 'unique_id': '123456789ABCDEFGH_temperature_warning', + 'unit_of_measurement': None, + }) +# --- +# name: test_states[binary_sensor.my_device_update-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.my_device_update', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Update', + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'update_available', + 'unique_id': '123456789ABCDEFGH_update_available', + 'unit_of_measurement': None, + }) +# --- +# name: test_states[binary_sensor.my_device_update-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'update', + 'friendly_name': 'my_device Update', + }), + 'context': , + 'entity_id': 'binary_sensor.my_device_update', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_states[my_device-entry] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'configuration_url': None, + 'connections': set({ + tuple( + 'mac', + 'aa:bb:cc:dd:ee:ff', + ), + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': '1.0.0', + 'id': , + 'identifiers': set({ + tuple( + 'tplink', + '123456789ABCDEFGH', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'TP-Link', + 'model': 'HS100', + 'name': 'my_device', + 'name_by_user': None, + 'serial_number': None, + 'suggested_area': None, + 'sw_version': '1.0.0', + 'via_device_id': None, + }) +# --- diff --git a/tests/components/tplink/snapshots/test_button.ambr b/tests/components/tplink/snapshots/test_button.ambr new file mode 100644 index 00000000000..f26829101f7 --- /dev/null +++ b/tests/components/tplink/snapshots/test_button.ambr @@ -0,0 +1,127 @@ +# serializer version: 1 +# name: test_states[button.my_device_stop_alarm-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.my_device_stop_alarm', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Stop alarm', + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'stop_alarm', + 'unique_id': '123456789ABCDEFGH_stop_alarm', + 'unit_of_measurement': None, + }) +# --- +# name: test_states[button.my_device_stop_alarm-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'my_device Stop alarm', + }), + 'context': , + 'entity_id': 'button.my_device_stop_alarm', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_states[button.my_device_test_alarm-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.my_device_test_alarm', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Test alarm', + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'test_alarm', + 'unique_id': '123456789ABCDEFGH_test_alarm', + 'unit_of_measurement': None, + }) +# --- +# name: test_states[button.my_device_test_alarm-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'my_device Test alarm', + }), + 'context': , + 'entity_id': 'button.my_device_test_alarm', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_states[my_device-entry] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'configuration_url': None, + 'connections': set({ + tuple( + 'mac', + 'aa:bb:cc:dd:ee:ff', + ), + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': '1.0.0', + 'id': , + 'identifiers': set({ + tuple( + 'tplink', + '123456789ABCDEFGH', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'TP-Link', + 'model': 'HS100', + 'name': 'my_device', + 'name_by_user': None, + 'serial_number': None, + 'suggested_area': None, + 'sw_version': '1.0.0', + 'via_device_id': None, + }) +# --- diff --git a/tests/components/tplink/snapshots/test_climate.ambr b/tests/components/tplink/snapshots/test_climate.ambr new file mode 100644 index 00000000000..d30f8cd3532 --- /dev/null +++ b/tests/components/tplink/snapshots/test_climate.ambr @@ -0,0 +1,94 @@ +# serializer version: 1 +# name: test_states[climate.thermostat-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'hvac_modes': list([ + , + , + ]), + 'max_temp': 65536, + 'min_temp': None, + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'climate', + 'entity_category': None, + 'entity_id': 'climate.thermostat', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': '123456789ABCDEFGH_climate', + 'unit_of_measurement': None, + }) +# --- +# name: test_states[climate.thermostat-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'current_temperature': 20, + 'friendly_name': 'thermostat', + 'hvac_action': , + 'hvac_modes': list([ + , + , + ]), + 'max_temp': 65536, + 'min_temp': None, + 'supported_features': , + 'temperature': 22, + }), + 'context': , + 'entity_id': 'climate.thermostat', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'heat', + }) +# --- +# name: test_states[thermostat-entry] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': '1.0.0', + 'id': , + 'identifiers': set({ + tuple( + 'tplink', + '123456789ABCDEFGH', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'TP-Link', + 'model': 'HS100', + 'name': 'thermostat', + 'name_by_user': None, + 'serial_number': None, + 'suggested_area': None, + 'sw_version': '1.0.0', + 'via_device_id': None, + }) +# --- diff --git a/tests/components/tplink/snapshots/test_fan.ambr b/tests/components/tplink/snapshots/test_fan.ambr new file mode 100644 index 00000000000..d692abdce03 --- /dev/null +++ b/tests/components/tplink/snapshots/test_fan.ambr @@ -0,0 +1,194 @@ +# serializer version: 1 +# name: test_states[fan.my_device-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'preset_modes': None, + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'fan', + 'entity_category': None, + 'entity_id': 'fan.my_device', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': '123456789ABCDEFGH', + 'unit_of_measurement': None, + }) +# --- +# name: test_states[fan.my_device-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'my_device', + 'percentage': None, + 'percentage_step': 25.0, + 'preset_mode': None, + 'preset_modes': None, + 'supported_features': , + }), + 'context': , + 'entity_id': 'fan.my_device', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_states[fan.my_device_my_fan_0-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'preset_modes': None, + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'fan', + 'entity_category': None, + 'entity_id': 'fan.my_device_my_fan_0', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'my_fan_0', + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': '123456789ABCDEFGH00', + 'unit_of_measurement': None, + }) +# --- +# name: test_states[fan.my_device_my_fan_0-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'my_device my_fan_0', + 'percentage': None, + 'percentage_step': 25.0, + 'preset_mode': None, + 'preset_modes': None, + 'supported_features': , + }), + 'context': , + 'entity_id': 'fan.my_device_my_fan_0', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_states[fan.my_device_my_fan_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'preset_modes': None, + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'fan', + 'entity_category': None, + 'entity_id': 'fan.my_device_my_fan_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'my_fan_1', + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': '123456789ABCDEFGH01', + 'unit_of_measurement': None, + }) +# --- +# name: test_states[fan.my_device_my_fan_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'my_device my_fan_1', + 'percentage': None, + 'percentage_step': 25.0, + 'preset_mode': None, + 'preset_modes': None, + 'supported_features': , + }), + 'context': , + 'entity_id': 'fan.my_device_my_fan_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_states[my_device-entry] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'configuration_url': None, + 'connections': set({ + tuple( + 'mac', + 'aa:bb:cc:dd:ee:ff', + ), + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': '1.0.0', + 'id': , + 'identifiers': set({ + tuple( + 'tplink', + '123456789ABCDEFGH', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'TP-Link', + 'model': 'HS100', + 'name': 'my_device', + 'name_by_user': None, + 'serial_number': None, + 'suggested_area': None, + 'sw_version': '1.0.0', + 'via_device_id': None, + }) +# --- diff --git a/tests/components/tplink/snapshots/test_number.ambr b/tests/components/tplink/snapshots/test_number.ambr new file mode 100644 index 00000000000..9bfc9c0126a --- /dev/null +++ b/tests/components/tplink/snapshots/test_number.ambr @@ -0,0 +1,255 @@ +# serializer version: 1 +# name: test_states[my_device-entry] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'configuration_url': None, + 'connections': set({ + tuple( + 'mac', + 'aa:bb:cc:dd:ee:ff', + ), + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': '1.0.0', + 'id': , + 'identifiers': set({ + tuple( + 'tplink', + '123456789ABCDEFGH', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'TP-Link', + 'model': 'HS100', + 'name': 'my_device', + 'name_by_user': None, + 'serial_number': None, + 'suggested_area': None, + 'sw_version': '1.0.0', + 'via_device_id': None, + }) +# --- +# name: test_states[number.my_device_smooth_off-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 65536, + 'min': 0, + 'mode': , + 'step': 1.0, + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.my_device_smooth_off', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Smooth off', + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'smooth_transition_off', + 'unique_id': '123456789ABCDEFGH_smooth_transition_off', + 'unit_of_measurement': None, + }) +# --- +# name: test_states[number.my_device_smooth_off-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'my_device Smooth off', + 'max': 65536, + 'min': 0, + 'mode': , + 'step': 1.0, + }), + 'context': , + 'entity_id': 'number.my_device_smooth_off', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'False', + }) +# --- +# name: test_states[number.my_device_smooth_on-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 65536, + 'min': 0, + 'mode': , + 'step': 1.0, + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.my_device_smooth_on', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Smooth on', + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'smooth_transition_on', + 'unique_id': '123456789ABCDEFGH_smooth_transition_on', + 'unit_of_measurement': None, + }) +# --- +# name: test_states[number.my_device_smooth_on-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'my_device Smooth on', + 'max': 65536, + 'min': 0, + 'mode': , + 'step': 1.0, + }), + 'context': , + 'entity_id': 'number.my_device_smooth_on', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'False', + }) +# --- +# name: test_states[number.my_device_temperature_offset-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 65536, + 'min': -10, + 'mode': , + 'step': 1.0, + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.my_device_temperature_offset', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Temperature offset', + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'temperature_offset', + 'unique_id': '123456789ABCDEFGH_temperature_offset', + 'unit_of_measurement': None, + }) +# --- +# name: test_states[number.my_device_temperature_offset-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'my_device Temperature offset', + 'max': 65536, + 'min': -10, + 'mode': , + 'step': 1.0, + }), + 'context': , + 'entity_id': 'number.my_device_temperature_offset', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'False', + }) +# --- +# name: test_states[number.my_device_turn_off_in-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 65536, + 'min': 0, + 'mode': , + 'step': 1.0, + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.my_device_turn_off_in', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Turn off in', + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'auto_off_minutes', + 'unique_id': '123456789ABCDEFGH_auto_off_minutes', + 'unit_of_measurement': None, + }) +# --- +# name: test_states[number.my_device_turn_off_in-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'my_device Turn off in', + 'max': 65536, + 'min': 0, + 'mode': , + 'step': 1.0, + }), + 'context': , + 'entity_id': 'number.my_device_turn_off_in', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'False', + }) +# --- diff --git a/tests/components/tplink/snapshots/test_select.ambr b/tests/components/tplink/snapshots/test_select.ambr new file mode 100644 index 00000000000..2cf02415238 --- /dev/null +++ b/tests/components/tplink/snapshots/test_select.ambr @@ -0,0 +1,238 @@ +# serializer version: 1 +# name: test_states[my_device-entry] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'configuration_url': None, + 'connections': set({ + tuple( + 'mac', + 'aa:bb:cc:dd:ee:ff', + ), + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': '1.0.0', + 'id': , + 'identifiers': set({ + tuple( + 'tplink', + '123456789ABCDEFGH', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'TP-Link', + 'model': 'HS100', + 'name': 'my_device', + 'name_by_user': None, + 'serial_number': None, + 'suggested_area': None, + 'sw_version': '1.0.0', + 'via_device_id': None, + }) +# --- +# name: test_states[select.my_device_alarm_sound-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'Doorbell Ring 1', + 'Doorbell Ring 2', + 'Doorbell Ring 3', + 'Doorbell Ring 4', + 'Doorbell Ring 5', + 'Doorbell Ring 6', + 'Doorbell Ring 7', + 'Doorbell Ring 8', + 'Doorbell Ring 9', + 'Doorbell Ring 10', + 'Phone Ring', + 'Alarm 1', + 'Alarm 2', + 'Alarm 3', + 'Alarm 4', + 'Dripping Tap', + 'Alarm 5', + 'Connection 1', + 'Connection 2', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.my_device_alarm_sound', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Alarm sound', + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'alarm_sound', + 'unique_id': '123456789ABCDEFGH_alarm_sound', + 'unit_of_measurement': None, + }) +# --- +# name: test_states[select.my_device_alarm_sound-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'my_device Alarm sound', + 'options': list([ + 'Doorbell Ring 1', + 'Doorbell Ring 2', + 'Doorbell Ring 3', + 'Doorbell Ring 4', + 'Doorbell Ring 5', + 'Doorbell Ring 6', + 'Doorbell Ring 7', + 'Doorbell Ring 8', + 'Doorbell Ring 9', + 'Doorbell Ring 10', + 'Phone Ring', + 'Alarm 1', + 'Alarm 2', + 'Alarm 3', + 'Alarm 4', + 'Dripping Tap', + 'Alarm 5', + 'Connection 1', + 'Connection 2', + ]), + }), + 'context': , + 'entity_id': 'select.my_device_alarm_sound', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'Phone Ring', + }) +# --- +# name: test_states[select.my_device_alarm_volume-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'low', + 'normal', + 'high', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.my_device_alarm_volume', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Alarm volume', + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'alarm_volume', + 'unique_id': '123456789ABCDEFGH_alarm_volume', + 'unit_of_measurement': None, + }) +# --- +# name: test_states[select.my_device_alarm_volume-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'my_device Alarm volume', + 'options': list([ + 'low', + 'normal', + 'high', + ]), + }), + 'context': , + 'entity_id': 'select.my_device_alarm_volume', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'normal', + }) +# --- +# name: test_states[select.my_device_light_preset-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'Off', + 'Preset 1', + 'Preset 2', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.my_device_light_preset', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Light preset', + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'light_preset', + 'unique_id': '123456789ABCDEFGH_light_preset', + 'unit_of_measurement': None, + }) +# --- +# name: test_states[select.my_device_light_preset-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'my_device Light preset', + 'options': list([ + 'Off', + 'Preset 1', + 'Preset 2', + ]), + }), + 'context': , + 'entity_id': 'select.my_device_light_preset', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'Off', + }) +# --- diff --git a/tests/components/tplink/snapshots/test_sensor.ambr b/tests/components/tplink/snapshots/test_sensor.ambr new file mode 100644 index 00000000000..cd8980bf57f --- /dev/null +++ b/tests/components/tplink/snapshots/test_sensor.ambr @@ -0,0 +1,790 @@ +# serializer version: 1 +# name: test_states[my_device-entry] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'configuration_url': None, + 'connections': set({ + tuple( + 'mac', + 'aa:bb:cc:dd:ee:ff', + ), + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': '1.0.0', + 'id': , + 'identifiers': set({ + tuple( + 'tplink', + '123456789ABCDEFGH', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'TP-Link', + 'model': 'HS100', + 'name': 'my_device', + 'name_by_user': None, + 'serial_number': None, + 'suggested_area': None, + 'sw_version': '1.0.0', + 'via_device_id': None, + }) +# --- +# name: test_states[sensor.my_device_alarm_source-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': , + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.my_device_alarm_source', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Alarm source', + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'alarm_source', + 'unique_id': '123456789ABCDEFGH_alarm_source', + 'unit_of_measurement': None, + }) +# --- +# name: test_states[sensor.my_device_auto_off_at-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.my_device_auto_off_at', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Auto off at', + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'auto_off_at', + 'unique_id': '123456789ABCDEFGH_auto_off_at', + 'unit_of_measurement': None, + }) +# --- +# name: test_states[sensor.my_device_auto_off_at-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'timestamp', + 'friendly_name': 'my_device Auto off at', + }), + 'context': , + 'entity_id': 'sensor.my_device_auto_off_at', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2024-06-24T09:03:11+00:00', + }) +# --- +# name: test_states[sensor.my_device_battery_level-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.my_device_battery_level', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery level', + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'battery_level', + 'unique_id': '123456789ABCDEFGH_battery_level', + 'unit_of_measurement': '%', + }) +# --- +# name: test_states[sensor.my_device_battery_level-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'my_device Battery level', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.my_device_battery_level', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '85', + }) +# --- +# name: test_states[sensor.my_device_current-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.my_device_current', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Current', + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'current', + 'unique_id': '123456789ABCDEFGH_current_a', + 'unit_of_measurement': 'A', + }) +# --- +# name: test_states[sensor.my_device_current-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'current', + 'friendly_name': 'my_device Current', + 'state_class': , + 'unit_of_measurement': 'A', + }), + 'context': , + 'entity_id': 'sensor.my_device_current', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '5.04', + }) +# --- +# name: test_states[sensor.my_device_current_consumption-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.my_device_current_consumption', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Current consumption', + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'current_consumption', + 'unique_id': '123456789ABCDEFGH_current_power_w', + 'unit_of_measurement': 'W', + }) +# --- +# name: test_states[sensor.my_device_current_consumption-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'my_device Current consumption', + 'state_class': , + 'unit_of_measurement': 'W', + }), + 'context': , + 'entity_id': 'sensor.my_device_current_consumption', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '5.2', + }) +# --- +# name: test_states[sensor.my_device_device_time-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': , + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.my_device_device_time', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Device time', + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'device_time', + 'unique_id': '123456789ABCDEFGH_device_time', + 'unit_of_measurement': None, + }) +# --- +# name: test_states[sensor.my_device_humidity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.my_device_humidity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Humidity', + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'humidity', + 'unique_id': '123456789ABCDEFGH_humidity', + 'unit_of_measurement': '%', + }) +# --- +# name: test_states[sensor.my_device_humidity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'humidity', + 'friendly_name': 'my_device Humidity', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.my_device_humidity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '12', + }) +# --- +# name: test_states[sensor.my_device_on_since-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': , + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.my_device_on_since', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'On since', + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'on_since', + 'unique_id': '123456789ABCDEFGH_on_since', + 'unit_of_measurement': None, + }) +# --- +# name: test_states[sensor.my_device_report_interval-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': , + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.my_device_report_interval', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Report interval', + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'report_interval', + 'unique_id': '123456789ABCDEFGH_report_interval', + 'unit_of_measurement': '%', + }) +# --- +# name: test_states[sensor.my_device_signal_level-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.my_device_signal_level', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Signal level', + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'signal_level', + 'unique_id': '123456789ABCDEFGH_signal_level', + 'unit_of_measurement': None, + }) +# --- +# name: test_states[sensor.my_device_signal_level-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'my_device Signal level', + 'state_class': , + }), + 'context': , + 'entity_id': 'sensor.my_device_signal_level', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2', + }) +# --- +# name: test_states[sensor.my_device_signal_strength-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': , + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.my_device_signal_strength', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Signal strength', + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'rssi', + 'unique_id': '123456789ABCDEFGH_rssi', + 'unit_of_measurement': None, + }) +# --- +# name: test_states[sensor.my_device_ssid-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': , + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.my_device_ssid', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'SSID', + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'ssid', + 'unique_id': '123456789ABCDEFGH_ssid', + 'unit_of_measurement': None, + }) +# --- +# name: test_states[sensor.my_device_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': , + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.my_device_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Temperature', + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'temperature', + 'unique_id': '123456789ABCDEFGH_temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_states[sensor.my_device_this_month_s_consumption-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.my_device_this_month_s_consumption', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 3, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': "This month's consumption", + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'consumption_this_month', + 'unique_id': '123456789ABCDEFGH_consumption_this_month', + 'unit_of_measurement': 'kWh', + }) +# --- +# name: test_states[sensor.my_device_this_month_s_consumption-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': "my_device This month's consumption", + 'state_class': , + 'unit_of_measurement': 'kWh', + }), + 'context': , + 'entity_id': 'sensor.my_device_this_month_s_consumption', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '15.345', + }) +# --- +# name: test_states[sensor.my_device_today_s_consumption-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.my_device_today_s_consumption', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 3, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': "Today's consumption", + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'consumption_today', + 'unique_id': '123456789ABCDEFGH_today_energy_kwh', + 'unit_of_measurement': 'kWh', + }) +# --- +# name: test_states[sensor.my_device_today_s_consumption-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': "my_device Today's consumption", + 'state_class': , + 'unit_of_measurement': 'kWh', + }), + 'context': , + 'entity_id': 'sensor.my_device_today_s_consumption', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '5.23', + }) +# --- +# name: test_states[sensor.my_device_total_consumption-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.my_device_total_consumption', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 3, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Total consumption', + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'consumption_total', + 'unique_id': '123456789ABCDEFGH_total_energy_kwh', + 'unit_of_measurement': 'kWh', + }) +# --- +# name: test_states[sensor.my_device_total_consumption-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'my_device Total consumption', + 'state_class': , + 'unit_of_measurement': 'kWh', + }), + 'context': , + 'entity_id': 'sensor.my_device_total_consumption', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '30.005', + }) +# --- +# name: test_states[sensor.my_device_voltage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.my_device_voltage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Voltage', + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'voltage', + 'unique_id': '123456789ABCDEFGH_voltage', + 'unit_of_measurement': 'v', + }) +# --- +# name: test_states[sensor.my_device_voltage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'my_device Voltage', + 'state_class': , + 'unit_of_measurement': 'v', + }), + 'context': , + 'entity_id': 'sensor.my_device_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '121.1', + }) +# --- diff --git a/tests/components/tplink/snapshots/test_switch.ambr b/tests/components/tplink/snapshots/test_switch.ambr new file mode 100644 index 00000000000..2fe1f6e6b08 --- /dev/null +++ b/tests/components/tplink/snapshots/test_switch.ambr @@ -0,0 +1,311 @@ +# serializer version: 1 +# name: test_states[my_device-entry] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'configuration_url': None, + 'connections': set({ + tuple( + 'mac', + 'aa:bb:cc:dd:ee:ff', + ), + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': '1.0.0', + 'id': , + 'identifiers': set({ + tuple( + 'tplink', + '123456789ABCDEFGH', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'TP-Link', + 'model': 'HS100', + 'name': 'my_device', + 'name_by_user': None, + 'serial_number': None, + 'suggested_area': None, + 'sw_version': '1.0.0', + 'via_device_id': None, + }) +# --- +# name: test_states[switch.my_device-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.my_device', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456789ABCDEFGH', + 'unit_of_measurement': None, + }) +# --- +# name: test_states[switch.my_device-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'my_device', + }), + 'context': , + 'entity_id': 'switch.my_device', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_states[switch.my_device_auto_off_enabled-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.my_device_auto_off_enabled', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Auto off enabled', + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'auto_off_enabled', + 'unique_id': '123456789ABCDEFGH_auto_off_enabled', + 'unit_of_measurement': None, + }) +# --- +# name: test_states[switch.my_device_auto_off_enabled-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'my_device Auto off enabled', + }), + 'context': , + 'entity_id': 'switch.my_device_auto_off_enabled', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_states[switch.my_device_auto_update_enabled-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.my_device_auto_update_enabled', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Auto update enabled', + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'auto_update_enabled', + 'unique_id': '123456789ABCDEFGH_auto_update_enabled', + 'unit_of_measurement': None, + }) +# --- +# name: test_states[switch.my_device_auto_update_enabled-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'my_device Auto update enabled', + }), + 'context': , + 'entity_id': 'switch.my_device_auto_update_enabled', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_states[switch.my_device_fan_sleep_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.my_device_fan_sleep_mode', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Fan sleep mode', + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'fan_sleep_mode', + 'unique_id': '123456789ABCDEFGH_fan_sleep_mode', + 'unit_of_measurement': None, + }) +# --- +# name: test_states[switch.my_device_fan_sleep_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'my_device Fan sleep mode', + }), + 'context': , + 'entity_id': 'switch.my_device_fan_sleep_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_states[switch.my_device_led-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.my_device_led', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'LED', + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'led', + 'unique_id': '123456789ABCDEFGH_led', + 'unit_of_measurement': None, + }) +# --- +# name: test_states[switch.my_device_led-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'my_device LED', + }), + 'context': , + 'entity_id': 'switch.my_device_led', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_states[switch.my_device_smooth_transitions-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.my_device_smooth_transitions', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Smooth transitions', + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'smooth_transitions', + 'unique_id': '123456789ABCDEFGH_smooth_transitions', + 'unit_of_measurement': None, + }) +# --- +# name: test_states[switch.my_device_smooth_transitions-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'my_device Smooth transitions', + }), + 'context': , + 'entity_id': 'switch.my_device_smooth_transitions', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- diff --git a/tests/components/tplink/test_binary_sensor.py b/tests/components/tplink/test_binary_sensor.py new file mode 100644 index 00000000000..e2b9cd08d13 --- /dev/null +++ b/tests/components/tplink/test_binary_sensor.py @@ -0,0 +1,124 @@ +"""Tests for tplink binary_sensor platform.""" + +from kasa import Feature +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components import tplink +from homeassistant.components.tplink.binary_sensor import BINARY_SENSOR_DESCRIPTIONS +from homeassistant.components.tplink.const import DOMAIN +from homeassistant.components.tplink.entity import EXCLUDED_FEATURES +from homeassistant.const import CONF_HOST, Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.setup import async_setup_component + +from . import ( + DEVICE_ID, + MAC_ADDRESS, + _mocked_device, + _mocked_feature, + _mocked_strip_children, + _patch_connect, + _patch_discovery, + setup_platform_for_device, + snapshot_platform, +) + +from tests.common import MockConfigEntry + + +@pytest.fixture +def mocked_feature_binary_sensor() -> Feature: + """Return mocked tplink binary sensor feature.""" + return _mocked_feature( + "overheated", + value=False, + name="Overheated", + type_=Feature.Type.BinarySensor, + category=Feature.Category.Primary, + ) + + +async def test_states( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, + device_registry: dr.DeviceRegistry, + snapshot: SnapshotAssertion, +) -> None: + """Test a sensor unique ids.""" + features = {description.key for description in BINARY_SENSOR_DESCRIPTIONS} + features.update(EXCLUDED_FEATURES) + device = _mocked_device(alias="my_device", features=features) + + await setup_platform_for_device( + hass, mock_config_entry, Platform.BINARY_SENSOR, device + ) + await snapshot_platform( + hass, entity_registry, device_registry, snapshot, mock_config_entry.entry_id + ) + + for excluded in EXCLUDED_FEATURES: + assert hass.states.get(f"sensor.my_device_{excluded}") is None + + +async def test_binary_sensor( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + mocked_feature_binary_sensor: Feature, +) -> None: + """Test a sensor unique ids.""" + mocked_feature = mocked_feature_binary_sensor + already_migrated_config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS + ) + already_migrated_config_entry.add_to_hass(hass) + + plug = _mocked_device(alias="my_plug", features=[mocked_feature]) + with _patch_discovery(device=plug), _patch_connect(device=plug): + await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) + await hass.async_block_till_done() + + # The entity_id is based on standard name from core. + entity_id = "binary_sensor.my_plug_overheated" + entity = entity_registry.async_get(entity_id) + assert entity + assert entity.unique_id == f"{DEVICE_ID}_{mocked_feature.id}" + + +async def test_binary_sensor_children( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + device_registry: dr.DeviceRegistry, + mocked_feature_binary_sensor: Feature, +) -> None: + """Test a sensor unique ids.""" + mocked_feature = mocked_feature_binary_sensor + already_migrated_config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS + ) + already_migrated_config_entry.add_to_hass(hass) + plug = _mocked_device( + alias="my_plug", + features=[mocked_feature], + children=_mocked_strip_children(features=[mocked_feature]), + ) + with _patch_discovery(device=plug), _patch_connect(device=plug): + await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) + await hass.async_block_till_done() + + entity_id = "binary_sensor.my_plug_overheated" + entity = entity_registry.async_get(entity_id) + assert entity + device = device_registry.async_get(entity.device_id) + + for plug_id in range(2): + child_entity_id = f"binary_sensor.my_plug_plug{plug_id}_overheated" + child_entity = entity_registry.async_get(child_entity_id) + assert child_entity + assert child_entity.unique_id == f"PLUG{plug_id}DEVICEID_{mocked_feature.id}" + assert child_entity.device_id != entity.device_id + child_device = device_registry.async_get(child_entity.device_id) + assert child_device + assert child_device.via_device_id == device.id diff --git a/tests/components/tplink/test_button.py b/tests/components/tplink/test_button.py new file mode 100644 index 00000000000..143a882a6cb --- /dev/null +++ b/tests/components/tplink/test_button.py @@ -0,0 +1,153 @@ +"""Tests for tplink button platform.""" + +from kasa import Feature +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components import tplink +from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS +from homeassistant.components.tplink.button import BUTTON_DESCRIPTIONS +from homeassistant.components.tplink.const import DOMAIN +from homeassistant.components.tplink.entity import EXCLUDED_FEATURES +from homeassistant.const import ATTR_ENTITY_ID, CONF_HOST, Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.setup import async_setup_component + +from . import ( + DEVICE_ID, + MAC_ADDRESS, + _mocked_device, + _mocked_feature, + _mocked_strip_children, + _patch_connect, + _patch_discovery, + setup_platform_for_device, + snapshot_platform, +) + +from tests.common import MockConfigEntry + + +@pytest.fixture +def mocked_feature_button() -> Feature: + """Return mocked tplink binary sensor feature.""" + return _mocked_feature( + "test_alarm", + value="", + name="Test alarm", + type_=Feature.Type.Action, + category=Feature.Category.Primary, + ) + + +async def test_states( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, + device_registry: dr.DeviceRegistry, + snapshot: SnapshotAssertion, +) -> None: + """Test a sensor unique ids.""" + features = {description.key for description in BUTTON_DESCRIPTIONS} + features.update(EXCLUDED_FEATURES) + device = _mocked_device(alias="my_device", features=features) + + await setup_platform_for_device(hass, mock_config_entry, Platform.BUTTON, device) + await snapshot_platform( + hass, entity_registry, device_registry, snapshot, mock_config_entry.entry_id + ) + + for excluded in EXCLUDED_FEATURES: + assert hass.states.get(f"sensor.my_device_{excluded}") is None + + +async def test_button( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + mocked_feature_button: Feature, +) -> None: + """Test a sensor unique ids.""" + mocked_feature = mocked_feature_button + already_migrated_config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS + ) + already_migrated_config_entry.add_to_hass(hass) + + plug = _mocked_device(alias="my_plug", features=[mocked_feature]) + with _patch_discovery(device=plug), _patch_connect(device=plug): + await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) + await hass.async_block_till_done() + + # The entity_id is based on standard name from core. + entity_id = "button.my_plug_test_alarm" + entity = entity_registry.async_get(entity_id) + assert entity + assert entity.unique_id == f"{DEVICE_ID}_{mocked_feature.id}" + + +async def test_button_children( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + device_registry: dr.DeviceRegistry, + mocked_feature_button: Feature, +) -> None: + """Test a sensor unique ids.""" + mocked_feature = mocked_feature_button + already_migrated_config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS + ) + already_migrated_config_entry.add_to_hass(hass) + plug = _mocked_device( + alias="my_plug", + features=[mocked_feature], + children=_mocked_strip_children(features=[mocked_feature]), + ) + with _patch_discovery(device=plug), _patch_connect(device=plug): + await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) + await hass.async_block_till_done() + + entity_id = "button.my_plug_test_alarm" + entity = entity_registry.async_get(entity_id) + assert entity + device = device_registry.async_get(entity.device_id) + + for plug_id in range(2): + child_entity_id = f"button.my_plug_plug{plug_id}_test_alarm" + child_entity = entity_registry.async_get(child_entity_id) + assert child_entity + assert child_entity.unique_id == f"PLUG{plug_id}DEVICEID_{mocked_feature.id}" + assert child_entity.device_id != entity.device_id + child_device = device_registry.async_get(child_entity.device_id) + assert child_device + assert child_device.via_device_id == device.id + + +async def test_button_press( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + mocked_feature_button: Feature, +) -> None: + """Test a number entity limits and setting values.""" + mocked_feature = mocked_feature_button + already_migrated_config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS + ) + already_migrated_config_entry.add_to_hass(hass) + plug = _mocked_device(alias="my_plug", features=[mocked_feature]) + with _patch_discovery(device=plug), _patch_connect(device=plug): + await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) + await hass.async_block_till_done() + + entity_id = "button.my_plug_test_alarm" + entity = entity_registry.async_get(entity_id) + assert entity + assert entity.unique_id == f"{DEVICE_ID}_test_alarm" + + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + mocked_feature.set_value.assert_called_with(True) diff --git a/tests/components/tplink/test_climate.py b/tests/components/tplink/test_climate.py new file mode 100644 index 00000000000..a80a74a5697 --- /dev/null +++ b/tests/components/tplink/test_climate.py @@ -0,0 +1,226 @@ +"""Tests for tplink climate platform.""" + +from datetime import timedelta + +from kasa import Device, Feature +from kasa.smart.modules.temperaturecontrol import ThermostatState +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.climate import ( + ATTR_CURRENT_TEMPERATURE, + ATTR_HVAC_ACTION, + ATTR_HVAC_MODE, + ATTR_TEMPERATURE, + DOMAIN as CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, + SERVICE_SET_TEMPERATURE, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + HVACAction, + HVACMode, +) +from homeassistant.const import ATTR_ENTITY_ID, Platform +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ServiceValidationError +from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.util import dt as dt_util + +from . import ( + DEVICE_ID, + _mocked_device, + _mocked_feature, + setup_platform_for_device, + snapshot_platform, +) + +from tests.common import MockConfigEntry, async_fire_time_changed + +ENTITY_ID = "climate.thermostat" + + +@pytest.fixture +async def mocked_hub(hass: HomeAssistant) -> Device: + """Return mocked tplink binary sensor feature.""" + + features = [ + _mocked_feature( + "temperature", value=20, category=Feature.Category.Primary, unit="celsius" + ), + _mocked_feature( + "target_temperature", + value=22, + type_=Feature.Type.Number, + category=Feature.Category.Primary, + unit="celsius", + ), + _mocked_feature( + "state", + value=True, + type_=Feature.Type.Switch, + category=Feature.Category.Primary, + ), + _mocked_feature( + "thermostat_mode", + value=ThermostatState.Heating, + type_=Feature.Type.Choice, + category=Feature.Category.Primary, + ), + ] + + thermostat = _mocked_device( + alias="thermostat", features=features, device_type=Device.Type.Thermostat + ) + + return _mocked_device( + alias="hub", children=[thermostat], device_type=Device.Type.Hub + ) + + +async def test_climate( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, + mocked_hub: Device, +) -> None: + """Test initialization.""" + await setup_platform_for_device( + hass, mock_config_entry, Platform.CLIMATE, mocked_hub + ) + + entity = entity_registry.async_get(ENTITY_ID) + assert entity + assert entity.unique_id == f"{DEVICE_ID}_climate" + + state = hass.states.get(ENTITY_ID) + assert state.attributes[ATTR_HVAC_ACTION] is HVACAction.HEATING + assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 20 + assert state.attributes[ATTR_TEMPERATURE] == 22 + + +async def test_states( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, + device_registry: dr.DeviceRegistry, + snapshot: SnapshotAssertion, + mocked_hub: Device, +) -> None: + """Snapshot test.""" + await setup_platform_for_device( + hass, mock_config_entry, Platform.CLIMATE, mocked_hub + ) + + await snapshot_platform( + hass, entity_registry, device_registry, snapshot, mock_config_entry.entry_id + ) + + +async def test_set_temperature( + hass: HomeAssistant, mock_config_entry: MockConfigEntry, mocked_hub: Device +) -> None: + """Test that set_temperature service calls the setter.""" + await setup_platform_for_device( + hass, mock_config_entry, Platform.CLIMATE, mocked_hub + ) + + mocked_thermostat = mocked_hub.children[0] + + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_TEMPERATURE, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_TEMPERATURE: 10}, + blocking=True, + ) + target_temp_feature = mocked_thermostat.features["target_temperature"] + target_temp_feature.set_value.assert_called_with(10) + + +async def test_set_hvac_mode( + hass: HomeAssistant, mock_config_entry: MockConfigEntry, mocked_hub: Device +) -> None: + """Test that set_hvac_mode service works.""" + await setup_platform_for_device( + hass, mock_config_entry, Platform.CLIMATE, mocked_hub + ) + + mocked_thermostat = mocked_hub.children[0] + mocked_state = mocked_thermostat.features["state"] + assert mocked_state is not None + + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_HVAC_MODE: HVACMode.OFF}, + blocking=True, + ) + + mocked_state.set_value.assert_called_with(False) + + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, + {ATTR_ENTITY_ID: [ENTITY_ID], ATTR_HVAC_MODE: HVACMode.HEAT}, + blocking=True, + ) + mocked_state.set_value.assert_called_with(True) + + with pytest.raises(ServiceValidationError): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, + {ATTR_ENTITY_ID: [ENTITY_ID], ATTR_HVAC_MODE: HVACMode.DRY}, + blocking=True, + ) + + +async def test_turn_on_and_off( + hass: HomeAssistant, mock_config_entry: MockConfigEntry, mocked_hub: Device +) -> None: + """Test that turn_on and turn_off services work as expected.""" + await setup_platform_for_device( + hass, mock_config_entry, Platform.CLIMATE, mocked_hub + ) + + mocked_thermostat = mocked_hub.children[0] + mocked_state = mocked_thermostat.features["state"] + + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: [ENTITY_ID]}, + blocking=True, + ) + + mocked_state.set_value.assert_called_with(False) + + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: [ENTITY_ID]}, + blocking=True, + ) + + mocked_state.set_value.assert_called_with(True) + + +async def test_unknown_mode( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mocked_hub: Device, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test that unknown device modes log a warning and default to off.""" + await setup_platform_for_device( + hass, mock_config_entry, Platform.CLIMATE, mocked_hub + ) + + mocked_thermostat = mocked_hub.children[0] + mocked_state = mocked_thermostat.features["thermostat_mode"] + mocked_state.value = ThermostatState.Unknown + + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=30)) + await hass.async_block_till_done() + state = hass.states.get(ENTITY_ID) + assert state.attributes[ATTR_HVAC_ACTION] == HVACAction.OFF + assert "Unknown thermostat state, defaulting to OFF" in caplog.text diff --git a/tests/components/tplink/test_config_flow.py b/tests/components/tplink/test_config_flow.py index 7bf3b8cce5e..7560ff4a72d 100644 --- a/tests/components/tplink/test_config_flow.py +++ b/tests/components/tplink/test_config_flow.py @@ -2,17 +2,17 @@ from unittest.mock import AsyncMock, patch -from kasa import TimeoutException +from kasa import TimeoutError import pytest from homeassistant import config_entries from homeassistant.components import dhcp from homeassistant.components.tplink import ( DOMAIN, - AuthenticationException, + AuthenticationError, Credentials, DeviceConfig, - SmartDeviceException, + KasaException, ) from homeassistant.components.tplink.const import CONF_DEVICE_CONFIG from homeassistant.config_entries import ConfigEntryState @@ -40,7 +40,7 @@ from . import ( MAC_ADDRESS, MAC_ADDRESS2, MODULE, - _mocked_bulb, + _mocked_device, _patch_connect, _patch_discovery, _patch_single_discovery, @@ -120,7 +120,7 @@ async def test_discovery_auth( ) -> None: """Test authenticated discovery.""" - mock_discovery["mock_device"].update.side_effect = AuthenticationException + mock_discovery["mock_device"].update.side_effect = AuthenticationError result = await hass.config_entries.flow.async_init( DOMAIN, @@ -155,8 +155,8 @@ async def test_discovery_auth( @pytest.mark.parametrize( ("error_type", "errors_msg", "error_placement"), [ - (AuthenticationException("auth_error_details"), "invalid_auth", CONF_PASSWORD), - (SmartDeviceException("smart_device_error_details"), "cannot_connect", "base"), + (AuthenticationError("auth_error_details"), "invalid_auth", CONF_PASSWORD), + (KasaException("smart_device_error_details"), "cannot_connect", "base"), ], ids=["invalid-auth", "unknown-error"], ) @@ -170,7 +170,7 @@ async def test_discovery_auth_errors( error_placement, ) -> None: """Test handling of discovery authentication errors.""" - mock_discovery["mock_device"].update.side_effect = AuthenticationException + mock_discovery["mock_device"].update.side_effect = AuthenticationError default_connect_side_effect = mock_connect["connect"].side_effect mock_connect["connect"].side_effect = error_type @@ -223,7 +223,7 @@ async def test_discovery_new_credentials( mock_init, ) -> None: """Test setting up discovery with new credentials.""" - mock_discovery["mock_device"].update.side_effect = AuthenticationException + mock_discovery["mock_device"].update.side_effect = AuthenticationError result = await hass.config_entries.flow.async_init( DOMAIN, @@ -272,10 +272,10 @@ async def test_discovery_new_credentials_invalid( mock_init, ) -> None: """Test setting up discovery with new invalid credentials.""" - mock_discovery["mock_device"].update.side_effect = AuthenticationException + mock_discovery["mock_device"].update.side_effect = AuthenticationError default_connect_side_effect = mock_connect["connect"].side_effect - mock_connect["connect"].side_effect = AuthenticationException + mock_connect["connect"].side_effect = AuthenticationError result = await hass.config_entries.flow.async_init( DOMAIN, @@ -514,7 +514,7 @@ async def test_manual_auth( assert result["step_id"] == "user" assert not result["errors"] - mock_discovery["mock_device"].update.side_effect = AuthenticationException + mock_discovery["mock_device"].update.side_effect = AuthenticationError result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_HOST: IP_ADDRESS} @@ -544,8 +544,8 @@ async def test_manual_auth( @pytest.mark.parametrize( ("error_type", "errors_msg", "error_placement"), [ - (AuthenticationException("auth_error_details"), "invalid_auth", CONF_PASSWORD), - (SmartDeviceException("smart_device_error_details"), "cannot_connect", "base"), + (AuthenticationError("auth_error_details"), "invalid_auth", CONF_PASSWORD), + (KasaException("smart_device_error_details"), "cannot_connect", "base"), ], ids=["invalid-auth", "unknown-error"], ) @@ -566,7 +566,7 @@ async def test_manual_auth_errors( assert result["step_id"] == "user" assert not result["errors"] - mock_discovery["mock_device"].update.side_effect = AuthenticationException + mock_discovery["mock_device"].update.side_effect = AuthenticationError default_connect_side_effect = mock_connect["connect"].side_effect mock_connect["connect"].side_effect = error_type @@ -765,7 +765,7 @@ async def test_integration_discovery_with_ip_change( mock_connect: AsyncMock, ) -> None: """Test reauth flow.""" - mock_connect["connect"].side_effect = SmartDeviceException() + mock_connect["connect"].side_effect = KasaException() mock_config_entry.add_to_hass(hass) with patch("homeassistant.components.tplink.Discover.discover", return_value={}): await hass.config_entries.async_setup(mock_config_entry.entry_id) @@ -797,7 +797,7 @@ async def test_integration_discovery_with_ip_change( config = DeviceConfig.from_dict(DEVICE_CONFIG_DICT_AUTH) mock_connect["connect"].reset_mock(side_effect=True) - bulb = _mocked_bulb( + bulb = _mocked_device( device_config=config, mac=mock_config_entry.unique_id, ) @@ -818,7 +818,7 @@ async def test_dhcp_discovery_with_ip_change( mock_connect: AsyncMock, ) -> None: """Test dhcp discovery with an IP change.""" - mock_connect["connect"].side_effect = SmartDeviceException() + mock_connect["connect"].side_effect = KasaException() mock_config_entry.add_to_hass(hass) with patch("homeassistant.components.tplink.Discover.discover", return_value={}): await hass.config_entries.async_setup(mock_config_entry.entry_id) @@ -883,7 +883,7 @@ async def test_reauth_update_from_discovery( mock_connect: AsyncMock, ) -> None: """Test reauth flow.""" - mock_connect["connect"].side_effect = AuthenticationException + mock_connect["connect"].side_effect = AuthenticationError mock_config_entry.add_to_hass(hass) with patch("homeassistant.components.tplink.Discover.discover", return_value={}): await hass.config_entries.async_setup(mock_config_entry.entry_id) @@ -920,7 +920,7 @@ async def test_reauth_update_from_discovery_with_ip_change( mock_connect: AsyncMock, ) -> None: """Test reauth flow.""" - mock_connect["connect"].side_effect = AuthenticationException() + mock_connect["connect"].side_effect = AuthenticationError() mock_config_entry.add_to_hass(hass) with patch("homeassistant.components.tplink.Discover.discover", return_value={}): await hass.config_entries.async_setup(mock_config_entry.entry_id) @@ -957,7 +957,7 @@ async def test_reauth_no_update_if_config_and_ip_the_same( mock_connect: AsyncMock, ) -> None: """Test reauth discovery does not update when the host and config are the same.""" - mock_connect["connect"].side_effect = AuthenticationException() + mock_connect["connect"].side_effect = AuthenticationError() mock_config_entry.add_to_hass(hass) hass.config_entries.async_update_entry( mock_config_entry, @@ -996,8 +996,8 @@ async def test_reauth_no_update_if_config_and_ip_the_same( @pytest.mark.parametrize( ("error_type", "errors_msg", "error_placement"), [ - (AuthenticationException("auth_error_details"), "invalid_auth", CONF_PASSWORD), - (SmartDeviceException("smart_device_error_details"), "cannot_connect", "base"), + (AuthenticationError("auth_error_details"), "invalid_auth", CONF_PASSWORD), + (KasaException("smart_device_error_details"), "cannot_connect", "base"), ], ids=["invalid-auth", "unknown-error"], ) @@ -1060,8 +1060,8 @@ async def test_reauth_errors( @pytest.mark.parametrize( ("error_type", "expected_flow"), [ - (AuthenticationException, FlowResultType.FORM), - (SmartDeviceException, FlowResultType.ABORT), + (AuthenticationError, FlowResultType.FORM), + (KasaException, FlowResultType.ABORT), ], ids=["invalid-auth", "unknown-error"], ) @@ -1119,7 +1119,7 @@ async def test_discovery_timeout_connect( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - mock_discovery["discover_single"].side_effect = TimeoutException + mock_discovery["discover_single"].side_effect = TimeoutError await hass.async_block_till_done() assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" @@ -1149,7 +1149,7 @@ async def test_reauth_update_other_flows( unique_id=MAC_ADDRESS2, ) default_side_effect = mock_connect["connect"].side_effect - mock_connect["connect"].side_effect = AuthenticationException() + mock_connect["connect"].side_effect = AuthenticationError() mock_config_entry.add_to_hass(hass) mock_config_entry2.add_to_hass(hass) with patch("homeassistant.components.tplink.Discover.discover", return_value={}): diff --git a/tests/components/tplink/test_diagnostics.py b/tests/components/tplink/test_diagnostics.py index 3543cf95572..7288d631f4a 100644 --- a/tests/components/tplink/test_diagnostics.py +++ b/tests/components/tplink/test_diagnostics.py @@ -2,12 +2,12 @@ import json -from kasa import SmartDevice +from kasa import Device import pytest from homeassistant.core import HomeAssistant -from . import _mocked_bulb, _mocked_plug, initialize_config_entry_for_device +from . import _mocked_device, initialize_config_entry_for_device from tests.common import load_fixture from tests.components.diagnostics import get_diagnostics_for_config_entry @@ -18,13 +18,13 @@ from tests.typing import ClientSessionGenerator ("mocked_dev", "fixture_file", "sysinfo_vars", "expected_oui"), [ ( - _mocked_bulb(), + _mocked_device(), "tplink-diagnostics-data-bulb-kl130.json", ["mic_mac", "deviceId", "oemId", "hwId", "alias"], "AA:BB:CC", ), ( - _mocked_plug(), + _mocked_device(), "tplink-diagnostics-data-plug-hs110.json", ["mac", "deviceId", "oemId", "hwId", "alias", "longitude_i", "latitude_i"], "AA:BB:CC", @@ -34,7 +34,7 @@ from tests.typing import ClientSessionGenerator async def test_diagnostics( hass: HomeAssistant, hass_client: ClientSessionGenerator, - mocked_dev: SmartDevice, + mocked_dev: Device, fixture_file: str, sysinfo_vars: list[str], expected_oui: str | None, diff --git a/tests/components/tplink/test_fan.py b/tests/components/tplink/test_fan.py new file mode 100644 index 00000000000..deba33abfa5 --- /dev/null +++ b/tests/components/tplink/test_fan.py @@ -0,0 +1,154 @@ +"""Tests for fan platform.""" + +from __future__ import annotations + +from datetime import timedelta + +from kasa import Device, Module +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.fan import ( + ATTR_PERCENTAGE, + DOMAIN as FAN_DOMAIN, + SERVICE_SET_PERCENTAGE, +) +from homeassistant.const import ATTR_ENTITY_ID, Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr, entity_registry as er +import homeassistant.util.dt as dt_util + +from . import DEVICE_ID, _mocked_device, setup_platform_for_device, snapshot_platform + +from tests.common import MockConfigEntry, async_fire_time_changed + + +async def test_states( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, + device_registry: dr.DeviceRegistry, + snapshot: SnapshotAssertion, +) -> None: + """Test a fan state.""" + child_fan_1 = _mocked_device( + modules=[Module.Fan], alias="my_fan_0", device_id=f"{DEVICE_ID}00" + ) + child_fan_2 = _mocked_device( + modules=[Module.Fan], alias="my_fan_1", device_id=f"{DEVICE_ID}01" + ) + parent_device = _mocked_device( + device_id=DEVICE_ID, + alias="my_device", + children=[child_fan_1, child_fan_2], + modules=[Module.Fan], + device_type=Device.Type.WallSwitch, + ) + + await setup_platform_for_device( + hass, mock_config_entry, Platform.FAN, parent_device + ) + await snapshot_platform( + hass, entity_registry, device_registry, snapshot, mock_config_entry.entry_id + ) + + +async def test_fan_unique_id( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + device_registry: dr.DeviceRegistry, +) -> None: + """Test a fan unique id.""" + fan = _mocked_device(modules=[Module.Fan], alias="my_fan") + await setup_platform_for_device(hass, mock_config_entry, Platform.FAN, fan) + + device_entries = dr.async_entries_for_config_entry( + device_registry, mock_config_entry.entry_id + ) + assert device_entries + entity_id = "fan.my_fan" + entity_registry = er.async_get(hass) + assert entity_registry.async_get(entity_id).unique_id == DEVICE_ID + + +async def test_fan(hass: HomeAssistant, mock_config_entry: MockConfigEntry) -> None: + """Test a color fan and that all transitions are correctly passed.""" + device = _mocked_device(modules=[Module.Fan], alias="my_fan") + fan = device.modules[Module.Fan] + fan.fan_speed_level = 0 + await setup_platform_for_device(hass, mock_config_entry, Platform.FAN, device) + + entity_id = "fan.my_fan" + + state = hass.states.get(entity_id) + assert state.state == "off" + + await hass.services.async_call( + FAN_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + fan.set_fan_speed_level.assert_called_once_with(4) + fan.set_fan_speed_level.reset_mock() + + fan.fan_speed_level = 4 + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10)) + await hass.async_block_till_done(wait_background_tasks=True) + state = hass.states.get(entity_id) + assert state.state == "on" + + await hass.services.async_call( + FAN_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + fan.set_fan_speed_level.assert_called_once_with(0) + fan.set_fan_speed_level.reset_mock() + + await hass.services.async_call( + FAN_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_PERCENTAGE: 50}, + blocking=True, + ) + fan.set_fan_speed_level.assert_called_once_with(2) + fan.set_fan_speed_level.reset_mock() + + await hass.services.async_call( + FAN_DOMAIN, + SERVICE_SET_PERCENTAGE, + {ATTR_ENTITY_ID: entity_id, ATTR_PERCENTAGE: 25}, + blocking=True, + ) + fan.set_fan_speed_level.assert_called_once_with(1) + fan.set_fan_speed_level.reset_mock() + + +async def test_fan_child( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, +) -> None: + """Test child fans are added to parent device with the right ids.""" + child_fan_1 = _mocked_device( + modules=[Module.Fan], alias="my_fan_0", device_id=f"{DEVICE_ID}00" + ) + child_fan_2 = _mocked_device( + modules=[Module.Fan], alias="my_fan_1", device_id=f"{DEVICE_ID}01" + ) + parent_device = _mocked_device( + device_id=DEVICE_ID, + alias="my_device", + children=[child_fan_1, child_fan_2], + modules=[Module.Fan], + device_type=Device.Type.WallSwitch, + ) + await setup_platform_for_device( + hass, mock_config_entry, Platform.FAN, parent_device + ) + + entity_id = "fan.my_device" + entity = entity_registry.async_get(entity_id) + assert entity + + for fan_id in range(2): + child_entity_id = f"fan.my_device_my_fan_{fan_id}" + child_entity = entity_registry.async_get(child_entity_id) + assert child_entity + assert child_entity.unique_id == f"{DEVICE_ID}0{fan_id}" + assert child_entity.device_id == entity.device_id diff --git a/tests/components/tplink/test_init.py b/tests/components/tplink/test_init.py index 481a9e0e2b3..61ec9decc10 100644 --- a/tests/components/tplink/test_init.py +++ b/tests/components/tplink/test_init.py @@ -4,10 +4,10 @@ from __future__ import annotations import copy from datetime import timedelta -from unittest.mock import AsyncMock, MagicMock, patch +from unittest.mock import AsyncMock, MagicMock, PropertyMock, patch from freezegun.api import FrozenDateTimeFactory -from kasa.exceptions import AuthenticationException +from kasa import AuthenticationError, Feature, KasaException, Module import pytest from homeassistant import setup @@ -21,19 +21,20 @@ from homeassistant.const import ( CONF_USERNAME, STATE_ON, STATE_UNAVAILABLE, + EntityCategory, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity_registry import EntityRegistry +from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util from . import ( CREATE_ENTRY_DATA_AUTH, + CREATE_ENTRY_DATA_LEGACY, DEVICE_CONFIG_AUTH, IP_ADDRESS, MAC_ADDRESS, - _mocked_dimmer, - _mocked_plug, + _mocked_device, _patch_connect, _patch_discovery, _patch_single_discovery, @@ -100,12 +101,12 @@ async def test_config_entry_retry(hass: HomeAssistant) -> None: async def test_dimmer_switch_unique_id_fix_original_entity_still_exists( - hass: HomeAssistant, entity_reg: EntityRegistry + hass: HomeAssistant, entity_reg: er.EntityRegistry ) -> None: """Test no migration happens if the original entity id still exists.""" config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=MAC_ADDRESS) config_entry.add_to_hass(hass) - dimmer = _mocked_dimmer() + dimmer = _mocked_device(alias="My dimmer", modules=[Module.Light]) rollout_unique_id = MAC_ADDRESS.replace(":", "").upper() original_unique_id = tplink.legacy_device_id(dimmer) original_dimmer_entity_reg = entity_reg.async_get_or_create( @@ -129,7 +130,7 @@ async def test_dimmer_switch_unique_id_fix_original_entity_still_exists( _patch_connect(device=dimmer), ): await setup.async_setup_component(hass, DOMAIN, {}) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) migrated_dimmer_entity_reg = entity_reg.async_get_or_create( config_entry=config_entry, @@ -238,8 +239,8 @@ async def test_config_entry_device_config_invalid( @pytest.mark.parametrize( ("error_type", "entry_state", "reauth_flows"), [ - (tplink.AuthenticationException, ConfigEntryState.SETUP_ERROR, True), - (tplink.SmartDeviceException, ConfigEntryState.SETUP_RETRY, False), + (tplink.AuthenticationError, ConfigEntryState.SETUP_ERROR, True), + (tplink.KasaException, ConfigEntryState.SETUP_RETRY, False), ], ids=["invalid-auth", "unknown-error"], ) @@ -275,15 +276,15 @@ async def test_plug_auth_fails(hass: HomeAssistant) -> None: domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS ) config_entry.add_to_hass(hass) - plug = _mocked_plug() - with _patch_discovery(device=plug), _patch_connect(device=plug): + device = _mocked_device(alias="my_plug", features=["state"]) + with _patch_discovery(device=device), _patch_connect(device=device): await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) await hass.async_block_till_done() entity_id = "switch.my_plug" state = hass.states.get(entity_id) assert state.state == STATE_ON - plug.update = AsyncMock(side_effect=AuthenticationException) + device.update = AsyncMock(side_effect=AuthenticationError) async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=30)) await hass.async_block_till_done() @@ -298,3 +299,166 @@ async def test_plug_auth_fails(hass: HomeAssistant) -> None: ) == 1 ) + + +async def test_update_attrs_fails_in_init( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test a smart plug auth failure.""" + config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS + ) + config_entry.add_to_hass(hass) + light = _mocked_device(modules=[Module.Light], alias="my_light") + light_module = light.modules[Module.Light] + p = PropertyMock(side_effect=KasaException) + type(light_module).color_temp = p + light.__str__ = lambda _: "MockLight" + with _patch_discovery(device=light), _patch_connect(device=light): + await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) + await hass.async_block_till_done() + + entity_id = "light.my_light" + entity = entity_registry.async_get(entity_id) + assert entity + state = hass.states.get(entity_id) + assert state.state == STATE_UNAVAILABLE + assert "Unable to read data for MockLight None:" in caplog.text + + +async def test_update_attrs_fails_on_update( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + freezer: FrozenDateTimeFactory, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test a smart plug auth failure.""" + config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS + ) + config_entry.add_to_hass(hass) + light = _mocked_device(modules=[Module.Light], alias="my_light") + light_module = light.modules[Module.Light] + + with _patch_discovery(device=light), _patch_connect(device=light): + await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) + await hass.async_block_till_done() + + entity_id = "light.my_light" + entity = entity_registry.async_get(entity_id) + assert entity + state = hass.states.get(entity_id) + assert state.state == STATE_ON + + p = PropertyMock(side_effect=KasaException) + type(light_module).color_temp = p + light.__str__ = lambda _: "MockLight" + freezer.tick(5) + async_fire_time_changed(hass) + entity = entity_registry.async_get(entity_id) + assert entity + state = hass.states.get(entity_id) + assert state.state == STATE_UNAVAILABLE + assert f"Unable to read data for MockLight {entity_id}:" in caplog.text + # Check only logs once + caplog.clear() + freezer.tick(5) + async_fire_time_changed(hass) + entity = entity_registry.async_get(entity_id) + assert entity + state = hass.states.get(entity_id) + assert state.state == STATE_UNAVAILABLE + assert f"Unable to read data for MockLight {entity_id}:" not in caplog.text + + +async def test_feature_no_category( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test a strip unique id.""" + already_migrated_config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS + ) + already_migrated_config_entry.add_to_hass(hass) + dev = _mocked_device( + alias="my_plug", + features=["led"], + ) + dev.features["led"].category = Feature.Category.Unset + with _patch_discovery(device=dev), _patch_connect(device=dev): + await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) + await hass.async_block_till_done() + + entity_id = "switch.my_plug_led" + entity = entity_registry.async_get(entity_id) + assert entity + assert entity.entity_category == EntityCategory.DIAGNOSTIC + assert "Unhandled category Category.Unset, fallback to DIAGNOSTIC" in caplog.text + + +@pytest.mark.parametrize( + ("identifier_base", "expected_message", "expected_count"), + [ + pytest.param("C0:06:C3:42:54:2B", "Replaced", 1, id="success"), + pytest.param("123456789", "Unable to replace", 3, id="failure"), + ], +) +async def test_unlink_devices( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + caplog: pytest.LogCaptureFixture, + identifier_base, + expected_message, + expected_count, +) -> None: + """Test for unlinking child device ids.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={**CREATE_ENTRY_DATA_LEGACY}, + entry_id="123456", + unique_id="any", + version=1, + minor_version=2, + ) + entry.add_to_hass(hass) + + # Setup initial device registry, with linkages + mac = "C0:06:C3:42:54:2B" + identifiers = [ + (DOMAIN, identifier_base), + (DOMAIN, f"{identifier_base}_0001"), + (DOMAIN, f"{identifier_base}_0002"), + ] + device_registry.async_get_or_create( + config_entry_id="123456", + connections={ + (dr.CONNECTION_NETWORK_MAC, mac.lower()), + }, + identifiers=set(identifiers), + model="hs300", + name="dummy", + ) + device_entries = dr.async_entries_for_config_entry(device_registry, entry.entry_id) + + assert device_entries[0].connections == { + (dr.CONNECTION_NETWORK_MAC, mac.lower()), + } + assert device_entries[0].identifiers == set(identifiers) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + device_entries = dr.async_entries_for_config_entry(device_registry, entry.entry_id) + + assert device_entries[0].connections == {(dr.CONNECTION_NETWORK_MAC, mac.lower())} + # If expected count is 1 will be the first identifier only + expected_identifiers = identifiers[:expected_count] + assert device_entries[0].identifiers == set(expected_identifiers) + assert entry.version == 1 + assert entry.minor_version == 3 + + msg = f"{expected_message} identifiers for device dummy (hs300): {set(identifiers)}" + assert msg in caplog.text diff --git a/tests/components/tplink/test_light.py b/tests/components/tplink/test_light.py index 9f352e7ffc4..c2f40f47e3d 100644 --- a/tests/components/tplink/test_light.py +++ b/tests/components/tplink/test_light.py @@ -5,7 +5,16 @@ from __future__ import annotations from datetime import timedelta from unittest.mock import MagicMock, PropertyMock -from kasa import AuthenticationException, SmartDeviceException, TimeoutException +from kasa import ( + AuthenticationError, + DeviceType, + KasaException, + LightState, + Module, + TimeoutError, +) +from kasa.interfaces import LightEffect +from kasa.iot import IotDevice import pytest from homeassistant.components import tplink @@ -23,6 +32,7 @@ from homeassistant.components.light import ( ATTR_TRANSITION, ATTR_XY_COLOR, DOMAIN as LIGHT_DOMAIN, + EFFECT_OFF, ) from homeassistant.components.tplink.const import DOMAIN from homeassistant.config_entries import SOURCE_REAUTH @@ -34,9 +44,9 @@ from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util from . import ( + DEVICE_ID, MAC_ADDRESS, - _mocked_bulb, - _mocked_smart_light_strip, + _mocked_device, _patch_connect, _patch_discovery, _patch_single_discovery, @@ -45,37 +55,77 @@ from . import ( from tests.common import MockConfigEntry, async_fire_time_changed +@pytest.mark.parametrize( + ("device_type"), + [ + pytest.param(DeviceType.Dimmer, id="Dimmer"), + pytest.param(DeviceType.Bulb, id="Bulb"), + pytest.param(DeviceType.LightStrip, id="LightStrip"), + pytest.param(DeviceType.WallSwitch, id="WallSwitch"), + ], +) async def test_light_unique_id( - hass: HomeAssistant, entity_registry: er.EntityRegistry + hass: HomeAssistant, entity_registry: er.EntityRegistry, device_type ) -> None: """Test a light unique id.""" already_migrated_config_entry = MockConfigEntry( domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS ) already_migrated_config_entry.add_to_hass(hass) - bulb = _mocked_bulb() - bulb.color_temp = None - with _patch_discovery(device=bulb), _patch_connect(device=bulb): + light = _mocked_device(modules=[Module.Light], alias="my_light") + light.device_type = device_type + with _patch_discovery(device=light), _patch_connect(device=light): await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) await hass.async_block_till_done() - entity_id = "light.my_bulb" - assert entity_registry.async_get(entity_id).unique_id == "AABBCCDDEEFF" + entity_id = "light.my_light" + assert ( + entity_registry.async_get(entity_id).unique_id + == MAC_ADDRESS.replace(":", "").upper() + ) + + +async def test_legacy_dimmer_unique_id(hass: HomeAssistant) -> None: + """Test a light unique id.""" + already_migrated_config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS + ) + already_migrated_config_entry.add_to_hass(hass) + light = _mocked_device( + modules=[Module.Light], + alias="my_light", + spec=IotDevice, + device_id="aa:bb:cc:dd:ee:ff", + ) + light.device_type = DeviceType.Dimmer + + with _patch_discovery(device=light), _patch_connect(device=light): + await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) + await hass.async_block_till_done() + + entity_id = "light.my_light" + entity_registry = er.async_get(hass) + assert entity_registry.async_get(entity_id).unique_id == "aa:bb:cc:dd:ee:ff" @pytest.mark.parametrize( - ("bulb", "transition"), [(_mocked_bulb(), 2.0), (_mocked_smart_light_strip(), None)] + ("device", "transition"), + [ + (_mocked_device(modules=[Module.Light]), 2.0), + (_mocked_device(modules=[Module.Light, Module.LightEffect]), None), + ], ) async def test_color_light( - hass: HomeAssistant, bulb: MagicMock, transition: float | None + hass: HomeAssistant, device: MagicMock, transition: float | None ) -> None: """Test a color light and that all transitions are correctly passed.""" already_migrated_config_entry = MockConfigEntry( domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS ) already_migrated_config_entry.add_to_hass(hass) - bulb.color_temp = None - with _patch_discovery(device=bulb), _patch_connect(device=bulb): + light = device.modules[Module.Light] + light.color_temp = None + with _patch_discovery(device=device), _patch_connect(device=device): await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) await hass.async_block_till_done() @@ -101,11 +151,16 @@ async def test_color_light( await hass.services.async_call( LIGHT_DOMAIN, "turn_off", BASE_PAYLOAD, blocking=True ) - bulb.turn_off.assert_called_once_with(transition=KASA_TRANSITION_VALUE) + light.set_state.assert_called_once_with( + LightState(light_on=False, transition=KASA_TRANSITION_VALUE) + ) + light.set_state.reset_mock() await hass.services.async_call(LIGHT_DOMAIN, "turn_on", BASE_PAYLOAD, blocking=True) - bulb.turn_on.assert_called_once_with(transition=KASA_TRANSITION_VALUE) - bulb.turn_on.reset_mock() + light.set_state.assert_called_once_with( + LightState(light_on=True, transition=KASA_TRANSITION_VALUE) + ) + light.set_state.reset_mock() await hass.services.async_call( LIGHT_DOMAIN, @@ -113,8 +168,8 @@ async def test_color_light( {**BASE_PAYLOAD, ATTR_BRIGHTNESS: 100}, blocking=True, ) - bulb.set_brightness.assert_called_with(39, transition=KASA_TRANSITION_VALUE) - bulb.set_brightness.reset_mock() + light.set_brightness.assert_called_with(39, transition=KASA_TRANSITION_VALUE) + light.set_brightness.reset_mock() await hass.services.async_call( LIGHT_DOMAIN, @@ -122,10 +177,10 @@ async def test_color_light( {**BASE_PAYLOAD, ATTR_COLOR_TEMP_KELVIN: 6666}, blocking=True, ) - bulb.set_color_temp.assert_called_with( + light.set_color_temp.assert_called_with( 6666, brightness=None, transition=KASA_TRANSITION_VALUE ) - bulb.set_color_temp.reset_mock() + light.set_color_temp.reset_mock() await hass.services.async_call( LIGHT_DOMAIN, @@ -133,10 +188,10 @@ async def test_color_light( {**BASE_PAYLOAD, ATTR_COLOR_TEMP_KELVIN: 6666}, blocking=True, ) - bulb.set_color_temp.assert_called_with( + light.set_color_temp.assert_called_with( 6666, brightness=None, transition=KASA_TRANSITION_VALUE ) - bulb.set_color_temp.reset_mock() + light.set_color_temp.reset_mock() await hass.services.async_call( LIGHT_DOMAIN, @@ -144,8 +199,8 @@ async def test_color_light( {**BASE_PAYLOAD, ATTR_HS_COLOR: (10, 30)}, blocking=True, ) - bulb.set_hsv.assert_called_with(10, 30, None, transition=KASA_TRANSITION_VALUE) - bulb.set_hsv.reset_mock() + light.set_hsv.assert_called_with(10, 30, None, transition=KASA_TRANSITION_VALUE) + light.set_hsv.reset_mock() async def test_color_light_no_temp(hass: HomeAssistant) -> None: @@ -154,14 +209,15 @@ async def test_color_light_no_temp(hass: HomeAssistant) -> None: domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS ) already_migrated_config_entry.add_to_hass(hass) - bulb = _mocked_bulb() - bulb.is_variable_color_temp = False - type(bulb).color_temp = PropertyMock(side_effect=Exception) - with _patch_discovery(device=bulb), _patch_connect(device=bulb): + device = _mocked_device(modules=[Module.Light], alias="my_light") + light = device.modules[Module.Light] + light.is_variable_color_temp = False + type(light).color_temp = PropertyMock(side_effect=Exception) + with _patch_discovery(device=device), _patch_connect(device=device): await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) await hass.async_block_till_done() - entity_id = "light.my_bulb" + entity_id = "light.my_light" state = hass.states.get(entity_id) assert state.state == "on" @@ -176,13 +232,14 @@ async def test_color_light_no_temp(hass: HomeAssistant) -> None: await hass.services.async_call( LIGHT_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True ) - bulb.turn_off.assert_called_once() + light.set_state.assert_called_once() + light.set_state.reset_mock() await hass.services.async_call( LIGHT_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True ) - bulb.turn_on.assert_called_once() - bulb.turn_on.reset_mock() + light.set_state.assert_called_once() + light.set_state.reset_mock() await hass.services.async_call( LIGHT_DOMAIN, @@ -190,8 +247,8 @@ async def test_color_light_no_temp(hass: HomeAssistant) -> None: {ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 100}, blocking=True, ) - bulb.set_brightness.assert_called_with(39, transition=None) - bulb.set_brightness.reset_mock() + light.set_brightness.assert_called_with(39, transition=None) + light.set_brightness.reset_mock() await hass.services.async_call( LIGHT_DOMAIN, @@ -199,12 +256,16 @@ async def test_color_light_no_temp(hass: HomeAssistant) -> None: {ATTR_ENTITY_ID: entity_id, ATTR_HS_COLOR: (10, 30)}, blocking=True, ) - bulb.set_hsv.assert_called_with(10, 30, None, transition=None) - bulb.set_hsv.reset_mock() + light.set_hsv.assert_called_with(10, 30, None, transition=None) + light.set_hsv.reset_mock() @pytest.mark.parametrize( - ("bulb", "is_color"), [(_mocked_bulb(), True), (_mocked_smart_light_strip(), False)] + ("bulb", "is_color"), + [ + (_mocked_device(modules=[Module.Light], alias="my_light"), True), + (_mocked_device(modules=[Module.Light], alias="my_light"), False), + ], ) async def test_color_temp_light( hass: HomeAssistant, bulb: MagicMock, is_color: bool @@ -214,22 +275,24 @@ async def test_color_temp_light( domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS ) already_migrated_config_entry.add_to_hass(hass) - bulb.is_color = is_color - bulb.color_temp = 4000 - bulb.is_variable_color_temp = True + device = _mocked_device(modules=[Module.Light], alias="my_light") + light = device.modules[Module.Light] + light.is_color = is_color + light.color_temp = 4000 + light.is_variable_color_temp = True - with _patch_discovery(device=bulb), _patch_connect(device=bulb): + with _patch_discovery(device=device), _patch_connect(device=device): await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) await hass.async_block_till_done() - entity_id = "light.my_bulb" + entity_id = "light.my_light" state = hass.states.get(entity_id) assert state.state == "on" attributes = state.attributes assert attributes[ATTR_BRIGHTNESS] == 128 assert attributes[ATTR_COLOR_MODE] == "color_temp" - if bulb.is_color: + if light.is_color: assert attributes[ATTR_SUPPORTED_COLOR_MODES] == ["color_temp", "hs"] else: assert attributes[ATTR_SUPPORTED_COLOR_MODES] == ["color_temp"] @@ -240,13 +303,14 @@ async def test_color_temp_light( await hass.services.async_call( LIGHT_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True ) - bulb.turn_off.assert_called_once() + light.set_state.assert_called_once() + light.set_state.reset_mock() await hass.services.async_call( LIGHT_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True ) - bulb.turn_on.assert_called_once() - bulb.turn_on.reset_mock() + light.set_state.assert_called_once() + light.set_state.reset_mock() await hass.services.async_call( LIGHT_DOMAIN, @@ -254,8 +318,8 @@ async def test_color_temp_light( {ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 100}, blocking=True, ) - bulb.set_brightness.assert_called_with(39, transition=None) - bulb.set_brightness.reset_mock() + light.set_brightness.assert_called_with(39, transition=None) + light.set_brightness.reset_mock() await hass.services.async_call( LIGHT_DOMAIN, @@ -263,8 +327,8 @@ async def test_color_temp_light( {ATTR_ENTITY_ID: entity_id, ATTR_COLOR_TEMP_KELVIN: 6666}, blocking=True, ) - bulb.set_color_temp.assert_called_with(6666, brightness=None, transition=None) - bulb.set_color_temp.reset_mock() + light.set_color_temp.assert_called_with(6666, brightness=None, transition=None) + light.set_color_temp.reset_mock() # Verify color temp is clamped to the valid range await hass.services.async_call( @@ -273,8 +337,8 @@ async def test_color_temp_light( {ATTR_ENTITY_ID: entity_id, ATTR_COLOR_TEMP_KELVIN: 20000}, blocking=True, ) - bulb.set_color_temp.assert_called_with(9000, brightness=None, transition=None) - bulb.set_color_temp.reset_mock() + light.set_color_temp.assert_called_with(9000, brightness=None, transition=None) + light.set_color_temp.reset_mock() # Verify color temp is clamped to the valid range await hass.services.async_call( @@ -283,8 +347,8 @@ async def test_color_temp_light( {ATTR_ENTITY_ID: entity_id, ATTR_COLOR_TEMP_KELVIN: 1}, blocking=True, ) - bulb.set_color_temp.assert_called_with(4000, brightness=None, transition=None) - bulb.set_color_temp.reset_mock() + light.set_color_temp.assert_called_with(4000, brightness=None, transition=None) + light.set_color_temp.reset_mock() async def test_brightness_only_light(hass: HomeAssistant) -> None: @@ -293,15 +357,16 @@ async def test_brightness_only_light(hass: HomeAssistant) -> None: domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS ) already_migrated_config_entry.add_to_hass(hass) - bulb = _mocked_bulb() - bulb.is_color = False - bulb.is_variable_color_temp = False + device = _mocked_device(modules=[Module.Light], alias="my_light") + light = device.modules[Module.Light] + light.is_color = False + light.is_variable_color_temp = False - with _patch_discovery(device=bulb), _patch_connect(device=bulb): + with _patch_discovery(device=device), _patch_connect(device=device): await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) await hass.async_block_till_done() - entity_id = "light.my_bulb" + entity_id = "light.my_light" state = hass.states.get(entity_id) assert state.state == "on" @@ -313,13 +378,14 @@ async def test_brightness_only_light(hass: HomeAssistant) -> None: await hass.services.async_call( LIGHT_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True ) - bulb.turn_off.assert_called_once() + light.set_state.assert_called_once() + light.set_state.reset_mock() await hass.services.async_call( LIGHT_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True ) - bulb.turn_on.assert_called_once() - bulb.turn_on.reset_mock() + light.set_state.assert_called_once() + light.set_state.reset_mock() await hass.services.async_call( LIGHT_DOMAIN, @@ -327,8 +393,8 @@ async def test_brightness_only_light(hass: HomeAssistant) -> None: {ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 100}, blocking=True, ) - bulb.set_brightness.assert_called_with(39, transition=None) - bulb.set_brightness.reset_mock() + light.set_brightness.assert_called_with(39, transition=None) + light.set_brightness.reset_mock() async def test_on_off_light(hass: HomeAssistant) -> None: @@ -337,16 +403,17 @@ async def test_on_off_light(hass: HomeAssistant) -> None: domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS ) already_migrated_config_entry.add_to_hass(hass) - bulb = _mocked_bulb() - bulb.is_color = False - bulb.is_variable_color_temp = False - bulb.is_dimmable = False + device = _mocked_device(modules=[Module.Light], alias="my_light") + light = device.modules[Module.Light] + light.is_color = False + light.is_variable_color_temp = False + light.is_dimmable = False - with _patch_discovery(device=bulb), _patch_connect(device=bulb): + with _patch_discovery(device=device), _patch_connect(device=device): await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) await hass.async_block_till_done() - entity_id = "light.my_bulb" + entity_id = "light.my_light" state = hass.states.get(entity_id) assert state.state == "on" @@ -356,13 +423,14 @@ async def test_on_off_light(hass: HomeAssistant) -> None: await hass.services.async_call( LIGHT_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True ) - bulb.turn_off.assert_called_once() + light.set_state.assert_called_once() + light.set_state.reset_mock() await hass.services.async_call( LIGHT_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True ) - bulb.turn_on.assert_called_once() - bulb.turn_on.reset_mock() + light.set_state.assert_called_once() + light.set_state.reset_mock() async def test_off_at_start_light(hass: HomeAssistant) -> None: @@ -371,17 +439,18 @@ async def test_off_at_start_light(hass: HomeAssistant) -> None: domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS ) already_migrated_config_entry.add_to_hass(hass) - bulb = _mocked_bulb() - bulb.is_color = False - bulb.is_variable_color_temp = False - bulb.is_dimmable = False - bulb.is_on = False + device = _mocked_device(modules=[Module.Light], alias="my_light") + light = device.modules[Module.Light] + light.is_color = False + light.is_variable_color_temp = False + light.is_dimmable = False + light.state = LightState(light_on=False) - with _patch_discovery(device=bulb), _patch_connect(device=bulb): + with _patch_discovery(device=device), _patch_connect(device=device): await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) await hass.async_block_till_done() - entity_id = "light.my_bulb" + entity_id = "light.my_light" state = hass.states.get(entity_id) assert state.state == "off" @@ -395,15 +464,16 @@ async def test_dimmer_turn_on_fix(hass: HomeAssistant) -> None: domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS ) already_migrated_config_entry.add_to_hass(hass) - bulb = _mocked_bulb() - bulb.is_dimmer = True - bulb.is_on = False + device = _mocked_device(modules=[Module.Light], alias="my_light") + light = device.modules[Module.Light] + device.device_type = DeviceType.Dimmer + light.state = LightState(light_on=False) - with _patch_discovery(device=bulb), _patch_connect(device=bulb): + with _patch_discovery(device=device), _patch_connect(device=device): await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) await hass.async_block_till_done() - entity_id = "light.my_bulb" + entity_id = "light.my_light" state = hass.states.get(entity_id) assert state.state == "off" @@ -411,8 +481,17 @@ async def test_dimmer_turn_on_fix(hass: HomeAssistant) -> None: await hass.services.async_call( LIGHT_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True ) - bulb.turn_on.assert_called_once_with(transition=1) - bulb.turn_on.reset_mock() + light.set_state.assert_called_once_with( + LightState( + light_on=True, + brightness=None, + hue=None, + saturation=None, + color_temp=None, + transition=1, + ) + ) + light.set_state.reset_mock() async def test_smart_strip_effects(hass: HomeAssistant) -> None: @@ -421,22 +500,26 @@ async def test_smart_strip_effects(hass: HomeAssistant) -> None: domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS ) already_migrated_config_entry.add_to_hass(hass) - strip = _mocked_smart_light_strip() + device = _mocked_device( + modules=[Module.Light, Module.LightEffect], alias="my_light" + ) + light = device.modules[Module.Light] + light_effect = device.modules[Module.LightEffect] with ( - _patch_discovery(device=strip), - _patch_single_discovery(device=strip), - _patch_connect(device=strip), + _patch_discovery(device=device), + _patch_single_discovery(device=device), + _patch_connect(device=device), ): await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) await hass.async_block_till_done() - entity_id = "light.my_bulb" + entity_id = "light.my_light" state = hass.states.get(entity_id) assert state.state == STATE_ON assert state.attributes[ATTR_EFFECT] == "Effect1" - assert state.attributes[ATTR_EFFECT_LIST] == ["Effect1", "Effect2"] + assert state.attributes[ATTR_EFFECT_LIST] == ["Off", "Effect1", "Effect2"] # Ensure setting color temp when an effect # is in progress calls set_hsv to clear the effect @@ -446,10 +529,10 @@ async def test_smart_strip_effects(hass: HomeAssistant) -> None: {ATTR_ENTITY_ID: entity_id, ATTR_COLOR_TEMP_KELVIN: 4000}, blocking=True, ) - strip.set_hsv.assert_called_once_with(0, 0, None) - strip.set_color_temp.assert_called_once_with(4000, brightness=None, transition=None) - strip.set_hsv.reset_mock() - strip.set_color_temp.reset_mock() + light.set_hsv.assert_called_once_with(0, 0, None) + light.set_color_temp.assert_called_once_with(4000, brightness=None, transition=None) + light.set_hsv.reset_mock() + light.set_color_temp.reset_mock() await hass.services.async_call( LIGHT_DOMAIN, @@ -457,21 +540,20 @@ async def test_smart_strip_effects(hass: HomeAssistant) -> None: {ATTR_ENTITY_ID: entity_id, ATTR_EFFECT: "Effect2"}, blocking=True, ) - strip.set_effect.assert_called_once_with( + light_effect.set_effect.assert_called_once_with( "Effect2", brightness=None, transition=None ) - strip.set_effect.reset_mock() + light_effect.set_effect.reset_mock() - strip.effect = {"name": "Effect1", "enable": 0, "custom": 0} + light_effect.effect = LightEffect.LIGHT_EFFECTS_OFF async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10)) await hass.async_block_till_done() state = hass.states.get(entity_id) assert state.state == STATE_ON - assert state.attributes[ATTR_EFFECT] is None + assert state.attributes[ATTR_EFFECT] == EFFECT_OFF - strip.is_off = True - strip.is_on = False + light.state = LightState(light_on=False) async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=20)) await hass.async_block_till_done() @@ -485,12 +567,11 @@ async def test_smart_strip_effects(hass: HomeAssistant) -> None: {ATTR_ENTITY_ID: entity_id}, blocking=True, ) - strip.turn_on.assert_called_once() - strip.turn_on.reset_mock() + light.set_state.assert_called_once() + light.set_state.reset_mock() - strip.is_off = False - strip.is_on = True - strip.effect_list = None + light.state = LightState(light_on=True) + light_effect.effect_list = None async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=30)) await hass.async_block_till_done() @@ -505,13 +586,17 @@ async def test_smart_strip_custom_random_effect(hass: HomeAssistant) -> None: domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS ) already_migrated_config_entry.add_to_hass(hass) - strip = _mocked_smart_light_strip() + device = _mocked_device( + modules=[Module.Light, Module.LightEffect], alias="my_light" + ) + light = device.modules[Module.Light] + light_effect = device.modules[Module.LightEffect] - with _patch_discovery(device=strip), _patch_connect(device=strip): + with _patch_discovery(device=device), _patch_connect(device=device): await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) await hass.async_block_till_done() - entity_id = "light.my_bulb" + entity_id = "light.my_light" state = hass.states.get(entity_id) assert state.state == STATE_ON @@ -526,7 +611,7 @@ async def test_smart_strip_custom_random_effect(hass: HomeAssistant) -> None: }, blocking=True, ) - strip.set_custom_effect.assert_called_once_with( + light_effect.set_custom_effect.assert_called_once_with( { "custom": 1, "id": "yMwcNpLxijmoKamskHCvvravpbnIqAIN", @@ -543,7 +628,7 @@ async def test_smart_strip_custom_random_effect(hass: HomeAssistant) -> None: "backgrounds": [(340, 20, 50), (20, 50, 50), (0, 100, 50)], } ) - strip.set_custom_effect.reset_mock() + light_effect.set_custom_effect.reset_mock() await hass.services.async_call( DOMAIN, @@ -555,7 +640,7 @@ async def test_smart_strip_custom_random_effect(hass: HomeAssistant) -> None: }, blocking=True, ) - strip.set_custom_effect.assert_called_once_with( + light_effect.set_custom_effect.assert_called_once_with( { "custom": 1, "id": "yMwcNpLxijmoKamskHCvvravpbnIqAIN", @@ -571,9 +656,9 @@ async def test_smart_strip_custom_random_effect(hass: HomeAssistant) -> None: "random_seed": 600, } ) - strip.set_custom_effect.reset_mock() + light_effect.set_custom_effect.reset_mock() - strip.effect = { + light_effect.effect = { "custom": 1, "id": "yMwcNpLxijmoKamskHCvvravpbnIqAIN", "brightness": 100, @@ -586,15 +671,8 @@ async def test_smart_strip_custom_random_effect(hass: HomeAssistant) -> None: state = hass.states.get(entity_id) assert state.state == STATE_ON - strip.is_off = True - strip.is_on = False - strip.effect = { - "custom": 1, - "id": "yMwcNpLxijmoKamskHCvvravpbnIqAIN", - "brightness": 100, - "name": "Custom", - "enable": 0, - } + light.state = LightState(light_on=False) + light_effect.effect = LightEffect.LIGHT_EFFECTS_OFF async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=20)) await hass.async_block_till_done() @@ -608,8 +686,8 @@ async def test_smart_strip_custom_random_effect(hass: HomeAssistant) -> None: {ATTR_ENTITY_ID: entity_id}, blocking=True, ) - strip.turn_on.assert_called_once() - strip.turn_on.reset_mock() + light.set_state.assert_called_once() + light.set_state.reset_mock() await hass.services.async_call( DOMAIN, @@ -631,7 +709,7 @@ async def test_smart_strip_custom_random_effect(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - strip.set_custom_effect.assert_called_once_with( + light_effect.set_custom_effect.assert_called_once_with( { "custom": 1, "id": "yMwcNpLxijmoKamskHCvvravpbnIqAIN", @@ -653,7 +731,7 @@ async def test_smart_strip_custom_random_effect(hass: HomeAssistant) -> None: "transition_range": [2000, 3000], } ) - strip.set_custom_effect.reset_mock() + light_effect.set_custom_effect.reset_mock() async def test_smart_strip_custom_random_effect_at_start(hass: HomeAssistant) -> None: @@ -662,19 +740,17 @@ async def test_smart_strip_custom_random_effect_at_start(hass: HomeAssistant) -> domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS ) already_migrated_config_entry.add_to_hass(hass) - strip = _mocked_smart_light_strip() - strip.effect = { - "custom": 1, - "id": "yMwcNpLxijmoKamskHCvvravpbnIqAIN", - "brightness": 100, - "name": "Custom", - "enable": 0, - } - with _patch_discovery(device=strip), _patch_connect(device=strip): + device = _mocked_device( + modules=[Module.Light, Module.LightEffect], alias="my_light" + ) + light = device.modules[Module.Light] + light_effect = device.modules[Module.LightEffect] + light_effect.effect = LightEffect.LIGHT_EFFECTS_OFF + with _patch_discovery(device=device), _patch_connect(device=device): await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) await hass.async_block_till_done() - entity_id = "light.my_bulb" + entity_id = "light.my_light" state = hass.states.get(entity_id) assert state.state == STATE_ON @@ -685,8 +761,8 @@ async def test_smart_strip_custom_random_effect_at_start(hass: HomeAssistant) -> {ATTR_ENTITY_ID: entity_id}, blocking=True, ) - strip.turn_on.assert_called_once() - strip.turn_on.reset_mock() + light.set_state.assert_called_once() + light.set_state.reset_mock() async def test_smart_strip_custom_sequence_effect(hass: HomeAssistant) -> None: @@ -695,13 +771,16 @@ async def test_smart_strip_custom_sequence_effect(hass: HomeAssistant) -> None: domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS ) already_migrated_config_entry.add_to_hass(hass) - strip = _mocked_smart_light_strip() + device = _mocked_device( + modules=[Module.Light, Module.LightEffect], alias="my_light" + ) + light_effect = device.modules[Module.LightEffect] - with _patch_discovery(device=strip), _patch_connect(device=strip): + with _patch_discovery(device=device), _patch_connect(device=device): await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) await hass.async_block_till_done() - entity_id = "light.my_bulb" + entity_id = "light.my_light" state = hass.states.get(entity_id) assert state.state == STATE_ON @@ -715,7 +794,7 @@ async def test_smart_strip_custom_sequence_effect(hass: HomeAssistant) -> None: }, blocking=True, ) - strip.set_custom_effect.assert_called_once_with( + light_effect.set_custom_effect.assert_called_once_with( { "custom": 1, "id": "yMwcNpLxijmoKamskHCvvravpbnIqAIN", @@ -733,24 +812,24 @@ async def test_smart_strip_custom_sequence_effect(hass: HomeAssistant) -> None: "direction": 4, } ) - strip.set_custom_effect.reset_mock() + light_effect.set_custom_effect.reset_mock() @pytest.mark.parametrize( ("exception_type", "msg", "reauth_expected"), [ ( - AuthenticationException, + AuthenticationError, "Device authentication error async_turn_on: test error", True, ), ( - TimeoutException, + TimeoutError, "Timeout communicating with the device async_turn_on: test error", False, ), ( - SmartDeviceException, + KasaException, "Unable to communicate with the device async_turn_on: test error", False, ), @@ -768,14 +847,15 @@ async def test_light_errors_when_turned_on( domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS ) already_migrated_config_entry.add_to_hass(hass) - bulb = _mocked_bulb() - bulb.turn_on.side_effect = exception_type(msg) + device = _mocked_device(modules=[Module.Light], alias="my_light") + light = device.modules[Module.Light] + light.set_state.side_effect = exception_type(msg) - with _patch_discovery(device=bulb), _patch_connect(device=bulb): + with _patch_discovery(device=device), _patch_connect(device=device): await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) await hass.async_block_till_done() - entity_id = "light.my_bulb" + entity_id = "light.my_light" assert not any( already_migrated_config_entry.async_get_active_flows(hass, {SOURCE_REAUTH}) @@ -786,7 +866,7 @@ async def test_light_errors_when_turned_on( LIGHT_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True ) await hass.async_block_till_done() - assert bulb.turn_on.call_count == 1 + assert light.set_state.call_count == 1 assert ( any( flow @@ -797,3 +877,42 @@ async def test_light_errors_when_turned_on( ) == reauth_expected ) + + +async def test_light_child( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, +) -> None: + """Test child lights are added to parent device with the right ids.""" + already_migrated_config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS + ) + already_migrated_config_entry.add_to_hass(hass) + + child_light_1 = _mocked_device( + modules=[Module.Light], alias="my_light_0", device_id=f"{DEVICE_ID}00" + ) + child_light_2 = _mocked_device( + modules=[Module.Light], alias="my_light_1", device_id=f"{DEVICE_ID}01" + ) + parent_device = _mocked_device( + device_id=DEVICE_ID, + alias="my_device", + children=[child_light_1, child_light_2], + modules=[Module.Light], + ) + + with _patch_discovery(device=parent_device), _patch_connect(device=parent_device): + await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) + await hass.async_block_till_done() + + entity_id = "light.my_device" + entity = entity_registry.async_get(entity_id) + assert entity + + for light_id in range(2): + child_entity_id = f"light.my_device_my_light_{light_id}" + child_entity = entity_registry.async_get(child_entity_id) + assert child_entity + assert child_entity.unique_id == f"{DEVICE_ID}0{light_id}" + assert child_entity.device_id == entity.device_id diff --git a/tests/components/tplink/test_number.py b/tests/components/tplink/test_number.py new file mode 100644 index 00000000000..865ce27ffc0 --- /dev/null +++ b/tests/components/tplink/test_number.py @@ -0,0 +1,163 @@ +"""Tests for tplink number platform.""" + +from kasa import Feature +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components import tplink +from homeassistant.components.number import ( + ATTR_VALUE, + DOMAIN as NUMBER_DOMAIN, + SERVICE_SET_VALUE, +) +from homeassistant.components.tplink.const import DOMAIN +from homeassistant.components.tplink.entity import EXCLUDED_FEATURES +from homeassistant.components.tplink.number import NUMBER_DESCRIPTIONS +from homeassistant.const import ATTR_ENTITY_ID, CONF_HOST, Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.setup import async_setup_component + +from . import ( + DEVICE_ID, + MAC_ADDRESS, + _mocked_device, + _mocked_feature, + _mocked_strip_children, + _patch_connect, + _patch_discovery, + setup_platform_for_device, + snapshot_platform, +) + +from tests.common import MockConfigEntry + + +async def test_states( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, + device_registry: dr.DeviceRegistry, + snapshot: SnapshotAssertion, +) -> None: + """Test a sensor unique ids.""" + features = {description.key for description in NUMBER_DESCRIPTIONS} + features.update(EXCLUDED_FEATURES) + device = _mocked_device(alias="my_device", features=features) + + await setup_platform_for_device(hass, mock_config_entry, Platform.NUMBER, device) + await snapshot_platform( + hass, entity_registry, device_registry, snapshot, mock_config_entry.entry_id + ) + + for excluded in EXCLUDED_FEATURES: + assert hass.states.get(f"sensor.my_device_{excluded}") is None + + +async def test_number(hass: HomeAssistant, entity_registry: er.EntityRegistry) -> None: + """Test a sensor unique ids.""" + already_migrated_config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS + ) + already_migrated_config_entry.add_to_hass(hass) + new_feature = _mocked_feature( + "temperature_offset", + value=10, + name="Temperature offset", + type_=Feature.Type.Number, + category=Feature.Category.Config, + minimum_value=1, + maximum_value=100, + ) + plug = _mocked_device(alias="my_plug", features=[new_feature]) + with _patch_discovery(device=plug), _patch_connect(device=plug): + await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) + await hass.async_block_till_done() + + entity_id = "number.my_plug_temperature_offset" + entity = entity_registry.async_get(entity_id) + assert entity + assert entity.unique_id == f"{DEVICE_ID}_temperature_offset" + + +async def test_number_children( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + device_registry: dr.DeviceRegistry, +) -> None: + """Test a sensor unique ids.""" + already_migrated_config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS + ) + already_migrated_config_entry.add_to_hass(hass) + new_feature = _mocked_feature( + "temperature_offset", + value=10, + name="Some number", + type_=Feature.Type.Number, + category=Feature.Category.Config, + minimum_value=1, + maximum_value=100, + ) + plug = _mocked_device( + alias="my_plug", + features=[new_feature], + children=_mocked_strip_children(features=[new_feature]), + ) + with _patch_discovery(device=plug), _patch_connect(device=plug): + await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) + await hass.async_block_till_done() + + entity_id = "number.my_plug_temperature_offset" + entity = entity_registry.async_get(entity_id) + assert entity + device = device_registry.async_get(entity.device_id) + + for plug_id in range(2): + child_entity_id = f"number.my_plug_plug{plug_id}_temperature_offset" + child_entity = entity_registry.async_get(child_entity_id) + assert child_entity + assert child_entity.unique_id == f"PLUG{plug_id}DEVICEID_temperature_offset" + assert child_entity.device_id != entity.device_id + child_device = device_registry.async_get(child_entity.device_id) + assert child_device + assert child_device.via_device_id == device.id + + +async def test_number_set( + hass: HomeAssistant, entity_registry: er.EntityRegistry +) -> None: + """Test a number entity limits and setting values.""" + already_migrated_config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS + ) + already_migrated_config_entry.add_to_hass(hass) + new_feature = _mocked_feature( + "temperature_offset", + value=10, + name="Some number", + type_=Feature.Type.Number, + category=Feature.Category.Config, + minimum_value=1, + maximum_value=200, + ) + plug = _mocked_device(alias="my_plug", features=[new_feature]) + with _patch_discovery(device=plug), _patch_connect(device=plug): + await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) + await hass.async_block_till_done() + + entity_id = "number.my_plug_temperature_offset" + entity = entity_registry.async_get(entity_id) + assert entity + assert entity.unique_id == f"{DEVICE_ID}_temperature_offset" + + state = hass.states.get(entity_id) + assert state is not None + assert state.state == "10" + + await hass.services.async_call( + NUMBER_DOMAIN, + SERVICE_SET_VALUE, + {ATTR_ENTITY_ID: entity_id, ATTR_VALUE: 50}, + blocking=True, + ) + new_feature.set_value.assert_called_with(50) diff --git a/tests/components/tplink/test_select.py b/tests/components/tplink/test_select.py new file mode 100644 index 00000000000..6c49185d91c --- /dev/null +++ b/tests/components/tplink/test_select.py @@ -0,0 +1,158 @@ +"""Tests for tplink select platform.""" + +from kasa import Feature +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components import tplink +from homeassistant.components.select import ( + ATTR_OPTION, + DOMAIN as SELECT_DOMAIN, + SERVICE_SELECT_OPTION, +) +from homeassistant.components.tplink.const import DOMAIN +from homeassistant.components.tplink.entity import EXCLUDED_FEATURES +from homeassistant.components.tplink.select import SELECT_DESCRIPTIONS +from homeassistant.const import ATTR_ENTITY_ID, CONF_HOST, Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.setup import async_setup_component + +from . import ( + DEVICE_ID, + MAC_ADDRESS, + _mocked_device, + _mocked_feature, + _mocked_strip_children, + _patch_connect, + _patch_discovery, + setup_platform_for_device, + snapshot_platform, +) + +from tests.common import MockConfigEntry + + +@pytest.fixture +def mocked_feature_select() -> Feature: + """Return mocked tplink binary sensor feature.""" + return _mocked_feature( + "light_preset", + value="First choice", + name="light_preset", + choices=["First choice", "Second choice"], + type_=Feature.Type.Choice, + category=Feature.Category.Config, + ) + + +async def test_states( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, + device_registry: dr.DeviceRegistry, + snapshot: SnapshotAssertion, +) -> None: + """Test a sensor unique ids.""" + features = {description.key for description in SELECT_DESCRIPTIONS} + features.update(EXCLUDED_FEATURES) + device = _mocked_device(alias="my_device", features=features) + + await setup_platform_for_device(hass, mock_config_entry, Platform.SELECT, device) + await snapshot_platform( + hass, entity_registry, device_registry, snapshot, mock_config_entry.entry_id + ) + + for excluded in EXCLUDED_FEATURES: + assert hass.states.get(f"sensor.my_device_{excluded}") is None + + +async def test_select( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + mocked_feature_select: Feature, +) -> None: + """Test a sensor unique ids.""" + mocked_feature = mocked_feature_select + already_migrated_config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS + ) + already_migrated_config_entry.add_to_hass(hass) + + plug = _mocked_device(alias="my_plug", features=[mocked_feature]) + with _patch_discovery(device=plug), _patch_connect(device=plug): + await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) + await hass.async_block_till_done() + + # The entity_id is based on standard name from core. + entity_id = "select.my_plug_light_preset" + entity = entity_registry.async_get(entity_id) + assert entity + assert entity.unique_id == f"{DEVICE_ID}_{mocked_feature.id}" + + +async def test_select_children( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + device_registry: dr.DeviceRegistry, + mocked_feature_select: Feature, +) -> None: + """Test a sensor unique ids.""" + mocked_feature = mocked_feature_select + already_migrated_config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS + ) + already_migrated_config_entry.add_to_hass(hass) + plug = _mocked_device( + alias="my_plug", + features=[mocked_feature], + children=_mocked_strip_children(features=[mocked_feature]), + ) + with _patch_discovery(device=plug), _patch_connect(device=plug): + await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) + await hass.async_block_till_done() + + entity_id = "select.my_plug_light_preset" + entity = entity_registry.async_get(entity_id) + assert entity + device = device_registry.async_get(entity.device_id) + + for plug_id in range(2): + child_entity_id = f"select.my_plug_plug{plug_id}_light_preset" + child_entity = entity_registry.async_get(child_entity_id) + assert child_entity + assert child_entity.unique_id == f"PLUG{plug_id}DEVICEID_{mocked_feature.id}" + assert child_entity.device_id != entity.device_id + child_device = device_registry.async_get(child_entity.device_id) + assert child_device + assert child_device.via_device_id == device.id + + +async def test_select_select( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + mocked_feature_select: Feature, +) -> None: + """Test a select setting values.""" + mocked_feature = mocked_feature_select + already_migrated_config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS + ) + already_migrated_config_entry.add_to_hass(hass) + plug = _mocked_device(alias="my_plug", features=[mocked_feature]) + with _patch_discovery(device=plug), _patch_connect(device=plug): + await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) + await hass.async_block_till_done() + + entity_id = "select.my_plug_light_preset" + entity = entity_registry.async_get(entity_id) + assert entity + assert entity.unique_id == f"{DEVICE_ID}_light_preset" + + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + {ATTR_ENTITY_ID: entity_id, ATTR_OPTION: "Second choice"}, + blocking=True, + ) + mocked_feature.set_value.assert_called_with("Second choice") diff --git a/tests/components/tplink/test_sensor.py b/tests/components/tplink/test_sensor.py index 43884083483..dda43c52430 100644 --- a/tests/components/tplink/test_sensor.py +++ b/tests/components/tplink/test_sensor.py @@ -1,35 +1,71 @@ """Tests for light platform.""" -from unittest.mock import Mock +from kasa import Device, Feature, Module +import pytest +from syrupy.assertion import SnapshotAssertion from homeassistant.components import tplink from homeassistant.components.tplink.const import DOMAIN -from homeassistant.const import CONF_HOST +from homeassistant.components.tplink.entity import EXCLUDED_FEATURES +from homeassistant.components.tplink.sensor import SENSOR_DESCRIPTIONS +from homeassistant.const import CONF_HOST, Platform from homeassistant.core import HomeAssistant -from homeassistant.helpers import entity_registry as er +from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.setup import async_setup_component -from . import MAC_ADDRESS, _mocked_bulb, _mocked_plug, _patch_connect, _patch_discovery +from . import ( + DEVICE_ID, + MAC_ADDRESS, + _mocked_device, + _mocked_energy_features, + _mocked_feature, + _mocked_strip_children, + _patch_connect, + _patch_discovery, + setup_platform_for_device, + snapshot_platform, +) from tests.common import MockConfigEntry +async def test_states( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, + device_registry: dr.DeviceRegistry, + snapshot: SnapshotAssertion, +) -> None: + """Test a sensor unique ids.""" + features = {description.key for description in SENSOR_DESCRIPTIONS} + features.update(EXCLUDED_FEATURES) + device = _mocked_device(alias="my_device", features=features) + + await setup_platform_for_device(hass, mock_config_entry, Platform.SENSOR, device) + await snapshot_platform( + hass, entity_registry, device_registry, snapshot, mock_config_entry.entry_id + ) + + for excluded in EXCLUDED_FEATURES: + assert hass.states.get(f"sensor.my_device_{excluded}") is None + + async def test_color_light_with_an_emeter(hass: HomeAssistant) -> None: """Test a light with an emeter.""" already_migrated_config_entry = MockConfigEntry( domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS ) already_migrated_config_entry.add_to_hass(hass) - bulb = _mocked_bulb() - bulb.color_temp = None - bulb.has_emeter = True - bulb.emeter_realtime = Mock( + emeter_features = _mocked_energy_features( power=None, total=None, voltage=None, current=5, + today=5000.0036, + ) + bulb = _mocked_device( + alias="my_bulb", modules=[Module.Light], features=["state", *emeter_features] ) - bulb.emeter_today = 5000.0036 with _patch_discovery(device=bulb), _patch_connect(device=bulb): await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) await hass.async_block_till_done() @@ -60,16 +96,13 @@ async def test_plug_with_an_emeter(hass: HomeAssistant) -> None: domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS ) already_migrated_config_entry.add_to_hass(hass) - plug = _mocked_plug() - plug.color_temp = None - plug.has_emeter = True - plug.emeter_realtime = Mock( + emeter_features = _mocked_energy_features( power=100.06, total=30.0049, voltage=121.19, current=5.035, ) - plug.emeter_today = None + plug = _mocked_device(alias="my_plug", features=["state", *emeter_features]) with _patch_discovery(device=plug), _patch_connect(device=plug): await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) await hass.async_block_till_done() @@ -95,8 +128,7 @@ async def test_color_light_no_emeter(hass: HomeAssistant) -> None: domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS ) already_migrated_config_entry.add_to_hass(hass) - bulb = _mocked_bulb() - bulb.color_temp = None + bulb = _mocked_device(alias="my_bulb", modules=[Module.Light]) bulb.has_emeter = False with _patch_discovery(device=bulb), _patch_connect(device=bulb): await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) @@ -126,26 +158,175 @@ async def test_sensor_unique_id( domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS ) already_migrated_config_entry.add_to_hass(hass) - plug = _mocked_plug() - plug.color_temp = None - plug.has_emeter = True - plug.emeter_realtime = Mock( + emeter_features = _mocked_energy_features( power=100, total=30, voltage=121, current=5, + today=None, ) - plug.emeter_today = None + plug = _mocked_device(alias="my_plug", features=emeter_features) with _patch_discovery(device=plug), _patch_connect(device=plug): await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) await hass.async_block_till_done() expected = { - "sensor.my_plug_current_consumption": "aa:bb:cc:dd:ee:ff_current_power_w", - "sensor.my_plug_total_consumption": "aa:bb:cc:dd:ee:ff_total_energy_kwh", - "sensor.my_plug_today_s_consumption": "aa:bb:cc:dd:ee:ff_today_energy_kwh", - "sensor.my_plug_voltage": "aa:bb:cc:dd:ee:ff_voltage", - "sensor.my_plug_current": "aa:bb:cc:dd:ee:ff_current_a", + "sensor.my_plug_current_consumption": f"{DEVICE_ID}_current_power_w", + "sensor.my_plug_total_consumption": f"{DEVICE_ID}_total_energy_kwh", + "sensor.my_plug_today_s_consumption": f"{DEVICE_ID}_today_energy_kwh", + "sensor.my_plug_voltage": f"{DEVICE_ID}_voltage", + "sensor.my_plug_current": f"{DEVICE_ID}_current_a", } for sensor_entity_id, value in expected.items(): assert entity_registry.async_get(sensor_entity_id).unique_id == value + + +async def test_undefined_sensor( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test a message is logged when discovering a feature without a description.""" + already_migrated_config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS + ) + already_migrated_config_entry.add_to_hass(hass) + new_feature = _mocked_feature( + "consumption_this_fortnight", + value=5.2, + name="Consumption for fortnight", + type_=Feature.Type.Sensor, + category=Feature.Category.Primary, + unit="A", + precision_hint=2, + ) + plug = _mocked_device(alias="my_plug", features=[new_feature]) + with _patch_discovery(device=plug), _patch_connect(device=plug): + await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) + await hass.async_block_till_done() + + msg = ( + "Device feature: Consumption for fortnight (consumption_this_fortnight) " + "needs an entity description defined in HA" + ) + assert msg in caplog.text + + +async def test_sensor_children_on_parent( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + device_registry: dr.DeviceRegistry, +) -> None: + """Test a WallSwitch sensor entities are added to parent.""" + already_migrated_config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS + ) + already_migrated_config_entry.add_to_hass(hass) + feature = _mocked_feature( + "consumption_this_month", + value=5.2, + # integration should ignore name and use the value from strings.json: + # This month's consumption + name="Consumption for month", + type_=Feature.Type.Sensor, + category=Feature.Category.Primary, + unit="A", + precision_hint=2, + ) + plug = _mocked_device( + alias="my_plug", + features=[feature], + children=_mocked_strip_children(features=[feature]), + device_type=Device.Type.WallSwitch, + ) + with _patch_discovery(device=plug), _patch_connect(device=plug): + await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) + await hass.async_block_till_done() + + entity_id = "sensor.my_plug_this_month_s_consumption" + entity = entity_registry.async_get(entity_id) + assert entity + device = device_registry.async_get(entity.device_id) + + for plug_id in range(2): + child_entity_id = f"sensor.my_plug_plug{plug_id}_this_month_s_consumption" + child_entity = entity_registry.async_get(child_entity_id) + assert child_entity + assert child_entity.unique_id == f"PLUG{plug_id}DEVICEID_consumption_this_month" + child_device = device_registry.async_get(child_entity.device_id) + assert child_device + + assert child_entity.device_id == entity.device_id + assert child_device.connections == device.connections + + +async def test_sensor_children_on_child( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + device_registry: dr.DeviceRegistry, +) -> None: + """Test strip sensors are on child device.""" + already_migrated_config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS + ) + already_migrated_config_entry.add_to_hass(hass) + feature = _mocked_feature( + "consumption_this_month", + value=5.2, + # integration should ignore name and use the value from strings.json: + # This month's consumption + name="Consumption for month", + type_=Feature.Type.Sensor, + category=Feature.Category.Primary, + unit="A", + precision_hint=2, + ) + plug = _mocked_device( + alias="my_plug", + features=[feature], + children=_mocked_strip_children(features=[feature]), + device_type=Device.Type.Strip, + ) + with _patch_discovery(device=plug), _patch_connect(device=plug): + await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) + await hass.async_block_till_done() + + entity_id = "sensor.my_plug_this_month_s_consumption" + entity = entity_registry.async_get(entity_id) + assert entity + device = device_registry.async_get(entity.device_id) + + for plug_id in range(2): + child_entity_id = f"sensor.my_plug_plug{plug_id}_this_month_s_consumption" + child_entity = entity_registry.async_get(child_entity_id) + assert child_entity + assert child_entity.unique_id == f"PLUG{plug_id}DEVICEID_consumption_this_month" + child_device = device_registry.async_get(child_entity.device_id) + assert child_device + + assert child_entity.device_id != entity.device_id + assert child_device.via_device_id == device.id + + +@pytest.mark.skip +async def test_new_datetime_sensor( + hass: HomeAssistant, entity_registry: er.EntityRegistry +) -> None: + """Test a sensor unique ids.""" + # Skipped temporarily while datetime handling on hold. + already_migrated_config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS + ) + already_migrated_config_entry.add_to_hass(hass) + plug = _mocked_device(alias="my_plug", features=["on_since"]) + with _patch_discovery(device=plug), _patch_connect(device=plug): + await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) + await hass.async_block_till_done() + + entity_id = "sensor.my_plug_on_since" + entity = entity_registry.async_get(entity_id) + assert entity + assert entity.unique_id == f"{DEVICE_ID}_on_since" + state = hass.states.get(entity_id) + assert state + assert state.attributes["device_class"] == "timestamp" diff --git a/tests/components/tplink/test_switch.py b/tests/components/tplink/test_switch.py index 02913e0c37e..e9c8cc07b67 100644 --- a/tests/components/tplink/test_switch.py +++ b/tests/components/tplink/test_switch.py @@ -3,12 +3,16 @@ from datetime import timedelta from unittest.mock import AsyncMock -from kasa import AuthenticationException, SmartDeviceException, TimeoutException +from kasa import AuthenticationError, Device, KasaException, Module, TimeoutError +from kasa.iot import IotStrip import pytest +from syrupy.assertion import SnapshotAssertion from homeassistant.components import tplink from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.components.tplink.const import DOMAIN +from homeassistant.components.tplink.entity import EXCLUDED_FEATURES +from homeassistant.components.tplink.switch import SWITCH_DESCRIPTIONS from homeassistant.config_entries import SOURCE_REAUTH from homeassistant.const import ( ATTR_ENTITY_ID, @@ -16,32 +20,57 @@ from homeassistant.const import ( STATE_OFF, STATE_ON, STATE_UNAVAILABLE, + Platform, ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import entity_registry as er +from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util, slugify from . import ( + DEVICE_ID, MAC_ADDRESS, - _mocked_dimmer, - _mocked_plug, - _mocked_strip, + _mocked_device, + _mocked_strip_children, _patch_connect, _patch_discovery, + setup_platform_for_device, + snapshot_platform, ) from tests.common import MockConfigEntry, async_fire_time_changed +async def test_states( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, + device_registry: dr.DeviceRegistry, + snapshot: SnapshotAssertion, +) -> None: + """Test a sensor unique ids.""" + features = {description.key for description in SWITCH_DESCRIPTIONS} + features.update(EXCLUDED_FEATURES) + device = _mocked_device(alias="my_device", features=features) + + await setup_platform_for_device(hass, mock_config_entry, Platform.SWITCH, device) + await snapshot_platform( + hass, entity_registry, device_registry, snapshot, mock_config_entry.entry_id + ) + + for excluded in EXCLUDED_FEATURES: + assert hass.states.get(f"sensor.my_device_{excluded}") is None + + async def test_plug(hass: HomeAssistant) -> None: """Test a smart plug.""" already_migrated_config_entry = MockConfigEntry( domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS ) already_migrated_config_entry.add_to_hass(hass) - plug = _mocked_plug() + plug = _mocked_device(alias="my_plug", features=["state"]) + feat = plug.features["state"] with _patch_discovery(device=plug), _patch_connect(device=plug): await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) await hass.async_block_till_done() @@ -53,29 +82,42 @@ async def test_plug(hass: HomeAssistant) -> None: await hass.services.async_call( SWITCH_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True ) - plug.turn_off.assert_called_once() - plug.turn_off.reset_mock() + feat.set_value.assert_called_once() + feat.set_value.reset_mock() await hass.services.async_call( SWITCH_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True ) - plug.turn_on.assert_called_once() - plug.turn_on.reset_mock() + feat.set_value.assert_called_once() + feat.set_value.reset_mock() @pytest.mark.parametrize( ("dev", "domain"), [ - (_mocked_plug(), "switch"), - (_mocked_strip(), "switch"), - (_mocked_dimmer(), "light"), + (_mocked_device(alias="my_plug", features=["state", "led"]), "switch"), + ( + _mocked_device( + alias="my_strip", + features=["state", "led"], + children=_mocked_strip_children(), + ), + "switch", + ), + ( + _mocked_device( + alias="my_light", modules=[Module.Light], features=["state", "led"] + ), + "light", + ), ], ) -async def test_led_switch(hass: HomeAssistant, dev, domain: str) -> None: +async def test_led_switch(hass: HomeAssistant, dev: Device, domain: str) -> None: """Test LED setting for plugs, strips and dimmers.""" already_migrated_config_entry = MockConfigEntry( domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS ) + feat = dev.features["led"] already_migrated_config_entry.add_to_hass(hass) with _patch_discovery(device=dev), _patch_connect(device=dev): await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) @@ -91,14 +133,14 @@ async def test_led_switch(hass: HomeAssistant, dev, domain: str) -> None: await hass.services.async_call( SWITCH_DOMAIN, "turn_off", {ATTR_ENTITY_ID: led_entity_id}, blocking=True ) - dev.set_led.assert_called_once_with(False) - dev.set_led.reset_mock() + feat.set_value.assert_called_once_with(False) + feat.set_value.reset_mock() await hass.services.async_call( SWITCH_DOMAIN, "turn_on", {ATTR_ENTITY_ID: led_entity_id}, blocking=True ) - dev.set_led.assert_called_once_with(True) - dev.set_led.reset_mock() + feat.set_value.assert_called_once_with(True) + feat.set_value.reset_mock() async def test_plug_unique_id( @@ -109,13 +151,13 @@ async def test_plug_unique_id( domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS ) already_migrated_config_entry.add_to_hass(hass) - plug = _mocked_plug() + plug = _mocked_device(alias="my_plug", features=["state", "led"]) with _patch_discovery(device=plug), _patch_connect(device=plug): await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) await hass.async_block_till_done() entity_id = "switch.my_plug" - assert entity_registry.async_get(entity_id).unique_id == "aa:bb:cc:dd:ee:ff" + assert entity_registry.async_get(entity_id).unique_id == DEVICE_ID async def test_plug_update_fails(hass: HomeAssistant) -> None: @@ -124,7 +166,7 @@ async def test_plug_update_fails(hass: HomeAssistant) -> None: domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS ) already_migrated_config_entry.add_to_hass(hass) - plug = _mocked_plug() + plug = _mocked_device(alias="my_plug", features=["state", "led"]) with _patch_discovery(device=plug), _patch_connect(device=plug): await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) await hass.async_block_till_done() @@ -132,7 +174,7 @@ async def test_plug_update_fails(hass: HomeAssistant) -> None: entity_id = "switch.my_plug" state = hass.states.get(entity_id) assert state.state == STATE_ON - plug.update = AsyncMock(side_effect=SmartDeviceException) + plug.update = AsyncMock(side_effect=KasaException) async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=30)) await hass.async_block_till_done() @@ -146,15 +188,18 @@ async def test_strip(hass: HomeAssistant) -> None: domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS ) already_migrated_config_entry.add_to_hass(hass) - strip = _mocked_strip() + strip = _mocked_device( + alias="my_strip", + children=_mocked_strip_children(features=["state"]), + features=["state", "led"], + spec=IotStrip, + ) + strip.children[0].features["state"].value = True + strip.children[1].features["state"].value = False with _patch_discovery(device=strip), _patch_connect(device=strip): await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) await hass.async_block_till_done() - # Verify we only create entities for the children - # since this is what the previous version did - assert hass.states.get("switch.my_strip") is None - entity_id = "switch.my_strip_plug0" state = hass.states.get(entity_id) assert state.state == STATE_ON @@ -162,14 +207,15 @@ async def test_strip(hass: HomeAssistant) -> None: await hass.services.async_call( SWITCH_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True ) - strip.children[0].turn_off.assert_called_once() - strip.children[0].turn_off.reset_mock() + feat = strip.children[0].features["state"] + feat.set_value.assert_called_once() + feat.set_value.reset_mock() await hass.services.async_call( SWITCH_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True ) - strip.children[0].turn_on.assert_called_once() - strip.children[0].turn_on.reset_mock() + feat.set_value.assert_called_once() + feat.set_value.reset_mock() entity_id = "switch.my_strip_plug1" state = hass.states.get(entity_id) @@ -178,14 +224,15 @@ async def test_strip(hass: HomeAssistant) -> None: await hass.services.async_call( SWITCH_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True ) - strip.children[1].turn_off.assert_called_once() - strip.children[1].turn_off.reset_mock() + feat = strip.children[1].features["state"] + feat.set_value.assert_called_once() + feat.set_value.reset_mock() await hass.services.async_call( SWITCH_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True ) - strip.children[1].turn_on.assert_called_once() - strip.children[1].turn_on.reset_mock() + feat.set_value.assert_called_once() + feat.set_value.reset_mock() async def test_strip_unique_ids( @@ -196,7 +243,11 @@ async def test_strip_unique_ids( domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS ) already_migrated_config_entry.add_to_hass(hass) - strip = _mocked_strip() + strip = _mocked_device( + alias="my_strip", + children=_mocked_strip_children(features=["state"]), + features=["state", "led"], + ) with _patch_discovery(device=strip), _patch_connect(device=strip): await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) await hass.async_block_till_done() @@ -208,21 +259,45 @@ async def test_strip_unique_ids( ) +async def test_strip_blank_alias( + hass: HomeAssistant, entity_registry: er.EntityRegistry +) -> None: + """Test a strip unique id.""" + already_migrated_config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS + ) + already_migrated_config_entry.add_to_hass(hass) + strip = _mocked_device( + alias="", + model="KS123", + children=_mocked_strip_children(features=["state", "led"], alias=""), + features=["state", "led"], + ) + with _patch_discovery(device=strip), _patch_connect(device=strip): + await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) + await hass.async_block_till_done() + + for plug_id in range(2): + entity_id = f"switch.unnamed_ks123_stripsocket_{plug_id + 1}" + state = hass.states.get(entity_id) + assert state.name == f"Unnamed KS123 Stripsocket {plug_id + 1}" + + @pytest.mark.parametrize( ("exception_type", "msg", "reauth_expected"), [ ( - AuthenticationException, + AuthenticationError, "Device authentication error async_turn_on: test error", True, ), ( - TimeoutException, + TimeoutError, "Timeout communicating with the device async_turn_on: test error", False, ), ( - SmartDeviceException, + KasaException, "Unable to communicate with the device async_turn_on: test error", False, ), @@ -240,8 +315,9 @@ async def test_plug_errors_when_turned_on( domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS ) already_migrated_config_entry.add_to_hass(hass) - plug = _mocked_plug() - plug.turn_on.side_effect = exception_type("test error") + plug = _mocked_device(alias="my_plug", features=["state", "led"]) + feat = plug.features["state"] + feat.set_value.side_effect = exception_type("test error") with _patch_discovery(device=plug), _patch_connect(device=plug): await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) @@ -258,7 +334,7 @@ async def test_plug_errors_when_turned_on( SWITCH_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True ) await hass.async_block_till_done() - assert plug.turn_on.call_count == 1 + assert feat.set_value.call_count == 1 assert ( any( flow From 1f0e47b25118537d41289791ca8a52800189e44e Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 25 Jun 2024 22:27:52 +0200 Subject: [PATCH 1193/1445] Migrate Airgradient select entities to be config source dependent (#120462) Co-authored-by: Robert Resch --- .../components/airgradient/select.py | 88 +++++++++++-------- .../components/airgradient/strings.json | 5 -- tests/components/airgradient/test_select.py | 60 +++++++------ 3 files changed, 84 insertions(+), 69 deletions(-) diff --git a/homeassistant/components/airgradient/select.py b/homeassistant/components/airgradient/select.py index e85e1224000..1cb902a2d3c 100644 --- a/homeassistant/components/airgradient/select.py +++ b/homeassistant/components/airgradient/select.py @@ -6,10 +6,14 @@ from dataclasses import dataclass from airgradient import AirGradientClient, Config from airgradient.models import ConfigurationControl, LedBarMode, TemperatureUnit -from homeassistant.components.select import SelectEntity, SelectEntityDescription +from homeassistant.components.select import ( + DOMAIN as SELECT_DOMAIN, + SelectEntity, + SelectEntityDescription, +) from homeassistant.const import EntityCategory -from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ServiceValidationError +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import AirGradientConfigEntry @@ -24,8 +28,6 @@ class AirGradientSelectEntityDescription(SelectEntityDescription): value_fn: Callable[[Config], str | None] set_value_fn: Callable[[AirGradientClient, str], Awaitable[None]] - requires_display: bool = False - requires_led_bar: bool = False CONFIG_CONTROL_ENTITY = AirGradientSelectEntityDescription( @@ -43,7 +45,7 @@ CONFIG_CONTROL_ENTITY = AirGradientSelectEntityDescription( ), ) -PROTECTED_SELECT_TYPES: tuple[AirGradientSelectEntityDescription, ...] = ( +DISPLAY_SELECT_TYPES: tuple[AirGradientSelectEntityDescription, ...] = ( AirGradientSelectEntityDescription( key="display_temperature_unit", translation_key="display_temperature_unit", @@ -53,7 +55,6 @@ PROTECTED_SELECT_TYPES: tuple[AirGradientSelectEntityDescription, ...] = ( set_value_fn=lambda client, value: client.set_temperature_unit( TemperatureUnit(value) ), - requires_display=True, ), AirGradientSelectEntityDescription( key="display_pm_standard", @@ -64,8 +65,10 @@ PROTECTED_SELECT_TYPES: tuple[AirGradientSelectEntityDescription, ...] = ( set_value_fn=lambda client, value: client.set_pm_standard( PM_STANDARD_REVERSE[value] ), - requires_display=True, ), +) + +LED_BAR_ENTITIES: tuple[AirGradientSelectEntityDescription, ...] = ( AirGradientSelectEntityDescription( key="led_bar_mode", translation_key="led_bar_mode", @@ -73,7 +76,6 @@ PROTECTED_SELECT_TYPES: tuple[AirGradientSelectEntityDescription, ...] = ( entity_category=EntityCategory.CONFIG, value_fn=lambda config: config.led_bar_mode, set_value_fn=lambda client, value: client.set_led_bar_mode(LedBarMode(value)), - requires_led_bar=True, ), ) @@ -85,22 +87,52 @@ async def async_setup_entry( ) -> None: """Set up AirGradient select entities based on a config entry.""" - config_coordinator = entry.runtime_data.config + coordinator = entry.runtime_data.config measurement_coordinator = entry.runtime_data.measurement - entities = [AirGradientSelect(config_coordinator, CONFIG_CONTROL_ENTITY)] + async_add_entities([AirGradientSelect(coordinator, CONFIG_CONTROL_ENTITY)]) + + model = measurement_coordinator.data.model + + added_entities = False + + @callback + def _async_check_entities() -> None: + nonlocal added_entities - entities.extend( - AirGradientProtectedSelect(config_coordinator, description) - for description in PROTECTED_SELECT_TYPES if ( - description.requires_display - and measurement_coordinator.data.model.startswith("I") - ) - or (description.requires_led_bar and "L" in measurement_coordinator.data.model) - ) + coordinator.data.configuration_control is ConfigurationControl.LOCAL + and not added_entities + ): + entities: list[AirGradientSelect] = [] + if "I" in model: + entities.extend( + AirGradientSelect(coordinator, description) + for description in DISPLAY_SELECT_TYPES + ) + if "L" in model: + entities.extend( + AirGradientSelect(coordinator, description) + for description in LED_BAR_ENTITIES + ) - async_add_entities(entities) + async_add_entities(entities) + added_entities = True + elif ( + coordinator.data.configuration_control is not ConfigurationControl.LOCAL + and added_entities + ): + entity_registry = er.async_get(hass) + for entity_description in DISPLAY_SELECT_TYPES + LED_BAR_ENTITIES: + unique_id = f"{coordinator.serial_number}-{entity_description.key}" + if entity_id := entity_registry.async_get_entity_id( + SELECT_DOMAIN, DOMAIN, unique_id + ): + entity_registry.async_remove(entity_id) + added_entities = False + + coordinator.async_add_listener(_async_check_entities) + _async_check_entities() class AirGradientSelect(AirGradientEntity, SelectEntity): @@ -128,19 +160,3 @@ class AirGradientSelect(AirGradientEntity, SelectEntity): """Change the selected option.""" await self.entity_description.set_value_fn(self.coordinator.client, option) await self.coordinator.async_request_refresh() - - -class AirGradientProtectedSelect(AirGradientSelect): - """Defines a protected AirGradient select entity.""" - - async def async_select_option(self, option: str) -> None: - """Change the selected option.""" - if ( - self.coordinator.data.configuration_control - is not ConfigurationControl.LOCAL - ): - raise ServiceValidationError( - translation_domain=DOMAIN, - translation_key="no_local_configuration", - ) - await super().async_select_option(option) diff --git a/homeassistant/components/airgradient/strings.json b/homeassistant/components/airgradient/strings.json index 0b5c245f04c..4e8973bdde2 100644 --- a/homeassistant/components/airgradient/strings.json +++ b/homeassistant/components/airgradient/strings.json @@ -125,10 +125,5 @@ "name": "[%key:component::airgradient::entity::number::display_brightness::name%]" } } - }, - "exceptions": { - "no_local_configuration": { - "message": "Device should be configured with local configuration to be able to change settings." - } } } diff --git a/tests/components/airgradient/test_select.py b/tests/components/airgradient/test_select.py index 84bf081af63..b4294112062 100644 --- a/tests/components/airgradient/test_select.py +++ b/tests/components/airgradient/test_select.py @@ -1,23 +1,30 @@ """Tests for the AirGradient select platform.""" +from datetime import timedelta from unittest.mock import AsyncMock, patch -from airgradient import ConfigurationControl +from airgradient import Config +from freezegun.api import FrozenDateTimeFactory import pytest from syrupy import SnapshotAssertion +from homeassistant.components.airgradient import DOMAIN from homeassistant.components.select import ( DOMAIN as SELECT_DOMAIN, SERVICE_SELECT_OPTION, ) from homeassistant.const import ATTR_ENTITY_ID, ATTR_OPTION, Platform from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ServiceValidationError from homeassistant.helpers import entity_registry as er from . import setup_integration -from tests.common import MockConfigEntry, snapshot_platform +from tests.common import ( + MockConfigEntry, + async_fire_time_changed, + load_fixture, + snapshot_platform, +) @pytest.mark.usefixtures("entity_registry_enabled_by_default") @@ -56,37 +63,34 @@ async def test_setting_value( assert mock_airgradient_client.get_config.call_count == 2 -async def test_setting_protected_value( +async def test_cloud_creates_no_number( hass: HomeAssistant, mock_cloud_airgradient_client: AsyncMock, mock_config_entry: MockConfigEntry, + freezer: FrozenDateTimeFactory, ) -> None: - """Test setting protected value.""" - await setup_integration(hass, mock_config_entry) + """Test cloud configuration control.""" + with patch("homeassistant.components.airgradient.PLATFORMS", [Platform.SELECT]): + await setup_integration(hass, mock_config_entry) - with pytest.raises(ServiceValidationError): - await hass.services.async_call( - SELECT_DOMAIN, - SERVICE_SELECT_OPTION, - { - ATTR_ENTITY_ID: "select.airgradient_display_temperature_unit", - ATTR_OPTION: "c", - }, - blocking=True, - ) - mock_cloud_airgradient_client.set_temperature_unit.assert_not_called() + assert len(hass.states.async_all()) == 1 - mock_cloud_airgradient_client.get_config.return_value.configuration_control = ( - ConfigurationControl.LOCAL + mock_cloud_airgradient_client.get_config.return_value = Config.from_json( + load_fixture("get_config_local.json", DOMAIN) ) - await hass.services.async_call( - SELECT_DOMAIN, - SERVICE_SELECT_OPTION, - { - ATTR_ENTITY_ID: "select.airgradient_display_temperature_unit", - ATTR_OPTION: "c", - }, - blocking=True, + freezer.tick(timedelta(minutes=5)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 4 + + mock_cloud_airgradient_client.get_config.return_value = Config.from_json( + load_fixture("get_config_cloud.json", DOMAIN) ) - mock_cloud_airgradient_client.set_temperature_unit.assert_called_once_with("c") + + freezer.tick(timedelta(minutes=5)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 1 From 5983344746b68babf98eecf09d48609754114cc5 Mon Sep 17 00:00:00 2001 From: Stefan Agner Date: Tue, 25 Jun 2024 22:34:56 +0200 Subject: [PATCH 1194/1445] Handle http connection errors to Prusa printers (#120456) --- homeassistant/components/prusalink/coordinator.py | 3 +++ tests/components/prusalink/test_init.py | 6 +++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/prusalink/coordinator.py b/homeassistant/components/prusalink/coordinator.py index 7d4526a8b45..1d1989119fa 100644 --- a/homeassistant/components/prusalink/coordinator.py +++ b/homeassistant/components/prusalink/coordinator.py @@ -9,6 +9,7 @@ import logging from time import monotonic from typing import TypeVar +from httpx import ConnectError from pyprusalink import JobInfo, LegacyPrinterStatus, PrinterStatus, PrusaLink from pyprusalink.types import InvalidAuth, PrusaLinkError @@ -47,6 +48,8 @@ class PrusaLinkUpdateCoordinator(DataUpdateCoordinator[T], ABC): raise UpdateFailed("Invalid authentication") from None except PrusaLinkError as err: raise UpdateFailed(str(err)) from err + except (TimeoutError, ConnectError) as err: + raise UpdateFailed("Cannot connect") from err self.update_interval = self._get_update_interval(data) return data diff --git a/tests/components/prusalink/test_init.py b/tests/components/prusalink/test_init.py index 2cdc6894eeb..bd0fb84cafd 100644 --- a/tests/components/prusalink/test_init.py +++ b/tests/components/prusalink/test_init.py @@ -3,6 +3,7 @@ from datetime import timedelta from unittest.mock import patch +from httpx import ConnectError from pyprusalink.types import InvalidAuth, PrusaLinkError import pytest @@ -36,7 +37,10 @@ async def test_unloading( assert state.state == "unavailable" -@pytest.mark.parametrize("exception", [InvalidAuth, PrusaLinkError]) +@pytest.mark.parametrize( + "exception", + [InvalidAuth, PrusaLinkError, ConnectError("All connection attempts failed")], +) async def test_failed_update( hass: HomeAssistant, mock_config_entry: ConfigEntry, exception ) -> None: From 038f2ce79f803049dd2648ab837fff04bfafdde1 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Wed, 26 Jun 2024 00:01:57 +0200 Subject: [PATCH 1195/1445] Cleanup mqtt platform tests part 1 (#120470) --- .../mqtt/test_alarm_control_panel.py | 10 +--- tests/components/mqtt/test_binary_sensor.py | 42 ++++---------- tests/components/mqtt/test_button.py | 16 +----- tests/components/mqtt/test_camera.py | 18 +----- tests/components/mqtt/test_climate.py | 56 ++++--------------- tests/components/mqtt/test_config_flow.py | 27 ++++----- 6 files changed, 41 insertions(+), 128 deletions(-) diff --git a/tests/components/mqtt/test_alarm_control_panel.py b/tests/components/mqtt/test_alarm_control_panel.py index cd7e8ab7339..aba2d5f6da2 100644 --- a/tests/components/mqtt/test_alarm_control_panel.py +++ b/tests/components/mqtt/test_alarm_control_panel.py @@ -1167,10 +1167,7 @@ async def test_entity_device_info_remove( ) -> None: """Test device registry remove.""" await help_test_entity_device_info_remove( - hass, - mqtt_mock_entry, - alarm_control_panel.DOMAIN, - DEFAULT_CONFIG, + hass, mqtt_mock_entry, alarm_control_panel.DOMAIN, DEFAULT_CONFIG ) @@ -1188,10 +1185,7 @@ async def test_entity_id_update_discovery_update( ) -> None: """Test MQTT discovery update when entity_id is updated.""" await help_test_entity_id_update_discovery_update( - hass, - mqtt_mock_entry, - alarm_control_panel.DOMAIN, - DEFAULT_CONFIG, + hass, mqtt_mock_entry, alarm_control_panel.DOMAIN, DEFAULT_CONFIG ) diff --git a/tests/components/mqtt/test_binary_sensor.py b/tests/components/mqtt/test_binary_sensor.py index 7ad394243df..6ba479fca74 100644 --- a/tests/components/mqtt/test_binary_sensor.py +++ b/tests/components/mqtt/test_binary_sensor.py @@ -977,10 +977,7 @@ async def test_entity_device_info_with_connection( ) -> None: """Test MQTT binary sensor device registry integration.""" await help_test_entity_device_info_with_connection( - hass, - mqtt_mock_entry, - binary_sensor.DOMAIN, - DEFAULT_CONFIG, + hass, mqtt_mock_entry, binary_sensor.DOMAIN, DEFAULT_CONFIG ) @@ -989,10 +986,7 @@ async def test_entity_device_info_with_identifier( ) -> None: """Test MQTT binary sensor device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, - mqtt_mock_entry, - binary_sensor.DOMAIN, - DEFAULT_CONFIG, + hass, mqtt_mock_entry, binary_sensor.DOMAIN, DEFAULT_CONFIG ) @@ -1001,10 +995,7 @@ async def test_entity_device_info_update( ) -> None: """Test device registry update.""" await help_test_entity_device_info_update( - hass, - mqtt_mock_entry, - binary_sensor.DOMAIN, - DEFAULT_CONFIG, + hass, mqtt_mock_entry, binary_sensor.DOMAIN, DEFAULT_CONFIG ) @@ -1013,10 +1004,7 @@ async def test_entity_device_info_remove( ) -> None: """Test device registry remove.""" await help_test_entity_device_info_remove( - hass, - mqtt_mock_entry, - binary_sensor.DOMAIN, - DEFAULT_CONFIG, + hass, mqtt_mock_entry, binary_sensor.DOMAIN, DEFAULT_CONFIG ) @@ -1034,10 +1022,7 @@ async def test_entity_id_update_discovery_update( ) -> None: """Test MQTT discovery update when entity_id is updated.""" await help_test_entity_id_update_discovery_update( - hass, - mqtt_mock_entry, - binary_sensor.DOMAIN, - DEFAULT_CONFIG, + hass, mqtt_mock_entry, binary_sensor.DOMAIN, DEFAULT_CONFIG ) @@ -1046,11 +1031,7 @@ async def test_entity_debug_info_message( ) -> None: """Test MQTT debug info.""" await help_test_entity_debug_info_message( - hass, - mqtt_mock_entry, - binary_sensor.DOMAIN, - DEFAULT_CONFIG, - None, + hass, mqtt_mock_entry, binary_sensor.DOMAIN, DEFAULT_CONFIG, None ) @@ -1104,10 +1085,10 @@ async def test_cleanup_triggers_and_restoring_state( tmp_path: Path, freezer: FrozenDateTimeFactory, hass_config: ConfigType, - payload1, - state1, - payload2, - state2, + payload1: str, + state1: str, + payload2: str, + state2: str, ) -> None: """Test cleanup old triggers at reloading and restoring the state.""" freezer.move_to("2022-02-02 12:01:00+01:00") @@ -1196,8 +1177,7 @@ async def test_setup_manual_entity_from_yaml( async def test_unload_entry( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test unloading the config entry.""" domain = binary_sensor.DOMAIN diff --git a/tests/components/mqtt/test_button.py b/tests/components/mqtt/test_button.py index 2d21128237e..7e5d748e2ab 100644 --- a/tests/components/mqtt/test_button.py +++ b/tests/components/mqtt/test_button.py @@ -159,13 +159,7 @@ async def test_default_availability_payload( } } await help_test_default_availability_payload( - hass, - mqtt_mock_entry, - button.DOMAIN, - config, - True, - "state-topic", - "1", + hass, mqtt_mock_entry, button.DOMAIN, config, True, "state-topic", "1" ) @@ -184,13 +178,7 @@ async def test_custom_availability_payload( } await help_test_custom_availability_payload( - hass, - mqtt_mock_entry, - button.DOMAIN, - config, - True, - "state-topic", - "1", + hass, mqtt_mock_entry, button.DOMAIN, config, True, "state-topic", "1" ) diff --git a/tests/components/mqtt/test_camera.py b/tests/components/mqtt/test_camera.py index 9dbf5035fc9..d02e19e6063 100644 --- a/tests/components/mqtt/test_camera.py +++ b/tests/components/mqtt/test_camera.py @@ -222,11 +222,7 @@ async def test_update_with_json_attrs_not_dict( ) -> None: """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_not_dict( - hass, - mqtt_mock_entry, - caplog, - camera.DOMAIN, - DEFAULT_CONFIG, + hass, mqtt_mock_entry, caplog, camera.DOMAIN, DEFAULT_CONFIG ) @@ -237,11 +233,7 @@ async def test_update_with_json_attrs_bad_json( ) -> None: """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_bad_json( - hass, - mqtt_mock_entry, - caplog, - camera.DOMAIN, - DEFAULT_CONFIG, + hass, mqtt_mock_entry, caplog, camera.DOMAIN, DEFAULT_CONFIG ) @@ -368,11 +360,7 @@ async def test_entity_id_update_subscriptions( ) -> None: """Test MQTT subscriptions are managed when entity_id is updated.""" await help_test_entity_id_update_subscriptions( - hass, - mqtt_mock_entry, - camera.DOMAIN, - DEFAULT_CONFIG, - ["test_topic"], + hass, mqtt_mock_entry, camera.DOMAIN, DEFAULT_CONFIG, ["test_topic"] ) diff --git a/tests/components/mqtt/test_climate.py b/tests/components/mqtt/test_climate.py index 5428dc9b3e1..c41a6366dfe 100644 --- a/tests/components/mqtt/test_climate.py +++ b/tests/components/mqtt/test_climate.py @@ -191,9 +191,7 @@ async def test_get_hvac_modes( @pytest.mark.parametrize("hass_config", [DEFAULT_CONFIG]) async def test_set_operation_bad_attr_and_state( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test setting operation mode without required attribute. @@ -454,9 +452,7 @@ async def test_turn_on_and_off_without_power_command( @pytest.mark.parametrize("hass_config", [DEFAULT_CONFIG]) async def test_set_fan_mode_bad_attr( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test setting fan mode without required attribute.""" await mqtt_mock_entry() @@ -551,9 +547,7 @@ async def test_set_fan_mode( @pytest.mark.parametrize("hass_config", [DEFAULT_CONFIG]) async def test_set_swing_mode_bad_attr( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test setting swing mode without required attribute.""" await mqtt_mock_entry() @@ -1046,9 +1040,7 @@ async def test_handle_action_received( @pytest.mark.parametrize("hass_config", [DEFAULT_CONFIG]) async def test_set_preset_mode_optimistic( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test setting of the preset mode.""" mqtt_mock = await mqtt_mock_entry() @@ -1104,9 +1096,7 @@ async def test_set_preset_mode_optimistic( ], ) async def test_set_preset_mode_explicit_optimistic( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test setting of the preset mode.""" mqtt_mock = await mqtt_mock_entry() @@ -1523,9 +1513,7 @@ async def test_get_with_templates( ], ) async def test_set_and_templates( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test setting various attributes with templates.""" mqtt_mock = await mqtt_mock_entry() @@ -2074,11 +2062,7 @@ async def test_entity_id_update_subscriptions( } } await help_test_entity_id_update_subscriptions( - hass, - mqtt_mock_entry, - climate.DOMAIN, - config, - ["test-topic", "avty-topic"], + hass, mqtt_mock_entry, climate.DOMAIN, config, ["test-topic", "avty-topic"] ) @@ -2170,20 +2154,8 @@ async def test_precision_whole( @pytest.mark.parametrize( ("service", "topic", "parameters", "payload", "template"), [ - ( - climate.SERVICE_TURN_ON, - "power_command_topic", - {}, - "ON", - None, - ), - ( - climate.SERVICE_TURN_OFF, - "power_command_topic", - {}, - "OFF", - None, - ), + (climate.SERVICE_TURN_ON, "power_command_topic", {}, "ON", None), + (climate.SERVICE_TURN_OFF, "power_command_topic", {}, "OFF", None), ( climate.SERVICE_SET_HVAC_MODE, "mode_command_topic", @@ -2346,9 +2318,7 @@ async def test_publishing_with_custom_encoding( ], ) async def test_humidity_configuration_validity( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - valid: bool, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, valid: bool ) -> None: """Test the validity of humidity configurations.""" assert await mqtt_mock_entry() @@ -2357,8 +2327,7 @@ async def test_humidity_configuration_validity( async def test_reloadable( - hass: HomeAssistant, - mqtt_client_mock: MqttMockPahoClient, + hass: HomeAssistant, mqtt_client_mock: MqttMockPahoClient ) -> None: """Test reloading the MQTT platform.""" domain = climate.DOMAIN @@ -2381,8 +2350,7 @@ async def test_setup_manual_entity_from_yaml( async def test_unload_entry( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test unloading the config entry.""" domain = climate.DOMAIN diff --git a/tests/components/mqtt/test_config_flow.py b/tests/components/mqtt/test_config_flow.py index 21ddf5ecc11..57975fdc309 100644 --- a/tests/components/mqtt/test_config_flow.py +++ b/tests/components/mqtt/test_config_flow.py @@ -187,11 +187,11 @@ def mock_process_uploaded_file( yield mock_upload +@pytest.mark.usefixtures("mqtt_client_mock") async def test_user_connection_works( hass: HomeAssistant, mock_try_connection: MagicMock, mock_finish_setup: MagicMock, - mqtt_client_mock: MqttMockPahoClient, ) -> None: """Test we can finish a config flow.""" mock_try_connection.return_value = True @@ -217,11 +217,11 @@ async def test_user_connection_works( assert len(mock_finish_setup.mock_calls) == 1 +@pytest.mark.usefixtures("mqtt_client_mock") async def test_user_v5_connection_works( hass: HomeAssistant, mock_try_connection: MagicMock, mock_finish_setup: MagicMock, - mqtt_client_mock: MqttMockPahoClient, ) -> None: """Test we can finish a config flow.""" mock_try_connection.return_value = True @@ -664,11 +664,11 @@ async def test_bad_certificate( ("100", False), ], ) +@pytest.mark.usefixtures("mock_reload_after_entry_update") async def test_keepalive_validation( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, mock_try_connection: MagicMock, - mock_reload_after_entry_update: MagicMock, input_value: str, error: bool, ) -> None: @@ -872,11 +872,11 @@ def get_suggested(schema: vol.Schema, key: str) -> Any: return schema_key.description["suggested_value"] +@pytest.mark.usefixtures("mock_reload_after_entry_update") async def test_option_flow_default_suggested_values( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, mock_try_connection_success: MqttMockPahoClient, - mock_reload_after_entry_update: MagicMock, ) -> None: """Test config flow options has default/suggested values.""" await mqtt_mock_entry() @@ -1030,11 +1030,11 @@ async def test_option_flow_default_suggested_values( @pytest.mark.parametrize( ("advanced_options", "step_id"), [(False, "options"), (True, "broker")] ) +@pytest.mark.usefixtures("mock_reload_after_entry_update") async def test_skipping_advanced_options( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, mock_try_connection: MagicMock, - mock_reload_after_entry_update: MagicMock, advanced_options: bool, step_id: str, ) -> None: @@ -1102,11 +1102,11 @@ async def test_skipping_advanced_options( ), ], ) +@pytest.mark.usefixtures("mock_reload_after_entry_update") async def test_step_reauth( hass: HomeAssistant, mqtt_client_mock: MqttMockPahoClient, mock_try_connection: MagicMock, - mock_reload_after_entry_update: MagicMock, test_input: dict[str, Any], user_input: dict[str, Any], new_password: str, @@ -1284,12 +1284,9 @@ async def test_options_bad_will_message_fails( @pytest.mark.parametrize( "hass_config", [{"mqtt": {"sensor": [{"state_topic": "some-topic"}]}}] ) +@pytest.mark.usefixtures("mock_ssl_context", "mock_process_uploaded_file") async def test_try_connection_with_advanced_parameters( - hass: HomeAssistant, - mock_try_connection_success: MqttMockPahoClient, - tmp_path: Path, - mock_ssl_context: dict[str, MagicMock], - mock_process_uploaded_file: MagicMock, + hass: HomeAssistant, mock_try_connection_success: MqttMockPahoClient ) -> None: """Test config flow with advanced parameters from config.""" config_entry = MockConfigEntry(domain=mqtt.DOMAIN) @@ -1402,10 +1399,10 @@ async def test_try_connection_with_advanced_parameters( await hass.async_block_till_done() +@pytest.mark.usefixtures("mock_ssl_context") async def test_setup_with_advanced_settings( hass: HomeAssistant, mock_try_connection: MagicMock, - mock_ssl_context: dict[str, MagicMock], mock_process_uploaded_file: MagicMock, ) -> None: """Test config flow setup with advanced parameters.""" @@ -1564,11 +1561,9 @@ async def test_setup_with_advanced_settings( } +@pytest.mark.usesfixtures("mock_ssl_context", "mock_process_uploaded_file") async def test_change_websockets_transport_to_tcp( - hass: HomeAssistant, - mock_try_connection, - mock_ssl_context: dict[str, MagicMock], - mock_process_uploaded_file: MagicMock, + hass: HomeAssistant, mock_try_connection: MagicMock ) -> None: """Test option flow setup with websockets transport settings.""" config_entry = MockConfigEntry(domain=mqtt.DOMAIN) From 60519702b4c2bd374ccb43732f4d2865baf2ae71 Mon Sep 17 00:00:00 2001 From: Luke Lashley Date: Tue, 25 Jun 2024 18:03:01 -0400 Subject: [PATCH 1196/1445] Bump python-roborock to 2.5.0 (#120466) --- homeassistant/components/roborock/manifest.json | 2 +- homeassistant/components/roborock/number.py | 2 +- homeassistant/components/roborock/switch.py | 2 +- homeassistant/components/roborock/time.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/roborock/manifest.json b/homeassistant/components/roborock/manifest.json index 51b1835247f..7a80a9083e9 100644 --- a/homeassistant/components/roborock/manifest.json +++ b/homeassistant/components/roborock/manifest.json @@ -7,7 +7,7 @@ "iot_class": "local_polling", "loggers": ["roborock"], "requirements": [ - "python-roborock==2.3.0", + "python-roborock==2.5.0", "vacuum-map-parser-roborock==0.1.2" ] } diff --git a/homeassistant/components/roborock/number.py b/homeassistant/components/roborock/number.py index f761d0b2274..8aa20fad838 100644 --- a/homeassistant/components/roborock/number.py +++ b/homeassistant/components/roborock/number.py @@ -31,7 +31,7 @@ class RoborockNumberDescription(NumberEntityDescription): # Gets the status of the switch cache_key: CacheableAttribute # Sets the status of the switch - update_value: Callable[[AttributeCache, float], Coroutine[Any, Any, dict]] + update_value: Callable[[AttributeCache, float], Coroutine[Any, Any, None]] NUMBER_DESCRIPTIONS: list[RoborockNumberDescription] = [ diff --git a/homeassistant/components/roborock/switch.py b/homeassistant/components/roborock/switch.py index 694bf864809..7e17844666e 100644 --- a/homeassistant/components/roborock/switch.py +++ b/homeassistant/components/roborock/switch.py @@ -32,7 +32,7 @@ class RoborockSwitchDescription(SwitchEntityDescription): # Gets the status of the switch cache_key: CacheableAttribute # Sets the status of the switch - update_value: Callable[[AttributeCache, bool], Coroutine[Any, Any, dict]] + update_value: Callable[[AttributeCache, bool], Coroutine[Any, Any, None]] # Attribute from cache attribute: str diff --git a/homeassistant/components/roborock/time.py b/homeassistant/components/roborock/time.py index 7c9c08bce4d..6ccc2ef0b27 100644 --- a/homeassistant/components/roborock/time.py +++ b/homeassistant/components/roborock/time.py @@ -33,7 +33,7 @@ class RoborockTimeDescription(TimeEntityDescription): # Gets the status of the switch cache_key: CacheableAttribute # Sets the status of the switch - update_value: Callable[[AttributeCache, datetime.time], Coroutine[Any, Any, dict]] + update_value: Callable[[AttributeCache, datetime.time], Coroutine[Any, Any, None]] # Attribute from cache get_value: Callable[[AttributeCache], datetime.time] diff --git a/requirements_all.txt b/requirements_all.txt index de167c2f7e1..61924dc2af9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2315,7 +2315,7 @@ python-rabbitair==0.0.8 python-ripple-api==0.0.3 # homeassistant.components.roborock -python-roborock==2.3.0 +python-roborock==2.5.0 # homeassistant.components.smarttub python-smarttub==0.0.36 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8eb468b0947..62d26ee172d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1809,7 +1809,7 @@ python-picnic-api==1.1.0 python-rabbitair==0.0.8 # homeassistant.components.roborock -python-roborock==2.3.0 +python-roborock==2.5.0 # homeassistant.components.smarttub python-smarttub==0.0.36 From 3cc3eb21b4baf3bfa1e5984c612cea46f30911f3 Mon Sep 17 00:00:00 2001 From: Tom Harris Date: Tue, 25 Jun 2024 18:04:09 -0400 Subject: [PATCH 1197/1445] Bump pyinsteon to 1.6.3 to fix Insteon device status (#120464) --- homeassistant/components/insteon/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/insteon/manifest.json b/homeassistant/components/insteon/manifest.json index 456bc124b66..c5791573195 100644 --- a/homeassistant/components/insteon/manifest.json +++ b/homeassistant/components/insteon/manifest.json @@ -17,7 +17,7 @@ "iot_class": "local_push", "loggers": ["pyinsteon", "pypubsub"], "requirements": [ - "pyinsteon==1.6.1", + "pyinsteon==1.6.3", "insteon-frontend-home-assistant==0.5.0" ], "usb": [ diff --git a/requirements_all.txt b/requirements_all.txt index 61924dc2af9..f42794d8086 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1905,7 +1905,7 @@ pyialarm==2.2.0 pyicloud==1.0.0 # homeassistant.components.insteon -pyinsteon==1.6.1 +pyinsteon==1.6.3 # homeassistant.components.intesishome pyintesishome==1.8.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 62d26ee172d..e70fd97c0c8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1498,7 +1498,7 @@ pyialarm==2.2.0 pyicloud==1.0.0 # homeassistant.components.insteon -pyinsteon==1.6.1 +pyinsteon==1.6.3 # homeassistant.components.ipma pyipma==3.0.7 From 9718a9571edd7797c4fecadb02a514a7c74e6d1b Mon Sep 17 00:00:00 2001 From: Thomas Kistler Date: Tue, 25 Jun 2024 17:11:52 -0700 Subject: [PATCH 1198/1445] Add @thomaskistler as an owner of hydrawise (#120477) --- CODEOWNERS | 4 ++-- homeassistant/components/hydrawise/manifest.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 2e954ed1315..b4ff315872d 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -631,8 +631,8 @@ build.json @home-assistant/supervisor /tests/components/huum/ @frwickst /homeassistant/components/hvv_departures/ @vigonotion /tests/components/hvv_departures/ @vigonotion -/homeassistant/components/hydrawise/ @dknowles2 @ptcryan -/tests/components/hydrawise/ @dknowles2 @ptcryan +/homeassistant/components/hydrawise/ @dknowles2 @thomaskistler @ptcryan +/tests/components/hydrawise/ @dknowles2 @thomaskistler @ptcryan /homeassistant/components/hyperion/ @dermotduffy /tests/components/hyperion/ @dermotduffy /homeassistant/components/ialarm/ @RyuzakiKK diff --git a/homeassistant/components/hydrawise/manifest.json b/homeassistant/components/hydrawise/manifest.json index b85ddca042e..c6f4d7d8dcd 100644 --- a/homeassistant/components/hydrawise/manifest.json +++ b/homeassistant/components/hydrawise/manifest.json @@ -1,7 +1,7 @@ { "domain": "hydrawise", "name": "Hunter Hydrawise", - "codeowners": ["@dknowles2", "@ptcryan"], + "codeowners": ["@dknowles2", "@thomaskistler", "@ptcryan"], "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/hydrawise", "iot_class": "cloud_polling", From ec2f98d0754e6fc86e7ba85571550938315d2e68 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 26 Jun 2024 02:12:04 +0200 Subject: [PATCH 1199/1445] Bump uiprotect to 3.7.0 (#120471) --- homeassistant/components/unifiprotect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index 7a1556387a8..8e29f5ffb9f 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -40,7 +40,7 @@ "integration_type": "hub", "iot_class": "local_push", "loggers": ["uiprotect", "unifi_discovery"], - "requirements": ["uiprotect==3.4.0", "unifi-discovery==1.1.8"], + "requirements": ["uiprotect==3.7.0", "unifi-discovery==1.1.8"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/requirements_all.txt b/requirements_all.txt index f42794d8086..cc8f8898430 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2791,7 +2791,7 @@ twitchAPI==4.0.0 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==3.4.0 +uiprotect==3.7.0 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e70fd97c0c8..f5d36af76f4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2171,7 +2171,7 @@ twitchAPI==4.0.0 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==3.4.0 +uiprotect==3.7.0 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 From 0bc597f8c74565896ad4fc9f8c8e45003e16fcf0 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Wed, 26 Jun 2024 02:13:09 +0200 Subject: [PATCH 1200/1445] Improve vol.Invalid handling (#120480) --- homeassistant/components/blueprint/errors.py | 2 +- homeassistant/components/logbook/rest_api.py | 4 ++-- homeassistant/components/mqtt/mixins.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/blueprint/errors.py b/homeassistant/components/blueprint/errors.py index 221279a39ac..e9a9defe05a 100644 --- a/homeassistant/components/blueprint/errors.py +++ b/homeassistant/components/blueprint/errors.py @@ -47,7 +47,7 @@ class InvalidBlueprint(BlueprintWithNameException): domain: str | None, blueprint_name: str | None, blueprint_data: Any, - msg_or_exc: vol.Invalid, + msg_or_exc: str | vol.Invalid, ) -> None: """Initialize an invalid blueprint error.""" if isinstance(msg_or_exc, vol.Invalid): diff --git a/homeassistant/components/logbook/rest_api.py b/homeassistant/components/logbook/rest_api.py index 5f1918ebccf..bd9efe7aba3 100644 --- a/homeassistant/components/logbook/rest_api.py +++ b/homeassistant/components/logbook/rest_api.py @@ -70,11 +70,11 @@ class LogbookView(HomeAssistantView): if entity_ids_str := request.query.get("entity"): try: entity_ids = cv.entity_ids(entity_ids_str) - except vol.Invalid: + except vol.Invalid as ex: raise InvalidEntityFormatError( f"Invalid entity id(s) encountered: {entity_ids_str}. " "Format should be ." - ) from vol.Invalid + ) from ex else: entity_ids = None diff --git a/homeassistant/components/mqtt/mixins.py b/homeassistant/components/mqtt/mixins.py index 0800aeb8ee4..aca88f2cb97 100644 --- a/homeassistant/components/mqtt/mixins.py +++ b/homeassistant/components/mqtt/mixins.py @@ -157,7 +157,7 @@ class SetupEntity(Protocol): @callback def async_handle_schema_error( - discovery_payload: MQTTDiscoveryPayload, err: vol.MultipleInvalid + discovery_payload: MQTTDiscoveryPayload, err: vol.Invalid ) -> None: """Help handling schema errors on MQTT discovery messages.""" discovery_topic: str = discovery_payload.discovery_data[ATTR_DISCOVERY_TOPIC] From 3937cc2963f7fc3338a06e0e0694cf807385732d Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Wed, 26 Jun 2024 02:20:48 +0200 Subject: [PATCH 1201/1445] Improve SERVICE_TO_METHOD typing (#120474) --- .coveragerc | 1 + .../components/bluesound/media_player.py | 27 +++++++++---- homeassistant/components/webostv/__init__.py | 28 ++++++++----- homeassistant/components/xiaomi_miio/fan.py | 15 +++---- homeassistant/components/xiaomi_miio/light.py | 39 +++++++++++-------- .../components/xiaomi_miio/switch.py | 27 ++++++------- .../components/xiaomi_miio/typing.py | 12 ++++++ 7 files changed, 95 insertions(+), 54 deletions(-) create mode 100644 homeassistant/components/xiaomi_miio/typing.py diff --git a/.coveragerc b/.coveragerc index da3b7b91ece..1952297eb5f 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1651,6 +1651,7 @@ omit = homeassistant/components/xiaomi_miio/remote.py homeassistant/components/xiaomi_miio/sensor.py homeassistant/components/xiaomi_miio/switch.py + homeassistant/components/xiaomi_miio/typing.py homeassistant/components/xiaomi_tv/media_player.py homeassistant/components/xmpp/notify.py homeassistant/components/xs1/* diff --git a/homeassistant/components/bluesound/media_player.py b/homeassistant/components/bluesound/media_player.py index 7be5a823bf8..73ce963d481 100644 --- a/homeassistant/components/bluesound/media_player.py +++ b/homeassistant/components/bluesound/media_player.py @@ -7,7 +7,7 @@ from asyncio import CancelledError, timeout from datetime import timedelta from http import HTTPStatus import logging -from typing import Any +from typing import Any, NamedTuple from urllib import parse import aiohttp @@ -85,15 +85,27 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( } ) + +class ServiceMethodDetails(NamedTuple): + """Details for SERVICE_TO_METHOD mapping.""" + + method: str + schema: vol.Schema + + BS_SCHEMA = vol.Schema({vol.Optional(ATTR_ENTITY_ID): cv.entity_ids}) BS_JOIN_SCHEMA = BS_SCHEMA.extend({vol.Required(ATTR_MASTER): cv.entity_id}) SERVICE_TO_METHOD = { - SERVICE_JOIN: {"method": "async_join", "schema": BS_JOIN_SCHEMA}, - SERVICE_UNJOIN: {"method": "async_unjoin", "schema": BS_SCHEMA}, - SERVICE_SET_TIMER: {"method": "async_increase_timer", "schema": BS_SCHEMA}, - SERVICE_CLEAR_TIMER: {"method": "async_clear_timer", "schema": BS_SCHEMA}, + SERVICE_JOIN: ServiceMethodDetails(method="async_join", schema=BS_JOIN_SCHEMA), + SERVICE_UNJOIN: ServiceMethodDetails(method="async_unjoin", schema=BS_SCHEMA), + SERVICE_SET_TIMER: ServiceMethodDetails( + method="async_increase_timer", schema=BS_SCHEMA + ), + SERVICE_CLEAR_TIMER: ServiceMethodDetails( + method="async_clear_timer", schema=BS_SCHEMA + ), } @@ -188,12 +200,11 @@ async def async_setup_platform( target_players = hass.data[DATA_BLUESOUND] for player in target_players: - await getattr(player, method["method"])(**params) + await getattr(player, method.method)(**params) for service, method in SERVICE_TO_METHOD.items(): - schema = method["schema"] hass.services.async_register( - DOMAIN, service, async_service_handler, schema=schema + DOMAIN, service, async_service_handler, schema=method.schema ) diff --git a/homeassistant/components/webostv/__init__.py b/homeassistant/components/webostv/__init__.py index 479407c3199..36950b0e02a 100644 --- a/homeassistant/components/webostv/__init__.py +++ b/homeassistant/components/webostv/__init__.py @@ -4,6 +4,7 @@ from __future__ import annotations from contextlib import suppress import logging +from typing import NamedTuple from aiowebostv import WebOsClient, WebOsTvPairError import voluptuous as vol @@ -43,6 +44,14 @@ CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) CALL_SCHEMA = vol.Schema({vol.Required(ATTR_ENTITY_ID): cv.comp_entity_ids}) + +class ServiceMethodDetails(NamedTuple): + """Details for SERVICE_TO_METHOD mapping.""" + + method: str + schema: vol.Schema + + BUTTON_SCHEMA = CALL_SCHEMA.extend({vol.Required(ATTR_BUTTON): cv.string}) COMMAND_SCHEMA = CALL_SCHEMA.extend( @@ -52,12 +61,14 @@ COMMAND_SCHEMA = CALL_SCHEMA.extend( SOUND_OUTPUT_SCHEMA = CALL_SCHEMA.extend({vol.Required(ATTR_SOUND_OUTPUT): cv.string}) SERVICE_TO_METHOD = { - SERVICE_BUTTON: {"method": "async_button", "schema": BUTTON_SCHEMA}, - SERVICE_COMMAND: {"method": "async_command", "schema": COMMAND_SCHEMA}, - SERVICE_SELECT_SOUND_OUTPUT: { - "method": "async_select_sound_output", - "schema": SOUND_OUTPUT_SCHEMA, - }, + SERVICE_BUTTON: ServiceMethodDetails(method="async_button", schema=BUTTON_SCHEMA), + SERVICE_COMMAND: ServiceMethodDetails( + method="async_command", schema=COMMAND_SCHEMA + ), + SERVICE_SELECT_SOUND_OUTPUT: ServiceMethodDetails( + method="async_select_sound_output", + schema=SOUND_OUTPUT_SCHEMA, + ), } _LOGGER = logging.getLogger(__name__) @@ -92,13 +103,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_service_handler(service: ServiceCall) -> None: method = SERVICE_TO_METHOD[service.service] data = service.data.copy() - data["method"] = method["method"] + data["method"] = method.method async_dispatcher_send(hass, DOMAIN, data) for service, method in SERVICE_TO_METHOD.items(): - schema = method["schema"] hass.services.async_register( - DOMAIN, service, async_service_handler, schema=schema + DOMAIN, service, async_service_handler, schema=method.schema ) hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id] = client diff --git a/homeassistant/components/xiaomi_miio/fan.py b/homeassistant/components/xiaomi_miio/fan.py index 75533513b5e..4e0e271b071 100644 --- a/homeassistant/components/xiaomi_miio/fan.py +++ b/homeassistant/components/xiaomi_miio/fan.py @@ -92,6 +92,7 @@ from .const import ( SERVICE_SET_EXTRA_FEATURES, ) from .device import XiaomiCoordinatedMiioEntity +from .typing import ServiceMethodDetails _LOGGER = logging.getLogger(__name__) @@ -182,11 +183,11 @@ SERVICE_SCHEMA_EXTRA_FEATURES = AIRPURIFIER_SERVICE_SCHEMA.extend( ) SERVICE_TO_METHOD = { - SERVICE_RESET_FILTER: {"method": "async_reset_filter"}, - SERVICE_SET_EXTRA_FEATURES: { - "method": "async_set_extra_features", - "schema": SERVICE_SCHEMA_EXTRA_FEATURES, - }, + SERVICE_RESET_FILTER: ServiceMethodDetails(method="async_reset_filter"), + SERVICE_SET_EXTRA_FEATURES: ServiceMethodDetails( + method="async_set_extra_features", + schema=SERVICE_SCHEMA_EXTRA_FEATURES, + ), } FAN_DIRECTIONS_MAP = { @@ -271,7 +272,7 @@ async def async_setup_entry( update_tasks = [] for entity in filtered_entities: - entity_method = getattr(entity, method["method"], None) + entity_method = getattr(entity, method.method, None) if not entity_method: continue await entity_method(**params) @@ -281,7 +282,7 @@ async def async_setup_entry( await asyncio.wait(update_tasks) for air_purifier_service, method in SERVICE_TO_METHOD.items(): - schema = method.get("schema", AIRPURIFIER_SERVICE_SCHEMA) + schema = method.schema or AIRPURIFIER_SERVICE_SCHEMA hass.services.async_register( DOMAIN, air_purifier_service, async_service_handler, schema=schema ) diff --git a/homeassistant/components/xiaomi_miio/light.py b/homeassistant/components/xiaomi_miio/light.py index 96f9595e0e8..35537e82b2e 100644 --- a/homeassistant/components/xiaomi_miio/light.py +++ b/homeassistant/components/xiaomi_miio/light.py @@ -68,6 +68,7 @@ from .const import ( ) from .device import XiaomiMiioEntity from .gateway import XiaomiGatewayDevice +from .typing import ServiceMethodDetails _LOGGER = logging.getLogger(__name__) @@ -108,20 +109,24 @@ SERVICE_SCHEMA_SET_DELAYED_TURN_OFF = XIAOMI_MIIO_SERVICE_SCHEMA.extend( ) SERVICE_TO_METHOD = { - SERVICE_SET_DELAYED_TURN_OFF: { - "method": "async_set_delayed_turn_off", - "schema": SERVICE_SCHEMA_SET_DELAYED_TURN_OFF, - }, - SERVICE_SET_SCENE: { - "method": "async_set_scene", - "schema": SERVICE_SCHEMA_SET_SCENE, - }, - SERVICE_REMINDER_ON: {"method": "async_reminder_on"}, - SERVICE_REMINDER_OFF: {"method": "async_reminder_off"}, - SERVICE_NIGHT_LIGHT_MODE_ON: {"method": "async_night_light_mode_on"}, - SERVICE_NIGHT_LIGHT_MODE_OFF: {"method": "async_night_light_mode_off"}, - SERVICE_EYECARE_MODE_ON: {"method": "async_eyecare_mode_on"}, - SERVICE_EYECARE_MODE_OFF: {"method": "async_eyecare_mode_off"}, + SERVICE_SET_DELAYED_TURN_OFF: ServiceMethodDetails( + method="async_set_delayed_turn_off", + schema=SERVICE_SCHEMA_SET_DELAYED_TURN_OFF, + ), + SERVICE_SET_SCENE: ServiceMethodDetails( + method="async_set_scene", + schema=SERVICE_SCHEMA_SET_SCENE, + ), + SERVICE_REMINDER_ON: ServiceMethodDetails(method="async_reminder_on"), + SERVICE_REMINDER_OFF: ServiceMethodDetails(method="async_reminder_off"), + SERVICE_NIGHT_LIGHT_MODE_ON: ServiceMethodDetails( + method="async_night_light_mode_on" + ), + SERVICE_NIGHT_LIGHT_MODE_OFF: ServiceMethodDetails( + method="async_night_light_mode_off" + ), + SERVICE_EYECARE_MODE_ON: ServiceMethodDetails(method="async_eyecare_mode_on"), + SERVICE_EYECARE_MODE_OFF: ServiceMethodDetails(method="async_eyecare_mode_off"), } @@ -232,9 +237,9 @@ async def async_setup_entry( update_tasks = [] for target_device in target_devices: - if not hasattr(target_device, method["method"]): + if not hasattr(target_device, method.method): continue - await getattr(target_device, method["method"])(**params) + await getattr(target_device, method.method)(**params) update_tasks.append( asyncio.create_task(target_device.async_update_ha_state(True)) ) @@ -243,7 +248,7 @@ async def async_setup_entry( await asyncio.wait(update_tasks) for xiaomi_miio_service, method in SERVICE_TO_METHOD.items(): - schema = method.get("schema", XIAOMI_MIIO_SERVICE_SCHEMA) + schema = method.schema or XIAOMI_MIIO_SERVICE_SCHEMA hass.services.async_register( DOMAIN, xiaomi_miio_service, async_service_handler, schema=schema ) diff --git a/homeassistant/components/xiaomi_miio/switch.py b/homeassistant/components/xiaomi_miio/switch.py index 34ebb9addf5..797a98d9fa1 100644 --- a/homeassistant/components/xiaomi_miio/switch.py +++ b/homeassistant/components/xiaomi_miio/switch.py @@ -115,6 +115,7 @@ from .const import ( ) from .device import XiaomiCoordinatedMiioEntity, XiaomiMiioEntity from .gateway import XiaomiGatewayDevice +from .typing import ServiceMethodDetails _LOGGER = logging.getLogger(__name__) @@ -176,16 +177,16 @@ SERVICE_SCHEMA_POWER_PRICE = SERVICE_SCHEMA.extend( ) SERVICE_TO_METHOD = { - SERVICE_SET_WIFI_LED_ON: {"method": "async_set_wifi_led_on"}, - SERVICE_SET_WIFI_LED_OFF: {"method": "async_set_wifi_led_off"}, - SERVICE_SET_POWER_MODE: { - "method": "async_set_power_mode", - "schema": SERVICE_SCHEMA_POWER_MODE, - }, - SERVICE_SET_POWER_PRICE: { - "method": "async_set_power_price", - "schema": SERVICE_SCHEMA_POWER_PRICE, - }, + SERVICE_SET_WIFI_LED_ON: ServiceMethodDetails(method="async_set_wifi_led_on"), + SERVICE_SET_WIFI_LED_OFF: ServiceMethodDetails(method="async_set_wifi_led_off"), + SERVICE_SET_POWER_MODE: ServiceMethodDetails( + method="async_set_power_mode", + schema=SERVICE_SCHEMA_POWER_MODE, + ), + SERVICE_SET_POWER_PRICE: ServiceMethodDetails( + method="async_set_power_price", + schema=SERVICE_SCHEMA_POWER_PRICE, + ), } MODEL_TO_FEATURES_MAP = { @@ -488,9 +489,9 @@ async def async_setup_other_entry(hass, config_entry, async_add_entities): update_tasks = [] for device in devices: - if not hasattr(device, method["method"]): + if not hasattr(device, method.method): continue - await getattr(device, method["method"])(**params) + await getattr(device, method.method)(**params) update_tasks.append( asyncio.create_task(device.async_update_ha_state(True)) ) @@ -499,7 +500,7 @@ async def async_setup_other_entry(hass, config_entry, async_add_entities): await asyncio.wait(update_tasks) for plug_service, method in SERVICE_TO_METHOD.items(): - schema = method.get("schema", SERVICE_SCHEMA) + schema = method.schema or SERVICE_SCHEMA hass.services.async_register( DOMAIN, plug_service, async_service_handler, schema=schema ) diff --git a/homeassistant/components/xiaomi_miio/typing.py b/homeassistant/components/xiaomi_miio/typing.py new file mode 100644 index 00000000000..8fbb8e3d83f --- /dev/null +++ b/homeassistant/components/xiaomi_miio/typing.py @@ -0,0 +1,12 @@ +"""Typings for the xiaomi_miio integration.""" + +from typing import NamedTuple + +import voluptuous as vol + + +class ServiceMethodDetails(NamedTuple): + """Details for SERVICE_TO_METHOD mapping.""" + + method: str + schema: vol.Schema | None = None From 2380696fcdac1ff6c351a5531ece11677fa34f5b Mon Sep 17 00:00:00 2001 From: EnjoyingM <6302356+mtielen@users.noreply.github.com> Date: Wed, 26 Jun 2024 02:22:09 +0200 Subject: [PATCH 1202/1445] Bump wolf-comm to 0.0.9 (#120473) --- homeassistant/components/wolflink/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/wolflink/manifest.json b/homeassistant/components/wolflink/manifest.json index e406217a0c8..6a98dcd6ca4 100644 --- a/homeassistant/components/wolflink/manifest.json +++ b/homeassistant/components/wolflink/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/wolflink", "iot_class": "cloud_polling", "loggers": ["wolf_comm"], - "requirements": ["wolf-comm==0.0.8"] + "requirements": ["wolf-comm==0.0.9"] } diff --git a/requirements_all.txt b/requirements_all.txt index cc8f8898430..82ca566a925 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2899,7 +2899,7 @@ wirelesstagpy==0.8.1 wled==0.18.0 # homeassistant.components.wolflink -wolf-comm==0.0.8 +wolf-comm==0.0.9 # homeassistant.components.wyoming wyoming==1.5.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f5d36af76f4..b0d39fdd0bc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2261,7 +2261,7 @@ wiffi==1.1.2 wled==0.18.0 # homeassistant.components.wolflink -wolf-comm==0.0.8 +wolf-comm==0.0.9 # homeassistant.components.wyoming wyoming==1.5.4 From 49df0c43668b921d7cbc53b5f0058fb1ce7e68b0 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Wed, 26 Jun 2024 02:25:30 +0200 Subject: [PATCH 1203/1445] Improve schema typing (2) (#120475) --- .../components/aurora_abb_powerone/config_flow.py | 6 ++++-- homeassistant/components/device_automation/__init__.py | 4 ++-- homeassistant/components/group/config_flow.py | 1 + homeassistant/components/knx/trigger.py | 4 ++-- homeassistant/components/mysensors/helpers.py | 3 ++- homeassistant/components/sma/config_flow.py | 2 +- homeassistant/components/zwave_js/helpers.py | 6 ++++-- homeassistant/helpers/config_validation.py | 7 ++++--- homeassistant/helpers/entity_platform.py | 2 +- homeassistant/helpers/llm.py | 1 + homeassistant/helpers/selector.py | 2 +- 11 files changed, 23 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/aurora_abb_powerone/config_flow.py b/homeassistant/components/aurora_abb_powerone/config_flow.py index a1e046f302f..f0093c62631 100644 --- a/homeassistant/components/aurora_abb_powerone/config_flow.py +++ b/homeassistant/components/aurora_abb_powerone/config_flow.py @@ -4,7 +4,7 @@ from __future__ import annotations from collections.abc import Mapping import logging -from typing import Any +from typing import TYPE_CHECKING, Any from aurorapy.client import AuroraError, AuroraSerialClient import serial.tools.list_ports @@ -78,7 +78,7 @@ class AuroraABBConfigFlow(ConfigFlow, domain=DOMAIN): def __init__(self): """Initialise the config flow.""" self.config = None - self._com_ports_list = None + self._com_ports_list: list[str] | None = None self._default_com_port = None async def async_step_user( @@ -92,6 +92,8 @@ class AuroraABBConfigFlow(ConfigFlow, domain=DOMAIN): self._com_ports_list, self._default_com_port = result if self._default_com_port is None: return self.async_abort(reason="no_serial_ports") + if TYPE_CHECKING: + assert isinstance(self._com_ports_list, list) # Handle the initial step. if user_input is not None: diff --git a/homeassistant/components/device_automation/__init__.py b/homeassistant/components/device_automation/__init__.py index 567b8fcc2d2..5e196f40aa1 100644 --- a/homeassistant/components/device_automation/__init__.py +++ b/homeassistant/components/device_automation/__init__.py @@ -29,7 +29,7 @@ from homeassistant.helpers import ( device_registry as dr, entity_registry as er, ) -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.typing import ConfigType, VolSchemaType from homeassistant.loader import IntegrationNotFound from homeassistant.requirements import ( RequirementsNotFound, @@ -340,7 +340,7 @@ def async_get_entity_registry_entry_or_raise( @callback def async_validate_entity_schema( - hass: HomeAssistant, config: ConfigType, schema: vol.Schema + hass: HomeAssistant, config: ConfigType, schema: VolSchemaType ) -> ConfigType: """Validate schema and resolve entity registry entry id to entity_id.""" config = schema(config) diff --git a/homeassistant/components/group/config_flow.py b/homeassistant/components/group/config_flow.py index b7341aff59a..4eb0f1cdd52 100644 --- a/homeassistant/components/group/config_flow.py +++ b/homeassistant/components/group/config_flow.py @@ -51,6 +51,7 @@ async def basic_group_options_schema( domain: str | list[str], handler: SchemaCommonFlowHandler | None ) -> vol.Schema: """Generate options schema.""" + entity_selector: selector.Selector[Any] | vol.Schema if handler is None: entity_selector = selector.selector( {"entity": {"domain": domain, "multiple": True}} diff --git a/homeassistant/components/knx/trigger.py b/homeassistant/components/knx/trigger.py index 1df1ffd6c3b..82149b21561 100644 --- a/homeassistant/components/knx/trigger.py +++ b/homeassistant/components/knx/trigger.py @@ -13,7 +13,7 @@ from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.typing import ConfigType, VolDictType from .const import DOMAIN from .schema import ga_validator @@ -32,7 +32,7 @@ CONF_KNX_INCOMING: Final = "incoming" CONF_KNX_OUTGOING: Final = "outgoing" -TELEGRAM_TRIGGER_SCHEMA: Final = { +TELEGRAM_TRIGGER_SCHEMA: VolDictType = { vol.Optional(CONF_KNX_DESTINATION): vol.All(cv.ensure_list, [ga_validator]), vol.Optional(CONF_KNX_GROUP_VALUE_WRITE, default=True): cv.boolean, vol.Optional(CONF_KNX_GROUP_VALUE_RESPONSE, default=True): cv.boolean, diff --git a/homeassistant/components/mysensors/helpers.py b/homeassistant/components/mysensors/helpers.py index c456cfd1f11..f060f3313dc 100644 --- a/homeassistant/components/mysensors/helpers.py +++ b/homeassistant/components/mysensors/helpers.py @@ -6,6 +6,7 @@ from collections import defaultdict from collections.abc import Callable from enum import IntEnum import logging +from typing import cast from mysensors import BaseAsyncGateway, Message from mysensors.sensor import ChildSensor @@ -151,7 +152,7 @@ def get_child_schema( ) -> vol.Schema: """Return a child schema.""" set_req = gateway.const.SetReq - child_schema = child.get_schema(gateway.protocol_version) + child_schema = cast(vol.Schema, child.get_schema(gateway.protocol_version)) return child_schema.extend( { vol.Required( diff --git a/homeassistant/components/sma/config_flow.py b/homeassistant/components/sma/config_flow.py index 3bfb66c4849..fe26cbee2c8 100644 --- a/homeassistant/components/sma/config_flow.py +++ b/homeassistant/components/sma/config_flow.py @@ -43,7 +43,7 @@ class SmaConfigFlow(ConfigFlow, domain=DOMAIN): def __init__(self) -> None: """Initialize.""" - self._data = { + self._data: dict[str, Any] = { CONF_HOST: vol.UNDEFINED, CONF_SSL: False, CONF_VERIFY_SSL: True, diff --git a/homeassistant/components/zwave_js/helpers.py b/homeassistant/components/zwave_js/helpers.py index 598cf2f78f6..737b8deff34 100644 --- a/homeassistant/components/zwave_js/helpers.py +++ b/homeassistant/components/zwave_js/helpers.py @@ -40,7 +40,7 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.group import expand_entity_ids -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.typing import ConfigType, VolSchemaType from .const import ( ATTR_COMMAND_CLASS, @@ -479,7 +479,9 @@ def copy_available_params( ) -def get_value_state_schema(value: ZwaveValue) -> vol.Schema | None: +def get_value_state_schema( + value: ZwaveValue, +) -> VolSchemaType | vol.Coerce | vol.In | None: """Return device automation schema for a config entry.""" if isinstance(value, ConfigurationValue): min_ = value.metadata.min diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 0463bb07e11..558baaeb779 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -108,6 +108,7 @@ from homeassistant.util.yaml.objects import NodeStrClass from . import script_variables as script_variables_helper, template as template_helper from .frame import get_integration_logger +from .typing import VolDictType, VolSchemaType TIME_PERIOD_ERROR = "offset {} should be format 'HH:MM', 'HH:MM:SS' or 'HH:MM:SS.F'" @@ -980,8 +981,8 @@ def removed( def key_value_schemas( key: str, - value_schemas: dict[Hashable, vol.Schema], - default_schema: vol.Schema | None = None, + value_schemas: dict[Hashable, VolSchemaType], + default_schema: VolSchemaType | None = None, default_description: str | None = None, ) -> Callable[[Any], dict[Hashable, Any]]: """Create a validator that validates based on a value for specific key. @@ -1355,7 +1356,7 @@ NUMERIC_STATE_THRESHOLD_SCHEMA = vol.Any( vol.All(str, entity_domain(["input_number", "number", "sensor", "zone"])), ) -CONDITION_BASE_SCHEMA = { +CONDITION_BASE_SCHEMA: VolDictType = { vol.Optional(CONF_ALIAS): string, vol.Optional(CONF_ENABLED): vol.Any(boolean, template), } diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index 6774780f00f..d868e582f8f 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -985,7 +985,7 @@ class EntityPlatform: def async_register_entity_service( self, name: str, - schema: VolDictType | VolSchemaType, + schema: VolDictType | VolSchemaType | None, func: str | Callable[..., Any], required_features: Iterable[int] | None = None, supports_response: SupportsResponse = SupportsResponse.NONE, diff --git a/homeassistant/helpers/llm.py b/homeassistant/helpers/llm.py index 480b9cb5237..4410de67ef2 100644 --- a/homeassistant/helpers/llm.py +++ b/homeassistant/helpers/llm.py @@ -660,6 +660,7 @@ class ScriptTool(Tool): description = config.get("description") if not description: description = config.get("name") + key: vol.Marker if config.get("required"): key = vol.Required(field, description=description) else: diff --git a/homeassistant/helpers/selector.py b/homeassistant/helpers/selector.py index 16aaa40db86..5a542657d10 100644 --- a/homeassistant/helpers/selector.py +++ b/homeassistant/helpers/selector.py @@ -1182,7 +1182,7 @@ class SelectSelector(Selector[SelectSelectorConfig]): for option in cast(Sequence[SelectOptionDict], config_options) ] - parent_schema = vol.In(options) + parent_schema: vol.In | vol.Any = vol.In(options) if self.config["custom_value"]: parent_schema = vol.Any(parent_schema, str) From 6fb32db151ad5cadea8b138b9605fd96a4c3c370 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Wed, 26 Jun 2024 03:03:54 +0200 Subject: [PATCH 1204/1445] Improve config vol.Invalid typing (#120482) --- homeassistant/config.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/config.py b/homeassistant/config.py index 8e22f2051f0..ff679d4df51 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -614,7 +614,7 @@ def _get_annotation(item: Any) -> tuple[str, int | str] | None: return (getattr(item, "__config_file__"), getattr(item, "__line__", "?")) -def _get_by_path(data: dict | list, items: list[str | int]) -> Any: +def _get_by_path(data: dict | list, items: list[Hashable]) -> Any: """Access a nested object in root by item sequence. Returns None in case of error. @@ -626,7 +626,7 @@ def _get_by_path(data: dict | list, items: list[str | int]) -> Any: def find_annotation( - config: dict | list, path: list[str | int] + config: dict | list, path: list[Hashable] ) -> tuple[str, int | str] | None: """Find file/line annotation for a node in config pointed to by path. @@ -636,7 +636,7 @@ def find_annotation( """ def find_annotation_for_key( - item: dict, path: list[str | int], tail: str | int + item: dict, path: list[Hashable], tail: Hashable ) -> tuple[str, int | str] | None: for key in item: if key == tail: @@ -646,7 +646,7 @@ def find_annotation( return None def find_annotation_rec( - config: dict | list, path: list[str | int], tail: str | int | None + config: dict | list, path: list[Hashable], tail: Hashable | None ) -> tuple[str, int | str] | None: item = _get_by_path(config, path) if isinstance(item, dict) and tail is not None: From 07b70cba1076e021595a72d522b10fe7baee0208 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 26 Jun 2024 08:32:43 +0200 Subject: [PATCH 1205/1445] Fix dropped unifiprotect motion events (#120489) --- .../components/unifiprotect/binary_sensor.py | 33 +++---------------- .../unifiprotect/test_binary_sensor.py | 3 +- 2 files changed, 5 insertions(+), 31 deletions(-) diff --git a/homeassistant/components/unifiprotect/binary_sensor.py b/homeassistant/components/unifiprotect/binary_sensor.py index fb60158580e..e35eb6f48f3 100644 --- a/homeassistant/components/unifiprotect/binary_sensor.py +++ b/homeassistant/components/unifiprotect/binary_sensor.py @@ -437,9 +437,6 @@ EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = ( ufp_enabled="is_motion_detection_on", ufp_event_obj="last_motion_event", ), -) - -SMART_EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = ( ProtectBinaryEventEntityDescription( key="smart_obj_any", name="Object detected", @@ -711,26 +708,6 @@ class ProtectEventBinarySensor(EventEntityMixin, BinarySensorEntity): entity_description: ProtectBinaryEventEntityDescription _state_attrs = ("_attr_available", "_attr_is_on", "_attr_extra_state_attributes") - @callback - def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None: - super()._async_update_device_from_protect(device) - description = self.entity_description - event = self.entity_description.get_event_obj(device) - if is_on := bool(description.get_ufp_value(device)): - if event: - self._set_event_attrs(event) - else: - self._attr_extra_state_attributes = {} - self._attr_is_on = is_on - - -class ProtectSmartEventBinarySensor(EventEntityMixin, BinarySensorEntity): - """A UniFi Protect Device Binary Sensor for smart events.""" - - device: Camera - entity_description: ProtectBinaryEventEntityDescription - _state_attrs = ("_attr_available", "_attr_is_on", "_attr_extra_state_attributes") - @callback def _set_event_done(self) -> None: self._attr_is_on = False @@ -749,7 +726,10 @@ class ProtectSmartEventBinarySensor(EventEntityMixin, BinarySensorEntity): if not ( event - and description.has_matching_smart(event) + and ( + description.ufp_obj_type is None + or description.has_matching_smart(event) + ) and not self._event_already_ended(prev_event, prev_event_end) ): self._set_event_done() @@ -774,11 +754,6 @@ def _async_event_entities( ) -> list[ProtectDeviceEntity]: entities: list[ProtectDeviceEntity] = [] for device in data.get_cameras() if ufp_device is None else [ufp_device]: - entities.extend( - ProtectSmartEventBinarySensor(data, device, description) - for description in SMART_EVENT_SENSORS - if description.has_required(device) - ) entities.extend( ProtectEventBinarySensor(data, device, description) for description in EVENT_SENSORS diff --git a/tests/components/unifiprotect/test_binary_sensor.py b/tests/components/unifiprotect/test_binary_sensor.py index 42782d10429..af8ce015955 100644 --- a/tests/components/unifiprotect/test_binary_sensor.py +++ b/tests/components/unifiprotect/test_binary_sensor.py @@ -25,7 +25,6 @@ from homeassistant.components.unifiprotect.binary_sensor import ( LIGHT_SENSORS, MOUNTABLE_SENSE_SENSORS, SENSE_SENSORS, - SMART_EVENT_SENSORS, ) from homeassistant.components.unifiprotect.const import ( ATTR_EVENT_SCORE, @@ -453,7 +452,7 @@ async def test_binary_sensor_package_detected( doorbell.smart_detect_settings.object_types.append(SmartDetectObjectType.PACKAGE) _, entity_id = ids_from_device_description( - Platform.BINARY_SENSOR, doorbell, SMART_EVENT_SENSORS[4] + Platform.BINARY_SENSOR, doorbell, EVENT_SENSORS[6] ) event = Event( From ba40340f82388b54b4c3c5adcb260d2fb6c8296a Mon Sep 17 00:00:00 2001 From: "Mr. Bubbles" Date: Wed, 26 Jun 2024 08:45:22 +0200 Subject: [PATCH 1206/1445] Align deviceinfo entries in pyLoad integration (#120478) --- homeassistant/components/pyload/button.py | 2 +- homeassistant/components/pyload/sensor.py | 14 +++++++++++--- tests/components/pyload/snapshots/test_button.ambr | 8 ++++---- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/pyload/button.py b/homeassistant/components/pyload/button.py index 1f6bf3c3d10..0d8a232142a 100644 --- a/homeassistant/components/pyload/button.py +++ b/homeassistant/components/pyload/button.py @@ -99,7 +99,7 @@ class PyLoadBinarySensor(CoordinatorEntity[PyLoadCoordinator], ButtonEntity): model=SERVICE_NAME, configuration_url=coordinator.pyload.api_url, identifiers={(DOMAIN, coordinator.config_entry.entry_id)}, - translation_key=DOMAIN, + sw_version=coordinator.version, ) async def async_press(self) -> None: diff --git a/homeassistant/components/pyload/sensor.py b/homeassistant/components/pyload/sensor.py index c4fea3e43bb..3d681c4b65d 100644 --- a/homeassistant/components/pyload/sensor.py +++ b/homeassistant/components/pyload/sensor.py @@ -34,7 +34,15 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateTyp from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import PyLoadConfigEntry -from .const import DEFAULT_HOST, DEFAULT_NAME, DEFAULT_PORT, DOMAIN, ISSUE_PLACEHOLDER +from .const import ( + DEFAULT_HOST, + DEFAULT_NAME, + DEFAULT_PORT, + DOMAIN, + ISSUE_PLACEHOLDER, + MANUFACTURER, + SERVICE_NAME, +) from .coordinator import PyLoadCoordinator @@ -175,8 +183,8 @@ class PyLoadSensor(CoordinatorEntity[PyLoadCoordinator], SensorEntity): self.entity_description = entity_description self.device_info = DeviceInfo( entry_type=DeviceEntryType.SERVICE, - manufacturer="PyLoad Team", - model="pyLoad", + manufacturer=MANUFACTURER, + model=SERVICE_NAME, configuration_url=coordinator.pyload.api_url, identifiers={(DOMAIN, coordinator.config_entry.entry_id)}, sw_version=coordinator.version, diff --git a/tests/components/pyload/snapshots/test_button.ambr b/tests/components/pyload/snapshots/test_button.ambr index c9a901aba15..bf1e1f59c98 100644 --- a/tests/components/pyload/snapshots/test_button.ambr +++ b/tests/components/pyload/snapshots/test_button.ambr @@ -35,7 +35,7 @@ # name: test_state[button.pyload_abort_all_running_downloads-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'pyload Abort all running downloads', + 'friendly_name': 'pyLoad Abort all running downloads', }), 'context': , 'entity_id': 'button.pyload_abort_all_running_downloads', @@ -81,7 +81,7 @@ # name: test_state[button.pyload_delete_finished_files_packages-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'pyload Delete finished files/packages', + 'friendly_name': 'pyLoad Delete finished files/packages', }), 'context': , 'entity_id': 'button.pyload_delete_finished_files_packages', @@ -127,7 +127,7 @@ # name: test_state[button.pyload_restart_all_failed_files-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'pyload Restart all failed files', + 'friendly_name': 'pyLoad Restart all failed files', }), 'context': , 'entity_id': 'button.pyload_restart_all_failed_files', @@ -173,7 +173,7 @@ # name: test_state[button.pyload_restart_pyload_core-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'pyload Restart pyload core', + 'friendly_name': 'pyLoad Restart pyload core', }), 'context': , 'entity_id': 'button.pyload_restart_pyload_core', From 0f7229f55fd7152b962f1799f2328960c3cac7b7 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Wed, 26 Jun 2024 08:45:53 +0200 Subject: [PATCH 1207/1445] Fix holiday using utc instead of local time (#120432) --- homeassistant/components/holiday/calendar.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/holiday/calendar.py b/homeassistant/components/holiday/calendar.py index f56f4f90831..6a336870857 100644 --- a/homeassistant/components/holiday/calendar.py +++ b/homeassistant/components/holiday/calendar.py @@ -122,7 +122,7 @@ class HolidayCalendarEntity(CalendarEntity): def _update_state_and_setup_listener(self) -> None: """Update state and setup listener for next interval.""" - now = dt_util.utcnow() + now = dt_util.now() self._attr_event = self.update_event(now) self.unsub = async_track_point_in_utc_time( self.hass, self.point_in_time_listener, self.get_next_interval(now) From cc6aac5e75c58820dc6c7269a66f3c7c588bbb6a Mon Sep 17 00:00:00 2001 From: "Mr. Bubbles" Date: Wed, 26 Jun 2024 08:50:45 +0200 Subject: [PATCH 1208/1445] Add missing textselectors in `USER_DATA_SCHEMA` in pyLoad integration (#120479) --- homeassistant/components/pyload/config_flow.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/pyload/config_flow.py b/homeassistant/components/pyload/config_flow.py index 7a2dfddeb5b..4825e6fa7cf 100644 --- a/homeassistant/components/pyload/config_flow.py +++ b/homeassistant/components/pyload/config_flow.py @@ -41,8 +41,18 @@ STEP_USER_DATA_SCHEMA = vol.Schema( vol.Required(CONF_PORT, default=DEFAULT_PORT): cv.port, vol.Required(CONF_SSL, default=False): cv.boolean, vol.Required(CONF_VERIFY_SSL, default=True): bool, - vol.Required(CONF_USERNAME): str, - vol.Required(CONF_PASSWORD): str, + vol.Required(CONF_USERNAME): TextSelector( + TextSelectorConfig( + type=TextSelectorType.TEXT, + autocomplete="username", + ), + ), + vol.Required(CONF_PASSWORD): TextSelector( + TextSelectorConfig( + type=TextSelectorType.PASSWORD, + autocomplete="current-password", + ), + ), } ) From f23020919f5e0566fcee6b40f1e5569a524542cc Mon Sep 17 00:00:00 2001 From: "Mr. Bubbles" Date: Wed, 26 Jun 2024 08:51:12 +0200 Subject: [PATCH 1209/1445] Remove unused translation strings in pyLoad integration (#120476) --- homeassistant/components/pyload/strings.json | 2 -- 1 file changed, 2 deletions(-) diff --git a/homeassistant/components/pyload/strings.json b/homeassistant/components/pyload/strings.json index 6efdb23eaf4..248cec99cc9 100644 --- a/homeassistant/components/pyload/strings.json +++ b/homeassistant/components/pyload/strings.json @@ -3,7 +3,6 @@ "step": { "user": { "data": { - "name": "[%key:common::config_flow::data::name%]", "host": "[%key:common::config_flow::data::host%]", "username": "[%key:common::config_flow::data::username%]", "password": "[%key:common::config_flow::data::password%]", @@ -12,7 +11,6 @@ "port": "[%key:common::config_flow::data::port%]" }, "data_description": { - "name": "The name to use for your pyLoad instance in Home Assistant", "host": "The hostname or IP address of the device running your pyLoad instance.", "port": "pyLoad uses port 8000 by default." } From b7e7905b54c61a6d80e4b6d29f1291463f85bcac Mon Sep 17 00:00:00 2001 From: Grubalex <74829763+Grubalex@users.noreply.github.com> Date: Wed, 26 Jun 2024 08:51:55 +0200 Subject: [PATCH 1210/1445] Add Philips WiZ Lightbulbs to Matter TRANSITION_BLOCKLIST (#120461) --- homeassistant/components/matter/light.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/matter/light.py b/homeassistant/components/matter/light.py index 777e4a69010..749d82fd661 100644 --- a/homeassistant/components/matter/light.py +++ b/homeassistant/components/matter/light.py @@ -57,6 +57,11 @@ TRANSITION_BLOCKLIST = ( (4999, 25057, "1.0", "27.0"), (4448, 36866, "V1", "V1.0.0.5"), (5009, 514, "1.0", "1.0.0"), + (4107, 8475, "v1.0", "v1.0"), + (4107, 8550, "v1.0", "v1.0"), + (4107, 8551, "v1.0", "v1.0"), + (4107, 8656, "v1.0", "v1.0"), + (4107, 8571, "v1.0", "v1.0"), ) From cef1d35e31b1a2269c10cc651810d1b67aad0cac Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 26 Jun 2024 08:51:57 +0200 Subject: [PATCH 1211/1445] Make fetching integrations with requirements safer (#120481) --- homeassistant/requirements.py | 50 ++++++++++++++--------------------- 1 file changed, 20 insertions(+), 30 deletions(-) diff --git a/homeassistant/requirements.py b/homeassistant/requirements.py index c0e92610b6e..4de5fed5a73 100644 --- a/homeassistant/requirements.py +++ b/homeassistant/requirements.py @@ -4,16 +4,16 @@ from __future__ import annotations import asyncio from collections.abc import Iterable +import contextlib import logging import os -from typing import Any, cast +from typing import Any from packaging.requirements import Requirement from .core import HomeAssistant, callback from .exceptions import HomeAssistantError from .helpers import singleton -from .helpers.typing import UNDEFINED, UndefinedType from .loader import Integration, IntegrationNotFound, async_get_integration from .util import package as pkg_util @@ -119,11 +119,6 @@ def _install_requirements_if_missing( return installed, failures -def _set_result_unless_done(future: asyncio.Future[None]) -> None: - if not future.done(): - future.set_result(None) - - class RequirementsManager: """Manage requirements.""" @@ -132,7 +127,7 @@ class RequirementsManager: self.hass = hass self.pip_lock = asyncio.Lock() self.integrations_with_reqs: dict[ - str, Integration | asyncio.Future[None] | None | UndefinedType + str, Integration | asyncio.Future[Integration] ] = {} self.install_failure_history: set[str] = set() self.is_installed_cache: set[str] = set() @@ -151,37 +146,32 @@ class RequirementsManager: else: done.add(domain) - if self.hass.config.skip_pip: - return await async_get_integration(self.hass, domain) - cache = self.integrations_with_reqs - int_or_fut = cache.get(domain, UNDEFINED) - - if isinstance(int_or_fut, asyncio.Future): - await int_or_fut - - # When we have waited and it's UNDEFINED, it doesn't exist - # We don't cache that it doesn't exist, or else people can't fix it - # and then restart, because their config will never be valid. - if (int_or_fut := cache.get(domain, UNDEFINED)) is UNDEFINED: - raise IntegrationNotFound(domain) - - if int_or_fut is not UNDEFINED: - return cast(Integration, int_or_fut) + if int_or_fut := cache.get(domain): + if isinstance(int_or_fut, Integration): + return int_or_fut + return await int_or_fut future = cache[domain] = self.hass.loop.create_future() - try: integration = await async_get_integration(self.hass, domain) - await self._async_process_integration(integration, done) - except Exception: + if not self.hass.config.skip_pip: + await self._async_process_integration(integration, done) + except BaseException as ex: + # We do not cache failures as we want to retry, or + # else people can't fix it and then restart, because + # their config will never be valid. del cache[domain] + future.set_exception(ex) + with contextlib.suppress(BaseException): + # Clear the flag as its normal that nothing + # will wait for this future to be resolved + # if there are no concurrent requirements fetches. + await future raise - finally: - _set_result_unless_done(future) cache[domain] = integration - _set_result_unless_done(future) + future.set_result(integration) return integration async def _async_process_integration( From fab901f9b690e856416f3c80ce3132dcb1d11f8f Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Wed, 26 Jun 2024 08:53:28 +0200 Subject: [PATCH 1212/1445] Cleanup mqtt platform tests part 2 (#120490) --- tests/components/mqtt/test_cover.py | 49 +++++----------- tests/components/mqtt/test_device_tracker.py | 27 +++------ tests/components/mqtt/test_device_trigger.py | 40 ++++++------- tests/components/mqtt/test_diagnostics.py | 1 - tests/components/mqtt/test_discovery.py | 61 ++++++-------------- tests/components/mqtt/test_event.py | 15 ++--- 6 files changed, 67 insertions(+), 126 deletions(-) diff --git a/tests/components/mqtt/test_cover.py b/tests/components/mqtt/test_cover.py index 988119d09c1..f37de8b6a2e 100644 --- a/tests/components/mqtt/test_cover.py +++ b/tests/components/mqtt/test_cover.py @@ -697,9 +697,7 @@ async def test_position_via_template_and_entity_id( ], ) async def test_optimistic_flag( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - assumed_state: bool, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, assumed_state: bool ) -> None: """Test assumed_state is set correctly.""" await mqtt_mock_entry() @@ -1073,10 +1071,9 @@ async def test_current_cover_position_inverted( } ], ) +@pytest.mark.usefixtures("hass") async def test_optimistic_position( - hass: HomeAssistant, - caplog: pytest.LogCaptureFixture, - mqtt_mock_entry: MqttMockHAClientGenerator, + caplog: pytest.LogCaptureFixture, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test optimistic position is not supported.""" assert await mqtt_mock_entry() @@ -1627,7 +1624,6 @@ async def test_tilt_via_invocation_defaults( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test tilt defaults on close/open.""" - await hass.async_block_till_done() mqtt_mock = await mqtt_mock_entry() await hass.services.async_call( @@ -2547,11 +2543,7 @@ async def test_update_with_json_attrs_not_dict( ) -> None: """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_not_dict( - hass, - mqtt_mock_entry, - caplog, - cover.DOMAIN, - DEFAULT_CONFIG, + hass, mqtt_mock_entry, caplog, cover.DOMAIN, DEFAULT_CONFIG ) @@ -2562,11 +2554,7 @@ async def test_update_with_json_attrs_bad_json( ) -> None: """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_bad_json( - hass, - mqtt_mock_entry, - caplog, - cover.DOMAIN, - DEFAULT_CONFIG, + hass, mqtt_mock_entry, caplog, cover.DOMAIN, DEFAULT_CONFIG ) @@ -3221,10 +3209,9 @@ async def test_position_via_position_topic_template_return_invalid_json( } ], ) +@pytest.mark.usefixtures("hass") async def test_set_position_topic_without_get_position_topic_error( - hass: HomeAssistant, - caplog: pytest.LogCaptureFixture, - mqtt_mock_entry: MqttMockHAClientGenerator, + caplog: pytest.LogCaptureFixture, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test error when set_position_topic is used without position_topic.""" assert await mqtt_mock_entry() @@ -3247,8 +3234,8 @@ async def test_set_position_topic_without_get_position_topic_error( } ], ) +@pytest.mark.usefixtures("hass") async def test_value_template_without_state_topic_error( - hass: HomeAssistant, caplog: pytest.LogCaptureFixture, mqtt_mock_entry: MqttMockHAClientGenerator, ) -> None: @@ -3273,8 +3260,8 @@ async def test_value_template_without_state_topic_error( } ], ) +@pytest.mark.usefixtures("hass") async def test_position_template_without_position_topic_error( - hass: HomeAssistant, caplog: pytest.LogCaptureFixture, mqtt_mock_entry: MqttMockHAClientGenerator, ) -> None: @@ -3300,10 +3287,9 @@ async def test_position_template_without_position_topic_error( } ], ) +@pytest.mark.usefixtures("hass") async def test_set_position_template_without_set_position_topic( - hass: HomeAssistant, - caplog: pytest.LogCaptureFixture, - mqtt_mock_entry: MqttMockHAClientGenerator, + caplog: pytest.LogCaptureFixture, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test error when set_position_template is used and set_position_topic is missing.""" assert await mqtt_mock_entry() @@ -3327,10 +3313,9 @@ async def test_set_position_template_without_set_position_topic( } ], ) +@pytest.mark.usefixtures("hass") async def test_tilt_command_template_without_tilt_command_topic( - hass: HomeAssistant, - caplog: pytest.LogCaptureFixture, - mqtt_mock_entry: MqttMockHAClientGenerator, + caplog: pytest.LogCaptureFixture, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test error when tilt_command_template is used and tilt_command_topic is missing.""" assert await mqtt_mock_entry() @@ -3354,10 +3339,9 @@ async def test_tilt_command_template_without_tilt_command_topic( } ], ) +@pytest.mark.usefixtures("hass") async def test_tilt_status_template_without_tilt_status_topic_topic( - hass: HomeAssistant, - caplog: pytest.LogCaptureFixture, - mqtt_mock_entry: MqttMockHAClientGenerator, + caplog: pytest.LogCaptureFixture, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test error when tilt_status_template is used and tilt_status_topic is missing.""" assert await mqtt_mock_entry() @@ -3423,8 +3407,7 @@ async def test_publishing_with_custom_encoding( async def test_reloadable( - hass: HomeAssistant, - mqtt_client_mock: MqttMockPahoClient, + hass: HomeAssistant, mqtt_client_mock: MqttMockPahoClient ) -> None: """Test reloading the MQTT platform.""" domain = cover.DOMAIN diff --git a/tests/components/mqtt/test_device_tracker.py b/tests/components/mqtt/test_device_tracker.py index 76129d4c549..9759dfcadd7 100644 --- a/tests/components/mqtt/test_device_tracker.py +++ b/tests/components/mqtt/test_device_tracker.py @@ -334,9 +334,7 @@ async def test_setting_device_tracker_value_via_mqtt_message( async def test_setting_device_tracker_value_via_mqtt_message_and_template( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test the setting of the value via MQTT.""" await mqtt_mock_entry() @@ -361,9 +359,7 @@ async def test_setting_device_tracker_value_via_mqtt_message_and_template( async def test_setting_device_tracker_value_via_mqtt_message_and_template2( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test the setting of the value via MQTT.""" await mqtt_mock_entry() @@ -391,9 +387,7 @@ async def test_setting_device_tracker_value_via_mqtt_message_and_template2( async def test_setting_device_tracker_location_via_mqtt_message( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test the setting of the location via MQTT.""" await mqtt_mock_entry() @@ -415,9 +409,7 @@ async def test_setting_device_tracker_location_via_mqtt_message( async def test_setting_device_tracker_location_via_lat_lon_message( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test the setting of the latitude and longitude via MQTT without state topic.""" await mqtt_mock_entry() @@ -472,9 +464,7 @@ async def test_setting_device_tracker_location_via_lat_lon_message( async def test_setting_device_tracker_location_via_reset_message( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test the automatic inference of zones via MQTT via reset.""" await mqtt_mock_entry() @@ -548,9 +538,7 @@ async def test_setting_device_tracker_location_via_reset_message( async def test_setting_device_tracker_location_via_abbr_reset_message( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test the setting of reset via abbreviated names and custom payloads via MQTT.""" await mqtt_mock_entry() @@ -625,8 +613,7 @@ async def test_setup_with_modern_schema( async def test_reloadable( - hass: HomeAssistant, - mqtt_client_mock: MqttMockPahoClient, + hass: HomeAssistant, mqtt_client_mock: MqttMockPahoClient ) -> None: """Test reloading the MQTT platform.""" domain = device_tracker.DOMAIN diff --git a/tests/components/mqtt/test_device_trigger.py b/tests/components/mqtt/test_device_trigger.py index 9e75ea5168b..ce75bd01a03 100644 --- a/tests/components/mqtt/test_device_trigger.py +++ b/tests/components/mqtt/test_device_trigger.py @@ -1,6 +1,7 @@ """The tests for MQTT device triggers.""" import json +from typing import Any import pytest from pytest_unordered import unordered @@ -194,7 +195,6 @@ async def test_update_remove_triggers( hass: HomeAssistant, device_registry: dr.DeviceRegistry, mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, ) -> None: """Test triggers can be updated and removed.""" await mqtt_mock_entry() @@ -1016,10 +1016,10 @@ async def test_attach_remove( await hass.async_block_till_done() device_entry = device_registry.async_get_device(identifiers={("mqtt", "0AFFD2")}) - calls = [] + callback_calls: list[dict[str, Any]] = [] - def callback(trigger): - calls.append(trigger["trigger"]["payload"]) + def trigger_callback(trigger): + callback_calls.append(trigger["trigger"]["payload"]) remove = await async_initialize_triggers( hass, @@ -1033,7 +1033,7 @@ async def test_attach_remove( "subtype": "button_1", }, ], - callback, + trigger_callback, DOMAIN, "mock-name", _LOGGER.log, @@ -1042,8 +1042,8 @@ async def test_attach_remove( # Fake short press. async_fire_mqtt_message(hass, "foobar/triggers/button1", "short_press") await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0] == "short_press" + assert len(callback_calls) == 1 + assert callback_calls[0] == "short_press" # Remove the trigger remove() @@ -1052,7 +1052,7 @@ async def test_attach_remove( # Verify the triggers are no longer active async_fire_mqtt_message(hass, "foobar/triggers/button1", "short_press") await hass.async_block_till_done() - assert len(calls) == 1 + assert len(callback_calls) == 1 async def test_attach_remove_late( @@ -1079,10 +1079,10 @@ async def test_attach_remove_late( await hass.async_block_till_done() device_entry = device_registry.async_get_device(identifiers={("mqtt", "0AFFD2")}) - calls = [] + callback_calls: list[dict[str, Any]] = [] - def callback(trigger): - calls.append(trigger["trigger"]["payload"]) + def trigger_callback(trigger): + callback_calls.append(trigger["trigger"]["payload"]) remove = await async_initialize_triggers( hass, @@ -1096,7 +1096,7 @@ async def test_attach_remove_late( "subtype": "button_1", }, ], - callback, + trigger_callback, DOMAIN, "mock-name", _LOGGER.log, @@ -1108,8 +1108,8 @@ async def test_attach_remove_late( # Fake short press. async_fire_mqtt_message(hass, "foobar/triggers/button1", "short_press") await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0] == "short_press" + assert len(callback_calls) == 1 + assert callback_calls[0] == "short_press" # Remove the trigger remove() @@ -1118,7 +1118,7 @@ async def test_attach_remove_late( # Verify the triggers are no longer active async_fire_mqtt_message(hass, "foobar/triggers/button1", "short_press") await hass.async_block_till_done() - assert len(calls) == 1 + assert len(callback_calls) == 1 async def test_attach_remove_late2( @@ -1145,10 +1145,10 @@ async def test_attach_remove_late2( await hass.async_block_till_done() device_entry = device_registry.async_get_device(identifiers={("mqtt", "0AFFD2")}) - calls = [] + callback_calls: list[dict[str, Any]] = [] - def callback(trigger): - calls.append(trigger["trigger"]["payload"]) + def trigger_callback(trigger): + callback_calls.append(trigger["trigger"]["payload"]) remove = await async_initialize_triggers( hass, @@ -1162,7 +1162,7 @@ async def test_attach_remove_late2( "subtype": "button_1", }, ], - callback, + trigger_callback, DOMAIN, "mock-name", _LOGGER.log, @@ -1178,7 +1178,7 @@ async def test_attach_remove_late2( # Verify the triggers are no longer active async_fire_mqtt_message(hass, "foobar/triggers/button1", "short_press") await hass.async_block_till_done() - assert len(calls) == 0 + assert len(callback_calls) == 0 # Try to remove the trigger twice with pytest.raises(HomeAssistantError): diff --git a/tests/components/mqtt/test_diagnostics.py b/tests/components/mqtt/test_diagnostics.py index f8b547ae1eb..b8499ba5812 100644 --- a/tests/components/mqtt/test_diagnostics.py +++ b/tests/components/mqtt/test_diagnostics.py @@ -26,7 +26,6 @@ default_config = { async def test_entry_diagnostics( hass: HomeAssistant, device_registry: dr.DeviceRegistry, - entity_registry: er.EntityRegistry, hass_client: ClientSessionGenerator, mqtt_mock_entry: MqttMockHAClientGenerator, ) -> None: diff --git a/tests/components/mqtt/test_discovery.py b/tests/components/mqtt/test_discovery.py index b9ef1a3c210..fbf878a040a 100644 --- a/tests/components/mqtt/test_discovery.py +++ b/tests/components/mqtt/test_discovery.py @@ -64,8 +64,7 @@ from tests.typing import ( [{mqtt.CONF_BROKER: "mock-broker", mqtt.CONF_DISCOVERY: False}], ) async def test_subscribing_config_topic( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test setting up discovery.""" mqtt_mock = await mqtt_mock_entry() @@ -205,8 +204,7 @@ async def test_only_valid_components( async def test_correct_config_discovery( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test sending in correct JSON.""" await mqtt_mock_entry() @@ -285,9 +283,7 @@ async def test_discovery_with_invalid_integration_info( """Test sending in correct JSON.""" await mqtt_mock_entry() async_fire_mqtt_message( - hass, - "homeassistant/binary_sensor/bla/config", - config_message, + hass, "homeassistant/binary_sensor/bla/config", config_message ) await hass.async_block_till_done() @@ -298,8 +294,7 @@ async def test_discovery_with_invalid_integration_info( async def test_discover_fan( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test discovering an MQTT fan.""" await mqtt_mock_entry() @@ -318,9 +313,7 @@ async def test_discover_fan( async def test_discover_climate( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test discovering an MQTT climate component.""" await mqtt_mock_entry() @@ -341,8 +334,7 @@ async def test_discover_climate( async def test_discover_alarm_control_panel( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test discovering an MQTT alarm control panel component.""" await mqtt_mock_entry() @@ -531,8 +523,7 @@ async def test_discovery_with_object_id( async def test_discovery_incl_nodeid( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test sending in correct JSON with optional node_id included.""" await mqtt_mock_entry() @@ -581,8 +572,7 @@ async def test_non_duplicate_discovery( async def test_removal( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test removal of component through empty discovery message.""" await mqtt_mock_entry() @@ -602,8 +592,7 @@ async def test_removal( async def test_rediscover( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test rediscover of removed component.""" await mqtt_mock_entry() @@ -632,8 +621,7 @@ async def test_rediscover( async def test_rapid_rediscover( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test immediate rediscover of removed component.""" await mqtt_mock_entry() @@ -684,8 +672,7 @@ async def test_rapid_rediscover( async def test_rapid_rediscover_unique( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test immediate rediscover of removed component.""" await mqtt_mock_entry() @@ -746,8 +733,7 @@ async def test_rapid_rediscover_unique( async def test_rapid_reconfigure( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test immediate reconfigure of added component.""" await mqtt_mock_entry() @@ -1110,8 +1096,7 @@ async def test_cleanup_device_multiple_config_entries_mqtt( async def test_discovery_expansion( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test expansion of abbreviated discovery payload.""" await mqtt_mock_entry() @@ -1172,8 +1157,7 @@ async def test_discovery_expansion( async def test_discovery_expansion_2( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test expansion of abbreviated discovery payload.""" await mqtt_mock_entry() @@ -1249,8 +1233,7 @@ async def test_discovery_expansion_3( async def test_discovery_expansion_without_encoding_and_value_template_1( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test expansion of raw availability payload with a template as list.""" await mqtt_mock_entry() @@ -1300,8 +1283,7 @@ async def test_discovery_expansion_without_encoding_and_value_template_1( async def test_discovery_expansion_without_encoding_and_value_template_2( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test expansion of raw availability payload with a template directly.""" await mqtt_mock_entry() @@ -1379,7 +1361,6 @@ EXCLUDED_MODULES = { async def test_missing_discover_abbreviations( - hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, ) -> None: """Check MQTT platforms for missing abbreviations.""" @@ -1403,8 +1384,7 @@ async def test_missing_discover_abbreviations( async def test_no_implicit_state_topic_switch( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test no implicit state topic for switch.""" await mqtt_mock_entry() @@ -1462,8 +1442,7 @@ async def test_complex_discovery_topic_prefix( @patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.0) @patch("homeassistant.components.mqtt.client.UNSUBSCRIBE_COOLDOWN", 0.0) async def test_mqtt_integration_discovery_subscribe_unsubscribe( - hass: HomeAssistant, - mqtt_client_mock: MqttMockPahoClient, + hass: HomeAssistant, mqtt_client_mock: MqttMockPahoClient ) -> None: """Check MQTT integration discovery subscribe and unsubscribe.""" @@ -1521,8 +1500,7 @@ async def test_mqtt_integration_discovery_subscribe_unsubscribe( @patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.0) @patch("homeassistant.components.mqtt.client.UNSUBSCRIBE_COOLDOWN", 0.0) async def test_mqtt_discovery_unsubscribe_once( - hass: HomeAssistant, - mqtt_client_mock: MqttMockPahoClient, + hass: HomeAssistant, mqtt_client_mock: MqttMockPahoClient ) -> None: """Check MQTT integration discovery unsubscribe once.""" @@ -1657,7 +1635,6 @@ async def test_clean_up_registry_monitoring( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, device_registry: dr.DeviceRegistry, - tmp_path: Path, ) -> None: """Test registry monitoring hook is removed after a reload.""" await mqtt_mock_entry() diff --git a/tests/components/mqtt/test_event.py b/tests/components/mqtt/test_event.py index 48f80bf41d7..662a279f639 100644 --- a/tests/components/mqtt/test_event.py +++ b/tests/components/mqtt/test_event.py @@ -325,10 +325,9 @@ async def test_discovery_update_availability( } ], ) +@pytest.mark.usefixtures("hass") async def test_invalid_device_class( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + mqtt_mock_entry: MqttMockHAClientGenerator, caplog: pytest.LogCaptureFixture ) -> None: """Test device_class option with invalid value.""" assert await mqtt_mock_entry() @@ -444,9 +443,7 @@ async def test_discovery_removal_event( async def test_discovery_update_event_template( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered mqtt event template.""" await mqtt_mock_entry() @@ -665,8 +662,7 @@ async def test_value_template_with_entity_id( async def test_reloadable( - hass: HomeAssistant, - mqtt_client_mock: MqttMockPahoClient, + hass: HomeAssistant, mqtt_client_mock: MqttMockPahoClient ) -> None: """Test reloading the MQTT platform.""" domain = event.DOMAIN @@ -689,8 +685,7 @@ async def test_setup_manual_entity_from_yaml( async def test_unload_entry( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test unloading the config entry.""" domain = event.DOMAIN From 005c71a4a581805366e5e46e6ea13b99758d4e6f Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 26 Jun 2024 08:55:28 +0200 Subject: [PATCH 1213/1445] Deduplicate alarm_control_panel services.yaml (#118796) --- .../alarm_control_panel/services.yaml | 48 +++++-------------- 1 file changed, 12 insertions(+), 36 deletions(-) diff --git a/homeassistant/components/alarm_control_panel/services.yaml b/homeassistant/components/alarm_control_panel/services.yaml index f7a3854b6b3..cabc43a8b80 100644 --- a/homeassistant/components/alarm_control_panel/services.yaml +++ b/homeassistant/components/alarm_control_panel/services.yaml @@ -1,14 +1,15 @@ # Describes the format for available alarm control panel services +.common_service_fields: &common_service_fields + code: + example: "1234" + selector: + text: alarm_disarm: target: entity: domain: alarm_control_panel - fields: - code: - example: "1234" - selector: - text: + fields: *common_service_fields alarm_arm_custom_bypass: target: @@ -16,11 +17,7 @@ alarm_arm_custom_bypass: domain: alarm_control_panel supported_features: - alarm_control_panel.AlarmControlPanelEntityFeature.ARM_CUSTOM_BYPASS - fields: - code: - example: "1234" - selector: - text: + fields: *common_service_fields alarm_arm_home: target: @@ -28,11 +25,7 @@ alarm_arm_home: domain: alarm_control_panel supported_features: - alarm_control_panel.AlarmControlPanelEntityFeature.ARM_HOME - fields: - code: - example: "1234" - selector: - text: + fields: *common_service_fields alarm_arm_away: target: @@ -40,23 +33,14 @@ alarm_arm_away: domain: alarm_control_panel supported_features: - alarm_control_panel.AlarmControlPanelEntityFeature.ARM_AWAY - fields: - code: - example: "1234" - selector: - text: - + fields: *common_service_fields alarm_arm_night: target: entity: domain: alarm_control_panel supported_features: - alarm_control_panel.AlarmControlPanelEntityFeature.ARM_NIGHT - fields: - code: - example: "1234" - selector: - text: + fields: *common_service_fields alarm_arm_vacation: target: @@ -64,11 +48,7 @@ alarm_arm_vacation: domain: alarm_control_panel supported_features: - alarm_control_panel.AlarmControlPanelEntityFeature.ARM_VACATION - fields: - code: - example: "1234" - selector: - text: + fields: *common_service_fields alarm_trigger: target: @@ -76,8 +56,4 @@ alarm_trigger: domain: alarm_control_panel supported_features: - alarm_control_panel.AlarmControlPanelEntityFeature.TRIGGER - fields: - code: - example: "1234" - selector: - text: + fields: *common_service_fields From 9f4bf6f11a97c7b21fe0f2b0ad21b76165fec9f5 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Wed, 26 Jun 2024 09:00:33 +0200 Subject: [PATCH 1214/1445] Create repair when HA auth provider is running in legacy mode (#119975) --- homeassistant/auth/__init__.py | 8 ++ homeassistant/auth/providers/homeassistant.py | 70 ++++++++------- homeassistant/components/auth/strings.json | 6 ++ tests/auth/providers/test_homeassistant.py | 90 +++++++++++++++++++ 4 files changed, 143 insertions(+), 31 deletions(-) diff --git a/homeassistant/auth/__init__.py b/homeassistant/auth/__init__.py index 8c991d3f227..665bc308d49 100644 --- a/homeassistant/auth/__init__.py +++ b/homeassistant/auth/__init__.py @@ -28,6 +28,7 @@ from .const import ACCESS_TOKEN_EXPIRATION, GROUP_ID_ADMIN, REFRESH_TOKEN_EXPIRA from .mfa_modules import MultiFactorAuthModule, auth_mfa_module_from_config from .models import AuthFlowResult from .providers import AuthProvider, LoginFlow, auth_provider_from_config +from .providers.homeassistant import HassAuthProvider EVENT_USER_ADDED = "user_added" EVENT_USER_UPDATED = "user_updated" @@ -73,6 +74,13 @@ async def auth_manager_from_config( key = (provider.type, provider.id) provider_hash[key] = provider + if isinstance(provider, HassAuthProvider): + # Can be removed in 2026.7 with the legacy mode of homeassistant auth provider + # We need to initialize the provider to create the repair if needed as otherwise + # the provider will be initialized on first use, which could be rare as users + # don't frequently change auth settings + await provider.async_initialize() + if module_configs: modules = await asyncio.gather( *(auth_mfa_module_from_config(hass, config) for config in module_configs) diff --git a/homeassistant/auth/providers/homeassistant.py b/homeassistant/auth/providers/homeassistant.py index 1ed2f1dd3f7..4e38260dd2f 100644 --- a/homeassistant/auth/providers/homeassistant.py +++ b/homeassistant/auth/providers/homeassistant.py @@ -14,6 +14,7 @@ import voluptuous as vol from homeassistant.const import CONF_ID from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import issue_registry as ir from homeassistant.helpers.storage import Store from ..models import AuthFlowResult, Credentials, UserMeta @@ -88,7 +89,7 @@ class Data: self._data: dict[str, list[dict[str, str]]] | None = None # Legacy mode will allow usernames to start/end with whitespace # and will compare usernames case-insensitive. - # Remove in 2020 or when we launch 1.0. + # Deprecated in June 2019 and will be removed in 2026.7 self.is_legacy = False @callback @@ -106,44 +107,49 @@ class Data: if (data := await self._store.async_load()) is None: data = cast(dict[str, list[dict[str, str]]], {"users": []}) - seen: set[str] = set() + self._async_check_for_not_normalized_usernames(data) + self._data = data + + @callback + def _async_check_for_not_normalized_usernames( + self, data: dict[str, list[dict[str, str]]] + ) -> None: + not_normalized_usernames: set[str] = set() for user in data["users"]: username = user["username"] - # check if we have duplicates - if (folded := username.casefold()) in seen: - self.is_legacy = True - + if self.normalize_username(username, force_normalize=True) != username: logging.getLogger(__name__).warning( ( "Home Assistant auth provider is running in legacy mode " - "because we detected usernames that are case-insensitive" - "equivalent. Please change the username: '%s'." + "because we detected usernames that are normalized (lowercase and without spaces)." + " Please change the username: '%s'." ), username, ) + not_normalized_usernames.add(username) - break - - seen.add(folded) - - # check if we have unstripped usernames - if username != username.strip(): - self.is_legacy = True - - logging.getLogger(__name__).warning( - ( - "Home Assistant auth provider is running in legacy mode " - "because we detected usernames that start or end in a " - "space. Please change the username: '%s'." - ), - username, - ) - - break - - self._data = data + if not_normalized_usernames: + self.is_legacy = True + ir.async_create_issue( + self.hass, + "auth", + "homeassistant_provider_not_normalized_usernames", + breaks_in_ha_version="2026.7.0", + is_fixable=False, + severity=ir.IssueSeverity.WARNING, + translation_key="homeassistant_provider_not_normalized_usernames", + translation_placeholders={ + "usernames": f'- "{'"\n- "'.join(sorted(not_normalized_usernames))}"' + }, + learn_more_url="homeassistant://config/users", + ) + else: + self.is_legacy = False + ir.async_delete_issue( + self.hass, "auth", "homeassistant_provider_not_normalized_usernames" + ) @property def users(self) -> list[dict[str, str]]: @@ -228,6 +234,7 @@ class Data: else: raise InvalidUser + @callback def _validate_new_username(self, new_username: str) -> None: """Validate that username is normalized and unique. @@ -251,6 +258,7 @@ class Data: translation_placeholders={"username": new_username}, ) + @callback def change_username(self, username: str, new_username: str) -> None: """Update the username. @@ -263,6 +271,8 @@ class Data: for user in self.users: if self.normalize_username(user["username"]) == username: user["username"] = new_username + assert self._data is not None + self._async_check_for_not_normalized_usernames(self._data) break else: raise InvalidUser @@ -346,9 +356,7 @@ class HassAuthProvider(AuthProvider): await self.async_initialize() assert self.data is not None - await self.hass.async_add_executor_job( - self.data.change_username, credential.data["username"], new_username - ) + self.data.change_username(credential.data["username"], new_username) self.hass.auth.async_update_user_credentials_data( credential, {**credential.data, "username": new_username} ) diff --git a/homeassistant/components/auth/strings.json b/homeassistant/components/auth/strings.json index 2b96b84c1cf..0e4cede78a3 100644 --- a/homeassistant/components/auth/strings.json +++ b/homeassistant/components/auth/strings.json @@ -39,5 +39,11 @@ "username_not_normalized": { "message": "Username \"{new_username}\" is not normalized" } + }, + "issues": { + "homeassistant_provider_not_normalized_usernames": { + "title": "Not normalized usernames detected", + "description": "The Home Assistant auth provider is running in legacy mode because we detected not normalized usernames. The legacy mode is deprecated and will be removed. Please change the following usernames:\n\n{usernames}\n\nNormalized usernames are case folded (lower case) and stripped of whitespaces." + } } } diff --git a/tests/auth/providers/test_homeassistant.py b/tests/auth/providers/test_homeassistant.py index 3224bf6b4f7..dd2ce65b480 100644 --- a/tests/auth/providers/test_homeassistant.py +++ b/tests/auth/providers/test_homeassistant.py @@ -1,6 +1,7 @@ """Test the Home Assistant local auth provider.""" import asyncio +from typing import Any from unittest.mock import Mock, patch import pytest @@ -13,6 +14,7 @@ from homeassistant.auth.providers import ( homeassistant as hass_auth, ) from homeassistant.core import HomeAssistant +from homeassistant.helpers import issue_registry as ir from homeassistant.setup import async_setup_component @@ -389,3 +391,91 @@ async def test_change_username_not_normalized( hass_auth.InvalidUsername, match='Username "TEST-user " is not normalized' ): data.change_username("test-user", "TEST-user ") + + +@pytest.mark.parametrize( + ("usernames_in_storage", "usernames_in_repair"), + [ + (["Uppercase"], '- "Uppercase"'), + ([" leading"], '- " leading"'), + (["trailing "], '- "trailing "'), + (["Test", "test", "Fritz "], '- "Fritz "\n- "Test"'), + ], +) +async def test_create_repair_on_legacy_usernames( + hass: HomeAssistant, + hass_storage: dict[str, Any], + issue_registry: ir.IssueRegistry, + usernames_in_storage: list[str], + usernames_in_repair: str, +) -> None: + """Test that we create a repair issue for legacy usernames.""" + assert not issue_registry.issues.get( + ("auth", "homeassistant_provider_not_normalized_usernames") + ), "Repair issue already exists" + + hass_storage[hass_auth.STORAGE_KEY] = { + "version": 1, + "minor_version": 1, + "key": "auth_provider.homeassistant", + "data": { + "users": [ + { + "username": username, + "password": "onlyherebecauseweneedapasswordstring", + } + for username in usernames_in_storage + ] + }, + } + data = hass_auth.Data(hass) + await data.async_load() + issue = issue_registry.issues.get( + ("auth", "homeassistant_provider_not_normalized_usernames") + ) + assert issue, "Repair issue not created" + assert issue.translation_placeholders == {"usernames": usernames_in_repair} + + +async def test_delete_repair_after_fixing_usernames( + hass: HomeAssistant, + hass_storage: dict[str, Any], + issue_registry: ir.IssueRegistry, +) -> None: + """Test that the repair is deleted after fixing the usernames.""" + hass_storage[hass_auth.STORAGE_KEY] = { + "version": 1, + "minor_version": 1, + "key": "auth_provider.homeassistant", + "data": { + "users": [ + { + "username": "Test", + "password": "onlyherebecauseweneedapasswordstring", + }, + { + "username": "bla ", + "password": "onlyherebecauseweneedapasswordstring", + }, + ] + }, + } + data = hass_auth.Data(hass) + await data.async_load() + issue = issue_registry.issues.get( + ("auth", "homeassistant_provider_not_normalized_usernames") + ) + assert issue, "Repair issue not created" + assert issue.translation_placeholders == {"usernames": '- "Test"\n- "bla "'} + + data.change_username("Test", "test") + issue = issue_registry.issues.get( + ("auth", "homeassistant_provider_not_normalized_usernames") + ) + assert issue + assert issue.translation_placeholders == {"usernames": '- "bla "'} + + data.change_username("bla ", "bla") + assert not issue_registry.issues.get( + ("auth", "homeassistant_provider_not_normalized_usernames") + ), "Repair issue should be deleted" From 8ce53d28e7fc333c9b4f16e0bf43d43b9f9416f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ab=C3=ADlio=20Costa?= Date: Wed, 26 Jun 2024 08:02:49 +0100 Subject: [PATCH 1215/1445] Handle availability in Idasen Desk height sensor (#120277) --- homeassistant/components/idasen_desk/cover.py | 2 +- .../components/idasen_desk/sensor.py | 6 +++++ tests/components/idasen_desk/test_sensors.py | 26 ++++++++++++++++++- 3 files changed, 32 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/idasen_desk/cover.py b/homeassistant/components/idasen_desk/cover.py index f5591eff0d8..eb6bf5523de 100644 --- a/homeassistant/components/idasen_desk/cover.py +++ b/homeassistant/components/idasen_desk/cover.py @@ -66,7 +66,7 @@ class IdasenDeskCover(CoordinatorEntity[IdasenDeskCoordinator], CoverEntity): @property def available(self) -> bool: """Return True if entity is available.""" - return self._desk.is_connected is True + return super().available and self._desk.is_connected is True @property def is_closed(self) -> bool: diff --git a/homeassistant/components/idasen_desk/sensor.py b/homeassistant/components/idasen_desk/sensor.py index 12a3b2ed4d9..8ed85d21a34 100644 --- a/homeassistant/components/idasen_desk/sensor.py +++ b/homeassistant/components/idasen_desk/sensor.py @@ -79,12 +79,18 @@ class IdasenDeskSensor(CoordinatorEntity[IdasenDeskCoordinator], SensorEntity): self._attr_unique_id = f"{description.key}-{address}" self._attr_device_info = device_info self._address = address + self._desk = coordinator.desk async def async_added_to_hass(self) -> None: """When entity is added to hass.""" await super().async_added_to_hass() self._update_native_value() + @property + def available(self) -> bool: + """Return True if entity is available.""" + return super().available and self._desk.is_connected is True + @callback def _handle_coordinator_update(self, *args: Any) -> None: """Handle data update.""" diff --git a/tests/components/idasen_desk/test_sensors.py b/tests/components/idasen_desk/test_sensors.py index a236555a506..614bce523e6 100644 --- a/tests/components/idasen_desk/test_sensors.py +++ b/tests/components/idasen_desk/test_sensors.py @@ -4,10 +4,13 @@ from unittest.mock import MagicMock import pytest +from homeassistant.const import STATE_UNAVAILABLE from homeassistant.core import HomeAssistant from . import init_integration +EXPECTED_INITIAL_HEIGHT = "1" + @pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_height_sensor(hass: HomeAssistant, mock_desk_api: MagicMock) -> None: @@ -17,7 +20,7 @@ async def test_height_sensor(hass: HomeAssistant, mock_desk_api: MagicMock) -> N entity_id = "sensor.test_height" state = hass.states.get(entity_id) assert state - assert state.state == "1" + assert state.state == EXPECTED_INITIAL_HEIGHT mock_desk_api.height = 1.2 mock_desk_api.trigger_update_callback(None) @@ -25,3 +28,24 @@ async def test_height_sensor(hass: HomeAssistant, mock_desk_api: MagicMock) -> N state = hass.states.get(entity_id) assert state assert state.state == "1.2" + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_sensor_available( + hass: HomeAssistant, + mock_desk_api: MagicMock, +) -> None: + """Test sensor available property.""" + await init_integration(hass) + + entity_id = "sensor.test_height" + state = hass.states.get(entity_id) + assert state + assert state.state == EXPECTED_INITIAL_HEIGHT + + mock_desk_api.is_connected = False + mock_desk_api.trigger_update_callback(None) + + state = hass.states.get(entity_id) + assert state + assert state.state == STATE_UNAVAILABLE From d3ceaef09893d54405345eaa350a3d2139ef758c Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Wed, 26 Jun 2024 02:06:56 -0500 Subject: [PATCH 1216/1445] Allow timer management from any device (#120440) --- homeassistant/components/intent/timers.py | 47 ++----- homeassistant/helpers/llm.py | 2 +- .../conversation/test_default_agent.py | 20 ++- tests/components/intent/test_timers.py | 115 ++++++++---------- tests/helpers/test_llm.py | 2 +- 5 files changed, 77 insertions(+), 109 deletions(-) diff --git a/homeassistant/components/intent/timers.py b/homeassistant/components/intent/timers.py index 40b55134e92..82f6121da53 100644 --- a/homeassistant/components/intent/timers.py +++ b/homeassistant/components/intent/timers.py @@ -490,7 +490,7 @@ class FindTimerFilter(StrEnum): def _find_timer( hass: HomeAssistant, - device_id: str, + device_id: str | None, slots: dict[str, Any], find_filter: FindTimerFilter | None = None, ) -> TimerInfo: @@ -577,7 +577,7 @@ def _find_timer( return matching_timers[0] # Use device id - if matching_timers: + if matching_timers and device_id: matching_device_timers = [ t for t in matching_timers if (t.device_id == device_id) ] @@ -626,7 +626,7 @@ def _find_timer( def _find_timers( - hass: HomeAssistant, device_id: str, slots: dict[str, Any] + hass: HomeAssistant, device_id: str | None, slots: dict[str, Any] ) -> list[TimerInfo]: """Match multiple timers with constraints or raise an error.""" timer_manager: TimerManager = hass.data[TIMER_DATA] @@ -689,6 +689,10 @@ def _find_timers( # No matches return matching_timers + if not device_id: + # Can't order using area/floor + return matching_timers + # Use device id to order remaining timers device_registry = dr.async_get(hass) device = device_registry.async_get(device_id) @@ -861,12 +865,6 @@ class CancelTimerIntentHandler(intent.IntentHandler): timer_manager: TimerManager = hass.data[TIMER_DATA] slots = self.async_validate_slots(intent_obj.slots) - if not ( - intent_obj.device_id and timer_manager.is_timer_device(intent_obj.device_id) - ): - # Fail early - raise TimersNotSupportedError(intent_obj.device_id) - timer = _find_timer(hass, intent_obj.device_id, slots) timer_manager.cancel_timer(timer.id) return intent_obj.create_response() @@ -890,12 +888,6 @@ class IncreaseTimerIntentHandler(intent.IntentHandler): timer_manager: TimerManager = hass.data[TIMER_DATA] slots = self.async_validate_slots(intent_obj.slots) - if not ( - intent_obj.device_id and timer_manager.is_timer_device(intent_obj.device_id) - ): - # Fail early - raise TimersNotSupportedError(intent_obj.device_id) - total_seconds = _get_total_seconds(slots) timer = _find_timer(hass, intent_obj.device_id, slots) timer_manager.add_time(timer.id, total_seconds) @@ -920,12 +912,6 @@ class DecreaseTimerIntentHandler(intent.IntentHandler): timer_manager: TimerManager = hass.data[TIMER_DATA] slots = self.async_validate_slots(intent_obj.slots) - if not ( - intent_obj.device_id and timer_manager.is_timer_device(intent_obj.device_id) - ): - # Fail early - raise TimersNotSupportedError(intent_obj.device_id) - total_seconds = _get_total_seconds(slots) timer = _find_timer(hass, intent_obj.device_id, slots) timer_manager.remove_time(timer.id, total_seconds) @@ -949,12 +935,6 @@ class PauseTimerIntentHandler(intent.IntentHandler): timer_manager: TimerManager = hass.data[TIMER_DATA] slots = self.async_validate_slots(intent_obj.slots) - if not ( - intent_obj.device_id and timer_manager.is_timer_device(intent_obj.device_id) - ): - # Fail early - raise TimersNotSupportedError(intent_obj.device_id) - timer = _find_timer( hass, intent_obj.device_id, slots, find_filter=FindTimerFilter.ONLY_ACTIVE ) @@ -979,12 +959,6 @@ class UnpauseTimerIntentHandler(intent.IntentHandler): timer_manager: TimerManager = hass.data[TIMER_DATA] slots = self.async_validate_slots(intent_obj.slots) - if not ( - intent_obj.device_id and timer_manager.is_timer_device(intent_obj.device_id) - ): - # Fail early - raise TimersNotSupportedError(intent_obj.device_id) - timer = _find_timer( hass, intent_obj.device_id, slots, find_filter=FindTimerFilter.ONLY_INACTIVE ) @@ -1006,15 +980,8 @@ class TimerStatusIntentHandler(intent.IntentHandler): async def async_handle(self, intent_obj: intent.Intent) -> intent.IntentResponse: """Handle the intent.""" hass = intent_obj.hass - timer_manager: TimerManager = hass.data[TIMER_DATA] slots = self.async_validate_slots(intent_obj.slots) - if not ( - intent_obj.device_id and timer_manager.is_timer_device(intent_obj.device_id) - ): - # Fail early - raise TimersNotSupportedError(intent_obj.device_id) - statuses: list[dict[str, Any]] = [] for timer in _find_timers(hass, intent_obj.device_id, slots): total_seconds = timer.seconds_left diff --git a/homeassistant/helpers/llm.py b/homeassistant/helpers/llm.py index 4410de67ef2..ba307a785ac 100644 --- a/homeassistant/helpers/llm.py +++ b/homeassistant/helpers/llm.py @@ -355,7 +355,7 @@ class AssistAPI(API): if not llm_context.device_id or not async_device_supports_timers( self.hass, llm_context.device_id ): - prompt.append("This device does not support timers.") + prompt.append("This device is not able to start timers.") if exposed_entities: prompt.append( diff --git a/tests/components/conversation/test_default_agent.py b/tests/components/conversation/test_default_agent.py index dee7b4ca0ff..f8a021475d5 100644 --- a/tests/components/conversation/test_default_agent.py +++ b/tests/components/conversation/test_default_agent.py @@ -860,13 +860,27 @@ async def test_error_feature_not_supported(hass: HomeAssistant) -> None: @pytest.mark.usefixtures("init_components") -async def test_error_no_timer_support(hass: HomeAssistant) -> None: +async def test_error_no_timer_support( + hass: HomeAssistant, + area_registry: ar.AreaRegistry, + device_registry: dr.DeviceRegistry, +) -> None: """Test error message when a device does not support timers (no handler is registered).""" - device_id = "test_device" + area_kitchen = area_registry.async_create("kitchen") + + entry = MockConfigEntry() + entry.add_to_hass(hass) + device_kitchen = device_registry.async_get_or_create( + config_entry_id=entry.entry_id, + connections=set(), + identifiers={("demo", "device-kitchen")}, + ) + device_registry.async_update_device(device_kitchen.id, area_id=area_kitchen.id) + device_id = device_kitchen.id # No timer handler is registered for the device result = await conversation.async_converse( - hass, "pause timer", None, Context(), None, device_id=device_id + hass, "set a 5 minute timer", None, Context(), None, device_id=device_id ) assert result.response.response_type == intent.IntentResponseType.ERROR diff --git a/tests/components/intent/test_timers.py b/tests/components/intent/test_timers.py index 329db6e8b2b..c2efe5d39e2 100644 --- a/tests/components/intent/test_timers.py +++ b/tests/components/intent/test_timers.py @@ -64,6 +64,7 @@ async def test_start_finish_timer(hass: HomeAssistant, init_components) -> None: async_register_timer_handler(hass, device_id, handle_timer) + # A device that has been registered to handle timers is required result = await intent.async_handle( hass, "test", @@ -185,6 +186,27 @@ async def test_cancel_timer(hass: HomeAssistant, init_components) -> None: async with asyncio.timeout(1): await cancelled_event.wait() + # Cancel without a device + timer_name = None + started_event.clear() + result = await intent.async_handle( + hass, + "test", + intent.INTENT_START_TIMER, + { + "hours": {"value": 1}, + "minutes": {"value": 2}, + "seconds": {"value": 3}, + }, + device_id=device_id, + ) + + async with asyncio.timeout(1): + await started_event.wait() + + result = await intent.async_handle(hass, "test", intent.INTENT_CANCEL_TIMER, {}) + assert result.response_type == intent.IntentResponseType.ACTION_DONE + async def test_increase_timer(hass: HomeAssistant, init_components) -> None: """Test increasing the time of a running timer.""" @@ -260,7 +282,6 @@ async def test_increase_timer(hass: HomeAssistant, init_components) -> None: "minutes": {"value": 0}, "seconds": {"value": 0}, }, - device_id=device_id, ) assert result.response_type == intent.IntentResponseType.ACTION_DONE @@ -279,7 +300,6 @@ async def test_increase_timer(hass: HomeAssistant, init_components) -> None: "minutes": {"value": 5}, "seconds": {"value": 30}, }, - device_id=device_id, ) assert result.response_type == intent.IntentResponseType.ACTION_DONE @@ -293,7 +313,6 @@ async def test_increase_timer(hass: HomeAssistant, init_components) -> None: "test", intent.INTENT_CANCEL_TIMER, {"name": {"value": timer_name}}, - device_id=device_id, ) assert result.response_type == intent.IntentResponseType.ACTION_DONE @@ -375,7 +394,6 @@ async def test_decrease_timer(hass: HomeAssistant, init_components) -> None: "start_seconds": {"value": 3}, "seconds": {"value": 30}, }, - device_id=device_id, ) assert result.response_type == intent.IntentResponseType.ACTION_DONE @@ -389,7 +407,6 @@ async def test_decrease_timer(hass: HomeAssistant, init_components) -> None: "test", intent.INTENT_CANCEL_TIMER, {"name": {"value": timer_name}}, - device_id=device_id, ) assert result.response_type == intent.IntentResponseType.ACTION_DONE @@ -467,7 +484,6 @@ async def test_decrease_timer_below_zero(hass: HomeAssistant, init_components) - "start_seconds": {"value": 3}, "seconds": {"value": original_total_seconds + 1}, }, - device_id=device_id, ) assert result.response_type == intent.IntentResponseType.ACTION_DONE @@ -482,43 +498,25 @@ async def test_find_timer_failed(hass: HomeAssistant, init_components) -> None: """Test finding a timer with the wrong info.""" device_id = "test_device" - for intent_name in ( - intent.INTENT_START_TIMER, - intent.INTENT_CANCEL_TIMER, - intent.INTENT_PAUSE_TIMER, - intent.INTENT_UNPAUSE_TIMER, - intent.INTENT_INCREASE_TIMER, - intent.INTENT_DECREASE_TIMER, - intent.INTENT_TIMER_STATUS, - ): - if intent_name in ( + # No device id + with pytest.raises(TimersNotSupportedError): + await intent.async_handle( + hass, + "test", intent.INTENT_START_TIMER, - intent.INTENT_INCREASE_TIMER, - intent.INTENT_DECREASE_TIMER, - ): - slots = {"minutes": {"value": 5}} - else: - slots = {} + {"minutes": {"value": 5}}, + device_id=None, + ) - # No device id - with pytest.raises(TimersNotSupportedError): - await intent.async_handle( - hass, - "test", - intent_name, - slots, - device_id=None, - ) - - # Unregistered device - with pytest.raises(TimersNotSupportedError): - await intent.async_handle( - hass, - "test", - intent_name, - slots, - device_id=device_id, - ) + # Unregistered device + with pytest.raises(TimersNotSupportedError): + await intent.async_handle( + hass, + "test", + intent.INTENT_START_TIMER, + {"minutes": {"value": 5}}, + device_id=device_id, + ) # Must register a handler before we can do anything with timers @callback @@ -543,7 +541,6 @@ async def test_find_timer_failed(hass: HomeAssistant, init_components) -> None: "test", intent.INTENT_INCREASE_TIMER, {"name": {"value": "PIZZA "}, "minutes": {"value": 1}}, - device_id=device_id, ) assert result.response_type == intent.IntentResponseType.ACTION_DONE @@ -554,7 +551,6 @@ async def test_find_timer_failed(hass: HomeAssistant, init_components) -> None: "test", intent.INTENT_CANCEL_TIMER, {"name": {"value": "does-not-exist"}}, - device_id=device_id, ) # Right start time @@ -563,7 +559,6 @@ async def test_find_timer_failed(hass: HomeAssistant, init_components) -> None: "test", intent.INTENT_INCREASE_TIMER, {"start_minutes": {"value": 5}, "minutes": {"value": 1}}, - device_id=device_id, ) assert result.response_type == intent.IntentResponseType.ACTION_DONE @@ -574,7 +569,6 @@ async def test_find_timer_failed(hass: HomeAssistant, init_components) -> None: "test", intent.INTENT_CANCEL_TIMER, {"start_minutes": {"value": 1}}, - device_id=device_id, ) @@ -903,9 +897,7 @@ async def test_pause_unpause_timer(hass: HomeAssistant, init_components) -> None # Pause the timer expected_active = False - result = await intent.async_handle( - hass, "test", intent.INTENT_PAUSE_TIMER, {}, device_id=device_id - ) + result = await intent.async_handle(hass, "test", intent.INTENT_PAUSE_TIMER, {}) assert result.response_type == intent.IntentResponseType.ACTION_DONE async with asyncio.timeout(1): @@ -913,16 +905,12 @@ async def test_pause_unpause_timer(hass: HomeAssistant, init_components) -> None # Pausing again will fail because there are no running timers with pytest.raises(TimerNotFoundError): - await intent.async_handle( - hass, "test", intent.INTENT_PAUSE_TIMER, {}, device_id=device_id - ) + await intent.async_handle(hass, "test", intent.INTENT_PAUSE_TIMER, {}) # Unpause the timer updated_event.clear() expected_active = True - result = await intent.async_handle( - hass, "test", intent.INTENT_UNPAUSE_TIMER, {}, device_id=device_id - ) + result = await intent.async_handle(hass, "test", intent.INTENT_UNPAUSE_TIMER, {}) assert result.response_type == intent.IntentResponseType.ACTION_DONE async with asyncio.timeout(1): @@ -930,9 +918,7 @@ async def test_pause_unpause_timer(hass: HomeAssistant, init_components) -> None # Unpausing again will fail because there are no paused timers with pytest.raises(TimerNotFoundError): - await intent.async_handle( - hass, "test", intent.INTENT_UNPAUSE_TIMER, {}, device_id=device_id - ) + await intent.async_handle(hass, "test", intent.INTENT_UNPAUSE_TIMER, {}) async def test_timer_not_found(hass: HomeAssistant) -> None: @@ -1101,13 +1087,14 @@ async def test_timer_status_with_names(hass: HomeAssistant, init_components) -> await started_event.wait() # No constraints returns all timers - result = await intent.async_handle( - hass, "test", intent.INTENT_TIMER_STATUS, {}, device_id=device_id - ) - assert result.response_type == intent.IntentResponseType.ACTION_DONE - timers = result.speech_slots.get("timers", []) - assert len(timers) == 4 - assert {t.get(ATTR_NAME) for t in timers} == {"pizza", "cookies", "chicken"} + for handle_device_id in (device_id, None): + result = await intent.async_handle( + hass, "test", intent.INTENT_TIMER_STATUS, {}, device_id=handle_device_id + ) + assert result.response_type == intent.IntentResponseType.ACTION_DONE + timers = result.speech_slots.get("timers", []) + assert len(timers) == 4 + assert {t.get(ATTR_NAME) for t in timers} == {"pizza", "cookies", "chicken"} # Get status of cookie timer result = await intent.async_handle( diff --git a/tests/helpers/test_llm.py b/tests/helpers/test_llm.py index 872297b09ec..ad18aa53071 100644 --- a/tests/helpers/test_llm.py +++ b/tests/helpers/test_llm.py @@ -578,7 +578,7 @@ async def test_assist_api_prompt( "(what comes before the dot in its entity id). " "When controlling an area, prefer passing just area name and domain." ) - no_timer_prompt = "This device does not support timers." + no_timer_prompt = "This device is not able to start timers." area_prompt = ( "When a user asks to turn on all devices of a specific type, " From e567f8f3d5d9cdfb485c06794473c0608a76d656 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Wed, 26 Jun 2024 09:14:33 +0200 Subject: [PATCH 1217/1445] Fix issue where an MQTT device is removed linked to two config entries (#120430) * Fix issue where an MQTT device is removed linked to two config entries * Update homeassistant/components/mqtt/discovery.py Co-authored-by: J. Nick Koston * Update homeassistant/components/mqtt/debug_info.py Co-authored-by: J. Nick Koston --------- Co-authored-by: J. Nick Koston --- homeassistant/components/mqtt/debug_info.py | 2 +- homeassistant/components/mqtt/discovery.py | 2 +- homeassistant/components/mqtt/tag.py | 5 +++-- tests/components/mqtt/test_discovery.py | 4 +++- 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/mqtt/debug_info.py b/homeassistant/components/mqtt/debug_info.py index a8fd318b1e9..2985e6d7707 100644 --- a/homeassistant/components/mqtt/debug_info.py +++ b/homeassistant/components/mqtt/debug_info.py @@ -138,7 +138,7 @@ def remove_trigger_discovery_data( hass: HomeAssistant, discovery_hash: tuple[str, str] ) -> None: """Remove discovery data.""" - del hass.data[DATA_MQTT].debug_info_triggers[discovery_hash] + hass.data[DATA_MQTT].debug_info_triggers.pop(discovery_hash, None) def _info_for_entity(hass: HomeAssistant, entity_id: str) -> dict[str, Any]: diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py index 0d93af26a57..cf2941a3665 100644 --- a/homeassistant/components/mqtt/discovery.py +++ b/homeassistant/components/mqtt/discovery.py @@ -73,7 +73,7 @@ class MQTTDiscoveryPayload(dict[str, Any]): def clear_discovery_hash(hass: HomeAssistant, discovery_hash: tuple[str, str]) -> None: """Clear entry from already discovered list.""" - hass.data[DATA_MQTT].discovery_already_discovered.remove(discovery_hash) + hass.data[DATA_MQTT].discovery_already_discovered.discard(discovery_hash) def set_discovery_hash(hass: HomeAssistant, discovery_hash: tuple[str, str]) -> None: diff --git a/homeassistant/components/mqtt/tag.py b/homeassistant/components/mqtt/tag.py index 22263a07499..fbb0ea813c2 100644 --- a/homeassistant/components/mqtt/tag.py +++ b/homeassistant/components/mqtt/tag.py @@ -180,5 +180,6 @@ class MQTTTagScanner(MqttDiscoveryDeviceUpdateMixin): self._sub_state = subscription.async_unsubscribe_topics( self.hass, self._sub_state ) - if self.device_id: - del self.hass.data[DATA_MQTT].tags[self.device_id][discovery_id] + tags = self.hass.data[DATA_MQTT].tags + if self.device_id in tags and discovery_id in tags[self.device_id]: + del tags[self.device_id][discovery_id] diff --git a/tests/components/mqtt/test_discovery.py b/tests/components/mqtt/test_discovery.py index fbf878a040a..23dea310199 100644 --- a/tests/components/mqtt/test_discovery.py +++ b/tests/components/mqtt/test_discovery.py @@ -809,7 +809,7 @@ async def test_duplicate_removal( assert "Component has already been discovered: binary_sensor bla" not in caplog.text -async def test_cleanup_device( +async def test_cleanup_device_manual( hass: HomeAssistant, hass_ws_client: WebSocketGenerator, device_registry: dr.DeviceRegistry, @@ -1012,6 +1012,7 @@ async def test_cleanup_device_multiple_config_entries( async def test_cleanup_device_multiple_config_entries_mqtt( hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, mqtt_mock_entry: MqttMockHAClientGenerator, @@ -1093,6 +1094,7 @@ async def test_cleanup_device_multiple_config_entries_mqtt( # Verify retained discovery topics have not been cleared again mqtt_mock.async_publish.assert_not_called() + assert "KeyError:" not in caplog.text async def test_discovery_expansion( From 30e0bcb3247fe46eb4973d7008d4ea9dbed12950 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 26 Jun 2024 09:20:43 +0200 Subject: [PATCH 1218/1445] Bump dbus-fast to 2.22.1 (#120491) --- homeassistant/components/bluetooth/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/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index df2278399ab..12bb37ac570 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -19,7 +19,7 @@ "bluetooth-adapters==0.19.3", "bluetooth-auto-recovery==1.4.2", "bluetooth-data-tools==1.19.3", - "dbus-fast==2.21.3", + "dbus-fast==2.22.1", "habluetooth==3.1.3" ] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 25d10874239..d3320e64fe3 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -24,7 +24,7 @@ cached_ipaddress==0.3.0 certifi>=2021.5.30 ciso8601==2.3.1 cryptography==42.0.8 -dbus-fast==2.21.3 +dbus-fast==2.22.1 fnv-hash-fast==0.5.0 ha-av==10.1.1 ha-ffmpeg==3.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 82ca566a925..3548be4ad60 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -697,7 +697,7 @@ datadog==0.15.0 datapoint==0.9.9 # homeassistant.components.bluetooth -dbus-fast==2.21.3 +dbus-fast==2.22.1 # homeassistant.components.debugpy debugpy==1.8.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b0d39fdd0bc..e1fb561ecee 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -584,7 +584,7 @@ datadog==0.15.0 datapoint==0.9.9 # homeassistant.components.bluetooth -dbus-fast==2.21.3 +dbus-fast==2.22.1 # homeassistant.components.debugpy debugpy==1.8.1 From bff9d12cc087bcadc82b892719d0d61a953013c7 Mon Sep 17 00:00:00 2001 From: Thomas Kistler Date: Wed, 26 Jun 2024 00:24:48 -0700 Subject: [PATCH 1219/1445] Add active watering time sensor to Hydrawise (#120177) --- .../components/hydrawise/coordinator.py | 22 +-- homeassistant/components/hydrawise/icons.json | 3 + homeassistant/components/hydrawise/sensor.py | 92 +++++++--- .../components/hydrawise/strings.json | 5 +- tests/components/hydrawise/conftest.py | 2 + .../hydrawise/snapshots/test_sensor.ambr | 171 ++++++++++++++++-- tests/components/hydrawise/test_sensor.py | 19 +- 7 files changed, 263 insertions(+), 51 deletions(-) diff --git a/homeassistant/components/hydrawise/coordinator.py b/homeassistant/components/hydrawise/coordinator.py index 50caaa0c0de..6cd233eb1df 100644 --- a/homeassistant/components/hydrawise/coordinator.py +++ b/homeassistant/components/hydrawise/coordinator.py @@ -23,7 +23,7 @@ class HydrawiseData: controllers: dict[int, Controller] zones: dict[int, Zone] sensors: dict[int, Sensor] - daily_water_use: dict[int, ControllerWaterUseSummary] + daily_water_summary: dict[int, ControllerWaterUseSummary] class HydrawiseDataUpdateCoordinator(DataUpdateCoordinator[HydrawiseData]): @@ -47,7 +47,7 @@ class HydrawiseDataUpdateCoordinator(DataUpdateCoordinator[HydrawiseData]): controllers = {} zones = {} sensors = {} - daily_water_use: dict[int, ControllerWaterUseSummary] = {} + daily_water_summary: dict[int, ControllerWaterUseSummary] = {} for controller in user.controllers: controllers[controller.id] = controller controller.zones = await self.api.get_zones(controller) @@ -55,22 +55,16 @@ class HydrawiseDataUpdateCoordinator(DataUpdateCoordinator[HydrawiseData]): zones[zone.id] = zone for sensor in controller.sensors: sensors[sensor.id] = sensor - if any( - "flow meter" in sensor.model.name.lower() - for sensor in controller.sensors - ): - daily_water_use[controller.id] = await self.api.get_water_use_summary( - controller, - now().replace(hour=0, minute=0, second=0, microsecond=0), - now(), - ) - else: - daily_water_use[controller.id] = ControllerWaterUseSummary() + daily_water_summary[controller.id] = await self.api.get_water_use_summary( + controller, + now().replace(hour=0, minute=0, second=0, microsecond=0), + now(), + ) return HydrawiseData( user=user, controllers=controllers, zones=zones, sensors=sensors, - daily_water_use=daily_water_use, + daily_water_summary=daily_water_summary, ) diff --git a/homeassistant/components/hydrawise/icons.json b/homeassistant/components/hydrawise/icons.json index 64deab590da..4af4fe75fcc 100644 --- a/homeassistant/components/hydrawise/icons.json +++ b/homeassistant/components/hydrawise/icons.json @@ -4,6 +4,9 @@ "daily_active_water_use": { "default": "mdi:water" }, + "daily_active_water_time": { + "default": "mdi:timelapse" + }, "daily_inactive_water_use": { "default": "mdi:water" }, diff --git a/homeassistant/components/hydrawise/sensor.py b/homeassistant/components/hydrawise/sensor.py index fe4b33d5851..563af893700 100644 --- a/homeassistant/components/hydrawise/sensor.py +++ b/homeassistant/components/hydrawise/sensor.py @@ -4,7 +4,7 @@ from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass -from datetime import datetime +from datetime import datetime, timedelta from typing import Any from homeassistant.components.sensor import ( @@ -44,28 +44,65 @@ def _get_zone_next_cycle(sensor: HydrawiseSensor) -> datetime | None: def _get_zone_daily_active_water_use(sensor: HydrawiseSensor) -> float: """Get active water use for the zone.""" - daily_water_summary = sensor.coordinator.data.daily_water_use[sensor.controller.id] + daily_water_summary = sensor.coordinator.data.daily_water_summary[ + sensor.controller.id + ] return float(daily_water_summary.active_use_by_zone_id.get(sensor.zone.id, 0.0)) +def _get_zone_daily_active_water_time(sensor: HydrawiseSensor) -> float | None: + """Get active water time for the zone.""" + daily_water_summary = sensor.coordinator.data.daily_water_summary[ + sensor.controller.id + ] + return daily_water_summary.active_time_by_zone_id.get( + sensor.zone.id, timedelta() + ).total_seconds() + + def _get_controller_daily_active_water_use(sensor: HydrawiseSensor) -> float | None: """Get active water use for the controller.""" - daily_water_summary = sensor.coordinator.data.daily_water_use[sensor.controller.id] + daily_water_summary = sensor.coordinator.data.daily_water_summary[ + sensor.controller.id + ] return daily_water_summary.total_active_use def _get_controller_daily_inactive_water_use(sensor: HydrawiseSensor) -> float | None: """Get inactive water use for the controller.""" - daily_water_summary = sensor.coordinator.data.daily_water_use[sensor.controller.id] + daily_water_summary = sensor.coordinator.data.daily_water_summary[ + sensor.controller.id + ] return daily_water_summary.total_inactive_use +def _get_controller_daily_active_water_time(sensor: HydrawiseSensor) -> float: + """Get active water time for the controller.""" + daily_water_summary = sensor.coordinator.data.daily_water_summary[ + sensor.controller.id + ] + return daily_water_summary.total_active_time.total_seconds() + + def _get_controller_daily_total_water_use(sensor: HydrawiseSensor) -> float | None: """Get inactive water use for the controller.""" - daily_water_summary = sensor.coordinator.data.daily_water_use[sensor.controller.id] + daily_water_summary = sensor.coordinator.data.daily_water_summary[ + sensor.controller.id + ] return daily_water_summary.total_use +CONTROLLER_SENSORS: tuple[HydrawiseSensorEntityDescription, ...] = ( + HydrawiseSensorEntityDescription( + key="daily_active_water_time", + translation_key="daily_active_water_time", + device_class=SensorDeviceClass.DURATION, + native_unit_of_measurement=UnitOfTime.SECONDS, + value_fn=_get_controller_daily_active_water_time, + ), +) + + FLOW_CONTROLLER_SENSORS: tuple[HydrawiseSensorEntityDescription, ...] = ( HydrawiseSensorEntityDescription( key="daily_total_water_use", @@ -113,6 +150,13 @@ ZONE_SENSORS: tuple[HydrawiseSensorEntityDescription, ...] = ( native_unit_of_measurement=UnitOfTime.MINUTES, value_fn=_get_zone_watering_time, ), + HydrawiseSensorEntityDescription( + key="daily_active_water_time", + translation_key="daily_active_water_time", + device_class=SensorDeviceClass.DURATION, + native_unit_of_measurement=UnitOfTime.SECONDS, + value_fn=_get_zone_daily_active_water_time, + ), ) FLOW_MEASUREMENT_KEYS = [x.key for x in FLOW_CONTROLLER_SENSORS] @@ -129,30 +173,31 @@ async def async_setup_entry( ] entities: list[HydrawiseSensor] = [] for controller in coordinator.data.controllers.values(): + entities.extend( + HydrawiseSensor(coordinator, description, controller) + for description in CONTROLLER_SENSORS + ) entities.extend( HydrawiseSensor(coordinator, description, controller, zone_id=zone.id) for zone in controller.zones for description in ZONE_SENSORS ) - entities.extend( - HydrawiseSensor(coordinator, description, controller, sensor_id=sensor.id) - for sensor in controller.sensors - for description in FLOW_CONTROLLER_SENSORS - if "flow meter" in sensor.model.name.lower() - ) - entities.extend( - HydrawiseSensor( - coordinator, - description, - controller, - zone_id=zone.id, - sensor_id=sensor.id, + if coordinator.data.daily_water_summary[controller.id].total_use is not None: + # we have a flow sensor for this controller + entities.extend( + HydrawiseSensor(coordinator, description, controller) + for description in FLOW_CONTROLLER_SENSORS + ) + entities.extend( + HydrawiseSensor( + coordinator, + description, + controller, + zone_id=zone.id, + ) + for zone in controller.zones + for description in FLOW_ZONE_SENSORS ) - for zone in controller.zones - for sensor in controller.sensors - for description in FLOW_ZONE_SENSORS - if "flow meter" in sensor.model.name.lower() - ) async_add_entities(entities) @@ -177,6 +222,7 @@ class HydrawiseSensor(HydrawiseEntity, SensorEntity): """Icon of the entity based on the value.""" if ( self.entity_description.key in FLOW_MEASUREMENT_KEYS + and self.entity_description.device_class == SensorDeviceClass.VOLUME and round(self.state, 2) == 0.0 ): return "mdi:water-outline" diff --git a/homeassistant/components/hydrawise/strings.json b/homeassistant/components/hydrawise/strings.json index 1bc5525c9d9..c455412d1a4 100644 --- a/homeassistant/components/hydrawise/strings.json +++ b/homeassistant/components/hydrawise/strings.json @@ -33,6 +33,9 @@ "daily_total_water_use": { "name": "Daily total water use" }, + "daily_active_water_time": { + "name": "Daily active watering time" + }, "daily_active_water_use": { "name": "Daily active water use" }, @@ -43,7 +46,7 @@ "name": "Next cycle" }, "watering_time": { - "name": "Watering time" + "name": "Remaining watering time" } }, "switch": { diff --git a/tests/components/hydrawise/conftest.py b/tests/components/hydrawise/conftest.py index 0b5327cd7b2..a938322414b 100644 --- a/tests/components/hydrawise/conftest.py +++ b/tests/components/hydrawise/conftest.py @@ -187,6 +187,8 @@ def controller_water_use_summary() -> ControllerWaterUseSummary: total_active_use=332.6, total_inactive_use=13.0, active_use_by_zone_id={5965394: 120.1, 5965395: 0.0}, + total_active_time=timedelta(seconds=123), + active_time_by_zone_id={5965394: timedelta(seconds=123), 5965395: timedelta()}, unit="gal", ) diff --git a/tests/components/hydrawise/snapshots/test_sensor.ambr b/tests/components/hydrawise/snapshots/test_sensor.ambr index 3472de98460..dadf3c44789 100644 --- a/tests/components/hydrawise/snapshots/test_sensor.ambr +++ b/tests/components/hydrawise/snapshots/test_sensor.ambr @@ -54,6 +54,55 @@ 'state': '1259.0279593584', }) # --- +# name: test_all_sensors[sensor.home_controller_daily_active_watering_time-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.home_controller_daily_active_watering_time', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Daily active watering time', + 'platform': 'hydrawise', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'daily_active_water_time', + 'unique_id': '52496_daily_active_water_time', + 'unit_of_measurement': , + }) +# --- +# name: test_all_sensors[sensor.home_controller_daily_active_watering_time-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by hydrawise.com', + 'device_class': 'duration', + 'friendly_name': 'Home Controller Daily active watering time', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_controller_daily_active_watering_time', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '123.0', + }) +# --- # name: test_all_sensors[sensor.home_controller_daily_inactive_water_use-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -219,6 +268,55 @@ 'state': '454.6279552584', }) # --- +# name: test_all_sensors[sensor.zone_one_daily_active_watering_time-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.zone_one_daily_active_watering_time', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Daily active watering time', + 'platform': 'hydrawise', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'daily_active_water_time', + 'unique_id': '5965394_daily_active_water_time', + 'unit_of_measurement': , + }) +# --- +# name: test_all_sensors[sensor.zone_one_daily_active_watering_time-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by hydrawise.com', + 'device_class': 'duration', + 'friendly_name': 'Zone One Daily active watering time', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.zone_one_daily_active_watering_time', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '123.0', + }) +# --- # name: test_all_sensors[sensor.zone_one_next_cycle-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -267,7 +365,7 @@ 'state': '2023-10-04T19:49:57+00:00', }) # --- -# name: test_all_sensors[sensor.zone_one_watering_time-entry] +# name: test_all_sensors[sensor.zone_one_remaining_watering_time-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -279,7 +377,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.zone_one_watering_time', + 'entity_id': 'sensor.zone_one_remaining_watering_time', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -291,7 +389,7 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Watering time', + 'original_name': 'Remaining watering time', 'platform': 'hydrawise', 'previous_unique_id': None, 'supported_features': 0, @@ -300,15 +398,15 @@ 'unit_of_measurement': , }) # --- -# name: test_all_sensors[sensor.zone_one_watering_time-state] +# name: test_all_sensors[sensor.zone_one_remaining_watering_time-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by hydrawise.com', - 'friendly_name': 'Zone One Watering time', + 'friendly_name': 'Zone One Remaining watering time', 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.zone_one_watering_time', + 'entity_id': 'sensor.zone_one_remaining_watering_time', 'last_changed': , 'last_reported': , 'last_updated': , @@ -371,6 +469,55 @@ 'state': '0.0', }) # --- +# name: test_all_sensors[sensor.zone_two_daily_active_watering_time-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.zone_two_daily_active_watering_time', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Daily active watering time', + 'platform': 'hydrawise', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'daily_active_water_time', + 'unique_id': '5965395_daily_active_water_time', + 'unit_of_measurement': , + }) +# --- +# name: test_all_sensors[sensor.zone_two_daily_active_watering_time-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by hydrawise.com', + 'device_class': 'duration', + 'friendly_name': 'Zone Two Daily active watering time', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.zone_two_daily_active_watering_time', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- # name: test_all_sensors[sensor.zone_two_next_cycle-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -419,7 +566,7 @@ 'state': 'unknown', }) # --- -# name: test_all_sensors[sensor.zone_two_watering_time-entry] +# name: test_all_sensors[sensor.zone_two_remaining_watering_time-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -431,7 +578,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.zone_two_watering_time', + 'entity_id': 'sensor.zone_two_remaining_watering_time', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -443,7 +590,7 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Watering time', + 'original_name': 'Remaining watering time', 'platform': 'hydrawise', 'previous_unique_id': None, 'supported_features': 0, @@ -452,15 +599,15 @@ 'unit_of_measurement': , }) # --- -# name: test_all_sensors[sensor.zone_two_watering_time-state] +# name: test_all_sensors[sensor.zone_two_remaining_watering_time-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by hydrawise.com', - 'friendly_name': 'Zone Two Watering time', + 'friendly_name': 'Zone Two Remaining watering time', 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.zone_two_watering_time', + 'entity_id': 'sensor.zone_two_remaining_watering_time', 'last_changed': , 'last_reported': , 'last_updated': , diff --git a/tests/components/hydrawise/test_sensor.py b/tests/components/hydrawise/test_sensor.py index af75ad69ade..b9ff99f0013 100644 --- a/tests/components/hydrawise/test_sensor.py +++ b/tests/components/hydrawise/test_sensor.py @@ -3,7 +3,7 @@ from collections.abc import Awaitable, Callable from unittest.mock import patch -from pydrawise.schema import Controller, User, Zone +from pydrawise.schema import Controller, ControllerWaterUseSummary, User, Zone import pytest from syrupy.assertion import SnapshotAssertion @@ -53,10 +53,15 @@ async def test_suspended_state( async def test_no_sensor_and_water_state( hass: HomeAssistant, controller: Controller, + controller_water_use_summary: ControllerWaterUseSummary, mock_add_config_entry: Callable[[], Awaitable[MockConfigEntry]], ) -> None: """Test rain sensor, flow sensor, and water use in the absence of flow and rain sensors.""" controller.sensors = [] + controller_water_use_summary.total_use = None + controller_water_use_summary.total_active_use = None + controller_water_use_summary.total_inactive_use = None + controller_water_use_summary.active_use_by_zone_id = {} await mock_add_config_entry() assert hass.states.get("sensor.zone_one_daily_active_water_use") is None @@ -65,6 +70,18 @@ async def test_no_sensor_and_water_state( assert hass.states.get("sensor.home_controller_daily_inactive_water_use") is None assert hass.states.get("binary_sensor.home_controller_rain_sensor") is None + sensor = hass.states.get("sensor.home_controller_daily_active_watering_time") + assert sensor is not None + assert sensor.state == "123.0" + + sensor = hass.states.get("sensor.zone_one_daily_active_watering_time") + assert sensor is not None + assert sensor.state == "123.0" + + sensor = hass.states.get("sensor.zone_two_daily_active_watering_time") + assert sensor is not None + assert sensor.state == "0.0" + sensor = hass.states.get("binary_sensor.home_controller_connectivity") assert sensor is not None assert sensor.state == "on" From 5a0841155ef6ce076f4f30d93c58cb4a2e3dcf38 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 26 Jun 2024 09:28:11 +0200 Subject: [PATCH 1220/1445] Add unique_id to MPD (#120495) --- homeassistant/components/mpd/media_player.py | 85 +++++++------------- 1 file changed, 30 insertions(+), 55 deletions(-) diff --git a/homeassistant/components/mpd/media_player.py b/homeassistant/components/mpd/media_player.py index f0df2cdbbe2..204bbc7f499 100644 --- a/homeassistant/components/mpd/media_player.py +++ b/homeassistant/components/mpd/media_player.py @@ -31,6 +31,7 @@ from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_PORT from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant from homeassistant.data_entry_flow import FlowResultType import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -129,7 +130,7 @@ async def async_setup_entry( entry.data[CONF_HOST], entry.data[CONF_PORT], entry.data.get(CONF_PASSWORD), - entry.title, + entry.entry_id, ) ], True, @@ -140,23 +141,26 @@ class MpdDevice(MediaPlayerEntity): """Representation of a MPD server.""" _attr_media_content_type = MediaType.MUSIC + _attr_has_entity_name = True + _attr_name = None - def __init__(self, server, port, password, name): + def __init__( + self, server: str, port: int, password: str | None, unique_id: str + ) -> None: """Initialize the MPD device.""" self.server = server self.port = port - self._name = name + self._attr_unique_id = unique_id + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, unique_id)}, + entry_type=DeviceEntryType.SERVICE, + ) self.password = password - self._status = {} + self._status: dict[str, Any] = {} self._currentsong = None - self._playlists = None - self._currentplaylist = None - self._is_available = None - self._muted = False + self._current_playlist: str | None = None self._muted_volume = None - self._media_position_updated_at = None - self._media_position = None self._media_image_hash = None # Track if the song changed so image doesn't have to be loaded every update. self._media_image_file = None @@ -188,7 +192,7 @@ class MpdDevice(MediaPlayerEntity): raise TimeoutError("Connection attempt timed out") from error if self.password is not None: await self._client.password(self.password) - self._is_available = True + self._attr_available = True yield except ( TimeoutError, @@ -199,12 +203,12 @@ class MpdDevice(MediaPlayerEntity): # Log a warning during startup or when previously connected; for # subsequent errors a debug message is sufficient. log_level = logging.DEBUG - if self._is_available is not False: + if self._attr_available is not False: log_level = logging.WARNING LOGGER.log( log_level, "Error connecting to '%s': %s", self.server, error ) - self._is_available = False + self._attr_available = False self._status = {} # Also yield on failure. Handling mpd.ConnectionErrors caused by # attempting to control a disconnected client is the @@ -228,24 +232,14 @@ class MpdDevice(MediaPlayerEntity): if isinstance(position, str) and ":" in position: position = position.split(":")[0] - if position is not None and self._media_position != position: - self._media_position_updated_at = dt_util.utcnow() - self._media_position = int(float(position)) + if position is not None and self._attr_media_position != position: + self._attr_media_position_updated_at = dt_util.utcnow() + self._attr_media_position = int(float(position)) await self._update_playlists() except (mpd.ConnectionError, ValueError) as error: LOGGER.debug("Error updating status: %s", error) - @property - def available(self) -> bool: - """Return true if MPD is available and connected.""" - return self._is_available is True - - @property - def name(self): - """Return the name of the device.""" - return self._name - @property def state(self) -> MediaPlayerState: """Return the media state.""" @@ -260,11 +254,6 @@ class MpdDevice(MediaPlayerEntity): return MediaPlayerState.OFF - @property - def is_volume_muted(self): - """Boolean if volume is currently muted.""" - return self._muted - @property def media_content_id(self): """Return the content ID of current playing media.""" @@ -282,20 +271,6 @@ class MpdDevice(MediaPlayerEntity): return None - @property - def media_position(self): - """Position of current playing media in seconds. - - This is returned as part of the mpd status rather than in the details - of the current song. - """ - return self._media_position - - @property - def media_position_updated_at(self): - """Last valid time of media position.""" - return self._media_position_updated_at - @property def media_title(self): """Return the title of current playing media.""" @@ -436,7 +411,7 @@ class MpdDevice(MediaPlayerEntity): | MediaPlayerEntityFeature.VOLUME_STEP | MediaPlayerEntityFeature.VOLUME_MUTE ) - if self._playlists is not None: + if self._attr_source_list is not None: supported |= MediaPlayerEntityFeature.SELECT_SOURCE return supported @@ -444,7 +419,7 @@ class MpdDevice(MediaPlayerEntity): @property def source(self): """Name of the current input source.""" - return self._currentplaylist + return self._current_playlist @property def source_list(self): @@ -459,12 +434,12 @@ class MpdDevice(MediaPlayerEntity): async def _update_playlists(self, **kwargs: Any) -> None: """Update available MPD playlists.""" try: - self._playlists = [] + self._attr_source_list = [] with suppress(mpd.ConnectionError): for playlist_data in await self._client.listplaylists(): - self._playlists.append(playlist_data["playlist"]) + self._attr_source_list.append(playlist_data["playlist"]) except mpd.CommandError as error: - self._playlists = None + self._attr_source_list = None LOGGER.warning("Playlists could not be updated: %s:", error) async def async_set_volume_level(self, volume: float) -> None: @@ -527,7 +502,7 @@ class MpdDevice(MediaPlayerEntity): await self.async_set_volume_level(0) elif self._muted_volume is not None: await self.async_set_volume_level(self._muted_volume) - self._muted = mute + self._attr_is_volume_muted = mute async def async_play_media( self, media_type: MediaType | str, media_id: str, **kwargs: Any @@ -543,17 +518,17 @@ class MpdDevice(MediaPlayerEntity): if media_type == MediaType.PLAYLIST: LOGGER.debug("Playing playlist: %s", media_id) - if media_id in self._playlists: - self._currentplaylist = media_id + if self._attr_source_list and media_id in self._attr_source_list: + self._current_playlist = media_id else: - self._currentplaylist = None + self._current_playlist = None LOGGER.warning("Unknown playlist name %s", media_id) await self._client.clear() await self._client.load(media_id) await self._client.play() else: await self._client.clear() - self._currentplaylist = None + self._current_playlist = None await self._client.add(media_id) await self._client.play() From c5b7d2d86855a6b5e0f49323f2ccb839d4a2657a Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Wed, 26 Jun 2024 09:29:40 +0200 Subject: [PATCH 1221/1445] Cleanup mqtt platform tests part 3 (#120493) --- tests/components/mqtt/test_fan.py | 31 ++++++++--------------- tests/components/mqtt/test_humidifier.py | 32 +++++++++--------------- tests/components/mqtt/test_image.py | 17 +++---------- 3 files changed, 26 insertions(+), 54 deletions(-) diff --git a/tests/components/mqtt/test_fan.py b/tests/components/mqtt/test_fan.py index 80e45c87789..2d1d717c58f 100644 --- a/tests/components/mqtt/test_fan.py +++ b/tests/components/mqtt/test_fan.py @@ -83,10 +83,9 @@ DEFAULT_CONFIG = { @pytest.mark.parametrize("hass_config", [{mqtt.DOMAIN: {fan.DOMAIN: {"name": "test"}}}]) +@pytest.mark.usefixtures("hass") async def test_fail_setup_if_no_command_topic( - hass: HomeAssistant, - caplog: pytest.LogCaptureFixture, - mqtt_mock_entry: MqttMockHAClientGenerator, + caplog: pytest.LogCaptureFixture, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test if command fails with command topic.""" assert await mqtt_mock_entry() @@ -611,8 +610,7 @@ async def test_controlling_state_via_topic_and_json_message_shared_topic( ], ) async def test_sending_mqtt_commands_and_optimistic( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test optimistic mode without state topic.""" mqtt_mock = await mqtt_mock_entry() @@ -861,9 +859,7 @@ async def test_sending_mqtt_commands_with_alternate_speed_range( ], ) async def test_sending_mqtt_commands_and_optimistic_no_legacy( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test optimistic mode without state topic without legacy speed command topic.""" mqtt_mock = await mqtt_mock_entry() @@ -1005,8 +1001,7 @@ async def test_sending_mqtt_commands_and_optimistic_no_legacy( ], ) async def test_sending_mqtt_command_templates_( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test optimistic mode without state topic without legacy speed command topic.""" mqtt_mock = await mqtt_mock_entry() @@ -1166,8 +1161,7 @@ async def test_sending_mqtt_command_templates_( ], ) async def test_sending_mqtt_commands_and_optimistic_no_percentage_topic( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test optimistic mode without state topic without percentage command topic.""" mqtt_mock = await mqtt_mock_entry() @@ -1237,8 +1231,7 @@ async def test_sending_mqtt_commands_and_optimistic_no_percentage_topic( ], ) async def test_sending_mqtt_commands_and_explicit_optimistic( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test optimistic mode with state topic and turn on attributes.""" mqtt_mock = await mqtt_mock_entry() @@ -1533,9 +1526,7 @@ async def test_encoding_subscribable_topics( ], ) async def test_attributes( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test attributes.""" await mqtt_mock_entry() @@ -2215,8 +2206,7 @@ async def test_publishing_with_custom_encoding( async def test_reloadable( - hass: HomeAssistant, - mqtt_client_mock: MqttMockPahoClient, + hass: HomeAssistant, mqtt_client_mock: MqttMockPahoClient ) -> None: """Test reloading the MQTT platform.""" domain = fan.DOMAIN @@ -2239,8 +2229,7 @@ async def test_setup_manual_entity_from_yaml( async def test_unload_entry( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test unloading the config entry.""" domain = fan.DOMAIN diff --git a/tests/components/mqtt/test_humidifier.py b/tests/components/mqtt/test_humidifier.py index b583412b4ff..05180c17b2f 100644 --- a/tests/components/mqtt/test_humidifier.py +++ b/tests/components/mqtt/test_humidifier.py @@ -83,17 +83,16 @@ DEFAULT_CONFIG = { } -async def async_turn_on( - hass: HomeAssistant, - entity_id=ENTITY_MATCH_ALL, -) -> None: +async def async_turn_on(hass: HomeAssistant, entity_id: str = ENTITY_MATCH_ALL) -> None: """Turn all or specified humidifier on.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} await hass.services.async_call(DOMAIN, SERVICE_TURN_ON, data, blocking=True) -async def async_turn_off(hass: HomeAssistant, entity_id=ENTITY_MATCH_ALL) -> None: +async def async_turn_off( + hass: HomeAssistant, entity_id: str = ENTITY_MATCH_ALL +) -> None: """Turn all or specified humidier off.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} @@ -101,7 +100,7 @@ async def async_turn_off(hass: HomeAssistant, entity_id=ENTITY_MATCH_ALL) -> Non async def async_set_mode( - hass: HomeAssistant, entity_id=ENTITY_MATCH_ALL, mode: str | None = None + hass: HomeAssistant, entity_id: str = ENTITY_MATCH_ALL, mode: str | None = None ) -> None: """Set mode for all or specified humidifier.""" data = { @@ -114,7 +113,7 @@ async def async_set_mode( async def async_set_humidity( - hass: HomeAssistant, entity_id=ENTITY_MATCH_ALL, humidity: int | None = None + hass: HomeAssistant, entity_id: str = ENTITY_MATCH_ALL, humidity: int | None = None ) -> None: """Set target humidity for all or specified humidifier.""" data = { @@ -129,10 +128,9 @@ async def async_set_humidity( @pytest.mark.parametrize( "hass_config", [{mqtt.DOMAIN: {humidifier.DOMAIN: {"name": "test"}}}] ) +@pytest.mark.usefixtures("hass") async def test_fail_setup_if_no_command_topic( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + mqtt_mock_entry: MqttMockHAClientGenerator, caplog: pytest.LogCaptureFixture ) -> None: """Test if command fails with command topic.""" assert await mqtt_mock_entry() @@ -892,9 +890,7 @@ async def test_encoding_subscribable_topics( ], ) async def test_attributes( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test attributes.""" await mqtt_mock_entry() @@ -1048,9 +1044,7 @@ async def test_attributes( ], ) async def test_validity_configurations( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - valid: bool, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, valid: bool ) -> None: """Test validity of configurations.""" await mqtt_mock_entry() @@ -1499,8 +1493,7 @@ async def test_publishing_with_custom_encoding( async def test_reloadable( - hass: HomeAssistant, - mqtt_client_mock: MqttMockPahoClient, + hass: HomeAssistant, mqtt_client_mock: MqttMockPahoClient ) -> None: """Test reloading the MQTT platform.""" domain = humidifier.DOMAIN @@ -1523,8 +1516,7 @@ async def test_setup_manual_entity_from_yaml( async def test_unload_config_entry( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test unloading the config entry.""" domain = humidifier.DOMAIN diff --git a/tests/components/mqtt/test_image.py b/tests/components/mqtt/test_image.py index a299474c0ac..29109ee12f4 100644 --- a/tests/components/mqtt/test_image.py +++ b/tests/components/mqtt/test_image.py @@ -356,7 +356,6 @@ async def test_image_from_url_content_type( hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator, mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, content_type: str, setup_ok: bool, ) -> None: @@ -425,7 +424,6 @@ async def test_image_from_url_fails( hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator, mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, side_effect: Exception, ) -> None: """Test setup with minimum configuration.""" @@ -501,9 +499,8 @@ async def test_image_from_url_fails( ), ], ) +@pytest.mark.usesfixtures("hass", "hass_client_no_auth") async def test_image_config_fails( - hass: HomeAssistant, - hass_client_no_auth: ClientSessionGenerator, mqtt_mock_entry: MqttMockHAClientGenerator, caplog: pytest.LogCaptureFixture, error_msg: str, @@ -721,11 +718,7 @@ async def test_entity_id_update_subscriptions( ) -> None: """Test MQTT subscriptions are managed when entity_id is updated.""" await help_test_entity_id_update_subscriptions( - hass, - mqtt_mock_entry, - image.DOMAIN, - DEFAULT_CONFIG, - ["test_topic"], + hass, mqtt_mock_entry, image.DOMAIN, DEFAULT_CONFIG, ["test_topic"] ) @@ -754,8 +747,7 @@ async def test_entity_debug_info_message( async def test_reloadable( - hass: HomeAssistant, - mqtt_client_mock: MqttMockPahoClient, + hass: HomeAssistant, mqtt_client_mock: MqttMockPahoClient ) -> None: """Test reloading the MQTT platform.""" domain = image.DOMAIN @@ -774,8 +766,7 @@ async def test_setup_manual_entity_from_yaml( async def test_unload_entry( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test unloading the config entry.""" domain = image.DOMAIN From 1b884489144769079a95647dcb659f0052251cd0 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Wed, 26 Jun 2024 09:34:45 +0200 Subject: [PATCH 1222/1445] Do not wait for Reolink firmware check (#120377) --- homeassistant/components/reolink/__init__.py | 25 +++++++++---------- homeassistant/components/reolink/host.py | 1 + tests/components/reolink/conftest.py | 1 + tests/components/reolink/test_config_flow.py | 12 ++++----- tests/components/reolink/test_init.py | 26 +++++++++++++++++--- 5 files changed, 41 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/reolink/__init__.py b/homeassistant/components/reolink/__init__.py index a3e49f1f526..150a23dc64e 100644 --- a/homeassistant/components/reolink/__init__.py +++ b/homeassistant/components/reolink/__init__.py @@ -67,7 +67,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b raise ConfigEntryNotReady( f"Error while trying to setup {host.api.host}:{host.api.port}: {err!s}" ) from err - except Exception: + except BaseException: await host.stop() raise @@ -75,8 +75,6 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, host.stop) ) - starting = True - async def async_device_config_update() -> None: """Update the host state cache and renew the ONVIF-subscription.""" async with asyncio.timeout(host.api.timeout * (RETRY_ATTEMPTS + 2)): @@ -103,7 +101,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b try: await host.api.check_new_firmware(host.firmware_ch_list) except ReolinkError as err: - if starting: + if host.starting: _LOGGER.debug( "Error checking Reolink firmware update at startup " "from %s, possibly internet access is blocked", @@ -116,6 +114,8 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b "if the camera is blocked from accessing the internet, " "disable the update entity" ) from err + finally: + host.starting = False device_coordinator = DataUpdateCoordinator( hass, @@ -131,17 +131,15 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b update_method=async_check_firmware_update, update_interval=FIRMWARE_UPDATE_INTERVAL, ) + + # If camera WAN blocked, firmware check fails and takes long, do not prevent setup + config_entry.async_create_task(hass, firmware_coordinator.async_refresh()) # Fetch initial data so we have data when entities subscribe - results = await asyncio.gather( - device_coordinator.async_config_entry_first_refresh(), - firmware_coordinator.async_config_entry_first_refresh(), - return_exceptions=True, - ) - # If camera WAN blocked, firmware check fails, do not prevent setup - # so don't check firmware_coordinator exceptions - if isinstance(results[0], BaseException): + try: + await device_coordinator.async_config_entry_first_refresh() + except BaseException: await host.stop() - raise results[0] + raise hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = ReolinkData( host=host, @@ -159,7 +157,6 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b config_entry.add_update_listener(entry_update_listener) ) - starting = False return True diff --git a/homeassistant/components/reolink/host.py b/homeassistant/components/reolink/host.py index bccb5c5b684..c9989f2c02b 100644 --- a/homeassistant/components/reolink/host.py +++ b/homeassistant/components/reolink/host.py @@ -79,6 +79,7 @@ class ReolinkHost: ) self.firmware_ch_list: list[int | None] = [] + self.starting: bool = True self.credential_errors: int = 0 self.webhook_id: str | None = None diff --git a/tests/components/reolink/conftest.py b/tests/components/reolink/conftest.py index 3541aa1f856..105815bae1d 100644 --- a/tests/components/reolink/conftest.py +++ b/tests/components/reolink/conftest.py @@ -87,6 +87,7 @@ def reolink_connect_class() -> Generator[MagicMock]: host_mock.camera_name.return_value = TEST_NVR_NAME host_mock.camera_hardware_version.return_value = "IPC_00001" host_mock.camera_sw_version.return_value = "v1.1.0.0.0.0000" + host_mock.camera_sw_version_update_required.return_value = False host_mock.camera_uid.return_value = TEST_UID_CAM host_mock.channel_for_uid.return_value = 0 host_mock.get_encoding.return_value = "h264" diff --git a/tests/components/reolink/test_config_flow.py b/tests/components/reolink/test_config_flow.py index de1e7a0bc83..ba845dc1697 100644 --- a/tests/components/reolink/test_config_flow.py +++ b/tests/components/reolink/test_config_flow.py @@ -397,7 +397,7 @@ async def test_dhcp_flow(hass: HomeAssistant, mock_setup_entry: MagicMock) -> No None, None, TEST_HOST2, - [TEST_HOST, TEST_HOST2, TEST_HOST2], + [TEST_HOST, TEST_HOST2], ), ( True, @@ -475,8 +475,8 @@ async def test_dhcp_ip_update( const.DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=dhcp_data ) - expected_calls = [ - call( + for host in host_call_list: + expected_call = call( host, TEST_USERNAME, TEST_PASSWORD, @@ -485,10 +485,10 @@ async def test_dhcp_ip_update( protocol=DEFAULT_PROTOCOL, timeout=DEFAULT_TIMEOUT, ) - for host in host_call_list - ] + assert expected_call in reolink_connect_class.call_args_list - assert reolink_connect_class.call_args_list == expected_calls + for exc_call in reolink_connect_class.call_args_list: + assert exc_call[0][0] in host_call_list assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/reolink/test_init.py b/tests/components/reolink/test_init.py index 922fe0829f6..a6c798f9415 100644 --- a/tests/components/reolink/test_init.py +++ b/tests/components/reolink/test_init.py @@ -1,5 +1,6 @@ """Test the Reolink init.""" +import asyncio from datetime import timedelta from typing import Any from unittest.mock import AsyncMock, MagicMock, Mock, patch @@ -39,6 +40,11 @@ from tests.common import MockConfigEntry, async_fire_time_changed pytestmark = pytest.mark.usefixtures("reolink_connect", "reolink_platforms") +async def test_wait(*args, **key_args): + """Ensure a mocked function takes a bit of time to be able to timeout in test.""" + await asyncio.sleep(0) + + @pytest.mark.parametrize( ("attr", "value", "expected"), [ @@ -377,9 +383,13 @@ async def test_no_repair_issue( async def test_https_repair_issue( - hass: HomeAssistant, config_entry: MockConfigEntry, issue_registry: ir.IssueRegistry + hass: HomeAssistant, + config_entry: MockConfigEntry, + reolink_connect: MagicMock, + issue_registry: ir.IssueRegistry, ) -> None: """Test repairs issue is raised when https local url is used.""" + reolink_connect.get_states = test_wait await async_process_ha_core_config( hass, {"country": "GB", "internal_url": "https://test_homeassistant_address"} ) @@ -400,9 +410,13 @@ async def test_https_repair_issue( async def test_ssl_repair_issue( - hass: HomeAssistant, config_entry: MockConfigEntry, issue_registry: ir.IssueRegistry + hass: HomeAssistant, + config_entry: MockConfigEntry, + reolink_connect: MagicMock, + issue_registry: ir.IssueRegistry, ) -> None: """Test repairs issue is raised when global ssl certificate is used.""" + reolink_connect.get_states = test_wait assert await async_setup_component(hass, "webhook", {}) hass.config.api.use_ssl = True @@ -446,9 +460,13 @@ async def test_port_repair_issue( async def test_webhook_repair_issue( - hass: HomeAssistant, config_entry: MockConfigEntry, issue_registry: ir.IssueRegistry + hass: HomeAssistant, + config_entry: MockConfigEntry, + reolink_connect: MagicMock, + issue_registry: ir.IssueRegistry, ) -> None: """Test repairs issue is raised when the webhook url is unreachable.""" + reolink_connect.get_states = test_wait with ( patch("homeassistant.components.reolink.host.FIRST_ONVIF_TIMEOUT", new=0), patch( @@ -471,7 +489,7 @@ async def test_firmware_repair_issue( issue_registry: ir.IssueRegistry, ) -> None: """Test firmware issue is raised when too old firmware is used.""" - reolink_connect.sw_version_update_required = True + reolink_connect.camera_sw_version_update_required.return_value = True assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() From c085057847f2c4b426ced9b6477aede99c64b5e7 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Wed, 26 Jun 2024 09:40:29 +0200 Subject: [PATCH 1223/1445] Add timezone testing in holiday (#120497) --- tests/components/holiday/test_calendar.py | 56 +++++++++++++++++++---- 1 file changed, 48 insertions(+), 8 deletions(-) diff --git a/tests/components/holiday/test_calendar.py b/tests/components/holiday/test_calendar.py index b5067a467ed..db58b7b1f73 100644 --- a/tests/components/holiday/test_calendar.py +++ b/tests/components/holiday/test_calendar.py @@ -3,6 +3,7 @@ from datetime import datetime, timedelta from freezegun.api import FrozenDateTimeFactory +import pytest from homeassistant.components.calendar import ( DOMAIN as CALENDAR_DOMAIN, @@ -17,12 +18,18 @@ from homeassistant.util import dt as dt_util from tests.common import MockConfigEntry, async_fire_time_changed +@pytest.mark.parametrize( + "time_zone", ["Asia/Tokyo", "Europe/Berlin", "America/Chicago", "US/Hawaii"] +) async def test_holiday_calendar_entity( hass: HomeAssistant, freezer: FrozenDateTimeFactory, + time_zone: str, ) -> None: """Test HolidayCalendarEntity functionality.""" - freezer.move_to(datetime(2023, 1, 1, 12, tzinfo=dt_util.UTC)) + await hass.config.async_set_time_zone(time_zone) + zone = await dt_util.async_get_time_zone(time_zone) + freezer.move_to(datetime(2023, 1, 1, 0, 1, 1, tzinfo=zone)) # New Years Day config_entry = MockConfigEntry( domain=DOMAIN, @@ -64,8 +71,16 @@ async def test_holiday_calendar_entity( assert state is not None assert state.state == "on" + freezer.move_to( + datetime(2023, 1, 2, 0, 1, 1, tzinfo=zone) + ) # Day after New Years Day + + state = hass.states.get("calendar.united_states_ak") + assert state is not None + assert state.state == "on" + # Test holidays for the next year - freezer.move_to(datetime(2023, 12, 31, 12, tzinfo=dt_util.UTC)) + freezer.move_to(datetime(2023, 12, 31, 12, tzinfo=zone)) response = await hass.services.async_call( CALENDAR_DOMAIN, @@ -91,12 +106,18 @@ async def test_holiday_calendar_entity( } +@pytest.mark.parametrize( + "time_zone", ["Asia/Tokyo", "Europe/Berlin", "America/Chicago", "US/Hawaii"] +) async def test_default_language( hass: HomeAssistant, freezer: FrozenDateTimeFactory, + time_zone: str, ) -> None: """Test default language.""" - freezer.move_to(datetime(2023, 1, 1, 12, tzinfo=dt_util.UTC)) + await hass.config.async_set_time_zone(time_zone) + zone = await dt_util.async_get_time_zone(time_zone) + freezer.move_to(datetime(2023, 1, 1, 12, tzinfo=zone)) config_entry = MockConfigEntry( domain=DOMAIN, @@ -162,12 +183,18 @@ async def test_default_language( } +@pytest.mark.parametrize( + "time_zone", ["Asia/Tokyo", "Europe/Berlin", "America/Chicago", "US/Hawaii"] +) async def test_no_language( hass: HomeAssistant, freezer: FrozenDateTimeFactory, + time_zone: str, ) -> None: """Test language defaults to English if language not exist.""" - freezer.move_to(datetime(2023, 1, 1, 12, tzinfo=dt_util.UTC)) + await hass.config.async_set_time_zone(time_zone) + zone = await dt_util.async_get_time_zone(time_zone) + freezer.move_to(datetime(2023, 1, 1, 12, tzinfo=zone)) config_entry = MockConfigEntry( domain=DOMAIN, @@ -203,12 +230,18 @@ async def test_no_language( } +@pytest.mark.parametrize( + "time_zone", ["Asia/Tokyo", "Europe/Berlin", "America/Chicago", "US/Hawaii"] +) async def test_no_next_event( hass: HomeAssistant, freezer: FrozenDateTimeFactory, + time_zone: str, ) -> None: """Test if there is no next event.""" - freezer.move_to(datetime(2023, 1, 1, 12, tzinfo=dt_util.UTC)) + await hass.config.async_set_time_zone(time_zone) + zone = await dt_util.async_get_time_zone(time_zone) + freezer.move_to(datetime(2023, 1, 1, 12, tzinfo=zone)) config_entry = MockConfigEntry( domain=DOMAIN, @@ -221,7 +254,7 @@ async def test_no_next_event( await hass.async_block_till_done() # Move time to out of reach - freezer.move_to(datetime(dt_util.now().year + 5, 1, 1, 12, tzinfo=dt_util.UTC)) + freezer.move_to(datetime(dt_util.now().year + 5, 1, 1, 12, tzinfo=zone)) async_fire_time_changed(hass) state = hass.states.get("calendar.germany") @@ -230,15 +263,22 @@ async def test_no_next_event( assert state.attributes == {"friendly_name": "Germany"} +@pytest.mark.parametrize( + "time_zone", ["Asia/Tokyo", "Europe/Berlin", "America/Chicago", "US/Hawaii"] +) async def test_language_not_exist( - hass: HomeAssistant, freezer: FrozenDateTimeFactory + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + time_zone: str, ) -> None: """Test when language doesn't exist it will fallback to country default language.""" + await hass.config.async_set_time_zone(time_zone) + zone = await dt_util.async_get_time_zone(time_zone) hass.config.language = "nb" # Norweigan language "Norks bokmål" hass.config.country = "NO" - freezer.move_to(datetime(2023, 1, 1, 12, tzinfo=dt_util.UTC)) + freezer.move_to(datetime(2023, 1, 1, 12, tzinfo=zone)) config_entry = MockConfigEntry( domain=DOMAIN, From 4a8669325453bb7402fba76e1f14a410a7be48c0 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 26 Jun 2024 09:41:16 +0200 Subject: [PATCH 1224/1445] Verify default timezone is restored when test ends (#116216) --- tests/conftest.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index 1d4699647c9..161ff458ac0 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -5,6 +5,7 @@ from __future__ import annotations import asyncio from collections.abc import Callable, Coroutine from contextlib import asynccontextmanager, contextmanager +import datetime import functools import gc import itertools @@ -76,7 +77,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.translation import _TranslationsCacheData from homeassistant.helpers.typing import ConfigType from homeassistant.setup import BASE_PLATFORMS, async_setup_component -from homeassistant.util import location +from homeassistant.util import dt as dt_util, location from homeassistant.util.async_ import create_eager_task from homeassistant.util.json import json_loads @@ -385,6 +386,13 @@ def verify_cleanup( "waitpid-" ) + try: + # Verify the default time zone has been restored + assert dt_util.DEFAULT_TIME_ZONE is datetime.UTC + finally: + # Restore the default time zone to not break subsequent tests + dt_util.DEFAULT_TIME_ZONE = datetime.UTC + @pytest.fixture(autouse=True) def reset_hass_threading_local_object() -> Generator[None]: From 82b8b73b8acb7621123a5fc2b214e72577ad5244 Mon Sep 17 00:00:00 2001 From: "Mr. Bubbles" Date: Wed, 26 Jun 2024 09:46:50 +0200 Subject: [PATCH 1225/1445] Add reconfiguration flow to pyLoad integration (#120485) --- .../components/pyload/config_flow.py | 46 +++++++++ homeassistant/components/pyload/strings.json | 17 +++- tests/components/pyload/test_config_flow.py | 93 ++++++++++++++++++- 3 files changed, 154 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/pyload/config_flow.py b/homeassistant/components/pyload/config_flow.py index 4825e6fa7cf..2f4f9519d30 100644 --- a/homeassistant/components/pyload/config_flow.py +++ b/homeassistant/components/pyload/config_flow.py @@ -199,3 +199,49 @@ class PyLoadConfigFlow(ConfigFlow, domain=DOMAIN): description_placeholders={CONF_NAME: self.config_entry.data[CONF_USERNAME]}, errors=errors, ) + + async def async_step_reconfigure( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: + """Perform a reconfiguration.""" + self.config_entry = self.hass.config_entries.async_get_entry( + self.context["entry_id"] + ) + return await self.async_step_reconfigure_confirm() + + async def async_step_reconfigure_confirm( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle the reconfiguration flow.""" + errors = {} + + if TYPE_CHECKING: + assert self.config_entry + + if user_input is not None: + try: + await validate_input(self.hass, user_input) + except (CannotConnect, ParserError): + errors["base"] = "cannot_connect" + except InvalidAuth: + errors["base"] = "invalid_auth" + except Exception: + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + return self.async_update_reload_and_abort( + self.config_entry, + data=user_input, + reload_even_if_entry_is_unchanged=False, + reason="reconfigure_successful", + ) + + return self.async_show_form( + step_id="reconfigure_confirm", + data_schema=self.add_suggested_values_to_schema( + STEP_USER_DATA_SCHEMA, + user_input or self.config_entry.data, + ), + description_placeholders={CONF_NAME: self.config_entry.data[CONF_USERNAME]}, + errors=errors, + ) diff --git a/homeassistant/components/pyload/strings.json b/homeassistant/components/pyload/strings.json index 248cec99cc9..31e1443b321 100644 --- a/homeassistant/components/pyload/strings.json +++ b/homeassistant/components/pyload/strings.json @@ -15,6 +15,20 @@ "port": "pyLoad uses port 8000 by default." } }, + "reconfigure_confirm": { + "data": { + "host": "[%key:common::config_flow::data::host%]", + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]", + "ssl": "[%key:common::config_flow::data::ssl%]", + "verify_ssl": "[%key:common::config_flow::data::verify_ssl%]", + "port": "[%key:common::config_flow::data::port%]" + }, + "data_description": { + "host": "The hostname or IP address of the device running your pyLoad instance.", + "port": "pyLoad uses port 8000 by default." + } + }, "reauth_confirm": { "title": "[%key:common::config_flow::title::reauth%]", "data": { @@ -30,7 +44,8 @@ }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", + "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]" } }, "entity": { diff --git a/tests/components/pyload/test_config_flow.py b/tests/components/pyload/test_config_flow.py index 63297de7127..8e9083a49c8 100644 --- a/tests/components/pyload/test_config_flow.py +++ b/tests/components/pyload/test_config_flow.py @@ -6,7 +6,12 @@ from pyloadapi.exceptions import CannotConnect, InvalidAuth, ParserError import pytest from homeassistant.components.pyload.const import DEFAULT_NAME, DOMAIN -from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_REAUTH, SOURCE_USER +from homeassistant.config_entries import ( + SOURCE_IMPORT, + SOURCE_REAUTH, + SOURCE_RECONFIGURE, + SOURCE_USER, +) from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -250,3 +255,89 @@ async def test_reauth_errors( assert result["reason"] == "reauth_successful" assert config_entry.data == NEW_INPUT assert len(hass.config_entries.async_entries()) == 1 + + +async def test_reconfiguration( + hass: HomeAssistant, + config_entry: MockConfigEntry, + mock_pyloadapi: AsyncMock, +) -> None: + """Test reauth flow.""" + + config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": SOURCE_RECONFIGURE, + "entry_id": config_entry.entry_id, + "unique_id": config_entry.unique_id, + }, + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reconfigure_confirm" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + USER_INPUT, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reconfigure_successful" + assert config_entry.data == USER_INPUT + assert len(hass.config_entries.async_entries()) == 1 + + +@pytest.mark.parametrize( + ("side_effect", "error_text"), + [ + (InvalidAuth, "invalid_auth"), + (CannotConnect, "cannot_connect"), + (IndexError, "unknown"), + ], +) +async def test_reconfigure_errors( + hass: HomeAssistant, + config_entry: MockConfigEntry, + mock_pyloadapi: AsyncMock, + side_effect: Exception, + error_text: str, +) -> None: + """Test reauth flow.""" + + config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": SOURCE_RECONFIGURE, + "entry_id": config_entry.entry_id, + "unique_id": config_entry.unique_id, + }, + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reconfigure_confirm" + + mock_pyloadapi.login.side_effect = side_effect + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + USER_INPUT, + ) + + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"base": error_text} + + mock_pyloadapi.login.side_effect = None + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + USER_INPUT, + ) + + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reconfigure_successful" + assert config_entry.data == USER_INPUT + assert len(hass.config_entries.async_entries()) == 1 From 59959141af32055367aaaee99c23396a21a225c0 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 26 Jun 2024 09:52:05 +0200 Subject: [PATCH 1226/1445] Remove Knocki triggers on runtime (#120452) * Bump Knocki to 0.2.0 * Remove triggers on runtime in Knocki * Fix --- homeassistant/components/knocki/__init__.py | 9 ++++- .../components/knocki/coordinator.py | 23 +++++++++++++ .../knocki/fixtures/more_triggers.json | 30 +++++++++++++++++ tests/components/knocki/test_event.py | 33 +++++++++++++++++-- 4 files changed, 92 insertions(+), 3 deletions(-) create mode 100644 tests/components/knocki/fixtures/more_triggers.json diff --git a/homeassistant/components/knocki/__init__.py b/homeassistant/components/knocki/__init__.py index ddf389649f2..42c3956bd68 100644 --- a/homeassistant/components/knocki/__init__.py +++ b/homeassistant/components/knocki/__init__.py @@ -2,7 +2,7 @@ from __future__ import annotations -from knocki import EventType, KnockiClient +from knocki import Event, EventType, KnockiClient from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_TOKEN, Platform @@ -30,6 +30,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: KnockiConfigEntry) -> bo client.register_listener(EventType.CREATED, coordinator.add_trigger) ) + async def _refresh_coordinator(_: Event) -> None: + await coordinator.async_refresh() + + entry.async_on_unload( + client.register_listener(EventType.DELETED, _refresh_coordinator) + ) + entry.runtime_data = coordinator await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) diff --git a/homeassistant/components/knocki/coordinator.py b/homeassistant/components/knocki/coordinator.py index 020b3921a1e..f70fbdf79a7 100644 --- a/homeassistant/components/knocki/coordinator.py +++ b/homeassistant/components/knocki/coordinator.py @@ -2,7 +2,9 @@ from knocki import Event, KnockiClient, KnockiConnectionError, Trigger +from homeassistant.components.event import DOMAIN as EVENT_DOMAIN from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import DOMAIN, LOGGER @@ -19,12 +21,20 @@ class KnockiCoordinator(DataUpdateCoordinator[dict[int, Trigger]]): name=DOMAIN, ) self.client = client + self._known_triggers: set[tuple[str, int]] = set() async def _async_update_data(self) -> dict[int, Trigger]: try: triggers = await self.client.get_triggers() except KnockiConnectionError as exc: raise UpdateFailed from exc + current_triggers = { + (trigger.device_id, trigger.details.trigger_id) for trigger in triggers + } + removed_triggers = self._known_triggers - current_triggers + for trigger in removed_triggers: + await self._delete_device(trigger) + self._known_triggers = current_triggers return {trigger.details.trigger_id: trigger for trigger in triggers} def add_trigger(self, event: Event) -> None: @@ -32,3 +42,16 @@ class KnockiCoordinator(DataUpdateCoordinator[dict[int, Trigger]]): self.async_set_updated_data( {**self.data, event.payload.details.trigger_id: event.payload} ) + self._known_triggers.add( + (event.payload.device_id, event.payload.details.trigger_id) + ) + + async def _delete_device(self, trigger: tuple[str, int]) -> None: + """Delete a device from the coordinator.""" + device_id, trigger_id = trigger + entity_registry = er.async_get(self.hass) + entity_entry = entity_registry.async_get_entity_id( + EVENT_DOMAIN, DOMAIN, f"{device_id}_{trigger_id}" + ) + if entity_entry: + entity_registry.async_remove(entity_entry) diff --git a/tests/components/knocki/fixtures/more_triggers.json b/tests/components/knocki/fixtures/more_triggers.json new file mode 100644 index 00000000000..dbe4823e3d5 --- /dev/null +++ b/tests/components/knocki/fixtures/more_triggers.json @@ -0,0 +1,30 @@ +[ + { + "device": "KNC1-W-00000214", + "gesture": "d060b870-15ba-42c9-a932-2d2951087152", + "details": { + "description": "Eeee", + "name": "Aaaa", + "id": 31 + }, + "type": "homeassistant", + "user": "7a4d5bf9-01b1-413b-bb4d-77728e931dcc", + "updatedAt": 1716378013721, + "createdAt": 1716378013721, + "id": "1a050b25-7fed-4e0e-b5af-792b8b4650de" + }, + { + "device": "KNC1-W-00000214", + "gesture": "d060b870-15ba-42c9-a932-2d2951087152", + "details": { + "description": "Eeee", + "name": "Bbbb", + "id": 32 + }, + "type": "homeassistant", + "user": "7a4d5bf9-01b1-413b-bb4d-77728e931dcc", + "updatedAt": 1716378013721, + "createdAt": 1716378013721, + "id": "1a050b25-7fed-4e0e-b5af-792b8b4650de" + } +] diff --git a/tests/components/knocki/test_event.py b/tests/components/knocki/test_event.py index 4740ddc9167..4f639e08773 100644 --- a/tests/components/knocki/test_event.py +++ b/tests/components/knocki/test_event.py @@ -1,6 +1,6 @@ """Tests for the Knocki event platform.""" -from collections.abc import Callable +from collections.abc import Awaitable, Callable from unittest.mock import AsyncMock from knocki import Event, EventType, Trigger, TriggerDetails @@ -89,10 +89,39 @@ async def test_adding_runtime_entities( assert not hass.states.get("event.knc1_w_00000214_aaaa") add_trigger_function: Callable[[Event], None] = ( - mock_knocki_client.register_listener.call_args[0][1] + mock_knocki_client.register_listener.call_args_list[0][0][1] ) trigger = Trigger.from_dict(load_json_array_fixture("triggers.json", DOMAIN)[0]) add_trigger_function(Event(EventType.CREATED, trigger)) assert hass.states.get("event.knc1_w_00000214_aaaa") is not None + + +async def test_removing_runtime_entities( + hass: HomeAssistant, + mock_knocki_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test we can create devices on runtime.""" + mock_knocki_client.get_triggers.return_value = [ + Trigger.from_dict(trigger) + for trigger in load_json_array_fixture("more_triggers.json", DOMAIN) + ] + + await setup_integration(hass, mock_config_entry) + + assert hass.states.get("event.knc1_w_00000214_aaaa") is not None + assert hass.states.get("event.knc1_w_00000214_bbbb") is not None + + remove_trigger_function: Callable[[Event], Awaitable[None]] = ( + mock_knocki_client.register_listener.call_args_list[1][0][1] + ) + trigger = Trigger.from_dict(load_json_array_fixture("triggers.json", DOMAIN)[0]) + + mock_knocki_client.get_triggers.return_value = [trigger] + + await remove_trigger_function(Event(EventType.DELETED, trigger)) + + assert hass.states.get("event.knc1_w_00000214_aaaa") is not None + assert hass.states.get("event.knc1_w_00000214_bbbb") is None From 4bfecea2f42bc9fc96e7b4f4e6980e992f3580db Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 26 Jun 2024 10:02:24 +0200 Subject: [PATCH 1227/1445] Force alias when importing notify PLATFORM_SCHEMA (#120494) --- homeassistant/components/apprise/notify.py | 4 +-- .../components/cisco_webex_teams/notify.py | 4 +-- homeassistant/components/clickatell/notify.py | 7 ++++-- homeassistant/components/clicksend/notify.py | 25 ++++++++----------- .../components/clicksend_tts/notify.py | 7 ++++-- homeassistant/components/facebook/notify.py | 4 +-- homeassistant/components/file/notify.py | 4 +-- homeassistant/components/flock/notify.py | 9 +++++-- .../components/free_mobile/notify.py | 7 ++++-- homeassistant/components/group/notify.py | 4 +-- homeassistant/components/homematic/notify.py | 4 +-- homeassistant/components/html5/notify.py | 4 +-- .../components/joaoapps_join/notify.py | 4 +-- homeassistant/components/kodi/notify.py | 4 +-- homeassistant/components/lannouncer/notify.py | 4 +-- .../components/llamalab_automate/notify.py | 4 +-- homeassistant/components/mailgun/notify.py | 4 +-- homeassistant/components/mastodon/notify.py | 4 +-- homeassistant/components/matrix/notify.py | 6 +++-- .../components/message_bird/notify.py | 4 +-- homeassistant/components/msteams/notify.py | 4 +-- homeassistant/components/prowl/notify.py | 4 +-- homeassistant/components/pushsafer/notify.py | 6 +++-- homeassistant/components/rest/notify.py | 4 +-- homeassistant/components/rocketchat/notify.py | 4 +-- homeassistant/components/sendgrid/notify.py | 4 +-- .../components/signal_messenger/notify.py | 4 +-- homeassistant/components/sinch/notify.py | 4 +-- homeassistant/components/smtp/notify.py | 4 +-- .../components/synology_chat/notify.py | 4 +-- homeassistant/components/syslog/notify.py | 4 +-- homeassistant/components/telegram/notify.py | 6 +++-- .../components/twilio_call/notify.py | 4 +-- homeassistant/components/twilio_sms/notify.py | 4 +-- homeassistant/components/twitter/notify.py | 4 +-- homeassistant/components/xmpp/notify.py | 4 +-- pyproject.toml | 1 + 37 files changed, 102 insertions(+), 84 deletions(-) diff --git a/homeassistant/components/apprise/notify.py b/homeassistant/components/apprise/notify.py index 57a7feb6e5c..eb4e21c127f 100644 --- a/homeassistant/components/apprise/notify.py +++ b/homeassistant/components/apprise/notify.py @@ -12,7 +12,7 @@ from homeassistant.components.notify import ( ATTR_TARGET, ATTR_TITLE, ATTR_TITLE_DEFAULT, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as NOTIFY_PLATFORM_SCHEMA, BaseNotificationService, ) from homeassistant.const import CONF_URL @@ -24,7 +24,7 @@ _LOGGER = logging.getLogger(__name__) CONF_FILE = "config" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = NOTIFY_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_URL): vol.All(cv.ensure_list, [str]), vol.Optional(CONF_FILE): cv.string, diff --git a/homeassistant/components/cisco_webex_teams/notify.py b/homeassistant/components/cisco_webex_teams/notify.py index 30f56ac4712..b93ebb273dd 100644 --- a/homeassistant/components/cisco_webex_teams/notify.py +++ b/homeassistant/components/cisco_webex_teams/notify.py @@ -9,7 +9,7 @@ from webexteamssdk import ApiError, WebexTeamsAPI, exceptions from homeassistant.components.notify import ( ATTR_TITLE, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as NOTIFY_PLATFORM_SCHEMA, BaseNotificationService, ) from homeassistant.const import CONF_TOKEN @@ -21,7 +21,7 @@ _LOGGER = logging.getLogger(__name__) CONF_ROOM_ID = "room_id" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = NOTIFY_PLATFORM_SCHEMA.extend( {vol.Required(CONF_TOKEN): cv.string, vol.Required(CONF_ROOM_ID): cv.string} ) diff --git a/homeassistant/components/clickatell/notify.py b/homeassistant/components/clickatell/notify.py index 70170217af2..c8d96d48faf 100644 --- a/homeassistant/components/clickatell/notify.py +++ b/homeassistant/components/clickatell/notify.py @@ -9,7 +9,10 @@ from typing import Any import requests import voluptuous as vol -from homeassistant.components.notify import PLATFORM_SCHEMA, BaseNotificationService +from homeassistant.components.notify import ( + PLATFORM_SCHEMA as NOTIFY_PLATFORM_SCHEMA, + BaseNotificationService, +) from homeassistant.const import CONF_API_KEY, CONF_RECIPIENT from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -21,7 +24,7 @@ DEFAULT_NAME = "clickatell" BASE_API_URL = "https://platform.clickatell.com/messages/http/send" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = NOTIFY_PLATFORM_SCHEMA.extend( {vol.Required(CONF_API_KEY): cv.string, vol.Required(CONF_RECIPIENT): cv.string} ) diff --git a/homeassistant/components/clicksend/notify.py b/homeassistant/components/clicksend/notify.py index 44954211748..d00d7b413cc 100644 --- a/homeassistant/components/clicksend/notify.py +++ b/homeassistant/components/clicksend/notify.py @@ -10,7 +10,10 @@ from typing import Any import requests import voluptuous as vol -from homeassistant.components.notify import PLATFORM_SCHEMA, BaseNotificationService +from homeassistant.components.notify import ( + PLATFORM_SCHEMA as NOTIFY_PLATFORM_SCHEMA, + BaseNotificationService, +) from homeassistant.const import ( CONF_API_KEY, CONF_RECIPIENT, @@ -31,19 +34,13 @@ TIMEOUT = 5 HEADERS = {"Content-Type": CONTENT_TYPE_JSON} -PLATFORM_SCHEMA = vol.Schema( - vol.All( - PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_API_KEY): cv.string, - vol.Required(CONF_RECIPIENT, default=[]): vol.All( - cv.ensure_list, [cv.string] - ), - vol.Optional(CONF_SENDER, default=DEFAULT_SENDER): cv.string, - } - ) - ) +PLATFORM_SCHEMA = NOTIFY_PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_API_KEY): cv.string, + vol.Required(CONF_RECIPIENT, default=[]): vol.All(cv.ensure_list, [cv.string]), + vol.Optional(CONF_SENDER, default=DEFAULT_SENDER): cv.string, + } ) diff --git a/homeassistant/components/clicksend_tts/notify.py b/homeassistant/components/clicksend_tts/notify.py index aeda1b26162..6b5f2040448 100644 --- a/homeassistant/components/clicksend_tts/notify.py +++ b/homeassistant/components/clicksend_tts/notify.py @@ -9,7 +9,10 @@ import logging import requests import voluptuous as vol -from homeassistant.components.notify import PLATFORM_SCHEMA, BaseNotificationService +from homeassistant.components.notify import ( + PLATFORM_SCHEMA as NOTIFY_PLATFORM_SCHEMA, + BaseNotificationService, +) from homeassistant.const import ( CONF_API_KEY, CONF_NAME, @@ -38,7 +41,7 @@ DEFAULT_LANGUAGE = "en-us" DEFAULT_VOICE = FEMALE_VOICE TIMEOUT = 5 -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = NOTIFY_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Required(CONF_USERNAME): cv.string, diff --git a/homeassistant/components/facebook/notify.py b/homeassistant/components/facebook/notify.py index 38ed78d125b..3319f6bdebd 100644 --- a/homeassistant/components/facebook/notify.py +++ b/homeassistant/components/facebook/notify.py @@ -12,7 +12,7 @@ import voluptuous as vol from homeassistant.components.notify import ( ATTR_DATA, ATTR_TARGET, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as NOTIFY_PLATFORM_SCHEMA, BaseNotificationService, ) from homeassistant.const import CONTENT_TYPE_JSON @@ -25,7 +25,7 @@ _LOGGER = logging.getLogger(__name__) CONF_PAGE_ACCESS_TOKEN = "page_access_token" BASE_URL = "https://graph.facebook.com/v2.6/me/messages" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = NOTIFY_PLATFORM_SCHEMA.extend( {vol.Required(CONF_PAGE_ACCESS_TOKEN): cv.string} ) diff --git a/homeassistant/components/file/notify.py b/homeassistant/components/file/notify.py index 244bd69aa32..1516efd6d96 100644 --- a/homeassistant/components/file/notify.py +++ b/homeassistant/components/file/notify.py @@ -13,7 +13,7 @@ import voluptuous as vol from homeassistant.components.notify import ( ATTR_TITLE, ATTR_TITLE_DEFAULT, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as NOTIFY_PLATFORM_SCHEMA, BaseNotificationService, NotifyEntity, NotifyEntityFeature, @@ -34,7 +34,7 @@ _LOGGER = logging.getLogger(__name__) # The legacy platform schema uses a filename, after import # The full file path is stored in the config entry -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = NOTIFY_PLATFORM_SCHEMA.extend( { vol.Required(CONF_FILENAME): cv.string, vol.Optional(CONF_TIMESTAMP, default=False): cv.boolean, diff --git a/homeassistant/components/flock/notify.py b/homeassistant/components/flock/notify.py index 61c9a29bd6c..811ee51749c 100644 --- a/homeassistant/components/flock/notify.py +++ b/homeassistant/components/flock/notify.py @@ -8,7 +8,10 @@ import logging import voluptuous as vol -from homeassistant.components.notify import PLATFORM_SCHEMA, BaseNotificationService +from homeassistant.components.notify import ( + PLATFORM_SCHEMA as NOTIFY_PLATFORM_SCHEMA, + BaseNotificationService, +) from homeassistant.const import CONF_ACCESS_TOKEN from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -18,7 +21,9 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType _LOGGER = logging.getLogger(__name__) _RESOURCE = "https://api.flock.com/hooks/sendMessage/" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({vol.Required(CONF_ACCESS_TOKEN): cv.string}) +PLATFORM_SCHEMA = NOTIFY_PLATFORM_SCHEMA.extend( + {vol.Required(CONF_ACCESS_TOKEN): cv.string} +) async def async_get_service( diff --git a/homeassistant/components/free_mobile/notify.py b/homeassistant/components/free_mobile/notify.py index d888ceadb18..90c8ef3246e 100644 --- a/homeassistant/components/free_mobile/notify.py +++ b/homeassistant/components/free_mobile/notify.py @@ -8,7 +8,10 @@ import logging from freesms import FreeClient import voluptuous as vol -from homeassistant.components.notify import PLATFORM_SCHEMA, BaseNotificationService +from homeassistant.components.notify import ( + PLATFORM_SCHEMA as NOTIFY_PLATFORM_SCHEMA, + BaseNotificationService, +) from homeassistant.const import CONF_ACCESS_TOKEN, CONF_USERNAME from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -16,7 +19,7 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType _LOGGER = logging.getLogger(__name__) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = NOTIFY_PLATFORM_SCHEMA.extend( {vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_ACCESS_TOKEN): cv.string} ) diff --git a/homeassistant/components/group/notify.py b/homeassistant/components/group/notify.py index 425dcf5a914..444658a6112 100644 --- a/homeassistant/components/group/notify.py +++ b/homeassistant/components/group/notify.py @@ -13,7 +13,7 @@ from homeassistant.components.notify import ( ATTR_DATA, ATTR_MESSAGE, DOMAIN, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as NOTIFY_PLATFORM_SCHEMA, BaseNotificationService, ) from homeassistant.const import ATTR_SERVICE @@ -23,7 +23,7 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType CONF_SERVICES = "services" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = NOTIFY_PLATFORM_SCHEMA.extend( { vol.Required(CONF_SERVICES): vol.All( cv.ensure_list, diff --git a/homeassistant/components/homematic/notify.py b/homeassistant/components/homematic/notify.py index 6b7e71bb7a9..ced8ea6a951 100644 --- a/homeassistant/components/homematic/notify.py +++ b/homeassistant/components/homematic/notify.py @@ -6,7 +6,7 @@ import voluptuous as vol from homeassistant.components.notify import ( ATTR_DATA, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as NOTIFY_PLATFORM_SCHEMA, BaseNotificationService, ) from homeassistant.core import HomeAssistant @@ -24,7 +24,7 @@ from .const import ( SERVICE_SET_DEVICE_VALUE, ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = NOTIFY_PLATFORM_SCHEMA.extend( { vol.Required(ATTR_ADDRESS): vol.All(cv.string, vol.Upper), vol.Required(ATTR_CHANNEL): vol.Coerce(int), diff --git a/homeassistant/components/html5/notify.py b/homeassistant/components/html5/notify.py index 6049f8e2434..cc03202ae88 100644 --- a/homeassistant/components/html5/notify.py +++ b/homeassistant/components/html5/notify.py @@ -26,7 +26,7 @@ from homeassistant.components.notify import ( ATTR_TARGET, ATTR_TITLE, ATTR_TITLE_DEFAULT, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as NOTIFY_PLATFORM_SCHEMA, BaseNotificationService, ) from homeassistant.const import ATTR_NAME, URL_ROOT @@ -61,7 +61,7 @@ def gcm_api_deprecated(value): return value -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = NOTIFY_PLATFORM_SCHEMA.extend( { vol.Optional("gcm_sender_id"): vol.All(cv.string, gcm_api_deprecated), vol.Optional("gcm_api_key"): cv.string, diff --git a/homeassistant/components/joaoapps_join/notify.py b/homeassistant/components/joaoapps_join/notify.py index 6e9efc4da21..7fab894b0e4 100644 --- a/homeassistant/components/joaoapps_join/notify.py +++ b/homeassistant/components/joaoapps_join/notify.py @@ -11,7 +11,7 @@ from homeassistant.components.notify import ( ATTR_DATA, ATTR_TITLE, ATTR_TITLE_DEFAULT, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as NOTIFY_PLATFORM_SCHEMA, BaseNotificationService, ) from homeassistant.const import CONF_API_KEY, CONF_DEVICE_ID @@ -24,7 +24,7 @@ _LOGGER = logging.getLogger(__name__) CONF_DEVICE_IDS = "device_ids" CONF_DEVICE_NAMES = "device_names" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = NOTIFY_PLATFORM_SCHEMA.extend( { vol.Required(CONF_API_KEY): cv.string, vol.Optional(CONF_DEVICE_ID): cv.string, diff --git a/homeassistant/components/kodi/notify.py b/homeassistant/components/kodi/notify.py index 05b5ff56be4..c811a073cbb 100644 --- a/homeassistant/components/kodi/notify.py +++ b/homeassistant/components/kodi/notify.py @@ -12,7 +12,7 @@ from homeassistant.components.notify import ( ATTR_DATA, ATTR_TITLE, ATTR_TITLE_DEFAULT, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as NOTIFY_PLATFORM_SCHEMA, BaseNotificationService, ) from homeassistant.const import ( @@ -34,7 +34,7 @@ DEFAULT_PORT = 8080 DEFAULT_PROXY_SSL = False DEFAULT_TIMEOUT = 5 -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = NOTIFY_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, diff --git a/homeassistant/components/lannouncer/notify.py b/homeassistant/components/lannouncer/notify.py index 525372710af..6c3cd1922cf 100644 --- a/homeassistant/components/lannouncer/notify.py +++ b/homeassistant/components/lannouncer/notify.py @@ -10,7 +10,7 @@ import voluptuous as vol from homeassistant.components.notify import ( ATTR_DATA, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as NOTIFY_PLATFORM_SCHEMA, BaseNotificationService, ) from homeassistant.const import CONF_HOST, CONF_PORT @@ -24,7 +24,7 @@ ATTR_METHOD_ALLOWED = ["speak", "alarm"] DEFAULT_PORT = 1035 -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = NOTIFY_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, diff --git a/homeassistant/components/llamalab_automate/notify.py b/homeassistant/components/llamalab_automate/notify.py index 6ce00db71c3..da13267aec3 100644 --- a/homeassistant/components/llamalab_automate/notify.py +++ b/homeassistant/components/llamalab_automate/notify.py @@ -10,7 +10,7 @@ import voluptuous as vol from homeassistant.components.notify import ( ATTR_DATA, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as NOTIFY_PLATFORM_SCHEMA, BaseNotificationService, ) from homeassistant.const import CONF_API_KEY, CONF_DEVICE @@ -25,7 +25,7 @@ ATTR_PRIORITY = "priority" CONF_TO = "to" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = NOTIFY_PLATFORM_SCHEMA.extend( { vol.Required(CONF_API_KEY): cv.string, vol.Required(CONF_TO): cv.string, diff --git a/homeassistant/components/mailgun/notify.py b/homeassistant/components/mailgun/notify.py index 39aea79d15e..26ff13f2a6f 100644 --- a/homeassistant/components/mailgun/notify.py +++ b/homeassistant/components/mailgun/notify.py @@ -16,7 +16,7 @@ from homeassistant.components.notify import ( ATTR_DATA, ATTR_TITLE, ATTR_TITLE_DEFAULT, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as NOTIFY_PLATFORM_SCHEMA, BaseNotificationService, ) from homeassistant.const import CONF_API_KEY, CONF_DOMAIN, CONF_RECIPIENT, CONF_SENDER @@ -32,7 +32,7 @@ ATTR_IMAGES = "images" DEFAULT_SANDBOX = False -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = NOTIFY_PLATFORM_SCHEMA.extend( {vol.Required(CONF_RECIPIENT): vol.Email(), vol.Optional(CONF_SENDER): vol.Email()} ) diff --git a/homeassistant/components/mastodon/notify.py b/homeassistant/components/mastodon/notify.py index 1ab47896b0d..f15b8c6f0ab 100644 --- a/homeassistant/components/mastodon/notify.py +++ b/homeassistant/components/mastodon/notify.py @@ -11,7 +11,7 @@ import voluptuous as vol from homeassistant.components.notify import ( ATTR_DATA, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as NOTIFY_PLATFORM_SCHEMA, BaseNotificationService, ) from homeassistant.const import CONF_ACCESS_TOKEN, CONF_CLIENT_ID, CONF_CLIENT_SECRET @@ -26,7 +26,7 @@ ATTR_TARGET = "target" ATTR_MEDIA_WARNING = "media_warning" ATTR_CONTENT_WARNING = "content_warning" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = NOTIFY_PLATFORM_SCHEMA.extend( { vol.Required(CONF_ACCESS_TOKEN): cv.string, vol.Required(CONF_CLIENT_ID): cv.string, diff --git a/homeassistant/components/matrix/notify.py b/homeassistant/components/matrix/notify.py index 0c8430afacd..b05c7952d1f 100644 --- a/homeassistant/components/matrix/notify.py +++ b/homeassistant/components/matrix/notify.py @@ -10,7 +10,7 @@ from homeassistant.components.notify import ( ATTR_DATA, ATTR_MESSAGE, ATTR_TARGET, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as NOTIFY_PLATFORM_SCHEMA, BaseNotificationService, ) from homeassistant.core import HomeAssistant @@ -22,7 +22,9 @@ from .const import DOMAIN, SERVICE_SEND_MESSAGE CONF_DEFAULT_ROOM = "default_room" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({vol.Required(CONF_DEFAULT_ROOM): cv.string}) +PLATFORM_SCHEMA = NOTIFY_PLATFORM_SCHEMA.extend( + {vol.Required(CONF_DEFAULT_ROOM): cv.string} +) def get_service( diff --git a/homeassistant/components/message_bird/notify.py b/homeassistant/components/message_bird/notify.py index b1b7b373e6a..6da0e8176ef 100644 --- a/homeassistant/components/message_bird/notify.py +++ b/homeassistant/components/message_bird/notify.py @@ -10,7 +10,7 @@ import voluptuous as vol from homeassistant.components.notify import ( ATTR_TARGET, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as NOTIFY_PLATFORM_SCHEMA, BaseNotificationService, ) from homeassistant.const import CONF_API_KEY, CONF_SENDER @@ -20,7 +20,7 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType _LOGGER = logging.getLogger(__name__) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = NOTIFY_PLATFORM_SCHEMA.extend( { vol.Required(CONF_API_KEY): cv.string, vol.Optional(CONF_SENDER, default="HA"): vol.All( diff --git a/homeassistant/components/msteams/notify.py b/homeassistant/components/msteams/notify.py index d1118ed7ab5..a4de5d126d5 100644 --- a/homeassistant/components/msteams/notify.py +++ b/homeassistant/components/msteams/notify.py @@ -11,7 +11,7 @@ from homeassistant.components.notify import ( ATTR_DATA, ATTR_TITLE, ATTR_TITLE_DEFAULT, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as NOTIFY_PLATFORM_SCHEMA, BaseNotificationService, ) from homeassistant.const import CONF_URL @@ -23,7 +23,7 @@ _LOGGER = logging.getLogger(__name__) ATTR_FILE_URL = "image_url" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({vol.Required(CONF_URL): cv.url}) +PLATFORM_SCHEMA = NOTIFY_PLATFORM_SCHEMA.extend({vol.Required(CONF_URL): cv.url}) def get_service( diff --git a/homeassistant/components/prowl/notify.py b/homeassistant/components/prowl/notify.py index b5556c15c6c..1118e747275 100644 --- a/homeassistant/components/prowl/notify.py +++ b/homeassistant/components/prowl/notify.py @@ -12,7 +12,7 @@ from homeassistant.components.notify import ( ATTR_DATA, ATTR_TITLE, ATTR_TITLE_DEFAULT, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as NOTIFY_PLATFORM_SCHEMA, BaseNotificationService, ) from homeassistant.const import CONF_API_KEY @@ -24,7 +24,7 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType _LOGGER = logging.getLogger(__name__) _RESOURCE = "https://api.prowlapp.com/publicapi/" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({vol.Required(CONF_API_KEY): cv.string}) +PLATFORM_SCHEMA = NOTIFY_PLATFORM_SCHEMA.extend({vol.Required(CONF_API_KEY): cv.string}) async def async_get_service( diff --git a/homeassistant/components/pushsafer/notify.py b/homeassistant/components/pushsafer/notify.py index f4f5bf88a22..b5c517c8662 100644 --- a/homeassistant/components/pushsafer/notify.py +++ b/homeassistant/components/pushsafer/notify.py @@ -16,7 +16,7 @@ from homeassistant.components.notify import ( ATTR_TARGET, ATTR_TITLE, ATTR_TITLE_DEFAULT, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as NOTIFY_PLATFORM_SCHEMA, BaseNotificationService, ) from homeassistant.const import ATTR_ICON @@ -54,7 +54,9 @@ ATTR_PICTURE1_USERNAME = "username" ATTR_PICTURE1_PASSWORD = "password" ATTR_PICTURE1_AUTH = "auth" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({vol.Required(CONF_DEVICE_KEY): cv.string}) +PLATFORM_SCHEMA = NOTIFY_PLATFORM_SCHEMA.extend( + {vol.Required(CONF_DEVICE_KEY): cv.string} +) def get_service( diff --git a/homeassistant/components/rest/notify.py b/homeassistant/components/rest/notify.py index 7744154c1c5..c8314d18707 100644 --- a/homeassistant/components/rest/notify.py +++ b/homeassistant/components/rest/notify.py @@ -14,7 +14,7 @@ from homeassistant.components.notify import ( ATTR_TARGET, ATTR_TITLE, ATTR_TITLE_DEFAULT, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as NOTIFY_PLATFORM_SCHEMA, BaseNotificationService, ) from homeassistant.const import ( @@ -45,7 +45,7 @@ DEFAULT_MESSAGE_PARAM_NAME = "message" DEFAULT_METHOD = "GET" DEFAULT_VERIFY_SSL = True -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = NOTIFY_PLATFORM_SCHEMA.extend( { vol.Required(CONF_RESOURCE): cv.url, vol.Optional( diff --git a/homeassistant/components/rocketchat/notify.py b/homeassistant/components/rocketchat/notify.py index 9b7b40873ce..e39fb2dc0a1 100644 --- a/homeassistant/components/rocketchat/notify.py +++ b/homeassistant/components/rocketchat/notify.py @@ -14,7 +14,7 @@ import voluptuous as vol from homeassistant.components.notify import ( ATTR_DATA, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as NOTIFY_PLATFORM_SCHEMA, BaseNotificationService, ) from homeassistant.const import CONF_PASSWORD, CONF_ROOM, CONF_URL, CONF_USERNAME @@ -24,7 +24,7 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType _LOGGER = logging.getLogger(__name__) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = NOTIFY_PLATFORM_SCHEMA.extend( { vol.Required(CONF_URL): vol.Url(), vol.Required(CONF_USERNAME): cv.string, diff --git a/homeassistant/components/sendgrid/notify.py b/homeassistant/components/sendgrid/notify.py index 01ceccf781a..86f01804574 100644 --- a/homeassistant/components/sendgrid/notify.py +++ b/homeassistant/components/sendgrid/notify.py @@ -11,7 +11,7 @@ import voluptuous as vol from homeassistant.components.notify import ( ATTR_TITLE, ATTR_TITLE_DEFAULT, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as NOTIFY_PLATFORM_SCHEMA, BaseNotificationService, ) from homeassistant.const import ( @@ -30,7 +30,7 @@ CONF_SENDER_NAME = "sender_name" DEFAULT_SENDER_NAME = "Home Assistant" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = NOTIFY_PLATFORM_SCHEMA.extend( { vol.Required(CONF_API_KEY): cv.string, vol.Required(CONF_SENDER): vol.Email(), diff --git a/homeassistant/components/signal_messenger/notify.py b/homeassistant/components/signal_messenger/notify.py index b93e5bb43e2..21d42f8912f 100644 --- a/homeassistant/components/signal_messenger/notify.py +++ b/homeassistant/components/signal_messenger/notify.py @@ -11,7 +11,7 @@ import voluptuous as vol from homeassistant.components.notify import ( ATTR_DATA, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as NOTIFY_PLATFORM_SCHEMA, BaseNotificationService, ) from homeassistant.core import HomeAssistant @@ -57,7 +57,7 @@ DATA_SCHEMA = vol.Any( DATA_URLS_SCHEMA, ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = NOTIFY_PLATFORM_SCHEMA.extend( { vol.Required(CONF_SENDER_NR): cv.string, vol.Required(CONF_SIGNAL_CLI_REST_API): cv.string, diff --git a/homeassistant/components/sinch/notify.py b/homeassistant/components/sinch/notify.py index 77443dd1a84..16780a05704 100644 --- a/homeassistant/components/sinch/notify.py +++ b/homeassistant/components/sinch/notify.py @@ -18,7 +18,7 @@ from homeassistant.components.notify import ( ATTR_DATA, ATTR_MESSAGE, ATTR_TARGET, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as NOTIFY_PLATFORM_SCHEMA, BaseNotificationService, ) from homeassistant.const import CONF_API_KEY, CONF_SENDER @@ -37,7 +37,7 @@ DEFAULT_SENDER = "Home Assistant" _LOGGER = logging.getLogger(__name__) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = NOTIFY_PLATFORM_SCHEMA.extend( { vol.Required(CONF_API_KEY): cv.string, vol.Required(CONF_SERVICE_PLAN_ID): cv.string, diff --git a/homeassistant/components/smtp/notify.py b/homeassistant/components/smtp/notify.py index bac18576f06..5d19a705d87 100644 --- a/homeassistant/components/smtp/notify.py +++ b/homeassistant/components/smtp/notify.py @@ -19,7 +19,7 @@ from homeassistant.components.notify import ( ATTR_TARGET, ATTR_TITLE, ATTR_TITLE_DEFAULT, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as NOTIFY_PLATFORM_SCHEMA, BaseNotificationService, ) from homeassistant.const import ( @@ -60,7 +60,7 @@ PLATFORMS = [Platform.NOTIFY] _LOGGER = logging.getLogger(__name__) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = NOTIFY_PLATFORM_SCHEMA.extend( { vol.Required(CONF_RECIPIENT): vol.All(cv.ensure_list, [vol.Email()]), vol.Required(CONF_SENDER): vol.Email(), diff --git a/homeassistant/components/synology_chat/notify.py b/homeassistant/components/synology_chat/notify.py index a36f073b8bb..38c302b7968 100644 --- a/homeassistant/components/synology_chat/notify.py +++ b/homeassistant/components/synology_chat/notify.py @@ -11,7 +11,7 @@ import voluptuous as vol from homeassistant.components.notify import ( ATTR_DATA, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as NOTIFY_PLATFORM_SCHEMA, BaseNotificationService, ) from homeassistant.const import CONF_RESOURCE, CONF_VERIFY_SSL @@ -21,7 +21,7 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType ATTR_FILE_URL = "file_url" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = NOTIFY_PLATFORM_SCHEMA.extend( { vol.Required(CONF_RESOURCE): cv.url, vol.Optional(CONF_VERIFY_SSL, default=True): cv.boolean, diff --git a/homeassistant/components/syslog/notify.py b/homeassistant/components/syslog/notify.py index b16d44fb504..dbbada65fb2 100644 --- a/homeassistant/components/syslog/notify.py +++ b/homeassistant/components/syslog/notify.py @@ -9,7 +9,7 @@ import voluptuous as vol from homeassistant.components.notify import ( ATTR_TITLE, ATTR_TITLE_DEFAULT, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as NOTIFY_PLATFORM_SCHEMA, BaseNotificationService, ) from homeassistant.core import HomeAssistant @@ -59,7 +59,7 @@ SYSLOG_PRIORITY = { -2: "LOG_DEBUG", } -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = NOTIFY_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_FACILITY, default="syslog"): vol.In(SYSLOG_FACILITY.keys()), vol.Optional(CONF_OPTION, default="pid"): vol.In(SYSLOG_OPTION.keys()), diff --git a/homeassistant/components/telegram/notify.py b/homeassistant/components/telegram/notify.py index 16952868525..adb947bcf6b 100644 --- a/homeassistant/components/telegram/notify.py +++ b/homeassistant/components/telegram/notify.py @@ -11,7 +11,7 @@ from homeassistant.components.notify import ( ATTR_MESSAGE, ATTR_TARGET, ATTR_TITLE, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as NOTIFY_PLATFORM_SCHEMA, BaseNotificationService, ) from homeassistant.components.telegram_bot import ( @@ -40,7 +40,9 @@ ATTR_DOCUMENT = "document" CONF_CHAT_ID = "chat_id" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({vol.Required(CONF_CHAT_ID): vol.Coerce(int)}) +PLATFORM_SCHEMA = NOTIFY_PLATFORM_SCHEMA.extend( + {vol.Required(CONF_CHAT_ID): vol.Coerce(int)} +) def get_service( diff --git a/homeassistant/components/twilio_call/notify.py b/homeassistant/components/twilio_call/notify.py index d3d128ccd25..5338bb59a79 100644 --- a/homeassistant/components/twilio_call/notify.py +++ b/homeassistant/components/twilio_call/notify.py @@ -10,7 +10,7 @@ import voluptuous as vol from homeassistant.components.notify import ( ATTR_TARGET, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as NOTIFY_PLATFORM_SCHEMA, BaseNotificationService, ) from homeassistant.components.twilio import DATA_TWILIO @@ -22,7 +22,7 @@ _LOGGER = logging.getLogger(__name__) CONF_FROM_NUMBER = "from_number" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = NOTIFY_PLATFORM_SCHEMA.extend( { vol.Required(CONF_FROM_NUMBER): vol.All( cv.string, vol.Match(r"^\+?[1-9]\d{1,14}$") diff --git a/homeassistant/components/twilio_sms/notify.py b/homeassistant/components/twilio_sms/notify.py index 2c04594f314..d1e2ca2888f 100644 --- a/homeassistant/components/twilio_sms/notify.py +++ b/homeassistant/components/twilio_sms/notify.py @@ -9,7 +9,7 @@ import voluptuous as vol from homeassistant.components.notify import ( ATTR_DATA, ATTR_TARGET, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as NOTIFY_PLATFORM_SCHEMA, BaseNotificationService, ) from homeassistant.components.twilio import DATA_TWILIO @@ -22,7 +22,7 @@ _LOGGER = logging.getLogger(__name__) CONF_FROM_NUMBER = "from_number" ATTR_MEDIAURL = "media_url" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = NOTIFY_PLATFORM_SCHEMA.extend( { vol.Required(CONF_FROM_NUMBER): vol.All( cv.string, diff --git a/homeassistant/components/twitter/notify.py b/homeassistant/components/twitter/notify.py index 718f4f7dbcf..66b076126b5 100644 --- a/homeassistant/components/twitter/notify.py +++ b/homeassistant/components/twitter/notify.py @@ -16,7 +16,7 @@ import voluptuous as vol from homeassistant.components.notify import ( ATTR_DATA, ATTR_TARGET, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as NOTIFY_PLATFORM_SCHEMA, BaseNotificationService, ) from homeassistant.const import CONF_ACCESS_TOKEN, CONF_USERNAME @@ -33,7 +33,7 @@ CONF_ACCESS_TOKEN_SECRET = "access_token_secret" ATTR_MEDIA = "media" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = NOTIFY_PLATFORM_SCHEMA.extend( { vol.Required(CONF_ACCESS_TOKEN): cv.string, vol.Required(CONF_ACCESS_TOKEN_SECRET): cv.string, diff --git a/homeassistant/components/xmpp/notify.py b/homeassistant/components/xmpp/notify.py index 4da1bf35d1a..824f996c675 100644 --- a/homeassistant/components/xmpp/notify.py +++ b/homeassistant/components/xmpp/notify.py @@ -24,7 +24,7 @@ import voluptuous as vol from homeassistant.components.notify import ( ATTR_TITLE, ATTR_TITLE_DEFAULT, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as NOTIFY_PLATFORM_SCHEMA, BaseNotificationService, ) from homeassistant.const import ( @@ -56,7 +56,7 @@ DEFAULT_CONTENT_TYPE = "application/octet-stream" DEFAULT_RESOURCE = "home-assistant" XEP_0363_TIMEOUT = 10 -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = NOTIFY_PLATFORM_SCHEMA.extend( { vol.Required(CONF_SENDER): cv.string, vol.Required(CONF_PASSWORD): cv.string, diff --git a/pyproject.toml b/pyproject.toml index 6ecbb8b51d1..db6c5f0c989 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -811,6 +811,7 @@ ignore = [ [tool.ruff.lint.flake8-import-conventions.extend-aliases] voluptuous = "vol" +"homeassistant.components.notify.PLATFORM_SCHEMA" = "NOTIFY_PLATFORM_SCHEMA" "homeassistant.helpers.area_registry" = "ar" "homeassistant.helpers.category_registry" = "cr" "homeassistant.helpers.config_validation" = "cv" From caa57c56f64bcda54b4c2d101c5a2b3cb57bcf7a Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 26 Jun 2024 10:03:29 +0200 Subject: [PATCH 1228/1445] Force alias when importing air_quality PLATFORM_SCHEMA (#120502) --- homeassistant/components/ampio/air_quality.py | 4 ++-- homeassistant/components/nilu/air_quality.py | 7 +++++-- homeassistant/components/norway_air/air_quality.py | 7 +++++-- homeassistant/components/opensensemap/air_quality.py | 7 +++++-- 4 files changed, 17 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/ampio/air_quality.py b/homeassistant/components/ampio/air_quality.py index ce7bff10aa8..05581df6371 100644 --- a/homeassistant/components/ampio/air_quality.py +++ b/homeassistant/components/ampio/air_quality.py @@ -9,7 +9,7 @@ from asmog import AmpioSmog import voluptuous as vol from homeassistant.components.air_quality import ( - PLATFORM_SCHEMA as BASE_PLATFORM_SCHEMA, + PLATFORM_SCHEMA as AIR_QUALITY_PLATFORM_SCHEMA, AirQualityEntity, ) from homeassistant.const import CONF_NAME @@ -24,7 +24,7 @@ from .const import CONF_STATION_ID, SCAN_INTERVAL _LOGGER: Final = logging.getLogger(__name__) -PLATFORM_SCHEMA: Final = BASE_PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA: Final = AIR_QUALITY_PLATFORM_SCHEMA.extend( {vol.Required(CONF_STATION_ID): cv.string, vol.Optional(CONF_NAME): cv.string} ) diff --git a/homeassistant/components/nilu/air_quality.py b/homeassistant/components/nilu/air_quality.py index 7b1068771d2..7600a878548 100644 --- a/homeassistant/components/nilu/air_quality.py +++ b/homeassistant/components/nilu/air_quality.py @@ -23,7 +23,10 @@ from niluclient import ( ) import voluptuous as vol -from homeassistant.components.air_quality import PLATFORM_SCHEMA, AirQualityEntity +from homeassistant.components.air_quality import ( + PLATFORM_SCHEMA as AIR_QUALITY_PLATFORM_SCHEMA, + AirQualityEntity, +) from homeassistant.const import ( CONF_LATITUDE, CONF_LONGITUDE, @@ -86,7 +89,7 @@ CONF_ALLOWED_AREAS = [ "Ålesund", ] -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = AIR_QUALITY_PLATFORM_SCHEMA.extend( { vol.Inclusive( CONF_LATITUDE, "coordinates", "Latitude and longitude must exist together" diff --git a/homeassistant/components/norway_air/air_quality.py b/homeassistant/components/norway_air/air_quality.py index c16df860751..bba4737550b 100644 --- a/homeassistant/components/norway_air/air_quality.py +++ b/homeassistant/components/norway_air/air_quality.py @@ -8,7 +8,10 @@ import logging import metno import voluptuous as vol -from homeassistant.components.air_quality import PLATFORM_SCHEMA, AirQualityEntity +from homeassistant.components.air_quality import ( + PLATFORM_SCHEMA as AIR_QUALITY_PLATFORM_SCHEMA, + AirQualityEntity, +) from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -26,7 +29,7 @@ DEFAULT_NAME = "Air quality Norway" OVERRIDE_URL = "https://aa015h6buqvih86i1.api.met.no/weatherapi/airqualityforecast/0.1/" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = AIR_QUALITY_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_FORECAST, default=DEFAULT_FORECAST): vol.Coerce(int), vol.Optional(CONF_LATITUDE): cv.latitude, diff --git a/homeassistant/components/opensensemap/air_quality.py b/homeassistant/components/opensensemap/air_quality.py index c9b4c726a59..eb8435751c0 100644 --- a/homeassistant/components/opensensemap/air_quality.py +++ b/homeassistant/components/opensensemap/air_quality.py @@ -9,7 +9,10 @@ from opensensemap_api import OpenSenseMap from opensensemap_api.exceptions import OpenSenseMapError import voluptuous as vol -from homeassistant.components.air_quality import PLATFORM_SCHEMA, AirQualityEntity +from homeassistant.components.air_quality import ( + PLATFORM_SCHEMA as AIR_QUALITY_PLATFORM_SCHEMA, + AirQualityEntity, +) from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.exceptions import PlatformNotReady @@ -26,7 +29,7 @@ CONF_STATION_ID = "station_id" SCAN_INTERVAL = timedelta(minutes=10) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = AIR_QUALITY_PLATFORM_SCHEMA.extend( {vol.Required(CONF_STATION_ID): cv.string, vol.Optional(CONF_NAME): cv.string} ) From d76a82e34085b23a4583749accb2f1fa08759714 Mon Sep 17 00:00:00 2001 From: "Mr. Bubbles" Date: Wed, 26 Jun 2024 10:21:54 +0200 Subject: [PATCH 1229/1445] Add switch platform to pyload integration (#120352) --- homeassistant/components/pyload/__init__.py | 2 +- homeassistant/components/pyload/icons.json | 16 ++ homeassistant/components/pyload/strings.json | 8 + homeassistant/components/pyload/switch.py | 122 +++++++++++++++ .../pyload/snapshots/test_switch.ambr | 142 ++++++++++++++++++ tests/components/pyload/test_switch.py | 105 +++++++++++++ 6 files changed, 394 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/pyload/switch.py create mode 100644 tests/components/pyload/snapshots/test_switch.ambr create mode 100644 tests/components/pyload/test_switch.py diff --git a/homeassistant/components/pyload/__init__.py b/homeassistant/components/pyload/__init__.py index 8bf065797e5..0a89fbb6140 100644 --- a/homeassistant/components/pyload/__init__.py +++ b/homeassistant/components/pyload/__init__.py @@ -22,7 +22,7 @@ from homeassistant.helpers.aiohttp_client import async_create_clientsession from .coordinator import PyLoadCoordinator -PLATFORMS: list[Platform] = [Platform.BUTTON, Platform.SENSOR] +PLATFORMS: list[Platform] = [Platform.BUTTON, Platform.SENSOR, Platform.SWITCH] type PyLoadConfigEntry = ConfigEntry[PyLoadCoordinator] diff --git a/homeassistant/components/pyload/icons.json b/homeassistant/components/pyload/icons.json index 8f6f016641f..0e307a43e51 100644 --- a/homeassistant/components/pyload/icons.json +++ b/homeassistant/components/pyload/icons.json @@ -27,6 +27,22 @@ "total": { "default": "mdi:cloud-alert" } + }, + "switch": { + "download": { + "default": "mdi:play", + "state": { + "on": "mdi:play", + "off": "mdi:pause" + } + }, + "reconnect": { + "default": "mdi:restart", + "state": { + "on": "mdi:restart", + "off": "mdi:restart-off" + } + } } } } diff --git a/homeassistant/components/pyload/strings.json b/homeassistant/components/pyload/strings.json index 31e1443b321..0ed016aafb8 100644 --- a/homeassistant/components/pyload/strings.json +++ b/homeassistant/components/pyload/strings.json @@ -79,6 +79,14 @@ "free_space": { "name": "Free space" } + }, + "switch": { + "download": { + "name": "Pause/Resume queue" + }, + "reconnect": { + "name": "Auto-Reconnect" + } } }, "issues": { diff --git a/homeassistant/components/pyload/switch.py b/homeassistant/components/pyload/switch.py new file mode 100644 index 00000000000..b9391ef818f --- /dev/null +++ b/homeassistant/components/pyload/switch.py @@ -0,0 +1,122 @@ +"""Support for monitoring pyLoad.""" + +from __future__ import annotations + +from collections.abc import Awaitable, Callable +from dataclasses import dataclass +from enum import StrEnum +from typing import Any + +from pyloadapi.api import PyLoadAPI + +from homeassistant.components.switch import ( + SwitchDeviceClass, + SwitchEntity, + SwitchEntityDescription, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from . import PyLoadConfigEntry +from .const import DOMAIN, MANUFACTURER, SERVICE_NAME +from .coordinator import PyLoadCoordinator + + +class PyLoadSwitchEntity(StrEnum): + """PyLoad Switch Entities.""" + + PAUSE_RESUME_QUEUE = "download" + RECONNECT = "reconnect" + + +@dataclass(kw_only=True, frozen=True) +class PyLoadSwitchEntityDescription(SwitchEntityDescription): + """Describes pyLoad switch entity.""" + + turn_on_fn: Callable[[PyLoadAPI], Awaitable[Any]] + turn_off_fn: Callable[[PyLoadAPI], Awaitable[Any]] + toggle_fn: Callable[[PyLoadAPI], Awaitable[Any]] + + +SENSOR_DESCRIPTIONS: tuple[PyLoadSwitchEntityDescription, ...] = ( + PyLoadSwitchEntityDescription( + key=PyLoadSwitchEntity.PAUSE_RESUME_QUEUE, + translation_key=PyLoadSwitchEntity.PAUSE_RESUME_QUEUE, + device_class=SwitchDeviceClass.SWITCH, + turn_on_fn=lambda api: api.unpause(), + turn_off_fn=lambda api: api.pause(), + toggle_fn=lambda api: api.toggle_pause(), + ), + PyLoadSwitchEntityDescription( + key=PyLoadSwitchEntity.RECONNECT, + translation_key=PyLoadSwitchEntity.RECONNECT, + device_class=SwitchDeviceClass.SWITCH, + turn_on_fn=lambda api: api.toggle_reconnect(), + turn_off_fn=lambda api: api.toggle_reconnect(), + toggle_fn=lambda api: api.toggle_reconnect(), + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: PyLoadConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the pyLoad sensors.""" + + coordinator = entry.runtime_data + + async_add_entities( + PyLoadBinarySensor(coordinator, description) + for description in SENSOR_DESCRIPTIONS + ) + + +class PyLoadBinarySensor(CoordinatorEntity[PyLoadCoordinator], SwitchEntity): + """Representation of a pyLoad sensor.""" + + _attr_has_entity_name = True + entity_description: PyLoadSwitchEntityDescription + + def __init__( + self, + coordinator: PyLoadCoordinator, + entity_description: PyLoadSwitchEntityDescription, + ) -> None: + """Initialize the sensor.""" + super().__init__(coordinator) + self._attr_unique_id = ( + f"{coordinator.config_entry.entry_id}_{entity_description.key}" + ) + self.entity_description = entity_description + self._attr_device_info = DeviceInfo( + entry_type=DeviceEntryType.SERVICE, + manufacturer=MANUFACTURER, + model=SERVICE_NAME, + configuration_url=coordinator.pyload.api_url, + identifiers={(DOMAIN, coordinator.config_entry.entry_id)}, + sw_version=coordinator.version, + ) + + @property + def is_on(self) -> bool | None: + """Return the state of the device.""" + return getattr(self.coordinator.data, self.entity_description.key) + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the entity on.""" + await self.entity_description.turn_on_fn(self.coordinator.pyload) + await self.coordinator.async_refresh() + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the entity on.""" + await self.entity_description.turn_off_fn(self.coordinator.pyload) + await self.coordinator.async_refresh() + + async def async_toggle(self, **kwargs: Any) -> None: + """Toggle the entity.""" + await self.entity_description.toggle_fn(self.coordinator.pyload) + await self.coordinator.async_refresh() diff --git a/tests/components/pyload/snapshots/test_switch.ambr b/tests/components/pyload/snapshots/test_switch.ambr new file mode 100644 index 00000000000..94f2910cad8 --- /dev/null +++ b/tests/components/pyload/snapshots/test_switch.ambr @@ -0,0 +1,142 @@ +# serializer version: 1 +# name: test_state[switch.pyload_auto_reconnect-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.pyload_auto_reconnect', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Auto-Reconnect', + 'platform': 'pyload', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'XXXXXXXXXXXXXX_reconnect', + 'unit_of_measurement': None, + }) +# --- +# name: test_state[switch.pyload_auto_reconnect-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'switch', + 'friendly_name': 'pyLoad Auto-Reconnect', + }), + 'context': , + 'entity_id': 'switch.pyload_auto_reconnect', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_state[switch.pyload_pause_resume_queue-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.pyload_pause_resume_queue', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Pause/Resume queue', + 'platform': 'pyload', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'XXXXXXXXXXXXXX_download', + 'unit_of_measurement': None, + }) +# --- +# name: test_state[switch.pyload_pause_resume_queue-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'switch', + 'friendly_name': 'pyLoad Pause/Resume queue', + }), + 'context': , + 'entity_id': 'switch.pyload_pause_resume_queue', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_state[switch.pyload_reconnect-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.pyload_reconnect', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Reconnect', + 'platform': 'pyload', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'XXXXXXXXXXXXXX_reconnect', + 'unit_of_measurement': None, + }) +# --- +# name: test_state[switch.pyload_reconnect-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'switch', + 'friendly_name': 'pyLoad Reconnect', + }), + 'context': , + 'entity_id': 'switch.pyload_reconnect', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- diff --git a/tests/components/pyload/test_switch.py b/tests/components/pyload/test_switch.py new file mode 100644 index 00000000000..e7bd5a24a87 --- /dev/null +++ b/tests/components/pyload/test_switch.py @@ -0,0 +1,105 @@ +"""Tests for the pyLoad Switches.""" + +from collections.abc import AsyncGenerator +from unittest.mock import AsyncMock, call, patch + +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.pyload.switch import PyLoadSwitchEntity +from homeassistant.components.switch import ( + DOMAIN as SWITCH_DOMAIN, + SERVICE_TOGGLE, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, +) +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import ATTR_ENTITY_ID, Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from tests.common import MockConfigEntry, snapshot_platform + +# Maps entity to the mock calls to assert +API_CALL = { + PyLoadSwitchEntity.PAUSE_RESUME_QUEUE: { + SERVICE_TURN_ON: call.unpause, + SERVICE_TURN_OFF: call.pause, + SERVICE_TOGGLE: call.toggle_pause, + }, + PyLoadSwitchEntity.RECONNECT: { + SERVICE_TURN_ON: call.toggle_reconnect, + SERVICE_TURN_OFF: call.toggle_reconnect, + SERVICE_TOGGLE: call.toggle_reconnect, + }, +} + + +@pytest.fixture(autouse=True) +async def switch_only() -> AsyncGenerator[None, None]: + """Enable only the switch platform.""" + with patch( + "homeassistant.components.pyload.PLATFORMS", + [Platform.SWITCH], + ): + yield + + +async def test_state( + hass: HomeAssistant, + config_entry: MockConfigEntry, + snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, + mock_pyloadapi: AsyncMock, +) -> None: + """Test switch state.""" + + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.LOADED + + await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id) + + +@pytest.mark.parametrize( + ("service_call"), + [ + SERVICE_TURN_ON, + SERVICE_TURN_OFF, + SERVICE_TOGGLE, + ], +) +async def test_turn_on_off( + hass: HomeAssistant, + config_entry: MockConfigEntry, + mock_pyloadapi: AsyncMock, + service_call: str, + entity_registry: er.EntityRegistry, +) -> None: + """Test switch turn on/off, toggle method.""" + + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.LOADED + + entity_entries = er.async_entries_for_config_entry( + entity_registry, config_entry.entry_id + ) + + for entity_entry in entity_entries: + await hass.services.async_call( + SWITCH_DOMAIN, + service_call, + {ATTR_ENTITY_ID: entity_entry.entity_id}, + blocking=True, + ) + await hass.async_block_till_done() + assert ( + API_CALL[entity_entry.translation_key][service_call] + in mock_pyloadapi.method_calls + ) + mock_pyloadapi.reset_mock() From 7b298f177c5728339417520f7695b1e73aec8bab Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 26 Jun 2024 10:23:37 +0200 Subject: [PATCH 1230/1445] Force alias when importing tts PLATFORM_SCHEMA (#120500) --- homeassistant/components/amazon_polly/tts.py | 4 ++-- homeassistant/components/baidu/tts.py | 8 ++++++-- homeassistant/components/demo/tts.py | 4 ++-- homeassistant/components/google_cloud/tts.py | 8 ++++++-- homeassistant/components/google_translate/tts.py | 4 ++-- homeassistant/components/marytts/tts.py | 8 ++++++-- homeassistant/components/microsoft/tts.py | 8 ++++++-- homeassistant/components/picotts/tts.py | 8 ++++++-- homeassistant/components/voicerss/tts.py | 8 ++++++-- homeassistant/components/watson_tts/tts.py | 7 +++++-- homeassistant/components/yandextts/tts.py | 8 ++++++-- 11 files changed, 53 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/amazon_polly/tts.py b/homeassistant/components/amazon_polly/tts.py index bde690a3163..d5cb7092fe3 100644 --- a/homeassistant/components/amazon_polly/tts.py +++ b/homeassistant/components/amazon_polly/tts.py @@ -10,7 +10,7 @@ import botocore import voluptuous as vol from homeassistant.components.tts import ( - PLATFORM_SCHEMA as BASE_PLATFORM_SCHEMA, + PLATFORM_SCHEMA as TTS_PLATFORM_SCHEMA, Provider, TtsAudioType, ) @@ -49,7 +49,7 @@ from .const import ( _LOGGER: Final = logging.getLogger(__name__) -PLATFORM_SCHEMA: Final = BASE_PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA: Final = TTS_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_REGION, default=DEFAULT_REGION): vol.In(SUPPORTED_REGIONS), vol.Inclusive(CONF_ACCESS_KEY_ID, ATTR_CREDENTIALS): cv.string, diff --git a/homeassistant/components/baidu/tts.py b/homeassistant/components/baidu/tts.py index 859e79adec9..cdb6697d143 100644 --- a/homeassistant/components/baidu/tts.py +++ b/homeassistant/components/baidu/tts.py @@ -5,7 +5,11 @@ import logging from aip import AipSpeech import voluptuous as vol -from homeassistant.components.tts import CONF_LANG, PLATFORM_SCHEMA, Provider +from homeassistant.components.tts import ( + CONF_LANG, + PLATFORM_SCHEMA as TTS_PLATFORM_SCHEMA, + Provider, +) from homeassistant.const import CONF_API_KEY import homeassistant.helpers.config_validation as cv @@ -22,7 +26,7 @@ CONF_PITCH = "pitch" CONF_VOLUME = "volume" CONF_PERSON = "person" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = TTS_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_LANG, default=DEFAULT_LANG): vol.In(SUPPORTED_LANGUAGES), vol.Required(CONF_APP_ID): cv.string, diff --git a/homeassistant/components/demo/tts.py b/homeassistant/components/demo/tts.py index c2fa367da29..1d28d1358e1 100644 --- a/homeassistant/components/demo/tts.py +++ b/homeassistant/components/demo/tts.py @@ -9,7 +9,7 @@ import voluptuous as vol from homeassistant.components.tts import ( CONF_LANG, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as TTS_PLATFORM_SCHEMA, Provider, TtsAudioType, ) @@ -20,7 +20,7 @@ SUPPORT_LANGUAGES = ["en", "de"] DEFAULT_LANG = "en" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = TTS_PLATFORM_SCHEMA.extend( {vol.Optional(CONF_LANG, default=DEFAULT_LANG): vol.In(SUPPORT_LANGUAGES)} ) diff --git a/homeassistant/components/google_cloud/tts.py b/homeassistant/components/google_cloud/tts.py index c5eeaa7d924..1002e594a87 100644 --- a/homeassistant/components/google_cloud/tts.py +++ b/homeassistant/components/google_cloud/tts.py @@ -7,7 +7,11 @@ import os from google.cloud import texttospeech import voluptuous as vol -from homeassistant.components.tts import CONF_LANG, PLATFORM_SCHEMA, Provider +from homeassistant.components.tts import ( + CONF_LANG, + PLATFORM_SCHEMA as TTS_PLATFORM_SCHEMA, + Provider, +) import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -136,7 +140,7 @@ GAIN_SCHEMA = vol.All(vol.Coerce(float), vol.Clamp(min=MIN_GAIN, max=MAX_GAIN)) PROFILES_SCHEMA = vol.All(cv.ensure_list, [vol.In(SUPPORTED_PROFILES)]) TEXT_TYPE_SCHEMA = vol.All(vol.Lower, vol.In(SUPPORTED_TEXT_TYPES)) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = TTS_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_KEY_FILE): cv.string, vol.Optional(CONF_LANG, default=DEFAULT_LANG): vol.In(SUPPORTED_LANGUAGES), diff --git a/homeassistant/components/google_translate/tts.py b/homeassistant/components/google_translate/tts.py index c34713caef7..221c99e7c20 100644 --- a/homeassistant/components/google_translate/tts.py +++ b/homeassistant/components/google_translate/tts.py @@ -11,7 +11,7 @@ import voluptuous as vol from homeassistant.components.tts import ( CONF_LANG, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as TTS_PLATFORM_SCHEMA, Provider, TextToSpeechEntity, TtsAudioType, @@ -35,7 +35,7 @@ _LOGGER = logging.getLogger(__name__) SUPPORT_OPTIONS = ["tld"] -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = TTS_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_LANG, default=DEFAULT_LANG): vol.In(SUPPORT_LANGUAGES), vol.Optional(CONF_TLD, default=DEFAULT_TLD): vol.In(SUPPORT_TLD), diff --git a/homeassistant/components/marytts/tts.py b/homeassistant/components/marytts/tts.py index 168d735a987..89832c01937 100644 --- a/homeassistant/components/marytts/tts.py +++ b/homeassistant/components/marytts/tts.py @@ -5,7 +5,11 @@ from __future__ import annotations from speak2mary import MaryTTS import voluptuous as vol -from homeassistant.components.tts import CONF_LANG, PLATFORM_SCHEMA, Provider +from homeassistant.components.tts import ( + CONF_LANG, + PLATFORM_SCHEMA as TTS_PLATFORM_SCHEMA, + Provider, +) from homeassistant.const import CONF_EFFECT, CONF_HOST, CONF_PORT import homeassistant.helpers.config_validation as cv @@ -26,7 +30,7 @@ DEFAULT_EFFECTS: dict[str, str] = {} MAP_MARYTTS_CODEC = {"WAVE_FILE": "wav", "AIFF_FILE": "aiff", "AU_FILE": "au"} -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = TTS_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, diff --git a/homeassistant/components/microsoft/tts.py b/homeassistant/components/microsoft/tts.py index ea95771429f..aa33072089f 100644 --- a/homeassistant/components/microsoft/tts.py +++ b/homeassistant/components/microsoft/tts.py @@ -6,7 +6,11 @@ from pycsspeechtts import pycsspeechtts from requests.exceptions import HTTPError import voluptuous as vol -from homeassistant.components.tts import CONF_LANG, PLATFORM_SCHEMA, Provider +from homeassistant.components.tts import ( + CONF_LANG, + PLATFORM_SCHEMA as TTS_PLATFORM_SCHEMA, + Provider, +) from homeassistant.const import CONF_API_KEY, CONF_REGION, CONF_TYPE, PERCENTAGE from homeassistant.generated.microsoft_tts import SUPPORTED_LANGUAGES import homeassistant.helpers.config_validation as cv @@ -31,7 +35,7 @@ DEFAULT_PITCH = "default" DEFAULT_CONTOUR = "" DEFAULT_REGION = "eastus" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = TTS_PLATFORM_SCHEMA.extend( { vol.Required(CONF_API_KEY): cv.string, vol.Optional(CONF_LANG, default=DEFAULT_LANG): vol.In(SUPPORTED_LANGUAGES), diff --git a/homeassistant/components/picotts/tts.py b/homeassistant/components/picotts/tts.py index 8ba17fdac17..44d33145b3d 100644 --- a/homeassistant/components/picotts/tts.py +++ b/homeassistant/components/picotts/tts.py @@ -8,7 +8,11 @@ import tempfile import voluptuous as vol -from homeassistant.components.tts import CONF_LANG, PLATFORM_SCHEMA, Provider +from homeassistant.components.tts import ( + CONF_LANG, + PLATFORM_SCHEMA as TTS_PLATFORM_SCHEMA, + Provider, +) _LOGGER = logging.getLogger(__name__) @@ -16,7 +20,7 @@ SUPPORT_LANGUAGES = ["en-US", "en-GB", "de-DE", "es-ES", "fr-FR", "it-IT"] DEFAULT_LANG = "en-US" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = TTS_PLATFORM_SCHEMA.extend( {vol.Optional(CONF_LANG, default=DEFAULT_LANG): vol.In(SUPPORT_LANGUAGES)} ) diff --git a/homeassistant/components/voicerss/tts.py b/homeassistant/components/voicerss/tts.py index 84bbcc19409..9f1615ffa01 100644 --- a/homeassistant/components/voicerss/tts.py +++ b/homeassistant/components/voicerss/tts.py @@ -7,7 +7,11 @@ import logging import aiohttp import voluptuous as vol -from homeassistant.components.tts import CONF_LANG, PLATFORM_SCHEMA, Provider +from homeassistant.components.tts import ( + CONF_LANG, + PLATFORM_SCHEMA as TTS_PLATFORM_SCHEMA, + Provider, +) from homeassistant.const import CONF_API_KEY from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv @@ -145,7 +149,7 @@ DEFAULT_CODEC = "mp3" DEFAULT_FORMAT = "8khz_8bit_mono" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = TTS_PLATFORM_SCHEMA.extend( { vol.Required(CONF_API_KEY): cv.string, vol.Optional(CONF_LANG, default=DEFAULT_LANG): vol.In(SUPPORT_LANGUAGES), diff --git a/homeassistant/components/watson_tts/tts.py b/homeassistant/components/watson_tts/tts.py index 3cf1582e008..373d17438c9 100644 --- a/homeassistant/components/watson_tts/tts.py +++ b/homeassistant/components/watson_tts/tts.py @@ -6,7 +6,10 @@ from ibm_cloud_sdk_core.authenticators import IAMAuthenticator from ibm_watson import TextToSpeechV1 import voluptuous as vol -from homeassistant.components.tts import PLATFORM_SCHEMA, Provider +from homeassistant.components.tts import ( + PLATFORM_SCHEMA as TTS_PLATFORM_SCHEMA, + Provider, +) import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -114,7 +117,7 @@ CONTENT_TYPE_EXTENSIONS = { DEFAULT_VOICE = "en-US_AllisonV3Voice" DEFAULT_OUTPUT_FORMAT = "audio/mp3" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = TTS_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_URL, default=DEFAULT_URL): cv.string, vol.Required(CONF_APIKEY): cv.string, diff --git a/homeassistant/components/yandextts/tts.py b/homeassistant/components/yandextts/tts.py index 1a5fc4a7903..850afd05150 100644 --- a/homeassistant/components/yandextts/tts.py +++ b/homeassistant/components/yandextts/tts.py @@ -7,7 +7,11 @@ import logging import aiohttp import voluptuous as vol -from homeassistant.components.tts import CONF_LANG, PLATFORM_SCHEMA, Provider +from homeassistant.components.tts import ( + CONF_LANG, + PLATFORM_SCHEMA as TTS_PLATFORM_SCHEMA, + Provider, +) from homeassistant.const import CONF_API_KEY from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv @@ -64,7 +68,7 @@ DEFAULT_VOICE = "zahar" DEFAULT_EMOTION = "neutral" DEFAULT_SPEED = 1 -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = TTS_PLATFORM_SCHEMA.extend( { vol.Required(CONF_API_KEY): cv.string, vol.Optional(CONF_LANG, default=DEFAULT_LANG): vol.In(SUPPORT_LANGUAGES), From 0a48cc29b6dda74122958880e896a035f5de7d0d Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk <11290930+bouwew@users.noreply.github.com> Date: Wed, 26 Jun 2024 10:24:29 +0200 Subject: [PATCH 1231/1445] Implement @plugwise_command for Plugwise Number platform (#120503) --- homeassistant/components/plugwise/number.py | 20 ++++---------------- tests/components/plugwise/test_number.py | 6 +++--- 2 files changed, 7 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/plugwise/number.py b/homeassistant/components/plugwise/number.py index 1f12b2374b3..06db5faa55b 100644 --- a/homeassistant/components/plugwise/number.py +++ b/homeassistant/components/plugwise/number.py @@ -2,11 +2,8 @@ from __future__ import annotations -from collections.abc import Awaitable, Callable from dataclasses import dataclass -from plugwise import Smile - from homeassistant.components.number import ( NumberDeviceClass, NumberEntity, @@ -21,13 +18,13 @@ from . import PlugwiseConfigEntry from .const import NumberType from .coordinator import PlugwiseDataUpdateCoordinator from .entity import PlugwiseEntity +from .util import plugwise_command @dataclass(frozen=True, kw_only=True) class PlugwiseNumberEntityDescription(NumberEntityDescription): """Class describing Plugwise Number entities.""" - command: Callable[[Smile, str, str, float], Awaitable[None]] key: NumberType @@ -35,9 +32,6 @@ NUMBER_TYPES = ( PlugwiseNumberEntityDescription( key="maximum_boiler_temperature", translation_key="maximum_boiler_temperature", - command=lambda api, dev_id, number, value: api.set_number( - dev_id, number, value - ), device_class=NumberDeviceClass.TEMPERATURE, entity_category=EntityCategory.CONFIG, native_unit_of_measurement=UnitOfTemperature.CELSIUS, @@ -45,9 +39,6 @@ NUMBER_TYPES = ( PlugwiseNumberEntityDescription( key="max_dhw_temperature", translation_key="max_dhw_temperature", - command=lambda api, dev_id, number, value: api.set_number( - dev_id, number, value - ), device_class=NumberDeviceClass.TEMPERATURE, entity_category=EntityCategory.CONFIG, native_unit_of_measurement=UnitOfTemperature.CELSIUS, @@ -55,9 +46,6 @@ NUMBER_TYPES = ( PlugwiseNumberEntityDescription( key="temperature_offset", translation_key="temperature_offset", - command=lambda api, dev_id, number, value: api.set_temperature_offset( - dev_id, value - ), device_class=NumberDeviceClass.TEMPERATURE, entity_category=EntityCategory.CONFIG, native_unit_of_measurement=UnitOfTemperature.CELSIUS, @@ -120,9 +108,9 @@ class PlugwiseNumberEntity(PlugwiseEntity, NumberEntity): """Return the present setpoint value.""" return self.device[self.entity_description.key]["setpoint"] + @plugwise_command async def async_set_native_value(self, value: float) -> None: """Change to the new setpoint value.""" - await self.entity_description.command( - self.coordinator.api, self.device_id, self.entity_description.key, value + await self.coordinator.api.set_number( + self.device_id, self.entity_description.key, value ) - await self.coordinator.async_request_refresh() diff --git a/tests/components/plugwise/test_number.py b/tests/components/plugwise/test_number.py index 8d49d07b9fb..e10a7caa9e9 100644 --- a/tests/components/plugwise/test_number.py +++ b/tests/components/plugwise/test_number.py @@ -97,7 +97,7 @@ async def test_adam_temperature_offset_change( blocking=True, ) - assert mock_smile_adam.set_temperature_offset.call_count == 1 - mock_smile_adam.set_temperature_offset.assert_called_with( - "6a3bf693d05e48e0b460c815a4fdd09d", 1.0 + assert mock_smile_adam.set_number.call_count == 1 + mock_smile_adam.set_number.assert_called_with( + "6a3bf693d05e48e0b460c815a4fdd09d", "temperature_offset", 1.0 ) From a4ba346dfc1308f8ab961ed151a7865958e945f4 Mon Sep 17 00:00:00 2001 From: Artur Pragacz <49985303+arturpragacz@users.noreply.github.com> Date: Wed, 26 Jun 2024 10:28:06 +0200 Subject: [PATCH 1232/1445] Switch onkyo to pyeiscp, making it local_push (#120026) * Switch onkyo to pyeiscp, making it local_push Major rewrite of the integration to use pyeiscp. This facilitates use of the async push updates. Streamline the code dealing with zones. Handle sound mode. Add myself to codeowners. * Add types * Add more types * Address feedback * Remove sound mode support for now * Fix zone detection * Keep legacy unique_id --- CODEOWNERS | 1 + homeassistant/components/onkyo/manifest.json | 8 +- .../components/onkyo/media_player.py | 760 ++++++++---------- homeassistant/generated/integrations.json | 2 +- requirements_all.txt | 6 +- 5 files changed, 364 insertions(+), 413 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index b4ff315872d..973780b811c 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -997,6 +997,7 @@ build.json @home-assistant/supervisor /tests/components/ondilo_ico/ @JeromeHXP /homeassistant/components/onewire/ @garbled1 @epenet /tests/components/onewire/ @garbled1 @epenet +/homeassistant/components/onkyo/ @arturpragacz /homeassistant/components/onvif/ @hunterjm /tests/components/onvif/ @hunterjm /homeassistant/components/open_meteo/ @frenck diff --git a/homeassistant/components/onkyo/manifest.json b/homeassistant/components/onkyo/manifest.json index 72ef2f14711..072dc9f9e3b 100644 --- a/homeassistant/components/onkyo/manifest.json +++ b/homeassistant/components/onkyo/manifest.json @@ -1,9 +1,9 @@ { "domain": "onkyo", "name": "Onkyo", - "codeowners": [], + "codeowners": ["@arturpragacz"], "documentation": "https://www.home-assistant.io/integrations/onkyo", - "iot_class": "local_polling", - "loggers": ["eiscp"], - "requirements": ["onkyo-eiscp==1.2.7"] + "iot_class": "local_push", + "loggers": ["pyeiscp"], + "requirements": ["pyeiscp==0.0.7"] } diff --git a/homeassistant/components/onkyo/media_player.py b/homeassistant/components/onkyo/media_player.py index 97e0b3e3631..181a8117443 100644 --- a/homeassistant/components/onkyo/media_player.py +++ b/homeassistant/components/onkyo/media_player.py @@ -2,11 +2,11 @@ from __future__ import annotations +import asyncio import logging from typing import Any -import eiscp -from eiscp import eISCP +import pyeiscp import voluptuous as vol from homeassistant.components.media_player import ( @@ -17,9 +17,14 @@ from homeassistant.components.media_player import ( MediaPlayerState, MediaType, ) -from homeassistant.const import ATTR_ENTITY_ID, CONF_HOST, CONF_NAME -from homeassistant.core import HomeAssistant, ServiceCall -import homeassistant.helpers.config_validation as cv +from homeassistant.const import ( + ATTR_ENTITY_ID, + CONF_HOST, + CONF_NAME, + EVENT_HOMEASSISTANT_STOP, +) +from homeassistant.core import HomeAssistant, ServiceCall, callback +from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -32,13 +37,12 @@ CONF_RECEIVER_MAX_VOLUME = "receiver_max_volume" DEFAULT_NAME = "Onkyo Receiver" SUPPORTED_MAX_VOLUME = 100 DEFAULT_RECEIVER_MAX_VOLUME = 80 - +ZONES = {"zone2": "Zone 2", "zone3": "Zone 3", "zone4": "Zone 4"} SUPPORT_ONKYO_WO_VOLUME = ( MediaPlayerEntityFeature.TURN_ON | MediaPlayerEntityFeature.TURN_OFF | MediaPlayerEntityFeature.SELECT_SOURCE - | MediaPlayerEntityFeature.PLAY | MediaPlayerEntityFeature.PLAY_MEDIA ) SUPPORT_ONKYO = ( @@ -49,6 +53,7 @@ SUPPORT_ONKYO = ( ) KNOWN_HOSTS: list[str] = [] + DEFAULT_SOURCES = { "tv": "TV", "bd": "Bluray", @@ -63,7 +68,6 @@ DEFAULT_SOURCES = { "video7": "Video 7", "fm": "Radio", } - DEFAULT_PLAYABLE_SOURCES = ("fm", "am", "tuner") PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( @@ -80,15 +84,39 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( } ) -TIMEOUT_MESSAGE = "Timeout waiting for response." - - ATTR_HDMI_OUTPUT = "hdmi_output" ATTR_PRESET = "preset" ATTR_AUDIO_INFORMATION = "audio_information" ATTR_VIDEO_INFORMATION = "video_information" ATTR_VIDEO_OUT = "video_out" +AUDIO_VIDEO_INFORMATION_UPDATE_WAIT_TIME = 8 + +AUDIO_INFORMATION_MAPPING = [ + "audio_input_port", + "input_signal_format", + "input_frequency", + "input_channels", + "listening_mode", + "output_channels", + "output_frequency", + "precision_quartz_lock_system", + "auto_phase_control_delay", + "auto_phase_control_phase", +] + +VIDEO_INFORMATION_MAPPING = [ + "video_input_port", + "input_resolution", + "input_color_schema", + "input_color_depth", + "video_output_port", + "output_resolution", + "output_color_schema", + "output_color_depth", + "picture_mode", +] + ACCEPTED_VALUES = [ "no", "analog", @@ -106,415 +134,187 @@ ONKYO_SELECT_OUTPUT_SCHEMA = vol.Schema( vol.Required(ATTR_HDMI_OUTPUT): vol.In(ACCEPTED_VALUES), } ) - SERVICE_SELECT_HDMI_OUTPUT = "onkyo_select_hdmi_output" -def _parse_onkyo_payload(payload): - """Parse a payload returned from the eiscp library.""" - if isinstance(payload, bool): - # command not supported by the device - return False - - if len(payload) < 2: - # no value - return None - - if isinstance(payload[1], str): - return payload[1].split(",") - - return payload[1] - - -def _tuple_get(tup, index, default=None): - """Return a tuple item at index or a default value if it doesn't exist.""" - return (tup[index : index + 1] or [default])[0] - - -def determine_zones(receiver): - """Determine what zones are available for the receiver.""" - out = {"zone2": False, "zone3": False} - try: - _LOGGER.debug("Checking for zone 2 capability") - response = receiver.raw("ZPWQSTN") - if response != "ZPWN/A": # Zone 2 Available - out["zone2"] = True - else: - _LOGGER.debug("Zone 2 not available") - except ValueError as error: - if str(error) != TIMEOUT_MESSAGE: - raise - _LOGGER.debug("Zone 2 timed out, assuming no functionality") - try: - _LOGGER.debug("Checking for zone 3 capability") - response = receiver.raw("PW3QSTN") - if response != "PW3N/A": - out["zone3"] = True - else: - _LOGGER.debug("Zone 3 not available") - except ValueError as error: - if str(error) != TIMEOUT_MESSAGE: - raise - _LOGGER.debug("Zone 3 timed out, assuming no functionality") - except AssertionError: - _LOGGER.error("Zone 3 detection failed") - - return out - - -def setup_platform( +async def async_setup_platform( hass: HomeAssistant, config: ConfigType, - add_entities: AddEntitiesCallback, + async_add_entities: AddEntitiesCallback, discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the Onkyo platform.""" - hosts: list[OnkyoDevice] = [] + receivers: dict[str, pyeiscp.Connection] = {} # indexed by host + entities: dict[str, dict[str, OnkyoMediaPlayer]] = {} # indexed by host and zone - def service_handle(service: ServiceCall) -> None: + async def async_service_handle(service: ServiceCall) -> None: """Handle for services.""" entity_ids = service.data[ATTR_ENTITY_ID] - devices = [d for d in hosts if d.entity_id in entity_ids] + targets = [ + entity + for h in entities.values() + for entity in h.values() + if entity.entity_id in entity_ids + ] - for device in devices: + for target in targets: if service.service == SERVICE_SELECT_HDMI_OUTPUT: - device.select_output(service.data[ATTR_HDMI_OUTPUT]) + await target.async_select_output(service.data[ATTR_HDMI_OUTPUT]) - hass.services.register( + hass.services.async_register( DOMAIN, SERVICE_SELECT_HDMI_OUTPUT, - service_handle, + async_service_handle, schema=ONKYO_SELECT_OUTPUT_SCHEMA, ) - if CONF_HOST in config and (host := config[CONF_HOST]) not in KNOWN_HOSTS: - try: - receiver = eiscp.eISCP(host) - hosts.append( - OnkyoDevice( - receiver, - config.get(CONF_SOURCES), - name=config.get(CONF_NAME), - max_volume=config.get(CONF_MAX_VOLUME), - receiver_max_volume=config.get(CONF_RECEIVER_MAX_VOLUME), - ) + host = config.get(CONF_HOST) + name = config[CONF_NAME] + max_volume = config[CONF_MAX_VOLUME] + receiver_max_volume = config[CONF_RECEIVER_MAX_VOLUME] + sources = config[CONF_SOURCES] + + @callback + def async_onkyo_update_callback(message: tuple[str, str, Any], origin: str) -> None: + """Process new message from receiver.""" + receiver = receivers[origin] + _LOGGER.debug("Received update callback from %s: %s", receiver.name, message) + + zone, _, value = message + entity = entities[origin].get(zone) + if entity is not None: + if entity.enabled: + entity.process_update(message) + elif zone in ZONES and value != "N/A": + # When we receive the status for a zone, and the value is not "N/A", + # then zone is available on the receiver, so we create the entity for it. + _LOGGER.debug("Discovered %s on %s", ZONES[zone], receiver.name) + zone_entity = OnkyoMediaPlayer( + receiver, sources, zone, max_volume, receiver_max_volume ) - KNOWN_HOSTS.append(host) + entities[origin][zone] = zone_entity + async_add_entities([zone_entity]) - zones = determine_zones(receiver) + @callback + def async_onkyo_connect_callback(origin: str) -> None: + """Receiver (re)connected.""" + receiver = receivers[origin] + _LOGGER.debug("Receiver (re)connected: %s (%s)", receiver.name, receiver.host) - # Add Zone2 if available - if zones["zone2"]: - _LOGGER.debug("Setting up zone 2") - hosts.append( - OnkyoDeviceZone( - "2", - receiver, - config.get(CONF_SOURCES), - name=f"{config[CONF_NAME]} Zone 2", - max_volume=config.get(CONF_MAX_VOLUME), - receiver_max_volume=config.get(CONF_RECEIVER_MAX_VOLUME), - ) - ) - # Add Zone3 if available - if zones["zone3"]: - _LOGGER.debug("Setting up zone 3") - hosts.append( - OnkyoDeviceZone( - "3", - receiver, - config.get(CONF_SOURCES), - name=f"{config[CONF_NAME]} Zone 3", - max_volume=config.get(CONF_MAX_VOLUME), - receiver_max_volume=config.get(CONF_RECEIVER_MAX_VOLUME), - ) - ) - except OSError: - _LOGGER.error("Unable to connect to receiver at %s", host) + for entity in entities[origin].values(): + entity.backfill_state() + + def setup_receiver(receiver: pyeiscp.Connection) -> None: + KNOWN_HOSTS.append(receiver.host) + + # Store the receiver object and create a dictionary to store its entities. + receivers[receiver.host] = receiver + entities[receiver.host] = {} + + # Discover what zones are available for the receiver by querying the power. + # If we get a response for the specific zone, it means it is available. + for zone in ZONES: + receiver.query_property(zone, "power") + + # Add the main zone to entities, since it is always active. + _LOGGER.debug("Adding Main Zone on %s", receiver.name) + main_entity = OnkyoMediaPlayer( + receiver, sources, "main", max_volume, receiver_max_volume + ) + entities[receiver.host]["main"] = main_entity + async_add_entities([main_entity]) + + if host is not None and host not in KNOWN_HOSTS: + _LOGGER.debug("Manually creating receiver: %s (%s)", name, host) + receiver = await pyeiscp.Connection.create( + host=host, + update_callback=async_onkyo_update_callback, + connect_callback=async_onkyo_connect_callback, + ) + + # The library automatically adds a name and identifier only on discovered hosts, + # so manually add them here instead. + receiver.name = name + receiver.identifier = None + + setup_receiver(receiver) else: - for receiver in eISCP.discover(): + + @callback + async def async_onkyo_discovery_callback(receiver: pyeiscp.Connection): + """Receiver discovered, connection not yet active.""" + _LOGGER.debug("Receiver discovered: %s (%s)", receiver.name, receiver.host) if receiver.host not in KNOWN_HOSTS: - hosts.append(OnkyoDevice(receiver, config.get(CONF_SOURCES))) - KNOWN_HOSTS.append(receiver.host) - add_entities(hosts, True) + await receiver.connect() + setup_receiver(receiver) + + _LOGGER.debug("Discovering receivers") + await pyeiscp.Connection.discover( + update_callback=async_onkyo_update_callback, + connect_callback=async_onkyo_connect_callback, + discovery_callback=async_onkyo_discovery_callback, + ) + + @callback + def close_receiver(_event): + for receiver in receivers.values(): + receiver.close() + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, close_receiver) -class OnkyoDevice(MediaPlayerEntity): - """Representation of an Onkyo device.""" +class OnkyoMediaPlayer(MediaPlayerEntity): + """Representation of an Onkyo Receiver Media Player (one per each zone).""" - _attr_supported_features = SUPPORT_ONKYO + _attr_should_poll = False + + _supports_volume: bool = False + _supports_audio_info: bool = False + _supports_video_info: bool = False + _query_timer: asyncio.TimerHandle | None = None def __init__( self, - receiver, - sources, - name=None, - max_volume=SUPPORTED_MAX_VOLUME, - receiver_max_volume=DEFAULT_RECEIVER_MAX_VOLUME, - ): + receiver: pyeiscp.Connection, + sources: dict[str, str], + zone: str, + max_volume: int, + receiver_max_volume: int, + ) -> None: """Initialize the Onkyo Receiver.""" self._receiver = receiver - self._attr_is_volume_muted = False - self._attr_volume_level = 0 - self._attr_state = MediaPlayerState.OFF - if name: - # not discovered - self._attr_name = name - else: + name = receiver.name + self._attr_name = f"{name}{' ' + ZONES[zone] if zone != 'main' else ''}" + identifier = receiver.identifier + if identifier is not None: # discovered - self._attr_unique_id = ( - f"{receiver.info['model_name']}_{receiver.info['identifier']}" - ) - self._attr_name = self._attr_unique_id + if zone == "main": + # keep legacy unique_id + self._attr_unique_id = f"{name}_{identifier}" + else: + self._attr_unique_id = f"{identifier}_{zone}" + else: + # not discovered + self._attr_unique_id = None - self._max_volume = max_volume - self._receiver_max_volume = receiver_max_volume - self._attr_source_list = list(sources.values()) + self._zone = zone self._source_mapping = sources self._reverse_mapping = {value: key for key, value in sources.items()} + self._max_volume = max_volume + self._receiver_max_volume = receiver_max_volume + + self._attr_source_list = list(sources.values()) self._attr_extra_state_attributes = {} - self._hdmi_out_supported = True - self._audio_info_supported = True - self._video_info_supported = True - def command(self, command): - """Run an eiscp command and catch connection errors.""" - try: - result = self._receiver.command(command) - except (ValueError, OSError, AttributeError, AssertionError): - if self._receiver.command_socket: - self._receiver.command_socket = None - _LOGGER.debug("Resetting connection to %s", self.name) - else: - _LOGGER.info("%s is disconnected. Attempting to reconnect", self.name) - return False - _LOGGER.debug("Result for %s: %s", command, result) - return result + async def async_added_to_hass(self) -> None: + """Entity has been added to hass.""" + self.backfill_state() - def update(self) -> None: - """Get the latest state from the device.""" - status = self.command("system-power query") - - if not status: - return - if status[1] == "on": - self._attr_state = MediaPlayerState.ON - else: - self._attr_state = MediaPlayerState.OFF - self._attr_extra_state_attributes.pop(ATTR_AUDIO_INFORMATION, None) - self._attr_extra_state_attributes.pop(ATTR_VIDEO_INFORMATION, None) - self._attr_extra_state_attributes.pop(ATTR_PRESET, None) - self._attr_extra_state_attributes.pop(ATTR_VIDEO_OUT, None) - return - - volume_raw = self.command("volume query") - mute_raw = self.command("audio-muting query") - current_source_raw = self.command("input-selector query") - # If the following command is sent to a device with only one HDMI out, - # the display shows 'Not Available'. - # We avoid this by checking if HDMI out is supported - if self._hdmi_out_supported: - hdmi_out_raw = self.command("hdmi-output-selector query") - else: - hdmi_out_raw = [] - preset_raw = self.command("preset query") - if self._audio_info_supported: - audio_information_raw = self.command("audio-information query") - self._parse_audio_information(audio_information_raw) - if self._video_info_supported: - video_information_raw = self.command("video-information query") - self._parse_video_information(video_information_raw) - if not (volume_raw and mute_raw and current_source_raw): - return - - sources = _parse_onkyo_payload(current_source_raw) - - for source in sources: - if source in self._source_mapping: - self._attr_source = self._source_mapping[source] - break - self._attr_source = "_".join(sources) - - if preset_raw and self.source and self.source.lower() == "radio": - self._attr_extra_state_attributes[ATTR_PRESET] = preset_raw[1] - elif ATTR_PRESET in self._attr_extra_state_attributes: - del self._attr_extra_state_attributes[ATTR_PRESET] - - self._attr_is_volume_muted = bool(mute_raw[1] == "on") - # AMP_VOL / (MAX_RECEIVER_VOL * (MAX_VOL / 100)) - self._attr_volume_level = volume_raw[1] / ( - self._receiver_max_volume * self._max_volume / 100 - ) - - if not hdmi_out_raw: - return - self._attr_extra_state_attributes[ATTR_VIDEO_OUT] = ",".join(hdmi_out_raw[1]) - if hdmi_out_raw[1] == "N/A": - self._hdmi_out_supported = False - - def turn_off(self) -> None: - """Turn the media player off.""" - self.command("system-power standby") - - def set_volume_level(self, volume: float) -> None: - """Set volume level, input is range 0..1. - - However full volume on the amp is usually far too loud so allow the user to - specify the upper range with CONF_MAX_VOLUME. We change as per max_volume - set by user. This means that if max volume is 80 then full volume in HA will - give 80% volume on the receiver. Then we convert that to the correct scale - for the receiver. - """ - # HA_VOL * (MAX VOL / 100) * MAX_RECEIVER_VOL - self.command( - "volume" - f" {int(volume * (self._max_volume / 100) * self._receiver_max_volume)}" - ) - - def volume_up(self) -> None: - """Increase volume by 1 step.""" - self.command("volume level-up") - - def volume_down(self) -> None: - """Decrease volume by 1 step.""" - self.command("volume level-down") - - def mute_volume(self, mute: bool) -> None: - """Mute (true) or unmute (false) media player.""" - if mute: - self.command("audio-muting on") - else: - self.command("audio-muting off") - - def turn_on(self) -> None: - """Turn the media player on.""" - self.command("system-power on") - - def select_source(self, source: str) -> None: - """Set the input source.""" - if self.source_list and source in self.source_list: - source = self._reverse_mapping[source] - self.command(f"input-selector {source}") - - def play_media( - self, media_type: MediaType | str, media_id: str, **kwargs: Any - ) -> None: - """Play radio station by preset number.""" - source = self._reverse_mapping[self._attr_source] - if media_type.lower() == "radio" and source in DEFAULT_PLAYABLE_SOURCES: - self.command(f"preset {media_id}") - - def select_output(self, output): - """Set hdmi-out.""" - self.command(f"hdmi-output-selector={output}") - - def _parse_audio_information(self, audio_information_raw): - values = _parse_onkyo_payload(audio_information_raw) - if values is False: - self._audio_info_supported = False - return - - if values: - info = { - "format": _tuple_get(values, 1), - "input_frequency": _tuple_get(values, 2), - "input_channels": _tuple_get(values, 3), - "listening_mode": _tuple_get(values, 4), - "output_channels": _tuple_get(values, 5), - "output_frequency": _tuple_get(values, 6), - } - self._attr_extra_state_attributes[ATTR_AUDIO_INFORMATION] = info - else: - self._attr_extra_state_attributes.pop(ATTR_AUDIO_INFORMATION, None) - - def _parse_video_information(self, video_information_raw): - values = _parse_onkyo_payload(video_information_raw) - if values is False: - self._video_info_supported = False - return - - if values: - info = { - "input_resolution": _tuple_get(values, 1), - "input_color_schema": _tuple_get(values, 2), - "input_color_depth": _tuple_get(values, 3), - "output_resolution": _tuple_get(values, 5), - "output_color_schema": _tuple_get(values, 6), - "output_color_depth": _tuple_get(values, 7), - "picture_mode": _tuple_get(values, 8), - "dynamic_range": _tuple_get(values, 9), - } - self._attr_extra_state_attributes[ATTR_VIDEO_INFORMATION] = info - else: - self._attr_extra_state_attributes.pop(ATTR_VIDEO_INFORMATION, None) - - -class OnkyoDeviceZone(OnkyoDevice): - """Representation of an Onkyo device's extra zone.""" - - def __init__( - self, - zone, - receiver, - sources, - name=None, - max_volume=SUPPORTED_MAX_VOLUME, - receiver_max_volume=DEFAULT_RECEIVER_MAX_VOLUME, - ): - """Initialize the Zone with the zone identifier.""" - self._zone = zone - self._supports_volume = True - super().__init__(receiver, sources, name, max_volume, receiver_max_volume) - - def update(self) -> None: - """Get the latest state from the device.""" - status = self.command(f"zone{self._zone}.power=query") - - if not status: - return - if status[1] == "on": - self._attr_state = MediaPlayerState.ON - else: - self._attr_state = MediaPlayerState.OFF - return - - volume_raw = self.command(f"zone{self._zone}.volume=query") - mute_raw = self.command(f"zone{self._zone}.muting=query") - current_source_raw = self.command(f"zone{self._zone}.selector=query") - preset_raw = self.command(f"zone{self._zone}.preset=query") - # If we received a source value, but not a volume value - # it's likely this zone permanently does not support volume. - if current_source_raw and not volume_raw: - self._supports_volume = False - - if not (volume_raw and mute_raw and current_source_raw): - return - - # It's possible for some players to have zones set to HDMI with - # no sound control. In this case, the string `N/A` is returned. - self._supports_volume = isinstance(volume_raw[1], (float, int)) - - # eiscp can return string or tuple. Make everything tuples. - if isinstance(current_source_raw[1], str): - current_source_tuples = (current_source_raw[0], (current_source_raw[1],)) - else: - current_source_tuples = current_source_raw - - for source in current_source_tuples[1]: - if source in self._source_mapping: - self._attr_source = self._source_mapping[source] - break - self._attr_source = "_".join(current_source_tuples[1]) - self._attr_is_volume_muted = bool(mute_raw[1] == "on") - if preset_raw and self.source and self.source.lower() == "radio": - self._attr_extra_state_attributes[ATTR_PRESET] = preset_raw[1] - elif ATTR_PRESET in self._attr_extra_state_attributes: - del self._attr_extra_state_attributes[ATTR_PRESET] - if self._supports_volume: - # AMP_VOL / (MAX_RECEIVER_VOL * (MAX_VOL / 100)) - self._attr_volume_level = volume_raw[1] / ( - self._receiver_max_volume * self._max_volume / 100 - ) + async def async_will_remove_from_hass(self) -> None: + """Cancel the query timer when the entity is removed.""" + if self._query_timer: + self._query_timer.cancel() + self._query_timer = None @property def supported_features(self) -> MediaPlayerEntityFeature: @@ -523,12 +323,26 @@ class OnkyoDeviceZone(OnkyoDevice): return SUPPORT_ONKYO return SUPPORT_ONKYO_WO_VOLUME - def turn_off(self) -> None: - """Turn the media player off.""" - self.command(f"zone{self._zone}.power=standby") + @callback + def _update_receiver(self, propname: str, value: Any) -> None: + """Update a property in the receiver.""" + self._receiver.update_property(self._zone, propname, value) - def set_volume_level(self, volume: float) -> None: - """Set volume level, input is range 0..1. + @callback + def _query_receiver(self, propname: str) -> None: + """Cause the receiver to send an update about a property.""" + self._receiver.query_property(self._zone, propname) + + async def async_turn_on(self) -> None: + """Turn the media player on.""" + self._update_receiver("power", "on") + + async def async_turn_off(self) -> None: + """Turn the media player off.""" + self._update_receiver("power", "standby") + + async def async_set_volume_level(self, volume: float) -> None: + """Set volume level, range 0..1. However full volume on the amp is usually far too loud so allow the user to specify the upper range with CONF_MAX_VOLUME. We change as per max_volume @@ -537,31 +351,167 @@ class OnkyoDeviceZone(OnkyoDevice): scale for the receiver. """ # HA_VOL * (MAX VOL / 100) * MAX_RECEIVER_VOL - self.command( - f"zone{self._zone}.volume={int(volume * (self._max_volume / 100) * self._receiver_max_volume)}" + self._update_receiver( + "volume", int(volume * (self._max_volume / 100) * self._receiver_max_volume) ) - def volume_up(self) -> None: + async def async_volume_up(self) -> None: """Increase volume by 1 step.""" - self.command(f"zone{self._zone}.volume=level-up") + self._update_receiver("volume", "level-up") - def volume_down(self) -> None: + async def async_volume_down(self) -> None: """Decrease volume by 1 step.""" - self.command(f"zone{self._zone}.volume=level-down") + self._update_receiver("volume", "level-down") - def mute_volume(self, mute: bool) -> None: - """Mute (true) or unmute (false) media player.""" - if mute: - self.command(f"zone{self._zone}.muting=on") - else: - self.command(f"zone{self._zone}.muting=off") + async def async_mute_volume(self, mute: bool) -> None: + """Mute the volume.""" + self._update_receiver( + "audio-muting" if self._zone == "main" else "muting", + "on" if mute else "off", + ) - def turn_on(self) -> None: - """Turn the media player on.""" - self.command(f"zone{self._zone}.power=on") - - def select_source(self, source: str) -> None: - """Set the input source.""" + async def async_select_source(self, source: str) -> None: + """Select input source.""" if self.source_list and source in self.source_list: source = self._reverse_mapping[source] - self.command(f"zone{self._zone}.selector={source}") + self._update_receiver( + "input-selector" if self._zone == "main" else "selector", source + ) + + async def async_select_output(self, hdmi_output: str) -> None: + """Set hdmi-out.""" + self._update_receiver("hdmi-output-selector", hdmi_output) + + async def async_play_media( + self, media_type: MediaType | str, media_id: str, **kwargs: Any + ) -> None: + """Play radio station by preset number.""" + if self.source is not None: + source = self._reverse_mapping[self.source] + if media_type.lower() == "radio" and source in DEFAULT_PLAYABLE_SOURCES: + self._update_receiver("preset", media_id) + + @callback + def backfill_state(self) -> None: + """Get the receiver to send all the info we care about. + + Usually run only on connect, as we can otherwise rely on the + receiver to keep us informed of changes. + """ + self._query_receiver("power") + self._query_receiver("volume") + self._query_receiver("preset") + if self._zone == "main": + self._query_receiver("hdmi-output-selector") + self._query_receiver("audio-muting") + self._query_receiver("input-selector") + self._query_receiver("listening-mode") + self._query_receiver("audio-information") + self._query_receiver("video-information") + else: + self._query_receiver("muting") + self._query_receiver("selector") + + @callback + def process_update(self, update: tuple[str, str, Any]) -> None: + """Store relevant updates so they can be queried later.""" + zone, command, value = update + if zone != self._zone: + return + + if command in ["system-power", "power"]: + if value == "on": + self._attr_state = MediaPlayerState.ON + else: + self._attr_state = MediaPlayerState.OFF + self._attr_extra_state_attributes.pop(ATTR_AUDIO_INFORMATION, None) + self._attr_extra_state_attributes.pop(ATTR_VIDEO_INFORMATION, None) + self._attr_extra_state_attributes.pop(ATTR_PRESET, None) + self._attr_extra_state_attributes.pop(ATTR_VIDEO_OUT, None) + elif command in ["volume", "master-volume"] and value != "N/A": + self._supports_volume = True + # AMP_VOL / (MAX_RECEIVER_VOL * (MAX_VOL / 100)) + self._attr_volume_level = value / ( + self._receiver_max_volume * self._max_volume / 100 + ) + elif command in ["muting", "audio-muting"]: + self._attr_is_volume_muted = bool(value == "on") + elif command in ["selector", "input-selector"]: + self._parse_source(value) + self._query_av_info_delayed() + elif command == "hdmi-output-selector": + self._attr_extra_state_attributes[ATTR_VIDEO_OUT] = ",".join(value) + elif command == "preset": + if self.source is not None and self.source.lower() == "radio": + self._attr_extra_state_attributes[ATTR_PRESET] = value + elif ATTR_PRESET in self._attr_extra_state_attributes: + del self._attr_extra_state_attributes[ATTR_PRESET] + elif command == "audio-information": + self._supports_audio_info = True + self._parse_audio_information(value) + elif command == "video-information": + self._supports_video_info = True + self._parse_video_information(value) + elif command == "fl-display-information": + self._query_av_info_delayed() + + self.async_write_ha_state() + + @callback + def _parse_source(self, source): + # source is either a tuple of values or a single value, + # so we convert to a tuple, when it is a single value. + if not isinstance(source, tuple): + source = (source,) + for value in source: + if value in self._source_mapping: + self._attr_source = self._source_mapping[value] + break + self._attr_source = "_".join(source) + + @callback + def _parse_audio_information(self, audio_information): + # If audio information is not available, N/A is returned, + # so only update the audio information, when it is not N/A. + if audio_information == "N/A": + self._attr_extra_state_attributes.pop(ATTR_AUDIO_INFORMATION, None) + return + + self._attr_extra_state_attributes[ATTR_AUDIO_INFORMATION] = { + name: value + for name, value in zip( + AUDIO_INFORMATION_MAPPING, audio_information, strict=False + ) + if len(value) > 0 + } + + @callback + def _parse_video_information(self, video_information): + # If video information is not available, N/A is returned, + # so only update the video information, when it is not N/A. + if video_information == "N/A": + self._attr_extra_state_attributes.pop(ATTR_VIDEO_INFORMATION, None) + return + + self._attr_extra_state_attributes[ATTR_VIDEO_INFORMATION] = { + name: value + for name, value in zip( + VIDEO_INFORMATION_MAPPING, video_information, strict=False + ) + if len(value) > 0 + } + + def _query_av_info_delayed(self): + if self._zone == "main" and not self._query_timer: + + @callback + def _query_av_info(): + if self._supports_audio_info: + self._query_receiver("audio-information") + if self._supports_video_info: + self._query_receiver("video-information") + self._query_timer = None + + self._query_timer = self.hass.loop.call_later( + AUDIO_VIDEO_INFORMATION_UPDATE_WAIT_TIME, _query_av_info + ) diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index d3380fdd17f..e98df79d096 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -4243,7 +4243,7 @@ "name": "Onkyo", "integration_type": "hub", "config_flow": false, - "iot_class": "local_polling" + "iot_class": "local_push" }, "onvif": { "name": "ONVIF", diff --git a/requirements_all.txt b/requirements_all.txt index 3548be4ad60..5967b3f2a94 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1470,9 +1470,6 @@ omnilogic==0.4.5 # homeassistant.components.ondilo_ico ondilo==0.5.0 -# homeassistant.components.onkyo -onkyo-eiscp==1.2.7 - # homeassistant.components.onvif onvif-zeep-async==3.1.12 @@ -1826,6 +1823,9 @@ pyefergy==22.5.0 # homeassistant.components.energenie_power_sockets pyegps==0.2.5 +# homeassistant.components.onkyo +pyeiscp==0.0.7 + # homeassistant.components.emoncms pyemoncms==0.0.7 From 9b8922a6787c68dff094b67f55bd806e75731794 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 26 Jun 2024 10:36:54 +0200 Subject: [PATCH 1233/1445] Force alias when importing switch PLATFORM_SCHEMA (#120504) --- homeassistant/components/acer_projector/switch.py | 7 +++++-- homeassistant/components/ads/switch.py | 7 +++++-- homeassistant/components/anel_pwrctrl/switch.py | 7 +++++-- homeassistant/components/aqualogic/switch.py | 7 +++++-- homeassistant/components/arest/switch.py | 7 +++++-- homeassistant/components/aten_pe/switch.py | 4 ++-- homeassistant/components/digital_ocean/switch.py | 7 +++++-- homeassistant/components/edimax/switch.py | 7 +++++-- homeassistant/components/enocean/switch.py | 7 +++++-- homeassistant/components/gc100/switch.py | 7 +++++-- homeassistant/components/group/switch.py | 8 ++++++-- homeassistant/components/hikvisioncam/switch.py | 7 +++++-- homeassistant/components/kankun/switch.py | 7 +++++-- homeassistant/components/linode/switch.py | 7 +++++-- homeassistant/components/mfi/switch.py | 7 +++++-- homeassistant/components/netio/switch.py | 7 +++++-- homeassistant/components/orvibo/switch.py | 7 +++++-- homeassistant/components/pencom/switch.py | 7 +++++-- homeassistant/components/pilight/switch.py | 7 +++++-- homeassistant/components/pulseaudio_loopback/switch.py | 7 +++++-- homeassistant/components/raincloud/switch.py | 7 +++++-- homeassistant/components/raspyrfm/switch.py | 7 +++++-- homeassistant/components/recswitch/switch.py | 7 +++++-- homeassistant/components/remote_rpi_gpio/switch.py | 7 +++++-- homeassistant/components/rest/switch.py | 4 ++-- homeassistant/components/rflink/switch.py | 7 +++++-- homeassistant/components/scsgate/switch.py | 7 +++++-- homeassistant/components/snmp/switch.py | 7 +++++-- homeassistant/components/sony_projector/switch.py | 7 +++++-- homeassistant/components/switchmate/switch.py | 7 +++++-- homeassistant/components/telnet/switch.py | 4 ++-- homeassistant/components/template/switch.py | 4 ++-- homeassistant/components/thinkingcleaner/switch.py | 4 ++-- homeassistant/components/vultr/switch.py | 7 +++++-- homeassistant/components/wake_on_lan/switch.py | 4 ++-- homeassistant/components/wirelesstag/switch.py | 4 ++-- homeassistant/components/zoneminder/switch.py | 7 +++++-- 37 files changed, 165 insertions(+), 74 deletions(-) diff --git a/homeassistant/components/acer_projector/switch.py b/homeassistant/components/acer_projector/switch.py index b29bbf9fa3f..5c1c37df5d8 100644 --- a/homeassistant/components/acer_projector/switch.py +++ b/homeassistant/components/acer_projector/switch.py @@ -9,7 +9,10 @@ from typing import Any import serial import voluptuous as vol -from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity +from homeassistant.components.switch import ( + PLATFORM_SCHEMA as SWITCH_PLATFORM_SCHEMA, + SwitchEntity, +) from homeassistant.const import ( CONF_FILENAME, CONF_NAME, @@ -38,7 +41,7 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SWITCH_PLATFORM_SCHEMA.extend( { vol.Required(CONF_FILENAME): cv.isdevice, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/ads/switch.py b/homeassistant/components/ads/switch.py index a793a5996cf..803b95a7d8a 100644 --- a/homeassistant/components/ads/switch.py +++ b/homeassistant/components/ads/switch.py @@ -7,7 +7,10 @@ from typing import Any import pyads import voluptuous as vol -from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity +from homeassistant.components.switch import ( + PLATFORM_SCHEMA as SWITCH_PLATFORM_SCHEMA, + SwitchEntity, +) from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -18,7 +21,7 @@ from . import CONF_ADS_VAR, DATA_ADS, STATE_KEY_STATE, AdsEntity DEFAULT_NAME = "ADS Switch" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SWITCH_PLATFORM_SCHEMA.extend( { vol.Required(CONF_ADS_VAR): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/anel_pwrctrl/switch.py b/homeassistant/components/anel_pwrctrl/switch.py index 94cd0a59398..6b27a61e065 100644 --- a/homeassistant/components/anel_pwrctrl/switch.py +++ b/homeassistant/components/anel_pwrctrl/switch.py @@ -9,7 +9,10 @@ from typing import Any from anel_pwrctrl import Device, DeviceMaster, Switch import voluptuous as vol -from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity +from homeassistant.components.switch import ( + PLATFORM_SCHEMA as SWITCH_PLATFORM_SCHEMA, + SwitchEntity, +) from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -24,7 +27,7 @@ CONF_PORT_SEND = "port_send" MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=5) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SWITCH_PLATFORM_SCHEMA.extend( { vol.Required(CONF_PORT_RECV): cv.port, vol.Required(CONF_PORT_SEND): cv.port, diff --git a/homeassistant/components/aqualogic/switch.py b/homeassistant/components/aqualogic/switch.py index 0f1a7e34b3c..ed0cc463263 100644 --- a/homeassistant/components/aqualogic/switch.py +++ b/homeassistant/components/aqualogic/switch.py @@ -7,7 +7,10 @@ from typing import Any from aqualogic.core import States import voluptuous as vol -from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity +from homeassistant.components.switch import ( + PLATFORM_SCHEMA as SWITCH_PLATFORM_SCHEMA, + SwitchEntity, +) from homeassistant.const import CONF_MONITORED_CONDITIONS from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -30,7 +33,7 @@ SWITCH_TYPES = { "aux_7": "Aux 7", } -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SWITCH_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_MONITORED_CONDITIONS, default=list(SWITCH_TYPES)): vol.All( cv.ensure_list, [vol.In(SWITCH_TYPES)] diff --git a/homeassistant/components/arest/switch.py b/homeassistant/components/arest/switch.py index 4b15e6726fe..bcdba36cb58 100644 --- a/homeassistant/components/arest/switch.py +++ b/homeassistant/components/arest/switch.py @@ -9,7 +9,10 @@ from typing import Any import requests import voluptuous as vol -from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity +from homeassistant.components.switch import ( + PLATFORM_SCHEMA as SWITCH_PLATFORM_SCHEMA, + SwitchEntity, +) from homeassistant.const import CONF_NAME, CONF_RESOURCE from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -31,7 +34,7 @@ PIN_FUNCTION_SCHEMA = vol.Schema( } ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SWITCH_PLATFORM_SCHEMA.extend( { vol.Required(CONF_RESOURCE): cv.url, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/aten_pe/switch.py b/homeassistant/components/aten_pe/switch.py index 1349014d8fb..39b18089284 100644 --- a/homeassistant/components/aten_pe/switch.py +++ b/homeassistant/components/aten_pe/switch.py @@ -9,7 +9,7 @@ from atenpdu import AtenPE, AtenPEError import voluptuous as vol from homeassistant.components.switch import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SWITCH_PLATFORM_SCHEMA, SwitchDeviceClass, SwitchEntity, ) @@ -30,7 +30,7 @@ DEFAULT_COMMUNITY = "private" DEFAULT_PORT = "161" DEFAULT_USERNAME = "administrator" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SWITCH_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, diff --git a/homeassistant/components/digital_ocean/switch.py b/homeassistant/components/digital_ocean/switch.py index a01965e3667..856c9301cfd 100644 --- a/homeassistant/components/digital_ocean/switch.py +++ b/homeassistant/components/digital_ocean/switch.py @@ -7,7 +7,10 @@ from typing import Any import voluptuous as vol -from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity +from homeassistant.components.switch import ( + PLATFORM_SCHEMA as SWITCH_PLATFORM_SCHEMA, + SwitchEntity, +) from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -32,7 +35,7 @@ _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = "Droplet" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SWITCH_PLATFORM_SCHEMA.extend( {vol.Required(CONF_DROPLETS): vol.All(cv.ensure_list, [cv.string])} ) diff --git a/homeassistant/components/edimax/switch.py b/homeassistant/components/edimax/switch.py index 61f3e6f4538..e0d063eb9fd 100644 --- a/homeassistant/components/edimax/switch.py +++ b/homeassistant/components/edimax/switch.py @@ -7,7 +7,10 @@ from typing import Any from pyedimax.smartplug import SmartPlug import voluptuous as vol -from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity +from homeassistant.components.switch import ( + PLATFORM_SCHEMA as SWITCH_PLATFORM_SCHEMA, + SwitchEntity, +) from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -20,7 +23,7 @@ DEFAULT_NAME = "Edimax Smart Plug" DEFAULT_PASSWORD = "1234" DEFAULT_USERNAME = "admin" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SWITCH_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/enocean/switch.py b/homeassistant/components/enocean/switch.py index 4fa75ff9712..9bf8b8e775c 100644 --- a/homeassistant/components/enocean/switch.py +++ b/homeassistant/components/enocean/switch.py @@ -7,7 +7,10 @@ from typing import Any from enocean.utils import combine_hex import voluptuous as vol -from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity +from homeassistant.components.switch import ( + PLATFORM_SCHEMA as SWITCH_PLATFORM_SCHEMA, + SwitchEntity, +) from homeassistant.const import CONF_ID, CONF_NAME, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv, entity_registry as er @@ -20,7 +23,7 @@ from .device import EnOceanEntity CONF_CHANNEL = "channel" DEFAULT_NAME = "EnOcean Switch" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SWITCH_PLATFORM_SCHEMA.extend( { vol.Required(CONF_ID): vol.All(cv.ensure_list, [vol.Coerce(int)]), vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/gc100/switch.py b/homeassistant/components/gc100/switch.py index ea90dde6abf..1bcdc7365cf 100644 --- a/homeassistant/components/gc100/switch.py +++ b/homeassistant/components/gc100/switch.py @@ -6,7 +6,10 @@ from typing import Any import voluptuous as vol -from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity +from homeassistant.components.switch import ( + PLATFORM_SCHEMA as SWITCH_PLATFORM_SCHEMA, + SwitchEntity, +) from homeassistant.const import DEVICE_DEFAULT_NAME from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -17,7 +20,7 @@ from . import CONF_PORTS, DATA_GC100 _SWITCH_SCHEMA = vol.Schema({cv.string: cv.string}) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SWITCH_PLATFORM_SCHEMA.extend( {vol.Required(CONF_PORTS): vol.All(cv.ensure_list, [_SWITCH_SCHEMA])} ) diff --git a/homeassistant/components/group/switch.py b/homeassistant/components/group/switch.py index 7be6b188e72..9db264c8041 100644 --- a/homeassistant/components/group/switch.py +++ b/homeassistant/components/group/switch.py @@ -7,7 +7,11 @@ from typing import Any import voluptuous as vol -from homeassistant.components.switch import DOMAIN, PLATFORM_SCHEMA, SwitchEntity +from homeassistant.components.switch import ( + DOMAIN, + PLATFORM_SCHEMA as SWITCH_PLATFORM_SCHEMA, + SwitchEntity, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_ENTITY_ID, @@ -33,7 +37,7 @@ CONF_ALL = "all" # No limit on parallel updates to enable a group calling another group PARALLEL_UPDATES = 0 -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SWITCH_PLATFORM_SCHEMA.extend( { vol.Required(CONF_ENTITIES): cv.entities_domain(DOMAIN), vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/hikvisioncam/switch.py b/homeassistant/components/hikvisioncam/switch.py index c455fcb5bbc..653d5a07174 100644 --- a/homeassistant/components/hikvisioncam/switch.py +++ b/homeassistant/components/hikvisioncam/switch.py @@ -9,7 +9,10 @@ import hikvision.api from hikvision.error import HikvisionError, MissingParamError import voluptuous as vol -from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity +from homeassistant.components.switch import ( + PLATFORM_SCHEMA as SWITCH_PLATFORM_SCHEMA, + SwitchEntity, +) from homeassistant.const import ( CONF_HOST, CONF_NAME, @@ -33,7 +36,7 @@ DEFAULT_PASSWORD = "12345" DEFAULT_PORT = 80 DEFAULT_USERNAME = "admin" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SWITCH_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/kankun/switch.py b/homeassistant/components/kankun/switch.py index f650494b3b1..a86bed5eb9a 100644 --- a/homeassistant/components/kankun/switch.py +++ b/homeassistant/components/kankun/switch.py @@ -8,7 +8,10 @@ from typing import Any import requests import voluptuous as vol -from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity +from homeassistant.components.switch import ( + PLATFORM_SCHEMA as SWITCH_PLATFORM_SCHEMA, + SwitchEntity, +) from homeassistant.const import ( CONF_HOST, CONF_NAME, @@ -39,7 +42,7 @@ SWITCH_SCHEMA = vol.Schema( } ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SWITCH_PLATFORM_SCHEMA.extend( {vol.Required(CONF_SWITCHES): cv.schema_with_slug_keys(SWITCH_SCHEMA)} ) diff --git a/homeassistant/components/linode/switch.py b/homeassistant/components/linode/switch.py index f2665671c0b..abaf77648ef 100644 --- a/homeassistant/components/linode/switch.py +++ b/homeassistant/components/linode/switch.py @@ -7,7 +7,10 @@ from typing import Any import voluptuous as vol -from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity +from homeassistant.components.switch import ( + PLATFORM_SCHEMA as SWITCH_PLATFORM_SCHEMA, + SwitchEntity, +) from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -30,7 +33,7 @@ _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = "Node" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SWITCH_PLATFORM_SCHEMA.extend( {vol.Required(CONF_NODES): vol.All(cv.ensure_list, [cv.string])} ) diff --git a/homeassistant/components/mfi/switch.py b/homeassistant/components/mfi/switch.py index fe0aeb902ee..833a2c21301 100644 --- a/homeassistant/components/mfi/switch.py +++ b/homeassistant/components/mfi/switch.py @@ -9,7 +9,10 @@ from mficlient.client import FailedToLogin, MFiClient import requests import voluptuous as vol -from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity +from homeassistant.components.switch import ( + PLATFORM_SCHEMA as SWITCH_PLATFORM_SCHEMA, + SwitchEntity, +) from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, @@ -30,7 +33,7 @@ DEFAULT_VERIFY_SSL = True SWITCH_MODELS = ["Outlet", "Output 5v", "Output 12v", "Output 24v", "Dimmer Switch"] -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SWITCH_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Required(CONF_USERNAME): cv.string, diff --git a/homeassistant/components/netio/switch.py b/homeassistant/components/netio/switch.py index 4cc77e44ec4..f5627f5e56b 100644 --- a/homeassistant/components/netio/switch.py +++ b/homeassistant/components/netio/switch.py @@ -12,7 +12,10 @@ import voluptuous as vol from homeassistant import util from homeassistant.components.http import HomeAssistantView -from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity +from homeassistant.components.switch import ( + PLATFORM_SCHEMA as SWITCH_PLATFORM_SCHEMA, + SwitchEntity, +) from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, @@ -44,7 +47,7 @@ REQ_CONF = [CONF_HOST, CONF_OUTLETS] URL_API_NETIO_EP = "/api/netio/{host}" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SWITCH_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Required(CONF_PORT, default=DEFAULT_PORT): cv.port, diff --git a/homeassistant/components/orvibo/switch.py b/homeassistant/components/orvibo/switch.py index ece833b7036..34bf63aaaab 100644 --- a/homeassistant/components/orvibo/switch.py +++ b/homeassistant/components/orvibo/switch.py @@ -8,7 +8,10 @@ from typing import Any from orvibo.s20 import S20, S20Exception, discover import voluptuous as vol -from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity +from homeassistant.components.switch import ( + PLATFORM_SCHEMA as SWITCH_PLATFORM_SCHEMA, + SwitchEntity, +) from homeassistant.const import ( CONF_DISCOVERY, CONF_HOST, @@ -26,7 +29,7 @@ _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = "Orvibo S20 Switch" DEFAULT_DISCOVERY = True -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SWITCH_PLATFORM_SCHEMA.extend( { vol.Required(CONF_SWITCHES, default=[]): vol.All( cv.ensure_list, diff --git a/homeassistant/components/pencom/switch.py b/homeassistant/components/pencom/switch.py index a1ec25a58e9..d16c7e1600c 100644 --- a/homeassistant/components/pencom/switch.py +++ b/homeassistant/components/pencom/switch.py @@ -8,7 +8,10 @@ from typing import Any from pencompy.pencompy import Pencompy import voluptuous as vol -from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity +from homeassistant.components.switch import ( + PLATFORM_SCHEMA as SWITCH_PLATFORM_SCHEMA, + SwitchEntity, +) from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT from homeassistant.core import HomeAssistant from homeassistant.exceptions import PlatformNotReady @@ -31,7 +34,7 @@ RELAY_SCHEMA = vol.Schema( } ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SWITCH_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Required(CONF_PORT): cv.port, diff --git a/homeassistant/components/pilight/switch.py b/homeassistant/components/pilight/switch.py index 0d0023d9cd6..5be63064b4a 100644 --- a/homeassistant/components/pilight/switch.py +++ b/homeassistant/components/pilight/switch.py @@ -4,7 +4,10 @@ from __future__ import annotations import voluptuous as vol -from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity +from homeassistant.components.switch import ( + PLATFORM_SCHEMA as SWITCH_PLATFORM_SCHEMA, + SwitchEntity, +) from homeassistant.const import CONF_SWITCHES from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -13,7 +16,7 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .base_class import SWITCHES_SCHEMA, PilightBaseDevice -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SWITCH_PLATFORM_SCHEMA.extend( {vol.Required(CONF_SWITCHES): vol.Schema({cv.string: SWITCHES_SCHEMA})} ) diff --git a/homeassistant/components/pulseaudio_loopback/switch.py b/homeassistant/components/pulseaudio_loopback/switch.py index 553a1b4a283..4ab1f905068 100644 --- a/homeassistant/components/pulseaudio_loopback/switch.py +++ b/homeassistant/components/pulseaudio_loopback/switch.py @@ -8,7 +8,10 @@ from typing import Any from pulsectl import Pulse, PulseError import voluptuous as vol -from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity +from homeassistant.components.switch import ( + PLATFORM_SCHEMA as SWITCH_PLATFORM_SCHEMA, + SwitchEntity, +) from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -27,7 +30,7 @@ DEFAULT_PORT = 4713 IGNORED_SWITCH_WARN = "Switch is already in the desired state. Ignoring." -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SWITCH_PLATFORM_SCHEMA.extend( { vol.Required(CONF_SINK_NAME): cv.string, vol.Required(CONF_SOURCE_NAME): cv.string, diff --git a/homeassistant/components/raincloud/switch.py b/homeassistant/components/raincloud/switch.py index d901f862133..45d0b4f0fc5 100644 --- a/homeassistant/components/raincloud/switch.py +++ b/homeassistant/components/raincloud/switch.py @@ -7,7 +7,10 @@ from typing import Any import voluptuous as vol -from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity +from homeassistant.components.switch import ( + PLATFORM_SCHEMA as SWITCH_PLATFORM_SCHEMA, + SwitchEntity, +) from homeassistant.const import CONF_MONITORED_CONDITIONS from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -25,7 +28,7 @@ from . import ( _LOGGER = logging.getLogger(__name__) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SWITCH_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_MONITORED_CONDITIONS, default=list(SWITCHES)): vol.All( cv.ensure_list, [vol.In(SWITCHES)] diff --git a/homeassistant/components/raspyrfm/switch.py b/homeassistant/components/raspyrfm/switch.py index ce69818beec..37835ecb40a 100644 --- a/homeassistant/components/raspyrfm/switch.py +++ b/homeassistant/components/raspyrfm/switch.py @@ -13,7 +13,10 @@ from raspyrfm_client.device_implementations.gateway.manufacturer.gateway_constan from raspyrfm_client.device_implementations.manufacturer_constants import Manufacturer import voluptuous as vol -from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity +from homeassistant.components.switch import ( + PLATFORM_SCHEMA as SWITCH_PLATFORM_SCHEMA, + SwitchEntity, +) from homeassistant.const import ( CONF_HOST, CONF_NAME, @@ -34,7 +37,7 @@ CONF_CHANNEL_CONFIG = "channel_config" DEFAULT_HOST = "127.0.0.1" # define configuration parameters -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SWITCH_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, vol.Optional(CONF_PORT): cv.port, diff --git a/homeassistant/components/recswitch/switch.py b/homeassistant/components/recswitch/switch.py index a0035d50582..78fc0a805f6 100644 --- a/homeassistant/components/recswitch/switch.py +++ b/homeassistant/components/recswitch/switch.py @@ -8,7 +8,10 @@ from typing import Any from pyrecswitch import RSNetwork, RSNetworkError import voluptuous as vol -from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity +from homeassistant.components.switch import ( + PLATFORM_SCHEMA as SWITCH_PLATFORM_SCHEMA, + SwitchEntity, +) from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -21,7 +24,7 @@ DEFAULT_NAME = "RecSwitch {0}" DATA_RSN = "RSN" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SWITCH_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Required(CONF_MAC): vol.All(cv.string, vol.Upper), diff --git a/homeassistant/components/remote_rpi_gpio/switch.py b/homeassistant/components/remote_rpi_gpio/switch.py index 756e9dcfce9..ff9ecbcd97b 100644 --- a/homeassistant/components/remote_rpi_gpio/switch.py +++ b/homeassistant/components/remote_rpi_gpio/switch.py @@ -6,7 +6,10 @@ from typing import Any import voluptuous as vol -from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity +from homeassistant.components.switch import ( + PLATFORM_SCHEMA as SWITCH_PLATFORM_SCHEMA, + SwitchEntity, +) from homeassistant.const import CONF_HOST, DEVICE_DEFAULT_NAME from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -20,7 +23,7 @@ CONF_PORTS = "ports" _SENSORS_SCHEMA = vol.Schema({cv.positive_int: cv.string}) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SWITCH_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Required(CONF_PORTS): _SENSORS_SCHEMA, diff --git a/homeassistant/components/rest/switch.py b/homeassistant/components/rest/switch.py index 99aadce6620..d01aab2cf9f 100644 --- a/homeassistant/components/rest/switch.py +++ b/homeassistant/components/rest/switch.py @@ -11,7 +11,7 @@ import voluptuous as vol from homeassistant.components.switch import ( DEVICE_CLASSES_SCHEMA, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SWITCH_PLATFORM_SCHEMA, SwitchEntity, ) from homeassistant.const import ( @@ -64,7 +64,7 @@ DEFAULT_VERIFY_SSL = True SUPPORT_REST_METHODS = ["post", "put", "patch"] -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SWITCH_PLATFORM_SCHEMA.extend( { **TEMPLATE_ENTITY_BASE_SCHEMA.schema, vol.Required(CONF_RESOURCE): cv.url, diff --git a/homeassistant/components/rflink/switch.py b/homeassistant/components/rflink/switch.py index fdf8f63ab7d..af4bbc43700 100644 --- a/homeassistant/components/rflink/switch.py +++ b/homeassistant/components/rflink/switch.py @@ -4,7 +4,10 @@ from __future__ import annotations import voluptuous as vol -from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity +from homeassistant.components.switch import ( + PLATFORM_SCHEMA as SWITCH_PLATFORM_SCHEMA, + SwitchEntity, +) from homeassistant.const import CONF_DEVICES, CONF_NAME from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -25,7 +28,7 @@ from . import ( PARALLEL_UPDATES = 0 -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SWITCH_PLATFORM_SCHEMA.extend( { vol.Optional( CONF_DEVICE_DEFAULTS, default=DEVICE_DEFAULTS_SCHEMA({}) diff --git a/homeassistant/components/scsgate/switch.py b/homeassistant/components/scsgate/switch.py index 8ad31106cf7..abc906a5533 100644 --- a/homeassistant/components/scsgate/switch.py +++ b/homeassistant/components/scsgate/switch.py @@ -9,7 +9,10 @@ from scsgate.messages import ScenarioTriggeredMessage, StateMessage from scsgate.tasks import ToggleStatusTask import voluptuous as vol -from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity +from homeassistant.components.switch import ( + PLATFORM_SCHEMA as SWITCH_PLATFORM_SCHEMA, + SwitchEntity, +) from homeassistant.const import ATTR_ENTITY_ID, ATTR_STATE, CONF_DEVICES, CONF_NAME from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -23,7 +26,7 @@ ATTR_SCENARIO_ID = "scenario_id" CONF_TRADITIONAL = "traditional" CONF_SCENARIO = "scenario" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SWITCH_PLATFORM_SCHEMA.extend( {vol.Required(CONF_DEVICES): cv.schema_with_slug_keys(SCSGATE_SCHEMA)} ) diff --git a/homeassistant/components/snmp/switch.py b/homeassistant/components/snmp/switch.py index 02a94aeb8c1..e3ce09cbf48 100644 --- a/homeassistant/components/snmp/switch.py +++ b/homeassistant/components/snmp/switch.py @@ -31,7 +31,10 @@ from pysnmp.proto.rfc1902 import ( ) import voluptuous as vol -from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity +from homeassistant.components.switch import ( + PLATFORM_SCHEMA as SWITCH_PLATFORM_SCHEMA, + SwitchEntity, +) from homeassistant.const import ( CONF_HOST, CONF_NAME, @@ -98,7 +101,7 @@ MAP_SNMP_VARTYPES = { "Unsigned32": Unsigned32, } -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SWITCH_PLATFORM_SCHEMA.extend( { vol.Required(CONF_BASEOID): cv.string, vol.Optional(CONF_COMMAND_OID): cv.string, diff --git a/homeassistant/components/sony_projector/switch.py b/homeassistant/components/sony_projector/switch.py index 7ecff46d3bd..e018c06e050 100644 --- a/homeassistant/components/sony_projector/switch.py +++ b/homeassistant/components/sony_projector/switch.py @@ -8,7 +8,10 @@ from typing import Any import pysdcp import voluptuous as vol -from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity +from homeassistant.components.switch import ( + PLATFORM_SCHEMA as SWITCH_PLATFORM_SCHEMA, + SwitchEntity, +) from homeassistant.const import CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -19,7 +22,7 @@ _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = "Sony Projector" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SWITCH_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/switchmate/switch.py b/homeassistant/components/switchmate/switch.py index ee8b65b47e2..8484eb5a2d1 100644 --- a/homeassistant/components/switchmate/switch.py +++ b/homeassistant/components/switchmate/switch.py @@ -8,7 +8,10 @@ from typing import Any from switchmate import Switchmate import voluptuous as vol -from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity +from homeassistant.components.switch import ( + PLATFORM_SCHEMA as SWITCH_PLATFORM_SCHEMA, + SwitchEntity, +) from homeassistant.const import CONF_MAC, CONF_NAME from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -20,7 +23,7 @@ DEFAULT_NAME = "Switchmate" SCAN_INTERVAL = timedelta(minutes=30) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SWITCH_PLATFORM_SCHEMA.extend( { vol.Required(CONF_MAC): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/telnet/switch.py b/homeassistant/components/telnet/switch.py index 6a6f758ff79..805f037dbae 100644 --- a/homeassistant/components/telnet/switch.py +++ b/homeassistant/components/telnet/switch.py @@ -11,7 +11,7 @@ import voluptuous as vol from homeassistant.components.switch import ( ENTITY_ID_FORMAT, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SWITCH_PLATFORM_SCHEMA, SwitchEntity, ) from homeassistant.const import ( @@ -49,7 +49,7 @@ SWITCH_SCHEMA = vol.Schema( } ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SWITCH_PLATFORM_SCHEMA.extend( {vol.Required(CONF_SWITCHES): cv.schema_with_slug_keys(SWITCH_SCHEMA)} ) diff --git a/homeassistant/components/template/switch.py b/homeassistant/components/template/switch.py index f585cd929c0..3a7cfcde0f7 100644 --- a/homeassistant/components/template/switch.py +++ b/homeassistant/components/template/switch.py @@ -8,7 +8,7 @@ import voluptuous as vol from homeassistant.components.switch import ( ENTITY_ID_FORMAT, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SWITCH_PLATFORM_SCHEMA, SwitchEntity, ) from homeassistant.const import ( @@ -55,7 +55,7 @@ SWITCH_SCHEMA = vol.All( ).extend(TEMPLATE_ENTITY_COMMON_SCHEMA_LEGACY.schema), ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SWITCH_PLATFORM_SCHEMA.extend( {vol.Required(CONF_SWITCHES): cv.schema_with_slug_keys(SWITCH_SCHEMA)} ) diff --git a/homeassistant/components/thinkingcleaner/switch.py b/homeassistant/components/thinkingcleaner/switch.py index f99cda4347a..76c7cdb0db2 100644 --- a/homeassistant/components/thinkingcleaner/switch.py +++ b/homeassistant/components/thinkingcleaner/switch.py @@ -11,7 +11,7 @@ import voluptuous as vol from homeassistant import util from homeassistant.components.switch import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SWITCH_PLATFORM_SCHEMA, SwitchEntity, SwitchEntityDescription, ) @@ -42,7 +42,7 @@ SWITCH_TYPES: tuple[SwitchEntityDescription, ...] = ( ), ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({vol.Optional(CONF_HOST): cv.string}) +PLATFORM_SCHEMA = SWITCH_PLATFORM_SCHEMA.extend({vol.Optional(CONF_HOST): cv.string}) def setup_platform( diff --git a/homeassistant/components/vultr/switch.py b/homeassistant/components/vultr/switch.py index 6758748b9f3..b03d613895a 100644 --- a/homeassistant/components/vultr/switch.py +++ b/homeassistant/components/vultr/switch.py @@ -7,7 +7,10 @@ from typing import Any import voluptuous as vol -from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity +from homeassistant.components.switch import ( + PLATFORM_SCHEMA as SWITCH_PLATFORM_SCHEMA, + SwitchEntity, +) from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -35,7 +38,7 @@ from . import ( _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = "Vultr {}" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SWITCH_PLATFORM_SCHEMA.extend( { vol.Required(CONF_SUBSCRIPTION): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/wake_on_lan/switch.py b/homeassistant/components/wake_on_lan/switch.py index e5c3a055310..cf38d05de38 100644 --- a/homeassistant/components/wake_on_lan/switch.py +++ b/homeassistant/components/wake_on_lan/switch.py @@ -10,7 +10,7 @@ import voluptuous as vol import wakeonlan from homeassistant.components.switch import ( - PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SWITCH_PLATFORM_SCHEMA, SwitchEntity, ) from homeassistant.const import ( @@ -36,7 +36,7 @@ CONF_OFF_ACTION = "turn_off" DEFAULT_NAME = "Wake on LAN" DEFAULT_PING_TIMEOUT = 1 -PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SWITCH_PLATFORM_SCHEMA.extend( { vol.Required(CONF_MAC): cv.string, vol.Optional(CONF_BROADCAST_ADDRESS): cv.string, diff --git a/homeassistant/components/wirelesstag/switch.py b/homeassistant/components/wirelesstag/switch.py index 0eafea0699b..239461df4ea 100644 --- a/homeassistant/components/wirelesstag/switch.py +++ b/homeassistant/components/wirelesstag/switch.py @@ -7,7 +7,7 @@ from typing import Any import voluptuous as vol from homeassistant.components.switch import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SWITCH_PLATFORM_SCHEMA, SwitchEntity, SwitchEntityDescription, ) @@ -48,7 +48,7 @@ SWITCH_TYPES: tuple[SwitchEntityDescription, ...] = ( SWITCH_KEYS: list[str] = [desc.key for desc in SWITCH_TYPES] -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SWITCH_PLATFORM_SCHEMA.extend( { vol.Required(CONF_MONITORED_CONDITIONS, default=[]): vol.All( cv.ensure_list, [vol.In(SWITCH_KEYS)] diff --git a/homeassistant/components/zoneminder/switch.py b/homeassistant/components/zoneminder/switch.py index 48cbe58a876..23adf2f4c88 100644 --- a/homeassistant/components/zoneminder/switch.py +++ b/homeassistant/components/zoneminder/switch.py @@ -9,7 +9,10 @@ import voluptuous as vol from zoneminder.monitor import Monitor, MonitorState from zoneminder.zm import ZoneMinder -from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity +from homeassistant.components.switch import ( + PLATFORM_SCHEMA as SWITCH_PLATFORM_SCHEMA, + SwitchEntity, +) from homeassistant.const import CONF_COMMAND_OFF, CONF_COMMAND_ON from homeassistant.core import HomeAssistant from homeassistant.exceptions import PlatformNotReady @@ -21,7 +24,7 @@ from . import DOMAIN as ZONEMINDER_DOMAIN _LOGGER = logging.getLogger(__name__) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SWITCH_PLATFORM_SCHEMA.extend( { vol.Required(CONF_COMMAND_ON): cv.string, vol.Required(CONF_COMMAND_OFF): cv.string, From 6c81885caeeffded1e6fc699826d9c9fe569d1dd Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 26 Jun 2024 10:40:17 +0200 Subject: [PATCH 1234/1445] Force alias when importing calendar PLATFORM_SCHEMA (#120512) --- homeassistant/components/caldav/calendar.py | 4 ++-- homeassistant/components/todoist/calendar.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/caldav/calendar.py b/homeassistant/components/caldav/calendar.py index b9f967d1a08..7591722b1ab 100644 --- a/homeassistant/components/caldav/calendar.py +++ b/homeassistant/components/caldav/calendar.py @@ -10,7 +10,7 @@ import voluptuous as vol from homeassistant.components.calendar import ( ENTITY_ID_FORMAT, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as CALENDAR_PLATFORM_SCHEMA, CalendarEntity, CalendarEvent, is_offset_reached, @@ -48,7 +48,7 @@ CONFIG_ENTRY_DEFAULT_DAYS = 7 # Only allow VCALENDARs that support this component type SUPPORTED_COMPONENT = "VEVENT" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = CALENDAR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_URL): vol.Url(), vol.Optional(CONF_CALENDARS, default=[]): vol.All(cv.ensure_list, [cv.string]), diff --git a/homeassistant/components/todoist/calendar.py b/homeassistant/components/todoist/calendar.py index baa7103f7eb..1c6f40005c1 100644 --- a/homeassistant/components/todoist/calendar.py +++ b/homeassistant/components/todoist/calendar.py @@ -14,7 +14,7 @@ from todoist_api_python.models import Due, Label, Task import voluptuous as vol from homeassistant.components.calendar import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as CALENDAR_PLATFORM_SCHEMA, CalendarEntity, CalendarEvent, ) @@ -82,7 +82,7 @@ NEW_TASK_SERVICE_SCHEMA = vol.Schema( } ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = CALENDAR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_TOKEN): cv.string, vol.Optional(CONF_EXTRA_PROJECTS, default=[]): vol.All( From c96dc00a3a3d6c9e1a342c06b6cbb98e06336354 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 26 Jun 2024 10:41:46 +0200 Subject: [PATCH 1235/1445] Force alias when importing alarm control panel PLATFORM_SCHEMA (#120505) --- homeassistant/components/concord232/alarm_control_panel.py | 4 ++-- homeassistant/components/ifttt/alarm_control_panel.py | 4 ++-- homeassistant/components/nx584/alarm_control_panel.py | 4 ++-- homeassistant/components/template/alarm_control_panel.py | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/concord232/alarm_control_panel.py b/homeassistant/components/concord232/alarm_control_panel.py index 0256f5aab37..661a2beacc0 100644 --- a/homeassistant/components/concord232/alarm_control_panel.py +++ b/homeassistant/components/concord232/alarm_control_panel.py @@ -10,7 +10,7 @@ import requests import voluptuous as vol from homeassistant.components.alarm_control_panel import ( - PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, + PLATFORM_SCHEMA as ALARM_CONTROL_PANEL_PLATFORM_SCHEMA, AlarmControlPanelEntity, AlarmControlPanelEntityFeature, CodeFormat, @@ -39,7 +39,7 @@ DEFAULT_MODE = "audible" SCAN_INTERVAL = datetime.timedelta(seconds=10) -PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = ALARM_CONTROL_PANEL_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/ifttt/alarm_control_panel.py b/homeassistant/components/ifttt/alarm_control_panel.py index 81ed9320bcb..1af23d716c8 100644 --- a/homeassistant/components/ifttt/alarm_control_panel.py +++ b/homeassistant/components/ifttt/alarm_control_panel.py @@ -7,7 +7,7 @@ import logging import voluptuous as vol from homeassistant.components.alarm_control_panel import ( - PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, + PLATFORM_SCHEMA as ALARM_CONTROL_PANEL_PLATFORM_SCHEMA, AlarmControlPanelEntity, AlarmControlPanelEntityFeature, CodeFormat, @@ -54,7 +54,7 @@ DEFAULT_EVENT_DISARM = "alarm_disarm" CONF_CODE_ARM_REQUIRED = "code_arm_required" -PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = ALARM_CONTROL_PANEL_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_CODE): cv.string, diff --git a/homeassistant/components/nx584/alarm_control_panel.py b/homeassistant/components/nx584/alarm_control_panel.py index 2e306de5908..61de4f611b8 100644 --- a/homeassistant/components/nx584/alarm_control_panel.py +++ b/homeassistant/components/nx584/alarm_control_panel.py @@ -10,7 +10,7 @@ import requests import voluptuous as vol from homeassistant.components.alarm_control_panel import ( - PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, + PLATFORM_SCHEMA as ALARM_CONTROL_PANEL_PLATFORM_SCHEMA, AlarmControlPanelEntity, AlarmControlPanelEntityFeature, CodeFormat, @@ -41,7 +41,7 @@ SERVICE_BYPASS_ZONE = "bypass_zone" SERVICE_UNBYPASS_ZONE = "unbypass_zone" ATTR_ZONE = "zone" -PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = ALARM_CONTROL_PANEL_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/template/alarm_control_panel.py b/homeassistant/components/template/alarm_control_panel.py index 4a1af80e25c..2ac91d39858 100644 --- a/homeassistant/components/template/alarm_control_panel.py +++ b/homeassistant/components/template/alarm_control_panel.py @@ -9,7 +9,7 @@ import voluptuous as vol from homeassistant.components.alarm_control_panel import ( ENTITY_ID_FORMAT, - PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, + PLATFORM_SCHEMA as ALARM_CONTROL_PANEL_PLATFORM_SCHEMA, AlarmControlPanelEntity, AlarmControlPanelEntityFeature, CodeFormat, @@ -94,7 +94,7 @@ ALARM_CONTROL_PANEL_SCHEMA = vol.Schema( } ) -PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = ALARM_CONTROL_PANEL_PLATFORM_SCHEMA.extend( { vol.Required(CONF_ALARM_CONTROL_PANELS): cv.schema_with_slug_keys( ALARM_CONTROL_PANEL_SCHEMA From 8e598ec3ff9b6c96dfe633d5510762c4053279c3 Mon Sep 17 00:00:00 2001 From: "Mr. Bubbles" Date: Wed, 26 Jun 2024 10:50:32 +0200 Subject: [PATCH 1236/1445] Rename sensor to finished downloads in pyLoad integration (#120483) --- homeassistant/components/pyload/strings.json | 2 +- .../pyload/snapshots/test_sensor.ambr | 232 ++---------------- 2 files changed, 25 insertions(+), 209 deletions(-) diff --git a/homeassistant/components/pyload/strings.json b/homeassistant/components/pyload/strings.json index 0ed016aafb8..9f043ba224a 100644 --- a/homeassistant/components/pyload/strings.json +++ b/homeassistant/components/pyload/strings.json @@ -74,7 +74,7 @@ "name": "Downloads in queue" }, "total": { - "name": "Total downlods" + "name": "Finished downloads" }, "free_space": { "name": "Free space" diff --git a/tests/components/pyload/snapshots/test_sensor.ambr b/tests/components/pyload/snapshots/test_sensor.ambr index 8675fb696a5..159309041e0 100644 --- a/tests/components/pyload/snapshots/test_sensor.ambr +++ b/tests/components/pyload/snapshots/test_sensor.ambr @@ -91,7 +91,7 @@ 'state': 'unavailable', }) # --- -# name: test_sensor_update_exceptions[CannotConnect][sensor.pyload_downloads_total-entry] +# name: test_sensor_update_exceptions[CannotConnect][sensor.pyload_finished_downloads-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -103,7 +103,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.pyload_downloads_total', + 'entity_id': 'sensor.pyload_finished_downloads', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -115,7 +115,7 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Downloads total', + 'original_name': 'Finished downloads', 'platform': 'pyload', 'previous_unique_id': None, 'supported_features': 0, @@ -124,13 +124,13 @@ 'unit_of_measurement': None, }) # --- -# name: test_sensor_update_exceptions[CannotConnect][sensor.pyload_downloads_total-state] +# name: test_sensor_update_exceptions[CannotConnect][sensor.pyload_finished_downloads-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'pyLoad Downloads total', + 'friendly_name': 'pyLoad Finished downloads', }), 'context': , - 'entity_id': 'sensor.pyload_downloads_total', + 'entity_id': 'sensor.pyload_finished_downloads', 'last_changed': , 'last_reported': , 'last_updated': , @@ -245,52 +245,6 @@ 'state': 'unavailable', }) # --- -# name: test_sensor_update_exceptions[CannotConnect][sensor.pyload_total_downlods-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.pyload_total_downlods', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Total downlods', - 'platform': 'pyload', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': , - 'unique_id': 'XXXXXXXXXXXXXX_total', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensor_update_exceptions[CannotConnect][sensor.pyload_total_downlods-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'pyLoad Total downlods', - }), - 'context': , - 'entity_id': 'sensor.pyload_total_downlods', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unavailable', - }) -# --- # name: test_sensor_update_exceptions[InvalidAuth][sensor.pyload_active_downloads-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -383,7 +337,7 @@ 'state': 'unavailable', }) # --- -# name: test_sensor_update_exceptions[InvalidAuth][sensor.pyload_downloads_total-entry] +# name: test_sensor_update_exceptions[InvalidAuth][sensor.pyload_finished_downloads-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -395,7 +349,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.pyload_downloads_total', + 'entity_id': 'sensor.pyload_finished_downloads', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -407,7 +361,7 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Downloads total', + 'original_name': 'Finished downloads', 'platform': 'pyload', 'previous_unique_id': None, 'supported_features': 0, @@ -416,13 +370,13 @@ 'unit_of_measurement': None, }) # --- -# name: test_sensor_update_exceptions[InvalidAuth][sensor.pyload_downloads_total-state] +# name: test_sensor_update_exceptions[InvalidAuth][sensor.pyload_finished_downloads-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'pyLoad Downloads total', + 'friendly_name': 'pyLoad Finished downloads', }), 'context': , - 'entity_id': 'sensor.pyload_downloads_total', + 'entity_id': 'sensor.pyload_finished_downloads', 'last_changed': , 'last_reported': , 'last_updated': , @@ -537,52 +491,6 @@ 'state': 'unavailable', }) # --- -# name: test_sensor_update_exceptions[InvalidAuth][sensor.pyload_total_downlods-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.pyload_total_downlods', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Total downlods', - 'platform': 'pyload', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': , - 'unique_id': 'XXXXXXXXXXXXXX_total', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensor_update_exceptions[InvalidAuth][sensor.pyload_total_downlods-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'pyLoad Total downlods', - }), - 'context': , - 'entity_id': 'sensor.pyload_total_downlods', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unavailable', - }) -# --- # name: test_sensor_update_exceptions[ParserError][sensor.pyload_active_downloads-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -675,7 +583,7 @@ 'state': 'unavailable', }) # --- -# name: test_sensor_update_exceptions[ParserError][sensor.pyload_downloads_total-entry] +# name: test_sensor_update_exceptions[ParserError][sensor.pyload_finished_downloads-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -687,7 +595,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.pyload_downloads_total', + 'entity_id': 'sensor.pyload_finished_downloads', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -699,7 +607,7 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Downloads total', + 'original_name': 'Finished downloads', 'platform': 'pyload', 'previous_unique_id': None, 'supported_features': 0, @@ -708,13 +616,13 @@ 'unit_of_measurement': None, }) # --- -# name: test_sensor_update_exceptions[ParserError][sensor.pyload_downloads_total-state] +# name: test_sensor_update_exceptions[ParserError][sensor.pyload_finished_downloads-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'pyLoad Downloads total', + 'friendly_name': 'pyLoad Finished downloads', }), 'context': , - 'entity_id': 'sensor.pyload_downloads_total', + 'entity_id': 'sensor.pyload_finished_downloads', 'last_changed': , 'last_reported': , 'last_updated': , @@ -829,52 +737,6 @@ 'state': 'unavailable', }) # --- -# name: test_sensor_update_exceptions[ParserError][sensor.pyload_total_downlods-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.pyload_total_downlods', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Total downlods', - 'platform': 'pyload', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': , - 'unique_id': 'XXXXXXXXXXXXXX_total', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensor_update_exceptions[ParserError][sensor.pyload_total_downlods-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'pyLoad Total downlods', - }), - 'context': , - 'entity_id': 'sensor.pyload_total_downlods', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unavailable', - }) -# --- # name: test_setup[sensor.pyload_active_downloads-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -967,7 +829,7 @@ 'state': '6', }) # --- -# name: test_setup[sensor.pyload_downloads_total-entry] +# name: test_setup[sensor.pyload_finished_downloads-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -979,7 +841,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.pyload_downloads_total', + 'entity_id': 'sensor.pyload_finished_downloads', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -991,7 +853,7 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Downloads total', + 'original_name': 'Finished downloads', 'platform': 'pyload', 'previous_unique_id': None, 'supported_features': 0, @@ -1000,13 +862,13 @@ 'unit_of_measurement': None, }) # --- -# name: test_setup[sensor.pyload_downloads_total-state] +# name: test_setup[sensor.pyload_finished_downloads-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'pyLoad Downloads total', + 'friendly_name': 'pyLoad Finished downloads', }), 'context': , - 'entity_id': 'sensor.pyload_downloads_total', + 'entity_id': 'sensor.pyload_finished_downloads', 'last_changed': , 'last_reported': , 'last_updated': , @@ -1121,49 +983,3 @@ 'state': '43.247704', }) # --- -# name: test_setup[sensor.pyload_total_downlods-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.pyload_total_downlods', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Total downlods', - 'platform': 'pyload', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': , - 'unique_id': 'XXXXXXXXXXXXXX_total', - 'unit_of_measurement': None, - }) -# --- -# name: test_setup[sensor.pyload_total_downlods-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'pyLoad Total downlods', - }), - 'context': , - 'entity_id': 'sensor.pyload_total_downlods', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '37', - }) -# --- From 399130bc95d2b374bed8454a3e138cf68c4897a1 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 26 Jun 2024 10:54:19 +0200 Subject: [PATCH 1237/1445] Force alias when importing binary sensor PLATFORM_SCHEMA (#120510) --- homeassistant/components/ads/binary_sensor.py | 4 ++-- homeassistant/components/arest/binary_sensor.py | 4 ++-- homeassistant/components/bayesian/binary_sensor.py | 4 ++-- homeassistant/components/bloomsky/binary_sensor.py | 4 ++-- homeassistant/components/concord232/binary_sensor.py | 4 ++-- homeassistant/components/digital_ocean/binary_sensor.py | 4 ++-- homeassistant/components/enocean/binary_sensor.py | 4 ++-- homeassistant/components/ffmpeg_motion/binary_sensor.py | 4 ++-- homeassistant/components/ffmpeg_noise/binary_sensor.py | 4 ++-- homeassistant/components/flic/binary_sensor.py | 7 +++++-- homeassistant/components/gc100/binary_sensor.py | 7 +++++-- homeassistant/components/group/binary_sensor.py | 4 ++-- homeassistant/components/hikvision/binary_sensor.py | 4 ++-- homeassistant/components/linode/binary_sensor.py | 4 ++-- homeassistant/components/meteoalarm/binary_sensor.py | 4 ++-- homeassistant/components/nx584/binary_sensor.py | 4 ++-- homeassistant/components/pilight/binary_sensor.py | 7 +++++-- homeassistant/components/raincloud/binary_sensor.py | 7 +++++-- homeassistant/components/random/binary_sensor.py | 4 ++-- homeassistant/components/remote_rpi_gpio/binary_sensor.py | 7 +++++-- homeassistant/components/rflink/binary_sensor.py | 4 ++-- homeassistant/components/tapsaff/binary_sensor.py | 7 +++++-- homeassistant/components/tcp/binary_sensor.py | 4 ++-- homeassistant/components/template/binary_sensor.py | 4 ++-- homeassistant/components/threshold/binary_sensor.py | 4 ++-- homeassistant/components/tod/binary_sensor.py | 4 ++-- homeassistant/components/trend/binary_sensor.py | 4 ++-- homeassistant/components/vultr/binary_sensor.py | 4 ++-- homeassistant/components/w800rf32/binary_sensor.py | 4 ++-- homeassistant/components/wirelesstag/binary_sensor.py | 7 +++++-- 30 files changed, 81 insertions(+), 60 deletions(-) diff --git a/homeassistant/components/ads/binary_sensor.py b/homeassistant/components/ads/binary_sensor.py index 2da76382c51..6ee17e07f0f 100644 --- a/homeassistant/components/ads/binary_sensor.py +++ b/homeassistant/components/ads/binary_sensor.py @@ -7,7 +7,7 @@ import voluptuous as vol from homeassistant.components.binary_sensor import ( DEVICE_CLASSES_SCHEMA, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as BINARY_SENSOR_PLATFORM_SCHEMA, BinarySensorDeviceClass, BinarySensorEntity, ) @@ -20,7 +20,7 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import CONF_ADS_VAR, DATA_ADS, STATE_KEY_STATE, AdsEntity DEFAULT_NAME = "ADS binary sensor" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = BINARY_SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_ADS_VAR): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/arest/binary_sensor.py b/homeassistant/components/arest/binary_sensor.py index 71f1c081f2d..00d4d6bbf9b 100644 --- a/homeassistant/components/arest/binary_sensor.py +++ b/homeassistant/components/arest/binary_sensor.py @@ -11,7 +11,7 @@ import voluptuous as vol from homeassistant.components.binary_sensor import ( DEVICE_CLASSES_SCHEMA, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as BINARY_SENSOR_PLATFORM_SCHEMA, BinarySensorEntity, ) from homeassistant.const import CONF_DEVICE_CLASS, CONF_NAME, CONF_PIN, CONF_RESOURCE @@ -25,7 +25,7 @@ _LOGGER = logging.getLogger(__name__) MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = BINARY_SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_RESOURCE): cv.url, vol.Optional(CONF_NAME): cv.string, diff --git a/homeassistant/components/bayesian/binary_sensor.py b/homeassistant/components/bayesian/binary_sensor.py index 470732f36d2..192d7987311 100644 --- a/homeassistant/components/bayesian/binary_sensor.py +++ b/homeassistant/components/bayesian/binary_sensor.py @@ -11,7 +11,7 @@ from uuid import UUID import voluptuous as vol from homeassistant.components.binary_sensor import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as BINARY_SENSOR_PLATFORM_SCHEMA, BinarySensorDeviceClass, BinarySensorEntity, ) @@ -99,7 +99,7 @@ TEMPLATE_SCHEMA = vol.Schema( required=True, ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = BINARY_SENSOR_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_UNIQUE_ID): cv.string, diff --git a/homeassistant/components/bloomsky/binary_sensor.py b/homeassistant/components/bloomsky/binary_sensor.py index 3582b186013..12d55f971e1 100644 --- a/homeassistant/components/bloomsky/binary_sensor.py +++ b/homeassistant/components/bloomsky/binary_sensor.py @@ -5,7 +5,7 @@ from __future__ import annotations import voluptuous as vol from homeassistant.components.binary_sensor import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as BINARY_SENSOR_PLATFORM_SCHEMA, BinarySensorDeviceClass, BinarySensorEntity, ) @@ -19,7 +19,7 @@ from . import DOMAIN SENSOR_TYPES = {"Rain": BinarySensorDeviceClass.MOISTURE, "Night": None} -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = BINARY_SENSOR_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_MONITORED_CONDITIONS, default=list(SENSOR_TYPES)): vol.All( cv.ensure_list, [vol.In(SENSOR_TYPES)] diff --git a/homeassistant/components/concord232/binary_sensor.py b/homeassistant/components/concord232/binary_sensor.py index 79cf0c758e1..a1dcbc222f7 100644 --- a/homeassistant/components/concord232/binary_sensor.py +++ b/homeassistant/components/concord232/binary_sensor.py @@ -11,7 +11,7 @@ import voluptuous as vol from homeassistant.components.binary_sensor import ( DEVICE_CLASSES_SCHEMA as BINARY_SENSOR_DEVICE_CLASSES_SCHEMA, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as BINARY_SENSOR_PLATFORM_SCHEMA, BinarySensorDeviceClass, BinarySensorEntity, ) @@ -36,7 +36,7 @@ SCAN_INTERVAL = datetime.timedelta(seconds=10) ZONE_TYPES_SCHEMA = vol.Schema({cv.positive_int: BINARY_SENSOR_DEVICE_CLASSES_SCHEMA}) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = BINARY_SENSOR_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_EXCLUDE_ZONES, default=[]): vol.All( cv.ensure_list, [cv.positive_int] diff --git a/homeassistant/components/digital_ocean/binary_sensor.py b/homeassistant/components/digital_ocean/binary_sensor.py index 9218d9bde0e..0d4b31faa2c 100644 --- a/homeassistant/components/digital_ocean/binary_sensor.py +++ b/homeassistant/components/digital_ocean/binary_sensor.py @@ -7,7 +7,7 @@ import logging import voluptuous as vol from homeassistant.components.binary_sensor import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as BINARY_SENSOR_PLATFORM_SCHEMA, BinarySensorDeviceClass, BinarySensorEntity, ) @@ -34,7 +34,7 @@ from . import ( _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = "Droplet" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = BINARY_SENSOR_PLATFORM_SCHEMA.extend( {vol.Required(CONF_DROPLETS): vol.All(cv.ensure_list, [cv.string])} ) diff --git a/homeassistant/components/enocean/binary_sensor.py b/homeassistant/components/enocean/binary_sensor.py index 9ebedc52c00..3ecf1ba4ba2 100644 --- a/homeassistant/components/enocean/binary_sensor.py +++ b/homeassistant/components/enocean/binary_sensor.py @@ -7,7 +7,7 @@ import voluptuous as vol from homeassistant.components.binary_sensor import ( DEVICE_CLASSES_SCHEMA, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as BINARY_SENSOR_PLATFORM_SCHEMA, BinarySensorDeviceClass, BinarySensorEntity, ) @@ -23,7 +23,7 @@ DEFAULT_NAME = "EnOcean binary sensor" DEPENDENCIES = ["enocean"] EVENT_BUTTON_PRESSED = "button_pressed" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = BINARY_SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_ID): vol.All(cv.ensure_list, [vol.Coerce(int)]), vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/ffmpeg_motion/binary_sensor.py b/homeassistant/components/ffmpeg_motion/binary_sensor.py index a9e1de2ea05..7dc32fd96a3 100644 --- a/homeassistant/components/ffmpeg_motion/binary_sensor.py +++ b/homeassistant/components/ffmpeg_motion/binary_sensor.py @@ -9,7 +9,7 @@ import haffmpeg.sensor as ffmpeg_sensor import voluptuous as vol from homeassistant.components.binary_sensor import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as BINARY_SENSOR_PLATFORM_SCHEMA, BinarySensorDeviceClass, BinarySensorEntity, ) @@ -34,7 +34,7 @@ CONF_REPEAT_TIME = "repeat_time" DEFAULT_NAME = "FFmpeg Motion" DEFAULT_INIT_STATE = True -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = BINARY_SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_INPUT): cv.string, vol.Optional(CONF_INITIAL_STATE, default=DEFAULT_INIT_STATE): cv.boolean, diff --git a/homeassistant/components/ffmpeg_noise/binary_sensor.py b/homeassistant/components/ffmpeg_noise/binary_sensor.py index a434b4a9924..abbf77eba6b 100644 --- a/homeassistant/components/ffmpeg_noise/binary_sensor.py +++ b/homeassistant/components/ffmpeg_noise/binary_sensor.py @@ -8,7 +8,7 @@ import haffmpeg.sensor as ffmpeg_sensor import voluptuous as vol from homeassistant.components.binary_sensor import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as BINARY_SENSOR_PLATFORM_SCHEMA, BinarySensorDeviceClass, ) from homeassistant.components.ffmpeg import ( @@ -33,7 +33,7 @@ CONF_RESET = "reset" DEFAULT_NAME = "FFmpeg Noise" DEFAULT_INIT_STATE = True -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = BINARY_SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_INPUT): cv.string, vol.Optional(CONF_INITIAL_STATE, default=DEFAULT_INIT_STATE): cv.boolean, diff --git a/homeassistant/components/flic/binary_sensor.py b/homeassistant/components/flic/binary_sensor.py index b7f8bb0c854..fcfe4b6604f 100644 --- a/homeassistant/components/flic/binary_sensor.py +++ b/homeassistant/components/flic/binary_sensor.py @@ -8,7 +8,10 @@ import threading import pyflic import voluptuous as vol -from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorEntity +from homeassistant.components.binary_sensor import ( + PLATFORM_SCHEMA as BINARY_SENSOR_PLATFORM_SCHEMA, + BinarySensorEntity, +) from homeassistant.const import ( CONF_DISCOVERY, CONF_HOST, @@ -42,7 +45,7 @@ EVENT_DATA_ADDRESS = "button_address" EVENT_DATA_TYPE = "click_type" EVENT_DATA_QUEUED_TIME = "queued_time" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = BINARY_SENSOR_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, diff --git a/homeassistant/components/gc100/binary_sensor.py b/homeassistant/components/gc100/binary_sensor.py index a03eae509d9..55df72cc3b9 100644 --- a/homeassistant/components/gc100/binary_sensor.py +++ b/homeassistant/components/gc100/binary_sensor.py @@ -4,7 +4,10 @@ from __future__ import annotations import voluptuous as vol -from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorEntity +from homeassistant.components.binary_sensor import ( + PLATFORM_SCHEMA as BINARY_SENSOR_PLATFORM_SCHEMA, + BinarySensorEntity, +) from homeassistant.const import DEVICE_DEFAULT_NAME from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -15,7 +18,7 @@ from . import CONF_PORTS, DATA_GC100 _SENSORS_SCHEMA = vol.Schema({cv.string: cv.string}) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = BINARY_SENSOR_PLATFORM_SCHEMA.extend( {vol.Required(CONF_PORTS): vol.All(cv.ensure_list, [_SENSORS_SCHEMA])} ) diff --git a/homeassistant/components/group/binary_sensor.py b/homeassistant/components/group/binary_sensor.py index 3fbadfb156c..06c810c2643 100644 --- a/homeassistant/components/group/binary_sensor.py +++ b/homeassistant/components/group/binary_sensor.py @@ -9,7 +9,7 @@ import voluptuous as vol from homeassistant.components.binary_sensor import ( DEVICE_CLASSES_SCHEMA, DOMAIN as BINARY_SENSOR_DOMAIN, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as BINARY_SENSOR_PLATFORM_SCHEMA, BinarySensorDeviceClass, BinarySensorEntity, ) @@ -36,7 +36,7 @@ DEFAULT_NAME = "Binary Sensor Group" CONF_ALL = "all" REG_KEY = f"{BINARY_SENSOR_DOMAIN}_registry" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = BINARY_SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_ENTITIES): cv.entities_domain(BINARY_SENSOR_DOMAIN), vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/hikvision/binary_sensor.py b/homeassistant/components/hikvision/binary_sensor.py index 2e4af361b38..0656733db6b 100644 --- a/homeassistant/components/hikvision/binary_sensor.py +++ b/homeassistant/components/hikvision/binary_sensor.py @@ -9,7 +9,7 @@ from pyhik.hikvision import HikCamera import voluptuous as vol from homeassistant.components.binary_sensor import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as BINARY_SENSOR_PLATFORM_SCHEMA, BinarySensorDeviceClass, BinarySensorEntity, ) @@ -74,7 +74,7 @@ CUSTOMIZE_SCHEMA = vol.Schema( } ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = BINARY_SENSOR_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_NAME): cv.string, vol.Required(CONF_HOST): cv.string, diff --git a/homeassistant/components/linode/binary_sensor.py b/homeassistant/components/linode/binary_sensor.py index 02c7a1ef383..d0c49c7171b 100644 --- a/homeassistant/components/linode/binary_sensor.py +++ b/homeassistant/components/linode/binary_sensor.py @@ -7,7 +7,7 @@ import logging import voluptuous as vol from homeassistant.components.binary_sensor import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as BINARY_SENSOR_PLATFORM_SCHEMA, BinarySensorDeviceClass, BinarySensorEntity, ) @@ -32,7 +32,7 @@ from . import ( _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = "Node" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = BINARY_SENSOR_PLATFORM_SCHEMA.extend( {vol.Required(CONF_NODES): vol.All(cv.ensure_list, [cv.string])} ) diff --git a/homeassistant/components/meteoalarm/binary_sensor.py b/homeassistant/components/meteoalarm/binary_sensor.py index 8fb0ae5cdc8..3400ca52f50 100644 --- a/homeassistant/components/meteoalarm/binary_sensor.py +++ b/homeassistant/components/meteoalarm/binary_sensor.py @@ -9,7 +9,7 @@ from meteoalertapi import Meteoalert import voluptuous as vol from homeassistant.components.binary_sensor import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as BINARY_SENSOR_PLATFORM_SCHEMA, BinarySensorDeviceClass, BinarySensorEntity, ) @@ -32,7 +32,7 @@ DEFAULT_NAME = "meteoalarm" SCAN_INTERVAL = timedelta(minutes=5) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = BINARY_SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_COUNTRY): cv.string, vol.Required(CONF_PROVINCE): cv.string, diff --git a/homeassistant/components/nx584/binary_sensor.py b/homeassistant/components/nx584/binary_sensor.py index 429b517fce4..04e79716423 100644 --- a/homeassistant/components/nx584/binary_sensor.py +++ b/homeassistant/components/nx584/binary_sensor.py @@ -12,7 +12,7 @@ import voluptuous as vol from homeassistant.components.binary_sensor import ( DEVICE_CLASSES_SCHEMA as BINARY_SENSOR_DEVICE_CLASSES_SCHEMA, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as BINARY_SENSOR_PLATFORM_SCHEMA, BinarySensorDeviceClass, BinarySensorEntity, ) @@ -33,7 +33,7 @@ DEFAULT_SSL = False ZONE_TYPES_SCHEMA = vol.Schema({cv.positive_int: BINARY_SENSOR_DEVICE_CLASSES_SCHEMA}) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = BINARY_SENSOR_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_EXCLUDE_ZONES, default=[]): vol.All( cv.ensure_list, [cv.positive_int] diff --git a/homeassistant/components/pilight/binary_sensor.py b/homeassistant/components/pilight/binary_sensor.py index 0ddb2de4603..4d68748e0f7 100644 --- a/homeassistant/components/pilight/binary_sensor.py +++ b/homeassistant/components/pilight/binary_sensor.py @@ -6,7 +6,10 @@ import datetime import voluptuous as vol -from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorEntity +from homeassistant.components.binary_sensor import ( + PLATFORM_SCHEMA as BINARY_SENSOR_PLATFORM_SCHEMA, + BinarySensorEntity, +) from homeassistant.const import ( CONF_DISARM_AFTER_TRIGGER, CONF_NAME, @@ -27,7 +30,7 @@ CONF_VARIABLE = "variable" CONF_RESET_DELAY_SEC = "reset_delay_sec" DEFAULT_NAME = "Pilight Binary Sensor" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = BINARY_SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_VARIABLE): cv.string, vol.Required(CONF_PAYLOAD): vol.Schema(dict), diff --git a/homeassistant/components/raincloud/binary_sensor.py b/homeassistant/components/raincloud/binary_sensor.py index 8530323dad1..90ad36985ef 100644 --- a/homeassistant/components/raincloud/binary_sensor.py +++ b/homeassistant/components/raincloud/binary_sensor.py @@ -6,7 +6,10 @@ import logging import voluptuous as vol -from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorEntity +from homeassistant.components.binary_sensor import ( + PLATFORM_SCHEMA as BINARY_SENSOR_PLATFORM_SCHEMA, + BinarySensorEntity, +) from homeassistant.const import CONF_MONITORED_CONDITIONS from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -17,7 +20,7 @@ from . import BINARY_SENSORS, DATA_RAINCLOUD, ICON_MAP, RainCloudEntity _LOGGER = logging.getLogger(__name__) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = BINARY_SENSOR_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_MONITORED_CONDITIONS, default=list(BINARY_SENSORS)): vol.All( cv.ensure_list, [vol.In(BINARY_SENSORS)] diff --git a/homeassistant/components/random/binary_sensor.py b/homeassistant/components/random/binary_sensor.py index bbcf87630c5..9d33ad52692 100644 --- a/homeassistant/components/random/binary_sensor.py +++ b/homeassistant/components/random/binary_sensor.py @@ -10,7 +10,7 @@ import voluptuous as vol from homeassistant.components.binary_sensor import ( DEVICE_CLASSES_SCHEMA, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as BINARY_SENSOR_PLATFORM_SCHEMA, BinarySensorEntity, ) from homeassistant.config_entries import ConfigEntry @@ -22,7 +22,7 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType DEFAULT_NAME = "Random binary sensor" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = BINARY_SENSOR_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, diff --git a/homeassistant/components/remote_rpi_gpio/binary_sensor.py b/homeassistant/components/remote_rpi_gpio/binary_sensor.py index ad995614ed4..98ae7328bc5 100644 --- a/homeassistant/components/remote_rpi_gpio/binary_sensor.py +++ b/homeassistant/components/remote_rpi_gpio/binary_sensor.py @@ -5,7 +5,10 @@ from __future__ import annotations import requests import voluptuous as vol -from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorEntity +from homeassistant.components.binary_sensor import ( + PLATFORM_SCHEMA as BINARY_SENSOR_PLATFORM_SCHEMA, + BinarySensorEntity, +) from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -26,7 +29,7 @@ CONF_PORTS = "ports" _SENSORS_SCHEMA = vol.Schema({cv.positive_int: cv.string}) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = BINARY_SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Required(CONF_PORTS): _SENSORS_SCHEMA, diff --git a/homeassistant/components/rflink/binary_sensor.py b/homeassistant/components/rflink/binary_sensor.py index 789e25c62b1..b731037fbfc 100644 --- a/homeassistant/components/rflink/binary_sensor.py +++ b/homeassistant/components/rflink/binary_sensor.py @@ -8,7 +8,7 @@ import voluptuous as vol from homeassistant.components.binary_sensor import ( DEVICE_CLASSES_SCHEMA, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as BINARY_SENSOR_PLATFORM_SCHEMA, BinarySensorDeviceClass, BinarySensorEntity, ) @@ -31,7 +31,7 @@ from . import CONF_ALIASES, RflinkDevice CONF_OFF_DELAY = "off_delay" DEFAULT_FORCE_UPDATE = False -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = BINARY_SENSOR_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_DEVICES, default={}): { cv.string: vol.Schema( diff --git a/homeassistant/components/tapsaff/binary_sensor.py b/homeassistant/components/tapsaff/binary_sensor.py index a8b3c138db5..0eb612bdc8e 100644 --- a/homeassistant/components/tapsaff/binary_sensor.py +++ b/homeassistant/components/tapsaff/binary_sensor.py @@ -8,7 +8,10 @@ import logging from tapsaff import TapsAff import voluptuous as vol -from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorEntity +from homeassistant.components.binary_sensor import ( + PLATFORM_SCHEMA as BINARY_SENSOR_PLATFORM_SCHEMA, + BinarySensorEntity, +) from homeassistant.const import CONF_LOCATION, CONF_NAME from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -21,7 +24,7 @@ DEFAULT_NAME = "Taps Aff" SCAN_INTERVAL = timedelta(minutes=30) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = BINARY_SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_LOCATION): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/tcp/binary_sensor.py b/homeassistant/components/tcp/binary_sensor.py index 3e432778910..638dfd53de5 100644 --- a/homeassistant/components/tcp/binary_sensor.py +++ b/homeassistant/components/tcp/binary_sensor.py @@ -5,7 +5,7 @@ from __future__ import annotations from typing import Final from homeassistant.components.binary_sensor import ( - PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, + PLATFORM_SCHEMA as BINARY_SENSOR_PLATFORM_SCHEMA, BinarySensorEntity, ) from homeassistant.core import HomeAssistant @@ -15,7 +15,7 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .common import TCP_PLATFORM_SCHEMA, TcpEntity from .const import CONF_VALUE_ON -PLATFORM_SCHEMA: Final = PARENT_PLATFORM_SCHEMA.extend(TCP_PLATFORM_SCHEMA) +PLATFORM_SCHEMA: Final = BINARY_SENSOR_PLATFORM_SCHEMA.extend(TCP_PLATFORM_SCHEMA) def setup_platform( diff --git a/homeassistant/components/template/binary_sensor.py b/homeassistant/components/template/binary_sensor.py index 0fa588a78f1..4618e30b1f3 100644 --- a/homeassistant/components/template/binary_sensor.py +++ b/homeassistant/components/template/binary_sensor.py @@ -14,7 +14,7 @@ from homeassistant.components.binary_sensor import ( DEVICE_CLASSES_SCHEMA, DOMAIN as BINARY_SENSOR_DOMAIN, ENTITY_ID_FORMAT, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as BINARY_SENSOR_PLATFORM_SCHEMA, BinarySensorEntity, ) from homeassistant.config_entries import ConfigEntry @@ -131,7 +131,7 @@ def rewrite_legacy_to_modern_conf(cfg: dict[str, dict]) -> list[dict]: return sensors -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = BINARY_SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_SENSORS): cv.schema_with_slug_keys( LEGACY_BINARY_SENSOR_SCHEMA diff --git a/homeassistant/components/threshold/binary_sensor.py b/homeassistant/components/threshold/binary_sensor.py index 8c3882ff360..a791658f049 100644 --- a/homeassistant/components/threshold/binary_sensor.py +++ b/homeassistant/components/threshold/binary_sensor.py @@ -10,7 +10,7 @@ import voluptuous as vol from homeassistant.components.binary_sensor import ( DEVICE_CLASSES_SCHEMA, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as BINARY_SENSOR_PLATFORM_SCHEMA, BinarySensorDeviceClass, BinarySensorEntity, ) @@ -60,7 +60,7 @@ TYPE_LOWER = "lower" TYPE_RANGE = "range" TYPE_UPPER = "upper" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = BINARY_SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_ENTITY_ID): cv.entity_id, vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, diff --git a/homeassistant/components/tod/binary_sensor.py b/homeassistant/components/tod/binary_sensor.py index 5b6c7077a97..907df849ea1 100644 --- a/homeassistant/components/tod/binary_sensor.py +++ b/homeassistant/components/tod/binary_sensor.py @@ -10,7 +10,7 @@ from typing import TYPE_CHECKING, Any, Literal, TypeGuard import voluptuous as vol from homeassistant.components.binary_sensor import ( - PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, + PLATFORM_SCHEMA as BINARY_SENSOR_PLATFORM_SCHEMA, BinarySensorEntity, ) from homeassistant.config_entries import ConfigEntry @@ -44,7 +44,7 @@ ATTR_AFTER = "after" ATTR_BEFORE = "before" ATTR_NEXT_UPDATE = "next_update" -PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = BINARY_SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_AFTER): vol.Any(cv.time, vol.All(vol.Lower, cv.sun_event)), vol.Required(CONF_BEFORE): vol.Any(cv.time, vol.All(vol.Lower, cv.sun_event)), diff --git a/homeassistant/components/trend/binary_sensor.py b/homeassistant/components/trend/binary_sensor.py index 6788d22219b..693c080e86e 100644 --- a/homeassistant/components/trend/binary_sensor.py +++ b/homeassistant/components/trend/binary_sensor.py @@ -14,7 +14,7 @@ import voluptuous as vol from homeassistant.components.binary_sensor import ( DEVICE_CLASSES_SCHEMA, ENTITY_ID_FORMAT, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as BINARY_SENSOR_PLATFORM_SCHEMA, BinarySensorDeviceClass, BinarySensorEntity, ) @@ -92,7 +92,7 @@ SENSOR_SCHEMA = vol.All( _validate_min_max, ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = BINARY_SENSOR_PLATFORM_SCHEMA.extend( {vol.Required(CONF_SENSORS): cv.schema_with_slug_keys(SENSOR_SCHEMA)} ) diff --git a/homeassistant/components/vultr/binary_sensor.py b/homeassistant/components/vultr/binary_sensor.py index 5c0db81e843..6a697eebe11 100644 --- a/homeassistant/components/vultr/binary_sensor.py +++ b/homeassistant/components/vultr/binary_sensor.py @@ -7,7 +7,7 @@ import logging import voluptuous as vol from homeassistant.components.binary_sensor import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as BINARY_SENSOR_PLATFORM_SCHEMA, BinarySensorDeviceClass, BinarySensorEntity, ) @@ -38,7 +38,7 @@ from . import ( _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = "Vultr {}" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = BINARY_SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_SUBSCRIPTION): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/w800rf32/binary_sensor.py b/homeassistant/components/w800rf32/binary_sensor.py index 49eec35cb1e..06e9e0dfdac 100644 --- a/homeassistant/components/w800rf32/binary_sensor.py +++ b/homeassistant/components/w800rf32/binary_sensor.py @@ -9,7 +9,7 @@ import W800rf32 as w800 from homeassistant.components.binary_sensor import ( DEVICE_CLASSES_SCHEMA, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as BINARY_SENSOR_PLATFORM_SCHEMA, BinarySensorEntity, ) from homeassistant.const import CONF_DEVICE_CLASS, CONF_DEVICES, CONF_NAME @@ -25,7 +25,7 @@ _LOGGER = logging.getLogger(__name__) CONF_OFF_DELAY = "off_delay" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = BINARY_SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_DEVICES): { cv.string: vol.Schema( diff --git a/homeassistant/components/wirelesstag/binary_sensor.py b/homeassistant/components/wirelesstag/binary_sensor.py index 85efab16e70..052f6547dd2 100644 --- a/homeassistant/components/wirelesstag/binary_sensor.py +++ b/homeassistant/components/wirelesstag/binary_sensor.py @@ -4,7 +4,10 @@ from __future__ import annotations import voluptuous as vol -from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorEntity +from homeassistant.components.binary_sensor import ( + PLATFORM_SCHEMA as BINARY_SENSOR_PLATFORM_SCHEMA, + BinarySensorEntity, +) from homeassistant.const import CONF_MONITORED_CONDITIONS, STATE_OFF, STATE_ON, Platform from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv @@ -65,7 +68,7 @@ SENSOR_TYPES = { } -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = BINARY_SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_MONITORED_CONDITIONS, default=[]): vol.All( cv.ensure_list, [vol.In(SENSOR_TYPES)] From e06d7050f2ddeccd75d0807569efc3492d1e4aa1 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 26 Jun 2024 10:55:09 +0200 Subject: [PATCH 1238/1445] Force alias when importing climate PLATFORM_SCHEMA (#120518) --- homeassistant/components/daikin/climate.py | 4 ++-- homeassistant/components/ephember/climate.py | 4 ++-- homeassistant/components/flexit/climate.py | 4 ++-- homeassistant/components/generic_thermostat/climate.py | 4 ++-- homeassistant/components/heatmiser/climate.py | 4 ++-- homeassistant/components/intesishome/climate.py | 4 ++-- homeassistant/components/oem/climate.py | 4 ++-- homeassistant/components/proliphix/climate.py | 4 ++-- homeassistant/components/schluter/climate.py | 4 ++-- homeassistant/components/tfiac/climate.py | 4 ++-- homeassistant/components/touchline/climate.py | 4 ++-- homeassistant/components/venstar/climate.py | 4 ++-- homeassistant/components/zhong_hong/climate.py | 4 ++-- 13 files changed, 26 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/daikin/climate.py b/homeassistant/components/daikin/climate.py index 34ae8701d5d..fc54d4b0427 100644 --- a/homeassistant/components/daikin/climate.py +++ b/homeassistant/components/daikin/climate.py @@ -12,7 +12,7 @@ from homeassistant.components.climate import ( ATTR_HVAC_MODE, ATTR_PRESET_MODE, ATTR_SWING_MODE, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as CLIMATE_PLATFORM_SCHEMA, PRESET_AWAY, PRESET_BOOST, PRESET_ECO, @@ -45,7 +45,7 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = CLIMATE_PLATFORM_SCHEMA.extend( {vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_NAME): cv.string} ) diff --git a/homeassistant/components/ephember/climate.py b/homeassistant/components/ephember/climate.py index 89d84a2c6fd..44e5986970d 100644 --- a/homeassistant/components/ephember/climate.py +++ b/homeassistant/components/ephember/climate.py @@ -20,7 +20,7 @@ from pyephember.pyephember import ( import voluptuous as vol from homeassistant.components.climate import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as CLIMATE_PLATFORM_SCHEMA, ClimateEntity, ClimateEntityFeature, HVACAction, @@ -44,7 +44,7 @@ SCAN_INTERVAL = timedelta(seconds=120) OPERATION_LIST = [HVACMode.HEAT_COOL, HVACMode.HEAT, HVACMode.OFF] -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = CLIMATE_PLATFORM_SCHEMA.extend( {vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string} ) diff --git a/homeassistant/components/flexit/climate.py b/homeassistant/components/flexit/climate.py index c15c74b4aac..d456fbef6fc 100644 --- a/homeassistant/components/flexit/climate.py +++ b/homeassistant/components/flexit/climate.py @@ -8,7 +8,7 @@ from typing import Any import voluptuous as vol from homeassistant.components.climate import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as CLIMATE_PLATFORM_SCHEMA, ClimateEntity, ClimateEntityFeature, HVACAction, @@ -36,7 +36,7 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType CALL_TYPE_WRITE_REGISTER = "write_register" CONF_HUB = "hub" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = CLIMATE_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_HUB, default=DEFAULT_HUB): cv.string, vol.Required(CONF_SLAVE): vol.All(int, vol.Range(min=0, max=32)), diff --git a/homeassistant/components/generic_thermostat/climate.py b/homeassistant/components/generic_thermostat/climate.py index 91ff1af122d..c080e8b82d7 100644 --- a/homeassistant/components/generic_thermostat/climate.py +++ b/homeassistant/components/generic_thermostat/climate.py @@ -13,7 +13,7 @@ import voluptuous as vol from homeassistant.components.climate import ( ATTR_PRESET_MODE, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as CLIMATE_PLATFORM_SCHEMA, PRESET_ACTIVITY, PRESET_AWAY, PRESET_COMFORT, @@ -125,7 +125,7 @@ PLATFORM_SCHEMA_COMMON = vol.Schema( ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(PLATFORM_SCHEMA_COMMON.schema) +PLATFORM_SCHEMA = CLIMATE_PLATFORM_SCHEMA.extend(PLATFORM_SCHEMA_COMMON.schema) async def async_setup_entry( diff --git a/homeassistant/components/heatmiser/climate.py b/homeassistant/components/heatmiser/climate.py index 8639d1f953e..f9f0cfacf60 100644 --- a/homeassistant/components/heatmiser/climate.py +++ b/homeassistant/components/heatmiser/climate.py @@ -9,7 +9,7 @@ from heatmiserV3 import connection, heatmiser import voluptuous as vol from homeassistant.components.climate import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as CLIMATE_PLATFORM_SCHEMA, ClimateEntity, ClimateEntityFeature, HVACMode, @@ -38,7 +38,7 @@ TSTATS_SCHEMA = vol.Schema( ) ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = CLIMATE_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Required(CONF_PORT): cv.string, diff --git a/homeassistant/components/intesishome/climate.py b/homeassistant/components/intesishome/climate.py index 7a504d7aced..82b653a34c7 100644 --- a/homeassistant/components/intesishome/climate.py +++ b/homeassistant/components/intesishome/climate.py @@ -11,7 +11,7 @@ import voluptuous as vol from homeassistant.components.climate import ( ATTR_HVAC_MODE, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as CLIMATE_PLATFORM_SCHEMA, PRESET_BOOST, PRESET_COMFORT, PRESET_ECO, @@ -44,7 +44,7 @@ IH_DEVICE_INTESISHOME = "IntesisHome" IH_DEVICE_AIRCONWITHME = "airconwithme" IH_DEVICE_ANYWAIR = "anywair" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = CLIMATE_PLATFORM_SCHEMA.extend( { vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, diff --git a/homeassistant/components/oem/climate.py b/homeassistant/components/oem/climate.py index 6c4b97ca450..cf16f1ba87e 100644 --- a/homeassistant/components/oem/climate.py +++ b/homeassistant/components/oem/climate.py @@ -9,7 +9,7 @@ import requests import voluptuous as vol from homeassistant.components.climate import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as CLIMATE_PLATFORM_SCHEMA, ClimateEntity, ClimateEntityFeature, HVACAction, @@ -29,7 +29,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = CLIMATE_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_NAME, default="Thermostat"): cv.string, diff --git a/homeassistant/components/proliphix/climate.py b/homeassistant/components/proliphix/climate.py index 23ccc03a038..18b974800a3 100644 --- a/homeassistant/components/proliphix/climate.py +++ b/homeassistant/components/proliphix/climate.py @@ -8,7 +8,7 @@ import proliphix import voluptuous as vol from homeassistant.components.climate import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as CLIMATE_PLATFORM_SCHEMA, ClimateEntity, ClimateEntityFeature, HVACAction, @@ -29,7 +29,7 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType ATTR_FAN = "fan" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = CLIMATE_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Required(CONF_USERNAME): cv.string, diff --git a/homeassistant/components/schluter/climate.py b/homeassistant/components/schluter/climate.py index 74e2d9a0194..6f0a49e6eb9 100644 --- a/homeassistant/components/schluter/climate.py +++ b/homeassistant/components/schluter/climate.py @@ -9,7 +9,7 @@ from requests import RequestException import voluptuous as vol from homeassistant.components.climate import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as CLIMATE_PLATFORM_SCHEMA, SCAN_INTERVAL, ClimateEntity, ClimateEntityFeature, @@ -29,7 +29,7 @@ from homeassistant.helpers.update_coordinator import ( from . import DATA_SCHLUTER_API, DATA_SCHLUTER_SESSION, DOMAIN _LOGGER = logging.getLogger(__name__) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = CLIMATE_PLATFORM_SCHEMA.extend( {vol.Optional(CONF_SCAN_INTERVAL): vol.All(vol.Coerce(int), vol.Range(min=1))} ) diff --git a/homeassistant/components/tfiac/climate.py b/homeassistant/components/tfiac/climate.py index 74d2c3fbe7e..81517a6f1f5 100644 --- a/homeassistant/components/tfiac/climate.py +++ b/homeassistant/components/tfiac/climate.py @@ -15,7 +15,7 @@ from homeassistant.components.climate import ( FAN_HIGH, FAN_LOW, FAN_MEDIUM, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as CLIMATE_PLATFORM_SCHEMA, SWING_BOTH, SWING_HORIZONTAL, SWING_OFF, @@ -32,7 +32,7 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType SCAN_INTERVAL = timedelta(seconds=60) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({vol.Required(CONF_HOST): cv.string}) +PLATFORM_SCHEMA = CLIMATE_PLATFORM_SCHEMA.extend({vol.Required(CONF_HOST): cv.string}) _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/touchline/climate.py b/homeassistant/components/touchline/climate.py index 5b1c52534c5..7b14404ee34 100644 --- a/homeassistant/components/touchline/climate.py +++ b/homeassistant/components/touchline/climate.py @@ -8,7 +8,7 @@ from pytouchline import PyTouchline import voluptuous as vol from homeassistant.components.climate import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as CLIMATE_PLATFORM_SCHEMA, ClimateEntity, ClimateEntityFeature, HVACMode, @@ -41,7 +41,7 @@ TOUCHLINE_HA_PRESETS = { for preset, settings in PRESET_MODES.items() } -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({vol.Required(CONF_HOST): cv.string}) +PLATFORM_SCHEMA = CLIMATE_PLATFORM_SCHEMA.extend({vol.Required(CONF_HOST): cv.string}) def setup_platform( diff --git a/homeassistant/components/venstar/climate.py b/homeassistant/components/venstar/climate.py index f47cf59be9c..ea833dc3183 100644 --- a/homeassistant/components/venstar/climate.py +++ b/homeassistant/components/venstar/climate.py @@ -10,7 +10,7 @@ from homeassistant.components.climate import ( ATTR_TARGET_TEMP_LOW, FAN_AUTO, FAN_ON, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as CLIMATE_PLATFORM_SCHEMA, PRESET_AWAY, PRESET_NONE, ClimateEntity, @@ -48,7 +48,7 @@ from .const import ( ) from .coordinator import VenstarDataUpdateCoordinator -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = CLIMATE_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_PASSWORD): cv.string, diff --git a/homeassistant/components/zhong_hong/climate.py b/homeassistant/components/zhong_hong/climate.py index b0a8f02a2f3..eaf00b5432f 100644 --- a/homeassistant/components/zhong_hong/climate.py +++ b/homeassistant/components/zhong_hong/climate.py @@ -11,7 +11,7 @@ from zhong_hong_hvac.hvac import HVAC as ZhongHongHVAC from homeassistant.components.climate import ( ATTR_HVAC_MODE, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as CLIMATE_PLATFORM_SCHEMA, ClimateEntity, ClimateEntityFeature, HVACMode, @@ -42,7 +42,7 @@ DEFAULT_GATEWAY_ADDRRESS = 1 SIGNAL_DEVICE_ADDED = "zhong_hong_device_added" SIGNAL_ZHONG_HONG_HUB_START = "zhong_hong_hub_start" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = CLIMATE_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, From 45dedf73c8859d955fab6d6ad76686bfe97f24ab Mon Sep 17 00:00:00 2001 From: "Mr. Bubbles" Date: Wed, 26 Jun 2024 11:00:31 +0200 Subject: [PATCH 1239/1445] Add exception translations for pyLoad integration (#120520) --- homeassistant/components/pyload/__init__.py | 13 ++++++++++--- homeassistant/components/pyload/coordinator.py | 5 ++++- homeassistant/components/pyload/strings.json | 11 +++++++++++ tests/components/pyload/test_sensor.py | 2 +- 4 files changed, 26 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/pyload/__init__.py b/homeassistant/components/pyload/__init__.py index 0a89fbb6140..8b85dfa29a4 100644 --- a/homeassistant/components/pyload/__init__.py +++ b/homeassistant/components/pyload/__init__.py @@ -20,6 +20,7 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_create_clientsession +from .const import DOMAIN from .coordinator import PyLoadCoordinator PLATFORMS: list[Platform] = [Platform.BUTTON, Platform.SENSOR, Platform.SWITCH] @@ -51,13 +52,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: PyLoadConfigEntry) -> bo await pyloadapi.login() except CannotConnect as e: raise ConfigEntryNotReady( - "Unable to connect and retrieve data from pyLoad API" + translation_domain=DOMAIN, + translation_key="setup_request_exception", ) from e except ParserError as e: - raise ConfigEntryNotReady("Unable to parse data from pyLoad API") from e + raise ConfigEntryNotReady( + translation_domain=DOMAIN, + translation_key="setup_parse_exception", + ) from e except InvalidAuth as e: raise ConfigEntryAuthFailed( - f"Authentication failed for {entry.data[CONF_USERNAME]}, check your login credentials" + translation_domain=DOMAIN, + translation_key="setup_authentication_exception", + translation_placeholders={CONF_USERNAME: entry.data[CONF_USERNAME]}, ) from e coordinator = PyLoadCoordinator(hass, pyloadapi) diff --git a/homeassistant/components/pyload/coordinator.py b/homeassistant/components/pyload/coordinator.py index b96a8d2ccbf..fd0e95192b3 100644 --- a/homeassistant/components/pyload/coordinator.py +++ b/homeassistant/components/pyload/coordinator.py @@ -7,6 +7,7 @@ import logging from pyloadapi import CannotConnect, InvalidAuth, ParserError, PyLoadAPI from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -64,7 +65,9 @@ class PyLoadCoordinator(DataUpdateCoordinator[pyLoadData]): await self.pyload.login() except InvalidAuth as exc: raise ConfigEntryAuthFailed( - f"Authentication failed for {self.pyload.username}, check your login credentials", + translation_domain=DOMAIN, + translation_key="setup_authentication_exception", + translation_placeholders={CONF_USERNAME: self.pyload.username}, ) from exc raise UpdateFailed( diff --git a/homeassistant/components/pyload/strings.json b/homeassistant/components/pyload/strings.json index 9f043ba224a..9fe311574fb 100644 --- a/homeassistant/components/pyload/strings.json +++ b/homeassistant/components/pyload/strings.json @@ -89,6 +89,17 @@ } } }, + "exceptions": { + "setup_request_exception": { + "message": "Unable to connect and retrieve data from pyLoad API, try again later" + }, + "setup_parse_exception": { + "message": "Unable to parse data from pyLoad API, try again later" + }, + "setup_authentication_exception": { + "message": "Authentication failed for {username}, verify your login credentials" + } + }, "issues": { "deprecated_yaml_import_issue_cannot_connect": { "title": "The pyLoad YAML configuration import failed", diff --git a/tests/components/pyload/test_sensor.py b/tests/components/pyload/test_sensor.py index 61a9a872f33..a44c9c8bf91 100644 --- a/tests/components/pyload/test_sensor.py +++ b/tests/components/pyload/test_sensor.py @@ -96,7 +96,7 @@ async def test_sensor_invalid_auth( await hass.async_block_till_done() assert ( - "Authentication failed for username, check your login credentials" + "Authentication failed for username, verify your login credentials" in caplog.text ) From 0c0f666a283a093538f77ffe629f73d0b1e18e57 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 26 Jun 2024 11:03:13 +0200 Subject: [PATCH 1240/1445] Force alias when importing camera PLATFORM_SCHEMA (#120514) --- homeassistant/components/canary/camera.py | 4 ++-- homeassistant/components/familyhub/camera.py | 7 +++++-- homeassistant/components/ffmpeg/camera.py | 8 ++++++-- homeassistant/components/local_file/camera.py | 7 +++++-- homeassistant/components/proxy/camera.py | 4 ++-- homeassistant/components/push/camera.py | 9 +++++++-- homeassistant/components/uvc/camera.py | 8 ++++++-- homeassistant/components/vivotek/camera.py | 8 ++++++-- homeassistant/components/xeoma/camera.py | 7 +++++-- homeassistant/components/xiaomi/camera.py | 7 +++++-- homeassistant/components/yi/camera.py | 7 +++++-- 11 files changed, 54 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/canary/camera.py b/homeassistant/components/canary/camera.py index e081d24e06a..a56d1ebc3de 100644 --- a/homeassistant/components/canary/camera.py +++ b/homeassistant/components/canary/camera.py @@ -14,7 +14,7 @@ import voluptuous as vol from homeassistant.components import ffmpeg from homeassistant.components.camera import ( - PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, + PLATFORM_SCHEMA as CAMERA_PLATFORM_SCHEMA, Camera, ) from homeassistant.components.ffmpeg import FFmpegManager, get_ffmpeg_manager @@ -40,7 +40,7 @@ FORCE_CAMERA_REFRESH_INTERVAL: Final = timedelta(minutes=15) PLATFORM_SCHEMA: Final = vol.All( cv.deprecated(CONF_FFMPEG_ARGUMENTS), - PARENT_PLATFORM_SCHEMA.extend( + CAMERA_PLATFORM_SCHEMA.extend( { vol.Optional( CONF_FFMPEG_ARGUMENTS, default=DEFAULT_FFMPEG_ARGUMENTS diff --git a/homeassistant/components/familyhub/camera.py b/homeassistant/components/familyhub/camera.py index da6f82cf56b..462983278b0 100644 --- a/homeassistant/components/familyhub/camera.py +++ b/homeassistant/components/familyhub/camera.py @@ -5,7 +5,10 @@ from __future__ import annotations from pyfamilyhublocal import FamilyHubCam import voluptuous as vol -from homeassistant.components.camera import PLATFORM_SCHEMA, Camera +from homeassistant.components.camera import ( + PLATFORM_SCHEMA as CAMERA_PLATFORM_SCHEMA, + Camera, +) from homeassistant.const import CONF_IP_ADDRESS, CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -15,7 +18,7 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType DEFAULT_NAME = "FamilyHub Camera" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = CAMERA_PLATFORM_SCHEMA.extend( { vol.Required(CONF_IP_ADDRESS): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/ffmpeg/camera.py b/homeassistant/components/ffmpeg/camera.py index c0ce4ad9746..2c46c4c29d1 100644 --- a/homeassistant/components/ffmpeg/camera.py +++ b/homeassistant/components/ffmpeg/camera.py @@ -9,7 +9,11 @@ from haffmpeg.camera import CameraMjpeg from haffmpeg.tools import IMAGE_JPEG import voluptuous as vol -from homeassistant.components.camera import PLATFORM_SCHEMA, Camera, CameraEntityFeature +from homeassistant.components.camera import ( + PLATFORM_SCHEMA as CAMERA_PLATFORM_SCHEMA, + Camera, + CameraEntityFeature, +) from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream @@ -28,7 +32,7 @@ from . import ( DEFAULT_NAME = "FFmpeg" DEFAULT_ARGUMENTS = "-pred 1" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = CAMERA_PLATFORM_SCHEMA.extend( { vol.Required(CONF_INPUT): cv.string, vol.Optional(CONF_EXTRA_ARGUMENTS, default=DEFAULT_ARGUMENTS): cv.string, diff --git a/homeassistant/components/local_file/camera.py b/homeassistant/components/local_file/camera.py index 72fe1a88b86..1306751f1a9 100644 --- a/homeassistant/components/local_file/camera.py +++ b/homeassistant/components/local_file/camera.py @@ -8,7 +8,10 @@ import os import voluptuous as vol -from homeassistant.components.camera import PLATFORM_SCHEMA, Camera +from homeassistant.components.camera import ( + PLATFORM_SCHEMA as CAMERA_PLATFORM_SCHEMA, + Camera, +) from homeassistant.const import ATTR_ENTITY_ID, CONF_FILE_PATH, CONF_NAME from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.helpers import config_validation as cv @@ -19,7 +22,7 @@ from .const import DATA_LOCAL_FILE, DEFAULT_NAME, DOMAIN, SERVICE_UPDATE_FILE_PA _LOGGER = logging.getLogger(__name__) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = CAMERA_PLATFORM_SCHEMA.extend( { vol.Required(CONF_FILE_PATH): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/proxy/camera.py b/homeassistant/components/proxy/camera.py index 5cd72b05871..e5e3d01591a 100644 --- a/homeassistant/components/proxy/camera.py +++ b/homeassistant/components/proxy/camera.py @@ -11,7 +11,7 @@ from PIL import Image import voluptuous as vol from homeassistant.components.camera import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as CAMERA_PLATFORM_SCHEMA, Camera, async_get_image, async_get_mjpeg_stream, @@ -45,7 +45,7 @@ MODE_CROP = "crop" DEFAULT_BASENAME = "Camera Proxy" DEFAULT_QUALITY = 75 -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = CAMERA_PLATFORM_SCHEMA.extend( { vol.Required(CONF_ENTITY_ID): cv.entity_id, vol.Optional(CONF_NAME): cv.string, diff --git a/homeassistant/components/push/camera.py b/homeassistant/components/push/camera.py index 8744ce8c2a1..1a37a10bf4f 100644 --- a/homeassistant/components/push/camera.py +++ b/homeassistant/components/push/camera.py @@ -11,7 +11,12 @@ import aiohttp import voluptuous as vol from homeassistant.components import webhook -from homeassistant.components.camera import DOMAIN, PLATFORM_SCHEMA, STATE_IDLE, Camera +from homeassistant.components.camera import ( + DOMAIN, + PLATFORM_SCHEMA as CAMERA_PLATFORM_SCHEMA, + STATE_IDLE, + Camera, +) from homeassistant.const import CONF_NAME, CONF_TIMEOUT, CONF_WEBHOOK_ID from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv @@ -32,7 +37,7 @@ ATTR_LAST_TRIP = "last_trip" PUSH_CAMERA_DATA = "push_camera" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = CAMERA_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_BUFFER_SIZE, default=1): cv.positive_int, diff --git a/homeassistant/components/uvc/camera.py b/homeassistant/components/uvc/camera.py index 3162fc67566..cd9594c7d31 100644 --- a/homeassistant/components/uvc/camera.py +++ b/homeassistant/components/uvc/camera.py @@ -10,7 +10,11 @@ import requests from uvcclient import camera as uvc_camera, nvr import voluptuous as vol -from homeassistant.components.camera import PLATFORM_SCHEMA, Camera, CameraEntityFeature +from homeassistant.components.camera import ( + PLATFORM_SCHEMA as CAMERA_PLATFORM_SCHEMA, + Camera, + CameraEntityFeature, +) from homeassistant.const import CONF_PASSWORD, CONF_PORT, CONF_SSL from homeassistant.core import HomeAssistant from homeassistant.exceptions import PlatformNotReady @@ -28,7 +32,7 @@ DEFAULT_PASSWORD = "ubnt" DEFAULT_PORT = 7080 DEFAULT_SSL = False -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = CAMERA_PLATFORM_SCHEMA.extend( { vol.Required(CONF_NVR): cv.string, vol.Required(CONF_KEY): cv.string, diff --git a/homeassistant/components/vivotek/camera.py b/homeassistant/components/vivotek/camera.py index 8719d55ec29..a8bf652e963 100644 --- a/homeassistant/components/vivotek/camera.py +++ b/homeassistant/components/vivotek/camera.py @@ -5,7 +5,11 @@ from __future__ import annotations from libpyvivotek import VivotekCamera import voluptuous as vol -from homeassistant.components.camera import PLATFORM_SCHEMA, Camera, CameraEntityFeature +from homeassistant.components.camera import ( + PLATFORM_SCHEMA as CAMERA_PLATFORM_SCHEMA, + Camera, + CameraEntityFeature, +) from homeassistant.const import ( CONF_AUTHENTICATION, CONF_IP_ADDRESS, @@ -32,7 +36,7 @@ DEFAULT_EVENT_0_KEY = "event_i0_enable" DEFAULT_SECURITY_LEVEL = "admin" DEFAULT_STREAM_SOURCE = "live.sdp" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = CAMERA_PLATFORM_SCHEMA.extend( { vol.Required(CONF_IP_ADDRESS): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/xeoma/camera.py b/homeassistant/components/xeoma/camera.py index 7d6abde8535..0c19e126fa7 100644 --- a/homeassistant/components/xeoma/camera.py +++ b/homeassistant/components/xeoma/camera.py @@ -7,7 +7,10 @@ import logging from pyxeoma.xeoma import Xeoma, XeomaError import voluptuous as vol -from homeassistant.components.camera import PLATFORM_SCHEMA, Camera +from homeassistant.components.camera import ( + PLATFORM_SCHEMA as CAMERA_PLATFORM_SCHEMA, + Camera, +) from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv @@ -32,7 +35,7 @@ CAMERAS_SCHEMA = vol.Schema( required=False, ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = CAMERA_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_CAMERAS): vol.Schema( diff --git a/homeassistant/components/xiaomi/camera.py b/homeassistant/components/xiaomi/camera.py index f3e850a7839..323a0f8a157 100644 --- a/homeassistant/components/xiaomi/camera.py +++ b/homeassistant/components/xiaomi/camera.py @@ -9,7 +9,10 @@ from haffmpeg.camera import CameraMjpeg import voluptuous as vol from homeassistant.components import ffmpeg -from homeassistant.components.camera import PLATFORM_SCHEMA, Camera +from homeassistant.components.camera import ( + PLATFORM_SCHEMA as CAMERA_PLATFORM_SCHEMA, + Camera, +) from homeassistant.components.ffmpeg import get_ffmpeg_manager from homeassistant.const import ( CONF_HOST, @@ -40,7 +43,7 @@ CONF_FFMPEG_ARGUMENTS = "ffmpeg_arguments" MODEL_YI = "yi" MODEL_XIAOFANG = "xiaofang" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = CAMERA_PLATFORM_SCHEMA.extend( { vol.Required(CONF_NAME): cv.string, vol.Required(CONF_HOST): cv.template, diff --git a/homeassistant/components/yi/camera.py b/homeassistant/components/yi/camera.py index f512d31cb6b..b2fac03954d 100644 --- a/homeassistant/components/yi/camera.py +++ b/homeassistant/components/yi/camera.py @@ -9,7 +9,10 @@ from haffmpeg.camera import CameraMjpeg import voluptuous as vol from homeassistant.components import ffmpeg -from homeassistant.components.camera import PLATFORM_SCHEMA, Camera +from homeassistant.components.camera import ( + PLATFORM_SCHEMA as CAMERA_PLATFORM_SCHEMA, + Camera, +) from homeassistant.components.ffmpeg import get_ffmpeg_manager from homeassistant.const import ( CONF_HOST, @@ -37,7 +40,7 @@ DEFAULT_ARGUMENTS = "-pred 1" CONF_FFMPEG_ARGUMENTS = "ffmpeg_arguments" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = CAMERA_PLATFORM_SCHEMA.extend( { vol.Required(CONF_NAME): cv.string, vol.Required(CONF_HOST): cv.string, From 41026b9227f624091406d30a6490dd21edff9c39 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk <11290930+bouwew@users.noreply.github.com> Date: Wed, 26 Jun 2024 11:04:00 +0200 Subject: [PATCH 1241/1445] Implement @plugwise_command for Plugwise Select platform (#120509) --- homeassistant/components/plugwise/const.py | 1 + homeassistant/components/plugwise/select.py | 23 ++++++++------------- tests/components/plugwise/test_select.py | 14 +++++++++---- 3 files changed, 20 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/plugwise/const.py b/homeassistant/components/plugwise/const.py index ed8cb2d2002..14599ce61fb 100644 --- a/homeassistant/components/plugwise/const.py +++ b/homeassistant/components/plugwise/const.py @@ -17,6 +17,7 @@ FLOW_SMILE: Final = "smile (Adam/Anna/P1)" FLOW_STRETCH: Final = "stretch (Stretch)" FLOW_TYPE: Final = "flow_type" GATEWAY: Final = "gateway" +LOCATION: Final = "location" PW_TYPE: Final = "plugwise_type" SMILE: Final = "smile" STRETCH: Final = "stretch" diff --git a/homeassistant/components/plugwise/select.py b/homeassistant/components/plugwise/select.py index c8c9791c0da..99aecacb96b 100644 --- a/homeassistant/components/plugwise/select.py +++ b/homeassistant/components/plugwise/select.py @@ -2,27 +2,24 @@ from __future__ import annotations -from collections.abc import Awaitable, Callable from dataclasses import dataclass -from plugwise import Smile - from homeassistant.components.select import SelectEntity, SelectEntityDescription from homeassistant.const import STATE_ON, EntityCategory from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import PlugwiseConfigEntry -from .const import SelectOptionsType, SelectType +from .const import LOCATION, SelectOptionsType, SelectType from .coordinator import PlugwiseDataUpdateCoordinator from .entity import PlugwiseEntity +from .util import plugwise_command @dataclass(frozen=True, kw_only=True) class PlugwiseSelectEntityDescription(SelectEntityDescription): """Class describing Plugwise Select entities.""" - command: Callable[[Smile, str, str], Awaitable[None]] key: SelectType options_key: SelectOptionsType @@ -31,28 +28,24 @@ SELECT_TYPES = ( PlugwiseSelectEntityDescription( key="select_schedule", translation_key="select_schedule", - command=lambda api, loc, opt: api.set_schedule_state(loc, STATE_ON, opt), options_key="available_schedules", ), PlugwiseSelectEntityDescription( key="select_regulation_mode", translation_key="regulation_mode", entity_category=EntityCategory.CONFIG, - command=lambda api, loc, opt: api.set_regulation_mode(opt), options_key="regulation_modes", ), PlugwiseSelectEntityDescription( key="select_dhw_mode", translation_key="dhw_mode", entity_category=EntityCategory.CONFIG, - command=lambda api, loc, opt: api.set_dhw_mode(opt), options_key="dhw_modes", ), PlugwiseSelectEntityDescription( key="select_gateway_mode", translation_key="gateway_mode", entity_category=EntityCategory.CONFIG, - command=lambda api, loc, opt: api.set_gateway_mode(opt), options_key="gateway_modes", ), ) @@ -109,10 +102,12 @@ class PlugwiseSelectEntity(PlugwiseEntity, SelectEntity): """Return the available select-options.""" return self.device[self.entity_description.options_key] + @plugwise_command async def async_select_option(self, option: str) -> None: - """Change to the selected entity option.""" - await self.entity_description.command( - self.coordinator.api, self.device["location"], option - ) + """Change to the selected entity option. - await self.coordinator.async_request_refresh() + self.device[LOCATION] and STATE_ON are required for the thermostat-schedule select. + """ + await self.coordinator.api.set_select( + self.entity_description.key, self.device[LOCATION], STATE_ON, option + ) diff --git a/tests/components/plugwise/test_select.py b/tests/components/plugwise/test_select.py index 86b21af9e8b..a6245ff11e7 100644 --- a/tests/components/plugwise/test_select.py +++ b/tests/components/plugwise/test_select.py @@ -38,8 +38,9 @@ async def test_adam_change_select_entity( blocking=True, ) - assert mock_smile_adam.set_schedule_state.call_count == 1 - mock_smile_adam.set_schedule_state.assert_called_with( + assert mock_smile_adam.set_select.call_count == 1 + mock_smile_adam.set_select.assert_called_with( + "select_schedule", "c50f167537524366a5af7aa3942feb1e", "on", "Badkamer Schema", @@ -69,5 +70,10 @@ async def test_adam_select_regulation_mode( }, blocking=True, ) - assert mock_smile_adam_3.set_regulation_mode.call_count == 1 - mock_smile_adam_3.set_regulation_mode.assert_called_with("heating") + assert mock_smile_adam_3.set_select.call_count == 1 + mock_smile_adam_3.set_select.assert_called_with( + "select_regulation_mode", + "bc93488efab249e5bc54fd7e175a6f91", + "on", + "heating", + ) From 2c48843739f57122d2b450e8cdb87537fab19b44 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 26 Jun 2024 11:13:27 +0200 Subject: [PATCH 1242/1445] Force alias when importing device tracker PLATFORM_SCHEMA (#120523) --- homeassistant/components/actiontec/device_tracker.py | 4 ++-- homeassistant/components/aprs/device_tracker.py | 4 ++-- homeassistant/components/arris_tg2492lg/device_tracker.py | 4 ++-- homeassistant/components/aruba/device_tracker.py | 4 ++-- homeassistant/components/bbox/device_tracker.py | 4 ++-- .../components/bluetooth_le_tracker/device_tracker.py | 4 ++-- .../components/bluetooth_tracker/device_tracker.py | 4 ++-- homeassistant/components/bt_home_hub_5/device_tracker.py | 4 ++-- homeassistant/components/bt_smarthub/device_tracker.py | 4 ++-- homeassistant/components/cisco_ios/device_tracker.py | 4 ++-- .../components/cisco_mobility_express/device_tracker.py | 4 ++-- homeassistant/components/cppm_tracker/device_tracker.py | 4 ++-- homeassistant/components/ddwrt/device_tracker.py | 4 ++-- homeassistant/components/fleetgo/device_tracker.py | 4 ++-- homeassistant/components/fortios/device_tracker.py | 4 ++-- homeassistant/components/google_maps/device_tracker.py | 4 ++-- homeassistant/components/hitron_coda/device_tracker.py | 4 ++-- homeassistant/components/linksys_smart/device_tracker.py | 6 ++++-- homeassistant/components/luci/device_tracker.py | 4 ++-- homeassistant/components/meraki/device_tracker.py | 4 ++-- homeassistant/components/mqtt_json/device_tracker.py | 4 ++-- homeassistant/components/quantum_gateway/device_tracker.py | 4 ++-- homeassistant/components/sky_hub/device_tracker.py | 6 ++++-- homeassistant/components/snmp/device_tracker.py | 4 ++-- homeassistant/components/swisscom/device_tracker.py | 4 ++-- homeassistant/components/thomson/device_tracker.py | 4 ++-- homeassistant/components/tomato/device_tracker.py | 4 ++-- homeassistant/components/traccar/device_tracker.py | 4 ++-- homeassistant/components/ubus/device_tracker.py | 4 ++-- homeassistant/components/unifi_direct/device_tracker.py | 4 ++-- homeassistant/components/upc_connect/device_tracker.py | 4 ++-- homeassistant/components/xiaomi/device_tracker.py | 4 ++-- homeassistant/components/xiaomi_miio/device_tracker.py | 4 ++-- 33 files changed, 70 insertions(+), 66 deletions(-) diff --git a/homeassistant/components/actiontec/device_tracker.py b/homeassistant/components/actiontec/device_tracker.py index 2afa772421c..8cab6552857 100644 --- a/homeassistant/components/actiontec/device_tracker.py +++ b/homeassistant/components/actiontec/device_tracker.py @@ -10,7 +10,7 @@ import voluptuous as vol from homeassistant.components.device_tracker import ( DOMAIN, - PLATFORM_SCHEMA as BASE_PLATFORM_SCHEMA, + PLATFORM_SCHEMA as DEVICE_TRACKER_PLATFORM_SCHEMA, DeviceScanner, ) from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME @@ -23,7 +23,7 @@ from .model import Device _LOGGER: Final = logging.getLogger(__name__) -PLATFORM_SCHEMA: Final = BASE_PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA: Final = DEVICE_TRACKER_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Required(CONF_PASSWORD): cv.string, diff --git a/homeassistant/components/aprs/device_tracker.py b/homeassistant/components/aprs/device_tracker.py index e96494db930..67d0736e526 100644 --- a/homeassistant/components/aprs/device_tracker.py +++ b/homeassistant/components/aprs/device_tracker.py @@ -12,7 +12,7 @@ import geopy.distance import voluptuous as vol from homeassistant.components.device_tracker import ( - PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, + PLATFORM_SCHEMA as DEVICE_TRACKER_PLATFORM_SCHEMA, SeeCallback, ) from homeassistant.const import ( @@ -53,7 +53,7 @@ FILTER_PORT = 14580 MSG_FORMATS = ["compressed", "uncompressed", "mic-e", "object"] -PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = DEVICE_TRACKER_PLATFORM_SCHEMA.extend( { vol.Required(CONF_CALLSIGNS): cv.ensure_list, vol.Required(CONF_USERNAME): cv.string, diff --git a/homeassistant/components/arris_tg2492lg/device_tracker.py b/homeassistant/components/arris_tg2492lg/device_tracker.py index 3975109e07a..58daead34f2 100644 --- a/homeassistant/components/arris_tg2492lg/device_tracker.py +++ b/homeassistant/components/arris_tg2492lg/device_tracker.py @@ -8,7 +8,7 @@ import voluptuous as vol from homeassistant.components.device_tracker import ( DOMAIN, - PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, + PLATFORM_SCHEMA as DEVICE_TRACKER_PLATFORM_SCHEMA, DeviceScanner, ) from homeassistant.const import CONF_HOST, CONF_PASSWORD @@ -19,7 +19,7 @@ from homeassistant.helpers.typing import ConfigType DEFAULT_HOST = "192.168.178.1" -PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = DEVICE_TRACKER_PLATFORM_SCHEMA.extend( { vol.Required(CONF_PASSWORD): cv.string, vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, diff --git a/homeassistant/components/aruba/device_tracker.py b/homeassistant/components/aruba/device_tracker.py index dd94a5975f0..4959ff7ef03 100644 --- a/homeassistant/components/aruba/device_tracker.py +++ b/homeassistant/components/aruba/device_tracker.py @@ -11,7 +11,7 @@ import voluptuous as vol from homeassistant.components.device_tracker import ( DOMAIN, - PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, + PLATFORM_SCHEMA as DEVICE_TRACKER_PLATFORM_SCHEMA, DeviceScanner, ) from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME @@ -27,7 +27,7 @@ _DEVICES_REGEX = re.compile( r"(?P([0-9a-f]{2}[:-]){5}([0-9a-f]{2}))\s+" ) -PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = DEVICE_TRACKER_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Required(CONF_PASSWORD): cv.string, diff --git a/homeassistant/components/bbox/device_tracker.py b/homeassistant/components/bbox/device_tracker.py index 5413c75d8e7..6ced2c73c9a 100644 --- a/homeassistant/components/bbox/device_tracker.py +++ b/homeassistant/components/bbox/device_tracker.py @@ -11,7 +11,7 @@ import voluptuous as vol from homeassistant.components.device_tracker import ( DOMAIN, - PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, + PLATFORM_SCHEMA as DEVICE_TRACKER_PLATFORM_SCHEMA, DeviceScanner, ) from homeassistant.const import CONF_HOST @@ -27,7 +27,7 @@ DEFAULT_HOST = "192.168.1.254" MIN_TIME_BETWEEN_SCANS = timedelta(seconds=60) -PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = DEVICE_TRACKER_PLATFORM_SCHEMA.extend( {vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string} ) diff --git a/homeassistant/components/bluetooth_le_tracker/device_tracker.py b/homeassistant/components/bluetooth_le_tracker/device_tracker.py index 1a88e1c5fa3..24b03b2f566 100644 --- a/homeassistant/components/bluetooth_le_tracker/device_tracker.py +++ b/homeassistant/components/bluetooth_le_tracker/device_tracker.py @@ -13,7 +13,7 @@ from homeassistant.components import bluetooth from homeassistant.components.bluetooth.match import BluetoothCallbackMatcher from homeassistant.components.device_tracker import ( CONF_TRACK_NEW, - PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, + PLATFORM_SCHEMA as DEVICE_TRACKER_PLATFORM_SCHEMA, SCAN_INTERVAL, SourceType, ) @@ -42,7 +42,7 @@ DATA_BLE_ADAPTER = "ADAPTER" BLE_PREFIX = "BLE_" MIN_SEEN_NEW = 5 -PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = DEVICE_TRACKER_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_TRACK_BATTERY, default=False): cv.boolean, vol.Optional( diff --git a/homeassistant/components/bluetooth_tracker/device_tracker.py b/homeassistant/components/bluetooth_tracker/device_tracker.py index 7cde6f848d5..1d64d31a248 100644 --- a/homeassistant/components/bluetooth_tracker/device_tracker.py +++ b/homeassistant/components/bluetooth_tracker/device_tracker.py @@ -15,7 +15,7 @@ from homeassistant.components.device_tracker import ( CONF_SCAN_INTERVAL, CONF_TRACK_NEW, DEFAULT_TRACK_NEW, - PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, + PLATFORM_SCHEMA as DEVICE_TRACKER_PLATFORM_SCHEMA, SCAN_INTERVAL, SourceType, ) @@ -41,7 +41,7 @@ from .const import ( _LOGGER: Final = logging.getLogger(__name__) -PLATFORM_SCHEMA: Final = PARENT_PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA: Final = DEVICE_TRACKER_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_TRACK_NEW): cv.boolean, vol.Optional(CONF_REQUEST_RSSI): cv.boolean, diff --git a/homeassistant/components/bt_home_hub_5/device_tracker.py b/homeassistant/components/bt_home_hub_5/device_tracker.py index 8706a04e7ad..60ded009d5f 100644 --- a/homeassistant/components/bt_home_hub_5/device_tracker.py +++ b/homeassistant/components/bt_home_hub_5/device_tracker.py @@ -9,7 +9,7 @@ import voluptuous as vol from homeassistant.components.device_tracker import ( DOMAIN, - PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, + PLATFORM_SCHEMA as DEVICE_TRACKER_PLATFORM_SCHEMA, DeviceScanner, ) from homeassistant.const import CONF_HOST @@ -21,7 +21,7 @@ _LOGGER = logging.getLogger(__name__) CONF_DEFAULT_IP = "192.168.1.254" -PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = DEVICE_TRACKER_PLATFORM_SCHEMA.extend( {vol.Optional(CONF_HOST, default=CONF_DEFAULT_IP): cv.string} ) diff --git a/homeassistant/components/bt_smarthub/device_tracker.py b/homeassistant/components/bt_smarthub/device_tracker.py index 8b5411e2014..10c8000fb93 100644 --- a/homeassistant/components/bt_smarthub/device_tracker.py +++ b/homeassistant/components/bt_smarthub/device_tracker.py @@ -10,7 +10,7 @@ import voluptuous as vol from homeassistant.components.device_tracker import ( DOMAIN, - PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, + PLATFORM_SCHEMA as DEVICE_TRACKER_PLATFORM_SCHEMA, DeviceScanner, ) from homeassistant.const import CONF_HOST @@ -23,7 +23,7 @@ _LOGGER = logging.getLogger(__name__) CONF_DEFAULT_IP = "192.168.1.254" CONF_SMARTHUB_MODEL = "smarthub_model" -PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = DEVICE_TRACKER_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_HOST, default=CONF_DEFAULT_IP): cv.string, vol.Optional(CONF_SMARTHUB_MODEL): vol.In([1, 2]), diff --git a/homeassistant/components/cisco_ios/device_tracker.py b/homeassistant/components/cisco_ios/device_tracker.py index 8a21b64cb9f..0b76a85424b 100644 --- a/homeassistant/components/cisco_ios/device_tracker.py +++ b/homeassistant/components/cisco_ios/device_tracker.py @@ -10,7 +10,7 @@ import voluptuous as vol from homeassistant.components.device_tracker import ( DOMAIN, - PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, + PLATFORM_SCHEMA as DEVICE_TRACKER_PLATFORM_SCHEMA, DeviceScanner, ) from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME @@ -21,7 +21,7 @@ from homeassistant.helpers.typing import ConfigType _LOGGER = logging.getLogger(__name__) PLATFORM_SCHEMA = vol.All( - PARENT_PLATFORM_SCHEMA.extend( + DEVICE_TRACKER_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Required(CONF_USERNAME): cv.string, diff --git a/homeassistant/components/cisco_mobility_express/device_tracker.py b/homeassistant/components/cisco_mobility_express/device_tracker.py index d96ab54a68f..38d2c78c66a 100644 --- a/homeassistant/components/cisco_mobility_express/device_tracker.py +++ b/homeassistant/components/cisco_mobility_express/device_tracker.py @@ -9,7 +9,7 @@ import voluptuous as vol from homeassistant.components.device_tracker import ( DOMAIN, - PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, + PLATFORM_SCHEMA as DEVICE_TRACKER_PLATFORM_SCHEMA, DeviceScanner, ) from homeassistant.const import ( @@ -28,7 +28,7 @@ _LOGGER = logging.getLogger(__name__) DEFAULT_SSL = False DEFAULT_VERIFY_SSL = True -PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = DEVICE_TRACKER_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Required(CONF_USERNAME): cv.string, diff --git a/homeassistant/components/cppm_tracker/device_tracker.py b/homeassistant/components/cppm_tracker/device_tracker.py index 9b1ebbb1ed8..a7a1a1b99e8 100644 --- a/homeassistant/components/cppm_tracker/device_tracker.py +++ b/homeassistant/components/cppm_tracker/device_tracker.py @@ -10,7 +10,7 @@ import voluptuous as vol from homeassistant.components.device_tracker import ( DOMAIN, - PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, + PLATFORM_SCHEMA as DEVICE_TRACKER_PLATFORM_SCHEMA, DeviceScanner, ) from homeassistant.const import CONF_API_KEY, CONF_CLIENT_ID, CONF_HOST @@ -22,7 +22,7 @@ SCAN_INTERVAL = timedelta(seconds=120) GRANT_TYPE = "client_credentials" -PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = DEVICE_TRACKER_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Required(CONF_CLIENT_ID): cv.string, diff --git a/homeassistant/components/ddwrt/device_tracker.py b/homeassistant/components/ddwrt/device_tracker.py index 555b6f8ff00..30ab3af53fb 100644 --- a/homeassistant/components/ddwrt/device_tracker.py +++ b/homeassistant/components/ddwrt/device_tracker.py @@ -11,7 +11,7 @@ import voluptuous as vol from homeassistant.components.device_tracker import ( DOMAIN, - PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, + PLATFORM_SCHEMA as DEVICE_TRACKER_PLATFORM_SCHEMA, DeviceScanner, ) from homeassistant.const import ( @@ -35,7 +35,7 @@ DEFAULT_VERIFY_SSL = True CONF_WIRELESS_ONLY = "wireless_only" DEFAULT_WIRELESS_ONLY = True -PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = DEVICE_TRACKER_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Required(CONF_PASSWORD): cv.string, diff --git a/homeassistant/components/fleetgo/device_tracker.py b/homeassistant/components/fleetgo/device_tracker.py index 3249e8035b4..008c0765c07 100644 --- a/homeassistant/components/fleetgo/device_tracker.py +++ b/homeassistant/components/fleetgo/device_tracker.py @@ -9,7 +9,7 @@ from ritassist import API import voluptuous as vol from homeassistant.components.device_tracker import ( - PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, + PLATFORM_SCHEMA as DEVICE_TRACKER_PLATFORM_SCHEMA, SeeCallback, ) from homeassistant.const import ( @@ -26,7 +26,7 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType _LOGGER = logging.getLogger(__name__) -PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = DEVICE_TRACKER_PLATFORM_SCHEMA.extend( { vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, diff --git a/homeassistant/components/fortios/device_tracker.py b/homeassistant/components/fortios/device_tracker.py index 7cc5bab7d16..192c1e4bc69 100644 --- a/homeassistant/components/fortios/device_tracker.py +++ b/homeassistant/components/fortios/device_tracker.py @@ -14,7 +14,7 @@ import voluptuous as vol from homeassistant.components.device_tracker import ( DOMAIN, - PLATFORM_SCHEMA as BASE_PLATFORM_SCHEMA, + PLATFORM_SCHEMA as DEVICE_TRACKER_PLATFORM_SCHEMA, DeviceScanner, ) from homeassistant.const import CONF_HOST, CONF_TOKEN, CONF_VERIFY_SSL @@ -26,7 +26,7 @@ _LOGGER = logging.getLogger(__name__) DEFAULT_VERIFY_SSL = False -PLATFORM_SCHEMA = BASE_PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = DEVICE_TRACKER_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Required(CONF_TOKEN): cv.string, diff --git a/homeassistant/components/google_maps/device_tracker.py b/homeassistant/components/google_maps/device_tracker.py index b3b0430271a..d703078d198 100644 --- a/homeassistant/components/google_maps/device_tracker.py +++ b/homeassistant/components/google_maps/device_tracker.py @@ -10,7 +10,7 @@ from locationsharinglib.locationsharinglibexceptions import InvalidCookies import voluptuous as vol from homeassistant.components.device_tracker import ( - PLATFORM_SCHEMA as PLATFORM_SCHEMA_BASE, + PLATFORM_SCHEMA as DEVICE_TRACKER_PLATFORM_SCHEMA, SeeCallback, SourceType, ) @@ -40,7 +40,7 @@ CREDENTIALS_FILE = ".google_maps_location_sharing.cookies" # the parent "device_tracker" have marked the schemas as legacy, so this # need to be refactored as part of a bigger rewrite. -PLATFORM_SCHEMA = PLATFORM_SCHEMA_BASE.extend( +PLATFORM_SCHEMA = DEVICE_TRACKER_PLATFORM_SCHEMA.extend( { vol.Required(CONF_USERNAME): cv.string, vol.Optional(CONF_MAX_GPS_ACCURACY, default=100000): vol.Coerce(float), diff --git a/homeassistant/components/hitron_coda/device_tracker.py b/homeassistant/components/hitron_coda/device_tracker.py index dec15e25b0b..68d93e9719d 100644 --- a/homeassistant/components/hitron_coda/device_tracker.py +++ b/homeassistant/components/hitron_coda/device_tracker.py @@ -11,7 +11,7 @@ import voluptuous as vol from homeassistant.components.device_tracker import ( DOMAIN, - PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, + PLATFORM_SCHEMA as DEVICE_TRACKER_PLATFORM_SCHEMA, DeviceScanner, ) from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_TYPE, CONF_USERNAME @@ -23,7 +23,7 @@ _LOGGER = logging.getLogger(__name__) DEFAULT_TYPE = "rogers" -PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = DEVICE_TRACKER_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Required(CONF_USERNAME): cv.string, diff --git a/homeassistant/components/linksys_smart/device_tracker.py b/homeassistant/components/linksys_smart/device_tracker.py index a33f0070c70..45ae1d328dd 100644 --- a/homeassistant/components/linksys_smart/device_tracker.py +++ b/homeassistant/components/linksys_smart/device_tracker.py @@ -10,7 +10,7 @@ import voluptuous as vol from homeassistant.components.device_tracker import ( DOMAIN, - PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, + PLATFORM_SCHEMA as DEVICE_TRACKER_PLATFORM_SCHEMA, DeviceScanner, ) from homeassistant.const import CONF_HOST @@ -22,7 +22,9 @@ DEFAULT_TIMEOUT = 10 _LOGGER = logging.getLogger(__name__) -PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend({vol.Required(CONF_HOST): cv.string}) +PLATFORM_SCHEMA = DEVICE_TRACKER_PLATFORM_SCHEMA.extend( + {vol.Required(CONF_HOST): cv.string} +) def get_scanner( diff --git a/homeassistant/components/luci/device_tracker.py b/homeassistant/components/luci/device_tracker.py index 183f383e7e4..59d4d12ddf6 100644 --- a/homeassistant/components/luci/device_tracker.py +++ b/homeassistant/components/luci/device_tracker.py @@ -9,7 +9,7 @@ import voluptuous as vol from homeassistant.components.device_tracker import ( DOMAIN, - PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, + PLATFORM_SCHEMA as DEVICE_TRACKER_PLATFORM_SCHEMA, DeviceScanner, ) from homeassistant.const import ( @@ -28,7 +28,7 @@ _LOGGER = logging.getLogger(__name__) DEFAULT_SSL = False DEFAULT_VERIFY_SSL = True -PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = DEVICE_TRACKER_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Required(CONF_USERNAME): cv.string, diff --git a/homeassistant/components/meraki/device_tracker.py b/homeassistant/components/meraki/device_tracker.py index a6eefe7345f..95ed2ba9089 100644 --- a/homeassistant/components/meraki/device_tracker.py +++ b/homeassistant/components/meraki/device_tracker.py @@ -9,7 +9,7 @@ import logging import voluptuous as vol from homeassistant.components.device_tracker import ( - PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, + PLATFORM_SCHEMA as DEVICE_TRACKER_PLATFORM_SCHEMA, AsyncSeeCallback, SourceType, ) @@ -27,7 +27,7 @@ ACCEPTED_VERSIONS = ["2.0", "2.1"] _LOGGER = logging.getLogger(__name__) -PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = DEVICE_TRACKER_PLATFORM_SCHEMA.extend( {vol.Required(CONF_VALIDATOR): cv.string, vol.Required(CONF_SECRET): cv.string} ) diff --git a/homeassistant/components/mqtt_json/device_tracker.py b/homeassistant/components/mqtt_json/device_tracker.py index 68f42479930..3200da56cf6 100644 --- a/homeassistant/components/mqtt_json/device_tracker.py +++ b/homeassistant/components/mqtt_json/device_tracker.py @@ -9,7 +9,7 @@ import voluptuous as vol from homeassistant.components import mqtt from homeassistant.components.device_tracker import ( - PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, + PLATFORM_SCHEMA as DEVICE_TRACKER_PLATFORM_SCHEMA, AsyncSeeCallback, ) from homeassistant.components.mqtt import CONF_QOS @@ -36,7 +36,7 @@ GPS_JSON_PAYLOAD_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) -PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend(mqtt.config.SCHEMA_BASE).extend( +PLATFORM_SCHEMA = DEVICE_TRACKER_PLATFORM_SCHEMA.extend(mqtt.config.SCHEMA_BASE).extend( {vol.Required(CONF_DEVICES): {cv.string: mqtt.valid_subscribe_topic}} ) diff --git a/homeassistant/components/quantum_gateway/device_tracker.py b/homeassistant/components/quantum_gateway/device_tracker.py index 1c43cbb14a8..88cb5d60028 100644 --- a/homeassistant/components/quantum_gateway/device_tracker.py +++ b/homeassistant/components/quantum_gateway/device_tracker.py @@ -10,7 +10,7 @@ import voluptuous as vol from homeassistant.components.device_tracker import ( DOMAIN, - PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, + PLATFORM_SCHEMA as DEVICE_TRACKER_PLATFORM_SCHEMA, DeviceScanner, ) from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_SSL @@ -22,7 +22,7 @@ _LOGGER = logging.getLogger(__name__) DEFAULT_HOST = "myfiosgateway.com" -PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = DEVICE_TRACKER_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, vol.Optional(CONF_SSL, default=True): cv.boolean, diff --git a/homeassistant/components/sky_hub/device_tracker.py b/homeassistant/components/sky_hub/device_tracker.py index bc4a0fdc743..140a174cc97 100644 --- a/homeassistant/components/sky_hub/device_tracker.py +++ b/homeassistant/components/sky_hub/device_tracker.py @@ -9,7 +9,7 @@ import voluptuous as vol from homeassistant.components.device_tracker import ( DOMAIN, - PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, + PLATFORM_SCHEMA as DEVICE_TRACKER_PLATFORM_SCHEMA, DeviceScanner, ) from homeassistant.const import CONF_HOST @@ -20,7 +20,9 @@ from homeassistant.helpers.typing import ConfigType _LOGGER = logging.getLogger(__name__) -PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend({vol.Optional(CONF_HOST): cv.string}) +PLATFORM_SCHEMA = DEVICE_TRACKER_PLATFORM_SCHEMA.extend( + {vol.Optional(CONF_HOST): cv.string} +) async def async_get_scanner( diff --git a/homeassistant/components/snmp/device_tracker.py b/homeassistant/components/snmp/device_tracker.py index d336838117f..91baa8f6b4c 100644 --- a/homeassistant/components/snmp/device_tracker.py +++ b/homeassistant/components/snmp/device_tracker.py @@ -19,7 +19,7 @@ import voluptuous as vol from homeassistant.components.device_tracker import ( DOMAIN, - PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, + PLATFORM_SCHEMA as DEVICE_TRACKER_PLATFORM_SCHEMA, DeviceScanner, ) from homeassistant.const import CONF_HOST @@ -44,7 +44,7 @@ from .util import RequestArgsType, async_create_request_cmd_args _LOGGER = logging.getLogger(__name__) -PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = DEVICE_TRACKER_PLATFORM_SCHEMA.extend( { vol.Required(CONF_BASEOID): cv.string, vol.Required(CONF_HOST): cv.string, diff --git a/homeassistant/components/swisscom/device_tracker.py b/homeassistant/components/swisscom/device_tracker.py index cd393c79e09..c13e5a322aa 100644 --- a/homeassistant/components/swisscom/device_tracker.py +++ b/homeassistant/components/swisscom/device_tracker.py @@ -10,7 +10,7 @@ import voluptuous as vol from homeassistant.components.device_tracker import ( DOMAIN, - PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, + PLATFORM_SCHEMA as DEVICE_TRACKER_PLATFORM_SCHEMA, DeviceScanner, ) from homeassistant.const import CONF_HOST @@ -22,7 +22,7 @@ _LOGGER = logging.getLogger(__name__) DEFAULT_IP = "192.168.1.1" -PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = DEVICE_TRACKER_PLATFORM_SCHEMA.extend( {vol.Optional(CONF_HOST, default=DEFAULT_IP): cv.string} ) diff --git a/homeassistant/components/thomson/device_tracker.py b/homeassistant/components/thomson/device_tracker.py index 544260a1e34..339b12f0dc9 100644 --- a/homeassistant/components/thomson/device_tracker.py +++ b/homeassistant/components/thomson/device_tracker.py @@ -10,7 +10,7 @@ import voluptuous as vol from homeassistant.components.device_tracker import ( DOMAIN, - PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, + PLATFORM_SCHEMA as DEVICE_TRACKER_PLATFORM_SCHEMA, DeviceScanner, ) from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME @@ -30,7 +30,7 @@ _DEVICES_REGEX = re.compile( r"(?P([^\s]+))" ) -PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = DEVICE_TRACKER_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Required(CONF_PASSWORD): cv.string, diff --git a/homeassistant/components/tomato/device_tracker.py b/homeassistant/components/tomato/device_tracker.py index d28fa505c61..aaa1d10d08d 100644 --- a/homeassistant/components/tomato/device_tracker.py +++ b/homeassistant/components/tomato/device_tracker.py @@ -12,7 +12,7 @@ import voluptuous as vol from homeassistant.components.device_tracker import ( DOMAIN, - PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, + PLATFORM_SCHEMA as DEVICE_TRACKER_PLATFORM_SCHEMA, DeviceScanner, ) from homeassistant.const import ( @@ -31,7 +31,7 @@ CONF_HTTP_ID = "http_id" _LOGGER = logging.getLogger(__name__) -PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = DEVICE_TRACKER_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_PORT): cv.port, diff --git a/homeassistant/components/traccar/device_tracker.py b/homeassistant/components/traccar/device_tracker.py index 5695e434eff..468d2fd4d05 100644 --- a/homeassistant/components/traccar/device_tracker.py +++ b/homeassistant/components/traccar/device_tracker.py @@ -10,7 +10,7 @@ from pytraccar import ApiClient, TraccarException import voluptuous as vol from homeassistant.components.device_tracker import ( - PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, + PLATFORM_SCHEMA as DEVICE_TRACKER_PLATFORM_SCHEMA, AsyncSeeCallback, SourceType, TrackerEntity, @@ -104,7 +104,7 @@ EVENTS = [ EVENT_ALL_EVENTS, ] -PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = DEVICE_TRACKER_PLATFORM_SCHEMA.extend( { vol.Required(CONF_PASSWORD): cv.string, vol.Required(CONF_USERNAME): cv.string, diff --git a/homeassistant/components/ubus/device_tracker.py b/homeassistant/components/ubus/device_tracker.py index b728059d0be..6170ad213a3 100644 --- a/homeassistant/components/ubus/device_tracker.py +++ b/homeassistant/components/ubus/device_tracker.py @@ -10,7 +10,7 @@ import voluptuous as vol from homeassistant.components.device_tracker import ( DOMAIN, - PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, + PLATFORM_SCHEMA as DEVICE_TRACKER_PLATFORM_SCHEMA, DeviceScanner, ) from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME @@ -24,7 +24,7 @@ CONF_DHCP_SOFTWARE = "dhcp_software" DEFAULT_DHCP_SOFTWARE = "dnsmasq" DHCP_SOFTWARES = ["dnsmasq", "odhcpd", "none"] -PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = DEVICE_TRACKER_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Required(CONF_PASSWORD): cv.string, diff --git a/homeassistant/components/unifi_direct/device_tracker.py b/homeassistant/components/unifi_direct/device_tracker.py index 51c9c412dad..c2cb9eba632 100644 --- a/homeassistant/components/unifi_direct/device_tracker.py +++ b/homeassistant/components/unifi_direct/device_tracker.py @@ -10,7 +10,7 @@ import voluptuous as vol from homeassistant.components.device_tracker import ( DOMAIN, - PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, + PLATFORM_SCHEMA as DEVICE_TRACKER_PLATFORM_SCHEMA, DeviceScanner, ) from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME @@ -22,7 +22,7 @@ _LOGGER = logging.getLogger(__name__) DEFAULT_SSH_PORT = 22 -PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = DEVICE_TRACKER_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Required(CONF_PASSWORD): cv.string, diff --git a/homeassistant/components/upc_connect/device_tracker.py b/homeassistant/components/upc_connect/device_tracker.py index 9e570c9d26b..1ec6dcd3107 100644 --- a/homeassistant/components/upc_connect/device_tracker.py +++ b/homeassistant/components/upc_connect/device_tracker.py @@ -10,7 +10,7 @@ import voluptuous as vol from homeassistant.components.device_tracker import ( DOMAIN, - PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, + PLATFORM_SCHEMA as DEVICE_TRACKER_PLATFORM_SCHEMA, DeviceScanner, ) from homeassistant.const import CONF_HOST, CONF_PASSWORD, EVENT_HOMEASSISTANT_STOP @@ -23,7 +23,7 @@ _LOGGER = logging.getLogger(__name__) DEFAULT_IP = "192.168.0.1" -PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = DEVICE_TRACKER_PLATFORM_SCHEMA.extend( { vol.Required(CONF_PASSWORD): cv.string, vol.Optional(CONF_HOST, default=DEFAULT_IP): cv.string, diff --git a/homeassistant/components/xiaomi/device_tracker.py b/homeassistant/components/xiaomi/device_tracker.py index 869a7a1cf1f..b3983e76aaa 100644 --- a/homeassistant/components/xiaomi/device_tracker.py +++ b/homeassistant/components/xiaomi/device_tracker.py @@ -10,7 +10,7 @@ import voluptuous as vol from homeassistant.components.device_tracker import ( DOMAIN, - PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, + PLATFORM_SCHEMA as DEVICE_TRACKER_PLATFORM_SCHEMA, DeviceScanner, ) from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME @@ -20,7 +20,7 @@ from homeassistant.helpers.typing import ConfigType _LOGGER = logging.getLogger(__name__) -PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = DEVICE_TRACKER_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Required(CONF_USERNAME, default="admin"): cv.string, diff --git a/homeassistant/components/xiaomi_miio/device_tracker.py b/homeassistant/components/xiaomi_miio/device_tracker.py index ba73ccc57f0..9acdb1cc53e 100644 --- a/homeassistant/components/xiaomi_miio/device_tracker.py +++ b/homeassistant/components/xiaomi_miio/device_tracker.py @@ -9,7 +9,7 @@ import voluptuous as vol from homeassistant.components.device_tracker import ( DOMAIN, - PLATFORM_SCHEMA as BASE_PLATFORM_SCHEMA, + PLATFORM_SCHEMA as DEVICE_TRACKER_PLATFORM_SCHEMA, DeviceScanner, ) from homeassistant.const import CONF_HOST, CONF_TOKEN @@ -19,7 +19,7 @@ from homeassistant.helpers.typing import ConfigType _LOGGER = logging.getLogger(__name__) -PLATFORM_SCHEMA = BASE_PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = DEVICE_TRACKER_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Required(CONF_TOKEN): vol.All(cv.string, vol.Length(min=32, max=32)), From d589eaf440f614024db2f05110145c0e30a6ffbc Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 26 Jun 2024 11:23:26 +0200 Subject: [PATCH 1243/1445] Simplify EVENT_STATE_REPORTED (#120508) --- homeassistant/const.py | 4 ++-- homeassistant/core.py | 38 +++++++++++++++++--------------------- tests/test_core.py | 8 ++++---- 3 files changed, 23 insertions(+), 27 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 577e8df6f39..3a970aefd38 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -18,7 +18,7 @@ from .util.hass_dict import HassKey from .util.signal_type import SignalType if TYPE_CHECKING: - from .core import EventStateChangedData + from .core import EventStateChangedData, EventStateReportedData from .helpers.typing import NoEventData APPLICATION_NAME: Final = "HomeAssistant" @@ -321,7 +321,7 @@ EVENT_LOGGING_CHANGED: Final = "logging_changed" EVENT_SERVICE_REGISTERED: Final = "service_registered" EVENT_SERVICE_REMOVED: Final = "service_removed" EVENT_STATE_CHANGED: EventType[EventStateChangedData] = EventType("state_changed") -EVENT_STATE_REPORTED: Final = "state_reported" +EVENT_STATE_REPORTED: EventType[EventStateReportedData] = EventType("state_reported") EVENT_THEMES_UPDATED: Final = "themes_updated" EVENT_PANELS_UPDATED: Final = "panels_updated" EVENT_LOVELACE_UPDATED: Final = "lovelace_updated" diff --git a/homeassistant/core.py b/homeassistant/core.py index f114049b2b2..2b43b2d40ff 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -159,13 +159,27 @@ class ConfigSource(enum.StrEnum): class EventStateChangedData(TypedDict): - """EventStateChanged data.""" + """EVENT_STATE_CHANGED data. + + A state changed event is fired when on state write when the state is changed. + """ entity_id: str old_state: State | None new_state: State | None +class EventStateReportedData(TypedDict): + """EVENT_STATE_REPORTED data. + + A state reported event is fired when on state write when the state is unchanged. + """ + + entity_id: str + old_last_reported: datetime.datetime + new_state: State | None + + # SOURCE_* are deprecated as of Home Assistant 2022.2, use ConfigSource instead _DEPRECATED_SOURCE_DISCOVERED = DeprecatedConstantEnum( ConfigSource.DISCOVERED, "2025.1" @@ -1604,27 +1618,8 @@ class EventBus: raise HomeAssistantError( f"Event filter is required for event {event_type}" ) - # Special case for EVENT_STATE_REPORTED, we also want to listen to - # EVENT_STATE_CHANGED - self._listeners[EVENT_STATE_REPORTED].append(filterable_job) - self._listeners[EVENT_STATE_CHANGED].append(filterable_job) - return functools.partial( - self._async_remove_multiple_listeners, - (EVENT_STATE_REPORTED, EVENT_STATE_CHANGED), - filterable_job, - ) return self._async_listen_filterable_job(event_type, filterable_job) - @callback - def _async_remove_multiple_listeners( - self, - keys: Iterable[EventType[_DataT] | str], - filterable_job: _FilterableJobType[Any], - ) -> None: - """Remove multiple listeners for specific event_types.""" - for key in keys: - self._async_remove_listener(key, filterable_job) - @callback def _async_listen_filterable_job( self, @@ -2278,7 +2273,8 @@ class StateMachine: old_last_reported = old_state.last_reported # type: ignore[union-attr] old_state.last_reported = now # type: ignore[union-attr] old_state.last_reported_timestamp = timestamp # type: ignore[union-attr] - self._bus.async_fire_internal( + # Avoid creating an EventStateReportedData + self._bus.async_fire_internal( # type: ignore[misc] EVENT_STATE_REPORTED, { "entity_id": entity_id, diff --git a/tests/test_core.py b/tests/test_core.py index a1748638342..5e6b51cc39e 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -3385,24 +3385,24 @@ async def test_statemachine_report_state(hass: HomeAssistant) -> None: hass.states.async_set("light.bowl", "on", None, True) await hass.async_block_till_done() assert len(state_changed_events) == 1 - assert len(state_reported_events) == 2 + assert len(state_reported_events) == 1 hass.states.async_set("light.bowl", "off") await hass.async_block_till_done() assert len(state_changed_events) == 2 - assert len(state_reported_events) == 3 + assert len(state_reported_events) == 1 hass.states.async_remove("light.bowl") await hass.async_block_till_done() assert len(state_changed_events) == 3 - assert len(state_reported_events) == 4 + assert len(state_reported_events) == 1 unsub() hass.states.async_set("light.bowl", "on") await hass.async_block_till_done() assert len(state_changed_events) == 4 - assert len(state_reported_events) == 4 + assert len(state_reported_events) == 1 async def test_report_state_listener_restrictions(hass: HomeAssistant) -> None: From 59ae297ccd1ce888fa7dc54a29328c160c5588ad Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 26 Jun 2024 11:24:21 +0200 Subject: [PATCH 1244/1445] Force alias when importing humidifier PLATFORM_SCHEMA (#120526) --- homeassistant/components/generic_hygrostat/humidifier.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/generic_hygrostat/humidifier.py b/homeassistant/components/generic_hygrostat/humidifier.py index c22904a4caa..a1f9936fa33 100644 --- a/homeassistant/components/generic_hygrostat/humidifier.py +++ b/homeassistant/components/generic_hygrostat/humidifier.py @@ -12,7 +12,7 @@ from homeassistant.components.humidifier import ( ATTR_HUMIDITY, MODE_AWAY, MODE_NORMAL, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as HUMIDIFIER_PLATFORM_SCHEMA, HumidifierAction, HumidifierDeviceClass, HumidifierEntity, @@ -72,7 +72,7 @@ _LOGGER = logging.getLogger(__name__) ATTR_SAVED_HUMIDITY = "saved_humidity" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(HYGROSTAT_SCHEMA.schema) +PLATFORM_SCHEMA = HUMIDIFIER_PLATFORM_SCHEMA.extend(HYGROSTAT_SCHEMA.schema) async def async_setup_platform( From 17946c4b45e3a4ceb72cc5e14bb54708ae029ee4 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 26 Jun 2024 11:24:45 +0200 Subject: [PATCH 1245/1445] Force alias when importing geo location PLATFORM_SCHEMA (#120525) --- homeassistant/components/ign_sismologia/geo_location.py | 7 +++++-- .../components/nsw_rural_fire_service_feed/geo_location.py | 7 +++++-- homeassistant/components/qld_bushfire/geo_location.py | 7 +++++-- .../components/usgs_earthquakes_feed/geo_location.py | 7 +++++-- 4 files changed, 20 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/ign_sismologia/geo_location.py b/homeassistant/components/ign_sismologia/geo_location.py index af7fab5b79b..779891f4bc2 100644 --- a/homeassistant/components/ign_sismologia/geo_location.py +++ b/homeassistant/components/ign_sismologia/geo_location.py @@ -13,7 +13,10 @@ from georss_ign_sismologia_client import ( ) import voluptuous as vol -from homeassistant.components.geo_location import PLATFORM_SCHEMA, GeolocationEvent +from homeassistant.components.geo_location import ( + PLATFORM_SCHEMA as GEO_LOCATION_PLATFORM_SCHEMA, + GeolocationEvent, +) from homeassistant.const import ( CONF_LATITUDE, CONF_LONGITUDE, @@ -47,7 +50,7 @@ SCAN_INTERVAL = timedelta(minutes=5) SOURCE = "ign_sismologia" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = GEO_LOCATION_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_LATITUDE): cv.latitude, vol.Optional(CONF_LONGITUDE): cv.longitude, diff --git a/homeassistant/components/nsw_rural_fire_service_feed/geo_location.py b/homeassistant/components/nsw_rural_fire_service_feed/geo_location.py index 24bae7f7b12..230141379e5 100644 --- a/homeassistant/components/nsw_rural_fire_service_feed/geo_location.py +++ b/homeassistant/components/nsw_rural_fire_service_feed/geo_location.py @@ -13,7 +13,10 @@ from aio_geojson_nsw_rfs_incidents.feed_entry import ( ) import voluptuous as vol -from homeassistant.components.geo_location import PLATFORM_SCHEMA, GeolocationEvent +from homeassistant.components.geo_location import ( + PLATFORM_SCHEMA as GEO_LOCATION_PLATFORM_SCHEMA, + GeolocationEvent, +) from homeassistant.const import ( ATTR_LOCATION, CONF_LATITUDE, @@ -59,7 +62,7 @@ SOURCE = "nsw_rural_fire_service_feed" VALID_CATEGORIES = ["Advice", "Emergency Warning", "Not Applicable", "Watch and Act"] -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = GEO_LOCATION_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_CATEGORIES, default=[]): vol.All( cv.ensure_list, [vol.In(VALID_CATEGORIES)] diff --git a/homeassistant/components/qld_bushfire/geo_location.py b/homeassistant/components/qld_bushfire/geo_location.py index 5d0173f8c54..c8cfc30b2b5 100644 --- a/homeassistant/components/qld_bushfire/geo_location.py +++ b/homeassistant/components/qld_bushfire/geo_location.py @@ -13,7 +13,10 @@ from georss_qld_bushfire_alert_client import ( ) import voluptuous as vol -from homeassistant.components.geo_location import PLATFORM_SCHEMA, GeolocationEvent +from homeassistant.components.geo_location import ( + PLATFORM_SCHEMA as GEO_LOCATION_PLATFORM_SCHEMA, + GeolocationEvent, +) from homeassistant.const import ( CONF_LATITUDE, CONF_LONGITUDE, @@ -56,7 +59,7 @@ VALID_CATEGORIES = [ "Information", ] -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = GEO_LOCATION_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_LATITUDE): cv.latitude, vol.Optional(CONF_LONGITUDE): cv.longitude, diff --git a/homeassistant/components/usgs_earthquakes_feed/geo_location.py b/homeassistant/components/usgs_earthquakes_feed/geo_location.py index c8ee88a84ed..33455dc11a9 100644 --- a/homeassistant/components/usgs_earthquakes_feed/geo_location.py +++ b/homeassistant/components/usgs_earthquakes_feed/geo_location.py @@ -13,7 +13,10 @@ from aio_geojson_usgs_earthquakes.feed_entry import ( ) import voluptuous as vol -from homeassistant.components.geo_location import PLATFORM_SCHEMA, GeolocationEvent +from homeassistant.components.geo_location import ( + PLATFORM_SCHEMA as GEO_LOCATION_PLATFORM_SCHEMA, + GeolocationEvent, +) from homeassistant.const import ( ATTR_TIME, CONF_LATITUDE, @@ -81,7 +84,7 @@ VALID_FEED_TYPES = [ "past_month_all_earthquakes", ] -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = GEO_LOCATION_PLATFORM_SCHEMA.extend( { vol.Required(CONF_FEED_TYPE): vol.In(VALID_FEED_TYPES), vol.Optional(CONF_LATITUDE): cv.latitude, From 2c17d84fab61eed6f00cc79c2cec89ce0c2ac024 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 26 Jun 2024 11:25:14 +0200 Subject: [PATCH 1246/1445] Force alias when importing cover PLATFORM_SCHEMA (#120522) --- homeassistant/components/ads/cover.py | 4 ++-- homeassistant/components/garadget/cover.py | 4 ++-- homeassistant/components/group/cover.py | 4 ++-- homeassistant/components/rflink/cover.py | 7 +++++-- homeassistant/components/scsgate/cover.py | 7 +++++-- homeassistant/components/template/cover.py | 4 ++-- 6 files changed, 18 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/ads/cover.py b/homeassistant/components/ads/cover.py index c54b3e14267..b0dded8d4d5 100644 --- a/homeassistant/components/ads/cover.py +++ b/homeassistant/components/ads/cover.py @@ -10,7 +10,7 @@ import voluptuous as vol from homeassistant.components.cover import ( ATTR_POSITION, DEVICE_CLASSES_SCHEMA, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as COVER_PLATFORM_SCHEMA, CoverEntity, CoverEntityFeature, ) @@ -36,7 +36,7 @@ CONF_ADS_VAR_OPEN = "adsvar_open" CONF_ADS_VAR_CLOSE = "adsvar_close" CONF_ADS_VAR_STOP = "adsvar_stop" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = COVER_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_ADS_VAR): cv.string, vol.Optional(CONF_ADS_VAR_POSITION): cv.string, diff --git a/homeassistant/components/garadget/cover.py b/homeassistant/components/garadget/cover.py index f168652b3cf..cb4f402d7bb 100644 --- a/homeassistant/components/garadget/cover.py +++ b/homeassistant/components/garadget/cover.py @@ -9,7 +9,7 @@ import requests import voluptuous as vol from homeassistant.components.cover import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as COVER_PLATFORM_SCHEMA, CoverDeviceClass, CoverEntity, ) @@ -61,7 +61,7 @@ COVER_SCHEMA = vol.Schema( } ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = COVER_PLATFORM_SCHEMA.extend( {vol.Required(CONF_COVERS): cv.schema_with_slug_keys(COVER_SCHEMA)} ) diff --git a/homeassistant/components/group/cover.py b/homeassistant/components/group/cover.py index 02e5ebbc7cd..5d7f99012fd 100644 --- a/homeassistant/components/group/cover.py +++ b/homeassistant/components/group/cover.py @@ -12,7 +12,7 @@ from homeassistant.components.cover import ( ATTR_POSITION, ATTR_TILT_POSITION, DOMAIN, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as COVER_PLATFORM_SCHEMA, CoverEntity, CoverEntityFeature, ) @@ -55,7 +55,7 @@ DEFAULT_NAME = "Cover Group" # No limit on parallel updates to enable a group calling another group PARALLEL_UPDATES = 0 -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = COVER_PLATFORM_SCHEMA.extend( { vol.Required(CONF_ENTITIES): cv.entities_domain(DOMAIN), vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/rflink/cover.py b/homeassistant/components/rflink/cover.py index d440b324532..54a84a68a2e 100644 --- a/homeassistant/components/rflink/cover.py +++ b/homeassistant/components/rflink/cover.py @@ -7,7 +7,10 @@ from typing import Any import voluptuous as vol -from homeassistant.components.cover import PLATFORM_SCHEMA, CoverEntity +from homeassistant.components.cover import ( + PLATFORM_SCHEMA as COVER_PLATFORM_SCHEMA, + CoverEntity, +) from homeassistant.const import CONF_DEVICES, CONF_NAME, CONF_TYPE, STATE_OPEN from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -34,7 +37,7 @@ PARALLEL_UPDATES = 0 TYPE_STANDARD = "standard" TYPE_INVERTED = "inverted" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = COVER_PLATFORM_SCHEMA.extend( { vol.Optional( CONF_DEVICE_DEFAULTS, default=DEVICE_DEFAULTS_SCHEMA({}) diff --git a/homeassistant/components/scsgate/cover.py b/homeassistant/components/scsgate/cover.py index 8f17ca170a0..b6d3317555c 100644 --- a/homeassistant/components/scsgate/cover.py +++ b/homeassistant/components/scsgate/cover.py @@ -12,7 +12,10 @@ from scsgate.tasks import ( ) import voluptuous as vol -from homeassistant.components.cover import PLATFORM_SCHEMA, CoverEntity +from homeassistant.components.cover import ( + PLATFORM_SCHEMA as COVER_PLATFORM_SCHEMA, + CoverEntity, +) from homeassistant.const import CONF_DEVICES, CONF_NAME from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -21,7 +24,7 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import CONF_SCS_ID, DOMAIN, SCSGATE_SCHEMA -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = COVER_PLATFORM_SCHEMA.extend( {vol.Required(CONF_DEVICES): cv.schema_with_slug_keys(SCSGATE_SCHEMA)} ) diff --git a/homeassistant/components/template/cover.py b/homeassistant/components/template/cover.py index 36ea9f93830..d50067f6278 100644 --- a/homeassistant/components/template/cover.py +++ b/homeassistant/components/template/cover.py @@ -12,7 +12,7 @@ from homeassistant.components.cover import ( ATTR_TILT_POSITION, DEVICE_CLASSES_SCHEMA, ENTITY_ID_FORMAT, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as COVER_PLATFORM_SCHEMA, CoverEntity, CoverEntityFeature, ) @@ -96,7 +96,7 @@ COVER_SCHEMA = vol.All( cv.has_at_least_one_key(OPEN_ACTION, POSITION_ACTION), ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = COVER_PLATFORM_SCHEMA.extend( {vol.Required(CONF_COVERS): cv.schema_with_slug_keys(COVER_SCHEMA)} ) From afbd24adfe05c46a071701b21d0dc08fdaec1854 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 26 Jun 2024 11:29:52 +0200 Subject: [PATCH 1247/1445] Force alias when importing image processing PLATFORM_SCHEMA (#120527) --- .../components/dlib_face_identify/image_processing.py | 4 ++-- homeassistant/components/doods/image_processing.py | 4 ++-- .../components/microsoft_face_detect/image_processing.py | 4 ++-- .../components/microsoft_face_identify/image_processing.py | 6 ++++-- homeassistant/components/openalpr_cloud/image_processing.py | 4 ++-- homeassistant/components/seven_segments/image_processing.py | 4 ++-- homeassistant/components/sighthound/image_processing.py | 4 ++-- homeassistant/components/tensorflow/image_processing.py | 4 ++-- 8 files changed, 18 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/dlib_face_identify/image_processing.py b/homeassistant/components/dlib_face_identify/image_processing.py index ac9e69ec9e1..e17f892a7fe 100644 --- a/homeassistant/components/dlib_face_identify/image_processing.py +++ b/homeassistant/components/dlib_face_identify/image_processing.py @@ -10,7 +10,7 @@ import voluptuous as vol from homeassistant.components.image_processing import ( CONF_CONFIDENCE, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as IMAGE_PROCESSING_PLATFORM_SCHEMA, ImageProcessingFaceEntity, ) from homeassistant.const import ATTR_NAME, CONF_ENTITY_ID, CONF_NAME, CONF_SOURCE @@ -23,7 +23,7 @@ _LOGGER = logging.getLogger(__name__) CONF_FACES = "faces" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = IMAGE_PROCESSING_PLATFORM_SCHEMA.extend( { vol.Required(CONF_FACES): {cv.string: cv.isfile}, vol.Optional(CONF_CONFIDENCE, default=0.6): vol.Coerce(float), diff --git a/homeassistant/components/doods/image_processing.py b/homeassistant/components/doods/image_processing.py index 11985ef4889..7ffb6655bb6 100644 --- a/homeassistant/components/doods/image_processing.py +++ b/homeassistant/components/doods/image_processing.py @@ -13,7 +13,7 @@ import voluptuous as vol from homeassistant.components.image_processing import ( CONF_CONFIDENCE, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as IMAGE_PROCESSING_PLATFORM_SCHEMA, ImageProcessingEntity, ) from homeassistant.const import ( @@ -66,7 +66,7 @@ LABEL_SCHEMA = vol.Schema( } ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = IMAGE_PROCESSING_PLATFORM_SCHEMA.extend( { vol.Required(CONF_URL): cv.string, vol.Required(CONF_DETECTOR): cv.string, diff --git a/homeassistant/components/microsoft_face_detect/image_processing.py b/homeassistant/components/microsoft_face_detect/image_processing.py index ef8a4f5df4b..80037a29fa8 100644 --- a/homeassistant/components/microsoft_face_detect/image_processing.py +++ b/homeassistant/components/microsoft_face_detect/image_processing.py @@ -10,7 +10,7 @@ from homeassistant.components.image_processing import ( ATTR_AGE, ATTR_GENDER, ATTR_GLASSES, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as IMAGE_PROCESSING_PLATFORM_SCHEMA, ImageProcessingFaceEntity, ) from homeassistant.components.microsoft_face import DATA_MICROSOFT_FACE @@ -37,7 +37,7 @@ def validate_attributes(list_attributes): return list_attributes -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = IMAGE_PROCESSING_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_ATTRIBUTES, default=DEFAULT_ATTRIBUTES): vol.All( cv.ensure_list, validate_attributes diff --git a/homeassistant/components/microsoft_face_identify/image_processing.py b/homeassistant/components/microsoft_face_identify/image_processing.py index d1af1d4a827..03a6ad22fcd 100644 --- a/homeassistant/components/microsoft_face_identify/image_processing.py +++ b/homeassistant/components/microsoft_face_identify/image_processing.py @@ -9,7 +9,7 @@ import voluptuous as vol from homeassistant.components.image_processing import ( ATTR_CONFIDENCE, CONF_CONFIDENCE, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as IMAGE_PROCESSING_PLATFORM_SCHEMA, ImageProcessingFaceEntity, ) from homeassistant.components.microsoft_face import DATA_MICROSOFT_FACE @@ -24,7 +24,9 @@ _LOGGER = logging.getLogger(__name__) CONF_GROUP = "group" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({vol.Required(CONF_GROUP): cv.slugify}) +PLATFORM_SCHEMA = IMAGE_PROCESSING_PLATFORM_SCHEMA.extend( + {vol.Required(CONF_GROUP): cv.slugify} +) async def async_setup_platform( diff --git a/homeassistant/components/openalpr_cloud/image_processing.py b/homeassistant/components/openalpr_cloud/image_processing.py index 2a8fe328c7d..e8a8d6859c1 100644 --- a/homeassistant/components/openalpr_cloud/image_processing.py +++ b/homeassistant/components/openalpr_cloud/image_processing.py @@ -13,7 +13,7 @@ import voluptuous as vol from homeassistant.components.image_processing import ( ATTR_CONFIDENCE, CONF_CONFIDENCE, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as IMAGE_PROCESSING_PLATFORM_SCHEMA, ImageProcessingDeviceClass, ImageProcessingEntity, ) @@ -57,7 +57,7 @@ OPENALPR_REGIONS = [ "vn2", ] -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = IMAGE_PROCESSING_PLATFORM_SCHEMA.extend( { vol.Required(CONF_API_KEY): cv.string, vol.Required(CONF_REGION): vol.All(vol.Lower, vol.In(OPENALPR_REGIONS)), diff --git a/homeassistant/components/seven_segments/image_processing.py b/homeassistant/components/seven_segments/image_processing.py index 622ceb761a0..7b41a1702c0 100644 --- a/homeassistant/components/seven_segments/image_processing.py +++ b/homeassistant/components/seven_segments/image_processing.py @@ -11,7 +11,7 @@ from PIL import Image import voluptuous as vol from homeassistant.components.image_processing import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as IMAGE_PROCESSING_PLATFORM_SCHEMA, ImageProcessingDeviceClass, ImageProcessingEntity, ) @@ -35,7 +35,7 @@ CONF_Y_POS = "y_position" DEFAULT_BINARY = "ssocr" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = IMAGE_PROCESSING_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_EXTRA_ARGUMENTS, default=""): cv.string, vol.Optional(CONF_DIGITS): cv.positive_int, diff --git a/homeassistant/components/sighthound/image_processing.py b/homeassistant/components/sighthound/image_processing.py index bcfa4bca3c2..706a8dd037a 100644 --- a/homeassistant/components/sighthound/image_processing.py +++ b/homeassistant/components/sighthound/image_processing.py @@ -11,7 +11,7 @@ import simplehound.core as hound import voluptuous as vol from homeassistant.components.image_processing import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as IMAGE_PROCESSING_PLATFORM_SCHEMA, ImageProcessingEntity, ) from homeassistant.const import ( @@ -41,7 +41,7 @@ DATETIME_FORMAT = "%Y-%m-%d_%H:%M:%S" DEV = "dev" PROD = "prod" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = IMAGE_PROCESSING_PLATFORM_SCHEMA.extend( { vol.Required(CONF_API_KEY): cv.string, vol.Optional(CONF_ACCOUNT_TYPE, default=DEV): vol.In([DEV, PROD]), diff --git a/homeassistant/components/tensorflow/image_processing.py b/homeassistant/components/tensorflow/image_processing.py index c78c2bc2312..85fe6439f1c 100644 --- a/homeassistant/components/tensorflow/image_processing.py +++ b/homeassistant/components/tensorflow/image_processing.py @@ -15,7 +15,7 @@ import voluptuous as vol from homeassistant.components.image_processing import ( CONF_CONFIDENCE, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as IMAGE_PROCESSING_PLATFORM_SCHEMA, ImageProcessingEntity, ) from homeassistant.const import ( @@ -68,7 +68,7 @@ CATEGORY_SCHEMA = vol.Schema( {vol.Required(CONF_CATEGORY): cv.string, vol.Optional(CONF_AREA): AREA_SCHEMA} ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = IMAGE_PROCESSING_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_FILE_OUT, default=[]): vol.All(cv.ensure_list, [cv.template]), vol.Required(CONF_MODEL): vol.Schema( From d527113d5992ab95c75fc8bd81b3d9a6ff067be7 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Wed, 26 Jun 2024 11:30:07 +0200 Subject: [PATCH 1248/1445] Improve schema typing (3) (#120521) --- .../components/input_button/__init__.py | 4 ++-- homeassistant/components/input_text/__init__.py | 4 ++-- homeassistant/components/light/__init__.py | 6 ++++-- .../components/motioneye/config_flow.py | 8 +++++--- homeassistant/components/zha/device_action.py | 5 +++-- .../components/zwave_js/triggers/event.py | 6 ++---- homeassistant/data_entry_flow.py | 9 +++++---- homeassistant/helpers/config_validation.py | 12 ++++++------ homeassistant/helpers/intent.py | 17 ++++++++++------- .../helpers/schema_config_entry_flow.py | 8 +++++--- 10 files changed, 44 insertions(+), 35 deletions(-) diff --git a/homeassistant/components/input_button/__init__.py b/homeassistant/components/input_button/__init__.py index e70bbacd933..47ec36969c6 100644 --- a/homeassistant/components/input_button/__init__.py +++ b/homeassistant/components/input_button/__init__.py @@ -58,9 +58,9 @@ class InputButtonStorageCollection(collection.DictStorageCollection): CREATE_UPDATE_SCHEMA = vol.Schema(STORAGE_FIELDS) - async def _process_create_data(self, data: dict) -> vol.Schema: + async def _process_create_data(self, data: dict) -> dict[str, str]: """Validate the config is valid.""" - return self.CREATE_UPDATE_SCHEMA(data) + return self.CREATE_UPDATE_SCHEMA(data) # type: ignore[no-any-return] @callback def _get_suggested_id(self, info: dict) -> str: diff --git a/homeassistant/components/input_text/__init__.py b/homeassistant/components/input_text/__init__.py index 3d75ff9f5c2..7d8f6663673 100644 --- a/homeassistant/components/input_text/__init__.py +++ b/homeassistant/components/input_text/__init__.py @@ -163,9 +163,9 @@ class InputTextStorageCollection(collection.DictStorageCollection): CREATE_UPDATE_SCHEMA = vol.Schema(vol.All(STORAGE_FIELDS, _cv_input_text)) - async def _process_create_data(self, data: dict[str, Any]) -> vol.Schema: + async def _process_create_data(self, data: dict[str, Any]) -> dict[str, Any]: """Validate the config is valid.""" - return self.CREATE_UPDATE_SCHEMA(data) + return self.CREATE_UPDATE_SCHEMA(data) # type: ignore[no-any-return] @callback def _get_suggested_id(self, info: dict[str, Any]) -> str: diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index 16367c35ec5..b61625edaf2 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -302,7 +302,7 @@ def is_on(hass: HomeAssistant, entity_id: str) -> bool: def preprocess_turn_on_alternatives( - hass: HomeAssistant, params: dict[str, Any] + hass: HomeAssistant, params: dict[str, Any] | dict[str | vol.Optional, Any] ) -> None: """Process extra data for turn light on request. @@ -406,7 +406,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: # of the light base platform. hass.async_create_task(profiles.async_initialize(), eager_start=True) - def preprocess_data(data: dict[str, Any]) -> dict[str | vol.Optional, Any]: + def preprocess_data( + data: dict[str | vol.Optional, Any], + ) -> dict[str | vol.Optional, Any]: """Preprocess the service data.""" base: dict[str | vol.Optional, Any] = { entity_field: data.pop(entity_field) diff --git a/homeassistant/components/motioneye/config_flow.py b/homeassistant/components/motioneye/config_flow.py index bbbd2bc7fba..49059b528db 100644 --- a/homeassistant/components/motioneye/config_flow.py +++ b/homeassistant/components/motioneye/config_flow.py @@ -226,14 +226,16 @@ class MotionEyeOptionsFlow(OptionsFlow): if self.show_advanced_options: # The input URL is not validated as being a URL, to allow for the possibility # the template input won't be a valid URL until after it's rendered - stream_kwargs = {} + description: dict[str, str] | None = None if CONF_STREAM_URL_TEMPLATE in self._config_entry.options: - stream_kwargs["description"] = { + description = { "suggested_value": self._config_entry.options[ CONF_STREAM_URL_TEMPLATE ] } - schema[vol.Optional(CONF_STREAM_URL_TEMPLATE, **stream_kwargs)] = str + schema[vol.Optional(CONF_STREAM_URL_TEMPLATE, description=description)] = ( + str + ) return self.async_show_form(step_id="init", data_schema=vol.Schema(schema)) diff --git a/homeassistant/components/zha/device_action.py b/homeassistant/components/zha/device_action.py index 8f5a03a7fe5..a0f16d61f41 100644 --- a/homeassistant/components/zha/device_action.py +++ b/homeassistant/components/zha/device_action.py @@ -167,8 +167,9 @@ async def async_get_action_capabilities( hass: HomeAssistant, config: ConfigType ) -> dict[str, vol.Schema]: """List action capabilities.""" - - return {"extra_fields": DEVICE_ACTION_SCHEMAS.get(config[CONF_TYPE], {})} + if (fields := DEVICE_ACTION_SCHEMAS.get(config[CONF_TYPE])) is None: + return {} + return {"extra_fields": fields} async def _execute_service_based_action( diff --git a/homeassistant/components/zwave_js/triggers/event.py b/homeassistant/components/zwave_js/triggers/event.py index 921cae19b3a..9938d08408c 100644 --- a/homeassistant/components/zwave_js/triggers/event.py +++ b/homeassistant/components/zwave_js/triggers/event.py @@ -80,10 +80,8 @@ def validate_event_data(obj: dict) -> dict: except ValidationError as exc: # Filter out required field errors if keys can be missing, and if there are # still errors, raise an exception - if errors := [ - error for error in exc.errors() if error["type"] != "value_error.missing" - ]: - raise vol.MultipleInvalid(errors) from exc + if [error for error in exc.errors() if error["type"] != "value_error.missing"]: + raise vol.MultipleInvalid from exc return obj diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py index 155e64d259e..f632e3e4dde 100644 --- a/homeassistant/data_entry_flow.py +++ b/homeassistant/data_entry_flow.py @@ -5,7 +5,7 @@ from __future__ import annotations import abc import asyncio from collections import defaultdict -from collections.abc import Callable, Container, Iterable, Mapping +from collections.abc import Callable, Container, Hashable, Iterable, Mapping from contextlib import suppress import copy from dataclasses import dataclass @@ -13,7 +13,7 @@ from enum import StrEnum from functools import partial import logging from types import MappingProxyType -from typing import Any, Generic, Required, TypedDict +from typing import Any, Generic, Required, TypedDict, cast from typing_extensions import TypeVar import voluptuous as vol @@ -120,7 +120,7 @@ class InvalidData(vol.Invalid): # type: ignore[misc] def __init__( self, message: str, - path: list[str | vol.Marker] | None, + path: list[Hashable] | None, error_message: str | None, schema_errors: dict[str, Any], **kwargs: Any, @@ -384,6 +384,7 @@ class FlowManager(abc.ABC, Generic[_FlowResultT, _HandlerT]): if ( data_schema := cur_step.get("data_schema") ) is not None and user_input is not None: + data_schema = cast(vol.Schema, data_schema) try: user_input = data_schema(user_input) # type: ignore[operator] except vol.Invalid as ex: @@ -694,7 +695,7 @@ class FlowHandler(Generic[_FlowResultT, _HandlerT]): ): # Copy the marker to not modify the flow schema new_key = copy.copy(key) - new_key.description = {"suggested_value": suggested_values[key]} + new_key.description = {"suggested_value": suggested_values[key.schema]} schema[new_key] = val return vol.Schema(schema) diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 558baaeb779..58c76a40c8e 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -981,7 +981,7 @@ def removed( def key_value_schemas( key: str, - value_schemas: dict[Hashable, VolSchemaType], + value_schemas: dict[Hashable, VolSchemaType | Callable[[Any], dict[str, Any]]], default_schema: VolSchemaType | None = None, default_description: str | None = None, ) -> Callable[[Any], dict[Hashable, Any]]: @@ -1016,12 +1016,12 @@ def key_value_schemas( # Validator helpers -def key_dependency( +def key_dependency[_KT: Hashable, _VT]( key: Hashable, dependency: Hashable -) -> Callable[[dict[Hashable, Any]], dict[Hashable, Any]]: +) -> Callable[[dict[_KT, _VT]], dict[_KT, _VT]]: """Validate that all dependencies exist for key.""" - def validator(value: dict[Hashable, Any]) -> dict[Hashable, Any]: + def validator(value: dict[_KT, _VT]) -> dict[_KT, _VT]: """Test dependencies.""" if not isinstance(value, dict): raise vol.Invalid("key dependencies require a dict") @@ -1405,13 +1405,13 @@ STATE_CONDITION_ATTRIBUTE_SCHEMA = vol.Schema( ) -def STATE_CONDITION_SCHEMA(value: Any) -> dict: +def STATE_CONDITION_SCHEMA(value: Any) -> dict[str, Any]: """Validate a state condition.""" if not isinstance(value, dict): raise vol.Invalid("Expected a dictionary") if CONF_ATTRIBUTE in value: - validated: dict = STATE_CONDITION_ATTRIBUTE_SCHEMA(value) + validated: dict[str, Any] = STATE_CONDITION_ATTRIBUTE_SCHEMA(value) else: validated = STATE_CONDITION_STATE_SCHEMA(value) diff --git a/homeassistant/helpers/intent.py b/homeassistant/helpers/intent.py index e191bddf102..1bf78ae3a29 100644 --- a/homeassistant/helpers/intent.py +++ b/homeassistant/helpers/intent.py @@ -4,7 +4,7 @@ from __future__ import annotations from abc import abstractmethod import asyncio -from collections.abc import Collection, Coroutine, Iterable +from collections.abc import Callable, Collection, Coroutine, Iterable import dataclasses from dataclasses import dataclass, field from enum import Enum, auto @@ -37,6 +37,9 @@ from .typing import VolSchemaType _LOGGER = logging.getLogger(__name__) type _SlotsType = dict[str, Any] +type _IntentSlotsType = dict[ + str | tuple[str, str], VolSchemaType | Callable[[Any], Any] +] INTENT_TURN_OFF = "HassTurnOff" INTENT_TURN_ON = "HassTurnOn" @@ -808,8 +811,8 @@ class DynamicServiceIntentHandler(IntentHandler): self, intent_type: str, speech: str | None = None, - required_slots: dict[str | tuple[str, str], VolSchemaType] | None = None, - optional_slots: dict[str | tuple[str, str], VolSchemaType] | None = None, + required_slots: _IntentSlotsType | None = None, + optional_slots: _IntentSlotsType | None = None, required_domains: set[str] | None = None, required_features: int | None = None, required_states: set[str] | None = None, @@ -825,7 +828,7 @@ class DynamicServiceIntentHandler(IntentHandler): self.description = description self.platforms = platforms - self.required_slots: dict[tuple[str, str], VolSchemaType] = {} + self.required_slots: _IntentSlotsType = {} if required_slots: for key, value_schema in required_slots.items(): if isinstance(key, str): @@ -834,7 +837,7 @@ class DynamicServiceIntentHandler(IntentHandler): self.required_slots[key] = value_schema - self.optional_slots: dict[tuple[str, str], VolSchemaType] = {} + self.optional_slots: _IntentSlotsType = {} if optional_slots: for key, value_schema in optional_slots.items(): if isinstance(key, str): @@ -1108,8 +1111,8 @@ class ServiceIntentHandler(DynamicServiceIntentHandler): domain: str, service: str, speech: str | None = None, - required_slots: dict[str | tuple[str, str], VolSchemaType] | None = None, - optional_slots: dict[str | tuple[str, str], VolSchemaType] | None = None, + required_slots: _IntentSlotsType | None = None, + optional_slots: _IntentSlotsType | None = None, required_domains: set[str] | None = None, required_features: int | None = None, required_states: set[str] | None = None, diff --git a/homeassistant/helpers/schema_config_entry_flow.py b/homeassistant/helpers/schema_config_entry_flow.py index 05e4a852ad9..7463c9945b2 100644 --- a/homeassistant/helpers/schema_config_entry_flow.py +++ b/homeassistant/helpers/schema_config_entry_flow.py @@ -175,7 +175,9 @@ class SchemaCommonFlowHandler: and key.default is not vol.UNDEFINED and key not in self._options ): - user_input[str(key.schema)] = key.default() + user_input[str(key.schema)] = cast( + Callable[[], Any], key.default + )() if user_input is not None and form_step.validate_user_input is not None: # Do extra validation of user input @@ -215,7 +217,7 @@ class SchemaCommonFlowHandler: ) ): # Key not present, delete keys old value (if present) too - values.pop(key, None) + values.pop(key.schema, None) async def _show_next_step_or_create_entry( self, form_step: SchemaFlowFormStep @@ -491,7 +493,7 @@ def wrapped_entity_config_entry_title( def entity_selector_without_own_entities( handler: SchemaOptionsFlowHandler, entity_selector_config: selector.EntitySelectorConfig, -) -> vol.Schema: +) -> selector.EntitySelector: """Return an entity selector which excludes own entities.""" entity_registry = er.async_get(handler.hass) entities = er.async_entries_for_config_entry( From ae73500bebe759d22652486cb23960deb4ca8ab1 Mon Sep 17 00:00:00 2001 From: hahn-th <15319212+hahn-th@users.noreply.github.com> Date: Wed, 26 Jun 2024 11:33:44 +0200 Subject: [PATCH 1249/1445] Add HmIP-ESI (#116863) --- .../homematicip_cloud/generic_entity.py | 26 +- .../components/homematicip_cloud/helpers.py | 12 + .../components/homematicip_cloud/sensor.py | 168 ++++++- .../fixtures/homematicip_cloud.json | 410 ++++++++++++++++++ .../homematicip_cloud/test_device.py | 2 +- .../homematicip_cloud/test_sensor.py | 123 ++++++ 6 files changed, 733 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/homematicip_cloud/generic_entity.py b/homeassistant/components/homematicip_cloud/generic_entity.py index a2e6f8a145f..5cd48515ad7 100644 --- a/homeassistant/components/homematicip_cloud/generic_entity.py +++ b/homeassistant/components/homematicip_cloud/generic_entity.py @@ -7,6 +7,7 @@ from typing import Any from homematicip.aio.device import AsyncDevice from homematicip.aio.group import AsyncGroup +from homematicip.base.functionalChannels import FunctionalChannel from homeassistant.const import ATTR_ID from homeassistant.core import callback @@ -91,6 +92,7 @@ class HomematicipGenericEntity(Entity): self._post = post self._channel = channel self._is_multi_channel = is_multi_channel + self.functional_channel = self.get_current_channel() # Marker showing that the HmIP device hase been removed. self.hmip_device_removed = False _LOGGER.info("Setting up %s (%s)", self.name, self._device.modelType) @@ -214,13 +216,14 @@ class HomematicipGenericEntity(Entity): @property def unique_id(self) -> str: """Return a unique ID.""" - unique_id = f"{self.__class__.__name__}_{self._device.id}" - if self._is_multi_channel: - unique_id = ( - f"{self.__class__.__name__}_Channel{self._channel}_{self._device.id}" - ) + suffix = "" + if self._post is not None: + suffix = f"_{self._post}" - return unique_id + if self._is_multi_channel: + return f"{self.__class__.__name__}_Channel{self._channel}_{self._device.id}{suffix}" + + return f"{self.__class__.__name__}_{self._device.id}{suffix}" @property def icon(self) -> str | None: @@ -251,3 +254,14 @@ class HomematicipGenericEntity(Entity): state_attr[ATTR_IS_GROUP] = True return state_attr + + def get_current_channel(self) -> FunctionalChannel: + """Return the FunctionalChannel for device.""" + if hasattr(self._device, "functionalChannels"): + if self._is_multi_channel: + return self._device.functionalChannels[self._channel] + + if len(self._device.functionalChannels) > 1: + return self._device.functionalChannels[1] + + return None diff --git a/homeassistant/components/homematicip_cloud/helpers.py b/homeassistant/components/homematicip_cloud/helpers.py index 4ac9af48ee1..5b7f98ad884 100644 --- a/homeassistant/components/homematicip_cloud/helpers.py +++ b/homeassistant/components/homematicip_cloud/helpers.py @@ -8,6 +8,9 @@ import json import logging from typing import Any, Concatenate, TypeGuard +from homematicip.base.enums import FunctionalChannelType +from homematicip.device import Device + from homeassistant.exceptions import HomeAssistantError from . import HomematicipGenericEntity @@ -47,3 +50,12 @@ def handle_errors[_HomematicipGenericEntityT: HomematicipGenericEntity, **_P]( ) return inner + + +def get_channels_from_device(device: Device, channel_type: FunctionalChannelType): + """Get all channels matching with channel_type from device.""" + return [ + ch + for ch in device.functionalChannels + if ch.functionalChannelType == channel_type + ] diff --git a/homeassistant/components/homematicip_cloud/sensor.py b/homeassistant/components/homematicip_cloud/sensor.py index d344639bbc9..6cdff6caef3 100644 --- a/homeassistant/components/homematicip_cloud/sensor.py +++ b/homeassistant/components/homematicip_cloud/sensor.py @@ -2,10 +2,13 @@ from __future__ import annotations +from collections.abc import Callable +from dataclasses import dataclass from typing import Any from homematicip.aio.device import ( AsyncBrandSwitchMeasuring, + AsyncEnergySensorsInterface, AsyncFullFlushSwitchMeasuring, AsyncHeatingThermostat, AsyncHeatingThermostatCompact, @@ -27,11 +30,13 @@ from homematicip.aio.device import ( AsyncWeatherSensorPlus, AsyncWeatherSensorPro, ) -from homematicip.base.enums import ValveState +from homematicip.base.enums import FunctionalChannelType, ValveState +from homematicip.base.functionalChannels import FunctionalChannel from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, + SensorEntityDescription, SensorStateClass, ) from homeassistant.config_entries import ConfigEntry @@ -43,12 +48,16 @@ from homeassistant.const import ( UnitOfPrecipitationDepth, UnitOfSpeed, UnitOfTemperature, + UnitOfVolume, + UnitOfVolumeFlowRate, ) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import StateType from . import DOMAIN as HMIPC_DOMAIN, HomematicipGenericEntity from .hap import HomematicipHAP +from .helpers import get_channels_from_device ATTR_CURRENT_ILLUMINATION = "current_illumination" ATTR_LOWEST_ILLUMINATION = "lowest_illumination" @@ -58,6 +67,18 @@ ATTR_RIGHT_COUNTER = "right_counter" ATTR_TEMPERATURE_OFFSET = "temperature_offset" ATTR_WIND_DIRECTION = "wind_direction" ATTR_WIND_DIRECTION_VARIATION = "wind_direction_variation_in_degree" +ATTR_ESI_TYPE = "type" +ESI_TYPE_UNKNOWN = "UNKNOWN" +ESI_CONNECTED_SENSOR_TYPE_IEC = "ES_IEC" +ESI_CONNECTED_SENSOR_TYPE_GAS = "ES_GAS" +ESI_CONNECTED_SENSOR_TYPE_LED = "ES_LED" + +ESI_TYPE_CURRENT_POWER_CONSUMPTION = "CurrentPowerConsumption" +ESI_TYPE_ENERGY_COUNTER_USAGE_HIGH_TARIFF = "ENERGY_COUNTER_USAGE_HIGH_TARIFF" +ESI_TYPE_ENERGY_COUNTER_USAGE_LOW_TARIFF = "ENERGY_COUNTER_USAGE_LOW_TARIFF" +ESI_TYPE_ENERGY_COUNTER_INPUT_SINGLE_TARIFF = "ENERGY_COUNTER_INPUT_SINGLE_TARIFF" +ESI_TYPE_CURRENT_GAS_FLOW = "CurrentGasFlow" +ESI_TYPE_CURRENT_GAS_VOLUME = "GasVolume" ILLUMINATION_DEVICE_ATTRIBUTES = { "currentIllumination": ATTR_CURRENT_ILLUMINATION, @@ -138,6 +159,23 @@ async def async_setup_entry( entities.append(HomematicpTemperatureExternalSensorCh1(hap, device)) entities.append(HomematicpTemperatureExternalSensorCh2(hap, device)) entities.append(HomematicpTemperatureExternalSensorDelta(hap, device)) + if isinstance(device, AsyncEnergySensorsInterface): + for ch in get_channels_from_device( + device, FunctionalChannelType.ENERGY_SENSORS_INTERFACE_CHANNEL + ): + if ch.connectedEnergySensorType not in SENSORS_ESI: + continue + + new_entities = [ + HmipEsiSensorEntity(hap, device, ch.index, description) + for description in SENSORS_ESI[ch.connectedEnergySensorType] + ] + + entities.extend( + entity + for entity in new_entities + if entity.entity_description.exists_fn(ch) + ) async_add_entities(entities) @@ -396,6 +434,134 @@ class HomematicpTemperatureExternalSensorDelta(HomematicipGenericEntity, SensorE return self._device.temperatureExternalDelta +@dataclass(kw_only=True, frozen=True) +class HmipEsiSensorEntityDescription(SensorEntityDescription): + """SensorEntityDescription for HmIP Sensors.""" + + value_fn: Callable[[AsyncEnergySensorsInterface], StateType] + exists_fn: Callable[[FunctionalChannel], bool] + type_fn: Callable[[AsyncEnergySensorsInterface], str] + + +SENSORS_ESI = { + ESI_CONNECTED_SENSOR_TYPE_IEC: [ + HmipEsiSensorEntityDescription( + key=ESI_TYPE_CURRENT_POWER_CONSUMPTION, + native_unit_of_measurement=UnitOfPower.WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda device: device.functional_channel.currentPowerConsumption, + exists_fn=lambda channel: channel.currentPowerConsumption is not None, + type_fn=lambda device: "CurrentPowerConsumption", + ), + HmipEsiSensorEntityDescription( + key=ESI_TYPE_ENERGY_COUNTER_USAGE_HIGH_TARIFF, + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + value_fn=lambda device: device.functional_channel.energyCounterOne, + exists_fn=lambda channel: channel.energyCounterOneType != ESI_TYPE_UNKNOWN, + type_fn=lambda device: device.functional_channel.energyCounterOneType, + ), + HmipEsiSensorEntityDescription( + key=ESI_TYPE_ENERGY_COUNTER_USAGE_LOW_TARIFF, + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + value_fn=lambda device: device.functional_channel.energyCounterTwo, + exists_fn=lambda channel: channel.energyCounterTwoType != ESI_TYPE_UNKNOWN, + type_fn=lambda device: device.functional_channel.energyCounterTwoType, + ), + HmipEsiSensorEntityDescription( + key=ESI_TYPE_ENERGY_COUNTER_INPUT_SINGLE_TARIFF, + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + value_fn=lambda device: device.functional_channel.energyCounterThree, + exists_fn=lambda channel: channel.energyCounterThreeType + != ESI_TYPE_UNKNOWN, + type_fn=lambda device: device.functional_channel.energyCounterThreeType, + ), + ], + ESI_CONNECTED_SENSOR_TYPE_LED: [ + HmipEsiSensorEntityDescription( + key=ESI_TYPE_CURRENT_POWER_CONSUMPTION, + native_unit_of_measurement=UnitOfPower.WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda device: device.functional_channel.currentPowerConsumption, + exists_fn=lambda channel: channel.currentPowerConsumption is not None, + type_fn=lambda device: "CurrentPowerConsumption", + ), + HmipEsiSensorEntityDescription( + key=ESI_TYPE_ENERGY_COUNTER_USAGE_HIGH_TARIFF, + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + value_fn=lambda device: device.functional_channel.energyCounterOne, + exists_fn=lambda channel: channel.energyCounterOne is not None, + type_fn=lambda device: ESI_TYPE_ENERGY_COUNTER_USAGE_HIGH_TARIFF, + ), + ], + ESI_CONNECTED_SENSOR_TYPE_GAS: [ + HmipEsiSensorEntityDescription( + key=ESI_TYPE_CURRENT_GAS_FLOW, + native_unit_of_measurement=UnitOfVolumeFlowRate.CUBIC_METERS_PER_HOUR, + device_class=SensorDeviceClass.VOLUME_FLOW_RATE, + state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda device: device.functional_channel.currentGasFlow, + exists_fn=lambda channel: channel.currentGasFlow is not None, + type_fn=lambda device: "CurrentGasFlow", + ), + HmipEsiSensorEntityDescription( + key=ESI_TYPE_CURRENT_GAS_VOLUME, + native_unit_of_measurement=UnitOfVolume.CUBIC_METERS, + device_class=SensorDeviceClass.VOLUME, + state_class=SensorStateClass.TOTAL_INCREASING, + value_fn=lambda device: device.functional_channel.gasVolume, + exists_fn=lambda channel: channel.gasVolume is not None, + type_fn=lambda device: "GasVolume", + ), + ], +} + + +class HmipEsiSensorEntity(HomematicipGenericEntity, SensorEntity): + """EntityDescription for HmIP-ESI Sensors.""" + + entity_description: HmipEsiSensorEntityDescription + + def __init__( + self, + hap: HomematicipHAP, + device: HomematicipGenericEntity, + channel_index: int, + entity_description: HmipEsiSensorEntityDescription, + ) -> None: + """Initialize Sensor Entity.""" + super().__init__( + hap=hap, + device=device, + channel=channel_index, + post=entity_description.key, + is_multi_channel=False, + ) + self.entity_description = entity_description + + @property + def extra_state_attributes(self) -> dict[str, Any]: + """Return the state attributes of the esi sensor.""" + state_attr = super().extra_state_attributes + state_attr[ATTR_ESI_TYPE] = self.entity_description.type_fn(self) + + return state_attr + + @property + def native_value(self) -> str | None: + """Return the state of the sensor.""" + return str(self.entity_description.value_fn(self)) + + class HomematicipPassageDetectorDeltaCounter(HomematicipGenericEntity, SensorEntity): """Representation of the HomematicIP passage detector delta counter.""" diff --git a/tests/components/homematicip_cloud/fixtures/homematicip_cloud.json b/tests/components/homematicip_cloud/fixtures/homematicip_cloud.json index 922601ca733..eba2c803b1f 100644 --- a/tests/components/homematicip_cloud/fixtures/homematicip_cloud.json +++ b/tests/components/homematicip_cloud/fixtures/homematicip_cloud.json @@ -7347,6 +7347,416 @@ "serializedGlobalTradeItemNumber": "3014F7110000000000000DLD", "type": "DOOR_LOCK_DRIVE", "updateState": "UP_TO_DATE" + }, + "3014F7110000000000ESIGAS": { + "availableFirmwareVersion": "1.2.2", + "connectionType": "HMIP_RF", + "deviceArchetype": "HMIP", + "firmwareVersion": "1.2.2", + "firmwareVersionInteger": 66050, + "functionalChannels": { + "0": { + "busConfigMismatch": null, + "coProFaulty": false, + "coProRestartNeeded": false, + "coProUpdateFailure": false, + "configPending": false, + "controlsMountingOrientation": null, + "daliBusState": null, + "defaultLinkedGroup": [], + "deviceCommunicationError": null, + "deviceDriveError": null, + "deviceDriveModeError": null, + "deviceId": "3014F7110000000000ESIGAS", + "deviceOperationMode": null, + "deviceOverheated": false, + "deviceOverloaded": false, + "devicePowerFailureDetected": false, + "deviceUndervoltage": false, + "displayContrast": null, + "dutyCycle": false, + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "groups": ["00000000-0000-0000-0000-000000000047"], + "index": 0, + "label": "", + "lockJammed": null, + "lowBat": false, + "mountingOrientation": null, + "multicastRoutingEnabled": false, + "particulateMatterSensorCommunicationError": null, + "particulateMatterSensorError": null, + "powerShortCircuit": null, + "profilePeriodLimitReached": null, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -73, + "rssiPeerValue": null, + "sensorCommunicationError": false, + "sensorError": false, + "shortCircuitDataLine": null, + "supportedOptionalFeatures": { + "IFeatureBusConfigMismatch": false, + "IFeatureDeviceCoProError": false, + "IFeatureDeviceCoProRestart": false, + "IFeatureDeviceCoProUpdate": false, + "IFeatureDeviceCommunicationError": false, + "IFeatureDeviceDaliBusError": false, + "IFeatureDeviceDriveError": false, + "IFeatureDeviceDriveModeError": false, + "IFeatureDeviceIdentify": false, + "IFeatureDeviceOverheated": false, + "IFeatureDeviceOverloaded": false, + "IFeatureDeviceParticulateMatterSensorCommunicationError": false, + "IFeatureDeviceParticulateMatterSensorError": false, + "IFeatureDevicePowerFailure": false, + "IFeatureDeviceSensorCommunicationError": true, + "IFeatureDeviceSensorError": true, + "IFeatureDeviceTemperatureHumiditySensorCommunicationError": false, + "IFeatureDeviceTemperatureHumiditySensorError": false, + "IFeatureDeviceTemperatureOutOfRange": false, + "IFeatureDeviceUndervoltage": false, + "IFeatureMulticastRouter": false, + "IFeaturePowerShortCircuit": false, + "IFeatureProfilePeriodLimit": false, + "IFeatureRssiValue": true, + "IFeatureShortCircuitDataLine": false, + "IOptionalFeatureDefaultLinkedGroup": false, + "IOptionalFeatureDeviceErrorLockJammed": false, + "IOptionalFeatureDeviceOperationMode": false, + "IOptionalFeatureDisplayContrast": false, + "IOptionalFeatureDutyCycle": true, + "IOptionalFeatureLowBat": true, + "IOptionalFeatureMountingOrientation": false + }, + "temperatureHumiditySensorCommunicationError": null, + "temperatureHumiditySensorError": null, + "temperatureOutOfRange": false, + "unreach": false + }, + "1": { + "channelRole": "ENERGY_SENSOR", + "connectedEnergySensorType": "ES_GAS", + "currentGasFlow": 1.03, + "currentPowerConsumption": null, + "deviceId": "3014F7110000000000ESIGAS", + "energyCounterOne": null, + "energyCounterOneType": "UNKNOWN", + "energyCounterThree": null, + "energyCounterThreeType": "UNKNOWN", + "energyCounterTwo": null, + "energyCounterTwoType": "UNKNOWN", + "functionalChannelType": "ENERGY_SENSORS_INTERFACE_CHANNEL", + "gasVolume": 1019.26, + "gasVolumePerImpulse": 0.01, + "groupIndex": 1, + "groups": ["00000000-0000-0000-0000-000000000077"], + "impulsesPerKWH": 10000, + "index": 1, + "label": "", + "supportedOptionalFeatures": { + "IOptionalFeatureCounterOffset": true, + "IOptionalFeatureCurrentGasFlow": true, + "IOptionalFeatureCurrentPowerConsumption": false, + "IOptionalFeatureEnergyCounterOne": false, + "IOptionalFeatureEnergyCounterThree": false, + "IOptionalFeatureEnergyCounterTwo": false, + "IOptionalFeatureGasVolume": true, + "IOptionalFeatureGasVolumePerImpulse": true, + "IOptionalFeatureImpulsesPerKWH": false + } + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000ESIGAS", + "label": "esi_gas", + "lastStatusUpdate": 1708880308351, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manuallyUpdateForced": false, + "manufacturerCode": 1, + "measuredAttributes": { + "1": { + "currentGasFlow": true, + "gasVolume": true + } + }, + "modelId": 509, + "modelType": "HmIP-ESI", + "oem": "eQ-3", + "permanentlyReachable": false, + "serializedGlobalTradeItemNumber": "3014F7110000000000ESIGAS", + "type": "ENERGY_SENSORS_INTERFACE", + "updateState": "UP_TO_DATE" + }, + "3014F7110000000000ESIIEC": { + "availableFirmwareVersion": "0.0.0", + "connectionType": "HMIP_RF", + "deviceArchetype": "HMIP", + "firmwareVersion": "1.0.6", + "firmwareVersionInteger": 65542, + "functionalChannels": { + "0": { + "busConfigMismatch": null, + "coProFaulty": false, + "coProRestartNeeded": false, + "coProUpdateFailure": false, + "configPending": false, + "controlsMountingOrientation": null, + "daliBusState": null, + "defaultLinkedGroup": [], + "deviceCommunicationError": null, + "deviceDriveError": null, + "deviceDriveModeError": null, + "deviceId": "3014F7110000000000ESIIEC", + "deviceOperationMode": null, + "deviceOverheated": false, + "deviceOverloaded": false, + "devicePowerFailureDetected": false, + "deviceUndervoltage": false, + "displayContrast": null, + "dutyCycle": false, + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "groups": ["00000000-0000-0000-0000-000000000031"], + "index": 0, + "label": "", + "lockJammed": null, + "lowBat": false, + "mountingOrientation": null, + "multicastRoutingEnabled": false, + "particulateMatterSensorCommunicationError": null, + "particulateMatterSensorError": null, + "powerShortCircuit": null, + "profilePeriodLimitReached": null, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -94, + "rssiPeerValue": null, + "sensorCommunicationError": false, + "sensorError": true, + "shortCircuitDataLine": null, + "supportedOptionalFeatures": { + "IFeatureBusConfigMismatch": false, + "IFeatureDeviceCoProError": false, + "IFeatureDeviceCoProRestart": false, + "IFeatureDeviceCoProUpdate": false, + "IFeatureDeviceCommunicationError": false, + "IFeatureDeviceDaliBusError": false, + "IFeatureDeviceDriveError": false, + "IFeatureDeviceDriveModeError": false, + "IFeatureDeviceIdentify": false, + "IFeatureDeviceOverheated": false, + "IFeatureDeviceOverloaded": false, + "IFeatureDeviceParticulateMatterSensorCommunicationError": false, + "IFeatureDeviceParticulateMatterSensorError": false, + "IFeatureDevicePowerFailure": false, + "IFeatureDeviceSensorCommunicationError": true, + "IFeatureDeviceSensorError": true, + "IFeatureDeviceTemperatureHumiditySensorCommunicationError": false, + "IFeatureDeviceTemperatureHumiditySensorError": false, + "IFeatureDeviceTemperatureOutOfRange": false, + "IFeatureDeviceUndervoltage": false, + "IFeatureMulticastRouter": false, + "IFeaturePowerShortCircuit": false, + "IFeatureProfilePeriodLimit": false, + "IFeatureRssiValue": true, + "IFeatureShortCircuitDataLine": false, + "IOptionalFeatureDefaultLinkedGroup": false, + "IOptionalFeatureDeviceErrorLockJammed": false, + "IOptionalFeatureDeviceOperationMode": false, + "IOptionalFeatureDisplayContrast": false, + "IOptionalFeatureDutyCycle": true, + "IOptionalFeatureLowBat": true, + "IOptionalFeatureMountingOrientation": false + }, + "temperatureHumiditySensorCommunicationError": null, + "temperatureHumiditySensorError": null, + "temperatureOutOfRange": false, + "unreach": false + }, + "1": { + "channelRole": null, + "connectedEnergySensorType": "ES_IEC", + "currentGasFlow": null, + "currentPowerConsumption": 432, + "deviceId": "3014F7110000000000ESIIEC", + "energyCounterOne": 194.0, + "energyCounterOneType": "ENERGY_COUNTER_USAGE_HIGH_TARIFF", + "energyCounterThree": 3.0, + "energyCounterThreeType": "ENERGY_COUNTER_INPUT_SINGLE_TARIFF", + "energyCounterTwo": 0.0, + "energyCounterTwoType": "ENERGY_COUNTER_USAGE_LOW_TARIFF", + "functionalChannelType": "ENERGY_SENSORS_INTERFACE_CHANNEL", + "gasVolume": null, + "gasVolumePerImpulse": 0.01, + "groupIndex": 1, + "groups": ["00000000-0000-0000-0000-000000000051"], + "impulsesPerKWH": 10000, + "index": 1, + "label": "", + "supportedOptionalFeatures": { + "IOptionalFeatureCounterOffset": false, + "IOptionalFeatureCurrentGasFlow": false, + "IOptionalFeatureCurrentPowerConsumption": true, + "IOptionalFeatureEnergyCounterOne": true, + "IOptionalFeatureEnergyCounterThree": true, + "IOptionalFeatureEnergyCounterTwo": true, + "IOptionalFeatureGasVolume": false, + "IOptionalFeatureGasVolumePerImpulse": false, + "IOptionalFeatureImpulsesPerKWH": false + } + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000ESIIEC", + "label": "esi_iec", + "lastStatusUpdate": 1702420986697, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manuallyUpdateForced": false, + "manufacturerCode": 1, + "measuredAttributes": {}, + "modelId": 509, + "modelType": "HmIP-ESI", + "oem": "eQ-3", + "permanentlyReachable": false, + "serializedGlobalTradeItemNumber": "3014F7110000000000ESIIEC", + "type": "ENERGY_SENSORS_INTERFACE", + "updateState": "UP_TO_DATE" + }, + "3014F711000000000ESIIEC2": { + "availableFirmwareVersion": "0.0.0", + "connectionType": "HMIP_RF", + "deviceArchetype": "HMIP", + "firmwareVersion": "1.0.6", + "firmwareVersionInteger": 65542, + "functionalChannels": { + "0": { + "busConfigMismatch": null, + "coProFaulty": false, + "coProRestartNeeded": false, + "coProUpdateFailure": false, + "configPending": false, + "controlsMountingOrientation": null, + "daliBusState": null, + "defaultLinkedGroup": [], + "deviceCommunicationError": null, + "deviceDriveError": null, + "deviceDriveModeError": null, + "deviceId": "3014F711000000000ESIIEC2", + "deviceOperationMode": null, + "deviceOverheated": false, + "deviceOverloaded": false, + "devicePowerFailureDetected": false, + "deviceUndervoltage": false, + "displayContrast": null, + "dutyCycle": false, + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "groups": ["00000000-0000-0000-0000-000000000031"], + "index": 0, + "label": "", + "lockJammed": null, + "lowBat": false, + "mountingOrientation": null, + "multicastRoutingEnabled": false, + "particulateMatterSensorCommunicationError": null, + "particulateMatterSensorError": null, + "powerShortCircuit": null, + "profilePeriodLimitReached": null, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -94, + "rssiPeerValue": null, + "sensorCommunicationError": false, + "sensorError": true, + "shortCircuitDataLine": null, + "supportedOptionalFeatures": { + "IFeatureBusConfigMismatch": false, + "IFeatureDeviceCoProError": false, + "IFeatureDeviceCoProRestart": false, + "IFeatureDeviceCoProUpdate": false, + "IFeatureDeviceCommunicationError": false, + "IFeatureDeviceDaliBusError": false, + "IFeatureDeviceDriveError": false, + "IFeatureDeviceDriveModeError": false, + "IFeatureDeviceIdentify": false, + "IFeatureDeviceOverheated": false, + "IFeatureDeviceOverloaded": false, + "IFeatureDeviceParticulateMatterSensorCommunicationError": false, + "IFeatureDeviceParticulateMatterSensorError": false, + "IFeatureDevicePowerFailure": false, + "IFeatureDeviceSensorCommunicationError": true, + "IFeatureDeviceSensorError": true, + "IFeatureDeviceTemperatureHumiditySensorCommunicationError": false, + "IFeatureDeviceTemperatureHumiditySensorError": false, + "IFeatureDeviceTemperatureOutOfRange": false, + "IFeatureDeviceUndervoltage": false, + "IFeatureMulticastRouter": false, + "IFeaturePowerShortCircuit": false, + "IFeatureProfilePeriodLimit": false, + "IFeatureRssiValue": true, + "IFeatureShortCircuitDataLine": false, + "IOptionalFeatureDefaultLinkedGroup": false, + "IOptionalFeatureDeviceErrorLockJammed": false, + "IOptionalFeatureDeviceOperationMode": false, + "IOptionalFeatureDisplayContrast": false, + "IOptionalFeatureDutyCycle": true, + "IOptionalFeatureLowBat": true, + "IOptionalFeatureMountingOrientation": false + }, + "temperatureHumiditySensorCommunicationError": null, + "temperatureHumiditySensorError": null, + "temperatureOutOfRange": false, + "unreach": false + }, + "1": { + "channelRole": null, + "connectedEnergySensorType": "ES_IEC", + "currentGasFlow": null, + "currentPowerConsumption": 432, + "deviceId": "3014F711000000000ESIIEC2", + "energyCounterOne": 194.0, + "energyCounterOneType": "ENERGY_COUNTER_USAGE_HIGH_TARIFF", + "energyCounterThree": 3.0, + "energyCounterThreeType": "UNKNOWN", + "energyCounterTwo": 0.0, + "energyCounterTwoType": "ENERGY_COUNTER_USAGE_LOW_TARIFF", + "functionalChannelType": "ENERGY_SENSORS_INTERFACE_CHANNEL", + "gasVolume": null, + "gasVolumePerImpulse": 0.01, + "groupIndex": 1, + "groups": ["00000000-0000-0000-0000-000000000051"], + "impulsesPerKWH": 10000, + "index": 1, + "label": "", + "supportedOptionalFeatures": { + "IOptionalFeatureCounterOffset": false, + "IOptionalFeatureCurrentGasFlow": false, + "IOptionalFeatureCurrentPowerConsumption": true, + "IOptionalFeatureEnergyCounterOne": true, + "IOptionalFeatureEnergyCounterThree": true, + "IOptionalFeatureEnergyCounterTwo": true, + "IOptionalFeatureGasVolume": false, + "IOptionalFeatureGasVolumePerImpulse": false, + "IOptionalFeatureImpulsesPerKWH": false + } + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F711000000000ESIIEC2", + "label": "esi_iec2", + "lastStatusUpdate": 1702420986697, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manuallyUpdateForced": false, + "manufacturerCode": 1, + "measuredAttributes": {}, + "modelId": 509, + "modelType": "HmIP-ESI", + "oem": "eQ-3", + "permanentlyReachable": false, + "serializedGlobalTradeItemNumber": "3014F711000000000ESIIEC2", + "type": "ENERGY_SENSORS_INTERFACE", + "updateState": "UP_TO_DATE" } }, "groups": { diff --git a/tests/components/homematicip_cloud/test_device.py b/tests/components/homematicip_cloud/test_device.py index fb7fe7d7deb..348171b3187 100644 --- a/tests/components/homematicip_cloud/test_device.py +++ b/tests/components/homematicip_cloud/test_device.py @@ -26,7 +26,7 @@ async def test_hmip_load_all_supported_devices( test_devices=None, test_groups=None ) - assert len(mock_hap.hmip_device_by_entity_id) == 278 + assert len(mock_hap.hmip_device_by_entity_id) == 290 async def test_hmip_remove_device( diff --git a/tests/components/homematicip_cloud/test_sensor.py b/tests/components/homematicip_cloud/test_sensor.py index 3089bb062e5..6951b750b2f 100644 --- a/tests/components/homematicip_cloud/test_sensor.py +++ b/tests/components/homematicip_cloud/test_sensor.py @@ -511,3 +511,126 @@ async def test_hmip_passage_detector_delta_counter( await async_manipulate_test_data(hass, hmip_device, "leftRightCounterDelta", 190) ha_state = hass.states.get(entity_id) assert ha_state.state == "190" + + +async def test_hmip_esi_iec_current_power_consumption( + hass: HomeAssistant, default_mock_hap_factory +) -> None: + """Test ESI-IEC currentPowerConsumption Sensor.""" + entity_id = "sensor.esi_iec_currentPowerConsumption" + entity_name = "esi_iec CurrentPowerConsumption" + device_model = "HmIP-ESI" + mock_hap = await default_mock_hap_factory.async_get_mock_hap( + test_devices=["esi_iec"] + ) + + ha_state, hmip_device = get_and_check_entity_basics( + hass, mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == "432" + + +async def test_hmip_esi_iec_energy_counter_usage_high_tariff( + hass: HomeAssistant, default_mock_hap_factory +) -> None: + """Test ESI-IEC ENERGY_COUNTER_USAGE_HIGH_TARIFF.""" + entity_id = "sensor.esi_iec_energy_counter_usage_high_tariff" + entity_name = "esi_iec ENERGY_COUNTER_USAGE_HIGH_TARIFF" + device_model = "HmIP-ESI" + mock_hap = await default_mock_hap_factory.async_get_mock_hap( + test_devices=["esi_iec"] + ) + + ha_state, hmip_device = get_and_check_entity_basics( + hass, mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == "194.0" + + +async def test_hmip_esi_iec_energy_counter_usage_low_tariff( + hass: HomeAssistant, default_mock_hap_factory +) -> None: + """Test ESI-IEC ENERGY_COUNTER_USAGE_LOW_TARIFF.""" + entity_id = "sensor.esi_iec_energy_counter_usage_low_tariff" + entity_name = "esi_iec ENERGY_COUNTER_USAGE_LOW_TARIFF" + device_model = "HmIP-ESI" + mock_hap = await default_mock_hap_factory.async_get_mock_hap( + test_devices=["esi_iec"] + ) + + ha_state, hmip_device = get_and_check_entity_basics( + hass, mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == "0.0" + + +async def test_hmip_esi_iec_energy_counter_input_single_tariff( + hass: HomeAssistant, default_mock_hap_factory +) -> None: + """Test ESI-IEC ENERGY_COUNTER_INPUT_SINGLE_TARIFF.""" + entity_id = "sensor.esi_iec_energy_counter_input_single_tariff" + entity_name = "esi_iec ENERGY_COUNTER_INPUT_SINGLE_TARIFF" + device_model = "HmIP-ESI" + mock_hap = await default_mock_hap_factory.async_get_mock_hap( + test_devices=["esi_iec"] + ) + + ha_state, hmip_device = get_and_check_entity_basics( + hass, mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == "3.0" + + +async def test_hmip_esi_iec_unknown_channel( + hass: HomeAssistant, default_mock_hap_factory +) -> None: + """Test devices are loaded partially.""" + not_existing_entity_id = "sensor.esi_iec2_energy_counter_input_single_tariff" + existing_entity_id = "sensor.esi_iec2_energy_counter_usage_high_tariff" + await default_mock_hap_factory.async_get_mock_hap(test_devices=["esi_iec2"]) + + not_existing_ha_state = hass.states.get(not_existing_entity_id) + existing_ha_state = hass.states.get(existing_entity_id) + + assert not_existing_ha_state is None + assert existing_ha_state.state == "194.0" + + +async def test_hmip_esi_gas_current_gas_flow( + hass: HomeAssistant, default_mock_hap_factory +) -> None: + """Test ESI-IEC CurrentGasFlow.""" + entity_id = "sensor.esi_gas_currentgasflow" + entity_name = "esi_gas CurrentGasFlow" + device_model = "HmIP-ESI" + mock_hap = await default_mock_hap_factory.async_get_mock_hap( + test_devices=["esi_gas"] + ) + + ha_state, hmip_device = get_and_check_entity_basics( + hass, mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == "1.03" + + +async def test_hmip_esi_gas_gas_volume( + hass: HomeAssistant, default_mock_hap_factory +) -> None: + """Test ESI-IEC GasVolume.""" + entity_id = "sensor.esi_gas_gasvolume" + entity_name = "esi_gas GasVolume" + device_model = "HmIP-ESI" + mock_hap = await default_mock_hap_factory.async_get_mock_hap( + test_devices=["esi_gas"] + ) + + ha_state, hmip_device = get_and_check_entity_basics( + hass, mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == "1019.26" From 7b7b97a7a47d13233e766e7f7c5aad91cdb301df Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 26 Jun 2024 11:35:39 +0200 Subject: [PATCH 1250/1445] Force alias when importing event and fan PLATFORM_SCHEMA (#120524) --- homeassistant/components/group/event.py | 4 ++-- homeassistant/components/group/fan.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/group/event.py b/homeassistant/components/group/event.py index e5752a7835f..67220b878a1 100644 --- a/homeassistant/components/group/event.py +++ b/homeassistant/components/group/event.py @@ -11,7 +11,7 @@ from homeassistant.components.event import ( ATTR_EVENT_TYPE, ATTR_EVENT_TYPES, DOMAIN, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as EVENT_PLATFORM_SCHEMA, EventEntity, ) from homeassistant.config_entries import ConfigEntry @@ -38,7 +38,7 @@ DEFAULT_NAME = "Event group" # No limit on parallel updates to enable a group calling another group PARALLEL_UPDATES = 0 -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = EVENT_PLATFORM_SCHEMA.extend( { vol.Required(CONF_ENTITIES): cv.entities_domain(DOMAIN), vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/group/fan.py b/homeassistant/components/group/fan.py index b70a4ff1531..e09477430ef 100644 --- a/homeassistant/components/group/fan.py +++ b/homeassistant/components/group/fan.py @@ -15,7 +15,7 @@ from homeassistant.components.fan import ( ATTR_PERCENTAGE, ATTR_PERCENTAGE_STEP, DOMAIN, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as FAN_PLATFORM_SCHEMA, SERVICE_OSCILLATE, SERVICE_SET_DIRECTION, SERVICE_SET_PERCENTAGE, @@ -54,7 +54,7 @@ DEFAULT_NAME = "Fan Group" # No limit on parallel updates to enable a group calling another group PARALLEL_UPDATES = 0 -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = FAN_PLATFORM_SCHEMA.extend( { vol.Required(CONF_ENTITIES): cv.entities_domain(DOMAIN), vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, From 44aad2b8211e93ae40a69bb8df993562821e144f Mon Sep 17 00:00:00 2001 From: Stefan Agner Date: Wed, 26 Jun 2024 11:43:51 +0200 Subject: [PATCH 1251/1445] Improve Matter Server version incompatibility handling (#120416) * Improve Matter Server version incompatibility handling Improve the handling of Matter Server version. Noteably fix the issues raised (add strings for the issue) and split the version check into two cases: One if the server is too old and one if the server is too new. * Bump Python Matter Server library to 6.2.0b1 * Address review feedback --- homeassistant/components/matter/__init__.py | 32 +++++++--- homeassistant/components/matter/manifest.json | 2 +- homeassistant/components/matter/strings.json | 10 +++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/matter/test_init.py | 63 ++++++++++++++++--- 6 files changed, 90 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/matter/__init__.py b/homeassistant/components/matter/__init__.py index 86b642f7389..75ae3df6b1a 100644 --- a/homeassistant/components/matter/__init__.py +++ b/homeassistant/components/matter/__init__.py @@ -7,7 +7,12 @@ from contextlib import suppress from functools import cache from matter_server.client import MatterClient -from matter_server.client.exceptions import CannotConnect, InvalidServerVersion +from matter_server.client.exceptions import ( + CannotConnect, + InvalidServerVersion, + ServerVersionTooNew, + ServerVersionTooOld, +) from matter_server.common.errors import MatterError, NodeNotExists from homeassistant.components.hassio import AddonError, AddonManager, AddonState @@ -71,17 +76,27 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: except (CannotConnect, TimeoutError) as err: raise ConfigEntryNotReady("Failed to connect to matter server") from err except InvalidServerVersion as err: - if use_addon: - addon_manager = _get_addon_manager(hass) - addon_manager.async_schedule_update_addon(catch_error=True) - else: + if isinstance(err, ServerVersionTooOld): + if use_addon: + addon_manager = _get_addon_manager(hass) + addon_manager.async_schedule_update_addon(catch_error=True) + else: + async_create_issue( + hass, + DOMAIN, + "server_version_version_too_old", + is_fixable=False, + severity=IssueSeverity.ERROR, + translation_key="server_version_version_too_old", + ) + elif isinstance(err, ServerVersionTooNew): async_create_issue( hass, DOMAIN, - "invalid_server_version", + "server_version_version_too_new", is_fixable=False, severity=IssueSeverity.ERROR, - translation_key="invalid_server_version", + translation_key="server_version_version_too_new", ) raise ConfigEntryNotReady(f"Invalid server version: {err}") from err @@ -91,7 +106,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: "Unknown error connecting to the Matter server" ) from err - async_delete_issue(hass, DOMAIN, "invalid_server_version") + async_delete_issue(hass, DOMAIN, "server_version_version_too_old") + async_delete_issue(hass, DOMAIN, "server_version_version_too_new") async def on_hass_stop(event: Event) -> None: """Handle incoming stop event from Home Assistant.""" diff --git a/homeassistant/components/matter/manifest.json b/homeassistant/components/matter/manifest.json index 369657df90c..8c88fcc8be2 100644 --- a/homeassistant/components/matter/manifest.json +++ b/homeassistant/components/matter/manifest.json @@ -6,6 +6,6 @@ "dependencies": ["websocket_api"], "documentation": "https://www.home-assistant.io/integrations/matter", "iot_class": "local_push", - "requirements": ["python-matter-server==6.1.0"], + "requirements": ["python-matter-server==6.2.0b1"], "zeroconf": ["_matter._tcp.local.", "_matterc._udp.local."] } diff --git a/homeassistant/components/matter/strings.json b/homeassistant/components/matter/strings.json index e94ab2e1780..3389a4bfe81 100644 --- a/homeassistant/components/matter/strings.json +++ b/homeassistant/components/matter/strings.json @@ -157,6 +157,16 @@ } } }, + "issues": { + "server_version_version_too_old": { + "description": "The version of the Matter Server you are currently running is too old for this version of Home Assistant. Please update the Matter Server to the latest version to fix this issue.", + "title": "Newer version of Matter Server needed" + }, + "server_version_version_too_new": { + "description": "The version of the Matter Server you are currently running is too new for this version of Home Assistant. Please update Home Assistant or downgrade the Matter Server to an older version to fix this issue.", + "title": "Older version of Matter Server needed" + } + }, "services": { "open_commissioning_window": { "name": "Open commissioning window", diff --git a/requirements_all.txt b/requirements_all.txt index 5967b3f2a94..1a297ef2b5c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2284,7 +2284,7 @@ python-kasa[speedups]==0.7.0.1 # python-lirc==1.2.3 # homeassistant.components.matter -python-matter-server==6.1.0 +python-matter-server==6.2.0b1 # homeassistant.components.xiaomi_miio python-miio==0.5.12 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e1fb561ecee..88623000c5e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1781,7 +1781,7 @@ python-juicenet==1.1.0 python-kasa[speedups]==0.7.0.1 # homeassistant.components.matter -python-matter-server==6.1.0 +python-matter-server==6.2.0b1 # homeassistant.components.xiaomi_miio python-miio==0.5.12 diff --git a/tests/components/matter/test_init.py b/tests/components/matter/test_init.py index d3712f24d12..c28385efca3 100644 --- a/tests/components/matter/test_init.py +++ b/tests/components/matter/test_init.py @@ -5,7 +5,11 @@ from __future__ import annotations import asyncio from unittest.mock import AsyncMock, MagicMock, call, patch -from matter_server.client.exceptions import CannotConnect, InvalidServerVersion +from matter_server.client.exceptions import ( + CannotConnect, + ServerVersionTooNew, + ServerVersionTooOld, +) from matter_server.client.models.node import MatterNode from matter_server.common.errors import MatterError from matter_server.common.helpers.util import dataclass_from_dict @@ -362,12 +366,30 @@ async def test_addon_info_failure( "backup_calls", "update_addon_side_effect", "create_backup_side_effect", + "connect_side_effect", ), [ - ("1.0.0", True, 1, 1, None, None), - ("1.0.0", False, 0, 0, None, None), - ("1.0.0", True, 1, 1, HassioAPIError("Boom"), None), - ("1.0.0", True, 0, 1, None, HassioAPIError("Boom")), + ("1.0.0", True, 1, 1, None, None, ServerVersionTooOld("Invalid version")), + ("1.0.0", True, 0, 0, None, None, ServerVersionTooNew("Invalid version")), + ("1.0.0", False, 0, 0, None, None, ServerVersionTooOld("Invalid version")), + ( + "1.0.0", + True, + 1, + 1, + HassioAPIError("Boom"), + None, + ServerVersionTooOld("Invalid version"), + ), + ( + "1.0.0", + True, + 0, + 1, + None, + HassioAPIError("Boom"), + ServerVersionTooOld("Invalid version"), + ), ], ) async def test_update_addon( @@ -386,13 +408,14 @@ async def test_update_addon( backup_calls: int, update_addon_side_effect: Exception | None, create_backup_side_effect: Exception | None, + connect_side_effect: Exception, ) -> None: """Test update the Matter add-on during entry setup.""" addon_info.return_value["version"] = addon_version addon_info.return_value["update_available"] = update_available create_backup.side_effect = create_backup_side_effect update_addon.side_effect = update_addon_side_effect - matter_client.connect.side_effect = InvalidServerVersion("Invalid version") + matter_client.connect.side_effect = connect_side_effect entry = MockConfigEntry( domain=DOMAIN, title="Matter", @@ -413,12 +436,32 @@ async def test_update_addon( # This tests needs to be adjusted to remove lingering tasks @pytest.mark.parametrize("expected_lingering_tasks", [True]) +@pytest.mark.parametrize( + ( + "connect_side_effect", + "issue_raised", + ), + [ + ( + ServerVersionTooOld("Invalid version"), + "server_version_version_too_old", + ), + ( + ServerVersionTooNew("Invalid version"), + "server_version_version_too_new", + ), + ], +) async def test_issue_registry_invalid_version( - hass: HomeAssistant, matter_client: MagicMock, issue_registry: ir.IssueRegistry + hass: HomeAssistant, + matter_client: MagicMock, + issue_registry: ir.IssueRegistry, + connect_side_effect: Exception, + issue_raised: str, ) -> None: """Test issue registry for invalid version.""" original_connect_side_effect = matter_client.connect.side_effect - matter_client.connect.side_effect = InvalidServerVersion("Invalid version") + matter_client.connect.side_effect = connect_side_effect entry = MockConfigEntry( domain=DOMAIN, title="Matter", @@ -434,7 +477,7 @@ async def test_issue_registry_invalid_version( entry_state = entry.state assert entry_state is ConfigEntryState.SETUP_RETRY - assert issue_registry.async_get_issue(DOMAIN, "invalid_server_version") + assert issue_registry.async_get_issue(DOMAIN, issue_raised) matter_client.connect.side_effect = original_connect_side_effect @@ -442,7 +485,7 @@ async def test_issue_registry_invalid_version( await hass.async_block_till_done() assert entry.state is ConfigEntryState.LOADED - assert not issue_registry.async_get_issue(DOMAIN, "invalid_server_version") + assert not issue_registry.async_get_issue(DOMAIN, issue_raised) @pytest.mark.parametrize( From 42d235ce4d4368c0160163118441ffa2cb2b2635 Mon Sep 17 00:00:00 2001 From: "Mr. Bubbles" Date: Wed, 26 Jun 2024 11:44:58 +0200 Subject: [PATCH 1252/1445] Add diagnostics platform to pyLoad integration (#120535) --- .../components/pyload/diagnostics.py | 26 +++++++++++++++++ .../pyload/snapshots/test_diagnostics.ambr | 17 +++++++++++ tests/components/pyload/test_diagnostics.py | 28 +++++++++++++++++++ 3 files changed, 71 insertions(+) create mode 100644 homeassistant/components/pyload/diagnostics.py create mode 100644 tests/components/pyload/snapshots/test_diagnostics.ambr create mode 100644 tests/components/pyload/test_diagnostics.py diff --git a/homeassistant/components/pyload/diagnostics.py b/homeassistant/components/pyload/diagnostics.py new file mode 100644 index 00000000000..d18e5a5fe0d --- /dev/null +++ b/homeassistant/components/pyload/diagnostics.py @@ -0,0 +1,26 @@ +"""Diagnostics support for pyLoad.""" + +from __future__ import annotations + +from typing import Any + +from homeassistant.components.diagnostics import async_redact_data +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import HomeAssistant + +from . import PyLoadConfigEntry +from .coordinator import pyLoadData + +TO_REDACT = {CONF_USERNAME, CONF_PASSWORD} + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, config_entry: PyLoadConfigEntry +) -> dict[str, Any]: + """Return diagnostics for a config entry.""" + pyload_data: pyLoadData = config_entry.runtime_data.data + + return { + "config_entry_data": async_redact_data(dict(config_entry.data), TO_REDACT), + "pyload_data": pyload_data, + } diff --git a/tests/components/pyload/snapshots/test_diagnostics.ambr b/tests/components/pyload/snapshots/test_diagnostics.ambr new file mode 100644 index 00000000000..8c3e110f2ec --- /dev/null +++ b/tests/components/pyload/snapshots/test_diagnostics.ambr @@ -0,0 +1,17 @@ +# serializer version: 1 +# name: test_diagnostics + dict({ + 'config_entry_data': dict({ + 'host': 'pyload.local', + 'password': '**REDACTED**', + 'port': 8000, + 'ssl': True, + 'username': '**REDACTED**', + 'verify_ssl': False, + }), + 'pyload_data': dict({ + '__type': "", + 'repr': 'pyLoadData(pause=False, active=1, queue=6, total=37, speed=5405963.0, download=True, reconnect=False, captcha=False, free_space=99999999999)', + }), + }) +# --- diff --git a/tests/components/pyload/test_diagnostics.py b/tests/components/pyload/test_diagnostics.py new file mode 100644 index 00000000000..9c5e73f853f --- /dev/null +++ b/tests/components/pyload/test_diagnostics.py @@ -0,0 +1,28 @@ +"""Tests for pyLoad diagnostics.""" + +from unittest.mock import AsyncMock + +from syrupy.assertion import SnapshotAssertion + +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry +from tests.components.diagnostics import get_diagnostics_for_config_entry +from tests.typing import ClientSessionGenerator + + +async def test_diagnostics( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + config_entry: MockConfigEntry, + mock_pyloadapi: AsyncMock, + snapshot: SnapshotAssertion, +) -> None: + """Test diagnostics.""" + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert ( + await get_diagnostics_for_config_entry(hass, hass_client, config_entry) + == snapshot + ) From a8bf671663a00aad825d122ff3d2f19fe196828f Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 26 Jun 2024 11:45:57 +0200 Subject: [PATCH 1253/1445] Force alias when importing remote PLATFORM_SCHEMA (#120533) --- homeassistant/components/itach/remote.py | 4 ++-- homeassistant/components/xiaomi_miio/remote.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/itach/remote.py b/homeassistant/components/itach/remote.py index 606ca4fd021..986dbfb8b95 100644 --- a/homeassistant/components/itach/remote.py +++ b/homeassistant/components/itach/remote.py @@ -13,7 +13,7 @@ from homeassistant.components import remote from homeassistant.components.remote import ( ATTR_NUM_REPEATS, DEFAULT_NUM_REPEATS, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as REMOTE_PLATFORM_SCHEMA, ) from homeassistant.const import ( CONF_DEVICES, @@ -42,7 +42,7 @@ CONF_COMMANDS = "commands" CONF_DATA = "data" CONF_IR_COUNT = "ir_count" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = REMOTE_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_MAC): cv.string, vol.Required(CONF_HOST): cv.string, diff --git a/homeassistant/components/xiaomi_miio/remote.py b/homeassistant/components/xiaomi_miio/remote.py index 5baaf614b01..959bf0a7bee 100644 --- a/homeassistant/components/xiaomi_miio/remote.py +++ b/homeassistant/components/xiaomi_miio/remote.py @@ -16,7 +16,7 @@ from homeassistant.components.remote import ( ATTR_DELAY_SECS, ATTR_NUM_REPEATS, DEFAULT_DELAY_SECS, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as REMOTE_PLATFORM_SCHEMA, RemoteEntity, ) from homeassistant.const import ( @@ -49,7 +49,7 @@ COMMAND_SCHEMA = vol.Schema( {vol.Required(CONF_COMMAND): vol.All(cv.ensure_list, [cv.string])} ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = REMOTE_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_NAME): cv.string, vol.Required(CONF_HOST): cv.string, From d4dc7d76d95b71295121307f72d6b501907d81a7 Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Wed, 26 Jun 2024 19:46:30 +1000 Subject: [PATCH 1254/1445] Refactor Tessie for future PR (#120406) * Bump tessie-api * Refactor * revert bump * Fix cover * Apply suggestions from code review Co-authored-by: G Johansson --------- Co-authored-by: G Johansson --- homeassistant/components/tessie/__init__.py | 34 +++++++++++++++---- .../components/tessie/binary_sensor.py | 6 ++-- homeassistant/components/tessie/button.py | 6 ++-- homeassistant/components/tessie/climate.py | 6 ++-- homeassistant/components/tessie/cover.py | 18 +++++----- .../components/tessie/device_tracker.py | 6 ++-- homeassistant/components/tessie/entity.py | 25 ++++---------- homeassistant/components/tessie/lock.py | 14 ++++---- .../components/tessie/media_player.py | 6 ++-- homeassistant/components/tessie/models.py | 13 ++++++- homeassistant/components/tessie/number.py | 6 ++-- homeassistant/components/tessie/select.py | 3 +- homeassistant/components/tessie/sensor.py | 6 ++-- homeassistant/components/tessie/switch.py | 8 ++--- homeassistant/components/tessie/update.py | 6 ++-- 15 files changed, 92 insertions(+), 71 deletions(-) diff --git a/homeassistant/components/tessie/__init__.py b/homeassistant/components/tessie/__init__.py index 9e7bc42fa27..37fb669e54b 100644 --- a/homeassistant/components/tessie/__init__.py +++ b/homeassistant/components/tessie/__init__.py @@ -11,9 +11,11 @@ from homeassistant.const import CONF_ACCESS_TOKEN, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.device_registry import DeviceInfo +from .const import DOMAIN, MODELS from .coordinator import TessieStateUpdateCoordinator -from .models import TessieData +from .models import TessieData, TessieVehicleData PLATFORMS = [ Platform.BINARY_SENSOR, @@ -40,7 +42,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: TessieConfigEntry) -> bo api_key = entry.data[CONF_ACCESS_TOKEN] try: - vehicles = await get_state_of_all_vehicles( + state_of_all_vehicles = await get_state_of_all_vehicles( session=async_get_clientsession(hass), api_key=api_key, only_active=True, @@ -54,13 +56,31 @@ async def async_setup_entry(hass: HomeAssistant, entry: TessieConfigEntry) -> bo raise ConfigEntryNotReady from e vehicles = [ - TessieStateUpdateCoordinator( - hass, - api_key=api_key, + TessieVehicleData( vin=vehicle["vin"], - data=vehicle["last_state"], + data_coordinator=TessieStateUpdateCoordinator( + hass, + api_key=api_key, + vin=vehicle["vin"], + data=vehicle["last_state"], + ), + device=DeviceInfo( + identifiers={(DOMAIN, vehicle["vin"])}, + manufacturer="Tesla", + configuration_url="https://my.tessie.com/", + name=vehicle["last_state"]["display_name"], + model=MODELS.get( + vehicle["last_state"]["vehicle_config"]["car_type"], + vehicle["last_state"]["vehicle_config"]["car_type"], + ), + sw_version=vehicle["last_state"]["vehicle_state"]["car_version"].split( + " " + )[0], + hw_version=vehicle["last_state"]["vehicle_config"]["driver_assist"], + serial_number=vehicle["vin"], + ), ) - for vehicle in vehicles["results"] + for vehicle in state_of_all_vehicles["results"] if vehicle["last_state"] is not None ] diff --git a/homeassistant/components/tessie/binary_sensor.py b/homeassistant/components/tessie/binary_sensor.py index 2d3f1134444..eee85ce8466 100644 --- a/homeassistant/components/tessie/binary_sensor.py +++ b/homeassistant/components/tessie/binary_sensor.py @@ -16,8 +16,8 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import TessieConfigEntry from .const import TessieState -from .coordinator import TessieStateUpdateCoordinator from .entity import TessieEntity +from .models import TessieVehicleData @dataclass(frozen=True, kw_only=True) @@ -180,11 +180,11 @@ class TessieBinarySensorEntity(TessieEntity, BinarySensorEntity): def __init__( self, - coordinator: TessieStateUpdateCoordinator, + vehicle: TessieVehicleData, description: TessieBinarySensorEntityDescription, ) -> None: """Initialize the sensor.""" - super().__init__(coordinator, description.key) + super().__init__(vehicle, description.key) self.entity_description = description @property diff --git a/homeassistant/components/tessie/button.py b/homeassistant/components/tessie/button.py index 43dadec60e6..8f80f27616b 100644 --- a/homeassistant/components/tessie/button.py +++ b/homeassistant/components/tessie/button.py @@ -19,8 +19,8 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import TessieConfigEntry -from .coordinator import TessieStateUpdateCoordinator from .entity import TessieEntity +from .models import TessieVehicleData @dataclass(frozen=True, kw_only=True) @@ -67,11 +67,11 @@ class TessieButtonEntity(TessieEntity, ButtonEntity): def __init__( self, - coordinator: TessieStateUpdateCoordinator, + vehicle: TessieVehicleData, description: TessieButtonEntityDescription, ) -> None: """Initialize the Button.""" - super().__init__(coordinator, description.key) + super().__init__(vehicle, description.key) self.entity_description = description async def async_press(self) -> None: diff --git a/homeassistant/components/tessie/climate.py b/homeassistant/components/tessie/climate.py index 2a3b77ab8ce..7676d2f071b 100644 --- a/homeassistant/components/tessie/climate.py +++ b/homeassistant/components/tessie/climate.py @@ -23,8 +23,8 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import TessieConfigEntry from .const import TessieClimateKeeper -from .coordinator import TessieStateUpdateCoordinator from .entity import TessieEntity +from .models import TessieVehicleData async def async_setup_entry( @@ -62,10 +62,10 @@ class TessieClimateEntity(TessieEntity, ClimateEntity): def __init__( self, - coordinator: TessieStateUpdateCoordinator, + vehicle: TessieVehicleData, ) -> None: """Initialize the Climate entity.""" - super().__init__(coordinator, "primary") + super().__init__(vehicle, "primary") @property def hvac_mode(self) -> HVACMode | None: diff --git a/homeassistant/components/tessie/cover.py b/homeassistant/components/tessie/cover.py index 5be08107a29..6fdd950b809 100644 --- a/homeassistant/components/tessie/cover.py +++ b/homeassistant/components/tessie/cover.py @@ -23,8 +23,8 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import TessieConfigEntry from .const import TessieCoverStates -from .coordinator import TessieStateUpdateCoordinator from .entity import TessieEntity +from .models import TessieVehicleData async def async_setup_entry( @@ -53,9 +53,9 @@ class TessieWindowEntity(TessieEntity, CoverEntity): _attr_device_class = CoverDeviceClass.WINDOW _attr_supported_features = CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE - def __init__(self, coordinator: TessieStateUpdateCoordinator) -> None: + def __init__(self, vehicle: TessieVehicleData) -> None: """Initialize the sensor.""" - super().__init__(coordinator, "windows") + super().__init__(vehicle, "windows") @property def is_closed(self) -> bool | None: @@ -94,9 +94,9 @@ class TessieChargePortEntity(TessieEntity, CoverEntity): _attr_device_class = CoverDeviceClass.DOOR _attr_supported_features = CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE - def __init__(self, coordinator: TessieStateUpdateCoordinator) -> None: + def __init__(self, vehicle: TessieVehicleData) -> None: """Initialize the sensor.""" - super().__init__(coordinator, "charge_state_charge_port_door_open") + super().__init__(vehicle, "charge_state_charge_port_door_open") @property def is_closed(self) -> bool | None: @@ -120,9 +120,9 @@ class TessieFrontTrunkEntity(TessieEntity, CoverEntity): _attr_device_class = CoverDeviceClass.DOOR _attr_supported_features = CoverEntityFeature.OPEN - def __init__(self, coordinator: TessieStateUpdateCoordinator) -> None: + def __init__(self, vehicle: TessieVehicleData) -> None: """Initialize the sensor.""" - super().__init__(coordinator, "vehicle_state_ft") + super().__init__(vehicle, "vehicle_state_ft") @property def is_closed(self) -> bool | None: @@ -141,9 +141,9 @@ class TessieRearTrunkEntity(TessieEntity, CoverEntity): _attr_device_class = CoverDeviceClass.DOOR _attr_supported_features = CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE - def __init__(self, coordinator: TessieStateUpdateCoordinator) -> None: + def __init__(self, vehicle: TessieVehicleData) -> None: """Initialize the sensor.""" - super().__init__(coordinator, "vehicle_state_rt") + super().__init__(vehicle, "vehicle_state_rt") @property def is_closed(self) -> bool | None: diff --git a/homeassistant/components/tessie/device_tracker.py b/homeassistant/components/tessie/device_tracker.py index 382c775c200..300aae7d858 100644 --- a/homeassistant/components/tessie/device_tracker.py +++ b/homeassistant/components/tessie/device_tracker.py @@ -9,8 +9,8 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from . import TessieConfigEntry -from .coordinator import TessieStateUpdateCoordinator from .entity import TessieEntity +from .models import TessieVehicleData async def async_setup_entry( @@ -36,10 +36,10 @@ class TessieDeviceTrackerEntity(TessieEntity, TrackerEntity): def __init__( self, - coordinator: TessieStateUpdateCoordinator, + vehicle: TessieVehicleData, ) -> None: """Initialize the device tracker.""" - super().__init__(coordinator, self.key) + super().__init__(vehicle, self.key) @property def source_type(self) -> SourceType | str: diff --git a/homeassistant/components/tessie/entity.py b/homeassistant/components/tessie/entity.py index 35d41af32f2..1b7ddcbe84c 100644 --- a/homeassistant/components/tessie/entity.py +++ b/homeassistant/components/tessie/entity.py @@ -6,11 +6,11 @@ from typing import Any from aiohttp import ClientResponseError from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import DOMAIN, MODELS +from .const import DOMAIN from .coordinator import TessieStateUpdateCoordinator +from .models import TessieVehicleData class TessieEntity(CoordinatorEntity[TessieStateUpdateCoordinator]): @@ -20,28 +20,17 @@ class TessieEntity(CoordinatorEntity[TessieStateUpdateCoordinator]): def __init__( self, - coordinator: TessieStateUpdateCoordinator, + vehicle: TessieVehicleData, key: str, ) -> None: """Initialize common aspects of a Tessie entity.""" - super().__init__(coordinator) - self.vin = coordinator.vin + super().__init__(vehicle.data_coordinator) + self.vin = vehicle.vin self.key = key - car_type = coordinator.data["vehicle_config_car_type"] - self._attr_translation_key = key - self._attr_unique_id = f"{self.vin}-{key}" - self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, self.vin)}, - manufacturer="Tesla", - configuration_url="https://my.tessie.com/", - name=coordinator.data["display_name"], - model=MODELS.get(car_type, car_type), - sw_version=coordinator.data["vehicle_state_car_version"].split(" ")[0], - hw_version=coordinator.data["vehicle_config_driver_assist"], - serial_number=self.vin, - ) + self._attr_unique_id = f"{vehicle.vin}-{key}" + self._attr_device_info = vehicle.device @property def _value(self) -> Any: diff --git a/homeassistant/components/tessie/lock.py b/homeassistant/components/tessie/lock.py index 0ea65ce4781..d73d83e399d 100644 --- a/homeassistant/components/tessie/lock.py +++ b/homeassistant/components/tessie/lock.py @@ -23,8 +23,8 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import TessieConfigEntry from .const import DOMAIN, TessieChargeCableLockStates -from .coordinator import TessieStateUpdateCoordinator from .entity import TessieEntity +from .models import TessieVehicleData async def async_setup_entry( @@ -82,10 +82,10 @@ class TessieLockEntity(TessieEntity, LockEntity): def __init__( self, - coordinator: TessieStateUpdateCoordinator, + vehicle: TessieVehicleData, ) -> None: """Initialize the sensor.""" - super().__init__(coordinator, "vehicle_state_locked") + super().__init__(vehicle, "vehicle_state_locked") @property def is_locked(self) -> bool | None: @@ -110,10 +110,10 @@ class TessieSpeedLimitEntity(TessieEntity, LockEntity): def __init__( self, - coordinator: TessieStateUpdateCoordinator, + vehicle: TessieVehicleData, ) -> None: """Initialize the sensor.""" - super().__init__(coordinator, "vehicle_state_speed_limit_mode_active") + super().__init__(vehicle, "vehicle_state_speed_limit_mode_active") @property def is_locked(self) -> bool | None: @@ -160,10 +160,10 @@ class TessieCableLockEntity(TessieEntity, LockEntity): def __init__( self, - coordinator: TessieStateUpdateCoordinator, + vehicle: TessieVehicleData, ) -> None: """Initialize the sensor.""" - super().__init__(coordinator, "charge_state_charge_port_latch") + super().__init__(vehicle, "charge_state_charge_port_latch") @property def is_locked(self) -> bool | None: diff --git a/homeassistant/components/tessie/media_player.py b/homeassistant/components/tessie/media_player.py index f99c8ad1e1f..f3b5e266604 100644 --- a/homeassistant/components/tessie/media_player.py +++ b/homeassistant/components/tessie/media_player.py @@ -11,8 +11,8 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import TessieConfigEntry -from .coordinator import TessieStateUpdateCoordinator from .entity import TessieEntity +from .models import TessieVehicleData STATES = { "Playing": MediaPlayerState.PLAYING, @@ -39,10 +39,10 @@ class TessieMediaEntity(TessieEntity, MediaPlayerEntity): def __init__( self, - coordinator: TessieStateUpdateCoordinator, + vehicle: TessieVehicleData, ) -> None: """Initialize the media player entity.""" - super().__init__(coordinator, "media") + super().__init__(vehicle, "media") @property def state(self) -> MediaPlayerState: diff --git a/homeassistant/components/tessie/models.py b/homeassistant/components/tessie/models.py index 3919db3f6d3..e96562ff8e1 100644 --- a/homeassistant/components/tessie/models.py +++ b/homeassistant/components/tessie/models.py @@ -4,6 +4,8 @@ from __future__ import annotations from dataclasses import dataclass +from homeassistant.helpers.device_registry import DeviceInfo + from .coordinator import TessieStateUpdateCoordinator @@ -11,4 +13,13 @@ from .coordinator import TessieStateUpdateCoordinator class TessieData: """Data for the Tessie integration.""" - vehicles: list[TessieStateUpdateCoordinator] + vehicles: list[TessieVehicleData] + + +@dataclass +class TessieVehicleData: + """Data for a Tessie vehicle.""" + + data_coordinator: TessieStateUpdateCoordinator + device: DeviceInfo + vin: str diff --git a/homeassistant/components/tessie/number.py b/homeassistant/components/tessie/number.py index 222922eba3e..56739193d7f 100644 --- a/homeassistant/components/tessie/number.py +++ b/homeassistant/components/tessie/number.py @@ -23,8 +23,8 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import TessieConfigEntry -from .coordinator import TessieStateUpdateCoordinator from .entity import TessieEntity +from .models import TessieVehicleData @dataclass(frozen=True, kw_only=True) @@ -101,11 +101,11 @@ class TessieNumberEntity(TessieEntity, NumberEntity): def __init__( self, - coordinator: TessieStateUpdateCoordinator, + vehicle: TessieVehicleData, description: TessieNumberEntityDescription, ) -> None: """Initialize the Number entity.""" - super().__init__(coordinator, description.key) + super().__init__(vehicle, description.key) self.entity_description = description @property diff --git a/homeassistant/components/tessie/select.py b/homeassistant/components/tessie/select.py index 801d465ea2a..43af8161697 100644 --- a/homeassistant/components/tessie/select.py +++ b/homeassistant/components/tessie/select.py @@ -35,7 +35,8 @@ async def async_setup_entry( TessieSeatHeaterSelectEntity(vehicle, key) for vehicle in data.vehicles for key in SEAT_HEATERS - if key in vehicle.data # not all vehicles have rear center or third row + if key + in vehicle.data_coordinator.data # not all vehicles have rear center or third row ) diff --git a/homeassistant/components/tessie/sensor.py b/homeassistant/components/tessie/sensor.py index c3023948f4c..dc910c7a03a 100644 --- a/homeassistant/components/tessie/sensor.py +++ b/homeassistant/components/tessie/sensor.py @@ -34,8 +34,8 @@ from homeassistant.util.variance import ignore_variance from . import TessieConfigEntry from .const import TessieChargeStates -from .coordinator import TessieStateUpdateCoordinator from .entity import TessieEntity +from .models import TessieVehicleData @callback @@ -280,11 +280,11 @@ class TessieSensorEntity(TessieEntity, SensorEntity): def __init__( self, - coordinator: TessieStateUpdateCoordinator, + vehicle: TessieVehicleData, description: TessieSensorEntityDescription, ) -> None: """Initialize the sensor.""" - super().__init__(coordinator, description.key) + super().__init__(vehicle, description.key) self.entity_description = description @property diff --git a/homeassistant/components/tessie/switch.py b/homeassistant/components/tessie/switch.py index 2f3902b3bd3..03bd018cd83 100644 --- a/homeassistant/components/tessie/switch.py +++ b/homeassistant/components/tessie/switch.py @@ -29,8 +29,8 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import TessieConfigEntry -from .coordinator import TessieStateUpdateCoordinator from .entity import TessieEntity +from .models import TessieVehicleData @dataclass(frozen=True, kw_only=True) @@ -84,7 +84,7 @@ async def async_setup_entry( TessieSwitchEntity(vehicle, description) for vehicle in entry.runtime_data.vehicles for description in DESCRIPTIONS - if description.key in vehicle.data + if description.key in vehicle.data_coordinator.data ), ( TessieChargeSwitchEntity(vehicle, CHARGE_DESCRIPTION) @@ -102,11 +102,11 @@ class TessieSwitchEntity(TessieEntity, SwitchEntity): def __init__( self, - coordinator: TessieStateUpdateCoordinator, + vehicle: TessieVehicleData, description: TessieSwitchEntityDescription, ) -> None: """Initialize the Switch.""" - super().__init__(coordinator, description.key) + super().__init__(vehicle, description.key) self.entity_description = description @property diff --git a/homeassistant/components/tessie/update.py b/homeassistant/components/tessie/update.py index 5f51a38d77d..73a01873e37 100644 --- a/homeassistant/components/tessie/update.py +++ b/homeassistant/components/tessie/update.py @@ -12,8 +12,8 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import TessieConfigEntry from .const import TessieUpdateStatus -from .coordinator import TessieStateUpdateCoordinator from .entity import TessieEntity +from .models import TessieVehicleData async def async_setup_entry( @@ -34,10 +34,10 @@ class TessieUpdateEntity(TessieEntity, UpdateEntity): def __init__( self, - coordinator: TessieStateUpdateCoordinator, + vehicle: TessieVehicleData, ) -> None: """Initialize the Update.""" - super().__init__(coordinator, "update") + super().__init__(vehicle, "update") @property def supported_features(self) -> UpdateEntityFeature: From 912136be258b973b8e07a996a039e3cc618729ec Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 26 Jun 2024 11:52:57 +0200 Subject: [PATCH 1255/1445] Force alias when importing lock PLATFORM_SCHEMA (#120531) --- homeassistant/components/group/lock.py | 4 ++-- homeassistant/components/kiwi/lock.py | 7 +++++-- homeassistant/components/sesame/lock.py | 7 +++++-- homeassistant/components/template/lock.py | 4 ++-- 4 files changed, 14 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/group/lock.py b/homeassistant/components/group/lock.py index 4da5829634b..8bb7b18ce29 100644 --- a/homeassistant/components/group/lock.py +++ b/homeassistant/components/group/lock.py @@ -9,7 +9,7 @@ import voluptuous as vol from homeassistant.components.lock import ( DOMAIN, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as LOCK_PLATFORM_SCHEMA, LockEntity, LockEntityFeature, ) @@ -43,7 +43,7 @@ DEFAULT_NAME = "Lock Group" # No limit on parallel updates to enable a group calling another group PARALLEL_UPDATES = 0 -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = LOCK_PLATFORM_SCHEMA.extend( { vol.Required(CONF_ENTITIES): cv.entities_domain(DOMAIN), vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/kiwi/lock.py b/homeassistant/components/kiwi/lock.py index 770b842091c..bde9a77f748 100644 --- a/homeassistant/components/kiwi/lock.py +++ b/homeassistant/components/kiwi/lock.py @@ -8,7 +8,10 @@ from typing import Any from kiwiki import KiwiClient, KiwiException import voluptuous as vol -from homeassistant.components.lock import PLATFORM_SCHEMA, LockEntity +from homeassistant.components.lock import ( + PLATFORM_SCHEMA as LOCK_PLATFORM_SCHEMA, + LockEntity, +) from homeassistant.const import ( ATTR_ID, ATTR_LATITUDE, @@ -32,7 +35,7 @@ ATTR_CAN_INVITE = "can_invite_others" UNLOCK_MAINTAIN_TIME = 5 -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = LOCK_PLATFORM_SCHEMA.extend( {vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string} ) diff --git a/homeassistant/components/sesame/lock.py b/homeassistant/components/sesame/lock.py index 050a5978acc..ad8b26f7034 100644 --- a/homeassistant/components/sesame/lock.py +++ b/homeassistant/components/sesame/lock.py @@ -7,7 +7,10 @@ from typing import Any import pysesame2 import voluptuous as vol -from homeassistant.components.lock import PLATFORM_SCHEMA, LockEntity +from homeassistant.components.lock import ( + PLATFORM_SCHEMA as LOCK_PLATFORM_SCHEMA, + LockEntity, +) from homeassistant.const import ATTR_BATTERY_LEVEL, ATTR_DEVICE_ID, CONF_API_KEY from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -16,7 +19,7 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType ATTR_SERIAL_NO = "serial" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({vol.Required(CONF_API_KEY): cv.string}) +PLATFORM_SCHEMA = LOCK_PLATFORM_SCHEMA.extend({vol.Required(CONF_API_KEY): cv.string}) def setup_platform( diff --git a/homeassistant/components/template/lock.py b/homeassistant/components/template/lock.py index 8259a6c12f0..0fa219fcd9b 100644 --- a/homeassistant/components/template/lock.py +++ b/homeassistant/components/template/lock.py @@ -7,7 +7,7 @@ from typing import Any import voluptuous as vol from homeassistant.components.lock import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as LOCK_PLATFORM_SCHEMA, STATE_JAMMED, STATE_LOCKING, STATE_UNLOCKING, @@ -44,7 +44,7 @@ CONF_UNLOCK = "unlock" DEFAULT_NAME = "Template Lock" DEFAULT_OPTIMISTIC = False -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = LOCK_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_NAME): cv.string, vol.Required(CONF_LOCK): cv.SCRIPT_SCHEMA, From 32bab97f006edb6d81cb4c33417dac9bc10670de Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 26 Jun 2024 11:53:15 +0200 Subject: [PATCH 1256/1445] Force alias when importing light PLATFORM_SCHEMA (#120529) --- homeassistant/components/ads/light.py | 4 ++-- homeassistant/components/avion/light.py | 4 ++-- homeassistant/components/blinksticklight/light.py | 4 ++-- homeassistant/components/decora/light.py | 4 ++-- homeassistant/components/decora_wifi/light.py | 4 ++-- homeassistant/components/enocean/light.py | 4 ++-- homeassistant/components/everlights/light.py | 4 ++-- homeassistant/components/futurenow/light.py | 4 ++-- homeassistant/components/greenwave/light.py | 4 ++-- homeassistant/components/group/light.py | 4 ++-- homeassistant/components/iglo/light.py | 4 ++-- homeassistant/components/limitlessled/light.py | 4 ++-- homeassistant/components/lw12wifi/light.py | 4 ++-- homeassistant/components/mochad/light.py | 4 ++-- homeassistant/components/niko_home_control/light.py | 4 ++-- homeassistant/components/opple/light.py | 4 ++-- homeassistant/components/osramlightify/light.py | 4 ++-- homeassistant/components/pilight/light.py | 4 ++-- homeassistant/components/rflink/light.py | 4 ++-- homeassistant/components/scsgate/light.py | 8 ++++++-- homeassistant/components/switch/light.py | 8 ++++++-- homeassistant/components/tikteck/light.py | 4 ++-- homeassistant/components/unifiled/light.py | 4 ++-- homeassistant/components/x10/light.py | 4 ++-- homeassistant/components/yeelightsunflower/light.py | 4 ++-- homeassistant/components/zengge/light.py | 4 ++-- 26 files changed, 60 insertions(+), 52 deletions(-) diff --git a/homeassistant/components/ads/light.py b/homeassistant/components/ads/light.py index 13ce9ec261c..0df69a60093 100644 --- a/homeassistant/components/ads/light.py +++ b/homeassistant/components/ads/light.py @@ -9,7 +9,7 @@ import voluptuous as vol from homeassistant.components.light import ( ATTR_BRIGHTNESS, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as LIGHT_PLATFORM_SCHEMA, ColorMode, LightEntity, ) @@ -29,7 +29,7 @@ from . import ( ) DEFAULT_NAME = "ADS Light" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = LIGHT_PLATFORM_SCHEMA.extend( { vol.Required(CONF_ADS_VAR): cv.string, vol.Optional(CONF_ADS_VAR_BRIGHTNESS): cv.string, diff --git a/homeassistant/components/avion/light.py b/homeassistant/components/avion/light.py index e26676a0169..687405e3064 100644 --- a/homeassistant/components/avion/light.py +++ b/homeassistant/components/avion/light.py @@ -10,7 +10,7 @@ import voluptuous as vol from homeassistant.components.light import ( ATTR_BRIGHTNESS, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as LIGHT_PLATFORM_SCHEMA, ColorMode, LightEntity, ) @@ -35,7 +35,7 @@ DEVICE_SCHEMA = vol.Schema( } ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = LIGHT_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_DEVICES, default={}): {cv.string: DEVICE_SCHEMA}, vol.Optional(CONF_USERNAME): cv.string, diff --git a/homeassistant/components/blinksticklight/light.py b/homeassistant/components/blinksticklight/light.py index 3e1f60e0f50..a789a7e0503 100644 --- a/homeassistant/components/blinksticklight/light.py +++ b/homeassistant/components/blinksticklight/light.py @@ -10,7 +10,7 @@ import voluptuous as vol from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_HS_COLOR, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as LIGHT_PLATFORM_SCHEMA, ColorMode, LightEntity, ) @@ -25,7 +25,7 @@ CONF_SERIAL = "serial" DEFAULT_NAME = "Blinkstick" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = LIGHT_PLATFORM_SCHEMA.extend( { vol.Required(CONF_SERIAL): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/decora/light.py b/homeassistant/components/decora/light.py index 3f8118a6e5d..cef7b98a2c1 100644 --- a/homeassistant/components/decora/light.py +++ b/homeassistant/components/decora/light.py @@ -16,7 +16,7 @@ import voluptuous as vol from homeassistant import util from homeassistant.components.light import ( ATTR_BRIGHTNESS, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as LIGHT_PLATFORM_SCHEMA, ColorMode, LightEntity, ) @@ -48,7 +48,7 @@ DEVICE_SCHEMA = vol.Schema( PLATFORM_SCHEMA = vol.Schema( vol.All( - PLATFORM_SCHEMA.extend( + LIGHT_PLATFORM_SCHEMA.extend( {vol.Optional(CONF_DEVICES, default={}): {cv.string: DEVICE_SCHEMA}} ), _name_validator, diff --git a/homeassistant/components/decora_wifi/light.py b/homeassistant/components/decora_wifi/light.py index 798243b5d4b..63ab5c2bf02 100644 --- a/homeassistant/components/decora_wifi/light.py +++ b/homeassistant/components/decora_wifi/light.py @@ -15,7 +15,7 @@ from homeassistant.components import persistent_notification from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_TRANSITION, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as LIGHT_PLATFORM_SCHEMA, ColorMode, LightEntity, LightEntityFeature, @@ -29,7 +29,7 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType _LOGGER = logging.getLogger(__name__) # Validation of the user's configuration -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = LIGHT_PLATFORM_SCHEMA.extend( {vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string} ) diff --git a/homeassistant/components/enocean/light.py b/homeassistant/components/enocean/light.py index 937930c4a31..1e81e3cd089 100644 --- a/homeassistant/components/enocean/light.py +++ b/homeassistant/components/enocean/light.py @@ -10,7 +10,7 @@ import voluptuous as vol from homeassistant.components.light import ( ATTR_BRIGHTNESS, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as LIGHT_PLATFORM_SCHEMA, ColorMode, LightEntity, ) @@ -26,7 +26,7 @@ CONF_SENDER_ID = "sender_id" DEFAULT_NAME = "EnOcean Light" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = LIGHT_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_ID, default=[]): vol.All(cv.ensure_list, [vol.Coerce(int)]), vol.Required(CONF_SENDER_ID): vol.All(cv.ensure_list, [vol.Coerce(int)]), diff --git a/homeassistant/components/everlights/light.py b/homeassistant/components/everlights/light.py index 334e464d67e..2ba47978353 100644 --- a/homeassistant/components/everlights/light.py +++ b/homeassistant/components/everlights/light.py @@ -13,7 +13,7 @@ from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_EFFECT, ATTR_HS_COLOR, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as LIGHT_PLATFORM_SCHEMA, ColorMode, LightEntity, LightEntityFeature, @@ -31,7 +31,7 @@ _LOGGER = logging.getLogger(__name__) SCAN_INTERVAL = timedelta(minutes=1) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = LIGHT_PLATFORM_SCHEMA.extend( {vol.Required(CONF_HOSTS): vol.All(cv.ensure_list, [cv.string])} ) diff --git a/homeassistant/components/futurenow/light.py b/homeassistant/components/futurenow/light.py index 8474c1073e9..d1ad6f42083 100644 --- a/homeassistant/components/futurenow/light.py +++ b/homeassistant/components/futurenow/light.py @@ -9,7 +9,7 @@ import voluptuous as vol from homeassistant.components.light import ( ATTR_BRIGHTNESS, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as LIGHT_PLATFORM_SCHEMA, ColorMode, LightEntity, ) @@ -31,7 +31,7 @@ DEVICE_SCHEMA = vol.Schema( } ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = LIGHT_PLATFORM_SCHEMA.extend( { vol.Required(CONF_DRIVER): vol.In(CONF_DRIVER_TYPES), vol.Required(CONF_HOST): cv.string, diff --git a/homeassistant/components/greenwave/light.py b/homeassistant/components/greenwave/light.py index aa592727220..89d3ca3a535 100644 --- a/homeassistant/components/greenwave/light.py +++ b/homeassistant/components/greenwave/light.py @@ -12,7 +12,7 @@ import voluptuous as vol from homeassistant.components.light import ( ATTR_BRIGHTNESS, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as LIGHT_PLATFORM_SCHEMA, ColorMode, LightEntity, ) @@ -27,7 +27,7 @@ _LOGGER = logging.getLogger(__name__) CONF_VERSION = "version" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = LIGHT_PLATFORM_SCHEMA.extend( {vol.Required(CONF_HOST): cv.string, vol.Required(CONF_VERSION): cv.positive_int} ) diff --git a/homeassistant/components/group/light.py b/homeassistant/components/group/light.py index 9adced828c7..228645df974 100644 --- a/homeassistant/components/group/light.py +++ b/homeassistant/components/group/light.py @@ -27,7 +27,7 @@ from homeassistant.components.light import ( ATTR_TRANSITION, ATTR_WHITE, ATTR_XY_COLOR, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as LIGHT_PLATFORM_SCHEMA, ColorMode, LightEntity, LightEntityFeature, @@ -60,7 +60,7 @@ CONF_ALL = "all" # No limit on parallel updates to enable a group calling another group PARALLEL_UPDATES = 0 -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = LIGHT_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_UNIQUE_ID): cv.string, diff --git a/homeassistant/components/iglo/light.py b/homeassistant/components/iglo/light.py index 1cd303b8856..a31183f4489 100644 --- a/homeassistant/components/iglo/light.py +++ b/homeassistant/components/iglo/light.py @@ -14,7 +14,7 @@ from homeassistant.components.light import ( ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_HS_COLOR, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as LIGHT_PLATFORM_SCHEMA, ColorMode, LightEntity, LightEntityFeature, @@ -29,7 +29,7 @@ import homeassistant.util.color as color_util DEFAULT_NAME = "iGlo Light" DEFAULT_PORT = 8080 -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = LIGHT_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/limitlessled/light.py b/homeassistant/components/limitlessled/light.py index 182c12eb395..4456d112d0f 100644 --- a/homeassistant/components/limitlessled/light.py +++ b/homeassistant/components/limitlessled/light.py @@ -27,7 +27,7 @@ from homeassistant.components.light import ( EFFECT_COLORLOOP, EFFECT_WHITE, FLASH_LONG, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as LIGHT_PLATFORM_SCHEMA, ColorMode, LightEntity, LightEntityFeature, @@ -75,7 +75,7 @@ SUPPORT_LIMITLESSLED_RGBWW = ( LightEntityFeature.EFFECT | LightEntityFeature.FLASH | LightEntityFeature.TRANSITION ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = LIGHT_PLATFORM_SCHEMA.extend( { vol.Required(CONF_BRIDGES): vol.All( cv.ensure_list, diff --git a/homeassistant/components/lw12wifi/light.py b/homeassistant/components/lw12wifi/light.py index 272fcd4a8a1..60741c861dd 100644 --- a/homeassistant/components/lw12wifi/light.py +++ b/homeassistant/components/lw12wifi/light.py @@ -13,7 +13,7 @@ from homeassistant.components.light import ( ATTR_EFFECT, ATTR_HS_COLOR, ATTR_TRANSITION, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as LIGHT_PLATFORM_SCHEMA, ColorMode, LightEntity, LightEntityFeature, @@ -31,7 +31,7 @@ _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = "LW-12 FC" DEFAULT_PORT = 5000 -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = LIGHT_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, diff --git a/homeassistant/components/mochad/light.py b/homeassistant/components/mochad/light.py index 4740823d85a..fe5a8ccd07d 100644 --- a/homeassistant/components/mochad/light.py +++ b/homeassistant/components/mochad/light.py @@ -11,7 +11,7 @@ import voluptuous as vol from homeassistant.components.light import ( ATTR_BRIGHTNESS, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as LIGHT_PLATFORM_SCHEMA, ColorMode, LightEntity, ) @@ -26,7 +26,7 @@ from . import CONF_COMM_TYPE, DOMAIN, REQ_LOCK, MochadCtrl _LOGGER = logging.getLogger(__name__) CONF_BRIGHTNESS_LEVELS = "brightness_levels" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = LIGHT_PLATFORM_SCHEMA.extend( { vol.Required(CONF_PLATFORM): DOMAIN, CONF_DEVICES: [ diff --git a/homeassistant/components/niko_home_control/light.py b/homeassistant/components/niko_home_control/light.py index 27a9cc22549..360b45cceed 100644 --- a/homeassistant/components/niko_home_control/light.py +++ b/homeassistant/components/niko_home_control/light.py @@ -11,7 +11,7 @@ import voluptuous as vol from homeassistant.components.light import ( ATTR_BRIGHTNESS, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as LIGHT_PLATFORM_SCHEMA, ColorMode, LightEntity, brightness_supported, @@ -28,7 +28,7 @@ _LOGGER = logging.getLogger(__name__) MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=1) SCAN_INTERVAL = timedelta(seconds=30) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({vol.Required(CONF_HOST): cv.string}) +PLATFORM_SCHEMA = LIGHT_PLATFORM_SCHEMA.extend({vol.Required(CONF_HOST): cv.string}) async def async_setup_platform( diff --git a/homeassistant/components/opple/light.py b/homeassistant/components/opple/light.py index 2fbbf6ae02a..a4aa98bbf69 100644 --- a/homeassistant/components/opple/light.py +++ b/homeassistant/components/opple/light.py @@ -11,7 +11,7 @@ import voluptuous as vol from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as LIGHT_PLATFORM_SCHEMA, ColorMode, LightEntity, ) @@ -29,7 +29,7 @@ _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = "opple light" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = LIGHT_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/osramlightify/light.py b/homeassistant/components/osramlightify/light.py index 50696530e8a..0254c478b42 100644 --- a/homeassistant/components/osramlightify/light.py +++ b/homeassistant/components/osramlightify/light.py @@ -16,7 +16,7 @@ from homeassistant.components.light import ( ATTR_HS_COLOR, ATTR_TRANSITION, EFFECT_RANDOM, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as LIGHT_PLATFORM_SCHEMA, ColorMode, LightEntity, LightEntityFeature, @@ -45,7 +45,7 @@ DEFAULT_ALLOW_LIGHTIFY_SWITCHES = True DEFAULT_INTERVAL_LIGHTIFY_STATUS = 5 DEFAULT_INTERVAL_LIGHTIFY_CONF = 3600 -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = LIGHT_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Optional( diff --git a/homeassistant/components/pilight/light.py b/homeassistant/components/pilight/light.py index 60713b59475..5665e96b9c9 100644 --- a/homeassistant/components/pilight/light.py +++ b/homeassistant/components/pilight/light.py @@ -8,7 +8,7 @@ import voluptuous as vol from homeassistant.components.light import ( ATTR_BRIGHTNESS, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as LIGHT_PLATFORM_SCHEMA, ColorMode, LightEntity, ) @@ -28,7 +28,7 @@ LIGHTS_SCHEMA = SWITCHES_SCHEMA.extend( } ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = LIGHT_PLATFORM_SCHEMA.extend( {vol.Required(CONF_LIGHTS): vol.Schema({cv.string: LIGHTS_SCHEMA})} ) diff --git a/homeassistant/components/rflink/light.py b/homeassistant/components/rflink/light.py index d354e317ccb..b29bb4f1d48 100644 --- a/homeassistant/components/rflink/light.py +++ b/homeassistant/components/rflink/light.py @@ -10,7 +10,7 @@ import voluptuous as vol from homeassistant.components.light import ( ATTR_BRIGHTNESS, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as LIGHT_PLATFORM_SCHEMA, ColorMode, LightEntity, ) @@ -46,7 +46,7 @@ TYPE_SWITCHABLE = "switchable" TYPE_HYBRID = "hybrid" TYPE_TOGGLE = "toggle" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = LIGHT_PLATFORM_SCHEMA.extend( { vol.Optional( CONF_DEVICE_DEFAULTS, default=DEVICE_DEFAULTS_SCHEMA({}) diff --git a/homeassistant/components/scsgate/light.py b/homeassistant/components/scsgate/light.py index a4bb78fcd1c..23b73a0fd6b 100644 --- a/homeassistant/components/scsgate/light.py +++ b/homeassistant/components/scsgate/light.py @@ -8,7 +8,11 @@ from typing import Any from scsgate.tasks import ToggleStatusTask import voluptuous as vol -from homeassistant.components.light import PLATFORM_SCHEMA, ColorMode, LightEntity +from homeassistant.components.light import ( + PLATFORM_SCHEMA as LIGHT_PLATFORM_SCHEMA, + ColorMode, + LightEntity, +) from homeassistant.const import ATTR_ENTITY_ID, ATTR_STATE, CONF_DEVICES, CONF_NAME from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -17,7 +21,7 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import CONF_SCS_ID, DOMAIN, SCSGATE_SCHEMA -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = LIGHT_PLATFORM_SCHEMA.extend( {vol.Required(CONF_DEVICES): cv.schema_with_slug_keys(SCSGATE_SCHEMA)} ) diff --git a/homeassistant/components/switch/light.py b/homeassistant/components/switch/light.py index f226ed57e2a..48d555e6616 100644 --- a/homeassistant/components/switch/light.py +++ b/homeassistant/components/switch/light.py @@ -6,7 +6,11 @@ from typing import Any import voluptuous as vol -from homeassistant.components.light import PLATFORM_SCHEMA, ColorMode, LightEntity +from homeassistant.components.light import ( + PLATFORM_SCHEMA as LIGHT_PLATFORM_SCHEMA, + ColorMode, + LightEntity, +) from homeassistant.const import ( ATTR_ENTITY_ID, CONF_ENTITY_ID, @@ -27,7 +31,7 @@ from .const import DOMAIN as SWITCH_DOMAIN DEFAULT_NAME = "Light Switch" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = LIGHT_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Required(CONF_ENTITY_ID): cv.entity_domain(SWITCH_DOMAIN), diff --git a/homeassistant/components/tikteck/light.py b/homeassistant/components/tikteck/light.py index 93549b26f48..26ffc0e7b6d 100644 --- a/homeassistant/components/tikteck/light.py +++ b/homeassistant/components/tikteck/light.py @@ -11,7 +11,7 @@ import voluptuous as vol from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_HS_COLOR, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as LIGHT_PLATFORM_SCHEMA, ColorMode, LightEntity, ) @@ -28,7 +28,7 @@ DEVICE_SCHEMA = vol.Schema( {vol.Optional(CONF_NAME): cv.string, vol.Required(CONF_PASSWORD): cv.string} ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = LIGHT_PLATFORM_SCHEMA.extend( {vol.Optional(CONF_DEVICES, default={}): {cv.string: DEVICE_SCHEMA}} ) diff --git a/homeassistant/components/unifiled/light.py b/homeassistant/components/unifiled/light.py index f69ea5712de..4e1981875f4 100644 --- a/homeassistant/components/unifiled/light.py +++ b/homeassistant/components/unifiled/light.py @@ -10,7 +10,7 @@ import voluptuous as vol from homeassistant.components.light import ( ATTR_BRIGHTNESS, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as LIGHT_PLATFORM_SCHEMA, ColorMode, LightEntity, ) @@ -23,7 +23,7 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType _LOGGER = logging.getLogger(__name__) # Validation of the user's configuration -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = LIGHT_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Required(CONF_USERNAME): cv.string, diff --git a/homeassistant/components/x10/light.py b/homeassistant/components/x10/light.py index 8f105d9c695..29c15f66993 100644 --- a/homeassistant/components/x10/light.py +++ b/homeassistant/components/x10/light.py @@ -10,7 +10,7 @@ import voluptuous as vol from homeassistant.components.light import ( ATTR_BRIGHTNESS, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as LIGHT_PLATFORM_SCHEMA, ColorMode, LightEntity, ) @@ -22,7 +22,7 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType _LOGGER = logging.getLogger(__name__) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = LIGHT_PLATFORM_SCHEMA.extend( { vol.Required(CONF_DEVICES): vol.All( cv.ensure_list, diff --git a/homeassistant/components/yeelightsunflower/light.py b/homeassistant/components/yeelightsunflower/light.py index 45b662846d5..0d8247fc865 100644 --- a/homeassistant/components/yeelightsunflower/light.py +++ b/homeassistant/components/yeelightsunflower/light.py @@ -11,7 +11,7 @@ import yeelightsunflower from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_HS_COLOR, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as LIGHT_PLATFORM_SCHEMA, ColorMode, LightEntity, ) @@ -24,7 +24,7 @@ import homeassistant.util.color as color_util _LOGGER = logging.getLogger(__name__) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({vol.Required(CONF_HOST): cv.string}) +PLATFORM_SCHEMA = LIGHT_PLATFORM_SCHEMA.extend({vol.Required(CONF_HOST): cv.string}) def setup_platform( diff --git a/homeassistant/components/zengge/light.py b/homeassistant/components/zengge/light.py index 6657bfb9edd..69b7c63476a 100644 --- a/homeassistant/components/zengge/light.py +++ b/homeassistant/components/zengge/light.py @@ -12,7 +12,7 @@ from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_HS_COLOR, ATTR_WHITE, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as LIGHT_PLATFORM_SCHEMA, ColorMode, LightEntity, ) @@ -27,7 +27,7 @@ _LOGGER = logging.getLogger(__name__) DEVICE_SCHEMA = vol.Schema({vol.Optional(CONF_NAME): cv.string}) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = LIGHT_PLATFORM_SCHEMA.extend( {vol.Optional(CONF_DEVICES, default={}): {cv.string: DEVICE_SCHEMA}} ) From 348ceca19f1fe5b45dadbbd7ec96093c64409a3f Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 26 Jun 2024 12:11:22 +0200 Subject: [PATCH 1257/1445] Force alias when importing scene PLATFORM_SCHEMA (#120534) --- homeassistant/components/config/scene.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/config/scene.py b/homeassistant/components/config/scene.py index a2e2693036a..fa23d02bcc8 100644 --- a/homeassistant/components/config/scene.py +++ b/homeassistant/components/config/scene.py @@ -5,7 +5,10 @@ from __future__ import annotations from typing import Any import uuid -from homeassistant.components.scene import DOMAIN, PLATFORM_SCHEMA +from homeassistant.components.scene import ( + DOMAIN, + PLATFORM_SCHEMA as SCENE_PLATFORM_SCHEMA, +) from homeassistant.config import SCENE_CONFIG_PATH from homeassistant.const import CONF_ID, SERVICE_RELOAD from homeassistant.core import DOMAIN as HA_DOMAIN, HomeAssistant, callback @@ -40,7 +43,7 @@ def async_setup(hass: HomeAssistant) -> bool: "config", SCENE_CONFIG_PATH, cv.string, - PLATFORM_SCHEMA, + SCENE_PLATFORM_SCHEMA, post_write_hook=hook, ) ) From c49fce5541d72d4142217c33193b4996cc53f93e Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 26 Jun 2024 12:14:13 +0200 Subject: [PATCH 1258/1445] Force alias when importing sensor PLATFORM_SCHEMA (#120536) --- homeassistant/components/ads/sensor.py | 7 +++++-- homeassistant/components/alpha_vantage/sensor.py | 7 +++++-- homeassistant/components/aqualogic/sensor.py | 4 ++-- homeassistant/components/arest/sensor.py | 7 +++++-- homeassistant/components/atome/sensor.py | 4 ++-- homeassistant/components/bbox/sensor.py | 4 ++-- homeassistant/components/beewi_smartclim/sensor.py | 4 ++-- homeassistant/components/bitcoin/sensor.py | 4 ++-- homeassistant/components/bizkaibus/sensor.py | 7 +++++-- homeassistant/components/blockchain/sensor.py | 7 +++++-- homeassistant/components/bloomsky/sensor.py | 4 ++-- homeassistant/components/cert_expiry/sensor.py | 4 ++-- homeassistant/components/comed_hourly_pricing/sensor.py | 4 ++-- homeassistant/components/comfoconnect/sensor.py | 4 ++-- homeassistant/components/cups/sensor.py | 7 +++++-- homeassistant/components/currencylayer/sensor.py | 7 +++++-- homeassistant/components/delijn/sensor.py | 4 ++-- homeassistant/components/derivative/sensor.py | 8 ++++++-- homeassistant/components/discogs/sensor.py | 4 ++-- homeassistant/components/dovado/sensor.py | 4 ++-- homeassistant/components/dte_energy_bridge/sensor.py | 4 ++-- homeassistant/components/dublin_bus_transport/sensor.py | 7 +++++-- homeassistant/components/dweet/sensor.py | 7 +++++-- homeassistant/components/ebox/sensor.py | 4 ++-- homeassistant/components/eddystone_temperature/sensor.py | 4 ++-- homeassistant/components/eliqonline/sensor.py | 4 ++-- homeassistant/components/emoncms/sensor.py | 4 ++-- homeassistant/components/enocean/sensor.py | 4 ++-- .../components/entur_public_transport/sensor.py | 7 +++++-- homeassistant/components/etherscan/sensor.py | 7 +++++-- homeassistant/components/fail2ban/sensor.py | 7 +++++-- homeassistant/components/fido/sensor.py | 4 ++-- homeassistant/components/file/sensor.py | 7 +++++-- homeassistant/components/filter/sensor.py | 4 ++-- homeassistant/components/fints/sensor.py | 7 +++++-- homeassistant/components/fitbit/sensor.py | 4 ++-- homeassistant/components/fixer/sensor.py | 7 +++++-- homeassistant/components/folder/sensor.py | 4 ++-- homeassistant/components/geo_rss_events/sensor.py | 7 +++++-- homeassistant/components/gitlab_ci/sensor.py | 7 +++++-- homeassistant/components/gitter/sensor.py | 7 +++++-- homeassistant/components/google_wifi/sensor.py | 4 ++-- homeassistant/components/gpsd/sensor.py | 4 ++-- homeassistant/components/group/sensor.py | 4 ++-- homeassistant/components/haveibeenpwned/sensor.py | 7 +++++-- homeassistant/components/hddtemp/sensor.py | 4 ++-- homeassistant/components/hp_ilo/sensor.py | 7 +++++-- homeassistant/components/iammeter/sensor.py | 4 ++-- homeassistant/components/irish_rail_transport/sensor.py | 7 +++++-- homeassistant/components/kwb/sensor.py | 9 ++++++--- homeassistant/components/lacrosse/sensor.py | 4 ++-- homeassistant/components/linux_battery/sensor.py | 4 ++-- homeassistant/components/london_air/sensor.py | 7 +++++-- homeassistant/components/london_underground/sensor.py | 7 +++++-- homeassistant/components/mfi/sensor.py | 4 ++-- homeassistant/components/min_max/sensor.py | 4 ++-- homeassistant/components/mold_indicator/sensor.py | 7 +++++-- homeassistant/components/mqtt_room/sensor.py | 7 +++++-- homeassistant/components/mvglive/sensor.py | 7 +++++-- .../components/nederlandse_spoorwegen/sensor.py | 7 +++++-- homeassistant/components/netdata/sensor.py | 7 +++++-- homeassistant/components/neurio_energy/sensor.py | 4 ++-- homeassistant/components/nmbs/sensor.py | 7 +++++-- homeassistant/components/noaa_tides/sensor.py | 7 +++++-- homeassistant/components/nsw_fuel_station/sensor.py | 7 +++++-- homeassistant/components/oasa_telematics/sensor.py | 4 ++-- homeassistant/components/ohmconnect/sensor.py | 7 +++++-- homeassistant/components/openevse/sensor.py | 4 ++-- homeassistant/components/openhardwaremonitor/sensor.py | 7 +++++-- homeassistant/components/oru/sensor.py | 6 ++++-- homeassistant/components/otp/sensor.py | 7 +++++-- homeassistant/components/pilight/sensor.py | 7 +++++-- homeassistant/components/pocketcasts/sensor.py | 7 +++++-- homeassistant/components/pyload/sensor.py | 4 ++-- homeassistant/components/raincloud/sensor.py | 7 +++++-- homeassistant/components/random/sensor.py | 7 +++++-- homeassistant/components/reddit/sensor.py | 7 +++++-- homeassistant/components/rejseplanen/sensor.py | 7 +++++-- homeassistant/components/rflink/sensor.py | 4 ++-- homeassistant/components/ripple/sensor.py | 7 +++++-- homeassistant/components/rmvtransport/sensor.py | 7 +++++-- homeassistant/components/rova/sensor.py | 4 ++-- homeassistant/components/rtorrent/sensor.py | 4 ++-- homeassistant/components/saj/sensor.py | 4 ++-- homeassistant/components/serial/sensor.py | 7 +++++-- homeassistant/components/serial_pm/sensor.py | 7 +++++-- homeassistant/components/seventeentrack/sensor.py | 7 +++++-- homeassistant/components/shodan/sensor.py | 7 +++++-- homeassistant/components/sigfox/sensor.py | 7 +++++-- homeassistant/components/simulated/sensor.py | 7 +++++-- homeassistant/components/skybeacon/sensor.py | 4 ++-- homeassistant/components/snmp/sensor.py | 7 +++++-- homeassistant/components/solaredge_local/sensor.py | 4 ++-- homeassistant/components/starlingbank/sensor.py | 7 +++++-- homeassistant/components/startca/sensor.py | 4 ++-- homeassistant/components/supervisord/sensor.py | 7 +++++-- .../components/swiss_hydrological_data/sensor.py | 7 +++++-- homeassistant/components/tank_utility/sensor.py | 7 +++++-- homeassistant/components/tcp/sensor.py | 4 ++-- homeassistant/components/ted5000/sensor.py | 4 ++-- homeassistant/components/tellstick/sensor.py | 4 ++-- homeassistant/components/temper/sensor.py | 4 ++-- homeassistant/components/thermoworks_smoke/sensor.py | 4 ++-- homeassistant/components/thinkingcleaner/sensor.py | 4 ++-- homeassistant/components/time_date/sensor.py | 4 ++-- homeassistant/components/tmb/sensor.py | 7 +++++-- homeassistant/components/torque/sensor.py | 7 +++++-- homeassistant/components/transport_nsw/sensor.py | 4 ++-- homeassistant/components/travisci/sensor.py | 4 ++-- homeassistant/components/uk_transport/sensor.py | 7 +++++-- homeassistant/components/vasttrafik/sensor.py | 7 +++++-- homeassistant/components/viaggiatreno/sensor.py | 7 +++++-- homeassistant/components/volkszaehler/sensor.py | 4 ++-- homeassistant/components/vultr/sensor.py | 4 ++-- homeassistant/components/wirelesstag/sensor.py | 4 ++-- homeassistant/components/worldclock/sensor.py | 7 +++++-- homeassistant/components/worldtidesinfo/sensor.py | 7 +++++-- homeassistant/components/worxlandroid/sensor.py | 7 +++++-- homeassistant/components/wsdot/sensor.py | 7 +++++-- homeassistant/components/yandex_transport/sensor.py | 4 ++-- homeassistant/components/zabbix/sensor.py | 7 +++++-- homeassistant/components/zestimate/sensor.py | 7 +++++-- homeassistant/components/zoneminder/sensor.py | 4 ++-- 123 files changed, 448 insertions(+), 247 deletions(-) diff --git a/homeassistant/components/ads/sensor.py b/homeassistant/components/ads/sensor.py index 4bcc8f776df..483fe2cd725 100644 --- a/homeassistant/components/ads/sensor.py +++ b/homeassistant/components/ads/sensor.py @@ -4,7 +4,10 @@ from __future__ import annotations import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import CONF_NAME, CONF_UNIT_OF_MEASUREMENT from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -22,7 +25,7 @@ from . import ( ) DEFAULT_NAME = "ADS sensor" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_ADS_VAR): cv.string, vol.Optional(CONF_ADS_FACTOR): cv.positive_int, diff --git a/homeassistant/components/alpha_vantage/sensor.py b/homeassistant/components/alpha_vantage/sensor.py index dc62a734d42..506cb41659a 100644 --- a/homeassistant/components/alpha_vantage/sensor.py +++ b/homeassistant/components/alpha_vantage/sensor.py @@ -10,7 +10,10 @@ from alpha_vantage.timeseries import TimeSeries import voluptuous as vol from homeassistant.components import persistent_notification -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import CONF_API_KEY, CONF_CURRENCY, CONF_NAME from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -59,7 +62,7 @@ CURRENCY_SCHEMA = vol.Schema( } ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_API_KEY): cv.string, vol.Optional(CONF_FOREIGN_EXCHANGE): vol.All(cv.ensure_list, [CURRENCY_SCHEMA]), diff --git a/homeassistant/components/aqualogic/sensor.py b/homeassistant/components/aqualogic/sensor.py index bdb582826dc..9c2ee9957af 100644 --- a/homeassistant/components/aqualogic/sensor.py +++ b/homeassistant/components/aqualogic/sensor.py @@ -7,7 +7,7 @@ from dataclasses import dataclass import voluptuous as vol from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, SensorEntityDescription, @@ -104,7 +104,7 @@ SENSOR_TYPES: tuple[AquaLogicSensorEntityDescription, ...] = ( SENSOR_KEYS: list[str] = [desc.key for desc in SENSOR_TYPES] -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_MONITORED_CONDITIONS, default=SENSOR_KEYS): vol.All( cv.ensure_list, [vol.In(SENSOR_KEYS)] diff --git a/homeassistant/components/arest/sensor.py b/homeassistant/components/arest/sensor.py index 917b255ef14..ab502fa275a 100644 --- a/homeassistant/components/arest/sensor.py +++ b/homeassistant/components/arest/sensor.py @@ -9,7 +9,10 @@ import logging import requests import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import ( CONF_MONITORED_VARIABLES, CONF_NAME, @@ -41,7 +44,7 @@ PIN_VARIABLE_SCHEMA = vol.Schema( } ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_RESOURCE): cv.url, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/atome/sensor.py b/homeassistant/components/atome/sensor.py index 84751b84855..fd8250e899f 100644 --- a/homeassistant/components/atome/sensor.py +++ b/homeassistant/components/atome/sensor.py @@ -9,7 +9,7 @@ from pyatome.client import AtomeClient, PyAtomeError import voluptuous as vol from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, SensorStateClass, @@ -49,7 +49,7 @@ WEEKLY_TYPE = "week" MONTHLY_TYPE = "month" YEARLY_TYPE = "year" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, diff --git a/homeassistant/components/bbox/sensor.py b/homeassistant/components/bbox/sensor.py index 858ad6c6e47..72fa870efbf 100644 --- a/homeassistant/components/bbox/sensor.py +++ b/homeassistant/components/bbox/sensor.py @@ -10,7 +10,7 @@ import requests import voluptuous as vol from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, SensorEntityDescription, @@ -80,7 +80,7 @@ SENSOR_TYPES_UPTIME: tuple[SensorEntityDescription, ...] = ( SENSOR_KEYS: list[str] = [desc.key for desc in (*SENSOR_TYPES, *SENSOR_TYPES_UPTIME)] -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_MONITORED_VARIABLES): vol.All( cv.ensure_list, [vol.In(SENSOR_KEYS)] diff --git a/homeassistant/components/beewi_smartclim/sensor.py b/homeassistant/components/beewi_smartclim/sensor.py index 3aaf4daaa80..1c80f62e64f 100644 --- a/homeassistant/components/beewi_smartclim/sensor.py +++ b/homeassistant/components/beewi_smartclim/sensor.py @@ -6,7 +6,7 @@ from beewi_smartclim import BeewiSmartClimPoller import voluptuous as vol from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, ) @@ -26,7 +26,7 @@ SENSOR_TYPES = [ [SensorDeviceClass.BATTERY, "Battery", PERCENTAGE], ] -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_MAC): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/bitcoin/sensor.py b/homeassistant/components/bitcoin/sensor.py index e003362ac7e..e4da2ddc2f4 100644 --- a/homeassistant/components/bitcoin/sensor.py +++ b/homeassistant/components/bitcoin/sensor.py @@ -9,7 +9,7 @@ from blockchain import exchangerates, statistics import voluptuous as vol from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, SensorEntity, SensorEntityDescription, ) @@ -127,7 +127,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( OPTION_KEYS = [desc.key for desc in SENSOR_TYPES] -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_DISPLAY_OPTIONS, default=[]): vol.All( cv.ensure_list, [vol.In(OPTION_KEYS)] diff --git a/homeassistant/components/bizkaibus/sensor.py b/homeassistant/components/bizkaibus/sensor.py index ff7d28b96c7..3efddf0b0d7 100644 --- a/homeassistant/components/bizkaibus/sensor.py +++ b/homeassistant/components/bizkaibus/sensor.py @@ -7,7 +7,10 @@ from contextlib import suppress from bizkaibus.bizkaibus import BizkaibusData import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import CONF_NAME, UnitOfTime from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -21,7 +24,7 @@ CONF_ROUTE = "route" DEFAULT_NAME = "Next bus" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_STOP_ID): cv.string, vol.Required(CONF_ROUTE): cv.string, diff --git a/homeassistant/components/blockchain/sensor.py b/homeassistant/components/blockchain/sensor.py index dafd47bcb20..8ae091fa95e 100644 --- a/homeassistant/components/blockchain/sensor.py +++ b/homeassistant/components/blockchain/sensor.py @@ -8,7 +8,10 @@ import logging from pyblockchain import get_balance, validate_address import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -23,7 +26,7 @@ DEFAULT_NAME = "Bitcoin Balance" SCAN_INTERVAL = timedelta(minutes=5) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_ADDRESSES): [cv.string], vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/bloomsky/sensor.py b/homeassistant/components/bloomsky/sensor.py index 1f63b4a7256..6d99506bd44 100644 --- a/homeassistant/components/bloomsky/sensor.py +++ b/homeassistant/components/bloomsky/sensor.py @@ -5,7 +5,7 @@ from __future__ import annotations import voluptuous as vol from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, ) @@ -63,7 +63,7 @@ SENSOR_DEVICE_CLASS = { # Which sensors to format numerically FORMAT_NUMBERS = ["Temperature", "Pressure", "Voltage"] -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_MONITORED_CONDITIONS, default=SENSOR_TYPES): vol.All( cv.ensure_list, [vol.In(SENSOR_TYPES)] diff --git a/homeassistant/components/cert_expiry/sensor.py b/homeassistant/components/cert_expiry/sensor.py index 674f7bb6341..f52ff8a40d8 100644 --- a/homeassistant/components/cert_expiry/sensor.py +++ b/homeassistant/components/cert_expiry/sensor.py @@ -8,7 +8,7 @@ from typing import Any import voluptuous as vol from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, ) @@ -27,7 +27,7 @@ from .const import DEFAULT_PORT, DOMAIN SCAN_INTERVAL = timedelta(hours=12) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, diff --git a/homeassistant/components/comed_hourly_pricing/sensor.py b/homeassistant/components/comed_hourly_pricing/sensor.py index 770866aa319..b47255828e8 100644 --- a/homeassistant/components/comed_hourly_pricing/sensor.py +++ b/homeassistant/components/comed_hourly_pricing/sensor.py @@ -11,7 +11,7 @@ import aiohttp import voluptuous as vol from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, SensorEntity, SensorEntityDescription, ) @@ -57,7 +57,7 @@ SENSORS_SCHEMA = vol.Schema( } ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( {vol.Required(CONF_MONITORED_FEEDS): [SENSORS_SCHEMA]} ) diff --git a/homeassistant/components/comfoconnect/sensor.py b/homeassistant/components/comfoconnect/sensor.py index 97cb7fc61eb..25726b3789b 100644 --- a/homeassistant/components/comfoconnect/sensor.py +++ b/homeassistant/components/comfoconnect/sensor.py @@ -31,7 +31,7 @@ from pycomfoconnect import ( import voluptuous as vol from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, SensorEntityDescription, @@ -263,7 +263,7 @@ SENSOR_TYPES = ( ), ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_RESOURCES, default=[]): vol.All( cv.ensure_list, [vol.In([desc.key for desc in SENSOR_TYPES])] diff --git a/homeassistant/components/cups/sensor.py b/homeassistant/components/cups/sensor.py index 647deee79a6..7f45e99f93d 100644 --- a/homeassistant/components/cups/sensor.py +++ b/homeassistant/components/cups/sensor.py @@ -9,7 +9,10 @@ from typing import Any import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import CONF_HOST, CONF_PORT, PERCENTAGE from homeassistant.core import HomeAssistant from homeassistant.exceptions import PlatformNotReady @@ -47,7 +50,7 @@ SCAN_INTERVAL = timedelta(minutes=1) PRINTER_STATES = {3: "idle", 4: "printing", 5: "stopped"} -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_PRINTERS): vol.All(cv.ensure_list, [cv.string]), vol.Optional(CONF_IS_CUPS_SERVER, default=DEFAULT_IS_CUPS_SERVER): cv.boolean, diff --git a/homeassistant/components/currencylayer/sensor.py b/homeassistant/components/currencylayer/sensor.py index 2fdf521ad9f..2ad0f88a2ab 100644 --- a/homeassistant/components/currencylayer/sensor.py +++ b/homeassistant/components/currencylayer/sensor.py @@ -8,7 +8,10 @@ import logging import requests import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import CONF_API_KEY, CONF_BASE, CONF_NAME, CONF_QUOTE from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -24,7 +27,7 @@ DEFAULT_NAME = "CurrencyLayer Sensor" SCAN_INTERVAL = timedelta(hours=4) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_API_KEY): cv.string, vol.Required(CONF_QUOTE): vol.All(cv.ensure_list, [cv.string]), diff --git a/homeassistant/components/delijn/sensor.py b/homeassistant/components/delijn/sensor.py index 5693a00e857..017a4c5b2fa 100644 --- a/homeassistant/components/delijn/sensor.py +++ b/homeassistant/components/delijn/sensor.py @@ -10,7 +10,7 @@ from pydelijn.common import HttpException import voluptuous as vol from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, ) @@ -31,7 +31,7 @@ CONF_NUMBER_OF_DEPARTURES = "number_of_departures" DEFAULT_NAME = "De Lijn" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_API_KEY): cv.string, vol.Required(CONF_NEXT_DEPARTURE): [ diff --git a/homeassistant/components/derivative/sensor.py b/homeassistant/components/derivative/sensor.py index fd430c6ef4d..36719b43ccb 100644 --- a/homeassistant/components/derivative/sensor.py +++ b/homeassistant/components/derivative/sensor.py @@ -9,7 +9,11 @@ from typing import TYPE_CHECKING import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, RestoreSensor, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + RestoreSensor, + SensorEntity, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, @@ -62,7 +66,7 @@ UNIT_TIME = { DEFAULT_ROUND = 3 DEFAULT_TIME_WINDOW = 0 -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_NAME): cv.string, vol.Required(CONF_SOURCE): cv.entity_id, diff --git a/homeassistant/components/discogs/sensor.py b/homeassistant/components/discogs/sensor.py index 4a732130485..3cea6ec4dac 100644 --- a/homeassistant/components/discogs/sensor.py +++ b/homeassistant/components/discogs/sensor.py @@ -10,7 +10,7 @@ import discogs_client import voluptuous as vol from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, SensorEntity, SensorEntityDescription, ) @@ -58,7 +58,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( ) SENSOR_KEYS: list[str] = [desc.key for desc in SENSOR_TYPES] -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_TOKEN): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/dovado/sensor.py b/homeassistant/components/dovado/sensor.py index bd53fb22ad2..013b51bfc8f 100644 --- a/homeassistant/components/dovado/sensor.py +++ b/homeassistant/components/dovado/sensor.py @@ -9,7 +9,7 @@ import re import voluptuous as vol from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, SensorEntityDescription, @@ -78,7 +78,7 @@ SENSOR_TYPES: tuple[DovadoSensorEntityDescription, ...] = ( SENSOR_KEYS: list[str] = [desc.key for desc in SENSOR_TYPES] -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( {vol.Required(CONF_SENSORS): vol.All(cv.ensure_list, [vol.In(SENSOR_KEYS)])} ) diff --git a/homeassistant/components/dte_energy_bridge/sensor.py b/homeassistant/components/dte_energy_bridge/sensor.py index 112ebd55f94..a0b9253034e 100644 --- a/homeassistant/components/dte_energy_bridge/sensor.py +++ b/homeassistant/components/dte_energy_bridge/sensor.py @@ -9,7 +9,7 @@ import requests import voluptuous as vol from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, SensorStateClass, @@ -30,7 +30,7 @@ DEFAULT_NAME = "Current Energy Usage" DEFAULT_VERSION = 1 DOMAIN = "dte_energy_bridge" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_IP_ADDRESS): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/dublin_bus_transport/sensor.py b/homeassistant/components/dublin_bus_transport/sensor.py index 3f9c57456f8..91773d08142 100644 --- a/homeassistant/components/dublin_bus_transport/sensor.py +++ b/homeassistant/components/dublin_bus_transport/sensor.py @@ -13,7 +13,10 @@ from http import HTTPStatus import requests import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import CONF_NAME, UnitOfTime from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -38,7 +41,7 @@ DEFAULT_NAME = "Next Bus" SCAN_INTERVAL = timedelta(minutes=1) TIME_STR_FORMAT = "%H:%M" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_STOP_ID): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/dweet/sensor.py b/homeassistant/components/dweet/sensor.py index 79e25bec0c1..01e0567ac8d 100644 --- a/homeassistant/components/dweet/sensor.py +++ b/homeassistant/components/dweet/sensor.py @@ -9,7 +9,10 @@ import logging import dweepy import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import ( CONF_DEVICE, CONF_NAME, @@ -27,7 +30,7 @@ DEFAULT_NAME = "Dweet.io Sensor" SCAN_INTERVAL = timedelta(minutes=1) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_DEVICE): cv.string, vol.Required(CONF_VALUE_TEMPLATE): cv.template, diff --git a/homeassistant/components/ebox/sensor.py b/homeassistant/components/ebox/sensor.py index aff154cca02..691e9dd8275 100644 --- a/homeassistant/components/ebox/sensor.py +++ b/homeassistant/components/ebox/sensor.py @@ -13,7 +13,7 @@ from pyebox.client import PyEboxError import voluptuous as vol from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, SensorEntityDescription, @@ -139,7 +139,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SENSOR_TYPE_KEYS: list[str] = [desc.key for desc in SENSOR_TYPES] -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_MONITORED_VARIABLES): vol.All( cv.ensure_list, [vol.In(SENSOR_TYPE_KEYS)] diff --git a/homeassistant/components/eddystone_temperature/sensor.py b/homeassistant/components/eddystone_temperature/sensor.py index b136b193686..637beffcf94 100644 --- a/homeassistant/components/eddystone_temperature/sensor.py +++ b/homeassistant/components/eddystone_temperature/sensor.py @@ -12,7 +12,7 @@ from beacontools import BeaconScanner, EddystoneFilter, EddystoneTLMFrame import voluptuous as vol from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, ) @@ -43,7 +43,7 @@ BEACON_SCHEMA = vol.Schema( } ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_BT_DEVICE_ID, default=0): cv.positive_int, vol.Required(CONF_BEACONS): vol.Schema({cv.string: BEACON_SCHEMA}), diff --git a/homeassistant/components/eliqonline/sensor.py b/homeassistant/components/eliqonline/sensor.py index 2aa0ab15746..7c9f76824e8 100644 --- a/homeassistant/components/eliqonline/sensor.py +++ b/homeassistant/components/eliqonline/sensor.py @@ -9,7 +9,7 @@ import eliqonline import voluptuous as vol from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, SensorStateClass, @@ -29,7 +29,7 @@ DEFAULT_NAME = "ELIQ Online" SCAN_INTERVAL = timedelta(seconds=60) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_ACCESS_TOKEN): cv.string, vol.Required(CONF_CHANNEL_ID): cv.positive_int, diff --git a/homeassistant/components/emoncms/sensor.py b/homeassistant/components/emoncms/sensor.py index 97c69619fa9..e239ffd6c21 100644 --- a/homeassistant/components/emoncms/sensor.py +++ b/homeassistant/components/emoncms/sensor.py @@ -10,7 +10,7 @@ from pyemoncms import EmoncmsClient import voluptuous as vol from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, SensorStateClass, @@ -54,7 +54,7 @@ DEFAULT_UNIT = UnitOfPower.WATT ONLY_INCL_EXCL_NONE = "only_include_exclude_or_none" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_API_KEY): cv.string, vol.Required(CONF_URL): cv.string, diff --git a/homeassistant/components/enocean/sensor.py b/homeassistant/components/enocean/sensor.py index c22a7d95760..177c95c2832 100644 --- a/homeassistant/components/enocean/sensor.py +++ b/homeassistant/components/enocean/sensor.py @@ -9,7 +9,7 @@ from enocean.utils import combine_hex import voluptuous as vol from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, RestoreSensor, SensorDeviceClass, SensorEntityDescription, @@ -87,7 +87,7 @@ SENSOR_DESC_WINDOWHANDLE = EnOceanSensorEntityDescription( ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_ID): vol.All(cv.ensure_list, [vol.Coerce(int)]), vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/entur_public_transport/sensor.py b/homeassistant/components/entur_public_transport/sensor.py index 70b86d0271f..f88bb99cea0 100644 --- a/homeassistant/components/entur_public_transport/sensor.py +++ b/homeassistant/components/entur_public_transport/sensor.py @@ -8,7 +8,10 @@ from random import randint from enturclient import EnturPublicTransportData import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import ( CONF_LATITUDE, CONF_LONGITUDE, @@ -46,7 +49,7 @@ ICONS = { SCAN_INTERVAL = timedelta(seconds=45) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_STOP_IDS): vol.All(cv.ensure_list, [cv.string]), vol.Optional(CONF_EXPAND_PLATFORMS, default=True): cv.boolean, diff --git a/homeassistant/components/etherscan/sensor.py b/homeassistant/components/etherscan/sensor.py index 38219bf659b..e64b596a119 100644 --- a/homeassistant/components/etherscan/sensor.py +++ b/homeassistant/components/etherscan/sensor.py @@ -7,7 +7,10 @@ from datetime import timedelta from pyetherscan import get_balance import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import CONF_ADDRESS, CONF_NAME, CONF_TOKEN from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -18,7 +21,7 @@ CONF_TOKEN_ADDRESS = "token_address" SCAN_INTERVAL = timedelta(minutes=5) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_ADDRESS): cv.string, vol.Optional(CONF_NAME): cv.string, diff --git a/homeassistant/components/fail2ban/sensor.py b/homeassistant/components/fail2ban/sensor.py index 53490e60c54..9e6d23556d2 100644 --- a/homeassistant/components/fail2ban/sensor.py +++ b/homeassistant/components/fail2ban/sensor.py @@ -9,7 +9,10 @@ import re import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import CONF_FILE_PATH, CONF_NAME from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -27,7 +30,7 @@ STATE_CURRENT_BANS = "current_bans" STATE_ALL_BANS = "total_bans" SCAN_INTERVAL = timedelta(seconds=120) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_JAILS): vol.All(cv.ensure_list, vol.Length(min=1)), vol.Optional(CONF_FILE_PATH): cv.isfile, diff --git a/homeassistant/components/fido/sensor.py b/homeassistant/components/fido/sensor.py index d2169ae32e8..bc6e6340111 100644 --- a/homeassistant/components/fido/sensor.py +++ b/homeassistant/components/fido/sensor.py @@ -14,7 +14,7 @@ from pyfido.client import PyFidoError import voluptuous as vol from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, SensorEntityDescription, @@ -172,7 +172,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SENSOR_KEYS: list[str] = [desc.key for desc in SENSOR_TYPES] -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_MONITORED_VARIABLES): vol.All( cv.ensure_list, [vol.In(SENSOR_KEYS)] diff --git a/homeassistant/components/file/sensor.py b/homeassistant/components/file/sensor.py index fa04ae7c62a..fda0d14a6aa 100644 --- a/homeassistant/components/file/sensor.py +++ b/homeassistant/components/file/sensor.py @@ -8,7 +8,10 @@ import os from file_read_backwards import FileReadBackwards import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_FILE_PATH, @@ -26,7 +29,7 @@ from .const import DEFAULT_NAME, FILE_ICON _LOGGER = logging.getLogger(__name__) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_FILE_PATH): cv.isfile, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/filter/sensor.py b/homeassistant/components/filter/sensor.py index decb1f0a33f..549d74ffd09 100644 --- a/homeassistant/components/filter/sensor.py +++ b/homeassistant/components/filter/sensor.py @@ -20,7 +20,7 @@ from homeassistant.components.recorder import get_instance, history from homeassistant.components.sensor import ( ATTR_STATE_CLASS, DOMAIN as SENSOR_DOMAIN, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, ) @@ -150,7 +150,7 @@ FILTER_TIME_THROTTLE_SCHEMA = FILTER_SCHEMA.extend( } ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_ENTITY_ID): vol.Any( cv.entity_domain(SENSOR_DOMAIN), diff --git a/homeassistant/components/fints/sensor.py b/homeassistant/components/fints/sensor.py index 4a4c2d05181..2f47fdc09eb 100644 --- a/homeassistant/components/fints/sensor.py +++ b/homeassistant/components/fints/sensor.py @@ -12,7 +12,10 @@ from fints.client import FinTS3PinTanClient from fints.models import SEPAAccount import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import CONF_NAME, CONF_PIN, CONF_URL, CONF_USERNAME from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -43,7 +46,7 @@ SCHEMA_ACCOUNTS = vol.Schema( } ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_BIN): cv.string, vol.Required(CONF_USERNAME): cv.string, diff --git a/homeassistant/components/fitbit/sensor.py b/homeassistant/components/fitbit/sensor.py index 6df4968739f..ab9a593e195 100644 --- a/homeassistant/components/fitbit/sensor.py +++ b/homeassistant/components/fitbit/sensor.py @@ -18,7 +18,7 @@ from homeassistant.components.application_credentials import ( async_import_client_credential, ) from homeassistant.components.sensor import ( - PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, SensorEntityDescription, @@ -538,7 +538,7 @@ FITBIT_RESOURCES_KEYS: Final[list[str]] = [ for desc in (*FITBIT_RESOURCES_LIST, FITBIT_RESOURCE_BATTERY, SLEEP_START_TIME) ] -PLATFORM_SCHEMA: Final = PARENT_PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA: Final = SENSOR_PLATFORM_SCHEMA.extend( { vol.Optional( CONF_MONITORED_RESOURCES, default=FITBIT_DEFAULT_RESOURCES diff --git a/homeassistant/components/fixer/sensor.py b/homeassistant/components/fixer/sensor.py index 10f05ca29f8..4a03de5d6de 100644 --- a/homeassistant/components/fixer/sensor.py +++ b/homeassistant/components/fixer/sensor.py @@ -9,7 +9,10 @@ from fixerio import Fixerio from fixerio.exceptions import FixerioException import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import CONF_API_KEY, CONF_NAME, CONF_TARGET from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -27,7 +30,7 @@ DEFAULT_NAME = "Exchange rate" SCAN_INTERVAL = timedelta(days=1) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_API_KEY): cv.string, vol.Required(CONF_TARGET): cv.string, diff --git a/homeassistant/components/folder/sensor.py b/homeassistant/components/folder/sensor.py index 6c8e4fc63a9..3a8a4fdc380 100644 --- a/homeassistant/components/folder/sensor.py +++ b/homeassistant/components/folder/sensor.py @@ -10,7 +10,7 @@ import os import voluptuous as vol from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, ) @@ -28,7 +28,7 @@ DEFAULT_FILTER = "*" SCAN_INTERVAL = timedelta(minutes=1) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_FOLDER_PATHS): cv.isdir, vol.Optional(CONF_FILTER, default=DEFAULT_FILTER): cv.string, diff --git a/homeassistant/components/geo_rss_events/sensor.py b/homeassistant/components/geo_rss_events/sensor.py index 8c704bcf16a..0dc8918b7dd 100644 --- a/homeassistant/components/geo_rss_events/sensor.py +++ b/homeassistant/components/geo_rss_events/sensor.py @@ -14,7 +14,10 @@ from georss_client import UPDATE_OK, UPDATE_OK_NO_DATA from georss_generic_client import GenericFeed import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import ( CONF_LATITUDE, CONF_LONGITUDE, @@ -46,7 +49,7 @@ DOMAIN = "geo_rss_events" SCAN_INTERVAL = timedelta(minutes=5) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_URL): cv.string, vol.Optional(CONF_LATITUDE): cv.latitude, diff --git a/homeassistant/components/gitlab_ci/sensor.py b/homeassistant/components/gitlab_ci/sensor.py index d247ef5af60..6ed3112b2af 100644 --- a/homeassistant/components/gitlab_ci/sensor.py +++ b/homeassistant/components/gitlab_ci/sensor.py @@ -8,7 +8,10 @@ import logging from gitlab import Gitlab, GitlabAuthenticationError, GitlabGetError import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import CONF_NAME, CONF_SCAN_INTERVAL, CONF_TOKEN, CONF_URL from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -39,7 +42,7 @@ ICON_SAD = "mdi:emoticon-sad" SCAN_INTERVAL = timedelta(seconds=300) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_GITLAB_ID): cv.string, vol.Required(CONF_TOKEN): cv.string, diff --git a/homeassistant/components/gitter/sensor.py b/homeassistant/components/gitter/sensor.py index 056c275c785..bc444655908 100644 --- a/homeassistant/components/gitter/sensor.py +++ b/homeassistant/components/gitter/sensor.py @@ -8,7 +8,10 @@ from gitterpy.client import GitterClient from gitterpy.errors import GitterRoomError, GitterTokenError import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import CONF_API_KEY, CONF_NAME, CONF_ROOM from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -25,7 +28,7 @@ DEFAULT_NAME = "Gitter messages" DEFAULT_ROOM = "home-assistant/home-assistant" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_API_KEY): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/google_wifi/sensor.py b/homeassistant/components/google_wifi/sensor.py index 776fb44a51b..3dd421d99da 100644 --- a/homeassistant/components/google_wifi/sensor.py +++ b/homeassistant/components/google_wifi/sensor.py @@ -10,7 +10,7 @@ import requests import voluptuous as vol from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, SensorEntity, SensorEntityDescription, ) @@ -93,7 +93,7 @@ SENSOR_TYPES: tuple[GoogleWifiSensorEntityDescription, ...] = ( SENSOR_KEYS: list[str] = [desc.key for desc in SENSOR_TYPES] -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, vol.Optional(CONF_MONITORED_CONDITIONS, default=SENSOR_KEYS): vol.All( diff --git a/homeassistant/components/gpsd/sensor.py b/homeassistant/components/gpsd/sensor.py index bc08b7b6203..5a978f9f66e 100644 --- a/homeassistant/components/gpsd/sensor.py +++ b/homeassistant/components/gpsd/sensor.py @@ -15,7 +15,7 @@ from gps3.agps3threaded import ( import voluptuous as vol from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, SensorEntityDescription, @@ -70,7 +70,7 @@ SENSOR_TYPES: tuple[GpsdSensorDescription, ...] = ( ), ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, diff --git a/homeassistant/components/group/sensor.py b/homeassistant/components/group/sensor.py index 2e6c321be1e..eaaedcf0e46 100644 --- a/homeassistant/components/group/sensor.py +++ b/homeassistant/components/group/sensor.py @@ -17,7 +17,7 @@ from homeassistant.components.sensor import ( DEVICE_CLASS_UNITS, DEVICE_CLASSES_SCHEMA, DOMAIN, - PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, STATE_CLASSES_SCHEMA, UNIT_CONVERTERS, SensorDeviceClass, @@ -93,7 +93,7 @@ SENSOR_TYPE_TO_ATTR = {v: k for k, v in SENSOR_TYPES.items()} # No limit on parallel updates to enable a group calling another group PARALLEL_UPDATES = 0 -PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_ENTITIES): cv.entities_domain( [DOMAIN, NUMBER_DOMAIN, INPUT_NUMBER_DOMAIN] diff --git a/homeassistant/components/haveibeenpwned/sensor.py b/homeassistant/components/haveibeenpwned/sensor.py index 9933ba11945..1aebe696e82 100644 --- a/homeassistant/components/haveibeenpwned/sensor.py +++ b/homeassistant/components/haveibeenpwned/sensor.py @@ -9,7 +9,10 @@ import logging import requests import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import CONF_API_KEY, CONF_EMAIL from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -30,7 +33,7 @@ MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=15) URL = "https://haveibeenpwned.com/api/v3/breachedaccount/" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_EMAIL): vol.All(cv.ensure_list, [cv.string]), vol.Required(CONF_API_KEY): cv.string, diff --git a/homeassistant/components/hddtemp/sensor.py b/homeassistant/components/hddtemp/sensor.py index 3dda9f44004..836e68abe9f 100644 --- a/homeassistant/components/hddtemp/sensor.py +++ b/homeassistant/components/hddtemp/sensor.py @@ -10,7 +10,7 @@ from telnetlib import Telnet # pylint: disable=deprecated-module import voluptuous as vol from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, ) @@ -38,7 +38,7 @@ DEFAULT_TIMEOUT = 5 SCAN_INTERVAL = timedelta(minutes=1) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_DISKS, default=[]): vol.All(cv.ensure_list, [cv.string]), vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, diff --git a/homeassistant/components/hp_ilo/sensor.py b/homeassistant/components/hp_ilo/sensor.py index 8d29b20381d..85908a45af4 100644 --- a/homeassistant/components/hp_ilo/sensor.py +++ b/homeassistant/components/hp_ilo/sensor.py @@ -8,7 +8,10 @@ import logging import hpilo import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import ( CONF_HOST, CONF_MONITORED_VARIABLES, @@ -47,7 +50,7 @@ SENSOR_TYPES = { "network_settings": ["Network Settings", "get_network_settings"], } -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Required(CONF_USERNAME): cv.string, diff --git a/homeassistant/components/iammeter/sensor.py b/homeassistant/components/iammeter/sensor.py index a3922b06980..1069c6696fc 100644 --- a/homeassistant/components/iammeter/sensor.py +++ b/homeassistant/components/iammeter/sensor.py @@ -12,7 +12,7 @@ from iammeter.client import IamMeter import voluptuous as vol from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, SensorEntityDescription, @@ -46,7 +46,7 @@ _LOGGER = logging.getLogger(__name__) DEFAULT_PORT = 80 DEFAULT_DEVICE_NAME = "IamMeter" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_DEVICE_NAME): cv.string, diff --git a/homeassistant/components/irish_rail_transport/sensor.py b/homeassistant/components/irish_rail_transport/sensor.py index b0ad9372f86..a96846558fa 100644 --- a/homeassistant/components/irish_rail_transport/sensor.py +++ b/homeassistant/components/irish_rail_transport/sensor.py @@ -7,7 +7,10 @@ from datetime import timedelta from pyirishrail.pyirishrail import IrishRailRTPI import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import CONF_NAME, UnitOfTime from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -36,7 +39,7 @@ DEFAULT_NAME = "Next Train" SCAN_INTERVAL = timedelta(minutes=2) TIME_STR_FORMAT = "%H:%M" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_STATION): cv.string, vol.Optional(CONF_DIRECTION): cv.string, diff --git a/homeassistant/components/kwb/sensor.py b/homeassistant/components/kwb/sensor.py index e55b90cf89f..dbe57f9a517 100644 --- a/homeassistant/components/kwb/sensor.py +++ b/homeassistant/components/kwb/sensor.py @@ -5,7 +5,10 @@ from __future__ import annotations from pykwb import kwb import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import ( CONF_DEVICE, CONF_HOST, @@ -27,7 +30,7 @@ MODE_TCP = 1 CONF_RAW = "raw" -SERIAL_SCHEMA = PLATFORM_SCHEMA.extend( +SERIAL_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_RAW, default=DEFAULT_RAW): cv.boolean, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, @@ -36,7 +39,7 @@ SERIAL_SCHEMA = PLATFORM_SCHEMA.extend( } ) -ETHERNET_SCHEMA = PLATFORM_SCHEMA.extend( +ETHERNET_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_RAW, default=DEFAULT_RAW): cv.boolean, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/lacrosse/sensor.py b/homeassistant/components/lacrosse/sensor.py index c059248b422..d7df7a08e76 100644 --- a/homeassistant/components/lacrosse/sensor.py +++ b/homeassistant/components/lacrosse/sensor.py @@ -12,7 +12,7 @@ import voluptuous as vol from homeassistant.components.sensor import ( ENTITY_ID_FORMAT, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, SensorStateClass, @@ -60,7 +60,7 @@ SENSOR_SCHEMA = vol.Schema( } ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_SENSORS): cv.schema_with_slug_keys(SENSOR_SCHEMA), vol.Optional(CONF_BAUD, default=DEFAULT_BAUD): cv.positive_int, diff --git a/homeassistant/components/linux_battery/sensor.py b/homeassistant/components/linux_battery/sensor.py index 9dc0e8c675d..789195e1169 100644 --- a/homeassistant/components/linux_battery/sensor.py +++ b/homeassistant/components/linux_battery/sensor.py @@ -9,7 +9,7 @@ from batinfo import Batteries import voluptuous as vol from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, ) @@ -49,7 +49,7 @@ DEFAULT_SYSTEM = "linux" SYSTEMS = ["android", "linux"] -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_BATTERY, default=DEFAULT_BATTERY): cv.positive_int, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/london_air/sensor.py b/homeassistant/components/london_air/sensor.py index 0895e507a85..c7b7abd51ed 100644 --- a/homeassistant/components/london_air/sensor.py +++ b/homeassistant/components/london_air/sensor.py @@ -9,7 +9,10 @@ import logging import requests import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -54,7 +57,7 @@ AUTHORITIES = [ URL = "http://api.erg.kcl.ac.uk/AirQuality/Hourly/MonitoringIndex/GroupName=London/Json" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_LOCATIONS, default=AUTHORITIES): vol.All( cv.ensure_list, [vol.In(AUTHORITIES)] diff --git a/homeassistant/components/london_underground/sensor.py b/homeassistant/components/london_underground/sensor.py index e5735aa7fba..015f7e8ecdc 100644 --- a/homeassistant/components/london_underground/sensor.py +++ b/homeassistant/components/london_underground/sensor.py @@ -8,7 +8,10 @@ from typing import Any from london_tube_status import TubeData import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.core import HomeAssistant from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -22,7 +25,7 @@ from .coordinator import LondonTubeCoordinator _LOGGER = logging.getLogger(__name__) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( {vol.Required(CONF_LINE): vol.All(cv.ensure_list, [vol.In(list(TUBE_LINES))])} ) diff --git a/homeassistant/components/mfi/sensor.py b/homeassistant/components/mfi/sensor.py index afa5e00bf02..b93cc669e62 100644 --- a/homeassistant/components/mfi/sensor.py +++ b/homeassistant/components/mfi/sensor.py @@ -9,7 +9,7 @@ import requests import voluptuous as vol from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, ) @@ -45,7 +45,7 @@ SENSOR_MODELS = [ "Input Digital", ] -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Required(CONF_USERNAME): cv.string, diff --git a/homeassistant/components/min_max/sensor.py b/homeassistant/components/min_max/sensor.py index f34067fea2e..89252a58864 100644 --- a/homeassistant/components/min_max/sensor.py +++ b/homeassistant/components/min_max/sensor.py @@ -10,7 +10,7 @@ from typing import Any import voluptuous as vol from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, SensorEntity, SensorStateClass, ) @@ -59,7 +59,7 @@ SENSOR_TYPES = { } SENSOR_TYPE_TO_ATTR = {v: k for k, v in SENSOR_TYPES.items()} -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_TYPE, default=SENSOR_TYPES[ATTR_MAX_VALUE]): vol.All( cv.string, vol.In(SENSOR_TYPES.values()) diff --git a/homeassistant/components/mold_indicator/sensor.py b/homeassistant/components/mold_indicator/sensor.py index cbb531d9672..9064e0387e5 100644 --- a/homeassistant/components/mold_indicator/sensor.py +++ b/homeassistant/components/mold_indicator/sensor.py @@ -8,7 +8,10 @@ import math import voluptuous as vol from homeassistant import util -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, CONF_NAME, @@ -46,7 +49,7 @@ DEFAULT_NAME = "Mold Indicator" MAGNUS_K2 = 17.62 MAGNUS_K3 = 243.12 -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_INDOOR_TEMP): cv.entity_id, vol.Required(CONF_OUTDOOR_TEMP): cv.entity_id, diff --git a/homeassistant/components/mqtt_room/sensor.py b/homeassistant/components/mqtt_room/sensor.py index df0be7b4968..849d4562423 100644 --- a/homeassistant/components/mqtt_room/sensor.py +++ b/homeassistant/components/mqtt_room/sensor.py @@ -11,7 +11,10 @@ import voluptuous as vol from homeassistant.components import mqtt from homeassistant.components.mqtt import CONF_STATE_TOPIC -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import ( ATTR_DEVICE_ID, ATTR_ID, @@ -40,7 +43,7 @@ DEFAULT_NAME = "Room Sensor" DEFAULT_TIMEOUT = 5 DEFAULT_TOPIC = "room_presence" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_DEVICE_ID): cv.string, vol.Required(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, diff --git a/homeassistant/components/mvglive/sensor.py b/homeassistant/components/mvglive/sensor.py index 6aefa83d4bb..966bfebb577 100644 --- a/homeassistant/components/mvglive/sensor.py +++ b/homeassistant/components/mvglive/sensor.py @@ -9,7 +9,10 @@ import logging import MVGLive import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import CONF_NAME, UnitOfTime from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -44,7 +47,7 @@ ATTRIBUTION = "Data provided by MVG-live.de" SCAN_INTERVAL = timedelta(seconds=30) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_NEXT_DEPARTURE): [ { diff --git a/homeassistant/components/nederlandse_spoorwegen/sensor.py b/homeassistant/components/nederlandse_spoorwegen/sensor.py index 33828e65019..ce3e7d3a002 100644 --- a/homeassistant/components/nederlandse_spoorwegen/sensor.py +++ b/homeassistant/components/nederlandse_spoorwegen/sensor.py @@ -10,7 +10,10 @@ from ns_api import RequestParametersError import requests import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import CONF_API_KEY, CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.exceptions import PlatformNotReady @@ -42,7 +45,7 @@ ROUTE_SCHEMA = vol.Schema( ROUTES_SCHEMA = vol.All(cv.ensure_list, [ROUTE_SCHEMA]) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( {vol.Required(CONF_API_KEY): cv.string, vol.Optional(CONF_ROUTES): ROUTES_SCHEMA} ) diff --git a/homeassistant/components/netdata/sensor.py b/homeassistant/components/netdata/sensor.py index abbb3bbb6c9..b77a4392ef4 100644 --- a/homeassistant/components/netdata/sensor.py +++ b/homeassistant/components/netdata/sensor.py @@ -8,7 +8,10 @@ from netdata import Netdata from netdata.exceptions import NetdataError import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import ( CONF_HOST, CONF_ICON, @@ -44,7 +47,7 @@ RESOURCE_SCHEMA = vol.Any( } ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/neurio_energy/sensor.py b/homeassistant/components/neurio_energy/sensor.py index 4a7ce43a0d7..a02a37b740d 100644 --- a/homeassistant/components/neurio_energy/sensor.py +++ b/homeassistant/components/neurio_energy/sensor.py @@ -10,7 +10,7 @@ import requests.exceptions import voluptuous as vol from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, SensorStateClass, @@ -38,7 +38,7 @@ DAILY_TYPE = "daily" MIN_TIME_BETWEEN_DAILY_UPDATES = timedelta(seconds=150) MIN_TIME_BETWEEN_ACTIVE_UPDATES = timedelta(seconds=10) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_API_KEY): cv.string, vol.Required(CONF_API_SECRET): cv.string, diff --git a/homeassistant/components/nmbs/sensor.py b/homeassistant/components/nmbs/sensor.py index a684b47e245..82fc6143b2d 100644 --- a/homeassistant/components/nmbs/sensor.py +++ b/homeassistant/components/nmbs/sensor.py @@ -7,7 +7,10 @@ import logging from pyrail import iRail import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import ( ATTR_LATITUDE, ATTR_LONGITUDE, @@ -35,7 +38,7 @@ CONF_STATION_TO = "station_to" CONF_STATION_LIVE = "station_live" CONF_EXCLUDE_VIAS = "exclude_vias" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_STATION_FROM): cv.string, vol.Required(CONF_STATION_TO): cv.string, diff --git a/homeassistant/components/noaa_tides/sensor.py b/homeassistant/components/noaa_tides/sensor.py index 5e213e847ba..b165478927e 100644 --- a/homeassistant/components/noaa_tides/sensor.py +++ b/homeassistant/components/noaa_tides/sensor.py @@ -10,7 +10,10 @@ import noaa_coops as coops import requests import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import CONF_NAME, CONF_TIME_ZONE, CONF_UNIT_SYSTEM from homeassistant.core import HomeAssistant from homeassistant.exceptions import PlatformNotReady @@ -34,7 +37,7 @@ SCAN_INTERVAL = timedelta(minutes=60) TIMEZONES = ["gmt", "lst", "lst_ldt"] UNIT_SYSTEMS = ["english", "metric"] -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_STATION_ID): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/nsw_fuel_station/sensor.py b/homeassistant/components/nsw_fuel_station/sensor.py index 7f28a9d28f2..f99790664da 100644 --- a/homeassistant/components/nsw_fuel_station/sensor.py +++ b/homeassistant/components/nsw_fuel_station/sensor.py @@ -6,7 +6,10 @@ import logging import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import CURRENCY_CENT, UnitOfVolume from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -41,7 +44,7 @@ CONF_ALLOWED_FUEL_TYPES = [ ] CONF_DEFAULT_FUEL_TYPES = ["E10", "U91"] -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_STATION_ID): cv.positive_int, vol.Optional(CONF_FUEL_TYPES, default=CONF_DEFAULT_FUEL_TYPES): vol.All( diff --git a/homeassistant/components/oasa_telematics/sensor.py b/homeassistant/components/oasa_telematics/sensor.py index 2a68c7ce15d..fef4cef48af 100644 --- a/homeassistant/components/oasa_telematics/sensor.py +++ b/homeassistant/components/oasa_telematics/sensor.py @@ -10,7 +10,7 @@ import oasatelematics import voluptuous as vol from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, ) @@ -39,7 +39,7 @@ DEFAULT_NAME = "OASA Telematics" SCAN_INTERVAL = timedelta(seconds=60) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_STOP_ID): cv.string, vol.Required(CONF_ROUTE_ID): cv.string, diff --git a/homeassistant/components/ohmconnect/sensor.py b/homeassistant/components/ohmconnect/sensor.py index 2598e5fe514..b32db33cc2d 100644 --- a/homeassistant/components/ohmconnect/sensor.py +++ b/homeassistant/components/ohmconnect/sensor.py @@ -9,7 +9,10 @@ import defusedxml.ElementTree as ET import requests import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import CONF_ID, CONF_NAME from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -23,7 +26,7 @@ DEFAULT_NAME = "OhmConnect Status" MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=1) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_ID): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/openevse/sensor.py b/homeassistant/components/openevse/sensor.py index b2360b13a6f..c228b6c1a14 100644 --- a/homeassistant/components/openevse/sensor.py +++ b/homeassistant/components/openevse/sensor.py @@ -9,7 +9,7 @@ from requests import RequestException import voluptuous as vol from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, SensorEntityDescription, @@ -80,7 +80,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SENSOR_KEYS: list[str] = [desc.key for desc in SENSOR_TYPES] -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_MONITORED_VARIABLES, default=["status"]): vol.All( diff --git a/homeassistant/components/openhardwaremonitor/sensor.py b/homeassistant/components/openhardwaremonitor/sensor.py index 4e15ca3dd57..4ef71a6c75f 100644 --- a/homeassistant/components/openhardwaremonitor/sensor.py +++ b/homeassistant/components/openhardwaremonitor/sensor.py @@ -8,7 +8,10 @@ import logging import requests import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import CONF_HOST, CONF_PORT from homeassistant.core import HomeAssistant from homeassistant.exceptions import PlatformNotReady @@ -36,7 +39,7 @@ OHM_MAX = "Max" OHM_CHILDREN = "Children" OHM_NAME = "Text" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( {vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_PORT, default=8085): cv.port} ) diff --git a/homeassistant/components/oru/sensor.py b/homeassistant/components/oru/sensor.py index b1d814dd98a..213350db6a4 100644 --- a/homeassistant/components/oru/sensor.py +++ b/homeassistant/components/oru/sensor.py @@ -9,7 +9,7 @@ from oru import Meter, MeterError import voluptuous as vol from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, ) @@ -25,7 +25,9 @@ CONF_METER_NUMBER = "meter_number" SCAN_INTERVAL = timedelta(minutes=15) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({vol.Required(CONF_METER_NUMBER): cv.string}) +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( + {vol.Required(CONF_METER_NUMBER): cv.string} +) def setup_platform( diff --git a/homeassistant/components/otp/sensor.py b/homeassistant/components/otp/sensor.py index 466fc994cdb..2e166859729 100644 --- a/homeassistant/components/otp/sensor.py +++ b/homeassistant/components/otp/sensor.py @@ -7,7 +7,10 @@ import time import pyotp import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_NAME, CONF_TOKEN from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant, callback @@ -21,7 +24,7 @@ from .const import DEFAULT_NAME, DOMAIN TIME_STEP = 30 # Default time step assumed by Google Authenticator -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_TOKEN): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/pilight/sensor.py b/homeassistant/components/pilight/sensor.py index 003e3428bdd..8e5f3b7d78a 100644 --- a/homeassistant/components/pilight/sensor.py +++ b/homeassistant/components/pilight/sensor.py @@ -6,7 +6,10 @@ import logging import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import CONF_NAME, CONF_PAYLOAD, CONF_UNIT_OF_MEASUREMENT from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -20,7 +23,7 @@ _LOGGER = logging.getLogger(__name__) CONF_VARIABLE = "variable" DEFAULT_NAME = "Pilight Sensor" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_VARIABLE): cv.string, vol.Required(CONF_PAYLOAD): vol.Schema(dict), diff --git a/homeassistant/components/pocketcasts/sensor.py b/homeassistant/components/pocketcasts/sensor.py index b2ad050fc14..1f6af298688 100644 --- a/homeassistant/components/pocketcasts/sensor.py +++ b/homeassistant/components/pocketcasts/sensor.py @@ -8,7 +8,10 @@ import logging from pycketcasts import pocketcasts import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -22,7 +25,7 @@ SENSOR_NAME = "Pocketcasts unlistened episodes" SCAN_INTERVAL = timedelta(minutes=5) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( {vol.Required(CONF_PASSWORD): cv.string, vol.Required(CONF_USERNAME): cv.string} ) diff --git a/homeassistant/components/pyload/sensor.py b/homeassistant/components/pyload/sensor.py index 3d681c4b65d..6cb432e12fd 100644 --- a/homeassistant/components/pyload/sensor.py +++ b/homeassistant/components/pyload/sensor.py @@ -7,7 +7,7 @@ from enum import StrEnum import voluptuous as vol from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, SensorEntityDescription, @@ -87,7 +87,7 @@ SENSOR_DESCRIPTIONS: tuple[SensorEntityDescription, ...] = ( ), ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, vol.Optional(CONF_MONITORED_VARIABLES, default=["speed"]): vol.All( diff --git a/homeassistant/components/raincloud/sensor.py b/homeassistant/components/raincloud/sensor.py index 34cd3f213ed..34a7cf73490 100644 --- a/homeassistant/components/raincloud/sensor.py +++ b/homeassistant/components/raincloud/sensor.py @@ -6,7 +6,10 @@ import logging import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import CONF_MONITORED_CONDITIONS from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -24,7 +27,7 @@ from . import ( _LOGGER = logging.getLogger(__name__) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_MONITORED_CONDITIONS, default=list(SENSORS)): vol.All( cv.ensure_list, [vol.In(SENSORS)] diff --git a/homeassistant/components/random/sensor.py b/homeassistant/components/random/sensor.py index 716350b2bb0..3c6e67c9918 100644 --- a/homeassistant/components/random/sensor.py +++ b/homeassistant/components/random/sensor.py @@ -8,7 +8,10 @@ from typing import Any import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_DEVICE_CLASS, @@ -30,7 +33,7 @@ ATTR_MINIMUM = "minimum" DEFAULT_NAME = "Random sensor" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_MAXIMUM, default=DEFAULT_MAX): cv.positive_int, vol.Optional(CONF_MINIMUM, default=DEFAULT_MIN): cv.positive_int, diff --git a/homeassistant/components/reddit/sensor.py b/homeassistant/components/reddit/sensor.py index 47aa2ab86f6..35962ac091b 100644 --- a/homeassistant/components/reddit/sensor.py +++ b/homeassistant/components/reddit/sensor.py @@ -8,7 +8,10 @@ import logging import praw import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import ( ATTR_ID, CONF_CLIENT_ID, @@ -44,7 +47,7 @@ LIST_TYPES = ["top", "controversial", "hot", "new"] SCAN_INTERVAL = timedelta(seconds=300) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_CLIENT_ID): cv.string, vol.Required(CONF_CLIENT_SECRET): cv.string, diff --git a/homeassistant/components/rejseplanen/sensor.py b/homeassistant/components/rejseplanen/sensor.py index d95b9e1b271..40b27014211 100644 --- a/homeassistant/components/rejseplanen/sensor.py +++ b/homeassistant/components/rejseplanen/sensor.py @@ -14,7 +14,10 @@ from operator import itemgetter import rjpl import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import CONF_NAME, UnitOfTime from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -51,7 +54,7 @@ BUS_TYPES = ["BUS", "EXB", "TB"] TRAIN_TYPES = ["LET", "S", "REG", "IC", "LYN", "TOG"] METRO_TYPES = ["M"] -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_STOP_ID): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/rflink/sensor.py b/homeassistant/components/rflink/sensor.py index b01d1f709fe..f3c3df7f46b 100644 --- a/homeassistant/components/rflink/sensor.py +++ b/homeassistant/components/rflink/sensor.py @@ -8,7 +8,7 @@ from rflink.parser import PACKET_FIELDS, UNITS import voluptuous as vol from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, SensorEntityDescription, @@ -262,7 +262,7 @@ SENSOR_TYPES = ( SENSOR_TYPES_DICT = {desc.key: desc for desc in SENSOR_TYPES} -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_AUTOMATIC_ADD, default=True): cv.boolean, vol.Optional(CONF_DEVICES, default={}): { diff --git a/homeassistant/components/ripple/sensor.py b/homeassistant/components/ripple/sensor.py index 1b65ec7ae09..72510ea251d 100644 --- a/homeassistant/components/ripple/sensor.py +++ b/homeassistant/components/ripple/sensor.py @@ -7,7 +7,10 @@ from datetime import timedelta from pyripple import get_balance import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import CONF_ADDRESS, CONF_NAME from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -18,7 +21,7 @@ DEFAULT_NAME = "Ripple Balance" SCAN_INTERVAL = timedelta(minutes=5) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_ADDRESS): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/rmvtransport/sensor.py b/homeassistant/components/rmvtransport/sensor.py index e53423d3b14..e8b976129c5 100644 --- a/homeassistant/components/rmvtransport/sensor.py +++ b/homeassistant/components/rmvtransport/sensor.py @@ -13,7 +13,10 @@ from RMVtransport.rmvtransport import ( ) import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import CONF_NAME, CONF_TIMEOUT, UnitOfTime from homeassistant.core import HomeAssistant from homeassistant.exceptions import PlatformNotReady @@ -55,7 +58,7 @@ ATTRIBUTION = "Data provided by opendata.rmv.de" SCAN_INTERVAL = timedelta(seconds=60) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_NEXT_DEPARTURE): [ { diff --git a/homeassistant/components/rova/sensor.py b/homeassistant/components/rova/sensor.py index f63b9893c02..e44e84f52fa 100644 --- a/homeassistant/components/rova/sensor.py +++ b/homeassistant/components/rova/sensor.py @@ -7,7 +7,7 @@ from datetime import datetime import voluptuous as vol from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, SensorEntityDescription, @@ -47,7 +47,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( ), ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_ZIP_CODE): cv.string, vol.Required(CONF_HOUSE_NUMBER): cv.string, diff --git a/homeassistant/components/rtorrent/sensor.py b/homeassistant/components/rtorrent/sensor.py index 099927f1893..654288927d3 100644 --- a/homeassistant/components/rtorrent/sensor.py +++ b/homeassistant/components/rtorrent/sensor.py @@ -8,7 +8,7 @@ import xmlrpc.client import voluptuous as vol from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, SensorEntityDescription, @@ -84,7 +84,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SENSOR_KEYS: list[str] = [desc.key for desc in SENSOR_TYPES] -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_URL): cv.url, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/saj/sensor.py b/homeassistant/components/saj/sensor.py index 75b56c98ac3..c8b40fd5476 100644 --- a/homeassistant/components/saj/sensor.py +++ b/homeassistant/components/saj/sensor.py @@ -11,7 +11,7 @@ import pysaj import voluptuous as vol from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, SensorStateClass, @@ -53,7 +53,7 @@ SAJ_UNIT_MAPPINGS = { "°C": UnitOfTemperature.CELSIUS, } -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_NAME): cv.string, diff --git a/homeassistant/components/serial/sensor.py b/homeassistant/components/serial/sensor.py index 9d60877bd1b..e3fee36c09e 100644 --- a/homeassistant/components/serial/sensor.py +++ b/homeassistant/components/serial/sensor.py @@ -10,7 +10,10 @@ from serial import SerialException import serial_asyncio_fast as serial_asyncio import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import CONF_NAME, CONF_VALUE_TEMPLATE, EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv @@ -37,7 +40,7 @@ DEFAULT_XONXOFF = False DEFAULT_RTSCTS = False DEFAULT_DSRDTR = False -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_SERIAL_PORT): cv.string, vol.Optional(CONF_BAUDRATE, default=DEFAULT_BAUDRATE): cv.positive_int, diff --git a/homeassistant/components/serial_pm/sensor.py b/homeassistant/components/serial_pm/sensor.py index 00ac4fe8731..b454424591d 100644 --- a/homeassistant/components/serial_pm/sensor.py +++ b/homeassistant/components/serial_pm/sensor.py @@ -7,7 +7,10 @@ import logging from pmsensor import serial_pm as pm import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONF_NAME from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -19,7 +22,7 @@ _LOGGER = logging.getLogger(__name__) CONF_BRAND = "brand" CONF_SERIAL_DEVICE = "serial_device" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_BRAND): cv.string, vol.Required(CONF_SERIAL_DEVICE): cv.string, diff --git a/homeassistant/components/seventeentrack/sensor.py b/homeassistant/components/seventeentrack/sensor.py index acc8471c030..fa6f283427d 100644 --- a/homeassistant/components/seventeentrack/sensor.py +++ b/homeassistant/components/seventeentrack/sensor.py @@ -7,7 +7,10 @@ from typing import Any import voluptuous as vol from homeassistant.components import persistent_notification -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( ATTR_FRIENDLY_NAME, @@ -46,7 +49,7 @@ from .const import ( VALUE_DELIVERED, ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, diff --git a/homeassistant/components/shodan/sensor.py b/homeassistant/components/shodan/sensor.py index fd608cbcb45..867b58ad1ba 100644 --- a/homeassistant/components/shodan/sensor.py +++ b/homeassistant/components/shodan/sensor.py @@ -8,7 +8,10 @@ import logging import shodan import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import CONF_API_KEY, CONF_NAME from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -23,7 +26,7 @@ DEFAULT_NAME = "Shodan Sensor" SCAN_INTERVAL = timedelta(minutes=15) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_API_KEY): cv.string, vol.Required(CONF_QUERY): cv.string, diff --git a/homeassistant/components/sigfox/sensor.py b/homeassistant/components/sigfox/sensor.py index fbda6fece21..8f9190e4436 100644 --- a/homeassistant/components/sigfox/sensor.py +++ b/homeassistant/components/sigfox/sensor.py @@ -11,7 +11,10 @@ from urllib.parse import urljoin import requests import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -26,7 +29,7 @@ CONF_API_LOGIN = "api_login" CONF_API_PASSWORD = "api_password" DEFAULT_NAME = "sigfox" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_API_LOGIN): cv.string, vol.Required(CONF_API_PASSWORD): cv.string, diff --git a/homeassistant/components/simulated/sensor.py b/homeassistant/components/simulated/sensor.py index 51ec19ac80b..b4180ba300d 100644 --- a/homeassistant/components/simulated/sensor.py +++ b/homeassistant/components/simulated/sensor.py @@ -8,7 +8,10 @@ from random import Random import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -35,7 +38,7 @@ DEFAULT_SEED = 999 DEFAULT_UNIT = "value" DEFAULT_RELATIVE_TO_EPOCH = True -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_AMP, default=DEFAULT_AMP): vol.Coerce(float), vol.Optional(CONF_FWHM, default=DEFAULT_FWHM): vol.Coerce(float), diff --git a/homeassistant/components/skybeacon/sensor.py b/homeassistant/components/skybeacon/sensor.py index 257ea2e92fa..a3a5eb48098 100644 --- a/homeassistant/components/skybeacon/sensor.py +++ b/homeassistant/components/skybeacon/sensor.py @@ -12,7 +12,7 @@ from pygatt.exceptions import BLEError, NotConnectedError, NotificationTimeout import voluptuous as vol from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, ) @@ -44,7 +44,7 @@ DEFAULT_NAME = "Skybeacon" SKIP_HANDLE_LOOKUP = True -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_MAC): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/snmp/sensor.py b/homeassistant/components/snmp/sensor.py index 0e5b215dcd4..fb7b87403cb 100644 --- a/homeassistant/components/snmp/sensor.py +++ b/homeassistant/components/snmp/sensor.py @@ -20,7 +20,10 @@ from pysnmp.proto.rfc1902 import Opaque from pysnmp.proto.rfc1905 import NoSuchObject import voluptuous as vol -from homeassistant.components.sensor import CONF_STATE_CLASS, PLATFORM_SCHEMA +from homeassistant.components.sensor import ( + CONF_STATE_CLASS, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, +) from homeassistant.const import ( CONF_DEVICE_CLASS, CONF_HOST, @@ -83,7 +86,7 @@ TRIGGER_ENTITY_OPTIONS = ( CONF_UNIT_OF_MEASUREMENT, ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_BASEOID): cv.string, vol.Optional(CONF_ACCEPT_ERRORS, default=False): cv.boolean, diff --git a/homeassistant/components/solaredge_local/sensor.py b/homeassistant/components/solaredge_local/sensor.py index ae009410692..a7940aa34b5 100644 --- a/homeassistant/components/solaredge_local/sensor.py +++ b/homeassistant/components/solaredge_local/sensor.py @@ -13,7 +13,7 @@ from solaredge_local import SolarEdge import voluptuous as vol from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, SensorEntityDescription, @@ -193,7 +193,7 @@ SENSOR_TYPES_ENERGY_EXPORT: tuple[SolarEdgeLocalSensorEntityDescription, ...] = ), ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_IP_ADDRESS): cv.string, vol.Optional(CONF_NAME, default="SolarEdge"): cv.string, diff --git a/homeassistant/components/starlingbank/sensor.py b/homeassistant/components/starlingbank/sensor.py index f6b11a41102..fd351416c28 100644 --- a/homeassistant/components/starlingbank/sensor.py +++ b/homeassistant/components/starlingbank/sensor.py @@ -9,7 +9,10 @@ import requests from starlingbank import StarlingAccount import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import CONF_ACCESS_TOKEN, CONF_NAME from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -41,7 +44,7 @@ ACCOUNT_SCHEMA = vol.Schema( } ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( {vol.Required(CONF_ACCOUNTS): vol.Schema([ACCOUNT_SCHEMA])} ) diff --git a/homeassistant/components/startca/sensor.py b/homeassistant/components/startca/sensor.py index fad001d6d29..5fc4872a754 100644 --- a/homeassistant/components/startca/sensor.py +++ b/homeassistant/components/startca/sensor.py @@ -12,7 +12,7 @@ import voluptuous as vol import xmltodict from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, SensorEntityDescription, @@ -127,7 +127,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SENSOR_KEYS: list[str] = [desc.key for desc in SENSOR_TYPES] -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_MONITORED_VARIABLES): vol.All( cv.ensure_list, [vol.In(SENSOR_KEYS)] diff --git a/homeassistant/components/supervisord/sensor.py b/homeassistant/components/supervisord/sensor.py index 7939232cd6f..24189fb7de0 100644 --- a/homeassistant/components/supervisord/sensor.py +++ b/homeassistant/components/supervisord/sensor.py @@ -7,7 +7,10 @@ import xmlrpc.client import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import CONF_URL from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -21,7 +24,7 @@ ATTR_GROUP = "group" DEFAULT_URL = "http://localhost:9001/RPC2" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( {vol.Optional(CONF_URL, default=DEFAULT_URL): cv.url} ) diff --git a/homeassistant/components/swiss_hydrological_data/sensor.py b/homeassistant/components/swiss_hydrological_data/sensor.py index e74d1f66046..c67045521b5 100644 --- a/homeassistant/components/swiss_hydrological_data/sensor.py +++ b/homeassistant/components/swiss_hydrological_data/sensor.py @@ -8,7 +8,10 @@ import logging from swisshydrodata import SwissHydroData import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import CONF_MONITORED_CONDITIONS from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -46,7 +49,7 @@ CONDITION_DETAILS = [ ATTR_MIN_24H, ] -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_STATION): vol.Coerce(int), vol.Optional(CONF_MONITORED_CONDITIONS, default=[SENSOR_TEMPERATURE]): vol.All( diff --git a/homeassistant/components/tank_utility/sensor.py b/homeassistant/components/tank_utility/sensor.py index b4d972f7c06..9bdcc1b6f4f 100644 --- a/homeassistant/components/tank_utility/sensor.py +++ b/homeassistant/components/tank_utility/sensor.py @@ -9,7 +9,10 @@ import requests from tank_utility import auth, device as tank_monitor import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import CONF_DEVICES, CONF_EMAIL, CONF_PASSWORD, PERCENTAGE from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -20,7 +23,7 @@ _LOGGER = logging.getLogger(__name__) SCAN_INTERVAL = datetime.timedelta(hours=1) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_EMAIL): cv.string, vol.Required(CONF_PASSWORD): cv.string, diff --git a/homeassistant/components/tcp/sensor.py b/homeassistant/components/tcp/sensor.py index 6c1e6563c50..a3bd4b2c619 100644 --- a/homeassistant/components/tcp/sensor.py +++ b/homeassistant/components/tcp/sensor.py @@ -5,7 +5,7 @@ from __future__ import annotations from typing import Final from homeassistant.components.sensor import ( - PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, SensorEntity, ) from homeassistant.const import CONF_UNIT_OF_MEASUREMENT @@ -15,7 +15,7 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateTyp from .common import TCP_PLATFORM_SCHEMA, TcpEntity -PLATFORM_SCHEMA: Final = PARENT_PLATFORM_SCHEMA.extend(TCP_PLATFORM_SCHEMA) +PLATFORM_SCHEMA: Final = SENSOR_PLATFORM_SCHEMA.extend(TCP_PLATFORM_SCHEMA) def setup_platform( diff --git a/homeassistant/components/ted5000/sensor.py b/homeassistant/components/ted5000/sensor.py index 99d8991a02e..68f4520a7e3 100644 --- a/homeassistant/components/ted5000/sensor.py +++ b/homeassistant/components/ted5000/sensor.py @@ -11,7 +11,7 @@ import voluptuous as vol import xmltodict from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, SensorEntityDescription, @@ -37,7 +37,7 @@ DEFAULT_NAME = "ted" MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=10) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_PORT, default=80): cv.port, diff --git a/homeassistant/components/tellstick/sensor.py b/homeassistant/components/tellstick/sensor.py index a2cba41b028..2c304f259da 100644 --- a/homeassistant/components/tellstick/sensor.py +++ b/homeassistant/components/tellstick/sensor.py @@ -10,7 +10,7 @@ import tellcore.constants as tellcore_constants import voluptuous as vol from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, ) @@ -40,7 +40,7 @@ CONF_TEMPERATURE_SCALE = "temperature_scale" DEFAULT_DATATYPE_MASK = 127 DEFAULT_TEMPERATURE_SCALE = UnitOfTemperature.CELSIUS -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Optional( CONF_TEMPERATURE_SCALE, default=DEFAULT_TEMPERATURE_SCALE diff --git a/homeassistant/components/temper/sensor.py b/homeassistant/components/temper/sensor.py index 7138f40a653..92b7fe3de43 100644 --- a/homeassistant/components/temper/sensor.py +++ b/homeassistant/components/temper/sensor.py @@ -8,7 +8,7 @@ from temperusb.temper import TemperHandler import voluptuous as vol from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, ) @@ -26,7 +26,7 @@ _LOGGER = logging.getLogger(__name__) CONF_SCALE = "scale" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_NAME, default=DEVICE_DEFAULT_NAME): vol.Coerce(str), vol.Optional(CONF_SCALE, default=1): vol.Coerce(float), diff --git a/homeassistant/components/thermoworks_smoke/sensor.py b/homeassistant/components/thermoworks_smoke/sensor.py index 57621ba1055..7dc845ecf60 100644 --- a/homeassistant/components/thermoworks_smoke/sensor.py +++ b/homeassistant/components/thermoworks_smoke/sensor.py @@ -14,7 +14,7 @@ import thermoworks_smoke import voluptuous as vol from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, ) @@ -57,7 +57,7 @@ SENSOR_TYPES = { # exclude these keys from thermoworks data EXCLUDE_KEYS = [FIRMWARE] -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_EMAIL): cv.string, vol.Required(CONF_PASSWORD): cv.string, diff --git a/homeassistant/components/thinkingcleaner/sensor.py b/homeassistant/components/thinkingcleaner/sensor.py index 86c5a8813d8..4d28912e20d 100644 --- a/homeassistant/components/thinkingcleaner/sensor.py +++ b/homeassistant/components/thinkingcleaner/sensor.py @@ -9,7 +9,7 @@ import voluptuous as vol from homeassistant import util from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, SensorEntity, SensorEntityDescription, ) @@ -66,7 +66,7 @@ STATES = { "st_unknown": "Unknown state", } -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({vol.Optional(CONF_HOST): cv.string}) +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend({vol.Optional(CONF_HOST): cv.string}) def setup_platform( diff --git a/homeassistant/components/time_date/sensor.py b/homeassistant/components/time_date/sensor.py index ed999e5a0b2..442442f0e1d 100644 --- a/homeassistant/components/time_date/sensor.py +++ b/homeassistant/components/time_date/sensor.py @@ -11,7 +11,7 @@ import voluptuous as vol from homeassistant.components.sensor import ( ENTITY_ID_FORMAT, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, SensorEntity, ) from homeassistant.config_entries import ConfigEntry @@ -30,7 +30,7 @@ _LOGGER = logging.getLogger(__name__) TIME_STR_FORMAT = "%H:%M" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_DISPLAY_OPTIONS, default=["time"]): vol.All( cv.ensure_list, [vol.In(OPTION_TYPES)] diff --git a/homeassistant/components/tmb/sensor.py b/homeassistant/components/tmb/sensor.py index 4ec86434ea0..126c3128f91 100644 --- a/homeassistant/components/tmb/sensor.py +++ b/homeassistant/components/tmb/sensor.py @@ -9,7 +9,10 @@ from requests import HTTPError from tmb import IBus import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import CONF_NAME, UnitOfTime from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -37,7 +40,7 @@ LINE_STOP_SCHEMA = vol.Schema( } ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_APP_ID): cv.string, vol.Required(CONF_APP_KEY): cv.string, diff --git a/homeassistant/components/torque/sensor.py b/homeassistant/components/torque/sensor.py index 8572a5a0bba..543046fac1c 100644 --- a/homeassistant/components/torque/sensor.py +++ b/homeassistant/components/torque/sensor.py @@ -8,7 +8,10 @@ from aiohttp import web import voluptuous as vol from homeassistant.components.http import HomeAssistantView -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import CONF_EMAIL, CONF_NAME, DEGREE from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv @@ -31,7 +34,7 @@ NAME_KEY = re.compile(SENSOR_NAME_KEY) UNIT_KEY = re.compile(SENSOR_UNIT_KEY) VALUE_KEY = re.compile(SENSOR_VALUE_KEY) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_EMAIL): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/transport_nsw/sensor.py b/homeassistant/components/transport_nsw/sensor.py index 4ec4301dc7b..787f3298e59 100644 --- a/homeassistant/components/transport_nsw/sensor.py +++ b/homeassistant/components/transport_nsw/sensor.py @@ -8,7 +8,7 @@ from TransportNSW import TransportNSW import voluptuous as vol from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, SensorStateClass, @@ -44,7 +44,7 @@ ICONS = { SCAN_INTERVAL = timedelta(seconds=60) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_STOP_ID): cv.string, vol.Required(CONF_API_KEY): cv.string, diff --git a/homeassistant/components/travisci/sensor.py b/homeassistant/components/travisci/sensor.py index 0a3118b3cca..fe4a6541d9e 100644 --- a/homeassistant/components/travisci/sensor.py +++ b/homeassistant/components/travisci/sensor.py @@ -11,7 +11,7 @@ import voluptuous as vol from homeassistant.components import persistent_notification from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, SensorEntity, SensorEntityDescription, ) @@ -74,7 +74,7 @@ SENSOR_KEYS: list[str] = [desc.key for desc in SENSOR_TYPES] NOTIFICATION_ID = "travisci" NOTIFICATION_TITLE = "Travis CI Sensor Setup" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_API_KEY): cv.string, vol.Required(CONF_MONITORED_CONDITIONS, default=SENSOR_KEYS): vol.All( diff --git a/homeassistant/components/uk_transport/sensor.py b/homeassistant/components/uk_transport/sensor.py index 134dd675163..8e874be0bca 100644 --- a/homeassistant/components/uk_transport/sensor.py +++ b/homeassistant/components/uk_transport/sensor.py @@ -10,7 +10,10 @@ import re import requests import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import CONF_MODE, UnitOfTime from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -44,7 +47,7 @@ _QUERY_SCHEME = vol.Schema( } ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_API_APP_ID): cv.string, vol.Required(CONF_API_APP_KEY): cv.string, diff --git a/homeassistant/components/vasttrafik/sensor.py b/homeassistant/components/vasttrafik/sensor.py index 611f571336c..48f659103e1 100644 --- a/homeassistant/components/vasttrafik/sensor.py +++ b/homeassistant/components/vasttrafik/sensor.py @@ -8,7 +8,10 @@ import logging import vasttrafik import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import CONF_DELAY, CONF_NAME from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -38,7 +41,7 @@ DEFAULT_DELAY = 0 MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=120) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_KEY): cv.string, vol.Required(CONF_SECRET): cv.string, diff --git a/homeassistant/components/viaggiatreno/sensor.py b/homeassistant/components/viaggiatreno/sensor.py index 9c6c6bca422..1ea12ed6a41 100644 --- a/homeassistant/components/viaggiatreno/sensor.py +++ b/homeassistant/components/viaggiatreno/sensor.py @@ -10,7 +10,10 @@ import time import aiohttp import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import UnitOfTime from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -52,7 +55,7 @@ CANCELLED_STRING = "Cancelled" NOT_DEPARTED_STRING = "Not departed yet" NO_INFORMATION_STRING = "No information for this train now" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_TRAIN_ID): cv.string, vol.Required(CONF_STATION_ID): cv.string, diff --git a/homeassistant/components/volkszaehler/sensor.py b/homeassistant/components/volkszaehler/sensor.py index ce5691b1193..c4fa7b1088b 100644 --- a/homeassistant/components/volkszaehler/sensor.py +++ b/homeassistant/components/volkszaehler/sensor.py @@ -10,7 +10,7 @@ from volkszaehler.exceptions import VolkszaehlerApiConnectionError import voluptuous as vol from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, SensorEntityDescription, @@ -73,7 +73,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SENSOR_KEYS: list[str] = [desc.key for desc in SENSOR_TYPES] -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_UUID): cv.string, vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, diff --git a/homeassistant/components/vultr/sensor.py b/homeassistant/components/vultr/sensor.py index 816a55736be..843aa416297 100644 --- a/homeassistant/components/vultr/sensor.py +++ b/homeassistant/components/vultr/sensor.py @@ -7,7 +7,7 @@ import logging import voluptuous as vol from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, SensorEntityDescription, @@ -45,7 +45,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( ) SENSOR_KEYS: list[str] = [desc.key for desc in SENSOR_TYPES] -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_SUBSCRIPTION): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/wirelesstag/sensor.py b/homeassistant/components/wirelesstag/sensor.py index 0e88272a41c..87906bdc2ae 100644 --- a/homeassistant/components/wirelesstag/sensor.py +++ b/homeassistant/components/wirelesstag/sensor.py @@ -7,7 +7,7 @@ import logging import voluptuous as vol from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, SensorEntityDescription, @@ -65,7 +65,7 @@ SENSOR_TYPES: dict[str, SensorEntityDescription] = { SENSOR_KEYS: list[str] = list(SENSOR_TYPES) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_MONITORED_CONDITIONS, default=[]): vol.All( cv.ensure_list, [vol.In(SENSOR_KEYS)] diff --git a/homeassistant/components/worldclock/sensor.py b/homeassistant/components/worldclock/sensor.py index 9b2cb600ac1..d9b4aa90f07 100644 --- a/homeassistant/components/worldclock/sensor.py +++ b/homeassistant/components/worldclock/sensor.py @@ -6,7 +6,10 @@ from datetime import tzinfo import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import CONF_NAME, CONF_TIME_ZONE from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -19,7 +22,7 @@ CONF_TIME_FORMAT = "time_format" DEFAULT_NAME = "Worldclock Sensor" DEFAULT_TIME_STR_FORMAT = "%H:%M" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_TIME_ZONE): cv.time_zone, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/worldtidesinfo/sensor.py b/homeassistant/components/worldtidesinfo/sensor.py index a4d663cc184..45f39894abb 100644 --- a/homeassistant/components/worldtidesinfo/sensor.py +++ b/homeassistant/components/worldtidesinfo/sensor.py @@ -9,7 +9,10 @@ import time import requests import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -24,7 +27,7 @@ DEFAULT_NAME = "WorldTidesInfo" SCAN_INTERVAL = timedelta(seconds=3600) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_API_KEY): cv.string, vol.Optional(CONF_LATITUDE): cv.latitude, diff --git a/homeassistant/components/worxlandroid/sensor.py b/homeassistant/components/worxlandroid/sensor.py index 10f40bea685..50700b78f35 100644 --- a/homeassistant/components/worxlandroid/sensor.py +++ b/homeassistant/components/worxlandroid/sensor.py @@ -8,7 +8,10 @@ import logging import aiohttp import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import CONF_HOST, CONF_PIN, CONF_TIMEOUT, PERCENTAGE from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -22,7 +25,7 @@ CONF_ALLOW_UNREACHABLE = "allow_unreachable" DEFAULT_TIMEOUT = 5 -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Required(CONF_PIN): vol.All(vol.Coerce(str), vol.Match(r"\d{4}")), diff --git a/homeassistant/components/wsdot/sensor.py b/homeassistant/components/wsdot/sensor.py index 14e21f79282..3aae6746ea9 100644 --- a/homeassistant/components/wsdot/sensor.py +++ b/homeassistant/components/wsdot/sensor.py @@ -10,7 +10,10 @@ import re import requests import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import ATTR_NAME, CONF_API_KEY, CONF_ID, CONF_NAME, UnitOfTime from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -39,7 +42,7 @@ RESOURCE = ( SCAN_INTERVAL = timedelta(minutes=3) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_API_KEY): cv.string, vol.Required(CONF_TRAVEL_TIMES): [ diff --git a/homeassistant/components/yandex_transport/sensor.py b/homeassistant/components/yandex_transport/sensor.py index bcef8248aa3..30227e3261e 100644 --- a/homeassistant/components/yandex_transport/sensor.py +++ b/homeassistant/components/yandex_transport/sensor.py @@ -9,7 +9,7 @@ from aioymaps import CaptchaError, YandexMapsRequester import voluptuous as vol from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, ) @@ -34,7 +34,7 @@ DEFAULT_NAME = "Yandex Transport" SCAN_INTERVAL = timedelta(minutes=1) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_STOP_ID): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/zabbix/sensor.py b/homeassistant/components/zabbix/sensor.py index 4c6af57f780..2187deb22e8 100644 --- a/homeassistant/components/zabbix/sensor.py +++ b/homeassistant/components/zabbix/sensor.py @@ -9,7 +9,10 @@ from typing import Any from pyzabbix import ZabbixAPI import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -35,7 +38,7 @@ _ZABBIX_TRIGGER_SCHEMA = vol.Schema( # SCAN_INTERVAL = 30 # -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( {vol.Required(_CONF_TRIGGERS): vol.Any(_ZABBIX_TRIGGER_SCHEMA, None)} ) diff --git a/homeassistant/components/zestimate/sensor.py b/homeassistant/components/zestimate/sensor.py index 8bbda7de73a..12831c96932 100644 --- a/homeassistant/components/zestimate/sensor.py +++ b/homeassistant/components/zestimate/sensor.py @@ -9,7 +9,10 @@ import requests import voluptuous as vol import xmltodict -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import CONF_API_KEY, CONF_NAME from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -33,7 +36,7 @@ ATTR_LAST_UPDATED = "amount_last_updated" ATTR_VAL_HI = "valuation_range_high" ATTR_VAL_LOW = "valuation_range_low" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_API_KEY): cv.string, vol.Required(CONF_ZPID): vol.All(cv.ensure_list, [cv.string]), diff --git a/homeassistant/components/zoneminder/sensor.py b/homeassistant/components/zoneminder/sensor.py index 700344f44da..75769d9fd98 100644 --- a/homeassistant/components/zoneminder/sensor.py +++ b/homeassistant/components/zoneminder/sensor.py @@ -9,7 +9,7 @@ from zoneminder.monitor import Monitor, TimePeriod from zoneminder.zm import ZoneMinder from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, SensorEntity, SensorEntityDescription, ) @@ -53,7 +53,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SENSOR_KEYS: list[str] = [desc.key for desc in SENSOR_TYPES] -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Optional( CONF_INCLUDE_ARCHIVED, default=DEFAULT_INCLUDE_ARCHIVED From d00fe1ce7f5542e40b598cb628ba5bbd2ad141e7 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk <11290930+bouwew@users.noreply.github.com> Date: Wed, 26 Jun 2024 12:17:17 +0200 Subject: [PATCH 1259/1445] Import DOMAIN constants for Plugwise and implement (#120530) --- tests/components/plugwise/test_climate.py | 64 +++++++++++++---------- tests/components/plugwise/test_switch.py | 46 +++++++++------- 2 files changed, 61 insertions(+), 49 deletions(-) diff --git a/tests/components/plugwise/test_climate.py b/tests/components/plugwise/test_climate.py index b3f42031ed8..c91e4d37ba6 100644 --- a/tests/components/plugwise/test_climate.py +++ b/tests/components/plugwise/test_climate.py @@ -6,7 +6,13 @@ from unittest.mock import MagicMock, patch from plugwise.exceptions import PlugwiseError import pytest -from homeassistant.components.climate import HVACMode +from homeassistant.components.climate import ( + DOMAIN as CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, + SERVICE_SET_PRESET_MODE, + SERVICE_SET_TEMPERATURE, + HVACMode, +) from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.util.dt import utcnow @@ -153,8 +159,8 @@ async def test_adam_climate_adjust_negative_testing( with pytest.raises(HomeAssistantError): await hass.services.async_call( - "climate", - "set_temperature", + CLIMATE_DOMAIN, + SERVICE_SET_TEMPERATURE, {"entity_id": "climate.zone_lisa_wk", "temperature": 25}, blocking=True, ) @@ -165,8 +171,8 @@ async def test_adam_climate_entity_climate_changes( ) -> None: """Test handling of user requests in adam climate device environment.""" await hass.services.async_call( - "climate", - "set_temperature", + CLIMATE_DOMAIN, + SERVICE_SET_TEMPERATURE, {"entity_id": "climate.zone_lisa_wk", "temperature": 25}, blocking=True, ) @@ -176,8 +182,8 @@ async def test_adam_climate_entity_climate_changes( ) await hass.services.async_call( - "climate", - "set_temperature", + CLIMATE_DOMAIN, + SERVICE_SET_TEMPERATURE, { "entity_id": "climate.zone_lisa_wk", "hvac_mode": "heat", @@ -192,15 +198,15 @@ async def test_adam_climate_entity_climate_changes( with pytest.raises(ValueError): await hass.services.async_call( - "climate", - "set_temperature", + CLIMATE_DOMAIN, + SERVICE_SET_TEMPERATURE, {"entity_id": "climate.zone_lisa_wk", "temperature": 150}, blocking=True, ) await hass.services.async_call( - "climate", - "set_preset_mode", + CLIMATE_DOMAIN, + SERVICE_SET_PRESET_MODE, {"entity_id": "climate.zone_lisa_wk", "preset_mode": "away"}, blocking=True, ) @@ -210,8 +216,8 @@ async def test_adam_climate_entity_climate_changes( ) await hass.services.async_call( - "climate", - "set_hvac_mode", + CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, {"entity_id": "climate.zone_lisa_wk", "hvac_mode": "heat"}, blocking=True, ) @@ -222,8 +228,8 @@ async def test_adam_climate_entity_climate_changes( with pytest.raises(HomeAssistantError): await hass.services.async_call( - "climate", - "set_hvac_mode", + CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, { "entity_id": "climate.zone_thermostat_jessie", "hvac_mode": "dry", @@ -242,8 +248,8 @@ async def test_adam_climate_off_mode_change( assert state assert state.state == HVACMode.OFF await hass.services.async_call( - "climate", - "set_hvac_mode", + CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, { "entity_id": "climate.slaapkamer", "hvac_mode": "heat", @@ -258,8 +264,8 @@ async def test_adam_climate_off_mode_change( assert state assert state.state == HVACMode.HEAT await hass.services.async_call( - "climate", - "set_hvac_mode", + CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, { "entity_id": "climate.kinderkamer", "hvac_mode": "off", @@ -274,8 +280,8 @@ async def test_adam_climate_off_mode_change( assert state assert state.state == HVACMode.HEAT await hass.services.async_call( - "climate", - "set_hvac_mode", + CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, { "entity_id": "climate.logeerkamer", "hvac_mode": "heat", @@ -353,8 +359,8 @@ async def test_anna_climate_entity_climate_changes( ) -> None: """Test handling of user requests in anna climate device environment.""" await hass.services.async_call( - "climate", - "set_temperature", + CLIMATE_DOMAIN, + SERVICE_SET_TEMPERATURE, {"entity_id": "climate.anna", "target_temp_high": 30, "target_temp_low": 20}, blocking=True, ) @@ -365,8 +371,8 @@ async def test_anna_climate_entity_climate_changes( ) await hass.services.async_call( - "climate", - "set_preset_mode", + CLIMATE_DOMAIN, + SERVICE_SET_PRESET_MODE, {"entity_id": "climate.anna", "preset_mode": "away"}, blocking=True, ) @@ -376,8 +382,8 @@ async def test_anna_climate_entity_climate_changes( ) await hass.services.async_call( - "climate", - "set_hvac_mode", + CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, {"entity_id": "climate.anna", "hvac_mode": "auto"}, blocking=True, ) @@ -385,8 +391,8 @@ async def test_anna_climate_entity_climate_changes( assert mock_smile_anna.set_schedule_state.call_count == 0 await hass.services.async_call( - "climate", - "set_hvac_mode", + CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, {"entity_id": "climate.anna", "hvac_mode": "heat_cool"}, blocking=True, ) diff --git a/tests/components/plugwise/test_switch.py b/tests/components/plugwise/test_switch.py index 6b2393476ae..5da76bb0ebd 100644 --- a/tests/components/plugwise/test_switch.py +++ b/tests/components/plugwise/test_switch.py @@ -7,6 +7,12 @@ import pytest from homeassistant.components.plugwise.const import DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +from homeassistant.const import ( + SERVICE_TOGGLE, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_ON, +) from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er @@ -20,11 +26,11 @@ async def test_adam_climate_switch_entities( """Test creation of climate related switch entities.""" state = hass.states.get("switch.cv_pomp_relay") assert state - assert state.state == "on" + assert state.state == STATE_ON state = hass.states.get("switch.fibaro_hc2_relay") assert state - assert state.state == "on" + assert state.state == STATE_ON async def test_adam_climate_switch_negative_testing( @@ -35,8 +41,8 @@ async def test_adam_climate_switch_negative_testing( with pytest.raises(HomeAssistantError): await hass.services.async_call( - "switch", - "turn_off", + SWITCH_DOMAIN, + SERVICE_TURN_OFF, {"entity_id": "switch.cv_pomp_relay"}, blocking=True, ) @@ -48,8 +54,8 @@ async def test_adam_climate_switch_negative_testing( with pytest.raises(HomeAssistantError): await hass.services.async_call( - "switch", - "turn_on", + SWITCH_DOMAIN, + SERVICE_TURN_ON, {"entity_id": "switch.fibaro_hc2_relay"}, blocking=True, ) @@ -65,8 +71,8 @@ async def test_adam_climate_switch_changes( ) -> None: """Test changing of climate related switch entities.""" await hass.services.async_call( - "switch", - "turn_off", + SWITCH_DOMAIN, + SERVICE_TURN_OFF, {"entity_id": "switch.cv_pomp_relay"}, blocking=True, ) @@ -77,8 +83,8 @@ async def test_adam_climate_switch_changes( ) await hass.services.async_call( - "switch", - "toggle", + SWITCH_DOMAIN, + SERVICE_TOGGLE, {"entity_id": "switch.fibaro_hc2_relay"}, blocking=True, ) @@ -89,8 +95,8 @@ async def test_adam_climate_switch_changes( ) await hass.services.async_call( - "switch", - "turn_on", + SWITCH_DOMAIN, + SERVICE_TURN_ON, {"entity_id": "switch.fibaro_hc2_relay"}, blocking=True, ) @@ -107,11 +113,11 @@ async def test_stretch_switch_entities( """Test creation of climate related switch entities.""" state = hass.states.get("switch.koelkast_92c4a_relay") assert state - assert state.state == "on" + assert state.state == STATE_ON state = hass.states.get("switch.droger_52559_relay") assert state - assert state.state == "on" + assert state.state == STATE_ON async def test_stretch_switch_changes( @@ -119,8 +125,8 @@ async def test_stretch_switch_changes( ) -> None: """Test changing of power related switch entities.""" await hass.services.async_call( - "switch", - "turn_off", + SWITCH_DOMAIN, + SERVICE_TURN_OFF, {"entity_id": "switch.koelkast_92c4a_relay"}, blocking=True, ) @@ -130,8 +136,8 @@ async def test_stretch_switch_changes( ) await hass.services.async_call( - "switch", - "toggle", + SWITCH_DOMAIN, + SERVICE_TOGGLE, {"entity_id": "switch.droger_52559_relay"}, blocking=True, ) @@ -141,8 +147,8 @@ async def test_stretch_switch_changes( ) await hass.services.async_call( - "switch", - "turn_on", + SWITCH_DOMAIN, + SERVICE_TURN_ON, {"entity_id": "switch.droger_52559_relay"}, blocking=True, ) From fac8349c37a44f9888c00d3adb8c6d3c1712456a Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 26 Jun 2024 12:18:33 +0200 Subject: [PATCH 1260/1445] Add learning offset select to Airgradient (#120532) --- .../components/airgradient/select.py | 68 +++- .../components/airgradient/strings.json | 34 +- .../airgradient/snapshots/test_select.ambr | 366 ++++++++++++++++++ .../airgradient/snapshots/test_sensor.ambr | 48 +-- tests/components/airgradient/test_select.py | 2 +- 5 files changed, 489 insertions(+), 29 deletions(-) diff --git a/homeassistant/components/airgradient/select.py b/homeassistant/components/airgradient/select.py index 1cb902a2d3c..c37df0483d1 100644 --- a/homeassistant/components/airgradient/select.py +++ b/homeassistant/components/airgradient/select.py @@ -79,6 +79,65 @@ LED_BAR_ENTITIES: tuple[AirGradientSelectEntityDescription, ...] = ( ), ) +LEARNING_TIME_OFFSET_OPTIONS = { + 12: "12", + 60: "60", + 120: "120", + 360: "360", + 720: "720", +} +LEARNING_TIME_OFFSET_OPTIONS_INVERSE = { + v: k for k, v in LEARNING_TIME_OFFSET_OPTIONS.items() +} +ABC_DAYS = { + 8: "8", + 30: "30", + 90: "90", + 180: "180", + 0: "off", +} +ABC_DAYS_INVERSE = {v: k for k, v in ABC_DAYS.items()} + +CONTROL_ENTITIES: tuple[AirGradientSelectEntityDescription, ...] = ( + AirGradientSelectEntityDescription( + key="nox_index_learning_time_offset", + translation_key="nox_index_learning_time_offset", + options=list(LEARNING_TIME_OFFSET_OPTIONS_INVERSE), + entity_category=EntityCategory.CONFIG, + value_fn=lambda config: LEARNING_TIME_OFFSET_OPTIONS.get( + config.nox_learning_offset + ), + set_value_fn=lambda client, value: client.set_nox_learning_offset( + LEARNING_TIME_OFFSET_OPTIONS_INVERSE.get(value, 12) + ), + ), + AirGradientSelectEntityDescription( + key="voc_index_learning_time_offset", + translation_key="voc_index_learning_time_offset", + options=list(LEARNING_TIME_OFFSET_OPTIONS_INVERSE), + entity_category=EntityCategory.CONFIG, + value_fn=lambda config: LEARNING_TIME_OFFSET_OPTIONS.get( + config.nox_learning_offset + ), + set_value_fn=lambda client, value: client.set_tvoc_learning_offset( + LEARNING_TIME_OFFSET_OPTIONS_INVERSE.get(value, 12) + ), + ), + AirGradientSelectEntityDescription( + key="co2_automatic_baseline_calibration", + translation_key="co2_automatic_baseline_calibration", + options=list(ABC_DAYS_INVERSE), + entity_category=EntityCategory.CONFIG, + value_fn=lambda config: ABC_DAYS.get( + config.co2_automatic_baseline_calibration_days + ), + set_value_fn=lambda client, + value: client.set_co2_automatic_baseline_calibration( + ABC_DAYS_INVERSE.get(value, 0) + ), + ), +) + async def async_setup_entry( hass: HomeAssistant, @@ -104,7 +163,10 @@ async def async_setup_entry( coordinator.data.configuration_control is ConfigurationControl.LOCAL and not added_entities ): - entities: list[AirGradientSelect] = [] + entities: list[AirGradientSelect] = [ + AirGradientSelect(coordinator, description) + for description in CONTROL_ENTITIES + ] if "I" in model: entities.extend( AirGradientSelect(coordinator, description) @@ -123,7 +185,9 @@ async def async_setup_entry( and added_entities ): entity_registry = er.async_get(hass) - for entity_description in DISPLAY_SELECT_TYPES + LED_BAR_ENTITIES: + for entity_description in ( + DISPLAY_SELECT_TYPES + LED_BAR_ENTITIES + CONTROL_ENTITIES + ): unique_id = f"{coordinator.serial_number}-{entity_description.key}" if entity_id := entity_registry.async_get_entity_id( SELECT_DOMAIN, DOMAIN, unique_id diff --git a/homeassistant/components/airgradient/strings.json b/homeassistant/components/airgradient/strings.json index 4e8973bdde2..eb529a99ae3 100644 --- a/homeassistant/components/airgradient/strings.json +++ b/homeassistant/components/airgradient/strings.json @@ -69,6 +69,36 @@ "co2": "Carbon dioxide", "pm": "Particulate matter" } + }, + "nox_index_learning_time_offset": { + "name": "NOx index learning offset", + "state": { + "12": "12 hours", + "60": "60 hours", + "120": "120 hours", + "360": "360 hours", + "720": "720 hours" + } + }, + "voc_index_learning_time_offset": { + "name": "VOC index learning offset", + "state": { + "12": "[%key:component::airgradient::entity::select::nox_index_learning_time_offset::state::12%]", + "60": "[%key:component::airgradient::entity::select::nox_index_learning_time_offset::state::60%]", + "120": "[%key:component::airgradient::entity::select::nox_index_learning_time_offset::state::120%]", + "360": "[%key:component::airgradient::entity::select::nox_index_learning_time_offset::state::360%]", + "720": "[%key:component::airgradient::entity::select::nox_index_learning_time_offset::state::720%]" + } + }, + "co2_automatic_baseline_calibration": { + "name": "CO2 automatic baseline calibration", + "state": { + "8": "8 days", + "30": "30 days", + "90": "90 days", + "180": "180 days", + "0": "[%key:common::state::off%]" + } } }, "sensor": { @@ -98,10 +128,10 @@ "name": "Carbon dioxide automatic baseline calibration" }, "nox_learning_offset": { - "name": "NOx learning offset" + "name": "[%key:component::airgradient::entity::select::nox_index_learning_time_offset::name%]" }, "tvoc_learning_offset": { - "name": "VOC learning offset" + "name": "[%key:component::airgradient::entity::select::voc_index_learning_time_offset::name%]" }, "led_bar_mode": { "name": "[%key:component::airgradient::entity::select::led_bar_mode::name%]", diff --git a/tests/components/airgradient/snapshots/test_select.ambr b/tests/components/airgradient/snapshots/test_select.ambr index 409eae52225..19cdc2134fc 100644 --- a/tests/components/airgradient/snapshots/test_select.ambr +++ b/tests/components/airgradient/snapshots/test_select.ambr @@ -1,4 +1,65 @@ # serializer version: 1 +# name: test_all_entities[indoor][select.airgradient_co2_automatic_baseline_calibration-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + '8', + '30', + '90', + '180', + 'off', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.airgradient_co2_automatic_baseline_calibration', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'CO2 automatic baseline calibration', + 'platform': 'airgradient', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'co2_automatic_baseline_calibration', + 'unique_id': '84fce612f5b8-co2_automatic_baseline_calibration', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[indoor][select.airgradient_co2_automatic_baseline_calibration-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Airgradient CO2 automatic baseline calibration', + 'options': list([ + '8', + '30', + '90', + '180', + 'off', + ]), + }), + 'context': , + 'entity_id': 'select.airgradient_co2_automatic_baseline_calibration', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '8', + }) +# --- # name: test_all_entities[indoor][select.airgradient_configuration_source-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -221,6 +282,189 @@ 'state': 'co2', }) # --- +# name: test_all_entities[indoor][select.airgradient_nox_index_learning_offset-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + '12', + '60', + '120', + '360', + '720', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.airgradient_nox_index_learning_offset', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'NOx index learning offset', + 'platform': 'airgradient', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'nox_index_learning_time_offset', + 'unique_id': '84fce612f5b8-nox_index_learning_time_offset', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[indoor][select.airgradient_nox_index_learning_offset-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Airgradient NOx index learning offset', + 'options': list([ + '12', + '60', + '120', + '360', + '720', + ]), + }), + 'context': , + 'entity_id': 'select.airgradient_nox_index_learning_offset', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '12', + }) +# --- +# name: test_all_entities[indoor][select.airgradient_voc_index_learning_offset-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + '12', + '60', + '120', + '360', + '720', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.airgradient_voc_index_learning_offset', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'VOC index learning offset', + 'platform': 'airgradient', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'voc_index_learning_time_offset', + 'unique_id': '84fce612f5b8-voc_index_learning_time_offset', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[indoor][select.airgradient_voc_index_learning_offset-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Airgradient VOC index learning offset', + 'options': list([ + '12', + '60', + '120', + '360', + '720', + ]), + }), + 'context': , + 'entity_id': 'select.airgradient_voc_index_learning_offset', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '12', + }) +# --- +# name: test_all_entities[outdoor][select.airgradient_co2_automatic_baseline_calibration-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + '8', + '30', + '90', + '180', + 'off', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.airgradient_co2_automatic_baseline_calibration', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'CO2 automatic baseline calibration', + 'platform': 'airgradient', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'co2_automatic_baseline_calibration', + 'unique_id': '84fce612f5b8-co2_automatic_baseline_calibration', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[outdoor][select.airgradient_co2_automatic_baseline_calibration-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Airgradient CO2 automatic baseline calibration', + 'options': list([ + '8', + '30', + '90', + '180', + 'off', + ]), + }), + 'context': , + 'entity_id': 'select.airgradient_co2_automatic_baseline_calibration', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '8', + }) +# --- # name: test_all_entities[outdoor][select.airgradient_configuration_source-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -276,3 +520,125 @@ 'state': 'local', }) # --- +# name: test_all_entities[outdoor][select.airgradient_nox_index_learning_offset-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + '12', + '60', + '120', + '360', + '720', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.airgradient_nox_index_learning_offset', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'NOx index learning offset', + 'platform': 'airgradient', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'nox_index_learning_time_offset', + 'unique_id': '84fce612f5b8-nox_index_learning_time_offset', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[outdoor][select.airgradient_nox_index_learning_offset-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Airgradient NOx index learning offset', + 'options': list([ + '12', + '60', + '120', + '360', + '720', + ]), + }), + 'context': , + 'entity_id': 'select.airgradient_nox_index_learning_offset', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '12', + }) +# --- +# name: test_all_entities[outdoor][select.airgradient_voc_index_learning_offset-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + '12', + '60', + '120', + '360', + '720', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.airgradient_voc_index_learning_offset', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'VOC index learning offset', + 'platform': 'airgradient', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'voc_index_learning_time_offset', + 'unique_id': '84fce612f5b8-voc_index_learning_time_offset', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[outdoor][select.airgradient_voc_index_learning_offset-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Airgradient VOC index learning offset', + 'options': list([ + '12', + '60', + '120', + '360', + '720', + ]), + }), + 'context': , + 'entity_id': 'select.airgradient_voc_index_learning_offset', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '12', + }) +# --- diff --git a/tests/components/airgradient/snapshots/test_sensor.ambr b/tests/components/airgradient/snapshots/test_sensor.ambr index c3d14c7d8fc..ff83fdcc111 100644 --- a/tests/components/airgradient/snapshots/test_sensor.ambr +++ b/tests/components/airgradient/snapshots/test_sensor.ambr @@ -462,7 +462,7 @@ 'state': '1', }) # --- -# name: test_all_entities[indoor][sensor.airgradient_nox_learning_offset-entry] +# name: test_all_entities[indoor][sensor.airgradient_nox_index_learning_offset-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -474,7 +474,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': , - 'entity_id': 'sensor.airgradient_nox_learning_offset', + 'entity_id': 'sensor.airgradient_nox_index_learning_offset', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -486,7 +486,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'NOx learning offset', + 'original_name': 'NOx index learning offset', 'platform': 'airgradient', 'previous_unique_id': None, 'supported_features': 0, @@ -495,15 +495,15 @@ 'unit_of_measurement': , }) # --- -# name: test_all_entities[indoor][sensor.airgradient_nox_learning_offset-state] +# name: test_all_entities[indoor][sensor.airgradient_nox_index_learning_offset-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'duration', - 'friendly_name': 'Airgradient NOx learning offset', + 'friendly_name': 'Airgradient NOx index learning offset', 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.airgradient_nox_learning_offset', + 'entity_id': 'sensor.airgradient_nox_index_learning_offset', 'last_changed': , 'last_reported': , 'last_updated': , @@ -964,7 +964,7 @@ 'state': '99', }) # --- -# name: test_all_entities[indoor][sensor.airgradient_voc_learning_offset-entry] +# name: test_all_entities[indoor][sensor.airgradient_voc_index_learning_offset-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -976,7 +976,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': , - 'entity_id': 'sensor.airgradient_voc_learning_offset', + 'entity_id': 'sensor.airgradient_voc_index_learning_offset', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -988,7 +988,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'VOC learning offset', + 'original_name': 'VOC index learning offset', 'platform': 'airgradient', 'previous_unique_id': None, 'supported_features': 0, @@ -997,15 +997,15 @@ 'unit_of_measurement': , }) # --- -# name: test_all_entities[indoor][sensor.airgradient_voc_learning_offset-state] +# name: test_all_entities[indoor][sensor.airgradient_voc_index_learning_offset-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'duration', - 'friendly_name': 'Airgradient VOC learning offset', + 'friendly_name': 'Airgradient VOC index learning offset', 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.airgradient_voc_learning_offset', + 'entity_id': 'sensor.airgradient_voc_index_learning_offset', 'last_changed': , 'last_reported': , 'last_updated': , @@ -1109,7 +1109,7 @@ 'state': '1', }) # --- -# name: test_all_entities[outdoor][sensor.airgradient_nox_learning_offset-entry] +# name: test_all_entities[outdoor][sensor.airgradient_nox_index_learning_offset-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1121,7 +1121,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': , - 'entity_id': 'sensor.airgradient_nox_learning_offset', + 'entity_id': 'sensor.airgradient_nox_index_learning_offset', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -1133,7 +1133,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'NOx learning offset', + 'original_name': 'NOx index learning offset', 'platform': 'airgradient', 'previous_unique_id': None, 'supported_features': 0, @@ -1142,15 +1142,15 @@ 'unit_of_measurement': , }) # --- -# name: test_all_entities[outdoor][sensor.airgradient_nox_learning_offset-state] +# name: test_all_entities[outdoor][sensor.airgradient_nox_index_learning_offset-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'duration', - 'friendly_name': 'Airgradient NOx learning offset', + 'friendly_name': 'Airgradient NOx index learning offset', 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.airgradient_nox_learning_offset', + 'entity_id': 'sensor.airgradient_nox_index_learning_offset', 'last_changed': , 'last_reported': , 'last_updated': , @@ -1357,7 +1357,7 @@ 'state': '49', }) # --- -# name: test_all_entities[outdoor][sensor.airgradient_voc_learning_offset-entry] +# name: test_all_entities[outdoor][sensor.airgradient_voc_index_learning_offset-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1369,7 +1369,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': , - 'entity_id': 'sensor.airgradient_voc_learning_offset', + 'entity_id': 'sensor.airgradient_voc_index_learning_offset', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -1381,7 +1381,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'VOC learning offset', + 'original_name': 'VOC index learning offset', 'platform': 'airgradient', 'previous_unique_id': None, 'supported_features': 0, @@ -1390,15 +1390,15 @@ 'unit_of_measurement': , }) # --- -# name: test_all_entities[outdoor][sensor.airgradient_voc_learning_offset-state] +# name: test_all_entities[outdoor][sensor.airgradient_voc_index_learning_offset-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'duration', - 'friendly_name': 'Airgradient VOC learning offset', + 'friendly_name': 'Airgradient VOC index learning offset', 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.airgradient_voc_learning_offset', + 'entity_id': 'sensor.airgradient_voc_index_learning_offset', 'last_changed': , 'last_reported': , 'last_updated': , diff --git a/tests/components/airgradient/test_select.py b/tests/components/airgradient/test_select.py index b4294112062..61679a15c07 100644 --- a/tests/components/airgradient/test_select.py +++ b/tests/components/airgradient/test_select.py @@ -83,7 +83,7 @@ async def test_cloud_creates_no_number( async_fire_time_changed(hass) await hass.async_block_till_done() - assert len(hass.states.async_all()) == 4 + assert len(hass.states.async_all()) == 7 mock_cloud_airgradient_client.get_config.return_value = Config.from_json( load_fixture("get_config_cloud.json", DOMAIN) From 36d8ffa79ab602c937f8f68621a6e96cfbe0ef3c Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 26 Jun 2024 12:19:04 +0200 Subject: [PATCH 1261/1445] Force alias when importing media player PLATFORM_SCHEMA (#120537) --- homeassistant/components/aquostv/media_player.py | 4 ++-- homeassistant/components/bluesound/media_player.py | 4 ++-- homeassistant/components/channels/media_player.py | 4 ++-- homeassistant/components/clementine/media_player.py | 4 ++-- homeassistant/components/cmus/media_player.py | 4 ++-- homeassistant/components/denon/media_player.py | 4 ++-- homeassistant/components/emby/media_player.py | 4 ++-- homeassistant/components/group/media_player.py | 4 ++-- homeassistant/components/gstreamer/media_player.py | 4 ++-- homeassistant/components/harman_kardon_avr/media_player.py | 4 ++-- homeassistant/components/horizon/media_player.py | 4 ++-- homeassistant/components/itunes/media_player.py | 4 ++-- homeassistant/components/kef/media_player.py | 4 ++-- homeassistant/components/kodi/media_player.py | 4 ++-- homeassistant/components/lg_netcast/media_player.py | 4 ++-- homeassistant/components/mediaroom/media_player.py | 4 ++-- homeassistant/components/mpd/media_player.py | 4 ++-- homeassistant/components/nad/media_player.py | 4 ++-- homeassistant/components/onkyo/media_player.py | 4 ++-- homeassistant/components/panasonic_bluray/media_player.py | 4 ++-- homeassistant/components/pioneer/media_player.py | 4 ++-- homeassistant/components/pjlink/media_player.py | 4 ++-- homeassistant/components/russound_rio/media_player.py | 4 ++-- homeassistant/components/russound_rnet/media_player.py | 4 ++-- homeassistant/components/ue_smart_radio/media_player.py | 4 ++-- homeassistant/components/universal/media_player.py | 4 ++-- homeassistant/components/vlc/media_player.py | 4 ++-- homeassistant/components/xiaomi_tv/media_player.py | 4 ++-- homeassistant/components/yamaha/media_player.py | 4 ++-- homeassistant/components/ziggo_mediabox_xl/media_player.py | 4 ++-- 30 files changed, 60 insertions(+), 60 deletions(-) diff --git a/homeassistant/components/aquostv/media_player.py b/homeassistant/components/aquostv/media_player.py index 64631ed1948..343cb6492da 100644 --- a/homeassistant/components/aquostv/media_player.py +++ b/homeassistant/components/aquostv/media_player.py @@ -10,7 +10,7 @@ import sharp_aquos_rc import voluptuous as vol from homeassistant.components.media_player import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as MEDIA_PLAYER_PLATFORM_SCHEMA, MediaPlayerEntity, MediaPlayerEntityFeature, MediaPlayerState, @@ -37,7 +37,7 @@ DEFAULT_PASSWORD = "password" DEFAULT_TIMEOUT = 0.5 DEFAULT_RETRIES = 2 -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = MEDIA_PLAYER_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/bluesound/media_player.py b/homeassistant/components/bluesound/media_player.py index 73ce963d481..0e752ac1f72 100644 --- a/homeassistant/components/bluesound/media_player.py +++ b/homeassistant/components/bluesound/media_player.py @@ -18,7 +18,7 @@ import xmltodict from homeassistant.components import media_source from homeassistant.components.media_player import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as MEDIA_PLAYER_PLATFORM_SCHEMA, BrowseMedia, MediaPlayerEntity, MediaPlayerEntityFeature, @@ -70,7 +70,7 @@ UPDATE_CAPTURE_INTERVAL = timedelta(minutes=30) UPDATE_PRESETS_INTERVAL = timedelta(minutes=30) UPDATE_SERVICES_INTERVAL = timedelta(minutes=30) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = MEDIA_PLAYER_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_HOSTS): vol.All( cv.ensure_list, diff --git a/homeassistant/components/channels/media_player.py b/homeassistant/components/channels/media_player.py index 2b8fc4a2b3e..07ed8ce7d66 100644 --- a/homeassistant/components/channels/media_player.py +++ b/homeassistant/components/channels/media_player.py @@ -8,7 +8,7 @@ from pychannels import Channels import voluptuous as vol from homeassistant.components.media_player import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as MEDIA_PLAYER_PLATFORM_SCHEMA, MediaPlayerEntity, MediaPlayerEntityFeature, MediaPlayerState, @@ -26,7 +26,7 @@ DATA_CHANNELS = "channels" DEFAULT_NAME = "Channels" DEFAULT_PORT = 57000 -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = MEDIA_PLAYER_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/clementine/media_player.py b/homeassistant/components/clementine/media_player.py index 84052aa64b9..233ffc840c0 100644 --- a/homeassistant/components/clementine/media_player.py +++ b/homeassistant/components/clementine/media_player.py @@ -9,7 +9,7 @@ from clementineremote import ClementineRemote import voluptuous as vol from homeassistant.components.media_player import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as MEDIA_PLAYER_PLATFORM_SCHEMA, MediaPlayerEntity, MediaPlayerEntityFeature, MediaPlayerState, @@ -26,7 +26,7 @@ DEFAULT_PORT = 5500 SCAN_INTERVAL = timedelta(seconds=5) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = MEDIA_PLAYER_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_ACCESS_TOKEN): cv.positive_int, diff --git a/homeassistant/components/cmus/media_player.py b/homeassistant/components/cmus/media_player.py index ca9ad8f8489..d55e9ca8f0b 100644 --- a/homeassistant/components/cmus/media_player.py +++ b/homeassistant/components/cmus/media_player.py @@ -9,7 +9,7 @@ from pycmus import exceptions, remote import voluptuous as vol from homeassistant.components.media_player import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as MEDIA_PLAYER_PLATFORM_SCHEMA, MediaPlayerEntity, MediaPlayerEntityFeature, MediaPlayerState, @@ -26,7 +26,7 @@ _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = "cmus" DEFAULT_PORT = 3000 -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = MEDIA_PLAYER_PLATFORM_SCHEMA.extend( { vol.Inclusive(CONF_HOST, "remote"): cv.string, vol.Inclusive(CONF_PASSWORD, "remote"): cv.string, diff --git a/homeassistant/components/denon/media_player.py b/homeassistant/components/denon/media_player.py index 1d49323f0cc..b3b3ba97baa 100644 --- a/homeassistant/components/denon/media_player.py +++ b/homeassistant/components/denon/media_player.py @@ -8,7 +8,7 @@ import telnetlib # pylint: disable=deprecated-module import voluptuous as vol from homeassistant.components.media_player import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as MEDIA_PLAYER_PLATFORM_SCHEMA, MediaPlayerEntity, MediaPlayerEntityFeature, MediaPlayerState, @@ -38,7 +38,7 @@ SUPPORT_MEDIA_MODES = ( | MediaPlayerEntityFeature.PLAY ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = MEDIA_PLAYER_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/emby/media_player.py b/homeassistant/components/emby/media_player.py index 22d7939a14e..21ee6449c11 100644 --- a/homeassistant/components/emby/media_player.py +++ b/homeassistant/components/emby/media_player.py @@ -8,7 +8,7 @@ from pyemby import EmbyServer import voluptuous as vol from homeassistant.components.media_player import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as MEDIA_PLAYER_PLATFORM_SCHEMA, MediaPlayerEntity, MediaPlayerEntityFeature, MediaPlayerState, @@ -47,7 +47,7 @@ SUPPORT_EMBY = ( | MediaPlayerEntityFeature.PLAY ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = MEDIA_PLAYER_PLATFORM_SCHEMA.extend( { vol.Required(CONF_API_KEY): cv.string, vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, diff --git a/homeassistant/components/group/media_player.py b/homeassistant/components/group/media_player.py index 6c49f88a12f..4b71cf7f81d 100644 --- a/homeassistant/components/group/media_player.py +++ b/homeassistant/components/group/media_player.py @@ -16,7 +16,7 @@ from homeassistant.components.media_player import ( ATTR_MEDIA_VOLUME_LEVEL, ATTR_MEDIA_VOLUME_MUTED, DOMAIN, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as MEDIA_PLAYER_PLATFORM_SCHEMA, SERVICE_CLEAR_PLAYLIST, SERVICE_PLAY_MEDIA, MediaPlayerEntity, @@ -71,7 +71,7 @@ KEY_VOLUME = "volume" DEFAULT_NAME = "Media Group" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = MEDIA_PLAYER_PLATFORM_SCHEMA.extend( { vol.Required(CONF_ENTITIES): cv.entities_domain(DOMAIN), vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/gstreamer/media_player.py b/homeassistant/components/gstreamer/media_player.py index 054b31c2fbe..fd9de62c016 100644 --- a/homeassistant/components/gstreamer/media_player.py +++ b/homeassistant/components/gstreamer/media_player.py @@ -10,7 +10,7 @@ import voluptuous as vol from homeassistant.components import media_source from homeassistant.components.media_player import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as MEDIA_PLAYER_PLATFORM_SCHEMA, BrowseMedia, MediaPlayerEntity, MediaPlayerEntityFeature, @@ -30,7 +30,7 @@ CONF_PIPELINE = "pipeline" DOMAIN = "gstreamer" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = MEDIA_PLAYER_PLATFORM_SCHEMA.extend( {vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_PIPELINE): cv.string} ) diff --git a/homeassistant/components/harman_kardon_avr/media_player.py b/homeassistant/components/harman_kardon_avr/media_player.py index 815a8f52b42..b8d9f27bcf1 100644 --- a/homeassistant/components/harman_kardon_avr/media_player.py +++ b/homeassistant/components/harman_kardon_avr/media_player.py @@ -6,7 +6,7 @@ import hkavr import voluptuous as vol from homeassistant.components.media_player import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as MEDIA_PLAYER_PLATFORM_SCHEMA, MediaPlayerEntity, MediaPlayerEntityFeature, MediaPlayerState, @@ -20,7 +20,7 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType DEFAULT_NAME = "Harman Kardon AVR" DEFAULT_PORT = 10025 -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = MEDIA_PLAYER_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/horizon/media_player.py b/homeassistant/components/horizon/media_player.py index c03bcc73f41..9531f9c0ed7 100644 --- a/homeassistant/components/horizon/media_player.py +++ b/homeassistant/components/horizon/media_player.py @@ -12,7 +12,7 @@ import voluptuous as vol from homeassistant import util from homeassistant.components.media_player import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as MEDIA_PLAYER_PLATFORM_SCHEMA, MediaPlayerEntity, MediaPlayerEntityFeature, MediaPlayerState, @@ -34,7 +34,7 @@ MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(seconds=1) MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = MEDIA_PLAYER_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/itunes/media_player.py b/homeassistant/components/itunes/media_player.py index 13ad66f1417..c32ca287793 100644 --- a/homeassistant/components/itunes/media_player.py +++ b/homeassistant/components/itunes/media_player.py @@ -8,7 +8,7 @@ import requests import voluptuous as vol from homeassistant.components.media_player import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as MEDIA_PLAYER_PLATFORM_SCHEMA, MediaPlayerEntity, MediaPlayerEntityFeature, MediaPlayerState, @@ -27,7 +27,7 @@ DEFAULT_TIMEOUT = 10 DOMAIN = "itunes" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = MEDIA_PLAYER_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/kef/media_player.py b/homeassistant/components/kef/media_player.py index 04ecd633d70..ad335499ba4 100644 --- a/homeassistant/components/kef/media_player.py +++ b/homeassistant/components/kef/media_player.py @@ -13,7 +13,7 @@ from getmac import get_mac_address import voluptuous as vol from homeassistant.components.media_player import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as MEDIA_PLAYER_PLATFORM_SCHEMA, MediaPlayerEntity, MediaPlayerEntityFeature, MediaPlayerState, @@ -59,7 +59,7 @@ SERVICE_UPDATE_DSP = "update_dsp" DSP_SCAN_INTERVAL = timedelta(seconds=3600) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = MEDIA_PLAYER_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Required(CONF_TYPE): vol.In(["LS50", "LSX"]), diff --git a/homeassistant/components/kodi/media_player.py b/homeassistant/components/kodi/media_player.py index 3ba5804f8b3..290b3b1e566 100644 --- a/homeassistant/components/kodi/media_player.py +++ b/homeassistant/components/kodi/media_player.py @@ -15,7 +15,7 @@ import voluptuous as vol from homeassistant.components import media_source from homeassistant.components.media_player import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as MEDIA_PLAYER_PLATFORM_SCHEMA, BrowseError, BrowseMedia, MediaPlayerEntity, @@ -118,7 +118,7 @@ MAP_KODI_MEDIA_TYPES: dict[MediaType | str, str] = { } -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = MEDIA_PLAYER_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_NAME): cv.string, diff --git a/homeassistant/components/lg_netcast/media_player.py b/homeassistant/components/lg_netcast/media_player.py index 3fc07cab12b..4dc694cd085 100644 --- a/homeassistant/components/lg_netcast/media_player.py +++ b/homeassistant/components/lg_netcast/media_player.py @@ -10,7 +10,7 @@ from requests import RequestException import voluptuous as vol from homeassistant.components.media_player import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as MEDIA_PLAYER_PLATFORM_SCHEMA, MediaPlayerDeviceClass, MediaPlayerEntity, MediaPlayerEntityFeature, @@ -49,7 +49,7 @@ SUPPORT_LGTV = ( | MediaPlayerEntityFeature.STOP ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = MEDIA_PLAYER_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_ON_ACTION): cv.SCRIPT_SCHEMA, vol.Required(CONF_HOST): cv.string, diff --git a/homeassistant/components/mediaroom/media_player.py b/homeassistant/components/mediaroom/media_player.py index 22417adcf51..8e60609fbac 100644 --- a/homeassistant/components/mediaroom/media_player.py +++ b/homeassistant/components/mediaroom/media_player.py @@ -15,7 +15,7 @@ from pymediaroom import ( import voluptuous as vol from homeassistant.components.media_player import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as MEDIA_PLAYER_PLATFORM_SCHEMA, MediaPlayerEntity, MediaPlayerEntityFeature, MediaPlayerState, @@ -46,7 +46,7 @@ MEDIA_TYPE_MEDIAROOM = "mediaroom" SIGNAL_STB_NOTIFY = "mediaroom_stb_discovered" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = MEDIA_PLAYER_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_HOST): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/mpd/media_player.py b/homeassistant/components/mpd/media_player.py index 204bbc7f499..0c4a2224e63 100644 --- a/homeassistant/components/mpd/media_player.py +++ b/homeassistant/components/mpd/media_player.py @@ -17,7 +17,7 @@ import voluptuous as vol from homeassistant.components import media_source from homeassistant.components.media_player import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as MEDIA_PLAYER_PLATFORM_SCHEMA, BrowseMedia, MediaPlayerEntity, MediaPlayerEntityFeature, @@ -61,7 +61,7 @@ SUPPORT_MPD = ( | MediaPlayerEntityFeature.BROWSE_MEDIA ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = MEDIA_PLAYER_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/nad/media_player.py b/homeassistant/components/nad/media_player.py index fa9ce4dd08e..e3c22b42d28 100644 --- a/homeassistant/components/nad/media_player.py +++ b/homeassistant/components/nad/media_player.py @@ -6,7 +6,7 @@ from nad_receiver import NADReceiver, NADReceiverTCP, NADReceiverTelnet import voluptuous as vol from homeassistant.components.media_player import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as MEDIA_PLAYER_PLATFORM_SCHEMA, MediaPlayerEntity, MediaPlayerEntityFeature, MediaPlayerState, @@ -43,7 +43,7 @@ CONF_SOURCE_DICT = "sources" # for NADReceiver # Max value based on a C658 with an MDC HDM-2 card installed SOURCE_DICT_SCHEMA = vol.Schema({vol.Range(min=1, max=12): cv.string}) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = MEDIA_PLAYER_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_TYPE, default=DEFAULT_TYPE): vol.In( ["RS232", "Telnet", "TCP"] diff --git a/homeassistant/components/onkyo/media_player.py b/homeassistant/components/onkyo/media_player.py index 181a8117443..63e76e28dbb 100644 --- a/homeassistant/components/onkyo/media_player.py +++ b/homeassistant/components/onkyo/media_player.py @@ -11,7 +11,7 @@ import voluptuous as vol from homeassistant.components.media_player import ( DOMAIN, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as MEDIA_PLAYER_PLATFORM_SCHEMA, MediaPlayerEntity, MediaPlayerEntityFeature, MediaPlayerState, @@ -70,7 +70,7 @@ DEFAULT_SOURCES = { } DEFAULT_PLAYABLE_SOURCES = ("fm", "am", "tuner") -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = MEDIA_PLAYER_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_HOST): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/panasonic_bluray/media_player.py b/homeassistant/components/panasonic_bluray/media_player.py index a121da93486..a7cb0780ca9 100644 --- a/homeassistant/components/panasonic_bluray/media_player.py +++ b/homeassistant/components/panasonic_bluray/media_player.py @@ -8,7 +8,7 @@ from panacotta import PanasonicBD import voluptuous as vol from homeassistant.components.media_player import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as MEDIA_PLAYER_PLATFORM_SCHEMA, MediaPlayerEntity, MediaPlayerEntityFeature, MediaPlayerState, @@ -25,7 +25,7 @@ DEFAULT_NAME = "Panasonic Blu-Ray" SCAN_INTERVAL = timedelta(seconds=30) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = MEDIA_PLAYER_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/pioneer/media_player.py b/homeassistant/components/pioneer/media_player.py index 15cd3cbf303..670ccffaea7 100644 --- a/homeassistant/components/pioneer/media_player.py +++ b/homeassistant/components/pioneer/media_player.py @@ -9,7 +9,7 @@ from typing import Final import voluptuous as vol from homeassistant.components.media_player import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as MEDIA_PLAYER_PLATFORM_SCHEMA, MediaPlayerEntity, MediaPlayerEntityFeature, MediaPlayerState, @@ -33,7 +33,7 @@ DEFAULT_SOURCES: dict[str, str] = {} MAX_VOLUME = 185 MAX_SOURCE_NUMBERS = 60 -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = MEDIA_PLAYER_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/pjlink/media_player.py b/homeassistant/components/pjlink/media_player.py index ff3be3266a0..93f8ea5ad9b 100644 --- a/homeassistant/components/pjlink/media_player.py +++ b/homeassistant/components/pjlink/media_player.py @@ -7,7 +7,7 @@ from pypjlink.projector import ProjectorError import voluptuous as vol from homeassistant.components.media_player import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as MEDIA_PLAYER_PLATFORM_SCHEMA, MediaPlayerEntity, MediaPlayerEntityFeature, MediaPlayerState, @@ -22,7 +22,7 @@ from .const import CONF_ENCODING, DEFAULT_ENCODING, DEFAULT_PORT, DOMAIN ERR_PROJECTOR_UNAVAILABLE = "projector unavailable" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = MEDIA_PLAYER_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, diff --git a/homeassistant/components/russound_rio/media_player.py b/homeassistant/components/russound_rio/media_player.py index 74339153f69..faea8b7193e 100644 --- a/homeassistant/components/russound_rio/media_player.py +++ b/homeassistant/components/russound_rio/media_player.py @@ -6,7 +6,7 @@ from russound_rio import Russound import voluptuous as vol from homeassistant.components.media_player import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as MEDIA_PLAYER_PLATFORM_SCHEMA, MediaPlayerEntity, MediaPlayerEntityFeature, MediaPlayerState, @@ -23,7 +23,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = MEDIA_PLAYER_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Required(CONF_NAME): cv.string, diff --git a/homeassistant/components/russound_rnet/media_player.py b/homeassistant/components/russound_rnet/media_player.py index 3b061d5a503..a08cfbe7747 100644 --- a/homeassistant/components/russound_rnet/media_player.py +++ b/homeassistant/components/russound_rnet/media_player.py @@ -9,7 +9,7 @@ from russound import russound import voluptuous as vol from homeassistant.components.media_player import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as MEDIA_PLAYER_PLATFORM_SCHEMA, MediaPlayerEntity, MediaPlayerEntityFeature, MediaPlayerState, @@ -30,7 +30,7 @@ ZONE_SCHEMA = vol.Schema({vol.Required(CONF_NAME): cv.string}) SOURCE_SCHEMA = vol.Schema({vol.Required(CONF_NAME): cv.string}) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = MEDIA_PLAYER_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Required(CONF_NAME): cv.string, diff --git a/homeassistant/components/ue_smart_radio/media_player.py b/homeassistant/components/ue_smart_radio/media_player.py index 90afca69816..62675c62c0e 100644 --- a/homeassistant/components/ue_smart_radio/media_player.py +++ b/homeassistant/components/ue_smart_radio/media_player.py @@ -8,7 +8,7 @@ import requests import voluptuous as vol from homeassistant.components.media_player import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as MEDIA_PLAYER_PLATFORM_SCHEMA, MediaPlayerEntity, MediaPlayerEntityFeature, MediaPlayerState, @@ -31,7 +31,7 @@ PLAYBACK_DICT = { "stop": MediaPlayerState.IDLE, } -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = MEDIA_PLAYER_PLATFORM_SCHEMA.extend( {vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string} ) diff --git a/homeassistant/components/universal/media_player.py b/homeassistant/components/universal/media_player.py index e4acc6b8657..c5bd9fb50c4 100644 --- a/homeassistant/components/universal/media_player.py +++ b/homeassistant/components/universal/media_player.py @@ -36,7 +36,7 @@ from homeassistant.components.media_player import ( ATTR_SOUND_MODE_LIST, DEVICE_CLASSES_SCHEMA, DOMAIN, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as MEDIA_PLAYER_PLATFORM_SCHEMA, SERVICE_CLEAR_PLAYLIST, SERVICE_PLAY_MEDIA, SERVICE_SELECT_SOUND_MODE, @@ -119,7 +119,7 @@ STATES_ORDER_IDLE = STATES_ORDER_LOOKUP[MediaPlayerState.IDLE] ATTRS_SCHEMA = cv.schema_with_slug_keys(cv.string) CMD_SCHEMA = cv.schema_with_slug_keys(cv.SERVICE_SCHEMA) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = MEDIA_PLAYER_PLATFORM_SCHEMA.extend( { vol.Required(CONF_NAME): cv.string, vol.Optional(CONF_CHILDREN, default=[]): cv.entity_ids, diff --git a/homeassistant/components/vlc/media_player.py b/homeassistant/components/vlc/media_player.py index 53831fb8db0..cd05c919d58 100644 --- a/homeassistant/components/vlc/media_player.py +++ b/homeassistant/components/vlc/media_player.py @@ -10,7 +10,7 @@ import voluptuous as vol from homeassistant.components import media_source from homeassistant.components.media_player import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as MEDIA_PLAYER_PLATFORM_SCHEMA, BrowseMedia, MediaPlayerEntity, MediaPlayerEntityFeature, @@ -30,7 +30,7 @@ _LOGGER = logging.getLogger(__name__) CONF_ARGUMENTS = "arguments" DEFAULT_NAME = "Vlc" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = MEDIA_PLAYER_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_ARGUMENTS, default=""): cv.string, vol.Optional(CONF_NAME): cv.string, diff --git a/homeassistant/components/xiaomi_tv/media_player.py b/homeassistant/components/xiaomi_tv/media_player.py index da692d21bfc..675c802f79c 100644 --- a/homeassistant/components/xiaomi_tv/media_player.py +++ b/homeassistant/components/xiaomi_tv/media_player.py @@ -8,7 +8,7 @@ import pymitv import voluptuous as vol from homeassistant.components.media_player import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as MEDIA_PLAYER_PLATFORM_SCHEMA, MediaPlayerEntity, MediaPlayerEntityFeature, MediaPlayerState, @@ -24,7 +24,7 @@ DEFAULT_NAME = "Xiaomi TV" _LOGGER = logging.getLogger(__name__) # No host is needed for configuration, however it can be set. -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = MEDIA_PLAYER_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_HOST): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/yamaha/media_player.py b/homeassistant/components/yamaha/media_player.py index c648994c38d..1be7cb03e17 100644 --- a/homeassistant/components/yamaha/media_player.py +++ b/homeassistant/components/yamaha/media_player.py @@ -10,7 +10,7 @@ import rxv import voluptuous as vol from homeassistant.components.media_player import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as MEDIA_PLAYER_PLATFORM_SCHEMA, MediaPlayerEntity, MediaPlayerEntityFeature, MediaPlayerState, @@ -68,7 +68,7 @@ SUPPORT_YAMAHA = ( | MediaPlayerEntityFeature.SELECT_SOUND_MODE ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = MEDIA_PLAYER_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_HOST): cv.string, diff --git a/homeassistant/components/ziggo_mediabox_xl/media_player.py b/homeassistant/components/ziggo_mediabox_xl/media_player.py index 7c97d38cff3..a81a206b5b2 100644 --- a/homeassistant/components/ziggo_mediabox_xl/media_player.py +++ b/homeassistant/components/ziggo_mediabox_xl/media_player.py @@ -9,7 +9,7 @@ import voluptuous as vol from ziggo_mediabox_xl import ZiggoMediaboxXL from homeassistant.components.media_player import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as MEDIA_PLAYER_PLATFORM_SCHEMA, MediaPlayerEntity, MediaPlayerEntityFeature, MediaPlayerState, @@ -24,7 +24,7 @@ _LOGGER = logging.getLogger(__name__) DATA_KNOWN_DEVICES = "ziggo_mediabox_xl_known_devices" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = MEDIA_PLAYER_PLATFORM_SCHEMA.extend( {vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_NAME): cv.string} ) From f55ddfecf463e1452d6dd6f27231bcba504061a6 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 26 Jun 2024 12:25:02 +0200 Subject: [PATCH 1262/1445] Correct type annotations in integration sensor tests (#120541) --- tests/components/integration/test_sensor.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/components/integration/test_sensor.py b/tests/components/integration/test_sensor.py index 03df38893a2..10f921ce603 100644 --- a/tests/components/integration/test_sensor.py +++ b/tests/components/integration/test_sensor.py @@ -321,7 +321,7 @@ async def test_restore_state_failed(hass: HomeAssistant, extra_attributes) -> No ) async def test_trapezoidal( hass: HomeAssistant, - sequence: tuple[tuple[float, float, float, ...]], + sequence: tuple[tuple[float, float, float], ...], force_update: bool, ) -> None: """Test integration sensor state.""" @@ -385,7 +385,7 @@ async def test_trapezoidal( ) async def test_left( hass: HomeAssistant, - sequence: tuple[tuple[float, float, float, ...]], + sequence: tuple[tuple[float, float, float], ...], force_update: bool, ) -> None: """Test integration sensor state with left reimann method.""" @@ -452,7 +452,7 @@ async def test_left( ) async def test_right( hass: HomeAssistant, - sequence: tuple[tuple[float, float, float, ...]], + sequence: tuple[tuple[float, float, float], ...], force_update: bool, ) -> None: """Test integration sensor state with left reimann method.""" From 9bbeb5d608ebbdb23e26e3f604ecec231dc80dd6 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 26 Jun 2024 12:26:24 +0200 Subject: [PATCH 1263/1445] Add primary_config_entry attribute to device registry entries (#119959) Co-authored-by: Franck Nijhof Co-authored-by: Robert Resch --- homeassistant/components/logbook/helpers.py | 2 +- homeassistant/components/unifiprotect/data.py | 2 +- homeassistant/helpers/device_registry.py | 93 +++-- .../airgradient/snapshots/test_init.ambr | 2 + .../aosmith/snapshots/test_device.ambr | 1 + .../components/config/test_device_registry.py | 13 +- .../snapshots/test_init.ambr | 1 + .../ecovacs/snapshots/test_init.ambr | 1 + .../elgato/snapshots/test_button.ambr | 2 + .../elgato/snapshots/test_light.ambr | 3 + .../elgato/snapshots/test_sensor.ambr | 5 + .../elgato/snapshots/test_switch.ambr | 2 + .../energyzero/snapshots/test_sensor.ambr | 6 + .../snapshots/test_diagnostics.ambr | 6 + .../snapshots/test_init.ambr | 1 + .../snapshots/test_init.ambr | 98 ++++++ .../homekit_controller/test_connection.py | 2 +- .../homewizard/snapshots/test_button.ambr | 1 + .../homewizard/snapshots/test_number.ambr | 2 + .../homewizard/snapshots/test_sensor.ambr | 218 ++++++++++++ .../homewizard/snapshots/test_switch.ambr | 11 + .../snapshots/test_init.ambr | 1 + tests/components/hyperion/test_camera.py | 2 +- tests/components/hyperion/test_light.py | 2 +- tests/components/hyperion/test_sensor.py | 2 +- tests/components/hyperion/test_switch.py | 2 +- .../ista_ecotrend/snapshots/test_init.ambr | 2 + .../kitchen_sink/snapshots/test_switch.ambr | 4 + .../lamarzocco/snapshots/test_switch.ambr | 1 + tests/components/lifx/test_migration.py | 6 +- .../components/lutron_caseta/test_logbook.py | 2 +- .../mealie/snapshots/test_init.ambr | 1 + tests/components/motioneye/test_camera.py | 2 +- tests/components/mqtt/test_discovery.py | 16 +- tests/components/mqtt/test_tag.py | 4 +- .../netatmo/snapshots/test_init.ambr | 38 ++ .../netgear_lte/snapshots/test_init.ambr | 1 + .../ondilo_ico/snapshots/test_init.ambr | 2 + .../onewire/snapshots/test_binary_sensor.ambr | 22 ++ .../onewire/snapshots/test_sensor.ambr | 22 ++ .../onewire/snapshots/test_switch.ambr | 22 ++ .../renault/snapshots/test_binary_sensor.ambr | 8 + .../renault/snapshots/test_button.ambr | 8 + .../snapshots/test_device_tracker.ambr | 8 + .../renault/snapshots/test_select.ambr | 8 + .../renault/snapshots/test_sensor.ambr | 8 + .../components/rova/snapshots/test_init.ambr | 1 + .../sfr_box/snapshots/test_binary_sensor.ambr | 2 + .../sfr_box/snapshots/test_button.ambr | 1 + .../sfr_box/snapshots/test_sensor.ambr | 1 + .../snapshots/test_binary_sensor.ambr | 2 + .../tailwind/snapshots/test_button.ambr | 1 + .../tailwind/snapshots/test_cover.ambr | 2 + .../tailwind/snapshots/test_number.ambr | 1 + tests/components/tasmota/test_discovery.py | 8 +- .../components/tedee/snapshots/test_init.ambr | 1 + .../components/tedee/snapshots/test_lock.ambr | 2 + .../teslemetry/snapshots/test_init.ambr | 4 + .../tplink/snapshots/test_binary_sensor.ambr | 1 + .../tplink/snapshots/test_button.ambr | 1 + .../tplink/snapshots/test_climate.ambr | 1 + .../components/tplink/snapshots/test_fan.ambr | 1 + .../tplink/snapshots/test_number.ambr | 1 + .../tplink/snapshots/test_select.ambr | 1 + .../tplink/snapshots/test_sensor.ambr | 1 + .../tplink/snapshots/test_switch.ambr | 1 + .../twentemilieu/snapshots/test_calendar.ambr | 1 + .../twentemilieu/snapshots/test_sensor.ambr | 5 + .../uptime/snapshots/test_sensor.ambr | 1 + .../components/vesync/snapshots/test_fan.ambr | 9 + .../vesync/snapshots/test_light.ambr | 9 + .../vesync/snapshots/test_sensor.ambr | 9 + .../vesync/snapshots/test_switch.ambr | 9 + .../whois/snapshots/test_sensor.ambr | 9 + .../wled/snapshots/test_button.ambr | 1 + .../wled/snapshots/test_number.ambr | 2 + .../wled/snapshots/test_select.ambr | 4 + .../wled/snapshots/test_switch.ambr | 4 + tests/helpers/test_device_registry.py | 332 ++++++++++++++++-- tests/helpers/test_entity_platform.py | 1 + tests/helpers/test_entity_registry.py | 8 +- tests/syrupy.py | 2 + 82 files changed, 1001 insertions(+), 105 deletions(-) diff --git a/homeassistant/components/logbook/helpers.py b/homeassistant/components/logbook/helpers.py index 674f1643793..4fa0da9033a 100644 --- a/homeassistant/components/logbook/helpers.py +++ b/homeassistant/components/logbook/helpers.py @@ -58,7 +58,7 @@ def _async_config_entries_for_ids( dev_reg = dr.async_get(hass) for device_id in device_ids: if (device := dev_reg.async_get(device_id)) and device.config_entries: - config_entry_ids.update(device.config_entries) + config_entry_ids |= device.config_entries return config_entry_ids diff --git a/homeassistant/components/unifiprotect/data.py b/homeassistant/components/unifiprotect/data.py index 6b502eaa5f3..a2ef72c7008 100644 --- a/homeassistant/components/unifiprotect/data.py +++ b/homeassistant/components/unifiprotect/data.py @@ -342,7 +342,7 @@ class ProtectData: @callback def async_ufp_instance_for_config_entry_ids( - hass: HomeAssistant, config_entry_ids: list[str] + hass: HomeAssistant, config_entry_ids: set[str] ) -> ProtectApiClient | None: """Find the UFP instance for the config entry ids.""" return next( diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index 36249733f71..cfafa63ec3a 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -55,7 +55,7 @@ EVENT_DEVICE_REGISTRY_UPDATED: EventType[EventDeviceRegistryUpdatedData] = Event ) STORAGE_KEY = "core.device_registry" STORAGE_VERSION_MAJOR = 1 -STORAGE_VERSION_MINOR = 5 +STORAGE_VERSION_MINOR = 6 CLEANUP_DELAY = 10 @@ -145,6 +145,9 @@ DEVICE_INFO_TYPES = { DEVICE_INFO_KEYS = set.union(*(itm for itm in DEVICE_INFO_TYPES.values())) +# Integrations which may share a device with a native integration +LOW_PRIO_CONFIG_ENTRY_DOMAINS = {"homekit_controller", "matter", "mqtt", "upnp"} + class _EventDeviceRegistryUpdatedData_CreateRemove(TypedDict): """EventDeviceRegistryUpdated data for action type 'create' and 'remove'.""" @@ -273,7 +276,7 @@ class DeviceEntry: """Device Registry Entry.""" area_id: str | None = attr.ib(default=None) - config_entries: list[str] = attr.ib(factory=list) + config_entries: set[str] = attr.ib(converter=set, factory=set) configuration_url: str | None = attr.ib(default=None) connections: set[tuple[str, str]] = attr.ib(converter=set, factory=set) disabled_by: DeviceEntryDisabler | None = attr.ib(default=None) @@ -286,6 +289,7 @@ class DeviceEntry: model: str | None = attr.ib(default=None) name_by_user: str | None = attr.ib(default=None) name: str | None = attr.ib(default=None) + primary_config_entry: str | None = attr.ib(default=None) serial_number: str | None = attr.ib(default=None) suggested_area: str | None = attr.ib(default=None) sw_version: str | None = attr.ib(default=None) @@ -307,7 +311,7 @@ class DeviceEntry: return { "area_id": self.area_id, "configuration_url": self.configuration_url, - "config_entries": self.config_entries, + "config_entries": list(self.config_entries), "connections": list(self.connections), "disabled_by": self.disabled_by, "entry_type": self.entry_type, @@ -319,6 +323,7 @@ class DeviceEntry: "model": self.model, "name_by_user": self.name_by_user, "name": self.name, + "primary_config_entry": self.primary_config_entry, "serial_number": self.serial_number, "sw_version": self.sw_version, "via_device_id": self.via_device_id, @@ -347,7 +352,7 @@ class DeviceEntry: json_bytes( { "area_id": self.area_id, - "config_entries": self.config_entries, + "config_entries": list(self.config_entries), "configuration_url": self.configuration_url, "connections": list(self.connections), "disabled_by": self.disabled_by, @@ -360,6 +365,7 @@ class DeviceEntry: "model": self.model, "name_by_user": self.name_by_user, "name": self.name, + "primary_config_entry": self.primary_config_entry, "serial_number": self.serial_number, "sw_version": self.sw_version, "via_device_id": self.via_device_id, @@ -372,7 +378,7 @@ class DeviceEntry: class DeletedDeviceEntry: """Deleted Device Registry Entry.""" - config_entries: list[str] = attr.ib() + config_entries: set[str] = attr.ib() connections: set[tuple[str, str]] = attr.ib() identifiers: set[tuple[str, str]] = attr.ib() id: str = attr.ib() @@ -387,7 +393,7 @@ class DeletedDeviceEntry: """Create DeviceEntry from DeletedDeviceEntry.""" return DeviceEntry( # type ignores: likely https://github.com/python/mypy/issues/8625 - config_entries=[config_entry_id], + config_entries={config_entry_id}, # type: ignore[arg-type] connections=self.connections & connections, # type: ignore[arg-type] identifiers=self.identifiers & identifiers, # type: ignore[arg-type] id=self.id, @@ -400,7 +406,7 @@ class DeletedDeviceEntry: return json_fragment( json_bytes( { - "config_entries": self.config_entries, + "config_entries": list(self.config_entries), "connections": list(self.connections), "identifiers": list(self.identifiers), "id": self.id, @@ -473,6 +479,10 @@ class DeviceRegistryStore(storage.Store[dict[str, list[dict[str, Any]]]]): # Introduced in 2024.3 for device in old_data["devices"]: device["labels"] = device.get("labels", []) + if old_minor_version < 6: + # Introduced in 2024.7 + for device in old_data["devices"]: + device.setdefault("primary_config_entry", None) if old_major_version > 1: raise NotImplementedError @@ -790,6 +800,7 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]): device.id, allow_collisions=True, add_config_entry_id=config_entry_id, + add_config_entry=config_entry, configuration_url=configuration_url, device_info_type=device_info_type, disabled_by=disabled_by, @@ -816,6 +827,7 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]): self, device_id: str, *, + add_config_entry: ConfigEntry | UndefinedType = UNDEFINED, add_config_entry_id: str | UndefinedType = UNDEFINED, # Temporary flag so we don't blow up when collisions are implicitly introduced # by calls to async_get_or_create. Must not be set by integrations. @@ -849,6 +861,14 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]): config_entries = old.config_entries + if add_config_entry_id is not UNDEFINED and add_config_entry is UNDEFINED: + config_entry = self.hass.config_entries.async_get_entry(add_config_entry_id) + if config_entry is None: + raise HomeAssistantError( + f"Can't link device to unknown config entry {add_config_entry_id}" + ) + add_config_entry = config_entry + if merge_connections is not UNDEFINED and new_connections is not UNDEFINED: raise HomeAssistantError( "Cannot define both merge_connections and new_connections" @@ -886,32 +906,40 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]): area = ar.async_get(self.hass).async_get_or_create(suggested_area) area_id = area.id - if add_config_entry_id is not UNDEFINED: - # primary ones have to be at the start. - if device_info_type == "primary": - # Move entry to first spot - if not config_entries or config_entries[0] != add_config_entry_id: - config_entries = [add_config_entry_id] + [ - entry - for entry in config_entries - if entry != add_config_entry_id - ] + if add_config_entry is not UNDEFINED: + primary_entry_id = old.primary_config_entry + if ( + device_info_type == "primary" + and add_config_entry.entry_id != primary_entry_id + ): + if ( + primary_entry_id is None + or not ( + primary_entry := self.hass.config_entries.async_get_entry( + primary_entry_id + ) + ) + or primary_entry.domain in LOW_PRIO_CONFIG_ENTRY_DOMAINS + ): + new_values["primary_config_entry"] = add_config_entry.entry_id + old_values["primary_config_entry"] = old.primary_config_entry - # Not primary, append - elif add_config_entry_id not in config_entries: - config_entries = [*config_entries, add_config_entry_id] + if add_config_entry.entry_id not in old.config_entries: + config_entries = old.config_entries | {add_config_entry.entry_id} if ( remove_config_entry_id is not UNDEFINED and remove_config_entry_id in config_entries ): - if config_entries == [remove_config_entry_id]: + if config_entries == {remove_config_entry_id}: self.async_remove_device(device_id) return None - config_entries = [ - entry for entry in config_entries if entry != remove_config_entry_id - ] + if remove_config_entry_id == old.primary_config_entry: + new_values["primary_config_entry"] = None + old_values["primary_config_entry"] = old.primary_config_entry + + config_entries = config_entries - {remove_config_entry_id} if config_entries != old.config_entries: new_values["config_entries"] = config_entries @@ -1095,7 +1123,7 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]): for device in data["devices"]: devices[device["id"]] = DeviceEntry( area_id=device["area_id"], - config_entries=device["config_entries"], + config_entries=set(device["config_entries"]), configuration_url=device["configuration_url"], # type ignores (if tuple arg was cast): likely https://github.com/python/mypy/issues/8625 connections={ @@ -1123,6 +1151,7 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]): model=device["model"], name_by_user=device["name_by_user"], name=device["name"], + primary_config_entry=device["primary_config_entry"], serial_number=device["serial_number"], sw_version=device["sw_version"], via_device_id=device["via_device_id"], @@ -1130,7 +1159,7 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]): # Introduced in 0.111 for device in data["deleted_devices"]: deleted_devices[device["id"]] = DeletedDeviceEntry( - config_entries=device["config_entries"], + config_entries=set(device["config_entries"]), connections={tuple(conn) for conn in device["connections"]}, identifiers={tuple(iden) for iden in device["identifiers"]}, id=device["id"], @@ -1161,15 +1190,13 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]): config_entries = deleted_device.config_entries if config_entry_id not in config_entries: continue - if config_entries == [config_entry_id]: + if config_entries == {config_entry_id}: # Add a time stamp when the deleted device became orphaned self.deleted_devices[deleted_device.id] = attr.evolve( - deleted_device, orphaned_timestamp=now_time, config_entries=[] + deleted_device, orphaned_timestamp=now_time, config_entries=set() ) else: - config_entries = [ - entry for entry in config_entries if entry != config_entry_id - ] + config_entries = config_entries - {config_entry_id} # No need to reindex here since we currently # do not have a lookup by config entry self.deleted_devices[deleted_device.id] = attr.evolve( @@ -1275,8 +1302,8 @@ def async_config_entry_disabled_by_changed( if device.disabled: # Device already disabled, do not overwrite continue - if len(device.config_entries) > 1 and any( - entry_id in enabled_config_entries for entry_id in device.config_entries + if len(device.config_entries) > 1 and device.config_entries.intersection( + enabled_config_entries ): continue registry.async_update_device( diff --git a/tests/components/airgradient/snapshots/test_init.ambr b/tests/components/airgradient/snapshots/test_init.ambr index 7c2e6ce4f78..4462a996a49 100644 --- a/tests/components/airgradient/snapshots/test_init.ambr +++ b/tests/components/airgradient/snapshots/test_init.ambr @@ -23,6 +23,7 @@ 'model': 'I-9PSL', 'name': 'Airgradient', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': '84fce612f5b8', 'suggested_area': None, 'sw_version': '3.1.1', @@ -53,6 +54,7 @@ 'model': 'O-1PPT', 'name': 'Airgradient', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': '84fce60bec38', 'suggested_area': None, 'sw_version': '3.1.1', diff --git a/tests/components/aosmith/snapshots/test_device.ambr b/tests/components/aosmith/snapshots/test_device.ambr index f6e2625afdb..d563090ce9d 100644 --- a/tests/components/aosmith/snapshots/test_device.ambr +++ b/tests/components/aosmith/snapshots/test_device.ambr @@ -23,6 +23,7 @@ 'model': 'HPTS-50 200 202172000', 'name': 'My water heater', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': 'serial', 'suggested_area': 'Basement', 'sw_version': '2.14', diff --git a/tests/components/config/test_device_registry.py b/tests/components/config/test_device_registry.py index 804cf29979e..0717bb6046d 100644 --- a/tests/components/config/test_device_registry.py +++ b/tests/components/config/test_device_registry.py @@ -70,6 +70,7 @@ async def test_list_devices( "model": "model", "name_by_user": None, "name": None, + "primary_config_entry": entry.entry_id, "serial_number": None, "sw_version": None, "via_device_id": None, @@ -88,6 +89,7 @@ async def test_list_devices( "model": "model", "name_by_user": None, "name": None, + "primary_config_entry": entry.entry_id, "serial_number": None, "sw_version": None, "via_device_id": dev1, @@ -119,6 +121,7 @@ async def test_list_devices( "model": "model", "name_by_user": None, "name": None, + "primary_config_entry": entry.entry_id, "serial_number": None, "sw_version": None, "via_device_id": None, @@ -274,7 +277,7 @@ async def test_remove_config_entry_from_device( config_entry_id=entry_2.entry_id, connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, ) - assert device_entry.config_entries == [entry_1.entry_id, entry_2.entry_id] + assert device_entry.config_entries == {entry_1.entry_id, entry_2.entry_id} # Try removing a config entry from the device, it should fail because # async_remove_config_entry_device returns False @@ -293,9 +296,9 @@ async def test_remove_config_entry_from_device( assert response["result"]["config_entries"] == [entry_2.entry_id] # Check that the config entry was removed from the device - assert device_registry.async_get(device_entry.id).config_entries == [ + assert device_registry.async_get(device_entry.id).config_entries == { entry_2.entry_id - ] + } # Remove the 2nd config entry response = await ws_client.remove_device(device_entry.id, entry_2.entry_id) @@ -365,11 +368,11 @@ async def test_remove_config_entry_from_device_fails( config_entry_id=entry_3.entry_id, connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, ) - assert device_entry.config_entries == [ + assert device_entry.config_entries == { entry_1.entry_id, entry_2.entry_id, entry_3.entry_id, - ] + } fake_entry_id = "abc123" assert entry_1.entry_id != fake_entry_id diff --git a/tests/components/devolo_home_network/snapshots/test_init.ambr b/tests/components/devolo_home_network/snapshots/test_init.ambr index b042dfec2f1..8c265400643 100644 --- a/tests/components/devolo_home_network/snapshots/test_init.ambr +++ b/tests/components/devolo_home_network/snapshots/test_init.ambr @@ -27,6 +27,7 @@ 'model': 'dLAN pro 1200+ WiFi ac', 'name': 'Mock Title', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': '1234567890', 'suggested_area': None, 'sw_version': '5.6.1', diff --git a/tests/components/ecovacs/snapshots/test_init.ambr b/tests/components/ecovacs/snapshots/test_init.ambr index f47e747b1cf..3ce872e7898 100644 --- a/tests/components/ecovacs/snapshots/test_init.ambr +++ b/tests/components/ecovacs/snapshots/test_init.ambr @@ -23,6 +23,7 @@ 'model': 'DEEBOT OZMO 950 Series', 'name': 'Ozmo 950', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': 'E1234567890000000001', 'suggested_area': None, 'sw_version': None, diff --git a/tests/components/elgato/snapshots/test_button.ambr b/tests/components/elgato/snapshots/test_button.ambr index e7477540f46..77555c85a06 100644 --- a/tests/components/elgato/snapshots/test_button.ambr +++ b/tests/components/elgato/snapshots/test_button.ambr @@ -74,6 +74,7 @@ 'model': 'Elgato Key Light Mini', 'name': 'Frenck', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': 'GW24L1A02987', 'suggested_area': None, 'sw_version': '1.0.4 (229)', @@ -155,6 +156,7 @@ 'model': 'Elgato Key Light Mini', 'name': 'Frenck', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': 'GW24L1A02987', 'suggested_area': None, 'sw_version': '1.0.4 (229)', diff --git a/tests/components/elgato/snapshots/test_light.ambr b/tests/components/elgato/snapshots/test_light.ambr index e2f663d294b..8e2962fc698 100644 --- a/tests/components/elgato/snapshots/test_light.ambr +++ b/tests/components/elgato/snapshots/test_light.ambr @@ -106,6 +106,7 @@ 'model': 'Elgato Key Light', 'name': 'Frenck', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': 'CN11A1A00001', 'suggested_area': None, 'sw_version': '1.0.3 (192)', @@ -221,6 +222,7 @@ 'model': 'Elgato Light Strip', 'name': 'Frenck', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': 'CN11A1A00001', 'suggested_area': None, 'sw_version': '1.0.3 (192)', @@ -336,6 +338,7 @@ 'model': 'Elgato Light Strip', 'name': 'Frenck', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': 'CN11A1A00001', 'suggested_area': None, 'sw_version': '1.0.3 (192)', diff --git a/tests/components/elgato/snapshots/test_sensor.ambr b/tests/components/elgato/snapshots/test_sensor.ambr index 2b52d6b9f23..c2bcde7a66b 100644 --- a/tests/components/elgato/snapshots/test_sensor.ambr +++ b/tests/components/elgato/snapshots/test_sensor.ambr @@ -81,6 +81,7 @@ 'model': 'Elgato Key Light Mini', 'name': 'Frenck', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': 'GW24L1A02987', 'suggested_area': None, 'sw_version': '1.0.4 (229)', @@ -172,6 +173,7 @@ 'model': 'Elgato Key Light Mini', 'name': 'Frenck', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': 'GW24L1A02987', 'suggested_area': None, 'sw_version': '1.0.4 (229)', @@ -263,6 +265,7 @@ 'model': 'Elgato Key Light Mini', 'name': 'Frenck', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': 'GW24L1A02987', 'suggested_area': None, 'sw_version': '1.0.4 (229)', @@ -351,6 +354,7 @@ 'model': 'Elgato Key Light Mini', 'name': 'Frenck', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': 'GW24L1A02987', 'suggested_area': None, 'sw_version': '1.0.4 (229)', @@ -442,6 +446,7 @@ 'model': 'Elgato Key Light Mini', 'name': 'Frenck', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': 'GW24L1A02987', 'suggested_area': None, 'sw_version': '1.0.4 (229)', diff --git a/tests/components/elgato/snapshots/test_switch.ambr b/tests/components/elgato/snapshots/test_switch.ambr index 41f3a8f3aaf..12857a71cb3 100644 --- a/tests/components/elgato/snapshots/test_switch.ambr +++ b/tests/components/elgato/snapshots/test_switch.ambr @@ -73,6 +73,7 @@ 'model': 'Elgato Key Light Mini', 'name': 'Frenck', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': 'GW24L1A02987', 'suggested_area': None, 'sw_version': '1.0.4 (229)', @@ -153,6 +154,7 @@ 'model': 'Elgato Key Light Mini', 'name': 'Frenck', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': 'GW24L1A02987', 'suggested_area': None, 'sw_version': '1.0.4 (229)', diff --git a/tests/components/energyzero/snapshots/test_sensor.ambr b/tests/components/energyzero/snapshots/test_sensor.ambr index 23b232379df..da52526192e 100644 --- a/tests/components/energyzero/snapshots/test_sensor.ambr +++ b/tests/components/energyzero/snapshots/test_sensor.ambr @@ -64,6 +64,7 @@ 'model': None, 'name': 'Energy market price', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -138,6 +139,7 @@ 'model': None, 'name': 'Energy market price', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -209,6 +211,7 @@ 'model': None, 'name': 'Energy market price', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -280,6 +283,7 @@ 'model': None, 'name': 'Energy market price', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -351,6 +355,7 @@ 'model': None, 'name': 'Energy market price', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -425,6 +430,7 @@ 'model': None, 'name': 'Gas market price', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, diff --git a/tests/components/enphase_envoy/snapshots/test_diagnostics.ambr b/tests/components/enphase_envoy/snapshots/test_diagnostics.ambr index 008922e8d2b..acaee292237 100644 --- a/tests/components/enphase_envoy/snapshots/test_diagnostics.ambr +++ b/tests/components/enphase_envoy/snapshots/test_diagnostics.ambr @@ -48,6 +48,7 @@ 'model': 'Envoy, phases: 3, phase mode: three, net-consumption CT, production CT, storage CT', 'name': 'Envoy <>', 'name_by_user': None, + 'primary_config_entry': '45a36e55aaddb2007c5f6602e0c38e72', 'serial_number': '<>', 'suggested_area': None, 'sw_version': '7.1.2', @@ -3772,6 +3773,7 @@ 'model': 'Inverter', 'name': 'Inverter 1', 'name_by_user': None, + 'primary_config_entry': '45a36e55aaddb2007c5f6602e0c38e72', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -4043,6 +4045,7 @@ 'model': 'Envoy, phases: 3, phase mode: three, net-consumption CT, production CT, storage CT', 'name': 'Envoy <>', 'name_by_user': None, + 'primary_config_entry': '45a36e55aaddb2007c5f6602e0c38e72', 'serial_number': '<>', 'suggested_area': None, 'sw_version': '7.1.2', @@ -7767,6 +7770,7 @@ 'model': 'Inverter', 'name': 'Inverter 1', 'name_by_user': None, + 'primary_config_entry': '45a36e55aaddb2007c5f6602e0c38e72', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -8078,6 +8082,7 @@ 'model': 'Envoy, phases: 3, phase mode: three, net-consumption CT, production CT, storage CT', 'name': 'Envoy <>', 'name_by_user': None, + 'primary_config_entry': '45a36e55aaddb2007c5f6602e0c38e72', 'serial_number': '<>', 'suggested_area': None, 'sw_version': '7.1.2', @@ -11802,6 +11807,7 @@ 'model': 'Inverter', 'name': 'Inverter 1', 'name_by_user': None, + 'primary_config_entry': '45a36e55aaddb2007c5f6602e0c38e72', 'serial_number': None, 'suggested_area': None, 'sw_version': None, diff --git a/tests/components/gardena_bluetooth/snapshots/test_init.ambr b/tests/components/gardena_bluetooth/snapshots/test_init.ambr index 82e17896d60..8cd77136f8f 100644 --- a/tests/components/gardena_bluetooth/snapshots/test_init.ambr +++ b/tests/components/gardena_bluetooth/snapshots/test_init.ambr @@ -23,6 +23,7 @@ 'model': 'Mock Model', 'name': 'Mock Title', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '1.2.3', diff --git a/tests/components/homekit_controller/snapshots/test_init.ambr b/tests/components/homekit_controller/snapshots/test_init.ambr index c52bf2c3b27..394a442787d 100644 --- a/tests/components/homekit_controller/snapshots/test_init.ambr +++ b/tests/components/homekit_controller/snapshots/test_init.ambr @@ -26,6 +26,7 @@ 'model': 'AP2', 'name': 'Airversa AP2 1808', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': '1234', 'suggested_area': None, 'sw_version': '0.8.16', @@ -622,6 +623,7 @@ 'model': 'T8010', 'name': 'eufy HomeBase2-0AAA', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': 'A0000A000000000A', 'suggested_area': None, 'sw_version': '2.1.6', @@ -695,6 +697,7 @@ 'model': 'T8113', 'name': 'eufyCam2-0000', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': 'A0000A000000000D', 'suggested_area': None, 'sw_version': '1.6.7', @@ -936,6 +939,7 @@ 'model': 'T8113', 'name': 'eufyCam2-000A', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': 'A0000A000000000B', 'suggested_area': None, 'sw_version': '1.6.7', @@ -1177,6 +1181,7 @@ 'model': 'T8113', 'name': 'eufyCam2-000A', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': 'A0000A000000000C', 'suggested_area': None, 'sw_version': '1.6.7', @@ -1422,6 +1427,7 @@ 'model': 'HE1-G01', 'name': 'Aqara-Hub-E1-00A0', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': '00aa00000a0', 'suggested_area': None, 'sw_version': '3.3.0', @@ -1628,6 +1634,7 @@ 'model': 'AS006', 'name': 'Contact Sensor', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': '158d0007c59c6a', 'suggested_area': None, 'sw_version': '0', @@ -1792,6 +1799,7 @@ 'model': 'ZHWA11LM', 'name': 'Aqara Hub-1563', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': '0000000123456789', 'suggested_area': None, 'sw_version': '1.4.7', @@ -2067,6 +2075,7 @@ 'model': 'AR004', 'name': 'Programmable Switch', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': '111a1111a1a111', 'suggested_area': None, 'sw_version': '9', @@ -2190,6 +2199,7 @@ 'model': 'ABC1000', 'name': 'ArloBabyA0', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': '00A0000000000', 'suggested_area': None, 'sw_version': '1.10.931', @@ -2674,6 +2684,7 @@ 'model': 'CS-IWO', 'name': 'InWall Outlet-0394DE', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': '1020301376', 'suggested_area': None, 'sw_version': '1.0.0', @@ -3103,6 +3114,7 @@ 'model': 'REMOTE SENSOR', 'name': 'Basement', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': 'AB3C', 'suggested_area': None, 'sw_version': '1.0.0', @@ -3262,6 +3274,7 @@ 'model': 'ecobee3', 'name': 'HomeW', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': '123456789012', 'suggested_area': None, 'sw_version': '4.2.394', @@ -3716,6 +3729,7 @@ 'model': 'REMOTE SENSOR', 'name': 'Kitchen', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': 'AB1C', 'suggested_area': None, 'sw_version': '1.0.0', @@ -3875,6 +3889,7 @@ 'model': 'REMOTE SENSOR', 'name': 'Porch', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': 'AB2C', 'suggested_area': None, 'sw_version': '1.0.0', @@ -4038,6 +4053,7 @@ 'model': 'ecobee3', 'name': 'HomeW', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': '123456789012', 'suggested_area': None, 'sw_version': '4.2.394', @@ -4496,6 +4512,7 @@ 'model': 'REMOTE SENSOR', 'name': 'Basement', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': 'AB3C', 'suggested_area': None, 'sw_version': '1.0.0', @@ -4610,6 +4627,7 @@ 'model': 'ecobee3', 'name': 'HomeW', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': '123456789012', 'suggested_area': None, 'sw_version': '4.2.394', @@ -4891,6 +4909,7 @@ 'model': 'REMOTE SENSOR', 'name': 'Kitchen', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': 'AB1C', 'suggested_area': None, 'sw_version': '1.0.0', @@ -5050,6 +5069,7 @@ 'model': 'REMOTE SENSOR', 'name': 'Porch', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': 'AB2C', 'suggested_area': None, 'sw_version': '1.0.0', @@ -5213,6 +5233,7 @@ 'model': 'ECB501', 'name': 'My ecobee', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': '123456789016', 'suggested_area': None, 'sw_version': '4.7.340214', @@ -5680,6 +5701,7 @@ 'model': 'ecobee Switch+', 'name': 'Master Fan', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': '111111111111', 'suggested_area': None, 'sw_version': '4.5.130201', @@ -5969,6 +5991,7 @@ 'model': 'Eve Degree 00AAA0000', 'name': 'Eve Degree AA11', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': 'AA00A0A00000', 'suggested_area': None, 'sw_version': '1.2.8', @@ -6325,6 +6348,7 @@ 'model': 'Eve Energy 20EAO8601', 'name': 'Eve Energy 50FF', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': 'AA00A0A00000', 'suggested_area': None, 'sw_version': '1.2.9', @@ -6663,6 +6687,7 @@ 'model': 'RavenSystem HAA', 'name': 'HAA-C718B3', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': 'C718B3-1', 'suggested_area': None, 'sw_version': '5.0.18', @@ -6868,6 +6893,7 @@ 'model': 'RavenSystem HAA', 'name': 'HAA-C718B3', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': 'C718B3-2', 'suggested_area': None, 'sw_version': '5.0.18', @@ -7303,6 +7329,7 @@ 'model': 'RYSE Shade', 'name': 'Family Room North', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': 'cover.family_door_north', 'suggested_area': None, 'sw_version': '3.6.2', @@ -7464,6 +7491,7 @@ 'model': 'Bridge', 'name': 'HASS Bridge S6', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': 'homekit.bridge', 'suggested_area': None, 'sw_version': '2024.2.0', @@ -7537,6 +7565,7 @@ 'model': 'RYSE Shade', 'name': 'Kitchen Window', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': 'cover.kitchen_window', 'suggested_area': None, 'sw_version': '3.6.2', @@ -7702,6 +7731,7 @@ 'model': 'Fan', 'name': 'Ceiling Fan', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': 'fan.ceiling_fan', 'suggested_area': None, 'sw_version': '0.104.0.dev0', @@ -7822,6 +7852,7 @@ 'model': 'Bridge', 'name': 'Home Assistant Bridge', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': 'homekit.bridge', 'suggested_area': None, 'sw_version': '0.104.0.dev0', @@ -7895,6 +7926,7 @@ 'model': 'Fan', 'name': 'Living Room Fan', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': 'fan.living_room_fan', 'suggested_area': None, 'sw_version': '0.104.0.dev0', @@ -8020,6 +8052,7 @@ 'model': 'Climate Control', 'name': '89 Living Room', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': 'climate.89_living_room', 'suggested_area': None, 'sw_version': '2024.2.0', @@ -8342,6 +8375,7 @@ 'model': 'Bridge', 'name': 'HASS Bridge S6', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': 'homekit.bridge', 'suggested_area': None, 'sw_version': '2024.2.0', @@ -8419,6 +8453,7 @@ 'model': 'Bridge', 'name': 'HASS Bridge S6', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': 'homekit.bridge', 'suggested_area': None, 'sw_version': '2024.2.0', @@ -8492,6 +8527,7 @@ 'model': '1039102', 'name': 'Laundry Smoke ED78', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': 'light.laundry_smoke_ed78', 'suggested_area': None, 'sw_version': '1.4.84', @@ -8665,6 +8701,7 @@ 'model': 'RYSE Shade', 'name': 'Family Room North', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': 'cover.family_door_north', 'suggested_area': None, 'sw_version': '3.6.2', @@ -8826,6 +8863,7 @@ 'model': 'Bridge', 'name': 'HASS Bridge S6', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': 'homekit.bridge', 'suggested_area': None, 'sw_version': '2024.2.0', @@ -8899,6 +8937,7 @@ 'model': 'RYSE Shade', 'name': 'Kitchen Window', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': 'cover.kitchen_window', 'suggested_area': None, 'sw_version': '3.6.2', @@ -9064,6 +9103,7 @@ 'model': 'Fan', 'name': 'Ceiling Fan', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': 'fan.ceiling_fan', 'suggested_area': None, 'sw_version': '0.104.0.dev0', @@ -9184,6 +9224,7 @@ 'model': 'Bridge', 'name': 'Home Assistant Bridge', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': 'homekit.bridge', 'suggested_area': None, 'sw_version': '0.104.0.dev0', @@ -9257,6 +9298,7 @@ 'model': 'Fan', 'name': 'Living Room Fan', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': 'fan.living_room_fan', 'suggested_area': None, 'sw_version': '0.104.0.dev0', @@ -9383,6 +9425,7 @@ 'model': 'Bridge', 'name': 'Home Assistant Bridge', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': 'homekit.bridge', 'suggested_area': None, 'sw_version': '0.104.0.dev0', @@ -9456,6 +9499,7 @@ 'model': 'Fan', 'name': 'Living Room Fan', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': 'fan.living_room_fan', 'suggested_area': None, 'sw_version': '0.104.0.dev0', @@ -9582,6 +9626,7 @@ 'model': 'Climate Control', 'name': '89 Living Room', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': 'climate.89_living_room', 'suggested_area': None, 'sw_version': '2024.2.0', @@ -9913,6 +9958,7 @@ 'model': 'Bridge', 'name': 'HASS Bridge S6', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': 'homekit.bridge', 'suggested_area': None, 'sw_version': '2024.2.0', @@ -9990,6 +10036,7 @@ 'model': 'Bridge', 'name': 'HASS Bridge S6', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': 'homekit.bridge', 'suggested_area': None, 'sw_version': '2024.2.0', @@ -10063,6 +10110,7 @@ 'model': 'WoHumi', 'name': 'Humidifier 182A', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': 'humidifier.humidifier_182a', 'suggested_area': None, 'sw_version': '2024.2.0', @@ -10243,6 +10291,7 @@ 'model': 'Bridge', 'name': 'HASS Bridge S6', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': 'homekit.bridge', 'suggested_area': None, 'sw_version': '2024.2.0', @@ -10316,6 +10365,7 @@ 'model': 'WoHumi', 'name': 'Humidifier 182A', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': 'humidifier.humidifier_182a', 'suggested_area': None, 'sw_version': '2024.2.0', @@ -10496,6 +10546,7 @@ 'model': 'Bridge', 'name': 'HASS Bridge S6', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': 'homekit.bridge', 'suggested_area': None, 'sw_version': '2024.2.0', @@ -10569,6 +10620,7 @@ 'model': '1039102', 'name': 'Laundry Smoke ED78', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': 'light.laundry_smoke_ed78', 'suggested_area': None, 'sw_version': '1.4.84', @@ -10757,6 +10809,7 @@ 'model': 'Daikin-fwec3a-esp32-homekit-bridge', 'name': 'Air Conditioner', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': '00000001', 'suggested_area': None, 'sw_version': '1.0.0', @@ -10955,6 +11008,7 @@ 'model': 'LTW012', 'name': 'Hue ambiance candle', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': '6623462395276914', 'suggested_area': None, 'sw_version': '1.46.13', @@ -11091,6 +11145,7 @@ 'model': 'LTW012', 'name': 'Hue ambiance candle', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': '6623462395276939', 'suggested_area': None, 'sw_version': '1.46.13', @@ -11227,6 +11282,7 @@ 'model': 'LTW012', 'name': 'Hue ambiance candle', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': '6623462403113447', 'suggested_area': None, 'sw_version': '1.46.13', @@ -11363,6 +11419,7 @@ 'model': 'LTW012', 'name': 'Hue ambiance candle', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': '6623462403233419', 'suggested_area': None, 'sw_version': '1.46.13', @@ -11499,6 +11556,7 @@ 'model': 'LTW013', 'name': 'Hue ambiance spot', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': '6623462412411853', 'suggested_area': None, 'sw_version': '1.46.13', @@ -11645,6 +11703,7 @@ 'model': 'LTW013', 'name': 'Hue ambiance spot', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': '6623462412413293', 'suggested_area': None, 'sw_version': '1.46.13', @@ -11791,6 +11850,7 @@ 'model': 'RWL021', 'name': 'Hue dimmer switch', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': '6623462389072572', 'suggested_area': None, 'sw_version': '45.1.17846', @@ -12106,6 +12166,7 @@ 'model': 'LWB010', 'name': 'Hue white lamp', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': '6623462378982941', 'suggested_area': None, 'sw_version': '1.46.13', @@ -12229,6 +12290,7 @@ 'model': 'LWB010', 'name': 'Hue white lamp', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': '6623462378983942', 'suggested_area': None, 'sw_version': '1.46.13', @@ -12352,6 +12414,7 @@ 'model': 'LWB010', 'name': 'Hue white lamp', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': '6623462379122122', 'suggested_area': None, 'sw_version': '1.46.13', @@ -12475,6 +12538,7 @@ 'model': 'LWB010', 'name': 'Hue white lamp', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': '6623462379123707', 'suggested_area': None, 'sw_version': '1.46.13', @@ -12598,6 +12662,7 @@ 'model': 'LWB010', 'name': 'Hue white lamp', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': '6623462383114163', 'suggested_area': None, 'sw_version': '1.46.13', @@ -12721,6 +12786,7 @@ 'model': 'LWB010', 'name': 'Hue white lamp', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': '6623462383114193', 'suggested_area': None, 'sw_version': '1.46.13', @@ -12844,6 +12910,7 @@ 'model': 'LWB010', 'name': 'Hue white lamp', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': '6623462385996792', 'suggested_area': None, 'sw_version': '1.46.13', @@ -12967,6 +13034,7 @@ 'model': 'BSB002', 'name': 'Philips hue - 482544', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': '123456', 'suggested_area': None, 'sw_version': '1.32.1932126170', @@ -13044,6 +13112,7 @@ 'model': 'LS1', 'name': 'Koogeek-LS1-20833F', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': 'AAAA011111111111', 'suggested_area': None, 'sw_version': '2.2.15', @@ -13186,6 +13255,7 @@ 'model': 'P1EU', 'name': 'Koogeek-P1-A00AA0', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': 'EUCP03190xxxxx48', 'suggested_area': None, 'sw_version': '2.3.7', @@ -13349,6 +13419,7 @@ 'model': 'KH02CN', 'name': 'Koogeek-SW2-187A91', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': 'CNNT061751001372', 'suggested_area': None, 'sw_version': '1.0.3', @@ -13551,6 +13622,7 @@ 'model': 'E30 2B', 'name': 'Lennox', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': 'XXXXXXXX', 'suggested_area': None, 'sw_version': '3.40.XX', @@ -13831,6 +13903,7 @@ 'model': 'OLED55B9PUA', 'name': 'LG webOS TV AF80', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': '999AAAAAA999', 'suggested_area': None, 'sw_version': '04.71.04', @@ -14010,6 +14083,7 @@ 'model': 'PD-FSQN-XX', 'name': 'Caséta® Wireless Fan Speed Control', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': '39024290', 'suggested_area': None, 'sw_version': '001.005', @@ -14130,6 +14204,7 @@ 'model': 'L-BDG2-WH', 'name': 'Smart Bridge 2', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': '12344331', 'suggested_area': None, 'sw_version': '08.08', @@ -14207,6 +14282,7 @@ 'model': 'MSS425F', 'name': 'MSS425F-15cc', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': 'HH41234', 'suggested_area': None, 'sw_version': '4.2.3', @@ -14484,6 +14560,7 @@ 'model': 'MSS565', 'name': 'MSS565-28da', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': 'BB1121', 'suggested_area': None, 'sw_version': '4.1.9', @@ -14611,6 +14688,7 @@ 'model': 'v1', 'name': 'Mysa-85dda9', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': 'AAAAAAA000', 'suggested_area': None, 'sw_version': '2.8.1', @@ -14939,6 +15017,7 @@ 'model': 'NL55', 'name': 'Nanoleaf Strip 3B32', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': 'AAAA011111111111', 'suggested_area': None, 'sw_version': '1.4.40', @@ -15209,6 +15288,7 @@ 'model': 'Netatmo Doorbell', 'name': 'Netatmo-Doorbell-g738658', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': 'g738658', 'suggested_area': None, 'sw_version': '80.0.0', @@ -15501,6 +15581,7 @@ 'model': 'Smart CO Alarm', 'name': 'Smart CO Alarm', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': '1234', 'suggested_area': None, 'sw_version': '1.0.3', @@ -15660,6 +15741,7 @@ 'model': 'Healthy Home Coach', 'name': 'Healthy Home Coach', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': 'AAAAAAAAAAAAA', 'suggested_area': None, 'sw_version': '59', @@ -15961,6 +16043,7 @@ 'model': 'SPK5 Pro', 'name': 'RainMachine-00ce4a', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': '00aa0000aa0a', 'suggested_area': None, 'sw_version': '1.0.4', @@ -16382,6 +16465,7 @@ 'model': 'RYSE Shade', 'name': 'Master Bath South', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': '1.0.0', 'suggested_area': None, 'sw_version': '3.0.8', @@ -16543,6 +16627,7 @@ 'model': 'RYSE SmartBridge', 'name': 'RYSE SmartBridge', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': '0101.3521.0436', 'suggested_area': None, 'sw_version': '1.3.0', @@ -16616,6 +16701,7 @@ 'model': 'RYSE Shade', 'name': 'RYSE SmartShade', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': '', 'suggested_area': None, 'sw_version': '', @@ -16781,6 +16867,7 @@ 'model': 'RYSE Shade', 'name': 'BR Left', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': '1.0.0', 'suggested_area': None, 'sw_version': '3.0.8', @@ -16942,6 +17029,7 @@ 'model': 'RYSE Shade', 'name': 'LR Left', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': '1.0.0', 'suggested_area': None, 'sw_version': '3.0.8', @@ -17103,6 +17191,7 @@ 'model': 'RYSE Shade', 'name': 'LR Right', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': '1.0.0', 'suggested_area': None, 'sw_version': '3.0.8', @@ -17264,6 +17353,7 @@ 'model': 'RYSE SmartBridge', 'name': 'RYSE SmartBridge', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': '0401.3521.0679', 'suggested_area': None, 'sw_version': '1.3.0', @@ -17337,6 +17427,7 @@ 'model': 'RYSE Shade', 'name': 'RZSS', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': '1.0.0', 'suggested_area': None, 'sw_version': '3.0.8', @@ -17502,6 +17593,7 @@ 'model': 'BE479CAM619', 'name': 'SENSE ', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': 'AAAAAAA000', 'suggested_area': None, 'sw_version': '004.027.000', @@ -17620,6 +17712,7 @@ 'model': 'SIMPLEconnect', 'name': 'SIMPLEconnect Fan-06F674', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': '1234567890abcd', 'suggested_area': None, 'sw_version': '', @@ -17795,6 +17888,7 @@ 'model': 'VELUX Gateway', 'name': 'VELUX Gateway', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': 'a1a11a1', 'suggested_area': None, 'sw_version': '70', @@ -17868,6 +17962,7 @@ 'model': 'VELUX Sensor', 'name': 'VELUX Sensor', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': 'a11b111', 'suggested_area': None, 'sw_version': '16', @@ -18076,6 +18171,7 @@ 'model': 'VELUX Window', 'name': 'VELUX Window', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': '1111111a114a111a', 'suggested_area': None, 'sw_version': '48', @@ -18196,6 +18292,7 @@ 'model': 'Flowerbud', 'name': 'VOCOlinc-Flowerbud-0d324b', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': 'AM01121849000327', 'suggested_area': None, 'sw_version': '3.121.2', @@ -18500,6 +18597,7 @@ 'model': 'VP3', 'name': 'VOCOlinc-VP3-123456', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': 'EU0121203xxxxx07', 'suggested_area': None, 'sw_version': '1.101.2', diff --git a/tests/components/homekit_controller/test_connection.py b/tests/components/homekit_controller/test_connection.py index 0f2cdb7c9db..0a77509d675 100644 --- a/tests/components/homekit_controller/test_connection.py +++ b/tests/components/homekit_controller/test_connection.py @@ -118,7 +118,7 @@ async def test_migrate_device_id_no_serial_skip_if_other_owner( bridge = device_registry.async_get(bridge.id) assert bridge.identifiers == variant.before - assert bridge.config_entries == [entry.entry_id] + assert bridge.config_entries == {entry.entry_id} @pytest.mark.parametrize("variant", DEVICE_MIGRATION_TESTS) diff --git a/tests/components/homewizard/snapshots/test_button.ambr b/tests/components/homewizard/snapshots/test_button.ambr index 5ab108d344c..eabaeb648aa 100644 --- a/tests/components/homewizard/snapshots/test_button.ambr +++ b/tests/components/homewizard/snapshots/test_button.ambr @@ -74,6 +74,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', diff --git a/tests/components/homewizard/snapshots/test_number.ambr b/tests/components/homewizard/snapshots/test_number.ambr index a9c9e45098d..f292847f2a2 100644 --- a/tests/components/homewizard/snapshots/test_number.ambr +++ b/tests/components/homewizard/snapshots/test_number.ambr @@ -83,6 +83,7 @@ 'model': 'HWE-SKT', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.03', @@ -173,6 +174,7 @@ 'model': 'HWE-SKT', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.07', diff --git a/tests/components/homewizard/snapshots/test_sensor.ambr b/tests/components/homewizard/snapshots/test_sensor.ambr index 5e8ddc0d6be..27dfd6399c7 100644 --- a/tests/components/homewizard/snapshots/test_sensor.ambr +++ b/tests/components/homewizard/snapshots/test_sensor.ambr @@ -60,6 +60,7 @@ 'model': 'HWE-KWH1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -145,6 +146,7 @@ 'model': 'HWE-KWH1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -230,6 +232,7 @@ 'model': 'HWE-KWH1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -315,6 +318,7 @@ 'model': 'HWE-KWH1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -400,6 +404,7 @@ 'model': 'HWE-KWH1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -485,6 +490,7 @@ 'model': 'HWE-KWH1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -573,6 +579,7 @@ 'model': 'HWE-KWH1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -658,6 +665,7 @@ 'model': 'HWE-KWH1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -743,6 +751,7 @@ 'model': 'HWE-KWH1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -828,6 +837,7 @@ 'model': 'HWE-KWH1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -908,6 +918,7 @@ 'model': 'HWE-KWH1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -992,6 +1003,7 @@ 'model': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -1077,6 +1089,7 @@ 'model': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -1162,6 +1175,7 @@ 'model': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -1247,6 +1261,7 @@ 'model': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -1332,6 +1347,7 @@ 'model': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -1417,6 +1433,7 @@ 'model': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -1502,6 +1519,7 @@ 'model': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -1587,6 +1605,7 @@ 'model': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -1672,6 +1691,7 @@ 'model': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -1757,6 +1777,7 @@ 'model': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -1842,6 +1863,7 @@ 'model': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -1927,6 +1949,7 @@ 'model': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -2015,6 +2038,7 @@ 'model': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -2100,6 +2124,7 @@ 'model': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -2185,6 +2210,7 @@ 'model': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -2270,6 +2296,7 @@ 'model': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -2358,6 +2385,7 @@ 'model': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -2446,6 +2474,7 @@ 'model': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -2534,6 +2563,7 @@ 'model': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -2619,6 +2649,7 @@ 'model': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -2704,6 +2735,7 @@ 'model': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -2789,6 +2821,7 @@ 'model': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -2874,6 +2907,7 @@ 'model': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -2959,6 +2993,7 @@ 'model': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -3044,6 +3079,7 @@ 'model': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -3129,6 +3165,7 @@ 'model': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -3209,6 +3246,7 @@ 'model': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -3293,6 +3331,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -3375,6 +3414,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -3460,6 +3500,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -3545,6 +3586,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -3630,6 +3672,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -3710,6 +3753,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -3795,6 +3839,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -3880,6 +3925,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -3965,6 +4011,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -4050,6 +4097,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -4135,6 +4183,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -4220,6 +4269,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -4305,6 +4355,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -4390,6 +4441,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -4475,6 +4527,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -4560,6 +4613,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -4645,6 +4699,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -4725,6 +4780,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -4807,6 +4863,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -4895,6 +4952,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -4975,6 +5033,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -5063,6 +5122,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -5151,6 +5211,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -5239,6 +5300,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -5319,6 +5381,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -5399,6 +5462,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -5493,6 +5557,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -5578,6 +5643,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -5663,6 +5729,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -5748,6 +5815,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -5833,6 +5901,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -5913,6 +5982,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -5993,6 +6063,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -6073,6 +6144,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -6153,6 +6225,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -6233,6 +6306,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -6313,6 +6387,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -6397,6 +6472,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -6477,6 +6553,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -6557,6 +6634,7 @@ 'model': 'HWE-P1', 'name': 'Gas meter', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': 'gas_meter_G001', 'suggested_area': None, 'sw_version': None, @@ -6638,6 +6716,7 @@ 'model': 'HWE-P1', 'name': 'Heat meter', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': 'heat_meter_H001', 'suggested_area': None, 'sw_version': None, @@ -6719,6 +6798,7 @@ 'model': 'HWE-P1', 'name': 'Inlet heat meter', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': 'inlet_heat_meter_IH001', 'suggested_area': None, 'sw_version': None, @@ -6799,6 +6879,7 @@ 'model': 'HWE-P1', 'name': 'Warm water meter', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': 'warm_water_meter_WW001', 'suggested_area': None, 'sw_version': None, @@ -6880,6 +6961,7 @@ 'model': 'HWE-P1', 'name': 'Water meter', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': 'water_meter_W001', 'suggested_area': None, 'sw_version': None, @@ -6965,6 +7047,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -7047,6 +7130,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -7132,6 +7216,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -7217,6 +7302,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -7302,6 +7388,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -7382,6 +7469,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -7467,6 +7555,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -7552,6 +7641,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -7637,6 +7727,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -7722,6 +7813,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -7807,6 +7899,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -7892,6 +7985,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -7977,6 +8071,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -8062,6 +8157,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -8147,6 +8243,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -8232,6 +8329,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -8317,6 +8415,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -8397,6 +8496,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -8479,6 +8579,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -8567,6 +8668,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -8647,6 +8749,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -8735,6 +8838,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -8823,6 +8927,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -8911,6 +9016,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -8991,6 +9097,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -9071,6 +9178,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -9165,6 +9273,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -9250,6 +9359,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -9335,6 +9445,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -9420,6 +9531,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -9505,6 +9617,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -9585,6 +9698,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -9665,6 +9779,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -9745,6 +9860,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -9825,6 +9941,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -9905,6 +10022,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -9985,6 +10103,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -10069,6 +10188,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -10149,6 +10269,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -10229,6 +10350,7 @@ 'model': 'HWE-P1', 'name': 'Gas meter', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': 'gas_meter_\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', 'suggested_area': None, 'sw_version': None, @@ -10310,6 +10432,7 @@ 'model': 'HWE-P1', 'name': 'Heat meter', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': 'heat_meter_\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', 'suggested_area': None, 'sw_version': None, @@ -10391,6 +10514,7 @@ 'model': 'HWE-P1', 'name': 'Inlet heat meter', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': 'inlet_heat_meter_\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', 'suggested_area': None, 'sw_version': None, @@ -10471,6 +10595,7 @@ 'model': 'HWE-P1', 'name': 'Warm water meter', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': 'warm_water_meter_\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', 'suggested_area': None, 'sw_version': None, @@ -10552,6 +10677,7 @@ 'model': 'HWE-P1', 'name': 'Water meter', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': 'water_meter_\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', 'suggested_area': None, 'sw_version': None, @@ -10637,6 +10763,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -10719,6 +10846,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -10804,6 +10932,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -10889,6 +11018,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -10974,6 +11104,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -11059,6 +11190,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -11144,6 +11276,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -11229,6 +11362,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -11314,6 +11448,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -11399,6 +11534,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -11484,6 +11620,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -11569,6 +11706,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -11654,6 +11792,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -11739,6 +11878,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -11824,6 +11964,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -11909,6 +12050,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -11989,6 +12131,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -12077,6 +12220,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -12157,6 +12301,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -12245,6 +12390,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -12333,6 +12479,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -12421,6 +12568,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -12506,6 +12654,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -12591,6 +12740,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -12676,6 +12826,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -12761,6 +12912,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -12841,6 +12993,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -12921,6 +13074,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -13001,6 +13155,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -13081,6 +13236,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -13161,6 +13317,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -13241,6 +13398,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -13325,6 +13483,7 @@ 'model': 'HWE-SKT', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.03', @@ -13410,6 +13569,7 @@ 'model': 'HWE-SKT', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.03', @@ -13495,6 +13655,7 @@ 'model': 'HWE-SKT', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.03', @@ -13583,6 +13744,7 @@ 'model': 'HWE-SKT', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.03', @@ -13671,6 +13833,7 @@ 'model': 'HWE-SKT', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.03', @@ -13751,6 +13914,7 @@ 'model': 'HWE-SKT', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.03', @@ -13835,6 +13999,7 @@ 'model': 'HWE-SKT', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.07', @@ -13920,6 +14085,7 @@ 'model': 'HWE-SKT', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.07', @@ -14005,6 +14171,7 @@ 'model': 'HWE-SKT', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.07', @@ -14090,6 +14257,7 @@ 'model': 'HWE-SKT', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.07', @@ -14175,6 +14343,7 @@ 'model': 'HWE-SKT', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.07', @@ -14260,6 +14429,7 @@ 'model': 'HWE-SKT', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.07', @@ -14348,6 +14518,7 @@ 'model': 'HWE-SKT', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.07', @@ -14433,6 +14604,7 @@ 'model': 'HWE-SKT', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.07', @@ -14521,6 +14693,7 @@ 'model': 'HWE-SKT', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.07', @@ -14606,6 +14779,7 @@ 'model': 'HWE-SKT', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.07', @@ -14691,6 +14865,7 @@ 'model': 'HWE-SKT', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.07', @@ -14771,6 +14946,7 @@ 'model': 'HWE-SKT', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.07', @@ -14855,6 +15031,7 @@ 'model': 'HWE-WTR', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '2.03', @@ -14940,6 +15117,7 @@ 'model': 'HWE-WTR', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '2.03', @@ -15024,6 +15202,7 @@ 'model': 'HWE-WTR', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '2.03', @@ -15104,6 +15283,7 @@ 'model': 'HWE-WTR', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '2.03', @@ -15188,6 +15368,7 @@ 'model': 'SDM230-wifi', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -15273,6 +15454,7 @@ 'model': 'SDM230-wifi', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -15358,6 +15540,7 @@ 'model': 'SDM230-wifi', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -15443,6 +15626,7 @@ 'model': 'SDM230-wifi', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -15528,6 +15712,7 @@ 'model': 'SDM230-wifi', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -15613,6 +15798,7 @@ 'model': 'SDM230-wifi', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -15701,6 +15887,7 @@ 'model': 'SDM230-wifi', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -15786,6 +15973,7 @@ 'model': 'SDM230-wifi', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -15871,6 +16059,7 @@ 'model': 'SDM230-wifi', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -15956,6 +16145,7 @@ 'model': 'SDM230-wifi', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -16036,6 +16226,7 @@ 'model': 'SDM230-wifi', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -16120,6 +16311,7 @@ 'model': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -16205,6 +16397,7 @@ 'model': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -16290,6 +16483,7 @@ 'model': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -16375,6 +16569,7 @@ 'model': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -16460,6 +16655,7 @@ 'model': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -16545,6 +16741,7 @@ 'model': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -16630,6 +16827,7 @@ 'model': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -16715,6 +16913,7 @@ 'model': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -16800,6 +16999,7 @@ 'model': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -16885,6 +17085,7 @@ 'model': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -16970,6 +17171,7 @@ 'model': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -17055,6 +17257,7 @@ 'model': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -17143,6 +17346,7 @@ 'model': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -17228,6 +17432,7 @@ 'model': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -17313,6 +17518,7 @@ 'model': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -17398,6 +17604,7 @@ 'model': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -17486,6 +17693,7 @@ 'model': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -17574,6 +17782,7 @@ 'model': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -17662,6 +17871,7 @@ 'model': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -17747,6 +17957,7 @@ 'model': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -17832,6 +18043,7 @@ 'model': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -17917,6 +18129,7 @@ 'model': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -18002,6 +18215,7 @@ 'model': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -18087,6 +18301,7 @@ 'model': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -18172,6 +18387,7 @@ 'model': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -18257,6 +18473,7 @@ 'model': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -18337,6 +18554,7 @@ 'model': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', diff --git a/tests/components/homewizard/snapshots/test_switch.ambr b/tests/components/homewizard/snapshots/test_switch.ambr index 99a5bcab6cb..ba630e2f0b4 100644 --- a/tests/components/homewizard/snapshots/test_switch.ambr +++ b/tests/components/homewizard/snapshots/test_switch.ambr @@ -73,6 +73,7 @@ 'model': 'HWE-KWH1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -153,6 +154,7 @@ 'model': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -234,6 +236,7 @@ 'model': 'HWE-SKT', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.03', @@ -314,6 +317,7 @@ 'model': 'HWE-SKT', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.03', @@ -394,6 +398,7 @@ 'model': 'HWE-SKT', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.03', @@ -475,6 +480,7 @@ 'model': 'HWE-SKT', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.07', @@ -555,6 +561,7 @@ 'model': 'HWE-SKT', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.07', @@ -635,6 +642,7 @@ 'model': 'HWE-SKT', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.07', @@ -715,6 +723,7 @@ 'model': 'HWE-WTR', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '2.03', @@ -795,6 +804,7 @@ 'model': 'SDM230-wifi', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -875,6 +885,7 @@ 'model': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', diff --git a/tests/components/husqvarna_automower/snapshots/test_init.ambr b/tests/components/husqvarna_automower/snapshots/test_init.ambr index c3a7191b4b9..efe1eb8bd51 100644 --- a/tests/components/husqvarna_automower/snapshots/test_init.ambr +++ b/tests/components/husqvarna_automower/snapshots/test_init.ambr @@ -23,6 +23,7 @@ 'model': '450XH-TEST', 'name': 'Test Mower 1', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': 123, 'suggested_area': 'Garden', 'sw_version': None, diff --git a/tests/components/hyperion/test_camera.py b/tests/components/hyperion/test_camera.py index 41b66f4ad4a..0169759f328 100644 --- a/tests/components/hyperion/test_camera.py +++ b/tests/components/hyperion/test_camera.py @@ -198,7 +198,7 @@ async def test_device_info( device = device_registry.async_get_device(identifiers={(DOMAIN, device_id)}) assert device - assert device.config_entries == [TEST_CONFIG_ENTRY_ID] + assert device.config_entries == {TEST_CONFIG_ENTRY_ID} assert device.identifiers == {(DOMAIN, device_id)} assert device.manufacturer == HYPERION_MANUFACTURER_NAME assert device.model == HYPERION_MODEL_NAME diff --git a/tests/components/hyperion/test_light.py b/tests/components/hyperion/test_light.py index b7aef3ac2ac..e1e7711e702 100644 --- a/tests/components/hyperion/test_light.py +++ b/tests/components/hyperion/test_light.py @@ -803,7 +803,7 @@ async def test_device_info( device = device_registry.async_get_device(identifiers={(DOMAIN, device_id)}) assert device - assert device.config_entries == [TEST_CONFIG_ENTRY_ID] + assert device.config_entries == {TEST_CONFIG_ENTRY_ID} assert device.identifiers == {(DOMAIN, device_id)} assert device.manufacturer == HYPERION_MANUFACTURER_NAME assert device.model == HYPERION_MODEL_NAME diff --git a/tests/components/hyperion/test_sensor.py b/tests/components/hyperion/test_sensor.py index bc58c07ac7b..5ace34eaac0 100644 --- a/tests/components/hyperion/test_sensor.py +++ b/tests/components/hyperion/test_sensor.py @@ -66,7 +66,7 @@ async def test_device_info( device = device_registry.async_get_device(identifiers={(DOMAIN, device_identifer)}) assert device - assert device.config_entries == [TEST_CONFIG_ENTRY_ID] + assert device.config_entries == {TEST_CONFIG_ENTRY_ID} assert device.identifiers == {(DOMAIN, device_identifer)} assert device.manufacturer == HYPERION_MANUFACTURER_NAME assert device.model == HYPERION_MODEL_NAME diff --git a/tests/components/hyperion/test_switch.py b/tests/components/hyperion/test_switch.py index 17a1872f832..da458820c81 100644 --- a/tests/components/hyperion/test_switch.py +++ b/tests/components/hyperion/test_switch.py @@ -170,7 +170,7 @@ async def test_device_info( device = device_registry.async_get_device(identifiers={(DOMAIN, device_identifer)}) assert device - assert device.config_entries == [TEST_CONFIG_ENTRY_ID] + assert device.config_entries == {TEST_CONFIG_ENTRY_ID} assert device.identifiers == {(DOMAIN, device_identifer)} assert device.manufacturer == HYPERION_MANUFACTURER_NAME assert device.model == HYPERION_MODEL_NAME diff --git a/tests/components/ista_ecotrend/snapshots/test_init.ambr b/tests/components/ista_ecotrend/snapshots/test_init.ambr index a9d13510b54..c5dec7d9d56 100644 --- a/tests/components/ista_ecotrend/snapshots/test_init.ambr +++ b/tests/components/ista_ecotrend/snapshots/test_init.ambr @@ -23,6 +23,7 @@ 'model': 'ista EcoTrend', 'name': 'Luxemburger Str. 1', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -53,6 +54,7 @@ 'model': 'ista EcoTrend', 'name': 'Bahnhofsstr. 1A', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, diff --git a/tests/components/kitchen_sink/snapshots/test_switch.ambr b/tests/components/kitchen_sink/snapshots/test_switch.ambr index 1cd903a59d6..277b4888e05 100644 --- a/tests/components/kitchen_sink/snapshots/test_switch.ambr +++ b/tests/components/kitchen_sink/snapshots/test_switch.ambr @@ -69,6 +69,7 @@ 'model': None, 'name': 'Outlet 1', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -99,6 +100,7 @@ 'model': None, 'name': 'Power strip with 2 sockets', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -175,6 +177,7 @@ 'model': None, 'name': 'Outlet 2', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -205,6 +208,7 @@ 'model': None, 'name': 'Power strip with 2 sockets', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, diff --git a/tests/components/lamarzocco/snapshots/test_switch.ambr b/tests/components/lamarzocco/snapshots/test_switch.ambr index 09864be1d5c..0f462955a33 100644 --- a/tests/components/lamarzocco/snapshots/test_switch.ambr +++ b/tests/components/lamarzocco/snapshots/test_switch.ambr @@ -115,6 +115,7 @@ 'model': , 'name': 'GS01234', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': 'GS01234', 'suggested_area': None, 'sw_version': '1.40', diff --git a/tests/components/lifx/test_migration.py b/tests/components/lifx/test_migration.py index 62018790906..0604ee1c8a7 100644 --- a/tests/components/lifx/test_migration.py +++ b/tests/components/lifx/test_migration.py @@ -65,7 +65,7 @@ async def test_migration_device_online_end_to_end( assert migrated_entry is not None - assert device.config_entries == [migrated_entry.entry_id] + assert device.config_entries == {migrated_entry.entry_id} assert light_entity_reg.config_entry_id == migrated_entry.entry_id assert er.async_entries_for_config_entry(entity_reg, config_entry) == [] @@ -195,7 +195,7 @@ async def test_migration_device_online_end_to_end_after_downgrade( async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=20)) await hass.async_block_till_done() - assert device.config_entries == [config_entry.entry_id] + assert device.config_entries == {config_entry.entry_id} assert light_entity_reg.config_entry_id == config_entry.entry_id assert er.async_entries_for_config_entry(entity_reg, config_entry) == [] @@ -276,7 +276,7 @@ async def test_migration_device_online_end_to_end_ignores_other_devices( assert new_entry is not None assert legacy_entry is None - assert device.config_entries == [legacy_config_entry.entry_id] + assert device.config_entries == {legacy_config_entry.entry_id} assert light_entity_reg.config_entry_id == legacy_config_entry.entry_id assert ignored_entity_reg.config_entry_id == other_domain_config_entry.entry_id assert garbage_entity_reg.config_entry_id == legacy_config_entry.entry_id diff --git a/tests/components/lutron_caseta/test_logbook.py b/tests/components/lutron_caseta/test_logbook.py index 51c96b9d9a9..b6e8840c85c 100644 --- a/tests/components/lutron_caseta/test_logbook.py +++ b/tests/components/lutron_caseta/test_logbook.py @@ -111,7 +111,7 @@ async def test_humanify_lutron_caseta_button_event_integration_not_loaded( await hass.async_block_till_done() for device in device_registry.devices.values(): - if device.config_entries == [config_entry.entry_id]: + if device.config_entries == {config_entry.entry_id}: dr_device_id = device.id break diff --git a/tests/components/mealie/snapshots/test_init.ambr b/tests/components/mealie/snapshots/test_init.ambr index c2752d938e4..1333b292dac 100644 --- a/tests/components/mealie/snapshots/test_init.ambr +++ b/tests/components/mealie/snapshots/test_init.ambr @@ -23,6 +23,7 @@ 'model': None, 'name': 'Mealie', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, diff --git a/tests/components/motioneye/test_camera.py b/tests/components/motioneye/test_camera.py index ccbdc022495..0f3a7d6f904 100644 --- a/tests/components/motioneye/test_camera.py +++ b/tests/components/motioneye/test_camera.py @@ -339,7 +339,7 @@ async def test_device_info( device = device_registry.async_get_device(identifiers={device_identifier}) assert device - assert device.config_entries == [TEST_CONFIG_ENTRY_ID] + assert device.config_entries == {TEST_CONFIG_ENTRY_ID} assert device.identifiers == {device_identifier} assert device.manufacturer == MOTIONEYE_MANUFACTURER assert device.model == MOTIONEYE_MANUFACTURER diff --git a/tests/components/mqtt/test_discovery.py b/tests/components/mqtt/test_discovery.py index 23dea310199..8c51e295998 100644 --- a/tests/components/mqtt/test_discovery.py +++ b/tests/components/mqtt/test_discovery.py @@ -965,10 +965,10 @@ async def test_cleanup_device_multiple_config_entries( connections={("mac", "12:34:56:AB:CD:EF")} ) assert device_entry is not None - assert device_entry.config_entries == [ - config_entry.entry_id, + assert device_entry.config_entries == { mqtt_config_entry.entry_id, - ] + config_entry.entry_id, + } entity_entry = entity_registry.async_get("sensor.none_mqtt_sensor") assert entity_entry is not None @@ -991,7 +991,7 @@ async def test_cleanup_device_multiple_config_entries( ) assert device_entry is not None entity_entry = entity_registry.async_get("sensor.none_mqtt_sensor") - assert device_entry.config_entries == [config_entry.entry_id] + assert device_entry.config_entries == {config_entry.entry_id} assert entity_entry is None # Verify state is removed @@ -1060,10 +1060,10 @@ async def test_cleanup_device_multiple_config_entries_mqtt( connections={("mac", "12:34:56:AB:CD:EF")} ) assert device_entry is not None - assert device_entry.config_entries == [ - config_entry.entry_id, + assert device_entry.config_entries == { mqtt_config_entry.entry_id, - ] + config_entry.entry_id, + } entity_entry = entity_registry.async_get("sensor.none_mqtt_sensor") assert entity_entry is not None @@ -1084,7 +1084,7 @@ async def test_cleanup_device_multiple_config_entries_mqtt( ) assert device_entry is not None entity_entry = entity_registry.async_get("sensor.none_mqtt_sensor") - assert device_entry.config_entries == [config_entry.entry_id] + assert device_entry.config_entries == {config_entry.entry_id} assert entity_entry is None # Verify state is removed diff --git a/tests/components/mqtt/test_tag.py b/tests/components/mqtt/test_tag.py index e70c06c2c4a..0d0765258f2 100644 --- a/tests/components/mqtt/test_tag.py +++ b/tests/components/mqtt/test_tag.py @@ -587,7 +587,7 @@ async def test_cleanup_tag( identifiers={("mqtt", "helloworld")} ) assert device_entry1 is not None - assert device_entry1.config_entries == [config_entry.entry_id, mqtt_entry.entry_id] + assert device_entry1.config_entries == {config_entry.entry_id, mqtt_entry.entry_id} device_entry2 = device_registry.async_get_device(identifiers={("mqtt", "hejhopp")}) assert device_entry2 is not None @@ -599,7 +599,7 @@ async def test_cleanup_tag( identifiers={("mqtt", "helloworld")} ) assert device_entry1 is not None - assert device_entry1.config_entries == [mqtt_entry.entry_id] + assert device_entry1.config_entries == {mqtt_entry.entry_id} device_entry2 = device_registry.async_get_device(identifiers={("mqtt", "hejhopp")}) assert device_entry2 is not None mqtt_mock.async_publish.assert_not_called() diff --git a/tests/components/netatmo/snapshots/test_init.ambr b/tests/components/netatmo/snapshots/test_init.ambr index 8f4b357fc5f..38a54f507a0 100644 --- a/tests/components/netatmo/snapshots/test_init.ambr +++ b/tests/components/netatmo/snapshots/test_init.ambr @@ -23,6 +23,7 @@ 'model': 'Roller Shutter', 'name': 'Entrance Blinds', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -53,6 +54,7 @@ 'model': 'Orientable Shutter', 'name': 'Bubendorff blind', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -83,6 +85,7 @@ 'model': '2 wire light switch/dimmer', 'name': 'Unknown 00:11:22:33:00:11:45:fe', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -113,6 +116,7 @@ 'model': 'Smarther with Netatmo', 'name': 'Corridor', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': 'Corridor', 'sw_version': None, @@ -143,6 +147,7 @@ 'model': 'Connected Energy Meter', 'name': 'Consumption meter', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -173,6 +178,7 @@ 'model': 'Light switch/dimmer with neutral', 'name': 'Bathroom light', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -203,6 +209,7 @@ 'model': 'Connected Ecometer', 'name': 'Line 1', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -233,6 +240,7 @@ 'model': 'Connected Ecometer', 'name': 'Line 2', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -263,6 +271,7 @@ 'model': 'Connected Ecometer', 'name': 'Line 3', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -293,6 +302,7 @@ 'model': 'Connected Ecometer', 'name': 'Line 4', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -323,6 +333,7 @@ 'model': 'Connected Ecometer', 'name': 'Line 5', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -353,6 +364,7 @@ 'model': 'Connected Ecometer', 'name': 'Total', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -383,6 +395,7 @@ 'model': 'Connected Ecometer', 'name': 'Gas', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -413,6 +426,7 @@ 'model': 'Connected Ecometer', 'name': 'Hot water', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -443,6 +457,7 @@ 'model': 'Connected Ecometer', 'name': 'Cold water', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -473,6 +488,7 @@ 'model': 'Connected Ecometer', 'name': 'Écocompteur', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -503,6 +519,7 @@ 'model': 'Smart Indoor Camera', 'name': 'Hall', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -533,6 +550,7 @@ 'model': 'Smart Anemometer', 'name': 'Villa Garden', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -563,6 +581,7 @@ 'model': 'Smart Outdoor Camera', 'name': 'Front', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -593,6 +612,7 @@ 'model': 'Smart Video Doorbell', 'name': 'Netatmo-Doorbell', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -623,6 +643,7 @@ 'model': 'Smart Indoor Air Quality Monitor', 'name': 'Kitchen', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -653,6 +674,7 @@ 'model': 'Smart Indoor Air Quality Monitor', 'name': 'Livingroom', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -683,6 +705,7 @@ 'model': 'Smart Indoor Air Quality Monitor', 'name': 'Baby Bedroom', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -713,6 +736,7 @@ 'model': 'Smart Indoor Air Quality Monitor', 'name': 'Bedroom', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -743,6 +767,7 @@ 'model': 'Smart Indoor Air Quality Monitor', 'name': 'Parents Bedroom', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -773,6 +798,7 @@ 'model': 'Plug', 'name': 'Prise', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -803,6 +829,7 @@ 'model': 'Smart Outdoor Module', 'name': 'Villa Outdoor', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -833,6 +860,7 @@ 'model': 'Smart Indoor Module', 'name': 'Villa Bedroom', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -863,6 +891,7 @@ 'model': 'Smart Indoor Module', 'name': 'Villa Bathroom', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -893,6 +922,7 @@ 'model': 'Smart Home Weather station', 'name': 'Villa', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -923,6 +953,7 @@ 'model': 'Smart Rain Gauge', 'name': 'Villa Rain', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -953,6 +984,7 @@ 'model': 'OpenTherm Modulating Thermostat', 'name': 'Bureau Modulate', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': 'Bureau', 'sw_version': None, @@ -983,6 +1015,7 @@ 'model': 'Smart Thermostat', 'name': 'Livingroom', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': 'Livingroom', 'sw_version': None, @@ -1013,6 +1046,7 @@ 'model': 'Smart Valve', 'name': 'Valve1', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': 'Entrada', 'sw_version': None, @@ -1043,6 +1077,7 @@ 'model': 'Smart Valve', 'name': 'Valve2', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': 'Cocina', 'sw_version': None, @@ -1073,6 +1108,7 @@ 'model': 'Climate', 'name': 'MYHOME', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -1103,6 +1139,7 @@ 'model': 'Public Weather station', 'name': 'Home avg', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -1133,6 +1170,7 @@ 'model': 'Public Weather station', 'name': 'Home max', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, diff --git a/tests/components/netgear_lte/snapshots/test_init.ambr b/tests/components/netgear_lte/snapshots/test_init.ambr index 8af22f98e02..e893d36a06e 100644 --- a/tests/components/netgear_lte/snapshots/test_init.ambr +++ b/tests/components/netgear_lte/snapshots/test_init.ambr @@ -23,6 +23,7 @@ 'model': 'LM1200', 'name': 'Netgear LM1200', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': 'FFFFFFFFFFFFF', 'suggested_area': None, 'sw_version': 'EC25AFFDR07A09M4G', diff --git a/tests/components/ondilo_ico/snapshots/test_init.ambr b/tests/components/ondilo_ico/snapshots/test_init.ambr index c488b1e3c15..355c5902722 100644 --- a/tests/components/ondilo_ico/snapshots/test_init.ambr +++ b/tests/components/ondilo_ico/snapshots/test_init.ambr @@ -23,6 +23,7 @@ 'model': 'ICO', 'name': 'Pool 1', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '1.7.1-stable', @@ -53,6 +54,7 @@ 'model': 'ICO', 'name': 'Pool 2', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '1.7.1-stable', diff --git a/tests/components/onewire/snapshots/test_binary_sensor.ambr b/tests/components/onewire/snapshots/test_binary_sensor.ambr index 999794ec20d..b3d330291ab 100644 --- a/tests/components/onewire/snapshots/test_binary_sensor.ambr +++ b/tests/components/onewire/snapshots/test_binary_sensor.ambr @@ -36,6 +36,7 @@ 'model': 'DS2405', 'name': '05.111111111111', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -76,6 +77,7 @@ 'model': 'DS18S20', 'name': '10.111111111111', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -116,6 +118,7 @@ 'model': 'DS2406', 'name': '12.111111111111', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -256,6 +259,7 @@ 'model': 'DS2423', 'name': '1D.111111111111', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -296,6 +300,7 @@ 'model': 'DS2409', 'name': '1F.111111111111', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -324,6 +329,7 @@ 'model': 'DS2423', 'name': '1D.111111111111', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -364,6 +370,7 @@ 'model': 'DS1822', 'name': '22.111111111111', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -404,6 +411,7 @@ 'model': 'DS2438', 'name': '26.111111111111', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -444,6 +452,7 @@ 'model': 'DS18B20', 'name': '28.111111111111', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -484,6 +493,7 @@ 'model': 'DS18B20', 'name': '28.222222222222', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -524,6 +534,7 @@ 'model': 'DS18B20', 'name': '28.222222222223', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -564,6 +575,7 @@ 'model': 'DS2408', 'name': '29.111111111111', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -956,6 +968,7 @@ 'model': 'DS2760', 'name': '30.111111111111', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -996,6 +1009,7 @@ 'model': 'DS2413', 'name': '3A.111111111111', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -1124,6 +1138,7 @@ 'model': 'DS1825', 'name': '3B.111111111111', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -1164,6 +1179,7 @@ 'model': 'DS28EA00', 'name': '42.111111111111', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -1204,6 +1220,7 @@ 'model': 'EDS0068', 'name': '7E.111111111111', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -1244,6 +1261,7 @@ 'model': 'EDS0066', 'name': '7E.222222222222', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -1284,6 +1302,7 @@ 'model': 'DS2438', 'name': 'A6.111111111111', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -1324,6 +1343,7 @@ 'model': 'HobbyBoards_EF', 'name': 'EF.111111111111', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -1364,6 +1384,7 @@ 'model': 'HB_MOISTURE_METER', 'name': 'EF.111111111112', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -1404,6 +1425,7 @@ 'model': 'HB_HUB', 'name': 'EF.111111111113', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, diff --git a/tests/components/onewire/snapshots/test_sensor.ambr b/tests/components/onewire/snapshots/test_sensor.ambr index 59ed167197d..acf9ea6a8c8 100644 --- a/tests/components/onewire/snapshots/test_sensor.ambr +++ b/tests/components/onewire/snapshots/test_sensor.ambr @@ -36,6 +36,7 @@ 'model': 'DS2405', 'name': '05.111111111111', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -76,6 +77,7 @@ 'model': 'DS18S20', 'name': '10.111111111111', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -165,6 +167,7 @@ 'model': 'DS2406', 'name': '12.111111111111', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -315,6 +318,7 @@ 'model': 'DS2423', 'name': '1D.111111111111', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -451,6 +455,7 @@ 'model': 'DS2409', 'name': '1F.111111111111', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -479,6 +484,7 @@ 'model': 'DS2423', 'name': '1D.111111111111', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -615,6 +621,7 @@ 'model': 'DS1822', 'name': '22.111111111111', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -704,6 +711,7 @@ 'model': 'DS2438', 'name': '26.111111111111', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -1283,6 +1291,7 @@ 'model': 'DS18B20', 'name': '28.111111111111', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -1372,6 +1381,7 @@ 'model': 'DS18B20', 'name': '28.222222222222', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -1461,6 +1471,7 @@ 'model': 'DS18B20', 'name': '28.222222222223', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -1550,6 +1561,7 @@ 'model': 'DS2408', 'name': '29.111111111111', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -1590,6 +1602,7 @@ 'model': 'DS2760', 'name': '30.111111111111', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -1826,6 +1839,7 @@ 'model': 'DS2413', 'name': '3A.111111111111', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -1866,6 +1880,7 @@ 'model': 'DS1825', 'name': '3B.111111111111', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -1955,6 +1970,7 @@ 'model': 'DS28EA00', 'name': '42.111111111111', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -2044,6 +2060,7 @@ 'model': 'EDS0068', 'name': '7E.111111111111', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -2280,6 +2297,7 @@ 'model': 'EDS0066', 'name': '7E.222222222222', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -2418,6 +2436,7 @@ 'model': 'DS2438', 'name': 'A6.111111111111', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -2997,6 +3016,7 @@ 'model': 'HobbyBoards_EF', 'name': 'EF.111111111111', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -3184,6 +3204,7 @@ 'model': 'HB_MOISTURE_METER', 'name': 'EF.111111111112', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -3420,6 +3441,7 @@ 'model': 'HB_HUB', 'name': 'EF.111111111113', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, diff --git a/tests/components/onewire/snapshots/test_switch.ambr b/tests/components/onewire/snapshots/test_switch.ambr index 8fd1e2aeef6..d6cbb6f3fef 100644 --- a/tests/components/onewire/snapshots/test_switch.ambr +++ b/tests/components/onewire/snapshots/test_switch.ambr @@ -36,6 +36,7 @@ 'model': 'DS2405', 'name': '05.111111111111', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -120,6 +121,7 @@ 'model': 'DS18S20', 'name': '10.111111111111', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -160,6 +162,7 @@ 'model': 'DS2406', 'name': '12.111111111111', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -388,6 +391,7 @@ 'model': 'DS2423', 'name': '1D.111111111111', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -428,6 +432,7 @@ 'model': 'DS2409', 'name': '1F.111111111111', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -456,6 +461,7 @@ 'model': 'DS2423', 'name': '1D.111111111111', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -496,6 +502,7 @@ 'model': 'DS1822', 'name': '22.111111111111', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -536,6 +543,7 @@ 'model': 'DS2438', 'name': '26.111111111111', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -620,6 +628,7 @@ 'model': 'DS18B20', 'name': '28.111111111111', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -660,6 +669,7 @@ 'model': 'DS18B20', 'name': '28.222222222222', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -700,6 +710,7 @@ 'model': 'DS18B20', 'name': '28.222222222223', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -740,6 +751,7 @@ 'model': 'DS2408', 'name': '29.111111111111', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -1484,6 +1496,7 @@ 'model': 'DS2760', 'name': '30.111111111111', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -1524,6 +1537,7 @@ 'model': 'DS2413', 'name': '3A.111111111111', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -1652,6 +1666,7 @@ 'model': 'DS1825', 'name': '3B.111111111111', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -1692,6 +1707,7 @@ 'model': 'DS28EA00', 'name': '42.111111111111', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -1732,6 +1748,7 @@ 'model': 'EDS0068', 'name': '7E.111111111111', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -1772,6 +1789,7 @@ 'model': 'EDS0066', 'name': '7E.222222222222', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -1812,6 +1830,7 @@ 'model': 'DS2438', 'name': 'A6.111111111111', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -1896,6 +1915,7 @@ 'model': 'HobbyBoards_EF', 'name': 'EF.111111111111', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -1936,6 +1956,7 @@ 'model': 'HB_MOISTURE_METER', 'name': 'EF.111111111112', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -2328,6 +2349,7 @@ 'model': 'HB_HUB', 'name': 'EF.111111111113', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, diff --git a/tests/components/renault/snapshots/test_binary_sensor.ambr b/tests/components/renault/snapshots/test_binary_sensor.ambr index 7f30faac38e..8f49d7ef761 100644 --- a/tests/components/renault/snapshots/test_binary_sensor.ambr +++ b/tests/components/renault/snapshots/test_binary_sensor.ambr @@ -24,6 +24,7 @@ 'model': 'Captur ii', 'name': 'REG-NUMBER', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': 'XJB1SU', @@ -322,6 +323,7 @@ 'model': 'Captur ii', 'name': 'REG-NUMBER', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': 'XJB1SU', @@ -706,6 +708,7 @@ 'model': 'Zoe', 'name': 'REG-NUMBER', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': 'X101VE', @@ -874,6 +877,7 @@ 'model': 'Zoe', 'name': 'REG-NUMBER', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': 'X102VE', @@ -1300,6 +1304,7 @@ 'model': 'Captur ii', 'name': 'REG-NUMBER', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': 'XJB1SU', @@ -1598,6 +1603,7 @@ 'model': 'Captur ii', 'name': 'REG-NUMBER', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': 'XJB1SU', @@ -1982,6 +1988,7 @@ 'model': 'Zoe', 'name': 'REG-NUMBER', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': 'X101VE', @@ -2150,6 +2157,7 @@ 'model': 'Zoe', 'name': 'REG-NUMBER', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': 'X102VE', diff --git a/tests/components/renault/snapshots/test_button.ambr b/tests/components/renault/snapshots/test_button.ambr index daef84b5c0a..7fa37319b2e 100644 --- a/tests/components/renault/snapshots/test_button.ambr +++ b/tests/components/renault/snapshots/test_button.ambr @@ -24,6 +24,7 @@ 'model': 'Captur ii', 'name': 'REG-NUMBER', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': 'XJB1SU', @@ -106,6 +107,7 @@ 'model': 'Captur ii', 'name': 'REG-NUMBER', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': 'XJB1SU', @@ -272,6 +274,7 @@ 'model': 'Zoe', 'name': 'REG-NUMBER', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': 'X101VE', @@ -438,6 +441,7 @@ 'model': 'Zoe', 'name': 'REG-NUMBER', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': 'X102VE', @@ -604,6 +608,7 @@ 'model': 'Captur ii', 'name': 'REG-NUMBER', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': 'XJB1SU', @@ -686,6 +691,7 @@ 'model': 'Captur ii', 'name': 'REG-NUMBER', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': 'XJB1SU', @@ -852,6 +858,7 @@ 'model': 'Zoe', 'name': 'REG-NUMBER', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': 'X101VE', @@ -1018,6 +1025,7 @@ 'model': 'Zoe', 'name': 'REG-NUMBER', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': 'X102VE', diff --git a/tests/components/renault/snapshots/test_device_tracker.ambr b/tests/components/renault/snapshots/test_device_tracker.ambr index 8fe1713dc0b..61232d0268d 100644 --- a/tests/components/renault/snapshots/test_device_tracker.ambr +++ b/tests/components/renault/snapshots/test_device_tracker.ambr @@ -24,6 +24,7 @@ 'model': 'Captur ii', 'name': 'REG-NUMBER', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': 'XJB1SU', @@ -107,6 +108,7 @@ 'model': 'Captur ii', 'name': 'REG-NUMBER', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': 'XJB1SU', @@ -190,6 +192,7 @@ 'model': 'Zoe', 'name': 'REG-NUMBER', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': 'X101VE', @@ -230,6 +233,7 @@ 'model': 'Zoe', 'name': 'REG-NUMBER', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': 'X102VE', @@ -313,6 +317,7 @@ 'model': 'Captur ii', 'name': 'REG-NUMBER', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': 'XJB1SU', @@ -399,6 +404,7 @@ 'model': 'Captur ii', 'name': 'REG-NUMBER', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': 'XJB1SU', @@ -485,6 +491,7 @@ 'model': 'Zoe', 'name': 'REG-NUMBER', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': 'X101VE', @@ -525,6 +532,7 @@ 'model': 'Zoe', 'name': 'REG-NUMBER', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': 'X102VE', diff --git a/tests/components/renault/snapshots/test_select.ambr b/tests/components/renault/snapshots/test_select.ambr index 0722cb5cab3..30181fd3b9c 100644 --- a/tests/components/renault/snapshots/test_select.ambr +++ b/tests/components/renault/snapshots/test_select.ambr @@ -24,6 +24,7 @@ 'model': 'Captur ii', 'name': 'REG-NUMBER', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': 'XJB1SU', @@ -64,6 +65,7 @@ 'model': 'Captur ii', 'name': 'REG-NUMBER', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': 'XJB1SU', @@ -159,6 +161,7 @@ 'model': 'Zoe', 'name': 'REG-NUMBER', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': 'X101VE', @@ -254,6 +257,7 @@ 'model': 'Zoe', 'name': 'REG-NUMBER', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': 'X102VE', @@ -349,6 +353,7 @@ 'model': 'Captur ii', 'name': 'REG-NUMBER', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': 'XJB1SU', @@ -389,6 +394,7 @@ 'model': 'Captur ii', 'name': 'REG-NUMBER', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': 'XJB1SU', @@ -484,6 +490,7 @@ 'model': 'Zoe', 'name': 'REG-NUMBER', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': 'X101VE', @@ -579,6 +586,7 @@ 'model': 'Zoe', 'name': 'REG-NUMBER', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': 'X102VE', diff --git a/tests/components/renault/snapshots/test_sensor.ambr b/tests/components/renault/snapshots/test_sensor.ambr index 5909c66bc5c..1ae033101d4 100644 --- a/tests/components/renault/snapshots/test_sensor.ambr +++ b/tests/components/renault/snapshots/test_sensor.ambr @@ -24,6 +24,7 @@ 'model': 'Captur ii', 'name': 'REG-NUMBER', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': 'XJB1SU', @@ -332,6 +333,7 @@ 'model': 'Captur ii', 'name': 'REG-NUMBER', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': 'XJB1SU', @@ -1085,6 +1087,7 @@ 'model': 'Zoe', 'name': 'REG-NUMBER', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': 'X101VE', @@ -1834,6 +1837,7 @@ 'model': 'Zoe', 'name': 'REG-NUMBER', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': 'X102VE', @@ -2626,6 +2630,7 @@ 'model': 'Captur ii', 'name': 'REG-NUMBER', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': 'XJB1SU', @@ -2934,6 +2939,7 @@ 'model': 'Captur ii', 'name': 'REG-NUMBER', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': 'XJB1SU', @@ -3687,6 +3693,7 @@ 'model': 'Zoe', 'name': 'REG-NUMBER', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': 'X101VE', @@ -4436,6 +4443,7 @@ 'model': 'Zoe', 'name': 'REG-NUMBER', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': 'X102VE', diff --git a/tests/components/rova/snapshots/test_init.ambr b/tests/components/rova/snapshots/test_init.ambr index 340b0e6d472..ffb08ee082e 100644 --- a/tests/components/rova/snapshots/test_init.ambr +++ b/tests/components/rova/snapshots/test_init.ambr @@ -23,6 +23,7 @@ 'model': None, 'name': '8381BE 13', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, diff --git a/tests/components/sfr_box/snapshots/test_binary_sensor.ambr b/tests/components/sfr_box/snapshots/test_binary_sensor.ambr index 7422c1395c3..f14ec98a418 100644 --- a/tests/components/sfr_box/snapshots/test_binary_sensor.ambr +++ b/tests/components/sfr_box/snapshots/test_binary_sensor.ambr @@ -24,6 +24,7 @@ 'model': 'NB6VAC-FXC-r0', 'name': 'SFR Box', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': 'NB6VAC-MAIN-R4.0.44k', @@ -150,6 +151,7 @@ 'model': 'NB6VAC-FXC-r0', 'name': 'SFR Box', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': 'NB6VAC-MAIN-R4.0.44k', diff --git a/tests/components/sfr_box/snapshots/test_button.ambr b/tests/components/sfr_box/snapshots/test_button.ambr index 0dfbf187f6d..eee419bf373 100644 --- a/tests/components/sfr_box/snapshots/test_button.ambr +++ b/tests/components/sfr_box/snapshots/test_button.ambr @@ -24,6 +24,7 @@ 'model': 'NB6VAC-FXC-r0', 'name': 'SFR Box', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': 'NB6VAC-MAIN-R4.0.44k', diff --git a/tests/components/sfr_box/snapshots/test_sensor.ambr b/tests/components/sfr_box/snapshots/test_sensor.ambr index 0f39eed9e60..649c94c89dc 100644 --- a/tests/components/sfr_box/snapshots/test_sensor.ambr +++ b/tests/components/sfr_box/snapshots/test_sensor.ambr @@ -24,6 +24,7 @@ 'model': 'NB6VAC-FXC-r0', 'name': 'SFR Box', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': 'NB6VAC-MAIN-R4.0.44k', diff --git a/tests/components/tailwind/snapshots/test_binary_sensor.ambr b/tests/components/tailwind/snapshots/test_binary_sensor.ambr index ea2a539363d..20a3282db55 100644 --- a/tests/components/tailwind/snapshots/test_binary_sensor.ambr +++ b/tests/components/tailwind/snapshots/test_binary_sensor.ambr @@ -70,6 +70,7 @@ 'model': 'iQ3', 'name': 'Door 1', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '10.10', @@ -147,6 +148,7 @@ 'model': 'iQ3', 'name': 'Door 2', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '10.10', diff --git a/tests/components/tailwind/snapshots/test_button.ambr b/tests/components/tailwind/snapshots/test_button.ambr index 560d3fe692c..3ddbbb3f81d 100644 --- a/tests/components/tailwind/snapshots/test_button.ambr +++ b/tests/components/tailwind/snapshots/test_button.ambr @@ -74,6 +74,7 @@ 'model': 'iQ3', 'name': 'Tailwind iQ3', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '10.10', diff --git a/tests/components/tailwind/snapshots/test_cover.ambr b/tests/components/tailwind/snapshots/test_cover.ambr index 0ecd172b2ca..4ac6d6adc7d 100644 --- a/tests/components/tailwind/snapshots/test_cover.ambr +++ b/tests/components/tailwind/snapshots/test_cover.ambr @@ -71,6 +71,7 @@ 'model': 'iQ3', 'name': 'Door 1', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '10.10', @@ -149,6 +150,7 @@ 'model': 'iQ3', 'name': 'Door 2', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '10.10', diff --git a/tests/components/tailwind/snapshots/test_number.ambr b/tests/components/tailwind/snapshots/test_number.ambr index cbd61d31a6c..b4e73f4b2aa 100644 --- a/tests/components/tailwind/snapshots/test_number.ambr +++ b/tests/components/tailwind/snapshots/test_number.ambr @@ -83,6 +83,7 @@ 'model': 'iQ3', 'name': 'Tailwind iQ3', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '10.10', diff --git a/tests/components/tasmota/test_discovery.py b/tests/components/tasmota/test_discovery.py index 5405e6c417d..91832f1f2f0 100644 --- a/tests/components/tasmota/test_discovery.py +++ b/tests/components/tasmota/test_discovery.py @@ -340,7 +340,7 @@ async def test_device_remove_multiple_config_entries_1( connections={(dr.CONNECTION_NETWORK_MAC, mac)} ) assert device_entry is not None - assert device_entry.config_entries == [tasmota_entry.entry_id, mock_entry.entry_id] + assert device_entry.config_entries == {tasmota_entry.entry_id, mock_entry.entry_id} async_fire_mqtt_message( hass, @@ -354,7 +354,7 @@ async def test_device_remove_multiple_config_entries_1( connections={(dr.CONNECTION_NETWORK_MAC, mac)} ) assert device_entry is not None - assert device_entry.config_entries == [mock_entry.entry_id] + assert device_entry.config_entries == {mock_entry.entry_id} async def test_device_remove_multiple_config_entries_2( @@ -396,7 +396,7 @@ async def test_device_remove_multiple_config_entries_2( connections={(dr.CONNECTION_NETWORK_MAC, mac)} ) assert device_entry is not None - assert device_entry.config_entries == [tasmota_entry.entry_id, mock_entry.entry_id] + assert device_entry.config_entries == {tasmota_entry.entry_id, mock_entry.entry_id} assert other_device_entry.id != device_entry.id # Remove other config entry from the device @@ -410,7 +410,7 @@ async def test_device_remove_multiple_config_entries_2( connections={(dr.CONNECTION_NETWORK_MAC, mac)} ) assert device_entry is not None - assert device_entry.config_entries == [tasmota_entry.entry_id] + assert device_entry.config_entries == {tasmota_entry.entry_id} mqtt_mock.async_publish.assert_not_called() # Remove other config entry from the other device - Tasmota should not do any cleanup diff --git a/tests/components/tedee/snapshots/test_init.ambr b/tests/components/tedee/snapshots/test_init.ambr index 83ab032dfb4..c91fb3ca484 100644 --- a/tests/components/tedee/snapshots/test_init.ambr +++ b/tests/components/tedee/snapshots/test_init.ambr @@ -23,6 +23,7 @@ 'model': 'Bridge', 'name': 'Bridge-AB1C', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': '0000-0000', 'suggested_area': None, 'sw_version': None, diff --git a/tests/components/tedee/snapshots/test_lock.ambr b/tests/components/tedee/snapshots/test_lock.ambr index 8e4fc464479..8fa8ab7668d 100644 --- a/tests/components/tedee/snapshots/test_lock.ambr +++ b/tests/components/tedee/snapshots/test_lock.ambr @@ -70,6 +70,7 @@ 'model': 'Tedee PRO', 'name': 'Lock-1A2B', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -147,6 +148,7 @@ 'model': 'Tedee GO', 'name': 'Lock-2C3D', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, diff --git a/tests/components/teslemetry/snapshots/test_init.ambr b/tests/components/teslemetry/snapshots/test_init.ambr index 951e4557bdd..e5dd23ada6e 100644 --- a/tests/components/teslemetry/snapshots/test_init.ambr +++ b/tests/components/teslemetry/snapshots/test_init.ambr @@ -23,6 +23,7 @@ 'model': 'Powerwall 2, Tesla Backup Gateway 2', 'name': 'Energy Site', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': '123456', 'suggested_area': None, 'sw_version': None, @@ -53,6 +54,7 @@ 'model': 'Model X', 'name': 'Test', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': 'LRWXF7EK4KC700000', 'suggested_area': None, 'sw_version': None, @@ -83,6 +85,7 @@ 'model': 'Gen 3 Wall Connector', 'name': 'Wall Connector', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': '123', 'suggested_area': None, 'sw_version': None, @@ -113,6 +116,7 @@ 'model': 'Gen 3 Wall Connector', 'name': 'Wall Connector', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': '234', 'suggested_area': None, 'sw_version': None, diff --git a/tests/components/tplink/snapshots/test_binary_sensor.ambr b/tests/components/tplink/snapshots/test_binary_sensor.ambr index 27b1372df27..b45494d1001 100644 --- a/tests/components/tplink/snapshots/test_binary_sensor.ambr +++ b/tests/components/tplink/snapshots/test_binary_sensor.ambr @@ -361,6 +361,7 @@ 'model': 'HS100', 'name': 'my_device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '1.0.0', diff --git a/tests/components/tplink/snapshots/test_button.ambr b/tests/components/tplink/snapshots/test_button.ambr index f26829101f7..0167256877d 100644 --- a/tests/components/tplink/snapshots/test_button.ambr +++ b/tests/components/tplink/snapshots/test_button.ambr @@ -119,6 +119,7 @@ 'model': 'HS100', 'name': 'my_device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '1.0.0', diff --git a/tests/components/tplink/snapshots/test_climate.ambr b/tests/components/tplink/snapshots/test_climate.ambr index d30f8cd3532..4bdfe52b9b1 100644 --- a/tests/components/tplink/snapshots/test_climate.ambr +++ b/tests/components/tplink/snapshots/test_climate.ambr @@ -86,6 +86,7 @@ 'model': 'HS100', 'name': 'thermostat', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '1.0.0', diff --git a/tests/components/tplink/snapshots/test_fan.ambr b/tests/components/tplink/snapshots/test_fan.ambr index d692abdce03..0a51909affe 100644 --- a/tests/components/tplink/snapshots/test_fan.ambr +++ b/tests/components/tplink/snapshots/test_fan.ambr @@ -186,6 +186,7 @@ 'model': 'HS100', 'name': 'my_device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '1.0.0', diff --git a/tests/components/tplink/snapshots/test_number.ambr b/tests/components/tplink/snapshots/test_number.ambr index 9bfc9c0126a..8cda0a728b3 100644 --- a/tests/components/tplink/snapshots/test_number.ambr +++ b/tests/components/tplink/snapshots/test_number.ambr @@ -27,6 +27,7 @@ 'model': 'HS100', 'name': 'my_device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '1.0.0', diff --git a/tests/components/tplink/snapshots/test_select.ambr b/tests/components/tplink/snapshots/test_select.ambr index 2cf02415238..555b0eb74d1 100644 --- a/tests/components/tplink/snapshots/test_select.ambr +++ b/tests/components/tplink/snapshots/test_select.ambr @@ -27,6 +27,7 @@ 'model': 'HS100', 'name': 'my_device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '1.0.0', diff --git a/tests/components/tplink/snapshots/test_sensor.ambr b/tests/components/tplink/snapshots/test_sensor.ambr index cd8980bf57f..46fe897500f 100644 --- a/tests/components/tplink/snapshots/test_sensor.ambr +++ b/tests/components/tplink/snapshots/test_sensor.ambr @@ -27,6 +27,7 @@ 'model': 'HS100', 'name': 'my_device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '1.0.0', diff --git a/tests/components/tplink/snapshots/test_switch.ambr b/tests/components/tplink/snapshots/test_switch.ambr index 2fe1f6e6b08..65eead6ddf4 100644 --- a/tests/components/tplink/snapshots/test_switch.ambr +++ b/tests/components/tplink/snapshots/test_switch.ambr @@ -27,6 +27,7 @@ 'model': 'HS100', 'name': 'my_device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '1.0.0', diff --git a/tests/components/twentemilieu/snapshots/test_calendar.ambr b/tests/components/twentemilieu/snapshots/test_calendar.ambr index 78b2d56afca..e6de21fdca1 100644 --- a/tests/components/twentemilieu/snapshots/test_calendar.ambr +++ b/tests/components/twentemilieu/snapshots/test_calendar.ambr @@ -101,6 +101,7 @@ 'model': None, 'name': 'Twente Milieu', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, diff --git a/tests/components/twentemilieu/snapshots/test_sensor.ambr b/tests/components/twentemilieu/snapshots/test_sensor.ambr index a0f3b75da57..22dcb0331cd 100644 --- a/tests/components/twentemilieu/snapshots/test_sensor.ambr +++ b/tests/components/twentemilieu/snapshots/test_sensor.ambr @@ -70,6 +70,7 @@ 'model': None, 'name': 'Twente Milieu', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -147,6 +148,7 @@ 'model': None, 'name': 'Twente Milieu', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -224,6 +226,7 @@ 'model': None, 'name': 'Twente Milieu', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -301,6 +304,7 @@ 'model': None, 'name': 'Twente Milieu', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -378,6 +382,7 @@ 'model': None, 'name': 'Twente Milieu', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, diff --git a/tests/components/uptime/snapshots/test_sensor.ambr b/tests/components/uptime/snapshots/test_sensor.ambr index 0e7ae6dceaa..92baf939eb3 100644 --- a/tests/components/uptime/snapshots/test_sensor.ambr +++ b/tests/components/uptime/snapshots/test_sensor.ambr @@ -63,6 +63,7 @@ 'model': None, 'name': 'Uptime', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, diff --git a/tests/components/vesync/snapshots/test_fan.ambr b/tests/components/vesync/snapshots/test_fan.ambr index 59304e92d9d..a9210447f1e 100644 --- a/tests/components/vesync/snapshots/test_fan.ambr +++ b/tests/components/vesync/snapshots/test_fan.ambr @@ -24,6 +24,7 @@ 'model': 'LV-PUR131S', 'name': 'Air Purifier 131s', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -114,6 +115,7 @@ 'model': 'Core200S', 'name': 'Air Purifier 200s', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -209,6 +211,7 @@ 'model': 'LAP-C401S-WJP', 'name': 'Air Purifier 400s', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -306,6 +309,7 @@ 'model': 'LAP-C601S-WUS', 'name': 'Air Purifier 600s', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -403,6 +407,7 @@ 'model': 'ESL100', 'name': 'Dimmable Light', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -439,6 +444,7 @@ 'model': 'ESWD16', 'name': 'Dimmer Switch', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -491,6 +497,7 @@ 'model': 'wifi-switch-1.3', 'name': 'Outlet', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -527,6 +534,7 @@ 'model': 'ESL100CW', 'name': 'Temperature Light', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -563,6 +571,7 @@ 'model': 'ESWL01', 'name': 'Wall Switch', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, diff --git a/tests/components/vesync/snapshots/test_light.ambr b/tests/components/vesync/snapshots/test_light.ambr index 9990395a36c..c2c9854fa9f 100644 --- a/tests/components/vesync/snapshots/test_light.ambr +++ b/tests/components/vesync/snapshots/test_light.ambr @@ -24,6 +24,7 @@ 'model': 'LV-PUR131S', 'name': 'Air Purifier 131s', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -60,6 +61,7 @@ 'model': 'Core200S', 'name': 'Air Purifier 200s', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -96,6 +98,7 @@ 'model': 'LAP-C401S-WJP', 'name': 'Air Purifier 400s', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -132,6 +135,7 @@ 'model': 'LAP-C601S-WUS', 'name': 'Air Purifier 600s', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -168,6 +172,7 @@ 'model': 'ESL100', 'name': 'Dimmable Light', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -256,6 +261,7 @@ 'model': 'ESWD16', 'name': 'Dimmer Switch', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -362,6 +368,7 @@ 'model': 'wifi-switch-1.3', 'name': 'Outlet', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -398,6 +405,7 @@ 'model': 'ESL100CW', 'name': 'Temperature Light', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -501,6 +509,7 @@ 'model': 'ESWL01', 'name': 'Wall Switch', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, diff --git a/tests/components/vesync/snapshots/test_sensor.ambr b/tests/components/vesync/snapshots/test_sensor.ambr index 268718fb2fe..97013b4e9ce 100644 --- a/tests/components/vesync/snapshots/test_sensor.ambr +++ b/tests/components/vesync/snapshots/test_sensor.ambr @@ -24,6 +24,7 @@ 'model': 'LV-PUR131S', 'name': 'Air Purifier 131s', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -152,6 +153,7 @@ 'model': 'Core200S', 'name': 'Air Purifier 200s', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -236,6 +238,7 @@ 'model': 'LAP-C401S-WJP', 'name': 'Air Purifier 400s', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -413,6 +416,7 @@ 'model': 'LAP-C601S-WUS', 'name': 'Air Purifier 600s', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -590,6 +594,7 @@ 'model': 'ESL100', 'name': 'Dimmable Light', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -626,6 +631,7 @@ 'model': 'ESWD16', 'name': 'Dimmer Switch', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -678,6 +684,7 @@ 'model': 'wifi-switch-1.3', 'name': 'Outlet', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -1008,6 +1015,7 @@ 'model': 'ESL100CW', 'name': 'Temperature Light', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -1044,6 +1052,7 @@ 'model': 'ESWL01', 'name': 'Wall Switch', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, diff --git a/tests/components/vesync/snapshots/test_switch.ambr b/tests/components/vesync/snapshots/test_switch.ambr index 3df26f74bcf..86b3b0ff5cd 100644 --- a/tests/components/vesync/snapshots/test_switch.ambr +++ b/tests/components/vesync/snapshots/test_switch.ambr @@ -24,6 +24,7 @@ 'model': 'LV-PUR131S', 'name': 'Air Purifier 131s', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -60,6 +61,7 @@ 'model': 'Core200S', 'name': 'Air Purifier 200s', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -96,6 +98,7 @@ 'model': 'LAP-C401S-WJP', 'name': 'Air Purifier 400s', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -132,6 +135,7 @@ 'model': 'LAP-C601S-WUS', 'name': 'Air Purifier 600s', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -168,6 +172,7 @@ 'model': 'ESL100', 'name': 'Dimmable Light', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -204,6 +209,7 @@ 'model': 'ESWD16', 'name': 'Dimmer Switch', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -256,6 +262,7 @@ 'model': 'wifi-switch-1.3', 'name': 'Outlet', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -336,6 +343,7 @@ 'model': 'ESL100CW', 'name': 'Temperature Light', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -372,6 +380,7 @@ 'model': 'ESWL01', 'name': 'Wall Switch', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, diff --git a/tests/components/whois/snapshots/test_sensor.ambr b/tests/components/whois/snapshots/test_sensor.ambr index 61762c36e59..9bc125f204b 100644 --- a/tests/components/whois/snapshots/test_sensor.ambr +++ b/tests/components/whois/snapshots/test_sensor.ambr @@ -69,6 +69,7 @@ 'model': None, 'name': 'home-assistant.io', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -146,6 +147,7 @@ 'model': None, 'name': 'home-assistant.io', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -227,6 +229,7 @@ 'model': None, 'name': 'home-assistant.io', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -304,6 +307,7 @@ 'model': None, 'name': 'home-assistant.io', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -381,6 +385,7 @@ 'model': None, 'name': 'home-assistant.io', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -457,6 +462,7 @@ 'model': None, 'name': 'home-assistant.io', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -533,6 +539,7 @@ 'model': None, 'name': 'home-assistant.io', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -609,6 +616,7 @@ 'model': None, 'name': 'home-assistant.io', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -685,6 +693,7 @@ 'model': None, 'name': 'home-assistant.io', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, diff --git a/tests/components/wled/snapshots/test_button.ambr b/tests/components/wled/snapshots/test_button.ambr index b489bcc0a71..9c91c0e0050 100644 --- a/tests/components/wled/snapshots/test_button.ambr +++ b/tests/components/wled/snapshots/test_button.ambr @@ -74,6 +74,7 @@ 'model': 'DIY light', 'name': 'WLED RGB Light', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '0.8.5', diff --git a/tests/components/wled/snapshots/test_number.ambr b/tests/components/wled/snapshots/test_number.ambr index c3440108148..bee3e180090 100644 --- a/tests/components/wled/snapshots/test_number.ambr +++ b/tests/components/wled/snapshots/test_number.ambr @@ -82,6 +82,7 @@ 'model': 'DIY light', 'name': 'WLED RGB Light', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '0.8.5', @@ -171,6 +172,7 @@ 'model': 'DIY light', 'name': 'WLED RGB Light', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '0.8.5', diff --git a/tests/components/wled/snapshots/test_select.ambr b/tests/components/wled/snapshots/test_select.ambr index 6d64ec43658..f6447f699c9 100644 --- a/tests/components/wled/snapshots/test_select.ambr +++ b/tests/components/wled/snapshots/test_select.ambr @@ -84,6 +84,7 @@ 'model': 'DIY light', 'name': 'WLED RGB Light', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '0.8.5', @@ -269,6 +270,7 @@ 'model': 'DIY light', 'name': 'WLED RGB Light', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '0.8.5', @@ -358,6 +360,7 @@ 'model': 'DIY light', 'name': 'WLED RGBW Light', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '0.8.6b4', @@ -447,6 +450,7 @@ 'model': 'DIY light', 'name': 'WLED RGBW Light', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '0.8.6b4', diff --git a/tests/components/wled/snapshots/test_switch.ambr b/tests/components/wled/snapshots/test_switch.ambr index da69e686f07..6bca0a2ed3b 100644 --- a/tests/components/wled/snapshots/test_switch.ambr +++ b/tests/components/wled/snapshots/test_switch.ambr @@ -76,6 +76,7 @@ 'model': 'DIY light', 'name': 'WLED RGB Light', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '0.8.5', @@ -156,6 +157,7 @@ 'model': 'DIY light', 'name': 'WLED RGB Light', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '0.8.5', @@ -237,6 +239,7 @@ 'model': 'DIY light', 'name': 'WLED RGB Light', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '0.8.5', @@ -318,6 +321,7 @@ 'model': 'DIY light', 'name': 'WLED RGB Light', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '0.8.5', diff --git a/tests/helpers/test_device_registry.py b/tests/helpers/test_device_registry.py index f8f10baad08..fa57cc7557e 100644 --- a/tests/helpers/test_device_registry.py +++ b/tests/helpers/test_device_registry.py @@ -90,7 +90,7 @@ async def test_get_or_create_returns_same_entry( await hass.async_block_till_done() # Only 2 update events. The third entry did not generate any changes. - assert len(update_events) == 2, update_events + assert len(update_events) == 2 assert update_events[0].data == { "action": "create", "device_id": entry.id, @@ -170,9 +170,10 @@ async def test_multiple_config_entries( assert len(device_registry.devices) == 1 assert entry.id == entry2.id assert entry.id == entry3.id - assert entry2.config_entries == [config_entry_2.entry_id, config_entry_1.entry_id] - # the 3rd get_or_create was a primary update, so that's now first config entry - assert entry3.config_entries == [config_entry_1.entry_id, config_entry_2.entry_id] + assert entry2.config_entries == {config_entry_1.entry_id, config_entry_2.entry_id} + assert entry2.primary_config_entry == config_entry_1.entry_id + assert entry3.config_entries == {config_entry_1.entry_id, config_entry_2.entry_id} + assert entry3.primary_config_entry == config_entry_1.entry_id @pytest.mark.parametrize("load_registries", [False]) @@ -202,6 +203,7 @@ async def test_loading_from_storage( "model": "model", "name_by_user": "Test Friendly Name", "name": "name", + "primary_config_entry": mock_config_entry.entry_id, "serial_number": "serial_no", "sw_version": "version", "via_device_id": None, @@ -233,7 +235,7 @@ async def test_loading_from_storage( ) assert entry == dr.DeviceEntry( area_id="12345A", - config_entries=[mock_config_entry.entry_id], + config_entries={mock_config_entry.entry_id}, configuration_url="https://example.com/config", connections={("Zigbee", "01.23.45.67.89")}, disabled_by=dr.DeviceEntryDisabler.USER, @@ -246,11 +248,12 @@ async def test_loading_from_storage( model="model", name_by_user="Test Friendly Name", name="name", + primary_config_entry=mock_config_entry.entry_id, serial_number="serial_no", suggested_area=None, # Not stored sw_version="version", ) - assert isinstance(entry.config_entries, list) + assert isinstance(entry.config_entries, set) assert isinstance(entry.connections, set) assert isinstance(entry.identifiers, set) @@ -263,26 +266,27 @@ async def test_loading_from_storage( model="model", ) assert entry == dr.DeviceEntry( - config_entries=[mock_config_entry.entry_id], + config_entries={mock_config_entry.entry_id}, connections={("Zigbee", "23.45.67.89.01")}, id="bcdefghijklmn", identifiers={("serial", "3456ABCDEF12")}, manufacturer="manufacturer", model="model", + primary_config_entry=mock_config_entry.entry_id, ) assert entry.id == "bcdefghijklmn" - assert isinstance(entry.config_entries, list) + assert isinstance(entry.config_entries, set) assert isinstance(entry.connections, set) assert isinstance(entry.identifiers, set) @pytest.mark.parametrize("load_registries", [False]) -async def test_migration_1_1_to_1_5( +async def test_migration_1_1_to_1_6( hass: HomeAssistant, hass_storage: dict[str, Any], mock_config_entry: MockConfigEntry, ) -> None: - """Test migration from version 1.1 to 1.5.""" + """Test migration from version 1.1 to 1.6.""" hass_storage[dr.STORAGE_KEY] = { "version": 1, "minor_version": 1, @@ -371,6 +375,7 @@ async def test_migration_1_1_to_1_5( "model": "model", "name": "name", "name_by_user": None, + "primary_config_entry": mock_config_entry.entry_id, "serial_number": None, "sw_version": "new_version", "via_device_id": None, @@ -390,6 +395,7 @@ async def test_migration_1_1_to_1_5( "model": None, "name_by_user": None, "name": None, + "primary_config_entry": None, "serial_number": None, "sw_version": None, "via_device_id": None, @@ -409,12 +415,12 @@ async def test_migration_1_1_to_1_5( @pytest.mark.parametrize("load_registries", [False]) -async def test_migration_1_2_to_1_5( +async def test_migration_1_2_to_1_6( hass: HomeAssistant, hass_storage: dict[str, Any], mock_config_entry: MockConfigEntry, ) -> None: - """Test migration from version 1.2 to 1.5.""" + """Test migration from version 1.2 to 1.6.""" hass_storage[dr.STORAGE_KEY] = { "version": 1, "minor_version": 2, @@ -502,6 +508,7 @@ async def test_migration_1_2_to_1_5( "model": "model", "name": "name", "name_by_user": None, + "primary_config_entry": mock_config_entry.entry_id, "serial_number": None, "sw_version": "new_version", "via_device_id": None, @@ -521,6 +528,7 @@ async def test_migration_1_2_to_1_5( "model": None, "name_by_user": None, "name": None, + "primary_config_entry": None, "serial_number": None, "sw_version": None, "via_device_id": None, @@ -532,12 +540,12 @@ async def test_migration_1_2_to_1_5( @pytest.mark.parametrize("load_registries", [False]) -async def test_migration_1_3_to_1_5( +async def test_migration_1_3_to_1_6( hass: HomeAssistant, hass_storage: dict[str, Any], mock_config_entry: MockConfigEntry, ) -> None: - """Test migration from version 1.3 to 1.5.""" + """Test migration from version 1.3 to 1.6.""" hass_storage[dr.STORAGE_KEY] = { "version": 1, "minor_version": 3, @@ -627,6 +635,7 @@ async def test_migration_1_3_to_1_5( "model": "model", "name": "name", "name_by_user": None, + "primary_config_entry": mock_config_entry.entry_id, "serial_number": None, "sw_version": "new_version", "via_device_id": None, @@ -644,8 +653,9 @@ async def test_migration_1_3_to_1_5( "labels": [], "manufacturer": None, "model": None, - "name_by_user": None, "name": None, + "name_by_user": None, + "primary_config_entry": None, "serial_number": None, "sw_version": None, "via_device_id": None, @@ -657,12 +667,12 @@ async def test_migration_1_3_to_1_5( @pytest.mark.parametrize("load_registries", [False]) -async def test_migration_1_4_to_1_5( +async def test_migration_1_4_to_1_6( hass: HomeAssistant, hass_storage: dict[str, Any], mock_config_entry: MockConfigEntry, ) -> None: - """Test migration from version 1.4 to 1.5.""" + """Test migration from version 1.4 to 1.6.""" hass_storage[dr.STORAGE_KEY] = { "version": 1, "minor_version": 4, @@ -754,6 +764,7 @@ async def test_migration_1_4_to_1_5( "model": "model", "name": "name", "name_by_user": None, + "primary_config_entry": mock_config_entry.entry_id, "serial_number": None, "sw_version": "new_version", "via_device_id": None, @@ -773,6 +784,138 @@ async def test_migration_1_4_to_1_5( "model": None, "name_by_user": None, "name": None, + "primary_config_entry": None, + "serial_number": None, + "sw_version": None, + "via_device_id": None, + }, + ], + "deleted_devices": [], + }, + } + + +@pytest.mark.parametrize("load_registries", [False]) +async def test_migration_1_5_to_1_6( + hass: HomeAssistant, + hass_storage: dict[str, Any], + mock_config_entry: MockConfigEntry, +) -> None: + """Test migration from version 1.5 to 1.6.""" + hass_storage[dr.STORAGE_KEY] = { + "version": 1, + "minor_version": 5, + "key": dr.STORAGE_KEY, + "data": { + "devices": [ + { + "area_id": None, + "config_entries": [mock_config_entry.entry_id], + "configuration_url": None, + "connections": [["Zigbee", "01.23.45.67.89"]], + "disabled_by": None, + "entry_type": "service", + "hw_version": "hw_version", + "id": "abcdefghijklm", + "identifiers": [["serial", "123456ABCDEF"]], + "labels": ["blah"], + "manufacturer": "manufacturer", + "model": "model", + "name": "name", + "name_by_user": None, + "serial_number": None, + "sw_version": "new_version", + "via_device_id": None, + }, + { + "area_id": None, + "config_entries": [None], + "configuration_url": None, + "connections": [], + "disabled_by": None, + "entry_type": None, + "hw_version": None, + "id": "invalid-entry-type", + "identifiers": [["serial", "mock-id-invalid-entry"]], + "labels": ["blah"], + "manufacturer": None, + "model": None, + "name_by_user": None, + "name": None, + "serial_number": None, + "sw_version": None, + "via_device_id": None, + }, + ], + "deleted_devices": [], + }, + } + + await dr.async_load(hass) + registry = dr.async_get(hass) + + # Test data was loaded + entry = registry.async_get_or_create( + config_entry_id=mock_config_entry.entry_id, + connections={("Zigbee", "01.23.45.67.89")}, + identifiers={("serial", "123456ABCDEF")}, + ) + assert entry.id == "abcdefghijklm" + + # Update to trigger a store + entry = registry.async_get_or_create( + config_entry_id=mock_config_entry.entry_id, + connections={("Zigbee", "01.23.45.67.89")}, + identifiers={("serial", "123456ABCDEF")}, + sw_version="new_version", + ) + assert entry.id == "abcdefghijklm" + + # Check we store migrated data + await flush_store(registry._store) + + assert hass_storage[dr.STORAGE_KEY] == { + "version": dr.STORAGE_VERSION_MAJOR, + "minor_version": dr.STORAGE_VERSION_MINOR, + "key": dr.STORAGE_KEY, + "data": { + "devices": [ + { + "area_id": None, + "config_entries": [mock_config_entry.entry_id], + "configuration_url": None, + "connections": [["Zigbee", "01.23.45.67.89"]], + "disabled_by": None, + "entry_type": "service", + "hw_version": "hw_version", + "id": "abcdefghijklm", + "identifiers": [["serial", "123456ABCDEF"]], + "labels": ["blah"], + "manufacturer": "manufacturer", + "model": "model", + "name": "name", + "name_by_user": None, + "primary_config_entry": mock_config_entry.entry_id, + "serial_number": None, + "sw_version": "new_version", + "via_device_id": None, + }, + { + "area_id": None, + "config_entries": [None], + "configuration_url": None, + "connections": [], + "disabled_by": None, + "entry_type": None, + "hw_version": None, + "id": "invalid-entry-type", + "identifiers": [["serial", "mock-id-invalid-entry"]], + "labels": ["blah"], + "manufacturer": None, + "model": None, + "name_by_user": None, + "name": None, + "primary_config_entry": None, "serial_number": None, "sw_version": None, "via_device_id": None, @@ -818,7 +961,7 @@ async def test_removing_config_entries( assert len(device_registry.devices) == 2 assert entry.id == entry2.id assert entry.id != entry3.id - assert entry2.config_entries == [config_entry_2.entry_id, config_entry_1.entry_id] + assert entry2.config_entries == {config_entry_1.entry_id, config_entry_2.entry_id} device_registry.async_clear_config_entry(config_entry_1.entry_id) entry = device_registry.async_get_device(identifiers={("bridgeid", "0123")}) @@ -826,7 +969,7 @@ async def test_removing_config_entries( identifiers={("bridgeid", "4567")} ) - assert entry.config_entries == [config_entry_2.entry_id] + assert entry.config_entries == {config_entry_2.entry_id} assert entry3_removed is None await hass.async_block_till_done() @@ -839,7 +982,9 @@ async def test_removing_config_entries( assert update_events[1].data == { "action": "update", "device_id": entry.id, - "changes": {"config_entries": [config_entry_1.entry_id]}, + "changes": { + "config_entries": {config_entry_1.entry_id}, + }, } assert update_events[2].data == { "action": "create", @@ -849,7 +994,8 @@ async def test_removing_config_entries( "action": "update", "device_id": entry.id, "changes": { - "config_entries": [config_entry_2.entry_id, config_entry_1.entry_id] + "config_entries": {config_entry_1.entry_id, config_entry_2.entry_id}, + "primary_config_entry": config_entry_1.entry_id, }, } assert update_events[4].data == { @@ -894,7 +1040,7 @@ async def test_deleted_device_removing_config_entries( assert len(device_registry.deleted_devices) == 0 assert entry.id == entry2.id assert entry.id != entry3.id - assert entry2.config_entries == [config_entry_2.entry_id, config_entry_1.entry_id] + assert entry2.config_entries == {config_entry_1.entry_id, config_entry_2.entry_id} device_registry.async_remove_device(entry.id) device_registry.async_remove_device(entry3.id) @@ -911,7 +1057,9 @@ async def test_deleted_device_removing_config_entries( assert update_events[1].data == { "action": "update", "device_id": entry2.id, - "changes": {"config_entries": [config_entry_1.entry_id]}, + "changes": { + "config_entries": {config_entry_1.entry_id}, + }, } assert update_events[2].data == { "action": "create", @@ -1290,7 +1438,7 @@ async def test_update( assert updated_entry != entry assert updated_entry == dr.DeviceEntry( area_id="12345A", - config_entries=[mock_config_entry.entry_id], + config_entries={mock_config_entry.entry_id}, configuration_url="https://example.com/config", connections={("mac", "65:43:21:fe:dc:ba")}, disabled_by=dr.DeviceEntryDisabler.USER, @@ -1473,6 +1621,8 @@ async def test_update_remove_config_entries( config_entry_1.add_to_hass(hass) config_entry_2 = MockConfigEntry() config_entry_2.add_to_hass(hass) + config_entry_3 = MockConfigEntry() + config_entry_3.add_to_hass(hass) entry = device_registry.async_get_or_create( config_entry_id=config_entry_1.entry_id, @@ -1495,20 +1645,34 @@ async def test_update_remove_config_entries( manufacturer="manufacturer", model="model", ) + entry4 = device_registry.async_update_device( + entry2.id, add_config_entry_id=config_entry_3.entry_id + ) + # Try to add an unknown config entry + with pytest.raises(HomeAssistantError): + device_registry.async_update_device(entry2.id, add_config_entry_id="blabla") assert len(device_registry.devices) == 2 - assert entry.id == entry2.id + assert entry.id == entry2.id == entry4.id assert entry.id != entry3.id - assert entry2.config_entries == [config_entry_2.entry_id, config_entry_1.entry_id] + assert entry2.config_entries == {config_entry_1.entry_id, config_entry_2.entry_id} + assert entry4.config_entries == { + config_entry_1.entry_id, + config_entry_2.entry_id, + config_entry_3.entry_id, + } - updated_entry = device_registry.async_update_device( + device_registry.async_update_device( entry2.id, remove_config_entry_id=config_entry_1.entry_id ) + updated_entry = device_registry.async_update_device( + entry2.id, remove_config_entry_id=config_entry_3.entry_id + ) removed_entry = device_registry.async_update_device( entry3.id, remove_config_entry_id=config_entry_1.entry_id ) - assert updated_entry.config_entries == [config_entry_2.entry_id] + assert updated_entry.config_entries == {config_entry_2.entry_id} assert removed_entry is None removed_entry = device_registry.async_get_device(identifiers={("bridgeid", "4567")}) @@ -1517,7 +1681,7 @@ async def test_update_remove_config_entries( await hass.async_block_till_done() - assert len(update_events) == 5 + assert len(update_events) == 7 assert update_events[0].data == { "action": "create", "device_id": entry.id, @@ -1525,7 +1689,9 @@ async def test_update_remove_config_entries( assert update_events[1].data == { "action": "update", "device_id": entry2.id, - "changes": {"config_entries": [config_entry_1.entry_id]}, + "changes": { + "config_entries": {config_entry_1.entry_id}, + }, } assert update_events[2].data == { "action": "create", @@ -1535,10 +1701,29 @@ async def test_update_remove_config_entries( "action": "update", "device_id": entry.id, "changes": { - "config_entries": [config_entry_2.entry_id, config_entry_1.entry_id] + "config_entries": {config_entry_1.entry_id, config_entry_2.entry_id} }, } assert update_events[4].data == { + "action": "update", + "device_id": entry2.id, + "changes": { + "config_entries": { + config_entry_1.entry_id, + config_entry_2.entry_id, + config_entry_3.entry_id, + }, + "primary_config_entry": config_entry_1.entry_id, + }, + } + assert update_events[5].data == { + "action": "update", + "device_id": entry2.id, + "changes": { + "config_entries": {config_entry_2.entry_id, config_entry_3.entry_id} + }, + } + assert update_events[6].data == { "action": "remove", "device_id": entry3.id, } @@ -1768,7 +1953,7 @@ async def test_restore_device( assert len(device_registry.devices) == 2 assert len(device_registry.deleted_devices) == 0 - assert isinstance(entry3.config_entries, list) + assert isinstance(entry3.config_entries, set) assert isinstance(entry3.connections, set) assert isinstance(entry3.identifiers, set) @@ -1900,7 +2085,7 @@ async def test_restore_shared_device( assert len(device_registry.devices) == 1 assert len(device_registry.deleted_devices) == 0 - assert isinstance(entry2.config_entries, list) + assert isinstance(entry2.config_entries, set) assert isinstance(entry2.connections, set) assert isinstance(entry2.identifiers, set) @@ -1918,7 +2103,7 @@ async def test_restore_shared_device( assert len(device_registry.devices) == 1 assert len(device_registry.deleted_devices) == 0 - assert isinstance(entry3.config_entries, list) + assert isinstance(entry3.config_entries, set) assert isinstance(entry3.connections, set) assert isinstance(entry3.identifiers, set) @@ -1934,7 +2119,7 @@ async def test_restore_shared_device( assert len(device_registry.devices) == 1 assert len(device_registry.deleted_devices) == 0 - assert isinstance(entry4.config_entries, list) + assert isinstance(entry4.config_entries, set) assert isinstance(entry4.connections, set) assert isinstance(entry4.identifiers, set) @@ -1949,7 +2134,7 @@ async def test_restore_shared_device( "action": "update", "device_id": entry.id, "changes": { - "config_entries": [config_entry_1.entry_id], + "config_entries": {config_entry_1.entry_id}, "identifiers": {("entry_123", "0123")}, }, } @@ -1973,7 +2158,7 @@ async def test_restore_shared_device( "action": "update", "device_id": entry.id, "changes": { - "config_entries": [config_entry_2.entry_id], + "config_entries": {config_entry_2.entry_id}, "identifiers": {("entry_234", "2345")}, }, } @@ -2291,6 +2476,7 @@ async def test_loading_invalid_configuration_url_from_storage( "model": None, "name_by_user": None, "name": None, + "primary_config_entry": "1234", "serial_number": None, "sw_version": None, "via_device_id": None, @@ -2794,3 +2980,75 @@ async def test_device_registry_identifiers_collision( device3_refetched = device_registry.async_get(device3.id) device1_refetched = device_registry.async_get(device1.id) assert not device1_refetched.identifiers.isdisjoint(device3_refetched.identifiers) + + +async def test_primary_config_entry( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, +) -> None: + """Test the primary integration field.""" + mock_config_entry_1 = MockConfigEntry(domain="mqtt", title=None) + mock_config_entry_1.add_to_hass(hass) + mock_config_entry_2 = MockConfigEntry(title=None) + mock_config_entry_2.add_to_hass(hass) + mock_config_entry_3 = MockConfigEntry(title=None) + mock_config_entry_3.add_to_hass(hass) + mock_config_entry_4 = MockConfigEntry(domain="matter", title=None) + mock_config_entry_4.add_to_hass(hass) + + # Create device without model name etc, config entry will not be marked primary + device = device_registry.async_get_or_create( + config_entry_id=mock_config_entry_1.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + identifiers=set(), + ) + assert device.primary_config_entry is None + + # Set model, mqtt config entry will be promoted to primary + device = device_registry.async_get_or_create( + config_entry_id=mock_config_entry_1.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + model="model", + ) + assert device.primary_config_entry == mock_config_entry_1.entry_id + + # New config entry with model will be promoted to primary + device = device_registry.async_get_or_create( + config_entry_id=mock_config_entry_2.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + model="model 2", + ) + assert device.primary_config_entry == mock_config_entry_2.entry_id + + # New config entry with model will not be promoted to primary + device = device_registry.async_get_or_create( + config_entry_id=mock_config_entry_3.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + model="model 3", + ) + assert device.primary_config_entry == mock_config_entry_2.entry_id + + # New matter config entry with model will not be promoted to primary + device = device_registry.async_get_or_create( + config_entry_id=mock_config_entry_4.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + model="model 3", + ) + assert device.primary_config_entry == mock_config_entry_2.entry_id + + # Remove the primary config entry + device = device_registry.async_update_device( + device.id, + remove_config_entry_id=mock_config_entry_2.entry_id, + ) + assert device.primary_config_entry is None + + # Create new + device = device_registry.async_get_or_create( + config_entry_id=mock_config_entry_1.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + identifiers=set(), + manufacturer="manufacturer", + model="model", + ) + assert device.primary_config_entry == mock_config_entry_1.entry_id diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py index 68024bc936f..4e761a21e8c 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -1187,6 +1187,7 @@ async def test_device_info_called( assert device.manufacturer == "test-manuf" assert device.model == "test-model" assert device.name == "test-name" + assert device.primary_config_entry == config_entry.entry_id assert device.suggested_area == "Heliport" assert device.sw_version == "test-sw" assert device.hw_version == "test-hw" diff --git a/tests/helpers/test_entity_registry.py b/tests/helpers/test_entity_registry.py index 1390ef3889d..4dc8d79be3f 100644 --- a/tests/helpers/test_entity_registry.py +++ b/tests/helpers/test_entity_registry.py @@ -1106,10 +1106,10 @@ async def test_remove_config_entry_from_device_removes_entities( config_entry_id=config_entry_2.entry_id, connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, ) - assert device_entry.config_entries == [ + assert device_entry.config_entries == { config_entry_1.entry_id, config_entry_2.entry_id, - ] + } # Create one entity for each config entry entry_1 = entity_registry.async_get_or_create( @@ -1174,10 +1174,10 @@ async def test_remove_config_entry_from_device_removes_entities_2( config_entry_id=config_entry_2.entry_id, connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, ) - assert device_entry.config_entries == [ + assert device_entry.config_entries == { config_entry_1.entry_id, config_entry_2.entry_id, - ] + } # Create one entity for each config entry entry_1 = entity_registry.async_get_or_create( diff --git a/tests/syrupy.py b/tests/syrupy.py index e5bbf017bb3..52bd5756798 100644 --- a/tests/syrupy.py +++ b/tests/syrupy.py @@ -159,6 +159,8 @@ class HomeAssistantSnapshotSerializer(AmberDataSerializer): ) if serialized["via_device_id"] is not None: serialized["via_device_id"] = ANY + if serialized["primary_config_entry"] is not None: + serialized["primary_config_entry"] = ANY return serialized @classmethod From d5bcfe98221db55fdf1a20f7d1bb72f87a77fc1a Mon Sep 17 00:00:00 2001 From: Richard Kroegel <42204099+rikroe@users.noreply.github.com> Date: Wed, 26 Jun 2024 12:27:55 +0200 Subject: [PATCH 1264/1445] Improve BMW tests (#119171) Co-authored-by: Richard --- .../bmw_connected_drive/__init__.py | 5 +- .../bmw_connected_drive/test_init.py | 56 ++++++++++++++++++- .../bmw_connected_drive/test_sensor.py | 44 ++++++++++++++- 3 files changed, 101 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/bmw_connected_drive/__init__.py b/homeassistant/components/bmw_connected_drive/__init__.py index 663003a5e4b..bd4e1cf7360 100644 --- a/homeassistant/components/bmw_connected_drive/__init__.py +++ b/homeassistant/components/bmw_connected_drive/__init__.py @@ -72,7 +72,10 @@ def _async_migrate_options_from_data_if_missing( options = dict(entry.options) if CONF_READ_ONLY in data or list(options) != list(DEFAULT_OPTIONS): - options = dict(DEFAULT_OPTIONS, **options) + options = dict( + DEFAULT_OPTIONS, + **{k: v for k, v in options.items() if k in DEFAULT_OPTIONS}, + ) options[CONF_READ_ONLY] = data.pop(CONF_READ_ONLY, False) hass.config_entries.async_update_entry(entry, data=data, options=options) diff --git a/tests/components/bmw_connected_drive/test_init.py b/tests/components/bmw_connected_drive/test_init.py index d648ad65f5d..52bc8a7ce05 100644 --- a/tests/components/bmw_connected_drive/test_init.py +++ b/tests/components/bmw_connected_drive/test_init.py @@ -4,7 +4,11 @@ from unittest.mock import patch import pytest -from homeassistant.components.bmw_connected_drive.const import DOMAIN as BMW_DOMAIN +from homeassistant.components.bmw_connected_drive import DEFAULT_OPTIONS +from homeassistant.components.bmw_connected_drive.const import ( + CONF_READ_ONLY, + DOMAIN as BMW_DOMAIN, +) from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er @@ -18,6 +22,56 @@ VEHICLE_NAME = "i3 (+ REX)" VEHICLE_NAME_SLUG = "i3_rex" +@pytest.mark.usefixtures("bmw_fixture") +@pytest.mark.parametrize( + "options", + [ + DEFAULT_OPTIONS, + {"other_value": 1, **DEFAULT_OPTIONS}, + {}, + ], +) +async def test_migrate_options( + hass: HomeAssistant, + options: dict, +) -> None: + """Test successful migration of options.""" + + config_entry = FIXTURE_CONFIG_ENTRY.copy() + config_entry["options"] = options + + mock_config_entry = MockConfigEntry(**config_entry) + mock_config_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert len( + hass.config_entries.async_get_entry(mock_config_entry.entry_id).options + ) == len(DEFAULT_OPTIONS) + + +@pytest.mark.usefixtures("bmw_fixture") +async def test_migrate_options_from_data(hass: HomeAssistant) -> None: + """Test successful migration of options.""" + + config_entry = FIXTURE_CONFIG_ENTRY.copy() + config_entry["options"] = {} + config_entry["data"].update({CONF_READ_ONLY: False}) + + mock_config_entry = MockConfigEntry(**config_entry) + mock_config_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + updated_config_entry = hass.config_entries.async_get_entry( + mock_config_entry.entry_id + ) + assert len(updated_config_entry.options) == len(DEFAULT_OPTIONS) + assert CONF_READ_ONLY not in updated_config_entry.data + + @pytest.mark.parametrize( ("entitydata", "old_unique_id", "new_unique_id"), [ diff --git a/tests/components/bmw_connected_drive/test_sensor.py b/tests/components/bmw_connected_drive/test_sensor.py index 6607bed280d..c02f6d425cd 100644 --- a/tests/components/bmw_connected_drive/test_sensor.py +++ b/tests/components/bmw_connected_drive/test_sensor.py @@ -2,13 +2,17 @@ from unittest.mock import patch +from bimmer_connected.models import StrEnum +from bimmer_connected.vehicle import fuel_and_battery +from freezegun.api import FrozenDateTimeFactory import pytest from syrupy.assertion import SnapshotAssertion from homeassistant.components.bmw_connected_drive import DOMAIN as BMW_DOMAIN +from homeassistant.components.bmw_connected_drive.const import SCAN_INTERVALS from homeassistant.components.bmw_connected_drive.sensor import SENSOR_TYPES from homeassistant.components.sensor import SensorDeviceClass -from homeassistant.const import Platform +from homeassistant.const import STATE_UNAVAILABLE, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.helpers.translation import async_get_translations @@ -20,7 +24,7 @@ from homeassistant.util.unit_system import ( from . import setup_mocked_integration -from tests.common import snapshot_platform +from tests.common import async_fire_time_changed, snapshot_platform @pytest.mark.freeze_time("2023-06-22 10:30:00+00:00") @@ -107,3 +111,39 @@ async def test_entity_option_translations( } assert sensor_options == translation_states + + +@pytest.mark.usefixtures("bmw_fixture") +async def test_enum_sensor_unknown( + hass: HomeAssistant, monkeypatch: pytest.MonkeyPatch, freezer: FrozenDateTimeFactory +) -> None: + """Test conversion handling of enum sensors.""" + + # Setup component + assert await setup_mocked_integration(hass) + + entity_id = "sensor.i4_edrive40_charging_status" + + # Check normal state + entity = hass.states.get(entity_id) + assert entity.state == "not_charging" + + class ChargingStateUnkown(StrEnum): + """Charging state of electric vehicle.""" + + UNKNOWN = "UNKNOWN" + + # Setup enum returning only UNKNOWN + monkeypatch.setattr( + fuel_and_battery, + "ChargingState", + ChargingStateUnkown, + ) + + freezer.tick(SCAN_INTERVALS["rest_of_world"]) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + # Check normal state + entity = hass.states.get("sensor.i4_edrive40_charging_status") + assert entity.state == STATE_UNAVAILABLE From be7a2c2cc29c88bbd4a889ff6d3e48b0223eaf26 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 26 Jun 2024 12:40:41 +0200 Subject: [PATCH 1265/1445] Revert "Force alias when importing scene PLATFORM_SCHEMA" (#120540) Revert "Force alias when importing scene PLATFORM_SCHEMA (#120534)" This reverts commit 348ceca19f1fe5b45dadbbd7ec96093c64409a3f. --- homeassistant/components/config/scene.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/config/scene.py b/homeassistant/components/config/scene.py index fa23d02bcc8..a2e2693036a 100644 --- a/homeassistant/components/config/scene.py +++ b/homeassistant/components/config/scene.py @@ -5,10 +5,7 @@ from __future__ import annotations from typing import Any import uuid -from homeassistant.components.scene import ( - DOMAIN, - PLATFORM_SCHEMA as SCENE_PLATFORM_SCHEMA, -) +from homeassistant.components.scene import DOMAIN, PLATFORM_SCHEMA from homeassistant.config import SCENE_CONFIG_PATH from homeassistant.const import CONF_ID, SERVICE_RELOAD from homeassistant.core import DOMAIN as HA_DOMAIN, HomeAssistant, callback @@ -43,7 +40,7 @@ def async_setup(hass: HomeAssistant) -> bool: "config", SCENE_CONFIG_PATH, cv.string, - SCENE_PLATFORM_SCHEMA, + PLATFORM_SCHEMA, post_write_hook=hook, ) ) From 7ef1db054968e8765dbff61f41fefeab6caabaea Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 26 Jun 2024 12:52:31 +0200 Subject: [PATCH 1266/1445] Fix release in MPD issue (#120545) --- homeassistant/components/mpd/media_player.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/mpd/media_player.py b/homeassistant/components/mpd/media_player.py index 0c4a2224e63..eb34fb6289f 100644 --- a/homeassistant/components/mpd/media_player.py +++ b/homeassistant/components/mpd/media_player.py @@ -92,7 +92,7 @@ async def async_setup_platform( hass, HOMEASSISTANT_DOMAIN, f"deprecated_yaml_{DOMAIN}", - breaks_in_ha_version="2024.12.0", + breaks_in_ha_version="2025.1.0", is_fixable=False, issue_domain=DOMAIN, severity=IssueSeverity.WARNING, @@ -107,7 +107,7 @@ async def async_setup_platform( hass, DOMAIN, f"deprecated_yaml_import_issue_{result['reason']}", - breaks_in_ha_version="2024.12.0", + breaks_in_ha_version="2025.1.0", is_fixable=False, issue_domain=DOMAIN, severity=IssueSeverity.WARNING, From 34e266762e56ba9ee6f0eb498bed1b7ba439ccfb Mon Sep 17 00:00:00 2001 From: "Mr. Bubbles" Date: Wed, 26 Jun 2024 13:09:50 +0200 Subject: [PATCH 1267/1445] Remove unnecessary icon states in pyLoad integration (#120548) Remove unnecessary icon states --- homeassistant/components/pyload/icons.json | 2 -- 1 file changed, 2 deletions(-) diff --git a/homeassistant/components/pyload/icons.json b/homeassistant/components/pyload/icons.json index 0e307a43e51..8bcc95c72d7 100644 --- a/homeassistant/components/pyload/icons.json +++ b/homeassistant/components/pyload/icons.json @@ -32,14 +32,12 @@ "download": { "default": "mdi:play", "state": { - "on": "mdi:play", "off": "mdi:pause" } }, "reconnect": { "default": "mdi:restart", "state": { - "on": "mdi:restart", "off": "mdi:restart-off" } } From e8a3e3c8dbef00c894e934cf5fa8ba0988085efd Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Wed, 26 Jun 2024 13:19:34 +0200 Subject: [PATCH 1268/1445] Fix airgradient select entities (#120549) --- .../components/airgradient/select.py | 69 +++++++++---------- .../airgradient/snapshots/test_select.ambr | 8 +-- 2 files changed, 37 insertions(+), 40 deletions(-) diff --git a/homeassistant/components/airgradient/select.py b/homeassistant/components/airgradient/select.py index c37df0483d1..a64ce596806 100644 --- a/homeassistant/components/airgradient/select.py +++ b/homeassistant/components/airgradient/select.py @@ -79,62 +79,59 @@ LED_BAR_ENTITIES: tuple[AirGradientSelectEntityDescription, ...] = ( ), ) -LEARNING_TIME_OFFSET_OPTIONS = { - 12: "12", - 60: "60", - 120: "120", - 360: "360", - 720: "720", -} -LEARNING_TIME_OFFSET_OPTIONS_INVERSE = { - v: k for k, v in LEARNING_TIME_OFFSET_OPTIONS.items() -} -ABC_DAYS = { - 8: "8", - 30: "30", - 90: "90", - 180: "180", - 0: "off", -} -ABC_DAYS_INVERSE = {v: k for k, v in ABC_DAYS.items()} +LEARNING_TIME_OFFSET_OPTIONS = [ + "12", + "60", + "120", + "360", + "720", +] + +ABC_DAYS = [ + "8", + "30", + "90", + "180", + "0", +] + + +def _get_value(value: int, values: list[str]) -> str | None: + str_value = str(value) + return str_value if str_value in values else None + CONTROL_ENTITIES: tuple[AirGradientSelectEntityDescription, ...] = ( AirGradientSelectEntityDescription( key="nox_index_learning_time_offset", translation_key="nox_index_learning_time_offset", - options=list(LEARNING_TIME_OFFSET_OPTIONS_INVERSE), + options=LEARNING_TIME_OFFSET_OPTIONS, entity_category=EntityCategory.CONFIG, - value_fn=lambda config: LEARNING_TIME_OFFSET_OPTIONS.get( - config.nox_learning_offset - ), - set_value_fn=lambda client, value: client.set_nox_learning_offset( - LEARNING_TIME_OFFSET_OPTIONS_INVERSE.get(value, 12) + value_fn=lambda config: _get_value( + config.nox_learning_offset, LEARNING_TIME_OFFSET_OPTIONS ), + set_value_fn=lambda client, value: client.set_nox_learning_offset(int(value)), ), AirGradientSelectEntityDescription( key="voc_index_learning_time_offset", translation_key="voc_index_learning_time_offset", - options=list(LEARNING_TIME_OFFSET_OPTIONS_INVERSE), + options=LEARNING_TIME_OFFSET_OPTIONS, entity_category=EntityCategory.CONFIG, - value_fn=lambda config: LEARNING_TIME_OFFSET_OPTIONS.get( - config.nox_learning_offset - ), - set_value_fn=lambda client, value: client.set_tvoc_learning_offset( - LEARNING_TIME_OFFSET_OPTIONS_INVERSE.get(value, 12) + value_fn=lambda config: _get_value( + config.tvoc_learning_offset, LEARNING_TIME_OFFSET_OPTIONS ), + set_value_fn=lambda client, value: client.set_tvoc_learning_offset(int(value)), ), AirGradientSelectEntityDescription( key="co2_automatic_baseline_calibration", translation_key="co2_automatic_baseline_calibration", - options=list(ABC_DAYS_INVERSE), + options=ABC_DAYS, entity_category=EntityCategory.CONFIG, - value_fn=lambda config: ABC_DAYS.get( - config.co2_automatic_baseline_calibration_days + value_fn=lambda config: _get_value( + config.co2_automatic_baseline_calibration_days, ABC_DAYS ), set_value_fn=lambda client, - value: client.set_co2_automatic_baseline_calibration( - ABC_DAYS_INVERSE.get(value, 0) - ), + value: client.set_co2_automatic_baseline_calibration(int(value)), ), ) diff --git a/tests/components/airgradient/snapshots/test_select.ambr b/tests/components/airgradient/snapshots/test_select.ambr index 19cdc2134fc..ece563b40c6 100644 --- a/tests/components/airgradient/snapshots/test_select.ambr +++ b/tests/components/airgradient/snapshots/test_select.ambr @@ -10,7 +10,7 @@ '30', '90', '180', - 'off', + '0', ]), }), 'config_entry_id': , @@ -49,7 +49,7 @@ '30', '90', '180', - 'off', + '0', ]), }), 'context': , @@ -415,7 +415,7 @@ '30', '90', '180', - 'off', + '0', ]), }), 'config_entry_id': , @@ -454,7 +454,7 @@ '30', '90', '180', - 'off', + '0', ]), }), 'context': , From f0590f08b131b4e6037506e170f02d27acbb230a Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 26 Jun 2024 13:26:53 +0200 Subject: [PATCH 1269/1445] Update frontend to 20240626.0 (#120546) --- 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 1b17601a2f6..063f7db34a0 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==20240610.1"] + "requirements": ["home-assistant-frontend==20240626.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index d3320e64fe3..18461d6398b 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -32,7 +32,7 @@ habluetooth==3.1.3 hass-nabucasa==0.81.1 hassil==1.7.1 home-assistant-bluetooth==1.12.2 -home-assistant-frontend==20240610.1 +home-assistant-frontend==20240626.0 home-assistant-intents==2024.6.21 httpx==0.27.0 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 1a297ef2b5c..2ec7f38e5e3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1093,7 +1093,7 @@ hole==0.8.0 holidays==0.51 # homeassistant.components.frontend -home-assistant-frontend==20240610.1 +home-assistant-frontend==20240626.0 # homeassistant.components.conversation home-assistant-intents==2024.6.21 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 88623000c5e..f0e856c2f6f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -898,7 +898,7 @@ hole==0.8.0 holidays==0.51 # homeassistant.components.frontend -home-assistant-frontend==20240610.1 +home-assistant-frontend==20240626.0 # homeassistant.components.conversation home-assistant-intents==2024.6.21 From a36c40a4346b4e0ffd22b615659cd57dc3fad140 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 26 Jun 2024 13:35:01 +0200 Subject: [PATCH 1270/1445] Use state_reported events in Riemann sum sensor (#113869) --- .../components/integration/sensor.py | 131 ++++++++++++------ tests/components/integration/test_sensor.py | 78 +++-------- 2 files changed, 113 insertions(+), 96 deletions(-) diff --git a/homeassistant/components/integration/sensor.py b/homeassistant/components/integration/sensor.py index 106eb9cc79c..60cbee5549f 100644 --- a/homeassistant/components/integration/sensor.py +++ b/homeassistant/components/integration/sensor.py @@ -8,7 +8,7 @@ from datetime import UTC, datetime, timedelta from decimal import Decimal, InvalidOperation from enum import Enum import logging -from typing import Any, Final, Self +from typing import TYPE_CHECKING, Any, Final, Self import voluptuous as vol @@ -27,6 +27,8 @@ from homeassistant.const import ( CONF_METHOD, CONF_NAME, CONF_UNIQUE_ID, + EVENT_STATE_CHANGED, + EVENT_STATE_REPORTED, STATE_UNAVAILABLE, UnitOfTime, ) @@ -34,6 +36,7 @@ from homeassistant.core import ( CALLBACK_TYPE, Event, EventStateChangedData, + EventStateReportedData, HomeAssistant, State, callback, @@ -42,7 +45,7 @@ from homeassistant.helpers import config_validation as cv, entity_registry as er from homeassistant.helpers.device import async_device_info_to_link_from_entity from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.event import async_call_later, async_track_state_change_event +from homeassistant.helpers.event import async_call_later from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .const import ( @@ -107,9 +110,7 @@ class _IntegrationMethod(ABC): return _NAME_TO_INTEGRATION_METHOD[method_name]() @abstractmethod - def validate_states( - self, left: State, right: State - ) -> tuple[Decimal, Decimal] | None: + def validate_states(self, left: str, right: str) -> tuple[Decimal, Decimal] | None: """Check state requirements for integration.""" @abstractmethod @@ -130,11 +131,9 @@ class _Trapezoidal(_IntegrationMethod): ) -> Decimal: return elapsed_time * (left + right) / 2 - def validate_states( - self, left: State, right: State - ) -> tuple[Decimal, Decimal] | None: - if (left_dec := _decimal_state(left.state)) is None or ( - right_dec := _decimal_state(right.state) + def validate_states(self, left: str, right: str) -> tuple[Decimal, Decimal] | None: + if (left_dec := _decimal_state(left)) is None or ( + right_dec := _decimal_state(right) ) is None: return None return (left_dec, right_dec) @@ -146,10 +145,8 @@ class _Left(_IntegrationMethod): ) -> Decimal: return self.calculate_area_with_one_state(elapsed_time, left) - def validate_states( - self, left: State, right: State - ) -> tuple[Decimal, Decimal] | None: - if (left_dec := _decimal_state(left.state)) is None: + def validate_states(self, left: str, right: str) -> tuple[Decimal, Decimal] | None: + if (left_dec := _decimal_state(left)) is None: return None return (left_dec, left_dec) @@ -160,10 +157,8 @@ class _Right(_IntegrationMethod): ) -> Decimal: return self.calculate_area_with_one_state(elapsed_time, right) - def validate_states( - self, left: State, right: State - ) -> tuple[Decimal, Decimal] | None: - if (right_dec := _decimal_state(right.state)) is None: + def validate_states(self, left: str, right: str) -> tuple[Decimal, Decimal] | None: + if (right_dec := _decimal_state(right)) is None: return None return (right_dec, right_dec) @@ -183,7 +178,7 @@ _NAME_TO_INTEGRATION_METHOD: dict[str, type[_IntegrationMethod]] = { class _IntegrationTrigger(Enum): - StateChange = "state_change" + StateEvent = "state_event" TimeElapsed = "time_elapsed" @@ -343,7 +338,7 @@ class IntegrationSensor(RestoreSensor): ) self._max_sub_interval_exceeded_callback: CALLBACK_TYPE = lambda *args: None self._last_integration_time: datetime = datetime.now(tz=UTC) - self._last_integration_trigger = _IntegrationTrigger.StateChange + self._last_integration_trigger = _IntegrationTrigger.StateEvent self._attr_suggested_display_precision = round_digits or 2 def _calculate_unit(self, source_unit: str) -> str: @@ -433,9 +428,11 @@ class IntegrationSensor(RestoreSensor): source_state = self.hass.states.get(self._sensor_source_id) self._schedule_max_sub_interval_exceeded_if_state_is_numeric(source_state) self.async_on_remove(self._cancel_max_sub_interval_exceeded_callback) - handle_state_change = self._integrate_on_state_change_and_max_sub_interval + handle_state_change = self._integrate_on_state_change_with_max_sub_interval + handle_state_report = self._integrate_on_state_report_with_max_sub_interval else: handle_state_change = self._integrate_on_state_change_callback + handle_state_report = self._integrate_on_state_report_callback if ( state := self.hass.states.get(self._source_entity) @@ -443,16 +440,50 @@ class IntegrationSensor(RestoreSensor): self._derive_and_set_attributes_from_state(state) self.async_on_remove( - async_track_state_change_event( - self.hass, - [self._sensor_source_id], + self.hass.bus.async_listen( + EVENT_STATE_CHANGED, handle_state_change, + event_filter=callback( + lambda event_data: event_data["entity_id"] == self._sensor_source_id + ), + run_immediately=True, + ) + ) + self.async_on_remove( + self.hass.bus.async_listen( + EVENT_STATE_REPORTED, + handle_state_report, + event_filter=callback( + lambda event_data: event_data["entity_id"] == self._sensor_source_id + ), + run_immediately=True, ) ) @callback - def _integrate_on_state_change_and_max_sub_interval( + def _integrate_on_state_change_with_max_sub_interval( self, event: Event[EventStateChangedData] + ) -> None: + """Handle sensor state update when sub interval is configured.""" + self._integrate_on_state_update_with_max_sub_interval( + None, event.data["old_state"], event.data["new_state"] + ) + + @callback + def _integrate_on_state_report_with_max_sub_interval( + self, event: Event[EventStateReportedData] + ) -> None: + """Handle sensor state report when sub interval is configured.""" + self._integrate_on_state_update_with_max_sub_interval( + event.data["old_last_reported"], None, event.data["new_state"] + ) + + @callback + def _integrate_on_state_update_with_max_sub_interval( + self, + old_last_reported: datetime | None, + old_state: State | None, + new_state: State | None, ) -> None: """Integrate based on state change and time. @@ -460,11 +491,9 @@ class IntegrationSensor(RestoreSensor): reschedules time based integration. """ self._cancel_max_sub_interval_exceeded_callback() - old_state = event.data["old_state"] - new_state = event.data["new_state"] try: - self._integrate_on_state_change(old_state, new_state) - self._last_integration_trigger = _IntegrationTrigger.StateChange + self._integrate_on_state_change(old_last_reported, old_state, new_state) + self._last_integration_trigger = _IntegrationTrigger.StateEvent self._last_integration_time = datetime.now(tz=UTC) finally: # When max_sub_interval exceeds without state change the source is assumed @@ -475,13 +504,25 @@ class IntegrationSensor(RestoreSensor): def _integrate_on_state_change_callback( self, event: Event[EventStateChangedData] ) -> None: - """Handle the sensor state changes.""" - old_state = event.data["old_state"] - new_state = event.data["new_state"] - return self._integrate_on_state_change(old_state, new_state) + """Handle sensor state change.""" + return self._integrate_on_state_change( + None, event.data["old_state"], event.data["new_state"] + ) + + @callback + def _integrate_on_state_report_callback( + self, event: Event[EventStateReportedData] + ) -> None: + """Handle sensor state report.""" + return self._integrate_on_state_change( + event.data["old_last_reported"], None, event.data["new_state"] + ) def _integrate_on_state_change( - self, old_state: State | None, new_state: State | None + self, + old_last_reported: datetime | None, + old_state: State | None, + new_state: State | None, ) -> None: if new_state is None: return @@ -491,21 +532,33 @@ class IntegrationSensor(RestoreSensor): self.async_write_ha_state() return + if old_state: + # state has changed, we recover old_state from the event + old_state_state = old_state.state + old_last_reported = old_state.last_reported + else: + # event state reported without any state change + old_state_state = new_state.state + self._attr_available = True self._derive_and_set_attributes_from_state(new_state) - if old_state is None: + if old_last_reported is None and old_state is None: self.async_write_ha_state() return - if not (states := self._method.validate_states(old_state, new_state)): + if not ( + states := self._method.validate_states(old_state_state, new_state.state) + ): self.async_write_ha_state() return + if TYPE_CHECKING: + assert old_last_reported is not None elapsed_seconds = Decimal( - (new_state.last_updated - old_state.last_updated).total_seconds() - if self._last_integration_trigger == _IntegrationTrigger.StateChange - else (new_state.last_updated - self._last_integration_time).total_seconds() + (new_state.last_reported - old_last_reported).total_seconds() + if self._last_integration_trigger == _IntegrationTrigger.StateEvent + else (new_state.last_reported - self._last_integration_time).total_seconds() ) area = self._method.calculate_area_with_two_states(elapsed_seconds, *states) diff --git a/tests/components/integration/test_sensor.py b/tests/components/integration/test_sensor.py index 10f921ce603..974c8bb8691 100644 --- a/tests/components/integration/test_sensor.py +++ b/tests/components/integration/test_sensor.py @@ -294,28 +294,16 @@ async def test_restore_state_failed(hass: HomeAssistant, extra_attributes) -> No assert state.state == STATE_UNKNOWN +@pytest.mark.parametrize("force_update", [False, True]) @pytest.mark.parametrize( - ("force_update", "sequence"), + "sequence", [ ( - False, - ( - (20, 10, 1.67), - (30, 30, 5.0), - (40, 5, 7.92), - (50, 5, 7.92), - (60, 0, 8.75), - ), - ), - ( - True, - ( - (20, 10, 1.67), - (30, 30, 5.0), - (40, 5, 7.92), - (50, 5, 8.75), - (60, 0, 9.17), - ), + (20, 10, 1.67), + (30, 30, 5.0), + (40, 5, 7.92), + (50, 5, 8.75), + (60, 0, 9.17), ), ], ) @@ -358,28 +346,16 @@ async def test_trapezoidal( assert state.attributes.get("unit_of_measurement") == UnitOfEnergy.KILO_WATT_HOUR +@pytest.mark.parametrize("force_update", [False, True]) @pytest.mark.parametrize( - ("force_update", "sequence"), + "sequence", [ ( - False, - ( - (20, 10, 0.0), - (30, 30, 1.67), - (40, 5, 6.67), - (50, 5, 6.67), - (60, 0, 8.33), - ), - ), - ( - True, - ( - (20, 10, 0.0), - (30, 30, 1.67), - (40, 5, 6.67), - (50, 5, 7.5), - (60, 0, 8.33), - ), + (20, 10, 0.0), + (30, 30, 1.67), + (40, 5, 6.67), + (50, 5, 7.5), + (60, 0, 8.33), ), ], ) @@ -425,28 +401,16 @@ async def test_left( assert state.attributes.get("unit_of_measurement") == UnitOfEnergy.KILO_WATT_HOUR +@pytest.mark.parametrize("force_update", [False, True]) @pytest.mark.parametrize( - ("force_update", "sequence"), + "sequence", [ ( - False, - ( - (20, 10, 3.33), - (30, 30, 8.33), - (40, 5, 9.17), - (50, 5, 9.17), - (60, 0, 9.17), - ), - ), - ( - True, - ( - (20, 10, 3.33), - (30, 30, 8.33), - (40, 5, 9.17), - (50, 5, 10.0), - (60, 0, 10.0), - ), + (20, 10, 3.33), + (30, 30, 8.33), + (40, 5, 9.17), + (50, 5, 10.0), + (60, 0, 10.0), ), ], ) From 13a9efb6a6045bbe9afc3f01be12cbd66ba3a332 Mon Sep 17 00:00:00 2001 From: "Mr. Bubbles" Date: Wed, 26 Jun 2024 13:36:01 +0200 Subject: [PATCH 1271/1445] Convert dataclass to dict in pyLoad diagnostics (#120552) --- homeassistant/components/pyload/diagnostics.py | 3 ++- .../components/pyload/snapshots/test_diagnostics.ambr | 11 +++++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/pyload/diagnostics.py b/homeassistant/components/pyload/diagnostics.py index d18e5a5fe0d..95ff37bf9f8 100644 --- a/homeassistant/components/pyload/diagnostics.py +++ b/homeassistant/components/pyload/diagnostics.py @@ -2,6 +2,7 @@ from __future__ import annotations +from dataclasses import asdict from typing import Any from homeassistant.components.diagnostics import async_redact_data @@ -22,5 +23,5 @@ async def async_get_config_entry_diagnostics( return { "config_entry_data": async_redact_data(dict(config_entry.data), TO_REDACT), - "pyload_data": pyload_data, + "pyload_data": asdict(pyload_data), } diff --git a/tests/components/pyload/snapshots/test_diagnostics.ambr b/tests/components/pyload/snapshots/test_diagnostics.ambr index 8c3e110f2ec..0e078e000c9 100644 --- a/tests/components/pyload/snapshots/test_diagnostics.ambr +++ b/tests/components/pyload/snapshots/test_diagnostics.ambr @@ -10,8 +10,15 @@ 'verify_ssl': False, }), 'pyload_data': dict({ - '__type': "", - 'repr': 'pyLoadData(pause=False, active=1, queue=6, total=37, speed=5405963.0, download=True, reconnect=False, captcha=False, free_space=99999999999)', + 'active': 1, + 'captcha': False, + 'download': True, + 'free_space': 99999999999, + 'pause': False, + 'queue': 6, + 'reconnect': False, + 'speed': 5405963.0, + 'total': 37, }), }) # --- From 972b85a75b644ca24dc3ebf5b60575ae656321fd Mon Sep 17 00:00:00 2001 From: "Mr. Bubbles" Date: Wed, 26 Jun 2024 13:36:25 +0200 Subject: [PATCH 1272/1445] Fix class and variable naming errors in pyLoad integration (#120547) --- homeassistant/components/pyload/sensor.py | 2 +- homeassistant/components/pyload/switch.py | 14 +++++++------- tests/components/pyload/snapshots/test_switch.ambr | 4 ++-- tests/components/pyload/test_switch.py | 7 +++---- 4 files changed, 13 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/pyload/sensor.py b/homeassistant/components/pyload/sensor.py index 6cb432e12fd..83585a60c6d 100644 --- a/homeassistant/components/pyload/sensor.py +++ b/homeassistant/components/pyload/sensor.py @@ -181,7 +181,7 @@ class PyLoadSensor(CoordinatorEntity[PyLoadCoordinator], SensorEntity): f"{coordinator.config_entry.entry_id}_{entity_description.key}" ) self.entity_description = entity_description - self.device_info = DeviceInfo( + self._attr_device_info = DeviceInfo( entry_type=DeviceEntryType.SERVICE, manufacturer=MANUFACTURER, model=SERVICE_NAME, diff --git a/homeassistant/components/pyload/switch.py b/homeassistant/components/pyload/switch.py index b9391ef818f..b0628005008 100644 --- a/homeassistant/components/pyload/switch.py +++ b/homeassistant/components/pyload/switch.py @@ -24,7 +24,7 @@ from .const import DOMAIN, MANUFACTURER, SERVICE_NAME from .coordinator import PyLoadCoordinator -class PyLoadSwitchEntity(StrEnum): +class PyLoadSwitch(StrEnum): """PyLoad Switch Entities.""" PAUSE_RESUME_QUEUE = "download" @@ -42,16 +42,16 @@ class PyLoadSwitchEntityDescription(SwitchEntityDescription): SENSOR_DESCRIPTIONS: tuple[PyLoadSwitchEntityDescription, ...] = ( PyLoadSwitchEntityDescription( - key=PyLoadSwitchEntity.PAUSE_RESUME_QUEUE, - translation_key=PyLoadSwitchEntity.PAUSE_RESUME_QUEUE, + key=PyLoadSwitch.PAUSE_RESUME_QUEUE, + translation_key=PyLoadSwitch.PAUSE_RESUME_QUEUE, device_class=SwitchDeviceClass.SWITCH, turn_on_fn=lambda api: api.unpause(), turn_off_fn=lambda api: api.pause(), toggle_fn=lambda api: api.toggle_pause(), ), PyLoadSwitchEntityDescription( - key=PyLoadSwitchEntity.RECONNECT, - translation_key=PyLoadSwitchEntity.RECONNECT, + key=PyLoadSwitch.RECONNECT, + translation_key=PyLoadSwitch.RECONNECT, device_class=SwitchDeviceClass.SWITCH, turn_on_fn=lambda api: api.toggle_reconnect(), turn_off_fn=lambda api: api.toggle_reconnect(), @@ -70,12 +70,12 @@ async def async_setup_entry( coordinator = entry.runtime_data async_add_entities( - PyLoadBinarySensor(coordinator, description) + PyLoadSwitchEntity(coordinator, description) for description in SENSOR_DESCRIPTIONS ) -class PyLoadBinarySensor(CoordinatorEntity[PyLoadCoordinator], SwitchEntity): +class PyLoadSwitchEntity(CoordinatorEntity[PyLoadCoordinator], SwitchEntity): """Representation of a pyLoad sensor.""" _attr_has_entity_name = True diff --git a/tests/components/pyload/snapshots/test_switch.ambr b/tests/components/pyload/snapshots/test_switch.ambr index 94f2910cad8..b6465341b0a 100644 --- a/tests/components/pyload/snapshots/test_switch.ambr +++ b/tests/components/pyload/snapshots/test_switch.ambr @@ -27,7 +27,7 @@ 'platform': 'pyload', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': , + 'translation_key': , 'unique_id': 'XXXXXXXXXXXXXX_reconnect', 'unit_of_measurement': None, }) @@ -74,7 +74,7 @@ 'platform': 'pyload', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': , + 'translation_key': , 'unique_id': 'XXXXXXXXXXXXXX_download', 'unit_of_measurement': None, }) diff --git a/tests/components/pyload/test_switch.py b/tests/components/pyload/test_switch.py index e7bd5a24a87..42a6bfa6f14 100644 --- a/tests/components/pyload/test_switch.py +++ b/tests/components/pyload/test_switch.py @@ -6,7 +6,7 @@ from unittest.mock import AsyncMock, call, patch import pytest from syrupy.assertion import SnapshotAssertion -from homeassistant.components.pyload.switch import PyLoadSwitchEntity +from homeassistant.components.pyload.switch import PyLoadSwitch from homeassistant.components.switch import ( DOMAIN as SWITCH_DOMAIN, SERVICE_TOGGLE, @@ -22,12 +22,12 @@ from tests.common import MockConfigEntry, snapshot_platform # Maps entity to the mock calls to assert API_CALL = { - PyLoadSwitchEntity.PAUSE_RESUME_QUEUE: { + PyLoadSwitch.PAUSE_RESUME_QUEUE: { SERVICE_TURN_ON: call.unpause, SERVICE_TURN_OFF: call.pause, SERVICE_TOGGLE: call.toggle_pause, }, - PyLoadSwitchEntity.RECONNECT: { + PyLoadSwitch.RECONNECT: { SERVICE_TURN_ON: call.toggle_reconnect, SERVICE_TURN_OFF: call.toggle_reconnect, SERVICE_TOGGLE: call.toggle_reconnect, @@ -97,7 +97,6 @@ async def test_turn_on_off( {ATTR_ENTITY_ID: entity_entry.entity_id}, blocking=True, ) - await hass.async_block_till_done() assert ( API_CALL[entity_entry.translation_key][service_call] in mock_pyloadapi.method_calls From b07453dca404c42637733fa134259c7c531239c7 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk <11290930+bouwew@users.noreply.github.com> Date: Wed, 26 Jun 2024 13:37:08 +0200 Subject: [PATCH 1273/1445] Implement remaining select-adaptions for Plugwise (#120544) --- homeassistant/components/plugwise/select.py | 2 +- tests/components/plugwise/test_select.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/plugwise/select.py b/homeassistant/components/plugwise/select.py index 99aecacb96b..b7d4a0a1ded 100644 --- a/homeassistant/components/plugwise/select.py +++ b/homeassistant/components/plugwise/select.py @@ -109,5 +109,5 @@ class PlugwiseSelectEntity(PlugwiseEntity, SelectEntity): self.device[LOCATION] and STATE_ON are required for the thermostat-schedule select. """ await self.coordinator.api.set_select( - self.entity_description.key, self.device[LOCATION], STATE_ON, option + self.entity_description.key, self.device[LOCATION], option, STATE_ON ) diff --git a/tests/components/plugwise/test_select.py b/tests/components/plugwise/test_select.py index a6245ff11e7..b9dec283bc4 100644 --- a/tests/components/plugwise/test_select.py +++ b/tests/components/plugwise/test_select.py @@ -42,8 +42,8 @@ async def test_adam_change_select_entity( mock_smile_adam.set_select.assert_called_with( "select_schedule", "c50f167537524366a5af7aa3942feb1e", - "on", "Badkamer Schema", + "on", ) @@ -74,6 +74,6 @@ async def test_adam_select_regulation_mode( mock_smile_adam_3.set_select.assert_called_with( "select_regulation_mode", "bc93488efab249e5bc54fd7e175a6f91", - "on", "heating", + "on", ) From 1d0aa6bff0b69e7388fdfd0d761c71cfd239017d Mon Sep 17 00:00:00 2001 From: "Mr. Bubbles" Date: Wed, 26 Jun 2024 13:40:20 +0200 Subject: [PATCH 1274/1445] Update docstrings in pyLoad tests (#120556) --- tests/components/pyload/test_button.py | 2 +- tests/components/pyload/test_config_flow.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/components/pyload/test_button.py b/tests/components/pyload/test_button.py index b30a4cefd42..b5aa18ad3d9 100644 --- a/tests/components/pyload/test_button.py +++ b/tests/components/pyload/test_button.py @@ -59,7 +59,7 @@ async def test_button_press( mock_pyloadapi: AsyncMock, entity_registry: er.EntityRegistry, ) -> None: - """Test switch turn on method.""" + """Test button press method.""" config_entry.add_to_hass(hass) await hass.config_entries.async_setup(config_entry.entry_id) diff --git a/tests/components/pyload/test_config_flow.py b/tests/components/pyload/test_config_flow.py index 8e9083a49c8..8c775412371 100644 --- a/tests/components/pyload/test_config_flow.py +++ b/tests/components/pyload/test_config_flow.py @@ -262,7 +262,7 @@ async def test_reconfiguration( config_entry: MockConfigEntry, mock_pyloadapi: AsyncMock, ) -> None: - """Test reauth flow.""" + """Test reconfiguration flow.""" config_entry.add_to_hass(hass) @@ -304,7 +304,7 @@ async def test_reconfigure_errors( side_effect: Exception, error_text: str, ) -> None: - """Test reauth flow.""" + """Test reconfiguration flow.""" config_entry.add_to_hass(hass) From 0d2aeb846f3cc9f9f799cfd0db83567c4f338e14 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 26 Jun 2024 14:05:24 +0200 Subject: [PATCH 1275/1445] Increase max temperature to 40 for Tado (#120560) --- homeassistant/components/tado/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/tado/const.py b/homeassistant/components/tado/const.py index a41003da95f..5c6a80c5beb 100644 --- a/homeassistant/components/tado/const.py +++ b/homeassistant/components/tado/const.py @@ -226,7 +226,7 @@ HA_TERMINATION_TYPE = "default_overlay_type" HA_TERMINATION_DURATION = "default_overlay_seconds" TADO_DEFAULT_MIN_TEMP = 5 -TADO_DEFAULT_MAX_TEMP = 25 +TADO_DEFAULT_MAX_TEMP = 40 # Constants for service calls SERVICE_ADD_METER_READING = "add_meter_reading" CONF_CONFIG_ENTRY = "config_entry" From 69e0227682eec31d365e2efab9aec58f66972f49 Mon Sep 17 00:00:00 2001 From: Luke Lashley Date: Wed, 26 Jun 2024 08:13:49 -0400 Subject: [PATCH 1276/1445] Add Roborock to strict typing (#120379) --- .strict-typing | 1 + homeassistant/components/roborock/image.py | 6 ++++-- homeassistant/components/roborock/number.py | 3 ++- homeassistant/components/roborock/switch.py | 10 +++++----- mypy.ini | 10 ++++++++++ 5 files changed, 22 insertions(+), 8 deletions(-) diff --git a/.strict-typing b/.strict-typing index 2a6edfedd32..a6deb6eca3a 100644 --- a/.strict-typing +++ b/.strict-typing @@ -370,6 +370,7 @@ homeassistant.components.rhasspy.* homeassistant.components.ridwell.* homeassistant.components.ring.* homeassistant.components.rituals_perfume_genie.* +homeassistant.components.roborock.* homeassistant.components.roku.* homeassistant.components.romy.* homeassistant.components.rpi_power.* diff --git a/homeassistant/components/roborock/image.py b/homeassistant/components/roborock/image.py index afe1e781a88..33b8b0a2c90 100644 --- a/homeassistant/components/roborock/image.py +++ b/homeassistant/components/roborock/image.py @@ -1,6 +1,7 @@ """Support for Roborock image.""" import asyncio +from datetime import datetime import io from itertools import chain @@ -48,6 +49,7 @@ class RoborockMap(RoborockCoordinatedEntity, ImageEntity): """A class to let you visualize the map.""" _attr_has_entity_name = True + image_last_updated: datetime def __init__( self, @@ -76,7 +78,7 @@ class RoborockMap(RoborockCoordinatedEntity, ImageEntity): self._attr_entity_category = EntityCategory.DIAGNOSTIC @property - def available(self): + def available(self) -> bool: """Determines if the entity is available.""" return self.cached_map != b"" @@ -98,7 +100,7 @@ class RoborockMap(RoborockCoordinatedEntity, ImageEntity): and bool(self.coordinator.roborock_device_info.props.status.in_cleaning) ) - def _handle_coordinator_update(self): + def _handle_coordinator_update(self) -> None: # Bump last updated every third time the coordinator runs, so that async_image # will be called and we will evaluate on the new coordinator data if we should # update the cache. diff --git a/homeassistant/components/roborock/number.py b/homeassistant/components/roborock/number.py index 8aa20fad838..a432c527b0e 100644 --- a/homeassistant/components/roborock/number.py +++ b/homeassistant/components/roborock/number.py @@ -107,7 +107,8 @@ class RoborockNumberEntity(RoborockEntity, NumberEntity): @property def native_value(self) -> float | None: """Get native value.""" - return self.get_cache(self.entity_description.cache_key).value + val: float = self.get_cache(self.entity_description.cache_key).value + return val async def async_set_native_value(self, value: float) -> None: """Set number value.""" diff --git a/homeassistant/components/roborock/switch.py b/homeassistant/components/roborock/switch.py index 7e17844666e..9a34060fe96 100644 --- a/homeassistant/components/roborock/switch.py +++ b/homeassistant/components/roborock/switch.py @@ -167,9 +167,9 @@ class RoborockSwitch(RoborockEntity, SwitchEntity): @property def is_on(self) -> bool | None: """Return True if entity is on.""" - return ( - self.get_cache(self.entity_description.cache_key).value.get( - self.entity_description.attribute - ) - == 1 + status = self.get_cache(self.entity_description.cache_key).value.get( + self.entity_description.attribute ) + if status is None: + return status + return bool(status) diff --git a/mypy.ini b/mypy.ini index 740eb4f2b5b..d94e5a37194 100644 --- a/mypy.ini +++ b/mypy.ini @@ -3463,6 +3463,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.roborock.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.roku.*] check_untyped_defs = true disallow_incomplete_defs = true From f5ff19d60274a662c3d186d312ba95c5829a23d4 Mon Sep 17 00:00:00 2001 From: "Mr. Bubbles" Date: Wed, 26 Jun 2024 14:14:48 +0200 Subject: [PATCH 1277/1445] Add measurement unit and state_class to sensors in pyLoad (#120551) --- homeassistant/components/pyload/const.py | 2 + homeassistant/components/pyload/sensor.py | 8 ++ .../pyload/snapshots/test_sensor.ambr | 96 ++++++++++++++----- 3 files changed, 82 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/pyload/const.py b/homeassistant/components/pyload/const.py index 9419786fd88..a0b66687bd0 100644 --- a/homeassistant/components/pyload/const.py +++ b/homeassistant/components/pyload/const.py @@ -10,3 +10,5 @@ ISSUE_PLACEHOLDER = {"url": "/config/integrations/dashboard/add?domain=pyload"} MANUFACTURER = "pyLoad Team" SERVICE_NAME = "pyLoad" + +UNIT_DOWNLOADS = "downloads" diff --git a/homeassistant/components/pyload/sensor.py b/homeassistant/components/pyload/sensor.py index 83585a60c6d..bc90fdb7ccb 100644 --- a/homeassistant/components/pyload/sensor.py +++ b/homeassistant/components/pyload/sensor.py @@ -11,6 +11,7 @@ from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, SensorEntityDescription, + SensorStateClass, ) from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import ( @@ -42,6 +43,7 @@ from .const import ( ISSUE_PLACEHOLDER, MANUFACTURER, SERVICE_NAME, + UNIT_DOWNLOADS, ) from .coordinator import PyLoadCoordinator @@ -68,14 +70,20 @@ SENSOR_DESCRIPTIONS: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key=PyLoadSensorEntity.ACTIVE, translation_key=PyLoadSensorEntity.ACTIVE, + native_unit_of_measurement=UNIT_DOWNLOADS, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=PyLoadSensorEntity.QUEUE, translation_key=PyLoadSensorEntity.QUEUE, + native_unit_of_measurement=UNIT_DOWNLOADS, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=PyLoadSensorEntity.TOTAL, translation_key=PyLoadSensorEntity.TOTAL, + native_unit_of_measurement=UNIT_DOWNLOADS, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=PyLoadSensorEntity.FREE_SPACE, diff --git a/tests/components/pyload/snapshots/test_sensor.ambr b/tests/components/pyload/snapshots/test_sensor.ambr index 159309041e0..c1e5a9d6c3a 100644 --- a/tests/components/pyload/snapshots/test_sensor.ambr +++ b/tests/components/pyload/snapshots/test_sensor.ambr @@ -4,7 +4,9 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'device_class': None, 'device_id': , @@ -29,13 +31,15 @@ 'supported_features': 0, 'translation_key': , 'unique_id': 'XXXXXXXXXXXXXX_active', - 'unit_of_measurement': None, + 'unit_of_measurement': 'downloads', }) # --- # name: test_sensor_update_exceptions[CannotConnect][sensor.pyload_active_downloads-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'pyLoad Active downloads', + 'state_class': , + 'unit_of_measurement': 'downloads', }), 'context': , 'entity_id': 'sensor.pyload_active_downloads', @@ -50,7 +54,9 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'device_class': None, 'device_id': , @@ -75,13 +81,15 @@ 'supported_features': 0, 'translation_key': , 'unique_id': 'XXXXXXXXXXXXXX_queue', - 'unit_of_measurement': None, + 'unit_of_measurement': 'downloads', }) # --- # name: test_sensor_update_exceptions[CannotConnect][sensor.pyload_downloads_in_queue-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'pyLoad Downloads in queue', + 'state_class': , + 'unit_of_measurement': 'downloads', }), 'context': , 'entity_id': 'sensor.pyload_downloads_in_queue', @@ -96,7 +104,9 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'device_class': None, 'device_id': , @@ -121,13 +131,15 @@ 'supported_features': 0, 'translation_key': , 'unique_id': 'XXXXXXXXXXXXXX_total', - 'unit_of_measurement': None, + 'unit_of_measurement': 'downloads', }) # --- # name: test_sensor_update_exceptions[CannotConnect][sensor.pyload_finished_downloads-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'pyLoad Finished downloads', + 'state_class': , + 'unit_of_measurement': 'downloads', }), 'context': , 'entity_id': 'sensor.pyload_finished_downloads', @@ -250,7 +262,9 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'device_class': None, 'device_id': , @@ -275,13 +289,15 @@ 'supported_features': 0, 'translation_key': , 'unique_id': 'XXXXXXXXXXXXXX_active', - 'unit_of_measurement': None, + 'unit_of_measurement': 'downloads', }) # --- # name: test_sensor_update_exceptions[InvalidAuth][sensor.pyload_active_downloads-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'pyLoad Active downloads', + 'state_class': , + 'unit_of_measurement': 'downloads', }), 'context': , 'entity_id': 'sensor.pyload_active_downloads', @@ -296,7 +312,9 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'device_class': None, 'device_id': , @@ -321,13 +339,15 @@ 'supported_features': 0, 'translation_key': , 'unique_id': 'XXXXXXXXXXXXXX_queue', - 'unit_of_measurement': None, + 'unit_of_measurement': 'downloads', }) # --- # name: test_sensor_update_exceptions[InvalidAuth][sensor.pyload_downloads_in_queue-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'pyLoad Downloads in queue', + 'state_class': , + 'unit_of_measurement': 'downloads', }), 'context': , 'entity_id': 'sensor.pyload_downloads_in_queue', @@ -342,7 +362,9 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'device_class': None, 'device_id': , @@ -367,13 +389,15 @@ 'supported_features': 0, 'translation_key': , 'unique_id': 'XXXXXXXXXXXXXX_total', - 'unit_of_measurement': None, + 'unit_of_measurement': 'downloads', }) # --- # name: test_sensor_update_exceptions[InvalidAuth][sensor.pyload_finished_downloads-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'pyLoad Finished downloads', + 'state_class': , + 'unit_of_measurement': 'downloads', }), 'context': , 'entity_id': 'sensor.pyload_finished_downloads', @@ -496,7 +520,9 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'device_class': None, 'device_id': , @@ -521,13 +547,15 @@ 'supported_features': 0, 'translation_key': , 'unique_id': 'XXXXXXXXXXXXXX_active', - 'unit_of_measurement': None, + 'unit_of_measurement': 'downloads', }) # --- # name: test_sensor_update_exceptions[ParserError][sensor.pyload_active_downloads-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'pyLoad Active downloads', + 'state_class': , + 'unit_of_measurement': 'downloads', }), 'context': , 'entity_id': 'sensor.pyload_active_downloads', @@ -542,7 +570,9 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'device_class': None, 'device_id': , @@ -567,13 +597,15 @@ 'supported_features': 0, 'translation_key': , 'unique_id': 'XXXXXXXXXXXXXX_queue', - 'unit_of_measurement': None, + 'unit_of_measurement': 'downloads', }) # --- # name: test_sensor_update_exceptions[ParserError][sensor.pyload_downloads_in_queue-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'pyLoad Downloads in queue', + 'state_class': , + 'unit_of_measurement': 'downloads', }), 'context': , 'entity_id': 'sensor.pyload_downloads_in_queue', @@ -588,7 +620,9 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'device_class': None, 'device_id': , @@ -613,13 +647,15 @@ 'supported_features': 0, 'translation_key': , 'unique_id': 'XXXXXXXXXXXXXX_total', - 'unit_of_measurement': None, + 'unit_of_measurement': 'downloads', }) # --- # name: test_sensor_update_exceptions[ParserError][sensor.pyload_finished_downloads-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'pyLoad Finished downloads', + 'state_class': , + 'unit_of_measurement': 'downloads', }), 'context': , 'entity_id': 'sensor.pyload_finished_downloads', @@ -742,7 +778,9 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'device_class': None, 'device_id': , @@ -767,13 +805,15 @@ 'supported_features': 0, 'translation_key': , 'unique_id': 'XXXXXXXXXXXXXX_active', - 'unit_of_measurement': None, + 'unit_of_measurement': 'downloads', }) # --- # name: test_setup[sensor.pyload_active_downloads-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'pyLoad Active downloads', + 'state_class': , + 'unit_of_measurement': 'downloads', }), 'context': , 'entity_id': 'sensor.pyload_active_downloads', @@ -788,7 +828,9 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'device_class': None, 'device_id': , @@ -813,13 +855,15 @@ 'supported_features': 0, 'translation_key': , 'unique_id': 'XXXXXXXXXXXXXX_queue', - 'unit_of_measurement': None, + 'unit_of_measurement': 'downloads', }) # --- # name: test_setup[sensor.pyload_downloads_in_queue-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'pyLoad Downloads in queue', + 'state_class': , + 'unit_of_measurement': 'downloads', }), 'context': , 'entity_id': 'sensor.pyload_downloads_in_queue', @@ -834,7 +878,9 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'device_class': None, 'device_id': , @@ -859,13 +905,15 @@ 'supported_features': 0, 'translation_key': , 'unique_id': 'XXXXXXXXXXXXXX_total', - 'unit_of_measurement': None, + 'unit_of_measurement': 'downloads', }) # --- # name: test_setup[sensor.pyload_finished_downloads-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'pyLoad Finished downloads', + 'state_class': , + 'unit_of_measurement': 'downloads', }), 'context': , 'entity_id': 'sensor.pyload_finished_downloads', From d515a7f0634641a6aa4990d86621542f249cf175 Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Wed, 26 Jun 2024 07:20:11 -0500 Subject: [PATCH 1278/1445] Add created_seconds to timer info and pass to ESPHome devices (#120364) --- .../components/esphome/voice_assistant.py | 2 +- homeassistant/components/intent/timers.py | 17 +++++++++++ .../esphome/test_voice_assistant.py | 29 +++++++++++++++++-- tests/components/intent/test_timers.py | 7 +++++ 4 files changed, 52 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/esphome/voice_assistant.py b/homeassistant/components/esphome/voice_assistant.py index 10358d871ca..a6cedee30ab 100644 --- a/homeassistant/components/esphome/voice_assistant.py +++ b/homeassistant/components/esphome/voice_assistant.py @@ -467,7 +467,7 @@ def handle_timer_event( native_event_type, timer_info.id, timer_info.name, - timer_info.seconds, + timer_info.created_seconds, timer_info.seconds_left, timer_info.is_active, ) diff --git a/homeassistant/components/intent/timers.py b/homeassistant/components/intent/timers.py index 82f6121da53..a8576509a4b 100644 --- a/homeassistant/components/intent/timers.py +++ b/homeassistant/components/intent/timers.py @@ -93,6 +93,13 @@ class TimerInfo: This agent will be used to execute the conversation command. """ + _created_seconds: int = 0 + """Number of seconds on the timer when it was created.""" + + def __post_init__(self) -> None: + """Post initialization.""" + self._created_seconds = self.seconds + @property def seconds_left(self) -> int: """Return number of seconds left on the timer.""" @@ -103,6 +110,15 @@ class TimerInfo: seconds_running = int((now - self.updated_at) / 1e9) return max(0, self.seconds - seconds_running) + @property + def created_seconds(self) -> int: + """Return number of seconds on the timer when it was created. + + This value is increased if time is added to the timer, exceeding its + original created_seconds. + """ + return self._created_seconds + @cached_property def name_normalized(self) -> str: """Return normalized timer name.""" @@ -131,6 +147,7 @@ class TimerInfo: Seconds may be negative to remove time instead. """ self.seconds = max(0, self.seconds_left + seconds) + self._created_seconds = max(self._created_seconds, self.seconds) self.updated_at = time.monotonic_ns() def finish(self) -> None: diff --git a/tests/components/esphome/test_voice_assistant.py b/tests/components/esphome/test_voice_assistant.py index bcd49f91c03..c347c3dc7d3 100644 --- a/tests/components/esphome/test_voice_assistant.py +++ b/tests/components/esphome/test_voice_assistant.py @@ -836,6 +836,7 @@ async def test_timer_events( connections={(dr.CONNECTION_NETWORK_MAC, mock_device.entry.unique_id)} ) + total_seconds = (1 * 60 * 60) + (2 * 60) + 3 await intent_helper.async_handle( hass, "test", @@ -853,8 +854,32 @@ async def test_timer_events( VoiceAssistantTimerEventType.VOICE_ASSISTANT_TIMER_STARTED, ANY, "test timer", - 3723, - 3723, + total_seconds, + total_seconds, + True, + ) + + # Increase timer beyond original time and check total_seconds has increased + mock_client.send_voice_assistant_timer_event.reset_mock() + + total_seconds += 5 * 60 + await intent_helper.async_handle( + hass, + "test", + intent_helper.INTENT_INCREASE_TIMER, + { + "name": {"value": "test timer"}, + "minutes": {"value": 5}, + }, + device_id=dev.id, + ) + + mock_client.send_voice_assistant_timer_event.assert_called_with( + VoiceAssistantTimerEventType.VOICE_ASSISTANT_TIMER_UPDATED, + ANY, + "test timer", + total_seconds, + ANY, True, ) diff --git a/tests/components/intent/test_timers.py b/tests/components/intent/test_timers.py index c2efe5d39e2..d194d532513 100644 --- a/tests/components/intent/test_timers.py +++ b/tests/components/intent/test_timers.py @@ -54,6 +54,7 @@ async def test_start_finish_timer(hass: HomeAssistant, init_components) -> None: assert timer.start_minutes is None assert timer.start_seconds == 0 assert timer.seconds_left == 0 + assert timer.created_seconds == 0 if event_type == TimerEventType.STARTED: timer_id = timer.id @@ -218,6 +219,7 @@ async def test_increase_timer(hass: HomeAssistant, init_components) -> None: timer_name = "test timer" timer_id: str | None = None original_total_seconds = -1 + seconds_added = 0 @callback def handle_timer(event_type: TimerEventType, timer: TimerInfo) -> None: @@ -238,12 +240,14 @@ async def test_increase_timer(hass: HomeAssistant, init_components) -> None: + (60 * timer.start_minutes) + timer.start_seconds ) + assert timer.created_seconds == original_total_seconds started_event.set() elif event_type == TimerEventType.UPDATED: assert timer.id == timer_id # Timer was increased assert timer.seconds_left > original_total_seconds + assert timer.created_seconds == original_total_seconds + seconds_added updated_event.set() elif event_type == TimerEventType.CANCELLED: assert timer.id == timer_id @@ -270,6 +274,7 @@ async def test_increase_timer(hass: HomeAssistant, init_components) -> None: await started_event.wait() # Adding 0 seconds has no effect + seconds_added = 0 result = await intent.async_handle( hass, "test", @@ -288,6 +293,7 @@ async def test_increase_timer(hass: HomeAssistant, init_components) -> None: assert not updated_event.is_set() # Add 30 seconds to the timer + seconds_added = (1 * 60 * 60) + (5 * 60) + 30 result = await intent.async_handle( hass, "test", @@ -357,6 +363,7 @@ async def test_decrease_timer(hass: HomeAssistant, init_components) -> None: # Timer was decreased assert timer.seconds_left <= (original_total_seconds - 30) + assert timer.created_seconds == original_total_seconds updated_event.set() elif event_type == TimerEventType.CANCELLED: From e39d26bdc0d99392cf013436e85bb3e91dc1eed0 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 26 Jun 2024 14:21:30 +0200 Subject: [PATCH 1279/1445] Add switch platform to Airgradient (#120559) --- .../components/airgradient/__init__.py | 1 + .../components/airgradient/strings.json | 5 + .../components/airgradient/switch.py | 110 ++++++++++++++++++ .../airgradient/snapshots/test_switch.ambr | 47 ++++++++ tests/components/airgradient/test_number.py | 2 +- tests/components/airgradient/test_switch.py | 101 ++++++++++++++++ 6 files changed, 265 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/airgradient/switch.py create mode 100644 tests/components/airgradient/snapshots/test_switch.ambr create mode 100644 tests/components/airgradient/test_switch.py diff --git a/homeassistant/components/airgradient/__init__.py b/homeassistant/components/airgradient/__init__.py index b1b5a28ef67..fe01d239f3c 100644 --- a/homeassistant/components/airgradient/__init__.py +++ b/homeassistant/components/airgradient/__init__.py @@ -20,6 +20,7 @@ PLATFORMS: list[Platform] = [ Platform.NUMBER, Platform.SELECT, Platform.SENSOR, + Platform.SWITCH, ] diff --git a/homeassistant/components/airgradient/strings.json b/homeassistant/components/airgradient/strings.json index eb529a99ae3..1dd5fc61a16 100644 --- a/homeassistant/components/airgradient/strings.json +++ b/homeassistant/components/airgradient/strings.json @@ -154,6 +154,11 @@ "display_brightness": { "name": "[%key:component::airgradient::entity::number::display_brightness::name%]" } + }, + "switch": { + "post_data_to_airgradient": { + "name": "Post data to Airgradient" + } } } } diff --git a/homeassistant/components/airgradient/switch.py b/homeassistant/components/airgradient/switch.py new file mode 100644 index 00000000000..60c3f83ae5e --- /dev/null +++ b/homeassistant/components/airgradient/switch.py @@ -0,0 +1,110 @@ +"""Support for AirGradient switch entities.""" + +from collections.abc import Awaitable, Callable +from dataclasses import dataclass +from typing import Any + +from airgradient import AirGradientClient, Config +from airgradient.models import ConfigurationControl + +from homeassistant.components.switch import ( + DOMAIN as SWITCH_DOMAIN, + SwitchEntity, + SwitchEntityDescription, +) +from homeassistant.const import EntityCategory +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import AirGradientConfigEntry +from .const import DOMAIN +from .coordinator import AirGradientConfigCoordinator +from .entity import AirGradientEntity + + +@dataclass(frozen=True, kw_only=True) +class AirGradientSwitchEntityDescription(SwitchEntityDescription): + """Describes AirGradient switch entity.""" + + value_fn: Callable[[Config], bool] + set_value_fn: Callable[[AirGradientClient, bool], Awaitable[None]] + + +POST_DATA_TO_AIRGRADIENT = AirGradientSwitchEntityDescription( + key="post_data_to_airgradient", + translation_key="post_data_to_airgradient", + entity_category=EntityCategory.CONFIG, + value_fn=lambda config: config.post_data_to_airgradient, + set_value_fn=lambda client, value: client.enable_sharing_data(enable=value), +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: AirGradientConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up AirGradient switch entities based on a config entry.""" + coordinator = entry.runtime_data.config + + added_entities = False + + @callback + def _async_check_entities() -> None: + nonlocal added_entities + + if ( + coordinator.data.configuration_control is ConfigurationControl.LOCAL + and not added_entities + ): + async_add_entities( + [AirGradientSwitch(coordinator, POST_DATA_TO_AIRGRADIENT)] + ) + added_entities = True + elif ( + coordinator.data.configuration_control is not ConfigurationControl.LOCAL + and added_entities + ): + entity_registry = er.async_get(hass) + unique_id = f"{coordinator.serial_number}-{POST_DATA_TO_AIRGRADIENT.key}" + if entity_id := entity_registry.async_get_entity_id( + SWITCH_DOMAIN, DOMAIN, unique_id + ): + entity_registry.async_remove(entity_id) + added_entities = False + + coordinator.async_add_listener(_async_check_entities) + _async_check_entities() + + +class AirGradientSwitch(AirGradientEntity, SwitchEntity): + """Defines an AirGradient switch entity.""" + + entity_description: AirGradientSwitchEntityDescription + coordinator: AirGradientConfigCoordinator + + def __init__( + self, + coordinator: AirGradientConfigCoordinator, + description: AirGradientSwitchEntityDescription, + ) -> None: + """Initialize AirGradient switch.""" + super().__init__(coordinator) + self.entity_description = description + self._attr_unique_id = f"{coordinator.serial_number}-{description.key}" + + @property + def is_on(self) -> bool: + """Return the state of the switch.""" + return self.entity_description.value_fn(self.coordinator.data) + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the switch on.""" + await self.entity_description.set_value_fn(self.coordinator.client, True) + await self.coordinator.async_request_refresh() + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the switch off.""" + await self.entity_description.set_value_fn(self.coordinator.client, False) + await self.coordinator.async_request_refresh() diff --git a/tests/components/airgradient/snapshots/test_switch.ambr b/tests/components/airgradient/snapshots/test_switch.ambr new file mode 100644 index 00000000000..752355dbe97 --- /dev/null +++ b/tests/components/airgradient/snapshots/test_switch.ambr @@ -0,0 +1,47 @@ +# serializer version: 1 +# name: test_all_entities[switch.airgradient_post_data_to_airgradient-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.airgradient_post_data_to_airgradient', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Post data to Airgradient', + 'platform': 'airgradient', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'post_data_to_airgradient', + 'unique_id': '84fce612f5b8-post_data_to_airgradient', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[switch.airgradient_post_data_to_airgradient-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Airgradient Post data to Airgradient', + }), + 'context': , + 'entity_id': 'switch.airgradient_post_data_to_airgradient', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- diff --git a/tests/components/airgradient/test_number.py b/tests/components/airgradient/test_number.py index ba659829c50..0803c0d437f 100644 --- a/tests/components/airgradient/test_number.py +++ b/tests/components/airgradient/test_number.py @@ -1,4 +1,4 @@ -"""Tests for the AirGradient button platform.""" +"""Tests for the AirGradient number platform.""" from datetime import timedelta from unittest.mock import AsyncMock, patch diff --git a/tests/components/airgradient/test_switch.py b/tests/components/airgradient/test_switch.py new file mode 100644 index 00000000000..20a1cb7470b --- /dev/null +++ b/tests/components/airgradient/test_switch.py @@ -0,0 +1,101 @@ +"""Tests for the AirGradient switch platform.""" + +from datetime import timedelta +from unittest.mock import AsyncMock, patch + +from airgradient import Config +from freezegun.api import FrozenDateTimeFactory +from syrupy import SnapshotAssertion + +from homeassistant.components.airgradient import DOMAIN +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +from homeassistant.const import ( + ATTR_ENTITY_ID, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + Platform, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import setup_integration + +from tests.common import ( + MockConfigEntry, + async_fire_time_changed, + load_fixture, + snapshot_platform, +) + + +async def test_all_entities( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + mock_airgradient_client: AsyncMock, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, +) -> None: + """Test all entities.""" + with patch("homeassistant.components.airgradient.PLATFORMS", [Platform.SWITCH]): + await setup_integration(hass, mock_config_entry) + + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + +async def test_setting_value( + hass: HomeAssistant, + mock_airgradient_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test setting value.""" + await setup_integration(hass, mock_config_entry) + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + target={ATTR_ENTITY_ID: "switch.airgradient_post_data_to_airgradient"}, + blocking=True, + ) + mock_airgradient_client.enable_sharing_data.assert_called_once() + mock_airgradient_client.enable_sharing_data.reset_mock() + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + target={ATTR_ENTITY_ID: "switch.airgradient_post_data_to_airgradient"}, + blocking=True, + ) + mock_airgradient_client.enable_sharing_data.assert_called_once() + + +async def test_cloud_creates_no_switch( + hass: HomeAssistant, + mock_cloud_airgradient_client: AsyncMock, + mock_config_entry: MockConfigEntry, + freezer: FrozenDateTimeFactory, +) -> None: + """Test cloud configuration control.""" + with patch("homeassistant.components.airgradient.PLATFORMS", [Platform.SWITCH]): + await setup_integration(hass, mock_config_entry) + + assert len(hass.states.async_all()) == 0 + + mock_cloud_airgradient_client.get_config.return_value = Config.from_json( + load_fixture("get_config_local.json", DOMAIN) + ) + + freezer.tick(timedelta(minutes=5)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 1 + + mock_cloud_airgradient_client.get_config.return_value = Config.from_json( + load_fixture("get_config_cloud.json", DOMAIN) + ) + + freezer.tick(timedelta(minutes=5)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 0 From fd67fe417e19c2440355d4c65ad6bee894657d7c Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 26 Jun 2024 14:22:52 +0200 Subject: [PATCH 1280/1445] Use ruff to force alias when importing PLATFORM_SCHEMA (#120539) --- homeassistant/components/config/scene.py | 7 +++- .../dlib_face_detect/image_processing.py | 9 ++-- pyproject.toml | 41 +++++++++++++++++++ tests/components/tts/common.py | 4 +- 4 files changed, 54 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/config/scene.py b/homeassistant/components/config/scene.py index a2e2693036a..8192c0051b0 100644 --- a/homeassistant/components/config/scene.py +++ b/homeassistant/components/config/scene.py @@ -5,7 +5,10 @@ from __future__ import annotations from typing import Any import uuid -from homeassistant.components.scene import DOMAIN, PLATFORM_SCHEMA +from homeassistant.components.scene import ( + DOMAIN, + PLATFORM_SCHEMA as SCENE_PLATFORM_SCHEMA, +) from homeassistant.config import SCENE_CONFIG_PATH from homeassistant.const import CONF_ID, SERVICE_RELOAD from homeassistant.core import DOMAIN as HA_DOMAIN, HomeAssistant, callback @@ -14,6 +17,8 @@ from homeassistant.helpers import config_validation as cv, entity_registry as er from .const import ACTION_DELETE from .view import EditIdBasedConfigView +PLATFORM_SCHEMA = SCENE_PLATFORM_SCHEMA + @callback def async_setup(hass: HomeAssistant) -> bool: diff --git a/homeassistant/components/dlib_face_detect/image_processing.py b/homeassistant/components/dlib_face_detect/image_processing.py index 9f6b30dee61..80becdf9992 100644 --- a/homeassistant/components/dlib_face_detect/image_processing.py +++ b/homeassistant/components/dlib_face_detect/image_processing.py @@ -6,15 +6,16 @@ import io import face_recognition -from homeassistant.components.image_processing import ImageProcessingFaceEntity +from homeassistant.components.image_processing import ( + PLATFORM_SCHEMA as IMAGE_PROCESSING_PLATFORM_SCHEMA, + ImageProcessingFaceEntity, +) from homeassistant.const import ATTR_LOCATION, CONF_ENTITY_ID, CONF_NAME, CONF_SOURCE from homeassistant.core import HomeAssistant, split_entity_id from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from homeassistant.components.image_processing import ( # noqa: F401, isort:skip - PLATFORM_SCHEMA, -) +PLATFORM_SCHEMA = IMAGE_PROCESSING_PLATFORM_SCHEMA def setup_platform( diff --git a/pyproject.toml b/pyproject.toml index db6c5f0c989..4edb1535411 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -811,7 +811,48 @@ ignore = [ [tool.ruff.lint.flake8-import-conventions.extend-aliases] voluptuous = "vol" +"homeassistant.components.air_quality.PLATFORM_SCHEMA" = "AIR_QUALITY_PLATFORM_SCHEMA" +"homeassistant.components.alarm_control_panel.PLATFORM_SCHEMA" = "ALARM_CONTROL_PANEL_PLATFORM_SCHEMA" +"homeassistant.components.binary_sensor.PLATFORM_SCHEMA" = "BINARY_SENSOR_PLATFORM_SCHEMA" +"homeassistant.components.button.PLATFORM_SCHEMA" = "BUTTON_PLATFORM_SCHEMA" +"homeassistant.components.calendar.PLATFORM_SCHEMA" = "CALENDAR_PLATFORM_SCHEMA" +"homeassistant.components.camera.PLATFORM_SCHEMA" = "CAMERA_PLATFORM_SCHEMA" +"homeassistant.components.climate.PLATFORM_SCHEMA" = "CLIMATE_PLATFORM_SCHEMA" +"homeassistant.components.conversation.PLATFORM_SCHEMA" = "CONVERSATION_PLATFORM_SCHEMA" +"homeassistant.components.cover.PLATFORM_SCHEMA" = "COVER_PLATFORM_SCHEMA" +"homeassistant.components.date.PLATFORM_SCHEMA" = "DATE_PLATFORM_SCHEMA" +"homeassistant.components.datetime.PLATFORM_SCHEMA" = "DATETIME_PLATFORM_SCHEMA" +"homeassistant.components.device_tracker.PLATFORM_SCHEMA" = "DEVICE_TRACKER_PLATFORM_SCHEMA" +"homeassistant.components.event.PLATFORM_SCHEMA" = "EVENT_PLATFORM_SCHEMA" +"homeassistant.components.fan.PLATFORM_SCHEMA" = "FAN_PLATFORM_SCHEMA" +"homeassistant.components.geo_location.PLATFORM_SCHEMA" = "GEO_LOCATION_PLATFORM_SCHEMA" +"homeassistant.components.humidifier.PLATFORM_SCHEMA" = "HUMIDIFIER_PLATFORM_SCHEMA" +"homeassistant.components.image.PLATFORM_SCHEMA" = "IMAGE_PLATFORM_SCHEMA" +"homeassistant.components.image_processing.PLATFORM_SCHEMA" = "IMAGE_PROCESSING_PLATFORM_SCHEMA" +"homeassistant.components.lawn_mower.PLATFORM_SCHEMA" = "LAWN_MOWER_PLATFORM_SCHEMA" +"homeassistant.components.light.PLATFORM_SCHEMA" = "LIGHT_PLATFORM_SCHEMA" +"homeassistant.components.lock.PLATFORM_SCHEMA" = "LOCK_PLATFORM_SCHEMA" +"homeassistant.components.mailbox.PLATFORM_SCHEMA" = "MAILBOX_PLATFORM_SCHEMA" +"homeassistant.components.media_player.PLATFORM_SCHEMA" = "MEDIA_PLAYER_PLATFORM_SCHEMA" "homeassistant.components.notify.PLATFORM_SCHEMA" = "NOTIFY_PLATFORM_SCHEMA" +"homeassistant.components.number.PLATFORM_SCHEMA" = "NUMBER_PLATFORM_SCHEMA" +"homeassistant.components.remote.PLATFORM_SCHEMA" = "REMOTE_PLATFORM_SCHEMA" +"homeassistant.components.scene.PLATFORM_SCHEMA" = "SCENE_PLATFORM_SCHEMA" +"homeassistant.components.select.PLATFORM_SCHEMA" = "SELECT_PLATFORM_SCHEMA" +"homeassistant.components.sensor.PLATFORM_SCHEMA" = "SENSOR_PLATFORM_SCHEMA" +"homeassistant.components.siren.PLATFORM_SCHEMA" = "SIREN_PLATFORM_SCHEMA" +"homeassistant.components.stt.PLATFORM_SCHEMA" = "STT_PLATFORM_SCHEMA" +"homeassistant.components.switch.PLATFORM_SCHEMA" = "SWITCH_PLATFORM_SCHEMA" +"homeassistant.components.text.PLATFORM_SCHEMA" = "TEXT_PLATFORM_SCHEMA" +"homeassistant.components.time.PLATFORM_SCHEMA" = "TIME_PLATFORM_SCHEMA" +"homeassistant.components.todo.PLATFORM_SCHEMA" = "TODO_PLATFORM_SCHEMA" +"homeassistant.components.tts.PLATFORM_SCHEMA" = "TTS_PLATFORM_SCHEMA" +"homeassistant.components.vacuum.PLATFORM_SCHEMA" = "VACUUM_PLATFORM_SCHEMA" +"homeassistant.components.valve.PLATFORM_SCHEMA" = "VALVE_PLATFORM_SCHEMA" +"homeassistant.components.update.PLATFORM_SCHEMA" = "UPDATE_PLATFORM_SCHEMA" +"homeassistant.components.wake_word.PLATFORM_SCHEMA" = "WAKE_WORD_PLATFORM_SCHEMA" +"homeassistant.components.water_heater.PLATFORM_SCHEMA" = "WATER_HEATER_PLATFORM_SCHEMA" +"homeassistant.components.weather.PLATFORM_SCHEMA" = "WEATHER_PLATFORM_SCHEMA" "homeassistant.helpers.area_registry" = "ar" "homeassistant.helpers.category_registry" = "cr" "homeassistant.helpers.config_validation" = "cv" diff --git a/tests/components/tts/common.py b/tests/components/tts/common.py index e1d9d973f25..b99e6400273 100644 --- a/tests/components/tts/common.py +++ b/tests/components/tts/common.py @@ -15,7 +15,7 @@ from homeassistant.components import media_source from homeassistant.components.tts import ( CONF_LANG, DOMAIN as TTS_DOMAIN, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as TTS_PLATFORM_SCHEMA, Provider, TextToSpeechEntity, TtsAudioType, @@ -184,7 +184,7 @@ class MockTTSEntity(BaseProvider, TextToSpeechEntity): class MockTTS(MockPlatform): """A mock TTS platform.""" - PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + PLATFORM_SCHEMA = TTS_PLATFORM_SCHEMA.extend( {vol.Optional(CONF_LANG, default=DEFAULT_LANG): vol.In(SUPPORT_LANGUAGES)} ) From ec16fc235bc1049abf9fb3fe4549a1ccc0f7bf41 Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Wed, 26 Jun 2024 22:23:06 +1000 Subject: [PATCH 1281/1445] Add new coordinators to Tessie (#118452) * WIP * wip * Add energy classes * Add basis for Testing * Bump Library * fix case * bump library * bump library again * bump library for teslemetry * reorder * Fix super * Update strings.json * Tests * Small tweaks * Bump * Bump teslemetry * Remove version * Add WC states * Bump to match dev * Review feedback Co-authored-by: Joost Lekkerkerker * Review feedback * Review feedback 1 * Review feedback 2 * TessieWallConnectorStates Enum * fixes * Fix translations and value * Update homeassistant/components/tessie/strings.json * Update homeassistant/components/tessie/strings.json --------- Co-authored-by: Joost Lekkerkerker --- homeassistant/components/tessie/__init__.py | 54 +- homeassistant/components/tessie/const.py | 15 + .../components/tessie/coordinator.py | 91 +- homeassistant/components/tessie/entity.py | 130 ++- homeassistant/components/tessie/icons.json | 36 + homeassistant/components/tessie/manifest.json | 2 +- homeassistant/components/tessie/models.py | 20 +- homeassistant/components/tessie/sensor.py | 216 +++- homeassistant/components/tessie/strings.json | 54 + requirements_all.txt | 1 + requirements_test_all.txt | 1 + tests/components/tessie/common.py | 8 + tests/components/tessie/conftest.py | 47 + .../tessie/fixtures/live_status.json | 33 + .../components/tessie/fixtures/products.json | 121 +++ .../components/tessie/fixtures/site_info.json | 125 +++ .../tessie/snapshots/test_sensor.ambr | 944 +++++++++++++++++- tests/components/tessie/test_coordinator.py | 96 +- tests/components/tessie/test_init.py | 14 + 19 files changed, 1910 insertions(+), 98 deletions(-) create mode 100644 tests/components/tessie/fixtures/live_status.json create mode 100644 tests/components/tessie/fixtures/products.json create mode 100644 tests/components/tessie/fixtures/site_info.json diff --git a/homeassistant/components/tessie/__init__.py b/homeassistant/components/tessie/__init__.py index 37fb669e54b..e8891d6665f 100644 --- a/homeassistant/components/tessie/__init__.py +++ b/homeassistant/components/tessie/__init__.py @@ -1,9 +1,12 @@ """Tessie integration.""" +import asyncio from http import HTTPStatus import logging from aiohttp import ClientError, ClientResponseError +from tesla_fleet_api import EnergySpecific, Tessie +from tesla_fleet_api.exceptions import TeslaFleetError from tessie_api import get_state_of_all_vehicles from homeassistant.config_entries import ConfigEntry @@ -14,8 +17,12 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.device_registry import DeviceInfo from .const import DOMAIN, MODELS -from .coordinator import TessieStateUpdateCoordinator -from .models import TessieData, TessieVehicleData +from .coordinator import ( + TessieEnergySiteInfoCoordinator, + TessieEnergySiteLiveCoordinator, + TessieStateUpdateCoordinator, +) +from .models import TessieData, TessieEnergyData, TessieVehicleData PLATFORMS = [ Platform.BINARY_SENSOR, @@ -40,10 +47,11 @@ type TessieConfigEntry = ConfigEntry[TessieData] async def async_setup_entry(hass: HomeAssistant, entry: TessieConfigEntry) -> bool: """Set up Tessie config.""" api_key = entry.data[CONF_ACCESS_TOKEN] + session = async_get_clientsession(hass) try: state_of_all_vehicles = await get_state_of_all_vehicles( - session=async_get_clientsession(hass), + session=session, api_key=api_key, only_active=True, ) @@ -84,7 +92,45 @@ async def async_setup_entry(hass: HomeAssistant, entry: TessieConfigEntry) -> bo if vehicle["last_state"] is not None ] - entry.runtime_data = TessieData(vehicles=vehicles) + # Energy Sites + tessie = Tessie(session, api_key) + try: + products = (await tessie.products())["response"] + except TeslaFleetError as e: + raise ConfigEntryNotReady from e + + energysites: list[TessieEnergyData] = [] + for product in products: + if "energy_site_id" in product: + site_id = product["energy_site_id"] + api = EnergySpecific(tessie.energy, site_id) + energysites.append( + TessieEnergyData( + api=api, + id=site_id, + live_coordinator=TessieEnergySiteLiveCoordinator(hass, api), + info_coordinator=TessieEnergySiteInfoCoordinator(hass, api), + device=DeviceInfo( + identifiers={(DOMAIN, str(site_id))}, + manufacturer="Tesla", + name=product.get("site_name", "Energy Site"), + ), + ) + ) + + # Populate coordinator data before forwarding to platforms + await asyncio.gather( + *( + energysite.live_coordinator.async_config_entry_first_refresh() + for energysite in energysites + ), + *( + energysite.info_coordinator.async_config_entry_first_refresh() + for energysite in energysites + ), + ) + + entry.runtime_data = TessieData(vehicles, energysites) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/tessie/const.py b/homeassistant/components/tessie/const.py index f717d758f5a..bdb20193613 100644 --- a/homeassistant/components/tessie/const.py +++ b/homeassistant/components/tessie/const.py @@ -79,3 +79,18 @@ TessieChargeStates = { "Disconnected": "disconnected", "NoPower": "no_power", } + + +class TessieWallConnectorStates(IntEnum): + """Tessie Wall Connector states.""" + + BOOTING = 0 + CHARGING = 1 + DISCONNECTED = 2 + CONNECTED = 4 + SCHEDULED = 5 + NEGOTIATING = 6 + ERROR = 7 + CHARGING_FINISHED = 8 + WAITING_CAR = 9 + CHARGING_REDUCED = 10 diff --git a/homeassistant/components/tessie/coordinator.py b/homeassistant/components/tessie/coordinator.py index bea1bf72a8d..4582260bfb2 100644 --- a/homeassistant/components/tessie/coordinator.py +++ b/homeassistant/components/tessie/coordinator.py @@ -6,21 +6,37 @@ import logging from typing import Any from aiohttp import ClientResponseError +from tesla_fleet_api import EnergySpecific +from tesla_fleet_api.exceptions import InvalidToken, MissingToken, TeslaFleetError from tessie_api import get_state, get_status from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import TessieStatus # This matches the update interval Tessie performs server side TESSIE_SYNC_INTERVAL = 10 +TESSIE_FLEET_API_SYNC_INTERVAL = timedelta(seconds=30) _LOGGER = logging.getLogger(__name__) +def flatten(data: dict[str, Any], parent: str | None = None) -> dict[str, Any]: + """Flatten the data structure.""" + result = {} + for key, value in data.items(): + if parent: + key = f"{parent}_{key}" + if isinstance(value, dict): + result.update(flatten(value, key)) + else: + result[key] = value + return result + + class TessieStateUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): """Class to manage fetching data from the Tessie API.""" @@ -41,7 +57,7 @@ class TessieStateUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): self.api_key = api_key self.vin = vin self.session = async_get_clientsession(hass) - self.data = self._flatten(data) + self.data = flatten(data) async def _async_update_data(self) -> dict[str, Any]: """Update vehicle data using Tessie API.""" @@ -68,18 +84,61 @@ class TessieStateUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): raise ConfigEntryAuthFailed from e raise - return self._flatten(vehicle) + return flatten(vehicle) - def _flatten( - self, data: dict[str, Any], parent: str | None = None - ) -> dict[str, Any]: - """Flatten the data structure.""" - result = {} - for key, value in data.items(): - if parent: - key = f"{parent}_{key}" - if isinstance(value, dict): - result.update(self._flatten(value, key)) - else: - result[key] = value - return result + +class TessieEnergySiteLiveCoordinator(DataUpdateCoordinator[dict[str, Any]]): + """Class to manage fetching energy site live status from the Tessie API.""" + + def __init__(self, hass: HomeAssistant, api: EnergySpecific) -> None: + """Initialize Tessie Energy Site Live coordinator.""" + super().__init__( + hass, + _LOGGER, + name="Tessie Energy Site Live", + update_interval=TESSIE_FLEET_API_SYNC_INTERVAL, + ) + self.api = api + + async def _async_update_data(self) -> dict[str, Any]: + """Update energy site data using Tessie API.""" + + try: + data = (await self.api.live_status())["response"] + except (InvalidToken, MissingToken) as e: + raise ConfigEntryAuthFailed from e + except TeslaFleetError as e: + raise UpdateFailed(e.message) from e + + # Convert Wall Connectors from array to dict + data["wall_connectors"] = { + wc["din"]: wc for wc in (data.get("wall_connectors") or []) + } + + return data + + +class TessieEnergySiteInfoCoordinator(DataUpdateCoordinator[dict[str, Any]]): + """Class to manage fetching energy site info from the Tessie API.""" + + def __init__(self, hass: HomeAssistant, api: EnergySpecific) -> None: + """Initialize Tessie Energy Info coordinator.""" + super().__init__( + hass, + _LOGGER, + name="Tessie Energy Site Info", + update_interval=TESSIE_FLEET_API_SYNC_INTERVAL, + ) + self.api = api + + async def _async_update_data(self) -> dict[str, Any]: + """Update energy site data using Tessie API.""" + + try: + data = (await self.api.site_info())["response"] + except (InvalidToken, MissingToken) as e: + raise ConfigEntryAuthFailed from e + except TeslaFleetError as e: + raise UpdateFailed(e.message) from e + + return flatten(data) diff --git a/homeassistant/components/tessie/entity.py b/homeassistant/components/tessie/entity.py index 1b7ddcbe84c..93b9f10ae67 100644 --- a/homeassistant/components/tessie/entity.py +++ b/homeassistant/components/tessie/entity.py @@ -1,36 +1,47 @@ """Tessie parent entity class.""" +from abc import abstractmethod from collections.abc import Awaitable, Callable from typing import Any from aiohttp import ClientResponseError from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN -from .coordinator import TessieStateUpdateCoordinator -from .models import TessieVehicleData +from .coordinator import ( + TessieEnergySiteInfoCoordinator, + TessieEnergySiteLiveCoordinator, + TessieStateUpdateCoordinator, +) +from .models import TessieEnergyData, TessieVehicleData -class TessieEntity(CoordinatorEntity[TessieStateUpdateCoordinator]): - """Parent class for Tessie Entities.""" +class TessieBaseEntity( + CoordinatorEntity[ + TessieStateUpdateCoordinator + | TessieEnergySiteInfoCoordinator + | TessieEnergySiteLiveCoordinator + ] +): + """Parent class for Tessie entities.""" _attr_has_entity_name = True def __init__( self, - vehicle: TessieVehicleData, + coordinator: TessieStateUpdateCoordinator + | TessieEnergySiteInfoCoordinator + | TessieEnergySiteLiveCoordinator, key: str, ) -> None: """Initialize common aspects of a Tessie entity.""" - super().__init__(vehicle.data_coordinator) - self.vin = vehicle.vin - self.key = key + self.key = key self._attr_translation_key = key - self._attr_unique_id = f"{vehicle.vin}-{key}" - self._attr_device_info = vehicle.device + super().__init__(coordinator) @property def _value(self) -> Any: @@ -41,15 +52,53 @@ class TessieEntity(CoordinatorEntity[TessieStateUpdateCoordinator]): """Return a specific value from coordinator data.""" return self.coordinator.data.get(key or self.key, default) + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + self._async_update_attrs() + super()._handle_coordinator_update() + + @abstractmethod + def _async_update_attrs(self) -> None: + """Update the attributes of the entity.""" + + +class TessieEntity(TessieBaseEntity): + """Parent class for Tessie vehicle entities.""" + + def __init__( + self, + vehicle: TessieVehicleData, + key: str, + ) -> None: + """Initialize common aspects of a Tessie vehicle entity.""" + self.vin = vehicle.vin + self._session = vehicle.data_coordinator.session + self._api_key = vehicle.data_coordinator.api_key + self._attr_unique_id = f"{vehicle.vin}-{key}" + self._attr_device_info = vehicle.device + + super().__init__(vehicle.data_coordinator, key) + + @property + def _value(self) -> Any: + """Return value from coordinator data.""" + return self.coordinator.data.get(self.key) + + def set(self, *args: Any) -> None: + """Set a value in coordinator data.""" + for key, value in args: + self.coordinator.data[key] = value + self.async_write_ha_state() + async def run( self, func: Callable[..., Awaitable[dict[str, Any]]], **kargs: Any ) -> None: """Run a tessie_api function and handle exceptions.""" try: response = await func( - session=self.coordinator.session, + session=self._session, vin=self.vin, - api_key=self.coordinator.api_key, + api_key=self._api_key, **kargs, ) except ClientResponseError as e: @@ -63,8 +112,55 @@ class TessieEntity(CoordinatorEntity[TessieStateUpdateCoordinator]): translation_placeholders={"name": name}, ) - def set(self, *args: Any) -> None: - """Set a value in coordinator data.""" - for key, value in args: - self.coordinator.data[key] = value - self.async_write_ha_state() + def _async_update_attrs(self) -> None: + """Update the attributes of the entity.""" + # Not used in this class yet + + +class TessieEnergyEntity(TessieBaseEntity): + """Parent class for Tessie energy site entities.""" + + def __init__( + self, + data: TessieEnergyData, + coordinator: TessieEnergySiteInfoCoordinator | TessieEnergySiteLiveCoordinator, + key: str, + ) -> None: + """Initialize common aspects of a Tessie energy site entity.""" + + self._attr_unique_id = f"{data.id}-{key}" + self._attr_device_info = data.device + + super().__init__(coordinator, key) + + +class TessieWallConnectorEntity(TessieBaseEntity): + """Parent class for Tessie wall connector entities.""" + + def __init__( + self, + data: TessieEnergyData, + din: str, + key: str, + ) -> None: + """Initialize common aspects of a Teslemetry entity.""" + self.din = din + self._attr_unique_id = f"{data.id}-{din}-{key}" + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, din)}, + manufacturer="Tesla", + name="Wall Connector", + via_device=(DOMAIN, str(data.id)), + serial_number=din.split("-")[-1], + ) + + super().__init__(data.live_coordinator, key) + + @property + def _value(self) -> int: + """Return a specific wall connector value from coordinator data.""" + return ( + self.coordinator.data.get("wall_connectors", {}) + .get(self.din, {}) + .get(self.key) + ) diff --git a/homeassistant/components/tessie/icons.json b/homeassistant/components/tessie/icons.json index 0b1051e662f..2543b3ab9e1 100644 --- a/homeassistant/components/tessie/icons.json +++ b/homeassistant/components/tessie/icons.json @@ -189,6 +189,42 @@ }, "drive_state_active_route_destination": { "default": "mdi:map-marker" + }, + "battery_power": { + "default": "mdi:home-battery" + }, + "energy_left": { + "default": "mdi:battery" + }, + "generator_power": { + "default": "mdi:generator-stationary" + }, + "grid_power": { + "default": "mdi:transmission-tower" + }, + "grid_services_power": { + "default": "mdi:transmission-tower" + }, + "load_power": { + "default": "mdi:power-plug" + }, + "solar_power": { + "default": "mdi:solar-power" + }, + "total_pack_energy": { + "default": "mdi:battery-high" + }, + "vin": { + "default": "mdi:car-electric" + }, + "wall_connector_fault_state": { + "default": "mdi:ev-station" + }, + "wall_connector_power": { + "default": "mdi:ev-station" + }, + "wall_connector_state": { + "default": "mdi:ev-station" } }, "switch": { diff --git a/homeassistant/components/tessie/manifest.json b/homeassistant/components/tessie/manifest.json index 52fc8dd5be1..bf1ab5f61e4 100644 --- a/homeassistant/components/tessie/manifest.json +++ b/homeassistant/components/tessie/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/tessie", "iot_class": "cloud_polling", "loggers": ["tessie"], - "requirements": ["tessie-api==0.0.9"] + "requirements": ["tessie-api==0.0.9", "tesla-fleet-api==0.6.1"] } diff --git a/homeassistant/components/tessie/models.py b/homeassistant/components/tessie/models.py index e96562ff8e1..ca670b9650b 100644 --- a/homeassistant/components/tessie/models.py +++ b/homeassistant/components/tessie/models.py @@ -4,9 +4,15 @@ from __future__ import annotations from dataclasses import dataclass +from tesla_fleet_api import EnergySpecific + from homeassistant.helpers.device_registry import DeviceInfo -from .coordinator import TessieStateUpdateCoordinator +from .coordinator import ( + TessieEnergySiteInfoCoordinator, + TessieEnergySiteLiveCoordinator, + TessieStateUpdateCoordinator, +) @dataclass @@ -14,6 +20,18 @@ class TessieData: """Data for the Tessie integration.""" vehicles: list[TessieVehicleData] + energysites: list[TessieEnergyData] + + +@dataclass +class TessieEnergyData: + """Data for a Energy Site in the Tessie integration.""" + + api: EnergySpecific + live_coordinator: TessieEnergySiteLiveCoordinator + info_coordinator: TessieEnergySiteInfoCoordinator + id: int + device: DeviceInfo @dataclass diff --git a/homeassistant/components/tessie/sensor.py b/homeassistant/components/tessie/sensor.py index dc910c7a03a..586162fe779 100644 --- a/homeassistant/components/tessie/sensor.py +++ b/homeassistant/components/tessie/sensor.py @@ -5,6 +5,7 @@ from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass from datetime import datetime, timedelta +from itertools import chain from typing import cast from homeassistant.components.sensor import ( @@ -33,9 +34,9 @@ from homeassistant.util import dt as dt_util from homeassistant.util.variance import ignore_variance from . import TessieConfigEntry -from .const import TessieChargeStates -from .entity import TessieEntity -from .models import TessieVehicleData +from .const import TessieChargeStates, TessieWallConnectorStates +from .entity import TessieEnergyEntity, TessieEntity, TessieWallConnectorEntity +from .models import TessieEnergyData, TessieVehicleData @callback @@ -257,6 +258,115 @@ DESCRIPTIONS: tuple[TessieSensorEntityDescription, ...] = ( ), ) +ENERGY_LIVE_DESCRIPTIONS: tuple[TessieSensorEntityDescription, ...] = ( + TessieSensorEntityDescription( + key="solar_power", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfPower.WATT, + suggested_unit_of_measurement=UnitOfPower.KILO_WATT, + suggested_display_precision=2, + device_class=SensorDeviceClass.POWER, + ), + TessieSensorEntityDescription( + key="energy_left", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, + suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + suggested_display_precision=2, + device_class=SensorDeviceClass.ENERGY_STORAGE, + entity_category=EntityCategory.DIAGNOSTIC, + ), + TessieSensorEntityDescription( + key="total_pack_energy", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, + suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + suggested_display_precision=2, + device_class=SensorDeviceClass.ENERGY_STORAGE, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + TessieSensorEntityDescription( + key="percentage_charged", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=PERCENTAGE, + device_class=SensorDeviceClass.BATTERY, + suggested_display_precision=2, + ), + TessieSensorEntityDescription( + key="battery_power", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfPower.WATT, + suggested_unit_of_measurement=UnitOfPower.KILO_WATT, + suggested_display_precision=2, + device_class=SensorDeviceClass.POWER, + ), + TessieSensorEntityDescription( + key="load_power", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfPower.WATT, + suggested_unit_of_measurement=UnitOfPower.KILO_WATT, + suggested_display_precision=2, + device_class=SensorDeviceClass.POWER, + ), + TessieSensorEntityDescription( + key="grid_power", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfPower.WATT, + suggested_unit_of_measurement=UnitOfPower.KILO_WATT, + suggested_display_precision=2, + device_class=SensorDeviceClass.POWER, + ), + TessieSensorEntityDescription( + key="grid_services_power", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfPower.WATT, + suggested_unit_of_measurement=UnitOfPower.KILO_WATT, + suggested_display_precision=2, + device_class=SensorDeviceClass.POWER, + ), + TessieSensorEntityDescription( + key="generator_power", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfPower.WATT, + suggested_unit_of_measurement=UnitOfPower.KILO_WATT, + suggested_display_precision=2, + device_class=SensorDeviceClass.POWER, + entity_registry_enabled_default=False, + ), +) + +WALL_CONNECTOR_DESCRIPTIONS: tuple[TessieSensorEntityDescription, ...] = ( + TessieSensorEntityDescription( + key="wall_connector_state", + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + device_class=SensorDeviceClass.ENUM, + value_fn=lambda x: TessieWallConnectorStates(cast(int, x)).name.lower(), + options=[state.name.lower() for state in TessieWallConnectorStates], + ), + TessieSensorEntityDescription( + key="wall_connector_power", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfPower.WATT, + suggested_unit_of_measurement=UnitOfPower.KILO_WATT, + suggested_display_precision=2, + device_class=SensorDeviceClass.POWER, + ), + TessieSensorEntityDescription( + key="vin", + ), +) + +ENERGY_INFO_DESCRIPTIONS: tuple[TessieSensorEntityDescription, ...] = ( + TessieSensorEntityDescription( + key="vpp_backup_reserve_percent", + entity_category=EntityCategory.DIAGNOSTIC, + device_class=SensorDeviceClass.BATTERY, + native_unit_of_measurement=PERCENTAGE, + ), +) + async def async_setup_entry( hass: HomeAssistant, @@ -264,17 +374,38 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Tessie sensor platform from a config entry.""" - data = entry.runtime_data async_add_entities( - TessieSensorEntity(vehicle, description) - for vehicle in data.vehicles - for description in DESCRIPTIONS + chain( + ( # Add vehicles + TessieVehicleSensorEntity(vehicle, description) + for vehicle in entry.runtime_data.vehicles + for description in DESCRIPTIONS + ), + ( # Add energy site info + TessieEnergyInfoSensorEntity(energysite, description) + for energysite in entry.runtime_data.energysites + for description in ENERGY_INFO_DESCRIPTIONS + if description.key in energysite.info_coordinator.data + ), + ( # Add energy site live + TessieEnergyLiveSensorEntity(energysite, description) + for energysite in entry.runtime_data.energysites + for description in ENERGY_LIVE_DESCRIPTIONS + if description.key in energysite.live_coordinator.data + ), + ( # Add wall connectors + TessieWallConnectorSensorEntity(energysite, din, description) + for energysite in entry.runtime_data.energysites + for din in energysite.live_coordinator.data.get("wall_connectors", {}) + for description in WALL_CONNECTOR_DESCRIPTIONS + ), + ) ) -class TessieSensorEntity(TessieEntity, SensorEntity): - """Base class for Tessie metric sensors.""" +class TessieVehicleSensorEntity(TessieEntity, SensorEntity): + """Base class for Tessie sensor entities.""" entity_description: TessieSensorEntityDescription @@ -284,8 +415,8 @@ class TessieSensorEntity(TessieEntity, SensorEntity): description: TessieSensorEntityDescription, ) -> None: """Initialize the sensor.""" - super().__init__(vehicle, description.key) self.entity_description = description + super().__init__(vehicle, description.key) @property def native_value(self) -> StateType | datetime: @@ -296,3 +427,68 @@ class TessieSensorEntity(TessieEntity, SensorEntity): def available(self) -> bool: """Return if sensor is available.""" return super().available and self.entity_description.available_fn(self.get()) + + +class TessieEnergyLiveSensorEntity(TessieEnergyEntity, SensorEntity): + """Base class for Tessie energy site sensor entity.""" + + entity_description: TessieSensorEntityDescription + + def __init__( + self, + data: TessieEnergyData, + description: TessieSensorEntityDescription, + ) -> None: + """Initialize the sensor.""" + self.entity_description = description + super().__init__(data, data.live_coordinator, description.key) + + def _async_update_attrs(self) -> None: + """Update the attributes of the sensor.""" + self._attr_available = self._value is not None + self._attr_native_value = self.entity_description.value_fn(self._value) + + +class TessieEnergyInfoSensorEntity(TessieEnergyEntity, SensorEntity): + """Base class for Tessie energy site sensor entity.""" + + entity_description: TessieSensorEntityDescription + + def __init__( + self, + data: TessieEnergyData, + description: TessieSensorEntityDescription, + ) -> None: + """Initialize the sensor.""" + self.entity_description = description + super().__init__(data, data.info_coordinator, description.key) + + def _async_update_attrs(self) -> None: + """Update the attributes of the sensor.""" + self._attr_available = self._value is not None + self._attr_native_value = self._value + + +class TessieWallConnectorSensorEntity(TessieWallConnectorEntity, SensorEntity): + """Base class for Tessie wall connector sensor entity.""" + + entity_description: TessieSensorEntityDescription + + def __init__( + self, + data: TessieEnergyData, + din: str, + description: TessieSensorEntityDescription, + ) -> None: + """Initialize the sensor.""" + self.entity_description = description + super().__init__( + data, + din, + description.key, + ) + + def _async_update_attrs(self) -> None: + """Update the attributes of the sensor.""" + self._attr_available = self._value is not None + self._attr_native_value = self.entity_description.value_fn(self._value) diff --git a/homeassistant/components/tessie/strings.json b/homeassistant/components/tessie/strings.json index ea75660ddb7..8e617f137dc 100644 --- a/homeassistant/components/tessie/strings.json +++ b/homeassistant/components/tessie/strings.json @@ -167,6 +167,60 @@ }, "drive_state_active_route_destination": { "name": "Destination" + }, + "battery_power": { + "name": "Battery power" + }, + "energy_left": { + "name": "Energy left" + }, + "generator_power": { + "name": "Generator power" + }, + "grid_power": { + "name": "Grid power" + }, + "grid_services_power": { + "name": "Grid services power" + }, + "load_power": { + "name": "Load power" + }, + "percentage_charged": { + "name": "Percentage charged" + }, + "solar_power": { + "name": "Solar power" + }, + "total_pack_energy": { + "name": "Total pack energy" + }, + "vin": { + "name": "Vehicle" + }, + "vpp_backup_reserve_percent": { + "name": "VPP backup reserve" + }, + "wall_connector_fault_state": { + "name": "Fault state code" + }, + "wall_connector_power": { + "name": "Power" + }, + "wall_connector_state": { + "name": "State", + "state": { + "booting": "Booting", + "charging": "[%key:component::tessie::entity::sensor::charge_state_charging_state::state::charging%]", + "disconnected": "[%key:common::state::disconnected%]", + "connected": "[%key:common::state::connected%]", + "scheduled": "Scheduled", + "negotiating": "Negotiating", + "error": "Error", + "charging_finished": "Charging finished", + "waiting_car": "Waiting car", + "charging_reduced": "Charging reduced" + } } }, "cover": { diff --git a/requirements_all.txt b/requirements_all.txt index 2ec7f38e5e3..f4de7dafa87 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2713,6 +2713,7 @@ temperusb==1.6.1 # tensorflow==2.5.0 # homeassistant.components.teslemetry +# homeassistant.components.tessie tesla-fleet-api==0.6.1 # homeassistant.components.powerwall diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f0e856c2f6f..12ab15a7d76 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2111,6 +2111,7 @@ temescal==0.5 temperusb==1.6.1 # homeassistant.components.teslemetry +# homeassistant.components.tessie tesla-fleet-api==0.6.1 # homeassistant.components.powerwall diff --git a/tests/components/tessie/common.py b/tests/components/tessie/common.py index c19f6f65201..3d24c6b233a 100644 --- a/tests/components/tessie/common.py +++ b/tests/components/tessie/common.py @@ -16,6 +16,7 @@ from homeassistant.helpers.typing import UNDEFINED, UndefinedType from tests.common import MockConfigEntry, load_json_object_fixture +# Tessie library TEST_STATE_OF_ALL_VEHICLES = load_json_object_fixture("vehicles.json", DOMAIN) TEST_VEHICLE_STATE_ONLINE = load_json_object_fixture("online.json", DOMAIN) TEST_VEHICLE_STATUS_AWAKE = {"status": TessieStatus.AWAKE} @@ -47,6 +48,13 @@ ERROR_VIRTUAL_KEY = ClientResponseError( ) ERROR_CONNECTION = ClientConnectionError() +# Fleet API library +PRODUCTS = load_json_object_fixture("products.json", DOMAIN) +LIVE_STATUS = load_json_object_fixture("live_status.json", DOMAIN) +SITE_INFO = load_json_object_fixture("site_info.json", DOMAIN) +RESPONSE_OK = {"response": {}, "error": None} +COMMAND_OK = {"response": {"result": True, "reason": ""}} + async def setup_platform( hass: HomeAssistant, platforms: list[Platform] | UndefinedType = UNDEFINED diff --git a/tests/components/tessie/conftest.py b/tests/components/tessie/conftest.py index 77d1e3fd3e2..79cc9aa44c6 100644 --- a/tests/components/tessie/conftest.py +++ b/tests/components/tessie/conftest.py @@ -2,16 +2,23 @@ from __future__ import annotations +from copy import deepcopy from unittest.mock import patch import pytest from .common import ( + COMMAND_OK, + LIVE_STATUS, + PRODUCTS, + SITE_INFO, TEST_STATE_OF_ALL_VEHICLES, TEST_VEHICLE_STATE_ONLINE, TEST_VEHICLE_STATUS_AWAKE, ) +# Tessie + @pytest.fixture(autouse=True) def mock_get_state(): @@ -41,3 +48,43 @@ def mock_get_state_of_all_vehicles(): return_value=TEST_STATE_OF_ALL_VEHICLES, ) as mock_get_state_of_all_vehicles: yield mock_get_state_of_all_vehicles + + +# Fleet API +@pytest.fixture(autouse=True) +def mock_products(): + """Mock Tesla Fleet Api products method.""" + with patch( + "homeassistant.components.tessie.Tessie.products", return_value=PRODUCTS + ) as mock_products: + yield mock_products + + +@pytest.fixture(autouse=True) +def mock_request(): + """Mock Tesla Fleet API request method.""" + with patch( + "homeassistant.components.tessie.Tessie._request", + return_value=COMMAND_OK, + ) as mock_request: + yield mock_request + + +@pytest.fixture(autouse=True) +def mock_live_status(): + """Mock Tesla Fleet API EnergySpecific live_status method.""" + with patch( + "homeassistant.components.tessie.EnergySpecific.live_status", + side_effect=lambda: deepcopy(LIVE_STATUS), + ) as mock_live_status: + yield mock_live_status + + +@pytest.fixture(autouse=True) +def mock_site_info(): + """Mock Tesla Fleet API EnergySpecific site_info method.""" + with patch( + "homeassistant.components.tessie.EnergySpecific.site_info", + side_effect=lambda: deepcopy(SITE_INFO), + ) as mock_live_status: + yield mock_live_status diff --git a/tests/components/tessie/fixtures/live_status.json b/tests/components/tessie/fixtures/live_status.json new file mode 100644 index 00000000000..486f9f4fadd --- /dev/null +++ b/tests/components/tessie/fixtures/live_status.json @@ -0,0 +1,33 @@ +{ + "response": { + "solar_power": 1185, + "energy_left": 38896.47368421053, + "total_pack_energy": 40727, + "percentage_charged": 95.50537403739663, + "backup_capable": true, + "battery_power": 5060, + "load_power": 6245, + "grid_status": "Active", + "grid_services_active": false, + "grid_power": 0, + "grid_services_power": 0, + "generator_power": 0, + "island_status": "on_grid", + "storm_mode_active": false, + "timestamp": "2024-01-01T00:00:00+00:00", + "wall_connectors": [ + { + "din": "abd-123", + "wall_connector_state": 2, + "wall_connector_fault_state": 2, + "wall_connector_power": 0 + }, + { + "din": "bcd-234", + "wall_connector_state": 2, + "wall_connector_fault_state": 2, + "wall_connector_power": 0 + } + ] + } +} diff --git a/tests/components/tessie/fixtures/products.json b/tests/components/tessie/fixtures/products.json new file mode 100644 index 00000000000..e1b76e4cefb --- /dev/null +++ b/tests/components/tessie/fixtures/products.json @@ -0,0 +1,121 @@ +{ + "response": [ + { + "id": 1234, + "user_id": 1234, + "vehicle_id": 1234, + "vin": "LRWXF7EK4KC700000", + "color": null, + "access_type": "OWNER", + "display_name": "Test", + "option_codes": null, + "cached_data": null, + "granular_access": { "hide_private": false }, + "tokens": ["abc", "def"], + "state": "asleep", + "in_service": false, + "id_s": "1234", + "calendar_enabled": true, + "api_version": 71, + "backseat_token": null, + "backseat_token_updated_at": null, + "ble_autopair_enrolled": false, + "vehicle_config": { + "aux_park_lamps": "Eu", + "badge_version": 1, + "can_accept_navigation_requests": true, + "can_actuate_trunks": true, + "car_special_type": "base", + "car_type": "model3", + "charge_port_type": "CCS", + "cop_user_set_temp_supported": false, + "dashcam_clip_save_supported": true, + "default_charge_to_max": false, + "driver_assist": "TeslaAP3", + "ece_restrictions": false, + "efficiency_package": "M32021", + "eu_vehicle": true, + "exterior_color": "DeepBlue", + "exterior_trim": "Black", + "exterior_trim_override": "", + "has_air_suspension": false, + "has_ludicrous_mode": false, + "has_seat_cooling": false, + "headlamp_type": "Global", + "interior_trim_type": "White2", + "key_version": 2, + "motorized_charge_port": true, + "paint_color_override": "0,9,25,0.7,0.04", + "performance_package": "Base", + "plg": true, + "pws": true, + "rear_drive_unit": "PM216MOSFET", + "rear_seat_heaters": 1, + "rear_seat_type": 0, + "rhd": true, + "roof_color": "RoofColorGlass", + "seat_type": null, + "spoiler_type": "None", + "sun_roof_installed": null, + "supports_qr_pairing": false, + "third_row_seats": "None", + "timestamp": 1705701487912, + "trim_badging": "74d", + "use_range_badging": true, + "utc_offset": 36000, + "webcam_selfie_supported": true, + "webcam_supported": true, + "wheel_type": "Pinwheel18CapKit" + }, + "command_signing": "allowed", + "release_notes_supported": true + }, + { + "energy_site_id": 123456, + "resource_type": "battery", + "site_name": "Energy Site", + "id": "ABC123", + "gateway_id": "ABC123", + "asset_site_id": "c0ffee", + "warp_site_number": "GA123456", + "energy_left": 23286.105263157893, + "total_pack_energy": 40804, + "percentage_charged": 57.068192488868476, + "battery_type": "ac_powerwall", + "backup_capable": true, + "battery_power": 14990, + "go_off_grid_test_banner_enabled": null, + "storm_mode_enabled": true, + "powerwall_onboarding_settings_set": true, + "powerwall_tesla_electric_interested_in": null, + "vpp_tour_enabled": null, + "sync_grid_alert_enabled": true, + "breaker_alert_enabled": true, + "components": { + "battery": true, + "battery_type": "ac_powerwall", + "solar": true, + "solar_type": "pv_panel", + "grid": true, + "load_meter": true, + "market_type": "residential", + "wall_connectors": [ + { + "device_id": "abc-123", + "din": "123-abc", + "is_active": true + }, + { + "device_id": "bcd-234", + "din": "234-bcd", + "is_active": true + } + ] + }, + "features": { + "rate_plan_manager_no_pricing_constraint": true + } + } + ], + "count": 2 +} diff --git a/tests/components/tessie/fixtures/site_info.json b/tests/components/tessie/fixtures/site_info.json new file mode 100644 index 00000000000..f581707ff14 --- /dev/null +++ b/tests/components/tessie/fixtures/site_info.json @@ -0,0 +1,125 @@ +{ + "response": { + "id": "1233-abcd", + "site_name": "Site", + "backup_reserve_percent": 0, + "default_real_mode": "self_consumption", + "installation_date": "2022-01-01T00:00:00+00:00", + "user_settings": { + "go_off_grid_test_banner_enabled": false, + "storm_mode_enabled": true, + "powerwall_onboarding_settings_set": true, + "powerwall_tesla_electric_interested_in": false, + "vpp_tour_enabled": true, + "sync_grid_alert_enabled": true, + "breaker_alert_enabled": false + }, + "components": { + "solar": true, + "solar_type": "pv_panel", + "battery": true, + "grid": true, + "backup": true, + "gateway": "teg", + "load_meter": true, + "tou_capable": true, + "storm_mode_capable": true, + "flex_energy_request_capable": false, + "car_charging_data_supported": false, + "off_grid_vehicle_charging_reserve_supported": true, + "vehicle_charging_performance_view_enabled": false, + "vehicle_charging_solar_offset_view_enabled": false, + "battery_solar_offset_view_enabled": true, + "solar_value_enabled": true, + "energy_value_header": "Energy Value", + "energy_value_subheader": "Estimated Value", + "energy_service_self_scheduling_enabled": true, + "show_grid_import_battery_source_cards": true, + "set_islanding_mode_enabled": true, + "wifi_commissioning_enabled": true, + "backup_time_remaining_enabled": true, + "battery_type": "ac_powerwall", + "configurable": true, + "grid_services_enabled": false, + "gateways": [ + { + "device_id": "gateway-id", + "din": "gateway-din", + "serial_number": "CN00000000J50D", + "part_number": "1152100-14-J", + "part_type": 10, + "part_name": "Tesla Backup Gateway 2", + "is_active": true, + "site_id": "1234-abcd", + "firmware_version": "24.4.0 0fe780c9", + "updated_datetime": "2024-05-14T00:00:00.000Z" + } + ], + "batteries": [ + { + "device_id": "battery-1-id", + "din": "battery-1-din", + "serial_number": "TG000000001DA5", + "part_number": "3012170-10-B", + "part_type": 2, + "part_name": "Powerwall 2", + "nameplate_max_charge_power": 5000, + "nameplate_max_discharge_power": 5000, + "nameplate_energy": 13500 + }, + { + "device_id": "battery-2-id", + "din": "battery-2-din", + "serial_number": "TG000000002DA5", + "part_number": "3012170-05-C", + "part_type": 2, + "part_name": "Powerwall 2", + "nameplate_max_charge_power": 5000, + "nameplate_max_discharge_power": 5000, + "nameplate_energy": 13500 + } + ], + "wall_connectors": [ + { + "device_id": "123abc", + "din": "abc123", + "is_active": true + }, + { + "device_id": "234bcd", + "din": "bcd234", + "is_active": true + } + ], + "disallow_charge_from_grid_with_solar_installed": true, + "customer_preferred_export_rule": "pv_only", + "net_meter_mode": "battery_ok", + "system_alerts_enabled": true + }, + "version": "23.44.0 eb113390", + "battery_count": 2, + "tou_settings": { + "optimization_strategy": "economics", + "schedule": [ + { + "target": "off_peak", + "week_days": [1, 0], + "start_seconds": 0, + "end_seconds": 3600 + }, + { + "target": "peak", + "week_days": [1, 0], + "start_seconds": 3600, + "end_seconds": 0 + } + ] + }, + "nameplate_power": 15000, + "nameplate_energy": 40500, + "installation_time_zone": "", + "max_site_meter_power_ac": 1000000000, + "min_site_meter_power_ac": -1000000000, + "vpp_backup_reserve_percent": 0 + } +} diff --git a/tests/components/tessie/snapshots/test_sensor.ambr b/tests/components/tessie/snapshots/test_sensor.ambr index 48beab6133c..ba7b4eae0a5 100644 --- a/tests/components/tessie/snapshots/test_sensor.ambr +++ b/tests/components/tessie/snapshots/test_sensor.ambr @@ -1,4 +1,562 @@ # serializer version: 1 +# name: test_sensors[sensor.energy_site_battery_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.energy_site_battery_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery power', + 'platform': 'tessie', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'battery_power', + 'unique_id': '123456-battery_power', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.energy_site_battery_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Energy Site Battery power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.energy_site_battery_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.energy_site_energy_left-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.energy_site_energy_left', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Energy left', + 'platform': 'tessie', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'energy_left', + 'unique_id': '123456-energy_left', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.energy_site_energy_left-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy_storage', + 'friendly_name': 'Energy Site Energy left', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.energy_site_energy_left', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.energy_site_generator_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.energy_site_generator_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Generator power', + 'platform': 'tessie', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'generator_power', + 'unique_id': '123456-generator_power', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.energy_site_generator_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Energy Site Generator power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.energy_site_generator_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.energy_site_grid_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.energy_site_grid_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Grid power', + 'platform': 'tessie', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'grid_power', + 'unique_id': '123456-grid_power', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.energy_site_grid_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Energy Site Grid power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.energy_site_grid_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.energy_site_grid_services_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.energy_site_grid_services_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Grid services power', + 'platform': 'tessie', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'grid_services_power', + 'unique_id': '123456-grid_services_power', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.energy_site_grid_services_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Energy Site Grid services power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.energy_site_grid_services_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.energy_site_load_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.energy_site_load_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Load power', + 'platform': 'tessie', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'load_power', + 'unique_id': '123456-load_power', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.energy_site_load_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Energy Site Load power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.energy_site_load_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.energy_site_percentage_charged-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.energy_site_percentage_charged', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Percentage charged', + 'platform': 'tessie', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'percentage_charged', + 'unique_id': '123456-percentage_charged', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensors[sensor.energy_site_percentage_charged-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Energy Site Percentage charged', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.energy_site_percentage_charged', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.energy_site_solar_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.energy_site_solar_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Solar power', + 'platform': 'tessie', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'solar_power', + 'unique_id': '123456-solar_power', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.energy_site_solar_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Energy Site Solar power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.energy_site_solar_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.energy_site_total_pack_energy-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.energy_site_total_pack_energy', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Total pack energy', + 'platform': 'tessie', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'total_pack_energy', + 'unique_id': '123456-total_pack_energy', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.energy_site_total_pack_energy-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy_storage', + 'friendly_name': 'Energy Site Total pack energy', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.energy_site_total_pack_energy', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.energy_site_vpp_backup_reserve-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.energy_site_vpp_backup_reserve', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'VPP backup reserve', + 'platform': 'tessie', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'vpp_backup_reserve_percent', + 'unique_id': '123456-vpp_backup_reserve_percent', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensors[sensor.energy_site_vpp_backup_reserve-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Energy Site VPP backup reserve', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.energy_site_vpp_backup_reserve', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- # name: test_sensors[sensor.test_battery_level-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -592,42 +1150,6 @@ 'state': 'Giga Texas', }) # --- -# name: test_sensors[sensor.test_distance-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': , - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.test_distance', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - 'sensor.private': dict({ - 'suggested_unit_of_measurement': , - }), - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Distance', - 'platform': 'tessie', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'charge_state_est_battery_range', - 'unique_id': 'VINVINVIN-charge_state_est_battery_range', - 'unit_of_measurement': , - }) -# --- # name: test_sensors[sensor.test_distance_to_arrival-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -1544,3 +2066,353 @@ 'state': '0', }) # --- +# name: test_sensors[sensor.wall_connector_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.wall_connector_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Power', + 'platform': 'tessie', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'wall_connector_power', + 'unique_id': '123456-abd-123-wall_connector_power', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.wall_connector_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Wall Connector Power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.wall_connector_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.wall_connector_power_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.wall_connector_power_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Power', + 'platform': 'tessie', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'wall_connector_power', + 'unique_id': '123456-bcd-234-wall_connector_power', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.wall_connector_power_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Wall Connector Power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.wall_connector_power_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.wall_connector_state-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'booting', + 'charging', + 'disconnected', + 'connected', + 'scheduled', + 'negotiating', + 'error', + 'charging_finished', + 'waiting_car', + 'charging_reduced', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.wall_connector_state', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'State', + 'platform': 'tessie', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'wall_connector_state', + 'unique_id': '123456-abd-123-wall_connector_state', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.wall_connector_state-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Wall Connector State', + 'options': list([ + 'booting', + 'charging', + 'disconnected', + 'connected', + 'scheduled', + 'negotiating', + 'error', + 'charging_finished', + 'waiting_car', + 'charging_reduced', + ]), + }), + 'context': , + 'entity_id': 'sensor.wall_connector_state', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.wall_connector_state_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'booting', + 'charging', + 'disconnected', + 'connected', + 'scheduled', + 'negotiating', + 'error', + 'charging_finished', + 'waiting_car', + 'charging_reduced', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.wall_connector_state_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'State', + 'platform': 'tessie', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'wall_connector_state', + 'unique_id': '123456-bcd-234-wall_connector_state', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.wall_connector_state_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Wall Connector State', + 'options': list([ + 'booting', + 'charging', + 'disconnected', + 'connected', + 'scheduled', + 'negotiating', + 'error', + 'charging_finished', + 'waiting_car', + 'charging_reduced', + ]), + }), + 'context': , + 'entity_id': 'sensor.wall_connector_state_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.wall_connector_vehicle-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.wall_connector_vehicle', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Vehicle', + 'platform': 'tessie', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'vin', + 'unique_id': '123456-abd-123-vin', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.wall_connector_vehicle-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Wall Connector Vehicle', + }), + 'context': , + 'entity_id': 'sensor.wall_connector_vehicle', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.wall_connector_vehicle_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.wall_connector_vehicle_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Vehicle', + 'platform': 'tessie', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'vin', + 'unique_id': '123456-bcd-234-vin', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.wall_connector_vehicle_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Wall Connector Vehicle', + }), + 'context': , + 'entity_id': 'sensor.wall_connector_vehicle_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- diff --git a/tests/components/tessie/test_coordinator.py b/tests/components/tessie/test_coordinator.py index c4c1b6d1e72..77b2829b53a 100644 --- a/tests/components/tessie/test_coordinator.py +++ b/tests/components/tessie/test_coordinator.py @@ -2,11 +2,17 @@ from datetime import timedelta +from freezegun.api import FrozenDateTimeFactory +from tesla_fleet_api.exceptions import Forbidden, InvalidToken + from homeassistant.components.tessie import PLATFORMS -from homeassistant.components.tessie.coordinator import TESSIE_SYNC_INTERVAL +from homeassistant.components.tessie.coordinator import ( + TESSIE_FLEET_API_SYNC_INTERVAL, + TESSIE_SYNC_INTERVAL, +) +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE, Platform from homeassistant.core import HomeAssistant -from homeassistant.util.dt import utcnow from .common import ( ERROR_AUTH, @@ -22,60 +28,124 @@ WAIT = timedelta(seconds=TESSIE_SYNC_INTERVAL) async def test_coordinator_online( - hass: HomeAssistant, mock_get_state, mock_get_status + hass: HomeAssistant, mock_get_state, mock_get_status, freezer: FrozenDateTimeFactory ) -> None: """Tests that the coordinator handles online vehicles.""" await setup_platform(hass, PLATFORMS) - async_fire_time_changed(hass, utcnow() + WAIT) + freezer.tick(WAIT) + async_fire_time_changed(hass) await hass.async_block_till_done() mock_get_status.assert_called_once() mock_get_state.assert_called_once() assert hass.states.get("binary_sensor.test_status").state == STATE_ON -async def test_coordinator_asleep(hass: HomeAssistant, mock_get_status) -> None: +async def test_coordinator_asleep( + hass: HomeAssistant, mock_get_status, freezer: FrozenDateTimeFactory +) -> None: """Tests that the coordinator handles asleep vehicles.""" await setup_platform(hass, [Platform.BINARY_SENSOR]) mock_get_status.return_value = TEST_VEHICLE_STATUS_ASLEEP - async_fire_time_changed(hass, utcnow() + WAIT) + freezer.tick(WAIT) + async_fire_time_changed(hass) await hass.async_block_till_done() mock_get_status.assert_called_once() assert hass.states.get("binary_sensor.test_status").state == STATE_OFF -async def test_coordinator_clienterror(hass: HomeAssistant, mock_get_status) -> None: +async def test_coordinator_clienterror( + hass: HomeAssistant, mock_get_status, freezer: FrozenDateTimeFactory +) -> None: """Tests that the coordinator handles client errors.""" mock_get_status.side_effect = ERROR_UNKNOWN await setup_platform(hass, [Platform.BINARY_SENSOR]) - async_fire_time_changed(hass, utcnow() + WAIT) + freezer.tick(WAIT) + async_fire_time_changed(hass) await hass.async_block_till_done() mock_get_status.assert_called_once() assert hass.states.get("binary_sensor.test_status").state == STATE_UNAVAILABLE -async def test_coordinator_auth(hass: HomeAssistant, mock_get_status) -> None: - """Tests that the coordinator handles timeout errors.""" +async def test_coordinator_auth( + hass: HomeAssistant, mock_get_status, freezer: FrozenDateTimeFactory +) -> None: + """Tests that the coordinator handles auth errors.""" mock_get_status.side_effect = ERROR_AUTH await setup_platform(hass, [Platform.BINARY_SENSOR]) - async_fire_time_changed(hass, utcnow() + WAIT) + freezer.tick(WAIT) + async_fire_time_changed(hass) await hass.async_block_till_done() mock_get_status.assert_called_once() -async def test_coordinator_connection(hass: HomeAssistant, mock_get_status) -> None: +async def test_coordinator_connection( + hass: HomeAssistant, mock_get_status, freezer: FrozenDateTimeFactory +) -> None: """Tests that the coordinator handles connection errors.""" mock_get_status.side_effect = ERROR_CONNECTION await setup_platform(hass, [Platform.BINARY_SENSOR]) - async_fire_time_changed(hass, utcnow() + WAIT) + freezer.tick(WAIT) + async_fire_time_changed(hass) await hass.async_block_till_done() mock_get_status.assert_called_once() assert hass.states.get("binary_sensor.test_status").state == STATE_UNAVAILABLE + + +async def test_coordinator_live_error( + hass: HomeAssistant, mock_live_status, freezer: FrozenDateTimeFactory +) -> None: + """Tests that the energy live coordinator handles fleet errors.""" + + await setup_platform(hass, [Platform.SENSOR]) + + mock_live_status.reset_mock() + mock_live_status.side_effect = Forbidden + freezer.tick(TESSIE_FLEET_API_SYNC_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done() + mock_live_status.assert_called_once() + assert hass.states.get("sensor.energy_site_solar_power").state == STATE_UNAVAILABLE + + +async def test_coordinator_info_error( + hass: HomeAssistant, mock_site_info, freezer: FrozenDateTimeFactory +) -> None: + """Tests that the energy info coordinator handles fleet errors.""" + + await setup_platform(hass, [Platform.SENSOR]) + + mock_site_info.reset_mock() + mock_site_info.side_effect = Forbidden + freezer.tick(TESSIE_FLEET_API_SYNC_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done() + mock_site_info.assert_called_once() + assert ( + hass.states.get("sensor.energy_site_vpp_backup_reserve").state + == STATE_UNAVAILABLE + ) + + +async def test_coordinator_live_reauth(hass: HomeAssistant, mock_live_status) -> None: + """Tests that the energy live coordinator handles auth errors.""" + + mock_live_status.side_effect = InvalidToken + entry = await setup_platform(hass, [Platform.SENSOR]) + assert entry.state is ConfigEntryState.SETUP_ERROR + + +async def test_coordinator_info_reauth(hass: HomeAssistant, mock_site_info) -> None: + """Tests that the energy info coordinator handles auth errors.""" + + mock_site_info.side_effect = InvalidToken + entry = await setup_platform(hass, [Platform.SENSOR]) + assert entry.state is ConfigEntryState.SETUP_ERROR diff --git a/tests/components/tessie/test_init.py b/tests/components/tessie/test_init.py index 81d1d758edf..e37512ea8c4 100644 --- a/tests/components/tessie/test_init.py +++ b/tests/components/tessie/test_init.py @@ -1,5 +1,9 @@ """Test the Tessie init.""" +from unittest.mock import patch + +from tesla_fleet_api.exceptions import TeslaFleetError + from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant @@ -44,3 +48,13 @@ async def test_connection_failure( mock_get_state_of_all_vehicles.side_effect = ERROR_CONNECTION entry = await setup_platform(hass) assert entry.state is ConfigEntryState.SETUP_RETRY + + +async def test_fleet_error(hass: HomeAssistant) -> None: + """Test init with a fleet error.""" + + with patch( + "homeassistant.components.tessie.Tessie.products", side_effect=TeslaFleetError + ): + entry = await setup_platform(hass) + assert entry.state is ConfigEntryState.SETUP_RETRY From b9be491016286840d436eed0bd9213417f463edc Mon Sep 17 00:00:00 2001 From: Luke Lashley Date: Wed, 26 Jun 2024 08:32:26 -0400 Subject: [PATCH 1282/1445] Add options flow to Roborock (#104345) Co-authored-by: Robert Resch --- homeassistant/components/roborock/__init__.py | 10 ++- .../components/roborock/config_flow.py | 61 +++++++++++++++++-- homeassistant/components/roborock/const.py | 27 ++++++-- homeassistant/components/roborock/image.py | 20 ++++-- .../components/roborock/strings.json | 26 ++++++++ tests/components/roborock/test_config_flow.py | 25 +++++++- 6 files changed, 152 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/roborock/__init__.py b/homeassistant/components/roborock/__init__.py index d7ce0e0f5ec..cdbddbda95b 100644 --- a/homeassistant/components/roborock/__init__.py +++ b/homeassistant/components/roborock/__init__.py @@ -31,6 +31,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up roborock from a config entry.""" _LOGGER.debug("Integration async setup entry: %s", entry.as_dict()) + entry.async_on_unload(entry.add_update_listener(update_listener)) user_data = UserData.from_dict(entry.data[CONF_USER_DATA]) api_client = RoborockApiClient(entry.data[CONF_USERNAME], entry.data[CONF_BASE_URL]) @@ -50,8 +51,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: translation_key="home_data_fail", ) from err _LOGGER.debug("Got home data %s", home_data) + all_devices: list[HomeDataDevice] = home_data.devices + home_data.received_devices device_map: dict[str, HomeDataDevice] = { - device.duid: device for device in home_data.devices + home_data.received_devices + device.duid: device for device in all_devices } product_info: dict[str, HomeDataProduct] = { product.id: product for product in home_data.products @@ -177,3 +179,9 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN].pop(entry.entry_id) await asyncio.gather(*release_tasks) return unload_ok + + +async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Handle options update.""" + # Reload entry to update data + await hass.config_entries.async_reload(entry.entry_id) diff --git a/homeassistant/components/roborock/config_flow.py b/homeassistant/components/roborock/config_flow.py index c7347178612..2b409bdf8c4 100644 --- a/homeassistant/components/roborock/config_flow.py +++ b/homeassistant/components/roborock/config_flow.py @@ -17,10 +17,24 @@ from roborock.exceptions import ( from roborock.web_api import RoborockApiClient import voluptuous as vol -from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult +from homeassistant.config_entries import ( + ConfigEntry, + ConfigFlow, + ConfigFlowResult, + OptionsFlow, + OptionsFlowWithConfigEntry, +) from homeassistant.const import CONF_USERNAME +from homeassistant.core import callback -from .const import CONF_BASE_URL, CONF_ENTRY_CODE, CONF_USER_DATA, DOMAIN +from .const import ( + CONF_BASE_URL, + CONF_ENTRY_CODE, + CONF_USER_DATA, + DEFAULT_DRAWABLES, + DOMAIN, + DRAWABLES, +) _LOGGER = logging.getLogger(__name__) @@ -107,9 +121,6 @@ class RoborockFlowHandler(ConfigFlow, domain=DOMAIN): CONF_USER_DATA: login_data.as_dict(), }, ) - await self.hass.config_entries.async_reload( - self.reauth_entry.entry_id - ) return self.async_abort(reason="reauth_successful") return self._create_entry(self._client, self._username, login_data) @@ -154,3 +165,43 @@ class RoborockFlowHandler(ConfigFlow, domain=DOMAIN): CONF_BASE_URL: client.base_url, }, ) + + @staticmethod + @callback + def async_get_options_flow( + config_entry: ConfigEntry, + ) -> OptionsFlow: + """Create the options flow.""" + return RoborockOptionsFlowHandler(config_entry) + + +class RoborockOptionsFlowHandler(OptionsFlowWithConfigEntry): + """Handle an option flow for Roborock.""" + + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Manage the options.""" + return await self.async_step_drawables() + + async def async_step_drawables( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Manage the map object drawable options.""" + if user_input is not None: + self.options.setdefault(DRAWABLES, {}).update(user_input) + return self.async_create_entry(title="", data=self.options) + data_schema = {} + for drawable, default_value in DEFAULT_DRAWABLES.items(): + data_schema[ + vol.Required( + drawable.value, + default=self.config_entry.options.get(DRAWABLES, {}).get( + drawable, default_value + ), + ) + ] = bool + return self.async_show_form( + step_id=DRAWABLES, + data_schema=vol.Schema(data_schema), + ) diff --git a/homeassistant/components/roborock/const.py b/homeassistant/components/roborock/const.py index 6b1ed975fca..834b25965c3 100644 --- a/homeassistant/components/roborock/const.py +++ b/homeassistant/components/roborock/const.py @@ -9,6 +9,28 @@ CONF_ENTRY_CODE = "code" CONF_BASE_URL = "base_url" CONF_USER_DATA = "user_data" +# Option Flow steps +DRAWABLES = "drawables" + +DEFAULT_DRAWABLES = { + Drawable.CHARGER: True, + Drawable.CLEANED_AREA: False, + Drawable.GOTO_PATH: False, + Drawable.IGNORED_OBSTACLES: False, + Drawable.IGNORED_OBSTACLES_WITH_PHOTO: False, + Drawable.MOP_PATH: False, + Drawable.NO_CARPET_AREAS: False, + Drawable.NO_GO_AREAS: False, + Drawable.NO_MOPPING_AREAS: False, + Drawable.OBSTACLES: False, + Drawable.OBSTACLES_WITH_PHOTO: False, + Drawable.PATH: True, + Drawable.PREDICTED_PATH: False, + Drawable.VACUUM_POSITION: True, + Drawable.VIRTUAL_WALLS: False, + Drawable.ZONES: False, +} + PLATFORMS = [ Platform.BINARY_SENSOR, Platform.BUTTON, @@ -21,11 +43,6 @@ PLATFORMS = [ Platform.VACUUM, ] -IMAGE_DRAWABLES: list[Drawable] = [ - Drawable.PATH, - Drawable.CHARGER, - Drawable.VACUUM_POSITION, -] IMAGE_CACHE_INTERVAL = 90 diff --git a/homeassistant/components/roborock/image.py b/homeassistant/components/roborock/image.py index 33b8b0a2c90..9dfe8d53cd3 100644 --- a/homeassistant/components/roborock/image.py +++ b/homeassistant/components/roborock/image.py @@ -7,6 +7,7 @@ from itertools import chain from roborock import RoborockCommand from vacuum_map_parser_base.config.color import ColorsPalette +from vacuum_map_parser_base.config.drawable import Drawable from vacuum_map_parser_base.config.image_config import ImageConfig from vacuum_map_parser_base.config.size import Sizes from vacuum_map_parser_roborock.map_data_parser import RoborockMapDataParser @@ -20,7 +21,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import slugify import homeassistant.util.dt as dt_util -from .const import DOMAIN, IMAGE_CACHE_INTERVAL, IMAGE_DRAWABLES, MAP_SLEEP +from .const import DEFAULT_DRAWABLES, DOMAIN, DRAWABLES, IMAGE_CACHE_INTERVAL, MAP_SLEEP from .coordinator import RoborockDataUpdateCoordinator from .device import RoborockCoordinatedEntity @@ -35,10 +36,18 @@ async def async_setup_entry( coordinators: dict[str, RoborockDataUpdateCoordinator] = hass.data[DOMAIN][ config_entry.entry_id ] + drawables = [ + drawable + for drawable, default_value in DEFAULT_DRAWABLES.items() + if config_entry.options.get(DRAWABLES, {}).get(drawable, default_value) + ] entities = list( chain.from_iterable( await asyncio.gather( - *(create_coordinator_maps(coord) for coord in coordinators.values()) + *( + create_coordinator_maps(coord, drawables) + for coord in coordinators.values() + ) ) ) ) @@ -58,13 +67,14 @@ class RoborockMap(RoborockCoordinatedEntity, ImageEntity): map_flag: int, starting_map: bytes, map_name: str, + drawables: list[Drawable], ) -> None: """Initialize a Roborock map.""" RoborockCoordinatedEntity.__init__(self, unique_id, coordinator) ImageEntity.__init__(self, coordinator.hass) self._attr_name = map_name self.parser = RoborockMapDataParser( - ColorsPalette(), Sizes(), IMAGE_DRAWABLES, ImageConfig(), [] + ColorsPalette(), Sizes(), drawables, ImageConfig(), [] ) self._attr_image_last_updated = dt_util.utcnow() self.map_flag = map_flag @@ -140,7 +150,7 @@ class RoborockMap(RoborockCoordinatedEntity, ImageEntity): async def create_coordinator_maps( - coord: RoborockDataUpdateCoordinator, + coord: RoborockDataUpdateCoordinator, drawables: list[Drawable] ) -> list[RoborockMap]: """Get the starting map information for all maps for this device. @@ -148,7 +158,6 @@ async def create_coordinator_maps( Only one map can be loaded at a time per device. """ entities = [] - cur_map = coord.current_map # This won't be None at this point as the coordinator will have run first. assert cur_map is not None @@ -180,6 +189,7 @@ async def create_coordinator_maps( map_flag, api_data, map_info.name, + drawables, ) ) if len(coord.maps) != 1: diff --git a/homeassistant/components/roborock/strings.json b/homeassistant/components/roborock/strings.json index 30aa64f626a..aaf476d7fc6 100644 --- a/homeassistant/components/roborock/strings.json +++ b/homeassistant/components/roborock/strings.json @@ -31,6 +31,32 @@ "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" } }, + "options": { + "step": { + "drawables": { + "description": "Specify which features to draw on the map.", + "data": { + "charger": "Charger", + "cleaned_area": "Cleaned area", + "goto_path": "Go-to path", + "ignored_obstacles": "Ignored obstacles", + "ignored_obstacles_with_photo": "Ignored obstacles with photo", + "mop_path": "Mop path", + "no_carpet_zones": "No carpet zones", + "no_go_zones": "No-go zones", + "no_mopping_zones": "No mopping zones", + "obstacles": "Obstacles", + "obstacles_with_photo": "Obstacles with photo", + "path": "Path", + "predicted_path": "Predicted path", + "room_names": "Room names", + "vacuum_position": "Vacuum position", + "virtual_walls": "Virtual walls", + "zones": "Zones" + } + } + } + }, "entity": { "binary_sensor": { "in_cleaning": { diff --git a/tests/components/roborock/test_config_flow.py b/tests/components/roborock/test_config_flow.py index 5134ef7eea2..a5a86e44372 100644 --- a/tests/components/roborock/test_config_flow.py +++ b/tests/components/roborock/test_config_flow.py @@ -11,9 +11,10 @@ from roborock.exceptions import ( RoborockInvalidEmail, RoborockUrlException, ) +from vacuum_map_parser_base.config.drawable import Drawable from homeassistant import config_entries -from homeassistant.components.roborock.const import CONF_ENTRY_CODE, DOMAIN +from homeassistant.components.roborock.const import CONF_ENTRY_CODE, DOMAIN, DRAWABLES from homeassistant.const import CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -185,6 +186,28 @@ async def test_config_flow_failures_code_login( assert len(mock_setup.mock_calls) == 1 +async def test_options_flow_drawables( + hass: HomeAssistant, setup_entry: MockConfigEntry +) -> None: + """Test that the options flow works.""" + result = await hass.config_entries.options.async_init(setup_entry.entry_id) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == DRAWABLES + with patch( + "homeassistant.components.roborock.async_setup_entry", return_value=True + ) as mock_setup: + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={Drawable.PREDICTED_PATH: True}, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert setup_entry.options[DRAWABLES][Drawable.PREDICTED_PATH] is True + assert len(mock_setup.mock_calls) == 1 + + async def test_reauth_flow( hass: HomeAssistant, bypass_api_fixture, mock_roborock_entry: MockConfigEntry ) -> None: From fc2968bc1be34168445b311e19d1e3a9e8a0cba9 Mon Sep 17 00:00:00 2001 From: Teemu R Date: Wed, 26 Jun 2024 14:35:22 +0200 Subject: [PATCH 1283/1445] Adjust tplink codeowners (#120561) --- CODEOWNERS | 4 ++-- homeassistant/components/tplink/manifest.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 973780b811c..7834add43f6 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1460,8 +1460,8 @@ build.json @home-assistant/supervisor /tests/components/tomorrowio/ @raman325 @lymanepp /homeassistant/components/totalconnect/ @austinmroczek /tests/components/totalconnect/ @austinmroczek -/homeassistant/components/tplink/ @rytilahti @thegardenmonkey @bdraco @sdb9696 -/tests/components/tplink/ @rytilahti @thegardenmonkey @bdraco @sdb9696 +/homeassistant/components/tplink/ @rytilahti @bdraco @sdb9696 +/tests/components/tplink/ @rytilahti @bdraco @sdb9696 /homeassistant/components/tplink_omada/ @MarkGodwin /tests/components/tplink_omada/ @MarkGodwin /homeassistant/components/traccar/ @ludeeus diff --git a/homeassistant/components/tplink/manifest.json b/homeassistant/components/tplink/manifest.json index 5b8e6f8fc1b..74b80771c65 100644 --- a/homeassistant/components/tplink/manifest.json +++ b/homeassistant/components/tplink/manifest.json @@ -1,7 +1,7 @@ { "domain": "tplink", "name": "TP-Link Smart Home", - "codeowners": ["@rytilahti", "@thegardenmonkey", "@bdraco", "@sdb9696"], + "codeowners": ["@rytilahti", "@bdraco", "@sdb9696"], "config_flow": true, "dependencies": ["network"], "dhcp": [ From 7eb9875a9e11dd45f0fe20f0a54c268340154f77 Mon Sep 17 00:00:00 2001 From: "Mr. Bubbles" Date: Wed, 26 Jun 2024 14:45:04 +0200 Subject: [PATCH 1284/1445] Add Base class for entities in PyLoad integration (#120563) * Add Base class for entities * Remove constructors --- homeassistant/components/pyload/button.py | 28 +---------- homeassistant/components/pyload/entity.py | 37 ++++++++++++++ homeassistant/components/pyload/sensor.py | 59 ++++++++++------------- homeassistant/components/pyload/switch.py | 28 +---------- 4 files changed, 66 insertions(+), 86 deletions(-) create mode 100644 homeassistant/components/pyload/entity.py diff --git a/homeassistant/components/pyload/button.py b/homeassistant/components/pyload/button.py index 0d8a232142a..950177f8751 100644 --- a/homeassistant/components/pyload/button.py +++ b/homeassistant/components/pyload/button.py @@ -11,13 +11,10 @@ from pyloadapi.api import PyLoadAPI from homeassistant.components.button import ButtonEntity, ButtonEntityDescription from homeassistant.core import HomeAssistant -from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import PyLoadConfigEntry -from .const import DOMAIN, MANUFACTURER, SERVICE_NAME -from .coordinator import PyLoadCoordinator +from .entity import BasePyLoadEntity @dataclass(kw_only=True, frozen=True) @@ -76,32 +73,11 @@ async def async_setup_entry( ) -class PyLoadBinarySensor(CoordinatorEntity[PyLoadCoordinator], ButtonEntity): +class PyLoadBinarySensor(BasePyLoadEntity, ButtonEntity): """Representation of a pyLoad button.""" - _attr_has_entity_name = True entity_description: PyLoadButtonEntityDescription - def __init__( - self, - coordinator: PyLoadCoordinator, - entity_description: PyLoadButtonEntityDescription, - ) -> None: - """Initialize the button.""" - super().__init__(coordinator) - self._attr_unique_id = ( - f"{coordinator.config_entry.entry_id}_{entity_description.key}" - ) - self.entity_description = entity_description - self._attr_device_info = DeviceInfo( - entry_type=DeviceEntryType.SERVICE, - manufacturer=MANUFACTURER, - model=SERVICE_NAME, - configuration_url=coordinator.pyload.api_url, - identifiers={(DOMAIN, coordinator.config_entry.entry_id)}, - sw_version=coordinator.version, - ) - async def async_press(self) -> None: """Handle the button press.""" await self.entity_description.press_fn(self.coordinator.pyload) diff --git a/homeassistant/components/pyload/entity.py b/homeassistant/components/pyload/entity.py new file mode 100644 index 00000000000..58e93431ca1 --- /dev/null +++ b/homeassistant/components/pyload/entity.py @@ -0,0 +1,37 @@ +"""Base entity for pyLoad.""" + +from __future__ import annotations + +from homeassistant.components.button import EntityDescription +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN, MANUFACTURER, SERVICE_NAME +from .coordinator import PyLoadCoordinator + + +class BasePyLoadEntity(CoordinatorEntity[PyLoadCoordinator]): + """BaseEntity for pyLoad.""" + + _attr_has_entity_name = True + + def __init__( + self, + coordinator: PyLoadCoordinator, + entity_description: EntityDescription, + ) -> None: + """Initialize the Entity.""" + super().__init__(coordinator) + + self._attr_unique_id = ( + f"{coordinator.config_entry.entry_id}_{entity_description.key}" + ) + self.entity_description = entity_description + self._attr_device_info = DeviceInfo( + entry_type=DeviceEntryType.SERVICE, + manufacturer=MANUFACTURER, + model=SERVICE_NAME, + configuration_url=coordinator.pyload.api_url, + identifiers={(DOMAIN, coordinator.config_entry.entry_id)}, + sw_version=coordinator.version, + ) diff --git a/homeassistant/components/pyload/sensor.py b/homeassistant/components/pyload/sensor.py index bc90fdb7ccb..4a0502707b6 100644 --- a/homeassistant/components/pyload/sensor.py +++ b/homeassistant/components/pyload/sensor.py @@ -2,6 +2,8 @@ from __future__ import annotations +from collections.abc import Callable +from dataclasses import dataclass from enum import StrEnum import voluptuous as vol @@ -28,11 +30,9 @@ from homeassistant.const import ( from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant from homeassistant.data_entry_flow import FlowResultType import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType -from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import PyLoadConfigEntry from .const import ( @@ -41,11 +41,10 @@ from .const import ( DEFAULT_PORT, DOMAIN, ISSUE_PLACEHOLDER, - MANUFACTURER, - SERVICE_NAME, UNIT_DOWNLOADS, ) -from .coordinator import PyLoadCoordinator +from .coordinator import pyLoadData +from .entity import BasePyLoadEntity class PyLoadSensorEntity(StrEnum): @@ -58,40 +57,52 @@ class PyLoadSensorEntity(StrEnum): TOTAL = "total" -SENSOR_DESCRIPTIONS: tuple[SensorEntityDescription, ...] = ( - SensorEntityDescription( +@dataclass(kw_only=True, frozen=True) +class PyLoadSensorEntityDescription(SensorEntityDescription): + """Describes pyLoad switch entity.""" + + value_fn: Callable[[pyLoadData], StateType] + + +SENSOR_DESCRIPTIONS: tuple[PyLoadSensorEntityDescription, ...] = ( + PyLoadSensorEntityDescription( key=PyLoadSensorEntity.SPEED, translation_key=PyLoadSensorEntity.SPEED, device_class=SensorDeviceClass.DATA_RATE, native_unit_of_measurement=UnitOfDataRate.BYTES_PER_SECOND, suggested_unit_of_measurement=UnitOfDataRate.MEGABITS_PER_SECOND, suggested_display_precision=1, + value_fn=lambda data: data.speed, ), - SensorEntityDescription( + PyLoadSensorEntityDescription( key=PyLoadSensorEntity.ACTIVE, translation_key=PyLoadSensorEntity.ACTIVE, native_unit_of_measurement=UNIT_DOWNLOADS, state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda data: data.active, ), - SensorEntityDescription( + PyLoadSensorEntityDescription( key=PyLoadSensorEntity.QUEUE, translation_key=PyLoadSensorEntity.QUEUE, native_unit_of_measurement=UNIT_DOWNLOADS, state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda data: data.queue, ), - SensorEntityDescription( + PyLoadSensorEntityDescription( key=PyLoadSensorEntity.TOTAL, translation_key=PyLoadSensorEntity.TOTAL, native_unit_of_measurement=UNIT_DOWNLOADS, state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda data: data.total, ), - SensorEntityDescription( + PyLoadSensorEntityDescription( key=PyLoadSensorEntity.FREE_SPACE, translation_key=PyLoadSensorEntity.FREE_SPACE, device_class=SensorDeviceClass.DATA_SIZE, native_unit_of_measurement=UnitOfInformation.BYTES, suggested_unit_of_measurement=UnitOfInformation.GIBIBYTES, suggested_display_precision=1, + value_fn=lambda data: data.free_space, ), ) @@ -173,32 +184,12 @@ async def async_setup_entry( ) -class PyLoadSensor(CoordinatorEntity[PyLoadCoordinator], SensorEntity): +class PyLoadSensor(BasePyLoadEntity, SensorEntity): """Representation of a pyLoad sensor.""" - _attr_has_entity_name = True - - def __init__( - self, - coordinator: PyLoadCoordinator, - entity_description: SensorEntityDescription, - ) -> None: - """Initialize a new pyLoad sensor.""" - super().__init__(coordinator) - self._attr_unique_id = ( - f"{coordinator.config_entry.entry_id}_{entity_description.key}" - ) - self.entity_description = entity_description - self._attr_device_info = DeviceInfo( - entry_type=DeviceEntryType.SERVICE, - manufacturer=MANUFACTURER, - model=SERVICE_NAME, - configuration_url=coordinator.pyload.api_url, - identifiers={(DOMAIN, coordinator.config_entry.entry_id)}, - sw_version=coordinator.version, - ) + entity_description: PyLoadSensorEntityDescription @property def native_value(self) -> StateType: """Return the state of the sensor.""" - return getattr(self.coordinator.data, self.entity_description.key) + return self.entity_description.value_fn(self.coordinator.data) diff --git a/homeassistant/components/pyload/switch.py b/homeassistant/components/pyload/switch.py index b0628005008..4ed3e925488 100644 --- a/homeassistant/components/pyload/switch.py +++ b/homeassistant/components/pyload/switch.py @@ -15,13 +15,10 @@ from homeassistant.components.switch import ( SwitchEntityDescription, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import PyLoadConfigEntry -from .const import DOMAIN, MANUFACTURER, SERVICE_NAME -from .coordinator import PyLoadCoordinator +from .entity import BasePyLoadEntity class PyLoadSwitch(StrEnum): @@ -75,32 +72,11 @@ async def async_setup_entry( ) -class PyLoadSwitchEntity(CoordinatorEntity[PyLoadCoordinator], SwitchEntity): +class PyLoadSwitchEntity(BasePyLoadEntity, SwitchEntity): """Representation of a pyLoad sensor.""" - _attr_has_entity_name = True entity_description: PyLoadSwitchEntityDescription - def __init__( - self, - coordinator: PyLoadCoordinator, - entity_description: PyLoadSwitchEntityDescription, - ) -> None: - """Initialize the sensor.""" - super().__init__(coordinator) - self._attr_unique_id = ( - f"{coordinator.config_entry.entry_id}_{entity_description.key}" - ) - self.entity_description = entity_description - self._attr_device_info = DeviceInfo( - entry_type=DeviceEntryType.SERVICE, - manufacturer=MANUFACTURER, - model=SERVICE_NAME, - configuration_url=coordinator.pyload.api_url, - identifiers={(DOMAIN, coordinator.config_entry.entry_id)}, - sw_version=coordinator.version, - ) - @property def is_on(self) -> bool | None: """Return the state of the device.""" From 43d686e0f122781023402ae4148648396a2ee542 Mon Sep 17 00:00:00 2001 From: "Mr. Bubbles" Date: Wed, 26 Jun 2024 14:59:14 +0200 Subject: [PATCH 1285/1445] Redact the hostname in pyLoad diagnostics (#120567) --- homeassistant/components/pyload/diagnostics.py | 4 ++-- tests/components/pyload/snapshots/test_diagnostics.ambr | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/pyload/diagnostics.py b/homeassistant/components/pyload/diagnostics.py index 95ff37bf9f8..1b719ffc7b9 100644 --- a/homeassistant/components/pyload/diagnostics.py +++ b/homeassistant/components/pyload/diagnostics.py @@ -6,13 +6,13 @@ from dataclasses import asdict from typing import Any from homeassistant.components.diagnostics import async_redact_data -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from . import PyLoadConfigEntry from .coordinator import pyLoadData -TO_REDACT = {CONF_USERNAME, CONF_PASSWORD} +TO_REDACT = {CONF_USERNAME, CONF_PASSWORD, CONF_HOST} async def async_get_config_entry_diagnostics( diff --git a/tests/components/pyload/snapshots/test_diagnostics.ambr b/tests/components/pyload/snapshots/test_diagnostics.ambr index 0e078e000c9..e2b51ad184a 100644 --- a/tests/components/pyload/snapshots/test_diagnostics.ambr +++ b/tests/components/pyload/snapshots/test_diagnostics.ambr @@ -2,7 +2,7 @@ # name: test_diagnostics dict({ 'config_entry_data': dict({ - 'host': 'pyload.local', + 'host': '**REDACTED**', 'password': '**REDACTED**', 'port': 8000, 'ssl': True, From af9b4b98ca8a0615caf12962f33548bbec5163f5 Mon Sep 17 00:00:00 2001 From: "Mr. Bubbles" Date: Wed, 26 Jun 2024 15:09:42 +0200 Subject: [PATCH 1286/1445] Add value_fn to switch entity description in pyLoad (#120569) --- homeassistant/components/pyload/switch.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/pyload/switch.py b/homeassistant/components/pyload/switch.py index 4ed3e925488..21c8d75aaa0 100644 --- a/homeassistant/components/pyload/switch.py +++ b/homeassistant/components/pyload/switch.py @@ -18,6 +18,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import PyLoadConfigEntry +from .coordinator import pyLoadData from .entity import BasePyLoadEntity @@ -35,6 +36,7 @@ class PyLoadSwitchEntityDescription(SwitchEntityDescription): turn_on_fn: Callable[[PyLoadAPI], Awaitable[Any]] turn_off_fn: Callable[[PyLoadAPI], Awaitable[Any]] toggle_fn: Callable[[PyLoadAPI], Awaitable[Any]] + value_fn: Callable[[pyLoadData], bool] SENSOR_DESCRIPTIONS: tuple[PyLoadSwitchEntityDescription, ...] = ( @@ -45,6 +47,7 @@ SENSOR_DESCRIPTIONS: tuple[PyLoadSwitchEntityDescription, ...] = ( turn_on_fn=lambda api: api.unpause(), turn_off_fn=lambda api: api.pause(), toggle_fn=lambda api: api.toggle_pause(), + value_fn=lambda data: data.download, ), PyLoadSwitchEntityDescription( key=PyLoadSwitch.RECONNECT, @@ -53,6 +56,7 @@ SENSOR_DESCRIPTIONS: tuple[PyLoadSwitchEntityDescription, ...] = ( turn_on_fn=lambda api: api.toggle_reconnect(), turn_off_fn=lambda api: api.toggle_reconnect(), toggle_fn=lambda api: api.toggle_reconnect(), + value_fn=lambda data: data.reconnect, ), ) @@ -80,7 +84,9 @@ class PyLoadSwitchEntity(BasePyLoadEntity, SwitchEntity): @property def is_on(self) -> bool | None: """Return the state of the device.""" - return getattr(self.coordinator.data, self.entity_description.key) + return self.entity_description.value_fn( + self.coordinator.data, + ) async def async_turn_on(self, **kwargs: Any) -> None: """Turn the entity on.""" From 4defc4a58f605dd680bddc88c2552f54a89d0d78 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk <11290930+bouwew@users.noreply.github.com> Date: Wed, 26 Jun 2024 15:28:50 +0200 Subject: [PATCH 1287/1445] Implement a reboot-button for Plugwise (#120554) Co-authored-by: Franck Nijhof --- homeassistant/components/plugwise/button.py | 52 +++++++++++++++++++ homeassistant/components/plugwise/const.py | 3 ++ .../components/plugwise/coordinator.py | 14 ++--- .../components/plugwise/strings.json | 5 ++ tests/components/plugwise/test_button.py | 39 ++++++++++++++ tests/components/plugwise/test_init.py | 6 ++- 6 files changed, 111 insertions(+), 8 deletions(-) create mode 100644 homeassistant/components/plugwise/button.py create mode 100644 tests/components/plugwise/test_button.py diff --git a/homeassistant/components/plugwise/button.py b/homeassistant/components/plugwise/button.py new file mode 100644 index 00000000000..078d31bea12 --- /dev/null +++ b/homeassistant/components/plugwise/button.py @@ -0,0 +1,52 @@ +"""Plugwise Button component for Home Assistant.""" + +from __future__ import annotations + +from homeassistant.components.button import ButtonDeviceClass, ButtonEntity +from homeassistant.const import EntityCategory +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import PlugwiseConfigEntry +from .const import GATEWAY_ID, REBOOT +from .coordinator import PlugwiseDataUpdateCoordinator +from .entity import PlugwiseEntity +from .util import plugwise_command + + +async def async_setup_entry( + hass: HomeAssistant, + entry: PlugwiseConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Plugwise buttons from a ConfigEntry.""" + coordinator = entry.runtime_data + + gateway = coordinator.data.gateway + async_add_entities( + PlugwiseButtonEntity(coordinator, device_id) + for device_id in coordinator.data.devices + if device_id == gateway[GATEWAY_ID] and REBOOT in gateway + ) + + +class PlugwiseButtonEntity(PlugwiseEntity, ButtonEntity): + """Defines a Plugwise button.""" + + _attr_device_class = ButtonDeviceClass.RESTART + _attr_entity_category = EntityCategory.CONFIG + + def __init__( + self, + coordinator: PlugwiseDataUpdateCoordinator, + device_id: str, + ) -> None: + """Initialize the button.""" + super().__init__(coordinator, device_id) + self._attr_translation_key = REBOOT + self._attr_unique_id = f"{device_id}-reboot" + + @plugwise_command + async def async_press(self) -> None: + """Triggers the Plugwise button press service.""" + await self.coordinator.api.reboot_gateway() diff --git a/homeassistant/components/plugwise/const.py b/homeassistant/components/plugwise/const.py index 14599ce61fb..5e4dea5586b 100644 --- a/homeassistant/components/plugwise/const.py +++ b/homeassistant/components/plugwise/const.py @@ -17,14 +17,17 @@ FLOW_SMILE: Final = "smile (Adam/Anna/P1)" FLOW_STRETCH: Final = "stretch (Stretch)" FLOW_TYPE: Final = "flow_type" GATEWAY: Final = "gateway" +GATEWAY_ID: Final = "gateway_id" LOCATION: Final = "location" PW_TYPE: Final = "plugwise_type" +REBOOT: Final = "reboot" SMILE: Final = "smile" STRETCH: Final = "stretch" STRETCH_USERNAME: Final = "stretch" PLATFORMS: Final[list[str]] = [ Platform.BINARY_SENSOR, + Platform.BUTTON, Platform.CLIMATE, Platform.NUMBER, Platform.SELECT, diff --git a/homeassistant/components/plugwise/coordinator.py b/homeassistant/components/plugwise/coordinator.py index 1dff11d26d8..bc12ef4443b 100644 --- a/homeassistant/components/plugwise/coordinator.py +++ b/homeassistant/components/plugwise/coordinator.py @@ -7,6 +7,7 @@ from plugwise.exceptions import ( ConnectionFailedError, InvalidAuthentication, InvalidXMLError, + PlugwiseError, ResponseError, UnsupportedDeviceError, ) @@ -64,22 +65,23 @@ class PlugwiseDataUpdateCoordinator(DataUpdateCoordinator[PlugwiseData]): async def _async_update_data(self) -> PlugwiseData: """Fetch data from Plugwise.""" - + data = PlugwiseData({}, {}) try: if not self._connected: await self._connect() data = await self.api.async_update() + except ConnectionFailedError as err: + raise UpdateFailed("Failed to connect") from err except InvalidAuthentication as err: - raise ConfigEntryError("Invalid username or Smile ID") from err + raise ConfigEntryError("Authentication failed") from err except (InvalidXMLError, ResponseError) as err: raise UpdateFailed( - "Invalid XML data, or error indication received for the Plugwise" - " Adam/Smile/Stretch" + "Invalid XML data, or error indication received from the Plugwise Adam/Smile/Stretch" ) from err + except PlugwiseError as err: + raise UpdateFailed("Data incomplete or missing") from err except UnsupportedDeviceError as err: raise ConfigEntryError("Device with unsupported firmware") from err - except ConnectionFailedError as err: - raise UpdateFailed("Failed to connect to the Plugwise Smile") from err else: self.new_devices = set(data.devices) - self._current_devices self._current_devices = set(data.devices) diff --git a/homeassistant/components/plugwise/strings.json b/homeassistant/components/plugwise/strings.json index ef2d6458441..f74fc036e2a 100644 --- a/homeassistant/components/plugwise/strings.json +++ b/homeassistant/components/plugwise/strings.json @@ -55,6 +55,11 @@ "name": "Plugwise notification" } }, + "button": { + "reboot": { + "name": "Reboot" + } + }, "climate": { "plugwise": { "state_attributes": { diff --git a/tests/components/plugwise/test_button.py b/tests/components/plugwise/test_button.py new file mode 100644 index 00000000000..23003b3ffe6 --- /dev/null +++ b/tests/components/plugwise/test_button.py @@ -0,0 +1,39 @@ +"""Tests for Plugwise button entities.""" + +from unittest.mock import MagicMock + +from homeassistant.components.button import ( + DOMAIN as BUTTON_DOMAIN, + SERVICE_PRESS, + ButtonDeviceClass, +) +from homeassistant.const import ATTR_DEVICE_CLASS, ATTR_ENTITY_ID, STATE_UNKNOWN +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from tests.common import MockConfigEntry + + +async def test_adam_reboot_button( + hass: HomeAssistant, mock_smile_adam: MagicMock, init_integration: MockConfigEntry +) -> None: + """Test creation of button entities.""" + state = hass.states.get("button.adam_reboot") + assert state + assert state.state == STATE_UNKNOWN + assert state.attributes.get(ATTR_DEVICE_CLASS) == ButtonDeviceClass.RESTART + + registry = er.async_get(hass) + entry = registry.async_get("button.adam_reboot") + assert entry + assert entry.unique_id == "fe799307f1624099878210aa0b9f1475-reboot" + + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + {ATTR_ENTITY_ID: "button.adam_reboot"}, + blocking=True, + ) + + assert mock_smile_adam.reboot_gateway.call_count == 1 + mock_smile_adam.reboot_gateway.assert_called_with() diff --git a/tests/components/plugwise/test_init.py b/tests/components/plugwise/test_init.py index 9c709f1c4f6..d3f23a18285 100644 --- a/tests/components/plugwise/test_init.py +++ b/tests/components/plugwise/test_init.py @@ -7,6 +7,7 @@ from plugwise.exceptions import ( ConnectionFailedError, InvalidAuthentication, InvalidXMLError, + PlugwiseError, ResponseError, UnsupportedDeviceError, ) @@ -83,6 +84,7 @@ async def test_load_unload_config_entry( (ConnectionFailedError, ConfigEntryState.SETUP_RETRY), (InvalidAuthentication, ConfigEntryState.SETUP_ERROR), (InvalidXMLError, ConfigEntryState.SETUP_RETRY), + (PlugwiseError, ConfigEntryState.SETUP_RETRY), (ResponseError, ConfigEntryState.SETUP_RETRY), (UnsupportedDeviceError, ConfigEntryState.SETUP_ERROR), ], @@ -219,7 +221,7 @@ async def test_update_device( entity_registry, mock_config_entry.entry_id ) ) - == 28 + == 29 ) assert ( len( @@ -242,7 +244,7 @@ async def test_update_device( entity_registry, mock_config_entry.entry_id ) ) - == 33 + == 34 ) assert ( len( From d0f82d6f020627152db6f619f7702d158c0e578c Mon Sep 17 00:00:00 2001 From: Luke Lashley Date: Wed, 26 Jun 2024 09:40:19 -0400 Subject: [PATCH 1288/1445] Add support for Dyad vacuums to Roborock (#115331) --- homeassistant/components/roborock/__init__.py | 87 ++++- .../components/roborock/binary_sensor.py | 18 +- homeassistant/components/roborock/button.py | 20 +- .../components/roborock/coordinator.py | 60 ++- homeassistant/components/roborock/device.py | 67 +++- .../components/roborock/diagnostics.py | 6 +- homeassistant/components/roborock/image.py | 15 +- homeassistant/components/roborock/models.py | 15 + homeassistant/components/roborock/number.py | 13 +- homeassistant/components/roborock/select.py | 22 +- homeassistant/components/roborock/sensor.py | 111 +++++- .../components/roborock/strings.json | 48 +++ homeassistant/components/roborock/switch.py | 13 +- homeassistant/components/roborock/time.py | 13 +- homeassistant/components/roborock/vacuum.py | 19 +- tests/components/roborock/conftest.py | 45 ++- .../roborock/snapshots/test_diagnostics.ambr | 363 ++++++++++++++++++ tests/components/roborock/test_init.py | 48 ++- tests/components/roborock/test_sensor.py | 8 +- 19 files changed, 874 insertions(+), 117 deletions(-) diff --git a/homeassistant/components/roborock/__init__.py b/homeassistant/components/roborock/__init__.py index cdbddbda95b..310c5fee92b 100644 --- a/homeassistant/components/roborock/__init__.py +++ b/homeassistant/components/roborock/__init__.py @@ -4,6 +4,7 @@ from __future__ import annotations import asyncio from collections.abc import Coroutine +from dataclasses import dataclass from datetime import timedelta import logging from typing import Any @@ -12,6 +13,7 @@ from roborock import HomeDataRoom, RoborockException, RoborockInvalidCredentials from roborock.code_mappings import RoborockCategory from roborock.containers import DeviceData, HomeDataDevice, HomeDataProduct, UserData from roborock.version_1_apis.roborock_mqtt_client_v1 import RoborockMqttClientV1 +from roborock.version_a01_apis import RoborockMqttClientA01 from roborock.web_api import RoborockApiClient from homeassistant.config_entries import ConfigEntry @@ -20,13 +22,27 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from .const import CONF_BASE_URL, CONF_USER_DATA, DOMAIN, PLATFORMS -from .coordinator import RoborockDataUpdateCoordinator +from .coordinator import RoborockDataUpdateCoordinator, RoborockDataUpdateCoordinatorA01 SCAN_INTERVAL = timedelta(seconds=30) _LOGGER = logging.getLogger(__name__) +@dataclass +class RoborockCoordinators: + """Roborock coordinators type.""" + + v1: list[RoborockDataUpdateCoordinator] + a01: list[RoborockDataUpdateCoordinatorA01] + + def values( + self, + ) -> list[RoborockDataUpdateCoordinator | RoborockDataUpdateCoordinatorA01]: + """Return all coordinators.""" + return self.v1 + self.a01 + + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up roborock from a config entry.""" @@ -37,7 +53,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: api_client = RoborockApiClient(entry.data[CONF_USERNAME], entry.data[CONF_BASE_URL]) _LOGGER.debug("Getting home data") try: - home_data = await api_client.get_home_data(user_data) + home_data = await api_client.get_home_data_v2(user_data) except RoborockInvalidCredentials as err: raise ConfigEntryAuthFailed( "Invalid credentials", @@ -66,21 +82,24 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return_exceptions=True, ) # Valid coordinators are those where we had networking cached or we could get networking - valid_coordinators: list[RoborockDataUpdateCoordinator] = [ + v1_coords = [ coord for coord in coordinators if isinstance(coord, RoborockDataUpdateCoordinator) ] - if len(valid_coordinators) == 0: + a01_coords = [ + coord + for coord in coordinators + if isinstance(coord, RoborockDataUpdateCoordinatorA01) + ] + if len(v1_coords) + len(a01_coords) == 0: raise ConfigEntryNotReady( "No devices were able to successfully setup", translation_domain=DOMAIN, translation_key="no_coordinators", ) - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = { - coordinator.api.device_info.device.duid: coordinator - for coordinator in valid_coordinators - } + valid_coordinators = RoborockCoordinators(v1_coords, a01_coords) + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = valid_coordinators await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True @@ -92,14 +111,19 @@ def build_setup_functions( user_data: UserData, product_info: dict[str, HomeDataProduct], home_data_rooms: list[HomeDataRoom], -) -> list[Coroutine[Any, Any, RoborockDataUpdateCoordinator | None]]: +) -> list[ + Coroutine[ + Any, + Any, + RoborockDataUpdateCoordinator | RoborockDataUpdateCoordinatorA01 | None, + ] +]: """Create a list of setup functions that can later be called asynchronously.""" return [ setup_device( hass, user_data, device, product_info[device.product_id], home_data_rooms ) for device in device_map.values() - if product_info[device.product_id].category == RoborockCategory.VACUUM ] @@ -109,11 +133,33 @@ async def setup_device( device: HomeDataDevice, product_info: HomeDataProduct, home_data_rooms: list[HomeDataRoom], +) -> RoborockDataUpdateCoordinator | RoborockDataUpdateCoordinatorA01 | None: + """Set up a coordinator for a given device.""" + if device.pv == "1.0": + return await setup_device_v1( + hass, user_data, device, product_info, home_data_rooms + ) + if device.pv == "A01": + if product_info.category == RoborockCategory.WET_DRY_VAC: + return await setup_device_a01(hass, user_data, device, product_info) + _LOGGER.info( + "Not adding device %s because its protocol version %s or category %s is not supported", + device.duid, + device.pv, + product_info.category.name, + ) + return None + + +async def setup_device_v1( + hass: HomeAssistant, + user_data: UserData, + device: HomeDataDevice, + product_info: HomeDataProduct, + home_data_rooms: list[HomeDataRoom], ) -> RoborockDataUpdateCoordinator | None: """Set up a device Coordinator.""" - mqtt_client = RoborockMqttClientV1( - user_data, DeviceData(device, product_info.model) - ) + mqtt_client = RoborockMqttClientV1(user_data, DeviceData(device, product_info.name)) try: networking = await mqtt_client.get_networking() if networking is None: @@ -170,6 +216,21 @@ async def setup_device( return coordinator +async def setup_device_a01( + hass: HomeAssistant, + user_data: UserData, + device: HomeDataDevice, + product_info: HomeDataProduct, +) -> RoborockDataUpdateCoordinatorA01 | None: + """Set up a A01 protocol device.""" + mqtt_client = RoborockMqttClientA01( + user_data, DeviceData(device, product_info.name), product_info.category + ) + coord = RoborockDataUpdateCoordinatorA01(hass, device, product_info, mqtt_client) + await coord.async_config_entry_first_refresh() + return coord + + async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Handle removal of an entry.""" if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): diff --git a/homeassistant/components/roborock/binary_sensor.py b/homeassistant/components/roborock/binary_sensor.py index 00716207f7a..2fd6dd8d7d5 100644 --- a/homeassistant/components/roborock/binary_sensor.py +++ b/homeassistant/components/roborock/binary_sensor.py @@ -18,9 +18,10 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import slugify +from . import RoborockCoordinators from .const import DOMAIN from .coordinator import RoborockDataUpdateCoordinator -from .device import RoborockCoordinatedEntity +from .device import RoborockCoordinatedEntityV1 @dataclass(frozen=True, kw_only=True) @@ -75,34 +76,33 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Roborock vacuum binary sensors.""" - coordinators: dict[str, RoborockDataUpdateCoordinator] = hass.data[DOMAIN][ - config_entry.entry_id - ] + coordinators: RoborockCoordinators = hass.data[DOMAIN][config_entry.entry_id] async_add_entities( RoborockBinarySensorEntity( - f"{description.key}_{slugify(device_id)}", coordinator, description, ) - for device_id, coordinator in coordinators.items() + for coordinator in coordinators.v1 for description in BINARY_SENSOR_DESCRIPTIONS if description.value_fn(coordinator.roborock_device_info.props) is not None ) -class RoborockBinarySensorEntity(RoborockCoordinatedEntity, BinarySensorEntity): +class RoborockBinarySensorEntity(RoborockCoordinatedEntityV1, BinarySensorEntity): """Representation of a Roborock binary sensor.""" entity_description: RoborockBinarySensorDescription def __init__( self, - unique_id: str, coordinator: RoborockDataUpdateCoordinator, description: RoborockBinarySensorDescription, ) -> None: """Initialize the entity.""" - super().__init__(unique_id, coordinator) + super().__init__( + f"{description.key}_{slugify(coordinator.duid)}", + coordinator, + ) self.entity_description = description @property diff --git a/homeassistant/components/roborock/button.py b/homeassistant/components/roborock/button.py index fe6dfabb56c..445033a0f6d 100644 --- a/homeassistant/components/roborock/button.py +++ b/homeassistant/components/roborock/button.py @@ -13,9 +13,10 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import slugify +from . import RoborockCoordinators from .const import DOMAIN from .coordinator import RoborockDataUpdateCoordinator -from .device import RoborockEntity +from .device import RoborockEntityV1 @dataclass(frozen=True, kw_only=True) @@ -68,33 +69,34 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up Roborock button platform.""" - coordinators: dict[str, RoborockDataUpdateCoordinator] = hass.data[DOMAIN][ - config_entry.entry_id - ] + coordinators: RoborockCoordinators = hass.data[DOMAIN][config_entry.entry_id] async_add_entities( RoborockButtonEntity( - f"{description.key}_{slugify(device_id)}", coordinator, description, ) - for device_id, coordinator in coordinators.items() + for coordinator in coordinators.v1 for description in CONSUMABLE_BUTTON_DESCRIPTIONS + if isinstance(coordinator, RoborockDataUpdateCoordinator) ) -class RoborockButtonEntity(RoborockEntity, ButtonEntity): +class RoborockButtonEntity(RoborockEntityV1, ButtonEntity): """A class to define Roborock button entities.""" entity_description: RoborockButtonDescription def __init__( self, - unique_id: str, coordinator: RoborockDataUpdateCoordinator, entity_description: RoborockButtonDescription, ) -> None: """Create a button entity.""" - super().__init__(unique_id, coordinator.device_info, coordinator.api) + super().__init__( + f"{entity_description.key}_{slugify(coordinator.duid)}", + coordinator.device_info, + coordinator.api, + ) self.entity_description = entity_description async def async_press(self) -> None: diff --git a/homeassistant/components/roborock/coordinator.py b/homeassistant/components/roborock/coordinator.py index 32b7a487ac8..430e2815a7b 100644 --- a/homeassistant/components/roborock/coordinator.py +++ b/homeassistant/components/roborock/coordinator.py @@ -9,18 +9,21 @@ import logging from roborock import HomeDataRoom from roborock.containers import DeviceData, HomeDataDevice, HomeDataProduct, NetworkInfo from roborock.exceptions import RoborockException +from roborock.roborock_message import RoborockDyadDataProtocol, RoborockZeoProtocol from roborock.roborock_typing import DeviceProp from roborock.version_1_apis.roborock_local_client_v1 import RoborockLocalClientV1 from roborock.version_1_apis.roborock_mqtt_client_v1 import RoborockMqttClientV1 +from roborock.version_a01_apis import RoborockClientA01 from homeassistant.const import ATTR_CONNECTIONS from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import DOMAIN -from .models import RoborockHassDeviceInfo, RoborockMapInfo +from .models import RoborockA01HassDeviceInfo, RoborockHassDeviceInfo, RoborockMapInfo SCAN_INTERVAL = timedelta(seconds=30) @@ -77,6 +80,7 @@ class RoborockDataUpdateCoordinator(DataUpdateCoordinator[DeviceProp]): "Using the cloud API for device %s. This is not recommended as it can lead to rate limiting. We recommend making your vacuum accessible by your Home Assistant instance", self.roborock_device_info.device.duid, ) + await self.api.async_disconnect() # We use the cloud api if the local api fails to connect. self.api = self.cloud_api # Right now this should never be called if the cloud api is the primary api, @@ -137,3 +141,57 @@ class RoborockDataUpdateCoordinator(DataUpdateCoordinator[DeviceProp]): self.maps[self.current_map].rooms[room.segment_id] = ( self._home_data_rooms.get(room.iot_id, "Unknown") ) + + @property + def duid(self) -> str: + """Get the unique id of the device as specified by Roborock.""" + return self.roborock_device_info.device.duid + + +class RoborockDataUpdateCoordinatorA01( + DataUpdateCoordinator[ + dict[RoborockDyadDataProtocol | RoborockZeoProtocol, StateType] + ] +): + """Class to manage fetching data from the API for A01 devices.""" + + def __init__( + self, + hass: HomeAssistant, + device: HomeDataDevice, + product_info: HomeDataProduct, + api: RoborockClientA01, + ) -> None: + """Initialize.""" + super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=SCAN_INTERVAL) + self.api = api + self.device_info = DeviceInfo( + name=device.name, + identifiers={(DOMAIN, device.duid)}, + manufacturer="Roborock", + model=product_info.model, + sw_version=device.fv, + ) + self.request_protocols: list[RoborockDyadDataProtocol | RoborockZeoProtocol] = [ + RoborockDyadDataProtocol.STATUS, + RoborockDyadDataProtocol.POWER, + RoborockDyadDataProtocol.MESH_LEFT, + RoborockDyadDataProtocol.BRUSH_LEFT, + RoborockDyadDataProtocol.ERROR, + RoborockDyadDataProtocol.TOTAL_RUN_TIME, + ] + self.roborock_device_info = RoborockA01HassDeviceInfo(device, product_info) + + async def _async_update_data( + self, + ) -> dict[RoborockDyadDataProtocol | RoborockZeoProtocol, StateType]: + return await self.api.update_values(self.request_protocols) + + async def release(self) -> None: + """Disconnect from API.""" + await self.api.async_release() + + @property + def duid(self) -> str: + """Get the unique id of the device as specified by Roborock.""" + return self.roborock_device_info.device.duid diff --git a/homeassistant/components/roborock/device.py b/homeassistant/components/roborock/device.py index 6450d849859..4a16ada5967 100644 --- a/homeassistant/components/roborock/device.py +++ b/homeassistant/components/roborock/device.py @@ -2,6 +2,7 @@ from typing import Any +from roborock.api import RoborockClient from roborock.command_cache import CacheableAttribute from roborock.containers import Consumable, Status from roborock.exceptions import RoborockException @@ -9,6 +10,7 @@ from roborock.roborock_message import RoborockDataProtocol from roborock.roborock_typing import RoborockCommand from roborock.version_1_apis.roborock_client_v1 import AttributeCache, RoborockClientV1 from roborock.version_1_apis.roborock_mqtt_client_v1 import RoborockMqttClientV1 +from roborock.version_a01_apis import RoborockClientA01 from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.device_registry import DeviceInfo @@ -16,7 +18,7 @@ from homeassistant.helpers.entity import Entity from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN -from .coordinator import RoborockDataUpdateCoordinator +from .coordinator import RoborockDataUpdateCoordinator, RoborockDataUpdateCoordinatorA01 class RoborockEntity(Entity): @@ -28,17 +30,24 @@ class RoborockEntity(Entity): self, unique_id: str, device_info: DeviceInfo, - api: RoborockClientV1, + api: RoborockClient, ) -> None: - """Initialize the coordinated Roborock Device.""" + """Initialize the Roborock Device.""" self._attr_unique_id = unique_id self._attr_device_info = device_info self._api = api - @property - def api(self) -> RoborockClientV1: - """Returns the api.""" - return self._api + +class RoborockEntityV1(RoborockEntity): + """Representation of a base Roborock V1 Entity.""" + + _api: RoborockClientV1 + + def __init__( + self, unique_id: str, device_info: DeviceInfo, api: RoborockClientV1 + ) -> None: + """Initialize the Roborock Device.""" + super().__init__(unique_id, device_info, api) def get_cache(self, attribute: CacheableAttribute) -> AttributeCache: """Get an item from the api cache.""" @@ -66,9 +75,26 @@ class RoborockEntity(Entity): ) from err return response + @property + def api(self) -> RoborockClientV1: + """Returns the api.""" + return self._api -class RoborockCoordinatedEntity( - RoborockEntity, CoordinatorEntity[RoborockDataUpdateCoordinator] + +class RoborockEntityA01(RoborockEntity): + """Representation of a base Roborock Entity for A01 devices.""" + + _api: RoborockClientA01 + + def __init__( + self, unique_id: str, device_info: DeviceInfo, api: RoborockClientA01 + ) -> None: + """Initialize the Roborock Device.""" + super().__init__(unique_id, device_info, api) + + +class RoborockCoordinatedEntityV1( + RoborockEntityV1, CoordinatorEntity[RoborockDataUpdateCoordinator] ): """Representation of a base a coordinated Roborock Entity.""" @@ -83,7 +109,7 @@ class RoborockCoordinatedEntity( | None = None, ) -> None: """Initialize the coordinated Roborock Device.""" - RoborockEntity.__init__( + RoborockEntityV1.__init__( self, unique_id=unique_id, device_info=coordinator.device_info, @@ -138,3 +164,24 @@ class RoborockCoordinatedEntity( self.coordinator.roborock_device_info.props.consumable = value self.coordinator.data = self.coordinator.roborock_device_info.props self.schedule_update_ha_state() + + +class RoborockCoordinatedEntityA01( + RoborockEntityA01, CoordinatorEntity[RoborockDataUpdateCoordinatorA01] +): + """Representation of a base a coordinated Roborock Entity.""" + + def __init__( + self, + unique_id: str, + coordinator: RoborockDataUpdateCoordinatorA01, + ) -> None: + """Initialize the coordinated Roborock Device.""" + RoborockEntityA01.__init__( + self, + unique_id=unique_id, + device_info=coordinator.device_info, + api=coordinator.api, + ) + CoordinatorEntity.__init__(self, coordinator=coordinator) + self._attr_unique_id = unique_id diff --git a/homeassistant/components/roborock/diagnostics.py b/homeassistant/components/roborock/diagnostics.py index 79a9f0bafed..9be8b6f4d63 100644 --- a/homeassistant/components/roborock/diagnostics.py +++ b/homeassistant/components/roborock/diagnostics.py @@ -9,8 +9,8 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_UNIQUE_ID from homeassistant.core import HomeAssistant +from . import RoborockCoordinators from .const import DOMAIN -from .coordinator import RoborockDataUpdateCoordinator TO_REDACT_CONFIG = ["token", "sn", "rruid", CONF_UNIQUE_ID, "username", "uid"] @@ -21,9 +21,7 @@ async def async_get_config_entry_diagnostics( hass: HomeAssistant, config_entry: ConfigEntry ) -> dict[str, Any]: """Return diagnostics for a config entry.""" - coordinators: dict[str, RoborockDataUpdateCoordinator] = hass.data[DOMAIN][ - config_entry.entry_id - ] + coordinators: RoborockCoordinators = hass.data[DOMAIN][config_entry.entry_id] return { "config_entry": async_redact_data(config_entry.data, TO_REDACT_CONFIG), diff --git a/homeassistant/components/roborock/image.py b/homeassistant/components/roborock/image.py index 9dfe8d53cd3..d1731d289db 100644 --- a/homeassistant/components/roborock/image.py +++ b/homeassistant/components/roborock/image.py @@ -21,9 +21,10 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import slugify import homeassistant.util.dt as dt_util +from . import RoborockCoordinators from .const import DEFAULT_DRAWABLES, DOMAIN, DRAWABLES, IMAGE_CACHE_INTERVAL, MAP_SLEEP from .coordinator import RoborockDataUpdateCoordinator -from .device import RoborockCoordinatedEntity +from .device import RoborockCoordinatedEntityV1 async def async_setup_entry( @@ -33,9 +34,7 @@ async def async_setup_entry( ) -> None: """Set up Roborock image platform.""" - coordinators: dict[str, RoborockDataUpdateCoordinator] = hass.data[DOMAIN][ - config_entry.entry_id - ] + coordinators: RoborockCoordinators = hass.data[DOMAIN][config_entry.entry_id] drawables = [ drawable for drawable, default_value in DEFAULT_DRAWABLES.items() @@ -46,7 +45,7 @@ async def async_setup_entry( await asyncio.gather( *( create_coordinator_maps(coord, drawables) - for coord in coordinators.values() + for coord in coordinators.v1 ) ) ) @@ -54,7 +53,7 @@ async def async_setup_entry( async_add_entities(entities) -class RoborockMap(RoborockCoordinatedEntity, ImageEntity): +class RoborockMap(RoborockCoordinatedEntityV1, ImageEntity): """A class to let you visualize the map.""" _attr_has_entity_name = True @@ -70,7 +69,7 @@ class RoborockMap(RoborockCoordinatedEntity, ImageEntity): drawables: list[Drawable], ) -> None: """Initialize a Roborock map.""" - RoborockCoordinatedEntity.__init__(self, unique_id, coordinator) + RoborockCoordinatedEntityV1.__init__(self, unique_id, coordinator) ImageEntity.__init__(self, coordinator.hass) self._attr_name = map_name self.parser = RoborockMapDataParser( @@ -184,7 +183,7 @@ async def create_coordinator_maps( api_data: bytes = map_update[0] if isinstance(map_update[0], bytes) else b"" entities.append( RoborockMap( - f"{slugify(coord.roborock_device_info.device.duid)}_map_{map_info.name}", + f"{slugify(coord.duid)}_map_{map_info.name}", coord, map_flag, api_data, diff --git a/homeassistant/components/roborock/models.py b/homeassistant/components/roborock/models.py index b516c0ee05c..4b8ab43b4a1 100644 --- a/homeassistant/components/roborock/models.py +++ b/homeassistant/components/roborock/models.py @@ -26,6 +26,21 @@ class RoborockHassDeviceInfo: } +@dataclass +class RoborockA01HassDeviceInfo: + """A model to describe A01 roborock devices.""" + + device: HomeDataDevice + product: HomeDataProduct + + def as_dict(self) -> dict[str, dict[str, Any]]: + """Turn RoborockA01HassDeviceInfo into a dictionary.""" + return { + "device": self.device.as_dict(), + "product": self.product.as_dict(), + } + + @dataclass class RoborockMapInfo: """A model to describe all information about a map we may want.""" diff --git a/homeassistant/components/roborock/number.py b/homeassistant/components/roborock/number.py index a432c527b0e..5e776d40f2d 100644 --- a/homeassistant/components/roborock/number.py +++ b/homeassistant/components/roborock/number.py @@ -17,9 +17,10 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import slugify +from . import RoborockCoordinators from .const import DOMAIN from .coordinator import RoborockDataUpdateCoordinator -from .device import RoborockEntity +from .device import RoborockEntityV1 _LOGGER = logging.getLogger(__name__) @@ -54,14 +55,12 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up Roborock number platform.""" - coordinators: dict[str, RoborockDataUpdateCoordinator] = hass.data[DOMAIN][ - config_entry.entry_id - ] + coordinators: RoborockCoordinators = hass.data[DOMAIN][config_entry.entry_id] possible_entities: list[ tuple[RoborockDataUpdateCoordinator, RoborockNumberDescription] ] = [ (coordinator, description) - for coordinator in coordinators.values() + for coordinator in coordinators.v1 for description in NUMBER_DESCRIPTIONS ] # We need to check if this function is supported by the device. @@ -81,7 +80,7 @@ async def async_setup_entry( else: valid_entities.append( RoborockNumberEntity( - f"{description.key}_{slugify(coordinator.roborock_device_info.device.duid)}", + f"{description.key}_{slugify(coordinator.duid)}", coordinator, description, ) @@ -89,7 +88,7 @@ async def async_setup_entry( async_add_entities(valid_entities) -class RoborockNumberEntity(RoborockEntity, NumberEntity): +class RoborockNumberEntity(RoborockEntityV1, NumberEntity): """A class to let you set options on a Roborock vacuum where the potential options are fixed.""" entity_description: RoborockNumberDescription diff --git a/homeassistant/components/roborock/select.py b/homeassistant/components/roborock/select.py index fa7f4250804..c6073645086 100644 --- a/homeassistant/components/roborock/select.py +++ b/homeassistant/components/roborock/select.py @@ -14,9 +14,10 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import slugify +from . import RoborockCoordinators from .const import DOMAIN from .coordinator import RoborockDataUpdateCoordinator -from .device import RoborockCoordinatedEntity +from .device import RoborockCoordinatedEntityV1 @dataclass(frozen=True, kw_only=True) @@ -69,14 +70,10 @@ async def async_setup_entry( ) -> None: """Set up Roborock select platform.""" - coordinators: dict[str, RoborockDataUpdateCoordinator] = hass.data[DOMAIN][ - config_entry.entry_id - ] + coordinators: RoborockCoordinators = hass.data[DOMAIN][config_entry.entry_id] async_add_entities( - RoborockSelectEntity( - f"{description.key}_{slugify(device_id)}", coordinator, description, options - ) - for device_id, coordinator in coordinators.items() + RoborockSelectEntity(coordinator, description, options) + for coordinator in coordinators.v1 for description in SELECT_DESCRIPTIONS if ( options := description.options_lambda( @@ -87,21 +84,24 @@ async def async_setup_entry( ) -class RoborockSelectEntity(RoborockCoordinatedEntity, SelectEntity): +class RoborockSelectEntity(RoborockCoordinatedEntityV1, SelectEntity): """A class to let you set options on a Roborock vacuum where the potential options are fixed.""" entity_description: RoborockSelectDescription def __init__( self, - unique_id: str, coordinator: RoborockDataUpdateCoordinator, entity_description: RoborockSelectDescription, options: list[str], ) -> None: """Create a select entity.""" self.entity_description = entity_description - super().__init__(unique_id, coordinator, entity_description.protocol_listener) + super().__init__( + f"{entity_description.key}_{slugify(coordinator.duid)}", + coordinator, + entity_description.protocol_listener, + ) self._attr_options = options async def async_select_option(self, option: str) -> None: diff --git a/homeassistant/components/roborock/sensor.py b/homeassistant/components/roborock/sensor.py index acee1688cc7..3be7461d149 100644 --- a/homeassistant/components/roborock/sensor.py +++ b/homeassistant/components/roborock/sensor.py @@ -6,13 +6,14 @@ from collections.abc import Callable from dataclasses import dataclass import datetime +from roborock.code_mappings import DyadError, RoborockDyadStateCode from roborock.containers import ( RoborockDockErrorCode, RoborockDockTypeCode, RoborockErrorCode, RoborockStateCode, ) -from roborock.roborock_message import RoborockDataProtocol +from roborock.roborock_message import RoborockDataProtocol, RoborockDyadDataProtocol from roborock.roborock_typing import DeviceProp from homeassistant.components.sensor import ( @@ -32,9 +33,10 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from homeassistant.util import slugify +from . import RoborockCoordinators from .const import DOMAIN -from .coordinator import RoborockDataUpdateCoordinator -from .device import RoborockCoordinatedEntity +from .coordinator import RoborockDataUpdateCoordinator, RoborockDataUpdateCoordinatorA01 +from .device import RoborockCoordinatedEntityA01, RoborockCoordinatedEntityV1 @dataclass(frozen=True, kw_only=True) @@ -46,6 +48,13 @@ class RoborockSensorDescription(SensorEntityDescription): protocol_listener: RoborockDataProtocol | None = None +@dataclass(frozen=True, kw_only=True) +class RoborockSensorDescriptionA01(SensorEntityDescription): + """A class that describes Roborock sensors.""" + + data_protocol: RoborockDyadDataProtocol + + def _dock_error_value_fn(properties: DeviceProp) -> str | None: if ( status := properties.status.dock_error_status @@ -193,41 +202,101 @@ SENSOR_DESCRIPTIONS = [ ] +A01_SENSOR_DESCRIPTIONS: list[RoborockSensorDescriptionA01] = [ + RoborockSensorDescriptionA01( + key="status", + data_protocol=RoborockDyadDataProtocol.STATUS, + translation_key="a01_status", + entity_category=EntityCategory.DIAGNOSTIC, + device_class=SensorDeviceClass.ENUM, + options=RoborockDyadStateCode.keys(), + ), + RoborockSensorDescriptionA01( + key="battery", + data_protocol=RoborockDyadDataProtocol.POWER, + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=PERCENTAGE, + device_class=SensorDeviceClass.BATTERY, + ), + RoborockSensorDescriptionA01( + key="filter_time_left", + data_protocol=RoborockDyadDataProtocol.MESH_LEFT, + native_unit_of_measurement=UnitOfTime.SECONDS, + device_class=SensorDeviceClass.DURATION, + translation_key="filter_time_left", + entity_category=EntityCategory.DIAGNOSTIC, + ), + RoborockSensorDescriptionA01( + key="brush_remaining", + data_protocol=RoborockDyadDataProtocol.BRUSH_LEFT, + native_unit_of_measurement=UnitOfTime.SECONDS, + device_class=SensorDeviceClass.DURATION, + translation_key="brush_remaining", + entity_category=EntityCategory.DIAGNOSTIC, + ), + RoborockSensorDescriptionA01( + key="error", + data_protocol=RoborockDyadDataProtocol.ERROR, + device_class=SensorDeviceClass.ENUM, + translation_key="a01_error", + entity_category=EntityCategory.DIAGNOSTIC, + options=DyadError.keys(), + ), + RoborockSensorDescriptionA01( + key="total_cleaning_time", + native_unit_of_measurement=UnitOfTime.MINUTES, + data_protocol=RoborockDyadDataProtocol.TOTAL_RUN_TIME, + device_class=SensorDeviceClass.DURATION, + translation_key="total_cleaning_time", + entity_category=EntityCategory.DIAGNOSTIC, + ), +] + + async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Roborock vacuum sensors.""" - coordinators: dict[str, RoborockDataUpdateCoordinator] = hass.data[DOMAIN][ - config_entry.entry_id - ] + coordinators: RoborockCoordinators = hass.data[DOMAIN][config_entry.entry_id] async_add_entities( RoborockSensorEntity( - f"{description.key}_{slugify(device_id)}", coordinator, description, ) - for device_id, coordinator in coordinators.items() + for coordinator in coordinators.v1 for description in SENSOR_DESCRIPTIONS if description.value_fn(coordinator.roborock_device_info.props) is not None ) + async_add_entities( + RoborockSensorEntityA01( + coordinator, + description, + ) + for coordinator in coordinators.a01 + for description in A01_SENSOR_DESCRIPTIONS + if description.data_protocol in coordinator.data + ) -class RoborockSensorEntity(RoborockCoordinatedEntity, SensorEntity): +class RoborockSensorEntity(RoborockCoordinatedEntityV1, SensorEntity): """Representation of a Roborock sensor.""" entity_description: RoborockSensorDescription def __init__( self, - unique_id: str, coordinator: RoborockDataUpdateCoordinator, description: RoborockSensorDescription, ) -> None: """Initialize the entity.""" self.entity_description = description - super().__init__(unique_id, coordinator, description.protocol_listener) + super().__init__( + f"{description.key}_{slugify(coordinator.duid)}", + coordinator, + description.protocol_listener, + ) @property def native_value(self) -> StateType | datetime.datetime: @@ -235,3 +304,23 @@ class RoborockSensorEntity(RoborockCoordinatedEntity, SensorEntity): return self.entity_description.value_fn( self.coordinator.roborock_device_info.props ) + + +class RoborockSensorEntityA01(RoborockCoordinatedEntityA01, SensorEntity): + """Representation of a A01 Roborock sensor.""" + + entity_description: RoborockSensorDescriptionA01 + + def __init__( + self, + coordinator: RoborockDataUpdateCoordinatorA01, + description: RoborockSensorDescriptionA01, + ) -> None: + """Initialize the entity.""" + self.entity_description = description + super().__init__(f"{description.key}_{slugify(coordinator.duid)}", coordinator) + + @property + def native_value(self) -> StateType: + """Return the value reported by the sensor.""" + return self.coordinator.data[self.entity_description.data_protocol] diff --git a/homeassistant/components/roborock/strings.json b/homeassistant/components/roborock/strings.json index aaf476d7fc6..c7fc34386fd 100644 --- a/homeassistant/components/roborock/strings.json +++ b/homeassistant/components/roborock/strings.json @@ -95,6 +95,54 @@ } }, "sensor": { + "a01_error": { + "name": "Error", + "state": { + "none": "[%key:component::roborock::entity::sensor::vacuum_error::state::none%]", + "dirty_tank_full": "Dirty tank full", + "water_level_sensor_stuck": "Water level sensor stuck.", + "clean_tank_empty": "Clean tank empty", + "clean_head_entangled": "Cleaning head entangled", + "clean_head_too_hot": "Cleaning head too hot.", + "fan_protection_e5": "Fan protection", + "cleaning_head_blocked": "Cleaning head blocked", + "temperature_protection": "Temperature protection", + "fan_protection_e4": "[%key:component::roborock::entity::sensor::a01_error::state::fan_protection_e5%]", + "fan_protection_e9": "[%key:component::roborock::entity::sensor::a01_error::state::fan_protection_e5%]", + "battery_temperature_protection_e0": "[%key:component::roborock::entity::sensor::a01_error::state::temperature_protection%]", + "battery_temperature_protection": "Battery temperature protection", + "battery_temperature_protection_2": "[%key:component::roborock::entity::sensor::a01_error::state::battery_temperature_protection%]", + "power_adapter_error": "Power adapter error", + "dirty_charging_contacts": "Clean charging contacts", + "low_battery": "[%key:component::roborock::entity::sensor::vacuum_error::state::low_battery%]", + "battery_under_10": "Battery under 10%" + } + }, + "a01_status": { + "name": "Status", + "state": { + "unknown": "[%key:component::roborock::entity::sensor::status::state::unknown%]", + "fetching": "Fetching", + "fetch_failed": "Fetch failed", + "updating": "[%key:component::roborock::entity::sensor::status::state::updating%]", + "washing": "Washing", + "ready": "Ready", + "charging": "[%key:component::roborock::entity::sensor::status::state::charging%]", + "mop_washing": "Washing mop", + "self_clean_cleaning": "Self clean cleaning", + "self_clean_deep_cleaning": "Self clean deep cleaning", + "self_clean_rinsing": "Self clean rinsing", + "self_clean_dehydrating": "Self clean drying", + "drying": "Drying", + "ventilating": "Ventilating", + "reserving": "Reserving", + "mop_washing_paused": "Mop washing paused", + "dusting_mode": "Dusting mode" + } + }, + "brush_remaining": { + "name": "Roller left" + }, "cleaning_area": { "name": "Cleaning area" }, diff --git a/homeassistant/components/roborock/switch.py b/homeassistant/components/roborock/switch.py index 9a34060fe96..cdfc0c2dc96 100644 --- a/homeassistant/components/roborock/switch.py +++ b/homeassistant/components/roborock/switch.py @@ -18,9 +18,10 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import slugify +from . import RoborockCoordinators from .const import DOMAIN from .coordinator import RoborockDataUpdateCoordinator -from .device import RoborockEntity +from .device import RoborockEntityV1 _LOGGER = logging.getLogger(__name__) @@ -102,14 +103,12 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up Roborock switch platform.""" - coordinators: dict[str, RoborockDataUpdateCoordinator] = hass.data[DOMAIN][ - config_entry.entry_id - ] + coordinators: RoborockCoordinators = hass.data[DOMAIN][config_entry.entry_id] possible_entities: list[ tuple[RoborockDataUpdateCoordinator, RoborockSwitchDescription] ] = [ (coordinator, description) - for coordinator in coordinators.values() + for coordinator in coordinators.v1 for description in SWITCH_DESCRIPTIONS ] # We need to check if this function is supported by the device. @@ -129,7 +128,7 @@ async def async_setup_entry( else: valid_entities.append( RoborockSwitch( - f"{description.key}_{slugify(coordinator.roborock_device_info.device.duid)}", + f"{description.key}_{slugify(coordinator.duid)}", coordinator, description, ) @@ -137,7 +136,7 @@ async def async_setup_entry( async_add_entities(valid_entities) -class RoborockSwitch(RoborockEntity, SwitchEntity): +class RoborockSwitch(RoborockEntityV1, SwitchEntity): """A class to let you turn functionality on Roborock devices on and off that does need a coordinator.""" entity_description: RoborockSwitchDescription diff --git a/homeassistant/components/roborock/time.py b/homeassistant/components/roborock/time.py index 6ccc2ef0b27..21ab26c0013 100644 --- a/homeassistant/components/roborock/time.py +++ b/homeassistant/components/roborock/time.py @@ -19,9 +19,10 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import slugify +from . import RoborockCoordinators from .const import DOMAIN from .coordinator import RoborockDataUpdateCoordinator -from .device import RoborockEntity +from .device import RoborockEntityV1 _LOGGER = logging.getLogger(__name__) @@ -118,14 +119,12 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up Roborock time platform.""" - coordinators: dict[str, RoborockDataUpdateCoordinator] = hass.data[DOMAIN][ - config_entry.entry_id - ] + coordinators: RoborockCoordinators = hass.data[DOMAIN][config_entry.entry_id] possible_entities: list[ tuple[RoborockDataUpdateCoordinator, RoborockTimeDescription] ] = [ (coordinator, description) - for coordinator in coordinators.values() + for coordinator in coordinators.v1 for description in TIME_DESCRIPTIONS ] # We need to check if this function is supported by the device. @@ -145,7 +144,7 @@ async def async_setup_entry( else: valid_entities.append( RoborockTimeEntity( - f"{description.key}_{slugify(coordinator.roborock_device_info.device.duid)}", + f"{description.key}_{slugify(coordinator.duid)}", coordinator, description, ) @@ -153,7 +152,7 @@ async def async_setup_entry( async_add_entities(valid_entities) -class RoborockTimeEntity(RoborockEntity, TimeEntity): +class RoborockTimeEntity(RoborockEntityV1, TimeEntity): """A class to let you set options on a Roborock vacuum where the potential options are fixed.""" entity_description: RoborockTimeDescription diff --git a/homeassistant/components/roborock/vacuum.py b/homeassistant/components/roborock/vacuum.py index 16cf518aa02..cefcc85d7f8 100644 --- a/homeassistant/components/roborock/vacuum.py +++ b/homeassistant/components/roborock/vacuum.py @@ -23,9 +23,10 @@ from homeassistant.helpers import entity_platform from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import slugify +from . import RoborockCoordinators from .const import DOMAIN, GET_MAPS_SERVICE_NAME from .coordinator import RoborockDataUpdateCoordinator -from .device import RoborockCoordinatedEntity +from .device import RoborockCoordinatedEntityV1 STATE_CODE_TO_STATE = { RoborockStateCode.starting: STATE_IDLE, # "Starting" @@ -60,12 +61,11 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Roborock sensor.""" - coordinators: dict[str, RoborockDataUpdateCoordinator] = hass.data[DOMAIN][ - config_entry.entry_id - ] + coordinators: RoborockCoordinators = hass.data[DOMAIN][config_entry.entry_id] async_add_entities( - RoborockVacuum(slugify(device_id), coordinator) - for device_id, coordinator in coordinators.items() + RoborockVacuum(coordinator) + for coordinator in coordinators.v1 + if isinstance(coordinator, RoborockDataUpdateCoordinator) ) platform = entity_platform.async_get_current_platform() @@ -78,7 +78,7 @@ async def async_setup_entry( ) -class RoborockVacuum(RoborockCoordinatedEntity, StateVacuumEntity): +class RoborockVacuum(RoborockCoordinatedEntityV1, StateVacuumEntity): """General Representation of a Roborock vacuum.""" _attr_icon = "mdi:robot-vacuum" @@ -99,14 +99,13 @@ class RoborockVacuum(RoborockCoordinatedEntity, StateVacuumEntity): def __init__( self, - unique_id: str, coordinator: RoborockDataUpdateCoordinator, ) -> None: """Initialize a vacuum.""" StateVacuumEntity.__init__(self) - RoborockCoordinatedEntity.__init__( + RoborockCoordinatedEntityV1.__init__( self, - unique_id, + slugify(coordinator.duid), coordinator, listener_request=[ RoborockDataProtocol.FAN_POWER, diff --git a/tests/components/roborock/conftest.py b/tests/components/roborock/conftest.py index d3bb0a221b1..a7ebbf10af3 100644 --- a/tests/components/roborock/conftest.py +++ b/tests/components/roborock/conftest.py @@ -1,9 +1,13 @@ """Global fixtures for Roborock integration.""" +from copy import deepcopy from unittest.mock import patch import pytest from roborock import RoomMapping +from roborock.code_mappings import DyadError, RoborockDyadStateCode +from roborock.roborock_message import RoborockDyadDataProtocol, RoborockZeoProtocol +from roborock.version_a01_apis import RoborockMqttClientA01 from homeassistant.components.roborock.const import ( CONF_BASE_URL, @@ -28,6 +32,28 @@ from .mock_data import ( from tests.common import MockConfigEntry +class A01Mock(RoborockMqttClientA01): + """A class to mock the A01 client.""" + + def __init__(self, user_data, device_info, category) -> None: + """Initialize the A01Mock.""" + super().__init__(user_data, device_info, category) + self.protocol_responses = { + RoborockDyadDataProtocol.STATUS: RoborockDyadStateCode.drying.name, + RoborockDyadDataProtocol.POWER: 100, + RoborockDyadDataProtocol.MESH_LEFT: 111, + RoborockDyadDataProtocol.BRUSH_LEFT: 222, + RoborockDyadDataProtocol.ERROR: DyadError.none.name, + RoborockDyadDataProtocol.TOTAL_RUN_TIME: 213, + } + + async def update_values( + self, dyad_data_protocols: list[RoborockDyadDataProtocol | RoborockZeoProtocol] + ): + """Update values with a predetermined response that can be overridden.""" + return {prot: self.protocol_responses[prot] for prot in dyad_data_protocols} + + @pytest.fixture(name="bypass_api_fixture") def bypass_api_fixture() -> None: """Skip calls to the API.""" @@ -35,7 +61,7 @@ def bypass_api_fixture() -> None: patch("homeassistant.components.roborock.RoborockMqttClientV1.async_connect"), patch("homeassistant.components.roborock.RoborockMqttClientV1._send_command"), patch( - "homeassistant.components.roborock.RoborockApiClient.get_home_data", + "homeassistant.components.roborock.RoborockApiClient.get_home_data_v2", return_value=HOME_DATA, ), patch( @@ -95,6 +121,23 @@ def bypass_api_fixture() -> None: "homeassistant.components.roborock.coordinator.RoborockMqttClientV1.get_map_v1", return_value=b"123", ), + patch( + "homeassistant.components.roborock.coordinator.RoborockClientA01", + A01Mock, + ), + patch("homeassistant.components.roborock.RoborockMqttClientA01", A01Mock), + ): + yield + + +@pytest.fixture +def bypass_api_fixture_v1_only(bypass_api_fixture) -> None: + """Bypass api for tests that require only having v1 devices.""" + home_data_copy = deepcopy(HOME_DATA) + home_data_copy.received_devices = [] + with patch( + "homeassistant.components.roborock.RoborockApiClient.get_home_data_v2", + return_value=home_data_copy, ): yield diff --git a/tests/components/roborock/snapshots/test_diagnostics.ambr b/tests/components/roborock/snapshots/test_diagnostics.ambr index 3d78e5fd638..4318b537a2c 100644 --- a/tests/components/roborock/snapshots/test_diagnostics.ambr +++ b/tests/components/roborock/snapshots/test_diagnostics.ambr @@ -588,6 +588,369 @@ }), }), }), + '**REDACTED-2**': dict({ + 'api': dict({ + 'misc_info': dict({ + }), + }), + 'roborock_device_info': dict({ + 'device': dict({ + 'activeTime': 1700754026, + 'deviceStatus': dict({ + '10001': '{"f":"t"}', + '10002': '', + '10004': '{"sid_in_use":25,"sid_version":5,"location":"de","bom":"A.03.0291","language":"en"}', + '10005': '{"sn":"dyad_sn","ssid":"dyad_ssid","timezone":"Europe/Stockholm","posix_timezone":"CET-1CEST,M3.5.0,M10.5.0/3","ip":"1.123.12.1","mac":"b0:4a:33:33:33:33","oba":{"language":"en","name":"A.03.0291_CE","bom":"A.03.0291","location":"de","wifiplan":"EU","timezone":"CET-1CEST,M3.5.0,M10.5.0/3;Europe/Berlin","logserver":"awsde0","featureset":"0"}"}', + '10007': '{"mqttOtaData":{"mqttOtaStatus":{"status":"IDLE"}}}', + '200': 0, + '201': 3, + '202': 0, + '203': 2, + '204': 1, + '205': 1, + '206': 3, + '207': 4, + '208': 1, + '209': 100, + '210': 0, + '212': 1, + '213': 1, + '214': 513, + '215': 513, + '216': 0, + '221': 100, + '222': 0, + '223': 2, + '224': 1, + '225': 360, + '226': 0, + '227': 1320, + '228': 360, + '229': '000,000,003,000,005,000,000,000,003,000,005,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,012,003,000,000', + '230': 352, + '235': 0, + '237': 0, + }), + 'duid': '**REDACTED**', + 'f': False, + 'fv': '01.12.34', + 'iconUrl': '', + 'localKey': '**REDACTED**', + 'name': 'Dyad Pro', + 'online': True, + 'productId': 'dyad_product', + 'pv': 'A01', + 'share': True, + 'shareTime': 1701367095, + 'silentOtaSwitch': False, + 'timeZoneId': 'Europe/Stockholm', + 'tuyaMigrated': False, + }), + 'product': dict({ + 'capability': 2, + 'category': 'roborock.wetdryvac', + 'id': 'dyad_product', + 'model': 'roborock.wetdryvac.a56', + 'name': 'Roborock Dyad Pro', + 'schema': list([ + dict({ + 'code': 'drying_status', + 'id': '134', + 'mode': 'ro', + 'name': '烘干状态', + 'type': 'RAW', + }), + dict({ + 'code': 'start', + 'id': '200', + 'mode': 'rw', + 'name': '启停', + 'type': 'VALUE', + }), + dict({ + 'code': 'status', + 'id': '201', + 'mode': 'ro', + 'name': '状态', + 'type': 'VALUE', + }), + dict({ + 'code': 'self_clean_mode', + 'id': '202', + 'mode': 'rw', + 'name': '自清洁模式', + 'type': 'VALUE', + }), + dict({ + 'code': 'self_clean_level', + 'id': '203', + 'mode': 'rw', + 'name': '自清洁强度', + 'type': 'VALUE', + }), + dict({ + 'code': 'warm_level', + 'id': '204', + 'mode': 'rw', + 'name': '烘干强度', + 'type': 'VALUE', + }), + dict({ + 'code': 'clean_mode', + 'id': '205', + 'mode': 'rw', + 'name': '洗地模式', + 'type': 'VALUE', + }), + dict({ + 'code': 'suction', + 'id': '206', + 'mode': 'rw', + 'name': '吸力', + 'type': 'VALUE', + }), + dict({ + 'code': 'water_level', + 'id': '207', + 'mode': 'rw', + 'name': '水量', + 'type': 'VALUE', + }), + dict({ + 'code': 'brush_speed', + 'id': '208', + 'mode': 'rw', + 'name': '滚刷转速', + 'type': 'VALUE', + }), + dict({ + 'code': 'power', + 'id': '209', + 'mode': 'ro', + 'name': '电量', + 'type': 'VALUE', + }), + dict({ + 'code': 'countdown_time', + 'id': '210', + 'mode': 'rw', + 'name': '预约时间', + 'type': 'VALUE', + }), + dict({ + 'code': 'auto_self_clean_set', + 'id': '212', + 'mode': 'rw', + 'name': '自动自清洁', + 'type': 'VALUE', + }), + dict({ + 'code': 'auto_dry', + 'id': '213', + 'mode': 'rw', + 'name': '自动烘干', + 'type': 'VALUE', + }), + dict({ + 'code': 'mesh_left', + 'id': '214', + 'mode': 'ro', + 'name': '滤网已工作时间', + 'type': 'VALUE', + }), + dict({ + 'code': 'brush_left', + 'id': '215', + 'mode': 'ro', + 'name': '滚刷已工作时间', + 'type': 'VALUE', + }), + dict({ + 'code': 'error', + 'id': '216', + 'mode': 'ro', + 'name': '错误值', + 'type': 'VALUE', + }), + dict({ + 'code': 'mesh_reset', + 'id': '218', + 'mode': 'rw', + 'name': '滤网重置', + 'type': 'VALUE', + }), + dict({ + 'code': 'brush_reset', + 'id': '219', + 'mode': 'rw', + 'name': '滚刷重置', + 'type': 'VALUE', + }), + dict({ + 'code': 'volume_set', + 'id': '221', + 'mode': 'rw', + 'name': '音量', + 'type': 'VALUE', + }), + dict({ + 'code': 'stand_lock_auto_run', + 'id': '222', + 'mode': 'rw', + 'name': '直立解锁自动运行开关', + 'type': 'VALUE', + }), + dict({ + 'code': 'auto_self_clean_set_mode', + 'id': '223', + 'mode': 'rw', + 'name': '自动自清洁 - 模式', + 'type': 'VALUE', + }), + dict({ + 'code': 'auto_dry_mode', + 'id': '224', + 'mode': 'rw', + 'name': '自动烘干 - 模式', + 'type': 'VALUE', + }), + dict({ + 'code': 'silent_dry_duration', + 'id': '225', + 'mode': 'rw', + 'name': '静音烘干时长', + 'type': 'VALUE', + }), + dict({ + 'code': 'silent_mode', + 'id': '226', + 'mode': 'rw', + 'name': '勿扰模式开关', + 'type': 'VALUE', + }), + dict({ + 'code': 'silent_mode_start_time', + 'id': '227', + 'mode': 'rw', + 'name': '勿扰开启时间', + 'type': 'VALUE', + }), + dict({ + 'code': 'silent_mode_end_time', + 'id': '228', + 'mode': 'rw', + 'name': '勿扰结束时间', + 'type': 'VALUE', + }), + dict({ + 'code': 'recent_run_time', + 'id': '229', + 'mode': 'rw', + 'name': '近30天每天洗地时长', + 'type': 'STRING', + }), + dict({ + 'code': 'total_run_time', + 'id': '230', + 'mode': 'rw', + 'name': '洗地总时长', + 'type': 'VALUE', + }), + dict({ + 'code': 'feature_info', + 'id': '235', + 'mode': 'ro', + 'name': 'featureinfo', + 'type': 'VALUE', + }), + dict({ + 'code': 'recover_settings', + 'id': '236', + 'mode': 'rw', + 'name': '恢复初始设置', + 'type': 'VALUE', + }), + dict({ + 'code': 'dry_countdown', + 'id': '237', + 'mode': 'ro', + 'name': '烘干倒计时', + 'type': 'VALUE', + }), + dict({ + 'code': 'id_query', + 'id': '10000', + 'mode': 'rw', + 'name': 'ID点数据查询', + 'type': 'STRING', + }), + dict({ + 'code': 'f_c', + 'id': '10001', + 'mode': 'ro', + 'name': '防串货', + 'type': 'STRING', + }), + dict({ + 'code': 'schedule_task', + 'id': '10002', + 'mode': 'rw', + 'name': '定时任务', + 'type': 'STRING', + }), + dict({ + 'code': 'snd_switch', + 'id': '10003', + 'mode': 'rw', + 'name': '语音包切换', + 'type': 'STRING', + }), + dict({ + 'code': 'snd_state', + 'id': '10004', + 'mode': 'rw', + 'name': '语音包/OBA信息', + 'type': 'STRING', + }), + dict({ + 'code': 'product_info', + 'id': '10005', + 'mode': 'ro', + 'name': '产品信息', + 'type': 'STRING', + }), + dict({ + 'code': 'privacy_info', + 'id': '10006', + 'mode': 'rw', + 'name': '隐私协议', + 'type': 'STRING', + }), + dict({ + 'code': 'ota_nfo', + 'id': '10007', + 'mode': 'ro', + 'name': 'OTA info', + 'type': 'STRING', + }), + dict({ + 'code': 'rpc_req', + 'id': '10101', + 'mode': 'wo', + 'name': 'rpc req', + 'type': 'STRING', + }), + dict({ + 'code': 'rpc_resp', + 'id': '10102', + 'mode': 'ro', + 'name': 'rpc resp', + 'type': 'STRING', + }), + ]), + }), + }), + }), }), }) # --- diff --git a/tests/components/roborock/test_init.py b/tests/components/roborock/test_init.py index de858ef7cb2..0437ce781f1 100644 --- a/tests/components/roborock/test_init.py +++ b/tests/components/roborock/test_init.py @@ -1,7 +1,9 @@ """Test for Roborock init.""" +from copy import deepcopy from unittest.mock import patch +import pytest from roborock import RoborockException, RoborockInvalidCredentials from homeassistant.components.roborock.const import DOMAIN @@ -9,6 +11,8 @@ from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component +from .mock_data import HOME_DATA + from tests.common import MockConfigEntry @@ -34,7 +38,7 @@ async def test_config_entry_not_ready( """Test that when coordinator update fails, entry retries.""" with ( patch( - "homeassistant.components.roborock.RoborockApiClient.get_home_data", + "homeassistant.components.roborock.RoborockApiClient.get_home_data_v2", ), patch( "homeassistant.components.roborock.coordinator.RoborockLocalClientV1.get_prop", @@ -51,7 +55,7 @@ async def test_config_entry_not_ready_home_data( """Test that when we fail to get home data, entry retries.""" with ( patch( - "homeassistant.components.roborock.RoborockApiClient.get_home_data", + "homeassistant.components.roborock.RoborockApiClient.get_home_data_v2", side_effect=RoborockException(), ), patch( @@ -64,7 +68,9 @@ async def test_config_entry_not_ready_home_data( async def test_get_networking_fails( - hass: HomeAssistant, mock_roborock_entry: MockConfigEntry, bypass_api_fixture + hass: HomeAssistant, + mock_roborock_entry: MockConfigEntry, + bypass_api_fixture_v1_only, ) -> None: """Test that when networking fails, we attempt to retry.""" with patch( @@ -76,7 +82,9 @@ async def test_get_networking_fails( async def test_get_networking_fails_none( - hass: HomeAssistant, mock_roborock_entry: MockConfigEntry, bypass_api_fixture + hass: HomeAssistant, + mock_roborock_entry: MockConfigEntry, + bypass_api_fixture_v1_only, ) -> None: """Test that when networking returns None, we attempt to retry.""" with patch( @@ -88,7 +96,9 @@ async def test_get_networking_fails_none( async def test_cloud_client_fails_props( - hass: HomeAssistant, mock_roborock_entry: MockConfigEntry, bypass_api_fixture + hass: HomeAssistant, + mock_roborock_entry: MockConfigEntry, + bypass_api_fixture_v1_only, ) -> None: """Test that if networking succeeds, but we can't communicate with the vacuum, we can't get props, fail.""" with ( @@ -106,7 +116,9 @@ async def test_cloud_client_fails_props( async def test_local_client_fails_props( - hass: HomeAssistant, mock_roborock_entry: MockConfigEntry, bypass_api_fixture + hass: HomeAssistant, + mock_roborock_entry: MockConfigEntry, + bypass_api_fixture_v1_only, ) -> None: """Test that if networking succeeds, but we can't communicate locally with the vacuum, we can't get props, fail.""" with patch( @@ -118,7 +130,9 @@ async def test_local_client_fails_props( async def test_fails_maps_continue( - hass: HomeAssistant, mock_roborock_entry: MockConfigEntry, bypass_api_fixture + hass: HomeAssistant, + mock_roborock_entry: MockConfigEntry, + bypass_api_fixture_v1_only, ) -> None: """Test that if we fail to get the maps, we still setup.""" with patch( @@ -136,7 +150,7 @@ async def test_reauth_started( ) -> None: """Test reauth flow started.""" with patch( - "homeassistant.components.roborock.RoborockApiClient.get_home_data", + "homeassistant.components.roborock.RoborockApiClient.get_home_data_v2", side_effect=RoborockInvalidCredentials(), ): await async_setup_component(hass, DOMAIN, {}) @@ -145,3 +159,21 @@ async def test_reauth_started( flows = hass.config_entries.flow.async_progress() assert len(flows) == 1 assert flows[0]["step_id"] == "reauth_confirm" + + +async def test_not_supported_protocol( + hass: HomeAssistant, + bypass_api_fixture, + mock_roborock_entry: MockConfigEntry, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test that we output a message on incorrect protocol.""" + home_data_copy = deepcopy(HOME_DATA) + home_data_copy.received_devices[0].pv = "random" + with patch( + "homeassistant.components.roborock.RoborockApiClient.get_home_data_v2", + return_value=home_data_copy, + ): + await async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + assert "because its protocol version random" in caplog.text diff --git a/tests/components/roborock/test_sensor.py b/tests/components/roborock/test_sensor.py index 88ed6e1098c..e608895ca43 100644 --- a/tests/components/roborock/test_sensor.py +++ b/tests/components/roborock/test_sensor.py @@ -21,7 +21,7 @@ from tests.common import MockConfigEntry async def test_sensors(hass: HomeAssistant, setup_entry: MockConfigEntry) -> None: """Test sensors and check test values are correctly set.""" - assert len(hass.states.async_all("sensor")) == 28 + assert len(hass.states.async_all("sensor")) == 34 assert hass.states.get("sensor.roborock_s7_maxv_main_brush_time_left").state == str( MAIN_BRUSH_REPLACE_TIME - 74382 ) @@ -54,6 +54,12 @@ async def test_sensors(hass: HomeAssistant, setup_entry: MockConfigEntry) -> Non hass.states.get("sensor.roborock_s7_maxv_last_clean_end").state == "2023-01-01T03:43:58+00:00" ) + assert hass.states.get("sensor.dyad_pro_status").state == "drying" + assert hass.states.get("sensor.dyad_pro_battery").state == "100" + assert hass.states.get("sensor.dyad_pro_filter_time_left").state == "111" + assert hass.states.get("sensor.dyad_pro_roller_left").state == "222" + assert hass.states.get("sensor.dyad_pro_error").state == "none" + assert hass.states.get("sensor.dyad_pro_total_cleaning_time").state == "213" async def test_listener_update( From 66a803e56ca019a6e99b84fb1ff9399ccc13654e Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 26 Jun 2024 15:41:20 +0200 Subject: [PATCH 1289/1445] Disable Aladdin Connect (#120558) Co-authored-by: Robert Resch --- .../components/aladdin_connect/__init__.py | 4 +- .../components/aladdin_connect/api.py | 3 +- .../components/aladdin_connect/coordinator.py | 6 +- .../components/aladdin_connect/cover.py | 4 +- .../components/aladdin_connect/entity.py | 4 +- .../components/aladdin_connect/manifest.json | 1 + .../components/aladdin_connect/ruff.toml | 5 + .../components/aladdin_connect/sensor.py | 6 +- requirements_all.txt | 3 - requirements_test_all.txt | 3 - tests/components/aladdin_connect/conftest.py | 4 +- .../aladdin_connect/test_config_flow.py | 451 +++++++++--------- 12 files changed, 249 insertions(+), 245 deletions(-) create mode 100644 homeassistant/components/aladdin_connect/ruff.toml diff --git a/homeassistant/components/aladdin_connect/__init__.py b/homeassistant/components/aladdin_connect/__init__.py index 436e797271f..ed284c0e6bb 100644 --- a/homeassistant/components/aladdin_connect/__init__.py +++ b/homeassistant/components/aladdin_connect/__init__.py @@ -1,9 +1,9 @@ """The Aladdin Connect Genie integration.""" +# mypy: ignore-errors from __future__ import annotations -from genie_partner_sdk.client import AladdinConnectClient - +# from genie_partner_sdk.client import AladdinConnectClient from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant diff --git a/homeassistant/components/aladdin_connect/api.py b/homeassistant/components/aladdin_connect/api.py index c4a19ef0081..4377fc8fbcb 100644 --- a/homeassistant/components/aladdin_connect/api.py +++ b/homeassistant/components/aladdin_connect/api.py @@ -1,10 +1,11 @@ """API for Aladdin Connect Genie bound to Home Assistant OAuth.""" +# mypy: ignore-errors from typing import cast from aiohttp import ClientSession -from genie_partner_sdk.auth import Auth +# from genie_partner_sdk.auth import Auth from homeassistant.helpers.config_entry_oauth2_flow import OAuth2Session API_URL = "https://twdvzuefzh.execute-api.us-east-2.amazonaws.com/v1" diff --git a/homeassistant/components/aladdin_connect/coordinator.py b/homeassistant/components/aladdin_connect/coordinator.py index d9af0da9450..9af3e330409 100644 --- a/homeassistant/components/aladdin_connect/coordinator.py +++ b/homeassistant/components/aladdin_connect/coordinator.py @@ -1,11 +1,11 @@ """Define an object to coordinate fetching Aladdin Connect data.""" +# mypy: ignore-errors from datetime import timedelta import logging -from genie_partner_sdk.client import AladdinConnectClient -from genie_partner_sdk.model import GarageDoor - +# from genie_partner_sdk.client import AladdinConnectClient +# from genie_partner_sdk.model import GarageDoor from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator diff --git a/homeassistant/components/aladdin_connect/cover.py b/homeassistant/components/aladdin_connect/cover.py index b8c48048192..1be41e6b516 100644 --- a/homeassistant/components/aladdin_connect/cover.py +++ b/homeassistant/components/aladdin_connect/cover.py @@ -1,9 +1,9 @@ """Cover Entity for Genie Garage Door.""" +# mypy: ignore-errors from typing import Any -from genie_partner_sdk.model import GarageDoor - +# from genie_partner_sdk.model import GarageDoor from homeassistant.components.cover import ( CoverDeviceClass, CoverEntity, diff --git a/homeassistant/components/aladdin_connect/entity.py b/homeassistant/components/aladdin_connect/entity.py index 8d9eeefcdfb..2615cbc636e 100644 --- a/homeassistant/components/aladdin_connect/entity.py +++ b/homeassistant/components/aladdin_connect/entity.py @@ -1,6 +1,6 @@ """Defines a base Aladdin Connect entity.""" - -from genie_partner_sdk.model import GarageDoor +# mypy: ignore-errors +# from genie_partner_sdk.model import GarageDoor from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity diff --git a/homeassistant/components/aladdin_connect/manifest.json b/homeassistant/components/aladdin_connect/manifest.json index 69b38399cce..dce95492272 100644 --- a/homeassistant/components/aladdin_connect/manifest.json +++ b/homeassistant/components/aladdin_connect/manifest.json @@ -4,6 +4,7 @@ "codeowners": ["@swcloudgenie"], "config_flow": true, "dependencies": ["application_credentials"], + "disabled": "This integration is disabled because it uses non-open source code to operate.", "documentation": "https://www.home-assistant.io/integrations/aladdin_connect", "iot_class": "cloud_polling", "requirements": ["genie-partner-sdk==1.0.2"] diff --git a/homeassistant/components/aladdin_connect/ruff.toml b/homeassistant/components/aladdin_connect/ruff.toml new file mode 100644 index 00000000000..38f6f586aef --- /dev/null +++ b/homeassistant/components/aladdin_connect/ruff.toml @@ -0,0 +1,5 @@ +extend = "../../../pyproject.toml" + +lint.extend-ignore = [ + "F821" +] diff --git a/homeassistant/components/aladdin_connect/sensor.py b/homeassistant/components/aladdin_connect/sensor.py index 2bd0168a500..cd1fff12c97 100644 --- a/homeassistant/components/aladdin_connect/sensor.py +++ b/homeassistant/components/aladdin_connect/sensor.py @@ -1,13 +1,13 @@ """Support for Aladdin Connect Garage Door sensors.""" +# mypy: ignore-errors from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass -from genie_partner_sdk.client import AladdinConnectClient -from genie_partner_sdk.model import GarageDoor - +# from genie_partner_sdk.client import AladdinConnectClient +# from genie_partner_sdk.model import GarageDoor from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, diff --git a/requirements_all.txt b/requirements_all.txt index f4de7dafa87..a3a62b58b4b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -926,9 +926,6 @@ gassist-text==0.0.11 # homeassistant.components.google gcal-sync==6.0.4 -# homeassistant.components.aladdin_connect -genie-partner-sdk==1.0.2 - # homeassistant.components.geniushub geniushub-client==0.7.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 12ab15a7d76..3f05bcc3d33 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -764,9 +764,6 @@ gassist-text==0.0.11 # homeassistant.components.google gcal-sync==6.0.4 -# homeassistant.components.aladdin_connect -genie-partner-sdk==1.0.2 - # homeassistant.components.geocaching geocachingapi==0.2.1 diff --git a/tests/components/aladdin_connect/conftest.py b/tests/components/aladdin_connect/conftest.py index c7e5190d527..2c158998f49 100644 --- a/tests/components/aladdin_connect/conftest.py +++ b/tests/components/aladdin_connect/conftest.py @@ -5,8 +5,6 @@ from unittest.mock import AsyncMock, patch import pytest from typing_extensions import Generator -from homeassistant.components.aladdin_connect import DOMAIN - from tests.common import MockConfigEntry @@ -23,7 +21,7 @@ def mock_setup_entry() -> Generator[AsyncMock]: def mock_config_entry() -> MockConfigEntry: """Return an Aladdin Connect config entry.""" return MockConfigEntry( - domain=DOMAIN, + domain="aladdin_connect", data={}, title="test@test.com", unique_id="aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", diff --git a/tests/components/aladdin_connect/test_config_flow.py b/tests/components/aladdin_connect/test_config_flow.py index 1537e0f35da..7154c53b9f6 100644 --- a/tests/components/aladdin_connect/test_config_flow.py +++ b/tests/components/aladdin_connect/test_config_flow.py @@ -1,225 +1,230 @@ """Test the Aladdin Connect Garage Door config flow.""" -from unittest.mock import AsyncMock - -import pytest - -from homeassistant.components.aladdin_connect.const import ( - DOMAIN, - OAUTH2_AUTHORIZE, - OAUTH2_TOKEN, -) -from homeassistant.components.application_credentials import ( - ClientCredential, - async_import_client_credential, -) -from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER, ConfigFlowResult -from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import FlowResultType -from homeassistant.helpers import config_entry_oauth2_flow -from homeassistant.setup import async_setup_component - -from tests.common import MockConfigEntry -from tests.test_util.aiohttp import AiohttpClientMocker -from tests.typing import ClientSessionGenerator - -CLIENT_ID = "1234" -CLIENT_SECRET = "5678" - -EXAMPLE_TOKEN = ( - "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhYWFhYWFhYS1iYmJiLWNjY2MtZGRk" - "ZC1lZWVlZWVlZWVlZWUiLCJuYW1lIjoiSm9obiBEb2UiLCJpYXQiOjE1MTYyMzkwMjIsInVzZXJuYW" - "1lIjoidGVzdEB0ZXN0LmNvbSJ9.CTU1YItIrUl8nSM3koJxlFJr5CjLghgc9gS6h45D8dE" -) - - -@pytest.fixture -async def setup_credentials(hass: HomeAssistant) -> None: - """Fixture to setup credentials.""" - assert await async_setup_component(hass, "application_credentials", {}) - await async_import_client_credential( - hass, - DOMAIN, - ClientCredential(CLIENT_ID, CLIENT_SECRET), - ) - - -async def _oauth_actions( - hass: HomeAssistant, - result: ConfigFlowResult, - hass_client_no_auth: ClientSessionGenerator, - aioclient_mock: AiohttpClientMocker, -) -> None: - state = config_entry_oauth2_flow._encode_jwt( - hass, - { - "flow_id": result["flow_id"], - "redirect_uri": "https://example.com/auth/external/callback", - }, - ) - - assert result["url"] == ( - f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}" - "&redirect_uri=https://example.com/auth/external/callback" - f"&state={state}" - ) - - client = await hass_client_no_auth() - resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") - assert resp.status == 200 - assert resp.headers["content-type"] == "text/html; charset=utf-8" - - aioclient_mock.post( - OAUTH2_TOKEN, - json={ - "refresh_token": "mock-refresh-token", - "access_token": EXAMPLE_TOKEN, - "type": "Bearer", - "expires_in": 60, - }, - ) - - -@pytest.mark.usefixtures("current_request_with_host") -async def test_full_flow( - hass: HomeAssistant, - hass_client_no_auth: ClientSessionGenerator, - aioclient_mock: AiohttpClientMocker, - setup_credentials: None, - mock_setup_entry: AsyncMock, -) -> None: - """Check full flow.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER} - ) - await _oauth_actions(hass, result, hass_client_no_auth, aioclient_mock) - - result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] is FlowResultType.CREATE_ENTRY - assert result["title"] == "test@test.com" - assert result["data"]["token"]["access_token"] == EXAMPLE_TOKEN - assert result["data"]["token"]["refresh_token"] == "mock-refresh-token" - assert result["result"].unique_id == "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" - - assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - assert len(mock_setup_entry.mock_calls) == 1 - - -@pytest.mark.usefixtures("current_request_with_host") -async def test_duplicate_entry( - hass: HomeAssistant, - hass_client_no_auth: ClientSessionGenerator, - aioclient_mock: AiohttpClientMocker, - setup_credentials: None, - mock_config_entry: MockConfigEntry, -) -> None: - """Test we abort with duplicate entry.""" - mock_config_entry.add_to_hass(hass) - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER} - ) - await _oauth_actions(hass, result, hass_client_no_auth, aioclient_mock) - - result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] is FlowResultType.ABORT - assert result["reason"] == "already_configured" - - -@pytest.mark.usefixtures("current_request_with_host") -async def test_reauth( - hass: HomeAssistant, - hass_client_no_auth: ClientSessionGenerator, - aioclient_mock: AiohttpClientMocker, - setup_credentials: None, - mock_config_entry: MockConfigEntry, - mock_setup_entry: AsyncMock, -) -> None: - """Test reauthentication.""" - mock_config_entry.add_to_hass(hass) - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={ - "source": SOURCE_REAUTH, - "entry_id": mock_config_entry.entry_id, - }, - data=mock_config_entry.data, - ) - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "reauth_confirm" - result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - await _oauth_actions(hass, result, hass_client_no_auth, aioclient_mock) - - result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] is FlowResultType.ABORT - assert result["reason"] == "reauth_successful" - - -@pytest.mark.usefixtures("current_request_with_host") -async def test_reauth_wrong_account( - hass: HomeAssistant, - hass_client_no_auth: ClientSessionGenerator, - aioclient_mock: AiohttpClientMocker, - setup_credentials: None, - mock_setup_entry: AsyncMock, -) -> None: - """Test reauthentication with wrong account.""" - config_entry = MockConfigEntry( - domain=DOMAIN, - data={}, - title="test@test.com", - unique_id="aaaaaaaa-bbbb-ffff-dddd-eeeeeeeeeeee", - version=2, - ) - config_entry.add_to_hass(hass) - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={ - "source": SOURCE_REAUTH, - "entry_id": config_entry.entry_id, - }, - data=config_entry.data, - ) - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "reauth_confirm" - result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - await _oauth_actions(hass, result, hass_client_no_auth, aioclient_mock) - - result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] is FlowResultType.ABORT - assert result["reason"] == "wrong_account" - - -@pytest.mark.usefixtures("current_request_with_host") -async def test_reauth_old_account( - hass: HomeAssistant, - hass_client_no_auth: ClientSessionGenerator, - aioclient_mock: AiohttpClientMocker, - setup_credentials: None, - mock_setup_entry: AsyncMock, -) -> None: - """Test reauthentication with old account.""" - config_entry = MockConfigEntry( - domain=DOMAIN, - data={}, - title="test@test.com", - unique_id="test@test.com", - version=2, - ) - config_entry.add_to_hass(hass) - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={ - "source": SOURCE_REAUTH, - "entry_id": config_entry.entry_id, - }, - data=config_entry.data, - ) - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "reauth_confirm" - result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - await _oauth_actions(hass, result, hass_client_no_auth, aioclient_mock) - - result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] is FlowResultType.ABORT - assert result["reason"] == "reauth_successful" - assert config_entry.unique_id == "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" +# from unittest.mock import AsyncMock +# +# import pytest +# +# from homeassistant.components.aladdin_connect.const import ( +# DOMAIN, +# OAUTH2_AUTHORIZE, +# OAUTH2_TOKEN, +# ) +# from homeassistant.components.application_credentials import ( +# ClientCredential, +# async_import_client_credential, +# ) +# from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER, ConfigFlowResult +# from homeassistant.core import HomeAssistant +# from homeassistant.data_entry_flow import FlowResultType +# from homeassistant.helpers import config_entry_oauth2_flow +# from homeassistant.setup import async_setup_component +# +# from tests.common import MockConfigEntry +# from tests.test_util.aiohttp import AiohttpClientMocker +# from tests.typing import ClientSessionGenerator +# +# CLIENT_ID = "1234" +# CLIENT_SECRET = "5678" +# +# EXAMPLE_TOKEN = ( +# "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhYWFhYWFhYS1iYmJiLWNjY2MtZGRk" +# "ZC1lZWVlZWVlZWVlZWUiLCJuYW1lIjoiSm9obiBEb2UiLCJpYXQiOjE1MTYyMzkwMjIsInVzZXJuYW" +# "1lIjoidGVzdEB0ZXN0LmNvbSJ9.CTU1YItIrUl8nSM3koJxlFJr5CjLghgc9gS6h45D8dE" +# ) +# +# +# @pytest.fixture +# async def setup_credentials(hass: HomeAssistant) -> None: +# """Fixture to setup credentials.""" +# assert await async_setup_component(hass, "application_credentials", {}) +# await async_import_client_credential( +# hass, +# DOMAIN, +# ClientCredential(CLIENT_ID, CLIENT_SECRET), +# ) +# +# +# async def _oauth_actions( +# hass: HomeAssistant, +# result: ConfigFlowResult, +# hass_client_no_auth: ClientSessionGenerator, +# aioclient_mock: AiohttpClientMocker, +# ) -> None: +# state = config_entry_oauth2_flow._encode_jwt( +# hass, +# { +# "flow_id": result["flow_id"], +# "redirect_uri": "https://example.com/auth/external/callback", +# }, +# ) +# +# assert result["url"] == ( +# f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}" +# "&redirect_uri=https://example.com/auth/external/callback" +# f"&state={state}" +# ) +# +# client = await hass_client_no_auth() +# resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") +# assert resp.status == 200 +# assert resp.headers["content-type"] == "text/html; charset=utf-8" +# +# aioclient_mock.post( +# OAUTH2_TOKEN, +# json={ +# "refresh_token": "mock-refresh-token", +# "access_token": EXAMPLE_TOKEN, +# "type": "Bearer", +# "expires_in": 60, +# }, +# ) +# +# +# @pytest.mark.skip(reason="Integration disabled") +# @pytest.mark.usefixtures("current_request_with_host") +# async def test_full_flow( +# hass: HomeAssistant, +# hass_client_no_auth: ClientSessionGenerator, +# aioclient_mock: AiohttpClientMocker, +# setup_credentials: None, +# mock_setup_entry: AsyncMock, +# ) -> None: +# """Check full flow.""" +# result = await hass.config_entries.flow.async_init( +# DOMAIN, context={"source": SOURCE_USER} +# ) +# await _oauth_actions(hass, result, hass_client_no_auth, aioclient_mock) +# +# result = await hass.config_entries.flow.async_configure(result["flow_id"]) +# assert result["type"] is FlowResultType.CREATE_ENTRY +# assert result["title"] == "test@test.com" +# assert result["data"]["token"]["access_token"] == EXAMPLE_TOKEN +# assert result["data"]["token"]["refresh_token"] == "mock-refresh-token" +# assert result["result"].unique_id == "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" +# +# assert len(hass.config_entries.async_entries(DOMAIN)) == 1 +# assert len(mock_setup_entry.mock_calls) == 1 +# +# +# @pytest.mark.skip(reason="Integration disabled") +# @pytest.mark.usefixtures("current_request_with_host") +# async def test_duplicate_entry( +# hass: HomeAssistant, +# hass_client_no_auth: ClientSessionGenerator, +# aioclient_mock: AiohttpClientMocker, +# setup_credentials: None, +# mock_config_entry: MockConfigEntry, +# ) -> None: +# """Test we abort with duplicate entry.""" +# mock_config_entry.add_to_hass(hass) +# result = await hass.config_entries.flow.async_init( +# DOMAIN, context={"source": SOURCE_USER} +# ) +# await _oauth_actions(hass, result, hass_client_no_auth, aioclient_mock) +# +# result = await hass.config_entries.flow.async_configure(result["flow_id"]) +# assert result["type"] is FlowResultType.ABORT +# assert result["reason"] == "already_configured" +# +# +# @pytest.mark.skip(reason="Integration disabled") +# @pytest.mark.usefixtures("current_request_with_host") +# async def test_reauth( +# hass: HomeAssistant, +# hass_client_no_auth: ClientSessionGenerator, +# aioclient_mock: AiohttpClientMocker, +# setup_credentials: None, +# mock_config_entry: MockConfigEntry, +# mock_setup_entry: AsyncMock, +# ) -> None: +# """Test reauthentication.""" +# mock_config_entry.add_to_hass(hass) +# result = await hass.config_entries.flow.async_init( +# DOMAIN, +# context={ +# "source": SOURCE_REAUTH, +# "entry_id": mock_config_entry.entry_id, +# }, +# data=mock_config_entry.data, +# ) +# assert result["type"] is FlowResultType.FORM +# assert result["step_id"] == "reauth_confirm" +# result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) +# await _oauth_actions(hass, result, hass_client_no_auth, aioclient_mock) +# +# result = await hass.config_entries.flow.async_configure(result["flow_id"]) +# assert result["type"] is FlowResultType.ABORT +# assert result["reason"] == "reauth_successful" +# +# +# @pytest.mark.skip(reason="Integration disabled") +# @pytest.mark.usefixtures("current_request_with_host") +# async def test_reauth_wrong_account( +# hass: HomeAssistant, +# hass_client_no_auth: ClientSessionGenerator, +# aioclient_mock: AiohttpClientMocker, +# setup_credentials: None, +# mock_setup_entry: AsyncMock, +# ) -> None: +# """Test reauthentication with wrong account.""" +# config_entry = MockConfigEntry( +# domain=DOMAIN, +# data={}, +# title="test@test.com", +# unique_id="aaaaaaaa-bbbb-ffff-dddd-eeeeeeeeeeee", +# version=2, +# ) +# config_entry.add_to_hass(hass) +# result = await hass.config_entries.flow.async_init( +# DOMAIN, +# context={ +# "source": SOURCE_REAUTH, +# "entry_id": config_entry.entry_id, +# }, +# data=config_entry.data, +# ) +# assert result["type"] is FlowResultType.FORM +# assert result["step_id"] == "reauth_confirm" +# result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) +# await _oauth_actions(hass, result, hass_client_no_auth, aioclient_mock) +# +# result = await hass.config_entries.flow.async_configure(result["flow_id"]) +# assert result["type"] is FlowResultType.ABORT +# assert result["reason"] == "wrong_account" +# +# +# @pytest.mark.skip(reason="Integration disabled") +# @pytest.mark.usefixtures("current_request_with_host") +# async def test_reauth_old_account( +# hass: HomeAssistant, +# hass_client_no_auth: ClientSessionGenerator, +# aioclient_mock: AiohttpClientMocker, +# setup_credentials: None, +# mock_setup_entry: AsyncMock, +# ) -> None: +# """Test reauthentication with old account.""" +# config_entry = MockConfigEntry( +# domain=DOMAIN, +# data={}, +# title="test@test.com", +# unique_id="test@test.com", +# version=2, +# ) +# config_entry.add_to_hass(hass) +# result = await hass.config_entries.flow.async_init( +# DOMAIN, +# context={ +# "source": SOURCE_REAUTH, +# "entry_id": config_entry.entry_id, +# }, +# data=config_entry.data, +# ) +# assert result["type"] is FlowResultType.FORM +# assert result["step_id"] == "reauth_confirm" +# result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) +# await _oauth_actions(hass, result, hass_client_no_auth, aioclient_mock) +# +# result = await hass.config_entries.flow.async_configure(result["flow_id"]) +# assert result["type"] is FlowResultType.ABORT +# assert result["reason"] == "reauth_successful" +# assert config_entry.unique_id == "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" From 294e1d4fc487766d60a4f72f8a5f5977723f38f0 Mon Sep 17 00:00:00 2001 From: "Mr. Bubbles" Date: Wed, 26 Jun 2024 15:42:03 +0200 Subject: [PATCH 1290/1445] Fix class name and deprecation version (#120570) --- homeassistant/components/pyload/coordinator.py | 8 ++++---- homeassistant/components/pyload/diagnostics.py | 4 ++-- homeassistant/components/pyload/sensor.py | 8 ++++---- homeassistant/components/pyload/switch.py | 4 ++-- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/pyload/coordinator.py b/homeassistant/components/pyload/coordinator.py index fd0e95192b3..c55ca4c1630 100644 --- a/homeassistant/components/pyload/coordinator.py +++ b/homeassistant/components/pyload/coordinator.py @@ -20,7 +20,7 @@ SCAN_INTERVAL = timedelta(seconds=20) @dataclass(kw_only=True) -class pyLoadData: +class PyLoadData: """Data from pyLoad.""" pause: bool @@ -34,7 +34,7 @@ class pyLoadData: free_space: int -class PyLoadCoordinator(DataUpdateCoordinator[pyLoadData]): +class PyLoadCoordinator(DataUpdateCoordinator[PyLoadData]): """pyLoad coordinator.""" config_entry: ConfigEntry @@ -50,12 +50,12 @@ class PyLoadCoordinator(DataUpdateCoordinator[pyLoadData]): self.pyload = pyload self.version: str | None = None - async def _async_update_data(self) -> pyLoadData: + async def _async_update_data(self) -> PyLoadData: """Fetch data from API endpoint.""" try: if not self.version: self.version = await self.pyload.version() - return pyLoadData( + return PyLoadData( **await self.pyload.get_status(), free_space=await self.pyload.free_space(), ) diff --git a/homeassistant/components/pyload/diagnostics.py b/homeassistant/components/pyload/diagnostics.py index 1b719ffc7b9..e9688a3369b 100644 --- a/homeassistant/components/pyload/diagnostics.py +++ b/homeassistant/components/pyload/diagnostics.py @@ -10,7 +10,7 @@ from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from . import PyLoadConfigEntry -from .coordinator import pyLoadData +from .coordinator import PyLoadData TO_REDACT = {CONF_USERNAME, CONF_PASSWORD, CONF_HOST} @@ -19,7 +19,7 @@ async def async_get_config_entry_diagnostics( hass: HomeAssistant, config_entry: PyLoadConfigEntry ) -> dict[str, Any]: """Return diagnostics for a config entry.""" - pyload_data: pyLoadData = config_entry.runtime_data.data + pyload_data: PyLoadData = config_entry.runtime_data.data return { "config_entry_data": async_redact_data(dict(config_entry.data), TO_REDACT), diff --git a/homeassistant/components/pyload/sensor.py b/homeassistant/components/pyload/sensor.py index 4a0502707b6..a1b29b46260 100644 --- a/homeassistant/components/pyload/sensor.py +++ b/homeassistant/components/pyload/sensor.py @@ -43,7 +43,7 @@ from .const import ( ISSUE_PLACEHOLDER, UNIT_DOWNLOADS, ) -from .coordinator import pyLoadData +from .coordinator import PyLoadData from .entity import BasePyLoadEntity @@ -61,7 +61,7 @@ class PyLoadSensorEntity(StrEnum): class PyLoadSensorEntityDescription(SensorEntityDescription): """Describes pyLoad switch entity.""" - value_fn: Callable[[pyLoadData], StateType] + value_fn: Callable[[PyLoadData], StateType] SENSOR_DESCRIPTIONS: tuple[PyLoadSensorEntityDescription, ...] = ( @@ -142,7 +142,7 @@ async def async_setup_platform( f"deprecated_yaml_{DOMAIN}", is_fixable=False, issue_domain=DOMAIN, - breaks_in_ha_version="2025.2.0", + breaks_in_ha_version="2025.1.0", severity=IssueSeverity.WARNING, translation_key="deprecated_yaml", translation_placeholders={ @@ -155,7 +155,7 @@ async def async_setup_platform( hass, DOMAIN, f"deprecated_yaml_import_issue_{error}", - breaks_in_ha_version="2025.2.0", + breaks_in_ha_version="2025.1.0", is_fixable=False, issue_domain=DOMAIN, severity=IssueSeverity.WARNING, diff --git a/homeassistant/components/pyload/switch.py b/homeassistant/components/pyload/switch.py index 21c8d75aaa0..5e8c61823dd 100644 --- a/homeassistant/components/pyload/switch.py +++ b/homeassistant/components/pyload/switch.py @@ -18,7 +18,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import PyLoadConfigEntry -from .coordinator import pyLoadData +from .coordinator import PyLoadData from .entity import BasePyLoadEntity @@ -36,7 +36,7 @@ class PyLoadSwitchEntityDescription(SwitchEntityDescription): turn_on_fn: Callable[[PyLoadAPI], Awaitable[Any]] turn_off_fn: Callable[[PyLoadAPI], Awaitable[Any]] toggle_fn: Callable[[PyLoadAPI], Awaitable[Any]] - value_fn: Callable[[pyLoadData], bool] + value_fn: Callable[[PyLoadData], bool] SENSOR_DESCRIPTIONS: tuple[PyLoadSwitchEntityDescription, ...] = ( From f4fa5b581ee5d60fd81061ed672760c2197717fe Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 26 Jun 2024 15:43:48 +0200 Subject: [PATCH 1291/1445] Import PLATFORM_SCHEMA from platform not from helpers (#120565) --- homeassistant/components/enigma2/media_player.py | 6 +++--- homeassistant/components/foobot/sensor.py | 6 +++--- homeassistant/components/openerz/sensor.py | 10 ++++++---- homeassistant/components/template/light.py | 8 +++----- homeassistant/components/template/weather.py | 9 +++------ 5 files changed, 18 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/enigma2/media_player.py b/homeassistant/components/enigma2/media_player.py index 8e090e7cecb..63acdd8be72 100644 --- a/homeassistant/components/enigma2/media_player.py +++ b/homeassistant/components/enigma2/media_player.py @@ -11,6 +11,7 @@ from openwebif.enums import PowerState, RemoteControlCodes, SetVolumeOption import voluptuous as vol from homeassistant.components.media_player import ( + PLATFORM_SCHEMA as MEDIA_PLAYER_PLATFORM_SCHEMA, MediaPlayerEntity, MediaPlayerEntityFeature, MediaPlayerState, @@ -26,8 +27,7 @@ from homeassistant.const import ( CONF_USERNAME, ) from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.config_validation import PLATFORM_SCHEMA +from homeassistant.helpers import config_validation as cv from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -57,7 +57,7 @@ ATTR_MEDIA_START_TIME = "media_start_time" _LOGGER = getLogger(__name__) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = MEDIA_PLAYER_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/foobot/sensor.py b/homeassistant/components/foobot/sensor.py index ac8c7e3eec8..f3c6513f051 100644 --- a/homeassistant/components/foobot/sensor.py +++ b/homeassistant/components/foobot/sensor.py @@ -11,6 +11,7 @@ from foobot_async import FoobotClient import voluptuous as vol from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, SensorEntityDescription, @@ -27,9 +28,8 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import PlatformNotReady +from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.config_validation import PLATFORM_SCHEMA from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util import Throttle @@ -86,7 +86,7 @@ PARALLEL_UPDATES = 1 TIMEOUT = 10 -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( {vol.Required(CONF_TOKEN): cv.string, vol.Required(CONF_USERNAME): cv.string} ) diff --git a/homeassistant/components/openerz/sensor.py b/homeassistant/components/openerz/sensor.py index 7447f2eafe4..f41b468b224 100644 --- a/homeassistant/components/openerz/sensor.py +++ b/homeassistant/components/openerz/sensor.py @@ -7,10 +7,12 @@ from datetime import timedelta from openerz_api.main import OpenERZConnector import voluptuous as vol -from homeassistant.components.sensor import SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.config_validation import PLATFORM_SCHEMA +from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -20,7 +22,7 @@ CONF_ZIP = "zip" CONF_WASTE_TYPE = "waste_type" CONF_NAME = "name" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_ZIP): cv.positive_int, vol.Required(CONF_WASTE_TYPE, default="waste"): cv.string, diff --git a/homeassistant/components/template/light.py b/homeassistant/components/template/light.py index de8a2998d34..ba6b8ce846b 100644 --- a/homeassistant/components/template/light.py +++ b/homeassistant/components/template/light.py @@ -17,6 +17,7 @@ from homeassistant.components.light import ( ATTR_RGBWW_COLOR, ATTR_TRANSITION, ENTITY_ID_FORMAT, + PLATFORM_SCHEMA as LIGHT_PLATFORM_SCHEMA, ColorMode, LightEntity, LightEntityFeature, @@ -33,10 +34,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import TemplateError -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.config_validation import ( - PLATFORM_SCHEMA as BASE_PLATFORM_SCHEMA, -) +from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.script import Script @@ -117,7 +115,7 @@ PLATFORM_SCHEMA = vol.All( # CONF_WHITE_VALUE_* is deprecated, support will be removed in release 2022.9 cv.removed(CONF_WHITE_VALUE_ACTION), cv.removed(CONF_WHITE_VALUE_TEMPLATE), - BASE_PLATFORM_SCHEMA.extend( + LIGHT_PLATFORM_SCHEMA.extend( {vol.Required(CONF_LIGHTS): cv.schema_with_slug_keys(LIGHT_SCHEMA)} ), ) diff --git a/homeassistant/components/template/weather.py b/homeassistant/components/template/weather.py index 0f80f65f501..5c3e4107b2c 100644 --- a/homeassistant/components/template/weather.py +++ b/homeassistant/components/template/weather.py @@ -26,6 +26,7 @@ from homeassistant.components.weather import ( ATTR_CONDITION_WINDY_VARIANT, DOMAIN as WEATHER_DOMAIN, ENTITY_ID_FORMAT, + PLATFORM_SCHEMA as WEATHER_PLATFORM_SCHEMA, Forecast, WeatherEntity, WeatherEntityFeature, @@ -39,11 +40,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import TemplateError -from homeassistant.helpers import template -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.config_validation import ( - PLATFORM_SCHEMA as BASE_PLATFORM_SCHEMA, -) +from homeassistant.helpers import config_validation as cv, template from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import ExtraStoredData, RestoreEntity @@ -138,7 +135,7 @@ WEATHER_SCHEMA = vol.Schema( PLATFORM_SCHEMA = vol.All( cv.deprecated(CONF_FORECAST_TEMPLATE), - BASE_PLATFORM_SCHEMA.extend(WEATHER_SCHEMA.schema), + WEATHER_PLATFORM_SCHEMA.extend(WEATHER_SCHEMA.schema), ) From f65d91f6d221832d852ee55dccc3ae54b9c4cc87 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 26 Jun 2024 15:44:59 +0200 Subject: [PATCH 1292/1445] Refactor PLATFORM_SCHEMA imports in platforms (#120564) --- .../components/air_quality/__init__.py | 14 +++++------ .../alarm_control_panel/__init__.py | 6 ++--- .../components/binary_sensor/__init__.py | 11 ++++----- homeassistant/components/button/__init__.py | 12 ++++------ homeassistant/components/calendar/__init__.py | 11 ++++----- homeassistant/components/camera/__init__.py | 14 +++++------ homeassistant/components/climate/__init__.py | 23 ++++++++----------- homeassistant/components/cover/__init__.py | 10 ++++---- homeassistant/components/date/__init__.py | 10 ++++---- homeassistant/components/datetime/__init__.py | 10 ++++---- homeassistant/components/event/__init__.py | 13 ++++------- homeassistant/components/fan/__init__.py | 11 ++++----- .../components/geo_location/__init__.py | 16 ++++++------- .../components/humidifier/__init__.py | 12 ++++------ homeassistant/components/image/__init__.py | 9 ++++---- .../components/lawn_mower/__init__.py | 11 ++++----- homeassistant/components/light/__init__.py | 13 ++++------- homeassistant/components/lock/__init__.py | 18 +++++++-------- .../components/media_player/__init__.py | 11 ++++----- homeassistant/components/number/__init__.py | 9 ++++---- homeassistant/components/remote/__init__.py | 20 +++++++--------- homeassistant/components/select/__init__.py | 11 ++++----- homeassistant/components/sensor/__init__.py | 9 +++----- homeassistant/components/siren/__init__.py | 8 +++---- homeassistant/components/switch/__init__.py | 12 ++++------ homeassistant/components/text/__init__.py | 10 ++++---- homeassistant/components/time/__init__.py | 10 ++++---- homeassistant/components/todo/__init__.py | 11 ++++----- homeassistant/components/update/__init__.py | 11 ++++----- homeassistant/components/vacuum/__init__.py | 7 ++---- homeassistant/components/valve/__init__.py | 10 ++++---- .../components/water_heater/__init__.py | 14 +++++------ homeassistant/components/weather/__init__.py | 14 +++++------ 33 files changed, 156 insertions(+), 235 deletions(-) diff --git a/homeassistant/components/air_quality/__init__.py b/homeassistant/components/air_quality/__init__.py index 78f2616a74d..9a80ee39e86 100644 --- a/homeassistant/components/air_quality/__init__.py +++ b/homeassistant/components/air_quality/__init__.py @@ -9,10 +9,7 @@ from typing import Final, final from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONCENTRATION_MICROGRAMS_PER_CUBIC_METER from homeassistant.core import HomeAssistant -from homeassistant.helpers.config_validation import ( # noqa: F401 - PLATFORM_SCHEMA, - PLATFORM_SCHEMA_BASE, -) +from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.typing import ConfigType, StateType @@ -21,6 +18,11 @@ from .const import DOMAIN _LOGGER: Final = logging.getLogger(__name__) +ENTITY_ID_FORMAT: Final = DOMAIN + ".{}" +PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA +PLATFORM_SCHEMA_BASE = cv.PLATFORM_SCHEMA_BASE +SCAN_INTERVAL: Final = timedelta(seconds=30) + ATTR_AQI: Final = "air_quality_index" ATTR_CO2: Final = "carbon_dioxide" ATTR_CO: Final = "carbon_monoxide" @@ -33,10 +35,6 @@ ATTR_PM_10: Final = "particulate_matter_10" ATTR_PM_2_5: Final = "particulate_matter_2_5" ATTR_SO2: Final = "sulphur_dioxide" -ENTITY_ID_FORMAT: Final = DOMAIN + ".{}" - -SCAN_INTERVAL: Final = timedelta(seconds=30) - PROP_TO_ATTR: Final[dict[str, str]] = { "air_quality_index": ATTR_AQI, "carbon_dioxide": ATTR_CO2, diff --git a/homeassistant/components/alarm_control_panel/__init__.py b/homeassistant/components/alarm_control_panel/__init__.py index f33e168c031..b09d5867d26 100644 --- a/homeassistant/components/alarm_control_panel/__init__.py +++ b/homeassistant/components/alarm_control_panel/__init__.py @@ -52,8 +52,10 @@ from .const import ( # noqa: F401 _LOGGER: Final = logging.getLogger(__name__) -SCAN_INTERVAL: Final = timedelta(seconds=30) ENTITY_ID_FORMAT: Final = DOMAIN + ".{}" +PLATFORM_SCHEMA: Final = cv.PLATFORM_SCHEMA +PLATFORM_SCHEMA_BASE: Final = cv.PLATFORM_SCHEMA_BASE +SCAN_INTERVAL: Final = timedelta(seconds=30) CONF_DEFAULT_CODE = "default_code" @@ -61,8 +63,6 @@ ALARM_SERVICE_SCHEMA: Final = make_entity_service_schema( {vol.Optional(ATTR_CODE): cv.string} ) -PLATFORM_SCHEMA: Final = cv.PLATFORM_SCHEMA -PLATFORM_SCHEMA_BASE: Final = cv.PLATFORM_SCHEMA_BASE # mypy: disallow-any-generics diff --git a/homeassistant/components/binary_sensor/__init__.py b/homeassistant/components/binary_sensor/__init__.py index dad398e2525..0b3e423e339 100644 --- a/homeassistant/components/binary_sensor/__init__.py +++ b/homeassistant/components/binary_sensor/__init__.py @@ -14,10 +14,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_OFF, STATE_ON, EntityCategory from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.config_validation import ( # noqa: F401 - PLATFORM_SCHEMA, - PLATFORM_SCHEMA_BASE, -) +from homeassistant.helpers import config_validation as cv from homeassistant.helpers.deprecation import ( DeprecatedConstantEnum, all_with_deprecated_constants, @@ -30,11 +27,11 @@ from homeassistant.helpers.typing import ConfigType _LOGGER = logging.getLogger(__name__) - DOMAIN = "binary_sensor" -SCAN_INTERVAL = timedelta(seconds=30) - ENTITY_ID_FORMAT = DOMAIN + ".{}" +PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA +PLATFORM_SCHEMA_BASE = cv.PLATFORM_SCHEMA_BASE +SCAN_INTERVAL = timedelta(seconds=30) class BinarySensorDeviceClass(StrEnum): diff --git a/homeassistant/components/button/__init__.py b/homeassistant/components/button/__init__.py index cb8ac7745b2..323f9eddd77 100644 --- a/homeassistant/components/button/__init__.py +++ b/homeassistant/components/button/__init__.py @@ -13,10 +13,7 @@ import voluptuous as vol from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_UNAVAILABLE from homeassistant.core import HomeAssistant -from homeassistant.helpers.config_validation import ( # noqa: F401 - PLATFORM_SCHEMA, - PLATFORM_SCHEMA_BASE, -) +from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity import EntityDescription from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.restore_state import RestoreEntity @@ -25,14 +22,15 @@ from homeassistant.util import dt as dt_util from .const import DOMAIN, SERVICE_PRESS -SCAN_INTERVAL = timedelta(seconds=30) +_LOGGER = logging.getLogger(__name__) ENTITY_ID_FORMAT = DOMAIN + ".{}" +PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA +PLATFORM_SCHEMA_BASE = cv.PLATFORM_SCHEMA_BASE +SCAN_INTERVAL = timedelta(seconds=30) MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) -_LOGGER = logging.getLogger(__name__) - class ButtonDeviceClass(StrEnum): """Device class for buttons.""" diff --git a/homeassistant/components/calendar/__init__.py b/homeassistant/components/calendar/__init__.py index 621356f20e2..b94a6eb935f 100644 --- a/homeassistant/components/calendar/__init__.py +++ b/homeassistant/components/calendar/__init__.py @@ -29,12 +29,7 @@ from homeassistant.core import ( callback, ) from homeassistant.exceptions import HomeAssistantError -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.config_validation import ( # noqa: F401 - PLATFORM_SCHEMA, - PLATFORM_SCHEMA_BASE, - time_period_str, -) +from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.event import async_track_point_in_time @@ -74,6 +69,8 @@ _LOGGER = logging.getLogger(__name__) DOMAIN = "calendar" ENTITY_ID_FORMAT = DOMAIN + ".{}" +PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA +PLATFORM_SCHEMA_BASE = cv.PLATFORM_SCHEMA_BASE SCAN_INTERVAL = datetime.timedelta(seconds=60) # Don't support rrules more often than daily @@ -469,7 +466,7 @@ def extract_offset(summary: str, offset_prefix: str) -> tuple[str, datetime.time else: time = f"0:{time}" - offset_time = time_period_str(time) + offset_time = cv.time_period_str(time) summary = (summary[: search.start()] + summary[search.end() :]).strip() return (summary, offset_time) return (summary, datetime.timedelta()) diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index 428e8d856fb..d8fa4bfbc7a 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -48,11 +48,7 @@ from homeassistant.const import ( ) from homeassistant.core import Event, HomeAssistant, ServiceCall, callback from homeassistant.exceptions import HomeAssistantError -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.config_validation import ( # noqa: F401 - PLATFORM_SCHEMA, - PLATFORM_SCHEMA_BASE, -) +from homeassistant.helpers import config_validation as cv from homeassistant.helpers.deprecation import ( DeprecatedConstantEnum, all_with_deprecated_constants, @@ -87,14 +83,16 @@ from .prefs import CameraPreferences, DynamicStreamSettings # noqa: F401 _LOGGER = logging.getLogger(__name__) +ENTITY_ID_FORMAT: Final = DOMAIN + ".{}" +PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA +PLATFORM_SCHEMA_BASE = cv.PLATFORM_SCHEMA_BASE +SCAN_INTERVAL: Final = timedelta(seconds=30) + SERVICE_ENABLE_MOTION: Final = "enable_motion_detection" SERVICE_DISABLE_MOTION: Final = "disable_motion_detection" SERVICE_SNAPSHOT: Final = "snapshot" SERVICE_PLAY_STREAM: Final = "play_stream" -SCAN_INTERVAL: Final = timedelta(seconds=30) -ENTITY_ID_FORMAT: Final = DOMAIN + ".{}" - ATTR_FILENAME: Final = "filename" ATTR_MEDIA_PLAYER: Final = "media_player" ATTR_FORMAT: Final = "format" diff --git a/homeassistant/components/climate/__init__.py b/homeassistant/components/climate/__init__.py index ac6297dc5b6..bc81ce6e241 100644 --- a/homeassistant/components/climate/__init__.py +++ b/homeassistant/components/climate/__init__.py @@ -25,13 +25,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.exceptions import ServiceValidationError -from homeassistant.helpers import issue_registry as ir -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.config_validation import ( # noqa: F401 - PLATFORM_SCHEMA, - PLATFORM_SCHEMA_BASE, - make_entity_service_schema, -) +from homeassistant.helpers import config_validation as cv, issue_registry as ir from homeassistant.helpers.deprecation import ( all_with_deprecated_constants, check_if_deprecated_constant, @@ -117,24 +111,25 @@ from .const import ( # noqa: F401 HVACMode, ) +_LOGGER = logging.getLogger(__name__) + +ENTITY_ID_FORMAT = DOMAIN + ".{}" +PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA +PLATFORM_SCHEMA_BASE = cv.PLATFORM_SCHEMA_BASE +SCAN_INTERVAL = timedelta(seconds=60) + DEFAULT_MIN_TEMP = 7 DEFAULT_MAX_TEMP = 35 DEFAULT_MIN_HUMIDITY = 30 DEFAULT_MAX_HUMIDITY = 99 -ENTITY_ID_FORMAT = DOMAIN + ".{}" -SCAN_INTERVAL = timedelta(seconds=60) - CONVERTIBLE_ATTRIBUTE = [ATTR_TEMPERATURE, ATTR_TARGET_TEMP_LOW, ATTR_TARGET_TEMP_HIGH] -_LOGGER = logging.getLogger(__name__) - - SET_TEMPERATURE_SCHEMA = vol.All( cv.has_at_least_one_key( ATTR_TEMPERATURE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW ), - make_entity_service_schema( + cv.make_entity_service_schema( { vol.Exclusive(ATTR_TEMPERATURE, "temperature"): vol.Coerce(float), vol.Inclusive(ATTR_TARGET_TEMP_HIGH, "temperature"): vol.Coerce(float), diff --git a/homeassistant/components/cover/__init__.py b/homeassistant/components/cover/__init__.py index 852c5fd9cae..645bd88de7a 100644 --- a/homeassistant/components/cover/__init__.py +++ b/homeassistant/components/cover/__init__.py @@ -30,10 +30,7 @@ from homeassistant.const import ( STATE_OPENING, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers.config_validation import ( # noqa: F401 - PLATFORM_SCHEMA, - PLATFORM_SCHEMA_BASE, -) +from homeassistant.helpers import config_validation as cv from homeassistant.helpers.deprecation import ( DeprecatedConstantEnum, all_with_deprecated_constants, @@ -49,9 +46,10 @@ from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -SCAN_INTERVAL = timedelta(seconds=15) - ENTITY_ID_FORMAT = DOMAIN + ".{}" +PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA +PLATFORM_SCHEMA_BASE = cv.PLATFORM_SCHEMA_BASE +SCAN_INTERVAL = timedelta(seconds=15) class CoverDeviceClass(StrEnum): diff --git a/homeassistant/components/date/__init__.py b/homeassistant/components/date/__init__.py index ddd85ffbf06..7914c6d2984 100644 --- a/homeassistant/components/date/__init__.py +++ b/homeassistant/components/date/__init__.py @@ -13,21 +13,19 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_DATE from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.config_validation import ( # noqa: F401 - PLATFORM_SCHEMA, - PLATFORM_SCHEMA_BASE, -) from homeassistant.helpers.entity import Entity, EntityDescription from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.typing import ConfigType from .const import DOMAIN, SERVICE_SET_VALUE -SCAN_INTERVAL = timedelta(seconds=30) +_LOGGER = logging.getLogger(__name__) ENTITY_ID_FORMAT = DOMAIN + ".{}" +PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA +PLATFORM_SCHEMA_BASE = cv.PLATFORM_SCHEMA_BASE +SCAN_INTERVAL = timedelta(seconds=30) -_LOGGER = logging.getLogger(__name__) __all__ = ["DOMAIN", "DateEntity", "DateEntityDescription"] diff --git a/homeassistant/components/datetime/__init__.py b/homeassistant/components/datetime/__init__.py index f2b8526ced6..f418f81da03 100644 --- a/homeassistant/components/datetime/__init__.py +++ b/homeassistant/components/datetime/__init__.py @@ -12,10 +12,6 @@ import voluptuous as vol from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.config_validation import ( # noqa: F401 - PLATFORM_SCHEMA, - PLATFORM_SCHEMA_BASE, -) from homeassistant.helpers.entity import Entity, EntityDescription from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.typing import ConfigType @@ -23,11 +19,13 @@ from homeassistant.util import dt as dt_util from .const import ATTR_DATETIME, DOMAIN, SERVICE_SET_VALUE -SCAN_INTERVAL = timedelta(seconds=30) +_LOGGER = logging.getLogger(__name__) ENTITY_ID_FORMAT = DOMAIN + ".{}" +PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA +PLATFORM_SCHEMA_BASE = cv.PLATFORM_SCHEMA_BASE +SCAN_INTERVAL = timedelta(seconds=30) -_LOGGER = logging.getLogger(__name__) __all__ = ["ATTR_DATETIME", "DOMAIN", "DateTimeEntity", "DateTimeEntityDescription"] diff --git a/homeassistant/components/event/__init__.py b/homeassistant/components/event/__init__.py index 925c0855c71..4ca000f6a40 100644 --- a/homeassistant/components/event/__init__.py +++ b/homeassistant/components/event/__init__.py @@ -11,10 +11,7 @@ from typing import Any, Self, final from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers.config_validation import ( - PLATFORM_SCHEMA, - PLATFORM_SCHEMA_BASE, -) +from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity import EntityDescription from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.restore_state import ExtraStoredData, RestoreEntity @@ -23,11 +20,11 @@ from homeassistant.util import dt as dt_util from .const import ATTR_EVENT_TYPE, ATTR_EVENT_TYPES, DOMAIN -SCAN_INTERVAL = timedelta(seconds=30) - -ENTITY_ID_FORMAT = DOMAIN + ".{}" - _LOGGER = logging.getLogger(__name__) +ENTITY_ID_FORMAT = DOMAIN + ".{}" +PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA +PLATFORM_SCHEMA_BASE = cv.PLATFORM_SCHEMA_BASE +SCAN_INTERVAL = timedelta(seconds=30) class EventDeviceClass(StrEnum): diff --git a/homeassistant/components/fan/__init__.py b/homeassistant/components/fan/__init__.py index 0bed3eb1ff2..ef6c075a356 100644 --- a/homeassistant/components/fan/__init__.py +++ b/homeassistant/components/fan/__init__.py @@ -21,11 +21,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ServiceValidationError -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.config_validation import ( # noqa: F401 - PLATFORM_SCHEMA, - PLATFORM_SCHEMA_BASE, -) +from homeassistant.helpers import config_validation as cv from homeassistant.helpers.deprecation import ( DeprecatedConstantEnum, all_with_deprecated_constants, @@ -44,9 +40,10 @@ from homeassistant.util.percentage import ( _LOGGER = logging.getLogger(__name__) DOMAIN = "fan" -SCAN_INTERVAL = timedelta(seconds=30) - ENTITY_ID_FORMAT = DOMAIN + ".{}" +PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA +PLATFORM_SCHEMA_BASE = cv.PLATFORM_SCHEMA_BASE +SCAN_INTERVAL = timedelta(seconds=30) class FanEntityFeature(IntFlag): diff --git a/homeassistant/components/geo_location/__init__.py b/homeassistant/components/geo_location/__init__.py index 48e2f35ccc1..e0c8d806fe6 100644 --- a/homeassistant/components/geo_location/__init__.py +++ b/homeassistant/components/geo_location/__init__.py @@ -10,24 +10,22 @@ from typing import Any, final from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE from homeassistant.core import HomeAssistant -from homeassistant.helpers.config_validation import ( # noqa: F401 - PLATFORM_SCHEMA, - PLATFORM_SCHEMA_BASE, -) +from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.typing import ConfigType _LOGGER = logging.getLogger(__name__) +DOMAIN = "geo_location" +ENTITY_ID_FORMAT = DOMAIN + ".{}" +PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA +PLATFORM_SCHEMA_BASE = cv.PLATFORM_SCHEMA_BASE +SCAN_INTERVAL = timedelta(seconds=60) + ATTR_DISTANCE = "distance" ATTR_SOURCE = "source" -DOMAIN = "geo_location" - -ENTITY_ID_FORMAT = DOMAIN + ".{}" - -SCAN_INTERVAL = timedelta(seconds=60) # mypy: disallow-any-generics diff --git a/homeassistant/components/humidifier/__init__.py b/homeassistant/components/humidifier/__init__.py index da79df6d52f..ce94eaaf5a0 100644 --- a/homeassistant/components/humidifier/__init__.py +++ b/homeassistant/components/humidifier/__init__.py @@ -19,11 +19,7 @@ from homeassistant.const import ( STATE_ON, ) from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.config_validation import ( # noqa: F401 - PLATFORM_SCHEMA, - PLATFORM_SCHEMA_BASE, -) +from homeassistant.helpers import config_validation as cv from homeassistant.helpers.deprecation import ( all_with_deprecated_constants, check_if_deprecated_constant, @@ -58,10 +54,10 @@ from .const import ( # noqa: F401 _LOGGER = logging.getLogger(__name__) - -SCAN_INTERVAL = timedelta(seconds=60) - ENTITY_ID_FORMAT = DOMAIN + ".{}" +PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA +PLATFORM_SCHEMA_BASE = cv.PLATFORM_SCHEMA_BASE +SCAN_INTERVAL = timedelta(seconds=60) class HumidifierDeviceClass(StrEnum): diff --git a/homeassistant/components/image/__init__.py b/homeassistant/components/image/__init__.py index f40958a28ea..2307a66d5a1 100644 --- a/homeassistant/components/image/__init__.py +++ b/homeassistant/components/image/__init__.py @@ -20,10 +20,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONTENT_TYPE_MULTIPART, EVENT_HOMEASSISTANT_STOP from homeassistant.core import Event, EventStateChangedData, HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.config_validation import ( # noqa: F401 - PLATFORM_SCHEMA, - PLATFORM_SCHEMA_BASE, -) +from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity import Entity, EntityDescription from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.event import ( @@ -37,8 +34,10 @@ from .const import DOMAIN, IMAGE_TIMEOUT _LOGGER = logging.getLogger(__name__) -SCAN_INTERVAL: Final = timedelta(seconds=30) ENTITY_ID_FORMAT: Final = DOMAIN + ".{}" +PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA +PLATFORM_SCHEMA_BASE = cv.PLATFORM_SCHEMA_BASE +SCAN_INTERVAL: Final = timedelta(seconds=30) DEFAULT_CONTENT_TYPE: Final = "image/jpeg" ENTITY_IMAGE_URL: Final = "/api/image_proxy/{0}?token={1}" diff --git a/homeassistant/components/lawn_mower/__init__.py b/homeassistant/components/lawn_mower/__init__.py index 8cb9850bde7..27765d207d8 100644 --- a/homeassistant/components/lawn_mower/__init__.py +++ b/homeassistant/components/lawn_mower/__init__.py @@ -9,10 +9,7 @@ from typing import final from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers.config_validation import ( # noqa: F401 - PLATFORM_SCHEMA, - PLATFORM_SCHEMA_BASE, -) +from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity import Entity, EntityDescription from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.typing import ConfigType @@ -26,10 +23,12 @@ from .const import ( LawnMowerEntityFeature, ) -SCAN_INTERVAL = timedelta(seconds=60) - _LOGGER = logging.getLogger(__name__) +PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA +PLATFORM_SCHEMA_BASE = cv.PLATFORM_SCHEMA_BASE +SCAN_INTERVAL = timedelta(seconds=60) + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the lawn_mower component.""" diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index b61625edaf2..67000b6aaaf 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -24,11 +24,6 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv, entity_registry as er -from homeassistant.helpers.config_validation import ( # noqa: F401 - PLATFORM_SCHEMA, - PLATFORM_SCHEMA_BASE, - make_entity_service_schema, -) from homeassistant.helpers.entity import ToggleEntity, ToggleEntityDescription from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.typing import ConfigType, VolDictType @@ -36,10 +31,12 @@ from homeassistant.loader import bind_hass import homeassistant.util.color as color_util DOMAIN = "light" -SCAN_INTERVAL = timedelta(seconds=30) -DATA_PROFILES = "light_profiles" - ENTITY_ID_FORMAT = DOMAIN + ".{}" +PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA +PLATFORM_SCHEMA_BASE = cv.PLATFORM_SCHEMA_BASE +SCAN_INTERVAL = timedelta(seconds=30) + +DATA_PROFILES = "light_profiles" class LightEntityFeature(IntFlag): diff --git a/homeassistant/components/lock/__init__.py b/homeassistant/components/lock/__init__.py index 21533353ac7..fd3f60d3502 100644 --- a/homeassistant/components/lock/__init__.py +++ b/homeassistant/components/lock/__init__.py @@ -30,11 +30,6 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ServiceValidationError import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.config_validation import ( # noqa: F401 - PLATFORM_SCHEMA, - PLATFORM_SCHEMA_BASE, - make_entity_service_schema, -) from homeassistant.helpers.deprecation import ( DeprecatedConstantEnum, all_with_deprecated_constants, @@ -49,16 +44,19 @@ from .const import DOMAIN _LOGGER = logging.getLogger(__name__) +ENTITY_ID_FORMAT = DOMAIN + ".{}" +PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA +PLATFORM_SCHEMA_BASE = cv.PLATFORM_SCHEMA_BASE +SCAN_INTERVAL = timedelta(seconds=30) + ATTR_CHANGED_BY = "changed_by" CONF_DEFAULT_CODE = "default_code" -SCAN_INTERVAL = timedelta(seconds=30) - -ENTITY_ID_FORMAT = DOMAIN + ".{}" - MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) -LOCK_SERVICE_SCHEMA = make_entity_service_schema({vol.Optional(ATTR_CODE): cv.string}) +LOCK_SERVICE_SCHEMA = cv.make_entity_service_schema( + {vol.Optional(ATTR_CODE): cv.string} +) class LockEntityFeature(IntFlag): diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index 3679b5f89c5..d499ee8d6d3 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -52,12 +52,8 @@ from homeassistant.const import ( # noqa: F401 STATE_STANDBY, ) from homeassistant.core import HomeAssistant +from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.config_validation import ( # noqa: F401 - PLATFORM_SCHEMA, - PLATFORM_SCHEMA_BASE, -) from homeassistant.helpers.entity import Entity, EntityDescription from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.network import get_url @@ -137,6 +133,9 @@ from .errors import BrowseError _LOGGER = logging.getLogger(__name__) ENTITY_ID_FORMAT = DOMAIN + ".{}" +PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA +PLATFORM_SCHEMA_BASE = cv.PLATFORM_SCHEMA_BASE +SCAN_INTERVAL = dt.timedelta(seconds=10) CACHE_IMAGES: Final = "images" CACHE_MAXSIZE: Final = "maxsize" @@ -144,8 +143,6 @@ CACHE_LOCK: Final = "lock" CACHE_URL: Final = "url" CACHE_CONTENT: Final = "content" -SCAN_INTERVAL = dt.timedelta(seconds=10) - class MediaPlayerEnqueue(StrEnum): """Enqueue types for playing media.""" diff --git a/homeassistant/components/number/__init__.py b/homeassistant/components/number/__init__.py index 77dde242b7e..2c750bd834e 100644 --- a/homeassistant/components/number/__init__.py +++ b/homeassistant/components/number/__init__.py @@ -22,10 +22,7 @@ from homeassistant.core import ( callback, ) from homeassistant.exceptions import ServiceValidationError -from homeassistant.helpers.config_validation import ( - PLATFORM_SCHEMA, - PLATFORM_SCHEMA_BASE, -) +from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity import Entity, EntityDescription from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.restore_state import ExtraStoredData, RestoreEntity @@ -53,10 +50,12 @@ from .websocket_api import async_setup as async_setup_ws_api _LOGGER = logging.getLogger(__name__) ENTITY_ID_FORMAT = DOMAIN + ".{}" +PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA +PLATFORM_SCHEMA_BASE = cv.PLATFORM_SCHEMA_BASE +SCAN_INTERVAL = timedelta(seconds=30) MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) -SCAN_INTERVAL = timedelta(seconds=30) __all__ = [ "ATTR_MAX", diff --git a/homeassistant/components/remote/__init__.py b/homeassistant/components/remote/__init__.py index 88813e4a70c..cb67a7568e2 100644 --- a/homeassistant/components/remote/__init__.py +++ b/homeassistant/components/remote/__init__.py @@ -21,12 +21,7 @@ from homeassistant.const import ( STATE_ON, ) from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.config_validation import ( # noqa: F401 - PLATFORM_SCHEMA, - PLATFORM_SCHEMA_BASE, - make_entity_service_schema, -) +from homeassistant.helpers import config_validation as cv from homeassistant.helpers.deprecation import ( DeprecatedConstantEnum, all_with_deprecated_constants, @@ -40,6 +35,12 @@ from homeassistant.loader import bind_hass _LOGGER = logging.getLogger(__name__) +DOMAIN = "remote" +ENTITY_ID_FORMAT = DOMAIN + ".{}" +PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA +PLATFORM_SCHEMA_BASE = cv.PLATFORM_SCHEMA_BASE +SCAN_INTERVAL = timedelta(seconds=30) + ATTR_ACTIVITY = "activity" ATTR_ACTIVITY_LIST = "activity_list" ATTR_CURRENT_ACTIVITY = "current_activity" @@ -51,11 +52,6 @@ ATTR_HOLD_SECS = "hold_secs" ATTR_ALTERNATIVE = "alternative" ATTR_TIMEOUT = "timeout" -DOMAIN = "remote" -SCAN_INTERVAL = timedelta(seconds=30) - -ENTITY_ID_FORMAT = DOMAIN + ".{}" - MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) SERVICE_SEND_COMMAND = "send_command" @@ -89,7 +85,7 @@ _DEPRECATED_SUPPORT_ACTIVITY = DeprecatedConstantEnum( ) -REMOTE_SERVICE_ACTIVITY_SCHEMA = make_entity_service_schema( +REMOTE_SERVICE_ACTIVITY_SCHEMA = cv.make_entity_service_schema( {vol.Optional(ATTR_ACTIVITY): cv.string} ) diff --git a/homeassistant/components/select/__init__.py b/homeassistant/components/select/__init__.py index 6e134c8958c..27d41dafcd1 100644 --- a/homeassistant/components/select/__init__.py +++ b/homeassistant/components/select/__init__.py @@ -13,10 +13,6 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ServiceValidationError from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.config_validation import ( - PLATFORM_SCHEMA, - PLATFORM_SCHEMA_BASE, -) from homeassistant.helpers.entity import Entity, EntityDescription from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.typing import ConfigType @@ -33,14 +29,15 @@ from .const import ( SERVICE_SELECT_PREVIOUS, ) -SCAN_INTERVAL = timedelta(seconds=30) +_LOGGER = logging.getLogger(__name__) ENTITY_ID_FORMAT = DOMAIN + ".{}" +PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA +PLATFORM_SCHEMA_BASE = cv.PLATFORM_SCHEMA_BASE +SCAN_INTERVAL = timedelta(seconds=30) MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) -_LOGGER = logging.getLogger(__name__) - __all__ = [ "ATTR_CYCLE", "ATTR_OPTION", diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index 8d81df6431f..63b853f971e 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -50,11 +50,7 @@ from homeassistant.const import ( # noqa: F401 ) from homeassistant.core import HomeAssistant, State, callback from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import entity_registry as er -from homeassistant.helpers.config_validation import ( - PLATFORM_SCHEMA, - PLATFORM_SCHEMA_BASE, -) +from homeassistant.helpers import config_validation as cv, entity_registry as er from homeassistant.helpers.deprecation import ( all_with_deprecated_constants, check_if_deprecated_constant, @@ -93,7 +89,8 @@ from .websocket_api import async_setup as async_setup_ws_api _LOGGER: Final = logging.getLogger(__name__) ENTITY_ID_FORMAT: Final = DOMAIN + ".{}" - +PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA +PLATFORM_SCHEMA_BASE = cv.PLATFORM_SCHEMA_BASE SCAN_INTERVAL: Final = timedelta(seconds=30) __all__ = [ diff --git a/homeassistant/components/siren/__init__.py b/homeassistant/components/siren/__init__.py index e5837fdd1bf..216e111b7db 100644 --- a/homeassistant/components/siren/__init__.py +++ b/homeassistant/components/siren/__init__.py @@ -12,11 +12,7 @@ import voluptuous as vol from homeassistant.config_entries import ConfigEntry from homeassistant.const import SERVICE_TOGGLE, SERVICE_TURN_OFF, SERVICE_TURN_ON from homeassistant.core import HomeAssistant, ServiceCall -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.config_validation import ( # noqa: F401 - PLATFORM_SCHEMA, - PLATFORM_SCHEMA_BASE, -) +from homeassistant.helpers import config_validation as cv from homeassistant.helpers.deprecation import ( all_with_deprecated_constants, check_if_deprecated_constant, @@ -42,6 +38,8 @@ from .const import ( # noqa: F401 _LOGGER = logging.getLogger(__name__) +PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA +PLATFORM_SCHEMA_BASE = cv.PLATFORM_SCHEMA_BASE SCAN_INTERVAL = timedelta(seconds=60) TURN_ON_SCHEMA: VolDictType = { diff --git a/homeassistant/components/switch/__init__.py b/homeassistant/components/switch/__init__.py index 995bcda294f..55e0a7a767e 100644 --- a/homeassistant/components/switch/__init__.py +++ b/homeassistant/components/switch/__init__.py @@ -17,10 +17,7 @@ from homeassistant.const import ( STATE_ON, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers.config_validation import ( # noqa: F401 - PLATFORM_SCHEMA, - PLATFORM_SCHEMA_BASE, -) +from homeassistant.helpers import config_validation as cv from homeassistant.helpers.deprecation import ( DeprecatedConstantEnum, all_with_deprecated_constants, @@ -34,14 +31,15 @@ from homeassistant.loader import bind_hass from .const import DOMAIN -SCAN_INTERVAL = timedelta(seconds=30) +_LOGGER = logging.getLogger(__name__) ENTITY_ID_FORMAT = DOMAIN + ".{}" +PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA +PLATFORM_SCHEMA_BASE = cv.PLATFORM_SCHEMA_BASE +SCAN_INTERVAL = timedelta(seconds=30) MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) -_LOGGER = logging.getLogger(__name__) - class SwitchDeviceClass(StrEnum): """Device class for switches.""" diff --git a/homeassistant/components/text/__init__.py b/homeassistant/components/text/__init__.py index f45a9cf3563..33589be8f41 100644 --- a/homeassistant/components/text/__init__.py +++ b/homeassistant/components/text/__init__.py @@ -16,10 +16,6 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import MAX_LENGTH_STATE_STATE from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.config_validation import ( # noqa: F401 - PLATFORM_SCHEMA, - PLATFORM_SCHEMA_BASE, -) from homeassistant.helpers.entity import Entity, EntityDescription from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.restore_state import ExtraStoredData, RestoreEntity @@ -35,13 +31,15 @@ from .const import ( SERVICE_SET_VALUE, ) -SCAN_INTERVAL = timedelta(seconds=30) +_LOGGER = logging.getLogger(__name__) ENTITY_ID_FORMAT = DOMAIN + ".{}" +PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA +PLATFORM_SCHEMA_BASE = cv.PLATFORM_SCHEMA_BASE +SCAN_INTERVAL = timedelta(seconds=30) MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) -_LOGGER = logging.getLogger(__name__) __all__ = ["DOMAIN", "TextEntity", "TextEntityDescription", "TextMode"] diff --git a/homeassistant/components/time/__init__.py b/homeassistant/components/time/__init__.py index 4e101ddd67d..23c9796ec2e 100644 --- a/homeassistant/components/time/__init__.py +++ b/homeassistant/components/time/__init__.py @@ -13,21 +13,19 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TIME from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.config_validation import ( # noqa: F401 - PLATFORM_SCHEMA, - PLATFORM_SCHEMA_BASE, -) from homeassistant.helpers.entity import Entity, EntityDescription from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.typing import ConfigType from .const import DOMAIN, SERVICE_SET_VALUE -SCAN_INTERVAL = timedelta(seconds=30) +_LOGGER = logging.getLogger(__name__) ENTITY_ID_FORMAT = DOMAIN + ".{}" +PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA +PLATFORM_SCHEMA_BASE = cv.PLATFORM_SCHEMA_BASE +SCAN_INTERVAL = timedelta(seconds=30) -_LOGGER = logging.getLogger(__name__) __all__ = ["DOMAIN", "TimeEntity", "TimeEntityDescription"] diff --git a/homeassistant/components/todo/__init__.py b/homeassistant/components/todo/__init__.py index e574c6372a7..a515f0805e7 100644 --- a/homeassistant/components/todo/__init__.py +++ b/homeassistant/components/todo/__init__.py @@ -21,11 +21,7 @@ from homeassistant.core import ( callback, ) from homeassistant.exceptions import HomeAssistantError, ServiceValidationError -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.config_validation import ( # noqa: F401 - PLATFORM_SCHEMA, - PLATFORM_SCHEMA_BASE, -) +from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.typing import ConfigType @@ -44,9 +40,10 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -SCAN_INTERVAL = datetime.timedelta(seconds=60) - ENTITY_ID_FORMAT = DOMAIN + ".{}" +PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA +PLATFORM_SCHEMA_BASE = cv.PLATFORM_SCHEMA_BASE +SCAN_INTERVAL = datetime.timedelta(seconds=60) @dataclasses.dataclass diff --git a/homeassistant/components/update/__init__.py b/homeassistant/components/update/__init__.py index 352237bf201..e7813b354c1 100644 --- a/homeassistant/components/update/__init__.py +++ b/homeassistant/components/update/__init__.py @@ -17,10 +17,6 @@ from homeassistant.const import ATTR_ENTITY_PICTURE, STATE_OFF, STATE_ON, Entity from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.config_validation import ( - PLATFORM_SCHEMA, - PLATFORM_SCHEMA_BASE, -) from homeassistant.helpers.entity import ABCCachedProperties, EntityDescription from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.restore_state import RestoreEntity @@ -43,11 +39,12 @@ from .const import ( UpdateEntityFeature, ) -SCAN_INTERVAL = timedelta(minutes=15) +_LOGGER = logging.getLogger(__name__) ENTITY_ID_FORMAT: Final = DOMAIN + ".{}" - -_LOGGER = logging.getLogger(__name__) +PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA +PLATFORM_SCHEMA_BASE = cv.PLATFORM_SCHEMA_BASE +SCAN_INTERVAL = timedelta(minutes=15) class UpdateDeviceClass(StrEnum): diff --git a/homeassistant/components/vacuum/__init__.py b/homeassistant/components/vacuum/__init__.py index f68f9a4f082..90018e2d8cc 100644 --- a/homeassistant/components/vacuum/__init__.py +++ b/homeassistant/components/vacuum/__init__.py @@ -23,11 +23,6 @@ from homeassistant.const import ( # noqa: F401 # STATE_PAUSED/IDLE are API ) from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.config_validation import ( # noqa: F401 - PLATFORM_SCHEMA, - PLATFORM_SCHEMA_BASE, - make_entity_service_schema, -) from homeassistant.helpers.entity import Entity, EntityDescription from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.icon import icon_for_battery_level @@ -39,6 +34,8 @@ from .const import DOMAIN, STATE_CLEANING, STATE_DOCKED, STATE_ERROR, STATE_RETU _LOGGER = logging.getLogger(__name__) ENTITY_ID_FORMAT = DOMAIN + ".{}" +PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA +PLATFORM_SCHEMA_BASE = cv.PLATFORM_SCHEMA_BASE SCAN_INTERVAL = timedelta(seconds=20) ATTR_BATTERY_ICON = "battery_icon" diff --git a/homeassistant/components/valve/__init__.py b/homeassistant/components/valve/__init__.py index 0363ef55832..e97a68c2e82 100644 --- a/homeassistant/components/valve/__init__.py +++ b/homeassistant/components/valve/__init__.py @@ -23,10 +23,7 @@ from homeassistant.const import ( STATE_OPENING, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers.config_validation import ( # noqa: F401 - PLATFORM_SCHEMA, - PLATFORM_SCHEMA_BASE, -) +from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity import Entity, EntityDescription from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.typing import ConfigType @@ -34,9 +31,10 @@ from homeassistant.helpers.typing import ConfigType _LOGGER = logging.getLogger(__name__) DOMAIN = "valve" -SCAN_INTERVAL = timedelta(seconds=15) - ENTITY_ID_FORMAT = DOMAIN + ".{}" +PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA +PLATFORM_SCHEMA_BASE = cv.PLATFORM_SCHEMA_BASE +SCAN_INTERVAL = timedelta(seconds=15) class ValveDeviceClass(StrEnum): diff --git a/homeassistant/components/water_heater/__init__.py b/homeassistant/components/water_heater/__init__.py index 1623b391e53..731a513fb66 100644 --- a/homeassistant/components/water_heater/__init__.py +++ b/homeassistant/components/water_heater/__init__.py @@ -25,11 +25,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.exceptions import ServiceValidationError -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.config_validation import ( # noqa: F401 - PLATFORM_SCHEMA, - PLATFORM_SCHEMA_BASE, -) +from homeassistant.helpers import config_validation as cv from homeassistant.helpers.deprecation import ( DeprecatedConstantEnum, all_with_deprecated_constants, @@ -44,12 +40,14 @@ from homeassistant.util.unit_conversion import TemperatureConverter from .const import DOMAIN +ENTITY_ID_FORMAT = DOMAIN + ".{}" +PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA +PLATFORM_SCHEMA_BASE = cv.PLATFORM_SCHEMA_BASE +SCAN_INTERVAL = timedelta(seconds=60) + DEFAULT_MIN_TEMP = 110 DEFAULT_MAX_TEMP = 140 -ENTITY_ID_FORMAT = DOMAIN + ".{}" -SCAN_INTERVAL = timedelta(seconds=60) - SERVICE_SET_AWAY_MODE = "set_away_mode" SERVICE_SET_TEMPERATURE = "set_temperature" SERVICE_SET_OPERATION_MODE = "set_operation_mode" diff --git a/homeassistant/components/weather/__init__.py b/homeassistant/components/weather/__init__.py index b3ce52510d2..468c023b470 100644 --- a/homeassistant/components/weather/__init__.py +++ b/homeassistant/components/weather/__init__.py @@ -31,10 +31,7 @@ from homeassistant.core import ( callback, ) from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.config_validation import ( # noqa: F401 - PLATFORM_SCHEMA, - PLATFORM_SCHEMA_BASE, -) +from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity import ABCCachedProperties, Entity, EntityDescription from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.typing import ConfigType @@ -74,6 +71,11 @@ from .websocket_api import async_setup as async_setup_ws_api _LOGGER = logging.getLogger(__name__) +ENTITY_ID_FORMAT = DOMAIN + ".{}" +PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA +PLATFORM_SCHEMA_BASE = cv.PLATFORM_SCHEMA_BASE +SCAN_INTERVAL = timedelta(seconds=30) + ATTR_CONDITION_CLASS = "condition_class" ATTR_CONDITION_CLEAR_NIGHT = "clear-night" ATTR_CONDITION_CLOUDY = "cloudy" @@ -115,10 +117,6 @@ ATTR_FORECAST_DEW_POINT: Final = "dew_point" ATTR_FORECAST_CLOUD_COVERAGE: Final = "cloud_coverage" ATTR_FORECAST_UV_INDEX: Final = "uv_index" -ENTITY_ID_FORMAT = DOMAIN + ".{}" - -SCAN_INTERVAL = timedelta(seconds=30) - ROUNDING_PRECISION = 2 SERVICE_GET_FORECASTS: Final = "get_forecasts" From 862cd76f893c5db076f340b38dfe59cd78bc1731 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 26 Jun 2024 15:49:34 +0200 Subject: [PATCH 1293/1445] Add explanatory comment in tests/patch_time.py (#120572) --- tests/patch_time.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/patch_time.py b/tests/patch_time.py index c8052b3b8ac..a93d3c8ec4f 100644 --- a/tests/patch_time.py +++ b/tests/patch_time.py @@ -25,4 +25,5 @@ dt_util.utcnow = _utcnow # type: ignore[assignment] event_helper.time_tracker_utcnow = _utcnow # type: ignore[assignment] util.utcnow = _utcnow # type: ignore[assignment] +# Replace bound methods which are not found by freezegun runner.monotonic = _monotonic # type: ignore[assignment] From 30a3e9af2bfefa1a4c6c9720743e358f52dff5b6 Mon Sep 17 00:00:00 2001 From: treetip Date: Wed, 26 Jun 2024 16:54:13 +0300 Subject: [PATCH 1294/1445] Add profile duration sensor for Vallox integration (#120240) --- homeassistant/components/vallox/sensor.py | 21 +++++++++ homeassistant/components/vallox/strings.json | 3 ++ tests/components/vallox/conftest.py | 6 +-- tests/components/vallox/test_sensor.py | 45 ++++++++++++++++++++ 4 files changed, 72 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/vallox/sensor.py b/homeassistant/components/vallox/sensor.py index 281bc002f68..0bb509a9c5a 100644 --- a/homeassistant/components/vallox/sensor.py +++ b/homeassistant/components/vallox/sensor.py @@ -18,6 +18,7 @@ from homeassistant.const import ( REVOLUTIONS_PER_MINUTE, EntityCategory, UnitOfTemperature, + UnitOfTime, ) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -127,6 +128,18 @@ class ValloxCellStateSensor(ValloxSensorEntity): return VALLOX_CELL_STATE_TO_STR.get(super_native_value) +class ValloxProfileDurationSensor(ValloxSensorEntity): + """Child class for profile duration reporting.""" + + @property + def native_value(self) -> StateType: + """Return the value reported by the sensor.""" + + return self.coordinator.data.get_remaining_profile_duration( + self.coordinator.data.profile + ) + + @dataclass(frozen=True) class ValloxSensorEntityDescription(SensorEntityDescription): """Describes Vallox sensor entity.""" @@ -253,6 +266,14 @@ SENSOR_ENTITIES: tuple[ValloxSensorEntityDescription, ...] = ( native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, entity_registry_enabled_default=False, ), + ValloxSensorEntityDescription( + key="profile_duration", + translation_key="profile_duration", + device_class=SensorDeviceClass.DURATION, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfTime.MINUTES, + entity_type=ValloxProfileDurationSensor, + ), ) diff --git a/homeassistant/components/vallox/strings.json b/homeassistant/components/vallox/strings.json index 072b59b78e0..4df57b81bb5 100644 --- a/homeassistant/components/vallox/strings.json +++ b/homeassistant/components/vallox/strings.json @@ -87,6 +87,9 @@ }, "efficiency": { "name": "Efficiency" + }, + "profile_duration": { + "name": "Profile duration" } }, "switch": { diff --git a/tests/components/vallox/conftest.py b/tests/components/vallox/conftest.py index 9f65734b926..a6ea95944b3 100644 --- a/tests/components/vallox/conftest.py +++ b/tests/components/vallox/conftest.py @@ -112,9 +112,9 @@ def default_metrics(): "A_CYC_UUID5": 10, "A_CYC_UUID6": 11, "A_CYC_UUID7": 12, - "A_CYC_BOOST_TIMER": 30, - "A_CYC_FIREPLACE_TIMER": 30, - "A_CYC_EXTRA_TIMER": 30, + "A_CYC_BOOST_TIMER": 0, + "A_CYC_FIREPLACE_TIMER": 0, + "A_CYC_EXTRA_TIMER": 0, "A_CYC_MODE": 0, "A_CYC_STATE": 0, "A_CYC_FILTER_CHANGED_YEAR": 24, diff --git a/tests/components/vallox/test_sensor.py b/tests/components/vallox/test_sensor.py index d7af7bbb576..dd8d8026d06 100644 --- a/tests/components/vallox/test_sensor.py +++ b/tests/components/vallox/test_sensor.py @@ -135,3 +135,48 @@ async def test_cell_state_sensor( # Assert sensor = hass.states.get("sensor.vallox_cell_state") assert sensor.state == expected_state + + +@pytest.mark.parametrize( + ("metrics", "expected_state"), + [ + ( + {"A_CYC_STATE": 0}, + "unknown", + ), + ( + {"A_CYC_STATE": 1}, + "unknown", + ), + ( + {"A_CYC_EXTRA_TIMER": 10}, + "10", + ), + ( + {"A_CYC_FIREPLACE_TIMER": 9}, + "9", + ), + ( + {"A_CYC_BOOST_TIMER": 8}, + "8", + ), + ], +) +async def test_profile_duration_sensor( + metrics, + expected_state, + mock_entry: MockConfigEntry, + hass: HomeAssistant, + setup_fetch_metric_data_mock, +) -> None: + """Test profile sensor in different states.""" + # Arrange + setup_fetch_metric_data_mock(metrics=metrics) + + # Act + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + + # Assert + sensor = hass.states.get("sensor.vallox_profile_duration") + assert sensor.state == expected_state From 3d5d4f8ddb2ab0751c21638a6928a598f7510d70 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Wed, 26 Jun 2024 16:06:35 +0200 Subject: [PATCH 1295/1445] Add config flow to statistics (#120496) --- .../components/statistics/__init__.py | 21 ++ .../components/statistics/config_flow.py | 150 ++++++++++ .../components/statistics/manifest.json | 2 + homeassistant/components/statistics/sensor.py | 37 +++ .../components/statistics/strings.json | 111 +++++++ homeassistant/generated/config_flows.py | 1 + homeassistant/generated/integrations.json | 12 +- tests/components/statistics/conftest.py | 90 ++++++ .../components/statistics/test_config_flow.py | 273 ++++++++++++++++++ tests/components/statistics/test_init.py | 17 ++ tests/components/statistics/test_sensor.py | 43 ++- 11 files changed, 749 insertions(+), 8 deletions(-) create mode 100644 homeassistant/components/statistics/config_flow.py create mode 100644 tests/components/statistics/conftest.py create mode 100644 tests/components/statistics/test_config_flow.py create mode 100644 tests/components/statistics/test_init.py diff --git a/homeassistant/components/statistics/__init__.py b/homeassistant/components/statistics/__init__.py index a6419c2fb4d..70739c618f7 100644 --- a/homeassistant/components/statistics/__init__.py +++ b/homeassistant/components/statistics/__init__.py @@ -1,6 +1,27 @@ """The statistics component.""" +from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform +from homeassistant.core import HomeAssistant DOMAIN = "statistics" PLATFORMS = [Platform.SENSOR] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Statistics from a config entry.""" + + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + entry.async_on_unload(entry.add_update_listener(update_listener)) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload Statistics config entry.""" + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + + +async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Handle options update.""" + await hass.config_entries.async_reload(entry.entry_id) diff --git a/homeassistant/components/statistics/config_flow.py b/homeassistant/components/statistics/config_flow.py new file mode 100644 index 00000000000..773c3d1c364 --- /dev/null +++ b/homeassistant/components/statistics/config_flow.py @@ -0,0 +1,150 @@ +"""Config flow for statistics.""" + +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, cast + +import voluptuous as vol + +from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.const import CONF_ENTITY_ID, CONF_NAME +from homeassistant.core import split_entity_id +from homeassistant.helpers.schema_config_entry_flow import ( + SchemaCommonFlowHandler, + SchemaConfigFlowHandler, + SchemaFlowError, + SchemaFlowFormStep, +) +from homeassistant.helpers.selector import ( + BooleanSelector, + DurationSelector, + DurationSelectorConfig, + EntitySelector, + EntitySelectorConfig, + NumberSelector, + NumberSelectorConfig, + NumberSelectorMode, + SelectSelector, + SelectSelectorConfig, + SelectSelectorMode, + TextSelector, +) + +from . import DOMAIN +from .sensor import ( + CONF_KEEP_LAST_SAMPLE, + CONF_MAX_AGE, + CONF_PERCENTILE, + CONF_PRECISION, + CONF_SAMPLES_MAX_BUFFER_SIZE, + CONF_STATE_CHARACTERISTIC, + DEFAULT_NAME, + DEFAULT_PRECISION, + STATS_BINARY_SUPPORT, + STATS_NUMERIC_SUPPORT, +) + + +async def get_state_characteristics(handler: SchemaCommonFlowHandler) -> vol.Schema: + """Return schema with state characteristics.""" + is_binary = ( + split_entity_id(handler.options[CONF_ENTITY_ID])[0] == BINARY_SENSOR_DOMAIN + ) + if is_binary: + options = STATS_BINARY_SUPPORT + else: + options = STATS_NUMERIC_SUPPORT + + return vol.Schema( + { + vol.Required(CONF_STATE_CHARACTERISTIC): SelectSelector( + SelectSelectorConfig( + options=list(options), + translation_key=CONF_STATE_CHARACTERISTIC, + sort=True, + mode=SelectSelectorMode.DROPDOWN, + ) + ), + } + ) + + +async def validate_options( + handler: SchemaCommonFlowHandler, user_input: dict[str, Any] +) -> dict[str, Any]: + """Validate options selected.""" + if ( + user_input.get(CONF_SAMPLES_MAX_BUFFER_SIZE) is None + and user_input.get(CONF_MAX_AGE) is None + ): + raise SchemaFlowError("missing_max_age_or_sampling_size") + + if ( + user_input.get(CONF_KEEP_LAST_SAMPLE) is True + and user_input.get(CONF_MAX_AGE) is None + ): + raise SchemaFlowError("missing_keep_last_sample") + + handler.parent_handler._async_abort_entries_match({**handler.options, **user_input}) # noqa: SLF001 + + return user_input + + +DATA_SCHEMA_SETUP = vol.Schema( + { + vol.Required(CONF_NAME, default=DEFAULT_NAME): TextSelector(), + vol.Required(CONF_ENTITY_ID): EntitySelector( + EntitySelectorConfig(domain=[BINARY_SENSOR_DOMAIN, SENSOR_DOMAIN]) + ), + } +) +DATA_SCHEMA_OPTIONS = vol.Schema( + { + vol.Optional(CONF_SAMPLES_MAX_BUFFER_SIZE): NumberSelector( + NumberSelectorConfig(min=0, step=1, mode=NumberSelectorMode.BOX) + ), + vol.Optional(CONF_MAX_AGE): DurationSelector( + DurationSelectorConfig(enable_day=False, allow_negative=False) + ), + vol.Optional(CONF_KEEP_LAST_SAMPLE, default=False): BooleanSelector(), + vol.Optional(CONF_PERCENTILE, default=50): NumberSelector( + NumberSelectorConfig(min=1, max=99, step=1, mode=NumberSelectorMode.BOX) + ), + vol.Optional(CONF_PRECISION, default=DEFAULT_PRECISION): NumberSelector( + NumberSelectorConfig(min=0, step=1, mode=NumberSelectorMode.BOX) + ), + } +) + +CONFIG_FLOW = { + "user": SchemaFlowFormStep( + schema=DATA_SCHEMA_SETUP, + next_step="state_characteristic", + ), + "state_characteristic": SchemaFlowFormStep( + schema=get_state_characteristics, next_step="options" + ), + "options": SchemaFlowFormStep( + schema=DATA_SCHEMA_OPTIONS, + validate_user_input=validate_options, + ), +} +OPTIONS_FLOW = { + "init": SchemaFlowFormStep( + DATA_SCHEMA_OPTIONS, + validate_user_input=validate_options, + ), +} + + +class StatisticsConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN): + """Handle a config flow for Statistics.""" + + config_flow = CONFIG_FLOW + options_flow = OPTIONS_FLOW + + def async_config_entry_title(self, options: Mapping[str, Any]) -> str: + """Return config entry title.""" + return cast(str, options[CONF_NAME]) diff --git a/homeassistant/components/statistics/manifest.json b/homeassistant/components/statistics/manifest.json index 04b5277ecf5..24d4b4914cb 100644 --- a/homeassistant/components/statistics/manifest.json +++ b/homeassistant/components/statistics/manifest.json @@ -3,7 +3,9 @@ "name": "Statistics", "after_dependencies": ["recorder"], "codeowners": ["@ThomDietrich"], + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/statistics", + "integration_type": "helper", "iot_class": "local_polling", "quality_scale": "internal" } diff --git a/homeassistant/components/statistics/sensor.py b/homeassistant/components/statistics/sensor.py index eb4df4d98b2..8d28254ad61 100644 --- a/homeassistant/components/statistics/sensor.py +++ b/homeassistant/components/statistics/sensor.py @@ -22,6 +22,7 @@ from homeassistant.components.sensor import ( SensorEntity, SensorStateClass, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_UNIT_OF_MEASUREMENT, @@ -282,6 +283,42 @@ async def async_setup_platform( ) +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Statistics sensor entry.""" + sampling_size = entry.options.get(CONF_SAMPLES_MAX_BUFFER_SIZE) + if sampling_size: + sampling_size = int(sampling_size) + + max_age = None + if max_age_input := entry.options.get(CONF_MAX_AGE): + max_age = timedelta( + hours=max_age_input["hours"], + minutes=max_age_input["minutes"], + seconds=max_age_input["seconds"], + ) + + async_add_entities( + [ + StatisticsSensor( + source_entity_id=entry.options[CONF_ENTITY_ID], + name=entry.options[CONF_NAME], + unique_id=entry.entry_id, + state_characteristic=entry.options[CONF_STATE_CHARACTERISTIC], + samples_max_buffer_size=sampling_size, + samples_max_age=max_age, + samples_keep_last=entry.options[CONF_KEEP_LAST_SAMPLE], + precision=int(entry.options[CONF_PRECISION]), + percentile=int(entry.options[CONF_PERCENTILE]), + ) + ], + True, + ) + + class StatisticsSensor(SensorEntity): """Representation of a Statistics sensor.""" diff --git a/homeassistant/components/statistics/strings.json b/homeassistant/components/statistics/strings.json index 6d7bda36fae..5f32b203bfd 100644 --- a/homeassistant/components/statistics/strings.json +++ b/homeassistant/components/statistics/strings.json @@ -1,4 +1,115 @@ { + "config": { + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_account%]" + }, + "error": { + "missing_max_age_or_sampling_size": "The sensor configuration must provide 'max_age' and/or 'sampling_size'", + "missing_keep_last_sample": "The sensor configuration must provide 'max_age' if 'keep_last_sample' is True" + }, + "step": { + "user": { + "description": "Add a statistics sensor", + "data": { + "name": "[%key:common::config_flow::data::name%]", + "entity_id": "Entity" + }, + "data_description": { + "name": "Name for the created entity.", + "entity_id": "Entity to get statistics from." + } + }, + "state_characteristic": { + "description": "Read the documention for further details on available options and how to use them.", + "data": { + "state_characteristic": "State_characteristic" + }, + "data_description": { + "state_characteristic": "The characteristic that should be used as the state of the statistics sensor." + } + }, + "options": { + "description": "Read the documention for further details on how to configure the statistics sensor using these options.", + "data": { + "sampling_size": "Sampling size", + "max_age": "Max age", + "keep_last_sample": "Keep last sample", + "percentile": "Percentile", + "precision": "Precision" + }, + "data_description": { + "sampling_size": "Maximum number of source sensor measurements stored.", + "max_age": "Maximum age of source sensor measurements stored.", + "keep_last_sample": "Defines whether the most recent sampled value should be preserved regardless of the 'max age' setting.", + "percentile": "Only relevant in combination with the 'percentile' characteristic. Must be a value between 1 and 99.", + "precision": "Defines the number of decimal places of the calculated sensor value." + } + } + } + }, + "options": { + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_account%]" + }, + "error": { + "missing_max_age_or_sampling_size": "[%key:component::statistics::config::error::missing_max_age_or_sampling_size%]", + "missing_keep_last_sample": "[%key:component::statistics::config::error::missing_keep_last_sample%]" + }, + "step": { + "init": { + "description": "[%key:component::statistics::config::step::options::description%]", + "data": { + "sampling_size": "[%key:component::statistics::config::step::options::data::sampling_size%]", + "max_age": "[%key:component::statistics::config::step::options::data::max_age%]", + "keep_last_sample": "[%key:component::statistics::config::step::options::data::keep_last_sample%]", + "percentile": "[%key:component::statistics::config::step::options::data::percentile%]", + "precision": "[%key:component::statistics::config::step::options::data::precision%]" + }, + "data_description": { + "sampling_size": "[%key:component::statistics::config::step::options::data_description::sampling_size%]", + "max_age": "[%key:component::statistics::config::step::options::data_description::max_age%]", + "keep_last_sample": "[%key:component::statistics::config::step::options::data_description::keep_last_sample%]", + "percentile": "[%key:component::statistics::config::step::options::data_description::percentile%]", + "precision": "[%key:component::statistics::config::step::options::data_description::precision%]" + } + } + } + }, + "selector": { + "state_characteristic": { + "options": { + "average_linear": "Average linear", + "average_step": "Average step", + "average_timeless": "Average timeless", + "change": "Change", + "change_sample": "Change sample", + "change_second": "Change second", + "count": "Count", + "count_on": "Count on", + "count_off": "Count off", + "datetime_newest": "Newest datetime", + "datetime_oldest": "Oldest datetime", + "datetime_value_max": "Max value datetime", + "datetime_value_min": "Min value datetime", + "distance_95_percent_of_values": "Distance 95% of values", + "distance_99_percent_of_values": "Distance 99% of values", + "distance_absolute": "Absolute distance", + "mean": "Mean", + "mean_circular": "Mean circular", + "median": "Median", + "noisiness": "Noisiness", + "percentile": "Percentile", + "standard_deviation": "Standard deviation", + "sum": "Sum", + "sum_differences": "Sum of differences", + "sum_differences_nonnegative": "Sum of differences non-negative", + "total": "Total", + "value_max": "Max value", + "value_min": "Min value", + "variance": "Variance" + } + } + }, "services": { "reload": { "name": "[%key:common::action::reload%]", diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index e5eeeb29403..23a13bcbfd8 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -12,6 +12,7 @@ FLOWS = { "integration", "min_max", "random", + "statistics", "switch_as_x", "template", "threshold", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index e98df79d096..3371c8de0fa 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -5772,12 +5772,6 @@ "config_flow": false, "iot_class": "cloud_polling" }, - "statistics": { - "name": "Statistics", - "integration_type": "hub", - "config_flow": false, - "iot_class": "local_polling" - }, "statsd": { "name": "StatsD", "integration_type": "hub", @@ -7213,6 +7207,12 @@ "integration_type": "helper", "config_flow": false }, + "statistics": { + "name": "Statistics", + "integration_type": "helper", + "config_flow": true, + "iot_class": "local_polling" + }, "switch_as_x": { "integration_type": "helper", "config_flow": true, diff --git a/tests/components/statistics/conftest.py b/tests/components/statistics/conftest.py new file mode 100644 index 00000000000..e62488c4cf6 --- /dev/null +++ b/tests/components/statistics/conftest.py @@ -0,0 +1,90 @@ +"""Fixtures for the Statistics integration.""" + +from __future__ import annotations + +from collections.abc import Generator +from typing import Any +from unittest.mock import AsyncMock, patch + +import pytest + +from homeassistant.components.statistics import DOMAIN +from homeassistant.components.statistics.sensor import ( + CONF_KEEP_LAST_SAMPLE, + CONF_MAX_AGE, + CONF_PERCENTILE, + CONF_PRECISION, + CONF_SAMPLES_MAX_BUFFER_SIZE, + CONF_STATE_CHARACTERISTIC, + DEFAULT_NAME, + STAT_AVERAGE_LINEAR, +) +from homeassistant.config_entries import SOURCE_USER +from homeassistant.const import ( + ATTR_UNIT_OF_MEASUREMENT, + CONF_ENTITY_ID, + CONF_NAME, + UnitOfTemperature, +) +from homeassistant.core import HomeAssistant + +from .test_sensor import VALUES_NUMERIC + +from tests.common import MockConfigEntry + + +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock]: + """Automatically path uuid generator.""" + with patch( + "homeassistant.components.statistics.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + yield mock_setup_entry + + +@pytest.fixture(name="get_config") +async def get_config_to_integration_load() -> dict[str, Any]: + """Return configuration. + + To override the config, tests can be marked with: + @pytest.mark.parametrize("get_config", [{...}]) + """ + return { + CONF_NAME: DEFAULT_NAME, + CONF_ENTITY_ID: "sensor.test_monitored", + CONF_STATE_CHARACTERISTIC: STAT_AVERAGE_LINEAR, + CONF_SAMPLES_MAX_BUFFER_SIZE: 20.0, + CONF_MAX_AGE: {"hours": 8, "minutes": 5, "seconds": 5}, + CONF_KEEP_LAST_SAMPLE: False, + CONF_PERCENTILE: 50.0, + CONF_PRECISION: 2.0, + } + + +@pytest.fixture(name="loaded_entry") +async def load_integration( + hass: HomeAssistant, get_config: dict[str, Any] +) -> MockConfigEntry: + """Set up the Statistics integration in Home Assistant.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + source=SOURCE_USER, + options=get_config, + entry_id="1", + ) + + config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + for value in VALUES_NUMERIC: + hass.states.async_set( + "sensor.test_monitored", + str(value), + {ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS}, + ) + await hass.async_block_till_done() + + return config_entry diff --git a/tests/components/statistics/test_config_flow.py b/tests/components/statistics/test_config_flow.py new file mode 100644 index 00000000000..7c9ed5bed47 --- /dev/null +++ b/tests/components/statistics/test_config_flow.py @@ -0,0 +1,273 @@ +"""Test the Scrape config flow.""" + +from __future__ import annotations + +from unittest.mock import AsyncMock + +from homeassistant import config_entries +from homeassistant.components.statistics import DOMAIN +from homeassistant.components.statistics.sensor import ( + CONF_KEEP_LAST_SAMPLE, + CONF_MAX_AGE, + CONF_PERCENTILE, + CONF_PRECISION, + CONF_SAMPLES_MAX_BUFFER_SIZE, + CONF_STATE_CHARACTERISTIC, + DEFAULT_NAME, + STAT_AVERAGE_LINEAR, + STAT_COUNT, +) +from homeassistant.const import CONF_ENTITY_ID, CONF_NAME +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +from tests.common import MockConfigEntry + + +async def test_form_sensor(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: + """Test we get the form for sensor.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["step_id"] == "user" + assert result["type"] is FlowResultType.FORM + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_NAME: DEFAULT_NAME, + CONF_ENTITY_ID: "sensor.test_monitored", + }, + ) + await hass.async_block_till_done() + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_STATE_CHARACTERISTIC: STAT_AVERAGE_LINEAR, + }, + ) + await hass.async_block_till_done() + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_SAMPLES_MAX_BUFFER_SIZE: 20.0, + CONF_MAX_AGE: {"hours": 8, "minutes": 0, "seconds": 0}, + }, + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["version"] == 1 + assert result["options"] == { + CONF_NAME: DEFAULT_NAME, + CONF_ENTITY_ID: "sensor.test_monitored", + CONF_STATE_CHARACTERISTIC: STAT_AVERAGE_LINEAR, + CONF_SAMPLES_MAX_BUFFER_SIZE: 20.0, + CONF_MAX_AGE: {"hours": 8, "minutes": 0, "seconds": 0}, + CONF_KEEP_LAST_SAMPLE: False, + CONF_PERCENTILE: 50.0, + CONF_PRECISION: 2.0, + } + + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_binary_sensor( + hass: HomeAssistant, mock_setup_entry: AsyncMock +) -> None: + """Test we get the form for binary sensor.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["step_id"] == "user" + assert result["type"] is FlowResultType.FORM + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_NAME: DEFAULT_NAME, + CONF_ENTITY_ID: "binary_sensor.test_monitored", + }, + ) + await hass.async_block_till_done() + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_STATE_CHARACTERISTIC: STAT_COUNT, + }, + ) + await hass.async_block_till_done() + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_SAMPLES_MAX_BUFFER_SIZE: 20.0, + CONF_MAX_AGE: {"hours": 8, "minutes": 0, "seconds": 0}, + }, + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["version"] == 1 + assert result["options"] == { + CONF_NAME: DEFAULT_NAME, + CONF_ENTITY_ID: "binary_sensor.test_monitored", + CONF_STATE_CHARACTERISTIC: STAT_COUNT, + CONF_SAMPLES_MAX_BUFFER_SIZE: 20.0, + CONF_MAX_AGE: {"hours": 8, "minutes": 0, "seconds": 0}, + CONF_KEEP_LAST_SAMPLE: False, + CONF_PERCENTILE: 50.0, + CONF_PRECISION: 2.0, + } + + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_options_flow(hass: HomeAssistant, loaded_entry: MockConfigEntry) -> None: + """Test options flow.""" + + result = await hass.config_entries.options.async_init(loaded_entry.entry_id) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "init" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_SAMPLES_MAX_BUFFER_SIZE: 25.0, + CONF_MAX_AGE: {"hours": 16, "minutes": 0, "seconds": 0}, + }, + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["data"] == { + CONF_NAME: DEFAULT_NAME, + CONF_ENTITY_ID: "sensor.test_monitored", + CONF_STATE_CHARACTERISTIC: STAT_AVERAGE_LINEAR, + CONF_SAMPLES_MAX_BUFFER_SIZE: 25.0, + CONF_MAX_AGE: {"hours": 16, "minutes": 0, "seconds": 0}, + CONF_KEEP_LAST_SAMPLE: False, + CONF_PERCENTILE: 50.0, + CONF_PRECISION: 2.0, + } + + await hass.async_block_till_done() + + # Check the entity was updated, no new entity was created + assert len(hass.states.async_all()) == 2 + + state = hass.states.get("sensor.statistical_characteristic") + assert state is not None + + +async def test_validation_options( + hass: HomeAssistant, mock_setup_entry: AsyncMock +) -> None: + """Test validation.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["step_id"] == "user" + assert result["type"] is FlowResultType.FORM + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_NAME: DEFAULT_NAME, + CONF_ENTITY_ID: "sensor.test_monitored", + }, + ) + await hass.async_block_till_done() + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_STATE_CHARACTERISTIC: STAT_AVERAGE_LINEAR, + }, + ) + await hass.async_block_till_done() + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) + await hass.async_block_till_done() + + assert result["step_id"] == "options" + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"base": "missing_max_age_or_sampling_size"} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_KEEP_LAST_SAMPLE: True, CONF_SAMPLES_MAX_BUFFER_SIZE: 20.0}, + ) + await hass.async_block_till_done() + + assert result["step_id"] == "options" + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"base": "missing_keep_last_sample"} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_SAMPLES_MAX_BUFFER_SIZE: 20.0, + CONF_MAX_AGE: {"hours": 8, "minutes": 0, "seconds": 0}, + }, + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["version"] == 1 + assert result["options"] == { + CONF_NAME: DEFAULT_NAME, + CONF_ENTITY_ID: "sensor.test_monitored", + CONF_STATE_CHARACTERISTIC: STAT_AVERAGE_LINEAR, + CONF_SAMPLES_MAX_BUFFER_SIZE: 20.0, + CONF_MAX_AGE: {"hours": 8, "minutes": 0, "seconds": 0}, + CONF_KEEP_LAST_SAMPLE: False, + CONF_PERCENTILE: 50.0, + CONF_PRECISION: 2.0, + } + + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_entry_already_exist( + hass: HomeAssistant, loaded_entry: MockConfigEntry +) -> None: + """Test abort when entry already exist.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["step_id"] == "user" + assert result["type"] is FlowResultType.FORM + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_NAME: DEFAULT_NAME, + CONF_ENTITY_ID: "sensor.test_monitored", + }, + ) + await hass.async_block_till_done() + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_STATE_CHARACTERISTIC: STAT_AVERAGE_LINEAR, + }, + ) + await hass.async_block_till_done() + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_SAMPLES_MAX_BUFFER_SIZE: 20.0, + CONF_MAX_AGE: {"hours": 8, "minutes": 5, "seconds": 5}, + }, + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" diff --git a/tests/components/statistics/test_init.py b/tests/components/statistics/test_init.py new file mode 100644 index 00000000000..6cb943c0687 --- /dev/null +++ b/tests/components/statistics/test_init.py @@ -0,0 +1,17 @@ +"""Test Statistics component setup process.""" + +from __future__ import annotations + +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +async def test_unload_entry(hass: HomeAssistant, loaded_entry: MockConfigEntry) -> None: + """Test unload an entry.""" + + assert loaded_entry.state is ConfigEntryState.LOADED + assert await hass.config_entries.async_unload(loaded_entry.entry_id) + await hass.async_block_till_done() + assert loaded_entry.state is ConfigEntryState.NOT_LOADED diff --git a/tests/components/statistics/test_sensor.py b/tests/components/statistics/test_sensor.py index 5a716fd8ce8..269c17e34b9 100644 --- a/tests/components/statistics/test_sensor.py +++ b/tests/components/statistics/test_sensor.py @@ -19,10 +19,20 @@ from homeassistant.components.sensor import ( SensorStateClass, ) from homeassistant.components.statistics import DOMAIN as STATISTICS_DOMAIN -from homeassistant.components.statistics.sensor import StatisticsSensor +from homeassistant.components.statistics.sensor import ( + CONF_KEEP_LAST_SAMPLE, + CONF_PERCENTILE, + CONF_PRECISION, + CONF_SAMPLES_MAX_BUFFER_SIZE, + CONF_STATE_CHARACTERISTIC, + STAT_MEAN, + StatisticsSensor, +) from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_UNIT_OF_MEASUREMENT, + CONF_ENTITY_ID, + CONF_NAME, DEGREE, SERVICE_RELOAD, STATE_UNAVAILABLE, @@ -35,7 +45,7 @@ from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util -from tests.common import async_fire_time_changed, get_fixture_path +from tests.common import MockConfigEntry, async_fire_time_changed, get_fixture_path from tests.components.recorder.common import async_wait_recording_done VALUES_BINARY = ["on", "off", "on", "off", "on", "off", "on", "off", "on"] @@ -171,6 +181,35 @@ async def test_sensor_defaults_numeric(hass: HomeAssistant) -> None: assert new_state.attributes.get("source_value_valid") is False +@pytest.mark.parametrize( + "get_config", + [ + { + CONF_NAME: "test", + CONF_ENTITY_ID: "sensor.test_monitored", + CONF_STATE_CHARACTERISTIC: STAT_MEAN, + CONF_SAMPLES_MAX_BUFFER_SIZE: 20.0, + CONF_KEEP_LAST_SAMPLE: False, + CONF_PERCENTILE: 50.0, + CONF_PRECISION: 2.0, + } + ], +) +async def test_sensor_loaded_from_config_entry( + hass: HomeAssistant, loaded_entry: MockConfigEntry +) -> None: + """Test the sensor loaded from a config entry.""" + + state = hass.states.get("sensor.test") + assert state is not None + assert state.state == str(round(sum(VALUES_NUMERIC) / len(VALUES_NUMERIC), 2)) + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.CELSIUS + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT + assert state.attributes.get("buffer_usage_ratio") == round(9 / 20, 2) + assert state.attributes.get("source_value_valid") is True + assert "age_coverage_ratio" not in state.attributes + + async def test_sensor_defaults_binary(hass: HomeAssistant) -> None: """Test the general behavior of the sensor, with binary source sensor.""" assert await async_setup_component( From 55101fcc452de271d1332103b4e580277b1772de Mon Sep 17 00:00:00 2001 From: "Mr. Bubbles" Date: Wed, 26 Jun 2024 16:06:55 +0200 Subject: [PATCH 1296/1445] Add platinum scale to pyLoad integration (#120542) Add platinum scale --- homeassistant/components/pyload/manifest.json | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/pyload/manifest.json b/homeassistant/components/pyload/manifest.json index 95e73118c42..fe1888478f8 100644 --- a/homeassistant/components/pyload/manifest.json +++ b/homeassistant/components/pyload/manifest.json @@ -7,5 +7,6 @@ "integration_type": "service", "iot_class": "local_polling", "loggers": ["pyloadapi"], + "quality_scale": "platinum", "requirements": ["PyLoadAPI==1.2.0"] } From 32c07180f6e9b6bd8b93d1de3708e814c8be1508 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk <11290930+bouwew@users.noreply.github.com> Date: Wed, 26 Jun 2024 16:08:32 +0200 Subject: [PATCH 1297/1445] Delete removed device(s) at runtime in Plugwise (#120296) --- .../components/plugwise/coordinator.py | 46 +++++++++++++++++-- tests/components/plugwise/test_init.py | 29 +++++++++++- 2 files changed, 71 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/plugwise/coordinator.py b/homeassistant/components/plugwise/coordinator.py index bc12ef4443b..8958ecae930 100644 --- a/homeassistant/components/plugwise/coordinator.py +++ b/homeassistant/components/plugwise/coordinator.py @@ -16,11 +16,12 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryError +from homeassistant.helpers import device_registry as dr from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.debounce import Debouncer from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from .const import DEFAULT_PORT, DEFAULT_USERNAME, DOMAIN, LOGGER +from .const import DEFAULT_PORT, DEFAULT_USERNAME, DOMAIN, GATEWAY_ID, LOGGER class PlugwiseDataUpdateCoordinator(DataUpdateCoordinator[PlugwiseData]): @@ -83,7 +84,46 @@ class PlugwiseDataUpdateCoordinator(DataUpdateCoordinator[PlugwiseData]): except UnsupportedDeviceError as err: raise ConfigEntryError("Device with unsupported firmware") from err else: - self.new_devices = set(data.devices) - self._current_devices - self._current_devices = set(data.devices) + self._async_add_remove_devices(data, self.config_entry) return data + + def _async_add_remove_devices(self, data: PlugwiseData, entry: ConfigEntry) -> None: + """Add new Plugwise devices, remove non-existing devices.""" + # Check for new or removed devices + self.new_devices = set(data.devices) - self._current_devices + removed_devices = self._current_devices - set(data.devices) + self._current_devices = set(data.devices) + + if removed_devices: + self._async_remove_devices(data, entry) + + def _async_remove_devices(self, data: PlugwiseData, entry: ConfigEntry) -> None: + """Clean registries when removed devices found.""" + device_reg = dr.async_get(self.hass) + device_list = dr.async_entries_for_config_entry( + device_reg, self.config_entry.entry_id + ) + # via_device cannot be None, this will result in the deletion + # of other Plugwise Gateways when present! + via_device: str = "" + for device_entry in device_list: + if device_entry.identifiers: + item = list(list(device_entry.identifiers)[0]) + if item[0] == DOMAIN: + # First find the Plugwise via_device, this is always the first device + if item[1] == data.gateway[GATEWAY_ID]: + via_device = device_entry.id + elif ( # then remove the connected orphaned device(s) + device_entry.via_device_id == via_device + and item[1] not in data.devices + ): + device_reg.async_update_device( + device_entry.id, remove_config_entry_id=entry.entry_id + ) + LOGGER.debug( + "Removed %s device %s %s from device_registry", + DOMAIN, + device_entry.model, + item[1], + ) diff --git a/tests/components/plugwise/test_init.py b/tests/components/plugwise/test_init.py index d3f23a18285..26aedf864dc 100644 --- a/tests/components/plugwise/test_init.py +++ b/tests/components/plugwise/test_init.py @@ -39,7 +39,7 @@ TOM = { "hardware": "1", "location": "f871b8c4d63549319221e294e4f88074", "model": "Tom/Floor", - "name": "Tom Badkamer", + "name": "Tom Zolder", "sensors": { "battery": 99, "temperature": 18.6, @@ -258,3 +258,30 @@ async def test_update_device( for device_entry in list(device_registry.devices.values()): item_list.extend(x[1] for x in device_entry.identifiers) assert "01234567890abcdefghijklmnopqrstu" in item_list + + # Remove the existing Tom/Floor + data.devices.pop("1772a4ea304041adb83f357b751341ff") + with patch(HA_PLUGWISE_SMILE_ASYNC_UPDATE, return_value=data): + async_fire_time_changed(hass, utcnow + timedelta(minutes=1)) + await hass.async_block_till_done() + + assert ( + len( + er.async_entries_for_config_entry( + entity_registry, mock_config_entry.entry_id + ) + ) + == 29 + ) + assert ( + len( + dr.async_entries_for_config_entry( + device_registry, mock_config_entry.entry_id + ) + ) + == 6 + ) + item_list: list[str] = [] + for device_entry in list(device_registry.devices.values()): + item_list.extend(x[1] for x in device_entry.identifiers) + assert "1772a4ea304041adb83f357b751341ff" not in item_list From 713abf4c6bbed9b1ae33a69277244d053f8e6cdb Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 26 Jun 2024 16:09:20 +0200 Subject: [PATCH 1298/1445] Refactor PLATFORM_SCHEMA imports in tests (#120566) --- tests/test_setup.py | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/tests/test_setup.py b/tests/test_setup.py index 4ff0f465e21..1e19f1a7b76 100644 --- a/tests/test_setup.py +++ b/tests/test_setup.py @@ -12,11 +12,7 @@ from homeassistant import config_entries, loader, setup from homeassistant.const import EVENT_COMPONENT_LOADED, EVENT_HOMEASSISTANT_START from homeassistant.core import CoreState, HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import discovery, translation -from homeassistant.helpers.config_validation import ( - PLATFORM_SCHEMA, - PLATFORM_SCHEMA_BASE, -) +from homeassistant.helpers import config_validation as cv, discovery, translation from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, @@ -88,8 +84,8 @@ async def test_validate_platform_config( hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: """Test validating platform configuration.""" - platform_schema = PLATFORM_SCHEMA.extend({"hello": str}) - platform_schema_base = PLATFORM_SCHEMA_BASE.extend({}) + platform_schema = cv.PLATFORM_SCHEMA.extend({"hello": str}) + platform_schema_base = cv.PLATFORM_SCHEMA_BASE.extend({}) mock_integration( hass, MockModule("platform_conf", platform_schema_base=platform_schema_base), @@ -149,8 +145,8 @@ async def test_validate_platform_config_2( hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: """Test component PLATFORM_SCHEMA_BASE prio over PLATFORM_SCHEMA.""" - platform_schema = PLATFORM_SCHEMA.extend({"hello": str}) - platform_schema_base = PLATFORM_SCHEMA_BASE.extend({"hello": "world"}) + platform_schema = cv.PLATFORM_SCHEMA.extend({"hello": str}) + platform_schema_base = cv.PLATFORM_SCHEMA_BASE.extend({"hello": "world"}) mock_integration( hass, MockModule( @@ -183,8 +179,8 @@ async def test_validate_platform_config_3( hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: """Test fallback to component PLATFORM_SCHEMA.""" - component_schema = PLATFORM_SCHEMA_BASE.extend({"hello": str}) - platform_schema = PLATFORM_SCHEMA.extend({"cheers": str, "hello": "world"}) + component_schema = cv.PLATFORM_SCHEMA_BASE.extend({"hello": str}) + platform_schema = cv.PLATFORM_SCHEMA.extend({"cheers": str, "hello": "world"}) mock_integration( hass, MockModule("platform_conf", platform_schema=component_schema) ) @@ -210,8 +206,8 @@ async def test_validate_platform_config_3( async def test_validate_platform_config_4(hass: HomeAssistant) -> None: """Test entity_namespace in PLATFORM_SCHEMA.""" - component_schema = PLATFORM_SCHEMA_BASE - platform_schema = PLATFORM_SCHEMA + component_schema = cv.PLATFORM_SCHEMA_BASE + platform_schema = cv.PLATFORM_SCHEMA mock_integration( hass, MockModule("platform_conf", platform_schema_base=component_schema), @@ -386,7 +382,9 @@ async def test_component_setup_with_validation_and_dependency( async def test_platform_specific_config_validation(hass: HomeAssistant) -> None: """Test platform that specifies config.""" - platform_schema = PLATFORM_SCHEMA.extend({"valid": True}, extra=vol.PREVENT_EXTRA) + platform_schema = cv.PLATFORM_SCHEMA.extend( + {"valid": True}, extra=vol.PREVENT_EXTRA + ) mock_setup = Mock(spec_set=True) @@ -533,7 +531,7 @@ async def test_component_warn_slow_setup(hass: HomeAssistant) -> None: async def test_platform_no_warn_slow(hass: HomeAssistant) -> None: """Do not warn for long entity setup time.""" mock_integration( - hass, MockModule("test_component1", platform_schema=PLATFORM_SCHEMA) + hass, MockModule("test_component1", platform_schema=cv.PLATFORM_SCHEMA) ) with patch.object(hass.loop, "call_later") as mock_call: result = await setup.async_setup_component(hass, "test_component1", {}) From f5c640ee5b490a3eb08c4931dd8ba9d1887ebc11 Mon Sep 17 00:00:00 2001 From: Gerben Jongerius Date: Wed, 26 Jun 2024 16:11:21 +0200 Subject: [PATCH 1299/1445] Add additional tests to youless integration (#118869) --- .coveragerc | 2 - tests/components/youless/__init__.py | 38 + tests/components/youless/fixtures/device.json | 5 + .../components/youless/fixtures/enologic.json | 18 + .../youless/snapshots/test_sensor.ambr | 972 ++++++++++++++++++ tests/components/youless/test_init.py | 18 + tests/components/youless/test_sensor.py | 23 + 7 files changed, 1074 insertions(+), 2 deletions(-) create mode 100644 tests/components/youless/fixtures/device.json create mode 100644 tests/components/youless/fixtures/enologic.json create mode 100644 tests/components/youless/snapshots/test_sensor.ambr create mode 100644 tests/components/youless/test_init.py create mode 100644 tests/components/youless/test_sensor.py diff --git a/.coveragerc b/.coveragerc index 1952297eb5f..0784977ff55 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1690,8 +1690,6 @@ omit = homeassistant/components/yolink/siren.py homeassistant/components/yolink/switch.py homeassistant/components/yolink/valve.py - homeassistant/components/youless/__init__.py - homeassistant/components/youless/sensor.py homeassistant/components/zabbix/* homeassistant/components/zamg/coordinator.py homeassistant/components/zengge/light.py diff --git a/tests/components/youless/__init__.py b/tests/components/youless/__init__.py index 8711c6721bc..8770a7e2dc8 100644 --- a/tests/components/youless/__init__.py +++ b/tests/components/youless/__init__.py @@ -1 +1,39 @@ """Tests for the youless component.""" + +import requests_mock + +from homeassistant.components import youless +from homeassistant.const import CONF_DEVICE, CONF_HOST +from homeassistant.core import HomeAssistant + +from tests.common import ( + MockConfigEntry, + load_json_array_fixture, + load_json_object_fixture, +) + + +async def init_component(hass: HomeAssistant) -> MockConfigEntry: + """Check if the setup of the integration succeeds.""" + with requests_mock.Mocker() as mock: + mock.get( + "http://1.1.1.1/d", + json=load_json_object_fixture("device.json", youless.DOMAIN), + ) + mock.get( + "http://1.1.1.1/e", + json=load_json_array_fixture("enologic.json", youless.DOMAIN), + headers={"Content-Type": "application/json"}, + ) + + entry = MockConfigEntry( + domain=youless.DOMAIN, + title="localhost", + data={CONF_HOST: "1.1.1.1", CONF_DEVICE: "localhost"}, + ) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + return entry diff --git a/tests/components/youless/fixtures/device.json b/tests/components/youless/fixtures/device.json new file mode 100644 index 00000000000..7d089851923 --- /dev/null +++ b/tests/components/youless/fixtures/device.json @@ -0,0 +1,5 @@ +{ + "model": "LS120", + "fw": "1.4.2-EL", + "mac": "de2:2d2:3d23" +} diff --git a/tests/components/youless/fixtures/enologic.json b/tests/components/youless/fixtures/enologic.json new file mode 100644 index 00000000000..0189f43af5e --- /dev/null +++ b/tests/components/youless/fixtures/enologic.json @@ -0,0 +1,18 @@ +[ + { + "tm": 1611929119, + "net": 9194.164, + "pwr": 2382, + "ts0": 1608654000, + "cs0": 0.0, + "ps0": 0, + "p1": 4703.562, + "p2": 4490.631, + "n1": 0.029, + "n2": 0.0, + "gas": 1624.264, + "gts": 0, + "wtr": 1234.564, + "wts": 0 + } +] diff --git a/tests/components/youless/snapshots/test_sensor.ambr b/tests/components/youless/snapshots/test_sensor.ambr new file mode 100644 index 00000000000..22e480c390e --- /dev/null +++ b/tests/components/youless/snapshots/test_sensor.ambr @@ -0,0 +1,972 @@ +# serializer version: 1 +# name: test_sensors[sensor.energy_delivery_high-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.energy_delivery_high', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Energy delivery high', + 'platform': 'youless', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'youless_localhost_delivery_high', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.energy_delivery_high-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Energy delivery high', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.energy_delivery_high', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_sensors[sensor.energy_delivery_low-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.energy_delivery_low', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Energy delivery low', + 'platform': 'youless', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'youless_localhost_delivery_low', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.energy_delivery_low-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Energy delivery low', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.energy_delivery_low', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_sensors[sensor.energy_high-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.energy_high', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Energy high', + 'platform': 'youless', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'youless_localhost_power_high', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.energy_high-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Energy high', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.energy_high', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '4490.631', + }) +# --- +# name: test_sensors[sensor.energy_low-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.energy_low', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Energy low', + 'platform': 'youless', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'youless_localhost_power_low', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.energy_low-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Energy low', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.energy_low', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '4703.562', + }) +# --- +# name: test_sensors[sensor.energy_total-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.energy_total', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Energy total', + 'platform': 'youless', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'youless_localhost_power_total', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.energy_total-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Energy total', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.energy_total', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '9194.164', + }) +# --- +# name: test_sensors[sensor.extra_total-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.extra_total', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Extra total', + 'platform': 'youless', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'youless_localhost_extra_total', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.extra_total-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Extra total', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.extra_total', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_sensors[sensor.extra_usage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.extra_usage', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Extra usage', + 'platform': 'youless', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'youless_localhost_extra_usage', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.extra_usage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Extra usage', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.extra_usage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensors[sensor.gas_usage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.gas_usage', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': 'mdi:fire', + 'original_name': 'Gas usage', + 'platform': 'youless', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'youless_localhost_gas', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.gas_usage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'gas', + 'friendly_name': 'Gas usage', + 'icon': 'mdi:fire', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.gas_usage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1234.564', + }) +# --- +# name: test_sensors[sensor.phase_1_current-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.phase_1_current', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Phase 1 current', + 'platform': 'youless', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'youless_localhost_phase_1_current', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.phase_1_current-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'current', + 'friendly_name': 'Phase 1 current', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.phase_1_current', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_sensors[sensor.phase_1_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.phase_1_power', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Phase 1 power', + 'platform': 'youless', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'youless_localhost_phase_1_power', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.phase_1_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Phase 1 power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.phase_1_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_sensors[sensor.phase_1_voltage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.phase_1_voltage', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Phase 1 voltage', + 'platform': 'youless', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'youless_localhost_phase_1_voltage', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.phase_1_voltage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'Phase 1 voltage', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.phase_1_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_sensors[sensor.phase_2_current-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.phase_2_current', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Phase 2 current', + 'platform': 'youless', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'youless_localhost_phase_2_current', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.phase_2_current-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'current', + 'friendly_name': 'Phase 2 current', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.phase_2_current', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_sensors[sensor.phase_2_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.phase_2_power', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Phase 2 power', + 'platform': 'youless', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'youless_localhost_phase_2_power', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.phase_2_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Phase 2 power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.phase_2_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_sensors[sensor.phase_2_voltage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.phase_2_voltage', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Phase 2 voltage', + 'platform': 'youless', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'youless_localhost_phase_2_voltage', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.phase_2_voltage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'Phase 2 voltage', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.phase_2_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_sensors[sensor.phase_3_current-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.phase_3_current', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Phase 3 current', + 'platform': 'youless', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'youless_localhost_phase_3_current', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.phase_3_current-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'current', + 'friendly_name': 'Phase 3 current', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.phase_3_current', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_sensors[sensor.phase_3_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.phase_3_power', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Phase 3 power', + 'platform': 'youless', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'youless_localhost_phase_3_power', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.phase_3_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Phase 3 power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.phase_3_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_sensors[sensor.phase_3_voltage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.phase_3_voltage', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Phase 3 voltage', + 'platform': 'youless', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'youless_localhost_phase_3_voltage', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.phase_3_voltage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'Phase 3 voltage', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.phase_3_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_sensors[sensor.power_usage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.power_usage', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Power Usage', + 'platform': 'youless', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'youless_localhost_usage', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.power_usage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Power Usage', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.power_usage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2382', + }) +# --- +# name: test_sensors[sensor.water_usage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.water_usage', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': 'mdi:water', + 'original_name': 'Water usage', + 'platform': 'youless', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'youless_localhost_water', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.water_usage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'water', + 'friendly_name': 'Water usage', + 'icon': 'mdi:water', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.water_usage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- diff --git a/tests/components/youless/test_init.py b/tests/components/youless/test_init.py new file mode 100644 index 00000000000..29db8c66af0 --- /dev/null +++ b/tests/components/youless/test_init.py @@ -0,0 +1,18 @@ +"""Test the setup of the Youless integration.""" + +from homeassistant import setup +from homeassistant.components import youless +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant + +from . import init_component + + +async def test_async_setup_entry(hass: HomeAssistant) -> None: + """Check if the setup of the integration succeeds.""" + + entry = await init_component(hass) + + assert await setup.async_setup_component(hass, youless.DOMAIN, {}) + assert entry.state is ConfigEntryState.LOADED + assert len(hass.states.async_entity_ids()) == 19 diff --git a/tests/components/youless/test_sensor.py b/tests/components/youless/test_sensor.py new file mode 100644 index 00000000000..67dff314df7 --- /dev/null +++ b/tests/components/youless/test_sensor.py @@ -0,0 +1,23 @@ +"""Test the sensor classes for youless.""" + +from unittest.mock import patch + +from syrupy import SnapshotAssertion + +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import init_component + +from tests.common import snapshot_platform + + +async def test_sensors( + hass: HomeAssistant, entity_registry: er.EntityRegistry, snapshot: SnapshotAssertion +) -> None: + """Test the sensor classes for youless.""" + with patch("homeassistant.components.youless.PLATFORMS", [Platform.SENSOR]): + entry = await init_component(hass) + + await snapshot_platform(hass, entity_registry, snapshot, entry.entry_id) From 3492171ff8a6291176ddd5bf78c7e91e368e8327 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 26 Jun 2024 16:17:57 +0200 Subject: [PATCH 1300/1445] Bump version to 2024.7.0b0 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 3a970aefd38..54d7f26a5f0 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -24,7 +24,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 7 -PATCH_VERSION: Final = "0.dev0" +PATCH_VERSION: Final = "0b0" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0) diff --git a/pyproject.toml b/pyproject.toml index 4edb1535411..0b490d621a3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.7.0.dev0" +version = "2024.7.0b0" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From ba456f256448f6a5d9e9b23f2b64c51c9eb5883a Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Wed, 26 Jun 2024 21:53:02 +0300 Subject: [PATCH 1301/1445] Align Shelly sleeping devices timeout with non-sleeping (#118969) --- homeassistant/components/shelly/const.py | 4 +--- homeassistant/components/shelly/coordinator.py | 9 ++++----- tests/components/shelly/test_binary_sensor.py | 4 ++-- tests/components/shelly/test_coordinator.py | 9 ++++----- 4 files changed, 11 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/shelly/const.py b/homeassistant/components/shelly/const.py index fcc7cc44af9..c5bdb88bbd1 100644 --- a/homeassistant/components/shelly/const.py +++ b/homeassistant/components/shelly/const.py @@ -83,11 +83,9 @@ REST_SENSORS_UPDATE_INTERVAL: Final = 60 # Refresh interval for RPC polling sensors RPC_SENSORS_POLLING_INTERVAL: Final = 60 -# Multiplier used to calculate the "update_interval" for sleeping devices. -SLEEP_PERIOD_MULTIPLIER: Final = 1.2 CONF_SLEEP_PERIOD: Final = "sleep_period" -# Multiplier used to calculate the "update_interval" for non-sleeping devices. +# Multiplier used to calculate the "update_interval" for shelly devices. UPDATE_PERIOD_MULTIPLIER: Final = 2.2 # Reconnect interval for GEN2 devices diff --git a/homeassistant/components/shelly/coordinator.py b/homeassistant/components/shelly/coordinator.py index 82d358b33d8..a4ff34f7d9a 100644 --- a/homeassistant/components/shelly/coordinator.py +++ b/homeassistant/components/shelly/coordinator.py @@ -54,7 +54,6 @@ from .const import ( RPC_RECONNECT_INTERVAL, RPC_SENSORS_POLLING_INTERVAL, SHBTN_MODELS, - SLEEP_PERIOD_MULTIPLIER, UPDATE_PERIOD_MULTIPLIER, BLEScannerMode, ) @@ -229,7 +228,7 @@ class ShellyBlockCoordinator(ShellyCoordinatorBase[BlockDevice]): """Initialize the Shelly block device coordinator.""" self.entry = entry if self.sleep_period: - update_interval = SLEEP_PERIOD_MULTIPLIER * self.sleep_period + update_interval = UPDATE_PERIOD_MULTIPLIER * self.sleep_period else: update_interval = ( UPDATE_PERIOD_MULTIPLIER * device.settings["coiot"]["update_period"] @@ -429,7 +428,7 @@ class ShellyRestCoordinator(ShellyCoordinatorBase[BlockDevice]): in BATTERY_DEVICES_WITH_PERMANENT_CONNECTION ): update_interval = ( - SLEEP_PERIOD_MULTIPLIER * device.settings["coiot"]["update_period"] + UPDATE_PERIOD_MULTIPLIER * device.settings["coiot"]["update_period"] ) super().__init__(hass, entry, device, update_interval) @@ -459,7 +458,7 @@ class ShellyRpcCoordinator(ShellyCoordinatorBase[RpcDevice]): """Initialize the Shelly RPC device coordinator.""" self.entry = entry if self.sleep_period: - update_interval = SLEEP_PERIOD_MULTIPLIER * self.sleep_period + update_interval = UPDATE_PERIOD_MULTIPLIER * self.sleep_period else: update_interval = RPC_RECONNECT_INTERVAL super().__init__(hass, entry, device, update_interval) @@ -486,7 +485,7 @@ class ShellyRpcCoordinator(ShellyCoordinatorBase[RpcDevice]): data[CONF_SLEEP_PERIOD] = wakeup_period self.hass.config_entries.async_update_entry(self.entry, data=data) - update_interval = SLEEP_PERIOD_MULTIPLIER * wakeup_period + update_interval = UPDATE_PERIOD_MULTIPLIER * wakeup_period self.update_interval = timedelta(seconds=update_interval) return True diff --git a/tests/components/shelly/test_binary_sensor.py b/tests/components/shelly/test_binary_sensor.py index 026a7041863..3bfbf350f7e 100644 --- a/tests/components/shelly/test_binary_sensor.py +++ b/tests/components/shelly/test_binary_sensor.py @@ -7,7 +7,7 @@ from freezegun.api import FrozenDateTimeFactory import pytest from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN -from homeassistant.components.shelly.const import SLEEP_PERIOD_MULTIPLIER +from homeassistant.components.shelly.const import UPDATE_PERIOD_MULTIPLIER from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNKNOWN from homeassistant.core import HomeAssistant, State from homeassistant.helpers.device_registry import DeviceRegistry @@ -122,7 +122,7 @@ async def test_block_rest_binary_sensor_connected_battery_devices( assert hass.states.get(entity_id).state == STATE_OFF # Verify update on slow intervals - await mock_rest_update(hass, freezer, seconds=SLEEP_PERIOD_MULTIPLIER * 3600) + await mock_rest_update(hass, freezer, seconds=UPDATE_PERIOD_MULTIPLIER * 3600) assert hass.states.get(entity_id).state == STATE_ON entry = entity_registry.async_get(entity_id) diff --git a/tests/components/shelly/test_coordinator.py b/tests/components/shelly/test_coordinator.py index 1e0af115c9e..35123a2db91 100644 --- a/tests/components/shelly/test_coordinator.py +++ b/tests/components/shelly/test_coordinator.py @@ -20,7 +20,6 @@ from homeassistant.components.shelly.const import ( ENTRY_RELOAD_COOLDOWN, MAX_PUSH_UPDATE_FAILURES, RPC_RECONNECT_INTERVAL, - SLEEP_PERIOD_MULTIPLIER, UPDATE_PERIOD_MULTIPLIER, BLEScannerMode, ) @@ -564,7 +563,7 @@ async def test_rpc_update_entry_sleep_period( # Move time to generate sleep period update monkeypatch.setitem(mock_rpc_device.status["sys"], "wakeup_period", 3600) - freezer.tick(timedelta(seconds=600 * SLEEP_PERIOD_MULTIPLIER)) + freezer.tick(timedelta(seconds=600 * UPDATE_PERIOD_MULTIPLIER)) async_fire_time_changed(hass) await hass.async_block_till_done(wait_background_tasks=True) @@ -596,7 +595,7 @@ async def test_rpc_sleeping_device_no_periodic_updates( assert get_entity_state(hass, entity_id) == "22.9" # Move time to generate polling - freezer.tick(timedelta(seconds=SLEEP_PERIOD_MULTIPLIER * 1000)) + freezer.tick(timedelta(seconds=UPDATE_PERIOD_MULTIPLIER * 1000)) async_fire_time_changed(hass) await hass.async_block_till_done(wait_background_tasks=True) @@ -889,7 +888,7 @@ async def test_block_sleeping_device_connection_error( assert get_entity_state(hass, entity_id) == STATE_ON # Move time to generate sleep period update - freezer.tick(timedelta(seconds=sleep_period * SLEEP_PERIOD_MULTIPLIER)) + freezer.tick(timedelta(seconds=sleep_period * UPDATE_PERIOD_MULTIPLIER)) async_fire_time_changed(hass) await hass.async_block_till_done(wait_background_tasks=True) @@ -934,7 +933,7 @@ async def test_rpc_sleeping_device_connection_error( assert get_entity_state(hass, entity_id) == STATE_ON # Move time to generate sleep period update - freezer.tick(timedelta(seconds=sleep_period * SLEEP_PERIOD_MULTIPLIER)) + freezer.tick(timedelta(seconds=sleep_period * UPDATE_PERIOD_MULTIPLIER)) async_fire_time_changed(hass) await hass.async_block_till_done(wait_background_tasks=True) From 6dd1e09354383cf19eb83946a9722f41b29ee5f5 Mon Sep 17 00:00:00 2001 From: Florian Date: Wed, 26 Jun 2024 21:45:17 +0200 Subject: [PATCH 1302/1445] Don't allow switch toggle when device in locked in AVM FRITZ!SmartHome (#120132) * fix: set state of the FritzBox-Switch to disabled if the option for manuel switching in the userinterface is disabled * feat: raise an error instead of disabling switch * feat: rename method signature * fix: tests * fix: wrong import * feat: Update homeassistant/components/fritzbox/strings.json Update error message Co-authored-by: Michael <35783820+mib1185@users.noreply.github.com> * Update tests/components/fritzbox/test_switch.py feat: update test Co-authored-by: Michael <35783820+mib1185@users.noreply.github.com> * make ruff happy * fix expected error message check --------- Co-authored-by: Michael <35783820+mib1185@users.noreply.github.com> --- .../components/fritzbox/strings.json | 3 ++ homeassistant/components/fritzbox/switch.py | 12 ++++++++ tests/components/fritzbox/__init__.py | 2 +- tests/components/fritzbox/test_switch.py | 30 +++++++++++++++++++ 4 files changed, 46 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/fritzbox/strings.json b/homeassistant/components/fritzbox/strings.json index cee0afa26c1..d4f59fd1c08 100644 --- a/homeassistant/components/fritzbox/strings.json +++ b/homeassistant/components/fritzbox/strings.json @@ -81,6 +81,9 @@ } }, "exceptions": { + "manual_switching_disabled": { + "message": "Can't toggle switch while manual switching is disabled for the device." + }, "change_preset_while_active_mode": { "message": "Can't change preset while holiday or summer mode is active on the device." }, diff --git a/homeassistant/components/fritzbox/switch.py b/homeassistant/components/fritzbox/switch.py index 0bdf7a9f944..d13f21e1c14 100644 --- a/homeassistant/components/fritzbox/switch.py +++ b/homeassistant/components/fritzbox/switch.py @@ -6,9 +6,11 @@ from typing import Any from homeassistant.components.switch import SwitchEntity from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import FritzBoxDeviceEntity +from .const import DOMAIN from .coordinator import FritzboxConfigEntry @@ -48,10 +50,20 @@ class FritzboxSwitch(FritzBoxDeviceEntity, SwitchEntity): async def async_turn_on(self, **kwargs: Any) -> None: """Turn the switch on.""" + self.check_lock_state() await self.hass.async_add_executor_job(self.data.set_switch_state_on) await self.coordinator.async_refresh() async def async_turn_off(self, **kwargs: Any) -> None: """Turn the switch off.""" + self.check_lock_state() await self.hass.async_add_executor_job(self.data.set_switch_state_off) await self.coordinator.async_refresh() + + def check_lock_state(self) -> None: + """Raise an Error if manual switching via FRITZ!Box user interface is disabled.""" + if self.data.lock: + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="manual_switching_disabled", + ) diff --git a/tests/components/fritzbox/__init__.py b/tests/components/fritzbox/__init__.py index 2bd8f26d73b..61312805e91 100644 --- a/tests/components/fritzbox/__init__.py +++ b/tests/components/fritzbox/__init__.py @@ -151,7 +151,7 @@ class FritzDeviceSwitchMock(FritzEntityBaseMock): has_thermostat = False has_blind = False switch_state = "fake_state" - lock = "fake_locked" + lock = False power = 5678 present = True temperature = 1.23 diff --git a/tests/components/fritzbox/test_switch.py b/tests/components/fritzbox/test_switch.py index 417b355b396..ba3b1de9b2f 100644 --- a/tests/components/fritzbox/test_switch.py +++ b/tests/components/fritzbox/test_switch.py @@ -3,6 +3,7 @@ from datetime import timedelta from unittest.mock import Mock +import pytest from requests.exceptions import HTTPError from homeassistant.components.fritzbox.const import DOMAIN as FB_DOMAIN @@ -29,6 +30,7 @@ from homeassistant.const import ( UnitOfTemperature, ) from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er import homeassistant.util.dt as dt_util @@ -130,6 +132,7 @@ async def test_turn_on(hass: HomeAssistant, fritz: Mock) -> None: async def test_turn_off(hass: HomeAssistant, fritz: Mock) -> None: """Test turn device off.""" device = FritzDeviceSwitchMock() + assert await setup_config_entry( hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz ) @@ -137,9 +140,36 @@ async def test_turn_off(hass: HomeAssistant, fritz: Mock) -> None: await hass.services.async_call( DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_ID}, True ) + assert device.set_switch_state_off.call_count == 1 +async def test_toggle_while_locked(hass: HomeAssistant, fritz: Mock) -> None: + """Test toggling while device is locked.""" + device = FritzDeviceSwitchMock() + device.lock = True + + assert await setup_config_entry( + hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz + ) + + with pytest.raises( + HomeAssistantError, + match="Can't toggle switch while manual switching is disabled for the device", + ): + await hass.services.async_call( + DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_ID}, True + ) + + with pytest.raises( + HomeAssistantError, + match="Can't toggle switch while manual switching is disabled for the device", + ): + await hass.services.async_call( + DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: ENTITY_ID}, True + ) + + async def test_update(hass: HomeAssistant, fritz: Mock) -> None: """Test update without error.""" device = FritzDeviceSwitchMock() From 3d164c672181a68a7b4e01a300ac6b5db54e452a Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Wed, 26 Jun 2024 18:15:53 +0200 Subject: [PATCH 1303/1445] Bump ZHA dependencies (#120581) --- homeassistant/components/zha/manifest.json | 8 ++++---- requirements_all.txt | 8 ++++---- requirements_test_all.txt | 8 ++++---- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index f517742f16f..7087ff0b2f0 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -23,12 +23,12 @@ "requirements": [ "bellows==0.39.1", "pyserial==3.5", - "zha-quirks==0.0.116", - "zigpy-deconz==0.23.1", + "zha-quirks==0.0.117", + "zigpy-deconz==0.23.2", "zigpy==0.64.1", "zigpy-xbee==0.20.1", - "zigpy-zigate==0.12.0", - "zigpy-znp==0.12.1", + "zigpy-zigate==0.12.1", + "zigpy-znp==0.12.2", "universal-silabs-flasher==0.0.20", "pyserial-asyncio-fast==0.11" ], diff --git a/requirements_all.txt b/requirements_all.txt index a3a62b58b4b..3aaec74c36f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2966,7 +2966,7 @@ zeroconf==0.132.2 zeversolar==0.3.1 # homeassistant.components.zha -zha-quirks==0.0.116 +zha-quirks==0.0.117 # homeassistant.components.zhong_hong zhong-hong-hvac==1.0.12 @@ -2975,16 +2975,16 @@ zhong-hong-hvac==1.0.12 ziggo-mediabox-xl==1.1.0 # homeassistant.components.zha -zigpy-deconz==0.23.1 +zigpy-deconz==0.23.2 # homeassistant.components.zha zigpy-xbee==0.20.1 # homeassistant.components.zha -zigpy-zigate==0.12.0 +zigpy-zigate==0.12.1 # homeassistant.components.zha -zigpy-znp==0.12.1 +zigpy-znp==0.12.2 # homeassistant.components.zha zigpy==0.64.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3f05bcc3d33..4b1777f4bae 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2319,19 +2319,19 @@ zeroconf==0.132.2 zeversolar==0.3.1 # homeassistant.components.zha -zha-quirks==0.0.116 +zha-quirks==0.0.117 # homeassistant.components.zha -zigpy-deconz==0.23.1 +zigpy-deconz==0.23.2 # homeassistant.components.zha zigpy-xbee==0.20.1 # homeassistant.components.zha -zigpy-zigate==0.12.0 +zigpy-zigate==0.12.1 # homeassistant.components.zha -zigpy-znp==0.12.1 +zigpy-znp==0.12.2 # homeassistant.components.zha zigpy==0.64.1 From d3d0e05817938d17c9b7a6095d8043c77d26908c Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Wed, 26 Jun 2024 19:19:28 +0300 Subject: [PATCH 1304/1445] Change Shelly connect task log message level to error (#120582) --- homeassistant/components/shelly/coordinator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/shelly/coordinator.py b/homeassistant/components/shelly/coordinator.py index a4ff34f7d9a..02feef3633b 100644 --- a/homeassistant/components/shelly/coordinator.py +++ b/homeassistant/components/shelly/coordinator.py @@ -167,7 +167,7 @@ class ShellyCoordinatorBase[_DeviceT: BlockDevice | RpcDevice]( await self.device.initialize() update_device_fw_info(self.hass, self.device, self.entry) except DeviceConnectionError as err: - LOGGER.debug( + LOGGER.error( "Error connecting to Shelly device %s, error: %r", self.name, err ) return False From 1b45069620ed640cf13da880266e3fdff492cf50 Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Wed, 26 Jun 2024 11:13:01 -0500 Subject: [PATCH 1305/1445] Bump intents to 2024.6.26 (#120584) Bump intents --- homeassistant/components/conversation/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../components/conversation/snapshots/test_init.ambr | 12 ++++++------ 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/conversation/manifest.json b/homeassistant/components/conversation/manifest.json index ee0b29f22fc..2302d03bf4c 100644 --- a/homeassistant/components/conversation/manifest.json +++ b/homeassistant/components/conversation/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/conversation", "integration_type": "system", "quality_scale": "internal", - "requirements": ["hassil==1.7.1", "home-assistant-intents==2024.6.21"] + "requirements": ["hassil==1.7.1", "home-assistant-intents==2024.6.26"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 18461d6398b..e42ef84d34c 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -33,7 +33,7 @@ hass-nabucasa==0.81.1 hassil==1.7.1 home-assistant-bluetooth==1.12.2 home-assistant-frontend==20240626.0 -home-assistant-intents==2024.6.21 +home-assistant-intents==2024.6.26 httpx==0.27.0 ifaddr==0.2.0 Jinja2==3.1.4 diff --git a/requirements_all.txt b/requirements_all.txt index 3aaec74c36f..eca0100e5df 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1093,7 +1093,7 @@ holidays==0.51 home-assistant-frontend==20240626.0 # homeassistant.components.conversation -home-assistant-intents==2024.6.21 +home-assistant-intents==2024.6.26 # homeassistant.components.home_connect homeconnect==0.7.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4b1777f4bae..bf8fd1dc081 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -898,7 +898,7 @@ holidays==0.51 home-assistant-frontend==20240626.0 # homeassistant.components.conversation -home-assistant-intents==2024.6.21 +home-assistant-intents==2024.6.26 # homeassistant.components.home_connect homeconnect==0.7.2 diff --git a/tests/components/conversation/snapshots/test_init.ambr b/tests/components/conversation/snapshots/test_init.ambr index 403c72aaa10..6264e61863f 100644 --- a/tests/components/conversation/snapshots/test_init.ambr +++ b/tests/components/conversation/snapshots/test_init.ambr @@ -563,7 +563,7 @@ 'speech': dict({ 'plain': dict({ 'extra_data': None, - 'speech': 'Sorry, I am not aware of any device called kitchen', + 'speech': 'Sorry, I am not aware of any device called kitchen light', }), }), }), @@ -703,7 +703,7 @@ 'speech': dict({ 'plain': dict({ 'extra_data': None, - 'speech': 'Sorry, I am not aware of any device called late added', + 'speech': 'Sorry, I am not aware of any device called late added light', }), }), }), @@ -783,7 +783,7 @@ 'speech': dict({ 'plain': dict({ 'extra_data': None, - 'speech': 'Sorry, I am not aware of any device called kitchen', + 'speech': 'Sorry, I am not aware of any device called kitchen light', }), }), }), @@ -803,7 +803,7 @@ 'speech': dict({ 'plain': dict({ 'extra_data': None, - 'speech': 'Sorry, I am not aware of any device called my cool', + 'speech': 'Sorry, I am not aware of any device called my cool light', }), }), }), @@ -943,7 +943,7 @@ 'speech': dict({ 'plain': dict({ 'extra_data': None, - 'speech': 'Sorry, I am not aware of any device called kitchen', + 'speech': 'Sorry, I am not aware of any device called kitchen light', }), }), }), @@ -993,7 +993,7 @@ 'speech': dict({ 'plain': dict({ 'extra_data': None, - 'speech': 'Sorry, I am not aware of any device called renamed', + 'speech': 'Sorry, I am not aware of any device called renamed light', }), }), }), From b35442ed2de4abfe49aea2a54bb3c151c2fec755 Mon Sep 17 00:00:00 2001 From: Markus Jacobsen Date: Wed, 26 Jun 2024 21:46:59 +0200 Subject: [PATCH 1306/1445] Improve Bang & Olufsen error messages (#120587) * Convert logger messages to raised errors where applicable * Modify exception types * Improve deezer / tidal error message * Update homeassistant/components/bang_olufsen/strings.json Co-authored-by: Michael <35783820+mib1185@users.noreply.github.com> * Update homeassistant/components/bang_olufsen/media_player.py Co-authored-by: Michael <35783820+mib1185@users.noreply.github.com> --------- Co-authored-by: Michael <35783820+mib1185@users.noreply.github.com> --- .../components/bang_olufsen/media_player.py | 41 ++++++++++++------- .../components/bang_olufsen/strings.json | 12 ++++++ 2 files changed, 39 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/bang_olufsen/media_player.py b/homeassistant/components/bang_olufsen/media_player.py index d23c75046ff..0eff9f2bb85 100644 --- a/homeassistant/components/bang_olufsen/media_player.py +++ b/homeassistant/components/bang_olufsen/media_player.py @@ -45,7 +45,7 @@ from homeassistant.components.media_player import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_MODEL from homeassistant.core import HomeAssistant, callback -from homeassistant.exceptions import HomeAssistantError +from homeassistant.exceptions import HomeAssistantError, ServiceValidationError from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -316,7 +316,7 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity): @callback def _async_update_playback_error(self, data: PlaybackError) -> None: """Show playback error.""" - _LOGGER.error(data.error) + raise HomeAssistantError(data.error) @callback def _async_update_playback_progress(self, data: PlaybackProgress) -> None: @@ -516,7 +516,9 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity): self.async_write_ha_state() else: - _LOGGER.error("Seeking is currently only supported when using Deezer") + raise HomeAssistantError( + translation_domain=DOMAIN, translation_key="non_deezer_seeking" + ) async def async_media_previous_track(self) -> None: """Send the previous track command.""" @@ -529,12 +531,14 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity): async def async_select_source(self, source: str) -> None: """Select an input source.""" if source not in self._sources.values(): - _LOGGER.error( - "Invalid source: %s. Valid sources are: %s", - source, - list(self._sources.values()), + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="invalid_source", + translation_placeholders={ + "invalid_source": source, + "valid_sources": ",".join(list(self._sources.values())), + }, ) - return key = [x for x in self._sources if self._sources[x] == source][0] @@ -559,12 +563,14 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity): media_type = MediaType.MUSIC if media_type not in VALID_MEDIA_TYPES: - _LOGGER.error( - "%s is an invalid type. Valid values are: %s", - media_type, - VALID_MEDIA_TYPES, + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="invalid_media_type", + translation_placeholders={ + "invalid_media_type": media_type, + "valid_media_types": ",".join(VALID_MEDIA_TYPES), + }, ) - return if media_source.is_media_source_id(media_id): sourced_media = await media_source.async_resolve_media( @@ -681,7 +687,14 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity): ) except ApiException as error: - _LOGGER.error(json.loads(error.body)["message"]) + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="play_media_error", + translation_placeholders={ + "media_type": media_type, + "error_message": json.loads(error.body)["message"], + }, + ) from error async def async_browse_media( self, diff --git a/homeassistant/components/bang_olufsen/strings.json b/homeassistant/components/bang_olufsen/strings.json index 93b55cf0db2..cf5b212d424 100644 --- a/homeassistant/components/bang_olufsen/strings.json +++ b/homeassistant/components/bang_olufsen/strings.json @@ -28,6 +28,18 @@ "exceptions": { "m3u_invalid_format": { "message": "Media sources with the .m3u extension are not supported." + }, + "non_deezer_seeking": { + "message": "Seeking is currently only supported when using Deezer" + }, + "invalid_source": { + "message": "Invalid source: {invalid_source}. Valid sources are: {valid_sources}" + }, + "invalid_media_type": { + "message": "{invalid_media_type} is an invalid type. Valid values are: {valid_media_types}." + }, + "play_media_error": { + "message": "An error occurred while attempting to play {media_type}: {error_message}." } } } From 2e01e169ef96ad5ea9844ae6de1f6f8505d65827 Mon Sep 17 00:00:00 2001 From: Max Date: Wed, 26 Jun 2024 20:55:25 +0200 Subject: [PATCH 1307/1445] Correct deprecation warning `async_register_static_paths` (#120592) --- homeassistant/components/http/__init__.py | 2 +- tests/components/http/test_init.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index 38f0b628b2c..0d86ab57d3f 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -483,7 +483,7 @@ class HomeAssistantHTTP: frame.report( "calls hass.http.register_static_path which is deprecated because " "it does blocking I/O in the event loop, instead " - "call `await hass.http.async_register_static_path(" + "call `await hass.http.async_register_static_paths(" f'[StaticPathConfig("{url_path}", "{path}", {cache_headers})])`; ' "This function will be removed in 2025.7", exclude_integrations={"http"}, diff --git a/tests/components/http/test_init.py b/tests/components/http/test_init.py index 7a9fb329fcd..2895209b5f9 100644 --- a/tests/components/http/test_init.py +++ b/tests/components/http/test_init.py @@ -543,5 +543,5 @@ async def test_register_static_paths( "Detected code that calls hass.http.register_static_path " "which is deprecated because it does blocking I/O in the " "event loop, instead call " - "`await hass.http.async_register_static_path" + "`await hass.http.async_register_static_paths" ) in caplog.text From 80e70993c8a5a4694e38486ab389aae62208103e Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 26 Jun 2024 20:55:41 +0200 Subject: [PATCH 1308/1445] Remove deprecated run_immediately flag from integration sensor (#120593) --- homeassistant/components/integration/sensor.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/homeassistant/components/integration/sensor.py b/homeassistant/components/integration/sensor.py index 60cbee5549f..4fca92e9b40 100644 --- a/homeassistant/components/integration/sensor.py +++ b/homeassistant/components/integration/sensor.py @@ -446,7 +446,6 @@ class IntegrationSensor(RestoreSensor): event_filter=callback( lambda event_data: event_data["entity_id"] == self._sensor_source_id ), - run_immediately=True, ) ) self.async_on_remove( @@ -456,7 +455,6 @@ class IntegrationSensor(RestoreSensor): event_filter=callback( lambda event_data: event_data["entity_id"] == self._sensor_source_id ), - run_immediately=True, ) ) From b5c34808e6893646593f6e9deb94f43257229815 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Wed, 26 Jun 2024 21:35:23 +0300 Subject: [PATCH 1309/1445] Add last_error reporting to Shelly diagnostics (#120595) --- homeassistant/components/shelly/diagnostics.py | 10 ++++++++++ tests/components/shelly/test_diagnostics.py | 13 ++++++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/shelly/diagnostics.py b/homeassistant/components/shelly/diagnostics.py index db69abc8f55..e70b76a7c00 100644 --- a/homeassistant/components/shelly/diagnostics.py +++ b/homeassistant/components/shelly/diagnostics.py @@ -24,6 +24,8 @@ async def async_get_config_entry_diagnostics( device_settings: str | dict = "not initialized" device_status: str | dict = "not initialized" bluetooth: str | dict = "not initialized" + last_error: str = "not initialized" + if shelly_entry_data.block: block_coordinator = shelly_entry_data.block assert block_coordinator @@ -55,6 +57,10 @@ async def async_get_config_entry_diagnostics( "uptime", ] } + + if block_coordinator.device.last_error: + last_error = repr(block_coordinator.device.last_error) + else: rpc_coordinator = shelly_entry_data.rpc assert rpc_coordinator @@ -79,6 +85,9 @@ async def async_get_config_entry_diagnostics( "scanner": await scanner.async_diagnostics(), } + if rpc_coordinator.device.last_error: + last_error = repr(rpc_coordinator.device.last_error) + if isinstance(device_status, dict): device_status = async_redact_data(device_status, ["ssid"]) @@ -87,5 +96,6 @@ async def async_get_config_entry_diagnostics( "device_info": device_info, "device_settings": device_settings, "device_status": device_status, + "last_error": last_error, "bluetooth": bluetooth, } diff --git a/tests/components/shelly/test_diagnostics.py b/tests/components/shelly/test_diagnostics.py index f7f238f3327..4fc8ea6ca8f 100644 --- a/tests/components/shelly/test_diagnostics.py +++ b/tests/components/shelly/test_diagnostics.py @@ -1,9 +1,10 @@ """Tests for Shelly diagnostics platform.""" -from unittest.mock import ANY, Mock +from unittest.mock import ANY, Mock, PropertyMock from aioshelly.ble.const import BLE_SCAN_RESULT_EVENT from aioshelly.const import MODEL_25 +from aioshelly.exceptions import DeviceConnectionError import pytest from homeassistant.components.diagnostics import REDACTED @@ -36,6 +37,10 @@ async def test_block_config_entry_diagnostics( {key: REDACTED for key in TO_REDACT if key in entry_dict["data"]} ) + type(mock_block_device).last_error = PropertyMock( + return_value=DeviceConnectionError() + ) + result = await get_diagnostics_for_config_entry(hass, hass_client, entry) assert result == { @@ -48,6 +53,7 @@ async def test_block_config_entry_diagnostics( }, "device_settings": {"coiot": {"update_period": 15}}, "device_status": MOCK_STATUS_COAP, + "last_error": "DeviceConnectionError()", } @@ -91,6 +97,10 @@ async def test_rpc_config_entry_diagnostics( {key: REDACTED for key in TO_REDACT if key in entry_dict["data"]} ) + type(mock_rpc_device).last_error = PropertyMock( + return_value=DeviceConnectionError() + ) + result = await get_diagnostics_for_config_entry(hass, hass_client, entry) assert result == { @@ -152,4 +162,5 @@ async def test_rpc_config_entry_diagnostics( }, "wifi": {"rssi": -63}, }, + "last_error": "DeviceConnectionError()", } From da01635a075a2264f92afc2ba55a9c39b10fdcb9 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Wed, 26 Jun 2024 20:57:27 +0200 Subject: [PATCH 1310/1445] Prevent changes to mutable bmw_connected_drive fixture data (#120600) --- .../bmw_connected_drive/test_config_flow.py | 7 ++++--- tests/components/bmw_connected_drive/test_init.py | 14 +++++++++----- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/tests/components/bmw_connected_drive/test_config_flow.py b/tests/components/bmw_connected_drive/test_config_flow.py index b562e2b898f..3c7f452a011 100644 --- a/tests/components/bmw_connected_drive/test_config_flow.py +++ b/tests/components/bmw_connected_drive/test_config_flow.py @@ -92,7 +92,7 @@ async def test_api_error(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, - data=FIXTURE_USER_INPUT, + data=deepcopy(FIXTURE_USER_INPUT), ) assert result["type"] is FlowResultType.FORM @@ -116,7 +116,7 @@ async def test_full_user_flow_implementation(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, - data=FIXTURE_USER_INPUT, + data=deepcopy(FIXTURE_USER_INPUT), ) assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == FIXTURE_COMPLETE_ENTRY[CONF_USERNAME] @@ -137,7 +137,8 @@ async def test_options_flow_implementation(hass: HomeAssistant) -> None: return_value=True, ) as mock_setup_entry, ): - config_entry = MockConfigEntry(**FIXTURE_CONFIG_ENTRY) + config_entry_args = deepcopy(FIXTURE_CONFIG_ENTRY) + config_entry = MockConfigEntry(**config_entry_args) config_entry.add_to_hass(hass) await hass.config_entries.async_setup(config_entry.entry_id) diff --git a/tests/components/bmw_connected_drive/test_init.py b/tests/components/bmw_connected_drive/test_init.py index 52bc8a7ce05..5cd6362d6fa 100644 --- a/tests/components/bmw_connected_drive/test_init.py +++ b/tests/components/bmw_connected_drive/test_init.py @@ -1,5 +1,6 @@ """Test Axis component setup process.""" +from copy import deepcopy from unittest.mock import patch import pytest @@ -37,7 +38,7 @@ async def test_migrate_options( ) -> None: """Test successful migration of options.""" - config_entry = FIXTURE_CONFIG_ENTRY.copy() + config_entry = deepcopy(FIXTURE_CONFIG_ENTRY) config_entry["options"] = options mock_config_entry = MockConfigEntry(**config_entry) @@ -55,7 +56,7 @@ async def test_migrate_options( async def test_migrate_options_from_data(hass: HomeAssistant) -> None: """Test successful migration of options.""" - config_entry = FIXTURE_CONFIG_ENTRY.copy() + config_entry = deepcopy(FIXTURE_CONFIG_ENTRY) config_entry["options"] = {} config_entry["data"].update({CONF_READ_ONLY: False}) @@ -107,7 +108,8 @@ async def test_migrate_unique_ids( entity_registry: er.EntityRegistry, ) -> None: """Test successful migration of entity unique_ids.""" - mock_config_entry = MockConfigEntry(**FIXTURE_CONFIG_ENTRY) + confg_entry = deepcopy(FIXTURE_CONFIG_ENTRY) + mock_config_entry = MockConfigEntry(**confg_entry) mock_config_entry.add_to_hass(hass) entity: er.RegistryEntry = entity_registry.async_get_or_create( @@ -153,7 +155,8 @@ async def test_dont_migrate_unique_ids( entity_registry: er.EntityRegistry, ) -> None: """Test successful migration of entity unique_ids.""" - mock_config_entry = MockConfigEntry(**FIXTURE_CONFIG_ENTRY) + confg_entry = deepcopy(FIXTURE_CONFIG_ENTRY) + mock_config_entry = MockConfigEntry(**confg_entry) mock_config_entry.add_to_hass(hass) # create existing entry with new_unique_id @@ -196,7 +199,8 @@ async def test_remove_stale_devices( device_registry: dr.DeviceRegistry, ) -> None: """Test remove stale device registry entries.""" - mock_config_entry = MockConfigEntry(**FIXTURE_CONFIG_ENTRY) + config_entry = deepcopy(FIXTURE_CONFIG_ENTRY) + mock_config_entry = MockConfigEntry(**config_entry) mock_config_entry.add_to_hass(hass) device_registry.async_get_or_create( From 74204e2ee6be38b6a51f6c3fdd4a03f6411d9228 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Wed, 26 Jun 2024 21:30:30 +0200 Subject: [PATCH 1311/1445] Fix mqtt test fixture usage (#120602) --- tests/components/mqtt/test_config_flow.py | 2 +- tests/components/mqtt/test_image.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/components/mqtt/test_config_flow.py b/tests/components/mqtt/test_config_flow.py index 57975fdc309..457bd19c16f 100644 --- a/tests/components/mqtt/test_config_flow.py +++ b/tests/components/mqtt/test_config_flow.py @@ -1561,7 +1561,7 @@ async def test_setup_with_advanced_settings( } -@pytest.mark.usesfixtures("mock_ssl_context", "mock_process_uploaded_file") +@pytest.mark.usefixtures("mock_ssl_context", "mock_process_uploaded_file") async def test_change_websockets_transport_to_tcp( hass: HomeAssistant, mock_try_connection: MagicMock ) -> None: diff --git a/tests/components/mqtt/test_image.py b/tests/components/mqtt/test_image.py index 29109ee12f4..bb029fba231 100644 --- a/tests/components/mqtt/test_image.py +++ b/tests/components/mqtt/test_image.py @@ -499,7 +499,7 @@ async def test_image_from_url_fails( ), ], ) -@pytest.mark.usesfixtures("hass", "hass_client_no_auth") +@pytest.mark.usefixtures("hass", "hass_client_no_auth") async def test_image_config_fails( mqtt_mock_entry: MqttMockHAClientGenerator, caplog: pytest.LogCaptureFixture, From 242b3fa6099a23bbd409c987d95745ee8a9ab286 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 26 Jun 2024 22:05:30 +0200 Subject: [PATCH 1312/1445] Update adguardhome to 0.7.0 (#120605) --- homeassistant/components/adguard/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/adguard/manifest.json b/homeassistant/components/adguard/manifest.json index 52add51a663..f1b82177d5b 100644 --- a/homeassistant/components/adguard/manifest.json +++ b/homeassistant/components/adguard/manifest.json @@ -7,5 +7,5 @@ "integration_type": "service", "iot_class": "local_polling", "loggers": ["adguardhome"], - "requirements": ["adguardhome==0.6.3"] + "requirements": ["adguardhome==0.7.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index eca0100e5df..4e7d43ccdd2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -149,7 +149,7 @@ adb-shell[async]==0.4.4 adext==0.4.3 # homeassistant.components.adguard -adguardhome==0.6.3 +adguardhome==0.7.0 # homeassistant.components.advantage_air advantage-air==0.4.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bf8fd1dc081..2f0a793cc28 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -128,7 +128,7 @@ adb-shell[async]==0.4.4 adext==0.4.3 # homeassistant.components.adguard -adguardhome==0.6.3 +adguardhome==0.7.0 # homeassistant.components.advantage_air advantage-air==0.4.4 From 7d5d81b2298c394f946313e2082a51c60327ec4f Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 26 Jun 2024 22:51:27 +0200 Subject: [PATCH 1313/1445] Bump version to 2024.7.0b1 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 54d7f26a5f0..fe0989a54f8 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -24,7 +24,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 7 -PATCH_VERSION: Final = "0b0" +PATCH_VERSION: Final = "0b1" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0) diff --git a/pyproject.toml b/pyproject.toml index 0b490d621a3..ea264a29fc6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.7.0b0" +version = "2024.7.0b1" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From bea6fe30b86cd526bd12e159839c7e2535b996c3 Mon Sep 17 00:00:00 2001 From: Luca Angemi Date: Wed, 26 Jun 2024 23:45:47 +0200 Subject: [PATCH 1314/1445] Fix telegram bot thread_id key error (#120613) --- homeassistant/components/telegram_bot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/telegram_bot/__init__.py b/homeassistant/components/telegram_bot/__init__.py index f37a84a83a6..fed9021a46e 100644 --- a/homeassistant/components/telegram_bot/__init__.py +++ b/homeassistant/components/telegram_bot/__init__.py @@ -702,7 +702,7 @@ class TelegramNotificationService: } if message_tag is not None: event_data[ATTR_MESSAGE_TAG] = message_tag - if kwargs_msg[ATTR_MESSAGE_THREAD_ID] is not None: + if kwargs_msg.get(ATTR_MESSAGE_THREAD_ID) is not None: event_data[ATTR_MESSAGE_THREAD_ID] = kwargs_msg[ ATTR_MESSAGE_THREAD_ID ] From 0701b0daa93b400a377f4d8c13f513eb25621fa3 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Wed, 26 Jun 2024 23:54:07 +0200 Subject: [PATCH 1315/1445] Update frontend to 20240626.2 (#120614) --- 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 063f7db34a0..89c8fbe30ca 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==20240626.0"] + "requirements": ["home-assistant-frontend==20240626.2"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index e42ef84d34c..174de784eba 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -32,7 +32,7 @@ habluetooth==3.1.3 hass-nabucasa==0.81.1 hassil==1.7.1 home-assistant-bluetooth==1.12.2 -home-assistant-frontend==20240626.0 +home-assistant-frontend==20240626.2 home-assistant-intents==2024.6.26 httpx==0.27.0 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 4e7d43ccdd2..67ad67799e9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1090,7 +1090,7 @@ hole==0.8.0 holidays==0.51 # homeassistant.components.frontend -home-assistant-frontend==20240626.0 +home-assistant-frontend==20240626.2 # homeassistant.components.conversation home-assistant-intents==2024.6.26 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2f0a793cc28..350b59c0eab 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -895,7 +895,7 @@ hole==0.8.0 holidays==0.51 # homeassistant.components.frontend -home-assistant-frontend==20240626.0 +home-assistant-frontend==20240626.2 # homeassistant.components.conversation home-assistant-intents==2024.6.26 From 3da8d0a741d26cb9d38d1602bb9c6427e023a8f7 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 26 Jun 2024 23:55:20 +0200 Subject: [PATCH 1316/1445] Bump version to 2024.7.0b2 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index fe0989a54f8..8291fb93fd7 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -24,7 +24,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 7 -PATCH_VERSION: Final = "0b1" +PATCH_VERSION: Final = "0b2" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0) diff --git a/pyproject.toml b/pyproject.toml index ea264a29fc6..709022534b1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.7.0b1" +version = "2024.7.0b2" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 53e49861a1c9ee061a9a55cd358c05274f1845db Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 27 Jun 2024 23:26:38 +1200 Subject: [PATCH 1317/1445] Mark esphome integration as platinum (#112565) Co-authored-by: J. Nick Koston --- homeassistant/components/esphome/manifest.json | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index ab175028bea..6e30febd7db 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -15,6 +15,7 @@ "iot_class": "local_push", "loggers": ["aioesphomeapi", "noiseprotocol", "bleak_esphome"], "mqtt": ["esphome/discover/#"], + "quality_scale": "platinum", "requirements": [ "aioesphomeapi==24.6.1", "esphome-dashboard-api==1.2.3", From 2c2261254b45e074a62ddc3625428181a9d1ba61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexey=20ALERT=20Rubash=D1=91ff?= Date: Thu, 27 Jun 2024 23:05:58 +0300 Subject: [PATCH 1318/1445] Improve AtlanticDomesticHotWaterProductionMBLComponent support in Overkiz (#114178) * add overkiz AtlanticDHW support Adds support of Overkiz water heater entity selection based on device controllable_name Adds support of Atlantic water heater based on Atlantic Steatite Cube WI-FI VM 150 S4CS 2400W Adds more Overkiz water heater binary_sensors, numbers, and sensors * Changed class annotation * min_temp and max_temp as properties * reverted binary_sensors, number, sensor to make separate PRs * Update homeassistant/components/overkiz/water_heater_entities/atlantic_dhw.py Co-authored-by: Mick Vleeshouwer * Update homeassistant/components/overkiz/water_heater_entities/atlantic_dhw.py Co-authored-by: Mick Vleeshouwer * Update homeassistant/components/overkiz/water_heater_entities/atlantic_dhw.py Co-authored-by: Mick Vleeshouwer * Update homeassistant/components/overkiz/water_heater.py Co-authored-by: Mick Vleeshouwer * Update homeassistant/components/overkiz/water_heater_entities/atlantic_dhw.py Co-authored-by: Mick Vleeshouwer * Update homeassistant/components/overkiz/water_heater_entities/atlantic_dhw.py Co-authored-by: Mick Vleeshouwer * review fixes, typos, and pylint * review fix * review fix * ruff * temperature properties changed to constructor attributes * logger removed * constants usage consistency * redundant mapping removed * Update homeassistant/components/overkiz/water_heater_entities/atlantic_dhw.py Co-authored-by: Mick Vleeshouwer * boost mode method annotation typo * removed away mode for atlantic dwh * absence and boost mode attributes now support 'prog' state * heating status bugfix * electrical consumption sensor * warm water remaining volume sensor * away mode reintroduced * mypy check * boost plus state support * Update homeassistant/components/overkiz/sensor.py Co-authored-by: Mick Vleeshouwer * sensors reverted to separate them into their own PR * check away and boost modes on before switching them off * atlantic_dhw renamed to atlantic_domestic_hot_water_production * annotation changed * AtlanticDomesticHotWaterProductionMBLComponent file renamed, annotation change reverted --------- Co-authored-by: Mick Vleeshouwer --- .../components/overkiz/binary_sensor.py | 9 +- .../components/overkiz/water_heater.py | 29 ++- .../overkiz/water_heater_entities/__init__.py | 7 + ...stic_hot_water_production_mlb_component.py | 182 ++++++++++++++++++ 4 files changed, 216 insertions(+), 11 deletions(-) create mode 100644 homeassistant/components/overkiz/water_heater_entities/atlantic_domestic_hot_water_production_mlb_component.py diff --git a/homeassistant/components/overkiz/binary_sensor.py b/homeassistant/components/overkiz/binary_sensor.py index c37afc9cb0c..8ea86e03e8c 100644 --- a/homeassistant/components/overkiz/binary_sensor.py +++ b/homeassistant/components/overkiz/binary_sensor.py @@ -109,17 +109,20 @@ BINARY_SENSOR_DESCRIPTIONS: list[OverkizBinarySensorDescription] = [ key=OverkizState.CORE_HEATING_STATUS, name="Heating status", device_class=BinarySensorDeviceClass.HEAT, - value_fn=lambda state: state == OverkizCommandParam.ON, + value_fn=lambda state: cast(str, state).lower() + in (OverkizCommandParam.ON, OverkizCommandParam.HEATING), ), OverkizBinarySensorDescription( key=OverkizState.MODBUSLINK_DHW_ABSENCE_MODE, name="Absence mode", - value_fn=lambda state: state == OverkizCommandParam.ON, + value_fn=lambda state: state + in (OverkizCommandParam.ON, OverkizCommandParam.PROG), ), OverkizBinarySensorDescription( key=OverkizState.MODBUSLINK_DHW_BOOST_MODE, name="Boost mode", - value_fn=lambda state: state == OverkizCommandParam.ON, + value_fn=lambda state: state + in (OverkizCommandParam.ON, OverkizCommandParam.PROG), ), ] diff --git a/homeassistant/components/overkiz/water_heater.py b/homeassistant/components/overkiz/water_heater.py index c76f6d5099f..99bfb279e4c 100644 --- a/homeassistant/components/overkiz/water_heater.py +++ b/homeassistant/components/overkiz/water_heater.py @@ -9,7 +9,11 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import HomeAssistantOverkizData from .const import DOMAIN -from .water_heater_entities import WIDGET_TO_WATER_HEATER_ENTITY +from .entity import OverkizEntity +from .water_heater_entities import ( + CONTROLLABLE_NAME_TO_WATER_HEATER_ENTITY, + WIDGET_TO_WATER_HEATER_ENTITY, +) async def async_setup_entry( @@ -19,11 +23,20 @@ async def async_setup_entry( ) -> None: """Set up the Overkiz DHW from a config entry.""" data: HomeAssistantOverkizData = hass.data[DOMAIN][entry.entry_id] + entities: list[OverkizEntity] = [] - async_add_entities( - WIDGET_TO_WATER_HEATER_ENTITY[device.widget]( - device.device_url, data.coordinator - ) - for device in data.platforms[Platform.WATER_HEATER] - if device.widget in WIDGET_TO_WATER_HEATER_ENTITY - ) + for device in data.platforms[Platform.WATER_HEATER]: + if device.controllable_name in CONTROLLABLE_NAME_TO_WATER_HEATER_ENTITY: + entities.append( + CONTROLLABLE_NAME_TO_WATER_HEATER_ENTITY[device.controllable_name]( + device.device_url, data.coordinator + ) + ) + elif device.widget in WIDGET_TO_WATER_HEATER_ENTITY: + entities.append( + WIDGET_TO_WATER_HEATER_ENTITY[device.widget]( + device.device_url, data.coordinator + ) + ) + + async_add_entities(entities) diff --git a/homeassistant/components/overkiz/water_heater_entities/__init__.py b/homeassistant/components/overkiz/water_heater_entities/__init__.py index 6f6539ef659..fdc41f213c6 100644 --- a/homeassistant/components/overkiz/water_heater_entities/__init__.py +++ b/homeassistant/components/overkiz/water_heater_entities/__init__.py @@ -2,6 +2,9 @@ from pyoverkiz.enums.ui import UIWidget +from .atlantic_domestic_hot_water_production_mlb_component import ( + AtlanticDomesticHotWaterProductionMBLComponent, +) from .atlantic_pass_apc_dhw import AtlanticPassAPCDHW from .domestic_hot_water_production import DomesticHotWaterProduction from .hitachi_dhw import HitachiDHW @@ -11,3 +14,7 @@ WIDGET_TO_WATER_HEATER_ENTITY = { UIWidget.DOMESTIC_HOT_WATER_PRODUCTION: DomesticHotWaterProduction, UIWidget.HITACHI_DHW: HitachiDHW, } + +CONTROLLABLE_NAME_TO_WATER_HEATER_ENTITY = { + "modbuslink:AtlanticDomesticHotWaterProductionMBLComponent": AtlanticDomesticHotWaterProductionMBLComponent, +} diff --git a/homeassistant/components/overkiz/water_heater_entities/atlantic_domestic_hot_water_production_mlb_component.py b/homeassistant/components/overkiz/water_heater_entities/atlantic_domestic_hot_water_production_mlb_component.py new file mode 100644 index 00000000000..de995a2bd1a --- /dev/null +++ b/homeassistant/components/overkiz/water_heater_entities/atlantic_domestic_hot_water_production_mlb_component.py @@ -0,0 +1,182 @@ +"""Support for AtlanticDomesticHotWaterProductionMBLComponent.""" + +from typing import Any, cast + +from pyoverkiz.enums import OverkizCommand, OverkizCommandParam, OverkizState + +from homeassistant.components.water_heater import ( + STATE_ECO, + STATE_OFF, + STATE_PERFORMANCE, + WaterHeaterEntity, + WaterHeaterEntityFeature, +) +from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature + +from .. import OverkizDataUpdateCoordinator +from ..entity import OverkizEntity + + +class AtlanticDomesticHotWaterProductionMBLComponent(OverkizEntity, WaterHeaterEntity): + """Representation of AtlanticDomesticHotWaterProductionMBLComponent (modbuslink).""" + + _attr_temperature_unit = UnitOfTemperature.CELSIUS + _attr_supported_features = ( + WaterHeaterEntityFeature.TARGET_TEMPERATURE + | WaterHeaterEntityFeature.OPERATION_MODE + | WaterHeaterEntityFeature.AWAY_MODE + | WaterHeaterEntityFeature.ON_OFF + ) + _attr_operation_list = [ + OverkizCommandParam.PERFORMANCE, + OverkizCommandParam.ECO, + OverkizCommandParam.MANUAL, + ] + + def __init__( + self, device_url: str, coordinator: OverkizDataUpdateCoordinator + ) -> None: + """Init method.""" + super().__init__(device_url, coordinator) + self._attr_max_temp = cast( + float, + self.executor.select_state( + OverkizState.CORE_MAXIMAL_TEMPERATURE_MANUAL_MODE + ), + ) + self._attr_min_temp = cast( + float, + self.executor.select_state( + OverkizState.CORE_MINIMAL_TEMPERATURE_MANUAL_MODE + ), + ) + + @property + def current_temperature(self) -> float: + """Return the current temperature.""" + return cast( + float, + self.executor.select_state( + OverkizState.MODBUSLINK_MIDDLE_WATER_TEMPERATURE + ), + ) + + @property + def target_temperature(self) -> float: + """Return the temperature corresponding to the PRESET.""" + return cast( + float, + self.executor.select_state(OverkizState.CORE_WATER_TARGET_TEMPERATURE), + ) + + async def async_set_temperature(self, **kwargs: Any) -> None: + """Set new temperature.""" + temperature = kwargs[ATTR_TEMPERATURE] + await self.executor.async_execute_command( + OverkizCommand.SET_TARGET_DHW_TEMPERATURE, temperature + ) + + @property + def is_boost_mode_on(self) -> bool: + """Return true if boost mode is on.""" + return self.executor.select_state(OverkizState.MODBUSLINK_DHW_BOOST_MODE) in ( + OverkizCommandParam.ON, + OverkizCommandParam.PROG, + ) + + @property + def is_eco_mode_on(self) -> bool: + """Return true if eco mode is on.""" + return self.executor.select_state(OverkizState.MODBUSLINK_DHW_MODE) in ( + OverkizCommandParam.MANUAL_ECO_ACTIVE, + OverkizCommandParam.AUTO_MODE, + ) + + @property + def is_away_mode_on(self) -> bool: + """Return true if away mode is on.""" + return ( + self.executor.select_state(OverkizState.MODBUSLINK_DHW_ABSENCE_MODE) + == OverkizCommandParam.ON + ) + + @property + def current_operation(self) -> str: + """Return current operation.""" + if self.is_away_mode_on: + return STATE_OFF + + if self.is_boost_mode_on: + return STATE_PERFORMANCE + + if self.is_eco_mode_on: + return STATE_ECO + + if ( + cast(str, self.executor.select_state(OverkizState.MODBUSLINK_DHW_MODE)) + == OverkizCommandParam.MANUAL_ECO_INACTIVE + ): + return OverkizCommandParam.MANUAL + + return STATE_OFF + + async def async_set_operation_mode(self, operation_mode: str) -> None: + """Set new operation mode.""" + if operation_mode in (STATE_PERFORMANCE, OverkizCommandParam.BOOST): + if self.is_away_mode_on: + await self.async_turn_away_mode_off() + await self.async_turn_boost_mode_on() + elif operation_mode in ( + OverkizCommandParam.ECO, + OverkizCommandParam.MANUAL_ECO_ACTIVE, + ): + if self.is_away_mode_on: + await self.async_turn_away_mode_off() + if self.is_boost_mode_on: + await self.async_turn_boost_mode_off() + await self.executor.async_execute_command( + OverkizCommand.SET_DHW_MODE, OverkizCommandParam.AUTO_MODE + ) + elif operation_mode in ( + OverkizCommandParam.MANUAL, + OverkizCommandParam.MANUAL_ECO_INACTIVE, + ): + if self.is_away_mode_on: + await self.async_turn_away_mode_off() + if self.is_boost_mode_on: + await self.async_turn_boost_mode_off() + await self.executor.async_execute_command( + OverkizCommand.SET_DHW_MODE, OverkizCommandParam.MANUAL_ECO_INACTIVE + ) + else: + if self.is_away_mode_on: + await self.async_turn_away_mode_off() + if self.is_boost_mode_on: + await self.async_turn_boost_mode_off() + await self.executor.async_execute_command( + OverkizCommand.SET_DHW_MODE, operation_mode + ) + + async def async_turn_away_mode_on(self) -> None: + """Turn away mode on.""" + await self.executor.async_execute_command( + OverkizCommand.SET_ABSENCE_MODE, OverkizCommandParam.ON + ) + + async def async_turn_away_mode_off(self) -> None: + """Turn away mode off.""" + await self.executor.async_execute_command( + OverkizCommand.SET_ABSENCE_MODE, OverkizCommandParam.OFF + ) + + async def async_turn_boost_mode_on(self) -> None: + """Turn boost mode on.""" + await self.executor.async_execute_command( + OverkizCommand.SET_BOOST_MODE, OverkizCommandParam.ON + ) + + async def async_turn_boost_mode_off(self) -> None: + """Turn boost mode off.""" + await self.executor.async_execute_command( + OverkizCommand.SET_BOOST_MODE, OverkizCommandParam.OFF + ) From dcffd6bd7ae0a91a01bc2758462b89603b4cae99 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 26 Jun 2024 20:37:08 -0500 Subject: [PATCH 1319/1445] Remove unused fields from unifiprotect event sensors (#120568) --- homeassistant/components/unifiprotect/binary_sensor.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/homeassistant/components/unifiprotect/binary_sensor.py b/homeassistant/components/unifiprotect/binary_sensor.py index e35eb6f48f3..c4e1aa87df2 100644 --- a/homeassistant/components/unifiprotect/binary_sensor.py +++ b/homeassistant/components/unifiprotect/binary_sensor.py @@ -426,14 +426,12 @@ EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = ( device_class=BinarySensorDeviceClass.OCCUPANCY, icon="mdi:doorbell-video", ufp_required_field="feature_flags.is_doorbell", - ufp_value="is_ringing", ufp_event_obj="last_ring_event", ), ProtectBinaryEventEntityDescription( key="motion", name="Motion", device_class=BinarySensorDeviceClass.MOTION, - ufp_value="is_motion_currently_detected", ufp_enabled="is_motion_detection_on", ufp_event_obj="last_motion_event", ), From 210e906a4da8d8f950bdcb2ba2cf7d7d8a980267 Mon Sep 17 00:00:00 2001 From: Steven B <51370195+sdb9696@users.noreply.github.com> Date: Thu, 27 Jun 2024 13:34:12 +0100 Subject: [PATCH 1320/1445] Store tplink credentials_hash outside of device_config (#120597) --- homeassistant/components/tplink/__init__.py | 42 +++- .../components/tplink/config_flow.py | 43 +++- homeassistant/components/tplink/const.py | 2 + tests/components/tplink/__init__.py | 19 +- tests/components/tplink/test_config_flow.py | 81 ++++++- tests/components/tplink/test_init.py | 217 +++++++++++++++++- 6 files changed, 373 insertions(+), 31 deletions(-) diff --git a/homeassistant/components/tplink/__init__.py b/homeassistant/components/tplink/__init__.py index 764867f0bee..6d300f68aa0 100644 --- a/homeassistant/components/tplink/__init__.py +++ b/homeassistant/components/tplink/__init__.py @@ -43,6 +43,7 @@ from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.typing import ConfigType from .const import ( + CONF_CREDENTIALS_HASH, CONF_DEVICE_CONFIG, CONNECT_TIMEOUT, DISCOVERY_TIMEOUT, @@ -73,6 +74,7 @@ def async_trigger_discovery( discovered_devices: dict[str, Device], ) -> None: """Trigger config flows for discovered devices.""" + for formatted_mac, device in discovered_devices.items(): discovery_flow.async_create_flow( hass, @@ -83,7 +85,6 @@ def async_trigger_discovery( CONF_HOST: device.host, CONF_MAC: formatted_mac, CONF_DEVICE_CONFIG: device.config.to_dict( - credentials_hash=device.credentials_hash, exclude_credentials=True, ), }, @@ -133,6 +134,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: TPLinkConfigEntry) -> bo """Set up TPLink from a config entry.""" host: str = entry.data[CONF_HOST] credentials = await get_credentials(hass) + entry_credentials_hash = entry.data.get(CONF_CREDENTIALS_HASH) config: DeviceConfig | None = None if config_dict := entry.data.get(CONF_DEVICE_CONFIG): @@ -151,19 +153,31 @@ async def async_setup_entry(hass: HomeAssistant, entry: TPLinkConfigEntry) -> bo config.timeout = CONNECT_TIMEOUT if config.uses_http is True: config.http_client = create_async_tplink_clientsession(hass) + + # If we have in memory credentials use them otherwise check for credentials_hash if credentials: config.credentials = credentials + elif entry_credentials_hash: + config.credentials_hash = entry_credentials_hash + try: device: Device = await Device.connect(config=config) except AuthenticationError as ex: + # If the stored credentials_hash was used but doesn't work remove it + if not credentials and entry_credentials_hash: + data = {k: v for k, v in entry.data.items() if k != CONF_CREDENTIALS_HASH} + hass.config_entries.async_update_entry(entry, data=data) raise ConfigEntryAuthFailed from ex except KasaException as ex: raise ConfigEntryNotReady from ex - device_config_dict = device.config.to_dict( - credentials_hash=device.credentials_hash, exclude_credentials=True - ) + device_credentials_hash = device.credentials_hash + device_config_dict = device.config.to_dict(exclude_credentials=True) + # Do not store the credentials hash inside the device_config + device_config_dict.pop(CONF_CREDENTIALS_HASH, None) updates: dict[str, Any] = {} + if device_credentials_hash and device_credentials_hash != entry_credentials_hash: + updates[CONF_CREDENTIALS_HASH] = device_credentials_hash if device_config_dict != config_dict: updates[CONF_DEVICE_CONFIG] = device_config_dict if entry.data.get(CONF_ALIAS) != device.alias: @@ -326,7 +340,25 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> minor_version = 3 hass.config_entries.async_update_entry(config_entry, minor_version=3) - _LOGGER.debug("Migration to version %s.%s successful", version, minor_version) + if version == 1 and minor_version == 3: + # credentials_hash stored in the device_config should be moved to data. + updates: dict[str, Any] = {} + if config_dict := config_entry.data.get(CONF_DEVICE_CONFIG): + assert isinstance(config_dict, dict) + if credentials_hash := config_dict.pop(CONF_CREDENTIALS_HASH, None): + updates[CONF_CREDENTIALS_HASH] = credentials_hash + updates[CONF_DEVICE_CONFIG] = config_dict + minor_version = 4 + hass.config_entries.async_update_entry( + config_entry, + data={ + **config_entry.data, + **updates, + }, + minor_version=minor_version, + ) + _LOGGER.debug("Migration to version %s.%s complete", version, minor_version) + return True diff --git a/homeassistant/components/tplink/config_flow.py b/homeassistant/components/tplink/config_flow.py index 7bead2207a3..5608ccfa72f 100644 --- a/homeassistant/components/tplink/config_flow.py +++ b/homeassistant/components/tplink/config_flow.py @@ -44,7 +44,13 @@ from . import ( mac_alias, set_credentials, ) -from .const import CONF_DEVICE_CONFIG, CONNECT_TIMEOUT, DOMAIN +from .const import ( + CONF_CONNECTION_TYPE, + CONF_CREDENTIALS_HASH, + CONF_DEVICE_CONFIG, + CONNECT_TIMEOUT, + DOMAIN, +) STEP_AUTH_DATA_SCHEMA = vol.Schema( {vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str} @@ -55,7 +61,7 @@ class TPLinkConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for tplink.""" VERSION = 1 - MINOR_VERSION = 3 + MINOR_VERSION = 4 reauth_entry: ConfigEntry | None = None def __init__(self) -> None: @@ -95,9 +101,18 @@ class TPLinkConfigFlow(ConfigFlow, domain=DOMAIN): entry_config_dict = entry_data.get(CONF_DEVICE_CONFIG) if entry_config_dict == config and entry_data[CONF_HOST] == host: return None + updates = {**entry.data, CONF_DEVICE_CONFIG: config, CONF_HOST: host} + # If the connection parameters have changed the credentials_hash will be invalid. + if ( + entry_config_dict + and isinstance(entry_config_dict, dict) + and entry_config_dict.get(CONF_CONNECTION_TYPE) + != config.get(CONF_CONNECTION_TYPE) + ): + updates.pop(CONF_CREDENTIALS_HASH, None) return self.async_update_reload_and_abort( entry, - data={**entry.data, CONF_DEVICE_CONFIG: config, CONF_HOST: host}, + data=updates, reason="already_configured", ) @@ -345,18 +360,22 @@ class TPLinkConfigFlow(ConfigFlow, domain=DOMAIN): @callback def _async_create_entry_from_device(self, device: Device) -> ConfigFlowResult: """Create a config entry from a smart device.""" + # This is only ever called after a successful device update so we know that + # the credential_hash is correct and should be saved. self._abort_if_unique_id_configured(updates={CONF_HOST: device.host}) + data = { + CONF_HOST: device.host, + CONF_ALIAS: device.alias, + CONF_MODEL: device.model, + CONF_DEVICE_CONFIG: device.config.to_dict( + exclude_credentials=True, + ), + } + if device.credentials_hash: + data[CONF_CREDENTIALS_HASH] = device.credentials_hash return self.async_create_entry( title=f"{device.alias} {device.model}", - data={ - CONF_HOST: device.host, - CONF_ALIAS: device.alias, - CONF_MODEL: device.model, - CONF_DEVICE_CONFIG: device.config.to_dict( - credentials_hash=device.credentials_hash, - exclude_credentials=True, - ), - }, + data=data, ) async def _async_try_discover_and_update( diff --git a/homeassistant/components/tplink/const.py b/homeassistant/components/tplink/const.py index d77d415aa9c..babd92e2c34 100644 --- a/homeassistant/components/tplink/const.py +++ b/homeassistant/components/tplink/const.py @@ -20,6 +20,8 @@ ATTR_TODAY_ENERGY_KWH: Final = "today_energy_kwh" ATTR_TOTAL_ENERGY_KWH: Final = "total_energy_kwh" CONF_DEVICE_CONFIG: Final = "device_config" +CONF_CREDENTIALS_HASH: Final = "credentials_hash" +CONF_CONNECTION_TYPE: Final = "connection_type" PLATFORMS: Final = [ Platform.BINARY_SENSOR, diff --git a/tests/components/tplink/__init__.py b/tests/components/tplink/__init__.py index 9c8aeb99be1..b3092d62904 100644 --- a/tests/components/tplink/__init__.py +++ b/tests/components/tplink/__init__.py @@ -22,6 +22,7 @@ from syrupy import SnapshotAssertion from homeassistant.components.tplink import ( CONF_ALIAS, + CONF_CREDENTIALS_HASH, CONF_DEVICE_CONFIG, CONF_HOST, CONF_MODEL, @@ -53,9 +54,7 @@ MAC_ADDRESS2 = "11:22:33:44:55:66" DEFAULT_ENTRY_TITLE = f"{ALIAS} {MODEL}" CREDENTIALS_HASH_LEGACY = "" DEVICE_CONFIG_LEGACY = DeviceConfig(IP_ADDRESS) -DEVICE_CONFIG_DICT_LEGACY = DEVICE_CONFIG_LEGACY.to_dict( - credentials_hash=CREDENTIALS_HASH_LEGACY, exclude_credentials=True -) +DEVICE_CONFIG_DICT_LEGACY = DEVICE_CONFIG_LEGACY.to_dict(exclude_credentials=True) CREDENTIALS = Credentials("foo", "bar") CREDENTIALS_HASH_AUTH = "abcdefghijklmnopqrstuv==" DEVICE_CONFIG_AUTH = DeviceConfig( @@ -74,12 +73,8 @@ DEVICE_CONFIG_AUTH2 = DeviceConfig( ), uses_http=True, ) -DEVICE_CONFIG_DICT_AUTH = DEVICE_CONFIG_AUTH.to_dict( - credentials_hash=CREDENTIALS_HASH_AUTH, exclude_credentials=True -) -DEVICE_CONFIG_DICT_AUTH2 = DEVICE_CONFIG_AUTH2.to_dict( - credentials_hash=CREDENTIALS_HASH_AUTH, exclude_credentials=True -) +DEVICE_CONFIG_DICT_AUTH = DEVICE_CONFIG_AUTH.to_dict(exclude_credentials=True) +DEVICE_CONFIG_DICT_AUTH2 = DEVICE_CONFIG_AUTH2.to_dict(exclude_credentials=True) CREATE_ENTRY_DATA_LEGACY = { CONF_HOST: IP_ADDRESS, @@ -92,14 +87,20 @@ CREATE_ENTRY_DATA_AUTH = { CONF_HOST: IP_ADDRESS, CONF_ALIAS: ALIAS, CONF_MODEL: MODEL, + CONF_CREDENTIALS_HASH: CREDENTIALS_HASH_AUTH, CONF_DEVICE_CONFIG: DEVICE_CONFIG_DICT_AUTH, } CREATE_ENTRY_DATA_AUTH2 = { CONF_HOST: IP_ADDRESS2, CONF_ALIAS: ALIAS, CONF_MODEL: MODEL, + CONF_CREDENTIALS_HASH: CREDENTIALS_HASH_AUTH, CONF_DEVICE_CONFIG: DEVICE_CONFIG_DICT_AUTH2, } +NEW_CONNECTION_TYPE = DeviceConnectionParameters( + DeviceFamily.IotSmartPlugSwitch, DeviceEncryptionType.Aes +) +NEW_CONNECTION_TYPE_DICT = NEW_CONNECTION_TYPE.to_dict() def _load_feature_fixtures(): diff --git a/tests/components/tplink/test_config_flow.py b/tests/components/tplink/test_config_flow.py index 7560ff4a72d..e9ae7957520 100644 --- a/tests/components/tplink/test_config_flow.py +++ b/tests/components/tplink/test_config_flow.py @@ -14,8 +14,12 @@ from homeassistant.components.tplink import ( DeviceConfig, KasaException, ) -from homeassistant.components.tplink.const import CONF_DEVICE_CONFIG -from homeassistant.config_entries import ConfigEntryState +from homeassistant.components.tplink.const import ( + CONF_CONNECTION_TYPE, + CONF_CREDENTIALS_HASH, + CONF_DEVICE_CONFIG, +) +from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState from homeassistant.const import ( CONF_ALIAS, CONF_DEVICE, @@ -32,6 +36,7 @@ from . import ( CREATE_ENTRY_DATA_AUTH, CREATE_ENTRY_DATA_AUTH2, CREATE_ENTRY_DATA_LEGACY, + CREDENTIALS_HASH_AUTH, DEFAULT_ENTRY_TITLE, DEVICE_CONFIG_DICT_AUTH, DEVICE_CONFIG_DICT_LEGACY, @@ -40,6 +45,7 @@ from . import ( MAC_ADDRESS, MAC_ADDRESS2, MODULE, + NEW_CONNECTION_TYPE_DICT, _mocked_device, _patch_connect, _patch_discovery, @@ -811,6 +817,77 @@ async def test_integration_discovery_with_ip_change( mock_connect["connect"].assert_awaited_once_with(config=config) +async def test_integration_discovery_with_connection_change( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_discovery: AsyncMock, + mock_connect: AsyncMock, +) -> None: + """Test that config entry is updated with new device config. + + And that connection_hash is removed as it will be invalid. + """ + mock_connect["connect"].side_effect = KasaException() + + mock_config_entry = MockConfigEntry( + title="TPLink", + domain=DOMAIN, + data=CREATE_ENTRY_DATA_AUTH, + unique_id=MAC_ADDRESS, + ) + mock_config_entry.add_to_hass(hass) + with patch("homeassistant.components.tplink.Discover.discover", return_value={}): + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done(wait_background_tasks=True) + + assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY + assert ( + len( + hass.config_entries.flow.async_progress_by_handler( + DOMAIN, match_context={"source": SOURCE_REAUTH} + ) + ) + == 0 + ) + assert mock_config_entry.data[CONF_DEVICE_CONFIG] == DEVICE_CONFIG_DICT_AUTH + assert mock_config_entry.data[CONF_DEVICE_CONFIG].get(CONF_HOST) == "127.0.0.1" + assert mock_config_entry.data[CONF_CREDENTIALS_HASH] == CREDENTIALS_HASH_AUTH + + NEW_DEVICE_CONFIG = { + **DEVICE_CONFIG_DICT_AUTH, + CONF_CONNECTION_TYPE: NEW_CONNECTION_TYPE_DICT, + } + config = DeviceConfig.from_dict(NEW_DEVICE_CONFIG) + # Reset the connect mock so when the config flow reloads the entry it succeeds + mock_connect["connect"].reset_mock(side_effect=True) + bulb = _mocked_device( + device_config=config, + mac=mock_config_entry.unique_id, + ) + mock_connect["connect"].return_value = bulb + + discovery_result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, + data={ + CONF_HOST: "127.0.0.1", + CONF_MAC: MAC_ADDRESS, + CONF_ALIAS: ALIAS, + CONF_DEVICE_CONFIG: NEW_DEVICE_CONFIG, + }, + ) + await hass.async_block_till_done(wait_background_tasks=True) + assert discovery_result["type"] is FlowResultType.ABORT + assert discovery_result["reason"] == "already_configured" + assert mock_config_entry.data[CONF_DEVICE_CONFIG] == NEW_DEVICE_CONFIG + assert mock_config_entry.data[CONF_HOST] == "127.0.0.1" + assert CREDENTIALS_HASH_AUTH not in mock_config_entry.data + + assert mock_config_entry.state is ConfigEntryState.LOADED + + mock_connect["connect"].assert_awaited_once_with(config=config) + + async def test_dhcp_discovery_with_ip_change( hass: HomeAssistant, mock_config_entry: MockConfigEntry, diff --git a/tests/components/tplink/test_init.py b/tests/components/tplink/test_init.py index 61ec9decc10..bfb7e02b63d 100644 --- a/tests/components/tplink/test_init.py +++ b/tests/components/tplink/test_init.py @@ -7,12 +7,16 @@ from datetime import timedelta from unittest.mock import AsyncMock, MagicMock, PropertyMock, patch from freezegun.api import FrozenDateTimeFactory -from kasa import AuthenticationError, Feature, KasaException, Module +from kasa import AuthenticationError, DeviceConfig, Feature, KasaException, Module import pytest from homeassistant import setup from homeassistant.components import tplink -from homeassistant.components.tplink.const import CONF_DEVICE_CONFIG, DOMAIN +from homeassistant.components.tplink.const import ( + CONF_CREDENTIALS_HASH, + CONF_DEVICE_CONFIG, + DOMAIN, +) from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState from homeassistant.const import ( CONF_AUTHENTICATION, @@ -458,7 +462,214 @@ async def test_unlink_devices( expected_identifiers = identifiers[:expected_count] assert device_entries[0].identifiers == set(expected_identifiers) assert entry.version == 1 - assert entry.minor_version == 3 + assert entry.minor_version == 4 msg = f"{expected_message} identifiers for device dummy (hs300): {set(identifiers)}" assert msg in caplog.text + + +async def test_move_credentials_hash( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test credentials hash moved to parent. + + As async_setup_entry will succeed the hash on the parent is updated + from the device. + """ + device_config = { + **DEVICE_CONFIG_AUTH.to_dict( + exclude_credentials=True, credentials_hash="theHash" + ) + } + entry_data = {**CREATE_ENTRY_DATA_AUTH, CONF_DEVICE_CONFIG: device_config} + + entry = MockConfigEntry( + title="TPLink", + domain=DOMAIN, + data=entry_data, + entry_id="123456", + unique_id=MAC_ADDRESS, + version=1, + minor_version=3, + ) + assert entry.data[CONF_DEVICE_CONFIG][CONF_CREDENTIALS_HASH] == "theHash" + entry.add_to_hass(hass) + + async def _connect(config): + config.credentials_hash = "theNewHash" + return _mocked_device(device_config=config, credentials_hash="theNewHash") + + with ( + patch("homeassistant.components.tplink.Device.connect", new=_connect), + patch("homeassistant.components.tplink.PLATFORMS", []), + ): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert entry.minor_version == 4 + assert entry.state is ConfigEntryState.LOADED + assert CONF_CREDENTIALS_HASH not in entry.data[CONF_DEVICE_CONFIG] + assert CONF_CREDENTIALS_HASH in entry.data + # Gets the new hash from the successful connection. + assert entry.data[CONF_CREDENTIALS_HASH] == "theNewHash" + assert "Migration to version 1.4 complete" in caplog.text + + +async def test_move_credentials_hash_auth_error( + hass: HomeAssistant, +) -> None: + """Test credentials hash moved to parent. + + If there is an auth error it should be deleted after migration + in async_setup_entry. + """ + device_config = { + **DEVICE_CONFIG_AUTH.to_dict( + exclude_credentials=True, credentials_hash="theHash" + ) + } + entry_data = {**CREATE_ENTRY_DATA_AUTH, CONF_DEVICE_CONFIG: device_config} + + entry = MockConfigEntry( + title="TPLink", + domain=DOMAIN, + data=entry_data, + unique_id=MAC_ADDRESS, + version=1, + minor_version=3, + ) + assert entry.data[CONF_DEVICE_CONFIG][CONF_CREDENTIALS_HASH] == "theHash" + + with ( + patch( + "homeassistant.components.tplink.Device.connect", + side_effect=AuthenticationError, + ), + patch("homeassistant.components.tplink.PLATFORMS", []), + ): + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert entry.minor_version == 4 + assert entry.state is ConfigEntryState.SETUP_ERROR + assert CONF_CREDENTIALS_HASH not in entry.data[CONF_DEVICE_CONFIG] + # Auth failure deletes the hash + assert CONF_CREDENTIALS_HASH not in entry.data + + +async def test_move_credentials_hash_other_error( + hass: HomeAssistant, +) -> None: + """Test credentials hash moved to parent. + + When there is a KasaException the same hash should still be on the parent + at the end of the test. + """ + device_config = { + **DEVICE_CONFIG_AUTH.to_dict( + exclude_credentials=True, credentials_hash="theHash" + ) + } + entry_data = {**CREATE_ENTRY_DATA_AUTH, CONF_DEVICE_CONFIG: device_config} + + entry = MockConfigEntry( + title="TPLink", + domain=DOMAIN, + data=entry_data, + unique_id=MAC_ADDRESS, + version=1, + minor_version=3, + ) + assert entry.data[CONF_DEVICE_CONFIG][CONF_CREDENTIALS_HASH] == "theHash" + + with ( + patch( + "homeassistant.components.tplink.Device.connect", side_effect=KasaException + ), + patch("homeassistant.components.tplink.PLATFORMS", []), + ): + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert entry.minor_version == 4 + assert entry.state is ConfigEntryState.SETUP_RETRY + assert CONF_CREDENTIALS_HASH not in entry.data[CONF_DEVICE_CONFIG] + assert CONF_CREDENTIALS_HASH in entry.data + assert entry.data[CONF_CREDENTIALS_HASH] == "theHash" + + +async def test_credentials_hash( + hass: HomeAssistant, +) -> None: + """Test credentials_hash used to call connect.""" + device_config = {**DEVICE_CONFIG_AUTH.to_dict(exclude_credentials=True)} + entry_data = { + **CREATE_ENTRY_DATA_AUTH, + CONF_DEVICE_CONFIG: device_config, + CONF_CREDENTIALS_HASH: "theHash", + } + + entry = MockConfigEntry( + title="TPLink", + domain=DOMAIN, + data=entry_data, + unique_id=MAC_ADDRESS, + ) + + async def _connect(config): + config.credentials_hash = "theHash" + return _mocked_device(device_config=config, credentials_hash="theHash") + + with ( + patch("homeassistant.components.tplink.PLATFORMS", []), + patch("homeassistant.components.tplink.Device.connect", new=_connect), + ): + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state is ConfigEntryState.LOADED + assert CONF_CREDENTIALS_HASH not in entry.data[CONF_DEVICE_CONFIG] + assert CONF_CREDENTIALS_HASH in entry.data + assert entry.data[CONF_DEVICE_CONFIG] == device_config + assert entry.data[CONF_CREDENTIALS_HASH] == "theHash" + + +async def test_credentials_hash_auth_error( + hass: HomeAssistant, +) -> None: + """Test credentials_hash is deleted after an auth failure.""" + device_config = {**DEVICE_CONFIG_AUTH.to_dict(exclude_credentials=True)} + entry_data = { + **CREATE_ENTRY_DATA_AUTH, + CONF_DEVICE_CONFIG: device_config, + CONF_CREDENTIALS_HASH: "theHash", + } + + entry = MockConfigEntry( + title="TPLink", + domain=DOMAIN, + data=entry_data, + unique_id=MAC_ADDRESS, + ) + + with ( + patch("homeassistant.components.tplink.PLATFORMS", []), + patch( + "homeassistant.components.tplink.Device.connect", + side_effect=AuthenticationError, + ) as connect_mock, + ): + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + expected_config = DeviceConfig.from_dict( + DEVICE_CONFIG_AUTH.to_dict(exclude_credentials=True, credentials_hash="theHash") + ) + connect_mock.assert_called_with(config=expected_config) + assert entry.state is ConfigEntryState.SETUP_ERROR + assert CONF_CREDENTIALS_HASH not in entry.data From 18d283bed6ecef32dfe1abe92229c832a59ab048 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 27 Jun 2024 09:26:31 +0200 Subject: [PATCH 1321/1445] Don't allow updating a device to have no connections or identifiers (#120603) * Don't allow updating a device to have no connections or identifiers * Move check to the top of the function --- homeassistant/helpers/device_registry.py | 5 +++++ tests/helpers/test_device_registry.py | 19 +++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index cfafa63ec3a..4579739f0e1 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -869,6 +869,11 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]): ) add_config_entry = config_entry + if not new_connections and not new_identifiers: + raise HomeAssistantError( + "A device must have at least one of identifiers or connections" + ) + if merge_connections is not UNDEFINED and new_connections is not UNDEFINED: raise HomeAssistantError( "Cannot define both merge_connections and new_connections" diff --git a/tests/helpers/test_device_registry.py b/tests/helpers/test_device_registry.py index fa57cc7557e..3a525f00870 100644 --- a/tests/helpers/test_device_registry.py +++ b/tests/helpers/test_device_registry.py @@ -3052,3 +3052,22 @@ async def test_primary_config_entry( model="model", ) assert device.primary_config_entry == mock_config_entry_1.entry_id + + +async def test_update_device_no_connections_or_identifiers( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, +) -> None: + """Test updating a device clearing connections and identifiers.""" + mock_config_entry = MockConfigEntry(domain="mqtt", title=None) + mock_config_entry.add_to_hass(hass) + + device = device_registry.async_get_or_create( + config_entry_id=mock_config_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + identifiers={("bridgeid", "0123")}, + ) + with pytest.raises(HomeAssistantError): + device_registry.async_update_device( + device.id, new_connections=set(), new_identifiers=set() + ) From ef47daad9d398de853989f4abdb317b17e442aca Mon Sep 17 00:00:00 2001 From: Luke Lashley Date: Wed, 26 Jun 2024 19:14:18 -0400 Subject: [PATCH 1322/1445] Bump anova_wifi to 0.14.0 (#120616) --- homeassistant/components/anova/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/anova/manifest.json b/homeassistant/components/anova/manifest.json index 331a4f61118..d75a791a6f5 100644 --- a/homeassistant/components/anova/manifest.json +++ b/homeassistant/components/anova/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/anova", "iot_class": "cloud_push", "loggers": ["anova_wifi"], - "requirements": ["anova-wifi==0.12.0"] + "requirements": ["anova-wifi==0.14.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 67ad67799e9..a3aa0bbacc3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -449,7 +449,7 @@ androidtvremote2==0.1.1 anel-pwrctrl-homeassistant==0.0.1.dev2 # homeassistant.components.anova -anova-wifi==0.12.0 +anova-wifi==0.14.0 # homeassistant.components.anthemav anthemav==1.4.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 350b59c0eab..9b1d4743c9b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -413,7 +413,7 @@ androidtv[async]==0.0.73 androidtvremote2==0.1.1 # homeassistant.components.anova -anova-wifi==0.12.0 +anova-wifi==0.14.0 # homeassistant.components.anthemav anthemav==1.4.1 From 7519603bf5ead3a979cc14ed792c2f309fe79f25 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 26 Jun 2024 20:37:28 -0500 Subject: [PATCH 1323/1445] Bump uiprotect to 4.0.0 (#120617) --- homeassistant/components/unifiprotect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index 8e29f5ffb9f..bdbdacae90e 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -40,7 +40,7 @@ "integration_type": "hub", "iot_class": "local_push", "loggers": ["uiprotect", "unifi_discovery"], - "requirements": ["uiprotect==3.7.0", "unifi-discovery==1.1.8"], + "requirements": ["uiprotect==4.0.0", "unifi-discovery==1.1.8"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/requirements_all.txt b/requirements_all.txt index a3aa0bbacc3..44bc9f73b1d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2789,7 +2789,7 @@ twitchAPI==4.0.0 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==3.7.0 +uiprotect==4.0.0 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9b1d4743c9b..45cb1087cb4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2169,7 +2169,7 @@ twitchAPI==4.0.0 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==3.7.0 +uiprotect==4.0.0 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 From 7256f23376be8e28d4e16e210c7020abb107d619 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 27 Jun 2024 01:50:41 -0500 Subject: [PATCH 1324/1445] Fix performance regression in integration from state_reported (#120621) * Fix performance regression in integration from state_reported Because the callbacks were no longer indexed by entity id, users saw upwards of 1M calls/min https://github.com/home-assistant/core/pull/113869/files#r1655580523 * Update homeassistant/helpers/event.py * coverage --------- Co-authored-by: Paulus Schoutsen --- .../components/integration/sensor.py | 24 +++++++++---------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/integration/sensor.py b/homeassistant/components/integration/sensor.py index 4fca92e9b40..8cc5341f081 100644 --- a/homeassistant/components/integration/sensor.py +++ b/homeassistant/components/integration/sensor.py @@ -27,8 +27,6 @@ from homeassistant.const import ( CONF_METHOD, CONF_NAME, CONF_UNIQUE_ID, - EVENT_STATE_CHANGED, - EVENT_STATE_REPORTED, STATE_UNAVAILABLE, UnitOfTime, ) @@ -45,7 +43,11 @@ from homeassistant.helpers import config_validation as cv, entity_registry as er from homeassistant.helpers.device import async_device_info_to_link_from_entity from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.event import async_call_later +from homeassistant.helpers.event import ( + async_call_later, + async_track_state_change_event, + async_track_state_reported_event, +) from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .const import ( @@ -440,21 +442,17 @@ class IntegrationSensor(RestoreSensor): self._derive_and_set_attributes_from_state(state) self.async_on_remove( - self.hass.bus.async_listen( - EVENT_STATE_CHANGED, + async_track_state_change_event( + self.hass, + self._sensor_source_id, handle_state_change, - event_filter=callback( - lambda event_data: event_data["entity_id"] == self._sensor_source_id - ), ) ) self.async_on_remove( - self.hass.bus.async_listen( - EVENT_STATE_REPORTED, + async_track_state_reported_event( + self.hass, + self._sensor_source_id, handle_state_report, - event_filter=callback( - lambda event_data: event_data["entity_id"] == self._sensor_source_id - ), ) ) From 38601d48ffc155fb0d09ec08ad5090bd3d4163e6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 26 Jun 2024 22:04:27 -0500 Subject: [PATCH 1325/1445] Add async_track_state_reported_event to fix integration performance regression (#120622) split from https://github.com/home-assistant/core/pull/120621 --- homeassistant/helpers/event.py | 37 ++++++++++++++++++++++++++++------ tests/helpers/test_event.py | 32 ++++++++++++++++++++++++++++- 2 files changed, 62 insertions(+), 7 deletions(-) diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index 4150d871b6b..ebd51948e3b 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -17,6 +17,7 @@ from typing import TYPE_CHECKING, Any, Concatenate, Generic, TypeVar from homeassistant.const import ( EVENT_CORE_CONFIG_UPDATE, EVENT_STATE_CHANGED, + EVENT_STATE_REPORTED, MATCH_ALL, SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET, @@ -26,6 +27,7 @@ from homeassistant.core import ( Event, # Explicit reexport of 'EventStateChangedData' for backwards compatibility EventStateChangedData as EventStateChangedData, # noqa: PLC0414 + EventStateReportedData, HassJob, HassJobType, HomeAssistant, @@ -57,6 +59,9 @@ from .typing import TemplateVarsType _TRACK_STATE_CHANGE_DATA: HassKey[_KeyedEventData[EventStateChangedData]] = HassKey( "track_state_change_data" ) +_TRACK_STATE_REPORTED_DATA: HassKey[_KeyedEventData[EventStateReportedData]] = HassKey( + "track_state_reported_data" +) _TRACK_STATE_ADDED_DOMAIN_DATA: HassKey[_KeyedEventData[EventStateChangedData]] = ( HassKey("track_state_added_domain_data") ) @@ -324,8 +329,8 @@ def async_track_state_change_event( @callback def _async_dispatch_entity_id_event( hass: HomeAssistant, - callbacks: dict[str, list[HassJob[[Event[EventStateChangedData]], Any]]], - event: Event[EventStateChangedData], + callbacks: dict[str, list[HassJob[[Event[_TypedDictT]], Any]]], + event: Event[_TypedDictT], ) -> None: """Dispatch to listeners.""" if not (callbacks_list := callbacks.get(event.data["entity_id"])): @@ -342,10 +347,10 @@ def _async_dispatch_entity_id_event( @callback -def _async_state_change_filter( +def _async_state_filter( hass: HomeAssistant, - callbacks: dict[str, list[HassJob[[Event[EventStateChangedData]], Any]]], - event_data: EventStateChangedData, + callbacks: dict[str, list[HassJob[[Event[_TypedDictT]], Any]]], + event_data: _TypedDictT, ) -> bool: """Filter state changes by entity_id.""" return event_data["entity_id"] in callbacks @@ -355,7 +360,7 @@ _KEYED_TRACK_STATE_CHANGE = _KeyedEventTracker( key=_TRACK_STATE_CHANGE_DATA, event_type=EVENT_STATE_CHANGED, dispatcher_callable=_async_dispatch_entity_id_event, - filter_callable=_async_state_change_filter, + filter_callable=_async_state_filter, ) @@ -372,6 +377,26 @@ def _async_track_state_change_event( ) +_KEYED_TRACK_STATE_REPORTED = _KeyedEventTracker( + key=_TRACK_STATE_REPORTED_DATA, + event_type=EVENT_STATE_REPORTED, + dispatcher_callable=_async_dispatch_entity_id_event, + filter_callable=_async_state_filter, +) + + +def async_track_state_reported_event( + hass: HomeAssistant, + entity_ids: str | Iterable[str], + action: Callable[[Event[EventStateReportedData]], Any], + job_type: HassJobType | None = None, +) -> CALLBACK_TYPE: + """Track EVENT_STATE_REPORTED by entity_id without lowercasing.""" + return _async_track_event( + _KEYED_TRACK_STATE_REPORTED, hass, entity_ids, action, job_type + ) + + @callback def _remove_empty_listener() -> None: """Remove a listener that does nothing.""" diff --git a/tests/helpers/test_event.py b/tests/helpers/test_event.py index edce36218e8..4f983120e36 100644 --- a/tests/helpers/test_event.py +++ b/tests/helpers/test_event.py @@ -15,7 +15,13 @@ import pytest from homeassistant.const import MATCH_ALL import homeassistant.core as ha -from homeassistant.core import Event, EventStateChangedData, HomeAssistant, callback +from homeassistant.core import ( + Event, + EventStateChangedData, + EventStateReportedData, + HomeAssistant, + callback, +) from homeassistant.exceptions import TemplateError from homeassistant.helpers.device_registry import EVENT_DEVICE_REGISTRY_UPDATED from homeassistant.helpers.entity_registry import EVENT_ENTITY_REGISTRY_UPDATED @@ -34,6 +40,7 @@ from homeassistant.helpers.event import ( async_track_state_change_event, async_track_state_change_filtered, async_track_state_removed_domain, + async_track_state_reported_event, async_track_sunrise, async_track_sunset, async_track_template, @@ -4907,3 +4914,26 @@ async def test_track_point_in_time_repr( assert "Exception in callback _TrackPointUTCTime" in caplog.text assert "._raise_exception" in caplog.text await hass.async_block_till_done(wait_background_tasks=True) + + +async def test_async_track_state_reported_event(hass: HomeAssistant) -> None: + """Test async_track_state_reported_event.""" + tracker_called: list[ha.State] = [] + + @ha.callback + def single_run_callback(event: Event[EventStateReportedData]) -> None: + new_state = event.data["new_state"] + tracker_called.append(new_state) + + unsub = async_track_state_reported_event( + hass, ["light.bowl", "light.top"], single_run_callback + ) + hass.states.async_set("light.bowl", "on") + hass.states.async_set("light.top", "on") + await hass.async_block_till_done() + assert len(tracker_called) == 0 + hass.states.async_set("light.bowl", "on") + hass.states.async_set("light.top", "on") + await hass.async_block_till_done() + assert len(tracker_called) == 2 + unsub() From 1933454b76249bf6b9ba971e7acc0244f18b5a69 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 27 Jun 2024 13:45:15 +0200 Subject: [PATCH 1326/1445] Rename async_track_state_reported_event to async_track_state_report_event (#120637) * Rename async_track_state_reported_event to async_track_state_report_event * Update tests --- homeassistant/components/integration/sensor.py | 4 ++-- homeassistant/helpers/event.py | 12 ++++++------ tests/helpers/test_event.py | 8 ++++---- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/integration/sensor.py b/homeassistant/components/integration/sensor.py index 8cc5341f081..a053e5cea5c 100644 --- a/homeassistant/components/integration/sensor.py +++ b/homeassistant/components/integration/sensor.py @@ -46,7 +46,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import ( async_call_later, async_track_state_change_event, - async_track_state_reported_event, + async_track_state_report_event, ) from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -449,7 +449,7 @@ class IntegrationSensor(RestoreSensor): ) ) self.async_on_remove( - async_track_state_reported_event( + async_track_state_report_event( self.hass, self._sensor_source_id, handle_state_report, diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index ebd51948e3b..51c1a7ba30f 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -59,8 +59,8 @@ from .typing import TemplateVarsType _TRACK_STATE_CHANGE_DATA: HassKey[_KeyedEventData[EventStateChangedData]] = HassKey( "track_state_change_data" ) -_TRACK_STATE_REPORTED_DATA: HassKey[_KeyedEventData[EventStateReportedData]] = HassKey( - "track_state_reported_data" +_TRACK_STATE_REPORT_DATA: HassKey[_KeyedEventData[EventStateReportedData]] = HassKey( + "track_state_report_data" ) _TRACK_STATE_ADDED_DOMAIN_DATA: HassKey[_KeyedEventData[EventStateChangedData]] = ( HassKey("track_state_added_domain_data") @@ -377,15 +377,15 @@ def _async_track_state_change_event( ) -_KEYED_TRACK_STATE_REPORTED = _KeyedEventTracker( - key=_TRACK_STATE_REPORTED_DATA, +_KEYED_TRACK_STATE_REPORT = _KeyedEventTracker( + key=_TRACK_STATE_REPORT_DATA, event_type=EVENT_STATE_REPORTED, dispatcher_callable=_async_dispatch_entity_id_event, filter_callable=_async_state_filter, ) -def async_track_state_reported_event( +def async_track_state_report_event( hass: HomeAssistant, entity_ids: str | Iterable[str], action: Callable[[Event[EventStateReportedData]], Any], @@ -393,7 +393,7 @@ def async_track_state_reported_event( ) -> CALLBACK_TYPE: """Track EVENT_STATE_REPORTED by entity_id without lowercasing.""" return _async_track_event( - _KEYED_TRACK_STATE_REPORTED, hass, entity_ids, action, job_type + _KEYED_TRACK_STATE_REPORT, hass, entity_ids, action, job_type ) diff --git a/tests/helpers/test_event.py b/tests/helpers/test_event.py index 4f983120e36..4bb4c1a1967 100644 --- a/tests/helpers/test_event.py +++ b/tests/helpers/test_event.py @@ -40,7 +40,7 @@ from homeassistant.helpers.event import ( async_track_state_change_event, async_track_state_change_filtered, async_track_state_removed_domain, - async_track_state_reported_event, + async_track_state_report_event, async_track_sunrise, async_track_sunset, async_track_template, @@ -4916,8 +4916,8 @@ async def test_track_point_in_time_repr( await hass.async_block_till_done(wait_background_tasks=True) -async def test_async_track_state_reported_event(hass: HomeAssistant) -> None: - """Test async_track_state_reported_event.""" +async def test_async_track_state_report_event(hass: HomeAssistant) -> None: + """Test async_track_state_report_event.""" tracker_called: list[ha.State] = [] @ha.callback @@ -4925,7 +4925,7 @@ async def test_async_track_state_reported_event(hass: HomeAssistant) -> None: new_state = event.data["new_state"] tracker_called.append(new_state) - unsub = async_track_state_reported_event( + unsub = async_track_state_report_event( hass, ["light.bowl", "light.top"], single_run_callback ) hass.states.async_set("light.bowl", "on") From 89ac3ce832981fac544befde1fea1a6f3347e0c0 Mon Sep 17 00:00:00 2001 From: Josef Zweck <24647999+zweckj@users.noreply.github.com> Date: Thu, 27 Jun 2024 09:21:41 +0200 Subject: [PATCH 1327/1445] Fix the version that raises the issue (#120638) --- homeassistant/components/lamarzocco/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/lamarzocco/__init__.py b/homeassistant/components/lamarzocco/__init__.py index 9c66fdd1b60..dfcaa54047d 100644 --- a/homeassistant/components/lamarzocco/__init__.py +++ b/homeassistant/components/lamarzocco/__init__.py @@ -112,7 +112,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: LaMarzoccoConfigEntry) - entry.runtime_data = coordinator gateway_version = coordinator.device.firmware[FirmwareType.GATEWAY].current_version - if version.parse(gateway_version) < version.parse("v3.5-rc5"): + if version.parse(gateway_version) < version.parse("v3.4-rc5"): # incompatible gateway firmware, create an issue ir.async_create_issue( hass, From b290e9535055398e4457aae802da5bd4afc073c2 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 27 Jun 2024 13:08:19 +0200 Subject: [PATCH 1328/1445] Improve typing of state event helpers (#120639) --- homeassistant/core.py | 15 +++++++++------ homeassistant/helpers/event.py | 10 ++++++---- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/homeassistant/core.py b/homeassistant/core.py index 2b43b2d40ff..71ee5f4bd1d 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -158,26 +158,29 @@ class ConfigSource(enum.StrEnum): YAML = "yaml" -class EventStateChangedData(TypedDict): +class EventStateEventData(TypedDict): + """Base class for EVENT_STATE_CHANGED and EVENT_STATE_CHANGED data.""" + + entity_id: str + new_state: State | None + + +class EventStateChangedData(EventStateEventData): """EVENT_STATE_CHANGED data. A state changed event is fired when on state write when the state is changed. """ - entity_id: str old_state: State | None - new_state: State | None -class EventStateReportedData(TypedDict): +class EventStateReportedData(EventStateEventData): """EVENT_STATE_REPORTED data. A state reported event is fired when on state write when the state is unchanged. """ - entity_id: str old_last_reported: datetime.datetime - new_state: State | None # SOURCE_* are deprecated as of Home Assistant 2022.2, use ConfigSource instead diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index 51c1a7ba30f..0c77809079e 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -27,6 +27,7 @@ from homeassistant.core import ( Event, # Explicit reexport of 'EventStateChangedData' for backwards compatibility EventStateChangedData as EventStateChangedData, # noqa: PLC0414 + EventStateEventData, EventStateReportedData, HassJob, HassJobType, @@ -89,6 +90,7 @@ RANDOM_MICROSECOND_MIN = 50000 RANDOM_MICROSECOND_MAX = 500000 _TypedDictT = TypeVar("_TypedDictT", bound=Mapping[str, Any]) +_StateEventDataT = TypeVar("_StateEventDataT", bound=EventStateEventData) @dataclass(slots=True, frozen=True) @@ -329,8 +331,8 @@ def async_track_state_change_event( @callback def _async_dispatch_entity_id_event( hass: HomeAssistant, - callbacks: dict[str, list[HassJob[[Event[_TypedDictT]], Any]]], - event: Event[_TypedDictT], + callbacks: dict[str, list[HassJob[[Event[_StateEventDataT]], Any]]], + event: Event[_StateEventDataT], ) -> None: """Dispatch to listeners.""" if not (callbacks_list := callbacks.get(event.data["entity_id"])): @@ -349,8 +351,8 @@ def _async_dispatch_entity_id_event( @callback def _async_state_filter( hass: HomeAssistant, - callbacks: dict[str, list[HassJob[[Event[_TypedDictT]], Any]]], - event_data: _TypedDictT, + callbacks: dict[str, list[HassJob[[Event[_StateEventDataT]], Any]]], + event_data: _StateEventDataT, ) -> bool: """Filter state changes by entity_id.""" return event_data["entity_id"] in callbacks From 4836d6620b833186d16e120d0825ef1c4b029193 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 27 Jun 2024 10:43:28 +0200 Subject: [PATCH 1329/1445] Add snapshots to tasmota sensor test (#120647) --- .../tasmota/snapshots/test_sensor.ambr | 1526 +++++++++++++++++ tests/components/tasmota/test_sensor.py | 218 +-- 2 files changed, 1533 insertions(+), 211 deletions(-) create mode 100644 tests/components/tasmota/snapshots/test_sensor.ambr diff --git a/tests/components/tasmota/snapshots/test_sensor.ambr b/tests/components/tasmota/snapshots/test_sensor.ambr new file mode 100644 index 00000000000..744554c7246 --- /dev/null +++ b/tests/components/tasmota/snapshots/test_sensor.ambr @@ -0,0 +1,1526 @@ +# serializer version: 1 +# name: test_controlling_state_via_mqtt[sensor_config0-entity_ids0-messages0] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Tasmota DHT11 Temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_dht11_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config0-entity_ids0-messages0].1 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.tasmota_dht11_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'DHT11 Temperature', + 'platform': 'tasmota', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000049A3BC_sensor_sensor_DHT11_Temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config0-entity_ids0-messages0].2 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Tasmota DHT11 Temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_dht11_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '20.5', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config0-entity_ids0-messages0].3 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Tasmota DHT11 Temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_dht11_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '20.0', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config1-entity_ids1-messages1] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Tasmota TX23 Speed Act', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_tx23_speed_act', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config1-entity_ids1-messages1].1 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.tasmota_tx23_speed_act', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'TX23 Speed Act', + 'platform': 'tasmota', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000049A3BC_sensor_sensor_TX23_Speed_Act', + 'unit_of_measurement': , + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config1-entity_ids1-messages1].2 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Tasmota TX23 Dir Card', + }), + 'context': , + 'entity_id': 'sensor.tasmota_tx23_dir_card', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config1-entity_ids1-messages1].3 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.tasmota_tx23_dir_card', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'TX23 Dir Card', + 'platform': 'tasmota', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000049A3BC_sensor_sensor_TX23_Dir_Card', + 'unit_of_measurement': None, + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config1-entity_ids1-messages1].4 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Tasmota TX23 Speed Act', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_tx23_speed_act', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '12.3', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config1-entity_ids1-messages1].5 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Tasmota TX23 Dir Card', + }), + 'context': , + 'entity_id': 'sensor.tasmota_tx23_dir_card', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'WSW', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config1-entity_ids1-messages1].6 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Tasmota TX23 Speed Act', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_tx23_speed_act', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '23.4', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config1-entity_ids1-messages1].7 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Tasmota TX23 Dir Card', + }), + 'context': , + 'entity_id': 'sensor.tasmota_tx23_dir_card', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'ESE', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config2-entity_ids2-messages2] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Tasmota ENERGY TotalTariff 0', + }), + 'context': , + 'entity_id': 'sensor.tasmota_energy_totaltariff_0', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config2-entity_ids2-messages2].1 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.tasmota_energy_totaltariff_0', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'ENERGY TotalTariff 0', + 'platform': 'tasmota', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000049A3BC_sensor_sensor_ENERGY_TotalTariff_0', + 'unit_of_measurement': None, + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config2-entity_ids2-messages2].2 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Tasmota ENERGY TotalTariff 1', + }), + 'context': , + 'entity_id': 'sensor.tasmota_energy_totaltariff_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config2-entity_ids2-messages2].3 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.tasmota_energy_totaltariff_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'ENERGY TotalTariff 1', + 'platform': 'tasmota', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000049A3BC_sensor_sensor_ENERGY_TotalTariff_1', + 'unit_of_measurement': None, + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config2-entity_ids2-messages2].4 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Tasmota ENERGY TotalTariff 0', + }), + 'context': , + 'entity_id': 'sensor.tasmota_energy_totaltariff_0', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1.2', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config2-entity_ids2-messages2].5 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Tasmota ENERGY TotalTariff 1', + }), + 'context': , + 'entity_id': 'sensor.tasmota_energy_totaltariff_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '3.4', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config2-entity_ids2-messages2].6 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Tasmota ENERGY TotalTariff 0', + }), + 'context': , + 'entity_id': 'sensor.tasmota_energy_totaltariff_0', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '5.6', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config2-entity_ids2-messages2].7 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Tasmota ENERGY TotalTariff 1', + }), + 'context': , + 'entity_id': 'sensor.tasmota_energy_totaltariff_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '7.8', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config3-entity_ids3-messages3] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Tasmota DS18B20 Temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_ds18b20_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config3-entity_ids3-messages3].1 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.tasmota_ds18b20_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'DS18B20 Temperature', + 'platform': 'tasmota', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000049A3BC_sensor_sensor_DS18B20_Temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config3-entity_ids3-messages3].2 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Tasmota DS18B20 Id', + }), + 'context': , + 'entity_id': 'sensor.tasmota_ds18b20_id', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config3-entity_ids3-messages3].3 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.tasmota_ds18b20_id', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'DS18B20 Id', + 'platform': 'tasmota', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000049A3BC_sensor_sensor_DS18B20_Id', + 'unit_of_measurement': None, + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config3-entity_ids3-messages3].4 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Tasmota DS18B20 Temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_ds18b20_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '12.3', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config3-entity_ids3-messages3].5 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Tasmota DS18B20 Id', + }), + 'context': , + 'entity_id': 'sensor.tasmota_ds18b20_id', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '01191ED79190', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config3-entity_ids3-messages3].6 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Tasmota DS18B20 Temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_ds18b20_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '23.4', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config3-entity_ids3-messages3].7 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Tasmota DS18B20 Id', + }), + 'context': , + 'entity_id': 'sensor.tasmota_ds18b20_id', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'meep', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config4-entity_ids4-messages4] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Tasmota ENERGY Total', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_energy_total', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config4-entity_ids4-messages4].1 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.tasmota_energy_total', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'ENERGY Total', + 'platform': 'tasmota', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000049A3BC_sensor_sensor_ENERGY_Total', + 'unit_of_measurement': , + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config4-entity_ids4-messages4].2 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Tasmota ENERGY Total', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_energy_total', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1.2', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config4-entity_ids4-messages4].3 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Tasmota ENERGY Total', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_energy_total', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '5.6', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config5-entity_ids5-messages5] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Tasmota ENERGY Total 0', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_energy_total_0', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config5-entity_ids5-messages5].1 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.tasmota_energy_total_0', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'ENERGY Total 0', + 'platform': 'tasmota', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000049A3BC_sensor_sensor_ENERGY_Total_0', + 'unit_of_measurement': , + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config5-entity_ids5-messages5].2 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Tasmota ENERGY Total 1', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_energy_total_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config5-entity_ids5-messages5].3 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.tasmota_energy_total_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'ENERGY Total 1', + 'platform': 'tasmota', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000049A3BC_sensor_sensor_ENERGY_Total_1', + 'unit_of_measurement': , + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config5-entity_ids5-messages5].4 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Tasmota ENERGY Total 0', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_energy_total_0', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1.2', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config5-entity_ids5-messages5].5 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Tasmota ENERGY Total 1', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_energy_total_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '3.4', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config5-entity_ids5-messages5].6 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Tasmota ENERGY Total 0', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_energy_total_0', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '5.6', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config5-entity_ids5-messages5].7 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Tasmota ENERGY Total 1', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_energy_total_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '7.8', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config6-entity_ids6-messages6] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Tasmota ENERGY Total Phase1', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_energy_total_phase1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config6-entity_ids6-messages6].1 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.tasmota_energy_total_phase1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'ENERGY Total Phase1', + 'platform': 'tasmota', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000049A3BC_sensor_sensor_ENERGY_Total_Phase1', + 'unit_of_measurement': , + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config6-entity_ids6-messages6].2 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Tasmota ENERGY Total Phase2', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_energy_total_phase2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config6-entity_ids6-messages6].3 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.tasmota_energy_total_phase2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'ENERGY Total Phase2', + 'platform': 'tasmota', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000049A3BC_sensor_sensor_ENERGY_Total_Phase2', + 'unit_of_measurement': , + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config6-entity_ids6-messages6].4 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Tasmota ENERGY Total Phase1', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_energy_total_phase1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1.2', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config6-entity_ids6-messages6].5 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Tasmota ENERGY Total Phase2', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_energy_total_phase2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '3.4', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config6-entity_ids6-messages6].6 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Tasmota ENERGY Total Phase1', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_energy_total_phase1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '5.6', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config6-entity_ids6-messages6].7 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Tasmota ENERGY Total Phase2', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_energy_total_phase2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '7.8', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config7-entity_ids7-messages7] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Tasmota ANALOG Temperature1', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_analog_temperature1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config7-entity_ids7-messages7].1 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.tasmota_analog_temperature1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'ANALOG Temperature1', + 'platform': 'tasmota', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000049A3BC_sensor_sensor_ANALOG_Temperature1', + 'unit_of_measurement': , + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config7-entity_ids7-messages7].10 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Tasmota ANALOG Temperature2', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_analog_temperature2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '9.0', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config7-entity_ids7-messages7].11 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'illuminance', + 'friendly_name': 'Tasmota ANALOG Illuminance3', + 'state_class': , + 'unit_of_measurement': 'lx', + }), + 'context': , + 'entity_id': 'sensor.tasmota_analog_illuminance3', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1.2', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config7-entity_ids7-messages7].2 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Tasmota ANALOG Temperature2', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_analog_temperature2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config7-entity_ids7-messages7].3 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.tasmota_analog_temperature2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'ANALOG Temperature2', + 'platform': 'tasmota', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000049A3BC_sensor_sensor_ANALOG_Temperature2', + 'unit_of_measurement': , + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config7-entity_ids7-messages7].4 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'illuminance', + 'friendly_name': 'Tasmota ANALOG Illuminance3', + 'state_class': , + 'unit_of_measurement': 'lx', + }), + 'context': , + 'entity_id': 'sensor.tasmota_analog_illuminance3', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config7-entity_ids7-messages7].5 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.tasmota_analog_illuminance3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'ANALOG Illuminance3', + 'platform': 'tasmota', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000049A3BC_sensor_sensor_ANALOG_Illuminance3', + 'unit_of_measurement': 'lx', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config7-entity_ids7-messages7].6 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Tasmota ANALOG Temperature1', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_analog_temperature1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1.2', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config7-entity_ids7-messages7].7 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Tasmota ANALOG Temperature2', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_analog_temperature2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '3.4', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config7-entity_ids7-messages7].8 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'illuminance', + 'friendly_name': 'Tasmota ANALOG Illuminance3', + 'state_class': , + 'unit_of_measurement': 'lx', + }), + 'context': , + 'entity_id': 'sensor.tasmota_analog_illuminance3', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '5.6', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config7-entity_ids7-messages7].9 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Tasmota ANALOG Temperature1', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_analog_temperature1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '7.8', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config8-entity_ids8-messages8] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Tasmota ANALOG CTEnergy1 Energy', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_analog_ctenergy1_energy', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config8-entity_ids8-messages8].1 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.tasmota_analog_ctenergy1_energy', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'ANALOG CTEnergy1 Energy', + 'platform': 'tasmota', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000049A3BC_sensor_sensor_ANALOG_CTEnergy1_Energy', + 'unit_of_measurement': , + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config8-entity_ids8-messages8].10 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'Tasmota ANALOG CTEnergy1 Voltage', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_analog_ctenergy1_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '230', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config8-entity_ids8-messages8].11 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'current', + 'friendly_name': 'Tasmota ANALOG CTEnergy1 Current', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_analog_ctenergy1_current', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '10', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config8-entity_ids8-messages8].12 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Tasmota ANALOG CTEnergy1 Energy', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_analog_ctenergy1_energy', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1.0', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config8-entity_ids8-messages8].13 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Tasmota ANALOG CTEnergy1 Power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_analog_ctenergy1_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1150', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config8-entity_ids8-messages8].14 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'Tasmota ANALOG CTEnergy1 Voltage', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_analog_ctenergy1_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '230', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config8-entity_ids8-messages8].15 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'current', + 'friendly_name': 'Tasmota ANALOG CTEnergy1 Current', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_analog_ctenergy1_current', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '5', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config8-entity_ids8-messages8].2 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Tasmota ANALOG CTEnergy1 Power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_analog_ctenergy1_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config8-entity_ids8-messages8].3 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.tasmota_analog_ctenergy1_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'ANALOG CTEnergy1 Power', + 'platform': 'tasmota', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000049A3BC_sensor_sensor_ANALOG_CTEnergy1_Power', + 'unit_of_measurement': , + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config8-entity_ids8-messages8].4 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'Tasmota ANALOG CTEnergy1 Voltage', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_analog_ctenergy1_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config8-entity_ids8-messages8].5 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.tasmota_analog_ctenergy1_voltage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'ANALOG CTEnergy1 Voltage', + 'platform': 'tasmota', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000049A3BC_sensor_sensor_ANALOG_CTEnergy1_Voltage', + 'unit_of_measurement': , + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config8-entity_ids8-messages8].6 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'current', + 'friendly_name': 'Tasmota ANALOG CTEnergy1 Current', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_analog_ctenergy1_current', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config8-entity_ids8-messages8].7 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.tasmota_analog_ctenergy1_current', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'ANALOG CTEnergy1 Current', + 'platform': 'tasmota', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000049A3BC_sensor_sensor_ANALOG_CTEnergy1_Current', + 'unit_of_measurement': , + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config8-entity_ids8-messages8].8 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Tasmota ANALOG CTEnergy1 Energy', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_analog_ctenergy1_energy', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.5', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config8-entity_ids8-messages8].9 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Tasmota ANALOG CTEnergy1 Power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_analog_ctenergy1_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2300', + }) +# --- diff --git a/tests/components/tasmota/test_sensor.py b/tests/components/tasmota/test_sensor.py index 2de80de4319..c01485d12a7 100644 --- a/tests/components/tasmota/test_sensor.py +++ b/tests/components/tasmota/test_sensor.py @@ -13,9 +13,9 @@ from hatasmota.utils import ( get_topic_tele_will, ) import pytest +from syrupy import SnapshotAssertion from homeassistant import config_entries -from homeassistant.components.sensor import ATTR_STATE_CLASS, SensorStateClass from homeassistant.components.tasmota.const import DEFAULT_PREFIX from homeassistant.const import ATTR_ASSUMED_STATE, STATE_UNKNOWN, Platform from homeassistant.core import HomeAssistant @@ -175,7 +175,7 @@ TEMPERATURE_SENSOR_CONFIG = { @pytest.mark.parametrize( - ("sensor_config", "entity_ids", "messages", "states"), + ("sensor_config", "entity_ids", "messages"), [ ( DEFAULT_SENSOR_CONFIG, @@ -184,20 +184,6 @@ TEMPERATURE_SENSOR_CONFIG = { '{"DHT11":{"Temperature":20.5}}', '{"StatusSNS":{"DHT11":{"Temperature":20.0}}}', ), - ( - { - "sensor.tasmota_dht11_temperature": { - "state": "20.5", - "attributes": { - "device_class": "temperature", - "unit_of_measurement": "°C", - }, - }, - }, - { - "sensor.tasmota_dht11_temperature": {"state": "20.0"}, - }, - ), ), ( DICT_SENSOR_CONFIG_1, @@ -206,22 +192,6 @@ TEMPERATURE_SENSOR_CONFIG = { '{"TX23":{"Speed":{"Act":"12.3"},"Dir": {"Card": "WSW"}}}', '{"StatusSNS":{"TX23":{"Speed":{"Act":"23.4"},"Dir": {"Card": "ESE"}}}}', ), - ( - { - "sensor.tasmota_tx23_speed_act": { - "state": "12.3", - "attributes": { - "device_class": None, - "unit_of_measurement": "km/h", - }, - }, - "sensor.tasmota_tx23_dir_card": {"state": "WSW"}, - }, - { - "sensor.tasmota_tx23_speed_act": {"state": "23.4"}, - "sensor.tasmota_tx23_dir_card": {"state": "ESE"}, - }, - ), ), ( LIST_SENSOR_CONFIG, @@ -233,22 +203,6 @@ TEMPERATURE_SENSOR_CONFIG = { '{"ENERGY":{"TotalTariff":[1.2,3.4]}}', '{"StatusSNS":{"ENERGY":{"TotalTariff":[5.6,7.8]}}}', ), - ( - { - "sensor.tasmota_energy_totaltariff_0": { - "state": "1.2", - "attributes": { - "device_class": None, - "unit_of_measurement": None, - }, - }, - "sensor.tasmota_energy_totaltariff_1": {"state": "3.4"}, - }, - { - "sensor.tasmota_energy_totaltariff_0": {"state": "5.6"}, - "sensor.tasmota_energy_totaltariff_1": {"state": "7.8"}, - }, - ), ), ( TEMPERATURE_SENSOR_CONFIG, @@ -257,22 +211,6 @@ TEMPERATURE_SENSOR_CONFIG = { '{"DS18B20":{"Id": "01191ED79190","Temperature": 12.3}}', '{"StatusSNS":{"DS18B20":{"Id": "meep","Temperature": 23.4}}}', ), - ( - { - "sensor.tasmota_ds18b20_temperature": { - "state": "12.3", - "attributes": { - "device_class": "temperature", - "unit_of_measurement": "°C", - }, - }, - "sensor.tasmota_ds18b20_id": {"state": "01191ED79190"}, - }, - { - "sensor.tasmota_ds18b20_temperature": {"state": "23.4"}, - "sensor.tasmota_ds18b20_id": {"state": "meep"}, - }, - ), ), # Test simple Total sensor ( @@ -282,21 +220,6 @@ TEMPERATURE_SENSOR_CONFIG = { '{"ENERGY":{"Total":1.2,"TotalStartTime":"2018-11-23T15:33:47"}}', '{"StatusSNS":{"ENERGY":{"Total":5.6,"TotalStartTime":"2018-11-23T16:33:47"}}}', ), - ( - { - "sensor.tasmota_energy_total": { - "state": "1.2", - "attributes": { - "device_class": "energy", - ATTR_STATE_CLASS: SensorStateClass.TOTAL, - "unit_of_measurement": "kWh", - }, - }, - }, - { - "sensor.tasmota_energy_total": {"state": "5.6"}, - }, - ), ), # Test list Total sensors ( @@ -306,30 +229,6 @@ TEMPERATURE_SENSOR_CONFIG = { '{"ENERGY":{"Total":[1.2, 3.4],"TotalStartTime":"2018-11-23T15:33:47"}}', '{"StatusSNS":{"ENERGY":{"Total":[5.6, 7.8],"TotalStartTime":"2018-11-23T16:33:47"}}}', ), - ( - { - "sensor.tasmota_energy_total_0": { - "state": "1.2", - "attributes": { - "device_class": "energy", - ATTR_STATE_CLASS: SensorStateClass.TOTAL, - "unit_of_measurement": "kWh", - }, - }, - "sensor.tasmota_energy_total_1": { - "state": "3.4", - "attributes": { - "device_class": "energy", - ATTR_STATE_CLASS: SensorStateClass.TOTAL, - "unit_of_measurement": "kWh", - }, - }, - }, - { - "sensor.tasmota_energy_total_0": {"state": "5.6"}, - "sensor.tasmota_energy_total_1": {"state": "7.8"}, - }, - ), ), # Test dict Total sensors ( @@ -342,30 +241,6 @@ TEMPERATURE_SENSOR_CONFIG = { '{"ENERGY":{"Total":{"Phase1":1.2, "Phase2":3.4},"TotalStartTime":"2018-11-23T15:33:47"}}', '{"StatusSNS":{"ENERGY":{"Total":{"Phase1":5.6, "Phase2":7.8},"TotalStartTime":"2018-11-23T15:33:47"}}}', ), - ( - { - "sensor.tasmota_energy_total_phase1": { - "state": "1.2", - "attributes": { - "device_class": "energy", - ATTR_STATE_CLASS: SensorStateClass.TOTAL, - "unit_of_measurement": "kWh", - }, - }, - "sensor.tasmota_energy_total_phase2": { - "state": "3.4", - "attributes": { - "device_class": "energy", - ATTR_STATE_CLASS: SensorStateClass.TOTAL, - "unit_of_measurement": "kWh", - }, - }, - }, - { - "sensor.tasmota_energy_total_phase1": {"state": "5.6"}, - "sensor.tasmota_energy_total_phase2": {"state": "7.8"}, - }, - ), ), ( NUMBERED_SENSOR_CONFIG, @@ -384,39 +259,6 @@ TEMPERATURE_SENSOR_CONFIG = { '"Illuminance3":1.2}}}' ), ), - ( - { - "sensor.tasmota_analog_temperature1": { - "state": "1.2", - "attributes": { - "device_class": "temperature", - ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, - "unit_of_measurement": "°C", - }, - }, - "sensor.tasmota_analog_temperature2": { - "state": "3.4", - "attributes": { - "device_class": "temperature", - ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, - "unit_of_measurement": "°C", - }, - }, - "sensor.tasmota_analog_illuminance3": { - "state": "5.6", - "attributes": { - "device_class": "illuminance", - ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, - "unit_of_measurement": "lx", - }, - }, - }, - { - "sensor.tasmota_analog_temperature1": {"state": "7.8"}, - "sensor.tasmota_analog_temperature2": {"state": "9.0"}, - "sensor.tasmota_analog_illuminance3": {"state": "1.2"}, - }, - ), ), ( NUMBERED_SENSOR_CONFIG_2, @@ -436,48 +278,6 @@ TEMPERATURE_SENSOR_CONFIG = { '{"Energy":1.0,"Power":1150,"Voltage":230,"Current":5}}}}' ), ), - ( - { - "sensor.tasmota_analog_ctenergy1_energy": { - "state": "0.5", - "attributes": { - "device_class": "energy", - ATTR_STATE_CLASS: SensorStateClass.TOTAL, - "unit_of_measurement": "kWh", - }, - }, - "sensor.tasmota_analog_ctenergy1_power": { - "state": "2300", - "attributes": { - "device_class": "power", - ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, - "unit_of_measurement": "W", - }, - }, - "sensor.tasmota_analog_ctenergy1_voltage": { - "state": "230", - "attributes": { - "device_class": "voltage", - ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, - "unit_of_measurement": "V", - }, - }, - "sensor.tasmota_analog_ctenergy1_current": { - "state": "10", - "attributes": { - "device_class": "current", - ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, - "unit_of_measurement": "A", - }, - }, - }, - { - "sensor.tasmota_analog_ctenergy1_energy": {"state": "1.0"}, - "sensor.tasmota_analog_ctenergy1_power": {"state": "1150"}, - "sensor.tasmota_analog_ctenergy1_voltage": {"state": "230"}, - "sensor.tasmota_analog_ctenergy1_current": {"state": "5"}, - }, - ), ), ], ) @@ -485,11 +285,11 @@ async def test_controlling_state_via_mqtt( hass: HomeAssistant, entity_registry: er.EntityRegistry, mqtt_mock: MqttMockHAClient, + snapshot: SnapshotAssertion, setup_tasmota, sensor_config, entity_ids, messages, - states, ) -> None: """Test state update via MQTT.""" config = copy.deepcopy(DEFAULT_CONFIG) @@ -513,11 +313,13 @@ async def test_controlling_state_via_mqtt( state = hass.states.get(entity_id) assert state.state == "unavailable" assert not state.attributes.get(ATTR_ASSUMED_STATE) + assert state == snapshot entry = entity_registry.async_get(entity_id) assert entry.disabled is False assert entry.disabled_by is None assert entry.entity_category is None + assert entry == snapshot async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") await hass.async_block_till_done() @@ -530,19 +332,13 @@ async def test_controlling_state_via_mqtt( async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/SENSOR", messages[0]) for entity_id in entity_ids: state = hass.states.get(entity_id) - expected_state = states[0][entity_id] - assert state.state == expected_state["state"] - for attribute, expected in expected_state.get("attributes", {}).items(): - assert state.attributes.get(attribute) == expected + assert state == snapshot # Test polled state update async_fire_mqtt_message(hass, "tasmota_49A3BC/stat/STATUS10", messages[1]) for entity_id in entity_ids: state = hass.states.get(entity_id) - expected_state = states[1][entity_id] - assert state.state == expected_state["state"] - for attribute, expected in expected_state.get("attributes", {}).items(): - assert state.attributes.get(attribute) == expected + assert state == snapshot @pytest.mark.parametrize( From 3022d3bfa04400577fa39de44f38d1864f707ec5 Mon Sep 17 00:00:00 2001 From: Josef Zweck <24647999+zweckj@users.noreply.github.com> Date: Thu, 27 Jun 2024 10:34:01 +0200 Subject: [PATCH 1330/1445] Move Auto On/off switches to Config EntityCategory (#120648) --- homeassistant/components/lamarzocco/switch.py | 2 ++ tests/components/lamarzocco/snapshots/test_switch.ambr | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/lamarzocco/switch.py b/homeassistant/components/lamarzocco/switch.py index e21cd2f3d94..c57e0662ab2 100644 --- a/homeassistant/components/lamarzocco/switch.py +++ b/homeassistant/components/lamarzocco/switch.py @@ -9,6 +9,7 @@ from lmcloud.lm_machine import LaMarzoccoMachine from lmcloud.models import LaMarzoccoMachineConfig from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription +from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -105,6 +106,7 @@ class LaMarzoccoAutoOnOffSwitchEntity(LaMarzoccoBaseEntity, SwitchEntity): super().__init__(coordinator, f"auto_on_off_{identifier}") self._identifier = identifier self._attr_translation_placeholders = {"id": identifier} + self.entity_category = EntityCategory.CONFIG async def _async_enable(self, state: bool) -> None: """Enable or disable the auto on/off schedule.""" diff --git a/tests/components/lamarzocco/snapshots/test_switch.ambr b/tests/components/lamarzocco/snapshots/test_switch.ambr index 0f462955a33..edda4ffee3b 100644 --- a/tests/components/lamarzocco/snapshots/test_switch.ambr +++ b/tests/components/lamarzocco/snapshots/test_switch.ambr @@ -10,7 +10,7 @@ 'device_id': , 'disabled_by': None, 'domain': 'switch', - 'entity_category': None, + 'entity_category': , 'entity_id': 'switch.gs01234_auto_on_off_os2oswx', 'has_entity_name': True, 'hidden_by': None, @@ -43,7 +43,7 @@ 'device_id': , 'disabled_by': None, 'domain': 'switch', - 'entity_category': None, + 'entity_category': , 'entity_id': 'switch.gs01234_auto_on_off_axfz5bj', 'has_entity_name': True, 'hidden_by': None, From 68495977643ebf39ff891c8cc727fd18f6c4eb36 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 27 Jun 2024 12:55:49 +0200 Subject: [PATCH 1331/1445] Bump hatasmota to 0.9.1 (#120649) --- .../components/tasmota/manifest.json | 2 +- homeassistant/components/tasmota/sensor.py | 4 +++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../tasmota/snapshots/test_sensor.ambr | 34 +++++++++++++++---- 5 files changed, 35 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/tasmota/manifest.json b/homeassistant/components/tasmota/manifest.json index 2ce81772774..69233de07d8 100644 --- a/homeassistant/components/tasmota/manifest.json +++ b/homeassistant/components/tasmota/manifest.json @@ -8,5 +8,5 @@ "iot_class": "local_push", "loggers": ["hatasmota"], "mqtt": ["tasmota/discovery/#"], - "requirements": ["HATasmota==0.8.0"] + "requirements": ["HATasmota==0.9.1"] } diff --git a/homeassistant/components/tasmota/sensor.py b/homeassistant/components/tasmota/sensor.py index 546e3eb4539..a7fb415f037 100644 --- a/homeassistant/components/tasmota/sensor.py +++ b/homeassistant/components/tasmota/sensor.py @@ -190,6 +190,10 @@ SENSOR_DEVICE_CLASS_ICON_MAP: dict[str, dict[str, Any]] = { DEVICE_CLASS: SensorDeviceClass.ENERGY, STATE_CLASS: SensorStateClass.TOTAL, }, + hc.SENSOR_TOTAL_TARIFF: { + DEVICE_CLASS: SensorDeviceClass.ENERGY, + STATE_CLASS: SensorStateClass.TOTAL, + }, hc.SENSOR_TOTAL_START_TIME: {ICON: "mdi:progress-clock"}, hc.SENSOR_TVOC: {ICON: "mdi:air-filter"}, hc.SENSOR_VOLTAGE: { diff --git a/requirements_all.txt b/requirements_all.txt index 44bc9f73b1d..06e5b6ef223 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -22,7 +22,7 @@ DoorBirdPy==2.1.0 HAP-python==4.9.1 # homeassistant.components.tasmota -HATasmota==0.8.0 +HATasmota==0.9.1 # homeassistant.components.mastodon Mastodon.py==1.8.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 45cb1087cb4..58691727bec 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -19,7 +19,7 @@ DoorBirdPy==2.1.0 HAP-python==4.9.1 # homeassistant.components.tasmota -HATasmota==0.8.0 +HATasmota==0.9.1 # homeassistant.components.doods # homeassistant.components.generic diff --git a/tests/components/tasmota/snapshots/test_sensor.ambr b/tests/components/tasmota/snapshots/test_sensor.ambr index 744554c7246..c5d70487749 100644 --- a/tests/components/tasmota/snapshots/test_sensor.ambr +++ b/tests/components/tasmota/snapshots/test_sensor.ambr @@ -232,7 +232,10 @@ # name: test_controlling_state_via_mqtt[sensor_config2-entity_ids2-messages2] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'energy', 'friendly_name': 'Tasmota ENERGY TotalTariff 0', + 'state_class': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.tasmota_energy_totaltariff_0', @@ -247,7 +250,9 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'device_class': None, 'device_id': , @@ -264,7 +269,7 @@ 'name': None, 'options': dict({ }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, 'original_name': 'ENERGY TotalTariff 0', 'platform': 'tasmota', @@ -272,13 +277,16 @@ 'supported_features': 0, 'translation_key': None, 'unique_id': '00000049A3BC_sensor_sensor_ENERGY_TotalTariff_0', - 'unit_of_measurement': None, + 'unit_of_measurement': , }) # --- # name: test_controlling_state_via_mqtt[sensor_config2-entity_ids2-messages2].2 StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'energy', 'friendly_name': 'Tasmota ENERGY TotalTariff 1', + 'state_class': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.tasmota_energy_totaltariff_1', @@ -293,7 +301,9 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'device_class': None, 'device_id': , @@ -310,7 +320,7 @@ 'name': None, 'options': dict({ }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, 'original_name': 'ENERGY TotalTariff 1', 'platform': 'tasmota', @@ -318,13 +328,16 @@ 'supported_features': 0, 'translation_key': None, 'unique_id': '00000049A3BC_sensor_sensor_ENERGY_TotalTariff_1', - 'unit_of_measurement': None, + 'unit_of_measurement': , }) # --- # name: test_controlling_state_via_mqtt[sensor_config2-entity_ids2-messages2].4 StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'energy', 'friendly_name': 'Tasmota ENERGY TotalTariff 0', + 'state_class': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.tasmota_energy_totaltariff_0', @@ -337,7 +350,10 @@ # name: test_controlling_state_via_mqtt[sensor_config2-entity_ids2-messages2].5 StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'energy', 'friendly_name': 'Tasmota ENERGY TotalTariff 1', + 'state_class': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.tasmota_energy_totaltariff_1', @@ -350,7 +366,10 @@ # name: test_controlling_state_via_mqtt[sensor_config2-entity_ids2-messages2].6 StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'energy', 'friendly_name': 'Tasmota ENERGY TotalTariff 0', + 'state_class': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.tasmota_energy_totaltariff_0', @@ -363,7 +382,10 @@ # name: test_controlling_state_via_mqtt[sensor_config2-entity_ids2-messages2].7 StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'energy', 'friendly_name': 'Tasmota ENERGY TotalTariff 1', + 'state_class': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.tasmota_energy_totaltariff_1', From 0e1dc9878f042f9f6ad79377f11e6ba530b49bc8 Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Thu, 27 Jun 2024 21:17:15 +1000 Subject: [PATCH 1332/1445] Fix values at startup for Tessie (#120652) --- homeassistant/components/tessie/entity.py | 1 + .../tessie/snapshots/test_lock.ambr | 48 ------------------- .../tessie/snapshots/test_sensor.ambr | 20 ++++---- 3 files changed, 11 insertions(+), 58 deletions(-) diff --git a/homeassistant/components/tessie/entity.py b/homeassistant/components/tessie/entity.py index 93b9f10ae67..d2a59f205fc 100644 --- a/homeassistant/components/tessie/entity.py +++ b/homeassistant/components/tessie/entity.py @@ -132,6 +132,7 @@ class TessieEnergyEntity(TessieBaseEntity): self._attr_device_info = data.device super().__init__(coordinator, key) + self._async_update_attrs() class TessieWallConnectorEntity(TessieBaseEntity): diff --git a/tests/components/tessie/snapshots/test_lock.ambr b/tests/components/tessie/snapshots/test_lock.ambr index 1eff418b202..cea2bebbddb 100644 --- a/tests/components/tessie/snapshots/test_lock.ambr +++ b/tests/components/tessie/snapshots/test_lock.ambr @@ -93,51 +93,3 @@ 'state': 'locked', }) # --- -# name: test_locks[lock.test_speed_limit-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'lock', - 'entity_category': None, - 'entity_id': 'lock.test_speed_limit', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Speed limit', - 'platform': 'tessie', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'vehicle_state_speed_limit_mode_active', - 'unique_id': 'VINVINVIN-vehicle_state_speed_limit_mode_active', - 'unit_of_measurement': None, - }) -# --- -# name: test_locks[lock.test_speed_limit-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'code_format': '^\\d\\d\\d\\d$', - 'friendly_name': 'Test Speed limit', - 'supported_features': , - }), - 'context': , - 'entity_id': 'lock.test_speed_limit', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unlocked', - }) -# --- diff --git a/tests/components/tessie/snapshots/test_sensor.ambr b/tests/components/tessie/snapshots/test_sensor.ambr index ba7b4eae0a5..afe229feba0 100644 --- a/tests/components/tessie/snapshots/test_sensor.ambr +++ b/tests/components/tessie/snapshots/test_sensor.ambr @@ -53,7 +53,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '5.06', }) # --- # name: test_sensors[sensor.energy_site_energy_left-entry] @@ -110,7 +110,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '38.8964736842105', }) # --- # name: test_sensors[sensor.energy_site_generator_power-entry] @@ -167,7 +167,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '0.0', }) # --- # name: test_sensors[sensor.energy_site_grid_power-entry] @@ -224,7 +224,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '0.0', }) # --- # name: test_sensors[sensor.energy_site_grid_services_power-entry] @@ -281,7 +281,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '0.0', }) # --- # name: test_sensors[sensor.energy_site_load_power-entry] @@ -338,7 +338,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '6.245', }) # --- # name: test_sensors[sensor.energy_site_percentage_charged-entry] @@ -392,7 +392,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '95.5053740373966', }) # --- # name: test_sensors[sensor.energy_site_solar_power-entry] @@ -449,7 +449,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '1.185', }) # --- # name: test_sensors[sensor.energy_site_total_pack_energy-entry] @@ -506,7 +506,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '40.727', }) # --- # name: test_sensors[sensor.energy_site_vpp_backup_reserve-entry] @@ -554,7 +554,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '0', }) # --- # name: test_sensors[sensor.test_battery_level-entry] From a8d6866f9f69de331d0eeb4f325ec140b282b218 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 27 Jun 2024 12:29:32 +0200 Subject: [PATCH 1333/1445] Disable polling for Knocki (#120656) --- homeassistant/components/knocki/event.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/knocki/event.py b/homeassistant/components/knocki/event.py index adaf344e468..74dc5a0f64c 100644 --- a/homeassistant/components/knocki/event.py +++ b/homeassistant/components/knocki/event.py @@ -48,6 +48,7 @@ class KnockiTrigger(EventEntity): _attr_event_types = [EVENT_TRIGGERED] _attr_has_entity_name = True + _attr_should_poll = False _attr_translation_key = "knocki" def __init__(self, trigger: Trigger, client: KnockiClient) -> None: From 03d198dd645487338e632b2630e4b14d945803bd Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 27 Jun 2024 19:41:21 +0200 Subject: [PATCH 1334/1445] Fix unknown attribute in MPD (#120657) --- homeassistant/components/mpd/media_player.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/homeassistant/components/mpd/media_player.py b/homeassistant/components/mpd/media_player.py index eb34fb6289f..3538b1c7973 100644 --- a/homeassistant/components/mpd/media_player.py +++ b/homeassistant/components/mpd/media_player.py @@ -421,11 +421,6 @@ class MpdDevice(MediaPlayerEntity): """Name of the current input source.""" return self._current_playlist - @property - def source_list(self): - """Return the list of available input sources.""" - return self._playlists - async def async_select_source(self, source: str) -> None: """Choose a different available playlist and play it.""" await self.async_play_media(MediaType.PLAYLIST, source) From be086c581c43ebb43dff97b03780f1c3d3d697c6 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 27 Jun 2024 12:47:58 +0200 Subject: [PATCH 1335/1445] Fix Airgradient ABC days name (#120659) --- .../components/airgradient/select.py | 1 + .../components/airgradient/strings.json | 3 +- .../airgradient/snapshots/test_select.ambr | 28 +++++++++++-------- 3 files changed, 19 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/airgradient/select.py b/homeassistant/components/airgradient/select.py index a64ce596806..532f7167dff 100644 --- a/homeassistant/components/airgradient/select.py +++ b/homeassistant/components/airgradient/select.py @@ -88,6 +88,7 @@ LEARNING_TIME_OFFSET_OPTIONS = [ ] ABC_DAYS = [ + "1", "8", "30", "90", diff --git a/homeassistant/components/airgradient/strings.json b/homeassistant/components/airgradient/strings.json index 1dd5fc61a16..12049e7b720 100644 --- a/homeassistant/components/airgradient/strings.json +++ b/homeassistant/components/airgradient/strings.json @@ -91,8 +91,9 @@ } }, "co2_automatic_baseline_calibration": { - "name": "CO2 automatic baseline calibration", + "name": "CO2 automatic baseline duration", "state": { + "1": "1 day", "8": "8 days", "30": "30 days", "90": "90 days", diff --git a/tests/components/airgradient/snapshots/test_select.ambr b/tests/components/airgradient/snapshots/test_select.ambr index ece563b40c6..b8fca4a110b 100644 --- a/tests/components/airgradient/snapshots/test_select.ambr +++ b/tests/components/airgradient/snapshots/test_select.ambr @@ -1,11 +1,12 @@ # serializer version: 1 -# name: test_all_entities[indoor][select.airgradient_co2_automatic_baseline_calibration-entry] +# name: test_all_entities[indoor][select.airgradient_co2_automatic_baseline_duration-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), 'area_id': None, 'capabilities': dict({ 'options': list([ + '1', '8', '30', '90', @@ -19,7 +20,7 @@ 'disabled_by': None, 'domain': 'select', 'entity_category': , - 'entity_id': 'select.airgradient_co2_automatic_baseline_calibration', + 'entity_id': 'select.airgradient_co2_automatic_baseline_duration', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -31,7 +32,7 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'CO2 automatic baseline calibration', + 'original_name': 'CO2 automatic baseline duration', 'platform': 'airgradient', 'previous_unique_id': None, 'supported_features': 0, @@ -40,11 +41,12 @@ 'unit_of_measurement': None, }) # --- -# name: test_all_entities[indoor][select.airgradient_co2_automatic_baseline_calibration-state] +# name: test_all_entities[indoor][select.airgradient_co2_automatic_baseline_duration-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Airgradient CO2 automatic baseline calibration', + 'friendly_name': 'Airgradient CO2 automatic baseline duration', 'options': list([ + '1', '8', '30', '90', @@ -53,7 +55,7 @@ ]), }), 'context': , - 'entity_id': 'select.airgradient_co2_automatic_baseline_calibration', + 'entity_id': 'select.airgradient_co2_automatic_baseline_duration', 'last_changed': , 'last_reported': , 'last_updated': , @@ -404,13 +406,14 @@ 'state': '12', }) # --- -# name: test_all_entities[outdoor][select.airgradient_co2_automatic_baseline_calibration-entry] +# name: test_all_entities[outdoor][select.airgradient_co2_automatic_baseline_duration-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), 'area_id': None, 'capabilities': dict({ 'options': list([ + '1', '8', '30', '90', @@ -424,7 +427,7 @@ 'disabled_by': None, 'domain': 'select', 'entity_category': , - 'entity_id': 'select.airgradient_co2_automatic_baseline_calibration', + 'entity_id': 'select.airgradient_co2_automatic_baseline_duration', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -436,7 +439,7 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'CO2 automatic baseline calibration', + 'original_name': 'CO2 automatic baseline duration', 'platform': 'airgradient', 'previous_unique_id': None, 'supported_features': 0, @@ -445,11 +448,12 @@ 'unit_of_measurement': None, }) # --- -# name: test_all_entities[outdoor][select.airgradient_co2_automatic_baseline_calibration-state] +# name: test_all_entities[outdoor][select.airgradient_co2_automatic_baseline_duration-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Airgradient CO2 automatic baseline calibration', + 'friendly_name': 'Airgradient CO2 automatic baseline duration', 'options': list([ + '1', '8', '30', '90', @@ -458,7 +462,7 @@ ]), }), 'context': , - 'entity_id': 'select.airgradient_co2_automatic_baseline_calibration', + 'entity_id': 'select.airgradient_co2_automatic_baseline_duration', 'last_changed': , 'last_reported': , 'last_updated': , From f9ca85735d089bbbe61abb293e23f43282f73d69 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 27 Jun 2024 23:08:40 +1200 Subject: [PATCH 1336/1445] [esphome] Add more tests to bring integration to 100% coverage (#120661) --- tests/components/esphome/conftest.py | 147 +++++++++++++++++- tests/components/esphome/test_manager.py | 108 ++++++++++++- .../esphome/test_voice_assistant.py | 14 +- 3 files changed, 258 insertions(+), 11 deletions(-) diff --git a/tests/components/esphome/conftest.py b/tests/components/esphome/conftest.py index f55ab9cbe4a..ac1558b8aa0 100644 --- a/tests/components/esphome/conftest.py +++ b/tests/components/esphome/conftest.py @@ -4,7 +4,7 @@ from __future__ import annotations import asyncio from asyncio import Event -from collections.abc import Awaitable, Callable +from collections.abc import Awaitable, Callable, Coroutine from pathlib import Path from typing import Any from unittest.mock import AsyncMock, MagicMock, Mock, patch @@ -19,6 +19,8 @@ from aioesphomeapi import ( HomeassistantServiceCall, ReconnectLogic, UserService, + VoiceAssistantAudioSettings, + VoiceAssistantEventType, VoiceAssistantFeature, ) import pytest @@ -32,6 +34,11 @@ from homeassistant.components.esphome.const import ( DEFAULT_NEW_CONFIG_ALLOW_ALLOW_SERVICE_CALLS, DOMAIN, ) +from homeassistant.components.esphome.entry_data import RuntimeEntryData +from homeassistant.components.esphome.voice_assistant import ( + VoiceAssistantAPIPipeline, + VoiceAssistantUDPPipeline, +) from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component @@ -40,6 +47,8 @@ from . import DASHBOARD_HOST, DASHBOARD_PORT, DASHBOARD_SLUG from tests.common import MockConfigEntry +_ONE_SECOND = 16000 * 2 # 16Khz 16-bit + @pytest.fixture(autouse=True) def mock_bluetooth(enable_bluetooth: None) -> None: @@ -196,6 +205,20 @@ class MockESPHomeDevice: self.home_assistant_state_subscription_callback: Callable[ [str, str | None], None ] + self.voice_assistant_handle_start_callback: Callable[ + [str, int, VoiceAssistantAudioSettings, str | None], + Coroutine[Any, Any, int | None], + ] + self.voice_assistant_handle_stop_callback: Callable[ + [], Coroutine[Any, Any, None] + ] + self.voice_assistant_handle_audio_callback: ( + Callable[ + [bytes], + Coroutine[Any, Any, None], + ] + | None + ) self.device_info = device_info def set_state_callback(self, state_callback: Callable[[EntityState], None]) -> None: @@ -255,6 +278,47 @@ class MockESPHomeDevice: """Mock a state subscription.""" self.home_assistant_state_subscription_callback(entity_id, attribute) + def set_subscribe_voice_assistant_callbacks( + self, + handle_start: Callable[ + [str, int, VoiceAssistantAudioSettings, str | None], + Coroutine[Any, Any, int | None], + ], + handle_stop: Callable[[], Coroutine[Any, Any, None]], + handle_audio: ( + Callable[ + [bytes], + Coroutine[Any, Any, None], + ] + | None + ) = None, + ) -> None: + """Set the voice assistant subscription callbacks.""" + self.voice_assistant_handle_start_callback = handle_start + self.voice_assistant_handle_stop_callback = handle_stop + self.voice_assistant_handle_audio_callback = handle_audio + + async def mock_voice_assistant_handle_start( + self, + conversation_id: str, + flags: int, + settings: VoiceAssistantAudioSettings, + wake_word_phrase: str | None, + ) -> int | None: + """Mock voice assistant handle start.""" + return await self.voice_assistant_handle_start_callback( + conversation_id, flags, settings, wake_word_phrase + ) + + async def mock_voice_assistant_handle_stop(self) -> None: + """Mock voice assistant handle stop.""" + await self.voice_assistant_handle_stop_callback() + + async def mock_voice_assistant_handle_audio(self, audio: bytes) -> None: + """Mock voice assistant handle audio.""" + assert self.voice_assistant_handle_audio_callback is not None + await self.voice_assistant_handle_audio_callback(audio) + async def _mock_generic_device_entry( hass: HomeAssistant, @@ -318,8 +382,33 @@ async def _mock_generic_device_entry( """Subscribe to home assistant states.""" mock_device.set_home_assistant_state_subscription_callback(on_state_sub) + def _subscribe_voice_assistant( + *, + handle_start: Callable[ + [str, int, VoiceAssistantAudioSettings, str | None], + Coroutine[Any, Any, int | None], + ], + handle_stop: Callable[[], Coroutine[Any, Any, None]], + handle_audio: ( + Callable[ + [bytes], + Coroutine[Any, Any, None], + ] + | None + ) = None, + ) -> Callable[[], None]: + """Subscribe to voice assistant.""" + mock_device.set_subscribe_voice_assistant_callbacks( + handle_start, handle_stop, handle_audio + ) + + def unsub(): + pass + + return unsub + mock_client.device_info = AsyncMock(return_value=mock_device.device_info) - mock_client.subscribe_voice_assistant = Mock() + mock_client.subscribe_voice_assistant = _subscribe_voice_assistant mock_client.list_entities_services = AsyncMock( return_value=mock_list_entities_services ) @@ -524,3 +613,57 @@ async def mock_esphome_device( ) return _mock_device + + +@pytest.fixture +def mock_voice_assistant_api_pipeline() -> VoiceAssistantAPIPipeline: + """Return the API Pipeline factory.""" + mock_pipeline = Mock(spec=VoiceAssistantAPIPipeline) + + def mock_constructor( + hass: HomeAssistant, + entry_data: RuntimeEntryData, + handle_event: Callable[[VoiceAssistantEventType, dict[str, str] | None], None], + handle_finished: Callable[[], None], + api_client: APIClient, + ): + """Fake the constructor.""" + mock_pipeline.hass = hass + mock_pipeline.entry_data = entry_data + mock_pipeline.handle_event = handle_event + mock_pipeline.handle_finished = handle_finished + mock_pipeline.api_client = api_client + return mock_pipeline + + mock_pipeline.side_effect = mock_constructor + with patch( + "homeassistant.components.esphome.voice_assistant.VoiceAssistantAPIPipeline", + new=mock_pipeline, + ): + yield mock_pipeline + + +@pytest.fixture +def mock_voice_assistant_udp_pipeline() -> VoiceAssistantUDPPipeline: + """Return the API Pipeline factory.""" + mock_pipeline = Mock(spec=VoiceAssistantUDPPipeline) + + def mock_constructor( + hass: HomeAssistant, + entry_data: RuntimeEntryData, + handle_event: Callable[[VoiceAssistantEventType, dict[str, str] | None], None], + handle_finished: Callable[[], None], + ): + """Fake the constructor.""" + mock_pipeline.hass = hass + mock_pipeline.entry_data = entry_data + mock_pipeline.handle_event = handle_event + mock_pipeline.handle_finished = handle_finished + return mock_pipeline + + mock_pipeline.side_effect = mock_constructor + with patch( + "homeassistant.components.esphome.voice_assistant.VoiceAssistantUDPPipeline", + new=mock_pipeline, + ): + yield mock_pipeline diff --git a/tests/components/esphome/test_manager.py b/tests/components/esphome/test_manager.py index 92c21842e78..01f267581f4 100644 --- a/tests/components/esphome/test_manager.py +++ b/tests/components/esphome/test_manager.py @@ -2,7 +2,7 @@ import asyncio from collections.abc import Awaitable, Callable -from unittest.mock import AsyncMock, call +from unittest.mock import AsyncMock, call, patch from aioesphomeapi import ( APIClient, @@ -17,6 +17,7 @@ from aioesphomeapi import ( UserService, UserServiceArg, UserServiceArgType, + VoiceAssistantFeature, ) import pytest @@ -28,6 +29,10 @@ from homeassistant.components.esphome.const import ( DOMAIN, STABLE_BLE_VERSION_STR, ) +from homeassistant.components.esphome.voice_assistant import ( + VoiceAssistantAPIPipeline, + VoiceAssistantUDPPipeline, +) from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, @@ -39,7 +44,7 @@ from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import device_registry as dr, issue_registry as ir from homeassistant.setup import async_setup_component -from .conftest import MockESPHomeDevice +from .conftest import _ONE_SECOND, MockESPHomeDevice from tests.common import MockConfigEntry, async_capture_events, async_mock_service @@ -1181,3 +1186,102 @@ async def test_entry_missing_unique_id( await mock_esphome_device(mock_client=mock_client, mock_storage=True) await hass.async_block_till_done() assert entry.unique_id == "11:22:33:44:55:aa" + + +async def test_manager_voice_assistant_handlers_api( + hass: HomeAssistant, + mock_client: APIClient, + mock_esphome_device: Callable[ + [APIClient, list[EntityInfo], list[UserService], list[EntityState]], + Awaitable[MockESPHomeDevice], + ], + caplog: pytest.LogCaptureFixture, + mock_voice_assistant_api_pipeline: VoiceAssistantAPIPipeline, +) -> None: + """Test the handlers are correctly executed in manager.py.""" + + device: MockESPHomeDevice = await mock_esphome_device( + mock_client=mock_client, + entity_info=[], + user_service=[], + states=[], + device_info={ + "voice_assistant_feature_flags": VoiceAssistantFeature.VOICE_ASSISTANT + | VoiceAssistantFeature.API_AUDIO + }, + ) + + await hass.async_block_till_done() + + with ( + patch( + "homeassistant.components.esphome.manager.VoiceAssistantAPIPipeline", + new=mock_voice_assistant_api_pipeline, + ), + ): + port: int | None = await device.mock_voice_assistant_handle_start( + "", 0, None, None + ) + + assert port == 0 + + port: int | None = await device.mock_voice_assistant_handle_start( + "", 0, None, None + ) + + assert "Voice assistant UDP server was not stopped" in caplog.text + + await device.mock_voice_assistant_handle_audio(bytes(_ONE_SECOND)) + + mock_voice_assistant_api_pipeline.receive_audio_bytes.assert_called_with( + bytes(_ONE_SECOND) + ) + + mock_voice_assistant_api_pipeline.receive_audio_bytes.reset_mock() + + await device.mock_voice_assistant_handle_stop() + mock_voice_assistant_api_pipeline.handle_finished() + + await device.mock_voice_assistant_handle_audio(bytes(_ONE_SECOND)) + + mock_voice_assistant_api_pipeline.receive_audio_bytes.assert_not_called() + + +async def test_manager_voice_assistant_handlers_udp( + hass: HomeAssistant, + mock_client: APIClient, + mock_esphome_device: Callable[ + [APIClient, list[EntityInfo], list[UserService], list[EntityState]], + Awaitable[MockESPHomeDevice], + ], + mock_voice_assistant_udp_pipeline: VoiceAssistantUDPPipeline, +) -> None: + """Test the handlers are correctly executed in manager.py.""" + + device: MockESPHomeDevice = await mock_esphome_device( + mock_client=mock_client, + entity_info=[], + user_service=[], + states=[], + device_info={ + "voice_assistant_feature_flags": VoiceAssistantFeature.VOICE_ASSISTANT + }, + ) + + await hass.async_block_till_done() + + with ( + patch( + "homeassistant.components.esphome.manager.VoiceAssistantUDPPipeline", + new=mock_voice_assistant_udp_pipeline, + ), + ): + await device.mock_voice_assistant_handle_start("", 0, None, None) + + mock_voice_assistant_udp_pipeline.run_pipeline.assert_called() + + await device.mock_voice_assistant_handle_stop() + mock_voice_assistant_udp_pipeline.handle_finished() + + mock_voice_assistant_udp_pipeline.stop.assert_called() + mock_voice_assistant_udp_pipeline.close.assert_called() diff --git a/tests/components/esphome/test_voice_assistant.py b/tests/components/esphome/test_voice_assistant.py index c347c3dc7d3..eafc0243dc6 100644 --- a/tests/components/esphome/test_voice_assistant.py +++ b/tests/components/esphome/test_voice_assistant.py @@ -37,15 +37,13 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import intent as intent_helper import homeassistant.helpers.device_registry as dr -from .conftest import MockESPHomeDevice +from .conftest import _ONE_SECOND, MockESPHomeDevice _TEST_INPUT_TEXT = "This is an input test" _TEST_OUTPUT_TEXT = "This is an output test" _TEST_OUTPUT_URL = "output.mp3" _TEST_MEDIA_ID = "12345" -_ONE_SECOND = 16000 * 2 # 16Khz 16-bit - @pytest.fixture def voice_assistant_udp_pipeline( @@ -813,6 +811,7 @@ async def test_wake_word_abort_exception( async def test_timer_events( hass: HomeAssistant, + device_registry: dr.DeviceRegistry, mock_client: APIClient, mock_esphome_device: Callable[ [APIClient, list[EntityInfo], list[UserService], list[EntityState]], @@ -831,8 +830,8 @@ async def test_timer_events( | VoiceAssistantFeature.TIMERS }, ) - dev_reg = dr.async_get(hass) - dev = dev_reg.async_get_device( + await hass.async_block_till_done() + dev = device_registry.async_get_device( connections={(dr.CONNECTION_NETWORK_MAC, mock_device.entry.unique_id)} ) @@ -886,6 +885,7 @@ async def test_timer_events( async def test_unknown_timer_event( hass: HomeAssistant, + device_registry: dr.DeviceRegistry, mock_client: APIClient, mock_esphome_device: Callable[ [APIClient, list[EntityInfo], list[UserService], list[EntityState]], @@ -904,8 +904,8 @@ async def test_unknown_timer_event( | VoiceAssistantFeature.TIMERS }, ) - dev_reg = dr.async_get(hass) - dev = dev_reg.async_get_device( + await hass.async_block_till_done() + dev = device_registry.async_get_device( connections={(dr.CONNECTION_NETWORK_MAC, mock_device.entry.unique_id)} ) From f6aa25c717125e2af4b5f6c7d295b43261049f58 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 27 Jun 2024 15:00:14 +0200 Subject: [PATCH 1337/1445] Fix docstring for EventStateEventData (#120662) --- homeassistant/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/core.py b/homeassistant/core.py index 71ee5f4bd1d..c4392f62c52 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -159,7 +159,7 @@ class ConfigSource(enum.StrEnum): class EventStateEventData(TypedDict): - """Base class for EVENT_STATE_CHANGED and EVENT_STATE_CHANGED data.""" + """Base class for EVENT_STATE_CHANGED and EVENT_STATE_REPORTED data.""" entity_id: str new_state: State | None From 94f8f8281f66f150c894af80044115074fac6a44 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 27 Jun 2024 12:58:42 -0500 Subject: [PATCH 1338/1445] Bump uiprotect to 4.2.0 (#120669) --- homeassistant/components/unifiprotect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index bdbdacae90e..6f61bb97480 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -40,7 +40,7 @@ "integration_type": "hub", "iot_class": "local_push", "loggers": ["uiprotect", "unifi_discovery"], - "requirements": ["uiprotect==4.0.0", "unifi-discovery==1.1.8"], + "requirements": ["uiprotect==4.2.0", "unifi-discovery==1.1.8"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/requirements_all.txt b/requirements_all.txt index 06e5b6ef223..a4f34b61e3f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2789,7 +2789,7 @@ twitchAPI==4.0.0 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==4.0.0 +uiprotect==4.2.0 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 58691727bec..8c9cb2c8a23 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2169,7 +2169,7 @@ twitchAPI==4.0.0 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==4.0.0 +uiprotect==4.2.0 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 From f9c5661c669f357eee0073ac155f8054cc8578bc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 27 Jun 2024 15:10:48 -0500 Subject: [PATCH 1339/1445] Bump unifi-discovery to 1.2.0 (#120684) --- homeassistant/components/unifiprotect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index 6f61bb97480..06716e5342a 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -40,7 +40,7 @@ "integration_type": "hub", "iot_class": "local_push", "loggers": ["uiprotect", "unifi_discovery"], - "requirements": ["uiprotect==4.2.0", "unifi-discovery==1.1.8"], + "requirements": ["uiprotect==4.2.0", "unifi-discovery==1.2.0"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/requirements_all.txt b/requirements_all.txt index a4f34b61e3f..397a967788a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2795,7 +2795,7 @@ uiprotect==4.2.0 ultraheat-api==0.5.7 # homeassistant.components.unifiprotect -unifi-discovery==1.1.8 +unifi-discovery==1.2.0 # homeassistant.components.unifi_direct unifi_ap==0.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8c9cb2c8a23..87ec9e11e0f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2175,7 +2175,7 @@ uiprotect==4.2.0 ultraheat-api==0.5.7 # homeassistant.components.unifiprotect -unifi-discovery==1.1.8 +unifi-discovery==1.2.0 # homeassistant.components.zha universal-silabs-flasher==0.0.20 From 07dd832c58d86efa90398b1e17f9f249763131b9 Mon Sep 17 00:00:00 2001 From: Glenn Waters Date: Thu, 27 Jun 2024 13:08:18 -0400 Subject: [PATCH 1340/1445] Bump Environment Canada to 0.7.0 (#120686) --- .../components/environment_canada/manifest.json | 2 +- homeassistant/components/environment_canada/sensor.py | 9 --------- homeassistant/components/environment_canada/strings.json | 3 --- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 3 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/environment_canada/manifest.json b/homeassistant/components/environment_canada/manifest.json index a0bdd5d4919..69a6cd7c69b 100644 --- a/homeassistant/components/environment_canada/manifest.json +++ b/homeassistant/components/environment_canada/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/environment_canada", "iot_class": "cloud_polling", "loggers": ["env_canada"], - "requirements": ["env-canada==0.6.3"] + "requirements": ["env-canada==0.7.0"] } diff --git a/homeassistant/components/environment_canada/sensor.py b/homeassistant/components/environment_canada/sensor.py index 8a734f74dd6..1a5d096203d 100644 --- a/homeassistant/components/environment_canada/sensor.py +++ b/homeassistant/components/environment_canada/sensor.py @@ -19,7 +19,6 @@ from homeassistant.const import ( PERCENTAGE, UV_INDEX, UnitOfLength, - UnitOfPrecipitationDepth, UnitOfPressure, UnitOfSpeed, UnitOfTemperature, @@ -114,14 +113,6 @@ SENSOR_TYPES: tuple[ECSensorEntityDescription, ...] = ( native_unit_of_measurement=PERCENTAGE, value_fn=lambda data: data.conditions.get("pop", {}).get("value"), ), - ECSensorEntityDescription( - key="precip_yesterday", - translation_key="precip_yesterday", - device_class=SensorDeviceClass.PRECIPITATION, - native_unit_of_measurement=UnitOfPrecipitationDepth.MILLIMETERS, - state_class=SensorStateClass.MEASUREMENT, - value_fn=lambda data: data.conditions.get("precip_yesterday", {}).get("value"), - ), ECSensorEntityDescription( key="pressure", translation_key="pressure", diff --git a/homeassistant/components/environment_canada/strings.json b/homeassistant/components/environment_canada/strings.json index fc03550b64e..28ca55c6195 100644 --- a/homeassistant/components/environment_canada/strings.json +++ b/homeassistant/components/environment_canada/strings.json @@ -52,9 +52,6 @@ "pop": { "name": "Chance of precipitation" }, - "precip_yesterday": { - "name": "Precipitation yesterday" - }, "pressure": { "name": "Barometric pressure" }, diff --git a/requirements_all.txt b/requirements_all.txt index 397a967788a..2b99124d5b3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -816,7 +816,7 @@ enocean==0.50 enturclient==0.2.4 # homeassistant.components.environment_canada -env-canada==0.6.3 +env-canada==0.7.0 # homeassistant.components.season ephem==4.1.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 87ec9e11e0f..1a230af870f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -673,7 +673,7 @@ energyzero==2.1.0 enocean==0.50 # homeassistant.components.environment_canada -env-canada==0.6.3 +env-canada==0.7.0 # homeassistant.components.season ephem==4.1.5 From 09dbd8e7eb02bf8f4c56202a2c5a9ba05b64b0a2 Mon Sep 17 00:00:00 2001 From: MatthewFlamm <39341281+MatthewFlamm@users.noreply.github.com> Date: Thu, 27 Jun 2024 13:47:25 -0400 Subject: [PATCH 1341/1445] Use more observations in NWS (#120687) Use more observations --- homeassistant/components/nws/const.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/nws/const.py b/homeassistant/components/nws/const.py index ba3a22e5818..381537775da 100644 --- a/homeassistant/components/nws/const.py +++ b/homeassistant/components/nws/const.py @@ -78,8 +78,8 @@ HOURLY = "hourly" OBSERVATION_VALID_TIME = timedelta(minutes=60) FORECAST_VALID_TIME = timedelta(minutes=45) -# A lot of stations update once hourly plus some wiggle room -UPDATE_TIME_PERIOD = timedelta(minutes=70) +# Ask for observations for last four hours +UPDATE_TIME_PERIOD = timedelta(minutes=240) DEBOUNCE_TIME = 10 * 60 # in seconds DEFAULT_SCAN_INTERVAL = timedelta(minutes=10) From b9c9921847c6c27e618845c814c18215dfab6186 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 27 Jun 2024 12:20:37 -0500 Subject: [PATCH 1342/1445] Add newer models to unifi integrations discovery (#120688) --- homeassistant/components/unifi/manifest.json | 4 ++++ homeassistant/components/unifiprotect/manifest.json | 4 ++++ homeassistant/generated/ssdp.py | 8 ++++++++ 3 files changed, 16 insertions(+) diff --git a/homeassistant/components/unifi/manifest.json b/homeassistant/components/unifi/manifest.json index f4bfaec2d42..aa9b553cb67 100644 --- a/homeassistant/components/unifi/manifest.json +++ b/homeassistant/components/unifi/manifest.json @@ -21,6 +21,10 @@ { "manufacturer": "Ubiquiti Networks", "modelDescription": "UniFi Dream Machine SE" + }, + { + "manufacturer": "Ubiquiti Networks", + "modelDescription": "UniFi Dream Machine Pro Max" } ] } diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index 06716e5342a..6691d738cd0 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -53,6 +53,10 @@ { "manufacturer": "Ubiquiti Networks", "modelDescription": "UniFi Dream Machine SE" + }, + { + "manufacturer": "Ubiquiti Networks", + "modelDescription": "UniFi Dream Machine Pro Max" } ] } diff --git a/homeassistant/generated/ssdp.py b/homeassistant/generated/ssdp.py index 8e7319917f0..9ed65bab868 100644 --- a/homeassistant/generated/ssdp.py +++ b/homeassistant/generated/ssdp.py @@ -297,6 +297,10 @@ SSDP = { "manufacturer": "Ubiquiti Networks", "modelDescription": "UniFi Dream Machine SE", }, + { + "manufacturer": "Ubiquiti Networks", + "modelDescription": "UniFi Dream Machine Pro Max", + }, ], "unifiprotect": [ { @@ -311,6 +315,10 @@ SSDP = { "manufacturer": "Ubiquiti Networks", "modelDescription": "UniFi Dream Machine SE", }, + { + "manufacturer": "Ubiquiti Networks", + "modelDescription": "UniFi Dream Machine Pro Max", + }, ], "upnp": [ { From e756328d523042910a6a147655f267c44ae71620 Mon Sep 17 00:00:00 2001 From: Glenn Waters Date: Thu, 27 Jun 2024 13:41:36 -0400 Subject: [PATCH 1343/1445] Bump upb-lib to 0.5.7 (#120689) --- homeassistant/components/upb/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/upb/manifest.json b/homeassistant/components/upb/manifest.json index a5e32dd298e..b208edbc0e5 100644 --- a/homeassistant/components/upb/manifest.json +++ b/homeassistant/components/upb/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/upb", "iot_class": "local_push", "loggers": ["upb_lib"], - "requirements": ["upb-lib==0.5.6"] + "requirements": ["upb-lib==0.5.7"] } diff --git a/requirements_all.txt b/requirements_all.txt index 2b99124d5b3..9cbfe64afd0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2807,7 +2807,7 @@ unifiled==0.11 universal-silabs-flasher==0.0.20 # homeassistant.components.upb -upb-lib==0.5.6 +upb-lib==0.5.7 # homeassistant.components.upcloud upcloud-api==2.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1a230af870f..01b356747b2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2181,7 +2181,7 @@ unifi-discovery==1.2.0 universal-silabs-flasher==0.0.20 # homeassistant.components.upb -upb-lib==0.5.6 +upb-lib==0.5.7 # homeassistant.components.upcloud upcloud-api==2.5.1 From 476b9909ac879cf60b6d2d9de877fa8d5099cab6 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 27 Jun 2024 22:06:30 +0200 Subject: [PATCH 1344/1445] Update frontend to 20240627.0 (#120693) --- 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 89c8fbe30ca..cd46b358335 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==20240626.2"] + "requirements": ["home-assistant-frontend==20240627.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 174de784eba..91db2564fa6 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -32,7 +32,7 @@ habluetooth==3.1.3 hass-nabucasa==0.81.1 hassil==1.7.1 home-assistant-bluetooth==1.12.2 -home-assistant-frontend==20240626.2 +home-assistant-frontend==20240627.0 home-assistant-intents==2024.6.26 httpx==0.27.0 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 9cbfe64afd0..0086de879db 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1090,7 +1090,7 @@ hole==0.8.0 holidays==0.51 # homeassistant.components.frontend -home-assistant-frontend==20240626.2 +home-assistant-frontend==20240627.0 # homeassistant.components.conversation home-assistant-intents==2024.6.26 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 01b356747b2..0862fc33ea4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -895,7 +895,7 @@ hole==0.8.0 holidays==0.51 # homeassistant.components.frontend -home-assistant-frontend==20240626.2 +home-assistant-frontend==20240627.0 # homeassistant.components.conversation home-assistant-intents==2024.6.26 From f3ab3bd5cbe60d3fce5c8e46d40cb69b58ceef0c Mon Sep 17 00:00:00 2001 From: Thomas55555 <59625598+Thomas55555@users.noreply.github.com> Date: Thu, 27 Jun 2024 21:29:17 +0200 Subject: [PATCH 1345/1445] Bump aioautomower to 2024.6.3 (#120697) --- homeassistant/components/husqvarna_automower/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/husqvarna_automower/manifest.json b/homeassistant/components/husqvarna_automower/manifest.json index 5ca1b500340..7883b057a3f 100644 --- a/homeassistant/components/husqvarna_automower/manifest.json +++ b/homeassistant/components/husqvarna_automower/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/husqvarna_automower", "iot_class": "cloud_push", "loggers": ["aioautomower"], - "requirements": ["aioautomower==2024.6.1"] + "requirements": ["aioautomower==2024.6.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index 0086de879db..a8d7a62d848 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -204,7 +204,7 @@ aioaseko==0.1.1 aioasuswrt==1.4.0 # homeassistant.components.husqvarna_automower -aioautomower==2024.6.1 +aioautomower==2024.6.3 # homeassistant.components.azure_devops aioazuredevops==2.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0862fc33ea4..802231b63d1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -183,7 +183,7 @@ aioaseko==0.1.1 aioasuswrt==1.4.0 # homeassistant.components.husqvarna_automower -aioautomower==2024.6.1 +aioautomower==2024.6.3 # homeassistant.components.azure_devops aioazuredevops==2.1.1 From 411633d3b3786a9b210c9c1bf925c09e31b5a6b8 Mon Sep 17 00:00:00 2001 From: Glenn Waters Date: Thu, 27 Jun 2024 16:10:11 -0400 Subject: [PATCH 1346/1445] Bump Environment Canada to 0.7.1 (#120699) --- homeassistant/components/environment_canada/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/environment_canada/manifest.json b/homeassistant/components/environment_canada/manifest.json index 69a6cd7c69b..c77d35b1769 100644 --- a/homeassistant/components/environment_canada/manifest.json +++ b/homeassistant/components/environment_canada/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/environment_canada", "iot_class": "cloud_polling", "loggers": ["env_canada"], - "requirements": ["env-canada==0.7.0"] + "requirements": ["env-canada==0.7.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index a8d7a62d848..657e961d803 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -816,7 +816,7 @@ enocean==0.50 enturclient==0.2.4 # homeassistant.components.environment_canada -env-canada==0.7.0 +env-canada==0.7.1 # homeassistant.components.season ephem==4.1.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 802231b63d1..3eb005b1dac 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -673,7 +673,7 @@ energyzero==2.1.0 enocean==0.50 # homeassistant.components.environment_canada -env-canada==0.7.0 +env-canada==0.7.1 # homeassistant.components.season ephem==4.1.5 From 0b8dd738f1a07074029e04845a8e731f1724fbfa Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 27 Jun 2024 22:09:33 +0200 Subject: [PATCH 1347/1445] Bump ttls to 1.8.3 (#120700) --- homeassistant/components/twinkly/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/twinkly/manifest.json b/homeassistant/components/twinkly/manifest.json index 6ec89261b3d..a84eebf0f28 100644 --- a/homeassistant/components/twinkly/manifest.json +++ b/homeassistant/components/twinkly/manifest.json @@ -14,5 +14,5 @@ "documentation": "https://www.home-assistant.io/integrations/twinkly", "iot_class": "local_polling", "loggers": ["ttls"], - "requirements": ["ttls==1.5.1"] + "requirements": ["ttls==1.8.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index 657e961d803..98b5548cd38 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2768,7 +2768,7 @@ tplink-omada-client==1.3.12 transmission-rpc==7.0.3 # homeassistant.components.twinkly -ttls==1.5.1 +ttls==1.8.3 # homeassistant.components.thethingsnetwork ttn_client==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3eb005b1dac..2857aee8c9b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2148,7 +2148,7 @@ tplink-omada-client==1.3.12 transmission-rpc==7.0.3 # homeassistant.components.twinkly -ttls==1.5.1 +ttls==1.8.3 # homeassistant.components.thethingsnetwork ttn_client==1.0.0 From 23056f839b1dba2f2469176a5896a99d46179322 Mon Sep 17 00:00:00 2001 From: Steven B <51370195+sdb9696@users.noreply.github.com> Date: Thu, 27 Jun 2024 13:54:34 +0100 Subject: [PATCH 1348/1445] Update tplink unlink identifiers to deal with ids from other domains (#120596) --- homeassistant/components/tplink/__init__.py | 98 +++++++++++++-------- tests/components/tplink/__init__.py | 1 + tests/components/tplink/test_init.py | 82 ++++++++++++----- 3 files changed, 123 insertions(+), 58 deletions(-) diff --git a/homeassistant/components/tplink/__init__.py b/homeassistant/components/tplink/__init__.py index 6d300f68aa0..83cfc733716 100644 --- a/homeassistant/components/tplink/__init__.py +++ b/homeassistant/components/tplink/__init__.py @@ -3,6 +3,7 @@ from __future__ import annotations import asyncio +from collections.abc import Iterable from datetime import timedelta import logging from typing import Any @@ -282,6 +283,28 @@ def mac_alias(mac: str) -> str: return mac.replace(":", "")[-4:].upper() +def _mac_connection_or_none(device: dr.DeviceEntry) -> str | None: + return next( + ( + conn + for type_, conn in device.connections + if type_ == dr.CONNECTION_NETWORK_MAC + ), + None, + ) + + +def _device_id_is_mac_or_none(mac: str, device_ids: Iterable[str]) -> str | None: + # Previously only iot devices had child devices and iot devices use + # the upper and lcase MAC addresses as device_id so match on case + # insensitive mac address as the parent device. + upper_mac = mac.upper() + return next( + (device_id for device_id in device_ids if device_id.upper() == upper_mac), + None, + ) + + async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Migrate old entry.""" version = config_entry.version @@ -298,49 +321,48 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> # always be linked into one device. dev_reg = dr.async_get(hass) for device in dr.async_entries_for_config_entry(dev_reg, config_entry.entry_id): - new_identifiers: set[tuple[str, str]] | None = None - if len(device.identifiers) > 1 and ( - mac := next( - iter( - [ - conn[1] - for conn in device.connections - if conn[0] == dr.CONNECTION_NETWORK_MAC - ] - ), - None, + original_identifiers = device.identifiers + # Get only the tplink identifier, could be tapo or other integrations. + tplink_identifiers = [ + ident[1] for ident in original_identifiers if ident[0] == DOMAIN + ] + # Nothing to fix if there's only one identifier. mac connection + # should never be none but if it is there's no problem. + if len(tplink_identifiers) <= 1 or not ( + mac := _mac_connection_or_none(device) + ): + continue + if not ( + tplink_parent_device_id := _device_id_is_mac_or_none( + mac, tplink_identifiers ) ): - for identifier in device.identifiers: - # Previously only iot devices that use the MAC address as - # device_id had child devices so check for mac as the - # parent device. - if identifier[0] == DOMAIN and identifier[1].upper() == mac.upper(): - new_identifiers = {identifier} - break - if new_identifiers: - dev_reg.async_update_device( - device.id, new_identifiers=new_identifiers - ) - _LOGGER.debug( - "Replaced identifiers for device %s (%s): %s with: %s", - device.name, - device.model, - device.identifiers, - new_identifiers, - ) - else: - # No match on mac so raise an error. - _LOGGER.error( - "Unable to replace identifiers for device %s (%s): %s", - device.name, - device.model, - device.identifiers, - ) + # No match on mac so raise an error. + _LOGGER.error( + "Unable to replace identifiers for device %s (%s): %s", + device.name, + device.model, + device.identifiers, + ) + continue + # Retain any identifiers for other domains + new_identifiers = { + ident for ident in device.identifiers if ident[0] != DOMAIN + } + new_identifiers.add((DOMAIN, tplink_parent_device_id)) + dev_reg.async_update_device(device.id, new_identifiers=new_identifiers) + _LOGGER.debug( + "Replaced identifiers for device %s (%s): %s with: %s", + device.name, + device.model, + original_identifiers, + new_identifiers, + ) minor_version = 3 hass.config_entries.async_update_entry(config_entry, minor_version=3) - _LOGGER.debug("Migration to version %s.%s successful", version, minor_version) + + _LOGGER.debug("Migration to version %s.%s complete", version, minor_version) if version == 1 and minor_version == 3: # credentials_hash stored in the device_config should be moved to data. diff --git a/tests/components/tplink/__init__.py b/tests/components/tplink/__init__.py index b3092d62904..d12858017cc 100644 --- a/tests/components/tplink/__init__.py +++ b/tests/components/tplink/__init__.py @@ -49,6 +49,7 @@ ALIAS = "My Bulb" MODEL = "HS100" MAC_ADDRESS = "aa:bb:cc:dd:ee:ff" DEVICE_ID = "123456789ABCDEFGH" +DEVICE_ID_MAC = "AA:BB:CC:DD:EE:FF" DHCP_FORMATTED_MAC_ADDRESS = MAC_ADDRESS.replace(":", "") MAC_ADDRESS2 = "11:22:33:44:55:66" DEFAULT_ENTRY_TITLE = f"{ALIAS} {MODEL}" diff --git a/tests/components/tplink/test_init.py b/tests/components/tplink/test_init.py index bfb7e02b63d..c5c5e2ce6db 100644 --- a/tests/components/tplink/test_init.py +++ b/tests/components/tplink/test_init.py @@ -36,6 +36,8 @@ from . import ( CREATE_ENTRY_DATA_AUTH, CREATE_ENTRY_DATA_LEGACY, DEVICE_CONFIG_AUTH, + DEVICE_ID, + DEVICE_ID_MAC, IP_ADDRESS, MAC_ADDRESS, _mocked_device, @@ -404,19 +406,48 @@ async def test_feature_no_category( @pytest.mark.parametrize( - ("identifier_base", "expected_message", "expected_count"), + ("device_id", "id_count", "domains", "expected_message"), [ - pytest.param("C0:06:C3:42:54:2B", "Replaced", 1, id="success"), - pytest.param("123456789", "Unable to replace", 3, id="failure"), + pytest.param(DEVICE_ID_MAC, 1, [DOMAIN], None, id="mac-id-no-children"), + pytest.param(DEVICE_ID_MAC, 3, [DOMAIN], "Replaced", id="mac-id-children"), + pytest.param( + DEVICE_ID_MAC, + 1, + [DOMAIN, "other"], + None, + id="mac-id-no-children-other-domain", + ), + pytest.param( + DEVICE_ID_MAC, + 3, + [DOMAIN, "other"], + "Replaced", + id="mac-id-children-other-domain", + ), + pytest.param(DEVICE_ID, 1, [DOMAIN], None, id="not-mac-id-no-children"), + pytest.param( + DEVICE_ID, 3, [DOMAIN], "Unable to replace", id="not-mac-children" + ), + pytest.param( + DEVICE_ID, 1, [DOMAIN, "other"], None, id="not-mac-no-children-other-domain" + ), + pytest.param( + DEVICE_ID, + 3, + [DOMAIN, "other"], + "Unable to replace", + id="not-mac-children-other-domain", + ), ], ) async def test_unlink_devices( hass: HomeAssistant, device_registry: dr.DeviceRegistry, caplog: pytest.LogCaptureFixture, - identifier_base, + device_id, + id_count, + domains, expected_message, - expected_count, ) -> None: """Test for unlinking child device ids.""" entry = MockConfigEntry( @@ -429,43 +460,54 @@ async def test_unlink_devices( ) entry.add_to_hass(hass) - # Setup initial device registry, with linkages - mac = "C0:06:C3:42:54:2B" - identifiers = [ - (DOMAIN, identifier_base), - (DOMAIN, f"{identifier_base}_0001"), - (DOMAIN, f"{identifier_base}_0002"), + # Generate list of test identifiers + test_identifiers = [ + (domain, f"{device_id}{"" if i == 0 else f"_000{i}"}") + for i in range(id_count) + for domain in domains ] + update_msg_fragment = "identifiers for device dummy (hs300):" + update_msg = f"{expected_message} {update_msg_fragment}" if expected_message else "" + + # Expected identifiers should include all other domains or all the newer non-mac device ids + # or just the parent mac device id + expected_identifiers = [ + (domain, device_id) + for domain, device_id in test_identifiers + if domain != DOMAIN + or device_id.startswith(DEVICE_ID) + or device_id == DEVICE_ID_MAC + ] + device_registry.async_get_or_create( config_entry_id="123456", connections={ - (dr.CONNECTION_NETWORK_MAC, mac.lower()), + (dr.CONNECTION_NETWORK_MAC, MAC_ADDRESS), }, - identifiers=set(identifiers), + identifiers=set(test_identifiers), model="hs300", name="dummy", ) device_entries = dr.async_entries_for_config_entry(device_registry, entry.entry_id) assert device_entries[0].connections == { - (dr.CONNECTION_NETWORK_MAC, mac.lower()), + (dr.CONNECTION_NETWORK_MAC, MAC_ADDRESS), } - assert device_entries[0].identifiers == set(identifiers) + assert device_entries[0].identifiers == set(test_identifiers) await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() device_entries = dr.async_entries_for_config_entry(device_registry, entry.entry_id) - assert device_entries[0].connections == {(dr.CONNECTION_NETWORK_MAC, mac.lower())} - # If expected count is 1 will be the first identifier only - expected_identifiers = identifiers[:expected_count] + assert device_entries[0].connections == {(dr.CONNECTION_NETWORK_MAC, MAC_ADDRESS)} + assert device_entries[0].identifiers == set(expected_identifiers) assert entry.version == 1 assert entry.minor_version == 4 - msg = f"{expected_message} identifiers for device dummy (hs300): {set(identifiers)}" - assert msg in caplog.text + assert update_msg in caplog.text + assert "Migration to version 1.3 complete" in caplog.text async def test_move_credentials_hash( From 9b5d0f72dcdee18e07746eb45995fe159006e1b1 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 27 Jun 2024 22:20:25 +0200 Subject: [PATCH 1349/1445] Bump version to 2024.7.0b3 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 8291fb93fd7..33a86f57a5e 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -24,7 +24,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 7 -PATCH_VERSION: Final = "0b2" +PATCH_VERSION: Final = "0b3" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0) diff --git a/pyproject.toml b/pyproject.toml index 709022534b1..e4ccd9898e0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.7.0b2" +version = "2024.7.0b3" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From f28cbf1909f89d508d35a80c0bf6d7b58886ec7d Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 28 Jun 2024 08:42:47 +0200 Subject: [PATCH 1350/1445] Set stateclass on unknown numeric Tasmota sensors (#120650) --- homeassistant/components/tasmota/sensor.py | 9 + .../tasmota/snapshots/test_sensor.ambr | 298 ++++++++++++++++++ tests/components/tasmota/test_sensor.py | 25 ++ 3 files changed, 332 insertions(+) diff --git a/homeassistant/components/tasmota/sensor.py b/homeassistant/components/tasmota/sensor.py index a7fb415f037..db404884e67 100644 --- a/homeassistant/components/tasmota/sensor.py +++ b/homeassistant/components/tasmota/sensor.py @@ -298,6 +298,15 @@ class TasmotaSensor(TasmotaAvailability, TasmotaDiscoveryUpdate, SensorEntity): self._attr_native_unit_of_measurement = SENSOR_UNIT_MAP.get( self._tasmota_entity.unit, self._tasmota_entity.unit ) + if ( + self._attr_device_class is None + and self._attr_state_class is None + and self._attr_native_unit_of_measurement is None + ): + # If the sensor has a numeric value, but we couldn't detect what it is, + # set state class to measurement. + if self._tasmota_entity.discovered_as_numeric: + self._attr_state_class = SensorStateClass.MEASUREMENT async def async_added_to_hass(self) -> None: """Subscribe to MQTT events.""" diff --git a/tests/components/tasmota/snapshots/test_sensor.ambr b/tests/components/tasmota/snapshots/test_sensor.ambr index c5d70487749..b56115f189c 100644 --- a/tests/components/tasmota/snapshots/test_sensor.ambr +++ b/tests/components/tasmota/snapshots/test_sensor.ambr @@ -1546,3 +1546,301 @@ 'state': '2300', }) # --- +# name: test_controlling_state_via_mqtt[sensor_config9-entity_ids9-messages9] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Tasmota SENSOR1 Unknown', + }), + 'context': , + 'entity_id': 'sensor.tasmota_sensor1_unknown', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config9-entity_ids9-messages9].1 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.tasmota_sensor1_unknown', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'SENSOR1 Unknown', + 'platform': 'tasmota', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000049A3BC_sensor_sensor_SENSOR1_Unknown', + 'unit_of_measurement': None, + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config9-entity_ids9-messages9].10 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Tasmota SENSOR3 Unknown', + 'state_class': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_sensor3_unknown', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '20.5', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config9-entity_ids9-messages9].11 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Tasmota SENSOR4 Unknown', + 'state_class': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_sensor4_unknown', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '20.5', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config9-entity_ids9-messages9].12 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Tasmota SENSOR1 Unknown', + }), + 'context': , + 'entity_id': 'sensor.tasmota_sensor1_unknown', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '20', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config9-entity_ids9-messages9].13 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Tasmota SENSOR2 Unknown', + }), + 'context': , + 'entity_id': 'sensor.tasmota_sensor2_unknown', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '20', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config9-entity_ids9-messages9].14 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Tasmota SENSOR3 Unknown', + 'state_class': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_sensor3_unknown', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '20', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config9-entity_ids9-messages9].15 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Tasmota SENSOR4 Unknown', + 'state_class': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_sensor4_unknown', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '20', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config9-entity_ids9-messages9].2 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Tasmota SENSOR2 Unknown', + }), + 'context': , + 'entity_id': 'sensor.tasmota_sensor2_unknown', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config9-entity_ids9-messages9].3 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.tasmota_sensor2_unknown', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'SENSOR2 Unknown', + 'platform': 'tasmota', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000049A3BC_sensor_sensor_SENSOR2_Unknown', + 'unit_of_measurement': None, + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config9-entity_ids9-messages9].4 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Tasmota SENSOR3 Unknown', + 'state_class': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_sensor3_unknown', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config9-entity_ids9-messages9].5 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.tasmota_sensor3_unknown', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'SENSOR3 Unknown', + 'platform': 'tasmota', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000049A3BC_sensor_sensor_SENSOR3_Unknown', + 'unit_of_measurement': None, + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config9-entity_ids9-messages9].6 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Tasmota SENSOR4 Unknown', + 'state_class': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_sensor4_unknown', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config9-entity_ids9-messages9].7 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.tasmota_sensor4_unknown', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'SENSOR4 Unknown', + 'platform': 'tasmota', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000049A3BC_sensor_sensor_SENSOR4_Unknown', + 'unit_of_measurement': None, + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config9-entity_ids9-messages9].8 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Tasmota SENSOR1 Unknown', + }), + 'context': , + 'entity_id': 'sensor.tasmota_sensor1_unknown', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '20.5', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config9-entity_ids9-messages9].9 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Tasmota SENSOR2 Unknown', + }), + 'context': , + 'entity_id': 'sensor.tasmota_sensor2_unknown', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '20.5', + }) +# --- diff --git a/tests/components/tasmota/test_sensor.py b/tests/components/tasmota/test_sensor.py index c01485d12a7..44a6ce65fd1 100644 --- a/tests/components/tasmota/test_sensor.py +++ b/tests/components/tasmota/test_sensor.py @@ -50,6 +50,17 @@ BAD_LIST_SENSOR_CONFIG_3 = { } } +# This configuration has sensors which type we can't guess +DEFAULT_SENSOR_CONFIG_UNKNOWN = { + "sn": { + "Time": "2020-09-25T12:47:15", + "SENSOR1": {"Unknown": None}, + "SENSOR2": {"Unknown": "123"}, + "SENSOR3": {"Unknown": 123}, + "SENSOR4": {"Unknown": 123.0}, + } +} + # This configuration has some sensors where values are lists # Home Assistant maps this to one sensor for each list item LIST_SENSOR_CONFIG = { @@ -279,6 +290,20 @@ TEMPERATURE_SENSOR_CONFIG = { ), ), ), + # Test we automatically set state class to measurement on unknown numerical sensors + ( + DEFAULT_SENSOR_CONFIG_UNKNOWN, + [ + "sensor.tasmota_sensor1_unknown", + "sensor.tasmota_sensor2_unknown", + "sensor.tasmota_sensor3_unknown", + "sensor.tasmota_sensor4_unknown", + ], + ( + '{"SENSOR1":{"Unknown":20.5},"SENSOR2":{"Unknown":20.5},"SENSOR3":{"Unknown":20.5},"SENSOR4":{"Unknown":20.5}}', + '{"StatusSNS":{"SENSOR1":{"Unknown":20},"SENSOR2":{"Unknown":20},"SENSOR3":{"Unknown":20},"SENSOR4":{"Unknown":20}}}', + ), + ), ], ) async def test_controlling_state_via_mqtt( From 876fb234ceb906e71d672ba3d94856d5a0137b61 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 27 Jun 2024 22:56:22 +0200 Subject: [PATCH 1351/1445] Bump hatasmota to 0.9.2 (#120670) --- .../components/tasmota/manifest.json | 2 +- homeassistant/components/tasmota/sensor.py | 78 +++--- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../tasmota/snapshots/test_sensor.ambr | 232 +++++++++++++++--- tests/components/tasmota/test_sensor.py | 6 +- 6 files changed, 247 insertions(+), 75 deletions(-) diff --git a/homeassistant/components/tasmota/manifest.json b/homeassistant/components/tasmota/manifest.json index 69233de07d8..783483c6ffd 100644 --- a/homeassistant/components/tasmota/manifest.json +++ b/homeassistant/components/tasmota/manifest.json @@ -8,5 +8,5 @@ "iot_class": "local_push", "loggers": ["hatasmota"], "mqtt": ["tasmota/discovery/#"], - "requirements": ["HATasmota==0.9.1"] + "requirements": ["HATasmota==0.9.2"] } diff --git a/homeassistant/components/tasmota/sensor.py b/homeassistant/components/tasmota/sensor.py index db404884e67..e87ff88092e 100644 --- a/homeassistant/components/tasmota/sensor.py +++ b/homeassistant/components/tasmota/sensor.py @@ -53,26 +53,10 @@ ICON = "icon" # A Tasmota sensor type may be mapped to either a device class or an icon, # both can only be set if the default device class icon is not appropriate SENSOR_DEVICE_CLASS_ICON_MAP: dict[str, dict[str, Any]] = { - hc.SENSOR_ACTIVE_ENERGYEXPORT: { - DEVICE_CLASS: SensorDeviceClass.ENERGY, - STATE_CLASS: SensorStateClass.TOTAL, - }, - hc.SENSOR_ACTIVE_ENERGYIMPORT: { - DEVICE_CLASS: SensorDeviceClass.ENERGY, - STATE_CLASS: SensorStateClass.TOTAL, - }, - hc.SENSOR_ACTIVE_POWERUSAGE: { - DEVICE_CLASS: SensorDeviceClass.POWER, - STATE_CLASS: SensorStateClass.MEASUREMENT, - }, hc.SENSOR_AMBIENT: { DEVICE_CLASS: SensorDeviceClass.ILLUMINANCE, STATE_CLASS: SensorStateClass.MEASUREMENT, }, - hc.SENSOR_APPARENT_POWERUSAGE: { - DEVICE_CLASS: SensorDeviceClass.APPARENT_POWER, - STATE_CLASS: SensorStateClass.MEASUREMENT, - }, hc.SENSOR_BATTERY: { DEVICE_CLASS: SensorDeviceClass.BATTERY, STATE_CLASS: SensorStateClass.MEASUREMENT, @@ -92,7 +76,7 @@ SENSOR_DEVICE_CLASS_ICON_MAP: dict[str, dict[str, Any]] = { DEVICE_CLASS: SensorDeviceClass.CURRENT, STATE_CLASS: SensorStateClass.MEASUREMENT, }, - hc.SENSOR_CURRENTNEUTRAL: { + hc.SENSOR_CURRENT_NEUTRAL: { DEVICE_CLASS: SensorDeviceClass.CURRENT, STATE_CLASS: SensorStateClass.MEASUREMENT, }, @@ -110,6 +94,34 @@ SENSOR_DEVICE_CLASS_ICON_MAP: dict[str, dict[str, Any]] = { DEVICE_CLASS: SensorDeviceClass.ENERGY, STATE_CLASS: SensorStateClass.TOTAL, }, + hc.SENSOR_ENERGY_EXPORT_ACTIVE: { + DEVICE_CLASS: SensorDeviceClass.ENERGY, + STATE_CLASS: SensorStateClass.TOTAL, + }, + hc.SENSOR_ENERGY_EXPORT_REACTIVE: {STATE_CLASS: SensorStateClass.TOTAL}, + hc.SENSOR_ENERGY_EXPORT_TARIFF: { + DEVICE_CLASS: SensorDeviceClass.ENERGY, + STATE_CLASS: SensorStateClass.TOTAL, + }, + hc.SENSOR_ENERGY_IMPORT_ACTIVE: { + DEVICE_CLASS: SensorDeviceClass.ENERGY, + STATE_CLASS: SensorStateClass.TOTAL, + }, + hc.SENSOR_ENERGY_IMPORT_REACTIVE: {STATE_CLASS: SensorStateClass.TOTAL}, + hc.SENSOR_ENERGY_IMPORT_TODAY: { + DEVICE_CLASS: SensorDeviceClass.ENERGY, + STATE_CLASS: SensorStateClass.TOTAL_INCREASING, + }, + hc.SENSOR_ENERGY_IMPORT_TOTAL: { + DEVICE_CLASS: SensorDeviceClass.ENERGY, + STATE_CLASS: SensorStateClass.TOTAL, + }, + hc.SENSOR_ENERGY_IMPORT_TOTAL_TARIFF: { + DEVICE_CLASS: SensorDeviceClass.ENERGY, + STATE_CLASS: SensorStateClass.TOTAL, + }, + hc.SENSOR_ENERGY_IMPORT_YESTERDAY: {DEVICE_CLASS: SensorDeviceClass.ENERGY}, + hc.SENSOR_ENERGY_TOTAL_START_TIME: {ICON: "mdi:progress-clock"}, hc.SENSOR_FREQUENCY: { DEVICE_CLASS: SensorDeviceClass.FREQUENCY, STATE_CLASS: SensorStateClass.MEASUREMENT, @@ -122,6 +134,14 @@ SENSOR_DEVICE_CLASS_ICON_MAP: dict[str, dict[str, Any]] = { DEVICE_CLASS: SensorDeviceClass.ILLUMINANCE, STATE_CLASS: SensorStateClass.MEASUREMENT, }, + hc.SENSOR_POWER_ACTIVE: { + DEVICE_CLASS: SensorDeviceClass.POWER, + STATE_CLASS: SensorStateClass.MEASUREMENT, + }, + hc.SENSOR_POWER_APPARENT: { + DEVICE_CLASS: SensorDeviceClass.APPARENT_POWER, + STATE_CLASS: SensorStateClass.MEASUREMENT, + }, hc.SENSOR_STATUS_IP: {ICON: "mdi:ip-network"}, hc.SENSOR_STATUS_LINK_COUNT: {ICON: "mdi:counter"}, hc.SENSOR_MOISTURE: {DEVICE_CLASS: SensorDeviceClass.MOISTURE}, @@ -144,11 +164,11 @@ SENSOR_DEVICE_CLASS_ICON_MAP: dict[str, dict[str, Any]] = { DEVICE_CLASS: SensorDeviceClass.PM25, STATE_CLASS: SensorStateClass.MEASUREMENT, }, - hc.SENSOR_POWERFACTOR: { + hc.SENSOR_POWER_FACTOR: { DEVICE_CLASS: SensorDeviceClass.POWER_FACTOR, STATE_CLASS: SensorStateClass.MEASUREMENT, }, - hc.SENSOR_POWERUSAGE: { + hc.SENSOR_POWER: { DEVICE_CLASS: SensorDeviceClass.POWER, STATE_CLASS: SensorStateClass.MEASUREMENT, }, @@ -156,14 +176,12 @@ SENSOR_DEVICE_CLASS_ICON_MAP: dict[str, dict[str, Any]] = { DEVICE_CLASS: SensorDeviceClass.PRESSURE, STATE_CLASS: SensorStateClass.MEASUREMENT, }, - hc.SENSOR_PRESSUREATSEALEVEL: { + hc.SENSOR_PRESSURE_AT_SEA_LEVEL: { DEVICE_CLASS: SensorDeviceClass.PRESSURE, STATE_CLASS: SensorStateClass.MEASUREMENT, }, hc.SENSOR_PROXIMITY: {ICON: "mdi:ruler"}, - hc.SENSOR_REACTIVE_ENERGYEXPORT: {STATE_CLASS: SensorStateClass.TOTAL}, - hc.SENSOR_REACTIVE_ENERGYIMPORT: {STATE_CLASS: SensorStateClass.TOTAL}, - hc.SENSOR_REACTIVE_POWERUSAGE: { + hc.SENSOR_POWER_REACTIVE: { DEVICE_CLASS: SensorDeviceClass.REACTIVE_POWER, STATE_CLASS: SensorStateClass.MEASUREMENT, }, @@ -182,19 +200,6 @@ SENSOR_DEVICE_CLASS_ICON_MAP: dict[str, dict[str, Any]] = { DEVICE_CLASS: SensorDeviceClass.TEMPERATURE, STATE_CLASS: SensorStateClass.MEASUREMENT, }, - hc.SENSOR_TODAY: { - DEVICE_CLASS: SensorDeviceClass.ENERGY, - STATE_CLASS: SensorStateClass.TOTAL_INCREASING, - }, - hc.SENSOR_TOTAL: { - DEVICE_CLASS: SensorDeviceClass.ENERGY, - STATE_CLASS: SensorStateClass.TOTAL, - }, - hc.SENSOR_TOTAL_TARIFF: { - DEVICE_CLASS: SensorDeviceClass.ENERGY, - STATE_CLASS: SensorStateClass.TOTAL, - }, - hc.SENSOR_TOTAL_START_TIME: {ICON: "mdi:progress-clock"}, hc.SENSOR_TVOC: {ICON: "mdi:air-filter"}, hc.SENSOR_VOLTAGE: { DEVICE_CLASS: SensorDeviceClass.VOLTAGE, @@ -204,7 +209,6 @@ SENSOR_DEVICE_CLASS_ICON_MAP: dict[str, dict[str, Any]] = { DEVICE_CLASS: SensorDeviceClass.WEIGHT, STATE_CLASS: SensorStateClass.MEASUREMENT, }, - hc.SENSOR_YESTERDAY: {DEVICE_CLASS: SensorDeviceClass.ENERGY}, } SENSOR_UNIT_MAP = { diff --git a/requirements_all.txt b/requirements_all.txt index 98b5548cd38..d2e51a365e7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -22,7 +22,7 @@ DoorBirdPy==2.1.0 HAP-python==4.9.1 # homeassistant.components.tasmota -HATasmota==0.9.1 +HATasmota==0.9.2 # homeassistant.components.mastodon Mastodon.py==1.8.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2857aee8c9b..21e6eedebc7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -19,7 +19,7 @@ DoorBirdPy==2.1.0 HAP-python==4.9.1 # homeassistant.components.tasmota -HATasmota==0.9.1 +HATasmota==0.9.2 # homeassistant.components.doods # homeassistant.components.generic diff --git a/tests/components/tasmota/snapshots/test_sensor.ambr b/tests/components/tasmota/snapshots/test_sensor.ambr index b56115f189c..be011e595b9 100644 --- a/tests/components/tasmota/snapshots/test_sensor.ambr +++ b/tests/components/tasmota/snapshots/test_sensor.ambr @@ -280,6 +280,102 @@ 'unit_of_measurement': , }) # --- +# name: test_controlling_state_via_mqtt[sensor_config2-entity_ids2-messages2].10 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Tasmota ENERGY ExportTariff 0', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_energy_exporttariff_0', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '5.6', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config2-entity_ids2-messages2].11 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Tasmota ENERGY ExportTariff 1', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_energy_exporttariff_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '7.8', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config2-entity_ids2-messages2].12 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Tasmota ENERGY TotalTariff 0', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_energy_totaltariff_0', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '5.6', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config2-entity_ids2-messages2].13 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Tasmota ENERGY TotalTariff 1', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_energy_totaltariff_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '7.8', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config2-entity_ids2-messages2].14 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Tasmota ENERGY ExportTariff 0', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_energy_exporttariff_0', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1.2', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config2-entity_ids2-messages2].15 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Tasmota ENERGY ExportTariff 1', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_energy_exporttariff_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '3.4', + }) +# --- # name: test_controlling_state_via_mqtt[sensor_config2-entity_ids2-messages2].2 StateSnapshot({ 'attributes': ReadOnlyDict({ @@ -332,6 +428,108 @@ }) # --- # name: test_controlling_state_via_mqtt[sensor_config2-entity_ids2-messages2].4 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Tasmota ENERGY ExportTariff 0', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_energy_exporttariff_0', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config2-entity_ids2-messages2].5 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.tasmota_energy_exporttariff_0', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'ENERGY ExportTariff 0', + 'platform': 'tasmota', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000049A3BC_sensor_sensor_ENERGY_ExportTariff_0', + 'unit_of_measurement': , + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config2-entity_ids2-messages2].6 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Tasmota ENERGY ExportTariff 1', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_energy_exporttariff_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config2-entity_ids2-messages2].7 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.tasmota_energy_exporttariff_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'ENERGY ExportTariff 1', + 'platform': 'tasmota', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000049A3BC_sensor_sensor_ENERGY_ExportTariff_1', + 'unit_of_measurement': , + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config2-entity_ids2-messages2].8 StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'energy', @@ -347,7 +545,7 @@ 'state': '1.2', }) # --- -# name: test_controlling_state_via_mqtt[sensor_config2-entity_ids2-messages2].5 +# name: test_controlling_state_via_mqtt[sensor_config2-entity_ids2-messages2].9 StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'energy', @@ -363,38 +561,6 @@ 'state': '3.4', }) # --- -# name: test_controlling_state_via_mqtt[sensor_config2-entity_ids2-messages2].6 - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'energy', - 'friendly_name': 'Tasmota ENERGY TotalTariff 0', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.tasmota_energy_totaltariff_0', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '5.6', - }) -# --- -# name: test_controlling_state_via_mqtt[sensor_config2-entity_ids2-messages2].7 - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'energy', - 'friendly_name': 'Tasmota ENERGY TotalTariff 1', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.tasmota_energy_totaltariff_1', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '7.8', - }) -# --- # name: test_controlling_state_via_mqtt[sensor_config3-entity_ids3-messages3] StateSnapshot({ 'attributes': ReadOnlyDict({ diff --git a/tests/components/tasmota/test_sensor.py b/tests/components/tasmota/test_sensor.py index 44a6ce65fd1..78235f7ebf5 100644 --- a/tests/components/tasmota/test_sensor.py +++ b/tests/components/tasmota/test_sensor.py @@ -209,10 +209,12 @@ TEMPERATURE_SENSOR_CONFIG = { [ "sensor.tasmota_energy_totaltariff_0", "sensor.tasmota_energy_totaltariff_1", + "sensor.tasmota_energy_exporttariff_0", + "sensor.tasmota_energy_exporttariff_1", ], ( - '{"ENERGY":{"TotalTariff":[1.2,3.4]}}', - '{"StatusSNS":{"ENERGY":{"TotalTariff":[5.6,7.8]}}}', + '{"ENERGY":{"ExportTariff":[5.6,7.8],"TotalTariff":[1.2,3.4]}}', + '{"StatusSNS":{"ENERGY":{"ExportTariff":[1.2,3.4],"TotalTariff":[5.6,7.8]}}}', ), ), ( From ca515f740e53a83c8b1b5a4fefb2fa2309807a25 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 28 Jun 2024 11:15:44 +0200 Subject: [PATCH 1352/1445] Bump panasonic_viera to 0.4.2 (#120692) * Bump panasonic_viera to 0.4.2 * Bump panasonic_viera to 0.4.2 * Bump panasonic_viera to 0.4.2 * Fix Keys --- .../components/panasonic_viera/__init__.py | 8 ++++---- .../components/panasonic_viera/manifest.json | 2 +- .../components/panasonic_viera/media_player.py | 18 +++++++++--------- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../components/panasonic_viera/test_remote.py | 2 +- 6 files changed, 17 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/panasonic_viera/__init__.py b/homeassistant/components/panasonic_viera/__init__.py index b2f3bbba91a..2cf91792800 100644 --- a/homeassistant/components/panasonic_viera/__init__.py +++ b/homeassistant/components/panasonic_viera/__init__.py @@ -196,10 +196,10 @@ class Remote: self.muted = self._control.get_mute() self.volume = self._control.get_volume() / 100 - async def async_send_key(self, key): + async def async_send_key(self, key: Keys | str) -> None: """Send a key to the TV and handle exceptions.""" try: - key = getattr(Keys, key) + key = getattr(Keys, key.upper()) except (AttributeError, TypeError): key = getattr(key, "value", key) @@ -211,13 +211,13 @@ class Remote: await self._on_action.async_run(context=context) await self.async_update() elif self.state != STATE_ON: - await self.async_send_key(Keys.power) + await self.async_send_key(Keys.POWER) await self.async_update() async def async_turn_off(self): """Turn off the TV.""" if self.state != STATE_OFF: - await self.async_send_key(Keys.power) + await self.async_send_key(Keys.POWER) self.state = STATE_OFF await self.async_update() diff --git a/homeassistant/components/panasonic_viera/manifest.json b/homeassistant/components/panasonic_viera/manifest.json index 2afa6599cb2..d9809e5883a 100644 --- a/homeassistant/components/panasonic_viera/manifest.json +++ b/homeassistant/components/panasonic_viera/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/panasonic_viera", "iot_class": "local_polling", "loggers": ["panasonic_viera"], - "requirements": ["panasonic-viera==0.3.6"] + "requirements": ["panasonic-viera==0.4.2"] } diff --git a/homeassistant/components/panasonic_viera/media_player.py b/homeassistant/components/panasonic_viera/media_player.py index 44063022129..76ca76c1ca6 100644 --- a/homeassistant/components/panasonic_viera/media_player.py +++ b/homeassistant/components/panasonic_viera/media_player.py @@ -126,11 +126,11 @@ class PanasonicVieraTVEntity(MediaPlayerEntity): async def async_volume_up(self) -> None: """Volume up the media player.""" - await self._remote.async_send_key(Keys.volume_up) + await self._remote.async_send_key(Keys.VOLUME_UP) async def async_volume_down(self) -> None: """Volume down media player.""" - await self._remote.async_send_key(Keys.volume_down) + await self._remote.async_send_key(Keys.VOLUME_DOWN) async def async_mute_volume(self, mute: bool) -> None: """Send mute command.""" @@ -143,33 +143,33 @@ class PanasonicVieraTVEntity(MediaPlayerEntity): async def async_media_play_pause(self) -> None: """Simulate play pause media player.""" if self._remote.playing: - await self._remote.async_send_key(Keys.pause) + await self._remote.async_send_key(Keys.PAUSE) self._remote.playing = False else: - await self._remote.async_send_key(Keys.play) + await self._remote.async_send_key(Keys.PLAY) self._remote.playing = True async def async_media_play(self) -> None: """Send play command.""" - await self._remote.async_send_key(Keys.play) + await self._remote.async_send_key(Keys.PLAY) self._remote.playing = True async def async_media_pause(self) -> None: """Send pause command.""" - await self._remote.async_send_key(Keys.pause) + await self._remote.async_send_key(Keys.PAUSE) self._remote.playing = False async def async_media_stop(self) -> None: """Stop playback.""" - await self._remote.async_send_key(Keys.stop) + await self._remote.async_send_key(Keys.STOP) async def async_media_next_track(self) -> None: """Send the fast forward command.""" - await self._remote.async_send_key(Keys.fast_forward) + await self._remote.async_send_key(Keys.FAST_FORWARD) async def async_media_previous_track(self) -> None: """Send the rewind command.""" - await self._remote.async_send_key(Keys.rewind) + await self._remote.async_send_key(Keys.REWIND) async def async_play_media( self, media_type: MediaType | str, media_id: str, **kwargs: Any diff --git a/requirements_all.txt b/requirements_all.txt index d2e51a365e7..57cbb7b7710 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1528,7 +1528,7 @@ paho-mqtt==1.6.1 panacotta==0.2 # homeassistant.components.panasonic_viera -panasonic-viera==0.3.6 +panasonic-viera==0.4.2 # homeassistant.components.dunehd pdunehd==1.3.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 21e6eedebc7..306b791488a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1228,7 +1228,7 @@ p1monitor==3.0.0 paho-mqtt==1.6.1 # homeassistant.components.panasonic_viera -panasonic-viera==0.3.6 +panasonic-viera==0.4.2 # homeassistant.components.dunehd pdunehd==1.3.2 diff --git a/tests/components/panasonic_viera/test_remote.py b/tests/components/panasonic_viera/test_remote.py index 05254753d3f..3ae241fc5e9 100644 --- a/tests/components/panasonic_viera/test_remote.py +++ b/tests/components/panasonic_viera/test_remote.py @@ -46,7 +46,7 @@ async def test_onoff(hass: HomeAssistant, mock_remote) -> None: await hass.services.async_call(REMOTE_DOMAIN, SERVICE_TURN_ON, data) await hass.async_block_till_done() - power = getattr(Keys.power, "value", Keys.power) + power = getattr(Keys.POWER, "value", Keys.POWER) assert mock_remote.send_key.call_args_list == [call(power), call(power)] From ef3ecb618350a1f60b7650cd935399b3f74ff9c0 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 28 Jun 2024 08:36:10 +0200 Subject: [PATCH 1353/1445] Bump apsystems-ez1 to 1.3.3 (#120702) --- homeassistant/components/apsystems/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/apsystems/manifest.json b/homeassistant/components/apsystems/manifest.json index 8e0ac00796d..cba3e59dba0 100644 --- a/homeassistant/components/apsystems/manifest.json +++ b/homeassistant/components/apsystems/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/apsystems", "integration_type": "device", "iot_class": "local_polling", - "requirements": ["apsystems-ez1==1.3.1"] + "requirements": ["apsystems-ez1==1.3.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index 57cbb7b7710..cd2a2335987 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -464,7 +464,7 @@ apprise==1.8.0 aprslib==0.7.2 # homeassistant.components.apsystems -apsystems-ez1==1.3.1 +apsystems-ez1==1.3.3 # homeassistant.components.aqualogic aqualogic==2.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 306b791488a..6bc406c6982 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -428,7 +428,7 @@ apprise==1.8.0 aprslib==0.7.2 # homeassistant.components.apsystems -apsystems-ez1==1.3.1 +apsystems-ez1==1.3.3 # homeassistant.components.aranet aranet4==2.3.4 From 1227d56aa219391484b2a07c0100109690ae782c Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Thu, 27 Jun 2024 23:12:20 +0200 Subject: [PATCH 1354/1445] Bump `nextdns` to version 3.1.0 (#120703) Co-authored-by: Maciej Bieniek <478555+bieniu@users.noreply.github.com> --- homeassistant/components/nextdns/__init__.py | 6 +++--- homeassistant/components/nextdns/config_flow.py | 11 +++++------ homeassistant/components/nextdns/coordinator.py | 12 ++++++++---- homeassistant/components/nextdns/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/nextdns/test_config_flow.py | 2 ++ tests/components/nextdns/test_init.py | 9 +++++++-- tests/components/nextdns/test_switch.py | 13 +++++++++++-- 9 files changed, 39 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/nextdns/__init__.py b/homeassistant/components/nextdns/__init__.py index f11611007c2..4256126b3c7 100644 --- a/homeassistant/components/nextdns/__init__.py +++ b/homeassistant/components/nextdns/__init__.py @@ -18,6 +18,7 @@ from nextdns import ( NextDns, Settings, ) +from tenacity import RetryError from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_API_KEY, Platform @@ -84,9 +85,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: NextDnsConfigEntry) -> b websession = async_get_clientsession(hass) try: - async with asyncio.timeout(10): - nextdns = await NextDns.create(websession, api_key) - except (ApiError, ClientConnectorError, TimeoutError) as err: + nextdns = await NextDns.create(websession, api_key) + except (ApiError, ClientConnectorError, RetryError, TimeoutError) as err: raise ConfigEntryNotReady from err tasks = [] diff --git a/homeassistant/components/nextdns/config_flow.py b/homeassistant/components/nextdns/config_flow.py index 4955bbb4cad..bd79112b1f9 100644 --- a/homeassistant/components/nextdns/config_flow.py +++ b/homeassistant/components/nextdns/config_flow.py @@ -2,11 +2,11 @@ from __future__ import annotations -import asyncio from typing import Any from aiohttp.client_exceptions import ClientConnectorError from nextdns import ApiError, InvalidApiKeyError, NextDns +from tenacity import RetryError import voluptuous as vol from homeassistant.config_entries import ConfigFlow, ConfigFlowResult @@ -37,13 +37,12 @@ class NextDnsFlowHandler(ConfigFlow, domain=DOMAIN): if user_input is not None: self.api_key = user_input[CONF_API_KEY] try: - async with asyncio.timeout(10): - self.nextdns = await NextDns.create( - websession, user_input[CONF_API_KEY] - ) + self.nextdns = await NextDns.create( + websession, user_input[CONF_API_KEY] + ) except InvalidApiKeyError: errors["base"] = "invalid_api_key" - except (ApiError, ClientConnectorError, TimeoutError): + except (ApiError, ClientConnectorError, RetryError, TimeoutError): errors["base"] = "cannot_connect" except Exception: # noqa: BLE001 errors["base"] = "unknown" diff --git a/homeassistant/components/nextdns/coordinator.py b/homeassistant/components/nextdns/coordinator.py index cad1aeac070..5210807bd3c 100644 --- a/homeassistant/components/nextdns/coordinator.py +++ b/homeassistant/components/nextdns/coordinator.py @@ -1,6 +1,5 @@ """NextDns coordinator.""" -import asyncio from datetime import timedelta import logging from typing import TypeVar @@ -19,6 +18,7 @@ from nextdns import ( Settings, ) from nextdns.model import NextDnsData +from tenacity import RetryError from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo @@ -58,9 +58,13 @@ class NextDnsUpdateCoordinator(DataUpdateCoordinator[CoordinatorDataT]): async def _async_update_data(self) -> CoordinatorDataT: """Update data via internal method.""" try: - async with asyncio.timeout(10): - return await self._async_update_data_internal() - except (ApiError, ClientConnectorError, InvalidApiKeyError) as err: + return await self._async_update_data_internal() + except ( + ApiError, + ClientConnectorError, + InvalidApiKeyError, + RetryError, + ) as err: raise UpdateFailed(err) from err async def _async_update_data_internal(self) -> CoordinatorDataT: diff --git a/homeassistant/components/nextdns/manifest.json b/homeassistant/components/nextdns/manifest.json index 1e7145ef6d1..b65706ef1ce 100644 --- a/homeassistant/components/nextdns/manifest.json +++ b/homeassistant/components/nextdns/manifest.json @@ -8,5 +8,5 @@ "iot_class": "cloud_polling", "loggers": ["nextdns"], "quality_scale": "platinum", - "requirements": ["nextdns==3.0.0"] + "requirements": ["nextdns==3.1.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index cd2a2335987..cf0e22167a0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1404,7 +1404,7 @@ nextcloudmonitor==1.5.0 nextcord==2.6.0 # homeassistant.components.nextdns -nextdns==3.0.0 +nextdns==3.1.0 # homeassistant.components.nibe_heatpump nibe==2.8.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6bc406c6982..f7254727d3c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1143,7 +1143,7 @@ nextcloudmonitor==1.5.0 nextcord==2.6.0 # homeassistant.components.nextdns -nextdns==3.0.0 +nextdns==3.1.0 # homeassistant.components.nibe_heatpump nibe==2.8.0 diff --git a/tests/components/nextdns/test_config_flow.py b/tests/components/nextdns/test_config_flow.py index 9247288eebf..7571eef347e 100644 --- a/tests/components/nextdns/test_config_flow.py +++ b/tests/components/nextdns/test_config_flow.py @@ -4,6 +4,7 @@ from unittest.mock import patch from nextdns import ApiError, InvalidApiKeyError import pytest +from tenacity import RetryError from homeassistant.components.nextdns.const import CONF_PROFILE_ID, DOMAIN from homeassistant.config_entries import SOURCE_USER @@ -57,6 +58,7 @@ async def test_form_create_entry(hass: HomeAssistant) -> None: [ (ApiError("API Error"), "cannot_connect"), (InvalidApiKeyError, "invalid_api_key"), + (RetryError("Retry Error"), "cannot_connect"), (TimeoutError, "cannot_connect"), (ValueError, "unknown"), ], diff --git a/tests/components/nextdns/test_init.py b/tests/components/nextdns/test_init.py index f7b85bb8a54..61a487d917c 100644 --- a/tests/components/nextdns/test_init.py +++ b/tests/components/nextdns/test_init.py @@ -3,6 +3,8 @@ from unittest.mock import patch from nextdns import ApiError +import pytest +from tenacity import RetryError from homeassistant.components.nextdns.const import CONF_PROFILE_ID, DOMAIN from homeassistant.config_entries import ConfigEntryState @@ -24,7 +26,10 @@ async def test_async_setup_entry(hass: HomeAssistant) -> None: assert state.state == "20.0" -async def test_config_not_ready(hass: HomeAssistant) -> None: +@pytest.mark.parametrize( + "exc", [ApiError("API Error"), RetryError("Retry Error"), TimeoutError] +) +async def test_config_not_ready(hass: HomeAssistant, exc: Exception) -> None: """Test for setup failure if the connection to the service fails.""" entry = MockConfigEntry( domain=DOMAIN, @@ -35,7 +40,7 @@ async def test_config_not_ready(hass: HomeAssistant) -> None: with patch( "homeassistant.components.nextdns.NextDns.get_profiles", - side_effect=ApiError("API Error"), + side_effect=exc, ): entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) diff --git a/tests/components/nextdns/test_switch.py b/tests/components/nextdns/test_switch.py index 059585e9ffe..6e344e34336 100644 --- a/tests/components/nextdns/test_switch.py +++ b/tests/components/nextdns/test_switch.py @@ -8,6 +8,7 @@ from aiohttp.client_exceptions import ClientConnectorError from nextdns import ApiError import pytest from syrupy import SnapshotAssertion +from tenacity import RetryError from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.const import ( @@ -94,7 +95,15 @@ async def test_switch_off(hass: HomeAssistant) -> None: mock_switch_on.assert_called_once() -async def test_availability(hass: HomeAssistant) -> None: +@pytest.mark.parametrize( + "exc", + [ + ApiError("API Error"), + RetryError("Retry Error"), + TimeoutError, + ], +) +async def test_availability(hass: HomeAssistant, exc: Exception) -> None: """Ensure that we mark the entities unavailable correctly when service causes an error.""" await init_integration(hass) @@ -106,7 +115,7 @@ async def test_availability(hass: HomeAssistant) -> None: future = utcnow() + timedelta(minutes=10) with patch( "homeassistant.components.nextdns.NextDns.get_settings", - side_effect=ApiError("API Error"), + side_effect=exc, ): async_fire_time_changed(hass, future) await hass.async_block_till_done(wait_background_tasks=True) From 35d145d3bc1d34f126864ad71c2d6f82694487b9 Mon Sep 17 00:00:00 2001 From: dougiteixeira <31328123+dougiteixeira@users.noreply.github.com> Date: Fri, 28 Jun 2024 07:22:24 -0300 Subject: [PATCH 1355/1445] Link the Statistics helper entity to the source entity device (#120705) --- .../components/statistics/__init__.py | 11 ++- homeassistant/components/statistics/sensor.py | 8 ++ tests/components/statistics/test_init.py | 92 +++++++++++++++++++ tests/components/statistics/test_sensor.py | 49 +++++++++- 4 files changed, 158 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/statistics/__init__.py b/homeassistant/components/statistics/__init__.py index 70739c618f7..f71274e0ee7 100644 --- a/homeassistant/components/statistics/__init__.py +++ b/homeassistant/components/statistics/__init__.py @@ -1,8 +1,11 @@ """The statistics component.""" from homeassistant.config_entries import ConfigEntry -from homeassistant.const import Platform +from homeassistant.const import CONF_ENTITY_ID, Platform from homeassistant.core import HomeAssistant +from homeassistant.helpers.device import ( + async_remove_stale_devices_links_keep_entity_device, +) DOMAIN = "statistics" PLATFORMS = [Platform.SENSOR] @@ -11,6 +14,12 @@ PLATFORMS = [Platform.SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Statistics from a config entry.""" + async_remove_stale_devices_links_keep_entity_device( + hass, + entry.entry_id, + entry.options[CONF_ENTITY_ID], + ) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload(entry.add_update_listener(update_listener)) diff --git a/homeassistant/components/statistics/sensor.py b/homeassistant/components/statistics/sensor.py index 8d28254ad61..ca1d75b57ed 100644 --- a/homeassistant/components/statistics/sensor.py +++ b/homeassistant/components/statistics/sensor.py @@ -43,6 +43,7 @@ from homeassistant.core import ( split_entity_id, ) from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.device import async_device_info_to_link_from_entity from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import ( async_track_point_in_utc_time, @@ -268,6 +269,7 @@ async def async_setup_platform( async_add_entities( new_entities=[ StatisticsSensor( + hass=hass, source_entity_id=config[CONF_ENTITY_ID], name=config[CONF_NAME], unique_id=config.get(CONF_UNIQUE_ID), @@ -304,6 +306,7 @@ async def async_setup_entry( async_add_entities( [ StatisticsSensor( + hass=hass, source_entity_id=entry.options[CONF_ENTITY_ID], name=entry.options[CONF_NAME], unique_id=entry.entry_id, @@ -327,6 +330,7 @@ class StatisticsSensor(SensorEntity): def __init__( self, + hass: HomeAssistant, source_entity_id: str, name: str, unique_id: str | None, @@ -341,6 +345,10 @@ class StatisticsSensor(SensorEntity): self._attr_name: str = name self._attr_unique_id: str | None = unique_id self._source_entity_id: str = source_entity_id + self._attr_device_info = async_device_info_to_link_from_entity( + hass, + source_entity_id, + ) self.is_binary: bool = ( split_entity_id(self._source_entity_id)[0] == BINARY_SENSOR_DOMAIN ) diff --git a/tests/components/statistics/test_init.py b/tests/components/statistics/test_init.py index 6cb943c0687..64829ea7d66 100644 --- a/tests/components/statistics/test_init.py +++ b/tests/components/statistics/test_init.py @@ -2,8 +2,10 @@ from __future__ import annotations +from homeassistant.components.statistics import DOMAIN as STATISTICS_DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr, entity_registry as er from tests.common import MockConfigEntry @@ -15,3 +17,93 @@ async def test_unload_entry(hass: HomeAssistant, loaded_entry: MockConfigEntry) assert await hass.config_entries.async_unload(loaded_entry.entry_id) await hass.async_block_till_done() assert loaded_entry.state is ConfigEntryState.NOT_LOADED + + +async def test_device_cleaning( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, +) -> None: + """Test the cleaning of devices linked to the helper Statistics.""" + + # Source entity device config entry + source_config_entry = MockConfigEntry() + source_config_entry.add_to_hass(hass) + + # Device entry of the source entity + source_device1_entry = device_registry.async_get_or_create( + config_entry_id=source_config_entry.entry_id, + identifiers={("sensor", "identifier_test1")}, + connections={("mac", "30:31:32:33:34:01")}, + ) + + # Source entity registry + source_entity = entity_registry.async_get_or_create( + "sensor", + "test", + "source", + config_entry=source_config_entry, + device_id=source_device1_entry.id, + ) + await hass.async_block_till_done() + assert entity_registry.async_get("sensor.test_source") is not None + + # Configure the configuration entry for Statistics + statistics_config_entry = MockConfigEntry( + data={}, + domain=STATISTICS_DOMAIN, + options={ + "name": "Statistics", + "entity_id": "sensor.test_source", + "state_characteristic": "mean", + "keep_last_sample": False, + "percentile": 50.0, + "precision": 2.0, + "sampling_size": 20.0, + }, + title="Statistics", + ) + statistics_config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(statistics_config_entry.entry_id) + await hass.async_block_till_done() + + # Confirm the link between the source entity device and the statistics sensor + statistics_entity = entity_registry.async_get("sensor.statistics") + assert statistics_entity is not None + assert statistics_entity.device_id == source_entity.device_id + + # Device entry incorrectly linked to Statistics config entry + device_registry.async_get_or_create( + config_entry_id=statistics_config_entry.entry_id, + identifiers={("sensor", "identifier_test2")}, + connections={("mac", "30:31:32:33:34:02")}, + ) + device_registry.async_get_or_create( + config_entry_id=statistics_config_entry.entry_id, + identifiers={("sensor", "identifier_test3")}, + connections={("mac", "30:31:32:33:34:03")}, + ) + await hass.async_block_till_done() + + # Before reloading the config entry, two devices are expected to be linked + devices_before_reload = device_registry.devices.get_devices_for_config_entry_id( + statistics_config_entry.entry_id + ) + assert len(devices_before_reload) == 3 + + # Config entry reload + await hass.config_entries.async_reload(statistics_config_entry.entry_id) + await hass.async_block_till_done() + + # Confirm the link between the source entity device and the statistics sensor + statistics_entity = entity_registry.async_get("sensor.statistics") + assert statistics_entity is not None + assert statistics_entity.device_id == source_entity.device_id + + # After reloading the config entry, only one linked device is expected + devices_after_reload = device_registry.devices.get_devices_for_config_entry_id( + statistics_config_entry.entry_id + ) + assert len(devices_after_reload) == 1 + + assert devices_after_reload[0].id == source_device1_entry.id diff --git a/tests/components/statistics/test_sensor.py b/tests/components/statistics/test_sensor.py index 269c17e34b9..c90d685714c 100644 --- a/tests/components/statistics/test_sensor.py +++ b/tests/components/statistics/test_sensor.py @@ -41,7 +41,7 @@ from homeassistant.const import ( UnitOfTemperature, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers import entity_registry as er +from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util @@ -1654,3 +1654,50 @@ async def test_reload(recorder_mock: Recorder, hass: HomeAssistant) -> None: assert hass.states.get("sensor.test") is None assert hass.states.get("sensor.cputest") + + +async def test_device_id( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + device_registry: dr.DeviceRegistry, +) -> None: + """Test for source entity device for Statistics.""" + source_config_entry = MockConfigEntry() + source_config_entry.add_to_hass(hass) + source_device_entry = device_registry.async_get_or_create( + config_entry_id=source_config_entry.entry_id, + identifiers={("sensor", "identifier_test")}, + connections={("mac", "30:31:32:33:34:35")}, + ) + source_entity = entity_registry.async_get_or_create( + "sensor", + "test", + "source", + config_entry=source_config_entry, + device_id=source_device_entry.id, + ) + await hass.async_block_till_done() + assert entity_registry.async_get("sensor.test_source") is not None + + statistics_config_entry = MockConfigEntry( + data={}, + domain=STATISTICS_DOMAIN, + options={ + "name": "Statistics", + "entity_id": "sensor.test_source", + "state_characteristic": "mean", + "keep_last_sample": False, + "percentile": 50.0, + "precision": 2.0, + "sampling_size": 20.0, + }, + title="Statistics", + ) + statistics_config_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(statistics_config_entry.entry_id) + await hass.async_block_till_done() + + statistics_entity = entity_registry.async_get("sensor.statistics") + assert statistics_entity is not None + assert statistics_entity.device_id == source_entity.device_id From 3932ce57b9da11ffced4b76b7aa5c55186c628d4 Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Fri, 28 Jun 2024 19:21:59 +1000 Subject: [PATCH 1356/1445] Check Tessie scopes to fix startup bug (#120710) * Add scope check * Add tests * Bump Teslemetry --- .../components/teslemetry/manifest.json | 2 +- homeassistant/components/tessie/__init__.py | 68 +++++++++++-------- homeassistant/components/tessie/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/tessie/common.py | 11 +++ tests/components/tessie/conftest.py | 11 +++ tests/components/tessie/test_init.py | 14 +++- 8 files changed, 76 insertions(+), 36 deletions(-) diff --git a/homeassistant/components/teslemetry/manifest.json b/homeassistant/components/teslemetry/manifest.json index 2eb3e221855..49d73909a71 100644 --- a/homeassistant/components/teslemetry/manifest.json +++ b/homeassistant/components/teslemetry/manifest.json @@ -7,5 +7,5 @@ "iot_class": "cloud_polling", "loggers": ["tesla-fleet-api"], "quality_scale": "platinum", - "requirements": ["tesla-fleet-api==0.6.1"] + "requirements": ["tesla-fleet-api==0.6.2"] } diff --git a/homeassistant/components/tessie/__init__.py b/homeassistant/components/tessie/__init__.py index e8891d6665f..1d6e2a27786 100644 --- a/homeassistant/components/tessie/__init__.py +++ b/homeassistant/components/tessie/__init__.py @@ -6,6 +6,7 @@ import logging from aiohttp import ClientError, ClientResponseError from tesla_fleet_api import EnergySpecific, Tessie +from tesla_fleet_api.const import Scope from tesla_fleet_api.exceptions import TeslaFleetError from tessie_api import get_state_of_all_vehicles @@ -94,41 +95,48 @@ async def async_setup_entry(hass: HomeAssistant, entry: TessieConfigEntry) -> bo # Energy Sites tessie = Tessie(session, api_key) + energysites: list[TessieEnergyData] = [] + try: - products = (await tessie.products())["response"] + scopes = await tessie.scopes() except TeslaFleetError as e: raise ConfigEntryNotReady from e - energysites: list[TessieEnergyData] = [] - for product in products: - if "energy_site_id" in product: - site_id = product["energy_site_id"] - api = EnergySpecific(tessie.energy, site_id) - energysites.append( - TessieEnergyData( - api=api, - id=site_id, - live_coordinator=TessieEnergySiteLiveCoordinator(hass, api), - info_coordinator=TessieEnergySiteInfoCoordinator(hass, api), - device=DeviceInfo( - identifiers={(DOMAIN, str(site_id))}, - manufacturer="Tesla", - name=product.get("site_name", "Energy Site"), - ), - ) - ) + if Scope.ENERGY_DEVICE_DATA in scopes: + try: + products = (await tessie.products())["response"] + except TeslaFleetError as e: + raise ConfigEntryNotReady from e - # Populate coordinator data before forwarding to platforms - await asyncio.gather( - *( - energysite.live_coordinator.async_config_entry_first_refresh() - for energysite in energysites - ), - *( - energysite.info_coordinator.async_config_entry_first_refresh() - for energysite in energysites - ), - ) + for product in products: + if "energy_site_id" in product: + site_id = product["energy_site_id"] + api = EnergySpecific(tessie.energy, site_id) + energysites.append( + TessieEnergyData( + api=api, + id=site_id, + live_coordinator=TessieEnergySiteLiveCoordinator(hass, api), + info_coordinator=TessieEnergySiteInfoCoordinator(hass, api), + device=DeviceInfo( + identifiers={(DOMAIN, str(site_id))}, + manufacturer="Tesla", + name=product.get("site_name", "Energy Site"), + ), + ) + ) + + # Populate coordinator data before forwarding to platforms + await asyncio.gather( + *( + energysite.live_coordinator.async_config_entry_first_refresh() + for energysite in energysites + ), + *( + energysite.info_coordinator.async_config_entry_first_refresh() + for energysite in energysites + ), + ) entry.runtime_data = TessieData(vehicles, energysites) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) diff --git a/homeassistant/components/tessie/manifest.json b/homeassistant/components/tessie/manifest.json index bf1ab5f61e4..493feeaa77e 100644 --- a/homeassistant/components/tessie/manifest.json +++ b/homeassistant/components/tessie/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/tessie", "iot_class": "cloud_polling", "loggers": ["tessie"], - "requirements": ["tessie-api==0.0.9", "tesla-fleet-api==0.6.1"] + "requirements": ["tessie-api==0.0.9", "tesla-fleet-api==0.6.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index cf0e22167a0..ba007cb230f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2711,7 +2711,7 @@ temperusb==1.6.1 # homeassistant.components.teslemetry # homeassistant.components.tessie -tesla-fleet-api==0.6.1 +tesla-fleet-api==0.6.2 # homeassistant.components.powerwall tesla-powerwall==0.5.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f7254727d3c..05397f24983 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2109,7 +2109,7 @@ temperusb==1.6.1 # homeassistant.components.teslemetry # homeassistant.components.tessie -tesla-fleet-api==0.6.1 +tesla-fleet-api==0.6.2 # homeassistant.components.powerwall tesla-powerwall==0.5.2 diff --git a/tests/components/tessie/common.py b/tests/components/tessie/common.py index 3d24c6b233a..37a38fffaa4 100644 --- a/tests/components/tessie/common.py +++ b/tests/components/tessie/common.py @@ -54,6 +54,17 @@ LIVE_STATUS = load_json_object_fixture("live_status.json", DOMAIN) SITE_INFO = load_json_object_fixture("site_info.json", DOMAIN) RESPONSE_OK = {"response": {}, "error": None} COMMAND_OK = {"response": {"result": True, "reason": ""}} +SCOPES = [ + "user_data", + "vehicle_device_data", + "vehicle_cmds", + "vehicle_charging_cmds", + "energy_device_data", + "energy_cmds", + "offline_access", + "openid", +] +NO_SCOPES = ["user_data", "offline_access", "openid"] async def setup_platform( diff --git a/tests/components/tessie/conftest.py b/tests/components/tessie/conftest.py index 79cc9aa44c6..e0aba73af17 100644 --- a/tests/components/tessie/conftest.py +++ b/tests/components/tessie/conftest.py @@ -11,6 +11,7 @@ from .common import ( COMMAND_OK, LIVE_STATUS, PRODUCTS, + SCOPES, SITE_INFO, TEST_STATE_OF_ALL_VEHICLES, TEST_VEHICLE_STATE_ONLINE, @@ -51,6 +52,16 @@ def mock_get_state_of_all_vehicles(): # Fleet API +@pytest.fixture(autouse=True) +def mock_scopes(): + """Mock scopes function.""" + with patch( + "homeassistant.components.tessie.Tessie.scopes", + return_value=SCOPES, + ) as mock_scopes: + yield mock_scopes + + @pytest.fixture(autouse=True) def mock_products(): """Mock Tesla Fleet Api products method.""" diff --git a/tests/components/tessie/test_init.py b/tests/components/tessie/test_init.py index e37512ea8c4..921ef93b1ae 100644 --- a/tests/components/tessie/test_init.py +++ b/tests/components/tessie/test_init.py @@ -50,11 +50,21 @@ async def test_connection_failure( assert entry.state is ConfigEntryState.SETUP_RETRY -async def test_fleet_error(hass: HomeAssistant) -> None: - """Test init with a fleet error.""" +async def test_products_error(hass: HomeAssistant) -> None: + """Test init with a fleet error on products.""" with patch( "homeassistant.components.tessie.Tessie.products", side_effect=TeslaFleetError ): entry = await setup_platform(hass) assert entry.state is ConfigEntryState.SETUP_RETRY + + +async def test_scopes_error(hass: HomeAssistant) -> None: + """Test init with a fleet error on scopes.""" + + with patch( + "homeassistant.components.tessie.Tessie.scopes", side_effect=TeslaFleetError + ): + entry = await setup_platform(hass) + assert entry.state is ConfigEntryState.SETUP_RETRY From 76780ca04eaf4c23ed1029d9c0acf2b1c9d2e37f Mon Sep 17 00:00:00 2001 From: Dave Leaver Date: Fri, 28 Jun 2024 19:44:54 +1200 Subject: [PATCH 1357/1445] Bump airtouch5py to 1.2.0 (#120715) * Bump airtouch5py to fix console 1.2.0 * Bump airtouch5py again --- homeassistant/components/airtouch5/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/airtouch5/manifest.json b/homeassistant/components/airtouch5/manifest.json index 0d4cbc32761..312a627d0e8 100644 --- a/homeassistant/components/airtouch5/manifest.json +++ b/homeassistant/components/airtouch5/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/airtouch5", "iot_class": "local_push", "loggers": ["airtouch5py"], - "requirements": ["airtouch5py==0.2.8"] + "requirements": ["airtouch5py==0.2.10"] } diff --git a/requirements_all.txt b/requirements_all.txt index ba007cb230f..788a1ff1be7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -428,7 +428,7 @@ airthings-cloud==0.2.0 airtouch4pyapi==1.0.5 # homeassistant.components.airtouch5 -airtouch5py==0.2.8 +airtouch5py==0.2.10 # homeassistant.components.alpha_vantage alpha-vantage==2.3.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 05397f24983..d41bfa7f997 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -401,7 +401,7 @@ airthings-cloud==0.2.0 airtouch4pyapi==1.0.5 # homeassistant.components.airtouch5 -airtouch5py==0.2.8 +airtouch5py==0.2.10 # homeassistant.components.amberelectric amberelectric==1.1.0 From 0ae11b033576ae41303f9ad68a8e29827a491084 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 28 Jun 2024 11:03:01 +0200 Subject: [PATCH 1358/1445] Bump renault-api to 0.2.4 (#120727) --- homeassistant/components/renault/binary_sensor.py | 2 +- homeassistant/components/renault/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/renault/fixtures/hvac_status.1.json | 2 +- tests/components/renault/fixtures/hvac_status.2.json | 2 +- tests/components/renault/snapshots/test_diagnostics.ambr | 4 ++-- 7 files changed, 8 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/renault/binary_sensor.py b/homeassistant/components/renault/binary_sensor.py index 7ebc77b8e77..2041499b711 100644 --- a/homeassistant/components/renault/binary_sensor.py +++ b/homeassistant/components/renault/binary_sensor.py @@ -81,7 +81,7 @@ BINARY_SENSOR_TYPES: tuple[RenaultBinarySensorEntityDescription, ...] = tuple( key="hvac_status", coordinator="hvac_status", on_key="hvacStatus", - on_value=2, + on_value="on", translation_key="hvac_status", ), RenaultBinarySensorEntityDescription( diff --git a/homeassistant/components/renault/manifest.json b/homeassistant/components/renault/manifest.json index 8407893011c..ffa1cd6acef 100644 --- a/homeassistant/components/renault/manifest.json +++ b/homeassistant/components/renault/manifest.json @@ -8,5 +8,5 @@ "iot_class": "cloud_polling", "loggers": ["renault_api"], "quality_scale": "platinum", - "requirements": ["renault-api==0.2.3"] + "requirements": ["renault-api==0.2.4"] } diff --git a/requirements_all.txt b/requirements_all.txt index 788a1ff1be7..e6ce960ebfc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2454,7 +2454,7 @@ refoss-ha==1.2.1 regenmaschine==2024.03.0 # homeassistant.components.renault -renault-api==0.2.3 +renault-api==0.2.4 # homeassistant.components.renson renson-endura-delta==1.7.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d41bfa7f997..37c5e47c5fa 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1918,7 +1918,7 @@ refoss-ha==1.2.1 regenmaschine==2024.03.0 # homeassistant.components.renault -renault-api==0.2.3 +renault-api==0.2.4 # homeassistant.components.renson renson-endura-delta==1.7.1 diff --git a/tests/components/renault/fixtures/hvac_status.1.json b/tests/components/renault/fixtures/hvac_status.1.json index 7cbd7a9fe37..f48cbae68ae 100644 --- a/tests/components/renault/fixtures/hvac_status.1.json +++ b/tests/components/renault/fixtures/hvac_status.1.json @@ -2,6 +2,6 @@ "data": { "type": "Car", "id": "VF1AAAAA555777999", - "attributes": { "externalTemperature": 8.0, "hvacStatus": 1 } + "attributes": { "externalTemperature": 8.0, "hvacStatus": "off" } } } diff --git a/tests/components/renault/fixtures/hvac_status.2.json b/tests/components/renault/fixtures/hvac_status.2.json index 8bb4f941e06..a2ca08a71e9 100644 --- a/tests/components/renault/fixtures/hvac_status.2.json +++ b/tests/components/renault/fixtures/hvac_status.2.json @@ -4,7 +4,7 @@ "id": "VF1AAAAA555777999", "attributes": { "socThreshold": 30.0, - "hvacStatus": 1, + "hvacStatus": "off", "lastUpdateTime": "2020-12-03T00:00:00Z" } } diff --git a/tests/components/renault/snapshots/test_diagnostics.ambr b/tests/components/renault/snapshots/test_diagnostics.ambr index ae90115fcb6..a2921dff35e 100644 --- a/tests/components/renault/snapshots/test_diagnostics.ambr +++ b/tests/components/renault/snapshots/test_diagnostics.ambr @@ -22,7 +22,7 @@ }), 'hvac_status': dict({ 'externalTemperature': 8.0, - 'hvacStatus': 1, + 'hvacStatus': 'off', }), 'res_state': dict({ }), @@ -227,7 +227,7 @@ }), 'hvac_status': dict({ 'externalTemperature': 8.0, - 'hvacStatus': 1, + 'hvacStatus': 'off', }), 'res_state': dict({ }), From fe8b5656dd166bf25f4f3504140f6baf6619daef Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 28 Jun 2024 13:26:31 +0200 Subject: [PATCH 1359/1445] Separate renault strings (#120737) --- homeassistant/components/renault/strings.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/renault/strings.json b/homeassistant/components/renault/strings.json index 322f7a207d7..5217b4ff65a 100644 --- a/homeassistant/components/renault/strings.json +++ b/homeassistant/components/renault/strings.json @@ -73,9 +73,9 @@ "charge_mode": { "name": "Charge mode", "state": { - "always": "Instant", - "always_charging": "[%key:component::renault::entity::select::charge_mode::state::always%]", - "schedule_mode": "Planner", + "always": "Always", + "always_charging": "Always charging", + "schedule_mode": "Schedule mode", "scheduled": "Scheduled" } } From c5fa9ad2729f83eef666e58a21f6a7db9721df88 Mon Sep 17 00:00:00 2001 From: Illia <146177275+ikalnyi@users.noreply.github.com> Date: Fri, 28 Jun 2024 12:14:44 +0200 Subject: [PATCH 1360/1445] Bump asyncarve to 0.1.1 (#120740) --- homeassistant/components/arve/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/arve/manifest.json b/homeassistant/components/arve/manifest.json index fa33b3309ce..4c63d377371 100644 --- a/homeassistant/components/arve/manifest.json +++ b/homeassistant/components/arve/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/arve", "iot_class": "cloud_polling", - "requirements": ["asyncarve==0.0.9"] + "requirements": ["asyncarve==0.1.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index e6ce960ebfc..34eb86f9ddc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -493,7 +493,7 @@ asterisk_mbox==0.5.0 async-upnp-client==0.39.0 # homeassistant.components.arve -asyncarve==0.0.9 +asyncarve==0.1.1 # homeassistant.components.keyboard_remote asyncinotify==4.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 37c5e47c5fa..c08fe273b8e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -448,7 +448,7 @@ asterisk_mbox==0.5.0 async-upnp-client==0.39.0 # homeassistant.components.arve -asyncarve==0.0.9 +asyncarve==0.1.1 # homeassistant.components.sleepiq asyncsleepiq==1.5.2 From cada78496b048e33f9843ef72f43a76c30772494 Mon Sep 17 00:00:00 2001 From: tronikos Date: Fri, 28 Jun 2024 04:25:55 -0700 Subject: [PATCH 1361/1445] Fix Google Generative AI: 400 Request contains an invalid argument (#120741) --- .../conversation.py | 9 +- .../snapshots/test_conversation.ambr | 166 ++++++++++++++++++ .../test_conversation.py | 83 +++++++++ 3 files changed, 255 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/google_generative_ai_conversation/conversation.py b/homeassistant/components/google_generative_ai_conversation/conversation.py index fb7f5c3b21c..8052ee66f40 100644 --- a/homeassistant/components/google_generative_ai_conversation/conversation.py +++ b/homeassistant/components/google_generative_ai_conversation/conversation.py @@ -95,9 +95,12 @@ def _format_tool( ) -> dict[str, Any]: """Format tool specification.""" - parameters = _format_schema( - convert(tool.parameters, custom_serializer=custom_serializer) - ) + if tool.parameters.schema: + parameters = _format_schema( + convert(tool.parameters, custom_serializer=custom_serializer) + ) + else: + parameters = None return protos.Tool( { diff --git a/tests/components/google_generative_ai_conversation/snapshots/test_conversation.ambr b/tests/components/google_generative_ai_conversation/snapshots/test_conversation.ambr index b0a0ce967de..7f28c172970 100644 --- a/tests/components/google_generative_ai_conversation/snapshots/test_conversation.ambr +++ b/tests/components/google_generative_ai_conversation/snapshots/test_conversation.ambr @@ -409,3 +409,169 @@ ), ]) # --- +# name: test_function_call + list([ + tuple( + '', + tuple( + ), + dict({ + 'generation_config': dict({ + 'max_output_tokens': 150, + 'temperature': 1.0, + 'top_k': 64, + 'top_p': 0.95, + }), + 'model_name': 'models/gemini-1.5-flash-latest', + 'safety_settings': dict({ + 'DANGEROUS': 'BLOCK_MEDIUM_AND_ABOVE', + 'HARASSMENT': 'BLOCK_MEDIUM_AND_ABOVE', + 'HATE': 'BLOCK_MEDIUM_AND_ABOVE', + 'SEXUAL': 'BLOCK_MEDIUM_AND_ABOVE', + }), + 'system_instruction': ''' + Current time is 05:00:00. Today's date is 2024-05-24. + You are a voice assistant for Home Assistant. + Answer questions about the world truthfully. + Answer in plain text. Keep it simple and to the point. + Only if the user wants to control a device, tell them to expose entities to their voice assistant in Home Assistant. + ''', + 'tools': list([ + function_declarations { + name: "test_tool" + description: "Test function" + parameters { + type_: OBJECT + properties { + key: "param1" + value { + type_: ARRAY + description: "Test parameters" + items { + type_: STRING + format_: "lower" + } + } + } + } + } + , + ]), + }), + ), + tuple( + '().start_chat', + tuple( + ), + dict({ + 'history': list([ + ]), + }), + ), + tuple( + '().start_chat().send_message_async', + tuple( + 'Please call the test function', + ), + dict({ + }), + ), + tuple( + '().start_chat().send_message_async', + tuple( + parts { + function_response { + name: "test_tool" + response { + fields { + key: "result" + value { + string_value: "Test response" + } + } + } + } + } + , + ), + dict({ + }), + ), + ]) +# --- +# name: test_function_call_without_parameters + list([ + tuple( + '', + tuple( + ), + dict({ + 'generation_config': dict({ + 'max_output_tokens': 150, + 'temperature': 1.0, + 'top_k': 64, + 'top_p': 0.95, + }), + 'model_name': 'models/gemini-1.5-flash-latest', + 'safety_settings': dict({ + 'DANGEROUS': 'BLOCK_MEDIUM_AND_ABOVE', + 'HARASSMENT': 'BLOCK_MEDIUM_AND_ABOVE', + 'HATE': 'BLOCK_MEDIUM_AND_ABOVE', + 'SEXUAL': 'BLOCK_MEDIUM_AND_ABOVE', + }), + 'system_instruction': ''' + Current time is 05:00:00. Today's date is 2024-05-24. + You are a voice assistant for Home Assistant. + Answer questions about the world truthfully. + Answer in plain text. Keep it simple and to the point. + Only if the user wants to control a device, tell them to expose entities to their voice assistant in Home Assistant. + ''', + 'tools': list([ + function_declarations { + name: "test_tool" + description: "Test function" + } + , + ]), + }), + ), + tuple( + '().start_chat', + tuple( + ), + dict({ + 'history': list([ + ]), + }), + ), + tuple( + '().start_chat().send_message_async', + tuple( + 'Please call the test function', + ), + dict({ + }), + ), + tuple( + '().start_chat().send_message_async', + tuple( + parts { + function_response { + name: "test_tool" + response { + fields { + key: "result" + value { + string_value: "Test response" + } + } + } + } + } + , + ), + dict({ + }), + ), + ]) +# --- diff --git a/tests/components/google_generative_ai_conversation/test_conversation.py b/tests/components/google_generative_ai_conversation/test_conversation.py index 990058aa89d..30016335f3b 100644 --- a/tests/components/google_generative_ai_conversation/test_conversation.py +++ b/tests/components/google_generative_ai_conversation/test_conversation.py @@ -172,6 +172,7 @@ async def test_function_call( hass: HomeAssistant, mock_config_entry_with_assist: MockConfigEntry, mock_init_component, + snapshot: SnapshotAssertion, ) -> None: """Test function calling.""" agent_id = mock_config_entry_with_assist.entry_id @@ -256,6 +257,7 @@ async def test_function_call( device_id="test_device", ), ) + assert [tuple(mock_call) for mock_call in mock_model.mock_calls] == snapshot # Test conversating tracing traces = trace.async_get_traces() @@ -272,6 +274,87 @@ async def test_function_call( assert "Answer in plain text" in detail_event["data"]["prompt"] +@patch( + "homeassistant.components.google_generative_ai_conversation.conversation.llm.AssistAPI._async_get_tools" +) +async def test_function_call_without_parameters( + mock_get_tools, + hass: HomeAssistant, + mock_config_entry_with_assist: MockConfigEntry, + mock_init_component, + snapshot: SnapshotAssertion, +) -> None: + """Test function calling without parameters.""" + agent_id = mock_config_entry_with_assist.entry_id + context = Context() + + mock_tool = AsyncMock() + mock_tool.name = "test_tool" + mock_tool.description = "Test function" + mock_tool.parameters = vol.Schema({}) + + mock_get_tools.return_value = [mock_tool] + + with patch("google.generativeai.GenerativeModel") as mock_model: + mock_chat = AsyncMock() + mock_model.return_value.start_chat.return_value = mock_chat + chat_response = MagicMock() + mock_chat.send_message_async.return_value = chat_response + mock_part = MagicMock() + mock_part.function_call = FunctionCall(name="test_tool", args={}) + + def tool_call(hass, tool_input, tool_context): + mock_part.function_call = None + mock_part.text = "Hi there!" + return {"result": "Test response"} + + mock_tool.async_call.side_effect = tool_call + chat_response.parts = [mock_part] + result = await conversation.async_converse( + hass, + "Please call the test function", + None, + context, + agent_id=agent_id, + device_id="test_device", + ) + + assert result.response.response_type == intent.IntentResponseType.ACTION_DONE + assert result.response.as_dict()["speech"]["plain"]["speech"] == "Hi there!" + mock_tool_call = mock_chat.send_message_async.mock_calls[1][1][0] + mock_tool_call = type(mock_tool_call).to_dict(mock_tool_call) + assert mock_tool_call == { + "parts": [ + { + "function_response": { + "name": "test_tool", + "response": { + "result": "Test response", + }, + }, + }, + ], + "role": "", + } + + mock_tool.async_call.assert_awaited_once_with( + hass, + llm.ToolInput( + tool_name="test_tool", + tool_args={}, + ), + llm.LLMContext( + platform="google_generative_ai_conversation", + context=context, + user_prompt="Please call the test function", + language="en", + assistant="conversation", + device_id="test_device", + ), + ) + assert [tuple(mock_call) for mock_call in mock_model.mock_calls] == snapshot + + @patch( "homeassistant.components.google_generative_ai_conversation.conversation.llm.AssistAPI._async_get_tools" ) From d7a59748cf7d80b0922a7e24578a42b5ade63504 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 28 Jun 2024 13:38:24 +0200 Subject: [PATCH 1362/1445] Bump version to 2024.7.0b4 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 33a86f57a5e..1ab2a3f6893 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -24,7 +24,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 7 -PATCH_VERSION: Final = "0b3" +PATCH_VERSION: Final = "0b4" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0) diff --git a/pyproject.toml b/pyproject.toml index e4ccd9898e0..e96c329fd5a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.7.0b3" +version = "2024.7.0b4" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 2d5961fa4f606e15422a4d0a8fe1c9fd00d3b105 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sat, 29 Jun 2024 08:45:51 -0700 Subject: [PATCH 1363/1445] Bump gcal_sync to 6.1.3 (#120278) --- homeassistant/components/google/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/google/test_calendar.py | 12 ++++++++++++ 4 files changed, 15 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/google/manifest.json b/homeassistant/components/google/manifest.json index 062bf58d2f5..5fc28d2f398 100644 --- a/homeassistant/components/google/manifest.json +++ b/homeassistant/components/google/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/calendar.google", "iot_class": "cloud_polling", "loggers": ["googleapiclient"], - "requirements": ["gcal-sync==6.0.4", "oauth2client==4.1.3", "ical==8.0.1"] + "requirements": ["gcal-sync==6.1.3", "oauth2client==4.1.3", "ical==8.0.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 34eb86f9ddc..f351dc563ed 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -924,7 +924,7 @@ gardena-bluetooth==1.4.2 gassist-text==0.0.11 # homeassistant.components.google -gcal-sync==6.0.4 +gcal-sync==6.1.3 # homeassistant.components.geniushub geniushub-client==0.7.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c08fe273b8e..a29f8c7d5cf 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -762,7 +762,7 @@ gardena-bluetooth==1.4.2 gassist-text==0.0.11 # homeassistant.components.google -gcal-sync==6.0.4 +gcal-sync==6.1.3 # homeassistant.components.geocaching geocachingapi==0.2.1 diff --git a/tests/components/google/test_calendar.py b/tests/components/google/test_calendar.py index 8e934925f46..5fe26585fe5 100644 --- a/tests/components/google/test_calendar.py +++ b/tests/components/google/test_calendar.py @@ -385,6 +385,9 @@ async def test_update_error( with patch("homeassistant.util.utcnow", return_value=now): async_fire_time_changed(hass, now) await hass.async_block_till_done() + # Ensure coordinator update completes + await hass.async_block_till_done() + await hass.async_block_till_done() # Entity is marked uanvailable due to API failure state = hass.states.get(TEST_ENTITY) @@ -414,6 +417,9 @@ async def test_update_error( with patch("homeassistant.util.utcnow", return_value=now): async_fire_time_changed(hass, now) await hass.async_block_till_done() + # Ensure coordinator update completes + await hass.async_block_till_done() + await hass.async_block_till_done() # State updated with new API response state = hass.states.get(TEST_ENTITY) @@ -606,6 +612,9 @@ async def test_future_event_update_behavior( freezer.move_to(now) async_fire_time_changed(hass, now) await hass.async_block_till_done() + # Ensure coordinator update completes + await hass.async_block_till_done() + await hass.async_block_till_done() # Event has started state = hass.states.get(TEST_ENTITY) @@ -643,6 +652,9 @@ async def test_future_event_offset_update_behavior( freezer.move_to(now) async_fire_time_changed(hass, now) await hass.async_block_till_done() + # Ensure coordinator update completes + await hass.async_block_till_done() + await hass.async_block_till_done() # Event has not started, but the offset was reached state = hass.states.get(TEST_ENTITY) From 5fd589053aa17b390d138a19cb9c38d95e2397d9 Mon Sep 17 00:00:00 2001 From: wittypluck Date: Fri, 28 Jun 2024 22:47:20 +0200 Subject: [PATCH 1364/1445] Reject small uptime updates for Unifi clients (#120398) Extend logic to reject small uptime updates to Unifi clients + add unit tests --- homeassistant/components/unifi/sensor.py | 5 ++-- tests/components/unifi/test_sensor.py | 36 ++++++++++++++++++------ 2 files changed, 31 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/unifi/sensor.py b/homeassistant/components/unifi/sensor.py index 028d70d8880..071230a9652 100644 --- a/homeassistant/components/unifi/sensor.py +++ b/homeassistant/components/unifi/sensor.py @@ -139,7 +139,7 @@ def async_device_uptime_value_fn(hub: UnifiHub, device: Device) -> datetime | No @callback -def async_device_uptime_value_changed_fn( +def async_uptime_value_changed_fn( old: StateType | date | datetime | Decimal, new: datetime | float | str | None ) -> bool: """Reject the new uptime value if it's too similar to the old one. Avoids unwanted fluctuation.""" @@ -310,6 +310,7 @@ ENTITY_DESCRIPTIONS: tuple[UnifiSensorEntityDescription, ...] = ( supported_fn=lambda hub, _: hub.config.option_allow_uptime_sensors, unique_id_fn=lambda hub, obj_id: f"uptime-{obj_id}", value_fn=async_client_uptime_value_fn, + value_changed_fn=async_uptime_value_changed_fn, ), UnifiSensorEntityDescription[Wlans, Wlan]( key="WLAN clients", @@ -396,7 +397,7 @@ ENTITY_DESCRIPTIONS: tuple[UnifiSensorEntityDescription, ...] = ( object_fn=lambda api, obj_id: api.devices[obj_id], unique_id_fn=lambda hub, obj_id: f"device_uptime-{obj_id}", value_fn=async_device_uptime_value_fn, - value_changed_fn=async_device_uptime_value_changed_fn, + value_changed_fn=async_uptime_value_changed_fn, ), UnifiSensorEntityDescription[Devices, Device]( key="Device temperature", diff --git a/tests/components/unifi/test_sensor.py b/tests/components/unifi/test_sensor.py index 960a5d3e529..48e524aef76 100644 --- a/tests/components/unifi/test_sensor.py +++ b/tests/components/unifi/test_sensor.py @@ -484,12 +484,12 @@ async def test_bandwidth_sensors( ], ) @pytest.mark.parametrize( - ("initial_uptime", "event_uptime", "new_uptime"), + ("initial_uptime", "event_uptime", "small_variation_uptime", "new_uptime"), [ # Uptime listed in epoch time should never change - (1609462800, 1609462800, 1612141200), + (1609462800, 1609462800, 1609462800, 1612141200), # Uptime counted in seconds increases with every event - (60, 64, 60), + (60, 240, 480, 60), ], ) @pytest.mark.usefixtures("entity_registry_enabled_by_default") @@ -503,6 +503,7 @@ async def test_uptime_sensors( client_payload: list[dict[str, Any]], initial_uptime, event_uptime, + small_variation_uptime, new_uptime, ) -> None: """Verify that uptime sensors are working as expected.""" @@ -519,15 +520,24 @@ async def test_uptime_sensors( ) # Verify normal new event doesn't change uptime - # 4 seconds has passed + # 4 minutes have passed uptime_client["uptime"] = event_uptime - now = datetime(2021, 1, 1, 1, 1, 4, tzinfo=dt_util.UTC) + now = datetime(2021, 1, 1, 1, 4, 0, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.now", return_value=now): mock_websocket_message(message=MessageKey.CLIENT, data=uptime_client) await hass.async_block_till_done() assert hass.states.get("sensor.client1_uptime").state == "2021-01-01T01:00:00+00:00" + # Verify small variation of uptime (<120 seconds) is ignored + # 15 seconds variation after 8 minutes + uptime_client["uptime"] = small_variation_uptime + now = datetime(2021, 1, 1, 1, 8, 15, tzinfo=dt_util.UTC) + with patch("homeassistant.util.dt.now", return_value=now): + mock_websocket_message(message=MessageKey.CLIENT, data=uptime_client) + + assert hass.states.get("sensor.client1_uptime").state == "2021-01-01T01:00:00+00:00" + # Verify new event change uptime # 1 month has passed uptime_client["uptime"] = new_uptime @@ -911,10 +921,20 @@ async def test_device_uptime( ) # Verify normal new event doesn't change uptime - # 4 seconds has passed + # 4 minutes have passed device = device_payload[0] - device["uptime"] = 64 - now = datetime(2021, 1, 1, 1, 1, 4, tzinfo=dt_util.UTC) + device["uptime"] = 240 + now = datetime(2021, 1, 1, 1, 4, 0, tzinfo=dt_util.UTC) + with patch("homeassistant.util.dt.now", return_value=now): + mock_websocket_message(message=MessageKey.DEVICE, data=device) + + assert hass.states.get("sensor.device_uptime").state == "2021-01-01T01:00:00+00:00" + + # Verify small variation of uptime (<120 seconds) is ignored + # 15 seconds variation after 8 minutes + device = device_payload[0] + device["uptime"] = 480 + now = datetime(2021, 1, 1, 1, 8, 15, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.now", return_value=now): mock_websocket_message(message=MessageKey.DEVICE, data=device) From b350ba9657c88f7edf3b27786de8dbcee1ab7371 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexey=20ALERT=20Rubash=D1=91ff?= Date: Fri, 28 Jun 2024 11:45:27 +0300 Subject: [PATCH 1365/1445] Add electrical consumption sensor to Overkiz (#120717) electrical consumption sensor --- homeassistant/components/overkiz/sensor.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/homeassistant/components/overkiz/sensor.py b/homeassistant/components/overkiz/sensor.py index c62840eea97..d313faf0c1d 100644 --- a/homeassistant/components/overkiz/sensor.py +++ b/homeassistant/components/overkiz/sensor.py @@ -182,6 +182,13 @@ SENSOR_DESCRIPTIONS: list[OverkizSensorDescription] = [ native_unit_of_measurement=UnitOfPower.WATT, state_class=SensorStateClass.MEASUREMENT, ), + OverkizSensorDescription( + key=OverkizState.MODBUSLINK_POWER_HEAT_ELECTRICAL, + name="Electric power consumption", + device_class=SensorDeviceClass.POWER, + native_unit_of_measurement=UnitOfPower.WATT, + state_class=SensorStateClass.MEASUREMENT, + ), OverkizSensorDescription( key=OverkizState.CORE_CONSUMPTION_TARIFF1, name="Consumption tariff 1", From 8994ab1686f729e83cc45b44182da5c1564446de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexey=20ALERT=20Rubash=D1=91ff?= Date: Fri, 28 Jun 2024 11:53:07 +0300 Subject: [PATCH 1366/1445] Add warm water remaining volume sensor to Overkiz (#120718) * warm water remaining volume sensor * Update homeassistant/components/overkiz/sensor.py Co-authored-by: Dave T <17680170+davet2001@users.noreply.github.com> --------- Co-authored-by: Dave T <17680170+davet2001@users.noreply.github.com> --- homeassistant/components/overkiz/sensor.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/homeassistant/components/overkiz/sensor.py b/homeassistant/components/overkiz/sensor.py index d313faf0c1d..bf9608358eb 100644 --- a/homeassistant/components/overkiz/sensor.py +++ b/homeassistant/components/overkiz/sensor.py @@ -420,6 +420,13 @@ SENSOR_DESCRIPTIONS: list[OverkizSensorDescription] = [ state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfTemperature.CELSIUS, ), + OverkizSensorDescription( + key=OverkizState.CORE_REMAINING_HOT_WATER, + name="Warm water remaining", + device_class=SensorDeviceClass.VOLUME, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfVolume.LITERS, + ), # Cover OverkizSensorDescription( key=OverkizState.CORE_TARGET_CLOSURE, From f57c9429019bde0b16fa675143deea7f54201642 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 28 Jun 2024 14:53:29 +0200 Subject: [PATCH 1367/1445] Bump sense-energy to 0.12.4 (#120744) * Bump sense-energy to 0.12.4 * Fix --- homeassistant/components/emulated_kasa/manifest.json | 2 +- homeassistant/components/sense/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/emulated_kasa/manifest.json b/homeassistant/components/emulated_kasa/manifest.json index 843aeddde7b..640a2113d6f 100644 --- a/homeassistant/components/emulated_kasa/manifest.json +++ b/homeassistant/components/emulated_kasa/manifest.json @@ -6,5 +6,5 @@ "iot_class": "local_push", "loggers": ["sense_energy"], "quality_scale": "internal", - "requirements": ["sense-energy==0.12.2"] + "requirements": ["sense-energy==0.12.4"] } diff --git a/homeassistant/components/sense/manifest.json b/homeassistant/components/sense/manifest.json index 7ef1caefe48..116b714ba82 100644 --- a/homeassistant/components/sense/manifest.json +++ b/homeassistant/components/sense/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/sense", "iot_class": "cloud_polling", "loggers": ["sense_energy"], - "requirements": ["sense-energy==0.12.2"] + "requirements": ["sense-energy==0.12.4"] } diff --git a/requirements_all.txt b/requirements_all.txt index f351dc563ed..b26425b30bc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2539,7 +2539,7 @@ sendgrid==6.8.2 # homeassistant.components.emulated_kasa # homeassistant.components.sense -sense-energy==0.12.2 +sense-energy==0.12.4 # homeassistant.components.sensirion_ble sensirion-ble==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a29f8c7d5cf..e95c0b7d600 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1976,7 +1976,7 @@ securetar==2024.2.1 # homeassistant.components.emulated_kasa # homeassistant.components.sense -sense-energy==0.12.2 +sense-energy==0.12.4 # homeassistant.components.sensirion_ble sensirion-ble==0.1.1 From 20ac0aa7b116683adcaabd8be8c72c1dd097fae3 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 28 Jun 2024 16:21:32 +0200 Subject: [PATCH 1368/1445] Bump govee-local-api to 1.5.1 (#120747) --- homeassistant/components/govee_light_local/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/govee_light_local/manifest.json b/homeassistant/components/govee_light_local/manifest.json index 93a19408182..168a13e2477 100644 --- a/homeassistant/components/govee_light_local/manifest.json +++ b/homeassistant/components/govee_light_local/manifest.json @@ -6,5 +6,5 @@ "dependencies": ["network"], "documentation": "https://www.home-assistant.io/integrations/govee_light_local", "iot_class": "local_push", - "requirements": ["govee-local-api==1.5.0"] + "requirements": ["govee-local-api==1.5.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index b26425b30bc..c2bcb3ed65e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -995,7 +995,7 @@ gotailwind==0.2.3 govee-ble==0.31.3 # homeassistant.components.govee_light_local -govee-local-api==1.5.0 +govee-local-api==1.5.1 # homeassistant.components.remote_rpi_gpio gpiozero==1.6.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e95c0b7d600..7197bc17a04 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -821,7 +821,7 @@ gotailwind==0.2.3 govee-ble==0.31.3 # homeassistant.components.govee_light_local -govee-local-api==1.5.0 +govee-local-api==1.5.1 # homeassistant.components.gpsd gps3==0.33.3 From 83df47030738164f1454c5ec731444bf9f1ca911 Mon Sep 17 00:00:00 2001 From: Klaas Schoute Date: Fri, 28 Jun 2024 16:14:31 +0200 Subject: [PATCH 1369/1445] Bump easyenergy lib to v2.1.2 (#120753) --- homeassistant/components/easyenergy/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/easyenergy/manifest.json b/homeassistant/components/easyenergy/manifest.json index 4dcce0fd705..4d45dc2d399 100644 --- a/homeassistant/components/easyenergy/manifest.json +++ b/homeassistant/components/easyenergy/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/easyenergy", "iot_class": "cloud_polling", "quality_scale": "platinum", - "requirements": ["easyenergy==2.1.1"] + "requirements": ["easyenergy==2.1.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index c2bcb3ed65e..a98be6b1a9f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -771,7 +771,7 @@ dynalite-panel==0.0.4 eagle100==0.1.1 # homeassistant.components.easyenergy -easyenergy==2.1.1 +easyenergy==2.1.2 # homeassistant.components.ebusd ebusdpy==0.0.17 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7197bc17a04..834c6c5bf99 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -643,7 +643,7 @@ dynalite-panel==0.0.4 eagle100==0.1.1 # homeassistant.components.easyenergy -easyenergy==2.1.1 +easyenergy==2.1.2 # homeassistant.components.electric_kiwi electrickiwi-api==0.8.5 From 6028e5b77add83c795e1be3c61271bd296da5711 Mon Sep 17 00:00:00 2001 From: Klaas Schoute Date: Fri, 28 Jun 2024 16:20:44 +0200 Subject: [PATCH 1370/1445] Bump p1monitor lib to v3.0.1 (#120756) --- homeassistant/components/p1_monitor/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/p1_monitor/manifest.json b/homeassistant/components/p1_monitor/manifest.json index 0dfe1f3a46c..4702de3546d 100644 --- a/homeassistant/components/p1_monitor/manifest.json +++ b/homeassistant/components/p1_monitor/manifest.json @@ -7,5 +7,5 @@ "iot_class": "local_polling", "loggers": ["p1monitor"], "quality_scale": "platinum", - "requirements": ["p1monitor==3.0.0"] + "requirements": ["p1monitor==3.0.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index a98be6b1a9f..3ce72af693b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1519,7 +1519,7 @@ ourgroceries==1.5.4 ovoenergy==2.0.0 # homeassistant.components.p1_monitor -p1monitor==3.0.0 +p1monitor==3.0.1 # homeassistant.components.mqtt paho-mqtt==1.6.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 834c6c5bf99..f87edeff701 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1222,7 +1222,7 @@ ourgroceries==1.5.4 ovoenergy==2.0.0 # homeassistant.components.p1_monitor -p1monitor==3.0.0 +p1monitor==3.0.1 # homeassistant.components.mqtt paho-mqtt==1.6.1 From 59bb8b360e636ce115d92e27e9668b39ba7ed725 Mon Sep 17 00:00:00 2001 From: Clifford Roche <1007595+cmroche@users.noreply.github.com> Date: Sat, 29 Jun 2024 02:25:22 -0400 Subject: [PATCH 1371/1445] Bump greeclimate to 1.4.6 (#120758) --- homeassistant/components/gree/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/gree/manifest.json b/homeassistant/components/gree/manifest.json index 58404e90353..a7c884c4042 100644 --- a/homeassistant/components/gree/manifest.json +++ b/homeassistant/components/gree/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/gree", "iot_class": "local_polling", "loggers": ["greeclimate"], - "requirements": ["greeclimate==1.4.1"] + "requirements": ["greeclimate==1.4.6"] } diff --git a/requirements_all.txt b/requirements_all.txt index 3ce72af693b..873a2460c1f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1004,7 +1004,7 @@ gpiozero==1.6.2 gps3==0.33.3 # homeassistant.components.gree -greeclimate==1.4.1 +greeclimate==1.4.6 # homeassistant.components.greeneye_monitor greeneye_monitor==3.0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f87edeff701..51afc6123ec 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -827,7 +827,7 @@ govee-local-api==1.5.1 gps3==0.33.3 # homeassistant.components.gree -greeclimate==1.4.1 +greeclimate==1.4.6 # homeassistant.components.greeneye_monitor greeneye_monitor==3.0.3 From 917eeba98422c8a00e678507f54a91e3779a28f6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Jun 2024 10:50:55 -0500 Subject: [PATCH 1372/1445] Increase mqtt availablity timeout to 50s (#120760) --- homeassistant/components/mqtt/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/mqtt/util.py b/homeassistant/components/mqtt/util.py index 97fa616fdd1..27bdb4f2a35 100644 --- a/homeassistant/components/mqtt/util.py +++ b/homeassistant/components/mqtt/util.py @@ -36,7 +36,7 @@ from .const import ( ) from .models import DATA_MQTT, DATA_MQTT_AVAILABLE, ReceiveMessage -AVAILABILITY_TIMEOUT = 30.0 +AVAILABILITY_TIMEOUT = 50.0 TEMP_DIR_NAME = f"home-assistant-{DOMAIN}" From d1a96ef3621ff6ef09c7baeb8648bb7f4501c34f Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Fri, 28 Jun 2024 18:34:24 +0200 Subject: [PATCH 1373/1445] Do not call async_delete_issue() if there is no issue to delete in Shelly integration (#120762) Co-authored-by: Maciej Bieniek <478555+bieniu@users.noreply.github.com> --- homeassistant/components/shelly/coordinator.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/shelly/coordinator.py b/homeassistant/components/shelly/coordinator.py index 02feef3633b..33ed07c35de 100644 --- a/homeassistant/components/shelly/coordinator.py +++ b/homeassistant/components/shelly/coordinator.py @@ -377,12 +377,13 @@ class ShellyBlockCoordinator(ShellyCoordinatorBase[BlockDevice]): eager_start=True, ) elif update_type is BlockUpdateType.COAP_PERIODIC: + if self._push_update_failures >= MAX_PUSH_UPDATE_FAILURES: + ir.async_delete_issue( + self.hass, + DOMAIN, + PUSH_UPDATE_ISSUE_ID.format(unique=self.mac), + ) self._push_update_failures = 0 - ir.async_delete_issue( - self.hass, - DOMAIN, - PUSH_UPDATE_ISSUE_ID.format(unique=self.mac), - ) elif update_type is BlockUpdateType.COAP_REPLY: self._push_update_failures += 1 if self._push_update_failures == MAX_PUSH_UPDATE_FAILURES: From 0f3ed3bb674d4df1d99e99bd5a7f5630883064f8 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 28 Jun 2024 17:51:34 +0200 Subject: [PATCH 1374/1445] Bump aiowithings to 3.0.2 (#120765) --- homeassistant/components/withings/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/withings/manifest.json b/homeassistant/components/withings/manifest.json index 4c97f43fd80..090f8c4588e 100644 --- a/homeassistant/components/withings/manifest.json +++ b/homeassistant/components/withings/manifest.json @@ -9,5 +9,5 @@ "iot_class": "cloud_push", "loggers": ["aiowithings"], "quality_scale": "platinum", - "requirements": ["aiowithings==3.0.1"] + "requirements": ["aiowithings==3.0.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 873a2460c1f..598fef62903 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -407,7 +407,7 @@ aiowatttime==0.1.1 aiowebostv==0.4.0 # homeassistant.components.withings -aiowithings==3.0.1 +aiowithings==3.0.2 # homeassistant.components.yandex_transport aioymaps==1.2.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 51afc6123ec..5e34afbdcf2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -380,7 +380,7 @@ aiowatttime==0.1.1 aiowebostv==0.4.0 # homeassistant.components.withings -aiowithings==3.0.1 +aiowithings==3.0.2 # homeassistant.components.yandex_transport aioymaps==1.2.2 From 8165acddeb8a35f9556d32ce666f96405b3f153e Mon Sep 17 00:00:00 2001 From: Matthew FitzGerald-Chamberlain Date: Fri, 28 Jun 2024 15:15:34 -0500 Subject: [PATCH 1375/1445] Bump pyaprilaire to 0.7.4 (#120782) --- homeassistant/components/aprilaire/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/aprilaire/manifest.json b/homeassistant/components/aprilaire/manifest.json index 43ba4417638..3cc44786989 100644 --- a/homeassistant/components/aprilaire/manifest.json +++ b/homeassistant/components/aprilaire/manifest.json @@ -7,5 +7,5 @@ "integration_type": "device", "iot_class": "local_push", "loggers": ["pyaprilaire"], - "requirements": ["pyaprilaire==0.7.0"] + "requirements": ["pyaprilaire==0.7.4"] } diff --git a/requirements_all.txt b/requirements_all.txt index 598fef62903..4e407a30b48 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1710,7 +1710,7 @@ pyairnow==1.2.1 pyairvisual==2023.08.1 # homeassistant.components.aprilaire -pyaprilaire==0.7.0 +pyaprilaire==0.7.4 # homeassistant.components.asuswrt pyasuswrt==0.1.21 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5e34afbdcf2..b899463e178 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1360,7 +1360,7 @@ pyairnow==1.2.1 pyairvisual==2023.08.1 # homeassistant.components.aprilaire -pyaprilaire==0.7.0 +pyaprilaire==0.7.4 # homeassistant.components.asuswrt pyasuswrt==0.1.21 From b30b4d5a3a63e27460b2e8bbfaf7172a7397727f Mon Sep 17 00:00:00 2001 From: Klaas Schoute Date: Fri, 28 Jun 2024 22:23:44 +0200 Subject: [PATCH 1376/1445] Bump energyzero lib to v2.1.1 (#120783) --- homeassistant/components/energyzero/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/energyzero/manifest.json b/homeassistant/components/energyzero/manifest.json index 025f929a4f6..807a0419967 100644 --- a/homeassistant/components/energyzero/manifest.json +++ b/homeassistant/components/energyzero/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/energyzero", "iot_class": "cloud_polling", "quality_scale": "platinum", - "requirements": ["energyzero==2.1.0"] + "requirements": ["energyzero==2.1.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 4e407a30b48..d7a5eaa4a62 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -807,7 +807,7 @@ emulated-roku==0.3.0 energyflip-client==0.2.2 # homeassistant.components.energyzero -energyzero==2.1.0 +energyzero==2.1.1 # homeassistant.components.enocean enocean==0.50 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b899463e178..a6cf6b7e6d1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -667,7 +667,7 @@ emulated-roku==0.3.0 energyflip-client==0.2.2 # homeassistant.components.energyzero -energyzero==2.1.0 +energyzero==2.1.1 # homeassistant.components.enocean enocean==0.50 From 723c4a1eb5276307a11b138f66c26dab9b0df640 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Sat, 29 Jun 2024 06:14:00 +0200 Subject: [PATCH 1377/1445] Update frontend to 20240628.0 (#120785) Co-authored-by: J. Nick Koston --- 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 cd46b358335..70f1f5f4f4f 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==20240627.0"] + "requirements": ["home-assistant-frontend==20240628.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 91db2564fa6..7cccd58d73f 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -32,7 +32,7 @@ habluetooth==3.1.3 hass-nabucasa==0.81.1 hassil==1.7.1 home-assistant-bluetooth==1.12.2 -home-assistant-frontend==20240627.0 +home-assistant-frontend==20240628.0 home-assistant-intents==2024.6.26 httpx==0.27.0 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index d7a5eaa4a62..34d6daa8ab2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1090,7 +1090,7 @@ hole==0.8.0 holidays==0.51 # homeassistant.components.frontend -home-assistant-frontend==20240627.0 +home-assistant-frontend==20240628.0 # homeassistant.components.conversation home-assistant-intents==2024.6.26 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a6cf6b7e6d1..d76492f1c65 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -895,7 +895,7 @@ hole==0.8.0 holidays==0.51 # homeassistant.components.frontend -home-assistant-frontend==20240627.0 +home-assistant-frontend==20240628.0 # homeassistant.components.conversation home-assistant-intents==2024.6.26 From ec577c7bd333cceff2020ed220c7236c5fa4acde Mon Sep 17 00:00:00 2001 From: Klaas Schoute Date: Fri, 28 Jun 2024 23:20:16 +0200 Subject: [PATCH 1378/1445] Bump odp-amsterdam lib to v6.0.2 (#120788) --- homeassistant/components/garages_amsterdam/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/garages_amsterdam/manifest.json b/homeassistant/components/garages_amsterdam/manifest.json index ebda913abbb..4d4bb9f6fb5 100644 --- a/homeassistant/components/garages_amsterdam/manifest.json +++ b/homeassistant/components/garages_amsterdam/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/garages_amsterdam", "iot_class": "cloud_polling", - "requirements": ["odp-amsterdam==6.0.1"] + "requirements": ["odp-amsterdam==6.0.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 34d6daa8ab2..7f8d32f59d7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1453,7 +1453,7 @@ oauth2client==4.1.3 objgraph==3.5.0 # homeassistant.components.garages_amsterdam -odp-amsterdam==6.0.1 +odp-amsterdam==6.0.2 # homeassistant.components.oem oemthermostat==1.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d76492f1c65..f1d09d9c406 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1177,7 +1177,7 @@ oauth2client==4.1.3 objgraph==3.5.0 # homeassistant.components.garages_amsterdam -odp-amsterdam==6.0.1 +odp-amsterdam==6.0.2 # homeassistant.components.ollama ollama-hass==0.1.7 From b45eff9a2b7bcd7c386d45db04f061facf1516bf Mon Sep 17 00:00:00 2001 From: Klaas Schoute Date: Sat, 29 Jun 2024 00:24:43 +0200 Subject: [PATCH 1379/1445] Bump gridnet lib to v5.0.1 (#120793) --- homeassistant/components/pure_energie/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/pure_energie/manifest.json b/homeassistant/components/pure_energie/manifest.json index 19098c41208..ff52ec0ecf9 100644 --- a/homeassistant/components/pure_energie/manifest.json +++ b/homeassistant/components/pure_energie/manifest.json @@ -6,7 +6,7 @@ "documentation": "https://www.home-assistant.io/integrations/pure_energie", "iot_class": "local_polling", "quality_scale": "platinum", - "requirements": ["gridnet==5.0.0"], + "requirements": ["gridnet==5.0.1"], "zeroconf": [ { "type": "_http._tcp.local.", diff --git a/requirements_all.txt b/requirements_all.txt index 7f8d32f59d7..60346dd1959 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1013,7 +1013,7 @@ greeneye_monitor==3.0.3 greenwavereality==0.5.1 # homeassistant.components.pure_energie -gridnet==5.0.0 +gridnet==5.0.1 # homeassistant.components.growatt_server growattServer==1.5.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f1d09d9c406..b3725e8e237 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -833,7 +833,7 @@ greeclimate==1.4.6 greeneye_monitor==3.0.3 # homeassistant.components.pure_energie -gridnet==5.0.0 +gridnet==5.0.1 # homeassistant.components.growatt_server growattServer==1.5.0 From 0dcfd38cdc1d7b924f4356e88fd5b8dbdfb7db70 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 29 Jun 2024 00:01:18 -0500 Subject: [PATCH 1380/1445] Fix missing f-string in loop util (#120800) --- homeassistant/util/loop.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/util/loop.py b/homeassistant/util/loop.py index 8a469569601..866f35e79e2 100644 --- a/homeassistant/util/loop.py +++ b/homeassistant/util/loop.py @@ -106,10 +106,10 @@ def raise_for_blocking_call( if strict: raise RuntimeError( - "Caught blocking call to {func.__name__} with args " - f"{mapped_args.get('args')} inside the event loop by" + f"Caught blocking call to {func.__name__} with args " + f"{mapped_args.get('args')} inside the event loop by " f"{'custom ' if integration_frame.custom_integration else ''}" - "integration '{integration_frame.integration}' at " + f"integration '{integration_frame.integration}' at " f"{integration_frame.relative_filename}, line {integration_frame.line_number}:" f" {integration_frame.line}. (offender: {offender_filename}, line " f"{offender_lineno}: {offender_line}), please {report_issue}\n" From 0ec07001bd275da93709a0b7df4c7d05e1d43e75 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 29 Jun 2024 00:40:35 -0500 Subject: [PATCH 1381/1445] Fix blocking I/O in xmpp notify to read uploaded files (#120801) detected by ruff in https://github.com/home-assistant/core/pull/120799 --- homeassistant/components/xmpp/notify.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/xmpp/notify.py b/homeassistant/components/xmpp/notify.py index 824f996c675..c73248f2524 100644 --- a/homeassistant/components/xmpp/notify.py +++ b/homeassistant/components/xmpp/notify.py @@ -305,16 +305,20 @@ async def async_send_message( # noqa: C901 timeout=timeout, ) - async def upload_file_from_path(self, path, timeout=None): + def _read_upload_file(self, path: str) -> bytes: + """Read file from path.""" + with open(path, "rb") as upfile: + _LOGGER.debug("Reading file %s", path) + return upfile.read() + + async def upload_file_from_path(self, path: str, timeout=None): """Upload a file from a local file path via XEP_0363.""" _LOGGER.info("Uploading file from path, %s", path) if not hass.config.is_allowed_path(path): raise PermissionError("Could not access file. Path not allowed") - with open(path, "rb") as upfile: - _LOGGER.debug("Reading file %s", path) - input_file = upfile.read() + input_file = await hass.async_add_executor_job(self._read_upload_file, path) filesize = len(input_file) _LOGGER.debug("Filesize is %s bytes", filesize) From 66932e3d9a264df08bac40219064e411cbc976a5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 29 Jun 2024 00:42:08 -0500 Subject: [PATCH 1382/1445] Fix unneeded dict values for MATCH_ALL recorder attrs exclude (#120804) * Small cleanup to handling MATCH_ALL recorder attrs exclude * Fix unneeded dict values for MATCH_ALL recorder attrs exclude The exclude is a set so the dict values were not needed * Fix unneeded dict values for MATCH_ALL recorder attrs exclude The exclude is a set so the dict values were not needed * Fix unneeded dict values for MATCH_ALL recorder attrs exclude The exclude is a set so the dict values were not needed --- .../components/recorder/db_schema.py | 22 ++++++++----------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/recorder/db_schema.py b/homeassistant/components/recorder/db_schema.py index ce463067824..ba4a6106bce 100644 --- a/homeassistant/components/recorder/db_schema.py +++ b/homeassistant/components/recorder/db_schema.py @@ -142,6 +142,13 @@ _DEFAULT_TABLE_ARGS = { "mariadb_engine": MYSQL_ENGINE, } +_MATCH_ALL_KEEP = { + ATTR_DEVICE_CLASS, + ATTR_STATE_CLASS, + ATTR_UNIT_OF_MEASUREMENT, + ATTR_FRIENDLY_NAME, +} + class UnusedDateTime(DateTime): """An unused column type that behaves like a datetime.""" @@ -597,19 +604,8 @@ class StateAttributes(Base): if MATCH_ALL in unrecorded_attributes: # Don't exclude device class, state class, unit of measurement # or friendly name when using the MATCH_ALL exclude constant - _exclude_attributes = { - k: v - for k, v in state.attributes.items() - if k - not in ( - ATTR_DEVICE_CLASS, - ATTR_STATE_CLASS, - ATTR_UNIT_OF_MEASUREMENT, - ATTR_FRIENDLY_NAME, - ) - } - exclude_attrs.update(_exclude_attributes) - + exclude_attrs.update(state.attributes) + exclude_attrs -= _MATCH_ALL_KEEP else: exclude_attrs = ALL_DOMAIN_EXCLUDE_ATTRS encoder = json_bytes_strip_null if dialect == PSQL_DIALECT else json_bytes From 7319492bf3a14c51e59b5be6fb67c01e5015e5d7 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sat, 29 Jun 2024 13:48:43 +0200 Subject: [PATCH 1383/1445] Bump aiomealie to 0.5.0 (#120815) --- homeassistant/components/mealie/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/mealie/manifest.json b/homeassistant/components/mealie/manifest.json index fb81ff850b8..918dd743726 100644 --- a/homeassistant/components/mealie/manifest.json +++ b/homeassistant/components/mealie/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/mealie", "integration_type": "service", "iot_class": "local_polling", - "requirements": ["aiomealie==0.4.0"] + "requirements": ["aiomealie==0.5.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 60346dd1959..e2badab1ccd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -294,7 +294,7 @@ aiolookin==1.0.0 aiolyric==1.1.0 # homeassistant.components.mealie -aiomealie==0.4.0 +aiomealie==0.5.0 # homeassistant.components.modern_forms aiomodernforms==0.1.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b3725e8e237..7320c66cbdb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -267,7 +267,7 @@ aiolookin==1.0.0 aiolyric==1.1.0 # homeassistant.components.mealie -aiomealie==0.4.0 +aiomealie==0.5.0 # homeassistant.components.modern_forms aiomodernforms==0.1.8 From bb52bfd73d048eef06edcf1ebe247a25de66abd7 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sat, 29 Jun 2024 17:48:28 +0200 Subject: [PATCH 1384/1445] Add unique id to Mealie config entry (#120816) --- homeassistant/components/mealie/calendar.py | 5 +--- .../components/mealie/config_flow.py | 5 ++-- homeassistant/components/mealie/entity.py | 7 ++++-- tests/components/mealie/conftest.py | 6 ++++- .../mealie/fixtures/users_self.json | 24 +++++++++++++++++++ .../mealie/snapshots/test_calendar.ambr | 8 +++---- .../mealie/snapshots/test_init.ambr | 2 +- tests/components/mealie/test_config_flow.py | 5 ++-- tests/components/mealie/test_init.py | 2 +- 9 files changed, 47 insertions(+), 17 deletions(-) create mode 100644 tests/components/mealie/fixtures/users_self.json diff --git a/homeassistant/components/mealie/calendar.py b/homeassistant/components/mealie/calendar.py index 08e90ebf5ea..62c1473057d 100644 --- a/homeassistant/components/mealie/calendar.py +++ b/homeassistant/components/mealie/calendar.py @@ -50,12 +50,9 @@ class MealieMealplanCalendarEntity(MealieEntity, CalendarEntity): self, coordinator: MealieCoordinator, entry_type: MealplanEntryType ) -> None: """Create the Calendar entity.""" - super().__init__(coordinator) + super().__init__(coordinator, entry_type.name.lower()) self._entry_type = entry_type self._attr_translation_key = entry_type.name.lower() - self._attr_unique_id = ( - f"{self.coordinator.config_entry.entry_id}_{entry_type.name.lower()}" - ) @property def event(self) -> CalendarEvent | None: diff --git a/homeassistant/components/mealie/config_flow.py b/homeassistant/components/mealie/config_flow.py index b25cade148a..550e4679720 100644 --- a/homeassistant/components/mealie/config_flow.py +++ b/homeassistant/components/mealie/config_flow.py @@ -28,14 +28,13 @@ class MealieConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a flow initialized by the user.""" errors: dict[str, str] = {} if user_input: - self._async_abort_entries_match({CONF_HOST: user_input[CONF_HOST]}) client = MealieClient( user_input[CONF_HOST], token=user_input[CONF_API_TOKEN], session=async_get_clientsession(self.hass), ) try: - await client.get_mealplan_today() + info = await client.get_user_info() except MealieConnectionError: errors["base"] = "cannot_connect" except MealieAuthenticationError: @@ -44,6 +43,8 @@ class MealieConfigFlow(ConfigFlow, domain=DOMAIN): LOGGER.exception("Unexpected error") errors["base"] = "unknown" else: + await self.async_set_unique_id(info.user_id) + self._abort_if_unique_id_configured() return self.async_create_entry( title="Mealie", data=user_input, diff --git a/homeassistant/components/mealie/entity.py b/homeassistant/components/mealie/entity.py index 5e339c1d4b8..765ae2b99d7 100644 --- a/homeassistant/components/mealie/entity.py +++ b/homeassistant/components/mealie/entity.py @@ -12,10 +12,13 @@ class MealieEntity(CoordinatorEntity[MealieCoordinator]): _attr_has_entity_name = True - def __init__(self, coordinator: MealieCoordinator) -> None: + def __init__(self, coordinator: MealieCoordinator, key: str) -> None: """Initialize Mealie entity.""" super().__init__(coordinator) + unique_id = coordinator.config_entry.unique_id + assert unique_id is not None + self._attr_unique_id = f"{unique_id}_{key}" self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, coordinator.config_entry.entry_id)}, + identifiers={(DOMAIN, unique_id)}, entry_type=DeviceEntryType.SERVICE, ) diff --git a/tests/components/mealie/conftest.py b/tests/components/mealie/conftest.py index dd6309cb524..9bda9e3c46d 100644 --- a/tests/components/mealie/conftest.py +++ b/tests/components/mealie/conftest.py @@ -2,7 +2,7 @@ from unittest.mock import patch -from aiomealie import Mealplan, MealplanResponse +from aiomealie import Mealplan, MealplanResponse, UserInfo from mashumaro.codecs.orjson import ORJSONDecoder import pytest from typing_extensions import Generator @@ -44,6 +44,9 @@ def mock_mealie_client() -> Generator[AsyncMock]: client.get_mealplan_today.return_value = ORJSONDecoder(list[Mealplan]).decode( load_fixture("get_mealplan_today.json", DOMAIN) ) + client.get_user_info.return_value = UserInfo.from_json( + load_fixture("users_self.json", DOMAIN) + ) yield client @@ -55,4 +58,5 @@ def mock_config_entry() -> MockConfigEntry: title="Mealie", data={CONF_HOST: "demo.mealie.io", CONF_API_TOKEN: "token"}, entry_id="01J0BC4QM2YBRP6H5G933CETT7", + unique_id="bf1c62fe-4941-4332-9886-e54e88dbdba0", ) diff --git a/tests/components/mealie/fixtures/users_self.json b/tests/components/mealie/fixtures/users_self.json new file mode 100644 index 00000000000..6d5901c8cc0 --- /dev/null +++ b/tests/components/mealie/fixtures/users_self.json @@ -0,0 +1,24 @@ +{ + "id": "bf1c62fe-4941-4332-9886-e54e88dbdba0", + "username": "admin", + "fullName": "Change Me", + "email": "changeme@example.com", + "authMethod": "Mealie", + "admin": true, + "group": "home", + "advanced": true, + "canInvite": true, + "canManage": true, + "canOrganize": true, + "groupId": "24477569-f6af-4b53-9e3f-6d04b0ca6916", + "groupSlug": "home", + "tokens": [ + { + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb25nX3Rva2VuIjp0cnVlLCJpZCI6ImJmMWM2MmZlLTQ5NDEtNDMzMi05ODg2LWU1NGU4OGRiZGJhMCIsIm5hbWUiOiJ0ZXN0aW5nIiwiaW50ZWdyYXRpb25faWQiOiJnZW5lcmljIiwiZXhwIjoxODczOTA5ODk4fQ.xwXZp4fL2g1RbIqGtBeOaS6RDfsYbQDHj8XtRM3wlX0", + "name": "testing", + "id": 2, + "createdAt": "2024-05-20T10:31:38.179669" + } + ], + "cacheKey": "1234" +} diff --git a/tests/components/mealie/snapshots/test_calendar.ambr b/tests/components/mealie/snapshots/test_calendar.ambr index 6af53c112de..3db0da0d765 100644 --- a/tests/components/mealie/snapshots/test_calendar.ambr +++ b/tests/components/mealie/snapshots/test_calendar.ambr @@ -178,7 +178,7 @@ 'previous_unique_id': None, 'supported_features': 0, 'translation_key': 'breakfast', - 'unique_id': '01J0BC4QM2YBRP6H5G933CETT7_breakfast', + 'unique_id': 'bf1c62fe-4941-4332-9886-e54e88dbdba0_breakfast', 'unit_of_measurement': None, }) # --- @@ -230,7 +230,7 @@ 'previous_unique_id': None, 'supported_features': 0, 'translation_key': 'dinner', - 'unique_id': '01J0BC4QM2YBRP6H5G933CETT7_dinner', + 'unique_id': 'bf1c62fe-4941-4332-9886-e54e88dbdba0_dinner', 'unit_of_measurement': None, }) # --- @@ -282,7 +282,7 @@ 'previous_unique_id': None, 'supported_features': 0, 'translation_key': 'lunch', - 'unique_id': '01J0BC4QM2YBRP6H5G933CETT7_lunch', + 'unique_id': 'bf1c62fe-4941-4332-9886-e54e88dbdba0_lunch', 'unit_of_measurement': None, }) # --- @@ -334,7 +334,7 @@ 'previous_unique_id': None, 'supported_features': 0, 'translation_key': 'side', - 'unique_id': '01J0BC4QM2YBRP6H5G933CETT7_side', + 'unique_id': 'bf1c62fe-4941-4332-9886-e54e88dbdba0_side', 'unit_of_measurement': None, }) # --- diff --git a/tests/components/mealie/snapshots/test_init.ambr b/tests/components/mealie/snapshots/test_init.ambr index 1333b292dac..8f800676945 100644 --- a/tests/components/mealie/snapshots/test_init.ambr +++ b/tests/components/mealie/snapshots/test_init.ambr @@ -13,7 +13,7 @@ 'identifiers': set({ tuple( 'mealie', - '01J0BC4QM2YBRP6H5G933CETT7', + 'bf1c62fe-4941-4332-9886-e54e88dbdba0', ), }), 'is_new': False, diff --git a/tests/components/mealie/test_config_flow.py b/tests/components/mealie/test_config_flow.py index ac68ed2fac5..777bb1e4ad1 100644 --- a/tests/components/mealie/test_config_flow.py +++ b/tests/components/mealie/test_config_flow.py @@ -37,6 +37,7 @@ async def test_full_flow( CONF_HOST: "demo.mealie.io", CONF_API_TOKEN: "token", } + assert result["result"].unique_id == "bf1c62fe-4941-4332-9886-e54e88dbdba0" @pytest.mark.parametrize( @@ -55,7 +56,7 @@ async def test_flow_errors( error: str, ) -> None: """Test flow errors.""" - mock_mealie_client.get_mealplan_today.side_effect = exception + mock_mealie_client.get_user_info.side_effect = exception result = await hass.config_entries.flow.async_init( DOMAIN, @@ -72,7 +73,7 @@ async def test_flow_errors( assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": error} - mock_mealie_client.get_mealplan_today.side_effect = None + mock_mealie_client.get_user_info.side_effect = None result = await hass.config_entries.flow.async_configure( result["flow_id"], diff --git a/tests/components/mealie/test_init.py b/tests/components/mealie/test_init.py index 7d63ad135f9..5a7a5387897 100644 --- a/tests/components/mealie/test_init.py +++ b/tests/components/mealie/test_init.py @@ -26,7 +26,7 @@ async def test_device_info( """Test device registry integration.""" await setup_integration(hass, mock_config_entry) device_entry = device_registry.async_get_device( - identifiers={(DOMAIN, mock_config_entry.entry_id)} + identifiers={(DOMAIN, mock_config_entry.unique_id)} ) assert device_entry is not None assert device_entry == snapshot From 05c63eb88491ca0c1bfe18a2f91ed9aae54cf749 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sat, 29 Jun 2024 14:53:42 +0200 Subject: [PATCH 1385/1445] Bump python-opensky to 1.0.1 (#120818) --- homeassistant/components/opensky/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/opensky/manifest.json b/homeassistant/components/opensky/manifest.json index 106103cf752..831abbc9cbf 100644 --- a/homeassistant/components/opensky/manifest.json +++ b/homeassistant/components/opensky/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/opensky", "iot_class": "cloud_polling", - "requirements": ["python-opensky==1.0.0"] + "requirements": ["python-opensky==1.0.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index e2badab1ccd..ba3e2f5fadb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2296,7 +2296,7 @@ python-mystrom==2.2.0 python-opendata-transport==0.4.0 # homeassistant.components.opensky -python-opensky==1.0.0 +python-opensky==1.0.1 # homeassistant.components.otbr # homeassistant.components.thread diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7320c66cbdb..b0a6fb54b3a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1793,7 +1793,7 @@ python-mystrom==2.2.0 python-opendata-transport==0.4.0 # homeassistant.components.opensky -python-opensky==1.0.0 +python-opensky==1.0.1 # homeassistant.components.otbr # homeassistant.components.thread From e866417c01c932dcf8ba894e6ca15eff82afb71b Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sat, 29 Jun 2024 17:46:44 +0200 Subject: [PATCH 1386/1445] Add icons to Airgradient (#120820) --- .../components/airgradient/icons.json | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/homeassistant/components/airgradient/icons.json b/homeassistant/components/airgradient/icons.json index 45d1e12d46e..22188d72faa 100644 --- a/homeassistant/components/airgradient/icons.json +++ b/homeassistant/components/airgradient/icons.json @@ -8,6 +8,34 @@ "default": "mdi:lightbulb-on-outline" } }, + "number": { + "led_bar_brightness": { + "default": "mdi:brightness-percent" + }, + "display_brightness": { + "default": "mdi:brightness-percent" + } + }, + "select": { + "configuration_control": { + "default": "mdi:cloud-cog" + }, + "display_temperature_unit": { + "default": "mdi:thermometer-lines" + }, + "led_bar_mode": { + "default": "mdi:led-strip" + }, + "nox_index_learning_time_offset": { + "default": "mdi:clock-outline" + }, + "voc_index_learning_time_offset": { + "default": "mdi:clock-outline" + }, + "co2_automatic_baseline_calibration": { + "default": "mdi:molecule-co2" + } + }, "sensor": { "total_volatile_organic_component_index": { "default": "mdi:molecule" @@ -17,6 +45,32 @@ }, "pm003_count": { "default": "mdi:blur" + }, + "led_bar_brightness": { + "default": "mdi:brightness-percent" + }, + "display_brightness": { + "default": "mdi:brightness-percent" + }, + "display_temperature_unit": { + "default": "mdi:thermometer-lines" + }, + "led_bar_mode": { + "default": "mdi:led-strip" + }, + "nox_index_learning_time_offset": { + "default": "mdi:clock-outline" + }, + "voc_index_learning_time_offset": { + "default": "mdi:clock-outline" + }, + "co2_automatic_baseline_calibration": { + "default": "mdi:molecule-co2" + } + }, + "switch": { + "post_data_to_airgradient": { + "default": "mdi:cogs" } } } From 3ee8f6edbac8fc1a6c8ec8e45887f3457505c247 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sat, 29 Jun 2024 17:47:21 +0200 Subject: [PATCH 1387/1445] Use meal note as fallback in Mealie (#120828) --- homeassistant/components/mealie/calendar.py | 4 ++-- .../components/mealie/fixtures/get_mealplans.json | 11 +++++++++++ .../components/mealie/snapshots/test_calendar.ambr | 14 ++++++++++++++ 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/mealie/calendar.py b/homeassistant/components/mealie/calendar.py index 62c1473057d..fb628754f06 100644 --- a/homeassistant/components/mealie/calendar.py +++ b/homeassistant/components/mealie/calendar.py @@ -30,8 +30,8 @@ async def async_setup_entry( def _get_event_from_mealplan(mealplan: Mealplan) -> CalendarEvent: """Create a CalendarEvent from a Mealplan.""" - description: str | None = None - name = "No recipe" + description: str | None = mealplan.description + name = mealplan.title or "No recipe" if mealplan.recipe: name = mealplan.recipe.name description = mealplan.recipe.description diff --git a/tests/components/mealie/fixtures/get_mealplans.json b/tests/components/mealie/fixtures/get_mealplans.json index 2d63b753d99..9255f9b7396 100644 --- a/tests/components/mealie/fixtures/get_mealplans.json +++ b/tests/components/mealie/fixtures/get_mealplans.json @@ -605,6 +605,17 @@ "updateAt": "2024-01-02T06:35:05.209189", "lastMade": "2024-01-02T22:59:59" } + }, + { + "date": "2024-01-21", + "entryType": "dinner", + "title": "Aquavite", + "text": "Dineren met de boys", + "recipeId": null, + "id": 1, + "groupId": "3931df86-0679-4579-8c63-4bedc9ca9a85", + "userId": "6caa6e4d-521f-4ef4-9ed7-388bdd63f47d", + "recipe": null } ], "next": null, diff --git a/tests/components/mealie/snapshots/test_calendar.ambr b/tests/components/mealie/snapshots/test_calendar.ambr index 3db0da0d765..c3b26e1e9e2 100644 --- a/tests/components/mealie/snapshots/test_calendar.ambr +++ b/tests/components/mealie/snapshots/test_calendar.ambr @@ -147,6 +147,20 @@ 'summary': 'Mousse de saumon', 'uid': None, }), + dict({ + 'description': 'Dineren met de boys', + 'end': dict({ + 'date': '2024-01-22', + }), + 'location': None, + 'recurrence_id': None, + 'rrule': None, + 'start': dict({ + 'date': '2024-01-21', + }), + 'summary': 'Aquavite', + 'uid': None, + }), ]) # --- # name: test_entities[calendar.mealie_breakfast-entry] From 08a0eaf1847d995995909d2f7ae53c5821cd594e Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 29 Jun 2024 17:51:45 +0200 Subject: [PATCH 1388/1445] Bump version to 2024.7.0b5 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 1ab2a3f6893..fa19aa7349e 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -24,7 +24,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 7 -PATCH_VERSION: Final = "0b4" +PATCH_VERSION: Final = "0b5" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0) diff --git a/pyproject.toml b/pyproject.toml index e96c329fd5a..3b42dfa2d6b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.7.0b4" +version = "2024.7.0b5" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 38a30b343dee78b3715ad8dab28a982ea367be3a Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sun, 30 Jun 2024 15:28:01 +0200 Subject: [PATCH 1389/1445] Bump pizzapi to 0.0.6 (#120691) --- homeassistant/components/dominos/__init__.py | 14 +++++++------- homeassistant/components/dominos/manifest.json | 2 +- requirements_all.txt | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/dominos/__init__.py b/homeassistant/components/dominos/__init__.py index ce7b36f2280..9b11b667e84 100644 --- a/homeassistant/components/dominos/__init__.py +++ b/homeassistant/components/dominos/__init__.py @@ -4,11 +4,11 @@ from datetime import timedelta import logging from pizzapi import Address, Customer, Order -from pizzapi.address import StoreException import voluptuous as vol from homeassistant.components import http from homeassistant.core import HomeAssistant, ServiceCall, callback +from homeassistant.exceptions import HomeAssistantError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent @@ -118,7 +118,7 @@ class Dominos: self.country = conf.get(ATTR_COUNTRY) try: self.closest_store = self.address.closest_store() - except StoreException: + except Exception: # noqa: BLE001 self.closest_store = None def handle_order(self, call: ServiceCall) -> None: @@ -139,7 +139,7 @@ class Dominos: """Update the shared closest store (if open).""" try: self.closest_store = self.address.closest_store() - except StoreException: + except Exception: # noqa: BLE001 self.closest_store = None return False return True @@ -219,7 +219,7 @@ class DominosOrder(Entity): """Update the order state and refreshes the store.""" try: self.dominos.update_closest_store() - except StoreException: + except Exception: # noqa: BLE001 self._orderable = False return @@ -227,13 +227,13 @@ class DominosOrder(Entity): order = self.order() order.pay_with() self._orderable = True - except StoreException: + except Exception: # noqa: BLE001 self._orderable = False def order(self): """Create the order object.""" if self.dominos.closest_store is None: - raise StoreException + raise HomeAssistantError("No store available") order = Order( self.dominos.closest_store, @@ -252,7 +252,7 @@ class DominosOrder(Entity): try: order = self.order() order.place() - except StoreException: + except Exception: # noqa: BLE001 self._orderable = False _LOGGER.warning( "Attempted to order Dominos - Order invalid or store closed" diff --git a/homeassistant/components/dominos/manifest.json b/homeassistant/components/dominos/manifest.json index dfb8966013f..442f433db7c 100644 --- a/homeassistant/components/dominos/manifest.json +++ b/homeassistant/components/dominos/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/dominos", "iot_class": "cloud_polling", "loggers": ["pizzapi"], - "requirements": ["pizzapi==0.0.3"] + "requirements": ["pizzapi==0.0.6"] } diff --git a/requirements_all.txt b/requirements_all.txt index ba3e2f5fadb..d44ea880636 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1557,7 +1557,7 @@ pigpio==1.78 pilight==0.1.1 # homeassistant.components.dominos -pizzapi==0.0.3 +pizzapi==0.0.6 # homeassistant.components.plex plexauth==0.0.6 From a7246400b3db10a4daa24c6c989368f187f7506c Mon Sep 17 00:00:00 2001 From: mkmer Date: Sun, 30 Jun 2024 09:30:52 -0400 Subject: [PATCH 1390/1445] Allow EM heat on from any mode in Honeywell (#120750) --- homeassistant/components/honeywell/switch.py | 13 ++++++------ tests/components/honeywell/test_switch.py | 21 +------------------- 2 files changed, 7 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/honeywell/switch.py b/homeassistant/components/honeywell/switch.py index 53a9b27ee72..b90dd339593 100644 --- a/homeassistant/components/honeywell/switch.py +++ b/homeassistant/components/honeywell/switch.py @@ -71,13 +71,12 @@ class HoneywellSwitch(SwitchEntity): async def async_turn_on(self, **kwargs: Any) -> None: """Turn the switch on if heat mode is enabled.""" - if self._device.system_mode == "heat": - try: - await self._device.set_system_mode("emheat") - except SomeComfortError as err: - raise HomeAssistantError( - translation_domain=DOMAIN, translation_key="switch_failed_on" - ) from err + try: + await self._device.set_system_mode("emheat") + except SomeComfortError as err: + raise HomeAssistantError( + translation_domain=DOMAIN, translation_key="switch_failed_on" + ) from err async def async_turn_off(self, **kwargs: Any) -> None: """Turn the switch off if on.""" diff --git a/tests/components/honeywell/test_switch.py b/tests/components/honeywell/test_switch.py index 73052871ef1..482b9837b93 100644 --- a/tests/components/honeywell/test_switch.py +++ b/tests/components/honeywell/test_switch.py @@ -24,26 +24,6 @@ async def test_emheat_switch( await init_integration(hass, config_entry) entity_id = f"switch.{device.name}_emergency_heat" - await hass.services.async_call( - SWITCH_DOMAIN, - SERVICE_TURN_ON, - {ATTR_ENTITY_ID: entity_id}, - blocking=True, - ) - device.set_system_mode.assert_not_called() - - device.set_system_mode.reset_mock() - - await hass.services.async_call( - SWITCH_DOMAIN, - SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: entity_id}, - blocking=True, - ) - device.set_system_mode.assert_not_called() - - device.system_mode = "heat" - await hass.services.async_call( SWITCH_DOMAIN, SERVICE_TURN_ON, @@ -53,6 +33,7 @@ async def test_emheat_switch( device.set_system_mode.assert_called_once_with("emheat") device.set_system_mode.reset_mock() + device.system_mode = "emheat" await hass.services.async_call( SWITCH_DOMAIN, From f58eafe8fca3782cd59989a57a5aabfd8a276f8a Mon Sep 17 00:00:00 2001 From: Simon Lamon <32477463+silamon@users.noreply.github.com> Date: Sun, 30 Jun 2024 15:16:41 +0200 Subject: [PATCH 1391/1445] Fix routes with transfer in nmbs integration (#120808) --- homeassistant/components/nmbs/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/nmbs/sensor.py b/homeassistant/components/nmbs/sensor.py index 82fc6143b2d..6ccdc742430 100644 --- a/homeassistant/components/nmbs/sensor.py +++ b/homeassistant/components/nmbs/sensor.py @@ -261,7 +261,7 @@ class NMBSSensor(SensorEntity): attrs["via_arrival_platform"] = via["arrival"]["platform"] attrs["via_transfer_platform"] = via["departure"]["platform"] attrs["via_transfer_time"] = get_delay_in_minutes( - via["timeBetween"] + via["timebetween"] ) + get_delay_in_minutes(via["departure"]["delay"]) if delay > 0: From ad9e0ef8e49a17ec48c19d0ce1c436b9a442d607 Mon Sep 17 00:00:00 2001 From: Etienne Soufflet Date: Sun, 30 Jun 2024 20:38:35 +0200 Subject: [PATCH 1392/1445] Fix Tado fan mode (#120809) --- homeassistant/components/tado/climate.py | 17 ++- homeassistant/components/tado/helper.py | 12 ++ .../tado/fixtures/smartac4.with_fanlevel.json | 88 ++++++++++++ .../components/tado/fixtures/zone_states.json | 73 ++++++++++ ...th_fanlevel_horizontal_vertical_swing.json | 130 ++++++++++++++++++ tests/components/tado/fixtures/zones.json | 40 ++++++ tests/components/tado/test_climate.py | 32 +++++ tests/components/tado/util.py | 18 +++ 8 files changed, 401 insertions(+), 9 deletions(-) create mode 100644 tests/components/tado/fixtures/smartac4.with_fanlevel.json create mode 100644 tests/components/tado/fixtures/zone_with_fanlevel_horizontal_vertical_swing.json diff --git a/homeassistant/components/tado/climate.py b/homeassistant/components/tado/climate.py index 40bdb19b31b..116985796d5 100644 --- a/homeassistant/components/tado/climate.py +++ b/homeassistant/components/tado/climate.py @@ -73,7 +73,7 @@ from .const import ( TYPE_HEATING, ) from .entity import TadoZoneEntity -from .helper import decide_duration, decide_overlay_mode +from .helper import decide_duration, decide_overlay_mode, generate_supported_fanmodes _LOGGER = logging.getLogger(__name__) @@ -200,15 +200,14 @@ def create_climate_entity( continue if capabilities[mode].get("fanSpeeds"): - supported_fan_modes = [ - TADO_TO_HA_FAN_MODE_MAP_LEGACY[speed] - for speed in capabilities[mode]["fanSpeeds"] - ] + supported_fan_modes = generate_supported_fanmodes( + TADO_TO_HA_FAN_MODE_MAP_LEGACY, capabilities[mode]["fanSpeeds"] + ) + else: - supported_fan_modes = [ - TADO_TO_HA_FAN_MODE_MAP[level] - for level in capabilities[mode]["fanLevel"] - ] + supported_fan_modes = generate_supported_fanmodes( + TADO_TO_HA_FAN_MODE_MAP, capabilities[mode]["fanLevel"] + ) cool_temperatures = capabilities[CONST_MODE_COOL]["temperatures"] else: diff --git a/homeassistant/components/tado/helper.py b/homeassistant/components/tado/helper.py index efcd3e7c4ea..81bff1e36c3 100644 --- a/homeassistant/components/tado/helper.py +++ b/homeassistant/components/tado/helper.py @@ -49,3 +49,15 @@ def decide_duration( ) return duration + + +def generate_supported_fanmodes(tado_to_ha_mapping: dict[str, str], options: list[str]): + """Return correct list of fan modes or None.""" + supported_fanmodes = [ + tado_to_ha_mapping.get(option) + for option in options + if tado_to_ha_mapping.get(option) is not None + ] + if not supported_fanmodes: + return None + return supported_fanmodes diff --git a/tests/components/tado/fixtures/smartac4.with_fanlevel.json b/tests/components/tado/fixtures/smartac4.with_fanlevel.json new file mode 100644 index 00000000000..ea1f9cbd8e5 --- /dev/null +++ b/tests/components/tado/fixtures/smartac4.with_fanlevel.json @@ -0,0 +1,88 @@ +{ + "tadoMode": "HOME", + "geolocationOverride": false, + "geolocationOverrideDisableTime": null, + "preparation": null, + "setting": { + "type": "AIR_CONDITIONING", + "power": "ON", + "mode": "HEAT", + "temperature": { + "celsius": 25.0, + "fahrenheit": 77.0 + }, + "fanLevel": "LEVEL3", + "verticalSwing": "ON", + "horizontalSwing": "ON" + }, + "overlayType": "MANUAL", + "overlay": { + "type": "MANUAL", + "setting": { + "type": "AIR_CONDITIONING", + "power": "ON", + "mode": "HEAT", + "temperature": { + "celsius": 25.0, + "fahrenheit": 77.0 + }, + "fanLevel": "LEVEL3", + "verticalSwing": "ON" + }, + "termination": { + "type": "MANUAL", + "typeSkillBasedApp": "MANUAL", + "projectedExpiry": null + } + }, + "openWindow": null, + "nextScheduleChange": { + "start": "2024-07-01T05: 45: 00Z", + "setting": { + "type": "AIR_CONDITIONING", + "power": "ON", + "mode": "HEAT", + "temperature": { + "celsius": 24.0, + "fahrenheit": 75.2 + }, + "fanLevel": "LEVEL3", + "verticalSwing": "ON", + "horizontalSwing": "ON" + } + }, + "nextTimeBlock": { + "start": "2024-07-01T05: 45: 00.000Z" + }, + "link": { + "state": "ONLINE" + }, + "runningOfflineSchedule": false, + "activityDataPoints": { + "acPower": { + "timestamp": "2022-07-13T18: 06: 58.183Z", + "type": "POWER", + "value": "ON" + } + }, + "sensorDataPoints": { + "insideTemperature": { + "celsius": 24.3, + "fahrenheit": 75.74, + "timestamp": "2024-06-28T22: 23: 15.679Z", + "type": "TEMPERATURE", + "precision": { + "celsius": 0.1, + "fahrenheit": 0.1 + } + }, + "humidity": { + "type": "PERCENTAGE", + "percentage": 70.9, + "timestamp": "2024-06-28T22: 23: 15.679Z" + } + }, + "terminationCondition": { + "type": "MANUAL" + } +} diff --git a/tests/components/tado/fixtures/zone_states.json b/tests/components/tado/fixtures/zone_states.json index 64d457f3b50..df1a99a80f3 100644 --- a/tests/components/tado/fixtures/zone_states.json +++ b/tests/components/tado/fixtures/zone_states.json @@ -287,6 +287,79 @@ "timestamp": "2020-03-28T02:09:27.830Z" } } + }, + "6": { + "tadoMode": "HOME", + "geolocationOverride": false, + "geolocationOverrideDisableTime": null, + "preparation": null, + "setting": { + "type": "AIR_CONDITIONING", + "power": "OFF" + }, + "overlayType": "MANUAL", + "overlay": { + "type": "MANUAL", + "setting": { + "type": "AIR_CONDITIONING", + "power": "OFF" + }, + "termination": { + "type": "MANUAL", + "typeSkillBasedApp": "MANUAL", + "projectedExpiry": null + } + }, + "openWindow": null, + "nextScheduleChange": { + "start": "2024-07-01T05: 45: 00Z", + "setting": { + "type": "AIR_CONDITIONING", + "power": "ON", + "mode": "HEAT", + "temperature": { + "celsius": 24.0, + "fahrenheit": 75.2 + }, + "fanLevel": "LEVEL3", + "verticalSwing": "ON", + "horizontalSwing": "ON" + } + }, + "nextTimeBlock": { + "start": "2024-07-01T05: 45: 00.000Z" + }, + "link": { + "state": "ONLINE" + }, + "runningOfflineSchedule": false, + "activityDataPoints": { + "acPower": { + "timestamp": "2022-07-13T18: 06: 58.183Z", + "type": "POWER", + "value": "OFF" + } + }, + "sensorDataPoints": { + "insideTemperature": { + "celsius": 24.21, + "fahrenheit": 75.58, + "timestamp": "2024-06-28T21: 43: 51.067Z", + "type": "TEMPERATURE", + "precision": { + "celsius": 0.1, + "fahrenheit": 0.1 + } + }, + "humidity": { + "type": "PERCENTAGE", + "percentage": 71.4, + "timestamp": "2024-06-28T21: 43: 51.067Z" + } + }, + "terminationCondition": { + "type": "MANUAL" + } } } } diff --git a/tests/components/tado/fixtures/zone_with_fanlevel_horizontal_vertical_swing.json b/tests/components/tado/fixtures/zone_with_fanlevel_horizontal_vertical_swing.json new file mode 100644 index 00000000000..51ba70b4065 --- /dev/null +++ b/tests/components/tado/fixtures/zone_with_fanlevel_horizontal_vertical_swing.json @@ -0,0 +1,130 @@ +{ + "type": "AIR_CONDITIONING", + "COOL": { + "temperatures": { + "celsius": { + "min": 16, + "max": 31, + "step": 1.0 + }, + "fahrenheit": { + "min": 61, + "max": 88, + "step": 1.0 + } + }, + "fanLevel": ["LEVEL3", "LEVEL2", "AUTO", "LEVEL1", "LEVEL4", "LEVEL5"], + "verticalSwing": ["MID_UP", "MID_DOWN", "ON", "OFF", "UP", "MID", "DOWN"], + "horizontalSwing": ["OFF", "ON"], + "light": ["ON", "OFF"] + }, + "FAN": { + "temperatures": { + "celsius": { + "min": 16, + "max": 31, + "step": 1.0 + }, + "fahrenheit": { + "min": 61, + "max": 88, + "step": 1.0 + } + }, + "fanLevel": ["LEVEL3", "LEVEL2", "AUTO", "LEVEL1", "LEVEL4", "LEVEL5"], + "verticalSwing": ["MID_UP", "MID_DOWN", "ON", "OFF", "UP", "MID", "DOWN"], + "horizontalSwing": ["OFF", "ON"], + "light": ["ON", "OFF"] + }, + "AUTO": { + "fanLevel": ["LEVEL3", "LEVEL2", "AUTO", "LEVEL1", "LEVEL4", "LEVEL5"], + "verticalSwing": ["MID_UP", "MID_DOWN", "ON", "OFF", "UP", "MID", "DOWN"], + "horizontalSwing": ["OFF", "ON"], + "light": ["ON", "OFF"] + }, + "HEAT": { + "temperatures": { + "celsius": { + "min": 16, + "max": 31, + "step": 1.0 + }, + "fahrenheit": { + "min": 61, + "max": 88, + "step": 1.0 + } + }, + "fanLevel": ["LEVEL3", "LEVEL2", "AUTO", "LEVEL1", "LEVEL4", "LEVEL5"], + "verticalSwing": ["MID_UP", "MID_DOWN", "ON", "OFF", "UP", "MID", "DOWN"], + "horizontalSwing": ["OFF", "ON"], + "light": ["ON", "OFF"] + }, + "DRY": { + "temperatures": { + "celsius": { + "min": 16, + "max": 31, + "step": 1.0 + }, + "fahrenheit": { + "min": 61, + "max": 88, + "step": 1.0 + } + }, + "verticalSwing": ["MID_UP", "MID_DOWN", "ON", "OFF", "UP", "MID", "DOWN"], + "horizontalSwing": ["OFF", "ON"], + "light": ["ON", "OFF"] + }, + "initialStates": { + "mode": "COOL", + "modes": { + "COOL": { + "temperature": { + "celsius": 24, + "fahrenheit": 75 + }, + "fanLevel": "LEVEL3", + "verticalSwing": "OFF", + "horizontalSwing": "OFF", + "light": "ON" + }, + "HEAT": { + "temperature": { + "celsius": 24, + "fahrenheit": 75 + }, + "fanLevel": "LEVEL3", + "verticalSwing": "OFF", + "horizontalSwing": "OFF", + "light": "ON" + }, + "DRY": { + "temperature": { + "celsius": 24, + "fahrenheit": 75 + }, + "verticalSwing": "OFF", + "horizontalSwing": "OFF", + "light": "ON" + }, + "FAN": { + "temperature": { + "celsius": 24, + "fahrenheit": 75 + }, + "fanLevel": "LEVEL3", + "verticalSwing": "OFF", + "horizontalSwing": "OFF", + "light": "ON" + }, + "AUTO": { + "fanLevel": "LEVEL3", + "verticalSwing": "OFF", + "horizontalSwing": "OFF", + "light": "ON" + } + } + } +} diff --git a/tests/components/tado/fixtures/zones.json b/tests/components/tado/fixtures/zones.json index 5ef7374a660..e1d2ec759ba 100644 --- a/tests/components/tado/fixtures/zones.json +++ b/tests/components/tado/fixtures/zones.json @@ -178,5 +178,45 @@ "deviceTypes": ["WR02"], "reportAvailable": false, "type": "AIR_CONDITIONING" + }, + { + "id": 6, + "name": "Air Conditioning with fanlevel", + "type": "AIR_CONDITIONING", + "dateCreated": "2022-07-13T18: 06: 58.183Z", + "deviceTypes": ["WR02"], + "devices": [ + { + "deviceType": "WR02", + "serialNo": "WR5", + "shortSerialNo": "WR5", + "currentFwVersion": "118.7", + "connectionState": { + "value": true, + "timestamp": "2024-06-28T21: 04: 23.463Z" + }, + "characteristics": { + "capabilities": ["INSIDE_TEMPERATURE_MEASUREMENT", "IDENTIFY"] + }, + "accessPointWiFi": { + "ssid": "tado8480" + }, + "commandTableUploadState": "FINISHED", + "duties": ["ZONE_UI", "ZONE_DRIVER", "ZONE_LEADER"] + } + ], + "reportAvailable": false, + "showScheduleSetup": false, + "supportsDazzle": true, + "dazzleEnabled": true, + "dazzleMode": { + "supported": true, + "enabled": true + }, + "openWindowDetection": { + "supported": true, + "enabled": true, + "timeoutInSeconds": 900 + } } ] diff --git a/tests/components/tado/test_climate.py b/tests/components/tado/test_climate.py index 98fd2d753a4..5a43c728b6e 100644 --- a/tests/components/tado/test_climate.py +++ b/tests/components/tado/test_climate.py @@ -89,3 +89,35 @@ async def test_smartac_with_swing(hass: HomeAssistant) -> None: # Only test for a subset of attributes in case # HA changes the implementation and a new one appears assert all(item in state.attributes.items() for item in expected_attributes.items()) + + +async def test_smartac_with_fanlevel_vertical_and_horizontal_swing( + hass: HomeAssistant, +) -> None: + """Test creation of smart ac with swing climate.""" + + await async_init_integration(hass) + + state = hass.states.get("climate.air_conditioning_with_fanlevel") + assert state.state == "heat" + + expected_attributes = { + "current_humidity": 70.9, + "current_temperature": 24.3, + "fan_mode": "high", + "fan_modes": ["high", "medium", "auto", "low"], + "friendly_name": "Air Conditioning with fanlevel", + "hvac_action": "heating", + "hvac_modes": ["off", "auto", "heat", "cool", "heat_cool", "dry", "fan_only"], + "max_temp": 31.0, + "min_temp": 16.0, + "preset_mode": "auto", + "preset_modes": ["away", "home", "auto"], + "swing_modes": ["vertical", "horizontal", "both", "off"], + "supported_features": 441, + "target_temp_step": 1.0, + "temperature": 25.0, + } + # Only test for a subset of attributes in case + # HA changes the implementation and a new one appears + assert all(item in state.attributes.items() for item in expected_attributes.items()) diff --git a/tests/components/tado/util.py b/tests/components/tado/util.py index dd7c108c984..de4fd515e5a 100644 --- a/tests/components/tado/util.py +++ b/tests/components/tado/util.py @@ -27,6 +27,12 @@ async def async_init_integration( # WR1 Device device_wr1_fixture = "tado/device_wr1.json" + # Smart AC with fanLevel, Vertical and Horizontal swings + zone_6_state_fixture = "tado/smartac4.with_fanlevel.json" + zone_6_capabilities_fixture = ( + "tado/zone_with_fanlevel_horizontal_vertical_swing.json" + ) + # Smart AC with Swing zone_5_state_fixture = "tado/smartac3.with_swing.json" zone_5_capabilities_fixture = "tado/zone_with_swing_capabilities.json" @@ -95,6 +101,10 @@ async def async_init_integration( "https://my.tado.com/api/v2/homes/1/zoneStates", text=load_fixture(zone_states_fixture), ) + m.get( + "https://my.tado.com/api/v2/homes/1/zones/6/capabilities", + text=load_fixture(zone_6_capabilities_fixture), + ) m.get( "https://my.tado.com/api/v2/homes/1/zones/5/capabilities", text=load_fixture(zone_5_capabilities_fixture), @@ -135,6 +145,14 @@ async def async_init_integration( "https://my.tado.com/api/v2/homes/1/zones/5/defaultOverlay", text=load_fixture(zone_def_overlay), ) + m.get( + "https://my.tado.com/api/v2/homes/1/zones/6/defaultOverlay", + text=load_fixture(zone_def_overlay), + ) + m.get( + "https://my.tado.com/api/v2/homes/1/zones/6/state", + text=load_fixture(zone_6_state_fixture), + ) m.get( "https://my.tado.com/api/v2/homes/1/zones/5/state", text=load_fixture(zone_5_state_fixture), From becf9fcce205f0c0e171c3f276f53d5f1e81d556 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Sat, 29 Jun 2024 22:07:37 +0300 Subject: [PATCH 1393/1445] Bump aiowebostv to 0.4.1 (#120838) --- homeassistant/components/webostv/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/webostv/manifest.json b/homeassistant/components/webostv/manifest.json index ed8e1a6cc6e..bcafb82a4b0 100644 --- a/homeassistant/components/webostv/manifest.json +++ b/homeassistant/components/webostv/manifest.json @@ -7,7 +7,7 @@ "iot_class": "local_push", "loggers": ["aiowebostv"], "quality_scale": "platinum", - "requirements": ["aiowebostv==0.4.0"], + "requirements": ["aiowebostv==0.4.1"], "ssdp": [ { "st": "urn:lge-com:service:webos-second-screen:1" diff --git a/requirements_all.txt b/requirements_all.txt index d44ea880636..d15c2d13ae0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -404,7 +404,7 @@ aiowaqi==3.1.0 aiowatttime==0.1.1 # homeassistant.components.webostv -aiowebostv==0.4.0 +aiowebostv==0.4.1 # homeassistant.components.withings aiowithings==3.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b0a6fb54b3a..20a9b828069 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -377,7 +377,7 @@ aiowaqi==3.1.0 aiowatttime==0.1.1 # homeassistant.components.webostv -aiowebostv==0.4.0 +aiowebostv==0.4.1 # homeassistant.components.withings aiowithings==3.0.2 From bcec268c047e30b7fa634070c9f112596679e39f Mon Sep 17 00:00:00 2001 From: Tsvi Mostovicz Date: Sun, 30 Jun 2024 01:08:24 +0300 Subject: [PATCH 1394/1445] Fix Jewish calendar unique id move to entity (#120842) --- homeassistant/components/jewish_calendar/entity.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/jewish_calendar/entity.py b/homeassistant/components/jewish_calendar/entity.py index aba76599f63..c11925df954 100644 --- a/homeassistant/components/jewish_calendar/entity.py +++ b/homeassistant/components/jewish_calendar/entity.py @@ -28,7 +28,7 @@ class JewishCalendarEntity(Entity): ) -> None: """Initialize a Jewish Calendar entity.""" self.entity_description = description - self._attr_unique_id = f"{config_entry.entry_id}_{description.key}" + self._attr_unique_id = f"{config_entry.entry_id}-{description.key}" self._attr_device_info = DeviceInfo( entry_type=DeviceEntryType.SERVICE, identifiers={(DOMAIN, config_entry.entry_id)}, From 4fc89e886198c599007807d8989acfbf9272a1ed Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sat, 29 Jun 2024 21:35:48 -0700 Subject: [PATCH 1395/1445] Rollback PyFlume to 0.6.5 (#120846) --- homeassistant/components/flume/coordinator.py | 2 +- homeassistant/components/flume/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/flume/coordinator.py b/homeassistant/components/flume/coordinator.py index c75bffdc615..30e7962304c 100644 --- a/homeassistant/components/flume/coordinator.py +++ b/homeassistant/components/flume/coordinator.py @@ -98,7 +98,7 @@ class FlumeNotificationDataUpdateCoordinator(DataUpdateCoordinator[None]): # The related binary sensors (leak detected, high flow, low battery) # will be active until the notification is deleted in the Flume app. self.notifications = pyflume.FlumeNotificationList( - self.auth, read=None, sort_direction="DESC" + self.auth, read=None ).notification_list _LOGGER.debug("Notifications %s", self.notifications) diff --git a/homeassistant/components/flume/manifest.json b/homeassistant/components/flume/manifest.json index bb6783bafbe..953d9791f2f 100644 --- a/homeassistant/components/flume/manifest.json +++ b/homeassistant/components/flume/manifest.json @@ -11,5 +11,5 @@ "documentation": "https://www.home-assistant.io/integrations/flume", "iot_class": "cloud_polling", "loggers": ["pyflume"], - "requirements": ["PyFlume==0.8.7"] + "requirements": ["PyFlume==0.6.5"] } diff --git a/requirements_all.txt b/requirements_all.txt index d15c2d13ae0..d7a7ecb6eb5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -54,7 +54,7 @@ PyChromecast==14.0.1 PyFlick==0.0.2 # homeassistant.components.flume -PyFlume==0.8.7 +PyFlume==0.6.5 # homeassistant.components.fronius PyFronius==0.7.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 20a9b828069..507b40f8e47 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -45,7 +45,7 @@ PyChromecast==14.0.1 PyFlick==0.0.2 # homeassistant.components.flume -PyFlume==0.8.7 +PyFlume==0.6.5 # homeassistant.components.fronius PyFronius==0.7.3 From af733425c2d85d3ae559ee3f272cca7bdbddfd1a Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Sun, 30 Jun 2024 14:56:12 +0200 Subject: [PATCH 1396/1445] Bump pyfritzhome to 0.6.12 (#120861) --- homeassistant/components/fritzbox/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/fritzbox/manifest.json b/homeassistant/components/fritzbox/manifest.json index de2e9e0200a..3735c16571e 100644 --- a/homeassistant/components/fritzbox/manifest.json +++ b/homeassistant/components/fritzbox/manifest.json @@ -8,7 +8,7 @@ "iot_class": "local_polling", "loggers": ["pyfritzhome"], "quality_scale": "gold", - "requirements": ["pyfritzhome==0.6.11"], + "requirements": ["pyfritzhome==0.6.12"], "ssdp": [ { "st": "urn:schemas-upnp-org:device:fritzbox:1" diff --git a/requirements_all.txt b/requirements_all.txt index d7a7ecb6eb5..dd68902baae 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1866,7 +1866,7 @@ pyforked-daapd==0.1.14 pyfreedompro==1.1.0 # homeassistant.components.fritzbox -pyfritzhome==0.6.11 +pyfritzhome==0.6.12 # homeassistant.components.ifttt pyfttt==0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 507b40f8e47..54e86d60186 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1465,7 +1465,7 @@ pyforked-daapd==0.1.14 pyfreedompro==1.1.0 # homeassistant.components.fritzbox -pyfritzhome==0.6.11 +pyfritzhome==0.6.12 # homeassistant.components.ifttt pyfttt==0.3 From 14af3661f3fe68332bbdee929c1b6586264f384d Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 30 Jun 2024 20:42:10 +0200 Subject: [PATCH 1397/1445] Bump version to 2024.7.0b6 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index fa19aa7349e..e97f14f830c 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -24,7 +24,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 7 -PATCH_VERSION: Final = "0b5" +PATCH_VERSION: Final = "0b6" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0) diff --git a/pyproject.toml b/pyproject.toml index 3b42dfa2d6b..0f4b25eb0cc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.7.0b5" +version = "2024.7.0b6" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 3bbf8df6d640bbc748b942996196c124e2585215 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Wed, 26 Jun 2024 23:57:41 +0200 Subject: [PATCH 1398/1445] Cleanup mqtt platform tests part 4 (init) (#120574) --- tests/components/mqtt/test_init.py | 91 ++++++++---------------------- 1 file changed, 23 insertions(+), 68 deletions(-) diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index 231379601c6..bcadf4a6506 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -5,7 +5,6 @@ from copy import deepcopy from datetime import datetime, timedelta from functools import partial import json -import logging import socket import ssl import time @@ -16,15 +15,11 @@ import certifi from freezegun.api import FrozenDateTimeFactory import paho.mqtt.client as paho_mqtt import pytest -from typing_extensions import Generator import voluptuous as vol from homeassistant.components import mqtt from homeassistant.components.mqtt import debug_info -from homeassistant.components.mqtt.client import ( - _LOGGER as CLIENT_LOGGER, - RECONNECT_INTERVAL_SECONDS, -) +from homeassistant.components.mqtt.client import RECONNECT_INTERVAL_SECONDS from homeassistant.components.mqtt.models import ( MessageCallbackType, MqttCommandTemplateException, @@ -100,15 +95,6 @@ def mock_storage(hass_storage: dict[str, Any]) -> None: """Autouse hass_storage for the TestCase tests.""" -@pytest.fixture -def client_debug_log() -> Generator[None]: - """Set the mqtt client log level to DEBUG.""" - logger = logging.getLogger("mqtt_client_tests_debug") - logger.setLevel(logging.DEBUG) - with patch.object(CLIENT_LOGGER, "parent", logger): - yield - - def help_assert_message( msg: ReceiveMessage, topic: str | None = None, @@ -130,8 +116,7 @@ def help_assert_message( async def test_mqtt_connects_on_home_assistant_mqtt_setup( - hass: HomeAssistant, - setup_with_birth_msg_client_mock: MqttMockPahoClient, + hass: HomeAssistant, setup_with_birth_msg_client_mock: MqttMockPahoClient ) -> None: """Test if client is connected after mqtt init on bootstrap.""" mqtt_client_mock = setup_with_birth_msg_client_mock @@ -150,9 +135,7 @@ async def test_mqtt_does_not_disconnect_on_home_assistant_stop( assert mqtt_client_mock.disconnect.call_count == 0 -async def test_mqtt_await_ack_at_disconnect( - hass: HomeAssistant, -) -> None: +async def test_mqtt_await_ack_at_disconnect(hass: HomeAssistant) -> None: """Test if ACK is awaited correctly when disconnecting.""" class FakeInfo: @@ -208,8 +191,7 @@ async def test_mqtt_await_ack_at_disconnect( @pytest.mark.parametrize("mqtt_config_entry_data", [ENTRY_DEFAULT_BIRTH_MESSAGE]) async def test_publish( - hass: HomeAssistant, - setup_with_birth_msg_client_mock: MqttMockPahoClient, + hass: HomeAssistant, setup_with_birth_msg_client_mock: MqttMockPahoClient ) -> None: """Test the publish function.""" publish_mock: MagicMock = setup_with_birth_msg_client_mock.publish @@ -340,9 +322,7 @@ async def test_command_template_value(hass: HomeAssistant) -> None: ], ) async def test_command_template_variables( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - config: ConfigType, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, config: ConfigType ) -> None: """Test the rendering of entity variables.""" topic = "test/select" @@ -888,7 +868,7 @@ def test_entity_device_info_schema() -> None: {"identifiers": [], "connections": [], "name": "Beer"} ) - # not an valid URL + # not a valid URL with pytest.raises(vol.Invalid): MQTT_ENTITY_DEVICE_INFO_SCHEMA( { @@ -1049,10 +1029,9 @@ async def test_subscribe_topic( unsub() +@pytest.mark.usefixtures("mqtt_mock_entry") async def test_subscribe_topic_not_initialize( - hass: HomeAssistant, - record_calls: MessageCallbackType, - mqtt_mock_entry: MqttMockHAClientGenerator, + hass: HomeAssistant, record_calls: MessageCallbackType ) -> None: """Test the subscription of a topic when MQTT was not initialized.""" with pytest.raises( @@ -1084,7 +1063,6 @@ async def test_subscribe_mqtt_config_entry_disabled( async def test_subscribe_and_resubscribe( hass: HomeAssistant, - client_debug_log: None, mock_debouncer: asyncio.Event, setup_with_birth_msg_client_mock: MqttMockPahoClient, recorded_calls: list[ReceiveMessage], @@ -1892,10 +1870,10 @@ async def test_subscribed_at_highest_qos( assert help_all_subscribe_calls(mqtt_client_mock) == [("test/state", 2)] +@pytest.mark.usefixtures("mqtt_client_mock") async def test_reload_entry_with_restored_subscriptions( hass: HomeAssistant, mock_debouncer: asyncio.Event, - mqtt_client_mock: MqttMockPahoClient, record_calls: MessageCallbackType, recorded_calls: list[ReceiveMessage], ) -> None: @@ -1995,7 +1973,6 @@ async def test_logs_error_if_no_connect_broker( @pytest.mark.parametrize("return_code", [4, 5]) async def test_triggers_reauth_flow_if_auth_fails( hass: HomeAssistant, - caplog: pytest.LogCaptureFixture, setup_with_birth_msg_client_mock: MqttMockPahoClient, return_code: int, ) -> None: @@ -2132,9 +2109,7 @@ async def test_handle_message_callback( ], ) async def test_setup_manual_mqtt_with_platform_key( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + mqtt_mock_entry: MqttMockHAClientGenerator, caplog: pytest.LogCaptureFixture ) -> None: """Test set up a manual MQTT item with a platform key.""" assert await mqtt_mock_entry() @@ -2146,9 +2121,7 @@ async def test_setup_manual_mqtt_with_platform_key( @pytest.mark.parametrize("hass_config", [{mqtt.DOMAIN: {"light": {"name": "test"}}}]) async def test_setup_manual_mqtt_with_invalid_config( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + mqtt_mock_entry: MqttMockHAClientGenerator, caplog: pytest.LogCaptureFixture ) -> None: """Test set up a manual MQTT item with an invalid config.""" assert await mqtt_mock_entry() @@ -2182,9 +2155,7 @@ async def test_setup_manual_mqtt_with_invalid_config( ], ) async def test_setup_mqtt_client_protocol( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - protocol: int, + mqtt_mock_entry: MqttMockHAClientGenerator, protocol: int ) -> None: """Test MQTT client protocol setup.""" with patch( @@ -2383,8 +2354,7 @@ async def test_custom_birth_message( [ENTRY_DEFAULT_BIRTH_MESSAGE], ) async def test_default_birth_message( - hass: HomeAssistant, - setup_with_birth_msg_client_mock: MqttMockPahoClient, + hass: HomeAssistant, setup_with_birth_msg_client_mock: MqttMockPahoClient ) -> None: """Test sending birth message.""" mqtt_client_mock = setup_with_birth_msg_client_mock @@ -2470,10 +2440,7 @@ async def test_delayed_birth_message( [ENTRY_DEFAULT_BIRTH_MESSAGE], ) async def test_subscription_done_when_birth_message_is_sent( - hass: HomeAssistant, - mqtt_config_entry_data: dict[str, Any], setup_with_birth_msg_client_mock: MqttMockPahoClient, - record_calls: MessageCallbackType, ) -> None: """Test sending birth message until initial subscription has been completed.""" mqtt_client_mock = setup_with_birth_msg_client_mock @@ -2517,7 +2484,6 @@ async def test_custom_will_message( async def test_default_will_message( - hass: HomeAssistant, setup_with_birth_msg_client_mock: MqttMockPahoClient, ) -> None: """Test will message.""" @@ -2647,11 +2613,9 @@ async def test_mqtt_subscribes_and_unsubscribes_in_chunks( assert len(mqtt_client_mock.unsubscribe.mock_calls[1][1][0]) == 2 +@pytest.mark.usefixtures("mqtt_client_mock") async def test_default_entry_setting_are_applied( - hass: HomeAssistant, - device_registry: dr.DeviceRegistry, - mqtt_client_mock: MqttMockPahoClient, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, device_registry: dr.DeviceRegistry ) -> None: """Test if the MQTT component loads when config entry data not has all default settings.""" data = ( @@ -2704,11 +2668,9 @@ async def test_message_callback_exception_gets_logged( @pytest.mark.no_fail_on_log_exception +@pytest.mark.usefixtures("mock_debouncer", "setup_with_birth_msg_client_mock") async def test_message_partial_callback_exception_gets_logged( - hass: HomeAssistant, - caplog: pytest.LogCaptureFixture, - mock_debouncer: asyncio.Event, - setup_with_birth_msg_client_mock: MqttMockPahoClient, + hass: HomeAssistant, caplog: pytest.LogCaptureFixture, mock_debouncer: asyncio.Event ) -> None: """Test exception raised by message handler.""" @@ -3730,9 +3692,7 @@ async def test_setup_manual_items_with_unique_ids( ], ) async def test_link_config_entry( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test manual and dynamically setup entities are linked to the config entry.""" # set up manual item @@ -3818,9 +3778,7 @@ async def test_link_config_entry( ], ) async def test_reload_config_entry( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test manual entities reloaded and set up correctly.""" await mqtt_mock_entry() @@ -3966,8 +3924,7 @@ async def test_reload_config_entry( ], ) async def test_reload_with_invalid_config( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test reloading yaml config fails.""" await mqtt_mock_entry() @@ -4007,8 +3964,7 @@ async def test_reload_with_invalid_config( ], ) async def test_reload_with_empty_config( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test reloading yaml config fails.""" await mqtt_mock_entry() @@ -4043,8 +3999,7 @@ async def test_reload_with_empty_config( ], ) async def test_reload_with_new_platform_config( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test reloading yaml with new platform config.""" await mqtt_mock_entry() @@ -4389,6 +4344,6 @@ async def test_loop_write_failure( "valid_subscribe_topic", ], ) -async def test_mqtt_integration_level_imports(hass: HomeAssistant, attr: str) -> None: +async def test_mqtt_integration_level_imports(attr: str) -> None: """Test mqtt integration level public published imports are available.""" assert hasattr(mqtt, attr) From 40384b9acdfac3eab46ebe19e902b89de3a6d755 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Thu, 27 Jun 2024 19:37:43 +0200 Subject: [PATCH 1399/1445] Split mqtt client tests (#120636) --- tests/components/mqtt/test_client.py | 1980 ++++++++++++++++++++++++++ tests/components/mqtt/test_init.py | 1962 +------------------------ 2 files changed, 1983 insertions(+), 1959 deletions(-) create mode 100644 tests/components/mqtt/test_client.py diff --git a/tests/components/mqtt/test_client.py b/tests/components/mqtt/test_client.py new file mode 100644 index 00000000000..49b590383d1 --- /dev/null +++ b/tests/components/mqtt/test_client.py @@ -0,0 +1,1980 @@ +"""The tests for the MQTT client.""" + +import asyncio +from datetime import datetime, timedelta +import socket +import ssl +from typing import Any +from unittest.mock import MagicMock, Mock, call, patch + +import certifi +import paho.mqtt.client as paho_mqtt +import pytest + +from homeassistant.components import mqtt +from homeassistant.components.mqtt.client import RECONNECT_INTERVAL_SECONDS +from homeassistant.components.mqtt.models import MessageCallbackType, ReceiveMessage +from homeassistant.config_entries import ConfigEntryDisabler, ConfigEntryState +from homeassistant.const import ( + CONF_PROTOCOL, + EVENT_HOMEASSISTANT_STARTED, + EVENT_HOMEASSISTANT_STOP, + UnitOfTemperature, +) +from homeassistant.core import CALLBACK_TYPE, CoreState, HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError +from homeassistant.util.dt import utcnow + +from .conftest import ENTRY_DEFAULT_BIRTH_MESSAGE +from .test_common import help_all_subscribe_calls + +from tests.common import ( + MockConfigEntry, + async_fire_mqtt_message, + async_fire_time_changed, +) +from tests.typing import MqttMockHAClient, MqttMockHAClientGenerator, MqttMockPahoClient + + +@pytest.fixture(autouse=True) +def mock_storage(hass_storage: dict[str, Any]) -> None: + """Autouse hass_storage for the TestCase tests.""" + + +def help_assert_message( + msg: ReceiveMessage, + topic: str | None = None, + payload: str | None = None, + qos: int | None = None, + retain: bool | None = None, +) -> bool: + """Return True if all of the given attributes match with the message.""" + match: bool = True + if topic is not None: + match &= msg.topic == topic + if payload is not None: + match &= msg.payload == payload + if qos is not None: + match &= msg.qos == qos + if retain is not None: + match &= msg.retain == retain + return match + + +async def test_mqtt_connects_on_home_assistant_mqtt_setup( + hass: HomeAssistant, setup_with_birth_msg_client_mock: MqttMockPahoClient +) -> None: + """Test if client is connected after mqtt init on bootstrap.""" + mqtt_client_mock = setup_with_birth_msg_client_mock + assert mqtt_client_mock.connect.call_count == 1 + + +async def test_mqtt_does_not_disconnect_on_home_assistant_stop( + hass: HomeAssistant, + mock_debouncer: asyncio.Event, + setup_with_birth_msg_client_mock: MqttMockPahoClient, +) -> None: + """Test if client is not disconnected on HA stop.""" + mqtt_client_mock = setup_with_birth_msg_client_mock + hass.bus.fire(EVENT_HOMEASSISTANT_STOP) + await mock_debouncer.wait() + assert mqtt_client_mock.disconnect.call_count == 0 + + +async def test_mqtt_await_ack_at_disconnect(hass: HomeAssistant) -> None: + """Test if ACK is awaited correctly when disconnecting.""" + + class FakeInfo: + """Returns a simulated client publish response.""" + + mid = 100 + rc = 0 + + with patch( + "homeassistant.components.mqtt.async_client.AsyncMQTTClient" + ) as mock_client: + mqtt_client = mock_client.return_value + mqtt_client.connect = MagicMock( + return_value=0, + side_effect=lambda *args, **kwargs: hass.loop.call_soon_threadsafe( + mqtt_client.on_connect, mqtt_client, None, 0, 0, 0 + ), + ) + mqtt_client.publish = MagicMock(return_value=FakeInfo()) + entry = MockConfigEntry( + domain=mqtt.DOMAIN, + data={ + "certificate": "auto", + mqtt.CONF_BROKER: "test-broker", + mqtt.CONF_DISCOVERY: False, + }, + ) + entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(entry.entry_id) + + mqtt_client = mock_client.return_value + + # publish from MQTT client without awaiting + hass.async_create_task( + mqtt.async_publish(hass, "test-topic", "some-payload", 0, False) + ) + await asyncio.sleep(0) + # Simulate late ACK callback from client with mid 100 + mqtt_client.on_publish(0, 0, 100) + # disconnect the MQTT client + await hass.async_stop() + await hass.async_block_till_done() + # assert the payload was sent through the client + assert mqtt_client.publish.called + assert mqtt_client.publish.call_args[0] == ( + "test-topic", + "some-payload", + 0, + False, + ) + await hass.async_block_till_done(wait_background_tasks=True) + + +@pytest.mark.parametrize("mqtt_config_entry_data", [ENTRY_DEFAULT_BIRTH_MESSAGE]) +async def test_publish( + hass: HomeAssistant, setup_with_birth_msg_client_mock: MqttMockPahoClient +) -> None: + """Test the publish function.""" + publish_mock: MagicMock = setup_with_birth_msg_client_mock.publish + await mqtt.async_publish(hass, "test-topic", "test-payload") + await hass.async_block_till_done() + assert publish_mock.called + assert publish_mock.call_args[0] == ( + "test-topic", + "test-payload", + 0, + False, + ) + publish_mock.reset_mock() + + await mqtt.async_publish(hass, "test-topic", "test-payload", 2, True) + await hass.async_block_till_done() + assert publish_mock.called + assert publish_mock.call_args[0] == ( + "test-topic", + "test-payload", + 2, + True, + ) + publish_mock.reset_mock() + + mqtt.publish(hass, "test-topic2", "test-payload2") + await hass.async_block_till_done() + assert publish_mock.called + assert publish_mock.call_args[0] == ( + "test-topic2", + "test-payload2", + 0, + False, + ) + publish_mock.reset_mock() + + mqtt.publish(hass, "test-topic2", "test-payload2", 2, True) + await hass.async_block_till_done() + assert publish_mock.called + assert publish_mock.call_args[0] == ( + "test-topic2", + "test-payload2", + 2, + True, + ) + publish_mock.reset_mock() + + # test binary pass-through + mqtt.publish( + hass, + "test-topic3", + b"\xde\xad\xbe\xef", + 0, + False, + ) + await hass.async_block_till_done() + assert publish_mock.called + assert publish_mock.call_args[0] == ( + "test-topic3", + b"\xde\xad\xbe\xef", + 0, + False, + ) + publish_mock.reset_mock() + + # test null payload + mqtt.publish( + hass, + "test-topic3", + None, + 0, + False, + ) + await hass.async_block_till_done() + assert publish_mock.called + assert publish_mock.call_args[0] == ( + "test-topic3", + None, + 0, + False, + ) + + publish_mock.reset_mock() + + +async def test_convert_outgoing_payload(hass: HomeAssistant) -> None: + """Test the converting of outgoing MQTT payloads without template.""" + command_template = mqtt.MqttCommandTemplate(None, hass=hass) + assert command_template.async_render(b"\xde\xad\xbe\xef") == b"\xde\xad\xbe\xef" + assert ( + command_template.async_render("b'\\xde\\xad\\xbe\\xef'") + == "b'\\xde\\xad\\xbe\\xef'" + ) + assert command_template.async_render(1234) == 1234 + assert command_template.async_render(1234.56) == 1234.56 + assert command_template.async_render(None) is None + + +async def test_all_subscriptions_run_when_decode_fails( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + recorded_calls: list[ReceiveMessage], + record_calls: MessageCallbackType, +) -> None: + """Test all other subscriptions still run when decode fails for one.""" + await mqtt_mock_entry() + await mqtt.async_subscribe(hass, "test-topic", record_calls, encoding="ascii") + await mqtt.async_subscribe(hass, "test-topic", record_calls) + + async_fire_mqtt_message(hass, "test-topic", UnitOfTemperature.CELSIUS) + + await hass.async_block_till_done() + assert len(recorded_calls) == 1 + + +async def test_subscribe_topic( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + recorded_calls: list[ReceiveMessage], + record_calls: MessageCallbackType, +) -> None: + """Test the subscription of a topic.""" + await mqtt_mock_entry() + unsub = await mqtt.async_subscribe(hass, "test-topic", record_calls) + + async_fire_mqtt_message(hass, "test-topic", "test-payload") + + await hass.async_block_till_done() + assert len(recorded_calls) == 1 + assert recorded_calls[0].topic == "test-topic" + assert recorded_calls[0].payload == "test-payload" + + unsub() + + async_fire_mqtt_message(hass, "test-topic", "test-payload") + + await hass.async_block_till_done() + assert len(recorded_calls) == 1 + + # Cannot unsubscribe twice + with pytest.raises(HomeAssistantError): + unsub() + + +@pytest.mark.usefixtures("mqtt_mock_entry") +async def test_subscribe_topic_not_initialize( + hass: HomeAssistant, record_calls: MessageCallbackType +) -> None: + """Test the subscription of a topic when MQTT was not initialized.""" + with pytest.raises( + HomeAssistantError, match=r".*make sure MQTT is set up correctly" + ): + await mqtt.async_subscribe(hass, "test-topic", record_calls) + + +async def test_subscribe_mqtt_config_entry_disabled( + hass: HomeAssistant, mqtt_mock: MqttMockHAClient, record_calls: MessageCallbackType +) -> None: + """Test the subscription of a topic when MQTT config entry is disabled.""" + mqtt_mock.connected = True + + mqtt_config_entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0] + assert mqtt_config_entry.state is ConfigEntryState.LOADED + + assert await hass.config_entries.async_unload(mqtt_config_entry.entry_id) + assert mqtt_config_entry.state is ConfigEntryState.NOT_LOADED + + await hass.config_entries.async_set_disabled_by( + mqtt_config_entry.entry_id, ConfigEntryDisabler.USER + ) + mqtt_mock.connected = False + + with pytest.raises(HomeAssistantError, match=r".*MQTT is not enabled"): + await mqtt.async_subscribe(hass, "test-topic", record_calls) + + +async def test_subscribe_and_resubscribe( + hass: HomeAssistant, + mock_debouncer: asyncio.Event, + setup_with_birth_msg_client_mock: MqttMockPahoClient, + recorded_calls: list[ReceiveMessage], + record_calls: MessageCallbackType, +) -> None: + """Test resubscribing within the debounce time.""" + mqtt_client_mock = setup_with_birth_msg_client_mock + with ( + patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.4), + patch("homeassistant.components.mqtt.client.UNSUBSCRIBE_COOLDOWN", 0.4), + ): + mock_debouncer.clear() + unsub = await mqtt.async_subscribe(hass, "test-topic", record_calls) + # This unsub will be un-done with the following subscribe + # unsubscribe should not be called at the broker + unsub() + unsub = await mqtt.async_subscribe(hass, "test-topic", record_calls) + await mock_debouncer.wait() + mock_debouncer.clear() + + async_fire_mqtt_message(hass, "test-topic", "test-payload") + + assert len(recorded_calls) == 1 + assert recorded_calls[0].topic == "test-topic" + assert recorded_calls[0].payload == "test-payload" + # assert unsubscribe was not called + mqtt_client_mock.unsubscribe.assert_not_called() + + mock_debouncer.clear() + unsub() + + await mock_debouncer.wait() + mqtt_client_mock.unsubscribe.assert_called_once_with(["test-topic"]) + + +async def test_subscribe_topic_non_async( + hass: HomeAssistant, + mock_debouncer: asyncio.Event, + mqtt_mock_entry: MqttMockHAClientGenerator, + recorded_calls: list[ReceiveMessage], + record_calls: MessageCallbackType, +) -> None: + """Test the subscription of a topic using the non-async function.""" + await mqtt_mock_entry() + await mock_debouncer.wait() + mock_debouncer.clear() + unsub = await hass.async_add_executor_job( + mqtt.subscribe, hass, "test-topic", record_calls + ) + await mock_debouncer.wait() + + async_fire_mqtt_message(hass, "test-topic", "test-payload") + + assert len(recorded_calls) == 1 + assert recorded_calls[0].topic == "test-topic" + assert recorded_calls[0].payload == "test-payload" + + mock_debouncer.clear() + await hass.async_add_executor_job(unsub) + await mock_debouncer.wait() + + async_fire_mqtt_message(hass, "test-topic", "test-payload") + + assert len(recorded_calls) == 1 + + +async def test_subscribe_bad_topic( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + record_calls: MessageCallbackType, +) -> None: + """Test the subscription of a topic.""" + await mqtt_mock_entry() + with pytest.raises(HomeAssistantError): + await mqtt.async_subscribe(hass, 55, record_calls) # type: ignore[arg-type] + + +async def test_subscribe_topic_not_match( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + recorded_calls: list[ReceiveMessage], + record_calls: MessageCallbackType, +) -> None: + """Test if subscribed topic is not a match.""" + await mqtt_mock_entry() + await mqtt.async_subscribe(hass, "test-topic", record_calls) + + async_fire_mqtt_message(hass, "another-test-topic", "test-payload") + + await hass.async_block_till_done() + assert len(recorded_calls) == 0 + + +async def test_subscribe_topic_level_wildcard( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + recorded_calls: list[ReceiveMessage], + record_calls: MessageCallbackType, +) -> None: + """Test the subscription of wildcard topics.""" + await mqtt_mock_entry() + await mqtt.async_subscribe(hass, "test-topic/+/on", record_calls) + + async_fire_mqtt_message(hass, "test-topic/bier/on", "test-payload") + + await hass.async_block_till_done() + assert len(recorded_calls) == 1 + assert recorded_calls[0].topic == "test-topic/bier/on" + assert recorded_calls[0].payload == "test-payload" + + +async def test_subscribe_topic_level_wildcard_no_subtree_match( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + recorded_calls: list[ReceiveMessage], + record_calls: MessageCallbackType, +) -> None: + """Test the subscription of wildcard topics.""" + await mqtt_mock_entry() + await mqtt.async_subscribe(hass, "test-topic/+/on", record_calls) + + async_fire_mqtt_message(hass, "test-topic/bier", "test-payload") + + await hass.async_block_till_done() + assert len(recorded_calls) == 0 + + +async def test_subscribe_topic_level_wildcard_root_topic_no_subtree_match( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + recorded_calls: list[ReceiveMessage], + record_calls: MessageCallbackType, +) -> None: + """Test the subscription of wildcard topics.""" + await mqtt_mock_entry() + await mqtt.async_subscribe(hass, "test-topic/#", record_calls) + + async_fire_mqtt_message(hass, "test-topic-123", "test-payload") + + await hass.async_block_till_done() + assert len(recorded_calls) == 0 + + +async def test_subscribe_topic_subtree_wildcard_subtree_topic( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + recorded_calls: list[ReceiveMessage], + record_calls: MessageCallbackType, +) -> None: + """Test the subscription of wildcard topics.""" + await mqtt_mock_entry() + await mqtt.async_subscribe(hass, "test-topic/#", record_calls) + + async_fire_mqtt_message(hass, "test-topic/bier/on", "test-payload") + + await hass.async_block_till_done() + assert len(recorded_calls) == 1 + assert recorded_calls[0].topic == "test-topic/bier/on" + assert recorded_calls[0].payload == "test-payload" + + +async def test_subscribe_topic_subtree_wildcard_root_topic( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + recorded_calls: list[ReceiveMessage], + record_calls: MessageCallbackType, +) -> None: + """Test the subscription of wildcard topics.""" + await mqtt_mock_entry() + await mqtt.async_subscribe(hass, "test-topic/#", record_calls) + + async_fire_mqtt_message(hass, "test-topic", "test-payload") + + await hass.async_block_till_done() + assert len(recorded_calls) == 1 + assert recorded_calls[0].topic == "test-topic" + assert recorded_calls[0].payload == "test-payload" + + +async def test_subscribe_topic_subtree_wildcard_no_match( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + recorded_calls: list[ReceiveMessage], + record_calls: MessageCallbackType, +) -> None: + """Test the subscription of wildcard topics.""" + await mqtt_mock_entry() + await mqtt.async_subscribe(hass, "test-topic/#", record_calls) + + async_fire_mqtt_message(hass, "another-test-topic", "test-payload") + + await hass.async_block_till_done() + assert len(recorded_calls) == 0 + + +async def test_subscribe_topic_level_wildcard_and_wildcard_root_topic( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + recorded_calls: list[ReceiveMessage], + record_calls: MessageCallbackType, +) -> None: + """Test the subscription of wildcard topics.""" + await mqtt_mock_entry() + await mqtt.async_subscribe(hass, "+/test-topic/#", record_calls) + + async_fire_mqtt_message(hass, "hi/test-topic", "test-payload") + + await hass.async_block_till_done() + assert len(recorded_calls) == 1 + assert recorded_calls[0].topic == "hi/test-topic" + assert recorded_calls[0].payload == "test-payload" + + +async def test_subscribe_topic_level_wildcard_and_wildcard_subtree_topic( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + recorded_calls: list[ReceiveMessage], + record_calls: MessageCallbackType, +) -> None: + """Test the subscription of wildcard topics.""" + await mqtt_mock_entry() + await mqtt.async_subscribe(hass, "+/test-topic/#", record_calls) + + async_fire_mqtt_message(hass, "hi/test-topic/here-iam", "test-payload") + + await hass.async_block_till_done() + assert len(recorded_calls) == 1 + assert recorded_calls[0].topic == "hi/test-topic/here-iam" + assert recorded_calls[0].payload == "test-payload" + + +async def test_subscribe_topic_level_wildcard_and_wildcard_level_no_match( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + recorded_calls: list[ReceiveMessage], + record_calls: MessageCallbackType, +) -> None: + """Test the subscription of wildcard topics.""" + await mqtt_mock_entry() + await mqtt.async_subscribe(hass, "+/test-topic/#", record_calls) + + async_fire_mqtt_message(hass, "hi/here-iam/test-topic", "test-payload") + + await hass.async_block_till_done() + assert len(recorded_calls) == 0 + + +async def test_subscribe_topic_level_wildcard_and_wildcard_no_match( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + recorded_calls: list[ReceiveMessage], + record_calls: MessageCallbackType, +) -> None: + """Test the subscription of wildcard topics.""" + await mqtt_mock_entry() + await mqtt.async_subscribe(hass, "+/test-topic/#", record_calls) + + async_fire_mqtt_message(hass, "hi/another-test-topic", "test-payload") + + await hass.async_block_till_done() + assert len(recorded_calls) == 0 + + +async def test_subscribe_topic_sys_root( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + recorded_calls: list[ReceiveMessage], + record_calls: MessageCallbackType, +) -> None: + """Test the subscription of $ root topics.""" + await mqtt_mock_entry() + await mqtt.async_subscribe(hass, "$test-topic/subtree/on", record_calls) + + async_fire_mqtt_message(hass, "$test-topic/subtree/on", "test-payload") + + await hass.async_block_till_done() + assert len(recorded_calls) == 1 + assert recorded_calls[0].topic == "$test-topic/subtree/on" + assert recorded_calls[0].payload == "test-payload" + + +async def test_subscribe_topic_sys_root_and_wildcard_topic( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + recorded_calls: list[ReceiveMessage], + record_calls: MessageCallbackType, +) -> None: + """Test the subscription of $ root and wildcard topics.""" + await mqtt_mock_entry() + await mqtt.async_subscribe(hass, "$test-topic/#", record_calls) + + async_fire_mqtt_message(hass, "$test-topic/some-topic", "test-payload") + + await hass.async_block_till_done() + assert len(recorded_calls) == 1 + assert recorded_calls[0].topic == "$test-topic/some-topic" + assert recorded_calls[0].payload == "test-payload" + + +async def test_subscribe_topic_sys_root_and_wildcard_subtree_topic( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + recorded_calls: list[ReceiveMessage], + record_calls: MessageCallbackType, +) -> None: + """Test the subscription of $ root and wildcard subtree topics.""" + await mqtt_mock_entry() + await mqtt.async_subscribe(hass, "$test-topic/subtree/#", record_calls) + + async_fire_mqtt_message(hass, "$test-topic/subtree/some-topic", "test-payload") + + await hass.async_block_till_done() + assert len(recorded_calls) == 1 + assert recorded_calls[0].topic == "$test-topic/subtree/some-topic" + assert recorded_calls[0].payload == "test-payload" + + +async def test_subscribe_special_characters( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + recorded_calls: list[ReceiveMessage], + record_calls: MessageCallbackType, +) -> None: + """Test the subscription to topics with special characters.""" + await mqtt_mock_entry() + topic = "/test-topic/$(.)[^]{-}" + payload = "p4y.l[]a|> ?" + + await mqtt.async_subscribe(hass, topic, record_calls) + + async_fire_mqtt_message(hass, topic, payload) + await hass.async_block_till_done() + assert len(recorded_calls) == 1 + assert recorded_calls[0].topic == topic + assert recorded_calls[0].payload == payload + + +async def test_subscribe_same_topic( + hass: HomeAssistant, + mock_debouncer: asyncio.Event, + setup_with_birth_msg_client_mock: MqttMockPahoClient, +) -> None: + """Test subscribing to same topic twice and simulate retained messages. + + When subscribing to the same topic again, SUBSCRIBE must be sent to the broker again + for it to resend any retained messages. + """ + mqtt_client_mock = setup_with_birth_msg_client_mock + calls_a: list[ReceiveMessage] = [] + calls_b: list[ReceiveMessage] = [] + + @callback + def _callback_a(msg: ReceiveMessage) -> None: + calls_a.append(msg) + + @callback + def _callback_b(msg: ReceiveMessage) -> None: + calls_b.append(msg) + + mqtt_client_mock.reset_mock() + mock_debouncer.clear() + await mqtt.async_subscribe(hass, "test/state", _callback_a, qos=0) + # Simulate a non retained message after the first subscription + async_fire_mqtt_message(hass, "test/state", "online", qos=0, retain=False) + await mock_debouncer.wait() + assert len(calls_a) == 1 + mqtt_client_mock.subscribe.assert_called() + calls_a = [] + mqtt_client_mock.reset_mock() + + await hass.async_block_till_done() + mock_debouncer.clear() + await mqtt.async_subscribe(hass, "test/state", _callback_b, qos=1) + # Simulate an other non retained message after the second subscription + async_fire_mqtt_message(hass, "test/state", "online", qos=0, retain=False) + await mock_debouncer.wait() + # Both subscriptions should receive updates + assert len(calls_a) == 1 + assert len(calls_b) == 1 + mqtt_client_mock.subscribe.assert_called() + + +async def test_replaying_payload_same_topic( + hass: HomeAssistant, + mock_debouncer: asyncio.Event, + setup_with_birth_msg_client_mock: MqttMockPahoClient, +) -> None: + """Test replaying retained messages. + + When subscribing to the same topic again, SUBSCRIBE must be sent to the broker again + for it to resend any retained messages for new subscriptions. + Retained messages must only be replayed for new subscriptions, except + when the MQTT client is reconnecting. + """ + mqtt_client_mock = setup_with_birth_msg_client_mock + calls_a: list[ReceiveMessage] = [] + calls_b: list[ReceiveMessage] = [] + + @callback + def _callback_a(msg: ReceiveMessage) -> None: + calls_a.append(msg) + + @callback + def _callback_b(msg: ReceiveMessage) -> None: + calls_b.append(msg) + + mqtt_client_mock.reset_mock() + mock_debouncer.clear() + await mqtt.async_subscribe(hass, "test/state", _callback_a) + await mock_debouncer.wait() + async_fire_mqtt_message( + hass, "test/state", "online", qos=0, retain=True + ) # Simulate a (retained) message played back + assert len(calls_a) == 1 + mqtt_client_mock.subscribe.assert_called() + calls_a = [] + mqtt_client_mock.reset_mock() + + mock_debouncer.clear() + await mqtt.async_subscribe(hass, "test/state", _callback_b) + await mock_debouncer.wait() + + # Simulate edge case where non retained message was received + # after subscription at HA but before the debouncer delay was passed. + # The message without retain flag directly after a subscription should + # be processed by both subscriptions. + async_fire_mqtt_message(hass, "test/state", "online", qos=0, retain=False) + + # Simulate a (retained) message played back on new subscriptions + async_fire_mqtt_message(hass, "test/state", "online", qos=0, retain=True) + + # The current subscription only received the message without retain flag + assert len(calls_a) == 1 + assert help_assert_message(calls_a[0], "test/state", "online", qos=0, retain=False) + # The retained message playback should only be processed by the new subscription. + # The existing subscription already got the latest update, hence the existing + # subscription should not receive the replayed (retained) message. + # Messages without retain flag are received on both subscriptions. + assert len(calls_b) == 2 + assert help_assert_message(calls_b[0], "test/state", "online", qos=0, retain=False) + assert help_assert_message(calls_b[1], "test/state", "online", qos=0, retain=True) + mqtt_client_mock.subscribe.assert_called() + + calls_a = [] + calls_b = [] + mqtt_client_mock.reset_mock() + + # Simulate new message played back on new subscriptions + # After connecting the retain flag will not be set, even if the + # payload published was retained, we cannot see that + async_fire_mqtt_message(hass, "test/state", "online", qos=0, retain=False) + assert len(calls_a) == 1 + assert help_assert_message(calls_a[0], "test/state", "online", qos=0, retain=False) + assert len(calls_b) == 1 + assert help_assert_message(calls_b[0], "test/state", "online", qos=0, retain=False) + + # Now simulate the broker was disconnected shortly + calls_a = [] + calls_b = [] + mqtt_client_mock.reset_mock() + mqtt_client_mock.on_disconnect(None, None, 0) + + mock_debouncer.clear() + mqtt_client_mock.on_connect(None, None, None, 0) + await mock_debouncer.wait() + mqtt_client_mock.subscribe.assert_called() + # Simulate a (retained) message played back after reconnecting + async_fire_mqtt_message(hass, "test/state", "online", qos=0, retain=True) + # Both subscriptions now should replay the retained message + assert len(calls_a) == 1 + assert help_assert_message(calls_a[0], "test/state", "online", qos=0, retain=True) + assert len(calls_b) == 1 + assert help_assert_message(calls_b[0], "test/state", "online", qos=0, retain=True) + + +async def test_replaying_payload_after_resubscribing( + hass: HomeAssistant, + mock_debouncer: asyncio.Event, + setup_with_birth_msg_client_mock: MqttMockPahoClient, +) -> None: + """Test replaying and filtering retained messages after resubscribing. + + When subscribing to the same topic again, SUBSCRIBE must be sent to the broker again + for it to resend any retained messages for new subscriptions. + Retained messages must only be replayed for new subscriptions, except + when the MQTT client is reconnection. + """ + mqtt_client_mock = setup_with_birth_msg_client_mock + calls_a: list[ReceiveMessage] = [] + + @callback + def _callback_a(msg: ReceiveMessage) -> None: + calls_a.append(msg) + + mqtt_client_mock.reset_mock() + mock_debouncer.clear() + unsub = await mqtt.async_subscribe(hass, "test/state", _callback_a) + await mock_debouncer.wait() + mqtt_client_mock.subscribe.assert_called() + + # Simulate a (retained) message played back + async_fire_mqtt_message(hass, "test/state", "online", qos=0, retain=True) + assert help_assert_message(calls_a[0], "test/state", "online", qos=0, retain=True) + calls_a.clear() + + # Test we get updates + async_fire_mqtt_message(hass, "test/state", "offline", qos=0, retain=False) + assert help_assert_message(calls_a[0], "test/state", "offline", qos=0, retain=False) + calls_a.clear() + + # Test we filter new retained updates + async_fire_mqtt_message(hass, "test/state", "offline", qos=0, retain=True) + await hass.async_block_till_done() + assert len(calls_a) == 0 + + # Unsubscribe an resubscribe again + mock_debouncer.clear() + unsub() + unsub = await mqtt.async_subscribe(hass, "test/state", _callback_a) + await mock_debouncer.wait() + mqtt_client_mock.subscribe.assert_called() + + # Simulate we can receive a (retained) played back message again + async_fire_mqtt_message(hass, "test/state", "online", qos=0, retain=True) + assert help_assert_message(calls_a[0], "test/state", "online", qos=0, retain=True) + + +async def test_replaying_payload_wildcard_topic( + hass: HomeAssistant, + mock_debouncer: asyncio.Event, + setup_with_birth_msg_client_mock: MqttMockPahoClient, +) -> None: + """Test replaying retained messages. + + When we have multiple subscriptions to the same wildcard topic, + SUBSCRIBE must be sent to the broker again + for it to resend any retained messages for new subscriptions. + Retained messages should only be replayed for new subscriptions, except + when the MQTT client is reconnection. + """ + mqtt_client_mock = setup_with_birth_msg_client_mock + calls_a: list[ReceiveMessage] = [] + calls_b: list[ReceiveMessage] = [] + + @callback + def _callback_a(msg: ReceiveMessage) -> None: + calls_a.append(msg) + + @callback + def _callback_b(msg: ReceiveMessage) -> None: + calls_b.append(msg) + + mqtt_client_mock.reset_mock() + mock_debouncer.clear() + await mqtt.async_subscribe(hass, "test/#", _callback_a) + await mock_debouncer.wait() + # Simulate (retained) messages being played back on new subscriptions + async_fire_mqtt_message(hass, "test/state1", "new_value_1", qos=0, retain=True) + async_fire_mqtt_message(hass, "test/state2", "new_value_2", qos=0, retain=True) + assert len(calls_a) == 2 + mqtt_client_mock.subscribe.assert_called() + calls_a = [] + mqtt_client_mock.reset_mock() + + # resubscribe to the wild card topic again + mock_debouncer.clear() + await mqtt.async_subscribe(hass, "test/#", _callback_b) + await mock_debouncer.wait() + # Simulate (retained) messages being played back on new subscriptions + async_fire_mqtt_message(hass, "test/state1", "initial_value_1", qos=0, retain=True) + async_fire_mqtt_message(hass, "test/state2", "initial_value_2", qos=0, retain=True) + # The retained messages playback should only be processed for the new subscriptions + assert len(calls_a) == 0 + assert len(calls_b) == 2 + mqtt_client_mock.subscribe.assert_called() + + calls_a = [] + calls_b = [] + mqtt_client_mock.reset_mock() + + # Simulate new messages being received + async_fire_mqtt_message(hass, "test/state1", "update_value_1", qos=0, retain=False) + async_fire_mqtt_message(hass, "test/state2", "update_value_2", qos=0, retain=False) + assert len(calls_a) == 2 + assert len(calls_b) == 2 + + # Now simulate the broker was disconnected shortly + calls_a = [] + calls_b = [] + mqtt_client_mock.reset_mock() + mqtt_client_mock.on_disconnect(None, None, 0) + + mock_debouncer.clear() + mqtt_client_mock.on_connect(None, None, None, 0) + await mock_debouncer.wait() + + mqtt_client_mock.subscribe.assert_called() + # Simulate the (retained) messages are played back after reconnecting + # for all subscriptions + async_fire_mqtt_message(hass, "test/state1", "update_value_1", qos=0, retain=True) + async_fire_mqtt_message(hass, "test/state2", "update_value_2", qos=0, retain=True) + # Both subscriptions should replay + assert len(calls_a) == 2 + assert len(calls_b) == 2 + + +async def test_not_calling_unsubscribe_with_active_subscribers( + hass: HomeAssistant, + mock_debouncer: asyncio.Event, + setup_with_birth_msg_client_mock: MqttMockPahoClient, + record_calls: MessageCallbackType, +) -> None: + """Test not calling unsubscribe() when other subscribers are active.""" + mqtt_client_mock = setup_with_birth_msg_client_mock + mqtt_client_mock.reset_mock() + mock_debouncer.clear() + unsub = await mqtt.async_subscribe(hass, "test/state", record_calls, 2) + await mqtt.async_subscribe(hass, "test/state", record_calls, 1) + await mock_debouncer.wait() + assert mqtt_client_mock.subscribe.called + + mock_debouncer.clear() + unsub() + await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) + async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) # cooldown + assert not mqtt_client_mock.unsubscribe.called + assert not mock_debouncer.is_set() + + +async def test_not_calling_subscribe_when_unsubscribed_within_cooldown( + hass: HomeAssistant, + mock_debouncer: asyncio.Event, + mqtt_mock_entry: MqttMockHAClientGenerator, + record_calls: MessageCallbackType, +) -> None: + """Test not calling subscribe() when it is unsubscribed. + + Make sure subscriptions are cleared if unsubscribed before + the subscribe cool down period has ended. + """ + mqtt_mock = await mqtt_mock_entry() + mqtt_client_mock = mqtt_mock._mqttc + await mock_debouncer.wait() + + mock_debouncer.clear() + mqtt_client_mock.subscribe.reset_mock() + unsub = await mqtt.async_subscribe(hass, "test/state", record_calls) + unsub() + await mock_debouncer.wait() + # The debouncer executes without an pending subscribes + assert not mqtt_client_mock.subscribe.called + + +async def test_unsubscribe_race( + hass: HomeAssistant, + mock_debouncer: asyncio.Event, + setup_with_birth_msg_client_mock: MqttMockPahoClient, +) -> None: + """Test not calling unsubscribe() when other subscribers are active.""" + mqtt_client_mock = setup_with_birth_msg_client_mock + calls_a: list[ReceiveMessage] = [] + calls_b: list[ReceiveMessage] = [] + + @callback + def _callback_a(msg: ReceiveMessage) -> None: + calls_a.append(msg) + + @callback + def _callback_b(msg: ReceiveMessage) -> None: + calls_b.append(msg) + + mqtt_client_mock.reset_mock() + + mock_debouncer.clear() + unsub = await mqtt.async_subscribe(hass, "test/state", _callback_a) + unsub() + await mqtt.async_subscribe(hass, "test/state", _callback_b) + await mock_debouncer.wait() + + async_fire_mqtt_message(hass, "test/state", "online") + assert not calls_a + assert calls_b + + # We allow either calls [subscribe, unsubscribe, subscribe], [subscribe, subscribe] or + # when both subscriptions were combined [subscribe] + expected_calls_1 = [ + call.subscribe([("test/state", 0)]), + call.unsubscribe("test/state"), + call.subscribe([("test/state", 0)]), + ] + expected_calls_2 = [ + call.subscribe([("test/state", 0)]), + call.subscribe([("test/state", 0)]), + ] + expected_calls_3 = [ + call.subscribe([("test/state", 0)]), + ] + assert mqtt_client_mock.mock_calls in ( + expected_calls_1, + expected_calls_2, + expected_calls_3, + ) + + +@pytest.mark.parametrize( + "mqtt_config_entry_data", + [{mqtt.CONF_BROKER: "mock-broker", mqtt.CONF_DISCOVERY: False}], +) +async def test_restore_subscriptions_on_reconnect( + hass: HomeAssistant, + mock_debouncer: asyncio.Event, + setup_with_birth_msg_client_mock: MqttMockPahoClient, + record_calls: MessageCallbackType, +) -> None: + """Test subscriptions are restored on reconnect.""" + mqtt_client_mock = setup_with_birth_msg_client_mock + + mqtt_client_mock.reset_mock() + + mock_debouncer.clear() + await mqtt.async_subscribe(hass, "test/state", record_calls) + async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) # cooldown + await mock_debouncer.wait() + assert ("test/state", 0) in help_all_subscribe_calls(mqtt_client_mock) + + mqtt_client_mock.reset_mock() + mqtt_client_mock.on_disconnect(None, None, 0) + + mock_debouncer.clear() + mqtt_client_mock.on_connect(None, None, None, 0) + await mock_debouncer.wait() + assert ("test/state", 0) in help_all_subscribe_calls(mqtt_client_mock) + + +@pytest.mark.parametrize( + "mqtt_config_entry_data", + [{mqtt.CONF_BROKER: "mock-broker", mqtt.CONF_DISCOVERY: False}], +) +async def test_restore_all_active_subscriptions_on_reconnect( + hass: HomeAssistant, + mock_debouncer: asyncio.Event, + setup_with_birth_msg_client_mock: MqttMockPahoClient, + record_calls: MessageCallbackType, +) -> None: + """Test active subscriptions are restored correctly on reconnect.""" + mqtt_client_mock = setup_with_birth_msg_client_mock + mqtt_client_mock.reset_mock() + mock_debouncer.clear() + unsub = await mqtt.async_subscribe(hass, "test/state", record_calls, qos=2) + await mqtt.async_subscribe(hass, "test/state", record_calls, qos=1) + await mqtt.async_subscribe(hass, "test/state", record_calls, qos=0) + # cooldown + await mock_debouncer.wait() + + # the subscription with the highest QoS should survive + expected = [ + call([("test/state", 2)]), + ] + assert mqtt_client_mock.subscribe.mock_calls == expected + + unsub() + assert mqtt_client_mock.unsubscribe.call_count == 0 + + mqtt_client_mock.on_disconnect(None, None, 0) + + mock_debouncer.clear() + mqtt_client_mock.on_connect(None, None, None, 0) + # wait for cooldown + await mock_debouncer.wait() + + expected.append(call([("test/state", 1)])) + for expected_call in expected: + assert mqtt_client_mock.subscribe.hass_call(expected_call) + + +@pytest.mark.parametrize( + "mqtt_config_entry_data", + [{mqtt.CONF_BROKER: "mock-broker", mqtt.CONF_DISCOVERY: False}], +) +async def test_subscribed_at_highest_qos( + hass: HomeAssistant, + mock_debouncer: asyncio.Event, + setup_with_birth_msg_client_mock: MqttMockPahoClient, + record_calls: MessageCallbackType, +) -> None: + """Test the highest qos as assigned when subscribing to the same topic.""" + mqtt_client_mock = setup_with_birth_msg_client_mock + mqtt_client_mock.reset_mock() + mock_debouncer.clear() + await mqtt.async_subscribe(hass, "test/state", record_calls, qos=0) + await hass.async_block_till_done() + # cooldown + await mock_debouncer.wait() + assert ("test/state", 0) in help_all_subscribe_calls(mqtt_client_mock) + mqtt_client_mock.reset_mock() + + mock_debouncer.clear() + await mqtt.async_subscribe(hass, "test/state", record_calls, qos=1) + await mqtt.async_subscribe(hass, "test/state", record_calls, qos=2) + # cooldown + await mock_debouncer.wait() + + # the subscription with the highest QoS should survive + assert help_all_subscribe_calls(mqtt_client_mock) == [("test/state", 2)] + + +async def test_initial_setup_logs_error( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, + mqtt_client_mock: MqttMockPahoClient, +) -> None: + """Test for setup failure if initial client connection fails.""" + entry = MockConfigEntry(domain=mqtt.DOMAIN, data={mqtt.CONF_BROKER: "test-broker"}) + entry.add_to_hass(hass) + mqtt_client_mock.connect.side_effect = MagicMock(return_value=1) + try: + assert await hass.config_entries.async_setup(entry.entry_id) + except HomeAssistantError: + assert True + assert "Failed to connect to MQTT server:" in caplog.text + + +async def test_logs_error_if_no_connect_broker( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, + setup_with_birth_msg_client_mock: MqttMockPahoClient, +) -> None: + """Test for setup failure if connection to broker is missing.""" + mqtt_client_mock = setup_with_birth_msg_client_mock + # test with rc = 3 -> broker unavailable + mqtt_client_mock.on_disconnect(Mock(), None, 0) + mqtt_client_mock.on_connect(Mock(), None, None, 3) + await hass.async_block_till_done() + assert ( + "Unable to connect to the MQTT broker: Connection Refused: broker unavailable." + in caplog.text + ) + + +@pytest.mark.parametrize("return_code", [4, 5]) +async def test_triggers_reauth_flow_if_auth_fails( + hass: HomeAssistant, + setup_with_birth_msg_client_mock: MqttMockPahoClient, + return_code: int, +) -> None: + """Test re-auth is triggered if authentication is failing.""" + mqtt_client_mock = setup_with_birth_msg_client_mock + # test with rc = 4 -> CONNACK_REFUSED_NOT_AUTHORIZED and 5 -> CONNACK_REFUSED_BAD_USERNAME_PASSWORD + mqtt_client_mock.on_disconnect(Mock(), None, 0) + mqtt_client_mock.on_connect(Mock(), None, None, return_code) + await hass.async_block_till_done() + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + assert flows[0]["context"]["source"] == "reauth" + + +@patch("homeassistant.components.mqtt.client.TIMEOUT_ACK", 0.3) +async def test_handle_mqtt_on_callback( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, + setup_with_birth_msg_client_mock: MqttMockPahoClient, +) -> None: + """Test receiving an ACK callback before waiting for it.""" + mqtt_client_mock = setup_with_birth_msg_client_mock + with patch.object(mqtt_client_mock, "get_mid", return_value=100): + # Simulate an ACK for mid == 100, this will call mqtt_mock._async_get_mid_future(mid) + mqtt_client_mock.on_publish(mqtt_client_mock, None, 100) + await hass.async_block_till_done() + # Make sure the ACK has been received + await hass.async_block_till_done() + # Now call publish without call back, this will call _async_async_wait_for_mid(msg_info.mid) + await mqtt.async_publish(hass, "no_callback/test-topic", "test-payload") + # Since the mid event was already set, we should not see any timeout warning in the log + await hass.async_block_till_done() + assert "No ACK from MQTT server" not in caplog.text + + +async def test_handle_mqtt_on_callback_after_timeout( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, + mqtt_mock_entry: MqttMockHAClientGenerator, + mqtt_client_mock: MqttMockPahoClient, +) -> None: + """Test receiving an ACK after a timeout.""" + mqtt_mock = await mqtt_mock_entry() + # Simulate the mid future getting a timeout + mqtt_mock()._async_get_mid_future(101).set_exception(asyncio.TimeoutError) + # Simulate an ACK for mid == 101, being received after the timeout + mqtt_client_mock.on_publish(mqtt_client_mock, None, 101) + await hass.async_block_till_done() + assert "No ACK from MQTT server" not in caplog.text + assert "InvalidStateError" not in caplog.text + + +async def test_publish_error( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Test publish error.""" + entry = MockConfigEntry(domain=mqtt.DOMAIN, data={mqtt.CONF_BROKER: "test-broker"}) + entry.add_to_hass(hass) + + # simulate an Out of memory error + with patch( + "homeassistant.components.mqtt.async_client.AsyncMQTTClient" + ) as mock_client: + mock_client().connect = lambda *args: 1 + mock_client().publish().rc = 1 + assert await hass.config_entries.async_setup(entry.entry_id) + with pytest.raises(HomeAssistantError): + await mqtt.async_publish( + hass, "some-topic", b"test-payload", qos=0, retain=False, encoding=None + ) + assert "Failed to connect to MQTT server: Out of memory." in caplog.text + + +async def test_subscribe_error( + hass: HomeAssistant, + setup_with_birth_msg_client_mock: MqttMockPahoClient, + record_calls: MessageCallbackType, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test publish error.""" + mqtt_client_mock = setup_with_birth_msg_client_mock + mqtt_client_mock.reset_mock() + # simulate client is not connected error before subscribing + mqtt_client_mock.subscribe.side_effect = lambda *args: (4, None) + await mqtt.async_subscribe(hass, "some-topic", record_calls) + while mqtt_client_mock.subscribe.call_count == 0: + await hass.async_block_till_done() + await hass.async_block_till_done() + assert ( + "Error talking to MQTT: The client is not currently connected." in caplog.text + ) + + +async def test_handle_message_callback( + hass: HomeAssistant, + mock_debouncer: asyncio.Event, + setup_with_birth_msg_client_mock: MqttMockPahoClient, +) -> None: + """Test for handling an incoming message callback.""" + mqtt_client_mock = setup_with_birth_msg_client_mock + callbacks = [] + + @callback + def _callback(args) -> None: + callbacks.append(args) + + msg = ReceiveMessage( + "some-topic", b"test-payload", 1, False, "some-topic", datetime.now() + ) + mock_debouncer.clear() + await mqtt.async_subscribe(hass, "some-topic", _callback) + await mock_debouncer.wait() + mqtt_client_mock.reset_mock() + mqtt_client_mock.on_message(None, None, msg) + + assert len(callbacks) == 1 + assert callbacks[0].topic == "some-topic" + assert callbacks[0].qos == 1 + assert callbacks[0].payload == "test-payload" + + +@pytest.mark.parametrize( + ("mqtt_config_entry_data", "protocol"), + [ + ( + { + mqtt.CONF_BROKER: "mock-broker", + CONF_PROTOCOL: "3.1", + }, + 3, + ), + ( + { + mqtt.CONF_BROKER: "mock-broker", + CONF_PROTOCOL: "3.1.1", + }, + 4, + ), + ( + { + mqtt.CONF_BROKER: "mock-broker", + CONF_PROTOCOL: "5", + }, + 5, + ), + ], +) +async def test_setup_mqtt_client_protocol( + mqtt_mock_entry: MqttMockHAClientGenerator, protocol: int +) -> None: + """Test MQTT client protocol setup.""" + with patch( + "homeassistant.components.mqtt.async_client.AsyncMQTTClient" + ) as mock_client: + await mqtt_mock_entry() + + # check if protocol setup was correctly + assert mock_client.call_args[1]["protocol"] == protocol + + +@patch("homeassistant.components.mqtt.client.TIMEOUT_ACK", 0.2) +async def test_handle_mqtt_timeout_on_callback( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture, mock_debouncer: asyncio.Event +) -> None: + """Test publish without receiving an ACK callback.""" + mid = 0 + + class FakeInfo: + """Returns a simulated client publish response.""" + + mid = 102 + rc = 0 + + with patch( + "homeassistant.components.mqtt.async_client.AsyncMQTTClient" + ) as mock_client: + + def _mock_ack(topic: str, qos: int = 0) -> tuple[int, int]: + # Handle ACK for subscribe normally + nonlocal mid + mid += 1 + mock_client.on_subscribe(0, 0, mid) + return (0, mid) + + # We want to simulate the publish behaviour MQTT client + mock_client = mock_client.return_value + mock_client.publish.return_value = FakeInfo() + # Mock we get a mid and rc=0 + mock_client.subscribe.side_effect = _mock_ack + mock_client.unsubscribe.side_effect = _mock_ack + mock_client.connect = MagicMock( + return_value=0, + side_effect=lambda *args, **kwargs: hass.loop.call_soon_threadsafe( + mock_client.on_connect, mock_client, None, 0, 0, 0 + ), + ) + + entry = MockConfigEntry( + domain=mqtt.DOMAIN, data={mqtt.CONF_BROKER: "test-broker"} + ) + entry.add_to_hass(hass) + + # Set up the integration + mock_debouncer.clear() + assert await hass.config_entries.async_setup(entry.entry_id) + + # Now call we publish without simulating and ACK callback + await mqtt.async_publish(hass, "no_callback/test-topic", "test-payload") + await hass.async_block_till_done() + # There is no ACK so we should see a timeout in the log after publishing + assert len(mock_client.publish.mock_calls) == 1 + assert "No ACK from MQTT server" in caplog.text + # Ensure we stop lingering background tasks + await hass.config_entries.async_unload(entry.entry_id) + # Assert we did not have any completed subscribes, + # because the debouncer subscribe job failed to receive an ACK, + # and the time auto caused the debouncer job to fail. + assert not mock_debouncer.is_set() + + +async def test_setup_raises_config_entry_not_ready_if_no_connect_broker( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Test for setup failure if connection to broker is missing.""" + entry = MockConfigEntry(domain=mqtt.DOMAIN, data={mqtt.CONF_BROKER: "test-broker"}) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.mqtt.async_client.AsyncMQTTClient" + ) as mock_client: + mock_client().connect = MagicMock(side_effect=OSError("Connection error")) + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + assert "Failed to connect to MQTT server due to exception:" in caplog.text + + +@pytest.mark.parametrize( + ("mqtt_config_entry_data", "insecure_param"), + [ + ({"broker": "test-broker", "certificate": "auto"}, "not set"), + ( + {"broker": "test-broker", "certificate": "auto", "tls_insecure": False}, + False, + ), + ({"broker": "test-broker", "certificate": "auto", "tls_insecure": True}, True), + ], +) +async def test_setup_uses_certificate_on_certificate_set_to_auto_and_insecure( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + insecure_param: bool | str, +) -> None: + """Test setup uses bundled certs when certificate is set to auto and insecure.""" + calls = [] + insecure_check = {"insecure": "not set"} + + def mock_tls_set( + certificate, certfile=None, keyfile=None, tls_version=None + ) -> None: + calls.append((certificate, certfile, keyfile, tls_version)) + + def mock_tls_insecure_set(insecure_param) -> None: + insecure_check["insecure"] = insecure_param + + with patch( + "homeassistant.components.mqtt.async_client.AsyncMQTTClient" + ) as mock_client: + mock_client().tls_set = mock_tls_set + mock_client().tls_insecure_set = mock_tls_insecure_set + await mqtt_mock_entry() + await hass.async_block_till_done() + + assert calls + + expected_certificate = certifi.where() + assert calls[0][0] == expected_certificate + + # test if insecure is set + assert insecure_check["insecure"] == insecure_param + + +@pytest.mark.parametrize( + "mqtt_config_entry_data", + [ + { + mqtt.CONF_BROKER: "mock-broker", + mqtt.CONF_CERTIFICATE: "auto", + } + ], +) +async def test_tls_version( + hass: HomeAssistant, + mqtt_client_mock: MqttMockPahoClient, + mqtt_mock_entry: MqttMockHAClientGenerator, +) -> None: + """Test setup defaults for tls.""" + await mqtt_mock_entry() + await hass.async_block_till_done() + assert ( + mqtt_client_mock.tls_set.mock_calls[0][2]["tls_version"] + == ssl.PROTOCOL_TLS_CLIENT + ) + + +@pytest.mark.parametrize( + "mqtt_config_entry_data", + [ + { + mqtt.CONF_BROKER: "mock-broker", + mqtt.CONF_BIRTH_MESSAGE: { + mqtt.ATTR_TOPIC: "birth", + mqtt.ATTR_PAYLOAD: "birth", + mqtt.ATTR_QOS: 0, + mqtt.ATTR_RETAIN: False, + }, + } + ], +) +@patch("homeassistant.components.mqtt.client.INITIAL_SUBSCRIBE_COOLDOWN", 0.0) +@patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.0) +@patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.0) +async def test_custom_birth_message( + hass: HomeAssistant, + mock_debouncer: asyncio.Event, + mqtt_config_entry_data: dict[str, Any], + mqtt_client_mock: MqttMockPahoClient, +) -> None: + """Test sending birth message.""" + + entry = MockConfigEntry(domain=mqtt.DOMAIN, data=mqtt_config_entry_data) + entry.add_to_hass(hass) + hass.config.components.add(mqtt.DOMAIN) + assert await hass.config_entries.async_setup(entry.entry_id) + mock_debouncer.clear() + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + # discovery cooldown + await mock_debouncer.wait() + # Wait for publish call to finish + await hass.async_block_till_done(wait_background_tasks=True) + mqtt_client_mock.publish.assert_called_with("birth", "birth", 0, False) + + +@pytest.mark.parametrize( + "mqtt_config_entry_data", + [ENTRY_DEFAULT_BIRTH_MESSAGE], +) +async def test_default_birth_message( + hass: HomeAssistant, setup_with_birth_msg_client_mock: MqttMockPahoClient +) -> None: + """Test sending birth message.""" + mqtt_client_mock = setup_with_birth_msg_client_mock + await hass.async_block_till_done(wait_background_tasks=True) + mqtt_client_mock.publish.assert_called_with( + "homeassistant/status", "online", 0, False + ) + + +@pytest.mark.parametrize( + "mqtt_config_entry_data", + [{mqtt.CONF_BROKER: "mock-broker", mqtt.CONF_BIRTH_MESSAGE: {}}], +) +@patch("homeassistant.components.mqtt.client.INITIAL_SUBSCRIBE_COOLDOWN", 0.0) +@patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.0) +@patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.0) +async def test_no_birth_message( + hass: HomeAssistant, + record_calls: MessageCallbackType, + mock_debouncer: asyncio.Event, + mqtt_config_entry_data: dict[str, Any], + mqtt_client_mock: MqttMockPahoClient, +) -> None: + """Test disabling birth message.""" + entry = MockConfigEntry(domain=mqtt.DOMAIN, data=mqtt_config_entry_data) + entry.add_to_hass(hass) + hass.config.components.add(mqtt.DOMAIN) + mock_debouncer.clear() + assert await hass.config_entries.async_setup(entry.entry_id) + # Wait for discovery cooldown + await mock_debouncer.wait() + # Ensure any publishing could have been processed + await hass.async_block_till_done(wait_background_tasks=True) + mqtt_client_mock.publish.assert_not_called() + + mqtt_client_mock.reset_mock() + mock_debouncer.clear() + await mqtt.async_subscribe(hass, "homeassistant/some-topic", record_calls) + # Wait for discovery cooldown + await mock_debouncer.wait() + mqtt_client_mock.subscribe.assert_called() + + +@pytest.mark.parametrize( + "mqtt_config_entry_data", + [ENTRY_DEFAULT_BIRTH_MESSAGE], +) +@patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.2) +async def test_delayed_birth_message( + hass: HomeAssistant, + mqtt_config_entry_data: dict[str, Any], + mqtt_client_mock: MqttMockPahoClient, +) -> None: + """Test sending birth message does not happen until Home Assistant starts.""" + hass.set_state(CoreState.starting) + await hass.async_block_till_done() + birth = asyncio.Event() + entry = MockConfigEntry(domain=mqtt.DOMAIN, data=mqtt_config_entry_data) + entry.add_to_hass(hass) + hass.config.components.add(mqtt.DOMAIN) + assert await hass.config_entries.async_setup(entry.entry_id) + + @callback + def wait_birth(msg: ReceiveMessage) -> None: + """Handle birth message.""" + birth.set() + + await mqtt.async_subscribe(hass, "homeassistant/status", wait_birth) + with pytest.raises(TimeoutError): + await asyncio.wait_for(birth.wait(), 0.05) + assert not mqtt_client_mock.publish.called + assert not birth.is_set() + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await birth.wait() + mqtt_client_mock.publish.assert_called_with( + "homeassistant/status", "online", 0, False + ) + + +@pytest.mark.parametrize( + "mqtt_config_entry_data", + [ENTRY_DEFAULT_BIRTH_MESSAGE], +) +async def test_subscription_done_when_birth_message_is_sent( + setup_with_birth_msg_client_mock: MqttMockPahoClient, +) -> None: + """Test sending birth message until initial subscription has been completed.""" + mqtt_client_mock = setup_with_birth_msg_client_mock + subscribe_calls = help_all_subscribe_calls(mqtt_client_mock) + assert ("homeassistant/+/+/config", 0) in subscribe_calls + assert ("homeassistant/+/+/+/config", 0) in subscribe_calls + mqtt_client_mock.publish.assert_called_with( + "homeassistant/status", "online", 0, False + ) + + +@pytest.mark.parametrize( + "mqtt_config_entry_data", + [ + { + mqtt.CONF_BROKER: "mock-broker", + mqtt.CONF_WILL_MESSAGE: { + mqtt.ATTR_TOPIC: "death", + mqtt.ATTR_PAYLOAD: "death", + mqtt.ATTR_QOS: 0, + mqtt.ATTR_RETAIN: False, + }, + } + ], +) +async def test_custom_will_message( + hass: HomeAssistant, + mqtt_config_entry_data: dict[str, Any], + mqtt_client_mock: MqttMockPahoClient, +) -> None: + """Test will message.""" + entry = MockConfigEntry(domain=mqtt.DOMAIN, data=mqtt_config_entry_data) + entry.add_to_hass(hass) + hass.config.components.add(mqtt.DOMAIN) + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + mqtt_client_mock.will_set.assert_called_with( + topic="death", payload="death", qos=0, retain=False + ) + + +async def test_default_will_message( + setup_with_birth_msg_client_mock: MqttMockPahoClient, +) -> None: + """Test will message.""" + mqtt_client_mock = setup_with_birth_msg_client_mock + mqtt_client_mock.will_set.assert_called_with( + topic="homeassistant/status", payload="offline", qos=0, retain=False + ) + + +@pytest.mark.parametrize( + "mqtt_config_entry_data", + [{mqtt.CONF_BROKER: "mock-broker", mqtt.CONF_WILL_MESSAGE: {}}], +) +async def test_no_will_message( + hass: HomeAssistant, + mqtt_config_entry_data: dict[str, Any], + mqtt_client_mock: MqttMockPahoClient, +) -> None: + """Test will message.""" + entry = MockConfigEntry(domain=mqtt.DOMAIN, data=mqtt_config_entry_data) + entry.add_to_hass(hass) + hass.config.components.add(mqtt.DOMAIN) + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + mqtt_client_mock.will_set.assert_not_called() + + +@pytest.mark.parametrize( + "mqtt_config_entry_data", + [ENTRY_DEFAULT_BIRTH_MESSAGE | {mqtt.CONF_DISCOVERY: False}], +) +async def test_mqtt_subscribes_topics_on_connect( + hass: HomeAssistant, + mock_debouncer: asyncio.Event, + setup_with_birth_msg_client_mock: MqttMockPahoClient, + record_calls: MessageCallbackType, +) -> None: + """Test subscription to topic on connect.""" + mqtt_client_mock = setup_with_birth_msg_client_mock + + mock_debouncer.clear() + await mqtt.async_subscribe(hass, "topic/test", record_calls) + await mqtt.async_subscribe(hass, "home/sensor", record_calls, 2) + await mqtt.async_subscribe(hass, "still/pending", record_calls) + await mqtt.async_subscribe(hass, "still/pending", record_calls, 1) + await mock_debouncer.wait() + + mqtt_client_mock.on_disconnect(Mock(), None, 0) + + mqtt_client_mock.reset_mock() + + mock_debouncer.clear() + mqtt_client_mock.on_connect(Mock(), None, 0, 0) + await mock_debouncer.wait() + + subscribe_calls = help_all_subscribe_calls(mqtt_client_mock) + assert ("topic/test", 0) in subscribe_calls + assert ("home/sensor", 2) in subscribe_calls + assert ("still/pending", 1) in subscribe_calls + + +@pytest.mark.parametrize( + "mqtt_config_entry_data", + [ENTRY_DEFAULT_BIRTH_MESSAGE], +) +async def test_mqtt_subscribes_in_single_call( + hass: HomeAssistant, + mock_debouncer: asyncio.Event, + setup_with_birth_msg_client_mock: MqttMockPahoClient, + record_calls: MessageCallbackType, +) -> None: + """Test bundled client subscription to topic.""" + mqtt_client_mock = setup_with_birth_msg_client_mock + mqtt_client_mock.subscribe.reset_mock() + mock_debouncer.clear() + await mqtt.async_subscribe(hass, "topic/test", record_calls) + await mqtt.async_subscribe(hass, "home/sensor", record_calls) + # Make sure the debouncer finishes + await mock_debouncer.wait() + + assert mqtt_client_mock.subscribe.call_count == 1 + # Assert we have a single subscription call with both subscriptions + assert mqtt_client_mock.subscribe.mock_calls[0][1][0] in [ + [("topic/test", 0), ("home/sensor", 0)], + [("home/sensor", 0), ("topic/test", 0)], + ] + + +@pytest.mark.parametrize("mqtt_config_entry_data", [ENTRY_DEFAULT_BIRTH_MESSAGE]) +@patch("homeassistant.components.mqtt.client.MAX_SUBSCRIBES_PER_CALL", 2) +@patch("homeassistant.components.mqtt.client.MAX_UNSUBSCRIBES_PER_CALL", 2) +async def test_mqtt_subscribes_and_unsubscribes_in_chunks( + hass: HomeAssistant, + mock_debouncer: asyncio.Event, + setup_with_birth_msg_client_mock: MqttMockPahoClient, + record_calls: MessageCallbackType, +) -> None: + """Test chunked client subscriptions.""" + mqtt_client_mock = setup_with_birth_msg_client_mock + + mqtt_client_mock.subscribe.reset_mock() + unsub_tasks: list[CALLBACK_TYPE] = [] + mock_debouncer.clear() + unsub_tasks.append(await mqtt.async_subscribe(hass, "topic/test1", record_calls)) + unsub_tasks.append(await mqtt.async_subscribe(hass, "home/sensor1", record_calls)) + unsub_tasks.append(await mqtt.async_subscribe(hass, "topic/test2", record_calls)) + unsub_tasks.append(await mqtt.async_subscribe(hass, "home/sensor2", record_calls)) + # Make sure the debouncer finishes + await mock_debouncer.wait() + + assert mqtt_client_mock.subscribe.call_count == 2 + # Assert we have a 2 subscription calls with both 2 subscriptions + assert len(mqtt_client_mock.subscribe.mock_calls[0][1][0]) == 2 + assert len(mqtt_client_mock.subscribe.mock_calls[1][1][0]) == 2 + + # Unsubscribe all topics + mock_debouncer.clear() + for task in unsub_tasks: + task() + # Make sure the debouncer finishes + await mock_debouncer.wait() + + assert mqtt_client_mock.unsubscribe.call_count == 2 + # Assert we have a 2 unsubscribe calls with both 2 topic + assert len(mqtt_client_mock.unsubscribe.mock_calls[0][1][0]) == 2 + assert len(mqtt_client_mock.unsubscribe.mock_calls[1][1][0]) == 2 + + +async def test_auto_reconnect( + hass: HomeAssistant, + setup_with_birth_msg_client_mock: MqttMockPahoClient, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test reconnection is automatically done.""" + mqtt_client_mock = setup_with_birth_msg_client_mock + assert mqtt_client_mock.connect.call_count == 1 + mqtt_client_mock.reconnect.reset_mock() + + mqtt_client_mock.disconnect() + mqtt_client_mock.on_disconnect(None, None, 0) + await hass.async_block_till_done() + + mqtt_client_mock.reconnect.side_effect = OSError("foo") + async_fire_time_changed( + hass, utcnow() + timedelta(seconds=RECONNECT_INTERVAL_SECONDS) + ) + await hass.async_block_till_done() + assert len(mqtt_client_mock.reconnect.mock_calls) == 1 + assert "Error re-connecting to MQTT server due to exception: foo" in caplog.text + + mqtt_client_mock.reconnect.side_effect = None + async_fire_time_changed( + hass, utcnow() + timedelta(seconds=RECONNECT_INTERVAL_SECONDS) + ) + await hass.async_block_till_done() + assert len(mqtt_client_mock.reconnect.mock_calls) == 2 + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + + mqtt_client_mock.disconnect() + mqtt_client_mock.on_disconnect(None, None, 0) + await hass.async_block_till_done() + + async_fire_time_changed( + hass, utcnow() + timedelta(seconds=RECONNECT_INTERVAL_SECONDS) + ) + await hass.async_block_till_done() + # Should not reconnect after stop + assert len(mqtt_client_mock.reconnect.mock_calls) == 2 + + +async def test_server_sock_connect_and_disconnect( + hass: HomeAssistant, + mock_debouncer: asyncio.Event, + setup_with_birth_msg_client_mock: MqttMockPahoClient, + recorded_calls: list[ReceiveMessage], + record_calls: MessageCallbackType, +) -> None: + """Test handling the socket connected and disconnected.""" + mqtt_client_mock = setup_with_birth_msg_client_mock + assert mqtt_client_mock.connect.call_count == 1 + + mqtt_client_mock.loop_misc.return_value = paho_mqtt.MQTT_ERR_SUCCESS + + client, server = socket.socketpair( + family=socket.AF_UNIX, type=socket.SOCK_STREAM, proto=0 + ) + client.setblocking(False) + server.setblocking(False) + mqtt_client_mock.on_socket_open(mqtt_client_mock, None, client) + mqtt_client_mock.on_socket_register_write(mqtt_client_mock, None, client) + await hass.async_block_till_done() + + server.close() # mock the server closing the connection on us + + mock_debouncer.clear() + unsub = await mqtt.async_subscribe(hass, "test-topic", record_calls) + await mock_debouncer.wait() + + mqtt_client_mock.loop_misc.return_value = paho_mqtt.MQTT_ERR_CONN_LOST + mqtt_client_mock.on_socket_unregister_write(mqtt_client_mock, None, client) + mqtt_client_mock.on_socket_close(mqtt_client_mock, None, client) + mqtt_client_mock.on_disconnect(mqtt_client_mock, None, client) + await hass.async_block_till_done() + mock_debouncer.clear() + unsub() + await hass.async_block_till_done() + assert not mock_debouncer.is_set() + + # Should have failed + assert len(recorded_calls) == 0 + + +async def test_server_sock_buffer_size( + hass: HomeAssistant, + setup_with_birth_msg_client_mock: MqttMockPahoClient, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test handling the socket buffer size fails.""" + mqtt_client_mock = setup_with_birth_msg_client_mock + assert mqtt_client_mock.connect.call_count == 1 + + mqtt_client_mock.loop_misc.return_value = paho_mqtt.MQTT_ERR_SUCCESS + + client, server = socket.socketpair( + family=socket.AF_UNIX, type=socket.SOCK_STREAM, proto=0 + ) + client.setblocking(False) + server.setblocking(False) + with patch.object(client, "setsockopt", side_effect=OSError("foo")): + mqtt_client_mock.on_socket_open(mqtt_client_mock, None, client) + mqtt_client_mock.on_socket_register_write(mqtt_client_mock, None, client) + await hass.async_block_till_done() + assert "Unable to increase the socket buffer size" in caplog.text + + +async def test_server_sock_buffer_size_with_websocket( + hass: HomeAssistant, + setup_with_birth_msg_client_mock: MqttMockPahoClient, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test handling the socket buffer size fails.""" + mqtt_client_mock = setup_with_birth_msg_client_mock + assert mqtt_client_mock.connect.call_count == 1 + + mqtt_client_mock.loop_misc.return_value = paho_mqtt.MQTT_ERR_SUCCESS + + client, server = socket.socketpair( + family=socket.AF_UNIX, type=socket.SOCK_STREAM, proto=0 + ) + client.setblocking(False) + server.setblocking(False) + + class FakeWebsocket(paho_mqtt.WebsocketWrapper): + def _do_handshake(self, *args, **kwargs): + pass + + wrapped_socket = FakeWebsocket(client, "127.0.01", 1, False, "/", None) + + with patch.object(client, "setsockopt", side_effect=OSError("foo")): + mqtt_client_mock.on_socket_open(mqtt_client_mock, None, wrapped_socket) + mqtt_client_mock.on_socket_register_write( + mqtt_client_mock, None, wrapped_socket + ) + await hass.async_block_till_done() + assert "Unable to increase the socket buffer size" in caplog.text + + +async def test_client_sock_failure_after_connect( + hass: HomeAssistant, + setup_with_birth_msg_client_mock: MqttMockPahoClient, + recorded_calls: list[ReceiveMessage], + record_calls: MessageCallbackType, +) -> None: + """Test handling the socket connected and disconnected.""" + mqtt_client_mock = setup_with_birth_msg_client_mock + assert mqtt_client_mock.connect.call_count == 1 + + mqtt_client_mock.loop_misc.return_value = paho_mqtt.MQTT_ERR_SUCCESS + + client, server = socket.socketpair( + family=socket.AF_UNIX, type=socket.SOCK_STREAM, proto=0 + ) + client.setblocking(False) + server.setblocking(False) + mqtt_client_mock.on_socket_open(mqtt_client_mock, None, client) + mqtt_client_mock.on_socket_register_writer(mqtt_client_mock, None, client) + await hass.async_block_till_done() + + mqtt_client_mock.loop_write.side_effect = OSError("foo") + client.close() # close the client socket out from under the client + + assert mqtt_client_mock.connect.call_count == 1 + unsub = await mqtt.async_subscribe(hass, "test-topic", record_calls) + async_fire_time_changed(hass, utcnow() + timedelta(seconds=5)) + await hass.async_block_till_done() + + unsub() + # Should have failed + assert len(recorded_calls) == 0 + + +async def test_loop_write_failure( + hass: HomeAssistant, + setup_with_birth_msg_client_mock: MqttMockPahoClient, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test handling the socket connected and disconnected.""" + mqtt_client_mock = setup_with_birth_msg_client_mock + assert mqtt_client_mock.connect.call_count == 1 + + mqtt_client_mock.loop_misc.return_value = paho_mqtt.MQTT_ERR_SUCCESS + + client, server = socket.socketpair( + family=socket.AF_UNIX, type=socket.SOCK_STREAM, proto=0 + ) + client.setblocking(False) + server.setblocking(False) + mqtt_client_mock.on_socket_open(mqtt_client_mock, None, client) + mqtt_client_mock.on_socket_register_write(mqtt_client_mock, None, client) + mqtt_client_mock.loop_write.return_value = paho_mqtt.MQTT_ERR_CONN_LOST + mqtt_client_mock.loop_read.return_value = paho_mqtt.MQTT_ERR_CONN_LOST + + # Fill up the outgoing buffer to ensure that loop_write + # and loop_read are called that next time control is + # returned to the event loop + try: + for _ in range(1000): + server.send(b"long" * 100) + except BlockingIOError: + pass + + server.close() + # Once for the reader callback + await hass.async_block_till_done() + # Another for the writer callback + await hass.async_block_till_done() + # Final for the disconnect callback + await hass.async_block_till_done() + + assert "Disconnected from MQTT server test-broker:1883" in caplog.text diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index bcadf4a6506..403f7974878 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -1,25 +1,20 @@ -"""The tests for the MQTT component.""" +"""The tests for the MQTT component setup and helpers.""" import asyncio from copy import deepcopy from datetime import datetime, timedelta from functools import partial import json -import socket -import ssl import time from typing import Any, TypedDict -from unittest.mock import ANY, MagicMock, Mock, call, mock_open, patch +from unittest.mock import ANY, MagicMock, Mock, mock_open, patch -import certifi from freezegun.api import FrozenDateTimeFactory -import paho.mqtt.client as paho_mqtt import pytest import voluptuous as vol from homeassistant.components import mqtt from homeassistant.components.mqtt import debug_info -from homeassistant.components.mqtt.client import RECONNECT_INTERVAL_SECONDS from homeassistant.components.mqtt.models import ( MessageCallbackType, MqttCommandTemplateException, @@ -31,16 +26,12 @@ from homeassistant.components.sensor import SensorDeviceClass from homeassistant.config_entries import ConfigEntryDisabler, ConfigEntryState from homeassistant.const import ( ATTR_ASSUMED_STATE, - CONF_PROTOCOL, - EVENT_HOMEASSISTANT_STARTED, - EVENT_HOMEASSISTANT_STOP, SERVICE_RELOAD, STATE_UNAVAILABLE, STATE_UNKNOWN, - UnitOfTemperature, ) import homeassistant.core as ha -from homeassistant.core import CALLBACK_TYPE, CoreState, HomeAssistant, callback +from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError, ServiceValidationError from homeassistant.helpers import device_registry as dr, entity_registry as er, template from homeassistant.helpers.entity import Entity @@ -50,9 +41,6 @@ from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util from homeassistant.util.dt import utcnow -from .conftest import ENTRY_DEFAULT_BIRTH_MESSAGE -from .test_common import help_all_subscribe_calls - from tests.common import ( MockConfigEntry, MockEntity, @@ -63,7 +51,6 @@ from tests.common import ( ) from tests.components.sensor.common import MockSensor from tests.typing import ( - MqttMockHAClient, MqttMockHAClientGenerator, MqttMockPahoClient, WebSocketGenerator, @@ -95,205 +82,6 @@ def mock_storage(hass_storage: dict[str, Any]) -> None: """Autouse hass_storage for the TestCase tests.""" -def help_assert_message( - msg: ReceiveMessage, - topic: str | None = None, - payload: str | None = None, - qos: int | None = None, - retain: bool | None = None, -) -> bool: - """Return True if all of the given attributes match with the message.""" - match: bool = True - if topic is not None: - match &= msg.topic == topic - if payload is not None: - match &= msg.payload == payload - if qos is not None: - match &= msg.qos == qos - if retain is not None: - match &= msg.retain == retain - return match - - -async def test_mqtt_connects_on_home_assistant_mqtt_setup( - hass: HomeAssistant, setup_with_birth_msg_client_mock: MqttMockPahoClient -) -> None: - """Test if client is connected after mqtt init on bootstrap.""" - mqtt_client_mock = setup_with_birth_msg_client_mock - assert mqtt_client_mock.connect.call_count == 1 - - -async def test_mqtt_does_not_disconnect_on_home_assistant_stop( - hass: HomeAssistant, - mock_debouncer: asyncio.Event, - setup_with_birth_msg_client_mock: MqttMockPahoClient, -) -> None: - """Test if client is not disconnected on HA stop.""" - mqtt_client_mock = setup_with_birth_msg_client_mock - hass.bus.fire(EVENT_HOMEASSISTANT_STOP) - await mock_debouncer.wait() - assert mqtt_client_mock.disconnect.call_count == 0 - - -async def test_mqtt_await_ack_at_disconnect(hass: HomeAssistant) -> None: - """Test if ACK is awaited correctly when disconnecting.""" - - class FakeInfo: - """Returns a simulated client publish response.""" - - mid = 100 - rc = 0 - - with patch( - "homeassistant.components.mqtt.async_client.AsyncMQTTClient" - ) as mock_client: - mqtt_client = mock_client.return_value - mqtt_client.connect = MagicMock( - return_value=0, - side_effect=lambda *args, **kwargs: hass.loop.call_soon_threadsafe( - mqtt_client.on_connect, mqtt_client, None, 0, 0, 0 - ), - ) - mqtt_client.publish = MagicMock(return_value=FakeInfo()) - entry = MockConfigEntry( - domain=mqtt.DOMAIN, - data={ - "certificate": "auto", - mqtt.CONF_BROKER: "test-broker", - mqtt.CONF_DISCOVERY: False, - }, - ) - entry.add_to_hass(hass) - assert await hass.config_entries.async_setup(entry.entry_id) - - mqtt_client = mock_client.return_value - - # publish from MQTT client without awaiting - hass.async_create_task( - mqtt.async_publish(hass, "test-topic", "some-payload", 0, False) - ) - await asyncio.sleep(0) - # Simulate late ACK callback from client with mid 100 - mqtt_client.on_publish(0, 0, 100) - # disconnect the MQTT client - await hass.async_stop() - await hass.async_block_till_done() - # assert the payload was sent through the client - assert mqtt_client.publish.called - assert mqtt_client.publish.call_args[0] == ( - "test-topic", - "some-payload", - 0, - False, - ) - await hass.async_block_till_done(wait_background_tasks=True) - - -@pytest.mark.parametrize("mqtt_config_entry_data", [ENTRY_DEFAULT_BIRTH_MESSAGE]) -async def test_publish( - hass: HomeAssistant, setup_with_birth_msg_client_mock: MqttMockPahoClient -) -> None: - """Test the publish function.""" - publish_mock: MagicMock = setup_with_birth_msg_client_mock.publish - await mqtt.async_publish(hass, "test-topic", "test-payload") - await hass.async_block_till_done() - assert publish_mock.called - assert publish_mock.call_args[0] == ( - "test-topic", - "test-payload", - 0, - False, - ) - publish_mock.reset_mock() - - await mqtt.async_publish(hass, "test-topic", "test-payload", 2, True) - await hass.async_block_till_done() - assert publish_mock.called - assert publish_mock.call_args[0] == ( - "test-topic", - "test-payload", - 2, - True, - ) - publish_mock.reset_mock() - - mqtt.publish(hass, "test-topic2", "test-payload2") - await hass.async_block_till_done() - assert publish_mock.called - assert publish_mock.call_args[0] == ( - "test-topic2", - "test-payload2", - 0, - False, - ) - publish_mock.reset_mock() - - mqtt.publish(hass, "test-topic2", "test-payload2", 2, True) - await hass.async_block_till_done() - assert publish_mock.called - assert publish_mock.call_args[0] == ( - "test-topic2", - "test-payload2", - 2, - True, - ) - publish_mock.reset_mock() - - # test binary pass-through - mqtt.publish( - hass, - "test-topic3", - b"\xde\xad\xbe\xef", - 0, - False, - ) - await hass.async_block_till_done() - assert publish_mock.called - assert publish_mock.call_args[0] == ( - "test-topic3", - b"\xde\xad\xbe\xef", - 0, - False, - ) - publish_mock.reset_mock() - - # test null payload - mqtt.publish( - hass, - "test-topic3", - None, - 0, - False, - ) - await hass.async_block_till_done() - assert publish_mock.called - assert publish_mock.call_args[0] == ( - "test-topic3", - None, - 0, - False, - ) - - publish_mock.reset_mock() - - -async def test_convert_outgoing_payload(hass: HomeAssistant) -> None: - """Test the converting of outgoing MQTT payloads without template.""" - command_template = mqtt.MqttCommandTemplate(None, hass=hass) - assert command_template.async_render(b"\xde\xad\xbe\xef") == b"\xde\xad\xbe\xef" - - assert ( - command_template.async_render("b'\\xde\\xad\\xbe\\xef'") - == "b'\\xde\\xad\\xbe\\xef'" - ) - - assert command_template.async_render(1234) == 1234 - - assert command_template.async_render(1234.56) == 1234.56 - - assert command_template.async_render(None) is None - - async def test_command_template_value(hass: HomeAssistant) -> None: """Test the rendering of MQTT command template.""" @@ -983,893 +771,6 @@ async def test_receiving_message_with_non_utf8_topic_gets_logged( ) -async def test_all_subscriptions_run_when_decode_fails( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - recorded_calls: list[ReceiveMessage], - record_calls: MessageCallbackType, -) -> None: - """Test all other subscriptions still run when decode fails for one.""" - await mqtt_mock_entry() - await mqtt.async_subscribe(hass, "test-topic", record_calls, encoding="ascii") - await mqtt.async_subscribe(hass, "test-topic", record_calls) - - async_fire_mqtt_message(hass, "test-topic", UnitOfTemperature.CELSIUS) - - await hass.async_block_till_done() - assert len(recorded_calls) == 1 - - -async def test_subscribe_topic( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - recorded_calls: list[ReceiveMessage], - record_calls: MessageCallbackType, -) -> None: - """Test the subscription of a topic.""" - await mqtt_mock_entry() - unsub = await mqtt.async_subscribe(hass, "test-topic", record_calls) - - async_fire_mqtt_message(hass, "test-topic", "test-payload") - - await hass.async_block_till_done() - assert len(recorded_calls) == 1 - assert recorded_calls[0].topic == "test-topic" - assert recorded_calls[0].payload == "test-payload" - - unsub() - - async_fire_mqtt_message(hass, "test-topic", "test-payload") - - await hass.async_block_till_done() - assert len(recorded_calls) == 1 - - # Cannot unsubscribe twice - with pytest.raises(HomeAssistantError): - unsub() - - -@pytest.mark.usefixtures("mqtt_mock_entry") -async def test_subscribe_topic_not_initialize( - hass: HomeAssistant, record_calls: MessageCallbackType -) -> None: - """Test the subscription of a topic when MQTT was not initialized.""" - with pytest.raises( - HomeAssistantError, match=r".*make sure MQTT is set up correctly" - ): - await mqtt.async_subscribe(hass, "test-topic", record_calls) - - -async def test_subscribe_mqtt_config_entry_disabled( - hass: HomeAssistant, mqtt_mock: MqttMockHAClient, record_calls: MessageCallbackType -) -> None: - """Test the subscription of a topic when MQTT config entry is disabled.""" - mqtt_mock.connected = True - - mqtt_config_entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0] - assert mqtt_config_entry.state is ConfigEntryState.LOADED - - assert await hass.config_entries.async_unload(mqtt_config_entry.entry_id) - assert mqtt_config_entry.state is ConfigEntryState.NOT_LOADED - - await hass.config_entries.async_set_disabled_by( - mqtt_config_entry.entry_id, ConfigEntryDisabler.USER - ) - mqtt_mock.connected = False - - with pytest.raises(HomeAssistantError, match=r".*MQTT is not enabled"): - await mqtt.async_subscribe(hass, "test-topic", record_calls) - - -async def test_subscribe_and_resubscribe( - hass: HomeAssistant, - mock_debouncer: asyncio.Event, - setup_with_birth_msg_client_mock: MqttMockPahoClient, - recorded_calls: list[ReceiveMessage], - record_calls: MessageCallbackType, -) -> None: - """Test resubscribing within the debounce time.""" - mqtt_client_mock = setup_with_birth_msg_client_mock - with ( - patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.4), - patch("homeassistant.components.mqtt.client.UNSUBSCRIBE_COOLDOWN", 0.4), - ): - mock_debouncer.clear() - unsub = await mqtt.async_subscribe(hass, "test-topic", record_calls) - # This unsub will be un-done with the following subscribe - # unsubscribe should not be called at the broker - unsub() - unsub = await mqtt.async_subscribe(hass, "test-topic", record_calls) - await mock_debouncer.wait() - mock_debouncer.clear() - - async_fire_mqtt_message(hass, "test-topic", "test-payload") - - assert len(recorded_calls) == 1 - assert recorded_calls[0].topic == "test-topic" - assert recorded_calls[0].payload == "test-payload" - # assert unsubscribe was not called - mqtt_client_mock.unsubscribe.assert_not_called() - - mock_debouncer.clear() - unsub() - - await mock_debouncer.wait() - mqtt_client_mock.unsubscribe.assert_called_once_with(["test-topic"]) - - -async def test_subscribe_topic_non_async( - hass: HomeAssistant, - mock_debouncer: asyncio.Event, - mqtt_mock_entry: MqttMockHAClientGenerator, - recorded_calls: list[ReceiveMessage], - record_calls: MessageCallbackType, -) -> None: - """Test the subscription of a topic using the non-async function.""" - await mqtt_mock_entry() - await mock_debouncer.wait() - mock_debouncer.clear() - unsub = await hass.async_add_executor_job( - mqtt.subscribe, hass, "test-topic", record_calls - ) - await mock_debouncer.wait() - - async_fire_mqtt_message(hass, "test-topic", "test-payload") - - assert len(recorded_calls) == 1 - assert recorded_calls[0].topic == "test-topic" - assert recorded_calls[0].payload == "test-payload" - - mock_debouncer.clear() - await hass.async_add_executor_job(unsub) - await mock_debouncer.wait() - - async_fire_mqtt_message(hass, "test-topic", "test-payload") - - assert len(recorded_calls) == 1 - - -async def test_subscribe_bad_topic( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - record_calls: MessageCallbackType, -) -> None: - """Test the subscription of a topic.""" - await mqtt_mock_entry() - with pytest.raises(HomeAssistantError): - await mqtt.async_subscribe(hass, 55, record_calls) # type: ignore[arg-type] - - -async def test_subscribe_topic_not_match( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - recorded_calls: list[ReceiveMessage], - record_calls: MessageCallbackType, -) -> None: - """Test if subscribed topic is not a match.""" - await mqtt_mock_entry() - await mqtt.async_subscribe(hass, "test-topic", record_calls) - - async_fire_mqtt_message(hass, "another-test-topic", "test-payload") - - await hass.async_block_till_done() - assert len(recorded_calls) == 0 - - -async def test_subscribe_topic_level_wildcard( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - recorded_calls: list[ReceiveMessage], - record_calls: MessageCallbackType, -) -> None: - """Test the subscription of wildcard topics.""" - await mqtt_mock_entry() - await mqtt.async_subscribe(hass, "test-topic/+/on", record_calls) - - async_fire_mqtt_message(hass, "test-topic/bier/on", "test-payload") - - await hass.async_block_till_done() - assert len(recorded_calls) == 1 - assert recorded_calls[0].topic == "test-topic/bier/on" - assert recorded_calls[0].payload == "test-payload" - - -async def test_subscribe_topic_level_wildcard_no_subtree_match( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - recorded_calls: list[ReceiveMessage], - record_calls: MessageCallbackType, -) -> None: - """Test the subscription of wildcard topics.""" - await mqtt_mock_entry() - await mqtt.async_subscribe(hass, "test-topic/+/on", record_calls) - - async_fire_mqtt_message(hass, "test-topic/bier", "test-payload") - - await hass.async_block_till_done() - assert len(recorded_calls) == 0 - - -async def test_subscribe_topic_level_wildcard_root_topic_no_subtree_match( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - recorded_calls: list[ReceiveMessage], - record_calls: MessageCallbackType, -) -> None: - """Test the subscription of wildcard topics.""" - await mqtt_mock_entry() - await mqtt.async_subscribe(hass, "test-topic/#", record_calls) - - async_fire_mqtt_message(hass, "test-topic-123", "test-payload") - - await hass.async_block_till_done() - assert len(recorded_calls) == 0 - - -async def test_subscribe_topic_subtree_wildcard_subtree_topic( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - recorded_calls: list[ReceiveMessage], - record_calls: MessageCallbackType, -) -> None: - """Test the subscription of wildcard topics.""" - await mqtt_mock_entry() - await mqtt.async_subscribe(hass, "test-topic/#", record_calls) - - async_fire_mqtt_message(hass, "test-topic/bier/on", "test-payload") - - await hass.async_block_till_done() - assert len(recorded_calls) == 1 - assert recorded_calls[0].topic == "test-topic/bier/on" - assert recorded_calls[0].payload == "test-payload" - - -async def test_subscribe_topic_subtree_wildcard_root_topic( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - recorded_calls: list[ReceiveMessage], - record_calls: MessageCallbackType, -) -> None: - """Test the subscription of wildcard topics.""" - await mqtt_mock_entry() - await mqtt.async_subscribe(hass, "test-topic/#", record_calls) - - async_fire_mqtt_message(hass, "test-topic", "test-payload") - - await hass.async_block_till_done() - assert len(recorded_calls) == 1 - assert recorded_calls[0].topic == "test-topic" - assert recorded_calls[0].payload == "test-payload" - - -async def test_subscribe_topic_subtree_wildcard_no_match( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - recorded_calls: list[ReceiveMessage], - record_calls: MessageCallbackType, -) -> None: - """Test the subscription of wildcard topics.""" - await mqtt_mock_entry() - await mqtt.async_subscribe(hass, "test-topic/#", record_calls) - - async_fire_mqtt_message(hass, "another-test-topic", "test-payload") - - await hass.async_block_till_done() - assert len(recorded_calls) == 0 - - -async def test_subscribe_topic_level_wildcard_and_wildcard_root_topic( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - recorded_calls: list[ReceiveMessage], - record_calls: MessageCallbackType, -) -> None: - """Test the subscription of wildcard topics.""" - await mqtt_mock_entry() - await mqtt.async_subscribe(hass, "+/test-topic/#", record_calls) - - async_fire_mqtt_message(hass, "hi/test-topic", "test-payload") - - await hass.async_block_till_done() - assert len(recorded_calls) == 1 - assert recorded_calls[0].topic == "hi/test-topic" - assert recorded_calls[0].payload == "test-payload" - - -async def test_subscribe_topic_level_wildcard_and_wildcard_subtree_topic( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - recorded_calls: list[ReceiveMessage], - record_calls: MessageCallbackType, -) -> None: - """Test the subscription of wildcard topics.""" - await mqtt_mock_entry() - await mqtt.async_subscribe(hass, "+/test-topic/#", record_calls) - - async_fire_mqtt_message(hass, "hi/test-topic/here-iam", "test-payload") - - await hass.async_block_till_done() - assert len(recorded_calls) == 1 - assert recorded_calls[0].topic == "hi/test-topic/here-iam" - assert recorded_calls[0].payload == "test-payload" - - -async def test_subscribe_topic_level_wildcard_and_wildcard_level_no_match( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - recorded_calls: list[ReceiveMessage], - record_calls: MessageCallbackType, -) -> None: - """Test the subscription of wildcard topics.""" - await mqtt_mock_entry() - await mqtt.async_subscribe(hass, "+/test-topic/#", record_calls) - - async_fire_mqtt_message(hass, "hi/here-iam/test-topic", "test-payload") - - await hass.async_block_till_done() - assert len(recorded_calls) == 0 - - -async def test_subscribe_topic_level_wildcard_and_wildcard_no_match( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - recorded_calls: list[ReceiveMessage], - record_calls: MessageCallbackType, -) -> None: - """Test the subscription of wildcard topics.""" - await mqtt_mock_entry() - await mqtt.async_subscribe(hass, "+/test-topic/#", record_calls) - - async_fire_mqtt_message(hass, "hi/another-test-topic", "test-payload") - - await hass.async_block_till_done() - assert len(recorded_calls) == 0 - - -async def test_subscribe_topic_sys_root( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - recorded_calls: list[ReceiveMessage], - record_calls: MessageCallbackType, -) -> None: - """Test the subscription of $ root topics.""" - await mqtt_mock_entry() - await mqtt.async_subscribe(hass, "$test-topic/subtree/on", record_calls) - - async_fire_mqtt_message(hass, "$test-topic/subtree/on", "test-payload") - - await hass.async_block_till_done() - assert len(recorded_calls) == 1 - assert recorded_calls[0].topic == "$test-topic/subtree/on" - assert recorded_calls[0].payload == "test-payload" - - -async def test_subscribe_topic_sys_root_and_wildcard_topic( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - recorded_calls: list[ReceiveMessage], - record_calls: MessageCallbackType, -) -> None: - """Test the subscription of $ root and wildcard topics.""" - await mqtt_mock_entry() - await mqtt.async_subscribe(hass, "$test-topic/#", record_calls) - - async_fire_mqtt_message(hass, "$test-topic/some-topic", "test-payload") - - await hass.async_block_till_done() - assert len(recorded_calls) == 1 - assert recorded_calls[0].topic == "$test-topic/some-topic" - assert recorded_calls[0].payload == "test-payload" - - -async def test_subscribe_topic_sys_root_and_wildcard_subtree_topic( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - recorded_calls: list[ReceiveMessage], - record_calls: MessageCallbackType, -) -> None: - """Test the subscription of $ root and wildcard subtree topics.""" - await mqtt_mock_entry() - await mqtt.async_subscribe(hass, "$test-topic/subtree/#", record_calls) - - async_fire_mqtt_message(hass, "$test-topic/subtree/some-topic", "test-payload") - - await hass.async_block_till_done() - assert len(recorded_calls) == 1 - assert recorded_calls[0].topic == "$test-topic/subtree/some-topic" - assert recorded_calls[0].payload == "test-payload" - - -async def test_subscribe_special_characters( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - recorded_calls: list[ReceiveMessage], - record_calls: MessageCallbackType, -) -> None: - """Test the subscription to topics with special characters.""" - await mqtt_mock_entry() - topic = "/test-topic/$(.)[^]{-}" - payload = "p4y.l[]a|> ?" - - await mqtt.async_subscribe(hass, topic, record_calls) - - async_fire_mqtt_message(hass, topic, payload) - await hass.async_block_till_done() - assert len(recorded_calls) == 1 - assert recorded_calls[0].topic == topic - assert recorded_calls[0].payload == payload - - -async def test_subscribe_same_topic( - hass: HomeAssistant, - mock_debouncer: asyncio.Event, - setup_with_birth_msg_client_mock: MqttMockPahoClient, -) -> None: - """Test subscribing to same topic twice and simulate retained messages. - - When subscribing to the same topic again, SUBSCRIBE must be sent to the broker again - for it to resend any retained messages. - """ - mqtt_client_mock = setup_with_birth_msg_client_mock - calls_a: list[ReceiveMessage] = [] - calls_b: list[ReceiveMessage] = [] - - @callback - def _callback_a(msg: ReceiveMessage) -> None: - calls_a.append(msg) - - @callback - def _callback_b(msg: ReceiveMessage) -> None: - calls_b.append(msg) - - mqtt_client_mock.reset_mock() - mock_debouncer.clear() - await mqtt.async_subscribe(hass, "test/state", _callback_a, qos=0) - # Simulate a non retained message after the first subscription - async_fire_mqtt_message(hass, "test/state", "online", qos=0, retain=False) - await mock_debouncer.wait() - assert len(calls_a) == 1 - mqtt_client_mock.subscribe.assert_called() - calls_a = [] - mqtt_client_mock.reset_mock() - - await hass.async_block_till_done() - mock_debouncer.clear() - await mqtt.async_subscribe(hass, "test/state", _callback_b, qos=1) - # Simulate an other non retained message after the second subscription - async_fire_mqtt_message(hass, "test/state", "online", qos=0, retain=False) - await mock_debouncer.wait() - # Both subscriptions should receive updates - assert len(calls_a) == 1 - assert len(calls_b) == 1 - mqtt_client_mock.subscribe.assert_called() - - -async def test_replaying_payload_same_topic( - hass: HomeAssistant, - mock_debouncer: asyncio.Event, - setup_with_birth_msg_client_mock: MqttMockPahoClient, -) -> None: - """Test replaying retained messages. - - When subscribing to the same topic again, SUBSCRIBE must be sent to the broker again - for it to resend any retained messages for new subscriptions. - Retained messages must only be replayed for new subscriptions, except - when the MQTT client is reconnecting. - """ - mqtt_client_mock = setup_with_birth_msg_client_mock - calls_a: list[ReceiveMessage] = [] - calls_b: list[ReceiveMessage] = [] - - @callback - def _callback_a(msg: ReceiveMessage) -> None: - calls_a.append(msg) - - @callback - def _callback_b(msg: ReceiveMessage) -> None: - calls_b.append(msg) - - mqtt_client_mock.reset_mock() - mock_debouncer.clear() - await mqtt.async_subscribe(hass, "test/state", _callback_a) - await mock_debouncer.wait() - async_fire_mqtt_message( - hass, "test/state", "online", qos=0, retain=True - ) # Simulate a (retained) message played back - assert len(calls_a) == 1 - mqtt_client_mock.subscribe.assert_called() - calls_a = [] - mqtt_client_mock.reset_mock() - - mock_debouncer.clear() - await mqtt.async_subscribe(hass, "test/state", _callback_b) - await mock_debouncer.wait() - - # Simulate edge case where non retained message was received - # after subscription at HA but before the debouncer delay was passed. - # The message without retain flag directly after a subscription should - # be processed by both subscriptions. - async_fire_mqtt_message(hass, "test/state", "online", qos=0, retain=False) - - # Simulate a (retained) message played back on new subscriptions - async_fire_mqtt_message(hass, "test/state", "online", qos=0, retain=True) - - # The current subscription only received the message without retain flag - assert len(calls_a) == 1 - assert help_assert_message(calls_a[0], "test/state", "online", qos=0, retain=False) - # The retained message playback should only be processed by the new subscription. - # The existing subscription already got the latest update, hence the existing - # subscription should not receive the replayed (retained) message. - # Messages without retain flag are received on both subscriptions. - assert len(calls_b) == 2 - assert help_assert_message(calls_b[0], "test/state", "online", qos=0, retain=False) - assert help_assert_message(calls_b[1], "test/state", "online", qos=0, retain=True) - mqtt_client_mock.subscribe.assert_called() - - calls_a = [] - calls_b = [] - mqtt_client_mock.reset_mock() - - # Simulate new message played back on new subscriptions - # After connecting the retain flag will not be set, even if the - # payload published was retained, we cannot see that - async_fire_mqtt_message(hass, "test/state", "online", qos=0, retain=False) - assert len(calls_a) == 1 - assert help_assert_message(calls_a[0], "test/state", "online", qos=0, retain=False) - assert len(calls_b) == 1 - assert help_assert_message(calls_b[0], "test/state", "online", qos=0, retain=False) - - # Now simulate the broker was disconnected shortly - calls_a = [] - calls_b = [] - mqtt_client_mock.reset_mock() - mqtt_client_mock.on_disconnect(None, None, 0) - - mock_debouncer.clear() - mqtt_client_mock.on_connect(None, None, None, 0) - await mock_debouncer.wait() - mqtt_client_mock.subscribe.assert_called() - # Simulate a (retained) message played back after reconnecting - async_fire_mqtt_message(hass, "test/state", "online", qos=0, retain=True) - # Both subscriptions now should replay the retained message - assert len(calls_a) == 1 - assert help_assert_message(calls_a[0], "test/state", "online", qos=0, retain=True) - assert len(calls_b) == 1 - assert help_assert_message(calls_b[0], "test/state", "online", qos=0, retain=True) - - -async def test_replaying_payload_after_resubscribing( - hass: HomeAssistant, - mock_debouncer: asyncio.Event, - setup_with_birth_msg_client_mock: MqttMockPahoClient, -) -> None: - """Test replaying and filtering retained messages after resubscribing. - - When subscribing to the same topic again, SUBSCRIBE must be sent to the broker again - for it to resend any retained messages for new subscriptions. - Retained messages must only be replayed for new subscriptions, except - when the MQTT client is reconnection. - """ - mqtt_client_mock = setup_with_birth_msg_client_mock - calls_a: list[ReceiveMessage] = [] - - @callback - def _callback_a(msg: ReceiveMessage) -> None: - calls_a.append(msg) - - mqtt_client_mock.reset_mock() - mock_debouncer.clear() - unsub = await mqtt.async_subscribe(hass, "test/state", _callback_a) - await mock_debouncer.wait() - mqtt_client_mock.subscribe.assert_called() - - # Simulate a (retained) message played back - async_fire_mqtt_message(hass, "test/state", "online", qos=0, retain=True) - assert help_assert_message(calls_a[0], "test/state", "online", qos=0, retain=True) - calls_a.clear() - - # Test we get updates - async_fire_mqtt_message(hass, "test/state", "offline", qos=0, retain=False) - assert help_assert_message(calls_a[0], "test/state", "offline", qos=0, retain=False) - calls_a.clear() - - # Test we filter new retained updates - async_fire_mqtt_message(hass, "test/state", "offline", qos=0, retain=True) - await hass.async_block_till_done() - assert len(calls_a) == 0 - - # Unsubscribe an resubscribe again - mock_debouncer.clear() - unsub() - unsub = await mqtt.async_subscribe(hass, "test/state", _callback_a) - await mock_debouncer.wait() - mqtt_client_mock.subscribe.assert_called() - - # Simulate we can receive a (retained) played back message again - async_fire_mqtt_message(hass, "test/state", "online", qos=0, retain=True) - assert help_assert_message(calls_a[0], "test/state", "online", qos=0, retain=True) - - -async def test_replaying_payload_wildcard_topic( - hass: HomeAssistant, - mock_debouncer: asyncio.Event, - setup_with_birth_msg_client_mock: MqttMockPahoClient, -) -> None: - """Test replaying retained messages. - - When we have multiple subscriptions to the same wildcard topic, - SUBSCRIBE must be sent to the broker again - for it to resend any retained messages for new subscriptions. - Retained messages should only be replayed for new subscriptions, except - when the MQTT client is reconnection. - """ - mqtt_client_mock = setup_with_birth_msg_client_mock - calls_a: list[ReceiveMessage] = [] - calls_b: list[ReceiveMessage] = [] - - @callback - def _callback_a(msg: ReceiveMessage) -> None: - calls_a.append(msg) - - @callback - def _callback_b(msg: ReceiveMessage) -> None: - calls_b.append(msg) - - mqtt_client_mock.reset_mock() - mock_debouncer.clear() - await mqtt.async_subscribe(hass, "test/#", _callback_a) - await mock_debouncer.wait() - # Simulate (retained) messages being played back on new subscriptions - async_fire_mqtt_message(hass, "test/state1", "new_value_1", qos=0, retain=True) - async_fire_mqtt_message(hass, "test/state2", "new_value_2", qos=0, retain=True) - assert len(calls_a) == 2 - mqtt_client_mock.subscribe.assert_called() - calls_a = [] - mqtt_client_mock.reset_mock() - - # resubscribe to the wild card topic again - mock_debouncer.clear() - await mqtt.async_subscribe(hass, "test/#", _callback_b) - await mock_debouncer.wait() - # Simulate (retained) messages being played back on new subscriptions - async_fire_mqtt_message(hass, "test/state1", "initial_value_1", qos=0, retain=True) - async_fire_mqtt_message(hass, "test/state2", "initial_value_2", qos=0, retain=True) - # The retained messages playback should only be processed for the new subscriptions - assert len(calls_a) == 0 - assert len(calls_b) == 2 - mqtt_client_mock.subscribe.assert_called() - - calls_a = [] - calls_b = [] - mqtt_client_mock.reset_mock() - - # Simulate new messages being received - async_fire_mqtt_message(hass, "test/state1", "update_value_1", qos=0, retain=False) - async_fire_mqtt_message(hass, "test/state2", "update_value_2", qos=0, retain=False) - assert len(calls_a) == 2 - assert len(calls_b) == 2 - - # Now simulate the broker was disconnected shortly - calls_a = [] - calls_b = [] - mqtt_client_mock.reset_mock() - mqtt_client_mock.on_disconnect(None, None, 0) - - mock_debouncer.clear() - mqtt_client_mock.on_connect(None, None, None, 0) - await mock_debouncer.wait() - - mqtt_client_mock.subscribe.assert_called() - # Simulate the (retained) messages are played back after reconnecting - # for all subscriptions - async_fire_mqtt_message(hass, "test/state1", "update_value_1", qos=0, retain=True) - async_fire_mqtt_message(hass, "test/state2", "update_value_2", qos=0, retain=True) - # Both subscriptions should replay - assert len(calls_a) == 2 - assert len(calls_b) == 2 - - -async def test_not_calling_unsubscribe_with_active_subscribers( - hass: HomeAssistant, - mock_debouncer: asyncio.Event, - setup_with_birth_msg_client_mock: MqttMockPahoClient, - record_calls: MessageCallbackType, -) -> None: - """Test not calling unsubscribe() when other subscribers are active.""" - mqtt_client_mock = setup_with_birth_msg_client_mock - mqtt_client_mock.reset_mock() - mock_debouncer.clear() - unsub = await mqtt.async_subscribe(hass, "test/state", record_calls, 2) - await mqtt.async_subscribe(hass, "test/state", record_calls, 1) - await mock_debouncer.wait() - assert mqtt_client_mock.subscribe.called - - mock_debouncer.clear() - unsub() - await hass.async_block_till_done() - await hass.async_block_till_done(wait_background_tasks=True) - async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) # cooldown - assert not mqtt_client_mock.unsubscribe.called - assert not mock_debouncer.is_set() - - -async def test_not_calling_subscribe_when_unsubscribed_within_cooldown( - hass: HomeAssistant, - mock_debouncer: asyncio.Event, - mqtt_mock_entry: MqttMockHAClientGenerator, - record_calls: MessageCallbackType, -) -> None: - """Test not calling subscribe() when it is unsubscribed. - - Make sure subscriptions are cleared if unsubscribed before - the subscribe cool down period has ended. - """ - mqtt_mock = await mqtt_mock_entry() - mqtt_client_mock = mqtt_mock._mqttc - await mock_debouncer.wait() - - mock_debouncer.clear() - mqtt_client_mock.subscribe.reset_mock() - unsub = await mqtt.async_subscribe(hass, "test/state", record_calls) - unsub() - await mock_debouncer.wait() - # The debouncer executes without an pending subscribes - assert not mqtt_client_mock.subscribe.called - - -async def test_unsubscribe_race( - hass: HomeAssistant, - mock_debouncer: asyncio.Event, - setup_with_birth_msg_client_mock: MqttMockPahoClient, -) -> None: - """Test not calling unsubscribe() when other subscribers are active.""" - mqtt_client_mock = setup_with_birth_msg_client_mock - calls_a: list[ReceiveMessage] = [] - calls_b: list[ReceiveMessage] = [] - - @callback - def _callback_a(msg: ReceiveMessage) -> None: - calls_a.append(msg) - - @callback - def _callback_b(msg: ReceiveMessage) -> None: - calls_b.append(msg) - - mqtt_client_mock.reset_mock() - - mock_debouncer.clear() - unsub = await mqtt.async_subscribe(hass, "test/state", _callback_a) - unsub() - await mqtt.async_subscribe(hass, "test/state", _callback_b) - await mock_debouncer.wait() - - async_fire_mqtt_message(hass, "test/state", "online") - assert not calls_a - assert calls_b - - # We allow either calls [subscribe, unsubscribe, subscribe], [subscribe, subscribe] or - # when both subscriptions were combined [subscribe] - expected_calls_1 = [ - call.subscribe([("test/state", 0)]), - call.unsubscribe("test/state"), - call.subscribe([("test/state", 0)]), - ] - expected_calls_2 = [ - call.subscribe([("test/state", 0)]), - call.subscribe([("test/state", 0)]), - ] - expected_calls_3 = [ - call.subscribe([("test/state", 0)]), - ] - assert mqtt_client_mock.mock_calls in ( - expected_calls_1, - expected_calls_2, - expected_calls_3, - ) - - -@pytest.mark.parametrize( - "mqtt_config_entry_data", - [{mqtt.CONF_BROKER: "mock-broker", mqtt.CONF_DISCOVERY: False}], -) -async def test_restore_subscriptions_on_reconnect( - hass: HomeAssistant, - mock_debouncer: asyncio.Event, - setup_with_birth_msg_client_mock: MqttMockPahoClient, - record_calls: MessageCallbackType, -) -> None: - """Test subscriptions are restored on reconnect.""" - mqtt_client_mock = setup_with_birth_msg_client_mock - - mqtt_client_mock.reset_mock() - - mock_debouncer.clear() - await mqtt.async_subscribe(hass, "test/state", record_calls) - async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) # cooldown - await mock_debouncer.wait() - assert ("test/state", 0) in help_all_subscribe_calls(mqtt_client_mock) - - mqtt_client_mock.reset_mock() - mqtt_client_mock.on_disconnect(None, None, 0) - - mock_debouncer.clear() - mqtt_client_mock.on_connect(None, None, None, 0) - await mock_debouncer.wait() - assert ("test/state", 0) in help_all_subscribe_calls(mqtt_client_mock) - - -@pytest.mark.parametrize( - "mqtt_config_entry_data", - [{mqtt.CONF_BROKER: "mock-broker", mqtt.CONF_DISCOVERY: False}], -) -async def test_restore_all_active_subscriptions_on_reconnect( - hass: HomeAssistant, - mock_debouncer: asyncio.Event, - setup_with_birth_msg_client_mock: MqttMockPahoClient, - record_calls: MessageCallbackType, -) -> None: - """Test active subscriptions are restored correctly on reconnect.""" - mqtt_client_mock = setup_with_birth_msg_client_mock - mqtt_client_mock.reset_mock() - mock_debouncer.clear() - unsub = await mqtt.async_subscribe(hass, "test/state", record_calls, qos=2) - await mqtt.async_subscribe(hass, "test/state", record_calls, qos=1) - await mqtt.async_subscribe(hass, "test/state", record_calls, qos=0) - # cooldown - await mock_debouncer.wait() - - # the subscription with the highest QoS should survive - expected = [ - call([("test/state", 2)]), - ] - assert mqtt_client_mock.subscribe.mock_calls == expected - - unsub() - assert mqtt_client_mock.unsubscribe.call_count == 0 - - mqtt_client_mock.on_disconnect(None, None, 0) - - mock_debouncer.clear() - mqtt_client_mock.on_connect(None, None, None, 0) - # wait for cooldown - await mock_debouncer.wait() - - expected.append(call([("test/state", 1)])) - for expected_call in expected: - assert mqtt_client_mock.subscribe.hass_call(expected_call) - - -@pytest.mark.parametrize( - "mqtt_config_entry_data", - [{mqtt.CONF_BROKER: "mock-broker", mqtt.CONF_DISCOVERY: False}], -) -async def test_subscribed_at_highest_qos( - hass: HomeAssistant, - mock_debouncer: asyncio.Event, - setup_with_birth_msg_client_mock: MqttMockPahoClient, - record_calls: MessageCallbackType, -) -> None: - """Test the highest qos as assigned when subscribing to the same topic.""" - mqtt_client_mock = setup_with_birth_msg_client_mock - mqtt_client_mock.reset_mock() - mock_debouncer.clear() - await mqtt.async_subscribe(hass, "test/state", record_calls, qos=0) - await hass.async_block_till_done() - # cooldown - await mock_debouncer.wait() - assert ("test/state", 0) in help_all_subscribe_calls(mqtt_client_mock) - mqtt_client_mock.reset_mock() - - mock_debouncer.clear() - await mqtt.async_subscribe(hass, "test/state", record_calls, qos=1) - await mqtt.async_subscribe(hass, "test/state", record_calls, qos=2) - # cooldown - await mock_debouncer.wait() - - # the subscription with the highest QoS should survive - assert help_all_subscribe_calls(mqtt_client_mock) == [("test/state", 2)] - - @pytest.mark.usefixtures("mqtt_client_mock") async def test_reload_entry_with_restored_subscriptions( hass: HomeAssistant, @@ -1937,163 +838,6 @@ async def test_reload_entry_with_restored_subscriptions( assert recorded_calls[1].payload == "wild-card-payload3" -async def test_initial_setup_logs_error( - hass: HomeAssistant, - caplog: pytest.LogCaptureFixture, - mqtt_client_mock: MqttMockPahoClient, -) -> None: - """Test for setup failure if initial client connection fails.""" - entry = MockConfigEntry(domain=mqtt.DOMAIN, data={mqtt.CONF_BROKER: "test-broker"}) - entry.add_to_hass(hass) - mqtt_client_mock.connect.side_effect = MagicMock(return_value=1) - try: - assert await hass.config_entries.async_setup(entry.entry_id) - except HomeAssistantError: - assert True - assert "Failed to connect to MQTT server:" in caplog.text - - -async def test_logs_error_if_no_connect_broker( - hass: HomeAssistant, - caplog: pytest.LogCaptureFixture, - setup_with_birth_msg_client_mock: MqttMockPahoClient, -) -> None: - """Test for setup failure if connection to broker is missing.""" - mqtt_client_mock = setup_with_birth_msg_client_mock - # test with rc = 3 -> broker unavailable - mqtt_client_mock.on_disconnect(Mock(), None, 0) - mqtt_client_mock.on_connect(Mock(), None, None, 3) - await hass.async_block_till_done() - assert ( - "Unable to connect to the MQTT broker: Connection Refused: broker unavailable." - in caplog.text - ) - - -@pytest.mark.parametrize("return_code", [4, 5]) -async def test_triggers_reauth_flow_if_auth_fails( - hass: HomeAssistant, - setup_with_birth_msg_client_mock: MqttMockPahoClient, - return_code: int, -) -> None: - """Test re-auth is triggered if authentication is failing.""" - mqtt_client_mock = setup_with_birth_msg_client_mock - # test with rc = 4 -> CONNACK_REFUSED_NOT_AUTHORIZED and 5 -> CONNACK_REFUSED_BAD_USERNAME_PASSWORD - mqtt_client_mock.on_disconnect(Mock(), None, 0) - mqtt_client_mock.on_connect(Mock(), None, None, return_code) - await hass.async_block_till_done() - flows = hass.config_entries.flow.async_progress() - assert len(flows) == 1 - assert flows[0]["context"]["source"] == "reauth" - - -@patch("homeassistant.components.mqtt.client.TIMEOUT_ACK", 0.3) -async def test_handle_mqtt_on_callback( - hass: HomeAssistant, - caplog: pytest.LogCaptureFixture, - setup_with_birth_msg_client_mock: MqttMockPahoClient, -) -> None: - """Test receiving an ACK callback before waiting for it.""" - mqtt_client_mock = setup_with_birth_msg_client_mock - with patch.object(mqtt_client_mock, "get_mid", return_value=100): - # Simulate an ACK for mid == 100, this will call mqtt_mock._async_get_mid_future(mid) - mqtt_client_mock.on_publish(mqtt_client_mock, None, 100) - await hass.async_block_till_done() - # Make sure the ACK has been received - await hass.async_block_till_done() - # Now call publish without call back, this will call _async_async_wait_for_mid(msg_info.mid) - await mqtt.async_publish(hass, "no_callback/test-topic", "test-payload") - # Since the mid event was already set, we should not see any timeout warning in the log - await hass.async_block_till_done() - assert "No ACK from MQTT server" not in caplog.text - - -async def test_handle_mqtt_on_callback_after_timeout( - hass: HomeAssistant, - caplog: pytest.LogCaptureFixture, - mqtt_mock_entry: MqttMockHAClientGenerator, - mqtt_client_mock: MqttMockPahoClient, -) -> None: - """Test receiving an ACK after a timeout.""" - mqtt_mock = await mqtt_mock_entry() - # Simulate the mid future getting a timeout - mqtt_mock()._async_get_mid_future(101).set_exception(asyncio.TimeoutError) - # Simulate an ACK for mid == 101, being received after the timeout - mqtt_client_mock.on_publish(mqtt_client_mock, None, 101) - await hass.async_block_till_done() - assert "No ACK from MQTT server" not in caplog.text - assert "InvalidStateError" not in caplog.text - - -async def test_publish_error( - hass: HomeAssistant, caplog: pytest.LogCaptureFixture -) -> None: - """Test publish error.""" - entry = MockConfigEntry(domain=mqtt.DOMAIN, data={mqtt.CONF_BROKER: "test-broker"}) - entry.add_to_hass(hass) - - # simulate an Out of memory error - with patch( - "homeassistant.components.mqtt.async_client.AsyncMQTTClient" - ) as mock_client: - mock_client().connect = lambda *args: 1 - mock_client().publish().rc = 1 - assert await hass.config_entries.async_setup(entry.entry_id) - with pytest.raises(HomeAssistantError): - await mqtt.async_publish( - hass, "some-topic", b"test-payload", qos=0, retain=False, encoding=None - ) - assert "Failed to connect to MQTT server: Out of memory." in caplog.text - - -async def test_subscribe_error( - hass: HomeAssistant, - setup_with_birth_msg_client_mock: MqttMockPahoClient, - record_calls: MessageCallbackType, - caplog: pytest.LogCaptureFixture, -) -> None: - """Test publish error.""" - mqtt_client_mock = setup_with_birth_msg_client_mock - mqtt_client_mock.reset_mock() - # simulate client is not connected error before subscribing - mqtt_client_mock.subscribe.side_effect = lambda *args: (4, None) - await mqtt.async_subscribe(hass, "some-topic", record_calls) - while mqtt_client_mock.subscribe.call_count == 0: - await hass.async_block_till_done() - await hass.async_block_till_done() - assert ( - "Error talking to MQTT: The client is not currently connected." in caplog.text - ) - - -async def test_handle_message_callback( - hass: HomeAssistant, - mock_debouncer: asyncio.Event, - setup_with_birth_msg_client_mock: MqttMockPahoClient, -) -> None: - """Test for handling an incoming message callback.""" - mqtt_client_mock = setup_with_birth_msg_client_mock - callbacks = [] - - @callback - def _callback(args) -> None: - callbacks.append(args) - - msg = ReceiveMessage( - "some-topic", b"test-payload", 1, False, "some-topic", datetime.now() - ) - mock_debouncer.clear() - await mqtt.async_subscribe(hass, "some-topic", _callback) - await mock_debouncer.wait() - mqtt_client_mock.reset_mock() - mqtt_client_mock.on_message(None, None, msg) - - assert len(callbacks) == 1 - assert callbacks[0].topic == "some-topic" - assert callbacks[0].qos == 1 - assert callbacks[0].payload == "test-payload" - - @pytest.mark.parametrize( "hass_config", [ @@ -2128,491 +872,6 @@ async def test_setup_manual_mqtt_with_invalid_config( assert "required key not provided" in caplog.text -@pytest.mark.parametrize( - ("mqtt_config_entry_data", "protocol"), - [ - ( - { - mqtt.CONF_BROKER: "mock-broker", - CONF_PROTOCOL: "3.1", - }, - 3, - ), - ( - { - mqtt.CONF_BROKER: "mock-broker", - CONF_PROTOCOL: "3.1.1", - }, - 4, - ), - ( - { - mqtt.CONF_BROKER: "mock-broker", - CONF_PROTOCOL: "5", - }, - 5, - ), - ], -) -async def test_setup_mqtt_client_protocol( - mqtt_mock_entry: MqttMockHAClientGenerator, protocol: int -) -> None: - """Test MQTT client protocol setup.""" - with patch( - "homeassistant.components.mqtt.async_client.AsyncMQTTClient" - ) as mock_client: - await mqtt_mock_entry() - - # check if protocol setup was correctly - assert mock_client.call_args[1]["protocol"] == protocol - - -@patch("homeassistant.components.mqtt.client.TIMEOUT_ACK", 0.2) -async def test_handle_mqtt_timeout_on_callback( - hass: HomeAssistant, caplog: pytest.LogCaptureFixture, mock_debouncer: asyncio.Event -) -> None: - """Test publish without receiving an ACK callback.""" - mid = 0 - - class FakeInfo: - """Returns a simulated client publish response.""" - - mid = 102 - rc = 0 - - with patch( - "homeassistant.components.mqtt.async_client.AsyncMQTTClient" - ) as mock_client: - - def _mock_ack(topic: str, qos: int = 0) -> tuple[int, int]: - # Handle ACK for subscribe normally - nonlocal mid - mid += 1 - mock_client.on_subscribe(0, 0, mid) - return (0, mid) - - # We want to simulate the publish behaviour MQTT client - mock_client = mock_client.return_value - mock_client.publish.return_value = FakeInfo() - # Mock we get a mid and rc=0 - mock_client.subscribe.side_effect = _mock_ack - mock_client.unsubscribe.side_effect = _mock_ack - mock_client.connect = MagicMock( - return_value=0, - side_effect=lambda *args, **kwargs: hass.loop.call_soon_threadsafe( - mock_client.on_connect, mock_client, None, 0, 0, 0 - ), - ) - - entry = MockConfigEntry( - domain=mqtt.DOMAIN, data={mqtt.CONF_BROKER: "test-broker"} - ) - entry.add_to_hass(hass) - - # Set up the integration - mock_debouncer.clear() - assert await hass.config_entries.async_setup(entry.entry_id) - - # Now call we publish without simulating and ACK callback - await mqtt.async_publish(hass, "no_callback/test-topic", "test-payload") - await hass.async_block_till_done() - # There is no ACK so we should see a timeout in the log after publishing - assert len(mock_client.publish.mock_calls) == 1 - assert "No ACK from MQTT server" in caplog.text - # Ensure we stop lingering background tasks - await hass.config_entries.async_unload(entry.entry_id) - # Assert we did not have any completed subscribes, - # because the debouncer subscribe job failed to receive an ACK, - # and the time auto caused the debouncer job to fail. - assert not mock_debouncer.is_set() - - -async def test_setup_raises_config_entry_not_ready_if_no_connect_broker( - hass: HomeAssistant, caplog: pytest.LogCaptureFixture -) -> None: - """Test for setup failure if connection to broker is missing.""" - entry = MockConfigEntry(domain=mqtt.DOMAIN, data={mqtt.CONF_BROKER: "test-broker"}) - entry.add_to_hass(hass) - - with patch( - "homeassistant.components.mqtt.async_client.AsyncMQTTClient" - ) as mock_client: - mock_client().connect = MagicMock(side_effect=OSError("Connection error")) - assert await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - assert "Failed to connect to MQTT server due to exception:" in caplog.text - - -@pytest.mark.parametrize( - ("mqtt_config_entry_data", "insecure_param"), - [ - ({"broker": "test-broker", "certificate": "auto"}, "not set"), - ( - {"broker": "test-broker", "certificate": "auto", "tls_insecure": False}, - False, - ), - ({"broker": "test-broker", "certificate": "auto", "tls_insecure": True}, True), - ], -) -async def test_setup_uses_certificate_on_certificate_set_to_auto_and_insecure( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - insecure_param: bool | str, -) -> None: - """Test setup uses bundled certs when certificate is set to auto and insecure.""" - calls = [] - insecure_check = {"insecure": "not set"} - - def mock_tls_set( - certificate, certfile=None, keyfile=None, tls_version=None - ) -> None: - calls.append((certificate, certfile, keyfile, tls_version)) - - def mock_tls_insecure_set(insecure_param) -> None: - insecure_check["insecure"] = insecure_param - - with patch( - "homeassistant.components.mqtt.async_client.AsyncMQTTClient" - ) as mock_client: - mock_client().tls_set = mock_tls_set - mock_client().tls_insecure_set = mock_tls_insecure_set - await mqtt_mock_entry() - await hass.async_block_till_done() - - assert calls - - expected_certificate = certifi.where() - assert calls[0][0] == expected_certificate - - # test if insecure is set - assert insecure_check["insecure"] == insecure_param - - -@pytest.mark.parametrize( - "mqtt_config_entry_data", - [ - { - mqtt.CONF_BROKER: "mock-broker", - mqtt.CONF_CERTIFICATE: "auto", - } - ], -) -async def test_tls_version( - hass: HomeAssistant, - mqtt_client_mock: MqttMockPahoClient, - mqtt_mock_entry: MqttMockHAClientGenerator, -) -> None: - """Test setup defaults for tls.""" - await mqtt_mock_entry() - await hass.async_block_till_done() - assert ( - mqtt_client_mock.tls_set.mock_calls[0][2]["tls_version"] - == ssl.PROTOCOL_TLS_CLIENT - ) - - -@pytest.mark.parametrize( - "mqtt_config_entry_data", - [ - { - mqtt.CONF_BROKER: "mock-broker", - mqtt.CONF_BIRTH_MESSAGE: { - mqtt.ATTR_TOPIC: "birth", - mqtt.ATTR_PAYLOAD: "birth", - mqtt.ATTR_QOS: 0, - mqtt.ATTR_RETAIN: False, - }, - } - ], -) -@patch("homeassistant.components.mqtt.client.INITIAL_SUBSCRIBE_COOLDOWN", 0.0) -@patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.0) -@patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.0) -async def test_custom_birth_message( - hass: HomeAssistant, - mock_debouncer: asyncio.Event, - mqtt_config_entry_data: dict[str, Any], - mqtt_client_mock: MqttMockPahoClient, -) -> None: - """Test sending birth message.""" - - entry = MockConfigEntry(domain=mqtt.DOMAIN, data=mqtt_config_entry_data) - entry.add_to_hass(hass) - hass.config.components.add(mqtt.DOMAIN) - assert await hass.config_entries.async_setup(entry.entry_id) - mock_debouncer.clear() - hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) - # discovery cooldown - await mock_debouncer.wait() - # Wait for publish call to finish - await hass.async_block_till_done(wait_background_tasks=True) - mqtt_client_mock.publish.assert_called_with("birth", "birth", 0, False) - - -@pytest.mark.parametrize( - "mqtt_config_entry_data", - [ENTRY_DEFAULT_BIRTH_MESSAGE], -) -async def test_default_birth_message( - hass: HomeAssistant, setup_with_birth_msg_client_mock: MqttMockPahoClient -) -> None: - """Test sending birth message.""" - mqtt_client_mock = setup_with_birth_msg_client_mock - await hass.async_block_till_done(wait_background_tasks=True) - mqtt_client_mock.publish.assert_called_with( - "homeassistant/status", "online", 0, False - ) - - -@pytest.mark.parametrize( - "mqtt_config_entry_data", - [{mqtt.CONF_BROKER: "mock-broker", mqtt.CONF_BIRTH_MESSAGE: {}}], -) -@patch("homeassistant.components.mqtt.client.INITIAL_SUBSCRIBE_COOLDOWN", 0.0) -@patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.0) -@patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.0) -async def test_no_birth_message( - hass: HomeAssistant, - record_calls: MessageCallbackType, - mock_debouncer: asyncio.Event, - mqtt_config_entry_data: dict[str, Any], - mqtt_client_mock: MqttMockPahoClient, -) -> None: - """Test disabling birth message.""" - entry = MockConfigEntry(domain=mqtt.DOMAIN, data=mqtt_config_entry_data) - entry.add_to_hass(hass) - hass.config.components.add(mqtt.DOMAIN) - mock_debouncer.clear() - assert await hass.config_entries.async_setup(entry.entry_id) - # Wait for discovery cooldown - await mock_debouncer.wait() - # Ensure any publishing could have been processed - await hass.async_block_till_done(wait_background_tasks=True) - mqtt_client_mock.publish.assert_not_called() - - mqtt_client_mock.reset_mock() - mock_debouncer.clear() - await mqtt.async_subscribe(hass, "homeassistant/some-topic", record_calls) - # Wait for discovery cooldown - await mock_debouncer.wait() - mqtt_client_mock.subscribe.assert_called() - - -@pytest.mark.parametrize( - "mqtt_config_entry_data", - [ENTRY_DEFAULT_BIRTH_MESSAGE], -) -@patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.2) -async def test_delayed_birth_message( - hass: HomeAssistant, - mqtt_config_entry_data: dict[str, Any], - mqtt_client_mock: MqttMockPahoClient, -) -> None: - """Test sending birth message does not happen until Home Assistant starts.""" - hass.set_state(CoreState.starting) - await hass.async_block_till_done() - birth = asyncio.Event() - entry = MockConfigEntry(domain=mqtt.DOMAIN, data=mqtt_config_entry_data) - entry.add_to_hass(hass) - hass.config.components.add(mqtt.DOMAIN) - assert await hass.config_entries.async_setup(entry.entry_id) - - @callback - def wait_birth(msg: ReceiveMessage) -> None: - """Handle birth message.""" - birth.set() - - await mqtt.async_subscribe(hass, "homeassistant/status", wait_birth) - with pytest.raises(TimeoutError): - await asyncio.wait_for(birth.wait(), 0.05) - assert not mqtt_client_mock.publish.called - assert not birth.is_set() - - hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) - await birth.wait() - mqtt_client_mock.publish.assert_called_with( - "homeassistant/status", "online", 0, False - ) - - -@pytest.mark.parametrize( - "mqtt_config_entry_data", - [ENTRY_DEFAULT_BIRTH_MESSAGE], -) -async def test_subscription_done_when_birth_message_is_sent( - setup_with_birth_msg_client_mock: MqttMockPahoClient, -) -> None: - """Test sending birth message until initial subscription has been completed.""" - mqtt_client_mock = setup_with_birth_msg_client_mock - subscribe_calls = help_all_subscribe_calls(mqtt_client_mock) - assert ("homeassistant/+/+/config", 0) in subscribe_calls - assert ("homeassistant/+/+/+/config", 0) in subscribe_calls - mqtt_client_mock.publish.assert_called_with( - "homeassistant/status", "online", 0, False - ) - - -@pytest.mark.parametrize( - "mqtt_config_entry_data", - [ - { - mqtt.CONF_BROKER: "mock-broker", - mqtt.CONF_WILL_MESSAGE: { - mqtt.ATTR_TOPIC: "death", - mqtt.ATTR_PAYLOAD: "death", - mqtt.ATTR_QOS: 0, - mqtt.ATTR_RETAIN: False, - }, - } - ], -) -async def test_custom_will_message( - hass: HomeAssistant, - mqtt_config_entry_data: dict[str, Any], - mqtt_client_mock: MqttMockPahoClient, -) -> None: - """Test will message.""" - entry = MockConfigEntry(domain=mqtt.DOMAIN, data=mqtt_config_entry_data) - entry.add_to_hass(hass) - hass.config.components.add(mqtt.DOMAIN) - assert await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - - mqtt_client_mock.will_set.assert_called_with( - topic="death", payload="death", qos=0, retain=False - ) - - -async def test_default_will_message( - setup_with_birth_msg_client_mock: MqttMockPahoClient, -) -> None: - """Test will message.""" - mqtt_client_mock = setup_with_birth_msg_client_mock - mqtt_client_mock.will_set.assert_called_with( - topic="homeassistant/status", payload="offline", qos=0, retain=False - ) - - -@pytest.mark.parametrize( - "mqtt_config_entry_data", - [{mqtt.CONF_BROKER: "mock-broker", mqtt.CONF_WILL_MESSAGE: {}}], -) -async def test_no_will_message( - hass: HomeAssistant, - mqtt_config_entry_data: dict[str, Any], - mqtt_client_mock: MqttMockPahoClient, -) -> None: - """Test will message.""" - entry = MockConfigEntry(domain=mqtt.DOMAIN, data=mqtt_config_entry_data) - entry.add_to_hass(hass) - hass.config.components.add(mqtt.DOMAIN) - assert await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - - mqtt_client_mock.will_set.assert_not_called() - - -@pytest.mark.parametrize( - "mqtt_config_entry_data", - [ENTRY_DEFAULT_BIRTH_MESSAGE | {mqtt.CONF_DISCOVERY: False}], -) -async def test_mqtt_subscribes_topics_on_connect( - hass: HomeAssistant, - mock_debouncer: asyncio.Event, - setup_with_birth_msg_client_mock: MqttMockPahoClient, - record_calls: MessageCallbackType, -) -> None: - """Test subscription to topic on connect.""" - mqtt_client_mock = setup_with_birth_msg_client_mock - - mock_debouncer.clear() - await mqtt.async_subscribe(hass, "topic/test", record_calls) - await mqtt.async_subscribe(hass, "home/sensor", record_calls, 2) - await mqtt.async_subscribe(hass, "still/pending", record_calls) - await mqtt.async_subscribe(hass, "still/pending", record_calls, 1) - await mock_debouncer.wait() - - mqtt_client_mock.on_disconnect(Mock(), None, 0) - - mqtt_client_mock.reset_mock() - - mock_debouncer.clear() - mqtt_client_mock.on_connect(Mock(), None, 0, 0) - await mock_debouncer.wait() - - subscribe_calls = help_all_subscribe_calls(mqtt_client_mock) - assert ("topic/test", 0) in subscribe_calls - assert ("home/sensor", 2) in subscribe_calls - assert ("still/pending", 1) in subscribe_calls - - -@pytest.mark.parametrize( - "mqtt_config_entry_data", - [ENTRY_DEFAULT_BIRTH_MESSAGE], -) -async def test_mqtt_subscribes_in_single_call( - hass: HomeAssistant, - mock_debouncer: asyncio.Event, - setup_with_birth_msg_client_mock: MqttMockPahoClient, - record_calls: MessageCallbackType, -) -> None: - """Test bundled client subscription to topic.""" - mqtt_client_mock = setup_with_birth_msg_client_mock - mqtt_client_mock.subscribe.reset_mock() - mock_debouncer.clear() - await mqtt.async_subscribe(hass, "topic/test", record_calls) - await mqtt.async_subscribe(hass, "home/sensor", record_calls) - # Make sure the debouncer finishes - await mock_debouncer.wait() - - assert mqtt_client_mock.subscribe.call_count == 1 - # Assert we have a single subscription call with both subscriptions - assert mqtt_client_mock.subscribe.mock_calls[0][1][0] in [ - [("topic/test", 0), ("home/sensor", 0)], - [("home/sensor", 0), ("topic/test", 0)], - ] - - -@pytest.mark.parametrize("mqtt_config_entry_data", [ENTRY_DEFAULT_BIRTH_MESSAGE]) -@patch("homeassistant.components.mqtt.client.MAX_SUBSCRIBES_PER_CALL", 2) -@patch("homeassistant.components.mqtt.client.MAX_UNSUBSCRIBES_PER_CALL", 2) -async def test_mqtt_subscribes_and_unsubscribes_in_chunks( - hass: HomeAssistant, - mock_debouncer: asyncio.Event, - setup_with_birth_msg_client_mock: MqttMockPahoClient, - record_calls: MessageCallbackType, -) -> None: - """Test chunked client subscriptions.""" - mqtt_client_mock = setup_with_birth_msg_client_mock - - mqtt_client_mock.subscribe.reset_mock() - unsub_tasks: list[CALLBACK_TYPE] = [] - mock_debouncer.clear() - unsub_tasks.append(await mqtt.async_subscribe(hass, "topic/test1", record_calls)) - unsub_tasks.append(await mqtt.async_subscribe(hass, "home/sensor1", record_calls)) - unsub_tasks.append(await mqtt.async_subscribe(hass, "topic/test2", record_calls)) - unsub_tasks.append(await mqtt.async_subscribe(hass, "home/sensor2", record_calls)) - # Make sure the debouncer finishes - await mock_debouncer.wait() - - assert mqtt_client_mock.subscribe.call_count == 2 - # Assert we have a 2 subscription calls with both 2 subscriptions - assert len(mqtt_client_mock.subscribe.mock_calls[0][1][0]) == 2 - assert len(mqtt_client_mock.subscribe.mock_calls[1][1][0]) == 2 - - # Unsubscribe all topics - mock_debouncer.clear() - for task in unsub_tasks: - task() - # Make sure the debouncer finishes - await mock_debouncer.wait() - - assert mqtt_client_mock.unsubscribe.call_count == 2 - # Assert we have a 2 unsubscribe calls with both 2 topic - assert len(mqtt_client_mock.unsubscribe.mock_calls[0][1][0]) == 2 - assert len(mqtt_client_mock.unsubscribe.mock_calls[1][1][0]) == 2 - - @pytest.mark.usefixtures("mqtt_client_mock") async def test_default_entry_setting_are_applied( hass: HomeAssistant, device_registry: dr.DeviceRegistry @@ -4106,221 +2365,6 @@ async def test_multi_platform_discovery( ) -async def test_auto_reconnect( - hass: HomeAssistant, - setup_with_birth_msg_client_mock: MqttMockPahoClient, - caplog: pytest.LogCaptureFixture, -) -> None: - """Test reconnection is automatically done.""" - mqtt_client_mock = setup_with_birth_msg_client_mock - assert mqtt_client_mock.connect.call_count == 1 - mqtt_client_mock.reconnect.reset_mock() - - mqtt_client_mock.disconnect() - mqtt_client_mock.on_disconnect(None, None, 0) - await hass.async_block_till_done() - - mqtt_client_mock.reconnect.side_effect = OSError("foo") - async_fire_time_changed( - hass, utcnow() + timedelta(seconds=RECONNECT_INTERVAL_SECONDS) - ) - await hass.async_block_till_done() - assert len(mqtt_client_mock.reconnect.mock_calls) == 1 - assert "Error re-connecting to MQTT server due to exception: foo" in caplog.text - - mqtt_client_mock.reconnect.side_effect = None - async_fire_time_changed( - hass, utcnow() + timedelta(seconds=RECONNECT_INTERVAL_SECONDS) - ) - await hass.async_block_till_done() - assert len(mqtt_client_mock.reconnect.mock_calls) == 2 - - hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) - - mqtt_client_mock.disconnect() - mqtt_client_mock.on_disconnect(None, None, 0) - await hass.async_block_till_done() - - async_fire_time_changed( - hass, utcnow() + timedelta(seconds=RECONNECT_INTERVAL_SECONDS) - ) - await hass.async_block_till_done() - # Should not reconnect after stop - assert len(mqtt_client_mock.reconnect.mock_calls) == 2 - - -async def test_server_sock_connect_and_disconnect( - hass: HomeAssistant, - mock_debouncer: asyncio.Event, - setup_with_birth_msg_client_mock: MqttMockPahoClient, - recorded_calls: list[ReceiveMessage], - record_calls: MessageCallbackType, -) -> None: - """Test handling the socket connected and disconnected.""" - mqtt_client_mock = setup_with_birth_msg_client_mock - assert mqtt_client_mock.connect.call_count == 1 - - mqtt_client_mock.loop_misc.return_value = paho_mqtt.MQTT_ERR_SUCCESS - - client, server = socket.socketpair( - family=socket.AF_UNIX, type=socket.SOCK_STREAM, proto=0 - ) - client.setblocking(False) - server.setblocking(False) - mqtt_client_mock.on_socket_open(mqtt_client_mock, None, client) - mqtt_client_mock.on_socket_register_write(mqtt_client_mock, None, client) - await hass.async_block_till_done() - - server.close() # mock the server closing the connection on us - - mock_debouncer.clear() - unsub = await mqtt.async_subscribe(hass, "test-topic", record_calls) - await mock_debouncer.wait() - - mqtt_client_mock.loop_misc.return_value = paho_mqtt.MQTT_ERR_CONN_LOST - mqtt_client_mock.on_socket_unregister_write(mqtt_client_mock, None, client) - mqtt_client_mock.on_socket_close(mqtt_client_mock, None, client) - mqtt_client_mock.on_disconnect(mqtt_client_mock, None, client) - await hass.async_block_till_done() - mock_debouncer.clear() - unsub() - await hass.async_block_till_done() - assert not mock_debouncer.is_set() - - # Should have failed - assert len(recorded_calls) == 0 - - -async def test_server_sock_buffer_size( - hass: HomeAssistant, - setup_with_birth_msg_client_mock: MqttMockPahoClient, - caplog: pytest.LogCaptureFixture, -) -> None: - """Test handling the socket buffer size fails.""" - mqtt_client_mock = setup_with_birth_msg_client_mock - assert mqtt_client_mock.connect.call_count == 1 - - mqtt_client_mock.loop_misc.return_value = paho_mqtt.MQTT_ERR_SUCCESS - - client, server = socket.socketpair( - family=socket.AF_UNIX, type=socket.SOCK_STREAM, proto=0 - ) - client.setblocking(False) - server.setblocking(False) - with patch.object(client, "setsockopt", side_effect=OSError("foo")): - mqtt_client_mock.on_socket_open(mqtt_client_mock, None, client) - mqtt_client_mock.on_socket_register_write(mqtt_client_mock, None, client) - await hass.async_block_till_done() - assert "Unable to increase the socket buffer size" in caplog.text - - -async def test_server_sock_buffer_size_with_websocket( - hass: HomeAssistant, - setup_with_birth_msg_client_mock: MqttMockPahoClient, - caplog: pytest.LogCaptureFixture, -) -> None: - """Test handling the socket buffer size fails.""" - mqtt_client_mock = setup_with_birth_msg_client_mock - assert mqtt_client_mock.connect.call_count == 1 - - mqtt_client_mock.loop_misc.return_value = paho_mqtt.MQTT_ERR_SUCCESS - - client, server = socket.socketpair( - family=socket.AF_UNIX, type=socket.SOCK_STREAM, proto=0 - ) - client.setblocking(False) - server.setblocking(False) - - class FakeWebsocket(paho_mqtt.WebsocketWrapper): - def _do_handshake(self, *args, **kwargs): - pass - - wrapped_socket = FakeWebsocket(client, "127.0.01", 1, False, "/", None) - - with patch.object(client, "setsockopt", side_effect=OSError("foo")): - mqtt_client_mock.on_socket_open(mqtt_client_mock, None, wrapped_socket) - mqtt_client_mock.on_socket_register_write( - mqtt_client_mock, None, wrapped_socket - ) - await hass.async_block_till_done() - assert "Unable to increase the socket buffer size" in caplog.text - - -async def test_client_sock_failure_after_connect( - hass: HomeAssistant, - setup_with_birth_msg_client_mock: MqttMockPahoClient, - recorded_calls: list[ReceiveMessage], - record_calls: MessageCallbackType, -) -> None: - """Test handling the socket connected and disconnected.""" - mqtt_client_mock = setup_with_birth_msg_client_mock - assert mqtt_client_mock.connect.call_count == 1 - - mqtt_client_mock.loop_misc.return_value = paho_mqtt.MQTT_ERR_SUCCESS - - client, server = socket.socketpair( - family=socket.AF_UNIX, type=socket.SOCK_STREAM, proto=0 - ) - client.setblocking(False) - server.setblocking(False) - mqtt_client_mock.on_socket_open(mqtt_client_mock, None, client) - mqtt_client_mock.on_socket_register_writer(mqtt_client_mock, None, client) - await hass.async_block_till_done() - - mqtt_client_mock.loop_write.side_effect = OSError("foo") - client.close() # close the client socket out from under the client - - assert mqtt_client_mock.connect.call_count == 1 - unsub = await mqtt.async_subscribe(hass, "test-topic", record_calls) - async_fire_time_changed(hass, utcnow() + timedelta(seconds=5)) - await hass.async_block_till_done() - - unsub() - # Should have failed - assert len(recorded_calls) == 0 - - -async def test_loop_write_failure( - hass: HomeAssistant, - setup_with_birth_msg_client_mock: MqttMockPahoClient, - caplog: pytest.LogCaptureFixture, -) -> None: - """Test handling the socket connected and disconnected.""" - mqtt_client_mock = setup_with_birth_msg_client_mock - assert mqtt_client_mock.connect.call_count == 1 - - mqtt_client_mock.loop_misc.return_value = paho_mqtt.MQTT_ERR_SUCCESS - - client, server = socket.socketpair( - family=socket.AF_UNIX, type=socket.SOCK_STREAM, proto=0 - ) - client.setblocking(False) - server.setblocking(False) - mqtt_client_mock.on_socket_open(mqtt_client_mock, None, client) - mqtt_client_mock.on_socket_register_write(mqtt_client_mock, None, client) - mqtt_client_mock.loop_write.return_value = paho_mqtt.MQTT_ERR_CONN_LOST - mqtt_client_mock.loop_read.return_value = paho_mqtt.MQTT_ERR_CONN_LOST - - # Fill up the outgoing buffer to ensure that loop_write - # and loop_read are called that next time control is - # returned to the event loop - try: - for _ in range(1000): - server.send(b"long" * 100) - except BlockingIOError: - pass - - server.close() - # Once for the reader callback - await hass.async_block_till_done() - # Another for the writer callback - await hass.async_block_till_done() - # Final for the disconnect callback - await hass.async_block_till_done() - - assert "Disconnected from MQTT server test-broker:1883" in caplog.text - - @pytest.mark.parametrize( "attr", [ From 6f716c175387e8dc61c0db0383d3be61d5094e93 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 29 Jun 2024 11:06:56 -0500 Subject: [PATCH 1400/1445] Fix publish cancellation handling in MQTT (#120826) --- homeassistant/components/mqtt/client.py | 4 ++-- tests/components/mqtt/test_client.py | 17 +++++++++++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/mqtt/client.py b/homeassistant/components/mqtt/client.py index 7788c1db641..f65769badfa 100644 --- a/homeassistant/components/mqtt/client.py +++ b/homeassistant/components/mqtt/client.py @@ -1141,8 +1141,8 @@ class MQTT: # see https://github.com/eclipse/paho.mqtt.python/issues/687 # properties and reason codes are not used in Home Assistant future = self._async_get_mid_future(mid) - if future.done() and future.exception(): - # Timed out + if future.done() and (future.cancelled() or future.exception()): + # Timed out or cancelled return future.set_result(None) diff --git a/tests/components/mqtt/test_client.py b/tests/components/mqtt/test_client.py index 49b590383d1..cd02d805e1c 100644 --- a/tests/components/mqtt/test_client.py +++ b/tests/components/mqtt/test_client.py @@ -1194,6 +1194,23 @@ async def test_handle_mqtt_on_callback( assert "No ACK from MQTT server" not in caplog.text +async def test_handle_mqtt_on_callback_after_cancellation( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, + mqtt_mock_entry: MqttMockHAClientGenerator, + mqtt_client_mock: MqttMockPahoClient, +) -> None: + """Test receiving an ACK after a cancellation.""" + mqtt_mock = await mqtt_mock_entry() + # Simulate the mid future getting a cancellation + mqtt_mock()._async_get_mid_future(101).cancel() + # Simulate an ACK for mid == 101, being received after the cancellation + mqtt_client_mock.on_publish(mqtt_client_mock, None, 101) + await hass.async_block_till_done() + assert "No ACK from MQTT server" not in caplog.text + assert "InvalidStateError" not in caplog.text + + async def test_handle_mqtt_on_callback_after_timeout( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, From c19fb35d0233da33b50c357f590a0b84428587c1 Mon Sep 17 00:00:00 2001 From: Yuxin Wang Date: Mon, 1 Jul 2024 01:30:08 -0400 Subject: [PATCH 1401/1445] Add handling for different STATFLAG formats in APCUPSD (#120870) * Add handling for different STATFLAG formats * Just use removesuffix --- .../components/apcupsd/binary_sensor.py | 6 +++++- .../components/apcupsd/test_binary_sensor.py | 21 +++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/apcupsd/binary_sensor.py b/homeassistant/components/apcupsd/binary_sensor.py index 77b2b8591e5..5f86ceb6eec 100644 --- a/homeassistant/components/apcupsd/binary_sensor.py +++ b/homeassistant/components/apcupsd/binary_sensor.py @@ -68,4 +68,8 @@ class OnlineStatus(CoordinatorEntity[APCUPSdCoordinator], BinarySensorEntity): """Returns true if the UPS is online.""" # Check if ONLINE bit is set in STATFLAG. key = self.entity_description.key.upper() - return int(self.coordinator.data[key], 16) & _VALUE_ONLINE_MASK != 0 + # The daemon could either report just a hex ("0x05000008"), or a hex with a "Status Flag" + # suffix ("0x05000008 Status Flag") in older versions. + # Here we trim the suffix if it exists to support both. + flag = self.coordinator.data[key].removesuffix(" Status Flag") + return int(flag, 16) & _VALUE_ONLINE_MASK != 0 diff --git a/tests/components/apcupsd/test_binary_sensor.py b/tests/components/apcupsd/test_binary_sensor.py index 7616a960b21..02351109603 100644 --- a/tests/components/apcupsd/test_binary_sensor.py +++ b/tests/components/apcupsd/test_binary_sensor.py @@ -1,5 +1,7 @@ """Test binary sensors of APCUPSd integration.""" +import pytest + from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.util import slugify @@ -31,3 +33,22 @@ async def test_no_binary_sensor(hass: HomeAssistant) -> None: device_slug = slugify(MOCK_STATUS["UPSNAME"]) state = hass.states.get(f"binary_sensor.{device_slug}_online_status") assert state is None + + +@pytest.mark.parametrize( + ("override", "expected"), + [ + ("0x008", "on"), + ("0x02040010 Status Flag", "off"), + ], +) +async def test_statflag(hass: HomeAssistant, override: str, expected: str) -> None: + """Test binary sensor for different STATFLAG values.""" + status = MOCK_STATUS.copy() + status["STATFLAG"] = override + await async_init_integration(hass, status=status) + + device_slug = slugify(MOCK_STATUS["UPSNAME"]) + assert ( + hass.states.get(f"binary_sensor.{device_slug}_online_status").state == expected + ) From 3a0e85beb8e4cb68f2b1ae00408fae05e7888b31 Mon Sep 17 00:00:00 2001 From: Thomas55555 <59625598+Thomas55555@users.noreply.github.com> Date: Mon, 1 Jul 2024 01:12:33 +0200 Subject: [PATCH 1402/1445] Bump aioautomower to 2024.6.4 (#120875) --- homeassistant/components/husqvarna_automower/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/husqvarna_automower/manifest.json b/homeassistant/components/husqvarna_automower/manifest.json index 7883b057a3f..f27b04ef0c0 100644 --- a/homeassistant/components/husqvarna_automower/manifest.json +++ b/homeassistant/components/husqvarna_automower/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/husqvarna_automower", "iot_class": "cloud_push", "loggers": ["aioautomower"], - "requirements": ["aioautomower==2024.6.3"] + "requirements": ["aioautomower==2024.6.4"] } diff --git a/requirements_all.txt b/requirements_all.txt index dd68902baae..358147bfe73 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -204,7 +204,7 @@ aioaseko==0.1.1 aioasuswrt==1.4.0 # homeassistant.components.husqvarna_automower -aioautomower==2024.6.3 +aioautomower==2024.6.4 # homeassistant.components.azure_devops aioazuredevops==2.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 54e86d60186..9abd9e10de7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -183,7 +183,7 @@ aioaseko==0.1.1 aioasuswrt==1.4.0 # homeassistant.components.husqvarna_automower -aioautomower==2024.6.3 +aioautomower==2024.6.4 # homeassistant.components.azure_devops aioazuredevops==2.1.1 From a9740faeda331c46d3cfa7b9d0ffc7b8b85a4412 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Mon, 1 Jul 2024 20:06:56 +0300 Subject: [PATCH 1403/1445] Fix Shelly device shutdown (#120881) --- homeassistant/components/shelly/__init__.py | 6 ++++++ .../components/shelly/config_flow.py | 19 ++++++++++++------- homeassistant/components/shelly/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 21 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index 184b7c8bb6b..75f66d0bced 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -174,10 +174,13 @@ async def _async_setup_block_entry( await device.initialize() if not device.firmware_supported: async_create_issue_unsupported_firmware(hass, entry) + await device.shutdown() raise ConfigEntryNotReady except (DeviceConnectionError, MacAddressMismatchError) as err: + await device.shutdown() raise ConfigEntryNotReady(repr(err)) from err except InvalidAuthError as err: + await device.shutdown() raise ConfigEntryAuthFailed(repr(err)) from err runtime_data.block = ShellyBlockCoordinator(hass, entry, device) @@ -247,10 +250,13 @@ async def _async_setup_rpc_entry(hass: HomeAssistant, entry: ShellyConfigEntry) await device.initialize() if not device.firmware_supported: async_create_issue_unsupported_firmware(hass, entry) + await device.shutdown() raise ConfigEntryNotReady except (DeviceConnectionError, MacAddressMismatchError) as err: + await device.shutdown() raise ConfigEntryNotReady(repr(err)) from err except InvalidAuthError as err: + await device.shutdown() raise ConfigEntryAuthFailed(repr(err)) from err runtime_data.rpc = ShellyRpcCoordinator(hass, entry, device) diff --git a/homeassistant/components/shelly/config_flow.py b/homeassistant/components/shelly/config_flow.py index c044d032170..cb3bca6aa47 100644 --- a/homeassistant/components/shelly/config_flow.py +++ b/homeassistant/components/shelly/config_flow.py @@ -102,10 +102,11 @@ async def validate_input( ws_context, options, ) - await rpc_device.initialize() - await rpc_device.shutdown() - - sleep_period = get_rpc_device_wakeup_period(rpc_device.status) + try: + await rpc_device.initialize() + sleep_period = get_rpc_device_wakeup_period(rpc_device.status) + finally: + await rpc_device.shutdown() return { "title": rpc_device.name, @@ -121,11 +122,15 @@ async def validate_input( coap_context, options, ) - await block_device.initialize() - await block_device.shutdown() + try: + await block_device.initialize() + sleep_period = get_block_device_sleep_period(block_device.settings) + finally: + await block_device.shutdown() + return { "title": block_device.name, - CONF_SLEEP_PERIOD: get_block_device_sleep_period(block_device.settings), + CONF_SLEEP_PERIOD: sleep_period, "model": block_device.model, CONF_GEN: gen, } diff --git a/homeassistant/components/shelly/manifest.json b/homeassistant/components/shelly/manifest.json index b1b00e40c66..4076f53c28c 100644 --- a/homeassistant/components/shelly/manifest.json +++ b/homeassistant/components/shelly/manifest.json @@ -9,7 +9,7 @@ "iot_class": "local_push", "loggers": ["aioshelly"], "quality_scale": "platinum", - "requirements": ["aioshelly==10.0.1"], + "requirements": ["aioshelly==11.0.0"], "zeroconf": [ { "type": "_http._tcp.local.", diff --git a/requirements_all.txt b/requirements_all.txt index 358147bfe73..5649fd1d86c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -362,7 +362,7 @@ aioruuvigateway==0.1.0 aiosenz==1.0.0 # homeassistant.components.shelly -aioshelly==10.0.1 +aioshelly==11.0.0 # homeassistant.components.skybell aioskybell==22.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9abd9e10de7..d6755f889b7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -335,7 +335,7 @@ aioruuvigateway==0.1.0 aiosenz==1.0.0 # homeassistant.components.shelly -aioshelly==10.0.1 +aioshelly==11.0.0 # homeassistant.components.skybell aioskybell==22.7.0 From 779a7ddaa23e265e395f0c2f7fd1b15bf3b50576 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Mon, 1 Jul 2024 01:46:10 -0700 Subject: [PATCH 1404/1445] Bump ical to 8.1.1 (#120888) --- homeassistant/components/google/manifest.json | 2 +- homeassistant/components/local_calendar/manifest.json | 2 +- homeassistant/components/local_todo/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/google/manifest.json b/homeassistant/components/google/manifest.json index 5fc28d2f398..d40daa89b0e 100644 --- a/homeassistant/components/google/manifest.json +++ b/homeassistant/components/google/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/calendar.google", "iot_class": "cloud_polling", "loggers": ["googleapiclient"], - "requirements": ["gcal-sync==6.1.3", "oauth2client==4.1.3", "ical==8.0.1"] + "requirements": ["gcal-sync==6.1.3", "oauth2client==4.1.3", "ical==8.1.1"] } diff --git a/homeassistant/components/local_calendar/manifest.json b/homeassistant/components/local_calendar/manifest.json index 73619b6bfe9..95c65089c79 100644 --- a/homeassistant/components/local_calendar/manifest.json +++ b/homeassistant/components/local_calendar/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/local_calendar", "iot_class": "local_polling", "loggers": ["ical"], - "requirements": ["ical==8.0.1"] + "requirements": ["ical==8.1.1"] } diff --git a/homeassistant/components/local_todo/manifest.json b/homeassistant/components/local_todo/manifest.json index 4fa8e2982f9..313315a34f6 100644 --- a/homeassistant/components/local_todo/manifest.json +++ b/homeassistant/components/local_todo/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/local_todo", "iot_class": "local_polling", - "requirements": ["ical==8.0.1"] + "requirements": ["ical==8.1.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 5649fd1d86c..d946ef51dcf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1131,7 +1131,7 @@ ibmiotf==0.3.4 # homeassistant.components.google # homeassistant.components.local_calendar # homeassistant.components.local_todo -ical==8.0.1 +ical==8.1.1 # homeassistant.components.ping icmplib==3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d6755f889b7..7980e3e4b64 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -927,7 +927,7 @@ ibeacon-ble==1.2.0 # homeassistant.components.google # homeassistant.components.local_calendar # homeassistant.components.local_todo -ical==8.0.1 +ical==8.1.1 # homeassistant.components.ping icmplib==3.0 From 5a052feb87b561dda15c8d434f4834b7fbe08a39 Mon Sep 17 00:00:00 2001 From: dougiteixeira <31328123+dougiteixeira@users.noreply.github.com> Date: Mon, 1 Jul 2024 09:21:41 -0300 Subject: [PATCH 1405/1445] Add missing translations for device class in Scrape (#120891) --- homeassistant/components/scrape/strings.json | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/scrape/strings.json b/homeassistant/components/scrape/strings.json index 9b534aed77b..42cf3001b75 100644 --- a/homeassistant/components/scrape/strings.json +++ b/homeassistant/components/scrape/strings.json @@ -139,18 +139,19 @@ "selector": { "device_class": { "options": { - "date": "[%key:component::sensor::entity_component::date::name%]", - "duration": "[%key:component::sensor::entity_component::duration::name%]", "apparent_power": "[%key:component::sensor::entity_component::apparent_power::name%]", "aqi": "[%key:component::sensor::entity_component::aqi::name%]", "atmospheric_pressure": "[%key:component::sensor::entity_component::atmospheric_pressure::name%]", "battery": "[%key:component::sensor::entity_component::battery::name%]", - "carbon_monoxide": "[%key:component::sensor::entity_component::carbon_monoxide::name%]", "carbon_dioxide": "[%key:component::sensor::entity_component::carbon_dioxide::name%]", + "carbon_monoxide": "[%key:component::sensor::entity_component::carbon_monoxide::name%]", + "conductivity": "[%key:component::sensor::entity_component::conductivity::name%]", "current": "[%key:component::sensor::entity_component::current::name%]", "data_rate": "[%key:component::sensor::entity_component::data_rate::name%]", "data_size": "[%key:component::sensor::entity_component::data_size::name%]", + "date": "[%key:component::sensor::entity_component::date::name%]", "distance": "[%key:component::sensor::entity_component::distance::name%]", + "duration": "[%key:component::sensor::entity_component::duration::name%]", "energy": "[%key:component::sensor::entity_component::energy::name%]", "energy_storage": "[%key:component::sensor::entity_component::energy_storage::name%]", "frequency": "[%key:component::sensor::entity_component::frequency::name%]", @@ -168,8 +169,8 @@ "pm1": "[%key:component::sensor::entity_component::pm1::name%]", "pm10": "[%key:component::sensor::entity_component::pm10::name%]", "pm25": "[%key:component::sensor::entity_component::pm25::name%]", - "power_factor": "[%key:component::sensor::entity_component::power_factor::name%]", "power": "[%key:component::sensor::entity_component::power::name%]", + "power_factor": "[%key:component::sensor::entity_component::power_factor::name%]", "precipitation": "[%key:component::sensor::entity_component::precipitation::name%]", "precipitation_intensity": "[%key:component::sensor::entity_component::precipitation_intensity::name%]", "pressure": "[%key:component::sensor::entity_component::pressure::name%]", @@ -184,6 +185,7 @@ "volatile_organic_compounds_parts": "[%key:component::sensor::entity_component::volatile_organic_compounds::name%]", "voltage": "[%key:component::sensor::entity_component::voltage::name%]", "volume": "[%key:component::sensor::entity_component::volume::name%]", + "volume_flow_rate": "[%key:component::sensor::entity_component::volume_flow_rate::name%]", "volume_storage": "[%key:component::sensor::entity_component::volume_storage::name%]", "water": "[%key:component::sensor::entity_component::water::name%]", "weight": "[%key:component::sensor::entity_component::weight::name%]", From a0f8012f4858e4c6ff3f86515e11e06a6a90414b Mon Sep 17 00:00:00 2001 From: dougiteixeira <31328123+dougiteixeira@users.noreply.github.com> Date: Mon, 1 Jul 2024 02:44:59 -0300 Subject: [PATCH 1406/1445] Add missing translations for device class in SQL (#120892) --- homeassistant/components/sql/strings.json | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/sql/strings.json b/homeassistant/components/sql/strings.json index 361585b8876..cd36ccf7731 100644 --- a/homeassistant/components/sql/strings.json +++ b/homeassistant/components/sql/strings.json @@ -71,18 +71,19 @@ "selector": { "device_class": { "options": { - "date": "[%key:component::sensor::entity_component::date::name%]", - "duration": "[%key:component::sensor::entity_component::duration::name%]", "apparent_power": "[%key:component::sensor::entity_component::apparent_power::name%]", "aqi": "[%key:component::sensor::entity_component::aqi::name%]", "atmospheric_pressure": "[%key:component::sensor::entity_component::atmospheric_pressure::name%]", "battery": "[%key:component::sensor::entity_component::battery::name%]", - "carbon_monoxide": "[%key:component::sensor::entity_component::carbon_monoxide::name%]", "carbon_dioxide": "[%key:component::sensor::entity_component::carbon_dioxide::name%]", + "carbon_monoxide": "[%key:component::sensor::entity_component::carbon_monoxide::name%]", + "conductivity": "[%key:component::sensor::entity_component::conductivity::name%]", "current": "[%key:component::sensor::entity_component::current::name%]", "data_rate": "[%key:component::sensor::entity_component::data_rate::name%]", "data_size": "[%key:component::sensor::entity_component::data_size::name%]", + "date": "[%key:component::sensor::entity_component::date::name%]", "distance": "[%key:component::sensor::entity_component::distance::name%]", + "duration": "[%key:component::sensor::entity_component::duration::name%]", "energy": "[%key:component::sensor::entity_component::energy::name%]", "energy_storage": "[%key:component::sensor::entity_component::energy_storage::name%]", "frequency": "[%key:component::sensor::entity_component::frequency::name%]", @@ -100,8 +101,8 @@ "pm1": "[%key:component::sensor::entity_component::pm1::name%]", "pm10": "[%key:component::sensor::entity_component::pm10::name%]", "pm25": "[%key:component::sensor::entity_component::pm25::name%]", - "power_factor": "[%key:component::sensor::entity_component::power_factor::name%]", "power": "[%key:component::sensor::entity_component::power::name%]", + "power_factor": "[%key:component::sensor::entity_component::power_factor::name%]", "precipitation": "[%key:component::sensor::entity_component::precipitation::name%]", "precipitation_intensity": "[%key:component::sensor::entity_component::precipitation_intensity::name%]", "pressure": "[%key:component::sensor::entity_component::pressure::name%]", @@ -116,6 +117,7 @@ "volatile_organic_compounds_parts": "[%key:component::sensor::entity_component::volatile_organic_compounds::name%]", "voltage": "[%key:component::sensor::entity_component::voltage::name%]", "volume": "[%key:component::sensor::entity_component::volume::name%]", + "volume_flow_rate": "[%key:component::sensor::entity_component::volume_flow_rate::name%]", "volume_storage": "[%key:component::sensor::entity_component::volume_storage::name%]", "water": "[%key:component::sensor::entity_component::water::name%]", "weight": "[%key:component::sensor::entity_component::weight::name%]", From 16d7764f18f65712b4e78b9e3345fff5e060b9e8 Mon Sep 17 00:00:00 2001 From: dougiteixeira <31328123+dougiteixeira@users.noreply.github.com> Date: Mon, 1 Jul 2024 02:55:13 -0300 Subject: [PATCH 1407/1445] Add missing translations for device class in Template (#120893) --- homeassistant/components/template/strings.json | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/template/strings.json b/homeassistant/components/template/strings.json index 4a1377cbf0b..dc481b76ff8 100644 --- a/homeassistant/components/template/strings.json +++ b/homeassistant/components/template/strings.json @@ -105,6 +105,7 @@ "battery": "[%key:component::sensor::entity_component::battery::name%]", "carbon_dioxide": "[%key:component::sensor::entity_component::carbon_dioxide::name%]", "carbon_monoxide": "[%key:component::sensor::entity_component::carbon_monoxide::name%]", + "conductivity": "[%key:component::sensor::entity_component::conductivity::name%]", "current": "[%key:component::sensor::entity_component::current::name%]", "data_rate": "[%key:component::sensor::entity_component::data_rate::name%]", "data_size": "[%key:component::sensor::entity_component::data_size::name%]", From 88ed43c7792ff8732ad756522a39c925da6de408 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Mon, 1 Jul 2024 18:27:40 +0200 Subject: [PATCH 1408/1445] Improve add user error messages (#120909) --- homeassistant/auth/providers/homeassistant.py | 20 ++++++++-------- homeassistant/components/auth/strings.json | 5 +++- .../config/auth_provider_homeassistant.py | 24 ++++--------------- .../test_auth_provider_homeassistant.py | 16 +++++++++++-- 4 files changed, 32 insertions(+), 33 deletions(-) diff --git a/homeassistant/auth/providers/homeassistant.py b/homeassistant/auth/providers/homeassistant.py index 4e38260dd2f..ec39bdbdcdc 100644 --- a/homeassistant/auth/providers/homeassistant.py +++ b/homeassistant/auth/providers/homeassistant.py @@ -55,13 +55,6 @@ class InvalidUser(HomeAssistantError): Will not be raised when validating authentication. """ - -class InvalidUsername(InvalidUser): - """Raised when invalid username is specified. - - Will not be raised when validating authentication. - """ - def __init__( self, *args: object, @@ -77,6 +70,13 @@ class InvalidUsername(InvalidUser): ) +class InvalidUsername(InvalidUser): + """Raised when invalid username is specified. + + Will not be raised when validating authentication. + """ + + class Data: """Hold the user data.""" @@ -216,7 +216,7 @@ class Data: break if index is None: - raise InvalidUser + raise InvalidUser(translation_key="user_not_found") self.users.pop(index) @@ -232,7 +232,7 @@ class Data: user["password"] = self.hash_password(new_password, True).decode() break else: - raise InvalidUser + raise InvalidUser(translation_key="user_not_found") @callback def _validate_new_username(self, new_username: str) -> None: @@ -275,7 +275,7 @@ class Data: self._async_check_for_not_normalized_usernames(self._data) break else: - raise InvalidUser + raise InvalidUser(translation_key="user_not_found") async def async_save(self) -> None: """Save data.""" diff --git a/homeassistant/components/auth/strings.json b/homeassistant/components/auth/strings.json index 0e4cede78a3..c8622880f0f 100644 --- a/homeassistant/components/auth/strings.json +++ b/homeassistant/components/auth/strings.json @@ -37,7 +37,10 @@ "message": "Username \"{username}\" already exists" }, "username_not_normalized": { - "message": "Username \"{new_username}\" is not normalized" + "message": "Username \"{new_username}\" is not normalized. Please make sure the username is lowercase and does not contain any whitespace." + }, + "user_not_found": { + "message": "User not found" } }, "issues": { diff --git a/homeassistant/components/config/auth_provider_homeassistant.py b/homeassistant/components/config/auth_provider_homeassistant.py index 1cfcda6d4b2..8513c53bd07 100644 --- a/homeassistant/components/config/auth_provider_homeassistant.py +++ b/homeassistant/components/config/auth_provider_homeassistant.py @@ -53,11 +53,7 @@ async def websocket_create( ) return - try: - await provider.async_add_auth(msg["username"], msg["password"]) - except auth_ha.InvalidUser: - connection.send_error(msg["id"], "username_exists", "Username already exists") - return + await provider.async_add_auth(msg["username"], msg["password"]) credentials = await provider.async_get_or_create_credentials( {"username": msg["username"]} @@ -94,13 +90,7 @@ async def websocket_delete( connection.send_result(msg["id"]) return - try: - await provider.async_remove_auth(msg["username"]) - except auth_ha.InvalidUser: - connection.send_error( - msg["id"], "auth_not_found", "Given username was not found." - ) - return + await provider.async_remove_auth(msg["username"]) connection.send_result(msg["id"]) @@ -187,14 +177,8 @@ async def websocket_admin_change_password( ) return - try: - await provider.async_change_password(username, msg["password"]) - connection.send_result(msg["id"]) - except auth_ha.InvalidUser: - connection.send_error( - msg["id"], "credentials_not_found", "Credentials not found" - ) - return + await provider.async_change_password(username, msg["password"]) + connection.send_result(msg["id"]) @websocket_api.websocket_command( diff --git a/tests/components/config/test_auth_provider_homeassistant.py b/tests/components/config/test_auth_provider_homeassistant.py index ffee88f91ec..6b580013968 100644 --- a/tests/components/config/test_auth_provider_homeassistant.py +++ b/tests/components/config/test_auth_provider_homeassistant.py @@ -183,7 +183,13 @@ async def test_create_auth_duplicate_username( result = await client.receive_json() assert not result["success"], result - assert result["error"]["code"] == "username_exists" + assert result["error"] == { + "code": "home_assistant_error", + "message": "username_already_exists", + "translation_key": "username_already_exists", + "translation_placeholders": {"username": "test-user"}, + "translation_domain": "auth", + } async def test_delete_removes_just_auth( @@ -282,7 +288,13 @@ async def test_delete_unknown_auth( result = await client.receive_json() assert not result["success"], result - assert result["error"]["code"] == "auth_not_found" + assert result["error"] == { + "code": "home_assistant_error", + "message": "user_not_found", + "translation_key": "user_not_found", + "translation_placeholders": None, + "translation_domain": "auth", + } async def test_change_password( From a787ce863371efc0620d6ea7e557d954dc637874 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Mon, 1 Jul 2024 13:06:14 +0200 Subject: [PATCH 1409/1445] Bump incomfort-client dependency to 0.6.3 (#120913) --- homeassistant/components/incomfort/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/incomfort/manifest.json b/homeassistant/components/incomfort/manifest.json index c0b536dabe5..93f350a8e2c 100644 --- a/homeassistant/components/incomfort/manifest.json +++ b/homeassistant/components/incomfort/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/incomfort", "iot_class": "local_polling", "loggers": ["incomfortclient"], - "requirements": ["incomfort-client==0.6.2"] + "requirements": ["incomfort-client==0.6.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index d946ef51dcf..6e51947fca1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1152,7 +1152,7 @@ ihcsdk==2.8.5 imgw_pib==1.0.5 # homeassistant.components.incomfort -incomfort-client==0.6.2 +incomfort-client==0.6.3 # homeassistant.components.influxdb influxdb-client==1.24.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7980e3e4b64..a0f675bc256 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -942,7 +942,7 @@ ifaddr==0.2.0 imgw_pib==1.0.5 # homeassistant.components.incomfort -incomfort-client==0.6.2 +incomfort-client==0.6.3 # homeassistant.components.influxdb influxdb-client==1.24.0 From 887ab1dc58c118f69bf3f2009042b3c4ccd93018 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 1 Jul 2024 17:52:30 +0200 Subject: [PATCH 1410/1445] Bump openai to 1.35.1 (#120926) Bump openai to 1.35.7 --- homeassistant/components/openai_conversation/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/openai_conversation/manifest.json b/homeassistant/components/openai_conversation/manifest.json index 0c06a3d4cd8..fcbdc996ce5 100644 --- a/homeassistant/components/openai_conversation/manifest.json +++ b/homeassistant/components/openai_conversation/manifest.json @@ -8,5 +8,5 @@ "documentation": "https://www.home-assistant.io/integrations/openai_conversation", "integration_type": "service", "iot_class": "cloud_polling", - "requirements": ["openai==1.3.8"] + "requirements": ["openai==1.35.7"] } diff --git a/requirements_all.txt b/requirements_all.txt index 6e51947fca1..d96b0266043 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1477,7 +1477,7 @@ open-garage==0.2.0 open-meteo==0.3.1 # homeassistant.components.openai_conversation -openai==1.3.8 +openai==1.35.7 # homeassistant.components.openerz openerz-api==0.3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a0f675bc256..2753f42ee93 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1198,7 +1198,7 @@ open-garage==0.2.0 open-meteo==0.3.1 # homeassistant.components.openai_conversation -openai==1.3.8 +openai==1.35.7 # homeassistant.components.openerz openerz-api==0.3.0 From 8a7e2c05a5844d6d2d72d0d5969a439030bd9a0e Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Mon, 1 Jul 2024 17:30:23 +0200 Subject: [PATCH 1411/1445] Mark dry/fan-only climate modes as supported for Panasonic room air conditioner (#120939) --- homeassistant/components/matter/climate.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/matter/climate.py b/homeassistant/components/matter/climate.py index d2656d59138..c97124f4305 100644 --- a/homeassistant/components/matter/climate.py +++ b/homeassistant/components/matter/climate.py @@ -60,6 +60,7 @@ SUPPORT_DRY_MODE_DEVICES: set[tuple[int, int]] = { # In the list below specify tuples of (vendorid, productid) of devices that # support dry mode. (0x0001, 0x0108), + (0x0001, 0x010A), (0x1209, 0x8007), } @@ -68,6 +69,7 @@ SUPPORT_FAN_MODE_DEVICES: set[tuple[int, int]] = { # In the list below specify tuples of (vendorid, productid) of devices that # support fan-only mode. (0x0001, 0x0108), + (0x0001, 0x010A), (0x1209, 0x8007), } From 4b2be448f0195b7db2d4a093e38ee187cec6b040 Mon Sep 17 00:00:00 2001 From: Steven B <51370195+sdb9696@users.noreply.github.com> Date: Mon, 1 Jul 2024 15:50:35 +0100 Subject: [PATCH 1412/1445] Bump python-kasa to 0.7.0.2 (#120940) --- homeassistant/components/tplink/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tplink/manifest.json b/homeassistant/components/tplink/manifest.json index 74b80771c65..1270bb3469b 100644 --- a/homeassistant/components/tplink/manifest.json +++ b/homeassistant/components/tplink/manifest.json @@ -297,5 +297,5 @@ "iot_class": "local_polling", "loggers": ["kasa"], "quality_scale": "platinum", - "requirements": ["python-kasa[speedups]==0.7.0.1"] + "requirements": ["python-kasa[speedups]==0.7.0.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index d96b0266043..c18ed2f439a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2275,7 +2275,7 @@ python-join-api==0.0.9 python-juicenet==1.1.0 # homeassistant.components.tplink -python-kasa[speedups]==0.7.0.1 +python-kasa[speedups]==0.7.0.2 # homeassistant.components.lirc # python-lirc==1.2.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2753f42ee93..6291a3dddca 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1775,7 +1775,7 @@ python-izone==1.2.9 python-juicenet==1.1.0 # homeassistant.components.tplink -python-kasa[speedups]==0.7.0.1 +python-kasa[speedups]==0.7.0.2 # homeassistant.components.matter python-matter-server==6.2.0b1 From d8f55763c50aa6c61b787a9364c5092cd559d223 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 1 Jul 2024 09:26:20 -0700 Subject: [PATCH 1413/1445] Downgrade logging previously reported asyncio block to debug (#120942) --- homeassistant/util/loop.py | 123 +++++++++++----- tests/util/test_loop.py | 282 +++++++++++++++++++++---------------- 2 files changed, 244 insertions(+), 161 deletions(-) diff --git a/homeassistant/util/loop.py b/homeassistant/util/loop.py index 866f35e79e2..d7593013046 100644 --- a/homeassistant/util/loop.py +++ b/homeassistant/util/loop.py @@ -4,6 +4,7 @@ from __future__ import annotations from collections.abc import Callable import functools +from functools import cache import linecache import logging import threading @@ -26,6 +27,11 @@ def _get_line_from_cache(filename: str, lineno: int) -> str: return (linecache.getline(filename, lineno) or "?").strip() +# Set of previously reported blocking calls +# (integration, filename, lineno) +_PREVIOUSLY_REPORTED: set[tuple[str | None, str, int | Any]] = set() + + def raise_for_blocking_call( func: Callable[..., Any], check_allowed: Callable[[dict[str, Any]], bool] | None = None, @@ -42,28 +48,48 @@ def raise_for_blocking_call( offender_filename = offender_frame.f_code.co_filename offender_lineno = offender_frame.f_lineno offender_line = _get_line_from_cache(offender_filename, offender_lineno) + report_key: tuple[str | None, str, int | Any] try: integration_frame = get_integration_frame() except MissingIntegrationFrame: # Did not source from integration? Hard error. + report_key = (None, offender_filename, offender_lineno) + was_reported = report_key in _PREVIOUSLY_REPORTED + _PREVIOUSLY_REPORTED.add(report_key) if not strict_core: - _LOGGER.warning( - "Detected blocking call to %s with args %s in %s, " - "line %s: %s inside the event loop; " - "This is causing stability issues. " - "Please create a bug report at " - "https://github.com/home-assistant/core/issues?q=is%%3Aopen+is%%3Aissue\n" - "%s\n" - "Traceback (most recent call last):\n%s", - func.__name__, - mapped_args.get("args"), - offender_filename, - offender_lineno, - offender_line, - _dev_help_message(func.__name__), - "".join(traceback.format_stack(f=offender_frame)), - ) + if was_reported: + _LOGGER.debug( + "Detected blocking call to %s with args %s in %s, " + "line %s: %s inside the event loop; " + "This is causing stability issues. " + "Please create a bug report at " + "https://github.com/home-assistant/core/issues?q=is%%3Aopen+is%%3Aissue\n" + "%s\n", + func.__name__, + mapped_args.get("args"), + offender_filename, + offender_lineno, + offender_line, + _dev_help_message(func.__name__), + ) + else: + _LOGGER.warning( + "Detected blocking call to %s with args %s in %s, " + "line %s: %s inside the event loop; " + "This is causing stability issues. " + "Please create a bug report at " + "https://github.com/home-assistant/core/issues?q=is%%3Aopen+is%%3Aissue\n" + "%s\n" + "Traceback (most recent call last):\n%s", + func.__name__, + mapped_args.get("args"), + offender_filename, + offender_lineno, + offender_line, + _dev_help_message(func.__name__), + "".join(traceback.format_stack(f=offender_frame)), + ) return if found_frame is None: @@ -77,32 +103,56 @@ def raise_for_blocking_call( f"{_dev_help_message(func.__name__)}" ) + report_key = (integration_frame.integration, offender_filename, offender_lineno) + was_reported = report_key in _PREVIOUSLY_REPORTED + _PREVIOUSLY_REPORTED.add(report_key) + report_issue = async_suggest_report_issue( async_get_hass_or_none(), integration_domain=integration_frame.integration, module=integration_frame.module, ) - _LOGGER.warning( - "Detected blocking call to %s with args %s " - "inside the event loop by %sintegration '%s' " - "at %s, line %s: %s (offender: %s, line %s: %s), please %s\n" - "%s\n" - "Traceback (most recent call last):\n%s", - func.__name__, - mapped_args.get("args"), - "custom " if integration_frame.custom_integration else "", - integration_frame.integration, - integration_frame.relative_filename, - integration_frame.line_number, - integration_frame.line, - offender_filename, - offender_lineno, - offender_line, - report_issue, - _dev_help_message(func.__name__), - "".join(traceback.format_stack(f=integration_frame.frame)), - ) + if was_reported: + _LOGGER.debug( + "Detected blocking call to %s with args %s " + "inside the event loop by %sintegration '%s' " + "at %s, line %s: %s (offender: %s, line %s: %s), please %s\n" + "%s\n", + func.__name__, + mapped_args.get("args"), + "custom " if integration_frame.custom_integration else "", + integration_frame.integration, + integration_frame.relative_filename, + integration_frame.line_number, + integration_frame.line, + offender_filename, + offender_lineno, + offender_line, + report_issue, + _dev_help_message(func.__name__), + ) + else: + _LOGGER.warning( + "Detected blocking call to %s with args %s " + "inside the event loop by %sintegration '%s' " + "at %s, line %s: %s (offender: %s, line %s: %s), please %s\n" + "%s\n" + "Traceback (most recent call last):\n%s", + func.__name__, + mapped_args.get("args"), + "custom " if integration_frame.custom_integration else "", + integration_frame.integration, + integration_frame.relative_filename, + integration_frame.line_number, + integration_frame.line, + offender_filename, + offender_lineno, + offender_line, + report_issue, + _dev_help_message(func.__name__), + "".join(traceback.format_stack(f=integration_frame.frame)), + ) if strict: raise RuntimeError( @@ -117,6 +167,7 @@ def raise_for_blocking_call( ) +@cache def _dev_help_message(what: str) -> str: """Generate help message to guide developers.""" return ( diff --git a/tests/util/test_loop.py b/tests/util/test_loop.py index 585f32a965f..f4846d98898 100644 --- a/tests/util/test_loop.py +++ b/tests/util/test_loop.py @@ -1,5 +1,7 @@ """Tests for async util methods from Python source.""" +from collections.abc import Generator +import contextlib import threading from unittest.mock import Mock, patch @@ -15,57 +17,14 @@ def banned_function(): """Mock banned function.""" -async def test_raise_for_blocking_call_async() -> None: - """Test raise_for_blocking_call detects when called from event loop without integration context.""" - with pytest.raises(RuntimeError): - haloop.raise_for_blocking_call(banned_function) - - -async def test_raise_for_blocking_call_async_non_strict_core( - caplog: pytest.LogCaptureFixture, -) -> None: - """Test non_strict_core raise_for_blocking_call detects from event loop without integration context.""" - haloop.raise_for_blocking_call(banned_function, strict_core=False) - assert "Detected blocking call to banned_function" in caplog.text - assert "Traceback (most recent call last)" in caplog.text - assert ( - "Please create a bug report at https://github.com/home-assistant/core/issues" - in caplog.text - ) - assert ( - "For developers, please see " - "https://developers.home-assistant.io/docs/asyncio_blocking_operations/#banned_function" - ) in caplog.text - - -async def test_raise_for_blocking_call_async_integration( - caplog: pytest.LogCaptureFixture, -) -> None: - """Test raise_for_blocking_call detects and raises when called from event loop from integration context.""" - frames = extract_stack_to_frame( - [ - Mock( - filename="/home/paulus/homeassistant/core.py", - lineno="23", - line="do_something()", - ), - Mock( - filename="/home/paulus/homeassistant/components/hue/light.py", - lineno="23", - line="self.light.is_on", - ), - Mock( - filename="/home/paulus/aiohue/lights.py", - lineno="2", - line="something()", - ), - ] - ) +@contextlib.contextmanager +def patch_get_current_frame(stack: list[Mock]) -> Generator[None, None, None]: + """Patch get_current_frame.""" + frames = extract_stack_to_frame(stack) with ( - pytest.raises(RuntimeError), patch( "homeassistant.helpers.frame.linecache.getline", - return_value="self.light.is_on", + return_value=stack[1].line, ), patch( "homeassistant.util.loop._get_line_from_cache", @@ -79,13 +38,104 @@ async def test_raise_for_blocking_call_async_integration( "homeassistant.helpers.frame.get_current_frame", return_value=frames, ), + ): + yield + + +async def test_raise_for_blocking_call_async() -> None: + """Test raise_for_blocking_call detects when called from event loop without integration context.""" + with pytest.raises(RuntimeError): + haloop.raise_for_blocking_call(banned_function) + + +async def test_raise_for_blocking_call_async_non_strict_core( + caplog: pytest.LogCaptureFixture, +) -> None: + """Test non_strict_core raise_for_blocking_call detects from event loop without integration context.""" + stack = [ + Mock( + filename="/home/paulus/homeassistant/core.py", + lineno="12", + line="do_something()", + ), + Mock( + filename="/home/paulus/homeassistant/core.py", + lineno="12", + line="self.light.is_on", + ), + Mock( + filename="/home/paulus/aiohue/lights.py", + lineno="2", + line="something()", + ), + ] + with patch_get_current_frame(stack): + haloop.raise_for_blocking_call(banned_function, strict_core=False) + assert "Detected blocking call to banned_function" in caplog.text + assert "Traceback (most recent call last)" in caplog.text + assert ( + "Please create a bug report at https://github.com/home-assistant/core/issues" + in caplog.text + ) + assert ( + "For developers, please see " + "https://developers.home-assistant.io/docs/asyncio_blocking_operations/#banned_function" + ) in caplog.text + + warnings = [ + record for record in caplog.get_records("call") if record.levelname == "WARNING" + ] + assert len(warnings) == 1 + caplog.clear() + + # Second call should log at debug + with patch_get_current_frame(stack): + haloop.raise_for_blocking_call(banned_function, strict_core=False) + + warnings = [ + record for record in caplog.get_records("call") if record.levelname == "WARNING" + ] + assert len(warnings) == 0 + assert ( + "For developers, please see " + "https://developers.home-assistant.io/docs/asyncio_blocking_operations/#banned_function" + ) in caplog.text + + # no expensive traceback on debug + assert "Traceback (most recent call last)" not in caplog.text + + +async def test_raise_for_blocking_call_async_integration( + caplog: pytest.LogCaptureFixture, +) -> None: + """Test raise_for_blocking_call detects and raises when called from event loop from integration context.""" + stack = [ + Mock( + filename="/home/paulus/homeassistant/core.py", + lineno="18", + line="do_something()", + ), + Mock( + filename="/home/paulus/homeassistant/components/hue/light.py", + lineno="18", + line="self.light.is_on", + ), + Mock( + filename="/home/paulus/aiohue/lights.py", + lineno="8", + line="something()", + ), + ] + with ( + pytest.raises(RuntimeError), + patch_get_current_frame(stack), ): haloop.raise_for_blocking_call(banned_function) assert ( "Detected blocking call to banned_function with args None" " inside the event loop by integration" - " 'hue' at homeassistant/components/hue/light.py, line 23: self.light.is_on " - "(offender: /home/paulus/aiohue/lights.py, line 2: mock_line), please create " + " 'hue' at homeassistant/components/hue/light.py, line 18: self.light.is_on " + "(offender: /home/paulus/aiohue/lights.py, line 8: mock_line), please create " "a bug report at https://github.com/home-assistant/core/issues?" "q=is%3Aopen+is%3Aissue+label%3A%22integration%3A+hue%22" in caplog.text ) @@ -99,55 +149,37 @@ async def test_raise_for_blocking_call_async_integration_non_strict( caplog: pytest.LogCaptureFixture, ) -> None: """Test raise_for_blocking_call detects when called from event loop from integration context.""" - frames = extract_stack_to_frame( - [ - Mock( - filename="/home/paulus/homeassistant/core.py", - lineno="23", - line="do_something()", - ), - Mock( - filename="/home/paulus/homeassistant/components/hue/light.py", - lineno="23", - line="self.light.is_on", - ), - Mock( - filename="/home/paulus/aiohue/lights.py", - lineno="2", - line="something()", - ), - ] - ) - with ( - patch( - "homeassistant.helpers.frame.linecache.getline", - return_value="self.light.is_on", + stack = [ + Mock( + filename="/home/paulus/homeassistant/core.py", + lineno="15", + line="do_something()", ), - patch( - "homeassistant.util.loop._get_line_from_cache", - return_value="mock_line", + Mock( + filename="/home/paulus/homeassistant/components/hue/light.py", + lineno="15", + line="self.light.is_on", ), - patch( - "homeassistant.util.loop.get_current_frame", - return_value=frames, + Mock( + filename="/home/paulus/aiohue/lights.py", + lineno="1", + line="something()", ), - patch( - "homeassistant.helpers.frame.get_current_frame", - return_value=frames, - ), - ): + ] + with patch_get_current_frame(stack): haloop.raise_for_blocking_call(banned_function, strict=False) + assert ( "Detected blocking call to banned_function with args None" " inside the event loop by integration" - " 'hue' at homeassistant/components/hue/light.py, line 23: self.light.is_on " - "(offender: /home/paulus/aiohue/lights.py, line 2: mock_line), " + " 'hue' at homeassistant/components/hue/light.py, line 15: self.light.is_on " + "(offender: /home/paulus/aiohue/lights.py, line 1: mock_line), " "please create a bug report at https://github.com/home-assistant/core/issues?" "q=is%3Aopen+is%3Aissue+label%3A%22integration%3A+hue%22" in caplog.text ) assert "Traceback (most recent call last)" in caplog.text assert ( - 'File "/home/paulus/homeassistant/components/hue/light.py", line 23' + 'File "/home/paulus/homeassistant/components/hue/light.py", line 15' in caplog.text ) assert ( @@ -158,62 +190,62 @@ async def test_raise_for_blocking_call_async_integration_non_strict( "For developers, please see " "https://developers.home-assistant.io/docs/asyncio_blocking_operations/#banned_function" ) in caplog.text + warnings = [ + record for record in caplog.get_records("call") if record.levelname == "WARNING" + ] + assert len(warnings) == 1 + caplog.clear() + + # Second call should log at debug + with patch_get_current_frame(stack): + haloop.raise_for_blocking_call(banned_function, strict=False) + + warnings = [ + record for record in caplog.get_records("call") if record.levelname == "WARNING" + ] + assert len(warnings) == 0 + assert ( + "For developers, please see " + "https://developers.home-assistant.io/docs/asyncio_blocking_operations/#banned_function" + ) in caplog.text + # no expensive traceback on debug + assert "Traceback (most recent call last)" not in caplog.text async def test_raise_for_blocking_call_async_custom( caplog: pytest.LogCaptureFixture, ) -> None: """Test raise_for_blocking_call detects when called from event loop with custom component context.""" - frames = extract_stack_to_frame( - [ - Mock( - filename="/home/paulus/homeassistant/core.py", - lineno="23", - line="do_something()", - ), - Mock( - filename="/home/paulus/config/custom_components/hue/light.py", - lineno="23", - line="self.light.is_on", - ), - Mock( - filename="/home/paulus/aiohue/lights.py", - lineno="2", - line="something()", - ), - ] - ) - with ( - pytest.raises(RuntimeError), - patch( - "homeassistant.helpers.frame.linecache.getline", - return_value="self.light.is_on", + stack = [ + Mock( + filename="/home/paulus/homeassistant/core.py", + lineno="12", + line="do_something()", ), - patch( - "homeassistant.util.loop._get_line_from_cache", - return_value="mock_line", + Mock( + filename="/home/paulus/config/custom_components/hue/light.py", + lineno="12", + line="self.light.is_on", ), - patch( - "homeassistant.util.loop.get_current_frame", - return_value=frames, + Mock( + filename="/home/paulus/aiohue/lights.py", + lineno="3", + line="something()", ), - patch( - "homeassistant.helpers.frame.get_current_frame", - return_value=frames, - ), - ): + ] + with pytest.raises(RuntimeError), patch_get_current_frame(stack): haloop.raise_for_blocking_call(banned_function) assert ( "Detected blocking call to banned_function with args None" " inside the event loop by custom " - "integration 'hue' at custom_components/hue/light.py, line 23: self.light.is_on" - " (offender: /home/paulus/aiohue/lights.py, line 2: mock_line), " + "integration 'hue' at custom_components/hue/light.py, line 12: self.light.is_on" + " (offender: /home/paulus/aiohue/lights.py, line 3: mock_line), " "please create a bug report at https://github.com/home-assistant/core/issues?" "q=is%3Aopen+is%3Aissue+label%3A%22integration%3A+hue%22" ) in caplog.text assert "Traceback (most recent call last)" in caplog.text assert ( - 'File "/home/paulus/config/custom_components/hue/light.py", line 23' + 'File "/home/paulus/config/custom_components/hue/light.py", line 12' in caplog.text ) assert ( From 2f307d6a8a8b08cbd793f5c2f6de84019fa5641b Mon Sep 17 00:00:00 2001 From: Markus Jacobsen Date: Mon, 1 Jul 2024 19:02:43 +0200 Subject: [PATCH 1414/1445] Fix Bang & Olufsen jumping volume bar (#120946) --- homeassistant/components/bang_olufsen/media_player.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/bang_olufsen/media_player.py b/homeassistant/components/bang_olufsen/media_player.py index 0eff9f2bb85..07e38d633a1 100644 --- a/homeassistant/components/bang_olufsen/media_player.py +++ b/homeassistant/components/bang_olufsen/media_player.py @@ -366,7 +366,7 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity): @property def volume_level(self) -> float | None: """Volume level of the media player (0..1).""" - if self._volume.level and self._volume.level.level: + if self._volume.level and self._volume.level.level is not None: return float(self._volume.level.level / 100) return None From 74687f3b6009715e1685fbe260c00c2a2274ec53 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 1 Jul 2024 19:44:51 +0200 Subject: [PATCH 1415/1445] Bump version to 2024.7.0b7 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index e97f14f830c..5f020a02624 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -24,7 +24,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 7 -PATCH_VERSION: Final = "0b6" +PATCH_VERSION: Final = "0b7" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0) diff --git a/pyproject.toml b/pyproject.toml index 0f4b25eb0cc..6320551a082 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.7.0b6" +version = "2024.7.0b7" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 1e6dc74812000d4fd98de4031b3157afa71517b5 Mon Sep 17 00:00:00 2001 From: Teemu R Date: Tue, 2 Jul 2024 08:23:07 +0200 Subject: [PATCH 1416/1445] Minor polishing for tplink (#120868) --- homeassistant/components/tplink/climate.py | 11 ++++--- homeassistant/components/tplink/entity.py | 24 +++++++------- homeassistant/components/tplink/fan.py | 3 +- homeassistant/components/tplink/light.py | 32 +++++++++---------- homeassistant/components/tplink/sensor.py | 18 +---------- homeassistant/components/tplink/switch.py | 19 +---------- .../components/tplink/fixtures/features.json | 2 +- .../tplink/snapshots/test_sensor.ambr | 4 +-- tests/components/tplink/test_light.py | 16 ++++++---- 9 files changed, 51 insertions(+), 78 deletions(-) diff --git a/homeassistant/components/tplink/climate.py b/homeassistant/components/tplink/climate.py index 99a8c43fac3..3bd6aba5c26 100644 --- a/homeassistant/components/tplink/climate.py +++ b/homeassistant/components/tplink/climate.py @@ -77,16 +77,17 @@ class TPLinkClimateEntity(CoordinatedTPLinkEntity, ClimateEntity): parent: Device, ) -> None: """Initialize the climate entity.""" - super().__init__(device, coordinator, parent=parent) - self._state_feature = self._device.features["state"] - self._mode_feature = self._device.features["thermostat_mode"] - self._temp_feature = self._device.features["temperature"] - self._target_feature = self._device.features["target_temperature"] + self._state_feature = device.features["state"] + self._mode_feature = device.features["thermostat_mode"] + self._temp_feature = device.features["temperature"] + self._target_feature = device.features["target_temperature"] self._attr_min_temp = self._target_feature.minimum_value self._attr_max_temp = self._target_feature.maximum_value self._attr_temperature_unit = UNIT_MAPPING[cast(str, self._temp_feature.unit)] + super().__init__(device, coordinator, parent=parent) + @async_refresh_after async def async_set_temperature(self, **kwargs: Any) -> None: """Set target temperature.""" diff --git a/homeassistant/components/tplink/entity.py b/homeassistant/components/tplink/entity.py index 4e8ec0e0779..4ec0480cf82 100644 --- a/homeassistant/components/tplink/entity.py +++ b/homeassistant/components/tplink/entity.py @@ -56,15 +56,21 @@ DEVICETYPES_WITH_SPECIALIZED_PLATFORMS = { DeviceType.Thermostat, } +# Primary features to always include even when the device type has its own platform +FEATURES_ALLOW_LIST = { + # lights have current_consumption and a specialized platform + "current_consumption" +} + + # Features excluded due to future platform additions EXCLUDED_FEATURES = { # update "current_firmware_version", "available_firmware_version", - # fan - "fan_speed_level", } + LEGACY_KEY_MAPPING = { "current": ATTR_CURRENT_A, "current_consumption": ATTR_CURRENT_POWER_W, @@ -179,15 +185,12 @@ class CoordinatedTPLinkEntity(CoordinatorEntity[TPLinkDataUpdateCoordinator], AB self._attr_unique_id = self._get_unique_id() + self._async_call_update_attrs() + def _get_unique_id(self) -> str: """Return unique ID for the entity.""" return legacy_device_id(self._device) - async def async_added_to_hass(self) -> None: - """Handle being added to hass.""" - self._async_call_update_attrs() - return await super().async_added_to_hass() - @abstractmethod @callback def _async_update_attrs(self) -> None: @@ -196,11 +199,7 @@ class CoordinatedTPLinkEntity(CoordinatorEntity[TPLinkDataUpdateCoordinator], AB @callback def _async_call_update_attrs(self) -> None: - """Call update_attrs and make entity unavailable on error. - - update_attrs can sometimes fail if a device firmware update breaks the - downstream library. - """ + """Call update_attrs and make entity unavailable on errors.""" try: self._async_update_attrs() except Exception as ex: # noqa: BLE001 @@ -358,6 +357,7 @@ class CoordinatedTPLinkFeatureEntity(CoordinatedTPLinkEntity, ABC): and ( feat.category is not Feature.Category.Primary or device.device_type not in DEVICETYPES_WITH_SPECIALIZED_PLATFORMS + or feat.id in FEATURES_ALLOW_LIST ) and ( desc := cls._description_for_feature( diff --git a/homeassistant/components/tplink/fan.py b/homeassistant/components/tplink/fan.py index 947a9072329..292240bca94 100644 --- a/homeassistant/components/tplink/fan.py +++ b/homeassistant/components/tplink/fan.py @@ -69,11 +69,12 @@ class TPLinkFanEntity(CoordinatedTPLinkEntity, FanEntity): parent: Device | None = None, ) -> None: """Initialize the fan.""" - super().__init__(device, coordinator, parent=parent) self.fan_module = fan_module # If _attr_name is None the entity name will be the device name self._attr_name = None if parent is None else device.alias + super().__init__(device, coordinator, parent=parent) + @async_refresh_after async def async_turn_on( self, diff --git a/homeassistant/components/tplink/light.py b/homeassistant/components/tplink/light.py index 633648bbf23..a736a0ba1e1 100644 --- a/homeassistant/components/tplink/light.py +++ b/homeassistant/components/tplink/light.py @@ -140,9 +140,7 @@ async def async_setup_entry( parent_coordinator = data.parent_coordinator device = parent_coordinator.device entities: list[TPLinkLightEntity | TPLinkLightEffectEntity] = [] - if ( - effect_module := device.modules.get(Module.LightEffect) - ) and effect_module.has_custom_effects: + if effect_module := device.modules.get(Module.LightEffect): entities.append( TPLinkLightEffectEntity( device, @@ -151,17 +149,18 @@ async def async_setup_entry( effect_module=effect_module, ) ) - platform = entity_platform.async_get_current_platform() - platform.async_register_entity_service( - SERVICE_RANDOM_EFFECT, - RANDOM_EFFECT_DICT, - "async_set_random_effect", - ) - platform.async_register_entity_service( - SERVICE_SEQUENCE_EFFECT, - SEQUENCE_EFFECT_DICT, - "async_set_sequence_effect", - ) + if effect_module.has_custom_effects: + platform = entity_platform.async_get_current_platform() + platform.async_register_entity_service( + SERVICE_RANDOM_EFFECT, + RANDOM_EFFECT_DICT, + "async_set_random_effect", + ) + platform.async_register_entity_service( + SERVICE_SEQUENCE_EFFECT, + SEQUENCE_EFFECT_DICT, + "async_set_sequence_effect", + ) elif Module.Light in device.modules: entities.append( TPLinkLightEntity( @@ -197,7 +196,6 @@ class TPLinkLightEntity(CoordinatedTPLinkEntity, LightEntity): ) -> None: """Initialize the light.""" self._parent = parent - super().__init__(device, coordinator, parent=parent) self._light_module = light_module # If _attr_name is None the entity name will be the device name self._attr_name = None if parent is None else device.alias @@ -215,7 +213,8 @@ class TPLinkLightEntity(CoordinatedTPLinkEntity, LightEntity): if len(self._attr_supported_color_modes) == 1: # If the light supports only a single color mode, set it now self._fixed_color_mode = next(iter(self._attr_supported_color_modes)) - self._async_call_update_attrs() + + super().__init__(device, coordinator, parent=parent) def _get_unique_id(self) -> str: """Return unique ID for the entity.""" @@ -371,6 +370,7 @@ class TPLinkLightEffectEntity(TPLinkLightEntity): effect_module = self._effect_module if effect_module.effect != LightEffect.LIGHT_EFFECTS_OFF: self._attr_effect = effect_module.effect + self._attr_color_mode = ColorMode.BRIGHTNESS else: self._attr_effect = EFFECT_OFF if effect_list := effect_module.effect_list: diff --git a/homeassistant/components/tplink/sensor.py b/homeassistant/components/tplink/sensor.py index 474ee6bfacf..3da414d74d3 100644 --- a/homeassistant/components/tplink/sensor.py +++ b/homeassistant/components/tplink/sensor.py @@ -5,7 +5,7 @@ from __future__ import annotations from dataclasses import dataclass from typing import cast -from kasa import Device, Feature +from kasa import Feature from homeassistant.components.sensor import ( SensorDeviceClass, @@ -18,7 +18,6 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import TPLinkConfigEntry from .const import UNIT_MAPPING -from .coordinator import TPLinkDataUpdateCoordinator from .entity import CoordinatedTPLinkFeatureEntity, TPLinkFeatureEntityDescription @@ -144,21 +143,6 @@ class TPLinkSensorEntity(CoordinatedTPLinkFeatureEntity, SensorEntity): entity_description: TPLinkSensorEntityDescription - def __init__( - self, - device: Device, - coordinator: TPLinkDataUpdateCoordinator, - *, - feature: Feature, - description: TPLinkSensorEntityDescription, - parent: Device | None = None, - ) -> None: - """Initialize the sensor.""" - super().__init__( - device, coordinator, description=description, feature=feature, parent=parent - ) - self._async_call_update_attrs() - @callback def _async_update_attrs(self) -> None: """Update the entity's attributes.""" diff --git a/homeassistant/components/tplink/switch.py b/homeassistant/components/tplink/switch.py index 2520de9dd3e..62957d48ac4 100644 --- a/homeassistant/components/tplink/switch.py +++ b/homeassistant/components/tplink/switch.py @@ -6,14 +6,13 @@ from dataclasses import dataclass import logging from typing import Any -from kasa import Device, Feature +from kasa import Feature from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import TPLinkConfigEntry -from .coordinator import TPLinkDataUpdateCoordinator from .entity import ( CoordinatedTPLinkFeatureEntity, TPLinkFeatureEntityDescription, @@ -80,22 +79,6 @@ class TPLinkSwitch(CoordinatedTPLinkFeatureEntity, SwitchEntity): entity_description: TPLinkSwitchEntityDescription - def __init__( - self, - device: Device, - coordinator: TPLinkDataUpdateCoordinator, - *, - feature: Feature, - description: TPLinkSwitchEntityDescription, - parent: Device | None = None, - ) -> None: - """Initialize the switch.""" - super().__init__( - device, coordinator, description=description, feature=feature, parent=parent - ) - - self._async_call_update_attrs() - @async_refresh_after async def async_turn_on(self, **kwargs: Any) -> None: """Turn the switch on.""" diff --git a/tests/components/tplink/fixtures/features.json b/tests/components/tplink/fixtures/features.json index daf86a74643..7cfe979ea25 100644 --- a/tests/components/tplink/fixtures/features.json +++ b/tests/components/tplink/fixtures/features.json @@ -73,7 +73,7 @@ "value": 121.1, "type": "Sensor", "category": "Primary", - "unit": "v", + "unit": "V", "precision_hint": 1 }, "device_id": { diff --git a/tests/components/tplink/snapshots/test_sensor.ambr b/tests/components/tplink/snapshots/test_sensor.ambr index 46fe897500f..9ea22af45fd 100644 --- a/tests/components/tplink/snapshots/test_sensor.ambr +++ b/tests/components/tplink/snapshots/test_sensor.ambr @@ -770,7 +770,7 @@ 'supported_features': 0, 'translation_key': 'voltage', 'unique_id': '123456789ABCDEFGH_voltage', - 'unit_of_measurement': 'v', + 'unit_of_measurement': 'V', }) # --- # name: test_states[sensor.my_device_voltage-state] @@ -779,7 +779,7 @@ 'device_class': 'voltage', 'friendly_name': 'my_device Voltage', 'state_class': , - 'unit_of_measurement': 'v', + 'unit_of_measurement': 'V', }), 'context': , 'entity_id': 'sensor.my_device_voltage', diff --git a/tests/components/tplink/test_light.py b/tests/components/tplink/test_light.py index c2f40f47e3d..6fce04ec454 100644 --- a/tests/components/tplink/test_light.py +++ b/tests/components/tplink/test_light.py @@ -140,13 +140,17 @@ async def test_color_light( assert state.state == "on" attributes = state.attributes assert attributes[ATTR_BRIGHTNESS] == 128 - assert attributes[ATTR_COLOR_MODE] == "hs" assert attributes[ATTR_SUPPORTED_COLOR_MODES] == ["color_temp", "hs"] - assert attributes[ATTR_MIN_MIREDS] == 111 - assert attributes[ATTR_MAX_MIREDS] == 250 - assert attributes[ATTR_HS_COLOR] == (10, 30) - assert attributes[ATTR_RGB_COLOR] == (255, 191, 178) - assert attributes[ATTR_XY_COLOR] == (0.42, 0.336) + # If effect is active, only the brightness can be controlled + if attributes.get(ATTR_EFFECT) is not None: + assert attributes[ATTR_COLOR_MODE] == "brightness" + else: + assert attributes[ATTR_COLOR_MODE] == "hs" + assert attributes[ATTR_MIN_MIREDS] == 111 + assert attributes[ATTR_MAX_MIREDS] == 250 + assert attributes[ATTR_HS_COLOR] == (10, 30) + assert attributes[ATTR_RGB_COLOR] == (255, 191, 178) + assert attributes[ATTR_XY_COLOR] == (0.42, 0.336) await hass.services.async_call( LIGHT_DOMAIN, "turn_off", BASE_PAYLOAD, blocking=True From 3b6acd538042a3b2d4b7c164ae0f992c944c2fb5 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 2 Jul 2024 23:51:14 +1200 Subject: [PATCH 1417/1445] [ESPHome] Disable dashboard based update entities by default (#120907) Co-authored-by: Paulus Schoutsen --- homeassistant/components/esphome/update.py | 1 + tests/components/esphome/test_update.py | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/homeassistant/components/esphome/update.py b/homeassistant/components/esphome/update.py index cb3d36dab9d..e86c88ddf5b 100644 --- a/homeassistant/components/esphome/update.py +++ b/homeassistant/components/esphome/update.py @@ -97,6 +97,7 @@ class ESPHomeDashboardUpdateEntity( _attr_title = "ESPHome" _attr_name = "Firmware" _attr_release_url = "https://esphome.io/changelog/" + _attr_entity_registry_enabled_default = False def __init__( self, entry_data: RuntimeEntryData, coordinator: ESPHomeDashboardCoordinator diff --git a/tests/components/esphome/test_update.py b/tests/components/esphome/test_update.py index fc845299142..cca1dd1851f 100644 --- a/tests/components/esphome/test_update.py +++ b/tests/components/esphome/test_update.py @@ -33,6 +33,11 @@ from homeassistant.exceptions import HomeAssistantError from .conftest import MockESPHomeDevice +@pytest.fixture(autouse=True) +def enable_entity(entity_registry_enabled_by_default: None) -> None: + """Enable update entity.""" + + @pytest.fixture def stub_reconnect(): """Stub reconnect.""" From efd3252849aa3e4a9d2994bb623f6952ba834c49 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 2 Jul 2024 15:48:35 +0200 Subject: [PATCH 1418/1445] Create log files in an executor thread (#120912) --- homeassistant/bootstrap.py | 59 +++++++++++++++++++++----------------- tests/test_bootstrap.py | 28 +++++++++--------- 2 files changed, 47 insertions(+), 40 deletions(-) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 8435fe73d40..c5229634053 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -8,7 +8,7 @@ import contextlib from functools import partial from itertools import chain import logging -import logging.handlers +from logging.handlers import RotatingFileHandler, TimedRotatingFileHandler import mimetypes from operator import contains, itemgetter import os @@ -257,12 +257,12 @@ async def async_setup_hass( ) -> core.HomeAssistant | None: """Set up Home Assistant.""" - def create_hass() -> core.HomeAssistant: + async def create_hass() -> core.HomeAssistant: """Create the hass object and do basic setup.""" hass = core.HomeAssistant(runtime_config.config_dir) loader.async_setup(hass) - async_enable_logging( + await async_enable_logging( hass, runtime_config.verbose, runtime_config.log_rotate_days, @@ -287,7 +287,7 @@ async def async_setup_hass( async with hass.timeout.async_timeout(10): await hass.async_stop() - hass = create_hass() + hass = await create_hass() if runtime_config.skip_pip or runtime_config.skip_pip_packages: _LOGGER.warning( @@ -326,13 +326,13 @@ async def async_setup_hass( if config_dict is None: recovery_mode = True await stop_hass(hass) - hass = create_hass() + hass = await create_hass() elif not basic_setup_success: _LOGGER.warning("Unable to set up core integrations. Activating recovery mode") recovery_mode = True await stop_hass(hass) - hass = create_hass() + hass = await create_hass() elif any(domain not in hass.config.components for domain in CRITICAL_INTEGRATIONS): _LOGGER.warning( @@ -345,7 +345,7 @@ async def async_setup_hass( recovery_mode = True await stop_hass(hass) - hass = create_hass() + hass = await create_hass() if old_logging: hass.data[DATA_LOGGING] = old_logging @@ -523,8 +523,7 @@ async def async_from_config_dict( return hass -@core.callback -def async_enable_logging( +async def async_enable_logging( hass: core.HomeAssistant, verbose: bool = False, log_rotate_days: int | None = None, @@ -607,23 +606,9 @@ def async_enable_logging( if (err_path_exists and os.access(err_log_path, os.W_OK)) or ( not err_path_exists and os.access(err_dir, os.W_OK) ): - err_handler: ( - logging.handlers.RotatingFileHandler - | logging.handlers.TimedRotatingFileHandler + err_handler = await hass.async_add_executor_job( + _create_log_file, err_log_path, log_rotate_days ) - if log_rotate_days: - err_handler = logging.handlers.TimedRotatingFileHandler( - err_log_path, when="midnight", backupCount=log_rotate_days - ) - else: - err_handler = _RotatingFileHandlerWithoutShouldRollOver( - err_log_path, backupCount=1 - ) - - try: - err_handler.doRollover() - except OSError as err: - _LOGGER.error("Error rolling over log file: %s", err) err_handler.setLevel(logging.INFO if verbose else logging.WARNING) err_handler.setFormatter(logging.Formatter(fmt, datefmt=FORMAT_DATETIME)) @@ -640,7 +625,29 @@ def async_enable_logging( async_activate_log_queue_handler(hass) -class _RotatingFileHandlerWithoutShouldRollOver(logging.handlers.RotatingFileHandler): +def _create_log_file( + err_log_path: str, log_rotate_days: int | None +) -> RotatingFileHandler | TimedRotatingFileHandler: + """Create log file and do roll over.""" + err_handler: RotatingFileHandler | TimedRotatingFileHandler + if log_rotate_days: + err_handler = TimedRotatingFileHandler( + err_log_path, when="midnight", backupCount=log_rotate_days + ) + else: + err_handler = _RotatingFileHandlerWithoutShouldRollOver( + err_log_path, backupCount=1 + ) + + try: + err_handler.doRollover() + except OSError as err: + _LOGGER.error("Error rolling over log file: %s", err) + + return err_handler + + +class _RotatingFileHandlerWithoutShouldRollOver(RotatingFileHandler): """RotatingFileHandler that does not check if it should roll over on every log.""" def shouldRollover(self, record: logging.LogRecord) -> bool: diff --git a/tests/test_bootstrap.py b/tests/test_bootstrap.py index ca864006852..56599a15d34 100644 --- a/tests/test_bootstrap.py +++ b/tests/test_bootstrap.py @@ -70,7 +70,7 @@ def mock_http_start_stop() -> Generator[None]: yield -@patch("homeassistant.bootstrap.async_enable_logging", Mock()) +@patch("homeassistant.bootstrap.async_enable_logging", AsyncMock()) async def test_home_assistant_core_config_validation(hass: HomeAssistant) -> None: """Test if we pass in wrong information for HA conf.""" # Extensive HA conf validation testing is done @@ -94,10 +94,10 @@ async def test_async_enable_logging( side_effect=OSError, ), ): - bootstrap.async_enable_logging(hass) + await bootstrap.async_enable_logging(hass) mock_async_activate_log_queue_handler.assert_called_once() mock_async_activate_log_queue_handler.reset_mock() - bootstrap.async_enable_logging( + await bootstrap.async_enable_logging( hass, log_rotate_days=5, log_file="test.log", @@ -141,7 +141,7 @@ async def test_config_does_not_turn_off_debug(hass: HomeAssistant) -> None: @pytest.mark.parametrize("hass_config", [{"frontend": {}}]) @pytest.mark.usefixtures("mock_hass_config") async def test_asyncio_debug_on_turns_hass_debug_on( - mock_enable_logging: Mock, + mock_enable_logging: AsyncMock, mock_is_virtual_env: Mock, mock_mount_local_lib_path: AsyncMock, mock_ensure_config_exists: AsyncMock, @@ -598,7 +598,7 @@ def mock_is_virtual_env() -> Generator[Mock]: @pytest.fixture -def mock_enable_logging() -> Generator[Mock]: +def mock_enable_logging() -> Generator[AsyncMock]: """Mock enable logging.""" with patch("homeassistant.bootstrap.async_enable_logging") as enable_logging: yield enable_logging @@ -634,7 +634,7 @@ def mock_ensure_config_exists() -> Generator[AsyncMock]: @pytest.mark.parametrize("hass_config", [{"browser": {}, "frontend": {}}]) @pytest.mark.usefixtures("mock_hass_config") async def test_setup_hass( - mock_enable_logging: Mock, + mock_enable_logging: AsyncMock, mock_is_virtual_env: Mock, mock_mount_local_lib_path: AsyncMock, mock_ensure_config_exists: AsyncMock, @@ -687,7 +687,7 @@ async def test_setup_hass( @pytest.mark.parametrize("hass_config", [{"browser": {}, "frontend": {}}]) @pytest.mark.usefixtures("mock_hass_config") async def test_setup_hass_takes_longer_than_log_slow_startup( - mock_enable_logging: Mock, + mock_enable_logging: AsyncMock, mock_is_virtual_env: Mock, mock_mount_local_lib_path: AsyncMock, mock_ensure_config_exists: AsyncMock, @@ -728,7 +728,7 @@ async def test_setup_hass_takes_longer_than_log_slow_startup( async def test_setup_hass_invalid_yaml( - mock_enable_logging: Mock, + mock_enable_logging: AsyncMock, mock_is_virtual_env: Mock, mock_mount_local_lib_path: AsyncMock, mock_ensure_config_exists: AsyncMock, @@ -755,7 +755,7 @@ async def test_setup_hass_invalid_yaml( async def test_setup_hass_config_dir_nonexistent( - mock_enable_logging: Mock, + mock_enable_logging: AsyncMock, mock_is_virtual_env: Mock, mock_mount_local_lib_path: AsyncMock, mock_ensure_config_exists: AsyncMock, @@ -781,7 +781,7 @@ async def test_setup_hass_config_dir_nonexistent( async def test_setup_hass_recovery_mode( - mock_enable_logging: Mock, + mock_enable_logging: AsyncMock, mock_is_virtual_env: Mock, mock_mount_local_lib_path: AsyncMock, mock_ensure_config_exists: AsyncMock, @@ -817,7 +817,7 @@ async def test_setup_hass_recovery_mode( @pytest.mark.usefixtures("mock_hass_config") async def test_setup_hass_safe_mode( - mock_enable_logging: Mock, + mock_enable_logging: AsyncMock, mock_is_virtual_env: Mock, mock_mount_local_lib_path: AsyncMock, mock_ensure_config_exists: AsyncMock, @@ -852,7 +852,7 @@ async def test_setup_hass_safe_mode( @pytest.mark.usefixtures("mock_hass_config") async def test_setup_hass_recovery_mode_and_safe_mode( - mock_enable_logging: Mock, + mock_enable_logging: AsyncMock, mock_is_virtual_env: Mock, mock_mount_local_lib_path: AsyncMock, mock_ensure_config_exists: AsyncMock, @@ -888,7 +888,7 @@ async def test_setup_hass_recovery_mode_and_safe_mode( @pytest.mark.parametrize("hass_config", [{"homeassistant": {"non-existing": 1}}]) @pytest.mark.usefixtures("mock_hass_config") async def test_setup_hass_invalid_core_config( - mock_enable_logging: Mock, + mock_enable_logging: AsyncMock, mock_is_virtual_env: Mock, mock_mount_local_lib_path: AsyncMock, mock_ensure_config_exists: AsyncMock, @@ -927,7 +927,7 @@ async def test_setup_hass_invalid_core_config( ) @pytest.mark.usefixtures("mock_hass_config") async def test_setup_recovery_mode_if_no_frontend( - mock_enable_logging: Mock, + mock_enable_logging: AsyncMock, mock_is_virtual_env: Mock, mock_mount_local_lib_path: AsyncMock, mock_ensure_config_exists: AsyncMock, From de458493f895be7fce2a194eea19db3dbd0c1907 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 1 Jul 2024 20:36:35 +0200 Subject: [PATCH 1419/1445] Fix missing airgradient string (#120957) --- homeassistant/components/airgradient/strings.json | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/airgradient/strings.json b/homeassistant/components/airgradient/strings.json index 12049e7b720..6bf7242f2f1 100644 --- a/homeassistant/components/airgradient/strings.json +++ b/homeassistant/components/airgradient/strings.json @@ -16,6 +16,7 @@ }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "invalid_version": "This firmware version is unsupported. Please upgrade the firmware of the device to at least version 3.1.1." }, "error": { From 23b905b4226ec00d4432c3ce89cd9ce9b685b607 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 2 Jul 2024 08:23:31 +0200 Subject: [PATCH 1420/1445] Bump airgradient to 0.6.1 (#120962) --- homeassistant/components/airgradient/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/airgradient/manifest.json b/homeassistant/components/airgradient/manifest.json index 7b892c4658a..d523aa4ca03 100644 --- a/homeassistant/components/airgradient/manifest.json +++ b/homeassistant/components/airgradient/manifest.json @@ -6,6 +6,6 @@ "documentation": "https://www.home-assistant.io/integrations/airgradient", "integration_type": "device", "iot_class": "local_polling", - "requirements": ["airgradient==0.6.0"], + "requirements": ["airgradient==0.6.1"], "zeroconf": ["_airgradient._tcp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index c18ed2f439a..2683ff24549 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -413,7 +413,7 @@ aiowithings==3.0.2 aioymaps==1.2.2 # homeassistant.components.airgradient -airgradient==0.6.0 +airgradient==0.6.1 # homeassistant.components.airly airly==1.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6291a3dddca..72c0b47ad61 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -386,7 +386,7 @@ aiowithings==3.0.2 aioymaps==1.2.2 # homeassistant.components.airgradient -airgradient==0.6.0 +airgradient==0.6.1 # homeassistant.components.airly airly==1.1.0 From 65d2ca53cb25209dc207b314818b3aa6074d71bd Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Tue, 2 Jul 2024 09:25:33 +0200 Subject: [PATCH 1421/1445] Bump reolink-aio to 0.9.4 (#120964) --- homeassistant/components/reolink/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/reolink/manifest.json b/homeassistant/components/reolink/manifest.json index 172a43a91b3..ee3ebe8a13a 100644 --- a/homeassistant/components/reolink/manifest.json +++ b/homeassistant/components/reolink/manifest.json @@ -18,5 +18,5 @@ "documentation": "https://www.home-assistant.io/integrations/reolink", "iot_class": "local_push", "loggers": ["reolink_aio"], - "requirements": ["reolink-aio==0.9.3"] + "requirements": ["reolink-aio==0.9.4"] } diff --git a/requirements_all.txt b/requirements_all.txt index 2683ff24549..93d38bf3b73 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2460,7 +2460,7 @@ renault-api==0.2.4 renson-endura-delta==1.7.1 # homeassistant.components.reolink -reolink-aio==0.9.3 +reolink-aio==0.9.4 # homeassistant.components.idteck_prox rfk101py==0.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 72c0b47ad61..9a5c062d76e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1924,7 +1924,7 @@ renault-api==0.2.4 renson-endura-delta==1.7.1 # homeassistant.components.reolink -reolink-aio==0.9.3 +reolink-aio==0.9.4 # homeassistant.components.rflink rflink==0.0.66 From 24afbde79e3caad272ff46ac1a5cf4b9f656373f Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 2 Jul 2024 12:35:10 +0200 Subject: [PATCH 1422/1445] Bump yt-dlp to 2024.07.01 (#120978) --- homeassistant/components/media_extractor/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/media_extractor/manifest.json b/homeassistant/components/media_extractor/manifest.json index 7ed4e93bb56..cfe44f5176b 100644 --- a/homeassistant/components/media_extractor/manifest.json +++ b/homeassistant/components/media_extractor/manifest.json @@ -8,6 +8,6 @@ "iot_class": "calculated", "loggers": ["yt_dlp"], "quality_scale": "internal", - "requirements": ["yt-dlp==2024.05.27"], + "requirements": ["yt-dlp==2024.07.01"], "single_config_entry": true } diff --git a/requirements_all.txt b/requirements_all.txt index 93d38bf3b73..7ba781583f5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2951,7 +2951,7 @@ youless-api==2.1.0 youtubeaio==1.1.5 # homeassistant.components.media_extractor -yt-dlp==2024.05.27 +yt-dlp==2024.07.01 # homeassistant.components.zamg zamg==0.3.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9a5c062d76e..65f9b4b1770 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2307,7 +2307,7 @@ youless-api==2.1.0 youtubeaio==1.1.5 # homeassistant.components.media_extractor -yt-dlp==2024.05.27 +yt-dlp==2024.07.01 # homeassistant.components.zamg zamg==0.3.6 From 98a2e46d4ac10fb9874108c9a74f0cc2bdf11b50 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 2 Jul 2024 13:51:44 +0200 Subject: [PATCH 1423/1445] Remove Aladdin Connect integration (#120980) --- .coveragerc | 5 - CODEOWNERS | 2 - .../components/aladdin_connect/__init__.py | 108 ++------ .../components/aladdin_connect/api.py | 33 --- .../application_credentials.py | 14 -- .../components/aladdin_connect/config_flow.py | 71 +----- .../components/aladdin_connect/const.py | 6 - .../components/aladdin_connect/coordinator.py | 38 --- .../components/aladdin_connect/cover.py | 84 ------- .../components/aladdin_connect/entity.py | 27 -- .../components/aladdin_connect/manifest.json | 8 +- .../components/aladdin_connect/ruff.toml | 5 - .../components/aladdin_connect/sensor.py | 80 ------ .../components/aladdin_connect/strings.json | 29 +-- .../generated/application_credentials.py | 1 - homeassistant/generated/config_flows.py | 1 - homeassistant/generated/integrations.json | 6 - tests/components/aladdin_connect/conftest.py | 29 --- .../aladdin_connect/test_config_flow.py | 230 ------------------ tests/components/aladdin_connect/test_init.py | 50 ++++ 20 files changed, 89 insertions(+), 738 deletions(-) delete mode 100644 homeassistant/components/aladdin_connect/api.py delete mode 100644 homeassistant/components/aladdin_connect/application_credentials.py delete mode 100644 homeassistant/components/aladdin_connect/const.py delete mode 100644 homeassistant/components/aladdin_connect/coordinator.py delete mode 100644 homeassistant/components/aladdin_connect/cover.py delete mode 100644 homeassistant/components/aladdin_connect/entity.py delete mode 100644 homeassistant/components/aladdin_connect/ruff.toml delete mode 100644 homeassistant/components/aladdin_connect/sensor.py delete mode 100644 tests/components/aladdin_connect/conftest.py delete mode 100644 tests/components/aladdin_connect/test_config_flow.py create mode 100644 tests/components/aladdin_connect/test_init.py diff --git a/.coveragerc b/.coveragerc index 0784977ff55..99a48360b41 100644 --- a/.coveragerc +++ b/.coveragerc @@ -58,11 +58,6 @@ omit = homeassistant/components/airvisual/sensor.py homeassistant/components/airvisual_pro/__init__.py homeassistant/components/airvisual_pro/sensor.py - homeassistant/components/aladdin_connect/__init__.py - homeassistant/components/aladdin_connect/api.py - homeassistant/components/aladdin_connect/application_credentials.py - homeassistant/components/aladdin_connect/cover.py - homeassistant/components/aladdin_connect/sensor.py homeassistant/components/alarmdecoder/__init__.py homeassistant/components/alarmdecoder/alarm_control_panel.py homeassistant/components/alarmdecoder/binary_sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index 7834add43f6..765f1624c33 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -80,8 +80,6 @@ build.json @home-assistant/supervisor /tests/components/airzone/ @Noltari /homeassistant/components/airzone_cloud/ @Noltari /tests/components/airzone_cloud/ @Noltari -/homeassistant/components/aladdin_connect/ @swcloudgenie -/tests/components/aladdin_connect/ @swcloudgenie /homeassistant/components/alarm_control_panel/ @home-assistant/core /tests/components/alarm_control_panel/ @home-assistant/core /homeassistant/components/alert/ @home-assistant/core @frenck diff --git a/homeassistant/components/aladdin_connect/__init__.py b/homeassistant/components/aladdin_connect/__init__.py index ed284c0e6bb..6d3f1d642b5 100644 --- a/homeassistant/components/aladdin_connect/__init__.py +++ b/homeassistant/components/aladdin_connect/__init__.py @@ -1,94 +1,38 @@ """The Aladdin Connect Genie integration.""" -# mypy: ignore-errors from __future__ import annotations -# from genie_partner_sdk.client import AladdinConnectClient -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import Platform +from homeassistant.config_entries import ConfigEntry, ConfigEntryState from homeassistant.core import HomeAssistant -from homeassistant.helpers import device_registry as dr -from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.config_entry_oauth2_flow import ( - OAuth2Session, - async_get_config_entry_implementation, -) +from homeassistant.helpers import issue_registry as ir -from .api import AsyncConfigEntryAuth -from .const import DOMAIN -from .coordinator import AladdinConnectCoordinator - -PLATFORMS: list[Platform] = [Platform.COVER, Platform.SENSOR] - -type AladdinConnectConfigEntry = ConfigEntry[AladdinConnectCoordinator] +DOMAIN = "aladdin_connect" -async def async_setup_entry( - hass: HomeAssistant, entry: AladdinConnectConfigEntry -) -> bool: - """Set up Aladdin Connect Genie from a config entry.""" - implementation = await async_get_config_entry_implementation(hass, entry) - - session = OAuth2Session(hass, entry, implementation) - auth = AsyncConfigEntryAuth(async_get_clientsession(hass), session) - coordinator = AladdinConnectCoordinator(hass, AladdinConnectClient(auth)) - - await coordinator.async_setup() - await coordinator.async_config_entry_first_refresh() - - entry.runtime_data = coordinator - - await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) - - async_remove_stale_devices(hass, entry) - - return True - - -async def async_unload_entry( - hass: HomeAssistant, entry: AladdinConnectConfigEntry -) -> bool: - """Unload a config entry.""" - return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - - -async def async_migrate_entry( - hass: HomeAssistant, config_entry: AladdinConnectConfigEntry -) -> bool: - """Migrate old config.""" - if config_entry.version < 2: - config_entry.async_start_reauth(hass) - hass.config_entries.async_update_entry( - config_entry, - version=2, - minor_version=1, - ) - - return True - - -def async_remove_stale_devices( - hass: HomeAssistant, config_entry: AladdinConnectConfigEntry -) -> None: - """Remove stale devices from device registry.""" - device_registry = dr.async_get(hass) - device_entries = dr.async_entries_for_config_entry( - device_registry, config_entry.entry_id +async def async_setup_entry(hass: HomeAssistant, _: ConfigEntry) -> bool: + """Set up Aladdin Connect from a config entry.""" + ir.async_create_issue( + hass, + DOMAIN, + DOMAIN, + is_fixable=False, + severity=ir.IssueSeverity.ERROR, + translation_key="integration_removed", + translation_placeholders={ + "entries": "/config/integrations/integration/aladdin_connect", + }, ) - all_device_ids = {door.unique_id for door in config_entry.runtime_data.doors} - for device_entry in device_entries: - device_id: str | None = None + return True - for identifier in device_entry.identifiers: - if identifier[0] == DOMAIN: - device_id = identifier[1] - break - if device_id is None or device_id not in all_device_ids: - # If device_id is None an invalid device entry was found for this config entry. - # If the device_id is not in existing device ids it's a stale device entry. - # Remove config entry from this device entry in either case. - device_registry.async_update_device( - device_entry.id, remove_config_entry_id=config_entry.entry_id - ) +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + if all( + config_entry.state is ConfigEntryState.NOT_LOADED + for config_entry in hass.config_entries.async_entries(DOMAIN) + if config_entry.entry_id != entry.entry_id + ): + ir.async_delete_issue(hass, DOMAIN, DOMAIN) + + return True diff --git a/homeassistant/components/aladdin_connect/api.py b/homeassistant/components/aladdin_connect/api.py deleted file mode 100644 index 4377fc8fbcb..00000000000 --- a/homeassistant/components/aladdin_connect/api.py +++ /dev/null @@ -1,33 +0,0 @@ -"""API for Aladdin Connect Genie bound to Home Assistant OAuth.""" - -# mypy: ignore-errors -from typing import cast - -from aiohttp import ClientSession - -# from genie_partner_sdk.auth import Auth -from homeassistant.helpers.config_entry_oauth2_flow import OAuth2Session - -API_URL = "https://twdvzuefzh.execute-api.us-east-2.amazonaws.com/v1" -API_KEY = "k6QaiQmcTm2zfaNns5L1Z8duBtJmhDOW8JawlCC3" - - -class AsyncConfigEntryAuth(Auth): # type: ignore[misc] - """Provide Aladdin Connect Genie authentication tied to an OAuth2 based config entry.""" - - def __init__( - self, - websession: ClientSession, - oauth_session: OAuth2Session, - ) -> None: - """Initialize Aladdin Connect Genie auth.""" - super().__init__( - websession, API_URL, oauth_session.token["access_token"], API_KEY - ) - self._oauth_session = oauth_session - - async def async_get_access_token(self) -> str: - """Return a valid access token.""" - await self._oauth_session.async_ensure_token_valid() - - return cast(str, self._oauth_session.token["access_token"]) diff --git a/homeassistant/components/aladdin_connect/application_credentials.py b/homeassistant/components/aladdin_connect/application_credentials.py deleted file mode 100644 index e8e959f1fa3..00000000000 --- a/homeassistant/components/aladdin_connect/application_credentials.py +++ /dev/null @@ -1,14 +0,0 @@ -"""application_credentials platform the Aladdin Connect Genie integration.""" - -from homeassistant.components.application_credentials import AuthorizationServer -from homeassistant.core import HomeAssistant - -from .const import OAUTH2_AUTHORIZE, OAUTH2_TOKEN - - -async def async_get_authorization_server(hass: HomeAssistant) -> AuthorizationServer: - """Return authorization server.""" - return AuthorizationServer( - authorize_url=OAUTH2_AUTHORIZE, - token_url=OAUTH2_TOKEN, - ) diff --git a/homeassistant/components/aladdin_connect/config_flow.py b/homeassistant/components/aladdin_connect/config_flow.py index 507085fa27f..a508ff89c68 100644 --- a/homeassistant/components/aladdin_connect/config_flow.py +++ b/homeassistant/components/aladdin_connect/config_flow.py @@ -1,70 +1,11 @@ -"""Config flow for Aladdin Connect Genie.""" +"""Config flow for Aladdin Connect integration.""" -from collections.abc import Mapping -import logging -from typing import Any +from homeassistant.config_entries import ConfigFlow -import jwt - -from homeassistant.config_entries import ConfigEntry, ConfigFlowResult -from homeassistant.const import CONF_ACCESS_TOKEN, CONF_TOKEN -from homeassistant.helpers.config_entry_oauth2_flow import AbstractOAuth2FlowHandler - -from .const import DOMAIN +from . import DOMAIN -class AladdinConnectOAuth2FlowHandler(AbstractOAuth2FlowHandler, domain=DOMAIN): - """Config flow to handle Aladdin Connect Genie OAuth2 authentication.""" +class AladdinConnectConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a config flow for Aladdin Connect.""" - DOMAIN = DOMAIN - VERSION = 2 - MINOR_VERSION = 1 - - reauth_entry: ConfigEntry | None = None - - async def async_step_reauth( - self, user_input: Mapping[str, Any] - ) -> ConfigFlowResult: - """Perform reauth upon API auth error or upgrade from v1 to v2.""" - self.reauth_entry = self.hass.config_entries.async_get_entry( - self.context["entry_id"] - ) - return await self.async_step_reauth_confirm() - - async def async_step_reauth_confirm( - self, user_input: Mapping[str, Any] | None = None - ) -> ConfigFlowResult: - """Dialog that informs the user that reauth is required.""" - if user_input is None: - return self.async_show_form(step_id="reauth_confirm") - return await self.async_step_user() - - async def async_oauth_create_entry(self, data: dict[str, Any]) -> ConfigFlowResult: - """Create an oauth config entry or update existing entry for reauth.""" - token_payload = jwt.decode( - data[CONF_TOKEN][CONF_ACCESS_TOKEN], options={"verify_signature": False} - ) - if not self.reauth_entry: - await self.async_set_unique_id(token_payload["sub"]) - self._abort_if_unique_id_configured() - - return self.async_create_entry( - title=token_payload["username"], - data=data, - ) - - if self.reauth_entry.unique_id == token_payload["username"]: - return self.async_update_reload_and_abort( - self.reauth_entry, - data=data, - unique_id=token_payload["sub"], - ) - if self.reauth_entry.unique_id == token_payload["sub"]: - return self.async_update_reload_and_abort(self.reauth_entry, data=data) - - return self.async_abort(reason="wrong_account") - - @property - def logger(self) -> logging.Logger: - """Return logger.""" - return logging.getLogger(__name__) + VERSION = 1 diff --git a/homeassistant/components/aladdin_connect/const.py b/homeassistant/components/aladdin_connect/const.py deleted file mode 100644 index a87147c8f09..00000000000 --- a/homeassistant/components/aladdin_connect/const.py +++ /dev/null @@ -1,6 +0,0 @@ -"""Constants for the Aladdin Connect Genie integration.""" - -DOMAIN = "aladdin_connect" - -OAUTH2_AUTHORIZE = "https://app.aladdinconnect.net/login.html" -OAUTH2_TOKEN = "https://twdvzuefzh.execute-api.us-east-2.amazonaws.com/v1/oauth2/token" diff --git a/homeassistant/components/aladdin_connect/coordinator.py b/homeassistant/components/aladdin_connect/coordinator.py deleted file mode 100644 index 9af3e330409..00000000000 --- a/homeassistant/components/aladdin_connect/coordinator.py +++ /dev/null @@ -1,38 +0,0 @@ -"""Define an object to coordinate fetching Aladdin Connect data.""" - -# mypy: ignore-errors -from datetime import timedelta -import logging - -# from genie_partner_sdk.client import AladdinConnectClient -# from genie_partner_sdk.model import GarageDoor -from homeassistant.core import HomeAssistant -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator - -from .const import DOMAIN - -_LOGGER = logging.getLogger(__name__) - - -class AladdinConnectCoordinator(DataUpdateCoordinator[None]): - """Aladdin Connect Data Update Coordinator.""" - - def __init__(self, hass: HomeAssistant, acc: AladdinConnectClient) -> None: - """Initialize.""" - super().__init__( - hass, - logger=_LOGGER, - name=DOMAIN, - update_interval=timedelta(seconds=15), - ) - self.acc = acc - self.doors: list[GarageDoor] = [] - - async def async_setup(self) -> None: - """Fetch initial data.""" - self.doors = await self.acc.get_doors() - - async def _async_update_data(self) -> None: - """Fetch data from API endpoint.""" - for door in self.doors: - await self.acc.update_door(door.device_id, door.door_number) diff --git a/homeassistant/components/aladdin_connect/cover.py b/homeassistant/components/aladdin_connect/cover.py deleted file mode 100644 index 1be41e6b516..00000000000 --- a/homeassistant/components/aladdin_connect/cover.py +++ /dev/null @@ -1,84 +0,0 @@ -"""Cover Entity for Genie Garage Door.""" - -# mypy: ignore-errors -from typing import Any - -# from genie_partner_sdk.model import GarageDoor -from homeassistant.components.cover import ( - CoverDeviceClass, - CoverEntity, - CoverEntityFeature, -) -from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity_platform import AddEntitiesCallback - -from . import AladdinConnectConfigEntry, AladdinConnectCoordinator -from .entity import AladdinConnectEntity - - -async def async_setup_entry( - hass: HomeAssistant, - config_entry: AladdinConnectConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up the Aladdin Connect platform.""" - coordinator = config_entry.runtime_data - - async_add_entities(AladdinDevice(coordinator, door) for door in coordinator.doors) - - -class AladdinDevice(AladdinConnectEntity, CoverEntity): - """Representation of Aladdin Connect cover.""" - - _attr_device_class = CoverDeviceClass.GARAGE - _attr_supported_features = CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE - _attr_name = None - - def __init__( - self, coordinator: AladdinConnectCoordinator, device: GarageDoor - ) -> None: - """Initialize the Aladdin Connect cover.""" - super().__init__(coordinator, device) - self._attr_unique_id = device.unique_id - - async def async_open_cover(self, **kwargs: Any) -> None: - """Issue open command to cover.""" - await self.coordinator.acc.open_door( - self._device.device_id, self._device.door_number - ) - - async def async_close_cover(self, **kwargs: Any) -> None: - """Issue close command to cover.""" - await self.coordinator.acc.close_door( - self._device.device_id, self._device.door_number - ) - - @property - def is_closed(self) -> bool | None: - """Update is closed attribute.""" - value = self.coordinator.acc.get_door_status( - self._device.device_id, self._device.door_number - ) - if value is None: - return None - return bool(value == "closed") - - @property - def is_closing(self) -> bool | None: - """Update is closing attribute.""" - value = self.coordinator.acc.get_door_status( - self._device.device_id, self._device.door_number - ) - if value is None: - return None - return bool(value == "closing") - - @property - def is_opening(self) -> bool | None: - """Update is opening attribute.""" - value = self.coordinator.acc.get_door_status( - self._device.device_id, self._device.door_number - ) - if value is None: - return None - return bool(value == "opening") diff --git a/homeassistant/components/aladdin_connect/entity.py b/homeassistant/components/aladdin_connect/entity.py deleted file mode 100644 index 2615cbc636e..00000000000 --- a/homeassistant/components/aladdin_connect/entity.py +++ /dev/null @@ -1,27 +0,0 @@ -"""Defines a base Aladdin Connect entity.""" -# mypy: ignore-errors -# from genie_partner_sdk.model import GarageDoor - -from homeassistant.helpers.device_registry import DeviceInfo -from homeassistant.helpers.update_coordinator import CoordinatorEntity - -from .const import DOMAIN -from .coordinator import AladdinConnectCoordinator - - -class AladdinConnectEntity(CoordinatorEntity[AladdinConnectCoordinator]): - """Defines a base Aladdin Connect entity.""" - - _attr_has_entity_name = True - - def __init__( - self, coordinator: AladdinConnectCoordinator, device: GarageDoor - ) -> None: - """Initialize the entity.""" - super().__init__(coordinator) - self._device = device - self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, device.unique_id)}, - name=device.name, - manufacturer="Overhead Door", - ) diff --git a/homeassistant/components/aladdin_connect/manifest.json b/homeassistant/components/aladdin_connect/manifest.json index dce95492272..adf0d9c9b5b 100644 --- a/homeassistant/components/aladdin_connect/manifest.json +++ b/homeassistant/components/aladdin_connect/manifest.json @@ -1,11 +1,9 @@ { "domain": "aladdin_connect", "name": "Aladdin Connect", - "codeowners": ["@swcloudgenie"], - "config_flow": true, - "dependencies": ["application_credentials"], - "disabled": "This integration is disabled because it uses non-open source code to operate.", + "codeowners": [], "documentation": "https://www.home-assistant.io/integrations/aladdin_connect", + "integration_type": "system", "iot_class": "cloud_polling", - "requirements": ["genie-partner-sdk==1.0.2"] + "requirements": [] } diff --git a/homeassistant/components/aladdin_connect/ruff.toml b/homeassistant/components/aladdin_connect/ruff.toml deleted file mode 100644 index 38f6f586aef..00000000000 --- a/homeassistant/components/aladdin_connect/ruff.toml +++ /dev/null @@ -1,5 +0,0 @@ -extend = "../../../pyproject.toml" - -lint.extend-ignore = [ - "F821" -] diff --git a/homeassistant/components/aladdin_connect/sensor.py b/homeassistant/components/aladdin_connect/sensor.py deleted file mode 100644 index cd1fff12c97..00000000000 --- a/homeassistant/components/aladdin_connect/sensor.py +++ /dev/null @@ -1,80 +0,0 @@ -"""Support for Aladdin Connect Garage Door sensors.""" - -# mypy: ignore-errors -from __future__ import annotations - -from collections.abc import Callable -from dataclasses import dataclass - -# from genie_partner_sdk.client import AladdinConnectClient -# from genie_partner_sdk.model import GarageDoor -from homeassistant.components.sensor import ( - SensorDeviceClass, - SensorEntity, - SensorEntityDescription, - SensorStateClass, -) -from homeassistant.const import PERCENTAGE -from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity_platform import AddEntitiesCallback - -from . import AladdinConnectConfigEntry, AladdinConnectCoordinator -from .entity import AladdinConnectEntity - - -@dataclass(frozen=True, kw_only=True) -class AccSensorEntityDescription(SensorEntityDescription): - """Describes AladdinConnect sensor entity.""" - - value_fn: Callable[[AladdinConnectClient, str, int], float | None] - - -SENSORS: tuple[AccSensorEntityDescription, ...] = ( - AccSensorEntityDescription( - key="battery_level", - device_class=SensorDeviceClass.BATTERY, - entity_registry_enabled_default=False, - native_unit_of_measurement=PERCENTAGE, - state_class=SensorStateClass.MEASUREMENT, - value_fn=AladdinConnectClient.get_battery_status, - ), -) - - -async def async_setup_entry( - hass: HomeAssistant, - entry: AladdinConnectConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up Aladdin Connect sensor devices.""" - coordinator = entry.runtime_data - - async_add_entities( - AladdinConnectSensor(coordinator, door, description) - for description in SENSORS - for door in coordinator.doors - ) - - -class AladdinConnectSensor(AladdinConnectEntity, SensorEntity): - """A sensor implementation for Aladdin Connect devices.""" - - entity_description: AccSensorEntityDescription - - def __init__( - self, - coordinator: AladdinConnectCoordinator, - device: GarageDoor, - description: AccSensorEntityDescription, - ) -> None: - """Initialize a sensor for an Aladdin Connect device.""" - super().__init__(coordinator, device) - self.entity_description = description - self._attr_unique_id = f"{device.unique_id}-{description.key}" - - @property - def native_value(self) -> float | None: - """Return the state of the sensor.""" - return self.entity_description.value_fn( - self.coordinator.acc, self._device.device_id, self._device.door_number - ) diff --git a/homeassistant/components/aladdin_connect/strings.json b/homeassistant/components/aladdin_connect/strings.json index 48f9b299a1d..f62e68de64e 100644 --- a/homeassistant/components/aladdin_connect/strings.json +++ b/homeassistant/components/aladdin_connect/strings.json @@ -1,29 +1,8 @@ { - "config": { - "step": { - "pick_implementation": { - "title": "[%key:common::config_flow::title::oauth2_pick_implementation%]" - }, - "reauth_confirm": { - "title": "[%key:common::config_flow::title::reauth%]", - "description": "Aladdin Connect needs to re-authenticate your account" - } - }, - "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", - "oauth_error": "[%key:common::config_flow::abort::oauth2_error%]", - "oauth_failed": "[%key:common::config_flow::abort::oauth2_failed%]", - "oauth_timeout": "[%key:common::config_flow::abort::oauth2_timeout%]", - "oauth_unauthorized": "[%key:common::config_flow::abort::oauth2_unauthorized%]", - "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", - "authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]", - "no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]", - "user_rejected_authorize": "[%key:common::config_flow::abort::oauth2_user_rejected_authorize%]", - "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" - }, - "create_entry": { - "default": "[%key:common::config_flow::create_entry::authenticated%]" + "issues": { + "integration_removed": { + "title": "The Aladdin Connect integration has been removed", + "description": "The Aladdin Connect integration has been removed from Home Assistant.\n\nTo resolve this issue, please remove the (now defunct) integration entries from your Home Assistant setup. [Click here to see your existing Aladdin Connect integration entries]({entries})." } } } diff --git a/homeassistant/generated/application_credentials.py b/homeassistant/generated/application_credentials.py index bc6b29e4c23..c576f242e30 100644 --- a/homeassistant/generated/application_credentials.py +++ b/homeassistant/generated/application_credentials.py @@ -4,7 +4,6 @@ To update, run python3 -m script.hassfest """ APPLICATION_CREDENTIALS = [ - "aladdin_connect", "electric_kiwi", "fitbit", "geocaching", diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 23a13bcbfd8..463a38feb9f 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -42,7 +42,6 @@ FLOWS = { "airvisual_pro", "airzone", "airzone_cloud", - "aladdin_connect", "alarmdecoder", "amberelectric", "ambient_network", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 3371c8de0fa..0ad8ac09c9e 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -180,12 +180,6 @@ } } }, - "aladdin_connect": { - "name": "Aladdin Connect", - "integration_type": "hub", - "config_flow": true, - "iot_class": "cloud_polling" - }, "alarmdecoder": { "name": "AlarmDecoder", "integration_type": "device", diff --git a/tests/components/aladdin_connect/conftest.py b/tests/components/aladdin_connect/conftest.py deleted file mode 100644 index 2c158998f49..00000000000 --- a/tests/components/aladdin_connect/conftest.py +++ /dev/null @@ -1,29 +0,0 @@ -"""Test fixtures for the Aladdin Connect Garage Door integration.""" - -from unittest.mock import AsyncMock, patch - -import pytest -from typing_extensions import Generator - -from tests.common import MockConfigEntry - - -@pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock]: - """Override async_setup_entry.""" - with patch( - "homeassistant.components.aladdin_connect.async_setup_entry", return_value=True - ) as mock_setup_entry: - yield mock_setup_entry - - -@pytest.fixture -def mock_config_entry() -> MockConfigEntry: - """Return an Aladdin Connect config entry.""" - return MockConfigEntry( - domain="aladdin_connect", - data={}, - title="test@test.com", - unique_id="aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", - version=2, - ) diff --git a/tests/components/aladdin_connect/test_config_flow.py b/tests/components/aladdin_connect/test_config_flow.py deleted file mode 100644 index 7154c53b9f6..00000000000 --- a/tests/components/aladdin_connect/test_config_flow.py +++ /dev/null @@ -1,230 +0,0 @@ -"""Test the Aladdin Connect Garage Door config flow.""" - -# from unittest.mock import AsyncMock -# -# import pytest -# -# from homeassistant.components.aladdin_connect.const import ( -# DOMAIN, -# OAUTH2_AUTHORIZE, -# OAUTH2_TOKEN, -# ) -# from homeassistant.components.application_credentials import ( -# ClientCredential, -# async_import_client_credential, -# ) -# from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER, ConfigFlowResult -# from homeassistant.core import HomeAssistant -# from homeassistant.data_entry_flow import FlowResultType -# from homeassistant.helpers import config_entry_oauth2_flow -# from homeassistant.setup import async_setup_component -# -# from tests.common import MockConfigEntry -# from tests.test_util.aiohttp import AiohttpClientMocker -# from tests.typing import ClientSessionGenerator -# -# CLIENT_ID = "1234" -# CLIENT_SECRET = "5678" -# -# EXAMPLE_TOKEN = ( -# "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhYWFhYWFhYS1iYmJiLWNjY2MtZGRk" -# "ZC1lZWVlZWVlZWVlZWUiLCJuYW1lIjoiSm9obiBEb2UiLCJpYXQiOjE1MTYyMzkwMjIsInVzZXJuYW" -# "1lIjoidGVzdEB0ZXN0LmNvbSJ9.CTU1YItIrUl8nSM3koJxlFJr5CjLghgc9gS6h45D8dE" -# ) -# -# -# @pytest.fixture -# async def setup_credentials(hass: HomeAssistant) -> None: -# """Fixture to setup credentials.""" -# assert await async_setup_component(hass, "application_credentials", {}) -# await async_import_client_credential( -# hass, -# DOMAIN, -# ClientCredential(CLIENT_ID, CLIENT_SECRET), -# ) -# -# -# async def _oauth_actions( -# hass: HomeAssistant, -# result: ConfigFlowResult, -# hass_client_no_auth: ClientSessionGenerator, -# aioclient_mock: AiohttpClientMocker, -# ) -> None: -# state = config_entry_oauth2_flow._encode_jwt( -# hass, -# { -# "flow_id": result["flow_id"], -# "redirect_uri": "https://example.com/auth/external/callback", -# }, -# ) -# -# assert result["url"] == ( -# f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}" -# "&redirect_uri=https://example.com/auth/external/callback" -# f"&state={state}" -# ) -# -# client = await hass_client_no_auth() -# resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") -# assert resp.status == 200 -# assert resp.headers["content-type"] == "text/html; charset=utf-8" -# -# aioclient_mock.post( -# OAUTH2_TOKEN, -# json={ -# "refresh_token": "mock-refresh-token", -# "access_token": EXAMPLE_TOKEN, -# "type": "Bearer", -# "expires_in": 60, -# }, -# ) -# -# -# @pytest.mark.skip(reason="Integration disabled") -# @pytest.mark.usefixtures("current_request_with_host") -# async def test_full_flow( -# hass: HomeAssistant, -# hass_client_no_auth: ClientSessionGenerator, -# aioclient_mock: AiohttpClientMocker, -# setup_credentials: None, -# mock_setup_entry: AsyncMock, -# ) -> None: -# """Check full flow.""" -# result = await hass.config_entries.flow.async_init( -# DOMAIN, context={"source": SOURCE_USER} -# ) -# await _oauth_actions(hass, result, hass_client_no_auth, aioclient_mock) -# -# result = await hass.config_entries.flow.async_configure(result["flow_id"]) -# assert result["type"] is FlowResultType.CREATE_ENTRY -# assert result["title"] == "test@test.com" -# assert result["data"]["token"]["access_token"] == EXAMPLE_TOKEN -# assert result["data"]["token"]["refresh_token"] == "mock-refresh-token" -# assert result["result"].unique_id == "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" -# -# assert len(hass.config_entries.async_entries(DOMAIN)) == 1 -# assert len(mock_setup_entry.mock_calls) == 1 -# -# -# @pytest.mark.skip(reason="Integration disabled") -# @pytest.mark.usefixtures("current_request_with_host") -# async def test_duplicate_entry( -# hass: HomeAssistant, -# hass_client_no_auth: ClientSessionGenerator, -# aioclient_mock: AiohttpClientMocker, -# setup_credentials: None, -# mock_config_entry: MockConfigEntry, -# ) -> None: -# """Test we abort with duplicate entry.""" -# mock_config_entry.add_to_hass(hass) -# result = await hass.config_entries.flow.async_init( -# DOMAIN, context={"source": SOURCE_USER} -# ) -# await _oauth_actions(hass, result, hass_client_no_auth, aioclient_mock) -# -# result = await hass.config_entries.flow.async_configure(result["flow_id"]) -# assert result["type"] is FlowResultType.ABORT -# assert result["reason"] == "already_configured" -# -# -# @pytest.mark.skip(reason="Integration disabled") -# @pytest.mark.usefixtures("current_request_with_host") -# async def test_reauth( -# hass: HomeAssistant, -# hass_client_no_auth: ClientSessionGenerator, -# aioclient_mock: AiohttpClientMocker, -# setup_credentials: None, -# mock_config_entry: MockConfigEntry, -# mock_setup_entry: AsyncMock, -# ) -> None: -# """Test reauthentication.""" -# mock_config_entry.add_to_hass(hass) -# result = await hass.config_entries.flow.async_init( -# DOMAIN, -# context={ -# "source": SOURCE_REAUTH, -# "entry_id": mock_config_entry.entry_id, -# }, -# data=mock_config_entry.data, -# ) -# assert result["type"] is FlowResultType.FORM -# assert result["step_id"] == "reauth_confirm" -# result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) -# await _oauth_actions(hass, result, hass_client_no_auth, aioclient_mock) -# -# result = await hass.config_entries.flow.async_configure(result["flow_id"]) -# assert result["type"] is FlowResultType.ABORT -# assert result["reason"] == "reauth_successful" -# -# -# @pytest.mark.skip(reason="Integration disabled") -# @pytest.mark.usefixtures("current_request_with_host") -# async def test_reauth_wrong_account( -# hass: HomeAssistant, -# hass_client_no_auth: ClientSessionGenerator, -# aioclient_mock: AiohttpClientMocker, -# setup_credentials: None, -# mock_setup_entry: AsyncMock, -# ) -> None: -# """Test reauthentication with wrong account.""" -# config_entry = MockConfigEntry( -# domain=DOMAIN, -# data={}, -# title="test@test.com", -# unique_id="aaaaaaaa-bbbb-ffff-dddd-eeeeeeeeeeee", -# version=2, -# ) -# config_entry.add_to_hass(hass) -# result = await hass.config_entries.flow.async_init( -# DOMAIN, -# context={ -# "source": SOURCE_REAUTH, -# "entry_id": config_entry.entry_id, -# }, -# data=config_entry.data, -# ) -# assert result["type"] is FlowResultType.FORM -# assert result["step_id"] == "reauth_confirm" -# result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) -# await _oauth_actions(hass, result, hass_client_no_auth, aioclient_mock) -# -# result = await hass.config_entries.flow.async_configure(result["flow_id"]) -# assert result["type"] is FlowResultType.ABORT -# assert result["reason"] == "wrong_account" -# -# -# @pytest.mark.skip(reason="Integration disabled") -# @pytest.mark.usefixtures("current_request_with_host") -# async def test_reauth_old_account( -# hass: HomeAssistant, -# hass_client_no_auth: ClientSessionGenerator, -# aioclient_mock: AiohttpClientMocker, -# setup_credentials: None, -# mock_setup_entry: AsyncMock, -# ) -> None: -# """Test reauthentication with old account.""" -# config_entry = MockConfigEntry( -# domain=DOMAIN, -# data={}, -# title="test@test.com", -# unique_id="test@test.com", -# version=2, -# ) -# config_entry.add_to_hass(hass) -# result = await hass.config_entries.flow.async_init( -# DOMAIN, -# context={ -# "source": SOURCE_REAUTH, -# "entry_id": config_entry.entry_id, -# }, -# data=config_entry.data, -# ) -# assert result["type"] is FlowResultType.FORM -# assert result["step_id"] == "reauth_confirm" -# result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) -# await _oauth_actions(hass, result, hass_client_no_auth, aioclient_mock) -# -# result = await hass.config_entries.flow.async_configure(result["flow_id"]) -# assert result["type"] is FlowResultType.ABORT -# assert result["reason"] == "reauth_successful" -# assert config_entry.unique_id == "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" diff --git a/tests/components/aladdin_connect/test_init.py b/tests/components/aladdin_connect/test_init.py new file mode 100644 index 00000000000..b01af287b7b --- /dev/null +++ b/tests/components/aladdin_connect/test_init.py @@ -0,0 +1,50 @@ +"""Tests for the Aladdin Connect integration.""" + +from homeassistant.components.aladdin_connect import DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant +from homeassistant.helpers import issue_registry as ir + +from tests.common import MockConfigEntry + + +async def test_aladdin_connect_repair_issue( + hass: HomeAssistant, issue_registry: ir.IssueRegistry +) -> None: + """Test the Aladdin Connect configuration entry loading/unloading handles the repair.""" + config_entry_1 = MockConfigEntry( + title="Example 1", + domain=DOMAIN, + ) + config_entry_1.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry_1.entry_id) + await hass.async_block_till_done() + assert config_entry_1.state is ConfigEntryState.LOADED + + # Add a second one + config_entry_2 = MockConfigEntry( + title="Example 2", + domain=DOMAIN, + ) + config_entry_2.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry_2.entry_id) + await hass.async_block_till_done() + + assert config_entry_2.state is ConfigEntryState.LOADED + assert issue_registry.async_get_issue(DOMAIN, DOMAIN) + + # Remove the first one + await hass.config_entries.async_remove(config_entry_1.entry_id) + await hass.async_block_till_done() + + assert config_entry_1.state is ConfigEntryState.NOT_LOADED + assert config_entry_2.state is ConfigEntryState.LOADED + assert issue_registry.async_get_issue(DOMAIN, DOMAIN) + + # Remove the second one + await hass.config_entries.async_remove(config_entry_2.entry_id) + await hass.async_block_till_done() + + assert config_entry_1.state is ConfigEntryState.NOT_LOADED + assert config_entry_2.state is ConfigEntryState.NOT_LOADED + assert issue_registry.async_get_issue(DOMAIN, DOMAIN) is None From 5cb41106b5628df354c17b81ec65429f6276c27c Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Tue, 2 Jul 2024 13:31:23 +0200 Subject: [PATCH 1424/1445] Reolink replace automatic removal of devices by manual removal (#120981) Co-authored-by: Robert Resch --- homeassistant/components/reolink/__init__.py | 87 ++++++++++---------- tests/components/reolink/test_init.py | 31 +++++-- 2 files changed, 70 insertions(+), 48 deletions(-) diff --git a/homeassistant/components/reolink/__init__.py b/homeassistant/components/reolink/__init__.py index 150a23dc64e..02d3cc16419 100644 --- a/homeassistant/components/reolink/__init__.py +++ b/homeassistant/components/reolink/__init__.py @@ -147,9 +147,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b firmware_coordinator=firmware_coordinator, ) - # first migrate and then cleanup, otherwise entities lost migrate_entity_ids(hass, config_entry.entry_id, host) - cleanup_disconnected_cams(hass, config_entry.entry_id, host) await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) @@ -179,6 +177,50 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> return unload_ok +async def async_remove_config_entry_device( + hass: HomeAssistant, config_entry: ConfigEntry, device: dr.DeviceEntry +) -> bool: + """Remove a device from a config entry.""" + host: ReolinkHost = hass.data[DOMAIN][config_entry.entry_id].host + (device_uid, ch) = get_device_uid_and_ch(device, host) + + if not host.api.is_nvr or ch is None: + _LOGGER.warning( + "Cannot remove Reolink device %s, because it is not a camera connected " + "to a NVR/Hub, please remove the integration entry instead", + device.name, + ) + return False # Do not remove the host/NVR itself + + if ch not in host.api.channels: + _LOGGER.debug( + "Removing Reolink device %s, " + "since no camera is connected to NVR channel %s anymore", + device.name, + ch, + ) + return True + + await host.api.get_state(cmd="GetChannelstatus") # update the camera_online status + if not host.api.camera_online(ch): + _LOGGER.debug( + "Removing Reolink device %s, " + "since the camera connected to channel %s is offline", + device.name, + ch, + ) + return True + + _LOGGER.warning( + "Cannot remove Reolink device %s on channel %s, because it is still connected " + "to the NVR/Hub, please first remove the camera from the NVR/Hub " + "in the reolink app", + device.name, + ch, + ) + return False + + def get_device_uid_and_ch( device: dr.DeviceEntry, host: ReolinkHost ) -> tuple[list[str], int | None]: @@ -197,47 +239,6 @@ def get_device_uid_and_ch( return (device_uid, ch) -def cleanup_disconnected_cams( - hass: HomeAssistant, config_entry_id: str, host: ReolinkHost -) -> None: - """Clean-up disconnected camera channels.""" - if not host.api.is_nvr: - return - - device_reg = dr.async_get(hass) - devices = dr.async_entries_for_config_entry(device_reg, config_entry_id) - for device in devices: - (device_uid, ch) = get_device_uid_and_ch(device, host) - if ch is None: - continue # Do not consider the NVR itself - - ch_model = host.api.camera_model(ch) - remove = False - if ch not in host.api.channels: - remove = True - _LOGGER.debug( - "Removing Reolink device %s, " - "since no camera is connected to NVR channel %s anymore", - device.name, - ch, - ) - if ch_model not in [device.model, "Unknown"]: - remove = True - _LOGGER.debug( - "Removing Reolink device %s, " - "since the camera model connected to channel %s changed from %s to %s", - device.name, - ch, - device.model, - ch_model, - ) - if not remove: - continue - - # clean device registry and associated entities - device_reg.async_remove_device(device.id) - - def migrate_entity_ids( hass: HomeAssistant, config_entry_id: str, host: ReolinkHost ) -> None: diff --git a/tests/components/reolink/test_init.py b/tests/components/reolink/test_init.py index a6c798f9415..f70fd312051 100644 --- a/tests/components/reolink/test_init.py +++ b/tests/components/reolink/test_init.py @@ -36,6 +36,7 @@ from .conftest import ( ) from tests.common import MockConfigEntry, async_fire_time_changed +from tests.typing import WebSocketGenerator pytestmark = pytest.mark.usefixtures("reolink_connect", "reolink_platforms") @@ -179,16 +180,27 @@ async def test_entry_reloading( None, [TEST_HOST_MODEL, TEST_CAM_MODEL], ), + ( + "is_nvr", + False, + [TEST_HOST_MODEL, TEST_CAM_MODEL], + ), ("channels", [], [TEST_HOST_MODEL]), ( - "camera_model", - Mock(return_value="RLC-567"), - [TEST_HOST_MODEL, "RLC-567"], + "camera_online", + Mock(return_value=False), + [TEST_HOST_MODEL], + ), + ( + "channel_for_uid", + Mock(return_value=-1), + [TEST_HOST_MODEL], ), ], ) -async def test_cleanup_disconnected_cams( +async def test_removing_disconnected_cams( hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, config_entry: MockConfigEntry, reolink_connect: MagicMock, device_registry: dr.DeviceRegistry, @@ -197,8 +209,10 @@ async def test_cleanup_disconnected_cams( value: Any, expected_models: list[str], ) -> None: - """Test device and entity registry are cleaned up when camera is disconnected from NVR.""" + """Test device and entity registry are cleaned up when camera is removed.""" reolink_connect.channels = [0] + assert await async_setup_component(hass, "config", {}) + client = await hass_ws_client(hass) # setup CH 0 and NVR switch entities/device with patch("homeassistant.components.reolink.PLATFORMS", [Platform.SWITCH]): assert await hass.config_entries.async_setup(config_entry.entry_id) @@ -215,6 +229,13 @@ async def test_cleanup_disconnected_cams( setattr(reolink_connect, attr, value) with patch("homeassistant.components.reolink.PLATFORMS", [Platform.SWITCH]): assert await hass.config_entries.async_reload(config_entry.entry_id) + await hass.async_block_till_done() + + expected_success = TEST_CAM_MODEL not in expected_models + for device in device_entries: + if device.model == TEST_CAM_MODEL: + response = await client.remove_device(device.id, config_entry.entry_id) + assert response["success"] == expected_success device_entries = dr.async_entries_for_config_entry( device_registry, config_entry.entry_id From 807ed0ce106c4ee448682561775b49bfd3ef7d35 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Tue, 2 Jul 2024 12:28:32 +0200 Subject: [PATCH 1425/1445] Do not hold core startup with reolink firmware check task (#120985) --- homeassistant/components/reolink/__init__.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/reolink/__init__.py b/homeassistant/components/reolink/__init__.py index 02d3cc16419..1caf4e79cd5 100644 --- a/homeassistant/components/reolink/__init__.py +++ b/homeassistant/components/reolink/__init__.py @@ -133,7 +133,11 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b ) # If camera WAN blocked, firmware check fails and takes long, do not prevent setup - config_entry.async_create_task(hass, firmware_coordinator.async_refresh()) + config_entry.async_create_background_task( + hass, + firmware_coordinator.async_refresh(), + f"Reolink firmware check {config_entry.entry_id}", + ) # Fetch initial data so we have data when entities subscribe try: await device_coordinator.async_config_entry_first_refresh() From b3e833f677bf027efbc825193b271d49c6285761 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Tue, 2 Jul 2024 21:03:01 +0200 Subject: [PATCH 1426/1445] Fix setting target temperature for single setpoint Matter thermostat (#121011) --- homeassistant/components/matter/climate.py | 28 ++++++++++++---------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/matter/climate.py b/homeassistant/components/matter/climate.py index c97124f4305..2c05fd3373e 100644 --- a/homeassistant/components/matter/climate.py +++ b/homeassistant/components/matter/climate.py @@ -267,19 +267,13 @@ class MatterClimate(MatterEntity, ClimateEntity): self._attr_hvac_action = HVACAction.FAN case _: self._attr_hvac_action = HVACAction.OFF - # update target_temperature - if self._attr_hvac_mode == HVACMode.HEAT_COOL: - self._attr_target_temperature = None - elif self._attr_hvac_mode == HVACMode.COOL: - self._attr_target_temperature = self._get_temperature_in_degrees( - clusters.Thermostat.Attributes.OccupiedCoolingSetpoint - ) - else: - self._attr_target_temperature = self._get_temperature_in_degrees( - clusters.Thermostat.Attributes.OccupiedHeatingSetpoint - ) # update target temperature high/low - if self._attr_hvac_mode == HVACMode.HEAT_COOL: + supports_range = ( + self._attr_supported_features + & ClimateEntityFeature.TARGET_TEMPERATURE_RANGE + ) + if supports_range and self._attr_hvac_mode == HVACMode.HEAT_COOL: + self._attr_target_temperature = None self._attr_target_temperature_high = self._get_temperature_in_degrees( clusters.Thermostat.Attributes.OccupiedCoolingSetpoint ) @@ -289,6 +283,16 @@ class MatterClimate(MatterEntity, ClimateEntity): else: self._attr_target_temperature_high = None self._attr_target_temperature_low = None + # update target_temperature + if self._attr_hvac_mode == HVACMode.COOL: + self._attr_target_temperature = self._get_temperature_in_degrees( + clusters.Thermostat.Attributes.OccupiedCoolingSetpoint + ) + else: + self._attr_target_temperature = self._get_temperature_in_degrees( + clusters.Thermostat.Attributes.OccupiedHeatingSetpoint + ) + # update min_temp if self._attr_hvac_mode == HVACMode.COOL: attribute = clusters.Thermostat.Attributes.AbsMinCoolSetpointLimit From 1fa6972a665052cc5f7c7dbb3d7fc5b32a8209fd Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Tue, 2 Jul 2024 21:02:29 +0200 Subject: [PATCH 1427/1445] Handle mains power for Matter appliances (#121023) --- homeassistant/components/matter/climate.py | 7 +++++++ homeassistant/components/matter/fan.py | 16 +++++++++++++++- tests/components/matter/test_climate.py | 9 +++++++-- tests/components/matter/test_fan.py | 6 ++++++ 4 files changed, 35 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/matter/climate.py b/homeassistant/components/matter/climate.py index 2c05fd3373e..192cb6b3bb4 100644 --- a/homeassistant/components/matter/climate.py +++ b/homeassistant/components/matter/climate.py @@ -227,6 +227,13 @@ class MatterClimate(MatterEntity, ClimateEntity): self._attr_current_temperature = self._get_temperature_in_degrees( clusters.Thermostat.Attributes.LocalTemperature ) + if self.get_matter_attribute_value(clusters.OnOff.Attributes.OnOff) is False: + # special case: the appliance has a dedicated Power switch on the OnOff cluster + # if the mains power is off - treat it as if the HVAC mode is off + self._attr_hvac_mode = HVACMode.OFF + self._attr_hvac_action = None + return + # update hvac_mode from SystemMode system_mode_value = int( self.get_matter_attribute_value(clusters.Thermostat.Attributes.SystemMode) diff --git a/homeassistant/components/matter/fan.py b/homeassistant/components/matter/fan.py index 0ce42f14d39..86f03dc7a03 100644 --- a/homeassistant/components/matter/fan.py +++ b/homeassistant/components/matter/fan.py @@ -170,6 +170,14 @@ class MatterFan(MatterEntity, FanEntity): """Update from device.""" if not hasattr(self, "_attr_preset_modes"): self._calculate_features() + + if self.get_matter_attribute_value(clusters.OnOff.Attributes.OnOff) is False: + # special case: the appliance has a dedicated Power switch on the OnOff cluster + # if the mains power is off - treat it as if the fan mode is off + self._attr_preset_mode = None + self._attr_percentage = 0 + return + if self._attr_supported_features & FanEntityFeature.DIRECTION: direction_value = self.get_matter_attribute_value( clusters.FanControl.Attributes.AirflowDirection @@ -200,7 +208,13 @@ class MatterFan(MatterEntity, FanEntity): wind_setting = self.get_matter_attribute_value( clusters.FanControl.Attributes.WindSetting ) - if ( + fan_mode = self.get_matter_attribute_value( + clusters.FanControl.Attributes.FanMode + ) + if fan_mode == clusters.FanControl.Enums.FanModeEnum.kOff: + self._attr_preset_mode = None + self._attr_percentage = 0 + elif ( self._attr_preset_modes and PRESET_NATURAL_WIND in self._attr_preset_modes and wind_setting & WindBitmap.kNaturalWind diff --git a/tests/components/matter/test_climate.py b/tests/components/matter/test_climate.py index 6a4cf34a640..e0015e8b445 100644 --- a/tests/components/matter/test_climate.py +++ b/tests/components/matter/test_climate.py @@ -315,14 +315,19 @@ async def test_room_airconditioner( state = hass.states.get("climate.room_airconditioner_thermostat") assert state assert state.attributes["current_temperature"] == 20 - assert state.attributes["min_temp"] == 16 - assert state.attributes["max_temp"] == 32 + # room airconditioner has mains power on OnOff cluster with value set to False + assert state.state == HVACMode.OFF # test supported features correctly parsed # WITHOUT temperature_range support mask = ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.TURN_OFF assert state.attributes["supported_features"] & mask == mask + # set mains power to ON (OnOff cluster) + set_node_attribute(room_airconditioner, 1, 6, 0, True) + await trigger_subscription_callback(hass, matter_client) + state = hass.states.get("climate.room_airconditioner_thermostat") + # test supported HVAC modes include fan and dry modes assert state.attributes["hvac_modes"] == [ HVACMode.OFF, diff --git a/tests/components/matter/test_fan.py b/tests/components/matter/test_fan.py index 30bd7f4a009..7e964d672ca 100644 --- a/tests/components/matter/test_fan.py +++ b/tests/components/matter/test_fan.py @@ -92,6 +92,12 @@ async def test_fan_base( await trigger_subscription_callback(hass, matter_client) state = hass.states.get(entity_id) assert state.attributes["preset_mode"] == "sleep_wind" + # set mains power to OFF (OnOff cluster) + set_node_attribute(air_purifier, 1, 6, 0, False) + await trigger_subscription_callback(hass, matter_client) + state = hass.states.get(entity_id) + assert state.attributes["preset_mode"] is None + assert state.attributes["percentage"] == 0 async def test_fan_turn_on_with_percentage( From 6b045a7d7bd0e7fb8cc916c78de2a41ca4f0858a Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 2 Jul 2024 21:09:55 +0200 Subject: [PATCH 1428/1445] Bump version to 2024.7.0b8 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 5f020a02624..3828f2cfbf7 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -24,7 +24,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 7 -PATCH_VERSION: Final = "0b7" +PATCH_VERSION: Final = "0b8" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0) diff --git a/pyproject.toml b/pyproject.toml index 6320551a082..bd34e19c555 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.7.0b7" +version = "2024.7.0b8" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 4377f4cbea5782686e83950e56854a1d90fc7a4b Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Tue, 2 Jul 2024 22:13:19 +0200 Subject: [PATCH 1429/1445] Temporarily set apprise log level to debug in tests (#121029) Co-authored-by: Franck Nijhof --- tests/components/apprise/test_notify.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/components/apprise/test_notify.py b/tests/components/apprise/test_notify.py index 7d37d7a5d99..d73fa72d6c7 100644 --- a/tests/components/apprise/test_notify.py +++ b/tests/components/apprise/test_notify.py @@ -1,14 +1,27 @@ """The tests for the apprise notification platform.""" +import logging from pathlib import Path from unittest.mock import MagicMock, patch +import pytest + from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component BASE_COMPONENT = "notify" +@pytest.fixture(autouse=True) +def reset_log_level(): + """Set and reset log level after each test case.""" + logger = logging.getLogger("apprise") + orig_level = logger.level + logger.setLevel(logging.DEBUG) + yield + logger.setLevel(orig_level) + + async def test_apprise_config_load_fail01(hass: HomeAssistant) -> None: """Test apprise configuration failures 1.""" From d1e76d5c3cf374e5d4ace63e0a6734d36e4e0037 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 2 Jul 2024 22:13:07 +0200 Subject: [PATCH 1430/1445] Update frontend to 20240702.0 (#121032) --- 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 70f1f5f4f4f..0d32624cf57 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==20240628.0"] + "requirements": ["home-assistant-frontend==20240702.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 7cccd58d73f..3ffa9d92f63 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -32,7 +32,7 @@ habluetooth==3.1.3 hass-nabucasa==0.81.1 hassil==1.7.1 home-assistant-bluetooth==1.12.2 -home-assistant-frontend==20240628.0 +home-assistant-frontend==20240702.0 home-assistant-intents==2024.6.26 httpx==0.27.0 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 7ba781583f5..de7cc9fa13f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1090,7 +1090,7 @@ hole==0.8.0 holidays==0.51 # homeassistant.components.frontend -home-assistant-frontend==20240628.0 +home-assistant-frontend==20240702.0 # homeassistant.components.conversation home-assistant-intents==2024.6.26 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 65f9b4b1770..0a63b696617 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -895,7 +895,7 @@ hole==0.8.0 holidays==0.51 # homeassistant.components.frontend -home-assistant-frontend==20240628.0 +home-assistant-frontend==20240702.0 # homeassistant.components.conversation home-assistant-intents==2024.6.26 From 1b9f27fab753997148f4cd962dee89847970a31e Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 2 Jul 2024 22:15:17 +0200 Subject: [PATCH 1431/1445] Bump version to 2024.7.0b9 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 3828f2cfbf7..5d20a8507bf 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -24,7 +24,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 7 -PATCH_VERSION: Final = "0b8" +PATCH_VERSION: Final = "0b9" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0) diff --git a/pyproject.toml b/pyproject.toml index bd34e19c555..1ebd3acf1e0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.7.0b8" +version = "2024.7.0b9" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 1665cb40acc05cf4263aa473f2c7dd74c9e1bb51 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Mon, 1 Jul 2024 08:52:19 -0700 Subject: [PATCH 1432/1445] Bump gcal_sync to 6.1.4 (#120941) --- homeassistant/components/google/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/google/manifest.json b/homeassistant/components/google/manifest.json index d40daa89b0e..163ad91fb7c 100644 --- a/homeassistant/components/google/manifest.json +++ b/homeassistant/components/google/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/calendar.google", "iot_class": "cloud_polling", "loggers": ["googleapiclient"], - "requirements": ["gcal-sync==6.1.3", "oauth2client==4.1.3", "ical==8.1.1"] + "requirements": ["gcal-sync==6.1.4", "oauth2client==4.1.3", "ical==8.1.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index de7cc9fa13f..cfc4f6f72bb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -924,7 +924,7 @@ gardena-bluetooth==1.4.2 gassist-text==0.0.11 # homeassistant.components.google -gcal-sync==6.1.3 +gcal-sync==6.1.4 # homeassistant.components.geniushub geniushub-client==0.7.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0a63b696617..775d3533c4c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -762,7 +762,7 @@ gardena-bluetooth==1.4.2 gassist-text==0.0.11 # homeassistant.components.google -gcal-sync==6.1.3 +gcal-sync==6.1.4 # homeassistant.components.geocaching geocachingapi==0.2.1 From febd1a377203e55544c38d48906a8b4fb163a0a0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 2 Jul 2024 19:12:17 -0700 Subject: [PATCH 1433/1445] Bump inkbird-ble to 0.5.7 (#121039) changelog: https://github.com/Bluetooth-Devices/inkbird-ble/compare/v0.5.6...v0.5.7 --- homeassistant/components/inkbird/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/inkbird/manifest.json b/homeassistant/components/inkbird/manifest.json index fcd95eadf9c..fb74d1c565a 100644 --- a/homeassistant/components/inkbird/manifest.json +++ b/homeassistant/components/inkbird/manifest.json @@ -28,5 +28,5 @@ "dependencies": ["bluetooth_adapters"], "documentation": "https://www.home-assistant.io/integrations/inkbird", "iot_class": "local_push", - "requirements": ["inkbird-ble==0.5.6"] + "requirements": ["inkbird-ble==0.5.7"] } diff --git a/requirements_all.txt b/requirements_all.txt index cfc4f6f72bb..480fbe162d6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1161,7 +1161,7 @@ influxdb-client==1.24.0 influxdb==5.3.1 # homeassistant.components.inkbird -inkbird-ble==0.5.6 +inkbird-ble==0.5.7 # homeassistant.components.insteon insteon-frontend-home-assistant==0.5.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 775d3533c4c..a2cffe6a526 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -951,7 +951,7 @@ influxdb-client==1.24.0 influxdb==5.3.1 # homeassistant.components.inkbird -inkbird-ble==0.5.6 +inkbird-ble==0.5.7 # homeassistant.components.insteon insteon-frontend-home-assistant==0.5.0 From 84204c38be7c0dd0b49a5f7216e8454b69ea7408 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 3 Jul 2024 08:59:52 +0200 Subject: [PATCH 1434/1445] Bump version to 2024.7.0b10 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 5d20a8507bf..b6800b44063 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -24,7 +24,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 7 -PATCH_VERSION: Final = "0b9" +PATCH_VERSION: Final = "0b10" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0) diff --git a/pyproject.toml b/pyproject.toml index 1ebd3acf1e0..36ca9abe1b5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.7.0b9" +version = "2024.7.0b10" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From c4956b66b0da95811cdd2b493ea836c74a9f55a3 Mon Sep 17 00:00:00 2001 From: Kevin Stillhammer Date: Mon, 1 Jul 2024 00:23:42 +0200 Subject: [PATCH 1435/1445] Bump here-routing to 1.0.1 (#120877) --- homeassistant/components/here_travel_time/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/here_travel_time/manifest.json b/homeassistant/components/here_travel_time/manifest.json index 19c5c4d73d9..2d6621c7c61 100644 --- a/homeassistant/components/here_travel_time/manifest.json +++ b/homeassistant/components/here_travel_time/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/here_travel_time", "iot_class": "cloud_polling", "loggers": ["here_routing", "here_transit", "homeassistant.helpers.location"], - "requirements": ["here-routing==0.2.0", "here-transit==1.2.0"] + "requirements": ["here-routing==1.0.1", "here-transit==1.2.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 480fbe162d6..a04485044d5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1065,7 +1065,7 @@ hdate==0.10.9 heatmiserV3==1.1.18 # homeassistant.components.here_travel_time -here-routing==0.2.0 +here-routing==1.0.1 # homeassistant.components.here_travel_time here-transit==1.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a2cffe6a526..2a93dd5d739 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -876,7 +876,7 @@ hassil==1.7.1 hdate==0.10.9 # homeassistant.components.here_travel_time -here-routing==0.2.0 +here-routing==1.0.1 # homeassistant.components.here_travel_time here-transit==1.2.0 From 16827ea09e1942f175ff5616f76bd10fe84ea0c3 Mon Sep 17 00:00:00 2001 From: Kevin Stillhammer Date: Wed, 3 Jul 2024 15:35:08 +0200 Subject: [PATCH 1436/1445] Bump here-transit to 1.2.1 (#120900) --- homeassistant/components/here_travel_time/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/here_travel_time/manifest.json b/homeassistant/components/here_travel_time/manifest.json index 2d6621c7c61..0365cf51d97 100644 --- a/homeassistant/components/here_travel_time/manifest.json +++ b/homeassistant/components/here_travel_time/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/here_travel_time", "iot_class": "cloud_polling", "loggers": ["here_routing", "here_transit", "homeassistant.helpers.location"], - "requirements": ["here-routing==1.0.1", "here-transit==1.2.0"] + "requirements": ["here-routing==1.0.1", "here-transit==1.2.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index a04485044d5..1a6fd81ecaa 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1068,7 +1068,7 @@ heatmiserV3==1.1.18 here-routing==1.0.1 # homeassistant.components.here_travel_time -here-transit==1.2.0 +here-transit==1.2.1 # homeassistant.components.hikvisioncam hikvision==0.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2a93dd5d739..b4042f70640 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -879,7 +879,7 @@ hdate==0.10.9 here-routing==1.0.1 # homeassistant.components.here_travel_time -here-transit==1.2.0 +here-transit==1.2.1 # homeassistant.components.hko hko==0.3.2 From 36e74cd9a6afae0b3d7733b449babf4486a47c1d Mon Sep 17 00:00:00 2001 From: Anton Tolchanov <1687799+knyar@users.noreply.github.com> Date: Wed, 3 Jul 2024 14:41:01 +0100 Subject: [PATCH 1437/1445] Generate Prometheus metrics in an executor job (#121058) --- homeassistant/components/prometheus/__init__.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/prometheus/__init__.py b/homeassistant/components/prometheus/__init__.py index 2159656f129..a0f0d69ce46 100644 --- a/homeassistant/components/prometheus/__init__.py +++ b/homeassistant/components/prometheus/__init__.py @@ -26,7 +26,7 @@ from homeassistant.components.cover import ( ATTR_CURRENT_POSITION, ATTR_CURRENT_TILT_POSITION, ) -from homeassistant.components.http import HomeAssistantView +from homeassistant.components.http import KEY_HASS, HomeAssistantView from homeassistant.components.humidifier import ATTR_AVAILABLE_MODES, ATTR_HUMIDITY from homeassistant.components.light import ATTR_BRIGHTNESS from homeassistant.components.sensor import SensorDeviceClass @@ -729,7 +729,11 @@ class PrometheusView(HomeAssistantView): """Handle request for Prometheus metrics.""" _LOGGER.debug("Received Prometheus metrics request") + hass = request.app[KEY_HASS] + body = await hass.async_add_executor_job( + prometheus_client.generate_latest, prometheus_client.REGISTRY + ) return web.Response( - body=prometheus_client.generate_latest(prometheus_client.REGISTRY), + body=body, content_type=CONTENT_TYPE_TEXT_PLAIN, ) From 6621cf475a7b66f5e5a377bbbc09e5ebe1363734 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 3 Jul 2024 15:41:43 +0200 Subject: [PATCH 1438/1445] Update frontend to 20240703.0 (#121063) --- 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 0d32624cf57..525ba507121 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==20240702.0"] + "requirements": ["home-assistant-frontend==20240703.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 3ffa9d92f63..04dc11f0183 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -32,7 +32,7 @@ habluetooth==3.1.3 hass-nabucasa==0.81.1 hassil==1.7.1 home-assistant-bluetooth==1.12.2 -home-assistant-frontend==20240702.0 +home-assistant-frontend==20240703.0 home-assistant-intents==2024.6.26 httpx==0.27.0 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 1a6fd81ecaa..ee0a07a2148 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1090,7 +1090,7 @@ hole==0.8.0 holidays==0.51 # homeassistant.components.frontend -home-assistant-frontend==20240702.0 +home-assistant-frontend==20240703.0 # homeassistant.components.conversation home-assistant-intents==2024.6.26 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b4042f70640..0033bda8949 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -895,7 +895,7 @@ hole==0.8.0 holidays==0.51 # homeassistant.components.frontend -home-assistant-frontend==20240702.0 +home-assistant-frontend==20240703.0 # homeassistant.components.conversation home-assistant-intents==2024.6.26 From 13631250b460f138ef4d78eae1004378c3668b5f Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 3 Jul 2024 16:16:13 +0200 Subject: [PATCH 1439/1445] Bump axis to v62 (#121070) --- homeassistant/components/axis/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/axis/manifest.json b/homeassistant/components/axis/manifest.json index 2f057f96286..e028736f4ca 100644 --- a/homeassistant/components/axis/manifest.json +++ b/homeassistant/components/axis/manifest.json @@ -30,7 +30,7 @@ "iot_class": "local_push", "loggers": ["axis"], "quality_scale": "platinum", - "requirements": ["axis==61"], + "requirements": ["axis==62"], "ssdp": [ { "manufacturer": "AXIS" diff --git a/requirements_all.txt b/requirements_all.txt index ee0a07a2148..dbc5f480b0f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -520,7 +520,7 @@ aurorapy==0.2.7 # avion==0.10 # homeassistant.components.axis -axis==61 +axis==62 # homeassistant.components.azure_event_hub azure-eventhub==5.11.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0033bda8949..6920608e656 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -460,7 +460,7 @@ auroranoaa==0.0.3 aurorapy==0.2.7 # homeassistant.components.axis -axis==61 +axis==62 # homeassistant.components.azure_event_hub azure-eventhub==5.11.1 From c89a9b5ce04967f73674f12a3f63637c8371836e Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Wed, 3 Jul 2024 16:27:45 +0200 Subject: [PATCH 1440/1445] Bump python-matter-server to 6.2.2 (#121072) --- homeassistant/components/matter/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/matter/manifest.json b/homeassistant/components/matter/manifest.json index 8c88fcc8be2..1dac5ef0cb2 100644 --- a/homeassistant/components/matter/manifest.json +++ b/homeassistant/components/matter/manifest.json @@ -6,6 +6,6 @@ "dependencies": ["websocket_api"], "documentation": "https://www.home-assistant.io/integrations/matter", "iot_class": "local_push", - "requirements": ["python-matter-server==6.2.0b1"], + "requirements": ["python-matter-server==6.2.2"], "zeroconf": ["_matter._tcp.local.", "_matterc._udp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index dbc5f480b0f..092536e3012 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2281,7 +2281,7 @@ python-kasa[speedups]==0.7.0.2 # python-lirc==1.2.3 # homeassistant.components.matter -python-matter-server==6.2.0b1 +python-matter-server==6.2.2 # homeassistant.components.xiaomi_miio python-miio==0.5.12 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6920608e656..cf29ec479a7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1778,7 +1778,7 @@ python-juicenet==1.1.0 python-kasa[speedups]==0.7.0.2 # homeassistant.components.matter -python-matter-server==6.2.0b1 +python-matter-server==6.2.2 # homeassistant.components.xiaomi_miio python-miio==0.5.12 From e8bcb3e11eb7b212db4f76827da7418bafbf650b Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Wed, 3 Jul 2024 09:55:21 -0500 Subject: [PATCH 1441/1445] Bump intents to 2024.7.3 (#121076) --- homeassistant/components/conversation/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/conversation/manifest.json b/homeassistant/components/conversation/manifest.json index 2302d03bf4c..6eeb461d79d 100644 --- a/homeassistant/components/conversation/manifest.json +++ b/homeassistant/components/conversation/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/conversation", "integration_type": "system", "quality_scale": "internal", - "requirements": ["hassil==1.7.1", "home-assistant-intents==2024.6.26"] + "requirements": ["hassil==1.7.1", "home-assistant-intents==2024.7.3"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 04dc11f0183..6058a781e2a 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -33,7 +33,7 @@ hass-nabucasa==0.81.1 hassil==1.7.1 home-assistant-bluetooth==1.12.2 home-assistant-frontend==20240703.0 -home-assistant-intents==2024.6.26 +home-assistant-intents==2024.7.3 httpx==0.27.0 ifaddr==0.2.0 Jinja2==3.1.4 diff --git a/requirements_all.txt b/requirements_all.txt index 092536e3012..3a8edbba7a2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1093,7 +1093,7 @@ holidays==0.51 home-assistant-frontend==20240703.0 # homeassistant.components.conversation -home-assistant-intents==2024.6.26 +home-assistant-intents==2024.7.3 # homeassistant.components.home_connect homeconnect==0.7.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cf29ec479a7..a56a072d62a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -898,7 +898,7 @@ holidays==0.51 home-assistant-frontend==20240703.0 # homeassistant.components.conversation -home-assistant-intents==2024.6.26 +home-assistant-intents==2024.7.3 # homeassistant.components.home_connect homeconnect==0.7.2 From 547b24ce583f91a223ea7b44e00dce43b6eb5c79 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Wed, 3 Jul 2024 17:11:09 +0200 Subject: [PATCH 1442/1445] Bump deebot-client to 8.1.0 (#121078) --- homeassistant/components/ecovacs/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ecovacs/manifest.json b/homeassistant/components/ecovacs/manifest.json index d14291576ff..9568bf2c3ac 100644 --- a/homeassistant/components/ecovacs/manifest.json +++ b/homeassistant/components/ecovacs/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/ecovacs", "iot_class": "cloud_push", "loggers": ["sleekxmppfs", "sucks", "deebot_client"], - "requirements": ["py-sucks==0.9.10", "deebot-client==8.0.0"] + "requirements": ["py-sucks==0.9.10", "deebot-client==8.1.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 3a8edbba7a2..5fd773147dc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -709,7 +709,7 @@ debugpy==1.8.1 # decora==0.6 # homeassistant.components.ecovacs -deebot-client==8.0.0 +deebot-client==8.1.0 # homeassistant.components.ihc # homeassistant.components.namecheapdns diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a56a072d62a..d5c4945da95 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -590,7 +590,7 @@ dbus-fast==2.22.1 debugpy==1.8.1 # homeassistant.components.ecovacs -deebot-client==8.0.0 +deebot-client==8.1.0 # homeassistant.components.ihc # homeassistant.components.namecheapdns From 85168239cdcfa8f3fcb04812f30e495c99ce3ff8 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Wed, 3 Jul 2024 17:16:51 +0200 Subject: [PATCH 1443/1445] Matter fix Energy sensor discovery schemas (#121080) --- homeassistant/components/matter/sensor.py | 98 +++++++++++++++++++++-- 1 file changed, 91 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/matter/sensor.py b/homeassistant/components/matter/sensor.py index d91d4d33471..9c19be7ee08 100644 --- a/homeassistant/components/matter/sensor.py +++ b/homeassistant/components/matter/sensor.py @@ -6,7 +6,11 @@ from dataclasses import dataclass from chip.clusters import Objects as clusters from chip.clusters.Types import Nullable, NullValue -from matter_server.common.custom_clusters import EveCluster +from matter_server.common.custom_clusters import ( + EveCluster, + NeoCluster, + ThirdRealityMeteringCluster, +) from homeassistant.components.sensor import ( SensorDeviceClass, @@ -171,9 +175,6 @@ DISCOVERY_SCHEMAS = [ ), entity_class=MatterSensor, required_attributes=(EveCluster.Attributes.Watt,), - # Add OnOff Attribute as optional attribute to poll - # the primary value when the relay is toggled - optional_attributes=(clusters.OnOff.Attributes.OnOff,), ), MatterDiscoverySchema( platform=Platform.SENSOR, @@ -213,9 +214,6 @@ DISCOVERY_SCHEMAS = [ ), entity_class=MatterSensor, required_attributes=(EveCluster.Attributes.Current,), - # Add OnOff Attribute as optional attribute to poll - # the primary value when the relay is toggled - optional_attributes=(clusters.OnOff.Attributes.OnOff,), ), MatterDiscoverySchema( platform=Platform.SENSOR, @@ -364,4 +362,90 @@ DISCOVERY_SCHEMAS = [ clusters.ActivatedCarbonFilterMonitoring.Attributes.Condition, ), ), + MatterDiscoverySchema( + platform=Platform.SENSOR, + entity_description=MatterSensorEntityDescription( + key="ThirdRealityEnergySensorWatt", + device_class=SensorDeviceClass.POWER, + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=UnitOfPower.WATT, + suggested_display_precision=2, + state_class=SensorStateClass.MEASUREMENT, + measurement_to_ha=lambda x: x / 1000, + ), + entity_class=MatterSensor, + required_attributes=( + ThirdRealityMeteringCluster.Attributes.InstantaneousDemand, + ), + ), + MatterDiscoverySchema( + platform=Platform.SENSOR, + entity_description=MatterSensorEntityDescription( + key="ThirdRealityEnergySensorWattAccumulated", + device_class=SensorDeviceClass.ENERGY, + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + suggested_display_precision=3, + state_class=SensorStateClass.TOTAL_INCREASING, + measurement_to_ha=lambda x: x / 1000, + ), + entity_class=MatterSensor, + required_attributes=( + ThirdRealityMeteringCluster.Attributes.CurrentSummationDelivered, + ), + ), + MatterDiscoverySchema( + platform=Platform.SENSOR, + entity_description=MatterSensorEntityDescription( + key="NeoEnergySensorWatt", + device_class=SensorDeviceClass.POWER, + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=UnitOfPower.WATT, + suggested_display_precision=2, + state_class=SensorStateClass.MEASUREMENT, + measurement_to_ha=lambda x: x / 10, + ), + entity_class=MatterSensor, + required_attributes=(NeoCluster.Attributes.Watt,), + ), + MatterDiscoverySchema( + platform=Platform.SENSOR, + entity_description=MatterSensorEntityDescription( + key="NeoEnergySensorWattAccumulated", + device_class=SensorDeviceClass.ENERGY, + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, + suggested_display_precision=1, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + entity_class=MatterSensor, + required_attributes=(NeoCluster.Attributes.WattAccumulated,), + ), + MatterDiscoverySchema( + platform=Platform.SENSOR, + entity_description=MatterSensorEntityDescription( + key="NeoEnergySensorVoltage", + device_class=SensorDeviceClass.VOLTAGE, + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=UnitOfElectricPotential.VOLT, + suggested_display_precision=0, + state_class=SensorStateClass.MEASUREMENT, + measurement_to_ha=lambda x: x / 10, + ), + entity_class=MatterSensor, + required_attributes=(NeoCluster.Attributes.Voltage,), + ), + MatterDiscoverySchema( + platform=Platform.SENSOR, + entity_description=MatterSensorEntityDescription( + key="NeoEnergySensorWattCurrent", + device_class=SensorDeviceClass.CURRENT, + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=UnitOfElectricCurrent.MILLIAMPERE, + suggested_display_precision=0, + state_class=SensorStateClass.MEASUREMENT, + ), + entity_class=MatterSensor, + required_attributes=(NeoCluster.Attributes.Current,), + ), ] From d94b36cfbbf486f3d873cc55aeb9b8411bdbb2be Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 3 Jul 2024 17:29:08 +0200 Subject: [PATCH 1444/1445] Bump version to 2024.7.0b11 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index b6800b44063..c76db961e8b 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -24,7 +24,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 7 -PATCH_VERSION: Final = "0b10" +PATCH_VERSION: Final = "0b11" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0) diff --git a/pyproject.toml b/pyproject.toml index 36ca9abe1b5..198d3438fc5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.7.0b10" +version = "2024.7.0b11" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 1080a4ef1e99a5f567f4a2b5aca058b84acf7352 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 3 Jul 2024 17:55:58 +0200 Subject: [PATCH 1445/1445] Bump version to 2024.7.0 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index c76db961e8b..d5c64823890 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -24,7 +24,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 7 -PATCH_VERSION: Final = "0b11" +PATCH_VERSION: Final = "0" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0) diff --git a/pyproject.toml b/pyproject.toml index 198d3438fc5..777ec8bb6a4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.7.0b11" +version = "2024.7.0" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst"